diff --git a/.cargo/config b/.cargo/config deleted file mode 100644 index d9e675b5bd..0000000000 --- a/.cargo/config +++ /dev/null @@ -1,29 +0,0 @@ -[alias] -test-gen-llvm = "test -p test_gen" -test-gen-dev = "test -p roc_gen_dev -p test_gen --no-default-features --features gen-dev" -test-gen-wasm = "test -p roc_gen_wasm -p test_gen --no-default-features --features gen-wasm" - -[target.wasm32-unknown-unknown] -# Rust compiler flags for minimum-sized .wasm binary in the web REPL -# opt-level=s Optimizations should focus more on size than speed -# lto=fat Spend extra effort on link-time optimization across crates -rustflags = ["-Copt-level=s", "-Clto=fat"] - -[env] -# Gives us the path of the workspace root for use in cargo tests without having -# to compute it per-package. -# https://github.com/rust-lang/cargo/issues/3946#issuecomment-973132993 -ROC_WORKSPACE_DIR = { value = "", relative = true } - -# Debug flags. Keep this up-to-date with compiler/debug_flags/src/lib.rs. -# Set = "1" to turn a debug flag on. -ROC_PRETTY_PRINT_ALIAS_CONTENTS = "0" -ROC_PRINT_UNIFICATIONS = "0" -ROC_PRINT_MISMATCHES = "0" -ROC_VERIFY_RIGID_LET_GENERALIZED = "0" -ROC_PRINT_IR_AFTER_SPECIALIZATION = "0" -ROC_PRINT_IR_AFTER_RESET_REUSE = "0" -ROC_PRINT_IR_AFTER_REFCOUNT = "0" -ROC_DEBUG_ALIAS_ANALYSIS = "0" -ROC_PRINT_LLVM_FN_VERIFICATION = "0" -ROC_PRINT_LOAD_LOG = "0" \ No newline at end of file diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000000..fa5d15fac5 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,50 @@ +[alias] +test-gen-llvm = "test -p test_gen" +test-gen-dev = "test -p roc_gen_dev -p test_gen --no-default-features --features gen-dev" +test-gen-wasm = "test -p roc_gen_wasm -p test_gen --no-default-features --features gen-wasm" +test-gen-llvm-wasm = "test -p roc_gen_wasm -p test_gen --no-default-features --features gen-llvm-wasm" + +nextest-gen-llvm = "nextest run -p test_gen" +nextest-gen-dev = "nextest run -p roc_gen_dev -p test_gen --no-default-features --features gen-dev" +nextest-gen-wasm = "nextest run -p roc_gen_wasm -p test_gen --no-default-features --features gen-wasm" +nextest-gen-llvm-wasm = "nextest run -p roc_gen_wasm -p test_gen --no-default-features --features gen-llvm-wasm" + +uitest = "test -p uitest" + +[target.wasm32-unknown-unknown] +# Rust compiler flags for minimum-sized .wasm binary in the web REPL +# opt-level=s Optimizations should focus more on size than speed +# lto=fat Spend extra effort on link-time optimization across crates +# embed-bitcode=yes Turn back on lto since it is no longer default +rustflags = ["-Copt-level=s", "-Clto=fat", "-Cembed-bitcode=yes"] + +# TODO: there is probably a more proper solution to this. +# We are pulling in roc_alloc and friends due to using roc_std. +# They ared defined in roc_glue, but windows linking breaks before we get there. +[target.'cfg(target_os = "windows")'] +rustflags = ["-Clink-args=/FORCE:UNRESOLVED"] + +[env] +# Gives us the path of the workspace root for use in cargo tests without having +# to compute it per-package. +# https://github.com/rust-lang/cargo/issues/3946#issuecomment-973132993 +ROC_WORKSPACE_DIR = { value = "", relative = true } + +# Debug flags. Keep this up-to-date with compiler/debug_flags/src/lib.rs. +# Set = "1" to turn a debug flag on. +ROC_PRETTY_PRINT_ALIAS_CONTENTS = "0" +ROC_PRINT_UNIFICATIONS = "0" +ROC_PRINT_UNDERIVABLE = "0" +ROC_TRACE_COMPACTION = "0" +ROC_PRINT_UNIFICATIONS_DERIVED = "0" +ROC_PRINT_MISMATCHES = "0" +ROC_VERIFY_RIGID_LET_GENERALIZED = "0" +ROC_CHECK_MONO_IR = "0" +ROC_PRINT_IR_AFTER_SPECIALIZATION = "0" +ROC_PRINT_IR_AFTER_RESET_REUSE = "0" +ROC_PRINT_IR_AFTER_DROP_SPECIALIZATION = "0" +ROC_PRINT_IR_AFTER_REFCOUNT = "0" +ROC_PRINT_RUNTIME_ERROR_GEN = "0" +ROC_DEBUG_ALIAS_ANALYSIS = "0" +ROC_PRINT_LLVM_FN_VERIFICATION = "0" +ROC_PRINT_LOAD_LOG = "0" diff --git a/.earthignore b/.earthignore deleted file mode 100644 index a291dd2885..0000000000 --- a/.earthignore +++ /dev/null @@ -1,4 +0,0 @@ -AUTHORS -nix -.envrc -.gitignore \ No newline at end of file diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000..f15056f5a7 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Require roc files to be checlked out with Unix line endings, even on windows +*.roc text eol=lf diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000000..fcaee54cc4 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,3 @@ +# These are supported funding model platforms + +github: roc-lang diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000000..c2ab52d433 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,11 @@ +version: 2 +updates: + - package-ecosystem: "cargo" + directory: "/" + schedule: + interval: "weekly" + day: "monday" + time: "07:00" + timezone: "Europe/Brussels" + # Disable all version updates, only critical security updates will be submitted + open-pull-requests-limit: 0 diff --git a/.github/workflows/basic_cli_build_release.yml b/.github/workflows/basic_cli_build_release.yml new file mode 100644 index 0000000000..fa2176db1f --- /dev/null +++ b/.github/workflows/basic_cli_build_release.yml @@ -0,0 +1,218 @@ +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 + +env: + # use .tar.gz for quick testing + ARCHIVE_FORMAT: .tar.gz + BASIC_CLI_BRANCH: main + +jobs: + fetch-releases: + runs-on: [ubuntu-20.04] + steps: + - uses: actions/checkout@v3 + + - run: curl -fOL https://github.com/roc-lang/roc/releases/download/nightly/roc_nightly-linux_x86_64-latest.tar.gz + - run: curl -fOL https://github.com/roc-lang/roc/releases/download/nightly/roc_nightly-linux_arm64-latest.tar.gz + - run: curl -fOL https://github.com/roc-lang/roc/releases/download/nightly/roc_nightly-macos_x86_64-latest.tar.gz + - run: curl -fOL https://github.com/roc-lang/roc/releases/download/nightly/roc_nightly-macos_apple_silicon-latest.tar.gz + + - name: Save roc_nightly archives + uses: actions/upload-artifact@v3 + with: + path: roc_nightly-* + + build-linux-x86_64-files: + runs-on: [ubuntu-20.04] + needs: [fetch-releases] + steps: + - uses: actions/checkout@v3 + + - name: Download the previously uploaded roc_nightly archives + uses: actions/download-artifact@v3 + + - name: build basic-cli with surgical linker and also with legacy linker + env: + CARGO_BUILD_TARGET: x86_64-unknown-linux-musl + run: ./ci/build_basic_cli.sh linux_x86_64 "--linker legacy" + + - name: Save .rh, .rm and .o file + uses: actions/upload-artifact@v3 + with: + name: linux-x86_64-files + path: | + basic-cli/src/metadata_linux-x64.rm + basic-cli/src/linux-x64.rh + basic-cli/src/linux-x64.o + + + build-linux-arm64-files: + runs-on: [self-hosted, Linux, ARM64] + needs: [fetch-releases] + steps: + - uses: actions/checkout@v3 + + - name: Download the previously uploaded roc_nightly archives + uses: actions/download-artifact@v3 + + - name: build basic-cli + env: + CARGO_BUILD_TARGET: aarch64-unknown-linux-musl + CC_aarch64_unknown_linux_musl: clang-16 + AR_aarch64_unknown_linux_musl: llvm-ar-16 + CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_RUSTFLAGS: "-Clink-self-contained=yes -Clinker=rust-lld" + run: ./ci/build_basic_cli.sh linux_arm64 + + - name: Save .o file + uses: actions/upload-artifact@v3 + with: + name: linux-arm64-files + path: | + basic-cli/src/linux-arm64.o + + build-macos-x86_64-files: + runs-on: [macos-11] # I expect the generated files to work on macOS 12 and up + needs: [fetch-releases] + steps: + - uses: actions/checkout@v3 + + - name: Download the previously uploaded roc_nightly archives + uses: actions/download-artifact@v3 + + - run: ./ci/build_basic_cli.sh macos_x86_64 + + - name: Save .o files + uses: actions/upload-artifact@v3 + with: + name: macos-x86_64-files + path: | + basic-cli/src/macos-x64.o + + build-macos-apple-silicon-files: + name: build apple silicon .o file + runs-on: [self-hosted, macOS, ARM64] + needs: [fetch-releases] + steps: + - uses: actions/checkout@v3 + + - name: Download the previously uploaded roc_nightly archives + uses: actions/download-artifact@v3 + + - run: ./ci/build_basic_cli.sh macos_apple_silicon + + - name: Save macos-arm64.o file + uses: actions/upload-artifact@v3 + with: + name: macos-apple-silicon-files + path: | + basic-cli/src/macos-arm64.o + + create-release-archive: + needs: [build-linux-x86_64-files, build-linux-arm64-files, build-macos-x86_64-files, build-macos-apple-silicon-files] + name: create release archive + runs-on: [ubuntu-20.04] + steps: + - uses: actions/checkout@v3 + + - name: remove all folders except the ci folder + run: ls | grep -v ci | xargs rm -rf + + - name: Download the previously uploaded files + uses: actions/download-artifact@v3 + + - name: mv roc nightly and simplify name + run: mv $(ls -d artifact/* | grep "roc_nightly.*tar\.gz" | grep "linux_x86_64") ./roc_nightly.tar.gz + + - name: decompress the tar + run: tar -xzvf roc_nightly.tar.gz + + - name: delete tar + run: rm roc_nightly.tar.gz + + - name: rename nightly folder + run: mv roc_nightly* roc_nightly + + - run: git clone https://github.com/roc-lang/basic-cli.git + + - run: cp macos-apple-silicon-files/* ./basic-cli/src + + - run: cp linux-x86_64-files/* ./basic-cli/src + + - run: cp linux-arm64-files/* ./basic-cli/src + + - run: cp macos-x86_64-files/* ./basic-cli/src + + - run: ./roc_nightly/roc build --bundle=${{ env.ARCHIVE_FORMAT }} ./basic-cli/src/main.roc + + - run: echo "TAR_FILENAME=$(ls -d basic-cli/src/* | grep ${{ env.ARCHIVE_FORMAT }})" >> $GITHUB_ENV + + - name: Upload platform archive + uses: actions/upload-artifact@v3 + with: + name: basic-cli-platform + path: | + ${{ env.TAR_FILENAME }} + + test-release-ubuntu: + needs: [create-release-archive] + runs-on: [ubuntu-20.04] + steps: + + - name: Download the previously uploaded files + uses: actions/download-artifact@v3 + + - name: mv roc nightly and simplify name + run: mv $(ls -d artifact/* | grep "roc_nightly.*tar\.gz" | grep "linux_x86_64") ./roc_nightly.tar.gz + + - name: decompress the tar + run: tar -xzvf roc_nightly.tar.gz + + - name: delete tar + run: rm roc_nightly.tar.gz + + - name: rename nightly folder + run: mv roc_nightly* roc_nightly + + - if: contains(env.ARCHIVE_FORMAT, 'gz') + run: | + cd basic-cli-platform && ls | grep "tar" | xargs tar -xzf + + - if: contains(env.ARCHIVE_FORMAT, 'br') + run: | + cd basic-cli-platform && ls | grep "tar" | xargs brotli -d + ls | grep "tar$" | xargs tar -xf + + - name: Install expect for tests if we dont have it yet + run: if ! dpkg -l | grep -qw expect; then sudo apt install -y expect; fi + + - name: Install ncat for tests if we dont have it yet + run: if ! dpkg -l | grep -qw ncat; then sudo apt install -y ncat; fi + + - name: prep testing + run: | + mv roc_nightly basic-cli-platform/. + cd basic-cli-platform + mkdir src + find . -maxdepth 1 -type f -exec mv {} src/ \; + + mkdir temp-basic-cli + cd temp-basic-cli + git clone https://github.com/roc-lang/basic-cli.git + cd basic-cli + git checkout ${{ env.BASIC_CLI_BRANCH }} + cp -r examples ../.. + cp -r ci ../.. + cp -r LICENSE ../.. + # LICENSE is necessary for command test + + - name: run tests + run: | + cd basic-cli-platform + ROC=./roc_nightly/roc EXAMPLES_DIR=./examples/ ROC_BUILD_FLAGS=--prebuilt-platform ./ci/all_tests.sh + diff --git a/.github/workflows/basic_cli_test_arm64.yml b/.github/workflows/basic_cli_test_arm64.yml new file mode 100644 index 0000000000..4ea02715c3 --- /dev/null +++ b/.github/workflows/basic_cli_test_arm64.yml @@ -0,0 +1,59 @@ +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 + +jobs: + test-basic-cli-release-arm64: + runs-on: [self-hosted, Linux, ARM64] + steps: + - name: clone basic-cli repo + uses: actions/checkout@v3 + with: + repository: roc-lang/basic-cli + ref: main + + - name: get latest roc nightly + run: | + curl -fOL https://github.com/roc-lang/roc/releases/download/nightly/roc_nightly-linux_arm64-latest.tar.gz + + - name: rename nightly tar + run: mv $(ls | grep "roc_nightly.*tar\.gz") roc_nightly.tar.gz + + - name: decompress the tar + run: tar -xzf roc_nightly.tar.gz + + - run: rm roc_nightly.tar.gz + + - name: simplify nightly folder name + run: mv roc_nightly* roc_nightly + + - run: ./roc_nightly/roc version + + - run: expect -v + + # Run all tests + - run: ROC=./roc_nightly/roc EXAMPLES_DIR=./examples/ ./ci/all_tests.sh + + ###### + # Now test the latest basic-cli release, not the main branch + ###### + + - name: Remove roc_nightly folder to keep things simple (we'll download it again later) + run: rm -rf roc_nightly + + - name: Get the repo of the latest basic-cli release + run: | + git clone --depth 1 https://github.com/roc-lang/basic-cli + cd basic-cli + git fetch --tags + latestTag=$(git describe --tags $(git rev-list --tags --max-count=1)) + git checkout $latestTag + + - name: Run all tests with latest roc nightly and latest basic-cli release + run: | + sed -i 's/x86_64/arm64/g' ./ci/test_latest_release.sh + ROC=./roc_nightly/roc EXAMPLES_DIR=./basic-cli/examples/ ./ci/test_latest_release.sh diff --git a/.github/workflows/basic_webserver_build_release.yml b/.github/workflows/basic_webserver_build_release.yml new file mode 100644 index 0000000000..aac03696b0 --- /dev/null +++ b/.github/workflows/basic_webserver_build_release.yml @@ -0,0 +1,164 @@ +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 + +env: + # use .tar.gz for quick testing + ARCHIVE_FORMAT: .tar.br + BASIC_WEBSERVER_BRANCH: main + +jobs: + fetch-releases: + runs-on: [ubuntu-20.04] + steps: + - uses: actions/checkout@v3 + + - run: curl -fOL https://github.com/roc-lang/roc/releases/download/nightly/roc_nightly-linux_x86_64-latest.tar.gz + - run: curl -fOL https://github.com/roc-lang/roc/releases/download/nightly/roc_nightly-linux_arm64-latest.tar.gz + - run: curl -fOL https://github.com/roc-lang/roc/releases/download/nightly/roc_nightly-macos_x86_64-latest.tar.gz + - run: curl -fOL https://github.com/roc-lang/roc/releases/download/nightly/roc_nightly-macos_apple_silicon-latest.tar.gz + + - name: Save roc_nightly archives + uses: actions/upload-artifact@v3 + with: + path: roc_nightly-* + + build-linux-x86_64-files: + runs-on: [ubuntu-20.04] + needs: [fetch-releases] + steps: + - uses: actions/checkout@v3 + + - name: Download the previously uploaded roc_nightly archives + uses: actions/download-artifact@v3 + + - name: build basic-webserver with legacy linker + env: + CARGO_BUILD_TARGET: x86_64-unknown-linux-musl + run: ./ci/build_basic_webserver.sh linux_x86_64 "--linker legacy" + + - name: Save .rh, .rm and .o file + uses: actions/upload-artifact@v3 + with: + name: linux-x86_64-files + path: | + basic-webserver/platform/metadata_linux-x64.rm + basic-webserver/platform/linux-x64.rh + basic-webserver/platform/linux-x64.o + + + build-linux-arm64-files: + runs-on: [self-hosted, Linux, ARM64] + needs: [fetch-releases] + steps: + - uses: actions/checkout@v3 + + - name: Download the previously uploaded roc_nightly archives + uses: actions/download-artifact@v3 + + - name: build basic-webserver + env: + CARGO_BUILD_TARGET: aarch64-unknown-linux-musl + CC_aarch64_unknown_linux_musl: clang-16 + AR_aarch64_unknown_linux_musl: llvm-ar-16 + CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_RUSTFLAGS: "-Clink-self-contained=yes -Clinker=rust-lld" + run: ./ci/build_basic_webserver.sh linux_arm64 + + - name: Save .o file + uses: actions/upload-artifact@v3 + with: + name: linux-arm64-files + path: | + basic-webserver/platform/linux-arm64.o + + build-macos-x86_64-files: + runs-on: [macos-11] # I expect the generated files to work on macOS 12 and 13 + needs: [fetch-releases] + steps: + - uses: actions/checkout@v3 + + - name: Download the previously uploaded roc_nightly archives + uses: actions/download-artifact@v3 + + - run: ./ci/build_basic_webserver.sh macos_x86_64 + + - name: Save .o files + uses: actions/upload-artifact@v3 + with: + name: macos-x86_64-files + path: | + basic-webserver/platform/macos-x64.o + + build-macos-apple-silicon-files: + name: build apple silicon .o file + runs-on: [self-hosted, macOS, ARM64] + needs: [fetch-releases] + steps: + - uses: actions/checkout@v3 + + - name: Download the previously uploaded roc_nightly archives + uses: actions/download-artifact@v3 + + - run: ./ci/build_basic_webserver.sh macos_apple_silicon + + - name: Save macos-arm64.o file + uses: actions/upload-artifact@v3 + with: + name: macos-apple-silicon-files + path: | + basic-webserver/platform/macos-arm64.o + + create-release-archive: + needs: [build-linux-x86_64-files, build-linux-arm64-files, build-macos-x86_64-files, build-macos-apple-silicon-files] + name: create release archive + runs-on: [ubuntu-20.04] + steps: + - uses: actions/checkout@v3 + + - name: remove all folders except the ci folder + run: ls | grep -v ci | xargs rm -rf + + - name: Download the previously uploaded files + uses: actions/download-artifact@v3 + + - name: mv roc nightly and simplify name + run: mv $(ls -d artifact/* | grep "roc_nightly.*tar\.gz" | grep "linux_x86_64") ./roc_nightly.tar.gz + + - name: decompress the tar + run: tar -xzvf roc_nightly.tar.gz + + - name: delete tar + run: rm roc_nightly.tar.gz + + - name: rename nightly folder + run: mv roc_nightly* roc_nightly + + - run: | + git clone https://github.com/roc-lang/basic-webserver.git + cd basic-webserver + git checkout ${{ env.BASIC_WEBSERVER_BRANCH }} + cd .. + + - run: cp macos-apple-silicon-files/* ./basic-webserver/platform + + - run: cp linux-x86_64-files/* ./basic-webserver/platform + + - run: cp linux-arm64-files/* ./basic-webserver/platform + + - run: cp macos-x86_64-files/* ./basic-webserver/platform + + - run: ./roc_nightly/roc build --bundle=${{ env.ARCHIVE_FORMAT }} ./basic-webserver/platform/main.roc + + - run: echo "TAR_FILENAME=$(ls -d basic-webserver/platform/* | grep ${{ env.ARCHIVE_FORMAT }})" >> $GITHUB_ENV + + - name: Upload platform archive + uses: actions/upload-artifact@v3 + with: + name: basic-webserver-platform + path: | + ${{ env.TAR_FILENAME }} diff --git a/.github/workflows/benchmarks.yml b/.github/workflows/benchmarks.yml index dbc7005e80..381477e925 100644 --- a/.github/workflows/benchmarks.yml +++ b/.github/workflows/benchmarks.yml @@ -1,11 +1,8 @@ -on: [pull_request] +on: + workflow_call: name: Benchmarks -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - env: RUST_BACKTRACE: 1 ROC_NUM_WORKERS: 1 @@ -18,30 +15,23 @@ jobs: env: FORCE_COLOR: 1 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 with: - ref: "trunk" + ref: "main" clean: "true" - - name: Earthly version - run: earthly --version + - name: on main; prepare a self-contained benchmark folder + run: nix develop -c ./ci/benchmarks/prep_folder.sh main - - name: on trunk; prepare a self-contained benchmark folder - run: ./ci/safe-earthly.sh --build-arg BENCH_SUFFIX=trunk +prep-bench-folder - - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 with: clean: "false" # we want to keep the benchmark folder - name: on current branch; prepare a self-contained benchmark folder - run: ./ci/safe-earthly.sh +prep-bench-folder - - - uses: actions-rs/toolchain@v1 - with: - toolchain: stable + run: nix develop -c ./ci/benchmarks/prep_folder.sh branch - name: build benchmark runner - run: cd ci/bench-runner && cargo build --release && cd ../.. + run: nix develop -c bash -c "cd ci/benchmarks/bench-runner && cargo build --release && cd ../../.." - name: run benchmarks with regression check - run: ./ci/bench-runner/target/release/bench-runner --check-executables-changed + run: nix develop -c ./ci/benchmarks/bench-runner/target/release/bench-runner --check-executables-changed diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index 845a8e9425..0000000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,28 +0,0 @@ -on: [pull_request] - -name: CI - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -env: - RUST_BACKTRACE: 1 - -jobs: - build-fmt-clippy-test: - name: fmt, clippy, test --release - runs-on: [self-hosted, i5-4690K] - timeout-minutes: 90 - env: - FORCE_COLOR: 1 - steps: - - uses: actions/checkout@v2 - with: - clean: "true" - - - name: Earthly version - run: earthly --version - - - name: install dependencies, build, run zig tests, rustfmt, clippy, cargo test --release - run: ./ci/safe-earthly.sh +test-all diff --git a/.github/workflows/ci_cleanup.yml b/.github/workflows/ci_cleanup.yml new file mode 100644 index 0000000000..80e3f9aecd --- /dev/null +++ b/.github/workflows/ci_cleanup.yml @@ -0,0 +1,56 @@ +on: + workflow_dispatch: + schedule: + - cron: '0 5 * * 1' + +name: Garbage collect nix store + +jobs: + clean-big-ci: + runs-on: [self-hosted, i7-6700K] + timeout-minutes: 120 + steps: + - name: Clean up nix store + run: nix-store --gc + + clean-small-ci: + runs-on: [self-hosted, i5-4690K] + timeout-minutes: 120 + steps: + - name: Clean up nix store + run: nix-store --gc + + clean-mac-mini-arm64: + runs-on: [self-hosted, macOS, ARM64] + timeout-minutes: 120 + steps: + - name: Clean up nix store + run: nix-store --gc + + - name: Clean up nix shells + run: rm -rf /private/tmp/nix-shell.* + + clean-rpi-1: + runs-on: [self-hosted, Linux, ARM64] + timeout-minutes: 120 + steps: + - name: Clean up nix store + run: nix-store --gc + + clean-rpi-2: + runs-on: [self-hosted, Linux, ARM64] + timeout-minutes: 120 + steps: + - name: Clean up nix store + run: nix-store --gc + + clean-mac-mini-x86-64: + runs-on: [self-hosted, macOS, X64] + timeout-minutes: 120 + steps: + - name: Clean up nix store + run: nix-store --gc + + - name: Clean up temp roc binaries + run: find /private/var/folders/hq -type f -name "roc_app_binary" -exec rm {} \; || true + diff --git a/.github/workflows/ci_cleanup_nix_mac.yml b/.github/workflows/ci_cleanup_nix_mac.yml new file mode 100644 index 0000000000..cde4f24580 --- /dev/null +++ b/.github/workflows/ci_cleanup_nix_mac.yml @@ -0,0 +1,17 @@ +on: + workflow_dispatch: + schedule: + - cron: '0 5 * * *' + +name: Clean up nix on mac mini m1 + +jobs: + clean-mac-mini-arm64: + runs-on: [self-hosted, macOS, ARM64] + timeout-minutes: 120 + steps: + - name: Clean up nix store + run: nix-store --gc + + - name: Clean up old nix shells + run: rm -rf /private/tmp/nix-shell.* diff --git a/.github/workflows/ci_manager.yml b/.github/workflows/ci_manager.yml new file mode 100644 index 0000000000..5a2e0419f0 --- /dev/null +++ b/.github/workflows/ci_manager.yml @@ -0,0 +1,114 @@ +on: + pull_request: + +name: CI manager + +# cancel current runs when a new commit is pushed +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + check-changes: + runs-on: ubuntu-22.04 + outputs: + run_tests: ${{ steps.filecheck.outputs.run_tests }} + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Check if only css, html or md files changed + id: filecheck + run: | + git fetch origin ${{ github.base_ref }} + if git diff --name-only origin/${{ github.base_ref }} HEAD | grep -qvE '(\.md$|\.css$|\.html$)'; then + echo "run_tests=full" >> $GITHUB_OUTPUT + else + echo "run_tests=none" >> $GITHUB_OUTPUT + fi + + - run: echo "debug output ${{ steps.filecheck.outputs.run_tests }}" + + start-nix-linux-x86-64-tests: + needs: check-changes + if: needs.check-changes.outputs.run_tests == 'full' + uses: ./.github/workflows/nix_linux_x86_64.yml + + start-nix-linux-aarch64-build-default-test: + needs: check-changes + if: needs.check-changes.outputs.run_tests == 'full' + uses: ./.github/workflows/nix_linux_arm64_default.yml + + start-nix-linux-aarch64-cargo-build-test: + needs: check-changes + if: needs.check-changes.outputs.run_tests == 'full' + uses: ./.github/workflows/nix_linux_arm64_cargo.yml + + start-nix-macos-apple-silicon-tests: + needs: check-changes + if: needs.check-changes.outputs.run_tests == 'full' + uses: ./.github/workflows/nix_macos_apple_silicon.yml + + start-macos-x86-64-tests: + needs: check-changes + if: needs.check-changes.outputs.run_tests == 'full' + uses: ./.github/workflows/ubuntu_x86_64.yml + + start-ubuntu-x86-64-tests: + needs: check-changes + if: needs.check-changes.outputs.run_tests == 'full' + uses: ./.github/workflows/ubuntu_x86_64.yml + + start-windows-release-build-test: + needs: check-changes + if: needs.check-changes.outputs.run_tests == 'full' + uses: ./.github/workflows/windows_release_build.yml + + start-windows-tests: + needs: check-changes + if: needs.check-changes.outputs.run_tests == 'full' + uses: ./.github/workflows/windows_tests.yml + + start-roc-benchmarks: + needs: check-changes + if: needs.check-changes.outputs.run_tests == 'full' + uses: ./.github/workflows/benchmarks.yml + + ran-full: + runs-on: ubuntu-22.04 + needs: [ + start-nix-linux-x86-64-tests, + start-nix-linux-aarch64-build-default-test, + start-nix-linux-aarch64-cargo-build-test, + start-nix-macos-apple-silicon-tests, + start-macos-x86-64-tests, + start-ubuntu-x86-64-tests, + start-windows-release-build-test, + start-windows-tests, + start-roc-benchmarks + ] + steps: + - run: echo "all workflows succeeded!" + + ran-none: + runs-on: ubuntu-22.04 + needs: [check-changes] + if: needs.check-changes.outputs.run_tests == 'none' + steps: + - run: echo "Only non-code files changed. CI manager did not run any workflows." + + # we need a single end job for the required checks under branch protection rules + finish: + runs-on: ubuntu-22.04 + needs: [ran-full, ran-none] + if: | + always() + && contains(needs.*.result, 'success') + && !contains(needs.*.result, 'failure') + && !contains(needs.*.result, 'cancelled') + && !(needs.ran-full.result == 'skipped' && needs.ran-none.result == 'skipped') + steps: + - run: echo "Workflow succeeded :)" + + + diff --git a/.github/workflows/devtools_test_linux_x86_64.yml b/.github/workflows/devtools_test_linux_x86_64.yml new file mode 100644 index 0000000000..7aa613b8e8 --- /dev/null +++ b/.github/workflows/devtools_test_linux_x86_64.yml @@ -0,0 +1,58 @@ +on: + pull_request: + +name: devtools nix files test - linux + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + devtools-test-linux: + name: devtools-test-linux + runs-on: [ubuntu-20.04] + timeout-minutes: 120 + steps: + - uses: actions/checkout@v3 + + - name: Only run all steps if a nix file changed + run: | + git fetch origin ${{ github.base_ref }} + if git diff --name-only origin/${{ github.base_ref }} HEAD | grep 'nix'; then + echo "A nix file was changed. Testing devtools nix files..." + echo "nix_changed=true" >> $GITHUB_ENV + else + echo "A nix file was changed. No need to run tests." + echo "nix_changed=false" >> $GITHUB_ENV + fi + + + - uses: cachix/install-nix-action@v23 + if: env.nix_changed == 'true' + with: + nix_path: nixpkgs=channel:nixos-unstable + + - name: test devtools/flake.nix + if: env.nix_changed == 'true' + id: devtools_test_step + run: | + sed -i "s|/home/username/gitrepos/roc|$(realpath .)|g" devtools/flake.nix + cat devtools/flake.nix + mkdir -p ../temp + cp devtools/flake.nix ../temp + cp devtools/flake.lock ../temp + cd ../temp + git init + git add flake.nix flake.lock + nix develop + + - name: Print tip on fail + if: steps.devtools_test_step.outcome == 'failure' + run: | + echo "The devtools test failed, this can likely be fixed by" + echo "locally deleting devtools/flake.lock and following the" + echo "instructions in devtools/README.md. This will create a" + echo "new flake.lock you should use to replace the old devtools/flake.lock" + + + diff --git a/.github/workflows/devtools_test_macos_apple_silicon.yml b/.github/workflows/devtools_test_macos_apple_silicon.yml new file mode 100644 index 0000000000..2abed8eac4 --- /dev/null +++ b/.github/workflows/devtools_test_macos_apple_silicon.yml @@ -0,0 +1,52 @@ +on: + pull_request: + +name: devtools nix files test - macos + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + devtools-test-macos: + name: devtools-test-mac + runs-on: [self-hosted, macOS, ARM64] + timeout-minutes: 120 + steps: + - uses: actions/checkout@v3 + + - name: Only run all steps if a nix file changed + run: | + git fetch origin ${{ github.base_ref }} + if git diff --name-only origin/${{ github.base_ref }} HEAD | grep 'nix'; then + echo "A nix file was changed. Testing devtools nix files..." + echo "nix_changed=true" >> $GITHUB_ENV + else + echo "A nix file was changed. No need to run tests." + echo "nix_changed=false" >> $GITHUB_ENV + fi + + - name: test devtools/flake.nix + if: env.nix_changed == 'true' + id: devtools_test_step + run: | + sed -i '' "s|/home/username/gitrepos/roc|$(realpath .)|g" devtools/flake.nix + cat devtools/flake.nix + mkdir -p ../temp + cp devtools/flake.nix ../temp + cp devtools/flake.lock ../temp + cd ../temp + git init + git add flake.nix flake.lock + nix develop --show-trace + + - name: Print tip on fail + if: steps.devtools_test_step.outcome == 'failure' + run: | + echo "The devtools test failed, this can likely be fixed by" + echo "locally deleting devtools/flake.lock and following the" + echo "instructions in devtools/README.md. This will create a" + echo "new flake.lock you should use to replace the old devtools/flake.lock" + + + diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 0000000000..d543aaf905 --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,103 @@ +on: + workflow_dispatch: + +name: Docker images tests + +jobs: + nightly-ubuntu-latest: + name: nightly-ubuntu-latest + runs-on: [ubuntu-22.04] + timeout-minutes: 120 + steps: + - uses: actions/checkout@v3 + + - name: Copy example docker file + run: cp docker/nightly-ubuntu-latest/docker-compose.example.yml docker/nightly-ubuntu-latest/docker-compose.yml + + - name: Build image + run: docker-compose -f docker/nightly-ubuntu-latest/docker-compose.yml build + + - name: Run hello world test + run: docker-compose -f docker/nightly-ubuntu-latest/docker-compose.yml run roc examples/helloWorld.roc + + + nightly-ubuntu-2204: + name: nightly-ubuntu-2204 + runs-on: [ubuntu-22.04] + timeout-minutes: 120 + steps: + - uses: actions/checkout@v3 + + - name: Copy example docker file + run: cp docker/nightly-ubuntu-2204/docker-compose.example.yml docker/nightly-ubuntu-2204/docker-compose.yml + + - name: Build image + run: docker-compose -f docker/nightly-ubuntu-2204/docker-compose.yml build + + - name: Run hello world test + run: docker-compose -f docker/nightly-ubuntu-2204/docker-compose.yml run roc examples/helloWorld.roc + + nightly-ubuntu-2004: + name: nightly-ubuntu-2004 + runs-on: [ubuntu-22.04] + timeout-minutes: 120 + steps: + - uses: actions/checkout@v3 + + - name: Copy example docker file + run: cp docker/nightly-ubuntu-2004/docker-compose.example.yml docker/nightly-ubuntu-2004/docker-compose.yml + + - name: Build image + run: docker-compose -f docker/nightly-ubuntu-2004/docker-compose.yml build + + - name: Run hello world test + run: docker-compose -f docker/nightly-ubuntu-2004/docker-compose.yml run roc examples/helloWorld.roc + + nightly-debian-latest: + name: nightly-debian-latest + runs-on: [ubuntu-22.04] + timeout-minutes: 120 + steps: + - uses: actions/checkout@v3 + + - name: Copy example docker file + run: cp docker/nightly-debian-latest/docker-compose.example.yml docker/nightly-debian-latest/docker-compose.yml + + - name: Build image + run: docker-compose -f docker/nightly-debian-latest/docker-compose.yml build + + - name: Run hello world test + run: docker-compose -f docker/nightly-debian-latest/docker-compose.yml run roc examples/helloWorld.roc + + nightly-debian-bookworm: + name: nightly-debian-bookworm + runs-on: [ubuntu-22.04] + timeout-minutes: 120 + steps: + - uses: actions/checkout@v3 + + - name: Copy example docker file + run: cp docker/nightly-debian-bookworm/docker-compose.example.yml docker/nightly-debian-bookworm/docker-compose.yml + + - name: Build image + run: docker-compose -f docker/nightly-debian-bookworm/docker-compose.yml build + + - name: Run hello world test + run: docker-compose -f docker/nightly-debian-bookworm/docker-compose.yml run roc examples/helloWorld.roc + + nightly-debian-buster: + name: nightly-debian-buster + runs-on: [ubuntu-22.04] + timeout-minutes: 120 + steps: + - uses: actions/checkout@v3 + + - name: Copy example docker file + run: cp docker/nightly-debian-buster/docker-compose.example.yml docker/nightly-debian-buster/docker-compose.yml + + - name: Build image + run: docker-compose -f docker/nightly-debian-buster/docker-compose.yml build + + - name: Run hello world test + run: docker-compose -f docker/nightly-debian-buster/docker-compose.yml run roc examples/helloWorld.roc + diff --git a/.github/workflows/macos_x86_64.yml b/.github/workflows/macos_x86_64.yml new file mode 100644 index 0000000000..a8b88390b4 --- /dev/null +++ b/.github/workflows/macos_x86_64.yml @@ -0,0 +1,33 @@ +on: + workflow_call: + +name: Macos x86-64 rust tests + +env: + RUST_BACKTRACE: 1 + +jobs: + test-rust-macos-x86-64: + runs-on: [self-hosted, macOS, X64] + timeout-minutes: 90 + env: + RUSTC_WRAPPER: /Users/username1/.cargo/bin/sccache + steps: + - uses: actions/checkout@v3 + + - name: set LLVM_SYS_160_PREFIX + run: echo "LLVM_SYS_160_PREFIX=$(brew --prefix llvm@16)" >> $GITHUB_ENV + + - name: Update PATH to use zig 11 + run: | + echo "PATH=/Users/username1/Downloads/zig-macos-x86_64-0.11.0:$PATH" >> $GITHUB_ENV + + - run: zig version + + - name: test_gen llvm tests + run: cargo nextest-gen-llvm --release --no-fail-fast --locked -E "package(test_gen) - test(gen_str::str_append_scalar)" + + - name: regular rust tests + run: cargo test --locked --release -- --skip opaque_wrap_function --skip gen_list::bool_list_literal --skip platform_switching_swift --skip swift_ui --skip gen_str::str_append_scalar --skip gen_tags::phantom_polymorphic_record && sccache --show-stats + # swift tests are skipped because of "Could not find or use auto-linked library 'swiftCompatibilityConcurrency'" on macos-11 x86_64 CI machine + # this issue may be caused by using older versions of XCode diff --git a/.github/workflows/markdown_link_check.yml b/.github/workflows/markdown_link_check.yml new file mode 100644 index 0000000000..1b60da4f8c --- /dev/null +++ b/.github/workflows/markdown_link_check.yml @@ -0,0 +1,30 @@ +on: + pull_request: + schedule: + - cron: '0 9 * * *' # 9=9am utc+0 + +name: Check Markdown links + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + markdown-link-check: + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v3 + - uses: gaurav-nelson/github-action-markdown-link-check@v1 + with: + use-quiet-mode: 'yes' + use-verbose-mode: 'yes' + base-branch: 'main' + check-modified-files-only: 'yes' + if: ${{ github.event_name == 'pull_request' }} + - uses: gaurav-nelson/github-action-markdown-link-check@v1 + with: + use-quiet-mode: 'yes' + use-verbose-mode: 'yes' + base-branch: 'main' + check-modified-files-only: 'no' + if: ${{ github.event_name == 'schedule' }} diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml deleted file mode 100644 index a7977235f7..0000000000 --- a/.github/workflows/nightly.yml +++ /dev/null @@ -1,31 +0,0 @@ -on: - schedule: - - cron: '0 9 * * *' - -name: Nightly Release Build - -jobs: - build: - name: Test and Build - runs-on: [self-hosted, i5-4690K] - timeout-minutes: 90 - env: - FORCE_COLOR: 1 # for earthly logging - steps: - - uses: actions/checkout@v2 - - name: Earthly print version - run: earthly --version - - name: install dependencies, build, run tests, build release - run: ./ci/safe-earthly.sh +build-nightly-release - - name: Create pre-release with test_archive.tar.gz - uses: WebFreak001/deploy-nightly@v1.1.0 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # automatically provided by github actions - with: - upload_url: https://uploads.github.com/repos/rtfeldman/roc/releases/51880579/assets{?name,label} - release_id: 51880579 - asset_path: ./roc_linux_x86_64.tar.gz - asset_name: roc_nightly-linux_x86_64-$$.tar.gz # $$ to inserts date (YYYYMMDD) and 6 letter commit hash - asset_content_type: application/gzip - max_releases: 3 - diff --git a/.github/workflows/nightly_linux_arm64.yml b/.github/workflows/nightly_linux_arm64.yml new file mode 100644 index 0000000000..96f9d5d958 --- /dev/null +++ b/.github/workflows/nightly_linux_arm64.yml @@ -0,0 +1,54 @@ +on: +# pull_request: + workflow_dispatch: + schedule: + - cron: '0 9 * * *' + +name: Nightly Release Linux arm64/aarch64 + +jobs: + build: + name: build and package nightly release + runs-on: [self-hosted, Linux, ARM64] + timeout-minutes: 90 + + steps: + - uses: actions/checkout@v3 + + - name: Update PATH to use zig 11 + run: | + echo "PATH=/home/username/Downloads/zig-linux-aarch64-0.11.0:$PATH" >> $GITHUB_ENV + + - run: zig version + + - name: create version.txt + run: ./ci/write_version.sh + + - name: build release with lto + run: cargo build --profile=release-with-lto --locked --bin roc + + - name: get commit SHA + run: echo "SHA=$(git rev-parse --short "$GITHUB_SHA")" >> $GITHUB_ENV + + - name: get date + run: echo "DATE=$(date "+%Y-%m-%d")" >> $GITHUB_ENV + + - name: build file name + env: + DATE: ${{ env.DATE }} + SHA: ${{ env.SHA }} + run: echo "RELEASE_FOLDER_NAME=roc_nightly-linux_arm64-$DATE-$SHA" >> $GITHUB_ENV + + # this makes the roc binary a lot smaller + - name: strip debug info + run: strip ./target/release-with-lto/roc + + - name: Make nightly release tar archive + run: ./ci/package_release.sh ${{ env.RELEASE_FOLDER_NAME }} + + - name: Upload roc nightly tar. Actually uploading to github releases has to be done manually. + uses: actions/upload-artifact@v3 + with: + name: ${{ env.RELEASE_FOLDER_NAME }}.tar.gz + path: ${{ env.RELEASE_FOLDER_NAME }}.tar.gz + retention-days: 4 diff --git a/.github/workflows/nightly_linux_x86_64.yml b/.github/workflows/nightly_linux_x86_64.yml new file mode 100644 index 0000000000..d285e65136 --- /dev/null +++ b/.github/workflows/nightly_linux_x86_64.yml @@ -0,0 +1,65 @@ +on: +# pull_request: + workflow_dispatch: + schedule: + - cron: '0 9 * * *' + +name: Nightly Release Linux x86_64 + +jobs: + build: + name: build and package nightly release + runs-on: [self-hosted, i7-6700K] + timeout-minutes: 90 + + steps: + - uses: actions/checkout@v3 + + - name: Update PATH to use zig 11 + run: | + echo "PATH=/home/big-ci-user/Downloads/zig-linux-x86_64-0.11.0:$PATH" >> $GITHUB_ENV + + - run: zig version + + - name: create version.txt + run: ./ci/write_version.sh + + - name: build release with lto + run: RUSTFLAGS="-C target-cpu=x86-64" cargo build --profile=release-with-lto --locked --bin roc + # target-cpu=x86-64 -> For maximal compatibility for all CPU's. This was also faster in our tests: https://roc.zulipchat.com/#narrow/stream/231635-compiler-development/topic/.2Ecargo.2Fconfig.2Etoml/near/325726299 + + - name: get commit SHA + run: echo "SHA=$(git rev-parse --short "$GITHUB_SHA")" >> $GITHUB_ENV + + - name: get date + run: echo "DATE=$(date "+%Y-%m-%d")" >> $GITHUB_ENV + + - name: build wasm repl + run: ./ci/www-repl.sh + + - name: Upload wasm repl tar. Actually uploading to github releases has to be done manually. + uses: actions/upload-artifact@v3 + with: + name: roc_repl_wasm.tar.gz + path: roc_repl_wasm.tar.gz + retention-days: 4 + + - name: build file name + env: + DATE: ${{ env.DATE }} + SHA: ${{ env.SHA }} + run: echo "RELEASE_FOLDER_NAME=roc_nightly-linux_x86_64-$DATE-$SHA" >> $GITHUB_ENV + + # this makes the roc binary a lot smaller + - name: strip debug info + run: strip ./target/release-with-lto/roc + + - name: Make nightly release tar archive + run: ./ci/package_release.sh ${{ env.RELEASE_FOLDER_NAME }} + + - name: Upload roc nightly tar. Actually uploading to github releases has to be done manually. + uses: actions/upload-artifact@v3 + with: + name: ${{ env.RELEASE_FOLDER_NAME }}.tar.gz + path: ${{ env.RELEASE_FOLDER_NAME }}.tar.gz + retention-days: 4 diff --git a/.github/workflows/nightly_macos_apple_silicon.yml b/.github/workflows/nightly_macos_apple_silicon.yml new file mode 100644 index 0000000000..63340de303 --- /dev/null +++ b/.github/workflows/nightly_macos_apple_silicon.yml @@ -0,0 +1,72 @@ +on: +# pull_request: + workflow_dispatch: + schedule: + - cron: '0 9 * * *' + +name: Nightly Release macOS Apple Silicon + +env: + RUST_BACKTRACE: 1 + LLVM_SYS_160_PREFIX: /opt/homebrew/opt/llvm@16 + +jobs: + test-and-build: + name: Rust tests, build and package nightly release + runs-on: [self-hosted, macOS, ARM64] + timeout-minutes: 90 + steps: + - uses: actions/checkout@v3 + + - run: zig version + + - name: llvm version + run: llc --version | grep LLVM + + - name: run tests + run: cargo test --locked --release + + - name: get commit SHA + run: echo "SHA=$(git rev-parse --short "$GITHUB_SHA")" >> $GITHUB_ENV + + - name: get date + run: echo "DATE=$(date "+%Y-%m-%d")" >> $GITHUB_ENV + + - name: build file name + env: + DATE: ${{ env.DATE }} + SHA: ${{ env.SHA }} + run: echo "RELEASE_FOLDER_NAME=roc_nightly-macos_apple_silicon-$DATE-$SHA" >> $GITHUB_ENV + + - name: write version to file + run: ./ci/write_version.sh + + - name: build nightly release + run: cargo build --locked --profile=release-with-lto --bin roc + + # this makes the roc binary a lot smaller + - name: strip debug info + run: strip ./target/release-with-lto/roc + + - name: package release + run: ./ci/package_release.sh ${{ env.RELEASE_FOLDER_NAME }} + + - name: delete everything except the tar + run: ls | grep -v "roc_nightly.*tar\.gz" | xargs rm -rf + + - name: extract tar for a quick test + run: ls | grep tar | xargs tar -xf + + - name: test with rust platform + run: cd ${{ env.RELEASE_FOLDER_NAME }} && ./roc examples/platform-switching/rocLovesRust.roc + + - name: print short commit SHA + run: git rev-parse --short "$GITHUB_SHA" + - name: print date + run: date + - name: Upload artifact Actually uploading to github releases has to be done manually + uses: actions/upload-artifact@v3 + with: + name: ${{ env.RELEASE_FOLDER_NAME }}.tar.gz + path: ${{ env.RELEASE_FOLDER_NAME }}.tar.gz + retention-days: 4 diff --git a/.github/workflows/nightly_macos_x86_64.yml b/.github/workflows/nightly_macos_x86_64.yml new file mode 100644 index 0000000000..6dd330aa99 --- /dev/null +++ b/.github/workflows/nightly_macos_x86_64.yml @@ -0,0 +1,63 @@ +on: +# pull_request: + workflow_dispatch: + schedule: + - cron: '0 9 * * *' # 9=9am utc+0 + +name: Nightly Release macOS x86_64 + +env: + LLVM_SYS_160_PREFIX: /usr/local/opt/llvm@16 + +jobs: + test-build-upload: + name: build, test, package and upload nightly release + runs-on: [self-hosted, macOS, X64] + timeout-minutes: 120 + steps: + - uses: actions/checkout@v3 + + - name: Update PATH to use zig 11 + run: | + echo "PATH=/Users/username1/Downloads/zig-macos-x86_64-0.11.0:$PATH" >> $GITHUB_ENV + + - run: zig version + + - name: write version to file + run: ./ci/write_version.sh + + - name: execute rust tests + run: cargo test --release --locked -- --skip opaque_wrap_function --skip gen_list::bool_list_literal --skip platform_switching_swift --skip swift_ui --skip gen_str::str_append_scalar --skip gen_tags::phantom_polymorphic_record + # swift tests are skipped because of "Could not find or use auto-linked library 'swiftCompatibilityConcurrency'" on macos-11 x86_64 CI machine + # this issue may be caused by using older versions of XCode + + - name: build release + run: RUSTFLAGS="-C target-cpu=x86-64" cargo build --profile=release-with-lto --locked --bin roc + # target-cpu=x86-64 -> For maximal compatibility for all CPU's. + + - name: get commit SHA + run: echo "SHA=$(git rev-parse --short "$GITHUB_SHA")" >> $GITHUB_ENV + + - name: get date + run: echo "DATE=$(date "+%Y-%m-%d")" >> $GITHUB_ENV + + - name: build file name + env: + DATE: ${{ env.DATE }} + SHA: ${{ env.SHA }} + run: echo "RELEASE_FOLDER_NAME=roc_nightly-macos_x86_64-$DATE-$SHA" >> $GITHUB_ENV + + # this makes the roc binary a lot smaller + - name: strip debug info + run: strip ./target/release-with-lto/roc + + - name: package release + run: ./ci/package_release.sh ${{ env.RELEASE_FOLDER_NAME }} + + - name: Upload artifact. Actually uploading to github releases has to be done manually. + uses: actions/upload-artifact@v3 + with: + name: ${{ env.RELEASE_FOLDER_NAME }}.tar.gz + path: ${{ env.RELEASE_FOLDER_NAME }}.tar.gz + retention-days: 4 + diff --git a/.github/workflows/nightly_netlify_build_deploy.yml b/.github/workflows/nightly_netlify_build_deploy.yml new file mode 100644 index 0000000000..907d806bf8 --- /dev/null +++ b/.github/workflows/nightly_netlify_build_deploy.yml @@ -0,0 +1,16 @@ +on: + schedule: + - cron: '0 9 * * *' + +name: Nightly netlify build and deploy + +jobs: + build: + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: trigger netlify build and deploy + env: + HOOK: ${{ secrets.NETLIFY_BUILD_HOOK }} + run: | + curl -X POST -d {} https://api.netlify.com/build_hooks/${HOOK} diff --git a/.github/workflows/nightly_old_linux_arm64.yml b/.github/workflows/nightly_old_linux_arm64.yml new file mode 100644 index 0000000000..19ad0c695c --- /dev/null +++ b/.github/workflows/nightly_old_linux_arm64.yml @@ -0,0 +1,39 @@ +on: +# pull_request: + workflow_dispatch: + schedule: + - cron: '0 9 * * *' + +name: Nightly Release Old Linux arm64 using Earthly + +jobs: + build: + name: build and package nightly release + runs-on: [self-hosted, Linux, ARM64] + timeout-minutes: 180 + steps: + - uses: actions/checkout@v3 + + - name: get commit SHA + run: echo "SHA=$(git rev-parse --short "$GITHUB_SHA")" >> $GITHUB_ENV + + - name: get date + run: echo "DATE=$(date "+%Y-%m-%d")" >> $GITHUB_ENV + + - name: build file name + env: + DATE: ${{ env.DATE }} + SHA: ${{ env.SHA }} + run: echo "RELEASE_FOLDER_NAME=roc_nightly-old_linux_arm64-$DATE-$SHA" >> $GITHUB_ENV + + - run: earthly --version + + - name: build release with earthly + run: earthly +build-nightly-release --RELEASE_FOLDER_NAME=${{ env.RELEASE_FOLDER_NAME }} --ZIG_ARCH=aarch64 + + - name: Upload roc nightly tar. Actually uploading to github releases has to be done manually. + uses: actions/upload-artifact@v3 + with: + name: ${{ env.RELEASE_FOLDER_NAME }}.tar.gz + path: ${{ env.RELEASE_FOLDER_NAME }}.tar.gz + retention-days: 4 diff --git a/.github/workflows/nightly_old_linux_x86_64.yml b/.github/workflows/nightly_old_linux_x86_64.yml new file mode 100644 index 0000000000..a541879658 --- /dev/null +++ b/.github/workflows/nightly_old_linux_x86_64.yml @@ -0,0 +1,42 @@ +on: +# pull_request: + workflow_dispatch: + schedule: + - cron: '0 9 * * *' + +name: Nightly Release Old Linux x86_64 using Earthly + +jobs: + build: + name: build and package nightly release + runs-on: [ubuntu-20.04] + timeout-minutes: 90 + steps: + - uses: actions/checkout@v3 + + - name: get commit SHA + run: echo "SHA=$(git rev-parse --short "$GITHUB_SHA")" >> $GITHUB_ENV + + - name: get date + run: echo "DATE=$(date "+%Y-%m-%d")" >> $GITHUB_ENV + + - name: build file name + env: + DATE: ${{ env.DATE }} + SHA: ${{ env.SHA }} + run: echo "RELEASE_FOLDER_NAME=roc_nightly-old_linux_x86_64-$DATE-$SHA" >> $GITHUB_ENV + + - name: install earthly + run: sudo /bin/sh -c 'wget https://github.com/earthly/earthly/releases/latest/download/earthly-linux-amd64 -O /usr/local/bin/earthly && chmod +x /usr/local/bin/earthly && /usr/local/bin/earthly bootstrap --with-autocomplete' + + - run: earthly --version + + - name: build release with earthly + run: earthly +build-nightly-release --RELEASE_FOLDER_NAME=${{ env.RELEASE_FOLDER_NAME }} --RUSTFLAGS="-C target-cpu=x86-64" + + - name: Upload roc nightly tar. Actually uploading to github releases has to be done manually. + uses: actions/upload-artifact@v3 + with: + name: ${{ env.RELEASE_FOLDER_NAME }}.tar.gz + path: ${{ env.RELEASE_FOLDER_NAME }}.tar.gz + retention-days: 4 diff --git a/.github/workflows/nix.yml b/.github/workflows/nix.yml deleted file mode 100644 index 149ed5a3f3..0000000000 --- a/.github/workflows/nix.yml +++ /dev/null @@ -1,28 +0,0 @@ -on: [pull_request] - -name: Nix M1 cargo test - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -env: - RUST_BACKTRACE: 1 - -jobs: - nix-m1: - name: nix-m1 - runs-on: [self-hosted, macOS, ARM64] - timeout-minutes: 90 - env: - FORCE_COLOR: 1 - steps: - - uses: actions/checkout@v2 - with: - clean: "true" - - - name: setup dependencies with nix and build the tests - run: nix develop -c cargo test --locked --release --no-run - - - name: execute tests with guaranteed success - run: nix develop -c cargo test --locked --release --no-fail-fast || true # || true to return a successful exit code so that test failures can be observed diff --git a/.github/workflows/nix_linux_arm64_cargo.yml b/.github/workflows/nix_linux_arm64_cargo.yml new file mode 100644 index 0000000000..488d6af721 --- /dev/null +++ b/.github/workflows/nix_linux_arm64_cargo.yml @@ -0,0 +1,22 @@ +on: + workflow_call: + +name: test cargo build on linux arm64 inside nix + +env: + RUST_BACKTRACE: 1 + +jobs: + nix-linux-arm64-cargo: + name: nix-linux-arm64 + runs-on: [self-hosted, Linux, ARM64] + timeout-minutes: 150 + steps: + - uses: actions/checkout@v3 + + - name: test release build + run: nix develop -c cargo build --release --locked + + # TODO + #- name: build tests without running + # run: cargo test --no-run --release diff --git a/.github/workflows/nix_linux_arm64_default.yml b/.github/workflows/nix_linux_arm64_default.yml new file mode 100644 index 0000000000..0352fc3fd8 --- /dev/null +++ b/.github/workflows/nix_linux_arm64_default.yml @@ -0,0 +1,18 @@ +on: + workflow_call: + +name: test default.nix on linux arm64 + +env: + RUST_BACKTRACE: 1 + +jobs: + nix-linux-arm64-default: + name: nix-linux-arm64 + runs-on: [self-hosted, Linux, ARM64] + timeout-minutes: 150 + steps: + - uses: actions/checkout@v3 + + - name: test building default.nix + run: nix-build diff --git a/.github/workflows/nix_linux_x86_64.yml b/.github/workflows/nix_linux_x86_64.yml new file mode 100644 index 0000000000..f3f957f70d --- /dev/null +++ b/.github/workflows/nix_linux_x86_64.yml @@ -0,0 +1,33 @@ +on: + workflow_call: + +name: Nix linux x86_64 cargo test + +env: + RUST_BACKTRACE: 1 + +jobs: + nix-linux-x86: + name: nix-linux-x86 + runs-on: [self-hosted, i5-4690K] + timeout-minutes: 90 + steps: + - uses: actions/checkout@v3 + + - name: test building default.nix + run: nix-build + + - name: execute tests with --release + run: nix develop -c cargo test --locked --release + + - name: test wasm32 cli_run + run: nix develop -c cargo test --locked --release --features="wasm32-cli-run" + + - name: test the dev backend # these tests require an explicit feature flag + run: nix develop -c cargo nextest run --locked --release --package test_gen --no-default-features --features gen-dev --no-fail-fast + + - name: wasm repl tests + run: nix develop -c crates/repl_test/test_wasm.sh + + - name: test building wasm repl + run: nix develop -c ./ci/www-repl.sh diff --git a/.github/workflows/nix_macos_apple_silicon.yml b/.github/workflows/nix_macos_apple_silicon.yml new file mode 100644 index 0000000000..baac08bc4f --- /dev/null +++ b/.github/workflows/nix_macos_apple_silicon.yml @@ -0,0 +1,59 @@ +on: + workflow_call: + +name: Nix apple silicon cargo test + +env: + RUST_BACKTRACE: 1 + +jobs: + nix-apple-silicon: + name: nix-apple-silicon + runs-on: [self-hosted, macOS, ARM64] + timeout-minutes: 90 + steps: + - uses: actions/checkout@v3 + + # These started to accumulate quickly since #5990, not sure why + - name: Clean up old nix shells + run: rm -rf /private/tmp/nix-shell.* + + - run: zig version + + - name: check formatting with rustfmt + run: nix develop -c cargo fmt --all -- --check + + - name: check code style with clippy + run: nix develop -c cargo clippy --workspace --tests -- --deny warnings + + - name: check code style with clippy --release + run: nix develop -c cargo clippy --workspace --tests --release -- --deny warnings + + - name: test building default.nix + run: nix-build + + - name: execute tests with --release + run: nix develop -c cargo test --locked --release + + - name: make a libapp.so for the next step + run: nix develop -c cargo run -- gen-stub-lib examples/platform-switching/rocLovesRust.roc + + - name: check that the platform`s produced dylib is loadable + run: cd examples/platform-switching/rust-platform && nix develop -c cargo test --release --locked + + - name: test aarch64 dev backend + run: nix develop -c cargo nextest-gen-dev --locked --release --no-fail-fast + + # we run the llvm wasm tests only on this machine because it is fast and wasm should be cross-target + - name: execute llvm wasm tests with --release + run: nix develop -c cargo test-gen-llvm-wasm --locked --release + + - name: set env var and test website build script + run: | + nix develop -c bash www/build.sh + + - name: wasm repl tests + run: nix develop -c crates/repl_test/test_wasm.sh + + - name: test building wasm repl + run: nix develop -c ./ci/www-repl.sh diff --git a/.github/workflows/nix_macos_x86_64.yml b/.github/workflows/nix_macos_x86_64.yml new file mode 100644 index 0000000000..8fde16b746 --- /dev/null +++ b/.github/workflows/nix_macos_x86_64.yml @@ -0,0 +1,27 @@ +on: + workflow_call: + +name: Nix macOS x86_64 cargo test + +env: + RUST_BACKTRACE: 1 + +jobs: + nix-macos-x86-64: + name: nix-macos-x86-64 + runs-on: [macos-12] + timeout-minutes: 90 + steps: + - uses: actions/checkout@v3 + + - uses: cachix/install-nix-action@v22 + + - name: execute cli_run tests only, the full tests take too long but are run nightly + run: nix develop -c cargo test --locked --release -p roc_cli -- --skip hello_gui + # see 5932 for hello_gui + + - name: make a libapp.so for the next step + run: nix develop -c cargo run -- gen-stub-lib examples/platform-switching/rocLovesRust.roc + + - name: check that the platform`s produced dylib is loadable + run: cd examples/platform-switching/rust-platform && nix develop -c cargo test --release --locked diff --git a/.github/workflows/spellcheck.yml b/.github/workflows/spellcheck.yml index 0a1eee7af9..36531f1a11 100644 --- a/.github/workflows/spellcheck.yml +++ b/.github/workflows/spellcheck.yml @@ -1,4 +1,5 @@ -on: [pull_request] +on: + pull_request: name: SpellCheck @@ -12,17 +13,14 @@ env: jobs: spell-check: name: spell check - runs-on: [self-hosted, linux] + runs-on: [ubuntu-20.04] timeout-minutes: 10 env: FORCE_COLOR: 1 steps: - - uses: actions/checkout@v2 - with: - clean: "true" + - uses: actions/checkout@v3 - - name: Earthly version - run: earthly --version + - run: cargo install typos-cli --version 1.0.11 - - name: install spell checker, do spell check - run: ./ci/safe-earthly.sh +check-typos + - name: do spell check with typos-cli 1.0.11 + run: typos diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 0000000000..05e0ffe858 --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,21 @@ +name: 'Close stale PRs' +on: + schedule: + - cron: '30 1 * * *' + +jobs: + stale: + runs-on: ubuntu-latest + permissions: + pull-requests: write + steps: + - uses: actions/stale@v5 + with: + delete-branch: true + exempt-pr-labels: 'blocked' + days-before-issue-close: -1 + days-before-pr-stale: 30 + days-before-pr-close: 30 + stale-pr-message: 'Thank you for your contribution! Sometimes PRs end up staying open for a long time without activity, which can make the list of open PRs get long and time-consuming to review. To keep things manageable for reviewers, this bot automatically closes PRs that haven’t had activity in 60 days. This PR hasn’t had activity in 30 days, so it will be automatically closed if there is no more activity in the next 30 days. Keep in mind that PRs marked `Closed` are not deleted, so no matter what, the PR will still be right here in the repo. You can always access it and reopen it anytime you like!' + stale-pr-label: 'inactive for 30 days' + close-pr-label: 'auto-closed' diff --git a/.github/workflows/test_nightly_macos_apple_silicon.yml b/.github/workflows/test_nightly_macos_apple_silicon.yml new file mode 100644 index 0000000000..c18d1838bb --- /dev/null +++ b/.github/workflows/test_nightly_macos_apple_silicon.yml @@ -0,0 +1,50 @@ +on: + workflow_dispatch: + +name: Test latest nightly release for macOS Apple Silicon + +jobs: + test-nightly: + name: test nightly macos aarch64 + runs-on: [self-hosted, macOS, ARM64] + timeout-minutes: 90 + steps: + - uses: actions/checkout@v3 + + - name: get the latest release archive + run: curl -fOL https://github.com/roc-lang/roc/releases/download/nightly/roc_nightly-macos_apple_silicon-latest.tar.gz + + - name: remove everything in this dir except the tar and ci folder # we want to test like a user who would have downloaded the release, so we clean up all files from the repo checkout + run: ls | grep -v "roc_nightly.*tar\.gz" | grep -v "^ci$" | xargs rm -rf + + - name: decompress the tar + run: ls | grep "roc_nightly.*tar\.gz" | xargs tar -xzvf + + - name: delete tar + run: ls | grep "roc_nightly.*tar\.gz" | xargs rm -rf + + - name: rename nightly folder + run: mv roc_nightly* roc_nightly + + - name: test roc hello world + run: cd roc_nightly && ./roc examples/helloWorld.roc + + - name: test platform switching rust + run: cd roc_nightly && ./roc examples/platform-switching/rocLovesRust.roc + + - name: test platform switching zig + run: cd roc_nightly && ./roc examples/platform-switching/rocLovesZig.roc + + - name: test platform switching c + run: cd roc_nightly && ./roc examples/platform-switching/rocLovesC.roc + + - name: test repl + run: | + cd ci/repl_basic_test + cargo build --release + cp target/release/repl_basic_test ../../roc_nightly + cd ../../roc_nightly + ./repl_basic_test + + + diff --git a/.github/workflows/test_nightly_many_os.yml b/.github/workflows/test_nightly_many_os.yml new file mode 100644 index 0000000000..53c2e254e5 --- /dev/null +++ b/.github/workflows/test_nightly_many_os.yml @@ -0,0 +1,46 @@ +on: + workflow_dispatch: + +name: Test latest nightly releases for macOS and Linux x86_64 + +jobs: + test-nightly: + name: test nightly macos 11/12/13, ubuntu 20.04/22.04 + strategy: + fail-fast: false + matrix: + os: [ macos-11, macos-12, macos-13, ubuntu-20.04, ubuntu-22.04 ] + runs-on: ${{ matrix.os }} + timeout-minutes: 90 + steps: + - uses: actions/checkout@v3 + - uses: goto-bus-stop/setup-zig@v2 + with: + version: 0.11.0 + + - name: get the latest release archive for linux (x86_64) + if: startsWith(matrix.os, 'ubuntu') + run: | + curl -fOL https://github.com/roc-lang/roc/releases/download/nightly/roc_nightly-linux_x86_64-latest.tar.gz + + + - name: get the latest release archive for macos (x86_64) + if: startsWith(matrix.os, 'macos') + run: curl -fOL https://github.com/roc-lang/roc/releases/download/nightly/roc_nightly-macos_x86_64-latest.tar.gz + + - run: zig version + + - name: prep and run basic tests + run: | + ./ci/basic_nightly_test.sh + + - name: clean up, get old linux release (x86_64), run tests + if: startsWith(matrix.os, 'ubuntu') + run: | + rm -rf roc_nightly + curl -fOL https://github.com/roc-lang/roc/releases/download/nightly/roc_nightly-old_linux_x86_64-latest.tar.gz + ./ci/basic_nightly_test.sh + + + + diff --git a/.github/workflows/ubuntu_x86_64.yml b/.github/workflows/ubuntu_x86_64.yml new file mode 100644 index 0000000000..d5c6338e0c --- /dev/null +++ b/.github/workflows/ubuntu_x86_64.yml @@ -0,0 +1,66 @@ +on: + workflow_call: + +name: CI + +env: + RUST_BACKTRACE: 1 + +jobs: + test-zig-rust-wasm: + name: test zig, rust, wasm... + runs-on: [self-hosted, i7-6700K] + timeout-minutes: 90 + env: + RUSTC_WRAPPER: /home/big-ci-user/.cargo/bin/sccache + steps: + - uses: actions/checkout@v3 + + - name: Check for duplicate AUTHORS + run: diff <(sort AUTHORS) <(sort AUTHORS | uniq) # The < operator treats a string as a file. diff 'succeeds' if no difference. + + - name: Update PATH to use zig 11 + run: | + echo "PATH=/home/big-ci-user/Downloads/zig-linux-x86_64-0.11.0:$PATH" >> $GITHUB_ENV + + - run: zig version + + - name: zig fmt check, zig tests + run: cd crates/compiler/builtins/bitcode && ./run-tests.sh + + - name: roc format check on builtins + run: cargo run --locked --release format --check crates/compiler/builtins/roc + + - name: zig wasm tests + run: cd crates/compiler/builtins/bitcode && ./run-wasm-tests.sh + + - name: regular rust tests + # see #5904 for skipped test + run: cargo test --locked --release -- --skip cli_run::expects_dev_and_test && sccache --show-stats + + - name: check that the platform`s produced dylib is loadable + run: cd examples/platform-switching/rust-platform && LD_LIBRARY_PATH=. cargo test --release --locked + + - name: test the dev backend # these tests require an explicit feature flag + run: cargo test --locked --release --package test_gen --no-default-features --features gen-dev && sccache --show-stats + + - name: test gen-wasm single threaded # gen-wasm has some multithreading problems to do with the wasmer runtime + run: cargo test --locked --release --package test_gen --no-default-features --features gen-wasm -- --test-threads=1 && sccache --show-stats + + - name: run `roc test` on Str builtins + run: cargo run --locked --release -- test crates/compiler/builtins/roc/Str.roc && sccache --show-stats + + - name: run `roc test` on Dict builtins + run: cargo run --locked --release -- test crates/compiler/builtins/roc/Dict.roc && sccache --show-stats + + - name: wasm repl test + run: crates/repl_test/test_wasm.sh && sccache --show-stats + + - name: test building wasm repl + run: ./ci/www-repl.sh && sccache --show-stats + + #TODO i386 (32-bit linux) cli tests + #TODO verify-no-git-changes + + - name: test website build script + run: bash www/build.sh diff --git a/.github/workflows/windows_release_build.yml b/.github/workflows/windows_release_build.yml new file mode 100644 index 0000000000..ef67101b4f --- /dev/null +++ b/.github/workflows/windows_release_build.yml @@ -0,0 +1,41 @@ +on: + workflow_call: + +name: windows - release build + +env: + RUST_BACKTRACE: 1 + +jobs: + windows-release-build: + name: windows-release-build + runs-on: windows-2022 + env: + LLVM_SYS_160_PREFIX: C:\LLVM-16.0.6-win64 + + timeout-minutes: 150 + steps: + - uses: actions/checkout@v3 + + - run: Add-Content -Path "$env:GITHUB_ENV" -Value "GITHUB_RUNNER_CPU=$((Get-CimInstance Win32_Processor).Name)" + + - name: download and install zig + run: | + curl.exe -f --output "C:\zig-windows-x86_64-0.11.0.zip" --url https://ziglang.org/download/0.11.0/zig-windows-x86_64-0.11.0.zip + cd C:\ + 7z x zig-windows-x86_64-0.11.0.zip + Add-Content $env:GITHUB_PATH "C:\zig-windows-x86_64-0.11.0\" + + - name: zig version + run: zig version + + - name: install rust nightly 1.71.0 + run: rustup install nightly-2023-05-28 + + - name: set up llvm 16 + run: | + curl.exe -f -L -O -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" https://github.com/roc-lang/llvm-package-windows/releases/download/v16.0.6/LLVM-16.0.6-win64.7z + 7z x LLVM-16.0.6-win64.7z -oC:\LLVM-16.0.6-win64 + + - name: cargo build release. + run: cargo build --locked --release diff --git a/.github/workflows/windows_tests.yml b/.github/workflows/windows_tests.yml new file mode 100644 index 0000000000..8271303b62 --- /dev/null +++ b/.github/workflows/windows_tests.yml @@ -0,0 +1,65 @@ +on: + workflow_call: + +name: windows - subset of tests + +env: + RUST_BACKTRACE: 1 + +jobs: + windows-test-subset: + name: windows-test-subset + runs-on: windows-2022 + env: + LLVM_SYS_160_PREFIX: C:\LLVM-16.0.6-win64 + + timeout-minutes: 150 + steps: + - uses: actions/checkout@v3 + + - run: Add-Content -Path "$env:GITHUB_ENV" -Value "GITHUB_RUNNER_CPU=$((Get-CimInstance Win32_Processor).Name)" + + - uses: Swatinem/rust-cache@v2 + with: + shared-key: "rust-cache-windows-${{env.GITHUB_RUNNER_CPU}}" + + - name: download and install zig + run: | + curl.exe -f --output "C:\zig-windows-x86_64-0.11.0.zip" --url https://ziglang.org/download/0.11.0/zig-windows-x86_64-0.11.0.zip + cd C:\ + 7z x zig-windows-x86_64-0.11.0.zip + Add-Content $env:GITHUB_PATH "C:\zig-windows-x86_64-0.11.0\" + + - run: zig version + + - name: zig tests + run: | + cd crates\compiler\builtins\bitcode\ + zig build test + + - name: install rust nightly 1.71.0 + run: rustup install nightly-2023-05-28 + + - name: set up llvm 16 + run: | + curl.exe -f -L -O -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" https://github.com/roc-lang/llvm-package-windows/releases/download/v16.0.6/LLVM-16.0.6-win64.7z + 7z x LLVM-16.0.6-win64.7z -oC:\LLVM-16.0.6-win64 + + - name: Build tests --release without running. + run: cargo test --locked --release --no-run + + # Why are these tests not build with previous command? => fingerprint error. Use `CARGO_LOG=cargo::core::compiler::fingerprint=info` to investigate + - name: Build specific tests without running. + run: cargo test --locked --release --no-run -p roc_ident -p roc_region -p roc_collections -p roc_can -p roc_types -p roc_solve -p roc_mono -p roc_gen_dev -p roc_gen_wasm -p roc_serialize -p roc_linker -p roc_cli -p test_gen + + - name: Test setjmp/longjmp logic + run: cargo test-gen-dev --locked --release nat_alias && cargo test-gen-dev --locked --release a_crash + + - name: Run gen tests + run: cargo test-gen-llvm --locked --release gen_str + + - name: Actually run the tests. + run: cargo test --locked --release -p roc_ident -p roc_region -p roc_collections -p roc_can -p roc_types -p roc_solve -p roc_mono -p roc_gen_dev -p roc_gen_wasm -p roc_serialize -p roc_linker -p roc_cli + + + diff --git a/.github/workflows/www.yml b/.github/workflows/www.yml index d02f905723..73e58c7501 100644 --- a/.github/workflows/www.yml +++ b/.github/workflows/www.yml @@ -1,10 +1,10 @@ name: deploy www.roc-lang.org -# Whenever a commit lands on trunk, deploy the site +# Whenever a commit lands on `main`, deploy the site on: push: branches: - - deploy-www # TODO change to trunk + - deploy-www jobs: deploy: diff --git a/.gitignore b/.gitignore index 87635559c0..fd1a81c005 100644 --- a/.gitignore +++ b/.gitignore @@ -5,31 +5,38 @@ zig-cache .envrc *.rs.bk *.o +*.so +*.so.* +*.obj +*.dll +*.dylib +*.lib +*.def *.tmp +*.wasm +*.exe +*.pdb # llvm human-readable output *.ll *.bc -#valgrind +# valgrind vgcore.* +# roc cache files +*.rh* +*.rm* +preprocessedhost +metadata + #editors .idea/ .vscode/ .ignore - -#files too big to track in git -editor/benches/resources/100000_lines.roc -editor/benches/resources/10000_lines.roc -editor/benches/resources/1000_lines.roc -editor/benches/resources/100_lines.roc -editor/benches/resources/25000000_lines.roc -editor/benches/resources/50000_lines.roc -editor/benches/resources/500_lines.roc - -# file editor creates when no arg is passed -new-roc-project +.exrc +.vimrc +.nvimrc # rust cache (sccache folder) sccache_dir @@ -45,3 +52,33 @@ roc_linux_x86_64.tar.gz # macOS .DS_Store files .DS_Store + +# files geneated when formatting fails +*.roc-format-failed +*.roc-format-failed-ast-after +*.roc-format-failed-ast-before + +# nix +result + +# Only keep Cargo.lock dependencies for the main compiler. +# Examples and test only crates should be fine to be unlocked. +# This remove unneccessary lock file versioning. +# It also ensures the compiler can always pull in 1 version of things and doesn't get restricted by sub lockfiles. +/**/Cargo.lock +!/Cargo.lock + +# snapshot tests temp file +*.pending-snap + +# checkmate +checkmate_*.json + +www/build/ +www/main +www/dist + +# ignore the examples folder in the WIP website, this is copied from roc-lang/examples in when building the site +www/content/examples +www/examples-main.zip +www/examples-main \ No newline at end of file diff --git a/.llvmenv b/.llvmenv index 02161ca86e..946789e619 100644 --- a/.llvmenv +++ b/.llvmenv @@ -1 +1 @@ -13.0.0 +16.0.0 diff --git a/.reuse/dep5 b/.reuse/dep5 index f1dd1329f6..eb10621a4f 100644 --- a/.reuse/dep5 +++ b/.reuse/dep5 @@ -1,7 +1,7 @@ Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ Upstream-Name: Roc Upstream-Contact: Richard Feldman -Source: https://github.com/rtfeldman/roc +Source: https://github.com/roc-lang/roc Files: * Copyright: © The Roc Contributors diff --git a/AUTHORS b/AUTHORS index a64c9a52cd..40dd984d2d 100644 --- a/AUTHORS +++ b/AUTHORS @@ -3,28 +3,27 @@ Folkert de Vries Chad Stearns Christoph Hermann Dan Bruder -Ian Mackenzie Mario Rogic -Anton-4 <17049058+Anton-4@users.noreply.github.com> +Ian Mackenzie +Anton <17049058+Anton-4@users.noreply.github.com> Sébastien Besnier +Wolfgang Schuster Harrison Bachrach -Wolfgag Schuster -Nathan Bleigh +Jared Ramirez Aksel Wester -Jared Ramirez Dimitar Apostolov -Brendan Hansknecht +Nathan Bleigh Pablo Hirafuji +Brendan Hansknecht Bob Shelline Guilherme Belmonte Stefan Ladwig Wojciech Piekutowski Zeljko Nesic -Lucas Rosa Pit Capitain +Lucas Rosa Celso Bonutti Filho Eric Henry -Ju Liu Peter Fields Brian J. Cardiff Basile Henry @@ -34,7 +33,7 @@ Dan Gieschen Knutson Joshua Hoeflich Brian Carroll Kofi Gumbs -Luiz de Oliveira +Luiz Carlos L. G. de Oliveira Chelsea Troy Shritesh Bhattarai Kevin Sjöberg @@ -44,27 +43,34 @@ Matthias Beyer Tim Whiting Logan Lowder Joshua Warner -Luiz Carlos L. G. de Oliveira -Oleksii Skidan -Martin Janiczek -Eric Newbury -Ayaz Hafiz -Johannes Maas Takeshi Sato +Oleksii Skidan +Eric Newbury +Martin Janiczek +Johannes Maas +Ayaz Hafiz +Michael Downey +Theo Felippe Joost Baas +Cristiano Piemontese Callum Dunster Martin Stewart James Hegedus -Cristiano Piemontese Yann Simon Shahn Hogan Tankor Smash +Ivo-Balbaert Matthias Devlamynck -Jan Van Bruggen -Mats Sigge <> +Jan Van Bruggen +Nick Mazuk +Mats Sigge Drew Lazzeri Tom Dohrmann +Ryan Olson Elijah Schow +Emi Simpson +Celso Bonutti +Jose Quesada Derek Gustafson Philippe Vinchon Pierre-Henri Trivier @@ -72,13 +78,95 @@ Elliot Waite <1767836+elliotwaite@users.noreply.github.com> zimt28 <1764689+zimt28@users.noreply.github.com> Ananda Umamil SylvanSign -Nikita Mounier <36044205+nikitamounier@users.noreply.github.com> Cai Bingjun <62678643+C-BJ@users.noreply.github.com> +Nikita Mounier +Kevin Gillette Jared Cone Sean Hagstrom -Kas Buunk +Kas Buunk +Tommy Graves Oskar Hahn Nuno Ferreira +Jonas Schell Mfon Eti-mfon Drake Bennion Hashi364 <49736221+Kiyoshi364@users.noreply.github.com> +Jared Forsyth +Patrick Kilgore +Marten/Qqwy +Tobias Steckenborn +Christoph Rüßler +Ralf Engbers +Mostly Void <7rat13@gmail.com> +Luis F. Gutierrez +Ross Smyth +David A. Kunz +Paul Young <84700+paulyoung@users.noreply.github.com> +Rod +Marko Vujanic +Jelle Besseling +isaacthefallenapple +Bryce Miller +Bjørn Madsen +David Dunn <26876072+doubledup@users.noreply.github.com> +Vilem <17603372+buggymcbugfix@users.noreply.github.com> +KilianVounckx +J Teeuwissen +Matthieu Pizenberg +rezzaghi +João Mota +Marcos Prieto +Prajwal S N +Christopher Duncan +Luke Boswell +Luca Cervello +Josh Mak +Jakub Kozłowski +Travis Staloch +Nick Gravgaard +Keerthana Kasthuril <76804118+keerthanak-tw@users.noreply.github.com> +Salman Shaik +Austin Clements +Georges Boris +Marc Walter +Nathan Freestone <17188138+nfreesto@users.noreply.github.com> +Lunarmagpie +Ahmad Sattar +Jack Kellenberger <107156696+jmkellenberger@users.noreply.github.com> +Christopher Bertels +Henrikh Kantuni +dankeyy +human154 <46430360+human154@users.noreply.github.com> +Ju Liu +Giacomo Cavalieri +Ajai Nelson <22969541+Aurelius333@users.noreply.github.com> +Agus Zubiaga +itmuckel +Seth Workman +Jacob Zimmerman +Yuki Omoto +Leonardo Taglialegne +Jonas Schell +Kiryl Dziamura +Isaac Van Doren <69181572+isaacvando@users.noreply.github.com> +Jarl André Hübenthal +Gabriel Dertoni +Mattia Maldini +David Smith +Abdullah Umer +Fábio Beirão +Tero Laxström +HajagosNorbert +Hannes +K. Eisuke +KekmaTime <136650032+KekmaTime@users.noreply.github.com> +NoaVidovic +Artsiom Shamsutdzinau +Kadin Sayani +Nachiket Kanore +pinage404 +Fabian Schmalzried +Isak Jones +Ch1n3du +Elias Mulhall +ABuffSeagull diff --git a/BUILDING_FROM_SOURCE.md b/BUILDING_FROM_SOURCE.md index 00f11ccd43..b2df065d92 100644 --- a/BUILDING_FROM_SOURCE.md +++ b/BUILDING_FROM_SOURCE.md @@ -1,81 +1,62 @@ # Building the Roc compiler from source +If you run into any problems getting Roc built from source, please ask for help in the `#beginners` channel on [Roc Zulip](https://roc.zulipchat.com) (the fastest way), or create an issue in this repo! + ## Using Nix -### On Linux/MacOS/NixOS x86_64/aarch64/arm64 +On MacOS and Linux, we highly recommend Using [nix](https://nixos.org/download.html) to quickly install all dependencies necessary to build roc. + +:warning: If you tried to run `cargo` in the repo folder before installing nix, make sure to execute `cargo clean` first. To prevent you from executing `cargo` outside of nix, tools like [direnv](https://github.com/nix-community/nix-direnv) and [lorri](https://github.com/nix-community/lorri) can put you in a nix shell automatically when you `cd` into the directory. + +### On Linux x86_64 or MacOS aarch64/arm64/x86_64 #### Install -We highly recommend Using [nix](https://nixos.org/download.html) to automatically install all dependencies necessary to build roc. - If you are running ArchLinux or a derivative like Manjaro, you'll need to run `sudo sysctl -w kernel.unprivileged_userns_clone=1` before installing nix. Install nix (not necessary on NixOS): -``` -curl -L https://nixos.org/nix/install | sh + +- If you are using WSL (Windows subsystem for Linux): + +```sh +sh <(curl -L https://nixos.org/nix/install) --no-daemon ``` -Start a fresh terminal session (= open a new terminal). +- For everything else: -install nixFlakes in your environment: -``` -nix-env -iA nixpkgs.nixFlakes +```sh +sh <(curl -L https://nixos.org/nix/install) --daemon ``` -Edit either `~/.config/nix/nix.conf` or `/etc/nix/nix.conf` and add: -``` +Open a new terminal and edit either `~/.config/nix/nix.conf` or `/etc/nix/nix.conf` and add: + +```text experimental-features = nix-command flakes ``` - If Nix was installed in multi-user mode, make sure to restart the nix-daemon. - If you don't know how to do this, restarting your computer will also do the job. +If Nix was installed in multi-user mode, make sure to restart the nix-daemon. +If you don't know how to do this, restarting your computer will also do the job. #### Usage Now with nix set up, you just need to run one command from the roc project root directory: -``` + +```sh nix develop ``` + You should be in a shell with everything needed to build already installed. Use `cargo run help` to see all subcommands. To use the `repl` subcommand, execute `cargo run repl`. Use `cargo build` to build the whole project. +Read the instructions [here](devtools/README.md) to make nix work well with your development tools (vscode, vim, rust-analyzer...) + #### Extra tips -If you want to load all dependencies automatically whenever you `cd` into `roc`, check out [direnv](https://direnv.net/) and [lorri](https://github.com/nix-community/lorri). +If you want to load all dependencies automatically whenever you `cd` into `roc`, check out [direnv](https://direnv.net/). Then you will no longer need to execute `nix develop` first. -### Editor - -The editor is a :construction:WIP:construction: and not ready yet to replace your favorite editor, although if you want to try it out on nix, read on. -`cargo run edit` should work from NixOS, if you use another OS, follow the instructions below. - -#### Nvidia GPU - -``` -nix run --override-input nixpkgs nixpkgs/nixos-21.11 --impure github:guibou/nixGL#nixVulkanNvidia -- cargo run edit -``` - -If you get an error like: -``` -error: unable to execute '/nix/store/qk6...wjla-nixVulkanNvidia-470.103.01/bin/nixVulkanNvidia': No such file or directory -``` -The intel command should work: -``` -nix run --override-input nixpkgs nixpkgs/nixos-21.11 --impure github:guibou/nixGL#nixVulkanIntel -- cargo run edit -``` - -##### Integrated Intel Graphics - -``` -nix run --override-input nixpkgs nixpkgs/nixos-21.11 --impure github:guibou/nixGL#nixVulkanIntel -- cargo run edit -``` - -##### Other configs - -Check the [nixGL repo](https://github.com/guibou/nixGL) for other graphics configurations. Feel free to ask us for help if you get stuck. - ## Troubleshooting Create an issue if you run into problems not listed here. @@ -85,14 +66,14 @@ That will help us improve this document for everyone who reads it in the future! To build the compiler, you need these installed: -* [Zig](https://ziglang.org/), see below for version -* `libxkbcommon` - macOS seems to have it already; on Ubuntu or Debian you can get it with `apt-get install libxkbcommon-dev` -* On Debian/Ubuntu `sudo apt-get install pkg-config` -* LLVM, see below for version +- [Zig](https://ziglang.org/), see below for version +- On Debian/Ubuntu `sudo apt-get install pkg-config` +- LLVM, see below for version +- [rust](https://rustup.rs/) To run the test suite (via `cargo test`), you additionally need to install: -* [`valgrind`](https://www.valgrind.org/) (needs special treatment to [install on macOS](https://stackoverflow.com/a/61359781) +- [`valgrind`](https://www.valgrind.org/) (needs special treatment to [install on macOS](https://stackoverflow.com/a/61359781) Alternatively, you can use `cargo test --no-fail-fast` or `cargo test -p specific_tests` to skip over the valgrind failures & tests. For debugging LLVM IR, we use [DebugIR](https://github.com/vaivaswatha/debugir). This dependency is only required to build with the `--debug` flag, and for normal development you should be fine without it. @@ -101,7 +82,7 @@ For debugging LLVM IR, we use [DebugIR](https://github.com/vaivaswatha/debugir). You may see an error like this during builds: -``` +```text /usr/bin/ld: cannot find -lxcb-render /usr/bin/ld: cannot find -lxcb-shape /usr/bin/ld: cannot find -lxcb-xfixes @@ -109,51 +90,32 @@ You may see an error like this during builds: If so, you can fix it like so: -``` +```sh sudo apt-get install libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev ``` ### Zig -**version: 0.9.1** + +**version: 0.11.0** For any OS, you can use [`zigup`](https://github.com/marler8997/zigup) to manage zig installations. If you prefer a package manager, you can try the following: -- For MacOS, you can install with `brew install zig` -- For, Ubuntu, you can use Snap, you can install with `snap install zig --classic --beta` -- For other systems, checkout this [page](https://github.com/ziglang/zig/wiki/Install-Zig-from-a-Package-Manager) -If you want to install it manually, you can also download Zig directly [here](https://ziglang.org/download/). Just make sure you download the right version, the bleeding edge master build is the first download link on this page. +- MacOS: `brew install zig@0.11.0` +- Systems with snap (such as Ubuntu): `snap install zig --classic --beta` +- Other systems: refer to the [zig documentation](https://github.com/ziglang/zig/wiki/Install-Zig-from-a-Package-Manager) + +If you want to install it manually, you can [download the binary](https://ziglang.org/download/#release-0.11.0) and place it on your PATH. +Apart from the binary, the archive contains a `lib` folder, which needs to be copied next to the binary. + +> WINDOWS NOTE: when you unpack the Zig archive on windows, the result is nested in an extra directory. The instructions on the zig website will seem to not work. So, double-check that the path to zig executable does not include the same directory name twice. ### LLVM -**version: 13.0.x** -For macOS, you can install LLVM 13 using `brew install llvm@13` and then adding -`$(brew --prefix llvm@13)/bin` to your `PATH`. You can confirm this worked by -running `llc --version` - it should mention "LLVM version 13.0.0" at the top. -You may also need to manually specify a prefix env var like so: -``` -export LLVM_SYS_130_PREFIX=/usr/local/opt/llvm@13 -``` +**version: 16.0.x** -For Ubuntu and Debian: -``` -sudo apt -y install lsb-release software-properties-common gnupg -wget https://apt.llvm.org/llvm.sh -chmod +x llvm.sh -./llvm.sh 13 -``` - -If you use this script, you'll need to add `clang` to your `PATH`. -By default, the script installs it as `clang-13`. You can address this with symlinks like so: - -``` -sudo ln -s /usr/bin/clang-13 /usr/bin/clang -``` - -There are also alternative installation options at http://releases.llvm.org/download.html - -[Troubleshooting](#troubleshooting) +See below for operating system specific installation instructions. ### Building @@ -161,9 +123,41 @@ Use `cargo build` to build the whole project. Use `cargo run help` to see all subcommands. To use the `repl` subcommand, execute `cargo run repl`. +The default is a developer build. For an optimized build, use: + +``` +cargo build --release --bin roc +``` + ### LLVM installation on Linux -For a current list of all dependency versions and their names in apt, see the Earthfile. +For Ubuntu and Debian: + +```sh +sudo apt -y install lsb-release software-properties-common gnupg +wget https://apt.llvm.org/llvm.sh +chmod +x llvm.sh +./llvm.sh 16 +``` + +If you use this script, you'll need to add `clang` to your `PATH`. +By default, the script installs it as `clang-16`. You can address this with symlinks like so: + +```sh +sudo ln -s /usr/bin/clang-16 /usr/bin/clang +``` + +There are also alternative installation options at + +[Troubleshooting](#troubleshooting) + +For Fedora: + +```sh +sudo dnf install llvm16 llvm16-devel +``` + +#### LLVM Linux troubleshooting On some Linux systems we've seen the error "failed to run custom build command for x11". On Ubuntu, running `sudo apt install pkg-config cmake libx11-dev` fixed this. @@ -171,44 +165,57 @@ On Ubuntu, running `sudo apt install pkg-config cmake libx11-dev` fixed this. If you encounter `cannot find -lz` run `sudo apt install zlib1g-dev`. If you encounter: -``` -error: No suitable version of LLVM was found system-wide or pointed - to by LLVM_SYS_130_PREFIX. -``` -Add `export LLVM_SYS_130_PREFIX=/usr/lib/llvm-13` to your `~/.bashrc` or equivalent file for your shell. -### LLVM installation on macOS +```text +error: No suitable version of LLVM was found system-wide or pointed + to by LLVM_SYS_160_PREFIX. +``` + +Add `export LLVM_SYS_160_PREFIX=/usr/lib/llvm-16` to your `~/.bashrc` or equivalent file for your shell. + +### LLVM installation on MacOS + +For macOS, you can install LLVM 16 using `brew install llvm@16` and then adding +`$(brew --prefix llvm@16)/bin` to your `PATH`. You can confirm this worked by +running `llc --version` - it should mention "LLVM version 16.0.x" at the top. +You may also need to manually specify a prefix env var like so: + +```sh +export LLVM_SYS_160_PREFIX=$(brew --prefix llvm@16) +``` + +#### LLVM MacOS troubleshooting If installing LLVM fails, it might help to run `sudo xcode-select -r` before installing again. It might also be useful to add these exports to your shell: -``` +```sh export LDFLAGS="-L/usr/local/opt/llvm/lib -Wl,-rpath,/usr/local/opt/llvm/lib" export CPPFLAGS="-I/usr/local/opt/llvm/include" ``` ### LLVM installation on Windows -**Warning** While `cargo build` works on windows, linking roc programs does not yet, see issue #2608. This also means the repl, the editor and many tests will not work on windows. -Installing LLVM's prebuilt binaries doesn't seem to be enough for the `llvm-sys` crate that Roc depends on, so I had to follow the steps below: +**Warning** While `cargo build` works on windows, linking roc programs does not yet, see issue #2608. This also means the repl, and many tests will not work on windows. +The official LLVM pre-built binaries for Windows lack features that roc needs. Instead: -1. I downloaded and installed [Build Tools for Visual Studio 2019](https://visualstudio.microsoft.com/thank-you-downloading-visual-studio/?sku=BuildTools&rel=16) (a full Visual Studio install should work too; the Build Tools are just the CLI tools, which is all I wanted) -1. Download the custom LLVM 7z archive [here](https://github.com/PLC-lang/llvm-package-windows/releases/tag/v13.0.1). +1. Download the custom LLVM 7z archive [here](https://github.com/roc-lang/llvm-package-windows/releases/download/v16.0.6/LLVM-16.0.6-win64.7z). 1. [Download 7-zip](https://www.7-zip.org/) to be able to extract this archive. -1. Extract the 7z file to where you want to permanently keep the folder. -1. In powershell, set the `LLVM_SYS_130_PREFIX` environment variable (check [here](https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_environment_variables?view=powershell-7.2#saving-changes-to-environment-variables) to make this a permanent environment variable): -``` -[Environment]::SetEnvironmentVariable( - "Path", - [Environment]::GetEnvironmentVariable("Path", "User") + ";C:\Users\anton\Downloads\LLVM-13.0.1-win64\bin", - "User" -) -``` +1. Extract the 7z file to where you want to permanently keep the folder. We recommend you pick a path without any spaces in it. +1. In powershell, set the `LLVM_SYS_160_PREFIX` environment variable (check [here](https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_environment_variables?view=powershell-7.2#saving-environment-variables-with-the-system-control-panel) to make this a permanent environment variable): +```text +<# ! Replace YOUR_USERNAME ! #> +$env:LLVM_SYS_160_PREFIX = 'C:\Users\YOUR_USERNAME\Downloads\LLVM-16.0.6-win64' +``` Once all that was done, `cargo build` ran successfully for Roc! +#### LLVM Windows troubleshooting + +If you see the build failing because some internal file is not available, it might be your anti-virus program. Cargo's behavior is kind of similar to a virus (downloading files from the internet, creating many files), and this has been known to cause problems. + ### Build speed on WSL/WSL2 If your Roc project folder is in the Windows filesystem but you're compiling from Linux, rebuilds may be as much as 20x slower than they should be! @@ -221,15 +228,15 @@ makes build times a lot faster, and I highly recommend it. Create `~/.cargo/config.toml` if it does not exist and add this to it: -``` +```toml [build] # Link with lld, per https://github.com/rust-lang/rust/issues/39915#issuecomment-538049306 # Use target-cpu=native, per https://deterministic.space/high-performance-rust.html rustflags = ["-C", "link-arg=-fuse-ld=lld", "-C", "target-cpu=native"] ``` -Then install `lld` version 13 (e.g. with `$ sudo apt-get install lld-13`) +Then install `lld` version 16 (e.g. with `$ sudo apt-get install lld-16`) and add make sure there's a `ld.lld` executable on your `PATH` which -is symlinked to `lld-13`. +is symlinked to `lld-16`. That's it! Enjoy the faster builds. diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 1040c40543..5f1ea8fc70 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -8,20 +8,20 @@ In the interest of fostering an open and welcoming environment, we as participan Examples of behavior that contributes to creating a positive environment include: -* Demonstrating empathy and kindness toward other people -* Being respectful of differing opinions, viewpoints, and experiences -* Kindly giving and gracefully accepting constructive feedback -* Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience -* Focusing on what is best not just for us as individuals, but for the overall +- Demonstrating empathy and kindness toward other people +- Being respectful of differing opinions, viewpoints, and experiences +- Kindly giving and gracefully accepting constructive feedback +- Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience +- Focusing on what is best not just for us as individuals, but for the overall community Examples of unacceptable behavior include: -* The use of sexualized language or imagery, and sexual attention or advances of any kind -* Trolling, insulting or derogatory comments, and personal or political attacks -* Public or private harassment -* Publishing others' private information, such as a physical or email address, without their explicit permission -* Telling others to be less sensitive, or that they should not feel hurt or offended by something +- The use of sexualized language or imagery, and sexual attention or advances of any kind +- Trolling, insulting or derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or email address, without their explicit permission +- Telling others to be less sensitive, or that they should not feel hurt or offended by something ## Enforcement Responsibilities @@ -41,4 +41,4 @@ Moderators who do not follow or enforce the Code of Conduct in good faith may fa ## Attribution -This Code of Conduct is adapted from the Contributor Covenant, version 1.4, available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html +This Code of Conduct is adapted from the Contributor Covenant, version 1.4, available at diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 54dd29a18f..2162635633 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,45 +2,96 @@ ## Code of Conduct -We are committed to providing a friendly, safe and welcoming environment for all. Make sure to take a look at the [Code of Conduct](CodeOfConduct.md)! +We are committed to providing a friendly, safe and welcoming environment for all. Make sure to take a look at the [Code of Conduct](CODE_OF_CONDUCT.md)! + +## How to contribute + +All contributions are appreciated! Typo fixes, bug fixes, feature requests, +bug reports are all helpful for the project. + +If you are looking for a good place to start, consider reaching out on the `#contributing` channel on [Roc Zulip][roc-zulip]. +Before making your first pull request, definitely talk to an existing contributor on [Roc Zulip][roc-zulip] first about what you plan to do! This can not only avoid duplicated effort, it can also avoid making a whole PR only to discover it won't be accepted because the change doesn't fit with the goals of the language's design or implementation. + +If you are interested in larger, implementation- or research-heavy projects +related to Roc, check out [Roc Project Ideas][project-ideas] and reach out to us +on Zulip! These projects may be suitable for academic theses, independent +research, or even just valuable projects to learn from and improve Roc with. ## Building from Source -Check [Build from source](BUILDING_FROM_SOURCE.md) for instructions. +Check [Building from source](BUILDING_FROM_SOURCE.md) for instructions. ## Running Tests -Most contributors execute the following commands befor pushing their code: -``` +Most contributors execute the following commands before pushing their code: + +```sh cargo test cargo fmt --all -- --check cargo clippy --workspace --tests -- --deny warnings ``` + Execute `cargo fmt --all` to fix the formatting. -If you want to run all tests and checks as they are run on CI, [install earthly](https://earthly.dev/get-earthly) and run: -``` -earthly +test-all +## Generating Docs + +If you make changes to [Roc's Standard Library](https://www.roc-lang.org/builtins/Str), you can add comments to the code following [the CommonMark Spec](https://spec.commonmark.org/current/) to further explain your intentions. You can view these changes locally with: + +```sh +cargo run docs crates/compiler/builtins/roc/main.roc ``` -Earthly may temporarily use a lot of disk space, up to 90 GB. This disk space is available again after rebooting. +This command will generate the documentation in the [`generated-docs`](generated-docs) directory. ## Contribution Tips +- If you've never made a pull request on github before, [this](https://www.freecodecamp.org/news/how-to-make-your-first-pull-request-on-github-3/) will be a good place to start. - Create an issue if the purpose of a struct/field/type/function/... is not immediately clear from its name or nearby comments. -- You find good first issues [here](https://github.com/rtfeldman/roc/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22). -- Before making your first pull request, definitely talk to an existing contributor on [Roc Zulip](https://roc.zulipchat.com) first about what you plan to do! This can not only avoid duplicated effort, it can also avoid making a whole PR only to discover it won't be accepted because the change doesn't fit with the goals of the language's design or implementation. -- It's a good idea to open a work-in-progress pull request as you begin working on something. This way, others can see that you're working on it, which avoids duplicate effort, and others can give feedback sooner rather than later if they notice a problem in the direction things are going. Be sure to include "WIP" in the title of the PR as long as it's not ready for review! -- Make sure to create a branch on the roc repository for your changes. We do not allow CI to be run on forks for security. -- All your commits need to be signed to prevent impersonation: - 1. If you have a Yubikey, follow [guide 1](https://dev.to/paulmicheli/using-your-yubikey-to-get-started-with-gpg-3h4k), [guide 2](https://dev.to/paulmicheli/using-your-yubikey-for-signed-git-commits-4l73) and skip the steps below. - 2. [Make a key to sign your commits.](https://docs.github.com/en/authentication/managing-commit-signature-verification/generating-a-new-gpg-key). - 3. [Configure git to use your key.](https://docs.github.com/en/authentication/managing-commit-signature-verification/telling-git-about-your-signing-key) - 4. Make git sign your commits automatically: - ``` - git config --global commit.gpgsign true - ``` +- You can find good first issues [here][good-first-issues]. Once you have gained some experience you can take a look at the [intermediate issues](https://github.com/roc-lang/roc/issues?q=is%3Aopen+is%3Aissue+label%3A%22intermediate+issue%22). +- [Fork](https://github.com/roc-lang/roc/fork) the repo so that you can apply your changes first on your own copy of the roc repo. +- It's a good idea to open a draft pull request as you begin working on something. This way, others can see that you're working on it, which avoids duplicate effort, and others can give feedback sooner rather than later if they notice a problem in the direction things are going. Click the button "ready for review" when it's ready. + +### Commit signing + +All your commits need to be signed [to prevent impersonation](https://dev.to/martiliones/how-i-got-linus-torvalds-in-my-contributors-on-github-3k4g). Check out [our guide for commit signing](devtools/signing.md). + +#### Commit signing on NixOS + +On NixOS pinentry can cause problems, the following setup works well for those with a KDE desktop. From `/etc/nixos/configuration.nix`: +``` +programs.gnupg.agent = { + enable = true; + pinentryFlavor = "qt"; + enableSSHSupport = true; + }; +``` + +### Forgot to sign commits? + +You can view your commits on github, those without the "Verified" badge still need to be signed. +If any of those is a merge commit, follow [these steps](https://stackoverflow.com/a/9958215/4200103) instead of the ones below. + +If you have only one commit, running `git commit --amend --no-edit -S` would sign the latest commit 🚀. + +In case you have multiple commits, you can sign them in two ways: + 1. Switching to interactive rebase mode and editing the file: + - Enter into interactive mode, by running `git rebase -i HEAD~n` where `n` is the number of commits up to the most current commit you would like to see. + - This would display a set of commits in a text file like below: + ``` + pick hash2 commit message 2 + pick hash1 commit message 1 + ``` + - On a new line below a commit you want to sign, add `exec git commit --amend --no-edit -S`. Do this for all your unsigned commits. + 2. Or run git rebase recursively: + - Find the oldest commit you want to sign, using the `git log --show-signature` command. + - Run the command `git rebase --exec 'git commit --amend --no-edit -n -S' -i HASH` which would sign all commits up to commit `HASH`. + +If you already pushed unsigned commits, you may have to do a force push with `git push origin -f `. ## Can we do better? Feel free to open an issue if you think this document can be improved or is unclear in any way. + +[roc-zulip]: https://roc.zulipchat.com +[good-first-issues]: https://github.com/roc-lang/roc/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22 +[project-ideas]: https://docs.google.com/document/d/1mMaxIi7vxyUyNAUCs98d68jYj6C9Fpq4JIZRU735Kwg/edit?usp=sharing diff --git a/Cargo.lock b/Cargo.lock index 95dacacb39..63f6268c72 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,29 +2,13 @@ # It is not intended for manual editing. version = 3 -[[package]] -name = "ab_glyph" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24606928a235e73cdef55a0c909719cadd72fce573e5713d58cb2952d8f5794c" -dependencies = [ - "ab_glyph_rasterizer", - "owned_ttf_parser 0.15.0", -] - -[[package]] -name = "ab_glyph_rasterizer" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a13739d7177fbd22bb0ed28badfff9f372f8bef46c863db4e1c6248f6b223b6e" - [[package]] name = "addr2line" -version = "0.17.0" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9ecd88a8c8378ca913a680cd98f0f13ac67383d35993f86c90a70e3f137816b" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" dependencies = [ - "gimli 0.26.1", + "gimli", ] [[package]] @@ -35,89 +19,123 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "ahash" -version = "0.7.6" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" +checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" dependencies = [ - "getrandom", + "cfg-if", "once_cell", "version_check", ] [[package]] name = "aho-corasick" -version = "0.7.18" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" +checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" dependencies = [ "memchr", ] [[package]] -name = "alsa" -version = "0.6.0" +name = "aligned" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5915f52fe2cf65e83924d037b6c5290b7cee097c6b5c8700746e6168a343fd6b" +checksum = "80a21b9440a626c7fc8573a9e3d3a06b75c7c97754c2949bc7857b90353ca655" dependencies = [ - "alsa-sys", - "bitflags", - "libc", - "nix 0.23.1", + "as-slice", ] [[package]] -name = "alsa-sys" -version = "0.3.1" +name = "alloc-no-stdlib" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db8fee663d06c4e303404ef5f40488a53e062f89ba8bfed81f42325aafad1527" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" dependencies = [ "libc", - "pkg-config", ] [[package]] -name = "andrew" -version = "0.3.1" +name = "anstream" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c4afb09dd642feec8408e33f92f3ffc4052946f6b20f32fb99c1f58cd4fa7cf" +checksum = "2ab91ebe16eb252986481c5b62f6098f3b698a45e34b5b98200cf20dd2484a44" dependencies = [ - "bitflags", - "rusttype", - "walkdir", - "xdg", - "xml-rs", + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "utf8parse", ] [[package]] -name = "ansi_term" -version = "0.12.1" +name = "anstyle" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" +checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" + +[[package]] +name = "anstyle-parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "317b9a89c1868f5ea6ff1d9539a69f45dffc21ce321ac1fd1160dfa48c8e2140" dependencies = [ - "winapi", + "utf8parse", ] [[package]] -name = "approx" -version = "0.4.0" +name = "anstyle-query" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f2a05fd1bd10b2527e20a2cd32d8873d115b8b39fe219ee25f42a8aca6ba278" +checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" dependencies = [ - "num-traits", + "windows-sys 0.48.0", ] [[package]] -name = "approx" -version = "0.5.1" +name = "anstyle-wincon" +version = "3.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" +checksum = "f0699d10d2f4d628a98ee7b57b289abbc98ff3bad977cb3152709d4bf2330628" dependencies = [ - "num-traits", + "anstyle", + "windows-sys 0.48.0", ] [[package]] name = "arena-pool" -version = "0.1.0" +version = "0.0.1" +dependencies = [ + "roc_error_macros", +] + +[[package]] +name = "arrayref" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545" [[package]] name = "arrayvec" @@ -127,17 +145,28 @@ checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" [[package]] name = "arrayvec" -version = "0.7.2" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6" +checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" [[package]] -name = "ash" -version = "0.33.3+1.2.191" +name = "as-slice" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc4f1d82f164f838ae413296d1131aa6fa79b917d25bebaa7033d25620c09219" +checksum = "516b6b4f0e40d50dcda9365d53964ec74560ad4284da2e7fc97122cd83174516" dependencies = [ - "libloading 0.7.3", + "stable_deref_trait", +] + +[[package]] +name = "async-trait" +version = "0.1.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b2d0f03b3640e3a630367e40c468cb7f309529c708ed1d88597047b0e7c6ef7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.38", ] [[package]] @@ -146,11 +175,23 @@ version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" dependencies = [ - "hermit-abi", + "hermit-abi 0.1.19", "libc", "winapi", ] +[[package]] +name = "auto_impl" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7862e21c893d65a1650125d157eaeec691439379a1cee17ee49031b79236ada4" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "autocfg" version = "1.1.0" @@ -159,24 +200,39 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "backtrace" -version = "0.3.65" +version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11a17d453482a265fd5f8479f2a3f405566e6ca627837aaddb85af8b1ab8ef61" +checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" dependencies = [ "addr2line", "cc", - "cfg-if 1.0.0", + "cfg-if", "libc", "miniz_oxide", - "object 0.28.3", + "object 0.32.1", "rustc-demangle", ] [[package]] -name = "base-x" -version = "0.2.10" +name = "base64" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc19a4937b4fbd3fe3379793130e42060d10627a360f2127802b10b87e7baf74" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[package]] +name = "base64" +version = "0.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ba43ea6f343b788c8764558649e08df62f86c6ef251fdaeb1ffd010a9ae50a2" + +[[package]] +name = "base64-url" +version = "1.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a99c239d0c7e77c85dddfa9cebce48704b3c49550fcd3b84dd637e4484899f" +dependencies = [ + "base64 0.13.1", +] [[package]] name = "bincode" @@ -187,30 +243,11 @@ dependencies = [ "serde", ] -[[package]] -name = "bindgen" -version = "0.59.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bd2a9a458e8f4304c52c43ebb0cfbd520289f8379a52e329a38afda99bf8eb8" -dependencies = [ - "bitflags", - "cexpr", - "clang-sys", - "lazy_static", - "lazycell", - "peeking_take_while", - "proc-macro2", - "quote", - "regex", - "rustc-hash", - "shlex", -] - [[package]] name = "bit-set" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e11e16035ea35e4e5997b393eacbf6f63983188f7a2ad25bfb13465f5ad59de" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" dependencies = [ "bit-vec", ] @@ -227,6 +264,12 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bitflags" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" + [[package]] name = "bitmaps" version = "2.1.0" @@ -238,149 +281,96 @@ dependencies = [ [[package]] name = "bitvec" -version = "0.22.3" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5237f00a8c86130a0cc317830e558b966dd7850d48a953d998c813f01a41b527" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" dependencies = [ - "funty 1.2.0", - "radium 0.6.2", + "funty", + "radium", "tap", - "wyz 0.4.0", + "wyz", ] [[package]] -name = "bitvec" -version = "1.0.0" +name = "blake3" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1489fcb93a5bb47da0462ca93ad252ad6af2145cce58d10d46a83931ba9f016b" +checksum = "0231f06152bf547e9c2b5194f247cd97aacf6dcd8b15d8e5ec0663f64580da87" dependencies = [ - "funty 2.0.0", - "radium 0.7.0", - "tap", - "wyz 0.5.0", + "arrayref", + "arrayvec 0.7.4", + "cc", + "cfg-if", + "constant_time_eq", ] [[package]] -name = "block" -version = "0.1.6" +name = "brotli" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" - -[[package]] -name = "block-buffer" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0940dc441f31689269e10ac70eb1002a3a1d3ad1390e030043662eb7fe4688b" +checksum = "516074a47ef4bce09577a3b379392300159ce5b1ba2e501ff1c819950066100f" dependencies = [ - "block-padding", - "byte-tools", - "byteorder", - "generic-array 0.12.4", + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", ] [[package]] -name = "block-buffer" -version = "0.9.0" +name = "brotli-decompressor" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +checksum = "da74e2b81409b1b743f8f0c62cc6254afefb8b8e50bbfe3735550f7aeefa3448" dependencies = [ - "generic-array 0.14.5", -] - -[[package]] -name = "block-padding" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa79dedbb091f449f1f39e53edf88d5dbe95f895dae6135a8d7b881fb5af73f5" -dependencies = [ - "byte-tools", -] - -[[package]] -name = "bstr" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223" -dependencies = [ - "lazy_static", - "memchr", - "regex-automata", - "serde", + "alloc-no-stdlib", + "alloc-stdlib", ] [[package]] name = "bumpalo" -version = "3.9.1" +version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4a45a46ab1f2412e53d3a0ade76ffad2025804294569aae387231a0cd6e0899" +checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" [[package]] -name = "byte-tools" -version = "0.3.1" +name = "byte-unit" +version = "4.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7" - -[[package]] -name = "bytecheck" -version = "0.6.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a31f923c2db9513e4298b72df143e6e655a759b3d6a0966df18f81223fff54f" +checksum = "da78b32057b8fdfc352504708feeba7216dcd65a2c9ab02978cbd288d1279b6c" dependencies = [ - "bytecheck_derive", - "ptr_meta", -] - -[[package]] -name = "bytecheck_derive" -version = "0.6.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edb17c862a905d912174daa27ae002326fff56dc8b8ada50a0a5f0976cb174f0" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "bytemuck" -version = "1.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdead85bdec19c194affaeeb670c0e41fe23de31459efd1c174d049269cf02cc" -dependencies = [ - "bytemuck_derive", -] - -[[package]] -name = "bytemuck_derive" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562e382481975bc61d11275ac5e62a19abd00b0547d99516a415336f183dcd0e" -dependencies = [ - "proc-macro2", - "quote", - "syn", + "serde", + "utf8-width", ] [[package]] name = "byteorder" -version = "1.4.3" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.1.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" +checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" [[package]] -name = "calloop" -version = "0.6.5" +name = "capstone" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b036167e76041694579972c28cf4877b4f92da222560ddb49008937b6a6727c" +checksum = "1097e608594dad3bad608295567f757742b883606fe150faf7a9740b849730d8" dependencies = [ - "log", - "nix 0.18.0", + "capstone-sys", + "libc", +] + +[[package]] +name = "capstone-sys" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e7f651d5ec4c2a2e6c508f2c8032655003cd728ec85663e9796616990e25b5a" +dependencies = [ + "cc", + "libc", ] [[package]] @@ -389,39 +379,18 @@ version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c24dab4283a142afa2fdca129b80ad2c6284e073930f964c3a1293c225ee39a" dependencies = [ - "rustc_version 0.4.0", + "rustc_version", ] [[package]] name = "cc" -version = "1.0.73" +version = "1.0.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" dependencies = [ - "jobserver", + "libc", ] -[[package]] -name = "cesu8" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" - -[[package]] -name = "cexpr" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" -dependencies = [ - "nom", -] - -[[package]] -name = "cfg-if" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" - [[package]] name = "cfg-if" version = "1.0.0" @@ -429,30 +398,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] -name = "cfg_aliases" -version = "0.1.1" +name = "chrono" +version = "0.4.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" - -[[package]] -name = "cgmath" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a98d30140e3296250832bbaaff83b27dcd6fa3cc70fb6f1f3e5c9c0023b5317" +checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" dependencies = [ - "approx 0.4.0", + "android-tzdata", + "iana-time-zone", + "js-sys", "num-traits", -] - -[[package]] -name = "clang-sys" -version = "1.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cc00842eed744b858222c4c9faf7243aafc6d33f92f96935263ef4d8a41ce21" -dependencies = [ - "glob", - "libc", - "libloading 0.7.3", + "wasm-bindgen", + "windows-targets 0.48.5", ] [[package]] @@ -461,90 +417,73 @@ version = "2.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" dependencies = [ - "bitflags", - "textwrap 0.11.0", + "bitflags 1.3.2", + "textwrap", "unicode-width", ] [[package]] name = "clap" -version = "3.1.17" +version = "4.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47582c09be7c8b32c0ab3a6181825ababb713fde6fff20fc573a3870dd45c6a0" +checksum = "d04704f56c2cde07f43e8e2c154b43f216dc5c92fc98ada720177362f953b956" dependencies = [ - "atty", - "bitflags", + "clap_builder", "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e231faeaca65ebd1ea3c737966bf858971cd38c3849107aa3ea7de90a804e45" +dependencies = [ + "anstream", + "anstyle", "clap_lex", - "indexmap", - "lazy_static", - "strsim 0.10.0", - "termcolor", - "textwrap 0.15.0", + "strsim", ] [[package]] name = "clap_derive" -version = "3.1.7" +version = "4.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3aab4734e083b809aaf5794e14e756d1c798d2c69c7f7de7a09a2f5214993c1" +checksum = "0862016ff20d69b84ef8247369fabf5c008a7417002411897d40ee1f4532b873" dependencies = [ "heck", - "proc-macro-error", "proc-macro2", "quote", - "syn", + "syn 2.0.38", ] [[package]] name = "clap_lex" -version = "0.2.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a37c35f1112dad5e6e0b1adaff798507497a18fceeb30cceb3bae7d1427b9213" -dependencies = [ - "os_str_bytes", -] - -[[package]] -name = "claxon" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bfbf56724aa9eca8afa4fcfadeb479e722935bb2a0900c2d37e0cc477af0688" +checksum = "cd7cc57abe963c6d3b9d8be5b06ba7c8957a930305ca90304f24ef040aa6f961" [[package]] name = "cli_utils" -version = "0.1.0" +version = "0.0.1" dependencies = [ "bumpalo", - "const_format", - "criterion 0.3.5 (git+https://github.com/Anton-4/criterion.rs)", + "criterion", "rlimit", - "roc_cli", "roc_collections", + "roc_command_utils", "roc_load", "roc_module", "roc_reporting", "serde", "serde-xml-rs", - "strip-ansi-escapes", "tempfile", ] [[package]] name = "clipboard-win" -version = "3.1.1" +version = "4.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fdf5e01086b6be750428ba4a40619f847eb2e95756eee84b18e06e5f0b50342" -dependencies = [ - "lazy-bytes-cast", - "winapi", -] - -[[package]] -name = "clipboard-win" -version = "4.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f3e1238132dc01f081e1cbb9dace14e5ef4c3a51ee244bd982275fb514605db" +checksum = "7191c27c2357d9b7ef96baac1773290d4ca63b24205b82a3fd8a0637afcf0362" dependencies = [ "error-code", "str-buf", @@ -552,76 +491,21 @@ dependencies = [ ] [[package]] -name = "cocoa" -version = "0.24.0" +name = "colorchoice" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f63902e9223530efb4e26ccd0cf55ec30d592d3b42e21a28defc42a9586e832" -dependencies = [ - "bitflags", - "block", - "cocoa-foundation", - "core-foundation 0.9.3", - "core-graphics 0.22.3", - "foreign-types", - "libc", - "objc", -] +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" [[package]] -name = "cocoa-foundation" -version = "0.1.0" +name = "console" +version = "0.15.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ade49b65d560ca58c403a479bb396592b155c0185eada742ee323d1d68d6318" +checksum = "c926e00cc70edefdc64d3a5ff31cc65bb97a3460097762bd23afb4d8145fccf8" dependencies = [ - "bitflags", - "block", - "core-foundation 0.9.3", - "core-graphics-types", - "foreign-types", - "libc", - "objc", -] - -[[package]] -name = "codespan-reporting" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" -dependencies = [ - "termcolor", - "unicode-width", -] - -[[package]] -name = "colored" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3616f750b84d8f0de8a58bda93e08e2a81ad3f523089b05f1dffecab48c6cbd" -dependencies = [ - "atty", + "encode_unicode 0.3.6", "lazy_static", - "winapi", -] - -[[package]] -name = "combine" -version = "4.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a604e93b79d1808327a6fca85a6f2d69de66461e7620f5a4cbf5fb4d1d7c948" -dependencies = [ - "bytes", - "memchr", -] - -[[package]] -name = "confy" -version = "0.4.0" -source = "git+https://github.com/rust-cli/confy#642822413139ce38a96b916190e8c7fd5b88e814" -dependencies = [ - "directories-next", - "serde", - "serde_yaml", - "thiserror", + "libc", + "windows-sys 0.45.0", ] [[package]] @@ -630,30 +514,24 @@ version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "wasm-bindgen", ] -[[package]] -name = "const_fn" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbdcdcb6d86f71c5e97409ad45898af11cbc995b4ee8112d59095a28d376c935" - [[package]] name = "const_format" -version = "0.2.23" +version = "0.2.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0936ffe6d0c8d6a51b3b0a73b2acbe925d786f346cf45bfddc8341d79fb7dc8a" +checksum = "e3a214c7af3d04997541b18d432afaff4c455e79e2029079647e72fc2bd27673" dependencies = [ "const_format_proc_macros", ] [[package]] name = "const_format_proc_macros" -version = "0.2.22" +version = "0.2.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef196d5d972878a48da7decb7686eded338b4858fbabeed513d63a7c98b2b82d" +checksum = "c7f6ff08fd20f4f299298a28e2dfa8a8ba1036e6cd2460ac1de7b425d76f2500" dependencies = [ "proc-macro2", "quote", @@ -661,34 +539,10 @@ dependencies = [ ] [[package]] -name = "copyless" -version = "0.1.5" +name = "constant_time_eq" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2df960f5d869b2dd8532793fde43eb5427cceb126c929747a26823ab0eeb536" - -[[package]] -name = "copypasta" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4423d79fed83ebd9ab81ec21fa97144300a961782158287dc9bf7eddac37ff0b" -dependencies = [ - "clipboard-win 3.1.1", - "objc", - "objc-foundation", - "objc_id", - "smithay-clipboard", - "x11-clipboard", -] - -[[package]] -name = "core-foundation" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57d24c7a13c43e870e37c1556b74555437870a04514f7685f5b354e090567171" -dependencies = [ - "core-foundation-sys 0.7.0", - "libc", -] +checksum = "f7144d30dcf0fafbce74250a3963025d8d52177934239851c917d29f1df280c2" [[package]] name = "core-foundation" @@ -696,184 +550,15 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" dependencies = [ - "core-foundation-sys 0.8.3", + "core-foundation-sys", "libc", ] [[package]] name = "core-foundation-sys" -version = "0.7.0" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3a71ab494c0b5b860bdc8407ae08978052417070c2ced38573a9157ad75b8ac" - -[[package]] -name = "core-foundation-sys" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" - -[[package]] -name = "core-graphics" -version = "0.19.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3889374e6ea6ab25dba90bb5d96202f61108058361f6dc72e8b03e6f8bbe923" -dependencies = [ - "bitflags", - "core-foundation 0.7.0", - "foreign-types", - "libc", -] - -[[package]] -name = "core-graphics" -version = "0.22.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2581bbab3b8ffc6fcbd550bf46c355135d16e9ff2a6ea032ad6b9bf1d7efe4fb" -dependencies = [ - "bitflags", - "core-foundation 0.9.3", - "core-graphics-types", - "foreign-types", - "libc", -] - -[[package]] -name = "core-graphics-types" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a68b68b3446082644c91ac778bf50cd4104bfb002b5a6a7c44cca5a2c70788b" -dependencies = [ - "bitflags", - "core-foundation 0.9.3", - "foreign-types", - "libc", -] - -[[package]] -name = "core-video-sys" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34ecad23610ad9757664d644e369246edde1803fcb43ed72876565098a5d3828" -dependencies = [ - "cfg-if 0.1.10", - "core-foundation-sys 0.7.0", - "core-graphics 0.19.2", - "libc", - "objc", -] - -[[package]] -name = "coreaudio-rs" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11894b20ebfe1ff903cbdc52259693389eea03b94918a2def2c30c3bf227ad88" -dependencies = [ - "bitflags", - "coreaudio-sys", -] - -[[package]] -name = "coreaudio-sys" -version = "0.2.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3dff444d80630d7073077d38d40b4501fd518bd2b922c2a55edcc8b0f7be57e6" -dependencies = [ - "bindgen", -] - -[[package]] -name = "cpal" -version = "0.13.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74117836a5124f3629e4b474eed03e479abaf98988b4bb317e29f08cfe0e4116" -dependencies = [ - "alsa", - "core-foundation-sys 0.8.3", - "coreaudio-rs", - "jni", - "js-sys", - "lazy_static", - "libc", - "mach", - "ndk 0.6.0", - "ndk-glue 0.6.2", - "nix 0.23.1", - "oboe", - "parking_lot 0.11.2", - "stdweb 0.1.3", - "thiserror", - "web-sys", - "winapi", -] - -[[package]] -name = "cpufeatures" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59a6001667ab124aebae2a495118e11d30984c3a653e99d86d58971708cf5e4b" -dependencies = [ - "libc", -] - -[[package]] -name = "cranelift-bforest" -version = "0.76.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e6bea67967505247f54fa2c85cf4f6e0e31c4e5692c9b70e4ae58e339067333" -dependencies = [ - "cranelift-entity", -] - -[[package]] -name = "cranelift-codegen" -version = "0.76.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48194035d2752bdd5bdae429e3ab88676e95f52a2b1355a5d4e809f9e39b1d74" -dependencies = [ - "cranelift-bforest", - "cranelift-codegen-meta", - "cranelift-codegen-shared", - "cranelift-entity", - "gimli 0.25.0", - "log", - "regalloc", - "smallvec", - "target-lexicon", -] - -[[package]] -name = "cranelift-codegen-meta" -version = "0.76.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "976efb22fcab4f2cd6bd4e9913764616a54d895c1a23530128d04e03633c555f" -dependencies = [ - "cranelift-codegen-shared", - "cranelift-entity", -] - -[[package]] -name = "cranelift-codegen-shared" -version = "0.76.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dabb5fe66e04d4652e434195b45ae65b5c8172d520247b8f66d8df42b2b45dc" - -[[package]] -name = "cranelift-entity" -version = "0.76.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3329733e4d4b8e91c809efcaa4faee80bf66f20164e3dd16d707346bd3494799" - -[[package]] -name = "cranelift-frontend" -version = "0.76.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "279afcc0d3e651b773f94837c3d581177b348c8d69e928104b2e9fccb226f921" -dependencies = [ - "cranelift-codegen", - "log", - "smallvec", - "target-lexicon", -] +checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" [[package]] name = "crc32fast" @@ -881,50 +566,24 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", ] [[package]] name = "criterion" version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1604dafd25fba2fe2d5895a9da139f8dc9b319a5fe5354ca137cbbce4e178d10" +source = "git+https://github.com/Anton-4/criterion.rs?rev=30ea0c5#30ea0c592d7423ed79772234fab13108d1f8de77" dependencies = [ "atty", "cast", "clap 2.34.0", - "criterion-plot 0.4.4", + "criterion-plot", "csv", - "itertools 0.10.3", + "itertools 0.10.5", "lazy_static", "num-traits", "oorandom", - "plotters 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", - "rayon", - "regex", - "serde", - "serde_cbor", - "serde_derive", - "serde_json", - "tinytemplate", - "walkdir", -] - -[[package]] -name = "criterion" -version = "0.3.5" -source = "git+https://github.com/Anton-4/criterion.rs#3e46ad2b234e36928fb5234d36cf53b5837cbb87" -dependencies = [ - "atty", - "cast", - "clap 2.34.0", - "criterion-plot 0.4.3", - "csv", - "itertools 0.10.3", - "lazy_static", - "num-traits", - "oorandom", - "plotters 0.3.1 (git+https://github.com/Anton-4/plotters)", + "plotters", "rayon", "regex", "serde", @@ -938,29 +597,19 @@ dependencies = [ [[package]] name = "criterion-plot" version = "0.4.3" -source = "git+https://github.com/Anton-4/criterion.rs#3e46ad2b234e36928fb5234d36cf53b5837cbb87" +source = "git+https://github.com/Anton-4/criterion.rs?rev=30ea0c5#30ea0c592d7423ed79772234fab13108d1f8de77" dependencies = [ "cast", "itertools 0.9.0", ] -[[package]] -name = "criterion-plot" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d00996de9f2f7559f7f4dc286073197f83e92256a59ed395f9aac01fe717da57" -dependencies = [ - "cast", - "itertools 0.10.3", -] - [[package]] name = "crossbeam" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ae5588f6b3c3cb05239e90bd110f257254aecd01e4635400391aeae07497845" +checksum = "2801af0d36612ae591caa9568261fddce32ce6e08a7275ea334a06a4ad021a2c" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "crossbeam-channel", "crossbeam-deque", "crossbeam-epoch", @@ -970,253 +619,136 @@ dependencies = [ [[package]] name = "crossbeam-channel" -version = "0.5.4" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aaa7bd5fb665c6864b5f963dd9097905c54125909c7aa94c9e18507cdbe6c53" +checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "crossbeam-utils", ] [[package]] name = "crossbeam-deque" -version = "0.8.1" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6455c0ca19f0d2fbf751b908d5c55c1f5cbc65e03c4225427254b46890bdde1e" +checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "crossbeam-epoch", "crossbeam-utils", ] [[package]] name = "crossbeam-epoch" -version = "0.9.8" +version = "0.9.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1145cf131a2c6ba0615079ab6a638f7e1973ac9c2634fcbeaaad6114246efe8c" +checksum = "ae211234986c545741a7dc064309f67ee1e5ad243d0e48335adc0484d960bcc7" dependencies = [ "autocfg", - "cfg-if 1.0.0", + "cfg-if", "crossbeam-utils", - "lazy_static", - "memoffset", + "memoffset 0.9.0", "scopeguard", ] [[package]] name = "crossbeam-queue" -version = "0.3.5" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f25d8400f4a7a5778f0e4e52384a48cbd9b5c495d110786187fc750075277a2" +checksum = "d1cfb3ea8a53f37c40dea2c7bedcbd88bdfae54f5e2175d6ecaff1c988353add" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "crossbeam-utils", ] [[package]] name = "crossbeam-utils" -version = "0.8.8" +version = "0.8.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf124c720b7686e3c2663cf54062ab0f68a88af2fb6a030e87e30bf721fcb38" +checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" dependencies = [ - "cfg-if 1.0.0", - "lazy_static", + "cfg-if", ] [[package]] name = "csv" -version = "1.1.6" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22813a6dc45b335f9bade10bf7271dc477e81113e89eb251a0bc2a8a81c536e1" +checksum = "ac574ff4d437a7b5ad237ef331c17ccca63c46479e5b5453eb8e10bb99a759fe" dependencies = [ - "bstr", "csv-core", - "itoa 0.4.8", + "itoa", "ryu", "serde", ] [[package]] name = "csv-core" -version = "0.1.10" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b2466559f260f48ad25fe6317b3c8dac77b5bdb5763ac7d9d6103530663bc90" +checksum = "5efa2b3d7902f4b634a20cae3c9c4e6209dc4779feb6863329607560143efa70" dependencies = [ "memchr", ] [[package]] -name = "ctor" -version = "0.1.22" +name = "cvt" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f877be4f7c9f246b183111634f75baa039715e3f46ce860677d3b19a69fb229c" +checksum = "d2ae9bf77fbf2d39ef573205d554d87e86c12f1994e9ea335b0651b9b278bcf1" dependencies = [ - "quote", - "syn", + "cfg-if", ] [[package]] -name = "cty" -version = "0.2.2" +name = "dashmap" +version = "5.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b365fabc795046672053e29c954733ec3b05e4be654ab130fe8f1f94d7051f35" - -[[package]] -name = "d3d12" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2daefd788d1e96e0a9d66dee4b828b883509bc3ea9ce30665f04c3246372690c" +checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" dependencies = [ - "bitflags", - "libloading 0.7.3", - "winapi", + "cfg-if", + "hashbrown 0.14.1", + "lock_api", + "once_cell", + "parking_lot_core", ] [[package]] -name = "darling" -version = "0.10.2" +name = "deranged" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d706e75d87e35569db781a9b5e2416cff1236a47ed380831f959382ccd5f858" +checksum = "0f32d04922c60427da6f9fef14d042d9edddef64cb9d4ce0d64d0685fbeb1fd3" dependencies = [ - "darling_core 0.10.2", - "darling_macro 0.10.2", -] - -[[package]] -name = "darling" -version = "0.13.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a01d95850c592940db9b8194bc39f4bc0e89dee5c4265e4b1807c34a9aba453c" -dependencies = [ - "darling_core 0.13.4", - "darling_macro 0.13.4", -] - -[[package]] -name = "darling_core" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0c960ae2da4de88a91b2d920c2a7233b400bc33cb28453a2987822d8392519b" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim 0.9.3", - "syn", -] - -[[package]] -name = "darling_core" -version = "0.13.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "859d65a907b6852c9361e3185c862aae7fafd2887876799fa55f5f99dc40d610" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim 0.10.0", - "syn", -] - -[[package]] -name = "darling_macro" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b5a2f4ac4969822c62224815d069952656cadc7084fdca9751e6d959189b72" -dependencies = [ - "darling_core 0.10.2", - "quote", - "syn", -] - -[[package]] -name = "darling_macro" -version = "0.13.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835" -dependencies = [ - "darling_core 0.13.4", - "quote", - "syn", + "powerfmt", ] [[package]] name = "diff" -version = "0.1.12" +version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e25ea47919b1560c4e3b7fe0aaab9becf5b84a10325ddf7db0f0ba5e1026499" - -[[package]] -name = "digest" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5" -dependencies = [ - "generic-array 0.12.4", -] - -[[package]] -name = "digest" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" -dependencies = [ - "generic-array 0.14.5", -] +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" [[package]] name = "dircpy" -version = "0.3.9" +version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70b2666334bac0698c34f849a823a049800f9fe86a950cfd192e2d2a817da920" +checksum = "8466f8d28ca6da4c9dfbbef6ad4bff6f2fdd5e412d821025b0d3f0a9d74a8c1e" dependencies = [ "jwalk", "log", "walkdir", ] -[[package]] -name = "directories-next" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "339ee130d97a610ea5a5872d2bbb130fdf68884ff09d3028b81bec8a1ac23bbc" -dependencies = [ - "cfg-if 1.0.0", - "dirs-sys-next", -] - -[[package]] -name = "dirs" -version = "4.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" -dependencies = [ - "dirs-sys", -] - [[package]] name = "dirs-next" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "dirs-sys-next", ] -[[package]] -name = "dirs-sys" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" -dependencies = [ - "libc", - "redox_users", - "winapi", -] - [[package]] name = "dirs-sys-next" version = "0.1.2" @@ -1228,91 +760,35 @@ dependencies = [ "winapi", ] -[[package]] -name = "discard" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "212d0f5754cb6769937f4501cc0e67f4f4483c8d2c3e1e922ee9edbe4ab4c7c0" - -[[package]] -name = "dispatch" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" - [[package]] name = "distance" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d9d8664cf849d7d0f3114a3a387d2f5e4303176d746d5a951aaddc66dfe9240" -[[package]] -name = "dlib" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b11f15d1e3268f140f68d390637d5e76d849782d971ae7063e0da69fe9709a76" -dependencies = [ - "libloading 0.6.7", -] - -[[package]] -name = "dlib" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac1b7517328c04c2aa68422fc60a41b92208182142ed04a25879c26c8f878794" -dependencies = [ - "libloading 0.7.3", -] - [[package]] name = "doc-comment" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" -[[package]] -name = "downcast-rs" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650" - [[package]] name = "dunce" -version = "1.0.2" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "453440c271cf5577fd2a40e4942540cb7d0d2f85e27c8d07dd0023c925a67541" +checksum = "56ce8c6da7551ec6c462cbaf3bfbc75131ebbfa1c944aeaa9dab51ca1c5f0c3b" [[package]] -name = "dynasm" -version = "1.2.3" +name = "dyn-clone" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "add9a102807b524ec050363f09e06f1504214b0e1c7797f64261c891022dce8b" -dependencies = [ - "bitflags", - "byteorder", - "lazy_static", - "proc-macro-error", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "dynasmrt" -version = "1.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64fba5a42bd76a17cad4bfa00de168ee1cbfa06a5e8ce992ae880218c05641a9" -dependencies = [ - "byteorder", - "dynasm", - "memmap2 0.5.3", -] +checksum = "23d2f3407d9a573d666de4b5bdf10569d73ca9478087346697dcbae6244bfbcd" [[package]] name = "either" -version = "1.6.1" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" +checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" [[package]] name = "encode_unicode" @@ -1320,53 +796,27 @@ version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + +[[package]] +name = "encoding_rs" +version = "0.8.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" +dependencies = [ + "cfg-if", +] + [[package]] name = "endian-type" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d" -[[package]] -name = "enum-iterator" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4eeac5c5edb79e4e39fe8439ef35207780a11f69c52cbe424ce3dfad4cb78de6" -dependencies = [ - "enum-iterator-derive", -] - -[[package]] -name = "enum-iterator-derive" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c134c37760b27a871ba422106eedbb8247da973a09e82558bf26d619c882b159" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "enumset" -version = "1.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4799cdb24d48f1f8a7a98d06b7fde65a85a2d1e42b25a889f5406aa1fbefe074" -dependencies = [ - "enumset_derive", -] - -[[package]] -name = "enumset_derive" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea83a3fbdc1d999ccfbcbee717eab36f8edf2d71693a23ce0d7cca19e085304c" -dependencies = [ - "darling 0.13.4", - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "env_logger" version = "0.8.4" @@ -1377,38 +827,14 @@ dependencies = [ "regex", ] -[[package]] -name = "env_logger" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b2cf0344971ee6c64c31be0d530793fba457d322dfec2810c453d0ef228f9c3" -dependencies = [ - "atty", - "humantime", - "log", - "regex", - "termcolor", -] - [[package]] name = "errno" -version = "0.2.8" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" +checksum = "ac3e13f66a2f95e32a39eaa81f6b95d42878ca0e1db0c7543723dfe12557e860" dependencies = [ - "errno-dragonfly", - "libc", - "winapi", -] - -[[package]] -name = "errno-dragonfly" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" -dependencies = [ - "cc", "libc", + "windows-sys 0.48.0", ] [[package]] @@ -1421,56 +847,36 @@ dependencies = [ "str-buf", ] -[[package]] -name = "fake-simd" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed" - -[[package]] -name = "fallible-iterator" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" - -[[package]] -name = "fastrand" -version = "1.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3fcf0cee53519c866c09b5de1f6c56ff9d647101f81c1964fa632e148896cdf" -dependencies = [ - "instant", -] - [[package]] name = "fd-lock" -version = "3.0.5" +version = "3.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46e245f4c8ec30c6415c56cb132c07e69e74f1942f6b4a4061da748b49f486ca" +checksum = "ef033ed5e9bad94e55838ca0ca906db0e043f517adda0c8b79c7a8c66c93c1b5" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "rustix", - "windows-sys 0.30.0", + "windows-sys 0.48.0", ] [[package]] -name = "find-crate" -version = "0.6.3" +name = "filetime" +version = "0.2.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59a98bbaacea1c0eb6a0876280051b892eb73594fd90cf3b20e9c817029c57d2" +checksum = "d4029edd3e734da6fe05b6cd7bd2960760a616bd2ddd0d59a0124746d6272af0" dependencies = [ - "toml", + "cfg-if", + "libc", + "redox_syscall 0.3.5", + "windows-sys 0.48.0", ] [[package]] name = "flate2" -version = "1.0.23" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39522e96686d38f4bc984b9198e3a0613264abaebaff2c5c918bfa6b6da09af" +checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" dependencies = [ - "cfg-if 1.0.0", "crc32fast", - "libc", "miniz_oxide", ] @@ -1481,31 +887,33 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] -name = "foreign-types" -version = "0.3.2" +name = "form_urlencoded" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" dependencies = [ - "foreign-types-shared", + "percent-encoding", ] [[package]] -name = "foreign-types-shared" -version = "0.1.1" +name = "fs_at" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" +checksum = "982f82cc75107eef84f417ad6c53ae89bf65b561937ca4a3b3b0fd04d0aa2425" +dependencies = [ + "aligned", + "cfg-if", + "cvt", + "libc", + "nix 0.26.4", + "windows-sys 0.48.0", +] [[package]] name = "fs_extra" -version = "1.2.0" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2022715d62ab30faffd124d40b76f4134a550a87792276512b18d63272333394" - -[[package]] -name = "funty" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1847abb9cb65d566acd5942e94aea9c8f547ad02c98e1649326fc0e8910b8b1e" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" [[package]] name = "funty" @@ -1515,9 +923,9 @@ checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" [[package]] name = "futures" -version = "0.3.21" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f73fe65f54d1e12b726f517d3e2135ca3125a437b6d998caf1962961f7172d9e" +checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40" dependencies = [ "futures-channel", "futures-core", @@ -1530,9 +938,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.21" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3083ce4b914124575708913bca19bfe887522d6e2e6d0952943f5eac4a74010" +checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" dependencies = [ "futures-core", "futures-sink", @@ -1540,15 +948,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.21" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c09fd04b7e4073ac7156a9539b57a484a8ea920f79c7c675d05d289ab6110d3" +checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" [[package]] name = "futures-executor" -version = "0.3.21" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9420b90cfa29e327d0429f19be13e7ddb68fa1cccb09d65e5706b8c7a749b8a6" +checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0" dependencies = [ "futures-core", "futures-task", @@ -1557,38 +965,38 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.21" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc4045962a5a5e935ee2fdedaa4e08284547402885ab326734432bed5d12966b" +checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" [[package]] name = "futures-macro" -version = "0.3.21" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33c1e13800337f4d4d7a316bf45a567dbcb6ffe087f16424852d97e97a91f512" +checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.38", ] [[package]] name = "futures-sink" -version = "0.3.21" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21163e139fa306126e6eedaf49ecdb4588f939600f0b1e770f4205ee4b7fa868" +checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" [[package]] name = "futures-task" -version = "0.3.21" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c66a976bf5909d801bbef33416c41372779507e7a6b3a5e25e4749c58f776a" +checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" [[package]] name = "futures-util" -version = "0.3.21" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8b7abd5d659d9b90c8cba917f6ec750a74e2dc23902ef9cd4cc8c8b22e6036a" +checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" dependencies = [ "futures-channel", "futures-core", @@ -1602,165 +1010,42 @@ dependencies = [ "slab", ] -[[package]] -name = "fxhash" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" -dependencies = [ - "byteorder", -] - -[[package]] -name = "generational-arena" -version = "0.2.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e1d3b771574f62d0548cee0ad9057857e9fc25d7a3335f140c84f6acd0bf601" -dependencies = [ - "cfg-if 0.1.10", -] - -[[package]] -name = "generic-array" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffdf9f34f1447443d37393cc6c2b8313aebddcd96906caf34e54c68d8e57d7bd" -dependencies = [ - "typenum", -] - -[[package]] -name = "generic-array" -version = "0.14.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd48d33ec7f05fbfa152300fdad764757cbded343c1aa1cff2fbaf4134851803" -dependencies = [ - "typenum", - "version_check", -] - [[package]] name = "getrandom" -version = "0.2.6" +version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9be70c98951c83b8d2f8f60d7065fa6d5146873094452a1008da8c2f1e4205ad" +checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", + "js-sys", "libc", "wasi", -] - -[[package]] -name = "gimli" -version = "0.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0a01e0497841a3b2db4f8afa483cce65f7e96a3498bd6c541734792aeac8fe7" -dependencies = [ - "fallible-iterator", - "indexmap", - "stable_deref_trait", -] - -[[package]] -name = "gimli" -version = "0.26.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78cc372d058dcf6d5ecd98510e7fbc9e5aec4d21de70f65fea8fecebcd881bd4" - -[[package]] -name = "glob" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" - -[[package]] -name = "glow" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8bd5877156a19b8ac83a29b2306fe20537429d318f3ff0a1a2119f8d9c61919" -dependencies = [ - "js-sys", - "slotmap", "wasm-bindgen", - "web-sys", ] [[package]] -name = "glyph_brush" -version = "0.7.4" +name = "gimli" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a69c65dd1f1fbb6209aa00f78636e436ad0a55b7d8e5de886d00720dcad9c6e2" -dependencies = [ - "glyph_brush_draw_cache", - "glyph_brush_layout", - "log", - "ordered-float", - "rustc-hash", - "twox-hash", -] +checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" [[package]] -name = "glyph_brush_draw_cache" -version = "0.1.5" +name = "h2" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6010675390f6889e09a21e2c8b575b3ee25667ea8237a8d59423f73cb8c28610" +checksum = "91fc23aa11be92976ef4729127f1a74adf36d8436f7816b185d18df956790833" dependencies = [ - "ab_glyph", - "crossbeam-channel", - "crossbeam-deque", - "linked-hash-map", - "rayon", - "rustc-hash", -] - -[[package]] -name = "glyph_brush_layout" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc32c2334f00ca5ac3695c5009ae35da21da8c62d255b5b96d56e2597a637a38" -dependencies = [ - "ab_glyph", - "approx 0.5.1", - "xi-unicode", -] - -[[package]] -name = "gpu-alloc" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fc59e5f710e310e76e6707f86c561dd646f69a8876da9131703b2f717de818d" -dependencies = [ - "bitflags", - "gpu-alloc-types", -] - -[[package]] -name = "gpu-alloc-types" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54804d0d6bc9d7f26db4eaec1ad10def69b599315f487d32c334a80d1efe67a5" -dependencies = [ - "bitflags", -] - -[[package]] -name = "gpu-descriptor" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a538f217be4d405ff4719a283ca68323cc2384003eca5baaa87501e821c81dda" -dependencies = [ - "bitflags", - "gpu-descriptor-types", - "hashbrown 0.11.2", -] - -[[package]] -name = "gpu-descriptor-types" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "363e3677e55ad168fef68cf9de3a4a310b53124c5e784c53a1d70e92d23f2126" -dependencies = [ - "bitflags", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", ] [[package]] @@ -1771,9 +1056,15 @@ checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7" [[package]] name = "hashbrown" -version = "0.11.2" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" dependencies = [ "ahash", "bumpalo", @@ -1781,18 +1072,15 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.12.1" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db0d4cf898abf0081f964436dc980e96670a0f36863e4b83aaacdb65c9d7ccc3" -dependencies = [ - "ahash", -] +checksum = "7dfda62a12f55daeae5015f81b0baea145391cb4520f86c248fc615d72640d12" [[package]] name = "heck" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" [[package]] name = "hermit-abi" @@ -1804,38 +1092,133 @@ dependencies = [ ] [[package]] -name = "hexf-parse" -version = "0.2.1" +name = "hermit-abi" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" +checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" [[package]] -name = "hound" -version = "3.4.0" +name = "html-escape" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a164bb2ceaeff4f42542bdb847c41517c78a60f5649671b2a07312b6e117549" - -[[package]] -name = "humantime" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" - -[[package]] -name = "iced-x86" -version = "1.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "158f5204401d08f91d19176112146d75e99b3cf745092e268fa7be33e09adcec" +checksum = "6d1ad449764d627e22bfd7cd5e8868264fc9236e07c752972b4080cd351cb476" dependencies = [ - "lazy_static", - "static_assertions 1.1.0", + "utf8-width", ] [[package]] -name = "ident_case" -version = "1.0.1" +name = "http" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" +checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" +dependencies = [ + "bytes", + "http", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "0.14.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2 0.4.9", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d78e1e73ec14cf7375674f74d7dde185c8206fd9dea6fb6295e8a98098aaa97" +dependencies = [ + "futures-util", + "http", + "hyper", + "rustls", + "tokio", + "tokio-rustls", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8326b86b6cff230b97d0d312a6c40a60726df3332e721f72a1b035f451663b20" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "iced-x86" +version = "1.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdd366a53278429c028367e0ba22a46cab6d565a57afb959f06e92c7a69e7828" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "idna" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] [[package]] name = "im" @@ -1867,74 +1250,61 @@ dependencies = [ [[package]] name = "indexmap" -version = "1.8.1" +version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f647032dfaa1f8b6dc29bd3edb7bbef4861b8b8007ebb118d6db284fd59f6ee" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ "autocfg", - "hashbrown 0.11.2", - "serde", + "hashbrown 0.12.3", ] [[package]] name = "indoc" -version = "1.0.6" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05a0bd019339e5d968b37855180087b7b9d512c5046fbd244cf8c95687927d6e" +checksum = "bfa799dd5ed20a7e349f3b4639aa80d74549c81716d9ec4f994c9b5815598306" [[package]] name = "inkwell" -version = "0.1.0" -dependencies = [ - "inkwell 0.1.0 (git+https://github.com/rtfeldman/inkwell?branch=master)", -] - -[[package]] -name = "inkwell" -version = "0.1.0" -source = "git+https://github.com/rtfeldman/inkwell?branch=master#accd406858a40ca2a1463ff77d79f3c5e4c96f4e" +version = "0.2.0" +source = "git+https://github.com/roc-lang/inkwell?branch=inkwell-llvm-16#d1a596391894933aa1f0f91ec87187e47917cf86" dependencies = [ "either", "inkwell_internals", "libc", "llvm-sys", "once_cell", - "parking_lot 0.12.0", + "thiserror", ] [[package]] name = "inkwell_internals" -version = "0.5.0" -source = "git+https://github.com/rtfeldman/inkwell?branch=master#accd406858a40ca2a1463ff77d79f3c5e4c96f4e" +version = "0.8.0" +source = "git+https://github.com/roc-lang/inkwell?branch=inkwell-llvm-16#d1a596391894933aa1f0f91ec87187e47917cf86" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.38", ] [[package]] -name = "inplace_it" -version = "0.3.3" +name = "insta" +version = "1.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90953f308a79fe6d62a4643e51f848fbfddcd05975a38e69fdf4ab86a7baf7ca" - -[[package]] -name = "instant" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +checksum = "5d64600be34b2fcfc267740a243fa7744441bb4947a619ac4e5bb6507f35fbfc" dependencies = [ - "cfg-if 1.0.0", - "js-sys", - "wasm-bindgen", - "web-sys", + "console", + "lazy_static", + "linked-hash-map", + "similar", + "yaml-rust", ] [[package]] -name = "io-lifetimes" -version = "0.6.1" +name = "ipnet" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9448015e586b611e5d322f6703812bbca2f1e709d5773ecd38ddb4e3bb649504" +checksum = "28b29a3cd74f0f4598934efe3aeba42bae0eb4680554128851ebbecb02af14e6" [[package]] name = "itertools" @@ -1947,183 +1317,117 @@ dependencies = [ [[package]] name = "itertools" -version = "0.10.3" +version = "0.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9a9d19fa1e79b6215ff29b9d6880b706147f16e9b1dbb1e4e5947b5b02bc5e3" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" dependencies = [ "either", ] [[package]] name = "itoa" -version = "0.4.8" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" - -[[package]] -name = "itoa" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35" - -[[package]] -name = "jni" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6df18c2e3db7e453d3c6ac5b3e9d5182664d28788126d39b91f2d1e22b017ec" -dependencies = [ - "cesu8", - "combine", - "jni-sys", - "log", - "thiserror", - "walkdir", -] - -[[package]] -name = "jni-sys" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" - -[[package]] -name = "jobserver" -version = "0.1.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af25a77299a7f711a01975c35a6a424eb6862092cc2d6c72c4ed6cbc56dfc1fa" -dependencies = [ - "libc", -] +checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" [[package]] name = "js-sys" -version = "0.3.57" +version = "0.3.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "671a26f820db17c2a2750743f1dd03bafd15b98c9f30c7c2628c024c05d73397" +checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a" dependencies = [ "wasm-bindgen", ] [[package]] name = "jwalk" -version = "0.6.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "172752e853a067cbce46427de8470ddf308af7fd8ceaf9b682ef31a5021b6bb9" +checksum = "2735847566356cd2179a2a38264839308f7079fa96e6bd5a42d740460e003c56" dependencies = [ "crossbeam", "rayon", ] -[[package]] -name = "khronos-egl" -version = "4.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c2352bd1d0bceb871cb9d40f24360c8133c11d7486b68b5381c1dd1a32015e3" -dependencies = [ - "libc", - "libloading 0.7.3", -] - -[[package]] -name = "lazy-bytes-cast" -version = "5.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10257499f089cd156ad82d0a9cd57d9501fa2c989068992a97eb3c27836f206b" - [[package]] name = "lazy_static" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" -[[package]] -name = "lazycell" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" - -[[package]] -name = "leb128" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67" - -[[package]] -name = "lewton" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "777b48df9aaab155475a83a7df3070395ea1ac6902f5cd062b8f2b028075c030" -dependencies = [ - "byteorder", - "ogg", - "tinyvec", -] - [[package]] name = "libc" -version = "0.2.125" +version = "0.2.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5916d2ae698f6de9bfb891ad7a8d65c09d232dc58cc4ac433c7da3b2fd84bc2b" +checksum = "a08173bc88b7955d1b3145aa561539096c421ac8debde8cbc3612ec635fee29b" [[package]] name = "libloading" -version = "0.6.7" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "351a32417a12d5f7e82c368a66781e307834dae04c6ce0cd4456d52989229883" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "winapi", ] [[package]] -name = "libloading" -version = "0.7.3" +name = "libm" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efbc0f03f9a775e9f6aed295c6a1ba2253c5757a9e03d55c6caa46a681abcddd" -dependencies = [ - "cfg-if 1.0.0", - "winapi", -] +checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" [[package]] name = "libmimalloc-sys" -version = "0.1.25" +version = "0.1.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11ca136052550448f55df7898c6dbe651c6b574fe38a0d9ea687a9f8088a2e2c" +checksum = "3979b5c37ece694f1f5e51e7ecc871fdb0f517ed04ee45f88d15d6d553cb9664" dependencies = [ "cc", + "libc", +] + +[[package]] +name = "libtest-mimic" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d8de370f98a6cb8a4606618e53e802f93b094ddec0f96988eaec2c27e6e9ce7" +dependencies = [ + "clap 4.4.6", + "termcolor", + "threadpool", ] [[package]] name = "linked-hash-map" -version = "0.5.4" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" [[package]] name = "linux-raw-sys" -version = "0.0.46" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4d2456c373231a208ad294c33dc5bff30051eafd954cd4caae83a712b12854d" +checksum = "da2479e8c062e40bf0066ffa0bc823de0a9368974af99c9f6df941d2c231e03f" [[package]] name = "llvm-sys" -version = "130.0.3" +version = "160.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95eb03b4f7ae21f48ef7c565a3e3aa22c50616aea64645fb1fd7f6f56b51c274" +checksum = "0bf51981ac0622b10fe4790763e3de1f3d68a0ee4222e03accaaab6731bd508d" dependencies = [ "cc", "lazy_static", "libc", "regex", - "semver 0.11.0", + "semver", ] [[package]] name = "lock_api" -version = "0.4.7" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "327fa5b6a6940e4699ec49a9beae1ea4845c6bab9314e4f84ac68742139d8c53" +checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" dependencies = [ "autocfg", "scopeguard", @@ -2131,50 +1435,37 @@ dependencies = [ [[package]] name = "log" -version = "0.4.17" +version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" + +[[package]] +name = "lsp-types" +version = "0.93.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9be6e9c7e2d18f651974370d7aff703f9513e0df6e464fd795660edc77e6ca51" dependencies = [ - "cfg-if 1.0.0", + "bitflags 1.3.2", + "serde", + "serde_json", + "serde_repr", + "url", ] [[package]] -name = "loupe" -version = "0.1.3" +name = "mach_object" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b6a72dfa44fe15b5e76b94307eeb2ff995a8c5b283b55008940c02e0c5b634d" -dependencies = [ - "indexmap", - "loupe-derive", - "rustversion", -] - -[[package]] -name = "loupe-derive" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0fbfc88337168279f2e9ae06e157cfed4efd3316e14dc96ed074d4f2e6c5952" -dependencies = [ - "quote", - "syn", -] - -[[package]] -name = "mach" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b823e83b2affd8f40a9ee8c29dbc56404c1e34cd2710921f2801e2cf29527afa" -dependencies = [ - "libc", -] - -[[package]] -name = "malloc_buf" -version = "0.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +checksum = "8b6f2d7176b94027af58085a2c9d27c4e416586caba409c314569213901d6068" dependencies = [ + "bitflags 1.3.2", + "byteorder", + "lazy_static", "libc", + "log", + "thiserror", + "time", + "uuid", ] [[package]] @@ -2184,34 +1475,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" [[package]] -name = "memchr" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" - -[[package]] -name = "memmap2" +name = "matchers" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b70ca2a6103ac8b665dc150b142ef0e4e89df640c9e6cf295d189c3caebe5a" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" dependencies = [ - "libc", + "regex-automata 0.1.10", ] [[package]] -name = "memmap2" -version = "0.3.1" +name = "memchr" +version = "2.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b6c2ebff6180198788f5db08d7ce3bc1d0b617176678831a7510825973e357" -dependencies = [ - "libc", -] +checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" [[package]] name = "memmap2" -version = "0.5.3" +version = "0.5.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "057a3db23999c867821a7a59feb06a578fcb03685e983dff90daf9e7d24ac08f" +checksum = "83faa42c0a078c393f6b29d5db232d8be22776a891f8f56e5284faee4a20b327" dependencies = [ "libc", ] @@ -2226,232 +1508,60 @@ dependencies = [ ] [[package]] -name = "metal" -version = "0.23.1" +name = "memoffset" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0514f491f4cc03632ab399ee01e2c1c1b12d3e1cf2d667c1ff5f87d6dcd2084" +checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" dependencies = [ - "bitflags", - "block", - "core-graphics-types", - "foreign-types", - "log", - "objc", + "autocfg", ] [[package]] name = "mimalloc" -version = "0.1.29" +version = "0.1.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f64ad83c969af2e732e907564deb0d0ed393cec4af80776f77dd77a1a427698" +checksum = "fa01922b5ea280a911e323e4d2fd24b7fe5cc4042e0d2cda3c40775cdc4bdc9c" dependencies = [ "libmimalloc-sys", ] [[package]] -name = "minimal-lexical" -version = "0.2.1" +name = "mime" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" - -[[package]] -name = "minimp3" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "985438f75febf74c392071a975a29641b420dd84431135a6e6db721de4b74372" -dependencies = [ - "minimp3-sys", - "slice-deque", - "thiserror", -] - -[[package]] -name = "minimp3-sys" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e21c73734c69dc95696c9ed8926a2b393171d98b3f5f5935686a26a487ab9b90" -dependencies = [ - "cc", -] +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] name = "miniz_oxide" -version = "0.5.1" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2b29bd4bc3f33391105ebee3589c19197c4271e3e5a9ec9bfe8127eeff8f082" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" dependencies = [ "adler", ] [[package]] name = "mio" -version = "0.7.14" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8067b404fe97c70829f082dec8bcf4f71225d7eaea1d8645349cb76fa06205cc" +checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" dependencies = [ "libc", - "log", - "miow", - "ntapi", - "winapi", + "wasi", + "windows-sys 0.48.0", ] -[[package]] -name = "mio-misc" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b47412f3a52115b936ff2a229b803498c7b4d332adeb87c2f1498c9da54c398c" -dependencies = [ - "crossbeam", - "crossbeam-queue", - "log", - "mio", -] - -[[package]] -name = "miow" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21" -dependencies = [ - "winapi", -] - -[[package]] -name = "more-asserts" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7843ec2de400bcbc6a6328c958dc38e5359da6e93e72e37bc5246bf1ae776389" - [[package]] name = "morphic_lib" version = "0.1.0" dependencies = [ - "sha2", + "blake3", + "roc_collections", "smallvec", "thiserror", "typed-arena", ] -[[package]] -name = "naga" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "806f448a7ce662ca79ef5484ef8f451a9b7c51b8166c95f5a667228b3825a6ca" -dependencies = [ - "bit-set", - "bitflags", - "codespan-reporting", - "fxhash", - "hexf-parse", - "indexmap", - "log", - "num-traits", - "spirv", - "thiserror", -] - -[[package]] -name = "ndk" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8794322172319b972f528bf90c6b467be0079f1fa82780ffb431088e741a73ab" -dependencies = [ - "jni-sys", - "ndk-sys 0.2.2", - "num_enum", - "thiserror", -] - -[[package]] -name = "ndk" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2032c77e030ddee34a6787a64166008da93f6a352b629261d0fee232b8742dd4" -dependencies = [ - "bitflags", - "jni-sys", - "ndk-sys 0.3.0", - "num_enum", - "thiserror", -] - -[[package]] -name = "ndk-context" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" - -[[package]] -name = "ndk-glue" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5caf0c24d51ac1c905c27d4eda4fa0635bbe0de596b8f79235e0b17a4d29385" -dependencies = [ - "lazy_static", - "libc", - "log", - "ndk 0.3.0", - "ndk-macro 0.2.0", - "ndk-sys 0.2.2", -] - -[[package]] -name = "ndk-glue" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d0c4a7b83860226e6b4183edac21851f05d5a51756e97a1144b7f5a6b63e65f" -dependencies = [ - "lazy_static", - "libc", - "log", - "ndk 0.6.0", - "ndk-context", - "ndk-macro 0.3.0", - "ndk-sys 0.3.0", -] - -[[package]] -name = "ndk-macro" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05d1c6307dc424d0f65b9b06e94f88248e6305726b14729fd67a5e47b2dc481d" -dependencies = [ - "darling 0.10.2", - "proc-macro-crate 0.1.5", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "ndk-macro" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0df7ac00c4672f9d5aece54ee3347520b7e20f158656c7db2e6de01902eb7a6c" -dependencies = [ - "darling 0.13.4", - "proc-macro-crate 1.1.3", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "ndk-sys" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1bcdd74c20ad5d95aacd60ef9ba40fdf77f767051040541df557b7a9b2a2121" - -[[package]] -name = "ndk-sys" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e5a6ae77c8ee183dcbbba6150e2e6b9f3f4196a7666c02a715a95692ec1fa97" -dependencies = [ - "jni-sys", -] - [[package]] name = "nibble_vec" version = "0.1.0" @@ -2463,230 +1573,94 @@ dependencies = [ [[package]] name = "nix" -version = "0.18.0" +version = "0.23.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83450fe6a6142ddd95fb064b746083fc4ef1705fe81f64a64e1d4b39f54a1055" +checksum = "8f3790c00a0150112de0f4cd161e3d7fc4b2d8a5542ffc35f099a2562aecb35c" dependencies = [ - "bitflags", + "bitflags 1.3.2", "cc", - "cfg-if 0.1.10", + "cfg-if", "libc", + "memoffset 0.6.5", ] [[package]] name = "nix" -version = "0.20.0" +version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa9b4819da1bc61c0ea48b63b7bc8604064dd43013e7cc325df098d49cd7c18a" +checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b" dependencies = [ - "bitflags", - "cc", - "cfg-if 1.0.0", + "bitflags 1.3.2", + "cfg-if", "libc", ] [[package]] -name = "nix" -version = "0.22.3" +name = "normpath" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4916f159ed8e5de0082076562152a76b7a1f64a01fd9d1e0fea002c37624faf" +checksum = "ec60c60a693226186f5d6edf073232bfb6464ed97eb22cf3b01c1e8198fd97f5" dependencies = [ - "bitflags", - "cc", - "cfg-if 1.0.0", - "libc", - "memoffset", + "windows-sys 0.48.0", ] [[package]] -name = "nix" -version = "0.23.1" +name = "nu-ansi-term" +version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f866317acbd3a240710c63f065ffb1e4fd466259045ccb504130b7f668f35c6" -dependencies = [ - "bitflags", - "cc", - "cfg-if 1.0.0", - "libc", - "memoffset", -] - -[[package]] -name = "nom" -version = "7.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8903e5a29a317527874d0402f867152a3d21c908bb0b933e416c65e301d4c36" -dependencies = [ - "memchr", - "minimal-lexical", -] - -[[package]] -name = "nonempty" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9e591e719385e6ebaeb5ce5d3887f7d5676fceca6411d1925ccc95745f3d6f7" - -[[package]] -name = "ntapi" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c28774a7fd2fbb4f0babd8237ce554b73af68021b5f695a3cebd6c59bac0980f" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" dependencies = [ + "overload", "winapi", ] -[[package]] -name = "num-derive" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "num-traits" -version = "0.2.15" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" dependencies = [ "autocfg", + "libm", ] [[package]] name = "num_cpus" -version = "1.13.1" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" dependencies = [ - "hermit-abi", + "hermit-abi 0.3.3", "libc", ] -[[package]] -name = "num_enum" -version = "0.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf5395665662ef45796a4ff5486c5d41d29e0c09640af4c5f17fd94ee2c119c9" -dependencies = [ - "num_enum_derive", -] - -[[package]] -name = "num_enum_derive" -version = "0.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b0498641e53dd6ac1a4f22547548caa6864cc4933784319cd1775271c5a46ce" -dependencies = [ - "proc-macro-crate 1.1.3", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "objc" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" -dependencies = [ - "malloc_buf", - "objc_exception", -] - -[[package]] -name = "objc-foundation" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" -dependencies = [ - "block", - "objc", - "objc_id", -] - -[[package]] -name = "objc_exception" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad970fb455818ad6cba4c122ad012fae53ae8b4795f86378bce65e4f6bab2ca4" -dependencies = [ - "cc", -] - -[[package]] -name = "objc_id" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" -dependencies = [ - "objc", -] - [[package]] name = "object" -version = "0.26.2" +version = "0.30.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39f37e50073ccad23b6d09bcb5b263f4e76d3bb6038e4a3c08e52162ffa8abc2" +checksum = "03b4680b86d9cfafba8fc491dc9b6df26b68cf40e9e6cd73909194759a63c385" dependencies = [ "crc32fast", "flate2", + "hashbrown 0.13.2", "indexmap", "memchr", ] [[package]] name = "object" -version = "0.28.3" +version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40bec70ba014595f99f7aa110b84331ffe1ee9aece7fe6f387cc7e3ecda4d456" +checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0" dependencies = [ - "crc32fast", - "hashbrown 0.11.2", - "indexmap", "memchr", ] -[[package]] -name = "oboe" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27f63c358b4fa0fbcfefd7c8be5cfc39c08ce2389f5325687e7762a48d30a5c1" -dependencies = [ - "jni", - "ndk 0.6.0", - "ndk-context", - "num-derive", - "num-traits", - "oboe-sys", -] - -[[package]] -name = "oboe-sys" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3370abb7372ed744232c12954d920d1a40f1c4686de9e79e800021ef492294bd" -dependencies = [ - "cc", -] - -[[package]] -name = "ogg" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6951b4e8bf21c8193da321bcce9c9dd2e13c858fe078bf9054a288b419ae5d6e" -dependencies = [ - "byteorder", -] - [[package]] name = "once_cell" -version = "1.10.0" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87f3e037eac156d1775da914196f0f37741a274155e34a0b7e427c35d2a2ecb9" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" [[package]] name = "oorandom" @@ -2695,174 +1669,61 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575" [[package]] -name = "opaque-debug" -version = "0.2.3" +name = "overload" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c" - -[[package]] -name = "opaque-debug" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" - -[[package]] -name = "ordered-float" -version = "3.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96bcbab4bfea7a59c2c0fe47211a1ac4e3e96bea6eb446d704f310bc5c732ae2" -dependencies = [ - "num-traits", -] - -[[package]] -name = "os_str_bytes" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e22443d1643a904602595ba1cd8f7d896afe56d26712531c5ff73a15b2fbf64" - -[[package]] -name = "output_vt100" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "628223faebab4e3e40667ee0b2336d34a5b960ff60ea743ddfdbcf7770bcfb66" -dependencies = [ - "winapi", -] - -[[package]] -name = "owned_ttf_parser" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f923fb806c46266c02ab4a5b239735c144bdeda724a50ed058e5226f594cde3" -dependencies = [ - "ttf-parser 0.6.2", -] - -[[package]] -name = "owned_ttf_parser" -version = "0.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fb1e509cfe7a12db2a90bfa057dfcdbc55a347f5da677c506b53dd099cfec9d" -dependencies = [ - "ttf-parser 0.15.0", -] +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" [[package]] name = "packed_struct" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c48e482b9a59ad6c2cdb06f7725e7bd33fe3525baaf4699fde7bfea6a5b77b1" +checksum = "36b29691432cc9eff8b282278473b63df73bea49bc3ec5e67f31a3ae9c3ec190" dependencies = [ - "bitvec 0.22.3", + "bitvec", "packed_struct_codegen", "serde", ] [[package]] name = "packed_struct_codegen" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e3692b867ec1d48ccb441e951637a2cc3130d0912c0059e48319e1c83e44bc" +checksum = "9cd6706dfe50d53e0f6aa09e12c034c44faacd23e966ae5a209e8bdb8f179f98" dependencies = [ "proc-macro2", "quote", - "syn", -] - -[[package]] -name = "page_size" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eebde548fbbf1ea81a99b128872779c437752fb99f217c45245e1a61dcd9edcd" -dependencies = [ - "libc", - "winapi", -] - -[[package]] -name = "palette" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9735f7e1e51a3f740bacd5dc2724b61a7806f23597a8736e679f38ee3435d18" -dependencies = [ - "approx 0.5.1", - "num-traits", - "palette_derive", - "phf", -] - -[[package]] -name = "palette_derive" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7799c3053ea8a6d8a1193c7ba42f534e7863cf52e378a7f90406f4a645d33bad" -dependencies = [ - "find-crate", - "proc-macro2", - "quote", - "syn", + "syn 1.0.109", ] [[package]] name = "parking_lot" -version = "0.11.2" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" -dependencies = [ - "instant", - "lock_api", - "parking_lot_core 0.8.5", -] - -[[package]] -name = "parking_lot" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87f5ec2493a61ac0506c0f4199f99070cbe83857b0337006a30f3e6719b8ef58" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" dependencies = [ "lock_api", - "parking_lot_core 0.9.3", + "parking_lot_core", ] [[package]] name = "parking_lot_core" -version = "0.8.5" +version = "0.9.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216" +checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" dependencies = [ - "cfg-if 1.0.0", - "instant", + "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.4.1", "smallvec", - "winapi", + "windows-targets 0.48.5", ] -[[package]] -name = "parking_lot_core" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09a279cbf25cb0757810394fbc1e359949b59e348145c643a939a525692e6929" -dependencies = [ - "cfg-if 1.0.0", - "libc", - "redox_syscall", - "smallvec", - "windows-sys 0.36.1", -] - -[[package]] -name = "peeking_take_while" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" - [[package]] name = "peg" -version = "0.8.0" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af728fe826811af3b38c37e93de6d104485953ea373d656eebae53d6987fcd2c" +checksum = "400bcab7d219c38abf8bd7cc2054eb9bbbd4312d66f6a5557d572a203f646f61" dependencies = [ "peg-macros", "peg-runtime", @@ -2870,9 +1731,9 @@ dependencies = [ [[package]] name = "peg-macros" -version = "0.8.0" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4536be147b770b824895cbad934fccce8e49f14b4c4946eaa46a6e4a12fcdc16" +checksum = "46e61cce859b76d19090f62da50a9fe92bab7c2a5f09e183763559a2ac392c90" dependencies = [ "peg-runtime", "proc-macro2", @@ -2881,108 +1742,41 @@ dependencies = [ [[package]] name = "peg-runtime" -version = "0.8.0" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9b0efd3ba03c3a409d44d60425f279ec442bcf0b9e63ff4e410da31c8b0f69f" +checksum = "36bae92c60fa2398ce4678b98b2c4b5a7c61099961ca1fa305aec04a9ad28922" [[package]] name = "percent-encoding" -version = "2.1.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" +checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" [[package]] -name = "pest" -version = "2.1.3" +name = "pin-project" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10f4872ae94d7b90ae48754df22fd42ad52ce740b8f370b03da4835417403e53" +checksum = "fda4ed1c6c173e3fc7a83629421152e01d7b1f9b7f65fb301e490e8cfc656422" dependencies = [ - "ucd-trie", + "pin-project-internal", ] [[package]] -name = "pest_derive" -version = "2.1.0" +name = "pin-project-internal" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "833d1ae558dc601e9a60366421196a8d94bc0ac980476d0b67e1d0988d72b2d0" +checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" dependencies = [ - "pest", - "pest_generator", -] - -[[package]] -name = "pest_generator" -version = "2.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99b8db626e31e5b81787b9783425769681b347011cc59471e33ea46d2ea0cf55" -dependencies = [ - "pest", - "pest_meta", "proc-macro2", "quote", - "syn", -] - -[[package]] -name = "pest_meta" -version = "2.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54be6e404f5317079812fc8f9f5279de376d8856929e21c184ecf6bbd692a11d" -dependencies = [ - "maplit", - "pest", - "sha-1", -] - -[[package]] -name = "phf" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2ac8b67553a7ca9457ce0e526948cad581819238f4a9d1ea74545851fa24f37" -dependencies = [ - "phf_macros", - "phf_shared", - "proc-macro-hack", -] - -[[package]] -name = "phf_generator" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d43f3220d96e0080cc9ea234978ccd80d904eafb17be31bb0f76daaea6493082" -dependencies = [ - "phf_shared", - "rand", -] - -[[package]] -name = "phf_macros" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b706f5936eb50ed880ae3009395b43ed19db5bff2ebd459c95e7bf013a89ab86" -dependencies = [ - "phf_generator", - "phf_shared", - "proc-macro-hack", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "phf_shared" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a68318426de33640f02be62b4ae8eb1261be2efbc337b60c54d845bf4484e0d9" -dependencies = [ - "siphasher", + "syn 2.0.38", ] [[package]] name = "pin-project-lite" -version = "0.2.9" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" +checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" [[package]] name = "pin-utils" @@ -2990,25 +1784,6 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" -[[package]] -name = "pkg-config" -version = "0.3.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae" - -[[package]] -name = "plotters" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a3fd9ec30b9749ce28cd91f255d569591cdf937fe280c312143e3c4bad6f2a" -dependencies = [ - "num-traits", - "plotters-backend", - "plotters-svg", - "wasm-bindgen", - "web-sys", -] - [[package]] name = "plotters" version = "0.3.1" @@ -3023,54 +1798,39 @@ dependencies = [ [[package]] name = "plotters-backend" -version = "0.3.2" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d88417318da0eaf0fdcdb51a0ee6c3bed624333bff8f946733049380be67ac1c" +checksum = "9e76628b4d3a7581389a35d5b6e2139607ad7c75b17aed325f210aa91f4a9609" [[package]] name = "plotters-svg" -version = "0.3.1" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "521fa9638fa597e1dc53e9412a4f9cefb01187ee1f7413076f9e6749e2885ba9" +checksum = "38f6d39893cca0701371e3c27294f09797214b86f1fb951b89ade8ec04e2abab" dependencies = [ "plotters-backend", ] [[package]] -name = "ppv-lite86" -version = "0.2.16" +name = "powerfmt" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "pretty_assertions" -version = "1.2.1" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c89f989ac94207d048d92db058e4f6ec7342b0971fc58d1271ca148b799b3563" +checksum = "af7cee1a6c8a5b9208b3cb1061f10c0cb689087b3d8ce85fb9d2dd7a29b6ba66" dependencies = [ - "ansi_term", - "ctor", "diff", - "output_vt100", -] - -[[package]] -name = "proc-macro-crate" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d6ea3c4595b96363c13943497db34af4460fb474a95c43f4446ad341b8c9785" -dependencies = [ - "toml", -] - -[[package]] -name = "proc-macro-crate" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e17d47ce914bf4de440332250b0edd23ce48c005f59fab39d3335866b114f11a" -dependencies = [ - "thiserror", - "toml", + "yansi", ] [[package]] @@ -3082,7 +1842,7 @@ dependencies = [ "proc-macro-error-attr", "proc-macro2", "quote", - "syn", + "syn 1.0.109", "version_check", ] @@ -3097,66 +1857,51 @@ dependencies = [ "version_check", ] -[[package]] -name = "proc-macro-hack" -version = "0.5.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5" - [[package]] name = "proc-macro2" -version = "1.0.38" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9027b48e9d4c9175fa2218adf3557f91c1137021739951d4932f5f8268ac48aa" +checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" dependencies = [ - "unicode-xid", + "unicode-ident", ] [[package]] -name = "profiling" -version = "1.0.5" +name = "proptest" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9145ac0af1d93c638c98c40cf7d25665f427b2a44ad0a99b1dccf3e2f25bb987" - -[[package]] -name = "ptr_meta" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1" +checksum = "7c003ac8c77cb07bb74f5f198bce836a689bcd5a42574612bf14d17bfd08c20e" dependencies = [ - "ptr_meta_derive", -] - -[[package]] -name = "ptr_meta_derive" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac" -dependencies = [ - "proc-macro2", - "quote", - "syn", + "bit-set", + "bit-vec", + "bitflags 2.4.1", + "lazy_static", + "num-traits", + "rand", + "rand_chacha", + "rand_xorshift", + "regex-syntax 0.7.5", + "rusty-fork", + "tempfile", + "unarray", ] [[package]] name = "pulldown-cmark" -version = "0.8.0" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffade02495f22453cd593159ea2f59827aae7f53fa8323f756799b670881dcf8" +checksum = "77a1a2f1f0a7ecff9c31abbe177637be0e97a0aef46cf8738ece09327985d998" dependencies = [ - "bitflags", + "bitflags 1.3.2", "memchr", "unicase", ] [[package]] -name = "quick-xml" -version = "0.22.0" +name = "quick-error" +version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8533f14c8382aaad0d592c812ac3b826162128b65662331e1127b45c3d18536b" -dependencies = [ - "memchr", -] +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" [[package]] name = "quickcheck" @@ -3164,7 +1909,7 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "588f6378e4dd99458b60ec275b4477add41ce4fa9f64dcba6f15adccb19b50d6" dependencies = [ - "env_logger 0.8.4", + "env_logger", "log", "rand", ] @@ -3177,24 +1922,18 @@ checksum = "b22a693222d716a9587786f37ac3f6b4faedb5b80c23914e7303ff5a1d8016e9" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.109", ] [[package]] name = "quote" -version = "1.0.18" +version = "1.0.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1feb54ed693b93a84e14094943b84b7c4eae204c512b7ccb95ab0c66d278ad1" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" dependencies = [ "proc-macro2", ] -[[package]] -name = "radium" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "643f8f41a8ebc4c5dc4515c82bb8abd397b527fc20fd681b7c011c2aee5d44fb" - [[package]] name = "radium" version = "0.7.0" @@ -3234,13 +1973,22 @@ dependencies = [ [[package]] name = "rand_core" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ "getrandom", ] +[[package]] +name = "rand_xorshift" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d25bf25ec5ae4a3f1b92f929810509a2f53d7dca2f50b794ff57e3face536c8f" +dependencies = [ + "rand_core", +] + [[package]] name = "rand_xoshiro" version = "0.6.0" @@ -3250,62 +1998,51 @@ dependencies = [ "rand_core", ] -[[package]] -name = "range-alloc" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63e935c45e09cc6dcf00d2f0b2d630a58f4095320223d47fc68918722f0538b6" - -[[package]] -name = "raw-window-handle" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e28f55143d0548dad60bb4fbdc835a3d7ac6acc3324506450c5fdd6e42903a76" -dependencies = [ - "libc", - "raw-window-handle 0.4.3", -] - -[[package]] -name = "raw-window-handle" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b800beb9b6e7d2df1fe337c9e3d04e3af22a124460fb4c30fcc22c9117cefb41" -dependencies = [ - "cty", -] - [[package]] name = "rayon" -version = "1.5.2" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd249e82c21598a9a426a4e00dd7adc1d640b22445ec8545feef801d1a74c221" +checksum = "9c27db03db7734835b3f53954b534c91069375ce6ccaa2e065441e07d9b6cdb1" dependencies = [ - "autocfg", - "crossbeam-deque", "either", "rayon-core", ] [[package]] name = "rayon-core" -version = "1.9.2" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f51245e1e62e1f1629cbfec37b5793bbabcaeb90f30e94d2ba03564687353e4" +checksum = "5ce3fb6ad83f861aac485e76e1985cd109d9a3713802152be56c3b1f0e0658ed" dependencies = [ - "crossbeam-channel", "crossbeam-deque", "crossbeam-utils", - "num_cpus", ] [[package]] name = "redox_syscall" -version = "0.2.13" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62f25bc4c7e55e0b0b7a1d43fb893f4fa1361d0abe38b9ce4f323c2adfe6ef42" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" dependencies = [ - "bitflags", + "bitflags 1.3.2", +] + +[[package]] +name = "redox_syscall" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", ] [[package]] @@ -3315,30 +2052,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" dependencies = [ "getrandom", - "redox_syscall", + "redox_syscall 0.2.16", "thiserror", ] -[[package]] -name = "regalloc" -version = "0.0.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "571f7f397d61c4755285cd37853fe8e03271c243424a907415909379659381c5" -dependencies = [ - "log", - "rustc-hash", - "smallvec", -] - [[package]] name = "regex" -version = "1.5.5" +version = "1.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a11647b6b25ff05a515cb92c365cec08801e83423a235b51e231e1808747286" +checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" dependencies = [ "aho-corasick", "memchr", - "regex-syntax", + "regex-automata 0.4.3", + "regex-syntax 0.8.2", ] [[package]] @@ -3346,24 +2073,38 @@ name = "regex-automata" version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", +] + +[[package]] +name = "regex-automata" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.8.2", +] [[package]] name = "regex-syntax" -version = "0.6.25" +version = "0.6.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] -name = "region" -version = "3.0.0" +name = "regex-syntax" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76e189c2369884dce920945e2ddf79b3dff49e071a167dd1817fa9c4c00d512e" -dependencies = [ - "bitflags", - "libc", - "mach", - "winapi", -] +checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" + +[[package]] +name = "regex-syntax" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" [[package]] name = "remove_dir_all" @@ -3376,171 +2117,163 @@ dependencies = [ [[package]] name = "remove_dir_all" -version = "0.7.0" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "882f368737489ea543bc5c340e6f3d34a28c39980bd9a979e47322b26f60ac40" +checksum = "23895cfadc1917fed9c6ed76a8c2903615fa3704f7493ff82b364c6540acc02b" dependencies = [ + "aligned", + "cfg-if", + "cvt", + "fs_at", + "lazy_static", "libc", - "log", - "num_cpus", - "rayon", - "winapi", + "normpath", + "windows-sys 0.45.0", ] -[[package]] -name = "rend" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79af64b4b6362ffba04eef3a4e10829718a4896dac19daa741851c86781edf95" -dependencies = [ - "bytecheck", -] - -[[package]] -name = "renderdoc-sys" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1382d1f0a252c4bf97dc20d979a2fdd05b024acd7c2ed0f7595d7817666a157" - [[package]] name = "repl_test" -version = "0.1.0" +version = "0.0.1" dependencies = [ + "bumpalo", "indoc", - "lazy_static", + "roc_build", "roc_cli", "roc_repl_cli", + "roc_repl_ui", + "roc_reporting", + "roc_target", "roc_test_utils", + "roc_wasm_interp", + "rustyline", "strip-ansi-escapes", - "wasmer", - "wasmer-wasi", + "target-lexicon", ] [[package]] -name = "rkyv" -version = "0.7.38" +name = "reqwest" +version = "0.11.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "517a3034eb2b1499714e9d1e49b2367ad567e07639b69776d35e259d9c27cca6" +checksum = "046cd98826c46c2ac8ddecae268eb5c2e58628688a5fc7a2643704a73faba95b" dependencies = [ - "bytecheck", - "hashbrown 0.12.1", - "ptr_meta", - "rend", - "rkyv_derive", - "seahash", + "base64 0.21.4", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "hyper", + "hyper-rustls", + "ipnet", + "js-sys", + "log", + "mime", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls", + "rustls-pemfile", + "serde", + "serde_json", + "serde_urlencoded", + "system-configuration", + "tokio", + "tokio-rustls", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots", + "winreg", ] [[package]] -name = "rkyv_derive" -version = "0.7.38" +name = "ring" +version = "0.16.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "505c209ee04111a006431abf39696e640838364d67a107c559ababaf6fd8c9dd" +checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" dependencies = [ - "proc-macro2", - "quote", - "syn", + "cc", + "libc", + "once_cell", + "spin", + "untrusted", + "web-sys", + "winapi", ] [[package]] name = "rlimit" -version = "0.6.2" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc0bf25554376fd362f54332b8410a625c71f15445bca32ffdfdf4ec9ac91726" +checksum = "f8a29d87a652dc4d43c586328706bb5cdff211f3f39a530f240b53f7221dab8e" dependencies = [ "libc", ] -[[package]] -name = "roc-bindgen" -version = "0.1.0" -dependencies = [ - "bumpalo", - "clap 3.1.17", - "cli_utils", - "ctor", - "dircpy", - "indexmap", - "indoc", - "pretty_assertions", - "roc_builtins", - "roc_can", - "roc_collections", - "roc_error_macros", - "roc_load", - "roc_module", - "roc_mono", - "roc_reporting", - "roc_std", - "roc_target", - "roc_test_utils", - "roc_types", - "strum", - "strum_macros", - "target-lexicon", - "tempfile", -] - [[package]] name = "roc_alias_analysis" -version = "0.1.0" +version = "0.0.1" dependencies = [ + "bumpalo", "morphic_lib", "roc_collections", "roc_debug_flags", + "roc_error_macros", "roc_module", "roc_mono", ] [[package]] -name = "roc_ast" -version = "0.1.0" +name = "roc_bitcode" +version = "0.0.1" dependencies = [ - "arrayvec 0.7.2", - "bumpalo", - "indoc", - "libc", - "page_size", - "roc_builtins", - "roc_can", - "roc_collections", + "dunce", + "roc_command_utils", "roc_error_macros", - "roc_load", - "roc_module", - "roc_parse", - "roc_problem", - "roc_region", - "roc_reporting", - "roc_target", - "roc_types", - "roc_unify", - "snafu", - "ven_graph", - "winapi", + "tempfile", +] + +[[package]] +name = "roc_bitcode_bc" +version = "0.0.1" +dependencies = [ + "dunce", + "roc_command_utils", + "roc_error_macros", + "tempfile", ] [[package]] name = "roc_build" -version = "0.1.0" +version = "0.0.1" dependencies = [ "bumpalo", - "inkwell 0.1.0", - "libloading 0.7.3", - "roc_builtins", + "indoc", + "inkwell", + "libloading", + "roc_bitcode", "roc_can", "roc_collections", + "roc_command_utils", "roc_constrain", "roc_error_macros", "roc_gen_dev", "roc_gen_llvm", "roc_gen_wasm", + "roc_linker", "roc_load", "roc_module", "roc_mono", + "roc_packaging", "roc_parse", "roc_problem", "roc_region", "roc_reporting", - "roc_solve", + "roc_solve_problem", "roc_std", "roc_target", "roc_types", @@ -3553,23 +2286,21 @@ dependencies = [ [[package]] name = "roc_builtins" -version = "0.1.0" +version = "0.0.1" dependencies = [ - "dunce", - "lazy_static", "roc_collections", + "roc_error_macros", "roc_module", "roc_region", "roc_target", - "roc_types", "tempfile", ] [[package]] name = "roc_can" -version = "0.1.0" +version = "0.0.1" dependencies = [ - "bitvec 1.0.0", + "bitvec", "bumpalo", "indoc", "pretty_assertions", @@ -3580,115 +2311,168 @@ dependencies = [ "roc_parse", "roc_problem", "roc_region", + "roc_serialize", "roc_types", - "static_assertions 1.1.0", + "static_assertions", + "ven_pretty", +] + +[[package]] +name = "roc_checkmate" +version = "0.0.1" +dependencies = [ + "chrono", + "roc_checkmate_schema", + "roc_module", + "roc_solve_schema", + "roc_types", + "serde_json", +] + +[[package]] +name = "roc_checkmate_schema" +version = "0.0.1" +dependencies = [ + "schemars", + "serde", + "serde_json", ] [[package]] name = "roc_cli" -version = "0.1.0" +version = "0.0.1" dependencies = [ "bumpalo", - "clap 3.1.17", + "clap 4.4.6", "cli_utils", "const_format", - "criterion 0.3.5 (git+https://github.com/Anton-4/criterion.rs)", + "criterion", + "distance", "errno", "indoc", + "inkwell", "libc", + "libloading", "mimalloc", + "parking_lot", "pretty_assertions", "roc_build", "roc_builtins", "roc_can", "roc_collections", + "roc_command_utils", "roc_docs", - "roc_editor", "roc_error_macros", "roc_fmt", + "roc_gen_dev", + "roc_gen_llvm", + "roc_glue", "roc_linker", "roc_load", "roc_module", "roc_mono", + "roc_packaging", "roc_parse", "roc_region", "roc_repl_cli", + "roc_repl_expect", "roc_reporting", "roc_target", "roc_test_utils", + "roc_tracing", + "roc_wasm_interp", "serial_test", + "signal-hook", "strum", - "strum_macros", "target-lexicon", "tempfile", - "wasmer", - "wasmer-wasi", -] - -[[package]] -name = "roc_code_markup" -version = "0.1.0" -dependencies = [ - "bumpalo", - "itertools 0.10.3", - "palette", - "roc_ast", - "roc_module", - "roc_utils", - "serde", - "snafu", + "ven_pretty", ] [[package]] name = "roc_collections" -version = "0.1.0" +version = "0.0.1" dependencies = [ - "bitvec 1.0.0", + "bitvec", "bumpalo", - "hashbrown 0.11.2", + "fnv", + "hashbrown 0.13.2", "im", "im-rc", + "smallvec", "wyhash", ] +[[package]] +name = "roc_command_utils" +version = "0.0.1" + [[package]] name = "roc_constrain" -version = "0.1.0" +version = "0.0.1" dependencies = [ - "arrayvec 0.7.2", - "roc_builtins", + "arrayvec 0.7.4", "roc_can", "roc_collections", "roc_error_macros", "roc_module", "roc_parse", + "roc_problem", "roc_region", "roc_types", ] [[package]] name = "roc_debug_flags" -version = "0.1.0" +version = "0.0.1" + +[[package]] +name = "roc_derive" +version = "0.0.1" +dependencies = [ + "bumpalo", + "roc_can", + "roc_checkmate", + "roc_collections", + "roc_derive_key", + "roc_error_macros", + "roc_module", + "roc_region", + "roc_solve_schema", + "roc_types", + "roc_unify", +] + +[[package]] +name = "roc_derive_key" +version = "0.0.1" +dependencies = [ + "roc_collections", + "roc_error_macros", + "roc_module", + "roc_region", + "roc_types", +] [[package]] name = "roc_docs" -version = "0.1.0" +version = "0.0.1" dependencies = [ "bumpalo", "peg", "pretty_assertions", "pulldown-cmark", - "roc_ast", "roc_builtins", "roc_can", - "roc_code_markup", "roc_collections", "roc_highlight", "roc_load", "roc_module", + "roc_packaging", "roc_parse", "roc_region", "roc_reporting", + "roc_solve", "roc_target", "roc_types", "snafu", @@ -3696,96 +2480,53 @@ dependencies = [ [[package]] name = "roc_docs_cli" -version = "0.1.0" +version = "0.0.1" dependencies = [ - "clap 3.1.17", + "clap 4.4.6", + "libc", "roc_docs", ] [[package]] -name = "roc_editor" -version = "0.1.0" +name = "roc_error_macros" +version = "0.0.1" + +[[package]] +name = "roc_error_utils" +version = "0.0.1" dependencies = [ - "arrayvec 0.7.2", - "bumpalo", - "bytemuck", - "cgmath", - "colored", - "confy", - "copypasta", - "env_logger 0.9.0", - "fs_extra", - "futures", - "glyph_brush", - "libc", - "log", - "nonempty", - "page_size", - "palette", - "pest", - "pest_derive", - "rand", - "roc_ast", - "roc_builtins", - "roc_can", - "roc_code_markup", - "roc_collections", - "roc_load", - "roc_module", - "roc_parse", - "roc_problem", - "roc_region", - "roc_reporting", - "roc_solve", - "roc_types", - "roc_unify", - "rodio", - "serde", "snafu", - "tempfile", - "threadpool", - "uuid", - "ven_graph", - "wgpu", - "wgpu_glyph", - "winit", ] -[[package]] -name = "roc_error_macros" -version = "0.1.0" - [[package]] name = "roc_exhaustive" -version = "0.1.0" +version = "0.0.1" dependencies = [ "roc_collections", + "roc_error_macros", "roc_module", + "roc_problem", "roc_region", - "roc_std", ] [[package]] name = "roc_fmt" -version = "0.1.0" +version = "0.0.1" dependencies = [ "bumpalo", - "indoc", - "pretty_assertions", "roc_collections", "roc_module", "roc_parse", "roc_region", - "roc_test_utils", - "walkdir", ] [[package]] name = "roc_gen_dev" -version = "0.1.0" +version = "0.0.1" dependencies = [ "bumpalo", - "object 0.26.2", + "capstone", + "object 0.30.4", "packed_struct", "roc_builtins", "roc_can", @@ -3806,18 +2547,20 @@ dependencies = [ [[package]] name = "roc_gen_llvm" -version = "0.1.0" +version = "0.0.1" dependencies = [ "bumpalo", - "inkwell 0.1.0", + "inkwell", "morphic_lib", "roc_alias_analysis", + "roc_bitcode_bc", "roc_builtins", "roc_collections", "roc_debug_flags", "roc_error_macros", "roc_module", "roc_mono", + "roc_region", "roc_std", "roc_target", "target-lexicon", @@ -3825,8 +2568,9 @@ dependencies = [ [[package]] name = "roc_gen_wasm" -version = "0.1.0" +version = "0.0.1" dependencies = [ + "bitvec", "bumpalo", "roc_builtins", "roc_collections", @@ -3835,151 +2579,276 @@ dependencies = [ "roc_mono", "roc_std", "roc_target", + "roc_wasm_module", +] + +[[package]] +name = "roc_glue" +version = "0.0.1" +dependencies = [ + "backtrace", + "bumpalo", + "cli_utils", + "dircpy", + "fnv", + "indexmap", + "indoc", + "libc", + "libloading", + "pretty_assertions", + "roc_build", + "roc_builtins", + "roc_can", + "roc_collections", + "roc_error_macros", + "roc_gen_llvm", + "roc_linker", + "roc_load", + "roc_module", + "roc_mono", + "roc_packaging", + "roc_reporting", + "roc_std", + "roc_target", + "roc_tracing", + "roc_types", + "strum", + "strum_macros", + "target-lexicon", + "tempfile", ] [[package]] name = "roc_highlight" -version = "0.1.0" +version = "0.0.1" dependencies = [ - "peg", - "roc_code_markup", + "html-escape", + "roc_parse", + "roc_region", ] [[package]] name = "roc_ident" -version = "0.1.0" +version = "0.0.1" + +[[package]] +name = "roc_lang_srv" +version = "0.0.1" +dependencies = [ + "bumpalo", + "parking_lot", + "roc_can", + "roc_collections", + "roc_fmt", + "roc_load", + "roc_module", + "roc_packaging", + "roc_parse", + "roc_problem", + "roc_region", + "roc_reporting", + "roc_solve_problem", + "roc_target", + "roc_types", + "tokio", + "tower-lsp", +] [[package]] name = "roc_late_solve" -version = "0.1.0" +version = "0.0.1" dependencies = [ "bumpalo", "roc_can", + "roc_checkmate", + "roc_collections", + "roc_derive", + "roc_error_macros", + "roc_module", "roc_solve", + "roc_solve_schema", "roc_types", "roc_unify", ] [[package]] name = "roc_linker" -version = "0.1.0" +version = "0.0.1" dependencies = [ "bincode", "bumpalo", - "clap 3.1.17", "iced-x86", - "memmap2 0.5.3", - "object 0.26.2", - "roc_build", + "indoc", + "libc", + "mach_object", + "memmap2", + "object 0.30.4", "roc_collections", + "roc_error_macros", + "roc_load", + "roc_module", "roc_mono", + "roc_packaging", + "roc_reporting", + "roc_solve", + "roc_target", "serde", + "serial_test", "target-lexicon", "tempfile", ] [[package]] name = "roc_load" -version = "0.1.0" +version = "0.0.1" dependencies = [ "bumpalo", - "roc_builtins", - "roc_collections", - "roc_constrain", - "roc_load_internal", - "roc_module", - "roc_reporting", - "roc_target", - "roc_types", -] - -[[package]] -name = "roc_load_internal" -version = "0.1.0" -dependencies = [ - "bumpalo", - "crossbeam", "indoc", - "maplit", - "parking_lot 0.12.0", + "insta", "pretty_assertions", "roc_builtins", "roc_can", "roc_collections", "roc_constrain", - "roc_debug_flags", + "roc_derive", "roc_error_macros", + "roc_load_internal", "roc_module", - "roc_mono", + "roc_packaging", "roc_parse", "roc_problem", "roc_region", "roc_reporting", "roc_solve", + "roc_solve_problem", "roc_target", "roc_test_utils", "roc_types", + "ven_pretty", +] + +[[package]] +name = "roc_load_internal" +version = "0.0.1" +dependencies = [ + "bumpalo", + "crossbeam", + "indoc", + "maplit", + "parking_lot", + "pretty_assertions", + "roc_builtins", + "roc_can", + "roc_checkmate", + "roc_collections", + "roc_constrain", + "roc_debug_flags", + "roc_derive", + "roc_derive_key", + "roc_error_macros", + "roc_late_solve", + "roc_module", + "roc_mono", + "roc_packaging", + "roc_parse", + "roc_problem", + "roc_region", + "roc_reporting", + "roc_solve", + "roc_solve_problem", + "roc_target", + "roc_test_utils", + "roc_tracing", + "roc_types", "roc_unify", + "tempfile", "ven_pretty", ] [[package]] name = "roc_module" -version = "0.1.0" +version = "0.0.1" dependencies = [ "bumpalo", - "lazy_static", "roc_collections", "roc_error_macros", "roc_ident", "roc_region", "snafu", - "static_assertions 1.1.0", + "static_assertions", ] [[package]] name = "roc_mono" -version = "0.1.0" +version = "0.0.1" dependencies = [ + "arrayvec 0.7.4", + "bitvec", "bumpalo", - "hashbrown 0.11.2", + "hashbrown 0.13.2", + "parking_lot", "roc_builtins", "roc_can", "roc_collections", "roc_debug_flags", + "roc_derive", + "roc_derive_key", "roc_error_macros", "roc_exhaustive", "roc_late_solve", "roc_module", "roc_problem", "roc_region", + "roc_solve_schema", "roc_std", "roc_target", + "roc_tracing", "roc_types", - "static_assertions 1.1.0", + "roc_unify", + "static_assertions", "ven_pretty", ] +[[package]] +name = "roc_packaging" +version = "0.0.1" +dependencies = [ + "base64-url", + "blake3", + "brotli", + "bumpalo", + "flate2", + "fs_extra", + "reqwest", + "roc_error_macros", + "roc_parse", + "tar", + "tempfile", + "walkdir", +] + [[package]] name = "roc_parse" -version = "0.1.0" +version = "0.0.1" dependencies = [ "bumpalo", - "criterion 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", - "encode_unicode", + "criterion", + "encode_unicode 1.0.0", "indoc", "pretty_assertions", + "proptest", "quickcheck", "quickcheck_macros", "roc_collections", + "roc_error_macros", "roc_module", "roc_region", - "roc_test_utils", + "tempfile", ] [[package]] name = "roc_problem" -version = "0.1.0" +version = "0.0.1" dependencies = [ "roc_collections", "roc_module", @@ -3990,27 +2859,32 @@ dependencies = [ [[package]] name = "roc_region" -version = "0.1.0" +version = "0.0.1" dependencies = [ - "static_assertions 1.1.0", + "static_assertions", ] [[package]] name = "roc_repl_cli" -version = "0.1.0" +version = "0.0.1" dependencies = [ "bumpalo", "const_format", - "inkwell 0.1.0", - "libloading 0.7.3", + "inkwell", + "libloading", + "roc_bitcode", "roc_build", "roc_builtins", "roc_collections", + "roc_error_macros", + "roc_gen_dev", "roc_gen_llvm", "roc_load", "roc_mono", "roc_parse", + "roc_region", "roc_repl_eval", + "roc_repl_ui", "roc_reporting", "roc_std", "roc_target", @@ -4018,11 +2892,13 @@ dependencies = [ "rustyline", "rustyline-derive", "target-lexicon", + "tempfile", + "unicode-segmentation", ] [[package]] name = "roc_repl_eval" -version = "0.1.0" +version = "0.0.1" dependencies = [ "bumpalo", "roc_builtins", @@ -4032,100 +2908,198 @@ dependencies = [ "roc_load", "roc_module", "roc_mono", + "roc_packaging", "roc_parse", + "roc_problem", "roc_region", "roc_reporting", + "roc_solve", "roc_std", "roc_target", "roc_types", ] +[[package]] +name = "roc_repl_expect" +version = "0.0.1" +dependencies = [ + "bumpalo", + "indoc", + "inkwell", + "libc", + "libloading", + "pretty_assertions", + "roc_build", + "roc_builtins", + "roc_can", + "roc_collections", + "roc_error_macros", + "roc_gen_llvm", + "roc_load", + "roc_module", + "roc_mono", + "roc_packaging", + "roc_parse", + "roc_region", + "roc_repl_eval", + "roc_reporting", + "roc_std", + "roc_target", + "roc_types", + "signal-hook", + "strip-ansi-escapes", + "target-lexicon", + "tempfile", +] + +[[package]] +name = "roc_repl_ui" +version = "0.0.1" +dependencies = [ + "bumpalo", + "const_format", + "roc_collections", + "roc_load", + "roc_parse", + "roc_region", + "roc_repl_eval", + "roc_reporting", + "roc_target", + "unicode-segmentation", +] + [[package]] name = "roc_repl_wasm" -version = "0.1.0" +version = "0.0.1" dependencies = [ "bumpalo", "console_error_panic_hook", "futures", + "getrandom", "js-sys", + "roc_bitcode", "roc_builtins", "roc_collections", "roc_gen_wasm", "roc_load", "roc_parse", "roc_repl_eval", + "roc_repl_ui", "roc_reporting", + "roc_solve", "roc_target", "roc_types", + "tempfile", + "wasi_libc_sys", "wasm-bindgen", "wasm-bindgen-futures", ] [[package]] name = "roc_reporting" -version = "0.1.0" +version = "0.0.1" dependencies = [ "bumpalo", + "byte-unit", "distance", - "indoc", - "pretty_assertions", - "roc_builtins", + "itertools 0.10.5", "roc_can", "roc_collections", - "roc_constrain", + "roc_error_macros", "roc_exhaustive", - "roc_load", + "roc_fmt", "roc_module", + "roc_packaging", "roc_parse", "roc_problem", "roc_region", - "roc_solve", + "roc_solve_problem", "roc_std", - "roc_target", - "roc_test_utils", "roc_types", "ven_pretty", ] [[package]] -name = "roc_solve" -version = "0.1.0" +name = "roc_serialize" +version = "0.0.1" dependencies = [ - "arrayvec 0.7.2", + "roc_collections", +] + +[[package]] +name = "roc_solve" +version = "0.0.1" +dependencies = [ + "arrayvec 0.7.4", "bumpalo", "indoc", + "insta", "lazy_static", + "libtest-mimic", "pretty_assertions", "regex", "roc_builtins", "roc_can", + "roc_checkmate", "roc_collections", "roc_debug_flags", + "roc_derive", + "roc_derive_key", "roc_error_macros", "roc_exhaustive", "roc_load", "roc_module", + "roc_packaging", "roc_parse", "roc_problem", "roc_region", "roc_reporting", - "roc_solve", + "roc_solve_problem", + "roc_solve_schema", "roc_target", "roc_types", "roc_unify", "tempfile", + "test_solve_helpers", +] + +[[package]] +name = "roc_solve_problem" +version = "0.0.1" +dependencies = [ + "roc_can", + "roc_collections", + "roc_exhaustive", + "roc_module", + "roc_problem", + "roc_region", + "roc_types", +] + +[[package]] +name = "roc_solve_schema" +version = "0.0.1" +dependencies = [ + "bitflags 1.3.2", ] [[package]] name = "roc_std" -version = "0.1.0" +version = "0.0.1" dependencies = [ - "static_assertions 0.1.1", + "arrayvec 0.7.4", + "libc", + "pretty_assertions", + "quickcheck", + "quickcheck_macros", + "serde", + "serde_json", + "static_assertions", ] [[package]] name = "roc_target" -version = "0.1.0" +version = "0.0.1" dependencies = [ "strum", "strum_macros", @@ -4134,77 +3108,76 @@ dependencies = [ [[package]] name = "roc_test_utils" -version = "0.1.0" +version = "0.0.1" dependencies = [ "pretty_assertions", - "remove_dir_all 0.7.0", + "remove_dir_all 0.8.2", +] + +[[package]] +name = "roc_tracing" +version = "0.0.1" +dependencies = [ + "tracing", + "tracing-appender", + "tracing-subscriber", ] [[package]] name = "roc_types" -version = "0.1.0" +version = "0.0.1" dependencies = [ "bumpalo", "roc_collections", "roc_debug_flags", "roc_error_macros", "roc_module", + "roc_parse", "roc_region", - "static_assertions 1.1.0", + "roc_serialize", + "static_assertions", + "ven_pretty", ] [[package]] name = "roc_unify" -version = "0.1.0" +version = "0.0.1" dependencies = [ - "bitflags", + "roc_checkmate", "roc_collections", "roc_debug_flags", "roc_error_macros", "roc_module", + "roc_solve_schema", + "roc_tracing", "roc_types", ] [[package]] -name = "roc_utils" -version = "0.1.0" +name = "roc_wasm_interp" +version = "0.0.1" dependencies = [ - "snafu", + "bitvec", + "bumpalo", + "clap 4.4.6", + "rand", + "roc_wasm_module", ] [[package]] -name = "rodio" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d98f5e557b61525057e2bc142c8cd7f0e70d75dc32852309bec440e6e046bf9" +name = "roc_wasm_module" +version = "0.0.1" dependencies = [ - "claxon", - "cpal", - "hound", - "lewton", - "minimp3", + "bitvec", + "bumpalo", + "roc_error_macros", ] [[package]] name = "rustc-demangle" -version = "0.1.21" +version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342" - -[[package]] -name = "rustc-hash" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" - -[[package]] -name = "rustc_version" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" -dependencies = [ - "semver 0.9.0", -] +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" [[package]] name = "rustc_version" @@ -4212,53 +3185,85 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" dependencies = [ - "semver 1.0.9", + "semver", ] [[package]] name = "rustix" -version = "0.34.6" +version = "0.38.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3e74b3f02f2b6eb33790923756784614f456de79d821d6b2670dc7d5fbea807" +checksum = "745ecfa778e66b2b63c88a61cb36e0eea109e803b0b86bf9879fbc77c70e86ed" dependencies = [ - "bitflags", + "bitflags 2.4.1", "errno", - "io-lifetimes", "libc", "linux-raw-sys", - "winapi", + "windows-sys 0.48.0", ] [[package]] -name = "rusttype" -version = "0.9.2" +name = "rustls" +version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc7c727aded0be18c5b80c1640eae0ac8e396abf6fa8477d96cb37d18ee5ec59" +checksum = "cd8d6c9f025a446bc4d18ad9632e69aec8f287aa84499ee335599fabd20c3fd8" dependencies = [ - "ab_glyph_rasterizer", - "owned_ttf_parser 0.6.0", + "log", + "ring", + "rustls-webpki", + "sct", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d3987094b1d07b653b7dfdc3f70ce9a1da9c51ac18c1b06b662e4f9a0e9f4b2" +dependencies = [ + "base64 0.21.4", +] + +[[package]] +name = "rustls-webpki" +version = "0.101.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c7d5dece342910d9ba34d259310cae3e0154b873b35408b787b59bce53d34fe" +dependencies = [ + "ring", + "untrusted", ] [[package]] name = "rustversion" -version = "1.0.6" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2cc38e8fa666e2de3c4aba7edeb5ffc5246c1c2ed0e3d17e560aeeba736b23f" +checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" + +[[package]] +name = "rusty-fork" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb3dcc6e454c328bb824492db107ab7c0ae8fcffe4ad210136ef014458c1bc4f" +dependencies = [ + "fnv", + "quick-error", + "tempfile", + "wait-timeout", +] [[package]] name = "rustyline" version = "9.1.1" -source = "git+https://github.com/rtfeldman/rustyline?rev=e74333c#e74333c0d618896b88175bf06645108f996fe6d0" +source = "git+https://github.com/roc-lang/rustyline?rev=e74333c#e74333c0d618896b88175bf06645108f996fe6d0" dependencies = [ - "bitflags", - "cfg-if 1.0.0", - "clipboard-win 4.4.1", + "bitflags 1.3.2", + "cfg-if", + "clipboard-win", "dirs-next", "fd-lock", "libc", "log", "memchr", - "nix 0.23.1", + "nix 0.23.2", "radix_trie", "scopeguard", "smallvec", @@ -4271,17 +3276,17 @@ dependencies = [ [[package]] name = "rustyline-derive" version = "0.6.0" -source = "git+https://github.com/rtfeldman/rustyline?rev=e74333c#e74333c0d618896b88175bf06645108f996fe6d0" +source = "git+https://github.com/roc-lang/rustyline?rev=e74333c#e74333c0d618896b88175bf06645108f996fe6d0" dependencies = [ "quote", - "syn", + "syn 1.0.109", ] [[package]] name = "ryu" -version = "1.0.9" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f" +checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" [[package]] name = "same-file" @@ -4293,76 +3298,65 @@ dependencies = [ ] [[package]] -name = "scoped-tls" -version = "1.0.0" +name = "schemars" +version = "0.8.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea6a9290e3c9cf0f18145ef7ffa62d68ee0bf5fcd651017e586dc7fd5da448c2" +checksum = "1f7b0ce13155372a76ee2e1c5ffba1fe61ede73fbea5630d61eee6fac4929c0c" +dependencies = [ + "dyn-clone", + "schemars_derive", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e85e2a16b12bdb763244c69ab79363d71db2b4b918a2def53f80b02e0574b13c" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 1.0.109", +] [[package]] name = "scopeguard" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] -name = "seahash" -version = "4.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" - -[[package]] -name = "semver" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" -dependencies = [ - "semver-parser 0.7.0", -] - -[[package]] -name = "semver" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6" -dependencies = [ - "semver-parser 0.10.2", -] - -[[package]] -name = "semver" -version = "1.0.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cb243bdfdb5936c8dc3c45762a19d12ab4550cdc753bc247637d4ec35a040fd" - -[[package]] -name = "semver-parser" +name = "sct" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" +checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" +dependencies = [ + "ring", + "untrusted", +] [[package]] -name = "semver-parser" -version = "0.10.2" +name = "semver" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b0bef5b7f9e0df16536d3961cfb6e84331c065b4066afb39768d0e319411f7" -dependencies = [ - "pest", -] +checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090" [[package]] name = "serde" -version = "1.0.137" +version = "1.0.189" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61ea8d54c77f8315140a05f4c7237403bf38b72704d031543aa1d16abbf517d1" +checksum = "8e422a44e74ad4001bdc8eede9a4570ab52f71190e9c076d14369f38b9200537" dependencies = [ "serde_derive", ] [[package]] name = "serde-xml-rs" -version = "0.5.1" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65162e9059be2f6a3421ebbb4fef3e74b7d9e7c60c50a0e292c6239f19f1edfa" +checksum = "fb3aa78ecda1ebc9ec9847d5d3aba7d618823446a049ba2491940506da6e2782" dependencies = [ "log", "serde", @@ -4370,15 +3364,6 @@ dependencies = [ "xml-rs", ] -[[package]] -name = "serde_bytes" -version = "0.11.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "212e73464ebcde48d723aa02eb270ba62eff38a9b732df31f33f1b4e145f3a54" -dependencies = [ - "serde", -] - [[package]] name = "serde_cbor" version = "0.11.2" @@ -4391,111 +3376,118 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.137" +version = "1.0.189" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f26faba0c3959972377d3b2d306ee9f71faee9714294e41bb777f83f88578be" +checksum = "1e48d1f918009ce3145511378cf68d613e3b3d9137d67272562080d68a2b32d5" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.38", +] + +[[package]] +name = "serde_derive_internals" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85bf8229e7920a9f636479437026331ce11aa132b4dde37d121944a44d6e5f3c" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", ] [[package]] name = "serde_json" -version = "1.0.81" +version = "1.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b7ce2b32a1aed03c558dc61a5cd328f15aff2dbc17daad8fb8af04d2100e15c" +checksum = "6b420ce6e3d8bd882e9b243c6eed35dbc9a6110c9769e74b584e0d68d1f20c65" dependencies = [ - "itoa 1.0.1", + "itoa", "ryu", "serde", ] [[package]] -name = "serde_yaml" -version = "0.8.24" +name = "serde_repr" +version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "707d15895415db6628332b737c838b88c598522e4dc70647e59b72312924aebc" +checksum = "6f0a21fba416426ac927b1691996e82079f8b6156e920c85345f135b2e9ba2de" dependencies = [ - "indexmap", + "proc-macro2", + "quote", + "syn 2.0.38", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", "ryu", "serde", - "yaml-rust", ] [[package]] name = "serial_test" -version = "0.5.1" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0bccbcf40c8938196944a3da0e133e031a33f4d6b72db3bda3cc556e361905d" +checksum = "538c30747ae860d6fb88330addbbd3e0ddbe46d662d032855596d8a8ca260611" dependencies = [ + "dashmap", + "futures", "lazy_static", - "parking_lot 0.11.2", + "log", + "parking_lot", "serial_test_derive", ] [[package]] name = "serial_test_derive" -version = "0.5.1" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2acd6defeddb41eb60bb468f8825d0cfd0c2a76bc03bfd235b6a1dc4f6a1ad5" +checksum = "079a83df15f85d89a68d64ae1238f142f172b1fa915d0d76b26a7cba1b659a69" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.109", ] [[package]] -name = "sha-1" -version = "0.8.2" +name = "sharded-slab" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7d94d0bede923b3cea61f3f1ff57ff8cdfd77b400fb8f9998949e0cf04163df" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" dependencies = [ - "block-buffer 0.7.3", - "digest 0.8.1", - "fake-simd", - "opaque-debug 0.2.3", + "lazy_static", ] [[package]] -name = "sha1" -version = "0.6.1" +name = "signal-hook" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1da05c97445caa12d05e848c4a4fcbbea29e748ac28f7e80e9b010392063770" +checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" dependencies = [ - "sha1_smol", + "libc", + "signal-hook-registry", ] [[package]] -name = "sha1_smol" -version = "1.0.0" +name = "signal-hook-registry" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012" - -[[package]] -name = "sha2" -version = "0.9.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" dependencies = [ - "block-buffer 0.9.0", - "cfg-if 1.0.0", - "cpufeatures", - "digest 0.9.0", - "opaque-debug 0.3.0", + "libc", ] [[package]] -name = "shlex" -version = "1.1.0" +name = "similar" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3" - -[[package]] -name = "siphasher" -version = "0.3.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bd3e3206899af3f8b12af284fafc038cc1dc2b41d1b89dd17297221c5d225de" +checksum = "2aeaf503862c419d66959f5d7ca015337d864e9c49485d771b732e2a20453597" [[package]] name = "sized-chunks" @@ -4509,88 +3501,24 @@ dependencies = [ [[package]] name = "slab" -version = "0.4.6" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb703cfe953bccee95685111adeedb76fabe4e97549a58d16f03ea7b9367bb32" - -[[package]] -name = "slice-deque" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31ef6ee280cdefba6d2d0b4b78a84a1c1a3f3a4cec98c2d4231c8bc225de0f25" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" dependencies = [ - "libc", - "mach", - "winapi", -] - -[[package]] -name = "slotmap" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1e08e261d0e8f5c43123b7adf3e4ca1690d655377ac93a03b2c9d3e98de1342" -dependencies = [ - "version_check", + "autocfg", ] [[package]] name = "smallvec" -version = "1.8.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83" - -[[package]] -name = "smithay-client-toolkit" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4750c76fd5d3ac95fa3ed80fe667d6a3d8590a960e5b575b98eea93339a80b80" -dependencies = [ - "andrew", - "bitflags", - "calloop", - "dlib 0.4.2", - "lazy_static", - "log", - "memmap2 0.1.0", - "nix 0.18.0", - "wayland-client 0.28.6", - "wayland-cursor 0.28.6", - "wayland-protocols 0.28.6", -] - -[[package]] -name = "smithay-client-toolkit" -version = "0.15.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a28f16a97fa0e8ce563b2774d1e732dd5d4025d2772c5dba0a41a0f90a29da3" -dependencies = [ - "bitflags", - "dlib 0.5.0", - "lazy_static", - "log", - "memmap2 0.3.1", - "nix 0.22.3", - "pkg-config", - "wayland-client 0.29.4", - "wayland-cursor 0.29.4", - "wayland-protocols 0.29.4", -] - -[[package]] -name = "smithay-clipboard" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "610b551bd25378bfd2b8e7a0fcbd83d427e8f2f6a40c47ae0f70688e9949dd55" -dependencies = [ - "smithay-client-toolkit 0.15.4", - "wayland-client 0.29.4", -] +checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a" [[package]] name = "snafu" -version = "0.6.10" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eab12d3c261b2308b0d80c26fffb58d17eba81a4be97890101f416b478c79ca7" +checksum = "e4de37ad025c587a29e8f3f5605c00f70b98715ef90b9061a815b9e59e9042d6" dependencies = [ "backtrace", "doc-comment", @@ -4599,112 +3527,59 @@ dependencies = [ [[package]] name = "snafu-derive" -version = "0.6.10" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1508efa03c362e23817f96cde18abed596a25219a8b2c66e8db33c03543d315b" +checksum = "990079665f075b699031e9c08fd3ab99be5029b96f3b78dc0709e8f77e4efebf" dependencies = [ + "heck", "proc-macro2", "quote", - "syn", + "syn 1.0.109", ] [[package]] -name = "spirv" -version = "0.2.0+1.5.4" +name = "socket2" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "246bfa38fe3db3f1dfc8ca5a2cdeb7348c78be2112740cc0ec8ef18b6d94f830" +checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662" dependencies = [ - "bitflags", - "num-traits", + "libc", + "winapi", ] +[[package]] +name = "socket2" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4031e820eb552adee9295814c0ced9e5cf38ddf1e8b7d566d6de8e2538ea989e" +dependencies = [ + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + [[package]] name = "stable_deref_trait" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" -[[package]] -name = "standback" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e113fb6f3de07a243d434a56ec6f186dfd51cb08448239fe7bcae73f87ff28ff" -dependencies = [ - "version_check", -] - -[[package]] -name = "static_assertions" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f406d6ee68db6796e11ffd7b4d171864c58b7451e79ef9460ea33c287a1f89a7" - [[package]] name = "static_assertions" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" -[[package]] -name = "stdweb" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef5430c8e36b713e13b48a9f709cc21e046723fe44ce34587b73a830203b533e" - -[[package]] -name = "stdweb" -version = "0.4.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d022496b16281348b52d0e30ae99e01a73d737b2f45d38fed4edf79f9325a1d5" -dependencies = [ - "discard", - "rustc_version 0.2.3", - "stdweb-derive", - "stdweb-internal-macros", - "stdweb-internal-runtime", - "wasm-bindgen", -] - -[[package]] -name = "stdweb-derive" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c87a60a40fccc84bef0652345bbbbbe20a605bf5d0ce81719fc476f5c03b50ef" -dependencies = [ - "proc-macro2", - "quote", - "serde", - "serde_derive", - "syn", -] - -[[package]] -name = "stdweb-internal-macros" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58fa5ff6ad0d98d1ffa8cb115892b6e69d67799f6763e162a1c9db421dc22e11" -dependencies = [ - "base-x", - "proc-macro2", - "quote", - "serde", - "serde_derive", - "serde_json", - "sha1", - "syn", -] - -[[package]] -name = "stdweb-internal-runtime" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "213701ba3370744dcd1a12960caa4843b3d68b4d1c0a5d575e0d65b2ee9d16c0" - [[package]] name = "str-buf" -version = "1.0.5" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d44a3643b4ff9caf57abcee9c2c621d6c03d9135e0d8b589bd9afb5992cb176a" +checksum = "9e08d8363704e6c71fc928674353e6b7c23dcea9d82d7012c8faf2a3a025f8d0" [[package]] name = "strip-ansi-escapes" @@ -4715,12 +3590,6 @@ dependencies = [ "vte", ] -[[package]] -name = "strsim" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6446ced80d6c486436db5c078dde11a9f73d42b57fb273121e160b84f63d894c" - [[package]] name = "strsim" version = "0.10.0" @@ -4729,32 +3598,67 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "strum" -version = "0.24.0" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e96acfc1b70604b8b2f1ffa4c57e59176c7dbb05d556c71ecd2f5498a1dee7f8" +checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f" +dependencies = [ + "strum_macros", +] [[package]] name = "strum_macros" -version = "0.24.0" +version = "0.24.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6878079b17446e4d3eba6192bb0a2950d5b14f0ed8424b852310e5a94345d0ef" +checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59" dependencies = [ "heck", "proc-macro2", "quote", "rustversion", - "syn", + "syn 1.0.109", ] [[package]] name = "syn" -version = "1.0.92" +version = "1.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ff7c592601f11445996a06f8ad0c27f094a58857c2f89e97974ab9235b92c52" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" dependencies = [ "proc-macro2", "quote", - "unicode-xid", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e96b79aaa137db8f61e26363a0c9b47d8b4ec75da28b7d1d614c2303e232408b" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", ] [[package]] @@ -4764,55 +3668,95 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] -name = "target-lexicon" -version = "0.12.3" +name = "tar" +version = "0.4.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7fa7e55043acb85fca6b3c01485a2eeb6b69c5d21002e273c79e465f43b7ac1" +checksum = "b16afcea1f22891c49a00c751c7b63b2233284064f11a200fc624137c51e2ddb" +dependencies = [ + "filetime", + "libc", + "xattr", +] + +[[package]] +name = "target-lexicon" +version = "0.12.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d0e916b1148c8e263850e1ebcbd046f333e0683c724876bb0da63ea4373dc8a" [[package]] name = "tempfile" -version = "3.3.0" +version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" +checksum = "dac1c663cfc93810f88aed9b8941d48cabf856a1b111c29a40439018d870eb22" dependencies = [ - "cfg-if 1.0.0", - "fastrand", + "cfg-if", "libc", - "redox_syscall", + "rand", + "redox_syscall 0.2.16", "remove_dir_all 0.5.3", "winapi", ] [[package]] name = "termcolor" -version = "1.1.3" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" +checksum = "6093bad37da69aab9d123a8091e4be0aa4a03e4d601ec641c327398315f62b64" dependencies = [ "winapi-util", ] [[package]] -name = "test_gen" -version = "0.1.0" +name = "test_derive" +version = "0.0.1" dependencies = [ "bumpalo", - "either", - "indoc", - "inkwell 0.1.0", - "libc", - "libloading 0.7.3", - "roc_build", + "insta", "roc_builtins", "roc_can", "roc_collections", "roc_constrain", + "roc_debug_flags", + "roc_derive", + "roc_derive_key", + "roc_load_internal", + "roc_module", + "roc_packaging", + "roc_region", + "roc_reporting", + "roc_solve", + "roc_target", + "roc_types", + "ven_pretty", +] + +[[package]] +name = "test_gen" +version = "0.0.1" +dependencies = [ + "bumpalo", + "criterion", + "indoc", + "inkwell", + "libc", + "libloading", + "roc_bitcode", + "roc_build", + "roc_builtins", + "roc_can", + "roc_collections", + "roc_command_utils", + "roc_constrain", + "roc_debug_flags", + "roc_error_macros", "roc_gen_dev", "roc_gen_llvm", "roc_gen_wasm", "roc_load", "roc_module", "roc_mono", + "roc_packaging", "roc_parse", "roc_problem", "roc_region", @@ -4822,15 +3766,16 @@ dependencies = [ "roc_target", "roc_types", "roc_unify", + "roc_wasm_interp", + "roc_wasm_module", "target-lexicon", "tempfile", - "wasmer", - "wasmer-wasi", + "wasi_libc_sys", ] [[package]] name = "test_mono" -version = "0.1.0" +version = "0.0.1" dependencies = [ "bumpalo", "indoc", @@ -4840,18 +3785,62 @@ dependencies = [ "roc_load", "roc_module", "roc_mono", + "roc_packaging", "roc_reporting", "roc_target", + "roc_tracing", "test_mono_macros", ] [[package]] name = "test_mono_macros" -version = "0.1.0" +version = "0.0.1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.109", +] + +[[package]] +name = "test_solve_helpers" +version = "0.0.1" +dependencies = [ + "bumpalo", + "indoc", + "insta", + "lazy_static", + "pretty_assertions", + "regex", + "roc_can", + "roc_derive", + "roc_late_solve", + "roc_load", + "roc_module", + "roc_packaging", + "roc_problem", + "roc_region", + "roc_reporting", + "roc_solve", + "roc_solve_problem", + "roc_target", + "roc_types", + "tempfile", +] + +[[package]] +name = "test_syntax" +version = "0.0.1" +dependencies = [ + "bumpalo", + "indoc", + "pretty_assertions", + "roc_collections", + "roc_fmt", + "roc_module", + "roc_parse", + "roc_region", + "roc_test_utils", + "walkdir", ] [[package]] @@ -4863,30 +3852,34 @@ dependencies = [ "unicode-width", ] -[[package]] -name = "textwrap" -version = "0.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" - [[package]] name = "thiserror" -version = "1.0.31" +version = "1.0.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd829fe32373d27f76265620b5309d0340cb8550f523c1dda251d6298069069a" +checksum = "1177e8c6d7ede7afde3585fd2513e611227efd6481bd78d2e82ba1ce16557ed4" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.31" +version = "1.0.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0396bc89e626244658bef819e22d0cc459e795a5ebe878e6ec336d1674a8d79a" +checksum = "10712f02019e9288794769fba95cd6847df9874d49d871d062172f9dd41bc4cc" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.38", +] + +[[package]] +name = "thread_local" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" +dependencies = [ + "cfg-if", + "once_cell", ] [[package]] @@ -4900,40 +3893,31 @@ dependencies = [ [[package]] name = "time" -version = "0.2.27" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4752a97f8eebd6854ff91f1c1824cd6160626ac4bd44287f7f4ea2035a02a242" +checksum = "c4a34ab300f2dee6e562c10a046fc05e358b29f9bf92277f30c3c8d82275f6f5" dependencies = [ - "const_fn", - "libc", - "standback", - "stdweb 0.4.20", + "deranged", + "itoa", + "powerfmt", + "serde", + "time-core", "time-macros", - "version_check", - "winapi", ] +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + [[package]] name = "time-macros" -version = "0.1.1" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "957e9c6e26f12cb6d0dd7fc776bb67a706312e7299aed74c8dd5b17ebb27e2f1" +checksum = "4ad70d68dba9e1f8aceda7aa6711965dfec1cac869f311a51bd08b3a2ccbce20" dependencies = [ - "proc-macro-hack", - "time-macros-impl", -] - -[[package]] -name = "time-macros-impl" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd3c141a1b43194f3f56a1411225df8646c55781d5f26db825b3d98507eb482f" -dependencies = [ - "proc-macro-hack", - "proc-macro2", - "quote", - "standback", - "syn", + "time-core", ] [[package]] @@ -4957,135 +3941,353 @@ dependencies = [ [[package]] name = "tinyvec_macros" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] -name = "toml" -version = "0.5.9" +name = "tokio" +version = "1.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d82e1a7758622a465f8cee077614c73484dac5b836c02ff6a40d5d1010324d7" +checksum = "4f38200e3ef7995e5ef13baec2f432a6da0aa9ac495b2c0e8f3b7eec2c92d653" dependencies = [ - "serde", + "backtrace", + "bytes", + "libc", + "mio", + "num_cpus", + "pin-project-lite", + "socket2 0.5.4", + "tokio-macros", + "windows-sys 0.48.0", ] [[package]] -name = "tracing" -version = "0.1.34" +name = "tokio-macros" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d0ecdcb44a79f0fe9844f0c4f33a342cbcbb5117de8001e6ba0dc2351327d09" +checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" dependencies = [ - "cfg-if 1.0.0", + "proc-macro2", + "quote", + "syn 2.0.38", +] + +[[package]] +name = "tokio-rustls" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d68074620f57a0b21594d9735eb2e98ab38b17f80d3fcb189fca266771ca60d" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", + "tracing", +] + +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "pin-project", + "pin-project-lite", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" + +[[package]] +name = "tower-lsp" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43e094780b4447366c59f79acfd65b1375ecaa84e61dddbde1421aa506334024" +dependencies = [ + "async-trait", + "auto_impl", + "bytes", + "dashmap", + "futures", + "httparse", "log", + "lsp-types", + "memchr", + "serde", + "serde_json", + "tokio", + "tokio-util", + "tower", + "tower-lsp-macros", +] + +[[package]] +name = "tower-lsp-macros" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ebd99eec668d0a450c177acbc4d05e0d0d13b1f8d3db13cd706c52cbec4ac04" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "tower-service" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" + +[[package]] +name = "tracing" +version = "0.1.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee2ef2af84856a50c1d430afce2fdded0a4ec7eda868db86409b4543df0797f9" +dependencies = [ "pin-project-lite", "tracing-attributes", "tracing-core", ] [[package]] -name = "tracing-attributes" -version = "0.1.21" +name = "tracing-appender" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc6b8ad3567499f98a1db7a752b07a7c8c7c7c34c332ec00effb2b0027974b7c" +checksum = "09d48f71a791638519505cefafe162606f706c25592e4bde4d97600c0195312e" +dependencies = [ + "crossbeam-channel", + "time", + "tracing-subscriber", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.38", ] [[package]] name = "tracing-core" -version = "0.1.26" +version = "0.1.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f54c8ca710e81886d498c2fd3331b56c93aa248d49de2222ad2742247c60072f" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922" dependencies = [ "lazy_static", + "log", + "tracing-core", ] [[package]] -name = "ttf-parser" -version = "0.6.2" +name = "tracing-subscriber" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e5d7cd7ab3e47dda6e56542f4bbf3824c15234958c6e1bd6aaa347e93499fdc" - -[[package]] -name = "ttf-parser" -version = "0.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c74c96594835e10fa545e2a51e8709f30b173a092bfd6036ef2cec53376244f3" - -[[package]] -name = "twox-hash" -version = "1.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97fee6b57c6a41524a810daee9286c02d7752c4253064d0b05472833a438f675" +checksum = "30a651bc37f915e81f087d86e62a18eec5f79550c7faff886f7090b4ea757c77" dependencies = [ - "cfg-if 1.0.0", - "rand", - "static_assertions 1.1.0", + "matchers", + "nu-ansi-term", + "once_cell", + "regex", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", ] +[[package]] +name = "try-lock" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" + [[package]] name = "typed-arena" -version = "2.0.1" +version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0685c84d5d54d1c26f7d3eb96cd41550adb97baed141a761cf335d3d33bcd0ae" +checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a" [[package]] name = "typenum" -version = "1.15.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] -name = "ucd-trie" -version = "0.1.3" +name = "uitest" +version = "0.0.1" +dependencies = [ + "bumpalo", + "indoc", + "insta", + "lazy_static", + "libtest-mimic", + "pretty_assertions", + "regex", + "roc_builtins", + "roc_collections", + "roc_derive", + "roc_load", + "roc_module", + "roc_mono", + "roc_packaging", + "roc_parse", + "roc_problem", + "roc_reporting", + "roc_solve", + "roc_target", + "tempfile", + "test_solve_helpers", +] + +[[package]] +name = "unarray" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c" +checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" [[package]] name = "unicase" -version = "2.6.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" +checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" dependencies = [ "version_check", ] [[package]] -name = "unicode-segmentation" -version = "1.9.0" +name = "unicode-bidi" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e8820f5d777f6224dc4be3632222971ac30164d4a258d595640799554ebfd99" +checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-segmentation" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" [[package]] name = "unicode-width" -version = "0.1.9" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" +checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" [[package]] name = "unicode-xid" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "957e51f3646910546462e67d5f7599b9e4fb8acdd304b087a6494730f9eebf04" +checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" + +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + +[[package]] +name = "url" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "143b538f18257fac9cad154828a57c6bf5157e1aa604d4816b5995bf6de87ae5" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8-width" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5190c9442dcdaf0ddd50f37420417d219ae5261bbf5db120d0f9bab996c9cba1" [[package]] name = "utf8parse" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "936e4b492acfd135421d8dca4b1aa80a7bfc26e702ef3af710e0752684df5372" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" [[package]] name = "uuid" -version = "0.8.2" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" +checksum = "79daa5ed5740825c40b389c5e50312b9c86df53fccd33f281df655642b43869d" + +[[package]] +name = "valgrind" +version = "0.0.1" dependencies = [ - "getrandom", + "bumpalo", + "cli_utils", + "indoc", + "roc_build", + "roc_command_utils", + "roc_linker", + "roc_load", + "roc_mono", + "roc_packaging", + "roc_reporting", + "target-lexicon", + "tempfile", ] +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + [[package]] name = "ven_graph" version = "2.0.5-pre" @@ -5097,7 +4299,7 @@ dependencies = [ name = "ven_pretty" version = "0.9.1-alpha.0" dependencies = [ - "arrayvec 0.7.2", + "arrayvec 0.7.4", "termcolor", "typed-arena", ] @@ -5130,58 +4332,78 @@ dependencies = [ ] [[package]] -name = "walkdir" -version = "2.3.2" +name = "wait-timeout" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" +checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" +dependencies = [ + "libc", +] + +[[package]] +name = "walkdir" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee" dependencies = [ "same-file", - "winapi", "winapi-util", ] [[package]] -name = "wasi" -version = "0.10.2+wasi-snapshot-preview1" +name = "want" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasi_libc_sys" -version = "0.1.0" +version = "0.0.1" +dependencies = [ + "roc_command_utils", +] [[package]] name = "wasm-bindgen" -version = "0.2.80" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27370197c907c55e3f1a9fbe26f44e937fe6451368324e009cba39e139dc08ad" +checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.80" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53e04185bfa3a779273da532f5025e33398409573f348985af9a1cbf3774d3f4" +checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" dependencies = [ "bumpalo", - "lazy_static", "log", + "once_cell", "proc-macro2", "quote", - "syn", + "syn 2.0.38", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.30" +version = "0.4.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f741de44b75e14c35df886aff5f1eb73aa114fa5d4d00dcd37b5e01259bf3b2" +checksum = "c02dbc21516f9f1f04f187958890d7e6026df8d16540b7ad9492bc34a67cea03" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "js-sys", "wasm-bindgen", "web-sys", @@ -5189,9 +4411,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.80" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17cae7ff784d7e83a2fe7611cfe766ecf034111b49deb850a3dc7699c08251f5" +checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -5199,570 +4421,38 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.80" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99ec0dc7a4756fffc231aab1b9f2f578d23cd391390ab27f952ae0c9b3ece20b" +checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.38", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.80" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d554b7f530dee5964d9a9468d95c1f8b8acae4f282807e7d27d4b03099a46744" - -[[package]] -name = "wasmer" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f727a39e7161f7438ddb8eafe571b67c576a8c2fb459f666d9053b5bba4afdea" -dependencies = [ - "cfg-if 1.0.0", - "indexmap", - "js-sys", - "loupe", - "more-asserts", - "target-lexicon", - "thiserror", - "wasm-bindgen", - "wasmer-compiler", - "wasmer-compiler-cranelift", - "wasmer-compiler-singlepass", - "wasmer-derive", - "wasmer-engine", - "wasmer-engine-dylib", - "wasmer-engine-universal", - "wasmer-types", - "wasmer-vm", - "wat", - "winapi", -] - -[[package]] -name = "wasmer-compiler" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e9951599222eb12bd13d4d91bcded0a880e4c22c2dfdabdf5dc7e5e803b7bf3" -dependencies = [ - "enumset", - "loupe", - "rkyv", - "serde", - "serde_bytes", - "smallvec", - "target-lexicon", - "thiserror", - "wasmer-types", - "wasmer-vm", - "wasmparser", -] - -[[package]] -name = "wasmer-compiler-cranelift" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c83273bce44e668f3a2b9ccb7f1193db918b1d6806f64acc5ff71f6ece5f20" -dependencies = [ - "cranelift-codegen", - "cranelift-entity", - "cranelift-frontend", - "gimli 0.25.0", - "loupe", - "more-asserts", - "rayon", - "smallvec", - "target-lexicon", - "tracing", - "wasmer-compiler", - "wasmer-types", - "wasmer-vm", -] - -[[package]] -name = "wasmer-compiler-singlepass" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5432e993840cdb8e6875ddc8c9eea64e7a129579b4706bd91b8eb474d9c4a860" -dependencies = [ - "byteorder", - "dynasm", - "dynasmrt", - "lazy_static", - "loupe", - "more-asserts", - "rayon", - "smallvec", - "wasmer-compiler", - "wasmer-types", - "wasmer-vm", -] - -[[package]] -name = "wasmer-derive" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "458dbd9718a837e6dbc52003aef84487d79eedef5fa28c7d28b6784be98ac08e" -dependencies = [ - "proc-macro-error", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "wasmer-engine" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ed603a6d037ebbb14014d7f739ae996a78455a4b86c41cfa4e81c590a1253b9" -dependencies = [ - "backtrace", - "enumset", - "lazy_static", - "loupe", - "memmap2 0.5.3", - "more-asserts", - "rustc-demangle", - "serde", - "serde_bytes", - "target-lexicon", - "thiserror", - "wasmer-compiler", - "wasmer-types", - "wasmer-vm", -] - -[[package]] -name = "wasmer-engine-dylib" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccd7fdc60e252a795c849b3f78a81a134783051407e7e279c10b7019139ef8dc" -dependencies = [ - "cfg-if 1.0.0", - "enum-iterator", - "enumset", - "leb128", - "libloading 0.7.3", - "loupe", - "object 0.28.3", - "rkyv", - "serde", - "tempfile", - "tracing", - "wasmer-compiler", - "wasmer-engine", - "wasmer-object", - "wasmer-types", - "wasmer-vm", - "which", -] - -[[package]] -name = "wasmer-engine-universal" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcff0cd2c01a8de6009fd863b14ea883132a468a24f2d2ee59dc34453d3a31b5" -dependencies = [ - "cfg-if 1.0.0", - "enum-iterator", - "enumset", - "leb128", - "loupe", - "region", - "rkyv", - "wasmer-compiler", - "wasmer-engine", - "wasmer-types", - "wasmer-vm", - "winapi", -] - -[[package]] -name = "wasmer-object" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24ce18ac2877050e59580d27ee1a88f3192d7a31e77fbba0852abc7888d6e0b5" -dependencies = [ - "object 0.28.3", - "thiserror", - "wasmer-compiler", - "wasmer-types", -] - -[[package]] -name = "wasmer-types" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "659fa3dd6c76f62630deff4ac8c7657b07f0b1e4d7e0f8243a552b9d9b448e24" -dependencies = [ - "indexmap", - "loupe", - "rkyv", - "serde", - "thiserror", -] - -[[package]] -name = "wasmer-vfs" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f02fc47308cf5cf2cc039ec61c098773320b3d3c099434f20580bd143beee63b" -dependencies = [ - "libc", - "thiserror", - "tracing", -] - -[[package]] -name = "wasmer-vm" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afdc46158517c2769f9938bc222a7d41b3bb330824196279d8aa2d667cd40641" -dependencies = [ - "backtrace", - "cc", - "cfg-if 1.0.0", - "enum-iterator", - "indexmap", - "libc", - "loupe", - "memoffset", - "more-asserts", - "region", - "rkyv", - "serde", - "thiserror", - "wasmer-types", - "winapi", -] - -[[package]] -name = "wasmer-wasi" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3087d48fe015928118ae23f66f05b533e75fbea5dfcd64c75a74b7b5f941cc65" -dependencies = [ - "cfg-if 1.0.0", - "generational-arena", - "getrandom", - "libc", - "thiserror", - "tracing", - "wasm-bindgen", - "wasmer", - "wasmer-vfs", - "wasmer-wasi-types", - "winapi", -] - -[[package]] -name = "wasmer-wasi-types" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69adbd8d0d89cd19fb8b1e0252c76e3f72dbc65c944f0db7a9c28c4157fbcd3a" -dependencies = [ - "byteorder", - "time", - "wasmer-types", -] - -[[package]] -name = "wasmparser" -version = "0.78.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52144d4c78e5cf8b055ceab8e5fa22814ce4315d6002ad32cfd914f37c12fd65" - -[[package]] -name = "wast" -version = "40.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9bb4f48a8b083dbc50e291e430afb8f524092bb00428957bcc63f49f856c64ac" -dependencies = [ - "leb128", - "memchr", - "unicode-width", -] - -[[package]] -name = "wat" -version = "1.0.42" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0401b6395ce0db91629a75b29597ccb66ea29950af9fc859f1bb3a736609c76e" -dependencies = [ - "wast", -] - -[[package]] -name = "wayland-client" -version = "0.28.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3ab332350e502f159382201394a78e3cc12d0f04db863429260164ea40e0355" -dependencies = [ - "bitflags", - "downcast-rs", - "libc", - "nix 0.20.0", - "scoped-tls", - "wayland-commons 0.28.6", - "wayland-scanner 0.28.6", - "wayland-sys 0.28.6", -] - -[[package]] -name = "wayland-client" -version = "0.29.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91223460e73257f697d9e23d401279123d36039a3f7a449e983f123292d4458f" -dependencies = [ - "bitflags", - "downcast-rs", - "libc", - "nix 0.22.3", - "scoped-tls", - "wayland-commons 0.29.4", - "wayland-scanner 0.29.4", - "wayland-sys 0.29.4", -] - -[[package]] -name = "wayland-commons" -version = "0.28.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a21817947c7011bbd0a27e11b17b337bfd022e8544b071a2641232047966fbda" -dependencies = [ - "nix 0.20.0", - "once_cell", - "smallvec", - "wayland-sys 0.28.6", -] - -[[package]] -name = "wayland-commons" -version = "0.29.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94f6e5e340d7c13490eca867898c4cec5af56c27a5ffe5c80c6fc4708e22d33e" -dependencies = [ - "nix 0.22.3", - "once_cell", - "smallvec", - "wayland-sys 0.29.4", -] - -[[package]] -name = "wayland-cursor" -version = "0.28.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be610084edd1586d45e7bdd275fe345c7c1873598caa464c4fb835dee70fa65a" -dependencies = [ - "nix 0.20.0", - "wayland-client 0.28.6", - "xcursor", -] - -[[package]] -name = "wayland-cursor" -version = "0.29.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c52758f13d5e7861fc83d942d3d99bf270c83269575e52ac29e5b73cb956a6bd" -dependencies = [ - "nix 0.22.3", - "wayland-client 0.29.4", - "xcursor", -] - -[[package]] -name = "wayland-protocols" -version = "0.28.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "286620ea4d803bacf61fa087a4242ee316693099ee5a140796aaba02b29f861f" -dependencies = [ - "bitflags", - "wayland-client 0.28.6", - "wayland-commons 0.28.6", - "wayland-scanner 0.28.6", -] - -[[package]] -name = "wayland-protocols" -version = "0.29.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60147ae23303402e41fe034f74fb2c35ad0780ee88a1c40ac09a3be1e7465741" -dependencies = [ - "bitflags", - "wayland-client 0.29.4", - "wayland-commons 0.29.4", - "wayland-scanner 0.29.4", -] - -[[package]] -name = "wayland-scanner" -version = "0.28.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce923eb2deb61de332d1f356ec7b6bf37094dc5573952e1c8936db03b54c03f1" -dependencies = [ - "proc-macro2", - "quote", - "xml-rs", -] - -[[package]] -name = "wayland-scanner" -version = "0.29.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39a1ed3143f7a143187156a2ab52742e89dac33245ba505c17224df48939f9e0" -dependencies = [ - "proc-macro2", - "quote", - "xml-rs", -] - -[[package]] -name = "wayland-sys" -version = "0.28.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d841fca9aed7febf9bed2e9796c49bf58d4152ceda8ac949ebe00868d8f0feb8" -dependencies = [ - "dlib 0.5.0", - "lazy_static", - "pkg-config", -] - -[[package]] -name = "wayland-sys" -version = "0.29.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9341df79a8975679188e37dab3889bfa57c44ac2cb6da166f519a81cbe452d4" -dependencies = [ - "dlib 0.5.0", - "lazy_static", - "pkg-config", -] +checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" [[package]] name = "web-sys" -version = "0.3.57" +version = "0.3.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b17e741662c70c8bd24ac5c5b18de314a2c26c32bf8346ee1e6f53de919c283" +checksum = "9b85cbef8c220a6abc02aefd892dfc0fc23afb1c6a426316ec33253a3877249b" dependencies = [ "js-sys", "wasm-bindgen", ] [[package]] -name = "wgpu" -version = "0.11.1" +name = "webpki-roots" +version = "0.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eae7181fe6ba5f4b632a9079cc9e922a64555156c87def72c063f94b180c7d68" -dependencies = [ - "arrayvec 0.7.2", - "js-sys", - "log", - "parking_lot 0.11.2", - "raw-window-handle 0.3.4", - "smallvec", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "wgpu-core", - "wgpu-hal", - "wgpu-types", -] - -[[package]] -name = "wgpu-core" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35600627b6c718ad0e23ed75fb6140bfe32cdf21c8f539ce3c9ab8180e2cb38e" -dependencies = [ - "arrayvec 0.7.2", - "bitflags", - "cfg_aliases", - "copyless", - "fxhash", - "log", - "naga", - "parking_lot 0.11.2", - "profiling", - "raw-window-handle 0.3.4", - "smallvec", - "thiserror", - "wgpu-hal", - "wgpu-types", -] - -[[package]] -name = "wgpu-hal" -version = "0.11.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af28b29ef0b44cd22dd9895d4349b9d5a687df42f58da234871198637eabe328" -dependencies = [ - "arrayvec 0.7.2", - "ash", - "bit-set", - "bitflags", - "block", - "core-graphics-types", - "d3d12", - "foreign-types", - "fxhash", - "glow", - "gpu-alloc", - "gpu-descriptor", - "inplace_it", - "js-sys", - "khronos-egl", - "libloading 0.7.3", - "log", - "metal", - "naga", - "objc", - "parking_lot 0.11.2", - "profiling", - "range-alloc", - "raw-window-handle 0.3.4", - "renderdoc-sys", - "thiserror", - "wasm-bindgen", - "web-sys", - "wgpu-types", - "winapi", -] - -[[package]] -name = "wgpu-types" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e15e44ba88ec415466e18e91881319e7c9e96cb905dc623305168aea65b85ccc" -dependencies = [ - "bitflags", -] - -[[package]] -name = "wgpu_glyph" -version = "0.15.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f5774759a6288a2bf29dd56d04ef79e73c1bc2b1c289fdf25e502a2a7dc3eda" -dependencies = [ - "bytemuck", - "glyph_brush", - "log", - "wgpu", -] - -[[package]] -name = "which" -version = "4.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c4fb54e6113b6a8772ee41c3404fb0301ac79604489467e0a9ce1f3e97c24ae" -dependencies = [ - "either", - "lazy_static", - "libc", -] +checksum = "14247bb57be4f377dfb94c72830b8ce8fc6beac03cf4bf7b9732eadd414123fc" [[package]] name = "winapi" @@ -5782,9 +4472,9 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" dependencies = [ "winapi", ] @@ -5796,121 +4486,154 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] -name = "windows-sys" -version = "0.30.0" +name = "windows-core" +version = "0.51.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "030b7ff91626e57a05ca64a07c481973cbb2db774e4852c9c7ca342408c6a99a" +checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64" dependencies = [ - "windows_aarch64_msvc 0.30.0", - "windows_i686_gnu 0.30.0", - "windows_i686_msvc 0.30.0", - "windows_x86_64_gnu 0.30.0", - "windows_x86_64_msvc 0.30.0", + "windows-targets 0.48.5", ] [[package]] name = "windows-sys" -version = "0.36.1" +version = "0.45.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" dependencies = [ - "windows_aarch64_msvc 0.36.1", - "windows_i686_gnu 0.36.1", - "windows_i686_msvc 0.36.1", - "windows_x86_64_gnu 0.36.1", - "windows_x86_64_msvc 0.36.1", + "windows-targets 0.42.2", ] [[package]] -name = "windows_aarch64_msvc" -version = "0.30.0" +name = "windows-sys" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29277a4435d642f775f63c7d1faeb927adba532886ce0287bd985bffb16b6bca" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.36.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" - -[[package]] -name = "windows_i686_gnu" -version = "0.30.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1145e1989da93956c68d1864f32fb97c8f561a8f89a5125f6a2b7ea75524e4b8" - -[[package]] -name = "windows_i686_gnu" -version = "0.36.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" - -[[package]] -name = "windows_i686_msvc" -version = "0.30.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4a09e3a0d4753b73019db171c1339cd4362c8c44baf1bcea336235e955954a6" - -[[package]] -name = "windows_i686_msvc" -version = "0.36.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.30.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ca64fcb0220d58db4c119e050e7af03c69e6f4f415ef69ec1773d9aab422d5a" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.36.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.30.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08cabc9f0066848fef4bc6a1c1668e6efce38b661d2aeec75d18d8617eebb5f1" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.36.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" - -[[package]] -name = "winit" -version = "0.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79610794594d5e86be473ef7763f604f2159cbac8c94debd00df8fb41e86c2f8" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ - "bitflags", - "cocoa", - "core-foundation 0.9.3", - "core-graphics 0.22.3", - "core-video-sys", - "dispatch", - "instant", - "lazy_static", - "libc", - "log", - "mio", - "mio-misc", - "ndk 0.3.0", - "ndk-glue 0.3.0", - "ndk-sys 0.2.2", - "objc", - "parking_lot 0.11.2", - "percent-encoding", - "raw-window-handle 0.3.4", - "scopeguard", - "smithay-client-toolkit 0.12.3", - "wayland-client 0.28.6", - "winapi", - "x11-dl", + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", ] [[package]] @@ -5924,82 +4647,27 @@ dependencies = [ [[package]] name = "wyz" -version = "0.4.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "129e027ad65ce1453680623c3fb5163cbf7107bfe1aa32257e7d0e63f9ced188" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" dependencies = [ "tap", ] [[package]] -name = "wyz" -version = "0.5.0" +name = "xattr" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30b31594f29d27036c383b53b59ed3476874d518f0efb151b27a4c275141390e" -dependencies = [ - "tap", -] - -[[package]] -name = "x11-clipboard" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "473068b7b80ac86a18328824f1054e5e007898c47b5bbc281bd7abe32bc3653c" -dependencies = [ - "xcb", -] - -[[package]] -name = "x11-dl" -version = "2.19.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea26926b4ce81a6f5d9d0f3a0bc401e5a37c6ae14a1bfaa8ff6099ca80038c59" -dependencies = [ - "lazy_static", - "libc", - "pkg-config", -] - -[[package]] -name = "xcb" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "771e2b996df720cd1c6dd9ff90f62d91698fd3610cc078388d0564bdd6622a9c" +checksum = "f4686009f71ff3e5c4dbcf1a282d0a44db3f021ba69350cd42086b3e5f1c6985" dependencies = [ "libc", - "log", - "quick-xml", ] -[[package]] -name = "xcursor" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "463705a63313cd4301184381c5e8042f0a7e9b4bb63653f216311d4ae74690b7" -dependencies = [ - "nom", -] - -[[package]] -name = "xdg" -version = "2.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c4583db5cbd4c4c0303df2d15af80f0539db703fa1c68802d4cbbd2dd0f88f6" -dependencies = [ - "dirs", -] - -[[package]] -name = "xi-unicode" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a67300977d3dc3f8034dae89778f502b6ba20b269527b3223ba59c0cf393bb8a" - [[package]] name = "xml-rs" -version = "0.8.4" +version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2d7d3948613f75c98fd9328cfdcc45acc4d360655289d0a7d4ec931392200a3" +checksum = "0fcb9cbac069e033553e8bb871be2fbdffcab578eb25bd0f7c508cedc6dcd75a" [[package]] name = "yaml-rust" @@ -6009,3 +4677,9 @@ checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" dependencies = [ "linked-hash-map", ] + +[[package]] +name = "yansi" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" diff --git a/Cargo.toml b/Cargo.toml index c104f5ddb4..579db508d2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,65 +1,40 @@ [workspace] members = [ - "compiler/ident", - "compiler/region", - "compiler/collections", - "compiler/exhaustive", - "compiler/module", - "compiler/parse", - "compiler/can", - "compiler/problem", - "compiler/types", - "compiler/builtins", - "compiler/constrain", - "compiler/unify", - "compiler/solve", - "compiler/late_solve", - "compiler/fmt", - "compiler/mono", - "compiler/alias_analysis", - "compiler/test_mono", - "compiler/load", - "compiler/load_internal", - "compiler/gen_llvm", - "compiler/gen_dev", - "compiler/gen_wasm", - "compiler/build", - "compiler/arena_pool", - "compiler/test_gen", - "compiler/roc_target", - "compiler/debug_flags", - "vendor/inkwell", - "vendor/pathfinding", - "vendor/pretty", - "bindgen", - "editor", - "ast", - "cli", - "code_markup", - "highlight", - "error_macros", - "reporting", - "repl_cli", - "repl_eval", - "repl_test", - "repl_wasm", - "test_utils", - "utils", - "docs", - "docs_cli", - "linker", - "wasi-libc-sys", + "crates/compiler/*", + "crates/vendor/*", + "crates/glue", + "crates/cli", + "crates/cli_utils", + "crates/highlight", + "crates/error_macros", + "crates/reporting", + "crates/packaging", + "crates/repl_cli", + "crates/repl_eval", + "crates/repl_test", + "crates/repl_ui", + "crates/repl_wasm", + "crates/repl_expect", + "crates/roc_std", + "crates/test_utils", + "crates/valgrind", + "crates/tracing", + "crates/utils/*", + "crates/docs", + "crates/docs_cli", + "crates/linker", + "crates/wasi-libc-sys", + "crates/wasm_module", + "crates/wasm_interp", + "crates/lang_srv", ] + exclude = [ - # Examples sometimes have Rust hosts in their platforms. The compiler should ignore those. - "examples", - "ci/bench-runner", - # Ignore building these normally. They are only imported by tests. - # The tests will still correctly build them. - "cli_utils", - "compiler/test_mono_macros", - # `cargo build` would cause roc_std to be built with default features which errors on windows - "roc_std", + "ci/benchmarks/bench-runner", + "ci/repl_basic_test", + # Examples sometimes have Rust hosts in their platforms. The compiler should ignore those. + "crates/cli_testing_examples", + "examples", ] # Needed to be able to run `cargo run -p roc_cli --no-default-features` - # see www/build.sh for more. @@ -68,12 +43,157 @@ exclude = [ # workspace, and without `resolver = "2"` here, you can't use `-p` like this. resolver = "2" +[workspace.package] +authors = ["The Roc Contributors"] +edition = "2021" +license = "UPL-1.0" +repository = "https://github.com/roc-lang/roc" +version = "0.0.1" + +[workspace.dependencies] +# NOTE: roc-lang/inkwell is a fork of TheDan64/inkwell which does not change anything. +# +# The reason for this fork is that the way Inkwell is designed, you have to use +# a particular branch (e.g. "llvm8-0") in Cargo.toml. That would be fine, except that +# breaking changes get pushed directly to that branch, which breaks our build +# without warning. +# +# We tried referencing a specific rev on TheDan64/inkwell directly (instead of branch), +# but although that worked locally, it did not work on GitHub Actions. (After a few +# hours of investigation, gave up trying to figure out why.) So this is the workaround: +# having an immutable tag on the roc-lang/inkwell fork which points to +# a particular "release" of Inkwell. +# +# When we want to update Inkwell, we can sync up roc-lang/inkwell to the latest +# commit of TheDan64/inkwell, push a new tag which points to the latest commit, +# change the tag value in this Cargo.toml to point to that tag, and `cargo update`. +# This way, GitHub Actions works and nobody's builds get broken. +# TODO: Switch this back to roc-lang/inkwell once it is updated +inkwell = { git = "https://github.com/roc-lang/inkwell", branch = "inkwell-llvm-16", features = ["llvm16-0"] } + +arrayvec = "0.7.2" # update roc_std/Cargo.toml on change +backtrace = "0.3.67" +base64-url = "1.4.13" +bincode = "1.3.3" +bitflags = "1.3.2" +bitvec = "1.0.1" +blake3 = "1.3.3" +brotli = "3.3.4" # used for decompressing tarballs over HTTPS, if the server supports brotli +bumpalo = { version = "3.12.0", features = ["collections"] } +bytemuck = { version = "1.13.1", features = ["derive"] } +capstone = { version = "0.11.0", default-features = false } +cgmath = "0.18.0" +chrono = "0.4.26" +clap = { version = "4.2.7", default-features = false, features = ["std", "color", "suggestions", "help", "usage", "error-context"] } +colored = "2.0.0" +console_error_panic_hook = "0.1.7" +const_format = { version = "0.2.30", features = ["const_generics"] } +copypasta = "0.8.2" +criterion = { git = "https://github.com/Anton-4/criterion.rs", features = ["html_reports"], rev = "30ea0c5" } +criterion-perf-events = { git = "https://github.com/Anton-4/criterion-perf-events", rev = "0f38c3e" } +crossbeam = "0.8.2" +dircpy = "0.3.14" +distance = "0.4.0" +encode_unicode = "1.0.0" +errno = "0.3.0" +flate2 = "1.0.25" +fnv = "1.0.7" +fs_extra = "1.3.0" +futures = "0.3.26" +glyph_brush = "0.7.7" +hashbrown = { version = "0.13.2", features = ["bumpalo"] } +iced-x86 = { version = "1.18.0", default-features = false, features = ["std", "decoder", "op_code_info", "instr_info"] } +im = "15.1.0" +im-rc = "15.1.0" +indexmap = "1.9.2" +indoc = "1.0.9" +insta = "1.28.0" +js-sys = "0.3.61" +lazy_static = "1.4.0" +libc = "0.2.139" # update roc_std/Cargo.toml on change +libfuzzer-sys = "0.4" +libloading = "0.7.4" +libtest-mimic = "0.6.0" +log = "0.4.17" +mach_object = "0.1" +maplit = "1.0.2" +memmap2 = "0.5.10" +mimalloc = { version = "0.1.34", default-features = false } +nonempty = "0.8.1" +object = { version = "0.30.3", features = ["read", "write"] } +packed_struct = "0.10.1" +page_size = "0.5.0" +palette = "0.6.1" +parking_lot = "0.12" +peg = "0.8.1" +perfcnt = "0.8.0" +pest = "2.5.6" +pest_derive = "2.5.6" +pretty_assertions = "1.3.0" # update roc_std/Cargo.toml on change +proc-macro2 = "1.0.63" +proptest = "1.1.0" +pulldown-cmark = { version = "0.9.2", default-features = false } +quickcheck = "1.0.3" # update roc_std/Cargo.toml on change +quickcheck_macros = "1.0.0" # update roc_std/Cargo.toml on change +quote = "1.0.23" +rand = "0.8.5" +regex = "1.7.1" +remove_dir_all = "0.8.1" +reqwest = { version = "0.11.20", default-features = false, features = ["blocking", "rustls-tls"] } # default-features=false removes libopenssl as a dependency on Linux, which might not be available! +rlimit = "0.9.1" +rustyline = { git = "https://github.com/roc-lang/rustyline", rev = "e74333c" } +rustyline-derive = { git = "https://github.com/roc-lang/rustyline", rev = "e74333c" } +schemars = "0.8.12" +serde = { version = "1.0.153", features = ["derive"] } # update roc_std/Cargo.toml on change +serde-xml-rs = "0.6.0" +serde_json = "1.0.94" # update roc_std/Cargo.toml on change +serial_test = "1.0.0" +signal-hook = "0.3.15" +smallvec = { version = "1.10.0", features = ["const_generics", "const_new"] } +snafu = { version = "0.7.4", features = ["backtraces"] } +static_assertions = "1.1.0" # update roc_std/Cargo.toml on change +strip-ansi-escapes = "0.1.1" +strum = { version = "0.24.1", features = ["derive"] } +strum_macros = "0.24.3" +syn = { version = "1.0.109", features = ["full", "extra-traits"] } +tar = "0.4.38" +target-lexicon = "0.12.6" +tempfile = "=3.2.0" +threadpool = "1.8.1" +tracing = { version = "0.1.37", features = ["release_max_level_off"] } +tracing-appender = "0.2.2" +tracing-subscriber = { version = "0.3.16", features = ["env-filter"] } +unicode-segmentation = "1.10.1" +uuid = { version = "1.3.0", features = ["v4"] } +walkdir = "2.3.2" +wasm-bindgen = "0.2.84" +wasm-bindgen-futures = "0.4.34" +wgpu = "0.12.0" +wgpu_glyph = "0.16.0" +winapi = { version = "0.3.9", features = ["memoryapi"] } +winit = "0.26.1" +wyhash = "0.5.0" + # Optimizations based on https://deterministic.space/high-performance-rust.html [profile.release] -lto = "thin" codegen-units = 1 - # debug = true # enable when profiling + +[profile.dev] +debug = "line-tables-only" + [profile.bench] -lto = "thin" codegen-units = 1 +lto = "thin" + +[profile.release-with-debug] +inherits = "release" +debug = true + +[profile.release-with-lto] +inherits = "release" +lto = "thin" # TODO: We could consider full here since this is only used for packaged release on github. + +[profile.debug-full] +inherits = "dev" +debug = true diff --git a/Earthfile b/Earthfile index 4dbde657ca..ae0df6e89c 100644 --- a/Earthfile +++ b/Earthfile @@ -1,4 +1,6 @@ -FROM rust:1.60.0-slim-bullseye # make sure to update rust-toolchain.toml too so that everything uses the same rust version + +VERSION 0.6 +FROM rust:1.71.1-slim-buster # make sure to update rust-toolchain.toml too so that everything uses the same rust version WORKDIR /earthbuild prep-debian: @@ -7,155 +9,53 @@ prep-debian: install-other-libs: FROM +prep-debian RUN apt -y install wget git - RUN apt -y install libxcb-shape0-dev libxcb-xfixes0-dev # for editor clipboard - RUN apt -y install libasound2-dev # for editor sounds - RUN apt -y install libunwind-dev pkg-config libx11-dev zlib1g-dev + RUN apt -y install libunwind-dev pkg-config zlib1g-dev RUN apt -y install unzip # for www/build.sh -install-zig-llvm-valgrind-clippy-rustfmt: +install-zig-llvm: + ARG ZIG_ARCH FROM +install-other-libs - # editor - RUN apt -y install libxkbcommon-dev # zig - RUN wget -c https://ziglang.org/download/0.9.1/zig-linux-x86_64-0.9.1.tar.xz --no-check-certificate - RUN tar -xf zig-linux-x86_64-0.9.1.tar.xz - RUN ln -s /earthbuild/zig-linux-x86_64-0.9.1/zig /bin/zig + RUN wget -c https://ziglang.org/download/0.11.0/zig-linux-$ZIG_ARCH-0.11.0.tar.xz --no-check-certificate + RUN tar -xf zig-linux-$ZIG_ARCH-0.11.0.tar.xz + RUN ln -s /earthbuild/zig-linux-$ZIG_ARCH-0.11.0/zig /bin/zig # zig builtins wasm tests RUN apt -y install build-essential - RUN cargo install wasmer-cli --features "singlepass" # llvm RUN apt -y install lsb-release software-properties-common gnupg RUN wget https://apt.llvm.org/llvm.sh RUN chmod +x llvm.sh - RUN ./llvm.sh 13 - RUN ln -s /usr/bin/clang-13 /usr/bin/clang + RUN ./llvm.sh 16 + RUN ln -s /usr/bin/clang-16 /usr/bin/clang # use lld as linker - RUN ln -s /usr/bin/lld-13 /usr/bin/ld.lld + RUN ln -s /usr/bin/lld-16 /usr/bin/ld.lld + RUN apt -y install libpolly-16-dev # required by llvm-sys crate ENV RUSTFLAGS="-C link-arg=-fuse-ld=lld -C target-cpu=native" - # valgrind - RUN apt -y install valgrind - # clippy - RUN rustup component add clippy - # rustfmt - RUN rustup component add rustfmt - # wasm repl & tests - RUN rustup target add wasm32-unknown-unknown wasm32-wasi RUN apt -y install libssl-dev RUN OPENSSL_NO_VENDOR=1 cargo install wasm-pack - # criterion - RUN cargo install cargo-criterion # sccache - RUN cargo install sccache + RUN cargo install sccache --locked RUN sccache -V ENV RUSTC_WRAPPER=/usr/local/cargo/bin/sccache ENV SCCACHE_DIR=/earthbuild/sccache_dir ENV CARGO_INCREMENTAL=0 # no need to recompile package when using new function copy-dirs: - FROM +install-zig-llvm-valgrind-clippy-rustfmt - COPY --dir bindgen cli cli_utils compiler docs docs_cli editor ast code_markup error_macros highlight utils test_utils reporting repl_cli repl_eval repl_test repl_wasm repl_www roc_std vendor examples linker Cargo.toml Cargo.lock version.txt www wasi-libc-sys ./ - -test-zig: - FROM +install-zig-llvm-valgrind-clippy-rustfmt - COPY --dir compiler/builtins/bitcode ./ - RUN cd bitcode && ./run-tests.sh && ./run-wasm-tests.sh - -build-rust-test: - FROM +copy-dirs - RUN --mount=type=cache,target=$SCCACHE_DIR \ - cargo test --locked --release --features with_sound --workspace --no-run && sccache --show-stats - -check-clippy: - FROM +build-rust-test - RUN cargo clippy -V - RUN --mount=type=cache,target=$SCCACHE_DIR \ - cargo clippy --workspace --tests -- --deny warnings - RUN --mount=type=cache,target=$SCCACHE_DIR \ - cargo clippy --workspace --tests --release -- --deny warnings - -check-rustfmt: - FROM +build-rust-test - RUN cargo fmt --version - RUN cargo fmt --all -- --check - -check-typos: - RUN cargo install typos-cli --version 1.0.11 # version set to prevent confusion if the version is updated automatically - COPY --dir .github ci cli cli_utils compiler docs editor examples ast code_markup highlight utils linker nightly_benches packages roc_std www *.md LEGAL_DETAILS shell.nix version.txt ./ - RUN typos - -test-rust: - FROM +build-rust-test - ENV ROC_WORKSPACE_DIR=/earthbuild - ENV RUST_BACKTRACE=1 - # for race condition problem with cli test - ENV ROC_NUM_WORKERS=1 - # run one of the benchmarks to make sure the host is compiled - # not pre-compiling the host can cause race conditions - RUN echo "4" | cargo run --release examples/benchmarks/NQueens.roc - RUN --mount=type=cache,target=$SCCACHE_DIR \ - cargo test --locked --release --features with_sound --workspace && sccache --show-stats - # test the dev and wasm backend: they require an explicit feature flag. - RUN --mount=type=cache,target=$SCCACHE_DIR \ - cargo test --locked --release --package test_gen --no-default-features --features gen-dev && sccache --show-stats - # gen-wasm has some multithreading problems to do with the wasmer runtime. Run it single-threaded as a separate job - RUN --mount=type=cache,target=$SCCACHE_DIR \ - cargo test --locked --release --package test_gen --no-default-features --features gen-wasm -- --test-threads=1 && sccache --show-stats - # repl_test: build the compiler for wasm target, then run the tests on native target - RUN --mount=type=cache,target=$SCCACHE_DIR \ - repl_test/test_wasm.sh && sccache --show-stats - # run i386 (32-bit linux) cli tests - # NOTE: disabled until zig 0.9 - # RUN echo "4" | cargo run --locked --release --features="target-x86" -- --target=x86_32 examples/benchmarks/NQueens.roc - # RUN --mount=type=cache,target=$SCCACHE_DIR \ - # cargo test --locked --release --features with_sound --test cli_run i386 --features="i386-cli-run" && sccache --show-stats - # make sure website deployment works (that is, make sure build.sh returns status code 0) - ENV REPL_DEBUG=1 - RUN bash www/build.sh - - -verify-no-git-changes: - FROM +test-rust - # If running tests caused anything to be changed or added (without being - # included in a .gitignore somewhere), fail the build! - # - # How it works: the `git ls-files` command lists all the modified or - # uncommitted files in the working tree, the `| grep -E .` command returns a - # zero exit code if it listed any files and nonzero otherwise (which is the - # opposite of what we want), and the `!` at the start inverts the exit code. - RUN ! git ls-files --deleted --modified --others --exclude-standard | grep -E . - -test-all: - BUILD +test-zig - BUILD +check-rustfmt - BUILD +check-clippy - BUILD +test-rust - BUILD +verify-no-git-changes + ARG ZIG_ARCH + FROM +install-zig-llvm --ZIG_ARCH=$ZIG_ARCH + COPY --dir crates examples Cargo.toml Cargo.lock version.txt .cargo www rust-toolchain.toml ./ build-nightly-release: - FROM +test-rust - COPY --dir .git LICENSE LEGAL_DETAILS ./ + ARG RELEASE_FOLDER_NAME + ARG RUSTFLAGS + ARG ZIG_ARCH=x86_64 + FROM +copy-dirs --ZIG_ARCH=$ZIG_ARCH + COPY --dir .git LICENSE LEGAL_DETAILS ci ./ # version.txt is used by the CLI: roc --version - RUN printf "nightly pre-release, built from commit " > version.txt - RUN git log --pretty=format:'%h' -n 1 >> version.txt - RUN printf " on: " >> version.txt - RUN date >> version.txt - RUN RUSTFLAGS="-C target-cpu=x86-64" cargo build --features with_sound --release - RUN cd ./target/release && tar -czvf roc_linux_x86_64.tar.gz ./roc ../../LICENSE ../../LEGAL_DETAILS ../../examples/hello-world ../../compiler/builtins/bitcode/src/ ../../roc_std - SAVE ARTIFACT ./target/release/roc_linux_x86_64.tar.gz AS LOCAL roc_linux_x86_64.tar.gz - -# compile everything needed for benchmarks and output a self-contained dir from which benchmarks can be run. -prep-bench-folder: - FROM +copy-dirs - ARG BENCH_SUFFIX=branch - RUN cargo criterion -V - RUN --mount=type=cache,target=$SCCACHE_DIR cd cli && cargo criterion --no-run - RUN mkdir -p bench-folder/compiler/builtins/bitcode/src - RUN mkdir -p bench-folder/target/release/deps - RUN mkdir -p bench-folder/examples/benchmarks - RUN cp examples/benchmarks/*.roc bench-folder/examples/benchmarks/ - RUN cp -r examples/benchmarks/platform bench-folder/examples/benchmarks/ - RUN cp compiler/builtins/bitcode/src/str.zig bench-folder/compiler/builtins/bitcode/src - RUN cp target/release/roc bench-folder/target/release - # copy the most recent time bench to bench-folder - RUN cp target/release/deps/`ls -t target/release/deps/ | grep time_bench | head -n 1` bench-folder/target/release/deps/time_bench - SAVE ARTIFACT bench-folder AS LOCAL bench-folder-$BENCH_SUFFIX + RUN ./ci/write_version.sh + RUN RUSTFLAGS=$RUSTFLAGS cargo build --profile=release-with-lto --locked --bin roc + # strip debug info + RUN strip ./target/release-with-lto/roc + RUN ./ci/package_release.sh $RELEASE_FOLDER_NAME + RUN ls + SAVE ARTIFACT ./$RELEASE_FOLDER_NAME.tar.gz AS LOCAL $RELEASE_FOLDER_NAME.tar.gz diff --git a/FAQ.md b/FAQ.md index 0479d1380d..ebd6088477 100644 --- a/FAQ.md +++ b/FAQ.md @@ -1,19 +1,78 @@ +Click the ☰ button in the top left to see and search the table of contents. + # Frequently Asked Questions -# Why make a new editor instead of making an LSP plugin for VSCode, Vim or Emacs? -The Roc editor is one of the key areas where we want to innovate. Constraining ourselves to a plugin for existing editors would severely limit our possibilities for innovation. +## Where did the name Roc come from? -A key part of our editor will be the use of plugins that are shipped with libraries. Think of a regex visualizer, parser debugger, or color picker. For library authors, it would be most convenient to write these plugins in Roc. Trying to dynamically load library plugins (written in Roc) in for example VSCode seems very difficult. +The Roc logo, an origami bird -## Is there syntax highlighting for Vim/Emacs/VS Code or a LSP? +The Roc programming language is named after [a mythical bird](). -Not currently. Although they will presumably exist someday, while Roc is in the early days there's actually a conscious -effort to focus on the Roc Editor *instead of* adding Roc support to other editors - specifically in order to give the Roc -Editor the best possible chance at kickstarting a virtuous cycle of plugin authorship. +That’s why the logo is a bird. It’s specifically an [_origami_ bird](https://youtu.be/9gni1t1k1uY) as an homage +to [Elm](https://elm-lang.org/)’s tangram logo. -This is an unusual approach, but there are more details in [this 2021 interview](https://youtu.be/ITrDd6-PbvY?t=212). +Roc is a direct descendant of Elm. The languages are similar, but not the same. +[Origami](https://en.wikipedia.org/wiki/Origami) likewise has similarities to [tangrams](https://en.wikipedia.org/wiki/Tangram), although they are not the same. +Both involve making a surprising variety of things +from simple primitives. [_Folds_]() +are also common in functional programming. -In the meantime, using CoffeeScript syntax highlighting for .roc files turns out to work surprisingly well! +The logo was made by tracing triangles onto a photo of a physical origami bird. +It’s made of triangles because triangles are a foundational primitive in +computer graphics. + +The name was chosen because it makes for a three-letter file extension, it means something +fantastical, and it has incredible potential for puns. Here are some different ways to spell it: + +- **Roc** - traditional +- **roc** - low-key +- **ROC** - [YELLING](https://package.elm-lang.org/packages/elm/core/latest/String#toUpper) +- **Röc** - [metal 🤘](https://en.wikipedia.org/wiki/Metal_umlaut) + +Fun fact: "roc" translates to 鹏 in Chinese, [which means](https://www.mdbg.net/chinese/dictionary?page=worddict&wdrst=0&wdqb=%E9%B9%8F) "a large fabulous bird." + +## Why can't functions be compared for equality using the `==` operator? + +Function equality has been proven to be undecidable in the general case because of the [halting problem](https://en.wikipedia.org/wiki/Halting_problem). +So while we as humans might be able to look at `\x -> x + 1` and `\x -> 1 + x` and know that they're equivalent, +in the general case it's not possible for a computer to do this reliably. + +There are some other potential ways to define function equality, but they all have problems. + +One way would be to have two functions be considered equal if their source code is equivalent. (Perhaps disregarding +comments and spaces.) This sounds reasonable, but it means that now revising a function to do +exactly the same thing as before (say, changing `\x -> x + 1` to `\x -> 1 + x`) can cause a bug in a +distant part of the code base. Defining function equality this way means that revising a function's internals +is no longer a safe, local operation - even if it gives all the same outputs for all the same inputs. + +Another option would be to define it using "reference equality." This is what JavaScript does, for example. +However, Roc does not use reference equality anywhere else in the language, and it would mean that (for example) +passing `\x -> x + 1` to a function compared to defining `fn = \x -> x + 1` elsewhere and then passing `fn` into +the function might give different answers. + +Both of these would make revising code riskier across the entire language, which is very undesirable. + +Another option would be to define that function equality always returns `false`. So both of these would evaluate +to `false`: + +- `(\x -> x + 1) == (\x -> 1 + x)` +- `(\x -> x + 1) == (\x -> x + 1)` + +This makes function equality effectively useless, while still technically allowing it. It has some other downsides: + +- Now if you put a function inside a record, using `==` on that record will still type-check, but it will then return `false`. This could lead to bugs if you didn't realize you had accidentally put a function in there - for example, because you were actually storing a different type (e.g. an opaque type) and didn't realize it had a function inside it. +- If you put a function (or a value containing a function) into a `Dict` or `Set`, you'll never be able to get it out again. This is a common problem with [NaN](https://en.wikipedia.org/wiki/NaN), which is also defined not to be equal to itself. + +The first of these problems could be addressed by having function equality always return true instead of false (since that way it would not affect other fields' equality checks in a record), but that design has its own problems: + +- Although function equality is still useless, `(\x -> x + 1) == (\x -> x)` returns `Bool.true`. Even if it didn't lead to bugs in practice, this would certainly be surprising and confusing to beginners. +- Now if you put several different functions into a `Dict` or `Set`, only one of them will be kept; the others will be discarded or overwritten. This could cause bugs if a value stored a function internally, and then other functions relied on that internal function for correctness. + +Each of these designs makes Roc a language that's some combination of more error-prone, more confusing, and more +brittle to change. Disallowing function equality at compile time eliminates all of these drawbacks. + +Note that you can provide a custom implementation of the `Eq` ability for an opaque type that contains a function, +in any way you like (including ignoring the function for equality). ## Why is there no way to specify "import everything this module exposes" in `imports`? @@ -40,50 +99,10 @@ circulate about how to unlock those speed boosts. If Roc had this feature, it's piece of advice would eventually circulate: "don't use this feature because it slows down your builds." If a feature exists in a language, but the common recommendation is never to use it, that's cause for reconsidering -whether the feature should be in the language at all. In the case of this feature, I think it's simpler if the +whether the feature should be in the language at all. In the case of this feature, it's simpler if the language doesn't have it; that way nobody has to learn (or spend time spreading the word) about the performance-boosting advice not to use it. -## Why can't functions be compared for equality using the `==` operator? - -Function equality has been proven to be undecidable in the general case because of the [halting problem](https://en.wikipedia.org/wiki/Halting_problem). -So while we as humans might be able to look at `\x -> x + 1` and `\x -> 1 + x` and know that they're equivalent, -in the general case it's not possible for a computer to do this reliably. - -There are some other potential ways to define function equality, but they all have problems. - -One way would be to have two functions be considered equal if their source code is equivalent. (Perhaps disregarding -comments and spaces.) This sounds reasonable, but it means that now revising a function to do -exactly the same thing as before (say, changing `\x -> x + 1` to `\x -> 1 + x`) can cause a bug in a -distant part of the code base. Defining function equality this way means that revising a function's internals -is no longer a safe, local operation - even if it gives all the same outputs for all the same inputs. - -Another option would be to define it using "reference equality." This is what JavaScript does, for example. -However, Roc does not use reference equality anywhere else in the language, and it would mean that (for example) -passing `\x -> x + 1` to a function compared to defining `fn = \x -> x + 1` elsewhere and then passing `fn` into -the function might give different answers. - -Both of these would make revising code riskier across the entire language, which is very undesirable. - -Another option would be to define that function equality always returns `False`. So both of these would evaluate -to `False`: - -* `(\x -> x + 1) == (\x -> 1 + x)` -* `(\x -> x + 1) == (\x -> x + 1)` - -This makes function equality effectively useless, while still technically allowing it. It has some other downsides: -* Now if you put a function inside a record, using `==` on that record will still type-check, but it will then return `False`. This could lead to bugs if you didn't realize you had accidentally put a function in there - for example, because you were actually storing a different type (e.g. an opaque type) and didn't realize it had a function inside it. -* If you put a function (or a value containing a function) into a `Dict` or `Set`, you'll never be able to get it out again. This is a common problem with [NaN](https://en.wikipedia.org/wiki/NaN), which is also defined not to be equal to itself. - -The first of these problems could be addressed by having function equality always return `True` instead of `False` (since that way it would not affect other fields' equality checks in a record), but that design has its own problems: -* Although function equality is still useless, `(\x -> x + 1) == (\x -> x)` returns `True`. Even if it didn't lead to bugs in practice, this would certainly be surprising and confusing to beginners. -* Now if you put several different functions into a `Dict` or `Set`, only one of them will be kept; the others will be discarded or overwritten. This could cause bugs if a value stored a function internally, and then other functions relied on that internal function for correctness. - -Each of these designs makes Roc a language that's some combination of more error-prone, more confusing, and more -brittle to change. Disallowing function equality at compile time eliminates all of these drawbacks. - -Note that you can implement the `Eq` ability for a record that contains a function any way you want (for example, ignoring the function). - ## Why doesn't Roc have a `Maybe` or `Option` or `Optional` type, or `null` or `nil` or `undefined`? It's common for programming languages to have a [null reference](https://en.wikipedia.org/wiki/Null_pointer) @@ -98,7 +117,7 @@ and `Optional` (like in Java). By design, Roc does not have one of these. There are several reasons for this. First, if a function returns a potential error, Roc has the convention to use `Result` with an error type that -has a single tag describing what went wrong. (For example, `List.first : List a -> Result a [ListWasEmpty]*` +has a single tag describing what went wrong. (For example, `List.first : List a -> Result a [ListWasEmpty]` instead of `List.first : List a -> Maybe a`.) This is not only more self-descriptive, it also composes better with other operations that can fail; there's no need to have functions like `Result.toMaybe` or `Maybe.toResult`, because in Roc, the convention is that operations that can fail always use `Result`. @@ -109,12 +128,12 @@ To describe something that's neither an optional field nor an operation that can more descriptive than something like `Maybe`. For example, if a record type has an `artist` field, but the artist information may not be available, compare these three alternative ways to represent that: -* `artist : Maybe Artist` -* `artist : [Loading, Loaded Artist]` -* `artist : [Unspecified, Specified Artist]` +- `artist : Maybe Artist` +- `artist : [Loading, Loaded Artist]` +- `artist : [Unspecified, Specified Artist]` All three versions tell us that we might not have access to an `Artist`. However, the `Maybe` version doesn't -tell us why that might be. The `Loading`/`Loaded` version tells us we don't have one *yet*, because we're +tell us why that might be. The `Loading`/`Loaded` version tells us we don't have one _yet_, because we're still loading it, whereas the `Unspecified`/`Specified` version tells us we don't have one and shouldn't expect to have one later if we wait, because it wasn't specified. @@ -128,17 +147,17 @@ for using `Maybe` even when it's less self-descriptive), we'd have to rewrite al helper functions. As such, a subtle downside of these helper functions is that they discourage any change to the data model that would break their call sites, even if that change would improve the data model overall. -On a historical note, `Maybe` may have been thought of as a substitute for null references—as opposed to something that emerged organically based on specific motivating use cases after `Result` already existed. That said, in languages that do not have an equivalent of Roc's tag unions, it's much less ergonomic to write something like `Result a [ListWasEmpty]*`, so that design would not fit those languages as well as it fits Roc. +On a historical note, `Maybe` may have been thought of as a substitute for null references—as opposed to something that emerged organically based on specific motivating use cases after `Result` already existed. That said, in languages that do not have an equivalent of Roc's tag unions, it's much less ergonomic to write something like `Result a [ListWasEmpty]`, so that design would not fit those languages as well as it fits Roc. ## Why doesn't Roc have higher-kinded polymorphism or arbitrary-rank types? -_Since this is a FAQ answer, I'm going to assume familiarity with higher-kinded types and higher-rank types instead of including a primer on them._ +_Since this is a FAQ answer, it assumes familiarity with higher-kinded types and higher-rank types instead of including a primer on them._ A valuable aspect of Roc's type system is that it has decidable [principal](https://en.wikipedia.org/wiki/Principal_type) type inference. This means that: -* At compile time, Roc can correctly infer the types for every expression in a program, even if you don't annotate any of the types. -* This inference always infers the most general type possible; you couldn't possibly add a valid type annotation that would make the type more flexible than the one that Roc would infer if you deleted the annotation. +- At compile time, Roc can correctly infer the types for every expression in a program, even if you don't annotate any of the types. +- This inference always infers the most general type possible; you couldn't possibly add a valid type annotation that would make the type more flexible than the one that Roc would infer if you deleted the annotation. It's been proven that any type system which supports either [higher-kinded polymorphism](https://www.cl.cam.ac.uk/~jdy22/papers/lightweight-higher-kinded-polymorphism.pdf) or [arbitrary-rank types](https://www.microsoft.com/en-us/research/wp-content/uploads/2016/02/putting.pdf) cannot have decidable principal type inference. With either of those features in the language, there will be situations where the compiler @@ -146,40 +165,33 @@ would be unable to infer a type—and you'd have to write a type annotation. Thi situations where the editor would not be able to reliably tell you the type of part of your program, unlike today where it can accurately tell you the type of anything, even if you have no type annotations in your entire code base. -### Arbitrary-rank types +This is one factor that higher-rank and higher-kinded types have in common. There are other factors which are specific +to each. -Unlike arbitrary-rank (aka "Rank-N") types, both Rank-1 and Rank-2 type systems are compatible with principal -type inference. Roc currently uses Rank-1 types, and the benefits of Rank-N over Rank-2 don't seem worth -sacrificing principal type inference to attain, so let's focus on the trade-offs between Rank-1 and Rank-2. +### Higher-rank types -Supporting Rank-2 types in Roc has been discussed before, but it has several important downsides: +Supporting higher-rank types in Roc has been discussed before, but it has several important downsides: -* It would increase the complexity of the language. -* It would make some compiler error messages more confusing (e.g. they might mention `forall` because that was the most general type that could be inferred, even if that wasn't helpful or related to the actual problem). -* It would substantially increase the complexity of the type checker, which would necessarily slow it down. +- It would increase the complexity of the language. +- It would make some compiler error messages more confusing (e.g. they might mention `forall` because that was the most general type that could be inferred, even if that wasn't helpful or related to the actual problem). +- It would substantially increase the complexity of the type checker, which would necessarily slow it down. +- It would make some Roc programs run significantly more slowly. Roc compiles programs by [monomorphizing](https://en.wikipedia.org/wiki/Monomorphization), and it's unclear how we could fully monomorphize programs containing Rank-2 types. This means compiling programs which include Rank-2 types (or higher) would require sacrificing monomorphization, which would substantially degrade runtime performance. -No implementation of Rank-2 types can remove any of these downsides. Thus far, we've been able to come up -with sufficiently nice APIs that only require Rank-1 types, and we haven't seen a really compelling use case -where the gap between the Rank-2 and Rank-1 designs was big enough to justify switching to Rank-2. - -Since I prefer Roc being simpler and having a faster compiler with nicer error messages, my hope is that Roc -will never get Rank-2 types. However, it may turn out that in the future we learn about currently-unknown -upsides that somehow outweigh these downsides, so I'm open to considering the possibility - while rooting against it. +As such, the plan is for Roc to stick with Rank-1 types indefinitely. ### Higher-kinded polymorphism -I want to be really clear about this one: the explicit plan is that Roc will never support higher-kinded polymorphism. +The explicit plan is that Roc will never support higher-kinded polymorphism. -On the technical side, the reasons for this are ordinary: I understand the practical benefits and -drawbacks of HKP, and I think the drawbacks outweigh the benefits when it comes to Roc. (Those who come to a -different conclusion may think HKP's drawbacks would be less of a big a deal in Roc than I do. That's reasonable; -we programmers often weigh the same trade-offs differently.) To be clear, I think this in the specific context of -Roc; there are plenty of other languages where HKP seems like a great fit. For example, it's hard to imagine Haskell -without it. Similarly, I think lifetime annotations are a great fit for Rust, but don't think they'd be right -for Roc either. +On the technical side, the reasons for this are ordinary: like any language feature, HKP has both benefits and drawbacks, +and in the context of Roc, the drawbacks seem to outweigh the benefits. (Those who come to a different conclusion may +think HKP's drawbacks would be less of a big a deal in Roc. That's reasonable; we programmers often weigh the same +trade-offs differently.) To be clear, this analysis of HKP is in the specific context of Roc; there are plenty of +other languages where HKP seems like a great fit. For example, it's hard to imagine Haskell without it. Similarly, +lifetime annotations might be a natural fit for Rust, but they wouldn't be a good fit for Roc either. -I also think it's important to consider the cultural implications of deciding whether or not to support HKP. -To illustrate what I mean, imagine this conversation: +It's also important to consider the cultural implications of deciding whether or not to support HKP. +To illustrate these implications, imagine this conversation: **Programmer 1:** "How do you feel about higher-kinded polymorphism?" @@ -189,9 +201,9 @@ To illustrate what I mean, imagine this conversation: **Programmer 2:** "OH NO." -I've had several variations of this conversation: I'm talking about higher-kinded types, -another programmer asks what that means, I give monads as an example, and their reaction is strongly negative. -I've also had plenty of conversations with programmers who love HKP and vigorously advocate for its addition +For some, this conversation does not require imagining, because it's so familiar: higher-kinded types come up in +conversation, another programmer asks what that means, monads are given as an example, and their reaction is +strongly negative. On the flip side, plenty of programmers love HKP and vigorously advocate for its addition to languages they use which don't have it. Feelings about HKP seem strongly divided, maybe more so than any other type system feature besides static and dynamic types. @@ -201,154 +213,153 @@ language will inevitably follow. If the language does support HKP, one or more a around monads will inevitably follow, along with corresponding cultural changes. (See Scala for example.) Culturally, to support HKP is to take a side, and to decline to support it is also to take a side. -Given this, language designers have three options: +Given this, languages have three options: -* Have HKP and have Monad in the standard library. Embrace them and build a culture and ecosystem around them. -* Have HKP and don't have Monad in the standard library. An alternate standard lbirary built around monads will inevitably emerge, and both the community and ecosystem will divide themselves along pro-monad and anti-monad lines. -* Don't have HKP; build a culture and ecosystem around other things. +- Have HKP and have Monad in the standard library. Embrace them and build a culture and ecosystem around them. +- Have HKP and don't have Monad in the standard library. An alternate standard library built around monads will inevitably emerge, and both the community and ecosystem will divide themselves along pro-monad and anti-monad lines. +- Don't have HKP; build a culture and ecosystem around other things. -Considering that these are the only three options, I think the best choice for Roc—not only on a technical -level, but on a cultural level as well—is to make it clear that the plan is for Roc never to support HKP. -I hope this clarity can save a lot of community members' time that would otherwise be spent on advocacy or -arguing between the two sides of the divide. Again, I think it's completely reasonable for anyone to have a -different preference, but given that language designers can only choose one of these options, I'm confident -I've made the right choice for Roc by designing it never to have higher-kinded polymorphism. - -## Why do Roc's syntax and standard library differ from Elm's? - -Roc is a direct descendant of [Elm](https://elm-lang.org/). However, there are some differences between the two languages. - -Syntactic differences are among these. This is a feature, not a bug; if Roc had identical syntax to Elm, then it's -predictable that people would write code that was designed to work in both languages - and would then rely on -that being true, for example by making a package which advertised "Works in both Elm and Roc!" This in turn -would mean that later if either language were to change its syntax in a way that didn't make sense for the other, -the result would be broken code and sadness. - -So why does Roc have the specific syntax changes it does? Here are some brief explanations: - -* `#` instead of `--` for comments - this allows [hashbang](https://senthilnayagan.medium.com/shebang-hashbang-10966b8f28a8)s to work without needing special syntax. That isn't a use case Elm supports, but it is one Roc is designed to support. -* `{}` instead of `()` for the unit type - Elm has both, and they can both be used as a unit type. Since `{}` has other uses in the type system, but `()` doesn't, I consider it redundant and took it out. -* `when`...`is` instead of `case`...`of` - I predict it will be easier for beginners to pick up, because usually the way I explain `case`...`of` to beginners is by saying the words "when" and "is" out loud - e.g. "when `color` is `Red`, it runs this first branch; when `color` is `Blue`, it runs this other branch..." -* `:` instead of `=` for record field definitions (e.g. `{ foo: bar }` where Elm syntax would be `{ foo = bar }`): I like `=` being reserved for definitions, and `:` is the most popular alternative. -* Backpassing syntax - since Roc is designed to be used for use cases like command-line apps, shell scripts, and servers, I expect chained effects to come up a lot more often than they do in Elm. I think backpassing is nice for those use cases, similarly to how `do` notation is nice for them in Haskell. -* Tag unions instead of Elm's custom types (aka algebraic data types). This isn't just a syntactic change; tag unions are mainly in Roc because they can facilitate errors being accumulated across chained effects, which (as noted a moment ago) I expect to be a lot more common in Roc than in Elm. If you have tag unions, you don't really need a separate language feature for algebraic data types, since closed tag unions essentially work the same way - aside from not giving you a way to selectively expose variants or define phantom types. Roc's opaque types language feature covers those use cases instead. -* No `::` operator, or `::` pattern matching for lists. Both of these are for the same reason: an Elm `List` is a linked list, so both prepending to it and removing an element from the front are very cheap operations. In contrast, a Roc `List` is a flat array, so both prepending to it and removing an element from the front are among the most expensive operations you can possibly do with it! To get good performance, this usage pattern should be encouraged in Elm and discouraged in Roc. Since having special syntax would encourage it, it would not be good for Roc to have that syntax! -* No `<|` operator. In Elm, I almost exclusively found myself wanting to use this in conjunction with anonymous functions (e.g. `foo <| \bar -> ...`) or conditionals (e.g. `foo <| if bar then ...`). In Roc you can do both of these without the `<|`. That means the main remaining use for `<|` is to reduce parentheses, but I tend to think `|>` is better at that (or else the parens are fine), so after the other syntactic changes, I considered `<|` an unnecessary stylistic alternative to `|>` or parens. -* The `|>` operator passes the expression before the `|>` as the *first* argument to the function after the `|>` instead of as the last argument. See the section on currying for details on why this works this way. -* `:` instead of `type alias` - I like to avoid reserved keywords for terms that are desirable in userspace, so that people don't have to name things `typ` because `type` is a reserved keyword, or `clazz` because `class` is reserved. (I couldn't think of satisfactory alternatives for `as`, `when`, `is`, or `if` other than different reserved keywords. I could see an argument for `then`—and maybe even `is`—being replaced with a `->` or `=>` or something, but I don't anticipate missing either of those words much in userspace. `then` is used in JavaScript promises, but I think there are several better names for that function.) -* No underscores in variable names - I've seen Elm beginners reflexively use `snake_case` over `camelCase` and then need to un-learn the habit after the compiler accepted it. I'd rather have the compiler give feedback that this isn't the way to do it in Roc, and suggest a camelCase alternative. I've also seen underscores used for lazy naming, e.g. `foo` and then `foo_`. If lazy naming is the goal, `foo2` is just as concise as `foo_`, but `foo3` is more concise than `foo__`. So in a way, removing `_` is a forcing function for improved laziness. (Of course, more descriptive naming would be even better.) -* Trailing commas - I've seen people walk away (in some cases physically!) from Elm as soon as they saw the leading commas in collection literals. While I think they've made a mistake by not pushing past this aesthetic preference to give the language a chance, I also would prefer not put them in a position to make such a mistake in the first place. Secondarily, while I'm personally fine with either style, between the two I prefer the look of trailing commas. -* The `!` unary prefix operator. I didn't want to have a `Basics` module (more on that in a moment), and without `Basics`, this would either need to be called fully-qualified (`Bool.not`) or else a module import of `Bool.{ not }` would be necessary. Both seemed less nice than supporting the `!` prefix that's common to so many widely-used languages, especially when we already have a unary prefix operator of `-` for negation (e.g. `-x`). -* `!=` for the inequality operator (instead of Elm's `/=`) - this one pairs more naturally with the `!` prefix operator and is also very common in other languages. - -Roc also has a different standard library from Elm. Some of the differences come down to platforms and applications (e.g. having `Task` in Roc's standard library wouldn't make sense), but others do not. Here are some brief explanations: - -* No `Basics` module. I wanted to have a simple rule of "all modules in the standard library are imported by default, and so are their exposed types," and that's it. Given that I wanted the comparison operators (e.g. `<`) to work only on numbers, it ended up that having `Num` and `Bool` modules meant that almost nothing would be left for a `Basics` equivalent in Roc except `identity` and `Never`. The Roc type `[]` (empty tag union) is equivalent to `Never`, so that wasn't necessary, and I generally think that `identity` is a good concept but a sign of an incomplete API whenever its use comes up in practice. For example, instead of calling `|> List.filterMap identity` I'd rather have access to a more self-descriptive function like `|> List.dropNothings`. With `Num` and `Bool`, and without `identity` and `Never`, there was nothing left in `Basics`. -* `Str` instead of `String` - after using the `str` type in Rust, I realized I had no issue whatsoever with the more concise name, especially since it was used in so many places (similar to `Msg` and `Cmd` in Elm) - so I decided to save a couple of letters. -* No function composition operators - I stopped using these in Elm so long ago, at one point I forgot they were in the language! See the FAQ entry on currying for details about why. -* No `Char`. What most people think of as a "character" is a rendered glyph. However, rendered glyphs are comprised of [grapheme clusters](https://stackoverflow.com/a/27331885), which are a variable number of Unicode code points - and there's no upper bound on how many code points there can be in a single cluster. In a world of emoji, I think this makes `Char` error-prone and it's better to have `Str` be the only first-class unit. For convenience when working with unicode code points (e.g. for performance-critical tasks like parsing), the single-quote syntax is sugar for the corresponding `U32` code point - for example, writing `'鹏'` is exactly the same as writing `40527`. Like Rust, you get a compiler error if you put something in single quotes that's not a valid [Unicode scalar value](http://www.unicode.org/glossary/#unicode_scalar_value). -* No `Debug.log` - the editor can do a better job at this, or you can write `expect x != x` to see what `x` is when the expectation fails. Using the editor means your code doesn't change, and using `expect` gives a natural reminder to remove the debugging code before shipping: the build will fail. -* No `Debug.todo` - instead you can write a type annotation with no implementation below it; the type checker will treat it normally, but attempting to use the value will cause a runtime exception. This is a feature I've often wanted in Elm, because I like prototyping APIs by writing out the types only, but then when I want the compiler to type-check them for me, I end up having to add `Debug.todo` in various places. -* No `Maybe`. See the "Why doesn't Roc have a `Maybe`/`Option`/`Optional` type" FAQ question +Considering that these are the only three options, an early decision in Roc's design—not only on a technical +level, but on a cultural level as well—was to make it clear that the plan is for Roc never to support HKP. +The hope is that this clarity can save a lot of community members' time that would otherwise be spent on advocacy or +arguing between the two sides of the divide. Again, it's completely reasonable for anyone to have a different preference, +but given that languages can only choose one of these options, it seems clear that the right choice for Roc +is for it to never have higher-kinded polymorphism. ## Why aren't Roc functions curried by default? Although technically any language with first-class functions makes it possible to curry -any function (e.g. I can manually curry a Roc function `\x, y, z ->` by writing `\x -> \y -> \z ->` instead), +any function (e.g. anyone can manually curry a Roc function `\x, y, z ->` by writing `\x -> \y -> \z ->` instead), typically what people mean when they say Roc isn't a curried language is that Roc functions aren't curried -by default. For the rest of this section, I'll use "currying" as a shorthand for "functions that are curried +by default. The rest of this section will use "currying" as a shorthand for "functions that are curried by default" for the sake of brevity. -As I see it, currying has one major upside and several major downsides. The upside: +Currying makes function calls more concise in some cases, but it has several significant downsides: -* It makes function calls more concise in some cases. - -The downsides: - -* It lowers error message quality, because there can no longer be an error for "function called with too few arguments." (Calling a function with fewer arguments is always valid in curried functions; the error you get instead will unavoidably be some other sort of type mismatch, and it will be up to you to figure out that the real problem was that you forgot an argument.) -* It makes the `|>` operator more error-prone in some cases. -* It makes higher-order function calls need more parentheses in some cases. -* It significantly increases the language's learning curve. (More on this later.) -* It facilitates pointfree function composition. (More on why this is listed as a downside later.) +- It lowers error message quality, because there can no longer be an error for "function called with too few arguments." (Calling a function with fewer arguments is always valid in curried functions; the error you get instead will unavoidably be some other sort of type mismatch, and it will be up to you to figure out that the real problem was that you forgot an argument.) +- It makes the `|>` operator more error-prone in some cases. +- It makes higher-order function calls need more parentheses in some cases. +- It significantly increases the language's learning curve. (More on this later.) +- It facilitates pointfree function composition. (More on why this is listed as a downside later.) There's also a downside that it would make runtime performance of compiled programs worse by default, -but I assume it would be possible to optimize that away at the cost of slightly longer compile times. +but it would most likely be possible to optimize that away at the cost of slightly longer compile times. -I consider the one upside (conciseness in some places) extremely minor, and have almost never missed it in Roc. -Here are some more details about the downsides as I see them. +These downsides seem to outweigh the one upside (conciseness in some places). Here are some more details about each of +the downsides. ### Currying and the `|>` operator -In Roc, this code produces `"Hello, World!"` +In Roc, both of these expressions evaluate to `"Hello, World!"` -```elm -"Hello, World" - |> Str.concat "!" +```elixir +Str.concat "Hello, " "World!" ``` -This is because Roc's `|>` operator uses the expression before the `|>` as the *first* argument to the function -after it. For functions where both arguments have the same type, but it's obvious which argument goes where (e.g. -`Str.concat "Hello, " "World!"`, `List.concat [1, 2] [3, 4]`), this works out well. Another example would -be `|> Num.sub 1`, which subtracts 1 from whatever came before the `|>`. +```elixir +"Hello, " +|> Str.concat "World!" +``` -For this reason, "pipeline-friendliness" in Roc means that the first argument to each function is typically -the one that's most likely to be built up using a pipeline. For example, `List.map`: +It's unsurprising to most beginners that these work the same way; it's common for a beginner who has recently learned +how `|>` works to assume that `|> Str.concat "!"` would concatenate `!` onto the end of a string. -```elm +This is not how it works in curried languages, however. In curried languages with a `|>` operator, the first expression +still returns `"Hello, World!"` but the second one returns `"World!Hello, "` instead. This can be an unpleasant surprise +for beginners, but even experienced users commonly find that this behavior is less useful than having both of +these expressions evaluate to the same thing. + +In Roc, both expressions evaluate to the same thing because Roc's `|>` operator uses the expression before the `|>` as the _first_ argument, whereas in curried languages, `|>` uses it as the _last_ argument. For example, this is how `|>` works in both [F#](https://docs.microsoft.com/en-us/dotnet/fsharp/language-reference/symbol-and-operator-reference/#function-symbols-and-operators) and in [Elm](https://package.elm-lang.org/packages/elm/core/1.0.5/Basics#|%3E), both of which are curried languages. In contrast, Roc's `|>` design uses the same argument ordering as [Elixir](https://hexdocs.pm/elixir/1.14.0/Kernel.html#%7C%3E/2) and [Gleam](https://gleam.run/book/tour/functions.html#pipe-operator), none of which are curried languages. + +This comes up in other situations besides string concatenation. For example, consider subtraction and division: + +```elixir +someNumber +|> Num.div 2 +``` + +```elixir +someNumber +|> Num.sub 1 +``` + +Again, it's reasonable to expect that `|> Num.div 2` will divide a number by 2, and that +`|> Num.sub 1` will subtract 1 from a number. In Roc, this is how they work, but in +curried languages they work the opposite way: `|> Num.div 2` takes the number 2 and +divides it by a number, and `|> Num.sub 1` takes the number 1 and subtracts a number +from it. This is once again both more surprising to beginners and less useful to +experienced users. + +The way `|>` works in Roc has a second benefit when it comes to higher-order functions. Consider these two examples: + +```elixir +answer = List.map numbers \num -> + someFunction + "some argument" + num + anotherArg +``` + +```elixir numbers - |> List.map Num.abs +|> List.map Num.abs ``` -This argument ordering convention also often makes it possible to pass anonymous functions to higher-order -functions without needing parentheses, like so: +In Roc, `List.map` takes a list and then a function. Because of the way `|>` works in Roc, `numbers |> List.map Num.abs` passes `numbers` as the first argument to `List.map`, and `Num.abs` as the second argument. So both of these examples work fine. -```elm -List.map numbers \num -> Num.abs (num - 1) +In a curried language, these two examples couldn't both be valid. In order for `|> List.map Num.abs` to work in a curried language (where `|>` works the other way), `List.map` would have to take its arguments in the opposite order: the function first and the list second. + +This means the first example would have to change from this... + +```elixir +answer = List.map numbers \num -> + someFunction + "some argument" + num + anotherArg ``` -(If the arguments were reversed, this would be `List.map (\num -> Num.abs (num - 1)) numbers` and the -extra parentheses would be required.) +...to this: -Neither of these benefits is compatible with the argument ordering currying encourages. Currying encourages -`List.map` to take the `List` as its second argument instead of the first, so that you can partially apply it -like `(List.map Num.abs)`; if Roc introduced currying but kept the order of `List.map` the same way it is today, -then partially applying `List.map` (e.g. `(List.map numbers)`) would be much less useful than if the arguments -were swapped - but that in turn would make it less useful with `|>` and would require parentheses when passing -it an anonymous function. +```elixir +answer = + List.map + (\num -> + someFunction + "some argument" + num + anotherArg + ) + numbers +``` -This is a fundamental design tension. One argument order works well with `|>` (at least the way it works in Roc -today) and with passing anonymous functions to higher-order functions, and the other works well with currying. -It's impossible to have both. +The Roc version of this is nicer in that it doesn't require parentheses around the function argument. A curried language +could theoretically adopt Roc's style of `|>` (where it pipes in the first argument instead of the last argument), but +to get this second benefit, the language would also need to have `List.map` take the function as its second argument +instead of the first. However, this would work against currying's one upside; it would no longer work to write +`(List.map negate)` if the `List.map` arguments were flipped, the way they are in Roc. So currying and `|>` are unavoidably +in tension. -Of note, one possible design is to have currying while also having `|>` pass the *last* argument instead of the first. -This is what Elm does, and it makes pipeline-friendliness and curry-friendliness the same thing. However, it also -means that either `|> Str.concat "!"` would add the `"!"` to the front of the string, or else `Str.concat`'s -arguments would have to be flipped - meaning that `Str.concat "Hello, World" "!"` would evaluate to `"!Hello, World"`. - -The only way to have `Str.concat` work the way it does in Roc today (where both pipelines and non-pipeline calling -do what you'd want them to) is to order function arguments in a way that is not conducive to currying. This design -tension only exists if there's currying in the language; without it, you can order arguments for pipeline-friendliness -without concern. +As a historical note, these stylistic benefits (of `|> Num.sub 1` working as expected, and being able to write `List.map numbers \num ->`) were not among the original reasons Roc did not have currying. These benefits were discovered after the decision had already been made that Roc would not be a curried language, and they served to reinforce after the fact that the decision was the right one for Roc given the language's goals. ### Currying and learning curve -Prior to designing Roc, I taught a lot of beginner [Elm](https://elm-lang.org/) workshops. Sometimes at -conferences, sometimes for [Frontend Masters](https://frontendmasters.com/courses/intro-elm/), -sometimes for free at local coding bootcamps or meetup groups. -In total I've spent well over 100 hours standing in front of a class, introducing the students to their -first pure functional programming language. +Currying leads to function signatures that look surprising to beginners. For example, in Roc, the +[`Bool.and`](https://www.roc-lang.org/builtins/Bool#and) function has the type `Bool, Bool -> Bool`. If Roc were a +curried language, this function would instead have the type `Bool -> Bool -> Bool`. Since no mainstream programming +languages today are curried, anyone who knows a mainstream language and is learning their first curried language will +require additional explanation about why function types look this way. -Here was my experience teaching currying: +This explanation is nontrivial. It requires explaining partial application, how curried functions facilitate partial +application, how function signatures accurately reflect that they're curried, and going through examples for all of these. +All of it builds up to the punchline that "technically, all functions in this language have a single argument," which +some percentage of learners find interesting, and some percentage still find confusing even after all that explanation. -* The only way to avoid teaching it is to refuse to explain why multi-argument functions have multiple `->`s in them. (If you don't explain it, at least one student will ask about it - and many if not all of the others will wonder.) -* Teaching currying properly takes a solid chunk of time, because it requires explaining partial application, explaining how curried functions facilitate partial application, how function signatures accurately reflect that they're curried, and going through examples for all of these. -* Even after doing all this, and iterating on my approach each time to try to explain it more effectively than I had the time before, I'd estimate that under 50% of the class ended up actually understanding currying. I consistently heard that in practice it only "clicked" for most people after spending significantly more time writing code with it. - -This is not the end of the world, especially because it's easy enough to think "okay, I still don't totally get this -even after that explanation, but I can remember that function arguments are separated by `->` in this language -and maybe I'll understand the rest later." (Which they almost always do, if they stick with the language.) -Clearly currying doesn't preclude a language from being easy to learn, because Elm has currying, and Elm's learning -curve is famously gentle. +It's common for beginners to report that currying only "clicked" for them after spending significant time writing code +in a curried language. This is not the end of the world, especially because it's easy enough to think "I still don't +totally get this even after that explanation, but I can remember that function arguments are separated by `->` in this +language and maybe I'll understand the rest later." Clearly currying doesn't preclude a language from being easy to learn, +because Elm has currying, and Elm's learning curve is famously gentle. That said, beginners who feel confused while learning the language are less likely to continue with it. And however easy Roc would be to learn if it had currying, the language is certainly easier to learn without it. @@ -367,41 +378,72 @@ compose : (a -> b), (c -> a) -> (c -> b) compose = \f, g, x -> f (g x) ``` -Here's how I would instead write this: +Here's a way to write it without pointfree function composition: ```elm reverseSort : List elem -> List elem reverseSort = \list -> List.reverse (List.sort list) ``` -I've consistently found that I can more quickly and accurately understand function definitions that use -named arguments, even though the code is longer. I suspect this is because I'm faster at reading than I am at -desugaring, and whenever I read the top version I end up needing to mentally desugar it into the bottom version. -In more complex examples (this is among the tamest pointfree function composition examples I've seen), I make -a mistake in my mental desugaring, and misunderstand what the function is doing - which can cause bugs. +It's common for programmers to build a mental model of what `compose List.reverse List.sort` does by mentally +translating it into `\list -> List.reverse (List.sort list)`. This extra mental translation step makes it take +longer to read and to understand despite being technically more concise. In more complex examples (this +is among the tamest of pointfree function composition examples), the chances increase of making a mistake in +the mental translation step, leading to a misundesrtanding of what the function is doing—which can cause bugs. -I assumed I would get faster and more accurate at this over time. However, by now it's been about a decade -since I first learned about the technique, and I'm still slower and less accurate at reading code that uses -pointfree function composition (including if I wrote it - but even moreso if I didn't) than code written with -with named arguments. I've asked a lot of other programmers about their experiences with pointfree function -composition over the years, and the overwhelming majority of responses have been consistent with my experience. - -As such, my opinion about pointfree function composition has gotten less and less nuanced over time. I've now moved -past "it's the right tool for the job, sometimes" to concluding it's best thought of as an antipattern. This is -because I realized how much time I was spending evaluating on a case-by-case basis whether it might be the -right fit for a given situation. The time spent on this analysis alone vastly outweighed the sum of all the -benefits I got in the rare cases where I concluded it was a fit. So I've found the way to get the most out of -pointfree function composition is to never even think about using it; every other strategy leads to a worse outcome. - -Currying facilitates the antipattern of pointfree function composition, which I view as a downside of currying. +Some languages place such a high value on conciseness that they would consider the conciceness upside to outweigh +these downsides, but Roc is not one of those languages. It's considered stylistically better in Roc to write the +second version above. Given this, since currying facilitates pointfree function composition, making Roc a curried +language would have the downside of facilitating an antipattern in the language. Stacking up all these downsides of currying against the one upside of making certain function calls more concise, -I concluded that it would be a mistake to have it in Roc. +it seems clear that Roc should not be a curried language. -## Why are both rust and zig used? +## Will Roc ever have linear types, dependent types, refinement types, or uniqueness types? -At the start of the project, we did not know zig well and it was not production ready. The reason zig entered the project because it has many different backends (wasm, various assembly formats, llvm IR) and can create code with minimal dependencies +The plan is for Roc to never have linear types, dependent types, refinement types, or uniqueness types. -Rust has much more overhead in terms of code size. It's objectively not a lot, but it's less with zig. +Fast compile times are a primary goal for Roc, and a major downside of refinement types is an exponential increase in compile times. This rules out refinement types for Roc. -We think rust is a nicer language to work in for a project of this size. It has a type system that we're more familiar with, it has a package ecosystem and excellent tooling. +If Roc were to have linear types or uniqueness types, they would move things that are currently behind-the-scenes performance optimizations into the type system. For them to be effective across the ecosystem, they couldn't really be opt-in; everyone would have to use them, even those for whom the current system of behind-the-scenes optimizations already met their performance needs without any added type system complexity. Since the overwhelming majority of use cases are expected to fall into that latter group, adding linear types or uniqueness types to Roc would be a net negative for the ecosystem. + +Dependent types are too risky of a bet for Roc to take. They have been implemented in programming languages for three decades, and for at least half that time period, it has been easy to find predictions that dependent types will be the future of type systems. Much harder to find are success stories of complex applications built with dependent types, which realized benefits that significantly outweighed the substantial complexity of introducing value semantics to a type system. + +Perhaps more success stories will emerge over time, but in the meantime it remains an open question whether dependent types are net beneficial in practice to application development. Further experimentation would be required to answer this question, and Roc is not the right language to do those experiments. + +## Will Roc's compiler ever be self-hosted? (That is, will it ever be written in Roc?) + +The plan is to never implement Roc's compiler in Roc. + +The goal is for Roc's compiler to deliver the best user experience possible. Compiler performance is strongly influenced by how memory is used, and there are many performance benefits to be gained from using a systems language like Rust which offers more direct control over memory than Roc ever should. + +Roc isn't trying to be the best possible language for high-performance compiler development, but it is trying to have a high-performance compiler. The best tool for that job is a language other than Roc, so that's what we're using! + +## Why does Roc use the license it does? + +The short explanation for why Roc is released under the [Universal Permissive License](https://opensource.org/licenses/UPL): + +- Like [MIT](https://opensource.org/licenses/MIT), it's permissive and concise +- Like [Apache2](https://opensource.org/licenses/Apache-2.0), it protects against contributors claiming software patents over contributed code after the fact (MIT and BSD do not include protections against this) +- It's compatible with [GPLv2](https://opensource.org/licenses/GPL-2.0) (which [Apache2 is not](https://www.apache.org/licenses/GPL-compatibility.html)) +- It's one license, unlike "MIT or Apache2, at your choice" (which is how [Rust addressed the problem](https://internals.rust-lang.org/t/rationale-of-apache-dual-licensing/8952/4) of MIT not having patent protections but Apache2 not being GPLv2 compatible) +- It's been approved by OSI, FSF, and Oracle's lawyers, so it has been not only vetted by three giants in the world of software licensing, but also three giants with competing interests - and they all approved it. + +There's also [a longer explanation](https://github.com/roc-lang/roc/issues/1199) with more detail about the motivation and thought process, if you're interested. + +## Why does Roc use both Rust and Zig? + +Roc's compiler has always been written in [Rust](https://www.rust-lang.org/). Roc's standard library was briefly written in Rust, but was soon rewritten in [Zig](https://ziglang.org/). + +There were a few reasons for this rewrite. + +1. We struggled to get Rust to emit LLVM bitcode in the format we needed, which is important so that LLVM can do whole-program optimizations across the standard library and compiled application. +2. Since the standard library has to interact with raw generated machine code (or LLVM bitcode), the Rust code unavoidably needed `unsafe` annotations all over the place. This made one of Rust's biggest selling points inapplicable in this particular use case. +3. Given that Rust's main selling points are inapplicable (its package ecosystem being another), Zig's much faster compile times are a welcome benefit. +4. Zig has more tools for working in a memory-unsafe environment, such as reporting memory leaks in tests. These have been helpful in finding bugs that are out of scope for safe Rust. + +The split of Rust for the compiler and Zig for the standard library has worked well so far, and there are no plans to change it. + +## Why is the website so basic? + +We have a very basic website on purpose, it helps set expectations that roc is a work in progress and not ready yet for a first release. diff --git a/LEGAL_DETAILS b/LEGAL_DETAILS index f5b1e21a11..0afbff3f94 100644 --- a/LEGAL_DETAILS +++ b/LEGAL_DETAILS @@ -52,7 +52,7 @@ limitations under the License. * Elm - https://github.com/elm/compiler -This source code can be found in editor/src/lang/solve.rs and compiler/src/solve.rs, and is licensed under the following terms: +This source code can be found in compiler/src/solve.rs, and is licensed under the following terms: Copyright 2012-present Evan Czaplicki @@ -119,23 +119,6 @@ limitations under the License. =========================================================== -* learn-wgpu - https://github.com/sotrh/learn-wgpu - -This source code can be found in editor/src/graphics/lowlevel/buffer.rs, editor/src/graphics/primitives/text.rs, and editor/src/graphics/primitives/lowlevel/vertex.rs, and is licensed under the following terms: - - -MIT License - -Copyright (c) 2020 Benjamin Hansen - -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. - -=========================================================== - * pretty - https://github.com/Marwes/pretty.rs This source code can be found in vendor/pretty/ and is licensed under the following terms: @@ -186,24 +169,9 @@ limitations under the License. =========================================================== -* Ropey - https://github.com/cessen/ropey - -This source code can be found in editor/src/ui/text/lines.rs and is licensed under the following terms: - - -Copyright (c) 2017 Nathan Vegdahl - -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. - -=========================================================== - * Zig - https://ziglang.org -This source code can be found in compiler/builtins/bitcode/src/hash.zig, highlight/tests/peg_grammar.rs and highlight/src/highlight_parser.rs and is licensed under the following terms: +This source code can be found in highlight/tests/peg_grammar.rs and highlight/src/highlight_parser.rs and is licensed under the following terms: The MIT License (Expat) diff --git a/README.md b/README.md index 7be87d5bba..315dcbae72 100644 --- a/README.md +++ b/README.md @@ -1,116 +1,48 @@ -# The Roc Programming Language +# Work in progress! -Roc is a language for making delightful software. +[Roc](https://www.roc-lang.org) is not ready for a 0.1 release yet, but we do have: -The [tutorial](TUTORIAL.md) is the best place to learn about how to use the language - it assumes no prior knowledge of Roc or similar languages. (If you already know [Elm](https://elm-lang.org/), then [Roc for Elm Programmers](https://github.com/rtfeldman/roc/blob/trunk/roc-for-elm-programmers.md) may be of interest.) +- [**installation** guide](https://github.com/roc-lang/roc/tree/main/getting_started) +- [**tutorial**](https://roc-lang.org/tutorial) +- [**docs** for the standard library](https://www.roc-lang.org/builtins) +- [**examples**](https://github.com/roc-lang/examples/tree/main/examples) +- [**faq**: frequently asked questions](https://github.com/roc-lang/roc/blob/main/FAQ.md) +- [**group chat**](https://roc.zulipchat.com) for help, questions and discussions -There's also a folder of [examples](https://github.com/rtfeldman/roc/tree/trunk/examples) - the [CLI example](https://github.com/rtfeldman/roc/tree/trunk/examples/cli) in particular is a reasonable starting point to build on. - -If you have a specific question, the [FAQ](FAQ.md) might have an answer, although [Roc Zulip chat](https://roc.zulipchat.com) is overall the best place to ask questions and get help! It's also where we discuss [ideas](https://roc.zulipchat.com/#narrow/stream/304641-ideas) for the language. If you want to get involved in contributing to the language, Zulip is also a great place to ask about good first projects. - -## State of Roc - -Roc is not ready for production yet. You are likely to encounter bugs. Publishing packages or documentation is not yet supported. -Many programs can however be compiled correctly. Check out [examples](examples) and [examples/benchmarks](examples/benchmarks). There are minimal platforms for Rust, Zig, C, Swift and an HTTP server. We are hard at work to make programming in Roc a delightful experience! - -## Getting started - -- [Linux x86](getting_started/linux_x86.md) -- [Windows](getting_started/windows.md) -- [Other](getting_started/other.md) - -### Examples - -Run examples as follows: -``` -cargo run examples/hello-world/helloWorld.roc -``` -Some examples like `examples/benchmarks/NQueens.roc` require input after running. -For NQueens, input 10 in the terminal and press enter. - -[examples/benchmarks](examples/benchmarks) contains larger examples. - -**Tip:** when programming in roc, we recommend to execute `./roc check myproject/Foo.roc` before `./roc myproject/Foo.roc` or `./roc build myproject/Foo.roc`. `./roc check` can produce clear error messages in cases where building/running may panic. +If you'd like to contribute, check out [good first issues](https://github.com/roc-lang/roc/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22). Don't hesitate to ask for help on our [group chat](https://roc.zulipchat.com), we're friendly! ## Sponsors -We are very grateful for our sponsors [NoRedInk](https://www.noredink.com/) and [rwx](https://www.rwx.com). +You can 💜 **sponsor** 💜 Roc on: +- [GitHub](https://github.com/sponsors/roc-lang) +- [Liberapay](https://liberapay.com/roc_lang) -[NoRedInk logo](https://www.noredink.com/) +We are very grateful for our corporate sponsors [Vendr](https://www.vendr.com/), [RWX](https://www.rwx.com), [Tweede golf](https://tweedegolf.nl/en), and [ohne-makler](https://www.ohne-makler.net): + +[Vendr logo](https://www.vendr.com)      -[rwx logo](https://www.rwx.com) +[RWX logo](https://www.rwx.com) +     +[tweede golf logo](https://tweedegolf.nl/en) +[ohne-makler logo](https://www.ohne-makler.net) -## Applications and Platforms +If you would like your company to become a corporate sponsor of Roc's development, please [DM Richard Feldman on Zulip](https://roc.zulipchat.com/#narrow/pm-with/281383-user281383)! -Applications are often built on a *framework.* Typically, both application and framework are written in the same language. -* [Rails](https://rubyonrails.org/) applications are written in Ruby, and so is Rails. -* [Angular](https://angularjs.org/) applications are written in TypeScript, and so is Angular. -* [Phoenix](https://phoenixframework.org/) applications are written in Elixir, and so is Phoenix. +We'd also like to express our gratitude to our generous [individual sponsors](https://github.com/sponsors/roc-lang/)! A special thanks to those sponsoring $25/month or more: -Some programs support plugins. Often the plugins are written in the same language as the underlying program. -* [Webpack](https://webpack.js.org/) plugins are written in JavaScript, and so is Webpack. -* [Eclipse](https://www.eclipse.org/ide/) plugins are written in Java, and so is Eclipse. -* [Leiningen](https://leiningen.org/) plugins are written in Clojure, and so is Leiningen. +* [Drew Lazzeri](https://github.com/asteroidb612) +* [Alex Binaei](https://github.com/mrmizz) +* [Jono Mallanyk](https://github.com/jonomallanyk) +* [Chris Packett](https://github.com/chris-packett) +* [James Birtles](https://github.com/jamesbirtles) +* [Ivo Balbaert](https://github.com/Ivo-Balbaert) +* [Lucas Rosa](https://github.com/rvcas) +* [Jonas Schell](https://github.com/Ocupe) +* [Christopher Dolan](https://github.com/cdolan) +* [Nick Gravgaard](https://github.com/nickgravgaard) +* [Zeljko Nesic](https://github.com/popara) +* [Shritesh Bhattarai](https://github.com/shritesh) +* [Richard Feldman](https://github.com/rtfeldman) +* [Ayaz Hafiz](https://github.com/ayazhafiz) -All of these can be considered examples of a platform/application relationship. There is an underlying platform, and many applications are built on top of it. (Plugins are a type of application in this sense.) - -Sometimes, platforms and their applications are written in different languages. - -* [Neovim](https://neovim.io/) is written in C for performance, and its plugins can be written in languages such as Python, JS, and Ruby. -* [NGINX](https://www.nginx.com/) is written in C for performance, and its plugins can be written in a [subset of JavaScript](https://www.nginx.com/blog/introduction-nginscript/). -* [Unity](https://unity.com/) is written in C++ for performance, and Unity applications (such as games) can be written in C#, Boo, or a JavaScript dialect called UnityScript. - -Like in the previous examples, application authors building on these platforms get to use high-level languages with automatic memory management. They make no ergonomics sacrifices, and may not even be aware that the underlying platform is written in a lower-level language. - -By using systems-level programming languages like C and C++, platform authors sacrifice development speed, but unlock the highest possible performance characteristics. This is a tradeoff many platform authors are happy to accept, for the sake of having applications built on their platforms run very fast. - -## Roc's Design - -Roc is designed to make the "systems-level platform, higher-level application" experience as nice as possible. - -* **Application** authors code exclusively in Roc. It's a language designed for nice ergonomics. The syntax resembles Ruby or CoffeeScript, and it has a fast compiler with full type inference. -* **Platform** authors code almost exclusively in a systems-level language like C, C++, Rust, Swift or [Zig](https://ziglang.org/), except for the thin Roc API they expose to application authors. Roc application code compiles to machine code, and production builds of Roc apps benefit from the same [LLVM](https://llvm.org/) optimizations that C++, Rust, Swift and Zig do. Roc application authors do not need to know this lower-level code exists; all they have to interact with is the platform's API, which is exposed as an ordinary Roc API. - -Every Roc application is built on top of exactly one Roc platform. There is no such thing as a Roc application that runs without a platform, and there is no default platform. You must choose one! - -The core Roc language and standard library include no I/O operations, which gives platform authors complete control over which effects they want to support. Some of the implications of this include: - -* A high-performance build tool (or text editor) written in Rust can be a Roc platform with a strong plugin security model. For example, it could expose only operations allowing plugin authors to modify the contents of certain files, rather than allowing plugins arbitrary read/write access to the entire filesystem. -* A VR or [Arduino](https://www.arduino.cc/) platform can expose uncommon I/O operations supported by that hardware, while omitting common I/O operations that are unsupported (such as reading keyboard input from a terminal that doesn't exist). -* A high-performance Web server written in Rust can be a Roc platform where all I/O operations are implemented in terms of Streams or Observables rather than a more traditional asynchronous abstraction like Futures or Promises. This would mean all code in that platform's ecosystem would be necessarily built on a common streaming abstraction. - -## Project Goals - -Roc is in relatively early stages of development. It's currently possible to build both platforms and applications (see the [examples](https://github.com/rtfeldman/roc/tree/trunk/examples) folder for some examples that aren't particularly organized at the moment), although [documentation](https://github.com/rtfeldman/roc/tree/trunk/compiler/builtins/roc) is in even earlier stages than the compiler itself. - -Besides the above language design, a separate goal is for Roc to ship with an ambitiously boundary-pushing graphical editor. Not like "an IDE," but rather something that makes people say "I have never seen anything remotely like this outside of Bret Victor demos." - -One of the reasons this editor is coupled with the language itself is to allow package authors to include custom editor tooling inside packages. - -A trivial example: suppose I'm writing a Roc app for an Arduino platform. I install a platform-specific package for displaying text on a grid of LEDs. Because I've installed this package, at the call site where I call the function to specify the color of the text on the LEDs, my Roc editor displays an inline color picker. As I move a slider around to try out different colors, not only does my code change to reflect that value in realtime, but the physical LEDs in my room change color in realtime as well. As the application author, all I did to get that experience was to install the "text on an LED grid" package, nothing else. - -The goal is for this to be one of the most trivial, bare minimum examples of what the editor experience would be like. Hopefully, people in the future will look back on this example and say "that's so embarrassingly basic; why didn't you talk about one of the *actually great* things in the seamless editor plugin ecosystem?" - -Finally, some implementation goals: - -* The web server for the package manager is written in Roc (with an underlying Rust platform for the web server, for example [warp](https://github.com/seanmonstar/warp)). -* The editor plugins are written in Roc (with an underlying Rust platform for the editor itself, for example using [gfx-hal](https://github.com/gfx-rs/gfx)). -* The CLI (for building Roc projects on CI platforms) has its user interface written in Roc (with an underlying Rust platform for fast compilation and basic CLI interactions). - -It's an ambitious project! It'll take a long time to get where it's going, but hopefully it'll be worth the wait. - -## Getting Involved - -The number of people involved in Roc's development has been steadily increasing -over time - which has been great, because it's meant we've been able to onboard -people at a nice pace. (Most people who have contributed to Roc had previously -never done anything with Rust and also never worked on a compiler, but we've -been able to find beginner-friendly projects to get people up to speed gradually.) - -If you're interested in getting involved, check out -[CONTRIBUTING.md](https://github.com/rtfeldman/roc/blob/trunk/CONTRIBUTING.md)! - -## Name and Logo - -If you're curious about where the language's name and logo came from, -[here's an explanation](https://github.com/rtfeldman/roc/blob/trunk/name-and-logo.md). +Thank you all so much for helping Roc progress! diff --git a/TUTORIAL.md b/TUTORIAL.md deleted file mode 100644 index 703dea10e3..0000000000 --- a/TUTORIAL.md +++ /dev/null @@ -1,1978 +0,0 @@ -# Tutorial - -This is a tutorial for how to build Roc applications. It covers the REPL, basic -types (strings, lists, tags, and functions), syntax (`when`, `if then else`) -and more! - -Enjoy! - -## Getting started - -Learn how to install roc on your machine [here](https://github.com/rtfeldman/roc#getting-started). - -## Strings and Numbers - -Let’s start by getting acquainted with Roc’s Read Eval Print Loop, or REPL for -short. Run this in a terminal: - -``` -$ roc repl -``` - -You should see this: - -``` -The rockin’ roc repl -``` - -Try typing this in and pressing enter: - -```coffee ->> "Hello, World!" -"Hello, World!" : Str -``` - -Congratulations! You've just written your first Roc code! - -Specifically, you entered the *expression* `"Hello, World!"` into the REPL, -and the REPL printed it back out. It also printed `: Str`, which is the -expression's type. We'll talk about types later; for now, we'll ignore the `:` -and whatever comes after it whenever the REPL prints them. - -Let's try putting in a more complicated expression: - -```coffee ->> 1 + 1 -2 : Num * -``` - -According to the Roc REPL, one plus one equals two. Checks out! - -Roc will respect [order of operations](https://en.wikipedia.org/wiki/Order_of_operations) when using multiple arithmetic operators -like `+` and `-`, but you can use parentheses to specify exactly how they should -be grouped. - -```coffee ->> 1 + 2 * (3 - 4) --1 : Num * -``` - -Let's try calling a function: - -```coffee ->> Str.concat "Hi " "there!" -"Hi there!" : Str -``` - -In this expression, we're calling the `Str.concat` function -passing two arguments: the string `"Hi "` and the string `"there!"`. The -`Str.concat` function *concatenates* two strings together (that is, it puts -one after the other) and returns the resulting combined string of -`"Hi there!"`. - -Note that in Roc, we don't need parentheses or commas to call functions. -We don't write `Str.concat("Hi ", "there!")` but rather `Str.concat "Hi " "there!"`. - -Just like in the arithmetic example above, we can use parentheses to specify -how nested function calls should work. For example, we could write this: - -```coffee ->> Str.concat "Birds: " (Num.toStr 42) -"Birds: 42" : Str -``` - -This calls `Num.toStr` on the number `42`, which converts it into the string -`"42"`, and then passes that string as the second argument to `Str.concat`. -The parentheses are important here to specify how the function calls nest! -Try removing them, and see what happens: - -```coffee ->> Str.concat "Birds: " Num.toStr 42 - -``` - -This error message says that we've given `Str.concat` too many arguments. -Indeed we have! We've passed it three arguments: the string `"Birds"`, the -function `Num.toStr`, and the number `42`. That's not what we wanted to do. -Putting parentheses around the `Num.toStr 42` call clarifies that we want it -to be evaluated as its own expression, rather than being two arguments to -`Str.concat`. - -Both the `Str.concat` function and the `Num.toStr` function have a `.` in -their names. In `Str.concat`, `Str` is the name of a *module*, and `concat` -is the name of a function inside that module. Similarly, `Num` is a different -module, and `toStr` is a function inside that module. - -We'll get into more depth about modules later, but for now you can think of -a module as a named collection of functions. It'll be awhile before we want -to use them for more than that anyway! - -## Building an Application - -Let's move out of the REPL and create our first Roc application. - -Create a new file called `Hello.roc` and put this inside it: - -```coffee -app "hello" - packages { pf: "examples/interactive/cli-platform" } - imports [pf.Stdout] - provides [main] to pf - -main = Stdout.line "I'm a Roc application!" -``` - -> **NOTE:** This assumes you've put Hello.roc in the root directory of the Roc -> source code. If you'd like to put it somewhere else, you'll need to replace -> `"examples/interactive/cli-platform"` with the path to the -> `examples/interactive/cli-platform` folder in that source code. In the future, -> Roc will have the tutorial built in, and this aside will no longer be -> necessary! - -Try running this with: - -``` -$ roc Hello.roc -``` - -You should see this: - -``` -I'm a Roc application! -``` - -Congratulations - you've now written your first Roc application! We'll go over what the parts of -this file above `main` do later, but first let's play around a bit. -Try replacing the `main` line with this: - -```coffee -main = Stdout.line "There are \(total) animals." - -birds = 3 - -iguanas = 2 - -total = Num.toStr (birds + iguanas) -``` - -Now if you run `roc Hello.roc`, you should see this: - -``` -There are 5 animals. -``` - -`Hello.roc` now has four definitions - or *defs* for -short - namely, `main`, `birds`, `iguanas`, and `total`. - -A definition names an expression. -- The first def assigns the name `main` to the expression `Stdout.line "I have \(numDefs) definitions."`. The `Stdout.line` function takes a string and prints it as a line to [`stdout`] (the terminal's standard output device). -- The next two defs assign the names `birds` and `iguanas` to the expressions `3` and `2`. -- The last def assigns the name `total` to the expression `Num.toStr (birds + iguanas)`. - -Once we have a def, we can use its name in other expressions. -For example, the `total` expression refers to `birds` and `iguanas`. - -We can also refer to defs inside strings using *string interpolation*. The -string `"There are \(total) animals."` evaluates to the same thing as calling -`Str.concat "There are " (Str.concat total " animals.")` directly. - -You can name a def using any combination of letters and numbers, but they have -to start with a letter. Note that definitions are constant; once we've assigned -a name to an expression, we can't reassign it! We'd get an error if we wrote this: - -```coffee -birds = 3 - -birds = 2 -``` - -Order of defs doesn't matter. We defined `birds` and `iguanas` before -`total` (which uses both of them), but we defined `main` before `total` even though -it uses `total`. If you like, you can change the order of these defs to anything -you like, and everything will still work the same way! - -This works because Roc expressions don't have *side effects*. We'll talk more -about side effects later. - -## Functions and `if` - -So far we've called functions like `Num.toStr`, `Str.concat`, and `Stdout.line`. -Next let's try defining a function of our own. - -```coffee -main = Stdout.line "There are \(total) animals." - -birds = 3 - -iguanas = 2 - -total = addAndStringify birds iguanas - -addAndStringify = \num1, num2 -> - Num.toStr (num1 + num2) -``` - -This new `addAndStringify` function we've defined takes two numbers, adds them, -calls `Num.toStr` on the result, and returns that. The `\num1, num2 ->` syntax -defines a function's arguments, and the expression after the `->` is the body -of the function. The expression at the end of the body (`Num.toStr (num1 + num2)` -in this case) is returned automatically. - -Let's modify the function to return an empty string if the numbers add to zero. - -```coffee -addAndStringify = \num1, num2 -> - sum = num1 + num2 - - if sum == 0 then - "" - else - Num.toStr sum -``` - -We did two things here: -* We introduced a local def named `sum`, and set it equal to `num1 + num2`. Because we defined `sum` inside `addAndStringify`, it will not be accessible outside that function. -* We added an `if` / `then` / `else` conditional to return either `""` or `Num.toStr sum` depending on whether `sum == 0`. - -Of note, we couldn't have done `total = num1 + num2` because that would be -redefining `total` in the global scope, and defs can't be redefined. (However, we could use the name -`sum` for a def in a different function, because then they'd be in completely -different scopes and wouldn't affect each other.) - -Also note that every `if` must be accompanied by both `then` and also `else`. -Having an `if` without an `else` is an error, because in Roc, everything is -an expression - which means it must evaluate to a value. If there were ever an -`if` without an `else`, that would be an expression that might not evaluate to -a value! - -We can combine `if` and `else` to get `else if`, like so: - -```coffee -addAndStringify = \num1, num2 -> - sum = num1 + num2 - - if sum == 0 then - "" - else if sum < 0 then - "negative" - else - Num.toStr sum -``` - -Note that `else if` is not a separate language keyword! It's just an `if`/`else` where -the `else` branch contains another `if`/`else`. This is easier to see with different indentation: - -```coffee -addAndStringify = \num1, num2 -> - sum = num1 + num2 - - if sum == 0 then - "" - else - if sum < 0 then - "negative" - else - Num.toStr sum -``` - -This code is equivalent to writing `else if sum < 0 then` on one line, although the stylistic -convention is to write `else if` on the same line. - -## Records - -Currently our `addAndStringify` function takes two arguments. We can instead make -it take one argument like so: - -```coffee -total = addAndStringify { birds: 5, iguanas: 7 } - -addAndStringify = \counts -> - Num.toStr (counts.birds + counts.iguanas) -``` - -The function now takes a *record*, which is a group of values that travel together. -Records are not objects; they don't have methods or inheritance, they just store values. - -We create the record when we write `{ birds: 5, iguanas: 7 }`. This defines -a record with two *fields* - namely, the `birds` field and the `iguanas` field - -and then assigns the number `5` to the `birds` field and the number `7` to the -`iguanas` field. Order doesn't matter with record fields; we could have also specified -`iguanas` first and `birds` second, and Roc would consider it the exact same record. - -When we write `counts.birds`, it accesses the `birds` field of the `counts` record, -and when we write `counts.iguanas` it accesses the `iguanas` field. When we use `==` -on records, it compares all the fields in both records with `==`, and only returns true -if all fields on both records return true for their `==` comparisons. If one record has -more fields than the other, or if the types associated with a given field are different -between one field and the other, the Roc compiler will give an error at build time. - -> **Note:** Some other languages have a concept of "identity equality" that's separate from -> the "structural equality" we just described. Roc does not have a concept of identity equality; -> this is the only way equality works! - -The `addAndStringify` function will accept any record with at least the fields `birds` and -`iguanas`, but it will also accept records with more fields. For example: - -```coffee -total = addAndStringify { birds: 5, iguanas: 7 } - -totalWithNote = addAndStringify { birds: 4, iguanas: 3, note: "Whee!" } - -addAndStringify = \counts -> - Num.toStr (counts.birds + counts.iguanas) -``` - -This works because `addAndStringify` only uses `counts.birds` and `counts.iguanas`. -If we were to use `counts.note` inside `addAndStringify`, then we would get an error -because `total` is calling `addAndStringify` passing a record that doesn't have a `note` field. - -Record fields can have any combination of types we want. `totalWithNote` uses a record that -has a mixture of numbers and strings, but we can also have record fields with other types of -values - including other records, or even functions! - -```coffee -{ birds: 4, nestedRecord: { someFunction: (\arg -> arg + 1), name: "Sam" } } -``` - -### Record shorthands - -Roc has a couple of shorthands you can use to express some record-related operations more concisely. - -Instead of writing `\record -> record.x` we can write `.x` and it will evaluate to the same thing: -a function that takes a record and returns its `x` field. You can do this with any field you want. -For example: - -```elm -returnFoo = .foo - -returnFoo { foo: "hi!", bar: "blah" } -# returns "hi!" -``` - -Whenever we're setting a field to be a def that has the same name as the field - -for example, `{ x: x }` - we can shorten it to just writing the name of the def alone - -for example, `{ x }`. We can do this with as many fields as we like, e.g. -`{ x: x, y: y }` can alternately be written `{ x, y }`, `{ x: x, y }`, or `{ x, y: y }`. - -### Record destructuring - -We can use *destructuring* to avoid naming a record in a function argument, instead -giving names to its individual fields: - -```coffee -addAndStringify = \{ birds, iguanas } -> - Num.toStr (birds + iguanas) -``` - -Here, we've *destructured* the record to create a `birds` def that's assigned to its `birds` -field, and an `iguanas` def that's assigned to its `iguanas` field. We can customize this if we -like: - -```coffee -addAndStringify = \{ birds, iguanas: lizards } -> - Num.toStr (birds + lizards) -``` - -In this version, we created a `lizards` def that's assigned to the record's `iguanas` field. -(We could also do something similar with the `birds` field if we like.) - -It's possible to destructure a record while still naming it. Here's an example where we -use the `as` keyword to name the record `counts` while also destructuring its fields: - -```coffee -addAndStringify = \{ iguanas: lizards } as counts -> - Num.toStr (counts.birds + lizards) -``` - -Notice that here we didn't bother destructuring the `birds` field. You can always omit fields -from a destructure if you aren't going to use them! - -Finally, destructuring can be used in defs too: - -```coffee -{ x, y } = { x: 5, y: 10 } -``` - -### Building records from other records - -So far we've only constructed records from scratch, by specifying all of their fields. We can -also construct new records by using another record to use as a starting point, and then -specifying only the fields we want to be different. For example, here are two ways to -get the same record: - -```coffee -original = { birds: 5, iguanas: 7, zebras: 2, goats: 1 } - -fromScratch = { birds: 4, iguanas: 3, zebras: 2, goats: 1 } -fromOriginal = { original & birds: 4, iguanas: 3 } -``` - -The `fromScratch` and `fromOriginal` records are equal, although they're assembled in -different ways. - -* `fromScratch` was built using the same record syntax we've been using up to this point. -* `fromOriginal` created a new record using the contents of `original` as defaults for fields that it didn't specify after the `&`. - -Note that when we do this, the fields you're overriding must all be present on the original record, -and their values must have the same type as the corresponding values in the original record. - -## Tags - -Sometimes we want to represent that something can have one of several values. For example: - -```coffee -stoplightColor = - if something > 0 then - Red - else if something == 0 then - Yellow - else - Green -``` - -Here, `stoplightColor` can have one of three values: `Red`, `Yellow`, or `Green`. -The capitalization is very important! If these were lowercase (`red`, `yellow`, `green`), -then they would refer to defs. However, because they are capitalized, they instead -refer to *tags*. - -A tag is a literal value just like a number or a string. Similarly to how I can write -the number `42` or the string `"forty-two"` without defining them first, I can also write -the tag `FortyTwo` without defining it first. Also, similarly to how `42 == 42` and -`"forty-two" == "forty-two"`, it's also the case that `FortyTwo == FortyTwo`. - -Speaking of equals, if we put `42 == 42` into `roc repl`, the output we'll see is `True`. -This is because booleans in Roc are tags; a boolean is either the tag `True` or the tag -`False`. So I can write `if True then` or `if False then` and it will work as expected, -even though I'd get an error if I wrote `if "true" then` or `if 1 then`. (Roc doesn't -have a concept of "truthiness" - you always have to use booleans for conditionals!) - -Let's say we wanted to turn `stoplightColor` from a `Red`, `Green`, or `Yellow` into -a string. Here's one way we could do that: - -```elm -stoplightStr = - if stoplightColor == Red then - "red" - else if stoplightColor == Green then - "green" - else - "yellow" -``` - -We can express this logic more concisely using `when`/`is` instead of `if`/`then`: - -```elm -stoplightStr = - when stoplightColor is - Red -> "red" - Green -> "green" - Yellow -> "yellow" -``` - -This results in the same value for `stoplightStr`. In both the `when` version and the `if` version, we -have three conditional branches, and each of them evaluates to a string. The difference is how the -conditions are specified; here, we specify between `when` and `is` that we're making comparisons against -`stoplightColor`, and then we specify the different things we're comparing it to: `Red`, `Green`, and `Yellow`. - -Besides being more concise, there are other advantages to using `when` here. - -1. We don't have to specify an `else` branch, so the code can be more self-documenting about exactly what all the options are. -2. We get more compiler help. If we try deleting any of these branches, we'll get a compile-time error saying that we forgot to cover a case that could come up. For example, if we delete the `Green ->` branch, the compiler will say that we didn't handle the possibility that `stoplightColor` could be `Green`. It knows this because `Green` is one of the possibilities in our `stoplightColor = if …` definition. - -We can still have the equivalent of an `else` branch in our `when` if we like. Instead of writing "else", we write -"_ ->" like so: - -```coffee -stoplightStr = - when stoplightColor is - Red -> "red" - _ -> "not red" -``` - -This lets us more concisely handle multiple cases. However, it has the downside that if we add a new case - -for example, if we introduce the possibility of `stoplightColor` being `Orange`, the compiler can no longer -tell us we forgot to handle that possibility in our `when`. After all, we are handling it - just maybe not -in the way we'd decide to if the compiler had drawn our attention to it! - -We can make this `when` *exhaustive* (that is, covering all possibilities) without using `_ ->` by using -`|` to specify multiple matching conditions for the same branch: - -```coffee -stoplightStr = - when stoplightColor is - Red -> "red" - Green | Yellow -> "not red" -``` - -You can read `Green | Yellow` as "either `Green` or `Yellow`". By writing it this way, if we introduce the -possibility that `stoplightColor` can be `Orange`, we'll get a compiler error telling us we forgot to cover -that case in this `when`, and then we can handle it however we think is best. - -We can also combine `if` and `when` to make branches more specific: - -```coffee -stoplightStr = - when stoplightColor is - Red -> "red" - Green | Yellow if contrast > 75 -> "not red, but very high contrast" - Green | Yellow if contrast > 50 -> "not red, but high contrast" - Green | Yellow -> "not red" -``` - -This will give the same answer for `spotlightStr` as if we had written the following: - -```coffee -stoplightStr = - when stoplightColor is - Red -> "red" - Green | Yellow -> - if contrast > 75 then - "not red, but very high contrast" - else if contrast > 50 then - "not red, but high contrast" - else - "not red" -``` - -Either style can be a reasonable choice depending on the circumstances. - -### Tags with payloads - -Tags can have *payloads* - that is, values contained within them. For example: - -```coffee -stoplightColor = - if something > 100 then - Red - else if something > 0 then - Yellow - else if something == 0 then - Green - else - Custom "some other color" - -stoplightStr = - when stoplightColor is - Red -> "red" - Green | Yellow -> "not red" - Custom description -> description -``` - -This makes two changes to our earlier `stoplightColor` / `stoplightStr` example. - -1. We sometimes set `stoplightColor` to be `Custom "some other color"`. When we did this, we gave the `Custom` tag a *payload* of the string `"some other color"`. -2. We added a `Custom` tag in our `when`, with a payload which we named `description`. Because we did this, we were able to refer to `description` in the body of the branch (that is, the part after the `->`) just like any other def. - -Any tag can be given a payload like this. A payload doesn't have to be a string; we could also have said (for example) `Custom { r: 40, g: 60, b: 80 }` to specify an RGB color instead of a string. Then in our `when` we could have written `Custom record ->` and then after the `->` used `record.r`, `record.g`, and `record.b` to access the `40`, `60`, `80` values. We could also have written `Custom { r, g, b } ->` to *destructure* the record, and then -accessed these `r`, `g`, and `b` defs after the `->` instead. - -A tag can also have a payload with more than one value. Instead of `Custom { r: 40, g: 60, b: 80 }` we could -write `Custom 40 60 80`. If we did that, then instead of destructuring a record with `Custom { r, g, b } ->` -inside a `when`, we would write `Custom r g b ->` to destructure the values directly out of the payload. - -We refer to whatever comes before a `->` in a `when` expression as a *pattern* - so for example, in the -`Custom description -> description` branch, `Custom description` would be a pattern. In programming, using -patterns in branching conditionals like `when` is known as [pattern matching](https://en.wikipedia.org/wiki/Pattern_matching). You may hear people say things like "let's pattern match on `Custom` here" as a way to -suggest making a `when` branch that begins with something like `Custom description ->`. - -## Lists - -Another thing we can do in Roc is to make a *list* of values. Here's an example: - -```coffee -names = ["Sam", "Lee", "Ari"] -``` - -This is a list with three elements in it, all strings. We can add a fourth -element using `List.append` like so: - -```coffee -List.append names "Jess" -``` - -This returns a **new** list with `"Jess"` after `"Ari"`, and doesn't modify the original list at all. -All values in Roc (including lists, but also records, strings, numbers, and so on) are immutable, -meaning whenever we want to "change" them, we want to instead pass them to a function which returns some -variation of what was passed in. - -### List.map - -A common way to transform one list into another is to use `List.map`. Here's an example of how to -use it: - -```coffee -List.map [1, 2, 3] \num -> num * 2 -``` - -This returns `[2, 4, 6]`. `List.map` takes two arguments: - -1. An input list -2. A function that will be called on each element of that list - -It then returns a list which it creates by calling the given function on each element in the input list. -In this example, `List.map` calls the function `\num -> num * 2` on each element in -`[1, 2, 3]` to get a new list of `[2, 4, 6]`. - -We can also give `List.map` a named function, instead of an anonymous one: - -For example, the `Num.isOdd` function returns `True` if it's given an odd number, and `False` otherwise. -So `Num.isOdd 5` returns `True` and `Num.isOdd 2` returns `False`. - -So calling `List.map [1, 2, 3] Num.isOdd` returns a new list of `[True, False, True]`. - -### List element type compatibility - -If we tried to give `List.map` a function that didn't work on the elements in the list, then we'd get -an error at compile time. Here's a valid, and then an invalid example: - -```coffee -# working example -List.map [-1, 2, 3, -4] Num.isNegative -# returns [True, False, False, True] -``` - -```coffee -# invalid example -List.map ["A", "B", "C"] Num.isNegative -# error: isNegative doesn't work on strings! -``` - -Because `Num.isNegative` works on numbers and not strings, calling `List.map` with `Num.isNegative` and a -list of numbers works, but doing the same with a list of strings doesn't work. - -This wouldn't work either: - -```coffee -List.map ["A", "B", "C", 1, 2, 3] Num.isNegative -``` - -In fact, this wouldn't work for a more fundamental reason: every element in a Roc list has to share the same type. -For example, we can have a list of strings like `["Sam", "Lee", "Ari"]`, or a list of numbers like -`[1, 2, 3, 4, 5]` but we can't have a list which mixes strings and numbers like `["Sam", 1, "Lee", 2, 3]` - -that would be a compile-time error. - -Ensuring all elements in a list share a type eliminates entire categories of problems. -For example, it means that whenever you use `List.append` to -add elements to a list, as long as you don't have any compile-time errors, you won't get any runtime errors -from calling `List.map` afterwards - no matter what you appended to the list! More generally, it's safe to assume -that unless you run out of memory, `List.map` will run successfully unless you got a compile-time error about an -incompatibility (like `Num.negate` on a list of strings). - -### Lists that hold elements of different types - -We can use tags with payloads to make a list that contains a mixture of different types. For example: - -```coffee -List.map [StrElem "A", StrElem "b", NumElem 1, StrElem "c", NumElem -3] \elem -> - when elem is - NumElem num -> Num.isNegative num - StrElem str -> Str.isCapitalized str - -# returns [True, False, False, False, True] -``` - -Compare this with the example from earlier, which caused a compile-time error: - -```coffee -List.map ["A", "B", "C", 1, 2, 3] Num.isNegative -``` - -The version that uses tags works because we aren't trying to call `Num.isNegative` on each element. -Instead, we're using a `when` to tell when we've got a string or a number, and then calling either -`Num.isNegative` or `Str.isCapitalized` depending on which type we have. - -We could take this as far as we like, adding more different tags (e.g. `BoolElem True`) and then adding -more branches to the `when` to handle them appropriately. - -### Using tags as functions - -Let's say I want to apply a tag to a bunch of elements in a list. For example: - -```elm -List.map ["a", "b", "c"] \str -> Foo str -``` - -This is a perfectly reasonable way to write it, but I can also write it like this: - -```elm -List.map ["a", "b", "c"] Foo -``` - -These two versions compile to the same thing. As a convenience, Roc lets you specify -a tag name where a function is expected; when you do this, the compiler infers that you -want a function which uses all of its arguments as the payload to the given tag. - -### `List.any` and `List.all` - -There are several functions that work like `List.map` - they walk through each element of a list and do -something with it. Another is `List.any`, which returns `True` if calling the given function on any element -in the list returns `True`: - -```coffee -List.any [1, 2, 3] Num.isOdd -# returns True because 1 and 3 are odd -``` -```coffee -List.any [1, 2, 3] Num.isNegative -# returns False because none of these is negative -``` - -There's also `List.all` which only returns `True` if all the elements in the list pass the test: - -```coffee -List.all [1, 2, 3] Num.isOdd -# returns False because 2 is not odd -``` -```coffee -List.all [1, 2, 3] Num.isPositive -# returns True because all of these are positive -``` - -### Removing elements from a list - -You can also drop elements from a list. One way is `List.dropAt` - for example: - -```coffee -List.dropAt ["Sam", "Lee", "Ari"] 1 -# drops the element at offset 1 ("Lee") and returns ["Sam", "Ari"] -``` - -Another way is to use `List.keepIf`, which passes each of the list's elements to the given -function, and then keeps them only if that function returns `True`. - -```coffee -List.keepIf [1, 2, 3, 4, 5] Num.isEven -# returns [2, 4] -``` - -There's also `List.dropIf`, which does the reverse: - -```coffee -List.dropIf [1, 2, 3, 4, 5] Num.isEven -# returns [1, 3, 5] -``` - -### Custom operations that walk over a list - -You can make your own custom operations that walk over all the elements in a list, using `List.walk`. -Let's look at an example and then walk (ha!) through it. - -```coffee -List.walk [1, 2, 3, 4, 5] { evens: [], odds: [] } \state, elem -> - if Num.isEven elem then - { state & evens: List.append state.evens elem } - else - { state & odds: List.append state.odds elem } - -# returns { evens: [2, 4], odds: [1, 3, 5] } -``` - -`List.walk` walks through each element of the list, building up a state as it goes. At the end, -it returns the final state - whatever it ended up being after processing the last element. The `\state, elem ->` -function it takes as its last argument accepts both the current state as well as the current list element -it's looking at, and then returns the new state based on whatever it decides to do with that element. - -In this example, we walk over the list `[1, 2, 3, 4, 5]` and add each element to either the `evens` or `odds` -field of a `state` record `{ evens, odds }`. By the end, that record has a list of all the even numbers in the -list as well as a list of all the odd numbers. - -The state doesn't have to be a record; it can be anything you want. For example, if you made it a boolean, you -could implement `List.any` using `List.walk`. You could also make the state be a list, and implement `List.map`, -`List.keepIf`, or `List.dropIf`. There are a lot of things you can do with `List.walk` - it's very flexible! - -It can be tricky to remember the argument order for `List.walk` at first. A helpful trick is that the arguments -follow the same pattern as what we've seen with `List.map`, `List.any`, `List.keepIf`, and `List.dropIf`: the -first argument is a list, and the last argument is a function. The difference here is that `List.walk` has one -more argument than those other functions; the only place it could go while preserving that pattern is the middle! - -That third argument specifies the initial `state` - what it's set to before the `\state, elem ->` function has -been called on it even once. (If the list is empty, the `\state, elem ->` function will never get called and -the initial state gets returned immediately.) - -> **Note:** Other languages give this operation different names, such as "fold," "reduce," "accumulate," -> "aggregate," "compress," and "inject." - -### Getting an individual element from a list - -Another thing we can do with a list is to get an individual element out of it. `List.get` is a common way to do this; -it takes a list and an index, and then returns the element at that index...if there is one. But what if there isn't? - -For example, what do each of these return? - -```coffee -List.get ["a", "b", "c"] 1 -``` -```coffee -List.get ["a", "b", "c"] 100 -``` - -The answer is that the first one returns `Ok "b"` and the second one returns `Err OutOfBounds`. -They both return tags! This is done so that the caller becomes responsible for handling the possibility that -the index is outside the bounds of that particular list. - -Here's how calling `List.get` can look in practice: - -```coffee -when List.get ["a", "b", "c"] index is - Ok str -> "I got this string: \(str)" - Err OutOfBounds -> "That index was out of bounds, sorry!" -``` - -There's also `List.first`, which always gets the first element, and `List.last` which always gets the last. -They return `Err ListWasEmpty` instead of `Err OutOfBounds`, because the only way they can fail is if you -pass them an empty list! - -These functions demonstrate a common pattern in Roc: operations that can fail returning either an `Ok` tag -with the answer (if successful), or an `Err` tag with another tag describing what went wrong (if unsuccessful). -In fact, it's such a common pattern that there's a whole module called `Result` which deals with these two tags. -Here are some examples of `Result` functions: - -```coffee -Result.withDefault (List.get ["a", "b", "c"] 100) "" -# returns "" because that's the default we said to use if List.get returned an Err -``` - -```coffee -Result.isOk (List.get ["a", "b", "c"] 1) -# returns True because `List.get` returned an `Ok` tag. (The payload gets ignored.) - -# Note: There's a Result.isErr function that works similarly. -``` - -### The pipe operator - -When you have nested function calls, sometimes it can be clearer to write them in a "pipelined" -style using the `|>` operator. Here are three examples of writing the same expression; they all -compile to exactly the same thing, but two of them use the `|>` operator to change how the calls look. - -```coffee -Result.withDefault (List.get ["a", "b", "c"] 1) "" -``` - -```coffee -List.get ["a", "b", "c"] 1 - |> Result.withDefault "" -``` - -The `|>` operator takes the value that comes before the `|>` and passes it as the first argument to whatever -comes after the `|>` - so in the example above, the `|>` takes `List.get ["a", "b", "c"] 1` and passes that -value as the first argument to `Result.withDefault` - making `""` the second argument to `Result.withDefault`. - -We can take this a step further like so: - -```coffee -["a", "b", "c"] - |> List.get 1 - |> Result.withDefault "" -``` - -This is still equivalent to the first expression. Since `|>` is known as the "pipe operator," we can read -this as "start with `["a", "b", "c"]`, then pipe it to `List.get`, then pipe it to `Result.withDefault`." - -One reason the `|>` operator injects the value as the first argument is to make it work better with -functions where argument order matters. For example, these two uses of `List.append` are equivalent: - -```coffee -List.append ["a", "b", "c"] "d" -``` -```coffee -["a", "b", "c"] - |> List.append "d" -``` - -Another example is `Num.div`. All three of the following do the same thing, because `a / b` in Roc is syntax -sugar for `Num.div a b`: - -```coffee -first / second -``` -```coffee -Num.div first second -``` -```coffee -first - |> Num.div second -``` - -All operators in Roc are syntax sugar for normal function calls. See the "Operator Desugaring Table" -at the end of this tutorial for a complete list of them. - -## Types - -Sometimes you may want to document the type of a definition. For example, you might write: - -```ruby -# Takes a firstName string and a lastName string, and returns a string -fullName = \firstName, lastName -> - "\(firstName) \(lastName)" -``` - -Comments can be valuable documentation, but they can also get out of date and become misleading. -If someone changes this function and forgets to update the comment, it will no longer be accurate. - -### Type annotations - -Here's another way to document this function's type, which doesn't have that problem: - -```coffee -fullName : Str, Str -> Str -fullName = \firstName, lastName -> - "\(firstName) \(lastName)" -``` - -The `fullName :` line is a *type annotation*. It's a strictly optional piece of metadata we can add -above a def to describe its type. Unlike a comment, the Roc compiler will check type annotations for -accuracy. If the annotation ever doesn't fit with the implementation, we'll get a compile-time error. - -The annotation `fullName : Str, Str -> Str` says "`fullName` is a function that takes two strings as -arguments and returns a string." - -We can give type annotations to any value, not just functions. For example: - -```coffee -firstName : Str -firstName = "Amy" - -lastName : Str -lastName = "Lee" -``` - -These annotations say that both `firstName` and `lastName` have the type `Str`. - -We can annotate records similarly. For example, we could move `firstName` and `lastName` into a record like so: - -```coffee -amy : { firstName : Str, lastName : Str } -amy = { firstName: "Amy", lastName: "Lee" } - -jen : { firstName : Str, lastName : Str } -jen = { firstName: "Jen", lastName: "Majura" } -``` - -When we have a recurring type annotation like this, it can be nice to give it its own name. We do this like -so: - -```coffee -Musician : { firstName : Str, lastName : Str } - -amy : Musician -amy = { firstName: "Amy", lastName: "Lee" } - -simone : Musician -simone = { firstName: "Simone", lastName: "Simons" } -``` - -Here, `Musician` is a *type alias*. A type alias is like a def, except it gives a name to a type -instead of to a value. Just like how you can read `name : Str` as "`name` has the type `Str`," -you can also read `Musician : { firstName : Str, lastName : Str }` as "`Musician` has the type -`{ firstName : Str, lastName : Str }`." - -We can also give type annotations to tag unions: - -```coffee -colorFromStr : Str -> [Red, Green, Yellow] -colorFromStr = \string -> - when string is - "red" -> Red - "green" -> Green - _ -> Yellow -``` - -You can read the type `[Red, Green, Yellow]` as "a tag union of the tags `Red`, `Green`, and `Yellow`." - -When we annotate a list type, we have to specify the type of its elements: - -```coffee -names : List Str -names = ["Amy", "Simone", "Tarja"] -``` - -You can read `List Str` as "a list of strings." Here, `Str` is a *type parameter* that tells us what type of -`List` we're dealing with. `List` is a *parameterized type*, which means it's a type that requires a type -parameter; there's no way to give something a type of `List` without a type parameter - you have to specify -what type of list it is, such as `List Str` or `List Bool` or `List { firstName : Str, lastName : Str }`. - -There are some functions that work on any list, regardless of its type parameter. For example, `List.isEmpty` -has this type: - -```coffee -isEmpty : List * -> Bool -``` - -The `*` is a *wildcard type* - that is, a type that's compatible with any other type. `List *` is compatible -with any type of `List` - so, `List Str`, `List Bool`, and so on. So you can call -`List.isEmpty ["I am a List Str"]` as well as `List.isEmpty [True]`, and they will both work fine. - -The wildcard type also comes up with empty lists. Suppose we have one function that takes a `List Str` and another -function that takes a `List Bool`. We might reasonably expect to be able to pass an empty list (that is, `[]`) to -either of these functions. And so we can! This is because a `[]` value has the type `List *` - that is, -"a list with a wildcard type parameter," or "a list whose element type could be anything." - -`List.reverse` works similarly to `List.isEmpty`, but with an important distinction. As with `isEmpty`, we can -call `List.reverse` on any list, regardless of its type parameter. However, consider these calls: - -```coffee -strings : List Str -strings = List.reverse ["a", "b"] - -bools : List Bool -bools = List.reverse [True, False] -``` - -In the `strings` example, we have `List.reverse` returning a `List Str`. In the `bools` example, it's returning a -`List Bool`. So what's the type of `List.reverse`? - -We saw that `List.isEmpty` has the type `List * -> Bool`, so we might think the type of `List.reverse` would be -`reverse : List * -> List *`. However, remember that we also saw that the type of the empty list is `List *`? -`List * -> List *` is actually the type of a function that always returns empty lists! That's not what we want. - -What we want is something like one of these: - -```coffee -reverse : List elem -> List elem -``` -```coffee -reverse : List value -> List value -``` -```coffee -reverse : List a -> List a -``` - -Any of these will work, because `elem`, `value`, and `a` are all *type variables*. A type variable connects -two or more types in the same annotation. So you can read `List elem -> List elem` as "takes a list and returns -a list that has the same element type." Just like `List.reverse` does! - -You can choose any name you like for a type variable, but it has to be lowercase. (You may have noticed all the -types we've used until now are uppercase; that is no accident! Lowercase types are always type variables, so -all other named types have to be uppercase.) All three of the above type annotations are equivalent; -the only difference is that we chose different names (`elem`, `value`, and `a`) for their type variables. - -You can tell some interesting things about functions based on the type parameters involved. For example, -any function that returns `List *` definitely always returns an empty list. You don't need to look at the rest -of the type annotation, or even the function's implementation! The only way to have a function that returns -`List *` is if it returns an empty list. - -Similarly, the only way to have a function whose type is `a -> a` is if the function's implementation returns -its argument without modifying it in any way. This is known as [the identity function](https://en.wikipedia.org/wiki/Identity_function). - -## Numeric types - -Roc has different numeric types that each have different tradeoffs. -They can all be broken down into two categories: [fractions](https://en.wikipedia.org/wiki/Fraction), -and [integers](https://en.wikipedia.org/wiki/Integer). In Roc we call these `Frac` and `Int` for short. - -### Integers - -Roc's integer types have two important characteristics: their *size* and their [*signedness*](https://en.wikipedia.org/wiki/Signedness). -Together, these two characteristics determine the range of numbers the integer type can represent. - -For example, the Roc type `U8` can represent the numbers 0 through 255, whereas the `I16` type can represent -the numbers -32768 through 32767. You can actually infer these ranges from their names (`U8` and `I16`) alone! - -The `U` in `U8` indicates that it's *unsigned*, meaning that it can't have a minus [sign](https://en.wikipedia.org/wiki/Sign_(mathematics)), and therefore can't be negative. The fact that it's unsigned tells us immediately that -its lowest value is zero. The 8 in `U8` means it is 8 [bits](https://en.wikipedia.org/wiki/Bit) in size, which -means it has room to represent 2⁸ (which is equal to 256) different numbers. Since one of those 256 different numbers -is 0, we can look at `U8` and know that it goes from `0` (since it's unsigned) to `255` (2⁸ - 1, since it's 8 bits). - -If we change `U8` to `I8`, making it a *signed* 8-bit integer, the range changes. Because it's still 8 bits, it still -has room to represent 2⁸ (that is, 256) different numbers. However, now in addition to one of those 256 numbers -being zero, about half of the rest will be negative, and the others positive. So instead of ranging from, say -255 -to 255 (which, counting zero, would represent 511 different numbers; too many to fit in 8 bits!) an `I8` value -ranges from -128 to 127. - -Notice that the negative extreme is `-128` versus `127` (not `128`) on the positive side. That's because of -needing room for zero; the slot for zero is taken from the positive range because zero doesn't have a minus sign. -So in general, you can find the lowest signed number by taking its total range (256 different numbers in the case -of an 8-bit integer) and dividing it in half (half of 256 is 128, so -128 is `I8`'s lowest number). To find the -highest number, take the positive version of the lowest number (so, convert `-128` to `128`) and then subtract 1 -to make room for zero (so, `128` becomes `127`; `I8` ranges from -128 to 127). - -Following this pattern, the 16 in `I16` means that it's a signed 16-bit integer. -That tells us it has room to represent 2¹⁶ (which is equal to 65536) different numbers. Half of 65536 is 32768, -so the lowest `I16` would be -32768, and the highest would be 32767. Knowing that, we can also quickly tell that -the lowest `U16` would be zero (since it always is for unsigned integers), and the highest `U16` would be 65535. - -Choosing a size depends on your performance needs and the range of numbers you want to represent. Consider: - -* Larger integer sizes can represent a wider range of numbers. If you absolutely need to represent numbers in a certain range, make sure to pick an integer size that can hold them! -* Smaller integer sizes take up less memory. These savings rarely matters in variables and function arguments, but the sizes of integers that you use in data structures can add up. This can also affect whether those data structures fit in [cache lines](https://en.wikipedia.org/wiki/CPU_cache#Cache_performance), which can easily be a performance bottleneck. -* Certain processors work faster on some numeric sizes than others. There isn't even a general rule like "larger numeric sizes run slower" (or the reverse, for that matter) that applies to all processors. In fact, if the CPU is taking too long to run numeric calculations, you may find a performance improvement by experimenting with numeric sizes that are larger than otherwise necessary. However, in practice, doing this typically degrades overall performance, so be careful to measure properly! - -Here are the different fixed-size integer types that Roc supports: - -| Range | Type | Size | -| -----------------------------------------------------------: | :---- | :------- | -| `-128`
`127` | `I8` | 1 Byte | -| `0`
`255` | `U8` | 1 Byte | -| `-32_768`
`32_767` | `I16` | 2 Bytes | -| `0`
`65_535` | `U16` | 2 Bytes | -| `-2_147_483_648`
`2_147_483_647` | `I32` | 4 Bytes | -| `0`
(over 4 billion) `4_294_967_295` | `U32` | 4 Bytes | -| `-9_223_372_036_854_775_808`
`9_223_372_036_854_775_807` | `I64` | 8 Bytes | -| `0`
(over 18 quintillion) `18_446_744_073_709_551_615` | `U64` | 8 Bytes | -| `-170_141_183_460_469_231_731_687_303_715_884_105_728`
`170_141_183_460_469_231_731_687_303_715_884_105_727` | `I128` | 16 Bytes | -| `0`
(over 340 undecillion) `340_282_366_920_938_463_463_374_607_431_768_211_455` | `U128` | 16 Bytes | - -Roc also has one variable-size integer type: `Nat` (short for "natural number"). -The size of `Nat` is equal to the size of a memory address, which varies by system. -For example, when compiling for a 64-bit system, `Nat` works the same way as `U64`. -When compiling for a 32-bit system, it works the same way as `U32`. Most popular -computing devices today are 64-bit, so `Nat` is usually the same as `U64`, but -Web Assembly is typically 32-bit - so when running a Roc program built for Web Assembly, -`Nat` will work like a `U32` in that program. - -A common use for `Nat` is to store the length of a collection like a `List`; -there's a function `List.len : List * -> Nat` which returns the length of the given list. -64-bit systems can represent longer lists in memory than 32-bit systems can, -which is why the length of a list is represented as a `Nat`. - -If any operation would result in an integer that is either too big -or too small to fit in that range (e.g. calling `Int.maxI32 + 1`, which adds 1 to -the highest possible 32-bit integer), then the operation will *overflow*. -When an overflow occurs, the program will crash. - -As such, it's very important to design your integer operations not to exceed these bounds! - -### Fractions - -Roc has three fractional types: - -* `F32`, a 32-bit [floating-point number](https://en.wikipedia.org/wiki/IEEE_754) -* `F64`, a 64-bit [floating-point number](https://en.wikipedia.org/wiki/IEEE_754) -* `Dec`, a 128-bit decimal [fixed-point number](https://en.wikipedia.org/wiki/Fixed-point_arithmetic) - -These are different from integers in that they can represent numbers with fractional components, -such as 1.5 and -0.123. - -`Dec` is the best default choice for representing base-10 decimal numbers -like currency, because it is base-10 under the hood. In contrast, -`F64` and `F32` are base-2 under the hood, which can lead to decimal -precision loss even when doing addition and subtraction. For example, when -using `F64`, running 0.1 + 0.2 returns 0.3000000000000000444089209850062616169452667236328125, -whereas when using `Dec`, 0.1 + 0.2 returns 0.3. - -`F32` and `F64` have direct hardware support on common processors today. There is no hardware support -for fixed-point decimals, so under the hood, a `Dec` is an `I128`; operations on it perform -[base-10 fixed-point arithmetic](https://en.wikipedia.org/wiki/Fixed-point_arithmetic) -with 18 decimal places of precision. - -This means a `Dec` can represent whole numbers up to slightly over 170 -quintillion, along with 18 decimal places. (To be precise, it can store -numbers between `-170_141_183_460_469_231_731.687303715884105728` -and `170_141_183_460_469_231_731.687303715884105727`.) Why 18 -decimal places? It's the highest number of decimal places where you can still -convert any `U64` to a `Dec` without losing information. - -While the fixed-point `Dec` has a fixed range, the floating-point `F32` and `F64` do not. -Instead, outside of a certain range they start to lose precision instead of immediately overflowing -the way integers and `Dec` do. `F64` can represent [between 15 and 17 significant digits](https://en.wikipedia.org/wiki/Double-precision_floating-point_format) before losing precision, whereas `F32` can only represent [between 6 and 9](https://en.wikipedia.org/wiki/Single-precision_floating-point_format#IEEE_754_single-precision_binary_floating-point_format:_binary32). - -There are some use cases where `F64` and `F32` can be better choices than `Dec` -despite their precision drawbacks. For example, in graphical applications they -can be a better choice for representing coordinates because they take up less memory, -various relevant calculations run faster, and decimal precision loss isn't as big a concern -when dealing with screen coordinates as it is when dealing with something like currency. - -### Num, Int, and Frac - -Some operations work on specific numeric types - such as `I64` or `Dec` - but operations support -multiple numeric types. For example, the `Num.abs` function works on any number, since you can -take the [absolute value](https://en.wikipedia.org/wiki/Absolute_value) of integers and fractions alike. -Its type is: - -```elm -abs : Num a -> Num a -``` - -This type says `abs` takes a number and then returns a number of the same type. That's because the -`Num` type is compatible with both integers and fractions. - -There's also an `Int` type which is only compatible with integers, and a `Frac` type which is only -compatible with fractions. For example: - -```elm -Num.xor : Int a, Int a -> Int a -``` - -```elm -Num.cos : Frac a -> Frac a -``` - -When you write a number literal in Roc, it has the type `Num *`. So you could call `Num.xor 1 1` -and also `Num.cos 1` and have them all work as expected; the number literal `1` has the type -`Num *`, which is compatible with the more constrained types `Int` and `Frac`. For the same reason, -you can pass number literals to functions expecting even more constrained types, like `I32` or `F64`. - -### Typed Number Literals -When writing a number literal in Roc you can specify the numeric type as a suffix of the literal. -`1u8` specifies `1` as an unsigned 8-bit integer, `5i32` specifies `5` as a signed 32-bit integer, etc. -The full list of possible suffixes includes: -`i8`, `u8`, `i16`, `u16`, `i32`, `u32`, `i64`, `u64`, `i128`, `u128`, `nat`, `f32`, `f64`, `dec` - -### Hexadecimal Integer Literals -Integer literals can be written in hexadecimal form by prefixing with `0x` followed by hexadecimal characters. -`0xFE` evaluates to decimal `254` -The integer type can be specified as a suffix to the hexadecimal literal, -so `0xC8u8` evaluates to decimal `200` as an unsigned 8-bit integer. - -### Binary Integer Literals -Integer literals can be written in binary form by prefixing with `0b` followed by the 1's and 0's representing -each bit. `0b0000_1000` evaluates to decimal `8` -The integer type can be specified as a suffix to the binary literal, -so `0b0100u8` evaluates to decimal `4` as an unsigned 8-bit integer. - -## Interface modules - -[This part of the tutorial has not been written yet. Coming soon!] - -## Builtin modules - -There are several modules that are built into the Roc compiler, which are imported automatically into every -Roc module. They are: - -1. `Bool` -2. `Str` -3. `Num` -4. `List` -5. `Result` -6. `Dict` -7. `Set` - -You may have noticed that we already used the first five - for example, when we wrote `Str.concat` and `Num.isEven`, -we were referencing functions stored in the `Str` and `Num` modules. - -These modules are not ordinary `.roc` files that live on your filesystem. Rather, they are built directly into the -Roc compiler. That's why they're called "builtins!" - -Besides being built into the compiler, the builtin modules are different from other modules in that: - -* They are always imported. You never need to add them to `imports`. -* All their types are imported unqualified automatically. So you never need to write `Num.Nat`, because it's as if the `Num` module was imported using `imports [Num.{ Nat }]` (and the same for all the other types in the `Num` module). - -## The app module header - -Let's take a closer look at the part of `Hello.roc` above `main`: - -```coffee -app "hello" - packages { pf: "examples/interactive/cli-platform" } - imports [pf.Stdout] - provides main to pf -``` - -This is known as a *module header*. Every `.roc` file is a *module*, and there -are different types of modules. We know this particular one is an *application module* -(or *app module* for short) because it begins with the `app` keyword. - -The line `app "hello"` states that this module defines a Roc application, and -that building this application should produce an executable named `hello`. This -means when you run `roc Hello.roc`, the Roc compiler will build an executable -named `hello` (or `hello.exe` on Windows) and run it. You can also build the executable -without running it by running `roc build Hello.roc`. - -The remaining lines all involve the *platform* this application is built on: - -```coffee -packages { pf: "examples/interactive/cli-platform" } -imports [pf.Stdout] -provides main to pf -``` - -The `packages { pf: "examples/interactive/cli-platform" }` part says two things: - -- We're going to be using a *package* (that is, a collection of modules) called `"examples/interactive/cli-platform"` -- We're going to name that package `pf` so we can refer to it more concisely in the future. - -The `imports [pf.Stdout]` line says that we want to import the `Stdout` module -from the `pf` package, and make it available in the current module. - -This import has a direct interaction with our definition of `main`. Let's look -at that again: - -```coffee -main = Stdout.line "I'm a Roc application!" -``` - -Here, `main` is calling a function called `Stdout.line`. More specifically, it's -calling a function named `line` which is exposed by a module named -`Stdout`. - -When we write `imports [pf.Stdout]`, it specifies that the `Stdout` -module comes from the `pf` package. - -Since `pf` was the name we chose for the `examples/interactive/cli-platform` -package (when we wrote `packages { pf: "examples/interactive/cli-platform" }`), -this `imports` line tells the Roc compiler that when we call `Stdout.line`, it -should look for that `line` function in the `Stdout` module of the -`examples/interactive/cli-platform` package. - -# Building a Command-Line Interface (CLI) - -## Tasks - -Tasks are technically not part of the Roc language, but they're very common in -platforms. Let's use the CLI platform in `examples/interactive/cli-platform` as an example! - -In the CLI platform, we have four operations we can do: - -* Write a string to the console -* Read a string from user input -* Write a string to a file -* Read a string from a file - -We'll use these four operations to learn about tasks. - -First, let's do a basic "Hello World" using the tutorial app. - -```coffee -app "cli-tutorial" - packages { pf: "examples/interactive/cli-platform" } - imports [pf.Stdout] - provides [main] to pf - -main = - Stdout.line "Hello, World!" -``` - -The `Stdout.line` function takes a `Str` and writes it to [standard output](https://en.wikipedia.org/wiki/Standard_streams#Standard_output_(stdout)). -It has this type: - -```coffee -Stdout.line : Str -> Task {} * -``` - -A `Task` represents an *effect* - that is, an interaction with state outside your Roc program, -such as the console's standard output, or a file. - -When we set `main` to be a `Task`, the task will get run when we run our program. Here, we've set -`main` to be a task that writes `"Hello, World!"` to `stdout` when it gets run, so that's what -our program does! - -`Task` has two type parameters: the type of value it produces when it finishes running, and any -errors that might happen when running it. `Stdout.line` has the type `Task {} *` because it doesn't -produce any values when it finishes (hence the `{}`) and there aren't any errors that can happen -when it runs (hence the `*`). - -In contrast, `Stdin.line` produces a `Str` when it finishes reading from [standard input](https://en.wikipedia.org/wiki/Standard_streams#Standard_input_(stdin)). That `Str` is reflected in its type: - -```coffee -Stdin.line : Task Str * -``` - -Let's change `main` to read a line from `stdin`, and then print it back out again: - -```swift -app "cli-tutorial" - packages { pf: "examples/interactive/cli-platform" } - imports [pf.Stdout, pf.Stdin, pf.Task] - provides [main] to pf - -main = - Task.await Stdin.line \text -> - Stdout.line "You just entered: \(text)" -``` - -If you run this program, at first it won't do anything. It's waiting for you to type something -in and press Enter! Once you do, it should print back out what you entered. - -The `Task.await` function combines two tasks into one bigger `Task` which first runs one of the -given tasks and then the other. In this case, it's combining a `Stdin.line` task with a `Stdout.line` -task into one bigger `Task`, and then setting `main` to be that bigger task. - -The type of `Task.await` is: - -```haskell -Task.await : Task a err, (a -> Task b err) -> Task b err -``` - -The second argument to `Task.await` is a "callback function" which runs after the first task -completes. This callback function receives the output of that first task, and then returns -the second task. This means the second task can make use of output from the first task, like -we did in our `\text -> …` callback function here: - -```swift -\text -> - Stdout.line "You just entered: \(text)" -``` - -Notice that, just like before, we're still setting `main` to be a single `Task`. This is how we'll -always do it! We'll keep building up bigger and bigger `Task`s out of smaller tasks, and then setting -`main` to be that one big `Task`. - -For example, we can print a prompt before we pause to read from `stdin`, so it no longer looks like -the program isn't doing anything when we start it up: - -```swift -main = - Task.await (Stdout.line "Type something press Enter:") \_ -> - Task.await Stdin.line \text -> - Stdout.line "You just entered: \(text)" -``` - -This works, but we can make it a little nicer to read. Let's change it to the following: - -```haskell -app "cli-tutorial" - packages { pf: "examples/interactive/cli-platform" } - imports [pf.Stdout, pf.Stdin, pf.Task.{ await }] - provides [main] to pf - -main = - await (Stdout.line "Type something press Enter:") \_ -> - await Stdin.line \text -> - Stdout.line "You just entered: \(text)" -``` - -Here we've changed how we're importing the `Task` module. Before it was -`pf.Task` and now it's `pf.Task.{ await }`. The difference is that we're -importing `await` in an *unqualified* way, meaning now whenever we write `await` -in this module, it will refer to `Task.await` - so we no longer need to write -`Task.` every time we want to `await`. - -It's most common in Roc to call functions from other modules in a *qualified* way -(`Task.await`) rather than unqualified (`await`) like this, but it can be nice -for a function with an uncommon name (like "await") which often gets called repeatedly -across a small number of lines of code. - -Speaking of calling `await` repeatedly, if we keep calling it more and more on this -code, we'll end up doing a lot of indenting. If we'd rather not indent so much, we -can rewrite `main` into this style which looks different but does the same thing: - -```swift -main = - _ <- await (Stdout.line "Type something press Enter:") - text <- await Stdin.line - - Stdout.line "You just entered: \(text)" -``` - -This `<-` syntax is called *backpassing*. The `<-` is a way to define an -anonymous function, just like `\ … ->` is. - -Here, we're using backpassing to define two anonymous functions. Here's one of them: - -```swift -text <- - -Stdout.line "You just entered: \(text)" -``` - -It may not look like it, but this code is defining an anonymous function! You might -remember it as the anonymous function we previously defined like this: - -```swift -\text -> - Stdout.line "You just entered: \(text)" -``` - -These two anonymous functions are the same, just defined using different syntax. - -The reason the `<-` syntax is called *backpassing* is because it both defines a -function and passes that function *back* as an argument to whatever comes after -the `<-` (which in this case is `await Stdin.line`). - -Let's look at these two complete expressions side by side. They are both -saying exactly the same thing, with different syntax! - -Here's the original: - -```swift -await Stdin.line \text -> - Stdout.line "You just entered: \(text)" -``` - -And here's the equivalent expression with backpassing syntax: - -```swift -text <- await Stdin.line - -Stdout.line "You just entered: \(text)" -``` - -Here's the other function we're defining with backpassing: - -```swift -_ <- -text <- await Stdin.line - -Stdout.line "You just entered: \(text)" -``` - -We could also have written that function this way if we preferred: - -```swift -_ <- - -await Stdin.line \text -> - Stdout.line "You just entered: \(text)" -``` - -This is using a mix of a backpassing function `_ <-` and a normal function `\text ->`, -which is totally allowed! Since backpassing is nothing more than syntax sugar for -defining a function and passing back as an argument to another function, there's no -reason we can't mix and match if we like. - -That said, the typical style in which this `main` would be written in Roc is using -backpassing for all the `await` calls, like we had above: - -```swift -main = - _ <- await (Stdout.line "Type something press Enter:") - text <- await Stdin.line - - Stdout.line "You just entered: \(text)" -``` - -This way, it reads like a series of instructions: -1. First, run the `Stdout.line` task and await its completion. Ignore its output (hence the underscore in `_ <-`) -2. Next, run the `Stdin.line` task and await its completion. Name its output `text`. -3. Finally, run the `Stdout.line` task again, using the `text` value we got from the `Stdin.line` effect. - -Some important things to note about backpassing and `await`: -* `await` is not a language keyword in Roc! It's referring to the `Task.await` function, which we imported unqualified by writing `Task.{ await }` in our module imports. (That said, it is playing a similar role here to the `await` keyword in languages that have `async`/`await` keywords, even though in this case it's a function instead of a special keyword.) -* Backpassing syntax does not need to be used with `await` in particular. It can be used with any function. -* Roc's compiler treats functions defined with backpassing exactly the same way as functions defined the other way. The only difference between `\text ->` and `text <-` is how they look, so feel free to use whichever looks nicer to you! - -# Appendix: Advanced Concepts - -Here are some concepts you likely won't need as a beginner, but may want to know about eventually. -This is listed as an appendix rather than the main tutorial, to emphasize that it's totally fine -to stop reading here and go build things! - -## Open Records and Closed Records - -Let's say I write a function which takes a record with a `firstName` -and `lastName` field, and puts them together with a space in between: - -```swift -fullName = \user -> - "\(user.firstName) \(user.lastName)" -``` - -I can pass this function a record that has more fields than just -`firstName` and `lastName`, as long as it has *at least* both of those fields -(and both of them are strings). So any of these calls would work: - -* `fullName { firstName: "Sam", lastName: "Sample" }` -* `fullName { firstName: "Sam", lastName: "Sample", email: "blah@example.com" }` -* `fullName { age: 5, firstName: "Sam", things: 3, lastName: "Sample", role: Admin }` - -This `user` argument is an *open record* - that is, a description of a minimum set of fields -on a record, and their types. When a function takes an open record as an argument, -it's okay if you pass it a record with more fields than just the ones specified. - -In contrast, a *closed record* is one that requires an exact set of fields (and their types), -with no additional fields accepted. - -If we add a type annotation to this `fullName` function, we can choose to have it accept either -an open record or a closed record: - -```coffee -# Closed record -fullName : { firstName : Str, lastName : Str } -> Str -fullName = \user -> - "\(user.firstName) \(user.lastName)" -``` - -```coffee -# Open record (because of the `*`) -fullName : { firstName : Str, lastName : Str }* -> Str -fullName = \user -> - "\(user.firstName) \(user.lastName)" -``` - -The `*` in the type `{ firstName : Str, lastName : Str }*` is what makes it an open record type. -This `*` is the *wildcard type* we saw earlier with empty lists. (An empty list has the type `List *`, -in contrast to something like `List Str` which is a list of strings.) - -This is because record types can optionally end in a type variable. Just like how we can have `List *` -or `List a -> List a`, we can also have `{ first : Str, last : Str }*` or -`{ first : Str, last : Str }a -> { first: Str, last : Str }a`. The differences are that in `List a`, -the type variable is required and appears with a space after `List`; in a record, the type variable -is optional, and appears (with no space) immediately after `}`. - -If the type variable in a record type is a `*` (such as in `{ first : Str, last : Str }*`), then -it's an open record. If the type variable is missing, then it's a closed record. You can also specify -a closed record by putting a `{}` as the type variable (so for example, `{ email : Str }{}` is another way to write -`{ email : Str }`). In practice, closed records are basically always written without the `{}` on the end, -but later on we'll see a situation where putting types other than `*` in that spot can be useful. - -## Constrained Records - -The type variable can also be a named type variable, like so: - -```coffee -addHttps : { url : Str }a -> { url : Str }a -addHttps = \record -> - { record & url: "https://\(record.url)" } -``` - -This function uses *constrained records* in its type. The annotation is saying: -* This function takes a record which has at least a `url` field, and possibly others -* That `url` field has the type `Str` -* It returns a record of exactly the same type as the one it was given - -So if we give this function a record with five fields, it will return a record with those -same five fields. The only requirement is that one of those fields must be `url : Str`. - -In practice, constrained records appear in type annotations much less often than open or closed records do. - -Here's when you can typically expect to encounter these three flavors of type variables in records: - -- *Open records* are what the compiler infers when you use a record as an argument, or when destructuring it (for example, `{ x, y } =`). -- *Closed records* are what the compiler infers when you create a new record (for example, `{ x: 5, y: 6 }`) -- *Constrained records* are what the compiler infers when you do a record update (for example, `{ user & email: newEmail }`) - -Of note, you can pass a closed record to a function that accepts a smaller open record, but not the reverse. -So a function `{ a : Str, b : Bool }* -> Str` can accept an `{ a : Str, b : Bool, c : Bool }` record, -but a function `{ a : Str, b : Bool, c : Bool } -> Str` would not accept an `{ a : Str, b : Bool }*` record. - -This is because if a function accepts `{ a : Str, b : Bool, c : Bool }`, that means it might access the `c` -field of that record. So if you passed it a record that was not guaranteed to have all three of those fields -present (such as an `{ a : Str, b : Bool }*` record, which only guarantees that the fields `a` and `b` are present), -the function might try to access a `c` field at runtime that did not exist! - -## Type Variables in Record Annotations - -You can add type annotations to make record types less flexible than what the compiler infers, but not more -flexible. For example, you can use an annotation to tell the compiler to treat a record as closed when it would -be inferred as open (or constrained), but you can't use an annotation to make a record open when it would be -inferred as closed. - -If you like, you can always annotate your functions as accepting open records. However, in practice this may not -always be the nicest choice. For example, let's say you have a `User` type alias, like so: - -```coffee -User : { - email : Str, - firstName : Str, - lastName : Str, -} -``` - -This defines `User` to be a closed record, which in practice is the most common way records named `User` -tend to be defined. - -If you want to have a function take a `User`, you might write its type like so: - -```elm -isValid : User -> Bool -``` - -If you want to have a function return a `User`, you might write its type like so: - -```elm -userFromEmail : Str -> User -``` - -A function which takes a user and returns a user might look like this: - -```elm -capitalizeNames : User -> User -``` - -This is a perfectly reasonable way to write all of these functions. However, I -might decide that I really want the `isValid` function to take an open record - -that is, a record with *at least* the fields of this `User` record, but possibly others as well. - -Since open records have a type variable (like `*` in `{ email : Str }*` or `a` in -`{ email : Str }a -> { email : Str }a`), in order to do this I'd need to add a -type variable to the `User` type alias: - -```coffee -User a : { - email : Str, - firstName : Str, - lastName : Str, -}a -``` - -Notice that the `a` type variable appears not only in `User a` but also in `}a` at the end of the -record type! - -Using `User a` type alias, I can still write the same three functions, but now their types need to look different. -This is what the first one would look like: - -```elm -isValid : User * -> Bool -``` - -Here, the `User *` type alias substitutes `*` for the type variable `a` in the type alias, -which takes it from `{ email : Str, … }a` to `{ email : Str, … }*`. Now I can pass it any -record that has at least the fields in `User`, and possibly others as well, which was my goal. - -```elm -userFromEmail : Str -> User {} -``` - -Here, the `User {}` type alias substitutes `{}` for the type variable `a` in the type alias, -which takes it from `{ email : Str, … }a` to `{ email : Str, … }{}`. As noted earlier, -this is another way to specify a closed record: putting a `{}` after it, in the same place that -you'd find a `*` in an open record. - -> **Aside:** This works because you can form new record types by replacing the type variable with -> other record types. For example, `{ a : Str, b : Str }` can also be written `{ a : Str }{ b : Str }`. -> You can chain these more than once, e.g. `{ a : Str }{ b : Str }{ c : Str, d : Str }`. -> This is more useful when used with type annotations; for example, `{ a : Str, b : Str }User` describes -> a closed record consisting of all the fields in the closed record `User`, plus `a : Str` and `b : Str`. - -This function still returns the same record as it always did, it just needs to be annotated as -`User {}` now instead of just `User`, because the `User` type alias has a variable in it that must be -specified. - -The third function might need to use a named type variable: - -```elm -capitalizeNames : User a -> User a -``` - -If this function does a record update on the given user, and returns that - for example, if its -definition were `capitalizeNames = \user -> { user & email: "blah" }` - then it needs to use the -same named type variable for both the argument and return value. - -However, if returns a new `User` that it created from scratch, then its type could instead be: - -```elm -capitalizeNames : User * -> User {} -``` - -This says that it takes a record with at least the fields specified in the `User` type alias, -and possibly others...and then returns a record with exactly the fields specified in the `User` -type alias, and no others. - -These three examples illustrate why it's relatively uncommon to use open records for type aliases: -it makes a lot of types need to incorporate a type variable that otherwise they could omit, -all so that `isValid` can be given something that has not only the fields `User` has, but -some others as well. (In the case of a `User` record in particular, it may be that the extra -fields were included due to a mistake rather than on purpose, and accepting an open record could -prevent the compiler from raising an error that would have revealed the mistake.) - -That said, this is a useful technique to know about if you want to (for example) make a record -type that accumulates more and more fields as it progresses through a series of operations. - -## Open and Closed Tag Unions - -Just like how Roc has open records and closed records, it also has open and closed tag unions. - -The *open tag union* (or *open union* for short) `[Foo Str, Bar Bool]*` represents a tag that might -be `Foo Str` and might be `Bar Bool`, but might also be some other tag whose type isn't known at compile time. - -Because an open union represents possibilities that are impossible to know ahead of time, any `when` I use on a -`[Foo Str, Bar Bool]*` value must include a catch-all `_ ->` branch. Otherwise, if one of those -unknown tags were to come up, the `when` would not know what to do with it! For example: - -```coffee -example : [Foo Str, Bar Bool]* -> Bool -example = \tag -> - when tag is - Foo str -> Str.isEmpty str - Bar bool -> bool - _ -> False -``` - -In contrast, a *closed tag union* (or *closed union*) like `[Foo Str, Bar Bool]` (without the `*`) -represents an exhaustive set of possible tags. If I use a `when` on one of these, I can match on `Foo` -only and then on `Bar` only, with no need for a catch-all branch. For example: - -```coffee -example : [Foo Str, Bar Bool] -> Bool -example = \tag -> - when tag is - Foo str -> Str.isEmpty str - Bar bool -> bool -``` - -If we were to remove the type annotations from the previous two code examples, Roc would infer the same -types for them anyway. - -It would infer `tag : [Foo Str, Bar Bool]` for the latter example because the `when tag is` expression -only includes a `Foo Str` branch and a `Bar Bool` branch, and nothing else. Since the `when` doesn't handle -any other possibilities, these two tags must be the only possible ones the `tag` argument could be. - -It would infer `tag : [Foo Str, Bar Bool]*` for the former example because the `when tag is` expression -includes a `Foo Str` branch and a `Bar Bool` branch - meaning we know about at least those two specific -possibilities - but also a `_ ->` branch, indicating that there may be other tags we don't know about. Since -the `when` is flexible enough to handle all possible tags, `tag` gets inferred as an open union. - -Putting these together, whether a tag union is inferred to be open or closed depends on which possibilities -the implementation actually handles. - -> **Aside:** As with open and closed records, we can use type annotations to make tag union types less flexible -> than what would be inferred. If we added a `_ ->` branch to the second example above, the compiler would still -> accept `example : [Foo Str, Bar Bool] -> Bool` as the type annotation, even though the catch-all branch -> would permit the more flexible `example : [Foo Str, Bar Bool]* -> Bool` annotation instead. - -## Combining Open Unions - -When we make a new record, it's inferred to be a closed record. For example, in `foo { a: "hi" }`, -the type of `{ a: "hi" }` is inferred to be `{ a : Str }`. In contrast, when we make a new tag, it's inferred -to be an open union. So in `foo (Bar "hi")`, the type of `Bar "hi"` is inferred to be `[Bar Str]*`. - -This is because open unions can accumulate additional tags based on how they're used in the program, -whereas closed unions cannot. For example, let's look at this conditional: - -```elm -if x > 5 then - "foo" -else - 7 -``` - -This will be a type mismatch because the two branches have incompatible types. Strings and numbers are not -type-compatible! Now let's look at another example: - -```elm -if x > 5 then - Ok "foo" -else - Err "bar" -``` - -This shouldn't be a type mismatch, because we can see that the two branches are compatible; they are both -tags that could easily coexist in the same tag union. But if the compiler inferred the type of `Ok "foo"` to be -the closed union `[Ok Str]`, and likewise for `Err "bar"` and `[Err Str]`, then this would have to be -a type mismatch - because those two closed unions are incompatible. - -Instead, the compiler infers `Ok "foo"` to be the open union `[Ok Str]*`, and `Err "bar"` to be the open -union `[Err Str]*`. Then, when using them together in this conditional, the inferred type of the conditional -becomes `[Ok Str, Err Str]*` - that is, the combination of the unions in each of its branches. (Branches in -a `when` work the same way with open unions.) - -Earlier we saw how a function which accepts an open union must account for more possibilities, by including -catch-all `_ ->` patterns in its `when` expressions. So *accepting* an open union means you have more requirements. -In contrast, when you already *have* a value which is an open union, you have fewer requirements. A value -which is an open union (like `Ok "foo"`, which has the type `[Ok Str]*`) can be provided to anything that's -expecting a tag union (no matter whether it's open or closed), as long as the expected tag union includes at least -the tags in the open union you're providing. - -So if I have an `[Ok Str]*` value, I can pass it to functions with any of these types (among others): - -* `[Ok Str]* -> Bool` -* `[Ok Str] -> Bool` -* `[Ok Str, Err Bool]* -> Bool` -* `[Ok Str, Err Bool] -> Bool` -* `[Ok Str, Err Bool, Whatever]* -> Bool` -* `[Ok Str, Err Bool, Whatever] -> Bool` -* `Result Str Bool -> Bool` -* `[Err Bool, Whatever]* -> Bool` - -That last one works because a function accepting an open union can accept any unrecognized tag, including -`Ok Str` - even though it is not mentioned as one of the tags in `[Err Bool, Whatever]*`! Remember, when -a function accepts an open tag union, any `when` branches on that union must include a catch-all `_ ->` branch, -which is the branch that will end up handling the `Ok Str` value we pass in. - -However, I could not pass an `[Ok Str]*` to a function with a *closed* tag union argument that did not -mention `Ok Str` as one of its tags. So if I tried to pass `[Ok Str]*` to a function with the type -`[Err Bool, Whatever] -> Str`, I would get a type mismatch - because a `when` in that function could -be handling the `Err Bool` possibility and the `Whatever` possibility, and since it would not necessarily have -a catch-all `_ ->` branch, it might not know what to do with an `Ok Str` if it received one. - -> **Note:** It wouldn't be accurate to say that a function which accepts an open union handles -> "all possible tags." For example, if I have a function `[Ok Str]* -> Bool` and I pass it -> `Ok 5`, that will still be a type mismatch. If you think about it, a `when` in that function might -> have the branch `Ok str ->` which assumes there's a string inside that `Ok`, and if `Ok 5` type-checked, -> then that assumption would be false and things would break! -> -> So `[Ok Str]*` is more restrictive than `[]*`. It's basically saying "this may or may not be an `Ok` tag, -> but if it is an `Ok` tag, then it's guaranteed to have a payload of exactly `Str`." - -In summary, here's a way to think about the difference between open unions in a value you have, compared to a value you're accepting: - -* If you *have* a closed union, that means it has all the tags it ever will, and can't accumulate more. -* If you *have* an open union, that means it can accumulate more tags through conditional branches. -* If you *accept* a closed union, that means you only have to handle the possibilities listed in the union. -* If you *accept* an open union, that means you have to handle the possibility that it has a tag you can't know about. - -## Type Variables in Tag Unions - -Earlier we saw these two examples, one with an open tag union and the other with a closed one: - -```coffee -example : [Foo Str, Bar Bool]* -> Bool -example = \tag -> - when tag is - Foo str -> Str.isEmpty str - Bar bool -> bool - _ -> False -``` - -```coffee -example : [Foo Str, Bar Bool] -> Bool -example = \tag -> - when tag is - Foo str -> Str.isEmpty str - Bar bool -> bool -``` - -Similarly to how there are open records with a `*`, closed records with nothing, -and constrained records with a named type variable, we can also have *constrained tag unions* -with a named type variable. Here's an example: - -```coffee -example : [Foo Str, Bar Bool]a -> [Foo Str, Bar Bool]a -example = \tag -> - when tag is - Foo str -> Bar (Str.isEmpty str) - Bar _ -> Bar False - other -> other -``` - -This type says that the `example` function will take either a `Foo Str` tag, or a `Bar Bool` tag, -or possibly another tag we don't know about at compile time - and it also says that the function's -return type is the same as the type of its argument. - -So if we give this function a `[Foo Str, Bar Bool, Baz (List Str)]` argument, then it will be guaranteed -to return a `[Foo Str, Bar Bool, Baz (List Str)]` value. This is more constrained than a function that -returned `[Foo Str, Bar Bool]*` because that would say it could return *any* other tag (in addition to -the `Foo Str` and `Bar Bool` we already know about). - -If we removed the type annotation from `example` above, Roc's compiler would infer the same type anyway. -This may be surprising if you look closely at the body of the function, because: - -* The return type includes `Foo Str`, but no branch explicitly returns `Foo`. Couldn't the return type be `[Bar Bool]a` instead? -* The argument type includes `Bar Bool` even though we never look at `Bar`'s payload. Couldn't the argument type be inferred to be `Bar *` instead of `Bar Bool`, since we never look at it? - -The reason it has this type is the `other -> other` branch. Take a look at that branch, and ask this question: -"What is the type of `other`?" There has to be exactly one answer! It can't be the case that `other` has one -type before the `->` and another type after it; whenever you see a named value in Roc, it is guaranteed to have -the same type everywhere it appears in that scope. - -For this reason, any time you see a function that only runs a `when` on its only argument, and that `when` -includes a branch like `x -> x` or `other -> other`, the function's argument type and return type must necessarily -be equivalent. - -> **Note:** Just like with records, you can also replace the type variable in tag union types with a concrete type. -> For example, `[Foo Str][Bar Bool][Baz (List Str)]` is equivalent to `[Foo Str, Bar Bool, Baz (List Str)]`. -> -> Also just like with records, you can use this to compose tag union type aliases. For example, you can write -> `NetworkError : [Timeout, Disconnected]` and then `Problem : [InvalidInput, UnknownFormat]NetworkError` - -## Phantom Types - -[This part of the tutorial has not been written yet. Coming soon!] - -## Operator Desugaring Table - -Here are various Roc expressions involving operators, and what they desugar to. - -| Expression | Desugars to | -| --------------- | ---------------- | -| `a + b` | `Num.add a b` | -| `a - b` | `Num.sub a b` | -| `a * b` | `Num.mul a b` | -| `a / b` | `Num.div a b` | -| `a // b` | `Num.divTrunc a b` | -| `a ^ b` | `Num.pow a b` | -| `a % b` | `Num.rem a b` | -| `a >> b` | `Num.shr a b` | -| `a << b` | `Num.shl a b` | -| `-a` | `Num.neg a` | -| `-f x y` | `Num.neg (f x y)` | -| `a == b` | `Bool.isEq a b` | -| `a != b` | `Bool.isNotEq a b` | -| `a && b` | `Bool.and a b` | -| `a \|\| b` | `Bool.or a b` | -| `!a` | `Bool.not a` | -| `!f x y` | `Bool.not (f x y)` | -| `a \|> b` | `b a` | -| `a b c \|> f x y` | `f (a b c) x y` | diff --git a/ast/Cargo.toml b/ast/Cargo.toml deleted file mode 100644 index 546b8dd81a..0000000000 --- a/ast/Cargo.toml +++ /dev/null @@ -1,35 +0,0 @@ -[package] -name = "roc_ast" -version = "0.1.0" -authors = ["The Roc Contributors"] -license = "UPL-1.0" -edition = "2021" -description = "AST as used by the editor and (soon) docs. In contrast to the compiler, these types do not keep track of a location in a file." - -[dependencies] -roc_builtins = { path = "../compiler/builtins"} -roc_can = { path = "../compiler/can" } -roc_collections = { path = "../compiler/collections" } -roc_region = { path = "../compiler/region" } -roc_module = { path = "../compiler/module" } -roc_parse = { path = "../compiler/parse" } -roc_problem = { path = "../compiler/problem" } -roc_types = { path = "../compiler/types" } -roc_unify = { path = "../compiler/unify"} -roc_load = { path = "../compiler/load" } -roc_target = { path = "../compiler/roc_target" } -roc_error_macros = { path = "../error_macros" } -roc_reporting = { path = "../reporting" } -arrayvec = "0.7.2" -bumpalo = { version = "3.8.0", features = ["collections"] } -page_size = "0.4.2" -snafu = { version = "0.6.10", features = ["backtraces"] } -ven_graph = { path = "../vendor/pathfinding" } -libc = "0.2.106" - -[dev-dependencies] -indoc = "1.0.3" - -[target.'cfg(windows)'.dependencies] -winapi = { version = "0.3.9", features = ["memoryapi"]} - diff --git a/ast/src/ast_error.rs b/ast/src/ast_error.rs deleted file mode 100644 index c733ca0ca5..0000000000 --- a/ast/src/ast_error.rs +++ /dev/null @@ -1,73 +0,0 @@ -use roc_module::{ident::Ident, module_err::ModuleError}; -use roc_parse::parser::SyntaxError; -use roc_region::all::{Loc, Region}; -use snafu::{Backtrace, Snafu}; - -use crate::lang::core::ast::ASTNodeId; - -#[derive(Debug, Snafu)] -#[snafu(visibility(pub))] -pub enum ASTError { - #[snafu(display( - "ASTNodeIdWithoutExprId: The expr_id_opt in ASTNode({:?}) was `None` but I was expecting `Some(ExprId)` .", - ast_node_id - ))] - ASTNodeIdWithoutExprId { - ast_node_id: ASTNodeId, - backtrace: Backtrace, - }, - #[snafu(display( - "UnexpectedASTNode: required a {} at this position, node was a {}.", - required_node_type, - encountered_node_type - ))] - UnexpectedASTNode { - required_node_type: String, - encountered_node_type: String, - backtrace: Backtrace, - }, - #[snafu(display( - "UnexpectedPattern2Variant: required a {} at this position, Pattern2 was a {}.", - required_pattern2, - encountered_pattern2, - ))] - UnexpectedPattern2Variant { - required_pattern2: String, - encountered_pattern2: String, - backtrace: Backtrace, - }, - #[snafu(display("IdentExistsError: {}", msg))] - IdentExistsError { msg: String }, - - WrapModuleError { - #[snafu(backtrace)] - source: ModuleError, - }, - - #[snafu(display("SyntaxError: {}", msg))] - SyntaxErrorNoBacktrace { msg: String }, -} - -pub type ASTResult = std::result::Result; - -impl From for ASTError { - fn from(module_err: ModuleError) -> Self { - Self::WrapModuleError { source: module_err } - } -} - -impl From<(Region, Loc)> for ASTError { - fn from(ident_exists_err: (Region, Loc)) -> Self { - Self::IdentExistsError { - msg: format!("{:?}", ident_exists_err), - } - } -} - -impl<'a> From> for ASTError { - fn from(syntax_err: SyntaxError) -> Self { - Self::SyntaxErrorNoBacktrace { - msg: format!("{:?}", syntax_err), - } - } -} diff --git a/ast/src/canonicalization/canonicalize.rs b/ast/src/canonicalization/canonicalize.rs deleted file mode 100644 index 434676c915..0000000000 --- a/ast/src/canonicalization/canonicalize.rs +++ /dev/null @@ -1,316 +0,0 @@ -use roc_collections::all::MutMap; -use roc_problem::can::Problem; -use roc_region::all::{Loc, Region}; -use roc_types::subs::Variable; - -use crate::{ - lang::{ - core::{ - def::def::References, - expr::{ - expr2::{Expr2, ExprId, WhenBranch}, - expr_to_expr2::expr_to_expr2, - output::Output, - record_field::RecordField, - }, - pattern::to_pattern2, - }, - env::Env, - scope::Scope, - }, - mem_pool::{pool_str::PoolStr, pool_vec::PoolVec, shallow_clone::ShallowClone}, -}; - -pub(crate) enum CanonicalizeRecordProblem { - #[allow(dead_code)] - InvalidOptionalValue { - field_name: PoolStr, - field_region: Region, - record_region: Region, - }, -} - -enum FieldVar { - VarAndExprId(Variable, ExprId), - OnlyVar(Variable), -} - -pub(crate) fn canonicalize_fields<'a>( - env: &mut Env<'a>, - scope: &mut Scope, - fields: &'a [Loc>>], -) -> Result<(PoolVec, Output), CanonicalizeRecordProblem> { - let mut can_fields: MutMap<&'a str, FieldVar> = MutMap::default(); - let mut output = Output::default(); - - for loc_field in fields.iter() { - match canonicalize_field(env, scope, &loc_field.value) { - Ok(can_field) => { - match can_field { - CanonicalField::LabelAndValue { - label, - value_expr, - value_output, - var, - } => { - let expr_id = env.pool.add(value_expr); - - let replaced = - can_fields.insert(label, FieldVar::VarAndExprId(var, expr_id)); - - if let Some(_old) = replaced { - // env.problems.push(Problem::DuplicateRecordFieldValue { - // field_name: label, - // field_region: loc_field.region, - // record_region: region, - // replaced_region: old.region, - // }); - todo!() - } - - output.references.union_mut(value_output.references); - } - CanonicalField::InvalidLabelOnly { label, var } => { - let replaced = can_fields.insert(label, FieldVar::OnlyVar(var)); - - if let Some(_old) = replaced { - todo!() - } - } - } - } - - Err(CanonicalizeFieldProblem::InvalidOptionalValue { - field_name: _, - field_region: _, - }) => { - // env.problem(Problem::InvalidOptionalValue { - // field_name: field_name.clone(), - // field_region, - // record_region: region, - // }); - // return Err(CanonicalizeRecordProblem::InvalidOptionalValue { - // field_name, - // field_region, - // record_region: region, - // }); - todo!() - } - } - } - - let pool_vec = PoolVec::with_capacity(can_fields.len() as u32, env.pool); - - for (node_id, (string, field_var)) in pool_vec.iter_node_ids().zip(can_fields.into_iter()) { - let name = PoolStr::new(string, env.pool); - - match field_var { - FieldVar::VarAndExprId(var, expr_id) => { - env.pool[node_id] = RecordField::LabeledValue(name, var, expr_id); - } - FieldVar::OnlyVar(var) => { - env.pool[node_id] = RecordField::InvalidLabelOnly(name, var); - } // TODO RecordField::LabelOnly - } - } - - Ok((pool_vec, output)) -} - -#[allow(dead_code)] -enum CanonicalizeFieldProblem { - InvalidOptionalValue { - field_name: PoolStr, - field_region: Region, - }, -} - -// TODO: the `value_output: Output` field takes _a lot_ of space! -#[allow(clippy::large_enum_variant)] -enum CanonicalField<'a> { - LabelAndValue { - label: &'a str, - value_expr: Expr2, - value_output: Output, - var: Variable, - }, - InvalidLabelOnly { - label: &'a str, - var: Variable, - }, // TODO make ValidLabelOnly -} - -fn canonicalize_field<'a>( - env: &mut Env<'a>, - scope: &mut Scope, - field: &'a roc_parse::ast::AssignedField<'a, roc_parse::ast::Expr<'a>>, -) -> Result, CanonicalizeFieldProblem> { - use roc_parse::ast::AssignedField::*; - - match field { - // Both a label and a value, e.g. `{ name: "blah" }` - RequiredValue(label, _, loc_expr) => { - let field_var = env.var_store.fresh(); - let (loc_can_expr, output) = - expr_to_expr2(env, scope, &loc_expr.value, loc_expr.region); - - match loc_can_expr { - Expr2::RuntimeError() => Ok(CanonicalField::InvalidLabelOnly { - label: label.value, - var: field_var, - }), - _ => Ok(CanonicalField::LabelAndValue { - label: label.value, - value_expr: loc_can_expr, - value_output: output, - var: field_var, - }), - } - } - - OptionalValue(label, _, loc_expr) => Err(CanonicalizeFieldProblem::InvalidOptionalValue { - field_name: PoolStr::new(label.value, env.pool), - field_region: Region::span_across(&label.region, &loc_expr.region), - }), - - // A label with no value, e.g. `{ name }` (this is sugar for { name: name }) - LabelOnly(label) => { - let field_var = env.var_store.fresh(); - // TODO return ValidLabel if label points to in scope variable - Ok(CanonicalField::InvalidLabelOnly { - label: label.value, - var: field_var, - }) - } - - SpaceBefore(sub_field, _) | SpaceAfter(sub_field, _) => { - canonicalize_field(env, scope, sub_field) - } - - Malformed(_string) => { - panic!("TODO canonicalize malformed record field"); - } - } -} - -#[inline(always)] -pub(crate) fn canonicalize_when_branch<'a>( - env: &mut Env<'a>, - scope: &mut Scope, - branch: &'a roc_parse::ast::WhenBranch<'a>, - output: &mut Output, -) -> (WhenBranch, References) { - let patterns = PoolVec::with_capacity(branch.patterns.len() as u32, env.pool); - - let original_scope = scope; - let mut scope = original_scope.shallow_clone(); - - // TODO report symbols not bound in all patterns - for (node_id, loc_pattern) in patterns.iter_node_ids().zip(branch.patterns.iter()) { - let (new_output, can_pattern) = to_pattern2( - env, - &mut scope, - roc_parse::pattern::PatternType::WhenBranch, - &loc_pattern.value, - loc_pattern.region, - ); - - output.union(new_output); - - env.set_region(node_id, loc_pattern.region); - env.pool[node_id] = can_pattern; - } - - let (value, mut branch_output) = - expr_to_expr2(env, &mut scope, &branch.value.value, branch.value.region); - let value_id = env.pool.add(value); - env.set_region(value_id, branch.value.region); - - let guard = match &branch.guard { - None => None, - Some(loc_expr) => { - let (can_guard, guard_branch_output) = - expr_to_expr2(env, &mut scope, &loc_expr.value, loc_expr.region); - - let expr_id = env.pool.add(can_guard); - env.set_region(expr_id, loc_expr.region); - - branch_output.union(guard_branch_output); - Some(expr_id) - } - }; - - // Now that we've collected all the references for this branch, check to see if - // any of the new idents it defined were unused. If any were, report it. - for (symbol, region) in scope.symbols() { - let symbol = symbol; - - if !output.references.has_lookup(symbol) - && !branch_output.references.has_lookup(symbol) - && !original_scope.contains_symbol(symbol) - { - env.problem(Problem::UnusedDef(symbol, region)); - } - } - - let references = branch_output.references.clone(); - output.union(branch_output); - - ( - WhenBranch { - patterns, - body: value_id, - guard, - }, - references, - ) -} - -pub(crate) fn canonicalize_lookup( - env: &mut Env<'_>, - scope: &mut Scope, - module_name: &str, - ident: &str, - region: Region, -) -> (Expr2, Output) { - use Expr2::*; - - let mut output = Output::default(); - let can_expr = if module_name.is_empty() { - // Since module_name was empty, this is an unqualified var. - // Look it up in scope! - match scope.lookup(&(*ident).into(), region) { - Ok(symbol) => { - output.references.lookups.insert(symbol); - - Var(symbol) - } - Err(problem) => { - env.problem(Problem::RuntimeError(problem)); - - RuntimeError() - } - } - } else { - // Since module_name was nonempty, this is a qualified var. - // Look it up in the env! - match env.qualified_lookup(module_name, ident, region) { - Ok(symbol) => { - output.references.lookups.insert(symbol); - - Var(symbol) - } - Err(problem) => { - // Either the module wasn't imported, or - // it was imported but it doesn't expose this ident. - env.problem(Problem::RuntimeError(problem)); - - RuntimeError() - } - } - }; - - // If it's valid, this ident should be in scope already. - - (can_expr, output) -} diff --git a/ast/src/canonicalization/mod.rs b/ast/src/canonicalization/mod.rs deleted file mode 100644 index fdbc754fc0..0000000000 --- a/ast/src/canonicalization/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod canonicalize; -pub mod module; diff --git a/ast/src/canonicalization/module.rs b/ast/src/canonicalization/module.rs deleted file mode 100644 index 1e7fb78242..0000000000 --- a/ast/src/canonicalization/module.rs +++ /dev/null @@ -1,326 +0,0 @@ -#![allow(clippy::all)] -#![allow(dead_code)] -#![allow(unused_imports)] -#![allow(unused_variables)] -use bumpalo::Bump; -use roc_can::operator::desugar_def; -use roc_collections::all::{default_hasher, ImMap, ImSet, MutMap, MutSet, SendMap}; -use roc_module::ident::Ident; -use roc_module::ident::Lowercase; -use roc_module::symbol::IdentIdsByModule; -use roc_module::symbol::{IdentIds, ModuleId, ModuleIds, Symbol}; -use roc_parse::ast; -use roc_parse::pattern::PatternType; -use roc_problem::can::{Problem, RuntimeError}; -use roc_region::all::{Loc, Region}; -use roc_types::subs::{VarStore, Variable}; - -use crate::lang::core::def::def::canonicalize_defs; -use crate::lang::core::def::def::Def; -use crate::lang::core::def::def::{sort_can_defs, Declaration}; -use crate::lang::core::expr::expr2::Expr2; -use crate::lang::core::expr::output::Output; -use crate::lang::core::pattern::Pattern2; -use crate::lang::core::types::Alias; -use crate::lang::core::val_def::ValueDef; -use crate::lang::env::Env; -use crate::lang::scope::Scope; -use crate::mem_pool::pool::NodeId; -use crate::mem_pool::pool::Pool; -use crate::mem_pool::pool_vec::PoolVec; -use crate::mem_pool::shallow_clone::ShallowClone; - -pub struct ModuleOutput { - pub aliases: MutMap>, - pub rigid_variables: MutMap, - pub declarations: Vec, - pub exposed_imports: MutMap, - pub lookups: Vec<(Symbol, Variable, Region)>, - pub problems: Vec, - pub ident_ids: IdentIds, - pub references: MutSet, -} - -// TODO trim these down -#[allow(clippy::too_many_arguments)] -pub fn canonicalize_module_defs<'a>( - arena: &Bump, - loc_defs: &'a [Loc>], - home: ModuleId, - module_ids: &ModuleIds, - exposed_ident_ids: IdentIds, - dep_idents: IdentIdsByModule, - aliases: MutMap, - exposed_imports: MutMap, - mut exposed_symbols: MutSet, - var_store: &mut VarStore, -) -> Result { - let mut pool = Pool::with_capacity(1 << 10); - let mut can_exposed_imports = MutMap::default(); - let mut scope = Scope::new(home, &mut pool, var_store); - let num_deps = dep_idents.len(); - - for (name, alias) in aliases.into_iter() { - let vars = PoolVec::with_capacity(alias.targs.len() as u32, &mut pool); - - for (node_id, targ_id) in vars.iter_node_ids().zip(alias.targs.iter_node_ids()) { - let (poolstr, var) = &pool[targ_id]; - pool[node_id] = (poolstr.shallow_clone(), *var); - } - scope.add_alias(&mut pool, name, vars, alias.actual); - } - - // Desugar operators (convert them to Apply calls, taking into account - // operator precedence and associativity rules), before doing other canonicalization. - // - // If we did this *during* canonicalization, then each time we - // visited a BinOp node we'd recursively try to apply this to each of its nested - // operators, and then again on *their* nested operators, ultimately applying the - // rules multiple times unnecessarily. - let mut desugared = - bumpalo::collections::Vec::with_capacity_in(loc_defs.len() + num_deps, arena); - - for loc_def in loc_defs.iter() { - desugared.push(&*arena.alloc(Loc { - value: desugar_def(arena, &loc_def.value), - region: loc_def.region, - })); - } - - let mut env = Env::new( - home, - arena, - &mut pool, - var_store, - dep_idents, - module_ids, - exposed_ident_ids, - ); - let mut lookups = Vec::with_capacity(num_deps); - let rigid_variables = MutMap::default(); - - // Exposed values are treated like defs that appear before any others, e.g. - // - // imports [Foo.{ bar, baz }] - // - // ...is basically the same as if we'd added these extra defs at the start of the module: - // - // bar = Foo.bar - // baz = Foo.baz - // - // Here we essentially add those "defs" to "the beginning of the module" - // by canonicalizing them right before we canonicalize the actual ast::Def nodes. - for (ident, (symbol, region)) in exposed_imports { - let first_char = ident.as_inline_str().chars().next().unwrap(); - - if first_char.is_lowercase() { - // this is a value definition - let expr_var = env.var_store.fresh(); - - match scope.import(ident, symbol, region) { - Ok(()) => { - // Add an entry to exposed_imports using the current module's name - // as the key; e.g. if this is the Foo module and we have - // exposes [Bar.{ baz }] then insert Foo.baz as the key, so when - // anything references `baz` in this Foo module, it will resolve to Bar.baz. - can_exposed_imports.insert(symbol, expr_var); - - // This will be used during constraint generation, - // to add the usual Lookup constraint as if this were a normal def. - lookups.push((symbol, expr_var, region)); - } - Err((_shadowed_symbol, _region)) => { - panic!("TODO gracefully handle shadowing in imports.") - } - } - } else { - // This is a type alias - - // the should already be added to the scope when this module is canonicalized - debug_assert!(scope.contains_alias(symbol)); - } - } - - let (defs, _scope, output, symbols_introduced) = canonicalize_defs( - &mut env, - Output::default(), - &scope, - &desugared, - PatternType::TopLevelDef, - ); - - // See if any of the new idents we defined went unused. - // If any were unused and also not exposed, report it. - for (symbol, region) in symbols_introduced { - if !output.references.has_lookup(symbol) && !exposed_symbols.contains(&symbol) { - env.problem(Problem::UnusedDef(symbol, region)); - } - } - - // TODO register rigids - // for (var, lowercase) in output.introduced_variables.name_by_var.clone() { - // rigid_variables.insert(var, lowercase); - // } - - let mut references = MutSet::default(); - - // Gather up all the symbols that were referenced across all the defs' lookups. - for symbol in output.references.lookups.iter() { - references.insert(*symbol); - } - - // Gather up all the symbols that were referenced across all the defs' calls. - for symbol in output.references.calls.iter() { - references.insert(*symbol); - } - - // Gather up all the symbols that were referenced from other modules. - for symbol in env.qualified_lookups.iter() { - references.insert(*symbol); - } - - // NOTE previously we inserted builtin defs into the list of defs here - // this is now done later, in file.rs. - - match sort_can_defs(&mut env, defs, Output::default()) { - (Ok(mut declarations), output) => { - use Declaration::*; - - for decl in declarations.iter() { - match decl { - Declare(def) => { - for symbol in def.symbols(env.pool) { - if exposed_symbols.contains(&symbol) { - // Remove this from exposed_symbols, - // so that at the end of the process, - // we can see if there were any - // exposed symbols which did not have - // corresponding defs. - exposed_symbols.remove(&symbol); - } - } - } - DeclareRec(defs) => { - for def in defs { - for symbol in def.symbols(env.pool) { - if exposed_symbols.contains(&symbol) { - // Remove this from exposed_symbols, - // so that at the end of the process, - // we can see if there were any - // exposed symbols which did not have - // corresponding defs. - exposed_symbols.remove(&symbol); - } - } - } - } - - InvalidCycle(identifiers, _) => { - panic!("TODO gracefully handle potentially attempting to expose invalid cyclic defs {:?}" , identifiers); - } - Builtin(def) => { - // Builtins cannot be exposed in module declarations. - // This should never happen! - debug_assert!(def - .symbols(env.pool) - .iter() - .all(|symbol| !exposed_symbols.contains(symbol))); - } - } - } - - let mut aliases = MutMap::default(); - - for (symbol, alias) in output.aliases { - // Remove this from exposed_symbols, - // so that at the end of the process, - // we can see if there were any - // exposed symbols which did not have - // corresponding defs. - exposed_symbols.remove(&symbol); - - aliases.insert(symbol, alias); - } - - // By this point, all exposed symbols should have been removed from - // exposed_symbols and added to exposed_vars_by_symbol. If any were - // not, that means they were declared as exposed but there was - // no actual declaration with that name! - for symbol in exposed_symbols { - env.problem(Problem::ExposedButNotDefined(symbol)); - - // In case this exposed value is referenced by other modules, - // create a decl for it whose implementation is a runtime error. - let mut pattern_vars = SendMap::default(); - pattern_vars.insert(symbol, env.var_store.fresh()); - - let runtime_error = RuntimeError::ExposedButNotDefined(symbol); - - let value_def = { - let pattern_id = env.pool.add(Pattern2::Identifier(symbol)); - let expr_id = env.pool.add(Expr2::RuntimeError()); - ValueDef::NoAnnotation { - pattern_id, - expr_id, - expr_var: env.var_store.fresh(), - } - }; - - let def = Def::Value(value_def); - - declarations.push(Declaration::Declare(def)); - } - - // Incorporate any remaining output.lookups entries into references. - for symbol in output.references.lookups { - references.insert(symbol); - } - - // Incorporate any remaining output.calls entries into references. - for symbol in output.references.calls { - references.insert(symbol); - } - - // Gather up all the symbols that were referenced from other modules. - for symbol in env.qualified_lookups.iter() { - references.insert(*symbol); - } - - // TODO find captured variables - // for declaration in declarations.iter_mut() { - // match declaration { - // Declare(def) => fix_values_captured_in_closure_def(def, &mut MutSet::default()), - // DeclareRec(defs) => { - // fix_values_captured_in_closure_defs(defs, &mut MutSet::default()) - // } - // InvalidCycle(_, _) | Builtin(_) => {} - // } - // } - - // TODO this loops over all symbols in the module, we can speed it up by having an - // iterator over all builtin symbols - - // TODO move over the builtins - // for symbol in references.iter() { - // if symbol.is_builtin() { - // // this can fail when the symbol is for builtin types, or has no implementation yet - // if let Some(def) = builtins::builtin_defs_map(*symbol, var_store) { - // declarations.push(Declaration::Builtin(def)); - // } - // } - // } - - Ok(ModuleOutput { - aliases, - rigid_variables, - declarations, - references, - exposed_imports: can_exposed_imports, - problems: vec![], // TODO env.problems, - lookups, - ident_ids: env.ident_ids, - }) - } - (Err(runtime_error), _) => Err(runtime_error), - } -} diff --git a/ast/src/constrain.rs b/ast/src/constrain.rs deleted file mode 100644 index da5d6e543a..0000000000 --- a/ast/src/constrain.rs +++ /dev/null @@ -1,2813 +0,0 @@ -use bumpalo::{collections::Vec as BumpVec, Bump}; - -use roc_can::expected::{Expected, PExpected}; -use roc_collections::all::{BumpMap, BumpMapDefault, HumanIndex, SendMap}; -use roc_module::{ - ident::{Lowercase, TagName}, - symbol::Symbol, -}; -use roc_region::all::Region; -use roc_types::{ - subs::Variable, - types::{self, AnnotationSource, PReason, PatternCategory}, - types::{Category, Reason}, -}; - -use crate::{ - lang::{ - core::{ - expr::{ - expr2::{ClosureExtra, Expr2, ExprId, WhenBranch}, - record_field::RecordField, - }, - fun_def::FunctionDef, - pattern::{DestructType, Pattern2, PatternId, PatternState2, RecordDestruct}, - types::{Type2, TypeId}, - val_def::ValueDef, - }, - env::Env, - }, - mem_pool::{pool::Pool, pool_vec::PoolVec, shallow_clone::ShallowClone}, -}; - -/// A presence constraint is an additive constraint that defines the lower bound -/// of a type. For example, `Present(t1, IncludesTag(A, []))` means that the -/// type `t1` must contain at least the tag `A`. The additive nature of these -/// constraints makes them behaviorally different from unification-based constraints. -#[derive(Debug)] -pub enum PresenceConstraint<'a> { - IncludesTag(TagName, BumpVec<'a, Type2>), - IsOpen, - Pattern(Region, PatternCategory, PExpected), -} - -#[derive(Debug)] -pub enum Constraint<'a> { - Eq(Type2, Expected, Category, Region), - // Store(Type, Variable, &'static str, u32), - Lookup(Symbol, Expected, Region), - Pattern(Region, PatternCategory, Type2, PExpected), - And(BumpVec<'a, Constraint<'a>>), - Let(&'a LetConstraint<'a>), - // SaveTheEnvironment, - True, // Used for things that always unify, e.g. blanks and runtime errors - Present(Type2, PresenceConstraint<'a>), -} - -#[derive(Debug)] -pub struct LetConstraint<'a> { - pub rigid_vars: BumpVec<'a, Variable>, - pub flex_vars: BumpVec<'a, Variable>, - pub def_types: BumpMap, - pub defs_constraint: Constraint<'a>, - pub ret_constraint: Constraint<'a>, -} - -pub fn constrain_expr<'a>( - arena: &'a Bump, - env: &mut Env, - expr: &Expr2, - expected: Expected, - region: Region, -) -> Constraint<'a> { - use Constraint::*; - - match expr { - Expr2::Blank | Expr2::RuntimeError() | Expr2::InvalidLookup(_) => True, - Expr2::Str(_) => Eq(str_type(env.pool), expected, Category::Str, region), - Expr2::SmallStr(_) => Eq(str_type(env.pool), expected, Category::Str, region), - Expr2::Var(symbol) => Lookup(*symbol, expected, region), - Expr2::EmptyRecord => constrain_empty_record(expected, region), - Expr2::SmallInt { var, .. } | Expr2::I128 { var, .. } | Expr2::U128 { var, .. } => { - let mut flex_vars = BumpVec::with_capacity_in(1, arena); - - flex_vars.push(*var); - - let precision_var = env.var_store.fresh(); - - let range_type = Type2::Variable(precision_var); - - let range_type_id = env.pool.add(range_type); - - exists( - arena, - flex_vars, - Eq( - num_num(env.pool, range_type_id), - expected, - Category::Num, - region, - ), - ) - } - Expr2::Float { var, .. } => { - let mut flex_vars = BumpVec::with_capacity_in(1, arena); - - let mut and_constraints = BumpVec::with_capacity_in(2, arena); - - let num_type = Type2::Variable(*var); - - flex_vars.push(*var); - - let precision_var = env.var_store.fresh(); - - let range_type = Type2::Variable(precision_var); - - let range_type_id = env.pool.add(range_type); - - and_constraints.push(Eq( - num_type.shallow_clone(), - Expected::ForReason( - Reason::FloatLiteral, - num_float(env.pool, range_type_id), - region, - ), - Category::Int, - region, - )); - - and_constraints.push(Eq(num_type, expected, Category::Float, region)); - - let defs_constraint = And(and_constraints); - - exists(arena, flex_vars, defs_constraint) - } - Expr2::List { - elem_var, elems, .. - } => { - let mut flex_vars = BumpVec::with_capacity_in(1, arena); - - flex_vars.push(*elem_var); - - if elems.is_empty() { - exists( - arena, - flex_vars, - Eq( - empty_list_type(env.pool, *elem_var), - expected, - Category::List, - region, - ), - ) - } else { - let mut constraints = BumpVec::with_capacity_in(1 + elems.len(), arena); - - let list_elem_type = Type2::Variable(*elem_var); - - let indexed_node_ids: Vec<(usize, ExprId)> = - elems.iter(env.pool).copied().enumerate().collect(); - - for (index, elem_node_id) in indexed_node_ids { - let elem_expr = env.pool.get(elem_node_id); - - let elem_expected = Expected::ForReason( - Reason::ElemInList { - index: HumanIndex::zero_based(index), - }, - list_elem_type.shallow_clone(), - region, - ); - - let constraint = constrain_expr(arena, env, elem_expr, elem_expected, region); - - constraints.push(constraint); - } - - constraints.push(Eq( - list_type(env.pool, list_elem_type), - expected, - Category::List, - region, - )); - - exists(arena, flex_vars, And(constraints)) - } - } - Expr2::Record { fields, record_var } => { - if fields.is_empty() { - constrain_empty_record(expected, region) - } else { - let field_types = PoolVec::with_capacity(fields.len() as u32, env.pool); - - let mut field_vars = BumpVec::with_capacity_in(fields.len(), arena); - - // Constraints need capacity for each field - // + 1 for the record itself + 1 for record var - let mut constraints = BumpVec::with_capacity_in(2 + fields.len(), arena); - - for (record_field_node_id, field_type_node_id) in - fields.iter_node_ids().zip(field_types.iter_node_ids()) - { - let record_field = env.pool.get(record_field_node_id); - - match record_field { - RecordField::LabeledValue(pool_str, var, node_id) => { - let expr = env.pool.get(*node_id); - - let (field_type, field_con) = constrain_field(arena, env, *var, expr); - - field_vars.push(*var); - - let field_type_id = env.pool.add(field_type); - - env.pool[field_type_node_id] = - (*pool_str, types::RecordField::Required(field_type_id)); - - constraints.push(field_con); - } - e => todo!("{:?}", e), - } - } - - let record_type = Type2::Record(field_types, env.pool.add(Type2::EmptyRec)); - - let record_con = Eq( - record_type, - expected.shallow_clone(), - Category::Record, - region, - ); - - constraints.push(record_con); - - // variable to store in the AST - let stored_con = Eq( - Type2::Variable(*record_var), - expected, - Category::Storage(std::file!(), std::line!()), - region, - ); - - field_vars.push(*record_var); - constraints.push(stored_con); - - exists(arena, field_vars, And(constraints)) - } - } - Expr2::Tag { - variant_var, - ext_var, - name, - arguments, - } => { - let tag_name = TagName(name.as_str(env.pool).into()); - - constrain_tag( - arena, - env, - expected, - region, - tag_name, - arguments, - *ext_var, - *variant_var, - ) - } - Expr2::Call { - args, - expr_var, - expr_id: expr_node_id, - closure_var, - fn_var, - called_via, - } => { - // The expression that evaluates to the function being called, e.g. `foo` in - // (foo) bar baz - let call_expr = env.pool.get(*expr_node_id); - - let opt_symbol = if let Expr2::Var(symbol) = call_expr { - Some(*symbol) - } else { - None - }; - - let fn_type = Type2::Variable(*fn_var); - let fn_region = region; - let fn_expected = Expected::NoExpectation(fn_type.shallow_clone()); - - let fn_reason = Reason::FnCall { - name: opt_symbol, - arity: args.len() as u8, - }; - - let fn_con = constrain_expr(arena, env, call_expr, fn_expected, region); - - // The function's return type - // TODO: don't use expr_var? - let ret_type = Type2::Variable(*expr_var); - - // type of values captured in the closure - let closure_type = Type2::Variable(*closure_var); - - // This will be used in the occurs check - let mut vars = BumpVec::with_capacity_in(2 + args.len(), arena); - - vars.push(*fn_var); - // TODO: don't use expr_var? - vars.push(*expr_var); - vars.push(*closure_var); - - let mut arg_types = BumpVec::with_capacity_in(args.len(), arena); - let mut arg_cons = BumpVec::with_capacity_in(args.len(), arena); - - for (index, arg_node_id) in args.iter_node_ids().enumerate() { - let (arg_var, arg) = env.pool.get(arg_node_id); - let arg_expr = env.pool.get(*arg); - - let region = region; - let arg_type = Type2::Variable(*arg_var); - - let reason = Reason::FnArg { - name: opt_symbol, - arg_index: HumanIndex::zero_based(index), - }; - - let expected_arg = Expected::ForReason(reason, arg_type.shallow_clone(), region); - - let arg_con = constrain_expr(arena, env, arg_expr, expected_arg, region); - - vars.push(*arg_var); - arg_types.push(arg_type); - arg_cons.push(arg_con); - } - - let expected_fn_type = Expected::ForReason( - fn_reason, - Type2::Function( - PoolVec::new(arg_types.into_iter(), env.pool), - env.pool.add(closure_type), - env.pool.add(ret_type.shallow_clone()), - ), - region, - ); - - let category = Category::CallResult(opt_symbol, *called_via); - - let mut and_constraints = BumpVec::with_capacity_in(4, arena); - - and_constraints.push(fn_con); - and_constraints.push(Eq(fn_type, expected_fn_type, category.clone(), fn_region)); - and_constraints.push(And(arg_cons)); - and_constraints.push(Eq(ret_type, expected, category, region)); - - exists(arena, vars, And(and_constraints)) - } - Expr2::Accessor { - function_var, - closure_var, - field, - record_var, - ext_var, - field_var, - } => { - let ext_var = *ext_var; - let ext_type = Type2::Variable(ext_var); - - let field_var = *field_var; - let field_type = Type2::Variable(field_var); - - let record_field = - types::RecordField::Demanded(env.pool.add(field_type.shallow_clone())); - - let record_type = Type2::Record( - PoolVec::new(vec![(*field, record_field)].into_iter(), env.pool), - env.pool.add(ext_type), - ); - - let category = Category::Accessor(field.as_str(env.pool).into()); - - let record_expected = Expected::NoExpectation(record_type.shallow_clone()); - let record_con = Eq( - Type2::Variable(*record_var), - record_expected, - category.clone(), - region, - ); - - let function_type = Type2::Function( - PoolVec::new(vec![record_type].into_iter(), env.pool), - env.pool.add(Type2::Variable(*closure_var)), - env.pool.add(field_type), - ); - - let mut flex_vars = BumpVec::with_capacity_in(5, arena); - - flex_vars.push(*record_var); - flex_vars.push(*function_var); - flex_vars.push(*closure_var); - flex_vars.push(field_var); - flex_vars.push(ext_var); - - let mut and_constraints = BumpVec::with_capacity_in(3, arena); - - and_constraints.push(Eq( - function_type.shallow_clone(), - expected, - category.clone(), - region, - )); - - and_constraints.push(Eq( - function_type, - Expected::NoExpectation(Type2::Variable(*function_var)), - category, - region, - )); - - and_constraints.push(record_con); - - exists(arena, flex_vars, And(and_constraints)) - } - Expr2::Access { - expr: expr_id, - field, - field_var, - record_var, - ext_var, - } => { - let ext_type = Type2::Variable(*ext_var); - - let field_type = Type2::Variable(*field_var); - - let record_field = - types::RecordField::Demanded(env.pool.add(field_type.shallow_clone())); - - let record_type = Type2::Record( - PoolVec::new(vec![(*field, record_field)].into_iter(), env.pool), - env.pool.add(ext_type), - ); - - let record_expected = Expected::NoExpectation(record_type); - - let category = Category::Access(field.as_str(env.pool).into()); - - let record_con = Eq( - Type2::Variable(*record_var), - record_expected.shallow_clone(), - category.clone(), - region, - ); - - let access_expr = env.pool.get(*expr_id); - - let constraint = constrain_expr(arena, env, access_expr, record_expected, region); - - let mut flex_vars = BumpVec::with_capacity_in(3, arena); - - flex_vars.push(*record_var); - flex_vars.push(*field_var); - flex_vars.push(*ext_var); - - let mut and_constraints = BumpVec::with_capacity_in(3, arena); - - and_constraints.push(constraint); - and_constraints.push(Eq(field_type, expected, category, region)); - and_constraints.push(record_con); - - exists(arena, flex_vars, And(and_constraints)) - } - Expr2::If { - cond_var, - expr_var, - branches, - final_else, - } => { - let expect_bool = |region| { - let bool_type = Type2::Variable(Variable::BOOL); - Expected::ForReason(Reason::IfCondition, bool_type, region) - }; - - let mut branch_cons = BumpVec::with_capacity_in(2 * branches.len() + 3, arena); - - // TODO why does this cond var exist? is it for error messages? - // let first_cond_region = branches[0].0.region; - let cond_var_is_bool_con = Eq( - Type2::Variable(*cond_var), - expect_bool(region), - Category::If, - region, - ); - - branch_cons.push(cond_var_is_bool_con); - - let final_else_expr = env.pool.get(*final_else); - - let mut flex_vars = BumpVec::with_capacity_in(2, arena); - - flex_vars.push(*cond_var); - flex_vars.push(*expr_var); - - match expected { - Expected::FromAnnotation(name, arity, ann_source, tipe) => { - let num_branches = branches.len() + 1; - - for (index, branch_id) in branches.iter_node_ids().enumerate() { - let (cond_id, body_id) = env.pool.get(branch_id); - - let cond = env.pool.get(*cond_id); - let body = env.pool.get(*body_id); - - let cond_con = - constrain_expr(arena, env, cond, expect_bool(region), region); - - let then_con = constrain_expr( - arena, - env, - body, - Expected::FromAnnotation( - name.clone(), - arity, - AnnotationSource::TypedIfBranch { - index: HumanIndex::zero_based(index), - num_branches, - region: ann_source.region(), - }, - tipe.shallow_clone(), - ), - region, - ); - - branch_cons.push(cond_con); - branch_cons.push(then_con); - } - - let else_con = constrain_expr( - arena, - env, - final_else_expr, - Expected::FromAnnotation( - name, - arity, - AnnotationSource::TypedIfBranch { - index: HumanIndex::zero_based(branches.len()), - num_branches, - region: ann_source.region(), - }, - tipe.shallow_clone(), - ), - region, - ); - - let ast_con = Eq( - Type2::Variable(*expr_var), - Expected::NoExpectation(tipe), - Category::Storage(std::file!(), std::line!()), - region, - ); - - branch_cons.push(ast_con); - branch_cons.push(else_con); - - exists(arena, flex_vars, And(branch_cons)) - } - _ => { - for (index, branch_id) in branches.iter_node_ids().enumerate() { - let (cond_id, body_id) = env.pool.get(branch_id); - - let cond = env.pool.get(*cond_id); - let body = env.pool.get(*body_id); - - let cond_con = - constrain_expr(arena, env, cond, expect_bool(region), region); - - let then_con = constrain_expr( - arena, - env, - body, - Expected::ForReason( - Reason::IfBranch { - index: HumanIndex::zero_based(index), - total_branches: branches.len(), - }, - Type2::Variable(*expr_var), - // should be from body - region, - ), - region, - ); - - branch_cons.push(cond_con); - branch_cons.push(then_con); - } - - let else_con = constrain_expr( - arena, - env, - final_else_expr, - Expected::ForReason( - Reason::IfBranch { - index: HumanIndex::zero_based(branches.len()), - total_branches: branches.len() + 1, - }, - Type2::Variable(*expr_var), - // should come from final_else - region, - ), - region, - ); - - branch_cons.push(Eq( - Type2::Variable(*expr_var), - expected, - Category::Storage(std::file!(), std::line!()), - region, - )); - - branch_cons.push(else_con); - - exists(arena, flex_vars, And(branch_cons)) - } - } - } - Expr2::When { - cond_var, - expr_var, - cond: cond_id, - branches, - } => { - // Infer the condition expression's type. - let cond_type = Type2::Variable(*cond_var); - - let cond = env.pool.get(*cond_id); - - let expr_con = constrain_expr( - arena, - env, - cond, - Expected::NoExpectation(cond_type.shallow_clone()), - region, - ); - - let mut constraints = BumpVec::with_capacity_in(branches.len() + 1, arena); - - constraints.push(expr_con); - - let mut flex_vars = BumpVec::with_capacity_in(2, arena); - - flex_vars.push(*cond_var); - flex_vars.push(*expr_var); - - match &expected { - Expected::FromAnnotation(name, arity, ann_source, _typ) => { - // NOTE deviation from elm. - // - // in elm, `_typ` is used, but because we have this `expr_var` too - // and need to constrain it, this is what works and gives better error messages - let typ = Type2::Variable(*expr_var); - - for (index, when_branch_id) in branches.iter_node_ids().enumerate() { - let when_branch = env.pool.get(when_branch_id); - - // let pattern_region = Region::across_all( - // when_branch.patterns.iter(env.pool).map(|v| &v.region), - // ); - - let pattern_expected = |sub_pattern, sub_region| { - PExpected::ForReason( - PReason::WhenMatch { - index: HumanIndex::zero_based(index), - sub_pattern, - }, - cond_type.shallow_clone(), - sub_region, - ) - }; - - let branch_con = constrain_when_branch( - arena, - env, - // TODO: when_branch.value.region, - region, - when_branch, - pattern_expected, - Expected::FromAnnotation( - name.clone(), - *arity, - AnnotationSource::TypedWhenBranch { - index: HumanIndex::zero_based(index), - region: ann_source.region(), - }, - typ.shallow_clone(), - ), - ); - - constraints.push(branch_con); - } - - constraints.push(Eq(typ, expected, Category::When, region)); - - return exists(arena, flex_vars, And(constraints)); - } - - _ => { - let branch_type = Type2::Variable(*expr_var); - let mut branch_cons = BumpVec::with_capacity_in(branches.len(), arena); - - for (index, when_branch_id) in branches.iter_node_ids().enumerate() { - let when_branch = env.pool.get(when_branch_id); - - // let pattern_region = - // Region::across_all(when_branch.patterns.iter().map(|v| &v.region)); - - let pattern_expected = |sub_pattern, sub_region| { - PExpected::ForReason( - PReason::WhenMatch { - index: HumanIndex::zero_based(index), - sub_pattern, - }, - cond_type.shallow_clone(), - sub_region, - ) - }; - - let branch_con = constrain_when_branch( - arena, - env, - region, - when_branch, - pattern_expected, - Expected::ForReason( - Reason::WhenBranch { - index: HumanIndex::zero_based(index), - }, - branch_type.shallow_clone(), - // TODO: when_branch.value.region, - region, - ), - ); - - branch_cons.push(branch_con); - } - - let mut and_constraints = BumpVec::with_capacity_in(2, arena); - - and_constraints.push(And(branch_cons)); - and_constraints.push(Eq(branch_type, expected, Category::When, region)); - - constraints.push(And(and_constraints)); - } - } - - // exhautiveness checking happens when converting to mono::Expr - exists(arena, flex_vars, And(constraints)) - } - Expr2::LetValue { - def_id, - body_id, - body_var, - } => { - let value_def = env.pool.get(*def_id); - let body = env.pool.get(*body_id); - - let body_con = constrain_expr(arena, env, body, expected.shallow_clone(), region); - - match value_def { - ValueDef::WithAnnotation { .. } => todo!("implement {:?}", value_def), - ValueDef::NoAnnotation { - pattern_id, - expr_id, - expr_var, - } => { - let pattern = env.pool.get(*pattern_id); - - let mut flex_vars = BumpVec::with_capacity_in(1, arena); - flex_vars.push(*body_var); - - let expr_type = Type2::Variable(*expr_var); - - let pattern_expected = PExpected::NoExpectation(expr_type.shallow_clone()); - let mut state = PatternState2 { - headers: BumpMap::new_in(arena), - vars: BumpVec::with_capacity_in(1, arena), - constraints: BumpVec::with_capacity_in(1, arena), - }; - - constrain_pattern( - arena, - env, - pattern, - region, - pattern_expected, - &mut state, - false, - ); - state.vars.push(*expr_var); - - let def_expr = env.pool.get(*expr_id); - - let constrained_def = Let(arena.alloc(LetConstraint { - rigid_vars: BumpVec::new_in(arena), - flex_vars: state.vars, - def_types: state.headers, - defs_constraint: Let(arena.alloc(LetConstraint { - rigid_vars: BumpVec::new_in(arena), // always empty - flex_vars: BumpVec::new_in(arena), // empty, because our functions have no arguments - def_types: BumpMap::new_in(arena), // empty, because our functions have no arguments! - defs_constraint: And(state.constraints), - ret_constraint: constrain_expr( - arena, - env, - def_expr, - Expected::NoExpectation(expr_type), - region, - ), - })), - ret_constraint: body_con, - })); - - let mut and_constraints = BumpVec::with_capacity_in(2, arena); - - and_constraints.push(constrained_def); - and_constraints.push(Eq( - Type2::Variable(*body_var), - expected, - Category::Storage(std::file!(), std::line!()), - // TODO: needs to be ret region - region, - )); - - exists(arena, flex_vars, And(and_constraints)) - } - } - } - // In an expression like - // id = \x -> x - // - // id 1 - // The `def_id` refers to the definition `id = \x -> x`, - // and the body refers to `id 1`. - Expr2::LetFunction { - def_id, - body_id, - body_var: _, - } => { - let body = env.pool.get(*body_id); - let body_con = constrain_expr(arena, env, body, expected.shallow_clone(), region); - - let function_def = env.pool.get(*def_id); - - let (name, arguments, body_id, rigid_vars, args_constrs) = match function_def { - FunctionDef::WithAnnotation { - name, - arguments, - body_id, - rigids, - return_type: _, - } => { - // The annotation gives us arguments with proper Type2s, but the constraints we - // generate below args bound to type variables. Create fresh ones and bind them - // to the types we already know. - let mut args_constrs = BumpVec::with_capacity_in(arguments.len(), arena); - let args_vars = PoolVec::with_capacity(arguments.len() as u32, env.pool); - for (arg_ty_node_id, arg_var_node_id) in - arguments.iter_node_ids().zip(args_vars.iter_node_ids()) - { - let (ty, pattern) = env.pool.get(arg_ty_node_id); - let arg_var = env.var_store.fresh(); - let ty = env.pool.get(*ty); - args_constrs.push(Eq( - Type2::Variable(arg_var), - Expected::NoExpectation(ty.shallow_clone()), - Category::Storage(std::file!(), std::line!()), - // TODO: should be the actual region of the argument - region, - )); - env.pool[arg_var_node_id] = (arg_var, *pattern); - } - - let rigids = env.pool.get(*rigids); - let rigid_vars: BumpVec = - BumpVec::from_iter_in(rigids.names.iter(env.pool).map(|&(_, v)| v), arena); - - (name, args_vars, body_id, rigid_vars, args_constrs) - } - FunctionDef::NoAnnotation { - name, - arguments, - body_id, - return_var: _, - } => { - ( - name, - arguments.shallow_clone(), - body_id, - BumpVec::new_in(arena), // The function is unannotated, so there are no rigid type vars - BumpVec::new_in(arena), // No extra constraints to generate for arguments - ) - } - }; - - // A function definition is equivalent to a named value definition, where the - // value is a closure. So, we create a closure definition in correspondence - // with the function definition, generate type constraints for it, and demand - // that type of the function is just the type of the resolved closure. - let fn_var = env.var_store.fresh(); - let fn_ty = Type2::Variable(fn_var); - - let extra = ClosureExtra { - return_type: env.var_store.fresh(), - captured_symbols: PoolVec::empty(env.pool), - closure_type: env.var_store.fresh(), - closure_ext_var: env.var_store.fresh(), - }; - let clos = Expr2::Closure { - args: arguments.shallow_clone(), - uniq_symbol: *name, - body_id: *body_id, - function_type: env.var_store.fresh(), - extra: env.pool.add(extra), - recursive: roc_can::expr::Recursive::Recursive, - }; - let clos_con = constrain_expr( - arena, - env, - &clos, - Expected::NoExpectation(fn_ty.shallow_clone()), - region, - ); - - // This is the `foo` part in `foo = \...`. We want to bind the name of the - // function with its type, whose constraints we generated above. - let mut def_pattern_state = PatternState2 { - headers: BumpMap::new_in(arena), - vars: BumpVec::new_in(arena), - constraints: args_constrs, - }; - def_pattern_state.headers.insert(*name, fn_ty); - def_pattern_state.vars.push(fn_var); - - Let(arena.alloc(LetConstraint { - rigid_vars, - flex_vars: def_pattern_state.vars, - def_types: def_pattern_state.headers, // Binding function name -> its type - defs_constraint: Let(arena.alloc(LetConstraint { - rigid_vars: BumpVec::new_in(arena), // always empty - flex_vars: BumpVec::new_in(arena), // empty, because our functions have no arguments - def_types: BumpMap::new_in(arena), // empty, because our functions have no arguments - defs_constraint: And(def_pattern_state.constraints), - ret_constraint: clos_con, - })), - ret_constraint: body_con, - })) - } - Expr2::Update { - symbol, - updates, - ext_var, - record_var, - } => { - let field_types = PoolVec::with_capacity(updates.len() as u32, env.pool); - let mut flex_vars = BumpVec::with_capacity_in(updates.len() + 2, arena); - let mut cons = BumpVec::with_capacity_in(updates.len() + 1, arena); - let mut record_key_updates = SendMap::default(); - - for (record_field_id, field_type_node_id) in - updates.iter_node_ids().zip(field_types.iter_node_ids()) - { - let record_field = env.pool.get(record_field_id); - - match record_field { - RecordField::LabeledValue(pool_str, var, node_id) => { - let expr = env.pool.get(*node_id); - - let (field_type, field_con) = constrain_field_update( - arena, - env, - *var, - pool_str.as_str(env.pool).into(), - expr, - ); - - let field_type_id = env.pool.add(field_type); - - env.pool[field_type_node_id] = - (*pool_str, types::RecordField::Required(field_type_id)); - - record_key_updates.insert(pool_str.as_str(env.pool).into(), Region::zero()); - - flex_vars.push(*var); - cons.push(field_con); - } - e => todo!("{:?}", e), - } - } - - let fields_type = Type2::Record(field_types, env.pool.add(Type2::Variable(*ext_var))); - let record_type = Type2::Variable(*record_var); - - // NOTE from elm compiler: fields_type is separate so that Error propagates better - let fields_con = Eq( - record_type.shallow_clone(), - Expected::NoExpectation(fields_type), - Category::Record, - region, - ); - let record_con = Eq( - record_type.shallow_clone(), - expected, - Category::Record, - region, - ); - - flex_vars.push(*record_var); - flex_vars.push(*ext_var); - - let con = Lookup( - *symbol, - Expected::ForReason( - Reason::RecordUpdateKeys(*symbol, record_key_updates), - record_type, - region, - ), - region, - ); - - // ensure constraints are solved in this order, gives better errors - cons.insert(0, fields_con); - cons.insert(1, con); - cons.insert(2, record_con); - - exists(arena, flex_vars, And(cons)) - } - - Expr2::RunLowLevel { op, args, ret_var } => { - // This is a modified version of what we do for function calls. - - // The operation's return type - let ret_type = Type2::Variable(*ret_var); - - // This will be used in the occurs check - let mut vars = BumpVec::with_capacity_in(1 + args.len(), arena); - - vars.push(*ret_var); - - let mut arg_types = BumpVec::with_capacity_in(args.len(), arena); - let mut arg_cons = BumpVec::with_capacity_in(args.len(), arena); - - for (index, node_id) in args.iter_node_ids().enumerate() { - let (arg_var, arg_id) = env.pool.get(node_id); - - vars.push(*arg_var); - - let arg_type = Type2::Variable(*arg_var); - - let reason = Reason::LowLevelOpArg { - op: *op, - arg_index: HumanIndex::zero_based(index), - }; - let expected_arg = - Expected::ForReason(reason, arg_type.shallow_clone(), Region::zero()); - let arg = env.pool.get(*arg_id); - - let arg_con = constrain_expr(arena, env, arg, expected_arg, Region::zero()); - - arg_types.push(arg_type); - arg_cons.push(arg_con); - } - - let category = Category::LowLevelOpResult(*op); - - let mut and_constraints = BumpVec::with_capacity_in(2, arena); - - and_constraints.push(And(arg_cons)); - and_constraints.push(Eq(ret_type, expected, category, region)); - - exists(arena, vars, And(and_constraints)) - } - Expr2::Closure { - args, - uniq_symbol, - body_id, - function_type: fn_var, - extra, - .. - } => { - // NOTE defs are treated somewhere else! - let body = env.pool.get(*body_id); - - let ClosureExtra { - captured_symbols, - return_type: ret_var, - closure_type: closure_var, - closure_ext_var, - } = env.pool.get(*extra); - - let closure_type = Type2::Variable(*closure_var); - let return_type = Type2::Variable(*ret_var); - - let (mut vars, pattern_state, function_type) = - constrain_untyped_args(arena, env, args, closure_type, return_type.shallow_clone()); - - vars.push(*ret_var); - vars.push(*closure_var); - vars.push(*closure_ext_var); - vars.push(*fn_var); - - let expected_body_type = Expected::NoExpectation(return_type); - // Region here should come from body expr - let ret_constraint = constrain_expr(arena, env, body, expected_body_type, region); - - let captured_symbols_as_vec = captured_symbols - .iter(env.pool) - .copied() - .collect::>(); - - // make sure the captured symbols are sorted! - debug_assert_eq!(captured_symbols_as_vec, { - let mut copy: Vec<(Symbol, Variable)> = captured_symbols_as_vec.clone(); - - copy.sort(); - - copy - }); - - let closure_constraint = constrain_closure_size( - arena, - env, - *uniq_symbol, - region, - captured_symbols, - *closure_var, - *closure_ext_var, - &mut vars, - ); - - let mut and_constraints = BumpVec::with_capacity_in(4, arena); - - and_constraints.push(Let(arena.alloc(LetConstraint { - rigid_vars: BumpVec::new_in(arena), - flex_vars: pattern_state.vars, - def_types: pattern_state.headers, - defs_constraint: And(pattern_state.constraints), - ret_constraint, - }))); - - // "the closure's type is equal to expected type" - and_constraints.push(Eq( - function_type.shallow_clone(), - expected, - Category::Lambda, - region, - )); - - // "fn_var is equal to the closure's type" - fn_var is used in code gen - and_constraints.push(Eq( - Type2::Variable(*fn_var), - Expected::NoExpectation(function_type), - Category::Storage(std::file!(), std::line!()), - region, - )); - - and_constraints.push(closure_constraint); - - exists(arena, vars, And(and_constraints)) - } - Expr2::LetRec { .. } => todo!(), - } -} - -fn exists<'a>( - arena: &'a Bump, - flex_vars: BumpVec<'a, Variable>, - defs_constraint: Constraint<'a>, -) -> Constraint<'a> { - Constraint::Let(arena.alloc(LetConstraint { - rigid_vars: BumpVec::new_in(arena), - flex_vars, - def_types: BumpMap::new_in(arena), - defs_constraint, - ret_constraint: Constraint::True, - })) -} - -#[allow(clippy::too_many_arguments)] -fn constrain_tag<'a>( - arena: &'a Bump, - env: &mut Env, - expected: Expected, - region: Region, - tag_name: TagName, - arguments: &PoolVec<(Variable, ExprId)>, - ext_var: Variable, - variant_var: Variable, -) -> Constraint<'a> { - use Constraint::*; - - let mut flex_vars = BumpVec::with_capacity_in(arguments.len(), arena); - let types = PoolVec::with_capacity(arguments.len() as u32, env.pool); - let mut arg_cons = BumpVec::with_capacity_in(arguments.len(), arena); - - for (argument_node_id, type_node_id) in arguments.iter_node_ids().zip(types.iter_node_ids()) { - let (var, expr_node_id) = env.pool.get(argument_node_id); - - let argument_expr = env.pool.get(*expr_node_id); - - let arg_con = constrain_expr( - arena, - env, - argument_expr, - Expected::NoExpectation(Type2::Variable(*var)), - region, - ); - - arg_cons.push(arg_con); - flex_vars.push(*var); - - env.pool[type_node_id] = Type2::Variable(*var); - } - - let union_con = Eq( - Type2::TagUnion( - PoolVec::new(std::iter::once((tag_name.clone(), types)), env.pool), - env.pool.add(Type2::Variable(ext_var)), - ), - expected.shallow_clone(), - Category::TagApply { - tag_name, - args_count: arguments.len(), - }, - region, - ); - - let ast_con = Eq( - Type2::Variable(variant_var), - expected, - Category::Storage(std::file!(), std::line!()), - region, - ); - - flex_vars.push(variant_var); - flex_vars.push(ext_var); - - arg_cons.push(union_con); - arg_cons.push(ast_con); - - exists(arena, flex_vars, And(arg_cons)) -} - -fn constrain_field<'a>( - arena: &'a Bump, - env: &mut Env, - field_var: Variable, - expr: &Expr2, -) -> (Type2, Constraint<'a>) { - let field_type = Type2::Variable(field_var); - let field_expected = Expected::NoExpectation(field_type.shallow_clone()); - let constraint = constrain_expr(arena, env, expr, field_expected, Region::zero()); - - (field_type, constraint) -} - -#[inline(always)] -fn constrain_field_update<'a>( - arena: &'a Bump, - env: &mut Env, - field_var: Variable, - field: Lowercase, - expr: &Expr2, -) -> (Type2, Constraint<'a>) { - let field_type = Type2::Variable(field_var); - let reason = Reason::RecordUpdateValue(field); - let field_expected = Expected::ForReason(reason, field_type.shallow_clone(), Region::zero()); - let con = constrain_expr(arena, env, expr, field_expected, Region::zero()); - - (field_type, con) -} - -fn constrain_empty_record<'a>(expected: Expected, region: Region) -> Constraint<'a> { - Constraint::Eq(Type2::EmptyRec, expected, Category::Record, region) -} - -#[inline(always)] -fn constrain_when_branch<'a>( - arena: &'a Bump, - env: &mut Env, - region: Region, - when_branch: &WhenBranch, - pattern_expected: impl Fn(HumanIndex, Region) -> PExpected, - expr_expected: Expected, -) -> Constraint<'a> { - let when_expr = env.pool.get(when_branch.body); - - let ret_constraint = constrain_expr(arena, env, when_expr, expr_expected, region); - - let mut state = PatternState2 { - headers: BumpMap::new_in(arena), - vars: BumpVec::with_capacity_in(1, arena), - constraints: BumpVec::with_capacity_in(1, arena), - }; - - // TODO investigate for error messages, is it better to unify all branches with a variable, - // then unify that variable with the expectation? - for (sub_pattern, pattern_id) in when_branch.patterns.iter_node_ids().enumerate() { - let pattern = env.pool.get(pattern_id); - - let pattern_expected = pattern_expected( - HumanIndex::zero_based(sub_pattern), - // TODO: use the proper subpattern region. Not available to us right now. - region, - ); - - constrain_pattern( - arena, - env, - pattern, - // loc_pattern.region, - region, - pattern_expected, - &mut state, - true, - ); - } - - if let Some(guard_id) = &when_branch.guard { - let guard = env.pool.get(*guard_id); - - let guard_constraint = constrain_expr( - arena, - env, - guard, - Expected::ForReason( - Reason::WhenGuard, - Type2::Variable(Variable::BOOL), - // TODO: loc_guard.region, - region, - ), - region, - ); - - // must introduce the headers from the pattern before constraining the guard - Constraint::Let(arena.alloc(LetConstraint { - rigid_vars: BumpVec::new_in(arena), - flex_vars: state.vars, - def_types: state.headers, - defs_constraint: Constraint::And(state.constraints), - ret_constraint: Constraint::Let(arena.alloc(LetConstraint { - rigid_vars: BumpVec::new_in(arena), - flex_vars: BumpVec::new_in(arena), - def_types: BumpMap::new_in(arena), - defs_constraint: guard_constraint, - ret_constraint, - })), - })) - } else { - Constraint::Let(arena.alloc(LetConstraint { - rigid_vars: BumpVec::new_in(arena), - flex_vars: state.vars, - def_types: state.headers, - defs_constraint: Constraint::And(state.constraints), - ret_constraint, - })) - } -} - -fn make_pattern_constraint( - region: Region, - category: PatternCategory, - actual: Type2, - expected: PExpected, - presence_con: bool, -) -> Constraint<'static> { - if presence_con { - Constraint::Present( - actual, - PresenceConstraint::Pattern(region, category, expected), - ) - } else { - Constraint::Pattern(region, category, actual, expected) - } -} - -/// This accepts PatternState (rather than returning it) so that the caller can -/// initialize the Vecs in PatternState using with_capacity -/// based on its knowledge of their lengths. -pub fn constrain_pattern<'a>( - arena: &'a Bump, - env: &mut Env, - pattern: &Pattern2, - region: Region, - expected: PExpected, - state: &mut PatternState2<'a>, - destruct_position: bool, -) { - use Pattern2::*; - - match pattern { - Underscore if destruct_position => { - // This is an underscore in a position where we destruct a variable, - // like a when expression: - // when x is - // A -> "" - // _ -> "" - // so, we know that "x" (in this case, a tag union) must be open. - state.constraints.push(Constraint::Present( - expected.get_type(), - PresenceConstraint::IsOpen, - )); - } - - Underscore | UnsupportedPattern(_) | MalformedPattern(_, _) | Shadowed { .. } => { - // Neither the _ pattern nor erroneous ones add any constraints. - } - - Identifier(symbol) => { - if destruct_position { - state.constraints.push(Constraint::Present( - expected.get_type_ref().shallow_clone(), - PresenceConstraint::IsOpen, - )); - } - state.headers.insert(*symbol, expected.get_type()); - } - - NumLiteral(var, _) => { - state.vars.push(*var); - - let type_id = env.pool.add(Type2::Variable(*var)); - - state.constraints.push(Constraint::Pattern( - region, - PatternCategory::Num, - num_num(env.pool, type_id), - expected, - )); - } - - IntLiteral(_int_val) => { - let precision_var = env.var_store.fresh(); - - let range = env.add(Type2::Variable(precision_var), region); - - state.constraints.push(Constraint::Pattern( - region, - PatternCategory::Int, - num_int(env.pool, range), - expected, - )); - } - - FloatLiteral(_float_val) => { - let precision_var = env.var_store.fresh(); - - let range = env.add(Type2::Variable(precision_var), region); - - state.constraints.push(Constraint::Pattern( - region, - PatternCategory::Float, - num_float(env.pool, range), - expected, - )); - } - - StrLiteral(_) => { - state.constraints.push(Constraint::Pattern( - region, - PatternCategory::Str, - str_type(env.pool), - expected, - )); - } - - CharacterLiteral(_) => { - state.constraints.push(Constraint::Pattern( - region, - PatternCategory::Character, - num_unsigned32(env.pool), - expected, - )); - } - - RecordDestructure { - whole_var, - ext_var, - destructs, - } => { - state.vars.push(*whole_var); - state.vars.push(*ext_var); - let ext_type = Type2::Variable(*ext_var); - - let mut field_types = Vec::new(); - - for destruct_id in destructs.iter_node_ids() { - let RecordDestruct { - var, - label, - symbol, - typ, - } = env.pool.get(destruct_id); - - let pat_type = Type2::Variable(*var); - let expected = PExpected::NoExpectation(pat_type.shallow_clone()); - - if !state.headers.contains_key(symbol) { - state.headers.insert(*symbol, pat_type.shallow_clone()); - } - - let destruct_type = env.pool.get(*typ); - - let field_type = match destruct_type { - DestructType::Guard(guard_var, guard_id) => { - state.constraints.push(make_pattern_constraint( - region, - PatternCategory::PatternGuard, - Type2::Variable(*guard_var), - PExpected::ForReason( - PReason::PatternGuard, - pat_type.shallow_clone(), - // TODO: region should be from guard_id - region, - ), - destruct_position, - )); - - state.vars.push(*guard_var); - - let guard = env.pool.get(*guard_id); - - // TODO: region should be from guard_id - constrain_pattern( - arena, - env, - guard, - region, - expected, - state, - destruct_position, - ); - - types::RecordField::Demanded(env.pool.add(pat_type)) - } - DestructType::Optional(expr_var, expr_id) => { - state.constraints.push(make_pattern_constraint( - region, - PatternCategory::PatternDefault, - Type2::Variable(*expr_var), - PExpected::ForReason( - PReason::OptionalField, - pat_type.shallow_clone(), - // TODO: region should be from expr_id - region, - ), - destruct_position, - )); - - state.vars.push(*expr_var); - - let expr_expected = Expected::ForReason( - Reason::RecordDefaultField(label.as_str(env.pool).into()), - pat_type.shallow_clone(), - // TODO: region should be from expr_id - region, - ); - - let expr = env.pool.get(*expr_id); - - // TODO: region should be from expr_id - let expr_con = constrain_expr(arena, env, expr, expr_expected, region); - - state.constraints.push(expr_con); - - types::RecordField::Optional(env.pool.add(pat_type)) - } - DestructType::Required => { - // No extra constraints necessary. - types::RecordField::Demanded(env.pool.add(pat_type)) - } - }; - - field_types.push((*label, field_type)); - - state.vars.push(*var); - } - - let record_type = Type2::Record( - PoolVec::new(field_types.into_iter(), env.pool), - env.pool.add(ext_type), - ); - - let whole_con = Constraint::Eq( - Type2::Variable(*whole_var), - Expected::NoExpectation(record_type), - Category::Storage(std::file!(), std::line!()), - region, - ); - - let record_con = make_pattern_constraint( - region, - PatternCategory::Record, - Type2::Variable(*whole_var), - expected, - destruct_position, - ); - - state.constraints.push(whole_con); - state.constraints.push(record_con); - } - Tag { - whole_var, - ext_var, - tag_name: name, - arguments, - } => { - let tag_name = TagName(name.as_str(env.pool).into()); - - constrain_tag_pattern( - arena, - env, - region, - expected, - state, - *whole_var, - *ext_var, - arguments, - tag_name, - destruct_position, - ); - } - } -} - -#[allow(clippy::too_many_arguments)] -fn constrain_tag_pattern<'a>( - arena: &'a Bump, - env: &mut Env, - region: Region, - expected: PExpected, - state: &mut PatternState2<'a>, - whole_var: Variable, - ext_var: Variable, - arguments: &PoolVec<(Variable, PatternId)>, - tag_name: TagName, - destruct_position: bool, -) { - let mut argument_types = Vec::with_capacity(arguments.len()); - - for (index, arg_id) in arguments.iter_node_ids().enumerate() { - let (pattern_var, pattern_id) = env.pool.get(arg_id); - let pattern = env.pool.get(*pattern_id); - - state.vars.push(*pattern_var); - - let pattern_type = Type2::Variable(*pattern_var); - argument_types.push(pattern_type.shallow_clone()); - - let expected = PExpected::ForReason( - PReason::TagArg { - tag_name: tag_name.clone(), - index: HumanIndex::zero_based(index), - }, - pattern_type, - region, - ); - - // TODO region should come from pattern - constrain_pattern(arena, env, pattern, region, expected, state, false); - } - - let whole_con = if destruct_position { - Constraint::Present( - expected.get_type_ref().shallow_clone(), - PresenceConstraint::IncludesTag( - tag_name.clone(), - BumpVec::from_iter_in(argument_types.into_iter(), arena), - ), - ) - } else { - Constraint::Eq( - Type2::Variable(whole_var), - Expected::NoExpectation(Type2::TagUnion( - PoolVec::new( - vec![( - tag_name.clone(), - PoolVec::new(argument_types.into_iter(), env.pool), - )] - .into_iter(), - env.pool, - ), - env.pool.add(Type2::Variable(ext_var)), - )), - Category::Storage(std::file!(), std::line!()), - region, - ) - }; - - let tag_con = make_pattern_constraint( - region, - PatternCategory::Ctor(tag_name), - Type2::Variable(whole_var), - expected, - destruct_position, - ); - - state.vars.push(whole_var); - state.vars.push(ext_var); - state.constraints.push(whole_con); - state.constraints.push(tag_con); -} - -fn constrain_untyped_args<'a>( - arena: &'a Bump, - env: &mut Env, - arguments: &PoolVec<(Variable, PatternId)>, - closure_type: Type2, - return_type: Type2, -) -> (BumpVec<'a, Variable>, PatternState2<'a>, Type2) { - let mut vars = BumpVec::with_capacity_in(arguments.len(), arena); - - let pattern_types = PoolVec::with_capacity(arguments.len() as u32, env.pool); - - let mut pattern_state = PatternState2 { - headers: BumpMap::new_in(arena), - vars: BumpVec::with_capacity_in(1, arena), - constraints: BumpVec::with_capacity_in(1, arena), - }; - - for (arg_node_id, pattern_type_id) in - arguments.iter_node_ids().zip(pattern_types.iter_node_ids()) - { - let (pattern_var, pattern_id) = env.pool.get(arg_node_id); - let pattern = env.pool.get(*pattern_id); - - let pattern_type = Type2::Variable(*pattern_var); - let pattern_expected = PExpected::NoExpectation(pattern_type.shallow_clone()); - - env.pool[pattern_type_id] = pattern_type; - - constrain_pattern( - arena, - env, - pattern, - // TODO needs to come from pattern - Region::zero(), - pattern_expected, - &mut pattern_state, - false, - ); - - vars.push(*pattern_var); - } - - let function_type = Type2::Function( - pattern_types, - env.pool.add(closure_type), - env.pool.add(return_type), - ); - - (vars, pattern_state, function_type) -} - -#[allow(clippy::too_many_arguments)] -fn constrain_closure_size<'a>( - arena: &'a Bump, - env: &mut Env, - _name: Symbol, - region: Region, - captured_symbols: &PoolVec<(Symbol, Variable)>, - closure_var: Variable, - closure_ext_var: Variable, - variables: &mut BumpVec<'a, Variable>, -) -> Constraint<'a> { - use Constraint::*; - - debug_assert!(variables.iter().any(|s| *s == closure_var)); - debug_assert!(variables.iter().any(|s| *s == closure_ext_var)); - - let tag_arguments = PoolVec::with_capacity(captured_symbols.len() as u32, env.pool); - let mut captured_symbols_constraints = BumpVec::with_capacity_in(captured_symbols.len(), arena); - - for (captured_symbol_id, tag_arg_id) in captured_symbols - .iter_node_ids() - .zip(tag_arguments.iter_node_ids()) - { - let (symbol, var) = env.pool.get(captured_symbol_id); - - // make sure the variable is registered - variables.push(*var); - - let tag_arg_type = Type2::Variable(*var); - - // this symbol is captured, so it must be part of the closure type - env.pool[tag_arg_id] = tag_arg_type.shallow_clone(); - - // make the variable equal to the looked-up type of symbol - captured_symbols_constraints.push(Lookup( - *symbol, - Expected::NoExpectation(tag_arg_type), - Region::zero(), - )); - } - - // This is incorrect, but the editor will be using the Can AST soon, so disregarding for now. - let tag_name = TagName("FAKE CLOSURE".into()); - let closure_type = Type2::TagUnion( - PoolVec::new(vec![(tag_name, tag_arguments)].into_iter(), env.pool), - env.pool.add(Type2::Variable(closure_ext_var)), - ); - - let finalizer = Eq( - Type2::Variable(closure_var), - Expected::NoExpectation(closure_type), - Category::ClosureSize, - region, - ); - - captured_symbols_constraints.push(finalizer); - - And(captured_symbols_constraints) -} - -#[inline(always)] -fn builtin_type(symbol: Symbol, args: PoolVec) -> Type2 { - Type2::Apply(symbol, args) -} - -#[inline(always)] -fn str_type(pool: &mut Pool) -> Type2 { - builtin_type(Symbol::STR_STR, PoolVec::empty(pool)) -} - -#[inline(always)] -fn empty_list_type(pool: &mut Pool, var: Variable) -> Type2 { - list_type(pool, Type2::Variable(var)) -} - -#[inline(always)] -fn list_type(pool: &mut Pool, typ: Type2) -> Type2 { - builtin_type(Symbol::LIST_LIST, PoolVec::new(vec![typ].into_iter(), pool)) -} - -#[inline(always)] -fn num_float(pool: &mut Pool, range: TypeId) -> Type2 { - let num_floatingpoint_type = num_floatingpoint(pool, range); - let num_floatingpoint_id = pool.add(num_floatingpoint_type); - - let num_num_type = num_num(pool, num_floatingpoint_id); - let num_num_id = pool.add(num_num_type); - - Type2::Alias( - Symbol::NUM_FRAC, - PoolVec::new(vec![range].into_iter(), pool), - num_num_id, - ) -} - -#[inline(always)] -fn num_floatingpoint(pool: &mut Pool, range: TypeId) -> Type2 { - let range_type = pool.get(range); - - let alias_content = range_type.shallow_clone(); - - Type2::Opaque( - Symbol::NUM_FLOATINGPOINT, - PoolVec::new(vec![range].into_iter(), pool), - pool.add(alias_content), - ) -} - -#[inline(always)] -fn num_int(pool: &mut Pool, range: TypeId) -> Type2 { - let num_integer_type = _num_integer(pool, range); - let num_integer_id = pool.add(num_integer_type); - - let num_num_type = num_num(pool, num_integer_id); - let num_num_id = pool.add(num_num_type); - - Type2::Alias( - Symbol::NUM_INT, - PoolVec::new(vec![range].into_iter(), pool), - num_num_id, - ) -} - -#[inline(always)] -fn _num_signed64(pool: &mut Pool) -> Type2 { - Type2::Alias( - Symbol::NUM_SIGNED64, - PoolVec::empty(pool), - pool.add(Type2::EmptyTagUnion), - ) -} - -#[inline(always)] -fn num_unsigned32(pool: &mut Pool) -> Type2 { - let alias_content = Type2::EmptyTagUnion; - - Type2::Alias( - Symbol::NUM_UNSIGNED32, - PoolVec::empty(pool), - pool.add(alias_content), - ) -} - -#[inline(always)] -fn _num_integer(pool: &mut Pool, range: TypeId) -> Type2 { - let range_type = pool.get(range); - - let alias_content = range_type.shallow_clone(); - - Type2::Opaque( - Symbol::NUM_INTEGER, - PoolVec::new(vec![range].into_iter(), pool), - pool.add(alias_content), - ) -} - -#[inline(always)] -fn num_num(pool: &mut Pool, type_id: TypeId) -> Type2 { - let range_type = pool.get(type_id); - - let alias_content = range_type.shallow_clone(); - - Type2::Opaque( - Symbol::NUM_NUM, - PoolVec::new(vec![type_id].into_iter(), pool), - pool.add(alias_content), - ) -} - -#[cfg(test)] -pub mod test_constrain { - use bumpalo::Bump; - use roc_can::expected::Expected; - use roc_collections::all::MutMap; - use roc_module::{ - ident::Lowercase, - symbol::{IdentIds, Interns, ModuleIds, Symbol}, - }; - use roc_parse::parser::{SourceError, SyntaxError}; - use roc_region::all::Region; - use roc_types::{ - pretty_print::{name_and_print_var, DebugPrint}, - solved_types::Solved, - subs::{Subs, VarStore, Variable}, - }; - - use super::Constraint; - use crate::{ - constrain::constrain_expr, - lang::{ - core::{ - expr::{expr2::Expr2, expr_to_expr2::loc_expr_to_expr2, output::Output}, - types::Type2, - }, - env::Env, - scope::Scope, - }, - mem_pool::pool::Pool, - solve_type, - }; - use indoc::indoc; - - fn run_solve<'a>( - arena: &'a Bump, - mempool: &mut Pool, - aliases: MutMap, - rigid_variables: MutMap, - constraint: Constraint, - var_store: VarStore, - ) -> (Solved, solve_type::Env, Vec) { - let env = solve_type::Env { - vars_by_symbol: MutMap::default(), - aliases, - }; - - let mut subs = Subs::new_from_varstore(var_store); - - for (var, name) in rigid_variables { - subs.rigid_var(var, name); - } - - // Now that the module is parsed, canonicalized, and constrained, - // we need to type check it. - let mut problems = Vec::new(); - - // Run the solver to populate Subs. - let (solved_subs, solved_env) = - solve_type::run(arena, mempool, &env, &mut problems, subs, &constraint); - - (solved_subs, solved_env, problems) - } - - fn infer_eq(actual: &str, expected_str: &str) { - let mut env_pool = Pool::with_capacity(1024); - let env_arena = Bump::new(); - let code_arena = Bump::new(); - - let mut var_store = VarStore::default(); - let var = var_store.fresh(); - let dep_idents = IdentIds::exposed_builtins(8); - let exposed_ident_ids = IdentIds::default(); - let mut module_ids = ModuleIds::default(); - let mod_id = module_ids.get_or_insert(&"ModId123".into()); - - let mut env = Env::new( - mod_id, - &env_arena, - &mut env_pool, - &mut var_store, - dep_idents, - &module_ids, - exposed_ident_ids, - ); - - let mut scope = Scope::new(env.home, env.pool, env.var_store); - - let region = Region::zero(); - - let expr2_result = str_to_expr2(&code_arena, actual, &mut env, &mut scope, region); - - match expr2_result { - Ok((expr, output)) => { - let constraint = constrain_expr( - &code_arena, - &mut env, - &expr, - Expected::NoExpectation(Type2::Variable(var)), - Region::zero(), - ); - - let Env { - pool, - var_store: ref_var_store, - mut dep_idents, - .. - } = env; - - // extract the var_store out of the env again - let mut var_store = VarStore::default(); - std::mem::swap(ref_var_store, &mut var_store); - - let rigids = output.introduced_variables.name_by_var; - - let (mut solved, _, _) = run_solve( - &code_arena, - pool, - Default::default(), - rigids, - constraint, - var_store, - ); - - let subs = solved.inner_mut(); - - // Connect the ModuleId to it's IdentIds - dep_idents.insert(mod_id, env.ident_ids); - - let interns = Interns { - module_ids: env.module_ids.clone(), - all_ident_ids: dep_idents, - }; - - let actual_str = - name_and_print_var(var, subs, mod_id, &interns, DebugPrint::NOTHING); - - assert_eq!(actual_str, expected_str); - } - Err(e) => panic!("syntax error {:?}", e), - } - } - - pub fn str_to_expr2<'a>( - arena: &'a Bump, - input: &'a str, - env: &mut Env<'a>, - scope: &mut Scope, - region: Region, - ) -> Result<(Expr2, Output), SourceError<'a, SyntaxError<'a>>> { - match roc_parse::test_helpers::parse_loc_with(arena, input.trim()) { - Ok(loc_expr) => Ok(loc_expr_to_expr2(arena, loc_expr, env, scope, region)), - Err(fail) => Err(fail), - } - } - - #[test] - fn constrain_str() { - infer_eq( - indoc!( - r#" - "type inference!" - "# - ), - "Str", - ) - } - - // This will be more useful once we actually map - // strings less than 15 chars to SmallStr - #[test] - fn constrain_small_str() { - infer_eq( - indoc!( - r#" - "a" - "# - ), - "Str", - ) - } - - #[test] - fn constrain_empty_record() { - infer_eq( - indoc!( - r#" - {} - "# - ), - "{}", - ) - } - - #[test] - fn constrain_small_int() { - infer_eq( - indoc!( - r#" - 12 - "# - ), - "Num *", - ) - } - - #[test] - fn constrain_float() { - infer_eq( - indoc!( - r#" - 3.14 - "# - ), - "Float *", - ) - } - - #[test] - fn constrain_record() { - infer_eq( - indoc!( - r#" - { x : 1, y : "hi" } - "# - ), - "{ x : Num *, y : Str }", - ) - } - - #[test] - fn constrain_empty_list() { - infer_eq( - indoc!( - r#" - [] - "# - ), - "List *", - ) - } - - #[test] - fn constrain_list() { - infer_eq( - indoc!( - r#" - [1, 2] - "# - ), - "List (Num *)", - ) - } - - #[test] - fn constrain_list_of_records() { - infer_eq( - indoc!( - r#" - [{ x: 1 }, { x: 3 }] - "# - ), - "List { x : Num * }", - ) - } - - #[test] - fn constrain_tag() { - infer_eq( - indoc!( - r#" - Foo - "# - ), - "[Foo]*", - ) - } - - #[test] - fn constrain_call_and_accessor() { - infer_eq( - indoc!( - r#" - .foo { foo: "bar" } - "# - ), - "Str", - ) - } - - #[test] - fn constrain_access() { - infer_eq( - indoc!( - r#" - { foo: "bar" }.foo - "# - ), - "Str", - ) - } - - #[test] - fn constrain_if() { - infer_eq( - indoc!( - r#" - if True then Green else Red - "# - ), - "[Green, Red]*", - ) - } - - #[test] - fn constrain_when() { - infer_eq( - indoc!( - r#" - when if True then Green else Red is - Green -> Blue - Red -> Purple - "# - ), - "[Blue, Purple]*", - ) - } - - #[test] - fn constrain_let_value() { - infer_eq( - indoc!( - r#" - person = { name: "roc" } - - person - "# - ), - "{ name : Str }", - ) - } - - #[test] - fn constrain_update() { - infer_eq( - indoc!( - r#" - person = { name: "roc" } - - { person & name: "bird" } - "# - ), - "{ name : Str }", - ) - } - - #[ignore = "TODO: implement builtins in the editor"] - #[test] - fn constrain_run_low_level() { - infer_eq( - indoc!( - r#" - List.map [{ name: "roc" }, { name: "bird" }] .name - "# - ), - "List Str", - ) - } - - #[test] - fn dual_arity_lambda() { - infer_eq( - indoc!( - r#" - \a, b -> Pair a b - "# - ), - "a, b -> [Pair a b]*", - ); - } - - #[test] - fn anonymous_identity() { - infer_eq( - indoc!( - r#" - (\a -> a) 3.14 - "# - ), - "Float *", - ); - } - - #[test] - fn identity_of_identity() { - infer_eq( - indoc!( - r#" - (\val -> val) (\val -> val) - "# - ), - "a -> a", - ); - } - - #[test] - fn identity_function() { - infer_eq( - indoc!( - r#" - \val -> val - "# - ), - "a -> a", - ); - } - - #[test] - fn apply_function() { - infer_eq( - indoc!( - r#" - \f, x -> f x - "# - ), - "(a -> b), a -> b", - ); - } - - #[test] - fn flip_function() { - infer_eq( - indoc!( - r#" - \f -> (\a, b -> f b a) - "# - ), - "(a, b -> c) -> (b, a -> c)", - ); - } - - #[test] - fn always_function() { - infer_eq( - indoc!( - r#" - \val -> \_ -> val - "# - ), - "a -> (* -> a)", - ); - } - - #[test] - fn pass_a_function() { - infer_eq( - indoc!( - r#" - \f -> f {} - "# - ), - "({} -> a) -> a", - ); - } - - #[test] - fn constrain_closure() { - infer_eq( - indoc!( - r#" - x = 1 - - \{} -> x - "# - ), - "{}* -> Num *", - ) - } - - #[test] - fn recursive_identity() { - infer_eq( - indoc!( - r#" - identity = \val -> val - - identity - "# - ), - "a -> a", - ); - } - - #[test] - fn use_apply() { - infer_eq( - indoc!( - r#" - identity = \a -> a - apply = \f, x -> f x - - apply identity 5 - "# - ), - "Num *", - ); - } - - #[test] - fn nested_let_function() { - infer_eq( - indoc!( - r#" - curryPair = \a -> - getB = \b -> Pair a b - getB - - curryPair - "# - ), - "a -> (b -> [Pair a b]*)", - ); - } - - #[test] - fn record_with_bound_var() { - infer_eq( - indoc!( - r#" - fn = \rec -> - x = rec.x - - rec - - fn - "# - ), - "{ x : a }b -> { x : a }b", - ); - } - - #[test] - fn using_type_signature() { - infer_eq( - indoc!( - r#" - bar : custom -> custom - bar = \x -> x - - bar - "# - ), - "custom -> custom", - ); - } - - #[ignore = "Currently panics at 'Invalid Cycle', ast/src/lang/core/def/def.rs:1212:21"] - #[test] - fn using_type_signature2() { - infer_eq( - indoc!( - r#" - id1 : tya -> tya - id1 = \x -> x - - id2 : tyb -> tyb - id2 = id1 - - id2 - "# - ), - "tyb -> tyb", - ); - } - - #[ignore = "Implement annotation-only decls"] - #[test] - fn type_signature_without_body() { - infer_eq( - indoc!( - r#" - foo: Str -> {} - - foo "hi" - "# - ), - "{}", - ); - } - - #[ignore = "Implement annotation-only decls"] - #[test] - fn type_signature_without_body_rigid() { - infer_eq( - indoc!( - r#" - foo : Num * -> custom - - foo 2 - "# - ), - "custom", - ); - } - - #[test] - fn inference_var_inside_arrow() { - infer_eq( - indoc!( - r#" - id : _ -> _ - id = \x -> x - id - "# - ), - "a -> a", - ) - } - - #[test] - #[ignore = "TODO: Type2::substitute"] - fn inference_var_inside_ctor() { - infer_eq( - indoc!( - r#" - canIGo : _ -> Result _ _ - canIGo = \color -> - when color is - "green" -> Ok "go!" - "yellow" -> Err (SlowIt "whoa, let's slow down!") - "red" -> Err (StopIt "absolutely not") - _ -> Err (UnknownColor "this is a weird stoplight") - canIGo - "# - ), - "Str -> Result Str [SlowIt Str, StopIt Str, UnknownColor Str]*", - ) - } - - #[test] - #[ignore = "TODO: Gives { x : *, y : * } -> { x : *, y : * }. This is a bug in typechecking defs with annotations."] - fn inference_var_inside_ctor_linked() { - infer_eq( - indoc!( - r#" - swapRcd: {x: _, y: _} -> {x: _, y: _} - swapRcd = \{x, y} -> {x: y, y: x} - swapRcd - "# - ), - "{ x : a, y : b } -> { x : b, y : a }", - ) - } - - #[test] - fn inference_var_link_with_rigid() { - infer_eq( - indoc!( - r#" - swapRcd: {x: tx, y: ty} -> {x: _, y: _} - swapRcd = \{x, y} -> {x: y, y: x} - swapRcd - "# - ), - "{ x : tx, y : ty } -> { x : ty, y : tx }", - ) - } - - #[test] - #[ignore = "TODO: Type2::substitute"] - fn inference_var_inside_tag_ctor() { - infer_eq( - indoc!( - r#" - badComics: Bool -> [CowTools _, Thagomizer _] - badComics = \c -> - when c is - True -> CowTools "The Far Side" - False -> Thagomizer "The Far Side" - badComics - "# - ), - "Bool -> [CowTools Str, Thagomizer Str]", - ) - } - - #[test] - fn inference_var_tag_union_ext() { - // TODO: we should really be inferring [Blue, Orange]a -> [Lavender, Peach]a here. - // See https://github.com/rtfeldman/roc/issues/2053 - infer_eq( - indoc!( - r#" - pastelize: _ -> [Lavender, Peach]_ - pastelize = \color -> - when color is - Blue -> Lavender - Orange -> Peach - col -> col - pastelize - "# - ), - "[Blue, Lavender, Orange, Peach]a -> [Blue, Lavender, Orange, Peach]a", - ) - } - - #[test] - #[ignore = "TODO: gives { email : a, name : b }c -> { email : a, name : b }c. This is a bug in typechecking defs with annotations."] - fn inference_var_rcd_union_ext() { - infer_eq( - indoc!( - r#" - setRocEmail : _ -> { name: Str, email: Str }_ - setRocEmail = \person -> - { person & email: "\(person.name)@roclang.com" } - setRocEmail - "# - ), - "{ email : Str, name : Str }a -> { email : Str, name : Str }a", - ) - } - - #[test] - fn infer_union_input_position1() { - infer_eq( - indoc!( - r#" - \tag -> - when tag is - A -> X - B -> Y - "# - ), - "[A, B] -> [X, Y]*", - ) - } - - #[test] - fn infer_union_input_position2() { - infer_eq( - indoc!( - r#" - \tag -> - when tag is - A -> X - B -> Y - _ -> Z - "# - ), - "[A, B]* -> [X, Y, Z]*", - ) - } - - #[test] - fn infer_union_input_position3() { - infer_eq( - indoc!( - r#" - \tag -> - when tag is - A M -> X - A N -> Y - "# - ), - "[A [M, N]] -> [X, Y]*", - ) - } - - #[test] - fn infer_union_input_position4() { - infer_eq( - indoc!( - r#" - \tag -> - when tag is - A M -> X - A N -> Y - A _ -> Z - "# - ), - "[A [M, N]*] -> [X, Y, Z]*", - ) - } - - #[test] - #[ignore = "TODO: currently [A [M [J]*, N [K]*]] -> [X]*"] - fn infer_union_input_position5() { - infer_eq( - indoc!( - r#" - \tag -> - when tag is - A (M J) -> X - A (N K) -> X - "# - ), - "[A [M [J], N [K]]] -> [X]*", - ) - } - - #[test] - fn infer_union_input_position6() { - infer_eq( - indoc!( - r#" - \tag -> - when tag is - A M -> X - B -> X - A N -> X - "# - ), - "[A [M, N], B] -> [X]*", - ) - } - - #[test] - #[ignore = "TODO: currently [A]* -> [A, X]*"] - fn infer_union_input_position7() { - infer_eq( - indoc!( - r#" - \tag -> - when tag is - A -> X - t -> t - "# - ), - // TODO: we could be a bit smarter by subtracting "A" as a possible - // tag in the union known by t, which would yield the principal type - // [A,]a -> [X]a - "[A, X]a -> [A, X]a", - ) - } - - #[test] - fn infer_union_input_position8() { - infer_eq( - indoc!( - r#" - \opt -> - when opt is - Some ({tag: A}) -> 1 - Some ({tag: B}) -> 1 - None -> 0 - "# - ), - "[None, Some { tag : [A, B] }*] -> Num *", - ) - } - - #[test] - #[ignore = "TODO: panicked at 'Invalid Cycle', ast/src/lang/core/def/def.rs:1208:21"] - fn infer_union_input_position9() { - infer_eq( - indoc!( - r#" - opt : [Some Str, None] - opt = Some "" - rcd = { opt } - - when rcd is - { opt: Some s } -> s - { opt: None } -> "?" - "# - ), - "Str", - ) - } - - #[test] - #[ignore = "TODO: currently -> Num a"] - fn infer_union_input_position10() { - infer_eq( - indoc!( - r#" - \r -> - when r is - { x: Blue, y ? 3 } -> y - { x: Red, y ? 5 } -> y - "# - ), - "{ x : [Blue, Red], y ? Num a }* -> Num a", - ) - } -} diff --git a/ast/src/lang/core/ast.rs b/ast/src/lang/core/ast.rs deleted file mode 100644 index 2f49644641..0000000000 --- a/ast/src/lang/core/ast.rs +++ /dev/null @@ -1,63 +0,0 @@ -use crate::{ - ast_error::{ASTNodeIdWithoutExprId, ASTResult}, - mem_pool::pool::Pool, -}; - -use super::{ - def::def2::{def2_to_string, DefId}, - expr::{expr2::ExprId, expr2_to_string::expr2_to_string}, - header::AppHeader, -}; - -#[derive(Debug)] -pub struct AST { - pub header: AppHeader, - pub def_ids: Vec, -} - -impl AST { - pub fn insert_def_at_index(&mut self, new_def_id: DefId, index: usize) { - self.def_ids.insert(index, new_def_id); - } - - // TODO print in tree shape, similar to linux tree command - pub fn ast_to_string(&self, pool: &Pool) -> String { - let mut full_ast_string = String::new(); - - for def_id in self.def_ids.iter() { - full_ast_string.push_str(&def2_to_string(*def_id, pool)); - full_ast_string.push_str("\n\n"); - } - - full_ast_string - } -} - -#[derive(Debug, PartialEq, Copy, Clone)] -pub enum ASTNodeId { - ADefId(DefId), - AExprId(ExprId), -} - -impl ASTNodeId { - pub fn to_expr_id(&self) -> ASTResult { - match self { - ASTNodeId::AExprId(expr_id) => Ok(*expr_id), - _ => ASTNodeIdWithoutExprId { ast_node_id: *self }.fail()?, - } - } - - pub fn to_def_id(&self) -> ASTResult { - match self { - ASTNodeId::ADefId(def_id) => Ok(*def_id), - _ => ASTNodeIdWithoutExprId { ast_node_id: *self }.fail()?, - } - } -} - -pub fn ast_node_to_string(node_id: ASTNodeId, pool: &Pool) -> String { - match node_id { - ASTNodeId::ADefId(def_id) => def2_to_string(def_id, pool), - ASTNodeId::AExprId(expr_id) => expr2_to_string(expr_id, pool), - } -} diff --git a/ast/src/lang/core/declaration.rs b/ast/src/lang/core/declaration.rs deleted file mode 100644 index e45668b484..0000000000 --- a/ast/src/lang/core/declaration.rs +++ /dev/null @@ -1,70 +0,0 @@ -use roc_types::subs::VarStore; - -use crate::{ - lang::core::{def::def::Def, expr::expr2::Expr2}, - mem_pool::{pool::Pool, pool_vec::PoolVec}, -}; - -use super::def::def::Declaration; - -pub(crate) fn decl_to_let( - pool: &mut Pool, - var_store: &mut VarStore, - decl: Declaration, - ret: Expr2, -) -> Expr2 { - match decl { - Declaration::Declare(def) => match def { - Def::AnnotationOnly { .. } => todo!(), - Def::Value(value_def) => { - let def_id = pool.add(value_def); - - let body_id = pool.add(ret); - - Expr2::LetValue { - def_id, - body_id, - body_var: var_store.fresh(), - } - } - Def::Function(function_def) => { - let def_id = pool.add(function_def); - let body_id = pool.add(ret); - - Expr2::LetFunction { - def_id, - body_id, - body_var: var_store.fresh(), - } - } - }, - Declaration::DeclareRec(defs) => { - let mut function_defs = vec![]; - - for def in defs { - match def { - Def::AnnotationOnly { .. } => todo!(), - Def::Function(function_def) => function_defs.push(function_def), - Def::Value(_) => unreachable!(), - } - } - - let body_id = pool.add(ret); - - Expr2::LetRec { - defs: PoolVec::new(function_defs.into_iter(), pool), - body_var: var_store.fresh(), - body_id, - } - } - Declaration::InvalidCycle(_entries, _) => { - // TODO: replace with something from Expr2 - // Expr::RuntimeError(RuntimeError::CircularDef(entries)) - todo!() - } - Declaration::Builtin(_) => { - // Builtins should only be added to top-level decls, not to let-exprs! - unreachable!() - } - } -} diff --git a/ast/src/lang/core/def/def.rs b/ast/src/lang/core/def/def.rs deleted file mode 100644 index 5ade55dcd0..0000000000 --- a/ast/src/lang/core/def/def.rs +++ /dev/null @@ -1,1465 +0,0 @@ -#![allow(clippy::all)] -#![allow(dead_code)] -#![allow(unused_imports)] -#![allow(unused_variables)] -// use crate::annotation::canonicalize_annotation; -// use crate::annotation::IntroducedVariables; -// use crate::env::Env; -// use crate::expr::Expr::{self, *}; -// use crate::expr::{ -// canonicalize_expr, local_successors, references_from_call, references_from_local, Output, -// Recursive, -// }; -// use crate::pattern::{bindings_from_patterns, canonicalize_pattern, Pattern}; -// use crate::procedure::References; -use roc_collections::all::{default_hasher, ImMap, MutMap, MutSet, SendMap}; -use roc_error_macros::{todo_abilities, todo_opaques}; -use roc_module::ident::Lowercase; -use roc_module::symbol::Symbol; -use roc_parse::ast::{self, TypeDef, TypeHeader, ValueDef as AstValueDef}; -use roc_parse::pattern::PatternType; -use roc_problem::can::{Problem, RuntimeError, ShadowKind}; -use roc_region::all::{Loc, Region}; -use roc_types::subs::{VarStore, Variable}; -use std::collections::HashMap; -use std::fmt::Debug; -use ven_graph::{strongly_connected_components, topological_sort_into_groups}; - -use crate::{ - lang::{ - core::{ - expr::{expr2::Expr2, expr_to_expr2::expr_to_expr2, output::Output}, - fun_def::FunctionDef, - pattern::{self, symbols_from_pattern, to_pattern_id, Pattern2, PatternId}, - types::{to_annotation2, Alias, Annotation2, Signature, Type2, TypeId}, - val_def::ValueDef, - }, - env::Env, - rigids::Rigids, - scope::Scope, - }, - mem_pool::{ - pool::{NodeId, Pool}, - pool_vec::PoolVec, - shallow_clone::ShallowClone, - }, -}; - -#[derive(Debug)] -pub enum Def { - AnnotationOnly { rigids: Rigids, annotation: TypeId }, - Value(ValueDef), - Function(FunctionDef), -} - -impl Def { - pub fn symbols(&self, pool: &Pool) -> MutSet { - let mut output = MutSet::default(); - - match self { - Def::AnnotationOnly { .. } => todo!("lost pattern information here ... "), - Def::Value(value_def) => match value_def { - ValueDef::WithAnnotation { pattern_id, .. } - | ValueDef::NoAnnotation { pattern_id, .. } => { - let pattern2 = &pool[*pattern_id]; - output.extend(symbols_from_pattern(pool, pattern2)); - } - }, - Def::Function(function_def) => match function_def { - FunctionDef::NoAnnotation { name, .. } - | FunctionDef::WithAnnotation { name, .. } => { - output.insert(*name); - } - }, - } - - output - } -} - -impl ShallowClone for Def { - fn shallow_clone(&self) -> Self { - match self { - Self::AnnotationOnly { rigids, annotation } => Self::AnnotationOnly { - rigids: rigids.shallow_clone(), - annotation: *annotation, - }, - Self::Value(def) => Self::Value(def.shallow_clone()), - Self::Function(def) => Self::Function(def.shallow_clone()), - } - } -} - -/// A Def that has had patterns and type annnotations canonicalized, -/// but no Expr canonicalization has happened yet. Also, it has had spaces -/// and nesting resolved, and knows whether annotations are standalone or not. -#[derive(Debug)] -pub enum PendingDef<'a> { - /// A standalone annotation with no body - AnnotationOnly( - &'a Loc>, - PatternId, - &'a Loc>, - ), - /// A body with no type annotation - Body(&'a Loc>, PatternId, &'a Loc>), - /// A body with a type annotation - TypedBody( - &'a Loc>, - PatternId, - &'a Loc>, - &'a Loc>, - ), - - /// A type alias, e.g. `Ints : List Int` - Alias { - name: Loc, - vars: Vec>, - ann: &'a Loc>, - }, - - /// An invalid alias, that is ignored in the rest of the pipeline - /// e.g. a shadowed alias, or a definition like `MyAlias 1 : Int` - /// with an incorrect pattern - InvalidAlias, -} - -fn to_pending_def<'a>( - env: &mut Env<'a>, - def: &'a ast::Def<'a>, - scope: &mut Scope, - pattern_type: PatternType, -) -> Option<(Output, PendingDef<'a>)> { - use roc_parse::ast::Def::*; - - match def { - Value(AstValueDef::Annotation(loc_pattern, loc_ann)) => { - // This takes care of checking for shadowing and adding idents to scope. - let (output, loc_can_pattern) = pattern::to_pattern_id( - env, - scope, - pattern_type, - &loc_pattern.value, - loc_pattern.region, - ); - - Some(( - output, - PendingDef::AnnotationOnly(loc_pattern, loc_can_pattern, loc_ann), - )) - } - Value(AstValueDef::Body(loc_pattern, loc_expr)) => { - // This takes care of checking for shadowing and adding idents to scope. - let (output, loc_can_pattern) = pattern::to_pattern_id( - env, - scope, - pattern_type, - &loc_pattern.value, - loc_pattern.region, - ); - - Some(( - output, - PendingDef::Body(loc_pattern, loc_can_pattern, loc_expr), - )) - } - - Value(AstValueDef::AnnotatedBody { - ann_pattern, - ann_type, - comment: _, - body_pattern, - body_expr, - }) => { - if ann_pattern.value.equivalent(&body_pattern.value) { - // NOTE: Pick the body pattern, picking the annotation one is - // incorrect in the presence of optional record fields! - // - // { x, y } : { x : Int, y ? Bool }* - // { x, y ? False } = rec - Some(pending_typed_body( - env, - body_pattern, - ann_type, - body_expr, - scope, - pattern_type, - )) - } else { - // the pattern of the annotation does not match the pattern of the body direc - env.problem(Problem::SignatureDefMismatch { - annotation_pattern: ann_pattern.region, - def_pattern: body_pattern.region, - }); - - // TODO: Should we instead build some PendingDef::InvalidAnnotatedBody ? This would - // remove the `Option` on this function (and be probably more reliable for further - // problem/error reporting) - None - } - } - - Type(TypeDef::Alias { - header: TypeHeader { name, vars }, - ann, - }) => { - let region = Region::span_across(&name.region, &ann.region); - - match scope.introduce( - name.value.into(), - &env.exposed_ident_ids, - &mut env.ident_ids, - region, - ) { - Ok(symbol) => { - let mut can_rigids: Vec> = Vec::with_capacity(vars.len()); - - for loc_var in vars.iter() { - match loc_var.value { - ast::Pattern::Identifier(name) - if name.chars().next().unwrap().is_lowercase() => - { - let lowercase = Lowercase::from(name); - can_rigids.push(Loc { - value: lowercase, - region: loc_var.region, - }); - } - _ => { - // any other pattern in this position is a syntax error. - env.problem(Problem::InvalidAliasRigid { - alias_name: symbol, - region: loc_var.region, - }); - - return Some((Output::default(), PendingDef::InvalidAlias)); - } - } - } - - Some(( - Output::default(), - PendingDef::Alias { - name: Loc { - region: name.region, - value: symbol, - }, - vars: can_rigids, - ann, - }, - )) - } - - Err((original_region, loc_shadowed_symbol)) => { - env.problem(Problem::Shadowing { - original_region, - shadow: loc_shadowed_symbol, - kind: ShadowKind::Variable, - }); - - Some((Output::default(), PendingDef::InvalidAlias)) - } - } - } - - Type(TypeDef::Opaque { .. }) => todo_opaques!(), - Type(TypeDef::Ability { .. }) => todo_abilities!(), - - Value(AstValueDef::Expect(_)) => todo!(), - - SpaceBefore(sub_def, _) | SpaceAfter(sub_def, _) => { - to_pending_def(env, sub_def, scope, pattern_type) - } - - NotYetImplemented(s) => todo!("{}", s), - } -} - -fn pending_typed_body<'a>( - env: &mut Env<'a>, - loc_pattern: &'a Loc>, - loc_ann: &'a Loc>, - loc_expr: &'a Loc>, - scope: &mut Scope, - pattern_type: PatternType, -) -> (Output, PendingDef<'a>) { - // This takes care of checking for shadowing and adding idents to scope. - let (output, loc_can_pattern) = to_pattern_id( - env, - scope, - pattern_type, - &loc_pattern.value, - loc_pattern.region, - ); - - ( - output, - PendingDef::TypedBody(loc_pattern, loc_can_pattern, loc_ann, loc_expr), - ) -} - -fn from_pending_alias<'a>( - env: &mut Env<'a>, - scope: &mut Scope, - name: Loc, - vars: Vec>, - ann: &'a Loc>, - mut output: Output, -) -> Output { - let symbol = name.value; - - match to_annotation2(env, scope, &ann.value, ann.region) { - Annotation2::Erroneous(_) => todo!(), - Annotation2::Annotation { - named_rigids, - unnamed_rigids, - symbols, - signature, - } => { - // Record all the annotation's references in output.references.lookups - - for symbol in symbols { - output.references.lookups.insert(symbol); - output.references.referenced_aliases.insert(symbol); - } - - for loc_lowercase in vars { - if !named_rigids.contains_key(&loc_lowercase.value) { - env.problem(Problem::PhantomTypeArgument { - typ: symbol, - variable_region: loc_lowercase.region, - variable_name: loc_lowercase.value.clone(), - }); - } - } - - let rigids = Rigids::new(named_rigids, unnamed_rigids, env.pool); - - let annotation = match signature { - Signature::Value { annotation } => annotation, - Signature::Function { - arguments, - closure_type_id, - return_type_id, - } => Type2::Function(arguments, closure_type_id, return_type_id), - Signature::FunctionWithAliases { annotation, .. } => annotation, - }; - - if annotation.contains_symbol(env.pool, symbol) { - // the alias is recursive. If it's a tag union, we attempt to fix this - if let Type2::TagUnion(tags, ext) = annotation { - // re-canonicalize the alias with the alias already in scope - let rec_var = env.var_store.fresh(); - let rec_type_union = Type2::RecursiveTagUnion(rec_var, tags, ext); - - // NOTE this only looks at the symbol, and just assumes that the - // recursion is not polymorphic - rec_type_union.substitute_alias(env.pool, symbol, Type2::Variable(rec_var)); - - let annotation_id = env.add(rec_type_union, ann.region); - let named = rigids.named(env.pool); - - scope.add_alias(env.pool, symbol, named, annotation_id); - } else { - env.problem(Problem::CyclicAlias(symbol, name.region, vec![])); - return output; - } - } else { - let annotation_id = env.add(annotation, ann.region); - let named = rigids.named(env.pool); - - scope.add_alias(env.pool, symbol, named, annotation_id); - } - - output - } - } -} - -// TODO trim down these arguments! -#[allow(clippy::too_many_arguments)] -#[allow(clippy::cognitive_complexity)] -fn canonicalize_pending_def<'a>( - env: &mut Env<'a>, - pending_def: PendingDef<'a>, - mut output: Output, - scope: &mut Scope, - can_defs_by_symbol: &mut MutMap, - refs_by_symbol: &mut MutMap, - aliases: &mut MutMap, -) -> Output { - use PendingDef::*; - - // Make types for the body expr, even if we won't end up having a body. - let expr_var = env.var_store.fresh(); - - match pending_def { - AnnotationOnly(_, loc_can_pattern, loc_ann) => { - // annotation sans body cannot introduce new rigids that are visible in other annotations - // but the rigids can show up in type error messages, so still register them - - match to_annotation2(env, scope, &loc_ann.value, loc_ann.region) { - Annotation2::Erroneous(_) => todo!(), - Annotation2::Annotation { - named_rigids, - unnamed_rigids, - symbols, - signature, - } => { - // Record all the annotation's references in output.references.lookups - - for symbol in symbols { - output.references.lookups.insert(symbol); - output.references.referenced_aliases.insert(symbol); - } - - let rigids = Rigids::new(named_rigids, unnamed_rigids, env.pool); - - let annotation = match signature { - Signature::Value { annotation } => annotation, - Signature::Function { - arguments, - closure_type_id, - return_type_id, - } => Type2::Function(arguments, closure_type_id, return_type_id), - Signature::FunctionWithAliases { annotation, .. } => annotation, - }; - let annotation = env.add(annotation, loc_ann.region); - - let def = Def::AnnotationOnly { rigids, annotation }; - - for symbol in symbols_from_pattern(env.pool, env.pool.get(loc_can_pattern)) { - can_defs_by_symbol.insert(symbol, def.shallow_clone()); - } - - output - } - } - } - - PendingDef::Alias { name, ann, vars } => { - from_pending_alias(env, scope, name, vars, ann, output) - } - - InvalidAlias => { - // invalid aliases (shadowed, incorrect patterns ) - todo!() - } - - TypedBody(loc_pattern, loc_can_pattern, loc_ann, loc_expr) => { - match to_annotation2(env, scope, &loc_ann.value, loc_ann.region) { - Annotation2::Erroneous(_) => todo!(), - Annotation2::Annotation { - named_rigids, - unnamed_rigids, - symbols, - signature, - } => { - // Record all the annotation's references in output.references.lookups - - for symbol in symbols { - output.references.lookups.insert(symbol); - output.references.referenced_aliases.insert(symbol); - } - - // Ensure rigid type vars and their names are known in the output. - for (name, &var) in named_rigids.iter() { - output.introduced_variables.insert_named(name.clone(), var); - } - let rigids = Rigids::new(named_rigids, unnamed_rigids, env.pool); - - // bookkeeping for tail-call detection. If we're assigning to an - // identifier (e.g. `f = \x -> ...`), then this symbol can be tail-called. - let outer_identifier = env.tailcallable_symbol; - - if let Pattern2::Identifier(ref defined_symbol) = env.pool[loc_can_pattern] { - env.tailcallable_symbol = Some(*defined_symbol); - }; - - // register the name of this closure, to make sure the closure won't capture it's own name - if let (Pattern2::Identifier(ref defined_symbol), &ast::Expr::Closure(_, _)) = - (&env.pool[loc_can_pattern], &loc_expr.value) - { - env.closure_name_symbol = Some(*defined_symbol); - }; - - let (loc_can_expr, can_output) = - expr_to_expr2(env, scope, &loc_expr.value, loc_expr.region); - - output.references.union_mut(can_output.references.clone()); - - // reset the tailcallable_symbol - env.tailcallable_symbol = outer_identifier; - - // First, make sure we are actually assigning an identifier instead of (for example) a tag. - // - // If we're assigning (UserId userId) = ... then this is certainly not a closure declaration, - // which also implies it's not a self tail call! - // - // Only defs of the form (foo = ...) can be closure declarations or self tail calls. - match loc_can_expr { - Expr2::Closure { - args: closure_args, - body_id, - extra, - uniq_symbol: closure_symbol, - .. - } => { - let symbol = match env.pool[loc_can_pattern] { - Pattern2::Identifier(ref s) => *s, - _ => todo!( - r"this is an error; functions must be bound with an identifier pattern!" - ), - }; - - // Since everywhere in the code it'll be referred to by its defined name, - // remove its generated name from the closure map. - let references = - env.closures.remove(&closure_symbol).unwrap_or_else(|| { - panic!( r"Tried to remove symbol {:?} from procedures, but it was not found: {:?}", closure_symbol, env.closures) - }); - - // TODO should we re-insert this function into env.closures? - env.closures.insert(symbol, references); - - // Recursion doesn't count as referencing. (If it did, all recursive functions - // would result in circular def errors!) - refs_by_symbol.entry(symbol).and_modify(|(_, refs)| { - refs.lookups.remove(&symbol); - }); - - // Functions' references don't count in defs. - // See 3d5a2560057d7f25813112dfa5309956c0f9e6a9 and its - // parent commit for the bug this fixed! - let refs = References::new(); - - let arguments: PoolVec<(NodeId, PatternId)> = - PoolVec::with_capacity(closure_args.len() as u32, env.pool); - - let return_type: TypeId; - - let annotation = match signature { - Signature::Value { .. } => { - todo!("type annotation says 0 arguments, but it's a function") - } - Signature::Function { - arguments: type_arguments, - closure_type_id, - return_type_id, - } - | Signature::FunctionWithAliases { - arguments: type_arguments, - closure_type_id, - return_type_id, - .. - } => { - if arguments.len() != type_arguments.len() { - panic!("argument number mismatch"); - } - - let it: Vec<_> = closure_args - .iter(env.pool) - .map(|(x, y)| (*x, *y)) - .zip( - type_arguments - .iter(env.pool) - .map(|t| t.shallow_clone()), - ) - .collect(); - - for (node_id, ((_, pattern_id), typ)) in - arguments.iter_node_ids().zip(it.into_iter()) - { - let typ = env.pool.add(typ); - env.pool[node_id] = (typ, pattern_id); - } - - return_type = return_type_id; - } - }; - - let function_def = FunctionDef::WithAnnotation { - name: symbol, - arguments, - rigids: env.pool.add(rigids), - return_type, - body_id, - }; - - let def = Def::Function(function_def); - - can_defs_by_symbol.insert(symbol, def); - - output - } - - _ => { - let annotation = match signature { - Signature::Value { annotation } => annotation, - Signature::Function { - arguments, - closure_type_id, - return_type_id, - } => Type2::Function(arguments, closure_type_id, return_type_id), - Signature::FunctionWithAliases { annotation, .. } => annotation, - }; - let annotation = env.add(annotation, loc_ann.region); - - let value_def = ValueDef::WithAnnotation { - pattern_id: loc_can_pattern, - expr_id: env.pool.add(loc_can_expr), - type_id: annotation, - rigids, - expr_var: env.var_store.fresh(), - }; - - let def = Def::Value(value_def); - - for symbol in - symbols_from_pattern(env.pool, env.pool.get(loc_can_pattern)) - { - can_defs_by_symbol.insert(symbol, def.shallow_clone()); - } - - for (_, (symbol, region)) in scope.idents() { - // if !vars_by_symbol.contains_key(&symbol) { - // continue; - // } - let refs = can_output.references.clone(); - refs_by_symbol.insert(*symbol, (*region, refs)); - } - - output - } - } - } - } - } - - Body(loc_pattern, loc_can_pattern, loc_expr) => { - // bookkeeping for tail-call detection. If we're assigning to an - // identifier (e.g. `f = \x -> ...`), then this symbol can be tail-called. - let outer_identifier = env.tailcallable_symbol; - - if let Pattern2::Identifier(ref defined_symbol) = env.pool[loc_can_pattern] { - env.tailcallable_symbol = Some(*defined_symbol); - }; - - // register the name of this closure, to make sure the closure won't capture it's own name - if let (Pattern2::Identifier(ref defined_symbol), &ast::Expr::Closure(_, _)) = - (&env.pool[loc_can_pattern], &loc_expr.value) - { - env.closure_name_symbol = Some(*defined_symbol); - }; - - let (loc_can_expr, can_output) = - expr_to_expr2(env, scope, &loc_expr.value, loc_expr.region); - - output.references.union_mut(can_output.references.clone()); - - // reset the tailcallable_symbol - env.tailcallable_symbol = outer_identifier; - - // First, make sure we are actually assigning an identifier instead of (for example) a tag. - // - // If we're assigning (UserId userId) = ... then this is certainly not a closure declaration, - // which also implies it's not a self tail call! - // - // Only defs of the form (foo = ...) can be closure declarations or self tail calls. - match loc_can_expr { - Expr2::Closure { - args: closure_args, - body_id, - extra, - uniq_symbol: closure_symbol, - .. - } => { - let symbol = match env.pool[loc_can_pattern] { - Pattern2::Identifier(ref s) => *s, - _ => todo!( - r"this is an error; functions must be bound with an identifier pattern!" - ), - }; - - // Since everywhere in the code it'll be referred to by its defined name, - // remove its generated name from the closure map. - let references = - env.closures.remove(&closure_symbol).unwrap_or_else(|| { - panic!( r"Tried to remove symbol {:?} from procedures, but it was not found: {:?}", closure_symbol, env.closures) - }); - - // TODO should we re-insert this function into env.closures? - env.closures.insert(symbol, references); - - // Recursion doesn't count as referencing. (If it did, all recursive functions - // would result in circular def errors!) - refs_by_symbol.entry(symbol).and_modify(|(_, refs)| { - refs.lookups.remove(&symbol); - }); - - // Functions' references don't count in defs. - // See 3d5a2560057d7f25813112dfa5309956c0f9e6a9 and its - // parent commit for the bug this fixed! - let refs = References::new(); - - let arguments: PoolVec<(Variable, PatternId)> = - PoolVec::with_capacity(closure_args.len() as u32, env.pool); - - let it: Vec<_> = closure_args.iter(env.pool).map(|(x, y)| (*x, *y)).collect(); - - for (node_id, (_, pattern_id)) in arguments.iter_node_ids().zip(it.into_iter()) - { - env.pool[node_id] = (env.var_store.fresh(), pattern_id); - } - - let function_def = FunctionDef::NoAnnotation { - name: symbol, - arguments, - return_var: env.var_store.fresh(), - body_id, - }; - - let def = Def::Function(function_def); - - can_defs_by_symbol.insert(symbol, def); - - output - } - - _ => { - let value_def = ValueDef::NoAnnotation { - pattern_id: loc_can_pattern, - expr_id: env.pool.add(loc_can_expr), - expr_var: env.var_store.fresh(), - }; - - let def = Def::Value(value_def); - - for symbol in symbols_from_pattern(env.pool, env.pool.get(loc_can_pattern)) { - can_defs_by_symbol.insert(symbol, def.shallow_clone()); - } - - for (_, (symbol, region)) in scope.idents() { - // if !vars_by_symbol.contains_key(&symbol) { - // continue; - // } - let refs = can_output.references.clone(); - refs_by_symbol.insert(*symbol, (*region, refs)); - } - - output - } - } - } - } -} - -#[derive(Clone, Debug, Default, PartialEq)] -pub struct References { - pub bound_symbols: MutSet, - pub lookups: MutSet, - pub referenced_aliases: MutSet, - pub calls: MutSet, -} - -impl References { - pub fn new() -> References { - Self::default() - } - - pub fn union_mut(&mut self, other: References) { - self.lookups.extend(other.lookups); - self.calls.extend(other.calls); - self.bound_symbols.extend(other.bound_symbols); - self.referenced_aliases.extend(other.referenced_aliases); - } - - pub fn has_lookup(&self, symbol: Symbol) -> bool { - self.lookups.contains(&symbol) - } -} - -#[derive(Debug)] -pub struct CanDefs { - pub refs_by_symbol: MutMap, - pub can_defs_by_symbol: MutMap, - pub aliases: MutMap, -} - -#[inline(always)] -pub fn canonicalize_defs<'a>( - env: &mut Env<'a>, - mut output: Output, - original_scope: &Scope, - loc_defs: &'a [&'a Loc>], - pattern_type: PatternType, -) -> (CanDefs, Scope, Output, MutMap) { - // Canonicalizing defs while detecting shadowing involves a multi-step process: - // - // 1. Go through each of the patterns. - // 2. For each identifier pattern, get the scope.symbol() for the ident. (That symbol will use the home module for its module.) - // 3. If that symbol is already in scope, then we're about to shadow it. Error! - // 4. Otherwise, add it to the scope immediately, so we can detect shadowing within the same - // pattern (e.g. (Foo a a) = ...) - // 5. Add this canonicalized pattern and its corresponding ast::Expr to pending_exprs. - // 5. Once every pattern has been processed and added to scope, go back and canonicalize the exprs from - // pending_exprs, this time building up a canonical def for each one. - // - // This way, whenever any expr is doing lookups, it knows everything that's in scope - - // even defs that appear after it in the source. - // - // This naturally handles recursion too, because a given expr which refers - // to itself won't be processed until after its def has been added to scope. - - // Record both the original and final idents from the scope, - // so we can diff them while detecting unused defs. - let mut scope = original_scope.shallow_clone(); - let num_defs = loc_defs.len(); - let mut refs_by_symbol = MutMap::default(); - let mut can_defs_by_symbol = HashMap::with_capacity_and_hasher(num_defs, default_hasher()); - let mut pending = Vec::with_capacity(num_defs); // TODO bump allocate this! - - // Canonicalize all the patterns, record shadowing problems, and store - // the ast::Expr values in pending_exprs for further canonicalization - // once we've finished assembling the entire scope. - for loc_def in loc_defs { - match to_pending_def(env, &loc_def.value, &mut scope, pattern_type) { - None => (), - Some((new_output, pending_def)) => { - // store the top-level defs, used to ensure that closures won't capture them - if let PatternType::TopLevelDef = pattern_type { - match &pending_def { - PendingDef::AnnotationOnly(_, loc_can_pattern, _) - | PendingDef::Body(_, loc_can_pattern, _) - | PendingDef::TypedBody(_, loc_can_pattern, _, _) => { - env.top_level_symbols.extend(symbols_from_pattern( - env.pool, - env.pool.get(*loc_can_pattern), - )) - } - PendingDef::Alias { .. } | PendingDef::InvalidAlias => {} - } - } - // Record the ast::Expr for later. We'll do another pass through these - // once we have the entire scope assembled. If we were to canonicalize - // the exprs right now, they wouldn't have symbols in scope from defs - // that get would have gotten added later in the defs list! - pending.push(pending_def); - output.union(new_output); - } - } - } - - if cfg!(debug_assertions) { - env.home.register_debug_idents(&env.ident_ids); - } - - // TODO what to do here? aliases are already in the scope! - let mut aliases = MutMap::default(); - let mut value_defs = Vec::new(); - - for pending_def in pending.into_iter() { - match pending_def { - PendingDef::Alias { name, vars, ann } => { - output = from_pending_alias(env, &mut scope, name, vars, ann, output); - } - other => value_defs.push(other), - } - } - - // TODO - // correct_mutual_recursive_type_alias(env, &mut aliases, var_store); - - // Now that we have the scope completely assembled, and shadowing resolved, - // we're ready to canonicalize any body exprs. - for pending_def in value_defs.into_iter() { - output = canonicalize_pending_def( - env, - pending_def, - output, - &mut scope, - &mut can_defs_by_symbol, - &mut refs_by_symbol, - &mut aliases, - ); - - // TODO we should do something with these references; they include - // things like type annotations. - } - - // Determine which idents we introduced in the course of this process. - let mut symbols_introduced = MutMap::default(); - - for (symbol, region) in scope.symbols() { - if !original_scope.contains_symbol(symbol) { - symbols_introduced.insert(symbol, region); - } - } - - // This returns both the defs info as well as the new scope. - // - // We have to return the new scope because we added defs to it - // (and those lookups shouldn't fail later, e.g. when canonicalizing - // the return expr), but we didn't want to mutate the original scope - // directly because we wanted to keep a clone of it around to diff - // when looking for unused idents. - // - // We have to return the scope separately from the defs, because the - // defs need to get moved later. - ( - CanDefs { - refs_by_symbol, - can_defs_by_symbol, - aliases, - }, - scope, - output, - symbols_introduced, - ) -} - -// See github.com/rtfeldman/roc/issues/800 for discussion of the large_enum_variant check. -#[derive(Debug)] -#[allow(clippy::large_enum_variant)] -pub enum Declaration { - Declare(Def), - DeclareRec(Vec), - Builtin(Def), - InvalidCycle(Vec, Vec<(Region /* pattern */, Region /* expr */)>), -} - -impl Declaration { - pub fn def_count(&self) -> usize { - use Declaration::*; - match self { - Declare(_) => 1, - DeclareRec(defs) => defs.len(), - InvalidCycle(_, _) => 0, - Builtin(_) => 0, - } - } -} - -#[inline(always)] -pub fn sort_can_defs( - env: &mut Env<'_>, - defs: CanDefs, - mut output: Output, -) -> (Result, RuntimeError>, Output) { - let CanDefs { - refs_by_symbol, - can_defs_by_symbol, - aliases, - } = defs; - - // for (symbol, alias) in aliases.into_iter() { - // output.aliases.insert(symbol, alias); - // } - - // Determine the full set of references by traversing the graph. - let mut visited_symbols = MutSet::default(); - let returned_lookups = MutSet::clone(&output.references.lookups); - - // Start with the return expression's referenced locals. They're the only ones that count! - // - // If I have two defs which reference each other, but neither of them is referenced - // in the return expression, I don't want either of them (or their references) to end up - // in the final output.references. They were unused, and so were their references! - // - // The reason we need a graph here is so we don't overlook transitive dependencies. - // For example, if I have `a = b + 1` and the def returns `a + 1`, then the - // def as a whole references both `a` *and* `b`, even though it doesn't - // directly mention `b` - because `a` depends on `b`. If we didn't traverse a graph here, - // we'd erroneously give a warning that `b` was unused since it wasn't directly referenced. - for symbol in returned_lookups.into_iter() { - // We only care about local symbols in this analysis. - if symbol.module_id() == env.home { - // Traverse the graph and look up *all* the references for this local symbol. - let refs = - references_from_local(symbol, &mut visited_symbols, &refs_by_symbol, &env.closures); - - output.references.union_mut(refs); - } - } - - for symbol in output.references.calls.clone() { - // Traverse the graph and look up *all* the references for this call. - // Reuse the same visited_symbols as before; if we already visited it, - // we won't learn anything new from visiting it again! - let refs = - references_from_call(symbol, &mut visited_symbols, &refs_by_symbol, &env.closures); - - output.references.union_mut(refs); - } - - let mut defined_symbols: Vec = Vec::new(); - let mut defined_symbols_set: MutSet = MutSet::default(); - - for symbol in can_defs_by_symbol.keys().into_iter() { - defined_symbols.push(*symbol); - defined_symbols_set.insert(*symbol); - } - - // Use topological sort to reorder the defs based on their dependencies to one another. - // This way, during code gen, no def will refer to a value that hasn't been initialized yet. - // As a bonus, the topological sort also reveals any cycles between the defs, allowing - // us to give a CircularAssignment error for invalid (mutual) recursion, and a `DeclareRec` for mutually - // recursive definitions. - - // All successors that occur in the body of a symbol. - let all_successors_without_self = |symbol: &Symbol| -> MutSet { - // This may not be in refs_by_symbol. For example, the `f` in `f x` here: - // - // f = \z -> z - // - // (\x -> - // a = f x - // x - // ) - // - // It's not part of the current defs (the one with `a = f x`); rather, - // it's in the enclosing scope. It's still referenced though, so successors - // will receive it as an argument! - match refs_by_symbol.get(symbol) { - Some((_, references)) => { - // We can only sort the symbols at the current level. That is safe because - // symbols defined at higher levels cannot refer to symbols at lower levels. - // Therefore they can never form a cycle! - // - // In the above example, `f` cannot reference `a`, and in the closure - // a call to `f` cannot cycle back to `a`. - let mut loc_succ = local_successors(&references, &env.closures); - - // if the current symbol is a closure, peek into its body - if let Some(References { lookups, .. }) = env.closures.get(symbol) { - let home = env.home; - - for lookup in lookups { - if lookup != symbol && lookup.module_id() == home { - // DO NOT register a self-call behind a lambda! - // - // We allow `boom = \_ -> boom {}`, but not `x = x` - loc_succ.insert(*lookup); - } - } - } - - // remove anything that is not defined in the current block - loc_succ.retain(|key| defined_symbols_set.contains(key)); - - loc_succ - } - None => MutSet::default(), - } - }; - - // All successors that occur in the body of a symbol, including the symbol itself - // This is required to determine whether a symbol is recursive. Recursive symbols - // (that are not faulty) always need a DeclareRec, even if there is just one symbol in the - // group - let mut all_successors_with_self = |symbol: &Symbol| -> MutSet { - // This may not be in refs_by_symbol. For example, the `f` in `f x` here: - // - // f = \z -> z - // - // (\x -> - // a = f x - // x - // ) - // - // It's not part of the current defs (the one with `a = f x`); rather, - // it's in the enclosing scope. It's still referenced though, so successors - // will receive it as an argument! - match refs_by_symbol.get(symbol) { - Some((_, references)) => { - // We can only sort the symbols at the current level. That is safe because - // symbols defined at higher levels cannot refer to symbols at lower levels. - // Therefore they can never form a cycle! - // - // In the above example, `f` cannot reference `a`, and in the closure - // a call to `f` cannot cycle back to `a`. - let mut loc_succ = local_successors(&references, &env.closures); - - // if the current symbol is a closure, peek into its body - if let Some(References { lookups, .. }) = env.closures.get(symbol) { - for lookup in lookups { - loc_succ.insert(*lookup); - } - } - - // remove anything that is not defined in the current block - loc_succ.retain(|key| defined_symbols_set.contains(key)); - - loc_succ - } - None => MutSet::default(), - } - }; - - // If a symbol is a direct successor of itself, there is an invalid cycle. - // The difference with the function above is that this one does not look behind lambdas, - // but does consider direct self-recursion. - let direct_successors = |symbol: &Symbol| -> MutSet { - match refs_by_symbol.get(symbol) { - Some((_, references)) => { - let mut loc_succ = local_successors(&references, &env.closures); - - // NOTE: if the symbol is a closure we DONT look into its body - - // remove anything that is not defined in the current block - loc_succ.retain(|key| defined_symbols_set.contains(key)); - - // NOTE: direct recursion does matter here: `x = x` is invalid recursion! - - loc_succ - } - None => MutSet::default(), - } - }; - - // TODO also do the same `addDirects` check elm/compiler does, so we can - // report an error if a recursive definition can't possibly terminate! - match ven_graph::topological_sort_into_groups( - defined_symbols.as_slice(), - all_successors_without_self, - ) { - Ok(groups) => { - let mut declarations = Vec::new(); - - // groups are in reversed order - let mut can_defs_by_symbol = can_defs_by_symbol; - let cdbs = &mut can_defs_by_symbol; - for group in groups.into_iter().rev() { - group_to_declaration( - &group, - &env.closures, - &mut all_successors_with_self, - cdbs, - &mut declarations, - ); - } - - (Ok(declarations), output) - } - Err((mut groups, nodes_in_cycle)) => { - let mut declarations = Vec::new(); - let problems = Vec::new(); - - // nodes_in_cycle are symbols that form a syntactic cycle. That isn't always a problem, - // and in general it's impossible to decide whether it is. So we use a crude heuristic: - // - // Definitions where the cycle occurs behind a lambda are OK - // - // boom = \_ -> boom {} - // - // But otherwise we report an error, e.g. - // - // foo = if b then foo else bar - - for cycle in strongly_connected_components(&nodes_in_cycle, all_successors_without_self) - { - // check whether the cycle is faulty, which is when it has - // a direct successor in the current cycle. This catches things like: - // - // x = x - // - // or - // - // p = q - // q = p - let is_invalid_cycle = match cycle.get(0) { - Some(symbol) => { - let mut succs = direct_successors(symbol); - - succs.retain(|key| cycle.contains(key)); - - !succs.is_empty() - } - None => false, - }; - - if is_invalid_cycle { - // We want to show the entire cycle in the error message, so expand it out. - let mut loc_symbols = Vec::new(); - - for symbol in cycle { - match refs_by_symbol.get(&symbol) { - None => unreachable!( - r#"Symbol `{:?}` not found in refs_by_symbol! refs_by_symbol was: {:?}"#, - symbol, refs_by_symbol - ), - Some((region, _)) => { - loc_symbols.push(Loc::at(*region, symbol)); - } - } - } - - // TODO we don't store those regions any more! - // let regions = Vec::with_capacity(can_defs_by_symbol.len()); - // for def in can_defs_by_symbol.values() { - // regions.push((def.loc_pattern.region, def.loc_expr.region)); - // } - // - // // Sort them by line number to make the report more helpful. - // loc_symbols.sort(); - // regions.sort(); - - // let symbols_in_cycle: Vec = - // loc_symbols.into_iter().map(|s| s.value).collect(); - // - // problems.push(Problem::RuntimeError(RuntimeError::CircularDef( - // symbols_in_cycle.clone(), - // regions.clone(), - // ))); - // - // declarations.push(Declaration::InvalidCycle(symbols_in_cycle, regions)); - panic!("Invalid Cycle"); - } else { - // slightly inefficient, because we know this becomes exactly one DeclareRec already - groups.push(cycle); - } - } - - // now we have a collection of groups whose dependencies are not cyclic. - // They are however not yet topologically sorted. Here we have to get a bit - // creative to get all the definitions in the correct sorted order. - - let mut group_ids = Vec::with_capacity(groups.len()); - let mut symbol_to_group_index = MutMap::default(); - for (i, group) in groups.iter().enumerate() { - for symbol in group { - symbol_to_group_index.insert(*symbol, i); - } - - group_ids.push(i); - } - - let successors_of_group = |group_id: &usize| { - let mut result = MutSet::default(); - - // for each symbol in this group - for symbol in &groups[*group_id] { - // find its successors - for succ in all_successors_without_self(symbol) { - // and add its group to the result - result.insert(symbol_to_group_index[&succ]); - } - } - - // don't introduce any cycles to self - result.remove(group_id); - - result - }; - - match ven_graph::topological_sort_into_groups(&group_ids, successors_of_group) { - Ok(sorted_group_ids) => { - let mut can_defs_by_symbol = can_defs_by_symbol; - let cdbs = &mut can_defs_by_symbol; - for sorted_group in sorted_group_ids.iter().rev() { - for group_id in sorted_group.iter().rev() { - let group = &groups[*group_id]; - - group_to_declaration( - group, - &env.closures, - &mut all_successors_with_self, - cdbs, - &mut declarations, - ); - } - } - } - Err(_) => unreachable!("there should be no cycles now!"), - } - - for problem in problems { - env.problem(problem); - } - - (Ok(declarations), output) - } - } -} - -pub fn references_from_local<'a, T>( - defined_symbol: Symbol, - visited: &'a mut MutSet, - refs_by_def: &'a MutMap, - closures: &'a MutMap, -) -> References -where - T: Debug, -{ - let mut answer: References = References::new(); - - match refs_by_def.get(&defined_symbol) { - Some((_, refs)) => { - visited.insert(defined_symbol); - - for local in refs.lookups.iter() { - if !visited.contains(&local) { - let other_refs: References = - references_from_local(*local, visited, refs_by_def, closures); - - answer.union_mut(other_refs); - } - - answer.lookups.insert(*local); - } - - for call in refs.calls.iter() { - if !visited.contains(&call) { - let other_refs = references_from_call(*call, visited, refs_by_def, closures); - - answer.union_mut(other_refs); - } - - answer.calls.insert(*call); - } - - answer - } - None => answer, - } -} - -pub fn references_from_call<'a, T>( - call_symbol: Symbol, - visited: &'a mut MutSet, - refs_by_def: &'a MutMap, - closures: &'a MutMap, -) -> References -where - T: Debug, -{ - match closures.get(&call_symbol) { - Some(references) => { - let mut answer = references.clone(); - - visited.insert(call_symbol); - - for closed_over_local in references.lookups.iter() { - if !visited.contains(&closed_over_local) { - let other_refs = - references_from_local(*closed_over_local, visited, refs_by_def, closures); - - answer.union_mut(other_refs); - } - - answer.lookups.insert(*closed_over_local); - } - - for call in references.calls.iter() { - if !visited.contains(&call) { - let other_refs = references_from_call(*call, visited, refs_by_def, closures); - - answer.union_mut(other_refs); - } - - answer.calls.insert(*call); - } - - answer - } - None => { - // If the call symbol was not in the closure map, that means we're calling a non-function and - // will get a type mismatch later. For now, assume no references as a result of the "call." - References::new() - } - } -} - -fn local_successors( - references: &References, - closures: &MutMap, -) -> MutSet { - let mut answer = references.lookups.clone(); - - for call_symbol in references.calls.iter() { - answer.extend(call_successors(*call_symbol, closures)); - } - - answer -} - -fn call_successors<'a>( - call_symbol: Symbol, - closures: &'a MutMap, -) -> MutSet { - let mut answer = MutSet::default(); - let mut seen = MutSet::default(); - let mut queue = vec![call_symbol]; - - while let Some(symbol) = queue.pop() { - if seen.contains(&symbol) { - continue; - } - - if let Some(references) = closures.get(&symbol) { - answer.extend(references.lookups.iter().copied()); - queue.extend(references.calls.iter().copied()); - - seen.insert(symbol); - } - } - - answer -} - -fn group_to_declaration( - group: &[Symbol], - closures: &MutMap, - successors: &mut dyn FnMut(&Symbol) -> MutSet, - can_defs_by_symbol: &mut MutMap, - declarations: &mut Vec, -) { - use Declaration::*; - - // We want only successors in the current group, otherwise definitions get duplicated - let filtered_successors = |symbol: &Symbol| -> MutSet { - let mut result = successors(symbol); - - result.retain(|key| group.contains(key)); - result - }; - - // TODO fix this - // Patterns like - // - // { x, y } = someDef - // - // Can bind multiple symbols. When not incorrectly recursive (which is guaranteed in this function), - // normally `someDef` would be inserted twice. We use the region of the pattern as a unique key - // for a definition, so every definition is only inserted (thus typechecked and emitted) once - // let mut seen_pattern_regions: MutSet = MutSet::default(); - - for cycle in strongly_connected_components(&group, filtered_successors) { - if cycle.len() == 1 { - let symbol = &cycle[0]; - - if let Some(can_def) = can_defs_by_symbol.remove(&symbol) { - // Determine recursivity of closures that are not tail-recursive - - let is_recursive = successors(&symbol).contains(&symbol); - - if is_recursive { - declarations.push(DeclareRec(vec![can_def])); - } else { - declarations.push(Declare(can_def)); - } - } - } else { - let mut can_defs = Vec::new(); - - // Topological sort gives us the reverse of the sorting we want! - for symbol in cycle.into_iter().rev() { - if let Some(can_def) = can_defs_by_symbol.remove(&symbol) { - can_defs.push(can_def); - } - } - - declarations.push(DeclareRec(can_defs)); - } - } -} diff --git a/ast/src/lang/core/def/def2.rs b/ast/src/lang/core/def/def2.rs deleted file mode 100644 index f5106b9dda..0000000000 --- a/ast/src/lang/core/def/def2.rs +++ /dev/null @@ -1,58 +0,0 @@ -use roc_module::symbol::IdentId; - -use crate::{ - lang::core::expr::{expr2::Expr2, expr2_to_string::expr2_to_string}, - mem_pool::pool::{NodeId, Pool}, -}; - -// A top level definition, not inside a function. For example: `main = "Hello, world!"` -#[derive(Debug)] -pub enum Def2 { - // ValueDef example: `main = "Hello, world!"`. identifier -> `main`, expr -> "Hello, world!" - ValueDef { - identifier_id: IdentId, - expr_id: NodeId, - }, - Blank, - CommentsBefore { - comments: String, - def_id: DefId, - }, - CommentsAfter { - comments: String, - def_id: DefId, - }, -} - -pub type DefId = NodeId; - -pub fn def2_to_string(node_id: DefId, pool: &Pool) -> String { - let mut full_string = String::new(); - let def2 = pool.get(node_id); - - match def2 { - Def2::ValueDef { - identifier_id, - expr_id, - } => { - full_string.push_str(&format!( - "Def2::ValueDef(identifier_id: >>{:?}), expr_id: >>{:?})", - identifier_id, - expr2_to_string(*expr_id, pool) - )); - } - Def2::Blank => { - full_string.push_str("Def2::Blank"); - } - Def2::CommentsBefore { - comments, - def_id: _, - } => full_string.push_str(comments), - Def2::CommentsAfter { - comments, - def_id: _, - } => full_string.push_str(comments), - } - - full_string -} diff --git a/ast/src/lang/core/def/def_to_def2.rs b/ast/src/lang/core/def/def_to_def2.rs deleted file mode 100644 index 1690fa346f..0000000000 --- a/ast/src/lang/core/def/def_to_def2.rs +++ /dev/null @@ -1,215 +0,0 @@ -use bumpalo::collections::Vec as BumpVec; -use bumpalo::Bump; -use roc_module::ident::{Ident, IdentStr}; -use roc_parse::{ast::CommentOrNewline, parser::SyntaxError}; -use roc_region::all::Region; - -use crate::lang::{core::expr::expr_to_expr2::loc_expr_to_expr2, env::Env, scope::Scope}; - -use super::def2::Def2; - -fn spaces_to_comments(spaces: &[CommentOrNewline]) -> Option { - if !spaces.is_empty() && !all_newlines(spaces) { - let mut all_comments_str = String::new(); - - for comment in spaces.iter().filter(|c_or_nl| !c_or_nl.is_newline()) { - all_comments_str.push_str(&comment.to_string_repr()); - } - - Some(all_comments_str) - } else { - None - } -} - -pub fn toplevel_defs_to_defs2<'a>( - arena: &'a Bump, - env: &mut Env<'a>, - scope: &mut Scope, - parsed_defs: roc_parse::ast::Defs<'a>, - region: Region, -) -> Vec { - let mut result = Vec::with_capacity(parsed_defs.tags.len()); - - for (index, def) in parsed_defs.defs().enumerate() { - let mut def = match def { - Err(roc_parse::ast::ValueDef::Body(&loc_pattern, &loc_expr)) => { - let expr2 = loc_expr_to_expr2(arena, loc_expr, env, scope, region).0; - let expr_id = env.pool.add(expr2); - - use roc_parse::ast::Pattern::*; - - match loc_pattern.value { - Identifier(id_str) => { - let identifier_id = - env.ident_ids.get_or_insert(&Ident(IdentStr::from(id_str))); - - // TODO support with annotation - Def2::ValueDef { - identifier_id, - expr_id, - } - } - other => { - unimplemented!( - "I don't yet know how to convert the pattern {:?} into an expr2", - other - ) - } - } - } - - other => { - unimplemented!( - "I don't know how to make an expr2 from this def yet: {:?}", - other - ) - } - }; - - let spaces_before = &parsed_defs.spaces[parsed_defs.space_before[index].indices()]; - let spaces_after = &parsed_defs.spaces[parsed_defs.space_after[index].indices()]; - - if let Some(comments) = spaces_to_comments(spaces_before) { - let inner_def_id = env.pool.add(def); - def = Def2::CommentsBefore { - comments, - def_id: inner_def_id, - }; - } - - if let Some(comments) = spaces_to_comments(spaces_after) { - let inner_def_id = env.pool.add(def); - def = Def2::CommentsAfter { - comments, - def_id: inner_def_id, - }; - } - - result.push(def) - } - - result -} - -pub fn defs_to_defs2<'a>( - arena: &'a Bump, - env: &mut Env<'a>, - scope: &mut Scope, - parsed_defs: &'a BumpVec>>, - region: Region, -) -> Vec { - parsed_defs - .iter() - .map(|loc| def_to_def2(arena, env, scope, &loc.value, region)) - .collect() -} - -pub fn def_to_def2<'a>( - arena: &'a Bump, - env: &mut Env<'a>, - scope: &mut Scope, - parsed_def: &'a roc_parse::ast::Def<'a>, - region: Region, -) -> Def2 { - use roc_parse::ast::Def::*; - //dbg!(parsed_def); - - match parsed_def { - SpaceBefore(inner_def, comments) => { - // filter comments - if !comments.is_empty() && !all_newlines(comments) { - let inner_def = def_to_def2(arena, env, scope, inner_def, region); - - let inner_def_id = env.pool.add(inner_def); - let mut all_comments_str = String::new(); - - for comment in comments.iter().filter(|c_or_nl| !c_or_nl.is_newline()) { - all_comments_str.push_str(&comment.to_string_repr()); - } - - Def2::CommentsBefore { - comments: all_comments_str, - def_id: inner_def_id, - } - } else { - def_to_def2(arena, env, scope, inner_def, region) - } - } - SpaceAfter(inner_def, comments) => { - // filter comments - if !comments.is_empty() && !all_newlines(comments) { - let inner_def = def_to_def2(arena, env, scope, inner_def, region); - - let inner_def_id = env.pool.add(inner_def); - let mut all_comments_str = String::new(); - - for comment in comments.iter().filter(|c_or_nl| !c_or_nl.is_newline()) { - all_comments_str.push_str(&comment.to_string_repr()); - } - - Def2::CommentsAfter { - def_id: inner_def_id, - comments: all_comments_str, - } - } else { - def_to_def2(arena, env, scope, inner_def, region) - } - } - Value(roc_parse::ast::ValueDef::Body(&loc_pattern, &loc_expr)) => { - let expr2 = loc_expr_to_expr2(arena, loc_expr, env, scope, region).0; - let expr_id = env.pool.add(expr2); - - use roc_parse::ast::Pattern::*; - - match loc_pattern.value { - Identifier(id_str) => { - let identifier_id = env.ident_ids.get_or_insert(&Ident(IdentStr::from(id_str))); - - // TODO support with annotation - Def2::ValueDef { - identifier_id, - expr_id, - } - } - other => { - unimplemented!( - "I don't yet know how to convert the pattern {:?} into an expr2", - other - ) - } - } - } - other => { - unimplemented!( - "I don't know how to make an expr2 from this def yet: {:?}", - other - ) - } - } -} - -fn all_newlines(comments: &[CommentOrNewline]) -> bool { - comments - .iter() - .all(|com_or_newline| com_or_newline.is_newline()) -} - -pub fn str_to_def2<'a>( - arena: &'a Bump, - input: &'a str, - env: &mut Env<'a>, - scope: &mut Scope, - region: Region, -) -> Result, SyntaxError<'a>> { - match roc_parse::test_helpers::parse_defs_with(arena, input.trim()) { - Ok(vec_loc_def) => Ok(defs_to_defs2( - arena, - env, - scope, - arena.alloc(vec_loc_def), - region, - )), - Err(fail) => Err(fail), - } -} diff --git a/ast/src/lang/core/def/mod.rs b/ast/src/lang/core/def/mod.rs deleted file mode 100644 index 7ab541b811..0000000000 --- a/ast/src/lang/core/def/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod def; -pub mod def2; -pub mod def_to_def2; diff --git a/ast/src/lang/core/expr/expr2.rs b/ast/src/lang/core/expr/expr2.rs deleted file mode 100644 index c6e5068b26..0000000000 --- a/ast/src/lang/core/expr/expr2.rs +++ /dev/null @@ -1,228 +0,0 @@ -use arrayvec::ArrayString; -use roc_types::subs::Variable; - -use crate::{ - lang::core::{fun_def::FunctionDef, pattern::Pattern2, val_def::ValueDef}, - mem_pool::{pool::NodeId, pool_str::PoolStr, pool_vec::PoolVec}, -}; -use roc_can::expr::Recursive; -use roc_module::called_via::CalledVia; -use roc_module::low_level::LowLevel; -use roc_module::symbol::Symbol; - -use super::record_field::RecordField; - -pub const ARR_STRING_CAPACITY: usize = 24; -pub type ArrString = ArrayString; - -// TODO make the inner types private? -pub type ExprId = NodeId; - -/// An Expr that fits in 32B. -/// It has a 1B discriminant and variants which hold payloads of at most 31B. -#[derive(Debug)] -pub enum Expr2 { - /// A negative number literal without a dot - SmallInt { - number: IntVal, // 16B - var: Variable, // 4B - style: IntStyle, // 1B - text: PoolStr, // 8B - }, - // TODO(rvcas): rename this eventually - /// A large (over 64-bit) negative number literal without a dot. - /// This variant can't use IntVal because if IntVal stored 128-bit - /// integers, it would be 32B on its own because of alignment. - I128 { - number: i128, // 16B - var: Variable, // 4B - style: IntStyle, // 1B - text: PoolStr, // 8B - }, - // TODO(rvcas): rename this eventually - /// A large (over 64-bit) nonnegative number literal without a dot - /// This variant can't use IntVal because if IntVal stored 128-bit - /// integers, it would be 32B on its own because of alignment. - U128 { - number: u128, // 16B - var: Variable, // 4B - style: IntStyle, // 1B - text: PoolStr, // 8B - }, - /// A floating-point literal (with a dot) - Float { - number: FloatVal, // 16B - var: Variable, // 4B - text: PoolStr, // 8B - }, - /// string literals of length up to 30B - SmallStr(ArrString), // 31B - /// string literals of length 31B or more - Str(PoolStr), // 8B - // Lookups - Var(Symbol), // 8B - InvalidLookup(PoolStr), // 8B - - List { - elem_var: Variable, // 4B - elems: PoolVec, // 8B - }, - If { - cond_var: Variable, // 4B - expr_var: Variable, // 4B - branches: PoolVec<(ExprId, ExprId)>, // 8B - final_else: ExprId, // 4B - }, - When { - cond_var: Variable, // 4B - expr_var: Variable, // 4B - branches: PoolVec, // 8B - cond: ExprId, // 4B - }, - LetRec { - defs: PoolVec, // 8B - body_var: Variable, // 8B - body_id: ExprId, // 4B - }, - LetFunction { - def_id: NodeId, // 4B - body_var: Variable, // 8B - body_id: ExprId, // 4B - }, - LetValue { - def_id: NodeId, // 4B - body_id: ExprId, // 4B - body_var: Variable, // 4B - }, - Call { - args: PoolVec<(Variable, ExprId)>, // 8B - expr_id: ExprId, // 4B - expr_var: Variable, // 4B - fn_var: Variable, // 4B - closure_var: Variable, // 4B - called_via: CalledVia, // 2B - }, - RunLowLevel { - op: LowLevel, // 1B - args: PoolVec<(Variable, ExprId)>, // 8B - ret_var: Variable, // 4B - }, - Closure { - args: PoolVec<(Variable, NodeId)>, // 8B - uniq_symbol: Symbol, // 8B This is a globally unique symbol for the closure - body_id: ExprId, // 4B - function_type: Variable, // 4B - recursive: Recursive, // 1B - extra: NodeId, // 4B - }, - // Product Types - Record { - record_var: Variable, // 4B - fields: PoolVec, // 8B - }, - /// Empty record constant - EmptyRecord, - /// Look up exactly one field on a record, e.g. (expr).foo. - Access { - field: PoolStr, // 4B - expr: ExprId, // 4B - record_var: Variable, // 4B - ext_var: Variable, // 4B - field_var: Variable, // 4B - }, - - /// field accessor as a function, e.g. (.foo) expr - Accessor { - function_var: Variable, // 4B - closure_var: Variable, // 4B - field: PoolStr, // 4B - record_var: Variable, // 4B - ext_var: Variable, // 4B - field_var: Variable, // 4B - }, - Update { - symbol: Symbol, // 8B - updates: PoolVec, // 8B - record_var: Variable, // 4B - ext_var: Variable, // 4B - }, - - // Sum Types - Tag { - name: PoolStr, // 4B - variant_var: Variable, // 4B - ext_var: Variable, // 4B - arguments: PoolVec<(Variable, ExprId)>, // 8B - }, - Blank, // Rendered as empty box in editor - - // Compiles, but will crash if reached - RuntimeError(/* TODO make a version of RuntimeError that fits in 15B */), -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] -pub enum Problem { - RanOutOfNodeIds, -} - -pub type Res = Result; - -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub enum IntStyle { - Decimal, - Octal, - Hex, - Binary, -} - -impl IntStyle { - pub fn from_base(base: roc_parse::ast::Base) -> Self { - use roc_parse::ast::Base; - match base { - Base::Decimal => Self::Decimal, - Base::Octal => Self::Octal, - Base::Hex => Self::Hex, - Base::Binary => Self::Binary, - } - } -} - -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub enum IntVal { - I64(i64), - U64(u64), - I32(i32), - U32(u32), - I16(i16), - U16(u16), - I8(i8), - U8(u8), -} - -#[test] -fn size_of_intval() { - assert_eq!(std::mem::size_of::(), 16); -} - -#[derive(Debug, Copy, Clone, PartialEq)] -pub enum FloatVal { - F64(f64), - F32(f32), -} - -#[derive(Debug)] -pub struct WhenBranch { - pub patterns: PoolVec, // 4B - pub body: ExprId, // 3B - pub guard: Option, // 4B -} - -/// This is overflow data from a Closure variant, which needs to store -/// more than 32B of total data -#[derive(Debug)] -pub struct ClosureExtra { - pub return_type: Variable, // 4B - pub captured_symbols: PoolVec<(Symbol, Variable)>, // 8B - pub closure_type: Variable, // 4B - pub closure_ext_var: Variable, // 4B -} diff --git a/ast/src/lang/core/expr/expr2_to_string.rs b/ast/src/lang/core/expr/expr2_to_string.rs deleted file mode 100644 index 7d557fd669..0000000000 --- a/ast/src/lang/core/expr/expr2_to_string.rs +++ /dev/null @@ -1,162 +0,0 @@ -use crate::{ - lang::core::{expr::record_field::RecordField, val_def::value_def_to_string}, - mem_pool::pool::Pool, -}; - -use super::expr2::{Expr2, ExprId}; -use roc_types::subs::Variable; - -pub fn expr2_to_string(node_id: ExprId, pool: &Pool) -> String { - let mut full_string = String::new(); - let expr2 = pool.get(node_id); - - expr2_to_string_helper(expr2, 0, pool, &mut full_string); - - full_string -} - -fn get_spacing(indent_level: usize) -> String { - std::iter::repeat(" ") - .take(indent_level) - .collect::>() - .join("") -} - -fn expr2_to_string_helper( - expr2: &Expr2, - indent_level: usize, - pool: &Pool, - out_string: &mut String, -) { - out_string.push_str(&get_spacing(indent_level)); - - match expr2 { - Expr2::SmallStr(arr_string) => out_string.push_str(&format!( - "{}{}{}", - "SmallStr(\"", - arr_string.as_str(), - "\")", - )), - Expr2::Str(pool_str) => { - out_string.push_str(&format!("{}{}{}", "Str(\"", pool_str.as_str(pool), "\")",)) - } - Expr2::Blank => out_string.push_str("Blank"), - Expr2::EmptyRecord => out_string.push_str("EmptyRecord"), - Expr2::Record { record_var, fields } => { - out_string.push_str("Record:\n"); - out_string.push_str(&var_to_string(record_var, indent_level + 1)); - - out_string.push_str(&format!("{}fields: [\n", get_spacing(indent_level + 1))); - - let mut first_child = true; - - for field in fields.iter(pool) { - if !first_child { - out_string.push_str(", ") - } else { - first_child = false; - } - - match field { - RecordField::InvalidLabelOnly(pool_str, var) => { - out_string.push_str(&format!( - "{}({}, Var({:?})", - get_spacing(indent_level + 2), - pool_str.as_str(pool), - var, - )); - } - RecordField::LabelOnly(pool_str, var, symbol) => { - out_string.push_str(&format!( - "{}({}, Var({:?}), Symbol({:?})", - get_spacing(indent_level + 2), - pool_str.as_str(pool), - var, - symbol - )); - } - RecordField::LabeledValue(pool_str, var, val_node_id) => { - out_string.push_str(&format!( - "{}({}, Var({:?}), Expr2(\n", - get_spacing(indent_level + 2), - pool_str.as_str(pool), - var, - )); - - let val_expr2 = pool.get(*val_node_id); - expr2_to_string_helper(val_expr2, indent_level + 3, pool, out_string); - out_string.push_str(&format!("{})\n", get_spacing(indent_level + 2))); - } - } - } - - out_string.push_str(&format!("{}]\n", get_spacing(indent_level + 1))); - } - Expr2::List { elem_var, elems } => { - out_string.push_str("List:\n"); - out_string.push_str(&var_to_string(elem_var, indent_level + 1)); - out_string.push_str(&format!("{}elems: [\n", get_spacing(indent_level + 1))); - - let mut first_elt = true; - - for elem_expr2_id in elems.iter(pool) { - if !first_elt { - out_string.push_str(", ") - } else { - first_elt = false; - } - - let elem_expr2 = pool.get(*elem_expr2_id); - - expr2_to_string_helper(elem_expr2, indent_level + 2, pool, out_string) - } - - out_string.push_str(&format!("{}]\n", get_spacing(indent_level + 1))); - } - Expr2::InvalidLookup(pool_str) => { - out_string.push_str(&format!("InvalidLookup({})", pool_str.as_str(pool))); - } - Expr2::SmallInt { text, .. } => { - out_string.push_str(&format!("SmallInt({})", text.as_str(pool))); - } - Expr2::LetValue { - def_id, body_id, .. - } => { - out_string.push_str(&format!( - "LetValue(def_id: >>{:?}), body_id: >>{:?})", - value_def_to_string(pool.get(*def_id), pool), - pool.get(*body_id) - )); - } - Expr2::Call { .. } => { - out_string.push_str(&format!("Call({:?})", expr2,)); - } - Expr2::Closure { args, .. } => { - out_string.push_str("Closure:\n"); - out_string.push_str(&format!("{}args: [\n", get_spacing(indent_level + 1))); - - for (_, pattern_id) in args.iter(pool) { - let arg_pattern2 = pool.get(*pattern_id); - - out_string.push_str(&format!( - "{}{:?}\n", - get_spacing(indent_level + 2), - arg_pattern2 - )); - } - } - &Expr2::Var { .. } => { - out_string.push_str(&format!("{:?}", expr2,)); - } - Expr2::RuntimeError { .. } => { - out_string.push_str("RuntimeError\n"); - } - other => todo!("Implement for {:?}", other), - } - - out_string.push('\n'); -} - -fn var_to_string(some_var: &Variable, indent_level: usize) -> String { - format!("{}Var({:?})\n", get_spacing(indent_level + 1), some_var) -} diff --git a/ast/src/lang/core/expr/expr_to_expr2.rs b/ast/src/lang/core/expr/expr_to_expr2.rs deleted file mode 100644 index 7f1e6d4e03..0000000000 --- a/ast/src/lang/core/expr/expr_to_expr2.rs +++ /dev/null @@ -1,707 +0,0 @@ -use bumpalo::Bump; -use roc_can::expr::{IntValue, Recursive}; -use roc_can::num::{ - finish_parsing_base, finish_parsing_float, finish_parsing_num, ParsedNumResult, -}; -use roc_can::operator::desugar_expr; -use roc_collections::all::MutSet; -use roc_module::symbol::Symbol; -use roc_parse::{ast::Expr, pattern::PatternType}; -use roc_problem::can::{Problem, RuntimeError}; -use roc_region::all::{Loc, Region}; - -use super::{expr2::Expr2, output::Output}; -use crate::canonicalization::canonicalize::{ - canonicalize_fields, canonicalize_lookup, canonicalize_when_branch, CanonicalizeRecordProblem, -}; -use crate::lang::core::declaration::decl_to_let; -use crate::lang::core::def::def::{canonicalize_defs, sort_can_defs}; -use crate::lang::core::expr::expr2::ClosureExtra; -use crate::lang::core::pattern::to_pattern2; -use crate::lang::core::str::flatten_str_literal; -use crate::mem_pool::shallow_clone::ShallowClone; -use crate::{ - lang::{ - core::expr::expr2::{ExprId, FloatVal, IntStyle, IntVal}, - env::Env, - scope::Scope, - }, - mem_pool::{pool_str::PoolStr, pool_vec::PoolVec}, -}; - -pub fn loc_expr_to_expr2<'a>( - arena: &'a Bump, - loc_expr: Loc>, - env: &mut Env<'a>, - scope: &mut Scope, - region: Region, -) -> (Expr2, Output) { - let desugared_loc_expr = desugar_expr(arena, arena.alloc(loc_expr)); - - expr_to_expr2(env, scope, arena.alloc(desugared_loc_expr.value), region) -} - -const ZERO: Region = Region::zero(); - -pub fn expr_to_expr2<'a>( - env: &mut Env<'a>, - scope: &mut Scope, - parse_expr: &'a roc_parse::ast::Expr<'a>, - region: Region, -) -> (Expr2, self::Output) { - use roc_parse::ast::Expr::*; - //dbg!("{:?}", parse_expr); - - match parse_expr { - Float(string) => { - match finish_parsing_float(string) { - Ok((string_without_suffix, float, _bound)) => { - let expr = Expr2::Float { - number: FloatVal::F64(float), - var: env.var_store.fresh(), - text: PoolStr::new(string_without_suffix, env.pool), - }; - - (expr, Output::default()) - } - Err((raw, error)) => { - // emit runtime error - let runtime_error = RuntimeError::InvalidFloat(error, ZERO, raw.into()); - - env.problem(Problem::RuntimeError(runtime_error)); - // - // Expr::RuntimeError(runtime_error) - todo!() - } - } - } - Num(string) => { - match finish_parsing_num(string) { - Ok(( - parsed, - ParsedNumResult::UnknownNum(int, _) | ParsedNumResult::Int(int, _), - )) => { - let expr = Expr2::SmallInt { - number: IntVal::I64(match int { - IntValue::U128(_) => todo!(), - IntValue::I128(n) => i128::from_ne_bytes(n) as i64, // FIXME - }), - var: env.var_store.fresh(), - // TODO non-hardcode - style: IntStyle::Decimal, - text: PoolStr::new(parsed, env.pool), - }; - - (expr, Output::default()) - } - Ok((parsed, ParsedNumResult::Float(float, _))) => { - let expr = Expr2::Float { - number: FloatVal::F64(float), - var: env.var_store.fresh(), - text: PoolStr::new(parsed, env.pool), - }; - - (expr, Output::default()) - } - Err((raw, error)) => { - // emit runtime error - let runtime_error = RuntimeError::InvalidInt( - error, - roc_parse::ast::Base::Decimal, - ZERO, - raw.into(), - ); - - env.problem(Problem::RuntimeError(runtime_error)); - // - // Expr::RuntimeError(runtime_error) - todo!() - } - } - } - NonBase10Int { - string, - base, - is_negative, - } => { - match finish_parsing_base(string, *base, *is_negative) { - Ok((int, _bound)) => { - let expr = Expr2::SmallInt { - number: IntVal::I64(match int { - IntValue::U128(_) => todo!(), - IntValue::I128(n) => i128::from_ne_bytes(n) as i64, // FIXME - }), - var: env.var_store.fresh(), - // TODO non-hardcode - style: IntStyle::from_base(*base), - text: PoolStr::new(string, env.pool), - }; - - (expr, Output::default()) - } - Err((raw, error)) => { - // emit runtime error - let runtime_error = RuntimeError::InvalidInt(error, *base, ZERO, raw.into()); - - env.problem(Problem::RuntimeError(runtime_error)); - // - // Expr::RuntimeError(runtime_error) - todo!() - } - } - } - - Str(literal) => flatten_str_literal(env, scope, literal), - - List(items) => { - let mut output = Output::default(); - let output_ref = &mut output; - - let elems: PoolVec = PoolVec::with_capacity(items.len() as u32, env.pool); - - for (node_id, item) in elems.iter_node_ids().zip(items.iter()) { - let (expr, sub_output) = expr_to_expr2(env, scope, &item.value, item.region); - - output_ref.union(sub_output); - - let expr_id = env.pool.add(expr); - env.pool[node_id] = expr_id; - } - - let expr = Expr2::List { - elem_var: env.var_store.fresh(), - elems, - }; - - (expr, output) - } - - Tag(tag) => { - // a tag without any arguments - ( - Expr2::Tag { - name: PoolStr::new(tag, env.pool), - variant_var: env.var_store.fresh(), - ext_var: env.var_store.fresh(), - arguments: PoolVec::empty(env.pool), - }, - Output::default(), - ) - } - - RecordUpdate { - fields, - update: loc_update, - } => { - let (can_update, update_out) = - expr_to_expr2(env, scope, &loc_update.value, loc_update.region); - - if let Expr2::Var(symbol) = &can_update { - match canonicalize_fields(env, scope, fields.items) { - Ok((can_fields, mut output)) => { - output.references.union_mut(update_out.references); - - let answer = Expr2::Update { - record_var: env.var_store.fresh(), - ext_var: env.var_store.fresh(), - symbol: *symbol, - updates: can_fields, - }; - - (answer, output) - } - Err(CanonicalizeRecordProblem::InvalidOptionalValue { - field_name: _, - field_region: _, - record_region: _, - }) => { - // let runtime_error = roc_problem::can::RuntimeError::InvalidOptionalValue { - // field_name, - // field_region, - // record_region, - // }; - // - // env.problem(Problem::RuntimeError(runtime_error)); - - todo!() - } - } - } else { - // only (optionally qualified) variables can be updated, not arbitrary expressions - - // let error = roc_problem::can::RuntimeError::InvalidRecordUpdate { - // region: can_update.region, - // }; - // - // let answer = Expr::RuntimeError(error.clone()); - // - // env.problems.push(Problem::RuntimeError(error)); - // - // (answer, Output::default()) - todo!("{:?}", &can_update) - } - } - - Record(fields) => { - if fields.is_empty() { - (Expr2::EmptyRecord, Output::default()) - } else { - match canonicalize_fields(env, scope, fields.items) { - Ok((can_fields, output)) => ( - Expr2::Record { - record_var: env.var_store.fresh(), - fields: can_fields, - }, - output, - ), - Err(CanonicalizeRecordProblem::InvalidOptionalValue { - field_name: _, - field_region: _, - record_region: _, - }) => { - // let runtime_error = RuntimeError::InvalidOptionalValue { - // field_name, - // field_region, - // record_region, - // }; - // - // env.problem(runtime_error); - // ( - // Expr::RuntimeError( - // ), - // Output::default(), - // - // ) - todo!() - } - } - } - } - - Access(record_expr, field) => { - // TODO - let region = ZERO; - let (record_expr_id, output) = to_expr_id(env, scope, record_expr, region); - - ( - Expr2::Access { - record_var: env.var_store.fresh(), - field_var: env.var_store.fresh(), - ext_var: env.var_store.fresh(), - expr: record_expr_id, - field: PoolStr::new(field, env.pool), - }, - output, - ) - } - - AccessorFunction(field) => ( - Expr2::Accessor { - function_var: env.var_store.fresh(), - record_var: env.var_store.fresh(), - ext_var: env.var_store.fresh(), - closure_var: env.var_store.fresh(), - field_var: env.var_store.fresh(), - field: PoolStr::new(field, env.pool), - }, - Output::default(), - ), - - If(branches, final_else) => { - let mut new_branches = Vec::with_capacity(branches.len()); - let mut output = Output::default(); - - for (condition, then_branch) in branches.iter() { - let (cond, cond_output) = - expr_to_expr2(env, scope, &condition.value, condition.region); - - let (then_expr, then_output) = - expr_to_expr2(env, scope, &then_branch.value, then_branch.region); - - output.references.union_mut(cond_output.references); - output.references.union_mut(then_output.references); - - new_branches.push((env.pool.add(cond), env.pool.add(then_expr))); - } - - let (else_expr, else_output) = - expr_to_expr2(env, scope, &final_else.value, final_else.region); - - output.references.union_mut(else_output.references); - - let expr = Expr2::If { - cond_var: env.var_store.fresh(), - expr_var: env.var_store.fresh(), - branches: PoolVec::new(new_branches.into_iter(), env.pool), - final_else: env.pool.add(else_expr), - }; - - (expr, output) - } - - When(loc_cond, branches) => { - // Infer the condition expression's type. - let cond_var = env.var_store.fresh(); - let (can_cond, mut output) = - expr_to_expr2(env, scope, &loc_cond.value, loc_cond.region); - - // the condition can never be a tail-call - output.tail_call = None; - - let can_branches = PoolVec::with_capacity(branches.len() as u32, env.pool); - - for (node_id, branch) in can_branches.iter_node_ids().zip(branches.iter()) { - let (can_when_branch, branch_references) = - canonicalize_when_branch(env, scope, *branch, &mut output); - - output.references.union_mut(branch_references); - - env.pool[node_id] = can_when_branch; - } - - // A "when" with no branches is a runtime error, but it will mess things up - // if code gen mistakenly thinks this is a tail call just because its condition - // happened to be one. (The condition gave us our initial output value.) - if branches.is_empty() { - output.tail_call = None; - } - - // Incorporate all three expressions into a combined Output value. - let expr = Expr2::When { - expr_var: env.var_store.fresh(), - cond_var, - cond: env.pool.add(can_cond), - branches: can_branches, - }; - - (expr, output) - } - - Closure(loc_arg_patterns, loc_body_expr) => { - // The globally unique symbol that will refer to this closure once it gets converted - // into a top-level procedure for code gen. - // - // In the Foo module, this will look something like Foo.$1 or Foo.$2. - let symbol = env - .closure_name_symbol - .unwrap_or_else(|| env.gen_unique_symbol()); - env.closure_name_symbol = None; - - // The body expression gets a new scope for canonicalization. - // Shadow `scope` to make sure we don't accidentally use the original one for the - // rest of this block, but keep the original around for later diffing. - let original_scope = scope; - let mut scope = original_scope.shallow_clone(); - let can_args = PoolVec::with_capacity(loc_arg_patterns.len() as u32, env.pool); - let mut output = Output::default(); - - let mut bound_by_argument_patterns = MutSet::default(); - - for (node_id, loc_pattern) in can_args.iter_node_ids().zip(loc_arg_patterns.iter()) { - let (new_output, can_arg) = to_pattern2( - env, - &mut scope, - roc_parse::pattern::PatternType::FunctionArg, - &loc_pattern.value, - loc_pattern.region, - ); - - bound_by_argument_patterns - .extend(new_output.references.bound_symbols.iter().copied()); - - output.union(new_output); - - let pattern_id = env.add(can_arg, loc_pattern.region); - env.pool[node_id] = (env.var_store.fresh(), pattern_id); - } - - let (body_expr, new_output) = - expr_to_expr2(env, &mut scope, &loc_body_expr.value, loc_body_expr.region); - - let mut captured_symbols: MutSet = - new_output.references.lookups.iter().copied().collect(); - - // filter out the closure's name itself - captured_symbols.remove(&symbol); - - // symbols bound either in this pattern or deeper down are not captured! - captured_symbols.retain(|s| !new_output.references.bound_symbols.contains(s)); - captured_symbols.retain(|s| !bound_by_argument_patterns.contains(s)); - - // filter out top-level symbols - // those will be globally available, and don't need to be captured - captured_symbols.retain(|s| !env.top_level_symbols.contains(s)); - - // filter out imported symbols - // those will be globally available, and don't need to be captured - captured_symbols.retain(|s| s.module_id() == env.home); - - // TODO any Closure that has an empty `captured_symbols` list could be excluded! - - output.union(new_output); - - // filter out aliases - captured_symbols.retain(|s| !output.references.referenced_aliases.contains(s)); - - // filter out functions that don't close over anything - captured_symbols.retain(|s| !output.non_closures.contains(s)); - - // Now that we've collected all the references, check to see if any of the args we defined - // went unreferenced. If any did, report them as unused arguments. - for (sub_symbol, region) in scope.symbols() { - if !original_scope.contains_symbol(sub_symbol) { - if !output.references.has_lookup(sub_symbol) { - // The body never referenced this argument we declared. It's an unused argument! - env.problem(Problem::UnusedArgument(symbol, sub_symbol, region)); - } - - // We shouldn't ultimately count arguments as referenced locals. Otherwise, - // we end up with weird conclusions like the expression (\x -> x + 1) - // references the (nonexistent) local variable x! - output.references.lookups.remove(&sub_symbol); - } - } - - env.register_closure(symbol, output.references.clone()); - - let mut captured_symbols: Vec<_> = captured_symbols - .into_iter() - .map(|s| (s, env.var_store.fresh())) - .collect(); - - // sort symbols, so we know the order in which they're stored in the closure record - captured_symbols.sort(); - - // store that this function doesn't capture anything. It will be promoted to a - // top-level function, and does not need to be captured by other surrounding functions. - if captured_symbols.is_empty() { - output.non_closures.insert(symbol); - } - - let captured_symbols = PoolVec::new(captured_symbols.into_iter(), env.pool); - - let extra = ClosureExtra { - return_type: env.var_store.fresh(), // 4B - captured_symbols, // 8B - closure_type: env.var_store.fresh(), // 4B - closure_ext_var: env.var_store.fresh(), // 4B - }; - - ( - Expr2::Closure { - function_type: env.var_store.fresh(), - uniq_symbol: symbol, - recursive: Recursive::NotRecursive, - args: can_args, - body_id: env.add(body_expr, loc_body_expr.region), - extra: env.pool.add(extra), - }, - output, - ) - } - - Apply(loc_fn, loc_args, application_style) => { - // The expression that evaluates to the function being called, e.g. `foo` in - // (foo) bar baz - let fn_region = loc_fn.region; - - // Canonicalize the function expression and its arguments - let (fn_expr, mut output) = expr_to_expr2(env, scope, &loc_fn.value, fn_region); - // The function's return type - let args = PoolVec::with_capacity(loc_args.len() as u32, env.pool); - - for (node_id, loc_arg) in args.iter_node_ids().zip(loc_args.iter()) { - let (arg_expr_id, arg_out) = to_expr_id(env, scope, &loc_arg.value, loc_arg.region); - - env.pool[node_id] = (env.var_store.fresh(), arg_expr_id); - - output.references.union_mut(arg_out.references); - } - - // Default: We're not tail-calling a symbol (by name), we're tail-calling a function value. - output.tail_call = None; - - let expr = match fn_expr { - Expr2::Var(ref symbol) => { - output.references.calls.insert(*symbol); - - // we're tail-calling a symbol by name, check if it's the tail-callable symbol - output.tail_call = match &env.tailcallable_symbol { - Some(tc_sym) if *tc_sym == *symbol => Some(*symbol), - Some(_) | None => None, - }; - - // IDEA: Expr2::CallByName? - let fn_expr_id = env.add(fn_expr, fn_region); - Expr2::Call { - args, - expr_id: fn_expr_id, - expr_var: env.var_store.fresh(), - fn_var: env.var_store.fresh(), - closure_var: env.var_store.fresh(), - called_via: *application_style, - } - } - Expr2::RuntimeError() => { - // We can't call a runtime error; bail out by propagating it! - return (fn_expr, output); - } - Expr2::Tag { - variant_var, - ext_var, - name, - .. - } => Expr2::Tag { - variant_var, - ext_var, - name, - arguments: args, - }, - _ => { - // This could be something like ((if True then fn1 else fn2) arg1 arg2). - let fn_expr_id = env.add(fn_expr, fn_region); - Expr2::Call { - args, - expr_id: fn_expr_id, - expr_var: env.var_store.fresh(), - fn_var: env.var_store.fresh(), - closure_var: env.var_store.fresh(), - called_via: *application_style, - } - } - }; - - (expr, output) - } - - Defs(loc_defs, loc_ret) => { - let (unsorted, mut scope, defs_output, symbols_introduced) = canonicalize_defs( - env, - Output::default(), - scope, - loc_defs, - PatternType::DefExpr, - ); - - // The def as a whole is a tail call iff its return expression is a tail call. - // Use its output as a starting point because its tail_call already has the right answer! - let (ret_expr, mut output) = - expr_to_expr2(env, &mut scope, &loc_ret.value, loc_ret.region); - - output - .introduced_variables - .union(&defs_output.introduced_variables); - - output.references.union_mut(defs_output.references); - - // Now that we've collected all the references, check to see if any of the new idents - // we defined went unused by the return expression. If any were unused, report it. - for (symbol, region) in symbols_introduced { - if !output.references.has_lookup(symbol) { - env.problem(Problem::UnusedDef(symbol, region)); - } - } - - let (can_defs, output) = sort_can_defs(env, unsorted, output); - - match can_defs { - Ok(decls) => { - let mut expr = ret_expr; - - for declaration in decls.into_iter().rev() { - expr = decl_to_let(env.pool, env.var_store, declaration, expr); - } - - (expr, output) - } - Err(_err) => { - // TODO: fix this to be something from Expr2 - // (RuntimeError(err), output) - todo!() - } - } - } - - PrecedenceConflict { .. } => { - // use roc_problem::can::RuntimeError::*; - // - // let problem = PrecedenceProblem::BothNonAssociative( - // *whole_region, - // binop1.clone(), - // binop2.clone(), - // ); - // - // env.problem(Problem::PrecedenceProblem(problem.clone())); - // - // ( - // RuntimeError(InvalidPrecedence(problem, region)), - // Output::default(), - // ) - todo!() - } - MalformedClosure => { - // use roc_problem::can::RuntimeError::*; - // (RuntimeError(MalformedClosure(region)), Output::default()) - todo!() - } - MalformedIdent(_name, _problem) => { - // use roc_problem::can::RuntimeError::*; - // - // let problem = MalformedIdentifier((*name).into(), region); - // env.problem(Problem::RuntimeError(problem.clone())); - // - // (RuntimeError(problem), Output::default()) - todo!() - } - Var { - module_name, // module_name will only be filled if the original Roc code stated something like `5 + SomeModule.myVar`, module_name will be blank if it was `5 + myVar` - ident, - } => canonicalize_lookup(env, scope, module_name, ident, region), - - // Below this point, we shouln't see any of these nodes anymore because - // operator desugaring should have removed them! - bad_expr @ ParensAround(_) => { - panic!( - "A ParensAround did not get removed during operator desugaring somehow: {:#?}", - bad_expr - ); - } - bad_expr @ SpaceBefore(_, _) => { - panic!( - "A SpaceBefore did not get removed during operator desugaring somehow: {:#?}", - bad_expr - ); - } - bad_expr @ SpaceAfter(_, _) => { - panic!( - "A SpaceAfter did not get removed during operator desugaring somehow: {:#?}", - bad_expr - ); - } - bad_expr @ BinOps { .. } => { - panic!( - "A binary operator chain did not get desugared somehow: {:#?}", - bad_expr - ); - } - bad_expr @ UnaryOp(_, _) => { - panic!( - "A unary operator did not get desugared somehow: {:#?}", - bad_expr - ); - } - - rest => todo!("not yet implemented {:?}", rest), - } -} - -pub fn to_expr_id<'a>( - env: &mut Env<'a>, - scope: &mut Scope, - parse_expr: &'a roc_parse::ast::Expr<'a>, - region: Region, -) -> (ExprId, Output) { - let (expr, output) = expr_to_expr2(env, scope, parse_expr, region); - - (env.add(expr, region), output) -} diff --git a/ast/src/lang/core/expr/introduced_vars.rs b/ast/src/lang/core/expr/introduced_vars.rs deleted file mode 100644 index 26bbe6b738..0000000000 --- a/ast/src/lang/core/expr/introduced_vars.rs +++ /dev/null @@ -1,51 +0,0 @@ -use roc_collections::all::MutMap; -use roc_module::ident::Lowercase; -use roc_module::symbol::Symbol; -use roc_types::subs::Variable; - -#[derive(Clone, Debug, PartialEq, Default)] -pub struct IntroducedVariables { - // Rigids must be unique within a type annotation. - // E.g. in `identity : a -> a`, there should only be one - // variable (a rigid one, with name "a"). - // Hence `rigids : Map` - // - // But then between annotations, the same name can occur multiple times, - // but a variable can only have one name. Therefore - // `ftv : Map`. - pub wildcards: Vec, - pub var_by_name: MutMap, - pub name_by_var: MutMap, - pub host_exposed_aliases: MutMap, -} - -impl IntroducedVariables { - pub fn insert_named(&mut self, name: Lowercase, var: Variable) { - self.var_by_name.insert(name.clone(), var); - self.name_by_var.insert(var, name); - } - - pub fn insert_wildcard(&mut self, var: Variable) { - self.wildcards.push(var); - } - - pub fn insert_host_exposed_alias(&mut self, symbol: Symbol, var: Variable) { - self.host_exposed_aliases.insert(symbol, var); - } - - pub fn union(&mut self, other: &Self) { - self.wildcards.extend(other.wildcards.iter().cloned()); - self.var_by_name.extend(other.var_by_name.clone()); - self.name_by_var.extend(other.name_by_var.clone()); - self.host_exposed_aliases - .extend(other.host_exposed_aliases.clone()); - } - - pub fn var_by_name(&self, name: &Lowercase) -> Option<&Variable> { - self.var_by_name.get(name) - } - - pub fn name_by_var(&self, var: Variable) -> Option<&Lowercase> { - self.name_by_var.get(&var) - } -} diff --git a/ast/src/lang/core/expr/mod.rs b/ast/src/lang/core/expr/mod.rs deleted file mode 100644 index bb27a4ad9b..0000000000 --- a/ast/src/lang/core/expr/mod.rs +++ /dev/null @@ -1,6 +0,0 @@ -pub mod expr2; -pub mod expr2_to_string; -pub mod expr_to_expr2; -mod introduced_vars; -pub(crate) mod output; -pub mod record_field; diff --git a/ast/src/lang/core/expr/output.rs b/ast/src/lang/core/expr/output.rs deleted file mode 100644 index 4287e3f72f..0000000000 --- a/ast/src/lang/core/expr/output.rs +++ /dev/null @@ -1,30 +0,0 @@ -use crate::{ - lang::core::{def::def::References, types::Alias}, - mem_pool::pool::NodeId, -}; -use roc_collections::all::{MutMap, MutSet}; -use roc_module::symbol::Symbol; - -use super::introduced_vars::IntroducedVariables; - -#[derive(Clone, Default, Debug, PartialEq)] -pub struct Output { - pub references: References, - pub tail_call: Option, - pub introduced_variables: IntroducedVariables, - pub aliases: MutMap>, - pub non_closures: MutSet, -} - -impl Output { - pub fn union(&mut self, other: Self) { - self.references.union_mut(other.references); - - if let (None, Some(later)) = (self.tail_call, other.tail_call) { - self.tail_call = Some(later); - } - - self.aliases.extend(other.aliases); - self.non_closures.extend(other.non_closures); - } -} diff --git a/ast/src/lang/core/expr/record_field.rs b/ast/src/lang/core/expr/record_field.rs deleted file mode 100644 index aaf464799f..0000000000 --- a/ast/src/lang/core/expr/record_field.rs +++ /dev/null @@ -1,49 +0,0 @@ -use roc_types::subs::Variable; - -use crate::mem_pool::pool_str::PoolStr; -use roc_module::symbol::Symbol; - -use super::expr2::ExprId; - -#[derive(Debug)] -pub enum RecordField { - InvalidLabelOnly(PoolStr, Variable), - LabelOnly(PoolStr, Variable, Symbol), - LabeledValue(PoolStr, Variable, ExprId), -} - -use RecordField::*; - -impl RecordField { - pub fn get_record_field_var(&self) -> &Variable { - match self { - InvalidLabelOnly(_, var) => var, - LabelOnly(_, var, _) => var, - LabeledValue(_, var, _) => var, - } - } - - pub fn get_record_field_pool_str(&self) -> &PoolStr { - match self { - InvalidLabelOnly(pool_str, _) => pool_str, - LabelOnly(pool_str, _, _) => pool_str, - LabeledValue(pool_str, _, _) => pool_str, - } - } - - pub fn get_record_field_pool_str_mut(&mut self) -> &mut PoolStr { - match self { - InvalidLabelOnly(pool_str, _) => pool_str, - LabelOnly(pool_str, _, _) => pool_str, - LabeledValue(pool_str, _, _) => pool_str, - } - } - - pub fn get_record_field_val_node_id(&self) -> Option { - match self { - InvalidLabelOnly(_, _) => None, - LabelOnly(_, _, _) => None, - LabeledValue(_, _, field_val_id) => Some(*field_val_id), - } - } -} diff --git a/ast/src/lang/core/fun_def.rs b/ast/src/lang/core/fun_def.rs deleted file mode 100644 index e8aa642a86..0000000000 --- a/ast/src/lang/core/fun_def.rs +++ /dev/null @@ -1,61 +0,0 @@ -use crate::{ - lang::rigids::Rigids, - mem_pool::{pool::NodeId, pool_vec::PoolVec, shallow_clone::ShallowClone}, -}; -use roc_module::symbol::Symbol; -use roc_types::subs::Variable; - -use super::{ - expr::expr2::ExprId, - pattern::PatternId, - types::{Type2, TypeId}, -}; - -#[derive(Debug)] -pub enum FunctionDef { - WithAnnotation { - name: Symbol, // 8B - arguments: PoolVec<(NodeId, PatternId)>, // 8B - rigids: NodeId, // 4B - return_type: TypeId, // 4B - body_id: ExprId, // 4B - }, - NoAnnotation { - name: Symbol, // 8B - arguments: PoolVec<(Variable, PatternId)>, // 8B - return_var: Variable, // 4B - body_id: ExprId, // 4B - }, -} - -impl ShallowClone for FunctionDef { - fn shallow_clone(&self) -> Self { - match self { - Self::WithAnnotation { - name, - arguments, - rigids, - return_type, - body_id, - } => Self::WithAnnotation { - name: *name, - arguments: arguments.shallow_clone(), - rigids: *rigids, - return_type: *return_type, - body_id: *body_id, - }, - - Self::NoAnnotation { - name, - arguments, - return_var, - body_id, - } => Self::NoAnnotation { - name: *name, - arguments: arguments.shallow_clone(), - return_var: *return_var, - body_id: *body_id, - }, - } - } -} diff --git a/ast/src/lang/core/header.rs b/ast/src/lang/core/header.rs deleted file mode 100644 index 4b10dad537..0000000000 --- a/ast/src/lang/core/header.rs +++ /dev/null @@ -1,10 +0,0 @@ -use super::expr::expr2::ExprId; - -#[derive(Debug)] -pub struct AppHeader { - pub app_name: String, - pub packages_base: String, - pub imports: Vec, - pub provides: Vec, - pub ast_node_id: ExprId, // TODO probably want to create and use HeaderId -} diff --git a/ast/src/lang/core/mod.rs b/ast/src/lang/core/mod.rs deleted file mode 100644 index 801f6afa18..0000000000 --- a/ast/src/lang/core/mod.rs +++ /dev/null @@ -1,10 +0,0 @@ -pub mod ast; -mod declaration; -pub mod def; -pub mod expr; -pub mod fun_def; -pub mod header; -pub mod pattern; -pub mod str; -pub mod types; -pub mod val_def; diff --git a/ast/src/lang/core/pattern.rs b/ast/src/lang/core/pattern.rs deleted file mode 100644 index 7dfbe8c722..0000000000 --- a/ast/src/lang/core/pattern.rs +++ /dev/null @@ -1,651 +0,0 @@ -#![allow(clippy::all)] -#![allow(dead_code)] -#![allow(unused_imports)] - -use bumpalo::collections::Vec as BumpVec; -use roc_can::expr::{unescape_char, IntValue}; -use roc_can::num::{ - finish_parsing_base, finish_parsing_float, finish_parsing_num, ParsedNumResult, -}; -use roc_collections::all::BumpMap; -use roc_error_macros::todo_opaques; -use roc_module::symbol::{Interns, Symbol}; -use roc_parse::ast::{StrLiteral, StrSegment}; -use roc_parse::pattern::PatternType; -use roc_problem::can::{MalformedPatternProblem, Problem, RuntimeError, ShadowKind}; -use roc_region::all::Region; -use roc_types::subs::Variable; - -use crate::ast_error::{ASTResult, UnexpectedPattern2Variant}; -use crate::constrain::Constraint; -use crate::lang::core::expr::expr_to_expr2::to_expr_id; -use crate::lang::env::Env; -use crate::lang::scope::Scope; -use crate::mem_pool::pool::{NodeId, Pool}; -use crate::mem_pool::pool_str::PoolStr; -use crate::mem_pool::pool_vec::PoolVec; -use crate::mem_pool::shallow_clone::ShallowClone; - -use super::expr::expr2::{ExprId, FloatVal, IntVal}; -use super::expr::output::Output; -use super::types::Type2; - -pub type PatternId = NodeId; - -#[derive(Debug)] -pub enum Pattern2 { - Identifier(Symbol), // 8B - NumLiteral(Variable, i64), // 4B + 8B - IntLiteral(IntVal), // 16B - FloatLiteral(FloatVal), // 16B - StrLiteral(PoolStr), // 8B - CharacterLiteral(char), // 4B - Underscore, // 0B - Tag { - whole_var: Variable, // 4B - ext_var: Variable, // 4B - tag_name: PoolStr, // 8B - arguments: PoolVec<(Variable, PatternId)>, // 8B - }, - RecordDestructure { - whole_var: Variable, // 4B - ext_var: Variable, // 4B - destructs: PoolVec, // 8B - }, - - // Runtime Exceptions - // TODO: figure out how to better handle regions - // to keep this member under 32. With 2 Regions - // it ends up at size 40 - Shadowed { - shadowed_ident: PoolStr, - // definition: Region, - // shadowed_at: Region, - }, - - /// Example: (5 = 1 + 2) is an unsupported pattern in an assignment; Int patterns aren't allowed in assignments! - UnsupportedPattern(Region), - // parse error patterns - MalformedPattern(MalformedPatternProblem, Region), -} - -impl ShallowClone for Pattern2 { - fn shallow_clone(&self) -> Self { - todo!() - } -} - -#[derive(Debug)] -pub struct PatternState2<'a> { - pub headers: BumpMap, - pub vars: BumpVec<'a, Variable>, - pub constraints: BumpVec<'a, Constraint<'a>>, -} - -#[derive(Debug)] -pub struct RecordDestruct { - pub var: Variable, // 4B - pub label: PoolStr, // 8B - pub symbol: Symbol, // 8B - pub typ: NodeId, // 4B -} - -#[derive(Clone, Debug)] -pub enum DestructType { - Required, - Optional(Variable, ExprId), // 4B + 4B - Guard(Variable, PatternId), // 4B + 4B -} - -pub fn as_pattern_id<'a>( - env: &mut Env<'a>, - scope: &mut Scope, - pattern_id: PatternId, - pattern_type: PatternType, - pattern: &roc_parse::ast::Pattern<'a>, - region: Region, -) -> Output { - let (output, can_pattern) = to_pattern2(env, scope, pattern_type, pattern, region); - - env.pool[pattern_id] = can_pattern; - env.set_region(pattern_id, region); - - output -} - -pub fn to_pattern_id<'a>( - env: &mut Env<'a>, - scope: &mut Scope, - pattern_type: PatternType, - pattern: &roc_parse::ast::Pattern<'a>, - region: Region, -) -> (Output, PatternId) { - let (output, can_pattern) = to_pattern2(env, scope, pattern_type, pattern, region); - - let pattern_id = env.pool.add(can_pattern); - env.set_region(pattern_id, region); - - (output, pattern_id) -} - -pub fn to_pattern2<'a>( - env: &mut Env<'a>, - scope: &mut Scope, - pattern_type: PatternType, - pattern: &roc_parse::ast::Pattern<'a>, - region: Region, -) -> (Output, Pattern2) { - use roc_parse::ast::Pattern::*; - use PatternType::*; - - let mut output = Output::default(); - let can_pattern = match pattern { - Identifier(name) => match scope.introduce( - (*name).into(), - &env.exposed_ident_ids, - &mut env.ident_ids, - region, - ) { - Ok(symbol) => { - output.references.bound_symbols.insert(symbol); - - Pattern2::Identifier(symbol) - } - Err((original_region, shadow)) => { - env.problem(Problem::RuntimeError(RuntimeError::Shadowing { - original_region, - shadow: shadow.clone(), - kind: ShadowKind::Variable, - })); - - let name: &str = shadow.value.as_ref(); - - Pattern2::Shadowed { - shadowed_ident: PoolStr::new(name, env.pool), - } - } - }, - - QualifiedIdentifier { .. } => { - let problem = MalformedPatternProblem::QualifiedIdentifier; - malformed_pattern(env, problem, region) - } - - Underscore(_) => match pattern_type { - WhenBranch | FunctionArg => Pattern2::Underscore, - TopLevelDef | DefExpr => underscore_in_def(env, region), - }, - - FloatLiteral(ref string) => match pattern_type { - WhenBranch => match finish_parsing_float(string) { - Err(_error) => { - let problem = MalformedPatternProblem::MalformedFloat; - malformed_pattern(env, problem, region) - } - Ok((_, float, _bound)) => Pattern2::FloatLiteral(FloatVal::F64(float)), - }, - ptype => unsupported_pattern(env, ptype, region), - }, - - NumLiteral(string) => match pattern_type { - WhenBranch => match finish_parsing_num(string) { - Err(_error) => { - let problem = MalformedPatternProblem::MalformedInt; - malformed_pattern(env, problem, region) - } - Ok((_, ParsedNumResult::UnknownNum(int, _bound))) => { - Pattern2::NumLiteral( - env.var_store.fresh(), - match int { - IntValue::U128(_) => todo!(), - IntValue::I128(n) => i128::from_ne_bytes(n) as i64, // FIXME - }, - ) - } - Ok((_, ParsedNumResult::Int(int, _bound))) => { - Pattern2::IntLiteral(IntVal::I64(match int { - IntValue::U128(_) => todo!(), - IntValue::I128(n) => i128::from_ne_bytes(n) as i64, // FIXME - })) - } - Ok((_, ParsedNumResult::Float(int, _bound))) => { - Pattern2::FloatLiteral(FloatVal::F64(int)) - } - }, - ptype => unsupported_pattern(env, ptype, region), - }, - - NonBase10Literal { - string, - base, - is_negative, - } => match pattern_type { - WhenBranch => match finish_parsing_base(string, *base, *is_negative) { - Err(_error) => { - let problem = MalformedPatternProblem::MalformedBase(*base); - malformed_pattern(env, problem, region) - } - Ok((int, _bound)) => { - let int = match int { - IntValue::U128(_) => todo!(), - IntValue::I128(n) => i128::from_ne_bytes(n) as i64, // FIXME - }; - if *is_negative { - Pattern2::IntLiteral(IntVal::I64(-int)) - } else { - Pattern2::IntLiteral(IntVal::I64(int)) - } - } - }, - ptype => unsupported_pattern(env, ptype, region), - }, - - StrLiteral(literal) => match pattern_type { - WhenBranch => flatten_str_literal(env.pool, literal), - ptype => unsupported_pattern(env, ptype, region), - }, - - SingleQuote(string) => match pattern_type { - WhenBranch => { - let mut it = string.chars().peekable(); - if let Some(char) = it.next() { - if it.peek().is_none() { - Pattern2::CharacterLiteral(char) - } else { - // multiple chars is found - let problem = MalformedPatternProblem::MultipleCharsInSingleQuote; - malformed_pattern(env, problem, region) - } - } else { - // no characters found - let problem = MalformedPatternProblem::EmptySingleQuote; - malformed_pattern(env, problem, region) - } - } - ptype => unsupported_pattern(env, ptype, region), - }, - - Tag(name) => { - // Canonicalize the tag's name. - Pattern2::Tag { - whole_var: env.var_store.fresh(), - ext_var: env.var_store.fresh(), - tag_name: PoolStr::new(name, env.pool), - arguments: PoolVec::empty(env.pool), - } - } - - OpaqueRef(..) => todo_opaques!(), - - Apply(tag, patterns) => { - let can_patterns = PoolVec::with_capacity(patterns.len() as u32, env.pool); - for (loc_pattern, node_id) in (*patterns).iter().zip(can_patterns.iter_node_ids()) { - let (new_output, can_pattern) = to_pattern2( - env, - scope, - pattern_type, - &loc_pattern.value, - loc_pattern.region, - ); - - output.union(new_output); - - let can_pattern_id = env.pool.add(can_pattern); - - env.pool[node_id] = (env.var_store.fresh(), can_pattern_id); - } - - match tag.value { - Tag(name) => Pattern2::Tag { - whole_var: env.var_store.fresh(), - ext_var: env.var_store.fresh(), - tag_name: PoolStr::new(name, env.pool), - arguments: can_patterns, - }, - _ => unreachable!("Other patterns cannot be applied"), - } - } - - RecordDestructure(patterns) => { - let ext_var = env.var_store.fresh(); - let whole_var = env.var_store.fresh(); - let destructs = PoolVec::with_capacity(patterns.len() as u32, env.pool); - let opt_erroneous = None; - - for (node_id, loc_pattern) in destructs.iter_node_ids().zip((*patterns).iter()) { - match loc_pattern.value { - Identifier(label) => { - match scope.introduce( - label.into(), - &env.exposed_ident_ids, - &mut env.ident_ids, - region, - ) { - Ok(symbol) => { - output.references.bound_symbols.insert(symbol); - - let destruct = RecordDestruct { - var: env.var_store.fresh(), - label: PoolStr::new(label, env.pool), - symbol, - typ: env.pool.add(DestructType::Required), - }; - - env.pool[node_id] = destruct; - env.set_region(node_id, loc_pattern.region); - } - Err((original_region, shadow)) => { - env.problem(Problem::RuntimeError(RuntimeError::Shadowing { - original_region, - shadow: shadow.clone(), - kind: ShadowKind::Variable, - })); - - // let shadowed = Pattern2::Shadowed { - // definition: original_region, - // shadowed_at: loc_pattern.region, - // shadowed_ident: shadow.value, - // }; - - // No matter what the other patterns - // are, we're definitely shadowed and will - // get a runtime exception as soon as we - // encounter the first bad pattern. - // opt_erroneous = Some(); - // env.pool[node_id] = sha; - // env.set_region(node_id, loc_pattern.region); - todo!("we must both report/store the problem, but also not lose any information") - } - }; - } - - RequiredField(label, loc_guard) => { - // a guard does not introduce the label into scope! - let symbol = scope.ignore(label.into(), &mut env.ident_ids); - let (new_output, can_guard) = to_pattern_id( - env, - scope, - pattern_type, - &loc_guard.value, - loc_guard.region, - ); - - let destruct = RecordDestruct { - var: env.var_store.fresh(), - label: PoolStr::new(label, env.pool), - symbol, - typ: env - .pool - .add(DestructType::Guard(env.var_store.fresh(), can_guard)), - }; - - output.union(new_output); - - env.pool[node_id] = destruct; - env.set_region(node_id, loc_pattern.region); - } - OptionalField(label, loc_default) => { - // an optional DOES introduce the label into scope! - match scope.introduce( - label.into(), - &env.exposed_ident_ids, - &mut env.ident_ids, - region, - ) { - Ok(symbol) => { - let (can_default, expr_output) = - to_expr_id(env, scope, &loc_default.value, loc_default.region); - - // an optional field binds the symbol! - output.references.bound_symbols.insert(symbol); - - output.union(expr_output); - - let destruct = RecordDestruct { - var: env.var_store.fresh(), - label: PoolStr::new(label, env.pool), - symbol, - typ: env.pool.add(DestructType::Optional( - env.var_store.fresh(), - can_default, - )), - }; - - env.pool[node_id] = destruct; - env.set_region(node_id, loc_pattern.region); - } - Err((original_region, shadow)) => { - env.problem(Problem::RuntimeError(RuntimeError::Shadowing { - original_region, - shadow: shadow.clone(), - kind: ShadowKind::Variable, - })); - - // No matter what the other patterns - // are, we're definitely shadowed and will - // get a runtime exception as soon as we - // encounter the first bad pattern. - // opt_erroneous = Some(Pattern::Shadowed(original_region, shadow)); - todo!("must report problem but also not loose any information") - } - }; - } - _ => unreachable!("Any other pattern should have given a parse error"), - } - } - - // If we encountered an erroneous pattern (e.g. one with shadowing), - // use the resulting RuntimeError. Otherwise, return a successful record destructure. - opt_erroneous.unwrap_or(Pattern2::RecordDestructure { - whole_var, - ext_var, - destructs, - }) - } - - RequiredField(_name, _loc_pattern) => { - unreachable!("should have been handled in RecordDestructure"); - } - OptionalField(_name, _loc_pattern) => { - unreachable!("should have been handled in RecordDestructure"); - } - - Malformed(_str) => { - let problem = MalformedPatternProblem::Unknown; - malformed_pattern(env, problem, region) - } - - MalformedIdent(_str, bad_ident) => { - let problem = MalformedPatternProblem::BadIdent(*bad_ident); - malformed_pattern(env, problem, region) - } - - SpaceBefore(sub_pattern, _) | SpaceAfter(sub_pattern, _) => { - return to_pattern2(env, scope, pattern_type, sub_pattern, region) - } - }; - - (output, can_pattern) -} - -pub fn symbols_from_pattern(pool: &Pool, initial: &Pattern2) -> Vec { - use Pattern2::*; - let mut symbols = Vec::new(); - let mut stack = vec![initial]; - - while let Some(pattern) = stack.pop() { - match pattern { - Identifier(symbol) => { - symbols.push(*symbol); - } - - Tag { arguments, .. } => { - for (_, pat_id) in arguments.iter(pool) { - let pat = pool.get(*pat_id); - stack.push(pat); - } - } - - RecordDestructure { destructs, .. } => { - for destruct in destructs.iter(pool) { - let destruct_type = pool.get(destruct.typ); - - if let DestructType::Guard(_, subpattern_id) = &destruct_type { - let subpattern = pool.get(*subpattern_id); - stack.push(subpattern); - } else { - symbols.push(destruct.symbol); - } - } - } - - NumLiteral(_, _) - | IntLiteral(_) - | FloatLiteral(_) - | StrLiteral(_) - | CharacterLiteral(_) - | Underscore - | MalformedPattern(_, _) - | Shadowed { .. } - | UnsupportedPattern(_) => {} - } - } - - symbols -} - -pub fn get_identifier_string(pattern: &Pattern2, interns: &Interns) -> ASTResult { - match pattern { - Pattern2::Identifier(symbol) => Ok(symbol.as_str(interns).to_string()), - other => UnexpectedPattern2Variant { - required_pattern2: "Identifier".to_string(), - encountered_pattern2: format!("{:?}", other), - } - .fail()?, - } -} - -pub fn symbols_and_variables_from_pattern( - pool: &Pool, - initial: &Pattern2, - initial_var: Variable, -) -> Vec<(Symbol, Variable)> { - use Pattern2::*; - let mut symbols = Vec::new(); - let mut stack = vec![(initial_var, initial)]; - - while let Some((variable, pattern)) = stack.pop() { - match pattern { - Identifier(symbol) => { - symbols.push((*symbol, variable)); - } - - Tag { arguments, .. } => { - for (var, pat_id) in arguments.iter(pool) { - let pat = pool.get(*pat_id); - stack.push((*var, pat)); - } - } - - RecordDestructure { destructs, .. } => { - for destruct in destructs.iter(pool) { - let destruct_type = pool.get(destruct.typ); - - if let DestructType::Guard(_, subpattern_id) = &destruct_type { - let subpattern = pool.get(*subpattern_id); - stack.push((destruct.var, subpattern)); - } else { - symbols.push((destruct.symbol, destruct.var)); - } - } - } - - NumLiteral(_, _) - | IntLiteral(_) - | FloatLiteral(_) - | StrLiteral(_) - | CharacterLiteral(_) - | Underscore - | MalformedPattern(_, _) - | Shadowed { .. } - | UnsupportedPattern(_) => {} - } - } - - symbols -} - -/// When we detect an unsupported pattern type (e.g. 5 = 1 + 2 is unsupported because you can't -/// assign to Int patterns), report it to Env and return an UnsupportedPattern runtime error pattern. -fn unsupported_pattern<'a>( - env: &mut Env<'a>, - pattern_type: PatternType, - region: Region, -) -> Pattern2 { - use roc_problem::can::BadPattern; - env.problem(Problem::UnsupportedPattern( - BadPattern::Unsupported(pattern_type), - region, - )); - - Pattern2::UnsupportedPattern(region) -} - -fn underscore_in_def<'a>(env: &mut Env<'a>, region: Region) -> Pattern2 { - use roc_problem::can::BadPattern; - env.problem(Problem::UnsupportedPattern( - BadPattern::UnderscoreInDef, - region, - )); - - Pattern2::UnsupportedPattern(region) -} - -pub(crate) fn flatten_str_literal(pool: &mut Pool, literal: &StrLiteral<'_>) -> Pattern2 { - use roc_parse::ast::StrLiteral::*; - - match literal { - PlainLine(str_slice) => Pattern2::StrLiteral(PoolStr::new(str_slice, pool)), - Line(segments) => flatten_str_lines(pool, &[segments]), - Block(lines) => flatten_str_lines(pool, lines), - } -} - -pub(crate) fn flatten_str_lines(pool: &mut Pool, lines: &[&[StrSegment<'_>]]) -> Pattern2 { - use StrSegment::*; - - let mut buf = String::new(); - - for line in lines { - for segment in line.iter() { - match segment { - Plaintext(string) => { - buf.push_str(string); - } - Unicode(loc_digits) => { - todo!("parse unicode digits {:?}", loc_digits); - } - Interpolated(loc_expr) => { - return Pattern2::UnsupportedPattern(loc_expr.region); - } - EscapedChar(escaped) => buf.push(unescape_char(escaped)), - } - } - } - - Pattern2::StrLiteral(PoolStr::new(&buf, pool)) -} - -/// When we detect a malformed pattern like `3.X` or `0b5`, -/// report it to Env and return an UnsupportedPattern runtime error pattern. -fn malformed_pattern<'a>( - env: &mut Env<'a>, - problem: MalformedPatternProblem, - region: Region, -) -> Pattern2 { - env.problem(Problem::RuntimeError(RuntimeError::MalformedPattern( - problem, region, - ))); - - Pattern2::MalformedPattern(problem, region) -} diff --git a/ast/src/lang/core/str.rs b/ast/src/lang/core/str.rs deleted file mode 100644 index dee65a3436..0000000000 --- a/ast/src/lang/core/str.rs +++ /dev/null @@ -1,252 +0,0 @@ -use roc_error_macros::internal_error; -use roc_module::{called_via::CalledVia, symbol::Symbol}; -use roc_parse::ast::StrLiteral; - -use crate::{ - ast_error::{ASTResult, UnexpectedASTNode}, - lang::{ - core::expr::{ - expr2::{ArrString, ARR_STRING_CAPACITY}, - expr_to_expr2::expr_to_expr2, - }, - env::Env, - scope::Scope, - }, - mem_pool::{pool::Pool, pool_str::PoolStr, pool_vec::PoolVec}, -}; - -use super::expr::{ - expr2::{Expr2, ExprId}, - output::Output, -}; - -pub(crate) fn flatten_str_literal<'a>( - env: &mut Env<'a>, - scope: &mut Scope, - literal: &StrLiteral<'a>, -) -> (Expr2, Output) { - use roc_parse::ast::StrLiteral::*; - - match literal { - PlainLine(str_slice) => { - // TODO use smallstr - let expr = Expr2::Str(PoolStr::new(str_slice, env.pool)); - - (expr, Output::default()) - } - Line(segments) => flatten_str_lines(env, scope, &[segments]), - Block(lines) => flatten_str_lines(env, scope, lines), - } -} - -enum StrSegment { - Interpolation(Expr2), - Plaintext(PoolStr), -} - -fn flatten_str_lines<'a>( - env: &mut Env<'a>, - scope: &mut Scope, - lines: &[&[roc_parse::ast::StrSegment<'a>]], -) -> (Expr2, Output) { - use roc_parse::ast::StrSegment::*; - - let mut buf = String::new(); - let mut segments = Vec::new(); - let mut output = Output::default(); - - for line in lines { - for segment in line.iter() { - match segment { - Plaintext(string) => { - buf.push_str(string); - } - Unicode(loc_hex_digits) => match u32::from_str_radix(loc_hex_digits.value, 16) { - Ok(code_pt) => match std::char::from_u32(code_pt) { - Some(ch) => { - buf.push(ch); - } - None => { - // env.problem(Problem::InvalidUnicodeCodePt(loc_hex_digits.region)); - // - // return ( - // Expr::RuntimeError(RuntimeError::InvalidUnicodeCodePt( - // loc_hex_digits.region, - // )), - // output, - // ); - todo!() - } - }, - Err(_) => { - // env.problem(Problem::InvalidHexadecimal(loc_hex_digits.region)); - // - // return ( - // Expr::RuntimeError(RuntimeError::InvalidHexadecimal( - // loc_hex_digits.region, - // )), - // output, - // ); - todo!() - } - }, - Interpolated(loc_expr) => { - if roc_can::expr::is_valid_interpolation(loc_expr.value) { - // Interpolations desugar to Str.concat calls - output.references.calls.insert(Symbol::STR_CONCAT); - - if !buf.is_empty() { - segments.push(StrSegment::Plaintext(PoolStr::new(&buf, env.pool))); - - buf = String::new(); - } - - let (loc_expr, new_output) = - expr_to_expr2(env, scope, loc_expr.value, loc_expr.region); - - output.union(new_output); - - segments.push(StrSegment::Interpolation(loc_expr)); - } else { - // env.problem(Problem::InvalidInterpolation(loc_expr.region)); - // - // return ( - // Expr::RuntimeError(RuntimeError::InvalidInterpolation(loc_expr.region)), - // output, - // ); - todo!() - } - } - EscapedChar(escaped) => buf.push(roc_can::expr::unescape_char(escaped)), - } - } - } - - if !buf.is_empty() { - segments.push(StrSegment::Plaintext(PoolStr::new(&buf, env.pool))); - } - - (desugar_str_segments(env, segments), output) -} - -/// Resolve string interpolations by desugaring a sequence of StrSegments -/// into nested calls to Str.concat -fn desugar_str_segments(env: &mut Env, segments: Vec) -> Expr2 { - use StrSegment::*; - - let pool = &mut env.pool; - let var_store = &mut env.var_store; - - let mut iter = segments.into_iter().rev(); - let mut expr = match iter.next() { - Some(Plaintext(pool_str)) => Expr2::Str(pool_str), - Some(Interpolation(expr_id)) => expr_id, - None => { - // No segments? Empty string! - - let pool_str = PoolStr::new("", pool); - Expr2::Str(pool_str) - } - }; - - for seg in iter { - let new_expr = match seg { - Plaintext(string) => Expr2::Str(string), - Interpolation(expr_id) => expr_id, - }; - - let concat_expr_id = pool.add(Expr2::Var(Symbol::STR_CONCAT)); - - let args = vec![ - (var_store.fresh(), pool.add(new_expr)), - (var_store.fresh(), pool.add(expr)), - ]; - let args = PoolVec::new(args.into_iter(), pool); - - let new_call = Expr2::Call { - args, - expr_id: concat_expr_id, - expr_var: var_store.fresh(), - fn_var: var_store.fresh(), - closure_var: var_store.fresh(), - called_via: CalledVia::Space, - }; - - expr = new_call - } - - expr -} - -pub fn update_str_expr( - node_id: ExprId, - new_char: char, - insert_index: usize, - pool: &mut Pool, -) -> ASTResult<()> { - let str_expr = pool.get_mut(node_id); - - enum Either { - MyArrString(ArrString), - OldPoolStr(PoolStr), - NewPoolStr(PoolStr), - } - - let insert_either = match str_expr { - Expr2::SmallStr(arr_string) => { - if arr_string.len() < arr_string.capacity() { - let mut new_bytes: [u8; ARR_STRING_CAPACITY] = Default::default(); - let arr_bytes = arr_string.as_str().as_bytes(); - new_bytes[..insert_index].copy_from_slice(&arr_bytes[..insert_index]); - new_bytes[insert_index] = new_char as u8; - new_bytes[insert_index + 1..arr_bytes.len() + 1] - .copy_from_slice(&arr_bytes[insert_index..]); - - let new_str = unsafe { - // all old characters have been checked on file load, new_char has been checked inside editor/src/editor/mvc/ed_update.rs - std::str::from_utf8_unchecked(&new_bytes[..arr_bytes.len() + 1]) - }; - - let new_arr_string = match ArrString::from(new_str) { - Ok(arr_string) => arr_string, - Err(e) => { - internal_error!("Failed to build valid ArrayString from str: {:?}", e) - } - }; - - Either::MyArrString(new_arr_string) - } else { - let mut new_string = arr_string.as_str().to_owned(); - - new_string.insert(insert_index, new_char); - - let new_pool_str = PoolStr::new(&new_string, pool); - Either::NewPoolStr(new_pool_str) - } - } - Expr2::Str(old_pool_str) => Either::OldPoolStr(*old_pool_str), - other => UnexpectedASTNode { - required_node_type: "SmallStr or Str", - encountered_node_type: format!("{:?}", other), - } - .fail()?, - }; - - match insert_either { - Either::MyArrString(arr_string) => { - pool.set(node_id, Expr2::SmallStr(arr_string)); - } - Either::OldPoolStr(old_pool_str) => { - let mut new_string = old_pool_str.as_str(pool).to_owned(); - - new_string.insert(insert_index, new_char); - - let new_pool_str = PoolStr::new(&new_string, pool); - - pool.set(node_id, Expr2::Str(new_pool_str)) - } - Either::NewPoolStr(new_pool_str) => pool.set(node_id, Expr2::Str(new_pool_str)), - } - - Ok(()) -} diff --git a/ast/src/lang/core/types.rs b/ast/src/lang/core/types.rs deleted file mode 100644 index b118c2e2df..0000000000 --- a/ast/src/lang/core/types.rs +++ /dev/null @@ -1,871 +0,0 @@ -#![allow(clippy::all)] -#![allow(dead_code)] -#![allow(unused_imports)] -// use roc_can::expr::Output; -use roc_collections::all::{MutMap, MutSet}; -use roc_error_macros::todo_abilities; -use roc_module::ident::{Ident, Lowercase, TagName, Uppercase}; -use roc_module::symbol::Symbol; -use roc_region::all::{Loc, Region}; -use roc_types::types::{Problem, RecordField}; -use roc_types::{subs::Variable, types::ErrorType}; - -use crate::lang::env::Env; -use crate::lang::scope::Scope; -use crate::mem_pool::pool::{NodeId, Pool}; -use crate::mem_pool::pool_str::PoolStr; -use crate::mem_pool::pool_vec::PoolVec; -use crate::mem_pool::shallow_clone::ShallowClone; - -pub type TypeId = NodeId; - -const TYPE2_SIZE: () = assert!(std::mem::size_of::() == 3 * 8 + 4); - -#[derive(Debug)] -pub enum Type2 { - Variable(Variable), // 4B - - Alias(Symbol, PoolVec, TypeId), // 24B = 8B + 8B + 4B + pad - Opaque(Symbol, PoolVec, TypeId), // 24B = 8B + 8B + 4B + pad - AsAlias(Symbol, PoolVec<(PoolStr, TypeId)>, TypeId), // 24B = 8B + 8B + 4B + pad - - // 24B - HostExposedAlias { - name: Symbol, // 8B - arguments: PoolVec<(PoolStr, TypeId)>, // 8B - actual_var: Variable, // 4B - actual: TypeId, // 4B - }, - EmptyTagUnion, - TagUnion(PoolVec<(TagName, PoolVec)>, TypeId), // 12B = 8B + 4B - RecursiveTagUnion(Variable, PoolVec<(TagName, PoolVec)>, TypeId), // 16B = 4B + 8B + 4B - - EmptyRec, - Record(PoolVec<(PoolStr, RecordField)>, TypeId), // 12B = 8B + 4B - - Function(PoolVec, TypeId, TypeId), // 16B = 8B + 4B + 4B - Apply(Symbol, PoolVec), // 16B = 8B + 8B - - Erroneous(Problem2), // 24B -} - -#[derive(Debug)] -pub enum Problem2 { - CanonicalizationProblem, - CircularType(Symbol, NodeId), // 12B = 8B + 4B - CyclicAlias(Symbol, PoolVec), // 20B = 8B + 12B - UnrecognizedIdent(PoolStr), // 8B - Shadowed(Loc), - BadTypeArguments { - symbol: Symbol, // 8B - type_got: u8, // 1B - alias_needs: u8, // 1B - }, - InvalidModule, - SolvedTypeError, -} - -impl ShallowClone for Type2 { - fn shallow_clone(&self) -> Self { - match self { - Self::Variable(var) => Self::Variable(*var), - Self::Alias(symbol, args, alias_type_id) => { - Self::Alias(*symbol, args.shallow_clone(), alias_type_id.clone()) - } - Self::Opaque(symbol, args, alias_type_id) => { - Self::Opaque(*symbol, args.shallow_clone(), alias_type_id.clone()) - } - Self::Record(fields, ext_id) => Self::Record(fields.shallow_clone(), ext_id.clone()), - Self::Function(args, closure_type_id, ret_type_id) => Self::Function( - args.shallow_clone(), - closure_type_id.clone(), - ret_type_id.clone(), - ), - rest => todo!("{:?}", rest), - } - } -} - -impl Type2 { - fn substitute(_pool: &mut Pool, _subs: &MutMap, _type_id: TypeId) { - todo!() - } - - pub fn variables(&self, pool: &mut Pool) -> MutSet { - use Type2::*; - - let mut stack = vec![self]; - let mut result = MutSet::default(); - - while let Some(this) = stack.pop() { - match this { - Variable(v) => { - result.insert(*v); - } - Alias(_, _, actual) | AsAlias(_, _, actual) | Opaque(_, _, actual) => { - stack.push(pool.get(*actual)); - } - HostExposedAlias { - actual_var, actual, .. - } => { - result.insert(*actual_var); - stack.push(pool.get(*actual)); - } - EmptyTagUnion | EmptyRec | Erroneous(_) => {} - TagUnion(tags, ext) => { - for (_, args) in tags.iter(pool) { - stack.extend(args.iter(pool)); - } - stack.push(pool.get(*ext)); - } - RecursiveTagUnion(rec, tags, ext) => { - for (_, args) in tags.iter(pool) { - stack.extend(args.iter(pool)); - } - stack.push(pool.get(*ext)); - result.insert(*rec); - } - Record(fields, ext) => { - for (_, field) in fields.iter(pool) { - stack.push(pool.get(*field.as_inner())); - } - stack.push(pool.get(*ext)); - } - Function(args, closure, result) => { - stack.extend(args.iter(pool)); - stack.push(pool.get(*closure)); - stack.push(pool.get(*result)); - } - Apply(_, args) => { - stack.extend(args.iter(pool)); - } - } - } - - result - } - - pub fn contains_symbol(&self, _pool: &mut Pool, _needle: Symbol) -> bool { - todo!() - } - - pub fn substitute_alias(&self, _pool: &mut Pool, _needle: Symbol, _actual: Self) { - todo!() - } -} - -impl NodeId { - pub fn variables(&self, _pool: &mut Pool) -> MutSet { - todo!() - } -} - -/// A temporary data structure to return a bunch of values to Def construction -pub enum Signature { - FunctionWithAliases { - annotation: Type2, - arguments: PoolVec, - closure_type_id: TypeId, - return_type_id: TypeId, - }, - Function { - arguments: PoolVec, - closure_type_id: TypeId, - return_type_id: TypeId, - }, - Value { - annotation: Type2, - }, -} - -pub enum Annotation2 { - Annotation { - named_rigids: MutMap, - unnamed_rigids: MutSet, - symbols: MutSet, - signature: Signature, - }, - Erroneous(roc_types::types::Problem), -} - -pub fn to_annotation2<'a>( - env: &mut Env, - scope: &mut Scope, - annotation: &'a roc_parse::ast::TypeAnnotation<'a>, - region: Region, -) -> Annotation2 { - let mut references = References::default(); - - let annotation = to_type2(env, scope, &mut references, annotation, region); - - // we dealias until we hit a non-alias, then we either hit a function type (and produce a - // function annotation) or anything else (and produce a value annotation) - match annotation { - Type2::Function(arguments, closure_type_id, return_type_id) => { - let References { - named, - unnamed, - symbols, - .. - } = references; - - let signature = Signature::Function { - arguments, - closure_type_id, - return_type_id, - }; - - Annotation2::Annotation { - named_rigids: named, - unnamed_rigids: unnamed, - symbols, - signature, - } - } - Type2::Alias(_, _, _) => { - // most of the time, the annotation is not an alias, so this case is comparatively - // less efficient - shallow_dealias(env, references, annotation) - } - _ => { - let References { - named, - unnamed, - symbols, - .. - } = references; - - let signature = Signature::Value { annotation }; - - Annotation2::Annotation { - named_rigids: named, - unnamed_rigids: unnamed, - symbols, - signature, - } - } - } -} - -fn shallow_dealias<'a>(env: &mut Env, references: References, annotation: Type2) -> Annotation2 { - let References { - named, - unnamed, - symbols, - .. - } = references; - let mut inner = &annotation; - - loop { - match inner { - Type2::Alias(_, _, actual) => { - inner = env.pool.get(*actual); - } - Type2::Function(arguments, closure_type_id, return_type_id) => { - let signature = Signature::FunctionWithAliases { - arguments: arguments.shallow_clone(), - closure_type_id: *closure_type_id, - return_type_id: *return_type_id, - annotation, - }; - - return Annotation2::Annotation { - named_rigids: named, - unnamed_rigids: unnamed, - symbols, - signature, - }; - } - _ => { - let signature = Signature::Value { annotation }; - - return Annotation2::Annotation { - named_rigids: named, - unnamed_rigids: unnamed, - symbols, - signature, - }; - } - } - } -} - -#[derive(Default)] -pub struct References { - named: MutMap, - unnamed: MutSet, - hidden: MutSet, - symbols: MutSet, -} - -pub fn to_type_id<'a>( - env: &mut Env, - scope: &mut Scope, - rigids: &mut References, - annotation: &roc_parse::ast::TypeAnnotation<'a>, - region: Region, -) -> TypeId { - let type2 = to_type2(env, scope, rigids, annotation, region); - - env.add(type2, region) -} - -pub fn as_type_id<'a>( - env: &mut Env, - scope: &mut Scope, - rigids: &mut References, - type_id: TypeId, - annotation: &roc_parse::ast::TypeAnnotation<'a>, - region: Region, -) { - let type2 = to_type2(env, scope, rigids, annotation, region); - - env.pool[type_id] = type2; - env.set_region(type_id, region); -} - -pub fn to_type2<'a>( - env: &mut Env, - scope: &mut Scope, - references: &mut References, - annotation: &roc_parse::ast::TypeAnnotation<'a>, - region: Region, -) -> Type2 { - use roc_parse::ast::Pattern; - use roc_parse::ast::TypeAnnotation::*; - use roc_parse::ast::TypeHeader; - - match annotation { - Apply(module_name, ident, targs) => { - match to_type_apply(env, scope, references, module_name, ident, targs, region) { - TypeApply::Apply(symbol, args) => { - references.symbols.insert(symbol); - Type2::Apply(symbol, args) - } - TypeApply::Alias(symbol, args, actual) => { - references.symbols.insert(symbol); - Type2::Alias(symbol, args, actual) - } - TypeApply::Erroneous(_problem) => { - // Type2::Erroneous(problem) - todo!() - } - } - } - Function(argument_types, return_type) => { - let arguments = PoolVec::with_capacity(argument_types.len() as u32, env.pool); - - for (type_id, loc_arg) in arguments.iter_node_ids().zip(argument_types.iter()) { - as_type_id( - env, - scope, - references, - type_id, - &loc_arg.value, - loc_arg.region, - ); - } - - let return_type_id = to_type_id( - env, - scope, - references, - &return_type.value, - return_type.region, - ); - - let closure_type = Type2::Variable(env.var_store.fresh()); - let closure_type_id = env.pool.add(closure_type); - - Type2::Function(arguments, closure_type_id, return_type_id) - } - BoundVariable(v) => { - // A rigid type variable. The parser should have already ensured that the name is indeed a lowercase. - let v = Lowercase::from(*v); - match references.named.get(&v) { - Some(var) => Type2::Variable(*var), - None => { - let var = env.var_store.fresh(); - - references.named.insert(v, var); - - Type2::Variable(var) - } - } - } - Inferred => { - let var = env.var_store.fresh(); - - Type2::Variable(var) - } - Wildcard | Malformed(_) => { - let var = env.var_store.fresh(); - - references.unnamed.insert(var); - - Type2::Variable(var) - } - Record { fields, ext, .. } => { - let field_types_map = - can_assigned_fields(env, scope, references, &fields.items, region); - - let field_types = PoolVec::with_capacity(field_types_map.len() as u32, env.pool); - - for (node_id, (label, field)) in field_types.iter_node_ids().zip(field_types_map) { - let poolstr = PoolStr::new(label.as_str(), env.pool); - - let rec_field = match field { - RecordField::Optional(_) => { - let field_id = env.pool.add(field.into_inner()); - RecordField::Optional(field_id) - } - RecordField::Demanded(_) => { - let field_id = env.pool.add(field.into_inner()); - RecordField::Demanded(field_id) - } - RecordField::Required(_) => { - let field_id = env.pool.add(field.into_inner()); - RecordField::Required(field_id) - } - }; - env.pool[node_id] = (poolstr, rec_field); - } - - let ext_type = match ext { - Some(loc_ann) => to_type_id(env, scope, references, &loc_ann.value, region), - None => env.add(Type2::EmptyRec, region), - }; - - Type2::Record(field_types, ext_type) - } - TagUnion { tags, ext, .. } => { - let tag_types_vec = can_tags(env, scope, references, tags.items, region); - - let tag_types = PoolVec::with_capacity(tag_types_vec.len() as u32, env.pool); - - for (node_id, (tag_name, field)) in tag_types.iter_node_ids().zip(tag_types_vec) { - env.pool[node_id] = (tag_name, field); - } - - let ext_type = match ext { - Some(loc_ann) => to_type_id(env, scope, references, &loc_ann.value, region), - None => env.add(Type2::EmptyTagUnion, region), - }; - - Type2::TagUnion(tag_types, ext_type) - } - As( - loc_inner, - _spaces, - TypeHeader { - name, - vars: loc_vars, - }, - ) => { - // e.g. `{ x : Int, y : Int } as Point` - let symbol = match scope.introduce( - name.value.into(), - &env.exposed_ident_ids, - &mut env.ident_ids, - region, - ) { - Ok(symbol) => symbol, - - Err((_original_region, _shadow)) => { - // let problem = Problem2::Shadowed(original_region, shadow.clone()); - - // env.problem(roc_problem::can::Problem::ShadowingInAnnotation { - // original_region, - // shadow, - // }); - - // return Type2::Erroneous(problem); - todo!(); - } - }; - - let inner_type = to_type2(env, scope, references, &loc_inner.value, region); - let vars = PoolVec::with_capacity(loc_vars.len() as u32, env.pool); - - let lowercase_vars = PoolVec::with_capacity(loc_vars.len() as u32, env.pool); - - for ((loc_var, named_id), var_id) in loc_vars - .iter() - .zip(lowercase_vars.iter_node_ids()) - .zip(vars.iter_node_ids()) - { - let var = match loc_var.value { - Pattern::Identifier(name) if name.chars().next().unwrap().is_lowercase() => { - name - } - _ => unreachable!("I thought this was validated during parsing"), - }; - let var_name = Lowercase::from(var); - - if let Some(var) = references.named.get(&var_name) { - let poolstr = PoolStr::new(var_name.as_str(), env.pool); - - let type_id = env.pool.add(Type2::Variable(*var)); - env.pool[var_id] = (poolstr.shallow_clone(), type_id); - - env.pool[named_id] = (poolstr, *var); - env.set_region(named_id, loc_var.region); - } else { - let var = env.var_store.fresh(); - - references.named.insert(var_name.clone(), var); - let poolstr = PoolStr::new(var_name.as_str(), env.pool); - - let type_id = env.pool.add(Type2::Variable(var)); - env.pool[var_id] = (poolstr.shallow_clone(), type_id); - - env.pool[named_id] = (poolstr, var); - env.set_region(named_id, loc_var.region); - } - } - - let alias_actual = inner_type; - // TODO instantiate recursive tag union - // let alias_actual = if let Type2::TagUnion(tags, ext) = inner_type { - // let rec_var = env.var_store.fresh(); - // - // let mut new_tags = Vec::with_capacity(tags.len()); - // for (tag_name, args) in tags { - // let mut new_args = Vec::with_capacity(args.len()); - // for arg in args { - // let mut new_arg = arg.clone(); - // new_arg.substitute_alias(symbol, &Type2::Variable(rec_var)); - // new_args.push(new_arg); - // } - // new_tags.push((tag_name.clone(), new_args)); - // } - // Type2::RecursiveTagUnion(rec_var, new_tags, ext) - // } else { - // inner_type - // }; - - let mut hidden_variables = MutSet::default(); - hidden_variables.extend(alias_actual.variables(env.pool)); - - for (_, var) in lowercase_vars.iter(env.pool) { - hidden_variables.remove(var); - } - - let alias_actual_id = env.pool.add(alias_actual); - scope.add_alias(env.pool, symbol, lowercase_vars, alias_actual_id); - - let alias = scope.lookup_alias(symbol).unwrap(); - // local_aliases.insert(symbol, alias.clone()); - - // TODO host-exposed - // if vars.is_empty() && env.home == symbol.module_id() { - // let actual_var = env.var_store.fresh(); - // rigids.host_exposed.insert(symbol, actual_var); - // Type::HostExposedAlias { - // name: symbol, - // arguments: vars, - // actual: Box::new(alias.typ.clone()), - // actual_var, - // } - // } else { - // Type::Alias(symbol, vars, Box::new(alias.typ.clone())) - // } - Type2::AsAlias(symbol, vars, alias.actual) - } - Where { .. } => todo_abilities!(), - SpaceBefore(nested, _) | SpaceAfter(nested, _) => { - to_type2(env, scope, references, nested, region) - } - } -} - -// TODO trim down these arguments! -#[allow(clippy::too_many_arguments)] -fn can_assigned_fields<'a>( - env: &mut Env, - scope: &mut Scope, - rigids: &mut References, - fields: &&[Loc>>], - region: Region, -) -> MutMap> { - use roc_parse::ast::AssignedField::*; - use roc_types::types::RecordField::*; - - // SendMap doesn't have a `with_capacity` - let mut field_types = MutMap::default(); - - // field names we've seen so far in this record - let mut seen = std::collections::HashMap::with_capacity(fields.len()); - - 'outer: for loc_field in fields.iter() { - let mut field = &loc_field.value; - - // use this inner loop to unwrap the SpaceAfter/SpaceBefore - // when we find the name of this field, break out of the loop - // with that value, so we can check whether the field name is - // a duplicate - let new_name = 'inner: loop { - match field { - RequiredValue(field_name, _, annotation) => { - let field_type = - to_type2(env, scope, rigids, &annotation.value, annotation.region); - - let label = Lowercase::from(field_name.value); - field_types.insert(label.clone(), Required(field_type)); - - break 'inner label; - } - OptionalValue(field_name, _, annotation) => { - let field_type = - to_type2(env, scope, rigids, &annotation.value, annotation.region); - - let label = Lowercase::from(field_name.value); - field_types.insert(label.clone(), Optional(field_type)); - - break 'inner label; - } - LabelOnly(loc_field_name) => { - // Interpret { a, b } as { a : a, b : b } - let field_name = Lowercase::from(loc_field_name.value); - let field_type = { - if let Some(var) = rigids.named.get(&field_name) { - Type2::Variable(*var) - } else { - let field_var = env.var_store.fresh(); - rigids.named.insert(field_name.clone(), field_var); - Type2::Variable(field_var) - } - }; - - field_types.insert(field_name.clone(), Required(field_type)); - - break 'inner field_name; - } - SpaceBefore(nested, _) | SpaceAfter(nested, _) => { - // check the nested field instead - field = nested; - continue 'inner; - } - Malformed(_) => { - // TODO report this? - // completely skip this element, advance to the next tag - continue 'outer; - } - } - }; - - // ensure that the new name is not already in this record: - // note that the right-most tag wins when there are two with the same name - if let Some(replaced_region) = seen.insert(new_name.clone(), loc_field.region) { - env.problem(roc_problem::can::Problem::DuplicateRecordFieldType { - field_name: new_name.into(), - record_region: region, - field_region: loc_field.region, - replaced_region, - }); - } - } - - field_types -} - -fn can_tags<'a>( - env: &mut Env, - scope: &mut Scope, - rigids: &mut References, - tags: &'a [Loc>], - region: Region, -) -> Vec<(TagName, PoolVec)> { - use roc_parse::ast::Tag; - let mut tag_types = Vec::with_capacity(tags.len()); - - // tag names we've seen so far in this tag union - let mut seen = std::collections::HashMap::with_capacity(tags.len()); - - 'outer: for loc_tag in tags.iter() { - let mut tag = &loc_tag.value; - - // use this inner loop to unwrap the SpaceAfter/SpaceBefore - // when we find the name of this tag, break out of the loop - // with that value, so we can check whether the tag name is - // a duplicate - let new_name = 'inner: loop { - match tag { - Tag::Apply { name, args } => { - let arg_types = PoolVec::with_capacity(args.len() as u32, env.pool); - - for (type_id, loc_arg) in arg_types.iter_node_ids().zip(args.iter()) { - as_type_id(env, scope, rigids, type_id, &loc_arg.value, loc_arg.region); - } - - let tag_name = TagName(name.value.into()); - tag_types.push((tag_name.clone(), arg_types)); - - break 'inner tag_name; - } - Tag::SpaceBefore(nested, _) | Tag::SpaceAfter(nested, _) => { - // check the nested tag instead - tag = nested; - continue 'inner; - } - Tag::Malformed(_) => { - // TODO report this? - // completely skip this element, advance to the next tag - continue 'outer; - } - } - }; - - // ensure that the new name is not already in this tag union: - // note that the right-most tag wins when there are two with the same name - if let Some(replaced_region) = seen.insert(new_name.clone(), loc_tag.region) { - env.problem(roc_problem::can::Problem::DuplicateTag { - tag_region: loc_tag.region, - tag_union_region: region, - replaced_region, - tag_name: new_name, - }); - } - } - - tag_types -} - -enum TypeApply { - Apply(Symbol, PoolVec), - Alias(Symbol, PoolVec, TypeId), - Erroneous(roc_types::types::Problem), -} - -#[inline(always)] -fn to_type_apply<'a>( - env: &mut Env, - scope: &mut Scope, - rigids: &mut References, - module_name: &str, - ident: &str, - type_arguments: &[Loc>], - region: Region, -) -> TypeApply { - let symbol = if module_name.is_empty() { - // Since module_name was empty, this is an unqualified type. - // Look it up in scope! - let ident: Ident = (*ident).into(); - - match scope.lookup(&ident, region) { - Ok(symbol) => symbol, - Err(problem) => { - env.problem(roc_problem::can::Problem::RuntimeError(problem)); - - return TypeApply::Erroneous(Problem::UnrecognizedIdent(ident.into())); - } - } - } else { - match env.qualified_lookup(module_name, ident, region) { - Ok(symbol) => symbol, - Err(problem) => { - // Either the module wasn't imported, or - // it was imported but it doesn't expose this ident. - env.problem(roc_problem::can::Problem::RuntimeError(problem)); - - return TypeApply::Erroneous(Problem::UnrecognizedIdent((*ident).into())); - } - } - }; - - let argument_type_ids = PoolVec::with_capacity(type_arguments.len() as u32, env.pool); - - for (type_id, loc_arg) in argument_type_ids.iter_node_ids().zip(type_arguments.iter()) { - as_type_id(env, scope, rigids, type_id, &loc_arg.value, loc_arg.region); - } - - let args = type_arguments; - let opt_alias = scope.lookup_alias(symbol); - match opt_alias { - Some(ref alias) => { - // use a known alias - let actual = alias.actual; - let mut substitutions: MutMap = MutMap::default(); - - if alias.targs.len() != args.len() { - let error = TypeApply::Erroneous(Problem::BadTypeArguments { - symbol, - region, - alias_needs: alias.targs.len() as u8, - type_got: args.len() as u8, - }); - return error; - } - - let arguments = PoolVec::with_capacity(type_arguments.len() as u32, env.pool); - - let it = arguments.iter_node_ids().zip( - argument_type_ids - .iter_node_ids() - .zip(alias.targs.iter_node_ids()), - ); - - for (node_id, (type_id, loc_var_id)) in it { - let loc_var = &env.pool[loc_var_id]; - let name = loc_var.0.shallow_clone(); - let var = loc_var.1; - - env.pool[node_id] = (name, type_id); - - substitutions.insert(var, type_id); - } - - // make sure the recursion variable is freshly instantiated - // have to allocate these outside of the if for lifetime reasons... - let new = env.var_store.fresh(); - let fresh = env.pool.add(Type2::Variable(new)); - if let Type2::RecursiveTagUnion(rvar, ref tags, ext) = &mut env.pool[actual] { - substitutions.insert(*rvar, fresh); - - env.pool[actual] = Type2::RecursiveTagUnion(new, tags.shallow_clone(), *ext); - } - - // make sure hidden variables are freshly instantiated - for var_id in alias.hidden_variables.iter_node_ids() { - let var = env.pool[var_id]; - let fresh = env.pool.add(Type2::Variable(env.var_store.fresh())); - substitutions.insert(var, fresh); - } - - // instantiate variables - Type2::substitute(env.pool, &substitutions, actual); - - let type_arguments = PoolVec::with_capacity(arguments.len() as u32, env.pool); - - for (node_id, type_id) in arguments - .iter_node_ids() - .zip(type_arguments.iter_node_ids()) - { - let typ = env.pool[node_id].1; - env.pool[type_id] = typ; - } - - TypeApply::Alias(symbol, type_arguments, actual) - } - None => TypeApply::Apply(symbol, argument_type_ids), - } -} - -#[derive(Debug)] -pub struct Alias { - pub targs: PoolVec<(PoolStr, Variable)>, - pub actual: TypeId, - - /// hidden type variables, like the closure variable in `a -> b` - pub hidden_variables: PoolVec, -} - -impl ShallowClone for Alias { - fn shallow_clone(&self) -> Self { - Self { - targs: self.targs.shallow_clone(), - hidden_variables: self.hidden_variables.shallow_clone(), - actual: self.actual, - } - } -} diff --git a/ast/src/lang/core/val_def.rs b/ast/src/lang/core/val_def.rs deleted file mode 100644 index b30bfb3ace..0000000000 --- a/ast/src/lang/core/val_def.rs +++ /dev/null @@ -1,101 +0,0 @@ -use crate::{ - lang::{core::expr::expr2_to_string::expr2_to_string, rigids::Rigids}, - mem_pool::{ - pool::{NodeId, Pool}, - shallow_clone::ShallowClone, - }, -}; -use roc_types::subs::Variable; - -use super::{ - expr::expr2::ExprId, - pattern::{Pattern2, PatternId}, - types::TypeId, -}; - -#[derive(Debug)] -pub enum ValueDef { - WithAnnotation { - pattern_id: PatternId, // 4B - expr_id: ExprId, // 4B - type_id: TypeId, - rigids: Rigids, - expr_var: Variable, // 4B - }, - NoAnnotation { - pattern_id: PatternId, // 4B - expr_id: ExprId, // 4B - expr_var: Variable, // 4B - }, -} - -impl ShallowClone for ValueDef { - fn shallow_clone(&self) -> Self { - match self { - Self::WithAnnotation { - pattern_id, - expr_id, - type_id, - rigids, - expr_var, - } => Self::WithAnnotation { - pattern_id: *pattern_id, - expr_id: *expr_id, - type_id: *type_id, - rigids: rigids.shallow_clone(), - expr_var: *expr_var, - }, - Self::NoAnnotation { - pattern_id, - expr_id, - expr_var, - } => Self::NoAnnotation { - pattern_id: *pattern_id, - expr_id: *expr_id, - expr_var: *expr_var, - }, - } - } -} - -impl ValueDef { - pub fn get_expr_id(&self) -> ExprId { - match self { - ValueDef::WithAnnotation { expr_id, .. } => *expr_id, - ValueDef::NoAnnotation { expr_id, .. } => *expr_id, - } - } - - pub fn get_pattern_id(&self) -> NodeId { - match self { - ValueDef::WithAnnotation { pattern_id, .. } => *pattern_id, - ValueDef::NoAnnotation { pattern_id, .. } => *pattern_id, - } - } -} - -pub fn value_def_to_string(val_def: &ValueDef, pool: &Pool) -> String { - match val_def { - ValueDef::WithAnnotation { - pattern_id, - expr_id, - type_id, - rigids, - expr_var, - } => { - format!("WithAnnotation {{ pattern_id: {:?}, expr_id: {:?}, type_id: {:?}, rigids: {:?}, expr_var: {:?}}}", pool.get(*pattern_id), expr2_to_string(*expr_id, pool), pool.get(*type_id), rigids, expr_var) - } - ValueDef::NoAnnotation { - pattern_id, - expr_id, - expr_var, - } => { - format!( - "NoAnnotation {{ pattern_id: {:?}, expr_id: {:?}, expr_var: {:?}}}", - pool.get(*pattern_id), - expr2_to_string(*expr_id, pool), - expr_var - ) - } - } -} diff --git a/ast/src/lang/env.rs b/ast/src/lang/env.rs deleted file mode 100644 index 6afee73494..0000000000 --- a/ast/src/lang/env.rs +++ /dev/null @@ -1,189 +0,0 @@ -use crate::mem_pool::pool::{NodeId, Pool}; -use bumpalo::{collections::Vec as BumpVec, Bump}; -use roc_collections::all::{MutMap, MutSet}; -use roc_module::ident::{Ident, Lowercase, ModuleName}; -use roc_module::symbol::{IdentIds, IdentIdsByModule, ModuleId, ModuleIds, Symbol}; -use roc_problem::can::{Problem, RuntimeError}; -use roc_region::all::{Loc, Region}; -use roc_types::subs::VarStore; - -use super::core::def::def::References; - -/// TODO document -#[derive(Debug)] -pub struct Env<'a> { - pub home: ModuleId, - pub var_store: &'a mut VarStore, - pub pool: &'a mut Pool, - pub arena: &'a Bump, - - pub problems: BumpVec<'a, Problem>, - - pub dep_idents: IdentIdsByModule, - pub module_ids: &'a ModuleIds, - pub ident_ids: IdentIds, - pub exposed_ident_ids: IdentIds, - - pub closures: MutMap, - /// Symbols which were referenced by qualified lookups. - pub qualified_lookups: MutSet, - - pub top_level_symbols: MutSet, - - pub closure_name_symbol: Option, - pub tailcallable_symbol: Option, -} - -impl<'a> Env<'a> { - #[allow(clippy::too_many_arguments)] - pub fn new( - home: ModuleId, - arena: &'a Bump, - pool: &'a mut Pool, - var_store: &'a mut VarStore, - dep_idents: IdentIdsByModule, - module_ids: &'a ModuleIds, - exposed_ident_ids: IdentIds, - ) -> Env<'a> { - Env { - home, - arena, - pool, - problems: BumpVec::new_in(arena), - var_store, - dep_idents, - module_ids, - ident_ids: exposed_ident_ids.clone(), // we start with these, but will add more later using Scope.introduce - exposed_ident_ids, - closures: MutMap::default(), - qualified_lookups: MutSet::default(), - tailcallable_symbol: None, - closure_name_symbol: None, - top_level_symbols: MutSet::default(), - } - } - - pub fn add(&mut self, item: T, region: Region) -> NodeId { - let id = self.pool.add(item); - self.set_region(id, region); - - id - } - - pub fn problem(&mut self, problem: Problem) { - self.problems.push(problem); - } - - pub fn set_region(&mut self, _node_id: NodeId, _region: Region) { - dbg!("Don't Forget to set the region eventually"); - } - - pub fn register_closure(&mut self, symbol: Symbol, references: References) { - self.closures.insert(symbol, references); - } - - /// Generates a unique, new symbol like "$1" or "$5", - /// using the home module as the module_id. - /// - /// This is used, for example, during canonicalization of an Expr::Closure - /// to generate a unique symbol to refer to that closure. - pub fn gen_unique_symbol(&mut self) -> Symbol { - let ident_id = self.ident_ids.gen_unique(); - - Symbol::new(self.home, ident_id) - } - - /// Returns Err if the symbol resolved, but it was not exposed by the given module - pub fn qualified_lookup( - &mut self, - module_name: &str, - ident: &str, - region: Region, - ) -> Result { - debug_assert!( - !module_name.is_empty(), - "Called env.qualified_lookup with an unqualified ident: {:?}", - ident - ); - - let module_name: ModuleName = module_name.into(); - - match self.module_ids.get_id(&module_name) { - Some(&module_id) => { - let ident: Ident = ident.into(); - - // You can do qualified lookups on your own module, e.g. - // if I'm in the Foo module, I can do a `Foo.bar` lookup. - if module_id == self.home { - match self.ident_ids.get_id(&ident) { - Some(ident_id) => { - let symbol = Symbol::new(module_id, ident_id); - - self.qualified_lookups.insert(symbol); - - Ok(symbol) - } - None => Err(RuntimeError::LookupNotInScope( - Loc { - value: ident, - region, - }, - self.ident_ids - .ident_strs() - .map(|(_, string)| string.into()) - .collect(), - )), - } - } else { - match self.dep_idents.get(&module_id) { - Some(exposed_ids) => match exposed_ids.get_id(&ident) { - Some(ident_id) => { - let symbol = Symbol::new(module_id, ident_id); - - self.qualified_lookups.insert(symbol); - - Ok(symbol) - } - None => { - let exposed_values = exposed_ids - .ident_strs() - .filter(|(_, ident)| { - ident.starts_with(|c: char| c.is_lowercase()) - }) - .map(|(_, ident)| Lowercase::from(ident)) - .collect(); - Err(RuntimeError::ValueNotExposed { - module_name, - ident, - region, - exposed_values, - }) - } - }, - None => Err(RuntimeError::ModuleNotImported { - module_name, - imported_modules: self - .dep_idents - .keys() - .filter_map(|module_id| self.module_ids.get_name(*module_id)) - .map(|module_name| module_name.as_ref().into()) - .collect(), - region, - module_exists: true, - }), - } - } - } - None => Err(RuntimeError::ModuleNotImported { - module_name, - imported_modules: self - .module_ids - .available_modules() - .map(|string| string.as_ref().into()) - .collect(), - region, - module_exists: false, - }), - } - } -} diff --git a/ast/src/lang/mod.rs b/ast/src/lang/mod.rs deleted file mode 100644 index fa21ea740d..0000000000 --- a/ast/src/lang/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub mod core; -pub mod env; -mod rigids; -pub mod scope; diff --git a/ast/src/lang/rigids.rs b/ast/src/lang/rigids.rs deleted file mode 100644 index d86aa5ec88..0000000000 --- a/ast/src/lang/rigids.rs +++ /dev/null @@ -1,83 +0,0 @@ -use std::{ - collections::{HashMap, HashSet}, - hash::BuildHasherDefault, -}; - -use crate::mem_pool::{ - pool::Pool, pool_str::PoolStr, pool_vec::PoolVec, shallow_clone::ShallowClone, -}; -use roc_collections::all::WyHash; -use roc_module::ident::Lowercase; -use roc_types::subs::Variable; - -#[derive(Debug)] -pub struct Rigids { - // Rigid type variable = type variable where type is specified by the programmer - pub names: PoolVec<(Option, Variable)>, // 8B - padding: [u8; 1], -} - -#[allow(clippy::needless_collect)] -impl Rigids { - pub fn new( - named: HashMap>, - unnamed: HashSet>, - pool: &mut Pool, - ) -> Self { - let names = PoolVec::with_capacity((named.len() + unnamed.len()) as u32, pool); - - let mut temp_names = Vec::new(); - - temp_names.extend(named.iter().map(|(name, var)| (Some(name.as_str()), *var))); - - temp_names.extend(unnamed.iter().map(|var| (None, *var))); - - for (node_id, (opt_name, variable)) in names.iter_node_ids().zip(temp_names) { - let poolstr = opt_name.map(|name| PoolStr::new(name, pool)); - - pool[node_id] = (poolstr, variable); - } - - Self { - names, - padding: Default::default(), - } - } - - pub fn named(&self, pool: &mut Pool) -> PoolVec<(PoolStr, Variable)> { - let named = self - .names - .iter(pool) - .filter_map(|(opt_pool_str, var)| { - opt_pool_str.as_ref().map(|pool_str| (*pool_str, *var)) - }) - .collect::>(); - - PoolVec::new(named.into_iter(), pool) - } - - pub fn unnamed(&self, pool: &mut Pool) -> PoolVec { - let unnamed = self - .names - .iter(pool) - .filter_map(|(opt_pool_str, var)| { - if opt_pool_str.is_none() { - Some(*var) - } else { - None - } - }) - .collect::>(); - - PoolVec::new(unnamed.into_iter(), pool) - } -} - -impl ShallowClone for Rigids { - fn shallow_clone(&self) -> Self { - Self { - names: self.names.shallow_clone(), - padding: self.padding, - } - } -} diff --git a/ast/src/lang/scope.rs b/ast/src/lang/scope.rs deleted file mode 100644 index c5ee9ae397..0000000000 --- a/ast/src/lang/scope.rs +++ /dev/null @@ -1,353 +0,0 @@ -#![allow(clippy::all)] -#![allow(dead_code)] -#![allow(unused_imports)] - -use std::fmt; - -use crate::ast_error::ASTResult; -use crate::mem_pool::pool::Pool; -use crate::mem_pool::pool_str::PoolStr; -use crate::mem_pool::pool_vec::PoolVec; -use crate::mem_pool::shallow_clone::ShallowClone; -use roc_collections::all::{MutMap, MutSet}; -use roc_module::ident::{Ident, Lowercase}; -use roc_module::symbol::{ - get_module_ident_ids, get_module_ident_ids_mut, IdentIds, IdentIdsByModule, Interns, ModuleId, - Symbol, -}; -use roc_problem::can::RuntimeError; -use roc_region::all::{Loc, Region}; -use roc_types::{ - builtin_aliases, - solved_types::{BuiltinAlias, FreeVars, SolvedType}, - subs::{VarId, VarStore, Variable}, -}; - -use super::core::types::{Alias, Type2, TypeId}; -use super::env::Env; - -fn solved_type_to_type_id( - pool: &mut Pool, - solved_type: &SolvedType, - free_vars: &mut FreeVars, - var_store: &mut VarStore, -) -> TypeId { - let typ2 = to_type2(pool, solved_type, free_vars, var_store); - - pool.add(typ2) -} - -fn to_type2( - pool: &mut Pool, - solved_type: &SolvedType, - free_vars: &mut FreeVars, - var_store: &mut VarStore, -) -> Type2 { - match solved_type { - // TODO(opaques): take opaques into account - SolvedType::Alias(symbol, solved_type_variables, _todo, solved_actual, _kind) => { - let type_variables = PoolVec::with_capacity(solved_type_variables.len() as u32, pool); - - for (type_variable_node_id, solved_arg) in type_variables - .iter_node_ids() - .zip(solved_type_variables.iter()) - { - let typ2 = to_type2(pool, solved_arg, free_vars, var_store); - - let node = pool.add(typ2); - - pool[type_variable_node_id] = node; - } - - let actual_typ2 = to_type2(pool, solved_actual, free_vars, var_store); - - let actual = pool.add(actual_typ2); - - let typ2 = Type2::Alias(*symbol, type_variables, actual); - - typ2 - } - SolvedType::TagUnion(tags, ext) => { - let new_tags = PoolVec::with_capacity(tags.len() as u32, pool); - - for (tag_node_id, (tag_name, args)) in new_tags.iter_node_ids().zip(tags.iter()) { - let new_args: PoolVec = PoolVec::with_capacity(args.len() as u32, pool); - - for (arg_node_id, arg) in new_args.iter_node_ids().zip(args.iter()) { - let node = to_type2(pool, arg, free_vars, var_store); - - pool[arg_node_id] = node; - } - - pool[tag_node_id] = (tag_name.clone(), new_args); - } - - let actual_typ2 = to_type2(pool, ext, free_vars, var_store); - - let actual = pool.add(actual_typ2); - - let typ2 = Type2::TagUnion(new_tags, actual); - - typ2 - } - SolvedType::Flex(var_id) => { - Type2::Variable(var_id_to_flex_var(*var_id, free_vars, var_store)) - } - SolvedType::EmptyTagUnion => Type2::EmptyTagUnion, - rest => todo!("{:?}", rest), - } -} - -fn var_id_to_flex_var( - var_id: VarId, - free_vars: &mut FreeVars, - var_store: &mut VarStore, -) -> Variable { - if let Some(var) = free_vars.unnamed_vars.get(&var_id) { - *var - } else { - let var = var_store.fresh(); - free_vars.unnamed_vars.insert(var_id, var); - - var - } -} - -#[derive(Debug)] -pub struct Scope { - /// All the identifiers in scope, mapped to were they were defined and - /// the Symbol they resolve to. - idents: MutMap, - - /// A cache of all the symbols in scope. This makes lookups much - /// faster when checking for unused defs and unused arguments. - symbols: MutMap, - - /// The type aliases currently in scope - aliases: MutMap, - - /// The current module being processed. This will be used to turn - /// unqualified idents into Symbols. - home: ModuleId, -} - -impl Scope { - pub fn new(home: ModuleId, pool: &mut Pool, var_store: &mut VarStore) -> Scope { - let solved_aliases = builtin_aliases::aliases(); - let mut aliases = MutMap::default(); - - for (symbol, builtin_alias) in solved_aliases { - // let BuiltinAlias { region, vars, typ } = builtin_alias; - let BuiltinAlias { vars, typ, .. } = builtin_alias; - - let mut free_vars = FreeVars::default(); - - // roc_types::solved_types::to_type(&typ, &mut free_vars, var_store); - let actual = solved_type_to_type_id(pool, &typ, &mut free_vars, var_store); - - // make sure to sort these variables to make them line up with the type arguments - let mut type_variables: Vec<_> = free_vars.unnamed_vars.into_iter().collect(); - type_variables.sort(); - - debug_assert_eq!(vars.len(), type_variables.len()); - let variables = PoolVec::with_capacity(vars.len() as u32, pool); - - let it = variables - .iter_node_ids() - .zip(vars.iter()) - .zip(type_variables); - for ((node_id, loc_name), (_, var)) in it { - // TODO region is ignored, but "fake" anyway. How to resolve? - let name = PoolStr::new(loc_name.value.as_str(), pool); - pool[node_id] = (name, var); - } - - let alias = Alias { - actual, - /// We know that builtin aliases have no hidden variables (e.g. in closures) - hidden_variables: PoolVec::empty(pool), - targs: variables, - }; - - aliases.insert(symbol, alias); - } - - let idents = Symbol::default_in_scope(); - let idents: MutMap<_, _> = idents.into_iter().collect(); - - Scope { - home, - idents, - symbols: MutMap::default(), - aliases, - } - } - - pub fn idents(&self) -> impl Iterator { - self.idents.iter() - } - - pub fn symbols(&self) -> impl Iterator + '_ { - self.symbols.iter().map(|(x, y)| (*x, *y)) - } - - pub fn contains_ident(&self, ident: &Ident) -> bool { - self.idents.contains_key(ident) - } - - pub fn contains_symbol(&self, symbol: Symbol) -> bool { - self.symbols.contains_key(&symbol) - } - - pub fn num_idents(&self) -> usize { - self.idents.len() - } - - pub fn lookup(&mut self, ident: &Ident, region: Region) -> Result { - match self.idents.get(ident) { - Some((symbol, _)) => Ok(*symbol), - None => Err(RuntimeError::LookupNotInScope( - Loc { - region, - value: ident.clone().into(), - }, - self.idents.keys().map(|v| v.as_ref().into()).collect(), - )), - } - } - - pub fn lookup_alias(&self, symbol: Symbol) -> Option<&Alias> { - self.aliases.get(&symbol) - } - - /// Introduce a new ident to scope. - /// - /// Returns Err if this would shadow an existing ident, including the - /// Symbol and Region of the ident we already had in scope under that name. - pub fn introduce( - &mut self, - ident: Ident, - exposed_ident_ids: &IdentIds, - all_ident_ids: &mut IdentIds, - region: Region, - ) -> Result)> { - match self.idents.get(&ident) { - Some((_, original_region)) => { - let shadow = Loc { - value: ident, - region, - }; - - Err((*original_region, shadow)) - } - None => { - // If this IdentId was already added previously - // when the value was exposed in the module header, - // use that existing IdentId. Otherwise, create a fresh one. - let ident_id = match exposed_ident_ids.get_id(&ident) { - Some(ident_id) => ident_id, - None => all_ident_ids.add_str(ident.as_str()), - }; - - let symbol = Symbol::new(self.home, ident_id); - - self.symbols.insert(symbol, region); - self.idents.insert(ident, (symbol, region)); - - Ok(symbol) - } - } - } - - /// Ignore an identifier. - /// - /// Used for record guards like { x: Just _ } - pub fn ignore(&mut self, ident: Ident, all_ident_ids: &mut IdentIds) -> Symbol { - let ident_id = all_ident_ids.add_str(ident.as_str()); - Symbol::new(self.home, ident_id) - } - - /// Import a Symbol from another module into this module's top-level scope. - /// - /// Returns Err if this would shadow an existing ident, including the - /// Symbol and Region of the ident we already had in scope under that name. - pub fn import( - &mut self, - ident: Ident, - symbol: Symbol, - region: Region, - ) -> Result<(), (Symbol, Region)> { - match self.idents.get(&ident) { - Some(shadowed) => Err(*shadowed), - None => { - self.symbols.insert(symbol, region); - self.idents.insert(ident, (symbol, region)); - - Ok(()) - } - } - } - - pub fn add_alias( - &mut self, - pool: &mut Pool, - name: Symbol, - vars: PoolVec<(PoolStr, Variable)>, - typ: TypeId, - ) { - let mut hidden_variables = MutSet::default(); - hidden_variables.extend(typ.variables(pool)); - - for loc_var in vars.iter(pool) { - hidden_variables.remove(&loc_var.1); - } - - let hidden_variables_vec = PoolVec::with_capacity(hidden_variables.len() as u32, pool); - - for (node_id, var) in hidden_variables_vec.iter_node_ids().zip(hidden_variables) { - pool[node_id] = var; - } - - let alias = Alias { - targs: vars, - hidden_variables: hidden_variables_vec, - actual: typ, - }; - - self.aliases.insert(name, alias); - } - - pub fn contains_alias(&mut self, name: Symbol) -> bool { - self.aliases.contains_key(&name) - } - - pub fn fill_scope(&mut self, env: &Env, all_ident_ids: &mut IdentIdsByModule) -> ASTResult<()> { - let ident_ids = get_module_ident_ids(all_ident_ids, &env.home)?.clone(); - - for (_, ident_ref) in ident_ids.ident_strs() { - self.introduce( - ident_ref.into(), - &env.exposed_ident_ids, - get_module_ident_ids_mut(all_ident_ids, &env.home)?, - Region::zero(), - )?; - } - - Ok(()) - } -} - -impl ShallowClone for Scope { - fn shallow_clone(&self) -> Self { - Self { - idents: self.idents.clone(), - symbols: self.symbols.clone(), - aliases: self - .aliases - .iter() - .map(|(s, a)| (*s, a.shallow_clone())) - .collect(), - home: self.home, - } - } -} diff --git a/ast/src/lib.rs b/ast/src/lib.rs deleted file mode 100644 index 092b86154e..0000000000 --- a/ast/src/lib.rs +++ /dev/null @@ -1,8 +0,0 @@ -pub mod ast_error; -mod canonicalization; -pub mod constrain; -pub mod lang; -pub mod mem_pool; -pub mod module; -pub mod parse; -pub mod solve_type; diff --git a/ast/src/mem_pool/mod.rs b/ast/src/mem_pool/mod.rs deleted file mode 100644 index b6c8c83b8f..0000000000 --- a/ast/src/mem_pool/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub mod pool; -pub mod pool_str; -pub mod pool_vec; -pub mod shallow_clone; diff --git a/ast/src/mem_pool/pool.rs b/ast/src/mem_pool/pool.rs deleted file mode 100644 index e28dbc33bd..0000000000 --- a/ast/src/mem_pool/pool.rs +++ /dev/null @@ -1,269 +0,0 @@ -/// A memory pool of 32-byte nodes. The node value 0 is reserved for the pool's -/// use, and valid nodes may never have that value. -/// -/// Internally, the pool is divided into pages of 4096 bytes. It stores nodes -/// into one page at a time, and when it runs out, it uses mmap to reserve an -/// anonymous memory page in which to store nodes. -/// -/// Since nodes are 32 bytes, one page can store 128 nodes; you can access a -/// particular node by its NodeId, which is an opaque wrapper around a pointer. -/// -/// Pages also use the node value 0 (all 0 bits) to mark nodes as unoccupied. -/// This is important for performance. -use std::any::type_name; -use std::ffi::c_void; -use std::marker::PhantomData; -use std::mem::{align_of, size_of, MaybeUninit}; - -pub const NODE_BYTES: usize = 32; - -// Each page has 128 slots. Each slot holds one 32B node -// This means each page is 4096B, which is the size of a memory page -// on typical systems where the compiler will be run. -// -// Nice things about this system include: -// * Allocating a new page is as simple as asking the OS for a memory page. -// * Since each node is 32B, each node's memory address will be a multiple of 16. -// * Thanks to the free lists and our consistent chunk sizes, we should -// end up with very little fragmentation. -// * Finding a slot for a given node should be very fast: see if the relevant -// free list has any openings; if not, try the next size up. -// -// Less nice things include: -// * This system makes it very hard to ever give a page back to the OS. -// We could try doing the Mesh Allocator strategy: whenever we allocate -// something, assign it to a random slot in the page, and then periodically -// try to merge two pages into one (by locking and remapping them in the OS) -// and then returning the redundant physical page back to the OS. This should -// work in theory, but is pretty complicated, and we'd need to schedule it. -// Keep in mind that we can't use the Mesh Allocator itself because it returns -// usize pointers, which would be too big for us to have 16B nodes. -// On the plus side, we could be okay with higher memory usage early on, -// and then later use the Mesh strategy to reduce long-running memory usage. -// -// With this system, we can allocate up to 4B nodes. If we wanted to keep -// a generational index in there, like https://crates.io/crates/sharded-slab -// does, we could use some of the 32 bits for that. For example, if we wanted -// to have a 5-bit generational index (supporting up to 32 generations), then -// we would have 27 bits remaining, meaning we could only support at most -// 134M nodes. Since the editor has a separate Pool for each module, is that -// enough for any single module we'll encounter in practice? Probably, and -// especially if we allocate super large collection literals on the heap instead -// of in the pool. -// -// Another possible design is to try to catch reuse bugs using an "ASan" like -// approach: in development builds, whenever we "free" a particular slot, we -// can add it to a dev-build-only "freed nodes" list and don't hand it back -// out (so, we leak the memory.) Then we can (again, in development builds only) -// check to see if we're about to store something in zeroed-out memory; if so, check -// to see if it was - -#[derive(Debug, Eq)] -pub struct NodeId { - pub(super) index: u32, - pub(super) _phantom: PhantomData, -} - -impl Clone for NodeId { - fn clone(&self) -> Self { - NodeId { - index: self.index, - _phantom: PhantomData::default(), - } - } -} - -impl PartialEq for NodeId { - fn eq(&self, other: &Self) -> bool { - self.index == other.index - } -} - -impl Copy for NodeId {} - -#[derive(Debug)] -pub struct Pool { - pub(super) nodes: *mut [MaybeUninit; NODE_BYTES], - num_nodes: u32, - capacity: u32, - // free_1node_slots: Vec>, -} - -impl Pool { - pub fn with_capacity(nodes: u32) -> Self { - // round up number of nodes requested to nearest page size in bytes - let bytes_per_page = page_size::get(); - let node_bytes = NODE_BYTES * nodes as usize; - let leftover = node_bytes % bytes_per_page; - let bytes_to_mmap = if leftover == 0 { - node_bytes - } else { - node_bytes + bytes_per_page - leftover - }; - - let nodes = unsafe { - // mmap anonymous memory pages - that is, contiguous virtual memory - // addresses from the OS which will be lazily translated into - // physical memory one 4096-byte page at a time, once we actually - // try to read or write in that page's address range. - #[cfg(unix)] - { - use libc::{MAP_ANONYMOUS, MAP_PRIVATE, PROT_READ, PROT_WRITE}; - - libc::mmap( - std::ptr::null_mut(), - bytes_to_mmap, - PROT_READ | PROT_WRITE, - MAP_PRIVATE | MAP_ANONYMOUS, - 0, - 0, - ) - } - #[cfg(windows)] - { - use winapi::um::memoryapi::VirtualAlloc; - use winapi::um::winnt::PAGE_READWRITE; - use winapi::um::winnt::{MEM_COMMIT, MEM_RESERVE}; - - VirtualAlloc( - std::ptr::null_mut(), - bytes_to_mmap, - MEM_COMMIT | MEM_RESERVE, - PAGE_READWRITE, - ) - } - } as *mut [MaybeUninit; NODE_BYTES]; - - // This is our actual capacity, in nodes. - // It might be higher than the requested capacity due to rounding up - // to nearest page size. - let capacity = (bytes_to_mmap / NODE_BYTES) as u32; - - Pool { - nodes, - num_nodes: 0, - capacity, - } - } - - pub fn add(&mut self, node: T) -> NodeId { - // It's only safe to store this if T fits in S. - debug_assert!( - size_of::() <= NODE_BYTES, - "{} has a size of {}, but it needs to be at most {}", - type_name::(), - size_of::(), - NODE_BYTES - ); - - let node_id = self.reserve(1); - - let node_ptr = self.get_ptr(node_id); - - unsafe { node_ptr.write(MaybeUninit::new(node)) }; - - node_id - } - - /// Reserves the given number of contiguous node slots, and returns - /// the NodeId of the first one. We only allow reserving 2^32 in a row. - pub(super) fn reserve(&mut self, nodes: u32) -> NodeId { - // TODO once we have a free list, look in there for an open slot first! - let index = self.num_nodes; - - if index < self.capacity { - self.num_nodes = index + nodes; - - NodeId { - index, - _phantom: PhantomData::default(), - } - } else { - todo!("pool ran out of capacity. TODO reallocate the nodes pointer to map to a bigger space. Can use mremap on Linux, but must memcpy lots of bytes on macOS and Windows."); - } - } - - pub fn get<'a, 'b, T>(&'a self, node_id: NodeId) -> &'b T { - unsafe { - let node_ptr = self.get_ptr(node_id) as *const T; - - &*node_ptr - } - } - - pub fn get_mut(&mut self, node_id: NodeId) -> &mut T { - unsafe { - let node_ptr = self.get_ptr(node_id) as *mut T; - - &mut *node_ptr - } - } - - pub fn set(&mut self, node_id: NodeId, element: T) { - unsafe { - let node_ptr = self.get_ptr(node_id); - - node_ptr.write(MaybeUninit::new(element)); - } - } - - fn get_ptr(&self, node_id: NodeId) -> *mut MaybeUninit { - let node_offset = unsafe { self.nodes.offset(node_id.index as isize) }; - - // This checks if the node_offset is aligned to T - assert!(0 == (node_offset as usize) & (align_of::() - 1)); - - node_offset as *mut MaybeUninit - } - - // A node is available iff its bytes are all zeroes - #[allow(dead_code)] - fn is_available(&self, node_id: NodeId) -> bool { - debug_assert_eq!(size_of::(), NODE_BYTES); - - unsafe { - let node_ptr = self.nodes.offset(node_id.index as isize) as *const [u8; NODE_BYTES]; - - *node_ptr == [0; NODE_BYTES] - } - } -} - -impl std::ops::Index> for Pool { - type Output = T; - - fn index(&self, node_id: NodeId) -> &Self::Output { - self.get(node_id) - } -} - -impl std::ops::IndexMut> for Pool { - fn index_mut(&mut self, node_id: NodeId) -> &mut Self::Output { - self.get_mut(node_id) - } -} - -impl Drop for Pool { - fn drop(&mut self) { - unsafe { - #[cfg(unix)] - { - libc::munmap( - self.nodes as *mut c_void, - NODE_BYTES * self.capacity as usize, - ); - } - #[cfg(windows)] - { - use winapi::um::memoryapi::VirtualFree; - use winapi::um::winnt::MEM_RELEASE; - - VirtualFree( - self.nodes as *mut c_void, - NODE_BYTES * self.capacity as usize, - MEM_RELEASE, - ); - } - } - } -} diff --git a/ast/src/mem_pool/pool_str.rs b/ast/src/mem_pool/pool_str.rs deleted file mode 100644 index d4ea9c28bf..0000000000 --- a/ast/src/mem_pool/pool_str.rs +++ /dev/null @@ -1,86 +0,0 @@ -use super::pool::{NodeId, Pool, NODE_BYTES}; -use super::shallow_clone::ShallowClone; -use std::ffi::c_void; -use std::marker::PhantomData; -use std::mem::size_of; - -/// A string containing at most 2^32 pool-allocated bytes. -#[derive(Debug, Copy, Clone)] -pub struct PoolStr { - first_node_id: NodeId<()>, - len: u32, -} - -#[test] -fn pool_str_size() { - assert_eq!(size_of::(), 8); -} - -impl PoolStr { - pub fn new(string: &str, pool: &mut Pool) -> Self { - debug_assert!(string.len() <= u32::MAX as usize); - - let chars_per_node = NODE_BYTES / size_of::(); - - let number_of_nodes = f64::ceil(string.len() as f64 / chars_per_node as f64) as u32; - - if number_of_nodes > 0 { - let first_node_id = pool.reserve(number_of_nodes); - let index = first_node_id.index as isize; - let next_node_ptr = unsafe { pool.nodes.offset(index) } as *mut c_void; - - unsafe { - libc::memcpy( - next_node_ptr, - string.as_ptr() as *const c_void, - string.len(), - ); - } - - PoolStr { - first_node_id, - len: string.len() as u32, - } - } else { - PoolStr { - first_node_id: NodeId { - index: 0, - _phantom: PhantomData::default(), - }, - len: 0, - } - } - } - - pub fn as_str(&self, pool: &Pool) -> &str { - unsafe { - let node_ptr = pool.nodes.offset(self.first_node_id.index as isize) as *const u8; - - let node_slice: &[u8] = std::slice::from_raw_parts(node_ptr, self.len as usize); - - std::str::from_utf8_unchecked(&node_slice[0..self.len as usize]) - } - } - - #[allow(clippy::len_without_is_empty)] - pub fn len(&self, pool: &Pool) -> usize { - let contents = self.as_str(pool); - - contents.len() - } - - pub fn is_empty(&self, pool: &Pool) -> bool { - self.len(pool) == 0 - } -} - -impl ShallowClone for PoolStr { - fn shallow_clone(&self) -> Self { - // Question: should this fully clone, or is a shallow copy - // (and the aliasing it entails) OK? - Self { - first_node_id: self.first_node_id, - len: self.len, - } - } -} diff --git a/ast/src/mem_pool/pool_vec.rs b/ast/src/mem_pool/pool_vec.rs deleted file mode 100644 index 75b3733fdf..0000000000 --- a/ast/src/mem_pool/pool_vec.rs +++ /dev/null @@ -1,323 +0,0 @@ -use super::pool::{NodeId, Pool, NODE_BYTES}; -use super::shallow_clone::ShallowClone; -use std::any::type_name; -use std::cmp::Ordering; -use std::ffi::c_void; -use std::marker::PhantomData; -use std::mem::size_of; - -/// An array of at most 2^32 pool-allocated nodes. -#[derive(Debug)] -pub struct PoolVec { - first_node_id: NodeId, - len: u32, -} - -#[test] -fn pool_vec_size() { - assert_eq!(size_of::>(), 8); -} - -impl<'a, T: 'a + Sized> PoolVec { - pub fn empty(pool: &mut Pool) -> Self { - Self::new(std::iter::empty(), pool) - } - - pub fn with_capacity(len: u32, pool: &mut Pool) -> Self { - debug_assert!( - size_of::() <= NODE_BYTES, - "{} has a size of {}", - type_name::(), - size_of::() - ); - - if len == 0 { - Self::empty(pool) - } else { - let first_node_id = pool.reserve(len); - - PoolVec { first_node_id, len } - } - } - - pub fn len(&self) -> usize { - self.len as usize - } - - pub fn is_empty(&self) -> bool { - self.len == 0 - } - - pub fn new>(nodes: I, pool: &mut Pool) -> Self { - debug_assert!(nodes.len() <= u32::MAX as usize); - debug_assert!(size_of::() <= NODE_BYTES); - - let len = nodes.len() as u32; - - if len > 0 { - let first_node_id = pool.reserve(len); - let index = first_node_id.index as isize; - let mut next_node_ptr = unsafe { pool.nodes.offset(index) } as *mut T; - - for (indx_inc, node) in nodes.enumerate() { - unsafe { - *next_node_ptr = node; - - next_node_ptr = pool.nodes.offset(index + (indx_inc as isize) + 1) as *mut T; - } - } - - PoolVec { first_node_id, len } - } else { - PoolVec { - first_node_id: NodeId { - index: 0, - _phantom: PhantomData::default(), - }, - len: 0, - } - } - } - - pub fn iter(&self, pool: &'a Pool) -> impl ExactSizeIterator { - self.pool_list_iter(pool) - } - - pub fn iter_mut(&self, pool: &'a mut Pool) -> impl ExactSizeIterator { - self.pool_list_iter_mut(pool) - } - - pub fn iter_node_ids(&self) -> impl ExactSizeIterator> { - self.pool_list_iter_node_ids() - } - - /// Private version of into_iter which exposes the implementation detail - /// of PoolVecIter. We don't want that struct to be public, but we - /// actually do want to have this separate function for code reuse - /// in the iterator's next() method. - #[inline(always)] - fn pool_list_iter(&self, pool: &'a Pool) -> PoolVecIter<'a, T> { - PoolVecIter { - pool, - current_node_id: self.first_node_id, - len_remaining: self.len, - } - } - - #[inline(always)] - fn pool_list_iter_mut(&self, pool: &'a Pool) -> PoolVecIterMut<'a, T> { - PoolVecIterMut { - pool, - current_node_id: self.first_node_id, - len_remaining: self.len, - } - } - - #[inline(always)] - fn pool_list_iter_node_ids(&self) -> PoolVecIterNodeIds { - PoolVecIterNodeIds { - current_node_id: self.first_node_id, - len_remaining: self.len, - } - } - - pub fn free(self, pool: &'a mut Pool) { - // zero out the memory - unsafe { - let index = self.first_node_id.index as isize; - let node_ptr = pool.nodes.offset(index) as *mut c_void; - let bytes = self.len as usize * NODE_BYTES; - - libc::memset(node_ptr, 0, bytes); - } - - // TODO insert it into the pool's free list - } -} - -impl ShallowClone for PoolVec { - fn shallow_clone(&self) -> Self { - // Question: should this fully clone, or is a shallow copy - // (and the aliasing it entails) OK? - Self { - first_node_id: self.first_node_id, - len: self.len, - } - } -} - -struct PoolVecIter<'a, T> { - pool: &'a Pool, - current_node_id: NodeId, - len_remaining: u32, -} - -impl<'a, T> ExactSizeIterator for PoolVecIter<'a, T> -where - T: 'a, -{ - fn len(&self) -> usize { - self.len_remaining as usize - } -} - -impl<'a, T> Iterator for PoolVecIter<'a, T> -where - T: 'a, -{ - type Item = &'a T; - - fn next(&mut self) -> Option { - let len_remaining = self.len_remaining; - - match len_remaining.cmp(&1) { - Ordering::Greater => { - // Get the current node - let index = self.current_node_id.index; - let node_ptr = unsafe { self.pool.nodes.offset(index as isize) } as *const T; - - // Advance the node pointer to the next node in the current page - self.current_node_id = NodeId { - index: index + 1, - _phantom: PhantomData::default(), - }; - self.len_remaining = len_remaining - 1; - - Some(unsafe { &*node_ptr }) - } - Ordering::Equal => { - self.len_remaining = 0; - - // Don't advance the node pointer's node, because that might - // advance past the end of the page! - - let index = self.current_node_id.index; - let node_ptr = unsafe { self.pool.nodes.offset(index as isize) } as *const T; - - Some(unsafe { &*node_ptr }) - } - Ordering::Less => { - // len_remaining was 0 - None - } - } - } -} - -struct PoolVecIterMut<'a, T> { - pool: &'a Pool, - current_node_id: NodeId, - len_remaining: u32, -} - -impl<'a, T> ExactSizeIterator for PoolVecIterMut<'a, T> -where - T: 'a, -{ - fn len(&self) -> usize { - self.len_remaining as usize - } -} - -impl<'a, T> Iterator for PoolVecIterMut<'a, T> -where - T: 'a, -{ - type Item = &'a mut T; - - fn next(&mut self) -> Option { - let len_remaining = self.len_remaining; - - match len_remaining.cmp(&1) { - Ordering::Greater => { - // Get the current node - let index = self.current_node_id.index; - let node_ptr = unsafe { self.pool.nodes.offset(index as isize) } as *mut T; - - // Advance the node pointer to the next node in the current page - self.current_node_id = NodeId { - index: index + 1, - _phantom: PhantomData::default(), - }; - self.len_remaining = len_remaining - 1; - - Some(unsafe { &mut *node_ptr }) - } - Ordering::Equal => { - self.len_remaining = 0; - - // Don't advance the node pointer's node, because that might - // advance past the end of the page! - - let index = self.current_node_id.index; - let node_ptr = unsafe { self.pool.nodes.offset(index as isize) } as *mut T; - - Some(unsafe { &mut *node_ptr }) - } - Ordering::Less => { - // len_remaining was 0 - None - } - } - } -} - -struct PoolVecIterNodeIds { - current_node_id: NodeId, - len_remaining: u32, -} - -impl ExactSizeIterator for PoolVecIterNodeIds { - fn len(&self) -> usize { - self.len_remaining as usize - } -} - -impl Iterator for PoolVecIterNodeIds { - type Item = NodeId; - - fn next(&mut self) -> Option { - let len_remaining = self.len_remaining; - - match len_remaining.cmp(&1) { - Ordering::Greater => { - // Get the current node - let current = self.current_node_id; - let index = current.index; - - // Advance the node pointer to the next node in the current page - self.current_node_id = NodeId { - index: index + 1, - _phantom: PhantomData::default(), - }; - self.len_remaining = len_remaining - 1; - - Some(current) - } - Ordering::Equal => { - self.len_remaining = 0; - - // Don't advance the node pointer's node, because that might - // advance past the end of the page! - - Some(self.current_node_id) - } - Ordering::Less => { - // len_remaining was 0 - None - } - } - } -} - -#[test] -fn pool_vec_iter_test() { - let expected_vec: Vec = vec![2, 4, 8, 16]; - - let mut test_pool = Pool::with_capacity(1024); - let pool_vec = PoolVec::new(expected_vec.clone().into_iter(), &mut test_pool); - - let current_vec: Vec = pool_vec.iter(&test_pool).copied().collect(); - - assert_eq!(current_vec, expected_vec); -} diff --git a/ast/src/mem_pool/shallow_clone.rs b/ast/src/mem_pool/shallow_clone.rs deleted file mode 100644 index 7bcd6b46e3..0000000000 --- a/ast/src/mem_pool/shallow_clone.rs +++ /dev/null @@ -1,35 +0,0 @@ -use roc_can::expected::Expected; -use roc_can::expected::PExpected; - -/// Clones the outer node, but does not clone any nodeids -pub trait ShallowClone { - fn shallow_clone(&self) -> Self; -} - -impl ShallowClone for Expected -where - T: ShallowClone, -{ - fn shallow_clone(&self) -> Self { - use Expected::*; - - match self { - NoExpectation(t) => NoExpectation(t.shallow_clone()), - ForReason(reason, t, region) => ForReason(reason.clone(), t.shallow_clone(), *region), - FromAnnotation(loc_pat, n, source, t) => { - FromAnnotation(loc_pat.clone(), *n, *source, t.shallow_clone()) - } - } - } -} - -impl ShallowClone for PExpected { - fn shallow_clone(&self) -> Self { - use PExpected::*; - - match self { - NoExpectation(t) => NoExpectation(t.shallow_clone()), - ForReason(reason, t, region) => ForReason(reason.clone(), t.shallow_clone(), *region), - } - } -} diff --git a/ast/src/module.rs b/ast/src/module.rs deleted file mode 100644 index 59a02185be..0000000000 --- a/ast/src/module.rs +++ /dev/null @@ -1,38 +0,0 @@ -use bumpalo::Bump; -use roc_load::{LoadedModule, Threading}; -use roc_target::TargetInfo; -use std::path::Path; - -pub fn load_module(src_file: &Path, threading: Threading) -> LoadedModule { - let subs_by_module = Default::default(); - - let arena = Bump::new(); - let loaded = roc_load::load_and_typecheck( - &arena, - src_file.to_path_buf(), - src_file.parent().unwrap_or_else(|| { - panic!( - "src_file {:?} did not have a parent directory but I need to have one.", - src_file - ) - }), - subs_by_module, - TargetInfo::default_x86_64(), - roc_reporting::report::RenderTarget::ColorTerminal, - threading, - ); - - match loaded { - Ok(x) => x, - Err(roc_load::LoadingProblem::FormattedReport(report)) => { - panic!( - "Failed to load module from src_file {:?}. Report: {}", - src_file, report - ); - } - Err(e) => panic!( - "Failed to load module from src_file {:?}: {:?}", - src_file, e - ), - } -} diff --git a/ast/src/parse/mod.rs b/ast/src/parse/mod.rs deleted file mode 100644 index 31bf78fd5e..0000000000 --- a/ast/src/parse/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod parse_ast; -pub mod parse_header; diff --git a/ast/src/parse/parse_ast.rs b/ast/src/parse/parse_ast.rs deleted file mode 100644 index 378b8838e2..0000000000 --- a/ast/src/parse/parse_ast.rs +++ /dev/null @@ -1,54 +0,0 @@ -use bumpalo::Bump; -use roc_module::symbol::Interns; -use roc_region::all::Region; - -use crate::{ - ast_error::ASTResult, - lang::{ - core::{ - ast::AST, - def::{def2::DefId, def_to_def2::str_to_def2}, - expr::expr2::Expr2, - }, - env::Env, - scope::Scope, - }, -}; - -use super::parse_header; - -pub fn parse_from_string<'a>( - code_str: &'a str, - env: &mut Env<'a>, - ast_arena: &'a Bump, - interns: &mut Interns, -) -> ASTResult { - let blank_line_indx = code_str - .find("\n\n") - .expect("I was expecting two newline chars to split header and rest of code."); - - let header_str = &code_str[0..blank_line_indx]; - let tail_str = &code_str[blank_line_indx..]; - - let mut scope = Scope::new(env.home, env.pool, env.var_store); - scope.fill_scope(env, &mut interns.all_ident_ids)?; - - let region = Region::zero(); - - let mut def_ids = Vec::::new(); - - let def2_vec = str_to_def2(ast_arena, tail_str, env, &mut scope, region)?; - - for def2 in def2_vec { - let def_id = env.pool.add(def2); - - def_ids.push(def_id); - } - - let ast_node_id = env.pool.add(Expr2::Blank); - - Ok(AST { - header: parse_header::parse_from_string(header_str, ast_node_id), - def_ids, - }) -} diff --git a/ast/src/parse/parse_header.rs b/ast/src/parse/parse_header.rs deleted file mode 100644 index 8ec881574a..0000000000 --- a/ast/src/parse/parse_header.rs +++ /dev/null @@ -1,12 +0,0 @@ -use crate::lang::core::{expr::expr2::ExprId, header::AppHeader}; - -// TODO don't use mock struct and actually parse string -pub fn parse_from_string(_header_str: &str, ast_node_id: ExprId) -> AppHeader { - AppHeader { - app_name: "\"untitled-app\"".to_owned(), - packages_base: "\"c-platform\"".to_owned(), - imports: vec![], - provides: vec!["main".to_owned()], - ast_node_id, - } -} diff --git a/ast/src/solve_type.rs b/ast/src/solve_type.rs deleted file mode 100644 index 9c87d37801..0000000000 --- a/ast/src/solve_type.rs +++ /dev/null @@ -1,1967 +0,0 @@ -#![allow(clippy::all)] -#![allow(dead_code)] -use bumpalo::Bump; -use roc_can::expected::{Expected, PExpected}; -use roc_collections::all::{BumpMap, BumpMapDefault, MutMap}; -use roc_error_macros::internal_error; -use roc_module::ident::TagName; -use roc_module::symbol::Symbol; -use roc_region::all::{Loc, Region}; -use roc_types::solved_types::Solved; -use roc_types::subs::{ - self, AliasVariables, Content, Descriptor, FlatType, Mark, OptVariable, Rank, RecordFields, - Subs, SubsSlice, UnionLambdas, UnionTags, Variable, VariableSubsSlice, -}; -use roc_types::types::{ - gather_fields_unsorted_iter, Alias, AliasKind, Category, ErrorType, PatternCategory, - RecordField, -}; -use roc_unify::unify::unify; -use roc_unify::unify::Mode; -use roc_unify::unify::Unified::*; - -use crate::constrain::{Constraint, PresenceConstraint}; -use crate::lang::core::types::Type2; -use crate::mem_pool::pool::Pool; -use crate::mem_pool::pool_vec::PoolVec; -use crate::mem_pool::shallow_clone::ShallowClone; - -// Type checking system adapted from Elm by Evan Czaplicki, BSD-3-Clause Licensed -// https://github.com/elm/compiler -// Thank you, Evan! - -// A lot of energy was put into making type inference fast. That means it's pretty intimidating. -// -// Fundamentally, type inference assigns very general types based on syntax, and then tries to -// make all the pieces fit together. For instance when writing -// -// > f x -// -// We know that `f` is a function, and thus must have some type `a -> b`. -// `x` is just a variable, that gets the type `c` -// -// Next comes constraint generation. For `f x` to be well-typed, -// it must be the case that `c = a`, So a constraint `Eq(c, a)` is generated. -// But `Eq` is a bit special: `c` does not need to equal `a` exactly, but they need to be equivalent. -// This allows for instance the use of aliases. `c` could be an alias, and so looks different from -// `a`, but they still represent the same type. -// -// Then we get to solving, which happens in this file. -// -// When we hit an `Eq` constraint, then we check whether the two involved types are in fact -// equivalent using unification, and when they are, we can substitute one for the other. -// -// When all constraints are processed, and no unification errors have occurred, then the program -// is type-correct. Otherwise the errors are reported. -// -// Now, coming back to efficiency, this type checker uses *ranks* to optimize -// The rank tracks the number of let-bindings a variable is "under". Top-level definitions -// have rank 1. A let in a top-level definition gets rank 2, and so on. -// -// This has to do with generalization of type variables. This is described here -// -// http://okmij.org/ftp/ML/generalization.html#levels -// -// The problem is that when doing inference naively, this program would fail to typecheck -// -// f = -// id = \x -> x -// -// { a: id 1, b: id "foo" } -// -// Because `id` is applied to an integer, the type `Int -> Int` is inferred, which then gives a -// type error for `id "foo"`. -// -// Thus instead the inferred type for `id` is generalized (see the `generalize` function) to `a -> a`. -// Ranks are used to limit the number of type variables considered for generalization. Only those inside -// of the let (so those used in inferring the type of `\x -> x`) are considered. - -#[derive(Debug, Clone)] -pub enum TypeError { - BadExpr(Region, Category, ErrorType, Expected), - BadPattern(Region, PatternCategory, ErrorType, PExpected), - CircularType(Region, Symbol, ErrorType), - BadType(roc_types::types::Problem), - UnexposedLookup(Symbol), -} - -#[derive(Clone, Debug, Default)] -pub struct Env { - pub vars_by_symbol: MutMap, - pub aliases: MutMap, -} - -const DEFAULT_POOLS: usize = 8; - -#[derive(Clone, Debug)] -struct Pools(Vec>); - -impl Default for Pools { - fn default() -> Self { - Pools::new(DEFAULT_POOLS) - } -} - -impl Pools { - pub fn new(num_pools: usize) -> Self { - Pools(vec![Vec::new(); num_pools]) - } - - pub fn len(&self) -> usize { - self.0.len() - } - - pub fn get_mut(&mut self, rank: Rank) -> &mut Vec { - self.0 - .get_mut(rank.into_usize()) - .unwrap_or_else(|| panic!("Compiler bug: could not find pool at rank {}", rank)) - } - - pub fn get(&self, rank: Rank) -> &Vec { - self.0 - .get(rank.into_usize()) - .unwrap_or_else(|| panic!("Compiler bug: could not find pool at rank {}", rank)) - } - - pub fn iter(&self) -> std::slice::Iter<'_, Vec> { - self.0.iter() - } - - pub fn split_last(&self) -> (&Vec, &[Vec]) { - self.0 - .split_last() - .unwrap_or_else(|| panic!("Attempted to split_last() on non-empty Pools")) - } - - pub fn extend_to(&mut self, n: usize) { - for _ in self.len()..n { - self.0.push(Vec::new()); - } - } -} - -#[derive(Clone)] -struct State { - env: Env, - mark: Mark, -} - -pub fn run<'a>( - arena: &'a Bump, - mempool: &mut Pool, - env: &Env, - problems: &mut Vec, - mut subs: Subs, - constraint: &Constraint, -) -> (Solved, Env) { - let env = run_in_place(arena, mempool, env, problems, &mut subs, constraint); - - (Solved(subs), env) -} - -/// Modify an existing subs in-place instead -pub fn run_in_place<'a>( - arena: &'a Bump, - mempool: &mut Pool, - env: &Env, - problems: &mut Vec, - subs: &mut Subs, - constraint: &Constraint, -) -> Env { - let mut pools = Pools::default(); - let state = State { - env: env.clone(), - mark: Mark::NONE.next(), - }; - let rank = Rank::toplevel(); - let state = solve( - arena, - mempool, - env, - state, - rank, - &mut pools, - problems, - &mut MutMap::default(), - subs, - constraint, - ); - - state.env -} - -#[allow(clippy::too_many_arguments)] -fn solve<'a>( - arena: &'a Bump, - mempool: &mut Pool, - env: &Env, - state: State, - rank: Rank, - pools: &mut Pools, - problems: &mut Vec, - cached_aliases: &mut MutMap, - subs: &mut Subs, - constraint: &Constraint, -) -> State { - use crate::solve_type::Constraint::*; - - match constraint { - True => state, - // SaveTheEnvironment => { - // // NOTE deviation: elm only copies the env into the state on SaveTheEnvironment - // let mut copy = state; - // - // copy.env = env.clone(); - // - // copy - // } - Eq(typ, expectation, category, region) => { - let actual = type_to_var(arena, mempool, subs, rank, pools, cached_aliases, typ); - let expected = type_to_var( - arena, - mempool, - subs, - rank, - pools, - cached_aliases, - expectation.get_type_ref(), - ); - - match unify(subs, actual, expected, Mode::EQ) { - Success { - vars, - must_implement_ability: _, - lambda_sets_to_specialize: _, // TODO ignored - } => { - // TODO(abilities) record deferred ability checks - introduce(subs, rank, pools, &vars); - - state - } - Failure(vars, actual_type, expected_type, _bad_impl) => { - introduce(subs, rank, pools, &vars); - - let problem = TypeError::BadExpr( - *region, - category.clone(), - actual_type, - expectation.replace_ref(expected_type), - ); - - problems.push(problem); - - state - } - BadType(vars, problem) => { - introduce(subs, rank, pools, &vars); - - problems.push(TypeError::BadType(problem)); - - state - } - } - } - // Store(source, target, _filename, _linenr) => { - // // a special version of Eq that is used to store types in the AST. - // // IT DOES NOT REPORT ERRORS! - // let actual = type_to_var(subs, rank, pools, cached_aliases, source); - // let target = *target; - // - // match unify(subs, actual, target) { - // Success(vars) => { - // introduce(subs, rank, pools, &vars); - // - // state - // } - // Failure(vars, _actual_type, _expected_type, _bad_impl) => { - // introduce(subs, rank, pools, &vars); - // - // // ERROR NOT REPORTED - // - // state - // } - // BadType(vars, _problem) => { - // introduce(subs, rank, pools, &vars); - // - // // ERROR NOT REPORTED - // - // state - // } - // } - // } - Lookup(symbol, expectation, region) => { - match env.vars_by_symbol.get(&symbol) { - Some(var) => { - // Deep copy the vars associated with this symbol before unifying them. - // Otherwise, suppose we have this: - // - // identity = \a -> a - // - // x = identity 5 - // - // When we call (identity 5), it's important that we not unify - // on identity's original vars. If we do, the type of `identity` will be - // mutated to be `Int -> Int` instead of `a -> `, which would be incorrect; - // the type of `identity` is more general than that! - // - // Instead, we want to unify on a *copy* of its vars. If the copy unifies - // successfully (in this case, to `Int -> Int`), we can use that to - // infer the type of this lookup (in this case, `Int`) without ever - // having mutated the original. - // - // If this Lookup is targeting a value in another module, - // then we copy from that module's Subs into our own. If the value - // is being looked up in this module, then we use our Subs as both - // the source and destination. - let actual = deep_copy_var(subs, rank, pools, *var); - - let expected = type_to_var( - arena, - mempool, - subs, - rank, - pools, - cached_aliases, - expectation.get_type_ref(), - ); - - match unify(subs, actual, expected, Mode::EQ) { - Success { - vars, - must_implement_ability: _, - lambda_sets_to_specialize: _, // TODO ignored - } => { - // TODO(abilities) record deferred ability checks - introduce(subs, rank, pools, &vars); - - state - } - - Failure(vars, actual_type, expected_type, _bad_impl) => { - introduce(subs, rank, pools, &vars); - - let problem = TypeError::BadExpr( - *region, - Category::Lookup(*symbol), - actual_type, - expectation.shallow_clone().replace(expected_type), - ); - - problems.push(problem); - - state - } - BadType(vars, problem) => { - introduce(subs, rank, pools, &vars); - - problems.push(TypeError::BadType(problem)); - - state - } - } - } - None => { - problems.push(TypeError::UnexposedLookup(*symbol)); - - state - } - } - } - And(sub_constraints) => { - let mut state = state; - - for sub_constraint in sub_constraints.iter() { - state = solve( - arena, - mempool, - env, - state, - rank, - pools, - problems, - cached_aliases, - subs, - sub_constraint, - ); - } - - state - } - Pattern(region, category, typ, expectation) - | Present(typ, PresenceConstraint::Pattern(region, category, expectation)) => { - let actual = type_to_var(arena, mempool, subs, rank, pools, cached_aliases, typ); - let expected = type_to_var( - arena, - mempool, - subs, - rank, - pools, - cached_aliases, - expectation.get_type_ref(), - ); - - // TODO(ayazhafiz): presence constraints for Expr2/Type2 - match unify(subs, actual, expected, Mode::EQ) { - Success { - vars, - must_implement_ability: _, - lambda_sets_to_specialize: _, // TODO ignored - } => { - // TODO(abilities) record deferred ability checks - introduce(subs, rank, pools, &vars); - - state - } - Failure(vars, actual_type, expected_type, _bad_impl) => { - introduce(subs, rank, pools, &vars); - - let problem = TypeError::BadPattern( - *region, - category.clone(), - actual_type, - expectation.shallow_clone().replace(expected_type), - ); - - problems.push(problem); - - state - } - BadType(vars, problem) => { - introduce(subs, rank, pools, &vars); - - problems.push(TypeError::BadType(problem)); - - state - } - } - } - Let(let_con) => { - match &let_con.ret_constraint { - True if let_con.rigid_vars.is_empty() => { - introduce(subs, rank, pools, &let_con.flex_vars); - - // If the return expression is guaranteed to solve, - // solve the assignments themselves and move on. - solve( - arena, - mempool, - &env, - state, - rank, - pools, - problems, - cached_aliases, - subs, - &let_con.defs_constraint, - ) - } - ret_con if let_con.rigid_vars.is_empty() && let_con.flex_vars.is_empty() => { - let state = solve( - arena, - mempool, - env, - state, - rank, - pools, - problems, - cached_aliases, - subs, - &let_con.defs_constraint, - ); - - // Add a variable for each def to new_vars_by_env. - let mut local_def_vars = BumpMap::new_in(arena); - - for (symbol, typ) in let_con.def_types.iter() { - let var = - type_to_var(arena, mempool, subs, rank, pools, cached_aliases, typ); - - // TODO: region should come from typ - local_def_vars.insert( - *symbol, - Loc { - value: var, - region: Region::zero(), - }, - ); - } - - let mut new_env = env.clone(); - for (symbol, loc_var) in local_def_vars.iter() { - if !new_env.vars_by_symbol.contains_key(&symbol) { - new_env.vars_by_symbol.insert(*symbol, loc_var.value); - } - } - - let new_state = solve( - arena, - mempool, - &new_env, - state, - rank, - pools, - problems, - cached_aliases, - subs, - ret_con, - ); - - for (symbol, loc_var) in local_def_vars { - check_for_infinite_type(subs, problems, symbol, loc_var); - } - - new_state - } - ret_con => { - let rigid_vars = &let_con.rigid_vars; - let flex_vars = &let_con.flex_vars; - - // work in the next pool to localize header - let next_rank = rank.next(); - - // introduce variables - for &var in rigid_vars.iter().chain(flex_vars.iter()) { - subs.set_rank(var, next_rank); - } - - // determine the next pool - let next_pools; - if next_rank.into_usize() < pools.len() { - next_pools = pools - } else { - // we should be off by one at this point - debug_assert_eq!(next_rank.into_usize(), 1 + pools.len()); - pools.extend_to(next_rank.into_usize()); - next_pools = pools; - } - - let pool: &mut Vec = next_pools.get_mut(next_rank); - - // Replace the contents of this pool with rigid_vars and flex_vars - pool.clear(); - pool.reserve(rigid_vars.len() + flex_vars.len()); - pool.extend(rigid_vars.iter()); - pool.extend(flex_vars.iter()); - - // run solver in next pool - - // Add a variable for each def to local_def_vars. - let mut local_def_vars = BumpMap::new_in(arena); - - for (symbol, typ) in let_con.def_types.iter() { - let var = type_to_var( - arena, - mempool, - subs, - next_rank, - next_pools, - cached_aliases, - typ, - ); - - // TODO: region should come from type - local_def_vars.insert( - *symbol, - Loc { - value: var, - region: Region::zero(), - }, - ); - } - - // Solve the assignments' constraints first. - let State { - env: saved_env, - mark, - } = solve( - arena, - mempool, - &env, - state, - next_rank, - next_pools, - problems, - cached_aliases, - subs, - &let_con.defs_constraint, - ); - - let young_mark = mark; - let visit_mark = young_mark.next(); - let final_mark = visit_mark.next(); - - debug_assert_eq!( - { - let offenders = next_pools - .get(next_rank) - .iter() - .filter(|var| { - let current_rank = - subs.get_rank(roc_types::subs::Variable::clone(var)); - - current_rank.into_usize() > next_rank.into_usize() - }) - .collect::>(); - - let result = offenders.len(); - - if result > 0 { - dbg!(&subs, &offenders, &let_con.def_types); - } - - result - }, - 0 - ); - - // pop pool - generalize(subs, young_mark, visit_mark, next_rank, next_pools); - - next_pools.get_mut(next_rank).clear(); - - // check that things went well - debug_assert!({ - // NOTE the `subs.redundant` check is added for the uniqueness - // inference, and does not come from elm. It's unclear whether this is - // a bug with uniqueness inference (something is redundant that - // shouldn't be) or that it just never came up in elm. - let failing: Vec<_> = rigid_vars - .iter() - .filter(|&var| { - !subs.redundant(*var) && subs.get_rank(*var) != Rank::NONE - }) - .collect(); - - if !failing.is_empty() { - println!("Rigids {:?}", &rigid_vars); - println!("Failing {:?}", failing); - } - - failing.is_empty() - }); - - let mut new_env = env.clone(); - for (symbol, loc_var) in local_def_vars.iter() { - // when there are duplicates, keep the one from `env` - if !new_env.vars_by_symbol.contains_key(&symbol) { - new_env.vars_by_symbol.insert(*symbol, loc_var.value); - } - } - - // Note that this vars_by_symbol is the one returned by the - // previous call to solve() - let temp_state = State { - env: saved_env, - mark: final_mark, - }; - - // Now solve the body, using the new vars_by_symbol which includes - // the assignments' name-to-variable mappings. - let new_state = solve( - arena, - mempool, - &new_env, - temp_state, - rank, - next_pools, - problems, - cached_aliases, - subs, - &ret_con, - ); - - for (symbol, loc_var) in local_def_vars { - check_for_infinite_type(subs, problems, symbol, loc_var); - } - - new_state - } - } - } - Present(typ, PresenceConstraint::IsOpen) => { - let actual = type_to_var(arena, mempool, subs, rank, pools, cached_aliases, typ); - let mut new_desc = subs.get(actual); - match new_desc.content { - Content::Structure(FlatType::TagUnion(tags, _)) => { - let new_ext = subs.fresh_unnamed_flex_var(); - let new_union = Content::Structure(FlatType::TagUnion(tags, new_ext)); - new_desc.content = new_union; - subs.set(actual, new_desc); - state - } - _ => { - // Today, an "open" constraint doesn't affect any types - // other than tag unions. Recursive tag unions are constructed - // at a later time (during occurs checks after tag unions are - // resolved), so that's not handled here either. - // NB: Handle record types here if we add presence constraints - // to their type inference as well. - state - } - } - } - Present(typ, PresenceConstraint::IncludesTag(tag_name, tys)) => { - let actual = type_to_var(arena, mempool, subs, rank, pools, cached_aliases, typ); - let tag_ty = Type2::TagUnion( - PoolVec::new( - std::iter::once(( - tag_name.clone(), - PoolVec::new(tys.into_iter().map(ShallowClone::shallow_clone), mempool), - )), - mempool, - ), - mempool.add(Type2::EmptyTagUnion), - ); - let includes = type_to_var(arena, mempool, subs, rank, pools, cached_aliases, &tag_ty); - - match unify(subs, actual, includes, Mode::PRESENT) { - Success { - vars, - must_implement_ability: _, - lambda_sets_to_specialize: _, // TODO ignored - } => { - // TODO(abilities) record deferred ability checks - introduce(subs, rank, pools, &vars); - - state - } - Failure(vars, actual_type, expected_type, _bad_impl) => { - introduce(subs, rank, pools, &vars); - - // TODO: do we need a better error type here? - let problem = TypeError::BadExpr( - Region::zero(), - Category::When, - actual_type, - Expected::NoExpectation(expected_type), - ); - - problems.push(problem); - - state - } - BadType(vars, problem) => { - introduce(subs, rank, pools, &vars); - - problems.push(TypeError::BadType(problem)); - - state - } - } - } - } -} - -fn type_to_var<'a>( - arena: &'a Bump, - mempool: &mut Pool, - subs: &mut Subs, - rank: Rank, - pools: &mut Pools, - cached: &mut MutMap, - typ: &Type2, -) -> Variable { - type_to_variable(arena, mempool, subs, rank, pools, cached, typ) -} - -/// Abusing existing functions for our purposes -/// this is to put a solved type back into subs -pub fn insert_type_into_subs<'a>( - arena: &'a Bump, - mempool: &mut Pool, - subs: &mut Subs, - typ: &Type2, -) -> Variable { - let rank = Rank::NONE; - let mut pools = Pools::default(); - let mut cached = MutMap::default(); - - type_to_variable(arena, mempool, subs, rank, &mut pools, &mut cached, typ) -} - -fn type_to_variable<'a>( - arena: &'a Bump, - mempool: &Pool, - subs: &mut Subs, - rank: Rank, - pools: &mut Pools, - cached: &mut MutMap, - typ: &Type2, -) -> Variable { - use Type2::*; - - match typ { - Variable(var) => *var, - Apply(symbol, args) => { - let mut arg_vars = Vec::with_capacity(args.len()); - for var_id in args.iter_node_ids() { - let arg = mempool.get(var_id); - arg_vars.push(type_to_variable( - arena, mempool, subs, rank, pools, cached, arg, - )) - } - - let arg_vars = VariableSubsSlice::insert_into_subs(subs, arg_vars); - let flat_type = FlatType::Apply(*symbol, arg_vars); - let content = Content::Structure(flat_type); - - register(subs, rank, pools, content) - } - - EmptyRec => roc_types::subs::Variable::EMPTY_RECORD, - EmptyTagUnion => roc_types::subs::Variable::EMPTY_TAG_UNION, - - Record(fields, ext_id) => { - let mut field_vars = Vec::new(); - - for node_id in fields.iter_node_ids() { - use RecordField::*; - - let (field, field_type) = mempool.get(node_id); - - let field_var = match field_type { - Required(type_id) => Required(type_to_variable( - arena, - mempool, - subs, - rank, - pools, - cached, - mempool.get(*type_id), - )), - Optional(type_id) => Optional(type_to_variable( - arena, - mempool, - subs, - rank, - pools, - cached, - mempool.get(*type_id), - )), - Demanded(type_id) => Demanded(type_to_variable( - arena, - mempool, - subs, - rank, - pools, - cached, - mempool.get(*type_id), - )), - }; - - field_vars.push((field.as_str(mempool).into(), field_var)); - } - - let ext = mempool.get(*ext_id); - let temp_ext_var = type_to_variable(arena, mempool, subs, rank, pools, cached, ext); - - let (it, new_ext_var) = - gather_fields_unsorted_iter(subs, RecordFields::empty(), temp_ext_var) - .expect("Something ended up weird in this record type"); - - let it = it - .into_iter() - .map(|(field, field_type)| (field.clone(), field_type)); - - field_vars.extend(it); - field_vars.sort_unstable_by(RecordFields::compare); - - let record_fields = RecordFields::insert_into_subs(subs, field_vars); - - let content = Content::Structure(FlatType::Record(record_fields, new_ext_var)); - - register(subs, rank, pools, content) - } - - Alias(symbol, args, alias_type_id) | Opaque(symbol, args, alias_type_id) => { - // TODO cache in uniqueness inference gives problems! all Int's get the same uniqueness var! - // Cache aliases without type arguments. Commonly used aliases like `Int` would otherwise get O(n) - // different variables (once for each occurrence). The recursion restriction is required - // for uniqueness types only: recursive aliases "introduce" an unbound uniqueness - // attribute in the body, when - // - // Peano : [S Peano, Z] - // - // becomes - // - // Peano : [S (Attr u Peano), Z] - // - // This `u` variable can be different between lists, so giving just one variable to - // this type is incorrect. - // TODO does caching work at all with uniqueness types? even Int then hides a uniqueness variable - - let alias_type = mempool.get(*alias_type_id); - let is_recursive = false; // alias_type.is_recursive(); - let no_args = args.is_empty(); - /* - if no_args && !is_recursive { - if let Some(var) = cached.get(symbol) { - return *var; - } - } - */ - - let mut arg_vars = Vec::with_capacity(args.len()); - - for arg_type_id in args.iter(mempool) { - let arg_type = mempool.get(*arg_type_id); - - let arg_var = type_to_variable(arena, mempool, subs, rank, pools, cached, arg_type); - - arg_vars.push(arg_var); - } - - let arg_vars = AliasVariables::insert_into_subs(subs, arg_vars, []); - - let alias_var = type_to_variable(arena, mempool, subs, rank, pools, cached, alias_type); - - let kind = match typ { - Alias(..) => AliasKind::Structural, - Opaque(..) => AliasKind::Opaque, - _ => internal_error!(), - }; - let content = Content::Alias(*symbol, arg_vars, alias_var, kind); - - let result = register(subs, rank, pools, content); - - if no_args && !is_recursive { - // cached.insert(*symbol, result); - } - - result - } - TagUnion(tags, ext_id) => { - let ext = mempool.get(*ext_id); - - let (union_tags, ext) = - type_to_union_tags(arena, mempool, subs, rank, pools, cached, tags, ext); - let content = Content::Structure(FlatType::TagUnion(union_tags, ext)); - - register(subs, rank, pools, content) - } - // This case is important for the rank of boolean variables - Function(arg_vars, closure_type_id, ret_type_id) => { - let closure_type = mempool.get(*closure_type_id); - let ret_type = mempool.get(*ret_type_id); - - let mut new_arg_vars = Vec::with_capacity(arg_vars.len()); - - for var_id in arg_vars.iter_node_ids() { - let arg = mempool.get(var_id); - let var = type_to_variable(arena, mempool, subs, rank, pools, cached, arg); - new_arg_vars.push(var) - } - - let arg_vars = VariableSubsSlice::insert_into_subs(subs, new_arg_vars); - - let ret_var = type_to_variable(arena, mempool, subs, rank, pools, cached, ret_type); - let closure_var = - type_to_variable(arena, mempool, subs, rank, pools, cached, closure_type); - - let content = Content::Structure(FlatType::Func(arg_vars, closure_var, ret_var)); - - register(subs, rank, pools, content) - } - other => todo!("not implemented {:?}", &other), - // RecursiveTagUnion(rec_var, tags, ext) => { - // let mut tag_vars = MutMap::default(); - // - // for (tag, tag_argument_types) in tags { - // let mut tag_argument_vars = Vec::with_capacity(tag_argument_types.len()); - // - // for arg_type in tag_argument_types { - // tag_argument_vars.push(type_to_variable(subs, rank, pools, cached, arg_type)); - // } - // - // tag_vars.insert(tag.clone(), tag_argument_vars); - // } - // - // let temp_ext_var = type_to_variable(subs, rank, pools, cached, ext); - // let mut ext_tag_vec = Vec::new(); - // let new_ext_var = match roc_types::pretty_print::chase_ext_tag_union( - // subs, - // temp_ext_var, - // &mut ext_tag_vec, - // ) { - // Ok(()) => Variable::EMPTY_TAG_UNION, - // Err((new, _)) => new, - // }; - // tag_vars.extend(ext_tag_vec.into_iter()); - // - // let content = - // Content::Structure(FlatType::RecursiveTagUnion(*rec_var, tag_vars, new_ext_var)); - // - // let tag_union_var = register(subs, rank, pools, content); - // - // subs.set_content( - // *rec_var, - // Content::RecursionVar { - // opt_name: None, - // structure: tag_union_var, - // }, - // ); - // - // tag_union_var - // } - // HostExposedAlias { - // name: symbol, - // arguments: args, - // actual: alias_type, - // actual_var, - // .. - // } => { - // let mut arg_vars = Vec::with_capacity(args.len()); - // let mut new_aliases = ImMap::default(); - // - // for (arg, arg_type) in args { - // let arg_var = type_to_variable(subs, rank, pools, cached, arg_type); - // - // arg_vars.push((arg.clone(), arg_var)); - // new_aliases.insert(arg.clone(), arg_var); - // } - // - // let alias_var = type_to_variable(subs, rank, pools, cached, alias_type); - // - // // unify the actual_var with the result var - // // this can be used to access the type of the actual_var - // // to determine its layout later - // // subs.set_content(*actual_var, descriptor.content); - // - // //subs.set(*actual_var, descriptor.clone()); - // let content = Content::Alias(*symbol, arg_vars, alias_var); - // - // let result = register(subs, rank, pools, content); - // - // // We only want to unify the actual_var with the alias once - // // if it's already redirected (and therefore, redundant) - // // don't do it again - // if !subs.redundant(*actual_var) { - // let descriptor = subs.get(result); - // subs.union(result, *actual_var, descriptor); - // } - // - // result - // } - // Erroneous(problem) => { - // let content = Content::Structure(FlatType::Erroneous(problem.clone())); - // - // register(subs, rank, pools, content) - // } - } -} - -fn type_to_union_tags<'a>( - arena: &'a Bump, - mempool: &Pool, - subs: &mut Subs, - rank: Rank, - pools: &mut Pools, - cached: &mut MutMap, - tags: &PoolVec<(TagName, PoolVec)>, - ext: &Type2, -) -> (UnionTags, Variable) { - let mut tag_vars = Vec::with_capacity(tags.len()); - - let mut tag_argument_vars = Vec::new(); - for id in tags.iter_node_ids() { - let (tag, tag_argument_types) = mempool.get(id); - for arg_id in tag_argument_types.iter_node_ids() { - let arg_type = mempool.get(arg_id); - let new_var = type_to_variable(arena, mempool, subs, rank, pools, cached, arg_type); - tag_argument_vars.push(new_var); - } - - let new_slice = VariableSubsSlice::insert_into_subs(subs, tag_argument_vars.drain(..)); - - tag_vars.push((tag.clone(), new_slice)); - } - - let temp_ext_var = type_to_variable(arena, mempool, subs, rank, pools, cached, ext); - - let ext = { - let (it, ext) = - roc_types::types::gather_tags_unsorted_iter(subs, UnionTags::default(), temp_ext_var); - - tag_vars.extend(it.map(|(n, v)| (n.clone(), v))); - tag_vars.sort_unstable_by(|(a, _), (b, _)| a.cmp(b)); - - // deduplicate, keeping the right-most occurrence of a tag name - let mut i = 0; - - while i < tag_vars.len() { - match (tag_vars.get(i), tag_vars.get(i + 1)) { - (Some((t1, _)), Some((t2, _))) => { - if t1 == t2 { - tag_vars.remove(i); - } else { - i += 1; - } - } - _ => break, - } - } - - ext - }; - - (UnionTags::insert_slices_into_subs(subs, tag_vars), ext) -} - -fn check_for_infinite_type( - subs: &mut Subs, - problems: &mut Vec, - symbol: Symbol, - loc_var: Loc, -) { - let var = loc_var.value; - - while let Err((recursive, _chain)) = subs.occurs(var) { - let description = subs.get(recursive); - let content = description.content; - - // try to make a tag union recursive, see if that helps - match content { - Content::Structure(FlatType::TagUnion(tags, ext_var)) => { - let rec_var = subs.fresh_unnamed_flex_var(); - subs.set_rank(rec_var, description.rank); - subs.set_content( - rec_var, - Content::RecursionVar { - opt_name: None, - structure: recursive, - }, - ); - - let mut new_tags = Vec::with_capacity(tags.len()); - - for (name_index, slice_index) in tags.iter_all() { - let slice = subs[slice_index]; - - let mut new_vars = Vec::new(); - for var_index in slice { - let var = subs[var_index]; - new_vars.push(subs.explicit_substitute(recursive, rec_var, var)); - } - - new_tags.push((subs[name_index].clone(), new_vars)); - } - - let new_ext_var = subs.explicit_substitute(recursive, rec_var, ext_var); - - let new_tags = UnionTags::insert_into_subs(subs, new_tags); - - let flat_type = FlatType::RecursiveTagUnion(rec_var, new_tags, new_ext_var); - - subs.set_content(recursive, Content::Structure(flat_type)); - } - - _other => circular_error(subs, problems, symbol, &loc_var), - } - } -} - -fn circular_error( - subs: &mut Subs, - problems: &mut Vec, - symbol: Symbol, - loc_var: &Loc, -) { - let var = loc_var.value; - let (error_type, _) = subs.var_to_error_type(var); - let problem = TypeError::CircularType(loc_var.region, symbol, error_type); - - subs.set_content(var, Content::Error); - - problems.push(problem); -} - -fn generalize( - subs: &mut Subs, - young_mark: Mark, - visit_mark: Mark, - young_rank: Rank, - pools: &mut Pools, -) { - let young_vars = pools.get(young_rank); - let rank_table = pool_to_rank_table(subs, young_mark, young_rank, young_vars); - - // Get the ranks right for each entry. - // Start at low ranks so we only have to pass over the information once. - for (index, table) in rank_table.iter().enumerate() { - for &var in table.iter() { - adjust_rank(subs, young_mark, visit_mark, Rank::from(index), var); - } - } - - let (last_pool, all_but_last_pool) = rank_table.split_last(); - - // For variables that have rank lowerer than young_rank, register them in - // the appropriate old pool if they are not redundant. - for vars in all_but_last_pool { - for &var in vars { - if !subs.redundant(var) { - let rank = subs.get_rank(var); - - pools.get_mut(rank).push(var); - } - } - } - - // For variables with rank young_rank, if rank < young_rank: register in old pool, - // otherwise generalize - for &var in last_pool { - if !subs.redundant(var) { - let desc_rank = subs.get_rank(var); - - if desc_rank < young_rank { - pools.get_mut(desc_rank).push(var); - } else { - subs.set_rank(var, Rank::NONE); - } - } - } -} - -fn pool_to_rank_table( - subs: &mut Subs, - young_mark: Mark, - young_rank: Rank, - young_vars: &[Variable], -) -> Pools { - let mut pools = Pools::new(young_rank.into_usize() + 1); - - // Sort the variables into buckets by rank. - for &var in young_vars.iter() { - let rank = subs.get_rank(var); - subs.set_mark(var, young_mark); - - debug_assert!(rank.into_usize() < young_rank.into_usize() + 1); - pools.get_mut(rank).push(var); - } - - pools -} - -/// Adjust variable ranks such that ranks never increase as you move deeper. -/// This way the outermost rank is representative of the entire structure. -fn adjust_rank( - subs: &mut Subs, - young_mark: Mark, - visit_mark: Mark, - group_rank: Rank, - var: Variable, -) -> Rank { - let desc = subs.get(var); - - if desc.mark == young_mark { - let Descriptor { - content, - rank: _, - mark: _, - copy, - } = desc; - - // Mark the variable as visited before adjusting content, as it may be cyclic. - subs.set_mark(var, visit_mark); - - let max_rank = adjust_rank_content(subs, young_mark, visit_mark, group_rank, &content); - - subs.set( - var, - Descriptor { - content, - rank: max_rank, - mark: visit_mark, - copy, - }, - ); - - max_rank - } else if desc.mark == visit_mark { - // nothing changes - desc.rank - } else { - let mut desc = desc; - - let min_rank = group_rank.min(desc.rank); - - // TODO from elm-compiler: how can min_rank ever be group_rank? - desc.rank = min_rank; - desc.mark = visit_mark; - - subs.set(var, desc); - - min_rank - } -} - -fn adjust_rank_content( - subs: &mut Subs, - young_mark: Mark, - visit_mark: Mark, - group_rank: Rank, - content: &Content, -) -> Rank { - use roc_types::subs::Content::*; - use roc_types::subs::FlatType::*; - - match content { - FlexVar(_) | RigidVar(_) | FlexAbleVar(..) | RigidAbleVar(..) | Error => group_rank, - - RecursionVar { .. } => group_rank, - - Structure(flat_type) => { - match flat_type { - Apply(_, args) => { - let mut rank = Rank::toplevel(); - - for var_index in args.into_iter() { - let var = subs[var_index]; - rank = rank.max(adjust_rank(subs, young_mark, visit_mark, group_rank, var)); - } - - rank - } - - Func(arg_vars, closure_var, ret_var) => { - let mut rank = adjust_rank(subs, young_mark, visit_mark, group_rank, *ret_var); - - // TODO investigate further. - // - // My theory is that because the closure_var contains variables already - // contained in the signature only, it does not need to be part of the rank - // calculuation - if true { - rank = rank.max(adjust_rank( - subs, - young_mark, - visit_mark, - group_rank, - *closure_var, - )); - } - - for index in arg_vars.into_iter() { - let var = subs[index]; - rank = rank.max(adjust_rank(subs, young_mark, visit_mark, group_rank, var)); - } - - rank - } - - EmptyRecord => { - // from elm-compiler: THEORY: an empty record never needs to get generalized - Rank::toplevel() - } - - EmptyTagUnion => Rank::toplevel(), - - Record(fields, ext_var) => { - let mut rank = adjust_rank(subs, young_mark, visit_mark, group_rank, *ext_var); - - for index in fields.iter_variables() { - let var = subs[index]; - rank = rank.max(adjust_rank(subs, young_mark, visit_mark, group_rank, var)); - } - - rank - } - - TagUnion(tags, ext_var) => { - let mut rank = adjust_rank(subs, young_mark, visit_mark, group_rank, *ext_var); - - for (_, index) in tags.iter_all() { - let slice = subs[index]; - for var_index in slice { - let var = subs[var_index]; - rank = rank - .max(adjust_rank(subs, young_mark, visit_mark, group_rank, var)); - } - } - - rank - } - - FunctionOrTagUnion(_, _, ext_var) => { - adjust_rank(subs, young_mark, visit_mark, group_rank, *ext_var) - } - - RecursiveTagUnion(rec_var, tags, ext_var) => { - let mut rank = adjust_rank(subs, young_mark, visit_mark, group_rank, *ext_var); - - for (_, index) in tags.iter_all() { - let slice = subs[index]; - for var_index in slice { - let var = subs[var_index]; - rank = rank - .max(adjust_rank(subs, young_mark, visit_mark, group_rank, var)); - } - } - - // THEORY: the recursion var has the same rank as the tag union itself - // all types it uses are also in the tags already, so it cannot influence the - // rank - debug_assert!( - rank >= adjust_rank(subs, young_mark, visit_mark, group_rank, *rec_var) - ); - - rank - } - - Erroneous(_) => group_rank, - } - } - - LambdaSet(subs::LambdaSet { - solved, - recursion_var, - // TODO: handle unspecialized - unspecialized: _, - }) => { - let mut rank = group_rank; - - for (_, index) in solved.iter_all() { - let slice = subs[index]; - for var_index in slice { - let var = subs[var_index]; - rank = rank.max(adjust_rank(subs, young_mark, visit_mark, group_rank, var)); - } - } - - if let Some(rec_var) = recursion_var.into_variable() { - // THEORY: the recursion var has the same rank as the tag union itself - // all types it uses are also in the tags already, so it cannot influence the - // rank - debug_assert!( - rank >= adjust_rank(subs, young_mark, visit_mark, group_rank, rec_var) - ); - } - - rank - } - - Alias(_, args, real_var, _) => { - let mut rank = Rank::toplevel(); - - for var_index in args.all_variables() { - let var = subs[var_index]; - rank = rank.max(adjust_rank(subs, young_mark, visit_mark, group_rank, var)); - } - - // from elm-compiler: THEORY: anything in the real_var would be Rank::toplevel() - // this theory is not true in Roc! aliases of function types capture the closure var - rank = rank.max(adjust_rank( - subs, young_mark, visit_mark, group_rank, *real_var, - )); - - rank - } - - RangedNumber(typ, _vars) => adjust_rank(subs, young_mark, visit_mark, group_rank, *typ), - } -} - -/// Introduce some variables to Pools at the given rank. -/// Also, set each of their ranks in Subs to be the given rank. -fn introduce(subs: &mut Subs, rank: Rank, pools: &mut Pools, vars: &[Variable]) { - let pool: &mut Vec = pools.get_mut(rank); - - for &var in vars.iter() { - subs.set_rank(var, rank); - } - - pool.extend(vars); -} - -/// Function that converts rigids variables to flex variables -/// this is used during the monomorphization process -pub fn instantiate_rigids(subs: &mut Subs, var: Variable) { - let rank = Rank::NONE; - let mut pools = Pools::default(); - - instantiate_rigids_help(subs, rank, &mut pools, var); -} - -fn instantiate_rigids_help( - subs: &mut Subs, - max_rank: Rank, - pools: &mut Pools, - var: Variable, -) -> Variable { - use roc_types::subs::Content::*; - use roc_types::subs::FlatType::*; - - let desc = subs.get_without_compacting(var); - - if let Some(copy) = desc.copy.into_variable() { - return copy; - } - - let make_descriptor = |content| Descriptor { - content, - rank: max_rank, - mark: Mark::NONE, - copy: OptVariable::NONE, - }; - - let content = desc.content; - let copy = var; - - pools.get_mut(max_rank).push(copy); - - // Link the original variable to the new variable. This lets us - // avoid making multiple copies of the variable we are instantiating. - // - // Need to do this before recursively copying to avoid looping. - subs.set( - var, - Descriptor { - content: content.clone(), - rank: desc.rank, - mark: Mark::NONE, - copy: copy.into(), - }, - ); - - // Now we recursively copy the content of the variable. - // We have already marked the variable as copied, so we - // will not repeat this work or crawl this variable again. - match content { - Structure(flat_type) => { - match flat_type { - Apply(_, args) => { - for var_index in args.into_iter() { - let var = subs[var_index]; - instantiate_rigids_help(subs, max_rank, pools, var); - } - } - - Func(arg_vars, closure_var, ret_var) => { - instantiate_rigids_help(subs, max_rank, pools, ret_var); - instantiate_rigids_help(subs, max_rank, pools, closure_var); - - for index in arg_vars.into_iter() { - let var = subs[index]; - instantiate_rigids_help(subs, max_rank, pools, var); - } - } - - EmptyRecord | EmptyTagUnion | Erroneous(_) => {} - - Record(fields, ext_var) => { - for index in fields.iter_variables() { - let var = subs[index]; - instantiate_rigids_help(subs, max_rank, pools, var); - } - - instantiate_rigids_help(subs, max_rank, pools, ext_var); - } - - TagUnion(tags, ext_var) => { - for (_, index) in tags.iter_all() { - let slice = subs[index]; - for var_index in slice { - let var = subs[var_index]; - instantiate_rigids_help(subs, max_rank, pools, var); - } - } - - instantiate_rigids_help(subs, max_rank, pools, ext_var); - } - - FunctionOrTagUnion(_tag_name, _symbol, ext_var) => { - instantiate_rigids_help(subs, max_rank, pools, ext_var); - } - - RecursiveTagUnion(rec_var, tags, ext_var) => { - instantiate_rigids_help(subs, max_rank, pools, rec_var); - - for (_, index) in tags.iter_all() { - let slice = subs[index]; - for var_index in slice { - let var = subs[var_index]; - instantiate_rigids_help(subs, max_rank, pools, var); - } - } - - instantiate_rigids_help(subs, max_rank, pools, ext_var); - } - }; - } - - FlexVar(_) | FlexAbleVar(_, _) | Error => {} - - RecursionVar { structure, .. } => { - instantiate_rigids_help(subs, max_rank, pools, structure); - } - - RigidVar(name) => { - // what it's all about: convert the rigid var into a flex var - subs.set(copy, make_descriptor(FlexVar(Some(name)))); - } - - RigidAbleVar(name, ability) => { - // what it's all about: convert the rigid var into a flex var - subs.set(copy, make_descriptor(FlexAbleVar(Some(name), ability))); - } - - Alias(_, args, real_type_var, _) => { - for var_index in args.all_variables() { - let var = subs[var_index]; - instantiate_rigids_help(subs, max_rank, pools, var); - } - - instantiate_rigids_help(subs, max_rank, pools, real_type_var); - } - - LambdaSet(subs::LambdaSet { - solved, - recursion_var, - // TODO: handle unspecialized - unspecialized: _, - }) => { - if let Some(rec_var) = recursion_var.into_variable() { - instantiate_rigids_help(subs, max_rank, pools, rec_var); - } - - for (_, index) in solved.iter_all() { - let slice = subs[index]; - for var_index in slice { - let var = subs[var_index]; - instantiate_rigids_help(subs, max_rank, pools, var); - } - } - } - - RangedNumber(typ, _vars) => { - instantiate_rigids_help(subs, max_rank, pools, typ); - } - } - - var -} - -fn deep_copy_var(subs: &mut Subs, rank: Rank, pools: &mut Pools, var: Variable) -> Variable { - let copy = deep_copy_var_help(subs, rank, pools, var); - - copy -} - -fn deep_copy_var_help( - subs: &mut Subs, - max_rank: Rank, - pools: &mut Pools, - var: Variable, -) -> Variable { - use roc_types::subs::Content::*; - use roc_types::subs::FlatType::*; - - let desc = subs.get_without_compacting(var); - - if let Some(copy) = desc.copy.into_variable() { - return copy; - } else if desc.rank != Rank::NONE { - return var; - } - - let make_descriptor = |content| Descriptor { - content, - rank: max_rank, - mark: Mark::NONE, - copy: OptVariable::NONE, - }; - - let content = desc.content; - let copy = subs.fresh(make_descriptor(content.clone())); - - pools.get_mut(max_rank).push(copy); - - // Link the original variable to the new variable. This lets us - // avoid making multiple copies of the variable we are instantiating. - // - // Need to do this before recursively copying to avoid looping. - subs.set( - var, - Descriptor { - content: content.clone(), - rank: desc.rank, - mark: Mark::NONE, - copy: copy.into(), - }, - ); - - // Now we recursively copy the content of the variable. - // We have already marked the variable as copied, so we - // will not repeat this work or crawl this variable again. - match content { - Structure(flat_type) => { - let new_flat_type = match flat_type { - Apply(symbol, args) => { - let mut new_arg_vars = Vec::with_capacity(args.len()); - - for index in args.into_iter() { - let var = subs[index]; - let copy_var = deep_copy_var_help(subs, max_rank, pools, var); - new_arg_vars.push(copy_var); - } - - let arg_vars = VariableSubsSlice::insert_into_subs(subs, new_arg_vars); - - Apply(symbol, arg_vars) - } - - Func(arg_vars, closure_var, ret_var) => { - let new_ret_var = deep_copy_var_help(subs, max_rank, pools, ret_var); - let new_closure_var = deep_copy_var_help(subs, max_rank, pools, closure_var); - - let mut new_arg_vars = Vec::with_capacity(arg_vars.len()); - - for index in arg_vars.into_iter() { - let var = subs[index]; - let copy_var = deep_copy_var_help(subs, max_rank, pools, var); - new_arg_vars.push(copy_var); - } - - let arg_vars = VariableSubsSlice::insert_into_subs(subs, new_arg_vars); - - Func(arg_vars, new_closure_var, new_ret_var) - } - - same @ EmptyRecord | same @ EmptyTagUnion | same @ Erroneous(_) => same, - - Record(fields, ext_var) => { - let record_fields = { - let mut new_vars = Vec::with_capacity(fields.len()); - - for index in fields.iter_variables() { - let var = subs[index]; - let copy_var = deep_copy_var_help(subs, max_rank, pools, var); - - new_vars.push(copy_var); - } - - let field_names_start = subs.field_names.len() as u32; - let variables_start = subs.variables.len() as u32; - let field_types_start = subs.record_fields.len() as u32; - - let mut length = 0; - - for ((i1, _, i3), var) in fields.iter_all().zip(new_vars) { - let record_field = subs[i3].map(|_| var); - - subs.field_names.push(subs[i1].clone()); - subs.record_fields.push(record_field.map(|_| ())); - subs.variables.push(*record_field.as_inner()); - - length += 1; - } - - RecordFields { - length, - field_names_start, - variables_start, - field_types_start, - } - }; - - Record( - record_fields, - deep_copy_var_help(subs, max_rank, pools, ext_var), - ) - } - - TagUnion(tags, ext_var) => { - let mut new_variable_slices = Vec::with_capacity(tags.len()); - - let mut new_variables = Vec::new(); - for index in tags.variables() { - let slice = subs[index]; - for var_index in slice { - let var = subs[var_index]; - let new_var = deep_copy_var_help(subs, max_rank, pools, var); - new_variables.push(new_var); - } - - let new_slice = - VariableSubsSlice::insert_into_subs(subs, new_variables.drain(..)); - - new_variable_slices.push(new_slice); - } - - let new_variables = { - let start = subs.variable_slices.len() as u32; - let length = new_variable_slices.len() as u16; - subs.variable_slices.extend(new_variable_slices); - - SubsSlice::new(start, length) - }; - - let union_tags = UnionTags::from_slices(tags.labels(), new_variables); - - let new_ext = deep_copy_var_help(subs, max_rank, pools, ext_var); - TagUnion(union_tags, new_ext) - } - - FunctionOrTagUnion(tag_name, symbol, ext_var) => FunctionOrTagUnion( - tag_name, - symbol, - deep_copy_var_help(subs, max_rank, pools, ext_var), - ), - - RecursiveTagUnion(rec_var, tags, ext_var) => { - let mut new_variable_slices = Vec::with_capacity(tags.len()); - - let mut new_variables = Vec::new(); - for index in tags.variables() { - let slice = subs[index]; - for var_index in slice { - let var = subs[var_index]; - let new_var = deep_copy_var_help(subs, max_rank, pools, var); - new_variables.push(new_var); - } - - let new_slice = - VariableSubsSlice::insert_into_subs(subs, new_variables.drain(..)); - - new_variable_slices.push(new_slice); - } - - let new_variables = { - let start = subs.variable_slices.len() as u32; - let length = new_variable_slices.len() as u16; - subs.variable_slices.extend(new_variable_slices); - - SubsSlice::new(start, length) - }; - - let union_tags = UnionTags::from_slices(tags.labels(), new_variables); - - let new_ext = deep_copy_var_help(subs, max_rank, pools, ext_var); - let new_rec_var = deep_copy_var_help(subs, max_rank, pools, rec_var); - FlatType::RecursiveTagUnion(new_rec_var, union_tags, new_ext) - } - }; - - subs.set(copy, make_descriptor(Structure(new_flat_type))); - - copy - } - - FlexVar(_) | FlexAbleVar(_, _) | Error => copy, - - RecursionVar { - opt_name, - structure, - } => { - let new_structure = deep_copy_var_help(subs, max_rank, pools, structure); - - subs.set( - copy, - make_descriptor(RecursionVar { - opt_name, - structure: new_structure, - }), - ); - - copy - } - - RigidVar(name) => { - subs.set(copy, make_descriptor(FlexVar(Some(name)))); - - copy - } - - RigidAbleVar(name, ability) => { - subs.set(copy, make_descriptor(FlexAbleVar(Some(name), ability))); - - copy - } - - Alias(symbol, mut args, real_type_var, kind) => { - let mut new_args = Vec::with_capacity(args.all_variables().len()); - - for var_index in args.all_variables() { - let var = subs[var_index]; - let new_var = deep_copy_var_help(subs, max_rank, pools, var); - new_args.push(new_var); - } - - args.replace_variables(subs, new_args); - - let new_real_type_var = deep_copy_var_help(subs, max_rank, pools, real_type_var); - let new_content = Alias(symbol, args, new_real_type_var, kind); - - subs.set(copy, make_descriptor(new_content)); - - copy - } - - LambdaSet(subs::LambdaSet { - solved, - recursion_var, - unspecialized, - }) => { - let mut new_variable_slices = Vec::with_capacity(solved.len()); - - let mut new_variables = Vec::new(); - for index in solved.variables() { - let slice = subs[index]; - for var_index in slice { - let var = subs[var_index]; - let new_var = deep_copy_var_help(subs, max_rank, pools, var); - new_variables.push(new_var); - } - - let new_slice = VariableSubsSlice::insert_into_subs(subs, new_variables.drain(..)); - - new_variable_slices.push(new_slice); - } - - let new_variables = { - let start = subs.variable_slices.len() as u32; - let length = new_variable_slices.len() as u16; - subs.variable_slices.extend(new_variable_slices); - - SubsSlice::new(start, length) - }; - - let new_solved = UnionLambdas::from_slices(solved.labels(), new_variables); - let new_rec_var = - recursion_var.map(|rec_var| deep_copy_var_help(subs, max_rank, pools, rec_var)); - - let new_content = LambdaSet(subs::LambdaSet { - solved: new_solved, - recursion_var: new_rec_var, - // TODO: actually copy - unspecialized, - }); - - subs.set(copy, make_descriptor(new_content)); - - copy - } - - RangedNumber(typ, vars) => { - let new_real_type = deep_copy_var_help(subs, max_rank, pools, typ); - let new_content = RangedNumber(new_real_type, vars); - - subs.set(copy, make_descriptor(new_content)); - - copy - } - } -} - -fn register(subs: &mut Subs, rank: Rank, pools: &mut Pools, content: Content) -> Variable { - let var = subs.fresh(Descriptor { - content, - rank, - mark: Mark::NONE, - copy: OptVariable::NONE, - }); - - pools.get_mut(rank).push(var); - - var -} diff --git a/bindgen/Cargo.toml b/bindgen/Cargo.toml deleted file mode 100644 index 3274ed76de..0000000000 --- a/bindgen/Cargo.toml +++ /dev/null @@ -1,42 +0,0 @@ -[package] -name = "roc-bindgen" -version = "0.1.0" -authors = ["The Roc Contributors"] -license = "UPL-1.0" -repository = "https://github.com/rtfeldman/roc" -edition = "2021" -description = "A CLI for roc-bindgen" - -[[bin]] -name = "roc-bindgen" -path = "src/main.rs" -test = false -bench = false - -[dependencies] -roc_std = { path = "../roc_std" } -roc_can = { path = "../compiler/can" } -roc_mono = { path = "../compiler/mono" } -roc_load = { path = "../compiler/load" } -roc_reporting = { path = "../reporting" } -roc_types = { path = "../compiler/types" } -roc_builtins = { path = "../compiler/builtins" } -roc_module = { path = "../compiler/module" } -roc_collections = { path = "../compiler/collections" } -roc_target = { path = "../compiler/roc_target" } -roc_error_macros = { path = "../error_macros" } -bumpalo = { version = "3.8.0", features = ["collections"] } -target-lexicon = "0.12.3" -clap = { version = "3.1.15", default-features = false, features = ["std", "color", "suggestions", "derive"] } -strum = "0.24.0" -strum_macros = "0.24" -indexmap = "1.8.1" - -[dev-dependencies] -pretty_assertions = "1.0.0" -tempfile = "3.2.0" -indoc = "1.0.3" -cli_utils = { path = "../cli_utils" } -roc_test_utils = { path = "../test_utils" } -dircpy = "0.3.9" -ctor = "0.1.22" diff --git a/bindgen/src/bindgen.rs b/bindgen/src/bindgen.rs deleted file mode 100644 index 6cf203f4e8..0000000000 --- a/bindgen/src/bindgen.rs +++ /dev/null @@ -1,495 +0,0 @@ -use crate::structs::Structs; -use crate::types::{RocTagUnion, TypeId, Types}; -use crate::{ - enums::Enums, - types::{RocNum, RocType}, -}; -use bumpalo::Bump; -use roc_builtins::bitcode::{FloatWidth::*, IntWidth::*}; -use roc_collections::VecMap; -use roc_module::ident::TagName; -use roc_module::symbol::{Interns, Symbol}; -use roc_mono::layout::{ - cmp_fields, ext_var_is_empty_tag_union, Builtin, Layout, LayoutCache, TagOrClosure, -}; -use roc_types::subs::{Label, UnionLabels}; -use roc_types::{ - subs::{Content, FlatType, LambdaSet, Subs, Variable}, - types::RecordField, -}; -use std::fmt::Display; - -pub struct Env<'a> { - pub arena: &'a Bump, - pub subs: &'a Subs, - pub layout_cache: &'a mut LayoutCache<'a>, - pub interns: &'a Interns, - pub struct_names: Structs, - pub enum_names: Enums, - pub pending_recursive_types: VecMap, - pub known_recursive_types: VecMap, -} - -impl<'a> Env<'a> { - pub fn vars_to_types(&mut self, variables: I) -> Types - where - I: IntoIterator, - { - let mut types = Types::default(); - - for var in variables { - self.add_type(var, &mut types); - } - - self.resolve_pending_recursive_types(&mut types); - - types - } - - fn add_type(&mut self, var: Variable, types: &mut Types) -> TypeId { - let layout = self - .layout_cache - .from_var(self.arena, var, self.subs) - .expect("Something weird ended up in the content"); - - add_type_help(self, layout, var, None, types) - } - - fn resolve_pending_recursive_types(&mut self, types: &mut Types) { - // TODO if VecMap gets a drain() method, use that instead of doing take() and into_iter - let pending = core::mem::take(&mut self.pending_recursive_types); - - for (type_id, var) in pending.into_iter() { - let actual_type_id = self.known_recursive_types.get(&var).unwrap_or_else(|| { - unreachable!( - "There was no known recursive TypeId for the pending recursive variable {:?}", - var - ); - }); - - debug_assert!( - matches!(types.get(type_id), RocType::RecursivePointer(TypeId::PENDING)), - "The TypeId {:?} was registered as a pending recursive pointer, but was not stored in Types as one.", - type_id - ); - - types.replace(type_id, RocType::RecursivePointer(*actual_type_id)); - } - } -} - -fn add_type_help<'a>( - env: &mut Env<'a>, - layout: Layout<'a>, - var: Variable, - opt_name: Option, - types: &mut Types, -) -> TypeId { - let subs = env.subs; - - match subs.get_content_without_compacting(var) { - Content::FlexVar(_) - | Content::RigidVar(_) - | Content::FlexAbleVar(_, _) - | Content::RigidAbleVar(_, _) => { - todo!("TODO give a nice error message for a non-concrete type being passed to the host") - } - Content::Structure(FlatType::Record(fields, ext)) => { - let it = fields - .unsorted_iterator(subs, *ext) - .expect("something weird in content") - .flat_map(|(label, field)| { - match field { - RecordField::Required(field_var) | RecordField::Demanded(field_var) => { - Some((label.to_string(), field_var)) - } - RecordField::Optional(_) => { - // drop optional fields - None - } - } - }); - - let name = match opt_name { - Some(sym) => sym.as_str(env.interns).to_string(), - None => env.struct_names.get_name(var), - }; - - add_struct(env, name, it, types, |name, fields| RocType::Struct { - name, - fields, - }) - } - Content::LambdaSet(LambdaSet { - solved, - recursion_var: _, - unspecialized, - }) => { - debug_assert!( - unspecialized.is_empty(), - "unspecialized lambda sets left over" - ); - - add_union(env, opt_name, solved, var, types) - } - Content::Structure(FlatType::TagUnion(tags, ext_var)) => { - debug_assert!(ext_var_is_empty_tag_union(subs, *ext_var)); - - add_union(env, opt_name, tags, var, types) - } - Content::Structure(FlatType::RecursiveTagUnion(_rec_var, tag_vars, ext_var)) => { - debug_assert!(ext_var_is_empty_tag_union(subs, *ext_var)); - - add_union(env, opt_name, tag_vars, var, types) - } - Content::Structure(FlatType::Apply(symbol, _)) => match layout { - Layout::Builtin(builtin) => add_builtin_type(env, builtin, var, opt_name, types), - _ => { - if symbol.is_builtin() { - todo!( - "Handle Apply for builtin symbol {:?} and layout {:?}", - symbol, - layout - ) - } else { - todo!( - "Handle non-builtin Apply for symbol {:?} and layout {:?}", - symbol, - layout - ) - } - } - }, - Content::Structure(FlatType::Func(_, _, _)) => { - todo!() - } - Content::Structure(FlatType::FunctionOrTagUnion(_, _, _)) => { - todo!() - } - Content::Structure(FlatType::Erroneous(_)) => todo!(), - Content::Structure(FlatType::EmptyRecord) => todo!(), - Content::Structure(FlatType::EmptyTagUnion) => { - // This can happen when unwrapping a tag union; don't do anything. - todo!() - } - Content::Alias(name, _, real_var, _) => { - if name.is_builtin() { - match layout { - Layout::Builtin(builtin) => { - add_builtin_type(env, builtin, var, opt_name, types) - } - _ => { - unreachable!() - } - } - } else { - // If this was a non-builtin type alias, we can use that alias name - // in the generated bindings. - add_type_help(env, layout, *real_var, Some(*name), types) - } - } - Content::RangedNumber(_, _) => todo!(), - Content::Error => todo!(), - Content::RecursionVar { structure, .. } => { - let type_id = types.add(RocType::RecursivePointer(TypeId::PENDING)); - - env.pending_recursive_types.insert(type_id, *structure); - - type_id - } - } -} - -fn add_builtin_type<'a>( - env: &mut Env<'a>, - builtin: Builtin<'a>, - var: Variable, - opt_name: Option, - types: &mut Types, -) -> TypeId { - match builtin { - Builtin::Int(width) => match width { - U8 => types.add(RocType::Num(RocNum::U8)), - U16 => types.add(RocType::Num(RocNum::U16)), - U32 => types.add(RocType::Num(RocNum::U32)), - U64 => types.add(RocType::Num(RocNum::U64)), - U128 => types.add(RocType::Num(RocNum::U128)), - I8 => types.add(RocType::Num(RocNum::I8)), - I16 => types.add(RocType::Num(RocNum::I16)), - I32 => types.add(RocType::Num(RocNum::I32)), - I64 => types.add(RocType::Num(RocNum::I64)), - I128 => types.add(RocType::Num(RocNum::I128)), - }, - Builtin::Float(width) => match width { - F32 => types.add(RocType::Num(RocNum::F32)), - F64 => types.add(RocType::Num(RocNum::F64)), - F128 => types.add(RocType::Num(RocNum::F128)), - }, - Builtin::Decimal => types.add(RocType::Num(RocNum::Dec)), - Builtin::Bool => types.add(RocType::Bool), - Builtin::Str => types.add(RocType::RocStr), - Builtin::Dict(key_layout, val_layout) => { - // TODO FIXME this `var` is wrong - should have a different `var` for key and for val - let key_id = add_type_help(env, *key_layout, var, opt_name, types); - let val_id = add_type_help(env, *val_layout, var, opt_name, types); - let dict_id = types.add(RocType::RocDict(key_id, val_id)); - - types.depends(dict_id, key_id); - types.depends(dict_id, val_id); - - dict_id - } - Builtin::Set(elem_layout) => { - let elem_id = add_type_help(env, *elem_layout, var, opt_name, types); - let set_id = types.add(RocType::RocSet(elem_id)); - - types.depends(set_id, elem_id); - - set_id - } - Builtin::List(elem_layout) => { - let elem_id = add_type_help(env, *elem_layout, var, opt_name, types); - let list_id = types.add(RocType::RocList(elem_id)); - - types.depends(list_id, elem_id); - - list_id - } - } -} - -fn add_struct( - env: &mut Env<'_>, - name: String, - fields: I, - types: &mut Types, - to_type: F, -) -> TypeId -where - I: IntoIterator, - L: Display + Ord, - F: FnOnce(String, Vec<(L, TypeId)>) -> RocType, -{ - let subs = env.subs; - let fields_iter = &mut fields.into_iter(); - let mut sortables = - bumpalo::collections::Vec::with_capacity_in(fields_iter.size_hint().0, env.arena); - - for (label, field_var) in fields_iter { - sortables.push(( - label, - field_var, - env.layout_cache - .from_var(env.arena, field_var, subs) - .unwrap(), - )); - } - - sortables.sort_by(|(label1, _, layout1), (label2, _, layout2)| { - cmp_fields( - label1, - layout1, - label2, - layout2, - env.layout_cache.target_info, - ) - }); - - let fields = sortables - .into_iter() - .map(|(label, field_var, field_layout)| { - let type_id = add_type_help(env, field_layout, field_var, None, types); - - (label, type_id) - }) - .collect::>(); - - types.add(to_type(name, fields)) -} - -fn add_union( - env: &mut Env<'_>, - opt_name: Option, - union_tags: &UnionLabels, - var: Variable, - types: &mut Types, -) -> TypeId -where - L: Label + Into, -{ - let subs = env.subs; - let mut tags: Vec<(String, Vec)> = union_tags - .iter_from_subs(subs) - .map(|(label, payload_vars)| { - let name_str = match label.clone().into() { - TagOrClosure::Tag(TagName(uppercase)) => uppercase.as_str().to_string(), - TagOrClosure::Closure(_) => unreachable!(), - }; - - (name_str, payload_vars.to_vec()) - }) - .collect(); - - let layout = env.layout_cache.from_var(env.arena, var, subs).unwrap(); - let name = match opt_name { - Some(sym) => sym.as_str(env.interns).to_string(), - None => env.enum_names.get_name(var), - }; - - // Sort tags alphabetically by tag name - tags.sort_by(|(name1, _), (name2, _)| name1.cmp(name2)); - - let is_recursive = is_recursive_tag_union(&layout); - - let mut tags: Vec<_> = tags - .into_iter() - .map(|(tag_name, payload_vars)| { - match struct_fields_needed(env, payload_vars.iter().copied()) { - 0 => { - // no payload - (tag_name, None) - } - 1 if !is_recursive => { - // this isn't recursive and there's 1 payload item, so it doesn't - // need its own struct - e.g. for `[Foo Str, Bar Str]` both of them - // can have payloads of plain old Str, no struct wrapper needed. - let payload_var = payload_vars.get(0).unwrap(); - let layout = env - .layout_cache - .from_var(env.arena, *payload_var, env.subs) - .expect("Something weird ended up in the content"); - let payload_id = add_type_help(env, layout, *payload_var, None, types); - - (tag_name, Some(payload_id)) - } - _ => { - // create a RocType for the payload and save it - let struct_name = format!("{}_{}", name, tag_name); // e.g. "MyUnion_MyVariant" - let fields = payload_vars.iter().copied().enumerate(); - let struct_id = add_struct(env, struct_name, fields, types, |name, fields| { - RocType::TagUnionPayload { name, fields } - }); - - (tag_name, Some(struct_id)) - } - } - }) - .collect(); - - let typ = match layout { - Layout::Union(union_layout) => { - use roc_mono::layout::UnionLayout::*; - - match union_layout { - // A non-recursive tag union - // e.g. `Result ok err : [Ok ok, Err err]` - NonRecursive(_) => RocType::TagUnion(RocTagUnion::NonRecursive { name, tags }), - // A recursive tag union (general case) - // e.g. `Expr : [Sym Str, Add Expr Expr]` - Recursive(_) => RocType::TagUnion(RocTagUnion::Recursive { name, tags }), - // A recursive tag union with just one constructor - // Optimization: No need to store a tag ID (the payload is "unwrapped") - // e.g. `RoseTree a : [Tree a (List (RoseTree a))]` - NonNullableUnwrapped(_) => { - todo!() - } - // A recursive tag union that has an empty variant - // Optimization: Represent the empty variant as null pointer => no memory usage & fast comparison - // It has more than one other variant, so they need tag IDs (payloads are "wrapped") - // e.g. `FingerTree a : [Empty, Single a, More (Some a) (FingerTree (Tuple a)) (Some a)]` - // see also: https://youtu.be/ip92VMpf_-A?t=164 - NullableWrapped { .. } => { - todo!() - } - // A recursive tag union with only two variants, where one is empty. - // Optimizations: Use null for the empty variant AND don't store a tag ID for the other variant. - // e.g. `ConsList a : [Nil, Cons a (ConsList a)]` - NullableUnwrapped { - nullable_id: null_represents_first_tag, - other_fields: _, // TODO use this! - } => { - // NullableUnwrapped tag unions should always have exactly 2 tags. - debug_assert_eq!(tags.len(), 2); - - let null_tag; - let non_null; - - if null_represents_first_tag { - // If nullable_id is true, then the null tag is second, which means - // pop() will return it because it's at the end of the vec. - null_tag = tags.pop().unwrap().0; - non_null = tags.pop().unwrap(); - } else { - // The null tag is first, which means the tag with the payload is second. - non_null = tags.pop().unwrap(); - null_tag = tags.pop().unwrap().0; - } - - let (non_null_tag, non_null_payload) = non_null; - - RocType::TagUnion(RocTagUnion::NullableUnwrapped { - name, - null_tag, - non_null_tag, - non_null_payload: non_null_payload.unwrap(), - null_represents_first_tag, - }) - } - } - } - Layout::Builtin(Builtin::Int(_)) => RocType::TagUnion(RocTagUnion::Enumeration { - name, - tags: tags.into_iter().map(|(tag_name, _)| tag_name).collect(), - }), - Layout::Builtin(_) - | Layout::Struct { .. } - | Layout::Boxed(_) - | Layout::LambdaSet(_) - | Layout::RecursivePointer => { - // These must be single-tag unions. Bindgen ordinary nonrecursive - // tag unions for them, and let Rust do the unwrapping. - // - // This should be a very rare use case, and it's not worth overcomplicating - // the rest of bindgen to make it do something different. - RocType::TagUnion(RocTagUnion::NonRecursive { name, tags }) - } - }; - - let type_id = types.add(typ); - - if is_recursive { - env.known_recursive_types.insert(var, type_id); - } - - type_id -} - -fn is_recursive_tag_union(layout: &Layout) -> bool { - use roc_mono::layout::UnionLayout::*; - - match layout { - Layout::Union(tag_union) => match tag_union { - NonRecursive(_) => false, - Recursive(_) - | NonNullableUnwrapped(_) - | NullableWrapped { .. } - | NullableUnwrapped { .. } => true, - }, - _ => false, - } -} - -fn struct_fields_needed>(env: &mut Env<'_>, vars: I) -> usize { - let subs = env.subs; - let arena = env.arena; - - vars.into_iter().fold(0, |count, var| { - let layout = env.layout_cache.from_var(arena, var, subs).unwrap(); - - if layout.is_dropped_because_empty() { - count - } else { - count + 1 - } - }) -} diff --git a/bindgen/src/bindgen_c.rs b/bindgen/src/bindgen_c.rs deleted file mode 100644 index c19b087c87..0000000000 --- a/bindgen/src/bindgen_c.rs +++ /dev/null @@ -1,40 +0,0 @@ -use std::io; - -static TEMPLATE: &[u8] = include_bytes!("../templates/template.c"); - -pub fn write_template(writer: &mut impl io::Write) -> io::Result<()> { - writer.write_all(TEMPLATE)?; - - Ok(()) -} - -// pub fn write_bindings(_writer: &mut impl io::Write) -> io::Result<()> { -// extern struct RocStr roc__mainForHost_1_exposed(); - -// int main() { -// struct RocStr str = roc__mainForHost_1_exposed(); - -// // Determine str_len and the str_bytes pointer, -// // taking into account the small string optimization. -// size_t str_len = roc_str_len(str); -// char* str_bytes; - -// if (is_small_str(str)) { -// str_bytes = (char*)&str; -// } else { -// str_bytes = str.bytes; -// } - -// // Write to stdout -// if (write(1, str_bytes, str_len) >= 0) { -// // Writing succeeded! -// return 0; -// } else { -// printf("Error writing to stdout: %s\n", strerror(errno)); - -// return 1; -// } -// } - -// Ok(()) -// } diff --git a/bindgen/src/bindgen_rs.rs b/bindgen/src/bindgen_rs.rs deleted file mode 100644 index 77805ea051..0000000000 --- a/bindgen/src/bindgen_rs.rs +++ /dev/null @@ -1,1769 +0,0 @@ -use crate::types::{RocNum, RocTagUnion, RocType, TypeId, Types}; -use indexmap::IndexMap; -use roc_mono::layout::UnionLayout; -use roc_target::Architecture; -use std::convert::TryInto; -use std::fmt::Display; - -pub static TEMPLATE: &[u8] = include_bytes!("../templates/template.rs"); -pub static HEADER: &[u8] = include_bytes!("../templates/header.rs"); -const INDENT: &str = " "; -const VARIANT_DOC_COMMENT: &str = - "/// Returns which variant this tag union holds. Note that this never includes a payload!"; - -type Impls = IndexMap>>; -type Impl = Option; - -/// Recursive tag unions need a custom Clone which bumps refcount. -const RECURSIVE_TAG_UNION_CLONE: &str = r#"fn clone(&self) -> Self { - if let Some(storage) = self.storage() { - let mut new_storage = storage.get(); - if !new_storage.is_readonly() { - new_storage.increment_reference_count(); - storage.set(new_storage); - } - } - - Self { - pointer: self.pointer - } -}"#; - -const RECURSIVE_TAG_UNION_STORAGE: &str = r#"#[inline(always)] - fn storage(&self) -> Option<&core::cell::Cell> { - if self.pointer.is_null() { - None - } else { - unsafe { - Some(&*self.pointer.cast::>().sub(1)) - } - } - }"#; - -/// Add the given declaration body, along with the architecture, to the Impls. -/// This can optionally be within an `impl`, or if no `impl` is specified, -/// then it's added at the top level. -fn add_decl(impls: &mut Impls, opt_impl: Impl, architecture: Architecture, body: String) { - let decls = impls.entry(opt_impl).or_default(); - let architectures = decls.entry(body).or_default(); - - architectures.push(architecture); -} - -pub fn emit(types_by_architecture: &[(Architecture, Types)]) -> String { - let mut buf = String::new(); - let mut impls: Impls = IndexMap::default(); - - for (architecture, types) in types_by_architecture.iter() { - for id in types.sorted_ids() { - add_type(*architecture, id, types, &mut impls); - } - } - - for (opt_impl, decls) in impls { - let has_impl; - - if let Some(impl_str) = opt_impl { - has_impl = true; - - buf.push('\n'); - buf.push_str(&impl_str); - buf.push_str(" {"); - } else { - has_impl = false; - } - - for (decl, architectures) in decls { - // If we're inside an `impl` block, indent the cfg annotation - let indent = if has_impl { INDENT } else { "" }; - - // Push a newline and potentially an indent before the #[cfg(...)] line - buf.push('\n'); - buf.push_str(indent); - - match architectures.len() { - 1 => { - let arch = arch_to_str(architectures.get(0).unwrap()); - - buf.push_str(&format!("#[cfg(target_arch = \"{arch}\")]")); - } - _ => { - // We should never have a decl recorded with 0 architectures! - debug_assert_ne!(architectures.len(), 0); - - let alternatives = architectures - .iter() - .map(|arch| { - format!("{indent}{INDENT}target_arch = \"{}\"", arch_to_str(arch)) - }) - .collect::>() - .join(",\n"); - - buf.push_str(&format!("#[cfg(any(\n{alternatives}\n{indent}))]")); - } - } - - buf.push('\n'); // newline after the #[cfg(...)] line - - // indent and print the decl (e.g. a `fn`), with a newline at the end - buf.push_str(indent); - buf.push_str(&decl); - buf.push('\n'); - } - - // If this was an impl, it needs a closing brace at the end. - if has_impl { - buf.push_str("}\n"); - } - } - - buf -} - -fn add_type(architecture: Architecture, id: TypeId, types: &Types, impls: &mut Impls) { - match types.get(id) { - RocType::Struct { name, fields } => { - add_struct(name, architecture, fields, id, types, impls, false) - } - RocType::TagUnionPayload { name, fields } => { - add_struct(name, architecture, fields, id, types, impls, true) - } - RocType::TagUnion(tag_union) => { - match tag_union { - RocTagUnion::Enumeration { tags, name } => { - if tags.len() == 1 { - // An enumeration with one tag is a zero-sized unit type, so - // represent it as a zero-sized struct (e.g. "struct Foo()"). - let derive = derive_str(types.get(id), types, true); - let struct_name = type_name(id, types); - let body = format!("{derive}\nstruct {struct_name}();"); - - add_decl(impls, None, architecture, body); - } else { - add_enumeration( - name, - architecture, - types.get(id), - tags.iter(), - types, - impls, - ) - } - } - RocTagUnion::NonRecursive { tags, name } => { - // Empty tag unions can never come up at runtime, - // and so don't need declared types. - if !tags.is_empty() { - add_tag_union( - Recursiveness::NonRecursive, - name, - architecture, - id, - tags, - types, - impls, - ); - } - } - RocTagUnion::Recursive { tags, name } => { - // Empty tag unions can never come up at runtime, - // and so don't need declared types. - if !tags.is_empty() { - add_tag_union( - Recursiveness::Recursive, - name, - architecture, - id, - tags, - types, - impls, - ); - } - } - RocTagUnion::NullableWrapped { .. } => { - todo!(); - } - RocTagUnion::NullableUnwrapped { - name, - null_tag, - non_null_tag, - non_null_payload, - null_represents_first_tag, - } => add_nullable_unwrapped( - name, - architecture, - id, - null_tag, - non_null_tag, - *non_null_payload, - *null_represents_first_tag, - types, - impls, - ), - RocTagUnion::NonNullableUnwrapped { .. } => { - todo!(); - } - } - } - // These types don't need to be declared in Rust. - RocType::Num(_) - | RocType::Bool - | RocType::RocStr - | RocType::RocDict(_, _) - | RocType::RocSet(_) - | RocType::RocList(_) - | RocType::RocBox(_) => {} - RocType::RecursivePointer { .. } => { - // This is recursively pointing to a type that should already have been added, - // so no extra work needs to happen. - } - } -} - -fn add_discriminant( - name: &str, - architecture: Architecture, - tag_names: Vec, - types: &Types, - impls: &mut Impls, -) -> String { - // The tag union's discriminant, e.g. - // - // #[repr(u8)] - // pub enum tag_MyTagUnion { - // Bar, - // Foo, - // } - let discriminant_name = format!("variant_{name}"); - let discriminant_type = RocType::TagUnion(RocTagUnion::Enumeration { - name: discriminant_name.clone(), - tags: tag_names.clone(), - }); - - add_enumeration( - &discriminant_name, - architecture, - &discriminant_type, - tag_names.into_iter(), - types, - impls, - ); - - discriminant_name -} - -#[derive(Copy, Clone)] -enum Recursiveness { - Recursive, - NonRecursive, -} - -fn add_tag_union( - recursiveness: Recursiveness, - name: &str, - architecture: Architecture, - type_id: TypeId, - tags: &[(String, Option)], - types: &Types, - impls: &mut Impls, -) { - // We should never be attempting to bindgen empty tag unions; RocType should not - // have let this happen. - debug_assert_ne!(tags.len(), 0); - - let tag_names = tags.iter().map(|(name, _)| name).cloned().collect(); - let discriminant_name = add_discriminant(name, architecture, tag_names, types, impls); - let typ = types.get(type_id); - let target_info = architecture.into(); - let discriminant_offset = RocTagUnion::discriminant_offset(tags, types, target_info); - let size = typ.size(types, target_info); - let (actual_self, actual_self_mut, actual_other, union_name) = match recursiveness { - Recursiveness::Recursive => ( - "(&*self.union_pointer())", - "(&mut *self.union_pointer())", - "(&*other.union_pointer())", - format!("union_{name}"), - ), - Recursiveness::NonRecursive => ("self", "self", "other", name.to_string()), - }; - - { - // Recursive tag unions have a public struct, not a public union - let pub_str; - let decl_union_name: &str; - - match recursiveness { - Recursiveness::Recursive => { - add_decl( - impls, - None, - architecture, - format!( - r#" -pub struct {name} {{ - pointer: *mut {union_name}, -}} -"# - ), - ); - - pub_str = ""; - decl_union_name = &union_name; - } - Recursiveness::NonRecursive => { - pub_str = "pub "; - decl_union_name = name; - } - }; - - // No #[derive(...)] for unions; we have to generate each impl ourselves! - let mut buf = format!("#[repr(C)]\n{pub_str}union {decl_union_name} {{\n"); - - for (tag_name, opt_payload_id) in tags { - // If there's no payload, we don't need a variant for it. - if let Some(payload_id) = opt_payload_id { - let payload_type = types.get(*payload_id); - - buf.push_str(&format!("{INDENT}{tag_name}: ")); - - if payload_type.has_pointer(types) { - // types with pointers need ManuallyDrop - // because rust unions don't (and can't) - // know how to drop them automatically! - buf.push_str(&format!( - "core::mem::ManuallyDrop<{}>,\n", - type_name(*payload_id, types) - )); - } else { - buf.push_str(&type_name(*payload_id, types)); - buf.push_str(",\n"); - } - } - } - - // When there's no alignment padding after the largest variant, - // the compiler will make extra room for the discriminant. - // We need that to be reflected in the overall size of the enum, - // so add an extra variant with the appropriate size. - // - // (Do this even if theoretically shouldn't be necessary, since - // there's no runtime cost and it more explicitly syncs the - // union's size with what we think it should be.) - buf.push_str(&format!("{INDENT}_sizer: [u8; {size}],\n}}")); - - add_decl(impls, None, architecture, buf); - } - - // The impl for the tag union - { - let opt_impl = Some(format!("impl {name}")); - let bitmask; - - match recursiveness { - Recursiveness::Recursive => { - add_decl( - impls, - opt_impl.clone(), - architecture, - RECURSIVE_TAG_UNION_STORAGE.to_string(), - ); - - if tags.len() <= max_pointer_tagged_variants(architecture) { - bitmask = format!("{:#b}", tagged_pointer_bitmask(architecture)); - - add_decl( - impls, - opt_impl.clone(), - architecture, - format!( - r#"{VARIANT_DOC_COMMENT} - pub fn variant(&self) -> {discriminant_name} {{ - // The discriminant is stored in the unused bytes at the end of the recursive pointer - unsafe {{ core::mem::transmute::((self.pointer as u8) & {bitmask}) }} - }}"# - ), - ); - - add_decl( - impls, - opt_impl.clone(), - architecture, - format!( - r#"/// Internal helper - fn tag_discriminant(pointer: *mut {union_name}, discriminant: {discriminant_name}) -> *mut {union_name} {{ - // The discriminant is stored in the unused bytes at the end of the union pointer - unsafe {{ - let untagged = (pointer as usize) & (!{bitmask} as usize); - let tagged = untagged | (discriminant as usize); - - tagged as *mut {union_name} - }} - }}"# - ), - ); - - add_decl( - impls, - opt_impl.clone(), - architecture, - format!( - r#"/// Internal helper - fn union_pointer(&self) -> *mut {union_name} {{ - // The discriminant is stored in the unused bytes at the end of the union pointer - ((self.pointer as usize) & (!{bitmask} as usize)) as *mut {union_name} - }}"# - ), - ); - } else { - todo!( - "Support {} tags in a recursive tag union on architecture {:?}. (This is too many tags for pointer tagging to work, so we need to bindgen something different.)", - tags.len(), - architecture - ); - } - } - Recursiveness::NonRecursive => { - // The bitmask doesn't come up in a nonrecursive tag union. - bitmask = String::new(); - - // An old design, which ended up not working out, was that the tag union - // was a struct containing two fields: one for the `union`, and another - // for the discriminant. - // - // The problem with this was alignment; e.g. if you have one variant with a - // RocStr in it and another with an I128, then the `union` has a size of 32B - // and the discriminant is right after it - making the size of the whole struct - // round up to 48B total, since it has an alignment of 16 from the I128. - // - // However, Roc will generate the more efficient thing here: the whole thing will - // be 32B, and the discriminant will appear at offset 24 - right after the end of - // the RocStr. The current design recognizes this and works with it, by representing - // the entire structure as a union and manually setting the tag at the appropriate offset. - add_decl( - impls, - opt_impl.clone(), - architecture, - format!( - r#"{VARIANT_DOC_COMMENT} - pub fn variant(&self) -> {discriminant_name} {{ - unsafe {{ - let bytes = core::mem::transmute::<&Self, &[u8; core::mem::size_of::()]>(self); - - core::mem::transmute::(*bytes.as_ptr().add({discriminant_offset})) - }} - }}"# - ), - ); - - add_decl( - impls, - opt_impl.clone(), - architecture, - format!( - r#"/// Internal helper - fn set_discriminant(&mut self, discriminant: {discriminant_name}) {{ - let discriminant_ptr: *mut {discriminant_name} = (self as *mut {name}).cast(); - - unsafe {{ - *(discriminant_ptr.add({discriminant_offset})) = discriminant; - }} - }}"# - ), - ); - } - } - - for (tag_name, opt_payload_id) in tags { - // Add a convenience constructor function to the impl, e.g. - // - // /// Construct a tag named Foo, with the appropriate payload - // pub fn Foo(payload: roc_std::RocStr) -> Self { - // Self { - // tag: tag_MyTagUnion::Foo, - // variant: variant_MyTagUnion { - // Foo: core::mem::ManuallyDrop::new(payload), - // }, - // } - // } - if let Some(payload_id) = opt_payload_id { - let payload_type = types.get(*payload_id); - let self_for_into; - let payload_args; - let args_to_payload; - let owned_ret_type; - let borrowed_ret_type; - let owned_get_payload; - let borrowed_get_payload; - let owned_ret; - let borrowed_ret; - - match recursiveness { - Recursiveness::Recursive => { - if payload_type.has_pointer(types) { - owned_get_payload = format!( - r#"unsafe {{ - let ptr = (self.pointer as usize & !{bitmask}) as *mut {union_name}; - - core::mem::ManuallyDrop::take(&mut (*ptr).{tag_name}) - }}"# - ); - borrowed_get_payload = format!( - r#"unsafe {{ - let ptr = (self.pointer as usize & !{bitmask}) as *mut {union_name}; - - &(*ptr).{tag_name} - }}"# - ); - // we need `mut self` for the argument because of ManuallyDrop - self_for_into = "mut self"; - } else { - owned_get_payload = format!( - r#"unsafe {{ - let ptr = (self.pointer as usize & !{bitmask}) as *mut {union_name}; - - core::ptr::read(ptr).{tag_name} - }}"# - ); - borrowed_get_payload = format!( - r#"unsafe {{ - let ptr = (self.pointer as usize & !{bitmask}) as *mut {union_name}; - - (&ptr).{tag_name} - }}"# - ); - // we don't need `mut self` unless we need ManuallyDrop - self_for_into = "self"; - }; - } - Recursiveness::NonRecursive => { - if payload_type.has_pointer(types) { - owned_get_payload = format!( - "unsafe {{ core::mem::ManuallyDrop::take(&mut self.{tag_name}) }}" - ); - borrowed_get_payload = format!("unsafe {{ &self.{tag_name} }}"); - // we need `mut self` for the argument because of ManuallyDrop - self_for_into = "mut self"; - } else { - owned_get_payload = format!("unsafe {{ self.{tag_name} }}"); - borrowed_get_payload = format!("unsafe {{ &self.{tag_name} }}"); - // we don't need `mut self` unless we need ManuallyDrop - self_for_into = "self"; - }; - } - } - - match payload_type { - RocType::RocStr - | RocType::Bool - | RocType::Num(_) - | RocType::RocList(_) - | RocType::RocDict(_, _) - | RocType::RocSet(_) - | RocType::RocBox(_) - | RocType::TagUnion(_) - | RocType::RecursivePointer { .. } => { - owned_ret_type = type_name(*payload_id, types); - borrowed_ret_type = format!("&{}", owned_ret_type); - owned_ret = "payload".to_string(); - borrowed_ret = format!("&{owned_ret}"); - payload_args = format!("arg: {owned_ret_type}"); - args_to_payload = if payload_type.has_pointer(types) { - "core::mem::ManuallyDrop::new(arg)".to_string() - } else { - "arg".to_string() - }; - } - RocType::Struct { fields, .. } => { - let answer = - tag_union_struct_help(fields.iter(), *payload_id, types, false); - - owned_ret = answer.owned_ret; - borrowed_ret = answer.borrowed_ret; - owned_ret_type = answer.owned_ret_type; - borrowed_ret_type = answer.borrowed_ret_type; - payload_args = answer.payload_args; - args_to_payload = answer.args_to_payload; - } - RocType::TagUnionPayload { fields, .. } => { - let answer = tag_union_struct_help(fields.iter(), *payload_id, types, true); - - owned_ret = answer.owned_ret; - borrowed_ret = answer.borrowed_ret; - owned_ret_type = answer.owned_ret_type; - borrowed_ret_type = answer.borrowed_ret_type; - payload_args = answer.payload_args; - args_to_payload = answer.args_to_payload; - } - }; - - { - let body = match recursiveness { - Recursiveness::Recursive => { - format!( - r#" - let size = core::mem::size_of::<{union_name}>(); - let align = core::mem::align_of::<{union_name}>() as u32; - - unsafe {{ - let ptr = crate::roc_alloc(size, align) as *mut {union_name}; - - *ptr = {union_name} {{ - {tag_name}: {args_to_payload} - }}; - - Self {{ - pointer: Self::tag_discriminant(ptr, {discriminant_name}::{tag_name}), - }} - }}"# - ) - } - Recursiveness::NonRecursive => { - format!( - r#" - let mut answer = Self {{ - {tag_name}: {args_to_payload} - }}; - - answer.set_discriminant({discriminant_name}::{tag_name}); - - answer"# - ) - } - }; - - add_decl( - impls, - opt_impl.clone(), - architecture, - format!( - r#"/// Construct a tag named {tag_name}, with the appropriate payload - pub fn {tag_name}({payload_args}) -> Self {{{body} - }}"# - ), - ); - } - - add_decl( - impls, - opt_impl.clone(), - architecture, - format!( - r#"/// Unsafely assume the given {name} has a .variant() of {tag_name} and convert it to {tag_name}'s payload. - /// (Always examine .variant() first to make sure this is the correct variant!) - /// Panics in debug builds if the .variant() doesn't return {tag_name}. - pub unsafe fn into_{tag_name}({self_for_into}) -> {owned_ret_type} {{ - debug_assert_eq!(self.variant(), {discriminant_name}::{tag_name}); - - let payload = {owned_get_payload}; - - {owned_ret} - }}"#, - ), - ); - - add_decl( - impls, - opt_impl.clone(), - architecture, - format!( - r#"/// Unsafely assume the given {name} has a .variant() of {tag_name} and return its payload. - /// (Always examine .variant() first to make sure this is the correct variant!) - /// Panics in debug builds if the .variant() doesn't return {tag_name}. - pub unsafe fn as_{tag_name}(&self) -> {borrowed_ret_type} {{ - debug_assert_eq!(self.variant(), {discriminant_name}::{tag_name}); - - let payload = {borrowed_get_payload}; - - {borrowed_ret} - }}"#, - ), - ); - } else { - add_decl( - impls, - opt_impl.clone(), - architecture, - format!( - r#"/// A tag named {tag_name}, which has no payload. - pub const {tag_name}: Self = unsafe {{ - let mut bytes = [0; core::mem::size_of::<{name}>()]; - - bytes[{discriminant_offset}] = {discriminant_name}::{tag_name} as u8; - - core::mem::transmute::<[u8; core::mem::size_of::<{name}>()], {name}>(bytes) - }};"#, - ), - ); - - add_decl( - impls, - opt_impl.clone(), - architecture, - format!( - r#"/// Other `into_` methods return a payload, but since the {tag_name} tag - /// has no payload, this does nothing and is only here for completeness. - pub fn into_{tag_name}(self) {{ - () - }}"#, - ), - ); - - add_decl( - impls, - opt_impl.clone(), - architecture, - format!( - r#"/// Other `as` methods return a payload, but since the {tag_name} tag - /// has no payload, this does nothing and is only here for completeness. - pub unsafe fn as_{tag_name}(&self) {{ - () - }}"#, - ), - ); - } - } - } - - // The Drop impl for the tag union - { - let opt_impl = Some(format!("impl Drop for {name}")); - let mut buf = String::new(); - - write_impl_tags( - 2, - tags.iter(), - &discriminant_name, - &mut buf, - |tag_name, opt_payload_id| { - match opt_payload_id { - Some(payload_id) if types.get(payload_id).has_pointer(types) => { - format!("unsafe {{ core::mem::ManuallyDrop::drop(&mut {actual_self_mut}.{tag_name}) }},",) - } - _ => { - // If it had no payload, or if the payload had no pointers, - // there's nothing to clean up, so do `=> {}` for the branch. - "{}".to_string() - } - } - }, - ); - - add_decl( - impls, - opt_impl, - architecture, - format!("fn drop(&mut self) {{\n{buf}{INDENT}}}"), - ); - } - - // The PartialEq impl for the tag union - { - let opt_impl_prefix = if typ.has_float(types) { - String::new() - } else { - format!("impl Eq for {name} {{}}\n\n") - }; - let opt_impl = Some(format!("{opt_impl_prefix}impl PartialEq for {name}")); - let mut buf = r#"fn eq(&self, other: &Self) -> bool { - if self.variant() != other.variant() { - return false; - } - - unsafe { -"# - .to_string(); - - write_impl_tags( - 3, - tags.iter(), - &discriminant_name, - &mut buf, - |tag_name, opt_payload_id| { - if opt_payload_id.is_some() { - format!("{actual_self}.{tag_name} == {actual_other}.{tag_name},") - } else { - // if the tags themselves had been unequal, we already would have - // early-returned with false, so this means the tags were equal - // and there's no payload; return true! - "true,".to_string() - } - }, - ); - - buf.push_str(INDENT); - buf.push_str(INDENT); - buf.push_str("}\n"); - buf.push_str(INDENT); - buf.push('}'); - - add_decl(impls, opt_impl, architecture, buf); - } - - // The PartialOrd impl for the tag union - { - let opt_impl = Some(format!("impl PartialOrd for {name}")); - let mut buf = r#"fn partial_cmp(&self, other: &Self) -> Option { - match self.variant().partial_cmp(&other.variant()) { - Some(core::cmp::Ordering::Equal) => {} - not_eq => return not_eq, - } - - unsafe { -"# - .to_string(); - - write_impl_tags( - 3, - tags.iter(), - &discriminant_name, - &mut buf, - |tag_name, opt_payload_id| { - if opt_payload_id.is_some() { - format!("{actual_self}.{tag_name}.partial_cmp(&{actual_other}.{tag_name}),",) - } else { - // if the tags themselves had been unequal, we already would have - // early-returned, so this means the tags were equal and there's - // no payload; return Equal! - "Some(core::cmp::Ordering::Equal),".to_string() - } - }, - ); - - buf.push_str(INDENT); - buf.push_str(INDENT); - buf.push_str("}\n"); - buf.push_str(INDENT); - buf.push('}'); - - add_decl(impls, opt_impl, architecture, buf); - } - - // The Ord impl for the tag union - { - let opt_impl = Some(format!("impl Ord for {name}")); - let mut buf = r#"fn cmp(&self, other: &Self) -> core::cmp::Ordering { - match self.variant().cmp(&other.variant()) { - core::cmp::Ordering::Equal => {} - not_eq => return not_eq, - } - - unsafe { -"# - .to_string(); - - write_impl_tags( - 3, - tags.iter(), - &discriminant_name, - &mut buf, - |tag_name, opt_payload_id| { - if opt_payload_id.is_some() { - format!("{actual_self}.{tag_name}.cmp(&{actual_other}.{tag_name}),",) - } else { - // if the tags themselves had been unequal, we already would have - // early-returned, so this means the tags were equal and there's - // no payload; return Equal! - "core::cmp::Ordering::Equal,".to_string() - } - }, - ); - - buf.push_str(INDENT); - buf.push_str(INDENT); - buf.push_str("}\n"); - buf.push_str(INDENT); - buf.push('}'); - - add_decl(impls, opt_impl, architecture, buf); - } - - // The Clone impl for the tag union - { - let opt_impl_prefix = if typ.has_pointer(types) { - String::new() - } else { - format!("impl Copy for {name} {{}}\n\n") - }; - - let opt_impl = Some(format!("{opt_impl_prefix}impl Clone for {name}")); - let body = match recursiveness { - Recursiveness::Recursive => RECURSIVE_TAG_UNION_CLONE.to_string(), - Recursiveness::NonRecursive => { - let mut buf = r#"fn clone(&self) -> Self { - let mut answer = unsafe { -"# - .to_string(); - - write_impl_tags( - 3, - tags.iter(), - &discriminant_name, - &mut buf, - |tag_name, opt_payload_id| { - if opt_payload_id.is_some() { - match recursiveness { - Recursiveness::Recursive => { - format!( - r#"Self {{ - {union_name} {{ - {tag_name}: self.pointer - }} - }},"#, - ) - } - Recursiveness::NonRecursive => { - format!( - r#"Self {{ - {tag_name}: {actual_self}.{tag_name}.clone(), - }},"#, - ) - } - } - } else { - // when there's no payload, initialize to garbage memory. - format!( - r#"core::mem::transmute::< - core::mem::MaybeUninit<{name}>, - {name}, - >(core::mem::MaybeUninit::uninit()),"#, - ) - } - }, - ); - - buf.push_str( - r#" - }; - - answer.set_discriminant(self.variant()); - - answer - }"#, - ); - - buf - } - }; - - add_decl(impls, opt_impl, architecture, body); - } - - // The Hash impl for the tag union - { - let opt_impl = Some(format!("impl core::hash::Hash for {name}")); - let mut buf = r#"fn hash(&self, state: &mut H) {"#.to_string(); - - write_impl_tags( - 2, - tags.iter(), - &discriminant_name, - &mut buf, - |tag_name, opt_payload_id| { - let hash_tag = format!("{discriminant_name}::{tag_name}.hash(state)"); - - if opt_payload_id.is_some() { - format!( - r#"unsafe {{ - {hash_tag}; - {actual_self}.{tag_name}.hash(state); - }},"# - ) - } else { - format!("{},", hash_tag) - } - }, - ); - - buf.push_str(INDENT); - buf.push('}'); - - add_decl(impls, opt_impl, architecture, buf); - } - - // The Debug impl for the tag union - { - let opt_impl = Some(format!("impl core::fmt::Debug for {name}")); - let mut buf = format!( - r#"fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {{ - f.write_str("{name}::")?; - - unsafe {{ -"# - ); - - write_impl_tags( - 3, - tags.iter(), - &discriminant_name, - &mut buf, - |tag_name, opt_payload_id| match opt_payload_id { - Some(payload_id) => { - // If it's a ManuallyDrop, we need a `*` prefix to dereference it - // (because otherwise we're using ManuallyDrop's Debug instance - // rather than the Debug instance of the value it wraps). - let payload_type = types.get(payload_id); - let deref_str = if payload_type.has_pointer(types) { - "&*" - } else { - "&" - }; - - let fields_str = match payload_type { - RocType::RocStr - | RocType::Bool - | RocType::Num(_) - | RocType::RocList(_) - | RocType::RocDict(_, _) - | RocType::RocSet(_) - | RocType::RocBox(_) - | RocType::TagUnion(_) - | RocType::RecursivePointer { .. } => { - format!(".field({deref_str}{actual_self}.{tag_name})") - } - RocType::Struct { fields, .. } => { - let mut buf = Vec::new(); - - for (label, _) in fields { - buf.push(format!( - ".field(&({deref_str}{actual_self}.{tag_name}).{label})" - )); - } - - buf.join("\n") - } - RocType::TagUnionPayload { fields, .. } => { - let mut buf = Vec::new(); - - for (label, _) in fields { - // Needs an "f" prefix - buf.push(format!( - ".field(&({deref_str}{actual_self}.{tag_name}).f{label})" - )); - } - - buf.join("\n") - } - }; - - format!( - r#"f.debug_tuple("{tag_name}") - {fields_str} - .finish(),"#, - ) - } - None => format!(r#"f.write_str("{tag_name}"),"#), - }, - ); - - buf.push_str(INDENT); - buf.push_str(INDENT); - buf.push_str("}\n"); - buf.push_str(INDENT); - buf.push('}'); - - add_decl(impls, opt_impl, architecture, buf); - } -} - -fn write_impl_tags< - 'a, - I: IntoIterator)>, - F: Fn(&str, Option) -> String, ->( - indentations: usize, - tags: I, - discriminant_name: &str, - buf: &mut String, - to_branch_str: F, -) { - write_indents(indentations, buf); - - buf.push_str("match self.variant() {\n"); - - for (tag_name, opt_payload_id) in tags { - let branch_str = to_branch_str(tag_name, *opt_payload_id); - - write_indents(indentations + 1, buf); - - buf.push_str(&format!( - "{discriminant_name}::{tag_name} => {branch_str}\n" - )); - } - - write_indents(indentations, buf); - - buf.push_str("}\n"); -} - -fn add_enumeration, S: AsRef + Display>( - name: &str, - architecture: Architecture, - typ: &RocType, - tags: I, - types: &Types, - impls: &mut Impls, -) { - let tag_bytes: usize = UnionLayout::discriminant_size(tags.len()) - .stack_size() - .try_into() - .unwrap(); - - let derive = derive_str(typ, types, false); - let repr_bytes = tag_bytes * 8; - - // e.g. "#[repr(u8)]\npub enum Foo {\n" - let mut buf = format!("{derive}\n#[repr(u{repr_bytes})]\npub enum {name} {{\n"); - - // Debug impls should never vary by architecture. - let mut debug_buf = format!( - r#"impl core::fmt::Debug for {name} {{ - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {{ - match self {{ -"# - ); - - for (index, tag_name) in tags.enumerate() { - buf.push_str(&format!("{INDENT}{tag_name} = {index},\n")); - - write_indents(3, &mut debug_buf); - - debug_buf.push_str(&format!( - "Self::{tag_name} => f.write_str(\"{name}::{tag_name}\"),\n" - )); - } - - buf.push_str(&format!( - "}}\n\n{debug_buf}{INDENT}{INDENT}}}\n{INDENT}}}\n}}" - )); - - add_decl(impls, None, architecture, buf); -} - -fn add_struct( - name: &str, - architecture: Architecture, - fields: &[(S, TypeId)], - struct_id: TypeId, - types: &Types, - impls: &mut Impls, - is_tag_union_payload: bool, -) { - let derive = derive_str(types.get(struct_id), types, true); - let pub_str = if is_tag_union_payload { "" } else { "pub " }; - let mut buf = format!("{derive}\n#[repr(C)]\n{pub_str}struct {name} {{\n"); - - for (label, type_id) in fields { - let type_str = type_name(*type_id, types); - - // Tag union payloads have numbered fields, so we prefix them - // with an "f" because Rust doesn't allow struct fields to be numbers. - let label = if is_tag_union_payload { - format!("f{label}") - } else { - format!("{label}") - }; - - buf.push_str(&format!("{INDENT}pub {label}: {type_str},\n",)); - } - - buf.push('}'); - - add_decl(impls, None, architecture, buf); -} - -fn type_name(id: TypeId, types: &Types) -> String { - match types.get(id) { - RocType::RocStr => "roc_std::RocStr".to_string(), - RocType::Bool => "bool".to_string(), - RocType::Num(RocNum::U8) => "u8".to_string(), - RocType::Num(RocNum::U16) => "u16".to_string(), - RocType::Num(RocNum::U32) => "u32".to_string(), - RocType::Num(RocNum::U64) => "u64".to_string(), - RocType::Num(RocNum::U128) => "roc_std::U128".to_string(), - RocType::Num(RocNum::I8) => "i8".to_string(), - RocType::Num(RocNum::I16) => "i16".to_string(), - RocType::Num(RocNum::I32) => "i32".to_string(), - RocType::Num(RocNum::I64) => "i64".to_string(), - RocType::Num(RocNum::I128) => "roc_std::I128".to_string(), - RocType::Num(RocNum::F32) => "f32".to_string(), - RocType::Num(RocNum::F64) => "f64".to_string(), - RocType::Num(RocNum::F128) => "roc_std::F128".to_string(), - RocType::Num(RocNum::Dec) => "roc_std::RocDec".to_string(), - RocType::RocDict(key_id, val_id) => format!( - "roc_std::RocDict<{}, {}>", - type_name(*key_id, types), - type_name(*val_id, types) - ), - RocType::RocSet(elem_id) => format!("roc_std::RocSet<{}>", type_name(*elem_id, types)), - RocType::RocList(elem_id) => format!("roc_std::RocList<{}>", type_name(*elem_id, types)), - RocType::RocBox(elem_id) => format!("roc_std::RocBox<{}>", type_name(*elem_id, types)), - RocType::Struct { name, .. } - | RocType::TagUnionPayload { name, .. } - | RocType::TagUnion(RocTagUnion::NonRecursive { name, .. }) - | RocType::TagUnion(RocTagUnion::Recursive { name, .. }) - | RocType::TagUnion(RocTagUnion::Enumeration { name, .. }) - | RocType::TagUnion(RocTagUnion::NullableWrapped { name, .. }) - | RocType::TagUnion(RocTagUnion::NullableUnwrapped { name, .. }) - | RocType::TagUnion(RocTagUnion::NonNullableUnwrapped { name, .. }) => name.clone(), - RocType::RecursivePointer(content) => type_name(*content, types), - } -} - -/// This explicitly asks for whether to include Debug because in the very specific -/// case of a struct that's a payload for a recursive tag union, typ.has_enumeration() -/// will return true, but actually we want to derive Debug here anyway. -fn derive_str(typ: &RocType, types: &Types, include_debug: bool) -> String { - let mut buf = "#[derive(Clone, ".to_string(); - - if !typ.has_pointer(types) { - buf.push_str("Copy, "); - } - - if include_debug { - buf.push_str("Debug, "); - } - - if !typ.has_enumeration(types) { - buf.push_str("Default, "); - } - - if !typ.has_float(types) { - buf.push_str("Eq, Ord, Hash, "); - } - - buf.push_str("PartialEq, PartialOrd)]"); - - buf -} - -#[allow(clippy::too_many_arguments)] -fn add_nullable_unwrapped( - name: &str, - architecture: Architecture, - id: TypeId, - null_tag: &str, - non_null_tag: &str, - non_null_payload: TypeId, - _null_represents_first_tag: bool, // TODO use this! - types: &Types, - impls: &mut Impls, -) { - let mut tag_names = vec![null_tag.to_string(), non_null_tag.to_string()]; - - tag_names.sort(); - - let discriminant_name = add_discriminant(name, architecture, tag_names, types, impls); - let payload_type = types.get(non_null_payload); - let payload_type_name = type_name(non_null_payload, types); - let has_pointer = payload_type.has_pointer(types); - - // The opaque struct for the tag union - { - // This struct needs its own Clone impl because it has - // a refcount to bump - let derive_extras = if types.get(id).has_float(types) { - "" - } else { - ", Eq, Ord, Hash" - }; - let body = format!( - r#"#[repr(C)] -#[derive(PartialEq, PartialOrd{derive_extras})] -pub struct {name} {{ - pointer: *mut core::mem::ManuallyDrop<{payload_type_name}>, -}}"# - ); - - add_decl(impls, None, architecture, body); - } - - // The impl for the tag union - { - let opt_impl = Some(format!("impl {name}")); - - add_decl( - impls, - opt_impl.clone(), - architecture, - RECURSIVE_TAG_UNION_STORAGE.to_string(), - ); - - add_decl( - impls, - opt_impl.clone(), - architecture, - format!( - r#"{VARIANT_DOC_COMMENT} - pub fn variant(&self) -> {discriminant_name} {{ - if self.pointer.is_null() {{ - {discriminant_name}::{null_tag} - }} else {{ - {discriminant_name}::{non_null_tag} - }} - }}"# - ), - ); - - let owned_ret_type; - let borrowed_ret_type; - let payload_args; - let args_to_payload; - let owned_ret; - let borrowed_ret; - - match payload_type { - RocType::RocStr - | RocType::Bool - | RocType::Num(_) - | RocType::RocList(_) - | RocType::RocDict(_, _) - | RocType::RocSet(_) - | RocType::RocBox(_) - | RocType::TagUnion(_) - | RocType::RecursivePointer { .. } => { - owned_ret_type = type_name(non_null_payload, types); - borrowed_ret_type = format!("&{}", owned_ret_type); - payload_args = format!("arg: {owned_ret_type}"); - args_to_payload = "arg".to_string(); - owned_ret = "payload".to_string(); - borrowed_ret = format!("&{owned_ret}"); - } - RocType::Struct { fields, .. } => { - let answer = tag_union_struct_help(fields.iter(), non_null_payload, types, false); - - payload_args = answer.payload_args; - args_to_payload = answer.args_to_payload; - owned_ret = answer.owned_ret; - borrowed_ret = answer.borrowed_ret; - owned_ret_type = answer.owned_ret_type; - borrowed_ret_type = answer.borrowed_ret_type; - } - RocType::TagUnionPayload { fields, .. } => { - let answer = tag_union_struct_help(fields.iter(), non_null_payload, types, true); - - payload_args = answer.payload_args; - args_to_payload = answer.args_to_payload; - owned_ret = answer.owned_ret; - borrowed_ret = answer.borrowed_ret; - owned_ret_type = answer.owned_ret_type; - borrowed_ret_type = answer.borrowed_ret_type; - } - }; - - // Add a convenience constructor function for the tag with the payload, e.g. - // - // /// Construct a tag named Cons, with the appropriate payload - // pub fn Cons(payload: roc_std::RocStr) -> Self { - // let size = core::mem::size_of::(); - // let align = core::mem::align_of::(); - // - // unsafe { - // let pointer = - // roc_alloc(size, align as u32) as *mut core::mem::ManuallyDrop; - // - // *pointer = core::mem::ManuallyDrop::new(payload); - // - // Self { pointer } - // } - // } - add_decl( - impls, - opt_impl.clone(), - architecture, - format!( - r#"/// Construct a tag named {non_null_tag}, with the appropriate payload - pub fn {non_null_tag}({payload_args}) -> Self {{ - let payload_align = core::mem::align_of::<{payload_type_name}>(); - let self_align = core::mem::align_of::(); - let size = self_align + core::mem::size_of::<{payload_type_name}>(); - let payload = {args_to_payload}; - - unsafe {{ - // Store the payload at `self_align` bytes after the allocation, - // to leave room for the refcount. - let alloc_ptr = crate::roc_alloc(size, payload_align as u32); - let payload_ptr = alloc_ptr.cast::().add(self_align).cast::>(); - - *payload_ptr = payload; - - // The reference count is stored immediately before the payload, - // which isn't necessarily the same as alloc_ptr - e.g. when alloc_ptr - // needs an alignment of 16. - let storage_ptr = payload_ptr.cast::().sub(1); - storage_ptr.write(roc_std::Storage::new_reference_counted()); - - Self {{ pointer: payload_ptr }} - }} - }}"#, - ), - ); - - { - let assign_payload = if has_pointer { - "core::mem::ManuallyDrop::take(&mut *self.pointer)" - } else { - "*self.pointer" - }; - - add_decl( - impls, - opt_impl.clone(), - architecture, - format!( - r#"/// Unsafely assume the given {name} has a .variant() of {non_null_tag} and convert it to {non_null_tag}'s payload. - /// (Always examine .variant() first to make sure this is the correct variant!) - /// Panics in debug builds if the .variant() doesn't return {non_null_tag}. - pub unsafe fn into_{non_null_tag}(self) -> {owned_ret_type} {{ - debug_assert_eq!(self.variant(), {discriminant_name}::{non_null_tag}); - - let payload = {assign_payload}; - - core::mem::drop::(self); - - {owned_ret} - }}"#, - ), - ); - } - - add_decl( - impls, - opt_impl.clone(), - architecture, - format!( - r#"/// Unsafely assume the given {name} has a .variant() of {non_null_tag} and return its payload. - /// (Always examine .variant() first to make sure this is the correct variant!) - /// Panics in debug builds if the .variant() doesn't return {non_null_tag}. - pub unsafe fn as_{non_null_tag}(&self) -> {borrowed_ret_type} {{ - debug_assert_eq!(self.variant(), {discriminant_name}::{non_null_tag}); - - let payload = &*self.pointer; - - {borrowed_ret} - }}"#, - ), - ); - - // Add a convenience constructor function for the nullable tag, e.g. - // - // /// A tag named Nil, which has no payload. - // pub const Nil: Self = Self { - // pointer: core::ptr::null_mut(), - // }; - add_decl( - impls, - opt_impl.clone(), - architecture, - format!( - r#"/// A tag named {null_tag}, which has no payload. - pub const {null_tag}: Self = Self {{ - pointer: core::ptr::null_mut(), - }};"#, - ), - ); - - add_decl( - impls, - opt_impl.clone(), - architecture, - format!( - r#"/// Other `into_` methods return a payload, but since the {null_tag} tag - /// has no payload, this does nothing and is only here for completeness. - pub fn into_{null_tag}(self) {{ - () - }}"#, - ), - ); - - add_decl( - impls, - opt_impl, - architecture, - format!( - r#"/// Other `as` methods return a payload, but since the {null_tag} tag - /// has no payload, this does nothing and is only here for completeness. - pub unsafe fn as_{null_tag}(&self) {{ - () - }}"#, - ), - ); - } - - // The Clone impl for the tag union - { - // Note that these never have Copy because they always contain a pointer. - let opt_impl = Some(format!("impl Clone for {name}")); - - add_decl( - impls, - opt_impl, - architecture, - RECURSIVE_TAG_UNION_CLONE.to_string(), - ); - } - - // The Drop impl for the tag union - { - let opt_impl = Some(format!("impl Drop for {name}")); - - add_decl( - impls, - opt_impl, - architecture, - format!( - r#"fn drop(&mut self) {{ - if let Some(storage) = self.storage() {{ - // Decrement the refcount and return early if no dealloc is needed - {{ - let mut new_storage = storage.get(); - - if new_storage.is_readonly() {{ - return; - }} - - let needs_dealloc = new_storage.decrease(); - - if !needs_dealloc {{ - // Write the storage back. - storage.set(new_storage); - - return; - }} - }} - - if !self.pointer.is_null() {{ - // If there is a payload, drop it first. - let payload = unsafe {{ core::mem::ManuallyDrop::take(&mut *self.pointer) }}; - - core::mem::drop::<{payload_type_name}>(payload); - }} - - // Dealloc the pointer - unsafe {{ - let alignment = core::mem::align_of::().max(core::mem::align_of::()); - let alloc_ptr = self.pointer.cast::().sub(alignment); - - crate::roc_dealloc( - alloc_ptr as *mut core::ffi::c_void, - alignment as u32, - ); - }} - }} - }}"# - ), - ); - } - - // The Debug impl for the tag union - { - let opt_impl = Some(format!("impl core::fmt::Debug for {name}")); - let extra_deref = if has_pointer { "*" } else { "" }; - - let fields_str = match payload_type { - RocType::RocStr - | RocType::Bool - | RocType::Num(_) - | RocType::RocList(_) - | RocType::RocDict(_, _) - | RocType::RocSet(_) - | RocType::RocBox(_) - | RocType::TagUnion(_) - | RocType::RecursivePointer { .. } => { - format!( - r#"f.debug_tuple("{non_null_tag}").field(&*{extra_deref}self.pointer).finish()"# - ) - } - RocType::Struct { fields, .. } => { - let mut buf = Vec::new(); - - for (label, _) in fields { - buf.push(format!(".field(&(&*{extra_deref}self.pointer).{label})")); - } - - buf.join(&format!("\n{INDENT}{INDENT}{INDENT}{INDENT}{INDENT}")) - } - RocType::TagUnionPayload { fields, .. } => { - let mut buf = Vec::new(); - - for (label, _) in fields { - // Needs an "f" prefix - buf.push(format!(".field(&(&*{extra_deref}self.pointer).f{label})")); - } - - buf.join(&format!("\n{INDENT}{INDENT}{INDENT}{INDENT}{INDENT}")) - } - }; - - let body = format!( - r#"fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {{ - if self.pointer.is_null() {{ - f.write_str("{name}::{null_tag}") - }} else {{ - f.write_str("{name}::")?; - - unsafe {{ - f.debug_tuple("{non_null_tag}") - {fields_str} - .finish() - }} - }} - }}"# - ); - - add_decl(impls, opt_impl, architecture, body); - } -} - -fn arch_to_str(architecture: &Architecture) -> &'static str { - match architecture { - Architecture::X86_64 => "x86_64", - Architecture::X86_32 => "x86", - Architecture::Aarch64 => "aarch64", - Architecture::Aarch32 => "arm", - Architecture::Wasm32 => "wasm32", - } -} - -fn write_indents(indentations: usize, buf: &mut String) { - for _ in 0..indentations { - buf.push_str(INDENT); - } -} - -fn max_pointer_tagged_variants(architecture: Architecture) -> usize { - match architecture { - // On a 64-bit system, pointers have 3 bits that are unused, so return 2^3 = 8 - Architecture::X86_64 | Architecture::Aarch64 => 8, - // On a 32-bit system, pointers have 2 bits that are unused, so return 2^4 = 4 - Architecture::X86_32 | Architecture::Aarch32 | Architecture::Wasm32 => 4, - } -} - -#[inline(always)] -fn tagged_pointer_bitmask(architecture: Architecture) -> u8 { - match architecture { - // On a 64-bit system, pointers have 3 bits that are unused - Architecture::X86_64 | Architecture::Aarch64 => 0b0000_0111, - // On a 32-bit system, pointers have 2 bits that are unused - Architecture::X86_32 | Architecture::Aarch32 | Architecture::Wasm32 => 0b0000_0011, - } -} - -struct StructIngredients { - payload_args: String, - args_to_payload: String, - owned_ret: String, - borrowed_ret: String, - owned_ret_type: String, - borrowed_ret_type: String, -} - -fn tag_union_struct_help<'a, I: Iterator, L: Display + PartialOrd + 'a>( - fields: I, - payload_id: TypeId, - types: &Types, - is_tag_union_payload: bool, -) -> StructIngredients { - let mut sorted_fields = fields.collect::>(); - - sorted_fields.sort_by(|(label1, _), (label2, _)| label1.partial_cmp(label2).unwrap()); - - let mut ret_types = Vec::new(); - let mut ret_values = Vec::new(); - - for (label, type_id) in sorted_fields.iter() { - let label = if is_tag_union_payload { - // Tag union payload fields need "f" prefix - // because they're numbers - format!("f{}", label) - } else { - format!("{}", label) - }; - - ret_values.push(format!("payload.{label}")); - ret_types.push(type_name(*type_id, types)); - } - - let payload_type_name = type_name(payload_id, types); - let payload_args = ret_types - .iter() - .enumerate() - .map(|(index, typ)| format!("arg{index}: {typ}")) - .collect::>() - .join(", "); - let args_to_payload = format!( - "core::mem::ManuallyDrop::new({payload_type_name} {{\n{}\n{INDENT}{INDENT}{INDENT}{INDENT}}})", - sorted_fields - .iter() - .enumerate() - .map(|(index, (label, _))| { - let mut indents = String::new(); - - for _ in 0..5 { - indents.push_str(INDENT); - } - - let label = if is_tag_union_payload { - // Tag union payload fields need "f" prefix - // because they're numbers - format!("f{}", label) - } else { - format!("{}", label) - }; - - format!("{indents}{label}: arg{index},") - }) - .collect::>() - .join("\n") - ); - let owned_ret; - let borrowed_ret; - let owned_ret_type; - let borrowed_ret_type; - - if ret_types.len() == 1 { - owned_ret_type = ret_types.join(""); - borrowed_ret_type = format!("&{owned_ret_type}"); - - let ret_val = ret_values.first().unwrap(); - owned_ret = format!("\n{INDENT}{INDENT}{ret_val}"); - borrowed_ret = format!("\n{INDENT}{INDENT}&{ret_val}"); - } else { - owned_ret_type = format!("({})", ret_types.join(", ")); - borrowed_ret_type = format!( - "({})", - ret_types - .iter() - .map(|ret_type| { format!("&{ret_type}") }) - .collect::>() - .join(", ") - ); - owned_ret = { - let lines = ret_values - .iter() - .map(|line| format!("\n{INDENT}{INDENT}{INDENT}{line}")) - .collect::>() - .join(", "); - - format!("({lines}\n{INDENT}{INDENT})") - }; - borrowed_ret = { - let lines = ret_values - .iter() - .map(|line| format!("\n{INDENT}{INDENT}{INDENT}&{line}")) - .collect::>() - .join(", "); - - format!("({lines}\n{INDENT}{INDENT})") - }; - } - - StructIngredients { - payload_args, - args_to_payload, - owned_ret, - borrowed_ret, - owned_ret_type, - borrowed_ret_type, - } -} diff --git a/bindgen/src/bindgen_zig.rs b/bindgen/src/bindgen_zig.rs deleted file mode 100644 index d16c500d04..0000000000 --- a/bindgen/src/bindgen_zig.rs +++ /dev/null @@ -1,18 +0,0 @@ -use std::io; - -static TEMPLATE: &[u8] = include_bytes!("../templates/template.zig"); - -pub fn write_template(writer: &mut impl io::Write) -> io::Result<()> { - writer.write_all(TEMPLATE)?; - - Ok(()) -} - -pub fn write_bindings(_writer: &mut impl io::Write) -> io::Result<()> { - // extern "C" { - // #[link_name = "roc__mainForHost_1_exposed"] - // fn roc_main() -> RocStr; - // } - - Ok(()) -} diff --git a/bindgen/src/lib.rs b/bindgen/src/lib.rs deleted file mode 100644 index 5edf2775ea..0000000000 --- a/bindgen/src/lib.rs +++ /dev/null @@ -1,8 +0,0 @@ -pub mod bindgen; -pub mod bindgen_c; -pub mod bindgen_rs; -pub mod bindgen_zig; -pub mod enums; -pub mod load; -pub mod structs; -pub mod types; diff --git a/bindgen/src/load.rs b/bindgen/src/load.rs deleted file mode 100644 index 3688ebfc83..0000000000 --- a/bindgen/src/load.rs +++ /dev/null @@ -1,116 +0,0 @@ -use crate::bindgen::Env; -use crate::types::Types; -use bumpalo::Bump; -use roc_can::{ - def::{Declaration, Def}, - pattern::Pattern, -}; -use roc_load::{LoadedModule, Threading}; -use roc_mono::layout::LayoutCache; -use roc_reporting::report::RenderTarget; -use roc_target::Architecture; -use std::io; -use std::path::{Path, PathBuf}; -use strum::IntoEnumIterator; -use target_lexicon::Triple; - -pub fn load_types( - full_file_path: PathBuf, - dir: &Path, - threading: Threading, -) -> Result, io::Error> { - let target_info = (&Triple::host()).into(); - - let arena = &Bump::new(); - let subs_by_module = Default::default(); - let LoadedModule { - module_id: home, - mut can_problems, - mut type_problems, - mut declarations_by_id, - mut solved, - interns, - .. - } = roc_load::load_and_typecheck( - arena, - full_file_path, - dir, - subs_by_module, - target_info, - RenderTarget::Generic, - threading, - ) - .expect("Problem loading platform module"); - - let decls = declarations_by_id.remove(&home).unwrap(); - let subs = solved.inner_mut(); - - let can_problems = can_problems.remove(&home).unwrap_or_default(); - let type_problems = type_problems.remove(&home).unwrap_or_default(); - - if !can_problems.is_empty() || !type_problems.is_empty() { - todo!( - "Gracefully report compilation problems during bindgen: {:?}, {:?}", - can_problems, - type_problems - ); - } - - let mut answer = Vec::with_capacity(Architecture::iter().size_hint().0); - - for architecture in Architecture::iter() { - let defs_iter = decls.iter().flat_map(|decl| match decl { - Declaration::Declare(def) => { - vec![def.clone()] - } - Declaration::DeclareRec(defs, cycle_mark) => { - if cycle_mark.is_illegal(subs) { - Vec::new() - } else { - defs.clone() - } - } - Declaration::Builtin(..) => { - unreachable!("Builtin decl in userspace module?") - } - Declaration::InvalidCycle(..) => Vec::new(), - }); - - let vars_iter = defs_iter.filter_map( - |Def { - loc_pattern, - pattern_vars, - .. - }| { - if let Pattern::Identifier(sym) = loc_pattern.value { - let var = pattern_vars - .get(&sym) - .expect("Indetifier known but it has no var?"); - - Some(*var) - } else { - // figure out if we need to export non-identifier defs - when - // would that happen? - None - } - }, - ); - - let mut layout_cache = LayoutCache::new(architecture.into()); - let mut env = Env { - arena, - layout_cache: &mut layout_cache, - interns: &interns, - subs, - struct_names: Default::default(), - enum_names: Default::default(), - pending_recursive_types: Default::default(), - known_recursive_types: Default::default(), - }; - let types = env.vars_to_types(vars_iter); - - answer.push((architecture, types)); - } - - Ok(answer) -} diff --git a/bindgen/src/main.rs b/bindgen/src/main.rs deleted file mode 100644 index 1a5d91752d..0000000000 --- a/bindgen/src/main.rs +++ /dev/null @@ -1,115 +0,0 @@ -use clap::Parser; -use roc_bindgen::bindgen_rs; -use roc_bindgen::load::load_types; -use roc_load::Threading; -use std::ffi::OsStr; -use std::fs::File; -use std::io::{ErrorKind, Write}; -use std::path::PathBuf; -use std::process; - -/// Printed in error messages if you try to use an unsupported extension. -const SUPPORTED_EXTENSIONS: &str = ".c, .rs, .zig, and .json"; - -// TODO add an option for --targets so that you can specify -// e.g. 64-bit, 32-bit, *and* 16-bit (which can matter for alignment because of pointers) -#[derive(Debug, Parser)] -#[clap(about)] -struct Opts { - /// The path to the platform's Package-Config.roc file - platform_module: PathBuf, - - /// The output file, e.g. `test.rs` - dest: PathBuf, -} - -enum OutputType { - Rust, - C, - Zig, - Json, -} - -pub fn main() { - let opts = Opts::parse(); - let input_path = opts.platform_module; - let cwd = std::env::current_dir().unwrap(); - let output_path = opts.dest; - let output_type = match output_path.extension().and_then(OsStr::to_str) { - Some("rs") => OutputType::Rust, - Some("c") => OutputType::C, - Some("zig") => OutputType::Zig, - Some("json") => OutputType::Json, - Some(other) => { - eprintln!( - "Unsupported output file extension: \".{}\" - currently supported extensions are {}", - other, - SUPPORTED_EXTENSIONS - ); - - process::exit(1); - } - None => { - eprintln!("The output file path needs to have a file extension in order to tell what output format to use. Currently supported extensions are {}", SUPPORTED_EXTENSIONS); - - process::exit(1); - } - }; - - match load_types(input_path.clone(), &cwd, Threading::AllAvailable) { - Ok(types_by_architecture) => { - let mut buf; - - match output_type { - OutputType::Rust => { - buf = std::str::from_utf8(bindgen_rs::HEADER).unwrap().to_string(); - let body = bindgen_rs::emit(&types_by_architecture); - - buf.push_str(&body); - } - OutputType::C => todo!("TODO: Generate bindings for C"), - OutputType::Zig => todo!("TODO: Generate bindings for Zig"), - OutputType::Json => todo!("TODO: Generate bindings for JSON"), - }; - - let mut file = File::create(output_path.clone()).unwrap_or_else(|err| { - eprintln!( - "Unable to create output file {} - {:?}", - output_path.display(), - err - ); - - process::exit(1); - }); - - file.write_all(buf.as_bytes()).unwrap_or_else(|err| { - eprintln!( - "Unable to write bindings to output file {} - {:?}", - output_path.display(), - err - ); - - process::exit(1); - }); - - println!( - "🎉 Generated type declarations in:\n\n\t{}", - output_path.display() - ); - } - Err(err) => match err.kind() { - ErrorKind::NotFound => { - eprintln!("Platform module file not found: {}", input_path.display()); - process::exit(1); - } - error => { - eprintln!( - "Error loading platform module file {} - {:?}", - input_path.display(), - error - ); - process::exit(1); - } - }, - } -} diff --git a/bindgen/src/types.rs b/bindgen/src/types.rs deleted file mode 100644 index d5b4327d32..0000000000 --- a/bindgen/src/types.rs +++ /dev/null @@ -1,649 +0,0 @@ -use core::mem::align_of; -use roc_builtins::bitcode::{FloatWidth, IntWidth}; -use roc_collections::VecMap; -use roc_mono::layout::UnionLayout; -use roc_std::RocDec; -use roc_target::TargetInfo; -use std::convert::TryInto; - -#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord)] -pub struct TypeId(usize); - -impl TypeId { - /// Used when making recursive pointers, which need to temporarily - /// have *some* TypeId value until we later in the process determine - /// their real TypeId and can go back and fix them up. - pub(crate) const PENDING: Self = Self(usize::MAX); -} - -#[derive(Default, Debug, Clone)] -pub struct Types { - by_id: Vec, - - /// Dependencies - that is, which type depends on which other type. - /// This is important for declaration order in C; we need to output a - /// type declaration earlier in the file than where it gets referenced by another type. - deps: VecMap>, -} - -impl Types { - pub fn with_capacity(cap: usize) -> Self { - Self { - by_id: Vec::with_capacity(cap), - deps: VecMap::with_capacity(cap), - } - } - - pub fn add(&mut self, typ: RocType) -> TypeId { - let id = TypeId(self.by_id.len()); - - self.by_id.push(typ); - - id - } - - pub fn depends(&mut self, id: TypeId, depends_on: TypeId) { - self.deps.get_or_insert(id, Vec::new).push(depends_on); - } - - pub fn get(&self, id: TypeId) -> &RocType { - match self.by_id.get(id.0) { - Some(typ) => typ, - None => unreachable!(), - } - } - - pub fn replace(&mut self, id: TypeId, typ: RocType) { - debug_assert!(self.by_id.get(id.0).is_some()); - - self.by_id[id.0] = typ; - } - - pub fn ids(&self) -> impl ExactSizeIterator { - (0..self.by_id.len()).map(TypeId) - } - - pub fn sorted_ids(&self) -> Vec { - use roc_collections::{ReferenceMatrix, TopologicalSort}; - - let mut matrix = ReferenceMatrix::new(self.by_id.len()); - - for type_id in self.ids() { - for dep in self.deps.get(&type_id).iter().flat_map(|x| x.iter()) { - matrix.set_row_col(type_id.0, dep.0, true); - } - } - - match matrix.topological_sort_into_groups() { - TopologicalSort::Groups { groups } => groups - .into_iter() - .flatten() - .rev() - .map(|n| TypeId(n as usize)) - .collect(), - TopologicalSort::HasCycles { - groups: _, - nodes_in_cycle, - } => unreachable!("Cyclic type definitions: {:?}", nodes_in_cycle), - } - } - - pub fn iter(&self) -> impl ExactSizeIterator { - TypesIter { - types: self.by_id.as_slice(), - len: self.by_id.len(), - } - } -} - -struct TypesIter<'a> { - types: &'a [RocType], - len: usize, -} - -impl<'a> ExactSizeIterator for TypesIter<'a> { - fn len(&self) -> usize { - self.len - } -} - -impl<'a> Iterator for TypesIter<'a> { - type Item = &'a RocType; - - fn next(&mut self) -> Option { - let len = self.len; - let answer = self.types.get(self.types.len() - len); - - self.len = len.saturating_sub(1); - - answer - } -} - -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub enum RocType { - RocStr, - Bool, - Num(RocNum), - RocList(TypeId), - RocDict(TypeId, TypeId), - RocSet(TypeId), - RocBox(TypeId), - TagUnion(RocTagUnion), - Struct { - name: String, - fields: Vec<(String, TypeId)>, - }, - TagUnionPayload { - name: String, - fields: Vec<(usize, TypeId)>, - }, - /// A recursive pointer, e.g. in StrConsList : [Nil, Cons Str StrConsList], - /// this would be the field of Cons containing the (recursive) StrConsList type, - /// and the TypeId is the TypeId of StrConsList itself. - RecursivePointer(TypeId), -} - -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub enum RocNum { - I8, - U8, - I16, - U16, - I32, - U32, - I64, - U64, - I128, - U128, - F32, - F64, - F128, - Dec, -} - -impl RocNum { - fn size(&self) -> usize { - use core::mem::size_of; - use RocNum::*; - - match self { - I8 => size_of::(), - U8 => size_of::(), - I16 => size_of::(), - U16 => size_of::(), - I32 => size_of::(), - U32 => size_of::(), - I64 => size_of::(), - U64 => size_of::(), - I128 => size_of::(), - U128 => size_of::(), - F32 => size_of::(), - F64 => size_of::(), - F128 => todo!(), - Dec => size_of::(), - } - } - - fn alignment(&self, target_info: TargetInfo) -> usize { - use RocNum::*; - - match self { - I8 => IntWidth::I8.alignment_bytes(target_info) as usize, - U8 => IntWidth::U8.alignment_bytes(target_info) as usize, - I16 => IntWidth::I16.alignment_bytes(target_info) as usize, - U16 => IntWidth::U16.alignment_bytes(target_info) as usize, - I32 => IntWidth::I32.alignment_bytes(target_info) as usize, - U32 => IntWidth::U32.alignment_bytes(target_info) as usize, - I64 => IntWidth::I64.alignment_bytes(target_info) as usize, - U64 => IntWidth::U64.alignment_bytes(target_info) as usize, - I128 => IntWidth::I128.alignment_bytes(target_info) as usize, - U128 => IntWidth::U128.alignment_bytes(target_info) as usize, - F32 => FloatWidth::F32.alignment_bytes(target_info) as usize, - F64 => FloatWidth::F64.alignment_bytes(target_info) as usize, - F128 => FloatWidth::F128.alignment_bytes(target_info) as usize, - Dec => align_of::(), - } - } -} - -impl RocType { - /// Useful when determining whether to derive Copy in a Rust type. - pub fn has_pointer(&self, types: &Types) -> bool { - match self { - RocType::Bool - | RocType::Num(_) - | RocType::TagUnion(RocTagUnion::Enumeration { .. }) => false, - RocType::RocStr - | RocType::RocList(_) - | RocType::RocDict(_, _) - | RocType::RocSet(_) - | RocType::RocBox(_) - | RocType::TagUnion(RocTagUnion::NonNullableUnwrapped { .. }) - | RocType::TagUnion(RocTagUnion::NullableUnwrapped { .. }) - | RocType::TagUnion(RocTagUnion::NullableWrapped { .. }) - | RocType::TagUnion(RocTagUnion::Recursive { .. }) - | RocType::RecursivePointer { .. } => true, - RocType::TagUnion(RocTagUnion::NonRecursive { tags, .. }) => tags - .iter() - .any(|(_, payloads)| payloads.iter().any(|id| types.get(*id).has_pointer(types))), - RocType::Struct { fields, .. } => fields - .iter() - .any(|(_, type_id)| types.get(*type_id).has_pointer(types)), - RocType::TagUnionPayload { fields, .. } => fields - .iter() - .any(|(_, type_id)| types.get(*type_id).has_pointer(types)), - } - } - - /// Useful when determining whether to derive Eq, Ord, and Hash in a Rust type. - pub fn has_float(&self, types: &Types) -> bool { - self.has_float_help(types, &[]) - } - - fn has_float_help(&self, types: &Types, do_not_recurse: &[TypeId]) -> bool { - match self { - RocType::Num(num) => { - use RocNum::*; - - match num { - F32 | F64 | F128 => true, - I8 | U8 | I16 | U16 | I32 | U32 | I64 | U64 | I128 | U128 | Dec => false, - } - } - RocType::RocStr - | RocType::Bool - | RocType::TagUnion(RocTagUnion::Enumeration { .. }) => false, - RocType::RocList(id) | RocType::RocSet(id) | RocType::RocBox(id) => { - types.get(*id).has_float_help(types, do_not_recurse) - } - RocType::RocDict(key_id, val_id) => { - types.get(*key_id).has_float_help(types, do_not_recurse) - || types.get(*val_id).has_float_help(types, do_not_recurse) - } - RocType::Struct { fields, .. } => fields - .iter() - .any(|(_, type_id)| types.get(*type_id).has_float_help(types, do_not_recurse)), - RocType::TagUnionPayload { fields, .. } => fields - .iter() - .any(|(_, type_id)| types.get(*type_id).has_float_help(types, do_not_recurse)), - RocType::TagUnion(RocTagUnion::Recursive { tags, .. }) - | RocType::TagUnion(RocTagUnion::NonRecursive { tags, .. }) => { - tags.iter().any(|(_, payloads)| { - payloads - .iter() - .any(|id| types.get(*id).has_float_help(types, do_not_recurse)) - }) - } - RocType::TagUnion(RocTagUnion::NullableWrapped { non_null_tags, .. }) => { - non_null_tags.iter().any(|(_, _, payloads)| { - payloads - .iter() - .any(|id| types.get(*id).has_float_help(types, do_not_recurse)) - }) - } - RocType::TagUnion(RocTagUnion::NullableUnwrapped { - non_null_payload: content, - .. - }) - | RocType::TagUnion(RocTagUnion::NonNullableUnwrapped { content, .. }) - | RocType::RecursivePointer(content) => { - if do_not_recurse.contains(content) { - false - } else { - let mut do_not_recurse: Vec = do_not_recurse.into(); - - do_not_recurse.push(*content); - - types.get(*content).has_float_help(types, &do_not_recurse) - } - } - } - } - - /// Useful when determining whether to derive Default in a Rust type. - pub fn has_enumeration(&self, types: &Types) -> bool { - match self { - RocType::TagUnion { .. } | RocType::RecursivePointer { .. } => true, - RocType::RocStr | RocType::Bool | RocType::Num(_) => false, - RocType::RocList(id) | RocType::RocSet(id) | RocType::RocBox(id) => { - types.get(*id).has_enumeration(types) - } - RocType::RocDict(key_id, val_id) => { - types.get(*key_id).has_enumeration(types) - || types.get(*val_id).has_enumeration(types) - } - RocType::Struct { fields, .. } => fields - .iter() - .any(|(_, type_id)| types.get(*type_id).has_enumeration(types)), - RocType::TagUnionPayload { fields, .. } => fields - .iter() - .any(|(_, type_id)| types.get(*type_id).has_enumeration(types)), - } - } - - pub fn size(&self, types: &Types, target_info: TargetInfo) -> usize { - use std::mem::size_of; - - match self { - RocType::Bool => size_of::(), - RocType::Num(num) => num.size(), - RocType::RocStr | RocType::RocList(_) | RocType::RocDict(_, _) | RocType::RocSet(_) => { - 3 * target_info.ptr_size() - } - RocType::RocBox(_) => target_info.ptr_size(), - RocType::TagUnion(tag_union) => match tag_union { - RocTagUnion::Enumeration { tags, .. } => size_for_tag_count(tags.len()), - RocTagUnion::NonRecursive { tags, .. } | RocTagUnion::Recursive { tags, .. } => { - // The "unpadded" size (without taking alignment into account) - // is the sum of all the sizes of the fields. - let size_unpadded = tags.iter().fold(0, |total, (_, opt_payload_id)| { - if let Some(payload_id) = opt_payload_id { - let payload = types.get(*payload_id); - - total + payload.size(types, target_info) - } else { - total - } - }); - - // Round up to the next multiple of alignment, to incorporate - // any necessary alignment padding. - // - // e.g. if we have a record with a Str and a U8, that would be a - // size_unpadded of 25, because Str is three 8-byte pointers and U8 is 1 byte, - // but the 8-byte alignment of the pointers means we'll round 25 up to 32. - let discriminant_align = align_for_tag_count(tags.len(), target_info); - let align = self.alignment(types, target_info).max(discriminant_align); - let size_padded = (size_unpadded / align) * align; - - if size_unpadded == size_padded { - // We don't have any alignment padding, which means we can't - // put the discriminant in the padding and the compiler will - // add extra space for it. - let discriminant_size = size_for_tag_count(tags.len()); - - size_padded + discriminant_size.max(align) - } else { - size_padded - } - } - RocTagUnion::NonNullableUnwrapped { .. } => todo!(), - RocTagUnion::NullableWrapped { .. } => todo!(), - RocTagUnion::NullableUnwrapped { .. } => todo!(), - }, - RocType::Struct { fields, .. } => struct_size( - fields.iter().map(|(_, type_id)| *type_id), - types, - target_info, - self.alignment(types, target_info), - ), - RocType::TagUnionPayload { fields, .. } => struct_size( - fields.iter().map(|(_, type_id)| *type_id), - types, - target_info, - self.alignment(types, target_info), - ), - RocType::RecursivePointer { .. } => target_info.ptr_size(), - } - } - - pub fn alignment(&self, types: &Types, target_info: TargetInfo) -> usize { - match self { - RocType::RocStr - | RocType::RocList(_) - | RocType::RocDict(_, _) - | RocType::RocSet(_) - | RocType::RocBox(_) => target_info.ptr_alignment_bytes(), - RocType::Num(num) => num.alignment(target_info), - RocType::Bool => align_of::(), - RocType::TagUnion(RocTagUnion::NonRecursive { tags, .. }) => { - // The smallest alignment this could possibly have is based on the number of tags - e.g. - // 0 tags is an empty union (so, alignment 0), 1-255 tags has a u8 tag (so, alignment 1), etc. - let mut align = align_for_tag_count(tags.len(), target_info); - - for (_, payloads) in tags { - for id in payloads { - align = align.max(types.get(*id).alignment(types, target_info)); - } - } - - align - } - RocType::TagUnion(RocTagUnion::Recursive { tags, .. }) => { - // The smallest alignment this could possibly have is based on the number of tags - e.g. - // 0 tags is an empty union (so, alignment 0), 1-255 tags has a u8 tag (so, alignment 1), etc. - // - // Unlike a regular tag union, a recursive one also includes a pointer. - let ptr_align = target_info.ptr_alignment_bytes(); - let mut align = ptr_align.max(align_for_tag_count(tags.len(), target_info)); - - for (_, payloads) in tags { - for id in payloads { - align = align.max(types.get(*id).alignment(types, target_info)); - } - } - - align - } - RocType::TagUnion(RocTagUnion::NullableWrapped { non_null_tags, .. }) => { - // The smallest alignment this could possibly have is based on the number of tags - e.g. - // 0 tags is an empty union (so, alignment 0), 1-255 tags has a u8 tag (so, alignment 1), etc. - // - // Unlike a regular tag union, a recursive one also includes a pointer. - let ptr_align = target_info.ptr_alignment_bytes(); - let mut align = - ptr_align.max(align_for_tag_count(non_null_tags.len(), target_info)); - - for (_, _, payloads) in non_null_tags { - for id in payloads { - align = align.max(types.get(*id).alignment(types, target_info)); - } - } - - align - } - RocType::Struct { fields, .. } => fields.iter().fold(0, |align, (_, field_id)| { - align.max(types.get(*field_id).alignment(types, target_info)) - }), - RocType::TagUnionPayload { fields, .. } => { - fields.iter().fold(0, |align, (_, field_id)| { - align.max(types.get(*field_id).alignment(types, target_info)) - }) - } - RocType::TagUnion(RocTagUnion::NullableUnwrapped { - non_null_payload: content, - .. - }) - | RocType::TagUnion(RocTagUnion::NonNullableUnwrapped { content, .. }) => { - types.get(*content).alignment(types, target_info) - } - RocType::TagUnion(RocTagUnion::Enumeration { tags, .. }) => { - UnionLayout::discriminant_size(tags.len()) - .stack_size() - .try_into() - .unwrap() - } - RocType::RecursivePointer { .. } => target_info.ptr_alignment_bytes(), - } - } -} - -fn struct_size( - fields: impl Iterator, - types: &Types, - target_info: TargetInfo, - align: usize, -) -> usize { - // The "unpadded" size (without taking alignment into account) - // is the sum of all the sizes of the fields. - let size_unpadded = fields.fold(0, |total, field_id| { - total + types.get(field_id).size(types, target_info) - }); - - // Round up to the next multiple of alignment, to incorporate - // any necessary alignment padding. - // - // e.g. if we have a record with a Str and a U8, that would be a - // size_unpadded of 25, because Str is three 8-byte pointers and U8 is 1 byte, - // but the 8-byte alignment of the pointers means we'll round 25 up to 32. - (size_unpadded / align) * align -} - -fn size_for_tag_count(num_tags: usize) -> usize { - if num_tags == 0 { - // empty tag union - 0 - } else if num_tags < u8::MAX as usize { - IntWidth::U8.stack_size() as usize - } else if num_tags < u16::MAX as usize { - IntWidth::U16.stack_size() as usize - } else if num_tags < u32::MAX as usize { - IntWidth::U32.stack_size() as usize - } else if num_tags < u64::MAX as usize { - IntWidth::U64.stack_size() as usize - } else { - panic!( - "Too many tags. You can't have more than {} tags in a tag union!", - u64::MAX - ); - } -} - -/// Returns the alignment of the discriminant based on the target -/// (e.g. on wasm, these are always 4) -fn align_for_tag_count(num_tags: usize, target_info: TargetInfo) -> usize { - if num_tags == 0 { - // empty tag union - 0 - } else if num_tags < u8::MAX as usize { - IntWidth::U8.alignment_bytes(target_info) as usize - } else if num_tags < u16::MAX as usize { - IntWidth::U16.alignment_bytes(target_info) as usize - } else if num_tags < u32::MAX as usize { - IntWidth::U32.alignment_bytes(target_info) as usize - } else if num_tags < u64::MAX as usize { - IntWidth::U64.alignment_bytes(target_info) as usize - } else { - panic!( - "Too many tags. You can't have more than {} tags in a tag union!", - u64::MAX - ); - } -} - -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub enum RocTagUnion { - Enumeration { - name: String, - tags: Vec, - }, - /// A non-recursive tag union - /// e.g. `Result a e : [Ok a, Err e]` - NonRecursive { - name: String, - tags: Vec<(String, Option)>, - }, - /// A recursive tag union (general case) - /// e.g. `Expr : [Sym Str, Add Expr Expr]` - Recursive { - name: String, - tags: Vec<(String, Option)>, - }, - /// A recursive tag union with just one constructor - /// Optimization: No need to store a tag ID (the payload is "unwrapped") - /// e.g. `RoseTree a : [Tree a (List (RoseTree a))]` - NonNullableUnwrapped { - name: String, - content: TypeId, - }, - - /// A recursive tag union that has an empty variant - /// Optimization: Represent the empty variant as null pointer => no memory usage & fast comparison - /// It has more than one other variant, so they need tag IDs (payloads are "wrapped") - /// e.g. `FingerTree a : [Empty, Single a, More (Some a) (FingerTree (Tuple a)) (Some a)]` - /// see also: https://youtu.be/ip92VMpf_-A?t=164 - NullableWrapped { - name: String, - null_tag: String, - non_null_tags: Vec<(u16, String, Option)>, - }, - - /// A recursive tag union with only two variants, where one is empty. - /// Optimizations: Use null for the empty variant AND don't store a tag ID for the other variant. - /// e.g. `ConsList a : [Nil, Cons a (ConsList a)]` - NullableUnwrapped { - name: String, - /// e.g. Nil in `StrConsList : [Nil, Cons Str (ConsList Str)]` - null_tag: String, - /// e.g. Cons in `StrConsList : [Nil, Cons Str (ConsList Str)]` - non_null_tag: String, - /// There must be a payload associated with the non-null tag. - /// Otherwise, this would have been an Enumeration! - non_null_payload: TypeId, - /// True iff the first tag (alphabetically) is represented by null. - /// If this is false, it means the second tag is represented by null instead. - null_represents_first_tag: bool, - }, -} - -impl RocTagUnion { - /// The byte offset where the discriminant is located within the tag union's - /// in-memory representation. So if you take a pointer to the tag union itself, - /// and add discriminant_offset to it, you'll have a pointer to the discriminant. - /// - /// This is only useful when given tags from RocTagUnion::Recursive or - /// RocTagUnion::NonRecursive - other tag types do not store their discriminants - /// as plain numbers at a fixed offset! - pub fn discriminant_offset( - tags: &[(String, Option)], - types: &Types, - target_info: TargetInfo, - ) -> usize { - tags.iter() - .fold(0, |max_size, (_, opt_tag_id)| match opt_tag_id { - Some(tag_id) => { - let size_unpadded = match types.get(*tag_id) { - // For structs (that is, payloads), we actually want - // to get the size *before* alignment padding is taken - // into account, since the discriminant is - // stored after those bytes. - RocType::Struct { fields, .. } => { - fields.iter().fold(0, |total, (_, field_id)| { - total + types.get(*field_id).size(types, target_info) - }) - } - typ => max_size.max(typ.size(types, target_info)), - }; - - max_size.max(size_unpadded) - } - - None => max_size, - }) - } -} - -#[test] -fn sizes_agree_with_roc_std() { - use std::mem::size_of; - - let target_info = TargetInfo::from(&target_lexicon::Triple::host()); - let mut types = Types::default(); - - assert_eq!( - RocType::RocStr.size(&types, target_info), - size_of::(), - ); - - assert_eq!( - RocType::RocList(types.add(RocType::RocStr)).size(&types, target_info), - size_of::>(), - ); - - // TODO enable this once we have RocDict in roc_std - // assert_eq!( - // RocType::RocDict.size(&types, target_info), - // size_of::(), - // ); -} diff --git a/bindgen/templates/header.rs b/bindgen/templates/header.rs deleted file mode 100644 index 2b86043ffe..0000000000 --- a/bindgen/templates/header.rs +++ /dev/null @@ -1,7 +0,0 @@ -// ⚠️ GENERATED CODE ⚠️ - this entire file was generated by the `roc-bindgen` CLI - -#![allow(dead_code)] -#![allow(non_snake_case)] -#![allow(non_camel_case_types)] -#![allow(non_upper_case_globals)] -#![allow(clippy::undocumented_unsafe_blocks)] diff --git a/bindgen/templates/template.c b/bindgen/templates/template.c deleted file mode 100644 index 4e28b6d042..0000000000 --- a/bindgen/templates/template.c +++ /dev/null @@ -1,59 +0,0 @@ -#include -#include -#include -#include -#include -#include - -void* roc_alloc(size_t size, unsigned int alignment) { return malloc(size); } - -void* roc_realloc(void* ptr, size_t new_size, size_t old_size, unsigned int alignment) { - return realloc(ptr, new_size); -} - -void roc_dealloc(void* ptr, unsigned int alignment) { free(ptr); } - -void roc_panic(void* ptr, unsigned int alignment) { - char* msg = (char*)ptr; - fprintf(stderr, - "Application crashed with message\n\n %s\n\nShutting down\n", msg); - exit(0); -} - -void* roc_memcpy(void* dest, const void* src, size_t n) { - return memcpy(dest, src, n); -} - -void* roc_memset(void* str, int c, size_t n) { return memset(str, c, n); } - -/////////////////////////////////////////////////////////////////////////// -// -// roc_std -// -/////////////////////////////////////////////////////////////////////////// - -struct RocStr { - char* bytes; - size_t len; -}; - -bool is_small_str(struct RocStr str) { return ((ssize_t)str.len) < 0; } - -// Determine the length of the string, taking into -// account the small string optimization -size_t roc_str_len(struct RocStr str) { - char* bytes = (char*)&str; - char last_byte = bytes[sizeof(str) - 1]; - char last_byte_xored = last_byte ^ 0b10000000; - size_t small_len = (size_t)(last_byte_xored); - size_t big_len = str.len; - - // Avoid branch misprediction costs by always - // determining both small_len and big_len, - // so this compiles to a cmov instruction. - if (is_small_str(str)) { - return small_len; - } else { - return big_len; - } -} diff --git a/bindgen/templates/template.rs b/bindgen/templates/template.rs deleted file mode 100644 index 3ca441f5d2..0000000000 --- a/bindgen/templates/template.rs +++ /dev/null @@ -1,79 +0,0 @@ -#![allow(non_snake_case)] - -use core::ffi::c_void; - -// TODO don't have these depend on the libc crate; instead, use default -// allocator, built-in memset, etc. - -#[no_mangle] -pub unsafe extern "C" fn roc_alloc(size: usize, _alignment: u32) -> *mut c_void { - return libc::malloc(size); -} - -#[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 { - return libc::realloc(c_ptr, new_size); -} - -#[no_mangle] -pub unsafe extern "C" fn roc_dealloc(c_ptr: *mut c_void, _alignment: u32) { - return libc::free(c_ptr); -} - -#[no_mangle] -pub unsafe extern "C" fn roc_panic(c_ptr: *mut c_void, tag_id: u32) { - use std::ffi::CStr; - use std::os::raw::c_char; - - match tag_id { - 0 => { - let slice = CStr::from_ptr(c_ptr as *const c_char); - let string = slice.to_str().unwrap(); - eprintln!("Roc hit a panic: {}", string); - std::process::exit(1); - } - _ => todo!(), - } -} - -#[no_mangle] -pub unsafe extern "C" fn roc_memcpy(dst: *mut c_void, src: *mut c_void, n: usize) -> *mut c_void { - libc::memcpy(dst, src, n) -} - -#[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) -} - -//////////////////////////////////////////////////////////////////////////// -// -// TODO: rust_main should be removed once we use surgical linking everywhere. -// It's just a workaround to get cargo to build an object file the way -// the non-surgical linker needs it to. The surgical linker works on -// executables, not object files, so this workaround is not needed there. -// -//////////////////////////////////////////////////////////////////////////// -#[no_mangle] -pub extern "C" fn rust_main() -> i32 { - use roc_std::RocStr; - - unsafe { - let roc_str = roc_main(); - - let len = roc_str.len(); - let str_bytes = roc_str.as_bytes().as_ptr() as *const libc::c_void; - - if libc::write(1, str_bytes, len) < 0 { - panic!("Writing to stdout failed!"); - } - } - - // Exit code - 0 -} diff --git a/bindgen/templates/template.zig b/bindgen/templates/template.zig deleted file mode 100644 index a2e73bd5af..0000000000 --- a/bindgen/templates/template.zig +++ /dev/null @@ -1,71 +0,0 @@ -const std = @import("std"); -const str = @import("str"); - -comptime { - // This is a workaround for https://github.com/ziglang/zig/issues/8218 - // which is only necessary on macOS. - // - // Once that issue is fixed, we can undo the changes in - // 177cf12e0555147faa4d436e52fc15175c2c4ff0 and go back to passing - // -fcompiler-rt in link.rs instead of doing this. Note that this - // workaround is present in many host.zig files, so make sure to undo - // it everywhere! - if (std.builtin.os.tag == .macos) { - _ = @import("compiler_rt"); - } -} - -const Align = 2 * @alignOf(usize); -extern fn malloc(size: usize) callconv(.C) ?*align(Align) anyopaque; -extern fn realloc(c_ptr: [*]align(Align) u8, size: usize) callconv(.C) ?*anyopaque; -extern fn free(c_ptr: [*]align(Align) u8) callconv(.C) void; -extern fn memcpy(dst: [*]u8, src: [*]u8, size: usize) callconv(.C) void; -extern fn memset(dst: [*]u8, value: i32, size: usize) callconv(.C) void; - -const DEBUG: bool = false; - -export fn roc_alloc(size: usize, alignment: u32) callconv(.C) ?*anyopaque { - if (DEBUG) { - var ptr = malloc(size); - const stdout = std.io.getStdOut().writer(); - stdout.print("alloc: {d} (alignment {d}, size {d})\n", .{ ptr, alignment, size }) catch unreachable; - return ptr; - } else { - return malloc(size); - } -} - -export fn roc_realloc(c_ptr: *anyopaque, new_size: usize, old_size: usize, alignment: u32) callconv(.C) ?*anyopaque { - if (DEBUG) { - const stdout = std.io.getStdOut().writer(); - stdout.print("realloc: {d} (alignment {d}, old_size {d})\n", .{ c_ptr, alignment, old_size }) catch unreachable; - } - - return realloc(@alignCast(Align, @ptrCast([*]u8, c_ptr)), new_size); -} - -export fn roc_dealloc(c_ptr: *anyopaque, alignment: u32) callconv(.C) void { - if (DEBUG) { - const stdout = std.io.getStdOut().writer(); - stdout.print("dealloc: {d} (alignment {d})\n", .{ c_ptr, alignment }) catch unreachable; - } - - free(@alignCast(Align, @ptrCast([*]u8, c_ptr))); -} - -export fn roc_panic(c_ptr: *anyopaque, tag_id: u32) callconv(.C) void { - _ = tag_id; - - const stderr = std.io.getStdErr().writer(); - const msg = @ptrCast([*:0]const u8, c_ptr); - stderr.print("Application crashed with message\n\n {s}\n\nShutting down\n", .{msg}) catch unreachable; - std.process.exit(0); -} - -export fn roc_memcpy(dst: [*]u8, src: [*]u8, size: usize) callconv(.C) void { - return memcpy(dst, src, size); -} - -export fn roc_memset(dst: [*]u8, value: i32, size: usize) callconv(.C) void { - return memset(dst, value, size); -} diff --git a/bindgen/tests/fixture-templates/rust/Cargo.lock b/bindgen/tests/fixture-templates/rust/Cargo.lock deleted file mode 100644 index 9aaf28c23e..0000000000 --- a/bindgen/tests/fixture-templates/rust/Cargo.lock +++ /dev/null @@ -1,37 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 3 - -[[package]] -name = "host" -version = "0.1.0" -dependencies = [ - "indoc", - "libc", - "roc_std", -] - -[[package]] -name = "indoc" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05a0bd019339e5d968b37855180087b7b9d512c5046fbd244cf8c95687927d6e" - -[[package]] -name = "libc" -version = "0.2.92" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56d855069fafbb9b344c0f962150cd2c1187975cb1c22c1522c240d8c4986714" - -[[package]] -name = "roc_std" -version = "0.1.0" -dependencies = [ - "static_assertions", -] - -[[package]] -name = "static_assertions" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f406d6ee68db6796e11ffd7b4d171864c58b7451e79ef9460ea33c287a1f89a7" diff --git a/bindgen/tests/fixture-templates/rust/Cargo.toml b/bindgen/tests/fixture-templates/rust/Cargo.toml deleted file mode 100644 index f2bf016dc1..0000000000 --- a/bindgen/tests/fixture-templates/rust/Cargo.toml +++ /dev/null @@ -1,34 +0,0 @@ -# ⚠️ READ THIS BEFORE MODIFYING THIS FILE! ⚠️ -# -# This file is a fixture template. If the file you're looking at is -# in the fixture-templates/ directory, then you're all set - go ahead -# and modify it, and it will modify all the fixture tests. -# -# If this file is in the fixtures/ directory, on the other hand, then -# it is gitignored and will be overwritten the next time tests run. -# So you probably don't want to modify it by hand! Instead, modify the -# file with the same name in the fixture-templates/ directory. - -[package] -name = "host" -version = "0.1.0" -authors = ["The Roc Contributors"] -license = "UPL-1.0" -edition = "2018" -links = "app" - -[lib] -name = "host" -path = "src/lib.rs" -crate-type = ["staticlib", "rlib"] - -[[bin]] -name = "host" -path = "src/main.rs" - -[dependencies] -roc_std = { path = "../../../../roc_std" } -libc = "0.2" -indoc = "1.0.6" - -[workspace] diff --git a/bindgen/tests/fixture-templates/rust/build.rs b/bindgen/tests/fixture-templates/rust/build.rs deleted file mode 100644 index 7dcf8da4e9..0000000000 --- a/bindgen/tests/fixture-templates/rust/build.rs +++ /dev/null @@ -1,15 +0,0 @@ -// ⚠️ READ THIS BEFORE MODIFYING THIS FILE! ⚠️ -// -// This file is a fixture template. If the file you're looking at is -// in the fixture-templates/ directory, then you're all set - go ahead -// and modify it, and it will modify all the fixture tests. -// -// If this file is in the fixtures/ directory, on the other hand, then -// it is gitignored and will be overwritten the next time tests run. -// So you probably don't want to modify it by hand! Instead, modify the -// file with the same name in the fixture-templates/ directory. - -fn main() { - println!("cargo:rustc-link-lib=dylib=app"); - println!("cargo:rustc-link-search=."); -} diff --git a/bindgen/tests/fixture-templates/rust/host.c b/bindgen/tests/fixture-templates/rust/host.c deleted file mode 100644 index 3914d3f6ee..0000000000 --- a/bindgen/tests/fixture-templates/rust/host.c +++ /dev/null @@ -1,14 +0,0 @@ -// ⚠️ READ THIS BEFORE MODIFYING THIS FILE! ⚠️ -// -// This file is a fixture template. If the file you're looking at is -// in the fixture-templates/ directory, then you're all set - go ahead -// and modify it, and it will modify all the fixture tests. -// -// If this file is in the fixtures/ directory, on the other hand, then -// it is gitignored and will be overwritten the next time tests run. -// So you probably don't want to modify it by hand! Instead, modify the -// file with the same name in the fixture-templates/ directory. - -extern int rust_main(); - -int main() { return rust_main(); } diff --git a/bindgen/tests/fixture-templates/rust/src/main.rs b/bindgen/tests/fixture-templates/rust/src/main.rs deleted file mode 100644 index 51175f934b..0000000000 --- a/bindgen/tests/fixture-templates/rust/src/main.rs +++ /dev/null @@ -1,3 +0,0 @@ -fn main() { - std::process::exit(host::rust_main()); -} diff --git a/bindgen/tests/fixtures/.gitignore b/bindgen/tests/fixtures/.gitignore deleted file mode 100644 index cc22f3ac1f..0000000000 --- a/bindgen/tests/fixtures/.gitignore +++ /dev/null @@ -1,12 +0,0 @@ -Cargo.lock -Cargo.toml -build.rs -host.c -bindings.rs -roc_externs.rs -main.rs -app -dynhost -libapp.so -metadata -preprocessedhost diff --git a/bindgen/tests/fixtures/basic-record/app.roc b/bindgen/tests/fixtures/basic-record/app.roc deleted file mode 100644 index f083e6d724..0000000000 --- a/bindgen/tests/fixtures/basic-record/app.roc +++ /dev/null @@ -1,6 +0,0 @@ -app "app" - packages { pf: "." } - imports [] - provides [main] to pf - -main = { a: 1995, b: 42 } diff --git a/bindgen/tests/fixtures/basic-record/src/lib.rs b/bindgen/tests/fixtures/basic-record/src/lib.rs deleted file mode 100644 index 15e3ecd7e5..0000000000 --- a/bindgen/tests/fixtures/basic-record/src/lib.rs +++ /dev/null @@ -1,93 +0,0 @@ -mod bindings; - -extern "C" { - #[link_name = "roc__mainForHost_1_exposed_generic"] - fn roc_main(_: *mut bindings::MyRcd); -} - -#[no_mangle] -pub extern "C" fn rust_main() -> i32 { - use std::cmp::Ordering; - use std::collections::hash_set::HashSet; - - let record = unsafe { - let mut ret: core::mem::MaybeUninit = core::mem::MaybeUninit::uninit(); - - roc_main(ret.as_mut_ptr()); - - ret.assume_init() - }; - - // Verify that the record has all the expected traits. - - assert!(record == record); // PartialEq - assert!(record.clone() == record.clone()); // Clone - - // Since this is a move, later uses of `record` will fail unless `record` has Copy - let rec2 = record; // Copy - - assert!(rec2 != Default::default()); // Default - assert!(record.partial_cmp(&record) == Some(Ordering::Equal)); // PartialOrd - assert!(record.cmp(&record) == Ordering::Equal); // Ord - - let mut set = HashSet::new(); - - set.insert(record); // Eq, Hash - set.insert(rec2); - - assert_eq!(set.len(), 1); - - println!("Record was: {:?}", record); // Debug - - // Exit code - 0 -} - -// Externs required by roc_std and by the Roc app - -use core::ffi::c_void; -use std::ffi::CStr; -use std::os::raw::c_char; - -#[no_mangle] -pub unsafe extern "C" fn roc_alloc(size: usize, _alignment: u32) -> *mut c_void { - return libc::malloc(size); -} - -#[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 { - return libc::realloc(c_ptr, new_size); -} - -#[no_mangle] -pub unsafe extern "C" fn roc_dealloc(c_ptr: *mut c_void, _alignment: u32) { - return libc::free(c_ptr); -} - -#[no_mangle] -pub unsafe extern "C" fn roc_panic(c_ptr: *mut c_void, tag_id: u32) { - match tag_id { - 0 => { - let slice = CStr::from_ptr(c_ptr as *const c_char); - let string = slice.to_str().unwrap(); - eprintln!("Roc hit a panic: {}", string); - std::process::exit(1); - } - _ => todo!(), - } -} - -#[no_mangle] -pub unsafe extern "C" fn roc_memcpy(dst: *mut c_void, src: *mut c_void, n: usize) -> *mut c_void { - libc::memcpy(dst, src, n) -} - -#[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) -} diff --git a/bindgen/tests/fixtures/enumeration/app.roc b/bindgen/tests/fixtures/enumeration/app.roc deleted file mode 100644 index d404f021df..0000000000 --- a/bindgen/tests/fixtures/enumeration/app.roc +++ /dev/null @@ -1,6 +0,0 @@ -app "app" - packages { pf: "." } - imports [] - provides [main] to pf - -main = Foo diff --git a/bindgen/tests/fixtures/enumeration/src/lib.rs b/bindgen/tests/fixtures/enumeration/src/lib.rs deleted file mode 100644 index b63cee5ce8..0000000000 --- a/bindgen/tests/fixtures/enumeration/src/lib.rs +++ /dev/null @@ -1,97 +0,0 @@ -mod bindings; - -extern "C" { - #[link_name = "roc__mainForHost_1_exposed_generic"] - fn roc_main(_: *mut bindings::MyEnum); -} - -#[no_mangle] -pub extern "C" fn rust_main() -> i32 { - use std::cmp::Ordering; - use std::collections::hash_set::HashSet; - - let tag_union = unsafe { - let mut ret: core::mem::MaybeUninit = core::mem::MaybeUninit::uninit(); - - roc_main(ret.as_mut_ptr()); - - ret.assume_init() - }; - - // Verify that it has all the expected traits. - - assert!(tag_union == tag_union); // PartialEq - assert!(tag_union.clone() == tag_union.clone()); // Clone - - // Since this is a move, later uses of `tag_union` will fail unless `tag_union` has Copy - let union2 = tag_union; // Copy - - assert!(tag_union.partial_cmp(&tag_union) == Some(Ordering::Equal)); // PartialOrd - assert!(tag_union.cmp(&tag_union) == Ordering::Equal); // Ord - - let mut set = HashSet::new(); - - set.insert(tag_union); // Eq, Hash - set.insert(union2); - - assert_eq!(set.len(), 1); - - println!( - "tag_union was: {:?}, Bar is: {:?}, Baz is: {:?}", - tag_union, - bindings::MyEnum::Bar, - bindings::MyEnum::Baz, - ); // Debug - - // Exit code - 0 -} - -// Externs required by roc_std and by the Roc app - -use core::ffi::c_void; -use std::ffi::CStr; -use std::os::raw::c_char; - -#[no_mangle] -pub unsafe extern "C" fn roc_alloc(size: usize, _alignment: u32) -> *mut c_void { - return libc::malloc(size); -} - -#[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 { - return libc::realloc(c_ptr, new_size); -} - -#[no_mangle] -pub unsafe extern "C" fn roc_dealloc(c_ptr: *mut c_void, _alignment: u32) { - return libc::free(c_ptr); -} - -#[no_mangle] -pub unsafe extern "C" fn roc_panic(c_ptr: *mut c_void, tag_id: u32) { - match tag_id { - 0 => { - let slice = CStr::from_ptr(c_ptr as *const c_char); - let string = slice.to_str().unwrap(); - eprintln!("Roc hit a panic: {}", string); - std::process::exit(1); - } - _ => todo!(), - } -} - -#[no_mangle] -pub unsafe extern "C" fn roc_memcpy(dst: *mut c_void, src: *mut c_void, n: usize) -> *mut c_void { - libc::memcpy(dst, src, n) -} - -#[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) -} diff --git a/bindgen/tests/fixtures/nested-record/app.roc b/bindgen/tests/fixtures/nested-record/app.roc deleted file mode 100644 index 324fa83846..0000000000 --- a/bindgen/tests/fixtures/nested-record/app.roc +++ /dev/null @@ -1,6 +0,0 @@ -app "app" - packages { pf: "." } - imports [] - provides [main] to pf - -main = { x: { a: 5, b: 24 }, y: "foo", z: [1, 2] } diff --git a/bindgen/tests/fixtures/nested-record/src/lib.rs b/bindgen/tests/fixtures/nested-record/src/lib.rs deleted file mode 100644 index a660d079a9..0000000000 --- a/bindgen/tests/fixtures/nested-record/src/lib.rs +++ /dev/null @@ -1,95 +0,0 @@ -mod bindings; - -extern "C" { - #[link_name = "roc__mainForHost_1_exposed_generic"] - fn roc_main(_: *mut bindings::Outer); -} - -#[no_mangle] -pub extern "C" fn rust_main() -> i32 { - use std::cmp::Ordering; - - let outer = unsafe { - let mut ret: core::mem::MaybeUninit = core::mem::MaybeUninit::uninit(); - - roc_main(ret.as_mut_ptr()); - - ret.assume_init() - }; - - // Verify that `inner` has all the expected traits. - { - let inner = outer.x; - - assert!(inner == inner); // PartialEq - assert!(inner.clone() == inner.clone()); // Clone - - // Since this is a move, later uses of `inner` will fail unless `inner` has Copy - let inner2 = inner; // Copy - - assert!(inner2 != Default::default()); // Default - assert!(inner.partial_cmp(&inner) == Some(Ordering::Equal)); // PartialOrd - } - - // Verify that `outer` has all the expected traits. - { - assert!(outer == outer); // PartialEq - assert!(outer.clone() == outer.clone()); // Clone - assert!(outer != Default::default()); // Default - assert!(outer.partial_cmp(&outer) == Some(Ordering::Equal)); // PartialOrd - } - - println!("Record was: {:?}", outer); - - // Exit code - 0 -} - -// Externs required by roc_std and by the Roc app - -use core::ffi::c_void; -use std::ffi::CStr; -use std::os::raw::c_char; - -#[no_mangle] -pub unsafe extern "C" fn roc_alloc(size: usize, _alignment: u32) -> *mut c_void { - return libc::malloc(size); -} - -#[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 { - return libc::realloc(c_ptr, new_size); -} - -#[no_mangle] -pub unsafe extern "C" fn roc_dealloc(c_ptr: *mut c_void, _alignment: u32) { - return libc::free(c_ptr); -} - -#[no_mangle] -pub unsafe extern "C" fn roc_panic(c_ptr: *mut c_void, tag_id: u32) { - match tag_id { - 0 => { - let slice = CStr::from_ptr(c_ptr as *const c_char); - let string = slice.to_str().unwrap(); - eprintln!("Roc hit a panic: {}", string); - std::process::exit(1); - } - _ => todo!(), - } -} - -#[no_mangle] -pub unsafe extern "C" fn roc_memcpy(dst: *mut c_void, src: *mut c_void, n: usize) -> *mut c_void { - libc::memcpy(dst, src, n) -} - -#[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) -} diff --git a/bindgen/tests/fixtures/nullable-unwrapped/app.roc b/bindgen/tests/fixtures/nullable-unwrapped/app.roc deleted file mode 100644 index 187275a21a..0000000000 --- a/bindgen/tests/fixtures/nullable-unwrapped/app.roc +++ /dev/null @@ -1,6 +0,0 @@ -app "app" - packages { pf: "." } - imports [] - provides [main] to pf - -main = Cons "World!" (Cons "Hello " Nil) diff --git a/bindgen/tests/fixtures/nullable-unwrapped/src/lib.rs b/bindgen/tests/fixtures/nullable-unwrapped/src/lib.rs deleted file mode 100644 index 8b025e07dd..0000000000 --- a/bindgen/tests/fixtures/nullable-unwrapped/src/lib.rs +++ /dev/null @@ -1,103 +0,0 @@ -mod bindings; - -use bindings::StrConsList; -use indoc::indoc; - -extern "C" { - #[link_name = "roc__mainForHost_1_exposed_generic"] - fn roc_main(_: *mut StrConsList); -} - -#[no_mangle] -pub extern "C" fn rust_main() -> i32 { - use std::cmp::Ordering; - use std::collections::hash_set::HashSet; - - let tag_union = unsafe { - let mut ret: core::mem::MaybeUninit = core::mem::MaybeUninit::uninit(); - - roc_main(ret.as_mut_ptr()); - - ret.assume_init() - }; - - // Verify that it has all the expected traits. - - assert!(tag_union == tag_union); // PartialEq - assert!(tag_union.clone() == tag_union.clone()); // Clone - - assert!(tag_union.partial_cmp(&tag_union) == Some(Ordering::Equal)); // PartialOrd - assert!(tag_union.cmp(&tag_union) == Ordering::Equal); // Ord - - print!( - indoc!( - r#" - tag_union was: {:?} - `Cons "small str" Nil` is: {:?} - `Nil` is: {:?} - "# - ), - tag_union, - StrConsList::Cons("small str".into(), StrConsList::Nil), - StrConsList::Nil, - ); // Debug - - let mut set = HashSet::new(); - - set.insert(tag_union.clone()); // Eq, Hash - set.insert(tag_union); - - assert_eq!(set.len(), 1); - - // Exit code - 0 -} - -// Externs required by roc_std and by the Roc app - -use core::ffi::c_void; -use std::ffi::CStr; -use std::os::raw::c_char; - -#[no_mangle] -pub unsafe extern "C" fn roc_alloc(size: usize, _alignment: u32) -> *mut c_void { - return libc::malloc(size); -} - -#[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 { - return libc::realloc(c_ptr, new_size); -} - -#[no_mangle] -pub unsafe extern "C" fn roc_dealloc(c_ptr: *mut c_void, _alignment: u32) { - return libc::free(c_ptr); -} - -#[no_mangle] -pub unsafe extern "C" fn roc_panic(c_ptr: *mut c_void, tag_id: u32) { - match tag_id { - 0 => { - let slice = CStr::from_ptr(c_ptr as *const c_char); - let string = slice.to_str().unwrap(); - eprintln!("Roc hit a panic: {}", string); - std::process::exit(1); - } - _ => todo!(), - } -} - -#[no_mangle] -pub unsafe extern "C" fn roc_memcpy(dst: *mut c_void, src: *mut c_void, n: usize) -> *mut c_void { - libc::memcpy(dst, src, n) -} - -#[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) -} diff --git a/bindgen/tests/fixtures/recursive-union/Package-Config.roc b/bindgen/tests/fixtures/recursive-union/Package-Config.roc deleted file mode 100644 index 730c022dfb..0000000000 --- a/bindgen/tests/fixtures/recursive-union/Package-Config.roc +++ /dev/null @@ -1,11 +0,0 @@ -platform "test-platform" - requires {} { main : _ } - exposes [] - packages {} - imports [] - provides [mainForHost] - -Expr : [String Str, Concat Expr Expr] - -mainForHost : Expr -mainForHost = main diff --git a/bindgen/tests/fixtures/recursive-union/app.roc b/bindgen/tests/fixtures/recursive-union/app.roc deleted file mode 100644 index 98336ca8a8..0000000000 --- a/bindgen/tests/fixtures/recursive-union/app.roc +++ /dev/null @@ -1,6 +0,0 @@ -app "app" - packages { pf: "." } - imports [] - provides [main] to pf - -main = Concat (String "Hello, ") (String "World!") diff --git a/bindgen/tests/fixtures/recursive-union/src/lib.rs b/bindgen/tests/fixtures/recursive-union/src/lib.rs deleted file mode 100644 index 1cc2d76e9d..0000000000 --- a/bindgen/tests/fixtures/recursive-union/src/lib.rs +++ /dev/null @@ -1,106 +0,0 @@ -mod bindings; - -use bindings::Expr; -use indoc::indoc; - -extern "C" { - #[link_name = "roc__mainForHost_1_exposed_generic"] - fn roc_main(_: *mut Expr); -} - -#[no_mangle] -pub extern "C" fn rust_main() -> i32 { - use std::cmp::Ordering; - use std::collections::hash_set::HashSet; - - let tag_union = unsafe { - let mut ret: core::mem::MaybeUninit = core::mem::MaybeUninit::uninit(); - - roc_main(ret.as_mut_ptr()); - - ret.assume_init() - }; - - // Verify that it has all the expected traits. - - assert!(tag_union == tag_union); // PartialEq - assert!(tag_union.clone() == tag_union.clone()); // Clone - - assert!(tag_union.partial_cmp(&tag_union) == Some(Ordering::Equal)); // PartialOrd - assert!(tag_union.cmp(&tag_union) == Ordering::Equal); // Ord - - print!( - indoc!( - r#" - tag_union was: {:?} - `Concat (String "Hello, ") (String "World!")` is: {:?} - `String "this is a test"` is: {:?} - "# - ), - tag_union, - Expr::Concat( - Expr::String("Hello, ".into()), - Expr::String("World!".into()), - ), - Expr::String("this is a test".into()), - ); // Debug - - let mut set = HashSet::new(); - - set.insert(tag_union.clone()); // Eq, Hash - set.insert(tag_union); - - assert_eq!(set.len(), 1); - - // Exit code - 0 -} - -// Externs required by roc_std and by the Roc app - -use core::ffi::c_void; -use std::ffi::CStr; -use std::os::raw::c_char; - -#[no_mangle] -pub unsafe extern "C" fn roc_alloc(size: usize, _alignment: u32) -> *mut c_void { - return libc::malloc(size); -} - -#[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 { - return libc::realloc(c_ptr, new_size); -} - -#[no_mangle] -pub unsafe extern "C" fn roc_dealloc(c_ptr: *mut c_void, _alignment: u32) { - return libc::free(c_ptr); -} - -#[no_mangle] -pub unsafe extern "C" fn roc_panic(c_ptr: *mut c_void, tag_id: u32) { - match tag_id { - 0 => { - let slice = CStr::from_ptr(c_ptr as *const c_char); - let string = slice.to_str().unwrap(); - eprintln!("Roc hit a panic: {}", string); - std::process::exit(1); - } - _ => todo!(), - } -} - -#[no_mangle] -pub unsafe extern "C" fn roc_memcpy(dst: *mut c_void, src: *mut c_void, n: usize) -> *mut c_void { - libc::memcpy(dst, src, n) -} - -#[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) -} diff --git a/bindgen/tests/fixtures/union-with-padding/Package-Config.roc b/bindgen/tests/fixtures/union-with-padding/Package-Config.roc deleted file mode 100644 index 2d0ef5e1ab..0000000000 --- a/bindgen/tests/fixtures/union-with-padding/Package-Config.roc +++ /dev/null @@ -1,17 +0,0 @@ -platform "test-platform" - requires {} { main : _ } - exposes [] - packages {} - imports [] - provides [mainForHost] - -# This case is important to test because the U128 -# gives the whole struct an alignment of 16, but the -# Str is the largest variant, so the whole union has -# a size of 32 (due to alignment, rounded up from Str's 24), -# and the discriminant is stored in the 8+ bytes of padding -# that all variants have. -NonRecursive : [Foo Str, Bar U128, Blah I32, Baz] - -mainForHost : NonRecursive -mainForHost = main diff --git a/bindgen/tests/fixtures/union-with-padding/app.roc b/bindgen/tests/fixtures/union-with-padding/app.roc deleted file mode 100644 index 84aa5ac8a3..0000000000 --- a/bindgen/tests/fixtures/union-with-padding/app.roc +++ /dev/null @@ -1,6 +0,0 @@ -app "app" - packages { pf: "." } - imports [] - provides [main] to pf - -main = Foo "This is a test" diff --git a/bindgen/tests/fixtures/union-with-padding/src/lib.rs b/bindgen/tests/fixtures/union-with-padding/src/lib.rs deleted file mode 100644 index 408f574a4c..0000000000 --- a/bindgen/tests/fixtures/union-with-padding/src/lib.rs +++ /dev/null @@ -1,99 +0,0 @@ -mod bindings; - -use bindings::NonRecursive; - -extern "C" { - #[link_name = "roc__mainForHost_1_exposed_generic"] - fn roc_main(_: *mut NonRecursive); -} - -#[no_mangle] -pub extern "C" fn rust_main() -> i32 { - use std::cmp::Ordering; - use std::collections::hash_set::HashSet; - - let tag_union = unsafe { - let mut ret: core::mem::MaybeUninit = core::mem::MaybeUninit::uninit(); - - roc_main(ret.as_mut_ptr()); - - ret.assume_init() - }; - - // Verify that it has all the expected traits. - - assert!(tag_union == tag_union); // PartialEq - assert!(tag_union.clone() == tag_union.clone()); // Clone - - assert!(tag_union.partial_cmp(&tag_union) == Some(Ordering::Equal)); // PartialOrd - assert!(tag_union.cmp(&tag_union) == Ordering::Equal); // Ord - - println!( - "tag_union was: {:?}\n`Foo \"small str\"` is: {:?}\n`Foo \"A long enough string to not be small\"` is: {:?}\n`Bar 123` is: {:?}\n`Baz` is: {:?}\n`Blah 456` is: {:?}", - tag_union, - NonRecursive::Foo("small str".into()), - NonRecursive::Foo("A long enough string to not be small".into()), - NonRecursive::Bar(123.into()), - NonRecursive::Baz, - NonRecursive::Blah(456), - ); // Debug - - let mut set = HashSet::new(); - - set.insert(tag_union.clone()); // Eq, Hash - set.insert(tag_union); - - assert_eq!(set.len(), 1); - - // Exit code - 0 -} - -// Externs required by roc_std and by the Roc app - -use core::ffi::c_void; -use std::ffi::CStr; -use std::os::raw::c_char; - -#[no_mangle] -pub unsafe extern "C" fn roc_alloc(size: usize, _alignment: u32) -> *mut c_void { - return libc::malloc(size); -} - -#[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 { - return libc::realloc(c_ptr, new_size); -} - -#[no_mangle] -pub unsafe extern "C" fn roc_dealloc(c_ptr: *mut c_void, _alignment: u32) { - return libc::free(c_ptr); -} - -#[no_mangle] -pub unsafe extern "C" fn roc_panic(c_ptr: *mut c_void, tag_id: u32) { - match tag_id { - 0 => { - let slice = CStr::from_ptr(c_ptr as *const c_char); - let string = slice.to_str().unwrap(); - eprintln!("Roc hit a panic: {}", string); - std::process::exit(1); - } - _ => todo!(), - } -} - -#[no_mangle] -pub unsafe extern "C" fn roc_memcpy(dst: *mut c_void, src: *mut c_void, n: usize) -> *mut c_void { - libc::memcpy(dst, src, n) -} - -#[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) -} diff --git a/bindgen/tests/fixtures/union-without-padding/Package-Config.roc b/bindgen/tests/fixtures/union-without-padding/Package-Config.roc deleted file mode 100644 index 881b30b330..0000000000 --- a/bindgen/tests/fixtures/union-without-padding/Package-Config.roc +++ /dev/null @@ -1,15 +0,0 @@ -platform "test-platform" - requires {} { main : _ } - exposes [] - packages {} - imports [] - provides [mainForHost] - -# This case is important to test because there's no padding -# after the largest variant, so the compiler adds an extra u8 -# (rounded up to alignment, so an an extra 8 bytes) in which -# to store the discriminant. We have to bindgen accordingly! -NonRecursive : [Foo Str, Bar I64, Blah I32, Baz] - -mainForHost : NonRecursive -mainForHost = main diff --git a/bindgen/tests/fixtures/union-without-padding/app.roc b/bindgen/tests/fixtures/union-without-padding/app.roc deleted file mode 100644 index 84aa5ac8a3..0000000000 --- a/bindgen/tests/fixtures/union-without-padding/app.roc +++ /dev/null @@ -1,6 +0,0 @@ -app "app" - packages { pf: "." } - imports [] - provides [main] to pf - -main = Foo "This is a test" diff --git a/bindgen/tests/fixtures/union-without-padding/src/lib.rs b/bindgen/tests/fixtures/union-without-padding/src/lib.rs deleted file mode 100644 index ba4df484bf..0000000000 --- a/bindgen/tests/fixtures/union-without-padding/src/lib.rs +++ /dev/null @@ -1,97 +0,0 @@ -mod bindings; - -extern "C" { - #[link_name = "roc__mainForHost_1_exposed_generic"] - fn roc_main(_: *mut bindings::NonRecursive); -} - -#[no_mangle] -pub extern "C" fn rust_main() -> i32 { - use std::cmp::Ordering; - use std::collections::hash_set::HashSet; - - let tag_union = unsafe { - let mut ret: core::mem::MaybeUninit = - core::mem::MaybeUninit::uninit(); - - roc_main(ret.as_mut_ptr()); - - ret.assume_init() - }; - - // Verify that it has all the expected traits. - - assert!(tag_union == tag_union); // PartialEq - assert!(tag_union.clone() == tag_union.clone()); // Clone - - assert!(tag_union.partial_cmp(&tag_union) == Some(Ordering::Equal)); // PartialOrd - assert!(tag_union.cmp(&tag_union) == Ordering::Equal); // Ord - - println!( - "tag_union was: {:?}\n`Foo \"small str\"` is: {:?}\n`Bar 123` is: {:?}\n`Baz` is: {:?}\n`Blah 456` is: {:?}", - tag_union, - bindings::NonRecursive::Foo("small str".into()), - bindings::NonRecursive::Bar(123), - bindings::NonRecursive::Baz, - bindings::NonRecursive::Blah(456), - ); // Debug - - let mut set = HashSet::new(); - - set.insert(tag_union.clone()); // Eq, Hash - set.insert(tag_union); - - assert_eq!(set.len(), 1); - - // Exit code - 0 -} - -// Externs required by roc_std and by the Roc app - -use core::ffi::c_void; -use std::ffi::CStr; -use std::os::raw::c_char; - -#[no_mangle] -pub unsafe extern "C" fn roc_alloc(size: usize, _alignment: u32) -> *mut c_void { - return libc::malloc(size); -} - -#[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 { - return libc::realloc(c_ptr, new_size); -} - -#[no_mangle] -pub unsafe extern "C" fn roc_dealloc(c_ptr: *mut c_void, _alignment: u32) { - return libc::free(c_ptr); -} - -#[no_mangle] -pub unsafe extern "C" fn roc_panic(c_ptr: *mut c_void, tag_id: u32) { - match tag_id { - 0 => { - let slice = CStr::from_ptr(c_ptr as *const c_char); - let string = slice.to_str().unwrap(); - eprintln!("Roc hit a panic: {}", string); - std::process::exit(1); - } - _ => todo!(), - } -} - -#[no_mangle] -pub unsafe extern "C" fn roc_memcpy(dst: *mut c_void, src: *mut c_void, n: usize) -> *mut c_void { - libc::memcpy(dst, src, n) -} - -#[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) -} diff --git a/bindgen/tests/gen_rs.rs b/bindgen/tests/gen_rs.rs deleted file mode 100644 index d558a8e926..0000000000 --- a/bindgen/tests/gen_rs.rs +++ /dev/null @@ -1,236 +0,0 @@ -#[macro_use] -extern crate pretty_assertions; - -#[macro_use] -extern crate indoc; - -mod helpers; - -#[cfg(test)] -mod test_gen_rs { - use crate::helpers::generate_bindings; - - #[test] - fn record_aliased() { - let module = indoc!( - r#" - MyRcd : { a : U64, b : I128 } - - main : MyRcd - main = { a: 1u64, b: 2i128 } - "# - ); - - assert_eq!( - generate_bindings(module) - .strip_prefix('\n') - .unwrap_or_default(), - indoc!( - r#" - #[cfg(any( - target_arch = "x86_64", - target_arch = "x86", - target_arch = "aarch64", - target_arch = "arm", - target_arch = "wasm32" - ))] - #[derive(Clone, Copy, Debug, Default, Eq, Ord, Hash, PartialEq, PartialOrd)] - #[repr(C)] - pub struct MyRcd { - pub b: roc_std::I128, - pub a: u64, - } - "# - ) - ); - } - - #[test] - fn nested_record_aliased() { - let module = indoc!( - r#" - Outer : { x : Inner, y : Str, z : List U8 } - - Inner : { a : U16, b : F32 } - - main : Outer - main = { x: { a: 5, b: 24 }, y: "foo", z: [1, 2] } - "# - ); - - assert_eq!( - generate_bindings(module) - .strip_prefix('\n') - .unwrap_or_default(), - indoc!( - r#" - #[cfg(any( - target_arch = "x86_64", - target_arch = "aarch64" - ))] - #[derive(Clone, Debug, Default, PartialEq, PartialOrd)] - #[repr(C)] - pub struct Outer { - pub y: roc_std::RocStr, - pub z: roc_std::RocList, - pub x: Inner, - } - - #[cfg(any( - target_arch = "x86_64", - target_arch = "x86", - target_arch = "aarch64", - target_arch = "arm", - target_arch = "wasm32" - ))] - #[derive(Clone, Copy, Debug, Default, PartialEq, PartialOrd)] - #[repr(C)] - pub struct Inner { - pub b: f32, - pub a: u16, - } - - #[cfg(any( - target_arch = "x86", - target_arch = "arm", - target_arch = "wasm32" - ))] - #[derive(Clone, Debug, Default, PartialEq, PartialOrd)] - #[repr(C)] - pub struct Outer { - pub x: Inner, - pub y: roc_std::RocStr, - pub z: roc_std::RocList, - } - "# - ) - ); - } - - #[test] - fn record_anonymous() { - let module = "main = { a: 1u64, b: 2u128 }"; - - assert_eq!( - generate_bindings(module) - .strip_prefix('\n') - .unwrap_or_default(), - indoc!( - r#" - #[cfg(any( - target_arch = "x86_64", - target_arch = "x86", - target_arch = "aarch64", - target_arch = "arm", - target_arch = "wasm32" - ))] - #[derive(Clone, Copy, Debug, Default, Eq, Ord, Hash, PartialEq, PartialOrd)] - #[repr(C)] - pub struct R1 { - pub b: roc_std::U128, - pub a: u64, - } - "# - ) - ); - } - - #[test] - fn nested_record_anonymous() { - let module = r#"main = { x: { a: 5u16, b: 24f32 }, y: "foo", z: [1u8, 2] }"#; - - assert_eq!( - generate_bindings(module) - .strip_prefix('\n') - .unwrap_or_default(), - indoc!( - r#" - #[cfg(any( - target_arch = "x86_64", - target_arch = "aarch64" - ))] - #[derive(Clone, Debug, Default, PartialEq, PartialOrd)] - #[repr(C)] - pub struct R1 { - pub y: roc_std::RocStr, - pub z: roc_std::RocList, - pub x: R2, - } - - #[cfg(any( - target_arch = "x86_64", - target_arch = "x86", - target_arch = "aarch64", - target_arch = "arm", - target_arch = "wasm32" - ))] - #[derive(Clone, Copy, Debug, Default, PartialEq, PartialOrd)] - #[repr(C)] - pub struct R2 { - pub b: f32, - pub a: u16, - } - - #[cfg(any( - target_arch = "x86", - target_arch = "arm", - target_arch = "wasm32" - ))] - #[derive(Clone, Debug, Default, PartialEq, PartialOrd)] - #[repr(C)] - pub struct R1 { - pub x: R2, - pub y: roc_std::RocStr, - pub z: roc_std::RocList, - } - "# - ) - ); - } - - #[test] - fn tag_union_enumeration() { - let module = indoc!( - r#" - Enumeration : [Blah, Foo, Bar,] - - main : Enumeration - main = Foo - "# - ); - - assert_eq!( - generate_bindings(module) - .strip_prefix('\n') - .unwrap_or_default(), - indoc!( - r#" - #[cfg(any( - target_arch = "x86_64", - target_arch = "x86", - target_arch = "aarch64", - target_arch = "arm", - target_arch = "wasm32" - ))] - #[derive(Clone, Copy, Eq, Ord, Hash, PartialEq, PartialOrd)] - #[repr(u8)] - pub enum Enumeration { - Bar = 0, - Blah = 1, - Foo = 2, - } - - impl core::fmt::Debug for Enumeration { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - match self { - Self::Bar => f.write_str("Enumeration::Bar"), - Self::Blah => f.write_str("Enumeration::Blah"), - Self::Foo => f.write_str("Enumeration::Foo"), - } - } - } - "# - ) - ); - } -} diff --git a/bindgen/tests/helpers/mod.rs b/bindgen/tests/helpers/mod.rs deleted file mode 100644 index 93624e6956..0000000000 --- a/bindgen/tests/helpers/mod.rs +++ /dev/null @@ -1,76 +0,0 @@ -use roc_bindgen::bindgen_rs; -use roc_bindgen::load::load_types; -use roc_load::Threading; -use std::env; -use std::fs::File; -use std::io::Write; -use std::path::PathBuf; - -#[allow(dead_code)] -pub fn generate_bindings(decl_src: &str) -> String { - use tempfile::tempdir; - - let mut src = indoc!( - r#" - platform "main" - requires {} { nothing : {} } - exposes [] - packages {} - imports [] - provides [main] - - "# - ) - .to_string(); - - src.push_str(decl_src); - - let pairs = { - let dir = tempdir().expect("Unable to create tempdir"); - let filename = PathBuf::from("Package-Config.roc"); - let file_path = dir.path().join(filename); - let full_file_path = file_path.clone(); - let mut file = File::create(file_path).unwrap(); - writeln!(file, "{}", &src).unwrap(); - - let result = load_types(full_file_path, dir.path(), Threading::Single); - - dir.close().expect("Unable to close tempdir"); - - result.expect("had problems loading") - }; - - bindgen_rs::emit(&pairs) -} - -#[allow(dead_code)] -pub fn fixtures_dir(dir_name: &str) -> PathBuf { - let mut path = root_dir(); - - // Descend into cli/tests/fixtures/{dir_name} - path.push("bindgen"); - path.push("tests"); - path.push("fixtures"); - path.push(dir_name); - - path -} - -#[allow(dead_code)] -pub fn root_dir() -> PathBuf { - let mut path = env::current_exe().ok().unwrap(); - - // Get rid of the filename in target/debug/deps/cli_run-99c65e4e9a1fbd06 - path.pop(); - - // If we're in deps/ get rid of deps/ in target/debug/deps/ - if path.ends_with("deps") { - path.pop(); - } - - // Get rid of target/debug/ so we're back at the project root - path.pop(); - path.pop(); - - path -} diff --git a/bindgen/tests/test_bindgen_cli.rs b/bindgen/tests/test_bindgen_cli.rs deleted file mode 100644 index dad40d231b..0000000000 --- a/bindgen/tests/test_bindgen_cli.rs +++ /dev/null @@ -1,236 +0,0 @@ -#[macro_use] -extern crate pretty_assertions; - -#[macro_use] -extern crate indoc; - -extern crate dircpy; -extern crate roc_collections; - -mod helpers; - -#[cfg(test)] -mod bindgen_cli_run { - use crate::helpers::{fixtures_dir, root_dir}; - use cli_utils::helpers::{run_bindgen, run_roc, Out}; - use std::fs; - use std::path::Path; - use std::process::Command; - - // All of these tests rely on `target/` for the `cli` crate being up-to-date, - // so do a `cargo build` on it first! - #[ctor::ctor] - fn init() { - let args = if cfg!(debug_assertions) { - vec!["build"] - } else { - vec!["build", "--release"] - }; - - println!( - "Running `cargo {}` on the `cli` crate before running the tests. This may take a bit!", - args.join(" ") - ); - - let output = Command::new("cargo") - .args(args) - .current_dir(root_dir().join("cli")) - .output() - .unwrap_or_else(|err| { - panic!( - "Failed to `cargo build` roc CLI for bindgen CLI tests - error was: {:?}", - err - ) - }); - - assert!(output.status.success()); - } - - /// This macro does two things. - /// - /// First, it generates and runs a separate test for each of the given - /// expected stdout endings. Each of these should test a particular .roc file - /// in the fixtures/ directory. The fixtures themselves run assertions too, but - /// the stdout check verifies that we're actually running the code we think we are; - /// without it, it would be possible that the fixtures are just exiting without running - /// any assertions, and we would have no way to find out! - /// - /// Second, this generates an extra test which (non-recursively) traverses the - /// fixtures/ directory and verifies that each of the .roc files in there - /// has had a corresponding test generated in the previous step. This test - /// will fail if we ever add a new .roc file to fixtures/ and forget to - /// add a test for it here! - macro_rules! fixtures { - ($($test_name:ident:$fixture_dir:expr => $ends_with:expr,)+) => { - $( - #[test] - #[allow(non_snake_case)] - fn $test_name() { - let dir = fixtures_dir($fixture_dir); - - generate_bindings_for(&dir, std::iter::empty()); - let out = run_app(&dir.join("app.roc"), std::iter::empty()); - - assert!(out.status.success()); - assert_eq!(out.stderr, ""); - assert!( - out.stdout.ends_with($ends_with), - "Unexpected stdout ending - expected {:?} but stdout was: {:?}", - $ends_with, - out.stdout - ); - } - )* - - #[test] - fn all_fixtures_have_tests() { - use roc_collections::VecSet; - - let mut all_fixtures: VecSet = VecSet::default(); - - $( - all_fixtures.insert($fixture_dir.to_string()); - )* - - check_for_tests(&mut all_fixtures); - } - } - } - - fixtures! { - basic_record:"basic-record" => "Record was: MyRcd { b: 42, a: 1995 }\n", - nested_record:"nested-record" => "Record was: Outer { y: \"foo\", z: [1, 2], x: Inner { b: 24.0, a: 5 } }\n", - enumeration:"enumeration" => "tag_union was: MyEnum::Foo, Bar is: MyEnum::Bar, Baz is: MyEnum::Baz\n", - union_with_padding:"union-with-padding" => indoc!(r#" - tag_union was: NonRecursive::Foo("This is a test") - `Foo "small str"` is: NonRecursive::Foo("small str") - `Foo "A long enough string to not be small"` is: NonRecursive::Foo("A long enough string to not be small") - `Bar 123` is: NonRecursive::Bar(123) - `Baz` is: NonRecursive::Baz - `Blah 456` is: NonRecursive::Blah(456) - "#), - union_without_padding:"union-without-padding" => indoc!(r#" - tag_union was: NonRecursive::Foo("This is a test") - `Foo "small str"` is: NonRecursive::Foo("small str") - `Bar 123` is: NonRecursive::Bar(123) - `Baz` is: NonRecursive::Baz - `Blah 456` is: NonRecursive::Blah(456) - "#), - nullable_unwrapped:"nullable-unwrapped" => indoc!(r#" - tag_union was: StrConsList::Cons("World!", StrConsList::Cons("Hello ", StrConsList::Nil)) - `Cons "small str" Nil` is: StrConsList::Cons("small str", StrConsList::Nil) - `Nil` is: StrConsList::Nil - "#), - recursive_union:"recursive-union" => indoc!(r#" - tag_union was: Expr::Concat(Expr::String("Hello, "), Expr::String("World!")) - `Concat (String "Hello, ") (String "World!")` is: Expr::Concat(Expr::String("Hello, "), Expr::String("World!")) - `String "this is a test"` is: Expr::String("this is a test") - "#), - } - - fn check_for_tests(all_fixtures: &mut roc_collections::VecSet) { - use roc_collections::VecSet; - - // todo!("Remove a bunch of duplication - don't have a ton of files in there."); - - let fixtures = fixtures_dir(""); - let entries = std::fs::read_dir(fixtures.as_path()).unwrap_or_else(|err| { - panic!( - "Error trying to read {} as a fixtures directory: {}", - fixtures.to_string_lossy(), - err - ); - }); - - for entry in entries { - let entry = entry.unwrap(); - - if entry.file_type().unwrap().is_dir() { - let fixture_dir_name = entry.file_name().into_string().unwrap(); - - if !all_fixtures.remove(&fixture_dir_name) { - panic!( - "The bindgen fixture directory {} does not have any corresponding tests in cli_run. Please add one, so if it ever stops working, we'll know about it right away!", - entry.path().to_string_lossy() - ); - } - } - } - - assert_eq!(all_fixtures, &mut VecSet::default()); - } - - fn generate_bindings_for<'a, I: IntoIterator>( - platform_dir: &'a Path, - args: I, - ) -> Out { - let package_config = platform_dir.join("Package-Config.roc"); - let bindings_file = platform_dir.join("src").join("bindings.rs"); - let fixture_templates_dir = platform_dir - .parent() - .unwrap() - .parent() - .unwrap() - .join("fixture-templates"); - - // Copy the rust template from the templates directory into the fixture dir. - dircpy::CopyBuilder::new(fixture_templates_dir.join("rust"), platform_dir) - .overwrite(true) // overwrite any files that were already present - .run() - .unwrap(); - - // Delete the bindings file to make sure we're actually regenerating it! - if bindings_file.exists() { - fs::remove_file(&bindings_file) - .expect("Unable to remove bindings.rs in order to regenerate it in the test"); - } - - // Generate a fresh bindings.rs for this platform - let bindgen_out = run_bindgen( - // converting these all to String avoids lifetime issues - args.into_iter().map(|arg| arg.to_string()).chain([ - package_config.to_str().unwrap().to_string(), - bindings_file.to_str().unwrap().to_string(), - ]), - ); - - // If there is any stderr, it should be reporting the runtime and that's it! - if !(bindgen_out.stderr.is_empty() - || bindgen_out.stderr.starts_with("runtime: ") && bindgen_out.stderr.ends_with("ms\n")) - { - panic!( - "`roc-bindgen` command had unexpected stderr: {}", - bindgen_out.stderr - ); - } - - assert!(bindgen_out.status.success(), "bad status {:?}", bindgen_out); - - bindgen_out - } - - fn run_app<'a, I: IntoIterator>(app_file: &'a Path, args: I) -> Out { - // Generate bindings.rs for this platform - let compile_out = run_roc( - // converting these all to String avoids lifetime issues - args.into_iter() - .map(|arg| arg.to_string()) - .chain([app_file.to_str().unwrap().to_string()]), - &[], - ); - - // If there is any stderr, it should be reporting the runtime and that's it! - if !(compile_out.stderr.is_empty() - || compile_out.stderr.starts_with("runtime: ") && compile_out.stderr.ends_with("ms\n")) - { - panic!( - "`roc` command had unexpected stderr: {}", - compile_out.stderr - ); - } - - assert!(compile_out.status.success(), "bad status {:?}", compile_out); - - compile_out - } -} diff --git a/ci/basic_nightly_test.sh b/ci/basic_nightly_test.sh new file mode 100755 index 0000000000..a23ef30ba2 --- /dev/null +++ b/ci/basic_nightly_test.sh @@ -0,0 +1,55 @@ +#!/usr/bin/env bash + +# https://vaneyckt.io/posts/safer_bash_scripts_with_set_euxo_pipefail/ +set -euxo pipefail + +# if to prevent unset vars errror +if [ -n "$(ls | grep -v "roc_nightly.*tar\.gz" | grep -v "^ci$")" ]; then + + # Remove everything in this dir except the tar and ci folder. + # We want to test like a user who would have downloaded the release, so we clean up all files from the repo checkout. + to_delete=$(ls | grep -v "roc_nightly.*tar\.gz" | grep -v "^ci$") + + for file_or_dir in $to_delete + do + echo "Removing: $file_or_dir" + rm -rf "$file_or_dir" + done +fi + +if [[ "$(uname)" == "Darwin" ]]; then + brew install z3 # used by llvm +fi + +# decompress the tar +ls | grep "roc_nightly.*tar\.gz" | xargs tar -xzvf + +# delete tar +ls | grep "roc_nightly.*tar\.gz" | xargs rm -rf + +# rename nightly folder +mv roc_nightly* roc_nightly + +cd roc_nightly + +# test roc hello world +./roc examples/helloWorld.roc + +# test rust platform +./roc examples/platform-switching/rocLovesRust.roc + +# test zig platform +./roc examples/platform-switching/rocLovesZig.roc + +# test C platform +./roc examples/platform-switching/rocLovesC.roc + +# test repl +cd ../ci/repl_basic_test +cargo build --release +cp target/release/repl_basic_test ../../roc_nightly +cd ../../roc_nightly +./repl_basic_test + + +cd .. diff --git a/ci/bench-runner/Cargo.lock b/ci/bench-runner/Cargo.lock deleted file mode 100644 index 3a58084af0..0000000000 --- a/ci/bench-runner/Cargo.lock +++ /dev/null @@ -1,422 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 3 - -[[package]] -name = "aho-corasick" -version = "0.7.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" -dependencies = [ - "memchr", -] - -[[package]] -name = "atty" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" -dependencies = [ - "hermit-abi", - "libc", - "winapi", -] - -[[package]] -name = "autocfg" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" - -[[package]] -name = "bench-runner" -version = "0.1.0" -dependencies = [ - "clap", - "data-encoding", - "is_executable", - "regex", - "ring", -] - -[[package]] -name = "bitflags" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" - -[[package]] -name = "bumpalo" -version = "3.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c59e7af012c713f529e7a3ee57ce9b31ddd858d4b512923602f74608b009631" - -[[package]] -name = "cc" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e70cc2f62c6ce1868963827bd677764c62d07c3d9a3e1fb1177ee1a9ab199eb2" - -[[package]] -name = "cfg-if" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" - -[[package]] -name = "clap" -version = "3.1.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85a35a599b11c089a7f49105658d089b8f2cf0882993c17daf6de15285c2c35d" -dependencies = [ - "atty", - "bitflags", - "clap_derive", - "clap_lex", - "indexmap", - "lazy_static", - "strsim", - "termcolor", - "textwrap", -] - -[[package]] -name = "clap_derive" -version = "3.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3aab4734e083b809aaf5794e14e756d1c798d2c69c7f7de7a09a2f5214993c1" -dependencies = [ - "heck", - "proc-macro-error", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "clap_lex" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a37c35f1112dad5e6e0b1adaff798507497a18fceeb30cceb3bae7d1427b9213" -dependencies = [ - "os_str_bytes", -] - -[[package]] -name = "data-encoding" -version = "2.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ee2393c4a91429dffb4bedf19f4d6abf27d8a732c8ce4980305d782e5426d57" - -[[package]] -name = "hashbrown" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" - -[[package]] -name = "heck" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" - -[[package]] -name = "hermit-abi" -version = "0.1.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" -dependencies = [ - "libc", -] - -[[package]] -name = "indexmap" -version = "1.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc633605454125dec4b66843673f01c7df2b89479b32e0ed634e43a91cff62a5" -dependencies = [ - "autocfg", - "hashbrown", -] - -[[package]] -name = "is_executable" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa9acdc6d67b75e626ad644734e8bc6df893d9cd2a834129065d3dd6158ea9c8" -dependencies = [ - "winapi", -] - -[[package]] -name = "js-sys" -version = "0.3.51" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83bdfbace3a0e81a4253f73b49e960b053e396a11012cbd49b9b74d6a2b67062" -dependencies = [ - "wasm-bindgen", -] - -[[package]] -name = "lazy_static" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" - -[[package]] -name = "libc" -version = "0.2.98" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "320cfe77175da3a483efed4bc0adc1968ca050b098ce4f2f1c13a56626128790" - -[[package]] -name = "log" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "memchr" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b16bd47d9e329435e309c58469fe0791c2d0d1ba96ec0954152a5ae2b04387dc" - -[[package]] -name = "once_cell" -version = "1.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56" - -[[package]] -name = "os_str_bytes" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e22443d1643a904602595ba1cd8f7d896afe56d26712531c5ff73a15b2fbf64" - -[[package]] -name = "proc-macro-error" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" -dependencies = [ - "proc-macro-error-attr", - "proc-macro2", - "quote", - "syn", - "version_check", -] - -[[package]] -name = "proc-macro-error-attr" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" -dependencies = [ - "proc-macro2", - "quote", - "version_check", -] - -[[package]] -name = "proc-macro2" -version = "1.0.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c7ed8b8c7b886ea3ed7dde405212185f423ab44682667c8c6dd14aa1d9f6612" -dependencies = [ - "unicode-xid", -] - -[[package]] -name = "quote" -version = "1.0.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "regex" -version = "1.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.6.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" - -[[package]] -name = "ring" -version = "0.16.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" -dependencies = [ - "cc", - "libc", - "once_cell", - "spin", - "untrusted", - "web-sys", - "winapi", -] - -[[package]] -name = "spin" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" - -[[package]] -name = "strsim" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" - -[[package]] -name = "syn" -version = "1.0.74" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1873d832550d4588c3dbc20f01361ab00bfe741048f71e3fecf145a7cc18b29c" -dependencies = [ - "proc-macro2", - "quote", - "unicode-xid", -] - -[[package]] -name = "termcolor" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "textwrap" -version = "0.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" - -[[package]] -name = "unicode-xid" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" - -[[package]] -name = "untrusted" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" - -[[package]] -name = "version_check" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" - -[[package]] -name = "wasm-bindgen" -version = "0.2.74" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d54ee1d4ed486f78874278e63e4069fc1ab9f6a18ca492076ffb90c5eb2997fd" -dependencies = [ - "cfg-if", - "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.74" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b33f6a0694ccfea53d94db8b2ed1c3a8a4c86dd936b13b9f0a15ec4a451b900" -dependencies = [ - "bumpalo", - "lazy_static", - "log", - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.74" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "088169ca61430fe1e58b8096c24975251700e7b1f6fd91cc9d59b04fb9b18bd4" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.74" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be2241542ff3d9f241f5e2cb6dd09b37efe786df8851c54957683a49f0987a97" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-backend", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.74" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7cff876b8f18eed75a66cf49b65e7f967cb354a7aa16003fb55dbfd25b44b4f" - -[[package]] -name = "web-sys" -version = "0.3.51" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e828417b379f3df7111d3a2a9e5753706cae29c41f7c4029ee9fd77f3e09e582" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-util" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" -dependencies = [ - "winapi", -] - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/ci/bench-runner/Cargo.toml b/ci/bench-runner/Cargo.toml deleted file mode 100644 index 8449107831..0000000000 --- a/ci/bench-runner/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "bench-runner" -version = "0.1.0" -edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -clap = { version = "3.1.15", features = ["derive"] } -regex = "1.5.4" -is_executable = "1.0.1" -ring = "0.16.20" -data-encoding = "2.3.2" \ No newline at end of file diff --git a/ci/bench-runner/src/main.rs b/ci/bench-runner/src/main.rs deleted file mode 100644 index 6cab1cf003..0000000000 --- a/ci/bench-runner/src/main.rs +++ /dev/null @@ -1,255 +0,0 @@ -use clap::Parser; -use data_encoding::HEXUPPER; -use is_executable::IsExecutable; -use regex::Regex; -use ring::digest::{Context, Digest, SHA256}; -use std::fs::File; -use std::io::Read; -use std::{ - collections::{HashMap, HashSet, VecDeque}, - io::{self, BufRead, BufReader}, - path::Path, - process::{self, Command, Stdio}, -}; - -const BENCH_FOLDER_TRUNK: &str = "bench-folder-trunk"; -const BENCH_FOLDER_BRANCH: &str = "bench-folder-branch"; - -fn main() { - let optional_args: OptionalArgs = OptionalArgs::parse(); - - if Path::new(BENCH_FOLDER_TRUNK).exists() && Path::new(BENCH_FOLDER_BRANCH).exists() { - delete_old_bench_results(); - - if optional_args.check_executables_changed { - println!("Doing a test run to verify benchmarks are working correctly and generate executables."); - - std::env::set_var("BENCH_DRY_RUN", "1"); - - do_benchmark("trunk"); - do_benchmark("branch"); - - std::env::set_var("BENCH_DRY_RUN", "0"); - - if check_if_bench_executables_changed() { - println!( - "Comparison of sha256 of executables reveals changes, doing full benchmarks..." - ); - - let all_regressed_benches = do_all_benches(optional_args.nr_repeat_benchmarks); - - finish(all_regressed_benches, optional_args.nr_repeat_benchmarks); - } else { - println!("No benchmark executables have changed"); - } - } else { - let all_regressed_benches = do_all_benches(optional_args.nr_repeat_benchmarks); - - finish(all_regressed_benches, optional_args.nr_repeat_benchmarks); - } - } else { - eprintln!( - r#"I can't find bench-folder-trunk and bench-folder-branch from the current directory. - I should be executed from the repo root. - Use `./ci/safe-earthly.sh --build-arg BENCH_SUFFIX=trunk +prep-bench-folder` to generate bench-folder-trunk. - Use `./ci/safe-earthly.sh +prep-bench-folder` to generate bench-folder-branch."# - ); - - process::exit(1) - } -} - -fn finish(all_regressed_benches: HashSet, nr_repeat_benchmarks: usize) { - if !all_regressed_benches.is_empty() { - eprintln!( - r#" - - FAILED: The following benchmarks have shown a regression {:?} times: {:?} - - "#, - nr_repeat_benchmarks, all_regressed_benches - ); - - process::exit(1); - } -} - -// returns all benchmarks that have regressed -fn do_all_benches(nr_repeat_benchmarks: usize) -> HashSet { - delete_old_bench_results(); - do_benchmark("trunk"); - let mut all_regressed_benches = do_benchmark("branch"); - - // if no benches regressed this round, abort early - if all_regressed_benches.is_empty() { - return HashSet::new(); - } - - for _ in 1..nr_repeat_benchmarks { - delete_old_bench_results(); - do_benchmark("trunk"); - let regressed_benches = do_benchmark("branch"); - - // if no benches regressed this round, abort early - if regressed_benches.is_empty() { - return HashSet::new(); - } - - all_regressed_benches = all_regressed_benches - .intersection(®ressed_benches) - .map(|bench_name_str| bench_name_str.to_owned()) - .collect(); - } - - all_regressed_benches -} - -// returns Vec with names of regressed benchmarks -fn do_benchmark(branch_name: &'static str) -> HashSet { - let mut cmd_child = Command::new(format!( - "./bench-folder-{}/target/release/deps/time_bench", - branch_name - )) - .args(&["--bench", "--noplot"]) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .spawn() - .unwrap_or_else(|_| panic!("Failed to benchmark {}.", branch_name)); - - let stdout = cmd_child.stdout.as_mut().unwrap(); - let stdout_reader = BufReader::new(stdout); - let stdout_lines = stdout_reader.lines(); - - let mut regressed_benches: HashSet = HashSet::new(); - - let mut last_three_lines_queue: VecDeque = VecDeque::with_capacity(3); - let bench_name_regex = Regex::new(r#"".*""#).expect("Failed to build regex"); - - for line in stdout_lines { - let line_str = line.expect("Failed to get output from banchmark command."); - - if line_str.contains("regressed") { - let regressed_bench_name_line = last_three_lines_queue.get(2).expect( - "Failed to get line that contains benchmark name from last_three_lines_queue.", - ); - - let regex_match = bench_name_regex.find(regressed_bench_name_line).expect("This line should hoave the benchmark name between double quotes but I could not match it"); - - regressed_benches.insert(regex_match.as_str().to_string().replace("\"", "")); - } - - last_three_lines_queue.push_front(line_str.clone()); - - println!("bench {:?}: {:?}", branch_name, line_str); - } - - regressed_benches -} - -fn delete_old_bench_results() { - remove("target/criterion"); -} - -// does not error if fileOrFolder does not exist (-f flag) -fn remove(file_or_folder: &str) { - Command::new("rm") - .args(&["-rf", file_or_folder]) - .stdout(Stdio::inherit()) - .stderr(Stdio::inherit()) - .output() - .unwrap_or_else(|_| panic!("Something went wrong trying to remove {}", file_or_folder)); -} - -#[derive(Parser)] -struct OptionalArgs { - /// How many times to repeat the benchmarks. A single benchmark has to fail every for a regression to be reported. - #[clap(long, default_value = "3")] - nr_repeat_benchmarks: usize, - /// Do not run full benchmarks if no benchmark executable has changed - #[clap(long)] - check_executables_changed: bool, -} - -fn sha256_digest(mut reader: R) -> Result { - let mut context = Context::new(&SHA256); - let mut buffer = [0; 1024]; - - loop { - let count = reader.read(&mut buffer)?; - if count == 0 { - break; - } - context.update(&buffer[..count]); - } - - Ok(context.finish()) -} - -fn sha_file(file_path: &Path) -> Result { - let input = File::open(file_path)?; - let reader = BufReader::new(input); - let digest = sha256_digest(reader)?; - - Ok(HEXUPPER.encode(digest.as_ref())) -} - -fn calc_hashes_for_folder(benches_path_str: &str) -> HashMap { - let benches_path = Path::new(benches_path_str); - let all_bench_files = - std::fs::read_dir(benches_path).expect("Failed to create iterator for files in dir."); - - let non_src_files = all_bench_files - .into_iter() - .map(|file_res| { - file_res - .expect("Failed to get DirEntry from ReadDir all_bench_files") - .file_name() - .into_string() - .expect("Failed to create String from OsString for file_name.") - }) - .filter(|file_name_str| !file_name_str.contains(".roc")); - - let mut files_w_sha = HashMap::new(); - - for file_name in non_src_files { - let full_path_str = [benches_path_str, &file_name].join(""); - let full_path = Path::new(&full_path_str); - - if full_path.is_executable() { - files_w_sha.insert( - file_name.clone(), - sha_file(full_path).expect("Failed to calculate sha of file"), - ); - } - } - - files_w_sha -} - -fn check_if_bench_executables_changed() -> bool { - let bench_folder_str = "/examples/benchmarks/"; - - let trunk_benches_path_str = [BENCH_FOLDER_TRUNK, bench_folder_str].join(""); - let trunk_bench_hashes = calc_hashes_for_folder(&trunk_benches_path_str); - - let branch_benches_path_str = [BENCH_FOLDER_BRANCH, bench_folder_str].join(""); - let branch_bench_hashes = calc_hashes_for_folder(&branch_benches_path_str); - - if trunk_bench_hashes.keys().len() == branch_bench_hashes.keys().len() { - for key in trunk_bench_hashes.keys() { - if let Some(trunk_hash_val) = trunk_bench_hashes.get(key) { - if let Some(branch_hash_val) = branch_bench_hashes.get(key) { - if !trunk_hash_val.eq(branch_hash_val) { - return true; - } - } else { - return true; - } - } - } - - false - } else { - true - } -} diff --git a/ci/benchmarks/bench-runner/Cargo.toml b/ci/benchmarks/bench-runner/Cargo.toml new file mode 100644 index 0000000000..477b6672ae --- /dev/null +++ b/ci/benchmarks/bench-runner/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "bench-runner" +version = "0.0.1" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +clap = { version = "3.1.15", features = ["derive"] } +data-encoding = "2.3.2" +is_executable = "1.0.1" +regex = "1.5.5" +ring = "0.16.20" diff --git a/ci/benchmarks/bench-runner/src/main.rs b/ci/benchmarks/bench-runner/src/main.rs new file mode 100644 index 0000000000..adc9437696 --- /dev/null +++ b/ci/benchmarks/bench-runner/src/main.rs @@ -0,0 +1,272 @@ +use clap::Parser; +use data_encoding::HEXUPPER; +use is_executable::IsExecutable; +use regex::Regex; +use ring::digest::{Context, Digest, SHA256}; +use std::fs::File; +use std::io::Read; +use std::{ + collections::{HashMap, HashSet, VecDeque}, + io::{self, BufRead, BufReader}, + path::Path, + process::{self, Command, Stdio}, +}; + +const BENCH_FOLDER_MAIN: &str = "bench-folder-main"; +const BENCH_FOLDER_BRANCH: &str = "bench-folder-branch"; + +fn main() { + let optional_args: OptionalArgs = OptionalArgs::parse(); + + if Path::new(BENCH_FOLDER_MAIN).exists() && Path::new(BENCH_FOLDER_BRANCH).exists() { + delete_old_bench_results(); + + if optional_args.check_executables_changed { + println!("Doing a test run to verify benchmarks are working correctly and generate executables."); + + std::env::set_var("BENCH_DRY_RUN", "1"); + + do_benchmark("main"); + do_benchmark("branch"); + + std::env::set_var("BENCH_DRY_RUN", "0"); + + if check_if_bench_executables_changed() { + println!( + "\n\nComparison of sha256 of executables reveals changes, doing full benchmarks...\n\n" + ); + + let all_regressed_benches = do_all_benches(optional_args.nr_repeat_benchmarks); + + finish(all_regressed_benches, optional_args.nr_repeat_benchmarks); + } else { + println!("No benchmark executables have changed"); + } + } else { + let all_regressed_benches = do_all_benches(optional_args.nr_repeat_benchmarks); + + finish(all_regressed_benches, optional_args.nr_repeat_benchmarks); + } + } else { + eprintln!( + r#"I can't find bench-folder-main and bench-folder-branch from the current directory. + I should be executed from the repo root. + Use `./ci/benchmarks/prep_folder.sh main` to generate bench-folder-main. + Use `./ci/benchmarks/prep_folder.sh branch` to generate bench-folder-branch."# + ); + + process::exit(1) + } +} + +fn finish(all_regressed_benches: HashSet, nr_repeat_benchmarks: usize) { + if !all_regressed_benches.is_empty() { + eprintln!( + r#" + + FAILED: The following benchmarks have shown a regression {:?} times: {:?} + + TIP: It may be the case that you do not have a speedup that is on main, I recommend pulling in main to make sure. + + "#, + nr_repeat_benchmarks, all_regressed_benches + ); + + process::exit(1); + } +} + +// returns all benchmarks that have regressed +fn do_all_benches(nr_repeat_benchmarks: usize) -> HashSet { + delete_old_bench_results(); + do_benchmark("main"); + let mut all_regressed_benches = do_benchmark("branch"); + + // if no benches regressed this round, abort early + if all_regressed_benches.is_empty() { + return HashSet::new(); + } + + println!("\n\nDoing benchmarks {:?} times to reduce flukes.\n\n", nr_repeat_benchmarks); + + for _ in 1..nr_repeat_benchmarks { + delete_old_bench_results(); + do_benchmark("main"); + let regressed_benches = do_benchmark("branch"); + + // if no benches regressed this round, abort early + if regressed_benches.is_empty() { + return HashSet::new(); + } + + all_regressed_benches = all_regressed_benches + .intersection(®ressed_benches) + .map(|bench_name_str| bench_name_str.to_owned()) + .collect(); + } + + all_regressed_benches +} + +// returns Vec with names of regressed benchmarks +fn do_benchmark(branch_name: &'static str) -> HashSet { + let mut cmd_child = Command::new(format!( + "./bench-folder-{}/target/release/deps/time_bench", + branch_name + )) + .args(&["--bench", "--noplot"]) + .stdout(Stdio::piped()) + .stderr(Stdio::inherit()) + .spawn() + .unwrap_or_else(|_| panic!("Failed to benchmark {}.", branch_name)); + + let stdout = cmd_child.stdout.as_mut().unwrap(); + let stdout_reader = BufReader::new(stdout); + let stdout_lines = stdout_reader.lines(); + + let mut regressed_benches: HashSet = HashSet::new(); + + let mut last_three_lines_queue: VecDeque = VecDeque::with_capacity(3); + let bench_name_regex = Regex::new(r#"".*""#).expect("Failed to build regex"); + + for line in stdout_lines { + let line_str = line.expect("Failed to get output from banchmark command."); + + if line_str.contains("regressed") { + let regressed_bench_name_line = last_three_lines_queue.get(2).expect( + "Failed to get line that contains benchmark name from last_three_lines_queue.", + ); + + let regex_match = bench_name_regex.find(regressed_bench_name_line).expect("This line should have the benchmark name between double quotes but I could not match it"); + + regressed_benches.insert(regex_match.as_str().to_string().replace("\"", "")); + } + + last_three_lines_queue.push_front(line_str.clone()); + + println!(">>bench {:?}: {:?}", branch_name, line_str); + } + + regressed_benches +} + +fn delete_old_bench_results() { + remove("target/criterion"); +} + +// does not error if fileOrFolder does not exist (-f flag) +fn remove(file_or_folder: &str) { + Command::new("rm") + .args(&["-rf", file_or_folder]) + .stdout(Stdio::inherit()) + .stderr(Stdio::inherit()) + .output() + .unwrap_or_else(|_| panic!("Something went wrong trying to remove {}", file_or_folder)); +} + +#[derive(Parser)] +struct OptionalArgs { + /// How many times to repeat the benchmarks. A single benchmark has to fail every for a regression to be reported. + #[clap(long, default_value = "3")] + nr_repeat_benchmarks: usize, + /// Do not run full benchmarks if no benchmark executable has changed + #[clap(long)] + check_executables_changed: bool, +} + +fn sha256_digest(mut reader: R) -> Result { + let mut context = Context::new(&SHA256); + let mut buffer = [0; 1024]; + + loop { + let count = reader.read(&mut buffer)?; + if count == 0 { + break; + } + context.update(&buffer[..count]); + } + + Ok(context.finish()) +} + +fn sha_file(file_path: &Path) -> Result { + // Debug info is dependent on the dir in which executable was created, + // so we need to strip that to be able to compare binaries. + let no_debug_info_file_path = file_path.to_str().unwrap().to_string() + ("_no_debug_info"); + std::fs::copy(file_path, &no_debug_info_file_path)?; + + let strip_output = Command::new("strip") + .args(["--strip-debug", &no_debug_info_file_path]) + .output() + .expect("failed to execute process"); + + assert!(strip_output.status.success()); + + let no_debug_info_file = File::open(no_debug_info_file_path)?; + let reader = BufReader::new(no_debug_info_file); + let digest = sha256_digest(reader)?; + + Ok(HEXUPPER.encode(digest.as_ref())) +} + +fn calc_hashes_for_folder(benches_path_str: &str) -> HashMap { + let benches_path = Path::new(benches_path_str); + let all_bench_files = + std::fs::read_dir(benches_path).expect("Failed to create iterator for files in dir."); + + let non_src_files = all_bench_files + .into_iter() + .map(|file_res| { + file_res + .expect("Failed to get DirEntry from ReadDir all_bench_files") + .file_name() + .into_string() + .expect("Failed to create String from OsString for file_name.") + }) + .filter(|file_name_str| !file_name_str.contains(".roc")); + + let mut files_w_sha = HashMap::new(); + + for file_name in non_src_files { + let full_path_str = [benches_path_str, &file_name].join(""); + let full_path = Path::new(&full_path_str); + + if full_path.is_executable() { + files_w_sha.insert( + file_name.clone(), + sha_file(full_path).expect("Failed to calculate sha of file"), + ); + } + } + + files_w_sha +} + +fn check_if_bench_executables_changed() -> bool { + let bench_folder_str = "/crates/cli_testing_examples/benchmarks/"; + + let main_benches_path_str = [BENCH_FOLDER_MAIN, bench_folder_str].join(""); + + let main_bench_hashes = calc_hashes_for_folder(&main_benches_path_str); + + let branch_benches_path_str = [BENCH_FOLDER_BRANCH, bench_folder_str].join(""); + let branch_bench_hashes = calc_hashes_for_folder(&branch_benches_path_str); + + if main_bench_hashes.keys().len() == branch_bench_hashes.keys().len() { + for key in main_bench_hashes.keys() { + if let Some(main_hash_val) = main_bench_hashes.get(key) { + if let Some(branch_hash_val) = branch_bench_hashes.get(key) { + if !main_hash_val.eq(branch_hash_val) { + return true; + } + } else { + return true; + } + } + } + + false + } else { + true + } +} diff --git a/ci/benchmarks/prep_folder.sh b/ci/benchmarks/prep_folder.sh new file mode 100755 index 0000000000..486abe152c --- /dev/null +++ b/ci/benchmarks/prep_folder.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash + +# compile everything needed for benchmarks and output a self-contained dir from which benchmarks can be run. + +# https://vaneyckt.io/posts/safer_bash_scripts_with_set_euxo_pipefail/ +set -euxo pipefail + +# to make use of avx, avx2, sse2, sse4.2... instructions +RUSTFLAGS="-C link-arg=-fuse-ld=lld -C target-cpu=native" +BENCH_SUFFIX=$1 + +cargo criterion -V +cd crates/cli && cargo criterion --no-run && cd ../.. +mkdir -p bench-folder/crates/cli_testing_examples/benchmarks +mkdir -p bench-folder/crates/compiler/builtins/bitcode/src +mkdir -p bench-folder/target/release/deps +cp "crates/cli_testing_examples/benchmarks/"*".roc" bench-folder/crates/cli_testing_examples/benchmarks/ +cp -r crates/cli_testing_examples/benchmarks/platform bench-folder/crates/cli_testing_examples/benchmarks/ +cp crates/compiler/builtins/bitcode/src/str.zig bench-folder/crates/compiler/builtins/bitcode/src +cp target/release/roc bench-folder/target/release + +# copy the most recent time bench to bench-folder +cp target/release/deps/`ls -t target/release/deps/ | grep time_bench | head -n 1` bench-folder/target/release/deps/time_bench +mv bench-folder bench-folder-$BENCH_SUFFIX \ No newline at end of file diff --git a/ci/build_basic_cli.sh b/ci/build_basic_cli.sh new file mode 100755 index 0000000000..d4885ee2bd --- /dev/null +++ b/ci/build_basic_cli.sh @@ -0,0 +1,49 @@ +#!/usr/bin/env bash + +# https://vaneyckt.io/posts/safer_bash_scripts_with_set_euxo_pipefail/ +set -euxo pipefail + +git clone https://github.com/roc-lang/basic-cli.git + +if [ "$(uname -s)" == "Linux" ]; then + + # check if musl-tools is installed + if ! dpkg -l | grep -q musl-tools; then + # install musl-tools with timeout for sudo problems with CI + timeout 300s sudo apt-get install -y musl-tools + fi + + cd basic-cli/src # we cd to install the target for the right rust version + if [ "$(uname -m)" == "x86_64" ]; then + rustup target add x86_64-unknown-linux-musl + elif [ "$(uname -m)" == "aarch64" ]; then + rustup target add aarch64-unknown-linux-musl + fi + cd ../.. +fi + +mv $(ls -d artifact/* | grep "roc_nightly.*tar\.gz" | grep "$1") ./roc_nightly.tar.gz + +# decompress the tar +tar -xzvf roc_nightly.tar.gz + +# delete tar +rm roc_nightly.tar.gz + +# simplify dir name +mv roc_nightly* roc_nightly + +cd roc_nightly + +# build the basic cli platform +./roc build ../basic-cli/examples/countdown.roc --optimize + +# We need this extra variable so we can safely check if $2 is empty later +EXTRA_ARGS=${2:-} + +# In some rare cases it's nice to be able to use the legacy linker, so we produce the .o file to be able to do that +if [ -n "${EXTRA_ARGS}" ]; + then ./roc build $EXTRA_ARGS ../basic-cli/examples/countdown.roc --optimize +fi + +cd .. diff --git a/ci/build_basic_webserver.sh b/ci/build_basic_webserver.sh new file mode 100755 index 0000000000..bbd1e103c3 --- /dev/null +++ b/ci/build_basic_webserver.sh @@ -0,0 +1,56 @@ +#!/usr/bin/env bash + +# https://vaneyckt.io/posts/safer_bash_scripts_with_set_euxo_pipefail/ +set -euxo pipefail + +git clone https://github.com/roc-lang/basic-webserver.git + +OS=$(uname -s) +ARCH=$(uname -m) + +if [ "$OS" == "Linux" ]; then + + # check if musl-tools is installed + if ! dpkg -l | grep -q musl-tools; then + # install musl-tools with timeout for sudo problems with CI + timeout 300s sudo apt-get install -y musl-tools + fi + + cd basic-webserver/platform # we cd to install the target for the right rust version + if [ "$ARCH" == "x86_64" ]; then + rustup target add x86_64-unknown-linux-musl + elif [ "$ARCH" == "aarch64" ]; then + rustup target add aarch64-unknown-linux-musl + fi + cd ../.. +fi + +# simplify tar name +mv $(ls -d artifact/* | grep "roc_nightly.*tar\.gz" | grep "$1") ./roc_nightly.tar.gz + +# decompress the tar +tar -xzvf roc_nightly.tar.gz + +# delete tar +rm roc_nightly.tar.gz + +# simplify dir name +mv roc_nightly* roc_nightly + +cd roc_nightly + +# prevent https://github.com/roc-lang/basic-webserver/issues/9 +if [ "$OS" != "Linux" ] || [ "$ARCH" != "x86_64" ]; then + # build the basic-webserver platform + ./roc build ../basic-webserver/examples/echo.roc --optimize +fi + +# We need this extra variable so we can safely check if $2 is empty later +EXTRA_ARGS=${2:-} + +# In some rare cases it's nice to be able to use the legacy linker, so we produce the .o file to be able to do that +if [ -n "${EXTRA_ARGS}" ]; + then ./roc build $EXTRA_ARGS ../basic-webserver/examples/echo.roc --optimize +fi + +cd .. diff --git a/ci/enable-lld.sh b/ci/enable-lld.sh index a70301f8dc..4567172783 100755 --- a/ci/enable-lld.sh +++ b/ci/enable-lld.sh @@ -1,4 +1,7 @@ -#!/bin/bash +#!/usr/bin/env bash + +# https://vaneyckt.io/posts/safer_bash_scripts_with_set_euxo_pipefail/ +set -euxo pipefail mkdir -p $HOME/.cargo echo -e "[build]\nrustflags = [\"-C\", \"link-arg=-fuse-ld=lld\", \"-C\", \"target-cpu=native\"]" > $HOME/.cargo/config diff --git a/ci/package_release.sh b/ci/package_release.sh new file mode 100755 index 0000000000..e99680f3e5 --- /dev/null +++ b/ci/package_release.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash + +# https://vaneyckt.io/posts/safer_bash_scripts_with_set_euxo_pipefail/ +set -euxo pipefail + +cp target/release-with-lto/roc ./roc # to be able to delete "target" later + +# delete unnecessary files and folders +git clean -fdx --exclude roc + +mkdir $1 + + +mv roc LICENSE LEGAL_DETAILS $1 + +mkdir $1/examples +mv examples/helloWorld.roc examples/platform-switching examples/cli $1/examples + +mkdir -p $1/crates/compiler/builtins/bitcode +mv crates/roc_std $1/crates +mv crates/compiler/builtins/bitcode/src $1/crates/compiler/builtins/bitcode + +tar -czvf "$1.tar.gz" $1 \ No newline at end of file diff --git a/ci/repl_basic_test/Cargo.toml b/ci/repl_basic_test/Cargo.toml new file mode 100644 index 0000000000..3c8f814698 --- /dev/null +++ b/ci/repl_basic_test/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "repl_basic_test" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +rexpect = "0.5.0" diff --git a/ci/repl_basic_test/src/main.rs b/ci/repl_basic_test/src/main.rs new file mode 100644 index 0000000000..2d64944c64 --- /dev/null +++ b/ci/repl_basic_test/src/main.rs @@ -0,0 +1,36 @@ +use rexpect::error::Error; +use rexpect::session::PtyReplSession; +use rexpect::spawn; +use std::thread; +use std::time::Duration; + +fn roc_repl_session() -> Result { + let roc_repl = PtyReplSession { + echo_on: false, + prompt: "\u{1b}[0K\u{1b}[34m»\u{1b}[0m ".to_string(), + pty_session: spawn("./roc repl", Some(7000))?, + quit_command: None, + }; + thread::sleep(Duration::from_secs(1)); + Ok(roc_repl) +} + +fn main() -> Result<(), Error> { + let mut repl = roc_repl_session()?; + + repl.exp_regex(".*roc repl.*")?; + repl.send_line("1+1")?; + + thread::sleep(Duration::from_secs(1)); + + match repl.exp_regex(r".*2\u{1b}\[35m : \u{1b}\[0mNum *.*") { + Ok((a, b)) => { + println!("Expected output received."); + return Ok(()); + } + Err(_) => { + eprintln!("\nError: output was different from expected value."); + std::process::exit(1); + } + } +} \ No newline at end of file diff --git a/ci/safe-earthly.sh b/ci/safe-earthly.sh deleted file mode 100755 index 0ddc13c5c0..0000000000 --- a/ci/safe-earthly.sh +++ /dev/null @@ -1,23 +0,0 @@ -#!/usr/bin/env bash -LOG_FILE="earthly_log.txt" -touch $LOG_FILE - -# first arg + everything after -ARGS=${@:1} -FULL_CMD="earthly --config ci/earthly-conf.yml $ARGS" -echo $FULL_CMD -script -efq $LOG_FILE -c "$FULL_CMD" -EXIT_CODE=$? - -if grep -q "failed to mount" "$LOG_FILE"; then - echo "" - echo "" - echo "------<<<<<>>>>>------" - echo "DETECTED FAILURE TO MOUNT ERROR: running without cache" - echo "------<<<<<>>>>>------" - echo "" - echo "" - earthly --config ci/earthly-conf.yml --no-cache $ARGS -else - exit $EXIT_CODE -fi diff --git a/ci/write_version.sh b/ci/write_version.sh new file mode 100755 index 0000000000..8e6f99fdc7 --- /dev/null +++ b/ci/write_version.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +# https://vaneyckt.io/posts/safer_bash_scripts_with_set_euxo_pipefail/ +set -euxo pipefail + +# version.txt is used by the CLI: roc --version +printf 'nightly pre-release, built from commit ' > version.txt && git log --pretty=format:'%h' -n 1 >> version.txt && printf ' on ' >> version.txt && date -u >> version.txt \ No newline at end of file diff --git a/ci/www-repl.sh b/ci/www-repl.sh new file mode 100755 index 0000000000..1cacbbaacc --- /dev/null +++ b/ci/www-repl.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +# https://vaneyckt.io/posts/safer_bash_scripts_with_set_euxo_pipefail/ +set -euxo pipefail + +crates/repl_wasm/build-www.sh `pwd`/roc_repl_wasm.tar.gz diff --git a/cli/Cargo.toml b/cli/Cargo.toml deleted file mode 100644 index 948e1dd17b..0000000000 --- a/cli/Cargo.toml +++ /dev/null @@ -1,99 +0,0 @@ -[package] -name = "roc_cli" -version = "0.1.0" -authors = ["The Roc Contributors"] -license = "UPL-1.0" -repository = "https://github.com/rtfeldman/roc" -edition = "2021" -description = "A CLI for Roc" -default-run = "roc" - -[[bin]] -name = "roc" -path = "src/main.rs" -test = false -bench = false - -[features] -default = ["target-aarch64", "target-x86_64", "target-wasm32", "editor"] - -wasm32-cli-run = ["target-wasm32", "run-wasm32"] -i386-cli-run = ["target-x86"] - -editor = ["roc_editor"] - -run-wasm32 = ["wasmer", "wasmer-wasi"] - -# Compiling for a different platform than the host can cause linker errors. -target-arm = ["roc_build/target-arm", "roc_repl_cli/target-arm"] -target-aarch64 = ["roc_build/target-aarch64", "roc_repl_cli/target-aarch64"] -target-x86 = ["roc_build/target-x86", "roc_repl_cli/target-x86"] -target-x86_64 = ["roc_build/target-x86_64", "roc_repl_cli/target-x86_64"] -target-wasm32 = ["roc_build/target-wasm32", "roc_repl_cli/target-wasm32"] - -target-all = [ - "target-aarch64", - "target-arm", - "target-x86", - "target-x86_64", - "target-wasm32" -] - - -[dependencies] -roc_collections = { path = "../compiler/collections" } -roc_can = { path = "../compiler/can" } -roc_docs = { path = "../docs" } -roc_parse = { path = "../compiler/parse" } -roc_region = { path = "../compiler/region" } -roc_module = { path = "../compiler/module" } -roc_builtins = { path = "../compiler/builtins" } -roc_mono = { path = "../compiler/mono" } -roc_load = { path = "../compiler/load" } -roc_build = { path = "../compiler/build" } -roc_fmt = { path = "../compiler/fmt" } -roc_target = { path = "../compiler/roc_target" } -roc_reporting = { path = "../reporting" } -roc_error_macros = { path = "../error_macros" } -roc_editor = { path = "../editor", optional = true } -roc_linker = { path = "../linker" } -roc_repl_cli = { path = "../repl_cli", optional = true } -clap = { version = "3.1.15", default-features = false, features = ["std", "color", "suggestions"] } -const_format = { version = "0.2.23", features = ["const_generics"] } -bumpalo = { version = "3.8.0", features = ["collections"] } -mimalloc = { version = "0.1.26", default-features = false } -libc = "0.2.106" -errno = "0.2.8" - -target-lexicon = "0.12.3" -tempfile = "3.2.0" -wasmer-wasi = { version = "2.2.1", optional = true } - -# Wasmer singlepass compiler only works on x86_64. -[target.'cfg(target_arch = "x86_64")'.dependencies] -wasmer = { version = "2.2.1", optional = true, default-features = false, features = ["singlepass", "universal"] } - -[target.'cfg(not(target_arch = "x86_64"))'.dependencies] -wasmer = { version = "2.2.1", optional = true, default-features = false, features = ["cranelift", "universal"] } - -[dev-dependencies] -wasmer-wasi = "2.2.1" -pretty_assertions = "1.0.0" -roc_test_utils = { path = "../test_utils" } -indoc = "1.0.3" -serial_test = "0.5.1" -criterion = { git = "https://github.com/Anton-4/criterion.rs"} -cli_utils = { path = "../cli_utils" } -strum = "0.24.0" -strum_macros = "0.24" - -# Wasmer singlepass compiler only works on x86_64. -[target.'cfg(target_arch = "x86_64")'.dev-dependencies] -wasmer = { version = "2.2.1", default-features = false, features = ["singlepass", "universal"] } - -[target.'cfg(not(target_arch = "x86_64"))'.dev-dependencies] -wasmer = { version = "2.2.1", default-features = false, features = ["cranelift", "universal"] } - -[[bench]] -name = "time_bench" -harness = false diff --git a/cli/benches/README.md b/cli/benches/README.md deleted file mode 100644 index 0c91904cfa..0000000000 --- a/cli/benches/README.md +++ /dev/null @@ -1,17 +0,0 @@ - -# Running the benchmarks - -Install cargo criterion: -``` -cargo install cargo-criterion -``` - -To prevent stack overflow on the `CFold` benchmark: -``` -ulimit -s unlimited -``` - -In the `cli` folder execute: -``` -cargo criterion -``` \ No newline at end of file diff --git a/cli/src/build.rs b/cli/src/build.rs deleted file mode 100644 index a485a7fde1..0000000000 --- a/cli/src/build.rs +++ /dev/null @@ -1,497 +0,0 @@ -use bumpalo::Bump; -use roc_build::{ - link::{link, preprocess_host_wasm32, rebuild_host, LinkType, LinkingStrategy}, - program::{self, Problems}, -}; -use roc_builtins::bitcode; -use roc_load::{LoadingProblem, Threading}; -use roc_mono::ir::OptLevel; -use roc_reporting::report::RenderTarget; -use roc_target::TargetInfo; -use std::time::{Duration, SystemTime}; -use std::{path::PathBuf, thread::JoinHandle}; -use target_lexicon::Triple; -use tempfile::Builder; - -fn report_timing(buf: &mut String, label: &str, duration: Duration) { - buf.push_str(&format!( - " {:9.3} ms {}\n", - duration.as_secs_f64() * 1000.0, - label, - )); -} - -pub struct BuiltFile { - pub binary_path: PathBuf, - pub problems: Problems, - pub total_time: Duration, -} - -#[allow(clippy::too_many_arguments)] -pub fn build_file<'a>( - arena: &'a Bump, - target: &Triple, - src_dir: PathBuf, - roc_file_path: PathBuf, - opt_level: OptLevel, - emit_debug_info: bool, - emit_timings: bool, - link_type: LinkType, - linking_strategy: LinkingStrategy, - precompiled: bool, - target_valgrind: bool, - threading: Threading, -) -> Result> { - let compilation_start = SystemTime::now(); - let target_info = TargetInfo::from(target); - - // Step 1: compile the app and generate the .o file - let subs_by_module = Default::default(); - - let loaded = roc_load::load_and_monomorphize( - arena, - roc_file_path.clone(), - src_dir.as_path(), - subs_by_module, - target_info, - // TODO: expose this from CLI? - RenderTarget::ColorTerminal, - threading, - )?; - - use target_lexicon::Architecture; - let emit_wasm = matches!(target.architecture, Architecture::Wasm32); - - // TODO wasm host extension should be something else ideally - // .bc does not seem to work because - // - // > Non-Emscripten WebAssembly hasn't implemented __builtin_return_address - // - // and zig does not currently emit `.a` webassembly static libraries - let host_extension = if emit_wasm { - if matches!(opt_level, OptLevel::Development) { - "wasm" - } else { - "zig" - } - } else { - "o" - }; - let app_extension = if emit_wasm { - if matches!(opt_level, OptLevel::Development) { - "wasm" - } else { - "bc" - } - } else { - "o" - }; - - let cwd = roc_file_path.parent().unwrap(); - let mut binary_path = cwd.join(&*loaded.output_path); // TODO should join ".exe" on Windows - - if emit_wasm { - binary_path.set_extension("wasm"); - } - - let mut host_input_path = PathBuf::from(cwd); - let path_to_platform = loaded.platform_path.clone(); - host_input_path.push(&*path_to_platform); - host_input_path.push("host"); - host_input_path.set_extension(host_extension); - - // TODO this should probably be moved before load_and_monomorphize. - // To do this we will need to preprocess files just for their exported symbols. - // Also, we should no longer need to do this once we have platforms on - // a package repository, as we can then get precompiled hosts from there. - - let exposed_values = loaded - .exposed_to_host - .values - .keys() - .map(|x| x.as_str(&loaded.interns).to_string()) - .collect(); - - let exposed_closure_types = loaded - .exposed_to_host - .closure_types - .iter() - .map(|x| x.as_str(&loaded.interns).to_string()) - .collect(); - - let preprocessed_host_path = if emit_wasm { - host_input_path.with_file_name("preprocessedhost.o") - } else { - host_input_path.with_file_name("preprocessedhost") - }; - - let rebuild_thread = spawn_rebuild_thread( - opt_level, - linking_strategy, - precompiled, - host_input_path.clone(), - preprocessed_host_path.clone(), - binary_path.clone(), - target, - exposed_values, - exposed_closure_types, - target_valgrind, - ); - - // TODO try to move as much of this linking as possible to the precompiled - // host, to minimize the amount of host-application linking required. - let app_o_file = Builder::new() - .prefix("roc_app") - .suffix(&format!(".{}", app_extension)) - .tempfile() - .map_err(|err| { - todo!("TODO Gracefully handle tempfile creation error {:?}", err); - })?; - let app_o_file = app_o_file.path(); - let buf = &mut String::with_capacity(1024); - - let mut it = loaded.timings.iter().peekable(); - while let Some((module_id, module_timing)) = it.next() { - let module_name = loaded.interns.module_name(*module_id); - - buf.push_str(" "); - - if module_name.is_empty() { - // the App module - buf.push_str("Application Module"); - } else { - buf.push_str(module_name); - } - - buf.push('\n'); - - report_timing(buf, "Read .roc file from disk", module_timing.read_roc_file); - report_timing(buf, "Parse header", module_timing.parse_header); - report_timing(buf, "Parse body", module_timing.parse_body); - report_timing(buf, "Canonicalize", module_timing.canonicalize); - report_timing(buf, "Constrain", module_timing.constrain); - report_timing(buf, "Solve", module_timing.solve); - report_timing( - buf, - "Find Specializations", - module_timing.find_specializations, - ); - report_timing( - buf, - "Make Specializations", - module_timing.make_specializations, - ); - report_timing(buf, "Other", module_timing.other()); - buf.push('\n'); - report_timing(buf, "Total", module_timing.total()); - - if it.peek().is_some() { - buf.push('\n'); - } - } - - // This only needs to be mutable for report_problems. This can't be done - // inside a nested scope without causing a borrow error! - let mut loaded = loaded; - let problems = program::report_problems_monomorphized(&mut loaded); - let loaded = loaded; - - enum HostRebuildTiming { - BeforeApp(u128), - ConcurrentWithApp(JoinHandle), - } - - let rebuild_timing = if linking_strategy == LinkingStrategy::Additive { - let rebuild_duration = rebuild_thread.join().unwrap(); - if emit_timings && !precompiled { - println!( - "Finished rebuilding and preprocessing the host in {} ms\n", - rebuild_duration - ); - } - HostRebuildTiming::BeforeApp(rebuild_duration) - } else { - HostRebuildTiming::ConcurrentWithApp(rebuild_thread) - }; - - let code_gen_timing = program::gen_from_mono_module( - arena, - loaded, - &roc_file_path, - target, - app_o_file, - opt_level, - emit_debug_info, - &preprocessed_host_path, - ); - - buf.push('\n'); - buf.push_str(" "); - buf.push_str("Code Generation"); - buf.push('\n'); - - report_timing( - buf, - "Generate Assembly from Mono IR", - code_gen_timing.code_gen, - ); - report_timing(buf, "Emit .o file", code_gen_timing.emit_o_file); - - let compilation_end = compilation_start.elapsed().unwrap(); - - let size = std::fs::metadata(&app_o_file) - .unwrap_or_else(|err| { - panic!( - "Could not open {:?} - which was supposed to have been generated. Error: {:?}", - app_o_file, err - ); - }) - .len(); - - if emit_timings { - println!( - "\n\nCompilation finished!\n\nHere's how long each module took to compile:\n\n{}", - buf - ); - - println!( - "Finished compilation and code gen in {} ms\n\nProduced a app.o file of size {:?}\n", - compilation_end.as_millis(), - size, - ); - } - - if let HostRebuildTiming::ConcurrentWithApp(thread) = rebuild_timing { - let rebuild_duration = thread.join().unwrap(); - if emit_timings && !precompiled { - println!( - "Finished rebuilding and preprocessing the host in {} ms\n", - rebuild_duration - ); - } - } - - // Step 2: link the precompiled host and compiled app - let link_start = SystemTime::now(); - let problems = match (linking_strategy, link_type) { - (LinkingStrategy::Surgical, _) => { - roc_linker::link_preprocessed_host(target, &host_input_path, app_o_file, &binary_path) - .map_err(|err| { - todo!( - "gracefully handle failing to surgically link with error: {:?}", - err - ); - })?; - problems - } - (LinkingStrategy::Additive, _) | (LinkingStrategy::Legacy, LinkType::None) => { - // Just copy the object file to the output folder. - binary_path.set_extension(app_extension); - std::fs::copy(app_o_file, &binary_path).unwrap(); - problems - } - (LinkingStrategy::Legacy, _) => { - let mut inputs = vec![ - host_input_path.as_path().to_str().unwrap(), - app_o_file.to_str().unwrap(), - ]; - if matches!(opt_level, OptLevel::Development) { - inputs.push(bitcode::BUILTINS_HOST_OBJ_PATH); - } - - let (mut child, _) = // TODO use lld - link( - target, - binary_path.clone(), - &inputs, - link_type - ) - .map_err(|_| { - todo!("gracefully handle `ld` failing to spawn."); - })?; - - let exit_status = child.wait().map_err(|_| { - todo!("gracefully handle error after `ld` spawned"); - })?; - - if exit_status.success() { - problems - } else { - let mut problems = problems; - - // Add an error for `ld` failing - problems.errors += 1; - - problems - } - } - }; - - let linking_time = link_start.elapsed().unwrap(); - - if emit_timings { - println!("Finished linking in {} ms\n", linking_time.as_millis()); - } - - let total_time = compilation_start.elapsed().unwrap(); - - Ok(BuiltFile { - binary_path, - problems, - total_time, - }) -} - -#[allow(clippy::too_many_arguments)] -fn spawn_rebuild_thread( - opt_level: OptLevel, - linking_strategy: LinkingStrategy, - precompiled: bool, - host_input_path: PathBuf, - preprocessed_host_path: PathBuf, - binary_path: PathBuf, - target: &Triple, - exported_symbols: Vec, - exported_closure_types: Vec, - target_valgrind: bool, -) -> std::thread::JoinHandle { - let thread_local_target = target.clone(); - std::thread::spawn(move || { - if !precompiled { - println!("🔨 Rebuilding host..."); - } - - let rebuild_host_start = SystemTime::now(); - - if !precompiled { - match linking_strategy { - LinkingStrategy::Additive => { - let host_dest = rebuild_host( - opt_level, - &thread_local_target, - host_input_path.as_path(), - None, - target_valgrind, - ); - - preprocess_host_wasm32(host_dest.as_path(), &preprocessed_host_path); - } - LinkingStrategy::Surgical => { - roc_linker::build_and_preprocess_host( - opt_level, - &thread_local_target, - host_input_path.as_path(), - preprocessed_host_path.as_path(), - exported_symbols, - exported_closure_types, - target_valgrind, - ) - .unwrap(); - } - LinkingStrategy::Legacy => { - rebuild_host( - opt_level, - &thread_local_target, - host_input_path.as_path(), - None, - target_valgrind, - ); - } - } - } - if linking_strategy == LinkingStrategy::Surgical { - // Copy preprocessed host to executable location. - std::fs::copy(preprocessed_host_path, binary_path.as_path()).unwrap(); - } - let rebuild_host_end = rebuild_host_start.elapsed().unwrap(); - - rebuild_host_end.as_millis() - }) -} - -#[allow(clippy::too_many_arguments)] -pub fn check_file( - arena: &Bump, - src_dir: PathBuf, - roc_file_path: PathBuf, - emit_timings: bool, - threading: Threading, -) -> Result<(program::Problems, Duration), LoadingProblem> { - let compilation_start = SystemTime::now(); - - // only used for generating errors. We don't do code generation, so hardcoding should be fine - // we need monomorphization for when exhaustiveness checking - let target_info = TargetInfo::default_x86_64(); - - // Step 1: compile the app and generate the .o file - let subs_by_module = Default::default(); - - let mut loaded = roc_load::load_and_typecheck( - arena, - roc_file_path, - src_dir.as_path(), - subs_by_module, - target_info, - // TODO: expose this from CLI? - RenderTarget::ColorTerminal, - threading, - )?; - - let buf = &mut String::with_capacity(1024); - - let mut it = loaded.timings.iter().peekable(); - while let Some((module_id, module_timing)) = it.next() { - let module_name = loaded.interns.module_name(*module_id); - - buf.push_str(" "); - - if module_name.is_empty() { - // the App module - buf.push_str("Application Module"); - } else { - buf.push_str(module_name); - } - - buf.push('\n'); - - report_timing(buf, "Read .roc file from disk", module_timing.read_roc_file); - report_timing(buf, "Parse header", module_timing.parse_header); - report_timing(buf, "Parse body", module_timing.parse_body); - report_timing(buf, "Canonicalize", module_timing.canonicalize); - report_timing(buf, "Constrain", module_timing.constrain); - report_timing(buf, "Solve", module_timing.solve); - report_timing( - buf, - "Find Specializations", - module_timing.find_specializations, - ); - report_timing( - buf, - "Make Specializations", - module_timing.make_specializations, - ); - report_timing(buf, "Other", module_timing.other()); - buf.push('\n'); - report_timing(buf, "Total", module_timing.total()); - - if it.peek().is_some() { - buf.push('\n'); - } - } - - let compilation_end = compilation_start.elapsed().unwrap(); - - if emit_timings { - println!( - "\n\nCompilation finished!\n\nHere's how long each module took to compile:\n\n{}", - buf - ); - - println!("Finished checking in {} ms\n", compilation_end.as_millis(),); - } - - Ok(( - program::report_problems_typechecked(&mut loaded), - compilation_end, - )) -} diff --git a/cli/src/format.rs b/cli/src/format.rs deleted file mode 100644 index 5a04bfa17a..0000000000 --- a/cli/src/format.rs +++ /dev/null @@ -1,172 +0,0 @@ -use std::ffi::OsStr; -use std::path::{Path, PathBuf}; - -use crate::FormatMode; -use bumpalo::Bump; -use roc_error_macros::{internal_error, user_error}; -use roc_fmt::def::fmt_toplevel_defs; -use roc_fmt::module::fmt_module; -use roc_fmt::spaces::RemoveSpaces; -use roc_fmt::{Ast, Buf}; -use roc_parse::{ - module::{self, module_defs}, - parser::{Parser, SyntaxError}, - state::State, -}; - -fn flatten_directories(files: std::vec::Vec) -> std::vec::Vec { - let mut to_flatten = files; - let mut files = vec![]; - - while let Some(path) = to_flatten.pop() { - if path.is_dir() { - match path.read_dir() { - Ok(directory) => { - for item in directory { - match item { - Ok(file) => { - let file_path = file.path(); - if file_path.is_dir() { - to_flatten.push(file_path); - } else if is_roc_file(&file_path) { - files.push(file_path); - } - } - - Err(error) => internal_error!( - "There was an error while trying to read a file from a directory: {:?}", - error - ), - } - } - } - - Err(error) => internal_error!( - "There was an error while trying to read the contents of a directory: {:?}", - error - ), - } - } else if is_roc_file(&path) { - files.push(path); - } - } - - files -} - -fn is_roc_file(path: &Path) -> bool { - let ext = path.extension().and_then(OsStr::to_str); - return matches!(ext, Some("roc")); -} - -pub fn format(files: std::vec::Vec, mode: FormatMode) -> Result<(), String> { - let files = flatten_directories(files); - - for file in files { - let arena = Bump::new(); - - let src = std::fs::read_to_string(&file).unwrap(); - - let ast = arena.alloc(parse_all(&arena, &src).unwrap_or_else(|e| { - user_error!("Unexpected parse failure when parsing this formatting:\n\n{:?}\n\nParse error was:\n\n{:?}\n\n", src, e) - })); - let mut buf = Buf::new_in(&arena); - fmt_all(&mut buf, ast); - - let reparsed_ast = arena.alloc(parse_all(&arena, buf.as_str()).unwrap_or_else(|e| { - let mut fail_file = file.clone(); - fail_file.set_extension("roc-format-failed"); - std::fs::write(&fail_file, buf.as_str()).unwrap(); - internal_error!( - "Formatting bug; formatted code isn't valid\n\n\ - I wrote the incorrect result to this file for debugging purposes:\n{}\n\n\ - Parse error was: {:?}\n\n", - fail_file.display(), - e - ); - })); - - let ast_normalized = ast.remove_spaces(&arena); - let reparsed_ast_normalized = reparsed_ast.remove_spaces(&arena); - - // HACK! - // We compare the debug format strings of the ASTs, because I'm finding in practice that _somewhere_ deep inside the ast, - // the PartialEq implementation is returning `false` even when the Debug-formatted impl is exactly the same. - // I don't have the patience to debug this right now, so let's leave it for another day... - // TODO: fix PartialEq impl on ast types - if format!("{:?}", ast_normalized) != format!("{:?}", reparsed_ast_normalized) { - let mut fail_file = file.clone(); - fail_file.set_extension("roc-format-failed"); - std::fs::write(&fail_file, buf.as_str()).unwrap(); - - let mut before_file = file.clone(); - before_file.set_extension("roc-format-failed-ast-before"); - std::fs::write(&before_file, &format!("{:#?}\n", ast)).unwrap(); - - let mut after_file = file.clone(); - after_file.set_extension("roc-format-failed-ast-after"); - std::fs::write(&after_file, &format!("{:#?}\n", reparsed_ast)).unwrap(); - - internal_error!( - "Formatting bug; formatting didn't reparse as the same tree\n\n\ - I wrote the incorrect result to this file for debugging purposes:\n{}\n\n\ - I wrote the tree before and after formatting to these files for debugging purposes:\n{}\n{}\n\n", - fail_file.display(), - before_file.display(), - after_file.display()); - } - - // Now verify that the resultant formatting is _stable_ - i.e. that it doesn't change again if re-formatted - let mut reformatted_buf = Buf::new_in(&arena); - fmt_all(&mut reformatted_buf, reparsed_ast); - if buf.as_str() != reformatted_buf.as_str() { - let mut unstable_1_file = file.clone(); - unstable_1_file.set_extension("roc-format-unstable-1"); - std::fs::write(&unstable_1_file, buf.as_str()).unwrap(); - - let mut unstable_2_file = file.clone(); - unstable_2_file.set_extension("roc-format-unstable-2"); - std::fs::write(&unstable_2_file, reformatted_buf.as_str()).unwrap(); - - internal_error!( - "Formatting bug; formatting is not stable. Reformatting the formatted file changed it again.\n\n\ - I wrote the result of formatting to this file for debugging purposes:\n{}\n\n\ - I wrote the result of double-formatting here:\n{}\n\n", - unstable_1_file.display(), - unstable_2_file.display()); - } - - match mode { - FormatMode::CheckOnly => { - // If we notice that this file needs to be formatted, return early - if buf.as_str() != src { - return Err("One or more files need to be reformatted.".to_string()); - } - } - - FormatMode::Format => { - // If all the checks above passed, actually write out the new file. - std::fs::write(&file, buf.as_str()).unwrap(); - } - } - } - - Ok(()) -} - -fn parse_all<'a>(arena: &'a Bump, src: &'a str) -> Result, SyntaxError<'a>> { - let (module, state) = module::parse_header(arena, State::new(src.as_bytes())) - .map_err(|e| SyntaxError::Header(e.problem))?; - - let (_, defs, _) = module_defs().parse(arena, state).map_err(|(_, e, _)| e)?; - - Ok(Ast { module, defs }) -} - -fn fmt_all<'a>(buf: &mut Buf<'a>, ast: &'a Ast) { - fmt_module(buf, &ast.module); - - fmt_toplevel_defs(buf, &ast.defs, 0); - - buf.fmt_end_of_file(); -} diff --git a/cli/src/lib.rs b/cli/src/lib.rs deleted file mode 100644 index 53c6216066..0000000000 --- a/cli/src/lib.rs +++ /dev/null @@ -1,848 +0,0 @@ -#[macro_use] -extern crate const_format; - -use build::BuiltFile; -use bumpalo::Bump; -use clap::{Arg, ArgMatches, Command}; -use roc_build::link::{LinkType, LinkingStrategy}; -use roc_error_macros::{internal_error, user_error}; -use roc_load::{LoadingProblem, Threading}; -use roc_mono::ir::OptLevel; -use std::env; -use std::ffi::{CString, OsStr}; -use std::io; -use std::path::{Path, PathBuf}; -use std::process; -use target_lexicon::BinaryFormat; -use target_lexicon::{ - Architecture, Environment, OperatingSystem, Triple, Vendor, X86_32Architecture, -}; -#[cfg(not(target_os = "linux"))] -use tempfile::TempDir; - -pub mod build; -mod format; -pub use format::format; - -pub const CMD_BUILD: &str = "build"; -pub const CMD_RUN: &str = "run"; -pub const CMD_REPL: &str = "repl"; -pub const CMD_EDIT: &str = "edit"; -pub const CMD_DOCS: &str = "docs"; -pub const CMD_CHECK: &str = "check"; -pub const CMD_VERSION: &str = "version"; -pub const CMD_FORMAT: &str = "format"; - -pub const FLAG_DEBUG: &str = "debug"; -pub const FLAG_DEV: &str = "dev"; -pub const FLAG_OPTIMIZE: &str = "optimize"; -pub const FLAG_MAX_THREADS: &str = "max-threads"; -pub const FLAG_OPT_SIZE: &str = "opt-size"; -pub const FLAG_LIB: &str = "lib"; -pub const FLAG_NO_LINK: &str = "no-link"; -pub const FLAG_TARGET: &str = "target"; -pub const FLAG_TIME: &str = "time"; -pub const FLAG_LINKER: &str = "linker"; -pub const FLAG_PRECOMPILED: &str = "precompiled-host"; -pub const FLAG_VALGRIND: &str = "valgrind"; -pub const FLAG_CHECK: &str = "check"; -pub const ROC_FILE: &str = "ROC_FILE"; -pub const ROC_DIR: &str = "ROC_DIR"; -pub const DIRECTORY_OR_FILES: &str = "DIRECTORY_OR_FILES"; -pub const ARGS_FOR_APP: &str = "ARGS_FOR_APP"; - -const VERSION: &str = include_str!("../../version.txt"); - -pub fn build_app<'a>() -> Command<'a> { - let flag_optimize = Arg::new(FLAG_OPTIMIZE) - .long(FLAG_OPTIMIZE) - .help("Optimize the compiled program to run faster. (Optimization takes time to complete.)") - .requires(ROC_FILE) - .required(false); - - let flag_max_threads = Arg::new(FLAG_MAX_THREADS) - .long(FLAG_MAX_THREADS) - .help("Limit the number of threads (and hence cores) used during compilation.") - .requires(ROC_FILE) - .takes_value(true) - .validator(|s| s.parse::()) - .required(false); - - let flag_opt_size = Arg::new(FLAG_OPT_SIZE) - .long(FLAG_OPT_SIZE) - .help("Optimize the compiled program to have a small binary size. (Optimization takes time to complete.)") - .required(false); - - let flag_dev = Arg::new(FLAG_DEV) - .long(FLAG_DEV) - .help("Make compilation finish as soon as possible, at the expense of runtime performance.") - .required(false); - - let flag_debug = Arg::new(FLAG_DEBUG) - .long(FLAG_DEBUG) - .help("Store LLVM debug information in the generated program.") - .requires(ROC_FILE) - .required(false); - - let flag_valgrind = Arg::new(FLAG_VALGRIND) - .long(FLAG_VALGRIND) - .help("Some assembly instructions are not supported by valgrind, this flag prevents those from being output when building the host.") - .required(false); - - let flag_time = Arg::new(FLAG_TIME) - .long(FLAG_TIME) - .help("Prints detailed compilation time information.") - .required(false); - - let flag_linker = Arg::new(FLAG_LINKER) - .long(FLAG_LINKER) - .help("Sets which linker to use. The surgical linker is enabled by default only when building for wasm32 or x86_64 Linux, because those are the only targets it currently supports. Otherwise the legacy linker is used by default.") - .possible_values(["surgical", "legacy"]) - .required(false); - - let flag_precompiled = Arg::new(FLAG_PRECOMPILED) - .long(FLAG_PRECOMPILED) - .help("Assumes the host has been precompiled and skips recompiling the host. (Enabled by default when using `roc build` with a --target other than `--target host`)") - .possible_values(["true", "false"]) - .required(false); - - let roc_file_to_run = Arg::new(ROC_FILE) - .help("The .roc file of an app to run") - .allow_invalid_utf8(true); - - let args_for_app = Arg::new(ARGS_FOR_APP) - .help("Arguments to pass into the app being run") - .requires(ROC_FILE) - .allow_invalid_utf8(true) - .multiple_values(true); - - let app = Command::new("roc") - .version(concatcp!(VERSION, "\n")) - .about("Runs the given .roc file, if there are no compilation errors.\nUse one of the SUBCOMMANDS below to do something else!") - .subcommand(Command::new(CMD_BUILD) - .about("Build a binary from the given .roc file, but don't run it") - .arg(flag_optimize.clone()) - .arg(flag_max_threads.clone()) - .arg(flag_opt_size.clone()) - .arg(flag_dev.clone()) - .arg(flag_debug.clone()) - .arg(flag_time.clone()) - .arg(flag_linker.clone()) - .arg(flag_precompiled.clone()) - .arg(flag_valgrind.clone()) - .arg( - Arg::new(FLAG_TARGET) - .long(FLAG_TARGET) - .help("Choose a different target") - .default_value(Target::default().as_str()) - .possible_values(Target::OPTIONS) - .required(false), - ) - .arg( - Arg::new(FLAG_LIB) - .long(FLAG_LIB) - .help("Build a C library instead of an executable.") - .required(false), - ) - .arg( - Arg::new(FLAG_NO_LINK) - .long(FLAG_NO_LINK) - .help("Does not link. Instead just outputs the `.o` file") - .required(false), - ) - .arg( - Arg::new(ROC_FILE) - .help("The .roc file to build") - .allow_invalid_utf8(true) - .required(true), - ) - ) - .subcommand(Command::new(CMD_REPL) - .about("Launch the interactive Read Eval Print Loop (REPL)") - ) - .subcommand(Command::new(CMD_RUN) - .about("Run a .roc file even if it has build errors") - .arg(flag_optimize.clone()) - .arg(flag_max_threads.clone()) - .arg(flag_opt_size.clone()) - .arg(flag_dev.clone()) - .arg(flag_debug.clone()) - .arg(flag_time.clone()) - .arg(flag_linker.clone()) - .arg(flag_precompiled.clone()) - .arg(flag_valgrind.clone()) - .arg(roc_file_to_run.clone().required(true)) - .arg(args_for_app.clone()) - ) - .subcommand(Command::new(CMD_FORMAT) - .about("Format a .roc file using standard Roc formatting") - .arg( - Arg::new(DIRECTORY_OR_FILES) - .index(1) - .multiple_values(true) - .required(false) - .allow_invalid_utf8(true)) - .arg( - Arg::new(FLAG_CHECK) - .long(FLAG_CHECK) - .help("Checks that specified files are formatted. If formatting is needed, it will return a non-zero exit code.") - .required(false), - ) - ) - .subcommand(Command::new(CMD_VERSION) - .about(concatcp!("Print the Roc compiler’s version, which is currently ", VERSION))) - .subcommand(Command::new(CMD_CHECK) - .about("Check the code for problems, but doesn’t build or run it") - .arg(flag_time.clone()) - .arg(flag_max_threads.clone()) - .arg( - Arg::new(ROC_FILE) - .help("The .roc file of an app to check") - .allow_invalid_utf8(true) - .required(true), - ) - ) - .subcommand( - Command::new(CMD_DOCS) - .about("Generate documentation for Roc modules (Work In Progress)") - .arg(Arg::new(DIRECTORY_OR_FILES) - .multiple_values(true) - .required(false) - .help("The directory or files to build documentation for") - .allow_invalid_utf8(true) - ) - ) - .trailing_var_arg(true) - .arg(flag_optimize) - .arg(flag_max_threads.clone()) - .arg(flag_opt_size) - .arg(flag_dev) - .arg(flag_debug) - .arg(flag_time) - .arg(flag_linker) - .arg(flag_precompiled) - .arg(flag_valgrind) - .arg(roc_file_to_run.required(false)) - .arg(args_for_app); - - if cfg!(feature = "editor") { - app.subcommand( - Command::new(CMD_EDIT) - .about("Launch the Roc editor (Work In Progress)") - .arg( - Arg::new(DIRECTORY_OR_FILES) - .multiple_values(true) - .required(false) - .help("(optional) The directory or files to open on launch."), - ), - ) - } else { - app - } -} - -#[derive(Debug, PartialEq, Eq)] -pub enum BuildConfig { - BuildOnly, - BuildAndRun, - BuildAndRunIfNoErrors, -} - -pub enum FormatMode { - Format, - CheckOnly, -} - -pub fn build( - matches: &ArgMatches, - config: BuildConfig, - triple: Triple, - link_type: LinkType, -) -> io::Result { - use build::build_file; - use BuildConfig::*; - - let arena = Bump::new(); - let filename = matches.value_of_os(ROC_FILE).unwrap(); - let opt_level = match ( - matches.is_present(FLAG_OPTIMIZE), - matches.is_present(FLAG_OPT_SIZE), - matches.is_present(FLAG_DEV), - ) { - (true, false, false) => OptLevel::Optimize, - (false, true, false) => OptLevel::Size, - (false, false, true) => OptLevel::Development, - (false, false, false) => OptLevel::Normal, - _ => user_error!("build can be only one of `--dev`, `--optimize`, or `--opt-size`"), - }; - let emit_debug_info = matches.is_present(FLAG_DEBUG); - let emit_timings = matches.is_present(FLAG_TIME); - - let threading = match matches - .value_of(FLAG_MAX_THREADS) - .and_then(|s| s.parse::().ok()) - { - None => Threading::AllAvailable, - Some(0) => user_error!("cannot build with at most 0 threads"), - Some(1) => Threading::Single, - Some(n) => Threading::AtMost(n), - }; - - let wasm_dev_backend = matches!(opt_level, OptLevel::Development) - && matches!(triple.architecture, Architecture::Wasm32); - - let linking_strategy = if wasm_dev_backend { - LinkingStrategy::Additive - } else if !roc_linker::supported(&link_type, &triple) - || matches.value_of(FLAG_LINKER) == Some("legacy") - { - LinkingStrategy::Legacy - } else { - LinkingStrategy::Surgical - }; - - let precompiled = if matches.is_present(FLAG_PRECOMPILED) { - matches.value_of(FLAG_PRECOMPILED) == Some("true") - } else { - // When compiling for a different target, default to assuming a precompiled host. - // Otherwise compilation would most likely fail because many toolchains assume you're compiling for the host - // We make an exception for Wasm, because cross-compiling is the norm in that case. - triple != Triple::host() && !matches!(triple.architecture, Architecture::Wasm32) - }; - let path = Path::new(filename); - - // Spawn the root task - let path = path.canonicalize().unwrap_or_else(|err| { - use io::ErrorKind::*; - - match err.kind() { - NotFound => { - match path.to_str() { - Some(path_str) => println!("File not found: {}", path_str), - None => println!("Malformed file path : {:?}", path), - } - - process::exit(1); - } - _ => { - todo!("TODO Gracefully handle opening {:?} - {:?}", path, err); - } - } - }); - - let src_dir = path.parent().unwrap().canonicalize().unwrap(); - let target_valgrind = matches.is_present(FLAG_VALGRIND); - let res_binary_path = build_file( - &arena, - &triple, - src_dir, - path, - opt_level, - emit_debug_info, - emit_timings, - link_type, - linking_strategy, - precompiled, - target_valgrind, - threading, - ); - - match res_binary_path { - Ok(BuiltFile { - binary_path, - problems, - total_time, - }) => { - match config { - BuildOnly => { - // If possible, report the generated executable name relative to the current dir. - let generated_filename = binary_path - .strip_prefix(env::current_dir().unwrap()) - .unwrap_or(&binary_path); - - // No need to waste time freeing this memory, - // since the process is about to exit anyway. - std::mem::forget(arena); - - println!( - "\x1B[{}m{}\x1B[39m {} and \x1B[{}m{}\x1B[39m {} found in {} ms while successfully building:\n\n {}", - if problems.errors == 0 { - 32 // green - } else { - 33 // yellow - }, - problems.errors, - if problems.errors == 1 { - "error" - } else { - "errors" - }, - if problems.warnings == 0 { - 32 // green - } else { - 33 // yellow - }, - problems.warnings, - if problems.warnings == 1 { - "warning" - } else { - "warnings" - }, - total_time.as_millis(), - generated_filename.to_str().unwrap() - ); - - // Return a nonzero exit code if there were problems - Ok(problems.exit_code()) - } - BuildAndRun => { - if problems.errors > 0 || problems.warnings > 0 { - println!( - "\x1B[{}m{}\x1B[39m {} and \x1B[{}m{}\x1B[39m {} found in {} ms.\n\nRunning program anyway…\n\n\x1B[36m{}\x1B[39m", - if problems.errors == 0 { - 32 // green - } else { - 33 // yellow - }, - problems.errors, - if problems.errors == 1 { - "error" - } else { - "errors" - }, - if problems.warnings == 0 { - 32 // green - } else { - 33 // yellow - }, - problems.warnings, - if problems.warnings == 1 { - "warning" - } else { - "warnings" - }, - total_time.as_millis(), - "─".repeat(80) - ); - } - - let args = matches.values_of_os(ARGS_FOR_APP).unwrap_or_default(); - - let mut bytes = std::fs::read(&binary_path).unwrap(); - - let x = roc_run(arena, triple, args, &mut bytes); - std::mem::forget(bytes); - x - } - BuildAndRunIfNoErrors => { - if problems.errors == 0 { - if problems.warnings > 0 { - println!( - "\x1B[32m0\x1B[39m errors and \x1B[33m{}\x1B[39m {} found in {} ms.\n\nRunning program…\n\n\x1B[36m{}\x1B[39m", - problems.warnings, - if problems.warnings == 1 { - "warning" - } else { - "warnings" - }, - total_time.as_millis(), - "─".repeat(80) - ); - } - - let args = matches.values_of_os(ARGS_FOR_APP).unwrap_or_default(); - - let mut bytes = std::fs::read(&binary_path).unwrap(); - - let x = roc_run(arena, triple, args, &mut bytes); - std::mem::forget(bytes); - x - } else { - println!( - "\x1B[{}m{}\x1B[39m {} and \x1B[{}m{}\x1B[39m {} found in {} ms.\n\nYou can run the program anyway with: \x1B[32mroc run {}\x1B[39m", - if problems.errors == 0 { - 32 // green - } else { - 33 // yellow - }, - problems.errors, - if problems.errors == 1 { - "error" - } else { - "errors" - }, - if problems.warnings == 0 { - 32 // green - } else { - 33 // yellow - }, - problems.warnings, - if problems.warnings == 1 { - "warning" - } else { - "warnings" - }, - total_time.as_millis(), - filename.to_string_lossy() - ); - - Ok(problems.exit_code()) - } - } - } - } - Err(LoadingProblem::FormattedReport(report)) => { - print!("{}", report); - - Ok(1) - } - Err(other) => { - panic!("build_file failed with error:\n{:?}", other); - } - } -} - -fn roc_run<'a, I: IntoIterator>( - arena: Bump, // This should be passed an owned value, not a reference, so we can usefully mem::forget it! - triple: Triple, - args: I, - binary_bytes: &mut [u8], -) -> io::Result { - match triple.architecture { - Architecture::Wasm32 => { - let executable = roc_run_executable_file_path(binary_bytes)?; - let path = executable.as_path(); - // If possible, report the generated executable name relative to the current dir. - let generated_filename = path - .strip_prefix(env::current_dir().unwrap()) - .unwrap_or(path); - - // No need to waste time freeing this memory, - // since the process is about to exit anyway. - std::mem::forget(arena); - - if cfg!(target_family = "unix") { - use std::os::unix::ffi::OsStrExt; - - run_with_wasmer( - generated_filename, - args.into_iter().map(|os_str| os_str.as_bytes()), - ); - } else { - run_with_wasmer( - generated_filename, - args.into_iter().map(|os_str| { - os_str.to_str().expect( - "Roc does not currently support passing non-UTF8 arguments to Wasmer.", - ) - }), - ); - } - - Ok(0) - } - _ => roc_run_native(arena, args, binary_bytes), - } -} - -/// Run on the native OS (not on wasm) -#[cfg(target_family = "unix")] -fn roc_run_native, S: AsRef>( - arena: Bump, - args: I, - binary_bytes: &mut [u8], -) -> std::io::Result { - use bumpalo::collections::CollectIn; - use std::os::unix::ffi::OsStrExt; - - unsafe { - let executable = roc_run_executable_file_path(binary_bytes)?; - let path = executable.as_path(); - let path_cstring = CString::new(path.as_os_str().as_bytes()).unwrap(); - - // argv is an array of pointers to strings passed to the new program - // as its command-line arguments. By convention, the first of these - // strings (i.e., argv[0]) should contain the filename associated - // with the file being executed. The argv array must be terminated - // by a NULL pointer. (Thus, in the new program, argv[argc] will be NULL.) - let c_strings: bumpalo::collections::Vec = args - .into_iter() - .map(|x| CString::new(x.as_ref().as_bytes()).unwrap()) - .collect_in(&arena); - - let c_string_pointers = c_strings - .iter() - .map(|x| x.as_bytes_with_nul().as_ptr().cast()); - - let argv: bumpalo::collections::Vec<*const libc::c_char> = - std::iter::once(path_cstring.as_ptr()) - .chain(c_string_pointers) - .chain([std::ptr::null()]) - .collect_in(&arena); - - // envp is an array of pointers to strings, conventionally of the - // form key=value, which are passed as the environment of the new - // program. The envp array must be terminated by a NULL pointer. - let envp_cstrings: bumpalo::collections::Vec = std::env::vars_os() - .flat_map(|(k, v)| { - [ - CString::new(k.as_bytes()).unwrap(), - CString::new(v.as_bytes()).unwrap(), - ] - }) - .collect_in(&arena); - - let envp: bumpalo::collections::Vec<*const libc::c_char> = envp_cstrings - .iter() - .map(|s| s.as_ptr()) - .chain([std::ptr::null()]) - .collect_in(&arena); - - match executable { - #[cfg(target_os = "linux")] - ExecutableFile::MemFd(fd, _) => { - if libc::fexecve(fd, argv.as_ptr(), envp.as_ptr()) != 0 { - internal_error!( - "libc::fexecve({:?}, ..., ...) failed: {:?}", - path, - errno::errno() - ); - } - } - #[cfg(not(target_os = "linux"))] - ExecutableFile::OnDisk(_, _) => { - if libc::execve(path_cstring.as_ptr().cast(), argv.as_ptr(), envp.as_ptr()) != 0 { - internal_error!( - "libc::execve({:?}, ..., ...) failed: {:?}", - path, - errno::errno() - ); - } - } - } - } - - Ok(1) -} - -#[derive(Debug)] -enum ExecutableFile { - #[cfg(target_os = "linux")] - MemFd(libc::c_int, PathBuf), - #[cfg(not(target_os = "linux"))] - OnDisk(TempDir, PathBuf), -} - -impl ExecutableFile { - fn as_path(&self) -> &Path { - match self { - #[cfg(target_os = "linux")] - ExecutableFile::MemFd(_, path_buf) => path_buf.as_ref(), - #[cfg(not(target_os = "linux"))] - ExecutableFile::OnDisk(_, path_buf) => path_buf.as_ref(), - } - } -} - -#[cfg(target_os = "linux")] -fn roc_run_executable_file_path(binary_bytes: &mut [u8]) -> std::io::Result { - // on linux, we use the `memfd_create` function to create an in-memory anonymous file. - let flags = 0; - let anonymous_file_name = "roc_file_descriptor\0"; - let fd = unsafe { libc::memfd_create(anonymous_file_name.as_ptr().cast(), flags) }; - - if fd == 0 { - internal_error!( - "libc::memfd_create({:?}, {}) failed: file descriptor is 0", - anonymous_file_name, - flags - ); - } - - let path = PathBuf::from(format!("/proc/self/fd/{}", fd)); - - std::fs::write(&path, binary_bytes)?; - - Ok(ExecutableFile::MemFd(fd, path)) -} - -#[cfg(all(target_family = "unix", not(target_os = "linux")))] -fn roc_run_executable_file_path(binary_bytes: &mut [u8]) -> std::io::Result { - use std::fs::OpenOptions; - use std::io::Write; - use std::os::unix::fs::OpenOptionsExt; - - let temp_dir = tempfile::tempdir()?; - - // We have not found a way to use a virtual file on non-Linux OSes. - // Hence we fall back to just writing the file to the file system, and using that file. - let app_path_buf = temp_dir.path().join("roc_app_binary"); - let mut file = OpenOptions::new() - .create(true) - .write(true) - .mode(0o777) // create the file as executable - .open(&app_path_buf)?; - - file.write_all(binary_bytes)?; - - // We store the TempDir in this variant alongside the path to the executable, - // so that the TempDir doesn't get dropped until after we're done with the path. - // If we didn't do that, then the tempdir would potentially get deleted by the - // TempDir's Drop impl before the file had been executed. - Ok(ExecutableFile::OnDisk(temp_dir, app_path_buf)) -} - -/// Run on the native OS (not on wasm) -#[cfg(not(target_family = "unix"))] -fn roc_run_native, S: AsRef>( - _arena: Bump, // This should be passed an owned value, not a reference, so we can usefully mem::forget it! - _args: I, - _binary_bytes: &mut [u8], -) -> io::Result { - todo!("TODO support running roc programs on non-UNIX targets"); - // let mut cmd = std::process::Command::new(&binary_path); - - // // Run the compiled app - // let exit_status = cmd - // .spawn() - // .unwrap_or_else(|err| panic!("Failed to run app after building it: {:?}", err)) - // .wait() - // .expect("TODO gracefully handle block_on failing when `roc` spawns a subprocess for the compiled app"); - - // // `roc [FILE]` exits with the same status code as the app it ran. - // // - // // If you want to know whether there were compilation problems - // // via status code, use either `roc build` or `roc check` instead! - // match exit_status.code() { - // Some(code) => Ok(code), - // None => { - // todo!("TODO gracefully handle the `roc [FILE]` subprocess terminating with a signal."); - // } - // } -} - -#[cfg(feature = "run-wasm32")] -fn run_with_wasmer, S: AsRef<[u8]>>(wasm_path: &std::path::Path, args: I) { - use wasmer::{Instance, Module, Store}; - - let store = Store::default(); - let module = Module::from_file(&store, &wasm_path).unwrap(); - - // First, we create the `WasiEnv` - use wasmer_wasi::WasiState; - let mut wasi_env = WasiState::new("hello").args(args).finalize().unwrap(); - - // Then, we get the import object related to our WASI - // and attach it to the Wasm instance. - let import_object = wasi_env.import_object(&module).unwrap(); - - let instance = Instance::new(&module, &import_object).unwrap(); - - let start = instance.exports.get_function("_start").unwrap(); - - use wasmer_wasi::WasiError; - match start.call(&[]) { - Ok(_) => {} - Err(e) => match e.downcast::() { - Ok(WasiError::Exit(0)) => { - // we run the `_start` function, so exit(0) is expected - } - other => panic!("Wasmer error: {:?}", other), - }, - } -} - -#[cfg(not(feature = "run-wasm32"))] -fn run_with_wasmer, S: AsRef<[u8]>>(_wasm_path: &std::path::Path, _args: I) { - println!("Running wasm files is not supported on this target."); -} - -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub enum Target { - System, - Linux32, - Linux64, - Wasm32, -} - -impl Default for Target { - fn default() -> Self { - Target::System - } -} - -impl Target { - const fn as_str(&self) -> &'static str { - use Target::*; - - match self { - System => "system", - Linux32 => "linux32", - Linux64 => "linux64", - Wasm32 => "wasm32", - } - } - - /// NOTE keep up to date! - const OPTIONS: &'static [&'static str] = &[ - Target::System.as_str(), - Target::Linux32.as_str(), - Target::Linux64.as_str(), - Target::Wasm32.as_str(), - ]; - - pub fn to_triple(self) -> Triple { - use Target::*; - - match self { - System => Triple::host(), - Linux32 => Triple { - architecture: Architecture::X86_32(X86_32Architecture::I386), - vendor: Vendor::Unknown, - operating_system: OperatingSystem::Linux, - environment: Environment::Musl, - binary_format: BinaryFormat::Elf, - }, - Linux64 => Triple { - architecture: Architecture::X86_64, - vendor: Vendor::Unknown, - operating_system: OperatingSystem::Linux, - environment: Environment::Musl, - binary_format: BinaryFormat::Elf, - }, - Wasm32 => Triple { - architecture: Architecture::Wasm32, - vendor: Vendor::Unknown, - operating_system: OperatingSystem::Unknown, - environment: Environment::Unknown, - binary_format: BinaryFormat::Wasm, - }, - } - } -} - -impl From<&Target> for Triple { - fn from(target: &Target) -> Self { - target.to_triple() - } -} - -impl std::fmt::Display for Target { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(f, "{}", self.as_str()) - } -} - -impl std::str::FromStr for Target { - type Err = String; - - fn from_str(string: &str) -> Result { - match string { - "system" => Ok(Target::System), - "linux32" => Ok(Target::Linux32), - "linux64" => Ok(Target::Linux64), - "wasm32" => Ok(Target::Wasm32), - _ => Err(format!("Roc does not know how to compile to {}", string)), - } - } -} diff --git a/cli/src/main.rs b/cli/src/main.rs deleted file mode 100644 index 4ee66ef296..0000000000 --- a/cli/src/main.rs +++ /dev/null @@ -1,300 +0,0 @@ -use roc_build::link::LinkType; -use roc_cli::build::check_file; -use roc_cli::{ - build_app, format, BuildConfig, FormatMode, Target, CMD_BUILD, CMD_CHECK, CMD_DOCS, CMD_EDIT, - CMD_FORMAT, CMD_REPL, CMD_RUN, CMD_VERSION, DIRECTORY_OR_FILES, FLAG_CHECK, FLAG_LIB, - FLAG_NO_LINK, FLAG_TARGET, FLAG_TIME, ROC_FILE, -}; -use roc_docs::generate_docs_html; -use roc_error_macros::user_error; -use roc_load::{LoadingProblem, Threading}; -use std::fs::{self, FileType}; -use std::io; -use std::path::{Path, PathBuf}; -use target_lexicon::Triple; - -#[macro_use] -extern crate const_format; - -#[global_allocator] -static ALLOC: mimalloc::MiMalloc = mimalloc::MiMalloc; - -use std::ffi::{OsStr, OsString}; - -use roc_cli::build; - -fn main() -> io::Result<()> { - let matches = build_app().get_matches(); - - let exit_code = match matches.subcommand() { - None => { - if matches.is_present(ROC_FILE) { - build( - &matches, - BuildConfig::BuildAndRunIfNoErrors, - Triple::host(), - LinkType::Executable, - ) - } else { - launch_editor(None)?; - - Ok(0) - } - } - Some((CMD_RUN, matches)) => { - if matches.is_present(ROC_FILE) { - build( - matches, - BuildConfig::BuildAndRun, - Triple::host(), - LinkType::Executable, - ) - } else { - eprintln!("What .roc file do you want to run? Specify it at the end of the `roc run` command."); - - Ok(1) - } - } - Some((CMD_BUILD, matches)) => { - let target: Target = matches.value_of_t(FLAG_TARGET).unwrap_or_default(); - - let link_type = match ( - matches.is_present(FLAG_LIB), - matches.is_present(FLAG_NO_LINK), - ) { - (true, false) => LinkType::Dylib, - (true, true) => user_error!("build can only be one of `--lib` or `--no-link`"), - (false, true) => LinkType::None, - (false, false) => LinkType::Executable, - }; - - Ok(build( - matches, - BuildConfig::BuildOnly, - target.to_triple(), - link_type, - )?) - } - Some((CMD_CHECK, matches)) => { - let arena = bumpalo::Bump::new(); - - let emit_timings = matches.is_present(FLAG_TIME); - let filename = matches.value_of_os(ROC_FILE).unwrap(); - let roc_file_path = PathBuf::from(filename); - let src_dir = roc_file_path.parent().unwrap().to_owned(); - - let threading = match matches - .value_of(roc_cli::FLAG_MAX_THREADS) - .and_then(|s| s.parse::().ok()) - { - None => Threading::AllAvailable, - Some(0) => user_error!("cannot build with at most 0 threads"), - Some(1) => Threading::Single, - Some(n) => Threading::AtMost(n), - }; - - match check_file(&arena, src_dir, roc_file_path, emit_timings, threading) { - Ok((problems, total_time)) => { - println!( - "\x1B[{}m{}\x1B[39m {} and \x1B[{}m{}\x1B[39m {} found in {} ms.", - if problems.errors == 0 { - 32 // green - } else { - 33 // yellow - }, - problems.errors, - if problems.errors == 1 { - "error" - } else { - "errors" - }, - if problems.warnings == 0 { - 32 // green - } else { - 33 // yellow - }, - problems.warnings, - if problems.warnings == 1 { - "warning" - } else { - "warnings" - }, - total_time.as_millis(), - ); - - Ok(problems.exit_code()) - } - - Err(LoadingProblem::FormattedReport(report)) => { - print!("{}", report); - - Ok(1) - } - Err(other) => { - panic!("build_file failed with error:\n{:?}", other); - } - } - } - Some((CMD_REPL, _)) => { - { - roc_repl_cli::main()?; - - // Exit 0 if the repl exited normally - Ok(0) - } - } - Some((CMD_EDIT, matches)) => { - match matches - .values_of_os(DIRECTORY_OR_FILES) - .map(|mut values| values.next()) - { - Some(Some(os_str)) => { - launch_editor(Some(Path::new(os_str)))?; - } - _ => { - launch_editor(None)?; - } - } - - // Exit 0 if the editor exited normally - Ok(0) - } - Some((CMD_DOCS, matches)) => { - let maybe_values = matches.values_of_os(DIRECTORY_OR_FILES); - - let mut values: Vec = Vec::new(); - - match maybe_values { - None => { - let mut os_string_values: Vec = Vec::new(); - read_all_roc_files( - &std::env::current_dir()?.as_os_str().to_os_string(), - &mut os_string_values, - )?; - for os_string in os_string_values { - values.push(os_string); - } - } - Some(os_values) => { - for os_str in os_values { - values.push(os_str.to_os_string()); - } - } - } - - let mut roc_files = Vec::new(); - - // Populate roc_files - for os_str in values { - let metadata = fs::metadata(os_str.clone())?; - roc_files_recursive(os_str.as_os_str(), metadata.file_type(), &mut roc_files)?; - } - - generate_docs_html(roc_files); - - Ok(0) - } - Some((CMD_FORMAT, matches)) => { - let maybe_values = matches.values_of_os(DIRECTORY_OR_FILES); - - let mut values: Vec = Vec::new(); - - match maybe_values { - None => { - let mut os_string_values: Vec = Vec::new(); - read_all_roc_files( - &std::env::current_dir()?.as_os_str().to_os_string(), - &mut os_string_values, - )?; - for os_string in os_string_values { - values.push(os_string); - } - } - Some(os_values) => { - for os_str in os_values { - values.push(os_str.to_os_string()); - } - } - } - - let mut roc_files = Vec::new(); - - // Populate roc_files - for os_str in values { - let metadata = fs::metadata(os_str.clone())?; - roc_files_recursive(os_str.as_os_str(), metadata.file_type(), &mut roc_files)?; - } - - let format_mode = match matches.is_present(FLAG_CHECK) { - true => FormatMode::CheckOnly, - false => FormatMode::Format, - }; - - let format_exit_code = match format(roc_files, format_mode) { - Ok(_) => 0, - Err(message) => { - eprintln!("{}", message); - 1 - } - }; - - Ok(format_exit_code) - } - Some((CMD_VERSION, _)) => { - println!("roc {}", concatcp!(include_str!("../../version.txt"), "\n")); - - Ok(0) - } - _ => unreachable!(), - }?; - - std::process::exit(exit_code); -} - -fn read_all_roc_files( - dir: &OsString, - roc_file_paths: &mut Vec, -) -> Result<(), std::io::Error> { - let entries = fs::read_dir(dir)?; - - for entry in entries { - let path = entry?.path(); - - if path.is_dir() { - read_all_roc_files(&path.into_os_string(), roc_file_paths)?; - } else if path.extension().and_then(OsStr::to_str) == Some("roc") { - let file_path = path.into_os_string(); - roc_file_paths.push(file_path); - } - } - - Ok(()) -} - -fn roc_files_recursive>( - path: P, - file_type: FileType, - roc_files: &mut Vec, -) -> io::Result<()> { - if file_type.is_dir() { - for entry_res in fs::read_dir(path)? { - let entry = entry_res?; - - roc_files_recursive(entry.path(), entry.file_type()?, roc_files)?; - } - } else { - roc_files.push(path.as_ref().to_path_buf()); - } - - Ok(()) -} - -#[cfg(feature = "editor")] -fn launch_editor(project_dir_path: Option<&Path>) -> io::Result<()> { - roc_editor::launch(project_dir_path) -} - -#[cfg(not(feature = "editor"))] -fn launch_editor(_project_dir_path: Option<&Path>) -> io::Result<()> { - panic!("Cannot launch the editor because this build of roc did not include `feature = \"editor\"`!"); -} diff --git a/cli/tests/cli_run.rs b/cli/tests/cli_run.rs deleted file mode 100644 index d0b9136ff7..0000000000 --- a/cli/tests/cli_run.rs +++ /dev/null @@ -1,1114 +0,0 @@ -#[macro_use] -extern crate pretty_assertions; - -extern crate bumpalo; -extern crate indoc; -extern crate roc_collections; -extern crate roc_load; -extern crate roc_module; - -#[cfg(test)] -mod cli_run { - use cli_utils::helpers::{ - example_file, examples_dir, extract_valgrind_errors, fixture_file, fixtures_dir, - known_bad_file, run_cmd, run_roc, run_with_valgrind, strip_colors, Out, ValgrindError, - ValgrindErrorXWhat, - }; - use const_format::concatcp; - use indoc::indoc; - use roc_cli::{CMD_BUILD, CMD_CHECK, CMD_FORMAT, CMD_RUN}; - use roc_test_utils::assert_multiline_str_eq; - use serial_test::serial; - use std::iter; - use std::path::{Path, PathBuf}; - use strum::IntoEnumIterator; - use strum_macros::EnumIter; - - const OPTIMIZE_FLAG: &str = concatcp!("--", roc_cli::FLAG_OPTIMIZE); - const VALGRIND_FLAG: &str = concatcp!("--", roc_cli::FLAG_VALGRIND); - const LINKER_FLAG: &str = concatcp!("--", roc_cli::FLAG_LINKER); - const CHECK_FLAG: &str = concatcp!("--", roc_cli::FLAG_CHECK); - const PRECOMPILED_HOST: &str = concatcp!("--", roc_cli::FLAG_PRECOMPILED, "=true"); - #[allow(dead_code)] - const TARGET_FLAG: &str = concatcp!("--", roc_cli::FLAG_TARGET); - - use std::sync::Once; - static BENCHMARKS_BUILD_PLATFORM: Once = Once::new(); - - #[derive(Debug, EnumIter)] - enum CliMode { - RocBuild, - RocRun, - Roc, - } - - #[cfg(not(debug_assertions))] - use roc_collections::all::MutMap; - - #[cfg(all(target_os = "linux", target_arch = "x86_64"))] - const TEST_LEGACY_LINKER: bool = true; - - // Surgical linker currently only supports linux x86_64, - // so we're always testing the legacy linker on other targets. - #[cfg(not(all(target_os = "linux", target_arch = "x86_64")))] - const TEST_LEGACY_LINKER: bool = false; - - #[cfg(not(target_os = "macos"))] - const ALLOW_VALGRIND: bool = true; - - // Disallow valgrind on macOS by default, because it reports a ton - // of false positives. For local development on macOS, feel free to - // change this to true! - #[cfg(target_os = "macos")] - const ALLOW_VALGRIND: bool = false; - - #[derive(Debug, PartialEq, Eq)] - struct Example<'a> { - filename: &'a str, - executable_filename: &'a str, - stdin: &'a [&'a str], - input_file: Option<&'a str>, - expected_ending: &'a str, - use_valgrind: bool, - } - - fn check_compile_error(file: &Path, flags: &[&str], expected: &str) { - let compile_out = run_roc([CMD_CHECK, file.to_str().unwrap()].iter().chain(flags), &[]); - let err = compile_out.stdout.trim(); - let err = strip_colors(err); - - // e.g. "1 error and 0 warnings found in 123 ms." - let (before_first_digit, _) = err.split_at(err.rfind("found in ").unwrap()); - let err = format!("{}found in ms.", before_first_digit); - - assert_multiline_str_eq!(err.as_str(), expected); - } - - fn check_format_check_as_expected(file: &Path, expects_success_exit_code: bool) { - let out = run_roc([CMD_FORMAT, file.to_str().unwrap(), CHECK_FLAG], &[]); - - assert_eq!(out.status.success(), expects_success_exit_code); - } - - fn run_roc_on<'a, I: IntoIterator>( - file: &'a Path, - args: I, - stdin: &[&str], - input_file: Option, - ) -> Out { - let compile_out = match input_file { - Some(input_file) => run_roc( - // converting these all to String avoids lifetime issues - args.into_iter().map(|arg| arg.to_string()).chain([ - file.to_str().unwrap().to_string(), - input_file.to_str().unwrap().to_string(), - ]), - stdin, - ), - None => run_roc( - args.into_iter().chain(iter::once(file.to_str().unwrap())), - stdin, - ), - }; - - // If there is any stderr, it should be reporting the runtime and that's it! - if !(compile_out.stderr.is_empty() - || compile_out.stderr.starts_with("runtime: ") && compile_out.stderr.ends_with("ms\n")) - { - panic!( - "`roc` command had unexpected stderr: {}", - compile_out.stderr - ); - } - - assert!(compile_out.status.success(), "bad status {:?}", compile_out); - - compile_out - } - - fn check_output_with_stdin( - file: &Path, - stdin: &[&str], - executable_filename: &str, - flags: &[&str], - input_file: Option, - expected_ending: &str, - use_valgrind: bool, - ) { - for cli_mode in CliMode::iter() { - let flags = { - let mut vec = flags.to_vec(); - - if use_valgrind { - vec.push(VALGRIND_FLAG); - } - - vec.into_iter() - }; - - let out = match cli_mode { - CliMode::RocBuild => { - run_roc_on(file, iter::once(CMD_BUILD).chain(flags.clone()), &[], None); - - if use_valgrind && ALLOW_VALGRIND { - let (valgrind_out, raw_xml) = if let Some(ref input_file) = input_file { - run_with_valgrind( - stdin.iter().copied(), - &[ - file.with_file_name(executable_filename).to_str().unwrap(), - input_file.clone().to_str().unwrap(), - ], - ) - } else { - run_with_valgrind( - stdin.iter().copied(), - &[file.with_file_name(executable_filename).to_str().unwrap()], - ) - }; - - if valgrind_out.status.success() { - let memory_errors = extract_valgrind_errors(&raw_xml).unwrap_or_else(|err| { - panic!("failed to parse the `valgrind` xml output. Error was:\n\n{:?}\n\nvalgrind xml was: \"{}\"\n\nvalgrind stdout was: \"{}\"\n\nvalgrind stderr was: \"{}\"", err, raw_xml, valgrind_out.stdout, valgrind_out.stderr); - }); - - if !memory_errors.is_empty() { - for error in memory_errors { - let ValgrindError { - kind, - what: _, - xwhat, - } = error; - println!("Valgrind Error: {}\n", kind); - - if let Some(ValgrindErrorXWhat { - text, - leakedbytes: _, - leakedblocks: _, - }) = xwhat - { - println!(" {}", text); - } - } - panic!("Valgrind reported memory errors"); - } - } else { - let exit_code = match valgrind_out.status.code() { - Some(code) => format!("exit code {}", code), - None => "no exit code".to_string(), - }; - - panic!("`valgrind` exited with {}. valgrind stdout was: \"{}\"\n\nvalgrind stderr was: \"{}\"", exit_code, valgrind_out.stdout, valgrind_out.stderr); - } - - valgrind_out - } else if let Some(ref input_file) = input_file { - run_cmd( - file.with_file_name(executable_filename).to_str().unwrap(), - stdin.iter().copied(), - &[input_file.to_str().unwrap()], - ) - } else { - run_cmd( - file.with_file_name(executable_filename).to_str().unwrap(), - stdin.iter().copied(), - &[], - ) - } - } - CliMode::Roc => run_roc_on(file, flags.clone(), stdin, input_file.clone()), - CliMode::RocRun => run_roc_on( - file, - iter::once(CMD_RUN).chain(flags.clone()), - stdin, - input_file.clone(), - ), - }; - - if !&out.stdout.ends_with(expected_ending) { - panic!( - "expected output to end with {:?} but instead got {:#?} - stderr was: {:#?}", - expected_ending, out.stdout, out.stderr - ); - } - - assert!(out.status.success()); - } - } - - #[cfg(feature = "wasm32-cli-run")] - fn check_wasm_output_with_stdin( - file: &Path, - stdin: &[&str], - executable_filename: &str, - flags: &[&str], - input_file: Option, - expected_ending: &str, - ) { - assert_eq!(input_file, None, "Wasm does not support input files"); - let mut flags = flags.to_vec(); - flags.push(concatcp!(TARGET_FLAG, "=wasm32")); - - let compile_out = run_roc( - [CMD_BUILD, file.to_str().unwrap()] - .iter() - .chain(flags.as_slice()), - ); - if !compile_out.stderr.is_empty() { - panic!("{}", compile_out.stderr); - } - - assert!(compile_out.status.success(), "bad status {:?}", compile_out); - - let path = file.with_file_name(executable_filename); - let stdout = crate::run_with_wasmer(&path, stdin); - - if !stdout.ends_with(expected_ending) { - panic!( - "expected output to end with {:?} but instead got {:#?}", - expected_ending, stdout - ); - } - } - /// This macro does two things. - /// - /// First, it generates and runs a separate test for each of the given - /// Example expressions. Each of these should test a particular .roc file - /// in the examples/ directory. - /// - /// Second, it generates an extra test which (non-recursively) traverses the - /// examples/ directory and verifies that each of the .roc files in there - /// has had a corresponding test generated in the previous step. This test - /// will fail if we ever add a new .roc file to examples/ and forget to - /// add a test for it here! - macro_rules! examples { - ($($test_name:ident:$name:expr => $example:expr,)+) => { - $( - #[test] - #[allow(non_snake_case)] - fn $test_name() { - let dir_name = $name; - let example = $example; - let file_name = example_file(dir_name, example.filename); - - match example.executable_filename { - "helloWeb" => { - // this is a web webassembly example, but we don't test with JS at the moment - eprintln!("WARNING: skipping testing example {} because the test is broken right now!", example.filename); - return; - } - "form" => { - // test is skipped until we upgrate to zig 0.9 / llvm 13 - eprintln!("WARNING: skipping testing example {} because the test is broken right now!", example.filename); - return; - } - "helloSwift" => { - if cfg!(not(target_os = "macos")) { - eprintln!("WARNING: skipping testing example {} because it only works on MacOS.", example.filename); - return; - } - } - "hello-gui" | "breakout" => { - // Since these require opening a window, we do `roc build` on them but don't run them. - run_roc_on(&file_name, [CMD_BUILD, OPTIMIZE_FLAG], &[], None); - - return; - } - _ => {} - } - - // Check with and without optimizations - check_output_with_stdin( - &file_name, - example.stdin, - example.executable_filename, - &[], - example.input_file.and_then(|file| Some(example_file(dir_name, file))), - example.expected_ending, - example.use_valgrind, - ); - - // This is mostly because the false interpreter is still very slow - - // 25s for the cli tests is just not acceptable during development! - #[cfg(not(debug_assertions))] - check_output_with_stdin( - &file_name, - example.stdin, - example.executable_filename, - &[OPTIMIZE_FLAG], - example.input_file.and_then(|file| Some(example_file(dir_name, file))), - example.expected_ending, - example.use_valgrind, - ); - - // Also check with the legacy linker. - - if TEST_LEGACY_LINKER { - check_output_with_stdin( - &file_name, - example.stdin, - example.executable_filename, - &[LINKER_FLAG, "legacy"], - example.input_file.and_then(|file| Some(example_file(dir_name, file))), - example.expected_ending, - example.use_valgrind, - ); - } - } - )* - - #[test] - #[cfg(not(debug_assertions))] - fn all_examples_have_tests() { - let mut all_examples: MutMap<&str, Example<'_>> = MutMap::default(); - - $( - all_examples.insert($name, $example); - )* - - check_for_tests("../examples", &mut all_examples); - } - } - } - - // examples! macro format: - // - // "name-of-subdirectory-inside-examples-dir" => [ - // test_name_1: Example { - // ... - // }, - // test_name_2: Example { - // ... - // }, - // ] - examples! { - helloWorld:"hello-world" => Example { - filename: "helloWorld.roc", - executable_filename: "helloWorld", - stdin: &[], - input_file: None, - expected_ending:"Hello, World!\n", - use_valgrind: true, - }, - helloC:"hello-world/c-platform" => Example { - filename: "helloC.roc", - executable_filename: "helloC", - stdin: &[], - input_file: None, - expected_ending:"Hello, World!\n", - use_valgrind: true, - }, - helloZig:"hello-world/zig-platform" => Example { - filename: "helloZig.roc", - executable_filename: "helloZig", - stdin: &[], - input_file: None, - expected_ending:"Hello, World!\n", - use_valgrind: true, - }, - helloRust:"hello-world/rust-platform" => Example { - filename: "helloRust.roc", - executable_filename: "helloRust", - stdin: &[], - input_file: None, - expected_ending:"Hello, World!\n", - use_valgrind: true, - }, - helloSwift:"hello-world/swift-platform" => Example { - filename: "helloSwift.roc", - executable_filename: "helloSwift", - stdin: &[], - input_file: None, - expected_ending:"Hello, World!\n", - use_valgrind: true, - }, - helloWeb:"hello-world/web-platform" => Example { - filename: "helloWeb.roc", - executable_filename: "helloWeb", - stdin: &[], - input_file: None, - expected_ending:"Hello, World!\n", - use_valgrind: true, - }, - fib:"algorithms" => Example { - filename: "fibonacci.roc", - executable_filename: "fibonacci", - stdin: &[], - input_file: None, - expected_ending:"55\n", - use_valgrind: true, - }, - gui:"gui" => Example { - filename: "Hello.roc", - executable_filename: "hello-gui", - stdin: &[], - input_file: None, - expected_ending: "", - use_valgrind: false, - }, - breakout:"breakout" => Example { - filename: "breakout.roc", - executable_filename: "breakout", - stdin: &[], - input_file: None, - expected_ending: "", - use_valgrind: false, - }, - quicksort:"algorithms" => Example { - filename: "quicksort.roc", - executable_filename: "quicksort", - stdin: &[], - input_file: None, - expected_ending: "[0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2]\n", - use_valgrind: true, - }, - // shared_quicksort:"shared-quicksort" => Example { - // filename: "Quicksort.roc", - // executable_filename: "quicksort", - // stdin: &[], - // input_file: None, - // expected_ending: "[0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2]\n", - // use_valgrind: true, - // }, - effects:"interactive" => Example { - filename: "effects.roc", - executable_filename: "effects", - stdin: &["hi there!"], - input_file: None, - expected_ending: "hi there!\nIt is known\n", - use_valgrind: true, - }, - // tea:"tea" => Example { - // filename: "Main.roc", - // executable_filename: "tea-example", - // stdin: &[], - // input_file: None, - // expected_ending: "", - // use_valgrind: true, - // }, - cli:"interactive" => Example { - filename: "form.roc", - executable_filename: "form", - stdin: &["Giovanni\n", "Giorgio\n"], - input_file: None, - expected_ending: "Hi, Giovanni Giorgio! 👋\n", - use_valgrind: false, - }, - tui:"interactive" => Example { - filename: "tui.roc", - executable_filename: "tui", - stdin: &["foo\n"], // NOTE: adding more lines leads to memory leaks - input_file: None, - expected_ending: "Hello Worldfoo!\n", - use_valgrind: true, - }, - // custom_malloc:"custom-malloc" => Example { - // filename: "Main.roc", - // executable_filename: "custom-malloc-example", - // stdin: &[], - // input_file: None, - // expected_ending: "ms!\nThe list was small!\n", - // use_valgrind: true, - // }, - // task:"task" => Example { - // filename: "Main.roc", - // executable_filename: "task-example", - // stdin: &[], - // input_file: None, - // expected_ending: "successfully wrote to file\n", - // use_valgrind: true, - // }, - false_interpreter:"false-interpreter" => { - Example { - filename: "False.roc", - executable_filename: "false", - stdin: &[], - input_file: Some("examples/hello.false"), - expected_ending:"Hello, World!\n", - use_valgrind: false, - } - }, - } - - macro_rules! benchmarks { - ($($test_name:ident => $benchmark:expr,)+) => { - - $( - #[test] - #[cfg_attr(not(debug_assertions), serial(benchmark))] - #[cfg(all(not(feature = "wasm32-cli-run"), not(feature = "i386-cli-run")))] - fn $test_name() { - let benchmark = $benchmark; - let file_name = examples_dir("benchmarks").join(benchmark.filename); - - // TODO fix QuicksortApp and then remove this! - match benchmark.filename { - "QuicksortApp.roc" => { - eprintln!("WARNING: skipping testing benchmark {} because the test is broken right now!", benchmark.filename); - return; - } - _ => {} - } - - let mut ran_without_optimizations = false; - - BENCHMARKS_BUILD_PLATFORM.call_once( || { - // Check with and without optimizations - check_output_with_stdin( - &file_name, - benchmark.stdin, - benchmark.executable_filename, - &[], - benchmark.input_file.and_then(|file| Some(examples_dir("benchmarks").join(file))), - benchmark.expected_ending, - benchmark.use_valgrind, - ); - - ran_without_optimizations = true; - }); - - // now we can pass the `PRECOMPILED_HOST` flag, because the `call_once` will - // have compiled the host - - if !ran_without_optimizations { - // Check with and without optimizations - check_output_with_stdin( - &file_name, - benchmark.stdin, - benchmark.executable_filename, - &[PRECOMPILED_HOST], - benchmark.input_file.and_then(|file| Some(examples_dir("benchmarks").join(file))), - benchmark.expected_ending, - benchmark.use_valgrind, - ); - } - - check_output_with_stdin( - &file_name, - benchmark.stdin, - benchmark.executable_filename, - &[PRECOMPILED_HOST, OPTIMIZE_FLAG], - benchmark.input_file.and_then(|file| Some(examples_dir("benchmarks").join(file))), - benchmark.expected_ending, - benchmark.use_valgrind, - ); - } - - )* - - #[cfg(feature = "wasm32-cli-run")] - mod wasm32 { - use super::*; - $( - #[test] - #[cfg_attr(not(debug_assertions), serial(benchmark))] - fn $test_name() { - let benchmark = $benchmark; - let file_name = examples_dir("benchmarks").join(benchmark.filename); - - // TODO fix QuicksortApp and then remove this! - match benchmark.filename { - "QuicksortApp.roc" => { - eprintln!("WARNING: skipping testing benchmark {} because the test is broken right now!", benchmark.filename); - return; - } - _ => {} - } - - // Check with and without optimizations - check_wasm_output_with_stdin( - &file_name, - benchmark.stdin, - benchmark.executable_filename, - &[], - benchmark.input_file.and_then(|file| Some(examples_dir("benchmarks").join(file))), - benchmark.expected_ending, - ); - - check_wasm_output_with_stdin( - &file_name, - benchmark.stdin, - benchmark.executable_filename, - &[OPTIMIZE_FLAG], - benchmark.input_file.and_then(|file| Some(examples_dir("benchmarks").join(file))), - benchmark.expected_ending, - ); - } - )* - } - - #[cfg(feature = "i386-cli-run")] - mod i386 { - use super::*; - $( - #[test] - #[cfg_attr(not(debug_assertions), serial(benchmark))] - fn $test_name() { - let benchmark = $benchmark; - let file_name = examples_dir("benchmarks").join(benchmark.filename); - - // TODO fix QuicksortApp and then remove this! - match benchmark.filename { - "QuicksortApp.roc" => { - eprintln!("WARNING: skipping testing benchmark {} because the test is broken right now!", benchmark.filename); - return; - } - _ => {} - } - - // Check with and without optimizations - check_output_with_stdin( - &file_name, - benchmark.stdin, - benchmark.executable_filename, - [concatcp!(TARGET_FLAG, "=x86_32")], - benchmark.input_file.and_then(|file| Some(examples_dir("benchmarks").join(file))), - benchmark.expected_ending, - benchmark.use_valgrind, - ); - - check_output_with_stdin( - &file_name, - benchmark.stdin, - benchmark.executable_filename, - [concatcp!(TARGET_FLAG, "=x86_32"), OPTIMIZE_FLAG], - benchmark.input_file.and_then(|file| Some(examples_dir("benchmarks").join(file))), - benchmark.expected_ending, - benchmark.use_valgrind, - ); - } - )* - } - - #[test] - #[cfg(not(debug_assertions))] - fn all_benchmarks_have_tests() { - let mut all_benchmarks: MutMap<&str, Example<'_>> = MutMap::default(); - - $( - let benchmark = $benchmark; - - all_benchmarks.insert(benchmark.filename, benchmark); - )* - - check_for_benchmarks("../examples/benchmarks", &mut all_benchmarks); - } - } - } - - benchmarks! { - nqueens => Example { - filename: "NQueens.roc", - executable_filename: "nqueens", - stdin: &["6"], - input_file: None, - expected_ending: "4\n", - use_valgrind: true, - }, - cfold => Example { - filename: "CFold.roc", - executable_filename: "cfold", - stdin: &["3"], - input_file: None, - expected_ending: "11 & 11\n", - use_valgrind: true, - }, - deriv => Example { - filename: "Deriv.roc", - executable_filename: "deriv", - stdin: &["2"], - input_file: None, - expected_ending: "1 count: 6\n2 count: 22\n", - use_valgrind: true, - }, - rbtree_ck => Example { - filename: "RBTreeCk.roc", - executable_filename: "rbtree-ck", - stdin: &["100"], - input_file: None, - expected_ending: "10\n", - use_valgrind: true, - }, - rbtree_insert => Example { - filename: "RBTreeInsert.roc", - executable_filename: "rbtree-insert", - stdin: &[], - input_file: None, - expected_ending: "Node Black 0 {} Empty Empty\n", - use_valgrind: true, - }, - // rbtree_del => Example { - // filename: "RBTreeDel.roc", - // executable_filename: "rbtree-del", - // stdin: &["420"], - // input_file: None, - // expected_ending: "30\n", - // use_valgrind: true, - // }, - astar => Example { - filename: "TestAStar.roc", - executable_filename: "test-astar", - stdin: &[], - input_file: None, - expected_ending: "True\n", - use_valgrind: false, - }, - base64 => Example { - filename: "TestBase64.roc", - executable_filename: "test-base64", - stdin: &[], - input_file: None, - expected_ending: "encoded: SGVsbG8gV29ybGQ=\ndecoded: Hello World\n", - use_valgrind: true, - }, - closure => Example { - filename: "Closure.roc", - executable_filename: "closure", - stdin: &[], - input_file: None, - expected_ending: "", - use_valgrind: false, - }, - issue2279 => Example { - filename: "Issue2279.roc", - executable_filename: "issue2279", - stdin: &[], - input_file: None, - expected_ending: "Hello, world!\n", - use_valgrind: true, - }, - quicksort_app => Example { - filename: "QuicksortApp.roc", - executable_filename: "quicksortapp", - stdin: &[], - input_file: None, - expected_ending: "todo put the correct quicksort answer here", - use_valgrind: true, - }, - } - - #[cfg(not(debug_assertions))] - fn check_for_tests(examples_dir: &str, all_examples: &mut MutMap<&str, Example<'_>>) { - let entries = std::fs::read_dir(examples_dir).unwrap_or_else(|err| { - panic!( - "Error trying to read {} as an examples directory: {}", - examples_dir, err - ); - }); - - for entry in entries { - let entry = entry.unwrap(); - - if entry.file_type().unwrap().is_dir() { - let example_dir_name = entry.file_name().into_string().unwrap(); - - // TODO: Improve this with a more-dynamic approach. (Read all subdirectories?) - // Some hello-world examples live in nested directories - if example_dir_name == "hello-world" { - for sub_dir in [ - "c-platform", - "rust-platform", - "swift-platform", - "web-platform", - "zig-platform", - ] { - all_examples.remove(format!("{}/{}", example_dir_name, sub_dir).as_str()).unwrap_or_else(|| { - panic!("The example directory {}/{}/{} does not have any corresponding tests in cli_run. Please add one, so if it ever stops working, we'll know about it right away!", examples_dir, example_dir_name, sub_dir); - }); - } - } - - // We test benchmarks separately - if example_dir_name != "benchmarks" { - all_examples.remove(example_dir_name.as_str()).unwrap_or_else(|| { - panic!("The example directory {}/{} does not have any corresponding tests in cli_run. Please add one, so if it ever stops working, we'll know about it right away!", examples_dir, example_dir_name); - }); - } - } - } - - assert_eq!(all_examples, &mut MutMap::default()); - } - - #[cfg(not(debug_assertions))] - fn check_for_benchmarks(benchmarks_dir: &str, all_benchmarks: &mut MutMap<&str, Example<'_>>) { - use std::ffi::OsStr; - use std::fs::File; - use std::io::Read; - - let entries = std::fs::read_dir(benchmarks_dir).unwrap_or_else(|err| { - panic!( - "Error trying to read {} as a benchmark directory: {}", - benchmarks_dir, err - ); - }); - - for entry in entries { - let entry = entry.unwrap(); - let path = entry.path(); - - if let Some("roc") = path.extension().and_then(OsStr::to_str) { - let benchmark_file_name = entry.file_name().into_string().unwrap(); - - // Verify that this is an app module by reading the first 3 - // bytes of the file. - let buf: &mut [u8] = &mut [0, 0, 0]; - let mut file = File::open(path).unwrap(); - - file.read_exact(buf).unwrap(); - - // Only app modules in this directory are considered benchmarks. - if "app".as_bytes() == buf && !benchmark_file_name.contains("RBTreeDel") { - all_benchmarks.remove(benchmark_file_name.as_str()).unwrap_or_else(|| { - panic!("The benchmark {}/{} does not have any corresponding tests in cli_run. Please add one, so if it ever stops working, we'll know about it right away!", benchmarks_dir, benchmark_file_name); - }); - } - } - } - - assert_eq!(all_benchmarks, &mut MutMap::default()); - } - - #[test] - #[serial(multi_dep_str)] - fn run_multi_dep_str_unoptimized() { - check_output_with_stdin( - &fixture_file("multi-dep-str", "Main.roc"), - &[], - "multi-dep-str", - &[], - None, - "I am Dep2.str2\n", - true, - ); - } - - #[test] - #[serial(multi_dep_str)] - fn run_multi_dep_str_optimized() { - check_output_with_stdin( - &fixture_file("multi-dep-str", "Main.roc"), - &[], - "multi-dep-str", - &[OPTIMIZE_FLAG], - None, - "I am Dep2.str2\n", - true, - ); - } - - #[test] - #[serial(multi_dep_thunk)] - fn run_multi_dep_thunk_unoptimized() { - check_output_with_stdin( - &fixture_file("multi-dep-thunk", "Main.roc"), - &[], - "multi-dep-thunk", - &[], - None, - "I am Dep2.value2\n", - true, - ); - } - - #[test] - #[serial(multi_dep_thunk)] - fn run_multi_dep_thunk_optimized() { - check_output_with_stdin( - &fixture_file("multi-dep-thunk", "Main.roc"), - &[], - "multi-dep-thunk", - &[OPTIMIZE_FLAG], - None, - "I am Dep2.value2\n", - true, - ); - } - - #[test] - fn known_type_error() { - check_compile_error( - &known_bad_file("TypeError.roc"), - &[], - indoc!( - r#" - ── UNRECOGNIZED NAME ─────────────────────────── tests/known_bad/TypeError.roc ─ - - Nothing is named `d` in this scope. - - 10│ _ <- await (line d) - ^ - - Did you mean one of these? - - U8 - Ok - I8 - F64 - - ──────────────────────────────────────────────────────────────────────────────── - - 1 error and 0 warnings found in ms."# - ), - ); - } - - #[test] - fn exposed_not_defined() { - check_compile_error( - &known_bad_file("ExposedNotDefined.roc"), - &[], - indoc!( - r#" - ── MISSING DEFINITION ────────────────── tests/known_bad/ExposedNotDefined.roc ─ - - bar is listed as exposed, but it isn't defined in this module. - - You can fix this by adding a definition for bar, or by removing it - from exposes. - - ──────────────────────────────────────────────────────────────────────────────── - - 1 error and 0 warnings found in ms."# - ), - ); - } - - #[test] - fn unused_import() { - check_compile_error( - &known_bad_file("UnusedImport.roc"), - &[], - indoc!( - r#" - ── UNUSED IMPORT ──────────────────────────── tests/known_bad/UnusedImport.roc ─ - - Nothing from Symbol is used in this module. - - 3│ imports [Symbol.{ Ident }] - ^^^^^^^^^^^^^^^^ - - Since Symbol isn't used, you don't need to import it. - - ──────────────────────────────────────────────────────────────────────────────── - - 0 errors and 1 warning found in ms."# - ), - ); - } - - #[test] - fn unknown_generates_with() { - check_compile_error( - &known_bad_file("UnknownGeneratesWith.roc"), - &[], - indoc!( - r#" - ── UNKNOWN GENERATES FUNCTION ─────── tests/known_bad/UnknownGeneratesWith.roc ─ - - I don't know how to generate the foobar function. - - 4│ generates Effect with [after, map, always, foobar] - ^^^^^^ - - Only specific functions like `after` and `map` can be generated.Learn - more about hosted modules at TODO. - - ──────────────────────────────────────────────────────────────────────────────── - - 1 error and 0 warnings found in ms."# - ), - ); - } - - #[test] - fn format_check_good() { - check_format_check_as_expected(&fixture_file("format", "Formatted.roc"), true); - } - - #[test] - fn format_check_reformatting_needed() { - check_format_check_as_expected(&fixture_file("format", "NotFormatted.roc"), false); - } - - #[test] - fn format_check_folders() { - // This fails, because "NotFormatted.roc" is present in this folder - check_format_check_as_expected(&fixtures_dir("format"), false); - - // This doesn't fail, since only "Formatted.roc" and non-roc files are present in this folder - check_format_check_as_expected(&fixtures_dir("format/formatted_directory"), true); - } -} - -#[allow(dead_code)] -fn run_with_wasmer(wasm_path: &std::path::Path, stdin: &[&str]) -> String { - use std::io::Write; - use wasmer::{Instance, Module, Store}; - - // std::process::Command::new("cp") - // .args(&[ - // wasm_path.to_str().unwrap(), - // "/home/folkertdev/roc/wasm/nqueens.wasm", - // ]) - // .output() - // .unwrap(); - - let store = Store::default(); - let module = Module::from_file(&store, &wasm_path).unwrap(); - - let mut fake_stdin = wasmer_wasi::Pipe::new(); - let fake_stdout = wasmer_wasi::Pipe::new(); - let fake_stderr = wasmer_wasi::Pipe::new(); - - for line in stdin { - write!(fake_stdin, "{}", line).unwrap(); - } - - // First, we create the `WasiEnv` - use wasmer_wasi::WasiState; - let mut wasi_env = WasiState::new("hello") - .stdin(Box::new(fake_stdin)) - .stdout(Box::new(fake_stdout)) - .stderr(Box::new(fake_stderr)) - .finalize() - .unwrap(); - - // Then, we get the import object related to our WASI - // and attach it to the Wasm instance. - let import_object = wasi_env - .import_object(&module) - .unwrap_or_else(|_| wasmer::imports!()); - - let instance = Instance::new(&module, &import_object).unwrap(); - - let start = instance.exports.get_function("_start").unwrap(); - - match start.call(&[]) { - Ok(_) => read_wasi_stdout(wasi_env), - Err(e) => { - use wasmer_wasi::WasiError; - match e.downcast::() { - Ok(WasiError::Exit(0)) => { - // we run the `_start` function, so exit(0) is expected - read_wasi_stdout(wasi_env) - } - other => format!("Something went wrong running a wasm test: {:?}", other), - } - } - } -} - -#[allow(dead_code)] -fn read_wasi_stdout(wasi_env: wasmer_wasi::WasiEnv) -> String { - let mut state = wasi_env.state.lock().unwrap(); - - match state.fs.stdout_mut() { - Ok(Some(stdout)) => { - let mut buf = String::new(); - stdout.read_to_string(&mut buf).unwrap(); - - buf - } - _ => todo!(), - } -} diff --git a/cli/tests/fixtures/.gitignore b/cli/tests/fixtures/.gitignore deleted file mode 100644 index e80387874e..0000000000 --- a/cli/tests/fixtures/.gitignore +++ /dev/null @@ -1,7 +0,0 @@ -app -*.o -*.dSYM -dynhost -libapp.so -metadata -preprocessedhost diff --git a/cli/tests/fixtures/format/Formatted.roc b/cli/tests/fixtures/format/Formatted.roc deleted file mode 100644 index a338ddc5b6..0000000000 --- a/cli/tests/fixtures/format/Formatted.roc +++ /dev/null @@ -1,6 +0,0 @@ -app "formatted" - packages { pf: "platform" } imports [] - provides [main] to pf - -main : Str -main = Dep1.value1 {} diff --git a/cli/tests/fixtures/format/NotFormatted.roc b/cli/tests/fixtures/format/NotFormatted.roc deleted file mode 100644 index 62c4e91503..0000000000 --- a/cli/tests/fixtures/format/NotFormatted.roc +++ /dev/null @@ -1,6 +0,0 @@ -app "formatted" - packages { pf: "platform" } - provides [main] to pf - -main : Str -main = Dep1.value1 {} diff --git a/cli/tests/fixtures/format/formatted_directory/Formatted.roc b/cli/tests/fixtures/format/formatted_directory/Formatted.roc deleted file mode 100644 index a338ddc5b6..0000000000 --- a/cli/tests/fixtures/format/formatted_directory/Formatted.roc +++ /dev/null @@ -1,6 +0,0 @@ -app "formatted" - packages { pf: "platform" } imports [] - provides [main] to pf - -main : Str -main = Dep1.value1 {} diff --git a/cli/tests/fixtures/multi-dep-str/.gitignore b/cli/tests/fixtures/multi-dep-str/.gitignore deleted file mode 100644 index 0d35ca1e7a..0000000000 --- a/cli/tests/fixtures/multi-dep-str/.gitignore +++ /dev/null @@ -1 +0,0 @@ -multi-dep-str diff --git a/cli/tests/fixtures/multi-dep-str/Main.roc b/cli/tests/fixtures/multi-dep-str/Main.roc deleted file mode 100644 index b603cf16c4..0000000000 --- a/cli/tests/fixtures/multi-dep-str/Main.roc +++ /dev/null @@ -1,7 +0,0 @@ -app "multi-dep-str" - packages { pf: "platform" } - imports [Dep1] - provides [main] to pf - -main : Str -main = Dep1.str1 diff --git a/cli/tests/fixtures/multi-dep-str/platform/host.zig b/cli/tests/fixtures/multi-dep-str/platform/host.zig deleted file mode 100644 index d1e0e912fa..0000000000 --- a/cli/tests/fixtures/multi-dep-str/platform/host.zig +++ /dev/null @@ -1,100 +0,0 @@ -const std = @import("std"); -const builtin = @import("builtin"); -const str = @import("str"); -const RocStr = str.RocStr; -const testing = std.testing; -const expectEqual = testing.expectEqual; -const expect = testing.expect; - -comptime { - // This is a workaround for https://github.com/ziglang/zig/issues/8218 - // which is only necessary on macOS. - // - // Once that issue is fixed, we can undo the changes in - // 177cf12e0555147faa4d436e52fc15175c2c4ff0 and go back to passing - // -fcompiler-rt in link.rs instead of doing this. Note that this - // workaround is present in many host.zig files, so make sure to undo - // it everywhere! - if (builtin.os.tag == .macos) { - _ = @import("compiler_rt"); - } -} - -const mem = std.mem; -const Allocator = mem.Allocator; - -extern fn roc__mainForHost_1_exposed_generic(*RocStr) void; - -const Align = 2 * @alignOf(usize); -extern fn malloc(size: usize) callconv(.C) ?*align(Align) anyopaque; -extern fn realloc(c_ptr: [*]align(Align) u8, size: usize) callconv(.C) ?*anyopaque; -extern fn free(c_ptr: [*]align(Align) u8) callconv(.C) void; -extern fn memcpy(dst: [*]u8, src: [*]u8, size: usize) callconv(.C) void; -extern fn memset(dst: [*]u8, value: i32, size: usize) callconv(.C) void; - -export fn roc_alloc(size: usize, alignment: u32) callconv(.C) ?*anyopaque { - _ = alignment; - return malloc(size); -} - -export fn roc_realloc(c_ptr: *anyopaque, new_size: usize, old_size: usize, alignment: u32) callconv(.C) ?*anyopaque { - _ = old_size; - _ = alignment; - return realloc(@alignCast(16, @ptrCast([*]u8, c_ptr)), new_size); -} - -export fn roc_dealloc(c_ptr: *anyopaque, alignment: u32) callconv(.C) void { - _ = alignment; - free(@alignCast(16, @ptrCast([*]u8, c_ptr))); -} - -export fn roc_memcpy(dst: [*]u8, src: [*]u8, size: usize) callconv(.C) void { - return memcpy(dst, src, size); -} - -export fn roc_memset(dst: [*]u8, value: i32, size: usize) callconv(.C) void { - return memset(dst, value, size); -} - -export fn roc_panic(c_ptr: *anyopaque, tag_id: u32) callconv(.C) void { - _ = tag_id; - - const stderr = std.io.getStdErr().writer(); - const msg = @ptrCast([*:0]const u8, c_ptr); - stderr.print("Application crashed with message\n\n {s}\n\nShutting down\n", .{msg}) catch unreachable; - std.process.exit(0); -} - -const Unit = extern struct {}; - -pub export fn main() i32 { - const stdout = std.io.getStdOut().writer(); - const stderr = std.io.getStdErr().writer(); - - // start time - var ts1: std.os.timespec = undefined; - std.os.clock_gettime(std.os.CLOCK.REALTIME, &ts1) catch unreachable; - - // actually call roc to populate the callresult - var callresult = RocStr.empty(); - roc__mainForHost_1_exposed_generic(&callresult); - - // end time - var ts2: std.os.timespec = undefined; - std.os.clock_gettime(std.os.CLOCK.REALTIME, &ts2) catch unreachable; - - // stdout the result - stdout.print("{s}\n", .{callresult.asSlice()}) catch unreachable; - - callresult.deinit(); - - const delta = to_seconds(ts2) - to_seconds(ts1); - - stderr.print("runtime: {d:.3}ms\n", .{delta * 1000}) catch unreachable; - - return 0; -} - -fn to_seconds(tms: std.os.timespec) f64 { - return @intToFloat(f64, tms.tv_sec) + (@intToFloat(f64, tms.tv_nsec) / 1_000_000_000.0); -} diff --git a/cli/tests/fixtures/multi-dep-thunk/.gitignore b/cli/tests/fixtures/multi-dep-thunk/.gitignore deleted file mode 100644 index 2ffad1b586..0000000000 --- a/cli/tests/fixtures/multi-dep-thunk/.gitignore +++ /dev/null @@ -1 +0,0 @@ -multi-dep-thunk diff --git a/cli/tests/fixtures/multi-dep-thunk/Main.roc b/cli/tests/fixtures/multi-dep-thunk/Main.roc deleted file mode 100644 index 2c3d37051a..0000000000 --- a/cli/tests/fixtures/multi-dep-thunk/Main.roc +++ /dev/null @@ -1,7 +0,0 @@ -app "multi-dep-thunk" - packages { pf: "platform" } - imports [Dep1] - provides [main] to pf - -main : Str -main = Dep1.value1 {} diff --git a/cli/tests/fixtures/multi-dep-thunk/platform/host.zig b/cli/tests/fixtures/multi-dep-thunk/platform/host.zig deleted file mode 100644 index 1bf51a90e9..0000000000 --- a/cli/tests/fixtures/multi-dep-thunk/platform/host.zig +++ /dev/null @@ -1,99 +0,0 @@ -const std = @import("std"); -const builtin = @import("builtin"); -const str = @import("str"); -const RocStr = str.RocStr; -const testing = std.testing; -const expectEqual = testing.expectEqual; -const expect = testing.expect; - -comptime { - // This is a workaround for https://github.com/ziglang/zig/issues/8218 - // which is only necessary on macOS. - // - // Once that issue is fixed, we can undo the changes in - // 177cf12e0555147faa4d436e52fc15175c2c4ff0 and go back to passing - // -fcompiler-rt in link.rs instead of doing this. Note that this - // workaround is present in many host.zig files, so make sure to undo - // it everywhere! - if (builtin.os.tag == .macos) { - _ = @import("compiler_rt"); - } -} - -const mem = std.mem; -const Allocator = mem.Allocator; - -extern fn roc__mainForHost_1_exposed_generic(*RocStr) void; - -extern fn malloc(size: usize) callconv(.C) ?*anyopaque; -extern fn realloc(c_ptr: [*]align(@alignOf(u128)) u8, size: usize) callconv(.C) ?*anyopaque; -extern fn free(c_ptr: [*]align(@alignOf(u128)) u8) callconv(.C) void; -extern fn memcpy(dst: [*]u8, src: [*]u8, size: usize) callconv(.C) void; -extern fn memset(dst: [*]u8, value: i32, size: usize) callconv(.C) void; - -export fn roc_alloc(size: usize, alignment: u32) callconv(.C) ?*anyopaque { - _ = alignment; - return malloc(size); -} - -export fn roc_realloc(c_ptr: *anyopaque, new_size: usize, old_size: usize, alignment: u32) callconv(.C) ?*anyopaque { - _ = old_size; - _ = alignment; - return realloc(@alignCast(16, @ptrCast([*]u8, c_ptr)), new_size); -} - -export fn roc_dealloc(c_ptr: *anyopaque, alignment: u32) callconv(.C) void { - _ = alignment; - free(@alignCast(16, @ptrCast([*]u8, c_ptr))); -} - -export fn roc_memcpy(dst: [*]u8, src: [*]u8, size: usize) callconv(.C) void { - return memcpy(dst, src, size); -} - -export fn roc_memset(dst: [*]u8, value: i32, size: usize) callconv(.C) void { - return memset(dst, value, size); -} - -export fn roc_panic(c_ptr: *anyopaque, tag_id: u32) callconv(.C) void { - _ = tag_id; - - const stderr = std.io.getStdErr().writer(); - const msg = @ptrCast([*:0]const u8, c_ptr); - stderr.print("Application crashed with message\n\n {s}\n\nShutting down\n", .{msg}) catch unreachable; - std.process.exit(0); -} - -const Unit = extern struct {}; - -pub export fn main() i32 { - const stdout = std.io.getStdOut().writer(); - const stderr = std.io.getStdErr().writer(); - - // start time - var ts1: std.os.timespec = undefined; - std.os.clock_gettime(std.os.CLOCK.REALTIME, &ts1) catch unreachable; - - // actually call roc to populate the callresult - var callresult = RocStr.empty(); - roc__mainForHost_1_exposed_generic(&callresult); - - // end time - var ts2: std.os.timespec = undefined; - std.os.clock_gettime(std.os.CLOCK.REALTIME, &ts2) catch unreachable; - - // stdout the result - stdout.print("{s}\n", .{callresult.asSlice()}) catch unreachable; - - callresult.deinit(); - - const delta = to_seconds(ts2) - to_seconds(ts1); - - stderr.print("runtime: {d:.3}ms\n", .{delta * 1000}) catch unreachable; - - return 0; -} - -fn to_seconds(tms: std.os.timespec) f64 { - return @intToFloat(f64, tms.tv_sec) + (@intToFloat(f64, tms.tv_nsec) / 1_000_000_000.0); -} diff --git a/cli/tests/known_bad/ExposedNotDefined.roc b/cli/tests/known_bad/ExposedNotDefined.roc deleted file mode 100644 index d2b3dee855..0000000000 --- a/cli/tests/known_bad/ExposedNotDefined.roc +++ /dev/null @@ -1,3 +0,0 @@ -interface Foo - exposes [bar] - imports [] diff --git a/cli/tests/known_bad/TypeError.roc b/cli/tests/known_bad/TypeError.roc deleted file mode 100644 index 6bcafc46f3..0000000000 --- a/cli/tests/known_bad/TypeError.roc +++ /dev/null @@ -1,11 +0,0 @@ -app "type-error" - packages { pf: "platform" } - imports [pf.Stdout.{ line }, pf.Task.{ await }] - provides [main] to pf - -main = - _ <- await (line "a") - _ <- await (line "b") - _ <- await (line "c") - _ <- await (line d) - line "e" diff --git a/cli/tests/known_bad/platform b/cli/tests/known_bad/platform deleted file mode 120000 index b58068a18f..0000000000 --- a/cli/tests/known_bad/platform +++ /dev/null @@ -1 +0,0 @@ -../../../examples/interactive/cli-platform \ No newline at end of file diff --git a/cli_utils/Cargo.lock b/cli_utils/Cargo.lock deleted file mode 100644 index 07b8777f33..0000000000 --- a/cli_utils/Cargo.lock +++ /dev/null @@ -1,4301 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 3 - -[[package]] -name = "ab_glyph" -version = "0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20b228f2c198f98d4337ceb560333fb12cbb2f4948a953bf8c57d09deb219603" -dependencies = [ - "ab_glyph_rasterizer", - "owned_ttf_parser 0.13.2", -] - -[[package]] -name = "ab_glyph_rasterizer" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a13739d7177fbd22bb0ed28badfff9f372f8bef46c863db4e1c6248f6b223b6e" - -[[package]] -name = "addr2line" -version = "0.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9ecd88a8c8378ca913a680cd98f0f13ac67383d35993f86c90a70e3f137816b" -dependencies = [ - "gimli", -] - -[[package]] -name = "adler" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" - -[[package]] -name = "ahash" -version = "0.4.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "739f4a8db6605981345c5654f3a85b056ce52f37a39d34da03f25bf2151ea16e" - -[[package]] -name = "ahash" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" -dependencies = [ - "getrandom", - "once_cell", - "version_check", -] - -[[package]] -name = "aho-corasick" -version = "0.7.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" -dependencies = [ - "memchr", -] - -[[package]] -name = "andrew" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c4afb09dd642feec8408e33f92f3ffc4052946f6b20f32fb99c1f58cd4fa7cf" -dependencies = [ - "bitflags", - "rusttype", - "walkdir", - "xdg", - "xml-rs", -] - -[[package]] -name = "approx" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f2a05fd1bd10b2527e20a2cd32d8873d115b8b39fe219ee25f42a8aca6ba278" -dependencies = [ - "num-traits", -] - -[[package]] -name = "approx" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "072df7202e63b127ab55acfe16ce97013d5b97bf160489336d3f1840fd78e99e" -dependencies = [ - "num-traits", -] - -[[package]] -name = "arrayvec" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" - -[[package]] -name = "arrayvec" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6" - -[[package]] -name = "ash" -version = "0.33.3+1.2.191" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc4f1d82f164f838ae413296d1131aa6fa79b917d25bebaa7033d25620c09219" -dependencies = [ - "libloading 0.7.1", -] - -[[package]] -name = "atty" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" -dependencies = [ - "hermit-abi", - "libc", - "winapi", -] - -[[package]] -name = "autocfg" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" - -[[package]] -name = "backtrace" -version = "0.3.63" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "321629d8ba6513061f26707241fa9bc89524ff1cd7a915a97ef0c62c666ce1b6" -dependencies = [ - "addr2line", - "cc", - "cfg-if 1.0.0", - "libc", - "miniz_oxide", - "object 0.27.1", - "rustc-demangle", -] - -[[package]] -name = "bincode" -version = "1.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" -dependencies = [ - "serde", -] - -[[package]] -name = "bit-set" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e11e16035ea35e4e5997b393eacbf6f63983188f7a2ad25bfb13465f5ad59de" -dependencies = [ - "bit-vec", -] - -[[package]] -name = "bit-vec" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" - -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "bitmaps" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "031043d04099746d8db04daf1fa424b2bc8bd69d92b25962dcde24da39ab64a2" -dependencies = [ - "typenum", -] - -[[package]] -name = "bitvec" -version = "0.22.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5237f00a8c86130a0cc317830e558b966dd7850d48a953d998c813f01a41b527" -dependencies = [ - "funty 1.2.0", - "radium 0.6.2", - "tap", - "wyz 0.4.0", -] - -[[package]] -name = "bitvec" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1489fcb93a5bb47da0462ca93ad252ad6af2145cce58d10d46a83931ba9f016b" -dependencies = [ - "funty 2.0.0", - "radium 0.7.0", - "tap", - "wyz 0.5.0", -] - -[[package]] -name = "block" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" - -[[package]] -name = "block-buffer" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0940dc441f31689269e10ac70eb1002a3a1d3ad1390e030043662eb7fe4688b" -dependencies = [ - "block-padding", - "byte-tools", - "byteorder", - "generic-array 0.12.4", -] - -[[package]] -name = "block-buffer" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" -dependencies = [ - "generic-array 0.14.4", -] - -[[package]] -name = "block-padding" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa79dedbb091f449f1f39e53edf88d5dbe95f895dae6135a8d7b881fb5af73f5" -dependencies = [ - "byte-tools", -] - -[[package]] -name = "bstr" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223" -dependencies = [ - "lazy_static", - "memchr", - "regex-automata", - "serde", -] - -[[package]] -name = "bumpalo" -version = "3.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f1e260c3a9040a7c19a12468758f4c16f31a81a1fe087482be9570ec864bb6c" - -[[package]] -name = "byte-tools" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7" - -[[package]] -name = "bytemuck" -version = "1.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72957246c41db82b8ef88a5486143830adeb8227ef9837740bdec67724cf2c5b" -dependencies = [ - "bytemuck_derive", -] - -[[package]] -name = "bytemuck_derive" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e215f8c2f9f79cb53c8335e687ffd07d5bfcb6fe5fc80723762d0be46e7cc54" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "byteorder" -version = "1.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" - -[[package]] -name = "calloop" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b036167e76041694579972c28cf4877b4f92da222560ddb49008937b6a6727c" -dependencies = [ - "log", - "nix 0.18.0", -] - -[[package]] -name = "cast" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c24dab4283a142afa2fdca129b80ad2c6284e073930f964c3a1293c225ee39a" -dependencies = [ - "rustc_version", -] - -[[package]] -name = "cc" -version = "1.0.72" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22a9137b95ea06864e018375b72adfb7db6e6f68cfc8df5a04d00288050485ee" - -[[package]] -name = "cfg-if" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" - -[[package]] -name = "cfg-if" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" - -[[package]] -name = "cfg_aliases" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" - -[[package]] -name = "cgmath" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a98d30140e3296250832bbaaff83b27dcd6fa3cc70fb6f1f3e5c9c0023b5317" -dependencies = [ - "approx 0.4.0", - "num-traits", -] - -[[package]] -name = "clap" -version = "2.33.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002" -dependencies = [ - "bitflags", - "textwrap 0.11.0", - "unicode-width", -] - -[[package]] -name = "clap" -version = "3.1.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47582c09be7c8b32c0ab3a6181825ababb713fde6fff20fc573a3870dd45c6a0" -dependencies = [ - "atty", - "bitflags", - "clap_lex", - "indexmap", - "strsim 0.10.0", - "termcolor", - "textwrap 0.15.0", -] - -[[package]] -name = "clap_lex" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a37c35f1112dad5e6e0b1adaff798507497a18fceeb30cceb3bae7d1427b9213" -dependencies = [ - "os_str_bytes", -] - -[[package]] -name = "cli_utils" -version = "0.1.0" -dependencies = [ - "bumpalo", - "criterion", - "rlimit", - "roc_cli", - "roc_collections", - "roc_load", - "roc_module", - "serde", - "serde-xml-rs", - "strip-ansi-escapes", - "tempfile", -] - -[[package]] -name = "clipboard-win" -version = "3.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fdf5e01086b6be750428ba4a40619f847eb2e95756eee84b18e06e5f0b50342" -dependencies = [ - "lazy-bytes-cast", - "winapi", -] - -[[package]] -name = "clipboard-win" -version = "4.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3db8340083d28acb43451166543b98c838299b7e0863621be53a338adceea0ed" -dependencies = [ - "error-code", - "str-buf", - "winapi", -] - -[[package]] -name = "cocoa" -version = "0.24.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f63902e9223530efb4e26ccd0cf55ec30d592d3b42e21a28defc42a9586e832" -dependencies = [ - "bitflags", - "block", - "cocoa-foundation", - "core-foundation 0.9.2", - "core-graphics 0.22.3", - "foreign-types", - "libc", - "objc", -] - -[[package]] -name = "cocoa-foundation" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ade49b65d560ca58c403a479bb396592b155c0185eada742ee323d1d68d6318" -dependencies = [ - "bitflags", - "block", - "core-foundation 0.9.2", - "core-graphics-types", - "foreign-types", - "libc", - "objc", -] - -[[package]] -name = "codespan-reporting" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" -dependencies = [ - "termcolor", - "unicode-width", -] - -[[package]] -name = "colored" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3616f750b84d8f0de8a58bda93e08e2a81ad3f523089b05f1dffecab48c6cbd" -dependencies = [ - "atty", - "lazy_static", - "winapi", -] - -[[package]] -name = "confy" -version = "0.4.0" -source = "git+https://github.com/rust-cli/confy#664992aecd97b4af0eda8d9d2825885662e1c6b4" -dependencies = [ - "directories-next", - "serde", - "serde_yaml", -] - -[[package]] -name = "const_format" -version = "0.2.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22bc6cd49b0ec407b680c3e380182b6ac63b73991cb7602de350352fc309b614" -dependencies = [ - "const_format_proc_macros", -] - -[[package]] -name = "const_format_proc_macros" -version = "0.2.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef196d5d972878a48da7decb7686eded338b4858fbabeed513d63a7c98b2b82d" -dependencies = [ - "proc-macro2", - "quote", - "unicode-xid", -] - -[[package]] -name = "copyless" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2df960f5d869b2dd8532793fde43eb5427cceb126c929747a26823ab0eeb536" - -[[package]] -name = "copypasta" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4423d79fed83ebd9ab81ec21fa97144300a961782158287dc9bf7eddac37ff0b" -dependencies = [ - "clipboard-win 3.1.1", - "objc", - "objc-foundation", - "objc_id", - "smithay-clipboard", - "x11-clipboard", -] - -[[package]] -name = "core-foundation" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57d24c7a13c43e870e37c1556b74555437870a04514f7685f5b354e090567171" -dependencies = [ - "core-foundation-sys 0.7.0", - "libc", -] - -[[package]] -name = "core-foundation" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6888e10551bb93e424d8df1d07f1a8b4fceb0001a3a4b048bfc47554946f47b3" -dependencies = [ - "core-foundation-sys 0.8.3", - "libc", -] - -[[package]] -name = "core-foundation-sys" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3a71ab494c0b5b860bdc8407ae08978052417070c2ced38573a9157ad75b8ac" - -[[package]] -name = "core-foundation-sys" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" - -[[package]] -name = "core-graphics" -version = "0.19.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3889374e6ea6ab25dba90bb5d96202f61108058361f6dc72e8b03e6f8bbe923" -dependencies = [ - "bitflags", - "core-foundation 0.7.0", - "foreign-types", - "libc", -] - -[[package]] -name = "core-graphics" -version = "0.22.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2581bbab3b8ffc6fcbd550bf46c355135d16e9ff2a6ea032ad6b9bf1d7efe4fb" -dependencies = [ - "bitflags", - "core-foundation 0.9.2", - "core-graphics-types", - "foreign-types", - "libc", -] - -[[package]] -name = "core-graphics-types" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a68b68b3446082644c91ac778bf50cd4104bfb002b5a6a7c44cca5a2c70788b" -dependencies = [ - "bitflags", - "core-foundation 0.9.2", - "foreign-types", - "libc", -] - -[[package]] -name = "core-video-sys" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34ecad23610ad9757664d644e369246edde1803fcb43ed72876565098a5d3828" -dependencies = [ - "cfg-if 0.1.10", - "core-foundation-sys 0.7.0", - "core-graphics 0.19.2", - "libc", - "objc", -] - -[[package]] -name = "cpufeatures" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95059428f66df56b63431fdb4e1947ed2190586af5c5a8a8b71122bdf5a7f469" -dependencies = [ - "libc", -] - -[[package]] -name = "crc32fast" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81156fece84ab6a9f2afdb109ce3ae577e42b1228441eded99bd77f627953b1a" -dependencies = [ - "cfg-if 1.0.0", -] - -[[package]] -name = "criterion" -version = "0.3.5" -source = "git+https://github.com/Anton-4/criterion.rs#3e46ad2b234e36928fb5234d36cf53b5837cbb87" -dependencies = [ - "atty", - "cast", - "clap 2.33.3", - "criterion-plot", - "csv", - "itertools 0.10.1", - "lazy_static", - "num-traits", - "oorandom", - "plotters", - "rayon", - "regex", - "serde", - "serde_cbor", - "serde_derive", - "serde_json", - "tinytemplate", - "walkdir", -] - -[[package]] -name = "criterion-plot" -version = "0.4.3" -source = "git+https://github.com/Anton-4/criterion.rs#3e46ad2b234e36928fb5234d36cf53b5837cbb87" -dependencies = [ - "cast", - "itertools 0.9.0", -] - -[[package]] -name = "crossbeam" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ae5588f6b3c3cb05239e90bd110f257254aecd01e4635400391aeae07497845" -dependencies = [ - "cfg-if 1.0.0", - "crossbeam-channel", - "crossbeam-deque", - "crossbeam-epoch", - "crossbeam-queue", - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-channel" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06ed27e177f16d65f0f0c22a213e17c696ace5dd64b14258b52f9417ccb52db4" -dependencies = [ - "cfg-if 1.0.0", - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-deque" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6455c0ca19f0d2fbf751b908d5c55c1f5cbc65e03c4225427254b46890bdde1e" -dependencies = [ - "cfg-if 1.0.0", - "crossbeam-epoch", - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-epoch" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ec02e091aa634e2c3ada4a392989e7c3116673ef0ac5b72232439094d73b7fd" -dependencies = [ - "cfg-if 1.0.0", - "crossbeam-utils", - "lazy_static", - "memoffset", - "scopeguard", -] - -[[package]] -name = "crossbeam-queue" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b10ddc024425c88c2ad148c1b0fd53f4c6d38db9697c9f1588381212fa657c9" -dependencies = [ - "cfg-if 1.0.0", - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-utils" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d82cfc11ce7f2c3faef78d8a684447b40d503d9681acebed6cb728d45940c4db" -dependencies = [ - "cfg-if 1.0.0", - "lazy_static", -] - -[[package]] -name = "csv" -version = "1.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22813a6dc45b335f9bade10bf7271dc477e81113e89eb251a0bc2a8a81c536e1" -dependencies = [ - "bstr", - "csv-core", - "itoa", - "ryu", - "serde", -] - -[[package]] -name = "csv-core" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b2466559f260f48ad25fe6317b3c8dac77b5bdb5763ac7d9d6103530663bc90" -dependencies = [ - "memchr", -] - -[[package]] -name = "d3d12" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2daefd788d1e96e0a9d66dee4b828b883509bc3ea9ce30665f04c3246372690c" -dependencies = [ - "bitflags", - "libloading 0.7.1", - "winapi", -] - -[[package]] -name = "darling" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d706e75d87e35569db781a9b5e2416cff1236a47ed380831f959382ccd5f858" -dependencies = [ - "darling_core", - "darling_macro", -] - -[[package]] -name = "darling_core" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0c960ae2da4de88a91b2d920c2a7233b400bc33cb28453a2987822d8392519b" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim 0.9.3", - "syn", -] - -[[package]] -name = "darling_macro" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b5a2f4ac4969822c62224815d069952656cadc7084fdca9751e6d959189b72" -dependencies = [ - "darling_core", - "quote", - "syn", -] - -[[package]] -name = "derivative" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "digest" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5" -dependencies = [ - "generic-array 0.12.4", -] - -[[package]] -name = "digest" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" -dependencies = [ - "generic-array 0.14.4", -] - -[[package]] -name = "directories-next" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "339ee130d97a610ea5a5872d2bbb130fdf68884ff09d3028b81bec8a1ac23bbc" -dependencies = [ - "cfg-if 1.0.0", - "dirs-sys-next", -] - -[[package]] -name = "dirs" -version = "3.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30baa043103c9d0c2a57cf537cc2f35623889dc0d405e6c3cccfadbc81c71309" -dependencies = [ - "dirs-sys", -] - -[[package]] -name = "dirs-next" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" -dependencies = [ - "cfg-if 1.0.0", - "dirs-sys-next", -] - -[[package]] -name = "dirs-sys" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03d86534ed367a67548dc68113a0f5db55432fdfbb6e6f9d77704397d95d5780" -dependencies = [ - "libc", - "redox_users", - "winapi", -] - -[[package]] -name = "dirs-sys-next" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" -dependencies = [ - "libc", - "redox_users", - "winapi", -] - -[[package]] -name = "dispatch" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" - -[[package]] -name = "distance" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d9d8664cf849d7d0f3114a3a387d2f5e4303176d746d5a951aaddc66dfe9240" - -[[package]] -name = "dlib" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b11f15d1e3268f140f68d390637d5e76d849782d971ae7063e0da69fe9709a76" -dependencies = [ - "libloading 0.6.7", -] - -[[package]] -name = "dlib" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac1b7517328c04c2aa68422fc60a41b92208182142ed04a25879c26c8f878794" -dependencies = [ - "libloading 0.7.1", -] - -[[package]] -name = "doc-comment" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" - -[[package]] -name = "downcast-rs" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650" - -[[package]] -name = "dtoa" -version = "0.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56899898ce76aaf4a0f24d914c97ea6ed976d42fec6ad33fcbb0a1103e07b2b0" - -[[package]] -name = "dunce" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "453440c271cf5577fd2a40e4942540cb7d0d2f85e27c8d07dd0023c925a67541" - -[[package]] -name = "either" -version = "1.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" - -[[package]] -name = "encode_unicode" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" - -[[package]] -name = "endian-type" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d" - -[[package]] -name = "env_logger" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b2cf0344971ee6c64c31be0d530793fba457d322dfec2810c453d0ef228f9c3" -dependencies = [ - "atty", - "humantime", - "log", - "regex", - "termcolor", -] - -[[package]] -name = "error-code" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5115567ac25674e0043e472be13d14e537f37ea8aa4bdc4aef0c89add1db1ff" -dependencies = [ - "libc", - "str-buf", -] - -[[package]] -name = "fake-simd" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed" - -[[package]] -name = "fd-lock" -version = "3.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a16910e685088843d53132b04e0f10a571fdb193224fc589685b3ba1ce4cb03d" -dependencies = [ - "cfg-if 1.0.0", - "libc", - "windows-sys 0.28.0", -] - -[[package]] -name = "find-crate" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59a98bbaacea1c0eb6a0876280051b892eb73594fd90cf3b20e9c817029c57d2" -dependencies = [ - "toml", -] - -[[package]] -name = "flate2" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e6988e897c1c9c485f43b47a529cef42fde0547f9d8d41a7062518f1d8fc53f" -dependencies = [ - "cfg-if 1.0.0", - "crc32fast", - "libc", - "miniz_oxide", -] - -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - -[[package]] -name = "foreign-types" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" -dependencies = [ - "foreign-types-shared", -] - -[[package]] -name = "foreign-types-shared" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" - -[[package]] -name = "fs_extra" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2022715d62ab30faffd124d40b76f4134a550a87792276512b18d63272333394" - -[[package]] -name = "funty" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1847abb9cb65d566acd5942e94aea9c8f547ad02c98e1649326fc0e8910b8b1e" - -[[package]] -name = "funty" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" - -[[package]] -name = "futures" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a12aa0eb539080d55c3f2d45a67c3b58b6b0773c1a3ca2dfec66d58c97fd66ca" -dependencies = [ - "futures-channel", - "futures-core", - "futures-executor", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-channel" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5da6ba8c3bb3c165d3c7319fc1cc8304facf1fb8db99c5de877183c08a273888" -dependencies = [ - "futures-core", - "futures-sink", -] - -[[package]] -name = "futures-core" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88d1c26957f23603395cd326b0ffe64124b818f4449552f960d815cfba83a53d" - -[[package]] -name = "futures-executor" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45025be030969d763025784f7f355043dc6bc74093e4ecc5000ca4dc50d8745c" -dependencies = [ - "futures-core", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-io" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "522de2a0fe3e380f1bc577ba0474108faf3f6b18321dbf60b3b9c39a75073377" - -[[package]] -name = "futures-macro" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18e4a4b95cea4b4ccbcf1c5675ca7c4ee4e9e75eb79944d07defde18068f79bb" -dependencies = [ - "autocfg", - "proc-macro-hack", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "futures-sink" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36ea153c13024fe480590b3e3d4cad89a0cfacecc24577b68f86c6ced9c2bc11" - -[[package]] -name = "futures-task" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d3d00f4eddb73e498a54394f228cd55853bdf059259e8e7bc6e69d408892e99" - -[[package]] -name = "futures-util" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36568465210a3a6ee45e1f165136d68671471a501e632e9a98d96872222b5481" -dependencies = [ - "autocfg", - "futures-channel", - "futures-core", - "futures-io", - "futures-macro", - "futures-sink", - "futures-task", - "memchr", - "pin-project-lite", - "pin-utils", - "proc-macro-hack", - "proc-macro-nested", - "slab", -] - -[[package]] -name = "fxhash" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" -dependencies = [ - "byteorder", -] - -[[package]] -name = "generic-array" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffdf9f34f1447443d37393cc6c2b8313aebddcd96906caf34e54c68d8e57d7bd" -dependencies = [ - "typenum", -] - -[[package]] -name = "generic-array" -version = "0.14.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "501466ecc8a30d1d3b7fc9229b122b2ce8ed6e9d9223f1138d4babb253e51817" -dependencies = [ - "typenum", - "version_check", -] - -[[package]] -name = "getrandom" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753" -dependencies = [ - "cfg-if 1.0.0", - "libc", - "wasi", -] - -[[package]] -name = "gimli" -version = "0.26.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78cc372d058dcf6d5ecd98510e7fbc9e5aec4d21de70f65fea8fecebcd881bd4" - -[[package]] -name = "glow" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f04649123493bc2483cbef4daddb45d40bbdae5adb221a63a23efdb0cc99520" -dependencies = [ - "js-sys", - "slotmap", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "glyph_brush" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21932fbf719272848eec4583740d978203c6e7da4c4e203358f5b95946c97409" -dependencies = [ - "glyph_brush_draw_cache", - "glyph_brush_layout", - "log", - "ordered-float", - "rustc-hash", - "twox-hash", -] - -[[package]] -name = "glyph_brush_draw_cache" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6010675390f6889e09a21e2c8b575b3ee25667ea8237a8d59423f73cb8c28610" -dependencies = [ - "ab_glyph", - "crossbeam-channel", - "crossbeam-deque", - "linked-hash-map", - "rayon", - "rustc-hash", -] - -[[package]] -name = "glyph_brush_layout" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc32c2334f00ca5ac3695c5009ae35da21da8c62d255b5b96d56e2597a637a38" -dependencies = [ - "ab_glyph", - "approx 0.5.0", - "xi-unicode", -] - -[[package]] -name = "gpu-alloc" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e64cbb8d36508d3e19da95e56e196a84f674fc190881f2cc010000798838aa6" -dependencies = [ - "bitflags", - "gpu-alloc-types", -] - -[[package]] -name = "gpu-alloc-types" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54804d0d6bc9d7f26db4eaec1ad10def69b599315f487d32c334a80d1efe67a5" -dependencies = [ - "bitflags", -] - -[[package]] -name = "gpu-descriptor" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7a237f0419ab10d17006d55c62ac4f689a6bf52c75d3f38b8361d249e8d4b0b" -dependencies = [ - "bitflags", - "gpu-descriptor-types", - "hashbrown 0.9.1", -] - -[[package]] -name = "gpu-descriptor-types" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "363e3677e55ad168fef68cf9de3a4a310b53124c5e784c53a1d70e92d23f2126" -dependencies = [ - "bitflags", -] - -[[package]] -name = "half" -version = "1.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7" - -[[package]] -name = "hashbrown" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04" -dependencies = [ - "ahash 0.4.7", -] - -[[package]] -name = "hashbrown" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" -dependencies = [ - "ahash 0.7.6", - "bumpalo", -] - -[[package]] -name = "hermit-abi" -version = "0.1.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" -dependencies = [ - "libc", -] - -[[package]] -name = "hexf-parse" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" - -[[package]] -name = "humantime" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" - -[[package]] -name = "iced-x86" -version = "1.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46e977036f7f5139d580c7f19ad62df9cb8ebd8410bb569e73585226be80a86f" -dependencies = [ - "lazy_static", - "static_assertions 1.1.0", -] - -[[package]] -name = "ident_case" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" - -[[package]] -name = "im" -version = "15.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "111c1983f3c5bb72732df25cddacee9b546d08325fb584b5ebd38148be7b0246" -dependencies = [ - "bitmaps", - "rand_core 0.5.1", - "rand_xoshiro", - "sized-chunks", - "typenum", - "version_check", -] - -[[package]] -name = "im-rc" -version = "15.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ca8957e71f04a205cb162508f9326aea04676c8dfd0711220190d6b83664f3f" -dependencies = [ - "bitmaps", - "rand_core 0.5.1", - "rand_xoshiro", - "sized-chunks", - "typenum", - "version_check", -] - -[[package]] -name = "indexmap" -version = "1.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc633605454125dec4b66843673f01c7df2b89479b32e0ed634e43a91cff62a5" -dependencies = [ - "autocfg", - "hashbrown 0.11.2", -] - -[[package]] -name = "inkwell" -version = "0.1.0" -dependencies = [ - "inkwell 0.1.0 (git+https://github.com/rtfeldman/inkwell?branch=master)", -] - -[[package]] -name = "inkwell" -version = "0.1.0" -source = "git+https://github.com/rtfeldman/inkwell?branch=master#accd406858a40ca2a1463ff77d79f3c5e4c96f4e" -dependencies = [ - "either", - "inkwell_internals", - "libc", - "llvm-sys", - "once_cell", - "parking_lot 0.12.0", -] - -[[package]] -name = "inkwell_internals" -version = "0.5.0" -source = "git+https://github.com/rtfeldman/inkwell?branch=master#accd406858a40ca2a1463ff77d79f3c5e4c96f4e" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "inplace_it" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90953f308a79fe6d62a4643e51f848fbfddcd05975a38e69fdf4ab86a7baf7ca" - -[[package]] -name = "instant" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" -dependencies = [ - "cfg-if 1.0.0", - "js-sys", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "itertools" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "284f18f85651fe11e8a991b2adb42cb078325c996ed026d994719efcfca1d54b" -dependencies = [ - "either", -] - -[[package]] -name = "itertools" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69ddb889f9d0d08a67338271fa9b62996bc788c7796a5c18cf057420aaed5eaf" -dependencies = [ - "either", -] - -[[package]] -name = "itoa" -version = "0.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" - -[[package]] -name = "jni-sys" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" - -[[package]] -name = "js-sys" -version = "0.3.55" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cc9ffccd38c451a86bf13657df244e9c3f37493cce8e5e21e940963777acc84" -dependencies = [ - "wasm-bindgen", -] - -[[package]] -name = "khronos-egl" -version = "4.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c2352bd1d0bceb871cb9d40f24360c8133c11d7486b68b5381c1dd1a32015e3" -dependencies = [ - "libc", - "libloading 0.7.1", -] - -[[package]] -name = "lazy-bytes-cast" -version = "5.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10257499f089cd156ad82d0a9cd57d9501fa2c989068992a97eb3c27836f206b" - -[[package]] -name = "lazy_static" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" - -[[package]] -name = "libc" -version = "0.2.107" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbe5e23404da5b4f555ef85ebed98fb4083e55a00c317800bc2a50ede9f3d219" - -[[package]] -name = "libloading" -version = "0.6.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "351a32417a12d5f7e82c368a66781e307834dae04c6ce0cd4456d52989229883" -dependencies = [ - "cfg-if 1.0.0", - "winapi", -] - -[[package]] -name = "libloading" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0cf036d15402bea3c5d4de17b3fce76b3e4a56ebc1f577be0e7a72f7c607cf0" -dependencies = [ - "cfg-if 1.0.0", - "winapi", -] - -[[package]] -name = "libmimalloc-sys" -version = "0.1.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d1b8479c593dba88c2741fc50b92e13dbabbbe0bd504d979f244ccc1a5b1c01" -dependencies = [ - "cc", -] - -[[package]] -name = "linked-hash-map" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3" - -[[package]] -name = "llvm-sys" -version = "130.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95eb03b4f7ae21f48ef7c565a3e3aa22c50616aea64645fb1fd7f6f56b51c274" -dependencies = [ - "cc", - "lazy_static", - "libc", - "regex", - "semver 0.11.0", -] - -[[package]] -name = "lock_api" -version = "0.4.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "327fa5b6a6940e4699ec49a9beae1ea4845c6bab9314e4f84ac68742139d8c53" -dependencies = [ - "autocfg", - "scopeguard", -] - -[[package]] -name = "log" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" -dependencies = [ - "cfg-if 1.0.0", -] - -[[package]] -name = "malloc_buf" -version = "0.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" -dependencies = [ - "libc", -] - -[[package]] -name = "maplit" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" - -[[package]] -name = "memchr" -version = "2.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" - -[[package]] -name = "memmap2" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b70ca2a6103ac8b665dc150b142ef0e4e89df640c9e6cf295d189c3caebe5a" -dependencies = [ - "libc", -] - -[[package]] -name = "memmap2" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b6c2ebff6180198788f5db08d7ce3bc1d0b617176678831a7510825973e357" -dependencies = [ - "libc", -] - -[[package]] -name = "memmap2" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "057a3db23999c867821a7a59feb06a578fcb03685e983dff90daf9e7d24ac08f" -dependencies = [ - "libc", -] - -[[package]] -name = "memoffset" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59accc507f1338036a0477ef61afdae33cde60840f4dfe481319ce3ad116ddf9" -dependencies = [ - "autocfg", -] - -[[package]] -name = "metal" -version = "0.23.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0514f491f4cc03632ab399ee01e2c1c1b12d3e1cf2d667c1ff5f87d6dcd2084" -dependencies = [ - "bitflags", - "block", - "core-graphics-types", - "foreign-types", - "log", - "objc", -] - -[[package]] -name = "mimalloc" -version = "0.1.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb74897ce508e6c49156fd1476fc5922cbc6e75183c65e399c765a09122e5130" -dependencies = [ - "libmimalloc-sys", -] - -[[package]] -name = "minimal-lexical" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" - -[[package]] -name = "miniz_oxide" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a92518e98c078586bc6c934028adcca4c92a53d6a958196de835170a01d84e4b" -dependencies = [ - "adler", - "autocfg", -] - -[[package]] -name = "mio" -version = "0.7.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8067b404fe97c70829f082dec8bcf4f71225d7eaea1d8645349cb76fa06205cc" -dependencies = [ - "libc", - "log", - "miow", - "ntapi", - "winapi", -] - -[[package]] -name = "mio-misc" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ddf05411bb159cdb5801bb10002afb66cb4572be656044315e363460ce69dc2" -dependencies = [ - "crossbeam", - "crossbeam-queue", - "log", - "mio", -] - -[[package]] -name = "miow" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21" -dependencies = [ - "winapi", -] - -[[package]] -name = "morphic_lib" -version = "0.1.0" -dependencies = [ - "sha2", - "smallvec", - "thiserror", - "typed-arena", -] - -[[package]] -name = "naga" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eda66d09f712e1f0a6ab436137da4fac312f78301f6d4ac7cb8bfe96e988734f" -dependencies = [ - "bit-set", - "bitflags", - "codespan-reporting", - "fxhash", - "hexf-parse", - "indexmap", - "log", - "num-traits", - "spirv", - "thiserror", -] - -[[package]] -name = "ndk" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8794322172319b972f528bf90c6b467be0079f1fa82780ffb431088e741a73ab" -dependencies = [ - "jni-sys", - "ndk-sys", - "num_enum", - "thiserror", -] - -[[package]] -name = "ndk-glue" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5caf0c24d51ac1c905c27d4eda4fa0635bbe0de596b8f79235e0b17a4d29385" -dependencies = [ - "lazy_static", - "libc", - "log", - "ndk", - "ndk-macro", - "ndk-sys", -] - -[[package]] -name = "ndk-macro" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05d1c6307dc424d0f65b9b06e94f88248e6305726b14729fd67a5e47b2dc481d" -dependencies = [ - "darling", - "proc-macro-crate 0.1.5", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "ndk-sys" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c44922cb3dbb1c70b5e5f443d63b64363a898564d739ba5198e3a9138442868d" - -[[package]] -name = "nibble_vec" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77a5d83df9f36fe23f0c3648c6bbb8b0298bb5f1939c8f2704431371f4b84d43" -dependencies = [ - "smallvec", -] - -[[package]] -name = "nix" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83450fe6a6142ddd95fb064b746083fc4ef1705fe81f64a64e1d4b39f54a1055" -dependencies = [ - "bitflags", - "cc", - "cfg-if 0.1.10", - "libc", -] - -[[package]] -name = "nix" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa9b4819da1bc61c0ea48b63b7bc8604064dd43013e7cc325df098d49cd7c18a" -dependencies = [ - "bitflags", - "cc", - "cfg-if 1.0.0", - "libc", -] - -[[package]] -name = "nix" -version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf1e25ee6b412c2a1e3fcb6a4499a5c1bfe7f43e014bdce9a6b6666e5aa2d187" -dependencies = [ - "bitflags", - "cc", - "cfg-if 1.0.0", - "libc", - "memoffset", -] - -[[package]] -name = "nix" -version = "0.23.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f866317acbd3a240710c63f065ffb1e4fd466259045ccb504130b7f668f35c6" -dependencies = [ - "bitflags", - "cc", - "cfg-if 1.0.0", - "libc", - "memoffset", -] - -[[package]] -name = "nom" -version = "7.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b1d11e1ef389c76fe5b81bcaf2ea32cf88b62bc494e19f493d0b30e7a930109" -dependencies = [ - "memchr", - "minimal-lexical", - "version_check", -] - -[[package]] -name = "nonempty" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9e591e719385e6ebaeb5ce5d3887f7d5676fceca6411d1925ccc95745f3d6f7" - -[[package]] -name = "ntapi" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f6bb902e437b6d86e03cce10a7e2af662292c5dfef23b65899ea3ac9354ad44" -dependencies = [ - "winapi", -] - -[[package]] -name = "num-traits" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" -dependencies = [ - "autocfg", -] - -[[package]] -name = "num_cpus" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3" -dependencies = [ - "hermit-abi", - "libc", -] - -[[package]] -name = "num_enum" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9bd055fb730c4f8f4f57d45d35cd6b3f0980535b056dc7ff119cee6a66ed6f" -dependencies = [ - "derivative", - "num_enum_derive", -] - -[[package]] -name = "num_enum_derive" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "486ea01961c4a818096de679a8b740b26d9033146ac5291b1c98557658f8cdd9" -dependencies = [ - "proc-macro-crate 1.1.0", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "objc" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" -dependencies = [ - "malloc_buf", - "objc_exception", -] - -[[package]] -name = "objc-foundation" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" -dependencies = [ - "block", - "objc", - "objc_id", -] - -[[package]] -name = "objc_exception" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad970fb455818ad6cba4c122ad012fae53ae8b4795f86378bce65e4f6bab2ca4" -dependencies = [ - "cc", -] - -[[package]] -name = "objc_id" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" -dependencies = [ - "objc", -] - -[[package]] -name = "object" -version = "0.26.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39f37e50073ccad23b6d09bcb5b263f4e76d3bb6038e4a3c08e52162ffa8abc2" -dependencies = [ - "crc32fast", - "flate2", - "indexmap", - "memchr", -] - -[[package]] -name = "object" -version = "0.27.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67ac1d3f9a1d3616fd9a60c8d74296f22406a238b6a72f5cc1e6f314df4ffbf9" -dependencies = [ - "memchr", -] - -[[package]] -name = "once_cell" -version = "1.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56" - -[[package]] -name = "oorandom" -version = "11.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575" - -[[package]] -name = "opaque-debug" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c" - -[[package]] -name = "opaque-debug" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" - -[[package]] -name = "ordered-float" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97c9d06878b3a851e8026ef94bf7fef9ba93062cd412601da4d9cf369b1cc62d" -dependencies = [ - "num-traits", -] - -[[package]] -name = "os_str_bytes" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e22443d1643a904602595ba1cd8f7d896afe56d26712531c5ff73a15b2fbf64" - -[[package]] -name = "owned_ttf_parser" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f923fb806c46266c02ab4a5b239735c144bdeda724a50ed058e5226f594cde3" -dependencies = [ - "ttf-parser 0.6.2", -] - -[[package]] -name = "owned_ttf_parser" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65ee3f72636e6f164cc41c9f9057f4e58c4e13507699ea7f5e5242b64b8198ee" -dependencies = [ - "ttf-parser 0.13.2", -] - -[[package]] -name = "packed_struct" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c48e482b9a59ad6c2cdb06f7725e7bd33fe3525baaf4699fde7bfea6a5b77b1" -dependencies = [ - "bitvec 0.22.3", - "packed_struct_codegen", - "serde", -] - -[[package]] -name = "packed_struct_codegen" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e3692b867ec1d48ccb441e951637a2cc3130d0912c0059e48319e1c83e44bc" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "page_size" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eebde548fbbf1ea81a99b128872779c437752fb99f217c45245e1a61dcd9edcd" -dependencies = [ - "libc", - "winapi", -] - -[[package]] -name = "palette" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9735f7e1e51a3f740bacd5dc2724b61a7806f23597a8736e679f38ee3435d18" -dependencies = [ - "approx 0.5.0", - "num-traits", - "palette_derive", - "phf", -] - -[[package]] -name = "palette_derive" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7799c3053ea8a6d8a1193c7ba42f534e7863cf52e378a7f90406f4a645d33bad" -dependencies = [ - "find-crate", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "parking_lot" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" -dependencies = [ - "instant", - "lock_api", - "parking_lot_core 0.8.5", -] - -[[package]] -name = "parking_lot" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87f5ec2493a61ac0506c0f4199f99070cbe83857b0337006a30f3e6719b8ef58" -dependencies = [ - "lock_api", - "parking_lot_core 0.9.2", -] - -[[package]] -name = "parking_lot_core" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216" -dependencies = [ - "cfg-if 1.0.0", - "instant", - "libc", - "redox_syscall", - "smallvec", - "winapi", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "995f667a6c822200b0433ac218e05582f0e2efa1b922a3fd2fbaadc5f87bab37" -dependencies = [ - "cfg-if 1.0.0", - "libc", - "redox_syscall", - "smallvec", - "windows-sys 0.34.0", -] - -[[package]] -name = "peg" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af728fe826811af3b38c37e93de6d104485953ea373d656eebae53d6987fcd2c" -dependencies = [ - "peg-macros", - "peg-runtime", -] - -[[package]] -name = "peg-macros" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4536be147b770b824895cbad934fccce8e49f14b4c4946eaa46a6e4a12fcdc16" -dependencies = [ - "peg-runtime", - "proc-macro2", - "quote", -] - -[[package]] -name = "peg-runtime" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9b0efd3ba03c3a409d44d60425f279ec442bcf0b9e63ff4e410da31c8b0f69f" - -[[package]] -name = "percent-encoding" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" - -[[package]] -name = "pest" -version = "2.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10f4872ae94d7b90ae48754df22fd42ad52ce740b8f370b03da4835417403e53" -dependencies = [ - "ucd-trie", -] - -[[package]] -name = "pest_derive" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "833d1ae558dc601e9a60366421196a8d94bc0ac980476d0b67e1d0988d72b2d0" -dependencies = [ - "pest", - "pest_generator", -] - -[[package]] -name = "pest_generator" -version = "2.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99b8db626e31e5b81787b9783425769681b347011cc59471e33ea46d2ea0cf55" -dependencies = [ - "pest", - "pest_meta", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "pest_meta" -version = "2.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54be6e404f5317079812fc8f9f5279de376d8856929e21c184ecf6bbd692a11d" -dependencies = [ - "maplit", - "pest", - "sha-1", -] - -[[package]] -name = "phf" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2ac8b67553a7ca9457ce0e526948cad581819238f4a9d1ea74545851fa24f37" -dependencies = [ - "phf_macros", - "phf_shared", - "proc-macro-hack", -] - -[[package]] -name = "phf_generator" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d43f3220d96e0080cc9ea234978ccd80d904eafb17be31bb0f76daaea6493082" -dependencies = [ - "phf_shared", - "rand", -] - -[[package]] -name = "phf_macros" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b706f5936eb50ed880ae3009395b43ed19db5bff2ebd459c95e7bf013a89ab86" -dependencies = [ - "phf_generator", - "phf_shared", - "proc-macro-hack", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "phf_shared" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a68318426de33640f02be62b4ae8eb1261be2efbc337b60c54d845bf4484e0d9" -dependencies = [ - "siphasher", -] - -[[package]] -name = "pin-project-lite" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d31d11c69a6b52a174b42bdc0c30e5e11670f90788b2c471c31c1d17d449443" - -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - -[[package]] -name = "pkg-config" -version = "0.3.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12295df4f294471248581bc09bef3c38a5e46f1e36d6a37353621a0c6c357e1f" - -[[package]] -name = "plotters" -version = "0.3.1" -source = "git+https://github.com/Anton-4/plotters#d043988179b61db714ad60f678637ee145e363d3" -dependencies = [ - "num-traits", - "plotters-backend", - "plotters-svg", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "plotters-backend" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d88417318da0eaf0fdcdb51a0ee6c3bed624333bff8f946733049380be67ac1c" - -[[package]] -name = "plotters-svg" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "521fa9638fa597e1dc53e9412a4f9cefb01187ee1f7413076f9e6749e2885ba9" -dependencies = [ - "plotters-backend", -] - -[[package]] -name = "ppv-lite86" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed0cfbc8191465bed66e1718596ee0b0b35d5ee1f41c5df2189d0fe8bde535ba" - -[[package]] -name = "proc-macro-crate" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d6ea3c4595b96363c13943497db34af4460fb474a95c43f4446ad341b8c9785" -dependencies = [ - "toml", -] - -[[package]] -name = "proc-macro-crate" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ebace6889caf889b4d3f76becee12e90353f2b8c7d875534a71e5742f8f6f83" -dependencies = [ - "thiserror", - "toml", -] - -[[package]] -name = "proc-macro-hack" -version = "0.5.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5" - -[[package]] -name = "proc-macro-nested" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc881b2c22681370c6a780e47af9840ef841837bc98118431d4e1868bd0c1086" - -[[package]] -name = "proc-macro2" -version = "1.0.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba508cc11742c0dc5c1659771673afbab7a0efab23aa17e854cbab0837ed0b43" -dependencies = [ - "unicode-xid", -] - -[[package]] -name = "profiling" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9926767b8b8244d7b6b64546585121d193c3d0b4856ccd656b7bfa9deb91ab6a" - -[[package]] -name = "pulldown-cmark" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffade02495f22453cd593159ea2f59827aae7f53fa8323f756799b670881dcf8" -dependencies = [ - "bitflags", - "memchr", - "unicase", -] - -[[package]] -name = "quick-xml" -version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8533f14c8382aaad0d592c812ac3b826162128b65662331e1127b45c3d18536b" -dependencies = [ - "memchr", -] - -[[package]] -name = "quote" -version = "1.0.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "radium" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "643f8f41a8ebc4c5dc4515c82bb8abd397b527fc20fd681b7c011c2aee5d44fb" - -[[package]] -name = "radium" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" - -[[package]] -name = "radix_trie" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c069c179fcdc6a2fe24d8d18305cf085fdbd4f922c041943e203685d6a1c58fd" -dependencies = [ - "endian-type", - "nibble_vec", -] - -[[package]] -name = "rand" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8" -dependencies = [ - "libc", - "rand_chacha", - "rand_core 0.6.3", - "rand_hc", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core 0.6.3", -] - -[[package]] -name = "rand_core" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" - -[[package]] -name = "rand_core" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" -dependencies = [ - "getrandom", -] - -[[package]] -name = "rand_hc" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d51e9f596de227fda2ea6c84607f5558e196eeaf43c986b724ba4fb8fdf497e7" -dependencies = [ - "rand_core 0.6.3", -] - -[[package]] -name = "rand_xoshiro" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9fcdd2e881d02f1d9390ae47ad8e5696a9e4be7b547a1da2afbc61973217004" -dependencies = [ - "rand_core 0.5.1", -] - -[[package]] -name = "range-alloc" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63e935c45e09cc6dcf00d2f0b2d630a58f4095320223d47fc68918722f0538b6" - -[[package]] -name = "raw-window-handle" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a441a7a6c80ad6473bd4b74ec1c9a4c951794285bf941c2126f607c72e48211" -dependencies = [ - "libc", -] - -[[package]] -name = "rayon" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c06aca804d41dbc8ba42dfd964f0d01334eceb64314b9ecf7c5fad5188a06d90" -dependencies = [ - "autocfg", - "crossbeam-deque", - "either", - "rayon-core", -] - -[[package]] -name = "rayon-core" -version = "1.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d78120e2c850279833f1dd3582f730c4ab53ed95aeaaaa862a2a5c71b1656d8e" -dependencies = [ - "crossbeam-channel", - "crossbeam-deque", - "crossbeam-utils", - "lazy_static", - "num_cpus", -] - -[[package]] -name = "redox_syscall" -version = "0.2.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff" -dependencies = [ - "bitflags", -] - -[[package]] -name = "redox_users" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "528532f3d801c87aec9def2add9ca802fe569e44a544afe633765267840abe64" -dependencies = [ - "getrandom", - "redox_syscall", -] - -[[package]] -name = "regex" -version = "1.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" - -[[package]] -name = "regex-syntax" -version = "0.6.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" - -[[package]] -name = "remove_dir_all" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" -dependencies = [ - "winapi", -] - -[[package]] -name = "renderdoc-sys" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1382d1f0a252c4bf97dc20d979a2fdd05b024acd7c2ed0f7595d7817666a157" - -[[package]] -name = "rlimit" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc0bf25554376fd362f54332b8410a625c71f15445bca32ffdfdf4ec9ac91726" -dependencies = [ - "libc", -] - -[[package]] -name = "roc_alias_analysis" -version = "0.1.0" -dependencies = [ - "morphic_lib", - "roc_collections", - "roc_debug_flags", - "roc_module", - "roc_mono", -] - -[[package]] -name = "roc_ast" -version = "0.1.0" -dependencies = [ - "arrayvec 0.7.2", - "bumpalo", - "libc", - "page_size", - "roc_builtins", - "roc_can", - "roc_collections", - "roc_error_macros", - "roc_load", - "roc_module", - "roc_parse", - "roc_problem", - "roc_region", - "roc_reporting", - "roc_target", - "roc_types", - "roc_unify", - "snafu", - "ven_graph", - "winapi", -] - -[[package]] -name = "roc_build" -version = "0.1.0" -dependencies = [ - "bumpalo", - "inkwell 0.1.0", - "libloading 0.7.1", - "roc_builtins", - "roc_can", - "roc_collections", - "roc_constrain", - "roc_error_macros", - "roc_gen_dev", - "roc_gen_llvm", - "roc_gen_wasm", - "roc_load", - "roc_module", - "roc_mono", - "roc_parse", - "roc_problem", - "roc_region", - "roc_reporting", - "roc_solve", - "roc_std", - "roc_target", - "roc_types", - "roc_unify", - "serde_json", - "target-lexicon", - "tempfile", - "wasi_libc_sys", -] - -[[package]] -name = "roc_builtins" -version = "0.1.0" -dependencies = [ - "dunce", - "lazy_static", - "roc_collections", - "roc_module", - "roc_region", - "roc_target", - "roc_types", -] - -[[package]] -name = "roc_can" -version = "0.1.0" -dependencies = [ - "bitvec 1.0.0", - "bumpalo", - "roc_collections", - "roc_error_macros", - "roc_exhaustive", - "roc_module", - "roc_parse", - "roc_problem", - "roc_region", - "roc_types", - "static_assertions 1.1.0", -] - -[[package]] -name = "roc_cli" -version = "0.1.0" -dependencies = [ - "bumpalo", - "clap 3.1.17", - "const_format", - "mimalloc", - "roc_build", - "roc_builtins", - "roc_can", - "roc_collections", - "roc_docs", - "roc_editor", - "roc_error_macros", - "roc_fmt", - "roc_linker", - "roc_load", - "roc_module", - "roc_mono", - "roc_parse", - "roc_region", - "roc_repl_cli", - "roc_reporting", - "roc_target", - "target-lexicon", - "tempfile", -] - -[[package]] -name = "roc_code_markup" -version = "0.1.0" -dependencies = [ - "bumpalo", - "itertools 0.10.1", - "palette", - "roc_ast", - "roc_module", - "roc_utils", - "serde", - "snafu", -] - -[[package]] -name = "roc_collections" -version = "0.1.0" -dependencies = [ - "bumpalo", - "hashbrown 0.11.2", - "im", - "im-rc", - "wyhash", -] - -[[package]] -name = "roc_constrain" -version = "0.1.0" -dependencies = [ - "arrayvec 0.7.2", - "roc_builtins", - "roc_can", - "roc_collections", - "roc_error_macros", - "roc_module", - "roc_parse", - "roc_region", - "roc_types", -] - -[[package]] -name = "roc_debug_flags" -version = "0.1.0" - -[[package]] -name = "roc_docs" -version = "0.1.0" -dependencies = [ - "bumpalo", - "peg", - "pulldown-cmark", - "roc_ast", - "roc_builtins", - "roc_can", - "roc_code_markup", - "roc_collections", - "roc_highlight", - "roc_load", - "roc_module", - "roc_parse", - "roc_region", - "roc_reporting", - "roc_target", - "roc_types", - "snafu", -] - -[[package]] -name = "roc_editor" -version = "0.1.0" -dependencies = [ - "arrayvec 0.7.2", - "bumpalo", - "bytemuck", - "cgmath", - "colored", - "confy", - "copypasta", - "env_logger", - "fs_extra", - "futures", - "glyph_brush", - "libc", - "log", - "nonempty", - "page_size", - "palette", - "pest", - "pest_derive", - "roc_ast", - "roc_builtins", - "roc_can", - "roc_code_markup", - "roc_collections", - "roc_load", - "roc_module", - "roc_parse", - "roc_problem", - "roc_region", - "roc_reporting", - "roc_solve", - "roc_types", - "roc_unify", - "serde", - "snafu", - "threadpool", - "ven_graph", - "wgpu", - "wgpu_glyph", - "winit", -] - -[[package]] -name = "roc_error_macros" -version = "0.1.0" - -[[package]] -name = "roc_exhaustive" -version = "0.1.0" -dependencies = [ - "roc_collections", - "roc_module", - "roc_region", - "roc_std", -] - -[[package]] -name = "roc_fmt" -version = "0.1.0" -dependencies = [ - "bumpalo", - "roc_collections", - "roc_module", - "roc_parse", - "roc_region", -] - -[[package]] -name = "roc_gen_dev" -version = "0.1.0" -dependencies = [ - "bumpalo", - "object 0.26.2", - "packed_struct", - "roc_builtins", - "roc_collections", - "roc_error_macros", - "roc_module", - "roc_mono", - "roc_problem", - "roc_region", - "roc_solve", - "roc_target", - "roc_types", - "roc_unify", - "target-lexicon", -] - -[[package]] -name = "roc_gen_llvm" -version = "0.1.0" -dependencies = [ - "bumpalo", - "inkwell 0.1.0", - "morphic_lib", - "roc_alias_analysis", - "roc_builtins", - "roc_collections", - "roc_debug_flags", - "roc_error_macros", - "roc_module", - "roc_mono", - "roc_std", - "roc_target", - "target-lexicon", -] - -[[package]] -name = "roc_gen_wasm" -version = "0.1.0" -dependencies = [ - "bumpalo", - "roc_builtins", - "roc_collections", - "roc_error_macros", - "roc_module", - "roc_mono", - "roc_std", - "roc_target", -] - -[[package]] -name = "roc_highlight" -version = "0.1.0" -dependencies = [ - "peg", - "roc_code_markup", -] - -[[package]] -name = "roc_ident" -version = "0.1.0" - -[[package]] -name = "roc_linker" -version = "0.1.0" -dependencies = [ - "bincode", - "bumpalo", - "clap 3.1.17", - "iced-x86", - "memmap2 0.5.3", - "object 0.26.2", - "roc_build", - "roc_collections", - "roc_mono", - "serde", - "target-lexicon", - "tempfile", -] - -[[package]] -name = "roc_load" -version = "0.1.0" -dependencies = [ - "bumpalo", - "roc_builtins", - "roc_collections", - "roc_constrain", - "roc_load_internal", - "roc_module", - "roc_reporting", - "roc_target", - "roc_types", -] - -[[package]] -name = "roc_load_internal" -version = "0.1.0" -dependencies = [ - "bumpalo", - "crossbeam", - "num_cpus", - "parking_lot 0.12.0", - "roc_builtins", - "roc_can", - "roc_collections", - "roc_constrain", - "roc_debug_flags", - "roc_error_macros", - "roc_module", - "roc_mono", - "roc_parse", - "roc_problem", - "roc_region", - "roc_reporting", - "roc_solve", - "roc_target", - "roc_types", - "roc_unify", - "ven_pretty", -] - -[[package]] -name = "roc_module" -version = "0.1.0" -dependencies = [ - "bumpalo", - "lazy_static", - "roc_collections", - "roc_error_macros", - "roc_ident", - "roc_region", - "snafu", - "static_assertions 1.1.0", -] - -[[package]] -name = "roc_mono" -version = "0.1.0" -dependencies = [ - "bumpalo", - "hashbrown 0.11.2", - "roc_builtins", - "roc_can", - "roc_collections", - "roc_debug_flags", - "roc_error_macros", - "roc_exhaustive", - "roc_module", - "roc_problem", - "roc_region", - "roc_solve", - "roc_std", - "roc_target", - "roc_types", - "roc_unify", - "static_assertions 1.1.0", - "ven_graph", - "ven_pretty", -] - -[[package]] -name = "roc_parse" -version = "0.1.0" -dependencies = [ - "bumpalo", - "encode_unicode", - "roc_collections", - "roc_module", - "roc_region", -] - -[[package]] -name = "roc_problem" -version = "0.1.0" -dependencies = [ - "roc_collections", - "roc_module", - "roc_parse", - "roc_region", - "roc_types", -] - -[[package]] -name = "roc_region" -version = "0.1.0" -dependencies = [ - "static_assertions 1.1.0", -] - -[[package]] -name = "roc_repl_cli" -version = "0.1.0" -dependencies = [ - "bumpalo", - "const_format", - "inkwell 0.1.0", - "libloading 0.7.1", - "roc_build", - "roc_builtins", - "roc_collections", - "roc_gen_llvm", - "roc_load", - "roc_mono", - "roc_parse", - "roc_repl_eval", - "roc_reporting", - "roc_std", - "roc_target", - "roc_types", - "rustyline", - "rustyline-derive", - "target-lexicon", -] - -[[package]] -name = "roc_repl_eval" -version = "0.1.0" -dependencies = [ - "bumpalo", - "roc_builtins", - "roc_can", - "roc_collections", - "roc_fmt", - "roc_load", - "roc_module", - "roc_mono", - "roc_parse", - "roc_region", - "roc_reporting", - "roc_std", - "roc_target", - "roc_types", -] - -[[package]] -name = "roc_reporting" -version = "0.1.0" -dependencies = [ - "bumpalo", - "distance", - "roc_can", - "roc_collections", - "roc_exhaustive", - "roc_module", - "roc_parse", - "roc_problem", - "roc_region", - "roc_solve", - "roc_types", - "ven_pretty", -] - -[[package]] -name = "roc_solve" -version = "0.1.0" -dependencies = [ - "arrayvec 0.7.2", - "bumpalo", - "roc_can", - "roc_collections", - "roc_debug_flags", - "roc_error_macros", - "roc_exhaustive", - "roc_module", - "roc_region", - "roc_types", - "roc_unify", -] - -[[package]] -name = "roc_std" -version = "0.1.0" -dependencies = [ - "static_assertions 0.1.1", -] - -[[package]] -name = "roc_target" -version = "0.1.0" -dependencies = [ - "target-lexicon", -] - -[[package]] -name = "roc_types" -version = "0.1.0" -dependencies = [ - "bumpalo", - "roc_collections", - "roc_debug_flags", - "roc_error_macros", - "roc_module", - "roc_region", - "static_assertions 1.1.0", - "ven_ena", -] - -[[package]] -name = "roc_unify" -version = "0.1.0" -dependencies = [ - "bitflags", - "roc_collections", - "roc_debug_flags", - "roc_error_macros", - "roc_module", - "roc_types", -] - -[[package]] -name = "roc_utils" -version = "0.1.0" -dependencies = [ - "snafu", -] - -[[package]] -name = "rustc-demangle" -version = "0.1.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342" - -[[package]] -name = "rustc-hash" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" - -[[package]] -name = "rustc_version" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" -dependencies = [ - "semver 1.0.4", -] - -[[package]] -name = "rusttype" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc7c727aded0be18c5b80c1640eae0ac8e396abf6fa8477d96cb37d18ee5ec59" -dependencies = [ - "ab_glyph_rasterizer", - "owned_ttf_parser 0.6.0", -] - -[[package]] -name = "rustyline" -version = "9.1.1" -source = "git+https://github.com/rtfeldman/rustyline?rev=e74333c#e74333c0d618896b88175bf06645108f996fe6d0" -dependencies = [ - "bitflags", - "cfg-if 1.0.0", - "clipboard-win 4.2.2", - "dirs-next", - "fd-lock", - "libc", - "log", - "memchr", - "nix 0.23.1", - "radix_trie", - "scopeguard", - "smallvec", - "unicode-segmentation", - "unicode-width", - "utf8parse", - "winapi", -] - -[[package]] -name = "rustyline-derive" -version = "0.6.0" -source = "git+https://github.com/rtfeldman/rustyline?rev=e74333c#e74333c0d618896b88175bf06645108f996fe6d0" -dependencies = [ - "quote", - "syn", -] - -[[package]] -name = "ryu" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" - -[[package]] -name = "same-file" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "scoped-tls" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea6a9290e3c9cf0f18145ef7ffa62d68ee0bf5fcd651017e586dc7fd5da448c2" - -[[package]] -name = "scopeguard" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" - -[[package]] -name = "semver" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6" -dependencies = [ - "semver-parser", -] - -[[package]] -name = "semver" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "568a8e6258aa33c13358f81fd834adb854c6f7c9468520910a9b1e8fac068012" - -[[package]] -name = "semver-parser" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b0bef5b7f9e0df16536d3961cfb6e84331c065b4066afb39768d0e319411f7" -dependencies = [ - "pest", -] - -[[package]] -name = "serde" -version = "1.0.130" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f12d06de37cf59146fbdecab66aa99f9fe4f78722e3607577a5375d66bd0c913" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde-xml-rs" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65162e9059be2f6a3421ebbb4fef3e74b7d9e7c60c50a0e292c6239f19f1edfa" -dependencies = [ - "log", - "serde", - "thiserror", - "xml-rs", -] - -[[package]] -name = "serde_cbor" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bef2ebfde456fb76bbcf9f59315333decc4fda0b2b44b420243c11e0f5ec1f5" -dependencies = [ - "half", - "serde", -] - -[[package]] -name = "serde_derive" -version = "1.0.130" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7bc1a1ab1961464eae040d96713baa5a724a8152c1222492465b54322ec508b" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "serde_json" -version = "1.0.70" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e277c495ac6cd1a01a58d0a0c574568b4d1ddf14f59965c6a58b8d96400b54f3" -dependencies = [ - "itoa", - "ryu", - "serde", -] - -[[package]] -name = "serde_yaml" -version = "0.8.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8c608a35705a5d3cdc9fbe403147647ff34b921f8e833e49306df898f9b20af" -dependencies = [ - "dtoa", - "indexmap", - "serde", - "yaml-rust", -] - -[[package]] -name = "sha-1" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7d94d0bede923b3cea61f3f1ff57ff8cdfd77b400fb8f9998949e0cf04163df" -dependencies = [ - "block-buffer 0.7.3", - "digest 0.8.1", - "fake-simd", - "opaque-debug 0.2.3", -] - -[[package]] -name = "sha2" -version = "0.9.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b69f9a4c9740d74c5baa3fd2e547f9525fa8088a8a958e0ca2409a514e33f5fa" -dependencies = [ - "block-buffer 0.9.0", - "cfg-if 1.0.0", - "cpufeatures", - "digest 0.9.0", - "opaque-debug 0.3.0", -] - -[[package]] -name = "siphasher" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "533494a8f9b724d33625ab53c6c4800f7cc445895924a8ef649222dcb76e938b" - -[[package]] -name = "sized-chunks" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16d69225bde7a69b235da73377861095455d298f2b970996eec25ddbb42b3d1e" -dependencies = [ - "bitmaps", - "typenum", -] - -[[package]] -name = "slab" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9def91fd1e018fe007022791f865d0ccc9b3a0d5001e01aabb8b40e46000afb5" - -[[package]] -name = "slotmap" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1e08e261d0e8f5c43123b7adf3e4ca1690d655377ac93a03b2c9d3e98de1342" -dependencies = [ - "version_check", -] - -[[package]] -name = "smallvec" -version = "1.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ecab6c735a6bb4139c0caafd0cc3635748bbb3acf4550e8138122099251f309" - -[[package]] -name = "smithay-client-toolkit" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4750c76fd5d3ac95fa3ed80fe667d6a3d8590a960e5b575b98eea93339a80b80" -dependencies = [ - "andrew", - "bitflags", - "calloop", - "dlib 0.4.2", - "lazy_static", - "log", - "memmap2 0.1.0", - "nix 0.18.0", - "wayland-client 0.28.6", - "wayland-cursor 0.28.6", - "wayland-protocols 0.28.6", -] - -[[package]] -name = "smithay-client-toolkit" -version = "0.15.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "210cf40de565aaaa085face1d860b17f6aee9f76f9d2816307ea2cc45eeb64f3" -dependencies = [ - "bitflags", - "dlib 0.5.0", - "lazy_static", - "log", - "memmap2 0.3.1", - "nix 0.22.0", - "pkg-config", - "wayland-client 0.29.1", - "wayland-cursor 0.29.1", - "wayland-protocols 0.29.1", -] - -[[package]] -name = "smithay-clipboard" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "610b551bd25378bfd2b8e7a0fcbd83d427e8f2f6a40c47ae0f70688e9949dd55" -dependencies = [ - "smithay-client-toolkit 0.15.2", - "wayland-client 0.29.1", -] - -[[package]] -name = "snafu" -version = "0.6.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eab12d3c261b2308b0d80c26fffb58d17eba81a4be97890101f416b478c79ca7" -dependencies = [ - "backtrace", - "doc-comment", - "snafu-derive", -] - -[[package]] -name = "snafu-derive" -version = "0.6.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1508efa03c362e23817f96cde18abed596a25219a8b2c66e8db33c03543d315b" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "spirv" -version = "0.2.0+1.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "246bfa38fe3db3f1dfc8ca5a2cdeb7348c78be2112740cc0ec8ef18b6d94f830" -dependencies = [ - "bitflags", - "num-traits", -] - -[[package]] -name = "static_assertions" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f406d6ee68db6796e11ffd7b4d171864c58b7451e79ef9460ea33c287a1f89a7" - -[[package]] -name = "static_assertions" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" - -[[package]] -name = "str-buf" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d44a3643b4ff9caf57abcee9c2c621d6c03d9135e0d8b589bd9afb5992cb176a" - -[[package]] -name = "strip-ansi-escapes" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "011cbb39cf7c1f62871aea3cc46e5817b0937b49e9447370c93cacbe93a766d8" -dependencies = [ - "vte", -] - -[[package]] -name = "strsim" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6446ced80d6c486436db5c078dde11a9f73d42b57fb273121e160b84f63d894c" - -[[package]] -name = "strsim" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" - -[[package]] -name = "syn" -version = "1.0.81" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2afee18b8beb5a596ecb4a2dce128c719b4ba399d34126b9e4396e3f9860966" -dependencies = [ - "proc-macro2", - "quote", - "unicode-xid", -] - -[[package]] -name = "tap" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" - -[[package]] -name = "target-lexicon" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7fa7e55043acb85fca6b3c01485a2eeb6b69c5d21002e273c79e465f43b7ac1" - -[[package]] -name = "tempfile" -version = "3.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dac1c663cfc93810f88aed9b8941d48cabf856a1b111c29a40439018d870eb22" -dependencies = [ - "cfg-if 1.0.0", - "libc", - "rand", - "redox_syscall", - "remove_dir_all", - "winapi", -] - -[[package]] -name = "termcolor" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "textwrap" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" -dependencies = [ - "unicode-width", -] - -[[package]] -name = "textwrap" -version = "0.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" - -[[package]] -name = "thiserror" -version = "1.0.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417" -dependencies = [ - "thiserror-impl", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "threadpool" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa" -dependencies = [ - "num_cpus", -] - -[[package]] -name = "tinytemplate" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" -dependencies = [ - "serde", - "serde_json", -] - -[[package]] -name = "toml" -version = "0.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa" -dependencies = [ - "serde", -] - -[[package]] -name = "ttf-parser" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e5d7cd7ab3e47dda6e56542f4bbf3824c15234958c6e1bd6aaa347e93499fdc" - -[[package]] -name = "ttf-parser" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e835d06ed78a500d3d0e431a20c18ff5544b3f6e11376e834370cfd35e8948e" - -[[package]] -name = "twox-hash" -version = "1.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f559b464de2e2bdabcac6a210d12e9b5a5973c251e102c44c585c71d51bd78e" -dependencies = [ - "cfg-if 1.0.0", - "rand", - "static_assertions 1.1.0", -] - -[[package]] -name = "typed-arena" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0685c84d5d54d1c26f7d3eb96cd41550adb97baed141a761cf335d3d33bcd0ae" - -[[package]] -name = "typenum" -version = "1.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b63708a265f51345575b27fe43f9500ad611579e764c79edbc2037b1121959ec" - -[[package]] -name = "ucd-trie" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c" - -[[package]] -name = "unicase" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" -dependencies = [ - "version_check", -] - -[[package]] -name = "unicode-segmentation" -version = "1.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b" - -[[package]] -name = "unicode-width" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" - -[[package]] -name = "unicode-xid" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" - -[[package]] -name = "utf8parse" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "936e4b492acfd135421d8dca4b1aa80a7bfc26e702ef3af710e0752684df5372" - -[[package]] -name = "ven_ena" -version = "0.13.1" -dependencies = [ - "log", -] - -[[package]] -name = "ven_graph" -version = "2.0.5-pre" -dependencies = [ - "roc_collections", -] - -[[package]] -name = "ven_pretty" -version = "0.9.1-alpha.0" -dependencies = [ - "arrayvec 0.7.2", - "typed-arena", -] - -[[package]] -name = "version_check" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" - -[[package]] -name = "vte" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6cbce692ab4ca2f1f3047fcf732430249c0e971bfdd2b234cf2c47ad93af5983" -dependencies = [ - "arrayvec 0.5.2", - "utf8parse", - "vte_generate_state_changes", -] - -[[package]] -name = "vte_generate_state_changes" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d257817081c7dffcdbab24b9e62d2def62e2ff7d00b1c20062551e6cccc145ff" -dependencies = [ - "proc-macro2", - "quote", -] - -[[package]] -name = "walkdir" -version = "2.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" -dependencies = [ - "same-file", - "winapi", - "winapi-util", -] - -[[package]] -name = "wasi" -version = "0.10.2+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" - -[[package]] -name = "wasi_libc_sys" -version = "0.1.0" - -[[package]] -name = "wasm-bindgen" -version = "0.2.79" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25f1af7423d8588a3d840681122e72e6a24ddbcb3f0ec385cac0d12d24256c06" -dependencies = [ - "cfg-if 1.0.0", - "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.79" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b21c0df030f5a177f3cba22e9bc4322695ec43e7257d865302900290bcdedca" -dependencies = [ - "bumpalo", - "lazy_static", - "log", - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-futures" -version = "0.4.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e8d7523cb1f2a4c96c1317ca690031b714a51cc14e05f712446691f413f5d39" -dependencies = [ - "cfg-if 1.0.0", - "js-sys", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.79" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f4203d69e40a52ee523b2529a773d5ffc1dc0071801c87b3d270b471b80ed01" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.79" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa8a30d46208db204854cadbb5d4baf5fcf8071ba5bf48190c3e59937962ebc" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-backend", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.79" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d958d035c4438e28c70e4321a2911302f10135ce78a9c7834c0cab4123d06a2" - -[[package]] -name = "wayland-client" -version = "0.28.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3ab332350e502f159382201394a78e3cc12d0f04db863429260164ea40e0355" -dependencies = [ - "bitflags", - "downcast-rs", - "libc", - "nix 0.20.0", - "scoped-tls", - "wayland-commons 0.28.6", - "wayland-scanner 0.28.6", - "wayland-sys 0.28.6", -] - -[[package]] -name = "wayland-client" -version = "0.29.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9108ec1c37f4774d0c2937ba1a6c23d1786b2152c4a13bd9fdb20e42d16e8841" -dependencies = [ - "bitflags", - "downcast-rs", - "libc", - "nix 0.22.0", - "scoped-tls", - "wayland-commons 0.29.1", - "wayland-scanner 0.29.1", - "wayland-sys 0.29.1", -] - -[[package]] -name = "wayland-commons" -version = "0.28.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a21817947c7011bbd0a27e11b17b337bfd022e8544b071a2641232047966fbda" -dependencies = [ - "nix 0.20.0", - "once_cell", - "smallvec", - "wayland-sys 0.28.6", -] - -[[package]] -name = "wayland-commons" -version = "0.29.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "265ef51b3b3e5c9ef098f10425c39624663f459c3821dcaacc4748be975f1beb" -dependencies = [ - "nix 0.22.0", - "once_cell", - "smallvec", - "wayland-sys 0.29.1", -] - -[[package]] -name = "wayland-cursor" -version = "0.28.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be610084edd1586d45e7bdd275fe345c7c1873598caa464c4fb835dee70fa65a" -dependencies = [ - "nix 0.20.0", - "wayland-client 0.28.6", - "xcursor", -] - -[[package]] -name = "wayland-cursor" -version = "0.29.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c19bb6628daf4097e58b7911481e8371e13318d5a60894779901bd3267407a7" -dependencies = [ - "nix 0.22.0", - "wayland-client 0.29.1", - "xcursor", -] - -[[package]] -name = "wayland-protocols" -version = "0.28.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "286620ea4d803bacf61fa087a4242ee316693099ee5a140796aaba02b29f861f" -dependencies = [ - "bitflags", - "wayland-client 0.28.6", - "wayland-commons 0.28.6", - "wayland-scanner 0.28.6", -] - -[[package]] -name = "wayland-protocols" -version = "0.29.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b3b6f1dc0193072ef4eadcb144da30d58c1f2895516c063804d213310703c8e" -dependencies = [ - "bitflags", - "wayland-client 0.29.1", - "wayland-commons 0.29.1", - "wayland-scanner 0.29.1", -] - -[[package]] -name = "wayland-scanner" -version = "0.28.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce923eb2deb61de332d1f356ec7b6bf37094dc5573952e1c8936db03b54c03f1" -dependencies = [ - "proc-macro2", - "quote", - "xml-rs", -] - -[[package]] -name = "wayland-scanner" -version = "0.29.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eaaf2bc85e7b9143159af96bd23d954a5abe391c4376db712320643280fdc6f4" -dependencies = [ - "proc-macro2", - "quote", - "xml-rs", -] - -[[package]] -name = "wayland-sys" -version = "0.28.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d841fca9aed7febf9bed2e9796c49bf58d4152ceda8ac949ebe00868d8f0feb8" -dependencies = [ - "dlib 0.5.0", - "lazy_static", - "pkg-config", -] - -[[package]] -name = "wayland-sys" -version = "0.29.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba9e06acb775b3007f8d3094438306979e572d1d3b844d7a71557a84b055d959" -dependencies = [ - "dlib 0.5.0", - "lazy_static", - "pkg-config", -] - -[[package]] -name = "web-sys" -version = "0.3.55" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38eb105f1c59d9eaa6b5cdc92b859d85b926e82cb2e0945cd0c9259faa6fe9fb" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "wgpu" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1577ecc4f6992b9e965878ac594efb24eed2bdf089c11f45b3d1c5f216e2e30" -dependencies = [ - "arrayvec 0.7.2", - "js-sys", - "log", - "parking_lot 0.11.2", - "raw-window-handle", - "smallvec", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "wgpu-core", - "wgpu-hal", - "wgpu-types", -] - -[[package]] -name = "wgpu-core" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bdcbfa4885b32c2b1feb2faeb8b6a76065b752b8f08751b82f994e937687f46" -dependencies = [ - "arrayvec 0.7.2", - "bitflags", - "cfg_aliases", - "copyless", - "fxhash", - "log", - "naga", - "parking_lot 0.11.2", - "profiling", - "raw-window-handle", - "smallvec", - "thiserror", - "wgpu-hal", - "wgpu-types", -] - -[[package]] -name = "wgpu-hal" -version = "0.11.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e493835d9edb153d5c8a9d8d016e1811dbe32ddb707a110be1453c7b051d3ec" -dependencies = [ - "arrayvec 0.7.2", - "ash", - "bit-set", - "bitflags", - "block", - "core-graphics-types", - "d3d12", - "foreign-types", - "fxhash", - "glow", - "gpu-alloc", - "gpu-descriptor", - "inplace_it", - "js-sys", - "khronos-egl", - "libloading 0.7.1", - "log", - "metal", - "naga", - "objc", - "parking_lot 0.11.2", - "profiling", - "range-alloc", - "raw-window-handle", - "renderdoc-sys", - "thiserror", - "wasm-bindgen", - "web-sys", - "wgpu-types", - "winapi", -] - -[[package]] -name = "wgpu-types" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e15e44ba88ec415466e18e91881319e7c9e96cb905dc623305168aea65b85ccc" -dependencies = [ - "bitflags", -] - -[[package]] -name = "wgpu_glyph" -version = "0.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c78d49f4d168b245882ce000ee94fc67e744b33760c0119b0dbf8cb3caf20de5" -dependencies = [ - "bytemuck", - "glyph_brush", - "log", - "wgpu", -] - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-util" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" -dependencies = [ - "winapi", -] - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[package]] -name = "windows-sys" -version = "0.28.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82ca39602d5cbfa692c4b67e3bcbb2751477355141c1ed434c94da4186836ff6" -dependencies = [ - "windows_aarch64_msvc 0.28.0", - "windows_i686_gnu 0.28.0", - "windows_i686_msvc 0.28.0", - "windows_x86_64_gnu 0.28.0", - "windows_x86_64_msvc 0.28.0", -] - -[[package]] -name = "windows-sys" -version = "0.34.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5acdd78cb4ba54c0045ac14f62d8f94a03d10047904ae2a40afa1e99d8f70825" -dependencies = [ - "windows_aarch64_msvc 0.34.0", - "windows_i686_gnu 0.34.0", - "windows_i686_msvc 0.34.0", - "windows_x86_64_gnu 0.34.0", - "windows_x86_64_msvc 0.34.0", -] - -[[package]] -name = "windows_aarch64_msvc" -version = "0.28.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52695a41e536859d5308cc613b4a022261a274390b25bd29dfff4bf08505f3c2" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.34.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17cffbe740121affb56fad0fc0e421804adf0ae00891205213b5cecd30db881d" - -[[package]] -name = "windows_i686_gnu" -version = "0.28.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f54725ac23affef038fecb177de6c9bf065787c2f432f79e3c373da92f3e1d8a" - -[[package]] -name = "windows_i686_gnu" -version = "0.34.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2564fde759adb79129d9b4f54be42b32c89970c18ebf93124ca8870a498688ed" - -[[package]] -name = "windows_i686_msvc" -version = "0.28.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d5158a43cc43623c0729d1ad6647e62fa384a3d135fd15108d37c683461f64" - -[[package]] -name = "windows_i686_msvc" -version = "0.34.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cd9d32ba70453522332c14d38814bceeb747d80b3958676007acadd7e166956" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.28.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc31f409f565611535130cfe7ee8e6655d3fa99c1c61013981e491921b5ce954" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.34.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfce6deae227ee8d356d19effc141a509cc503dfd1f850622ec4b0f84428e1f4" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.28.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f2b8c7cbd3bfdddd9ab98769f9746a7fad1bca236554cd032b78d768bc0e89f" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.34.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d19538ccc21819d01deaf88d6a17eae6596a12e9aafdbb97916fb49896d89de9" - -[[package]] -name = "winit" -version = "0.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79610794594d5e86be473ef7763f604f2159cbac8c94debd00df8fb41e86c2f8" -dependencies = [ - "bitflags", - "cocoa", - "core-foundation 0.9.2", - "core-graphics 0.22.3", - "core-video-sys", - "dispatch", - "instant", - "lazy_static", - "libc", - "log", - "mio", - "mio-misc", - "ndk", - "ndk-glue", - "ndk-sys", - "objc", - "parking_lot 0.11.2", - "percent-encoding", - "raw-window-handle", - "scopeguard", - "smithay-client-toolkit 0.12.3", - "wayland-client 0.28.6", - "winapi", - "x11-dl", -] - -[[package]] -name = "wyhash" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf6e163c25e3fac820b4b453185ea2dea3b6a3e0a721d4d23d75bd33734c295" -dependencies = [ - "rand_core 0.6.3", -] - -[[package]] -name = "wyz" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "129e027ad65ce1453680623c3fb5163cbf7107bfe1aa32257e7d0e63f9ced188" -dependencies = [ - "tap", -] - -[[package]] -name = "wyz" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30b31594f29d27036c383b53b59ed3476874d518f0efb151b27a4c275141390e" -dependencies = [ - "tap", -] - -[[package]] -name = "x11-clipboard" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "473068b7b80ac86a18328824f1054e5e007898c47b5bbc281bd7abe32bc3653c" -dependencies = [ - "xcb", -] - -[[package]] -name = "x11-dl" -version = "2.19.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea26926b4ce81a6f5d9d0f3a0bc401e5a37c6ae14a1bfaa8ff6099ca80038c59" -dependencies = [ - "lazy_static", - "libc", - "pkg-config", -] - -[[package]] -name = "xcb" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "771e2b996df720cd1c6dd9ff90f62d91698fd3610cc078388d0564bdd6622a9c" -dependencies = [ - "libc", - "log", - "quick-xml", -] - -[[package]] -name = "xcursor" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "463705a63313cd4301184381c5e8042f0a7e9b4bb63653f216311d4ae74690b7" -dependencies = [ - "nom", -] - -[[package]] -name = "xdg" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a23fe958c70412687039c86f578938b4a0bb50ec788e96bce4d6ab00ddd5803" -dependencies = [ - "dirs", -] - -[[package]] -name = "xi-unicode" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a67300977d3dc3f8034dae89778f502b6ba20b269527b3223ba59c0cf393bb8a" - -[[package]] -name = "xml-rs" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2d7d3948613f75c98fd9328cfdcc45acc4d360655289d0a7d4ec931392200a3" - -[[package]] -name = "yaml-rust" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" -dependencies = [ - "linked-hash-map", -] diff --git a/cli_utils/Cargo.toml b/cli_utils/Cargo.toml deleted file mode 100644 index 1c21da3cd8..0000000000 --- a/cli_utils/Cargo.toml +++ /dev/null @@ -1,27 +0,0 @@ -[package] -name = "cli_utils" -version = "0.1.0" -authors = ["The Roc Contributors"] -license = "UPL-1.0" -repository = "https://github.com/rtfeldman/roc" -edition = "2021" -description = "Shared code for cli tests and benchmarks" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -roc_cli = { path = "../cli" } -roc_collections = { path = "../compiler/collections" } -roc_reporting = { path = "../reporting" } -roc_load = { path = "../compiler/load" } -roc_module = { path = "../compiler/module" } -bumpalo = { version = "3.8.0", features = ["collections"] } -criterion = { git = "https://github.com/Anton-4/criterion.rs"} -serde = { version = "1.0.130", features = ["derive"] } -serde-xml-rs = "0.5.1" -strip-ansi-escapes = "0.1.1" -tempfile = "3.2.0" -const_format = { version = "0.2.23", features = ["const_generics"] } - -[target.'cfg(unix)'.dependencies] -rlimit = "0.6.2" diff --git a/cli_utils/src/helpers.rs b/cli_utils/src/helpers.rs deleted file mode 100644 index c9c2adbab9..0000000000 --- a/cli_utils/src/helpers.rs +++ /dev/null @@ -1,414 +0,0 @@ -extern crate bumpalo; -extern crate roc_collections; -extern crate roc_load; -extern crate roc_module; -extern crate tempfile; - -use serde::Deserialize; -use serde_xml_rs::from_str; -use std::env; -use std::ffi::OsStr; -use std::io::Read; -use std::io::Write; -use std::path::Path; -use std::path::PathBuf; -use std::process::{Command, ExitStatus, Stdio}; -use tempfile::NamedTempFile; - -#[derive(Debug)] -pub struct Out { - pub stdout: String, - pub stderr: String, - pub status: ExitStatus, -} - -pub fn run_roc(args: I, stdin_vals: &[&str]) -> Out -where - I: IntoIterator, - S: AsRef, -{ - let roc_binary_path = path_to_roc_binary(); - - // If we don't have a /target/release/roc, rebuild it! - if !roc_binary_path.exists() { - // Remove the /target/release/roc part - let root_project_dir = roc_binary_path - .parent() - .unwrap() - .parent() - .unwrap() - .parent() - .unwrap(); - - // cargo build --bin roc - // (with --release iff the test is being built with --release) - let args = if cfg!(debug_assertions) { - vec!["build", "--bin", "roc"] - } else { - vec!["build", "--release", "--bin", "roc"] - }; - - let output = Command::new("cargo") - .current_dir(root_project_dir) - .args(args) - .output() - .unwrap(); - - if !output.status.success() { - panic!("cargo build --release --bin roc failed. stdout was:\n\n{:?}\n\nstderr was:\n\n{:?}\n", - output.stdout, - output.stderr - ); - } - } - - run_with_stdin(&roc_binary_path, args, stdin_vals) -} - -pub fn run_bindgen(args: I) -> Out -where - I: IntoIterator, - S: AsRef, -{ - run_with_stdin(&path_to_bindgen_binary(), args, &[]) -} - -pub fn path_to_roc_binary() -> PathBuf { - path_to_binary("roc") -} - -pub fn path_to_bindgen_binary() -> PathBuf { - path_to_binary("roc-bindgen") -} - -pub fn path_to_binary(binary_name: &str) -> PathBuf { - // Adapted from https://github.com/volta-cli/volta/blob/cefdf7436a15af3ce3a38b8fe53bb0cfdb37d3dd/tests/acceptance/support/sandbox.rs#L680 - // by the Volta Contributors - license information can be found in - // the LEGAL_DETAILS file in the root directory of this distribution. - // - // Thank you, Volta contributors! - let mut path = env::var_os("CARGO_BIN_PATH") - .map(PathBuf::from) - .or_else(|| { - env::current_exe().ok().map(|mut path| { - path.pop(); - if path.ends_with("deps") { - path.pop(); - } - path - }) - }) - .unwrap_or_else(|| panic!("CARGO_BIN_PATH wasn't set, and couldn't be inferred from context. Can't run CLI tests.")); - - path.push(binary_name); - - path -} - -pub fn strip_colors(str: &str) -> String { - use roc_reporting::report::ANSI_STYLE_CODES; - - str.replace(ANSI_STYLE_CODES.red, "") - .replace(ANSI_STYLE_CODES.green, "") - .replace(ANSI_STYLE_CODES.yellow, "") - .replace(ANSI_STYLE_CODES.blue, "") - .replace(ANSI_STYLE_CODES.magenta, "") - .replace(ANSI_STYLE_CODES.cyan, "") - .replace(ANSI_STYLE_CODES.white, "") - .replace(ANSI_STYLE_CODES.bold, "") - .replace(ANSI_STYLE_CODES.underline, "") - .replace(ANSI_STYLE_CODES.reset, "") - .replace(ANSI_STYLE_CODES.color_reset, "") -} - -pub fn run_with_stdin(path: &Path, args: I, stdin_vals: &[&str]) -> Out -where - I: IntoIterator, - S: AsRef, -{ - let mut cmd = Command::new(path); - - for arg in args { - cmd.arg(arg); - } - - let mut child = cmd - .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .spawn() - .unwrap_or_else(|err| { - panic!( - "failed to execute compiled binary {} in CLI test: {err}", - path.to_string_lossy() - ) - }); - - { - let stdin = child.stdin.as_mut().expect("Failed to open stdin"); - - for stdin_str in stdin_vals.iter() { - stdin - .write_all(stdin_str.as_bytes()) - .expect("Failed to write to stdin"); - } - } - - let output = child - .wait_with_output() - .expect("failed to get output for compiled binary in CLI test"); - - Out { - stdout: String::from_utf8(output.stdout).unwrap(), - stderr: String::from_utf8(output.stderr).unwrap(), - status: output.status, - } -} - -pub fn run_cmd<'a, I: IntoIterator>( - cmd_name: &str, - stdin_vals: I, - args: &[&str], -) -> Out { - let mut cmd = Command::new(cmd_name); - - for arg in args { - cmd.arg(arg); - } - - let mut child = cmd - .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .spawn() - .unwrap_or_else(|_| panic!("failed to execute cmd `{}` in CLI test", cmd_name)); - - { - let stdin = child.stdin.as_mut().expect("Failed to open stdin"); - - for stdin_str in stdin_vals { - stdin - .write_all(stdin_str.as_bytes()) - .expect("Failed to write to stdin"); - } - } - - let output = child - .wait_with_output() - .unwrap_or_else(|_| panic!("failed to execute cmd `{}` in CLI test", cmd_name)); - - Out { - stdout: String::from_utf8(output.stdout).unwrap(), - stderr: String::from_utf8(output.stderr).unwrap(), - status: output.status, - } -} - -pub fn run_with_valgrind<'a, I: IntoIterator>( - stdin_vals: I, - args: &[&str], -) -> (Out, String) { - //TODO: figure out if there is a better way to get the valgrind executable. - let mut cmd = Command::new("valgrind"); - let named_tempfile = - NamedTempFile::new().expect("Unable to create tempfile for valgrind results"); - let filepath = named_tempfile.path().to_str().unwrap(); - - cmd.arg("--tool=memcheck"); - cmd.arg("--xml=yes"); - - // If you are having valgrind issues on MacOS, you may need to suppress some - // of the errors. Read more here: https://github.com/rtfeldman/roc/issues/746 - if let Some(suppressions_file_os_str) = env::var_os("VALGRIND_SUPPRESSIONS") { - match suppressions_file_os_str.to_str() { - None => { - panic!("Could not determine suppression file location from OsStr"); - } - Some(suppressions_file) => { - let mut buf = String::new(); - - buf.push_str("--suppressions="); - buf.push_str(suppressions_file); - - cmd.arg(buf); - } - } - } - - cmd.arg(format!("--xml-file={}", filepath)); - - for arg in args { - cmd.arg(arg); - } - - cmd.stdin(Stdio::piped()); - cmd.stdout(Stdio::piped()); - cmd.stderr(Stdio::piped()); - - let mut child = cmd - .spawn() - .expect("failed to execute compiled `valgrind` binary in CLI test"); - - { - let stdin = child.stdin.as_mut().expect("Failed to open stdin"); - - for stdin_str in stdin_vals { - stdin - .write_all(stdin_str.as_bytes()) - .expect("Failed to write to stdin"); - } - } - - let output = child - .wait_with_output() - .expect("failed to execute compiled `valgrind` binary in CLI test"); - - let mut file = named_tempfile.into_file(); - let mut raw_xml = String::new(); - - file.read_to_string(&mut raw_xml).unwrap(); - - ( - Out { - stdout: String::from_utf8(output.stdout).unwrap(), - stderr: String::from_utf8(output.stderr).unwrap(), - status: output.status, - }, - raw_xml, - ) -} - -#[derive(Debug, Deserialize)] -struct ValgrindOutput { - #[serde(rename = "$value")] - pub fields: Vec, -} - -#[derive(Deserialize, Debug)] -#[serde(rename_all = "lowercase")] -enum ValgrindField { - ProtocolVersion(isize), - ProtocolTool(String), - Preamble(ValgrindDummyStruct), - Pid(isize), - PPid(isize), - Tool(String), - Args(ValgrindDummyStruct), - Error(ValgrindError), - Status(ValgrindDummyStruct), - Stack(ValgrindDummyStruct), - #[serde(rename = "fatal_signal")] - FatalSignal(ValgrindDummyStruct), - ErrorCounts(ValgrindDummyStruct), - SuppCounts(ValgrindDummyStruct), -} - -#[derive(Debug, Deserialize)] -struct ValgrindDummyStruct {} - -#[derive(Debug, Deserialize, Clone)] -pub struct ValgrindError { - pub kind: String, - #[serde(default)] - pub what: Option, - #[serde(default)] - pub xwhat: Option, -} - -#[derive(Debug, Deserialize, Clone)] -pub struct ValgrindErrorXWhat { - pub text: String, - #[serde(default)] - pub leakedbytes: Option, - #[serde(default)] - pub leakedblocks: Option, -} - -#[allow(dead_code)] -pub fn extract_valgrind_errors(xml: &str) -> Result, serde_xml_rs::Error> { - let parsed_xml: ValgrindOutput = from_str(xml)?; - let answer = parsed_xml - .fields - .iter() - .filter_map(|field| match field { - ValgrindField::Error(err) => Some(err.clone()), - _ => None, - }) - .collect(); - - Ok(answer) -} - -#[allow(dead_code)] -pub fn root_dir() -> PathBuf { - let mut path = env::current_exe().ok().unwrap(); - - // Get rid of the filename in target/debug/deps/cli_run-99c65e4e9a1fbd06 - path.pop(); - - // If we're in deps/ get rid of deps/ in target/debug/deps/ - if path.ends_with("deps") { - path.pop(); - } - - // Get rid of target/debug/ so we're back at the project root - path.pop(); - path.pop(); - - path -} - -#[allow(dead_code)] -pub fn examples_dir(dir_name: &str) -> PathBuf { - let mut path = root_dir(); - - // Descend into examples/{dir_name} - path.push("examples"); - path.push(dir_name); - - path -} - -#[allow(dead_code)] -pub fn example_file(dir_name: &str, file_name: &str) -> PathBuf { - let mut path = examples_dir(dir_name); - - path.push(file_name); - - path -} - -#[allow(dead_code)] -pub fn fixtures_dir(dir_name: &str) -> PathBuf { - let mut path = root_dir(); - - // Descend into cli/tests/fixtures/{dir_name} - path.push("cli"); - path.push("tests"); - path.push("fixtures"); - path.push(dir_name); - - path -} - -#[allow(dead_code)] -pub fn fixture_file(dir_name: &str, file_name: &str) -> PathBuf { - let mut path = fixtures_dir(dir_name); - - path.push(file_name); - - path -} - -#[allow(dead_code)] -pub fn known_bad_file(file_name: &str) -> PathBuf { - let mut path = root_dir(); - - // Descend into cli/tests/known_bad/{file_name} - path.push("cli"); - path.push("tests"); - path.push("known_bad"); - path.push(file_name); - - path -} diff --git a/cli_utils/src/lib.rs b/cli_utils/src/lib.rs deleted file mode 100644 index b5cded1f52..0000000000 --- a/cli_utils/src/lib.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod bench_utils; -pub mod helpers; diff --git a/code_markup/Cargo.toml b/code_markup/Cargo.toml deleted file mode 100644 index 0a18868e9b..0000000000 --- a/code_markup/Cargo.toml +++ /dev/null @@ -1,17 +0,0 @@ -[package] -name = "roc_code_markup" -version = "0.1.0" -authors = ["The Roc Contributors"] -license = "UPL-1.0" -edition = "2021" -description = "Our own markup language for Roc code. Used by the editor and the docs." - -[dependencies] -roc_ast = { path = "../ast" } -roc_module = { path = "../compiler/module" } -roc_utils = { path = "../utils" } -serde = { version = "1.0.130", features = ["derive"] } -palette = "0.6.0" -snafu = { version = "0.6.10", features = ["backtraces"] } -bumpalo = { version = "3.8.0", features = ["collections"] } -itertools = "0.10.1" diff --git a/code_markup/src/colors.rs b/code_markup/src/colors.rs deleted file mode 100644 index bf64fd7b95..0000000000 --- a/code_markup/src/colors.rs +++ /dev/null @@ -1,22 +0,0 @@ -use palette::{FromColor, Hsv, LinSrgb, Srgb}; - -pub type RgbaTup = (f32, f32, f32, f32); -pub const WHITE: RgbaTup = (1.0, 1.0, 1.0, 1.0); - -pub fn to_slice((r, g, b, a): RgbaTup) -> [f32; 4] { - [r, g, b, a] -} - -pub fn from_hsb(hue: usize, saturation: usize, brightness: usize) -> RgbaTup { - from_hsba(hue, saturation, brightness, 1.0) -} - -pub fn from_hsba(hue: usize, saturation: usize, brightness: usize, alpha: f32) -> RgbaTup { - let rgb = LinSrgb::from(Srgb::from_color(Hsv::new( - hue as f32, - (saturation as f32) / 100.0, - (brightness as f32) / 100.0, - ))); - - (rgb.red, rgb.green, rgb.blue, alpha) -} diff --git a/code_markup/src/lib.rs b/code_markup/src/lib.rs deleted file mode 100644 index 1b140a9678..0000000000 --- a/code_markup/src/lib.rs +++ /dev/null @@ -1,6 +0,0 @@ -pub mod colors; -pub mod markup; -pub mod markup_error; -pub mod slow_pool; -pub mod syntax_highlight; -pub mod underline_style; diff --git a/code_markup/src/markup/attribute.rs b/code_markup/src/markup/attribute.rs deleted file mode 100644 index 58f57bc206..0000000000 --- a/code_markup/src/markup/attribute.rs +++ /dev/null @@ -1,124 +0,0 @@ -#![allow(dead_code)] -use snafu::ensure; - -use crate::markup_error::{CaretNotFound, MarkResult}; - -#[derive(Debug, Copy, Clone)] -pub struct Caret { - pub offset_col: usize, -} - -impl Caret { - pub fn new_attr(offset_col: usize) -> Attribute { - Attribute::Caret { - caret: Caret { offset_col }, - } - } -} -#[derive(Debug)] -pub struct SelectionStart { - offset_col: usize, -} -#[derive(Debug)] -pub struct SelectionEnd { - offset_col: usize, -} - -// Highlight is used for example when searching for a specific string to highlight all search results in the module -#[derive(Debug)] -pub struct HighlightStart { - offset_col: usize, -} -#[derive(Debug)] -pub struct HighlightEnd { - offset_col: usize, -} - -// Underline is used for warnings and errors -#[derive(Debug)] -pub struct UnderlineStart { - offset_col: usize, -} -#[derive(Debug)] -pub struct UnderlineEnd { - offset_col: usize, -} - -#[derive(Debug)] -pub enum Attribute { - // Rust does not yet support types for enum variants so we have to do it like this - Caret { caret: Caret }, - - SelectionStart { selection_start: SelectionStart }, - SelectionEnd { selection_end: SelectionEnd }, - - HighlightStart { highlight_start: HighlightStart }, - HighlightEnd { highlight_end: HighlightEnd }, - - Underline { underline_spec: UnderlineSpec }, -} - -#[derive(Debug)] -pub enum UnderlineSpec { - Partial { start: usize, end: usize }, - Full, -} - -#[derive(Debug, Default)] -pub struct Attributes { - pub all: Vec, -} - -impl Attributes { - pub fn add(&mut self, attr: Attribute) { - self.all.push(attr); - } - - pub fn add_caret(&mut self, offset_col: usize) { - self.all.push(Attribute::Caret { - caret: Caret { offset_col }, - }); - } - - pub fn get_mut_carets(&mut self) -> Vec<&mut Caret> { - let mut carets = Vec::new(); - - for attr in self.all.iter_mut() { - if let Attribute::Caret { caret } = attr { - carets.push(caret) - } - } - - carets - } - - pub fn get_carets(&self) -> Vec { - let mut carets = Vec::new(); - - for attr in self.all.iter() { - if let Attribute::Caret { caret } = attr { - carets.push(*caret) - } - } - - carets - } - - pub fn delete_caret(&mut self, offset_col: usize, node_id: usize) -> MarkResult<()> { - let old_len = self.all.len(); - - self.all.retain(|attr| { - if let Attribute::Caret { caret } = attr { - caret.offset_col != offset_col - } else { - true - } - }); - - let new_len = self.all.len(); - - ensure!(old_len != new_len, CaretNotFound { node_id }); - - Ok(()) - } -} diff --git a/code_markup/src/markup/common_nodes.rs b/code_markup/src/markup/common_nodes.rs deleted file mode 100644 index 0309815f2f..0000000000 --- a/code_markup/src/markup/common_nodes.rs +++ /dev/null @@ -1,167 +0,0 @@ -use crate::{ - slow_pool::{MarkNodeId, SlowPool}, - syntax_highlight::HighlightStyle, -}; - -use super::{ - attribute::Attributes, - nodes::MarkupNode, - nodes::{self, make_nested_mn}, -}; - -pub fn new_equals_mn() -> MarkupNode { - common_text_node(nodes::EQUALS.to_owned(), HighlightStyle::Operator, 0) -} - -pub fn new_comma_mn() -> MarkupNode { - common_text_node(nodes::COMMA.to_owned(), HighlightStyle::Operator, 0) -} - -pub fn new_dot_mn() -> MarkupNode { - common_text_node(nodes::DOT.to_owned(), HighlightStyle::Operator, 0) -} - -pub fn new_blank_mn() -> MarkupNode { - MarkupNode::Blank { - attributes: Attributes::default(), - parent_id_opt: None, - newlines_at_end: 0, - } -} - -pub fn new_blank_mn_w_nls(nr_of_newlines: usize) -> MarkupNode { - MarkupNode::Blank { - attributes: Attributes::default(), - parent_id_opt: None, - newlines_at_end: nr_of_newlines, - } -} - -pub fn new_colon_mn() -> MarkupNode { - new_operator_mn(nodes::COLON.to_owned()) -} - -pub fn new_operator_mn(content: String) -> MarkupNode { - common_text_node(content, HighlightStyle::Operator, 0) -} - -pub fn new_left_accolade_mn() -> MarkupNode { - common_text_node(nodes::LEFT_ACCOLADE.to_owned(), HighlightStyle::Bracket, 0) -} - -pub fn new_right_accolade_mn() -> MarkupNode { - common_text_node(nodes::RIGHT_ACCOLADE.to_owned(), HighlightStyle::Bracket, 0) -} - -pub fn new_left_square_mn() -> MarkupNode { - common_text_node(nodes::LEFT_SQUARE_BR.to_owned(), HighlightStyle::Bracket, 0) -} - -pub fn new_right_square_mn() -> MarkupNode { - common_text_node( - nodes::RIGHT_SQUARE_BR.to_owned(), - HighlightStyle::Bracket, - 0, - ) -} - -pub fn new_func_name_mn(content: String) -> MarkupNode { - common_text_node(content, HighlightStyle::FunctionName, 0) -} - -pub fn new_arg_name_mn(content: String) -> MarkupNode { - common_text_node(content, HighlightStyle::FunctionArgName, 0) -} - -pub fn new_arrow_mn(newlines_at_end: usize) -> MarkupNode { - common_text_node( - nodes::ARROW.to_owned(), - HighlightStyle::Operator, - newlines_at_end, - ) -} - -pub fn new_comments_mn(comment: String, newlines_at_end: usize) -> MarkupNode { - common_text_node(comment, HighlightStyle::Comment, newlines_at_end) -} - -fn common_text_node( - content: String, - highlight_style: HighlightStyle, - newlines_at_end: usize, -) -> MarkupNode { - MarkupNode::Text { - content, - syn_high_style: highlight_style, - attributes: Attributes::default(), - parent_id_opt: None, - newlines_at_end, - } -} - -pub const NEW_LINES_AFTER_DEF: usize = 2; - -pub fn new_assign_mn( - val_name_mn_id: MarkNodeId, - equals_mn_id: MarkNodeId, - expr_mark_node_id: MarkNodeId, -) -> MarkupNode { - make_nested_mn( - vec![val_name_mn_id, equals_mn_id, expr_mark_node_id], - NEW_LINES_AFTER_DEF, - ) -} - -pub fn new_module_name_mn_id(mn_ids: Vec, mark_node_pool: &mut SlowPool) -> MarkNodeId { - if mn_ids.len() == 1 { - *mn_ids.get(0).unwrap() // safe because we checked the length before - } else { - let nested_node = make_nested_mn(mn_ids, 0); - mark_node_pool.add(nested_node) - } -} - -pub fn new_module_var_mn( - module_name_id: MarkNodeId, - dot_id: MarkNodeId, - ident_id: MarkNodeId, -) -> MarkupNode { - make_nested_mn(vec![module_name_id, dot_id, ident_id], 0) -} - -pub fn if_mn() -> MarkupNode { - keyword_mn("if ") -} - -pub fn then_mn() -> MarkupNode { - keyword_mn(" then ") -} - -pub fn else_mn() -> MarkupNode { - keyword_mn(" else ") -} - -fn keyword_mn(keyword: &str) -> MarkupNode { - common_text_node(keyword.to_owned(), HighlightStyle::Keyword, 0) -} - -pub fn new_if_expr_mn( - if_mn_id: MarkNodeId, - cond_expr_mn_id: MarkNodeId, - then_mn_id: MarkNodeId, - then_expr_mn_id: MarkNodeId, - else_mn_id: MarkNodeId, - else_expr_mn_id: MarkNodeId, -) -> MarkupNode { - make_nested_mn( - vec![ - if_mn_id, - cond_expr_mn_id, - then_mn_id, - then_expr_mn_id, - else_mn_id, - else_expr_mn_id, - ], - 1, - ) -} diff --git a/code_markup/src/markup/convert/from_ast.rs b/code_markup/src/markup/convert/from_ast.rs deleted file mode 100644 index 4c3d470b04..0000000000 --- a/code_markup/src/markup/convert/from_ast.rs +++ /dev/null @@ -1,50 +0,0 @@ -use roc_ast::{ - ast_error::ASTResult, - lang::{core::ast::AST, env::Env}, -}; -use roc_module::symbol::Interns; - -use crate::{ - markup::{ - convert::{from_def2::def2_to_markup, from_header::header_to_markup}, - mark_id_ast_id_map::MarkIdAstIdMap, - nodes::set_parent_for_all, - }, - slow_pool::{MarkNodeId, SlowPool}, -}; - -pub fn ast_to_mark_nodes<'a>( - env: &mut Env<'a>, - ast: &AST, - mark_node_pool: &mut SlowPool, - interns: &Interns, -) -> ASTResult<(Vec, MarkIdAstIdMap)> { - let mut mark_id_ast_id_map = MarkIdAstIdMap::default(); - let mut all_mark_node_ids = vec![header_to_markup( - &ast.header, - mark_node_pool, - &mut mark_id_ast_id_map, - )]; - - for &def_id in ast.def_ids.iter() { - // for debugging - //println!("{}", def2_to_string(def_id, env.pool)); - - let def2 = env.pool.get(def_id); - - let expr2_markup_id = def2_to_markup( - env, - def2, - def_id, - mark_node_pool, - &mut mark_id_ast_id_map, - interns, - )?; - - set_parent_for_all(expr2_markup_id, mark_node_pool); - - all_mark_node_ids.push(expr2_markup_id); - } - - Ok((all_mark_node_ids, mark_id_ast_id_map)) -} diff --git a/code_markup/src/markup/convert/from_def2.rs b/code_markup/src/markup/convert/from_def2.rs deleted file mode 100644 index 5e7e2ce818..0000000000 --- a/code_markup/src/markup/convert/from_def2.rs +++ /dev/null @@ -1,137 +0,0 @@ -use crate::{ - markup::{ - common_nodes::new_blank_mn_w_nls, - mark_id_ast_id_map::MarkIdAstIdMap, - nodes::MarkupNode, - top_level_def::{assignment_mark_node, tld_w_comments_mark_node}, - }, - slow_pool::{MarkNodeId, SlowPool}, -}; - -use super::from_expr2::expr2_to_markup; - -use roc_ast::{ - ast_error::ASTResult, - lang::{ - core::{ - ast::ASTNodeId, - def::def2::{Def2, DefId}, - }, - env::Env, - }, -}; -use roc_module::symbol::Interns; - -pub fn add_node( - mark_node: MarkupNode, - ast_node_id: ASTNodeId, - mark_node_pool: &mut SlowPool, - mark_id_ast_id_map: &mut MarkIdAstIdMap, -) -> MarkNodeId { - let mark_node_id = mark_node_pool.add(mark_node); - - mark_id_ast_id_map.insert(mark_node_id, ast_node_id); - - mark_node_id -} - -pub fn def2_to_markup<'a>( - env: &mut Env<'a>, - def2: &Def2, - def2_node_id: DefId, - mark_node_pool: &mut SlowPool, - mark_id_ast_id_map: &mut MarkIdAstIdMap, - interns: &Interns, -) -> ASTResult { - let ast_node_id = ASTNodeId::ADefId(def2_node_id); - - let mark_node_id = match def2 { - Def2::ValueDef { - identifier_id, - expr_id, - } => { - let expr_mn_id = expr2_to_markup( - env, - env.pool.get(*expr_id), - *expr_id, - mark_node_pool, - mark_id_ast_id_map, - interns, - 0, - )?; - - let tld_mn = assignment_mark_node( - *identifier_id, - expr_mn_id, - ast_node_id, - mark_node_pool, - mark_id_ast_id_map, - env, - )?; - - add_node(tld_mn, ast_node_id, mark_node_pool, mark_id_ast_id_map) - } - Def2::Blank => add_node( - new_blank_mn_w_nls(2), - ast_node_id, - mark_node_pool, - mark_id_ast_id_map, - ), - Def2::CommentsBefore { comments, def_id } => { - let inner_def = env.pool.get(*def_id); - let inner_def_mark_node_id = def2_to_markup( - env, - inner_def, - *def_id, - mark_node_pool, - mark_id_ast_id_map, - interns, - )?; - - let full_mark_node = tld_w_comments_mark_node( - comments.clone(), - inner_def_mark_node_id, - ast_node_id, - mark_node_pool, - mark_id_ast_id_map, - true, - )?; - - add_node( - full_mark_node, - ast_node_id, - mark_node_pool, - mark_id_ast_id_map, - ) - } - Def2::CommentsAfter { def_id, comments } => { - let inner_def = env.pool.get(*def_id); - let inner_def_mark_node_id = def2_to_markup( - env, - inner_def, - *def_id, - mark_node_pool, - mark_id_ast_id_map, - interns, - )?; - - let full_mark_node = tld_w_comments_mark_node( - comments.clone(), - inner_def_mark_node_id, - ast_node_id, - mark_node_pool, - mark_id_ast_id_map, - false, - )?; - - add_node( - full_mark_node, - ast_node_id, - mark_node_pool, - mark_id_ast_id_map, - ) - } - }; - - Ok(mark_node_id) -} diff --git a/code_markup/src/markup/convert/from_expr2.rs b/code_markup/src/markup/convert/from_expr2.rs deleted file mode 100644 index c859e5b8af..0000000000 --- a/code_markup/src/markup/convert/from_expr2.rs +++ /dev/null @@ -1,497 +0,0 @@ -use crate::{ - markup::{ - attribute::Attributes, - common_nodes::{ - new_arg_name_mn, new_arrow_mn, new_blank_mn, new_colon_mn, new_comma_mn, new_equals_mn, - new_left_accolade_mn, new_left_square_mn, new_operator_mn, new_right_accolade_mn, - new_right_square_mn, - }, - mark_id_ast_id_map::MarkIdAstIdMap, - nodes::{ - get_string, join_mark_nodes_commas, join_mark_nodes_spaces, new_markup_node, MarkupNode, - }, - }, - slow_pool::{MarkNodeId, SlowPool}, - syntax_highlight::HighlightStyle, -}; - -use itertools::Itertools; -use roc_ast::{ - ast_error::ASTResult, - lang::{ - core::{ - ast::ASTNodeId, - expr::{ - expr2::{Expr2, ExprId}, - record_field::RecordField, - }, - pattern::{get_identifier_string, Pattern2}, - val_def::ValueDef, - }, - env::Env, - }, -}; -use roc_module::{module_err::ModuleResult, symbol::Interns}; - -use super::from_def2::add_node; - -// make Markup Nodes: generate String representation, assign Highlighting Style -pub fn expr2_to_markup<'a>( - env: &Env<'a>, - expr2: &Expr2, - expr2_node_id: ExprId, - mark_node_pool: &mut SlowPool, - mark_id_ast_id_map: &mut MarkIdAstIdMap, - interns: &Interns, - indent_level: usize, -) -> ASTResult { - let ast_node_id = ASTNodeId::AExprId(expr2_node_id); - - // for debugging - //println!("EXPR2 {:?}", expr2); - - let mark_node_id = match expr2 { - Expr2::SmallInt { text, .. } - | Expr2::I128 { text, .. } - | Expr2::U128 { text, .. } - | Expr2::Float { text, .. } => { - let num_str = get_string(env, text); - - new_markup_node( - with_indent(indent_level, &num_str), - ast_node_id, - HighlightStyle::Number, - mark_node_pool, - mark_id_ast_id_map, - indent_level, - ) - } - Expr2::Str(text) => { - let content = format!("\"{}\"", text.as_str(env.pool)); - - string_mark_node( - &content, - indent_level, - ast_node_id, - mark_node_pool, - mark_id_ast_id_map, - ) - } - Expr2::SmallStr(array_str) => { - let content = format!("\"{}\"", array_str.as_str()); - - string_mark_node( - &content, - indent_level, - ast_node_id, - mark_node_pool, - mark_id_ast_id_map, - ) - } - Expr2::Tag { name, .. } => new_markup_node( - with_indent(indent_level, &get_string(env, name)), - ast_node_id, - HighlightStyle::Type, - mark_node_pool, - mark_id_ast_id_map, - indent_level, - ), - Expr2::Call { args, expr_id, .. } => { - let expr = env.pool.get(*expr_id); - let fun_call_mark_id = expr2_to_markup( - env, - expr, - *expr_id, - mark_node_pool, - mark_id_ast_id_map, - interns, - indent_level, - )?; - - let arg_expr_ids: Vec = - args.iter(env.pool).map(|(_, arg_id)| *arg_id).collect(); - - let arg_call_mark_ids: Vec = arg_expr_ids - .iter() - .map(|arg_id| { - let arg_expr = env.pool.get(*arg_id); - - expr2_to_markup( - env, - arg_expr, - *arg_id, - mark_node_pool, - mark_id_ast_id_map, - interns, - 0, - ) - }) - .collect::>>()?; - - let mut args_with_sapces = - join_mark_nodes_spaces(arg_call_mark_ids, true, mark_node_pool); - - let mut children_ids = vec![fun_call_mark_id]; - children_ids.append(&mut args_with_sapces); - - let call_node = MarkupNode::Nested { - children_ids, - parent_id_opt: None, - newlines_at_end: 0, - }; - - add_node(call_node, ast_node_id, mark_node_pool, mark_id_ast_id_map) - } - Expr2::Var(symbol) => { - let text = symbol.fully_qualified(interns, env.home); - - new_markup_node( - text.to_string(), - ast_node_id, - HighlightStyle::Value, - mark_node_pool, - mark_id_ast_id_map, - indent_level, - ) - } - Expr2::List { elems, .. } => { - let mut children_ids = vec![add_node( - new_left_square_mn(), - ast_node_id, - mark_node_pool, - mark_id_ast_id_map, - )]; - - let indexed_node_ids: Vec<(usize, ExprId)> = - elems.iter(env.pool).copied().enumerate().collect(); - - for (idx, node_id) in indexed_node_ids.iter() { - let sub_expr2 = env.pool.get(*node_id); - - children_ids.push(expr2_to_markup( - env, - sub_expr2, - *node_id, - mark_node_pool, - mark_id_ast_id_map, - interns, - indent_level, - )?); - - if idx + 1 < elems.len() { - children_ids.push(add_node( - new_comma_mn(), - ast_node_id, - mark_node_pool, - mark_id_ast_id_map, - )); - } - } - children_ids.push(add_node( - new_right_square_mn(), - ast_node_id, - mark_node_pool, - mark_id_ast_id_map, - )); - - let list_mn = MarkupNode::Nested { - children_ids, - parent_id_opt: None, - newlines_at_end: 0, - }; - - add_node(list_mn, ast_node_id, mark_node_pool, mark_id_ast_id_map) - } - Expr2::EmptyRecord => { - let children_ids = vec![ - add_node( - new_left_accolade_mn(), - ast_node_id, - mark_node_pool, - mark_id_ast_id_map, - ), - add_node( - new_right_accolade_mn(), - ast_node_id, - mark_node_pool, - mark_id_ast_id_map, - ), - ]; - - let record_mn = MarkupNode::Nested { - children_ids, - parent_id_opt: None, - newlines_at_end: 0, - }; - - add_node(record_mn, ast_node_id, mark_node_pool, mark_id_ast_id_map) - } - Expr2::Record { fields, .. } => { - let mut children_ids = vec![add_node( - new_left_accolade_mn(), - ast_node_id, - mark_node_pool, - mark_id_ast_id_map, - )]; - - for (idx, field_node_id) in fields.iter_node_ids().enumerate() { - let record_field = env.pool.get(field_node_id); - - let field_name = record_field.get_record_field_pool_str(); - - children_ids.push(new_markup_node( - field_name.as_str(env.pool).to_owned(), - ast_node_id, - HighlightStyle::RecordField, - mark_node_pool, - mark_id_ast_id_map, - indent_level, - )); - - match record_field { - RecordField::InvalidLabelOnly(_, _) => (), - RecordField::LabelOnly(_, _, _) => (), - RecordField::LabeledValue(_, _, sub_expr2_node_id) => { - children_ids.push(add_node( - new_colon_mn(), - ast_node_id, - mark_node_pool, - mark_id_ast_id_map, - )); - - let sub_expr2 = env.pool.get(*sub_expr2_node_id); - children_ids.push(expr2_to_markup( - env, - sub_expr2, - *sub_expr2_node_id, - mark_node_pool, - mark_id_ast_id_map, - interns, - indent_level, - )?); - } - } - - if idx + 1 < fields.len() { - children_ids.push(add_node( - new_comma_mn(), - ast_node_id, - mark_node_pool, - mark_id_ast_id_map, - )); - } - } - - children_ids.push(add_node( - new_right_accolade_mn(), - ast_node_id, - mark_node_pool, - mark_id_ast_id_map, - )); - - let record_mn = MarkupNode::Nested { - children_ids, - parent_id_opt: None, - newlines_at_end: 0, - }; - - add_node(record_mn, ast_node_id, mark_node_pool, mark_id_ast_id_map) - } - Expr2::Blank => add_node( - new_blank_mn(), - ast_node_id, - mark_node_pool, - mark_id_ast_id_map, - ), - Expr2::LetValue { - def_id, - body_id: _, - body_var: _, - } => { - let pattern_id = env.pool.get(*def_id).get_pattern_id(); - - let pattern2 = env.pool.get(pattern_id); - - let val_name = get_identifier_string(pattern2, interns)?; - - let val_name_mn = MarkupNode::Text { - content: val_name, - syn_high_style: HighlightStyle::Value, - attributes: Attributes::default(), - parent_id_opt: None, - newlines_at_end: 0, - }; - - let val_name_mn_id = - add_node(val_name_mn, ast_node_id, mark_node_pool, mark_id_ast_id_map); - - let equals_mn_id = add_node( - new_equals_mn(), - ast_node_id, - mark_node_pool, - mark_id_ast_id_map, - ); - - let value_def = env.pool.get(*def_id); - - match value_def { - ValueDef::NoAnnotation { - pattern_id: _, - expr_id, - expr_var: _, - } => { - let body_mn_id = expr2_to_markup( - env, - env.pool.get(*expr_id), - *expr_id, - mark_node_pool, - mark_id_ast_id_map, - interns, - indent_level, - )?; - - let body_mn = mark_node_pool.get_mut(body_mn_id); - body_mn.add_newline_at_end(); - - let full_let_mn = MarkupNode::Nested { - children_ids: vec![val_name_mn_id, equals_mn_id, body_mn_id], - parent_id_opt: None, - newlines_at_end: 1, - }; - - add_node(full_let_mn, ast_node_id, mark_node_pool, mark_id_ast_id_map) - } - other => { - unimplemented!( - "I don't know how to convert {:?} into a MarkupNode yet.", - other - ) - } - } - } - Expr2::Closure { - function_type: _, - uniq_symbol: _, - recursive: _, - args, - body_id, - extra: _, - } => { - let backslash_mn = new_operator_mn("\\".to_string()); - let backslash_mn_id = add_node( - backslash_mn, - ast_node_id, - mark_node_pool, - mark_id_ast_id_map, - ); - - let arg_names: Vec<&str> = args - .iter(env.pool) - .map(|(_, arg_node_id)| { - let arg_pattern2 = env.pool.get(*arg_node_id); - - match arg_pattern2 { - Pattern2::Identifier(id_symbol) => { - let ident_id = id_symbol.ident_id(); - - env.ident_ids.get_name_str_res(ident_id) - } - Pattern2::Shadowed { shadowed_ident } => { - Ok(shadowed_ident.as_str(env.pool)) - } - other => { - todo!( - "TODO: support the following pattern2 as function arg: {:?}", - other - ); - } - } - }) - .collect::>>()?; - - let arg_mark_nodes = arg_names - .iter() - .map(|arg_name| new_arg_name_mn(arg_name.to_string())) - .collect_vec(); - - let args_with_commas: Vec = join_mark_nodes_commas(arg_mark_nodes); - - let mut args_with_commas_ids: Vec = args_with_commas - .into_iter() - .map(|mark_node| { - add_node(mark_node, ast_node_id, mark_node_pool, mark_id_ast_id_map) - }) - .collect(); - - let arrow_mn = new_arrow_mn(1); - let arrow_mn_id = add_node(arrow_mn, ast_node_id, mark_node_pool, mark_id_ast_id_map); - - let mut children_ids = vec![backslash_mn_id]; - children_ids.append(&mut args_with_commas_ids); - children_ids.push(arrow_mn_id); - - let args_mn = MarkupNode::Nested { - children_ids, - parent_id_opt: None, - newlines_at_end: 0, - }; - let args_mn_id = add_node(args_mn, ast_node_id, mark_node_pool, mark_id_ast_id_map); - - let body_expr = env.pool.get(*body_id); - let body_mn_id = expr2_to_markup( - env, - body_expr, - *body_id, - mark_node_pool, - mark_id_ast_id_map, - interns, - indent_level + 1, - )?; - - let function_mn = MarkupNode::Nested { - children_ids: vec![args_mn_id, body_mn_id], - parent_id_opt: None, - newlines_at_end: 0, - }; - - add_node(function_mn, ast_node_id, mark_node_pool, mark_id_ast_id_map) - } - Expr2::RuntimeError() => new_markup_node( - "RunTimeError".to_string(), - ast_node_id, - HighlightStyle::Blank, - mark_node_pool, - mark_id_ast_id_map, - indent_level, - ), - rest => todo!("implement expr2_to_markup for {:?}", rest), - }; - - Ok(mark_node_id) -} - -fn with_indent(indent_level: usize, some_str: &str) -> String { - let full_indent = std::iter::repeat(" ").take(indent_level * 4); - let mut full_string: String = full_indent.collect(); - - full_string.push_str(some_str); - - full_string -} - -fn string_mark_node( - content: &str, - indent_level: usize, - ast_node_id: ASTNodeId, - mark_node_pool: &mut SlowPool, - mark_id_ast_id_map: &mut MarkIdAstIdMap, -) -> MarkNodeId { - new_markup_node( - with_indent(indent_level, content), - ast_node_id, - HighlightStyle::String, - mark_node_pool, - mark_id_ast_id_map, - indent_level, - ) -} diff --git a/code_markup/src/markup/convert/from_header.rs b/code_markup/src/markup/convert/from_header.rs deleted file mode 100644 index e7698b321c..0000000000 --- a/code_markup/src/markup/convert/from_header.rs +++ /dev/null @@ -1,305 +0,0 @@ -use roc_ast::lang::core::{ast::ASTNodeId, header::AppHeader}; - -use crate::{ - markup::{ - attribute::Attributes, - common_nodes::{ - new_comma_mn, new_left_accolade_mn, new_left_square_mn, new_right_accolade_mn, - new_right_square_mn, - }, - mark_id_ast_id_map::MarkIdAstIdMap, - nodes::{set_parent_for_all, MarkupNode}, - }, - slow_pool::{MarkNodeId, SlowPool}, - syntax_highlight::HighlightStyle, -}; - -use super::from_def2::add_node; - -pub fn header_to_markup( - app_header: &AppHeader, - mark_node_pool: &mut SlowPool, - mark_id_ast_id_map: &mut MarkIdAstIdMap, -) -> MarkNodeId { - let expr_id = app_header.ast_node_id; - let ast_node_id = ASTNodeId::AExprId(expr_id); - - let app_node_id = header_mn( - "app ".to_owned(), - ast_node_id, - mark_node_pool, - mark_id_ast_id_map, - ); - - let app_name_node_id = header_val_mn( - app_header.app_name.clone(), - ast_node_id, - HighlightStyle::String, - mark_node_pool, - mark_id_ast_id_map, - ); - - let full_app_node = MarkupNode::Nested { - children_ids: vec![app_node_id, app_name_node_id], - parent_id_opt: None, - newlines_at_end: 1, - }; - - let packages_node_id = header_mn( - " packages ".to_owned(), - ast_node_id, - mark_node_pool, - mark_id_ast_id_map, - ); - - let pack_left_acc_node_id = add_node( - new_left_accolade_mn(), - ast_node_id, - mark_node_pool, - mark_id_ast_id_map, - ); - - let pack_base_node_id = header_val_mn( - "base: ".to_owned(), - ast_node_id, - HighlightStyle::RecordField, - mark_node_pool, - mark_id_ast_id_map, - ); - - let pack_val_node_id = header_val_mn( - app_header.packages_base.clone(), - ast_node_id, - HighlightStyle::String, - mark_node_pool, - mark_id_ast_id_map, - ); - - let pack_right_acc_node_id = add_node( - new_right_accolade_mn(), - ast_node_id, - mark_node_pool, - mark_id_ast_id_map, - ); - - let full_packages_node = MarkupNode::Nested { - children_ids: vec![ - packages_node_id, - pack_left_acc_node_id, - pack_base_node_id, - pack_val_node_id, - pack_right_acc_node_id, - ], - parent_id_opt: None, - newlines_at_end: 1, - }; - - let imports_node_id = header_mn( - " imports ".to_owned(), - ast_node_id, - mark_node_pool, - mark_id_ast_id_map, - ); - - let imports_left_square_node_id = add_node( - new_left_square_mn(), - ast_node_id, - mark_node_pool, - mark_id_ast_id_map, - ); - - let mut import_child_ids: Vec = add_header_mn_list( - &app_header.imports, - ast_node_id, - HighlightStyle::Import, - mark_node_pool, - mark_id_ast_id_map, - ); - - let imports_right_square_node_id = add_node( - new_right_square_mn(), - ast_node_id, - mark_node_pool, - mark_id_ast_id_map, - ); - - let mut full_import_children = vec![imports_node_id, imports_left_square_node_id]; - - full_import_children.append(&mut import_child_ids); - full_import_children.push(imports_right_square_node_id); - - let full_import_node = MarkupNode::Nested { - children_ids: full_import_children, - parent_id_opt: None, - newlines_at_end: 1, - }; - - let provides_node_id = header_mn( - " provides ".to_owned(), - ast_node_id, - mark_node_pool, - mark_id_ast_id_map, - ); - - let provides_left_square_node_id = add_node( - new_left_square_mn(), - ast_node_id, - mark_node_pool, - mark_id_ast_id_map, - ); - - let mut provides_val_node_ids: Vec = add_header_mn_list( - &app_header.provides, - ast_node_id, - HighlightStyle::Provides, - mark_node_pool, - mark_id_ast_id_map, - ); - - let provides_right_square_node_id = add_node( - new_right_square_mn(), - ast_node_id, - mark_node_pool, - mark_id_ast_id_map, - ); - - let provides_end_node_id = header_mn( - " to base".to_owned(), - ast_node_id, - mark_node_pool, - mark_id_ast_id_map, - ); - - let mut full_provides_children = vec![provides_node_id, provides_left_square_node_id]; - - full_provides_children.append(&mut provides_val_node_ids); - full_provides_children.push(provides_right_square_node_id); - full_provides_children.push(provides_end_node_id); - - let full_provides_node = MarkupNode::Nested { - children_ids: full_provides_children, - parent_id_opt: None, - newlines_at_end: 1, - }; - - let full_app_node_id = add_node( - full_app_node, - ast_node_id, - mark_node_pool, - mark_id_ast_id_map, - ); - let full_packages_node = add_node( - full_packages_node, - ast_node_id, - mark_node_pool, - mark_id_ast_id_map, - ); - let full_import_node_id = add_node( - full_import_node, - ast_node_id, - mark_node_pool, - mark_id_ast_id_map, - ); - let full_provides_node_id = add_node( - full_provides_node, - ast_node_id, - mark_node_pool, - mark_id_ast_id_map, - ); - - let header_mark_node = MarkupNode::Nested { - children_ids: vec![ - full_app_node_id, - full_packages_node, - full_import_node_id, - full_provides_node_id, - ], - parent_id_opt: None, - newlines_at_end: 1, - }; - - let header_mn_id = add_node( - header_mark_node, - ast_node_id, - mark_node_pool, - mark_id_ast_id_map, - ); - - set_parent_for_all(header_mn_id, mark_node_pool); - - header_mn_id -} - -// Used for provides and imports -fn add_header_mn_list( - str_vec: &[String], - ast_node_id: ASTNodeId, - highlight_style: HighlightStyle, - mark_node_pool: &mut SlowPool, - mark_id_ast_id_map: &mut MarkIdAstIdMap, -) -> Vec { - let nr_of_elts = str_vec.len(); - - str_vec - .iter() - .enumerate() - .flat_map(|(indx, provide_str)| { - let provide_str = header_val_mn( - provide_str.to_owned(), - ast_node_id, - highlight_style, - mark_node_pool, - mark_id_ast_id_map, - ); - - if indx != nr_of_elts - 1 { - vec![ - provide_str, - add_node( - new_comma_mn(), - ast_node_id, - mark_node_pool, - mark_id_ast_id_map, - ), - ] - } else { - vec![provide_str] - } - }) - .collect() -} - -fn header_mn( - content: String, - ast_node_id: ASTNodeId, - mark_node_pool: &mut SlowPool, - mark_id_ast_id_map: &mut MarkIdAstIdMap, -) -> MarkNodeId { - let mark_node = MarkupNode::Text { - content, - syn_high_style: HighlightStyle::PackageRelated, - attributes: Attributes::default(), - parent_id_opt: None, - newlines_at_end: 0, - }; - - add_node(mark_node, ast_node_id, mark_node_pool, mark_id_ast_id_map) -} - -fn header_val_mn( - content: String, - ast_node_id: ASTNodeId, - highlight_style: HighlightStyle, - mark_node_pool: &mut SlowPool, - mark_id_ast_id_map: &mut MarkIdAstIdMap, -) -> MarkNodeId { - let mark_node = MarkupNode::Text { - content, - syn_high_style: highlight_style, - attributes: Attributes::default(), - parent_id_opt: None, - newlines_at_end: 0, - }; - - add_node(mark_node, ast_node_id, mark_node_pool, mark_id_ast_id_map) -} diff --git a/code_markup/src/markup/convert/mod.rs b/code_markup/src/markup/convert/mod.rs deleted file mode 100644 index 57b87c5918..0000000000 --- a/code_markup/src/markup/convert/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub mod from_ast; -pub mod from_def2; -pub mod from_expr2; -pub mod from_header; diff --git a/code_markup/src/markup/mark_id_ast_id_map.rs b/code_markup/src/markup/mark_id_ast_id_map.rs deleted file mode 100644 index ebd928ed87..0000000000 --- a/code_markup/src/markup/mark_id_ast_id_map.rs +++ /dev/null @@ -1,29 +0,0 @@ -use std::collections::HashMap; - -use roc_ast::lang::core::ast::ASTNodeId; - -use crate::markup_error::MarkNodeIdWithoutCorrespondingASTNodeId; -use crate::{markup_error::MarkResult, slow_pool::MarkNodeId}; - -/// A hashmap is wrapped to allow for an easy swap out with more performant alternatives -#[derive(Debug, Default)] -pub struct MarkIdAstIdMap { - map: HashMap, -} - -impl MarkIdAstIdMap { - pub fn insert(&mut self, mn_id: MarkNodeId, ast_id: ASTNodeId) { - self.map.insert(mn_id, ast_id); - } - - pub fn get(&self, mn_id: MarkNodeId) -> MarkResult { - match self.map.get(&mn_id) { - Some(ast_node_id) => Ok(*ast_node_id), - None => MarkNodeIdWithoutCorrespondingASTNodeId { - node_id: mn_id, - keys_str: format!("{:?}", self.map.keys()), - } - .fail(), - } - } -} diff --git a/code_markup/src/markup/mod.rs b/code_markup/src/markup/mod.rs deleted file mode 100644 index f9a4f0f98e..0000000000 --- a/code_markup/src/markup/mod.rs +++ /dev/null @@ -1,6 +0,0 @@ -pub mod attribute; -pub mod common_nodes; -pub mod convert; -pub mod mark_id_ast_id_map; -pub mod nodes; -pub mod top_level_def; diff --git a/code_markup/src/markup/nodes.rs b/code_markup/src/markup/nodes.rs deleted file mode 100644 index 6f226ae130..0000000000 --- a/code_markup/src/markup/nodes.rs +++ /dev/null @@ -1,492 +0,0 @@ -use crate::{ - markup_error::MarkResult, - slow_pool::{MarkNodeId, SlowPool}, - syntax_highlight::HighlightStyle, -}; - -use super::{ - attribute::Attributes, common_nodes::new_comma_mn, convert::from_def2::add_node, - mark_id_ast_id_map::MarkIdAstIdMap, -}; - -use crate::markup_error::{ExpectedTextNode, NestedNodeMissingChild, NestedNodeRequired}; -use itertools::Itertools; -use roc_ast::{ - lang::{core::ast::ASTNodeId, env::Env}, - mem_pool::pool_str::PoolStr, -}; -use roc_utils::{index_of, slice_get}; -use std::fmt; - -#[derive(Debug)] -pub enum MarkupNode { - Nested { - children_ids: Vec, - parent_id_opt: Option, - newlines_at_end: usize, - }, - Text { - content: String, - syn_high_style: HighlightStyle, - attributes: Attributes, - parent_id_opt: Option, - newlines_at_end: usize, - }, - Blank { - attributes: Attributes, - parent_id_opt: Option, - newlines_at_end: usize, - }, - Indent { - indent_level: usize, - parent_id_opt: Option, - }, -} - -impl MarkupNode { - pub fn get_parent_id_opt(&self) -> Option { - match self { - MarkupNode::Nested { parent_id_opt, .. } => *parent_id_opt, - MarkupNode::Text { parent_id_opt, .. } => *parent_id_opt, - MarkupNode::Blank { parent_id_opt, .. } => *parent_id_opt, - MarkupNode::Indent { parent_id_opt, .. } => *parent_id_opt, - } - } - - pub fn get_children_ids(&self) -> Vec { - match self { - MarkupNode::Nested { children_ids, .. } => children_ids.to_vec(), - MarkupNode::Text { .. } => vec![], - MarkupNode::Blank { .. } => vec![], - MarkupNode::Indent { .. } => vec![], - } - } - - pub fn get_sibling_ids(&self, mark_node_pool: &SlowPool) -> Vec { - if let Some(parent_id) = self.get_parent_id_opt() { - let parent_node = mark_node_pool.get(parent_id); - - parent_node.get_children_ids() - } else { - vec![] - } - } - - // return (index of child in list of children, closest ast index of child corresponding to ast node) - pub fn get_child_indices( - &self, - mark_node_id: MarkNodeId, - ast_node_id: ASTNodeId, - mark_id_ast_id_map: &MarkIdAstIdMap, - ) -> MarkResult<(usize, usize)> { - match self { - MarkupNode::Nested { children_ids, .. } => { - let mut mark_child_index_opt: Option = None; - let mut child_ids_with_ast: Vec = Vec::new(); - - for (indx, &mark_child_id) in children_ids.iter().enumerate() { - if mark_child_id == mark_node_id { - mark_child_index_opt = Some(indx); - } - - let child_ast_node_id = mark_id_ast_id_map.get(mark_child_id)?; - // a node that points to the same ast_node as the parent is a ',', '[', ']' - // those are not "real" ast children - if child_ast_node_id != ast_node_id { - child_ids_with_ast.push(mark_child_id) - } - } - - if let Some(child_index) = mark_child_index_opt { - if child_index == (children_ids.len() - 1) { - let ast_child_index = child_ids_with_ast.len(); - - Ok((child_index, ast_child_index)) - } else { - // we want to find the index of the closest ast mark node to child_index - let mut indices_in_mark = vec![]; - - for &c_id in child_ids_with_ast.iter() { - indices_in_mark.push(index_of(c_id, children_ids)?); - } - - let mut last_diff = usize::MAX; - let mut best_index = 0; - - for index in indices_in_mark.iter() { - let curr_diff = - isize::abs((*index as isize) - (child_index as isize)) as usize; - - if curr_diff >= last_diff { - break; - } else { - last_diff = curr_diff; - best_index = *index; - } - } - - let closest_ast_child = slice_get(best_index, children_ids)?; - - let closest_ast_child_index = - index_of(*closest_ast_child, &child_ids_with_ast)?; - - // +1 because we want to insert after ast_child - Ok((child_index, closest_ast_child_index + 1)) - } - } else { - NestedNodeMissingChild { - node_id: mark_node_id, - children_ids: children_ids.clone(), - } - .fail() - } - } - _ => NestedNodeRequired { - node_type: self.node_type_as_string(), - } - .fail(), - } - } - - pub fn get_content(&self) -> String { - match self { - MarkupNode::Nested { .. } => "".to_owned(), - MarkupNode::Text { content, .. } => content.clone(), - MarkupNode::Blank { .. } => BLANK_PLACEHOLDER.to_owned(), - MarkupNode::Indent { indent_level, .. } => SINGLE_INDENT.repeat(*indent_level), - } - } - - // gets content and adds newline from newline_at_end - pub fn get_full_content(&self) -> String { - let mut full_content = self.get_content(); - - for _ in 0..self.get_newlines_at_end() { - full_content.push('\n') - } - - full_content - } - - pub fn get_content_mut(&mut self) -> MarkResult<&mut String> { - match self { - MarkupNode::Text { content, .. } => Ok(content), - _ => ExpectedTextNode { - function_name: "set_content".to_owned(), - node_type: self.node_type_as_string(), - } - .fail(), - } - } - - pub fn is_all_alphanumeric(&self) -> bool { - self.get_content() - .chars() - .all(|chr| chr.is_ascii_alphanumeric()) - } - - pub fn add_child_at_index(&mut self, index: usize, child_id: MarkNodeId) -> MarkResult<()> { - if let MarkupNode::Nested { children_ids, .. } = self { - children_ids.splice(index..index, vec![child_id]); - } else { - NestedNodeRequired { - node_type: self.node_type_as_string(), - } - .fail()?; - } - - Ok(()) - } - - pub fn node_type_as_string(&self) -> String { - let type_str = match self { - MarkupNode::Nested { .. } => "Nested", - MarkupNode::Text { .. } => "Text", - MarkupNode::Blank { .. } => "Blank", - MarkupNode::Indent { .. } => "Indent", - }; - - type_str.to_owned() - } - - pub fn is_blank(&self) -> bool { - matches!(self, MarkupNode::Blank { .. }) - } - - pub fn is_nested(&self) -> bool { - matches!(self, MarkupNode::Nested { .. }) - } - - pub fn get_newlines_at_end(&self) -> usize { - match self { - MarkupNode::Nested { - newlines_at_end, .. - } => *newlines_at_end, - MarkupNode::Text { - newlines_at_end, .. - } => *newlines_at_end, - MarkupNode::Blank { - newlines_at_end, .. - } => *newlines_at_end, - MarkupNode::Indent { .. } => 0, - } - } - - pub fn add_newline_at_end(&mut self) { - match self { - MarkupNode::Nested { - newlines_at_end, .. - } => *newlines_at_end += 1, - MarkupNode::Text { - newlines_at_end, .. - } => *newlines_at_end += 1, - MarkupNode::Blank { - newlines_at_end, .. - } => *newlines_at_end += 1, - _ => {} - } - } -} - -pub fn make_nested_mn(children_ids: Vec, newlines_at_end: usize) -> MarkupNode { - MarkupNode::Nested { - children_ids, - parent_id_opt: None, - newlines_at_end, - } -} - -pub fn get_string<'a>(env: &Env<'a>, pool_str: &PoolStr) -> String { - pool_str.as_str(env.pool).to_owned() -} - -pub const BLANK_PLACEHOLDER: &str = " "; -pub const LEFT_ACCOLADE: &str = "{ "; -pub const RIGHT_ACCOLADE: &str = " }"; -pub const LEFT_SQUARE_BR: &str = "[ "; -pub const RIGHT_SQUARE_BR: &str = " ]"; -pub const COLON: &str = ": "; -pub const COMMA: &str = ", "; -pub const DOT: &str = "."; -pub const STRING_QUOTES: &str = "\"\""; -pub const EQUALS: &str = " = "; -pub const ARROW: &str = " -> "; -pub const SINGLE_INDENT: &str = " "; // 4 spaces - -pub fn new_markup_node( - text: String, - node_id: ASTNodeId, - highlight_style: HighlightStyle, - mark_node_pool: &mut SlowPool, - mark_id_ast_id_map: &mut MarkIdAstIdMap, - indent_level: usize, -) -> MarkNodeId { - let content_node = MarkupNode::Text { - content: text, - syn_high_style: highlight_style, - attributes: Attributes::default(), - parent_id_opt: None, - newlines_at_end: 0, - }; - - let content_node_id = add_node(content_node, node_id, mark_node_pool, mark_id_ast_id_map); - - if indent_level > 0 { - let indent_node = MarkupNode::Indent { - indent_level, - parent_id_opt: None, - }; - - let indent_node_id = add_node(indent_node, node_id, mark_node_pool, mark_id_ast_id_map); - - let nested_node = MarkupNode::Nested { - children_ids: vec![indent_node_id, content_node_id], - parent_id_opt: None, - newlines_at_end: 0, - }; - - add_node(nested_node, node_id, mark_node_pool, mark_id_ast_id_map) - } else { - content_node_id - } -} - -pub fn set_parent_for_all(markup_node_id: MarkNodeId, mark_node_pool: &mut SlowPool) { - let node = mark_node_pool.get(markup_node_id); - - if let MarkupNode::Nested { - children_ids, - parent_id_opt: _, - newlines_at_end: _, - } = node - { - // need to clone because of borrowing issues - let children_ids_clone = children_ids.clone(); - - for child_id in children_ids_clone { - set_parent_for_all_helper(child_id, markup_node_id, mark_node_pool); - } - } -} - -pub fn set_parent_for_all_helper( - markup_node_id: MarkNodeId, - parent_node_id: MarkNodeId, - mark_node_pool: &mut SlowPool, -) { - let node = mark_node_pool.get_mut(markup_node_id); - - match node { - MarkupNode::Nested { - children_ids, - parent_id_opt, - .. - } => { - *parent_id_opt = Some(parent_node_id); - - // need to clone because of borrowing issues - let children_ids_clone = children_ids.clone(); - - for child_id in children_ids_clone { - set_parent_for_all_helper(child_id, markup_node_id, mark_node_pool); - } - } - MarkupNode::Text { parent_id_opt, .. } => *parent_id_opt = Some(parent_node_id), - MarkupNode::Blank { parent_id_opt, .. } => *parent_id_opt = Some(parent_node_id), - MarkupNode::Indent { parent_id_opt, .. } => *parent_id_opt = Some(parent_node_id), - } -} - -impl fmt::Display for MarkupNode { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "{} ({}, {})", - self.node_type_as_string(), - self.get_content(), - self.get_newlines_at_end() - ) - } -} - -pub fn tree_as_string(root_node_id: MarkNodeId, mark_node_pool: &SlowPool) -> String { - let mut full_string = "\n(mark_node_tree)\n".to_owned(); - - let node = mark_node_pool.get(root_node_id); - - full_string.push_str(&format!("{} mn_id {}\n", node, root_node_id)); - - tree_as_string_helper(node, 1, &mut full_string, mark_node_pool); - - full_string -} - -fn tree_as_string_helper( - node: &MarkupNode, - level: usize, - tree_string: &mut String, - mark_node_pool: &SlowPool, -) { - for child_id in node.get_children_ids() { - let mut full_str = std::iter::repeat("|--- ") - .take(level) - .collect::>() - .join("") - .to_owned(); - - let child = mark_node_pool.get(child_id); - let child_str = format!("{}", mark_node_pool.get(child_id)).replace('\n', "\\n"); - - full_str.push_str(&format!("{} mn_id {}\n", child_str, child_id)); - - tree_string.push_str(&full_str); - - tree_as_string_helper(child, level + 1, tree_string, mark_node_pool); - } -} - -// return to the the root parent_id of the current node -pub fn get_root_mark_node_id(mark_node_id: MarkNodeId, mark_node_pool: &SlowPool) -> MarkNodeId { - let mut curr_mark_node_id = mark_node_id; - let mut curr_parent_id_opt = mark_node_pool.get(curr_mark_node_id).get_parent_id_opt(); - - while let Some(curr_parent_id) = curr_parent_id_opt { - curr_mark_node_id = curr_parent_id; - curr_parent_id_opt = mark_node_pool.get(curr_mark_node_id).get_parent_id_opt(); - } - - curr_mark_node_id -} - -// put space mark nodes between each node in mark_nodes -pub fn join_mark_nodes_spaces( - mark_nodes_ids: Vec, - with_prepend: bool, - mark_node_pool: &mut SlowPool, -) -> Vec { - let space_range_max = if with_prepend { - mark_nodes_ids.len() - } else { - mark_nodes_ids.len() - 1 - }; - - let join_nodes: Vec = (0..space_range_max) - .map(|_| { - let space_node = MarkupNode::Text { - content: " ".to_string(), - syn_high_style: HighlightStyle::Blank, - attributes: Attributes::default(), - parent_id_opt: None, - newlines_at_end: 0, - }; - - mark_node_pool.add(space_node) - }) - .collect(); - - if with_prepend { - join_nodes.into_iter().interleave(mark_nodes_ids).collect() - } else { - mark_nodes_ids.into_iter().interleave(join_nodes).collect() - } -} - -// put comma mark nodes between each node in mark_nodes -pub fn join_mark_nodes_commas(mark_nodes: Vec) -> Vec { - let join_nodes: Vec = (0..(mark_nodes.len() - 1)) - .map(|_| new_comma_mn()) - .collect(); - - mark_nodes.into_iter().interleave(join_nodes).collect() -} - -pub fn mark_nodes_to_string(markup_node_ids: &[MarkNodeId], mark_node_pool: &SlowPool) -> String { - let mut all_code_string = String::new(); - - for mark_node_id in markup_node_ids.iter() { - node_to_string_w_children(*mark_node_id, &mut all_code_string, mark_node_pool) - } - - all_code_string -} - -pub fn node_to_string_w_children( - node_id: MarkNodeId, - str_buffer: &mut String, - mark_node_pool: &SlowPool, -) { - let node = mark_node_pool.get(node_id); - - if node.is_nested() { - for child_id in node.get_children_ids() { - node_to_string_w_children(child_id, str_buffer, mark_node_pool); - } - for _ in 0..node.get_newlines_at_end() { - str_buffer.push('\n') - } - } else { - let node_content_str = node.get_full_content(); - - str_buffer.push_str(&node_content_str); - } -} diff --git a/code_markup/src/markup/top_level_def.rs b/code_markup/src/markup/top_level_def.rs deleted file mode 100644 index 3fbea9fe76..0000000000 --- a/code_markup/src/markup/top_level_def.rs +++ /dev/null @@ -1,84 +0,0 @@ -use roc_ast::{ - ast_error::ASTResult, - lang::{core::ast::ASTNodeId, env::Env}, -}; -use roc_module::symbol::IdentId; - -use crate::{ - markup::{ - attribute::Attributes, - common_nodes::{new_comments_mn, new_equals_mn}, - nodes::MarkupNode, - }, - slow_pool::{MarkNodeId, SlowPool}, - syntax_highlight::HighlightStyle, -}; - -use super::{ - common_nodes::new_assign_mn, convert::from_def2::add_node, mark_id_ast_id_map::MarkIdAstIdMap, -}; - -// represents for example: `main = "Hello, World!"` -pub fn assignment_mark_node<'a>( - identifier_id: IdentId, - expr_mark_node_id: MarkNodeId, - ast_node_id: ASTNodeId, - mark_node_pool: &mut SlowPool, - mark_id_ast_id_map: &mut MarkIdAstIdMap, - env: &Env<'a>, -) -> ASTResult { - let val_name = env.ident_ids.get_name_str_res(identifier_id)?; - - let val_name_mn = MarkupNode::Text { - content: val_name.to_owned(), - syn_high_style: HighlightStyle::Value, - attributes: Attributes::default(), - parent_id_opt: None, - newlines_at_end: 0, - }; - - let val_name_mn_id = add_node(val_name_mn, ast_node_id, mark_node_pool, mark_id_ast_id_map); - - let equals_mn_id = add_node( - new_equals_mn(), - ast_node_id, - mark_node_pool, - mark_id_ast_id_map, - ); - - Ok(new_assign_mn( - val_name_mn_id, - equals_mn_id, - expr_mark_node_id, - )) -} - -pub fn tld_w_comments_mark_node( - comments: String, - def_mark_node_id: MarkNodeId, - ast_node_id: ASTNodeId, - mark_node_pool: &mut SlowPool, - mark_id_ast_id_map: &mut MarkIdAstIdMap, - comments_before: bool, -) -> ASTResult { - let comment_mn_id = add_node( - new_comments_mn(comments, 1), - ast_node_id, - mark_node_pool, - mark_id_ast_id_map, - ); - - let children_ids = if comments_before { - vec![comment_mn_id, def_mark_node_id] - } else { - vec![def_mark_node_id, comment_mn_id] - }; - - let tld_w_comment_node = MarkupNode::Nested { - children_ids, - parent_id_opt: None, - newlines_at_end: 2, - }; - - Ok(tld_w_comment_node) -} diff --git a/code_markup/src/markup_error.rs b/code_markup/src/markup_error.rs deleted file mode 100644 index 7a10252437..0000000000 --- a/code_markup/src/markup_error.rs +++ /dev/null @@ -1,65 +0,0 @@ -use roc_utils::util_error::UtilError; -use snafu::{Backtrace, NoneError, ResultExt, Snafu}; - -use crate::slow_pool::MarkNodeId; - -#[derive(Debug, Snafu)] -#[snafu(visibility(pub))] -pub enum MarkError { - #[snafu(display( - "CaretNotFound: No carets were found in the expected node with id {}", - node_id - ))] - CaretNotFound { - node_id: MarkNodeId, - backtrace: Backtrace, - }, - #[snafu(display( - "ExpectedTextNode: the function {} expected a Text node, got {} instead.", - function_name, - node_type - ))] - ExpectedTextNode { - function_name: String, - node_type: String, - backtrace: Backtrace, - }, - #[snafu(display( - "MarkNodeIdWithoutCorrespondingASTNodeId: MarkupNode with id {} was not found in MarkIdAstIdMap, available keys are: {}.", - node_id, - keys_str - ))] - MarkNodeIdWithoutCorrespondingASTNodeId { - node_id: MarkNodeId, - keys_str: String, - backtrace: Backtrace, - }, - #[snafu(display("NestedNodeMissingChild: expected to find child with id {} in Nested MarkupNode, but it was missing. Id's of the children are {:?}.", node_id, children_ids))] - NestedNodeMissingChild { - node_id: MarkNodeId, - children_ids: Vec, - backtrace: Backtrace, - }, - #[snafu(display( - "NestedNodeRequired: required a Nested node at this position, node was a {}.", - node_type - ))] - NestedNodeRequired { - node_type: String, - backtrace: Backtrace, - }, - #[snafu(display("UtilError: {}", msg))] - UtilErrorBacktrace { msg: String, backtrace: Backtrace }, -} - -pub type MarkResult = std::result::Result; - -impl From for MarkError { - fn from(util_err: UtilError) -> Self { - let msg = format!("{}", util_err); - - // hack to handle MarkError derive - let dummy_res: Result<(), NoneError> = Err(NoneError {}); - dummy_res.context(UtilErrorBacktrace { msg }).unwrap_err() - } -} diff --git a/code_markup/src/slow_pool.rs b/code_markup/src/slow_pool.rs deleted file mode 100644 index 94621dc92a..0000000000 --- a/code_markup/src/slow_pool.rs +++ /dev/null @@ -1,69 +0,0 @@ -use crate::markup::{mark_id_ast_id_map::MarkIdAstIdMap, nodes::MarkupNode}; - -pub type MarkNodeId = usize; - -#[derive(Debug, Default)] -pub struct SlowPool { - nodes: Vec, -} - -impl SlowPool { - pub fn add(&mut self, node: MarkupNode) -> MarkNodeId { - let id = self.nodes.len(); - - self.nodes.push(node); - - id - } - - pub fn get(&self, node_id: MarkNodeId) -> &MarkupNode { - // unwrap because Pool doesn't return Result either - self.nodes.get(node_id).unwrap() - } - - pub fn get_mut(&mut self, node_id: MarkNodeId) -> &mut MarkupNode { - // unwrap because Pool doesn't return Result either - self.nodes.get_mut(node_id).unwrap() - } - - pub fn replace_node(&mut self, node_id: MarkNodeId, new_node: MarkupNode) { - self.nodes[node_id] = new_node; - - // TODO delete children of old node, this requires SlowPool to be changed to - // make sure the indexes still make sense after removal/compaction - } - - pub fn debug_string(&self, mark_id_ast_id_map: &MarkIdAstIdMap) -> String { - let mut ret_str = String::new(); - - for (mark_node_id, node) in self.nodes.iter().enumerate() { - let ast_node_id_str = match mark_id_ast_id_map.get(mark_node_id) { - Ok(ast_id) => format!("{:?}", ast_id), - Err(err) => format!("{:?}", err), - }; - let ast_node_id: String = ast_node_id_str - .chars() - .filter(|c| c.is_ascii_digit()) - .collect(); - - let mut child_str = String::new(); - - let node_children = node.get_children_ids(); - - if !node_children.is_empty() { - child_str = format!("children: {:?}", node_children); - } - - ret_str.push_str(&format!( - "{}: {} ({}) ast_id {:?} {}", - mark_node_id, - node.node_type_as_string(), - node.get_content(), - ast_node_id.parse::().unwrap(), - child_str - )); - } - - ret_str - } -} diff --git a/code_markup/src/syntax_highlight.rs b/code_markup/src/syntax_highlight.rs deleted file mode 100644 index 9f728a7913..0000000000 --- a/code_markup/src/syntax_highlight.rs +++ /dev/null @@ -1,60 +0,0 @@ -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; - -use crate::colors::{from_hsb, RgbaTup}; - -#[derive(Hash, Eq, PartialEq, Copy, Clone, Debug, Deserialize, Serialize)] -pub enum HighlightStyle { - Operator, // =+-<>... - String, - FunctionName, - FunctionArgName, - Type, - Bracket, - Number, - PackageRelated, // app, packages, imports, exposes, provides... - Value, - RecordField, - Import, - Provides, - Blank, - Comment, - DocsComment, - UppercaseIdent, - LowercaseIdent, // TODO we probably don't want all lowercase identifiers to have the same color? - Keyword, // if, else, when... -} - -pub fn default_highlight_map() -> HashMap { - use HighlightStyle::*; - - let almost_white = from_hsb(258, 5, 95); - - let mut highlight_map = HashMap::new(); - [ - (Operator, from_hsb(185, 50, 75)), - (String, from_hsb(346, 65, 97)), - (FunctionName, almost_white), - (FunctionArgName, from_hsb(225, 50, 100)), - (Type, almost_white), - (Bracket, from_hsb(347, 80, 100)), - (Number, from_hsb(225, 50, 100)), - (PackageRelated, almost_white), - (Value, almost_white), - (RecordField, from_hsb(258, 50, 90)), - (Import, from_hsb(225, 50, 100)), - (Provides, from_hsb(225, 50, 100)), - (Blank, from_hsb(258, 50, 90)), - (Comment, from_hsb(258, 50, 90)), // TODO check color - (DocsComment, from_hsb(258, 50, 90)), // TODO check color - (UppercaseIdent, almost_white), - (LowercaseIdent, from_hsb(225, 50, 100)), - (Keyword, almost_white), - ] - .iter() - .for_each(|tup| { - highlight_map.insert(tup.0, tup.1); - }); - - highlight_map -} diff --git a/code_markup/src/underline_style.rs b/code_markup/src/underline_style.rs deleted file mode 100644 index 4eb81ee73c..0000000000 --- a/code_markup/src/underline_style.rs +++ /dev/null @@ -1,20 +0,0 @@ -use std::collections::HashMap; - -use serde::{Deserialize, Serialize}; - -use crate::colors::{from_hsb, RgbaTup}; - -#[derive(Hash, Eq, PartialEq, Copy, Clone, Debug, Deserialize, Serialize)] -pub enum UnderlineStyle { - Error, - Warning, -} - -pub fn default_underline_color_map() -> HashMap { - let mut underline_colors = HashMap::new(); - - underline_colors.insert(UnderlineStyle::Error, from_hsb(0, 50, 75)); - underline_colors.insert(UnderlineStyle::Warning, from_hsb(60, 50, 75)); - - underline_colors -} diff --git a/compiler/README.md b/compiler/README.md deleted file mode 100644 index d4c93aab03..0000000000 --- a/compiler/README.md +++ /dev/null @@ -1,200 +0,0 @@ -Here's how the compiler is laid out. - -# Parsing - -The main goal of parsing is to take a plain old String (such as the contents a .roc source file read from the filesystem) and translate that String into an `Expr` value. - -`Expr` is an `enum` defined in the `expr` module. An `Expr` represents a Roc expression. - -For example, parsing would translate this string... - - "1 + 2" - -...into this `Expr` value: - - BinOp(Int(1), Plus, Int(2)) - -> Technically it would be `Box::new(Int(1))` and `Box::new(Int(2))`, but that's beside the point for now. - -This `Expr` representation of the expression is useful for things like: - -- Checking that all variables are declared before they're used -- Type checking - -> As of this writing, the compiler doesn't do any of those things yet. They'll be added later! - -Since the parser is only concerned with translating String values into Expr values, it will happily translate syntactically valid strings into expressions that won't work at runtime. - -For example, parsing will translate this string: - -not "foo", "bar" - -...into this `Expr`: - - CallByName("not", vec!["foo", "bar"]) - -Now we may know that `not` takes a `Bool` and returns another `Bool`, but the parser doesn't know that. - -The parser only knows how to translate a `String` into an `Expr`; it's the job of other parts of the compiler to figure out if `Expr` values have problems like type mismatches and non-exhaustive patterns. - -That said, the parser can still run into syntax errors. This won't parse: - - if then 5 then else then - -This is gibberish to the parser, so it will produce an error rather than an `Expr`. - -Roc's parser is implemented using the [`marwes/combine`](http://github.com/marwes/combine-language/) crate. - -# Evaluating - -One of the useful things we can do with an `Expr` is to evaluate it. - -The process of evaluation is basically to transform an `Expr` into the simplest `Expr` we can that's still equivalent to the original. - -For example, let's say we had this code: - - "1 + 8 - 3" - -The parser will translate this into the following `Expr`: - - BinOp( - Int(1), - Plus, - BinOp(Int(8), Minus, Int(3)) - ) - -The `eval` function will take this `Expr` and translate it into this much simpler `Expr`: - - Int(6) - -At this point it's become so simple that we can display it to the end user as the number `6`. So running `parse` and then `eval` on the original Roc string of `1 + 8 - 3` will result in displaying `6` as the final output. - -> The `expr` module includes an `impl fmt::Display for Expr` that takes care of translating `Int(6)` into `6`, `Char('x')` as `'x'`, and so on. - -`eval` accomplishes this by doing a `match` on an `Expr` and resolving every operation it encounters. For example, when it first sees this: - - BinOp( - Int(1), - Plus, - BinOp(Int(8), Minus, Int(3)) - ) - -The first thing it does is to call `eval` on the right `Expr` values on either side of the `Plus`. That results in: - -1. Calling `eval` on `Int(1)`, which returns `Int(1)` since it can't be reduced any further. -2. Calling `eval` on `BinOp(Int(8), Minus, Int(3))`, which in fact can be reduced further. - -Since the second call to `eval` will match on another `BinOp`, it's once again going to recursively call `eval` on both of its `Expr` values. Since those are both `Int` values, though, their `eval` calls will return them right away without doing anything else. - -Now that it's evaluated the expressions on either side of the `Minus`, `eval` will look at the particular operator being applied to those expressions (in this case, a minus operator) and check to see if the expressions it was given work with that operation. - -> Remember, this `Expr` value potentially came directly from the parser. `eval` can't be sure any type checking has been done on it! - -If `eval` detects a non-numeric `Expr` value (that is, the `Expr` is not `Int` or `Frac`) on either side of the `Minus`, then it will immediately give an error and halt the evaluation. This sort of runtime type error is common to dynamic languages, and you can think of `eval` as being a dynamic evaluation of Roc code that hasn't necessarily been type-checked. - -Assuming there's no type problem, `eval` can go ahead and run the Rust code of `8 - 3` and store the result in an `Int` expr. - -That concludes our original recursive call to `eval`, after which point we'll be evaluating this expression: - - BinOp( - Int(1), - Plus, - Int(5) - ) - -This will work the same way as `Minus` did, and will reduce down to `Int(6)`. - -## Optimization philosophy - -Focus on optimizations which are only safe in the absence of side effects, and leave the rest to LLVM. - -This focus may lead to some optimizations becoming transitively in scope. For example, some deforestation -examples in the MSR paper benefit from multiple rounds of interleaved deforestation, beta-reduction, and inlining. -To get those benefits, we'd have to do some inlining and beta-reduction that we could otherwise leave to LLVM's -inlining and constant propagation/folding. - -Even if we're doing those things, it may still make sense to have LLVM do a pass for them as well, since -early LLVM optimization passes may unlock later opportunities for inlining and constant propagation/folding. - -## Inlining - -If a function is called exactly once (it's a helper function), presumably we always want to inline those. -If a function is "small enough" it's probably worth inlining too. - -## Fusion - -https://www.microsoft.com/en-us/research/wp-content/uploads/2016/07/deforestation-short-cut.pdf - -Basic approach: - -Do list stuff using `build` passing Cons Nil (like a cons list) and then do foldr/build substitution/reduction. -Afterwards, we can do a separate pass to flatten nested Cons structures into properly initialized RRBTs. -This way we get both deforestation and efficient RRBT construction. Should work for the other collection types too. - -It looks like we need to do some amount of inlining and beta reductions on the Roc side, rather than -leaving all of those to LLVM. - -Advanced approach: - -Express operations like map and filter in terms of toStream and fromStream, to unlock more deforestation. -More info on here: - -https://wiki.haskell.org/GHC_optimisations#Fusion - -# Getting started with the code - -The compiler contains a lot of code! If you're new to the project it can be hard to know where to start. It's useful to have some sort of "main entry point", or at least a "good place to start" for each of the main phases. - -After you get into the details, you'll discover that some parts of the compiler have more than one entry point. And things can be interwoven together in subtle and complex ways, for reasons to do with performance, edge case handling, etc. But if this is "day one" for you, and you're just trying to get familiar with things, this should be "good enough". - -The compiler is invoked from the CLI via `build_file` in cli/src/build.rs - -| Phase | Entry point / main functions | -| ------------------------------------- | ------------------------------------------------ | -| Compiler entry point | load/src/file.rs: load, load_and_monomorphize | -| Parse header | parse/src/module.rs: parse_header | -| Parse definitions | parse/src/module.rs: module_defs | -| Canonicalize | can/src/def.rs: canonicalize_defs | -| Type check | solve/src/module.rs: run_solve | -| Gather types to specialize | mono/src/ir.rs: PartialProc::from_named_function | -| Solve specialized types | mono/src/ir.rs: from_can, with_hole | -| Insert reference counting | mono/src/ir.rs: Proc::insert_refcount_operations | -| Code gen (optimized but slow) | gen_llvm/src/llvm/build.rs: build_procedures | -| Code gen (unoptimized but fast, CPU) | gen_dev/src/object_builder.rs: build_module | -| Code gen (unoptimized but fast, Wasm) | gen_wasm/src/lib.rs: build_module | - -For a more detailed understanding of the compilation phases, see the `Phase`, `BuildTask`, and `Msg` enums in `load/src/file.rs`. - -## Debugging the compiler - -Please see the [debug flags](./debug_flags/src/lib.rs) for information on how to -ask the compiler to emit debug information during various stages of compilation. - -There are some goals for more sophisticated debugging tools: - -- A nicer unification debugger, see https://github.com/rtfeldman/roc/issues/2486. - Any interest in helping out here is greatly appreciated. - -### General Tips - -#### Miscompilations - -If you observe a miscomplication, you may first want to check the generated mono -IR for your code - maybe there was a problem during specialization or layout -generation. One way to do this is to add a test to `test_mono/src/tests.rs` -and run the tests with `cargo test -p test_mono`; this will write the mono -IR to a file. - -#### Typechecking errors - -First, try to minimize your reproduction into a test that fits in -[`solve_expr`](./solve/tests/solve_expr.rs). - -Once you've done this, check out the `ROC_PRINT_UNIFICATIONS` debug flag. It -will show you where type unification went right and wrong. This is usually -enough to figure out a fix for the bug. - -If that doesn't work and you know your error has something to do with ranks, -you may want to instrument `deep_copy_var_help` in [solve](./solve/src/solve.rs). - -If that doesn't work, chatting on Zulip is always a good strategy. diff --git a/compiler/alias_analysis/Cargo.toml b/compiler/alias_analysis/Cargo.toml deleted file mode 100644 index 98fd17f6f8..0000000000 --- a/compiler/alias_analysis/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -authors = ["The Roc Contributors"] -edition = "2021" -license = "UPL-1.0" -name = "roc_alias_analysis" -version = "0.1.0" - -[dependencies] -morphic_lib = {path = "../../vendor/morphic_lib"} -roc_collections = {path = "../collections"} -roc_module = {path = "../module"} -roc_mono = {path = "../mono"} -roc_debug_flags = {path = "../debug_flags"} diff --git a/compiler/alias_analysis/src/lib.rs b/compiler/alias_analysis/src/lib.rs deleted file mode 100644 index 91e331f8d5..0000000000 --- a/compiler/alias_analysis/src/lib.rs +++ /dev/null @@ -1,1908 +0,0 @@ -use morphic_lib::TypeContext; -use morphic_lib::{ - BlockExpr, BlockId, CalleeSpecVar, ConstDefBuilder, ConstName, EntryPointName, ExprContext, - FuncDef, FuncDefBuilder, FuncName, ModDefBuilder, ModName, ProgramBuilder, Result, - TypeDefBuilder, TypeId, TypeName, UpdateModeVar, ValueId, -}; -use roc_collections::all::{MutMap, MutSet}; -use roc_module::low_level::LowLevel; -use roc_module::symbol::Symbol; - -use roc_mono::ir::{ - Call, CallType, Expr, HigherOrderLowLevel, HostExposedLayouts, ListLiteralElement, Literal, - ModifyRc, OptLevel, Proc, Stmt, -}; -use roc_mono::layout::{Builtin, Layout, RawFunctionLayout, UnionLayout}; - -// just using one module for now -pub const MOD_APP: ModName = ModName(b"UserApp"); - -pub const STATIC_STR_NAME: ConstName = ConstName(&Symbol::STR_ALIAS_ANALYSIS_STATIC.to_ne_bytes()); -pub const STATIC_LIST_NAME: ConstName = ConstName(b"THIS IS A STATIC LIST"); - -const ENTRY_POINT_NAME: &[u8] = b"mainForHost"; - -pub fn func_name_bytes(proc: &Proc) -> [u8; SIZE] { - func_name_bytes_help(proc.name, proc.args.iter().map(|x| x.0), &proc.ret_layout) -} - -#[inline(always)] -fn debug() -> bool { - use roc_debug_flags::dbg_do; - - #[cfg(debug_assertions)] - use roc_debug_flags::ROC_DEBUG_ALIAS_ANALYSIS; - - dbg_do!(ROC_DEBUG_ALIAS_ANALYSIS, { - return true; - }); - false -} - -const SIZE: usize = 16; - -#[derive(Debug, Clone, Copy, Hash)] -struct TagUnionId(u64); - -fn recursive_tag_union_name_bytes(union_layout: &UnionLayout) -> TagUnionId { - use std::collections::hash_map::DefaultHasher; - use std::hash::Hash; - use std::hash::Hasher; - - let mut hasher = DefaultHasher::new(); - union_layout.hash(&mut hasher); - - TagUnionId(hasher.finish()) -} - -impl TagUnionId { - const fn as_bytes(&self) -> [u8; 8] { - self.0.to_ne_bytes() - } -} - -pub fn func_name_bytes_help<'a, I>( - symbol: Symbol, - argument_layouts: I, - return_layout: &Layout<'a>, -) -> [u8; SIZE] -where - I: IntoIterator>, -{ - let mut name_bytes = [0u8; SIZE]; - - use std::collections::hash_map::DefaultHasher; - use std::hash::Hash; - use std::hash::Hasher; - - let layout_hash = { - let mut hasher = DefaultHasher::new(); - - for layout in argument_layouts { - layout.hash(&mut hasher); - } - - return_layout.hash(&mut hasher); - - hasher.finish() - }; - - let sbytes = symbol.to_ne_bytes(); - let lbytes = layout_hash.to_ne_bytes(); - - let it = sbytes - .iter() - .chain(lbytes.iter()) - .zip(name_bytes.iter_mut()); - - for (source, target) in it { - *target = *source; - } - - if debug() { - for (i, c) in (format!("{:?}", symbol)).chars().take(25).enumerate() { - name_bytes[25 + i] = c as u8; - } - } - - name_bytes -} - -fn bytes_as_ascii(bytes: &[u8]) -> String { - use std::fmt::Write; - - let mut buf = String::new(); - - for byte in bytes { - write!(buf, "{:02X}", byte).unwrap(); - } - - buf -} - -pub fn spec_program<'a, I>( - opt_level: OptLevel, - entry_point: roc_mono::ir::EntryPoint<'a>, - procs: I, -) -> Result -where - I: Iterator>, -{ - let main_module = { - let mut m = ModDefBuilder::new(); - - // a const that models all static strings - let static_str_def = { - let mut cbuilder = ConstDefBuilder::new(); - let block = cbuilder.add_block(); - let cell = cbuilder.add_new_heap_cell(block)?; - let value_id = cbuilder.add_make_tuple(block, &[cell])?; - let root = BlockExpr(block, value_id); - let str_type_id = str_type(&mut cbuilder)?; - - cbuilder.build(str_type_id, root)? - }; - m.add_const(STATIC_STR_NAME, static_str_def)?; - - // a const that models all static lists - let static_list_def = { - let mut cbuilder = ConstDefBuilder::new(); - let block = cbuilder.add_block(); - let cell = cbuilder.add_new_heap_cell(block)?; - - let unit_type = cbuilder.add_tuple_type(&[])?; - let bag = cbuilder.add_empty_bag(block, unit_type)?; - let value_id = cbuilder.add_make_tuple(block, &[cell, bag])?; - let root = BlockExpr(block, value_id); - let list_type_id = static_list_type(&mut cbuilder)?; - - cbuilder.build(list_type_id, root)? - }; - m.add_const(STATIC_LIST_NAME, static_list_def)?; - - let mut type_definitions = MutSet::default(); - let mut host_exposed_functions = Vec::new(); - - // all other functions - for proc in procs { - let bytes = func_name_bytes(proc); - let func_name = FuncName(&bytes); - - if let HostExposedLayouts::HostExposed { aliases, .. } = &proc.host_exposed_layouts { - for (_, (symbol, top_level, layout)) in aliases { - match layout { - RawFunctionLayout::Function(_, _, _) => { - let it = top_level.arguments.iter().copied(); - let bytes = func_name_bytes_help(*symbol, it, &top_level.result); - - host_exposed_functions.push((bytes, top_level.arguments)); - } - RawFunctionLayout::ZeroArgumentThunk(_) => { - let bytes = - func_name_bytes_help(*symbol, [Layout::UNIT], &top_level.result); - - host_exposed_functions.push((bytes, top_level.arguments)); - } - } - } - } - - if debug() { - eprintln!( - "{:?}: {:?} with {:?} args", - proc.name, - bytes_as_ascii(&bytes), - (proc.args, proc.ret_layout), - ); - } - - let (spec, type_names) = proc_spec(proc)?; - - type_definitions.extend(type_names); - - m.add_func(func_name, spec)?; - } - - // the entry point wrapper - let roc_main_bytes = func_name_bytes_help( - entry_point.symbol, - entry_point.layout.arguments.iter().copied(), - &entry_point.layout.result, - ); - let roc_main = FuncName(&roc_main_bytes); - - let entry_point_function = - build_entry_point(entry_point.layout, roc_main, &host_exposed_functions)?; - let entry_point_name = FuncName(ENTRY_POINT_NAME); - m.add_func(entry_point_name, entry_point_function)?; - - for union_layout in type_definitions { - let type_name_bytes = recursive_tag_union_name_bytes(&union_layout).as_bytes(); - let type_name = TypeName(&type_name_bytes); - - let mut builder = TypeDefBuilder::new(); - - let variant_types = recursive_variant_types(&mut builder, &union_layout)?; - let root_type = if let UnionLayout::NonNullableUnwrapped(_) = union_layout { - debug_assert_eq!(variant_types.len(), 1); - variant_types[0] - } else { - let data_type = builder.add_union_type(&variant_types)?; - let cell_type = builder.add_heap_cell_type(); - - builder.add_tuple_type(&[cell_type, data_type])? - }; - - let type_def = builder.build(root_type)?; - - m.add_named_type(type_name, type_def)?; - } - - m.build()? - }; - - let program = { - let mut p = ProgramBuilder::new(); - p.add_mod(MOD_APP, main_module)?; - - let entry_point_name = FuncName(ENTRY_POINT_NAME); - p.add_entry_point(EntryPointName(ENTRY_POINT_NAME), MOD_APP, entry_point_name)?; - - p.build()? - }; - - if debug() { - eprintln!("{}", program.to_source_string()); - } - - match opt_level { - OptLevel::Development | OptLevel::Normal => morphic_lib::solve_trivial(program), - OptLevel::Optimize | OptLevel::Size => morphic_lib::solve(program), - } -} - -/// if you want an "escape hatch" which allows you construct "best-case scenario" values -/// of an arbitrary type in much the same way that 'unknown_with' allows you to construct -/// "worst-case scenario" values of an arbitrary type, you can use the following terrible hack: -/// use 'add_make_union' to construct an instance of variant 0 of a union type 'union {(), your_type}', -/// and then use 'add_unwrap_union' to extract variant 1 from the value you just constructed. -/// In the current implementation (but not necessarily in future versions), -/// I can promise this will effectively give you a value of type 'your_type' -/// all of whose heap cells are considered unique and mutable. -fn terrible_hack(builder: &mut FuncDefBuilder, block: BlockId, type_id: TypeId) -> Result { - let variant_types = vec![builder.add_tuple_type(&[])?, type_id]; - let unit = builder.add_make_tuple(block, &[])?; - let value = builder.add_make_union(block, &variant_types, 0, unit)?; - - builder.add_unwrap_union(block, value, 1) -} - -fn build_entry_point( - layout: roc_mono::ir::ProcLayout, - func_name: FuncName, - host_exposed_functions: &[([u8; SIZE], &[Layout])], -) -> Result { - let mut builder = FuncDefBuilder::new(); - let outer_block = builder.add_block(); - - let mut cases = Vec::new(); - - { - let block = builder.add_block(); - - // to the modelling language, the arguments appear out of thin air - let argument_type = - build_tuple_type(&mut builder, layout.arguments, &WhenRecursive::Unreachable)?; - - // does not make any assumptions about the input - // let argument = builder.add_unknown_with(block, &[], argument_type)?; - - // assumes the input can be updated in-place - let argument = terrible_hack(&mut builder, block, argument_type)?; - - let name_bytes = [0; 16]; - let spec_var = CalleeSpecVar(&name_bytes); - let result = builder.add_call(block, spec_var, MOD_APP, func_name, argument)?; - - // to the modelling language, the result disappears into the void - let unit_type = builder.add_tuple_type(&[])?; - let unit_value = builder.add_unknown_with(block, &[result], unit_type)?; - - cases.push(BlockExpr(block, unit_value)); - } - - // add fake calls to host-exposed functions so they are specialized - for (name_bytes, layouts) in host_exposed_functions { - let host_exposed_func_name = FuncName(name_bytes); - - if host_exposed_func_name == func_name { - continue; - } - - let block = builder.add_block(); - - let type_id = layout_spec( - &mut builder, - &Layout::struct_no_name_order(layouts), - &WhenRecursive::Unreachable, - )?; - - let argument = builder.add_unknown_with(block, &[], type_id)?; - - let spec_var = CalleeSpecVar(name_bytes); - let result = - builder.add_call(block, spec_var, MOD_APP, host_exposed_func_name, argument)?; - - let unit_type = builder.add_tuple_type(&[])?; - let unit_value = builder.add_unknown_with(block, &[result], unit_type)?; - - cases.push(BlockExpr(block, unit_value)); - } - - let unit_type = builder.add_tuple_type(&[])?; - let unit_value = builder.add_choice(outer_block, &cases)?; - - let root = BlockExpr(outer_block, unit_value); - let spec = builder.build(unit_type, unit_type, root)?; - - Ok(spec) -} - -fn proc_spec<'a>(proc: &Proc<'a>) -> Result<(FuncDef, MutSet>)> { - let mut builder = FuncDefBuilder::new(); - let mut env = Env::default(); - - let block = builder.add_block(); - - // introduce the arguments - let mut argument_layouts = Vec::new(); - for (i, (layout, symbol)) in proc.args.iter().enumerate() { - let value_id = builder.add_get_tuple_field(block, builder.get_argument(), i as u32)?; - env.symbols.insert(*symbol, value_id); - - argument_layouts.push(*layout); - } - - let value_id = stmt_spec(&mut builder, &mut env, block, &proc.ret_layout, &proc.body)?; - - let root = BlockExpr(block, value_id); - let arg_type_id = layout_spec( - &mut builder, - &Layout::struct_no_name_order(&argument_layouts), - &WhenRecursive::Unreachable, - )?; - let ret_type_id = layout_spec(&mut builder, &proc.ret_layout, &WhenRecursive::Unreachable)?; - - let spec = builder.build(arg_type_id, ret_type_id, root)?; - - Ok((spec, env.type_names)) -} - -#[derive(Default)] -struct Env<'a> { - symbols: MutMap, - join_points: MutMap, - type_names: MutSet>, -} - -fn stmt_spec<'a>( - builder: &mut FuncDefBuilder, - env: &mut Env<'a>, - block: BlockId, - layout: &Layout, - stmt: &Stmt<'a>, -) -> Result { - use Stmt::*; - - match stmt { - Let(symbol, expr, expr_layout, mut continuation) => { - let value_id = expr_spec(builder, env, block, expr_layout, expr)?; - env.symbols.insert(*symbol, value_id); - - let mut queue = vec![symbol]; - - while let Let(symbol, expr, expr_layout, c) = continuation { - let value_id = expr_spec(builder, env, block, expr_layout, expr)?; - env.symbols.insert(*symbol, value_id); - - queue.push(symbol); - continuation = c; - } - - let result = stmt_spec(builder, env, block, layout, continuation)?; - - for symbol in queue { - env.symbols.remove(symbol); - } - - Ok(result) - } - Switch { - cond_symbol: _, - cond_layout: _, - branches, - default_branch, - ret_layout: _lies, - } => { - let mut cases = Vec::with_capacity(branches.len() + 1); - - let it = branches - .iter() - .map(|(_, _, body)| body) - .chain(std::iter::once(default_branch.1)); - - for branch in it { - let block = builder.add_block(); - let value_id = stmt_spec(builder, env, block, layout, branch)?; - cases.push(BlockExpr(block, value_id)); - } - - builder.add_choice(block, &cases) - } - Expect { remainder, .. } => stmt_spec(builder, env, block, layout, remainder), - Ret(symbol) => Ok(env.symbols[symbol]), - Refcounting(modify_rc, continuation) => match modify_rc { - ModifyRc::Inc(symbol, _) => { - let argument = env.symbols[symbol]; - - // a recursive touch is never worse for optimizations than a normal touch - // and a bit more permissive in its type - builder.add_recursive_touch(block, argument)?; - - stmt_spec(builder, env, block, layout, continuation) - } - - ModifyRc::Dec(symbol) => { - let argument = env.symbols[symbol]; - - builder.add_recursive_touch(block, argument)?; - - stmt_spec(builder, env, block, layout, continuation) - } - ModifyRc::DecRef(symbol) => { - let argument = env.symbols[symbol]; - - builder.add_recursive_touch(block, argument)?; - - stmt_spec(builder, env, block, layout, continuation) - } - }, - Join { - id, - parameters, - body, - remainder, - } => { - let mut type_ids = Vec::new(); - - for p in parameters.iter() { - type_ids.push(layout_spec( - builder, - &p.layout, - &WhenRecursive::Unreachable, - )?); - } - - let ret_type_id = layout_spec(builder, layout, &WhenRecursive::Unreachable)?; - - let jp_arg_type_id = builder.add_tuple_type(&type_ids)?; - - let (jpid, jp_argument) = - builder.declare_continuation(block, jp_arg_type_id, ret_type_id)?; - - // NOTE join point arguments can shadow variables from the outer scope - // the ordering of steps here is important - - // add this ID so both body and remainder can reference it - env.join_points.insert(*id, jpid); - - // first, with the current variable bindings, process the remainder - let cont_block = builder.add_block(); - let cont_value_id = stmt_spec(builder, env, cont_block, layout, remainder)?; - - // only then introduce variables bound by the jump point, and process its body - let join_body_sub_block = { - let jp_body_block = builder.add_block(); - - // unpack the argument - for (i, p) in parameters.iter().enumerate() { - let value_id = - builder.add_get_tuple_field(jp_body_block, jp_argument, i as u32)?; - - env.symbols.insert(p.symbol, value_id); - } - - let jp_body_value_id = stmt_spec(builder, env, jp_body_block, layout, body)?; - - BlockExpr(jp_body_block, jp_body_value_id) - }; - - env.join_points.remove(id); - builder.define_continuation(jpid, join_body_sub_block)?; - - builder.add_sub_block(block, BlockExpr(cont_block, cont_value_id)) - } - Jump(id, symbols) => { - let ret_type_id = layout_spec(builder, layout, &WhenRecursive::Unreachable)?; - let argument = build_tuple_value(builder, env, block, symbols)?; - - let jpid = env.join_points[id]; - builder.add_jump(block, jpid, argument, ret_type_id) - } - RuntimeError(_) => { - let type_id = layout_spec(builder, layout, &WhenRecursive::Unreachable)?; - - builder.add_terminate(block, type_id) - } - } -} - -fn build_tuple_value( - builder: &mut FuncDefBuilder, - env: &Env, - block: BlockId, - symbols: &[Symbol], -) -> Result { - let mut value_ids = Vec::new(); - - for field in symbols.iter() { - let value_id = match env.symbols.get(field) { - None => panic!( - "Symbol {:?} is not defined in environment {:?}", - field, &env.symbols - ), - Some(x) => *x, - }; - value_ids.push(value_id); - } - - builder.add_make_tuple(block, &value_ids) -} - -#[derive(Clone, Debug, PartialEq)] -enum WhenRecursive<'a> { - Unreachable, - Loop(UnionLayout<'a>), -} - -fn build_recursive_tuple_type( - builder: &mut impl TypeContext, - layouts: &[Layout], - when_recursive: &WhenRecursive, -) -> Result { - let mut field_types = Vec::new(); - - for field in layouts.iter() { - field_types.push(layout_spec_help(builder, field, when_recursive)?); - } - - builder.add_tuple_type(&field_types) -} - -fn build_tuple_type( - builder: &mut impl TypeContext, - layouts: &[Layout], - when_recursive: &WhenRecursive, -) -> Result { - let mut field_types = Vec::new(); - - for field in layouts.iter() { - field_types.push(layout_spec(builder, field, when_recursive)?); - } - - builder.add_tuple_type(&field_types) -} - -#[repr(u32)] -#[derive(Clone, Copy)] -enum KeepResult { - Errs = ERR_TAG_ID, - Oks = OK_TAG_ID, -} - -impl KeepResult { - fn invert(&self) -> Self { - match self { - KeepResult::Errs => KeepResult::Oks, - KeepResult::Oks => KeepResult::Errs, - } - } -} - -#[derive(Clone, Copy)] -enum ResultRepr<'a> { - /// This is basically a `Result * whatever` or `Result [] whatever` (in keepOks, arguments flipped for keepErrs). - /// Such a `Result` gets a `Bool` layout at currently. We model the `*` or `[]` as a unit - /// (empty tuple) in morphic, otherwise we run into trouble when we need to crate a value of - /// type void - ResultStarStar, - ResultConcrete { - err: Layout<'a>, - ok: Layout<'a>, - }, -} - -impl<'a> ResultRepr<'a> { - fn from_layout(layout: &Layout<'a>) -> Self { - match layout { - Layout::Union(UnionLayout::NonRecursive(tags)) => ResultRepr::ResultConcrete { - err: tags[ERR_TAG_ID as usize][0], - ok: tags[OK_TAG_ID as usize][0], - }, - Layout::Builtin(Builtin::Bool) => ResultRepr::ResultStarStar, - other => unreachable!("unexpected layout: {:?}", other), - } - } - - fn unwrap( - &self, - builder: &mut FuncDefBuilder, - block: BlockId, - err_or_ok: ValueId, - keep_tag_id: u32, - ) -> Result { - match self { - ResultRepr::ResultConcrete { .. } => { - let unwrapped = builder.add_unwrap_union(block, err_or_ok, keep_tag_id)?; - - builder.add_get_tuple_field(block, unwrapped, 0) - } - ResultRepr::ResultStarStar => { - // Void/EmptyTagUnion is represented as a unit value in morphic - // using `union {}` runs into trouble where we have to crate a value of that type - builder.add_make_tuple(block, &[]) - } - } - } -} - -fn add_loop( - builder: &mut FuncDefBuilder, - block: BlockId, - state_type: TypeId, - init_state: ValueId, - make_body: impl for<'a> FnOnce(&'a mut FuncDefBuilder, BlockId, ValueId) -> Result, -) -> Result { - let sub_block = builder.add_block(); - let (loop_cont, loop_arg) = builder.declare_continuation(sub_block, state_type, state_type)?; - let body = builder.add_block(); - let ret_branch = builder.add_block(); - let loop_branch = builder.add_block(); - let new_state = make_body(builder, loop_branch, loop_arg)?; - let unreachable = builder.add_jump(loop_branch, loop_cont, new_state, state_type)?; - let result = builder.add_choice( - body, - &[ - BlockExpr(ret_branch, loop_arg), - BlockExpr(loop_branch, unreachable), - ], - )?; - builder.define_continuation(loop_cont, BlockExpr(body, result))?; - let unreachable = builder.add_jump(sub_block, loop_cont, init_state, state_type)?; - builder.add_sub_block(block, BlockExpr(sub_block, unreachable)) -} - -fn call_spec( - builder: &mut FuncDefBuilder, - env: &Env, - block: BlockId, - layout: &Layout, - call: &Call, -) -> Result { - use CallType::*; - - match &call.call_type { - ByName { - name: symbol, - ret_layout, - arg_layouts, - specialization_id, - } => { - let array = specialization_id.to_bytes(); - let spec_var = CalleeSpecVar(&array); - - let arg_value_id = build_tuple_value(builder, env, block, call.arguments)?; - let it = arg_layouts.iter().copied(); - let bytes = func_name_bytes_help(*symbol, it, ret_layout); - let name = FuncName(&bytes); - let module = MOD_APP; - builder.add_call(block, spec_var, module, name, arg_value_id) - } - Foreign { - foreign_symbol: _, - ret_layout, - } => { - let arguments: Vec<_> = call - .arguments - .iter() - .map(|symbol| env.symbols[symbol]) - .collect(); - - let result_type = layout_spec(builder, ret_layout, &WhenRecursive::Unreachable)?; - - builder.add_unknown_with(block, &arguments, result_type) - } - LowLevel { op, update_mode } => lowlevel_spec( - builder, - env, - block, - layout, - op, - *update_mode, - call.arguments, - ), - HigherOrder(HigherOrderLowLevel { - closure_env_layout, - update_mode, - op, - passed_function, - .. - }) => { - use roc_mono::low_level::HigherOrder::*; - - let array = passed_function.specialization_id.to_bytes(); - let spec_var = CalleeSpecVar(&array); - - let mode = update_mode.to_bytes(); - let update_mode_var = UpdateModeVar(&mode); - - let it = passed_function.argument_layouts.iter().copied(); - let bytes = - func_name_bytes_help(passed_function.name, it, &passed_function.return_layout); - let name = FuncName(&bytes); - let module = MOD_APP; - - let closure_env = env.symbols[&passed_function.captured_environment]; - - let return_layout = &passed_function.return_layout; - let argument_layouts = passed_function.argument_layouts; - - macro_rules! call_function { - ($builder: expr, $block:expr, [$($arg:expr),+ $(,)?]) => {{ - let argument = if closure_env_layout.is_none() { - $builder.add_make_tuple($block, &[$($arg),+])? - } else { - $builder.add_make_tuple($block, &[$($arg),+, closure_env])? - }; - - $builder.add_call($block, spec_var, module, name, argument)? - }}; - } - - match op { - DictWalk { xs, state } => { - let dict = env.symbols[xs]; - let state = env.symbols[state]; - - let loop_body = |builder: &mut FuncDefBuilder, block, state| { - let bag = builder.add_get_tuple_field(block, dict, DICT_BAG_INDEX)?; - - let element = builder.add_bag_get(block, bag)?; - - let key = builder.add_get_tuple_field(block, element, 0)?; - let val = builder.add_get_tuple_field(block, element, 1)?; - - let new_state = call_function!(builder, block, [state, key, val]); - - Ok(new_state) - }; - - let state_layout = argument_layouts[0]; - let state_type = - layout_spec(builder, &state_layout, &WhenRecursive::Unreachable)?; - let init_state = state; - - add_loop(builder, block, state_type, init_state, loop_body) - } - - ListWalk { xs, state } | ListWalkBackwards { xs, state } => { - let list = env.symbols[xs]; - let state = env.symbols[state]; - - let loop_body = |builder: &mut FuncDefBuilder, block, state| { - let bag = builder.add_get_tuple_field(block, list, LIST_BAG_INDEX)?; - - let element = builder.add_bag_get(block, bag)?; - - let new_state = call_function!(builder, block, [state, element]); - - Ok(new_state) - }; - - let state_layout = argument_layouts[0]; - let state_type = - layout_spec(builder, &state_layout, &WhenRecursive::Unreachable)?; - let init_state = state; - - add_loop(builder, block, state_type, init_state, loop_body) - } - ListWalkUntil { xs, state } => { - let list = env.symbols[xs]; - let state = env.symbols[state]; - - let loop_body = |builder: &mut FuncDefBuilder, block, state| { - let bag = builder.add_get_tuple_field(block, list, LIST_BAG_INDEX)?; - - let element = builder.add_bag_get(block, bag)?; - - let continue_or_stop = call_function!(builder, block, [state, element]); - - // just assume it is a continue - let unwrapped = builder.add_unwrap_union(block, continue_or_stop, 0)?; - let new_state = builder.add_get_tuple_field(block, unwrapped, 0)?; - - Ok(new_state) - }; - - let state_layout = argument_layouts[0]; - let state_type = - layout_spec(builder, &state_layout, &WhenRecursive::Unreachable)?; - let init_state = state; - - add_loop(builder, block, state_type, init_state, loop_body) - } - - // List.mapWithIndex : List before, (before, Nat -> after) -> List after - ListMapWithIndex { xs } => { - let list = env.symbols[xs]; - - let loop_body = |builder: &mut FuncDefBuilder, block, state| { - let input_bag = builder.add_get_tuple_field(block, list, LIST_BAG_INDEX)?; - - let element = builder.add_bag_get(block, input_bag)?; - let index = builder.add_make_tuple(block, &[])?; - - // before, Nat -> after - let new_element = call_function!(builder, block, [element, index]); - - list_append(builder, block, update_mode_var, state, new_element) - }; - - let output_element_type = - layout_spec(builder, return_layout, &WhenRecursive::Unreachable)?; - - let state_layout = Layout::Builtin(Builtin::List(return_layout)); - let state_type = - layout_spec(builder, &state_layout, &WhenRecursive::Unreachable)?; - - let init_state = new_list(builder, block, output_element_type)?; - - add_loop(builder, block, state_type, init_state, loop_body) - } - - ListMap { xs } => { - let list = env.symbols[xs]; - - let loop_body = |builder: &mut FuncDefBuilder, block, state| { - let input_bag = builder.add_get_tuple_field(block, list, LIST_BAG_INDEX)?; - - let element = builder.add_bag_get(block, input_bag)?; - - let new_element = call_function!(builder, block, [element]); - - list_append(builder, block, update_mode_var, state, new_element) - }; - - let output_element_type = - layout_spec(builder, return_layout, &WhenRecursive::Unreachable)?; - - let state_layout = Layout::Builtin(Builtin::List(return_layout)); - let state_type = - layout_spec(builder, &state_layout, &WhenRecursive::Unreachable)?; - - let init_state = new_list(builder, block, output_element_type)?; - - add_loop(builder, block, state_type, init_state, loop_body) - } - - ListSortWith { xs } => { - let list = env.symbols[xs]; - - let loop_body = |builder: &mut FuncDefBuilder, block, state| { - let bag = builder.add_get_tuple_field(block, state, LIST_BAG_INDEX)?; - let cell = builder.add_get_tuple_field(block, state, LIST_CELL_INDEX)?; - - let element_1 = builder.add_bag_get(block, bag)?; - let element_2 = builder.add_bag_get(block, bag)?; - - let _ = call_function!(builder, block, [element_1, element_2]); - - builder.add_update(block, update_mode_var, cell)?; - - with_new_heap_cell(builder, block, bag) - }; - - let state_layout = Layout::Builtin(Builtin::List(&argument_layouts[0])); - let state_type = - layout_spec(builder, &state_layout, &WhenRecursive::Unreachable)?; - let init_state = list; - - add_loop(builder, block, state_type, init_state, loop_body) - } - - ListMap2 { xs, ys } => { - let list1 = env.symbols[xs]; - let list2 = env.symbols[ys]; - - let loop_body = |builder: &mut FuncDefBuilder, block, state| { - let input_bag_1 = - builder.add_get_tuple_field(block, list1, LIST_BAG_INDEX)?; - let input_bag_2 = - builder.add_get_tuple_field(block, list2, LIST_BAG_INDEX)?; - - let element_1 = builder.add_bag_get(block, input_bag_1)?; - let element_2 = builder.add_bag_get(block, input_bag_2)?; - - let new_element = call_function!(builder, block, [element_1, element_2]); - - list_append(builder, block, update_mode_var, state, new_element) - }; - - let output_element_type = - layout_spec(builder, return_layout, &WhenRecursive::Unreachable)?; - - let state_layout = Layout::Builtin(Builtin::List(return_layout)); - let state_type = - layout_spec(builder, &state_layout, &WhenRecursive::Unreachable)?; - - let init_state = new_list(builder, block, output_element_type)?; - - add_loop(builder, block, state_type, init_state, loop_body) - } - - ListMap3 { xs, ys, zs } => { - let list1 = env.symbols[xs]; - let list2 = env.symbols[ys]; - let list3 = env.symbols[zs]; - - let loop_body = |builder: &mut FuncDefBuilder, block, state| { - let input_bag_1 = - builder.add_get_tuple_field(block, list1, LIST_BAG_INDEX)?; - let input_bag_2 = - builder.add_get_tuple_field(block, list2, LIST_BAG_INDEX)?; - let input_bag_3 = - builder.add_get_tuple_field(block, list3, LIST_BAG_INDEX)?; - - let element_1 = builder.add_bag_get(block, input_bag_1)?; - let element_2 = builder.add_bag_get(block, input_bag_2)?; - let element_3 = builder.add_bag_get(block, input_bag_3)?; - - let new_element = - call_function!(builder, block, [element_1, element_2, element_3]); - - list_append(builder, block, update_mode_var, state, new_element) - }; - - let output_element_type = - layout_spec(builder, return_layout, &WhenRecursive::Unreachable)?; - - let state_layout = Layout::Builtin(Builtin::List(return_layout)); - let state_type = - layout_spec(builder, &state_layout, &WhenRecursive::Unreachable)?; - - let init_state = new_list(builder, block, output_element_type)?; - - add_loop(builder, block, state_type, init_state, loop_body) - } - ListMap4 { xs, ys, zs, ws } => { - let list1 = env.symbols[xs]; - let list2 = env.symbols[ys]; - let list3 = env.symbols[zs]; - let list4 = env.symbols[ws]; - - let loop_body = |builder: &mut FuncDefBuilder, block, state| { - let input_bag_1 = - builder.add_get_tuple_field(block, list1, LIST_BAG_INDEX)?; - let input_bag_2 = - builder.add_get_tuple_field(block, list2, LIST_BAG_INDEX)?; - let input_bag_3 = - builder.add_get_tuple_field(block, list3, LIST_BAG_INDEX)?; - let input_bag_4 = - builder.add_get_tuple_field(block, list4, LIST_BAG_INDEX)?; - - let element_1 = builder.add_bag_get(block, input_bag_1)?; - let element_2 = builder.add_bag_get(block, input_bag_2)?; - let element_3 = builder.add_bag_get(block, input_bag_3)?; - let element_4 = builder.add_bag_get(block, input_bag_4)?; - - let new_element = call_function!( - builder, - block, - [element_1, element_2, element_3, element_4] - ); - - list_append(builder, block, update_mode_var, state, new_element) - }; - - let output_element_type = - layout_spec(builder, return_layout, &WhenRecursive::Unreachable)?; - - let state_layout = Layout::Builtin(Builtin::List(return_layout)); - let state_type = - layout_spec(builder, &state_layout, &WhenRecursive::Unreachable)?; - - let init_state = new_list(builder, block, output_element_type)?; - - add_loop(builder, block, state_type, init_state, loop_body) - } - ListKeepIf { xs } => { - let list = env.symbols[xs]; - - let loop_body = |builder: &mut FuncDefBuilder, block, state| { - let bag = builder.add_get_tuple_field(block, state, LIST_BAG_INDEX)?; - let cell = builder.add_get_tuple_field(block, state, LIST_CELL_INDEX)?; - - let element = builder.add_bag_get(block, bag)?; - - let _ = call_function!(builder, block, [element]); - - // NOTE: we assume the element is not kept - builder.add_update(block, update_mode_var, cell)?; - - let removed = builder.add_bag_remove(block, bag)?; - - // decrement the removed element - let removed_element = builder.add_get_tuple_field(block, removed, 1)?; - builder.add_recursive_touch(block, removed_element)?; - - let new_bag = builder.add_get_tuple_field(block, removed, 0)?; - - with_new_heap_cell(builder, block, new_bag) - }; - - let state_layout = Layout::Builtin(Builtin::List(&argument_layouts[0])); - let state_type = - layout_spec(builder, &state_layout, &WhenRecursive::Unreachable)?; - let init_state = list; - - add_loop(builder, block, state_type, init_state, loop_body) - } - ListKeepOks { xs } | ListKeepErrs { xs } => { - let list = env.symbols[xs]; - - let keep_result = match op { - ListKeepOks { .. } => KeepResult::Oks, - ListKeepErrs { .. } => KeepResult::Errs, - _ => unreachable!(), - }; - - let result_repr = ResultRepr::from_layout(return_layout); - - let output_element_layout = match (keep_result, result_repr) { - (KeepResult::Errs, ResultRepr::ResultConcrete { err, .. }) => err, - (KeepResult::Oks, ResultRepr::ResultConcrete { ok, .. }) => ok, - (_, ResultRepr::ResultStarStar) => { - // we represent this case as Unit, while Void is maybe more natural - // but using Void we'd need to crate values of type Void, which is not - // possible - Layout::UNIT - } - }; - - let loop_body = |builder: &mut FuncDefBuilder, block, state| { - let bag = builder.add_get_tuple_field(block, list, LIST_BAG_INDEX)?; - - let element = builder.add_bag_get(block, bag)?; - - let err_or_ok = call_function!(builder, block, [element]); - - let kept_branch = builder.add_block(); - let not_kept_branch = builder.add_block(); - - let element_kept = { - let block = kept_branch; - - // a Result can be represented as a Int1 - let new_element = result_repr.unwrap( - builder, - block, - err_or_ok, - keep_result as u32, - )?; - - list_append(builder, block, update_mode_var, state, new_element)? - }; - - let element_not_kept = { - let block = not_kept_branch; - - // a Result can be represented as a Int1 - let dropped_element = result_repr.unwrap( - builder, - block, - err_or_ok, - keep_result.invert() as u32, - )?; - - // decrement the element we will not keep - builder.add_recursive_touch(block, dropped_element)?; - - state - }; - - builder.add_choice( - block, - &[ - BlockExpr(not_kept_branch, element_not_kept), - BlockExpr(kept_branch, element_kept), - ], - ) - }; - - let output_element_type = - layout_spec(builder, &output_element_layout, &WhenRecursive::Unreachable)?; - let init_state = new_list(builder, block, output_element_type)?; - - let state_layout = Layout::Builtin(Builtin::List(&output_element_layout)); - let state_type = - layout_spec(builder, &state_layout, &WhenRecursive::Unreachable)?; - - add_loop(builder, block, state_type, init_state, loop_body) - } - ListAny { xs } => { - let list = env.symbols[xs]; - - let loop_body = |builder: &mut FuncDefBuilder, block, _state| { - let bag = builder.add_get_tuple_field(block, list, LIST_BAG_INDEX)?; - let element = builder.add_bag_get(block, bag)?; - - let new_state = call_function!(builder, block, [element]); - - Ok(new_state) - }; - - let state_layout = Layout::Builtin(Builtin::Bool); - let state_type = - layout_spec(builder, &state_layout, &WhenRecursive::Unreachable)?; - - let init_state = new_num(builder, block)?; - - add_loop(builder, block, state_type, init_state, loop_body) - } - ListAll { xs } => { - let list = env.symbols[xs]; - - let loop_body = |builder: &mut FuncDefBuilder, block, _state| { - let bag = builder.add_get_tuple_field(block, list, LIST_BAG_INDEX)?; - let element = builder.add_bag_get(block, bag)?; - - let new_state = call_function!(builder, block, [element]); - - Ok(new_state) - }; - - let state_layout = Layout::Builtin(Builtin::Bool); - let state_type = - layout_spec(builder, &state_layout, &WhenRecursive::Unreachable)?; - - let init_state = new_num(builder, block)?; - - add_loop(builder, block, state_type, init_state, loop_body) - } - ListFindUnsafe { xs } => { - let list = env.symbols[xs]; - - // ListFindUnsafe returns { value: v, found: Bool=Int1 } - let output_layouts = vec![argument_layouts[0], Layout::Builtin(Builtin::Bool)]; - let output_layout = Layout::struct_no_name_order(&output_layouts); - let output_type = - layout_spec(builder, &output_layout, &WhenRecursive::Unreachable)?; - - let loop_body = |builder: &mut FuncDefBuilder, block, output| { - let bag = builder.add_get_tuple_field(block, list, LIST_BAG_INDEX)?; - let element = builder.add_bag_get(block, bag)?; - let _is_found = call_function!(builder, block, [element]); - - // We may or may not use the element we got from the list in the output struct, - // depending on whether we found the element to satisfy the "find" predicate. - // If we did find the element, our output "changes" to be a record including that element. - let found_branch = builder.add_block(); - let new_output = - builder.add_unknown_with(block, &[element], output_type)?; - - let not_found_branch = builder.add_block(); - - builder.add_choice( - block, - &[ - BlockExpr(found_branch, new_output), - BlockExpr(not_found_branch, output), - ], - ) - }; - - // Assume the output is initially { found: False, value: \empty } - let output_state = builder.add_unknown_with(block, &[], output_type)?; - add_loop(builder, block, output_type, output_state, loop_body) - } - } - } - } -} - -fn list_append( - builder: &mut FuncDefBuilder, - block: BlockId, - update_mode_var: UpdateModeVar, - list: ValueId, - to_insert: ValueId, -) -> Result { - let bag = builder.add_get_tuple_field(block, list, LIST_BAG_INDEX)?; - let cell = builder.add_get_tuple_field(block, list, LIST_CELL_INDEX)?; - - let _unit = builder.add_update(block, update_mode_var, cell)?; - - let new_bag = builder.add_bag_insert(block, bag, to_insert)?; - - with_new_heap_cell(builder, block, new_bag) -} - -fn lowlevel_spec( - builder: &mut FuncDefBuilder, - env: &Env, - block: BlockId, - layout: &Layout, - op: &LowLevel, - update_mode: roc_mono::ir::UpdateModeId, - arguments: &[Symbol], -) -> Result { - use LowLevel::*; - - let type_id = layout_spec(builder, layout, &WhenRecursive::Unreachable)?; - let mode = update_mode.to_bytes(); - let update_mode_var = UpdateModeVar(&mode); - - match op { - NumAdd | NumSub => { - // NOTE these numeric operations panic (e.g. on overflow) - - let pass_block = { - let block = builder.add_block(); - let value = new_num(builder, block)?; - BlockExpr(block, value) - }; - - let fail_block = { - let block = builder.add_block(); - let value = builder.add_terminate(block, type_id)?; - BlockExpr(block, value) - }; - - let sub_block = { - let block = builder.add_block(); - let choice = builder.add_choice(block, &[pass_block, fail_block])?; - - BlockExpr(block, choice) - }; - - builder.add_sub_block(block, sub_block) - } - NumToFrac => { - // just dream up a unit value - builder.add_make_tuple(block, &[]) - } - Eq | NotEq => { - // just dream up a unit value - builder.add_make_tuple(block, &[]) - } - NumLte | NumLt | NumGt | NumGte | NumCompare => { - // just dream up a unit value - builder.add_make_tuple(block, &[]) - } - ListLen | DictSize => { - // TODO should this touch the heap cell? - // just dream up a unit value - builder.add_make_tuple(block, &[]) - } - ListGetUnsafe => { - // NOTE the ListGet lowlevel op is only evaluated if the index is in-bounds - let list = env.symbols[&arguments[0]]; - - let bag = builder.add_get_tuple_field(block, list, LIST_BAG_INDEX)?; - let cell = builder.add_get_tuple_field(block, list, LIST_CELL_INDEX)?; - - let _unit = builder.add_touch(block, cell)?; - - builder.add_bag_get(block, bag) - } - ListReplaceUnsafe => { - let list = env.symbols[&arguments[0]]; - let to_insert = env.symbols[&arguments[2]]; - - let bag = builder.add_get_tuple_field(block, list, LIST_BAG_INDEX)?; - let cell = builder.add_get_tuple_field(block, list, LIST_CELL_INDEX)?; - - let _unit1 = builder.add_touch(block, cell)?; - let _unit2 = builder.add_update(block, update_mode_var, cell)?; - - builder.add_bag_insert(block, bag, to_insert)?; - - let old_value = builder.add_bag_get(block, bag)?; - let new_list = with_new_heap_cell(builder, block, bag)?; - builder.add_make_tuple(block, &[new_list, old_value]) - } - ListSwap => { - let list = env.symbols[&arguments[0]]; - - let bag = builder.add_get_tuple_field(block, list, LIST_BAG_INDEX)?; - let cell = builder.add_get_tuple_field(block, list, LIST_CELL_INDEX)?; - - let _unit = builder.add_update(block, update_mode_var, cell)?; - - with_new_heap_cell(builder, block, bag) - } - ListReverse => { - let list = env.symbols[&arguments[0]]; - - let bag = builder.add_get_tuple_field(block, list, LIST_BAG_INDEX)?; - let cell = builder.add_get_tuple_field(block, list, LIST_CELL_INDEX)?; - - let _unit = builder.add_update(block, update_mode_var, cell)?; - - with_new_heap_cell(builder, block, bag) - } - ListAppend => { - let list = env.symbols[&arguments[0]]; - let to_insert = env.symbols[&arguments[1]]; - - list_append(builder, block, update_mode_var, list, to_insert) - } - StrToUtf8 => { - let string = env.symbols[&arguments[0]]; - - let u8_type = builder.add_tuple_type(&[])?; - let bag = builder.add_empty_bag(block, u8_type)?; - let cell = builder.add_get_tuple_field(block, string, LIST_CELL_INDEX)?; - - builder.add_make_tuple(block, &[cell, bag]) - } - StrFromUtf8 => { - let list = env.symbols[&arguments[0]]; - - let cell = builder.add_get_tuple_field(block, list, LIST_CELL_INDEX)?; - let string = builder.add_make_tuple(block, &[cell])?; - - let byte_index = builder.add_make_tuple(block, &[])?; - let is_ok = builder.add_make_tuple(block, &[])?; - let problem_code = builder.add_make_tuple(block, &[])?; - - builder.add_make_tuple(block, &[byte_index, string, is_ok, problem_code]) - } - DictEmpty => match layout { - Layout::Builtin(Builtin::Dict(key_layout, value_layout)) => { - let key_id = layout_spec(builder, key_layout, &WhenRecursive::Unreachable)?; - let value_id = layout_spec(builder, value_layout, &WhenRecursive::Unreachable)?; - new_dict(builder, block, key_id, value_id) - } - _ => unreachable!("empty array does not have a list layout"), - }, - DictGetUnsafe => { - // NOTE DictGetUnsafe returns a { flag: Bool, value: v } - // when the flag is True, the value is found and defined; - // otherwise it is not and `Dict.get` should return `Err ...` - - let dict = env.symbols[&arguments[0]]; - let key = env.symbols[&arguments[1]]; - - // indicate that we use the key - builder.add_recursive_touch(block, key)?; - - let bag = builder.add_get_tuple_field(block, dict, DICT_BAG_INDEX)?; - let cell = builder.add_get_tuple_field(block, dict, DICT_CELL_INDEX)?; - - let _unit = builder.add_touch(block, cell)?; - builder.add_bag_get(block, bag) - } - DictInsert => { - let dict = env.symbols[&arguments[0]]; - let key = env.symbols[&arguments[1]]; - let value = env.symbols[&arguments[2]]; - - let key_value = builder.add_make_tuple(block, &[key, value])?; - - let bag = builder.add_get_tuple_field(block, dict, DICT_BAG_INDEX)?; - let cell = builder.add_get_tuple_field(block, dict, DICT_CELL_INDEX)?; - - let _unit = builder.add_update(block, update_mode_var, cell)?; - - builder.add_bag_insert(block, bag, key_value)?; - - with_new_heap_cell(builder, block, bag) - } - _other => { - // println!("missing {:?}", _other); - // TODO overly pessimstic - let arguments: Vec<_> = arguments.iter().map(|symbol| env.symbols[symbol]).collect(); - - let result_type = layout_spec(builder, layout, &WhenRecursive::Unreachable)?; - - builder.add_unknown_with(block, &arguments, result_type) - } - } -} - -fn recursive_tag_variant( - builder: &mut impl TypeContext, - union_layout: &UnionLayout, - fields: &[Layout], -) -> Result { - let when_recursive = WhenRecursive::Loop(*union_layout); - - build_recursive_tuple_type(builder, fields, &when_recursive) -} - -fn recursive_variant_types( - builder: &mut impl TypeContext, - union_layout: &UnionLayout, -) -> Result> { - use UnionLayout::*; - - let mut result; - - match union_layout { - NonRecursive(_) => { - unreachable!() - } - Recursive(tags) => { - result = Vec::with_capacity(tags.len()); - - for tag in tags.iter() { - result.push(recursive_tag_variant(builder, union_layout, tag)?); - } - } - NonNullableUnwrapped(fields) => { - result = vec![recursive_tag_variant(builder, union_layout, fields)?]; - } - NullableWrapped { - nullable_id, - other_tags: tags, - } => { - result = Vec::with_capacity(tags.len() + 1); - - let cutoff = *nullable_id as usize; - - for tag in tags[..cutoff].iter() { - result.push(recursive_tag_variant(builder, union_layout, tag)?); - } - - result.push(recursive_tag_variant(builder, union_layout, &[])?); - - for tag in tags[cutoff..].iter() { - result.push(recursive_tag_variant(builder, union_layout, tag)?); - } - } - NullableUnwrapped { - nullable_id, - other_fields: fields, - } => { - let unit = recursive_tag_variant(builder, union_layout, &[])?; - let other_type = recursive_tag_variant(builder, union_layout, fields)?; - - if *nullable_id { - // nullable_id == 1 - result = vec![other_type, unit]; - } else { - result = vec![unit, other_type]; - } - } - } - - Ok(result) -} - -#[allow(dead_code)] -fn worst_case_type(context: &mut impl TypeContext) -> Result { - let cell = context.add_heap_cell_type(); - context.add_bag_type(cell) -} - -fn expr_spec<'a>( - builder: &mut FuncDefBuilder, - env: &mut Env<'a>, - block: BlockId, - layout: &Layout<'a>, - expr: &Expr<'a>, -) -> Result { - use Expr::*; - - match expr { - Literal(literal) => literal_spec(builder, block, literal), - Call(call) => call_spec(builder, env, block, layout, call), - Reuse { - tag_layout, - tag_name: _, - tag_id, - arguments, - .. - } - | Tag { - tag_layout, - tag_name: _, - tag_id, - arguments, - } => { - let data_id = build_tuple_value(builder, env, block, arguments)?; - - let value_id = match tag_layout { - UnionLayout::NonRecursive(tags) => { - let variant_types = - non_recursive_variant_types(builder, tags, &WhenRecursive::Unreachable)?; - let value_id = build_tuple_value(builder, env, block, arguments)?; - return builder.add_make_union(block, &variant_types, *tag_id as u32, value_id); - } - UnionLayout::NonNullableUnwrapped(_) => { - let value_id = data_id; - - let type_name_bytes = recursive_tag_union_name_bytes(tag_layout).as_bytes(); - let type_name = TypeName(&type_name_bytes); - - env.type_names.insert(*tag_layout); - - return builder.add_make_named(block, MOD_APP, type_name, value_id); - } - UnionLayout::Recursive(_) => data_id, - UnionLayout::NullableWrapped { .. } => data_id, - UnionLayout::NullableUnwrapped { .. } => data_id, - }; - - let variant_types = recursive_variant_types(builder, tag_layout)?; - - let union_id = - builder.add_make_union(block, &variant_types, *tag_id as u32, value_id)?; - - let tag_value_id = with_new_heap_cell(builder, block, union_id)?; - - let type_name_bytes = recursive_tag_union_name_bytes(tag_layout).as_bytes(); - let type_name = TypeName(&type_name_bytes); - - env.type_names.insert(*tag_layout); - - builder.add_make_named(block, MOD_APP, type_name, tag_value_id) - } - ExprBox { symbol } => { - let value_id = env.symbols[symbol]; - - with_new_heap_cell(builder, block, value_id) - } - ExprUnbox { symbol } => { - let tuple_id = env.symbols[symbol]; - - builder.add_get_tuple_field(block, tuple_id, BOX_VALUE_INDEX) - } - Struct(fields) => build_tuple_value(builder, env, block, fields), - UnionAtIndex { - index, - tag_id, - structure, - union_layout, - } => match union_layout { - UnionLayout::NonRecursive(_) => { - let index = (*index) as u32; - let tag_value_id = env.symbols[structure]; - let tuple_value_id = - builder.add_unwrap_union(block, tag_value_id, *tag_id as u32)?; - - builder.add_get_tuple_field(block, tuple_value_id, index) - } - UnionLayout::Recursive(_) - | UnionLayout::NullableUnwrapped { .. } - | UnionLayout::NullableWrapped { .. } => { - let index = (*index) as u32; - let tag_value_id = env.symbols[structure]; - - let type_name_bytes = recursive_tag_union_name_bytes(union_layout).as_bytes(); - let type_name = TypeName(&type_name_bytes); - - // unwrap the named wrapper - let union_id = builder.add_unwrap_named(block, MOD_APP, type_name, tag_value_id)?; - - // now we have a tuple (cell, union { ... }); decompose - let heap_cell = builder.add_get_tuple_field(block, union_id, TAG_CELL_INDEX)?; - let union_data = builder.add_get_tuple_field(block, union_id, TAG_DATA_INDEX)?; - - // we're reading from this value, so touch the heap cell - builder.add_touch(block, heap_cell)?; - - // next, unwrap the union at the tag id that we've got - let variant_id = builder.add_unwrap_union(block, union_data, *tag_id as u32)?; - - builder.add_get_tuple_field(block, variant_id, index) - } - UnionLayout::NonNullableUnwrapped { .. } => { - let index = (*index) as u32; - debug_assert!(*tag_id == 0); - - let tag_value_id = env.symbols[structure]; - - let type_name_bytes = recursive_tag_union_name_bytes(union_layout).as_bytes(); - let type_name = TypeName(&type_name_bytes); - - // a tuple ( cell, union { ... } ) - let union_id = builder.add_unwrap_named(block, MOD_APP, type_name, tag_value_id)?; - - // decompose - let heap_cell = builder.add_get_tuple_field(block, union_id, TAG_CELL_INDEX)?; - let union_data = builder.add_get_tuple_field(block, union_id, TAG_DATA_INDEX)?; - - // we're reading from this value, so touch the heap cell - builder.add_touch(block, heap_cell)?; - - // next, unwrap the union at the tag id that we've got - let variant_id = builder.add_unwrap_union(block, union_data, *tag_id as u32)?; - - builder.add_get_tuple_field(block, variant_id, index) - } - }, - StructAtIndex { - index, structure, .. - } => { - let value_id = env.symbols[structure]; - builder.add_get_tuple_field(block, value_id, *index as u32) - } - Array { elem_layout, elems } => { - let type_id = layout_spec(builder, elem_layout, &WhenRecursive::Unreachable)?; - - let list = new_list(builder, block, type_id)?; - - let mut bag = builder.add_get_tuple_field(block, list, LIST_BAG_INDEX)?; - let mut all_constants = true; - - for element in elems.iter() { - let value_id = if let ListLiteralElement::Symbol(symbol) = element { - all_constants = false; - env.symbols[symbol] - } else { - builder.add_make_tuple(block, &[]).unwrap() - }; - - bag = builder.add_bag_insert(block, bag, value_id)?; - } - - if all_constants { - new_static_list(builder, block) - } else { - with_new_heap_cell(builder, block, bag) - } - } - - EmptyArray => match layout { - Layout::Builtin(Builtin::List(element_layout)) => { - let type_id = layout_spec(builder, element_layout, &WhenRecursive::Unreachable)?; - new_list(builder, block, type_id) - } - _ => unreachable!("empty array does not have a list layout"), - }, - Reset { symbol, .. } => { - let type_id = layout_spec(builder, layout, &WhenRecursive::Unreachable)?; - let value_id = env.symbols[symbol]; - - builder.add_unknown_with(block, &[value_id], type_id) - } - RuntimeErrorFunction(_) => { - let type_id = layout_spec(builder, layout, &WhenRecursive::Unreachable)?; - - builder.add_terminate(block, type_id) - } - GetTagId { .. } => { - // TODO touch heap cell in recursive cases - - builder.add_make_tuple(block, &[]) - } - } -} - -fn literal_spec( - builder: &mut FuncDefBuilder, - block: BlockId, - literal: &Literal, -) -> Result { - use Literal::*; - - match literal { - Str(_) => new_static_string(builder, block), - Int(_) | U128(_) | Float(_) | Decimal(_) | Bool(_) | Byte(_) => { - builder.add_make_tuple(block, &[]) - } - } -} - -fn layout_spec( - builder: &mut impl TypeContext, - layout: &Layout, - when_recursive: &WhenRecursive, -) -> Result { - layout_spec_help(builder, layout, when_recursive) -} - -fn non_recursive_variant_types( - builder: &mut impl TypeContext, - tags: &[&[Layout]], - // If there is a recursive pointer latent within this layout, coming from a containing layout. - when_recursive: &WhenRecursive, -) -> Result> { - let mut result = Vec::with_capacity(tags.len()); - - for tag in tags.iter() { - result.push(build_tuple_type(builder, tag, when_recursive)?); - } - - Ok(result) -} - -fn layout_spec_help( - builder: &mut impl TypeContext, - layout: &Layout, - when_recursive: &WhenRecursive, -) -> Result { - use Layout::*; - - match layout { - Builtin(builtin) => builtin_spec(builder, builtin, when_recursive), - Struct { field_layouts, .. } => { - build_recursive_tuple_type(builder, field_layouts, when_recursive) - } - LambdaSet(lambda_set) => layout_spec_help( - builder, - &lambda_set.runtime_representation(), - when_recursive, - ), - Union(union_layout) => { - match union_layout { - UnionLayout::NonRecursive(&[]) => { - // must model Void as Unit, otherwise we run into problems where - // we have to construct values of the void type, - // which is of course not possible - builder.add_tuple_type(&[]) - } - UnionLayout::NonRecursive(tags) => { - let variant_types = non_recursive_variant_types(builder, tags, when_recursive)?; - builder.add_union_type(&variant_types) - } - UnionLayout::Recursive(_) - | UnionLayout::NullableUnwrapped { .. } - | UnionLayout::NullableWrapped { .. } - | UnionLayout::NonNullableUnwrapped(_) => { - let type_name_bytes = recursive_tag_union_name_bytes(union_layout).as_bytes(); - let type_name = TypeName(&type_name_bytes); - - Ok(builder.add_named_type(MOD_APP, type_name)) - } - } - } - - Boxed(inner_layout) => { - let inner_type = layout_spec_help(builder, inner_layout, when_recursive)?; - let cell_type = builder.add_heap_cell_type(); - - builder.add_tuple_type(&[cell_type, inner_type]) - } - RecursivePointer => match when_recursive { - WhenRecursive::Unreachable => { - unreachable!() - } - WhenRecursive::Loop(union_layout) => match union_layout { - UnionLayout::NonRecursive(_) => unreachable!(), - UnionLayout::Recursive(_) - | UnionLayout::NullableUnwrapped { .. } - | UnionLayout::NullableWrapped { .. } - | UnionLayout::NonNullableUnwrapped(_) => { - let type_name_bytes = recursive_tag_union_name_bytes(union_layout).as_bytes(); - let type_name = TypeName(&type_name_bytes); - - Ok(builder.add_named_type(MOD_APP, type_name)) - } - }, - }, - } -} - -fn builtin_spec( - builder: &mut impl TypeContext, - builtin: &Builtin, - when_recursive: &WhenRecursive, -) -> Result { - use Builtin::*; - - match builtin { - Int(_) | Bool => builder.add_tuple_type(&[]), - Decimal | Float(_) => builder.add_tuple_type(&[]), - Str => str_type(builder), - Dict(key_layout, value_layout) => { - let value_type = layout_spec_help(builder, value_layout, when_recursive)?; - let key_type = layout_spec_help(builder, key_layout, when_recursive)?; - let element_type = builder.add_tuple_type(&[key_type, value_type])?; - - let cell = builder.add_heap_cell_type(); - let bag = builder.add_bag_type(element_type)?; - builder.add_tuple_type(&[cell, bag]) - } - Set(key_layout) => { - let value_type = builder.add_tuple_type(&[])?; - let key_type = layout_spec_help(builder, key_layout, when_recursive)?; - let element_type = builder.add_tuple_type(&[key_type, value_type])?; - - let cell = builder.add_heap_cell_type(); - let bag = builder.add_bag_type(element_type)?; - builder.add_tuple_type(&[cell, bag]) - } - List(element_layout) => { - let element_type = layout_spec_help(builder, element_layout, when_recursive)?; - - let cell = builder.add_heap_cell_type(); - let bag = builder.add_bag_type(element_type)?; - - builder.add_tuple_type(&[cell, bag]) - } - } -} - -fn str_type(builder: &mut TC) -> Result { - let cell_id = builder.add_heap_cell_type(); - builder.add_tuple_type(&[cell_id]) -} - -fn static_list_type(builder: &mut TC) -> Result { - let unit_type = builder.add_tuple_type(&[])?; - let cell = builder.add_heap_cell_type(); - let bag = builder.add_bag_type(unit_type)?; - - builder.add_tuple_type(&[cell, bag]) -} - -const OK_TAG_ID: u32 = 1; -const ERR_TAG_ID: u32 = 0; - -const LIST_CELL_INDEX: u32 = 0; -const LIST_BAG_INDEX: u32 = 1; - -const DICT_CELL_INDEX: u32 = LIST_CELL_INDEX; -const DICT_BAG_INDEX: u32 = LIST_BAG_INDEX; - -#[allow(dead_code)] -const BOX_CELL_INDEX: u32 = LIST_CELL_INDEX; -const BOX_VALUE_INDEX: u32 = LIST_BAG_INDEX; - -const TAG_CELL_INDEX: u32 = 0; -const TAG_DATA_INDEX: u32 = 1; - -fn with_new_heap_cell( - builder: &mut FuncDefBuilder, - block: BlockId, - value: ValueId, -) -> Result { - let cell = builder.add_new_heap_cell(block)?; - builder.add_make_tuple(block, &[cell, value]) -} - -fn new_list(builder: &mut FuncDefBuilder, block: BlockId, element_type: TypeId) -> Result { - let bag = builder.add_empty_bag(block, element_type)?; - with_new_heap_cell(builder, block, bag) -} - -fn new_dict( - builder: &mut FuncDefBuilder, - block: BlockId, - key_type: TypeId, - value_type: TypeId, -) -> Result { - let element_type = builder.add_tuple_type(&[key_type, value_type])?; - let bag = builder.add_empty_bag(block, element_type)?; - with_new_heap_cell(builder, block, bag) -} - -fn new_static_string(builder: &mut FuncDefBuilder, block: BlockId) -> Result { - let module = MOD_APP; - - builder.add_const_ref(block, module, STATIC_STR_NAME) -} - -fn new_static_list(builder: &mut FuncDefBuilder, block: BlockId) -> Result { - let module = MOD_APP; - - builder.add_const_ref(block, module, STATIC_LIST_NAME) -} - -fn new_num(builder: &mut FuncDefBuilder, block: BlockId) -> Result { - // we model all our numbers as unit values - builder.add_make_tuple(block, &[]) -} diff --git a/compiler/arena_pool/Cargo.toml b/compiler/arena_pool/Cargo.toml deleted file mode 100644 index f3d0aa6e08..0000000000 --- a/compiler/arena_pool/Cargo.toml +++ /dev/null @@ -1,8 +0,0 @@ -[package] -name = "arena-pool" -version = "0.1.0" -authors = ["The Roc Contributors"] -license = "UPL-1.0" -repository = "https://github.com/rtfeldman/roc" -edition = "2021" -description = "A CLI for Roc" diff --git a/compiler/arena_pool/src/lib.rs b/compiler/arena_pool/src/lib.rs deleted file mode 100644 index c9e2bb1c79..0000000000 --- a/compiler/arena_pool/src/lib.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod pool; diff --git a/compiler/arena_pool/src/pool.rs b/compiler/arena_pool/src/pool.rs deleted file mode 100644 index f44abd3334..0000000000 --- a/compiler/arena_pool/src/pool.rs +++ /dev/null @@ -1,396 +0,0 @@ -use std::marker::PhantomPinned; -use std::ptr::{copy_nonoverlapping, NonNull}; - -pub struct ArenaRef { - ptr: NonNull, - _pin: PhantomPinned, -} - -impl ArenaRef { - pub fn get<'a, A: AsArena>(&'a self, arena: &A) -> &'a T { - arena.verify_ownership(self.ptr); - - // SAFETY: we know this pointer is safe to follow because it will only - // get deallocated once the pool where it was created gets deallocated - // (along with all of the Arenas it detached), and we just verified that - // this ArenaRef's ID matches a pool which has not yet been deallocated. - unsafe { self.ptr.as_ref() } - } - - pub fn get_mut<'a, A: AsArena>(&'a mut self, arena: &A) -> &'a mut T { - arena.verify_ownership(self.ptr); - - // SAFETY: we know this pointer is safe to follow because it will only - // get deallocated once the pool where it was created gets deallocated - // (along with all of the Arenas it detached), and we just verified that - // this ArenaRef's ID matches a pool which has not yet been deallocated. - unsafe { self.ptr.as_mut() } - } -} - -/// Like a Vec, except the capacity you give it initially is its maximum -/// capacity forever. If you ever exceed it, it'll panic! -pub struct ArenaVec { - buffer_ptr: NonNull, - len: usize, - capacity: usize, - _pin: PhantomPinned, -} - -impl ArenaVec { - pub fn new_in(arena: &mut Arena) -> Self { - // We can't start with a NonNull::dangling pointer because when we go - // to push elements into this, they'll try to verify the dangling - // pointer resides in the arena it was given, which will likely panic. - // - // Instead, we'll take a pointer inside the array but never use it - // other than for verification, because our capacity is 0. - Self::with_capacity_in(0, arena) - } - - pub fn with_capacity_in(capacity: usize, arena: &mut Arena) -> Self { - let ptr = arena.alloc_vec(capacity); - - Self { - buffer_ptr: unsafe { NonNull::new_unchecked(ptr) }, - capacity, - len: 0, - _pin: PhantomPinned, - } - } - - pub fn push(&mut self, val: T, arena: &mut Arena) { - // Verify that this is the arena where we originally got our buffer, - // and is therefore safe to read and to write to. (If we have sufficient - // capacity, we'll write to it, and otherwise we'll read from it when - // copying our buffer over to the new reserved block.) - arena.verify_ownership(self.buffer_ptr); - - if self.len <= self.capacity { - // We're all set! - // - // This empty branch is just here for branch prediction, - // since this should be the most common case in practice. - } else { - // Double our capacity and reserve a new block. - self.capacity *= 2; - - let ptr = arena.alloc_vec(self.capacity); - - // SAFETY: the existing buffer must have at least self.len elements, - // as must the new one, so copying that many between them is safe. - unsafe { - // Copy all elements from the current buffer into the new one - copy_nonoverlapping(self.buffer_ptr.as_ptr(), ptr, self.len); - } - - self.buffer_ptr = unsafe { NonNull::new_unchecked(ptr) }; - } - - // Store the element in the appropriate memory address. - let elem_ptr = unsafe { &mut *self.buffer_ptr.as_ptr().add(self.len) }; - - *elem_ptr = val; - - self.len += 1; - } - - pub fn get<'a>(&'a self, index: usize, arena: &Arena) -> Option<&'a T> { - arena.verify_ownership(self.buffer_ptr); - - if index < self.len { - // SAFETY: we know this pointer is safe to follow because we've - // done a bounds check, and because we know it will only get - // deallocated once the pool where it was created gets deallocated - // (along with all of the Arenas it detached), and we just verified that - // this ArenaRef's ID matches a pool which has not yet been deallocated. - Some(unsafe { &*self.buffer_ptr.as_ptr().add(index) }) - } else { - None - } - } - - pub fn get_mut<'a>(&'a mut self, index: usize, arena: &Arena) -> Option<&'a mut T> { - arena.verify_ownership(self.buffer_ptr); - - if index < self.len { - // SAFETY: we know this pointer is safe to follow because we've - // done a bounds check, and because we know it will only get - // deallocated once the pool where it was created gets deallocated - // (along with all of the Arenas it detached), and we just verified that - // this ArenaRef's ID matches a pool which has not yet been deallocated. - Some(unsafe { &mut *self.buffer_ptr.as_ptr().add(index) }) - } else { - None - } - } -} - -#[derive(PartialEq, Eq)] -pub struct ArenaPool { - first_chunk: Vec, - extra_chunks: Vec>, - num_leased: usize, - default_chunk_capacity: usize, -} - -impl ArenaPool { - const DEFAULT_CHUNK_SIZE: usize = 1024; - - /// Be careful! Both of these arguments are of type usize. - /// - /// The first is the number of elements that will be in each arena. - /// The second is the number of arenas. - /// - /// This returns a new Pool, and also an iterator of Arenas. These Arenas can - /// be given to different threads, where they can be used to allocate - /// ArenaRef and ArenaVec values which can then be dereferenced by the Arena - /// that created them, or by this pool once those Arenas have been - /// reabsorbed back into it. - /// - /// (A word of warning: if you try to use this pool to dereference ArenaRec - /// and ArenaVec values which were allocated by arenas that have *not* yet - /// been reabsorbed, it may work some of the time and panic other times, - /// depending on whether the arena needed to allocate extra chunks beyond - /// its initial chunk. tl;dr - doing that may panic, so don't try it!) - /// - /// Before this pool gets dropped, you must call reabsorb() on every - /// arena that has been leased - otherwise, you'll get a panic when this - /// gets dropped! The memory safety of the system depends on all arenas - /// having been reabsorbed before the pool gets deallocated, which is why - /// the pool's Drop implementation enforces it. - pub fn new(num_arenas: usize, elems_per_arena: usize) -> (ArenaPool, ArenaIter) { - Self::with_chunk_size(num_arenas, elems_per_arena, Self::DEFAULT_CHUNK_SIZE) - } - - /// Like `new`, except you can also specify the chunk size that each - /// arena will use to allocate its extra chunks if it runs out of space - /// in its main buffer. - /// - /// Things will run fastest if that main buffer never runs out, though! - pub fn with_chunk_size( - num_arenas: usize, - elems_per_arena: usize, - chunk_size: usize, - ) -> (ArenaPool, ArenaIter) { - let mut first_chunk = Vec::with_capacity(elems_per_arena * num_arenas); - let iter = ArenaIter { - ptr: first_chunk.as_mut_ptr(), - quantity_remaining: num_arenas, - first_chunk_capacity: elems_per_arena, - }; - let pool = Self { - first_chunk, - extra_chunks: Vec::new(), - num_leased: num_arenas, - default_chunk_capacity: chunk_size, - }; - - (pool, iter) - } - - /// Return an arena to the pool. (This would have been called "return" but - /// that's a reserved keyword.) - pub fn reabsorb(&mut self, arena: Arena) { - // Ensure we're reabsorbing an arena that was - // actually leased by this pool in the first place! - verify_ownership( - self.first_chunk.as_ptr(), - self.first_chunk.capacity(), - &self.extra_chunks, - arena.first_chunk_ptr, - ); - - // Add the arena's extra chunks to our own, so their memory remains live - // after the arena gets dropped. This is important, because at this - // point their pointers can still potentially be dereferenced! - self.extra_chunks.extend(arena.extra_chunks.into_iter()); - - self.num_leased -= 1; - } -} - -impl Drop for ArenaPool { - fn drop(&mut self) { - // When an ArenaPool gets dropped, it must not have any leased - // arenas remaining. If it does, there will be outstanding IDs which - // could be used with those non-reabsorbed Arenas to read freed memory! - // This would be a use-after-free; we panic rather than permit that. - assert_eq!(self.num_leased, 0); - } -} - -pub struct ArenaIter { - ptr: *mut T, - quantity_remaining: usize, - first_chunk_capacity: usize, -} - -// Implement `Iterator` for `Fibonacci`. -// The `Iterator` trait only requires a method to be defined for the `next` element. -impl Iterator for ArenaIter { - type Item = Arena; - - // Here, we define the sequence using `.curr` and `.next`. - // The return type is `Option`: - // * When the `Iterator` is finished, `None` is returned. - // * Otherwise, the next value is wrapped in `Some` and returned. - fn next(&mut self) -> Option> { - if self.quantity_remaining != 0 { - let first_chunk_ptr = self.ptr; - - self.ptr = unsafe { self.ptr.add(self.first_chunk_capacity) }; - self.quantity_remaining -= 1; - - Some(Arena { - first_chunk_ptr, - first_chunk_len: 0, - first_chunk_cap: self.first_chunk_capacity, - extra_chunks: Vec::new(), - }) - } else { - None - } - } -} - -#[derive(PartialEq, Eq)] -pub struct Arena { - first_chunk_ptr: *mut T, - first_chunk_len: usize, - first_chunk_cap: usize, - extra_chunks: Vec>, -} - -impl Arena { - pub fn alloc(&mut self, val: T) -> ArenaRef { - let ptr: *mut T = if self.first_chunk_len < self.first_chunk_cap { - // We have enough room in the first chunk for 1 allocation. - self.first_chunk_len += 1; - - // Return a pointer to the next available slot. - unsafe { self.first_chunk_ptr.add(self.first_chunk_len) } - } else { - // We ran out of space in the first chunk, so we turn to extra chunks. - // First, ensure that we have an extra chunk with enough space in it. - match self.extra_chunks.last() { - Some(chunk) => { - if chunk.len() >= chunk.capacity() { - // We've run out of space in our last chunk. Create a new one! - self.extra_chunks - .push(Vec::with_capacity(self.first_chunk_cap)); - } - } - None => { - // We've never had extra chunks until now. Create the first one! - self.extra_chunks - .push(Vec::with_capacity(self.first_chunk_cap)); - } - } - - let chunk = self.extra_chunks.last_mut().unwrap(); - let index = chunk.len(); - - chunk.push(val); - - // Get a pointer to a memory address within our particular chunk. - &mut chunk[index] - }; - - ArenaRef { - ptr: unsafe { NonNull::new_unchecked(ptr) }, - _pin: PhantomPinned, - } - } - - fn alloc_vec(&mut self, num_elems: usize) -> *mut T { - if self.first_chunk_len + num_elems <= self.first_chunk_cap { - // We have enough room in the first chunk for this vec. - self.first_chunk_len += num_elems; - - // Return a pointer to the next available element. - unsafe { self.first_chunk_ptr.add(self.first_chunk_len) } - } else { - let new_chunk_cap = self.first_chunk_cap.max(num_elems); - - // We ran out of space in the first chunk, so we turn to extra chunks. - // First, ensure that we have an extra chunk with enough space in it. - match self.extra_chunks.last() { - Some(chunk) => { - if chunk.len() + num_elems >= chunk.capacity() { - // We don't have enough space in our last chunk. - // Create a new one! - self.extra_chunks.push(Vec::with_capacity(new_chunk_cap)); - } - } - None => { - // We've never had extra chunks until now. Create the first one! - self.extra_chunks.push(Vec::with_capacity(new_chunk_cap)); - } - } - - let chunk = self.extra_chunks.last_mut().unwrap(); - let index = chunk.len(); - - // Get a pointer to a memory address within our particular chunk. - &mut chunk[index] - } - } -} - -pub trait AsArena { - fn verify_ownership(&self, ptr: NonNull); -} - -impl AsArena for ArenaPool { - fn verify_ownership(&self, ptr: NonNull) { - verify_ownership( - self.first_chunk.as_ptr(), - self.first_chunk.capacity(), - &self.extra_chunks, - ptr.as_ptr(), - ); - } -} - -impl AsArena for Arena { - fn verify_ownership(&self, ptr: NonNull) { - verify_ownership( - self.first_chunk_ptr, - self.first_chunk_cap, - &self.extra_chunks, - ptr.as_ptr(), - ); - } -} - -fn verify_ownership( - first_chunk_ptr: *const T, - first_chunk_cap: usize, - extra_chunks: &[Vec], - ptr: *const T, -) { - let addr = ptr as usize; - let start_addr = first_chunk_ptr as usize; - let end_addr = start_addr + first_chunk_cap; - - if start_addr <= addr && addr < end_addr { - // This is within our first chunk's address space, so it's verified! - } else { - // This wasn't within our first chunk's address space, so we need - // to see if we can find it in one of our extra_chunks. - for chunk in extra_chunks { - let start_addr = chunk.as_ptr() as usize; - let end_addr = start_addr + chunk.capacity(); - - if start_addr <= addr && addr < end_addr { - // Found it! No need to loop anymore; verification passed. - return; - } - } - - // The address wasn't within any of our chunks' bounds. - // Panic to avoid use-after-free errors! - panic!("Pointer ownership verification failed."); - } -} diff --git a/compiler/build/Cargo.toml b/compiler/build/Cargo.toml deleted file mode 100644 index 17ff3d5609..0000000000 --- a/compiler/build/Cargo.toml +++ /dev/null @@ -1,44 +0,0 @@ -[package] -name = "roc_build" -version = "0.1.0" -authors = ["The Roc Contributors"] -license = "UPL-1.0" -edition = "2021" - -[dependencies] -roc_collections = { path = "../collections" } -roc_can = { path = "../can" } -roc_parse = { path = "../parse" } -roc_region = { path = "../region" } -roc_module = { path = "../module" } -roc_problem = { path = "../problem" } -roc_types = { path = "../types" } -roc_builtins = { path = "../builtins" } -roc_constrain = { path = "../constrain" } -roc_unify = { path = "../unify" } -roc_solve = { path = "../solve" } -roc_mono = { path = "../mono" } -roc_load = { path = "../load" } -roc_target = { path = "../roc_target" } -roc_gen_llvm = { path = "../gen_llvm" } -roc_gen_wasm = { path = "../gen_wasm" } -roc_gen_dev = { path = "../gen_dev", default-features = false } -roc_reporting = { path = "../../reporting" } -roc_error_macros = { path = "../../error_macros" } -roc_std = { path = "../../roc_std", default-features = false } -bumpalo = { version = "3.8.0", features = ["collections"] } -libloading = "0.7.1" -tempfile = "3.2.0" -inkwell = { path = "../../vendor/inkwell" } -target-lexicon = "0.12.3" -wasi_libc_sys = { path = "../../wasi-libc-sys" } - -[target.'cfg(target_os = "macos")'.dependencies] -serde_json = "1.0.69" - -[features] -target-arm = [] -target-aarch64 = ["roc_gen_dev/target-aarch64"] -target-x86 = [] -target-x86_64 = ["roc_gen_dev/target-x86_64"] -target-wasm32 = [] diff --git a/compiler/build/src/lib.rs b/compiler/build/src/lib.rs deleted file mode 100644 index 7ee0e97721..0000000000 --- a/compiler/build/src/lib.rs +++ /dev/null @@ -1,6 +0,0 @@ -#![warn(clippy::dbg_macro)] -// See github.com/rtfeldman/roc/issues/800 for discussion of the large_enum_variant check. -#![allow(clippy::large_enum_variant)] -pub mod link; -pub mod program; -pub mod target; diff --git a/compiler/build/src/link.rs b/compiler/build/src/link.rs deleted file mode 100644 index 2145d9f10f..0000000000 --- a/compiler/build/src/link.rs +++ /dev/null @@ -1,1201 +0,0 @@ -use crate::target::{arch_str, target_zig_str}; -use libloading::{Error, Library}; -use roc_builtins::bitcode; -use roc_error_macros::internal_error; -use roc_mono::ir::OptLevel; -use std::collections::HashMap; -use std::env; -use std::io; -use std::path::{Path, PathBuf}; -use std::process::{self, Child, Command, Output}; -use target_lexicon::{Architecture, OperatingSystem, Triple}; -use wasi_libc_sys::{WASI_COMPILER_RT_PATH, WASI_LIBC_PATH}; - -fn zig_executable() -> String { - match std::env::var("ROC_ZIG") { - Ok(path) => path, - Err(_) => "zig".into(), - } -} - -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub enum LinkType { - // These numbers correspond to the --lib and --no-link flags - Executable = 0, - Dylib = 1, - None = 2, -} - -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub enum LinkingStrategy { - /// Compile app and host object files, then use a linker like lld or wasm-ld - Legacy, - /// Compile app and host object files, then use the Roc surgical linker - Surgical, - /// Initialise the backend from a host object file, then add the app to it. No linker needed. - Additive, -} - -/// input_paths can include the host as well as the app. e.g. &["host.o", "roc_app.o"] -pub fn link( - target: &Triple, - output_path: PathBuf, - input_paths: &[&str], - link_type: LinkType, -) -> io::Result<(Child, PathBuf)> { - match target { - Triple { - architecture: Architecture::Wasm32, - .. - } => link_wasm32(target, output_path, input_paths, link_type), - Triple { - operating_system: OperatingSystem::Linux, - .. - } => link_linux(target, output_path, input_paths, link_type), - Triple { - operating_system: OperatingSystem::Darwin, - .. - } => link_macos(target, output_path, input_paths, link_type), - Triple { - operating_system: OperatingSystem::Windows, - .. - } => link_windows(target, output_path, input_paths, link_type), - _ => panic!("TODO gracefully handle unsupported target: {:?}", target), - } -} - -fn find_zig_str_path() -> PathBuf { - let zig_str_path = PathBuf::from("compiler/builtins/bitcode/src/str.zig"); - - if std::path::Path::exists(&zig_str_path) { - return zig_str_path; - } - - // when running the tests, we start in the /cli directory - let zig_str_path = PathBuf::from("../compiler/builtins/bitcode/src/str.zig"); - if std::path::Path::exists(&zig_str_path) { - return zig_str_path; - } - - panic!("cannot find `str.zig`. Launch me from either the root of the roc repo or one level down(roc/examples, roc/cli...)") -} - -fn find_wasi_libc_path() -> PathBuf { - // Environment variable defined in wasi-libc-sys/build.rs - let wasi_libc_pathbuf = PathBuf::from(WASI_LIBC_PATH); - if std::path::Path::exists(&wasi_libc_pathbuf) { - return wasi_libc_pathbuf; - } - - panic!("cannot find `wasi-libc.a`") -} - -#[cfg(not(target_os = "macos"))] -#[allow(clippy::too_many_arguments)] -pub fn build_zig_host_native( - env_path: &str, - env_home: &str, - emit_bin: &str, - zig_host_src: &str, - zig_str_path: &str, - target: &str, - opt_level: OptLevel, - shared_lib_path: Option<&Path>, - _target_valgrind: bool, -) -> Output { - let mut command = Command::new(&zig_executable()); - command - .env_clear() - .env("PATH", env_path) - .env("HOME", env_home); - if let Some(shared_lib_path) = shared_lib_path { - command.args(&[ - "build-exe", - "-fPIE", - shared_lib_path.to_str().unwrap(), - bitcode::BUILTINS_HOST_OBJ_PATH, - ]); - } else { - command.args(&["build-obj", "-fPIC"]); - } - command.args(&[ - zig_host_src, - emit_bin, - "--pkg-begin", - "str", - zig_str_path, - "--pkg-end", - // include the zig runtime - "-fcompiler-rt", - // include libc - "--library", - "c", - // cross-compile? - "-target", - target, - ]); - - // use single threaded testing for cli_run and enable this code if valgrind fails with unhandled instruction bytes, see #1963. - /*if target_valgrind { - command.args(&[ - "-mcpu", - "x86_64" - ]); - }*/ - - if matches!(opt_level, OptLevel::Optimize) { - command.args(&["-O", "ReleaseSafe"]); - } else if matches!(opt_level, OptLevel::Size) { - command.args(&["-O", "ReleaseSmall"]); - } - command.output().unwrap() -} - -#[cfg(target_os = "macos")] -#[allow(clippy::too_many_arguments)] -pub fn build_zig_host_native( - env_path: &str, - env_home: &str, - emit_bin: &str, - zig_host_src: &str, - zig_str_path: &str, - _target: &str, - opt_level: OptLevel, - shared_lib_path: Option<&Path>, - // For compatibility with the non-macOS def above. Keep these in sync. - _target_valgrind: bool, -) -> Output { - use serde_json::Value; - - // Run `zig env` to find the location of zig's std/ directory - let zig_env_output = Command::new(&zig_executable()) - .args(&["env"]) - .output() - .unwrap(); - - let zig_env_json = if zig_env_output.status.success() { - std::str::from_utf8(&zig_env_output.stdout).unwrap_or_else(|utf8_err| { - panic!( - "`zig env` failed; its stderr output was invalid utf8 ({:?})", - utf8_err - ); - }) - } else { - match std::str::from_utf8(&zig_env_output.stderr) { - Ok(stderr) => panic!("`zig env` failed - stderr output was: {:?}", stderr), - Err(utf8_err) => panic!( - "`zig env` failed; its stderr output was invalid utf8 ({:?})", - utf8_err - ), - } - }; - - let mut zig_compiler_rt_path = match serde_json::from_str(zig_env_json) { - Ok(Value::Object(map)) => match map.get("std_dir") { - Some(Value::String(std_dir)) => PathBuf::from(Path::new(std_dir)), - _ => { - panic!("Expected JSON containing a `std_dir` String field from `zig env`, but got: {:?}", zig_env_json); - } - }, - _ => { - panic!( - "Expected JSON containing a `std_dir` field from `zig env`, but got: {:?}", - zig_env_json - ); - } - }; - - zig_compiler_rt_path.push("special"); - zig_compiler_rt_path.push("compiler_rt.zig"); - - let mut command = Command::new(&zig_executable()); - command - .env_clear() - .env("PATH", &env_path) - .env("HOME", &env_home); - if let Some(shared_lib_path) = shared_lib_path { - command.args(&[ - "build-exe", - "-fPIE", - shared_lib_path.to_str().unwrap(), - bitcode::BUILTINS_HOST_OBJ_PATH, - ]); - } else { - command.args(&["build-obj", "-fPIC"]); - } - command.args(&[ - zig_host_src, - emit_bin, - "--pkg-begin", - "str", - zig_str_path, - "--pkg-end", - // include the zig runtime - "--pkg-begin", - "compiler_rt", - zig_compiler_rt_path.to_str().unwrap(), - "--pkg-end", - // include libc - "--library", - "c", - ]); - if matches!(opt_level, OptLevel::Optimize) { - command.args(&["-O", "ReleaseSafe"]); - } else if matches!(opt_level, OptLevel::Size) { - command.args(&["-O", "ReleaseSmall"]); - } - command.output().unwrap() -} - -pub fn build_zig_host_wasm32( - env_path: &str, - env_home: &str, - emit_bin: &str, - zig_host_src: &str, - zig_str_path: &str, - opt_level: OptLevel, - shared_lib_path: Option<&Path>, -) -> Output { - if shared_lib_path.is_some() { - unimplemented!("Linking a shared library to wasm not yet implemented"); - } - - let zig_target = if matches!(opt_level, OptLevel::Development) { - "wasm32-wasi" - } else { - // For LLVM backend wasm we are emitting a .bc file anyway so this target is OK - "i386-linux-musl" - }; - - // NOTE currently just to get compiler warnings if the host code is invalid. - // the produced artifact is not used - // - // NOTE we're emitting LLVM IR here (again, it is not actually used) - // - // we'd like to compile with `-target wasm32-wasi` but that is blocked on - // - // https://github.com/ziglang/zig/issues/9414 - let mut command = Command::new(&zig_executable()); - let args = &[ - "build-obj", - zig_host_src, - emit_bin, - "--pkg-begin", - "str", - zig_str_path, - "--pkg-end", - // include the zig runtime - // "-fcompiler-rt", - // include libc - "--library", - "c", - "-target", - zig_target, - // "-femit-llvm-ir=/home/folkertdev/roc/roc/examples/benchmarks/platform/host.ll", - "-fPIC", - "--strip", - ]; - - command - .env_clear() - .env("PATH", env_path) - .env("HOME", env_home) - .args(args); - - if matches!(opt_level, OptLevel::Optimize) { - command.args(&["-O", "ReleaseSafe"]); - } else if matches!(opt_level, OptLevel::Size) { - command.args(&["-O", "ReleaseSmall"]); - } - command.output().unwrap() -} - -pub fn build_c_host_native( - env_path: &str, - env_home: &str, - dest: &str, - sources: &[&str], - opt_level: OptLevel, - shared_lib_path: Option<&Path>, -) -> Output { - let mut command = Command::new("clang"); - command - .env_clear() - .env("PATH", &env_path) - .env("HOME", &env_home) - .args(sources) - .args(&["-o", dest]); - if let Some(shared_lib_path) = shared_lib_path { - command.args(&[ - shared_lib_path.to_str().unwrap(), - bitcode::BUILTINS_HOST_OBJ_PATH, - "-fPIE", - "-pie", - "-lm", - "-lpthread", - "-ldl", - "-lrt", - "-lutil", - ]); - } else { - command.args(&["-fPIC", "-c"]); - } - if matches!(opt_level, OptLevel::Optimize) { - command.arg("-O3"); - } else if matches!(opt_level, OptLevel::Size) { - command.arg("-Os"); - } - command.output().unwrap() -} - -pub fn build_swift_host_native( - env_path: &str, - env_home: &str, - dest: &str, - sources: &[&str], - opt_level: OptLevel, - shared_lib_path: Option<&Path>, - objc_header_path: Option<&str>, -) -> Output { - if shared_lib_path.is_some() { - unimplemented!("Linking a shared library to Swift not yet implemented"); - } - - let mut command = Command::new("xcrun"); // xcrun helps swiftc to find the right header files - command - .env_clear() - .env("PATH", &env_path) - .env("HOME", &env_home) - .arg("swiftc") - .args(sources) - .arg("-emit-object") - .arg("-parse-as-library") - .args(&["-o", dest]); - - if let Some(objc_header) = objc_header_path { - command.args(&["-import-objc-header", objc_header]); - } - - if matches!(opt_level, OptLevel::Optimize) { - command.arg("-O"); - } else if matches!(opt_level, OptLevel::Size) { - command.arg("-Osize"); - } - - command.output().unwrap() -} - -pub fn rebuild_host( - opt_level: OptLevel, - target: &Triple, - host_input_path: &Path, - shared_lib_path: Option<&Path>, - target_valgrind: bool, -) -> PathBuf { - let c_host_src = host_input_path.with_file_name("host.c"); - let c_host_dest = host_input_path.with_file_name("c_host.o"); - let zig_host_src = host_input_path.with_file_name("host.zig"); - let rust_host_src = host_input_path.with_file_name("host.rs"); - let rust_host_dest = host_input_path.with_file_name("rust_host.o"); - let cargo_host_src = host_input_path.with_file_name("Cargo.toml"); - let swift_host_src = host_input_path.with_file_name("host.swift"); - let swift_host_header_src = host_input_path.with_file_name("host.h"); - - let host_dest = if matches!(target.architecture, Architecture::Wasm32) { - if matches!(opt_level, OptLevel::Development) { - host_input_path.with_file_name("host.o") - } else { - host_input_path.with_file_name("host.bc") - } - } else { - host_input_path.with_file_name(if shared_lib_path.is_some() { - "dynhost" - } else { - "host.o" - }) - }; - - let env_path = env::var("PATH").unwrap_or_else(|_| "".to_string()); - let env_home = env::var("HOME").unwrap_or_else(|_| "".to_string()); - - if zig_host_src.exists() { - // Compile host.zig - - let zig_str_path = find_zig_str_path(); - - debug_assert!( - std::path::Path::exists(&zig_str_path), - "Cannot find str.zig, looking at {:?}", - &zig_str_path - ); - - let output = match target.architecture { - Architecture::Wasm32 => { - let emit_bin = if matches!(opt_level, OptLevel::Development) { - format!("-femit-bin={}", host_dest.to_str().unwrap()) - } else { - format!("-femit-llvm-ir={}", host_dest.to_str().unwrap()) - }; - build_zig_host_wasm32( - &env_path, - &env_home, - &emit_bin, - zig_host_src.to_str().unwrap(), - zig_str_path.to_str().unwrap(), - opt_level, - shared_lib_path, - ) - } - Architecture::X86_64 => { - let emit_bin = format!("-femit-bin={}", host_dest.to_str().unwrap()); - build_zig_host_native( - &env_path, - &env_home, - &emit_bin, - zig_host_src.to_str().unwrap(), - zig_str_path.to_str().unwrap(), - "native", - opt_level, - shared_lib_path, - target_valgrind, - ) - } - Architecture::X86_32(_) => { - let emit_bin = format!("-femit-bin={}", host_dest.to_str().unwrap()); - build_zig_host_native( - &env_path, - &env_home, - &emit_bin, - zig_host_src.to_str().unwrap(), - zig_str_path.to_str().unwrap(), - "i386-linux-musl", - opt_level, - shared_lib_path, - target_valgrind, - ) - } - - Architecture::Aarch64(_) => { - let emit_bin = format!("-femit-bin={}", host_dest.to_str().unwrap()); - build_zig_host_native( - &env_path, - &env_home, - &emit_bin, - zig_host_src.to_str().unwrap(), - zig_str_path.to_str().unwrap(), - target_zig_str(target), - opt_level, - shared_lib_path, - target_valgrind, - ) - } - _ => panic!("Unsupported architecture {:?}", target.architecture), - }; - - validate_output("host.zig", &zig_executable(), output) - } else if cargo_host_src.exists() { - // Compile and link Cargo.toml, if it exists - let cargo_dir = host_input_path.parent().unwrap(); - let cargo_out_dir = cargo_dir.join("target").join( - if matches!(opt_level, OptLevel::Optimize | OptLevel::Size) { - "release" - } else { - "debug" - }, - ); - - let mut command = Command::new("cargo"); - command.arg("build").current_dir(cargo_dir); - // Rust doesn't expose size without editing the cargo.toml. Instead just use release. - if matches!(opt_level, OptLevel::Optimize | OptLevel::Size) { - command.arg("--release"); - } - let source_file = if shared_lib_path.is_some() { - command.env("RUSTFLAGS", "-C link-dead-code"); - command.args(&["--bin", "host"]); - "src/main.rs" - } else { - command.arg("--lib"); - "src/lib.rs" - }; - let output = command.output().unwrap(); - - validate_output(source_file, "cargo build", output); - - if shared_lib_path.is_some() { - // For surgical linking, just copy the dynamically linked rust app. - std::fs::copy(cargo_out_dir.join("host"), &host_dest).unwrap(); - } else { - // Cargo hosts depend on a c wrapper for the api. Compile host.c as well. - - let output = build_c_host_native( - &env_path, - &env_home, - c_host_dest.to_str().unwrap(), - &[c_host_src.to_str().unwrap()], - opt_level, - shared_lib_path, - ); - validate_output("host.c", "clang", output); - - let output = Command::new("ld") - .env_clear() - .env("PATH", &env_path) - .args(&[ - "-r", - "-L", - cargo_out_dir.to_str().unwrap(), - c_host_dest.to_str().unwrap(), - "-lhost", - "-o", - host_dest.to_str().unwrap(), - ]) - .output() - .unwrap(); - validate_output("c_host.o", "ld", output); - - // Clean up c_host.o - let output = Command::new("rm") - .env_clear() - .args(&["-f", c_host_dest.to_str().unwrap()]) - .output() - .unwrap(); - - validate_output("rust_host.o", "rm", output); - } - } else if rust_host_src.exists() { - // Compile and link host.rs, if it exists - let mut command = Command::new("rustc"); - command.args(&[ - rust_host_src.to_str().unwrap(), - "-o", - rust_host_dest.to_str().unwrap(), - ]); - if matches!(opt_level, OptLevel::Optimize) { - command.arg("-O"); - } else if matches!(opt_level, OptLevel::Size) { - command.arg("-C opt-level=s"); - } - let output = command.output().unwrap(); - - validate_output("host.rs", "rustc", output); - - // Rust hosts depend on a c wrapper for the api. Compile host.c as well. - if shared_lib_path.is_some() { - // If compiling to executable, let c deal with linking as well. - let output = build_c_host_native( - &env_path, - &env_home, - host_dest.to_str().unwrap(), - &[ - c_host_src.to_str().unwrap(), - rust_host_dest.to_str().unwrap(), - ], - opt_level, - shared_lib_path, - ); - validate_output("host.c", "clang", output); - } else { - let output = build_c_host_native( - &env_path, - &env_home, - c_host_dest.to_str().unwrap(), - &[c_host_src.to_str().unwrap()], - opt_level, - shared_lib_path, - ); - - validate_output("host.c", "clang", output); - let output = Command::new("ld") - .env_clear() - .env("PATH", &env_path) - .args(&[ - "-r", - c_host_dest.to_str().unwrap(), - rust_host_dest.to_str().unwrap(), - "-o", - host_dest.to_str().unwrap(), - ]) - .output() - .unwrap(); - - validate_output("rust_host.o", "ld", output); - } - - // Clean up rust_host.o and c_host.o - let output = Command::new("rm") - .env_clear() - .args(&[ - "-f", - rust_host_dest.to_str().unwrap(), - c_host_dest.to_str().unwrap(), - ]) - .output() - .unwrap(); - - validate_output("rust_host.o", "rm", output); - } else if c_host_src.exists() { - // Compile host.c, if it exists - let output = build_c_host_native( - &env_path, - &env_home, - host_dest.to_str().unwrap(), - &[c_host_src.to_str().unwrap()], - opt_level, - shared_lib_path, - ); - validate_output("host.c", "clang", output); - } else if swift_host_src.exists() { - // Compile host.swift, if it exists - let output = build_swift_host_native( - &env_path, - &env_home, - host_dest.to_str().unwrap(), - &[swift_host_src.to_str().unwrap()], - opt_level, - shared_lib_path, - swift_host_header_src - .exists() - .then(|| swift_host_header_src.to_str().unwrap()), - ); - validate_output("host.swift", "swiftc", output); - } - - host_dest -} - -fn nix_path_opt() -> Option { - env::var_os("NIX_GLIBC_PATH").map(|path| path.into_string().unwrap()) -} - -fn library_path(segments: [&str; N]) -> Option { - let mut guess_path = PathBuf::new(); - for s in segments { - guess_path.push(s); - } - if guess_path.exists() { - Some(guess_path) - } else { - None - } -} - -/// Given a list of library directories and the name of a library, find the 1st match -/// -/// The provided list of library directories should be in the form of a list of -/// directories, where each directory is represented by a series of path segments, like -/// -/// ["/usr", "lib"] -/// -/// Each directory will be checked for a file with the provided filename, and the first -/// match will be returned. -/// -/// If there are no matches, [`None`] will be returned. -fn look_for_library(lib_dirs: &[&[&str]], lib_filename: &str) -> Option { - lib_dirs - .iter() - .map(|lib_dir| { - lib_dir.iter().fold(PathBuf::new(), |mut path, segment| { - path.push(segment); - path - }) - }) - .map(|mut path| { - path.push(lib_filename); - path - }) - .find(|path| path.exists()) -} - -fn link_linux( - target: &Triple, - output_path: PathBuf, - input_paths: &[&str], - link_type: LinkType, -) -> io::Result<(Child, PathBuf)> { - let architecture = format!("{}-linux-gnu", target.architecture); - - // Command::new("cp") - // .args(&[input_paths[0], "/home/folkertdev/roc/wasm/host.o"]) - // .output() - // .unwrap(); - // - // Command::new("cp") - // .args(&[input_paths[1], "/home/folkertdev/roc/wasm/app.o"]) - // .output() - // .unwrap(); - - if let Architecture::X86_32(_) = target.architecture { - return Ok(( - Command::new(&zig_executable()) - .args(&["build-exe"]) - .args(input_paths) - .args(&[ - "-target", - "i386-linux-musl", - "-lc", - &format!("-femit-bin={}", output_path.to_str().unwrap()), - ]) - .spawn()?, - output_path, - )); - } - - // Some things we'll need to build a list of dirs to check for libraries - let maybe_nix_path = nix_path_opt(); - let usr_lib_arch = ["/usr", "lib", &architecture]; - let lib_arch = ["/lib", &architecture]; - let nix_path_segments; - let lib_dirs_if_nix: [&[&str]; 5]; - let lib_dirs_if_nonix: [&[&str]; 4]; - - // Build the aformentioned list - let lib_dirs: &[&[&str]] = - // give preference to nix_path if it's defined, this prevents bugs - if let Some(nix_path) = &maybe_nix_path { - nix_path_segments = [nix_path.as_str()]; - lib_dirs_if_nix = [ - &nix_path_segments, - &usr_lib_arch, - &lib_arch, - &["/usr", "lib"], - &["/usr", "lib64"], - ]; - &lib_dirs_if_nix - } else { - lib_dirs_if_nonix = [ - &usr_lib_arch, - &lib_arch, - &["/usr", "lib"], - &["/usr", "lib64"], - ]; - &lib_dirs_if_nonix - }; - - // Look for the libraries we'll need - - let libgcc_name = "libgcc_s.so.1"; - let libgcc_path = look_for_library(lib_dirs, libgcc_name); - - let crti_name = "crti.o"; - let crti_path = look_for_library(lib_dirs, crti_name); - - let crtn_name = "crtn.o"; - let crtn_path = look_for_library(lib_dirs, crtn_name); - - let scrt1_name = "Scrt1.o"; - let scrt1_path = look_for_library(lib_dirs, scrt1_name); - - // Unwrap all the paths at once so we can inform the user of all missing libs at once - let (libgcc_path, crti_path, crtn_path, scrt1_path) = - match (libgcc_path, crti_path, crtn_path, scrt1_path) { - (Some(libgcc), Some(crti), Some(crtn), Some(scrt1)) => (libgcc, crti, crtn, scrt1), - (maybe_gcc, maybe_crti, maybe_crtn, maybe_scrt1) => { - if maybe_gcc.is_none() { - eprintln!("Couldn't find libgcc_s.so.1!"); - eprintln!("You may need to install libgcc\n"); - } - if maybe_crti.is_none() | maybe_crtn.is_none() | maybe_scrt1.is_none() { - eprintln!("Couldn't find the glibc development files!"); - eprintln!("We need the objects crti.o, crtn.o, and Scrt1.o"); - eprintln!("You may need to install the glibc development package"); - eprintln!("(probably called glibc-dev or glibc-devel)\n"); - } - - let dirs = lib_dirs - .iter() - .map(|segments| segments.join("/")) - .collect::>() - .join("\n"); - eprintln!("We looked in the following directories:\n{}", dirs); - process::exit(1); - } - }; - - let ld_linux = match target.architecture { - Architecture::X86_64 => { - // give preference to nix_path if it's defined, this prevents bugs - if let Some(nix_path) = nix_path_opt() { - library_path([&nix_path, "ld-linux-x86-64.so.2"]) - } else { - library_path(["/lib64", "ld-linux-x86-64.so.2"]) - } - } - Architecture::Aarch64(_) => library_path(["/lib", "ld-linux-aarch64.so.1"]), - _ => panic!( - "TODO gracefully handle unsupported linux architecture: {:?}", - target.architecture - ), - }; - let ld_linux = ld_linux.unwrap(); - let ld_linux = ld_linux.to_str().unwrap(); - - let mut soname; - let (base_args, output_path) = match link_type { - LinkType::Executable => ( - // Presumably this S stands for Static, since if we include Scrt1.o - // in the linking for dynamic builds, linking fails. - vec![scrt1_path.to_string_lossy().into_owned()], - output_path, - ), - LinkType::Dylib => { - // TODO: do we actually need the version number on this? - // Do we even need the "-soname" argument? - // - // See https://software.intel.com/content/www/us/en/develop/articles/create-a-unix-including-linux-shared-library.html - - soname = output_path.clone(); - soname.set_extension("so.1"); - - let mut output_path = output_path; - - output_path.set_extension("so.1.0"); - - ( - // TODO: find a way to avoid using a vec! here - should theoretically be - // able to do this somehow using &[] but the borrow checker isn't having it. - // Also find a way to have these be string slices instead of Strings. - vec![ - "-shared".to_string(), - "-soname".to_string(), - soname.as_path().to_str().unwrap().to_string(), - ], - output_path, - ) - } - LinkType::None => internal_error!("link_linux should not be called with link type of none"), - }; - - let env_path = env::var("PATH").unwrap_or_else(|_| "".to_string()); - - // NOTE: order of arguments to `ld` matters here! - // The `-l` flags should go after the `.o` arguments - - let mut command = Command::new("ld"); - - command - // Don't allow LD_ env vars to affect this - .env_clear() - .env("PATH", &env_path) - // Keep NIX_ env vars - .envs( - env::vars() - .filter(|&(ref k, _)| k.starts_with("NIX_")) - .collect::>(), - ) - .args(&[ - "--gc-sections", - "--eh-frame-hdr", - "-A", - arch_str(target), - "-pie", - &*crti_path.to_string_lossy(), - &*crtn_path.to_string_lossy(), - ]) - .args(&base_args) - .args(&["-dynamic-linker", ld_linux]) - .args(input_paths) - // ld.lld requires this argument, and does not accept --arch - // .args(&["-L/usr/lib/x86_64-linux-gnu"]) - .args(&[ - // Libraries - see https://github.com/rtfeldman/roc/pull/554#discussion_r496365925 - // for discussion and further references - "-lc", - "-lm", - "-lpthread", - "-ldl", - "-lrt", - "-lutil", - "-lc_nonshared", - libgcc_path.to_str().unwrap(), - // Output - "-o", - output_path.as_path().to_str().unwrap(), // app (or app.so or app.dylib etc.) - ]); - - let output = command.spawn()?; - - Ok((output, output_path)) -} - -fn link_macos( - target: &Triple, - output_path: PathBuf, - input_paths: &[&str], - link_type: LinkType, -) -> io::Result<(Child, PathBuf)> { - let (link_type_arg, output_path) = match link_type { - LinkType::Executable => ("-execute", output_path), - LinkType::Dylib => { - let mut output_path = output_path; - - output_path.set_extension("dylib"); - - ("-dylib", output_path) - } - LinkType::None => internal_error!("link_macos should not be called with link type of none"), - }; - - let arch = match target.architecture { - Architecture::Aarch64(_) => "arm64".to_string(), - _ => target.architecture.to_string(), - }; - - let mut ld_command = Command::new("ld"); - - ld_command - // NOTE: order of arguments to `ld` matters here! - // The `-l` flags should go after the `.o` arguments - // Don't allow LD_ env vars to affect this - .env_clear() - .args(&[ - // NOTE: we don't do --gc-sections on macOS because the default - // macOS linker doesn't support it, but it's a performance - // optimization, so if we ever switch to a different linker, - // we'd like to re-enable it on macOS! - // "--gc-sections", - link_type_arg, - "-arch", - &arch, - "-macos_version_min", - &get_macos_version(), - ]) - .args(input_paths); - - let sdk_path = "/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/lib"; - if Path::new(sdk_path).exists() { - ld_command.arg(format!("-L{}", sdk_path)); - ld_command.arg(format!("-L{}/swift", sdk_path)); - }; - - let roc_link_flags = match env::var("ROC_LINK_FLAGS") { - Ok(flags) => { - println!("⚠️ CAUTION: The ROC_LINK_FLAGS environment variable is a temporary workaround, and will no longer do anything once surgical linking lands! If you're concerned about what this means for your use case, please ask about it on Zulip."); - - flags - } - Err(_) => "".to_string(), - }; - for roc_link_flag in roc_link_flags.split_whitespace() { - ld_command.arg(roc_link_flag); - } - - ld_command.args(&[ - // Libraries - see https://github.com/rtfeldman/roc/pull/554#discussion_r496392274 - // for discussion and further references - "-lSystem", - "-lresolv", - "-lpthread", - // This `-F PATH` flag is needed for `-framework` flags to work - "-F", - "/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/System/Library/Frameworks/", - // These frameworks are needed for GUI examples to work - "-framework", - "AudioUnit", - "-framework", - "Cocoa", - "-framework", - "CoreAudio", - "-framework", - "CoreVideo", - "-framework", - "IOKit", - "-framework", - "Metal", - "-framework", - "QuartzCore", - // "-lrt", // TODO shouldn't we need this? - // "-lc_nonshared", // TODO shouldn't we need this? - // "-lgcc", // TODO will eventually need compiler_rt from gcc or something - see https://github.com/rtfeldman/roc/pull/554#discussion_r496370840 - "-framework", - "Security", - // Output - "-o", - output_path.to_str().unwrap(), // app - ]); - - let mut ld_child = ld_command.spawn()?; - - match target.architecture { - Architecture::Aarch64(_) => { - ld_child.wait()?; - let codesign_child = Command::new("codesign") - .args(&["-s", "-", output_path.to_str().unwrap()]) - .spawn()?; - - Ok((codesign_child, output_path)) - } - _ => Ok((ld_child, output_path)), - } -} - -fn get_macos_version() -> String { - let cmd_stdout = Command::new("sw_vers") - .arg("-productVersion") - .output() - .expect("Failed to execute command 'sw_vers -productVersion'") - .stdout; - - let full_version_string = String::from_utf8(cmd_stdout) - .expect("Failed to convert output of command 'sw_vers -productVersion' into a utf8 string"); - - full_version_string - .trim_end() - .split('.') - .take(2) - .collect::>() - .join(".") -} - -fn link_wasm32( - _target: &Triple, - output_path: PathBuf, - input_paths: &[&str], - _link_type: LinkType, -) -> io::Result<(Child, PathBuf)> { - let zig_str_path = find_zig_str_path(); - let wasi_libc_path = find_wasi_libc_path(); - - let child = Command::new(&zig_executable()) - // .env_clear() - // .env("PATH", &env_path) - .args(&["build-exe"]) - .args(input_paths) - .args([ - // include wasi libc - // using `-lc` is broken in zig 8 (and early 9) in combination with ReleaseSmall - wasi_libc_path.to_str().unwrap(), - &format!("-femit-bin={}", output_path.to_str().unwrap()), - "-target", - "wasm32-wasi-musl", - "--pkg-begin", - "str", - zig_str_path.to_str().unwrap(), - "--pkg-end", - "--strip", - "-O", - "ReleaseSmall", - // useful for debugging - // "-femit-llvm-ir=/home/folkertdev/roc/roc/examples/benchmarks/platform/host.ll", - ]) - .spawn()?; - - Ok((child, output_path)) -} - -fn link_windows( - _target: &Triple, - _output_path: PathBuf, - _input_paths: &[&str], - _link_type: LinkType, -) -> io::Result<(Child, PathBuf)> { - todo!("Add windows support to the surgical linker. See issue #2608.") -} - -pub fn module_to_dylib( - module: &inkwell::module::Module, - target: &Triple, - opt_level: OptLevel, -) -> Result { - use crate::target::{self, convert_opt_level}; - use inkwell::targets::{FileType, RelocMode}; - - let dir = tempfile::tempdir().unwrap(); - let filename = PathBuf::from("Test.roc"); - let file_path = dir.path().join(filename); - let mut app_o_file = file_path; - - app_o_file.set_file_name("app.o"); - - // Emit the .o file using position-independent code (PIC) - needed for dylibs - let reloc = RelocMode::PIC; - let target_machine = - target::target_machine(target, convert_opt_level(opt_level), reloc).unwrap(); - - target_machine - .write_to_file(module, FileType::Object, &app_o_file) - .expect("Writing .o file failed"); - - // Link app.o into a dylib - e.g. app.so or app.dylib - let (mut child, dylib_path) = link( - &Triple::host(), - app_o_file.clone(), - &[app_o_file.to_str().unwrap()], - LinkType::Dylib, - ) - .unwrap(); - - child.wait().unwrap(); - - // Load the dylib - let path = dylib_path.as_path().to_str().unwrap(); - - if matches!(target.architecture, Architecture::Aarch64(_)) { - // On AArch64 darwin machines, calling `ldopen` on Roc-generated libs from multiple threads - // sometimes fails with - // cannot dlopen until fork() handlers have completed - // This may be due to codesigning. In any case, spinning until we are able to dlopen seems - // to be okay. - loop { - match unsafe { Library::new(path) } { - Ok(lib) => return Ok(lib), - Err(Error::DlOpen { .. }) => continue, - Err(other) => return Err(other), - } - } - } - - unsafe { Library::new(path) } -} - -pub fn preprocess_host_wasm32(host_input_path: &Path, preprocessed_host_path: &Path) { - let host_input = host_input_path.to_str().unwrap(); - let output_file = preprocessed_host_path.to_str().unwrap(); - - /* - Notes: - zig build-obj just gives you back the first input file, doesn't combine them! - zig build-lib works but doesn't emit relocations, even with --emit-relocs (bug?) - (gen_wasm needs relocs for host-to-app calls and stack size adjustment) - zig wasm-ld is a wrapper around wasm-ld and gives us maximum flexiblity - (but seems to be an unofficial API) - */ - - let mut command = Command::new(&zig_executable()); - let args = &[ - "wasm-ld", - bitcode::BUILTINS_WASM32_OBJ_PATH, - host_input, - WASI_LIBC_PATH, - WASI_COMPILER_RT_PATH, // builtins need __multi3, __udivti3, __fixdfti - "-o", - output_file, - "--export-all", - "--no-entry", - "--import-undefined", - "--relocatable", - ]; - - command.args(args); - - // println!("\npreprocess_host_wasm32"); - // println!("zig {}\n", args.join(" ")); - - let output = command.output().unwrap(); - validate_output(output_file, "zig", output) -} - -fn validate_output(file_name: &str, cmd_name: &str, output: Output) { - if !output.status.success() { - match std::str::from_utf8(&output.stderr) { - Ok(stderr) => panic!( - "Failed to rebuild {} - stderr of the `{}` command was:\n{}", - file_name, cmd_name, stderr - ), - Err(utf8_err) => panic!( - "Failed to rebuild {} - stderr of the `{}` command was invalid utf8 ({:?})", - file_name, cmd_name, utf8_err - ), - } - } -} diff --git a/compiler/build/src/program.rs b/compiler/build/src/program.rs deleted file mode 100644 index 8f5a704f4f..0000000000 --- a/compiler/build/src/program.rs +++ /dev/null @@ -1,550 +0,0 @@ -use roc_gen_llvm::llvm::build::module_from_builtins; -pub use roc_gen_llvm::llvm::build::FunctionIterator; -use roc_load::{LoadedModule, MonomorphizedModule}; -use roc_module::symbol::{Interns, ModuleId}; -use roc_mono::ir::OptLevel; -use roc_region::all::LineInfo; -use std::path::{Path, PathBuf}; -use std::time::{Duration, SystemTime}; - -use roc_collections::all::MutMap; -#[cfg(feature = "target-wasm32")] -use roc_collections::all::MutSet; - -#[derive(Debug, Clone, Copy, Default)] -pub struct CodeGenTiming { - pub code_gen: Duration, - pub emit_o_file: Duration, -} - -pub fn report_problems_monomorphized(loaded: &mut MonomorphizedModule) -> Problems { - report_problems_help( - loaded.total_problems(), - &loaded.sources, - &loaded.interns, - &mut loaded.can_problems, - &mut loaded.type_problems, - ) -} - -pub fn report_problems_typechecked(loaded: &mut LoadedModule) -> Problems { - report_problems_help( - loaded.total_problems(), - &loaded.sources, - &loaded.interns, - &mut loaded.can_problems, - &mut loaded.type_problems, - ) -} - -#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] -pub struct Problems { - pub errors: usize, - pub warnings: usize, -} - -impl Problems { - pub fn exit_code(&self) -> i32 { - // 0 means no problems, 1 means errors, 2 means warnings - if self.errors > 0 { - 1 - } else { - self.warnings.min(1) as i32 - } - } -} - -fn report_problems_help( - total_problems: usize, - sources: &MutMap)>, - interns: &Interns, - can_problems: &mut MutMap>, - type_problems: &mut MutMap>, -) -> Problems { - use roc_reporting::report::{ - can_problem, type_problem, Report, RocDocAllocator, Severity::*, DEFAULT_PALETTE, - }; - let palette = DEFAULT_PALETTE; - - // This will often over-allocate total memory, but it means we definitely - // never need to re-allocate either the warnings or the errors vec! - let mut warnings = Vec::with_capacity(total_problems); - let mut errors = Vec::with_capacity(total_problems); - - for (home, (module_path, src)) in sources.iter() { - let mut src_lines: Vec<&str> = Vec::new(); - - src_lines.extend(src.split('\n')); - - let lines = LineInfo::new(&src_lines.join("\n")); - - // Report parsing and canonicalization problems - let alloc = RocDocAllocator::new(&src_lines, *home, interns); - - let problems = can_problems.remove(home).unwrap_or_default(); - - for problem in problems.into_iter() { - let report = can_problem(&alloc, &lines, module_path.clone(), problem); - let severity = report.severity; - let mut buf = String::new(); - - report.render_color_terminal(&mut buf, &alloc, &palette); - - match severity { - Warning => { - warnings.push(buf); - } - RuntimeError => { - errors.push(buf); - } - } - } - - let problems = type_problems.remove(home).unwrap_or_default(); - - for problem in problems { - if let Some(report) = type_problem(&alloc, &lines, module_path.clone(), problem) { - let severity = report.severity; - let mut buf = String::new(); - - report.render_color_terminal(&mut buf, &alloc, &palette); - - match severity { - Warning => { - warnings.push(buf); - } - RuntimeError => { - errors.push(buf); - } - } - } - } - } - - let problems_reported; - - // Only print warnings if there are no errors - if errors.is_empty() { - problems_reported = warnings.len(); - - for warning in warnings.iter() { - println!("\n{}\n", warning); - } - } else { - problems_reported = errors.len(); - - for error in errors.iter() { - println!("\n{}\n", error); - } - } - - // If we printed any problems, print a horizontal rule at the end, - // and then clear any ANSI escape codes (e.g. colors) we've used. - // - // The horizontal rule is nice when running the program right after - // compiling it, as it lets you clearly see where the compiler - // errors/warnings end and the program output begins. - if problems_reported > 0 { - println!("{}\u{001B}[0m\n", Report::horizontal_rule(&palette)); - } - - Problems { - errors: errors.len(), - warnings: warnings.len(), - } -} - -#[allow(clippy::too_many_arguments)] -pub fn gen_from_mono_module( - arena: &bumpalo::Bump, - loaded: MonomorphizedModule, - roc_file_path: &Path, - target: &target_lexicon::Triple, - app_o_file: &Path, - opt_level: OptLevel, - emit_debug_info: bool, - preprocessed_host_path: &Path, -) -> CodeGenTiming { - match opt_level { - OptLevel::Normal | OptLevel::Size | OptLevel::Optimize => gen_from_mono_module_llvm( - arena, - loaded, - roc_file_path, - target, - app_o_file, - opt_level, - emit_debug_info, - ), - OptLevel::Development => { - gen_from_mono_module_dev(arena, loaded, target, app_o_file, preprocessed_host_path) - } - } -} - -// TODO how should imported modules factor into this? What if those use builtins too? -// TODO this should probably use more helper functions -// TODO make this polymorphic in the llvm functions so it can be reused for another backend. -pub fn gen_from_mono_module_llvm( - arena: &bumpalo::Bump, - loaded: MonomorphizedModule, - roc_file_path: &Path, - target: &target_lexicon::Triple, - app_o_file: &Path, - opt_level: OptLevel, - emit_debug_info: bool, -) -> CodeGenTiming { - use crate::target::{self, convert_opt_level}; - use inkwell::attributes::{Attribute, AttributeLoc}; - use inkwell::context::Context; - use inkwell::module::Linkage; - use inkwell::targets::{FileType, RelocMode}; - - let code_gen_start = SystemTime::now(); - - // Generate the binary - let target_info = roc_target::TargetInfo::from(target); - let context = Context::create(); - let module = arena.alloc(module_from_builtins(target, &context, "app")); - - // strip Zig debug stuff - // module.strip_debug_info(); - - // mark our zig-defined builtins as internal - let app_ll_file = { - let mut temp = PathBuf::from(roc_file_path); - temp.set_extension("ll"); - - temp - }; - - let kind_id = Attribute::get_named_enum_kind_id("alwaysinline"); - debug_assert!(kind_id > 0); - let enum_attr = context.create_enum_attribute(kind_id, 1); - - for function in FunctionIterator::from_module(module) { - let name = function.get_name().to_str().unwrap(); - - // mark our zig-defined builtins as internal - if name.starts_with("roc_builtins") { - function.set_linkage(Linkage::Internal); - } - - if name.starts_with("roc_builtins.dict") - || name.starts_with("roc_builtins.list") - || name.starts_with("roc_builtins.dec") - || name.starts_with("list.RocList") - || name.starts_with("dict.RocDict") - || name.contains("incref") - || name.contains("decref") - { - function.add_attribute(AttributeLoc::Function, enum_attr); - } - } - - let builder = context.create_builder(); - let (dibuilder, compile_unit) = roc_gen_llvm::llvm::build::Env::new_debug_info(module); - let (mpm, _fpm) = roc_gen_llvm::llvm::build::construct_optimization_passes(module, opt_level); - - // Compile and add all the Procs before adding main - let env = roc_gen_llvm::llvm::build::Env { - arena, - builder: &builder, - dibuilder: &dibuilder, - compile_unit: &compile_unit, - context: &context, - interns: loaded.interns, - module, - target_info, - // in gen_tests, the compiler provides roc_panic - // and sets up the setjump/longjump exception handling - is_gen_test: false, - exposed_to_host: loaded.exposed_to_host.values.keys().copied().collect(), - }; - - roc_gen_llvm::llvm::build::build_procedures( - &env, - opt_level, - loaded.procedures, - loaded.entry_point, - Some(&app_ll_file), - ); - - env.dibuilder.finalize(); - - // we don't use the debug info, and it causes weird errors. - module.strip_debug_info(); - - // Uncomment this to see the module's optimized LLVM instruction output: - // env.module.print_to_stderr(); - - mpm.run_on(module); - - // Verify the module - if let Err(errors) = env.module.verify() { - // write the ll code to a file, so we can modify it - env.module.print_to_file(&app_ll_file).unwrap(); - - panic!( - "😱 LLVM errors when defining module; I wrote the full LLVM IR to {:?}\n\n {}", - app_ll_file, - errors.to_string(), - ); - } - - // Uncomment this to see the module's optimized LLVM instruction output: - // env.module.print_to_stderr(); - - let code_gen = code_gen_start.elapsed().unwrap(); - let emit_o_file_start = SystemTime::now(); - - // annotate the LLVM IR output with debug info - // so errors are reported with the line number of the LLVM source - if emit_debug_info { - module.strip_debug_info(); - - let mut app_ll_dbg_file = PathBuf::from(roc_file_path); - app_ll_dbg_file.set_extension("dbg.ll"); - - let mut app_bc_file = PathBuf::from(roc_file_path); - app_bc_file.set_extension("bc"); - - use std::process::Command; - - // write the ll code to a file, so we can modify it - module.print_to_file(&app_ll_file).unwrap(); - - // run the debugir https://github.com/vaivaswatha/debugir tool - match Command::new("debugir") - .args(&["-instnamer", app_ll_file.to_str().unwrap()]) - .output() - { - Ok(_) => {} - Err(error) => { - use std::io::ErrorKind; - match error.kind() { - ErrorKind::NotFound => panic!( - r"I could not find the `debugir` tool on the PATH, install it from https://github.com/vaivaswatha/debugir" - ), - _ => panic!("{:?}", error), - } - } - } - - use target_lexicon::Architecture; - match target.architecture { - Architecture::X86_64 - | Architecture::X86_32(_) - | Architecture::Aarch64(_) - | Architecture::Wasm32 => { - let ll_to_bc = Command::new("llvm-as") - .args(&[ - app_ll_dbg_file.to_str().unwrap(), - "-o", - app_bc_file.to_str().unwrap(), - ]) - .output() - .unwrap(); - - assert!(ll_to_bc.stderr.is_empty(), "{:#?}", ll_to_bc); - - let llc_args = &[ - "-relocation-model=pic", - "-filetype=obj", - app_bc_file.to_str().unwrap(), - "-o", - app_o_file.to_str().unwrap(), - ]; - - // write the .o file. Note that this builds the .o for the local machine, - // and ignores the `target_machine` entirely. - // - // different systems name this executable differently, so we shotgun for - // the most common ones and then give up. - let bc_to_object = Command::new("llc").args(llc_args).output().unwrap(); - - assert!(bc_to_object.stderr.is_empty(), "{:#?}", bc_to_object); - } - _ => unreachable!(), - } - } else { - // Emit the .o file - use target_lexicon::Architecture; - match target.architecture { - Architecture::X86_64 | Architecture::X86_32(_) | Architecture::Aarch64(_) => { - let reloc = RelocMode::PIC; - let target_machine = - target::target_machine(target, convert_opt_level(opt_level), reloc).unwrap(); - - target_machine - .write_to_file(env.module, FileType::Object, app_o_file) - .expect("Writing .o file failed"); - } - Architecture::Wasm32 => { - // Useful for debugging - // module.print_to_file(app_ll_file); - module.write_bitcode_to_path(app_o_file); - } - _ => panic!( - "TODO gracefully handle unsupported architecture: {:?}", - target.architecture - ), - } - } - - let emit_o_file = emit_o_file_start.elapsed().unwrap(); - - CodeGenTiming { - code_gen, - emit_o_file, - } -} -#[cfg(feature = "target-wasm32")] -pub fn gen_from_mono_module_dev( - arena: &bumpalo::Bump, - loaded: MonomorphizedModule, - target: &target_lexicon::Triple, - app_o_file: &Path, - preprocessed_host_path: &Path, -) -> CodeGenTiming { - use target_lexicon::Architecture; - - match target.architecture { - Architecture::Wasm32 => { - gen_from_mono_module_dev_wasm32(arena, loaded, app_o_file, preprocessed_host_path) - } - Architecture::X86_64 | Architecture::Aarch64(_) => { - gen_from_mono_module_dev_assembly(arena, loaded, target, app_o_file) - } - _ => todo!(), - } -} - -#[cfg(not(feature = "target-wasm32"))] -pub fn gen_from_mono_module_dev( - arena: &bumpalo::Bump, - loaded: MonomorphizedModule, - target: &target_lexicon::Triple, - app_o_file: &Path, - _host_input_path: &Path, -) -> CodeGenTiming { - use target_lexicon::Architecture; - - match target.architecture { - Architecture::X86_64 | Architecture::Aarch64(_) => { - gen_from_mono_module_dev_assembly(arena, loaded, target, app_o_file) - } - _ => todo!(), - } -} - -#[cfg(feature = "target-wasm32")] -fn gen_from_mono_module_dev_wasm32( - arena: &bumpalo::Bump, - loaded: MonomorphizedModule, - app_o_file: &Path, - preprocessed_host_path: &Path, -) -> CodeGenTiming { - let code_gen_start = SystemTime::now(); - let MonomorphizedModule { - module_id, - procedures, - mut interns, - .. - } = loaded; - - let exposed_to_host = loaded - .exposed_to_host - .values - .keys() - .copied() - .collect::>(); - - let env = roc_gen_wasm::Env { - arena, - module_id, - exposed_to_host, - }; - - let host_bytes = std::fs::read(preprocessed_host_path).unwrap_or_else(|_| { - panic!( - "Failed to read host object file {}! Try setting --precompiled-host=false", - preprocessed_host_path.display() - ) - }); - - let host_module = roc_gen_wasm::parse_host(arena, &host_bytes).unwrap_or_else(|e| { - panic!( - "I ran into a problem with the host object file, {} at offset 0x{:x}:\n{}", - preprocessed_host_path.display(), - e.offset, - e.message - ) - }); - - let final_binary_bytes = - roc_gen_wasm::build_app_binary(&env, &mut interns, host_module, procedures); - - let code_gen = code_gen_start.elapsed().unwrap(); - let emit_o_file_start = SystemTime::now(); - - // The app_o_file is actually the final binary - std::fs::write(&app_o_file, &final_binary_bytes).unwrap_or_else(|e| { - panic!( - "I wasn't able to write to the output file {}\n{}", - app_o_file.display(), - e - ) - }); - - let emit_o_file = emit_o_file_start.elapsed().unwrap(); - - CodeGenTiming { - code_gen, - emit_o_file, - } -} - -fn gen_from_mono_module_dev_assembly( - arena: &bumpalo::Bump, - loaded: MonomorphizedModule, - target: &target_lexicon::Triple, - app_o_file: &Path, -) -> CodeGenTiming { - let code_gen_start = SystemTime::now(); - - let lazy_literals = true; - let generate_allocators = false; // provided by the platform - - let MonomorphizedModule { - module_id, - procedures, - mut interns, - exposed_to_host, - .. - } = loaded; - - let env = roc_gen_dev::Env { - arena, - module_id, - exposed_to_host: exposed_to_host.values.keys().copied().collect(), - lazy_literals, - generate_allocators, - }; - - let module_object = roc_gen_dev::build_module(&env, &mut interns, target, procedures); - - let code_gen = code_gen_start.elapsed().unwrap(); - let emit_o_file_start = SystemTime::now(); - - let module_out = module_object - .write() - .expect("failed to build output object"); - std::fs::write(&app_o_file, module_out).expect("failed to write object to file"); - - let emit_o_file = emit_o_file_start.elapsed().unwrap(); - - CodeGenTiming { - code_gen, - emit_o_file, - } -} diff --git a/compiler/builtins/Cargo.toml b/compiler/builtins/Cargo.toml deleted file mode 100644 index 074cfc326e..0000000000 --- a/compiler/builtins/Cargo.toml +++ /dev/null @@ -1,21 +0,0 @@ -[package] -name = "roc_builtins" -version = "0.1.0" -authors = ["The Roc Contributors"] -license = "UPL-1.0" -edition = "2021" - -[dependencies] -roc_collections = { path = "../collections" } -roc_region = { path = "../region" } -roc_module = { path = "../module" } -roc_types = { path = "../types" } -roc_target = { path = "../roc_target" } -lazy_static = "1.4.0" - -[build-dependencies] -# dunce can be removed once ziglang/zig#5109 is fixed -dunce = "1.0.2" - -[target.'cfg(target_os = "macos")'.build-dependencies] -tempfile = "3.2.0" diff --git a/compiler/builtins/README.md b/compiler/builtins/README.md deleted file mode 100644 index d8193c725b..0000000000 --- a/compiler/builtins/README.md +++ /dev/null @@ -1,98 +0,0 @@ -# So you want to add a builtin? - -Builtins are the functions and modules that are implicitly imported into every module. All of them compile down to llvm, but some are implemented directly as llvm and others in terms of intermediate functions. Either way, making a new builtin means touching many files. Lets make it easy for you and just list out which modules you need to visit to make a builtin. Here is what it takes: - -### module/src/symbol.rs - -Towards the bottom of `symbol.rs` there is a `define_builtins!` macro being used that takes many modules and function names. The first level (`List`, `Int` ..) is the module name, and the second level is the function or value name (`reverse`, `rem` ..). If you wanted to add a `Int` function called `addTwo` go to `2 Int: "Int" => {` and inside that case add to the bottom `38 INT_ADD_TWO: "addTwo"` (assuming there are 37 existing ones). - -Some of these have `#` inside their name (`first#list`, `#lt` ..). This is a trick we are doing to hide implementation details from Roc programmers. To a Roc programmer, a name with `#` in it is invalid, because `#` means everything after it is parsed to a comment. We are constructing these functions manually, so we are circumventing the parsing step and dont have such restrictions. We get to make functions and values with `#` which as a consequence are not accessible to Roc programmers. Roc programmers simply cannot reference them. - -But we can use these values and some of these are necessary for implementing builtins. For example, `List.get` returns tags, and it is not easy for us to create tags when composing LLVM. What is easier however, is: -- ..writing `List.#getUnsafe` that has the dangerous signature of `List elem, Nat -> elem` in LLVM -- ..writing `List elem, Nat -> Result elem [OutOfBounds]*` in a type safe way that uses `getUnsafe` internally, only after it checks if the `elem` at `Nat` index exists. - - -### can/src/builtins.rs - -Right at the top of this module is a function called `builtin_defs`. All this is doing is mapping the `Symbol` defined in `module/src/symbol.rs` to its implementation. Some of the builtins are quite complex, such as `list_get`. What makes `list_get` is that it returns tags, and in order to return tags it first has to defer to lower-level functions via an if statement. - -Lets look at `List.repeat : elem, Nat -> List elem`, which is more straight-forward, and points directly to its lower level implementation: -``` -fn list_repeat(symbol: Symbol, var_store: &mut VarStore) -> Def { - let elem_var = var_store.fresh(); - let len_var = var_store.fresh(); - let list_var = var_store.fresh(); - - let body = RunLowLevel { - op: LowLevel::ListRepeat, - args: vec![ - (elem_var, Var(Symbol::ARG_1)), - (len_var, Var(Symbol::ARG_2)), - ], - ret_var: list_var, - }; - - defn( - symbol, - vec![(elem_var, Symbol::ARG_1), (len_var, Symbol::ARG_2)], - var_store, - body, - list_var, - ) -} -``` -In these builtin definitions you will need to allocate for and list the arguments. For `List.repeat`, the arguments are the `elem_var` and the `len_var`. So in both the `body` and `defn` we list these arguments in a vector, with the `Symbol::ARG_1` and` Symvol::ARG_2` designating which argument is which. - -Since `List.repeat` is implemented entirely as low level functions, its `body` is a `RunLowLevel`, and the `op` is `LowLevel::ListRepeat`. Lets talk about `LowLevel` in the next section. - -## Connecting the definition to the implementation -### module/src/low_level.rs -This `LowLevel` thing connects the builtin defined in this module to its implementation. It's referenced in `can/src/builtins.rs` and it is used in `gen/src/llvm/build.rs`. - -## Bottom level LLVM values and functions -### gen/src/llvm/build.rs -This is where bottom-level functions that need to be written as LLVM are created. If the function leads to a tag thats a good sign it should not be written here in `build.rs`. If it's simple fundamental stuff like `INT_ADD` then it certainly should be written here. - -## Letting the compiler know these functions exist -### builtins/src/std.rs -It's one thing to actually write these functions, it's _another_ thing to let the Roc compiler know they exist as part of the standard library. You have to tell the compiler "Hey, this function exists, and it has this type signature". That happens in `std.rs`. - -## Specifying how we pass args to the function -### builtins/mono/src/borrow.rs -After we have all of this, we need to specify if the arguments we're passing are owned, borrowed or irrelevant. Towards the bottom of this file, add a new case for your builtin and specify each arg. Be sure to read the comment, as it explains this in more detail. - -## Testing it -### solve/tests/solve_expr.rs -To make sure that Roc is properly inferring the type of the new builtin, add a test to this file similar to: -``` - #[test] -fn atan() { - infer_eq_without_problem( - indoc!( - r#" - Num.atan - "# - ), - "Float -> Float", - ); -} -``` -But replace `Num.atan` and the type signature with the new builtin. - -### test_gen/test/*.rs -In this directory, there are a couple files like `gen_num.rs`, `gen_str.rs`, etc. For the `Str` module builtins, put the test in `gen_str.rs`, etc. Find the one for the new builtin, and add a test like: -``` -#[test] -fn atan() { - assert_evals_to!("Num.atan 10", 1.4711276743037347, f64); -} -``` -But replace `Num.atan`, the return value, and the return type with your new builtin. - -# Mistakes that are easy to make!! - -When implementing a new builtin, it is often easy to copy and paste the implementation for an existing builtin. This can take you quite far since many builtins are very similar, but it also risks forgetting to change one small part of what you copy and pasted and losing a lot of time later on when you cant figure out why things dont work. So, speaking from experience, even if you are copying an existing builtin, try and implement it manually without copying and pasting. Two recent instances of this (as of September 7th, 2020): - -- `List.keepIf` did not work for a long time because in builtins its `LowLevel` was `ListMap`. This was because I copy and pasted the `List.map` implementation in `builtins.rs -- `List.walkBackwards` had mysterious memory bugs for a little while because in `unique.rs` its return type was `list_type(flex(b))` instead of `flex(b)` since it was copy and pasted from `List.keepIf`. diff --git a/compiler/builtins/bitcode/.gitignore b/compiler/builtins/bitcode/.gitignore deleted file mode 100644 index 18b2c682d4..0000000000 --- a/compiler/builtins/bitcode/.gitignore +++ /dev/null @@ -1,7 +0,0 @@ -zig-cache -src/zig-cache -benchmark/zig-cache -builtins.ll -builtins.bc -builtins.o -dec diff --git a/compiler/builtins/bitcode/README.md b/compiler/builtins/bitcode/README.md deleted file mode 100644 index 1585c2e8fb..0000000000 --- a/compiler/builtins/bitcode/README.md +++ /dev/null @@ -1,35 +0,0 @@ -# Bitcode for Builtins - -## Adding a bitcode builtin - -To add a builtin: -1. Add the function to the relevant module. For `Num` builtin use it in `src/num.zig`, for `Str` builtins use `src/str.zig`, and so on. **For anything you add, you must add tests for it!** Not only does to make the builtins more maintainable, it's the the easiest way to test these functions on Zig. To run the test, run: `zig build test` -2. Make sure the function is public with the `pub` keyword and uses the C calling convention. This is really easy, just add `pub` and `callconv(.C)` to the function declaration like so: `pub fn atan(num: f64) callconv(.C) f64 { ... }` -3. In `src/main.zig`, export the function. This is also organized by module. For example, for a `Num` function find the `Num` section and add: `comptime { exportNumFn(num.atan, "atan"); }`. The first argument is the function, the second is the name of it in LLVM. -4. In `compiler/builtins/src/bitcode.rs`, add a constant for the new function. This is how we use it in Rust. Once again, this is organized by module, so just find the relevant area and add your new function. -5. You can now use your function in Rust using `call_bitcode_fn` in `llvm/src/build.rs`! - -## How it works - -Roc's builtins are implemented in the compiler using LLVM only. -When their implementations are simple enough (e.g. addition), they -can be implemented directly in Inkwell. - -When their implementations are complex enough, it's nicer to -implement them in a higher-level language like Zig, then compile -the result to LLVM bitcode, and import that bitcode into the compiler. - -Compiling the bitcode happens automatically in a Rust build script at `compiler/builtins/build.rs`. -Then `builtins/src/bitcode/rs` staticlly imports the compiled bitcode for use in the compiler. - -You can find the compiled bitcode in `target/debug/build/roc_builtins-[some random characters]/out/builtins.bc`. -There will be two directories like `roc_builtins-[some random characters]`, look for the one that has an -`out` directory as a child. - -> The bitcode is a bunch of bytes that aren't particularly human-readable. -> If you want to take a look at the human-readable LLVM IR, look at -> `compiler/builtins/bitcode/builtins.ll` - -## Calling bitcode functions - -use the `call_bitcode_fn` function defined in `llvm/src/build.rs` to call bitcode functions. diff --git a/compiler/builtins/bitcode/benchmark.sh b/compiler/builtins/bitcode/benchmark.sh deleted file mode 100755 index ef7b3d6b35..0000000000 --- a/compiler/builtins/bitcode/benchmark.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/bash - -set -euxo pipefail - -zig build-exe benchmark/dec.zig -O ReleaseFast --main-pkg-path . -./dec diff --git a/compiler/builtins/bitcode/benchmark/dec.zig b/compiler/builtins/bitcode/benchmark/dec.zig deleted file mode 100644 index 96d35a7af1..0000000000 --- a/compiler/builtins/bitcode/benchmark/dec.zig +++ /dev/null @@ -1,174 +0,0 @@ -const std = @import("std"); -const time = std.time; -const Timer = time.Timer; - -const RocStr = @import("../src/str.zig").RocStr; -const RocDec = @import("../src/dec.zig").RocDec; - -var timer: Timer = undefined; - -pub fn main() !void { - const stdout = std.io.getStdOut().writer(); - timer = try Timer.start(); - - try stdout.print("7 additions took ", .{}); - try avg_runs(add7); - - try stdout.print("7 subtractions took ", .{}); - try avg_runs(sub7); - - try stdout.print("7 multiplications took ", .{}); - try avg_runs(mul7); - - try stdout.print("7 divisions took ", .{}); - try avg_runs(div7); -} - -fn avg_runs(func: fn() u64) !void { - const stdout = std.io.getStdOut().writer(); - var first_run = func(); - var lowest = first_run; - var highest = first_run; - var sum = first_run; - - // 31 runs - var runs = [_]u64{ first_run, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; - var next_run: usize = 1; // we already did first_run - - while (next_run < runs.len) { - const run = func(); - - lowest = std.math.min(lowest, run); - highest = std.math.max(highest, run); - - runs[next_run] = run; - - next_run += 1; - } - - std.sort.sort(u64, &runs, {}, comptime std.sort.asc(u64)); - - const median = runs[runs.len / 2]; - - try stdout.print("{}ns (lowest: {}ns, highest: {}ns)\n", .{median, lowest, highest}); -} - -fn add7() u64 { - var str1 = RocStr.init("1.2", 3); - const dec1 = RocDec.fromStr(str1).?; - - var str2 = RocStr.init("3.4", 3); - const dec2 = RocDec.fromStr(str2).?; - - timer.reset(); - - var a = dec1.add(dec2); - a = a.add(dec1); - a = a.add(dec2); - a = a.add(dec1); - a = a.add(dec2); - a = a.add(dec1); - a = a.add(dec2); - a = a.add(dec1); - a = a.add(dec2); - a = a.add(dec1); - a = a.add(dec2); - a = a.add(dec1); - a = a.add(dec2); - a = a.add(dec1); - a = a.add(dec2); - a = a.add(dec1); - a = a.add(dec2); - - return timer.read(); -} - -fn sub7() u64 { - var str1 = RocStr.init("1.2", 3); - const dec1 = RocDec.fromStr(str1).?; - - var str2 = RocStr.init("3.4", 3); - const dec2 = RocDec.fromStr(str2).?; - - timer.reset(); - - var a = dec1.sub(dec2); - a = a.sub(dec1); - a = a.sub(dec2); - a = a.sub(dec1); - a = a.sub(dec2); - a = a.sub(dec1); - a = a.sub(dec2); - a = a.sub(dec1); - a = a.sub(dec2); - a = a.sub(dec1); - a = a.sub(dec2); - a = a.sub(dec1); - a = a.sub(dec2); - a = a.sub(dec1); - a = a.sub(dec2); - a = a.sub(dec1); - a = a.sub(dec2); - - return timer.read(); -} - -fn mul7() u64 { - var str1 = RocStr.init("1.2", 3); - const dec1 = RocDec.fromStr(str1).?; - - var str2 = RocStr.init("3.4", 3); - const dec2 = RocDec.fromStr(str2).?; - - timer.reset(); - - var a = dec1.mul(dec2); - a = a.mul(dec1); - a = a.mul(dec2); - a = a.mul(dec1); - a = a.mul(dec2); - a = a.mul(dec1); - a = a.mul(dec2); - a = a.mul(dec1); - a = a.mul(dec2); - a = a.mul(dec1); - a = a.mul(dec2); - a = a.mul(dec1); - a = a.mul(dec2); - a = a.mul(dec1); - a = a.mul(dec2); - a = a.mul(dec1); - a = a.mul(dec2); - - return timer.read(); -} - -fn div7() u64 { - var str1 = RocStr.init("1.2", 3); - const dec1 = RocDec.fromStr(str1).?; - - var str2 = RocStr.init("3.4", 3); - const dec2 = RocDec.fromStr(str2).?; - - timer.reset(); - - var a = dec1.div(dec2); - a = a.div(dec1); - a = a.div(dec2); - a = a.div(dec1); - a = a.div(dec2); - a = a.div(dec1); - a = a.div(dec2); - a = a.div(dec1); - a = a.div(dec2); - a = a.div(dec1); - a = a.div(dec2); - a = a.div(dec1); - a = a.div(dec2); - a = a.div(dec1); - a = a.div(dec2); - a = a.div(dec1); - a = a.div(dec2); - - return timer.read(); -} diff --git a/compiler/builtins/bitcode/build.zig b/compiler/builtins/bitcode/build.zig deleted file mode 100644 index b2860ecf57..0000000000 --- a/compiler/builtins/bitcode/build.zig +++ /dev/null @@ -1,131 +0,0 @@ -const std = @import("std"); -const mem = std.mem; -const Builder = std.build.Builder; -const CrossTarget = std.zig.CrossTarget; -const Arch = std.Target.Cpu.Arch; - -pub fn build(b: *Builder) void { - // b.setPreferredReleaseMode(.Debug); - b.setPreferredReleaseMode(.ReleaseFast); - const mode = b.standardReleaseOptions(); - - // Options - const fallback_main_path = "./src/main.zig"; - const main_path_desc = b.fmt("Override path to main.zig. Used by \"ir\" and \"test\". Defaults to \"{s}\". ", .{fallback_main_path}); - const main_path = b.option([]const u8, "main-path", main_path_desc) orelse fallback_main_path; - - // Tests - var main_tests = b.addTest(main_path); - main_tests.setBuildMode(mode); - main_tests.linkSystemLibrary("c"); - const test_step = b.step("test", "Run tests"); - test_step.dependOn(&main_tests.step); - - // Targets - const host_target = b.standardTargetOptions(.{ - .default_target = CrossTarget{ - .cpu_model = .baseline, - // TODO allow for native target for maximum speed - }, - }); - const linux32_target = makeLinux32Target(); - const linux64_target = makeLinux64Target(); - const wasm32_target = makeWasm32Target(); - - // LLVM IR - generateLlvmIrFile(b, mode, host_target, main_path, "ir", "builtins-host"); - generateLlvmIrFile(b, mode, linux32_target, main_path, "ir-i386", "builtins-i386"); - generateLlvmIrFile(b, mode, linux64_target, main_path, "ir-x86_64", "builtins-x86_64"); - generateLlvmIrFile(b, mode, wasm32_target, main_path, "ir-wasm32", "builtins-wasm32"); - - // Generate Object Files - generateObjectFile(b, mode, host_target, main_path, "object", "builtins-host"); - generateObjectFile(b, mode, wasm32_target, main_path, "wasm32-object", "builtins-wasm32"); - - removeInstallSteps(b); -} - -// TODO zig 0.9 can generate .bc directly, switch to that when it is released! -fn generateLlvmIrFile( - b: *Builder, - mode: std.builtin.Mode, - target: CrossTarget, - main_path: []const u8, - step_name: []const u8, - object_name: []const u8, -) void { - const obj = b.addObject(object_name, main_path); - obj.setBuildMode(mode); - obj.strip = true; - obj.emit_llvm_ir = .emit; - obj.emit_llvm_bc = .emit; - obj.emit_bin = .no_emit; - obj.target = target; - - const ir = b.step(step_name, "Build LLVM ir"); - ir.dependOn(&obj.step); -} - -// Generate Object File -// TODO: figure out how to get this to emit symbols that are only scoped to linkage (global but hidden). -// @bhansconnect: I believe anything with global scope will still be preserved by the linker even if it -// is never called. I think it could theoretically be called by a dynamic lib that links to the executable -// or something similar. -fn generateObjectFile( - b: *Builder, - mode: std.builtin.Mode, - target: CrossTarget, - main_path: []const u8, - step_name: []const u8, - object_name: []const u8, -) void { - const obj = b.addObject(object_name, main_path); - obj.setBuildMode(mode); - obj.linkSystemLibrary("c"); - obj.setOutputDir("."); - obj.strip = true; - obj.target = target; - obj.link_function_sections = true; - const obj_step = b.step(step_name, "Build object file for linking"); - obj_step.dependOn(&obj.step); -} - -fn makeLinux32Target() CrossTarget { - var target = CrossTarget.parse(.{}) catch unreachable; - - target.cpu_arch = std.Target.Cpu.Arch.i386; - target.os_tag = std.Target.Os.Tag.linux; - target.abi = std.Target.Abi.musl; - - return target; -} - -fn makeLinux64Target() CrossTarget { - var target = CrossTarget.parse(.{}) catch unreachable; - - target.cpu_arch = std.Target.Cpu.Arch.x86_64; - target.os_tag = std.Target.Os.Tag.linux; - target.abi = std.Target.Abi.musl; - - return target; -} - -fn makeWasm32Target() CrossTarget { - var target = CrossTarget.parse(.{}) catch unreachable; - - // 32-bit wasm - target.cpu_arch = std.Target.Cpu.Arch.wasm32; - target.os_tag = std.Target.Os.Tag.freestanding; - target.abi = std.Target.Abi.none; - - return target; -} - -fn removeInstallSteps(b: *Builder) void { - for (b.top_level_steps.items) |top_level_step, i| { - const name = top_level_step.step.name; - if (mem.eql(u8, name, "install") or mem.eql(u8, name, "uninstall")) { - _ = b.top_level_steps.swapRemove(i); - } - } -} diff --git a/compiler/builtins/bitcode/run-tests.sh b/compiler/builtins/bitcode/run-tests.sh deleted file mode 100755 index c34b9cba31..0000000000 --- a/compiler/builtins/bitcode/run-tests.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/bash - -set -euxo pipefail - -# Test every zig -zig build test - -# fmt every zig -find src/*.zig -type f -print0 | xargs -n 1 -0 zig fmt --check || (echo "zig fmt --check FAILED! Check the previous lines to see which files were improperly formatted." && exit 1) diff --git a/compiler/builtins/bitcode/run-wasm-tests.sh b/compiler/builtins/bitcode/run-wasm-tests.sh deleted file mode 100755 index 74b1712536..0000000000 --- a/compiler/builtins/bitcode/run-wasm-tests.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/bash - -set -euxo pipefail - -# Test failures will always point at the _start function -# Make sure to look at the rest of the stack trace! - -# Zig will try to run the test binary it produced, but it is a wasm object and hence your OS won't -# know how to run it. In the error message, it prints the binary it tried to run. We use some fun -# unix tools to get that path, then feed it to wasmer -zig test -target wasm32-wasi-musl -O ReleaseFast src/main.zig --test-cmd wasmer --test-cmd-bin diff --git a/compiler/builtins/bitcode/src/dec.zig b/compiler/builtins/bitcode/src/dec.zig deleted file mode 100644 index d19cfaca3b..0000000000 --- a/compiler/builtins/bitcode/src/dec.zig +++ /dev/null @@ -1,1095 +0,0 @@ -const std = @import("std"); -const str = @import("str.zig"); -const num_ = @import("num.zig"); -const utils = @import("utils.zig"); - -const math = std.math; -const always_inline = std.builtin.CallOptions.Modifier.always_inline; -const RocStr = str.RocStr; -const WithOverflow = utils.WithOverflow; - -pub const RocDec = extern struct { - num: i128, - - pub const decimal_places: u5 = 18; - pub const whole_number_places: u5 = 21; - const max_digits: u6 = 39; - const max_str_length: u6 = max_digits + 2; // + 2 here to account for the sign & decimal dot - - pub const min: RocDec = .{ .num = math.minInt(i128) }; - pub const max: RocDec = .{ .num = math.maxInt(i128) }; - - pub const one_point_zero_i128: i128 = math.pow(i128, 10, RocDec.decimal_places); - pub const one_point_zero: RocDec = .{ .num = one_point_zero_i128 }; - - pub fn fromU64(num: u64) RocDec { - return .{ .num = num * one_point_zero_i128 }; - } - - pub fn fromF64(num: f64) ?RocDec { - var result: f64 = num * comptime @intToFloat(f64, one_point_zero_i128); - - if (result > comptime @intToFloat(f64, math.maxInt(i128))) { - return null; - } - - if (result < comptime @intToFloat(f64, math.minInt(i128))) { - return null; - } - - var ret: RocDec = .{ .num = @floatToInt(i128, result) }; - return ret; - } - - pub fn fromStr(roc_str: RocStr) ?RocDec { - if (roc_str.isEmpty()) { - return null; - } - - const length = roc_str.len(); - - const roc_str_slice = roc_str.asSlice(); - - var is_negative: bool = roc_str_slice[0] == '-'; - var initial_index: usize = if (is_negative) 1 else 0; - - var point_index: ?usize = null; - var index: usize = initial_index; - while (index < length) { - var byte: u8 = roc_str_slice[index]; - if (byte == '.' and point_index == null) { - point_index = index; - index += 1; - continue; - } - - if (!isDigit(byte)) { - return null; - } - index += 1; - } - - var before_str_length = length; - var after_val_i128: ?i128 = null; - if (point_index) |pi| { - before_str_length = pi; - - var after_str_len = (length - 1) - pi; - if (after_str_len > decimal_places) { - @panic("TODO runtime exception for too many decimal places!"); - } - var diff_decimal_places = decimal_places - after_str_len; - - var after_str = roc_str_slice[pi + 1 .. length]; - var after_u64 = std.fmt.parseUnsigned(u64, after_str, 10) catch null; - after_val_i128 = if (after_u64) |f| @intCast(i128, f) * math.pow(i128, 10, diff_decimal_places) else null; - } - - var before_str = roc_str_slice[initial_index..before_str_length]; - var before_val_not_adjusted = std.fmt.parseUnsigned(i128, before_str, 10) catch null; - - var before_val_i128: ?i128 = null; - if (before_val_not_adjusted) |before| { - var result: i128 = undefined; - var overflowed = @mulWithOverflow(i128, before, one_point_zero_i128, &result); - if (overflowed) { - @panic("TODO runtime exception for overflow!"); - } - before_val_i128 = result; - } - - const dec: RocDec = blk: { - if (before_val_i128) |before| { - if (after_val_i128) |after| { - var result: i128 = undefined; - var overflowed = @addWithOverflow(i128, before, after, &result); - if (overflowed) { - @panic("TODO runtime exception for overflow!"); - } - break :blk .{ .num = result }; - } else { - break :blk .{ .num = before }; - } - } else if (after_val_i128) |after| { - break :blk .{ .num = after }; - } else { - return null; - } - }; - - if (is_negative) { - return dec.negate(); - } else { - return dec; - } - } - - inline fn isDigit(c: u8) bool { - return (c -% 48) <= 9; - } - - pub fn toStr(self: RocDec) ?RocStr { - // Special case - if (self.num == 0) { - return RocStr.init("0.0", 3); - } - - const num = self.num; - const is_negative = num < 0; - - // Format the backing i128 into an array of digit (ascii) characters (u8s) - var digit_bytes_storage: [max_digits + 1]u8 = undefined; - var num_digits = std.fmt.formatIntBuf(digit_bytes_storage[0..], num, 10, .lower, .{}); - var digit_bytes: [*]u8 = digit_bytes_storage[0..]; - - // space where we assemble all the characters that make up the final string - var str_bytes: [max_str_length]u8 = undefined; - var position: usize = 0; - - // if negative, the first character is a negating minus - if (is_negative) { - str_bytes[position] = '-'; - position += 1; - - // but also, we have one fewer digit than we have characters - num_digits -= 1; - - // and we drop the minus to make later arithmetic correct - digit_bytes += 1; - } - - // Get the slice for before the decimal point - var before_digits_offset: usize = 0; - if (num_digits > decimal_places) { - // we have more digits than fit after the decimal point, - // so we must have digits before the decimal point - before_digits_offset = num_digits - decimal_places; - - for (digit_bytes[0..before_digits_offset]) |c| { - str_bytes[position] = c; - position += 1; - } - } else { - // otherwise there are no actual digits before the decimal point - // but we format it with a '0' - str_bytes[position] = '0'; - position += 1; - } - - // we've done everything before the decimal point, so now we can put the decimal point in - str_bytes[position] = '.'; - position += 1; - - const trailing_zeros: u6 = count_trailing_zeros_base10(num); - if (trailing_zeros == decimal_places) { - // add just a single zero if all decimal digits are zero - str_bytes[position] = '0'; - position += 1; - } else { - // Figure out if we need to prepend any zeros to the after decimal point - // For example, for the number 0.000123 we need to prepend 3 zeros after the decimal point - const after_zeros_num = if (num_digits < decimal_places) decimal_places - num_digits else 0; - - var i: usize = 0; - while (i < after_zeros_num) : (i += 1) { - str_bytes[position] = '0'; - position += 1; - } - - // otherwise append the decimal digits except the trailing zeros - for (digit_bytes[before_digits_offset .. num_digits - trailing_zeros]) |c| { - str_bytes[position] = c; - position += 1; - } - } - - return RocStr.init(&str_bytes, position); - } - - pub fn eq(self: RocDec, other: RocDec) bool { - return self.num == other.num; - } - - pub fn neq(self: RocDec, other: RocDec) bool { - return self.num != other.num; - } - - pub fn negate(self: RocDec) ?RocDec { - var negated = math.negate(self.num) catch null; - return if (negated) |n| .{ .num = n } else null; - } - - pub fn addWithOverflow(self: RocDec, other: RocDec) WithOverflow(RocDec) { - var answer: i128 = undefined; - const overflowed = @addWithOverflow(i128, self.num, other.num, &answer); - - return .{ .value = RocDec{ .num = answer }, .has_overflowed = overflowed }; - } - - pub fn add(self: RocDec, other: RocDec) RocDec { - const answer = RocDec.addWithOverflow(self, other); - - if (answer.has_overflowed) { - @panic("TODO runtime exception for overflow!"); - } else { - return answer.value; - } - } - - pub fn subWithOverflow(self: RocDec, other: RocDec) WithOverflow(RocDec) { - var answer: i128 = undefined; - const overflowed = @subWithOverflow(i128, self.num, other.num, &answer); - - return .{ .value = RocDec{ .num = answer }, .has_overflowed = overflowed }; - } - - pub fn sub(self: RocDec, other: RocDec) RocDec { - const answer = RocDec.subWithOverflow(self, other); - - if (answer.has_overflowed) { - @panic("TODO runtime exception for overflow!"); - } else { - return answer.value; - } - } - - pub fn mulWithOverflow(self: RocDec, other: RocDec) WithOverflow(RocDec) { - const self_i128 = self.num; - const other_i128 = other.num; - // const answer = 0; //self_i256 * other_i256; - - const is_answer_negative = (self_i128 < 0) != (other_i128 < 0); - - const self_u128 = @intCast(u128, math.absInt(self_i128) catch { - if (other_i128 == 0) { - return .{ .value = RocDec{ .num = 0 }, .has_overflowed = false }; - } else if (other_i128 == RocDec.one_point_zero.num) { - return .{ .value = self, .has_overflowed = false }; - } else { - return .{ .value = undefined, .has_overflowed = true }; - } - }); - - const other_u128 = @intCast(u128, math.absInt(other_i128) catch { - if (self_i128 == 0) { - return .{ .value = RocDec{ .num = 0 }, .has_overflowed = false }; - } else if (self_i128 == RocDec.one_point_zero.num) { - return .{ .value = other, .has_overflowed = false }; - } else { - return .{ .value = undefined, .has_overflowed = true }; - } - }); - - const unsigned_answer: i128 = mul_and_decimalize(self_u128, other_u128); - - if (is_answer_negative) { - return .{ .value = RocDec{ .num = -unsigned_answer }, .has_overflowed = false }; - } else { - return .{ .value = RocDec{ .num = unsigned_answer }, .has_overflowed = false }; - } - } - - pub fn mul(self: RocDec, other: RocDec) RocDec { - const answer = RocDec.mulWithOverflow(self, other); - - if (answer.has_overflowed) { - @panic("TODO runtime exception for overflow!"); - } else { - return answer.value; - } - } - - pub fn div(self: RocDec, other: RocDec) RocDec { - const numerator_i128 = self.num; - const denominator_i128 = other.num; - - // (0 / n) is always 0 - if (numerator_i128 == 0) { - return RocDec{ .num = 0 }; - } - - // (n / 0) is an error - if (denominator_i128 == 0) { - @panic("TODO runtime exception for dividing by 0!"); - } - - // If they're both negative, or if neither is negative, the final answer - // is positive or zero. If one is negative and the denominator isn't, the - // final answer is negative (or zero, in which case final sign won't matter). - // - // It's important that we do this in terms of negatives, because doing - // it in terms of positives can cause bugs when one is zero. - const is_answer_negative = (numerator_i128 < 0) != (denominator_i128 < 0); - - // Break the two i128s into two { hi: u64, lo: u64 } tuples, discarding - // the sign for now. - // - // We'll multiply all 4 combinations of these (hi1 x lo1, hi2 x lo2, - // hi1 x lo2, hi2 x lo1) and add them as appropriate, then apply the - // appropriate sign at the very end. - // - // We do checked_abs because if we had -i128::MAX before, this will overflow. - - const numerator_abs_i128 = math.absInt(numerator_i128) catch { - // Currently, if you try to do multiplication on i64::MIN, panic - // unless you're specifically multiplying by 0 or 1. - // - // Maybe we could support more cases in the future - if (denominator_i128 == one_point_zero_i128) { - return self; - } else { - @panic("TODO runtime exception for overflow when dividing!"); - } - }; - const numerator_u128 = @intCast(u128, numerator_abs_i128); - - const denominator_abs_i128 = math.absInt(denominator_i128) catch { - // Currently, if you try to do multiplication on i64::MIN, panic - // unless you're specifically multiplying by 0 or 1. - // - // Maybe we could support more cases in the future - if (numerator_i128 == one_point_zero_i128) { - return other; - } else { - @panic("TODO runtime exception for overflow when dividing!"); - } - }; - const denominator_u128 = @intCast(u128, denominator_abs_i128); - - const numerator_u256: U256 = mul_u128(numerator_u128, math.pow(u128, 10, decimal_places)); - const answer = div_u256_by_u128(numerator_u256, denominator_u128); - - var unsigned_answer: i128 = undefined; - if (answer.hi == 0 and answer.lo <= math.maxInt(i128)) { - unsigned_answer = @intCast(i128, answer.lo); - } else { - @panic("TODO runtime exception for overflow when dividing!"); - } - - return RocDec{ .num = if (is_answer_negative) -unsigned_answer else unsigned_answer }; - } -}; - -// A number has `k` trailling zeros if `10^k` divides into it cleanly -inline fn count_trailing_zeros_base10(input: i128) u6 { - if (input == 0) { - // this should not happen in practice - return 0; - } - - var count: u6 = 0; - var k: i128 = 1; - - while (true) { - if (@mod(input, std.math.pow(i128, 10, k)) == 0) { - count += 1; - k += 1; - } else { - break; - } - } - - return count; -} - -const U256 = struct { - hi: u128, - lo: u128, -}; - -fn mul_and_decimalize(a: u128, b: u128) i128 { - const answer_u256 = mul_u128(a, b); - - var lhs_hi = answer_u256.hi; - var lhs_lo = answer_u256.lo; - - // Divide - or just add 1, multiply by floor(2^315/10^18), then right shift 315 times. - // floor(2^315/10^18) is 66749594872528440074844428317798503581334516323645399060845050244444366430645 - - // Add 1. - // This can't overflow because the initial numbers are only 127bit due to removing the sign bit. - var overflowed = @addWithOverflow(u128, lhs_lo, 1, &lhs_lo); - lhs_hi = blk: { - if (overflowed) { - break :blk lhs_hi + 1; - } else { - break :blk lhs_hi + 0; - } - }; - - // This needs to do multiplication in a way that expands, - // since we throw away 315 bits we care only about the higher end, not lower. - // So like need to do high low mult with 2 U256's and then bitshift. - // I bet this has a lot of room for multiplication optimization. - const rhs_hi: u128 = 0x9392ee8e921d5d073aff322e62439fcf; - const rhs_lo: u128 = 0x32d7f344649470f90cac0c573bf9e1b5; - - const ea = mul_u128(lhs_lo, rhs_lo); - const gf = mul_u128(lhs_hi, rhs_lo); - const jh = mul_u128(lhs_lo, rhs_hi); - const lk = mul_u128(lhs_hi, rhs_hi); - - const e = ea.hi; - // const _a = ea.lo; - - const g = gf.hi; - const f = gf.lo; - - const j = jh.hi; - const h = jh.lo; - - const l = lk.hi; - const k = lk.lo; - - // b = e + f + h - var e_plus_f: u128 = undefined; - overflowed = @addWithOverflow(u128, e, f, &e_plus_f); - var b_carry1: u128 = undefined; - if (overflowed) { - b_carry1 = 1; - } else { - b_carry1 = 0; - } - - var idk: u128 = undefined; - overflowed = @addWithOverflow(u128, e_plus_f, h, &idk); - var b_carry2: u128 = undefined; - if (overflowed) { - b_carry2 = 1; - } else { - b_carry2 = 0; - } - - // c = carry + g + j + k // it doesn't say +k but I think it should be? - var g_plus_j: u128 = undefined; - overflowed = @addWithOverflow(u128, g, j, &g_plus_j); - var c_carry1: u128 = undefined; - if (overflowed) { - c_carry1 = 1; - } else { - c_carry1 = 0; - } - - var g_plus_j_plus_k: u128 = undefined; - overflowed = @addWithOverflow(u128, g_plus_j, k, &g_plus_j_plus_k); - var c_carry2: u128 = undefined; - if (overflowed) { - c_carry2 = 1; - } else { - c_carry2 = 0; - } - - var c_without_bcarry2: u128 = undefined; - overflowed = @addWithOverflow(u128, g_plus_j_plus_k, b_carry1, &c_without_bcarry2); - var c_carry3: u128 = undefined; - if (overflowed) { - c_carry3 = 1; - } else { - c_carry3 = 0; - } - - var c: u128 = undefined; - overflowed = @addWithOverflow(u128, c_without_bcarry2, b_carry2, &c); - var c_carry4: u128 = undefined; - if (overflowed) { - c_carry4 = 1; - } else { - c_carry4 = 0; - } - - // d = carry + l - var d: u128 = undefined; - overflowed = @addWithOverflow(u128, l, c_carry1, &d); - overflowed = overflowed or @addWithOverflow(u128, d, c_carry2, &d); - overflowed = overflowed or @addWithOverflow(u128, d, c_carry3, &d); - overflowed = overflowed or @addWithOverflow(u128, d, c_carry4, &d); - - if (overflowed) { - @panic("TODO runtime exception for overflow!"); - } - - // Final 512bit value is d, c, b, a - // need to left shift 321 times - // 315 - 256 is 59. So left shift d, c 59 times. - return @intCast(i128, c >> 59 | (d << (128 - 59))); -} - -fn mul_u128(a: u128, b: u128) U256 { - var hi: u128 = undefined; - var lo: u128 = undefined; - - const bits_in_dword_2: u32 = 64; - const lower_mask: u128 = math.maxInt(u128) >> bits_in_dword_2; - - lo = (a & lower_mask) * (b & lower_mask); - - var t = lo >> bits_in_dword_2; - - lo &= lower_mask; - - t += (a >> bits_in_dword_2) * (b & lower_mask); - - lo += (t & lower_mask) << bits_in_dword_2; - - hi = t >> bits_in_dword_2; - - t = lo >> bits_in_dword_2; - - lo &= lower_mask; - - t += (b >> bits_in_dword_2) * (a & lower_mask); - - lo += (t & lower_mask) << bits_in_dword_2; - - hi += t >> bits_in_dword_2; - - hi += (a >> bits_in_dword_2) * (b >> bits_in_dword_2); - - return .{ .hi = hi, .lo = lo }; -} - -// Multiply two 128-bit ints and divide the result by 10^DECIMAL_PLACES -// -// Adapted from https://github.com/nlordell/ethnum-rs -// Copyright (c) 2020 Nicholas Rodrigues Lordello -// Licensed under the Apache License version 2.0 -// -// When translating this to Zig, we often have to use math.shr/shl instead of >>/<< -// This is because casting to the right types for Zig can be kind of tricky. -// See https://github.com/ziglang/zig/issues/7605 -fn div_u256_by_u128(numer: U256, denom: u128) U256 { - const N_UDWORD_BITS: u8 = 128; - const N_UTWORD_BITS: u9 = 256; - - var q: U256 = undefined; - var r: U256 = undefined; - var sr: u8 = undefined; - - // special case - if (numer.hi == 0) { - // 0 X - // --- - // 0 X - return .{ - .hi = 0, - .lo = numer.lo / denom, - }; - } - - // numer.hi != 0 - if (denom == 0) { - // K X - // --- - // 0 0 - return .{ - .hi = 0, - .lo = numer.hi / denom, - }; - } else { - // K X - // --- - // 0 K - // NOTE: Modified from `if (d.low() & (d.low() - 1)) == 0`. - if (math.isPowerOfTwo(denom)) { - // if d is a power of 2 - if (denom == 1) { - return numer; - } - - sr = @ctz(u128, denom); - - return .{ - .hi = math.shr(u128, numer.hi, sr), - .lo = math.shl(u128, numer.hi, N_UDWORD_BITS - sr) | math.shr(u128, numer.lo, sr), - }; - } - - // K X - // --- - // 0 K - var denom_leading_zeros = @clz(u128, denom); - var numer_hi_leading_zeros = @clz(u128, numer.hi); - sr = 1 + N_UDWORD_BITS + denom_leading_zeros - numer_hi_leading_zeros; - // 2 <= sr <= N_UTWORD_BITS - 1 - // q.all = n.all << (N_UTWORD_BITS - sr); - // r.all = n.all >> sr; - // #[allow(clippy::comparison_chain)] - if (sr == N_UDWORD_BITS) { - q = .{ - .hi = numer.lo, - .lo = 0, - }; - r = .{ - .hi = 0, - .lo = numer.hi, - }; - } else if (sr < N_UDWORD_BITS) { - // 2 <= sr <= N_UDWORD_BITS - 1 - q = .{ - .hi = math.shl(u128, numer.lo, N_UDWORD_BITS - sr), - .lo = 0, - }; - r = .{ - .hi = math.shr(u128, numer.hi, sr), - .lo = math.shl(u128, numer.hi, N_UDWORD_BITS - sr) | math.shr(u128, numer.lo, sr), - }; - } else { - // N_UDWORD_BITS + 1 <= sr <= N_UTWORD_BITS - 1 - q = .{ - .hi = math.shl(u128, numer.hi, N_UTWORD_BITS - sr) | math.shr(u128, numer.lo, sr - N_UDWORD_BITS), - .lo = math.shl(u128, numer.lo, N_UTWORD_BITS - sr), - }; - r = .{ - .hi = 0, - .lo = math.shr(u128, numer.hi, sr - N_UDWORD_BITS), - }; - } - } - - // Not a special case - // q and r are initialized with: - // q.all = n.all << (N_UTWORD_BITS - sr); - // r.all = n.all >> sr; - // 1 <= sr <= N_UTWORD_BITS - 1 - var carry: u128 = 0; - - while (sr > 0) { - // r:q = ((r:q) << 1) | carry - r.hi = (r.hi << 1) | (r.lo >> (N_UDWORD_BITS - 1)); - r.lo = (r.lo << 1) | (q.hi >> (N_UDWORD_BITS - 1)); - q.hi = (q.hi << 1) | (q.lo >> (N_UDWORD_BITS - 1)); - q.lo = (q.lo << 1) | carry; - - // carry = 0; - // if (r.all >= d.all) - // { - // r.all -= d.all; - // carry = 1; - // } - // NOTE: Modified from `(d - r - 1) >> (N_UTWORD_BITS - 1)` to be an - // **arithmetic** shift. - - var lo: u128 = undefined; - var lo_overflowed: bool = undefined; - var hi: u128 = undefined; - - lo_overflowed = @subWithOverflow(u128, denom, r.lo, &lo); - hi = 0 -% @intCast(u128, @bitCast(u1, lo_overflowed)) -% r.hi; - - lo_overflowed = @subWithOverflow(u128, lo, 1, &lo); - hi = hi -% @intCast(u128, @bitCast(u1, lo_overflowed)); - - // TODO this U256 was originally created by: - // - // ((hi as i128) >> 127).as_u256() - // - // ...however, I can't figure out where that function is defined. - // Maybe it's defined using a macro or something. Anyway, hopefully - // this is what it would do in this scenario. - var s = .{ - .hi = 0, - .lo = math.shr(u128, hi, 127), - }; - - carry = s.lo & 1; - - // var (lo, carry) = r.lo.overflowing_sub(denom & s.lo); - lo_overflowed = @subWithOverflow(u128, r.lo, (denom & s.lo), &lo); - hi = r.hi -% @intCast(u128, @bitCast(u1, lo_overflowed)); - - r = .{ .hi = hi, .lo = lo }; - - sr -= 1; - } - - var hi = (q.hi << 1) | (q.lo >> (127)); - var lo = (q.lo << 1) | carry; - - return .{ .hi = hi, .lo = lo }; -} - -const testing = std.testing; -const expectEqual = testing.expectEqual; -const expectError = testing.expectError; -const expectEqualSlices = testing.expectEqualSlices; -const expect = testing.expect; - -test "fromU64" { - var dec = RocDec.fromU64(25); - - try expectEqual(RocDec{ .num = 25000000000000000000 }, dec); -} - -test "fromF64" { - var dec = RocDec.fromF64(25.5); - try expectEqual(RocDec{ .num = 25500000000000000000 }, dec.?); -} - -test "fromF64 overflow" { - var dec = RocDec.fromF64(1e308); - try expectEqual(dec, null); -} - -test "fromStr: empty" { - var roc_str = RocStr.init("", 0); - var dec = RocDec.fromStr(roc_str); - - try expectEqual(dec, null); -} - -test "fromStr: 0" { - var roc_str = RocStr.init("0", 1); - var dec = RocDec.fromStr(roc_str); - - try expectEqual(RocDec{ .num = 0 }, dec.?); -} - -test "fromStr: 1" { - var roc_str = RocStr.init("1", 1); - var dec = RocDec.fromStr(roc_str); - - try expectEqual(RocDec.one_point_zero, dec.?); -} - -test "fromStr: 123.45" { - var roc_str = RocStr.init("123.45", 6); - var dec = RocDec.fromStr(roc_str); - - try expectEqual(RocDec{ .num = 123450000000000000000 }, dec.?); -} - -test "fromStr: .45" { - var roc_str = RocStr.init(".45", 3); - var dec = RocDec.fromStr(roc_str); - - try expectEqual(RocDec{ .num = 450000000000000000 }, dec.?); -} - -test "fromStr: 0.45" { - var roc_str = RocStr.init("0.45", 4); - var dec = RocDec.fromStr(roc_str); - - try expectEqual(RocDec{ .num = 450000000000000000 }, dec.?); -} - -test "fromStr: 123" { - var roc_str = RocStr.init("123", 3); - var dec = RocDec.fromStr(roc_str); - - try expectEqual(RocDec{ .num = 123000000000000000000 }, dec.?); -} - -test "fromStr: -.45" { - var roc_str = RocStr.init("-.45", 4); - var dec = RocDec.fromStr(roc_str); - - try expectEqual(RocDec{ .num = -450000000000000000 }, dec.?); -} - -test "fromStr: -0.45" { - var roc_str = RocStr.init("-0.45", 5); - var dec = RocDec.fromStr(roc_str); - - try expectEqual(RocDec{ .num = -450000000000000000 }, dec.?); -} - -test "fromStr: -123" { - var roc_str = RocStr.init("-123", 4); - var dec = RocDec.fromStr(roc_str); - - try expectEqual(RocDec{ .num = -123000000000000000000 }, dec.?); -} - -test "fromStr: -123.45" { - var roc_str = RocStr.init("-123.45", 7); - var dec = RocDec.fromStr(roc_str); - - try expectEqual(RocDec{ .num = -123450000000000000000 }, dec.?); -} - -test "fromStr: abc" { - var roc_str = RocStr.init("abc", 3); - var dec = RocDec.fromStr(roc_str); - - try expectEqual(dec, null); -} - -test "fromStr: 123.abc" { - var roc_str = RocStr.init("123.abc", 7); - var dec = RocDec.fromStr(roc_str); - - try expectEqual(dec, null); -} - -test "fromStr: abc.123" { - var roc_str = RocStr.init("abc.123", 7); - var dec = RocDec.fromStr(roc_str); - - try expectEqual(dec, null); -} - -test "fromStr: .123.1" { - var roc_str = RocStr.init(".123.1", 6); - var dec = RocDec.fromStr(roc_str); - - try expectEqual(dec, null); -} - -test "toStr: 123.45" { - var dec: RocDec = .{ .num = 123450000000000000000 }; - var res_roc_str = dec.toStr(); - - const res_slice: []const u8 = "123.45"[0..]; - try expectEqualSlices(u8, res_slice, res_roc_str.?.asSlice()); -} - -test "toStr: -123.45" { - var dec: RocDec = .{ .num = -123450000000000000000 }; - var res_roc_str = dec.toStr(); - - const res_slice: []const u8 = "-123.45"[0..]; - try expectEqualSlices(u8, res_slice, res_roc_str.?.asSlice()); -} - -test "toStr: 123.0" { - var dec: RocDec = .{ .num = 123000000000000000000 }; - var res_roc_str = dec.toStr(); - - const res_slice: []const u8 = "123.0"[0..]; - try expectEqualSlices(u8, res_slice, res_roc_str.?.asSlice()); -} - -test "toStr: -123.0" { - var dec: RocDec = .{ .num = -123000000000000000000 }; - var res_roc_str = dec.toStr(); - - const res_slice: []const u8 = "-123.0"[0..]; - try expectEqualSlices(u8, res_slice, res_roc_str.?.asSlice()); -} - -test "toStr: 0.45" { - var dec: RocDec = .{ .num = 450000000000000000 }; - var res_roc_str = dec.toStr(); - - const res_slice: []const u8 = "0.45"[0..]; - try expectEqualSlices(u8, res_slice, res_roc_str.?.asSlice()); -} - -test "toStr: -0.45" { - var dec: RocDec = .{ .num = -450000000000000000 }; - var res_roc_str = dec.toStr(); - - const res_slice: []const u8 = "-0.45"[0..]; - try expectEqualSlices(u8, res_slice, res_roc_str.?.asSlice()); -} - -test "toStr: 0.00045" { - var dec: RocDec = .{ .num = 000450000000000000 }; - var res_roc_str = dec.toStr(); - - const res_slice: []const u8 = "0.00045"[0..]; - try expectEqualSlices(u8, res_slice, res_roc_str.?.asSlice()); -} - -test "toStr: -0.00045" { - var dec: RocDec = .{ .num = -000450000000000000 }; - var res_roc_str = dec.toStr(); - - const res_slice: []const u8 = "-0.00045"[0..]; - try expectEqualSlices(u8, res_slice, res_roc_str.?.asSlice()); -} - -test "toStr: -111.123456" { - var dec: RocDec = .{ .num = -111123456000000000000 }; - var res_roc_str = dec.toStr(); - - const res_slice: []const u8 = "-111.123456"[0..]; - try expectEqualSlices(u8, res_slice, res_roc_str.?.asSlice()); -} - -test "toStr: 123.1111111" { - var dec: RocDec = .{ .num = 123111111100000000000 }; - var res_roc_str = dec.toStr(); - - const res_slice: []const u8 = "123.1111111"[0..]; - try expectEqualSlices(u8, res_slice, res_roc_str.?.asSlice()); -} - -test "toStr: 123.1111111111111 (big str)" { - var dec: RocDec = .{ .num = 123111111111111000000 }; - var res_roc_str = dec.toStr(); - errdefer res_roc_str.?.deinit(); - defer res_roc_str.?.deinit(); - - const res_slice: []const u8 = "123.111111111111"[0..]; - try expectEqualSlices(u8, res_slice, res_roc_str.?.asSlice()); -} - -test "toStr: 123.111111111111444444 (max number of decimal places)" { - var dec: RocDec = .{ .num = 123111111111111444444 }; - var res_roc_str = dec.toStr(); - errdefer res_roc_str.?.deinit(); - defer res_roc_str.?.deinit(); - - const res_slice: []const u8 = "123.111111111111444444"[0..]; - try expectEqualSlices(u8, res_slice, res_roc_str.?.asSlice()); -} - -test "toStr: 12345678912345678912.111111111111111111 (max number of digits)" { - var dec: RocDec = .{ .num = 12345678912345678912111111111111111111 }; - var res_roc_str = dec.toStr(); - errdefer res_roc_str.?.deinit(); - defer res_roc_str.?.deinit(); - - const res_slice: []const u8 = "12345678912345678912.111111111111111111"[0..]; - try expectEqualSlices(u8, res_slice, res_roc_str.?.asSlice()); -} - -test "toStr: std.math.maxInt" { - var dec: RocDec = .{ .num = std.math.maxInt(i128) }; - var res_roc_str = dec.toStr(); - errdefer res_roc_str.?.deinit(); - defer res_roc_str.?.deinit(); - - const res_slice: []const u8 = "170141183460469231731.687303715884105727"[0..]; - try expectEqualSlices(u8, res_slice, res_roc_str.?.asSlice()); -} - -test "toStr: std.math.minInt" { - var dec: RocDec = .{ .num = std.math.minInt(i128) }; - var res_roc_str = dec.toStr(); - errdefer res_roc_str.?.deinit(); - defer res_roc_str.?.deinit(); - - const res_slice: []const u8 = "-170141183460469231731.687303715884105728"[0..]; - try expectEqualSlices(u8, res_slice, res_roc_str.?.asSlice()); -} - -test "toStr: 0" { - var dec: RocDec = .{ .num = 0 }; - var res_roc_str = dec.toStr(); - - const res_slice: []const u8 = "0.0"[0..]; - try expectEqualSlices(u8, res_slice, res_roc_str.?.asSlice()); -} - -test "add: 0" { - var dec: RocDec = .{ .num = 0 }; - - try expectEqual(RocDec{ .num = 0 }, dec.add(.{ .num = 0 })); -} - -test "add: 1" { - var dec: RocDec = .{ .num = 0 }; - - try expectEqual(RocDec{ .num = 1 }, dec.add(.{ .num = 1 })); -} - -test "sub: 0" { - var dec: RocDec = .{ .num = 1 }; - - try expectEqual(RocDec{ .num = 1 }, dec.sub(.{ .num = 0 })); -} - -test "sub: 1" { - var dec: RocDec = .{ .num = 1 }; - - try expectEqual(RocDec{ .num = 0 }, dec.sub(.{ .num = 1 })); -} - -test "mul: by 0" { - var dec: RocDec = .{ .num = 0 }; - - try expectEqual(RocDec{ .num = 0 }, dec.mul(.{ .num = 0 })); -} - -test "mul: by 1" { - var dec: RocDec = RocDec.fromU64(15); - - try expectEqual(RocDec.fromU64(15), dec.mul(RocDec.fromU64(1))); -} - -test "mul: by 2" { - var dec: RocDec = RocDec.fromU64(15); - - try expectEqual(RocDec.fromU64(30), dec.mul(RocDec.fromU64(2))); -} - -test "div: 0 / 2" { - var dec: RocDec = RocDec.fromU64(0); - - try expectEqual(RocDec.fromU64(0), dec.div(RocDec.fromU64(2))); -} - -test "div: 2 / 2" { - var dec: RocDec = RocDec.fromU64(2); - - try expectEqual(RocDec.fromU64(1), dec.div(RocDec.fromU64(2))); -} - -test "div: 20 / 2" { - var dec: RocDec = RocDec.fromU64(20); - - try expectEqual(RocDec.fromU64(10), dec.div(RocDec.fromU64(2))); -} - -test "div: 8 / 5" { - var dec: RocDec = RocDec.fromU64(8); - var res: RocDec = RocDec.fromStr(RocStr.init("1.6", 3)).?; - try expectEqual(res, dec.div(RocDec.fromU64(5))); -} - -test "div: 10 / 3" { - var numer: RocDec = RocDec.fromU64(10); - var denom: RocDec = RocDec.fromU64(3); - - var roc_str = RocStr.init("3.333333333333333333", 20); - errdefer roc_str.deinit(); - defer roc_str.deinit(); - - var res: RocDec = RocDec.fromStr(roc_str).?; - - try expectEqual(res, numer.div(denom)); -} - -// exports - -pub fn fromStr(arg: RocStr) callconv(.C) num_.NumParseResult(i128) { - if (@call(.{ .modifier = always_inline }, RocDec.fromStr, .{arg})) |dec| { - return .{ .errorcode = 0, .value = dec.num }; - } else { - return .{ .errorcode = 1, .value = 0 }; - } -} - -pub fn fromF64C(arg: f64) callconv(.C) i128 { - return if (@call(.{ .modifier = always_inline }, RocDec.fromF64, .{arg})) |dec| dec.num else @panic("TODO runtime exception failing convert f64 to RocDec"); -} - -pub fn eqC(arg1: RocDec, arg2: RocDec) callconv(.C) bool { - return @call(.{ .modifier = always_inline }, RocDec.eq, .{ arg1, arg2 }); -} - -pub fn neqC(arg1: RocDec, arg2: RocDec) callconv(.C) bool { - return @call(.{ .modifier = always_inline }, RocDec.neq, .{ arg1, arg2 }); -} - -pub fn negateC(arg: RocDec) callconv(.C) i128 { - return if (@call(.{ .modifier = always_inline }, RocDec.negate, .{arg})) |dec| dec.num else @panic("TODO overflow for negating RocDec"); -} - -pub fn addC(arg1: RocDec, arg2: RocDec) callconv(.C) WithOverflow(RocDec) { - return @call(.{ .modifier = always_inline }, RocDec.addWithOverflow, .{ arg1, arg2 }); -} - -pub fn subC(arg1: RocDec, arg2: RocDec) callconv(.C) WithOverflow(RocDec) { - return @call(.{ .modifier = always_inline }, RocDec.subWithOverflow, .{ arg1, arg2 }); -} - -pub fn mulC(arg1: RocDec, arg2: RocDec) callconv(.C) WithOverflow(RocDec) { - return @call(.{ .modifier = always_inline }, RocDec.mulWithOverflow, .{ arg1, arg2 }); -} - -pub fn divC(arg1: RocDec, arg2: RocDec) callconv(.C) i128 { - return @call(.{ .modifier = always_inline }, RocDec.div, .{ arg1, arg2 }).num; -} diff --git a/compiler/builtins/bitcode/src/dict.zig b/compiler/builtins/bitcode/src/dict.zig deleted file mode 100644 index 6ab2c894c2..0000000000 --- a/compiler/builtins/bitcode/src/dict.zig +++ /dev/null @@ -1,815 +0,0 @@ -const std = @import("std"); -const testing = std.testing; -const expectEqual = testing.expectEqual; -const mem = std.mem; -const assert = std.debug.assert; - -const utils = @import("utils.zig"); -const RocList = @import("list.zig").RocList; - -const INITIAL_SEED = 0xc70f6907; - -const InPlace = enum(u8) { - InPlace, - Clone, -}; - -const Slot = enum(u8) { - Empty, - Filled, - PreviouslyFilled, -}; - -const MaybeIndexTag = enum { index, not_found }; - -const MaybeIndex = union(MaybeIndexTag) { index: usize, not_found: void }; - -fn nextSeed(seed: u64) u64 { - // TODO is this a valid way to get a new seed? are there better ways? - return seed + 1; -} - -fn totalCapacityAtLevel(input: usize) usize { - if (input == 0) { - return 0; - } - - var n = input; - var slots: usize = 8; - - while (n > 1) : (n -= 1) { - slots = slots * 2 + slots; - } - - return slots; -} - -fn capacityOfLevel(input: usize) usize { - if (input == 0) { - return 0; - } - - var n = input; - var slots: usize = 8; - - while (n > 1) : (n -= 1) { - slots = slots * 2; - } - - return slots; -} - -// aligmnent of elements. The number (16 or 8) indicates the maximum -// alignment of the key and value. The tag furthermore indicates -// which has the biggest aligmnent. If both are the same, we put -// the key first -const Alignment = extern struct { - bits: u8, - - const VALUE_BEFORE_KEY_FLAG: u8 = 0b1000_0000; - - fn toU32(self: Alignment) u32 { - if (self.bits >= VALUE_BEFORE_KEY_FLAG) { - return self.bits ^ Alignment.VALUE_BEFORE_KEY_FLAG; - } else { - return self.bits; - } - } - - fn keyFirst(self: Alignment) bool { - if (self.bits & Alignment.VALUE_BEFORE_KEY_FLAG > 0) { - return false; - } else { - return true; - } - } -}; - -pub fn decref( - bytes_or_null: ?[*]u8, - data_bytes: usize, - alignment: Alignment, -) void { - return utils.decref(bytes_or_null, data_bytes, alignment.toU32()); -} - -pub fn allocateWithRefcount( - data_bytes: usize, - alignment: Alignment, -) [*]u8 { - return utils.allocateWithRefcount(data_bytes, alignment.toU32()); -} - -pub const RocDict = extern struct { - dict_bytes: ?[*]u8, - dict_entries_len: usize, - number_of_levels: usize, - - pub fn empty() RocDict { - return RocDict{ - .dict_entries_len = 0, - .number_of_levels = 0, - .dict_bytes = null, - }; - } - - pub fn allocate( - number_of_levels: usize, - number_of_entries: usize, - alignment: Alignment, - key_size: usize, - value_size: usize, - ) RocDict { - const number_of_slots = totalCapacityAtLevel(number_of_levels); - const slot_size = slotSize(key_size, value_size); - const data_bytes = number_of_slots * slot_size; - - return RocDict{ - .dict_bytes = allocateWithRefcount(data_bytes, alignment), - .number_of_levels = number_of_levels, - .dict_entries_len = number_of_entries, - }; - } - - pub fn reallocate( - self: RocDict, - alignment: Alignment, - key_width: usize, - value_width: usize, - ) RocDict { - const new_level = self.number_of_levels + 1; - const slot_size = slotSize(key_width, value_width); - - const old_capacity = self.capacity(); - const new_capacity = totalCapacityAtLevel(new_level); - const delta_capacity = new_capacity - old_capacity; - - const data_bytes = new_capacity * slot_size; - const first_slot = allocateWithRefcount(data_bytes, alignment); - - // transfer the memory - - if (self.dict_bytes) |source_ptr| { - const dest_ptr = first_slot; - - var source_offset: usize = 0; - var dest_offset: usize = 0; - - if (alignment.keyFirst()) { - // copy keys - @memcpy(dest_ptr + dest_offset, source_ptr + source_offset, old_capacity * key_width); - - // copy values - source_offset = old_capacity * key_width; - dest_offset = new_capacity * key_width; - @memcpy(dest_ptr + dest_offset, source_ptr + source_offset, old_capacity * value_width); - } else { - // copy values - @memcpy(dest_ptr + dest_offset, source_ptr + source_offset, old_capacity * value_width); - - // copy keys - source_offset = old_capacity * value_width; - dest_offset = new_capacity * value_width; - @memcpy(dest_ptr + dest_offset, source_ptr + source_offset, old_capacity * key_width); - } - - // copy slots - source_offset = old_capacity * (key_width + value_width); - dest_offset = new_capacity * (key_width + value_width); - @memcpy(dest_ptr + dest_offset, source_ptr + source_offset, old_capacity * @sizeOf(Slot)); - } - - var i: usize = 0; - const first_new_slot_value = first_slot + old_capacity * slot_size + delta_capacity * (key_width + value_width); - while (i < (new_capacity - old_capacity)) : (i += 1) { - (first_new_slot_value)[i] = @enumToInt(Slot.Empty); - } - - const result = RocDict{ - .dict_bytes = first_slot, - .number_of_levels = self.number_of_levels + 1, - .dict_entries_len = self.dict_entries_len, - }; - - // NOTE we fuse an increment of all keys/values with a decrement of the input dict - decref(self.dict_bytes, self.capacity() * slotSize(key_width, value_width), alignment); - - return result; - } - - pub fn asU8ptr(self: RocDict) [*]u8 { - return @ptrCast([*]u8, self.dict_bytes); - } - - pub fn len(self: RocDict) usize { - return self.dict_entries_len; - } - - pub fn isEmpty(self: RocDict) bool { - return self.len() == 0; - } - - pub fn isUnique(self: RocDict) bool { - // the empty dict is unique (in the sense that copying it will not leak memory) - if (self.isEmpty()) { - return true; - } - - // otherwise, check if the refcount is one - const ptr: [*]usize = @ptrCast([*]usize, @alignCast(@alignOf(usize), self.dict_bytes)); - return (ptr - 1)[0] == utils.REFCOUNT_ONE; - } - - pub fn capacity(self: RocDict) usize { - return totalCapacityAtLevel(self.number_of_levels); - } - - pub fn makeUnique(self: RocDict, alignment: Alignment, key_width: usize, value_width: usize) RocDict { - if (self.isEmpty()) { - return self; - } - - if (self.isUnique()) { - return self; - } - - // unfortunately, we have to clone - var new_dict = RocDict.allocate(self.number_of_levels, self.dict_entries_len, alignment, key_width, value_width); - - var old_bytes: [*]u8 = @ptrCast([*]u8, self.dict_bytes); - var new_bytes: [*]u8 = @ptrCast([*]u8, new_dict.dict_bytes); - - const number_of_bytes = self.capacity() * (@sizeOf(Slot) + key_width + value_width); - @memcpy(new_bytes, old_bytes, number_of_bytes); - - // NOTE we fuse an increment of all keys/values with a decrement of the input dict - const data_bytes = self.capacity() * slotSize(key_width, value_width); - decref(self.dict_bytes, data_bytes, alignment); - - return new_dict; - } - - fn getSlot(self: *const RocDict, index: usize, key_width: usize, value_width: usize) Slot { - const offset = self.capacity() * (key_width + value_width) + index * @sizeOf(Slot); - - const ptr = self.dict_bytes orelse unreachable; - return @intToEnum(Slot, ptr[offset]); - } - - fn setSlot(self: *RocDict, index: usize, key_width: usize, value_width: usize, slot: Slot) void { - const offset = self.capacity() * (key_width + value_width) + index * @sizeOf(Slot); - - const ptr = self.dict_bytes orelse unreachable; - ptr[offset] = @enumToInt(slot); - } - - fn setKey(self: *RocDict, index: usize, alignment: Alignment, key_width: usize, value_width: usize, data: Opaque) void { - if (key_width == 0) { - return; - } - - const offset = blk: { - if (alignment.keyFirst()) { - break :blk (index * key_width); - } else { - break :blk (self.capacity() * value_width) + (index * key_width); - } - }; - - const ptr = self.dict_bytes orelse unreachable; - - const source = data orelse unreachable; - const dest = ptr + offset; - @memcpy(dest, source, key_width); - } - - fn getKey(self: *const RocDict, index: usize, alignment: Alignment, key_width: usize, value_width: usize) Opaque { - if (key_width == 0) { - return null; - } - - const offset = blk: { - if (alignment.keyFirst()) { - break :blk (index * key_width); - } else { - break :blk (self.capacity() * value_width) + (index * key_width); - } - }; - - const ptr = self.dict_bytes orelse unreachable; - return ptr + offset; - } - - fn setValue(self: *RocDict, index: usize, alignment: Alignment, key_width: usize, value_width: usize, data: Opaque) void { - if (value_width == 0) { - return; - } - - const offset = blk: { - if (alignment.keyFirst()) { - break :blk (self.capacity() * key_width) + (index * value_width); - } else { - break :blk (index * value_width); - } - }; - - const ptr = self.dict_bytes orelse unreachable; - - const source = data orelse unreachable; - const dest = ptr + offset; - @memcpy(dest, source, value_width); - } - - fn getValue(self: *const RocDict, index: usize, alignment: Alignment, key_width: usize, value_width: usize) Opaque { - if (value_width == 0) { - return null; - } - - const offset = blk: { - if (alignment.keyFirst()) { - break :blk (self.capacity() * key_width) + (index * value_width); - } else { - break :blk (index * value_width); - } - }; - - const ptr = self.dict_bytes orelse unreachable; - return ptr + offset; - } - - fn findIndex(self: *const RocDict, alignment: Alignment, key: Opaque, key_width: usize, value_width: usize, hash_fn: HashFn, is_eq: EqFn) MaybeIndex { - if (self.isEmpty()) { - return MaybeIndex.not_found; - } - - var seed: u64 = INITIAL_SEED; - - var current_level: usize = 1; - var current_level_size: usize = 8; - var next_level_size: usize = 2 * current_level_size; - - while (true) { - if (current_level > self.number_of_levels) { - return MaybeIndex.not_found; - } - - // hash the key, and modulo by the maximum size - // (so we get an in-bounds index) - const hash = hash_fn(seed, key); - const index = capacityOfLevel(current_level - 1) + @intCast(usize, (hash % current_level_size)); - - switch (self.getSlot(index, key_width, value_width)) { - Slot.Empty, Slot.PreviouslyFilled => { - return MaybeIndex.not_found; - }, - Slot.Filled => { - // is this the same key, or a new key? - const current_key = self.getKey(index, alignment, key_width, value_width); - - if (is_eq(key, current_key)) { - return MaybeIndex{ .index = index }; - } else { - current_level += 1; - current_level_size *= 2; - next_level_size *= 2; - - seed = nextSeed(seed); - continue; - } - }, - } - } - } -}; - -// Dict.empty -pub fn dictEmpty(dict: *RocDict) callconv(.C) void { - dict.* = RocDict.empty(); -} - -pub fn slotSize(key_size: usize, value_size: usize) usize { - return @sizeOf(Slot) + key_size + value_size; -} - -// Dict.len -pub fn dictLen(dict: RocDict) callconv(.C) usize { - return dict.dict_entries_len; -} - -// commonly used type aliases -const Opaque = ?[*]u8; -const HashFn = fn (u64, ?[*]u8) callconv(.C) u64; -const EqFn = fn (?[*]u8, ?[*]u8) callconv(.C) bool; - -const Inc = fn (?[*]u8) callconv(.C) void; -const IncN = fn (?[*]u8, usize) callconv(.C) void; -const Dec = fn (?[*]u8) callconv(.C) void; - -const Caller3 = fn (?[*]u8, ?[*]u8, ?[*]u8, ?[*]u8, ?[*]u8) callconv(.C) void; - -// Dict.insert : Dict k v, k, v -> Dict k v -pub fn dictInsert( - input: RocDict, - alignment: Alignment, - key: Opaque, - key_width: usize, - value: Opaque, - value_width: usize, - hash_fn: HashFn, - is_eq: EqFn, - dec_key: Dec, - dec_value: Dec, - output: *RocDict, -) callconv(.C) void { - var seed: u64 = INITIAL_SEED; - - var result = input.makeUnique(alignment, key_width, value_width); - - var current_level: usize = 1; - var current_level_size: usize = 8; - var next_level_size: usize = 2 * current_level_size; - - while (true) { - if (current_level > result.number_of_levels) { - result = result.reallocate(alignment, key_width, value_width); - } - - const hash = hash_fn(seed, key); - const index = capacityOfLevel(current_level - 1) + @intCast(usize, (hash % current_level_size)); - assert(index < result.capacity()); - - switch (result.getSlot(index, key_width, value_width)) { - Slot.Empty, Slot.PreviouslyFilled => { - result.setSlot(index, key_width, value_width, Slot.Filled); - result.setKey(index, alignment, key_width, value_width, key); - result.setValue(index, alignment, key_width, value_width, value); - - result.dict_entries_len += 1; - break; - }, - Slot.Filled => { - // is this the same key, or a new key? - const current_key = result.getKey(index, alignment, key_width, value_width); - - if (is_eq(key, current_key)) { - // we will override the old value, but first have to decrement its refcount - const current_value = result.getValue(index, alignment, key_width, value_width); - dec_value(current_value); - - // we must consume the key argument! - dec_key(key); - - result.setValue(index, alignment, key_width, value_width, value); - break; - } else { - seed = nextSeed(seed); - - current_level += 1; - current_level_size *= 2; - next_level_size *= 2; - - continue; - } - }, - } - } - - // write result into pointer - output.* = result; -} - -// Dict.remove : Dict k v, k -> Dict k v -pub fn dictRemove(input: RocDict, alignment: Alignment, key: Opaque, key_width: usize, value_width: usize, hash_fn: HashFn, is_eq: EqFn, dec_key: Dec, dec_value: Dec, output: *RocDict) callconv(.C) void { - switch (input.findIndex(alignment, key, key_width, value_width, hash_fn, is_eq)) { - MaybeIndex.not_found => { - // the key was not found; we're done - output.* = input; - return; - }, - MaybeIndex.index => |index| { - var dict = input.makeUnique(alignment, key_width, value_width); - - assert(index < dict.capacity()); - - dict.setSlot(index, key_width, value_width, Slot.PreviouslyFilled); - const old_key = dict.getKey(index, alignment, key_width, value_width); - const old_value = dict.getValue(index, alignment, key_width, value_width); - - dec_key(old_key); - dec_value(old_value); - dict.dict_entries_len -= 1; - - // if the dict is now completely empty, free its allocation - if (dict.dict_entries_len == 0) { - const data_bytes = dict.capacity() * slotSize(key_width, value_width); - decref(dict.dict_bytes, data_bytes, alignment); - output.* = RocDict.empty(); - return; - } - - output.* = dict; - }, - } -} - -// Dict.contains : Dict k v, k -> Bool -pub fn dictContains(dict: RocDict, alignment: Alignment, key: Opaque, key_width: usize, value_width: usize, hash_fn: HashFn, is_eq: EqFn) callconv(.C) bool { - switch (dict.findIndex(alignment, key, key_width, value_width, hash_fn, is_eq)) { - MaybeIndex.not_found => { - return false; - }, - MaybeIndex.index => |_| { - return true; - }, - } -} - -// Dict.get : Dict k v, k -> { flag: bool, value: Opaque } -pub fn dictGet(dict: RocDict, alignment: Alignment, key: Opaque, key_width: usize, value_width: usize, hash_fn: HashFn, is_eq: EqFn, inc_value: Inc) callconv(.C) extern struct { value: Opaque, flag: bool } { - switch (dict.findIndex(alignment, key, key_width, value_width, hash_fn, is_eq)) { - MaybeIndex.not_found => { - return .{ .flag = false, .value = null }; - }, - MaybeIndex.index => |index| { - var value = dict.getValue(index, alignment, key_width, value_width); - inc_value(value); - return .{ .flag = true, .value = value }; - }, - } -} - -// Dict.elementsRc -// increment or decrement all dict elements (but not the dict's allocation itself) -pub fn elementsRc(dict: RocDict, alignment: Alignment, key_width: usize, value_width: usize, modify_key: Inc, modify_value: Inc) callconv(.C) void { - const size = dict.capacity(); - - var i: usize = 0; - while (i < size) : (i += 1) { - switch (dict.getSlot(i, key_width, value_width)) { - Slot.Filled => { - modify_key(dict.getKey(i, alignment, key_width, value_width)); - modify_value(dict.getValue(i, alignment, key_width, value_width)); - }, - else => {}, - } - } -} - -pub fn dictKeys( - dict: RocDict, - alignment: Alignment, - key_width: usize, - value_width: usize, - inc_key: Inc, -) callconv(.C) RocList { - const size = dict.capacity(); - - var length: usize = 0; - var i: usize = 0; - while (i < size) : (i += 1) { - switch (dict.getSlot(i, key_width, value_width)) { - Slot.Filled => { - length += 1; - }, - else => {}, - } - } - - if (length == 0) { - return RocList.empty(); - } - - const data_bytes = length * key_width; - var ptr = allocateWithRefcount(data_bytes, alignment); - - i = 0; - var copied: usize = 0; - while (i < size) : (i += 1) { - switch (dict.getSlot(i, key_width, value_width)) { - Slot.Filled => { - const key = dict.getKey(i, alignment, key_width, value_width); - inc_key(key); - - const key_cast = @ptrCast([*]const u8, key); - @memcpy(ptr + (copied * key_width), key_cast, key_width); - copied += 1; - }, - else => {}, - } - } - - return RocList{ .bytes = ptr, .length = length, .capacity = length }; -} - -pub fn dictValues( - dict: RocDict, - alignment: Alignment, - key_width: usize, - value_width: usize, - inc_value: Inc, -) callconv(.C) RocList { - const size = dict.capacity(); - - var length: usize = 0; - var i: usize = 0; - while (i < size) : (i += 1) { - switch (dict.getSlot(i, key_width, value_width)) { - Slot.Filled => { - length += 1; - }, - else => {}, - } - } - - if (length == 0) { - return RocList.empty(); - } - - const data_bytes = length * value_width; - var ptr = allocateWithRefcount(data_bytes, alignment); - - i = 0; - var copied: usize = 0; - while (i < size) : (i += 1) { - switch (dict.getSlot(i, key_width, value_width)) { - Slot.Filled => { - const value = dict.getValue(i, alignment, key_width, value_width); - inc_value(value); - - const value_cast = @ptrCast([*]const u8, value); - @memcpy(ptr + (copied * value_width), value_cast, value_width); - copied += 1; - }, - else => {}, - } - } - - return RocList{ .bytes = ptr, .length = length, .capacity = length }; -} - -fn doNothing(_: Opaque) callconv(.C) void { - return; -} - -pub fn dictUnion( - dict1: RocDict, - dict2: RocDict, - alignment: Alignment, - key_width: usize, - value_width: usize, - hash_fn: HashFn, - is_eq: EqFn, - inc_key: Inc, - inc_value: Inc, - output: *RocDict, -) callconv(.C) void { - output.* = dict1.makeUnique(alignment, key_width, value_width); - - var i: usize = 0; - while (i < dict2.capacity()) : (i += 1) { - switch (dict2.getSlot(i, key_width, value_width)) { - Slot.Filled => { - const key = dict2.getKey(i, alignment, key_width, value_width); - - switch (output.findIndex(alignment, key, key_width, value_width, hash_fn, is_eq)) { - MaybeIndex.not_found => { - const value = dict2.getValue(i, alignment, key_width, value_width); - inc_value(value); - - // we need an extra RC token for the key - inc_key(key); - inc_value(value); - - // we know the newly added key is not a duplicate, so the `dec`s are unreachable - const dec_key = doNothing; - const dec_value = doNothing; - - dictInsert(output.*, alignment, key, key_width, value, value_width, hash_fn, is_eq, dec_key, dec_value, output); - }, - MaybeIndex.index => |_| { - // the key is already in the output dict - continue; - }, - } - }, - else => {}, - } - } -} - -pub fn dictIntersection(dict1: RocDict, dict2: RocDict, alignment: Alignment, key_width: usize, value_width: usize, hash_fn: HashFn, is_eq: EqFn, dec_key: Inc, dec_value: Inc, output: *RocDict) callconv(.C) void { - output.* = dict1.makeUnique(alignment, key_width, value_width); - - var i: usize = 0; - const size = dict1.capacity(); - while (i < size) : (i += 1) { - switch (output.getSlot(i, key_width, value_width)) { - Slot.Filled => { - const key = dict1.getKey(i, alignment, key_width, value_width); - - switch (dict2.findIndex(alignment, key, key_width, value_width, hash_fn, is_eq)) { - MaybeIndex.not_found => { - dictRemove(output.*, alignment, key, key_width, value_width, hash_fn, is_eq, dec_key, dec_value, output); - }, - MaybeIndex.index => |_| { - // keep this key/value - continue; - }, - } - }, - else => {}, - } - } -} - -pub fn dictDifference(dict1: RocDict, dict2: RocDict, alignment: Alignment, key_width: usize, value_width: usize, hash_fn: HashFn, is_eq: EqFn, dec_key: Dec, dec_value: Dec, output: *RocDict) callconv(.C) void { - output.* = dict1.makeUnique(alignment, key_width, value_width); - - var i: usize = 0; - const size = dict1.capacity(); - while (i < size) : (i += 1) { - switch (output.getSlot(i, key_width, value_width)) { - Slot.Filled => { - const key = dict1.getKey(i, alignment, key_width, value_width); - - switch (dict2.findIndex(alignment, key, key_width, value_width, hash_fn, is_eq)) { - MaybeIndex.not_found => { - // keep this key/value - continue; - }, - MaybeIndex.index => |_| { - dictRemove(output.*, alignment, key, key_width, value_width, hash_fn, is_eq, dec_key, dec_value, output); - }, - } - }, - else => {}, - } - } -} - -pub fn setFromList(list: RocList, alignment: Alignment, key_width: usize, value_width: usize, hash_fn: HashFn, is_eq: EqFn, dec_key: Dec, output: *RocDict) callconv(.C) void { - output.* = RocDict.empty(); - - var ptr = @ptrCast([*]u8, list.bytes); - - const dec_value = doNothing; - const value = null; - - const size = list.length; - var i: usize = 0; - while (i < size) : (i += 1) { - const key = ptr + i * key_width; - dictInsert(output.*, alignment, key, key_width, value, value_width, hash_fn, is_eq, dec_key, dec_value, output); - } - - // NOTE: decref checks for the empty case - const data_bytes = size * key_width; - decref(list.bytes, data_bytes, alignment); -} - -pub fn dictWalk( - dict: RocDict, - caller: Caller3, - data: Opaque, - inc_n_data: IncN, - data_is_owned: bool, - accum: Opaque, - alignment: Alignment, - key_width: usize, - value_width: usize, - accum_width: usize, - output: Opaque, -) callconv(.C) void { - const alignment_u32 = alignment.toU32(); - // allocate space to write the result of the stepper into - // experimentally aliasing the accum and output pointers is not a good idea - // TODO handle alloc failing! - const bytes_ptr: [*]u8 = utils.alloc(accum_width, alignment_u32) orelse unreachable; - var b1 = output orelse unreachable; - var b2 = bytes_ptr; - - if (data_is_owned) { - inc_n_data(data, dict.len()); - } - - @memcpy(b2, accum orelse unreachable, accum_width); - - var i: usize = 0; - const size = dict.capacity(); - while (i < size) : (i += 1) { - switch (dict.getSlot(i, key_width, value_width)) { - Slot.Filled => { - const key = dict.getKey(i, alignment, key_width, value_width); - const value = dict.getValue(i, alignment, key_width, value_width); - - caller(data, b2, key, value, b1); - - std.mem.swap([*]u8, &b1, &b2); - }, - else => {}, - } - } - - @memcpy(output orelse unreachable, b2, accum_width); - utils.dealloc(bytes_ptr, alignment_u32); -} diff --git a/compiler/builtins/bitcode/src/expect.zig b/compiler/builtins/bitcode/src/expect.zig deleted file mode 100644 index cc111b7005..0000000000 --- a/compiler/builtins/bitcode/src/expect.zig +++ /dev/null @@ -1,134 +0,0 @@ -const std = @import("std"); -const utils = @import("utils.zig"); -const CSlice = utils.CSlice; -const always_inline = std.builtin.CallOptions.Modifier.always_inline; - -const Failure = struct { - start_line: u32, - end_line: u32, - start_col: u16, - end_col: u16, -}; - -// BEGIN FAILURES GLOBALS /////////////////// -var failures_mutex = std.Thread.Mutex{}; -var failures: [*]Failure = undefined; -var failure_length: usize = 0; -var failure_capacity: usize = 0; -// END FAILURES GLOBALS ///////////////////// - -pub fn expectFailed( - start_line: u32, - end_line: u32, - start_col: u16, - end_col: u16, -) void { - const new_failure = Failure{ .start_line = start_line, .end_line = end_line, .start_col = start_col, .end_col = end_col }; - - // Lock the failures mutex before reading from any of the failures globals, - // and then release the lock once we're done modifying things. - failures_mutex.lock(); - defer failures_mutex.unlock(); - - // If we don't have enough capacity to add a failure, allocate a new failures pointer. - if (failure_length >= failure_capacity) { - if (failure_capacity > 0) { - // We already had previous failures allocated, so try to realloc in order - // to grow the size in-place without having to memcpy bytes over. - const old_pointer = failures; - const old_bytes = failure_capacity * @sizeOf(Failure); - - failure_capacity *= 2; - - const new_bytes = failure_capacity * @sizeOf(Failure); - const failures_u8 = @ptrCast([*]u8, @alignCast(@alignOf(Failure), failures)); - const raw_pointer = utils.realloc(failures_u8, new_bytes, old_bytes, @alignOf(Failure)); - - failures = @ptrCast([*]Failure, @alignCast(@alignOf(Failure), raw_pointer)); - - // If realloc wasn't able to expand in-place (that is, it returned a different pointer), - // then copy the data into the new pointer and dealloc the old one. - if (failures != old_pointer) { - const old_pointer_u8 = @ptrCast([*]u8, old_pointer); - utils.memcpy(@ptrCast([*]u8, failures), old_pointer_u8, old_bytes); - utils.dealloc(old_pointer_u8, @alignOf(Failure)); - } - } else { - // We've never had any failures before, so allocate the failures for the first time. - failure_capacity = 10; - - const raw_pointer = utils.alloc(failure_capacity * @sizeOf(Failure), @alignOf(Failure)); - - failures = @ptrCast([*]Failure, @alignCast(@alignOf(Failure), raw_pointer)); - } - } - - failures[failure_length] = new_failure; - failure_length += 1; -} - -pub fn expectFailedC( - start_line: u32, - end_line: u32, - start_col: u16, - end_col: u16, -) callconv(.C) void { - return @call(.{ .modifier = always_inline }, expectFailed, .{ start_line, end_line, start_col, end_col }); -} - -pub fn getExpectFailures() []Failure { - failures_mutex.lock(); - defer failures_mutex.unlock(); - - if (failure_length > 0) { - // defensively clone failures, in case someone modifies the originals after the mutex has been released. - const num_bytes = failure_length * @sizeOf(Failure); - // TODO handle the possibility of alloc failing - const raw_clones = utils.alloc(num_bytes, @alignOf(Failure)) orelse unreachable; - - utils.memcpy(raw_clones, @ptrCast([*]u8, failures), num_bytes); - - const clones = @ptrCast([*]Failure, @alignCast(@alignOf(Failure), raw_clones)); - - return clones[0..failure_length]; - } else { - return failures[0..0]; - } -} - -pub fn getExpectFailuresC() callconv(.C) CSlice { - var bytes = @ptrCast(*anyopaque, failures); - - return .{ .pointer = bytes, .len = failure_length }; -} - -pub fn deinitFailures() void { - failures_mutex.lock(); - defer failures_mutex.unlock(); - - utils.dealloc(@ptrCast([*]u8, failures), @alignOf(Failure)); - failure_length = 0; -} - -pub fn deinitFailuresC() callconv(.C) void { - return @call(.{ .modifier = always_inline }, deinitFailures, .{}); -} - -test "expectFailure does something" { - defer deinitFailures(); - - var fails = getExpectFailures(); - try std.testing.expectEqual(fails.len, 0); - - expectFailed(1, 2, 3, 4); - - fails = getExpectFailures(); - try std.testing.expectEqual(fails.len, 1); - utils.dealloc(@ptrCast([*]u8, fails.ptr), @alignOf([*]Failure)); - - const what_it_should_look_like = Failure{ .start_line = 1, .end_line = 2, .start_col = 3, .end_col = 4 }; - - fails = getExpectFailures(); - try std.testing.expectEqual(fails[0], what_it_should_look_like); - utils.dealloc(@ptrCast([*]u8, fails.ptr), @alignOf([*]Failure)); -} diff --git a/compiler/builtins/bitcode/src/list.zig b/compiler/builtins/bitcode/src/list.zig deleted file mode 100644 index 6d0171f2d9..0000000000 --- a/compiler/builtins/bitcode/src/list.zig +++ /dev/null @@ -1,1362 +0,0 @@ -const std = @import("std"); -const utils = @import("utils.zig"); -const RocResult = utils.RocResult; -const UpdateMode = utils.UpdateMode; -const mem = std.mem; - -const EqFn = fn (?[*]u8, ?[*]u8) callconv(.C) bool; -const CompareFn = fn (?[*]u8, ?[*]u8, ?[*]u8) callconv(.C) u8; -const Opaque = ?[*]u8; - -const Inc = fn (?[*]u8) callconv(.C) void; -const IncN = fn (?[*]u8, usize) callconv(.C) void; -const Dec = fn (?[*]u8) callconv(.C) void; -const HasTagId = fn (u16, ?[*]u8) callconv(.C) extern struct { matched: bool, data: ?[*]u8 }; - -pub const RocList = extern struct { - bytes: ?[*]u8, - length: usize, - capacity: usize, - - pub fn len(self: RocList) usize { - return self.length; - } - - pub fn isEmpty(self: RocList) bool { - return self.len() == 0; - } - - pub fn empty() RocList { - return RocList{ .bytes = null, .length = 0, .capacity = 0 }; - } - - pub fn isUnique(self: RocList) bool { - // the empty list is unique (in the sense that copying it will not leak memory) - if (self.isEmpty()) { - return true; - } - - // otherwise, check if the refcount is one - const ptr: [*]usize = @ptrCast([*]usize, @alignCast(@alignOf(usize), self.bytes)); - return (ptr - 1)[0] == utils.REFCOUNT_ONE; - } - - pub fn allocate( - alignment: u32, - length: usize, - element_size: usize, - ) RocList { - const data_bytes = length * element_size; - - return RocList{ - .bytes = utils.allocateWithRefcount(data_bytes, alignment), - .length = length, - .capacity = length, - }; - } - - pub fn makeUniqueExtra(self: RocList, alignment: u32, element_width: usize, update_mode: UpdateMode) RocList { - if (update_mode == .InPlace) { - return self; - } else { - return self.makeUnique(alignment, element_width); - } - } - - pub fn makeUnique(self: RocList, alignment: u32, element_width: usize) RocList { - if (self.isEmpty()) { - return self; - } - - if (self.isUnique()) { - return self; - } - - // unfortunately, we have to clone - var new_list = RocList.allocate(alignment, self.length, element_width); - - var old_bytes: [*]u8 = @ptrCast([*]u8, self.bytes); - var new_bytes: [*]u8 = @ptrCast([*]u8, new_list.bytes); - - const number_of_bytes = self.len() * element_width; - @memcpy(new_bytes, old_bytes, number_of_bytes); - - // NOTE we fuse an increment of all keys/values with a decrement of the input dict - const data_bytes = self.len() * element_width; - utils.decref(self.bytes, data_bytes, alignment); - - return new_list; - } - - pub fn reallocate( - self: RocList, - alignment: u32, - new_length: usize, - element_width: usize, - ) RocList { - if (self.bytes) |source_ptr| { - if (self.isUnique()) { - const new_source = utils.unsafeReallocate(source_ptr, alignment, self.len(), new_length, element_width); - - return RocList{ .bytes = new_source, .length = new_length, .capacity = new_length }; - } - } - - return self.reallocateFresh(alignment, new_length, element_width); - } - - /// reallocate by explicitly making a new allocation and copying elements over - fn reallocateFresh( - self: RocList, - alignment: u32, - new_length: usize, - element_width: usize, - ) RocList { - const old_length = self.length; - const delta_length = new_length - old_length; - - const data_bytes = new_length * element_width; - const first_slot = utils.allocateWithRefcount(data_bytes, alignment); - - // transfer the memory - - if (self.bytes) |source_ptr| { - const dest_ptr = first_slot; - - @memcpy(dest_ptr, source_ptr, old_length * element_width); - @memset(dest_ptr + old_length * element_width, 0, delta_length * element_width); - } - - const result = RocList{ - .bytes = first_slot, - .length = new_length, - .capacity = new_length, - }; - - utils.decref(self.bytes, old_length * element_width, alignment); - - return result; - } -}; - -const Caller0 = fn (?[*]u8, ?[*]u8) callconv(.C) void; -const Caller1 = fn (?[*]u8, ?[*]u8, ?[*]u8) callconv(.C) void; -const Caller2 = fn (?[*]u8, ?[*]u8, ?[*]u8, ?[*]u8) callconv(.C) void; -const Caller3 = fn (?[*]u8, ?[*]u8, ?[*]u8, ?[*]u8, ?[*]u8) callconv(.C) void; -const Caller4 = fn (?[*]u8, ?[*]u8, ?[*]u8, ?[*]u8, ?[*]u8, ?[*]u8) callconv(.C) void; - -pub fn listReverse(list: RocList, alignment: u32, element_width: usize, update_mode: UpdateMode) callconv(.C) RocList { - if (list.bytes) |source_ptr| { - const size = list.len(); - - var i: usize = 0; - const end: usize = size - 1; - - if (update_mode == .InPlace or list.isUnique()) { - - // Working from the front and back so - // we only need to go ~(n / 2) iterations. - // If the length is an odd number the middle - // element stays in the same place anyways. - while (i < (end - i)) : (i += 1) { - swapElements(source_ptr, element_width, i, end - i); - } - - return list; - } else { - const output = RocList.allocate(alignment, size, element_width); - - const target_ptr = output.bytes orelse unreachable; - - while (i < size) : (i += 1) { - const last_position = end - i; - - @memcpy(target_ptr + (i * element_width), source_ptr + (last_position * element_width), element_width); - } - - utils.decref(list.bytes, size * element_width, alignment); - - return output; - } - } else { - return RocList.empty(); - } -} - -pub fn listMap( - list: RocList, - caller: Caller1, - data: Opaque, - inc_n_data: IncN, - data_is_owned: bool, - alignment: u32, - old_element_width: usize, - new_element_width: usize, -) callconv(.C) RocList { - if (list.bytes) |source_ptr| { - const size = list.len(); - var i: usize = 0; - const output = RocList.allocate(alignment, size, new_element_width); - const target_ptr = output.bytes orelse unreachable; - - if (data_is_owned) { - inc_n_data(data, size); - } - - while (i < size) : (i += 1) { - caller(data, source_ptr + (i * old_element_width), target_ptr + (i * new_element_width)); - } - - return output; - } else { - return RocList.empty(); - } -} - -// List.mapWithIndex : List before, (before, Nat -> after) -> List after -pub fn listMapWithIndex( - list: RocList, - caller: Caller2, - data: Opaque, - inc_n_data: IncN, - data_is_owned: bool, - alignment: u32, - old_element_width: usize, - new_element_width: usize, -) callconv(.C) RocList { - if (list.bytes) |source_ptr| { - const size = list.len(); - var i: usize = 0; - const output = RocList.allocate(alignment, size, new_element_width); - const target_ptr = output.bytes orelse unreachable; - - if (data_is_owned) { - inc_n_data(data, size); - } - - while (i < size) : (i += 1) { - // before, Nat -> after - caller(data, source_ptr + (i * old_element_width), @ptrCast(?[*]u8, &i), target_ptr + (i * new_element_width)); - } - - return output; - } else { - return RocList.empty(); - } -} - -fn decrementTail(list: RocList, start_index: usize, element_width: usize, dec: Dec) void { - if (list.bytes) |source| { - var i = start_index; - while (i < list.len()) : (i += 1) { - const element = source + i * element_width; - dec(element); - } - } -} - -pub fn listMap2( - list1: RocList, - list2: RocList, - caller: Caller2, - data: Opaque, - inc_n_data: IncN, - data_is_owned: bool, - alignment: u32, - a_width: usize, - b_width: usize, - c_width: usize, - dec_a: Dec, - dec_b: Dec, -) callconv(.C) RocList { - const output_length = std.math.min(list1.len(), list2.len()); - - // if the lists don't have equal length, we must consume the remaining elements - // In this case we consume by (recursively) decrementing the elements - decrementTail(list1, output_length, a_width, dec_a); - decrementTail(list2, output_length, b_width, dec_b); - - if (data_is_owned) { - inc_n_data(data, output_length); - } - - if (list1.bytes) |source_a| { - if (list2.bytes) |source_b| { - const output = RocList.allocate(alignment, output_length, c_width); - const target_ptr = output.bytes orelse unreachable; - - var i: usize = 0; - while (i < output_length) : (i += 1) { - const element_a = source_a + i * a_width; - const element_b = source_b + i * b_width; - const target = target_ptr + i * c_width; - caller(data, element_a, element_b, target); - } - - return output; - } else { - return RocList.empty(); - } - } else { - return RocList.empty(); - } -} - -pub fn listMap3( - list1: RocList, - list2: RocList, - list3: RocList, - caller: Caller3, - data: Opaque, - inc_n_data: IncN, - data_is_owned: bool, - alignment: u32, - a_width: usize, - b_width: usize, - c_width: usize, - d_width: usize, - dec_a: Dec, - dec_b: Dec, - dec_c: Dec, -) callconv(.C) RocList { - const smaller_length = std.math.min(list1.len(), list2.len()); - const output_length = std.math.min(smaller_length, list3.len()); - - decrementTail(list1, output_length, a_width, dec_a); - decrementTail(list2, output_length, b_width, dec_b); - decrementTail(list3, output_length, c_width, dec_c); - - if (data_is_owned) { - inc_n_data(data, output_length); - } - - if (list1.bytes) |source_a| { - if (list2.bytes) |source_b| { - if (list3.bytes) |source_c| { - const output = RocList.allocate(alignment, output_length, d_width); - const target_ptr = output.bytes orelse unreachable; - - var i: usize = 0; - while (i < output_length) : (i += 1) { - const element_a = source_a + i * a_width; - const element_b = source_b + i * b_width; - const element_c = source_c + i * c_width; - const target = target_ptr + i * d_width; - - caller(data, element_a, element_b, element_c, target); - } - - return output; - } else { - return RocList.empty(); - } - } else { - return RocList.empty(); - } - } else { - return RocList.empty(); - } -} - -pub fn listMap4( - list1: RocList, - list2: RocList, - list3: RocList, - list4: RocList, - caller: Caller4, - data: Opaque, - inc_n_data: IncN, - data_is_owned: bool, - alignment: u32, - a_width: usize, - b_width: usize, - c_width: usize, - d_width: usize, - e_width: usize, - dec_a: Dec, - dec_b: Dec, - dec_c: Dec, - dec_d: Dec, -) callconv(.C) RocList { - const output_length = std.math.min(std.math.min(list1.len(), list2.len()), std.math.min(list3.len(), list4.len())); - - decrementTail(list1, output_length, a_width, dec_a); - decrementTail(list2, output_length, b_width, dec_b); - decrementTail(list3, output_length, c_width, dec_c); - decrementTail(list4, output_length, d_width, dec_d); - - if (data_is_owned) { - inc_n_data(data, output_length); - } - - if (list1.bytes) |source_a| { - if (list2.bytes) |source_b| { - if (list3.bytes) |source_c| { - if (list4.bytes) |source_d| { - const output = RocList.allocate(alignment, output_length, e_width); - const target_ptr = output.bytes orelse unreachable; - - var i: usize = 0; - while (i < output_length) : (i += 1) { - const element_a = source_a + i * a_width; - const element_b = source_b + i * b_width; - const element_c = source_c + i * c_width; - const element_d = source_d + i * d_width; - const target = target_ptr + i * e_width; - - caller(data, element_a, element_b, element_c, element_d, target); - } - - return output; - } else { - return RocList.empty(); - } - } else { - return RocList.empty(); - } - } else { - return RocList.empty(); - } - } else { - return RocList.empty(); - } -} - -pub fn listKeepIf( - list: RocList, - caller: Caller1, - data: Opaque, - inc_n_data: IncN, - data_is_owned: bool, - alignment: u32, - element_width: usize, - inc: Inc, - dec: Dec, -) callconv(.C) RocList { - if (list.bytes) |source_ptr| { - const size = list.len(); - var i: usize = 0; - var output = RocList.allocate(alignment, list.len(), list.len() * element_width); - const target_ptr = output.bytes orelse unreachable; - - if (data_is_owned) { - inc_n_data(data, size); - } - - var kept: usize = 0; - while (i < size) : (i += 1) { - var keep = false; - const element = source_ptr + (i * element_width); - inc(element); - caller(data, element, @ptrCast(?[*]u8, &keep)); - - if (keep) { - @memcpy(target_ptr + (kept * element_width), element, element_width); - - kept += 1; - } else { - dec(element); - } - } - - if (kept == 0) { - // if the output is empty, deallocate the space we made for the result - utils.decref(output.bytes, size * element_width, alignment); - return RocList.empty(); - } else { - output.length = kept; - - return output; - } - } else { - return RocList.empty(); - } -} - -pub fn listKeepOks( - list: RocList, - caller: Caller1, - data: Opaque, - inc_n_data: IncN, - data_is_owned: bool, - alignment: u32, - before_width: usize, - result_width: usize, - after_width: usize, - has_tag_id: HasTagId, - dec_result: Dec, -) callconv(.C) RocList { - const good_constructor: u16 = 1; - - return listKeepResult( - list, - good_constructor, - caller, - data, - inc_n_data, - data_is_owned, - alignment, - before_width, - result_width, - after_width, - has_tag_id, - dec_result, - ); -} - -pub fn listKeepErrs( - list: RocList, - caller: Caller1, - data: Opaque, - inc_n_data: IncN, - data_is_owned: bool, - alignment: u32, - before_width: usize, - result_width: usize, - after_width: usize, - has_tag_id: HasTagId, - dec_result: Dec, -) callconv(.C) RocList { - const good_constructor: u16 = 0; - - return listKeepResult( - list, - good_constructor, - caller, - data, - inc_n_data, - data_is_owned, - alignment, - before_width, - result_width, - after_width, - has_tag_id, - dec_result, - ); -} - -pub fn listKeepResult( - list: RocList, - good_constructor: u16, - caller: Caller1, - data: Opaque, - inc_n_data: IncN, - data_is_owned: bool, - alignment: u32, - before_width: usize, - result_width: usize, - after_width: usize, - has_tag_id: HasTagId, - dec_result: Dec, -) RocList { - if (list.bytes) |source_ptr| { - const size = list.len(); - var i: usize = 0; - var output = RocList.allocate(alignment, list.len(), list.len() * after_width); - const target_ptr = output.bytes orelse unreachable; - - // TODO handle alloc failing! - var temporary = utils.alloc(result_width, alignment) orelse unreachable; - - if (data_is_owned) { - inc_n_data(data, size); - } - - var kept: usize = 0; - while (i < size) : (i += 1) { - const before_element = source_ptr + (i * before_width); - caller(data, before_element, temporary); - - // a record { matched: bool, data: ?[*]u8 } - // for now, that data pointer is just the input `temporary` pointer - // this will change in the future to only return a pointer to the - // payload of the tag - const answer = has_tag_id(good_constructor, temporary); - if (answer.matched) { - const contents = (answer.data orelse unreachable); - @memcpy(target_ptr + (kept * after_width), contents, after_width); - kept += 1; - } else { - dec_result(temporary); - } - } - - utils.dealloc(temporary, alignment); - - if (kept == 0) { - utils.decref(output.bytes, size * after_width, alignment); - return RocList.empty(); - } else { - output.length = kept; - return output; - } - } else { - return RocList.empty(); - } -} - -pub fn listWalk( - list: RocList, - caller: Caller2, - data: Opaque, - inc_n_data: IncN, - data_is_owned: bool, - accum: Opaque, - alignment: u32, - element_width: usize, - accum_width: usize, - output: Opaque, -) callconv(.C) void { - if (accum_width == 0) { - return; - } - - if (list.isEmpty()) { - @memcpy(output orelse unreachable, accum orelse unreachable, accum_width); - return; - } - - if (data_is_owned) { - inc_n_data(data, list.len()); - } - - // TODO handle alloc failing! - const bytes_ptr: [*]u8 = utils.alloc(accum_width, alignment) orelse unreachable; - var b1 = output orelse unreachable; - var b2 = bytes_ptr; - - @memcpy(b2, accum orelse unreachable, accum_width); - - if (list.bytes) |source_ptr| { - var i: usize = 0; - const size = list.len(); - while (i < size) : (i += 1) { - const element = source_ptr + i * element_width; - caller(data, b2, element, b1); - - std.mem.swap([*]u8, &b1, &b2); - } - } - - @memcpy(output orelse unreachable, b2, accum_width); - utils.dealloc(bytes_ptr, alignment); -} - -pub fn listWalkBackwards( - list: RocList, - caller: Caller2, - data: Opaque, - inc_n_data: IncN, - data_is_owned: bool, - accum: Opaque, - alignment: u32, - element_width: usize, - accum_width: usize, - output: Opaque, -) callconv(.C) void { - if (accum_width == 0) { - return; - } - - if (list.isEmpty()) { - @memcpy(output orelse unreachable, accum orelse unreachable, accum_width); - return; - } - - if (data_is_owned) { - inc_n_data(data, list.len()); - } - - // TODO handle alloc failing! - const bytes_ptr: [*]u8 = utils.alloc(accum_width, alignment) orelse unreachable; - var b1 = output orelse unreachable; - var b2 = bytes_ptr; - - @memcpy(b2, accum orelse unreachable, accum_width); - - if (list.bytes) |source_ptr| { - const size = list.len(); - var i: usize = size; - while (i > 0) { - i -= 1; - const element = source_ptr + i * element_width; - caller(data, b2, element, b1); - - std.mem.swap([*]u8, &b1, &b2); - } - } - - @memcpy(output orelse unreachable, b2, accum_width); - utils.dealloc(bytes_ptr, alignment); -} - -pub fn listWalkUntil( - list: RocList, - caller: Caller2, - data: Opaque, - inc_n_data: IncN, - data_is_owned: bool, - accum: Opaque, - alignment: u32, - element_width: usize, - continue_stop_width: usize, - accum_width: usize, - has_tag_id: HasTagId, - dec: Dec, - output: Opaque, -) callconv(.C) void { - // [Continue a, Stop a] - - if (accum_width == 0) { - return; - } - - if (list.isEmpty()) { - @memcpy(output orelse unreachable, accum orelse unreachable, accum_width); - return; - } - - // TODO handle alloc failing! - const bytes_ptr: [*]u8 = utils.alloc(continue_stop_width, alignment) orelse unreachable; - - // NOTE: assumes data bytes are the first bytes in a tag - @memcpy(bytes_ptr, accum orelse unreachable, accum_width); - - if (list.bytes) |source_ptr| { - var i: usize = 0; - const size = list.len(); - while (i < size) : (i += 1) { - const element = source_ptr + i * element_width; - - if (data_is_owned) { - inc_n_data(data, 1); - } - - caller(data, bytes_ptr, element, bytes_ptr); - - // [Continue ..., Stop] - const tag_id = has_tag_id(0, bytes_ptr); - - if (!tag_id.matched) { - // decrement refcount of the remaining items - i += 1; - while (i < size) : (i += 1) { - dec(source_ptr + i * element_width); - } - break; - } - } - } - - @memcpy(output orelse unreachable, bytes_ptr, accum_width); - utils.dealloc(bytes_ptr, alignment); -} - -// List.contains : List k, k -> Bool -pub fn listContains(list: RocList, key: Opaque, key_width: usize, is_eq: EqFn) callconv(.C) bool { - if (list.bytes) |source_ptr| { - const size = list.len(); - var i: usize = 0; - while (i < size) : (i += 1) { - const element = source_ptr + i * key_width; - if (is_eq(element, key)) { - return true; - } - } - } - - return false; -} - -pub fn listRepeat(count: usize, alignment: u32, element: Opaque, element_width: usize, inc_n_element: IncN) callconv(.C) RocList { - if (count == 0) { - return RocList.empty(); - } - - var output = RocList.allocate(alignment, count, element_width); - - if (output.bytes) |target_ptr| { - // increment the element's RC N times - inc_n_element(element, count); - - var i: usize = 0; - const source = element orelse unreachable; - while (i < count) : (i += 1) { - @memcpy(target_ptr + i * element_width, source, element_width); - } - - return output; - } else { - unreachable; - } -} - -pub fn listSingle(alignment: u32, element: Opaque, element_width: usize) callconv(.C) RocList { - var output = RocList.allocate(alignment, 1, element_width); - - if (output.bytes) |target| { - if (element) |source| { - @memcpy(target, source, element_width); - } - } - - return output; -} - -pub fn listAppend(list: RocList, alignment: u32, element: Opaque, element_width: usize, update_mode: UpdateMode) callconv(.C) RocList { - const old_length = list.len(); - var output = list.reallocate(alignment, old_length + 1, element_width); - - // we'd need capacity to use update_mode here - _ = update_mode; - - if (output.bytes) |target| { - if (element) |source| { - @memcpy(target + old_length * element_width, source, element_width); - } - } - - return output; -} - -pub fn listPrepend(list: RocList, alignment: u32, element: Opaque, element_width: usize) callconv(.C) RocList { - const old_length = list.len(); - var output = list.reallocate(alignment, old_length + 1, element_width); - - // can't use one memcpy here because source and target overlap - if (output.bytes) |target| { - var i: usize = old_length; - - while (i > 0) { - i -= 1; - - // move the ith element to the (i + 1)th position - @memcpy(target + (i + 1) * element_width, target + i * element_width, element_width); - } - - // finally copy in the new first element - if (element) |source| { - @memcpy(target, source, element_width); - } - } - - return output; -} - -pub fn listSwap( - list: RocList, - alignment: u32, - element_width: usize, - index_1: usize, - index_2: usize, - update_mode: UpdateMode, -) callconv(.C) RocList { - const size = list.len(); - if (index_1 == index_2 or index_1 >= size or index_2 >= size) { - // Either index out of bounds so we just return - return list; - } - - const newList = blk: { - if (update_mode == .InPlace) { - break :blk list; - } else { - break :blk list.makeUnique(alignment, element_width); - } - }; - - const source_ptr = @ptrCast([*]u8, newList.bytes); - swapElements(source_ptr, element_width, index_1, index_2); - - return newList; -} - -pub fn listSublist( - list: RocList, - alignment: u32, - element_width: usize, - start: usize, - len: usize, - dec: Dec, -) callconv(.C) RocList { - if (len == 0) { - return RocList.empty(); - } - if (list.bytes) |source_ptr| { - const size = list.len(); - - if (start >= size) { - return RocList.empty(); - } - - const keep_len = std.math.min(len, size - start); - const drop_start_len = start; - const drop_end_len = size - (start + keep_len); - - // Decrement the reference counts of elements before `start`. - var i: usize = 0; - while (i < drop_start_len) : (i += 1) { - const element = source_ptr + i * element_width; - dec(element); - } - - // Decrement the reference counts of elements after `start + keep_len`. - i = 0; - while (i < drop_end_len) : (i += 1) { - const element = source_ptr + (start + keep_len + i) * element_width; - dec(element); - } - - const output = RocList.allocate(alignment, keep_len, element_width); - const target_ptr = output.bytes orelse unreachable; - - @memcpy(target_ptr, source_ptr + start * element_width, keep_len * element_width); - - utils.decref(list.bytes, size * element_width, alignment); - - return output; - } - - return RocList.empty(); -} - -pub fn listDropAt( - list: RocList, - alignment: u32, - element_width: usize, - drop_index: usize, - dec: Dec, -) callconv(.C) RocList { - if (list.bytes) |source_ptr| { - const size = list.len(); - - if (drop_index >= size) { - return list; - } - - if (drop_index < size) { - const element = source_ptr + drop_index * element_width; - dec(element); - } - - // NOTE - // we need to return an empty list explicitly, - // because we rely on the pointer field being null if the list is empty - // which also requires duplicating the utils.decref call to spend the RC token - if (size < 2) { - utils.decref(list.bytes, size * element_width, alignment); - return RocList.empty(); - } - - if (list.isUnique()) { - var i = drop_index; - while (i < size) : (i += 1) { - const copy_target = source_ptr + i * element_width; - const copy_source = copy_target + element_width; - @memcpy(copy_target, copy_source, element_width); - } - - var new_list = list; - - new_list.length -= 1; - return new_list; - } - - const output = RocList.allocate(alignment, size - 1, element_width); - const target_ptr = output.bytes orelse unreachable; - - const head_size = drop_index * element_width; - @memcpy(target_ptr, source_ptr, head_size); - - const tail_target = target_ptr + drop_index * element_width; - const tail_source = source_ptr + (drop_index + 1) * element_width; - const tail_size = (size - drop_index - 1) * element_width; - @memcpy(tail_target, tail_source, tail_size); - - utils.decref(list.bytes, size * element_width, alignment); - - return output; - } else { - return RocList.empty(); - } -} - -pub fn listRange(width: utils.IntWidth, low: Opaque, high: Opaque) callconv(.C) RocList { - return switch (width) { - .U8 => helper1(u8, low, high), - .U16 => helper1(u16, low, high), - .U32 => helper1(u32, low, high), - .U64 => helper1(u64, low, high), - .U128 => helper1(u128, low, high), - .I8 => helper1(i8, low, high), - .I16 => helper1(i16, low, high), - .I32 => helper1(i32, low, high), - .I64 => helper1(i64, low, high), - .I128 => helper1(i128, low, high), - }; -} - -fn helper1(comptime T: type, low: Opaque, high: Opaque) RocList { - const ptr1 = @ptrCast(*T, @alignCast(@alignOf(T), low)); - const ptr2 = @ptrCast(*T, @alignCast(@alignOf(T), high)); - - return listRangeHelp(T, ptr1.*, ptr2.*); -} - -fn listRangeHelp(comptime T: type, low: T, high: T) RocList { - const Order = std.math.Order; - - switch (std.math.order(low, high)) { - Order.gt => { - return RocList.empty(); - }, - - Order.eq => { - const list = RocList.allocate(@alignOf(usize), 1, @sizeOf(T)); - const buffer = @ptrCast([*]T, @alignCast(@alignOf(T), list.bytes orelse unreachable)); - - buffer[0] = low; - - return list; - }, - - Order.lt => { - const length: usize = @intCast(usize, high - low); - const list = RocList.allocate(@alignOf(usize), length, @sizeOf(T)); - - const buffer = @ptrCast([*]T, @alignCast(@alignOf(T), list.bytes orelse unreachable)); - - var i: usize = 0; - var current = low; - - while (i < length) { - buffer[i] = current; - - i += 1; - current += 1; - } - - return list; - }, - } -} - -fn partition(source_ptr: [*]u8, transform: Opaque, wrapper: CompareFn, element_width: usize, low: isize, high: isize) isize { - const pivot = source_ptr + (@intCast(usize, high) * element_width); - var i = (low - 1); // Index of smaller element and indicates the right position of pivot found so far - var j = low; - - while (j <= high - 1) : (j += 1) { - const current_elem = source_ptr + (@intCast(usize, j) * element_width); - - const ordering = wrapper(transform, current_elem, pivot); - const order = @intToEnum(utils.Ordering, ordering); - - switch (order) { - utils.Ordering.LT => { - // the current element is smaller than the pivot; swap it - i += 1; - swapElements(source_ptr, element_width, @intCast(usize, i), @intCast(usize, j)); - }, - utils.Ordering.EQ, utils.Ordering.GT => {}, - } - } - swapElements(source_ptr, element_width, @intCast(usize, i + 1), @intCast(usize, high)); - return (i + 1); -} - -fn quicksort(source_ptr: [*]u8, transform: Opaque, wrapper: CompareFn, element_width: usize, low: isize, high: isize) void { - if (low < high) { - // partition index - const pi = partition(source_ptr, transform, wrapper, element_width, low, high); - - _ = quicksort(source_ptr, transform, wrapper, element_width, low, pi - 1); // before pi - _ = quicksort(source_ptr, transform, wrapper, element_width, pi + 1, high); // after pi - } -} - -pub fn listSortWith( - input: RocList, - caller: CompareFn, - data: Opaque, - inc_n_data: IncN, - data_is_owned: bool, - alignment: u32, - element_width: usize, -) callconv(.C) RocList { - var list = input.makeUnique(alignment, element_width); - - if (data_is_owned) { - inc_n_data(data, list.len()); - } - - if (list.bytes) |source_ptr| { - const low = 0; - const high: isize = @intCast(isize, list.len()) - 1; - quicksort(source_ptr, data, caller, element_width, low, high); - } - - return list; -} - -pub fn listAny( - list: RocList, - caller: Caller1, - data: Opaque, - inc_n_data: IncN, - data_is_owned: bool, - element_width: usize, -) callconv(.C) bool { - if (list.bytes) |source_ptr| { - const size = list.len(); - - if (data_is_owned) { - inc_n_data(data, size); - } - - var i: usize = 0; - var satisfied = false; - while (i < size) : (i += 1) { - const element = source_ptr + i * element_width; - caller(data, element, @ptrCast(?[*]u8, &satisfied)); - - if (satisfied) { - return satisfied; - } - } - } - - return false; -} - -pub fn listAll( - list: RocList, - caller: Caller1, - data: Opaque, - inc_n_data: IncN, - data_is_owned: bool, - element_width: usize, -) callconv(.C) bool { - if (list.bytes) |source_ptr| { - const size = list.len(); - - if (data_is_owned) { - inc_n_data(data, size); - } - - var i: usize = 0; - while (i < size) : (i += 1) { - var satisfied = false; - const element = source_ptr + i * element_width; - caller(data, element, @ptrCast(?[*]u8, &satisfied)); - - if (!satisfied) { - return false; - } - } - return true; - } - return true; -} - -// SWAP ELEMENTS - -inline fn swapHelp(width: usize, temporary: [*]u8, ptr1: [*]u8, ptr2: [*]u8) void { - @memcpy(temporary, ptr1, width); - @memcpy(ptr1, ptr2, width); - @memcpy(ptr2, temporary, width); -} - -fn swap(width_initial: usize, p1: [*]u8, p2: [*]u8) void { - const threshold: usize = 64; - - var width = width_initial; - - var ptr1 = p1; - var ptr2 = p2; - - var buffer_actual: [threshold]u8 = undefined; - var buffer: [*]u8 = buffer_actual[0..]; - - while (true) { - if (width < threshold) { - swapHelp(width, buffer, ptr1, ptr2); - return; - } else { - swapHelp(threshold, buffer, ptr1, ptr2); - - ptr1 += threshold; - ptr2 += threshold; - - width -= threshold; - } - } -} - -fn swapElements(source_ptr: [*]u8, element_width: usize, index_1: usize, index_2: usize) void { - var element_at_i = source_ptr + (index_1 * element_width); - var element_at_j = source_ptr + (index_2 * element_width); - - return swap(element_width, element_at_i, element_at_j); -} - -pub fn listJoin(list_of_lists: RocList, alignment: u32, element_width: usize) callconv(.C) RocList { - var total_length: usize = 0; - - const slice_of_lists = @ptrCast([*]RocList, @alignCast(@alignOf(RocList), list_of_lists.bytes)); - - var i: usize = 0; - while (i < list_of_lists.len()) : (i += 1) { - total_length += slice_of_lists[i].len(); - } - - const output = RocList.allocate(alignment, total_length, element_width); - - if (output.bytes) |target| { - var elements_copied: usize = 0; - - i = 0; - while (i < list_of_lists.len()) : (i += 1) { - const list = slice_of_lists[i]; - if (list.bytes) |source| { - @memcpy(target + elements_copied * element_width, source, list.len() * element_width); - elements_copied += list.len(); - } - } - } - - return output; -} - -pub fn listConcat(list_a: RocList, list_b: RocList, alignment: u32, element_width: usize) callconv(.C) RocList { - if (list_a.isEmpty()) { - return list_b; - } else if (list_b.isEmpty()) { - return list_a; - } else if (!list_a.isEmpty() and list_a.isUnique()) { - const total_length: usize = list_a.len() + list_b.len(); - - if (list_a.bytes) |source| { - const new_source = utils.unsafeReallocate( - source, - alignment, - list_a.len(), - total_length, - element_width, - ); - - if (list_b.bytes) |source_b| { - @memcpy(new_source + list_a.len() * element_width, source_b, list_b.len() * element_width); - } - - return RocList{ .bytes = new_source, .length = total_length, .capacity = total_length }; - } - } - const total_length: usize = list_a.len() + list_b.len(); - - const output = RocList.allocate(alignment, total_length, element_width); - - if (output.bytes) |target| { - if (list_a.bytes) |source| { - @memcpy(target, source, list_a.len() * element_width); - } - if (list_b.bytes) |source| { - @memcpy(target + list_a.len() * element_width, source, list_b.len() * element_width); - } - } - - return output; -} - -pub fn listReplaceInPlace( - list: RocList, - index: usize, - element: Opaque, - element_width: usize, - out_element: ?[*]u8, -) callconv(.C) RocList { - // INVARIANT: bounds checking happens on the roc side - // - // at the time of writing, the function is implemented roughly as - // `if inBounds then LowLevelListReplace input index item else input` - // so we don't do a bounds check here. Hence, the list is also non-empty, - // because inserting into an empty list is always out of bounds - return listReplaceInPlaceHelp(list, index, element, element_width, out_element); -} - -pub fn listReplace( - list: RocList, - alignment: u32, - index: usize, - element: Opaque, - element_width: usize, - out_element: ?[*]u8, -) callconv(.C) RocList { - // INVARIANT: bounds checking happens on the roc side - // - // at the time of writing, the function is implemented roughly as - // `if inBounds then LowLevelListReplace input index item else input` - // so we don't do a bounds check here. Hence, the list is also non-empty, - // because inserting into an empty list is always out of bounds - return listReplaceInPlaceHelp(list.makeUnique(alignment, element_width), index, element, element_width, out_element); -} - -inline fn listReplaceInPlaceHelp( - list: RocList, - index: usize, - element: Opaque, - element_width: usize, - out_element: ?[*]u8, -) RocList { - // the element we will replace - var element_at_index = (list.bytes orelse undefined) + (index * element_width); - - // copy out the old element - @memcpy(out_element orelse undefined, element_at_index, element_width); - - // copy in the new element - @memcpy(element_at_index, element orelse undefined, element_width); - - return list; -} - -pub fn listFindUnsafe( - list: RocList, - caller: Caller1, - data: Opaque, - inc_n_data: IncN, - data_is_owned: bool, - element_width: usize, - inc: Inc, - dec: Dec, -) callconv(.C) extern struct { value: Opaque, found: bool } { - if (list.bytes) |source_ptr| { - const size = list.len(); - if (data_is_owned) { - inc_n_data(data, size); - } - - var i: usize = 0; - while (i < size) : (i += 1) { - var theOne = false; - const element = source_ptr + (i * element_width); - inc(element); - caller(data, element, @ptrCast(?[*]u8, &theOne)); - - if (theOne) { - return .{ .value = element, .found = true }; - } else { - dec(element); - } - } - return .{ .value = null, .found = false }; - } else { - return .{ .value = null, .found = false }; - } -} - -pub fn listIsUnique( - list: RocList, -) callconv(.C) bool { - return list.isEmpty() or list.isUnique(); -} diff --git a/compiler/builtins/bitcode/src/main.zig b/compiler/builtins/bitcode/src/main.zig deleted file mode 100644 index c772eafa1a..0000000000 --- a/compiler/builtins/bitcode/src/main.zig +++ /dev/null @@ -1,296 +0,0 @@ -const std = @import("std"); -const builtin = @import("builtin"); -const math = std.math; -const utils = @import("utils.zig"); -const expect = @import("expect.zig"); - -const ROC_BUILTINS = "roc_builtins"; -const NUM = "num"; -const STR = "str"; - -// Dec Module -const dec = @import("dec.zig"); - -comptime { - exportDecFn(dec.fromStr, "from_str"); - exportDecFn(dec.fromF64C, "from_f64"); - exportDecFn(dec.eqC, "eq"); - exportDecFn(dec.neqC, "neq"); - exportDecFn(dec.negateC, "negate"); - exportDecFn(dec.addC, "add_with_overflow"); - exportDecFn(dec.subC, "sub_with_overflow"); - exportDecFn(dec.mulC, "mul_with_overflow"); - exportDecFn(dec.divC, "div"); -} - -// List Module -const list = @import("list.zig"); - -comptime { - exportListFn(list.listMap, "map"); - exportListFn(list.listMap2, "map2"); - exportListFn(list.listMap3, "map3"); - exportListFn(list.listMap4, "map4"); - exportListFn(list.listMapWithIndex, "map_with_index"); - exportListFn(list.listKeepIf, "keep_if"); - exportListFn(list.listWalk, "walk"); - exportListFn(list.listWalkUntil, "walkUntil"); - exportListFn(list.listWalkBackwards, "walk_backwards"); - exportListFn(list.listKeepOks, "keep_oks"); - exportListFn(list.listKeepErrs, "keep_errs"); - exportListFn(list.listContains, "contains"); - exportListFn(list.listRepeat, "repeat"); - exportListFn(list.listAppend, "append"); - exportListFn(list.listPrepend, "prepend"); - exportListFn(list.listSingle, "single"); - exportListFn(list.listJoin, "join"); - exportListFn(list.listRange, "range"); - exportListFn(list.listReverse, "reverse"); - exportListFn(list.listSortWith, "sort_with"); - exportListFn(list.listConcat, "concat"); - exportListFn(list.listSublist, "sublist"); - exportListFn(list.listDropAt, "drop_at"); - exportListFn(list.listReplace, "replace"); - exportListFn(list.listReplaceInPlace, "replace_in_place"); - exportListFn(list.listSwap, "swap"); - exportListFn(list.listAny, "any"); - exportListFn(list.listAll, "all"); - exportListFn(list.listFindUnsafe, "find_unsafe"); - exportListFn(list.listIsUnique, "is_unique"); -} - -// Dict Module -const dict = @import("dict.zig"); -const hash = @import("hash.zig"); - -comptime { - exportDictFn(dict.dictLen, "len"); - exportDictFn(dict.dictEmpty, "empty"); - exportDictFn(dict.dictInsert, "insert"); - exportDictFn(dict.dictRemove, "remove"); - exportDictFn(dict.dictContains, "contains"); - exportDictFn(dict.dictGet, "get"); - exportDictFn(dict.elementsRc, "elementsRc"); - exportDictFn(dict.dictKeys, "keys"); - exportDictFn(dict.dictValues, "values"); - exportDictFn(dict.dictUnion, "union"); - exportDictFn(dict.dictIntersection, "intersection"); - exportDictFn(dict.dictDifference, "difference"); - exportDictFn(dict.dictWalk, "walk"); - - exportDictFn(dict.setFromList, "set_from_list"); - - exportDictFn(hash.wyhash, "hash"); - exportDictFn(hash.wyhash_rocstr, "hash_str"); -} - -// Num Module -const num = @import("num.zig"); - -const INTEGERS = [_]type{ i8, i16, i32, i64, i128, u8, u16, u32, u64, u128 }; -const FLOATS = [_]type{ f32, f64 }; -const NUMBERS = INTEGERS ++ FLOATS; - -comptime { - exportNumFn(num.bytesToU16C, "bytes_to_u16"); - exportNumFn(num.bytesToU32C, "bytes_to_u32"); - - inline for (INTEGERS) |T| { - num.exportPow(T, ROC_BUILTINS ++ "." ++ NUM ++ ".pow_int."); - num.exportDivCeil(T, ROC_BUILTINS ++ "." ++ NUM ++ ".div_ceil."); - } - - inline for (INTEGERS) |FROM| { - inline for (INTEGERS) |TO| { - // We're exporting more than we need here, but that's okay. - num.exportToIntCheckingMax(FROM, TO, ROC_BUILTINS ++ "." ++ NUM ++ ".int_to_" ++ @typeName(TO) ++ "_checking_max."); - num.exportToIntCheckingMaxAndMin(FROM, TO, ROC_BUILTINS ++ "." ++ NUM ++ ".int_to_" ++ @typeName(TO) ++ "_checking_max_and_min."); - } - - num.exportRoundF32(FROM, ROC_BUILTINS ++ "." ++ NUM ++ ".round_f32."); - num.exportRoundF64(FROM, ROC_BUILTINS ++ "." ++ NUM ++ ".round_f64."); - } - - inline for (FLOATS) |T| { - num.exportAsin(T, ROC_BUILTINS ++ "." ++ NUM ++ ".asin."); - num.exportAcos(T, ROC_BUILTINS ++ "." ++ NUM ++ ".acos."); - num.exportAtan(T, ROC_BUILTINS ++ "." ++ NUM ++ ".atan."); - - num.exportIsFinite(T, ROC_BUILTINS ++ "." ++ NUM ++ ".is_finite."); - } -} - -// Str Module -const str = @import("str.zig"); -comptime { - exportStrFn(str.init, "init"); - exportStrFn(str.strSplitInPlaceC, "str_split_in_place"); - exportStrFn(str.countSegments, "count_segments"); - exportStrFn(str.countGraphemeClusters, "count_grapheme_clusters"); - exportStrFn(str.startsWith, "starts_with"); - exportStrFn(str.startsWithCodePt, "starts_with_code_point"); - exportStrFn(str.endsWith, "ends_with"); - exportStrFn(str.strConcatC, "concat"); - exportStrFn(str.strJoinWithC, "joinWith"); - exportStrFn(str.strNumberOfBytes, "number_of_bytes"); - exportStrFn(str.strFromFloatC, "from_float"); - exportStrFn(str.strEqual, "equal"); - exportStrFn(str.strToUtf8C, "to_utf8"); - exportStrFn(str.fromUtf8C, "from_utf8"); - exportStrFn(str.fromUtf8RangeC, "from_utf8_range"); - exportStrFn(str.repeat, "repeat"); - exportStrFn(str.strTrim, "trim"); - exportStrFn(str.strTrimLeft, "trim_left"); - exportStrFn(str.strTrimRight, "trim_right"); - - inline for (INTEGERS) |T| { - str.exportFromInt(T, ROC_BUILTINS ++ "." ++ STR ++ ".from_int."); - num.exportParseInt(T, ROC_BUILTINS ++ "." ++ STR ++ ".to_int."); - } - - inline for (FLOATS) |T| { - num.exportParseFloat(T, ROC_BUILTINS ++ "." ++ STR ++ ".to_float."); - } -} - -// Utils -comptime { - exportUtilsFn(utils.test_panic, "test_panic"); - exportUtilsFn(utils.increfC, "incref"); - exportUtilsFn(utils.decrefC, "decref"); - exportUtilsFn(utils.decrefCheckNullC, "decref_check_null"); - exportUtilsFn(utils.allocateWithRefcountC, "allocate_with_refcount"); - exportExpectFn(expect.expectFailedC, "expect_failed"); - exportExpectFn(expect.getExpectFailuresC, "get_expect_failures"); - exportExpectFn(expect.deinitFailuresC, "deinit_failures"); - - @export(utils.panic, .{ .name = "roc_builtins.utils." ++ "panic", .linkage = .Weak }); - - if (builtin.target.cpu.arch == .aarch64) { - @export(__roc_force_setjmp, .{ .name = "__roc_force_setjmp", .linkage = .Weak }); - @export(__roc_force_longjmp, .{ .name = "__roc_force_longjmp", .linkage = .Weak }); - } -} - -// Utils continued - SJLJ -// For tests (in particular test_gen), roc_panic is implemented in terms of -// setjmp/longjmp. LLVM is unable to generate code for longjmp on AArch64 (https://github.com/rtfeldman/roc/issues/2965), -// so instead we ask Zig to please provide implementations for us, which is does -// (seemingly via musl). -pub extern fn setjmp([*c]c_int) c_int; -pub extern fn longjmp([*c]c_int, c_int) noreturn; -pub extern fn _setjmp([*c]c_int) c_int; -pub extern fn _longjmp([*c]c_int, c_int) noreturn; -pub extern fn sigsetjmp([*c]c_int, c_int) c_int; -pub extern fn siglongjmp([*c]c_int, c_int) noreturn; -pub extern fn longjmperror() void; -// Zig won't expose the externs (and hence link correctly) unless we force them to be used. -fn __roc_force_setjmp(it: [*c]c_int) callconv(.C) c_int { - return setjmp(it); -} -fn __roc_force_longjmp(a0: [*c]c_int, a1: c_int) callconv(.C) noreturn { - longjmp(a0, a1); -} - -// Export helpers - Must be run inside a comptime -fn exportBuiltinFn(comptime func: anytype, comptime func_name: []const u8) void { - @export(func, .{ .name = "roc_builtins." ++ func_name, .linkage = .Strong }); -} -fn exportNumFn(comptime func: anytype, comptime func_name: []const u8) void { - exportBuiltinFn(func, "num." ++ func_name); -} -fn exportStrFn(comptime func: anytype, comptime func_name: []const u8) void { - exportBuiltinFn(func, "str." ++ func_name); -} -fn exportDictFn(comptime func: anytype, comptime func_name: []const u8) void { - exportBuiltinFn(func, "dict." ++ func_name); -} -fn exportListFn(comptime func: anytype, comptime func_name: []const u8) void { - exportBuiltinFn(func, "list." ++ func_name); -} -fn exportDecFn(comptime func: anytype, comptime func_name: []const u8) void { - exportBuiltinFn(func, "dec." ++ func_name); -} - -fn exportUtilsFn(comptime func: anytype, comptime func_name: []const u8) void { - exportBuiltinFn(func, "utils." ++ func_name); -} - -fn exportExpectFn(comptime func: anytype, comptime func_name: []const u8) void { - exportBuiltinFn(func, "expect." ++ func_name); -} - -// Custom panic function, as builtin Zig version errors during LLVM verification -pub fn panic(message: []const u8, stacktrace: ?*std.builtin.StackTrace) noreturn { - if (builtin.is_test) { - std.debug.print("{s}: {?}", .{ message, stacktrace }); - } else { - _ = message; - _ = stacktrace; - } - - unreachable; -} - -// Run all tests in imported modules -// https://github.com/ziglang/zig/blob/master/lib/std/std.zig#L94 -test "" { - const testing = std.testing; - - testing.refAllDecls(@This()); -} - -// For unclear reasons, sometimes this function is not linked in on some machines. -// Therefore we provide it as LLVM bitcode and mark it as externally linked during our LLVM codegen -// -// Taken from -// https://github.com/ziglang/zig/blob/85755c51d529e7d9b406c6bdf69ce0a0f33f3353/lib/std/special/compiler_rt/muloti4.zig -// -// Thank you Zig Contributors! - -// Export it as weak incase it is already linked in by something else. -comptime { - @export(__muloti4, .{ .name = "__muloti4", .linkage = .Weak }); -} -fn __muloti4(a: i128, b: i128, overflow: *c_int) callconv(.C) i128 { - // @setRuntimeSafety(std.builtin.is_test); - - const min = @bitCast(i128, @as(u128, 1 << (128 - 1))); - const max = ~min; - overflow.* = 0; - - const r = a *% b; - if (a == min) { - if (b != 0 and b != 1) { - overflow.* = 1; - } - return r; - } - if (b == min) { - if (a != 0 and a != 1) { - overflow.* = 1; - } - return r; - } - - const sa = a >> (128 - 1); - const abs_a = (a ^ sa) -% sa; - const sb = b >> (128 - 1); - const abs_b = (b ^ sb) -% sb; - - if (abs_a < 2 or abs_b < 2) { - return r; - } - - if (sa == sb) { - if (abs_a > @divTrunc(max, abs_b)) { - overflow.* = 1; - } - } else { - if (abs_a > @divTrunc(min, -abs_b)) { - overflow.* = 1; - } - } - - return r; -} diff --git a/compiler/builtins/bitcode/src/num.zig b/compiler/builtins/bitcode/src/num.zig deleted file mode 100644 index d25b1fd653..0000000000 --- a/compiler/builtins/bitcode/src/num.zig +++ /dev/null @@ -1,169 +0,0 @@ -const std = @import("std"); -const always_inline = std.builtin.CallOptions.Modifier.always_inline; -const math = std.math; -const RocList = @import("list.zig").RocList; -const RocStr = @import("str.zig").RocStr; - -pub fn NumParseResult(comptime T: type) type { - // on the roc side we sort by alignment; putting the errorcode last - // always works out (no number with smaller alignment than 1) - return extern struct { - value: T, - errorcode: u8, // 0 indicates success - }; -} - -pub fn exportParseInt(comptime T: type, comptime name: []const u8) void { - comptime var f = struct { - fn func(buf: RocStr) callconv(.C) NumParseResult(T) { - // a radix of 0 will make zig determine the radix from the frefix: - // * A prefix of "0b" implies radix=2, - // * A prefix of "0o" implies radix=8, - // * A prefix of "0x" implies radix=16, - // * Otherwise radix=10 is assumed. - const radix = 0; - if (std.fmt.parseInt(T, buf.asSlice(), radix)) |success| { - return .{ .errorcode = 0, .value = success }; - } else |_| { - return .{ .errorcode = 1, .value = 0 }; - } - } - }.func; - @export(f, .{ .name = name ++ @typeName(T), .linkage = .Strong }); -} - -pub fn exportParseFloat(comptime T: type, comptime name: []const u8) void { - comptime var f = struct { - fn func(buf: RocStr) callconv(.C) NumParseResult(T) { - if (std.fmt.parseFloat(T, buf.asSlice())) |success| { - return .{ .errorcode = 0, .value = success }; - } else |_| { - return .{ .errorcode = 1, .value = 0 }; - } - } - }.func; - @export(f, .{ .name = name ++ @typeName(T), .linkage = .Strong }); -} - -pub fn exportPow(comptime T: type, comptime name: []const u8) void { - comptime var f = struct { - fn func(base: T, exp: T) callconv(.C) T { - return std.math.pow(T, base, exp); - } - }.func; - @export(f, .{ .name = name ++ @typeName(T), .linkage = .Strong }); -} - -pub fn exportIsFinite(comptime T: type, comptime name: []const u8) void { - comptime var f = struct { - fn func(input: T) callconv(.C) bool { - return std.math.isFinite(input); - } - }.func; - @export(f, .{ .name = name ++ @typeName(T), .linkage = .Strong }); -} - -pub fn exportAsin(comptime T: type, comptime name: []const u8) void { - comptime var f = struct { - fn func(input: T) callconv(.C) T { - return std.math.asin(input); - } - }.func; - @export(f, .{ .name = name ++ @typeName(T), .linkage = .Strong }); -} - -pub fn exportAcos(comptime T: type, comptime name: []const u8) void { - comptime var f = struct { - fn func(input: T) callconv(.C) T { - return std.math.acos(input); - } - }.func; - @export(f, .{ .name = name ++ @typeName(T), .linkage = .Strong }); -} - -pub fn exportAtan(comptime T: type, comptime name: []const u8) void { - comptime var f = struct { - fn func(input: T) callconv(.C) T { - return std.math.atan(input); - } - }.func; - @export(f, .{ .name = name ++ @typeName(T), .linkage = .Strong }); -} - -pub fn exportRoundF32(comptime T: type, comptime name: []const u8) void { - comptime var f = struct { - fn func(input: f32) callconv(.C) T { - return @floatToInt(T, (@round(input))); - } - }.func; - @export(f, .{ .name = name ++ @typeName(T), .linkage = .Strong }); -} - -pub fn exportRoundF64(comptime T: type, comptime name: []const u8) void { - comptime var f = struct { - fn func(input: f64) callconv(.C) T { - return @floatToInt(T, (@round(input))); - } - }.func; - @export(f, .{ .name = name ++ @typeName(T), .linkage = .Strong }); -} - -pub fn exportDivCeil(comptime T: type, comptime name: []const u8) void { - comptime var f = struct { - fn func(a: T, b: T) callconv(.C) T { - return math.divCeil(T, a, b) catch @panic("TODO runtime exception for dividing by 0!"); - } - }.func; - @export(f, .{ .name = name ++ @typeName(T), .linkage = .Strong }); -} - -pub fn ToIntCheckedResult(comptime T: type) type { - // On the Roc side we sort by alignment; putting the errorcode last - // always works out (no number with smaller alignment than 1). - return extern struct { - value: T, - out_of_bounds: bool, - }; -} - -pub fn exportToIntCheckingMax(comptime From: type, comptime To: type, comptime name: []const u8) void { - comptime var f = struct { - fn func(input: From) callconv(.C) ToIntCheckedResult(To) { - if (input > std.math.maxInt(To)) { - return .{ .out_of_bounds = true, .value = 0 }; - } - return .{ .out_of_bounds = false, .value = @intCast(To, input) }; - } - }.func; - @export(f, .{ .name = name ++ @typeName(From), .linkage = .Strong }); -} - -pub fn exportToIntCheckingMaxAndMin(comptime From: type, comptime To: type, comptime name: []const u8) void { - comptime var f = struct { - fn func(input: From) callconv(.C) ToIntCheckedResult(To) { - if (input > std.math.maxInt(To) or input < std.math.minInt(To)) { - return .{ .out_of_bounds = true, .value = 0 }; - } - return .{ .out_of_bounds = false, .value = @intCast(To, input) }; - } - }.func; - @export(f, .{ .name = name ++ @typeName(From), .linkage = .Strong }); -} - -pub fn bytesToU16C(arg: RocList, position: usize) callconv(.C) u16 { - return @call(.{ .modifier = always_inline }, bytesToU16, .{ arg, position }); -} - -fn bytesToU16(arg: RocList, position: usize) u16 { - const bytes = @ptrCast([*]const u8, arg.bytes); - return @bitCast(u16, [_]u8{ bytes[position], bytes[position + 1] }); -} - -pub fn bytesToU32C(arg: RocList, position: usize) callconv(.C) u32 { - return @call(.{ .modifier = always_inline }, bytesToU32, .{ arg, position }); -} - -fn bytesToU32(arg: RocList, position: usize) u32 { - const bytes = @ptrCast([*]const u8, arg.bytes); - return @bitCast(u32, [_]u8{ bytes[position], bytes[position + 1], bytes[position + 2], bytes[position + 3] }); -} diff --git a/compiler/builtins/bitcode/src/str.zig b/compiler/builtins/bitcode/src/str.zig deleted file mode 100644 index 9da85730f6..0000000000 --- a/compiler/builtins/bitcode/src/str.zig +++ /dev/null @@ -1,2047 +0,0 @@ -const utils = @import("utils.zig"); -const RocList = @import("list.zig").RocList; -const UpdateMode = utils.UpdateMode; -const std = @import("std"); -const mem = std.mem; -const always_inline = std.builtin.CallOptions.Modifier.always_inline; -const unicode = std.unicode; -const testing = std.testing; -const expectEqual = testing.expectEqual; -const expectError = testing.expectError; -const expect = testing.expect; - -const InPlace = enum(u8) { - InPlace, - Clone, -}; - -const MASK_ISIZE: isize = std.math.minInt(isize); -const MASK: usize = @bitCast(usize, MASK_ISIZE); - -const SMALL_STR_MAX_LENGTH = SMALL_STRING_SIZE - 1; -const SMALL_STRING_SIZE = @sizeOf(RocStr); - -fn init_blank_small_string(comptime n: usize) [n]u8 { - var prime_list: [n]u8 = undefined; - - var i = 0; - while (i < n) : (i += 1) { - prime_list[i] = 0; - } - - return prime_list; -} - -pub const RocStr = extern struct { - str_bytes: ?[*]u8, - str_len: usize, - str_capacity: usize, - - pub const alignment = @alignOf(usize); - - pub inline fn empty() RocStr { - return RocStr{ - .str_len = 0, - .str_bytes = null, - .str_capacity = MASK, - }; - } - - // This clones the pointed-to bytes if they won't fit in a - // small string, and returns a (pointer, len) tuple which points to them. - pub fn init(bytes_ptr: [*]const u8, length: usize) RocStr { - var result = RocStr.allocate(InPlace.Clone, length); - @memcpy(result.asU8ptr(), bytes_ptr, length); - - return result; - } - - pub fn initBig(_: InPlace, number_of_chars: usize) RocStr { - const first_element = utils.allocateWithRefcount(number_of_chars, @sizeOf(usize)); - - return RocStr{ - .str_bytes = first_element, - .str_len = number_of_chars, - .str_capacity = number_of_chars, - }; - } - - // allocate space for a (big or small) RocStr, but put nothing in it yet - pub fn allocate(result_in_place: InPlace, number_of_chars: usize) RocStr { - const result_is_big = number_of_chars >= SMALL_STRING_SIZE; - - if (result_is_big) { - return RocStr.initBig(result_in_place, number_of_chars); - } else { - var string = RocStr.empty(); - - string.asU8ptr()[@sizeOf(RocStr) - 1] = @intCast(u8, number_of_chars) | 0b1000_0000; - - return string; - } - } - - pub fn deinit(self: RocStr) void { - if (!self.isSmallStr()) { - utils.decref(self.str_bytes, self.str_len, RocStr.alignment); - } - } - - // This takes ownership of the pointed-to bytes if they won't fit in a - // small string, and returns a (pointer, len) tuple which points to them. - pub fn withCapacity(length: usize) RocStr { - const roc_str_size = @sizeOf(RocStr); - - if (length < roc_str_size) { - return RocStr.empty(); - } else { - var new_bytes = utils.alloc(length, RocStr.alignment) catch unreachable; - - var new_bytes_ptr: [*]u8 = @ptrCast([*]u8, &new_bytes); - - return RocStr{ - .str_bytes = new_bytes_ptr, - .str_len = length, - }; - } - } - - pub fn eq(self: RocStr, other: RocStr) bool { - // If they are byte-for-byte equal, they're definitely equal! - if (self.str_bytes == other.str_bytes and self.str_len == other.str_len) { - return true; - } - - const self_len = self.len(); - const other_len = other.len(); - - // If their lengths are different, they're definitely unequal. - if (self_len != other_len) { - return false; - } - - // Now we have to look at the string contents - const self_bytes = self.asU8ptr(); - const other_bytes = other.asU8ptr(); - - // It's faster to compare pointer-sized words rather than bytes, as far as possible - // The bytes are always pointer-size aligned due to the refcount - const self_words = @ptrCast([*]const usize, @alignCast(@alignOf(usize), self_bytes)); - const other_words = @ptrCast([*]const usize, @alignCast(@alignOf(usize), other_bytes)); - var w: usize = 0; - while (w < self_len / @sizeOf(usize)) : (w += 1) { - if (self_words[w] != other_words[w]) { - return false; - } - } - - // Compare the leftover bytes - var b = w * @sizeOf(usize); - while (b < self_len) : (b += 1) { - if (self_bytes[b] != other_bytes[b]) { - return false; - } - } - - return true; - } - - pub fn clone(in_place: InPlace, str: RocStr) RocStr { - if (str.isSmallStr()) { - // just return the bytes - return str; - } else { - var new_str = RocStr.initBig(in_place, str.str_len); - - var old_bytes: [*]u8 = @ptrCast([*]u8, str.str_bytes); - var new_bytes: [*]u8 = @ptrCast([*]u8, new_str.str_bytes); - - @memcpy(new_bytes, old_bytes, str.str_len); - - return new_str; - } - } - - pub fn reallocate( - self: RocStr, - new_length: usize, - ) RocStr { - const element_width = 1; - - if (self.str_bytes) |source_ptr| { - if (self.isUnique()) { - const new_source = utils.unsafeReallocate(source_ptr, RocStr.alignment, self.len(), new_length, element_width); - - return RocStr{ .str_bytes = new_source, .str_len = new_length }; - } - } - - return self.reallocateFresh(new_length); - } - - /// reallocate by explicitly making a new allocation and copying elements over - pub fn reallocateFresh( - self: RocStr, - new_length: usize, - ) RocStr { - const old_length = self.len(); - const delta_length = new_length - old_length; - - const result = RocStr.allocate(InPlace.Clone, new_length); - - // transfer the memory - - const source_ptr = self.asU8ptr(); - const dest_ptr = result.asU8ptr(); - - @memcpy(dest_ptr, source_ptr, old_length); - @memset(dest_ptr + old_length, 0, delta_length); - - self.deinit(); - - return result; - } - - // NOTE: returns false for empty string! - pub fn isSmallStr(self: RocStr) bool { - return @bitCast(isize, self.str_capacity) < 0; - } - - fn asArray(self: RocStr) [@sizeOf(RocStr)]u8 { - const as_int = @ptrToInt(&self); - const as_ptr = @intToPtr([*]u8, as_int); - const slice = as_ptr[0..@sizeOf(RocStr)]; - - return slice.*; - } - - pub fn len(self: RocStr) usize { - if (self.isSmallStr()) { - return self.asArray()[@sizeOf(RocStr) - 1] ^ 0b1000_0000; - } else { - return self.str_len; - } - } - - pub fn capacity(self: RocStr) usize { - return self.str_capacity ^ MASK; - } - - pub fn isEmpty(self: RocStr) bool { - return self.len() == 0; - } - - // If a string happens to be null-terminated already, then we can pass its - // bytes directly to functions (e.g. for opening files) that require - // null-terminated strings. Otherwise, we need to allocate and copy a new - // null-terminated string, which has a much higher performance cost! - fn isNullTerminated(self: RocStr) bool { - const length = self.len(); - const longest_small_str = @sizeOf(RocStr) - 1; - - // NOTE: We want to compare length here, *NOT* check for is_small_str! - // This is because we explicitly want the empty string to be handled in - // this branch, even though the empty string is not a small string. - // - // (The other branch dereferences the bytes pointer, which is not safe - // to do for the empty string.) - if (length <= longest_small_str) { - // If we're a small string, then usually the next byte after the - // end of the string will be zero. (Small strings set all their - // unused bytes to 0, so that comparison for equality can be fast.) - // - // However, empty strings are *not* null terminated, so if this is - // empty, it should return false. - // - // Also, if we are exactly a maximum-length small string, - // then the next byte is off the end of the struct; - // in that case, we are also not null-terminated! - return length != 0 and length != longest_small_str; - } else { - // This is a big string, and it's not empty, so we can safely - // dereference the pointer. - const ptr: [*]usize = @ptrCast([*]usize, @alignCast(@alignOf(usize), self.str_bytes)); - const capacity_or_refcount: isize = (ptr - 1)[0]; - - // If capacity_or_refcount is positive, then it's a capacity value. - // - // If we have excess capacity, then we can safely read the next - // byte after the end of the string. Maybe it happens to be zero! - if (capacity_or_refcount > @intCast(isize, length)) { - return self.str_bytes[length] == 0; - } else { - // This string was refcounted or immortal; we can't safely read - // the next byte, so assume the string is not null-terminated. - return false; - } - } - } - - pub fn isUnique(self: RocStr) bool { - // small strings can be copied - if (self.isSmallStr()) { - return true; - } - - // otherwise, check if the refcount is one - return @call(.{ .modifier = always_inline }, RocStr.isRefcountOne, .{self}); - } - - fn isRefcountOne(self: RocStr) bool { - const ptr: [*]usize = @ptrCast([*]usize, @alignCast(@alignOf(usize), self.str_bytes)); - return (ptr - 1)[0] == utils.REFCOUNT_ONE; - } - - pub fn asSlice(self: RocStr) []u8 { - return self.asU8ptr()[0..self.len()]; - } - - pub fn asU8ptr(self: RocStr) [*]u8 { - - // Since this conditional would be prone to branch misprediction, - // make sure it will compile to a cmov. - // return if (self.isSmallStr()) (&@bitCast([@sizeOf(RocStr)]u8, self)) else (@ptrCast([*]u8, self.str_bytes)); - if (self.isSmallStr()) { - const as_int = @ptrToInt(&self); - const as_ptr = @intToPtr([*]u8, as_int); - return as_ptr; - } else { - return @ptrCast([*]u8, self.str_bytes); - } - } - - // Given a pointer to some bytes, write the first (len) bytes of this - // RocStr's contents into it. - // - // One use for this function is writing into an `alloca` for a C string that - // only needs to live long enough to be passed as an argument to - // a C function - like the file path argument to `fopen`. - pub fn memcpy(self: RocStr, dest: [*]u8) void { - const src = self.asU8ptr(); - @memcpy(dest, src, self.len()); - } - - test "RocStr.eq: small, equal" { - const str1_len = 3; - var str1: [str1_len]u8 = "abc".*; - const str1_ptr: [*]u8 = &str1; - var roc_str1 = RocStr.init(str1_ptr, str1_len); - - const str2_len = 3; - var str2: [str2_len]u8 = "abc".*; - const str2_ptr: [*]u8 = &str2; - var roc_str2 = RocStr.init(str2_ptr, str2_len); - - try expect(roc_str1.eq(roc_str2)); - - roc_str1.deinit(); - roc_str2.deinit(); - } - - test "RocStr.eq: small, not equal, different length" { - const str1_len = 4; - var str1: [str1_len]u8 = "abcd".*; - const str1_ptr: [*]u8 = &str1; - var roc_str1 = RocStr.init(str1_ptr, str1_len); - - const str2_len = 3; - var str2: [str2_len]u8 = "abc".*; - const str2_ptr: [*]u8 = &str2; - var roc_str2 = RocStr.init(str2_ptr, str2_len); - - defer { - roc_str1.deinit(); - roc_str2.deinit(); - } - - try expect(!roc_str1.eq(roc_str2)); - } - - test "RocStr.eq: small, not equal, same length" { - const str1_len = 3; - var str1: [str1_len]u8 = "acb".*; - const str1_ptr: [*]u8 = &str1; - var roc_str1 = RocStr.init(str1_ptr, str1_len); - - const str2_len = 3; - var str2: [str2_len]u8 = "abc".*; - const str2_ptr: [*]u8 = &str2; - var roc_str2 = RocStr.init(str2_ptr, str2_len); - - defer { - roc_str1.deinit(); - roc_str2.deinit(); - } - - try expect(!roc_str1.eq(roc_str2)); - } - - test "RocStr.eq: large, equal" { - const content = "012345678901234567890123456789"; - const roc_str1 = RocStr.init(content, content.len); - const roc_str2 = RocStr.init(content, content.len); - - defer { - roc_str1.deinit(); - roc_str2.deinit(); - } - - try expect(roc_str1.eq(roc_str2)); - } - - test "RocStr.eq: large, different lengths, unequal" { - const content1 = "012345678901234567890123456789"; - const roc_str1 = RocStr.init(content1, content1.len); - const content2 = "012345678901234567890"; - const roc_str2 = RocStr.init(content2, content2.len); - - defer { - roc_str1.deinit(); - roc_str2.deinit(); - } - - try expect(!roc_str1.eq(roc_str2)); - } - - test "RocStr.eq: large, different content, unequal" { - const content1 = "012345678901234567890123456789!!"; - const roc_str1 = RocStr.init(content1, content1.len); - const content2 = "012345678901234567890123456789--"; - const roc_str2 = RocStr.init(content2, content2.len); - - defer { - roc_str1.deinit(); - roc_str2.deinit(); - } - - try expect(!roc_str1.eq(roc_str2)); - } - - test "RocStr.eq: large, garbage after end, equal" { - const content = "012345678901234567890123456789"; - const roc_str1 = RocStr.init(content, content.len); - const roc_str2 = RocStr.init(content, content.len); - try expect(roc_str1.str_bytes != roc_str2.str_bytes); - - // Insert garbage after the end of each string - roc_str1.str_bytes.?[30] = '!'; - roc_str1.str_bytes.?[31] = '!'; - roc_str2.str_bytes.?[30] = '-'; - roc_str2.str_bytes.?[31] = '-'; - - defer { - roc_str1.deinit(); - roc_str2.deinit(); - } - - try expect(roc_str1.eq(roc_str2)); - } -}; - -pub fn init(bytes_ptr: [*]const u8, length: usize) callconv(.C) RocStr { - return @call(.{ .modifier = always_inline }, RocStr.init, .{ bytes_ptr, length }); -} - -// Str.equal -pub fn strEqual(self: RocStr, other: RocStr) callconv(.C) bool { - return self.eq(other); -} - -// Str.numberOfBytes -pub fn strNumberOfBytes(string: RocStr) callconv(.C) usize { - return string.len(); -} - -// Str.fromInt -pub fn exportFromInt(comptime T: type, comptime name: []const u8) void { - comptime var f = struct { - fn func(int: T) callconv(.C) RocStr { - return @call(.{ .modifier = always_inline }, strFromIntHelp, .{ T, int }); - } - }.func; - - @export(f, .{ .name = name ++ @typeName(T), .linkage = .Strong }); -} - -fn strFromIntHelp(comptime T: type, int: T) RocStr { - // determine maximum size for this T - const size = comptime blk: { - // the string representation of the minimum i128 value uses at most 40 characters - var buf: [40]u8 = undefined; - var resultMin = std.fmt.bufPrint(&buf, "{}", .{std.math.minInt(T)}) catch unreachable; - var resultMax = std.fmt.bufPrint(&buf, "{}", .{std.math.maxInt(T)}) catch unreachable; - var result = if (resultMin.len > resultMax.len) resultMin.len else resultMax.len; - break :blk result; - }; - - var buf: [size]u8 = undefined; - const result = std.fmt.bufPrint(&buf, "{}", .{int}) catch unreachable; - - return RocStr.init(&buf, result.len); -} - -// Str.fromFloat -pub fn strFromFloatC(float: f64) callconv(.C) RocStr { - return @call(.{ .modifier = always_inline }, strFromFloatHelp, .{ f64, float }); -} - -fn strFromFloatHelp(comptime T: type, float: T) RocStr { - var buf: [100]u8 = undefined; - const result = std.fmt.bufPrint(&buf, "{d}", .{float}) catch unreachable; - - return RocStr.init(&buf, result.len); -} - -// Str.split -pub fn strSplitInPlaceC(opt_array: ?[*]RocStr, string: RocStr, delimiter: RocStr) callconv(.C) void { - if (opt_array) |array| { - return @call(.{ .modifier = always_inline }, strSplitInPlace, .{ array, string, delimiter }); - } else { - return; - } -} - -fn strSplitInPlace(array: [*]RocStr, string: RocStr, delimiter: RocStr) void { - var ret_array_index: usize = 0; - var slice_start_index: usize = 0; - var str_index: usize = 0; - - const str_bytes = string.asU8ptr(); - const str_len = string.len(); - - const delimiter_bytes_ptrs = delimiter.asU8ptr(); - const delimiter_len = delimiter.len(); - - if (str_len > delimiter_len and delimiter_len > 0) { - const end_index: usize = str_len - delimiter_len + 1; - while (str_index <= end_index) { - var delimiter_index: usize = 0; - var matches_delimiter = true; - - while (delimiter_index < delimiter_len) { - var delimiterChar = delimiter_bytes_ptrs[delimiter_index]; - var strChar = str_bytes[str_index + delimiter_index]; - - if (delimiterChar != strChar) { - matches_delimiter = false; - break; - } - - delimiter_index += 1; - } - - if (matches_delimiter) { - const segment_len: usize = str_index - slice_start_index; - - array[ret_array_index] = RocStr.init(str_bytes + slice_start_index, segment_len); - slice_start_index = str_index + delimiter_len; - ret_array_index += 1; - str_index += delimiter_len; - } else { - str_index += 1; - } - } - } - - array[ret_array_index] = RocStr.init(str_bytes + slice_start_index, str_len - slice_start_index); -} - -test "strSplitInPlace: empty delimiter" { - // Str.split "abc" "" == ["abc"] - const str_arr = "abc"; - const str = RocStr.init(str_arr, str_arr.len); - - const delimiter_arr = ""; - const delimiter = RocStr.init(delimiter_arr, delimiter_arr.len); - - var array: [1]RocStr = undefined; - const array_ptr: [*]RocStr = &array; - - strSplitInPlace(array_ptr, str, delimiter); - - var expected = [1]RocStr{ - str, - }; - - defer { - for (array) |roc_str| { - roc_str.deinit(); - } - - for (expected) |roc_str| { - roc_str.deinit(); - } - - str.deinit(); - delimiter.deinit(); - } - - try expectEqual(array.len, expected.len); - try expect(array[0].eq(expected[0])); -} - -test "strSplitInPlace: no delimiter" { - // Str.split "abc" "!" == ["abc"] - const str_arr = "abc"; - const str = RocStr.init(str_arr, str_arr.len); - - const delimiter_arr = "!"; - const delimiter = RocStr.init(delimiter_arr, delimiter_arr.len); - - var array: [1]RocStr = undefined; - const array_ptr: [*]RocStr = &array; - - strSplitInPlace(array_ptr, str, delimiter); - - var expected = [1]RocStr{ - str, - }; - - defer { - for (array) |roc_str| { - roc_str.deinit(); - } - - for (expected) |roc_str| { - roc_str.deinit(); - } - - str.deinit(); - delimiter.deinit(); - } - - try expectEqual(array.len, expected.len); - try expect(array[0].eq(expected[0])); -} - -test "strSplitInPlace: empty end" { - const str_arr = "1---- ---- ---- ---- ----2---- ---- ---- ---- ----"; - const str = RocStr.init(str_arr, str_arr.len); - - const delimiter_arr = "---- ---- ---- ---- ----"; - const delimiter = RocStr.init(delimiter_arr, delimiter_arr.len); - - const array_len: usize = 3; - var array: [array_len]RocStr = [_]RocStr{ - undefined, - undefined, - undefined, - }; - const array_ptr: [*]RocStr = &array; - - strSplitInPlace(array_ptr, str, delimiter); - - const one = RocStr.init("1", 1); - const two = RocStr.init("2", 1); - - var expected = [3]RocStr{ - one, two, RocStr.empty(), - }; - - defer { - for (array) |rocStr| { - rocStr.deinit(); - } - - for (expected) |rocStr| { - rocStr.deinit(); - } - - str.deinit(); - delimiter.deinit(); - } - - try expectEqual(array.len, expected.len); - try expect(array[0].eq(expected[0])); - try expect(array[1].eq(expected[1])); - try expect(array[2].eq(expected[2])); -} - -test "strSplitInPlace: delimiter on sides" { - const str_arr = "tttghittt"; - const str = RocStr.init(str_arr, str_arr.len); - - const delimiter_arr = "ttt"; - const delimiter = RocStr.init(delimiter_arr, delimiter_arr.len); - - const array_len: usize = 3; - var array: [array_len]RocStr = [_]RocStr{ - undefined, - undefined, - undefined, - }; - const array_ptr: [*]RocStr = &array; - strSplitInPlace(array_ptr, str, delimiter); - - const ghi_arr = "ghi"; - const ghi = RocStr.init(ghi_arr, ghi_arr.len); - - var expected = [3]RocStr{ - RocStr.empty(), ghi, RocStr.empty(), - }; - - defer { - for (array) |rocStr| { - rocStr.deinit(); - } - - for (expected) |rocStr| { - rocStr.deinit(); - } - - str.deinit(); - delimiter.deinit(); - } - - try expectEqual(array.len, expected.len); - try expect(array[0].eq(expected[0])); - try expect(array[1].eq(expected[1])); - try expect(array[2].eq(expected[2])); -} - -test "strSplitInPlace: three pieces" { - // Str.split "a!b!c" "!" == ["a", "b", "c"] - const str_arr = "a!b!c"; - const str = RocStr.init(str_arr, str_arr.len); - - const delimiter_arr = "!"; - const delimiter = RocStr.init(delimiter_arr, delimiter_arr.len); - - const array_len: usize = 3; - var array: [array_len]RocStr = undefined; - const array_ptr: [*]RocStr = &array; - - strSplitInPlace(array_ptr, str, delimiter); - - const a = RocStr.init("a", 1); - const b = RocStr.init("b", 1); - const c = RocStr.init("c", 1); - - var expected_array = [array_len]RocStr{ - a, b, c, - }; - - defer { - for (array) |roc_str| { - roc_str.deinit(); - } - - for (expected_array) |roc_str| { - roc_str.deinit(); - } - - str.deinit(); - delimiter.deinit(); - } - - try expectEqual(expected_array.len, array.len); - try expect(array[0].eq(expected_array[0])); - try expect(array[1].eq(expected_array[1])); - try expect(array[2].eq(expected_array[2])); -} - -// This is used for `Str.split : Str, Str -> Array Str -// It is used to count how many segments the input `_str` -// needs to be broken into, so that we can allocate a array -// of that size. It always returns at least 1. -pub fn countSegments(string: RocStr, delimiter: RocStr) callconv(.C) usize { - const str_bytes = string.asU8ptr(); - const str_len = string.len(); - - const delimiter_bytes_ptrs = delimiter.asU8ptr(); - const delimiter_len = delimiter.len(); - - var count: usize = 1; - - if (str_len > delimiter_len and delimiter_len > 0) { - var str_index: usize = 0; - const end_cond: usize = str_len - delimiter_len + 1; - - while (str_index < end_cond) { - var delimiter_index: usize = 0; - - var matches_delimiter = true; - - while (delimiter_index < delimiter_len) { - const delimiterChar = delimiter_bytes_ptrs[delimiter_index]; - const strChar = str_bytes[str_index + delimiter_index]; - - if (delimiterChar != strChar) { - matches_delimiter = false; - break; - } - - delimiter_index += 1; - } - - if (matches_delimiter) { - count += 1; - } - - str_index += 1; - } - } - - return count; -} - -test "countSegments: long delimiter" { - // Str.split "str" "delimiter" == ["str"] - // 1 segment - const str_arr = "str"; - const str = RocStr.init(str_arr, str_arr.len); - - const delimiter_arr = "delimiter"; - const delimiter = RocStr.init(delimiter_arr, delimiter_arr.len); - - defer { - str.deinit(); - delimiter.deinit(); - } - - const segments_count = countSegments(str, delimiter); - try expectEqual(segments_count, 1); -} - -test "countSegments: delimiter at start" { - // Str.split "hello there" "hello" == ["", " there"] - // 2 segments - const str_arr = "hello there"; - const str = RocStr.init(str_arr, str_arr.len); - - const delimiter_arr = "hello"; - const delimiter = RocStr.init(delimiter_arr, delimiter_arr.len); - - defer { - str.deinit(); - delimiter.deinit(); - } - - const segments_count = countSegments(str, delimiter); - - try expectEqual(segments_count, 2); -} - -test "countSegments: delimiter interspered" { - // Str.split "a!b!c" "!" == ["a", "b", "c"] - // 3 segments - const str_arr = "a!b!c"; - const str = RocStr.init(str_arr, str_arr.len); - - const delimiter_arr = "!"; - const delimiter = RocStr.init(delimiter_arr, delimiter_arr.len); - - defer { - str.deinit(); - delimiter.deinit(); - } - - const segments_count = countSegments(str, delimiter); - - try expectEqual(segments_count, 3); -} - -// Str.countGraphemeClusters -const grapheme = @import("helpers/grapheme.zig"); -pub fn countGraphemeClusters(string: RocStr) callconv(.C) usize { - if (string.isEmpty()) { - return 0; - } - - const bytes_len = string.len(); - const bytes_ptr = string.asU8ptr(); - - var bytes = bytes_ptr[0..bytes_len]; - var iter = (unicode.Utf8View.init(bytes) catch unreachable).iterator(); - - var count: usize = 0; - var grapheme_break_state: ?grapheme.BoundClass = null; - var grapheme_break_state_ptr = &grapheme_break_state; - var opt_last_codepoint: ?u21 = null; - while (iter.nextCodepoint()) |cur_codepoint| { - if (opt_last_codepoint) |last_codepoint| { - var did_break = grapheme.isGraphemeBreak(last_codepoint, cur_codepoint, grapheme_break_state_ptr); - if (did_break) { - count += 1; - grapheme_break_state = null; - } - } - opt_last_codepoint = cur_codepoint; - } - - // If there are no breaks, but the str is not empty, then there - // must be a single grapheme - if (bytes_len != 0) { - count += 1; - } - - return count; -} - -test "countGraphemeClusters: empty string" { - const count = countGraphemeClusters(RocStr.empty()); - try expectEqual(count, 0); -} - -test "countGraphemeClusters: ascii characters" { - const bytes_arr = "abcd"; - const bytes_len = bytes_arr.len; - const str = RocStr.init(bytes_arr, bytes_len); - defer str.deinit(); - - const count = countGraphemeClusters(str); - try expectEqual(count, 4); -} - -test "countGraphemeClusters: utf8 characters" { - const bytes_arr = "ãxā"; - const bytes_len = bytes_arr.len; - const str = RocStr.init(bytes_arr, bytes_len); - defer str.deinit(); - - const count = countGraphemeClusters(str); - try expectEqual(count, 3); -} - -test "countGraphemeClusters: emojis" { - const bytes_arr = "🤔🤔🤔"; - const bytes_len = bytes_arr.len; - const str = RocStr.init(bytes_arr, bytes_len); - defer str.deinit(); - - const count = countGraphemeClusters(str); - try expectEqual(count, 3); -} - -test "countGraphemeClusters: emojis and ut8 characters" { - const bytes_arr = "🤔å🤔¥🤔ç"; - const bytes_len = bytes_arr.len; - const str = RocStr.init(bytes_arr, bytes_len); - defer str.deinit(); - - const count = countGraphemeClusters(str); - try expectEqual(count, 6); -} - -test "countGraphemeClusters: emojis, ut8, and ascii characters" { - const bytes_arr = "6🤔å🤔e¥🤔çpp"; - const bytes_len = bytes_arr.len; - const str = RocStr.init(bytes_arr, bytes_len); - defer str.deinit(); - - const count = countGraphemeClusters(str); - try expectEqual(count, 10); -} - -// Str.startsWith -pub fn startsWith(string: RocStr, prefix: RocStr) callconv(.C) bool { - const bytes_len = string.len(); - const bytes_ptr = string.asU8ptr(); - - const prefix_len = prefix.len(); - const prefix_ptr = prefix.asU8ptr(); - - if (prefix_len > bytes_len) { - return false; - } - - // we won't exceed bytes_len due to the previous check - var i: usize = 0; - while (i < prefix_len) { - if (bytes_ptr[i] != prefix_ptr[i]) { - return false; - } - i += 1; - } - return true; -} - -// Str.repeat -pub fn repeat(string: RocStr, count: usize) callconv(.C) RocStr { - const bytes_len = string.len(); - const bytes_ptr = string.asU8ptr(); - - var ret_string = RocStr.allocate(.Clone, count * bytes_len); - var ret_string_ptr = ret_string.asU8ptr(); - - var i: usize = 0; - while (i < count) : (i += 1) { - @memcpy(ret_string_ptr + (i * bytes_len), bytes_ptr, bytes_len); - } - - return ret_string; -} - -// Str.startsWithCodePt -pub fn startsWithCodePt(string: RocStr, prefix: u32) callconv(.C) bool { - const bytes_ptr = string.asU8ptr(); - - var buffer: [4]u8 = undefined; - - var width = std.unicode.utf8Encode(@truncate(u21, prefix), &buffer) catch unreachable; - - var i: usize = 0; - while (i < width) : (i += 1) { - const a = buffer[i]; - const b = bytes_ptr[i]; - if (a != b) { - return false; - } - } - - return true; -} - -test "startsWithCodePt: ascii char" { - const whole = RocStr.init("foobar", 6); - const prefix = 'f'; - try expect(startsWithCodePt(whole, prefix)); -} - -test "startsWithCodePt: emoji" { - const yes = RocStr.init("💖foobar", 10); - const no = RocStr.init("foobar", 6); - const prefix = '💖'; - try expect(startsWithCodePt(yes, prefix)); - try expect(!startsWithCodePt(no, prefix)); -} - -test "startsWith: foo starts with fo" { - const foo = RocStr.init("foo", 3); - const fo = RocStr.init("fo", 2); - try expect(startsWith(foo, fo)); -} - -test "startsWith: 123456789123456789 starts with 123456789123456789" { - const str = RocStr.init("123456789123456789", 18); - defer str.deinit(); - try expect(startsWith(str, str)); -} - -test "startsWith: 12345678912345678910 starts with 123456789123456789" { - const str = RocStr.init("12345678912345678910", 20); - defer str.deinit(); - const prefix = RocStr.init("123456789123456789", 18); - defer prefix.deinit(); - - try expect(startsWith(str, prefix)); -} - -// Str.endsWith -pub fn endsWith(string: RocStr, suffix: RocStr) callconv(.C) bool { - const bytes_len = string.len(); - const bytes_ptr = string.asU8ptr(); - - const suffix_len = suffix.len(); - const suffix_ptr = suffix.asU8ptr(); - - if (suffix_len > bytes_len) { - return false; - } - - const offset: usize = bytes_len - suffix_len; - var i: usize = 0; - while (i < suffix_len) { - if (bytes_ptr[i + offset] != suffix_ptr[i]) { - return false; - } - i += 1; - } - return true; -} - -test "endsWith: foo ends with oo" { - const foo = RocStr.init("foo", 3); - const oo = RocStr.init("oo", 2); - defer foo.deinit(); - defer oo.deinit(); - - try expect(endsWith(foo, oo)); -} - -test "endsWith: 123456789123456789 ends with 123456789123456789" { - const str = RocStr.init("123456789123456789", 18); - defer str.deinit(); - try expect(endsWith(str, str)); -} - -test "endsWith: 12345678912345678910 ends with 345678912345678910" { - const str = RocStr.init("12345678912345678910", 20); - const suffix = RocStr.init("345678912345678910", 18); - defer str.deinit(); - defer suffix.deinit(); - - try expect(endsWith(str, suffix)); -} - -test "endsWith: hello world ends with world" { - const str = RocStr.init("hello world", 11); - const suffix = RocStr.init("world", 5); - defer str.deinit(); - defer suffix.deinit(); - - try expect(endsWith(str, suffix)); -} - -// Str.concat -pub fn strConcatC(arg1: RocStr, arg2: RocStr) callconv(.C) RocStr { - return @call(.{ .modifier = always_inline }, strConcat, .{ arg1, arg2 }); -} - -fn strConcat(arg1: RocStr, arg2: RocStr) RocStr { - if (arg1.isEmpty()) { - // the second argument is borrowed, so we must increment its refcount before returning - const result_in_place = InPlace.Clone; - return RocStr.clone(result_in_place, arg2); - } else if (arg2.isEmpty()) { - // the first argument is owned, so we can return it without cloning - return arg1; - } else { - const combined_length = arg1.len() + arg2.len(); - - const element_width = 1; - - if (!arg1.isSmallStr() and arg1.isUnique()) { - if (arg1.str_bytes) |source_ptr| { - const new_source = utils.unsafeReallocate( - source_ptr, - RocStr.alignment, - arg1.len(), - combined_length, - element_width, - ); - - @memcpy(new_source + arg1.len(), arg2.asU8ptr(), arg2.len()); - - return RocStr{ - .str_bytes = new_source, - .str_len = combined_length, - .str_capacity = combined_length, - }; - } - } - - var result = arg1.reallocateFresh(combined_length); - var result_ptr = result.asU8ptr(); - - arg1.memcpy(result_ptr); - arg2.memcpy(result_ptr + arg1.len()); - - return result; - } -} - -test "RocStr.concat: small concat small" { - const str1_len = 3; - var str1: [str1_len]u8 = "foo".*; - const str1_ptr: [*]u8 = &str1; - var roc_str1 = RocStr.init(str1_ptr, str1_len); - - const str2_len = 3; - var str2: [str2_len]u8 = "abc".*; - const str2_ptr: [*]u8 = &str2; - var roc_str2 = RocStr.init(str2_ptr, str2_len); - - const str3_len = 6; - var str3: [str3_len]u8 = "fooabc".*; - const str3_ptr: [*]u8 = &str3; - var roc_str3 = RocStr.init(str3_ptr, str3_len); - - defer { - roc_str1.deinit(); - roc_str2.deinit(); - roc_str3.deinit(); - } - - const result = strConcat(roc_str1, roc_str2); - - defer result.deinit(); - - try expect(roc_str3.eq(result)); -} - -pub const RocListStr = extern struct { - list_elements: ?[*]RocStr, - list_length: usize, - list_capacity: usize, -}; - -// Str.joinWith -pub fn strJoinWithC(list: RocList, separator: RocStr) callconv(.C) RocStr { - const roc_list_str = RocListStr{ - .list_elements = @ptrCast(?[*]RocStr, @alignCast(@alignOf(usize), list.bytes)), - .list_length = list.length, - .list_capacity = list.capacity, - }; - - return @call(.{ .modifier = always_inline }, strJoinWith, .{ roc_list_str, separator }); -} - -fn strJoinWith(list: RocListStr, separator: RocStr) RocStr { - const len = list.list_length; - - if (len == 0) { - return RocStr.empty(); - } else { - const ptr = @ptrCast([*]RocStr, list.list_elements); - const slice: []RocStr = ptr[0..len]; - - // determine the size of the result - var total_size: usize = 0; - for (slice) |substr| { - total_size += substr.len(); - } - - // include size of the separator - total_size += separator.len() * (len - 1); - - var result = RocStr.allocate(InPlace.Clone, total_size); - var result_ptr = result.asU8ptr(); - - var offset: usize = 0; - for (slice[0 .. len - 1]) |substr| { - substr.memcpy(result_ptr + offset); - offset += substr.len(); - - separator.memcpy(result_ptr + offset); - offset += separator.len(); - } - - const substr = slice[len - 1]; - substr.memcpy(result_ptr + offset); - - return result; - } -} - -test "RocStr.joinWith: result is big" { - const sep_len = 2; - var sep: [sep_len]u8 = ", ".*; - const sep_ptr: [*]u8 = &sep; - var roc_sep = RocStr.init(sep_ptr, sep_len); - - const elem_len = 13; - var elem: [elem_len]u8 = "foobarbazspam".*; - const elem_ptr: [*]u8 = &elem; - var roc_elem = RocStr.init(elem_ptr, elem_len); - - const result_len = 43; - var xresult: [result_len]u8 = "foobarbazspam, foobarbazspam, foobarbazspam".*; - const result_ptr: [*]u8 = &xresult; - var roc_result = RocStr.init(result_ptr, result_len); - - var elements: [3]RocStr = .{ roc_elem, roc_elem, roc_elem }; - const list = RocListStr{ - .list_length = 3, - .list_capacity = 3, - .list_elements = @ptrCast([*]RocStr, &elements), - }; - - defer { - roc_sep.deinit(); - roc_elem.deinit(); - roc_result.deinit(); - } - - const result = strJoinWith(list, roc_sep); - - defer result.deinit(); - - try expect(roc_result.eq(result)); -} - -// Str.toUtf8 -pub fn strToUtf8C(arg: RocStr) callconv(.C) RocList { - return strToBytes(arg); -} - -inline fn strToBytes(arg: RocStr) RocList { - if (arg.isEmpty()) { - return RocList.empty(); - } else if (arg.isSmallStr()) { - const length = arg.len(); - const ptr = utils.allocateWithRefcount(length, RocStr.alignment); - - @memcpy(ptr, arg.asU8ptr(), length); - - return RocList{ .length = length, .bytes = ptr, .capacity = length }; - } else { - return RocList{ .length = arg.len(), .bytes = arg.str_bytes, .capacity = arg.str_capacity }; - } -} - -const FromUtf8Result = extern struct { - byte_index: usize, - string: RocStr, - is_ok: bool, - problem_code: Utf8ByteProblem, -}; - -const CountAndStart = extern struct { - count: usize, - start: usize, -}; - -pub fn fromUtf8C(arg: RocList, update_mode: UpdateMode, output: *FromUtf8Result) callconv(.C) void { - output.* = fromUtf8(arg, update_mode); -} - -inline fn fromUtf8(arg: RocList, update_mode: UpdateMode) FromUtf8Result { - const bytes = @ptrCast([*]const u8, arg.bytes)[0..arg.length]; - - if (unicode.utf8ValidateSlice(bytes)) { - // the output will be correct. Now we need to take ownership of the input - if (arg.len() <= SMALL_STR_MAX_LENGTH) { - // turn the bytes into a small string - const string = RocStr.init(@ptrCast([*]u8, arg.bytes), arg.len()); - - // then decrement the input list - const data_bytes = arg.len(); - utils.decref(arg.bytes, data_bytes, RocStr.alignment); - - return FromUtf8Result{ - .is_ok = true, - .string = string, - .byte_index = 0, - .problem_code = Utf8ByteProblem.InvalidStartByte, - }; - } else { - const byte_list = arg.makeUniqueExtra(RocStr.alignment, @sizeOf(u8), update_mode); - - const string = RocStr{ - .str_bytes = byte_list.bytes, - .str_len = byte_list.length, - .str_capacity = byte_list.capacity, - }; - - return FromUtf8Result{ - .is_ok = true, - .string = string, - .byte_index = 0, - .problem_code = Utf8ByteProblem.InvalidStartByte, - }; - } - } else { - const temp = errorToProblem(@ptrCast([*]u8, arg.bytes), arg.length); - - // consume the input list - const data_bytes = arg.len(); - utils.decref(arg.bytes, data_bytes, RocStr.alignment); - - return FromUtf8Result{ - .is_ok = false, - .string = RocStr.empty(), - .byte_index = temp.index, - .problem_code = temp.problem, - }; - } -} - -pub fn fromUtf8RangeC(arg: RocList, countAndStart: CountAndStart, output: *FromUtf8Result) callconv(.C) void { - output.* = @call(.{ .modifier = always_inline }, fromUtf8Range, .{ arg, countAndStart }); -} - -fn fromUtf8Range(arg: RocList, countAndStart: CountAndStart) FromUtf8Result { - const bytes = @ptrCast([*]const u8, arg.bytes)[countAndStart.start..countAndStart.count]; - - if (unicode.utf8ValidateSlice(bytes)) { - // the output will be correct. Now we need to clone the input - const string = RocStr.init(@ptrCast([*]const u8, bytes), countAndStart.count); - - return FromUtf8Result{ .is_ok = true, .string = string, .byte_index = 0, .problem_code = Utf8ByteProblem.InvalidStartByte }; - } else { - const temp = errorToProblem(@ptrCast([*]u8, arg.bytes), arg.length); - return FromUtf8Result{ .is_ok = false, .string = RocStr.empty(), .byte_index = temp.index, .problem_code = temp.problem }; - } -} - -fn errorToProblem(bytes: [*]u8, length: usize) struct { index: usize, problem: Utf8ByteProblem } { - var index: usize = 0; - - while (index < length) { - const nextNumBytes = numberOfNextCodepointBytes(bytes, length, index) catch |err| { - switch (err) { - error.UnexpectedEof => { - return .{ .index = index, .problem = Utf8ByteProblem.UnexpectedEndOfSequence }; - }, - error.Utf8InvalidStartByte => return .{ .index = index, .problem = Utf8ByteProblem.InvalidStartByte }, - error.Utf8ExpectedContinuation => return .{ .index = index, .problem = Utf8ByteProblem.ExpectedContinuation }, - error.Utf8OverlongEncoding => return .{ .index = index, .problem = Utf8ByteProblem.OverlongEncoding }, - error.Utf8EncodesSurrogateHalf => return .{ .index = index, .problem = Utf8ByteProblem.EncodesSurrogateHalf }, - error.Utf8CodepointTooLarge => return .{ .index = index, .problem = Utf8ByteProblem.CodepointTooLarge }, - } - }; - index += nextNumBytes; - } - - unreachable; -} - -pub fn isValidUnicode(ptr: [*]u8, len: usize) callconv(.C) bool { - const bytes: []u8 = ptr[0..len]; - return @call(.{ .modifier = always_inline }, unicode.utf8ValidateSlice, .{bytes}); -} - -const Utf8DecodeError = error{ - UnexpectedEof, - Utf8InvalidStartByte, - Utf8ExpectedContinuation, - Utf8OverlongEncoding, - Utf8EncodesSurrogateHalf, - Utf8CodepointTooLarge, -}; - -// Essentially unicode.utf8ValidateSlice -> https://github.com/ziglang/zig/blob/0.7.x/lib/std/unicode.zig#L156 -// but only for the next codepoint from the index. Then we return the number of bytes of that codepoint. -// TODO: we only ever use the values 0-4, so can we use smaller int than `usize`? -pub fn numberOfNextCodepointBytes(ptr: [*]u8, len: usize, index: usize) Utf8DecodeError!usize { - const codepoint_len = try unicode.utf8ByteSequenceLength(ptr[index]); - const codepoint_end_index = index + codepoint_len; - if (codepoint_end_index > len) { - return error.UnexpectedEof; - } - _ = try unicode.utf8Decode(ptr[index..codepoint_end_index]); - return codepoint_end_index - index; -} - -// Return types for validateUtf8Bytes -// Values must be in alphabetical order. That is, lowest values are the first alphabetically. -pub const Utf8ByteProblem = enum(u8) { - CodepointTooLarge = 0, - EncodesSurrogateHalf = 1, - ExpectedContinuation = 2, - InvalidStartByte = 3, - OverlongEncoding = 4, - UnexpectedEndOfSequence = 5, -}; - -fn validateUtf8Bytes(bytes: [*]u8, length: usize) FromUtf8Result { - return fromUtf8(RocList{ .bytes = bytes, .length = length, .capacity = length }, .Immutable); -} - -fn validateUtf8BytesX(str: RocList) FromUtf8Result { - return fromUtf8(str, .Immutable); -} - -fn expectOk(result: FromUtf8Result) !void { - try expectEqual(result.is_ok, true); -} - -fn sliceHelp(bytes: [*]const u8, length: usize) RocList { - var list = RocList.allocate(RocStr.alignment, length, @sizeOf(u8)); - @memcpy(list.bytes orelse unreachable, bytes, length); - list.length = length; - - return list; -} - -fn toErrUtf8ByteResponse(index: usize, problem: Utf8ByteProblem) FromUtf8Result { - return FromUtf8Result{ .is_ok = false, .string = RocStr.empty(), .byte_index = index, .problem_code = problem }; -} - -// NOTE on memory: the validate function consumes a RC token of the input. Since -// we freshly created it (in `sliceHelp`), it has only one RC token, and input list will be deallocated. -// -// If we tested with big strings, we'd have to deallocate the output string, but never the input list - -test "validateUtf8Bytes: ascii" { - const raw = "abc"; - const ptr: [*]const u8 = @ptrCast([*]const u8, raw); - const list = sliceHelp(ptr, raw.len); - - try expectOk(validateUtf8BytesX(list)); -} - -test "validateUtf8Bytes: unicode œ" { - const raw = "œ"; - const ptr: [*]const u8 = @ptrCast([*]const u8, raw); - const list = sliceHelp(ptr, raw.len); - - try expectOk(validateUtf8BytesX(list)); -} - -test "validateUtf8Bytes: unicode ∆" { - const raw = "∆"; - const ptr: [*]const u8 = @ptrCast([*]const u8, raw); - const list = sliceHelp(ptr, raw.len); - - try expectOk(validateUtf8BytesX(list)); -} - -test "validateUtf8Bytes: emoji" { - const raw = "💖"; - const ptr: [*]const u8 = @ptrCast([*]const u8, raw); - const list = sliceHelp(ptr, raw.len); - - try expectOk(validateUtf8BytesX(list)); -} - -test "validateUtf8Bytes: unicode ∆ in middle of array" { - const raw = "œb∆c¬"; - const ptr: [*]const u8 = @ptrCast([*]const u8, raw); - const list = sliceHelp(ptr, raw.len); - - try expectOk(validateUtf8BytesX(list)); -} - -fn expectErr(list: RocList, index: usize, err: Utf8DecodeError, problem: Utf8ByteProblem) !void { - const str_ptr = @ptrCast([*]u8, list.bytes); - const str_len = list.length; - - try expectError(err, numberOfNextCodepointBytes(str_ptr, str_len, index)); - try expectEqual(toErrUtf8ByteResponse(index, problem), validateUtf8Bytes(str_ptr, str_len)); -} - -test "validateUtf8Bytes: invalid start byte" { - // https://github.com/ziglang/zig/blob/0.7.x/lib/std/unicode.zig#L426 - const raw = "ab\x80c"; - const ptr: [*]const u8 = @ptrCast([*]const u8, raw); - const list = sliceHelp(ptr, raw.len); - - try expectErr(list, 2, error.Utf8InvalidStartByte, Utf8ByteProblem.InvalidStartByte); -} - -test "validateUtf8Bytes: unexpected eof for 2 byte sequence" { - // https://github.com/ziglang/zig/blob/0.7.x/lib/std/unicode.zig#L426 - const raw = "abc\xc2"; - const ptr: [*]const u8 = @ptrCast([*]const u8, raw); - const list = sliceHelp(ptr, raw.len); - - try expectErr(list, 3, error.UnexpectedEof, Utf8ByteProblem.UnexpectedEndOfSequence); -} - -test "validateUtf8Bytes: expected continuation for 2 byte sequence" { - // https://github.com/ziglang/zig/blob/0.7.x/lib/std/unicode.zig#L426 - const raw = "abc\xc2\x00"; - const ptr: [*]const u8 = @ptrCast([*]const u8, raw); - const list = sliceHelp(ptr, raw.len); - - try expectErr(list, 3, error.Utf8ExpectedContinuation, Utf8ByteProblem.ExpectedContinuation); -} - -test "validateUtf8Bytes: unexpected eof for 3 byte sequence" { - // https://github.com/ziglang/zig/blob/0.7.x/lib/std/unicode.zig#L430 - const raw = "abc\xe0\x00"; - const ptr: [*]const u8 = @ptrCast([*]const u8, raw); - const list = sliceHelp(ptr, raw.len); - - try expectErr(list, 3, error.UnexpectedEof, Utf8ByteProblem.UnexpectedEndOfSequence); -} - -test "validateUtf8Bytes: expected continuation for 3 byte sequence" { - // https://github.com/ziglang/zig/blob/0.7.x/lib/std/unicode.zig#L430 - const raw = "abc\xe0\xa0\xc0"; - const ptr: [*]const u8 = @ptrCast([*]const u8, raw); - const list = sliceHelp(ptr, raw.len); - - try expectErr(list, 3, error.Utf8ExpectedContinuation, Utf8ByteProblem.ExpectedContinuation); -} - -test "validateUtf8Bytes: unexpected eof for 4 byte sequence" { - // https://github.com/ziglang/zig/blob/0.7.x/lib/std/unicode.zig#L437 - const raw = "abc\xf0\x90\x00"; - const ptr: [*]const u8 = @ptrCast([*]const u8, raw); - const list = sliceHelp(ptr, raw.len); - - try expectErr(list, 3, error.UnexpectedEof, Utf8ByteProblem.UnexpectedEndOfSequence); -} - -test "validateUtf8Bytes: expected continuation for 4 byte sequence" { - // https://github.com/ziglang/zig/blob/0.7.x/lib/std/unicode.zig#L437 - const raw = "abc\xf0\x90\x80\x00"; - const ptr: [*]const u8 = @ptrCast([*]const u8, raw); - const list = sliceHelp(ptr, raw.len); - - try expectErr(list, 3, error.Utf8ExpectedContinuation, Utf8ByteProblem.ExpectedContinuation); -} - -test "validateUtf8Bytes: overlong" { - // https://github.com/ziglang/zig/blob/0.7.x/lib/std/unicode.zig#L451 - const raw = "abc\xf0\x80\x80\x80"; - const ptr: [*]const u8 = @ptrCast([*]const u8, raw); - const list = sliceHelp(ptr, raw.len); - - try expectErr(list, 3, error.Utf8OverlongEncoding, Utf8ByteProblem.OverlongEncoding); -} - -test "validateUtf8Bytes: codepoint out too large" { - // https://github.com/ziglang/zig/blob/0.7.x/lib/std/unicode.zig#L465 - const raw = "abc\xf4\x90\x80\x80"; - const ptr: [*]const u8 = @ptrCast([*]const u8, raw); - const list = sliceHelp(ptr, raw.len); - - try expectErr(list, 3, error.Utf8CodepointTooLarge, Utf8ByteProblem.CodepointTooLarge); -} - -test "validateUtf8Bytes: surrogate halves" { - // https://github.com/ziglang/zig/blob/0.7.x/lib/std/unicode.zig#L468 - const raw = "abc\xed\xa0\x80"; - const ptr: [*]const u8 = @ptrCast([*]const u8, raw); - const list = sliceHelp(ptr, raw.len); - - try expectErr(list, 3, error.Utf8EncodesSurrogateHalf, Utf8ByteProblem.EncodesSurrogateHalf); -} - -fn isWhitespace(codepoint: u21) bool { - // https://www.unicode.org/Public/UCD/latest/ucd/PropList.txt - return switch (codepoint) { - 0x0009...0x000D => true, // control characters - 0x0020 => true, // space - 0x0085 => true, // control character - 0x00A0 => true, // no-break space - 0x1680 => true, // ogham space - 0x2000...0x200A => true, // en quad..hair space - 0x200E...0x200F => true, // left-to-right & right-to-left marks - 0x2028 => true, // line separator - 0x2029 => true, // paragraph separator - 0x202F => true, // narrow no-break space - 0x205F => true, // medium mathematical space - 0x3000 => true, // ideographic space - - else => false, - }; -} - -test "isWhitespace" { - try expect(isWhitespace(' ')); - try expect(isWhitespace('\u{00A0}')); - try expect(!isWhitespace('x')); -} - -pub fn strTrim(string: RocStr) callconv(.C) RocStr { - if (string.str_bytes) |bytes_ptr| { - const leading_bytes = countLeadingWhitespaceBytes(string); - const original_len = string.len(); - - if (original_len == leading_bytes) { - string.deinit(); - return RocStr.empty(); - } - - const trailing_bytes = countTrailingWhitespaceBytes(string); - const new_len = original_len - leading_bytes - trailing_bytes; - - const small_or_shared = new_len <= SMALL_STR_MAX_LENGTH or !string.isRefcountOne(); - if (small_or_shared) { - return RocStr.init(string.asU8ptr() + leading_bytes, new_len); - } - - // nonempty, large, and unique: - - if (leading_bytes > 0) { - var i: usize = 0; - while (i < new_len) : (i += 1) { - const dest = bytes_ptr + i; - const source = dest + leading_bytes; - @memcpy(dest, source, 1); - } - } - - var new_string = string; - new_string.str_len = new_len; - - return new_string; - } - - return RocStr.empty(); -} - -pub fn strTrimLeft(string: RocStr) callconv(.C) RocStr { - if (string.str_bytes) |bytes_ptr| { - const leading_bytes = countLeadingWhitespaceBytes(string); - const original_len = string.len(); - - if (original_len == leading_bytes) { - string.deinit(); - return RocStr.empty(); - } - - const new_len = original_len - leading_bytes; - - const small_or_shared = new_len <= SMALL_STR_MAX_LENGTH or !string.isRefcountOne(); - if (small_or_shared) { - return RocStr.init(string.asU8ptr() + leading_bytes, new_len); - } - - // nonempty, large, and unique: - - if (leading_bytes > 0) { - var i: usize = 0; - while (i < new_len) : (i += 1) { - const dest = bytes_ptr + i; - const source = dest + leading_bytes; - @memcpy(dest, source, 1); - } - } - - var new_string = string; - new_string.str_len = new_len; - - return new_string; - } - - return RocStr.empty(); -} - -pub fn strTrimRight(string: RocStr) callconv(.C) RocStr { - if (string.str_bytes) |bytes_ptr| { - const trailing_bytes = countTrailingWhitespaceBytes(string); - const original_len = string.len(); - - if (original_len == trailing_bytes) { - string.deinit(); - return RocStr.empty(); - } - - const new_len = original_len - trailing_bytes; - - const small_or_shared = new_len <= SMALL_STR_MAX_LENGTH or !string.isRefcountOne(); - if (small_or_shared) { - return RocStr.init(string.asU8ptr(), new_len); - } - - // nonempty, large, and unique: - - var i: usize = 0; - while (i < new_len) : (i += 1) { - const dest = bytes_ptr + i; - const source = dest; - @memcpy(dest, source, 1); - } - - var new_string = string; - new_string.str_len = new_len; - - return new_string; - } - - return RocStr.empty(); -} - -fn countLeadingWhitespaceBytes(string: RocStr) usize { - var byte_count: usize = 0; - - var bytes = string.asU8ptr()[0..string.len()]; - var iter = unicode.Utf8View.initUnchecked(bytes).iterator(); - while (iter.nextCodepoint()) |codepoint| { - if (isWhitespace(codepoint)) { - byte_count += unicode.utf8CodepointSequenceLength(codepoint) catch break; - } else { - break; - } - } - - return byte_count; -} - -fn countTrailingWhitespaceBytes(string: RocStr) usize { - var byte_count: usize = 0; - - var bytes = string.asU8ptr()[0..string.len()]; - var iter = ReverseUtf8View.initUnchecked(bytes).iterator(); - while (iter.nextCodepoint()) |codepoint| { - if (isWhitespace(codepoint)) { - byte_count += unicode.utf8CodepointSequenceLength(codepoint) catch break; - } else { - break; - } - } - - return byte_count; -} - -/// A backwards version of Utf8View from std.unicode -const ReverseUtf8View = struct { - bytes: []const u8, - - pub fn initUnchecked(s: []const u8) ReverseUtf8View { - return ReverseUtf8View{ .bytes = s }; - } - - pub fn iterator(s: ReverseUtf8View) ReverseUtf8Iterator { - return ReverseUtf8Iterator{ - .bytes = s.bytes, - .i = if (s.bytes.len > 0) s.bytes.len - 1 else null, - }; - } -}; - -/// A backwards version of Utf8Iterator from std.unicode -const ReverseUtf8Iterator = struct { - bytes: []const u8, - // NOTE null signifies complete/empty - i: ?usize, - - pub fn nextCodepointSlice(it: *ReverseUtf8Iterator) ?[]const u8 { - if (it.i) |index| { - var i = index; - - // NOTE this relies on the string being valid utf8 to not run off the end - while (!utf8BeginByte(it.bytes[i])) { - i -= 1; - } - - const cp_len = unicode.utf8ByteSequenceLength(it.bytes[i]) catch unreachable; - const slice = it.bytes[i .. i + cp_len]; - - it.i = if (i == 0) null else i - 1; - - return slice; - } else { - return null; - } - } - - pub fn nextCodepoint(it: *ReverseUtf8Iterator) ?u21 { - const slice = it.nextCodepointSlice() orelse return null; - - return switch (slice.len) { - 1 => @as(u21, slice[0]), - 2 => unicode.utf8Decode2(slice) catch unreachable, - 3 => unicode.utf8Decode3(slice) catch unreachable, - 4 => unicode.utf8Decode4(slice) catch unreachable, - else => unreachable, - }; - } -}; - -fn utf8BeginByte(byte: u8) bool { - return switch (byte) { - 0b1000_0000...0b1011_1111 => false, - else => true, - }; -} - -test "strTrim: empty" { - const trimmedEmpty = strTrim(RocStr.empty()); - try expect(trimmedEmpty.eq(RocStr.empty())); -} - -test "strTrim: blank" { - const original_bytes = " "; - const original = RocStr.init(original_bytes, original_bytes.len); - defer original.deinit(); - - const trimmed = strTrim(original); - - try expect(trimmed.eq(RocStr.empty())); -} - -test "strTrim: large to large" { - const original_bytes = " hello even more giant world "; - const original = RocStr.init(original_bytes, original_bytes.len); - defer original.deinit(); - - try expect(!original.isSmallStr()); - - const expected_bytes = "hello even more giant world"; - const expected = RocStr.init(expected_bytes, expected_bytes.len); - defer expected.deinit(); - - try expect(!expected.isSmallStr()); - - const trimmed = strTrim(original); - - try expect(trimmed.eq(expected)); -} - -test "strTrim: large to small" { - const original_bytes = " hello "; - const original = RocStr.init(original_bytes, original_bytes.len); - defer original.deinit(); - - try expect(!original.isSmallStr()); - - const expected_bytes = "hello"; - const expected = RocStr.init(expected_bytes, expected_bytes.len); - defer expected.deinit(); - - try expect(expected.isSmallStr()); - - const trimmed = strTrim(original); - - try expect(trimmed.eq(expected)); - try expect(trimmed.isSmallStr()); -} - -test "strTrim: small to small" { - const original_bytes = " hello "; - const original = RocStr.init(original_bytes, original_bytes.len); - defer original.deinit(); - - try expect(original.isSmallStr()); - - const expected_bytes = "hello"; - const expected = RocStr.init(expected_bytes, expected_bytes.len); - defer expected.deinit(); - - try expect(expected.isSmallStr()); - - const trimmed = strTrim(original); - - try expect(trimmed.eq(expected)); - try expect(trimmed.isSmallStr()); -} - -test "strTrimLeft: empty" { - const trimmedEmpty = strTrimLeft(RocStr.empty()); - try expect(trimmedEmpty.eq(RocStr.empty())); -} - -test "strTrimLeft: blank" { - const original_bytes = " "; - const original = RocStr.init(original_bytes, original_bytes.len); - defer original.deinit(); - - const trimmed = strTrimLeft(original); - - try expect(trimmed.eq(RocStr.empty())); -} - -test "strTrimLeft: large to large" { - const original_bytes = " hello even more giant world "; - const original = RocStr.init(original_bytes, original_bytes.len); - defer original.deinit(); - - try expect(!original.isSmallStr()); - - const expected_bytes = "hello even more giant world "; - const expected = RocStr.init(expected_bytes, expected_bytes.len); - defer expected.deinit(); - - try expect(!expected.isSmallStr()); - - const trimmed = strTrimLeft(original); - - try expect(trimmed.eq(expected)); -} - -test "strTrimLeft: large to small" { - const original_bytes = " hello "; - const original = RocStr.init(original_bytes, original_bytes.len); - defer original.deinit(); - - try expect(!original.isSmallStr()); - - const expected_bytes = "hello "; - const expected = RocStr.init(expected_bytes, expected_bytes.len); - defer expected.deinit(); - - try expect(expected.isSmallStr()); - - const trimmed = strTrimLeft(original); - - try expect(trimmed.eq(expected)); - try expect(trimmed.isSmallStr()); -} - -test "strTrimLeft: small to small" { - const original_bytes = " hello "; - const original = RocStr.init(original_bytes, original_bytes.len); - defer original.deinit(); - - try expect(original.isSmallStr()); - - const expected_bytes = "hello "; - const expected = RocStr.init(expected_bytes, expected_bytes.len); - defer expected.deinit(); - - try expect(expected.isSmallStr()); - - const trimmed = strTrimLeft(original); - - try expect(trimmed.eq(expected)); - try expect(trimmed.isSmallStr()); -} - -test "strTrimRight: empty" { - const trimmedEmpty = strTrimRight(RocStr.empty()); - try expect(trimmedEmpty.eq(RocStr.empty())); -} - -test "strTrimRight: blank" { - const original_bytes = " "; - const original = RocStr.init(original_bytes, original_bytes.len); - defer original.deinit(); - - const trimmed = strTrimRight(original); - - try expect(trimmed.eq(RocStr.empty())); -} - -test "strTrimRight: large to large" { - const original_bytes = " hello even more giant world "; - const original = RocStr.init(original_bytes, original_bytes.len); - defer original.deinit(); - - try expect(!original.isSmallStr()); - - const expected_bytes = " hello even more giant world"; - const expected = RocStr.init(expected_bytes, expected_bytes.len); - defer expected.deinit(); - - try expect(!expected.isSmallStr()); - - const trimmed = strTrimRight(original); - - try expect(trimmed.eq(expected)); -} - -test "strTrimRight: large to small" { - const original_bytes = " hello "; - const original = RocStr.init(original_bytes, original_bytes.len); - defer original.deinit(); - - try expect(!original.isSmallStr()); - - const expected_bytes = " hello"; - const expected = RocStr.init(expected_bytes, expected_bytes.len); - defer expected.deinit(); - - try expect(expected.isSmallStr()); - - const trimmed = strTrimRight(original); - - try expect(trimmed.eq(expected)); - try expect(trimmed.isSmallStr()); -} - -test "strTrimRight: small to small" { - const original_bytes = " hello "; - const original = RocStr.init(original_bytes, original_bytes.len); - defer original.deinit(); - - try expect(original.isSmallStr()); - - const expected_bytes = " hello"; - const expected = RocStr.init(expected_bytes, expected_bytes.len); - defer expected.deinit(); - - try expect(expected.isSmallStr()); - - const trimmed = strTrimRight(original); - - try expect(trimmed.eq(expected)); - try expect(trimmed.isSmallStr()); -} - -test "ReverseUtf8View: hello world" { - const original_bytes = "hello world"; - const expected_bytes = "dlrow olleh"; - - var i: usize = 0; - var iter = ReverseUtf8View.initUnchecked(original_bytes).iterator(); - while (iter.nextCodepoint()) |codepoint| { - try expect(expected_bytes[i] == codepoint); - i += 1; - } -} - -test "ReverseUtf8View: empty" { - const original_bytes = ""; - - var iter = ReverseUtf8View.initUnchecked(original_bytes).iterator(); - while (iter.nextCodepoint()) |_| { - try expect(false); - } -} diff --git a/compiler/builtins/bitcode/src/utils.zig b/compiler/builtins/bitcode/src/utils.zig deleted file mode 100644 index 2c3bdce0db..0000000000 --- a/compiler/builtins/bitcode/src/utils.zig +++ /dev/null @@ -1,310 +0,0 @@ -const std = @import("std"); -const always_inline = std.builtin.CallOptions.Modifier.always_inline; -const Monotonic = std.builtin.AtomicOrder.Monotonic; - -pub fn WithOverflow(comptime T: type) type { - return extern struct { value: T, has_overflowed: bool }; -} - -// If allocation fails, this must cxa_throw - it must not return a null pointer! -extern fn roc_alloc(size: usize, alignment: u32) callconv(.C) ?*anyopaque; - -// This should never be passed a null pointer. -// If allocation fails, this must cxa_throw - it must not return a null pointer! -extern fn roc_realloc(c_ptr: *anyopaque, new_size: usize, old_size: usize, alignment: u32) callconv(.C) ?*anyopaque; - -// This should never be passed a null pointer. -extern fn roc_dealloc(c_ptr: *anyopaque, alignment: u32) callconv(.C) void; - -// Signals to the host that the program has panicked -extern fn roc_panic(c_ptr: *anyopaque, tag_id: u32) callconv(.C) void; - -// should work just like libc memcpy (we can't assume libc is present) -extern fn roc_memcpy(dst: [*]u8, src: [*]u8, size: usize) callconv(.C) void; - -comptime { - const builtin = @import("builtin"); - // During tests, use the testing allocators to satisfy these functions. - if (builtin.is_test) { - @export(testing_roc_alloc, .{ .name = "roc_alloc", .linkage = .Strong }); - @export(testing_roc_realloc, .{ .name = "roc_realloc", .linkage = .Strong }); - @export(testing_roc_dealloc, .{ .name = "roc_dealloc", .linkage = .Strong }); - @export(testing_roc_panic, .{ .name = "roc_panic", .linkage = .Strong }); - @export(testing_roc_memcpy, .{ .name = "roc_memcpy", .linkage = .Strong }); - } -} - -fn testing_roc_alloc(size: usize, _: u32) callconv(.C) ?*anyopaque { - return @ptrCast(?*anyopaque, std.testing.allocator.alloc(u8, size) catch unreachable); -} - -fn testing_roc_realloc(c_ptr: *anyopaque, new_size: usize, old_size: usize, _: u32) callconv(.C) ?*anyopaque { - const ptr = @ptrCast([*]u8, @alignCast(2 * @alignOf(usize), c_ptr)); - const slice = ptr[0..old_size]; - - return @ptrCast(?*anyopaque, std.testing.allocator.realloc(slice, new_size) catch unreachable); -} - -fn testing_roc_dealloc(c_ptr: *anyopaque, _: u32) callconv(.C) void { - const ptr = @ptrCast([*]u8, @alignCast(2 * @alignOf(usize), c_ptr)); - - std.testing.allocator.destroy(ptr); -} - -fn testing_roc_panic(c_ptr: *anyopaque, tag_id: u32) callconv(.C) void { - _ = c_ptr; - _ = tag_id; - - @panic("Roc panicked"); -} - -fn testing_roc_memcpy(dest: *anyopaque, src: *anyopaque, bytes: usize) callconv(.C) ?*anyopaque { - const zig_dest = @ptrCast([*]u8, dest); - const zig_src = @ptrCast([*]u8, src); - - @memcpy(zig_dest, zig_src, bytes); - return dest; -} - -pub fn alloc(size: usize, alignment: u32) ?[*]u8 { - return @ptrCast(?[*]u8, @call(.{ .modifier = always_inline }, roc_alloc, .{ size, alignment })); -} - -pub fn realloc(c_ptr: [*]u8, new_size: usize, old_size: usize, alignment: u32) [*]u8 { - return @ptrCast([*]u8, @call(.{ .modifier = always_inline }, roc_realloc, .{ c_ptr, new_size, old_size, alignment })); -} - -pub fn dealloc(c_ptr: [*]u8, alignment: u32) void { - return @call(.{ .modifier = always_inline }, roc_dealloc, .{ c_ptr, alignment }); -} - -// must export this explicitly because right now it is not used from zig code -pub fn panic(c_ptr: *anyopaque, alignment: u32) callconv(.C) void { - return @call(.{ .modifier = always_inline }, roc_panic, .{ c_ptr, alignment }); -} - -pub fn memcpy(dst: [*]u8, src: [*]u8, size: usize) void { - @call(.{ .modifier = always_inline }, roc_memcpy, .{ dst, src, size }); -} - -// indirection because otherwise zig creates an alias to the panic function which our LLVM code -// does not know how to deal with -pub fn test_panic(c_ptr: *anyopaque, alignment: u32) callconv(.C) void { - _ = c_ptr; - _ = alignment; - // const cstr = @ptrCast([*:0]u8, c_ptr); - - // const stderr = std.io.getStdErr().writer(); - // stderr.print("Roc panicked: {s}!\n", .{cstr}) catch unreachable; - - // std.c.exit(1); -} - -pub const Inc = fn (?[*]u8) callconv(.C) void; -pub const IncN = fn (?[*]u8, u64) callconv(.C) void; -pub const Dec = fn (?[*]u8) callconv(.C) void; - -const REFCOUNT_MAX_ISIZE: isize = 0; -pub const REFCOUNT_ONE_ISIZE: isize = std.math.minInt(isize); -pub const REFCOUNT_ONE: usize = @bitCast(usize, REFCOUNT_ONE_ISIZE); - -pub const IntWidth = enum(u8) { - U8 = 0, - U16 = 1, - U32 = 2, - U64 = 3, - U128 = 4, - I8 = 5, - I16 = 6, - I32 = 7, - I64 = 8, - I128 = 9, -}; - -const Refcount = enum { - none, - normal, - atomic, -}; - -const RC_TYPE = Refcount.normal; - -pub fn increfC(ptr_to_refcount: *isize, amount: isize) callconv(.C) void { - if (RC_TYPE == Refcount.none) return; - var refcount = ptr_to_refcount.*; - if (refcount < REFCOUNT_MAX_ISIZE) { - switch (RC_TYPE) { - Refcount.normal => { - ptr_to_refcount.* = std.math.min(refcount + amount, REFCOUNT_MAX_ISIZE); - }, - Refcount.atomic => { - var next = std.math.min(refcount + amount, REFCOUNT_MAX_ISIZE); - while (@cmpxchgWeak(isize, ptr_to_refcount, refcount, next, Monotonic, Monotonic)) |found| { - refcount = found; - next = std.math.min(refcount + amount, REFCOUNT_MAX_ISIZE); - } - }, - Refcount.none => unreachable, - } - } -} - -pub fn decrefC( - bytes_or_null: ?[*]isize, - alignment: u32, -) callconv(.C) void { - // IMPORTANT: bytes_or_null is this case is expected to be a pointer to the refcount - // (NOT the start of the data, or the start of the allocation) - - // this is of course unsafe, but we trust what we get from the llvm side - var bytes = @ptrCast([*]isize, bytes_or_null); - - return @call(.{ .modifier = always_inline }, decref_ptr_to_refcount, .{ bytes, alignment }); -} - -pub fn decrefCheckNullC( - bytes_or_null: ?[*]u8, - alignment: u32, -) callconv(.C) void { - if (bytes_or_null) |bytes| { - const isizes: [*]isize = @ptrCast([*]isize, @alignCast(@sizeOf(isize), bytes)); - return @call(.{ .modifier = always_inline }, decref_ptr_to_refcount, .{ isizes - 1, alignment }); - } -} - -pub fn decref( - bytes_or_null: ?[*]u8, - data_bytes: usize, - alignment: u32, -) void { - if (data_bytes == 0) { - return; - } - - var bytes = bytes_or_null orelse return; - - const isizes: [*]isize = @ptrCast([*]isize, @alignCast(@sizeOf(isize), bytes)); - - decref_ptr_to_refcount(isizes - 1, alignment); -} - -inline fn decref_ptr_to_refcount( - refcount_ptr: [*]isize, - alignment: u32, -) void { - if (RC_TYPE == Refcount.none) return; - const extra_bytes = std.math.max(alignment, @sizeOf(usize)); - switch (RC_TYPE) { - Refcount.normal => { - const refcount: isize = refcount_ptr[0]; - if (refcount == REFCOUNT_ONE_ISIZE) { - dealloc(@ptrCast([*]u8, refcount_ptr) - (extra_bytes - @sizeOf(usize)), alignment); - } else if (refcount < REFCOUNT_MAX_ISIZE) { - refcount_ptr[0] = refcount - 1; - } - }, - Refcount.atomic => { - if (refcount_ptr[0] < REFCOUNT_MAX_ISIZE) { - var last = @atomicRmw(isize, &refcount_ptr[0], std.builtin.AtomicRmwOp.Sub, 1, Monotonic); - if (last == REFCOUNT_ONE_ISIZE) { - dealloc(@ptrCast([*]u8, refcount_ptr) - (extra_bytes - @sizeOf(usize)), alignment); - } - } - }, - Refcount.none => unreachable, - } -} - -pub fn allocateWithRefcountC( - data_bytes: usize, - element_alignment: u32, -) callconv(.C) [*]u8 { - return allocateWithRefcount(data_bytes, element_alignment); -} - -pub fn allocateWithRefcount( - data_bytes: usize, - element_alignment: u32, -) [*]u8 { - const ptr_width = @sizeOf(usize); - const alignment = std.math.max(ptr_width, element_alignment); - const length = alignment + data_bytes; - - var new_bytes: [*]u8 = alloc(length, alignment) orelse unreachable; - - const data_ptr = new_bytes + alignment; - const refcount_ptr = @ptrCast([*]usize, @alignCast(ptr_width, data_ptr) - ptr_width); - refcount_ptr[0] = if (RC_TYPE == Refcount.none) REFCOUNT_MAX_ISIZE else REFCOUNT_ONE; - - return data_ptr; -} - -pub const CSlice = extern struct { - pointer: *anyopaque, - len: usize, -}; - -pub fn unsafeReallocate( - source_ptr: [*]u8, - alignment: u32, - old_length: usize, - new_length: usize, - element_width: usize, -) [*]u8 { - const align_width: usize = std.math.max(alignment, @sizeOf(usize)); - - const old_width = align_width + old_length * element_width; - const new_width = align_width + new_length * element_width; - - // TODO handle out of memory - // NOTE realloc will dealloc the original allocation - const old_allocation = source_ptr - align_width; - const new_allocation = realloc(old_allocation, new_width, old_width, alignment); - - const new_source = @ptrCast([*]u8, new_allocation) + align_width; - return new_source; -} - -pub const RocResult = extern struct { - bytes: ?[*]u8, - - pub fn isOk(self: RocResult) bool { - // assumptions - // - // - the tag is the first field - // - the tag is usize bytes wide - // - Ok has tag_id 1, because Err < Ok - const usizes: [*]usize = @ptrCast([*]usize, @alignCast(@alignOf(usize), self.bytes)); - - return usizes[0] == 1; - } - - pub fn isErr(self: RocResult) bool { - return !self.isOk(); - } -}; - -pub const Ordering = enum(u8) { - EQ = 0, - GT = 1, - LT = 2, -}; - -pub const UpdateMode = enum(u8) { - Immutable = 0, - InPlace = 1, -}; - -test "increfC, refcounted data" { - var mock_rc: isize = REFCOUNT_ONE_ISIZE + 17; - var ptr_to_refcount: *isize = &mock_rc; - increfC(ptr_to_refcount, 2); - try std.testing.expectEqual(mock_rc, REFCOUNT_ONE_ISIZE + 19); -} - -test "increfC, static data" { - var mock_rc: isize = REFCOUNT_MAX_ISIZE; - var ptr_to_refcount: *isize = &mock_rc; - increfC(ptr_to_refcount, 2); - try std.testing.expectEqual(mock_rc, REFCOUNT_MAX_ISIZE); -} diff --git a/compiler/builtins/bitcode/tests/bitcode_test.rs b/compiler/builtins/bitcode/tests/bitcode_test.rs deleted file mode 100644 index 515e2d309b..0000000000 --- a/compiler/builtins/bitcode/tests/bitcode_test.rs +++ /dev/null @@ -1,64 +0,0 @@ -#[macro_use] -extern crate pretty_assertions; - -#[cfg(test)] -mod bitcode { - use roc_builtins_bitcode::{count_segments_, str_split_}; - - #[test] - fn count_segments() { - assert_eq!( - count_segments_((&"hello there").as_bytes(), (&"hello").as_bytes()), - 2 - ); - assert_eq!( - count_segments_((&"a\nb\nc").as_bytes(), (&"\n").as_bytes()), - 3 - ); - assert_eq!( - count_segments_((&"str").as_bytes(), (&"delimiter").as_bytes()), - 1 - ); - } - - #[test] - fn str_split() { - fn splits_to(string: &str, delimiter: &str, expectation: &[&[u8]]) { - assert_eq!( - str_split_( - &mut [(&"").as_bytes()].repeat(expectation.len()), - &string.as_bytes(), - &delimiter.as_bytes() - ), - expectation - ); - } - - splits_to( - "a!b!c", - "!", - &[(&"a").as_bytes(), (&"b").as_bytes(), (&"c").as_bytes()], - ); - - splits_to( - "a!?b!?c!?", - "!?", - &[ - (&"a").as_bytes(), - (&"b").as_bytes(), - (&"c").as_bytes(), - (&"").as_bytes(), - ], - ); - - splits_to("abc", "!", &[(&"abc").as_bytes()]); - - splits_to( - "tttttghittttt", - "ttttt", - &[(&"").as_bytes(), (&"ghi").as_bytes(), (&"").as_bytes()], - ); - - splits_to("def", "!!!!!!", &[(&"def").as_bytes()]); - } -} diff --git a/compiler/builtins/build.rs b/compiler/builtins/build.rs deleted file mode 100644 index 1ccdb5b4ca..0000000000 --- a/compiler/builtins/build.rs +++ /dev/null @@ -1,200 +0,0 @@ -use std::convert::AsRef; -use std::env; -use std::ffi::OsStr; -use std::fs; -use std::io; -use std::path::Path; -use std::process::Command; -use std::str; - -#[cfg(target_os = "macos")] -use tempfile::tempdir; - -/// To debug the zig code with debug prints, we need to disable the wasm code gen -const DEBUG: bool = false; - -fn zig_executable() -> String { - match std::env::var("ROC_ZIG") { - Ok(path) => path, - Err(_) => "zig".into(), - } -} - -fn main() { - println!("cargo:rerun-if-changed=build.rs"); - - // "." is relative to where "build.rs" is - // dunce can be removed once ziglang/zig#5109 is fixed - let build_script_dir_path = dunce::canonicalize(Path::new(".")).unwrap(); - let bitcode_path = build_script_dir_path.join("bitcode"); - - // workaround for github.com/ziglang/zig/issues/9711 - #[cfg(target_os = "macos")] - let zig_cache_dir = tempdir().expect("Failed to create temp directory for zig cache"); - #[cfg(target_os = "macos")] - std::env::set_var("ZIG_GLOBAL_CACHE_DIR", zig_cache_dir.path().as_os_str()); - - // LLVM .bc FILES - - generate_bc_file(&bitcode_path, "ir", "builtins-host"); - - if !DEBUG { - generate_bc_file(&bitcode_path, "ir-wasm32", "builtins-wasm32"); - } - - generate_bc_file(&bitcode_path, "ir-i386", "builtins-i386"); - - generate_bc_file(&bitcode_path, "ir-x86_64", "builtins-x86_64"); - - // OBJECT FILES - #[cfg(windows)] - const BUILTINS_HOST_FILE: &str = "builtins-host.obj"; - - #[cfg(not(windows))] - const BUILTINS_HOST_FILE: &str = "builtins-host.o"; - - generate_object_file( - &bitcode_path, - "BUILTINS_HOST_O", - "object", - BUILTINS_HOST_FILE, - ); - - generate_object_file( - &bitcode_path, - "BUILTINS_WASM32_O", - "wasm32-object", - "builtins-wasm32.o", - ); - - get_zig_files(bitcode_path.as_path(), &|path| { - let path: &Path = path; - println!( - "cargo:rerun-if-changed={}", - path.to_str().expect("Failed to convert path to str") - ); - }) - .unwrap(); - - #[cfg(target_os = "macos")] - zig_cache_dir - .close() - .expect("Failed to delete temp dir zig_cache_dir."); -} - -fn generate_object_file( - bitcode_path: &Path, - env_var_name: &str, - zig_object: &str, - object_file_name: &str, -) { - let out_dir = env::var_os("OUT_DIR").unwrap(); - - let dest_obj_path = Path::new(&out_dir).join(object_file_name); - let dest_obj = dest_obj_path.to_str().expect("Invalid dest object path"); - - // set the variable (e.g. BUILTINS_HOST_O) that is later used in - // `compiler/builtins/src/bitcode.rs` to load the object file - println!("cargo:rustc-env={}={}", env_var_name, dest_obj); - - let src_obj_path = bitcode_path.join(object_file_name); - let src_obj = src_obj_path.to_str().expect("Invalid src object path"); - - println!("Compiling zig object `{}` to: {}", zig_object, src_obj); - - if !DEBUG { - run_command( - &bitcode_path, - &zig_executable(), - &["build", zig_object, "-Drelease=true"], - ); - - println!("Moving zig object `{}` to: {}", zig_object, dest_obj); - - // we store this .o file in rust's `target` folder (for wasm we need to leave a copy here too) - fs::copy(src_obj, dest_obj).unwrap_or_else(|err| { - panic!( - "Failed to copy object file {} to {}: {:?}", - src_obj, dest_obj, err - ); - }); - } -} - -fn generate_bc_file(bitcode_path: &Path, zig_object: &str, file_name: &str) { - let mut ll_path = bitcode_path.join(file_name); - ll_path.set_extension("ll"); - let dest_ir_host = ll_path.to_str().expect("Invalid dest ir path"); - - println!("Compiling host ir to: {}", dest_ir_host); - - let mut bc_path = bitcode_path.join(file_name); - bc_path.set_extension("bc"); - let dest_bc_64bit = bc_path.to_str().expect("Invalid dest bc path"); - println!("Compiling 64-bit bitcode to: {}", dest_bc_64bit); - - // workaround for github.com/ziglang/zig/issues/9711 - #[cfg(target_os = "macos")] - let _ = fs::remove_dir_all("./bitcode/zig-cache"); - - run_command( - &bitcode_path, - &zig_executable(), - &["build", zig_object, "-Drelease=true"], - ); -} - -fn run_command + Copy>(path: P, command_str: &str, args: I) -where - I: IntoIterator, - S: AsRef, -{ - let output_result = Command::new(OsStr::new(&command_str)) - .current_dir(path) - .args(args) - .output(); - - match output_result { - Ok(output) => match output.status.success() { - true => (), - false => { - let error_str = match str::from_utf8(&output.stderr) { - Ok(stderr) => stderr.to_string(), - Err(_) => format!("Failed to run \"{}\"", command_str), - }; - - // flaky test error that only occurs sometimes inside MacOS ci run - if error_str.contains("unable to build stage1 zig object: FileNotFound") { - run_command(path, command_str, args) - } else { - panic!("{} failed: {}", command_str, error_str); - } - } - }, - Err(reason) => panic!("{} failed: {}", command_str, reason), - } -} - -fn get_zig_files(dir: &Path, cb: &dyn Fn(&Path)) -> io::Result<()> { - if dir.is_dir() { - for entry in fs::read_dir(dir)? { - let entry = entry?; - let path_buf = entry.path(); - if path_buf.is_dir() { - if !path_buf.ends_with("zig-cache") { - get_zig_files(&path_buf, cb).unwrap(); - } - } else { - let path = path_buf.as_path(); - - match path.extension() { - Some(osstr) if osstr == "zig" => { - cb(path); - } - _ => {} - } - } - } - } - Ok(()) -} diff --git a/compiler/builtins/roc/Bool.roc b/compiler/builtins/roc/Bool.roc deleted file mode 100644 index f264f19345..0000000000 --- a/compiler/builtins/roc/Bool.roc +++ /dev/null @@ -1,83 +0,0 @@ -interface Bool - exposes [Bool, and, or, not, isEq, isNotEq] - imports [] - -Bool : [True, False] - -## Returns `True` when given `True` and `True`, and `False` when either argument is `False`. -## -## `a && b` is shorthand for `Bool.and a b` -## -## >>> True && True -## -## >>> True && False -## -## >>> False && True -## -## >>> False && False -## -## ## Performance Notes -## -## In some languages, `&&` and `||` are special-cased in the compiler to skip -## evaluating the expression after the operator under certain circumstances. -## For example, in some languages, `enablePets && likesDogs user` would compile -## to the equivalent of: -## -## if enablePets then -## likesDogs user -## else -## False -## -## In Roc, however, `&&` and `||` are not special. They work the same way as -## other functions. Conditionals like `if` and `when` have a performance cost, -## and sometimes calling a function like `likesDogs user` can be faster across -## the board than doing an `if` to decide whether to skip calling it. -## -## (Naturally, if you expect the `if` to improve performance, you can always add -## one explicitly!) -and : Bool, Bool -> Bool - -## Returns `True` when given `True` for either argument, and `False` only when given `False` and `False`. -## -## `a || b` is shorthand for `Bool.or a b`. -## -## >>> True || True -## -## >>> True || False -## -## >>> False || True -## -## >>> False || False -## -## ## Performance Notes -## -## In some languages, `&&` and `||` are special-cased in the compiler to skip -## evaluating the expression after the operator under certain circumstances. -## In Roc, this is not the case. See the performance notes for [Bool.and] for details. -or : Bool, Bool -> Bool -# xor : Bool, Bool -> Bool # currently unimplemented -## Returns `False` when given `True`, and vice versa. -not : Bool -> Bool - -## Returns `True` if the two values are *structurally equal*, and `False` otherwise. -## -## `a == b` is shorthand for `Bool.isEq a b` -## -## Structural equality works as follows: -## -## 1. Tags are equal if they have the same tag name, and also their contents (if any) are equal. -## 2. Records are equal if all their fields are equal. -## 3. Collections ([Str], [List], [Dict], and [Set]) are equal if they are the same length, and also all their corresponding elements are equal. -## 4. [Num](Num#Num) values are equal if their numbers are equal, with one exception: if both arguments to `isEq` are *NaN*, then `isEq` returns `False`. See `Num.isNaN` for more about *NaN*. -## -## Note that `isEq` takes `'val` instead of `val`, which means `isEq` does not -## accept arguments whose types contain functions. -isEq : a, a -> Bool - -## Calls [isEq] on the given values, then calls [not] on the result. -## -## `a != b` is shorthand for `Bool.isNotEq a b` -## -## Note that `isNotEq` takes `'val` instead of `val`, which means `isNotEq` does not -## accept arguments whose types contain functions. -isNotEq : a, a -> Bool diff --git a/compiler/builtins/roc/Box.roc b/compiler/builtins/roc/Box.roc deleted file mode 100644 index 455ca8f49a..0000000000 --- a/compiler/builtins/roc/Box.roc +++ /dev/null @@ -1,15 +0,0 @@ -interface Box - exposes [box, unbox] - imports [] - -box : a -> Box a -unbox : Box a -> a - -# # we'd need reset/reuse for box for this to be efficient -# # that is currently not implemented -# map : Box a, (a -> b) -> Box b -# map = \boxed, transform = -# boxed -# |> Box.unbox -# |> transform -# |> Box.box diff --git a/compiler/builtins/roc/Dict.roc b/compiler/builtins/roc/Dict.roc deleted file mode 100644 index 8775e12a55..0000000000 --- a/compiler/builtins/roc/Dict.roc +++ /dev/null @@ -1,88 +0,0 @@ -interface Dict - exposes - [ - empty, - single, - get, - walk, - insert, - len, - remove, - contains, - keys, - values, - union, - intersection, - difference, - ] - imports - [ - Bool.{ Bool }, - ] - -## A [dictionary](https://en.wikipedia.org/wiki/Associative_array) that lets you can associate keys with values. -## -## ### Inserting -## -## The most basic way to use a dictionary is to start with an empty one and then: -## 1. Call [Dict.insert] passing a key and a value, to associate that key with that value in the dictionary. -## 2. Later, call [Dict.get] passing the same key as before, and it will return the value you stored. -## -## Here's an example of a dictionary which uses a city's name as the key, and its population as the associated value. -## -## populationByCity = -## Dict.empty -## |> Dict.insert "London" 8_961_989 -## |> Dict.insert "Philadelphia" 1_603_797 -## |> Dict.insert "Shanghai" 24_870_895 -## |> Dict.insert "Delhi" 16_787_941 -## |> Dict.insert "Amsterdam" 872_680 -## -## ### Accessing keys or values -## -## We can use [Dict.keys] and [Dict.values] functions to get only the keys or only the values. -## -## You may notice that these lists have the same order as the original insertion order. This will be true if -## all you ever do is [insert] and [get] operations on the dictionary, but [remove] operations can change this order. -## Let's see how that looks. -## -## ### Removing -## -## We can remove an element from the dictionary, like so: -## -## populationByCity -## |> Dict.remove "Philadelphia" -## |> Dict.keys -## == -## ["London", "Amsterdam", "Shanghai", "Delhi"] -## -## Notice that the order changed! Philadelphia has been not only removed from the list, but Amsterdam - the last -## entry we inserted - has been moved into the spot where Philadelphia was previously. This is exactly what -## [Dict.remove] does: it removes an element and moves the most recent insertion into the vacated spot. -## -## This move is done as a performance optimization, and it lets [remove] have -## [constant time complexity](https://en.wikipedia.org/wiki/Time_complexity#Constant_time). ## -## -## ### Equality -## -## When comparing two dictionaries for equality, they are `==` only if their both their contents and their -## orderings match. This preserves the property that if `dict1 == dict2`, you should be able to rely on -## `fn dict1 == fn dict2` also being `True`, even if `fn` relies on the dictionary's ordering. -## An empty dictionary. -empty : Dict k v -single : k, v -> Dict k v -get : Dict k v, k -> Result v [KeyNotFound]* -walk : Dict k v, state, (state, k, v -> state) -> state -insert : Dict k v, k, v -> Dict k v -len : Dict k v -> Nat -remove : Dict k v, k -> Dict k v -contains : Dict k v, k -> Bool - -## Returns a [List] of the dictionary's keys. -keys : Dict k v -> List k - -## Returns a [List] of the dictionary's values. -values : Dict k v -> List v -union : Dict k v, Dict k v -> Dict k v -intersection : Dict k v, Dict k v -> Dict k v -difference : Dict k v, Dict k v -> Dict k v diff --git a/compiler/builtins/roc/Encode.roc b/compiler/builtins/roc/Encode.roc deleted file mode 100644 index 0f6ea9e80a..0000000000 --- a/compiler/builtins/roc/Encode.roc +++ /dev/null @@ -1,65 +0,0 @@ -interface Encode - exposes - [ - Encoder, - Encoding, - toEncoder, - EncoderFormatting, - u8, - u16, - u32, - u64, - u128, - i8, - i16, - i32, - i64, - i128, - f32, - f64, - dec, - bool, - string, - list, - custom, - appendWith, - append, - toBytes, - ] - imports - [] - -Encoder fmt := List U8, fmt -> List U8 | fmt has EncoderFormatting - -Encoding has - toEncoder : val -> Encoder fmt | val has Encoding, fmt has EncoderFormatting - -EncoderFormatting has - u8 : U8 -> Encoder fmt | fmt has EncoderFormatting - u16 : U16 -> Encoder fmt | fmt has EncoderFormatting - u32 : U32 -> Encoder fmt | fmt has EncoderFormatting - u64 : U64 -> Encoder fmt | fmt has EncoderFormatting - u128 : U128 -> Encoder fmt | fmt has EncoderFormatting - i8 : I8 -> Encoder fmt | fmt has EncoderFormatting - i16 : I16 -> Encoder fmt | fmt has EncoderFormatting - i32 : I32 -> Encoder fmt | fmt has EncoderFormatting - i64 : I64 -> Encoder fmt | fmt has EncoderFormatting - i128 : I128 -> Encoder fmt | fmt has EncoderFormatting - f32 : F32 -> Encoder fmt | fmt has EncoderFormatting - f64 : F64 -> Encoder fmt | fmt has EncoderFormatting - dec : Dec -> Encoder fmt | fmt has EncoderFormatting - bool : Bool -> Encoder fmt | fmt has EncoderFormatting - string : Str -> Encoder fmt | fmt has EncoderFormatting - list : List elem, (elem -> Encoder fmt) -> Encoder fmt | fmt has EncoderFormatting - -custom : (List U8, fmt -> List U8) -> Encoder fmt | fmt has EncoderFormatting -custom = \encoder -> @Encoder encoder - -appendWith : List U8, Encoder fmt, fmt -> List U8 | fmt has EncoderFormatting -appendWith = \lst, @Encoder doEncoding, fmt -> doEncoding lst fmt - -append : List U8, val, fmt -> List U8 | val has Encoding, fmt has EncoderFormatting -append = \lst, val, fmt -> appendWith lst (toEncoder val) fmt - -toBytes : val, fmt -> List U8 | val has Encoding, fmt has EncoderFormatting -toBytes = \val, fmt -> appendWith [] (toEncoder val) fmt diff --git a/compiler/builtins/roc/Json.roc b/compiler/builtins/roc/Json.roc deleted file mode 100644 index a8f7059de1..0000000000 --- a/compiler/builtins/roc/Json.roc +++ /dev/null @@ -1,83 +0,0 @@ -interface Json - exposes - [ - Json, - format, - ] - imports - [ - Encode.{ - custom, - appendWith, - u8, - u16, - u32, - u64, - u128, - i8, - i16, - i32, - i64, - i128, - f32, - f64, - dec, - bool, - string, - list, - }, - ] - -Json := {} - -format = @Json {} - -numToBytes = \n -> - n |> Num.toStr |> Str.toUtf8 - -# impl EncoderFormatting for Json -u8 = \n -> custom \bytes, @Json {} -> List.concat bytes (numToBytes n) - -u16 = \n -> custom \bytes, @Json {} -> List.concat bytes (numToBytes n) - -u32 = \n -> custom \bytes, @Json {} -> List.concat bytes (numToBytes n) - -u64 = \n -> custom \bytes, @Json {} -> List.concat bytes (numToBytes n) - -u128 = \n -> custom \bytes, @Json {} -> List.concat bytes (numToBytes n) - -i8 = \n -> custom \bytes, @Json {} -> List.concat bytes (numToBytes n) - -i16 = \n -> custom \bytes, @Json {} -> List.concat bytes (numToBytes n) - -i32 = \n -> custom \bytes, @Json {} -> List.concat bytes (numToBytes n) - -i64 = \n -> custom \bytes, @Json {} -> List.concat bytes (numToBytes n) - -i128 = \n -> custom \bytes, @Json {} -> List.concat bytes (numToBytes n) - -f32 = \n -> custom \bytes, @Json {} -> List.concat bytes (numToBytes n) - -f64 = \n -> custom \bytes, @Json {} -> List.concat bytes (numToBytes n) - -dec = \n -> custom \bytes, @Json {} -> List.concat bytes (numToBytes n) - -bool = \b -> custom \bytes, @Json {} -> - if - b - then - List.concat bytes (Str.toUtf8 "true") - else - List.concat bytes (Str.toUtf8 "false") - -string = \s -> custom \bytes, @Json {} -> - List.append bytes (Num.toU8 '"') - |> List.concat (Str.toUtf8 s) - |> List.append (Num.toU8 '"') - -list = \lst, encodeElem -> - custom \bytes, @Json {} -> - head = List.append bytes (Num.toU8 '[') - withList = List.walk lst head (\bytes1, elem -> appendWith bytes1 (encodeElem elem) (@Json {})) - - List.append withList (Num.toU8 ']') diff --git a/compiler/builtins/roc/List.roc b/compiler/builtins/roc/List.roc deleted file mode 100644 index 757231d4c6..0000000000 --- a/compiler/builtins/roc/List.roc +++ /dev/null @@ -1,611 +0,0 @@ -interface List - exposes - [ - isEmpty, - get, - set, - replace, - append, - map, - len, - walkBackwards, - concat, - first, - single, - repeat, - reverse, - prepend, - join, - keepIf, - contains, - sum, - walk, - last, - keepOks, - keepErrs, - mapWithIndex, - map2, - map3, - product, - walkUntil, - range, - sortWith, - drop, - swap, - dropAt, - dropLast, - min, - max, - map4, - dropFirst, - joinMap, - any, - takeFirst, - takeLast, - find, - sublist, - intersperse, - split, - all, - dropIf, - sortAsc, - sortDesc, - ] - imports - [ - Bool.{ Bool }, - ] - -## Types -## A sequential list of values. -## -## >>> [1, 2, 3] # a list of numbers -## >>> ["a", "b", "c"] # a list of strings -## >>> [[1.1], [], [2.2, 3.3]] # a list of lists of numbers -## -## The maximum size of a [List] is limited by the amount of heap memory available -## to the current process. If there is not enough memory available, attempting to -## create the list could crash. (On Linux, where [overcommit](https://www.etalabs.net/overcommit.html) -## is normally enabled, not having enough memory could result in the list appearing -## to be created just fine, but then crashing later.) -## -## > The theoretical maximum length for a list created in Roc is half of -## > `Num.maxNat`. Attempting to create a list bigger than that -## > in Roc code will always fail, although in practice it is likely to fail -## > at much smaller lengths due to insufficient memory being available. -## -## ## Performance Details -## -## Under the hood, a list is a record containing a `len : Nat` field as well -## as a pointer to a reference count and a flat array of bytes. Unique lists -## store a capacity #Nat instead of a reference count. -## -## ## Shared Lists -## -## Shared lists are [reference counted](https://en.wikipedia.org/wiki/Reference_counting). -## -## Each time a given list gets referenced, its reference count ("refcount" for short) -## gets incremented. Each time a list goes out of scope, its refcount count gets -## decremented. Once a refcount, has been decremented more times than it has been -## incremented, we know nothing is referencing it anymore, and the list's memory -## will be immediately freed. -## -## Let's look at an example. -## -## ratings = [5, 4, 3] -## -## { foo: ratings, bar: ratings } -## -## The first line binds the name `ratings` to the list `[5, 4, 3]`. The list -## begins with a refcount of 1, because so far only `ratings` is referencing it. -## -## The second line alters this refcount. `{ foo: ratings` references -## the `ratings` list, which will result in its refcount getting incremented -## from 0 to 1. Similarly, `bar: ratings }` also references the `ratings` list, -## which will result in its refcount getting incremented from 1 to 2. -## -## Let's turn this example into a function. -## -## getRatings = \first -> -## ratings = [first, 4, 3] -## -## { foo: ratings, bar: ratings } -## -## getRatings 5 -## -## At the end of the `getRatings` function, when the record gets returned, -## the original `ratings =` binding has gone out of scope and is no longer -## accessible. (Trying to reference `ratings` outside the scope of the -## `getRatings` function would be an error!) -## -## Since `ratings` represented a way to reference the list, and that way is no -## longer accessible, the list's refcount gets decremented when `ratings` goes -## out of scope. It will decrease from 2 back down to 1. -## -## Putting these together, when we call `getRatings 5`, what we get back is -## a record with two fields, `foo`, and `bar`, each of which refers to the same -## list, and that list has a refcount of 1. -## -## Let's change the last line to be `(getRatings 5).bar` instead of `getRatings 5`: -## -## getRatings = \first -> -## ratings = [first, 4, 3] -## -## { foo: ratings, bar: ratings } -## -## (getRatings 5).bar -## -## Now, when this expression returns, only the `bar` field of the record will -## be returned. This will mean that the `foo` field becomes inaccessible, causing -## the list's refcount to get decremented from 2 to 1. At this point, the list is back -## where it started: there is only 1 reference to it. -## -## Finally let's suppose the final line were changed to this: -## -## List.first (getRatings 5).bar -## -## This call to [List.first] means that even the list in the `bar` field has become -## inaccessible. As such, this line will cause the list's refcount to get -## decremented all the way to 0. At that point, nothing is referencing the list -## anymore, and its memory will get freed. -## -## Things are different if this is a list of lists instead of a list of numbers. -## Let's look at a simpler example using [List.first] - first with a list of numbers, -## and then with a list of lists, to see how they differ. -## -## Here's the example using a list of numbers. -## -## nums = [1, 2, 3, 4, 5, 6, 7] -## -## first = List.first nums -## last = List.last nums -## -## first -## -## It makes a list, calls [List.first] and [List.last] on it, and then returns `first`. -## -## Here's the equivalent code with a list of lists: -## -## lists = [[1], [2, 3], [], [4, 5, 6, 7]] -## -## first = List.first lists -## last = List.last lists -## -## first -## -## TODO explain how in the former example, when we go to free `nums` at the end, -## we can free it immediately because there are no other refcounts. However, -## in the case of `lists`, we have to iterate through the list and decrement -## the refcounts of each of its contained lists - because they, too, have -## refcounts! Importantly, because the first element had its refcount incremented -## because the function returned `first`, that element will actually end up -## *not* getting freed at the end - but all the others will be. -## -## In the `lists` example, `lists = [...]` also creates a list with an initial -## refcount of 1. Separately, it also creates several other lists - each with -## their own refcounts - to go inside that list. (The empty list at the end -## does not use heap memory, and thus has no refcount.) -## -## At the end, we once again call [List.first] on the list, but this time -## -## * Copying small lists (64 elements or fewer) is typically slightly faster than copying small persistent data structures. This is because, at small sizes, persistent data structures tend to be thin wrappers around flat arrays anyway. They don't have any copying advantage until crossing a certain minimum size threshold. -## * Even when copying is faster, other list operations may still be slightly slower with persistent data structures. For example, even if it were a persistent data structure, [List.map], [List.walk], and [List.keepIf] would all need to traverse every element in the list and build up the result from scratch. These operations are all -## * Roc's compiler optimizes many list operations into in-place mutations behind the scenes, depending on how the list is being used. For example, [List.map], [List.keepIf], and [List.set] can all be optimized to perform in-place mutations. -## * If possible, it is usually best for performance to use large lists in a way where the optimizer can turn them into in-place mutations. If this is not possible, a persistent data structure might be faster - but this is a rare enough scenario that it would not be good for the average Roc program's performance if this were the way [List] worked by default. Instead, you can look outside Roc's standard modules for an implementation of a persistent data structure - likely built using [List] under the hood! -## Check if the list is empty. -## -## >>> List.isEmpty [1, 2, 3] -## -## >>> List.isEmpty [] -isEmpty : List a -> Bool -isEmpty = \list -> - List.len list == 0 - -get : List a, Nat -> Result a [OutOfBounds]* -replace : List a, Nat, a -> { list : List a, value : a } - -## Replaces the element at the given index with a replacement. -## -## >>> List.set ["a", "b", "c"] 1 "B" -## -## If the given index is outside the bounds of the list, returns the original -## list unmodified. -## -## To drop the element at a given index, instead of replacing it, see [List.dropAt]. -set : List a, Nat, a -> List a -set = \list, index, value -> - (List.replace list index value).list - -## Add a single element to the end of a list. -## -## >>> List.append [1, 2, 3] 4 -## -## >>> [0, 1, 2] -## >>> |> List.append 3 -append : List a, a -> List a - -## Add a single element to the beginning of a list. -## -## >>> List.prepend [1, 2, 3] 0 -## -## >>> [2, 3, 4] -## >>> |> List.prepend 1 -prepend : List a, a -> List a - -## Returns the length of the list - the number of elements it contains. -## -## One [List] can store up to 2,147,483,648 elements (just over 2 billion), which -## is exactly equal to the highest valid #I32 value. This means the #U32 this function -## returns can always be safely converted to an #I32 without losing any data. -len : List a -> Nat - -## Put two lists together. -## -## >>> List.concat [1, 2, 3] [4, 5] -## -## >>> [0, 1, 2] -## >>> |> List.concat [3, 4] -concat : List a, List a -> List a - -## Returns the last element in the list, or `ListWasEmpty` if it was empty. -last : List a -> Result a [ListWasEmpty]* - -## A list with a single element in it. -## -## This is useful in pipelines, like so: -## -## websites = -## Str.concat domain ".com" -## |> List.single -## -single : a -> List a -## Returns a list with the given length, where every element is the given value. -## -## -repeat : a, Nat -> List a - -## Returns the list with its elements reversed. -## -## >>> List.reverse [1, 2, 3] -reverse : List a -> List a - -## Join the given lists together into one list. -## -## >>> List.join [[1, 2, 3], [4, 5], [], [6, 7]] -## -## >>> List.join [[], []] -## -## >>> List.join [] -join : List (List a) -> List a -contains : List a, a -> Bool - -## Build a value using each element in the list. -## -## Starting with a given `state` value, this walks through each element in the -## list from first to last, running a given `step` function on that element -## which updates the `state`. It returns the final `state` at the end. -## -## You can use it in a pipeline: -## -## [2, 4, 8] -## |> List.walk { start: 0, step: Num.add } -## -## This returns 14 because: -## * `state` starts at 0 (because of `start: 0`) -## * Each `step` runs `Num.add state elem`, and the return value becomes the new `state`. -## -## Here is a table of how `state` changes as [List.walk] walks over the elements -## `[2, 4, 8]` using #Num.add as its `step` function to determine the next `state`. -## -## `state` | `elem` | `step state elem` (`Num.add state elem`) -## --------+--------+----------------------------------------- -## 0 | | -## 0 | 2 | 2 -## 2 | 4 | 6 -## 6 | 8 | 14 -## -## So `state` goes through these changes: -## 1. `0` (because of `start: 0`) -## 2. `1` (because of `Num.add state elem` with `state` = 0 and `elem` = 1 -## -## [1, 2, 3] -## |> List.walk { start: 0, step: Num.sub } -## -## This returns -6 because -## -## Note that in other languages, `walk` is sometimes called `reduce`, -## `fold`, `foldLeft`, or `foldl`. -walk : List elem, state, (state, elem -> state) -> state - -## Note that in other languages, `walkBackwards` is sometimes called `reduceRight`, -## `fold`, `foldRight`, or `foldr`. -walkBackwards : List elem, state, (state, elem -> state) -> state - -## Same as [List.walk], except you can stop walking early. -## -## ## Performance Details -## -## Compared to [List.walk], this can potentially visit fewer elements (which can -## improve performance) at the cost of making each step take longer. -## However, the added cost to each step is extremely small, and can easily -## be outweighed if it results in skipping even a small number of elements. -## -## As such, it is typically better for performance to use this over [List.walk] -## if returning `Done` earlier than the last element is expected to be common. -walkUntil : List elem, state, (state, elem -> [Continue state, Stop state]) -> state - -sum : List (Num a) -> Num a -sum = \list -> - List.walk list 0 Num.add - -product : List (Num a) -> Num a -product = \list -> - List.walk list 1 Num.mul - -## Run the given predicate on each element of the list, returning `True` if -## any of the elements satisfy it. -any : List a, (a -> Bool) -> Bool - -## Run the given predicate on each element of the list, returning `True` if -## all of the elements satisfy it. -all : List a, (a -> Bool) -> Bool - -## Run the given function on each element of a list, and return all the -## elements for which the function returned `True`. -## -## >>> List.keepIf [1, 2, 3, 4] (\num -> num > 2) -## -## ## Performance Details -## -## [List.keepIf] always returns a list that takes up exactly the same amount -## of memory as the original, even if its length decreases. This is because it -## can't know in advance exactly how much space it will need, and if it guesses a -## length that's too low, it would have to re-allocate. -## -## (If you want to do an operation like this which reduces the memory footprint -## of the resulting list, you can do two passes over the lis with [List.walk] - one -## to calculate the precise new size, and another to populate the new list.) -## -## If given a unique list, [List.keepIf] will mutate it in place to assemble the appropriate list. -## If that happens, this function will not allocate any new memory on the heap. -## If all elements in the list end up being kept, Roc will return the original -## list unaltered. -## -keepIf : List a, (a -> Bool) -> List a - -## Run the given function on each element of a list, and return all the -## elements for which the function returned `False`. -## -## >>> List.dropIf [1, 2, 3, 4] (\num -> num > 2) -## -## ## Performance Details -## -## `List.dropIf` has the same performance characteristics as [List.keepIf]. -## See its documentation for details on those characteristics! -dropIf : List a, (a -> Bool) -> List a -dropIf = \list, predicate -> - List.keepIf list (\e -> Bool.not (predicate e)) - -## This works like [List.map], except only the transformed values that are -## wrapped in `Ok` are kept. Any that are wrapped in `Err` are dropped. -## -## >>> List.keepOks [["a", "b"], [], [], ["c", "d", "e"]] List.last -## -## >>> fn = \str -> if Str.isEmpty str then Err StrWasEmpty else Ok (Str.len str) -## >>> -## >>> List.keepOks ["", "a", "bc", "", "d", "ef", ""] -keepOks : List before, (before -> Result after *) -> List after - -## This works like [List.map], except only the transformed values that are -## wrapped in `Err` are kept. Any that are wrapped in `Ok` are dropped. -## -## >>> List.keepErrs [["a", "b"], [], [], ["c", "d", "e"]] List.last -## -## >>> fn = \str -> if Str.isEmpty str then Err StrWasEmpty else Ok (Str.len str) -## >>> -## >>> List.keepErrs ["", "a", "bc", "", "d", "ef", ""] -keepErrs : List before, (before -> Result * after) -> List after - -## Convert each element in the list to something new, by calling a conversion -## function on each of them. Then return a new list of the converted values. -## -## > List.map [1, 2, 3] (\num -> num + 1) -## -## > List.map ["", "a", "bc"] Str.isEmpty -map : List a, (a -> b) -> List b - -## Run a transformation function on the first element of each list, -## and use that as the first element in the returned list. -## Repeat until a list runs out of elements. -## -## Some languages have a function named `zip`, which does something similar to -## calling [List.map2] passing two lists and `Pair`: -## -## >>> zipped = List.map2 ["a", "b", "c"] [1, 2, 3] Pair -map2 : List a, List b, (a, b -> c) -> List c - -## Run a transformation function on the first element of each list, -## and use that as the first element in the returned list. -## Repeat until a list runs out of elements. -map3 : List a, List b, List c, (a, b, c -> d) -> List d - -## Run a transformation function on the first element of each list, -## and use that as the first element in the returned list. -## Repeat until a list runs out of elements. -map4 : List a, List b, List c, List d, (a, b, c, d -> e) -> List e - -## This works like [List.map], except it also passes the index -## of the element to the conversion function. -mapWithIndex : List a, (a, Nat -> b) -> List b - -## Returns a list of all the integers between one and another, -## including both of the given numbers. -## -## >>> List.range 2 8 -range : Int a, Int a -> List (Int a) -sortWith : List a, (a, a -> [LT, EQ, GT]) -> List a - -## Sorts a list in ascending order (lowest to highest), using a function which -## specifies a way to represent each element as a number. -## -## To sort in descending order (highest to lowest), use [List.sortDesc] instead. -sortAsc : List (Num a) -> List (Num a) -sortAsc = \list -> List.sortWith list Num.compare - -## Sorts a list in descending order (highest to lowest), using a function which -## specifies a way to represent each element as a number. -## -## To sort in ascending order (lowest to highest), use [List.sortAsc] instead. -sortDesc : List (Num a) -> List (Num a) -sortDesc = \list -> List.sortWith list (\a, b -> Num.compare b a) - -swap : List a, Nat, Nat -> List a - -## Returns the first element in the list, or `ListWasEmpty` if it was empty. -first : List a -> Result a [ListWasEmpty]* - -## Remove the first element from the list. -## -## Returns the new list (with the removed element missing). -dropFirst : List elem -> List elem - -## Remove the last element from the list. -## -## Returns the new list (with the removed element missing). -dropLast : List elem -> List elem - -## Returns the given number of elements from the beginning of the list. -## -## >>> List.takeFirst 4 [1, 2, 3, 4, 5, 6, 7, 8] -## -## If there are fewer elements in the list than the requested number, -## returns the entire list. -## -## >>> List.takeFirst 5 [1, 2] -## -## To *remove* elements from the beginning of the list, use `List.takeLast`. -## -## To remove elements from both the beginning and end of the list, -## use `List.sublist`. -## -## To split the list into two lists, use `List.split`. -## -## ## Performance Details -## -## When given a Unique list, this runs extremely fast. It sets the list's length -## to the given length value, and frees the leftover elements. This runs very -## slightly faster than `List.takeLast`. -## -## In fact, `List.takeFirst 1 list` runs faster than `List.first list` when given -## a Unique list, because [List.first] returns the first element as well - -## which introduces a conditional bounds check as well as a memory load. -takeFirst : List elem, Nat -> List elem - -## Returns the given number of elements from the end of the list. -## -## >>> List.takeLast 4 [1, 2, 3, 4, 5, 6, 7, 8] -## -## If there are fewer elements in the list than the requested number, -## returns the entire list. -## -## >>> List.takeLast 5 [1, 2] -## -## To *remove* elements from the end of the list, use `List.takeFirst`. -## -## To remove elements from both the beginning and end of the list, -## use `List.sublist`. -## -## To split the list into two lists, use `List.split`. -## -## ## Performance Details -## -## When given a Unique list, this runs extremely fast. It moves the list's -## pointer to the index at the given length value, updates its length, -## and frees the leftover elements. This runs very nearly as fast as -## `List.takeFirst` on a Unique list. -## -## In fact, `List.takeLast 1 list` runs faster than `List.first list` when given -## a Unique list, because [List.first] returns the first element as well - -## which introduces a conditional bounds check as well as a memory load. -takeLast : List elem, Nat -> List elem - -## Drops n elements from the beginning of the list. -drop : List elem, Nat -> List elem - -## Drops the element at the given index from the list. -## -## This has no effect if the given index is outside the bounds of the list. -## -## To replace the element at a given index, instead of dropping it, see [List.set]. -dropAt : List elem, Nat -> List elem - -min : List (Num a) -> Result (Num a) [ListWasEmpty]* -min = \list -> - when List.first list is - Ok initial -> - Ok (minHelp list initial) - Err ListWasEmpty -> - Err ListWasEmpty - -minHelp : List (Num a), Num a -> Num a -minHelp = \list, initial -> - List.walk list initial \bestSoFar, current -> - if current < bestSoFar then - current - else - bestSoFar - -max : List (Num a) -> Result (Num a) [ListWasEmpty]* -max = \list -> - when List.first list is - Ok initial -> - Ok (maxHelp list initial) - Err ListWasEmpty -> - Err ListWasEmpty - -maxHelp : List (Num a), Num a -> Num a -maxHelp = \list, initial -> - List.walk list initial \bestSoFar, current -> - if current > bestSoFar then - current - else - bestSoFar - -## Like [List.map], except the transformation function wraps the return value -## in a list. At the end, all the lists get joined together into one list. -## -## You may know a similar function named `concatMap` in other languages. -joinMap : List a, (a -> List b) -> List b -joinMap = \list, mapper -> - List.walk list [] (\state, elem -> List.concat state (mapper elem)) - -## Returns the first element of the list satisfying a predicate function. -## If no satisfying element is found, an `Err NotFound` is returned. -find : List elem, (elem -> Bool) -> Result elem [NotFound]* - -## Returns a subsection of the given list, beginning at the `start` index and -## including a total of `len` elements. -## -## If `start` is outside the bounds of the given list, returns the empty list. -## -## >>> List.sublist { start: 4, len: 0 } [1, 2, 3] -## -## If more elements are requested than exist in the list, returns as many as it can. -## -## >>> List.sublist { start: 2, len: 10 } [1, 2, 3, 4, 5] -## -## > If you want a sublist which goes all the way to the end of the list, no -## > matter how long the list is, `List.takeLast` can do that more efficiently. -## -## Some languages have a function called **`slice`** which works similarly to this. -sublist : List elem, { start : Nat, len : Nat } -> List elem -intersperse : List elem, elem -> List elem - -## Splits the list into two lists, around the given index. -## -## The returned lists are labeled `before` and `others`. The `before` list will -## contain all the elements whose index in the original list was **less than** -## than the given index, # and the `others` list will be all the others. (This -## means if you give an index of 0, the `before` list will be empty and the -## `others` list will have the same elements as the original list.) -split : List elem, Nat -> { before : List elem, others : List elem } diff --git a/compiler/builtins/roc/Num.roc b/compiler/builtins/roc/Num.roc deleted file mode 100644 index 3a175c4393..0000000000 --- a/compiler/builtins/roc/Num.roc +++ /dev/null @@ -1,1290 +0,0 @@ -interface Num - exposes - [ - Num, - Int, - Frac, - Integer, - FloatingPoint, - I128, - I64, - I32, - I16, - I8, - U128, - U64, - U32, - U16, - U8, - Signed128, - Signed64, - Signed32, - Signed16, - Signed8, - Unsigned128, - Unsigned64, - Unsigned32, - Unsigned16, - Unsigned8, - Nat, - Dec, - F32, - F64, - Natural, - Decimal, - Binary32, - Binary64, - abs, - neg, - add, - sub, - mul, - isLt, - isLte, - isGt, - isGte, - sin, - cos, - tan, - atan, - acos, - asin, - isZero, - isEven, - isOdd, - toFrac, - isPositive, - isNegative, - rem, - remChecked, - div, - divChecked, - sqrt, - sqrtChecked, - log, - logChecked, - round, - ceiling, - floor, - compare, - pow, - powInt, - addWrap, - addChecked, - addSaturated, - bitwiseAnd, - bitwiseXor, - bitwiseOr, - shiftLeftBy, - shiftRightBy, - shiftRightZfBy, - subWrap, - subChecked, - subSaturated, - mulWrap, - mulChecked, - intCast, - bytesToU16, - bytesToU32, - divCeil, - divCeilChecked, - divTrunc, - divTruncChecked, - toStr, - isMultipleOf, - minI8, - maxI8, - minU8, - maxU8, - minI16, - maxI16, - minU16, - maxU16, - minI32, - maxI32, - minU32, - maxU32, - minI64, - maxI64, - minU64, - maxU64, - minI128, - maxI128, - minU128, - maxU128, - minF32, - maxF32, - minF64, - maxF64, - toI8, - toI8Checked, - toI16, - toI16Checked, - toI32, - toI32Checked, - toI64, - toI64Checked, - toI128, - toI128Checked, - toU8, - toU8Checked, - toU16, - toU16Checked, - toU32, - toU32Checked, - toU64, - toU64Checked, - toU128, - toU128Checked, - toNat, - toNatChecked, - toF32, - toF32Checked, - toF64, - toF64Checked, - ] - imports - [ - Bool.{ Bool }, - ] - -## Represents a number that could be either an [Int] or a [Frac]. -## -## This is useful for functions that can work on either, for example #Num.add, whose type is: -## -## ``` -## add : Num a, Num a -> Num a -## ``` -## -## The number 1.5 technically has the type `Num (Fraction *)`, so when you pass -## two of them to [Num.add], the answer you get is `3.0 : Num (Fraction *)`. -## -## Similarly, the number 0x1 (that is, the integer 1 in hexadecimal notation) -## technically has the type `Num (Integer *)`, so when you pass two of them to -## [Num.add], the answer you get is `2 : Num (Integer *)`. -## -## The type [`Frac a`](#Frac) is defined to be an alias for `Num (Fraction a)`, -## so `3.0 : Num (Fraction *)` is the same value as `3.0 : Frac *`. -## Similarly, the type [`Int a`](#Int) is defined to be an alias for -## `Num (Integer a)`, so `2 : Num (Integer *)` is the same value as -## `2 : Int *`. -## -## In this way, the [Num] type makes it possible to have `1 + 0x1` return -## `2 : Int *` and `1.5 + 1.5` return `3.0 : Frac`. -## -## ## Number Literals -## -## Number literals without decimal points (like `0`, `4` or `360`) -## have the type `Num *` at first, but usually end up taking on -## a more specific type based on how they're used. -## -## For example, in `(1 + List.len myList)`, the `1` has the type `Num *` at first, -## but because `List.len` returns a `Nat`, the `1` ends up changing from -## `Num *` to the more specific `Nat`, and the expression as a whole -## ends up having the type `Nat`. -## -## Sometimes number literals don't become more specific. For example, -## the `Num.toStr` function has the type `Num * -> Str`. This means that -## when calling `Num.toStr (5 + 6)`, the expression `(5 + 6)` -## still has the type `Num *`. When this happens, `Num *` defaults to -## being an [I64] - so this addition expression would overflow -## if either 5 or 6 were replaced with a number big enough to cause -## addition overflow on an [I64] value. -## -## If this default of [I64] is not big enough for your purposes, -## you can add an `i128` to the end of the number literal, like so: -## -## >>> Num.toStr 5_000_000_000i128 -## -## This `i128` suffix specifies that you want this number literal to be -## an [I128] instead of a `Num *`. All the other numeric types have -## suffixes just like `i128`; here are some other examples: -## -## * `215u8` is a `215` value of type [U8] -## * `76.4f32` is a `76.4` value of type [F32] -## * `123.45dec` is a `123.45` value of type [Dec] -## * `12345nat` is a `12345` value of type [Nat] -## -## In practice, these are rarely needed. It's most common to write -## number literals without any suffix. -Num range := range - -## A fixed-size integer - that is, a number with no fractional component. -## -## Integers come in two flavors: signed and unsigned. Signed integers can be -## negative ("signed" refers to how they can incorporate a minus sign), -## whereas unsigned integers cannot be negative. -## -## Since integers have a fixed size, the size you choose determines both the -## range of numbers it can represent, and also how much memory it takes up. -## -## [U8] is an an example of an integer. It is an unsigned [Int] that takes up 8 bits -## (aka 1 byte) in memory. The `U` is for Unsigned and the 8 is for 8 bits. -## Because it has 8 bits to work with, it can store 256 numbers (2^8), -## and because it is unsigned, its min value is 0. This means the 256 numbers -## it can store range from 0 to 255. -## -## [I8] is a signed integer that takes up 8 bits. The `I` is for Integer, since -## integers in mathematics are signed by default. Because it has 8 bits just -## like [U8], it can store 256 numbers (still 2^16), but because it is signed, -## the range is different. Its 256 numbers range from -128 to 127. -## -## Here are some other examples: -## -## * [U16] is like [U8], except it takes up 16 bits in memory. It can store 65,536 numbers (2^16), ranging from 0 to 65,536. -## * [I16] is like [U16], except it is signed. It can still store the same 65,536 numbers (2^16), ranging from -32,768 to 32,767. -## -## This pattern continues up to [U128] and [I128]. -## -## ## Performance notes -## -## In general, using smaller numeric sizes means your program will use less memory. -## However, if a mathematical operation results in an answer that is too big -## or too small to fit in the size available for that answer (which is typically -## the same size as the inputs), then you'll get an overflow error. -## -## As such, minimizing memory usage without causing overflows involves choosing -## number sizes based on your knowledge of what numbers you expect your program -## to encounter at runtime. -## -## Minimizing memory usage does not imply maximum runtime speed! -## CPUs are typically fastest at performing integer operations on integers that -## are the same size as that CPU's native machine word size. That means a 64-bit -## CPU is typically fastest at executing instructions on [U64] and [I64] values, -## whereas a 32-bit CPU is typically fastest on [U32] and [I32] values. -## -## Putting these factors together, here are some reasonable guidelines for optimizing performance through integer size choice: -## -## * Start by deciding if this integer should allow negative numbers, and choose signed or unsigned accordingly. -## * Next, think about the range of numbers you expect this number to hold. Choose the smallest size you will never expect to overflow, no matter the inputs your program receives. (Validating inputs for size, and presenting the user with an error if they are too big, can help guard against overflow.) -## * Finally, if a particular numeric calculation is running too slowly, you can try experimenting with other number sizes. This rarely makes a meaningful difference, but some processors can operate on different number sizes at different speeds. -## -## All number literals without decimal points are compatible with [Int] values. -## -## >>> 1 -## -## >>> 0 -## -## You can optionally put underscores in your [Int] literals. -## They have no effect on the number's value, but can make large numbers easier to read. -## -## >>> 1_000_000 -## -## Integers come in two flavors: *signed* and *unsigned*. -## -## * *Unsigned* integers can never be negative. The lowest value they can hold is zero. -## * *Signed* integers can be negative. -## -## Integers also come in different sizes. Choosing a size depends on your performance -## needs and the range of numbers you need to represent. At a high level, the -## general trade-offs are: -## -## * Larger integer sizes can represent a wider range of numbers. If you absolutely need to represent numbers in a certain range, make sure to pick an integer size that can hold them! -## * Smaller integer sizes take up less memory. This savings rarely matters in variables and function arguments, but the sizes of integers that you use in data structures can add up. This can also affect whether those data structures fit in [cache lines](https://en.wikipedia.org/wiki/CPU_cache#Cache_performance), which can be a performance bottleneck. -## * Certain CPUs work faster on some numeric sizes than others. If the CPU is taking too long to run numeric calculations, you may find a performance improvement by experimenting with numeric sizes that are larger than otherwise necessary. However, in practice, doing this typically degrades overall performance, so be careful to measure properly! -## -## Here are the different fixed size integer types: -## -## | Range | Type | Size | -## |--------------------------------------------------------|-------|----------| -## | ` -128` | [I8] | 1 Byte | -## | ` 127` | | | -## |--------------------------------------------------------|-------|----------| -## | ` 0` | [U8] | 1 Byte | -## | ` 255` | | | -## |--------------------------------------------------------|-------|----------| -## | ` -32_768` | [I16] | 2 Bytes | -## | ` 32_767` | | | -## |--------------------------------------------------------|-------|----------| -## | ` 0` | [U16] | 2 Bytes | -## | ` 65_535` | | | -## |--------------------------------------------------------|-------|----------| -## | ` -2_147_483_648` | [I32] | 4 Bytes | -## | ` 2_147_483_647` | | | -## |--------------------------------------------------------|-------|----------| -## | ` 0` | [U32] | 4 Bytes | -## | ` (over 4 billion) 4_294_967_295` | | | -## |--------------------------------------------------------|-------|----------| -## | ` -9_223_372_036_854_775_808` | [I64] | 8 Bytes | -## | ` 9_223_372_036_854_775_807` | | | -## |--------------------------------------------------------|-------|----------| -## | ` 0` | [U64] | 8 Bytes | -## | ` (over 18 quintillion) 18_446_744_073_709_551_615` | | | -## |--------------------------------------------------------|-------|----------| -## | `-170_141_183_460_469_231_731_687_303_715_884_105_728` | [I128]| 16 Bytes | -## | ` 170_141_183_460_469_231_731_687_303_715_884_105_727` | | | -## |--------------------------------------------------------|-------|----------| -## | ` (over 340 undecillion) 0` | [U128]| 16 Bytes | -## | ` 340_282_366_920_938_463_463_374_607_431_768_211_455` | | | -## -## Roc also has one variable-size integer type: [Nat]. The size of [Nat] is equal -## to the size of a memory address, which varies by system. For example, when -## compiling for a 64-bit system, [Nat] is the same as [U64]. When compiling for a -## 32-bit system, it's the same as [U32]. -## -## A common use for [Nat] is to store the length ("len" for short) of a -## collection like a [List]. 64-bit systems can represent longer -## lists in memory than 32-bit systems can, which is why the length of a list -## is represented as a [Nat] in Roc. -## -## If any operation would result in an [Int] that is either too big -## or too small to fit in that range (e.g. calling `Num.maxI32 + 1`), -## then the operation will *overflow*. When an overflow occurs, the program will crash. -## -## As such, it's very important to design your code not to exceed these bounds! -## If you need to do math outside these bounds, consider using a larger numeric size. -Int range : Num (Integer range) - -## A fixed-size number with a fractional component. -## -## Roc fractions come in two flavors: fixed-point base-10 and floating-point base-2. -## -## * [Dec] is a 128-bit [fixed-point](https://en.wikipedia.org/wiki/Fixed-point_arithmetic) base-10 number. It's a great default choice, especially when precision is important - for example when representing currency. With [Dec], 0.1 + 0.2 returns 0.3. -## * [F64] and [F32] are [floating-point](https://en.wikipedia.org/wiki/Floating-point_arithmetic) base-2 numbers. They sacrifice precision for lower memory usage and improved performance on some operations. This makes them a good fit for representing graphical coordinates. With [F64], 0.1 + 0.2 returns 0.3000000000000000444089209850062616169452667236328125. -## -## If you don't specify a type, Roc will default to using [Dec] because it's -## the least error-prone overall. For example, suppose you write this: -## -## wasItPrecise = 0.1 + 0.2 == 0.3 -## -## The value of `wasItPrecise` here will be `True`, because Roc uses [Dec] -## by default when there are no types specified. -## -## In contrast, suppose we use `f32` or `f64` for one of these numbers: -## -## wasItPrecise = 0.1f64 + 0.2 == 0.3 -## -## Here, `wasItPrecise` will be `False` because the entire calculation will have -## been done in a base-2 floating point calculation, which causes noticeable -## precision loss in this case. -## -## The floating-point numbers ([F32] and [F64]) also have three values which -## are not ordinary [finite numbers](https://en.wikipedia.org/wiki/Finite_number). -## They are: -## * ∞ ([infinity](https://en.wikipedia.org/wiki/Infinity)) -## * -∞ (negative infinity) -## * *NaN* ([not a number](https://en.wikipedia.org/wiki/NaN)) -## -## These values are different from ordinary numbers in that they only occur -## when a floating-point calculation encounters an error. For example: -## * Dividing a positive [F64] by `0.0` returns ∞. -## * Dividing a negative [F64] by `0.0` returns -∞. -## * Dividing a [F64] of `0.0` by `0.0` returns [*NaN*](Num.isNaN). -## -## These rules come from the [IEEE-754](https://en.wikipedia.org/wiki/IEEE_754) -## floating point standard. Because almost all modern processors are built to -## this standard, deviating from these rules has a significant performance -## cost! Since the most common reason to choose [F64] or [F32] over [Dec] is -## access to hardware-accelerated performance, Roc follows these rules exactly. -## -## There's no literal syntax for these error values, but you can check to see if -## you ended up with one of them by using [isNaN], [isFinite], and [isInfinite]. -## Whenever a function in this module could return one of these values, that -## possibility is noted in the function's documentation. -## -## ## Performance Notes -## -## On typical modern CPUs, performance is similar between [Dec], [F64], and [F32] -## for addition and subtraction. For example, [F32] and [F64] do addition using -## a single CPU floating-point addition instruction, which typically takes a -## few clock cycles to complete. In contrast, [Dec] does addition using a few -## CPU integer arithmetic instructions, each of which typically takes only one -## clock cycle to complete. Exact numbers will vary by CPU, but they should be -## similar overall. -## -## [Dec] is significantly slower for multiplication and division. It not only -## needs to do more arithmetic instructions than [F32] and [F64] do, but also -## those instructions typically take more clock cycles to complete. -## -## With [Num.sqrt] and trigonometry functions like [Num.cos], there is -## an even bigger performance difference. [F32] and [F64] can do these in a -## single instruction, whereas [Dec] needs entire custom procedures - which use -## loops and conditionals. If you need to do performance-critical trigonometry -## or square roots, either [F64] or [F32] is probably a better choice than the -## usual default choice of [Dec], despite the precision problems they bring. -Frac range : Num (FloatingPoint range) - -Signed128 := [] -Signed64 := [] -Signed32 := [] -Signed16 := [] -Signed8 := [] - -Unsigned128 := [] -Unsigned64 := [] -Unsigned32 := [] -Unsigned16 := [] -Unsigned8 := [] - -Natural := [] - -Integer range := range - -I128 : Num (Integer Signed128) -I64 : Num (Integer Signed64) -I32 : Num (Integer Signed32) -I16 : Num (Integer Signed16) - -## A signed 8-bit integer, ranging from -128 to 127 -I8 : Int Signed8 - -U128 : Num (Integer Unsigned128) -U64 : Num (Integer Unsigned64) -U32 : Num (Integer Unsigned32) -U16 : Num (Integer Unsigned16) -U8 : Num (Integer Unsigned8) - -## A [natural number](https://en.wikipedia.org/wiki/Natural_number) represented -## as a 64-bit unsigned integer on 64-bit systems, a 32-bit unsigned integer -## on 32-bit systems, and so on. -## -## This system-specific size makes it useful for certain data structure -## functions like [List.len], because the number of elements many data strucures -## can hold is also system-specific. For example, the maximum number of elements -## a [List] can hold on a 64-bit system fits in a 64-bit unsigned integer, and -## on a 32-bit system it fits in 32-bit unsigned integer. This makes [Nat] a -## good fit for [List.len] regardless of system. -Nat : Num (Integer Natural) - -Decimal := [] -Binary64 := [] -Binary32 := [] - -FloatingPoint range := range - -F64 : Num (FloatingPoint Binary64) -F32 : Num (FloatingPoint Binary32) - -## A decimal number. -## -## [Dec] is the best default choice for representing base-10 decimal numbers -## like currency, because it is base-10 under the hood. In contrast, -## [F64] and [F32] are base-2 under the hood, which can lead to decimal -## precision loss even when doing addition and subtraction. For example, when -## using [F64], running 0.1 + 0.2 returns 0.3000000000000000444089209850062616169452667236328125, -## whereas when using [Dec], 0.1 + 0.2 returns 0.3. -## -## Under the hood, a [Dec] is an [I128], and operations on it perform -## [base-10 fixed-point arithmetic](https://en.wikipedia.org/wiki/Fixed-point_arithmetic) -## with 18 decimal places of precision. -## -## This means a [Dec] can represent whole numbers up to slightly over 170 -## quintillion, along with 18 decimal places. (To be precise, it can store -## numbers betwween `-170_141_183_460_469_231_731.687303715884105728` -## and `170_141_183_460_469_231_731.687303715884105727`.) Why 18 -## decimal places? It's the highest number of decimal places where you can still -## convert any [U64] to a [Dec] without losing information. -## -## There are some use cases where [F64] and [F32] can be better choices than [Dec] -## despite their precision issues. For example, in graphical applications they -## can be a better choice for representing coordinates because they take up -## less memory, certain relevant calculations run faster (see performance -## details, below), and decimal precision loss isn't as big a concern when -## dealing with screen coordinates as it is when dealing with currency. -## -## ## Performance -## -## [Dec] typically takes slightly less time than [F64] to perform addition and -## subtraction, but 10-20 times longer to perform multiplication and division. -## [sqrt] and trigonometry are massively slower with [Dec] than with [F64]. -Dec : Num (FloatingPoint Decimal) - -# ------- Functions -## Convert a number to a [Str]. -## -## This is the same as calling `Num.format {}` - so for more details on -## exact formatting, see `Num.format`. -## -## >>> Num.toStr 42 -## -## Only [Frac] values will include a decimal point, and they will always include one. -## -## >>> Num.toStr 4.2 -## -## >>> Num.toStr 4.0 -## -## When this function is given a non-[finite](Num.isFinite) -## [F64] or [F32] value, the returned string will be `"NaN"`, `"∞"`, or `"-∞"`. -## -## To get strings in hexadecimal, octal, or binary format, use `Num.format`. -toStr : Num * -> Str -intCast : Int a -> Int b - -bytesToU16 : List U8, Nat -> Result U16 [OutOfBounds] -bytesToU32 : List U8, Nat -> Result U32 [OutOfBounds] - -compare : Num a, Num a -> [LT, EQ, GT] - -## Returns `True` if the first number is less than the second. -## -## `a < b` is shorthand for `Num.isLt a b`. -## -## If either argument is [*NaN*](Num.isNaN), returns `False` no matter what. (*NaN* -## is [defined to be unordered](https://en.wikipedia.org/wiki/NaN#Comparison_with_NaN).) -## -## >>> 5 -## >>> |> Num.isLt 6 -isLt : Num a, Num a -> Bool - -## Returns `True` if the first number is greater than the second. -## -## `a > b` is shorthand for `Num.isGt a b`. -## -## If either argument is [*NaN*](Num.isNaN), returns `False` no matter what. (*NaN* -## is [defined to be unordered](https://en.wikipedia.org/wiki/NaN#Comparison_with_NaN).) -## -## >>> 6 -## >>> |> Num.isGt 5 -isGt : Num a, Num a -> Bool - -## Returns `True` if the first number is less than or equal to the second. -## -## `a <= b` is shorthand for `Num.isLte a b`. -## -## If either argument is [*NaN*](Num.isNaN), returns `False` no matter what. (*NaN* -## is [defined to be unordered](https://en.wikipedia.org/wiki/NaN#Comparison_with_NaN).) -isLte : Num a, Num a -> Bool - -## Returns `True` if the first number is greater than or equal to the second. -## -## `a >= b` is shorthand for `Num.isGte a b`. -## -## If either argument is [*NaN*](Num.isNaN), returns `False` no matter what. (*NaN* -## is [defined to be unordered](https://en.wikipedia.org/wiki/NaN#Comparison_with_NaN).) -isGte : Num a, Num a -> Bool - -## Returns `True` if the number is `0`, and `False` otherwise. -isZero : Num a -> Bool - -## A number is even if dividing it by 2 gives a remainder of 0. -## -## Examples of even numbers: 0, 2, 4, 6, 8, -2, -4, -6, -8 -isEven : Int a -> Bool - -## A number is odd if dividing it by 2 gives a remainder of 1. -## -## Examples of odd numbers: 1, 3, 5, 7, -1, -3, -5, -7 -isOdd : Int a -> Bool - -## Positive numbers are greater than `0`. -isPositive : Num a -> Bool - -## Negative numbers are less than `0`. -isNegative : Num a -> Bool - -toFrac : Num * -> Frac * - -## Return the absolute value of the number. -## -## * For a positive number, returns the same number. -## * For a negative number, returns the same number except positive. -## * For zero, returns zero. -## -## >>> Num.abs 4 -## -## >>> Num.abs -2.5 -## -## >>> Num.abs 0 -## -## >>> Num.abs 0.0 -## -## This is safe to use with any [Frac], but it can cause overflow when used with certain [Int] values. -## -## For example, calling #Num.abs on the lowest value of a signed integer (such as [Num.minI64] or [Num.minI32]) will cause overflow. -## This is because, for any given size of signed integer (32-bit, 64-bit, etc.) its negated lowest value turns out to be 1 higher than -## the highest value it can represent. (For this reason, calling [Num.neg] on the lowest signed value will also cause overflow.) -## -## Calling this on an unsigned integer (like [U32] or [U64]) never does anything. -abs : Num a -> Num a - -## Return a negative number when given a positive one, and vice versa. -## -## >>> Num.neg 5 -## -## >>> Num.neg -2.5 -## -## >>> Num.neg 0 -## -## >>> Num.neg 0.0 -## -## This is safe to use with any [Frac], but it can cause overflow when used with certain [Int] values. -## -## For example, calling #Num.neg on the lowest value of a signed integer (such as [Num.minI64] or [Num.minI32]) will cause overflow. -## This is because, for any given size of signed integer (32-bit, 64-bit, etc.) its negated lowest value turns out to be 1 higher than -## the highest value it can represent. (For this reason, calling #Num.abs on the lowest signed value will also cause overflow.) -## -## Additionally, calling #Num.neg on any unsigned integer (such as any [U64] or [U32] value) other than zero will cause overflow. -## -## (It will never crash when given a [Frac], however, because of how floating point numbers represent positive and negative numbers.) -neg : Num a -> Num a - -## Add two numbers of the same type. -## -## (To add an [Int] and a [Frac], first convert one so that they both have the same type. There are functions in this module that can convert both [Int] to [Frac] and the other way around.) -## -## `a + b` is shorthand for `Num.add a b`. -## -## >>> 5 + 7 -## -## >>> Num.add 5 7 -## -## `Num.add` can be convenient in pipelines. -## -## >>> Frac.pi -## >>> |> Num.add 1.0 -## -## If the answer to this operation can't fit in the return value (e.g. an -## [I8] answer that's higher than 127 or lower than -128), the result is an -## *overflow*. For [F64] and [F32], overflow results in an answer of either -## ∞ or -∞. For all other number types, overflow results in a panic. -add : Num a, Num a -> Num a - -## Subtract two numbers of the same type. -## -## (To subtract an [Int] and a [Frac], first convert one so that they both have the same type. There are functions in this module that can convert both [Int] to [Frac] and the other way around.) -## -## `a - b` is shorthand for `Num.sub a b`. -## -## >>> 7 - 5 -## -## >>> Num.sub 7 5 -## -## `Num.sub` can be convenient in pipelines. -## -## >>> Frac.pi -## >>> |> Num.sub 2.0 -## -## If the answer to this operation can't fit in the return value (e.g. an -## [I8] answer that's higher than 127 or lower than -128), the result is an -## *overflow*. For [F64] and [F32], overflow results in an answer of either -## ∞ or -∞. For all other number types, overflow results in a panic. -sub : Num a, Num a -> Num a - -## Multiply two numbers of the same type. -## -## (To multiply an [Int] and a [Frac], first convert one so that they both have the same type. There are functions in this module that can convert both [Int] to [Frac] and the other way around.) -## -## `a * b` is shorthand for `Num.mul a b`. -## -## >>> 5 * 7 -## -## >>> Num.mul 5 7 -## -## `Num.mul` can be convenient in pipelines. -## -## >>> Frac.pi -## >>> |> Num.mul 2.0 -## -## If the answer to this operation can't fit in the return value (e.g. an -## [I8] answer that's higher than 127 or lower than -128), the result is an -## *overflow*. For [F64] and [F32], overflow results in an answer of either -## ∞ or -∞. For all other number types, overflow results in a panic. -mul : Num a, Num a -> Num a - -sin : Frac a -> Frac a -cos : Frac a -> Frac a -tan : Frac a -> Frac a - -asin : Frac a -> Frac a -acos : Frac a -> Frac a -atan : Frac a -> Frac a - -## Returns an approximation of the absolute value of a [Frac]'s square root. -## -## The square root of a negative number is an irrational number, and [Frac] only -## supports rational numbers. As such, you should make sure never to pass this -## function a negative number! Calling [sqrt] on a negative [Dec] will cause a panic. -## -## Calling [sqrt] on [F32] and [F64] values follows these rules: -## * Passing a negative [F64] or [F32] returns [*NaN*](Num.isNaN). -## * Passing [*NaN*](Num.isNaN) or -∞ also returns [*NaN*](Num.isNaN). -## * Passing ∞ returns ∞. -## -## > These rules come from the [IEEE-754](https://en.wikipedia.org/wiki/IEEE_754) -## > floating point standard. Because almost all modern processors are built to -## > this standard, deviating from these rules has a significant performance -## > cost! Since the most common reason to choose [F64] or [F32] over [Dec] is -## > access to hardware-accelerated performance, Roc follows these rules exactly. -## -## >>> Num.sqrt 4.0 -## -## >>> Num.sqrt 1.5 -## -## >>> Num.sqrt 0.0 -## -## >>> Num.sqrt -4.0f64 -sqrt : Frac a -> Frac a -sqrtChecked : Frac a -> Result (Frac a) [SqrtOfNegative]* -log : Frac a -> Frac a -logChecked : Frac a -> Result (Frac a) [LogNeedsPositive]* - -## Divide one [Frac] by another. -## -## `a / b` is shorthand for `Num.div a b`. -## -## [Division by zero is undefined in mathematics](https://en.wikipedia.org/wiki/Division_by_zero). -## As such, you should make sure never to pass zero as the denomaintor to this function! -## Calling [div] on a [Dec] denominator of zero will cause a panic. -## -## Calling [div] on [F32] and [F64] values follows these rules: -## * Dividing a positive [F64] or [F32] by zero returns ∞. -## * Dividing a negative [F64] or [F32] by zero returns -∞. -## * Dividing a zero [F64] or [F32] by zero returns [*NaN*](Num.isNaN). -## -## > These rules come from the [IEEE-754](https://en.wikipedia.org/wiki/IEEE_754) -## > floating point standard. Because almost all modern processors are built to -## > this standard, deviating from these rules has a significant performance -## > cost! Since the most common reason to choose [F64] or [F32] over [Dec] is -## > access to hardware-accelerated performance, Roc follows these rules exactly. -## -## To divide an [Int] and a [Frac], first convert the [Int] to a [Frac] using -## one of the functions in this module like #toDec. -## -## >>> 5.0 / 7.0 -## -## >>> Num.div 5 7 -## -## `Num.div` can be convenient in pipelines. -## -## >>> Num.pi -## >>> |> Num.div 2.0 -div : Frac a, Frac a -> Frac a -divChecked : Frac a, Frac a -> Result (Frac a) [DivByZero]* -divCeil : Int a, Int a -> Int a -divCeilChecked : Int a, Int a -> Result (Int a) [DivByZero]* - -## Divide two integers, truncating the result towards zero. -## -## `a // b` is shorthand for `Num.divTrunc a b`. -## -## Division by zero is undefined in mathematics. As such, you should make -## sure never to pass zero as the denomaintor to this function! If you do, -## it will crash. -## -## >>> 5 // 7 -## -## >>> Num.divTrunc 5 7 -## -## >>> 8 // -3 -## -## >>> Num.divTrunc 8 -3 -## -divTrunc : Int a, Int a -> Int a -divTruncChecked : Int a, Int a -> Result (Int a) [DivByZero]* - -## Obtain the remainder (truncating modulo) from the division of two integers. -## -## `a % b` is shorthand for `Num.rem a b`. -## -## >>> 5 % 7 -## -## >>> Num.rem 5 7 -## -## >>> -8 % -3 -## -## >>> Num.rem -8 -3 -rem : Int a, Int a -> Int a -remChecked : Int a, Int a -> Result (Int a) [DivByZero]* - -isMultipleOf : Int a, Int a -> Bool - -bitwiseAnd : Int a, Int a -> Int a -bitwiseXor : Int a, Int a -> Int a -bitwiseOr : Int a, Int a -> Int a -shiftLeftBy : Int a, Int a -> Int a -shiftRightBy : Int a, Int a -> Int a -shiftRightZfBy : Int a, Int a -> Int a - -## Round off the given fraction to the nearest integer. -round : Frac * -> Int * -floor : Frac * -> Int * -ceiling : Frac * -> Int * - -## Raises a [Frac] to the power of another [Frac]. -## -## For an [Int] alternative to this function, see [Num.powInt] -pow : Frac a, Frac a -> Frac a - -## Raises an integer to the power of another, by multiplying the integer by -## itself the given number of times. -## -## This process is known as [exponentiation by squaring](https://en.wikipedia.org/wiki/Exponentiation_by_squaring). -## -## For a [Frac] alternative to this function, which supports negative exponents, -## see #Num.exp. -## -## >>> Num.exp 5 0 -## -## >>> Num.exp 5 1 -## -## >>> Num.exp 5 2 -## -## >>> Num.exp 5 6 -## -## ## Performance Notes -## -## Be careful! It is very easy for this function to produce an answer -## so large it causes an overflow. -powInt : Int a, Int a -> Int a - -addWrap : Int range, Int range -> Int range - -## Add two numbers, clamping on the maximum representable number rather than -## overflowing. -## -## This is the same as [Num.add] except for the saturating behavior if the -## addition is to overflow. -## For example, if `x : U8` is 200 and `y : U8` is 100, `addSaturated x y` will -## yield 255, the maximum value of a `U8`. -addSaturated : Num a, Num a -> Num a - -## Add two numbers and check for overflow. -## -## This is the same as [Num.add] except if the operation overflows, instead of -## panicking or returning ∞ or -∞, it will return `Err Overflow`. -addChecked : Num a, Num a -> Result (Num a) [Overflow]* - -subWrap : Int range, Int range -> Int range - -## Subtract two numbers, clamping on the minimum representable number rather -## than overflowing. -## -## This is the same as [Num.sub] except for the saturating behavior if the -## subtraction is to overflow. -## For example, if `x : U8` is 10 and `y : U8` is 20, `subSaturated x y` will -## yield 0, the minimum value of a `U8`. -subSaturated : Num a, Num a -> Num a - -## Subtract two numbers and check for overflow. -## -## This is the same as [Num.sub] except if the operation overflows, instead of -## panicking or returning ∞ or -∞, it will return `Err Overflow`. -subChecked : Num a, Num a -> Result (Num a) [Overflow]* - -mulWrap : Int range, Int range -> Int range -# mulSaturated : Num a, Num a -> Num a -## Multiply two numbers and check for overflow. -## -## This is the same as [Num.mul] except if the operation overflows, instead of -## panicking or returning ∞ or -∞, it will return `Err Overflow`. -mulChecked : Num a, Num a -> Result (Num a) [Overflow]* - -## The lowest number that can be stored in an [I8] without underflowing its -## available memory and crashing. -## -## For reference, this number is `-128`. -## -## Note that the positive version of this number is larger than [Num.maxI8], -## which means if you call [Num.abs] on [Num.minI8], it will overflow and crash! -minI8 : I8 -minI8 = -128i8 - -## The highest number that can be stored in an [I8] without overflowing its -## available memory and crashing. -## -## For reference, this number is `127`. -## -## Note that this is smaller than the positive version of [Num.minI8], -## which means if you call [Num.abs] on [Num.minI8], it will overflow and crash! -maxI8 : I8 -maxI8 = 127i8 - -## The lowest number that can be stored in a [U8] without underflowing its -## available memory and crashing. -## -## For reference, this number is zero, because [U8] is -## [unsigned](https://en.wikipedia.org/wiki/Signed_number_representations), -## and zero is the lowest unsigned number. -## Unsigned numbers cannot be negative. -minU8 : U8 -minU8 = 0u8 - -## The highest number that can be stored in a [U8] without overflowing its -## available memory and crashing. -## -## For reference, this number is `255`. -maxU8 : U8 -maxU8 = 255u8 - -## The lowest number that can be stored in an [I16] without underflowing its -## available memory and crashing. -## -## For reference, this number is `-32_768`. -## -## Note that the positive version of this number is larger than [Num.maxI16], -## which means if you call [Num.abs] on [Num.minI16], it will overflow and crash! -minI16 : I16 -minI16 = -32768i16 - -## The highest number that can be stored in an [I16] without overflowing its -## available memory and crashing. -## -## For reference, this number is `32_767`. -## -## Note that this is smaller than the positive version of [Num.minI16], -## which means if you call [Num.abs] on [Num.minI16], it will overflow and crash! -maxI16 : I16 -maxI16 = 32767i16 - -## The lowest number that can be stored in a [U16] without underflowing its -## available memory and crashing. -## -## For reference, this number is zero, because [U16] is -## [unsigned](https://en.wikipedia.org/wiki/Signed_number_representations), -## and zero is the lowest unsigned number. -## Unsigned numbers cannot be negative. -minU16 : U16 -minU16 = 0u16 - -## The highest number that can be stored in a [U16] without overflowing its -## available memory and crashing. -## -## For reference, this number is `65_535`. -maxU16 : U16 -maxU16 = 65535u16 - -## The lowest number that can be stored in an [I32] without underflowing its -## available memory and crashing. -## -## For reference, this number is `-2_147_483_648`. -## -## Note that the positive version of this number is larger than [Num.maxI32], -## which means if you call [Num.abs] on [Num.minI32], it will overflow and crash! -minI32 : I32 -minI32 = -2147483648 - -## The highest number that can be stored in an [I32] without overflowing its -## available memory and crashing. -## -## For reference, this number is `2_147_483_647`, -## which is over 2 million. -## -## Note that this is smaller than the positive version of [Num.minI32], -## which means if you call [Num.abs] on [Num.minI32], it will overflow and crash! -maxI32 : I32 -maxI32 = 2147483647 - -## The lowest number that can be stored in a [U32] without underflowing its -## available memory and crashing. -## -## For reference, this number is zero, because [U32] is -## [unsigned](https://en.wikipedia.org/wiki/Signed_number_representations), -## and zero is the lowest unsigned number. -## Unsigned numbers cannot be negative. -minU32 : U32 -minU32 = 0 - -## The highest number that can be stored in a [U32] without overflowing its -## available memory and crashing. -## -## For reference, this number is `4_294_967_295`. -maxU32 : U32 -maxU32 = 4294967295 - -## The lowest number that can be stored in an [I64] without underflowing its -## available memory and crashing. -## -## For reference, this number is `-9_223_372_036_854_775_808`, -## which is under 9 quintillion. -## -## Note that the positive version of this number is larger than [Num.maxI64], -## which means if you call [Num.abs] on [Num.minI64], it will overflow and crash! -minI64 : I64 -minI64 = -9223372036854775808 - -## The highest number that can be stored in an [I64] without overflowing its -## available memory and crashing. -## -## For reference, this number is `9_223_372_036_854_775_807`, -## which is over 9 quintillion. -## -## Note that this is smaller than the positive version of [Num.minI64], -## which means if you call [Num.abs] on [Num.minI64], it will overflow and crash! -maxI64 : I64 -maxI64 = 9223372036854775807 - -## The lowest number that can be stored in a [U64] without underflowing its -## available memory and crashing. -## -## For reference, this number is zero, because [U64] is -## [unsigned](https://en.wikipedia.org/wiki/Signed_number_representations), -## and zero is the lowest unsigned number. -## Unsigned numbers cannot be negative. -minU64 : U64 -minU64 = 0 - -## The highest number that can be stored in a [U64] without overflowing its -## available memory and crashing. -## -## For reference, this number is `18_446_744_073_709_551_615`, -## which is over 18 quintillion. -maxU64 : U64 -maxU64 = 18446744073709551615 - -## The lowest number that can be stored in an [I128] without underflowing its -## available memory and crashing. -## -## For reference, this number is `-170_141_183_460_469_231_731_687_303_715_884_105_728`. -## which is under 170 undecillion. -## -## Note that the positive version of this number is larger than [Num.maxI128], -## which means if you call [Num.abs] on [Num.minI128], it will overflow and crash! -minI128 : I128 -minI128 = -170141183460469231731687303715884105728 - -## The highest number that can be stored in an [I128] without overflowing its -## available memory and crashing. -## -## For reference, this number is `170_141_183_460_469_231_731_687_303_715_884_105_727`, -## which is over 170 undecillion. -## -## Note that this is smaller than the positive version of [Num.minI128], -## which means if you call [Num.abs] on [Num.minI128], it will overflow and crash! -maxI128 : I128 -maxI128 = 170141183460469231731687303715884105727 - -## The lowest number that can be stored in a [U128] without underflowing its -## available memory and crashing. -## -## For reference, this number is zero, because [U128] is -## [unsigned](https://en.wikipedia.org/wiki/Signed_number_representations), -## and zero is the lowest unsigned number. -## Unsigned numbers cannot be negative. -minU128 : U128 -minU128 = 0 - -## The highest number that can be stored in a [U128] without overflowing its -## available memory and crashing. -## -## For reference, this number is `340_282_366_920_938_463_463_374_607_431_768_211_455`, -## which is over 340 undecillion. -maxU128 : U128 -maxU128 = 340282366920938463463374607431768211455 - -minF32 : F32 -minF32 = -3.40282347e38 - -maxF32 : F32 -maxF32 = 3.40282347e38 - -minF64 : F64 -minF64 = -1.7976931348623157e308 - -maxF64 : F64 -maxF64 = 1.7976931348623157e308 - -## Converts an [Int] to an [I8]. If the given number can't be precisely represented in an [I8], -## the returned number may be different from the given number. -toI8 : Int * -> I8 -toI16 : Int * -> I16 -toI32 : Int * -> I32 -toI64 : Int * -> I64 -toI128 : Int * -> I128 -toU8 : Int * -> U8 -toU16 : Int * -> U16 -toU32 : Int * -> U32 -toU64 : Int * -> U64 -toU128 : Int * -> U128 - -## Convert an [Int] to a [Nat]. If the given number doesn't fit in [Nat], it will be truncated. -## Since #Nat has a different maximum number depending on the system you're building -## for, this may give a different answer on different systems. -## -## For example, on a 32-bit system, #Num.maxNat will return the same answer as -## [Num.maxU32]. This means that calling `Num.toNat 9_000_000_000` on a 32-bit -## system will return [Num.maxU32] instead of 9 billion, because 9 billion is -## higher than [Num.maxU32] and will not fit in a [Nat] on a 32-bit system. -## -## However, calling `Num.toNat 9_000_000_000` on a 64-bit system will return -## the #Nat value of 9_000_000_000. This is because on a 64-bit system, [Nat] can -## hold up to [Num.maxU64], and 9_000_000_000 is lower than [Num.maxU64]. -## -## To convert a [Frac] to a [Nat], first call either #Num.round, #Num.ceil, or [Num.floor] -## on it, then call this on the resulting [Int]. -toNat : Int * -> Nat - -## Converts a [Num] to an [F32]. If the given number can't be precisely represented in an [F32], -## the returned number may be different from the given number. -toF32 : Num * -> F32 - -## Converts a [Num] to an [F64]. If the given number can't be precisely represented in an [F64], -## the returned number may be different from the given number. -toF64 : Num * -> F64 - -## Converts a [Int] to an [I8]. -## If the given integer can't be precisely represented in an [I8], returns -## `Err OutOfBounds`. -toI8Checked : Int * -> Result I8 [OutOfBounds]* -toI16Checked : Int * -> Result I16 [OutOfBounds]* -toI32Checked : Int * -> Result I32 [OutOfBounds]* -toI64Checked : Int * -> Result I64 [OutOfBounds]* -toI128Checked : Int * -> Result I128 [OutOfBounds]* -toU8Checked : Int * -> Result U8 [OutOfBounds]* -toU16Checked : Int * -> Result U16 [OutOfBounds]* -toU32Checked : Int * -> Result U32 [OutOfBounds]* -toU64Checked : Int * -> Result U64 [OutOfBounds]* -toU128Checked : Int * -> Result U128 [OutOfBounds]* -toNatChecked : Int * -> Result Nat [OutOfBounds]* -toF32Checked : Num * -> Result F32 [OutOfBounds]* -toF64Checked : Num * -> Result F64 [OutOfBounds]* - -# Special Floating-Point operations -## When given a [F64] or [F32] value, returns `False` if that value is -## [*NaN*](Num.isNaN), ∞ or -∞, and `True` otherwise. -## -## Always returns `True` when given a [Dec]. -## -## This is the opposite of [isInfinite], except when given [*NaN*](Num.isNaN). Both -## [isFinite] and [isInfinite] return `False` for [*NaN*](Num.isNaN). -# isFinite : Frac * -> Bool -## When given a [F64] or [F32] value, returns `True` if that value is either -## ∞ or -∞, and `False` otherwise. -## -## Always returns `False` when given a [Dec]. -## -## This is the opposite of [isFinite], except when given [*NaN*](Num.isNaN). Both -## [isFinite] and [isInfinite] return `False` for [*NaN*](Num.isNaN). -# isInfinite : Frac * -> Bool -## When given a [F64] or [F32] value, returns `True` if that value is -## *NaN* ([not a number](https://en.wikipedia.org/wiki/NaN)), and `False` otherwise. -## -## Always returns `False` when given a [Dec]. -## -## >>> Num.isNaN 12.3 -## -## >>> Num.isNaN (Num.pow -1 0.5) -## -## *NaN* is unusual from other numberic values in that: -## * *NaN* is not equal to any other number, even itself. [Bool.isEq] always returns `False` if either argument is *NaN*. -## * *NaN* has no ordering, so [isLt], [isLte], [isGt], and [isGte] always return `False` if either argument is *NaN*. -## -## These rules come from the [IEEE-754](https://en.wikipedia.org/wiki/IEEE_754) -## floating point standard. Because almost all modern processors are built to -## this standard, deviating from these rules has a significant performance -## cost! Since the most common reason to choose [F64] or [F32] over [Dec] is -## access to hardware-accelerated performance, Roc follows these rules exactly. -## -## Note that you should never put a *NaN* into a [Set], or use it as the key in -## a [Dict]. The result is entries that can never be removed from those -## collections! See the documentation for [Set.add] and [Dict.insert] for details. -# isNaN : Frac * -> Bool -## Returns the higher of two numbers. -## -## If either argument is [*NaN*](Num.isNaN), returns `False` no matter what. (*NaN* -## is [defined to be unordered](https://en.wikipedia.org/wiki/NaN#Comparison_with_NaN).) -# max : Num a, Num a -> Num a -## Returns the lower of two numbers. -## -## If either argument is [*NaN*](Num.isNaN), returns `False` no matter what. (*NaN* -## is [defined to be unordered](https://en.wikipedia.org/wiki/NaN#Comparison_with_NaN).) -# min : Num a, Num a -> Num a -# Branchless implementation that works for all numeric types: -# -# let is_lt = arg1 < arg2; -# let is_eq = arg1 == arg2; -# return (is_lt as i8 - is_eq as i8) + 1; -# -# 1, 1 -> (0 - 1) + 1 == 0 # Eq -# 5, 1 -> (0 - 0) + 1 == 1 # Gt -# 1, 5 -> (1 - 0) + 1 == 2 # Lt -## Returns `Lt` if the first number is less than the second, `Gt` if -## the first is greater than the second, and `Eq` if they're equal. -## -## Although this can be passed to `List.sort`, you'll get better performance -## by using `List.sortAsc` or `List.sortDesc` instead. -# compare : Num a, Num a -> [Lt, Eq, Gt] -## [Endianness](https://en.wikipedia.org/wiki/Endianness) -# Endi : [Big, Little, Native] -## The `Endi` argument does not matter for [U8] and [I8], since they have -## only one byte. -# toBytes : Num *, Endi -> List U8 -## when Num.parseBytes bytes Big is -## Ok { val: f64, rest } -> ... -## Err (ExpectedNum (Frac Binary64)) -> ... -# parseBytes : List U8, Endi -> Result { val : Num a, rest : List U8 } [ExpectedNum a]* -## when Num.fromBytes bytes Big is -## Ok f64 -> ... -## Err (ExpectedNum (Frac Binary64)) -> ... -# fromBytes : List U8, Endi -> Result (Num a) [ExpectedNum a]* -# Bit shifts -## [Logical bit shift](https://en.wikipedia.org/wiki/Bitwise_operation#Logical_shift) left. -## -## `a << b` is shorthand for `Num.shl a b`. -# shl : Int a, Int a -> Int a -## [Arithmetic bit shift](https://en.wikipedia.org/wiki/Bitwise_operation#Arithmetic_shift) left. -## -## This is called `shlWrap` because any bits shifted -## off the beginning of the number will be wrapped around to -## the end. (In contrast, [shl] replaces discarded bits with zeroes.) -# shlWrap : Int a, Int a -> Int a -## [Logical bit shift](https://en.wikipedia.org/wiki/Bitwise_operation#Logical_shift) right. -## -## `a >> b` is shorthand for `Num.shr a b`. -# shr : Int a, Int a -> Int a -## [Arithmetic bit shift](https://en.wikipedia.org/wiki/Bitwise_operation#Arithmetic_shift) right. -## -## This is called `shrWrap` because any bits shifted -## off the end of the number will be wrapped around to -## the beginning. (In contrast, [shr] replaces discarded bits with zeroes.) -# shrWrap : Int a, Int a -> Int a -# ## Convert a number into a [Str], formatted with the given options. -# ## -# ## Default options: -# ## * `base: Decimal` -# ## * `notation: Standard` -# ## * `decimalMark: HideForIntegers "."` -# ## * `decimalDigits: { min: 0, max: All }` -# ## * `minIntDigits: 1` -# ## * `wholeSep: { mark: ",", places: 3 }` -# ## -# ## ## Options -# ## -# ## -# ## ### decimalMark -# ## -# ## * `AlwaysShow` always shows the decimal mark, no matter what. -# ## * `HideForIntegers` hides the decimal mark if all the numbers after the decimal mark are 0. -# ## -# ## The [Str] included in either of these represents the mark itself. -# ## -# ## ### `decimalDigits -# ## -# ## With 0 decimal digits, the decimal mark will still be rendered if -# ## `decimalMark` is set to `AlwaysShow`. -# ## -# ## If `max` is less than `min`, then first the number will be truncated to `max` -# ## digits, and then zeroes will be added afterwards until it reaches `min` digits. -# ## -# ## >>> Num.format 1.23 { decPlaces: 0, decPointVis: AlwaysShow } -# ## -# ## ### minIntDigits -# ## -# ## If the integer portion of number is fewer than this many digits, zeroes will -# ## be added in front of it until there are at least `minWholeDigits` digits. -# ## -# ## If this is set to zero, then numbers less than 1 will begin with `"."` -# ## rather than `"0."`. -# ## -# ## ### wholeSep -# ## -# ## Examples: -# ## -# ## In some countries (e.g. USA and UK), a comma is used to separate thousands: -# ## >>> Num.format 1_000_000 { pf: Decimal, wholeSep: { mark: ",", places: 3 } } -# ## -# ## Sometimes when rendering bits, it's nice to group them into groups of 4: -# ## >>> Num.format 1_000_000 { pf: Binary, wholeSep: { mark: " ", places: 4 } } -# ## -# ## It's also common to render hexadecimal in groups of 2: -# ## >>> Num.format 1_000_000 { pf: Hexadecimal, wholeSep: { mark: " ", places: 2 } } -# format : -# Num *, -# { -# base ? [Decimal, Hexadecimal, Octal, Binary], -# notation ? [Standard, Scientific], -# decimalMark ? [AlwaysShow Str, HideForIntegers], -# decimalDigits ? { min : U16, max : [All, Trunc U16, Round U16, Floor U16, Ceil U16] }, -# minWholeDigits ? U16, -# wholeSep ? { mark : Str, places : U64 } -# } -# -> Str diff --git a/compiler/builtins/roc/Result.roc b/compiler/builtins/roc/Result.roc deleted file mode 100644 index 90f67ec33c..0000000000 --- a/compiler/builtins/roc/Result.roc +++ /dev/null @@ -1,94 +0,0 @@ -interface Result - exposes [Result, isOk, isErr, map, mapErr, after, withDefault] - imports [Bool.{ Bool }] - -## The result of an operation that could fail: either the operation went -## okay, or else there was an error of some sort. -Result ok err : [Ok ok, Err err] - -## Return True if the result indicates a success, else return False -## -## >>> Result.isOk (Ok 5) -isOk : Result ok err -> Bool -isOk = \result -> - when result is - Ok _ -> - True - Err _ -> - False - -## Return True if the result indicates a failure, else return False -## -## >>> Result.isErr (Err "uh oh") -isErr : Result ok err -> Bool -isErr = \result -> - when result is - Ok _ -> - False - Err _ -> - True - -## If the result is `Ok`, return the value it holds. Otherwise, return -## the given default value. -## -## >>> Result.withDefault (Ok 7) 42 -## -## >>> Result.withDefault (Err "uh oh") 42 -withDefault : Result ok err, ok -> ok -withDefault = \result, default -> - when result is - Ok value -> - value - Err _ -> - default - -## If the result is `Ok`, transform the value it holds by running a conversion -## function on it. Then return a new `Ok` holding the transformed value. -## -## (If the result is `Err`, this has no effect. Use [mapErr] to transform an `Err`.) -## -## >>> Result.map (Ok 12) Num.negate -## -## >>> Result.map (Err "yipes!") Num.negate -## -## `map` functions like this are common in Roc, and they all work similarly. -## See for example [List.map], `Set.map`, and `Dict.map`. -map : Result a err, (a -> b) -> Result b err -map = \result, transform -> - when result is - Ok v -> - Ok (transform v) - Err e -> - Err e - -## If the result is `Err`, transform the value it holds by running a conversion -## function on it. Then return a new `Err` holding the transformed value. -## -## (If the result is `Ok`, this has no effect. Use [map] to transform an `Ok`.) -## -## >>> Result.mapErr (Err "yipes!") Str.isEmpty -## -## >>> Result.mapErr (Ok 12) Str.isEmpty -mapErr : Result ok a, (a -> b) -> Result ok b -mapErr = \result, transform -> - when result is - Ok v -> - Ok v - Err e -> - Err (transform e) - -## If the result is `Ok`, transform the entire result by running a conversion -## function on the value the `Ok` holds. Then return that new result. -## -## (If the result is `Err`, this has no effect. Use `afterErr` to transform an `Err`.) -## -## >>> Result.after (Ok -1) \num -> if num < 0 then Err "negative!" else Ok -num -## -## >>> Result.after (Err "yipes!") \num -> if num < 0 then Err "negative!" else Ok -num -after : Result a err, (a -> Result b err) -> Result b err -after = \result, transform -> - when result is - Ok v -> - transform v - Err e -> - Err e diff --git a/compiler/builtins/roc/Set.roc b/compiler/builtins/roc/Set.roc deleted file mode 100644 index 73b842e39e..0000000000 --- a/compiler/builtins/roc/Set.roc +++ /dev/null @@ -1,45 +0,0 @@ -interface Set - exposes - [ - empty, - single, - walk, - insert, - len, - remove, - contains, - toList, - fromList, - union, - intersection, - difference, - ] - imports [List, Bool.{ Bool }, Dict.{ values }] - -## An empty set. -empty : Set k -single : k -> Set k - -## Make sure never to insert a *NaN* to a [Set]! Because *NaN* is defined to be -## unequal to *NaN*, adding a *NaN* results in an entry that can never be -## retrieved or removed from the [Set]. -insert : Set k, k -> Set k -len : Set k -> Nat - -## Drops the given element from the set. -remove : Set k, k -> Set k -contains : Set k, k -> Bool - -# toList = \set -> Dict.keys (toDict set) -toList : Set k -> List k -fromList : List k -> Set k - -union : Set k, Set k -> Set k -intersection : Set k, Set k -> Set k -difference : Set k, Set k -> Set k - -toDict : Set k -> Dict k {} - -walk : Set k, state, (state, k -> state) -> state -walk = \set, state, step -> - Dict.walk (Set.toDict set) state (\s, k, _ -> step s k) diff --git a/compiler/builtins/roc/Str.roc b/compiler/builtins/roc/Str.roc deleted file mode 100644 index 0aa86b6eb9..0000000000 --- a/compiler/builtins/roc/Str.roc +++ /dev/null @@ -1,215 +0,0 @@ -interface Str - exposes - [ - concat, - Utf8Problem, - Utf8ByteProblem, - isEmpty, - joinWith, - split, - repeat, - countGraphemes, - startsWithCodePt, - toUtf8, - fromUtf8, - fromUtf8Range, - startsWith, - endsWith, - trim, - trimLeft, - trimRight, - toDec, - toF64, - toF32, - toNat, - toU128, - toI128, - toU64, - toI64, - toU32, - toI32, - toU16, - toI16, - toU8, - toI8, - ] - imports [Bool.{ Bool }, Result.{ Result }] - -## # Types -## -## Dealing with text is a deep topic, so by design, Roc's `Str` module sticks -## to the basics. -## -## ### Unicode -## -## Unicode can represent text values which span multiple languages, symbols, and emoji. -## Here are some valid Roc strings: -## -## "Roc!" -## "鹏" -## "🕊" -## -## Every Unicode string is a sequence of [extended grapheme clusters](http://www.unicode.org/glossary/#extended_grapheme_cluster). -## An extended grapheme cluster represents what a person reading a string might -## call a "character" - like "A" or "ö" or "👩‍👩‍👦‍👦". -## Because the term "character" means different things in different areas of -## programming, and "extended grapheme cluster" is a mouthful, in Roc we use the -## term "grapheme" as a shorthand for the more precise "extended grapheme cluster." -## -## You can get the number of graphemes in a string by calling [Str.countGraphemes] on it: -## -## Str.countGraphemes "Roc!" -## Str.countGraphemes "折り紙" -## Str.countGraphemes "🕊" -## -## > The `countGraphemes` function walks through the entire string to get its answer, -## > so if you want to check whether a string is empty, you'll get much better performance -## > by calling `Str.isEmpty myStr` instead of `Str.countGraphemes myStr == 0`. -## -## ### Escape sequences -## -## If you put a `\` in a Roc string literal, it begins an *escape sequence*. -## An escape sequence is a convenient way to insert certain strings into other strings. -## For example, suppose you write this Roc string: -## -## "I took the one less traveled by,\nAnd that has made all the difference." -## -## The `"\n"` in the middle will insert a line break into this string. There are -## other ways of getting a line break in there, but `"\n"` is the most common. -## -## Another way you could insert a newlines is by writing `\u{0x0A}` instead of `\n`. -## That would result in the same string, because the `\u` escape sequence inserts -## [Unicode code points](https://unicode.org/glossary/#code_point) directly into -## the string. The Unicode code point 10 is a newline, and 10 is `0A` in hexadecimal. -## `0x0A` is a Roc hexadecimal literal, and `\u` escape sequences are always -## followed by a hexadecimal literal inside `{` and `}` like this. -## -## As another example, `"R\u{0x6F}c"` is the same string as `"Roc"`, because -## `"\u{0x6F}"` corresponds to the Unicode code point for lowercase `o`. If you -## want to [spice things up a bit](https://en.wikipedia.org/wiki/Metal_umlaut), -## you can write `"R\u{0xF6}c"` as an alternative way to get the string `"Röc"\. -## -## Roc strings also support these escape sequences: -## -## * `\\` - an actual backslash (writing a single `\` always begins an escape sequence!) -## * `\"` - an actual quotation mark (writing a `"` without a `\` ends the string) -## * `\r` - [carriage return](https://en.wikipedia.org/wiki/Carriage_Return) -## * `\t` - [horizontal tab](https://en.wikipedia.org/wiki/Tab_key#Tab_characters) -## * `\v` - [vertical tab](https://en.wikipedia.org/wiki/Tab_key#Tab_characters) -## -## You can also use escape sequences to insert named strings into other strings, like so: -## -## name = "Lee" -## city = "Roctown" -## -## greeting = "Hello there, \(name)! Welcome to \(city)." -## -## Here, `greeting` will become the string `"Hello there, Lee! Welcome to Roctown."`. -## This is known as [string interpolation](https://en.wikipedia.org/wiki/String_interpolation), -## and you can use it as many times as you like inside a string. The name -## between the parentheses must refer to a `Str` value that is currently in -## scope, and it must be a name - it can't be an arbitrary expression like a function call. -Utf8ByteProblem : - [ - InvalidStartByte, - UnexpectedEndOfSequence, - ExpectedContinuation, - OverlongEncoding, - CodepointTooLarge, - EncodesSurrogateHalf, - ] - -Utf8Problem : { byteIndex : Nat, problem : Utf8ByteProblem } - -## Returns `True` if the string is empty, and `False` otherwise. -## -## >>> Str.isEmpty "hi!" -## -## >>> Str.isEmpty "" -isEmpty : Str -> Bool -concat : Str, Str -> Str - -## Combine a list of strings into a single string, with a separator -## string in between each. -## -## >>> Str.joinWith ["one", "two", "three"] ", " -joinWith : List Str, Str -> Str - -## Split a string around a separator. -## -## >>> Str.split "1,2,3" "," -## -## Passing `""` for the separator is not useful; it returns the original string -## wrapped in a list. -## -## >>> Str.split "1,2,3" "" -## -## To split a string into its individual graphemes, use `Str.graphemes` -split : Str, Str -> List Str -repeat : Str, Nat -> Str - -## Count the number of [extended grapheme clusters](http://www.unicode.org/glossary/#extended_grapheme_cluster) -## in the string. -## -## Str.countGraphemes "Roc!" # 4 -## Str.countGraphemes "‰∏ÉÂ∑ßÊùø" # 3 -## Str.countGraphemes "üïä" # 1 -countGraphemes : Str -> Nat - -## If the string begins with a [Unicode code point](http://www.unicode.org/glossary/#code_point) -## equal to the given [U32], return `True`. Otherwise return `False`. -## -## If the given [Str] is empty, or if the given [U32] is not a valid -## code point, this will return `False`. -## -## **Performance Note:** This runs slightly faster than [Str.startsWith], so -## if you want to check whether a string begins with something that's representable -## in a single code point, you can use (for example) `Str.startsWithCodePt '鹏'` -## instead of `Str.startsWithCodePt "鹏"`. ('鹏' evaluates to the [U32] -## value `40527`.) This will not work for graphemes which take up multiple code -## points, however; `Str.startsWithCodePt '👩‍👩‍👦‍👦'` would be a compiler error -## because 👩‍👩‍👦‍👦 takes up multiple code points and cannot be represented as a -## single [U32]. You'd need to use `Str.startsWithCodePt "🕊"` instead. -startsWithCodePt : Str, U32 -> Bool - -## Return a [List] of the string's [U8] UTF-8 [code units](https://unicode.org/glossary/#code_unit). -## (To split the string into a [List] of smaller [Str] values instead of [U8] values, -## see [Str.split].) -## -## >>> Str.toUtf8 "👩‍👩‍👦‍👦" -## -## >>> Str.toUtf8 "Roc" -## -## >>> Str.toUtf8 "鹏" -## -## >>> Str.toUtf8 "🐦" -toUtf8 : Str -> List U8 - -# fromUtf8 : List U8 -> Result Str [BadUtf8 Utf8Problem]* -# fromUtf8Range : List U8 -> Result Str [BadUtf8 Utf8Problem Nat, OutOfBounds]* -fromUtf8 : List U8 -> Result Str [BadUtf8 Utf8ByteProblem Nat]* -fromUtf8Range : List U8, { start : Nat, count : Nat } -> Result Str [BadUtf8 Utf8ByteProblem Nat, OutOfBounds]* - -startsWith : Str, Str -> Bool -endsWith : Str, Str -> Bool - -## Return the string with any blank spaces removed from both the beginning -## as well as the end. -trim : Str -> Str -trimLeft : Str -> Str -trimRight : Str -> Str - -toDec : Str -> Result Dec [InvalidNumStr]* -toF64 : Str -> Result F64 [InvalidNumStr]* -toF32 : Str -> Result F32 [InvalidNumStr]* -toNat : Str -> Result Nat [InvalidNumStr]* -toU128 : Str -> Result U128 [InvalidNumStr]* -toI128 : Str -> Result I128 [InvalidNumStr]* -toU64 : Str -> Result U64 [InvalidNumStr]* -toI64 : Str -> Result I64 [InvalidNumStr]* -toU32 : Str -> Result U32 [InvalidNumStr]* -toI32 : Str -> Result I32 [InvalidNumStr]* -toU16 : Str -> Result U16 [InvalidNumStr]* -toI16 : Str -> Result I16 [InvalidNumStr]* -toU8 : Str -> Result U8 [InvalidNumStr]* -toI8 : Str -> Result I8 [InvalidNumStr]* diff --git a/compiler/builtins/src/bitcode.rs b/compiler/builtins/src/bitcode.rs deleted file mode 100644 index 308ba2eba9..0000000000 --- a/compiler/builtins/src/bitcode.rs +++ /dev/null @@ -1,427 +0,0 @@ -use roc_module::symbol::Symbol; -use roc_target::TargetInfo; -use std::ops::Index; - -pub const BUILTINS_HOST_OBJ_PATH: &str = env!( - "BUILTINS_HOST_O", - "Env var BUILTINS_HOST_O not found. Is there a problem with the build script?" -); - -pub const BUILTINS_WASM32_OBJ_PATH: &str = env!( - "BUILTINS_WASM32_O", - "Env var BUILTINS_WASM32_O not found. Is there a problem with the build script?" -); - -#[derive(Debug, Default, Copy, Clone)] -pub struct IntrinsicName { - pub options: [&'static str; 14], -} - -impl IntrinsicName { - pub const fn default() -> Self { - Self { options: [""; 14] } - } -} - -#[repr(u8)] -pub enum DecWidth { - Dec, -} - -#[repr(u8)] -#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)] -pub enum FloatWidth { - F32, - F64, - F128, -} - -impl FloatWidth { - pub const fn stack_size(&self) -> u32 { - use FloatWidth::*; - - // NOTE: this must never use mem::size_of, because that returns the size - // for the target of *the compiler itself* (e.g. this Rust code), not what - // the compiler is targeting (e.g. what the Roc code will be compiled to). - match self { - F32 => 4, - F64 => 8, - F128 => 16, - } - } - - pub const fn alignment_bytes(&self, target_info: TargetInfo) -> u32 { - use roc_target::Architecture; - use FloatWidth::*; - - // NOTE: this must never use mem::align_of, because that returns the alignment - // for the target of *the compiler itself* (e.g. this Rust code), not what - // the compiler is targeting (e.g. what the Roc code will be compiled to). - match self { - F32 => 4, - F64 | F128 => match target_info.architecture { - Architecture::X86_64 | Architecture::Aarch64 | Architecture::Wasm32 => 8, - Architecture::X86_32 | Architecture::Aarch32 => 4, - }, - } - } - - pub const fn try_from_symbol(symbol: Symbol) -> Option { - match symbol { - Symbol::NUM_F64 | Symbol::NUM_BINARY64 => Some(FloatWidth::F64), - Symbol::NUM_F32 | Symbol::NUM_BINARY32 => Some(FloatWidth::F32), - _ => None, - } - } -} - -#[repr(u8)] -#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)] -pub enum IntWidth { - U8 = 0, - U16 = 1, - U32 = 2, - U64 = 3, - U128 = 4, - I8 = 5, - I16 = 6, - I32 = 7, - I64 = 8, - I128 = 9, -} - -impl IntWidth { - pub const fn is_signed(&self) -> bool { - use IntWidth::*; - - matches!(self, I8 | I16 | I32 | I64 | I128) - } - - pub const fn stack_size(&self) -> u32 { - use IntWidth::*; - - // NOTE: this must never use mem::size_of, because that returns the size - // for the target of *the compiler itself* (e.g. this Rust code), not what - // the compiler is targeting (e.g. what the Roc code will be compiled to). - match self { - U8 | I8 => 1, - U16 | I16 => 2, - U32 | I32 => 4, - U64 | I64 => 8, - U128 | I128 => 16, - } - } - - pub const fn alignment_bytes(&self, target_info: TargetInfo) -> u32 { - use roc_target::Architecture; - use IntWidth::*; - - // NOTE: this must never use mem::align_of, because that returns the alignment - // for the target of *the compiler itself* (e.g. this Rust code), not what - // the compiler is targeting (e.g. what the Roc code will be compiled to). - match self { - U8 | I8 => 1, - U16 | I16 => 2, - U32 | I32 => 4, - U64 | I64 => match target_info.architecture { - Architecture::X86_64 - | Architecture::Aarch64 - | Architecture::Aarch32 - | Architecture::Wasm32 => 8, - Architecture::X86_32 => 4, - }, - U128 | I128 => { - // the C ABI defines 128-bit integers to always be 16B aligned, - // according to https://reviews.llvm.org/D28990#655487 - 16 - } - } - } - - pub const fn try_from_symbol(symbol: Symbol) -> Option { - match symbol { - Symbol::NUM_I128 | Symbol::NUM_SIGNED128 => Some(IntWidth::I128), - Symbol::NUM_I64 | Symbol::NUM_SIGNED64 => Some(IntWidth::I64), - Symbol::NUM_I32 | Symbol::NUM_SIGNED32 => Some(IntWidth::I32), - Symbol::NUM_I16 | Symbol::NUM_SIGNED16 => Some(IntWidth::I16), - Symbol::NUM_I8 | Symbol::NUM_SIGNED8 => Some(IntWidth::I8), - Symbol::NUM_U128 | Symbol::NUM_UNSIGNED128 => Some(IntWidth::U128), - Symbol::NUM_U64 | Symbol::NUM_UNSIGNED64 => Some(IntWidth::U64), - Symbol::NUM_U32 | Symbol::NUM_UNSIGNED32 => Some(IntWidth::U32), - Symbol::NUM_U16 | Symbol::NUM_UNSIGNED16 => Some(IntWidth::U16), - Symbol::NUM_U8 | Symbol::NUM_UNSIGNED8 => Some(IntWidth::U8), - _ => None, - } - } - - pub const fn type_name(&self) -> &'static str { - match self { - Self::I8 => "i8", - Self::I16 => "i16", - Self::I32 => "i32", - Self::I64 => "i64", - Self::I128 => "i128", - Self::U8 => "u8", - Self::U16 => "u16", - Self::U32 => "u32", - Self::U64 => "u64", - Self::U128 => "u128", - } - } -} - -impl Index for IntrinsicName { - type Output = str; - - fn index(&self, _: DecWidth) -> &Self::Output { - self.options[0] - } -} - -impl Index for IntrinsicName { - type Output = str; - - fn index(&self, index: FloatWidth) -> &Self::Output { - match index { - FloatWidth::F32 => self.options[1], - FloatWidth::F64 => self.options[2], - FloatWidth::F128 => self.options[3], - } - } -} - -impl Index for IntrinsicName { - type Output = str; - - fn index(&self, index: IntWidth) -> &Self::Output { - match index { - IntWidth::U8 => self.options[4], - IntWidth::U16 => self.options[5], - IntWidth::U32 => self.options[6], - IntWidth::U64 => self.options[7], - IntWidth::U128 => self.options[8], - IntWidth::I8 => self.options[9], - IntWidth::I16 => self.options[10], - IntWidth::I32 => self.options[11], - IntWidth::I64 => self.options[12], - IntWidth::I128 => self.options[13], - } - } -} - -#[macro_export] -macro_rules! float_intrinsic { - ($name:literal) => {{ - let mut output = IntrinsicName::default(); - - output.options[1] = concat!($name, ".f32"); - output.options[2] = concat!($name, ".f64"); - output.options[3] = concat!($name, ".f128"); - - output - }}; -} - -#[macro_export] -macro_rules! llvm_int_intrinsic { - ($signed_name:literal, $unsigned_name:literal) => {{ - let mut output = IntrinsicName::default(); - - // The indeces align with the `Index` impl for `IntrinsicName`. - // LLVM uses the same types for both signed and unsigned integers. - output.options[4] = concat!($unsigned_name, ".i8"); - output.options[5] = concat!($unsigned_name, ".i16"); - output.options[6] = concat!($unsigned_name, ".i32"); - output.options[7] = concat!($unsigned_name, ".i64"); - output.options[8] = concat!($unsigned_name, ".i128"); - - output.options[9] = concat!($signed_name, ".i8"); - output.options[10] = concat!($signed_name, ".i16"); - output.options[11] = concat!($signed_name, ".i32"); - output.options[12] = concat!($signed_name, ".i64"); - output.options[13] = concat!($signed_name, ".i128"); - - output - }}; - - ($name:literal) => { - int_intrinsic!($name, $name) - }; -} - -#[macro_export] -macro_rules! int_intrinsic { - ($name:expr) => {{ - let mut output = IntrinsicName::default(); - - // The indices align with the `Index` impl for `IntrinsicName`. - output.options[4] = concat!($name, ".u8"); - output.options[5] = concat!($name, ".u16"); - output.options[6] = concat!($name, ".u32"); - output.options[7] = concat!($name, ".u64"); - output.options[8] = concat!($name, ".u128"); - - output.options[9] = concat!($name, ".i8"); - output.options[10] = concat!($name, ".i16"); - output.options[11] = concat!($name, ".i32"); - output.options[12] = concat!($name, ".i64"); - output.options[13] = concat!($name, ".i128"); - - output - }}; -} - -pub const NUM_ASIN: IntrinsicName = float_intrinsic!("roc_builtins.num.asin"); -pub const NUM_ACOS: IntrinsicName = float_intrinsic!("roc_builtins.num.acos"); -pub const NUM_ATAN: IntrinsicName = float_intrinsic!("roc_builtins.num.atan"); -pub const NUM_IS_FINITE: IntrinsicName = float_intrinsic!("roc_builtins.num.is_finite"); -pub const NUM_POW_INT: IntrinsicName = int_intrinsic!("roc_builtins.num.pow_int"); -pub const NUM_DIV_CEIL: IntrinsicName = int_intrinsic!("roc_builtins.num.div_ceil"); - -pub const NUM_ROUND_F32: IntrinsicName = int_intrinsic!("roc_builtins.num.round_f32"); -pub const NUM_ROUND_F64: IntrinsicName = int_intrinsic!("roc_builtins.num.round_f64"); - -pub const NUM_BYTES_TO_U16: &str = "roc_builtins.num.bytes_to_u16"; -pub const NUM_BYTES_TO_U32: &str = "roc_builtins.num.bytes_to_u32"; - -pub const STR_INIT: &str = "roc_builtins.str.init"; -pub const STR_COUNT_SEGMENTS: &str = "roc_builtins.str.count_segments"; -pub const STR_CONCAT: &str = "roc_builtins.str.concat"; -pub const STR_JOIN_WITH: &str = "roc_builtins.str.joinWith"; -pub const STR_STR_SPLIT_IN_PLACE: &str = "roc_builtins.str.str_split_in_place"; -pub const STR_COUNT_GRAPEHEME_CLUSTERS: &str = "roc_builtins.str.count_grapheme_clusters"; -pub const STR_STARTS_WITH: &str = "roc_builtins.str.starts_with"; -pub const STR_STARTS_WITH_CODE_PT: &str = "roc_builtins.str.starts_with_code_point"; -pub const STR_ENDS_WITH: &str = "roc_builtins.str.ends_with"; -pub const STR_NUMBER_OF_BYTES: &str = "roc_builtins.str.number_of_bytes"; -pub const STR_FROM_INT: IntrinsicName = int_intrinsic!("roc_builtins.str.from_int"); -pub const STR_FROM_FLOAT: &str = "roc_builtins.str.from_float"; -pub const STR_TO_INT: IntrinsicName = int_intrinsic!("roc_builtins.str.to_int"); -pub const STR_TO_FLOAT: IntrinsicName = float_intrinsic!("roc_builtins.str.to_float"); -pub const STR_TO_DECIMAL: &str = "roc_builtins.str.to_decimal"; -pub const STR_EQUAL: &str = "roc_builtins.str.equal"; -pub const STR_TO_UTF8: &str = "roc_builtins.str.to_utf8"; -pub const STR_FROM_UTF8: &str = "roc_builtins.str.from_utf8"; -pub const STR_FROM_UTF8_RANGE: &str = "roc_builtins.str.from_utf8_range"; -pub const STR_REPEAT: &str = "roc_builtins.str.repeat"; -pub const STR_TRIM: &str = "roc_builtins.str.trim"; -pub const STR_TRIM_LEFT: &str = "roc_builtins.str.trim_left"; -pub const STR_TRIM_RIGHT: &str = "roc_builtins.str.trim_right"; - -pub const DICT_HASH: &str = "roc_builtins.dict.hash"; -pub const DICT_HASH_STR: &str = "roc_builtins.dict.hash_str"; -pub const DICT_LEN: &str = "roc_builtins.dict.len"; -pub const DICT_EMPTY: &str = "roc_builtins.dict.empty"; -pub const DICT_INSERT: &str = "roc_builtins.dict.insert"; -pub const DICT_REMOVE: &str = "roc_builtins.dict.remove"; -pub const DICT_CONTAINS: &str = "roc_builtins.dict.contains"; -pub const DICT_GET: &str = "roc_builtins.dict.get"; -pub const DICT_ELEMENTS_RC: &str = "roc_builtins.dict.elementsRc"; -pub const DICT_KEYS: &str = "roc_builtins.dict.keys"; -pub const DICT_VALUES: &str = "roc_builtins.dict.values"; -pub const DICT_UNION: &str = "roc_builtins.dict.union"; -pub const DICT_DIFFERENCE: &str = "roc_builtins.dict.difference"; -pub const DICT_INTERSECTION: &str = "roc_builtins.dict.intersection"; -pub const DICT_WALK: &str = "roc_builtins.dict.walk"; - -pub const SET_FROM_LIST: &str = "roc_builtins.dict.set_from_list"; - -pub const LIST_MAP: &str = "roc_builtins.list.map"; -pub const LIST_MAP2: &str = "roc_builtins.list.map2"; -pub const LIST_MAP3: &str = "roc_builtins.list.map3"; -pub const LIST_MAP4: &str = "roc_builtins.list.map4"; -pub const LIST_MAP_WITH_INDEX: &str = "roc_builtins.list.map_with_index"; -pub const LIST_KEEP_IF: &str = "roc_builtins.list.keep_if"; -pub const LIST_KEEP_OKS: &str = "roc_builtins.list.keep_oks"; -pub const LIST_KEEP_ERRS: &str = "roc_builtins.list.keep_errs"; -pub const LIST_WALK: &str = "roc_builtins.list.walk"; -pub const LIST_WALK_UNTIL: &str = "roc_builtins.list.walkUntil"; -pub const LIST_WALK_BACKWARDS: &str = "roc_builtins.list.walk_backwards"; -pub const LIST_CONTAINS: &str = "roc_builtins.list.contains"; -pub const LIST_REPEAT: &str = "roc_builtins.list.repeat"; -pub const LIST_APPEND: &str = "roc_builtins.list.append"; -pub const LIST_PREPEND: &str = "roc_builtins.list.prepend"; -pub const LIST_SUBLIST: &str = "roc_builtins.list.sublist"; -pub const LIST_DROP_AT: &str = "roc_builtins.list.drop_at"; -pub const LIST_SWAP: &str = "roc_builtins.list.swap"; -pub const LIST_SINGLE: &str = "roc_builtins.list.single"; -pub const LIST_JOIN: &str = "roc_builtins.list.join"; -pub const LIST_RANGE: &str = "roc_builtins.list.range"; -pub const LIST_REVERSE: &str = "roc_builtins.list.reverse"; -pub const LIST_SORT_WITH: &str = "roc_builtins.list.sort_with"; -pub const LIST_CONCAT: &str = "roc_builtins.list.concat"; -pub const LIST_REPLACE: &str = "roc_builtins.list.replace"; -pub const LIST_REPLACE_IN_PLACE: &str = "roc_builtins.list.replace_in_place"; -pub const LIST_ANY: &str = "roc_builtins.list.any"; -pub const LIST_ALL: &str = "roc_builtins.list.all"; -pub const LIST_FIND_UNSAFE: &str = "roc_builtins.list.find_unsafe"; -pub const LIST_IS_UNIQUE: &str = "roc_builtins.list.is_unique"; - -pub const DEC_FROM_STR: &str = "roc_builtins.dec.from_str"; -pub const DEC_FROM_F64: &str = "roc_builtins.dec.from_f64"; -pub const DEC_EQ: &str = "roc_builtins.dec.eq"; -pub const DEC_NEQ: &str = "roc_builtins.dec.neq"; -pub const DEC_NEGATE: &str = "roc_builtins.dec.negate"; -pub const DEC_ADD_WITH_OVERFLOW: &str = "roc_builtins.dec.add_with_overflow"; -pub const DEC_SUB_WITH_OVERFLOW: &str = "roc_builtins.dec.sub_with_overflow"; -pub const DEC_MUL_WITH_OVERFLOW: &str = "roc_builtins.dec.mul_with_overflow"; -pub const DEC_DIV: &str = "roc_builtins.dec.div"; - -pub const UTILS_TEST_PANIC: &str = "roc_builtins.utils.test_panic"; -pub const UTILS_ALLOCATE_WITH_REFCOUNT: &str = "roc_builtins.utils.allocate_with_refcount"; -pub const UTILS_INCREF: &str = "roc_builtins.utils.incref"; -pub const UTILS_DECREF: &str = "roc_builtins.utils.decref"; -pub const UTILS_DECREF_CHECK_NULL: &str = "roc_builtins.utils.decref_check_null"; -pub const UTILS_EXPECT_FAILED: &str = "roc_builtins.expect.expect_failed"; -pub const UTILS_GET_EXPECT_FAILURES: &str = "roc_builtins.expect.get_expect_failures"; -pub const UTILS_DEINIT_FAILURES: &str = "roc_builtins.expect.deinit_failures"; - -pub const UTILS_LONGJMP: &str = "longjmp"; -pub const UTILS_SETJMP: &str = "setjmp"; - -#[derive(Debug, Default)] -pub struct IntToIntrinsicName { - pub options: [IntrinsicName; 10], -} - -impl IntToIntrinsicName { - pub const fn default() -> Self { - Self { - options: [IntrinsicName::default(); 10], - } - } -} - -impl Index for IntToIntrinsicName { - type Output = IntrinsicName; - - fn index(&self, index: IntWidth) -> &Self::Output { - &self.options[index as usize] - } -} - -#[macro_export] -macro_rules! int_to_int_intrinsic { - ($name_prefix:literal, $name_suffix:literal) => {{ - let mut output = IntToIntrinsicName::default(); - - output.options[0] = int_intrinsic!(concat!($name_prefix, "u8", $name_suffix)); - output.options[1] = int_intrinsic!(concat!($name_prefix, "u16", $name_suffix)); - output.options[2] = int_intrinsic!(concat!($name_prefix, "u32", $name_suffix)); - output.options[3] = int_intrinsic!(concat!($name_prefix, "u64", $name_suffix)); - output.options[4] = int_intrinsic!(concat!($name_prefix, "u128", $name_suffix)); - - output.options[5] = int_intrinsic!(concat!($name_prefix, "i8", $name_suffix)); - output.options[6] = int_intrinsic!(concat!($name_prefix, "i16", $name_suffix)); - output.options[7] = int_intrinsic!(concat!($name_prefix, "i32", $name_suffix)); - output.options[8] = int_intrinsic!(concat!($name_prefix, "i64", $name_suffix)); - output.options[9] = int_intrinsic!(concat!($name_prefix, "i128", $name_suffix)); - - output - }}; -} - -pub const NUM_INT_TO_INT_CHECKING_MAX: IntToIntrinsicName = - int_to_int_intrinsic!("roc_builtins.num.int_to_", "_checking_max"); -pub const NUM_INT_TO_INT_CHECKING_MAX_AND_MIN: IntToIntrinsicName = - int_to_int_intrinsic!("roc_builtins.num.int_to_", "_checking_max_and_min"); diff --git a/compiler/builtins/src/lib.rs b/compiler/builtins/src/lib.rs deleted file mode 100644 index 51ce65ebfb..0000000000 --- a/compiler/builtins/src/lib.rs +++ /dev/null @@ -1,6 +0,0 @@ -#![warn(clippy::dbg_macro)] -// See github.com/rtfeldman/roc/issues/800 for discussion of the large_enum_variant check. -#![allow(clippy::large_enum_variant)] -pub mod bitcode; -pub mod roc; -pub mod std; diff --git a/compiler/builtins/src/roc.rs b/compiler/builtins/src/roc.rs deleted file mode 100644 index 430a9135ed..0000000000 --- a/compiler/builtins/src/roc.rs +++ /dev/null @@ -1,32 +0,0 @@ -use roc_module::symbol::ModuleId; - -#[inline(always)] -pub fn module_source(module_id: ModuleId) -> &'static str { - match module_id { - ModuleId::RESULT => RESULT, - ModuleId::NUM => NUM, - ModuleId::STR => STR, - ModuleId::LIST => LIST, - ModuleId::DICT => DICT, - ModuleId::SET => SET, - ModuleId::BOX => BOX, - ModuleId::BOOL => BOOL, - ModuleId::ENCODE => ENCODE, - ModuleId::JSON => JSON, - _ => panic!( - "ModuleId {:?} is not part of the standard library", - module_id - ), - } -} - -const RESULT: &str = include_str!("../roc/Result.roc"); -const NUM: &str = include_str!("../roc/Num.roc"); -const STR: &str = include_str!("../roc/Str.roc"); -const LIST: &str = include_str!("../roc/List.roc"); -const DICT: &str = include_str!("../roc/Dict.roc"); -const SET: &str = include_str!("../roc/Set.roc"); -const BOX: &str = include_str!("../roc/Box.roc"); -const BOOL: &str = include_str!("../roc/Bool.roc"); -const ENCODE: &str = include_str!("../roc/Encode.roc"); -const JSON: &str = include_str!("../roc/Json.roc"); diff --git a/compiler/builtins/src/std.rs b/compiler/builtins/src/std.rs deleted file mode 100644 index 3c15249dbb..0000000000 --- a/compiler/builtins/src/std.rs +++ /dev/null @@ -1,1876 +0,0 @@ -use roc_collections::all::{default_hasher, MutMap, MutSet}; -use roc_module::ident::TagName; -use roc_module::symbol::Symbol; -use roc_region::all::Region; -use roc_types::builtin_aliases::{ - bool_type, box_type, dec_type, dict_type, f32_type, f64_type, frac_type, i128_type, i16_type, - i32_type, i64_type, i8_type, int_type, list_type, nat_type, num_type, ordering_type, - result_type, set_type, str_type, str_utf8_byte_problem_type, u128_type, u16_type, u32_type, - u64_type, u8_type, -}; -use roc_types::solved_types::SolvedType; -use roc_types::subs::VarId; -use roc_types::types::RecordField; -use std::collections::HashMap; - -lazy_static::lazy_static! { - static ref STDLIB: StdLib = standard_stdlib(); -} - -/// A global static that stores our initialized standard library definitions -pub fn borrow_stdlib() -> &'static StdLib { - &STDLIB -} - -/// Example: -/// -/// let_tvars! { a, b, c } -/// -/// This is equivalent to: -/// -/// let a = VarId::from_u32(1); -/// let b = VarId::from_u32(2); -/// let c = VarId::from_u32(3); -/// -/// The idea is that this is less error-prone than assigning hardcoded IDs by hand. -macro_rules! let_tvars { - ($($name:ident,)+) => { let_tvars!($($name),+) }; - ($($name:ident),*) => { - let mut _current_tvar = 0; - - $( - _current_tvar += 1; - - let $name = VarId::from_u32(_current_tvar); - )* - }; -} - -#[derive(Debug, Clone)] -pub struct StdLib { - pub types: MutMap, - pub applies: MutSet, -} - -pub fn standard_stdlib() -> StdLib { - StdLib { - types: types(), - applies: vec![ - Symbol::LIST_LIST, - Symbol::SET_SET, - Symbol::DICT_DICT, - Symbol::STR_STR, - ] - .into_iter() - .collect(), - } -} - -/// Keep this up to date by hand! It's the number of builtin aliases that are imported by default. -const NUM_BUILTIN_IMPORTS: usize = 7; - -/// These can be shared between definitions, they will get instantiated when converted to Type -const TVAR1: VarId = VarId::from_u32(1); -const TVAR2: VarId = VarId::from_u32(2); -const TVAR3: VarId = VarId::from_u32(3); -const TVAR4: VarId = VarId::from_u32(4); -pub fn types() -> MutMap { - let mut types = HashMap::with_capacity_and_hasher(NUM_BUILTIN_IMPORTS, default_hasher()); - - macro_rules! add_type { - ($symbol:expr, $typ:expr $(,)?) => {{ - debug_assert!( - !types.contains_key(&$symbol), - "Duplicate type definition for {:?}", - $symbol - ); - - // TODO instead of using Region::zero for all of these, - // instead use the Region where they were defined in their - // source .roc files! This can give nicer error messages. - types.insert($symbol, ($typ, Region::zero())); - }}; - } - - macro_rules! add_top_level_function_type { - ($symbol:expr, $arguments:expr, $result:expr $(,)?) => {{ - debug_assert!( - !types.contains_key(&$symbol), - "Duplicate type definition for {:?}", - $symbol - ); - - let typ = SolvedType::Func( - $arguments, - Box::new(SolvedType::LambdaTag($symbol, vec![])), - $result, - ); - - // TODO instead of using Region::zero for all of these, - // instead use the Region where they were defined in their - // source .roc files! This can give nicer error messages. - types.insert($symbol, (typ, Region::zero())); - }}; - } - - // Num module - - // add or (+) : Num a, Num a -> Num a - add_top_level_function_type!( - Symbol::NUM_ADD, - vec![num_type(flex(TVAR1)), num_type(flex(TVAR1))], - Box::new(num_type(flex(TVAR1))), - ); - - fn overflow() -> SolvedType { - SolvedType::TagUnion( - vec![(TagName("Overflow".into()), vec![])], - Box::new(SolvedType::Wildcard), - ) - } - - // addChecked : Num a, Num a -> Result (Num a) [Overflow]* - add_top_level_function_type!( - Symbol::NUM_ADD_CHECKED, - vec![num_type(flex(TVAR1)), num_type(flex(TVAR1))], - Box::new(result_type(num_type(flex(TVAR1)), overflow())), - ); - - // addWrap : Int range, Int range -> Int range - add_top_level_function_type!( - Symbol::NUM_ADD_WRAP, - vec![int_type(flex(TVAR1)), int_type(flex(TVAR1))], - Box::new(int_type(flex(TVAR1))), - ); - - // addSaturated : Num a, Num a -> Num a - add_top_level_function_type!( - Symbol::NUM_ADD_SATURATED, - vec![int_type(flex(TVAR1)), int_type(flex(TVAR1))], - Box::new(int_type(flex(TVAR1))), - ); - - // sub or (-) : Num a, Num a -> Num a - add_top_level_function_type!( - Symbol::NUM_SUB, - vec![num_type(flex(TVAR1)), num_type(flex(TVAR1))], - Box::new(num_type(flex(TVAR1))), - ); - - // subWrap : Int range, Int range -> Int range - add_top_level_function_type!( - Symbol::NUM_SUB_WRAP, - vec![int_type(flex(TVAR1)), int_type(flex(TVAR1))], - Box::new(int_type(flex(TVAR1))), - ); - - // subChecked : Num a, Num a -> Result (Num a) [Overflow]* - add_top_level_function_type!( - Symbol::NUM_SUB_CHECKED, - vec![num_type(flex(TVAR1)), num_type(flex(TVAR1))], - Box::new(result_type(num_type(flex(TVAR1)), overflow())), - ); - - // subSaturated : Num a, Num a -> Num a - add_top_level_function_type!( - Symbol::NUM_SUB_SATURATED, - vec![int_type(flex(TVAR1)), int_type(flex(TVAR1))], - Box::new(int_type(flex(TVAR1))), - ); - - // mul or (*) : Num a, Num a -> Num a - add_top_level_function_type!( - Symbol::NUM_MUL, - vec![num_type(flex(TVAR1)), num_type(flex(TVAR1))], - Box::new(num_type(flex(TVAR1))), - ); - - // mulWrap : Int range, Int range -> Int range - add_top_level_function_type!( - Symbol::NUM_MUL_WRAP, - vec![int_type(flex(TVAR1)), int_type(flex(TVAR1))], - Box::new(int_type(flex(TVAR1))), - ); - - // mulChecked : Num a, Num a -> Result (Num a) [Overflow]* - add_top_level_function_type!( - Symbol::NUM_MUL_CHECKED, - vec![num_type(flex(TVAR1)), num_type(flex(TVAR1))], - Box::new(result_type(num_type(flex(TVAR1)), overflow())), - ); - - // abs : Num a -> Num a - add_top_level_function_type!( - Symbol::NUM_ABS, - vec![num_type(flex(TVAR1))], - Box::new(num_type(flex(TVAR1))) - ); - - // neg : Num a -> Num a - add_top_level_function_type!( - Symbol::NUM_NEG, - vec![num_type(flex(TVAR1))], - Box::new(num_type(flex(TVAR1))) - ); - - // isEq or (==) : a, a -> Bool - add_top_level_function_type!( - Symbol::BOOL_EQ, - vec![flex(TVAR1), flex(TVAR1)], - Box::new(bool_type()) - ); - - // isNeq or (!=) : a, a -> Bool - add_top_level_function_type!( - Symbol::BOOL_NEQ, - vec![flex(TVAR1), flex(TVAR1)], - Box::new(bool_type()) - ); - - // isLt or (<) : Num a, Num a -> Bool - add_top_level_function_type!( - Symbol::NUM_LT, - vec![num_type(flex(TVAR1)), num_type(flex(TVAR1))], - Box::new(bool_type()), - ); - - // isLte or (<=) : Num a, Num a -> Bool - add_top_level_function_type!( - Symbol::NUM_LTE, - vec![num_type(flex(TVAR1)), num_type(flex(TVAR1))], - Box::new(bool_type()), - ); - - // isGt or (>) : Num a, Num a -> Bool - add_top_level_function_type!( - Symbol::NUM_GT, - vec![num_type(flex(TVAR1)), num_type(flex(TVAR1))], - Box::new(bool_type()), - ); - - // isGte or (>=) : Num a, Num a -> Bool - add_top_level_function_type!( - Symbol::NUM_GTE, - vec![num_type(flex(TVAR1)), num_type(flex(TVAR1))], - Box::new(bool_type()), - ); - - // compare : Num a, Num a -> [LT, EQ, GT] - add_top_level_function_type!( - Symbol::NUM_COMPARE, - vec![num_type(flex(TVAR1)), num_type(flex(TVAR1))], - Box::new(ordering_type()), - ); - - // toFrac : Num * -> Frac * - add_top_level_function_type!( - Symbol::NUM_TO_FRAC, - vec![num_type(flex(TVAR1))], - Box::new(frac_type(flex(TVAR2))), - ); - - // isNegative : Num a -> Bool - add_top_level_function_type!( - Symbol::NUM_IS_NEGATIVE, - vec![num_type(flex(TVAR1))], - Box::new(bool_type()) - ); - - // isPositive : Num a -> Bool - add_top_level_function_type!( - Symbol::NUM_IS_POSITIVE, - vec![num_type(flex(TVAR1))], - Box::new(bool_type()) - ); - - // isZero : Num a -> Bool - add_top_level_function_type!( - Symbol::NUM_IS_ZERO, - vec![num_type(flex(TVAR1))], - Box::new(bool_type()) - ); - - // isEven : Num a -> Bool - add_top_level_function_type!( - Symbol::NUM_IS_EVEN, - vec![num_type(flex(TVAR1))], - Box::new(bool_type()) - ); - - // isOdd : Num a -> Bool - add_top_level_function_type!( - Symbol::NUM_IS_ODD, - vec![num_type(flex(TVAR1))], - Box::new(bool_type()) - ); - - let div_by_zero = SolvedType::TagUnion( - vec![(TagName("DivByZero".into()), vec![])], - Box::new(SolvedType::Wildcard), - ); - - // divTrunc : Int a, Int a -> Int a - add_top_level_function_type!( - Symbol::NUM_DIV_TRUNC, - vec![int_type(flex(TVAR1)), int_type(flex(TVAR1))], - Box::new(int_type(flex(TVAR1))) - ); - - // divTruncChecked : Int a, Int a -> Result (Int a) [DivByZero]* - add_top_level_function_type!( - Symbol::NUM_DIV_TRUNC_CHECKED, - vec![int_type(flex(TVAR1)), int_type(flex(TVAR1))], - Box::new(result_type(int_type(flex(TVAR1)), div_by_zero.clone())), - ); - - // divCeil : Int a, Int a -> Int a - add_top_level_function_type!( - Symbol::NUM_DIV_CEIL, - vec![int_type(flex(TVAR1)), int_type(flex(TVAR1))], - Box::new(int_type(flex(TVAR1))) - ); - - // divCeilChecked : Int a, Int a -> Result (Int a) [DivByZero]* - add_top_level_function_type!( - Symbol::NUM_DIV_CEIL_CHECKED, - vec![int_type(flex(TVAR1)), int_type(flex(TVAR1))], - Box::new(result_type(int_type(flex(TVAR1)), div_by_zero.clone())), - ); - - // bitwiseAnd : Int a, Int a -> Int a - add_top_level_function_type!( - Symbol::NUM_BITWISE_AND, - vec![int_type(flex(TVAR1)), int_type(flex(TVAR1))], - Box::new(int_type(flex(TVAR1))), - ); - - // bitwiseXor : Int a, Int a -> Int a - add_top_level_function_type!( - Symbol::NUM_BITWISE_XOR, - vec![int_type(flex(TVAR1)), int_type(flex(TVAR1))], - Box::new(int_type(flex(TVAR1))), - ); - - // bitwiseOr : Int a, Int a -> Int a - add_top_level_function_type!( - Symbol::NUM_BITWISE_OR, - vec![int_type(flex(TVAR1)), int_type(flex(TVAR1))], - Box::new(int_type(flex(TVAR1))), - ); - - // shiftLeftBy : Int a, Int a -> Int a - add_top_level_function_type!( - Symbol::NUM_SHIFT_LEFT, - vec![int_type(flex(TVAR1)), int_type(flex(TVAR1))], - Box::new(int_type(flex(TVAR1))), - ); - - // shiftRightBy : Int a, Int a -> Int a - add_top_level_function_type!( - Symbol::NUM_SHIFT_RIGHT, - vec![int_type(flex(TVAR1)), int_type(flex(TVAR1))], - Box::new(int_type(flex(TVAR1))), - ); - - // shiftRightZfBy : Int a, Int a -> Int a - add_top_level_function_type!( - Symbol::NUM_SHIFT_RIGHT_ZERO_FILL, - vec![int_type(flex(TVAR1)), int_type(flex(TVAR1))], - Box::new(int_type(flex(TVAR1))), - ); - - // intCast : Int a -> Int b - add_top_level_function_type!( - Symbol::NUM_INT_CAST, - vec![int_type(flex(TVAR1))], - Box::new(int_type(flex(TVAR2))) - ); - - // rem : Int a, Int a -> Int a - add_top_level_function_type!( - Symbol::NUM_REM, - vec![int_type(flex(TVAR1)), int_type(flex(TVAR1))], - Box::new(int_type(flex(TVAR1))), - ); - - // remChecked : Int a, Int a -> Result (Int a) [DivByZero]* - add_top_level_function_type!( - Symbol::NUM_REM_CHECKED, - vec![int_type(flex(TVAR1)), int_type(flex(TVAR1))], - Box::new(result_type(int_type(flex(TVAR1)), div_by_zero.clone())), - ); - - // isMultipleOf : Int a, Int a -> Bool - add_top_level_function_type!( - Symbol::NUM_IS_MULTIPLE_OF, - vec![int_type(flex(TVAR1)), int_type(flex(TVAR1))], - Box::new(bool_type()), - ); - - // minI8 : I8 - add_type!(Symbol::NUM_MIN_I8, i8_type()); - - // maxI8 : I8 - add_type!(Symbol::NUM_MAX_I8, i8_type()); - - // minU8 : U8 - add_type!(Symbol::NUM_MIN_U8, u8_type()); - - // maxU8 : U8 - add_type!(Symbol::NUM_MAX_U8, u8_type()); - - // minI16 : I16 - add_type!(Symbol::NUM_MIN_I16, i16_type()); - - // maxI16 : I16 - add_type!(Symbol::NUM_MAX_I16, i16_type()); - - // minU16 : U16 - add_type!(Symbol::NUM_MIN_U16, u16_type()); - - // maxU16 : U16 - add_type!(Symbol::NUM_MAX_U16, u16_type()); - - // minI32 : I32 - add_type!(Symbol::NUM_MIN_I32, i32_type()); - - // maxI32 : I32 - add_type!(Symbol::NUM_MAX_I32, i32_type()); - - // minU32 : U32 - add_type!(Symbol::NUM_MIN_U32, u32_type()); - - // maxU32 : U32 - add_type!(Symbol::NUM_MAX_U32, u32_type()); - - // minI64 : I64 - add_type!(Symbol::NUM_MIN_I64, i64_type()); - - // maxI64 : I64 - add_type!(Symbol::NUM_MAX_I64, i64_type()); - - // minU64 : U64 - add_type!(Symbol::NUM_MIN_U64, u64_type()); - - // maxU64 : U64 - add_type!(Symbol::NUM_MAX_U64, u64_type()); - - // minI128 : I128 - add_type!(Symbol::NUM_MIN_I128, i128_type()); - - // maxI128 : I128 - add_type!(Symbol::NUM_MAX_I128, i128_type()); - - // toI8 : Int * -> I8 - add_top_level_function_type!( - Symbol::NUM_TO_I8, - vec![int_type(flex(TVAR1))], - Box::new(i8_type()), - ); - - let out_of_bounds = SolvedType::TagUnion( - vec![(TagName("OutOfBounds".into()), vec![])], - Box::new(SolvedType::Wildcard), - ); - - // toI8Checked : Int * -> Result I8 [OutOfBounds]* - add_top_level_function_type!( - Symbol::NUM_TO_I8_CHECKED, - vec![int_type(flex(TVAR1))], - Box::new(result_type(i8_type(), out_of_bounds.clone())), - ); - - // toI16 : Int * -> I16 - add_top_level_function_type!( - Symbol::NUM_TO_I16, - vec![int_type(flex(TVAR1))], - Box::new(i16_type()), - ); - - // toI16Checked : Int * -> Result I16 [OutOfBounds]* - add_top_level_function_type!( - Symbol::NUM_TO_I16_CHECKED, - vec![int_type(flex(TVAR1))], - Box::new(result_type(i16_type(), out_of_bounds.clone())), - ); - - // toI32 : Int * -> I32 - add_top_level_function_type!( - Symbol::NUM_TO_I32, - vec![int_type(flex(TVAR1))], - Box::new(i32_type()), - ); - - // toI32Checked : Int * -> Result I32 [OutOfBounds]* - add_top_level_function_type!( - Symbol::NUM_TO_I32_CHECKED, - vec![int_type(flex(TVAR1))], - Box::new(result_type(i32_type(), out_of_bounds.clone())), - ); - - // toI64 : Int * -> I64 - add_top_level_function_type!( - Symbol::NUM_TO_I64, - vec![int_type(flex(TVAR1))], - Box::new(i64_type()), - ); - - // toI64Checked : Int * -> Result I64 [OutOfBounds]* - add_top_level_function_type!( - Symbol::NUM_TO_I64_CHECKED, - vec![int_type(flex(TVAR1))], - Box::new(result_type(i64_type(), out_of_bounds.clone())), - ); - - // toI128 : Int * -> I128 - add_top_level_function_type!( - Symbol::NUM_TO_I128, - vec![int_type(flex(TVAR1))], - Box::new(i128_type()), - ); - - // toI128Checked : Int * -> Result I128 [OutOfBounds]* - add_top_level_function_type!( - Symbol::NUM_TO_I128_CHECKED, - vec![int_type(flex(TVAR1))], - Box::new(result_type(i128_type(), out_of_bounds)), - ); - - // toU8 : Int * -> U8 - add_top_level_function_type!( - Symbol::NUM_TO_U8, - vec![int_type(flex(TVAR1))], - Box::new(u8_type()), - ); - - let out_of_bounds = SolvedType::TagUnion( - vec![(TagName("OutOfBounds".into()), vec![])], - Box::new(SolvedType::Wildcard), - ); - - // toU8Checked : Int * -> Result U8 [OutOfBounds]* - add_top_level_function_type!( - Symbol::NUM_TO_U8_CHECKED, - vec![int_type(flex(TVAR1))], - Box::new(result_type(u8_type(), out_of_bounds.clone())), - ); - - // toU16 : Int * -> U16 - add_top_level_function_type!( - Symbol::NUM_TO_U16, - vec![int_type(flex(TVAR1))], - Box::new(u16_type()), - ); - - // toU16Checked : Int * -> Result U16 [OutOfBounds]* - add_top_level_function_type!( - Symbol::NUM_TO_U16_CHECKED, - vec![int_type(flex(TVAR1))], - Box::new(result_type(u16_type(), out_of_bounds.clone())), - ); - - // toU32 : Int * -> U32 - add_top_level_function_type!( - Symbol::NUM_TO_U32, - vec![int_type(flex(TVAR1))], - Box::new(u32_type()), - ); - - // toU32Checked : Int * -> Result U32 [OutOfBounds]* - add_top_level_function_type!( - Symbol::NUM_TO_U32_CHECKED, - vec![int_type(flex(TVAR1))], - Box::new(result_type(u32_type(), out_of_bounds.clone())), - ); - - // toU64 : Int * -> U64 - add_top_level_function_type!( - Symbol::NUM_TO_U64, - vec![int_type(flex(TVAR1))], - Box::new(u64_type()), - ); - - // toU64Checked : Int * -> Result U64 [OutOfBounds]* - add_top_level_function_type!( - Symbol::NUM_TO_U64_CHECKED, - vec![int_type(flex(TVAR1))], - Box::new(result_type(u64_type(), out_of_bounds.clone())), - ); - - // toU128 : Int * -> U128 - add_top_level_function_type!( - Symbol::NUM_TO_U128, - vec![int_type(flex(TVAR1))], - Box::new(u128_type()), - ); - - // toU128Checked : Int * -> Result U128 [OutOfBounds]* - add_top_level_function_type!( - Symbol::NUM_TO_U128_CHECKED, - vec![int_type(flex(TVAR1))], - Box::new(result_type(u128_type(), out_of_bounds.clone())), - ); - - // toNat : Int * -> Nat - add_top_level_function_type!( - Symbol::NUM_TO_NAT, - vec![int_type(flex(TVAR1))], - Box::new(nat_type()), - ); - - // toNatChecked : Int * -> Result Nat [OutOfBounds]* - add_top_level_function_type!( - Symbol::NUM_TO_NAT_CHECKED, - vec![int_type(flex(TVAR1))], - Box::new(result_type(nat_type(), out_of_bounds.clone())), - ); - - // toF32 : Num * -> F32 - add_top_level_function_type!( - Symbol::NUM_TO_F32, - vec![num_type(flex(TVAR1))], - Box::new(f32_type()), - ); - - // toF32Checked : Num * -> Result F32 [OutOfBounds]* - add_top_level_function_type!( - Symbol::NUM_TO_F32_CHECKED, - vec![num_type(flex(TVAR1))], - Box::new(result_type(f32_type(), out_of_bounds.clone())), - ); - - // toF64 : Num * -> F64 - add_top_level_function_type!( - Symbol::NUM_TO_F64, - vec![num_type(flex(TVAR1))], - Box::new(f64_type()), - ); - - // toF64Checked : Num * -> Result F64 [OutOfBounds]* - add_top_level_function_type!( - Symbol::NUM_TO_F64_CHECKED, - vec![num_type(flex(TVAR1))], - Box::new(result_type(f64_type(), out_of_bounds)), - ); - - // toStr : Num a -> Str - add_top_level_function_type!( - Symbol::NUM_TO_STR, - vec![num_type(flex(TVAR1))], - Box::new(str_type()) - ); - - // Frac module - - // div : Frac a, Frac a -> Frac a - add_top_level_function_type!( - Symbol::NUM_DIV_FRAC, - vec![frac_type(flex(TVAR1)), frac_type(flex(TVAR1))], - Box::new(frac_type(flex(TVAR1))) - ); - - // divChecked : Frac a, Frac a -> Result (Frac a) [DivByZero]* - add_top_level_function_type!( - Symbol::NUM_DIV_FRAC_CHECKED, - vec![frac_type(flex(TVAR1)), frac_type(flex(TVAR1))], - Box::new(result_type(frac_type(flex(TVAR1)), div_by_zero)), - ); - - // sqrt : Frac a -> Frac a - add_top_level_function_type!( - Symbol::NUM_SQRT, - vec![frac_type(flex(TVAR1))], - Box::new(frac_type(flex(TVAR1))), - ); - - // sqrtChecked : Frac a -> Result (Frac a) [SqrtOfNegative]* - let sqrt_of_negative = SolvedType::TagUnion( - vec![(TagName("SqrtOfNegative".into()), vec![])], - Box::new(SolvedType::Wildcard), - ); - - add_top_level_function_type!( - Symbol::NUM_SQRT_CHECKED, - vec![frac_type(flex(TVAR1))], - Box::new(result_type(frac_type(flex(TVAR1)), sqrt_of_negative)), - ); - - // log : Frac a -> Frac a - add_top_level_function_type!( - Symbol::NUM_LOG, - vec![frac_type(flex(TVAR1))], - Box::new(frac_type(flex(TVAR1))), - ); - - // logChecked : Frac a -> Result (Frac a) [LogNeedsPositive]* - let log_needs_positive = SolvedType::TagUnion( - vec![(TagName("LogNeedsPositive".into()), vec![])], - Box::new(SolvedType::Wildcard), - ); - - add_top_level_function_type!( - Symbol::NUM_LOG_CHECKED, - vec![frac_type(flex(TVAR1))], - Box::new(result_type(frac_type(flex(TVAR1)), log_needs_positive)), - ); - - // round : Frac a -> Int b - add_top_level_function_type!( - Symbol::NUM_ROUND, - vec![frac_type(flex(TVAR1))], - Box::new(int_type(flex(TVAR2))), - ); - - // sin : Frac a -> Frac a - add_top_level_function_type!( - Symbol::NUM_SIN, - vec![frac_type(flex(TVAR1))], - Box::new(frac_type(flex(TVAR1))), - ); - - // cos : Frac a -> Frac a - add_top_level_function_type!( - Symbol::NUM_COS, - vec![frac_type(flex(TVAR1))], - Box::new(frac_type(flex(TVAR1))), - ); - - // tan : Frac a -> Frac a - add_top_level_function_type!( - Symbol::NUM_TAN, - vec![frac_type(flex(TVAR1))], - Box::new(frac_type(flex(TVAR1))), - ); - - // pow : Frac a, Frac a -> Frac a - add_top_level_function_type!( - Symbol::NUM_POW, - vec![frac_type(flex(TVAR1)), frac_type(flex(TVAR1))], - Box::new(frac_type(flex(TVAR1))), - ); - - // ceiling : Frac a -> Int b - add_top_level_function_type!( - Symbol::NUM_CEILING, - vec![frac_type(flex(TVAR1))], - Box::new(int_type(flex(TVAR2))), - ); - - // powInt : Int a, Int a -> Int a - add_top_level_function_type!( - Symbol::NUM_POW_INT, - vec![int_type(flex(TVAR1)), int_type(flex(TVAR1))], - Box::new(int_type(flex(TVAR1))), - ); - - // floor : Frac a -> Int b - add_top_level_function_type!( - Symbol::NUM_FLOOR, - vec![frac_type(flex(TVAR1))], - Box::new(int_type(flex(TVAR2))), - ); - - // atan : Frac a -> Frac a - add_top_level_function_type!( - Symbol::NUM_ATAN, - vec![frac_type(flex(TVAR1))], - Box::new(frac_type(flex(TVAR1))), - ); - - // acos : Frac a -> Frac a - add_top_level_function_type!( - Symbol::NUM_ACOS, - vec![frac_type(flex(TVAR1))], - Box::new(frac_type(flex(TVAR1))), - ); - - // asin : Frac a -> Frac a - add_top_level_function_type!( - Symbol::NUM_ASIN, - vec![frac_type(flex(TVAR1))], - Box::new(frac_type(flex(TVAR1))), - ); - - // bytesToU16 : List U8, Nat -> Result U16 [OutOfBounds] - { - let position_out_of_bounds = SolvedType::TagUnion( - vec![(TagName("OutOfBounds".into()), vec![])], - Box::new(SolvedType::Wildcard), - ); - add_top_level_function_type!( - Symbol::NUM_BYTES_TO_U16, - vec![list_type(u8_type()), nat_type()], - Box::new(result_type(u16_type(), position_out_of_bounds)), - ); - } - - // bytesToU32 : List U8, Nat -> Result U32 [OutOfBounds] - { - let position_out_of_bounds = SolvedType::TagUnion( - vec![(TagName("OutOfBounds".into()), vec![])], - Box::new(SolvedType::Wildcard), - ); - add_top_level_function_type!( - Symbol::NUM_BYTES_TO_U32, - vec![list_type(u8_type()), nat_type()], - Box::new(result_type(u32_type(), position_out_of_bounds)), - ); - } - - // Bool module - - // and : Bool, Bool -> Bool - add_top_level_function_type!( - Symbol::BOOL_AND, - vec![bool_type(), bool_type()], - Box::new(bool_type()) - ); - - // or : Bool, Bool -> Bool - add_top_level_function_type!( - Symbol::BOOL_OR, - vec![bool_type(), bool_type()], - Box::new(bool_type()) - ); - - // xor : Bool, Bool -> Bool - add_top_level_function_type!( - Symbol::BOOL_XOR, - vec![bool_type(), bool_type()], - Box::new(bool_type()) - ); - - // not : Bool -> Bool - add_top_level_function_type!(Symbol::BOOL_NOT, vec![bool_type()], Box::new(bool_type())); - - // Str module - - // Str.split : Str, Str -> List Str - add_top_level_function_type!( - Symbol::STR_SPLIT, - vec![str_type(), str_type()], - Box::new(list_type(str_type())), - ); - - // Str.concat : Str, Str -> Str - add_top_level_function_type!( - Symbol::STR_CONCAT, - vec![str_type(), str_type()], - Box::new(str_type()), - ); - - // Str.joinWith : List Str, Str -> Str - add_top_level_function_type!( - Symbol::STR_JOIN_WITH, - vec![list_type(str_type()), str_type()], - Box::new(str_type()), - ); - - // isEmpty : Str -> Bool - add_top_level_function_type!( - Symbol::STR_IS_EMPTY, - vec![str_type()], - Box::new(bool_type()) - ); - - // startsWith : Str, Str -> Bool - add_top_level_function_type!( - Symbol::STR_STARTS_WITH, - vec![str_type(), str_type()], - Box::new(bool_type()) - ); - - // startsWithCodePt : Str, U32 -> Bool - add_top_level_function_type!( - Symbol::STR_STARTS_WITH_CODE_PT, - vec![str_type(), u32_type()], - Box::new(bool_type()) - ); - - // endsWith : Str, Str -> Bool - add_top_level_function_type!( - Symbol::STR_ENDS_WITH, - vec![str_type(), str_type()], - Box::new(bool_type()) - ); - - // countGraphemes : Str -> Nat - add_top_level_function_type!( - Symbol::STR_COUNT_GRAPHEMES, - vec![str_type()], - Box::new(nat_type()) - ); - - // repeat : Str, Nat -> Str - add_top_level_function_type!( - Symbol::STR_REPEAT, - vec![str_type(), nat_type()], - Box::new(str_type()) - ); - - // trimLeft : Str -> Str - add_top_level_function_type!( - Symbol::STR_TRIM_LEFT, - vec![str_type()], - Box::new(str_type()) - ); - - // trimRight : Str -> Str - add_top_level_function_type!( - Symbol::STR_TRIM_RIGHT, - vec![str_type()], - Box::new(str_type()) - ); - - // trim : Str -> Str - add_top_level_function_type!(Symbol::STR_TRIM, vec![str_type()], Box::new(str_type())); - - // fromUtf8 : List U8 -> Result Str [BadUtf8 Utf8Problem]* - { - let bad_utf8 = SolvedType::TagUnion( - vec![( - TagName("BadUtf8".into()), - vec![str_utf8_byte_problem_type(), nat_type()], - )], - Box::new(SolvedType::Wildcard), - ); - - add_top_level_function_type!( - Symbol::STR_FROM_UTF8, - vec![list_type(u8_type())], - Box::new(result_type(str_type(), bad_utf8)), - ); - } - - // fromUtf8Range : List U8, { start : Nat, count : Nat } -> Result Str [BadUtf8 Utf8Problem, OutOfBounds]* - { - let bad_utf8 = SolvedType::TagUnion( - vec![ - ( - TagName("BadUtf8".into()), - vec![str_utf8_byte_problem_type(), nat_type()], - ), - (TagName("OutOfBounds".into()), vec![]), - ], - Box::new(SolvedType::Wildcard), - ); - - add_top_level_function_type!( - Symbol::STR_FROM_UTF8_RANGE, - vec![ - list_type(u8_type()), - SolvedType::Record { - fields: vec![ - ("start".into(), RecordField::Required(nat_type())), - ("count".into(), RecordField::Required(nat_type())), - ], - ext: Box::new(SolvedType::EmptyRecord), - } - ], - Box::new(result_type(str_type(), bad_utf8)), - ); - } - - // toUtf8 : Str -> List U8 - add_top_level_function_type!( - Symbol::STR_TO_UTF8, - vec![str_type()], - Box::new(list_type(u8_type())) - ); - - // toNum : Str -> Result (Num a) [InvalidNumStr] - // Because toNum doesn't work with floats & decimals by default without - // a point of usage to be able to infer the proper layout - // we decided that separate functions for each sub num type - // is the best approach. These below all end up mapping to - // `str_to_num` in can `builtins.rs` - let invalid_str = || { - SolvedType::TagUnion( - vec![(TagName("InvalidNumStr".into()), vec![])], - Box::new(SolvedType::Wildcard), - ) - }; - - // toDec : Str -> Result Dec [InvalidNumStr] - add_top_level_function_type!( - Symbol::STR_TO_DEC, - vec![str_type()], - Box::new(result_type(dec_type(), invalid_str())) - ); - - // toF64 : Str -> Result F64 [InvalidNumStr] - add_top_level_function_type!( - Symbol::STR_TO_F64, - vec![str_type()], - Box::new(result_type(f64_type(), invalid_str())) - ); - - // toF32 : Str -> Result F32 [InvalidNumStr] - add_top_level_function_type!( - Symbol::STR_TO_F32, - vec![str_type()], - Box::new(result_type(f32_type(), invalid_str())) - ); - - // toNat : Str -> Result Nat [InvalidNumStr] - add_top_level_function_type!( - Symbol::STR_TO_NAT, - vec![str_type()], - Box::new(result_type(nat_type(), invalid_str())) - ); - - // toU128 : Str -> Result U128 [InvalidNumStr] - add_top_level_function_type!( - Symbol::STR_TO_U128, - vec![str_type()], - Box::new(result_type(u128_type(), invalid_str())) - ); - - // toI128 : Str -> Result I128 [InvalidNumStr] - add_top_level_function_type!( - Symbol::STR_TO_I128, - vec![str_type()], - Box::new(result_type(i128_type(), invalid_str())) - ); - - // toU64 : Str -> Result U64 [InvalidNumStr] - add_top_level_function_type!( - Symbol::STR_TO_U64, - vec![str_type()], - Box::new(result_type(u64_type(), invalid_str())) - ); - - // toI64 : Str -> Result I64 [InvalidNumStr] - add_top_level_function_type!( - Symbol::STR_TO_I64, - vec![str_type()], - Box::new(result_type(i64_type(), invalid_str())) - ); - - // toU32 : Str -> Result U32 [InvalidNumStr] - add_top_level_function_type!( - Symbol::STR_TO_U32, - vec![str_type()], - Box::new(result_type(u32_type(), invalid_str())) - ); - - // toI32 : Str -> Result I32 [InvalidNumStr] - add_top_level_function_type!( - Symbol::STR_TO_I32, - vec![str_type()], - Box::new(result_type(i32_type(), invalid_str())) - ); - - // toU16 : Str -> Result U16 [InvalidNumStr] - add_top_level_function_type!( - Symbol::STR_TO_U16, - vec![str_type()], - Box::new(result_type(u16_type(), invalid_str())) - ); - - // toI16 : Str -> Result I16 [InvalidNumStr] - add_top_level_function_type!( - Symbol::STR_TO_I16, - vec![str_type()], - Box::new(result_type(i16_type(), invalid_str())) - ); - - // toU8 : Str -> Result U8 [InvalidNumStr] - add_top_level_function_type!( - Symbol::STR_TO_U8, - vec![str_type()], - Box::new(result_type(u8_type(), invalid_str())) - ); - - // toI8 : Str -> Result I8 [InvalidNumStr] - add_top_level_function_type!( - Symbol::STR_TO_I8, - vec![str_type()], - Box::new(result_type(i8_type(), invalid_str())) - ); - - // List module - - // get : List elem, Nat -> Result elem [OutOfBounds]* - let index_out_of_bounds = SolvedType::TagUnion( - vec![(TagName("OutOfBounds".into()), vec![])], - Box::new(SolvedType::Wildcard), - ); - - add_top_level_function_type!( - Symbol::LIST_GET, - vec![list_type(flex(TVAR1)), nat_type()], - Box::new(result_type(flex(TVAR1), index_out_of_bounds)), - ); - - // first : List elem -> Result elem [ListWasEmpty]* - let list_was_empty = SolvedType::TagUnion( - vec![(TagName("ListWasEmpty".into()), vec![])], - Box::new(SolvedType::Wildcard), - ); - - add_top_level_function_type!( - Symbol::LIST_FIRST, - vec![list_type(flex(TVAR1))], - Box::new(result_type(flex(TVAR1), list_was_empty.clone())), - ); - - // last : List elem -> Result elem [ListWasEmpty]* - add_top_level_function_type!( - Symbol::LIST_LAST, - vec![list_type(flex(TVAR1))], - Box::new(result_type(flex(TVAR1), list_was_empty.clone())), - ); - - // replace : List elem, Nat, elem -> { list: List elem, value: elem } - add_top_level_function_type!( - Symbol::LIST_REPLACE, - vec![list_type(flex(TVAR1)), nat_type(), flex(TVAR1)], - Box::new(SolvedType::Record { - fields: vec![ - ("list".into(), RecordField::Required(list_type(flex(TVAR1)))), - ("value".into(), RecordField::Required(flex(TVAR1))), - ], - ext: Box::new(SolvedType::EmptyRecord), - }), - ); - - // set : List elem, Nat, elem -> List elem - add_top_level_function_type!( - Symbol::LIST_SET, - vec![list_type(flex(TVAR1)), nat_type(), flex(TVAR1)], - Box::new(list_type(flex(TVAR1))), - ); - - // concat : List elem, List elem -> List elem - add_top_level_function_type!( - Symbol::LIST_CONCAT, - vec![list_type(flex(TVAR1)), list_type(flex(TVAR1))], - Box::new(list_type(flex(TVAR1))), - ); - - // contains : List elem, elem -> Bool - add_top_level_function_type!( - Symbol::LIST_CONTAINS, - vec![list_type(flex(TVAR1)), flex(TVAR1)], - Box::new(bool_type()), - ); - - // min : List (Num a) -> Result (Num a) [ListWasEmpty]* - add_top_level_function_type!( - Symbol::LIST_MIN, - vec![list_type(num_type(flex(TVAR1)))], - Box::new(result_type(num_type(flex(TVAR1)), list_was_empty.clone())), - ); - - // max : List (Num a) -> Result (Num a) [ListWasEmpty]* - add_top_level_function_type!( - Symbol::LIST_MAX, - vec![list_type(num_type(flex(TVAR1)))], - Box::new(result_type(num_type(flex(TVAR1)), list_was_empty)), - ); - - // sum : List (Num a) -> Num a - add_top_level_function_type!( - Symbol::LIST_SUM, - vec![list_type(num_type(flex(TVAR1)))], - Box::new(num_type(flex(TVAR1))), - ); - - // product : List (Num a) -> Num a - add_top_level_function_type!( - Symbol::LIST_PRODUCT, - vec![list_type(num_type(flex(TVAR1)))], - Box::new(num_type(flex(TVAR1))), - ); - - // walk : List elem, state, (state, elem -> state) -> state - add_top_level_function_type!( - Symbol::LIST_WALK, - vec![ - list_type(flex(TVAR1)), - flex(TVAR2), - closure(vec![flex(TVAR2), flex(TVAR1)], TVAR3, Box::new(flex(TVAR2))), - ], - Box::new(flex(TVAR2)), - ); - - // walkBackwards : List elem, state, (state, elem -> state) -> state - add_top_level_function_type!( - Symbol::LIST_WALK_BACKWARDS, - vec![ - list_type(flex(TVAR1)), - flex(TVAR2), - closure(vec![flex(TVAR2), flex(TVAR1)], TVAR3, Box::new(flex(TVAR2))), - ], - Box::new(flex(TVAR2)), - ); - - fn until_type(content: SolvedType) -> SolvedType { - // [LT, EQ, GT] - SolvedType::TagUnion( - vec![ - (TagName("Continue".into()), vec![content.clone()]), - (TagName("Stop".into()), vec![content]), - ], - Box::new(SolvedType::EmptyTagUnion), - ) - } - - // walkUntil : List elem, state, (state, elem -> [Continue state, Stop state]) -> state - add_top_level_function_type!( - Symbol::LIST_WALK_UNTIL, - vec![ - list_type(flex(TVAR1)), - flex(TVAR2), - closure( - vec![flex(TVAR2), flex(TVAR1)], - TVAR3, - Box::new(until_type(flex(TVAR2))), - ), - ], - Box::new(flex(TVAR2)), - ); - - // keepIf : List elem, (elem -> Bool) -> List elem - add_top_level_function_type!( - Symbol::LIST_KEEP_IF, - vec![ - list_type(flex(TVAR1)), - closure(vec![flex(TVAR1)], TVAR2, Box::new(bool_type())), - ], - Box::new(list_type(flex(TVAR1))), - ); - - // keepOks : List before, (before -> Result after *) -> List after - { - let_tvars! { star, cvar, before, after}; - add_top_level_function_type!( - Symbol::LIST_KEEP_OKS, - vec![ - list_type(flex(before)), - closure( - vec![flex(before)], - cvar, - Box::new(result_type(flex(after), flex(star))), - ), - ], - Box::new(list_type(flex(after))), - ) - }; - - // keepErrs: List before, (before -> Result * after) -> List after - { - let_tvars! { star, cvar, before, after}; - - add_top_level_function_type!( - Symbol::LIST_KEEP_ERRS, - vec![ - list_type(flex(before)), - closure( - vec![flex(before)], - cvar, - Box::new(result_type(flex(star), flex(after))), - ), - ], - Box::new(list_type(flex(after))), - ) - }; - - // range : Int a, Int a -> List (Int a) - add_top_level_function_type!( - Symbol::LIST_RANGE, - vec![int_type(flex(TVAR1)), int_type(flex(TVAR1))], - Box::new(list_type(int_type(flex(TVAR1)))), - ); - - // joinMap : List before, (before -> List after) -> List after - { - let_tvars! { cvar, before, after } - add_top_level_function_type!( - Symbol::LIST_JOIN_MAP, - vec![ - list_type(flex(before)), - closure(vec![flex(before)], cvar, Box::new(list_type(flex(after)))), - ], - Box::new(list_type(flex(after))), - ); - } - - // map : List before, (before -> after) -> List after - add_top_level_function_type!( - Symbol::LIST_MAP, - vec![ - list_type(flex(TVAR1)), - closure(vec![flex(TVAR1)], TVAR3, Box::new(flex(TVAR2))), - ], - Box::new(list_type(flex(TVAR2))), - ); - - // mapWithIndex : List before, (before, Nat -> after) -> List after - { - let_tvars! { cvar, before, after}; - add_top_level_function_type!( - Symbol::LIST_MAP_WITH_INDEX, - vec![ - list_type(flex(before)), - closure(vec![flex(before), nat_type()], cvar, Box::new(flex(after))), - ], - Box::new(list_type(flex(after))), - ) - }; - - // map2 : List a, List b, (a, b -> c) -> List c - { - let_tvars! {a, b, c, cvar}; - add_top_level_function_type!( - Symbol::LIST_MAP2, - vec![ - list_type(flex(a)), - list_type(flex(b)), - closure(vec![flex(a), flex(b)], cvar, Box::new(flex(c))), - ], - Box::new(list_type(flex(c))), - ) - }; - - { - let_tvars! {a, b, c, d, cvar}; - - // map3 : List a, List b, List c, (a, b, c -> d) -> List d - add_top_level_function_type!( - Symbol::LIST_MAP3, - vec![ - list_type(flex(a)), - list_type(flex(b)), - list_type(flex(c)), - closure(vec![flex(a), flex(b), flex(c)], cvar, Box::new(flex(d))), - ], - Box::new(list_type(flex(d))), - ) - }; - - { - let_tvars! {a, b, c, d, e, cvar}; - - // map4 : List a, List b, List c, List d, (a, b, c, d -> e) -> List e - add_top_level_function_type!( - Symbol::LIST_MAP4, - vec![ - list_type(flex(a)), - list_type(flex(b)), - list_type(flex(c)), - list_type(flex(d)), - closure( - vec![flex(a), flex(b), flex(c), flex(d)], - cvar, - Box::new(flex(e)) - ), - ], - Box::new(list_type(flex(e))), - ) - }; - - // append : List elem, elem -> List elem - add_top_level_function_type!( - Symbol::LIST_APPEND, - vec![list_type(flex(TVAR1)), flex(TVAR1)], - Box::new(list_type(flex(TVAR1))), - ); - - // takeFirst : List elem, Nat -> List elem - add_top_level_function_type!( - Symbol::LIST_TAKE_FIRST, - vec![list_type(flex(TVAR1)), nat_type()], - Box::new(list_type(flex(TVAR1))), - ); - - // takeLast : List elem, Nat -> List elem - add_top_level_function_type!( - Symbol::LIST_TAKE_LAST, - vec![list_type(flex(TVAR1)), nat_type()], - Box::new(list_type(flex(TVAR1))), - ); - - // sublist : List elem, { start : Nat, len : Nat } -> List elem - add_top_level_function_type!( - Symbol::LIST_SUBLIST, - vec![ - list_type(flex(TVAR1)), - SolvedType::Record { - fields: vec![ - ("start".into(), RecordField::Required(nat_type())), - ("len".into(), RecordField::Required(nat_type())), - ], - ext: Box::new(SolvedType::EmptyRecord), - }, - ], - Box::new(list_type(flex(TVAR1))), - ); - - // split : List elem, Nat -> { before: List elem, others: List elem } - add_top_level_function_type!( - Symbol::LIST_SPLIT, - vec![list_type(flex(TVAR1)), nat_type(),], - Box::new(SolvedType::Record { - fields: vec![ - ( - "before".into(), - RecordField::Required(list_type(flex(TVAR1))) - ), - ( - "others".into(), - RecordField::Required(list_type(flex(TVAR1))) - ), - ], - ext: Box::new(SolvedType::EmptyRecord), - },), - ); - - // drop : List elem, Nat -> List elem - add_top_level_function_type!( - Symbol::LIST_DROP, - vec![list_type(flex(TVAR1)), nat_type()], - Box::new(list_type(flex(TVAR1))), - ); - - // dropAt : List elem, Nat -> List elem - add_top_level_function_type!( - Symbol::LIST_DROP_AT, - vec![list_type(flex(TVAR1)), nat_type()], - Box::new(list_type(flex(TVAR1))), - ); - - // dropLast : List elem -> List elem - add_top_level_function_type!( - Symbol::LIST_DROP_LAST, - vec![list_type(flex(TVAR1))], - Box::new(list_type(flex(TVAR1))), - ); - - // dropFirst : List elem -> List elem - add_top_level_function_type!( - Symbol::LIST_DROP_FIRST, - vec![list_type(flex(TVAR1))], - Box::new(list_type(flex(TVAR1))), - ); - - // dropIf : List elem, (elem -> Bool) -> List elem - add_top_level_function_type!( - Symbol::LIST_DROP_IF, - vec![ - list_type(flex(TVAR1)), - closure(vec![flex(TVAR1)], TVAR2, Box::new(bool_type())), - ], - Box::new(list_type(flex(TVAR1))), - ); - - // swap : List elem, Nat, Nat -> List elem - add_top_level_function_type!( - Symbol::LIST_SWAP, - vec![list_type(flex(TVAR1)), nat_type(), nat_type()], - Box::new(list_type(flex(TVAR1))), - ); - - // prepend : List elem, elem -> List elem - add_top_level_function_type!( - Symbol::LIST_PREPEND, - vec![list_type(flex(TVAR1)), flex(TVAR1)], - Box::new(list_type(flex(TVAR1))), - ); - - // join : List (List elem) -> List elem - add_top_level_function_type!( - Symbol::LIST_JOIN, - vec![list_type(list_type(flex(TVAR1)))], - Box::new(list_type(flex(TVAR1))), - ); - - // single : a -> List a - add_top_level_function_type!( - Symbol::LIST_SINGLE, - vec![flex(TVAR1)], - Box::new(list_type(flex(TVAR1))) - ); - - // repeat : elem, Nat -> List elem - add_top_level_function_type!( - Symbol::LIST_REPEAT, - vec![flex(TVAR1), nat_type()], - Box::new(list_type(flex(TVAR1))), - ); - - // reverse : List elem -> List elem - add_top_level_function_type!( - Symbol::LIST_REVERSE, - vec![list_type(flex(TVAR1))], - Box::new(list_type(flex(TVAR1))), - ); - - // len : List * -> Nat - add_top_level_function_type!( - Symbol::LIST_LEN, - vec![list_type(flex(TVAR1))], - Box::new(nat_type()) - ); - - // isEmpty : List * -> Bool - add_top_level_function_type!( - Symbol::LIST_IS_EMPTY, - vec![list_type(flex(TVAR1))], - Box::new(bool_type()) - ); - - // any: List elem, (elem -> Bool) -> Bool - add_top_level_function_type!( - Symbol::LIST_ANY, - vec![ - list_type(flex(TVAR1)), - closure(vec![flex(TVAR1)], TVAR2, Box::new(bool_type())), - ], - Box::new(bool_type()), - ); - - // all: List elem, (elem -> Bool) -> Bool - add_top_level_function_type!( - Symbol::LIST_ALL, - vec![ - list_type(flex(TVAR1)), - closure(vec![flex(TVAR1)], TVAR2, Box::new(bool_type())), - ], - Box::new(bool_type()), - ); - - // sortWith : List a, (a, a -> Ordering) -> List a - add_top_level_function_type!( - Symbol::LIST_SORT_WITH, - vec![ - list_type(flex(TVAR1)), - closure( - vec![flex(TVAR1), flex(TVAR1)], - TVAR2, - Box::new(ordering_type()), - ), - ], - Box::new(list_type(flex(TVAR1))), - ); - - // sortAsc : List (Num a) -> List (Num a) - add_top_level_function_type!( - Symbol::LIST_SORT_ASC, - vec![list_type(num_type(flex(TVAR1)))], - Box::new(list_type(num_type(flex(TVAR1)))) - ); - - // sortDesc : List (Num a) -> List (Num a) - add_top_level_function_type!( - Symbol::LIST_SORT_DESC, - vec![list_type(num_type(flex(TVAR1)))], - Box::new(list_type(num_type(flex(TVAR1)))) - ); - - // find : List elem, (elem -> Bool) -> Result elem [NotFound]* - { - let not_found = SolvedType::TagUnion( - vec![(TagName("NotFound".into()), vec![])], - Box::new(SolvedType::Wildcard), - ); - let (elem, cvar) = (TVAR1, TVAR2); - add_top_level_function_type!( - Symbol::LIST_FIND, - vec![ - list_type(flex(elem)), - closure(vec![flex(elem)], cvar, Box::new(bool_type())), - ], - Box::new(result_type(flex(elem), not_found)), - ) - } - - // intersperse : List elem, elem -> List elem - add_top_level_function_type!( - Symbol::LIST_INTERSPERSE, - vec![list_type(flex(TVAR1)), flex(TVAR1)], - Box::new(list_type(flex(TVAR1))), - ); - - // Dict module - - // len : Dict * * -> Nat - add_top_level_function_type!( - Symbol::DICT_LEN, - vec![dict_type(flex(TVAR1), flex(TVAR2))], - Box::new(nat_type()), - ); - - // empty : Dict * * - add_type!(Symbol::DICT_EMPTY, dict_type(flex(TVAR1), flex(TVAR2))); - - // single : k, v -> Dict k v - add_top_level_function_type!( - Symbol::DICT_SINGLE, - vec![flex(TVAR1), flex(TVAR2)], - Box::new(dict_type(flex(TVAR1), flex(TVAR2))), - ); - - // get : Dict k v, k -> Result v [KeyNotFound]* - let key_not_found = SolvedType::TagUnion( - vec![(TagName("KeyNotFound".into()), vec![])], - Box::new(SolvedType::Wildcard), - ); - - add_top_level_function_type!( - Symbol::DICT_GET, - vec![dict_type(flex(TVAR1), flex(TVAR2)), flex(TVAR1)], - Box::new(result_type(flex(TVAR2), key_not_found)), - ); - - // Dict.insert : Dict k v, k, v -> Dict k v - add_top_level_function_type!( - Symbol::DICT_INSERT, - vec![ - dict_type(flex(TVAR1), flex(TVAR2)), - flex(TVAR1), - flex(TVAR2), - ], - Box::new(dict_type(flex(TVAR1), flex(TVAR2))), - ); - - // Dict.remove : Dict k v, k -> Dict k v - add_top_level_function_type!( - Symbol::DICT_REMOVE, - vec![dict_type(flex(TVAR1), flex(TVAR2)), flex(TVAR1)], - Box::new(dict_type(flex(TVAR1), flex(TVAR2))), - ); - - // Dict.contains : Dict k v, k -> Bool - add_top_level_function_type!( - Symbol::DICT_CONTAINS, - vec![dict_type(flex(TVAR1), flex(TVAR2)), flex(TVAR1)], - Box::new(bool_type()), - ); - - // Dict.keys : Dict k v -> List k - add_top_level_function_type!( - Symbol::DICT_KEYS, - vec![dict_type(flex(TVAR1), flex(TVAR2))], - Box::new(list_type(flex(TVAR1))), - ); - - // Dict.values : Dict k v -> List v - add_top_level_function_type!( - Symbol::DICT_VALUES, - vec![dict_type(flex(TVAR1), flex(TVAR2))], - Box::new(list_type(flex(TVAR2))), - ); - - // Dict.union : Dict k v, Dict k v -> Dict k v - add_top_level_function_type!( - Symbol::DICT_UNION, - vec![ - dict_type(flex(TVAR1), flex(TVAR2)), - dict_type(flex(TVAR1), flex(TVAR2)), - ], - Box::new(dict_type(flex(TVAR1), flex(TVAR2))), - ); - - // Dict.intersection : Dict k v, Dict k v -> Dict k v - add_top_level_function_type!( - Symbol::DICT_INTERSECTION, - vec![ - dict_type(flex(TVAR1), flex(TVAR2)), - dict_type(flex(TVAR1), flex(TVAR2)), - ], - Box::new(dict_type(flex(TVAR1), flex(TVAR2))), - ); - - // Dict.difference : Dict k v, Dict k v -> Dict k v - add_top_level_function_type!( - Symbol::DICT_DIFFERENCE, - vec![ - dict_type(flex(TVAR1), flex(TVAR2)), - dict_type(flex(TVAR1), flex(TVAR2)), - ], - Box::new(dict_type(flex(TVAR1), flex(TVAR2))), - ); - - // Dict.walk : Dict k v, state, (state, k, v -> state) -> state - add_top_level_function_type!( - Symbol::DICT_WALK, - vec![ - dict_type(flex(TVAR1), flex(TVAR2)), - flex(TVAR3), - closure( - vec![flex(TVAR3), flex(TVAR1), flex(TVAR2)], - TVAR4, - Box::new(flex(TVAR3)), - ), - ], - Box::new(flex(TVAR3)), - ); - - // Set module - - // empty : Set a - add_type!(Symbol::SET_EMPTY, set_type(flex(TVAR1))); - - // single : a -> Set a - add_top_level_function_type!( - Symbol::SET_SINGLE, - vec![flex(TVAR1)], - Box::new(set_type(flex(TVAR1))) - ); - - // len : Set * -> Nat - add_top_level_function_type!( - Symbol::SET_LEN, - vec![set_type(flex(TVAR1))], - Box::new(nat_type()) - ); - - // toList : Set a -> List a - add_top_level_function_type!( - Symbol::SET_TO_LIST, - vec![set_type(flex(TVAR1))], - Box::new(list_type(flex(TVAR1))), - ); - - // fromList : List a -> Set a - add_top_level_function_type!( - Symbol::SET_FROM_LIST, - vec![list_type(flex(TVAR1))], - Box::new(set_type(flex(TVAR1))), - ); - - // union : Set a, Set a -> Set a - add_top_level_function_type!( - Symbol::SET_UNION, - vec![set_type(flex(TVAR1)), set_type(flex(TVAR1))], - Box::new(set_type(flex(TVAR1))), - ); - - // difference : Set a, Set a -> Set a - add_top_level_function_type!( - Symbol::SET_DIFFERENCE, - vec![set_type(flex(TVAR1)), set_type(flex(TVAR1))], - Box::new(set_type(flex(TVAR1))), - ); - - // intersection : Set a, Set a -> Set a - add_top_level_function_type!( - Symbol::SET_INTERSECTION, - vec![set_type(flex(TVAR1)), set_type(flex(TVAR1))], - Box::new(set_type(flex(TVAR1))), - ); - - // Set.walk : Set a, (b, a -> b), b -> b - add_top_level_function_type!( - Symbol::SET_WALK, - vec![ - set_type(flex(TVAR1)), - flex(TVAR2), - closure(vec![flex(TVAR2), flex(TVAR1)], TVAR3, Box::new(flex(TVAR2))), - ], - Box::new(flex(TVAR2)), - ); - - add_top_level_function_type!( - Symbol::SET_INSERT, - vec![set_type(flex(TVAR1)), flex(TVAR1)], - Box::new(set_type(flex(TVAR1))), - ); - - add_top_level_function_type!( - Symbol::SET_REMOVE, - vec![set_type(flex(TVAR1)), flex(TVAR1)], - Box::new(set_type(flex(TVAR1))), - ); - - add_top_level_function_type!( - Symbol::SET_CONTAINS, - vec![set_type(flex(TVAR1)), flex(TVAR1)], - Box::new(bool_type()), - ); - - // Result module - - // map : Result a err, (a -> b) -> Result b err - add_top_level_function_type!( - Symbol::RESULT_MAP, - vec![ - result_type(flex(TVAR1), flex(TVAR3)), - closure(vec![flex(TVAR1)], TVAR4, Box::new(flex(TVAR2))), - ], - Box::new(result_type(flex(TVAR2), flex(TVAR3))), - ); - - // mapErr : Result a x, (x -> y) -> Result a x - add_top_level_function_type!( - Symbol::RESULT_MAP_ERR, - vec![ - result_type(flex(TVAR1), flex(TVAR3)), - closure(vec![flex(TVAR3)], TVAR4, Box::new(flex(TVAR2))), - ], - Box::new(result_type(flex(TVAR1), flex(TVAR2))), - ); - - // after : Result a err, (a -> Result b err) -> Result b err - add_top_level_function_type!( - Symbol::RESULT_AFTER, - vec![ - result_type(flex(TVAR1), flex(TVAR3)), - closure( - vec![flex(TVAR1)], - TVAR4, - Box::new(result_type(flex(TVAR2), flex(TVAR3))), - ), - ], - Box::new(result_type(flex(TVAR2), flex(TVAR3))), - ); - - // withDefault : Result a x, a -> a - add_top_level_function_type!( - Symbol::RESULT_WITH_DEFAULT, - vec![result_type(flex(TVAR1), flex(TVAR3)), flex(TVAR1)], - Box::new(flex(TVAR1)), - ); - - // isOk : Result * * -> bool - add_top_level_function_type!( - Symbol::RESULT_IS_OK, - vec![result_type(flex(TVAR1), flex(TVAR3))], - Box::new(bool_type()), - ); - - // isErr : Result * * -> bool - add_top_level_function_type!( - Symbol::RESULT_IS_ERR, - vec![result_type(flex(TVAR1), flex(TVAR3))], - Box::new(bool_type()), - ); - - // Box.box : a -> Box a - add_top_level_function_type!( - Symbol::BOX_BOX_FUNCTION, - vec![flex(TVAR1)], - Box::new(box_type(flex(TVAR1))), - ); - - // Box.unbox : Box a -> a - add_top_level_function_type!( - Symbol::BOX_UNBOX, - vec![box_type(flex(TVAR1))], - Box::new(flex(TVAR1)), - ); - - types -} - -#[inline(always)] -fn flex(tvar: VarId) -> SolvedType { - SolvedType::Flex(tvar) -} - -#[inline(always)] -fn closure(arguments: Vec, closure_var: VarId, ret: Box) -> SolvedType { - SolvedType::Func(arguments, Box::new(SolvedType::Flex(closure_var)), ret) -} diff --git a/compiler/can/Cargo.toml b/compiler/can/Cargo.toml deleted file mode 100644 index 79d99d746e..0000000000 --- a/compiler/can/Cargo.toml +++ /dev/null @@ -1,23 +0,0 @@ -[package] -name = "roc_can" -version = "0.1.0" -authors = ["The Roc Contributors"] -license = "UPL-1.0" -edition = "2021" - -[dependencies] -roc_collections = { path = "../collections" } -roc_error_macros = { path = "../../error_macros" } -roc_exhaustive = { path = "../exhaustive" } -roc_region = { path = "../region" } -roc_module = { path = "../module" } -roc_parse = { path = "../parse" } -roc_problem = { path = "../problem" } -roc_types = { path = "../types" } -bumpalo = { version = "3.8.0", features = ["collections"] } -static_assertions = "1.1.0" -bitvec = "1" - -[dev-dependencies] -pretty_assertions = "1.0.0" -indoc = "1.0.3" diff --git a/compiler/can/src/abilities.rs b/compiler/can/src/abilities.rs deleted file mode 100644 index f408dbcaf1..0000000000 --- a/compiler/can/src/abilities.rs +++ /dev/null @@ -1,385 +0,0 @@ -use roc_collections::{all::MutMap, VecMap, VecSet}; -use roc_error_macros::internal_error; -use roc_module::symbol::Symbol; -use roc_region::all::Region; -use roc_types::{subs::Variable, types::Type}; - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct MemberVariables { - pub able_vars: Vec, - /// This includes - named rigid vars, lambda sets, wildcards. See - /// [`IntroducedVariables::collect_rigid`](crate::annotation::IntroducedVariables::collect_rigid). - pub rigid_vars: Vec, - pub flex_vars: Vec, -} - -#[derive(Debug, Clone)] -pub enum MemberTypeInfo { - /// The member and its signature is defined locally, in the module the store is created for. - /// We need to instantiate and introduce this during solving. - Local { - signature_var: Variable, - signature: Type, - variables: MemberVariables, - }, - /// The member was defined in another module, so we'll import its variable when it's time to - /// solve. At that point we'll resolve `var` here. - Imported { signature_var: Option }, -} - -/// Stores information about an ability member definition, including the parent ability, the -/// defining type, and what type variables need to be instantiated with instances of the ability. -// TODO: SoA and put me in an arena -#[derive(Debug, Clone)] -pub struct AbilityMemberData { - pub parent_ability: Symbol, - pub region: Region, - pub typ: MemberTypeInfo, -} - -impl AbilityMemberData { - pub fn signature_var(&self) -> Option { - match self.typ { - MemberTypeInfo::Local { signature_var, .. } => Some(signature_var), - MemberTypeInfo::Imported { signature_var } => signature_var, - } - } -} - -/// (member, specialization type) -> specialization -pub type SolvedSpecializations = VecMap<(Symbol, Symbol), MemberSpecialization>; - -/// A particular specialization of an ability member. -#[derive(Debug, Clone)] -pub struct MemberSpecialization { - pub symbol: Symbol, - - /// Solved lambda sets for an ability member specialization. For example, if we have - /// - /// Default has default : {} -[[] + a:default:1]-> a | a has Default - /// - /// A := {} - /// default = \{} -[[closA]]-> @A {} - /// - /// and this [MemberSpecialization] is for `A`, then there is a mapping of - /// `1` to the variable representing `[[closA]]`. - pub specialization_lambda_sets: VecMap, -} - -impl MemberSpecialization { - pub fn new(symbol: Symbol, specialization_lambda_sets: VecMap) -> Self { - Self { - symbol, - specialization_lambda_sets, - } - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub struct SpecializationId(u32); - -#[allow(clippy::derivable_impls)] // let's be explicit about this -impl Default for SpecializationId { - fn default() -> Self { - Self(0) - } -} - -pub enum SpecializationLambdaSetError {} - -/// Stores information about what abilities exist in a scope, what it means to implement an -/// ability, and what types implement them. -// TODO(abilities): this should probably go on the Scope, I don't put it there for now because we -// are only dealing with intra-module abilities for now. -// TODO(abilities): many of these should be `VecMap`s. Do some benchmarking. -#[derive(Default, Debug, Clone)] -pub struct AbilitiesStore { - /// Maps an ability to the members defining it. - members_of_ability: MutMap>, - - /// Information about all members composing abilities. - ability_members: MutMap, - - /// Map of symbols that specialize an ability member to the root ability symbol name. - /// For example, for the program - /// Hash has hash : a -> U64 | a has Hash - /// ^^^^ gets the symbol "#hash" - /// hash = \@Id n -> n - /// ^^^^ gets the symbol "#hash1" - /// - /// We keep the mapping #hash1->#hash - specialization_to_root: MutMap, - - /// Maps a tuple (member, type) specifying that `type` declares an implementation of an ability - /// member `member`, to the exact symbol that implements the ability. - declared_specializations: SolvedSpecializations, - - next_specialization_id: u32, - - /// Resolved specializations for a symbol. These might be ephemeral (known due to type solving), - /// or resolved on-the-fly during mono. - resolved_specializations: MutMap, -} - -impl AbilitiesStore { - /// Records the definition of an ability, including its members. - pub fn register_ability(&mut self, ability: Symbol, members: I) - where - I: IntoIterator, - I::IntoIter: ExactSizeIterator, - { - let members = members.into_iter(); - let mut members_vec = Vec::with_capacity(members.len()); - for (member, member_data) in members { - members_vec.push(member); - let old_member = self.ability_members.insert(member, member_data); - debug_assert!(old_member.is_none(), "Replacing existing member definition"); - } - let old_ability = self.members_of_ability.insert(ability, members_vec); - debug_assert!( - old_ability.is_none(), - "Replacing existing ability definition" - ); - } - - pub fn is_ability(&self, ability: Symbol) -> bool { - self.members_of_ability.contains_key(&ability) - } - - /// Records a specialization of `ability_member` with specialized type `implementing_type`. - /// Entries via this function are considered a source of truth. It must be ensured that a - /// specialization is validated before being registered here. - pub fn register_specialization_for_type( - &mut self, - ability_member: Symbol, - implementing_type: Symbol, - specialization: MemberSpecialization, - ) { - let old_spec = self - .declared_specializations - .insert((ability_member, implementing_type), specialization); - debug_assert!(old_spec.is_none(), "Replacing existing specialization"); - } - - /// Checks if `name` is a root ability member symbol name. - /// Note that this will return `false` for specializations of an ability member, which have - /// different symbols from the root. - pub fn is_ability_member_name(&self, name: Symbol) -> bool { - self.ability_members.contains_key(&name) - } - - /// Returns information about all known ability members and their root symbols. - pub fn root_ability_members(&self) -> &MutMap { - &self.ability_members - } - - /// Records that the symbol `specializing_symbol` claims to specialize `ability_member`; for - /// example the symbol of `hash : Id -> U64` specializing `hash : a -> U64 | a has Hash`. - pub fn register_specializing_symbol( - &mut self, - specializing_symbol: Symbol, - ability_member: Symbol, - ) { - self.specialization_to_root - .insert(specializing_symbol, ability_member); - } - - /// Returns whether a symbol is declared to specialize an ability member. - pub fn is_specialization_name(&self, symbol: Symbol) -> bool { - self.specialization_to_root.contains_key(&symbol) - } - - /// Finds the symbol name and ability member definition for a symbol specializing the ability - /// member, if it specializes any. - /// For example, suppose `hash : Id -> U64` has symbol #hash1 and specializes - /// `hash : a -> U64 | a has Hash` with symbol #hash. Calling this with #hash1 would retrieve - /// the ability member data for #hash. - pub fn root_name_and_def( - &self, - specializing_symbol: Symbol, - ) -> Option<(Symbol, &AbilityMemberData)> { - let root_symbol = self.specialization_to_root.get(&specializing_symbol)?; - debug_assert!(self.ability_members.contains_key(root_symbol)); - let root_data = self.ability_members.get(root_symbol).unwrap(); - Some((*root_symbol, root_data)) - } - - /// Finds the ability member definition for a member name. - pub fn member_def(&self, member: Symbol) -> Option<&AbilityMemberData> { - self.ability_members.get(&member) - } - - /// Iterator over all abilities and their members that this store knows about. - pub fn iter_abilities(&self) -> impl Iterator { - self.members_of_ability - .iter() - .map(|(k, v)| (*k, v.as_slice())) - } - - /// Returns an iterator over pairs ((ability member, type), specialization) specifying that - /// "ability member" has a "specialization" for type "type". - pub fn iter_specializations( - &self, - ) -> impl Iterator + '_ { - self.declared_specializations.iter().map(|(k, v)| (*k, v)) - } - - /// Retrieves the specialization of `member` for `typ`, if it exists. - pub fn get_specialization(&self, member: Symbol, typ: Symbol) -> Option<&MemberSpecialization> { - self.declared_specializations.get(&(member, typ)) - } - - pub fn members_of_ability(&self, ability: Symbol) -> Option<&[Symbol]> { - self.members_of_ability.get(&ability).map(|v| v.as_ref()) - } - - pub fn fresh_specialization_id(&mut self) -> SpecializationId { - debug_assert!(self.next_specialization_id != std::u32::MAX); - - let id = SpecializationId(self.next_specialization_id); - self.next_specialization_id += 1; - id - } - - pub fn insert_resolved(&mut self, id: SpecializationId, specialization: Symbol) { - // May not be a thing in mono - // debug_assert!(self.is_specialization_name(specialization)); - - let old_specialization = self.resolved_specializations.insert(id, specialization); - - debug_assert!( - old_specialization.is_none(), - "Existing resolution: {:?}", - old_specialization - ); - } - - pub fn remove_resolved(&mut self, id: SpecializationId) { - let old_specialization = self.resolved_specializations.remove(&id); - - debug_assert!( - old_specialization.is_some(), - "Trying to remove a resolved specialization that was never there!", - ); - } - - pub fn get_resolved(&self, id: SpecializationId) -> Option { - self.resolved_specializations.get(&id).copied() - } - - /// Creates a store from [`self`] that closes over the abilities/members given by the - /// imported `symbols`, and their specializations (if any). - pub fn closure_from_imported(&self, symbols: &VecSet) -> Self { - let Self { - members_of_ability, - ability_members, - declared_specializations, - - // Covered by `declared_specializations` - specialization_to_root: _, - - // Taking closure for a new module, so specialization IDs can be fresh - next_specialization_id: _, - resolved_specializations: _, - } = self; - - let mut new = Self::default(); - - // 1. Figure out the abilities we need to introduce. - let mut abilities_to_introduce = VecSet::with_capacity(2); - symbols.iter().for_each(|symbol| { - if let Some(member_data) = ability_members.get(symbol) { - // If the symbol is member of an ability, we need to capture the entire ability. - abilities_to_introduce.insert(member_data.parent_ability); - } - if members_of_ability.contains_key(symbol) { - abilities_to_introduce.insert(*symbol); - } - }); - - // 2. Add each ability, and any specializations of its members we know about. - for ability in abilities_to_introduce.into_iter() { - let members = members_of_ability.get(&ability).unwrap(); - let mut imported_member_data = Vec::with_capacity(members.len()); - for member in members { - let mut member_data = ability_members.get(member).unwrap().clone(); - // All external members need to be marked as imported. We'll figure out their real - // type variables when it comes time to solve the module we're currently importing - // into. - member_data.typ = MemberTypeInfo::Imported { - signature_var: None, - }; - - imported_member_data.push((*member, member_data)); - } - - new.register_ability(ability, imported_member_data); - - // Add any specializations of the ability's members we know about. - declared_specializations - .iter() - .filter(|((member, _), _)| members.contains(member)) - .for_each(|(&(member, typ), specialization)| { - new.register_specializing_symbol(specialization.symbol, member); - new.register_specialization_for_type(member, typ, specialization.clone()); - }); - } - - new - } - - pub fn union(&mut self, other: Self) { - let Self { - members_of_ability: other_members_of_ability, - ability_members: mut other_ability_members, - specialization_to_root, - declared_specializations, - next_specialization_id, - resolved_specializations, - } = other; - - for (ability, members) in other_members_of_ability.into_iter() { - if let Some(my_members) = self.members_of_ability(ability) { - debug_assert!( - my_members == members, - "Two abilities have different definitions, definitely a bug" - ); - } - let member_data = members - .into_iter() - .map(|member| (member, other_ability_members.remove(&member).unwrap())); - self.register_ability(ability, member_data); - } - - for (specialization, member) in specialization_to_root.into_iter() { - let old_root = self.specialization_to_root.insert(specialization, member); - debug_assert!(old_root.is_none() || old_root.unwrap() == member); - } - - for ((member, typ), specialization) in declared_specializations.into_iter() { - let old_specialization = self - .declared_specializations - .insert((member, typ), specialization.clone()); - debug_assert!( - old_specialization.is_none() - || old_specialization.unwrap().symbol == specialization.symbol - ); - } - - debug_assert!(next_specialization_id == 0); - debug_assert!(self.next_specialization_id == 0); - debug_assert!(resolved_specializations.is_empty()); - debug_assert!(self.resolved_specializations.is_empty()); - } - - pub fn resolved_imported_member_var(&mut self, member: Symbol, var: Variable) { - let member_data = self.ability_members.get_mut(&member).unwrap(); - match &mut member_data.typ { - MemberTypeInfo::Imported { signature_var } => { - let old = signature_var.replace(var); - debug_assert!(old.is_none(), "Replacing existing variable!"); - } - _ => internal_error!("{:?} is not imported!", member), - } - } -} diff --git a/compiler/can/src/annotation.rs b/compiler/can/src/annotation.rs deleted file mode 100644 index 6c5617aab8..0000000000 --- a/compiler/can/src/annotation.rs +++ /dev/null @@ -1,1352 +0,0 @@ -use crate::env::Env; -use crate::procedure::References; -use crate::scope::Scope; -use roc_collections::{ImMap, MutSet, SendMap, VecMap, VecSet}; -use roc_module::ident::{Ident, Lowercase, TagName}; -use roc_module::symbol::Symbol; -use roc_parse::ast::{AssignedField, ExtractSpaces, Pattern, Tag, TypeAnnotation, TypeHeader}; -use roc_problem::can::ShadowKind; -use roc_region::all::{Loc, Region}; -use roc_types::subs::{VarStore, Variable}; -use roc_types::types::{ - name_type_var, Alias, AliasCommon, AliasKind, AliasVar, LambdaSet, OptAbleType, OptAbleVar, - Problem, RecordField, Type, TypeExtension, -}; - -#[derive(Clone, Debug)] -pub struct Annotation { - pub typ: Type, - pub introduced_variables: IntroducedVariables, - pub references: VecSet, - pub aliases: SendMap, -} - -impl Annotation { - pub fn add_to( - &self, - aliases: &mut VecMap, - references: &mut References, - introduced_variables: &mut IntroducedVariables, - ) { - for symbol in self.references.iter() { - references.insert_type_lookup(*symbol); - } - - introduced_variables.union(&self.introduced_variables); - - for (name, alias) in self.aliases.iter() { - if !aliases.contains_key(name) { - aliases.insert(*name, alias.clone()); - } - } - } -} - -#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] -pub enum NamedOrAbleVariable<'a> { - Named(&'a NamedVariable), - Able(&'a AbleVariable), -} - -impl<'a> NamedOrAbleVariable<'a> { - pub fn first_seen(&self) -> Region { - match self { - NamedOrAbleVariable::Named(nv) => nv.first_seen, - NamedOrAbleVariable::Able(av) => av.first_seen, - } - } - - pub fn name(&self) -> &Lowercase { - match self { - NamedOrAbleVariable::Named(nv) => &nv.name, - NamedOrAbleVariable::Able(av) => &av.name, - } - } - - pub fn variable(&self) -> Variable { - match self { - NamedOrAbleVariable::Named(nv) => nv.variable, - NamedOrAbleVariable::Able(av) => av.variable, - } - } -} - -pub enum OwnedNamedOrAble { - Named(NamedVariable), - Able(AbleVariable), -} - -impl OwnedNamedOrAble { - pub fn first_seen(&self) -> Region { - match self { - OwnedNamedOrAble::Named(nv) => nv.first_seen, - OwnedNamedOrAble::Able(av) => av.first_seen, - } - } - - pub fn ref_name(&self) -> &Lowercase { - match self { - OwnedNamedOrAble::Named(nv) => &nv.name, - OwnedNamedOrAble::Able(av) => &av.name, - } - } - - pub fn name(self) -> Lowercase { - match self { - OwnedNamedOrAble::Named(nv) => nv.name, - OwnedNamedOrAble::Able(av) => av.name, - } - } - - pub fn variable(&self) -> Variable { - match self { - OwnedNamedOrAble::Named(nv) => nv.variable, - OwnedNamedOrAble::Able(av) => av.variable, - } - } - - pub fn opt_ability(&self) -> Option { - match self { - OwnedNamedOrAble::Named(_) => None, - OwnedNamedOrAble::Able(av) => Some(av.ability), - } - } -} - -/// A named type variable, not bound to an ability. -#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] -pub struct NamedVariable { - pub variable: Variable, - pub name: Lowercase, - // NB: there may be multiple occurrences of a variable - pub first_seen: Region, -} - -/// A type variable bound to an ability, like "a has Hash". -#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] -pub struct AbleVariable { - pub variable: Variable, - pub name: Lowercase, - pub ability: Symbol, - // NB: there may be multiple occurrences of a variable - pub first_seen: Region, -} - -#[derive(Clone, Debug, Default)] -pub struct IntroducedVariables { - pub wildcards: Vec>, - pub lambda_sets: Vec, - pub inferred: Vec>, - pub named: VecSet, - pub able: VecSet, - pub host_exposed_aliases: VecMap, -} - -impl IntroducedVariables { - #[inline(always)] - fn debug_assert_not_already_present(&self, var: Variable) { - debug_assert!((self.wildcards.iter().map(|v| &v.value)) - .chain(self.lambda_sets.iter()) - .chain(self.inferred.iter().map(|v| &v.value)) - .chain(self.named.iter().map(|nv| &nv.variable)) - .chain(self.able.iter().map(|av| &av.variable)) - .chain(self.host_exposed_aliases.values()) - .all(|&v| v != var)); - } - - pub fn insert_named(&mut self, name: Lowercase, var: Loc) { - self.debug_assert_not_already_present(var.value); - - let named_variable = NamedVariable { - name, - variable: var.value, - first_seen: var.region, - }; - - self.named.insert(named_variable); - } - - pub fn insert_able(&mut self, name: Lowercase, var: Loc, ability: Symbol) { - self.debug_assert_not_already_present(var.value); - - let able_variable = AbleVariable { - name, - ability, - variable: var.value, - first_seen: var.region, - }; - - self.able.insert(able_variable); - } - - pub fn insert_wildcard(&mut self, var: Loc) { - self.debug_assert_not_already_present(var.value); - self.wildcards.push(var); - } - - pub fn insert_inferred(&mut self, var: Loc) { - self.debug_assert_not_already_present(var.value); - self.inferred.push(var); - } - - fn insert_lambda_set(&mut self, var: Variable) { - self.debug_assert_not_already_present(var); - self.lambda_sets.push(var); - } - - pub fn insert_host_exposed_alias(&mut self, symbol: Symbol, var: Variable) { - self.debug_assert_not_already_present(var); - self.host_exposed_aliases.insert(symbol, var); - } - - pub fn union(&mut self, other: &Self) { - self.wildcards.extend(other.wildcards.iter().copied()); - self.lambda_sets.extend(other.lambda_sets.iter().copied()); - self.inferred.extend(other.inferred.iter().copied()); - self.host_exposed_aliases - .extend(other.host_exposed_aliases.iter().map(|(k, v)| (*k, *v))); - - self.named.extend(other.named.iter().cloned()); - self.able.extend(other.able.iter().cloned()); - } - - pub fn union_owned(&mut self, other: Self) { - self.wildcards.extend(other.wildcards); - self.lambda_sets.extend(other.lambda_sets); - self.inferred.extend(other.inferred); - self.host_exposed_aliases.extend(other.host_exposed_aliases); - - self.named.extend(other.named); - self.able.extend(other.able.iter().cloned()); - } - - pub fn var_by_name(&self, name: &Lowercase) -> Option { - (self.named.iter().map(|nv| (&nv.name, nv.variable))) - .chain(self.able.iter().map(|av| (&av.name, av.variable))) - .find(|(cand, _)| cand == &name) - .map(|(_, var)| var) - } - - pub fn iter_named(&self) -> impl Iterator { - (self.named.iter().map(NamedOrAbleVariable::Named)) - .chain(self.able.iter().map(NamedOrAbleVariable::Able)) - } - - pub fn named_var_by_name(&self, name: &Lowercase) -> Option { - self.iter_named().find(|v| v.name() == name) - } - - pub fn collect_able(&self) -> Vec { - self.able.iter().map(|av| av.variable).collect() - } - - pub fn collect_rigid(&self) -> Vec { - (self.named.iter().map(|nv| nv.variable)) - .chain(self.wildcards.iter().map(|wc| wc.value)) - // For our purposes, lambda set vars are treated like rigids - .chain(self.lambda_sets.iter().copied()) - .collect() - } - - pub fn collect_flex(&self) -> Vec { - self.inferred.iter().map(|iv| iv.value).collect() - } -} - -fn malformed(env: &mut Env, region: Region, name: &str) { - use roc_problem::can::RuntimeError::*; - - let problem = MalformedTypeName((*name).into(), region); - env.problem(roc_problem::can::Problem::RuntimeError(problem)); -} - -/// Canonicalizes a top-level type annotation. -pub fn canonicalize_annotation( - env: &mut Env, - scope: &mut Scope, - annotation: &TypeAnnotation, - region: Region, - var_store: &mut VarStore, - pending_abilities_in_scope: &[Symbol], -) -> Annotation { - let mut introduced_variables = IntroducedVariables::default(); - let mut references = VecSet::default(); - let mut aliases = SendMap::default(); - - let (annotation, region) = match annotation { - TypeAnnotation::Where(annotation, clauses) => { - // Add each "has" clause. The association of a variable to an ability will be saved on - // `introduced_variables`, which we'll process later. - for clause in clauses.iter() { - let opt_err = canonicalize_has_clause( - env, - scope, - var_store, - &mut introduced_variables, - clause, - pending_abilities_in_scope, - &mut references, - ); - if let Err(err_type) = opt_err { - return Annotation { - typ: err_type, - introduced_variables, - references, - aliases, - }; - } - } - (&annotation.value, annotation.region) - } - annot => (annot, region), - }; - - let typ = can_annotation_help( - env, - annotation, - region, - scope, - var_store, - &mut introduced_variables, - &mut aliases, - &mut references, - ); - - Annotation { - typ, - introduced_variables, - references, - aliases, - } -} - -pub(crate) fn make_apply_symbol( - env: &mut Env, - region: Region, - scope: &mut Scope, - module_name: &str, - ident: &str, -) -> Result { - if module_name.is_empty() { - // Since module_name was empty, this is an unqualified type. - // Look it up in scope! - - match scope.lookup_str(ident, region) { - Ok(symbol) => Ok(symbol), - Err(problem) => { - env.problem(roc_problem::can::Problem::RuntimeError(problem)); - - let ident: Ident = (*ident).into(); - Err(Type::Erroneous(Problem::UnrecognizedIdent(ident))) - } - } - } else { - match env.qualified_lookup(scope, module_name, ident, region) { - Ok(symbol) => Ok(symbol), - Err(problem) => { - // Either the module wasn't imported, or - // it was imported but it doesn't expose this ident. - env.problem(roc_problem::can::Problem::RuntimeError(problem)); - - // A failed import should have already been reported through - // roc_can::env::Env::qualified_lookup's checks - Err(Type::Erroneous(Problem::SolvedTypeError)) - } - } - } -} - -/// Retrieves all symbols in an annotations that reference a type definition, that is either an -/// alias or an opaque type. -/// -/// For example, in `[A Age U8, B Str {}]`, there are three type definition references - `Age`, -/// `U8`, and `Str`. -pub fn find_type_def_symbols( - scope: &mut Scope, - initial_annotation: &roc_parse::ast::TypeAnnotation, -) -> Vec { - use roc_parse::ast::TypeAnnotation::*; - - let mut result = Vec::new(); - - let mut stack = vec![initial_annotation]; - - while let Some(annotation) = stack.pop() { - match annotation { - Apply(_module_name, ident, arguments) => { - let ident: Ident = (*ident).into(); - let symbol = scope.scopeless_symbol(&ident, Region::zero()); - - result.push(symbol); - - for t in arguments.iter() { - stack.push(&t.value); - } - } - Function(arguments, result) => { - for t in arguments.iter() { - stack.push(&t.value); - } - - stack.push(&result.value); - } - BoundVariable(_) => {} - As(actual, _, _) => { - stack.push(&actual.value); - } - Record { fields, ext } => { - let mut inner_stack = Vec::with_capacity(fields.items.len()); - - for field in fields.items.iter() { - inner_stack.push(&field.value) - } - - while let Some(assigned_field) = inner_stack.pop() { - match assigned_field { - AssignedField::RequiredValue(_, _, t) - | AssignedField::OptionalValue(_, _, t) => { - stack.push(&t.value); - } - AssignedField::LabelOnly(_) => {} - AssignedField::SpaceBefore(inner, _) - | AssignedField::SpaceAfter(inner, _) => inner_stack.push(inner), - AssignedField::Malformed(_) => {} - } - } - - for t in ext.iter() { - stack.push(&t.value); - } - } - TagUnion { ext, tags } => { - let mut inner_stack = Vec::with_capacity(tags.items.len()); - - for tag in tags.items.iter() { - inner_stack.push(&tag.value) - } - - while let Some(tag) = inner_stack.pop() { - match tag { - Tag::Apply { args, .. } => { - for t in args.iter() { - stack.push(&t.value); - } - } - Tag::SpaceBefore(inner, _) | Tag::SpaceAfter(inner, _) => { - inner_stack.push(inner) - } - Tag::Malformed(_) => {} - } - } - - for t in ext.iter() { - stack.push(&t.value); - } - } - SpaceBefore(inner, _) | SpaceAfter(inner, _) => { - stack.push(inner); - } - Where(annotation, clauses) => { - stack.push(&annotation.value); - - for has_clause in clauses.iter() { - stack.push(&has_clause.value.ability.value); - } - } - Inferred | Wildcard | Malformed(_) => {} - } - } - - result -} - -fn find_fresh_var_name(introduced_variables: &IntroducedVariables) -> Lowercase { - name_type_var(0, &mut introduced_variables.iter_named(), |var, str| { - var.name().as_str() == str - }) - .0 -} - -#[allow(clippy::too_many_arguments)] -fn can_annotation_help( - env: &mut Env, - annotation: &roc_parse::ast::TypeAnnotation, - region: Region, - scope: &mut Scope, - var_store: &mut VarStore, - introduced_variables: &mut IntroducedVariables, - local_aliases: &mut SendMap, - references: &mut VecSet, -) -> Type { - use roc_parse::ast::TypeAnnotation::*; - - match annotation { - Function(argument_types, return_type) => { - let mut args = Vec::new(); - - for arg in *argument_types { - let arg_ann = can_annotation_help( - env, - &arg.value, - arg.region, - scope, - var_store, - introduced_variables, - local_aliases, - references, - ); - - args.push(arg_ann); - } - - let ret = can_annotation_help( - env, - &return_type.value, - return_type.region, - scope, - var_store, - introduced_variables, - local_aliases, - references, - ); - - let lambda_set = var_store.fresh(); - introduced_variables.insert_lambda_set(lambda_set); - let closure = Type::Variable(lambda_set); - - Type::Function(args, Box::new(closure), Box::new(ret)) - } - Apply(module_name, ident, type_arguments) => { - let symbol = match make_apply_symbol(env, region, scope, module_name, ident) { - Err(problem) => return problem, - Ok(symbol) => symbol, - }; - - let mut args = Vec::new(); - - references.insert(symbol); - - if scope.abilities_store.is_ability(symbol) { - let fresh_ty_var = find_fresh_var_name(introduced_variables); - - env.problem(roc_problem::can::Problem::AbilityUsedAsType( - fresh_ty_var.clone(), - symbol, - region, - )); - - // Generate an variable bound to the ability so we can keep compiling. - let var = var_store.fresh(); - introduced_variables.insert_able(fresh_ty_var, Loc::at(region, var), symbol); - return Type::Variable(var); - } - - for arg in *type_arguments { - let arg_ann = can_annotation_help( - env, - &arg.value, - arg.region, - scope, - var_store, - introduced_variables, - local_aliases, - references, - ); - - args.push(arg_ann); - } - - match scope.lookup_alias(symbol) { - Some(alias) => { - // use a known alias - - if alias.type_variables.len() != args.len() { - let error = Type::Erroneous(Problem::BadTypeArguments { - symbol, - region, - alias_needs: alias.type_variables.len() as u8, - type_got: args.len() as u8, - }); - return error; - } - - let mut type_var_to_arg = Vec::new(); - - for (_, arg_ann) in alias.type_variables.iter().zip(args) { - type_var_to_arg.push(arg_ann); - } - - let mut lambda_set_variables = - Vec::with_capacity(alias.lambda_set_variables.len()); - - for _ in 0..alias.lambda_set_variables.len() { - let lvar = var_store.fresh(); - - introduced_variables.insert_lambda_set(lvar); - - lambda_set_variables.push(LambdaSet(Type::Variable(lvar))); - } - - Type::DelayedAlias(AliasCommon { - symbol, - type_arguments: type_var_to_arg, - lambda_set_variables, - }) - } - None => Type::Apply(symbol, args, region), - } - } - BoundVariable(v) => { - let name = Lowercase::from(*v); - - match introduced_variables.var_by_name(&name) { - Some(var) => Type::Variable(var), - None => { - let var = var_store.fresh(); - - introduced_variables.insert_named(name, Loc::at(region, var)); - - Type::Variable(var) - } - } - } - As( - loc_inner, - _spaces, - alias_header @ TypeHeader { - name, - vars: loc_vars, - }, - ) => { - let symbol = match scope.introduce(name.value.into(), region) { - Ok(symbol) => symbol, - - Err((original_region, shadow, _new_symbol)) => { - let problem = Problem::Shadowed(original_region, shadow.clone()); - - env.problem(roc_problem::can::Problem::Shadowing { - original_region, - shadow, - kind: ShadowKind::Variable, - }); - - return Type::Erroneous(problem); - } - }; - - let inner_type = can_annotation_help( - env, - &loc_inner.value, - region, - scope, - var_store, - introduced_variables, - local_aliases, - references, - ); - let mut vars = Vec::with_capacity(loc_vars.len()); - let mut lowercase_vars: Vec> = Vec::with_capacity(loc_vars.len()); - - references.insert(symbol); - - for loc_var in *loc_vars { - let var = match loc_var.value { - Pattern::Identifier(name) if name.chars().next().unwrap().is_lowercase() => { - name - } - _ => unreachable!("I thought this was validated during parsing"), - }; - let var_name = Lowercase::from(var); - - // TODO(abilities): check that there are no abilities bound here. - if let Some(var) = introduced_variables.var_by_name(&var_name) { - vars.push(Type::Variable(var)); - lowercase_vars.push(Loc::at( - loc_var.region, - AliasVar { - name: var_name, - var, - opt_bound_ability: None, - }, - )); - } else { - let var = var_store.fresh(); - - introduced_variables - .insert_named(var_name.clone(), Loc::at(loc_var.region, var)); - vars.push(Type::Variable(var)); - - lowercase_vars.push(Loc::at( - loc_var.region, - AliasVar { - name: var_name, - var, - opt_bound_ability: None, - }, - )); - } - } - - let alias_args = vars.clone(); - - let alias_actual = if let Type::TagUnion(tags, ext) = inner_type { - let rec_var = var_store.fresh(); - - let mut new_tags = Vec::with_capacity(tags.len()); - let mut is_nested_datatype = false; - for (tag_name, args) in tags { - let mut new_args = Vec::with_capacity(args.len()); - for arg in args { - let mut new_arg = arg.clone(); - let substitution_result = - new_arg.substitute_alias(symbol, &alias_args, &Type::Variable(rec_var)); - - if let Err(differing_recursion_region) = substitution_result { - env.problems - .push(roc_problem::can::Problem::NestedDatatype { - alias: symbol, - def_region: alias_header.region(), - differing_recursion_region, - }); - is_nested_datatype = true; - } - - // Either way, add the argument; not doing so would only result in more - // confusing error messages later on. - new_args.push(new_arg); - } - new_tags.push((tag_name.clone(), new_args)); - } - if is_nested_datatype { - // We don't have a way to represent nested data types; hence, we don't actually - // use the recursion var in them, and should avoid marking them as such. - Type::TagUnion(new_tags, ext) - } else { - Type::RecursiveTagUnion(rec_var, new_tags, ext) - } - } else { - inner_type - }; - - let mut hidden_variables = MutSet::default(); - hidden_variables.extend(alias_actual.variables()); - - for loc_var in lowercase_vars.iter() { - hidden_variables.remove(&loc_var.value.var); - } - - scope.add_alias( - symbol, - region, - lowercase_vars, - alias_actual, - AliasKind::Structural, // aliases in "as" are never opaque - ); - - let alias = scope.lookup_alias(symbol).unwrap(); - local_aliases.insert(symbol, alias.clone()); - - if vars.is_empty() && env.home == symbol.module_id() { - let actual_var = var_store.fresh(); - introduced_variables.insert_host_exposed_alias(symbol, actual_var); - Type::HostExposedAlias { - name: symbol, - type_arguments: vars, - lambda_set_variables: alias.lambda_set_variables.clone(), - actual: Box::new(alias.typ.clone()), - actual_var, - } - } else { - Type::Alias { - symbol, - type_arguments: vars - .into_iter() - .map(|typ| OptAbleType { - typ, - opt_ability: None, - }) - .collect(), - lambda_set_variables: alias.lambda_set_variables.clone(), - actual: Box::new(alias.typ.clone()), - kind: alias.kind, - } - } - } - - Record { fields, ext } => { - let ext_type = can_extension_type( - env, - scope, - var_store, - introduced_variables, - local_aliases, - references, - ext, - roc_problem::can::ExtensionTypeKind::Record, - ); - - if fields.is_empty() { - match ext { - Some(_) => { - // just `a` does not mean the same as `{}a`, so even - // if there are no fields, still make this a `Record`, - // not an EmptyRec - Type::Record(Default::default(), TypeExtension::from_type(ext_type)) - } - - None => Type::EmptyRec, - } - } else { - let field_types = can_assigned_fields( - env, - &fields.items, - region, - scope, - var_store, - introduced_variables, - local_aliases, - references, - ); - - Type::Record(field_types, TypeExtension::from_type(ext_type)) - } - } - TagUnion { tags, ext, .. } => { - let ext_type = can_extension_type( - env, - scope, - var_store, - introduced_variables, - local_aliases, - references, - ext, - roc_problem::can::ExtensionTypeKind::TagUnion, - ); - - if tags.is_empty() { - match ext { - Some(_) => { - // just `a` does not mean the same as `{}a`, so even - // if there are no fields, still make this a `Record`, - // not an EmptyRec - Type::TagUnion(Default::default(), TypeExtension::from_type(ext_type)) - } - - None => Type::EmptyTagUnion, - } - } else { - let mut tag_types = can_tags( - env, - tags.items, - region, - scope, - var_store, - introduced_variables, - local_aliases, - references, - ); - - // sort here; we later instantiate type aliases, so this type might get duplicated - // many times. Then, when inserting into the subs, the tags are sorted. - // in theory we save a lot of time by sorting once here - insertion_sort_by(&mut tag_types, |a, b| a.0.cmp(&b.0)); - - Type::TagUnion(tag_types, TypeExtension::from_type(ext_type)) - } - } - SpaceBefore(nested, _) | SpaceAfter(nested, _) => can_annotation_help( - env, - nested, - region, - scope, - var_store, - introduced_variables, - local_aliases, - references, - ), - Wildcard => { - let var = var_store.fresh(); - - introduced_variables.insert_wildcard(Loc::at(region, var)); - - Type::Variable(var) - } - Inferred => { - // Inference variables aren't bound to a rigid or a wildcard, so all we have to do is - // make a fresh unconstrained variable, and let the type solver fill it in for us 🤠 - let var = var_store.fresh(); - - introduced_variables.insert_inferred(Loc::at(region, var)); - - Type::Variable(var) - } - Where(_annotation, clauses) => { - debug_assert!(!clauses.is_empty()); - - // Has clauses are allowed only on the top level of a signature, which we handle elsewhere. - env.problem(roc_problem::can::Problem::IllegalHasClause { - region: Region::across_all(clauses.iter().map(|clause| &clause.region)), - }); - - Type::Erroneous(Problem::CanonicalizationProblem) - } - Malformed(string) => { - malformed(env, region, string); - - let var = var_store.fresh(); - - introduced_variables.insert_wildcard(Loc::at(region, var)); - - Type::Variable(var) - } - } -} - -fn canonicalize_has_clause( - env: &mut Env, - scope: &mut Scope, - var_store: &mut VarStore, - introduced_variables: &mut IntroducedVariables, - clause: &Loc>, - pending_abilities_in_scope: &[Symbol], - references: &mut VecSet, -) -> Result<(), Type> { - let Loc { - region, - value: roc_parse::ast::HasClause { var, ability }, - } = clause; - let region = *region; - - let var_name = var.extract_spaces().item; - debug_assert!( - var_name.starts_with(char::is_lowercase), - "Vars should have been parsed as lowercase" - ); - let var_name = Lowercase::from(var_name); - - let ability = match ability.value { - TypeAnnotation::Apply(module_name, ident, _type_arguments) => { - let symbol = make_apply_symbol(env, ability.region, scope, module_name, ident)?; - - // Ability defined locally, whose members we are constructing right now... - if !pending_abilities_in_scope.contains(&symbol) - // or an ability that was imported from elsewhere - && !scope.abilities_store.is_ability(symbol) - { - let region = ability.region; - env.problem(roc_problem::can::Problem::HasClauseIsNotAbility { region }); - return Err(Type::Erroneous(Problem::HasClauseIsNotAbility(region))); - } - symbol - } - _ => { - let region = ability.region; - env.problem(roc_problem::can::Problem::HasClauseIsNotAbility { region }); - return Err(Type::Erroneous(Problem::HasClauseIsNotAbility(region))); - } - }; - - references.insert(ability); - - if let Some(shadowing) = introduced_variables.named_var_by_name(&var_name) { - let var_name_ident = var_name.to_string().into(); - let shadow = Loc::at(region, var_name_ident); - env.problem(roc_problem::can::Problem::Shadowing { - original_region: shadowing.first_seen(), - shadow: shadow.clone(), - kind: ShadowKind::Variable, - }); - return Err(Type::Erroneous(Problem::Shadowed( - shadowing.first_seen(), - shadow, - ))); - } - - let var = var_store.fresh(); - - introduced_variables.insert_able(var_name, Loc::at(region, var), ability); - - Ok(()) -} - -#[allow(clippy::too_many_arguments)] -fn can_extension_type<'a>( - env: &mut Env, - scope: &mut Scope, - var_store: &mut VarStore, - introduced_variables: &mut IntroducedVariables, - local_aliases: &mut SendMap, - references: &mut VecSet, - opt_ext: &Option<&Loc>>, - ext_problem_kind: roc_problem::can::ExtensionTypeKind, -) -> Type { - fn valid_record_ext_type(typ: &Type) -> bool { - // Include erroneous types so that we don't overreport errors. - matches!( - typ, - Type::EmptyRec | Type::Record(..) | Type::Variable(..) | Type::Erroneous(..) - ) - } - fn valid_tag_ext_type(typ: &Type) -> bool { - matches!( - typ, - Type::EmptyTagUnion | Type::TagUnion(..) | Type::Variable(..) | Type::Erroneous(..) - ) - } - - use roc_problem::can::ExtensionTypeKind; - - let (empty_ext_type, valid_extension_type): (_, fn(&Type) -> bool) = match ext_problem_kind { - ExtensionTypeKind::Record => (Type::EmptyRec, valid_record_ext_type), - ExtensionTypeKind::TagUnion => (Type::EmptyTagUnion, valid_tag_ext_type), - }; - - match opt_ext { - Some(loc_ann) => { - let ext_type = can_annotation_help( - env, - &loc_ann.value, - loc_ann.region, - scope, - var_store, - introduced_variables, - local_aliases, - references, - ); - if valid_extension_type(shallow_dealias_with_scope(scope, &ext_type)) { - ext_type - } else { - // Report an error but mark the extension variable to be inferred - // so that we're as permissive as possible. - // - // THEORY: invalid extension types can appear in this position. Otherwise - // they would be caught as errors during unification. - env.problem(roc_problem::can::Problem::InvalidExtensionType { - region: loc_ann.region, - kind: ext_problem_kind, - }); - - let var = var_store.fresh(); - - introduced_variables.insert_inferred(Loc::at_zero(var)); - - Type::Variable(var) - } - } - None => empty_ext_type, - } -} - -/// a shallow dealias, continue until the first constructor is not an alias. -fn shallow_dealias_with_scope<'a>(scope: &'a mut Scope, typ: &'a Type) -> &'a Type { - let mut result = typ; - loop { - match result { - Type::Alias { actual, .. } => { - // another loop - result = actual; - } - Type::DelayedAlias(AliasCommon { symbol, .. }) => match scope.lookup_alias(*symbol) { - None => unreachable!(), - Some(alias) => { - result = &alias.typ; - } - }, - - _ => break, - } - } - - result -} - -pub fn instantiate_and_freshen_alias_type( - var_store: &mut VarStore, - introduced_variables: &mut IntroducedVariables, - type_variables: &[Loc], - type_arguments: Vec, - lambda_set_variables: &[LambdaSet], - mut actual_type: Type, -) -> (Vec<(Lowercase, Type)>, Vec, Type) { - let mut substitutions = ImMap::default(); - let mut type_var_to_arg = Vec::new(); - - for (loc_var, arg_ann) in type_variables.iter().zip(type_arguments.into_iter()) { - let name = loc_var.value.name.clone(); - let var = loc_var.value.var; - - substitutions.insert(var, arg_ann.clone()); - type_var_to_arg.push((name.clone(), arg_ann)); - } - - // make sure the recursion variable is freshly instantiated - if let Type::RecursiveTagUnion(rvar, _, _) = &mut actual_type { - let new = var_store.fresh(); - substitutions.insert(*rvar, Type::Variable(new)); - *rvar = new; - } - - // make sure hidden variables are freshly instantiated - let mut new_lambda_set_variables = Vec::with_capacity(lambda_set_variables.len()); - for typ in lambda_set_variables.iter() { - if let Type::Variable(var) = typ.0 { - let fresh = var_store.fresh(); - substitutions.insert(var, Type::Variable(fresh)); - introduced_variables.insert_lambda_set(fresh); - new_lambda_set_variables.push(LambdaSet(Type::Variable(fresh))); - } else { - unreachable!("at this point there should be only vars in there"); - } - } - - // instantiate variables - actual_type.substitute(&substitutions); - - (type_var_to_arg, new_lambda_set_variables, actual_type) -} - -pub fn freshen_opaque_def( - var_store: &mut VarStore, - opaque: &Alias, -) -> (Vec, Vec, Type) { - debug_assert!(opaque.kind == AliasKind::Opaque); - - let fresh_variables: Vec = opaque - .type_variables - .iter() - .map(|alias_var| OptAbleVar { - var: var_store.fresh(), - opt_ability: alias_var.value.opt_bound_ability, - }) - .collect(); - - let fresh_type_arguments = fresh_variables - .iter() - .map(|av| Type::Variable(av.var)) - .collect(); - - // NB: We don't introduce the fresh variables here, we introduce them during constraint gen. - // NB: If there are bugs, check whether this is a problem! - let mut introduced_variables = IntroducedVariables::default(); - - let (_fresh_type_arguments, fresh_lambda_set, fresh_type) = instantiate_and_freshen_alias_type( - var_store, - &mut introduced_variables, - &opaque.type_variables, - fresh_type_arguments, - &opaque.lambda_set_variables, - opaque.typ.clone(), - ); - - (fresh_variables, fresh_lambda_set, fresh_type) -} - -fn insertion_sort_by(arr: &mut [T], mut compare: F) -where - F: FnMut(&T, &T) -> std::cmp::Ordering, -{ - for i in 1..arr.len() { - let val = &arr[i]; - let mut j = i; - let pos = arr[..i] - .binary_search_by(|x| compare(x, val)) - .unwrap_or_else(|pos| pos); - // Swap all elements until specific position. - while j > pos { - arr.swap(j - 1, j); - j -= 1; - } - } -} - -// TODO trim down these arguments! -#[allow(clippy::too_many_arguments)] -fn can_assigned_fields<'a>( - env: &mut Env, - fields: &&[Loc>>], - region: Region, - scope: &mut Scope, - var_store: &mut VarStore, - introduced_variables: &mut IntroducedVariables, - local_aliases: &mut SendMap, - references: &mut VecSet, -) -> SendMap> { - use roc_parse::ast::AssignedField::*; - use roc_types::types::RecordField::*; - - // SendMap doesn't have a `with_capacity` - let mut field_types = SendMap::default(); - - // field names we've seen so far in this record - let mut seen = std::collections::HashMap::with_capacity(fields.len()); - - 'outer: for loc_field in fields.iter() { - let mut field = &loc_field.value; - - // use this inner loop to unwrap the SpaceAfter/SpaceBefore - // when we find the name of this field, break out of the loop - // with that value, so we can check whether the field name is - // a duplicate - let new_name = 'inner: loop { - match field { - RequiredValue(field_name, _, annotation) => { - let field_type = can_annotation_help( - env, - &annotation.value, - annotation.region, - scope, - var_store, - introduced_variables, - local_aliases, - references, - ); - - let label = Lowercase::from(field_name.value); - field_types.insert(label.clone(), Required(field_type)); - - break 'inner label; - } - OptionalValue(field_name, _, annotation) => { - let field_type = can_annotation_help( - env, - &annotation.value, - annotation.region, - scope, - var_store, - introduced_variables, - local_aliases, - references, - ); - - let label = Lowercase::from(field_name.value); - field_types.insert(label.clone(), Optional(field_type)); - - break 'inner label; - } - LabelOnly(loc_field_name) => { - // Interpret { a, b } as { a : a, b : b } - let field_name = Lowercase::from(loc_field_name.value); - let field_type = { - if let Some(var) = introduced_variables.var_by_name(&field_name) { - Type::Variable(var) - } else { - let field_var = var_store.fresh(); - introduced_variables.insert_named( - field_name.clone(), - Loc::at(loc_field_name.region, field_var), - ); - Type::Variable(field_var) - } - }; - - field_types.insert(field_name.clone(), Required(field_type)); - - break 'inner field_name; - } - SpaceBefore(nested, _) | SpaceAfter(nested, _) => { - // check the nested field instead - field = nested; - continue 'inner; - } - Malformed(string) => { - malformed(env, region, string); - - // completely skip this element, advance to the next tag - continue 'outer; - } - } - }; - - // ensure that the new name is not already in this record: - // note that the right-most tag wins when there are two with the same name - if let Some(replaced_region) = seen.insert(new_name.clone(), loc_field.region) { - env.problem(roc_problem::can::Problem::DuplicateRecordFieldType { - field_name: new_name, - record_region: region, - field_region: loc_field.region, - replaced_region, - }); - } - } - - field_types -} - -// TODO trim down these arguments! -#[allow(clippy::too_many_arguments)] -fn can_tags<'a>( - env: &mut Env, - tags: &'a [Loc>], - region: Region, - scope: &mut Scope, - var_store: &mut VarStore, - introduced_variables: &mut IntroducedVariables, - local_aliases: &mut SendMap, - references: &mut VecSet, -) -> Vec<(TagName, Vec)> { - let mut tag_types = Vec::with_capacity(tags.len()); - - // tag names we've seen so far in this tag union - let mut seen = std::collections::HashMap::with_capacity(tags.len()); - - 'outer: for loc_tag in tags.iter() { - let mut tag = &loc_tag.value; - - // use this inner loop to unwrap the SpaceAfter/SpaceBefore - // when we find the name of this tag, break out of the loop - // with that value, so we can check whether the tag name is - // a duplicate - let new_name = 'inner: loop { - match tag { - Tag::Apply { name, args } => { - let name = name.value.into(); - let mut arg_types = Vec::with_capacity(args.len()); - - for arg in args.iter() { - let ann = can_annotation_help( - env, - &arg.value, - arg.region, - scope, - var_store, - introduced_variables, - local_aliases, - references, - ); - - arg_types.push(ann); - } - - let tag_name = TagName(name); - tag_types.push((tag_name.clone(), arg_types)); - - break 'inner tag_name; - } - Tag::SpaceBefore(nested, _) | Tag::SpaceAfter(nested, _) => { - // check the nested tag instead - tag = nested; - continue 'inner; - } - Tag::Malformed(string) => { - malformed(env, region, string); - - // completely skip this element, advance to the next tag - continue 'outer; - } - } - }; - - // ensure that the new name is not already in this tag union: - // note that the right-most tag wins when there are two with the same name - if let Some(replaced_region) = seen.insert(new_name.clone(), loc_tag.region) { - env.problem(roc_problem::can::Problem::DuplicateTag { - tag_name: new_name, - tag_region: loc_tag.region, - tag_union_region: region, - replaced_region, - }); - } - } - - tag_types -} diff --git a/compiler/can/src/builtins.rs b/compiler/can/src/builtins.rs deleted file mode 100644 index 48bd09161d..0000000000 --- a/compiler/can/src/builtins.rs +++ /dev/null @@ -1,5457 +0,0 @@ -use crate::def::Def; -use crate::expr::{self, AnnotatedMark, ClosureData, Expr::*, IntValue}; -use crate::expr::{Expr, Field, Recursive}; -use crate::num::{FloatBound, IntBound, IntWidth, NumBound}; -use crate::pattern::Pattern; -use roc_collections::all::SendMap; -use roc_module::called_via::CalledVia; -use roc_module::ident::{Lowercase, TagName}; -use roc_module::low_level::LowLevel; -use roc_module::symbol::Symbol; -use roc_region::all::{Loc, Region}; -use roc_types::subs::{ExhaustiveMark, RedundantMark, VarStore, Variable}; - -macro_rules! macro_magic { - (@single $($x:tt)*) => (()); - (@count $($rest:expr),*) => (<[()]>::len(&[$(matches!(@single $rest)),*])); - - ($symbol:expr; $var_store:expr; $($key:ident => $func:expr,)+) => { macro_magic!($symbol; $var_store; $($key => $func),+) }; - ($symbol:expr; $var_store:expr; $($key:ident => $func:expr),*) => { - { - match $symbol { - $( - Symbol::$key => Some($func(Symbol::$key, $var_store)), - )* - _ => None, - } - } - }; -} - -/// Some builtins cannot be constructed in code gen alone, and need to be defined -/// as separate Roc defs. For example, List.get has this type: -/// -/// List.get : List elem, Nat -> Result elem [OutOfBounds]* -/// -/// Because this returns an open tag union for its Err type, it's not possible -/// for code gen to return a hardcoded value for OutOfBounds. For example, -/// if this Result unifies to [Foo, OutOfBounds] then OutOfBOunds will -/// get assigned the number 1 (because Foo got 0 alphabetically), whereas -/// if it unifies to [OutOfBounds, Qux] then OutOfBounds will get the number 0. -/// -/// Getting these numbers right requires having List.get participate in the -/// normal type-checking and monomorphization processes. As such, this function -/// returns a normal def for List.get, which performs a bounds check and then -/// delegates to the compiler-internal List.getUnsafe function to do the actual -/// lookup (if the bounds check passed). That internal function is hardcoded in code gen, -/// which works fine because it doesn't involve any open tag unions. - -/// Does a builtin depend on any other builtins? -/// -/// NOTE: you are supposed to give all symbols that are relied on, -/// even those that are relied on transitively! -pub fn builtin_dependencies(symbol: Symbol) -> &'static [Symbol] { - match symbol { - Symbol::LIST_SORT_ASC => &[Symbol::LIST_SORT_WITH, Symbol::NUM_COMPARE], - Symbol::LIST_SORT_DESC => &[Symbol::LIST_SORT_WITH], - Symbol::LIST_PRODUCT => &[Symbol::LIST_WALK, Symbol::NUM_MUL], - Symbol::LIST_SUM => &[Symbol::LIST_WALK, Symbol::NUM_ADD], - Symbol::LIST_JOIN_MAP => &[Symbol::LIST_WALK, Symbol::LIST_CONCAT], - Symbol::LIST_SET => &[Symbol::LIST_REPLACE], - _ => &[], - } -} - -/// Implementation for a builtin -pub fn builtin_defs_map(symbol: Symbol, var_store: &mut VarStore) -> Option { - debug_assert!(symbol.is_builtin()); - - macro_magic! { symbol; var_store; - BOOL_EQ => bool_eq, - BOOL_NEQ => bool_neq, - BOOL_AND => bool_and, - BOOL_OR => bool_or, - BOOL_NOT => bool_not, - STR_CONCAT => str_concat, - STR_JOIN_WITH => str_join_with, - STR_SPLIT => str_split, - STR_IS_EMPTY => str_is_empty, - STR_STARTS_WITH => str_starts_with, - STR_STARTS_WITH_CODE_PT => str_starts_with_code_point, - STR_ENDS_WITH => str_ends_with, - STR_COUNT_GRAPHEMES => str_count_graphemes, - STR_FROM_UTF8 => str_from_utf8, - STR_FROM_UTF8_RANGE => str_from_utf8_range, - STR_TO_UTF8 => str_to_utf8, - STR_REPEAT => str_repeat, - STR_TRIM => str_trim, - STR_TRIM_LEFT => str_trim_left, - STR_TRIM_RIGHT => str_trim_right, - STR_TO_DEC => str_to_num, - STR_TO_F64 => str_to_num, - STR_TO_F32 => str_to_num, - STR_TO_NAT => str_to_num, - STR_TO_U128 => str_to_num, - STR_TO_I128 => str_to_num, - STR_TO_U64 => str_to_num, - STR_TO_I64 => str_to_num, - STR_TO_U32 => str_to_num, - STR_TO_I32 => str_to_num, - STR_TO_U16 => str_to_num, - STR_TO_I16 => str_to_num, - STR_TO_U8 => str_to_num, - STR_TO_I8 => str_to_num, - LIST_LEN => list_len, - LIST_GET => list_get, - LIST_REPLACE => list_replace, - LIST_SET => list_set, - LIST_APPEND => list_append, - LIST_FIRST => list_first, - LIST_LAST => list_last, - LIST_IS_EMPTY => list_is_empty, - LIST_SINGLE => list_single, - LIST_REPEAT => list_repeat, - LIST_REVERSE => list_reverse, - LIST_CONCAT => list_concat, - LIST_CONTAINS => list_contains, - LIST_MIN => list_min, - LIST_MAX => list_max, - LIST_SUM => list_sum, - LIST_PRODUCT => list_product, - LIST_PREPEND => list_prepend, - LIST_JOIN => list_join, - LIST_JOIN_MAP => list_join_map, - LIST_MAP => list_map, - LIST_MAP2 => list_map2, - LIST_MAP3 => list_map3, - LIST_MAP4 => list_map4, - LIST_TAKE_FIRST => list_take_first, - LIST_TAKE_LAST => list_take_last, - LIST_SUBLIST => list_sublist, - LIST_SPLIT => list_split, - LIST_INTERSPERSE => list_intersperse, - LIST_DROP => list_drop, - LIST_DROP_AT => list_drop_at, - LIST_DROP_FIRST => list_drop_first, - LIST_DROP_IF => list_drop_if, - LIST_DROP_LAST => list_drop_last, - LIST_SWAP => list_swap, - LIST_MAP_WITH_INDEX => list_map_with_index, - LIST_KEEP_IF => list_keep_if, - LIST_KEEP_OKS => list_keep_oks, - LIST_KEEP_ERRS=> list_keep_errs, - LIST_RANGE => list_range, - LIST_WALK => list_walk, - LIST_WALK_BACKWARDS => list_walk_backwards, - LIST_WALK_UNTIL => list_walk_until, - LIST_SORT_WITH => list_sort_with, - LIST_SORT_ASC => list_sort_asc, - LIST_SORT_DESC => list_sort_desc, - LIST_ANY => list_any, - LIST_ALL => list_all, - LIST_FIND => list_find, - LIST_IS_UNIQUE => list_is_unique, - DICT_LEN => dict_len, - DICT_EMPTY => dict_empty, - DICT_SINGLE => dict_single, - DICT_INSERT => dict_insert, - DICT_REMOVE => dict_remove, - DICT_GET => dict_get, - DICT_CONTAINS => dict_contains, - DICT_KEYS => dict_keys, - DICT_VALUES => dict_values, - DICT_UNION=> dict_union, - DICT_INTERSECTION=> dict_intersection, - DICT_DIFFERENCE=> dict_difference, - DICT_WALK=> dict_walk, - SET_EMPTY => set_empty, - SET_LEN => set_len, - SET_SINGLE => set_single, - SET_UNION=> set_union, - SET_INTERSECTION => set_intersection, - SET_DIFFERENCE => set_difference, - SET_TO_LIST => set_to_list, - SET_FROM_LIST => set_from_list, - SET_TO_DICT=> set_to_dict, - SET_INSERT => set_insert, - SET_REMOVE => set_remove, - SET_CONTAINS => set_contains, - SET_WALK=> set_walk, - NUM_ADD => num_add, - NUM_ADD_CHECKED => num_add_checked, - NUM_ADD_WRAP => num_add_wrap, - NUM_ADD_SATURATED => num_add_saturated, - NUM_SUB => num_sub, - NUM_SUB_WRAP => num_sub_wrap, - NUM_SUB_CHECKED => num_sub_checked, - NUM_SUB_SATURATED => num_sub_saturated, - NUM_MUL => num_mul, - NUM_MUL_WRAP => num_mul_wrap, - NUM_MUL_CHECKED => num_mul_checked, - NUM_GT => num_gt, - NUM_GTE => num_gte, - NUM_LT => num_lt, - NUM_LTE => num_lte, - NUM_COMPARE => num_compare, - NUM_SIN => num_sin, - NUM_COS => num_cos, - NUM_TAN => num_tan, - NUM_DIV_FRAC => num_div_frac, - NUM_DIV_FRAC_CHECKED => num_div_frac_checked, - NUM_DIV_TRUNC => num_div_trunc, - NUM_DIV_TRUNC_CHECKED => num_div_trunc_checked, - NUM_DIV_CEIL => num_div_ceil, - NUM_DIV_CEIL_CHECKED => num_div_ceil_checked, - NUM_ABS => num_abs, - NUM_NEG => num_neg, - NUM_REM => num_rem, - NUM_REM_CHECKED => num_rem_checked, - NUM_IS_MULTIPLE_OF => num_is_multiple_of, - NUM_SQRT => num_sqrt, - NUM_SQRT_CHECKED => num_sqrt_checked, - NUM_LOG => num_log, - NUM_LOG_CHECKED => num_log_checked, - NUM_ROUND => num_round, - NUM_IS_ODD => num_is_odd, - NUM_IS_EVEN => num_is_even, - NUM_IS_ZERO => num_is_zero, - NUM_IS_POSITIVE => num_is_positive, - NUM_IS_NEGATIVE => num_is_negative, - NUM_TO_FRAC => num_to_frac, - NUM_POW => num_pow, - NUM_CEILING => num_ceiling, - NUM_POW_INT => num_pow_int, - NUM_FLOOR => num_floor, - NUM_ATAN => num_atan, - NUM_ACOS => num_acos, - NUM_ASIN => num_asin, - NUM_BYTES_TO_U16 => num_bytes_to_u16, - NUM_BYTES_TO_U32 => num_bytes_to_u32, - NUM_BITWISE_AND => num_bitwise_and, - NUM_BITWISE_XOR => num_bitwise_xor, - NUM_BITWISE_OR => num_bitwise_or, - NUM_SHIFT_LEFT=> num_shift_left_by, - NUM_SHIFT_RIGHT => num_shift_right_by, - NUM_SHIFT_RIGHT_ZERO_FILL => num_shift_right_zf_by, - NUM_INT_CAST=> num_int_cast, - NUM_TO_I8 => num_to_i8, - NUM_TO_I8_CHECKED => num_to_i8_checked, - NUM_TO_I16 => num_to_i16, - NUM_TO_I16_CHECKED => num_to_i16_checked, - NUM_TO_I32 => num_to_i32, - NUM_TO_I32_CHECKED => num_to_i32_checked, - NUM_TO_I64 => num_to_i64, - NUM_TO_I64_CHECKED => num_to_i64_checked, - NUM_TO_I128 => num_to_i128, - NUM_TO_I128_CHECKED => num_to_i128_checked, - NUM_TO_U8 => num_to_u8, - NUM_TO_U8_CHECKED => num_to_u8_checked, - NUM_TO_U16 => num_to_u16, - NUM_TO_U16_CHECKED => num_to_u16_checked, - NUM_TO_U32 => num_to_u32, - NUM_TO_U32_CHECKED => num_to_u32_checked, - NUM_TO_U64 => num_to_u64, - NUM_TO_U64_CHECKED => num_to_u64_checked, - NUM_TO_U128 => num_to_u128, - NUM_TO_U128_CHECKED => num_to_u128_checked, - NUM_TO_NAT => num_to_nat, - NUM_TO_NAT_CHECKED => num_to_nat_checked, - NUM_TO_F32 => num_to_f32, - NUM_TO_F32_CHECKED => num_to_f32_checked, - NUM_TO_F64 => num_to_f64, - NUM_TO_F64_CHECKED => num_to_f64_checked, - NUM_TO_STR => num_to_str, - RESULT_MAP => result_map, - RESULT_MAP_ERR => result_map_err, - RESULT_AFTER => result_after, - RESULT_WITH_DEFAULT => result_with_default, - RESULT_IS_OK => result_is_ok, - RESULT_IS_ERR => result_is_err, - BOX_BOX_FUNCTION => box_box, - BOX_UNBOX => box_unbox, - } -} - -fn lowlevel_1(symbol: Symbol, op: LowLevel, var_store: &mut VarStore) -> Def { - let arg1_var = var_store.fresh(); - let ret_var = var_store.fresh(); - - let body = RunLowLevel { - op, - args: vec![(arg1_var, Var(Symbol::ARG_1))], - ret_var, - }; - - defn( - symbol, - vec![(arg1_var, Symbol::ARG_1)], - var_store, - body, - ret_var, - ) -} - -fn lowlevel_2(symbol: Symbol, op: LowLevel, var_store: &mut VarStore) -> Def { - let arg1_var = var_store.fresh(); - let arg2_var = var_store.fresh(); - let ret_var = var_store.fresh(); - - let body = RunLowLevel { - op, - args: vec![ - (arg1_var, Var(Symbol::ARG_1)), - (arg2_var, Var(Symbol::ARG_2)), - ], - ret_var, - }; - - defn( - symbol, - vec![(arg1_var, Symbol::ARG_1), (arg2_var, Symbol::ARG_2)], - var_store, - body, - ret_var, - ) -} - -fn lowlevel_3(symbol: Symbol, op: LowLevel, var_store: &mut VarStore) -> Def { - let arg1_var = var_store.fresh(); - let arg2_var = var_store.fresh(); - let arg3_var = var_store.fresh(); - let ret_var = var_store.fresh(); - - let body = RunLowLevel { - op, - args: vec![ - (arg1_var, Var(Symbol::ARG_1)), - (arg2_var, Var(Symbol::ARG_2)), - (arg3_var, Var(Symbol::ARG_3)), - ], - ret_var, - }; - - defn( - symbol, - vec![ - (arg1_var, Symbol::ARG_1), - (arg2_var, Symbol::ARG_2), - (arg3_var, Symbol::ARG_3), - ], - var_store, - body, - ret_var, - ) -} - -fn lowlevel_4(symbol: Symbol, op: LowLevel, var_store: &mut VarStore) -> Def { - let arg1_var = var_store.fresh(); - let arg2_var = var_store.fresh(); - let arg3_var = var_store.fresh(); - let arg4_var = var_store.fresh(); - let ret_var = var_store.fresh(); - - let body = RunLowLevel { - op, - args: vec![ - (arg1_var, Var(Symbol::ARG_1)), - (arg2_var, Var(Symbol::ARG_2)), - (arg3_var, Var(Symbol::ARG_3)), - (arg4_var, Var(Symbol::ARG_4)), - ], - ret_var, - }; - - defn( - symbol, - vec![ - (arg1_var, Symbol::ARG_1), - (arg2_var, Symbol::ARG_2), - (arg3_var, Symbol::ARG_3), - (arg4_var, Symbol::ARG_4), - ], - var_store, - body, - ret_var, - ) -} - -fn lowlevel_5(symbol: Symbol, op: LowLevel, var_store: &mut VarStore) -> Def { - let arg1_var = var_store.fresh(); - let arg2_var = var_store.fresh(); - let arg3_var = var_store.fresh(); - let arg4_var = var_store.fresh(); - let arg5_var = var_store.fresh(); - let ret_var = var_store.fresh(); - - let body = RunLowLevel { - op, - args: vec![ - (arg1_var, Var(Symbol::ARG_1)), - (arg2_var, Var(Symbol::ARG_2)), - (arg3_var, Var(Symbol::ARG_3)), - (arg4_var, Var(Symbol::ARG_4)), - (arg5_var, Var(Symbol::ARG_5)), - ], - ret_var, - }; - - defn( - symbol, - vec![ - (arg1_var, Symbol::ARG_1), - (arg2_var, Symbol::ARG_2), - (arg3_var, Symbol::ARG_3), - (arg4_var, Symbol::ARG_4), - (arg5_var, Symbol::ARG_5), - ], - var_store, - body, - ret_var, - ) -} - -// Num.toI8 : Int * -> I8 -fn num_to_i8(symbol: Symbol, var_store: &mut VarStore) -> Def { - // Defer to IntCast - lowlevel_1(symbol, LowLevel::NumIntCast, var_store) -} - -// Num.toI16 : Int * -> I16 -fn num_to_i16(symbol: Symbol, var_store: &mut VarStore) -> Def { - // Defer to IntCast - lowlevel_1(symbol, LowLevel::NumIntCast, var_store) -} - -// Num.toI32 : Int * -> I32 -fn num_to_i32(symbol: Symbol, var_store: &mut VarStore) -> Def { - // Defer to IntCast - lowlevel_1(symbol, LowLevel::NumIntCast, var_store) -} - -// Num.toI64 : Int * -> I64 -fn num_to_i64(symbol: Symbol, var_store: &mut VarStore) -> Def { - // Defer to IntCast - lowlevel_1(symbol, LowLevel::NumIntCast, var_store) -} - -// Num.toI128 : Int * -> I128 -fn num_to_i128(symbol: Symbol, var_store: &mut VarStore) -> Def { - // Defer to IntCast - lowlevel_1(symbol, LowLevel::NumIntCast, var_store) -} - -// Num.toU8 : Int * -> U8 -fn num_to_u8(symbol: Symbol, var_store: &mut VarStore) -> Def { - // Defer to IntCast - lowlevel_1(symbol, LowLevel::NumIntCast, var_store) -} - -// Num.toU16 : Int * -> U16 -fn num_to_u16(symbol: Symbol, var_store: &mut VarStore) -> Def { - // Defer to IntCast - lowlevel_1(symbol, LowLevel::NumIntCast, var_store) -} - -// Num.toU32 : Int * -> U32 -fn num_to_u32(symbol: Symbol, var_store: &mut VarStore) -> Def { - // Defer to IntCast - lowlevel_1(symbol, LowLevel::NumIntCast, var_store) -} - -// Num.toU64 : Int * -> U64 -fn num_to_u64(symbol: Symbol, var_store: &mut VarStore) -> Def { - // Defer to IntCast - lowlevel_1(symbol, LowLevel::NumIntCast, var_store) -} - -// Num.toU128 : Int * -> U128 -fn num_to_u128(symbol: Symbol, var_store: &mut VarStore) -> Def { - // Defer to IntCast - lowlevel_1(symbol, LowLevel::NumIntCast, var_store) -} - -// Num.toNat : Int * -> Nat -fn num_to_nat(symbol: Symbol, var_store: &mut VarStore) -> Def { - // Defer to IntCast - lowlevel_1(symbol, LowLevel::NumIntCast, var_store) -} - -// Num.toF32 : Num * -> F32 -fn num_to_f32(symbol: Symbol, var_store: &mut VarStore) -> Def { - // Defer to NumToFloatCast - lowlevel_1(symbol, LowLevel::NumToFloatCast, var_store) -} - -// Num.toF64 : Num * -> F64 -fn num_to_f64(symbol: Symbol, var_store: &mut VarStore) -> Def { - // Defer to NumToFloatCast - lowlevel_1(symbol, LowLevel::NumToFloatCast, var_store) -} - -fn to_num_checked(symbol: Symbol, var_store: &mut VarStore, lowlevel: LowLevel) -> Def { - let bool_var = var_store.fresh(); - let num_var_1 = var_store.fresh(); - let num_var_2 = var_store.fresh(); - let ret_var = var_store.fresh(); - let record_var = var_store.fresh(); - - // let arg_2 = RunLowLevel NumToXXXChecked arg_1 - // if arg_2.b then - // Err OutOfBounds - // else - // Ok arg_2.a - // - // "a" and "b" because the lowlevel return value looks like { converted_val: XXX, out_of_bounds: bool }, - // and codegen will sort by alignment, so "a" will be the first key, etc. - - let cont = If { - branch_var: ret_var, - cond_var: bool_var, - branches: vec![( - // if-condition - no_region( - // arg_2.b - Access { - record_var, - ext_var: var_store.fresh(), - field: "b".into(), - field_var: var_store.fresh(), - loc_expr: Box::new(no_region(Var(Symbol::ARG_2))), - }, - ), - // out of bounds! - no_region(tag( - "Err", - vec![tag("OutOfBounds", Vec::new(), var_store)], - var_store, - )), - )], - final_else: Box::new( - // all is well - no_region( - // Ok arg_2.a - tag( - "Ok", - vec![ - // arg_2.a - Access { - record_var, - ext_var: var_store.fresh(), - field: "a".into(), - field_var: num_var_2, - loc_expr: Box::new(no_region(Var(Symbol::ARG_2))), - }, - ], - var_store, - ), - ), - ), - }; - - // arg_2 = RunLowLevel NumToXXXChecked arg_1 - let def = crate::def::Def { - loc_pattern: no_region(Pattern::Identifier(Symbol::ARG_2)), - loc_expr: no_region(RunLowLevel { - op: lowlevel, - args: vec![(num_var_1, Var(Symbol::ARG_1))], - ret_var: record_var, - }), - expr_var: record_var, - pattern_vars: SendMap::default(), - annotation: None, - }; - - let body = LetNonRec(Box::new(def), Box::new(no_region(cont))); - - defn( - symbol, - vec![(num_var_1, Symbol::ARG_1)], - var_store, - body, - ret_var, - ) -} - -macro_rules! num_to_checked { - ($($fn:ident)*) => {$( - // Num.toXXXChecked : Int * -> Result XXX [OutOfBounds]* - fn $fn(symbol: Symbol, var_store: &mut VarStore) -> Def { - // Use the generic `NumToIntChecked`; we'll figure out exactly what layout(s) we need - // during code generation after types are resolved. - to_num_checked(symbol, var_store, LowLevel::NumToIntChecked) - } - )*} -} - -num_to_checked! { - num_to_i8_checked - num_to_i16_checked - num_to_i32_checked - num_to_i64_checked - num_to_i128_checked - num_to_u8_checked - num_to_u16_checked - num_to_u32_checked - num_to_u64_checked - num_to_u128_checked - num_to_nat_checked - num_to_f32_checked - num_to_f64_checked -} - -// Num.toStr : Num a -> Str -fn num_to_str(symbol: Symbol, var_store: &mut VarStore) -> Def { - let num_var = var_store.fresh(); - let str_var = var_store.fresh(); - - let body = RunLowLevel { - op: LowLevel::NumToStr, - args: vec![(num_var, Var(Symbol::ARG_1))], - ret_var: str_var, - }; - - defn( - symbol, - vec![(num_var, Symbol::ARG_1)], - var_store, - body, - str_var, - ) -} - -/// Bool.isEq : val, val -> Bool -fn bool_eq(symbol: Symbol, var_store: &mut VarStore) -> Def { - let arg_var = var_store.fresh(); - let bool_var = var_store.fresh(); - let body = RunLowLevel { - op: LowLevel::Eq, - args: vec![(arg_var, Var(Symbol::ARG_1)), (arg_var, Var(Symbol::ARG_2))], - ret_var: bool_var, - }; - - defn( - symbol, - vec![(arg_var, Symbol::ARG_1), (arg_var, Symbol::ARG_2)], - var_store, - body, - bool_var, - ) -} - -/// Bool.isNotEq : val, val -> Bool -fn bool_neq(symbol: Symbol, var_store: &mut VarStore) -> Def { - let arg_var = var_store.fresh(); - let bool_var = var_store.fresh(); - let body = RunLowLevel { - op: LowLevel::NotEq, - args: vec![(arg_var, Var(Symbol::ARG_1)), (arg_var, Var(Symbol::ARG_2))], - ret_var: bool_var, - }; - - defn( - symbol, - vec![(arg_var, Symbol::ARG_1), (arg_var, Symbol::ARG_2)], - var_store, - body, - bool_var, - ) -} - -/// Bool.or : Bool, Bool -> Bool -fn bool_or(symbol: Symbol, var_store: &mut VarStore) -> Def { - let bool_var = var_store.fresh(); - let body = RunLowLevel { - op: LowLevel::Or, - args: vec![ - (bool_var, Var(Symbol::ARG_1)), - (bool_var, Var(Symbol::ARG_2)), - ], - ret_var: bool_var, - }; - - defn( - symbol, - vec![(bool_var, Symbol::ARG_1), (bool_var, Symbol::ARG_2)], - var_store, - body, - bool_var, - ) -} - -/// Bool.not : Bool -> Bool -fn bool_not(symbol: Symbol, var_store: &mut VarStore) -> Def { - let bool_var = var_store.fresh(); - let body = RunLowLevel { - op: LowLevel::Not, - args: vec![(bool_var, Var(Symbol::ARG_1))], - ret_var: bool_var, - }; - - defn( - symbol, - vec![(bool_var, Symbol::ARG_1)], - var_store, - body, - bool_var, - ) -} - -/// Bool.and : Bool, Bool -> Bool -fn bool_and(symbol: Symbol, var_store: &mut VarStore) -> Def { - let bool_var = var_store.fresh(); - let body = RunLowLevel { - op: LowLevel::And, - args: vec![ - (bool_var, Var(Symbol::ARG_1)), - (bool_var, Var(Symbol::ARG_2)), - ], - ret_var: var_store.fresh(), - }; - - defn( - symbol, - vec![(bool_var, Symbol::ARG_1), (bool_var, Symbol::ARG_2)], - var_store, - body, - bool_var, - ) -} - -fn num_unaryop(symbol: Symbol, var_store: &mut VarStore, op: LowLevel) -> Def { - let num_var = var_store.fresh(); - let body = RunLowLevel { - op, - args: vec![(num_var, Var(Symbol::ARG_1))], - ret_var: num_var, - }; - - defn( - symbol, - vec![(num_var, Symbol::ARG_1)], - var_store, - body, - num_var, - ) -} - -/// Num a, Num a -> Num a -fn num_binop(symbol: Symbol, var_store: &mut VarStore, op: LowLevel) -> Def { - let num_var = var_store.fresh(); - let body = RunLowLevel { - op, - args: vec![(num_var, Var(Symbol::ARG_1)), (num_var, Var(Symbol::ARG_2))], - ret_var: num_var, - }; - - defn( - symbol, - vec![(num_var, Symbol::ARG_1), (num_var, Symbol::ARG_2)], - var_store, - body, - num_var, - ) -} - -/// Num a, Num a -> b -fn num_num_other_binop(symbol: Symbol, var_store: &mut VarStore, op: LowLevel) -> Def { - let num_var = var_store.fresh(); - let bool_var = var_store.fresh(); - let body = RunLowLevel { - op, - args: vec![(num_var, Var(Symbol::ARG_1)), (num_var, Var(Symbol::ARG_2))], - ret_var: bool_var, - }; - - defn( - symbol, - vec![(num_var, Symbol::ARG_1), (num_var, Symbol::ARG_2)], - var_store, - body, - bool_var, - ) -} - -/// Num.add : Num a, Num a -> Num a -fn num_add(symbol: Symbol, var_store: &mut VarStore) -> Def { - num_binop(symbol, var_store, LowLevel::NumAdd) -} - -/// Num.addWrap : Int a, Int a -> Int a -fn num_add_wrap(symbol: Symbol, var_store: &mut VarStore) -> Def { - num_binop(symbol, var_store, LowLevel::NumAddWrap) -} - -fn num_overflow_checked(symbol: Symbol, var_store: &mut VarStore, lowlevel: LowLevel) -> Def { - let bool_var = var_store.fresh(); - let num_var_1 = var_store.fresh(); - let num_var_2 = var_store.fresh(); - let num_var_3 = var_store.fresh(); - let ret_var = var_store.fresh(); - let record_var = var_store.fresh(); - - // let arg_3 = RunLowLevel NumXXXChecked arg_1 arg_2 - // - // if arg_3.b then - // # overflow - // Err Overflow - // else - // # all is well - // Ok arg_3.a - - let cont = If { - branch_var: ret_var, - cond_var: bool_var, - branches: vec![( - // if-condition - no_region( - // arg_3.b - Access { - record_var, - ext_var: var_store.fresh(), - field: "b".into(), - field_var: var_store.fresh(), - loc_expr: Box::new(no_region(Var(Symbol::ARG_3))), - }, - ), - // overflow! - no_region(tag( - "Err", - vec![tag("Overflow", Vec::new(), var_store)], - var_store, - )), - )], - final_else: Box::new( - // all is well - no_region( - // Ok arg_3.a - tag( - "Ok", - vec![ - // arg_3.a - Access { - record_var, - ext_var: var_store.fresh(), - field: "a".into(), - field_var: num_var_3, - loc_expr: Box::new(no_region(Var(Symbol::ARG_3))), - }, - ], - var_store, - ), - ), - ), - }; - - // arg_3 = RunLowLevel NumXXXChecked arg_1 arg_2 - let def = crate::def::Def { - loc_pattern: no_region(Pattern::Identifier(Symbol::ARG_3)), - loc_expr: no_region(RunLowLevel { - op: lowlevel, - args: vec![ - (num_var_1, Var(Symbol::ARG_1)), - (num_var_2, Var(Symbol::ARG_2)), - ], - ret_var: record_var, - }), - expr_var: record_var, - pattern_vars: SendMap::default(), - annotation: None, - }; - - let body = LetNonRec(Box::new(def), Box::new(no_region(cont))); - - defn( - symbol, - vec![(num_var_1, Symbol::ARG_1), (num_var_2, Symbol::ARG_2)], - var_store, - body, - ret_var, - ) -} - -/// Num.addChecked : Num a, Num a -> Result (Num a) [Overflow]* -fn num_add_checked(symbol: Symbol, var_store: &mut VarStore) -> Def { - num_overflow_checked(symbol, var_store, LowLevel::NumAddChecked) -} - -/// Num.addSaturated : Int a, Int a -> Int a -fn num_add_saturated(symbol: Symbol, var_store: &mut VarStore) -> Def { - num_binop(symbol, var_store, LowLevel::NumAddSaturated) -} - -/// Num.sub : Num a, Num a -> Num a -fn num_sub(symbol: Symbol, var_store: &mut VarStore) -> Def { - num_binop(symbol, var_store, LowLevel::NumSub) -} - -/// Num.subWrap : Int a, Int a -> Int a -fn num_sub_wrap(symbol: Symbol, var_store: &mut VarStore) -> Def { - num_binop(symbol, var_store, LowLevel::NumSubWrap) -} - -/// Num.subChecked : Num a, Num a -> Result (Num a) [Overflow]* -fn num_sub_checked(symbol: Symbol, var_store: &mut VarStore) -> Def { - num_overflow_checked(symbol, var_store, LowLevel::NumSubChecked) -} - -/// Num.subSaturated : Int a, Int a -> Int a -fn num_sub_saturated(symbol: Symbol, var_store: &mut VarStore) -> Def { - num_binop(symbol, var_store, LowLevel::NumSubSaturated) -} - -/// Num.mul : Num a, Num a -> Num a -fn num_mul(symbol: Symbol, var_store: &mut VarStore) -> Def { - num_binop(symbol, var_store, LowLevel::NumMul) -} - -/// Num.mulWrap : Int a, Int a -> Int a -fn num_mul_wrap(symbol: Symbol, var_store: &mut VarStore) -> Def { - num_binop(symbol, var_store, LowLevel::NumMulWrap) -} - -/// Num.mulChecked : Num a, Num a -> Result (Num a) [Overflow]* -fn num_mul_checked(symbol: Symbol, var_store: &mut VarStore) -> Def { - num_overflow_checked(symbol, var_store, LowLevel::NumMulChecked) -} - -/// Num.isGt : Num a, Num a -> Bool -fn num_gt(symbol: Symbol, var_store: &mut VarStore) -> Def { - num_num_other_binop(symbol, var_store, LowLevel::NumGt) -} - -/// Num.isGte : Num a, Num a -> Bool -fn num_gte(symbol: Symbol, var_store: &mut VarStore) -> Def { - num_num_other_binop(symbol, var_store, LowLevel::NumGte) -} - -/// Num.isLt : Num a, Num a -> Bool -fn num_lt(symbol: Symbol, var_store: &mut VarStore) -> Def { - num_num_other_binop(symbol, var_store, LowLevel::NumLt) -} - -/// Num.isLte : Num a, Num a -> Bool -fn num_lte(symbol: Symbol, var_store: &mut VarStore) -> Def { - num_num_other_binop(symbol, var_store, LowLevel::NumLte) -} - -/// Num.compare : Num a, Num a -> [LT, EQ, GT] -fn num_compare(symbol: Symbol, var_store: &mut VarStore) -> Def { - num_num_other_binop(symbol, var_store, LowLevel::NumCompare) -} - -/// Num.sin : Frac -> Frac -fn num_sin(symbol: Symbol, var_store: &mut VarStore) -> Def { - let frac_var = var_store.fresh(); - let body = RunLowLevel { - op: LowLevel::NumSin, - args: vec![(frac_var, Var(Symbol::ARG_1))], - ret_var: frac_var, - }; - - defn( - symbol, - vec![(frac_var, Symbol::ARG_1)], - var_store, - body, - frac_var, - ) -} - -/// Num.cos : Frac -> Frac -fn num_cos(symbol: Symbol, var_store: &mut VarStore) -> Def { - let frac_var = var_store.fresh(); - let body = RunLowLevel { - op: LowLevel::NumCos, - args: vec![(frac_var, Var(Symbol::ARG_1))], - ret_var: frac_var, - }; - - defn( - symbol, - vec![(frac_var, Symbol::ARG_1)], - var_store, - body, - frac_var, - ) -} - -/// Num.tan : Frac -> Frac -fn num_tan(symbol: Symbol, var_store: &mut VarStore) -> Def { - let frac_var = var_store.fresh(); - let body = RunLowLevel { - op: LowLevel::NumDivUnchecked, - args: vec![ - ( - frac_var, - RunLowLevel { - op: LowLevel::NumSin, - args: vec![(frac_var, Var(Symbol::ARG_1))], - ret_var: frac_var, - }, - ), - ( - frac_var, - RunLowLevel { - op: LowLevel::NumCos, - args: vec![(frac_var, Var(Symbol::ARG_1))], - ret_var: frac_var, - }, - ), - ], - ret_var: frac_var, - }; - - defn( - symbol, - vec![(frac_var, Symbol::ARG_1)], - var_store, - body, - frac_var, - ) -} - -/// Num.isZero : Num * -> Bool -fn num_is_zero(symbol: Symbol, var_store: &mut VarStore) -> Def { - let arg_var = var_store.fresh(); - let bool_var = var_store.fresh(); - let unbound_zero_var = var_store.fresh(); - - let body = RunLowLevel { - op: LowLevel::Eq, - args: vec![ - (arg_var, Var(Symbol::ARG_1)), - (arg_var, num(unbound_zero_var, 0, num_no_bound())), - ], - ret_var: bool_var, - }; - - defn( - symbol, - vec![(arg_var, Symbol::ARG_1)], - var_store, - body, - bool_var, - ) -} - -/// Num.isNegative : Num * -> Bool -fn num_is_negative(symbol: Symbol, var_store: &mut VarStore) -> Def { - let arg_var = var_store.fresh(); - let bool_var = var_store.fresh(); - let unbound_zero_var = var_store.fresh(); - - let body = RunLowLevel { - op: LowLevel::NumGt, - args: vec![ - (arg_var, num(unbound_zero_var, 0, num_no_bound())), - (arg_var, Var(Symbol::ARG_1)), - ], - ret_var: bool_var, - }; - - defn( - symbol, - vec![(arg_var, Symbol::ARG_1)], - var_store, - body, - bool_var, - ) -} - -/// Num.isPositive : Num * -> Bool -fn num_is_positive(symbol: Symbol, var_store: &mut VarStore) -> Def { - let arg_var = var_store.fresh(); - let bool_var = var_store.fresh(); - let unbound_zero_var = var_store.fresh(); - - let body = RunLowLevel { - op: LowLevel::NumGt, - args: vec![ - (arg_var, Var(Symbol::ARG_1)), - (arg_var, num(unbound_zero_var, 0, num_no_bound())), - ], - ret_var: bool_var, - }; - - defn( - symbol, - vec![(arg_var, Symbol::ARG_1)], - var_store, - body, - bool_var, - ) -} - -/// Num.isOdd : Num * -> Bool -fn num_is_odd(symbol: Symbol, var_store: &mut VarStore) -> Def { - let arg_var = var_store.fresh(); - let bool_var = var_store.fresh(); - let unbound_two_var = var_store.fresh(); - - let body = RunLowLevel { - op: LowLevel::Eq, - args: vec![ - ( - arg_var, - int::(var_store.fresh(), var_store.fresh(), 1, int_no_bound()), - ), - ( - arg_var, - RunLowLevel { - op: LowLevel::NumRemUnchecked, - args: vec![ - (arg_var, Var(Symbol::ARG_1)), - (arg_var, num(unbound_two_var, 2, num_no_bound())), - ], - ret_var: arg_var, - }, - ), - ], - ret_var: bool_var, - }; - - defn( - symbol, - vec![(arg_var, Symbol::ARG_1)], - var_store, - body, - bool_var, - ) -} - -/// Num.isEven : Num * -> Bool -fn num_is_even(symbol: Symbol, var_store: &mut VarStore) -> Def { - let arg_var = var_store.fresh(); - let arg_num_var = var_store.fresh(); - let bool_var = var_store.fresh(); - - let body = RunLowLevel { - op: LowLevel::Eq, - args: vec![ - (arg_var, num(arg_num_var, 0, num_no_bound())), - ( - arg_var, - RunLowLevel { - op: LowLevel::NumRemUnchecked, - args: vec![ - (arg_var, Var(Symbol::ARG_1)), - (arg_var, num(arg_num_var, 2, num_no_bound())), - ], - ret_var: arg_var, - }, - ), - ], - ret_var: bool_var, - }; - - defn( - symbol, - vec![(arg_var, Symbol::ARG_1)], - var_store, - body, - bool_var, - ) -} - -/// Num.toFrac : Num * -> Frac -fn num_to_frac(symbol: Symbol, var_store: &mut VarStore) -> Def { - let arg_var = var_store.fresh(); - let frac_var = var_store.fresh(); - - let body = RunLowLevel { - op: LowLevel::NumToFrac, - args: vec![(arg_var, Var(Symbol::ARG_1))], - ret_var: frac_var, - }; - - defn( - symbol, - vec![(arg_var, Symbol::ARG_1)], - var_store, - body, - frac_var, - ) -} - -/// Num.sqrt : Frac a -> Frac a -fn num_sqrt(symbol: Symbol, var_store: &mut VarStore) -> Def { - num_unaryop(symbol, var_store, LowLevel::NumSqrtUnchecked) -} - -/// Num.sqrtChecked : Frac a -> Result (Frac a) [SqrtOfNegative]* -fn num_sqrt_checked(symbol: Symbol, var_store: &mut VarStore) -> Def { - let bool_var = var_store.fresh(); - let frac_var = var_store.fresh(); - let unbound_zero_var = var_store.fresh(); - let precision_var = var_store.fresh(); - let ret_var = var_store.fresh(); - - let body = If { - branch_var: ret_var, - cond_var: bool_var, - branches: vec![( - no_region(RunLowLevel { - op: LowLevel::NumGte, - args: vec![ - (frac_var, Var(Symbol::ARG_1)), - ( - frac_var, - frac(unbound_zero_var, precision_var, 0.0, float_no_bound()), - ), - ], - ret_var: bool_var, - }), - no_region(tag( - "Ok", - vec![RunLowLevel { - op: LowLevel::NumSqrtUnchecked, - args: vec![(frac_var, Var(Symbol::ARG_1))], - ret_var: frac_var, - }], - var_store, - )), - )], - final_else: Box::new(no_region(tag( - "Err", - vec![tag("SqrtOfNegative", Vec::new(), var_store)], - var_store, - ))), - }; - - defn( - symbol, - vec![(frac_var, Symbol::ARG_1)], - var_store, - body, - ret_var, - ) -} - -/// Num.log : Frac a -> Frac a -fn num_log(symbol: Symbol, var_store: &mut VarStore) -> Def { - num_unaryop(symbol, var_store, LowLevel::NumLogUnchecked) -} - -/// Num.logChecked : Frac a -> Result (Frac a) [LogNeedsPositive]* -fn num_log_checked(symbol: Symbol, var_store: &mut VarStore) -> Def { - let bool_var = var_store.fresh(); - let frac_var = var_store.fresh(); - let unbound_zero_var = var_store.fresh(); - let precision_var = var_store.fresh(); - let ret_var = var_store.fresh(); - - let body = If { - branch_var: ret_var, - cond_var: bool_var, - branches: vec![( - no_region(RunLowLevel { - op: LowLevel::NumGt, - args: vec![ - (frac_var, Var(Symbol::ARG_1)), - ( - frac_var, - frac(unbound_zero_var, precision_var, 0.0, float_no_bound()), - ), - ], - ret_var: bool_var, - }), - no_region(tag( - "Ok", - vec![RunLowLevel { - op: LowLevel::NumLogUnchecked, - args: vec![(frac_var, Var(Symbol::ARG_1))], - ret_var: frac_var, - }], - var_store, - )), - )], - final_else: Box::new(no_region(tag( - "Err", - vec![tag("LogNeedsPositive", Vec::new(), var_store)], - var_store, - ))), - }; - - defn( - symbol, - vec![(frac_var, Symbol::ARG_1)], - var_store, - body, - ret_var, - ) -} - -/// Num.round : Frac -> Int -fn num_round(symbol: Symbol, var_store: &mut VarStore) -> Def { - let frac_var = var_store.fresh(); - let int_var = var_store.fresh(); - - let body = RunLowLevel { - op: LowLevel::NumRound, - args: vec![(frac_var, Var(Symbol::ARG_1))], - ret_var: int_var, - }; - - defn( - symbol, - vec![(frac_var, Symbol::ARG_1)], - var_store, - body, - int_var, - ) -} - -/// Num.pow : Frac, Frac -> Frac -fn num_pow(symbol: Symbol, var_store: &mut VarStore) -> Def { - let frac_var = var_store.fresh(); - - let body = RunLowLevel { - op: LowLevel::NumPow, - args: vec![ - (frac_var, Var(Symbol::ARG_1)), - (frac_var, Var(Symbol::ARG_2)), - ], - ret_var: frac_var, - }; - - defn( - symbol, - vec![(frac_var, Symbol::ARG_1), (frac_var, Symbol::ARG_2)], - var_store, - body, - frac_var, - ) -} - -/// Num.ceiling : Frac -> Int -fn num_ceiling(symbol: Symbol, var_store: &mut VarStore) -> Def { - let frac_var = var_store.fresh(); - let int_var = var_store.fresh(); - - let body = RunLowLevel { - op: LowLevel::NumCeiling, - args: vec![(frac_var, Var(Symbol::ARG_1))], - ret_var: int_var, - }; - - defn( - symbol, - vec![(frac_var, Symbol::ARG_1)], - var_store, - body, - int_var, - ) -} - -/// Num.powInt : Int a, Int a -> Int a -fn num_pow_int(symbol: Symbol, var_store: &mut VarStore) -> Def { - let int_var = var_store.fresh(); - - let body = RunLowLevel { - op: LowLevel::NumPowInt, - args: vec![(int_var, Var(Symbol::ARG_1)), (int_var, Var(Symbol::ARG_2))], - ret_var: int_var, - }; - - defn( - symbol, - vec![(int_var, Symbol::ARG_1), (int_var, Symbol::ARG_2)], - var_store, - body, - int_var, - ) -} - -/// Num.floor : Frac -> Int -fn num_floor(symbol: Symbol, var_store: &mut VarStore) -> Def { - let frac_var = var_store.fresh(); - let int_var = var_store.fresh(); - - let body = RunLowLevel { - op: LowLevel::NumFloor, - args: vec![(frac_var, Var(Symbol::ARG_1))], - ret_var: int_var, - }; - - defn( - symbol, - vec![(frac_var, Symbol::ARG_1)], - var_store, - body, - int_var, - ) -} - -/// Num.atan : Frac -> Frac -fn num_atan(symbol: Symbol, var_store: &mut VarStore) -> Def { - let arg_frac_var = var_store.fresh(); - let ret_frac_var = var_store.fresh(); - - let body = RunLowLevel { - op: LowLevel::NumAtan, - args: vec![(arg_frac_var, Var(Symbol::ARG_1))], - ret_var: ret_frac_var, - }; - - defn( - symbol, - vec![(arg_frac_var, Symbol::ARG_1)], - var_store, - body, - ret_frac_var, - ) -} - -/// Num.acos : Frac -> Frac -fn num_acos(symbol: Symbol, var_store: &mut VarStore) -> Def { - let arg_frac_var = var_store.fresh(); - let ret_frac_var = var_store.fresh(); - - let body = RunLowLevel { - op: LowLevel::NumAcos, - args: vec![(arg_frac_var, Var(Symbol::ARG_1))], - ret_var: ret_frac_var, - }; - - defn( - symbol, - vec![(arg_frac_var, Symbol::ARG_1)], - var_store, - body, - ret_frac_var, - ) -} - -/// Num.asin : Frac -> Frac -fn num_asin(symbol: Symbol, var_store: &mut VarStore) -> Def { - let arg_frac_var = var_store.fresh(); - let ret_frac_var = var_store.fresh(); - - let body = RunLowLevel { - op: LowLevel::NumAsin, - args: vec![(arg_frac_var, Var(Symbol::ARG_1))], - ret_var: ret_frac_var, - }; - - defn( - symbol, - vec![(arg_frac_var, Symbol::ARG_1)], - var_store, - body, - ret_frac_var, - ) -} - -/// Num.bytesToU16 : List U8, Nat -> Result U16 [OutOfBounds] -fn num_bytes_to_u16(symbol: Symbol, var_store: &mut VarStore) -> Def { - num_bytes_to(symbol, var_store, 1, LowLevel::NumBytesToU16) -} - -/// Num.bytesToU32 : List U8, Nat -> Result U32 [OutOfBounds] -fn num_bytes_to_u32(symbol: Symbol, var_store: &mut VarStore) -> Def { - num_bytes_to(symbol, var_store, 3, LowLevel::NumBytesToU32) -} - -/// Num.bitwiseAnd : Int a, Int a -> Int a -fn num_bitwise_and(symbol: Symbol, var_store: &mut VarStore) -> Def { - num_binop(symbol, var_store, LowLevel::NumBitwiseAnd) -} - -/// Num.bitwiseXor : Int a, Int a -> Int a -fn num_bitwise_xor(symbol: Symbol, var_store: &mut VarStore) -> Def { - num_binop(symbol, var_store, LowLevel::NumBitwiseXor) -} - -/// Num.bitwiseOr: Int a, Int a -> Int a -fn num_bitwise_or(symbol: Symbol, var_store: &mut VarStore) -> Def { - num_binop(symbol, var_store, LowLevel::NumBitwiseOr) -} - -/// Num.shiftLeftBy: Nat, Int a -> Int a -fn num_shift_left_by(symbol: Symbol, var_store: &mut VarStore) -> Def { - lowlevel_2(symbol, LowLevel::NumShiftLeftBy, var_store) -} - -/// Num.shiftRightBy: Nat, Int a -> Int a -fn num_shift_right_by(symbol: Symbol, var_store: &mut VarStore) -> Def { - lowlevel_2(symbol, LowLevel::NumShiftRightBy, var_store) -} - -/// Num.shiftRightZfBy: Nat, Int a -> Int a -fn num_shift_right_zf_by(symbol: Symbol, var_store: &mut VarStore) -> Def { - lowlevel_2(symbol, LowLevel::NumShiftRightZfBy, var_store) -} - -/// Num.intCast: Int a -> Int b -fn num_int_cast(symbol: Symbol, var_store: &mut VarStore) -> Def { - lowlevel_1(symbol, LowLevel::NumIntCast, var_store) -} - -/// List.isEmpty : List * -> Bool -fn list_is_empty(symbol: Symbol, var_store: &mut VarStore) -> Def { - let list_var = var_store.fresh(); - let bool_var = var_store.fresh(); - let len_var = Variable::NAT; - let unbound_zero_var = Variable::NATURAL; - - let body = RunLowLevel { - op: LowLevel::Eq, - args: vec![ - (len_var, num(unbound_zero_var, 0, num_no_bound())), - ( - len_var, - RunLowLevel { - op: LowLevel::ListLen, - args: vec![(list_var, Var(Symbol::ARG_1))], - ret_var: len_var, - }, - ), - ], - ret_var: bool_var, - }; - - defn( - symbol, - vec![(list_var, Symbol::ARG_1)], - var_store, - body, - bool_var, - ) -} - -/// List.reverse : List elem -> List elem -fn list_reverse(symbol: Symbol, var_store: &mut VarStore) -> Def { - let list_var = var_store.fresh(); - - let body = RunLowLevel { - op: LowLevel::ListReverse, - args: vec![(list_var, Var(Symbol::ARG_1))], - ret_var: list_var, - }; - - defn( - symbol, - vec![(list_var, Symbol::ARG_1)], - var_store, - body, - list_var, - ) -} - -/// Str.split : Str, Str -> List Str -fn str_split(symbol: Symbol, var_store: &mut VarStore) -> Def { - let str_var = var_store.fresh(); - let ret_list_var = var_store.fresh(); - - let body = RunLowLevel { - op: LowLevel::StrSplit, - args: vec![(str_var, Var(Symbol::ARG_1)), (str_var, Var(Symbol::ARG_2))], - ret_var: ret_list_var, - }; - - defn( - symbol, - vec![(str_var, Symbol::ARG_1), (str_var, Symbol::ARG_2)], - var_store, - body, - ret_list_var, - ) -} - -/// Str.trim : Str -> Str -fn str_trim(symbol: Symbol, var_store: &mut VarStore) -> Def { - lowlevel_1(symbol, LowLevel::StrTrim, var_store) -} - -/// Str.trimLeft : Str -> Str -fn str_trim_left(symbol: Symbol, var_store: &mut VarStore) -> Def { - lowlevel_1(symbol, LowLevel::StrTrimLeft, var_store) -} - -/// Str.trimRight : Str -> Str -fn str_trim_right(symbol: Symbol, var_store: &mut VarStore) -> Def { - lowlevel_1(symbol, LowLevel::StrTrimRight, var_store) -} - -/// Str.toNum : Str -> Result (Num *) [InvalidNumStr]* -fn str_to_num(symbol: Symbol, var_store: &mut VarStore) -> Def { - let bool_var = var_store.fresh(); - let str_var = var_store.fresh(); - let num_var = var_store.fresh(); - let ret_var = var_store.fresh(); - let record_var = var_store.fresh(); - - let errorcode_var = var_store.fresh(); - - // let arg_2 = RunLowLevel StrToNum arg_1 - // - // if arg_2.errorcode then - // Err InvalidNumStr - // else - // Ok arg_2.value - - let cont = If { - branch_var: ret_var, - cond_var: bool_var, - branches: vec![( - // if-condition - no_region(RunLowLevel { - op: LowLevel::NumGt, - args: vec![ - ( - errorcode_var, - // arg_3.b - Access { - record_var, - ext_var: var_store.fresh(), - field: "b_errorcode".into(), - field_var: errorcode_var, - loc_expr: Box::new(no_region(Var(Symbol::ARG_2))), - }, - ), - ( - errorcode_var, - int::( - errorcode_var, - Variable::UNSIGNED8, - 0, - IntBound::Exact(IntWidth::U8), - ), - ), - ], - ret_var: bool_var, - }), - // overflow! - no_region(tag( - "Err", - vec![tag("InvalidNumStr", Vec::new(), var_store)], - var_store, - )), - )], - final_else: Box::new( - // all is well - no_region( - // Ok arg_2.value - tag( - "Ok", - vec![ - // arg_3.a - Access { - record_var, - ext_var: var_store.fresh(), - field: "a_value".into(), - field_var: num_var, - loc_expr: Box::new(no_region(Var(Symbol::ARG_2))), - }, - ], - var_store, - ), - ), - ), - }; - - let def = crate::def::Def { - loc_pattern: no_region(Pattern::Identifier(Symbol::ARG_2)), - loc_expr: no_region(RunLowLevel { - op: LowLevel::StrToNum, - args: vec![(str_var, Var(Symbol::ARG_1))], - ret_var: record_var, - }), - expr_var: record_var, - pattern_vars: SendMap::default(), - annotation: None, - }; - - let body = LetNonRec(Box::new(def), Box::new(no_region(cont))); - - defn( - symbol, - vec![(str_var, Symbol::ARG_1)], - var_store, - body, - ret_var, - ) -} - -/// Str.repeat : Str, Nat -> Str -fn str_repeat(symbol: Symbol, var_store: &mut VarStore) -> Def { - let str_var = var_store.fresh(); - let nat_var = var_store.fresh(); - - let body = RunLowLevel { - op: LowLevel::StrRepeat, - args: vec![(str_var, Var(Symbol::ARG_1)), (nat_var, Var(Symbol::ARG_2))], - ret_var: str_var, - }; - - defn( - symbol, - vec![(str_var, Symbol::ARG_1), (nat_var, Symbol::ARG_2)], - var_store, - body, - str_var, - ) -} - -/// Str.concat : Str, Str -> Str -fn str_concat(symbol: Symbol, var_store: &mut VarStore) -> Def { - let str_var = var_store.fresh(); - - let body = RunLowLevel { - op: LowLevel::StrConcat, - args: vec![(str_var, Var(Symbol::ARG_1)), (str_var, Var(Symbol::ARG_2))], - ret_var: str_var, - }; - - defn( - symbol, - vec![(str_var, Symbol::ARG_1), (str_var, Symbol::ARG_2)], - var_store, - body, - str_var, - ) -} - -/// Str.joinWith : List Str, Str -> Str -fn str_join_with(symbol: Symbol, var_store: &mut VarStore) -> Def { - let list_str_var = var_store.fresh(); - let str_var = var_store.fresh(); - - let body = RunLowLevel { - op: LowLevel::StrJoinWith, - args: vec![ - (list_str_var, Var(Symbol::ARG_1)), - (str_var, Var(Symbol::ARG_2)), - ], - ret_var: str_var, - }; - - defn( - symbol, - vec![(list_str_var, Symbol::ARG_1), (str_var, Symbol::ARG_2)], - var_store, - body, - str_var, - ) -} - -/// Str.isEmpty : Str -> Bool -fn str_is_empty(symbol: Symbol, var_store: &mut VarStore) -> Def { - let str_var = var_store.fresh(); - let bool_var = var_store.fresh(); - - let body = RunLowLevel { - op: LowLevel::StrIsEmpty, - args: vec![(str_var, Var(Symbol::ARG_1))], - ret_var: bool_var, - }; - - defn( - symbol, - vec![(str_var, Symbol::ARG_1)], - var_store, - body, - bool_var, - ) -} - -/// Str.startsWith : Str, Str -> Bool -fn str_starts_with(symbol: Symbol, var_store: &mut VarStore) -> Def { - lowlevel_2(symbol, LowLevel::StrStartsWith, var_store) -} - -/// Str.startsWithCodePt : Str, U32 -> Bool -fn str_starts_with_code_point(symbol: Symbol, var_store: &mut VarStore) -> Def { - lowlevel_2(symbol, LowLevel::StrStartsWithCodePt, var_store) -} - -/// Str.endsWith : Str, Str -> Bool -fn str_ends_with(symbol: Symbol, var_store: &mut VarStore) -> Def { - let str_var = var_store.fresh(); - let bool_var = var_store.fresh(); - - let body = RunLowLevel { - op: LowLevel::StrEndsWith, - args: vec![(str_var, Var(Symbol::ARG_1)), (str_var, Var(Symbol::ARG_2))], - ret_var: bool_var, - }; - - defn( - symbol, - vec![(str_var, Symbol::ARG_1), (str_var, Symbol::ARG_2)], - var_store, - body, - bool_var, - ) -} - -/// Str.countGraphemes : Str -> Int -fn str_count_graphemes(symbol: Symbol, var_store: &mut VarStore) -> Def { - let str_var = var_store.fresh(); - let int_var = var_store.fresh(); - - let body = RunLowLevel { - op: LowLevel::StrCountGraphemes, - args: vec![(str_var, Var(Symbol::ARG_1))], - ret_var: int_var, - }; - - defn( - symbol, - vec![(str_var, Symbol::ARG_1)], - var_store, - body, - int_var, - ) -} - -/// Str.fromUtf8 : List U8 -> Result Str [BadUtf8 { byteIndex : Nat, problem : Utf8Problem } }]* -fn str_from_utf8(symbol: Symbol, var_store: &mut VarStore) -> Def { - let bytes_var = var_store.fresh(); - let bool_var = var_store.fresh(); - let record_var = var_store.fresh(); - let ret_var = var_store.fresh(); - - // let arg_2 = RunLowLevel FromUtf8 arg_1 - // - // arg_2 : - // { a : Bool -- isOk - // , b : String -- result_str - // , c : Nat -- problem_byte_index - // , d : I8 -- problem_code - // } - // - // if arg_2.a then - // # all is well - // Ok arg_2.str - // else - // # problem - // Err (BadUtf8 { byteIndex: arg_2.byteIndex, problem : arg_2.problem }) - - let def = crate::def::Def { - loc_pattern: no_region(Pattern::Identifier(Symbol::ARG_2)), - loc_expr: no_region(RunLowLevel { - op: LowLevel::StrFromUtf8, - args: vec![(bytes_var, Var(Symbol::ARG_1))], - ret_var: record_var, - }), - expr_var: record_var, - pattern_vars: SendMap::default(), - annotation: None, - }; - - let cont = If { - branch_var: ret_var, - cond_var: bool_var, - branches: vec![( - // if-condition - no_region( - // arg_2.c -> Bool - Access { - record_var, - ext_var: var_store.fresh(), - field: "c_isOk".into(), - field_var: var_store.fresh(), - loc_expr: Box::new(no_region(Var(Symbol::ARG_2))), - }, - ), - // all is good - no_region(tag( - "Ok", - // arg_2.a -> Str - vec![Access { - record_var, - ext_var: var_store.fresh(), - field: "b_str".into(), - field_var: var_store.fresh(), - loc_expr: Box::new(no_region(Var(Symbol::ARG_2))), - }], - var_store, - )), - )], - final_else: Box::new( - // bad!! - no_region(tag( - "Err", - vec![tag( - "BadUtf8", - vec![ - Access { - record_var, - ext_var: var_store.fresh(), - field: "d_problem".into(), - field_var: var_store.fresh(), - loc_expr: Box::new(no_region(Var(Symbol::ARG_2))), - }, - Access { - record_var, - ext_var: var_store.fresh(), - field: "a_byteIndex".into(), - field_var: var_store.fresh(), - loc_expr: Box::new(no_region(Var(Symbol::ARG_2))), - }, - ], - var_store, - )], - var_store, - )), - ), - }; - - let body = LetNonRec(Box::new(def), Box::new(no_region(cont))); - - defn( - symbol, - vec![(bytes_var, Symbol::ARG_1)], - var_store, - body, - ret_var, - ) -} -/// Str.fromUtf8Range : List U8, { start : Nat, count : Nat } -> Result Str [BadUtf8 { byteIndex : Nat, problem : Utf8Problem } }]* -fn str_from_utf8_range(symbol: Symbol, var_store: &mut VarStore) -> Def { - let bytes_var = var_store.fresh(); - let bool_var = var_store.fresh(); - let arg_record_var = var_store.fresh(); - let ll_record_var = var_store.fresh(); - let ret_var = var_store.fresh(); - - // let arg_3 = RunLowLevel FromUtf8Range arg_1 arg_2 - // - // arg_3 : - // { a : Bool -- isOk - // , b : String -- result_str - // , c : Nat -- problem_byte_index - // , d : I8 -- problem_code - // } - // - // if arg_3.a then - // Ok arg_3.str - // else - // Err (BadUtf8 { byteIndex: arg_3.byteIndex, problem : arg_3.problem }) - - let def = crate::def::Def { - loc_pattern: no_region(Pattern::Identifier(Symbol::ARG_3)), - loc_expr: no_region(RunLowLevel { - op: LowLevel::StrFromUtf8Range, - args: vec![ - (bytes_var, Var(Symbol::ARG_1)), - (arg_record_var, Var(Symbol::ARG_2)), - ], - ret_var: ll_record_var, - }), - expr_var: ll_record_var, - pattern_vars: SendMap::default(), - annotation: None, - }; - - let cont = If { - branch_var: ret_var, - cond_var: bool_var, - branches: vec![( - // if-condition - no_region( - // arg_2.c -> Bool - Access { - record_var: ll_record_var, - ext_var: var_store.fresh(), - field: "c_isOk".into(), - field_var: var_store.fresh(), - loc_expr: Box::new(no_region(Var(Symbol::ARG_3))), - }, - ), - // all is good - no_region(tag( - "Ok", - // arg_2.a -> Str - vec![Access { - record_var: ll_record_var, - ext_var: var_store.fresh(), - field: "b_str".into(), - field_var: var_store.fresh(), - loc_expr: Box::new(no_region(Var(Symbol::ARG_3))), - }], - var_store, - )), - )], - final_else: Box::new( - // bad!! - no_region(tag( - "Err", - vec![tag( - "BadUtf8", - vec![ - Access { - record_var: ll_record_var, - ext_var: var_store.fresh(), - field: "d_problem".into(), - field_var: var_store.fresh(), - loc_expr: Box::new(no_region(Var(Symbol::ARG_3))), - }, - Access { - record_var: ll_record_var, - ext_var: var_store.fresh(), - field: "a_byteIndex".into(), - field_var: var_store.fresh(), - loc_expr: Box::new(no_region(Var(Symbol::ARG_3))), - }, - ], - var_store, - )], - var_store, - )), - ), - }; - - let roc_result = LetNonRec(Box::new(def), Box::new(no_region(cont))); - - // Only do the business with the let if we're in bounds! - - let bounds_var = var_store.fresh(); - let bounds_bool = var_store.fresh(); - let add_var = var_store.fresh(); - - let body = If { - cond_var: bounds_bool, - branch_var: ret_var, - branches: vec![( - no_region(RunLowLevel { - op: LowLevel::NumLte, - args: vec![ - ( - bounds_var, - RunLowLevel { - op: LowLevel::NumAdd, - args: vec![ - ( - add_var, - Access { - record_var: arg_record_var, - ext_var: var_store.fresh(), - field: "start".into(), - field_var: var_store.fresh(), - loc_expr: Box::new(no_region(Var(Symbol::ARG_2))), - }, - ), - ( - add_var, - Access { - record_var: arg_record_var, - ext_var: var_store.fresh(), - field: "count".into(), - field_var: var_store.fresh(), - loc_expr: Box::new(no_region(Var(Symbol::ARG_2))), - }, - ), - ], - ret_var: add_var, - }, - ), - ( - bounds_var, - RunLowLevel { - op: LowLevel::ListLen, - args: vec![(bytes_var, Var(Symbol::ARG_1))], - ret_var: bounds_var, - }, - ), - ], - ret_var: bounds_bool, - }), - no_region(roc_result), - )], - final_else: Box::new( - // else-branch - no_region( - // Err - tag( - "Err", - vec![tag("OutOfBounds", Vec::new(), var_store)], - var_store, - ), - ), - ), - }; - - defn( - symbol, - vec![(bytes_var, Symbol::ARG_1), (arg_record_var, Symbol::ARG_2)], - var_store, - body, - ret_var, - ) -} - -/// Str.toUtf8 : Str -> List U8 -fn str_to_utf8(symbol: Symbol, var_store: &mut VarStore) -> Def { - lowlevel_1(symbol, LowLevel::StrToUtf8, var_store) -} - -/// List.concat : List elem, List elem -> List elem -fn list_concat(symbol: Symbol, var_store: &mut VarStore) -> Def { - let list_var = var_store.fresh(); - - let body = RunLowLevel { - op: LowLevel::ListConcat, - args: vec![ - (list_var, Var(Symbol::ARG_1)), - (list_var, Var(Symbol::ARG_2)), - ], - ret_var: list_var, - }; - - defn( - symbol, - vec![(list_var, Symbol::ARG_1), (list_var, Symbol::ARG_2)], - var_store, - body, - list_var, - ) -} - -/// List.repeat : elem, Nat -> List elem -fn list_repeat(symbol: Symbol, var_store: &mut VarStore) -> Def { - let elem_var = var_store.fresh(); - let len_var = var_store.fresh(); - let list_var = var_store.fresh(); - - let body = RunLowLevel { - op: LowLevel::ListRepeat, - args: vec![ - (elem_var, Var(Symbol::ARG_1)), - (len_var, Var(Symbol::ARG_2)), - ], - ret_var: list_var, - }; - - defn( - symbol, - vec![(elem_var, Symbol::ARG_1), (len_var, Symbol::ARG_2)], - var_store, - body, - list_var, - ) -} - -/// List.single : elem -> List elem -fn list_single(symbol: Symbol, var_store: &mut VarStore) -> Def { - let elem_var = var_store.fresh(); - let list_var = var_store.fresh(); - - let body = RunLowLevel { - op: LowLevel::ListSingle, - args: vec![(elem_var, Var(Symbol::ARG_1))], - ret_var: list_var, - }; - - defn( - symbol, - vec![(elem_var, Symbol::ARG_1)], - var_store, - body, - list_var, - ) -} - -/// List.len : List * -> Int -fn list_len(symbol: Symbol, var_store: &mut VarStore) -> Def { - let len_var = var_store.fresh(); - let list_var = var_store.fresh(); - - let body = RunLowLevel { - op: LowLevel::ListLen, - args: vec![(list_var, Var(Symbol::ARG_1))], - ret_var: len_var, - }; - - defn( - symbol, - vec![(list_var, Symbol::ARG_1)], - var_store, - body, - len_var, - ) -} - -/// List.get : List elem, Int -> Result elem [OutOfBounds]* -/// -/// List.get : -/// Attr (* | u) (List (Attr u a)), -/// Attr * Int -/// -> Attr * (Result (Attr u a) (Attr * [OutOfBounds]*)) -fn list_get(symbol: Symbol, var_store: &mut VarStore) -> Def { - let arg_list = Symbol::ARG_1; - let arg_index = Symbol::ARG_2; - let bool_var = var_store.fresh(); - let len_var = var_store.fresh(); - let list_var = var_store.fresh(); - let elem_var = var_store.fresh(); - let ret_var = var_store.fresh(); - - // Perform a bounds check. If it passes, run LowLevel::ListGetUnsafe - let body = If { - cond_var: bool_var, - branch_var: var_store.fresh(), - branches: vec![( - // if-condition - no_region( - // index < List.len list - RunLowLevel { - op: LowLevel::NumLt, - args: vec![ - (len_var, Var(arg_index)), - ( - len_var, - RunLowLevel { - op: LowLevel::ListLen, - args: vec![(list_var, Var(arg_list))], - ret_var: len_var, - }, - ), - ], - ret_var: bool_var, - }, - ), - // then-branch - no_region( - // Ok - tag( - "Ok", - vec![ - // List#getUnsafe list index - RunLowLevel { - op: LowLevel::ListGetUnsafe, - args: vec![(list_var, Var(arg_list)), (len_var, Var(arg_index))], - ret_var: elem_var, - }, - ], - var_store, - ), - ), - )], - final_else: Box::new( - // else-branch - no_region( - // Err - tag( - "Err", - vec![tag("OutOfBounds", Vec::new(), var_store)], - var_store, - ), - ), - ), - }; - - defn( - symbol, - vec![(list_var, Symbol::ARG_1), (len_var, Symbol::ARG_2)], - var_store, - body, - ret_var, - ) -} - -/// List.replace : List elem, Nat, elem -> { list: List elem, value: elem } -fn list_replace(symbol: Symbol, var_store: &mut VarStore) -> Def { - let arg_list = Symbol::ARG_1; - let arg_index = Symbol::ARG_2; - let arg_elem = Symbol::ARG_3; - let bool_var = var_store.fresh(); - let len_var = var_store.fresh(); - let elem_var = var_store.fresh(); - let list_arg_var = var_store.fresh(); - let ret_record_var = var_store.fresh(); - let ret_result_var = var_store.fresh(); - - let list_field = Field { - var: list_arg_var, - region: Region::zero(), - loc_expr: Box::new(Loc::at_zero(Expr::Var(arg_list))), - }; - - let value_field = Field { - var: elem_var, - region: Region::zero(), - loc_expr: Box::new(Loc::at_zero(Expr::Var(arg_elem))), - }; - - // Perform a bounds check. If it passes, run LowLevel::ListReplaceUnsafe. - // Otherwise, return the list unmodified. - let body = If { - cond_var: bool_var, - branch_var: ret_result_var, - branches: vec![( - // if-condition - no_region( - // index < List.len list - RunLowLevel { - op: LowLevel::NumLt, - args: vec![ - (len_var, Var(arg_index)), - ( - len_var, - RunLowLevel { - op: LowLevel::ListLen, - args: vec![(list_arg_var, Var(arg_list))], - ret_var: len_var, - }, - ), - ], - ret_var: bool_var, - }, - ), - // then-branch - no_region( - // List.replaceUnsafe list index elem - RunLowLevel { - op: LowLevel::ListReplaceUnsafe, - args: vec![ - (list_arg_var, Var(arg_list)), - (len_var, Var(arg_index)), - (elem_var, Var(arg_elem)), - ], - ret_var: ret_record_var, - }, - ), - )], - final_else: Box::new( - // else-branch - no_region(record( - vec![("list".into(), list_field), ("value".into(), value_field)], - var_store, - )), - ), - }; - - defn( - symbol, - vec![ - (list_arg_var, Symbol::ARG_1), - (len_var, Symbol::ARG_2), - (elem_var, Symbol::ARG_3), - ], - var_store, - body, - ret_result_var, - ) -} - -/// List.set : List elem, Nat, elem -> List elem -/// -/// List.set : -/// Attr (w | u | v) (List (Attr u a)), -/// Attr * Int, -/// Attr (u | v) a -/// -> Attr * (List (Attr u a)) -fn list_set(symbol: Symbol, var_store: &mut VarStore) -> Def { - let arg_list = Symbol::ARG_1; - let arg_index = Symbol::ARG_2; - let arg_elem = Symbol::ARG_3; - let bool_var = var_store.fresh(); - let len_var = var_store.fresh(); - let elem_var = var_store.fresh(); - let replace_record_var = var_store.fresh(); - let list_arg_var = var_store.fresh(); // Uniqueness type Attr differs between - let list_ret_var = var_store.fresh(); // the arg list and the returned list - - let replace_function = ( - var_store.fresh(), - Loc::at_zero(Expr::Var(Symbol::LIST_REPLACE)), - var_store.fresh(), - replace_record_var, - ); - - let replace_call = Expr::Call( - Box::new(replace_function), - vec![ - (list_arg_var, Loc::at_zero(Var(arg_list))), - (len_var, Loc::at_zero(Var(arg_index))), - (elem_var, Loc::at_zero(Var(arg_elem))), - ], - CalledVia::Space, - ); - - // Perform a bounds check. If it passes, run LowLevel::ListSet. - // Otherwise, return the list unmodified. - let body = If { - cond_var: bool_var, - branch_var: list_ret_var, - branches: vec![( - // if-condition - no_region( - // index < List.len list - RunLowLevel { - op: LowLevel::NumLt, - args: vec![ - (len_var, Var(arg_index)), - ( - len_var, - RunLowLevel { - op: LowLevel::ListLen, - args: vec![(list_arg_var, Var(arg_list))], - ret_var: len_var, - }, - ), - ], - ret_var: bool_var, - }, - ), - // then-branch - no_region(Access { - record_var: replace_record_var, - ext_var: var_store.fresh(), - field_var: list_ret_var, - loc_expr: Box::new(no_region( - // List.replaceUnsafe list index elem - replace_call, - )), - field: "list".into(), - }), - )], - final_else: Box::new( - // else-branch - no_region(Var(arg_list)), - ), - }; - - defn( - symbol, - vec![ - (list_arg_var, Symbol::ARG_1), - (len_var, Symbol::ARG_2), - (elem_var, Symbol::ARG_3), - ], - var_store, - body, - list_ret_var, - ) -} - -/// List.swap : List elem, Nat, Nat -> List elem -fn list_swap(symbol: Symbol, var_store: &mut VarStore) -> Def { - let list_var = var_store.fresh(); - let index1_var = var_store.fresh(); - let index2_var = var_store.fresh(); - - let body = RunLowLevel { - op: LowLevel::ListSwap, - args: vec![ - (list_var, Var(Symbol::ARG_1)), - (index1_var, Var(Symbol::ARG_2)), - (index2_var, Var(Symbol::ARG_3)), - ], - ret_var: list_var, - }; - - defn( - symbol, - vec![ - (list_var, Symbol::ARG_1), - (index1_var, Symbol::ARG_2), - (index2_var, Symbol::ARG_3), - ], - var_store, - body, - list_var, - ) -} - -/// List.takeFirst : List elem, Nat -> List elem -fn list_take_first(symbol: Symbol, var_store: &mut VarStore) -> Def { - let list_var = var_store.fresh(); - let len_var = var_store.fresh(); - let zero = int::( - len_var, - Variable::NATURAL, - 0, - IntBound::Exact(IntWidth::Nat), - ); - - let body = RunLowLevel { - op: LowLevel::ListSublist, - args: vec![ - (list_var, Var(Symbol::ARG_1)), - (len_var, zero), - (len_var, Var(Symbol::ARG_2)), - ], - ret_var: list_var, - }; - - defn( - symbol, - vec![(list_var, Symbol::ARG_1), (len_var, Symbol::ARG_2)], - var_store, - body, - list_var, - ) -} - -/// List.takeLast : List elem, Nat -> List elem -fn list_take_last(symbol: Symbol, var_store: &mut VarStore) -> Def { - let list_var = var_store.fresh(); - let len_var = var_store.fresh(); - - let zero = int::( - len_var, - Variable::NATURAL, - 0, - IntBound::Exact(IntWidth::Nat), - ); - let bool_var = var_store.fresh(); - - let get_list_len = RunLowLevel { - op: LowLevel::ListLen, - args: vec![(list_var, Var(Symbol::ARG_1))], - ret_var: len_var, - }; - - let get_sub = RunLowLevel { - op: LowLevel::NumSubWrap, - args: vec![ - (len_var, get_list_len.clone()), - (len_var, Var(Symbol::ARG_2)), - ], - ret_var: len_var, - }; - - let get_start = If { - cond_var: bool_var, - branch_var: len_var, - branches: vec![( - no_region(RunLowLevel { - op: LowLevel::NumGt, - args: vec![(len_var, get_list_len), (len_var, Var(Symbol::ARG_2))], - ret_var: bool_var, - }), - no_region(get_sub), - )], - final_else: Box::new(no_region(zero)), - }; - - let body = RunLowLevel { - op: LowLevel::ListSublist, - args: vec![ - (list_var, Var(Symbol::ARG_1)), - (len_var, get_start), - (len_var, Var(Symbol::ARG_2)), - ], - ret_var: list_var, - }; - - defn( - symbol, - vec![(list_var, Symbol::ARG_1), (len_var, Symbol::ARG_2)], - var_store, - body, - list_var, - ) -} - -/// List.sublist : List elem, { start : Nat, len : Nat } -> List elem -fn list_sublist(symbol: Symbol, var_store: &mut VarStore) -> Def { - let list_var = var_store.fresh(); - let rec_var = var_store.fresh(); - - let sym_list = Symbol::ARG_1; - let sym_rec = Symbol::ARG_2; - - let start_var = var_store.fresh(); - let len_var = var_store.fresh(); - - let get_start = Access { - record_var: rec_var, - ext_var: var_store.fresh(), - field_var: var_store.fresh(), - loc_expr: Box::new(no_region(Var(sym_rec))), - field: "start".into(), - }; - - let get_len = Access { - record_var: rec_var, - ext_var: var_store.fresh(), - field_var: var_store.fresh(), - loc_expr: Box::new(no_region(Var(sym_rec))), - field: "len".into(), - }; - - let body = RunLowLevel { - op: LowLevel::ListSublist, - args: vec![ - (list_var, Var(sym_list)), - (start_var, get_start), - (len_var, get_len), - ], - ret_var: list_var, - }; - - defn( - symbol, - vec![(list_var, sym_list), (rec_var, sym_rec)], - var_store, - body, - list_var, - ) -} - -/// List.intersperse : List elem, elem -> List elem -fn list_intersperse(symbol: Symbol, var_store: &mut VarStore) -> Def { - let list_var = var_store.fresh(); - let sep_var = var_store.fresh(); - - let list_sym = Symbol::ARG_1; - let sep_sym = Symbol::ARG_2; - - let clos_var = var_store.fresh(); - let clos_acc_var = var_store.fresh(); - - let clos_sym = Symbol::LIST_INTERSPERSE_CLOS; - let clos_acc_sym = Symbol::ARG_3; - let clos_elem_sym = Symbol::ARG_4; - - let int_var = var_store.fresh(); - let zero = int::( - int_var, - Variable::NATURAL, - 0, - IntBound::Exact(IntWidth::Nat), - ); - - // \acc, elem -> acc |> List.append sep |> List.append elem - let clos = Closure(ClosureData { - function_type: clos_var, - closure_type: var_store.fresh(), - return_type: clos_acc_var, - name: clos_sym, - recursive: Recursive::NotRecursive, - captured_symbols: vec![(sep_sym, sep_var)], - arguments: vec![ - ( - clos_acc_var, - AnnotatedMark::new(var_store), - no_region(Pattern::Identifier(clos_acc_sym)), - ), - ( - sep_var, - AnnotatedMark::new(var_store), - no_region(Pattern::Identifier(clos_elem_sym)), - ), - ], - loc_body: { - let append_sep = RunLowLevel { - op: LowLevel::ListAppend, - args: vec![(clos_acc_var, Var(clos_acc_sym)), (sep_var, Var(sep_sym))], - ret_var: clos_acc_var, - }; - - Box::new(no_region(RunLowLevel { - op: LowLevel::ListAppend, - args: vec![(clos_acc_var, append_sep), (sep_var, Var(clos_elem_sym))], - ret_var: clos_acc_var, - })) - }, - }); - - // List.walk [] l (\acc, elem -> acc |> List.append sep |> List.append elem) - let acc = RunLowLevel { - op: LowLevel::ListWalk, - args: vec![ - (list_var, Var(list_sym)), - ( - clos_acc_var, - List { - elem_var: sep_var, - loc_elems: vec![], - }, - ), - (clos_var, clos), - ], - ret_var: clos_acc_var, - }; - - let body = RunLowLevel { - op: LowLevel::ListDropAt, - args: vec![(clos_acc_var, acc), (int_var, zero)], - ret_var: clos_acc_var, - }; - - defn( - symbol, - vec![(list_var, list_sym), (sep_var, sep_sym)], - var_store, - body, - clos_acc_var, - ) -} - -/// List.split : List elem, Nat -> { before: List elem, others: List elem } -fn list_split(symbol: Symbol, var_store: &mut VarStore) -> Def { - let list_var = var_store.fresh(); - let index_var = var_store.fresh(); - - let list_sym = Symbol::ARG_1; - let index_sym = Symbol::ARG_2; - - let clos_sym = Symbol::LIST_SPLIT_CLOS; - let clos_start_sym = Symbol::ARG_3; - let clos_len_sym = Symbol::ARG_4; - - let clos_fun_var = var_store.fresh(); - let clos_start_var = var_store.fresh(); - let clos_len_var = var_store.fresh(); - let clos_ret_var = var_store.fresh(); - - let ret_var = var_store.fresh(); - let zero = int::( - index_var, - Variable::NATURAL, - 0, - IntBound::Exact(IntWidth::Nat), - ); - - let clos = Closure(ClosureData { - function_type: clos_fun_var, - closure_type: var_store.fresh(), - return_type: clos_ret_var, - name: clos_sym, - recursive: Recursive::NotRecursive, - captured_symbols: vec![(list_sym, clos_ret_var)], - arguments: vec![ - ( - clos_start_var, - AnnotatedMark::new(var_store), - no_region(Pattern::Identifier(clos_start_sym)), - ), - ( - clos_len_var, - AnnotatedMark::new(var_store), - no_region(Pattern::Identifier(clos_len_sym)), - ), - ], - loc_body: { - Box::new(no_region(RunLowLevel { - op: LowLevel::ListSublist, - args: vec![ - (clos_ret_var, Var(list_sym)), - (clos_start_var, Var(clos_start_sym)), - (clos_len_var, Var(clos_len_sym)), - ], - ret_var: clos_ret_var, - })) - }, - }); - - let fun = Box::new(( - clos_fun_var, - no_region(clos), - var_store.fresh(), - clos_ret_var, - )); - - let get_before = Call( - fun.clone(), - vec![ - (index_var, no_region(zero)), - (index_var, no_region(Var(index_sym))), - ], - CalledVia::Space, - ); - - let get_list_len = RunLowLevel { - op: LowLevel::ListLen, - args: vec![(list_var, Var(list_sym))], - ret_var: index_var, - }; - - let get_others_len = RunLowLevel { - op: LowLevel::NumSubWrap, - args: vec![(index_var, get_list_len), (index_var, Var(index_sym))], - ret_var: index_var, - }; - - let get_others = Call( - fun, - vec![ - (index_var, no_region(Var(index_sym))), - (index_var, no_region(get_others_len)), - ], - CalledVia::Space, - ); - - let before = Field { - var: clos_ret_var, - region: Region::zero(), - loc_expr: Box::new(no_region(get_before)), - }; - let others = Field { - var: clos_ret_var, - region: Region::zero(), - loc_expr: Box::new(no_region(get_others)), - }; - - let body = record( - vec![("before".into(), before), ("others".into(), others)], - var_store, - ); - - defn( - symbol, - vec![(list_var, list_sym), (index_var, index_sym)], - var_store, - body, - ret_var, - ) -} - -/// List.drop : List elem, Nat -> List elem -fn list_drop(symbol: Symbol, var_store: &mut VarStore) -> Def { - let list_var = var_store.fresh(); - let len_var = var_store.fresh(); - - let get_list_len = RunLowLevel { - op: LowLevel::ListLen, - args: vec![(list_var, Var(Symbol::ARG_1))], - ret_var: len_var, - }; - - let get_len = RunLowLevel { - op: LowLevel::NumSubWrap, - args: vec![(len_var, get_list_len), (len_var, Var(Symbol::ARG_2))], - ret_var: len_var, - }; - - let body = RunLowLevel { - op: LowLevel::ListSublist, - args: vec![ - (list_var, Var(Symbol::ARG_1)), - (len_var, Var(Symbol::ARG_2)), - (len_var, get_len), - ], - ret_var: list_var, - }; - - defn( - symbol, - vec![(list_var, Symbol::ARG_1), (len_var, Symbol::ARG_2)], - var_store, - body, - list_var, - ) -} - -/// List.dropAt : List elem, Nat -> List elem -fn list_drop_at(symbol: Symbol, var_store: &mut VarStore) -> Def { - let list_var = var_store.fresh(); - let index_var = var_store.fresh(); - - let body = RunLowLevel { - op: LowLevel::ListDropAt, - args: vec![ - (list_var, Var(Symbol::ARG_1)), - (index_var, Var(Symbol::ARG_2)), - ], - ret_var: list_var, - }; - - defn( - symbol, - vec![(list_var, Symbol::ARG_1), (index_var, Symbol::ARG_2)], - var_store, - body, - list_var, - ) -} - -/// List.dropFirst : List elem -> Result { first: elem, others : List elem } [ListWasEmpty]* -fn list_drop_first(symbol: Symbol, var_store: &mut VarStore) -> Def { - let list_var = var_store.fresh(); - let index_var = var_store.fresh(); - let num_var = Variable::NAT; - let num_precision_var = Variable::NATURAL; - - let body = RunLowLevel { - op: LowLevel::ListDropAt, - args: vec![ - (list_var, Var(Symbol::ARG_1)), - ( - index_var, - int::(num_var, num_precision_var, 0, int_no_bound()), - ), - ], - ret_var: list_var, - }; - - defn( - symbol, - vec![(list_var, Symbol::ARG_1)], - var_store, - body, - list_var, - ) -} - -/// List.dropIf : List elem, (elem -> Bool) -> List elem -fn list_drop_if(symbol: Symbol, var_store: &mut VarStore) -> Def { - let sym_list = Symbol::ARG_1; - let sym_predicate = Symbol::ARG_2; - let t_list = var_store.fresh(); - let t_predicate = var_store.fresh(); - let t_keep_predicate = var_store.fresh(); - let t_elem = var_store.fresh(); - - // Defer to keepIf for implementation - // List.dropIf l p = List.keepIf l (\e -> Bool.not (p e)) - - let keep_predicate = Closure(ClosureData { - function_type: t_keep_predicate, - closure_type: var_store.fresh(), - return_type: Variable::BOOL, - name: Symbol::LIST_DROP_IF_PREDICATE, - recursive: Recursive::NotRecursive, - captured_symbols: vec![(sym_predicate, t_predicate)], - arguments: vec![( - t_elem, - AnnotatedMark::new(var_store), - no_region(Pattern::Identifier(Symbol::ARG_3)), - )], - loc_body: { - let should_drop = Call( - Box::new(( - t_predicate, - no_region(Var(sym_predicate)), - var_store.fresh(), - Variable::BOOL, - )), - vec![(t_elem, no_region(Var(Symbol::ARG_3)))], - CalledVia::Space, - ); - Box::new(no_region(RunLowLevel { - op: LowLevel::Not, - args: vec![(Variable::BOOL, should_drop)], - ret_var: Variable::BOOL, - })) - }, - }); - - let body = RunLowLevel { - op: LowLevel::ListKeepIf, - args: vec![(t_list, Var(sym_list)), (t_keep_predicate, keep_predicate)], - ret_var: t_list, - }; - - defn( - symbol, - vec![(t_list, sym_list), (t_predicate, sym_predicate)], - var_store, - body, - t_list, - ) -} - -/// List.dropLast: List elem -> List elem -fn list_drop_last(symbol: Symbol, var_store: &mut VarStore) -> Def { - let list_var = var_store.fresh(); - let index_var = var_store.fresh(); - let arg_var = var_store.fresh(); - let len_var = Variable::NAT; - let num_var = len_var; - let num_precision_var = Variable::NATURAL; - - let body = RunLowLevel { - op: LowLevel::ListDropAt, - args: vec![ - (list_var, Var(Symbol::ARG_1)), - ( - index_var, - // Num.sub (List.len list) 1 - RunLowLevel { - op: LowLevel::NumSubWrap, - args: vec![ - ( - arg_var, - // List.len list - RunLowLevel { - op: LowLevel::ListLen, - args: vec![(list_var, Var(Symbol::ARG_1))], - ret_var: len_var, - }, - ), - ( - arg_var, - int::(num_var, num_precision_var, 1, int_no_bound()), - ), - ], - ret_var: len_var, - }, - ), - ], - ret_var: list_var, - }; - - defn( - symbol, - vec![(list_var, Symbol::ARG_1)], - var_store, - body, - list_var, - ) -} -/// List.append : List elem, elem -> List elem -fn list_append(symbol: Symbol, var_store: &mut VarStore) -> Def { - let list_var = var_store.fresh(); - let elem_var = var_store.fresh(); - - let body = RunLowLevel { - op: LowLevel::ListAppend, - args: vec![ - (list_var, Var(Symbol::ARG_1)), - (elem_var, Var(Symbol::ARG_2)), - ], - ret_var: list_var, - }; - - defn( - symbol, - vec![(list_var, Symbol::ARG_1), (elem_var, Symbol::ARG_2)], - var_store, - body, - list_var, - ) -} - -/// List.prepend : List elem, elem -> List elem -fn list_prepend(symbol: Symbol, var_store: &mut VarStore) -> Def { - let list_var = var_store.fresh(); - let elem_var = var_store.fresh(); - - let body = RunLowLevel { - op: LowLevel::ListPrepend, - args: vec![ - (list_var, Var(Symbol::ARG_1)), - (elem_var, Var(Symbol::ARG_2)), - ], - ret_var: list_var, - }; - - defn( - symbol, - vec![(list_var, Symbol::ARG_1), (elem_var, Symbol::ARG_2)], - var_store, - body, - list_var, - ) -} - -/// List.join : List (List elem) -> List elem -fn list_join(symbol: Symbol, var_store: &mut VarStore) -> Def { - let list_var = var_store.fresh(); - let list_of_list_var = var_store.fresh(); - - let body = RunLowLevel { - op: LowLevel::ListJoin, - args: vec![(list_of_list_var, Var(Symbol::ARG_1))], - ret_var: list_var, - }; - - defn( - symbol, - vec![(list_of_list_var, Symbol::ARG_1)], - var_store, - body, - list_var, - ) -} - -/// List.walk : List elem, state, (state, elem -> state) -> state -fn list_walk(symbol: Symbol, var_store: &mut VarStore) -> Def { - lowlevel_3(symbol, LowLevel::ListWalk, var_store) -} - -/// List.walkBackwards : List elem, state, (state, elem -> state) -> state -fn list_walk_backwards(symbol: Symbol, var_store: &mut VarStore) -> Def { - lowlevel_3(symbol, LowLevel::ListWalkBackwards, var_store) -} - -/// List.walkUntil : List elem, state, (state, elem -> [Continue state, Stop state]) -> state -fn list_walk_until(symbol: Symbol, var_store: &mut VarStore) -> Def { - lowlevel_3(symbol, LowLevel::ListWalkUntil, var_store) -} - -/// List.joinMap : List before, (before -> List after) -> List after -fn list_join_map(symbol: Symbol, var_store: &mut VarStore) -> Def { - let before = var_store.fresh(); - let list_before = var_store.fresh(); - let after = var_store.fresh(); - let list_after = var_store.fresh(); - let before2list_after = var_store.fresh(); - let t_concat_clos = var_store.fresh(); - let mapper_lambda_set = var_store.fresh(); - - // \state, elem -> List.concat state (mapper elem) - let concat_clos = Closure(ClosureData { - function_type: t_concat_clos, - closure_type: var_store.fresh(), - return_type: list_after, - name: Symbol::LIST_JOIN_MAP_CONCAT, - recursive: Recursive::NotRecursive, - captured_symbols: vec![(Symbol::ARG_2, before2list_after)], - arguments: vec![ - ( - list_after, - AnnotatedMark::new(var_store), - no_region(Pattern::Identifier(Symbol::ARG_3)), - ), - ( - before, - AnnotatedMark::new(var_store), - no_region(Pattern::Identifier(Symbol::ARG_4)), - ), - ], - loc_body: { - let mapper = Box::new(( - before2list_after, - no_region(Var(Symbol::ARG_2)), - mapper_lambda_set, - list_after, // return type - )); - // (mapper elem) - let mapper_elem = Call( - mapper, - vec![(before, no_region(Var(Symbol::ARG_4)))], - CalledVia::Space, - ); - Box::new(no_region(RunLowLevel { - op: LowLevel::ListConcat, - args: vec![(list_after, Var(Symbol::ARG_3)), (list_after, mapper_elem)], - ret_var: list_after, - })) - }, - }); - - // List.joinMap = \input_list, mapper -> - // List.walk [] input_list (\state, elem -> List.concat state (mapper elem)) - let body = RunLowLevel { - op: LowLevel::ListWalk, - args: vec![ - // input_list : List before - (list_before, Var(Symbol::ARG_1)), - // [] : List after - ( - list_after, - List { - elem_var: after, - loc_elems: vec![], - }, - ), - // \state, elem -> List.concat state (mapper elem) - (t_concat_clos, concat_clos), - ], - ret_var: list_after, - }; - - defn( - symbol, - vec![ - (list_before, Symbol::ARG_1), - (before2list_after, Symbol::ARG_2), - ], - var_store, - body, - list_after, - ) -} - -// min : List (Num a) -> Result (Num a) [ListWasEmpty]* -fn list_min(symbol: Symbol, var_store: &mut VarStore) -> Def { - let arg_var = var_store.fresh(); - let bool_var = var_store.fresh(); - let list_var = var_store.fresh(); - let len_var = Variable::NAT; - let num_var = len_var; - let num_precision_var = Variable::NATURAL; - let list_elem_var = var_store.fresh(); - let ret_var = var_store.fresh(); - let closure_var = var_store.fresh(); - - // Perform a bounds check. If it passes, delegate to List.getUnsafe. - let body = If { - cond_var: bool_var, - branch_var: var_store.fresh(), - branches: vec![( - // if-condition - no_region( - // List.len list != 0 - RunLowLevel { - op: LowLevel::NotEq, - args: vec![ - ( - len_var, - int::(num_var, num_precision_var, 0, int_no_bound()), - ), - ( - len_var, - RunLowLevel { - op: LowLevel::ListLen, - args: vec![(list_var, Var(Symbol::ARG_1))], - ret_var: len_var, - }, - ), - ], - ret_var: bool_var, - }, - ), - // list was not empty - no_region( - // Ok ( List.walk list (List.getUnsafe list 0) \acc,elem -> if elem < acc then elem else acc ) - tag( - "Ok", - vec![ - // List.walk list (List.getUnsafe list 0) \acc,elem -> if elem < acc then elem else acc - RunLowLevel { - op: LowLevel::ListWalk, - args: vec![ - (list_var, Var(Symbol::ARG_1)), - // (List.getUnsafe list 0) - ( - list_elem_var, - RunLowLevel { - op: LowLevel::ListGetUnsafe, - args: vec![ - (list_var, Var(Symbol::ARG_1)), - ( - arg_var, - int::( - num_var, - num_precision_var, - 0, - int_no_bound(), - ), - ), - ], - ret_var: list_elem_var, - }, - ), - // \acc,elem -> if elem < acc then elem else acc - (closure_var, list_min_lt(list_elem_var, var_store)), - ], - ret_var: list_elem_var, - }, - ], - var_store, - ), - ), - )], - final_else: Box::new( - // list was empty - no_region( - // Err ListWasEmpty - tag( - "Err", - vec![tag("ListWasEmpty", Vec::new(), var_store)], - var_store, - ), - ), - ), - }; - - defn( - symbol, - vec![(list_var, Symbol::ARG_1)], - var_store, - body, - ret_var, - ) -} - -// \acc,elem -> if elem < acc then elem else acc -fn list_min_lt(list_elem_var: Variable, var_store: &mut VarStore) -> Expr { - let bool_var = var_store.fresh(); - // if elem < acc then elem else acc - let body = If { - cond_var: bool_var, - branch_var: list_elem_var, - branches: vec![( - // if-condition - no_region( - // elem < acc - RunLowLevel { - op: LowLevel::NumLt, - args: vec![ - (list_elem_var, Var(Symbol::ARG_4)), - (list_elem_var, Var(Symbol::ARG_3)), - ], - ret_var: bool_var, - }, - ), - // return elem - no_region(Var(Symbol::ARG_4)), - )], - // return acc - final_else: Box::new(no_region(Var(Symbol::ARG_3))), - }; - - defn_help( - Symbol::LIST_MIN_LT, - vec![ - (list_elem_var, Symbol::ARG_3), - (list_elem_var, Symbol::ARG_4), - ], - var_store, - body, - list_elem_var, - ) -} - -// max : List (Num a) -> Result (Num a) [ListWasEmpty]* -fn list_max(symbol: Symbol, var_store: &mut VarStore) -> Def { - let arg_var = var_store.fresh(); - let bool_var = var_store.fresh(); - let list_var = var_store.fresh(); - let len_var = Variable::NAT; - let num_var = len_var; - let num_precision_var = Variable::NATURAL; - let list_elem_var = var_store.fresh(); - let ret_var = var_store.fresh(); - let closure_var = var_store.fresh(); - - // Perform a bounds check. If it passes, delegate to List.getUnsafe. - let body = If { - cond_var: bool_var, - branch_var: var_store.fresh(), - branches: vec![( - // if-condition - no_region( - // List.len list != 0 - RunLowLevel { - op: LowLevel::NotEq, - args: vec![ - ( - len_var, - int::(num_var, num_precision_var, 0, int_no_bound()), - ), - ( - len_var, - RunLowLevel { - op: LowLevel::ListLen, - args: vec![(list_var, Var(Symbol::ARG_1))], - ret_var: len_var, - }, - ), - ], - ret_var: bool_var, - }, - ), - // list was not empty - no_region( - // Ok ( List.walk list (List.getUnsafe list 0) \acc,elem -> if elem < acc then elem else acc ) - tag( - "Ok", - vec![ - // List.walk list (List.getUnsafe list 0) \acc,elem -> if elem < acc then elem else acc - RunLowLevel { - op: LowLevel::ListWalk, - args: vec![ - (list_var, Var(Symbol::ARG_1)), - // (List.getUnsafe list 0) - ( - list_elem_var, - RunLowLevel { - op: LowLevel::ListGetUnsafe, - args: vec![ - (list_var, Var(Symbol::ARG_1)), - ( - arg_var, - int::( - num_var, - num_precision_var, - 0, - int_no_bound(), - ), - ), - ], - ret_var: list_elem_var, - }, - ), - // \acc,elem -> if elem < acc then elem else acc - (closure_var, list_max_gt(list_elem_var, var_store)), - ], - ret_var: list_elem_var, - }, - ], - var_store, - ), - ), - )], - final_else: Box::new( - // list was empty - no_region( - // Err ListWasEmpty - tag( - "Err", - vec![tag("ListWasEmpty", Vec::new(), var_store)], - var_store, - ), - ), - ), - }; - - defn( - symbol, - vec![(list_var, Symbol::ARG_1)], - var_store, - body, - ret_var, - ) -} - -// \acc,elem -> if elem > acc then elem else acc -fn list_max_gt(list_elem_var: Variable, var_store: &mut VarStore) -> Expr { - let bool_var = var_store.fresh(); - // if elem > acc then elem else acc - let body = If { - cond_var: bool_var, - branch_var: list_elem_var, - branches: vec![( - // if-condition - no_region( - // elem > acc - RunLowLevel { - op: LowLevel::NumGt, - args: vec![ - (list_elem_var, Var(Symbol::ARG_4)), - (list_elem_var, Var(Symbol::ARG_3)), - ], - ret_var: bool_var, - }, - ), - // return elem - no_region(Var(Symbol::ARG_4)), - )], - // return acc - final_else: Box::new(no_region(Var(Symbol::ARG_3))), - }; - - defn_help( - Symbol::LIST_MAX_GT, - vec![ - (list_elem_var, Symbol::ARG_3), - (list_elem_var, Symbol::ARG_4), - ], - var_store, - body, - list_elem_var, - ) -} - -/// List.sum : List (Num a) -> Num a -fn list_sum(symbol: Symbol, var_store: &mut VarStore) -> Def { - let num_var = var_store.fresh(); - let ret_var = num_var; - let list_var = var_store.fresh(); - let closure_var = var_store.fresh(); - - let function = ( - var_store.fresh(), - Loc::at_zero(Expr::Var(Symbol::LIST_WALK)), - var_store.fresh(), - ret_var, - ); - - let body = Expr::Call( - Box::new(function), - vec![ - (list_var, Loc::at_zero(Var(Symbol::ARG_1))), - ( - num_var, - Loc::at_zero(num(var_store.fresh(), 0, num_no_bound())), - ), - (closure_var, Loc::at_zero(Var(Symbol::NUM_ADD))), - ], - CalledVia::Space, - ); - - defn( - symbol, - vec![(list_var, Symbol::ARG_1)], - var_store, - body, - ret_var, - ) -} - -/// List.product : List (Num a) -> Num a -fn list_product(symbol: Symbol, var_store: &mut VarStore) -> Def { - let num_var = var_store.fresh(); - let list_var = var_store.fresh(); - let closure_var = var_store.fresh(); - - let function = ( - var_store.fresh(), - Loc::at_zero(Expr::Var(Symbol::LIST_WALK)), - var_store.fresh(), - num_var, - ); - - let body = Expr::Call( - Box::new(function), - vec![ - (list_var, Loc::at_zero(Var(Symbol::ARG_1))), - ( - num_var, - Loc::at_zero(num(var_store.fresh(), 1, num_no_bound())), - ), - (closure_var, Loc::at_zero(Var(Symbol::NUM_MUL))), - ], - CalledVia::Space, - ); - - defn( - symbol, - vec![(list_var, Symbol::ARG_1)], - var_store, - body, - num_var, - ) -} - -/// List.keepIf : List elem, (elem -> Bool) -> List elem -fn list_keep_if(symbol: Symbol, var_store: &mut VarStore) -> Def { - let list_var = var_store.fresh(); - let func_var = var_store.fresh(); - - let body = RunLowLevel { - op: LowLevel::ListKeepIf, - args: vec![ - (list_var, Var(Symbol::ARG_1)), - (func_var, Var(Symbol::ARG_2)), - ], - ret_var: list_var, - }; - - defn( - symbol, - vec![(list_var, Symbol::ARG_1), (func_var, Symbol::ARG_2)], - var_store, - body, - list_var, - ) -} - -/// List.contains : List elem, elem -> Bool -fn list_contains(symbol: Symbol, var_store: &mut VarStore) -> Def { - lowlevel_2(symbol, LowLevel::ListContains, var_store) -} - -/// List.keepOks : List before, (before -> Result after *) -> List after -fn list_keep_oks(symbol: Symbol, var_store: &mut VarStore) -> Def { - lowlevel_2(symbol, LowLevel::ListKeepOks, var_store) -} - -/// List.keepErrs: List before, (before -> Result * after) -> List after -fn list_keep_errs(symbol: Symbol, var_store: &mut VarStore) -> Def { - lowlevel_2(symbol, LowLevel::ListKeepErrs, var_store) -} - -/// List.range: Int a, Int a -> List (Int a) -fn list_range(symbol: Symbol, var_store: &mut VarStore) -> Def { - lowlevel_2(symbol, LowLevel::ListRange, var_store) -} - -/// List.map : List before, (before -> after) -> List after -fn list_map(symbol: Symbol, var_store: &mut VarStore) -> Def { - lowlevel_2(symbol, LowLevel::ListMap, var_store) -} - -/// List.mapWithIndex : List before, (before, Nat -> after) -> List after -fn list_map_with_index(symbol: Symbol, var_store: &mut VarStore) -> Def { - lowlevel_2(symbol, LowLevel::ListMapWithIndex, var_store) -} - -/// List.map2 : List a, List b, (a, b -> c) -> List c -fn list_map2(symbol: Symbol, var_store: &mut VarStore) -> Def { - lowlevel_3(symbol, LowLevel::ListMap2, var_store) -} - -/// List.map3 : List a, List b, List c, (a, b, c -> d) -> List d -fn list_map3(symbol: Symbol, var_store: &mut VarStore) -> Def { - lowlevel_4(symbol, LowLevel::ListMap3, var_store) -} - -/// List.map4 : List a, List b, List c, List d, (a, b, c, d -> e) -> List e -fn list_map4(symbol: Symbol, var_store: &mut VarStore) -> Def { - lowlevel_5(symbol, LowLevel::ListMap4, var_store) -} - -/// List.sortWith : List a, (a, a -> Ordering) -> List a -fn list_sort_with(symbol: Symbol, var_store: &mut VarStore) -> Def { - lowlevel_2(symbol, LowLevel::ListSortWith, var_store) -} - -/// List.sortAsc : List (Num a) -> List (Num a) -fn list_sort_asc(symbol: Symbol, var_store: &mut VarStore) -> Def { - let list_var = var_store.fresh(); - let closure_var = var_store.fresh(); - let ret_var = list_var; - - let function = ( - var_store.fresh(), - Loc::at_zero(Expr::Var(Symbol::LIST_SORT_WITH)), - var_store.fresh(), - ret_var, - ); - - let body = Expr::Call( - Box::new(function), - vec![ - (list_var, Loc::at_zero(Var(Symbol::ARG_1))), - (closure_var, Loc::at_zero(Var(Symbol::NUM_COMPARE))), - ], - CalledVia::Space, - ); - - defn( - symbol, - vec![(list_var, Symbol::ARG_1)], - var_store, - body, - ret_var, - ) -} - -/// List.sortDesc : List (Num a) -> List (Num a) -fn list_sort_desc(symbol: Symbol, var_store: &mut VarStore) -> Def { - let list_var = var_store.fresh(); - let num_var = var_store.fresh(); - let closure_var = var_store.fresh(); - let compare_ret_var = var_store.fresh(); - let ret_var = list_var; - - let closure = Closure(ClosureData { - function_type: closure_var, - closure_type: var_store.fresh(), - return_type: compare_ret_var, - name: Symbol::LIST_SORT_DESC_COMPARE, - recursive: Recursive::NotRecursive, - captured_symbols: vec![], - arguments: vec![ - ( - num_var, - AnnotatedMark::new(var_store), - no_region(Pattern::Identifier(Symbol::ARG_2)), - ), - ( - num_var, - AnnotatedMark::new(var_store), - no_region(Pattern::Identifier(Symbol::ARG_3)), - ), - ], - loc_body: { - Box::new(no_region(RunLowLevel { - op: LowLevel::NumCompare, - args: vec![(num_var, Var(Symbol::ARG_3)), (num_var, Var(Symbol::ARG_2))], - ret_var: compare_ret_var, - })) - }, - }); - - let function = ( - var_store.fresh(), - Loc::at_zero(Expr::Var(Symbol::LIST_SORT_WITH)), - var_store.fresh(), - ret_var, - ); - - let body = Expr::Call( - Box::new(function), - vec![ - (list_var, Loc::at_zero(Var(Symbol::ARG_1))), - (closure_var, Loc::at_zero(closure)), - ], - CalledVia::Space, - ); - - defn( - symbol, - vec![(list_var, Symbol::ARG_1)], - var_store, - body, - ret_var, - ) -} - -/// List.any: List elem, (elem -> Bool) -> Bool -fn list_any(symbol: Symbol, var_store: &mut VarStore) -> Def { - lowlevel_2(symbol, LowLevel::ListAny, var_store) -} - -/// List.all: List elem, (elem -> Bool) -> Bool -fn list_all(symbol: Symbol, var_store: &mut VarStore) -> Def { - lowlevel_2(symbol, LowLevel::ListAll, var_store) -} - -/// List.find : List elem, (elem -> Bool) -> Result elem [NotFound]* -fn list_find(symbol: Symbol, var_store: &mut VarStore) -> Def { - let list = Symbol::ARG_1; - let find_predicate = Symbol::ARG_2; - - let find_result = Symbol::LIST_FIND_RESULT; - - let t_list = var_store.fresh(); - let t_pred_fn = var_store.fresh(); - let t_bool = var_store.fresh(); - let t_found = var_store.fresh(); - let t_value = var_store.fresh(); - let t_ret = var_store.fresh(); - let t_find_result = var_store.fresh(); - let t_ext_var1 = var_store.fresh(); - let t_ext_var2 = var_store.fresh(); - - // ListFindUnsafe returns { value: elem, found: Bool }. - // When `found` is true, the value was found. Otherwise `List.find` should return `Err ...` - let find_result_def = Def { - annotation: None, - expr_var: t_find_result, - loc_expr: no_region(RunLowLevel { - op: LowLevel::ListFindUnsafe, - args: vec![(t_list, Var(list)), (t_pred_fn, Var(find_predicate))], - ret_var: t_find_result, - }), - loc_pattern: no_region(Pattern::Identifier(find_result)), - pattern_vars: Default::default(), - }; - - let get_value = Access { - record_var: t_find_result, - ext_var: t_ext_var1, - field_var: t_value, - loc_expr: Box::new(no_region(Var(find_result))), - field: "value".into(), - }; - - let get_found = Access { - record_var: t_find_result, - ext_var: t_ext_var2, - field_var: t_found, - loc_expr: Box::new(no_region(Var(find_result))), - field: "found".into(), - }; - - let make_ok = tag("Ok", vec![get_value], var_store); - - let make_err = tag( - "Err", - vec![tag("NotFound", Vec::new(), var_store)], - var_store, - ); - - let inspect = If { - cond_var: t_bool, - branch_var: t_ret, - branches: vec![( - // if-condition - no_region(get_found), - no_region(make_ok), - )], - final_else: Box::new(no_region(make_err)), - }; - - let body = LetNonRec(Box::new(find_result_def), Box::new(no_region(inspect))); - - defn( - symbol, - vec![(t_list, Symbol::ARG_1), (t_pred_fn, Symbol::ARG_2)], - var_store, - body, - t_ret, - ) -} - -/// List.isUnique : List * -> Bool -fn list_is_unique(symbol: Symbol, var_store: &mut VarStore) -> Def { - lowlevel_1(symbol, LowLevel::ListIsUnique, var_store) -} - -/// Dict.len : Dict * * -> Nat -fn dict_len(symbol: Symbol, var_store: &mut VarStore) -> Def { - let arg1_var = var_store.fresh(); - let ret_var = Variable::NAT; - - let body = RunLowLevel { - op: LowLevel::DictSize, - args: vec![(arg1_var, Var(Symbol::ARG_1))], - ret_var, - }; - - defn( - symbol, - vec![(arg1_var, Symbol::ARG_1)], - var_store, - body, - ret_var, - ) -} - -/// Dict.empty : Dict * * -fn dict_empty(symbol: Symbol, var_store: &mut VarStore) -> Def { - let dict_var = var_store.fresh(); - let body = RunLowLevel { - op: LowLevel::DictEmpty, - args: vec![], - ret_var: dict_var, - }; - - Def { - annotation: None, - expr_var: dict_var, - loc_expr: Loc::at_zero(body), - loc_pattern: Loc::at_zero(Pattern::Identifier(symbol)), - pattern_vars: SendMap::default(), - } -} - -/// Dict.single : k, v -> Dict k v -fn dict_single(symbol: Symbol, var_store: &mut VarStore) -> Def { - let key_var = var_store.fresh(); - let value_var = var_store.fresh(); - let dict_var = var_store.fresh(); - - let empty = RunLowLevel { - op: LowLevel::DictEmpty, - args: vec![], - ret_var: dict_var, - }; - - let body = RunLowLevel { - op: LowLevel::DictInsert, - args: vec![ - (dict_var, empty), - (key_var, Var(Symbol::ARG_1)), - (value_var, Var(Symbol::ARG_2)), - ], - ret_var: dict_var, - }; - - defn( - symbol, - vec![(key_var, Symbol::ARG_1), (value_var, Symbol::ARG_2)], - var_store, - body, - dict_var, - ) -} - -/// Dict.insert : Dict k v, k, v -> Dict k v -fn dict_insert(symbol: Symbol, var_store: &mut VarStore) -> Def { - lowlevel_3(symbol, LowLevel::DictInsert, var_store) -} - -/// Dict.remove : Dict k v, k -> Dict k v -fn dict_remove(symbol: Symbol, var_store: &mut VarStore) -> Def { - lowlevel_2(symbol, LowLevel::DictRemove, var_store) -} - -/// Dict.contains : Dict k v, k -> Bool -fn dict_contains(symbol: Symbol, var_store: &mut VarStore) -> Def { - lowlevel_2(symbol, LowLevel::DictContains, var_store) -} - -/// Dict.get : Dict k v, k -> Result v [KeyNotFound]* -fn dict_get(symbol: Symbol, var_store: &mut VarStore) -> Def { - let arg_dict = Symbol::ARG_1; - let arg_key = Symbol::ARG_2; - - let temp_record = Symbol::DICT_GET_RESULT; - - let bool_var = var_store.fresh(); - let flag_var = var_store.fresh(); - let key_var = var_store.fresh(); - let dict_var = var_store.fresh(); - let value_var = var_store.fresh(); - let ret_var = var_store.fresh(); - - let temp_record_var = var_store.fresh(); - let ext_var1 = var_store.fresh(); - let ext_var2 = var_store.fresh(); - - // NOTE DictGetUnsafe returns a { flag: Bool, value: v } - // when the flag is True, the value is found and defined; - // otherwise it is not and `Dict.get` should return `Err ...` - let def_body = RunLowLevel { - op: LowLevel::DictGetUnsafe, - args: vec![(dict_var, Var(arg_dict)), (key_var, Var(arg_key))], - ret_var: temp_record_var, - }; - - let def = Def { - annotation: None, - expr_var: temp_record_var, - loc_expr: Loc::at_zero(def_body), - loc_pattern: Loc::at_zero(Pattern::Identifier(temp_record)), - pattern_vars: Default::default(), - }; - - let get_value = Access { - record_var: temp_record_var, - ext_var: ext_var1, - field_var: value_var, - loc_expr: Box::new(no_region(Var(temp_record))), - field: "value".into(), - }; - - let get_flag = Access { - record_var: temp_record_var, - ext_var: ext_var2, - field_var: flag_var, - loc_expr: Box::new(no_region(Var(temp_record))), - field: "zflag".into(), - }; - - let make_ok = tag("Ok", vec![get_value], var_store); - - let make_err = tag( - "Err", - vec![tag("KeyNotFound", Vec::new(), var_store)], - var_store, - ); - - let inspect = If { - cond_var: bool_var, - branch_var: ret_var, - branches: vec![( - // if-condition - no_region(get_flag), - no_region(make_ok), - )], - final_else: Box::new(no_region(make_err)), - }; - - let body = LetNonRec(Box::new(def), Box::new(no_region(inspect))); - - defn( - symbol, - vec![(dict_var, Symbol::ARG_1), (key_var, Symbol::ARG_2)], - var_store, - body, - ret_var, - ) -} - -/// Dict.keys : Dict k v -> List k -fn dict_keys(symbol: Symbol, var_store: &mut VarStore) -> Def { - lowlevel_1(symbol, LowLevel::DictKeys, var_store) -} - -/// Dict.values : Dict k v -> List v -fn dict_values(symbol: Symbol, var_store: &mut VarStore) -> Def { - lowlevel_1(symbol, LowLevel::DictValues, var_store) -} - -/// Dict.union : Dict k v, Dict k v -> Dict k v -fn dict_union(symbol: Symbol, var_store: &mut VarStore) -> Def { - lowlevel_2(symbol, LowLevel::DictUnion, var_store) -} - -/// Dict.difference : Dict k v, Dict k v -> Dict k v -fn dict_difference(symbol: Symbol, var_store: &mut VarStore) -> Def { - lowlevel_2(symbol, LowLevel::DictDifference, var_store) -} - -/// Dict.intersection : Dict k v, Dict k v -> Dict k v -fn dict_intersection(symbol: Symbol, var_store: &mut VarStore) -> Def { - lowlevel_2(symbol, LowLevel::DictIntersection, var_store) -} - -/// Dict.walk : Dict k v, state, (state, k, v -> state) -> state -fn dict_walk(symbol: Symbol, var_store: &mut VarStore) -> Def { - lowlevel_3(symbol, LowLevel::DictWalk, var_store) -} - -/// Set.empty : Set * -fn set_empty(symbol: Symbol, var_store: &mut VarStore) -> Def { - let set_var = var_store.fresh(); - let body = RunLowLevel { - op: LowLevel::DictEmpty, - args: vec![], - ret_var: set_var, - }; - - Def { - annotation: None, - expr_var: set_var, - loc_expr: Loc::at_zero(body), - loc_pattern: Loc::at_zero(Pattern::Identifier(symbol)), - pattern_vars: SendMap::default(), - } -} - -/// Set.single : k -> Set k -fn set_single(symbol: Symbol, var_store: &mut VarStore) -> Def { - let key_var = var_store.fresh(); - let set_var = var_store.fresh(); - let value_var = Variable::EMPTY_RECORD; - - let empty = RunLowLevel { - op: LowLevel::DictEmpty, - args: vec![], - ret_var: set_var, - }; - - let body = RunLowLevel { - op: LowLevel::DictInsert, - args: vec![ - (set_var, empty), - (key_var, Var(Symbol::ARG_1)), - (value_var, EmptyRecord), - ], - ret_var: set_var, - }; - - defn( - symbol, - vec![(key_var, Symbol::ARG_1)], - var_store, - body, - set_var, - ) -} - -/// Set.len : Set * -> Nat -fn set_len(symbol: Symbol, var_store: &mut VarStore) -> Def { - lowlevel_1(symbol, LowLevel::DictSize, var_store) -} - -/// Dict.union : Dict k v, Dict k v -> Dict k v -fn set_union(symbol: Symbol, var_store: &mut VarStore) -> Def { - lowlevel_2(symbol, LowLevel::DictUnion, var_store) -} - -/// Dict.difference : Dict k v, Dict k v -> Dict k v -fn set_difference(symbol: Symbol, var_store: &mut VarStore) -> Def { - lowlevel_2(symbol, LowLevel::DictDifference, var_store) -} - -/// Dict.intersection : Dict k v, Dict k v -> Dict k v -fn set_intersection(symbol: Symbol, var_store: &mut VarStore) -> Def { - lowlevel_2(symbol, LowLevel::DictIntersection, var_store) -} - -/// Set.toList : Set k -> List k -fn set_to_list(symbol: Symbol, var_store: &mut VarStore) -> Def { - dict_keys(symbol, var_store) -} - -/// Set.fromList : List k -> Set k -fn set_from_list(symbol: Symbol, var_store: &mut VarStore) -> Def { - lowlevel_1(symbol, LowLevel::SetFromList, var_store) -} - -/// Set.toDict : Set k -> Dict k {} -fn set_to_dict(symbol: Symbol, var_store: &mut VarStore) -> Def { - lowlevel_1(symbol, LowLevel::SetToDict, var_store) -} - -/// Set.insert : Set k, k -> Set k -fn set_insert(symbol: Symbol, var_store: &mut VarStore) -> Def { - let dict_var = var_store.fresh(); - let key_var = var_store.fresh(); - let val_var = Variable::EMPTY_RECORD; - - let body = RunLowLevel { - op: LowLevel::DictInsert, - args: vec![ - (dict_var, Var(Symbol::ARG_1)), - (key_var, Var(Symbol::ARG_2)), - (val_var, EmptyRecord), - ], - ret_var: dict_var, - }; - - defn( - symbol, - vec![(dict_var, Symbol::ARG_1), (key_var, Symbol::ARG_2)], - var_store, - body, - dict_var, - ) -} - -/// Set.remove : Set k, k -> Set k -fn set_remove(symbol: Symbol, var_store: &mut VarStore) -> Def { - dict_remove(symbol, var_store) -} - -/// Set.remove : Set k, k -> Set k -fn set_contains(symbol: Symbol, var_store: &mut VarStore) -> Def { - dict_contains(symbol, var_store) -} - -/// Set.walk : Set k, (accum, k -> accum), accum -> accum -fn set_walk(symbol: Symbol, var_store: &mut VarStore) -> Def { - let dict_var = var_store.fresh(); - let func_var = var_store.fresh(); - let key_var = var_store.fresh(); - let accum_var = var_store.fresh(); - let wrapper_var = var_store.fresh(); - - let user_function = Box::new(( - func_var, - no_region(Var(Symbol::ARG_3)), - var_store.fresh(), - accum_var, - )); - - let call_func = Call( - user_function, - vec![ - (accum_var, no_region(Var(Symbol::ARG_5))), - (key_var, no_region(Var(Symbol::ARG_6))), - ], - CalledVia::Space, - ); - - let wrapper = Closure(ClosureData { - function_type: wrapper_var, - closure_type: var_store.fresh(), - return_type: accum_var, - name: Symbol::SET_WALK_USER_FUNCTION, - recursive: Recursive::NotRecursive, - captured_symbols: vec![(Symbol::ARG_3, func_var)], - arguments: vec![ - ( - accum_var, - AnnotatedMark::new(var_store), - no_region(Pattern::Identifier(Symbol::ARG_5)), - ), - ( - key_var, - AnnotatedMark::new(var_store), - no_region(Pattern::Identifier(Symbol::ARG_6)), - ), - ( - Variable::EMPTY_RECORD, - AnnotatedMark::new(var_store), - no_region(Pattern::Underscore), - ), - ], - loc_body: Box::new(no_region(call_func)), - }); - - let body = RunLowLevel { - op: LowLevel::DictWalk, - args: vec![ - (dict_var, Var(Symbol::ARG_1)), - (accum_var, Var(Symbol::ARG_2)), - (wrapper_var, wrapper), - ], - ret_var: accum_var, - }; - - defn( - symbol, - vec![ - (dict_var, Symbol::ARG_1), - (accum_var, Symbol::ARG_2), - (func_var, Symbol::ARG_3), - ], - var_store, - body, - accum_var, - ) -} - -/// Num.rem : Int a, Int a -> Int a -fn num_rem(symbol: Symbol, var_store: &mut VarStore) -> Def { - num_binop(symbol, var_store, LowLevel::NumRemUnchecked) -} - -/// Num.remChecked : Int a, Int a -> Result (Int a) [DivByZero]* -fn num_rem_checked(symbol: Symbol, var_store: &mut VarStore) -> Def { - let num_var = var_store.fresh(); - let unbound_zero_var = var_store.fresh(); - let bool_var = var_store.fresh(); - let ret_var = var_store.fresh(); - - let body = If { - branch_var: ret_var, - cond_var: bool_var, - branches: vec![( - // if condition - no_region( - // Num.isNeq arg2 0 - RunLowLevel { - op: LowLevel::NotEq, - args: vec![ - (num_var, Var(Symbol::ARG_2)), - (num_var, num(unbound_zero_var, 0, num_no_bound())), - ], - ret_var: bool_var, - }, - ), - // arg1 was not zero - no_region( - // Ok (Int.#remUnsafe arg1 arg2) - tag( - "Ok", - vec![ - // Num.#remUnsafe arg1 arg2 - RunLowLevel { - op: LowLevel::NumRemUnchecked, - args: vec![ - (num_var, Var(Symbol::ARG_1)), - (num_var, Var(Symbol::ARG_2)), - ], - ret_var: num_var, - }, - ], - var_store, - ), - ), - )], - final_else: Box::new(no_region(tag( - "Err", - vec![tag("DivByZero", Vec::new(), var_store)], - var_store, - ))), - }; - - defn( - symbol, - vec![(num_var, Symbol::ARG_1), (num_var, Symbol::ARG_2)], - var_store, - body, - ret_var, - ) -} - -/// Num.isMultipleOf : Int a, Int a -> Bool -fn num_is_multiple_of(symbol: Symbol, var_store: &mut VarStore) -> Def { - lowlevel_2(symbol, LowLevel::NumIsMultipleOf, var_store) -} - -/// Num.neg : Num a -> Num a -fn num_neg(symbol: Symbol, var_store: &mut VarStore) -> Def { - let num_var = var_store.fresh(); - - let body = RunLowLevel { - op: LowLevel::NumNeg, - args: vec![(num_var, Var(Symbol::ARG_1))], - ret_var: num_var, - }; - - defn( - symbol, - vec![(num_var, Symbol::ARG_1)], - var_store, - body, - num_var, - ) -} - -/// Num.abs : Num a -> Num a -fn num_abs(symbol: Symbol, var_store: &mut VarStore) -> Def { - let num_var = var_store.fresh(); - - let body = RunLowLevel { - op: LowLevel::NumAbs, - args: vec![(num_var, Var(Symbol::ARG_1))], - ret_var: num_var, - }; - - defn( - symbol, - vec![(num_var, Symbol::ARG_1)], - var_store, - body, - num_var, - ) -} - -/// Num.div : Frac, Frac -> Frac -fn num_div_frac(symbol: Symbol, var_store: &mut VarStore) -> Def { - num_binop(symbol, var_store, LowLevel::NumDivUnchecked) -} - -/// Num.divChecked : Frac, Frac -> Result Frac [DivByZero]* -fn num_div_frac_checked(symbol: Symbol, var_store: &mut VarStore) -> Def { - let bool_var = var_store.fresh(); - let num_var = var_store.fresh(); - let unbound_zero_var = var_store.fresh(); - let precision_var = var_store.fresh(); - let ret_var = var_store.fresh(); - - let body = If { - branch_var: ret_var, - cond_var: bool_var, - branches: vec![( - // if-condition - no_region( - // Num.neq denominator 0 - RunLowLevel { - op: LowLevel::NotEq, - args: vec![ - (num_var, Var(Symbol::ARG_2)), - ( - num_var, - frac(unbound_zero_var, precision_var, 0.0, float_no_bound()), - ), - ], - ret_var: bool_var, - }, - ), - // denominator was not zero - no_region( - // Ok (Frac.#divUnchecked numerator denominator) - tag( - "Ok", - vec![ - // Num.#divUnchecked numerator denominator - RunLowLevel { - op: LowLevel::NumDivUnchecked, - args: vec![ - (num_var, Var(Symbol::ARG_1)), - (num_var, Var(Symbol::ARG_2)), - ], - ret_var: num_var, - }, - ], - var_store, - ), - ), - )], - final_else: Box::new( - // denominator was zero - no_region(tag( - "Err", - vec![tag("DivByZero", Vec::new(), var_store)], - var_store, - )), - ), - }; - - defn( - symbol, - vec![(num_var, Symbol::ARG_1), (num_var, Symbol::ARG_2)], - var_store, - body, - ret_var, - ) -} - -/// Num.divTrunc : Int a, Int a -> Int a -fn num_div_trunc(symbol: Symbol, var_store: &mut VarStore) -> Def { - num_binop(symbol, var_store, LowLevel::NumDivUnchecked) -} - -/// Num.divTruncChecked : Int a , Int a -> Result (Int a) [DivByZero]* -fn num_div_trunc_checked(symbol: Symbol, var_store: &mut VarStore) -> Def { - let bool_var = var_store.fresh(); - let num_var = var_store.fresh(); - let unbound_zero_var = var_store.fresh(); - let unbound_zero_precision_var = var_store.fresh(); - let ret_var = var_store.fresh(); - - let body = If { - branch_var: ret_var, - cond_var: bool_var, - branches: vec![( - // if-condition - no_region( - // Num.neq denominator 0 - RunLowLevel { - op: LowLevel::NotEq, - args: vec![ - (num_var, Var(Symbol::ARG_2)), - ( - num_var, - int::( - unbound_zero_var, - unbound_zero_precision_var, - 0, - int_no_bound(), - ), - ), - ], - ret_var: bool_var, - }, - ), - // denominator was not zero - no_region( - // Ok (Int.#divUnchecked numerator denominator) - tag( - "Ok", - vec![ - // Num.#divUnchecked numerator denominator - RunLowLevel { - op: LowLevel::NumDivUnchecked, - args: vec![ - (num_var, Var(Symbol::ARG_1)), - (num_var, Var(Symbol::ARG_2)), - ], - ret_var: num_var, - }, - ], - var_store, - ), - ), - )], - final_else: Box::new( - // denominator was zero - no_region(tag( - "Err", - vec![tag("DivByZero", Vec::new(), var_store)], - var_store, - )), - ), - }; - - defn( - symbol, - vec![(num_var, Symbol::ARG_1), (num_var, Symbol::ARG_2)], - var_store, - body, - ret_var, - ) -} - -/// Num.divCeil : Int a, Int a -> Int a -fn num_div_ceil(symbol: Symbol, var_store: &mut VarStore) -> Def { - num_binop(symbol, var_store, LowLevel::NumDivCeilUnchecked) -} - -/// Num.divCeilChecked : Int a , Int a -> Result (Int a) [DivByZero]* -fn num_div_ceil_checked(symbol: Symbol, var_store: &mut VarStore) -> Def { - let bool_var = var_store.fresh(); - let num_var = var_store.fresh(); - let unbound_zero_var = var_store.fresh(); - let unbound_zero_precision_var = var_store.fresh(); - let ret_var = var_store.fresh(); - - let body = If { - branch_var: ret_var, - cond_var: bool_var, - branches: vec![( - // if-condition - no_region( - // Num.neq denominator 0 - RunLowLevel { - op: LowLevel::NotEq, - args: vec![ - (num_var, Var(Symbol::ARG_2)), - ( - num_var, - int::( - unbound_zero_var, - unbound_zero_precision_var, - 0, - int_no_bound(), - ), - ), - ], - ret_var: bool_var, - }, - ), - // denominator was not zero - no_region( - // Ok (Int.#divUnchecked numerator denominator) - tag( - "Ok", - vec![ - // Num.#divUnchecked numerator denominator - RunLowLevel { - op: LowLevel::NumDivCeilUnchecked, - args: vec![ - (num_var, Var(Symbol::ARG_1)), - (num_var, Var(Symbol::ARG_2)), - ], - ret_var: num_var, - }, - ], - var_store, - ), - ), - )], - final_else: Box::new( - // denominator was zero - no_region(tag( - "Err", - vec![tag("DivByZero", Vec::new(), var_store)], - var_store, - )), - ), - }; - - defn( - symbol, - vec![(num_var, Symbol::ARG_1), (num_var, Symbol::ARG_2)], - var_store, - body, - ret_var, - ) -} - -/// List.first : List elem -> Result elem [ListWasEmpty]* -/// -/// List.first : -/// Attr (* | u) (List (Attr u a)), -/// -> Attr * (Result (Attr u a) (Attr * [OutOfBounds]*)) -fn list_first(symbol: Symbol, var_store: &mut VarStore) -> Def { - let bool_var = var_store.fresh(); - let list_var = var_store.fresh(); - let len_var = Variable::NAT; - let zero_var = len_var; - let zero_precision_var = Variable::NATURAL; - let list_elem_var = var_store.fresh(); - let ret_var = var_store.fresh(); - - // Perform a bounds check. If it passes, delegate to List.getUnsafe. - let body = If { - cond_var: bool_var, - branch_var: var_store.fresh(), - branches: vec![( - // if-condition - no_region( - // List.len list != 0 - RunLowLevel { - op: LowLevel::NotEq, - args: vec![ - ( - len_var, - int::(zero_var, zero_precision_var, 0, int_no_bound()), - ), - ( - len_var, - RunLowLevel { - op: LowLevel::ListLen, - args: vec![(list_var, Var(Symbol::ARG_1))], - ret_var: len_var, - }, - ), - ], - ret_var: bool_var, - }, - ), - // list was not empty - no_region( - // Ok (List.#getUnsafe list 0) - tag( - "Ok", - vec![ - // List.#getUnsafe list 0 - RunLowLevel { - op: LowLevel::ListGetUnsafe, - args: vec![ - (list_var, Var(Symbol::ARG_1)), - ( - len_var, - int::(zero_var, zero_precision_var, 0, int_no_bound()), - ), - ], - ret_var: list_elem_var, - }, - ], - var_store, - ), - ), - )], - final_else: Box::new( - // list was empty - no_region( - // Err ListWasEmpty - tag( - "Err", - vec![tag("ListWasEmpty", Vec::new(), var_store)], - var_store, - ), - ), - ), - }; - - defn( - symbol, - vec![(list_var, Symbol::ARG_1)], - var_store, - body, - ret_var, - ) -} - -/// List.last : List elem -> Result elem [ListWasEmpty]* -/// -/// List.last : -/// Attr (* | u) (List (Attr u a)), -/// -> Attr * (Result (Attr u a) (Attr * [OutOfBounds]*)) -fn list_last(symbol: Symbol, var_store: &mut VarStore) -> Def { - let arg_var = var_store.fresh(); - let bool_var = var_store.fresh(); - let list_var = var_store.fresh(); - let len_var = Variable::NAT; - let num_var = len_var; - let num_precision_var = Variable::NATURAL; - let list_elem_var = var_store.fresh(); - let ret_var = var_store.fresh(); - - // Perform a bounds check. If it passes, delegate to List.getUnsafe. - let body = If { - cond_var: bool_var, - branch_var: var_store.fresh(), - branches: vec![( - // if-condition - no_region( - // List.len list != 0 - RunLowLevel { - op: LowLevel::NotEq, - args: vec![ - ( - len_var, - int::(num_var, num_precision_var, 0, int_no_bound()), - ), - ( - len_var, - RunLowLevel { - op: LowLevel::ListLen, - args: vec![(list_var, Var(Symbol::ARG_1))], - ret_var: len_var, - }, - ), - ], - ret_var: bool_var, - }, - ), - // list was not empty - no_region( - // Ok (List.getUnsafe list (Num.sub (List.len list) 1)) - tag( - "Ok", - vec![ - // List.getUnsafe list (Num.sub (List.len list) 1) - RunLowLevel { - op: LowLevel::ListGetUnsafe, - args: vec![ - (list_var, Var(Symbol::ARG_1)), - ( - len_var, - // Num.sub (List.len list) 1 - RunLowLevel { - op: LowLevel::NumSubWrap, - args: vec![ - ( - arg_var, - // List.len list - RunLowLevel { - op: LowLevel::ListLen, - args: vec![(list_var, Var(Symbol::ARG_1))], - ret_var: len_var, - }, - ), - ( - arg_var, - int::( - num_var, - num_precision_var, - 1, - int_no_bound(), - ), - ), - ], - ret_var: len_var, - }, - ), - ], - ret_var: list_elem_var, - }, - ], - var_store, - ), - ), - )], - final_else: Box::new( - // list was empty - no_region( - // Err ListWasEmpty - tag( - "Err", - vec![tag("ListWasEmpty", Vec::new(), var_store)], - var_store, - ), - ), - ), - }; - - defn( - symbol, - vec![(list_var, Symbol::ARG_1)], - var_store, - body, - ret_var, - ) -} - -fn result_map(symbol: Symbol, var_store: &mut VarStore) -> Def { - let ret_var = var_store.fresh(); - let func_var = var_store.fresh(); - let result_var = var_store.fresh(); - - let mut branches = vec![]; - - { - let user_function = Box::new(( - func_var, - no_region(Var(Symbol::ARG_2)), - var_store.fresh(), - var_store.fresh(), - )); - - let call_func = Call( - user_function, - vec![(var_store.fresh(), no_region(Var(Symbol::ARG_5)))], - CalledVia::Space, - ); - - let tag_name = TagName("Ok".into()); - - // ok branch - let ok = Tag { - variant_var: var_store.fresh(), - ext_var: var_store.fresh(), - name: tag_name.clone(), - arguments: vec![(var_store.fresh(), no_region(call_func))], - }; - - let pattern = Pattern::AppliedTag { - whole_var: result_var, - ext_var: var_store.fresh(), - tag_name, - arguments: vec![( - var_store.fresh(), - no_region(Pattern::Identifier(Symbol::ARG_5)), - )], - }; - - let branch = expr::WhenBranch { - patterns: vec![no_region(pattern)], - value: no_region(ok), - guard: None, - redundant: RedundantMark::new(var_store), - }; - - branches.push(branch); - } - - { - // err branch - let tag_name = TagName("Err".into()); - - let err = Tag { - variant_var: var_store.fresh(), - ext_var: var_store.fresh(), - name: tag_name.clone(), - arguments: vec![(var_store.fresh(), no_region(Var(Symbol::ARG_4)))], - }; - - let pattern = Pattern::AppliedTag { - whole_var: result_var, - ext_var: var_store.fresh(), - tag_name, - arguments: vec![( - var_store.fresh(), - no_region(Pattern::Identifier(Symbol::ARG_4)), - )], - }; - - let branch = expr::WhenBranch { - patterns: vec![no_region(pattern)], - value: no_region(err), - guard: None, - redundant: RedundantMark::new(var_store), - }; - - branches.push(branch); - } - - let body = When { - cond_var: result_var, - expr_var: ret_var, - region: Region::zero(), - loc_cond: Box::new(no_region(Var(Symbol::ARG_1))), - branches, - branches_cond_var: var_store.fresh(), - exhaustive: ExhaustiveMark::new(var_store), - }; - - defn( - symbol, - vec![(result_var, Symbol::ARG_1), (func_var, Symbol::ARG_2)], - var_store, - body, - ret_var, - ) -} - -fn result_map_err(symbol: Symbol, var_store: &mut VarStore) -> Def { - let ret_var = var_store.fresh(); - let func_var = var_store.fresh(); - let result_var = var_store.fresh(); - - let mut branches = vec![]; - - { - let user_function = Box::new(( - func_var, - no_region(Var(Symbol::ARG_2)), - var_store.fresh(), - var_store.fresh(), - )); - - let call_func = Call( - user_function, - vec![(var_store.fresh(), no_region(Var(Symbol::ARG_5)))], - CalledVia::Space, - ); - - let tag_name = TagName("Err".into()); - - // ok branch - let ok = Tag { - variant_var: var_store.fresh(), - ext_var: var_store.fresh(), - name: tag_name.clone(), - arguments: vec![(var_store.fresh(), no_region(call_func))], - }; - - let pattern = Pattern::AppliedTag { - whole_var: result_var, - ext_var: var_store.fresh(), - tag_name, - arguments: vec![( - var_store.fresh(), - no_region(Pattern::Identifier(Symbol::ARG_5)), - )], - }; - - let branch = expr::WhenBranch { - patterns: vec![no_region(pattern)], - value: no_region(ok), - guard: None, - redundant: RedundantMark::new(var_store), - }; - - branches.push(branch); - } - - { - // err branch - let tag_name = TagName("Ok".into()); - - let err = Tag { - variant_var: var_store.fresh(), - ext_var: var_store.fresh(), - name: tag_name.clone(), - arguments: vec![(var_store.fresh(), no_region(Var(Symbol::ARG_4)))], - }; - - let pattern = Pattern::AppliedTag { - whole_var: result_var, - ext_var: var_store.fresh(), - tag_name, - arguments: vec![( - var_store.fresh(), - no_region(Pattern::Identifier(Symbol::ARG_4)), - )], - }; - - let branch = expr::WhenBranch { - patterns: vec![no_region(pattern)], - value: no_region(err), - guard: None, - redundant: RedundantMark::new(var_store), - }; - - branches.push(branch); - } - - let body = When { - cond_var: result_var, - expr_var: ret_var, - region: Region::zero(), - loc_cond: Box::new(no_region(Var(Symbol::ARG_1))), - branches, - branches_cond_var: var_store.fresh(), - exhaustive: ExhaustiveMark::new(var_store), - }; - - defn( - symbol, - vec![(result_var, Symbol::ARG_1), (func_var, Symbol::ARG_2)], - var_store, - body, - ret_var, - ) -} - -fn result_with_default(symbol: Symbol, var_store: &mut VarStore) -> Def { - let ret_var = var_store.fresh(); - let result_var = var_store.fresh(); - - let mut branches = vec![]; - - { - // ok branch - let tag_name = TagName("Ok".into()); - - let pattern = Pattern::AppliedTag { - whole_var: result_var, - ext_var: var_store.fresh(), - tag_name, - arguments: vec![(ret_var, no_region(Pattern::Identifier(Symbol::ARG_3)))], - }; - - let branch = expr::WhenBranch { - patterns: vec![no_region(pattern)], - value: no_region(Var(Symbol::ARG_3)), - guard: None, - redundant: RedundantMark::new(var_store), - }; - - branches.push(branch); - } - - { - // err branch - let tag_name = TagName("Err".into()); - - let pattern = Pattern::AppliedTag { - whole_var: result_var, - ext_var: var_store.fresh(), - tag_name, - arguments: vec![(var_store.fresh(), no_region(Pattern::Underscore))], - }; - - let branch = expr::WhenBranch { - patterns: vec![no_region(pattern)], - value: no_region(Var(Symbol::ARG_2)), - guard: None, - redundant: RedundantMark::new(var_store), - }; - - branches.push(branch); - } - - let body = When { - cond_var: result_var, - expr_var: ret_var, - region: Region::zero(), - loc_cond: Box::new(no_region(Var(Symbol::ARG_1))), - branches, - branches_cond_var: var_store.fresh(), - exhaustive: ExhaustiveMark::new(var_store), - }; - - defn( - symbol, - vec![(result_var, Symbol::ARG_1), (ret_var, Symbol::ARG_2)], - var_store, - body, - ret_var, - ) -} - -fn result_is_err(symbol: Symbol, var_store: &mut VarStore) -> Def { - let ret_var = var_store.fresh(); - let result_var = var_store.fresh(); - - let mut branches = vec![]; - - { - // ok branch - let tag_name = TagName("Ok".into()); - - let pattern = Pattern::AppliedTag { - whole_var: result_var, - ext_var: var_store.fresh(), - tag_name, - arguments: vec![(var_store.fresh(), no_region(Pattern::Underscore))], - }; - - let false_expr = Tag { - variant_var: var_store.fresh(), - ext_var: var_store.fresh(), - name: TagName("False".into()), - arguments: vec![], - }; - - let branch = expr::WhenBranch { - patterns: vec![no_region(pattern)], - value: no_region(false_expr), - guard: None, - redundant: RedundantMark::new(var_store), - }; - - branches.push(branch); - } - - { - // err branch - let tag_name = TagName("Err".into()); - - let pattern = Pattern::AppliedTag { - whole_var: result_var, - ext_var: var_store.fresh(), - tag_name, - arguments: vec![(var_store.fresh(), no_region(Pattern::Underscore))], - }; - - let true_expr = Tag { - variant_var: var_store.fresh(), - ext_var: var_store.fresh(), - name: TagName("True".into()), - arguments: vec![], - }; - - let branch = expr::WhenBranch { - patterns: vec![no_region(pattern)], - value: no_region(true_expr), - guard: None, - redundant: RedundantMark::new(var_store), - }; - - branches.push(branch); - } - - let body = When { - cond_var: result_var, - expr_var: ret_var, - region: Region::zero(), - loc_cond: Box::new(no_region(Var(Symbol::ARG_1))), - branches, - branches_cond_var: var_store.fresh(), - exhaustive: ExhaustiveMark::new(var_store), - }; - - defn( - symbol, - vec![(result_var, Symbol::ARG_1)], - var_store, - body, - ret_var, - ) -} - -fn result_is_ok(symbol: Symbol, var_store: &mut VarStore) -> Def { - let ret_var = var_store.fresh(); - let result_var = var_store.fresh(); - - let mut branches = vec![]; - - { - // ok branch - let tag_name = TagName("Ok".into()); - - let pattern = Pattern::AppliedTag { - whole_var: result_var, - ext_var: var_store.fresh(), - tag_name, - arguments: vec![(var_store.fresh(), no_region(Pattern::Underscore))], - }; - - let true_expr = Tag { - variant_var: var_store.fresh(), - ext_var: var_store.fresh(), - name: TagName("True".into()), - arguments: vec![], - }; - - let branch = expr::WhenBranch { - patterns: vec![no_region(pattern)], - value: no_region(true_expr), - guard: None, - redundant: RedundantMark::new(var_store), - }; - - branches.push(branch); - } - - { - // err branch - let tag_name = TagName("Err".into()); - - let pattern = Pattern::AppliedTag { - whole_var: result_var, - ext_var: var_store.fresh(), - tag_name, - arguments: vec![(var_store.fresh(), no_region(Pattern::Underscore))], - }; - - let false_expr = Tag { - variant_var: var_store.fresh(), - ext_var: var_store.fresh(), - name: TagName("False".into()), - arguments: vec![], - }; - - let branch = expr::WhenBranch { - patterns: vec![no_region(pattern)], - value: no_region(false_expr), - guard: None, - redundant: RedundantMark::new(var_store), - }; - - branches.push(branch); - } - - let body = When { - cond_var: result_var, - expr_var: ret_var, - region: Region::zero(), - loc_cond: Box::new(no_region(Var(Symbol::ARG_1))), - branches, - branches_cond_var: var_store.fresh(), - exhaustive: ExhaustiveMark::new(var_store), - }; - - defn( - symbol, - vec![(result_var, Symbol::ARG_1)], - var_store, - body, - ret_var, - ) -} - -fn result_after(symbol: Symbol, var_store: &mut VarStore) -> Def { - let ret_var = var_store.fresh(); - let func_var = var_store.fresh(); - let result_var = var_store.fresh(); - - let mut branches = vec![]; - - { - let user_function = Box::new(( - func_var, - no_region(Var(Symbol::ARG_2)), - var_store.fresh(), - var_store.fresh(), - )); - - let call_func = Call( - user_function, - vec![(var_store.fresh(), no_region(Var(Symbol::ARG_5)))], - CalledVia::Space, - ); - - let tag_name = TagName("Ok".into()); - - // ok branch - let ok = call_func; - - let pattern = Pattern::AppliedTag { - whole_var: result_var, - ext_var: var_store.fresh(), - tag_name, - arguments: vec![( - var_store.fresh(), - no_region(Pattern::Identifier(Symbol::ARG_5)), - )], - }; - - let branch = expr::WhenBranch { - patterns: vec![no_region(pattern)], - value: no_region(ok), - guard: None, - redundant: RedundantMark::new(var_store), - }; - - branches.push(branch); - } - - { - // err branch - let tag_name = TagName("Err".into()); - - let err = Tag { - variant_var: var_store.fresh(), - ext_var: var_store.fresh(), - name: tag_name.clone(), - arguments: vec![(var_store.fresh(), no_region(Var(Symbol::ARG_4)))], - }; - - let pattern = Pattern::AppliedTag { - whole_var: result_var, - ext_var: var_store.fresh(), - tag_name, - arguments: vec![( - var_store.fresh(), - no_region(Pattern::Identifier(Symbol::ARG_4)), - )], - }; - - let branch = expr::WhenBranch { - patterns: vec![no_region(pattern)], - value: no_region(err), - guard: None, - redundant: RedundantMark::new(var_store), - }; - - branches.push(branch); - } - - let body = When { - cond_var: result_var, - expr_var: ret_var, - region: Region::zero(), - loc_cond: Box::new(no_region(Var(Symbol::ARG_1))), - branches, - branches_cond_var: var_store.fresh(), - exhaustive: ExhaustiveMark::new(var_store), - }; - - defn( - symbol, - vec![(result_var, Symbol::ARG_1), (func_var, Symbol::ARG_2)], - var_store, - body, - ret_var, - ) -} - -#[inline(always)] -fn no_region(value: T) -> Loc { - Loc { - region: Region::zero(), - value, - } -} - -#[inline(always)] -fn tag(name: &'static str, args: Vec, var_store: &mut VarStore) -> Expr { - Expr::Tag { - variant_var: var_store.fresh(), - ext_var: var_store.fresh(), - name: TagName(name.into()), - arguments: args - .into_iter() - .map(|expr| (var_store.fresh(), no_region(expr))) - .collect::)>>(), - } -} - -#[inline(always)] -fn record(fields: Vec<(Lowercase, Field)>, var_store: &mut VarStore) -> Expr { - let mut send_map = SendMap::default(); - for (k, v) in fields { - send_map.insert(k, v); - } - Expr::Record { - record_var: var_store.fresh(), - fields: send_map, - } -} - -#[inline(always)] -fn defn( - fn_name: Symbol, - args: Vec<(Variable, Symbol)>, - var_store: &mut VarStore, - body: Expr, - ret_var: Variable, -) -> Def { - let expr = defn_help(fn_name, args, var_store, body, ret_var); - - Def { - loc_pattern: Loc { - region: Region::zero(), - value: Pattern::Identifier(fn_name), - }, - loc_expr: Loc { - region: Region::zero(), - value: expr, - }, - expr_var: var_store.fresh(), - pattern_vars: SendMap::default(), - annotation: None, - } -} - -#[inline(always)] -fn num_bytes_to(symbol: Symbol, var_store: &mut VarStore, offset: i64, low_level: LowLevel) -> Def { - let len_var = var_store.fresh(); - let list_var = var_store.fresh(); - let elem_var = var_store.fresh(); - - let ret_var = var_store.fresh(); - let bool_var = var_store.fresh(); - let add_var = var_store.fresh(); - let cast_var = var_store.fresh(); - - // Perform a bounds check. If it passes, run LowLevel::low_level - let body = If { - cond_var: bool_var, - branch_var: var_store.fresh(), - branches: vec![( - // if-condition - no_region( - // index + offset < List.len list - RunLowLevel { - op: LowLevel::NumLt, - args: vec![ - ( - len_var, - RunLowLevel { - op: LowLevel::NumAdd, - args: vec![ - (add_var, Var(Symbol::ARG_2)), - ( - add_var, - RunLowLevel { - ret_var: cast_var, - args: vec![( - cast_var, - num(var_store.fresh(), offset, num_no_bound()), - )], - op: LowLevel::NumIntCast, - }, - ), - ], - ret_var: add_var, - }, - ), - ( - len_var, - RunLowLevel { - op: LowLevel::ListLen, - args: vec![(list_var, Var(Symbol::ARG_1))], - ret_var: len_var, - }, - ), - ], - ret_var: bool_var, - }, - ), - // then-branch - no_region( - // Ok - tag( - "Ok", - vec![RunLowLevel { - op: low_level, - args: vec![ - (list_var, Var(Symbol::ARG_1)), - (len_var, Var(Symbol::ARG_2)), - ], - ret_var: elem_var, - }], - var_store, - ), - ), - )], - final_else: Box::new( - // else-branch - no_region( - // Err - tag( - "Err", - vec![tag("OutOfBounds", Vec::new(), var_store)], - var_store, - ), - ), - ), - }; - - defn( - symbol, - vec![(list_var, Symbol::ARG_1), (len_var, Symbol::ARG_2)], - var_store, - body, - ret_var, - ) -} - -/// Box.box : a -> Box a -fn box_box(symbol: Symbol, var_store: &mut VarStore) -> Def { - lowlevel_1(symbol, LowLevel::BoxExpr, var_store) -} - -/// Box.unbox : Box a -> a -fn box_unbox(symbol: Symbol, var_store: &mut VarStore) -> Def { - lowlevel_1(symbol, LowLevel::UnboxExpr, var_store) -} - -#[inline(always)] -fn defn_help( - fn_name: Symbol, - args: Vec<(Variable, Symbol)>, - var_store: &mut VarStore, - body: Expr, - ret_var: Variable, -) -> Expr { - use crate::pattern::Pattern::*; - - let closure_args = args - .into_iter() - .map(|(var, symbol)| { - ( - var, - AnnotatedMark::new(var_store), - no_region(Identifier(symbol)), - ) - }) - .collect(); - - Closure(ClosureData { - function_type: var_store.fresh(), - closure_type: var_store.fresh(), - return_type: ret_var, - name: fn_name, - captured_symbols: Vec::new(), - recursive: Recursive::NotRecursive, - arguments: closure_args, - loc_body: Box::new(no_region(body)), - }) -} - -fn num_no_bound() -> NumBound { - NumBound::None -} - -fn int_no_bound() -> IntBound { - IntBound::None -} - -fn float_no_bound() -> FloatBound { - FloatBound::None -} - -#[inline(always)] -fn int(num_var: Variable, precision_var: Variable, i: I128, bound: IntBound) -> Expr -where - I128: Into, -{ - let ii = i.into(); - Int( - num_var, - precision_var, - ii.to_string().into_boxed_str(), - IntValue::I128(ii.to_ne_bytes()), - bound, - ) -} - -#[inline(always)] -fn frac(num_var: Variable, precision_var: Variable, f: f64, bound: FloatBound) -> Expr { - Float( - num_var, - precision_var, - f.to_string().into_boxed_str(), - f, - bound, - ) -} - -#[inline(always)] -fn num>(num_var: Variable, i: I, bound: NumBound) -> Expr { - let i = i.into(); - Num( - num_var, - i.to_string().into_boxed_str(), - IntValue::I128(i.to_ne_bytes()), - bound, - ) -} diff --git a/compiler/can/src/def.rs b/compiler/can/src/def.rs deleted file mode 100644 index 98297c3d6f..0000000000 --- a/compiler/can/src/def.rs +++ /dev/null @@ -1,2262 +0,0 @@ -use crate::abilities::AbilityMemberData; -use crate::abilities::MemberTypeInfo; -use crate::abilities::MemberVariables; -use crate::annotation::canonicalize_annotation; -use crate::annotation::find_type_def_symbols; -use crate::annotation::make_apply_symbol; -use crate::annotation::IntroducedVariables; -use crate::annotation::OwnedNamedOrAble; -use crate::env::Env; -use crate::expr::AnnotatedMark; -use crate::expr::ClosureData; -use crate::expr::Expr::{self, *}; -use crate::expr::{canonicalize_expr, Output, Recursive}; -use crate::pattern::{canonicalize_def_header_pattern, BindingsFromPattern, Pattern}; -use crate::procedure::References; -use crate::scope::create_alias; -use crate::scope::Scope; -use roc_collections::ReferenceMatrix; -use roc_collections::VecMap; -use roc_collections::{ImSet, MutMap, SendMap}; -use roc_module::ident::Ident; -use roc_module::ident::Lowercase; -use roc_module::symbol::IdentId; -use roc_module::symbol::ModuleId; -use roc_module::symbol::Symbol; -use roc_parse::ast; -use roc_parse::ast::AbilityMember; -use roc_parse::ast::ExtractSpaces; -use roc_parse::ast::TypeHeader; -use roc_parse::pattern::PatternType; -use roc_problem::can::ShadowKind; -use roc_problem::can::{CycleEntry, Problem, RuntimeError}; -use roc_region::all::{Loc, Region}; -use roc_types::subs::IllegalCycleMark; -use roc_types::subs::{VarStore, Variable}; -use roc_types::types::AliasCommon; -use roc_types::types::AliasKind; -use roc_types::types::AliasVar; -use roc_types::types::LambdaSet; -use roc_types::types::{Alias, Type}; -use std::fmt::Debug; - -#[derive(Clone, Debug)] -pub struct Def { - pub loc_pattern: Loc, - pub loc_expr: Loc, - pub expr_var: Variable, - pub pattern_vars: SendMap, - pub annotation: Option, -} - -impl Def { - pub fn region(&self) -> Region { - let head_region = match &self.annotation { - Some(ann) => { - if ann.region.start() < self.loc_pattern.region.start() { - ann.region - } else { - // Happens with annotation-only bodies like foo : T, since `T` is after the - // pattern. - self.loc_pattern.region - } - } - None => self.loc_pattern.region, - }; - Region::span_across(&head_region, &self.loc_expr.region) - } -} - -#[derive(Clone, Debug)] -pub struct Annotation { - pub signature: Type, - pub introduced_variables: IntroducedVariables, - pub aliases: SendMap, - pub region: Region, -} - -#[derive(Debug)] -pub(crate) struct CanDefs { - defs: Vec>, - def_ordering: DefOrdering, - aliases: VecMap, -} - -/// A Def that has had patterns and type annnotations canonicalized, -/// but no Expr canonicalization has happened yet. Also, it has had spaces -/// and nesting resolved, and knows whether annotations are standalone or not. -#[derive(Debug, Clone)] -enum PendingValueDef<'a> { - /// A standalone annotation with no body - AnnotationOnly( - &'a Loc>, - Loc, - &'a Loc>, - ), - /// A body with no type annotation - Body( - &'a Loc>, - Loc, - &'a Loc>, - ), - /// A body with a type annotation - TypedBody( - &'a Loc>, - Loc, - &'a Loc>, - &'a Loc>, - ), -} - -impl PendingValueDef<'_> { - fn loc_pattern(&self) -> &Loc { - match self { - PendingValueDef::AnnotationOnly(_, loc_pattern, _) => loc_pattern, - PendingValueDef::Body(_, loc_pattern, _) => loc_pattern, - PendingValueDef::TypedBody(_, loc_pattern, _, _) => loc_pattern, - } - } -} - -#[derive(Debug, Clone)] -enum PendingTypeDef<'a> { - /// A structural type alias, e.g. `Ints : List Int` - Alias { - name: Loc, - vars: Vec>, - ann: &'a Loc>, - }, - - /// An opaque type alias, e.g. `Age := U32`. - Opaque { - name: Loc, - vars: Vec>, - ann: &'a Loc>, - derived: Option<&'a Loc>>, - }, - - Ability { - name: Loc, - members: &'a [ast::AbilityMember<'a>], - }, - - /// An invalid alias, that is ignored in the rest of the pipeline - /// e.g. a definition like `MyAlias 1 : Int` - /// with an incorrect pattern - InvalidAlias { - #[allow(dead_code)] - kind: AliasKind, - symbol: Symbol, - region: Region, - }, - - /// An alias with a name that shadows another symbol - ShadowedAlias, - - /// An invalid ability, that is ignored in the rest of the pipeline. - /// E.g. a shadowed ability, or with a bad definition. - InvalidAbility { - symbol: Symbol, - region: Region, - }, - - AbilityNotOnToplevel, - AbilityShadows, -} - -impl PendingTypeDef<'_> { - fn introduction(&self) -> Option<(Symbol, Region)> { - match self { - PendingTypeDef::Alias { name, ann, .. } => { - let region = Region::span_across(&name.region, &ann.region); - - Some((name.value, region)) - } - PendingTypeDef::Opaque { - name, - vars: _, - ann, - derived, - } => { - let end = derived.map(|d| d.region).unwrap_or(ann.region); - let region = Region::span_across(&name.region, &end); - - Some((name.value, region)) - } - PendingTypeDef::Ability { name, .. } => Some((name.value, name.region)), - PendingTypeDef::InvalidAlias { symbol, region, .. } => Some((*symbol, *region)), - PendingTypeDef::ShadowedAlias { .. } => None, - PendingTypeDef::InvalidAbility { symbol, region } => Some((*symbol, *region)), - PendingTypeDef::AbilityNotOnToplevel => None, - PendingTypeDef::AbilityShadows => None, - } - } -} - -// See github.com/rtfeldman/roc/issues/800 for discussion of the large_enum_variant check. -#[derive(Clone, Debug)] -#[allow(clippy::large_enum_variant)] -pub enum Declaration { - Declare(Def), - DeclareRec(Vec, IllegalCycleMark), - Builtin(Def), - /// If we know a cycle is illegal during canonicalization. - /// Otherwise we will try to detect this during solving; see [`IllegalCycleMark`]. - InvalidCycle(Vec), -} - -impl Declaration { - pub fn def_count(&self) -> usize { - use Declaration::*; - match self { - Declare(_) => 1, - DeclareRec(defs, _) => defs.len(), - InvalidCycle { .. } => 0, - Builtin(_) => 0, - } - } - - pub fn region(&self) -> Region { - match self { - Declaration::Declare(def) => def.region(), - Declaration::DeclareRec(defs, _) => Region::span_across( - &defs.first().unwrap().region(), - &defs.last().unwrap().region(), - ), - Declaration::Builtin(def) => def.region(), - Declaration::InvalidCycle(cycles) => Region::span_across( - &cycles.first().unwrap().expr_region, - &cycles.last().unwrap().expr_region, - ), - } - } -} - -/// Returns a topologically sorted sequence of alias/opaque names -fn sort_type_defs_before_introduction( - referenced_symbols: VecMap>, -) -> Vec { - let capacity = referenced_symbols.len(); - let mut matrix = ReferenceMatrix::new(capacity); - - let (symbols, referenced) = referenced_symbols.unzip(); - - for (index, references) in referenced.iter().enumerate() { - for referenced in references { - match symbols.iter().position(|k| k == referenced) { - None => { /* not defined in this scope */ } - Some(ref_index) => matrix.set_row_col(index, ref_index, true), - } - } - } - - // find the strongly connected components and their relations - matrix - .strongly_connected_components_all() - .groups() - .flat_map(|group| group.iter_ones()) - .map(|index| symbols[index]) - .collect() -} - -#[inline(always)] -#[allow(clippy::too_many_arguments)] -fn canonicalize_alias<'a>( - env: &mut Env<'a>, - output: &mut Output, - var_store: &mut VarStore, - scope: &mut Scope, - pending_abilities_in_scope: &[Symbol], - - name: Loc, - ann: &'a Loc>, - vars: &[Loc], - kind: AliasKind, -) -> Result { - let symbol = name.value; - let can_ann = canonicalize_annotation( - env, - scope, - &ann.value, - ann.region, - var_store, - pending_abilities_in_scope, - ); - - // Record all the annotation's references in output.references.lookups - for symbol in can_ann.references { - output.references.insert_type_lookup(symbol); - } - - let mut can_vars: Vec> = Vec::with_capacity(vars.len()); - let mut is_phantom = false; - - let IntroducedVariables { - named, - able, - wildcards, - inferred, - .. - } = can_ann.introduced_variables; - - let mut named: Vec<_> = (named.into_iter().map(OwnedNamedOrAble::Named)) - .chain(able.into_iter().map(OwnedNamedOrAble::Able)) - .collect(); - for loc_lowercase in vars.iter() { - let opt_index = named - .iter() - .position(|nv| nv.ref_name() == &loc_lowercase.value); - match opt_index { - Some(index) => { - // This is a valid lowercase rigid var for the type def. - let named_variable = named.swap_remove(index); - let var = named_variable.variable(); - let opt_bound_ability = named_variable.opt_ability(); - let name = named_variable.name(); - - can_vars.push(Loc { - value: AliasVar { - name, - var, - opt_bound_ability, - }, - region: loc_lowercase.region, - }); - } - None => { - is_phantom = true; - - env.problems.push(Problem::PhantomTypeArgument { - typ: symbol, - variable_region: loc_lowercase.region, - variable_name: loc_lowercase.value.clone(), - }); - } - } - } - - if is_phantom { - // Bail out - return Err(()); - } - - let num_unbound = named.len() + wildcards.len() + inferred.len(); - if num_unbound > 0 { - let one_occurrence = named - .iter() - .map(|nv| Loc::at(nv.first_seen(), nv.variable())) - .chain(wildcards) - .chain(inferred) - .next() - .unwrap() - .region; - - env.problems.push(Problem::UnboundTypeVariable { - typ: symbol, - num_unbound, - one_occurrence, - kind, - }); - - // Bail out - return Err(()); - } - - Ok(create_alias( - symbol, - name.region, - can_vars.clone(), - can_ann.typ, - kind, - )) -} - -#[inline(always)] -#[allow(clippy::too_many_arguments)] -fn canonicalize_opaque<'a>( - env: &mut Env<'a>, - output: &mut Output, - var_store: &mut VarStore, - scope: &mut Scope, - pending_abilities_in_scope: &[Symbol], - - name: Loc, - ann: &'a Loc>, - vars: &[Loc], - derives: Option<&'a Loc>>, -) -> Result { - let alias = canonicalize_alias( - env, - output, - var_store, - scope, - pending_abilities_in_scope, - name, - ann, - vars, - AliasKind::Opaque, - )?; - - if let Some(derives) = derives { - let derives = derives.value.collection(); - - let mut can_derives = vec![]; - - for derived in derives.items { - let region = derived.region; - match derived.value.extract_spaces().item { - ast::TypeAnnotation::Apply(module_name, ident, []) => { - match make_apply_symbol(env, region, scope, module_name, ident) { - Ok(ability) if ability.is_builtin_ability() => { - can_derives.push(Loc::at(region, ability)); - } - Ok(_) => { - // Register the problem but keep going, we may still be able to compile the - // program even if a derive is missing. - env.problem(Problem::IllegalDerive(region)); - } - Err(_) => { - // This is bad apply; an error will have been reported for it - // already. - } - } - } - _ => { - // Register the problem but keep going, we may still be able to compile the - // program even if a derive is missing. - env.problem(Problem::IllegalDerive(region)); - } - } - } - - if !can_derives.is_empty() { - // Fresh instance of this opaque to be checked for derivability during solving. - let fresh_inst = Type::DelayedAlias(AliasCommon { - symbol: name.value, - type_arguments: alias - .type_variables - .iter() - .map(|_| Type::Variable(var_store.fresh())) - .collect(), - lambda_set_variables: alias - .lambda_set_variables - .iter() - .map(|_| LambdaSet(Type::Variable(var_store.fresh()))) - .collect(), - }); - - let old = output - .pending_derives - .insert(name.value, (fresh_inst, can_derives)); - debug_assert!(old.is_none()); - } - } - - Ok(alias) -} - -#[inline(always)] -pub(crate) fn canonicalize_defs<'a>( - env: &mut Env<'a>, - mut output: Output, - var_store: &mut VarStore, - scope: &mut Scope, - loc_defs: &'a [&'a Loc>], - pattern_type: PatternType, -) -> (CanDefs, Output, MutMap) { - // Canonicalizing defs while detecting shadowing involves a multi-step process: - // - // 1. Go through each of the patterns. - // 2. For each identifier pattern, get the scope.symbol() for the ident. (That symbol will use the home module for its module.) - // 3. If that symbol is already in scope, then we're about to shadow it. Error! - // 4. Otherwise, add it to the scope immediately, so we can detect shadowing within the same - // pattern (e.g. (Foo a a) = ...) - // 5. Add this canonicalized pattern and its corresponding ast::Expr to pending_exprs. - // 5. Once every pattern has been processed and added to scope, go back and canonicalize the exprs from - // pending_exprs, this time building up a canonical def for each one. - // - // This way, whenever any expr is doing lookups, it knows everything that's in scope - - // even defs that appear after it in the source. - // - // This naturally handles recursion too, because a given expr which refers - // to itself won't be processed until after its def has been added to scope. - - let num_defs = loc_defs.len(); - let mut pending_type_defs = Vec::with_capacity(num_defs); - let mut value_defs = Vec::with_capacity(num_defs); - - for loc_def in loc_defs { - match loc_def.value.unroll_def() { - Ok(type_def) => { - pending_type_defs.push(to_pending_type_def(env, type_def, scope, pattern_type)) - } - Err(value_def) => value_defs.push(Loc::at(loc_def.region, value_def)), - } - } - - if cfg!(debug_assertions) { - scope.register_debug_idents(); - } - - let (aliases, symbols_introduced) = canonicalize_type_defs( - env, - &mut output, - var_store, - scope, - pending_type_defs, - pattern_type, - ); - - // Now that we have the scope completely assembled, and shadowing resolved, - // we're ready to canonicalize any body exprs. - canonicalize_value_defs( - env, - output, - var_store, - scope, - &value_defs, - pattern_type, - aliases, - symbols_introduced, - ) -} - -#[inline(always)] -pub(crate) fn canonicalize_toplevel_defs<'a>( - env: &mut Env<'a>, - mut output: Output, - var_store: &mut VarStore, - scope: &mut Scope, - loc_defs: &'a mut roc_parse::ast::Defs<'a>, - pattern_type: PatternType, -) -> (CanDefs, Output, MutMap) { - // Canonicalizing defs while detecting shadowing involves a multi-step process: - // - // 1. Go through each of the patterns. - // 2. For each identifier pattern, get the scope.symbol() for the ident. (That symbol will use the home module for its module.) - // 3. If that symbol is already in scope, then we're about to shadow it. Error! - // 4. Otherwise, add it to the scope immediately, so we can detect shadowing within the same - // pattern (e.g. (Foo a a) = ...) - // 5. Add this canonicalized pattern and its corresponding ast::Expr to pending_exprs. - // 5. Once every pattern has been processed and added to scope, go back and canonicalize the exprs from - // pending_exprs, this time building up a canonical def for each one. - // - // This way, whenever any expr is doing lookups, it knows everything that's in scope - - // even defs that appear after it in the source. - // - // This naturally handles recursion too, because a given expr which refers - // to itself won't be processed until after its def has been added to scope. - - let mut pending_type_defs = Vec::with_capacity(loc_defs.type_defs.len()); - let mut value_defs = Vec::with_capacity(loc_defs.value_defs.len()); - - for (index, either_index) in loc_defs.tags.iter().enumerate() { - match either_index.split() { - Ok(type_index) => { - let type_def = &loc_defs.type_defs[type_index.index()]; - pending_type_defs.push(to_pending_type_def(env, type_def, scope, pattern_type)); - } - Err(value_index) => { - let value_def = &loc_defs.value_defs[value_index.index()]; - let region = loc_defs.regions[index]; - value_defs.push(Loc::at(region, value_def)); - } - } - } - - if cfg!(debug_assertions) { - scope.register_debug_idents(); - } - - let (aliases, symbols_introduced) = canonicalize_type_defs( - env, - &mut output, - var_store, - scope, - pending_type_defs, - pattern_type, - ); - - // Now that we have the scope completely assembled, and shadowing resolved, - // we're ready to canonicalize any body exprs. - canonicalize_value_defs( - env, - output, - var_store, - scope, - &value_defs, - pattern_type, - aliases, - symbols_introduced, - ) -} - -#[allow(clippy::too_many_arguments)] -fn canonicalize_value_defs<'a>( - env: &mut Env<'a>, - mut output: Output, - var_store: &mut VarStore, - scope: &mut Scope, - value_defs: &[Loc<&'a roc_parse::ast::ValueDef<'a>>], - pattern_type: PatternType, - mut aliases: VecMap, - mut symbols_introduced: MutMap, -) -> (CanDefs, Output, MutMap) { - // Canonicalize all the patterns, record shadowing problems, and store - // the ast::Expr values in pending_exprs for further canonicalization - // once we've finished assembling the entire scope. - let mut pending_value_defs = Vec::with_capacity(value_defs.len()); - for loc_def in value_defs { - let mut new_output = Output::default(); - match to_pending_value_def( - env, - var_store, - loc_def.value, - scope, - &mut new_output, - pattern_type, - ) { - None => { /* skip */ } - Some(pending_def) => { - // Record the ast::Expr for later. We'll do another pass through these - // once we have the entire scope assembled. If we were to canonicalize - // the exprs right now, they wouldn't have symbols in scope from defs - // that get would have gotten added later in the defs list! - pending_value_defs.push(pending_def); - output.union(new_output); - } - } - } - - let mut symbol_to_index: Vec<(IdentId, u32)> = Vec::with_capacity(pending_value_defs.len()); - - for (def_index, pending_def) in pending_value_defs.iter().enumerate() { - for (s, r) in BindingsFromPattern::new(pending_def.loc_pattern()) { - // store the top-level defs, used to ensure that closures won't capture them - if let PatternType::TopLevelDef = pattern_type { - env.top_level_symbols.insert(s); - } - - symbols_introduced.insert(s, r); - - debug_assert_eq!(env.home, s.module_id()); - debug_assert!( - !symbol_to_index.iter().any(|(id, _)| *id == s.ident_id()), - "{:?}", - s - ); - - symbol_to_index.push((s.ident_id(), def_index as u32)); - } - } - - let capacity = pending_value_defs.len(); - let mut defs = Vec::with_capacity(capacity); - let mut def_ordering = DefOrdering::from_symbol_to_id(env.home, symbol_to_index, capacity); - - for (def_id, pending_def) in pending_value_defs.into_iter().enumerate() { - let temp_output = canonicalize_pending_value_def( - env, - pending_def, - output, - scope, - var_store, - pattern_type, - &mut aliases, - ); - - output = temp_output.output; - - defs.push(Some(temp_output.def)); - - def_ordering.insert_symbol_references(def_id as u32, &temp_output.references) - } - - let can_defs = CanDefs { - defs, - def_ordering, - aliases, - }; - - (can_defs, output, symbols_introduced) -} - -fn canonicalize_type_defs<'a>( - env: &mut Env<'a>, - output: &mut Output, - var_store: &mut VarStore, - scope: &mut Scope, - pending_type_defs: Vec>, - pattern_type: PatternType, -) -> (VecMap, MutMap) { - enum TypeDef<'a> { - Alias( - Loc, - Vec>, - &'a Loc>, - ), - Opaque( - Loc, - Vec>, - &'a Loc>, - Option<&'a Loc>>, - ), - Ability(Loc, &'a [AbilityMember<'a>]), - } - - let mut type_defs = MutMap::default(); - let mut pending_abilities_in_scope = Vec::new(); - - let mut referenced_type_symbols = VecMap::default(); - - // Determine which idents we introduced in the course of this process. - let mut symbols_introduced = MutMap::default(); - - for pending_def in pending_type_defs.into_iter() { - if let Some((symbol, region)) = pending_def.introduction() { - symbols_introduced.insert(symbol, region); - } - - match pending_def { - PendingTypeDef::Alias { name, vars, ann } => { - let referenced_symbols = find_type_def_symbols(scope, &ann.value); - - referenced_type_symbols.insert(name.value, referenced_symbols); - - type_defs.insert(name.value, TypeDef::Alias(name, vars, ann)); - } - PendingTypeDef::Opaque { - name, - vars, - ann, - derived, - } => { - let referenced_symbols = find_type_def_symbols(scope, &ann.value); - - referenced_type_symbols.insert(name.value, referenced_symbols); - // Don't need to insert references for derived types, because these can only contain - // builtin abilities, and hence do not affect the type def sorting. We'll insert - // references of usages when canonicalizing the derives. - - type_defs.insert(name.value, TypeDef::Opaque(name, vars, ann, derived)); - } - PendingTypeDef::Ability { name, members } => { - let mut referenced_symbols = Vec::with_capacity(2); - - for member in members.iter() { - // Add the referenced type symbols of each member function. We need to make - // sure those are processed first before we resolve the whole ability - // definition. - referenced_symbols.extend(find_type_def_symbols(scope, &member.typ.value)); - } - - referenced_type_symbols.insert(name.value, referenced_symbols); - type_defs.insert(name.value, TypeDef::Ability(name, members)); - pending_abilities_in_scope.push(name.value); - } - PendingTypeDef::InvalidAlias { .. } - | PendingTypeDef::InvalidAbility { .. } - | PendingTypeDef::AbilityShadows - | PendingTypeDef::ShadowedAlias { .. } - | PendingTypeDef::AbilityNotOnToplevel => { /* ignore */ } - } - } - - let sorted = sort_type_defs_before_introduction(referenced_type_symbols); - let mut aliases = VecMap::default(); - let mut abilities = MutMap::default(); - - for type_name in sorted { - match type_defs.remove(&type_name).unwrap() { - TypeDef::Alias(name, vars, ann) => { - let alias = canonicalize_alias( - env, - output, - var_store, - scope, - &pending_abilities_in_scope, - name, - ann, - &vars, - AliasKind::Structural, - ); - - if let Ok(alias) = alias { - aliases.insert(name.value, alias); - } - } - - TypeDef::Opaque(name, vars, ann, derived) => { - let alias_and_derives = canonicalize_opaque( - env, - output, - var_store, - scope, - &pending_abilities_in_scope, - name, - ann, - &vars, - derived, - ); - - if let Ok(alias) = alias_and_derives { - aliases.insert(name.value, alias); - } - } - - TypeDef::Ability(name, members) => { - // For now we enforce that aliases cannot reference abilities, so let's wait to - // resolve ability definitions until aliases are resolved and in scope below. - abilities.insert(name.value, (name, members)); - } - } - } - - // Now that we know the alias dependency graph, we can try to insert recursion variables - // where aliases are recursive tag unions, or detect illegal recursions. - let aliases = correct_mutual_recursive_type_alias(env, aliases, var_store); - - for (symbol, alias) in aliases.iter() { - scope.add_alias( - *symbol, - alias.region, - alias.type_variables.clone(), - alias.typ.clone(), - alias.kind, - ); - } - - // Resolve all pending abilities, to add them to scope. - resolve_abilities( - env, - output, - var_store, - scope, - abilities, - &pending_abilities_in_scope, - pattern_type, - ); - - (aliases, symbols_introduced) -} - -/// Resolve all pending abilities, to add them to scope. -#[allow(clippy::too_many_arguments)] -fn resolve_abilities<'a>( - env: &mut Env<'a>, - output: &mut Output, - var_store: &mut VarStore, - scope: &mut Scope, - abilities: MutMap, &[AbilityMember])>, - pending_abilities_in_scope: &[Symbol], - pattern_type: PatternType, -) { - for (loc_ability_name, members) in abilities.into_values() { - let mut can_members = Vec::with_capacity(members.len()); - - for member in members { - let member_annot = canonicalize_annotation( - env, - scope, - &member.typ.value, - member.typ.region, - var_store, - pending_abilities_in_scope, - ); - - // Record all the annotation's references in output.references.lookups - for symbol in member_annot.references { - output.references.insert_type_lookup(symbol); - } - - let name_region = member.name.region; - let member_name = member.name.extract_spaces().item; - - let member_sym = match scope.introduce(member_name.into(), name_region) { - Ok(sym) => sym, - Err((original_region, shadow, _new_symbol)) => { - env.problem(roc_problem::can::Problem::Shadowing { - original_region, - shadow, - kind: ShadowKind::Variable, - }); - // Pretend the member isn't a part of the ability - continue; - } - }; - - if pattern_type == PatternType::TopLevelDef { - env.top_level_symbols.insert(member_sym); - } - - // What variables in the annotation are bound to the parent ability, and what variables - // are bound to some other ability? - let (variables_bound_to_ability, _variables_bound_to_other_abilities): ( - Vec<_>, - Vec<_>, - ) = member_annot - .introduced_variables - .able - .iter() - .partition(|av| av.ability == loc_ability_name.value); - - let var_bound_to_ability = match variables_bound_to_ability.as_slice() { - [one] => one.variable, - [] => { - // There are no variables bound to the parent ability - then this member doesn't - // need to be a part of the ability. - env.problem(Problem::AbilityMemberMissingHasClause { - member: member_sym, - ability: loc_ability_name.value, - region: name_region, - }); - // Pretend the member isn't a part of the ability - continue; - } - [..] => { - // There is more than one variable bound to the member signature, so something like - // Eq has eq : a, b -> Bool | a has Eq, b has Eq - // We have no way of telling what type implements a particular instance of Eq in - // this case (a or b?), so disallow it. - let span_has_clauses = Region::across_all( - variables_bound_to_ability.iter().map(|v| &v.first_seen), - ); - let bound_var_names = variables_bound_to_ability - .iter() - .map(|v| v.name.clone()) - .collect(); - env.problem(Problem::AbilityMemberMultipleBoundVars { - member: member_sym, - ability: loc_ability_name.value, - span_has_clauses, - bound_var_names, - }); - // Pretend the member isn't a part of the ability - continue; - } - }; - - // The introduced variables are good; add them to the output. - output - .introduced_variables - .union(&member_annot.introduced_variables); - - let iv = member_annot.introduced_variables; - - let variables = MemberVariables { - able_vars: iv.collect_able(), - rigid_vars: iv.collect_rigid(), - flex_vars: iv.collect_flex(), - }; - - let signature = { - let mut signature = member_annot.typ; - signature - .instantiate_lambda_sets_as_unspecialized(var_bound_to_ability, member_sym); - signature - }; - - can_members.push(( - member_sym, - AbilityMemberData { - parent_ability: loc_ability_name.value, - region: name_region, - typ: MemberTypeInfo::Local { - variables, - signature, - signature_var: var_store.fresh(), - }, - }, - )); - } - - // Store what symbols a type must define implementations for to have this ability. - scope - .abilities_store - .register_ability(loc_ability_name.value, can_members); - } -} - -#[derive(Debug)] -struct DefOrdering { - home: ModuleId, - symbol_to_id: Vec<(IdentId, u32)>, - - // an length x length matrix indicating who references who - references: ReferenceMatrix, - - // references without looking into closure bodies. - // Used to spot definitely-wrong recursion - direct_references: ReferenceMatrix, -} - -impl DefOrdering { - fn from_symbol_to_id( - home: ModuleId, - symbol_to_id: Vec<(IdentId, u32)>, - capacity: usize, - ) -> Self { - // NOTE: because of `Pair a b = someDef` patterns, we can have more symbols than defs - // but because `_ = someDef` we can also have more defs than symbols - - Self { - home, - symbol_to_id, - references: ReferenceMatrix::new(capacity), - direct_references: ReferenceMatrix::new(capacity), - } - } - - fn insert_symbol_references(&mut self, def_id: u32, def_references: &DefReferences) { - match def_references { - DefReferences::Value(references) => { - let it = references.value_lookups().chain(references.calls()); - - for referenced in it { - if let Some(ref_id) = self.get_id(*referenced) { - self.references - .set_row_col(def_id as usize, ref_id as usize, true); - - self.direct_references - .set_row_col(def_id as usize, ref_id as usize, true); - } - } - } - DefReferences::Function(references) => { - let it = references.value_lookups().chain(references.calls()); - - for referenced in it { - if let Some(ref_id) = self.get_id(*referenced) { - self.references - .set_row_col(def_id as usize, ref_id as usize, true); - } - } - } - DefReferences::AnnotationWithoutBody => { - // annotatations without bodies don't reference any other definitions - } - } - } - - fn get_id(&self, symbol: Symbol) -> Option { - if symbol.module_id() != self.home { - return None; - } - - let target = symbol.ident_id(); - - for (ident_id, def_id) in self.symbol_to_id.iter() { - if target == *ident_id { - return Some(*def_id); - } - } - - None - } - - fn get_symbol(&self, id: usize) -> Option { - for (ident_id, def_id) in self.symbol_to_id.iter() { - if id as u32 == *def_id { - return Some(Symbol::new(self.home, *ident_id)); - } - } - - None - } -} - -#[inline(always)] -pub(crate) fn sort_can_defs( - env: &mut Env<'_>, - var_store: &mut VarStore, - defs: CanDefs, - mut output: Output, -) -> (Vec, Output) { - let CanDefs { - mut defs, - def_ordering, - aliases, - } = defs; - - for (symbol, alias) in aliases.into_iter() { - output.aliases.insert(symbol, alias); - } - - macro_rules! take_def { - ($index:expr) => { - match defs[$index].take() { - Some(def) => def, - None => { - // NOTE: a `_ = someDef` can mean we don't have a symbol here - let symbol = def_ordering.get_symbol($index); - - roc_error_macros::internal_error!("def not available {:?}", symbol) - } - } - }; - } - - // We first perform SCC based on any reference, both variable usage and calls - // considering both value definitions and function bodies. This will spot any - // recursive relations between any 2 definitions. - let sccs = def_ordering.references.strongly_connected_components_all(); - - let mut declarations = Vec::with_capacity(defs.len()); - - for group in sccs.groups() { - if group.count_ones() == 1 { - // a group with a single Def, nice and simple - let index = group.iter_ones().next().unwrap(); - - let def = take_def!(index); - let is_specialization = matches!( - def.loc_pattern.value, - Pattern::AbilityMemberSpecialization { .. } - ); - - let declaration = if def_ordering.direct_references.get_row_col(index, index) { - // a definition like `x = x + 1`, which is invalid in roc - let symbol = def_ordering.get_symbol(index).unwrap(); - - let entries = vec![make_cycle_entry(symbol, &def)]; - - let problem = Problem::RuntimeError(RuntimeError::CircularDef(entries.clone())); - env.problem(problem); - - Declaration::InvalidCycle(entries) - } else if def_ordering.references.get_row_col(index, index) { - debug_assert!(!is_specialization, "Self-recursive specializations can only be determined during solving - but it was determined for {:?} now, that's a bug!", def); - - // this function calls itself, and must be typechecked as a recursive def - Declaration::DeclareRec(vec![mark_def_recursive(def)], IllegalCycleMark::empty()) - } else { - Declaration::Declare(def) - }; - - declarations.push(declaration); - } else { - // There is something recursive going on between the Defs of this group. - // Now we use the direct_references to see if it is clearly invalid recursion, e.g. - // - // x = y - // y = x - // - // We allow indirect recursion (behind a lambda), e.g. - // - // boom = \{} -> boom {} - // - // In general we cannot spot faulty recursion (halting problem), so this is our - // purely-syntactic heuristic. We'll have a second attempt once we know the types in - // the cycle. - let direct_sccs = def_ordering - .direct_references - .strongly_connected_components_subset(group); - - debug_assert!( - !group.iter_ones().any(|index| matches!((&defs[index]).as_ref().unwrap().loc_pattern.value, Pattern::AbilityMemberSpecialization{..})), - "A specialization is involved in a recursive cycle - this should not be knowable until solving"); - - let declaration = if direct_sccs.groups().count() == 1 { - // all defs are part of the same direct cycle, that is invalid! - let mut entries = Vec::with_capacity(group.count_ones()); - - for index in group.iter_ones() { - let def = take_def!(index); - let symbol = def_ordering.get_symbol(index).unwrap(); - - entries.push(make_cycle_entry(symbol, &def)) - } - - let problem = Problem::RuntimeError(RuntimeError::CircularDef(entries.clone())); - env.problem(problem); - - Declaration::InvalidCycle(entries) - } else { - let rec_defs = group - .iter_ones() - .map(|index| mark_def_recursive(take_def!(index))) - .collect(); - - Declaration::DeclareRec(rec_defs, IllegalCycleMark::new(var_store)) - }; - - declarations.push(declaration); - } - } - - (declarations, output) -} - -fn mark_def_recursive(mut def: Def) -> Def { - if let Closure(ClosureData { - recursive: recursive @ Recursive::NotRecursive, - .. - }) = &mut def.loc_expr.value - { - *recursive = Recursive::Recursive - } - - def -} - -fn make_cycle_entry(symbol: Symbol, def: &Def) -> CycleEntry { - CycleEntry { - symbol, - symbol_region: def.loc_pattern.region, - expr_region: def.loc_expr.region, - } -} - -fn pattern_to_vars_by_symbol( - vars_by_symbol: &mut SendMap, - pattern: &Pattern, - expr_var: Variable, -) { - use Pattern::*; - match pattern { - Identifier(symbol) | Shadowed(_, _, symbol) => { - vars_by_symbol.insert(*symbol, expr_var); - } - - AbilityMemberSpecialization { - ident, - specializes: _, - } => { - vars_by_symbol.insert(*ident, expr_var); - } - - AppliedTag { arguments, .. } => { - for (var, nested) in arguments { - pattern_to_vars_by_symbol(vars_by_symbol, &nested.value, *var); - } - } - - UnwrappedOpaque { - argument, opaque, .. - } => { - let (var, nested) = &**argument; - pattern_to_vars_by_symbol(vars_by_symbol, &nested.value, *var); - vars_by_symbol.insert(*opaque, expr_var); - } - - RecordDestructure { destructs, .. } => { - for destruct in destructs { - vars_by_symbol.insert(destruct.value.symbol, destruct.value.var); - } - } - - NumLiteral(..) - | IntLiteral(..) - | FloatLiteral(..) - | StrLiteral(_) - | SingleQuote(_) - | Underscore - | MalformedPattern(_, _) - | UnsupportedPattern(_) - | OpaqueNotInScope(..) => {} - } -} - -fn single_can_def( - loc_can_pattern: Loc, - loc_can_expr: Loc, - expr_var: Variable, - opt_loc_annotation: Option>, - pattern_vars: SendMap, -) -> Def { - let def_annotation = opt_loc_annotation.map(|loc_annotation| Annotation { - signature: loc_annotation.value.typ, - introduced_variables: loc_annotation.value.introduced_variables, - aliases: loc_annotation.value.aliases, - region: loc_annotation.region, - }); - - Def { - expr_var, - loc_pattern: loc_can_pattern, - loc_expr: Loc { - region: loc_can_expr.region, - value: loc_can_expr.value, - }, - pattern_vars, - annotation: def_annotation, - } -} - -// Functions' references don't count in defs. -// See 3d5a2560057d7f25813112dfa5309956c0f9e6a9 and its -// parent commit for the bug this fixed! -enum DefReferences { - /// A value may not reference itself - Value(References), - - /// If the def is a function, different rules apply (it can call itself) - Function(References), - - /// An annotation without a body references no other defs - AnnotationWithoutBody, -} - -struct DefOutput { - output: Output, - def: Def, - references: DefReferences, -} - -// TODO trim down these arguments! -#[allow(clippy::too_many_arguments)] -#[allow(clippy::cognitive_complexity)] -fn canonicalize_pending_value_def<'a>( - env: &mut Env<'a>, - pending_def: PendingValueDef<'a>, - mut output: Output, - scope: &mut Scope, - var_store: &mut VarStore, - pattern_type: PatternType, - aliases: &mut VecMap, -) -> DefOutput { - use PendingValueDef::*; - - // All abilities should be resolved by the time we're canonicalizing value defs. - let pending_abilities_in_scope = &[]; - - let output = match pending_def { - AnnotationOnly(_, loc_can_pattern, loc_ann) => { - // Make types for the body expr, even if we won't end up having a body. - let expr_var = var_store.fresh(); - let mut vars_by_symbol = SendMap::default(); - - // annotation sans body cannot introduce new rigids that are visible in other annotations - // but the rigids can show up in type error messages, so still register them - let type_annotation = canonicalize_annotation( - env, - scope, - &loc_ann.value, - loc_ann.region, - var_store, - pending_abilities_in_scope, - ); - - // Record all the annotation's references in output.references.lookups - type_annotation.add_to( - aliases, - &mut output.references, - &mut output.introduced_variables, - ); - - pattern_to_vars_by_symbol(&mut vars_by_symbol, &loc_can_pattern.value, expr_var); - - let arity = type_annotation.typ.arity(); - - let problem = match &loc_can_pattern.value { - Pattern::Identifier(symbol) => RuntimeError::NoImplementationNamed { - def_symbol: *symbol, - }, - Pattern::Shadowed(region, loc_ident, _new_symbol) => RuntimeError::Shadowing { - original_region: *region, - shadow: loc_ident.clone(), - kind: ShadowKind::Variable, - }, - _ => RuntimeError::NoImplementation, - }; - - // Fabricate a body for this annotation, that will error at runtime - let value = Expr::RuntimeError(problem); - let is_closure = arity > 0; - let loc_can_expr = if !is_closure { - Loc { - value, - region: loc_ann.region, - } - } else { - let symbol = scope.gen_unique_symbol(); - - // generate a fake pattern for each argument. this makes signatures - // that are functions only crash when they are applied. - let mut underscores = Vec::with_capacity(arity); - - for _ in 0..arity { - let underscore: Loc = Loc { - value: Pattern::Underscore, - region: Region::zero(), - }; - - underscores.push(( - var_store.fresh(), - AnnotatedMark::known_exhaustive(), - underscore, - )); - } - - let body_expr = Loc { - value, - region: loc_ann.region, - }; - - Loc { - value: Closure(ClosureData { - function_type: var_store.fresh(), - closure_type: var_store.fresh(), - return_type: var_store.fresh(), - name: symbol, - captured_symbols: Vec::new(), - recursive: Recursive::NotRecursive, - arguments: underscores, - loc_body: Box::new(body_expr), - }), - region: loc_ann.region, - } - }; - - let def = single_can_def( - loc_can_pattern, - loc_can_expr, - expr_var, - Some(Loc::at(loc_ann.region, type_annotation)), - vars_by_symbol.clone(), - ); - - DefOutput { - output, - references: DefReferences::AnnotationWithoutBody, - def, - } - } - - TypedBody(_loc_pattern, loc_can_pattern, loc_ann, loc_expr) => { - let type_annotation = canonicalize_annotation( - env, - scope, - &loc_ann.value, - loc_ann.region, - var_store, - pending_abilities_in_scope, - ); - - // Record all the annotation's references in output.references.lookups - type_annotation.add_to( - aliases, - &mut output.references, - &mut output.introduced_variables, - ); - - canonicalize_pending_body( - env, - output, - scope, - var_store, - loc_can_pattern, - loc_expr, - Some(Loc::at(loc_ann.region, type_annotation)), - ) - } - Body(_loc_pattern, loc_can_pattern, loc_expr) => { - // - canonicalize_pending_body( - env, - output, - scope, - var_store, - loc_can_pattern, - loc_expr, - None, - ) - } - }; - - // Disallow ability specializations that aren't on the toplevel (note: we might loosen this - // restriction later on). - if pattern_type != PatternType::TopLevelDef { - if let Loc { - value: Pattern::AbilityMemberSpecialization { specializes, .. }, - region, - } = output.def.loc_pattern - { - env.problem(Problem::NestedSpecialization(specializes, region)); - } - } - - output -} - -// TODO trim down these arguments! -#[allow(clippy::too_many_arguments)] -#[allow(clippy::cognitive_complexity)] -fn canonicalize_pending_body<'a>( - env: &mut Env<'a>, - mut output: Output, - scope: &mut Scope, - var_store: &mut VarStore, - - loc_can_pattern: Loc, - loc_expr: &'a Loc, - - opt_loc_annotation: Option>, -) -> DefOutput { - // We treat closure definitions `foo = \a, b -> ...` differently from other body expressions, - // because they need more bookkeeping (for tail calls, closure captures, etc.) - // - // Only defs of the form `foo = ...` can be closure declarations or self tail calls. - let (loc_can_expr, def_references) = { - match (&loc_can_pattern.value, &loc_expr.value) { - ( - Pattern::Identifier(defined_symbol) - | Pattern::AbilityMemberSpecialization { - ident: defined_symbol, - .. - }, - ast::Expr::Closure(arguments, body), - ) => { - // bookkeeping for tail-call detection. - let outer_tailcallable = env.tailcallable_symbol; - env.tailcallable_symbol = Some(*defined_symbol); - - let (mut closure_data, can_output) = crate::expr::canonicalize_closure( - env, - var_store, - scope, - arguments, - body, - Some(*defined_symbol), - ); - - // reset the tailcallable_symbol - env.tailcallable_symbol = outer_tailcallable; - - // The closure is self tail recursive iff it tail calls itself (by defined name). - let is_recursive = match can_output.tail_call { - Some(tail_symbol) if tail_symbol == *defined_symbol => Recursive::TailRecursive, - _ => Recursive::NotRecursive, - }; - - closure_data.recursive = is_recursive; - closure_data.name = *defined_symbol; - - let loc_can_expr = Loc::at(loc_expr.region, Expr::Closure(closure_data)); - - let def_references = DefReferences::Function(can_output.references.clone()); - output.union(can_output); - - (loc_can_expr, def_references) - } - - _ => { - let (loc_can_expr, can_output) = - canonicalize_expr(env, var_store, scope, loc_expr.region, &loc_expr.value); - - let def_references = DefReferences::Value(can_output.references.clone()); - output.union(can_output); - - (loc_can_expr, def_references) - } - } - }; - - let expr_var = var_store.fresh(); - let mut vars_by_symbol = SendMap::default(); - - pattern_to_vars_by_symbol(&mut vars_by_symbol, &loc_can_pattern.value, expr_var); - - let def = single_can_def( - loc_can_pattern, - loc_can_expr, - expr_var, - opt_loc_annotation, - vars_by_symbol, - ); - - DefOutput { - output, - references: def_references, - def, - } -} - -#[inline(always)] -pub fn can_defs_with_return<'a>( - env: &mut Env<'a>, - var_store: &mut VarStore, - scope: &mut Scope, - loc_defs: &'a [&'a Loc>], - loc_ret: &'a Loc>, -) -> (Expr, Output) { - let (unsorted, defs_output, symbols_introduced) = canonicalize_defs( - env, - Output::default(), - var_store, - scope, - loc_defs, - PatternType::DefExpr, - ); - - // The def as a whole is a tail call iff its return expression is a tail call. - // Use its output as a starting point because its tail_call already has the right answer! - let (ret_expr, mut output) = - canonicalize_expr(env, var_store, scope, loc_ret.region, &loc_ret.value); - - output - .introduced_variables - .union(&defs_output.introduced_variables); - output.references.union_mut(&defs_output.references); - - // Now that we've collected all the references, check to see if any of the new idents - // we defined went unused by the return expression. If any were unused, report it. - for (symbol, region) in symbols_introduced { - if !output.references.has_type_or_value_lookup(symbol) - && !scope.abilities_store.is_specialization_name(symbol) - { - env.problem(Problem::UnusedDef(symbol, region)); - } - } - - let (declarations, output) = sort_can_defs(env, var_store, unsorted, output); - - let mut loc_expr: Loc = ret_expr; - - for declaration in declarations.into_iter().rev() { - loc_expr = decl_to_let(declaration, loc_expr); - } - - (loc_expr.value, output) -} - -fn decl_to_let(decl: Declaration, loc_ret: Loc) -> Loc { - match decl { - Declaration::Declare(def) => { - let region = Region::span_across(&def.loc_pattern.region, &loc_ret.region); - let expr = Expr::LetNonRec(Box::new(def), Box::new(loc_ret)); - Loc::at(region, expr) - } - Declaration::DeclareRec(defs, cycle_mark) => { - let region = Region::span_across(&defs[0].loc_pattern.region, &loc_ret.region); - let expr = Expr::LetRec(defs, Box::new(loc_ret), cycle_mark); - Loc::at(region, expr) - } - Declaration::InvalidCycle(entries) => { - Loc::at_zero(Expr::RuntimeError(RuntimeError::CircularDef(entries))) - } - Declaration::Builtin(_) => { - // Builtins should only be added to top-level decls, not to let-exprs! - unreachable!() - } - } -} - -fn to_pending_alias_or_opaque<'a>( - env: &mut Env<'a>, - scope: &mut Scope, - name: &'a Loc<&'a str>, - vars: &'a [Loc>], - ann: &'a Loc>, - opt_derived: Option<&'a Loc>>, - kind: AliasKind, -) -> PendingTypeDef<'a> { - let shadow_kind = match kind { - AliasKind::Structural => ShadowKind::Alias, - AliasKind::Opaque => ShadowKind::Opaque, - }; - - let region = Region::span_across(&name.region, &ann.region); - - match scope.introduce_without_shadow_symbol(&Ident::from(name.value), region) { - Ok(symbol) => { - let mut can_rigids: Vec> = Vec::with_capacity(vars.len()); - - for loc_var in vars.iter() { - match loc_var.value { - ast::Pattern::Identifier(name) - if name.chars().next().unwrap().is_lowercase() => - { - let lowercase = Lowercase::from(name); - can_rigids.push(Loc { - value: lowercase, - region: loc_var.region, - }); - } - _ => { - // any other pattern in this position is a syntax error. - let problem = Problem::InvalidAliasRigid { - alias_name: symbol, - region: loc_var.region, - }; - env.problems.push(problem); - - return PendingTypeDef::InvalidAlias { - kind, - symbol, - region, - }; - } - } - } - - let name = Loc { - region: name.region, - value: symbol, - }; - - match kind { - AliasKind::Structural => PendingTypeDef::Alias { - name, - vars: can_rigids, - ann, - }, - AliasKind::Opaque => PendingTypeDef::Opaque { - name, - vars: can_rigids, - ann, - derived: opt_derived, - }, - } - } - - Err((original_region, loc_shadowed_symbol)) => { - env.problem(Problem::Shadowing { - original_region, - shadow: loc_shadowed_symbol, - kind: shadow_kind, - }); - - PendingTypeDef::ShadowedAlias - } - } -} - -fn to_pending_type_def<'a>( - env: &mut Env<'a>, - def: &'a ast::TypeDef<'a>, - scope: &mut Scope, - pattern_type: PatternType, -) -> PendingTypeDef<'a> { - use ast::TypeDef::*; - - match def { - Alias { - header: TypeHeader { name, vars }, - ann, - } => to_pending_alias_or_opaque(env, scope, name, vars, ann, None, AliasKind::Structural), - Opaque { - header: TypeHeader { name, vars }, - typ: ann, - derived, - } => to_pending_alias_or_opaque( - env, - scope, - name, - vars, - ann, - derived.as_ref(), - AliasKind::Opaque, - ), - - Ability { - header, members, .. - } if pattern_type != PatternType::TopLevelDef => { - let header_region = header.region(); - let region = Region::span_across( - &header_region, - &members.last().map(|m| m.region()).unwrap_or(header_region), - ); - env.problem(Problem::AbilityNotOnToplevel { region }); - - PendingTypeDef::AbilityNotOnToplevel - } - - Ability { - header: TypeHeader { name, vars }, - members, - loc_has: _, - } => { - let name = match scope - .introduce_without_shadow_symbol(&Ident::from(name.value), name.region) - { - Ok(symbol) => Loc::at(name.region, symbol), - Err((original_region, shadowed_symbol)) => { - env.problem(Problem::Shadowing { - original_region, - shadow: shadowed_symbol, - kind: ShadowKind::Ability, - }); - return PendingTypeDef::AbilityShadows; - } - }; - - if !vars.is_empty() { - // Disallow ability type arguments, at least for now. - let variables_region = Region::across_all(vars.iter().map(|v| &v.region)); - - env.problem(Problem::AbilityHasTypeVariables { - name: name.value, - variables_region, - }); - return PendingTypeDef::InvalidAbility { - symbol: name.value, - region: name.region, - }; - } - - PendingTypeDef::Ability { - name, - // We'll handle adding the member symbols later on when we do all value defs. - members, - } - } - } -} - -fn to_pending_value_def<'a>( - env: &mut Env<'a>, - var_store: &mut VarStore, - def: &'a ast::ValueDef<'a>, - scope: &mut Scope, - output: &mut Output, - pattern_type: PatternType, -) -> Option> { - use ast::ValueDef::*; - - match def { - Annotation(loc_pattern, loc_ann) => { - // This takes care of checking for shadowing and adding idents to scope. - let loc_can_pattern = canonicalize_def_header_pattern( - env, - var_store, - scope, - output, - pattern_type, - &loc_pattern.value, - loc_pattern.region, - ); - - Some(PendingValueDef::AnnotationOnly( - loc_pattern, - loc_can_pattern, - loc_ann, - )) - } - Body(loc_pattern, loc_expr) => { - // This takes care of checking for shadowing and adding idents to scope. - let loc_can_pattern = canonicalize_def_header_pattern( - env, - var_store, - scope, - output, - pattern_type, - &loc_pattern.value, - loc_pattern.region, - ); - - Some(PendingValueDef::Body( - loc_pattern, - loc_can_pattern, - loc_expr, - )) - } - - AnnotatedBody { - ann_pattern, - ann_type, - comment: _, - body_pattern, - body_expr, - } => { - if ann_pattern.value.equivalent(&body_pattern.value) { - // NOTE: Pick the body pattern, picking the annotation one is - // incorrect in the presence of optional record fields! - // - // { x, y } : { x : Int, y ? Bool }* - // { x, y ? False } = rec - // - // This takes care of checking for shadowing and adding idents to scope. - let loc_can_pattern = canonicalize_def_header_pattern( - env, - var_store, - scope, - output, - pattern_type, - &body_pattern.value, - body_pattern.region, - ); - - Some(PendingValueDef::TypedBody( - body_pattern, - loc_can_pattern, - ann_type, - body_expr, - )) - } else { - // the pattern of the annotation does not match the pattern of the body direc - env.problems.push(Problem::SignatureDefMismatch { - annotation_pattern: ann_pattern.region, - def_pattern: body_pattern.region, - }); - - // TODO: Should we instead build some PendingValueDef::InvalidAnnotatedBody ? This would - // remove the `Option` on this function (and be probably more reliable for further - // problem/error reporting) - None - } - } - - Expect(_condition) => todo!(), - } -} - -/// Make aliases recursive -fn correct_mutual_recursive_type_alias<'a>( - env: &mut Env<'a>, - original_aliases: VecMap, - var_store: &mut VarStore, -) -> VecMap { - let capacity = original_aliases.len(); - let mut matrix = ReferenceMatrix::new(capacity); - - let (symbols_introduced, mut aliases) = original_aliases.unzip(); - - for (index, alias) in aliases.iter().enumerate() { - for referenced in alias.typ.symbols() { - match symbols_introduced.iter().position(|k| referenced == *k) { - None => { /* ignore */ } - Some(ref_id) => matrix.set_row_col(index, ref_id, true), - } - } - } - - let mut solved_aliases = bitvec::vec::BitVec::::repeat(false, capacity); - - let sccs = matrix.strongly_connected_components_all(); - - // scratchpad to store aliases that are modified in the current iteration. - // Only used when there is are more than one alias in a group. See below why - // this is needed. - let scratchpad_capacity = sccs - .groups() - .map(|r| r.count_ones()) - .max() - .unwrap_or_default(); - let mut scratchpad = Vec::with_capacity(scratchpad_capacity); - - for cycle in sccs.groups() { - debug_assert!(cycle.count_ones() > 0); - - // We need to instantiate the alias with any symbols in the currrent module it - // depends on. - // - // the `strongly_connected_components` returns SCCs in a topologically sorted order: - // SCC_0 has those aliases that don't rely on any other, SCC_1 has only those that rely on SCC_1, etc. - // - // Hence, we only need to worry about symbols in the current SCC or any prior one. - // It cannot be using any of the others, and we've already instantiated aliases coming from other modules. - let mut to_instantiate = solved_aliases | cycle; - - // Make sure we report only one error for the cycle, not an error for every - // alias in the cycle. - let mut can_still_report_error = true; - - for index in cycle.iter_ones() { - // Don't try to instantiate the alias itself in its own definition. - to_instantiate.set(index, false); - - // Within a recursive group, we must instantiate all aliases like how they came to the - // loop. e.g. given - // - // A : [ConsA B, NilA] - // B : [ConsB A, NilB] - // - // Our goal is - // - // A : [ConsA [ConsB A, NilB], NilA] - // B : [ConsB [ConsA B, NilA], NilB] - // - // But if we would first instantiate B into A, then use the updated A to instantiate B, - // we get - // - // A : [ConsA [ConsB A, NilB], NilA] - // B : [ConsB [ConsA [ConsB A, NilB], NilA], NilB] - // - // Which is incorrect. We do need the instantiated version however. - // e.g. if in a next group we have: - // - // C : A - // - // Then we must use the instantiated version - // - // C : [ConsA [ConsB A, NilB], NilA] - // - // So, we cannot replace the original version of A with its instantiated version - // while we process A's group. We have to store the instantiated version until the - // current group is done, then move it to the `aliases` array. That is what the scratchpad is for. - let alias = if cycle.count_ones() == 1 { - // an optimization: we can modify the alias in the `aliases` list directly - // because it is the only alias in the group. - &mut aliases[index] - } else { - scratchpad.push((index, aliases[index].clone())); - - &mut scratchpad.last_mut().unwrap().1 - }; - - // Now, `alias` is possibly a mutable borrow from the `aliases` vector. But we also want - // to immutably borrow other elements from that vector to instantiate them into `alias`. - // The borrow checker disallows that. - // - // So we get creative: we swap out the element we want to modify with a dummy. We can - // then freely modify the type we moved out, and the `to_instantiate` mask - // makes sure that our dummy is not used. - - let alias_region = alias.region; - let mut alias_type = Type::EmptyRec; - - std::mem::swap(&mut alias_type, &mut alias.typ); - - let can_instantiate_symbol = |s| match symbols_introduced.iter().position(|i| *i == s) { - Some(s_index) if to_instantiate[s_index] => aliases.get(s_index), - _ => None, - }; - - let mut new_lambda_sets = ImSet::default(); - alias_type.instantiate_aliases( - alias_region, - &can_instantiate_symbol, - var_store, - &mut new_lambda_sets, - ); - - let alias = if cycle.count_ones() > 1 { - &mut scratchpad.last_mut().unwrap().1 - } else { - &mut aliases[index] - }; - - // swap the type back - std::mem::swap(&mut alias_type, &mut alias.typ); - - // We can instantiate this alias in future iterations - to_instantiate.set(index, true); - - // add any lambda sets that the instantiation created to the current alias - alias.lambda_set_variables.extend( - new_lambda_sets - .iter() - .map(|var| LambdaSet(Type::Variable(*var))), - ); - - // Now mark the alias recursive, if it needs to be. - let rec = symbols_introduced[index]; - let is_self_recursive = cycle.count_ones() == 1 && matrix.get_row_col(index, index); - let is_mutually_recursive = cycle.count_ones() > 1; - - if is_self_recursive || is_mutually_recursive { - let _made_recursive = make_tag_union_of_alias_recursive( - env, - rec, - alias, - vec![], - var_store, - &mut can_still_report_error, - ); - } - } - - // the current group has instantiated. Now we can move the updated aliases to the `aliases` vector - for (index, alias) in scratchpad.drain(..) { - aliases[index] = alias; - } - - // The cycle we just instantiated and marked recursive may still be an illegal cycle, if - // all the types in the cycle are narrow newtypes. We can't figure this out until now, - // because we need all the types to be deeply instantiated. - let all_are_narrow = cycle.iter_ones().all(|index| { - let typ = &aliases[index].typ; - matches!(typ, Type::RecursiveTagUnion(..)) && typ.is_narrow() - }); - - if all_are_narrow { - // This cycle is illegal! - let mut indices = cycle.iter_ones(); - let first_index = indices.next().unwrap(); - - let rest: Vec = indices.map(|i| symbols_introduced[i]).collect(); - - let alias_name = symbols_introduced[first_index]; - let alias = aliases.get_mut(first_index).unwrap(); - - mark_cyclic_alias( - env, - &mut alias.typ, - alias_name, - alias.region, - rest, - can_still_report_error, - ) - } - - // We've instantiated all we could, so all instantiatable aliases are solved now - solved_aliases = to_instantiate; - } - - // Safety: both vectors are equal length and there are no duplicates - unsafe { VecMap::zip(symbols_introduced, aliases) } -} - -fn make_tag_union_of_alias_recursive<'a>( - env: &mut Env<'a>, - alias_name: Symbol, - alias: &mut Alias, - others: Vec, - var_store: &mut VarStore, - can_report_cyclic_error: &mut bool, -) -> Result<(), ()> { - let alias_args = alias - .type_variables - .iter() - .map(|l| (l.value.name.clone(), Type::Variable(l.value.var))) - .collect::>(); - - let made_recursive = make_tag_union_recursive_help( - env, - Loc::at( - alias.header_region(), - (alias_name, alias_args.iter().map(|ta| &ta.1)), - ), - alias.region, - others, - &mut alias.typ, - var_store, - can_report_cyclic_error, - ); - - match made_recursive { - MakeTagUnionRecursive::Cyclic => Ok(()), - MakeTagUnionRecursive::MadeRecursive { recursion_variable } => { - alias.recursion_variables.clear(); - alias.recursion_variables.insert(recursion_variable); - - Ok(()) - } - MakeTagUnionRecursive::InvalidRecursion => Err(()), - } -} - -enum MakeTagUnionRecursive { - Cyclic, - MadeRecursive { recursion_variable: Variable }, - InvalidRecursion, -} - -/// Attempt to make a tag union recursive at the position of `recursive_alias`; for example, -/// -/// ```roc -/// [Cons a (ConsList a), Nil] as ConsList a -/// ``` -/// -/// can be made recursive at the position "ConsList a" with a fresh recursive variable, say r1: -/// -/// ```roc -/// [Cons a r1, Nil] as r1 -/// ``` -/// -/// Returns `Err` if the tag union is recursive, but there is no structure-preserving recursion -/// variable for it. This can happen when the type is a nested datatype, for example in either of -/// -/// ```roc -/// Nested a : [Chain a (Nested (List a)), Term] -/// DuoList a b : [Cons a (DuoList b a), Nil] -/// ``` -/// -/// When `Err` is returned, a problem will be added to `env`. -fn make_tag_union_recursive_help<'a, 'b>( - env: &mut Env<'a>, - recursive_alias: Loc<(Symbol, impl Iterator)>, - region: Region, - others: Vec, - typ: &'b mut Type, - var_store: &mut VarStore, - can_report_cyclic_error: &mut bool, -) -> MakeTagUnionRecursive { - use MakeTagUnionRecursive::*; - - let (symbol, args) = recursive_alias.value; - let alias_region = recursive_alias.region; - - match typ { - Type::TagUnion(tags, ext) => { - let recursion_variable = var_store.fresh(); - let type_arguments: Vec<_> = args.into_iter().cloned().collect(); - - let mut pending_typ = - Type::RecursiveTagUnion(recursion_variable, tags.to_vec(), ext.clone()); - let substitution_result = pending_typ.substitute_alias( - symbol, - &type_arguments, - &Type::Variable(recursion_variable), - ); - match substitution_result { - Ok(()) => { - // We can substitute the alias presence for the variable exactly. - *typ = pending_typ; - - MadeRecursive { recursion_variable } - } - Err(differing_recursion_region) => { - env.problems.push(Problem::NestedDatatype { - alias: symbol, - def_region: alias_region, - differing_recursion_region, - }); - - InvalidRecursion - } - } - } - Type::RecursiveTagUnion(recursion_variable, _, _) => MadeRecursive { - recursion_variable: *recursion_variable, - }, - Type::Alias { - actual, - type_arguments, - .. - } => { - // NB: We need to collect the type arguments to shut off rustc's closure type - // instantiator. Otherwise we get unfortunate errors like - // reached the recursion limit while instantiating `make_tag_union_recursive_help::<...n/src/def.rs:1879:65: 1879:77]>>` - #[allow(clippy::needless_collect)] - let type_arguments: Vec<&Type> = type_arguments.iter().map(|ta| &ta.typ).collect(); - let recursive_alias = Loc::at_zero((symbol, type_arguments.into_iter())); - - // try to make `actual` recursive - make_tag_union_recursive_help( - env, - recursive_alias, - region, - others, - actual, - var_store, - can_report_cyclic_error, - ) - } - _ => { - // take care to report a cyclic alias only once (not once for each alias in the cycle) - mark_cyclic_alias(env, typ, symbol, region, others, *can_report_cyclic_error); - *can_report_cyclic_error = false; - - Cyclic - } - } -} - -fn mark_cyclic_alias<'a>( - env: &mut Env<'a>, - typ: &mut Type, - symbol: Symbol, - region: Region, - others: Vec, - report: bool, -) { - let problem = roc_types::types::Problem::CyclicAlias(symbol, region, others.clone()); - *typ = Type::Erroneous(problem); - - if report { - let problem = Problem::CyclicAlias(symbol, region, others); - env.problems.push(problem); - } -} diff --git a/compiler/can/src/env.rs b/compiler/can/src/env.rs deleted file mode 100644 index 12cb7ce00c..0000000000 --- a/compiler/can/src/env.rs +++ /dev/null @@ -1,168 +0,0 @@ -use crate::procedure::References; -use crate::scope::Scope; -use roc_collections::{MutMap, VecSet}; -use roc_module::ident::{Ident, Lowercase, ModuleName}; -use roc_module::symbol::{IdentIdsByModule, ModuleId, ModuleIds, Symbol}; -use roc_problem::can::{Problem, RuntimeError}; -use roc_region::all::{Loc, Region}; - -/// The canonicalization environment for a particular module. -pub struct Env<'a> { - /// The module's path. Opaques and unqualified references to identifiers - /// are assumed to be relative to this path. - pub home: ModuleId, - - pub dep_idents: &'a IdentIdsByModule, - - pub module_ids: &'a ModuleIds, - - /// Problems we've encountered along the way, which will be reported to the user at the end. - pub problems: Vec, - - /// Closures - pub closures: MutMap, - - /// current tail-callable symbol - pub tailcallable_symbol: Option, - - /// Symbols of values/functions which were referenced by qualified lookups. - pub qualified_value_lookups: VecSet, - - /// Symbols of types which were referenced by qualified lookups. - pub qualified_type_lookups: VecSet, - - pub top_level_symbols: VecSet, -} - -impl<'a> Env<'a> { - pub fn new( - home: ModuleId, - dep_idents: &'a IdentIdsByModule, - module_ids: &'a ModuleIds, - ) -> Env<'a> { - Env { - home, - dep_idents, - module_ids, - problems: Vec::new(), - closures: MutMap::default(), - qualified_value_lookups: VecSet::default(), - qualified_type_lookups: VecSet::default(), - tailcallable_symbol: None, - top_level_symbols: VecSet::default(), - } - } - - /// Returns Err if the symbol resolved, but it was not exposed by the given module - pub fn qualified_lookup( - &mut self, - scope: &Scope, - module_name_str: &str, - ident: &str, - region: Region, - ) -> Result { - debug_assert!( - !module_name_str.is_empty(), - "Called env.qualified_lookup with an unqualified ident: {:?}", - ident - ); - - let is_type_name = ident.starts_with(|c: char| c.is_uppercase()); - - let module_name = ModuleName::from(module_name_str); - let ident = Ident::from(ident); - - match self.module_ids.get_id(&module_name) { - Some(&module_id) => { - // You can do qualified lookups on your own module, e.g. - // if I'm in the Foo module, I can do a `Foo.bar` lookup. - if module_id == self.home { - match scope.locals.ident_ids.get_id(&ident) { - Some(ident_id) => { - let symbol = Symbol::new(module_id, ident_id); - - if is_type_name { - self.qualified_type_lookups.insert(symbol); - } else { - self.qualified_value_lookups.insert(symbol); - } - - Ok(symbol) - } - None => { - let error = RuntimeError::LookupNotInScope( - Loc { - value: ident, - region, - }, - scope - .locals - .ident_ids - .ident_strs() - .map(|(_, string)| string.into()) - .collect(), - ); - Err(error) - } - } - } else { - match self.dep_idents.get(&module_id) { - Some(exposed_ids) => match exposed_ids.get_id(&ident) { - Some(ident_id) => { - let symbol = Symbol::new(module_id, ident_id); - - if is_type_name { - self.qualified_type_lookups.insert(symbol); - } else { - self.qualified_value_lookups.insert(symbol); - } - - Ok(symbol) - } - None => { - let exposed_values = exposed_ids - .ident_strs() - .filter(|(_, ident)| { - ident.starts_with(|c: char| c.is_lowercase()) - }) - .map(|(_, ident)| Lowercase::from(ident)) - .collect(); - Err(RuntimeError::ValueNotExposed { - module_name, - ident, - region, - exposed_values, - }) - } - }, - None => Err(RuntimeError::ModuleNotImported { - module_name, - imported_modules: self - .dep_idents - .keys() - .filter_map(|module_id| self.module_ids.get_name(*module_id)) - .map(|module_name| module_name.as_ref().into()) - .collect(), - region, - module_exists: true, - }), - } - } - } - None => Err(RuntimeError::ModuleNotImported { - module_name, - imported_modules: self - .module_ids - .available_modules() - .map(|string| string.as_ref().into()) - .collect(), - region, - module_exists: false, - }), - } - } - - pub fn problem(&mut self, problem: Problem) { - self.problems.push(problem) - } -} diff --git a/compiler/can/src/exhaustive.rs b/compiler/can/src/exhaustive.rs deleted file mode 100644 index 31a7665596..0000000000 --- a/compiler/can/src/exhaustive.rs +++ /dev/null @@ -1,423 +0,0 @@ -use crate::expr::{self, IntValue, WhenBranch}; -use crate::pattern::DestructType; -use roc_collections::all::HumanIndex; -use roc_error_macros::internal_error; -use roc_exhaustive::{ - is_useful, Ctor, CtorName, Error, Guard, Literal, Pattern, RenderAs, TagId, Union, -}; -use roc_module::ident::{TagIdIntType, TagName}; -use roc_region::all::{Loc, Region}; -use roc_types::subs::{Content, FlatType, RedundantMark, Subs, SubsFmtContent, Variable}; -use roc_types::types::AliasKind; - -pub use roc_exhaustive::Context as ExhaustiveContext; - -pub const GUARD_CTOR: &str = "#Guard"; -pub const NONEXHAUSIVE_CTOR: &str = "#Open"; - -pub struct ExhaustiveSummary { - pub errors: Vec, - pub exhaustive: bool, - pub redundancies: Vec, -} - -pub fn check( - subs: &Subs, - sketched_rows: SketchedRows, - context: ExhaustiveContext, -) -> ExhaustiveSummary { - let overall_region = sketched_rows.overall_region; - let mut all_errors = Vec::with_capacity(1); - - let NonRedundantSummary { - non_redundant_rows, - errors, - redundancies, - } = sketched_rows.reify_to_non_redundant(subs); - all_errors.extend(errors); - - let exhaustive = match roc_exhaustive::check(overall_region, context, non_redundant_rows) { - Ok(()) => true, - Err(errors) => { - all_errors.extend(errors); - false - } - }; - - ExhaustiveSummary { - errors: all_errors, - exhaustive, - redundancies, - } -} - -#[derive(Clone, Debug, PartialEq, Eq)] -enum SketchedPattern { - Anything, - Literal(Literal), - Ctor(Variable, TagName, Vec), - KnownCtor(Union, TagId, Vec), -} - -impl SketchedPattern { - fn reify(self, subs: &Subs) -> Pattern { - match self { - Self::Anything => Pattern::Anything, - Self::Literal(lit) => Pattern::Literal(lit), - Self::KnownCtor(union, tag_id, patterns) => Pattern::Ctor( - union, - tag_id, - patterns.into_iter().map(|pat| pat.reify(subs)).collect(), - ), - Self::Ctor(var, tag_name, patterns) => { - let (union, tag_id) = convert_tag(subs, var, &tag_name); - Pattern::Ctor( - union, - tag_id, - patterns.into_iter().map(|pat| pat.reify(subs)).collect(), - ) - } - } - } -} - -#[derive(Clone, Debug, PartialEq, Eq)] -struct SketchedRow { - patterns: Vec, - region: Region, - guard: Guard, - redundant_mark: RedundantMark, -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct SketchedRows { - rows: Vec, - overall_region: Region, -} - -impl SketchedRows { - fn reify_to_non_redundant(self, subs: &Subs) -> NonRedundantSummary { - to_nonredundant_rows(subs, self) - } -} - -fn sketch_pattern(var: Variable, pattern: &crate::pattern::Pattern) -> SketchedPattern { - use crate::pattern::Pattern::*; - use SketchedPattern as SP; - - match pattern { - &NumLiteral(_, _, IntValue::I128(n), _) | &IntLiteral(_, _, _, IntValue::I128(n), _) => { - SP::Literal(Literal::Int(n)) - } - &NumLiteral(_, _, IntValue::U128(n), _) | &IntLiteral(_, _, _, IntValue::U128(n), _) => { - SP::Literal(Literal::U128(n)) - } - &FloatLiteral(_, _, _, f, _) => SP::Literal(Literal::Float(f64::to_bits(f))), - StrLiteral(v) => SP::Literal(Literal::Str(v.clone())), - &SingleQuote(c) => SP::Literal(Literal::Byte(c as u8)), - RecordDestructure { destructs, .. } => { - let tag_id = TagId(0); - let mut patterns = std::vec::Vec::with_capacity(destructs.len()); - let mut field_names = std::vec::Vec::with_capacity(destructs.len()); - - for Loc { - value: destruct, - region: _, - } in destructs - { - field_names.push(destruct.label.clone()); - - match &destruct.typ { - DestructType::Required | DestructType::Optional(..) => { - patterns.push(SP::Anything) - } - DestructType::Guard(_, guard) => { - patterns.push(sketch_pattern(destruct.var, &guard.value)) - } - } - } - - let union = Union { - render_as: RenderAs::Record(field_names), - alternatives: vec![Ctor { - name: CtorName::Tag(TagName("#Record".into())), - tag_id, - arity: destructs.len(), - }], - }; - - SP::KnownCtor(union, tag_id, patterns) - } - - AppliedTag { - tag_name, - arguments, - .. - } => { - let simplified_args: std::vec::Vec<_> = arguments - .iter() - .map(|(var, arg)| sketch_pattern(*var, &arg.value)) - .collect(); - - SP::Ctor(var, tag_name.clone(), simplified_args) - } - - UnwrappedOpaque { - opaque, argument, .. - } => { - let (arg_var, argument) = &(**argument); - - let tag_id = TagId(0); - - let union = Union { - render_as: RenderAs::Opaque, - alternatives: vec![Ctor { - name: CtorName::Opaque(*opaque), - tag_id, - arity: 1, - }], - }; - - SP::KnownCtor( - union, - tag_id, - vec![sketch_pattern(*arg_var, &argument.value)], - ) - } - - // Treat this like a literal so we mark it as non-exhaustive - MalformedPattern(..) => SP::Literal(Literal::Byte(1)), - - Underscore - | Identifier(_) - | AbilityMemberSpecialization { .. } - | Shadowed(..) - | OpaqueNotInScope(..) - | UnsupportedPattern(..) => SP::Anything, - } -} - -pub fn sketch_when_branches( - target_var: Variable, - region: Region, - patterns: &[expr::WhenBranch], -) -> SketchedRows { - let mut rows: Vec = Vec::with_capacity(patterns.len()); - - // If any of the branches has a guard, e.g. - // - // when x is - // y if y < 10 -> "foo" - // _ -> "bar" - // - // then we treat it as a pattern match on the pattern and a boolean, wrapped in the #Guard - // constructor. We can use this special constructor name to generate better error messages. - // This transformation of the pattern match only works because we only report exhaustiveness - // errors: the Pattern created in this file is not used for code gen. - // - // when x is - // #Guard y True -> "foo" - // #Guard _ _ -> "bar" - let any_has_guard = patterns.iter().any(|branch| branch.guard.is_some()); - - use SketchedPattern as SP; - for WhenBranch { - patterns, - guard, - value: _, - redundant, - } in patterns - { - let guard = if guard.is_some() { - Guard::HasGuard - } else { - Guard::NoGuard - }; - - for loc_pat in patterns { - // Decompose each pattern in the branch into its own row. - - let patterns = if any_has_guard { - let guard_pattern = match guard { - Guard::HasGuard => SP::Literal(Literal::Bit(true)), - Guard::NoGuard => SP::Anything, - }; - - let tag_id = TagId(0); - - let union = Union { - render_as: RenderAs::Guard, - alternatives: vec![Ctor { - tag_id, - name: CtorName::Tag(TagName(GUARD_CTOR.into())), - arity: 2, - }], - }; - - vec![SP::KnownCtor( - union, - tag_id, - // NB: ordering the guard pattern first seems to be better at catching - // non-exhaustive constructors in the second argument; see the paper to see if - // there is a way to improve this in general. - vec![guard_pattern, sketch_pattern(target_var, &loc_pat.value)], - )] - } else { - // Simple case - vec![sketch_pattern(target_var, &loc_pat.value)] - }; - - let row = SketchedRow { - patterns, - region: loc_pat.region, - guard, - redundant_mark: *redundant, - }; - rows.push(row); - } - } - - SketchedRows { - rows, - overall_region: region, - } -} - -pub fn sketch_pattern_to_rows( - target_var: Variable, - region: Region, - pattern: &crate::pattern::Pattern, -) -> SketchedRows { - let row = SketchedRow { - patterns: vec![sketch_pattern(target_var, pattern)], - region, - // A single row cannot be redundant! - redundant_mark: RedundantMark::known_non_redundant(), - guard: Guard::NoGuard, - }; - SketchedRows { - rows: vec![row], - overall_region: region, - } -} - -/// REDUNDANT PATTERNS - -struct NonRedundantSummary { - non_redundant_rows: Vec>, - redundancies: Vec, - errors: Vec, -} - -/// INVARIANT: Produces a list of rows where (forall row. length row == 1) -fn to_nonredundant_rows(subs: &Subs, rows: SketchedRows) -> NonRedundantSummary { - let SketchedRows { - rows, - overall_region, - } = rows; - let mut checked_rows = Vec::with_capacity(rows.len()); - - let mut redundancies = vec![]; - let mut errors = vec![]; - - for SketchedRow { - patterns, - guard, - region, - redundant_mark, - } in rows.into_iter() - { - let next_row: Vec = patterns - .into_iter() - .map(|pattern| pattern.reify(subs)) - .collect(); - - if matches!(guard, Guard::HasGuard) || is_useful(checked_rows.clone(), next_row.clone()) { - checked_rows.push(next_row); - } else { - redundancies.push(redundant_mark); - errors.push(Error::Redundant { - overall_region, - branch_region: region, - index: HumanIndex::zero_based(checked_rows.len()), - }); - } - } - - NonRedundantSummary { - non_redundant_rows: checked_rows, - redundancies, - errors, - } -} - -fn convert_tag(subs: &Subs, whole_var: Variable, this_tag: &TagName) -> (Union, TagId) { - let content = subs.get_content_without_compacting(whole_var); - - use {Content::*, FlatType::*}; - - match dealias_tag(subs, content) { - Structure(TagUnion(tags, ext) | RecursiveTagUnion(_, tags, ext)) => { - let (sorted_tags, ext) = tags.sorted_iterator_and_ext(subs, *ext); - - let mut num_tags = sorted_tags.len(); - - // DEVIATION: model openness by attaching a #Open constructor, that can never - // be matched unless there's an `Anything` pattern. - let opt_openness_tag = match subs.get_content_without_compacting(ext) { - FlexVar(_) | RigidVar(_) => { - let openness_tag = TagName(NONEXHAUSIVE_CTOR.into()); - num_tags += 1; - Some((openness_tag, &[] as _)) - } - Structure(EmptyTagUnion) => None, - // Anything else is erroneous and we ignore - _ => None, - }; - - // High tag ID if we're out-of-bounds. - let mut my_tag_id = TagId(num_tags as TagIdIntType); - - let mut alternatives = Vec::with_capacity(num_tags); - let alternatives_iter = sorted_tags.into_iter().chain(opt_openness_tag.into_iter()); - - for (index, (tag, args)) in alternatives_iter.enumerate() { - let tag_id = TagId(index as TagIdIntType); - if this_tag == &tag { - my_tag_id = tag_id; - } - alternatives.push(Ctor { - name: CtorName::Tag(tag), - tag_id, - arity: args.len(), - }); - } - - let union = Union { - alternatives, - render_as: RenderAs::Tag, - }; - - (union, my_tag_id) - } - _ => internal_error!( - "Content is not a tag union: {:?}", - SubsFmtContent(content, subs) - ), - } -} - -pub fn dealias_tag<'a>(subs: &'a Subs, content: &'a Content) -> &'a Content { - use Content::*; - let mut result = content; - loop { - match result { - Alias(_, _, real_var, AliasKind::Structural) - | RecursionVar { - structure: real_var, - .. - } => result = subs.get_content_without_compacting(*real_var), - _ => return result, - } - } -} diff --git a/compiler/can/src/expr.rs b/compiler/can/src/expr.rs deleted file mode 100644 index 6dab856969..0000000000 --- a/compiler/can/src/expr.rs +++ /dev/null @@ -1,2043 +0,0 @@ -use crate::abilities::SpecializationId; -use crate::annotation::{freshen_opaque_def, IntroducedVariables}; -use crate::builtins::builtin_defs_map; -use crate::def::{can_defs_with_return, Def}; -use crate::env::Env; -use crate::num::{ - finish_parsing_base, finish_parsing_float, finish_parsing_num, float_expr_from_result, - int_expr_from_result, num_expr_from_result, FloatBound, IntBound, NumBound, -}; -use crate::pattern::{canonicalize_pattern, BindingsFromPattern, Pattern}; -use crate::procedure::References; -use crate::scope::Scope; -use roc_collections::{SendMap, VecMap, VecSet}; -use roc_module::called_via::CalledVia; -use roc_module::ident::{ForeignSymbol, Lowercase, TagName}; -use roc_module::low_level::LowLevel; -use roc_module::symbol::Symbol; -use roc_parse::ast::{self, EscapedChar, StrLiteral}; -use roc_parse::pattern::PatternType::*; -use roc_problem::can::{PrecedenceProblem, Problem, RuntimeError}; -use roc_region::all::{Loc, Region}; -use roc_types::subs::{ExhaustiveMark, IllegalCycleMark, RedundantMark, VarStore, Variable}; -use roc_types::types::{Alias, Category, LambdaSet, OptAbleVar, Type}; -use std::fmt::{Debug, Display}; -use std::{char, u32}; - -/// Derives that an opaque type has claimed, to checked and recorded after solving. -pub type PendingDerives = VecMap>)>; - -#[derive(Clone, Default, Debug)] -pub struct Output { - pub references: References, - pub tail_call: Option, - pub introduced_variables: IntroducedVariables, - pub aliases: VecMap, - pub non_closures: VecSet, - pub pending_derives: PendingDerives, -} - -impl Output { - pub fn union(&mut self, other: Self) { - self.references.union_mut(&other.references); - - if let (None, Some(later)) = (self.tail_call, other.tail_call) { - self.tail_call = Some(later); - } - - self.introduced_variables - .union_owned(other.introduced_variables); - self.aliases.extend(other.aliases); - self.non_closures.extend(other.non_closures); - - { - let expected_derives_size = self.pending_derives.len() + other.pending_derives.len(); - self.pending_derives.extend(other.pending_derives); - debug_assert!( - expected_derives_size == self.pending_derives.len(), - "Derives overwritten from nested scope - something is very wrong" - ); - } - } -} - -#[derive(Clone, Debug, PartialEq)] -pub enum IntValue { - I128([u8; 16]), - U128([u8; 16]), -} - -impl Display for IntValue { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - IntValue::I128(n) => Display::fmt(&i128::from_ne_bytes(*n), f), - IntValue::U128(n) => Display::fmt(&u128::from_ne_bytes(*n), f), - } - } -} - -#[derive(Clone, Debug)] -pub enum Expr { - // Literals - - // Num stores the `a` variable in `Num a`. Not the same as the variable - // stored in Int and Float below, which is strictly for better error messages - Num(Variable, Box, IntValue, NumBound), - - // Int and Float store a variable to generate better error messages - Int(Variable, Variable, Box, IntValue, IntBound), - Float(Variable, Variable, Box, f64, FloatBound), - Str(Box), - SingleQuote(char), - List { - elem_var: Variable, - loc_elems: Vec>, - }, - - // Lookups - Var(Symbol), - AbilityMember( - /// Actual member name - Symbol, - /// Specialization to use, and its variable - SpecializationId, - Variable, - ), - - // Branching - When { - /// The actual condition of the when expression. - loc_cond: Box>, - cond_var: Variable, - /// Result type produced by the branches. - expr_var: Variable, - region: Region, - /// The branches of the when, and the type of the condition that they expect to be matched - /// against. - branches: Vec, - branches_cond_var: Variable, - /// Whether the branches are exhaustive. - exhaustive: ExhaustiveMark, - }, - If { - cond_var: Variable, - branch_var: Variable, - branches: Vec<(Loc, Loc)>, - final_else: Box>, - }, - - // Let - LetRec(Vec, Box>, IllegalCycleMark), - LetNonRec(Box, Box>), - - /// This is *only* for calling functions, not for tag application. - /// The Tag variant contains any applied values inside it. - Call( - Box<(Variable, Loc, Variable, Variable)>, - Vec<(Variable, Loc)>, - CalledVia, - ), - RunLowLevel { - op: LowLevel, - args: Vec<(Variable, Expr)>, - ret_var: Variable, - }, - ForeignCall { - foreign_symbol: ForeignSymbol, - args: Vec<(Variable, Expr)>, - ret_var: Variable, - }, - - Closure(ClosureData), - - // Product Types - Record { - record_var: Variable, - fields: SendMap, - }, - - /// Empty record constant - EmptyRecord, - - /// Look up exactly one field on a record, e.g. (expr).foo. - Access { - record_var: Variable, - ext_var: Variable, - field_var: Variable, - loc_expr: Box>, - field: Lowercase, - }, - /// field accessor as a function, e.g. (.foo) expr - Accessor(AccessorData), - - Update { - record_var: Variable, - ext_var: Variable, - symbol: Symbol, - updates: SendMap, - }, - - // Sum Types - Tag { - variant_var: Variable, - ext_var: Variable, - name: TagName, - arguments: Vec<(Variable, Loc)>, - }, - - ZeroArgumentTag { - closure_name: Symbol, - variant_var: Variable, - ext_var: Variable, - name: TagName, - }, - - /// A wrapping of an opaque type, like `@Age 21` - OpaqueRef { - opaque_var: Variable, - name: Symbol, - argument: Box<(Variable, Loc)>, - - // The following help us link this opaque reference to the type specified by its - // definition, which we then use during constraint generation. For example - // suppose we have - // - // Id n := [Id U64 n] - // @Id "sasha" - // - // Then `opaque` is "Id", `argument` is "sasha", but this is not enough for us to - // infer the type of the expression as "Id Str" - we need to link the specialized type of - // the variable "n". - // That's what `specialized_def_type` and `type_arguments` are for; they are specialized - // for the expression from the opaque definition. `type_arguments` is something like - // [(n, fresh1)], and `specialized_def_type` becomes "[Id U64 fresh1]". - specialized_def_type: Box, - type_arguments: Vec, - lambda_set_variables: Vec, - }, - - // Test - Expect { - loc_condition: Box>, - loc_continuation: Box>, - lookups_in_cond: Vec<(Symbol, Variable)>, - }, - - /// Rendered as empty box in editor - TypedHole(Variable), - - /// Compiles, but will crash if reached - RuntimeError(RuntimeError), -} - -impl Expr { - pub fn category(&self) -> Category { - match self { - Self::Num(..) => Category::Num, - Self::Int(..) => Category::Int, - Self::Float(..) => Category::Float, - Self::Str(..) => Category::Str, - Self::SingleQuote(..) => Category::Character, - Self::List { .. } => Category::List, - &Self::Var(sym) => Category::Lookup(sym), - &Self::AbilityMember(sym, _, _) => Category::Lookup(sym), - Self::When { .. } => Category::When, - Self::If { .. } => Category::If, - Self::LetRec(_, expr, _) => expr.value.category(), - Self::LetNonRec(_, expr) => expr.value.category(), - &Self::Call(_, _, called_via) => Category::CallResult(None, called_via), - &Self::RunLowLevel { op, .. } => Category::LowLevelOpResult(op), - Self::ForeignCall { .. } => Category::ForeignCall, - Self::Closure(..) => Category::Lambda, - Self::Record { .. } => Category::Record, - Self::EmptyRecord => Category::Record, - Self::Access { field, .. } => Category::Access(field.clone()), - Self::Accessor(data) => Category::Accessor(data.field.clone()), - Self::Update { .. } => Category::Record, - Self::Tag { - name, arguments, .. - } => Category::TagApply { - tag_name: name.clone(), - args_count: arguments.len(), - }, - Self::ZeroArgumentTag { name, .. } => Category::TagApply { - tag_name: name.clone(), - args_count: 0, - }, - &Self::OpaqueRef { name, .. } => Category::OpaqueWrap(name), - Self::Expect { .. } => Category::Expect, - - // these nodes place no constraints on the expression's type - Self::TypedHole(_) | Self::RuntimeError(..) => Category::Unknown, - } - } -} - -/// Stores exhaustiveness-checking metadata for a closure argument that may -/// have an annotated type. -#[derive(Clone, Copy, Debug)] -pub struct AnnotatedMark { - pub annotation_var: Variable, - pub exhaustive: ExhaustiveMark, -} - -impl AnnotatedMark { - pub fn new(var_store: &mut VarStore) -> Self { - Self { - annotation_var: var_store.fresh(), - exhaustive: ExhaustiveMark::new(var_store), - } - } - - // NOTE: only ever use this if you *know* a pattern match is surely exhaustive! - // Otherwise you will get unpleasant unification errors. - pub fn known_exhaustive() -> Self { - Self { - annotation_var: Variable::EMPTY_TAG_UNION, - exhaustive: ExhaustiveMark::known_exhaustive(), - } - } -} - -#[derive(Clone, Debug)] -pub struct ClosureData { - pub function_type: Variable, - pub closure_type: Variable, - pub return_type: Variable, - pub name: Symbol, - pub captured_symbols: Vec<(Symbol, Variable)>, - pub recursive: Recursive, - pub arguments: Vec<(Variable, AnnotatedMark, Loc)>, - pub loc_body: Box>, -} - -/// A record accessor like `.foo`, which is equivalent to `\r -> r.foo` -/// Accessors are desugared to closures; they need to have a name -/// so the closure can have a correct lambda set. -/// -/// We distinguish them from closures so we can have better error messages -/// during constraint generation. -#[derive(Clone, Debug, PartialEq)] -pub struct AccessorData { - pub name: Symbol, - pub function_var: Variable, - pub record_var: Variable, - pub closure_var: Variable, - pub ext_var: Variable, - pub field_var: Variable, - pub field: Lowercase, -} - -impl AccessorData { - pub fn to_closure_data(self, record_symbol: Symbol) -> ClosureData { - let AccessorData { - name, - function_var, - record_var, - closure_var, - ext_var, - field_var, - field, - } = self; - - // IDEA: convert accessor from - // - // .foo - // - // into - // - // (\r -> r.foo) - let body = Expr::Access { - record_var, - ext_var, - field_var, - loc_expr: Box::new(Loc::at_zero(Expr::Var(record_symbol))), - field, - }; - - let loc_body = Loc::at_zero(body); - - let arguments = vec![( - record_var, - AnnotatedMark::known_exhaustive(), - Loc::at_zero(Pattern::Identifier(record_symbol)), - )]; - - ClosureData { - function_type: function_var, - closure_type: closure_var, - return_type: field_var, - name, - captured_symbols: vec![], - recursive: Recursive::NotRecursive, - arguments, - loc_body: Box::new(loc_body), - } - } -} - -#[derive(Clone, Debug)] -pub struct Field { - pub var: Variable, - // The region of the full `foo: f bar`, rather than just `f bar` - pub region: Region, - pub loc_expr: Box>, -} - -#[derive(Clone, Copy, Debug, PartialEq)] -pub enum Recursive { - NotRecursive = 0, - Recursive = 1, - TailRecursive = 2, -} - -#[derive(Clone, Debug)] -pub struct WhenBranch { - pub patterns: Vec>, - pub value: Loc, - pub guard: Option>, - /// Whether this branch is redundant in the `when` it appears in - pub redundant: RedundantMark, -} - -impl WhenBranch { - pub fn pattern_region(&self) -> Region { - Region::span_across( - &self - .patterns - .first() - .expect("when branch has no pattern?") - .region, - &self - .patterns - .last() - .expect("when branch has no pattern?") - .region, - ) - } -} - -impl WhenBranch { - pub fn region(&self) -> Region { - Region::across_all( - self.patterns - .iter() - .map(|p| &p.region) - .chain([self.value.region].iter()), - ) - } -} - -pub fn canonicalize_expr<'a>( - env: &mut Env<'a>, - var_store: &mut VarStore, - scope: &mut Scope, - region: Region, - expr: &'a ast::Expr<'a>, -) -> (Loc, Output) { - use Expr::*; - - let (expr, output) = match expr { - &ast::Expr::Num(str) => { - let answer = num_expr_from_result(var_store, finish_parsing_num(str), region, env); - - (answer, Output::default()) - } - &ast::Expr::Float(str) => { - let answer = float_expr_from_result(var_store, finish_parsing_float(str), region, env); - - (answer, Output::default()) - } - ast::Expr::Record(fields) => { - if fields.is_empty() { - (EmptyRecord, Output::default()) - } else { - match canonicalize_fields(env, var_store, scope, region, fields.items) { - Ok((can_fields, output)) => ( - Record { - record_var: var_store.fresh(), - fields: can_fields, - }, - output, - ), - Err(CanonicalizeRecordProblem::InvalidOptionalValue { - field_name, - field_region, - record_region, - }) => ( - Expr::RuntimeError(roc_problem::can::RuntimeError::InvalidOptionalValue { - field_name, - field_region, - record_region, - }), - Output::default(), - ), - } - } - } - ast::Expr::RecordUpdate { - fields, - update: loc_update, - } => { - let (can_update, update_out) = - canonicalize_expr(env, var_store, scope, loc_update.region, &loc_update.value); - if let Var(symbol) = &can_update.value { - match canonicalize_fields(env, var_store, scope, region, fields.items) { - Ok((can_fields, mut output)) => { - output.references.union_mut(&update_out.references); - - let answer = Update { - record_var: var_store.fresh(), - ext_var: var_store.fresh(), - symbol: *symbol, - updates: can_fields, - }; - - (answer, output) - } - Err(CanonicalizeRecordProblem::InvalidOptionalValue { - field_name, - field_region, - record_region, - }) => ( - Expr::RuntimeError(roc_problem::can::RuntimeError::InvalidOptionalValue { - field_name, - field_region, - record_region, - }), - Output::default(), - ), - } - } else { - // only (optionally qualified) variables can be updated, not arbitrary expressions - - let error = roc_problem::can::RuntimeError::InvalidRecordUpdate { - region: can_update.region, - }; - - let answer = Expr::RuntimeError(error.clone()); - - env.problems.push(Problem::RuntimeError(error)); - - (answer, Output::default()) - } - } - ast::Expr::Str(literal) => flatten_str_literal(env, var_store, scope, literal), - - ast::Expr::SingleQuote(string) => { - let mut it = string.chars().peekable(); - if let Some(char) = it.next() { - if it.peek().is_none() { - (Expr::SingleQuote(char), Output::default()) - } else { - // multiple chars is found - let error = roc_problem::can::RuntimeError::MultipleCharsInSingleQuote(region); - let answer = Expr::RuntimeError(error); - - (answer, Output::default()) - } - } else { - // no characters found - let error = roc_problem::can::RuntimeError::EmptySingleQuote(region); - let answer = Expr::RuntimeError(error); - - (answer, Output::default()) - } - } - - ast::Expr::List(loc_elems) => { - if loc_elems.is_empty() { - ( - List { - elem_var: var_store.fresh(), - loc_elems: Vec::new(), - }, - Output::default(), - ) - } else { - let mut can_elems = Vec::with_capacity(loc_elems.len()); - let mut references = References::new(); - - for loc_elem in loc_elems.iter() { - let (can_expr, elem_out) = - canonicalize_expr(env, var_store, scope, loc_elem.region, &loc_elem.value); - - references.union_mut(&elem_out.references); - - can_elems.push(can_expr); - } - - let output = Output { - references, - tail_call: None, - ..Default::default() - }; - - ( - List { - elem_var: var_store.fresh(), - loc_elems: can_elems, - }, - output, - ) - } - } - ast::Expr::Apply(loc_fn, loc_args, application_style) => { - // The expression that evaluates to the function being called, e.g. `foo` in - // (foo) bar baz - let fn_region = loc_fn.region; - - // The function's return type - let mut args = Vec::new(); - let mut output = Output::default(); - - for loc_arg in loc_args.iter() { - let (arg_expr, arg_out) = - canonicalize_expr(env, var_store, scope, loc_arg.region, &loc_arg.value); - - args.push((var_store.fresh(), arg_expr)); - output.references.union_mut(&arg_out.references); - } - - if let ast::Expr::OpaqueRef(name) = loc_fn.value { - // We treat opaques specially, since an opaque can wrap exactly one argument. - - debug_assert!(!args.is_empty()); - - if args.len() > 1 { - let problem = - roc_problem::can::RuntimeError::OpaqueAppliedToMultipleArgs(region); - env.problem(Problem::RuntimeError(problem.clone())); - (RuntimeError(problem), output) - } else { - match scope.lookup_opaque_ref(name, loc_fn.region) { - Err(runtime_error) => { - env.problem(Problem::RuntimeError(runtime_error.clone())); - (RuntimeError(runtime_error), output) - } - Ok((name, opaque_def)) => { - let argument = Box::new(args.pop().unwrap()); - output.references.insert_type_lookup(name); - - let (type_arguments, lambda_set_variables, specialized_def_type) = - freshen_opaque_def(var_store, opaque_def); - - let opaque_ref = OpaqueRef { - opaque_var: var_store.fresh(), - name, - argument, - specialized_def_type: Box::new(specialized_def_type), - type_arguments, - lambda_set_variables, - }; - - (opaque_ref, output) - } - } - } - } else { - // Canonicalize the function expression and its arguments - let (fn_expr, fn_expr_output) = - canonicalize_expr(env, var_store, scope, fn_region, &loc_fn.value); - - output.union(fn_expr_output); - - // Default: We're not tail-calling a symbol (by name), we're tail-calling a function value. - output.tail_call = None; - - let expr = match fn_expr.value { - Var(symbol) => { - output.references.insert_call(symbol); - - // we're tail-calling a symbol by name, check if it's the tail-callable symbol - output.tail_call = match &env.tailcallable_symbol { - Some(tc_sym) if *tc_sym == symbol => Some(symbol), - Some(_) | None => None, - }; - - Call( - Box::new(( - var_store.fresh(), - fn_expr, - var_store.fresh(), - var_store.fresh(), - )), - args, - *application_style, - ) - } - RuntimeError(_) => { - // We can't call a runtime error; bail out by propagating it! - return (fn_expr, output); - } - Tag { - variant_var, - ext_var, - name, - .. - } => Tag { - variant_var, - ext_var, - name, - arguments: args, - }, - ZeroArgumentTag { - variant_var, - ext_var, - name, - .. - } => Tag { - variant_var, - ext_var, - name, - arguments: args, - }, - _ => { - // This could be something like ((if True then fn1 else fn2) arg1 arg2). - Call( - Box::new(( - var_store.fresh(), - fn_expr, - var_store.fresh(), - var_store.fresh(), - )), - args, - *application_style, - ) - } - }; - - (expr, output) - } - } - ast::Expr::Var { module_name, ident } => { - canonicalize_var_lookup(env, var_store, scope, module_name, ident, region) - } - ast::Expr::Underscore(name) => { - // we parse underscores, but they are not valid expression syntax - let problem = roc_problem::can::RuntimeError::MalformedIdentifier( - (*name).into(), - roc_parse::ident::BadIdent::Underscore(region.start()), - region, - ); - - env.problem(Problem::RuntimeError(problem.clone())); - - (RuntimeError(problem), Output::default()) - } - ast::Expr::Defs(loc_defs, loc_ret) => { - // The body expression gets a new scope for canonicalization, - scope.inner_scope(|inner_scope| { - can_defs_with_return(env, var_store, inner_scope, loc_defs, loc_ret) - }) - } - ast::Expr::Backpassing(_, _, _) => { - unreachable!("Backpassing should have been desugared by now") - } - ast::Expr::Closure(loc_arg_patterns, loc_body_expr) => { - let (closure_data, output) = - canonicalize_closure(env, var_store, scope, loc_arg_patterns, loc_body_expr, None); - - (Closure(closure_data), output) - } - ast::Expr::When(loc_cond, branches) => { - // Infer the condition expression's type. - let cond_var = var_store.fresh(); - let (can_cond, mut output) = - canonicalize_expr(env, var_store, scope, loc_cond.region, &loc_cond.value); - - // the condition can never be a tail-call - output.tail_call = None; - - let mut can_branches = Vec::with_capacity(branches.len()); - - for branch in branches.iter() { - let (can_when_branch, branch_references) = scope.inner_scope(|inner_scope| { - canonicalize_when_branch( - env, - var_store, - inner_scope, - region, - *branch, - &mut output, - ) - }); - - output.references.union_mut(&branch_references); - - can_branches.push(can_when_branch); - } - - // A "when" with no branches is a runtime error, but it will mess things up - // if code gen mistakenly thinks this is a tail call just because its condition - // happened to be one. (The condition gave us our initial output value.) - if branches.is_empty() { - output.tail_call = None; - } - - // Incorporate all three expressions into a combined Output value. - let expr = When { - expr_var: var_store.fresh(), - cond_var, - region, - loc_cond: Box::new(can_cond), - branches: can_branches, - branches_cond_var: var_store.fresh(), - exhaustive: ExhaustiveMark::new(var_store), - }; - - (expr, output) - } - ast::Expr::Access(record_expr, field) => { - let (loc_expr, output) = canonicalize_expr(env, var_store, scope, region, record_expr); - - ( - Access { - record_var: var_store.fresh(), - field_var: var_store.fresh(), - ext_var: var_store.fresh(), - loc_expr: Box::new(loc_expr), - field: Lowercase::from(*field), - }, - output, - ) - } - ast::Expr::AccessorFunction(field) => ( - Accessor(AccessorData { - name: scope.gen_unique_symbol(), - function_var: var_store.fresh(), - record_var: var_store.fresh(), - ext_var: var_store.fresh(), - closure_var: var_store.fresh(), - field_var: var_store.fresh(), - field: (*field).into(), - }), - Output::default(), - ), - ast::Expr::Tag(tag) => { - let variant_var = var_store.fresh(); - let ext_var = var_store.fresh(); - - let symbol = scope.gen_unique_symbol(); - - ( - ZeroArgumentTag { - name: TagName((*tag).into()), - variant_var, - closure_name: symbol, - ext_var, - }, - Output::default(), - ) - } - ast::Expr::OpaqueRef(opaque_ref) => { - // If we're here, the opaque reference is definitely not wrapping an argument - wrapped - // arguments are handled in the Apply branch. - let problem = roc_problem::can::RuntimeError::OpaqueNotApplied(Loc::at( - region, - (*opaque_ref).into(), - )); - env.problem(Problem::RuntimeError(problem.clone())); - (RuntimeError(problem), Output::default()) - } - ast::Expr::Expect(condition, continuation) => { - let mut output = Output::default(); - - let (loc_condition, output1) = - canonicalize_expr(env, var_store, scope, condition.region, &condition.value); - - // Get all the lookups that were referenced in the condition, - // so we can print their values later. - let lookups_in_cond = get_lookup_symbols(&loc_condition.value, var_store); - - let (loc_continuation, output2) = canonicalize_expr( - env, - var_store, - scope, - continuation.region, - &continuation.value, - ); - - output.union(output1); - output.union(output2); - - ( - Expect { - loc_condition: Box::new(loc_condition), - loc_continuation: Box::new(loc_continuation), - lookups_in_cond, - }, - output, - ) - } - ast::Expr::If(if_thens, final_else_branch) => { - let mut branches = Vec::with_capacity(if_thens.len()); - let mut output = Output::default(); - - for (condition, then_branch) in if_thens.iter() { - let (loc_cond, cond_output) = - canonicalize_expr(env, var_store, scope, condition.region, &condition.value); - - let (loc_then, then_output) = canonicalize_expr( - env, - var_store, - scope, - then_branch.region, - &then_branch.value, - ); - - branches.push((loc_cond, loc_then)); - - output.references.union_mut(&cond_output.references); - output.references.union_mut(&then_output.references); - } - - let (loc_else, else_output) = canonicalize_expr( - env, - var_store, - scope, - final_else_branch.region, - &final_else_branch.value, - ); - - output.references.union_mut(&else_output.references); - - ( - If { - cond_var: var_store.fresh(), - branch_var: var_store.fresh(), - branches, - final_else: Box::new(loc_else), - }, - output, - ) - } - - ast::Expr::PrecedenceConflict(ast::PrecedenceConflict { - whole_region, - binop1_position, - binop2_position, - binop1, - binop2, - expr: _, - }) => { - use roc_problem::can::RuntimeError::*; - - let region1 = Region::new( - *binop1_position, - binop1_position.bump_column(binop1.width() as u32), - ); - let loc_binop1 = Loc::at(region1, *binop1); - - let region2 = Region::new( - *binop2_position, - binop2_position.bump_column(binop2.width() as u32), - ); - let loc_binop2 = Loc::at(region2, *binop2); - - let problem = - PrecedenceProblem::BothNonAssociative(*whole_region, loc_binop1, loc_binop2); - - env.problem(Problem::PrecedenceProblem(problem.clone())); - - ( - RuntimeError(InvalidPrecedence(problem, region)), - Output::default(), - ) - } - ast::Expr::MalformedClosure => { - use roc_problem::can::RuntimeError::*; - (RuntimeError(MalformedClosure(region)), Output::default()) - } - ast::Expr::MalformedIdent(name, bad_ident) => { - use roc_problem::can::RuntimeError::*; - - let problem = MalformedIdentifier((*name).into(), *bad_ident, region); - env.problem(Problem::RuntimeError(problem.clone())); - - (RuntimeError(problem), Output::default()) - } - &ast::Expr::NonBase10Int { - string, - base, - is_negative, - } => { - // the minus sign is added before parsing, to get correct overflow/underflow behavior - let answer = match finish_parsing_base(string, base, is_negative) { - Ok((int, bound)) => { - // Done in this kinda round about way with intermediate variables - // to keep borrowed values around and make this compile - let int_string = int.to_string(); - let int_str = int_string.as_str(); - int_expr_from_result(var_store, Ok((int_str, int, bound)), region, base, env) - } - Err(e) => int_expr_from_result(var_store, Err(e), region, base, env), - }; - - (answer, Output::default()) - } - // Below this point, we shouln't see any of these nodes anymore because - // operator desugaring should have removed them! - bad_expr @ ast::Expr::ParensAround(_) => { - panic!( - "A ParensAround did not get removed during operator desugaring somehow: {:#?}", - bad_expr - ); - } - bad_expr @ ast::Expr::SpaceBefore(_, _) => { - panic!( - "A SpaceBefore did not get removed during operator desugaring somehow: {:#?}", - bad_expr - ); - } - bad_expr @ ast::Expr::SpaceAfter(_, _) => { - panic!( - "A SpaceAfter did not get removed during operator desugaring somehow: {:#?}", - bad_expr - ); - } - bad_expr @ ast::Expr::BinOps { .. } => { - panic!( - "A binary operator chain did not get desugared somehow: {:#?}", - bad_expr - ); - } - bad_expr @ ast::Expr::UnaryOp(_, _) => { - panic!( - "A unary operator did not get desugared somehow: {:#?}", - bad_expr - ); - } - }; - - // At the end, diff used_idents and defined_idents to see which were unused. - // Add warnings for those! - - // In a later phase, unused top level declarations won't get monomorphized or code-genned. - // We aren't going to bother with DCE at the level of local defs. It's going to be - // a rounding error anyway (especially given that they'll be surfaced as warnings), LLVM will - // DCE them in optimized builds, and it's not worth the bookkeeping for dev builds. - ( - Loc { - region, - value: expr, - }, - output, - ) -} - -pub fn canonicalize_closure<'a>( - env: &mut Env<'a>, - var_store: &mut VarStore, - scope: &mut Scope, - loc_arg_patterns: &'a [Loc>], - loc_body_expr: &'a Loc>, - opt_def_name: Option, -) -> (ClosureData, Output) { - scope.inner_scope(|inner_scope| { - canonicalize_closure_body( - env, - var_store, - inner_scope, - loc_arg_patterns, - loc_body_expr, - opt_def_name, - ) - }) -} - -fn canonicalize_closure_body<'a>( - env: &mut Env<'a>, - var_store: &mut VarStore, - scope: &mut Scope, - loc_arg_patterns: &'a [Loc>], - loc_body_expr: &'a Loc>, - opt_def_name: Option, -) -> (ClosureData, Output) { - // The globally unique symbol that will refer to this closure once it gets converted - // into a top-level procedure for code gen. - let symbol = opt_def_name.unwrap_or_else(|| scope.gen_unique_symbol()); - - let mut can_args = Vec::with_capacity(loc_arg_patterns.len()); - let mut output = Output::default(); - - for loc_pattern in loc_arg_patterns.iter() { - let can_argument_pattern = canonicalize_pattern( - env, - var_store, - scope, - &mut output, - FunctionArg, - &loc_pattern.value, - loc_pattern.region, - ); - - can_args.push(( - var_store.fresh(), - AnnotatedMark::new(var_store), - can_argument_pattern, - )); - } - - let bound_by_argument_patterns: Vec<_> = - BindingsFromPattern::new_many(can_args.iter().map(|x| &x.2)).collect(); - - let (loc_body_expr, new_output) = canonicalize_expr( - env, - var_store, - scope, - loc_body_expr.region, - &loc_body_expr.value, - ); - - let mut captured_symbols: Vec<_> = new_output - .references - .value_lookups() - .copied() - // filter out the closure's name itself - .filter(|s| *s != symbol) - // symbols bound either in this pattern or deeper down are not captured! - .filter(|s| !new_output.references.bound_symbols().any(|x| x == s)) - .filter(|s| bound_by_argument_patterns.iter().all(|(k, _)| s != k)) - // filter out top-level symbols those will be globally available, and don't need to be captured - .filter(|s| !env.top_level_symbols.contains(s)) - // filter out imported symbols those will be globally available, and don't need to be captured - .filter(|s| s.module_id() == env.home) - // filter out functions that don't close over anything - .filter(|s| !new_output.non_closures.contains(s)) - .filter(|s| !output.non_closures.contains(s)) - .map(|s| (s, var_store.fresh())) - .collect(); - - output.union(new_output); - - // Now that we've collected all the references, check to see if any of the args we defined - // went unreferenced. If any did, report them as unused arguments. - for (sub_symbol, region) in bound_by_argument_patterns { - if !output.references.has_value_lookup(sub_symbol) { - // The body never referenced this argument we declared. It's an unused argument! - env.problem(Problem::UnusedArgument(symbol, sub_symbol, region)); - } else { - // We shouldn't ultimately count arguments as referenced locals. Otherwise, - // we end up with weird conclusions like the expression (\x -> x + 1) - // references the (nonexistent) local variable x! - output.references.remove_value_lookup(&sub_symbol); - } - } - - // store the references of this function in the Env. This information is used - // when we canonicalize a surrounding def (if it exists) - env.closures.insert(symbol, output.references.clone()); - - // sort symbols, so we know the order in which they're stored in the closure record - captured_symbols.sort(); - - // store that this function doesn't capture anything. It will be promoted to a - // top-level function, and does not need to be captured by other surrounding functions. - if captured_symbols.is_empty() { - output.non_closures.insert(symbol); - } - - let closure_data = ClosureData { - function_type: var_store.fresh(), - closure_type: var_store.fresh(), - return_type: var_store.fresh(), - name: symbol, - captured_symbols, - recursive: Recursive::NotRecursive, - arguments: can_args, - loc_body: Box::new(loc_body_expr), - }; - - (closure_data, output) -} - -#[inline(always)] -fn canonicalize_when_branch<'a>( - env: &mut Env<'a>, - var_store: &mut VarStore, - scope: &mut Scope, - _region: Region, - branch: &'a ast::WhenBranch<'a>, - output: &mut Output, -) -> (WhenBranch, References) { - let mut patterns = Vec::with_capacity(branch.patterns.len()); - - // TODO report symbols not bound in all patterns - for loc_pattern in branch.patterns.iter() { - let can_pattern = canonicalize_pattern( - env, - var_store, - scope, - output, - WhenBranch, - &loc_pattern.value, - loc_pattern.region, - ); - - patterns.push(can_pattern); - } - - let (value, mut branch_output) = canonicalize_expr( - env, - var_store, - scope, - branch.value.region, - &branch.value.value, - ); - - let guard = match &branch.guard { - None => None, - Some(loc_expr) => { - let (can_guard, guard_branch_output) = - canonicalize_expr(env, var_store, scope, loc_expr.region, &loc_expr.value); - - branch_output.union(guard_branch_output); - Some(can_guard) - } - }; - - let references = branch_output.references.clone(); - output.union(branch_output); - - // Now that we've collected all the references for this branch, check to see if - // any of the new idents it defined were unused. If any were, report it. - for (symbol, region) in BindingsFromPattern::new_many(patterns.iter()) { - if !output.references.has_value_lookup(symbol) { - env.problem(Problem::UnusedDef(symbol, region)); - } - } - - ( - WhenBranch { - patterns, - value, - guard, - redundant: RedundantMark::new(var_store), - }, - references, - ) -} - -enum CanonicalizeRecordProblem { - InvalidOptionalValue { - field_name: Lowercase, - field_region: Region, - record_region: Region, - }, -} -fn canonicalize_fields<'a>( - env: &mut Env<'a>, - var_store: &mut VarStore, - scope: &mut Scope, - region: Region, - fields: &'a [Loc>>], -) -> Result<(SendMap, Output), CanonicalizeRecordProblem> { - let mut can_fields = SendMap::default(); - let mut output = Output::default(); - - for loc_field in fields.iter() { - match canonicalize_field(env, var_store, scope, &loc_field.value, loc_field.region) { - Ok((label, field_expr, field_out, field_var)) => { - let field = Field { - var: field_var, - region: loc_field.region, - loc_expr: Box::new(field_expr), - }; - - let replaced = can_fields.insert(label.clone(), field); - - if let Some(old) = replaced { - env.problems.push(Problem::DuplicateRecordFieldValue { - field_name: label, - field_region: loc_field.region, - record_region: region, - replaced_region: old.region, - }); - } - - output.references.union_mut(&field_out.references); - } - Err(CanonicalizeFieldProblem::InvalidOptionalValue { - field_name, - field_region, - }) => { - env.problems.push(Problem::InvalidOptionalValue { - field_name: field_name.clone(), - field_region, - record_region: region, - }); - return Err(CanonicalizeRecordProblem::InvalidOptionalValue { - field_name, - field_region, - record_region: region, - }); - } - } - } - - Ok((can_fields, output)) -} - -enum CanonicalizeFieldProblem { - InvalidOptionalValue { - field_name: Lowercase, - field_region: Region, - }, -} -fn canonicalize_field<'a>( - env: &mut Env<'a>, - var_store: &mut VarStore, - scope: &mut Scope, - field: &'a ast::AssignedField<'a, ast::Expr<'a>>, - region: Region, -) -> Result<(Lowercase, Loc, Output, Variable), CanonicalizeFieldProblem> { - use roc_parse::ast::AssignedField::*; - - match field { - // Both a label and a value, e.g. `{ name: "blah" }` - RequiredValue(label, _, loc_expr) => { - let field_var = var_store.fresh(); - let (loc_can_expr, output) = - canonicalize_expr(env, var_store, scope, loc_expr.region, &loc_expr.value); - - Ok(( - Lowercase::from(label.value), - loc_can_expr, - output, - field_var, - )) - } - - OptionalValue(label, _, loc_expr) => Err(CanonicalizeFieldProblem::InvalidOptionalValue { - field_name: Lowercase::from(label.value), - field_region: Region::span_across(&label.region, &loc_expr.region), - }), - - // A label with no value, e.g. `{ name }` (this is sugar for { name: name }) - LabelOnly(_) => { - panic!("Somehow a LabelOnly record field was not desugared!"); - } - - SpaceBefore(sub_field, _) | SpaceAfter(sub_field, _) => { - canonicalize_field(env, var_store, scope, sub_field, region) - } - - Malformed(_string) => { - panic!("TODO canonicalize malformed record field"); - } - } -} - -fn canonicalize_var_lookup( - env: &mut Env<'_>, - var_store: &mut VarStore, - scope: &mut Scope, - module_name: &str, - ident: &str, - region: Region, -) -> (Expr, Output) { - use Expr::*; - - let mut output = Output::default(); - let can_expr = if module_name.is_empty() { - // Since module_name was empty, this is an unqualified var. - // Look it up in scope! - match scope.lookup_str(ident, region) { - Ok(symbol) => { - output.references.insert_value_lookup(symbol); - - if scope.abilities_store.is_ability_member_name(symbol) { - AbilityMember( - symbol, - scope.abilities_store.fresh_specialization_id(), - var_store.fresh(), - ) - } else { - Var(symbol) - } - } - Err(problem) => { - env.problem(Problem::RuntimeError(problem.clone())); - - RuntimeError(problem) - } - } - } else { - // Since module_name was nonempty, this is a qualified var. - // Look it up in the env! - match env.qualified_lookup(scope, module_name, ident, region) { - Ok(symbol) => { - output.references.insert_value_lookup(symbol); - - if scope.abilities_store.is_ability_member_name(symbol) { - AbilityMember( - symbol, - scope.abilities_store.fresh_specialization_id(), - var_store.fresh(), - ) - } else { - Var(symbol) - } - } - Err(problem) => { - // Either the module wasn't imported, or - // it was imported but it doesn't expose this ident. - env.problem(Problem::RuntimeError(problem.clone())); - - RuntimeError(problem) - } - } - }; - - // If it's valid, this ident should be in scope already. - - (can_expr, output) -} - -/// Currently uses the heuristic of "only inline if it's a builtin" -pub fn inline_calls(var_store: &mut VarStore, scope: &mut Scope, expr: Expr) -> Expr { - use Expr::*; - - match expr { - // Num stores the `a` variable in `Num a`. Not the same as the variable - // stored in Int and Float below, which is strictly for better error messages - other @ Num(..) - | other @ Int(..) - | other @ Float(..) - | other @ Str { .. } - | other @ SingleQuote(_) - | other @ RuntimeError(_) - | other @ EmptyRecord - | other @ Accessor { .. } - | other @ Update { .. } - | other @ Var(_) - | other @ AbilityMember(..) - | other @ RunLowLevel { .. } - | other @ TypedHole { .. } - | other @ ForeignCall { .. } => other, - - List { - elem_var, - loc_elems, - } => { - let mut new_elems = Vec::with_capacity(loc_elems.len()); - - for loc_elem in loc_elems { - let value = inline_calls(var_store, scope, loc_elem.value); - - new_elems.push(Loc { - value, - region: loc_elem.region, - }); - } - - List { - elem_var, - loc_elems: new_elems, - } - } - // Branching - When { - cond_var, - expr_var, - region, - loc_cond, - branches, - branches_cond_var, - exhaustive, - } => { - let loc_cond = Box::new(Loc { - region: loc_cond.region, - value: inline_calls(var_store, scope, loc_cond.value), - }); - - let mut new_branches = Vec::with_capacity(branches.len()); - - for branch in branches { - let value = Loc { - value: inline_calls(var_store, scope, branch.value.value), - region: branch.value.region, - }; - let guard = match branch.guard { - Some(loc_expr) => Some(Loc { - region: loc_expr.region, - value: inline_calls(var_store, scope, loc_expr.value), - }), - None => None, - }; - let new_branch = WhenBranch { - patterns: branch.patterns, - value, - guard, - redundant: RedundantMark::new(var_store), - }; - - new_branches.push(new_branch); - } - - When { - cond_var, - expr_var, - region, - loc_cond, - branches: new_branches, - branches_cond_var, - exhaustive, - } - } - If { - cond_var, - branch_var, - branches, - final_else, - } => { - let mut new_branches = Vec::with_capacity(branches.len()); - - for (loc_cond, loc_expr) in branches { - let loc_cond = Loc { - value: inline_calls(var_store, scope, loc_cond.value), - region: loc_cond.region, - }; - - let loc_expr = Loc { - value: inline_calls(var_store, scope, loc_expr.value), - region: loc_expr.region, - }; - - new_branches.push((loc_cond, loc_expr)); - } - - let final_else = Box::new(Loc { - region: final_else.region, - value: inline_calls(var_store, scope, final_else.value), - }); - - If { - cond_var, - branch_var, - branches: new_branches, - final_else, - } - } - - Expect { - loc_condition, - loc_continuation, - lookups_in_cond, - } => { - let loc_condition = Loc { - region: loc_condition.region, - value: inline_calls(var_store, scope, loc_condition.value), - }; - - let loc_continuation = Loc { - region: loc_continuation.region, - value: inline_calls(var_store, scope, loc_continuation.value), - }; - - Expect { - loc_condition: Box::new(loc_condition), - loc_continuation: Box::new(loc_continuation), - lookups_in_cond, - } - } - - LetRec(defs, loc_expr, mark) => { - let mut new_defs = Vec::with_capacity(defs.len()); - - for def in defs { - new_defs.push(Def { - loc_pattern: def.loc_pattern, - loc_expr: Loc { - region: def.loc_expr.region, - value: inline_calls(var_store, scope, def.loc_expr.value), - }, - expr_var: def.expr_var, - pattern_vars: def.pattern_vars, - annotation: def.annotation, - }); - } - - let loc_expr = Loc { - region: loc_expr.region, - value: inline_calls(var_store, scope, loc_expr.value), - }; - - LetRec(new_defs, Box::new(loc_expr), mark) - } - - LetNonRec(def, loc_expr) => { - let def = Def { - loc_pattern: def.loc_pattern, - loc_expr: Loc { - region: def.loc_expr.region, - value: inline_calls(var_store, scope, def.loc_expr.value), - }, - expr_var: def.expr_var, - pattern_vars: def.pattern_vars, - annotation: def.annotation, - }; - - let loc_expr = Loc { - region: loc_expr.region, - value: inline_calls(var_store, scope, loc_expr.value), - }; - - LetNonRec(Box::new(def), Box::new(loc_expr)) - } - - Closure(ClosureData { - function_type, - closure_type, - return_type, - recursive, - name, - captured_symbols, - arguments, - loc_body, - }) => { - let loc_expr = *loc_body; - let loc_expr = Loc { - value: inline_calls(var_store, scope, loc_expr.value), - region: loc_expr.region, - }; - - Closure(ClosureData { - function_type, - closure_type, - return_type, - recursive, - name, - captured_symbols, - arguments, - loc_body: Box::new(loc_expr), - }) - } - - Record { record_var, fields } => { - todo!( - "Inlining for Record with record_var {:?} and fields {:?}", - record_var, - fields - ); - } - - Access { - record_var, - ext_var, - field_var, - loc_expr, - field, - } => { - todo!("Inlining for Access with record_var {:?}, ext_var {:?}, field_var {:?}, loc_expr {:?}, field {:?}", record_var, ext_var, field_var, loc_expr, field); - } - - Tag { - variant_var, - ext_var, - name, - arguments, - } => { - todo!( - "Inlining for Tag with variant_var {:?}, ext_var {:?}, name {:?}, arguments {:?}", - variant_var, - ext_var, - name, - arguments - ); - } - - OpaqueRef { - opaque_var, - name, - argument, - specialized_def_type, - type_arguments, - lambda_set_variables, - } => { - let (var, loc_expr) = *argument; - let argument = Box::new(( - var, - loc_expr.map_owned(|expr| inline_calls(var_store, scope, expr)), - )); - - OpaqueRef { - opaque_var, - name, - argument, - specialized_def_type, - type_arguments, - lambda_set_variables, - } - } - - ZeroArgumentTag { - closure_name, - variant_var, - ext_var, - name, - } => { - todo!( - "Inlining for ZeroArgumentTag with closure_name {:?}, variant_var {:?}, ext_var {:?}, name {:?}", - closure_name, - variant_var, - ext_var, - name, - ); - } - - Call(boxed_tuple, args, called_via) => { - let (fn_var, loc_expr, closure_var, expr_var) = *boxed_tuple; - - match loc_expr.value { - Var(symbol) if symbol.is_builtin() => match builtin_defs_map(symbol, var_store) { - Some(Def { - loc_expr: - Loc { - value: - Closure(ClosureData { - recursive, - arguments: params, - loc_body: boxed_body, - .. - }), - .. - }, - .. - }) => { - debug_assert_eq!(recursive, Recursive::NotRecursive); - - // Since this is a canonicalized Expr, we should have - // already detected any arity mismatches and replaced this - // with a RuntimeError if there was a mismatch. - debug_assert_eq!(params.len(), args.len()); - - // Start with the function's body as the answer. - let mut loc_answer = *boxed_body; - - // Wrap the body in one LetNonRec for each argument, - // such that at the end we have all the arguments in - // scope with the values the caller provided. - for ((_param_var, _exhaustive_mark, loc_pattern), (expr_var, loc_expr)) in - params.iter().cloned().zip(args.into_iter()).rev() - { - // TODO get the correct vars into here. - // Not sure if param_var should be involved. - let pattern_vars = SendMap::default(); - - let def = Def { - loc_pattern, - loc_expr, - expr_var, - pattern_vars, - annotation: None, - }; - - loc_answer = Loc { - region: Region::zero(), - value: LetNonRec(Box::new(def), Box::new(loc_answer)), - }; - } - - loc_answer.value - } - Some(_) => { - unreachable!("Tried to inline a non-function"); - } - None => { - unreachable!( - "Tried to inline a builtin that wasn't registered: {:?}", - symbol - ); - } - }, - _ => { - // For now, we only inline calls to builtins. Leave this alone! - Call( - Box::new((fn_var, loc_expr, closure_var, expr_var)), - args, - called_via, - ) - } - } - } - } -} - -fn flatten_str_literal<'a>( - env: &mut Env<'a>, - var_store: &mut VarStore, - scope: &mut Scope, - literal: &StrLiteral<'a>, -) -> (Expr, Output) { - use ast::StrLiteral::*; - - match literal { - PlainLine(str_slice) => (Expr::Str((*str_slice).into()), Output::default()), - Line(segments) => flatten_str_lines(env, var_store, scope, &[segments]), - Block(lines) => flatten_str_lines(env, var_store, scope, lines), - } -} - -pub fn is_valid_interpolation(expr: &ast::Expr<'_>) -> bool { - match expr { - ast::Expr::Var { .. } => true, - ast::Expr::Access(sub_expr, _) => is_valid_interpolation(sub_expr), - _ => false, - } -} - -enum StrSegment { - Interpolation(Loc), - Plaintext(Box), -} - -fn flatten_str_lines<'a>( - env: &mut Env<'a>, - var_store: &mut VarStore, - scope: &mut Scope, - lines: &[&[ast::StrSegment<'a>]], -) -> (Expr, Output) { - use ast::StrSegment::*; - - let mut buf = String::new(); - let mut segments = Vec::new(); - let mut output = Output::default(); - - for line in lines { - for segment in line.iter() { - match segment { - Plaintext(string) => { - buf.push_str(string); - } - Unicode(loc_hex_digits) => match u32::from_str_radix(loc_hex_digits.value, 16) { - Ok(code_pt) => match char::from_u32(code_pt) { - Some(ch) => { - buf.push(ch); - } - None => { - env.problem(Problem::InvalidUnicodeCodePt(loc_hex_digits.region)); - - return ( - Expr::RuntimeError(RuntimeError::InvalidUnicodeCodePt( - loc_hex_digits.region, - )), - output, - ); - } - }, - Err(_) => { - env.problem(Problem::InvalidHexadecimal(loc_hex_digits.region)); - - return ( - Expr::RuntimeError(RuntimeError::InvalidHexadecimal( - loc_hex_digits.region, - )), - output, - ); - } - }, - Interpolated(loc_expr) => { - if is_valid_interpolation(loc_expr.value) { - // Interpolations desugar to Str.concat calls - output.references.insert_call(Symbol::STR_CONCAT); - - if !buf.is_empty() { - segments.push(StrSegment::Plaintext(buf.into())); - - buf = String::new(); - } - - let (loc_expr, new_output) = canonicalize_expr( - env, - var_store, - scope, - loc_expr.region, - loc_expr.value, - ); - - output.union(new_output); - - segments.push(StrSegment::Interpolation(loc_expr)); - } else { - env.problem(Problem::InvalidInterpolation(loc_expr.region)); - - return ( - Expr::RuntimeError(RuntimeError::InvalidInterpolation(loc_expr.region)), - output, - ); - } - } - EscapedChar(escaped) => buf.push(unescape_char(escaped)), - } - } - } - - if !buf.is_empty() { - segments.push(StrSegment::Plaintext(buf.into())); - } - - (desugar_str_segments(var_store, segments), output) -} - -/// Resolve string interpolations by desugaring a sequence of StrSegments -/// into nested calls to Str.concat -fn desugar_str_segments(var_store: &mut VarStore, segments: Vec) -> Expr { - use StrSegment::*; - - let mut iter = segments.into_iter().rev(); - let mut loc_expr = match iter.next() { - Some(Plaintext(string)) => Loc::at(Region::zero(), Expr::Str(string)), - Some(Interpolation(loc_expr)) => loc_expr, - None => { - // No segments? Empty string! - - Loc::at(Region::zero(), Expr::Str("".into())) - } - }; - - for seg in iter { - let loc_new_expr = match seg { - Plaintext(string) => Loc::at(Region::zero(), Expr::Str(string)), - Interpolation(loc_interpolated_expr) => loc_interpolated_expr, - }; - - let fn_expr = Loc::at(Region::zero(), Expr::Var(Symbol::STR_CONCAT)); - let expr = Expr::Call( - Box::new(( - var_store.fresh(), - fn_expr, - var_store.fresh(), - var_store.fresh(), - )), - vec![ - (var_store.fresh(), loc_new_expr), - (var_store.fresh(), loc_expr), - ], - CalledVia::StringInterpolation, - ); - - loc_expr = Loc::at(Region::zero(), expr); - } - - loc_expr.value -} - -/// Returns the char that would have been originally parsed to -pub fn unescape_char(escaped: &EscapedChar) -> char { - use EscapedChar::*; - - match escaped { - Backslash => '\\', - Quote => '"', - CarriageReturn => '\r', - Tab => '\t', - Newline => '\n', - } -} - -fn get_lookup_symbols(expr: &Expr, var_store: &mut VarStore) -> Vec<(Symbol, Variable)> { - let mut stack: Vec<&Expr> = vec![expr]; - let mut symbols = Vec::new(); - - while let Some(expr) = stack.pop() { - match expr { - Expr::Var(symbol) | Expr::Update { symbol, .. } | Expr::AbilityMember(symbol, _, _) => { - // Don't introduce duplicates, or make unused variables - if !symbols.iter().any(|(sym, _)| sym == symbol) { - symbols.push((*symbol, var_store.fresh())); - } - } - Expr::List { loc_elems, .. } => { - stack.extend(loc_elems.iter().map(|loc_elem| &loc_elem.value)); - } - Expr::When { - loc_cond, branches, .. - } => { - stack.push(&loc_cond.value); - - stack.reserve(branches.len()); - - for branch in branches { - stack.push(&branch.value.value); - - if let Some(guard) = &branch.guard { - stack.push(&guard.value); - } - } - } - Expr::If { - branches, - final_else, - .. - } => { - stack.reserve(1 + branches.len() * 2); - - for (loc_cond, loc_body) in branches { - stack.push(&loc_cond.value); - stack.push(&loc_body.value); - } - - stack.push(&final_else.value); - } - Expr::LetRec(_, _, _) => todo!(), - Expr::LetNonRec { .. } => todo!(), - Expr::Call(boxed_expr, args, _called_via) => { - stack.reserve(1 + args.len()); - - match &boxed_expr.1.value { - Expr::Var(_) => { - // do nothing - } - function_expr => { - // add the expr being called - stack.push(function_expr); - } - } - - for (_var, loc_arg) in args { - stack.push(&loc_arg.value); - } - } - Expr::Tag { arguments, .. } => { - stack.extend(arguments.iter().map(|(_var, loc_expr)| &loc_expr.value)); - } - Expr::RunLowLevel { args, .. } | Expr::ForeignCall { args, .. } => { - stack.extend(args.iter().map(|(_var, arg)| arg)); - } - Expr::OpaqueRef { argument, .. } => { - stack.push(&argument.1.value); - } - Expr::Access { loc_expr, .. } - | Expr::Closure(ClosureData { - loc_body: loc_expr, .. - }) => { - stack.push(&loc_expr.value); - } - Expr::Record { fields, .. } => { - stack.extend(fields.iter().map(|(_, field)| &field.loc_expr.value)); - } - Expr::Expect { - loc_continuation, .. - } => { - stack.push(&(*loc_continuation).value); - - // Intentionally ignore the lookups in the nested `expect` condition itself, - // because they couldn't possibly influence the outcome of this `expect`! - } - Expr::Num(_, _, _, _) - | Expr::Float(_, _, _, _, _) - | Expr::Int(_, _, _, _, _) - | Expr::Str(_) - | Expr::ZeroArgumentTag { .. } - | Expr::Accessor(_) - | Expr::SingleQuote(_) - | Expr::EmptyRecord - | Expr::TypedHole(_) - | Expr::RuntimeError(_) => {} - } - } - - symbols -} diff --git a/compiler/can/src/lib.rs b/compiler/can/src/lib.rs deleted file mode 100644 index b140f93267..0000000000 --- a/compiler/can/src/lib.rs +++ /dev/null @@ -1,21 +0,0 @@ -#![warn(clippy::dbg_macro)] -// See github.com/rtfeldman/roc/issues/800 for discussion of the large_enum_variant check. -#![allow(clippy::large_enum_variant)] -pub mod abilities; -pub mod annotation; -pub mod builtins; -pub mod constraint; -pub mod def; -pub mod effect_module; -pub mod env; -pub mod exhaustive; -pub mod expected; -pub mod expr; -pub mod module; -pub mod num; -pub mod operator; -pub mod pattern; -pub mod procedure; -pub mod scope; -pub mod string; -pub mod traverse; diff --git a/compiler/can/src/module.rs b/compiler/can/src/module.rs deleted file mode 100644 index 45c2f7790d..0000000000 --- a/compiler/can/src/module.rs +++ /dev/null @@ -1,809 +0,0 @@ -use crate::abilities::AbilitiesStore; -use crate::annotation::canonicalize_annotation; -use crate::def::{canonicalize_toplevel_defs, sort_can_defs, Declaration, Def}; -use crate::effect_module::HostedGeneratedFunctions; -use crate::env::Env; -use crate::expr::{ClosureData, Expr, Output, PendingDerives}; -use crate::operator::desugar_toplevel_defs; -use crate::pattern::Pattern; -use crate::scope::Scope; -use bumpalo::Bump; -use roc_collections::{MutMap, SendMap, VecSet}; -use roc_module::ident::Ident; -use roc_module::ident::Lowercase; -use roc_module::symbol::{IdentIds, IdentIdsByModule, ModuleId, ModuleIds, Symbol}; -use roc_parse::ast::{Defs, TypeAnnotation}; -use roc_parse::header::HeaderFor; -use roc_parse::pattern::PatternType; -use roc_problem::can::{Problem, RuntimeError}; -use roc_region::all::{Loc, Region}; -use roc_types::subs::{VarStore, Variable}; -use roc_types::types::{Alias, AliasKind, AliasVar, Type}; - -#[derive(Debug)] -pub struct Module { - pub module_id: ModuleId, - pub exposed_imports: MutMap, - pub exposed_symbols: VecSet, - pub referenced_values: VecSet, - pub referenced_types: VecSet, - /// all aliases. `bool` indicates whether it is exposed - pub aliases: MutMap, - pub rigid_variables: RigidVariables, - pub abilities_store: AbilitiesStore, -} - -#[derive(Debug, Default)] -pub struct RigidVariables { - pub named: MutMap, - pub able: MutMap, - pub wildcards: VecSet, -} - -#[derive(Debug)] -pub struct ModuleOutput { - pub aliases: MutMap, - pub rigid_variables: RigidVariables, - pub declarations: Vec, - pub exposed_imports: MutMap, - pub lookups: Vec<(Symbol, Variable, Region)>, - pub problems: Vec, - pub referenced_values: VecSet, - pub referenced_types: VecSet, - pub symbols_from_requires: Vec<(Loc, Loc)>, - pub pending_derives: PendingDerives, - pub scope: Scope, -} - -fn validate_generate_with<'a>( - generate_with: &'a [Loc>], -) -> (HostedGeneratedFunctions, Vec>) { - let mut functions = HostedGeneratedFunctions::default(); - let mut unknown = Vec::new(); - - for generated in generate_with { - match generated.value.as_str() { - "after" => functions.after = true, - "map" => functions.map = true, - "always" => functions.always = true, - "loop" => functions.loop_ = true, - "forever" => functions.forever = true, - other => { - // we don't know how to generate this function - let ident = Ident::from(other); - unknown.push(Loc::at(generated.region, ident)); - } - } - } - - (functions, unknown) -} - -#[derive(Debug)] -enum GeneratedInfo { - Hosted { - effect_symbol: Symbol, - generated_functions: HostedGeneratedFunctions, - }, - Builtin, - NotSpecial, -} - -impl GeneratedInfo { - fn from_header_for<'a>( - env: &mut Env, - scope: &mut Scope, - var_store: &mut VarStore, - header_for: &HeaderFor<'a>, - ) -> Self { - match header_for { - HeaderFor::Hosted { - generates, - generates_with, - } => { - let name: &str = generates.into(); - let (generated_functions, unknown_generated) = - validate_generate_with(generates_with); - - for unknown in unknown_generated { - env.problem(Problem::UnknownGeneratesWith(unknown)); - } - - let effect_symbol = scope.introduce(name.into(), Region::zero()).unwrap(); - - { - let a_var = var_store.fresh(); - - let actual = - crate::effect_module::build_effect_actual(Type::Variable(a_var), var_store); - - scope.add_alias( - effect_symbol, - Region::zero(), - vec![Loc::at_zero(AliasVar::unbound("a".into(), a_var))], - actual, - AliasKind::Opaque, - ); - } - - GeneratedInfo::Hosted { - effect_symbol, - generated_functions, - } - } - HeaderFor::Builtin { generates_with } => { - debug_assert!(generates_with.is_empty()); - GeneratedInfo::Builtin - } - _ => GeneratedInfo::NotSpecial, - } - } -} - -fn has_no_implementation(expr: &Expr) -> bool { - match expr { - Expr::RuntimeError(RuntimeError::NoImplementationNamed { .. }) => true, - Expr::Closure(closure_data) - if matches!( - closure_data.loc_body.value, - Expr::RuntimeError(RuntimeError::NoImplementationNamed { .. }) - ) => - { - true - } - - _ => false, - } -} - -// TODO trim these down -#[allow(clippy::too_many_arguments)] -pub fn canonicalize_module_defs<'a>( - arena: &'a Bump, - loc_defs: &'a mut Defs<'a>, - header_for: &roc_parse::header::HeaderFor, - home: ModuleId, - module_ids: &'a ModuleIds, - exposed_ident_ids: IdentIds, - dep_idents: &'a IdentIdsByModule, - aliases: MutMap, - imported_abilities_state: AbilitiesStore, - exposed_imports: MutMap, - exposed_symbols: &VecSet, - symbols_from_requires: &[(Loc, Loc>)], - var_store: &mut VarStore, -) -> ModuleOutput { - let mut can_exposed_imports = MutMap::default(); - let mut scope = Scope::new(home, exposed_ident_ids, imported_abilities_state); - let mut env = Env::new(home, dep_idents, module_ids); - let num_deps = dep_idents.len(); - - for (name, alias) in aliases.into_iter() { - scope.add_alias( - name, - alias.region, - alias.type_variables, - alias.typ, - alias.kind, - ); - } - - let generated_info = - GeneratedInfo::from_header_for(&mut env, &mut scope, var_store, header_for); - - // Desugar operators (convert them to Apply calls, taking into account - // operator precedence and associativity rules), before doing other canonicalization. - // - // If we did this *during* canonicalization, then each time we - // visited a BinOp node we'd recursively try to apply this to each of its nested - // operators, and then again on *their* nested operators, ultimately applying the - // rules multiple times unnecessarily. - desugar_toplevel_defs(arena, loc_defs); - - let mut lookups = Vec::with_capacity(num_deps); - let mut rigid_variables = RigidVariables::default(); - - // Exposed values are treated like defs that appear before any others, e.g. - // - // imports [Foo.{ bar, baz }] - // - // ...is basically the same as if we'd added these extra defs at the start of the module: - // - // bar = Foo.bar - // baz = Foo.baz - // - // Here we essentially add those "defs" to "the beginning of the module" - // by canonicalizing them right before we canonicalize the actual ast::Def nodes. - for (ident, (symbol, region)) in exposed_imports { - let first_char = ident.as_inline_str().as_str().chars().next().unwrap(); - - if first_char.is_lowercase() { - // this is a value definition - let expr_var = var_store.fresh(); - - match scope.import(ident, symbol, region) { - Ok(()) => { - // Add an entry to exposed_imports using the current module's name - // as the key; e.g. if this is the Foo module and we have - // exposes [Bar.{ baz }] then insert Foo.baz as the key, so when - // anything references `baz` in this Foo module, it will resolve to Bar.baz. - can_exposed_imports.insert(symbol, expr_var); - - // This will be used during constraint generation, - // to add the usual Lookup constraint as if this were a normal def. - lookups.push((symbol, expr_var, region)); - } - Err((_shadowed_symbol, _region)) => { - panic!("TODO gracefully handle shadowing in imports.") - } - } - } else if [ - Symbol::LIST_LIST, - Symbol::STR_STR, - Symbol::DICT_DICT, - Symbol::SET_SET, - Symbol::BOX_BOX_TYPE, - ] - .contains(&symbol) - { - // These are not aliases but Apply's and we make sure they are always in scope - } else { - // This is a type alias or ability - - // the symbol should already be added to the scope when this module is canonicalized - debug_assert!( - scope.contains_alias(symbol) || scope.abilities_store.is_ability(symbol), - "The {:?} is not a type alias or ability known in {:?}", - symbol, - home - ); - - // but now we know this symbol by a different identifier, so we still need to add it to - // the scope - match scope.import(ident, symbol, region) { - Ok(()) => { - // here we do nothing special - } - Err((shadowed_symbol, _region)) => { - panic!( - "TODO gracefully handle shadowing in imports, {:?} is shadowed.", - shadowed_symbol - ) - } - } - } - } - - let (defs, output, symbols_introduced) = canonicalize_toplevel_defs( - &mut env, - Output::default(), - var_store, - &mut scope, - loc_defs, - PatternType::TopLevelDef, - ); - - let pending_derives = output.pending_derives; - - // See if any of the new idents we defined went unused. - // If any were unused and also not exposed, report it. - for (symbol, region) in symbols_introduced { - if !output.references.has_type_or_value_lookup(symbol) - && !exposed_symbols.contains(&symbol) - && !scope.abilities_store.is_specialization_name(symbol) - { - env.problem(Problem::UnusedDef(symbol, region)); - } - } - - for named in output.introduced_variables.named { - rigid_variables.named.insert(named.variable, named.name); - } - - for able in output.introduced_variables.able { - rigid_variables - .able - .insert(able.variable, (able.name, able.ability)); - } - - for var in output.introduced_variables.wildcards { - rigid_variables.wildcards.insert(var.value); - } - - let mut referenced_values = VecSet::default(); - let mut referenced_types = VecSet::default(); - - // Gather up all the symbols that were referenced across all the defs' lookups. - referenced_values.extend(output.references.value_lookups().copied()); - referenced_types.extend(output.references.type_lookups().copied()); - - // Gather up all the symbols that were referenced across all the defs' calls. - referenced_values.extend(output.references.calls().copied()); - - // Gather up all the symbols that were referenced from other modules. - referenced_values.extend(env.qualified_value_lookups.iter().copied()); - referenced_types.extend(env.qualified_type_lookups.iter().copied()); - - // add any builtins used by other builtins - let transitive_builtins: Vec = referenced_values - .iter() - .filter(|s| s.is_builtin()) - .flat_map(|s| crate::builtins::builtin_dependencies(*s)) - .copied() - .collect(); - - referenced_values.extend(transitive_builtins); - - // NOTE previously we inserted builtin defs into the list of defs here - // this is now done later, in file.rs. - - // assume all exposed symbols are not actually defined in the module - // then as we walk the module and encounter the definitions, remove - // symbols from this set - let mut exposed_but_not_defined = exposed_symbols.clone(); - - let new_output = Output { - aliases: output.aliases, - ..Default::default() - }; - - let (mut declarations, mut output) = sort_can_defs(&mut env, var_store, defs, new_output); - - debug_assert!( - output.pending_derives.is_empty(), - "I thought pending derives are only found during def introduction" - ); - - let symbols_from_requires = symbols_from_requires - .iter() - .map(|(symbol, loc_ann)| { - // We've already canonicalized the module, so there are no pending abilities. - let pending_abilities_in_scope = &[]; - - let ann = canonicalize_annotation( - &mut env, - &mut scope, - &loc_ann.value, - loc_ann.region, - var_store, - pending_abilities_in_scope, - ); - - ann.add_to( - &mut output.aliases, - &mut output.references, - &mut output.introduced_variables, - ); - - ( - *symbol, - Loc { - value: ann.typ, - region: loc_ann.region, - }, - ) - }) - .collect(); - - if let GeneratedInfo::Hosted { - effect_symbol, - generated_functions, - } = generated_info - { - let mut exposed_symbols = VecSet::default(); - - // NOTE this currently builds all functions, not just the ones that the user requested - crate::effect_module::build_effect_builtins( - &mut scope, - effect_symbol, - var_store, - &mut exposed_symbols, - &mut declarations, - generated_functions, - ); - } - - use crate::def::Declaration::*; - for decl in declarations.iter_mut() { - match decl { - Declare(def) => { - for (symbol, _) in def.pattern_vars.iter() { - if exposed_but_not_defined.contains(symbol) { - // Remove this from exposed_symbols, - // so that at the end of the process, - // we can see if there were any - // exposed symbols which did not have - // corresponding defs. - exposed_but_not_defined.remove(symbol); - } - } - - // Temporary hack: we don't know exactly what symbols are hosted symbols, - // and which are meant to be normal definitions without a body. So for now - // we just assume they are hosted functions (meant to be provided by the platform) - if has_no_implementation(&def.loc_expr.value) { - match generated_info { - GeneratedInfo::Builtin => { - let symbol = def.pattern_vars.iter().next().unwrap().0; - match crate::builtins::builtin_defs_map(*symbol, var_store) { - None => { - panic!("A builtin module contains a signature without implementation for {:?}", symbol) - } - Some(mut replacement_def) => { - replacement_def.annotation = def.annotation.take(); - *def = replacement_def; - } - } - } - GeneratedInfo::Hosted { effect_symbol, .. } => { - let symbol = def.pattern_vars.iter().next().unwrap().0; - let ident_id = symbol.ident_id(); - let ident = scope - .locals - .ident_ids - .get_name(ident_id) - .unwrap() - .to_string(); - let def_annotation = def.annotation.clone().unwrap(); - let annotation = crate::annotation::Annotation { - typ: def_annotation.signature, - introduced_variables: def_annotation.introduced_variables, - references: Default::default(), - aliases: Default::default(), - }; - - let hosted_def = crate::effect_module::build_host_exposed_def( - &mut scope, - *symbol, - &ident, - effect_symbol, - var_store, - annotation, - ); - - *def = hosted_def; - } - _ => (), - } - } - } - DeclareRec(defs, _) => { - for def in defs { - for (symbol, _) in def.pattern_vars.iter() { - if exposed_but_not_defined.contains(symbol) { - // Remove this from exposed_symbols, - // so that at the end of the process, - // we can see if there were any - // exposed symbols which did not have - // corresponding defs. - exposed_but_not_defined.remove(symbol); - } - } - } - } - - InvalidCycle(entries) => { - env.problems.push(Problem::BadRecursion(entries.to_vec())); - } - Builtin(def) => { - // Builtins cannot be exposed in module declarations. - // This should never happen! - debug_assert!(def - .pattern_vars - .iter() - .all(|(symbol, _)| !exposed_but_not_defined.contains(symbol))); - } - } - } - - let mut aliases = MutMap::default(); - - if let GeneratedInfo::Hosted { effect_symbol, .. } = generated_info { - // Remove this from exposed_symbols, - // so that at the end of the process, - // we can see if there were any - // exposed symbols which did not have - // corresponding defs. - exposed_but_not_defined.remove(&effect_symbol); - - let hosted_alias = scope.lookup_alias(effect_symbol).unwrap().clone(); - aliases.insert(effect_symbol, hosted_alias); - } - - for (symbol, alias) in output.aliases { - // Remove this from exposed_symbols, - // so that at the end of the process, - // we can see if there were any - // exposed symbols which did not have - // corresponding defs. - exposed_but_not_defined.remove(&symbol); - - aliases.insert(symbol, alias); - } - - for (ability, members) in scope - .abilities_store - .iter_abilities() - .filter(|(ab, _)| ab.module_id() == home) - { - exposed_but_not_defined.remove(&ability); - members.iter().for_each(|member| { - debug_assert!(member.module_id() == home); - exposed_but_not_defined.remove(member); - }); - } - - // By this point, all exposed symbols should have been removed from - // exposed_symbols and added to exposed_vars_by_symbol. If any were - // not, that means they were declared as exposed but there was - // no actual declaration with that name! - for symbol in exposed_but_not_defined { - env.problem(Problem::ExposedButNotDefined(symbol)); - - // In case this exposed value is referenced by other modules, - // create a decl for it whose implementation is a runtime error. - let mut pattern_vars = SendMap::default(); - pattern_vars.insert(symbol, var_store.fresh()); - - let runtime_error = RuntimeError::ExposedButNotDefined(symbol); - let def = Def { - loc_pattern: Loc::at(Region::zero(), Pattern::Identifier(symbol)), - loc_expr: Loc::at(Region::zero(), Expr::RuntimeError(runtime_error)), - expr_var: var_store.fresh(), - pattern_vars, - annotation: None, - }; - - declarations.push(Declaration::Declare(def)); - } - - // Incorporate any remaining output.lookups entries into references. - referenced_values.extend(output.references.value_lookups().copied()); - referenced_types.extend(output.references.type_lookups().copied()); - - // Incorporate any remaining output.calls entries into references. - referenced_values.extend(output.references.calls().copied()); - - // Gather up all the symbols that were referenced from other modules. - referenced_values.extend(env.qualified_value_lookups.iter().copied()); - referenced_types.extend(env.qualified_type_lookups.iter().copied()); - - for declaration in declarations.iter_mut() { - match declaration { - Declare(def) => fix_values_captured_in_closure_def(def, &mut VecSet::default()), - DeclareRec(defs, _) => { - fix_values_captured_in_closure_defs(defs, &mut VecSet::default()) - } - InvalidCycle(_) | Builtin(_) => {} - } - } - - ModuleOutput { - scope, - aliases, - rigid_variables, - declarations, - referenced_values, - referenced_types, - exposed_imports: can_exposed_imports, - problems: env.problems, - symbols_from_requires, - pending_derives, - lookups, - } -} - -fn fix_values_captured_in_closure_def( - def: &mut crate::def::Def, - no_capture_symbols: &mut VecSet, -) { - // patterns can contain default expressions, so much go over them too! - fix_values_captured_in_closure_pattern(&mut def.loc_pattern.value, no_capture_symbols); - - fix_values_captured_in_closure_expr(&mut def.loc_expr.value, no_capture_symbols); -} - -fn fix_values_captured_in_closure_defs( - defs: &mut [crate::def::Def], - no_capture_symbols: &mut VecSet, -) { - // recursive defs cannot capture each other - for def in defs.iter() { - no_capture_symbols.extend( - crate::traverse::symbols_introduced_from_pattern(&def.loc_pattern).map(|ls| ls.value), - ); - } - - // TODO mutually recursive functions should both capture the union of both their capture sets - - for def in defs.iter_mut() { - fix_values_captured_in_closure_def(def, no_capture_symbols); - } -} - -fn fix_values_captured_in_closure_pattern( - pattern: &mut crate::pattern::Pattern, - no_capture_symbols: &mut VecSet, -) { - use crate::pattern::Pattern::*; - - match pattern { - AppliedTag { - arguments: loc_args, - .. - } => { - for (_, loc_arg) in loc_args.iter_mut() { - fix_values_captured_in_closure_pattern(&mut loc_arg.value, no_capture_symbols); - } - } - UnwrappedOpaque { argument, .. } => { - let (_, loc_arg) = &mut **argument; - fix_values_captured_in_closure_pattern(&mut loc_arg.value, no_capture_symbols); - } - RecordDestructure { destructs, .. } => { - for loc_destruct in destructs.iter_mut() { - use crate::pattern::DestructType::*; - match &mut loc_destruct.value.typ { - Required => {} - Optional(_, loc_expr) => { - fix_values_captured_in_closure_expr(&mut loc_expr.value, no_capture_symbols) - } - Guard(_, loc_pattern) => fix_values_captured_in_closure_pattern( - &mut loc_pattern.value, - no_capture_symbols, - ), - } - } - } - Identifier(_) - | NumLiteral(..) - | IntLiteral(..) - | FloatLiteral(..) - | StrLiteral(_) - | SingleQuote(_) - | Underscore - | Shadowed(..) - | MalformedPattern(_, _) - | UnsupportedPattern(_) - | OpaqueNotInScope(..) - | AbilityMemberSpecialization { .. } => (), - } -} - -fn fix_values_captured_in_closure_expr( - expr: &mut crate::expr::Expr, - no_capture_symbols: &mut VecSet, -) { - use crate::expr::Expr::*; - - match expr { - LetNonRec(def, loc_expr) => { - // LetNonRec(Box, Box>, Variable, Aliases), - fix_values_captured_in_closure_def(def, no_capture_symbols); - fix_values_captured_in_closure_expr(&mut loc_expr.value, no_capture_symbols); - } - LetRec(defs, loc_expr, _) => { - // LetRec(Vec, Box>, Variable, Aliases), - fix_values_captured_in_closure_defs(defs, no_capture_symbols); - fix_values_captured_in_closure_expr(&mut loc_expr.value, no_capture_symbols); - } - - Expect { - loc_condition, - loc_continuation, - lookups_in_cond: _, - } => { - fix_values_captured_in_closure_expr(&mut loc_condition.value, no_capture_symbols); - fix_values_captured_in_closure_expr(&mut loc_continuation.value, no_capture_symbols); - } - - Closure(ClosureData { - captured_symbols, - name, - arguments, - loc_body, - .. - }) => { - captured_symbols.retain(|(s, _)| !no_capture_symbols.contains(s)); - captured_symbols.retain(|(s, _)| s != name); - - if captured_symbols.is_empty() { - no_capture_symbols.insert(*name); - } - - // patterns can contain default expressions, so much go over them too! - for (_, _, loc_pat) in arguments.iter_mut() { - fix_values_captured_in_closure_pattern(&mut loc_pat.value, no_capture_symbols); - } - - fix_values_captured_in_closure_expr(&mut loc_body.value, no_capture_symbols); - } - - Num(..) - | Int(..) - | Float(..) - | Str(_) - | SingleQuote(_) - | Var(_) - | AbilityMember(..) - | EmptyRecord - | TypedHole { .. } - | RuntimeError(_) - | ZeroArgumentTag { .. } - | Accessor { .. } => {} - - List { loc_elems, .. } => { - for elem in loc_elems.iter_mut() { - fix_values_captured_in_closure_expr(&mut elem.value, no_capture_symbols); - } - } - - When { - loc_cond, branches, .. - } => { - fix_values_captured_in_closure_expr(&mut loc_cond.value, no_capture_symbols); - - for branch in branches.iter_mut() { - fix_values_captured_in_closure_expr(&mut branch.value.value, no_capture_symbols); - - // patterns can contain default expressions, so much go over them too! - for loc_pat in branch.patterns.iter_mut() { - fix_values_captured_in_closure_pattern(&mut loc_pat.value, no_capture_symbols); - } - - if let Some(guard) = &mut branch.guard { - fix_values_captured_in_closure_expr(&mut guard.value, no_capture_symbols); - } - } - } - - If { - branches, - final_else, - .. - } => { - for (loc_cond, loc_then) in branches.iter_mut() { - fix_values_captured_in_closure_expr(&mut loc_cond.value, no_capture_symbols); - fix_values_captured_in_closure_expr(&mut loc_then.value, no_capture_symbols); - } - - fix_values_captured_in_closure_expr(&mut final_else.value, no_capture_symbols); - } - - Call(function, arguments, _) => { - fix_values_captured_in_closure_expr(&mut function.1.value, no_capture_symbols); - - for (_, loc_arg) in arguments.iter_mut() { - fix_values_captured_in_closure_expr(&mut loc_arg.value, no_capture_symbols); - } - } - RunLowLevel { args, .. } | ForeignCall { args, .. } => { - for (_, arg) in args.iter_mut() { - fix_values_captured_in_closure_expr(arg, no_capture_symbols); - } - } - - Record { fields, .. } - | Update { - updates: fields, .. - } => { - for (_, field) in fields.iter_mut() { - fix_values_captured_in_closure_expr(&mut field.loc_expr.value, no_capture_symbols); - } - } - - Access { loc_expr, .. } => { - fix_values_captured_in_closure_expr(&mut loc_expr.value, no_capture_symbols); - } - - Tag { arguments, .. } => { - for (_, loc_arg) in arguments.iter_mut() { - fix_values_captured_in_closure_expr(&mut loc_arg.value, no_capture_symbols); - } - } - OpaqueRef { argument, .. } => { - let (_, loc_arg) = &mut **argument; - fix_values_captured_in_closure_expr(&mut loc_arg.value, no_capture_symbols); - } - } -} diff --git a/compiler/can/src/num.rs b/compiler/can/src/num.rs deleted file mode 100644 index 9a7041f8b4..0000000000 --- a/compiler/can/src/num.rs +++ /dev/null @@ -1,355 +0,0 @@ -use crate::env::Env; -use crate::expr::{Expr, IntValue}; -use roc_parse::ast::Base; -use roc_problem::can::Problem; -use roc_problem::can::RuntimeError::*; -use roc_problem::can::{FloatErrorKind, IntErrorKind}; -use roc_region::all::Region; -pub use roc_types::num::{FloatBound, FloatWidth, IntBound, IntWidth, NumBound, SignDemand}; -use roc_types::subs::VarStore; - -use std::str; - -#[inline(always)] -pub fn num_expr_from_result( - var_store: &mut VarStore, - result: Result<(&str, ParsedNumResult), (&str, IntErrorKind)>, - region: Region, - env: &mut Env, -) -> Expr { - match result { - Ok((str, ParsedNumResult::UnknownNum(num, bound))) => { - Expr::Num(var_store.fresh(), (*str).into(), num, bound) - } - Ok((str, ParsedNumResult::Int(num, bound))) => Expr::Int( - var_store.fresh(), - var_store.fresh(), - (*str).into(), - num, - bound, - ), - Ok((str, ParsedNumResult::Float(num, bound))) => Expr::Float( - var_store.fresh(), - var_store.fresh(), - (*str).into(), - num, - bound, - ), - Err((raw, error)) => { - // (Num *) compiles to Int if it doesn't - // get specialized to something else first, - // so use int's overflow bounds here. - let runtime_error = InvalidInt(error, Base::Decimal, region, raw.into()); - - env.problem(Problem::RuntimeError(runtime_error.clone())); - - Expr::RuntimeError(runtime_error) - } - } -} - -#[inline(always)] -pub fn int_expr_from_result( - var_store: &mut VarStore, - result: Result<(&str, IntValue, IntBound), (&str, IntErrorKind)>, - region: Region, - base: Base, - env: &mut Env, -) -> Expr { - // Int stores a variable to generate better error messages - match result { - Ok((str, int, bound)) => Expr::Int( - var_store.fresh(), - var_store.fresh(), - (*str).into(), - int, - bound, - ), - Err((raw, error)) => { - let runtime_error = InvalidInt(error, base, region, raw.into()); - - env.problem(Problem::RuntimeError(runtime_error.clone())); - - Expr::RuntimeError(runtime_error) - } - } -} - -#[inline(always)] -pub fn float_expr_from_result( - var_store: &mut VarStore, - result: Result<(&str, f64, FloatBound), (&str, FloatErrorKind)>, - region: Region, - env: &mut Env, -) -> Expr { - // Float stores a variable to generate better error messages - match result { - Ok((str, float, bound)) => Expr::Float( - var_store.fresh(), - var_store.fresh(), - (*str).into(), - float, - bound, - ), - Err((raw, error)) => { - let runtime_error = InvalidFloat(error, region, raw.into()); - - env.problem(Problem::RuntimeError(runtime_error.clone())); - - Expr::RuntimeError(runtime_error) - } - } -} - -pub enum ParsedNumResult { - Int(IntValue, IntBound), - Float(f64, FloatBound), - UnknownNum(IntValue, NumBound), -} - -#[inline(always)] -pub fn finish_parsing_num(raw: &str) -> Result<(&str, ParsedNumResult), (&str, IntErrorKind)> { - // Ignore underscores. - let radix = 10; - let (_, raw_without_suffix) = parse_literal_suffix(raw); - match from_str_radix(raw.replace('_', "").as_str(), radix) { - Ok(result) => Ok((raw_without_suffix, result)), - Err(e) => Err((raw, e)), - } -} - -#[inline(always)] -pub fn finish_parsing_base( - raw: &str, - base: Base, - is_negative: bool, -) -> Result<(IntValue, IntBound), (&str, IntErrorKind)> { - let radix = match base { - Base::Hex => 16, - Base::Decimal => 10, - Base::Octal => 8, - Base::Binary => 2, - }; - - // Ignore underscores, insert - when negative to get correct underflow/overflow behavior - (if is_negative { - from_str_radix(format!("-{}", raw.replace('_', "")).as_str(), radix) - } else { - from_str_radix(raw.replace('_', "").as_str(), radix) - }) - .and_then(|parsed| match parsed { - ParsedNumResult::Float(..) => Err(IntErrorKind::FloatSuffix), - ParsedNumResult::Int(val, bound) => Ok((val, bound)), - ParsedNumResult::UnknownNum(val, NumBound::None) => Ok((val, IntBound::None)), - ParsedNumResult::UnknownNum(val, NumBound::AtLeastIntOrFloat { sign, width }) => { - Ok((val, IntBound::AtLeast { sign, width })) - } - }) - .map_err(|e| (raw, e)) -} - -#[inline(always)] -pub fn finish_parsing_float(raw: &str) -> Result<(&str, f64, FloatBound), (&str, FloatErrorKind)> { - let (opt_bound, raw_without_suffix) = parse_literal_suffix(raw); - - let bound = match opt_bound { - None => FloatBound::None, - Some(ParsedWidth::Float(fw)) => FloatBound::Exact(fw), - Some(ParsedWidth::Int(_)) => return Err((raw, FloatErrorKind::IntSuffix)), - }; - - // Ignore underscores. - match raw_without_suffix.replace('_', "").parse::() { - Ok(float) if float.is_finite() => Ok((raw_without_suffix, float, bound)), - Ok(float) => { - if float.is_sign_positive() { - Err((raw, FloatErrorKind::PositiveInfinity)) - } else { - Err((raw, FloatErrorKind::NegativeInfinity)) - } - } - Err(_) => Err((raw, FloatErrorKind::Error)), - } -} - -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -enum ParsedWidth { - Int(IntWidth), - Float(FloatWidth), -} - -fn parse_literal_suffix(num_str: &str) -> (Option, &str) { - macro_rules! parse_num_suffix { - ($($suffix:expr, $width:expr)*) => {$( - if num_str.ends_with($suffix) { - return (Some($width), num_str.get(0..num_str.len() - $suffix.len()).unwrap()); - } - )*} - } - - parse_num_suffix! { - "u8", ParsedWidth::Int(IntWidth::U8) - "u16", ParsedWidth::Int(IntWidth::U16) - "u32", ParsedWidth::Int(IntWidth::U32) - "u64", ParsedWidth::Int(IntWidth::U64) - "u128", ParsedWidth::Int(IntWidth::U128) - "i8", ParsedWidth::Int(IntWidth::I8) - "i16", ParsedWidth::Int(IntWidth::I16) - "i32", ParsedWidth::Int(IntWidth::I32) - "i64", ParsedWidth::Int(IntWidth::I64) - "i128", ParsedWidth::Int(IntWidth::I128) - "nat", ParsedWidth::Int(IntWidth::Nat) - "dec", ParsedWidth::Float(FloatWidth::Dec) - "f32", ParsedWidth::Float(FloatWidth::F32) - "f64", ParsedWidth::Float(FloatWidth::F64) - } - - (None, num_str) -} - -/// Integer parsing code taken from the rust libcore, -/// pulled in so we can give custom error messages -/// -/// The Rust Project is dual-licensed under either Apache 2.0 or MIT, -/// at the user's choice. License information can be found in -/// the LEGAL_DETAILS file in the root directory of this distribution. -/// -/// Thanks to the Rust project and its contributors! -fn from_str_radix(src: &str, radix: u32) -> Result { - use self::IntErrorKind::*; - - assert!( - (2..=36).contains(&radix), - "from_str_radix_int: must lie in the range `[2, 36]` - found {}", - radix - ); - - let (opt_exact_bound, src) = parse_literal_suffix(src); - - use std::num::IntErrorKind as StdIEK; - let result = match i128::from_str_radix(src, radix) { - Ok(result) => IntValue::I128(result.to_ne_bytes()), - Err(pie) => match pie.kind() { - StdIEK::Empty => return Err(IntErrorKind::Empty), - StdIEK::InvalidDigit => return Err(IntErrorKind::InvalidDigit), - StdIEK::NegOverflow => return Err(IntErrorKind::Underflow), - StdIEK::PosOverflow => { - // try a u128 - match u128::from_str_radix(src, radix) { - Ok(result) => IntValue::U128(result.to_ne_bytes()), - Err(pie) => match pie.kind() { - StdIEK::InvalidDigit => return Err(IntErrorKind::InvalidDigit), - StdIEK::PosOverflow => return Err(IntErrorKind::Overflow), - StdIEK::Empty | StdIEK::Zero | StdIEK::NegOverflow => unreachable!(), - _ => unreachable!("I thought all possibilities were exhausted, but std::num added a new one") - }, - } - } - StdIEK::Zero => unreachable!("Parsed a i128"), - _ => unreachable!( - "I thought all possibilities were exhausted, but std::num added a new one" - ), - }, - }; - - let (lower_bound, is_negative) = match result { - IntValue::I128(bytes) => { - let num = i128::from_ne_bytes(bytes); - - (lower_bound_of_int(num), num < 0) - } - IntValue::U128(_) => (IntWidth::U128, false), - }; - - match opt_exact_bound { - None => { - // There's no exact bound, but we do have a lower bound. - let sign_demand = if is_negative { - SignDemand::Signed - } else { - SignDemand::NoDemand - }; - Ok(ParsedNumResult::UnknownNum( - result, - NumBound::AtLeastIntOrFloat { - sign: sign_demand, - width: lower_bound, - }, - )) - } - Some(ParsedWidth::Float(fw)) => { - // For now, assume floats can represent all integers - // TODO: this is somewhat incorrect, revisit - Ok(ParsedNumResult::Float( - match result { - IntValue::I128(n) => i128::from_ne_bytes(n) as f64, - IntValue::U128(n) => i128::from_ne_bytes(n) as f64, - }, - FloatBound::Exact(fw), - )) - } - Some(ParsedWidth::Int(exact_width)) => { - // We need to check if the exact bound >= lower bound. - if exact_width.is_superset(&lower_bound, is_negative) { - // Great! Use the exact bound. - Ok(ParsedNumResult::Int(result, IntBound::Exact(exact_width))) - } else { - // This is something like 200i8; the lower bound is u8, which holds strictly more - // ints on the positive side than i8 does. Report an error depending on which side - // of the integers we checked. - let err = if is_negative { - UnderflowsSuffix { - suffix_type: exact_width.type_str(), - min_value: exact_width.min_value(), - } - } else { - OverflowsSuffix { - suffix_type: exact_width.type_str(), - max_value: exact_width.max_value(), - } - }; - Err(err) - } - } - } -} - -fn lower_bound_of_int(result: i128) -> IntWidth { - use IntWidth::*; - if result >= 0 { - // Positive - let result = result as u128; - if result > U64.max_value() { - I128 - } else if result > I64.max_value() { - U64 - } else if result > U32.max_value() { - I64 - } else if result > I32.max_value() { - U32 - } else if result > U16.max_value() { - I32 - } else if result > I16.max_value() { - U16 - } else if result > U8.max_value() { - I16 - } else if result > I8.max_value() { - U8 - } else { - I8 - } - } else { - // Negative - if result < I64.min_value() { - I128 - } else if result < I32.min_value() { - I64 - } else if result < I16.min_value() { - I32 - } else if result < I8.min_value() { - I16 - } else { - I8 - } - } -} diff --git a/compiler/can/src/operator.rs b/compiler/can/src/operator.rs deleted file mode 100644 index 5dafc42c5f..0000000000 --- a/compiler/can/src/operator.rs +++ /dev/null @@ -1,596 +0,0 @@ -#![allow(clippy::manual_map)] - -use bumpalo::collections::Vec; -use bumpalo::Bump; -use roc_module::called_via::BinOp::Pizza; -use roc_module::called_via::{BinOp, CalledVia}; -use roc_module::ident::ModuleName; -use roc_parse::ast::Expr::{self, *}; -use roc_parse::ast::{AssignedField, Def, TypeDef, ValueDef, WhenBranch}; -use roc_region::all::{Loc, Region}; - -// BinOp precedence logic adapted from Gluon by Markus Westerlind -// https://github.com/gluon-lang/gluon - license information can be found in -// the LEGAL_DETAILS file in the root directory of this distribution. -// -// Thank you, Markus! - -fn new_op_call_expr<'a>( - arena: &'a Bump, - left: &'a Loc>, - loc_op: Loc, - right: &'a Loc>, -) -> Loc> { - let region = Region::span_across(&left.region, &right.region); - - let value = match loc_op.value { - Pizza => { - // Rewrite the Pizza operator into an Apply - - match &right.value { - Apply(function, arguments, _called_via) => { - let mut args = Vec::with_capacity_in(1 + arguments.len(), arena); - - args.push(left); - args.extend(arguments.iter()); - - let args = args.into_bump_slice(); - - Apply(function, args, CalledVia::BinOp(Pizza)) - } - _ => { - // e.g. `1 |> (if b then (\a -> a) else (\c -> c))` - Apply(right, arena.alloc([left]), CalledVia::BinOp(Pizza)) - } - } - } - binop => { - // This is a normal binary operator like (+), so desugar it - // into the appropriate function call. - let (module_name, ident) = binop_to_function(binop); - - let args = arena.alloc([left, right]); - - let loc_expr = arena.alloc(Loc { - value: Expr::Var { module_name, ident }, - region: loc_op.region, - }); - - Apply(loc_expr, args, CalledVia::BinOp(binop)) - } - }; - - Loc { region, value } -} - -fn desugar_def_helps<'a>( - arena: &'a Bump, - region: Region, - defs: &'a [&'a Loc>], - loc_ret: &'a Loc>, -) -> &'a Loc> { - let mut desugared_defs = Vec::with_capacity_in(defs.len(), arena); - - for loc_def in defs.iter() { - let loc_def = Loc { - value: desugar_def(arena, &loc_def.value), - region: loc_def.region, - }; - - desugared_defs.push(&*arena.alloc(loc_def)); - } - - let desugared_defs = desugared_defs.into_bump_slice(); - - arena.alloc(Loc { - value: Defs(desugared_defs, desugar_expr(arena, loc_ret)), - region, - }) -} - -fn desugar_type_def<'a>(def: &'a TypeDef<'a>) -> TypeDef<'a> { - use TypeDef::*; - - match def { - alias @ Alias { .. } => *alias, - opaque @ Opaque { .. } => *opaque, - ability @ Ability { .. } => *ability, - } -} - -fn desugar_value_def<'a>(arena: &'a Bump, def: &'a ValueDef<'a>) -> ValueDef<'a> { - use ValueDef::*; - - match def { - Body(loc_pattern, loc_expr) => Body(loc_pattern, desugar_expr(arena, loc_expr)), - ann @ Annotation(_, _) => *ann, - AnnotatedBody { - ann_pattern, - ann_type, - comment, - body_pattern, - body_expr, - } => AnnotatedBody { - ann_pattern, - ann_type, - comment: *comment, - body_pattern: *body_pattern, - body_expr: desugar_expr(arena, body_expr), - }, - Expect(condition) => { - let desugared_condition = &*arena.alloc(desugar_expr(arena, condition)); - Expect(desugared_condition) - } - } -} - -pub fn desugar_def<'a>(arena: &'a Bump, def: &'a Def<'a>) -> Def<'a> { - use roc_parse::ast::Def::*; - - match def { - Type(type_def) => Type(desugar_type_def(type_def)), - Value(value_def) => Value(desugar_value_def(arena, value_def)), - SpaceBefore(def, _) | SpaceAfter(def, _) => desugar_def(arena, def), - NotYetImplemented(s) => todo!("{}", s), - } -} - -pub fn desugar_toplevel_defs<'a>(arena: &'a Bump, defs: &mut roc_parse::ast::Defs<'a>) { - for value_def in defs.value_defs.iter_mut() { - *value_def = desugar_value_def(arena, arena.alloc(*value_def)); - } -} - -/// Reorder the expression tree based on operator precedence and associativity rules, -/// then replace the BinOp nodes with Apply nodes. Also drop SpaceBefore and SpaceAfter nodes. -pub fn desugar_expr<'a>(arena: &'a Bump, loc_expr: &'a Loc>) -> &'a Loc> { - match &loc_expr.value { - Float(..) - | Num(..) - | NonBase10Int { .. } - | Str(_) - | SingleQuote(_) - | AccessorFunction(_) - | Var { .. } - | Underscore { .. } - | MalformedIdent(_, _) - | MalformedClosure - | PrecedenceConflict { .. } - | Tag(_) - | OpaqueRef(_) => loc_expr, - - Access(sub_expr, paths) => { - let region = loc_expr.region; - let loc_sub_expr = Loc { - region, - value: **sub_expr, - }; - let value = Access(&desugar_expr(arena, arena.alloc(loc_sub_expr)).value, paths); - - arena.alloc(Loc { region, value }) - } - List(items) => { - let mut new_items = Vec::with_capacity_in(items.len(), arena); - - for item in items.iter() { - new_items.push(desugar_expr(arena, item)); - } - let new_items = new_items.into_bump_slice(); - let value: Expr<'a> = List(items.replace_items(new_items)); - - arena.alloc(Loc { - region: loc_expr.region, - value, - }) - } - Record(fields) => arena.alloc(Loc { - region: loc_expr.region, - value: Record(fields.map_items(arena, |field| { - let value = desugar_field(arena, &field.value); - Loc { - value, - region: field.region, - } - })), - }), - - RecordUpdate { fields, update } => { - // NOTE the `update` field is always a `Var { .. }`, we only desugar it to get rid of - // any spaces before/after - let new_update = desugar_expr(arena, update); - - let new_fields = fields.map_items(arena, |field| { - let value = desugar_field(arena, &field.value); - Loc { - value, - region: field.region, - } - }); - - arena.alloc(Loc { - region: loc_expr.region, - value: RecordUpdate { - update: new_update, - fields: new_fields, - }, - }) - } - Closure(loc_patterns, loc_ret) => arena.alloc(Loc { - region: loc_expr.region, - value: Closure(loc_patterns, desugar_expr(arena, loc_ret)), - }), - Backpassing(loc_patterns, loc_body, loc_ret) => { - // loc_patterns <- loc_body - // - // loc_ret - - // first desugar the body, because it may contain |> - let desugared_body = desugar_expr(arena, loc_body); - - let desugared_ret = desugar_expr(arena, loc_ret); - let closure = Expr::Closure(loc_patterns, desugared_ret); - let loc_closure = Loc::at(loc_expr.region, closure); - - match &desugared_body.value { - Expr::Apply(function, arguments, called_via) => { - let mut new_arguments: Vec<'a, &'a Loc>> = - Vec::with_capacity_in(arguments.len() + 1, arena); - new_arguments.extend(arguments.iter()); - new_arguments.push(arena.alloc(loc_closure)); - - let call = Expr::Apply(function, new_arguments.into_bump_slice(), *called_via); - let loc_call = Loc::at(loc_expr.region, call); - - arena.alloc(loc_call) - } - _ => { - // e.g. `x <- (if b then (\a -> a) else (\c -> c))` - let call = Expr::Apply( - desugared_body, - arena.alloc([&*arena.alloc(loc_closure)]), - CalledVia::Space, - ); - let loc_call = Loc::at(loc_expr.region, call); - - arena.alloc(loc_call) - } - } - } - BinOps(lefts, right) => desugar_bin_ops(arena, loc_expr.region, lefts, right), - Defs(defs, loc_ret) => desugar_def_helps(arena, loc_expr.region, *defs, loc_ret), - Apply(loc_fn, loc_args, called_via) => { - let mut desugared_args = Vec::with_capacity_in(loc_args.len(), arena); - - for loc_arg in loc_args.iter() { - desugared_args.push(desugar_expr(arena, loc_arg)); - } - - let desugared_args = desugared_args.into_bump_slice(); - - arena.alloc(Loc { - value: Apply(desugar_expr(arena, loc_fn), desugared_args, *called_via), - region: loc_expr.region, - }) - } - When(loc_cond_expr, branches) => { - let loc_desugared_cond = &*arena.alloc(desugar_expr(arena, loc_cond_expr)); - let mut desugared_branches = Vec::with_capacity_in(branches.len(), arena); - - for branch in branches.iter() { - let desugared = desugar_expr(arena, &branch.value); - - let mut alternatives = Vec::with_capacity_in(branch.patterns.len(), arena); - alternatives.extend(branch.patterns.iter().copied()); - - let desugared_guard = if let Some(guard) = &branch.guard { - Some(*desugar_expr(arena, guard)) - } else { - None - }; - - let alternatives = alternatives.into_bump_slice(); - - desugared_branches.push(&*arena.alloc(WhenBranch { - patterns: alternatives, - value: *desugared, - guard: desugared_guard, - })); - } - - let desugared_branches = desugared_branches.into_bump_slice(); - - arena.alloc(Loc { - value: When(loc_desugared_cond, desugared_branches), - region: loc_expr.region, - }) - } - UnaryOp(loc_arg, loc_op) => { - use roc_module::called_via::UnaryOp::*; - - let region = loc_op.region; - let op = loc_op.value; - // TODO desugar this in canonicalization instead, so we can work - // in terms of integers exclusively and not need to create strings - // which canonicalization then needs to look up, check if they're exposed, etc - let value = match op { - Negate => Var { - module_name: ModuleName::NUM, - ident: "neg", - }, - Not => Var { - module_name: ModuleName::BOOL, - ident: "not", - }, - }; - let loc_fn_var = arena.alloc(Loc { region, value }); - let desugared_args = arena.alloc([desugar_expr(arena, loc_arg)]); - - arena.alloc(Loc { - value: Apply(loc_fn_var, desugared_args, CalledVia::UnaryOp(op)), - region: loc_expr.region, - }) - } - SpaceBefore(expr, _) | SpaceAfter(expr, _) | ParensAround(expr) => { - // Since we've already begun canonicalization, spaces and parens - // are no longer needed and should be dropped. - desugar_expr( - arena, - arena.alloc(Loc { - value: **expr, - region: loc_expr.region, - }), - ) - } - If(if_thens, final_else_branch) => { - // If does not get desugared into `when` so we can give more targeted error messages during type checking. - let desugared_final_else = &*arena.alloc(desugar_expr(arena, final_else_branch)); - - let mut desugared_if_thens = Vec::with_capacity_in(if_thens.len(), arena); - - for (condition, then_branch) in if_thens.iter() { - desugared_if_thens.push(( - *desugar_expr(arena, condition), - *desugar_expr(arena, then_branch), - )); - } - - arena.alloc(Loc { - value: If(desugared_if_thens.into_bump_slice(), desugared_final_else), - region: loc_expr.region, - }) - } - Expect(condition, continuation) => { - let desugared_condition = &*arena.alloc(desugar_expr(arena, condition)); - let desugared_continuation = &*arena.alloc(desugar_expr(arena, continuation)); - arena.alloc(Loc { - value: Expect(desugared_condition, desugared_continuation), - region: loc_expr.region, - }) - } - } -} - -fn desugar_field<'a>( - arena: &'a Bump, - field: &'a AssignedField<'a, Expr<'a>>, -) -> AssignedField<'a, Expr<'a>> { - use roc_parse::ast::AssignedField::*; - - match field { - RequiredValue(loc_str, spaces, loc_expr) => RequiredValue( - Loc { - value: loc_str.value, - region: loc_str.region, - }, - spaces, - desugar_expr(arena, loc_expr), - ), - OptionalValue(loc_str, spaces, loc_expr) => OptionalValue( - Loc { - value: loc_str.value, - region: loc_str.region, - }, - spaces, - desugar_expr(arena, loc_expr), - ), - LabelOnly(loc_str) => { - // Desugar { x } into { x: x } - let loc_expr = Loc { - value: Var { - module_name: "", - ident: loc_str.value, - }, - region: loc_str.region, - }; - - RequiredValue( - Loc { - value: loc_str.value, - region: loc_str.region, - }, - &[], - desugar_expr(arena, arena.alloc(loc_expr)), - ) - } - SpaceBefore(field, _spaces) => desugar_field(arena, field), - SpaceAfter(field, _spaces) => desugar_field(arena, field), - - Malformed(string) => Malformed(string), - } -} - -// TODO move this desugaring to canonicalization, so we can use Symbols instead of strings -#[inline(always)] -fn binop_to_function(binop: BinOp) -> (&'static str, &'static str) { - use self::BinOp::*; - - match binop { - Caret => (ModuleName::NUM, "pow"), - Star => (ModuleName::NUM, "mul"), - Slash => (ModuleName::NUM, "div"), - DoubleSlash => (ModuleName::NUM, "divTrunc"), - Percent => (ModuleName::NUM, "rem"), - Plus => (ModuleName::NUM, "add"), - Minus => (ModuleName::NUM, "sub"), - Equals => (ModuleName::BOOL, "isEq"), - NotEquals => (ModuleName::BOOL, "isNotEq"), - LessThan => (ModuleName::NUM, "isLt"), - GreaterThan => (ModuleName::NUM, "isGt"), - LessThanOrEq => (ModuleName::NUM, "isLte"), - GreaterThanOrEq => (ModuleName::NUM, "isGte"), - And => (ModuleName::BOOL, "and"), - Or => (ModuleName::BOOL, "or"), - Pizza => unreachable!("Cannot desugar the |> operator"), - Assignment => unreachable!("Cannot desugar the = operator"), - IsAliasType => unreachable!("Cannot desugar the : operator"), - IsOpaqueType => unreachable!("Cannot desugar the := operator"), - Backpassing => unreachable!("Cannot desugar the <- operator"), - } -} - -fn desugar_bin_ops<'a>( - arena: &'a Bump, - whole_region: Region, - lefts: &'a [(Loc>, Loc)], - right: &'a Loc>, -) -> &'a Loc> { - let mut arg_stack: Vec<&'a Loc> = Vec::with_capacity_in(lefts.len() + 1, arena); - let mut op_stack: Vec> = Vec::with_capacity_in(lefts.len(), arena); - - for (loc_expr, loc_op) in lefts { - arg_stack.push(desugar_expr(arena, loc_expr)); - match run_binop_step(arena, whole_region, &mut arg_stack, &mut op_stack, *loc_op) { - Err(problem) => return problem, - Ok(()) => continue, - } - } - - let mut expr = desugar_expr(arena, right); - - for (left, loc_op) in arg_stack.into_iter().zip(op_stack.into_iter()).rev() { - expr = arena.alloc(new_op_call_expr(arena, left, loc_op, expr)); - } - - expr -} - -enum Step<'a> { - Error(&'a Loc>), - Push(Loc), - Skip, -} - -fn run_binop_step<'a>( - arena: &'a Bump, - whole_region: Region, - arg_stack: &mut Vec<&'a Loc>>, - op_stack: &mut Vec>, - next_op: Loc, -) -> Result<(), &'a Loc>> { - use Step::*; - - match binop_step(arena, whole_region, arg_stack, op_stack, next_op) { - Error(problem) => Err(problem), - Push(loc_op) => run_binop_step(arena, whole_region, arg_stack, op_stack, loc_op), - Skip => Ok(()), - } -} - -fn binop_step<'a>( - arena: &'a Bump, - whole_region: Region, - arg_stack: &mut Vec<&'a Loc>>, - op_stack: &mut Vec>, - next_op: Loc, -) -> Step<'a> { - use roc_module::called_via::Associativity::*; - use std::cmp::Ordering; - - match op_stack.pop() { - Some(stack_op) => { - match next_op.value.cmp(&stack_op.value) { - Ordering::Less => { - // Inline - let right = arg_stack.pop().unwrap(); - let left = arg_stack.pop().unwrap(); - - arg_stack.push(arena.alloc(new_op_call_expr(arena, left, stack_op, right))); - - Step::Push(next_op) - } - - Ordering::Greater => { - // Swap - op_stack.push(stack_op); - op_stack.push(next_op); - - Step::Skip - } - - Ordering::Equal => { - match ( - next_op.value.associativity(), - stack_op.value.associativity(), - ) { - (LeftAssociative, LeftAssociative) => { - // Inline - let right = arg_stack.pop().unwrap(); - let left = arg_stack.pop().unwrap(); - - arg_stack - .push(arena.alloc(new_op_call_expr(arena, left, stack_op, right))); - - Step::Push(next_op) - } - - (RightAssociative, RightAssociative) => { - // Swap - op_stack.push(stack_op); - op_stack.push(next_op); - - Step::Skip - } - - (NonAssociative, NonAssociative) => { - // Both operators were non-associative, e.g. (True == False == False). - // We should tell the author to disambiguate by grouping them with parens. - let bad_op = next_op; - let right = arg_stack.pop().unwrap(); - let left = arg_stack.pop().unwrap(); - let broken_expr = - arena.alloc(new_op_call_expr(arena, left, stack_op, right)); - let region = broken_expr.region; - let data = roc_parse::ast::PrecedenceConflict { - whole_region, - binop1_position: stack_op.region.start(), - binop1: stack_op.value, - binop2_position: bad_op.region.start(), - binop2: bad_op.value, - expr: arena.alloc(broken_expr), - }; - let value = Expr::PrecedenceConflict(arena.alloc(data)); - - Step::Error(arena.alloc(Loc { region, value })) - } - - _ => { - // The operators had the same precedence but different associativity. - // - // In many languages, this case can happen due to (for example) <| and |> having the same - // precedence but different associativity. Languages which support custom operators with - // (e.g. Haskell) can potentially have arbitrarily many of these cases. - // - // By design, Roc neither allows custom operators nor has any built-in operators with - // the same precedence and different associativity, so this should never happen! - panic!("BinOps had the same associativity, but different precedence. This should never happen!"); - } - } - } - } - } - None => { - op_stack.push(next_op); - Step::Skip - } - } -} diff --git a/compiler/can/src/pattern.rs b/compiler/can/src/pattern.rs deleted file mode 100644 index b8422cdb44..0000000000 --- a/compiler/can/src/pattern.rs +++ /dev/null @@ -1,789 +0,0 @@ -use crate::annotation::freshen_opaque_def; -use crate::env::Env; -use crate::expr::{canonicalize_expr, unescape_char, Expr, IntValue, Output}; -use crate::num::{ - finish_parsing_base, finish_parsing_float, finish_parsing_num, FloatBound, IntBound, NumBound, - ParsedNumResult, -}; -use crate::scope::Scope; -use roc_module::ident::{Ident, Lowercase, TagName}; -use roc_module::symbol::Symbol; -use roc_parse::ast::{self, StrLiteral, StrSegment}; -use roc_parse::pattern::PatternType; -use roc_problem::can::{MalformedPatternProblem, Problem, RuntimeError, ShadowKind}; -use roc_region::all::{Loc, Region}; -use roc_types::subs::{VarStore, Variable}; -use roc_types::types::{LambdaSet, OptAbleVar, PatternCategory, Type}; - -/// A pattern, including possible problems (e.g. shadowing) so that -/// codegen can generate a runtime error if this pattern is reached. -#[derive(Clone, Debug)] -pub enum Pattern { - Identifier(Symbol), - AppliedTag { - whole_var: Variable, - ext_var: Variable, - tag_name: TagName, - arguments: Vec<(Variable, Loc)>, - }, - UnwrappedOpaque { - whole_var: Variable, - opaque: Symbol, - argument: Box<(Variable, Loc)>, - - // The following help us link this opaque reference to the type specified by its - // definition, which we then use during constraint generation. For example - // suppose we have - // - // Id n := [Id U64 n] - // strToBool : Str -> Bool - // - // f = \@Id who -> strToBool who - // - // Then `opaque` is "Id", `argument` is "who", but this is not enough for us to - // infer the type of the expression as "Id Str" - we need to link the specialized type of - // the variable "n". - // That's what `specialized_def_type` and `type_arguments` are for; they are specialized - // for the expression from the opaque definition. `type_arguments` is something like - // [(n, fresh1)], and `specialized_def_type` becomes "[Id U64 fresh1]". - specialized_def_type: Box, - type_arguments: Vec, - lambda_set_variables: Vec, - }, - RecordDestructure { - whole_var: Variable, - ext_var: Variable, - destructs: Vec>, - }, - NumLiteral(Variable, Box, IntValue, NumBound), - IntLiteral(Variable, Variable, Box, IntValue, IntBound), - FloatLiteral(Variable, Variable, Box, f64, FloatBound), - StrLiteral(Box), - SingleQuote(char), - Underscore, - - /// An identifier that marks a specialization of an ability member. - /// For example, given an ability member definition `hash : a -> U64 | a has Hash`, - /// there may be the specialization `hash : Bool -> U64`. In this case we generate a - /// new symbol for the specialized "hash" identifier. - AbilityMemberSpecialization { - /// The symbol for this specialization. - ident: Symbol, - /// The ability name being specialized. - specializes: Symbol, - }, - - // Runtime Exceptions - Shadowed(Region, Loc, Symbol), - OpaqueNotInScope(Loc), - // Example: (5 = 1 + 2) is an unsupported pattern in an assignment; Int patterns aren't allowed in assignments! - UnsupportedPattern(Region), - // parse error patterns - MalformedPattern(MalformedPatternProblem, Region), -} - -impl Pattern { - pub fn opt_var(&self) -> Option { - use Pattern::*; - match self { - Identifier(_) => None, - - AppliedTag { whole_var, .. } => Some(*whole_var), - UnwrappedOpaque { whole_var, .. } => Some(*whole_var), - RecordDestructure { whole_var, .. } => Some(*whole_var), - NumLiteral(var, ..) => Some(*var), - IntLiteral(var, ..) => Some(*var), - FloatLiteral(var, ..) => Some(*var), - StrLiteral(_) => None, - SingleQuote(_) => None, - Underscore => None, - - AbilityMemberSpecialization { .. } => None, - - Shadowed(..) | OpaqueNotInScope(..) | UnsupportedPattern(..) | MalformedPattern(..) => { - None - } - } - } - - /// Is this pattern sure to cover all instances of a type T, assuming it typechecks against T? - pub fn surely_exhaustive(&self) -> bool { - use Pattern::*; - match self { - Identifier(..) - | Underscore - | Shadowed(..) - | OpaqueNotInScope(..) - | UnsupportedPattern(..) - | MalformedPattern(..) - | AbilityMemberSpecialization { .. } => true, - RecordDestructure { destructs, .. } => destructs.is_empty(), - AppliedTag { .. } - | NumLiteral(..) - | IntLiteral(..) - | FloatLiteral(..) - | StrLiteral(..) - | SingleQuote(..) => false, - UnwrappedOpaque { argument, .. } => { - // Opaques can only match against one constructor (the opaque symbol), so this is - // surely exhaustive against T if the inner pattern is surely exhaustive against - // its type U. - argument.1.value.surely_exhaustive() - } - } - } - - pub fn category(&self) -> PatternCategory { - use Pattern::*; - use PatternCategory as C; - - match self { - Identifier(_) => C::PatternDefault, - - AppliedTag { tag_name, .. } => C::Ctor(tag_name.clone()), - UnwrappedOpaque { opaque, .. } => C::Opaque(*opaque), - RecordDestructure { destructs, .. } if destructs.is_empty() => C::EmptyRecord, - RecordDestructure { .. } => C::Record, - NumLiteral(..) => C::Num, - IntLiteral(..) => C::Int, - FloatLiteral(..) => C::Float, - StrLiteral(_) => C::Str, - SingleQuote(_) => C::Character, - Underscore => C::PatternDefault, - - AbilityMemberSpecialization { .. } => C::PatternDefault, - - Shadowed(..) | OpaqueNotInScope(..) | UnsupportedPattern(..) | MalformedPattern(..) => { - C::PatternDefault - } - } - } -} - -#[derive(Clone, Debug)] -pub struct RecordDestruct { - pub var: Variable, - pub label: Lowercase, - pub symbol: Symbol, - pub typ: DestructType, -} - -#[derive(Clone, Debug)] -pub enum DestructType { - Required, - Optional(Variable, Loc), - Guard(Variable, Loc), -} - -pub fn canonicalize_def_header_pattern<'a>( - env: &mut Env<'a>, - var_store: &mut VarStore, - scope: &mut Scope, - output: &mut Output, - pattern_type: PatternType, - pattern: &ast::Pattern<'a>, - region: Region, -) -> Loc { - use roc_parse::ast::Pattern::*; - - match pattern { - // Identifiers that shadow ability members may appear (and may only appear) at the header of a def. - Identifier(name) => { - match scope.introduce_or_shadow_ability_member((*name).into(), region) { - Ok((symbol, shadowing_ability_member)) => { - let can_pattern = match shadowing_ability_member { - // A fresh identifier. - None => { - output.references.insert_bound(symbol); - Pattern::Identifier(symbol) - } - // Likely a specialization of an ability. - Some(ability_member_name) => { - output.references.insert_value_lookup(ability_member_name); - Pattern::AbilityMemberSpecialization { - ident: symbol, - specializes: ability_member_name, - } - } - }; - Loc::at(region, can_pattern) - } - Err((original_region, shadow, new_symbol)) => { - env.problem(Problem::RuntimeError(RuntimeError::Shadowing { - original_region, - shadow: shadow.clone(), - kind: ShadowKind::Variable, - })); - output.references.insert_bound(new_symbol); - - let can_pattern = Pattern::Shadowed(original_region, shadow, new_symbol); - Loc::at(region, can_pattern) - } - } - } - _ => canonicalize_pattern(env, var_store, scope, output, pattern_type, pattern, region), - } -} - -pub fn canonicalize_pattern<'a>( - env: &mut Env<'a>, - var_store: &mut VarStore, - scope: &mut Scope, - output: &mut Output, - pattern_type: PatternType, - pattern: &ast::Pattern<'a>, - region: Region, -) -> Loc { - use roc_parse::ast::Pattern::*; - use PatternType::*; - - let can_pattern = match pattern { - Identifier(name) => match scope.introduce_str(name, region) { - Ok(symbol) => { - output.references.insert_bound(symbol); - - Pattern::Identifier(symbol) - } - Err((original_region, shadow, new_symbol)) => { - env.problem(Problem::RuntimeError(RuntimeError::Shadowing { - original_region, - shadow: shadow.clone(), - kind: ShadowKind::Variable, - })); - output.references.insert_bound(new_symbol); - - Pattern::Shadowed(original_region, shadow, new_symbol) - } - }, - Tag(name) => { - // Canonicalize the tag's name. - Pattern::AppliedTag { - whole_var: var_store.fresh(), - ext_var: var_store.fresh(), - tag_name: TagName((*name).into()), - arguments: vec![], - } - } - OpaqueRef(name) => { - // If this opaque ref had an argument, we would be in the "Apply" branch. - let loc_name = Loc::at(region, (*name).into()); - env.problem(Problem::RuntimeError(RuntimeError::OpaqueNotApplied( - loc_name, - ))); - Pattern::UnsupportedPattern(region) - } - Apply(tag, patterns) => { - let mut can_patterns = Vec::with_capacity(patterns.len()); - for loc_pattern in *patterns { - let can_pattern = canonicalize_pattern( - env, - var_store, - scope, - output, - pattern_type, - &loc_pattern.value, - loc_pattern.region, - ); - - can_patterns.push((var_store.fresh(), can_pattern)); - } - - match tag.value { - Tag(name) => { - let tag_name = TagName(name.into()); - Pattern::AppliedTag { - whole_var: var_store.fresh(), - ext_var: var_store.fresh(), - tag_name, - arguments: can_patterns, - } - } - - OpaqueRef(name) => match scope.lookup_opaque_ref(name, tag.region) { - Ok((opaque, opaque_def)) => { - debug_assert!(!can_patterns.is_empty()); - - if can_patterns.len() > 1 { - env.problem(Problem::RuntimeError( - RuntimeError::OpaqueAppliedToMultipleArgs(region), - )); - - Pattern::UnsupportedPattern(region) - } else { - let argument = Box::new(can_patterns.pop().unwrap()); - - let (type_arguments, lambda_set_variables, specialized_def_type) = - freshen_opaque_def(var_store, opaque_def); - - output.references.insert_type_lookup(opaque); - - Pattern::UnwrappedOpaque { - whole_var: var_store.fresh(), - opaque, - argument, - specialized_def_type: Box::new(specialized_def_type), - type_arguments, - lambda_set_variables, - } - } - } - Err(runtime_error) => { - env.problem(Problem::RuntimeError(runtime_error)); - - Pattern::OpaqueNotInScope(Loc::at(tag.region, name.into())) - } - }, - _ => unreachable!("Other patterns cannot be applied"), - } - } - - &FloatLiteral(str) => match pattern_type { - WhenBranch => match finish_parsing_float(str) { - Err(_error) => { - let problem = MalformedPatternProblem::MalformedFloat; - malformed_pattern(env, problem, region) - } - Ok((str_without_suffix, float, bound)) => Pattern::FloatLiteral( - var_store.fresh(), - var_store.fresh(), - str_without_suffix.into(), - float, - bound, - ), - }, - ptype => unsupported_pattern(env, ptype, region), - }, - - Underscore(_) => match pattern_type { - WhenBranch | FunctionArg => Pattern::Underscore, - TopLevelDef | DefExpr => bad_underscore(env, region), - }, - - &NumLiteral(str) => match pattern_type { - WhenBranch => match finish_parsing_num(str) { - Err(_error) => { - let problem = MalformedPatternProblem::MalformedInt; - malformed_pattern(env, problem, region) - } - Ok((parsed, ParsedNumResult::UnknownNum(int, bound))) => { - Pattern::NumLiteral(var_store.fresh(), (parsed).into(), int, bound) - } - Ok((parsed, ParsedNumResult::Int(int, bound))) => Pattern::IntLiteral( - var_store.fresh(), - var_store.fresh(), - (parsed).into(), - int, - bound, - ), - Ok((parsed, ParsedNumResult::Float(float, bound))) => Pattern::FloatLiteral( - var_store.fresh(), - var_store.fresh(), - (parsed).into(), - float, - bound, - ), - }, - ptype => unsupported_pattern(env, ptype, region), - }, - - &NonBase10Literal { - string, - base, - is_negative, - } => match pattern_type { - WhenBranch => match finish_parsing_base(string, base, is_negative) { - Err(_error) => { - let problem = MalformedPatternProblem::MalformedBase(base); - malformed_pattern(env, problem, region) - } - Ok((IntValue::U128(_), _)) if is_negative => { - // Can't negate a u128; that doesn't fit in any integer literal type we support. - let problem = MalformedPatternProblem::MalformedInt; - malformed_pattern(env, problem, region) - } - Ok((int, bound)) => { - use std::ops::Neg; - - let sign_str = if is_negative { "-" } else { "" }; - let int_str = format!("{}{}", sign_str, int).into_boxed_str(); - let i = match int { - // Safety: this is fine because I128::MAX = |I128::MIN| - 1 - IntValue::I128(n) if is_negative => { - IntValue::I128(i128::from_ne_bytes(n).neg().to_ne_bytes()) - } - IntValue::I128(n) => IntValue::I128(n), - IntValue::U128(_) => unreachable!(), - }; - Pattern::IntLiteral(var_store.fresh(), var_store.fresh(), int_str, i, bound) - } - }, - ptype => unsupported_pattern(env, ptype, region), - }, - - StrLiteral(literal) => match pattern_type { - WhenBranch => flatten_str_literal(literal), - ptype => unsupported_pattern(env, ptype, region), - }, - - SingleQuote(string) => { - let mut it = string.chars().peekable(); - if let Some(char) = it.next() { - if it.peek().is_none() { - Pattern::SingleQuote(char) - } else { - // multiple chars is found - let problem = MalformedPatternProblem::MultipleCharsInSingleQuote; - malformed_pattern(env, problem, region) - } - } else { - // no characters found - let problem = MalformedPatternProblem::EmptySingleQuote; - malformed_pattern(env, problem, region) - } - } - - SpaceBefore(sub_pattern, _) | SpaceAfter(sub_pattern, _) => { - return canonicalize_pattern( - env, - var_store, - scope, - output, - pattern_type, - sub_pattern, - region, - ) - } - RecordDestructure(patterns) => { - let ext_var = var_store.fresh(); - let whole_var = var_store.fresh(); - let mut destructs = Vec::with_capacity(patterns.len()); - let mut opt_erroneous = None; - - for loc_pattern in patterns.iter() { - match loc_pattern.value { - Identifier(label) => { - match scope.introduce(label.into(), region) { - Ok(symbol) => { - output.references.insert_bound(symbol); - - destructs.push(Loc { - region: loc_pattern.region, - value: RecordDestruct { - var: var_store.fresh(), - label: Lowercase::from(label), - symbol, - typ: DestructType::Required, - }, - }); - } - Err((original_region, shadow, new_symbol)) => { - env.problem(Problem::RuntimeError(RuntimeError::Shadowing { - original_region, - shadow: shadow.clone(), - kind: ShadowKind::Variable, - })); - - // No matter what the other patterns - // are, we're definitely shadowed and will - // get a runtime exception as soon as we - // encounter the first bad pattern. - opt_erroneous = - Some(Pattern::Shadowed(original_region, shadow, new_symbol)); - } - }; - } - - RequiredField(label, loc_guard) => { - // a guard does not introduce the label into scope! - let symbol = - scope.scopeless_symbol(&Ident::from(label), loc_pattern.region); - let can_guard = canonicalize_pattern( - env, - var_store, - scope, - output, - pattern_type, - &loc_guard.value, - loc_guard.region, - ); - - destructs.push(Loc { - region: loc_pattern.region, - value: RecordDestruct { - var: var_store.fresh(), - label: Lowercase::from(label), - symbol, - typ: DestructType::Guard(var_store.fresh(), can_guard), - }, - }); - } - OptionalField(label, loc_default) => { - // an optional DOES introduce the label into scope! - match scope.introduce(label.into(), region) { - Ok(symbol) => { - let (can_default, expr_output) = canonicalize_expr( - env, - var_store, - scope, - loc_default.region, - &loc_default.value, - ); - - // an optional field binds the symbol! - output.references.insert_bound(symbol); - - output.union(expr_output); - - destructs.push(Loc { - region: loc_pattern.region, - value: RecordDestruct { - var: var_store.fresh(), - label: Lowercase::from(label), - symbol, - typ: DestructType::Optional(var_store.fresh(), can_default), - }, - }); - } - Err((original_region, shadow, new_symbol)) => { - env.problem(Problem::RuntimeError(RuntimeError::Shadowing { - original_region, - shadow: shadow.clone(), - kind: ShadowKind::Variable, - })); - - // No matter what the other patterns - // are, we're definitely shadowed and will - // get a runtime exception as soon as we - // encounter the first bad pattern. - opt_erroneous = - Some(Pattern::Shadowed(original_region, shadow, new_symbol)); - } - }; - } - _ => unreachable!("Any other pattern should have given a parse error"), - } - } - - // If we encountered an erroneous pattern (e.g. one with shadowing), - // use the resulting RuntimeError. Otherwise, return a successful record destructure. - opt_erroneous.unwrap_or(Pattern::RecordDestructure { - whole_var, - ext_var, - destructs, - }) - } - - RequiredField(_name, _loc_pattern) => { - unreachable!("should have been handled in RecordDestructure"); - } - OptionalField(_name, _loc_pattern) => { - unreachable!("should have been handled in RecordDestructure"); - } - - Malformed(_str) => { - let problem = MalformedPatternProblem::Unknown; - malformed_pattern(env, problem, region) - } - - MalformedIdent(_str, problem) => { - let problem = MalformedPatternProblem::BadIdent(*problem); - malformed_pattern(env, problem, region) - } - - QualifiedIdentifier { .. } => { - let problem = MalformedPatternProblem::QualifiedIdentifier; - malformed_pattern(env, problem, region) - } - }; - - Loc { - region, - value: can_pattern, - } -} - -/// When we detect an unsupported pattern type (e.g. 5 = 1 + 2 is unsupported because you can't -/// assign to Int patterns), report it to Env and return an UnsupportedPattern runtime error pattern. -fn unsupported_pattern(env: &mut Env, pattern_type: PatternType, region: Region) -> Pattern { - use roc_problem::can::BadPattern; - env.problem(Problem::UnsupportedPattern( - BadPattern::Unsupported(pattern_type), - region, - )); - - Pattern::UnsupportedPattern(region) -} - -fn bad_underscore(env: &mut Env, region: Region) -> Pattern { - use roc_problem::can::BadPattern; - env.problem(Problem::UnsupportedPattern( - BadPattern::UnderscoreInDef, - region, - )); - - Pattern::UnsupportedPattern(region) -} - -/// When we detect a malformed pattern like `3.X` or `0b5`, -/// report it to Env and return an UnsupportedPattern runtime error pattern. -fn malformed_pattern(env: &mut Env, problem: MalformedPatternProblem, region: Region) -> Pattern { - env.problem(Problem::RuntimeError(RuntimeError::MalformedPattern( - problem, region, - ))); - - Pattern::MalformedPattern(problem, region) -} - -/// An iterator over the bindings made by a pattern. -/// -/// We attempt to make no allocations when we can. -pub enum BindingsFromPattern<'a> { - Empty, - One(&'a Loc), - Many(Vec>), -} - -pub enum BindingsFromPatternWork<'a> { - Pattern(&'a Loc), - Destruct(&'a Loc), -} - -impl<'a> BindingsFromPattern<'a> { - pub fn new(initial: &'a Loc) -> Self { - Self::One(initial) - } - - pub fn new_many(mut it: I) -> Self - where - I: Iterator>, - { - if let (1, Some(1)) = it.size_hint() { - Self::new(it.next().unwrap()) - } else { - Self::Many(it.map(BindingsFromPatternWork::Pattern).collect()) - } - } - - fn next_many(stack: &mut Vec>) -> Option<(Symbol, Region)> { - use Pattern::*; - - while let Some(work) = stack.pop() { - match work { - BindingsFromPatternWork::Pattern(loc_pattern) => { - use BindingsFromPatternWork::*; - - match &loc_pattern.value { - Identifier(symbol) - | AbilityMemberSpecialization { - ident: symbol, - specializes: _, - } => { - return Some((*symbol, loc_pattern.region)); - } - AppliedTag { - arguments: loc_args, - .. - } => { - let it = loc_args.iter().rev().map(|(_, p)| Pattern(p)); - stack.extend(it); - } - UnwrappedOpaque { argument, .. } => { - let (_, loc_arg) = &**argument; - stack.push(Pattern(loc_arg)); - } - RecordDestructure { destructs, .. } => { - let it = destructs.iter().rev().map(Destruct); - stack.extend(it); - } - NumLiteral(..) - | IntLiteral(..) - | FloatLiteral(..) - | StrLiteral(_) - | SingleQuote(_) - | Underscore - | Shadowed(_, _, _) - | MalformedPattern(_, _) - | UnsupportedPattern(_) - | OpaqueNotInScope(..) => (), - } - } - BindingsFromPatternWork::Destruct(loc_destruct) => { - match &loc_destruct.value.typ { - DestructType::Required | DestructType::Optional(_, _) => { - return Some((loc_destruct.value.symbol, loc_destruct.region)); - } - DestructType::Guard(_, inner) => { - // a guard does not introduce the symbol - stack.push(BindingsFromPatternWork::Pattern(inner)) - } - } - } - } - } - - None - } -} - -impl<'a> Iterator for BindingsFromPattern<'a> { - type Item = (Symbol, Region); - - fn next(&mut self) -> Option { - use Pattern::*; - - match self { - BindingsFromPattern::Empty => None, - BindingsFromPattern::One(loc_pattern) => match &loc_pattern.value { - Identifier(symbol) - | AbilityMemberSpecialization { - ident: symbol, - specializes: _, - } => { - let region = loc_pattern.region; - *self = Self::Empty; - Some((*symbol, region)) - } - _ => { - *self = Self::Many(vec![BindingsFromPatternWork::Pattern(loc_pattern)]); - self.next() - } - }, - BindingsFromPattern::Many(stack) => Self::next_many(stack), - } - } -} - -fn flatten_str_literal(literal: &StrLiteral<'_>) -> Pattern { - use ast::StrLiteral::*; - - match literal { - PlainLine(str_slice) => Pattern::StrLiteral((*str_slice).into()), - Line(segments) => flatten_str_lines(&[segments]), - Block(lines) => flatten_str_lines(lines), - } -} - -fn flatten_str_lines(lines: &[&[StrSegment<'_>]]) -> Pattern { - use StrSegment::*; - - let mut buf = String::new(); - - for line in lines { - for segment in line.iter() { - match segment { - Plaintext(string) => { - buf.push_str(string); - } - Unicode(loc_digits) => { - todo!("parse unicode digits {:?}", loc_digits); - } - Interpolated(loc_expr) => { - return Pattern::UnsupportedPattern(loc_expr.region); - } - EscapedChar(escaped) => buf.push(unescape_char(escaped)), - } - } - } - - Pattern::StrLiteral(buf.into()) -} diff --git a/compiler/can/src/scope.rs b/compiler/can/src/scope.rs deleted file mode 100644 index 9a07c8c885..0000000000 --- a/compiler/can/src/scope.rs +++ /dev/null @@ -1,785 +0,0 @@ -use roc_collections::VecMap; -use roc_module::ident::Ident; -use roc_module::symbol::{IdentId, IdentIds, ModuleId, Symbol}; -use roc_problem::can::RuntimeError; -use roc_region::all::{Loc, Region}; -use roc_types::types::{Alias, AliasKind, AliasVar, Type}; - -use crate::abilities::AbilitiesStore; - -use bitvec::vec::BitVec; - -#[derive(Clone, Debug)] -pub struct Scope { - /// The type aliases currently in scope - pub aliases: VecMap, - - /// The abilities currently in scope, and their implementors. - pub abilities_store: AbilitiesStore, - - /// The current module being processed. This will be used to turn - /// unqualified idents into Symbols. - home: ModuleId, - - /// The first `exposed_ident_count` identifiers are exposed - exposed_ident_count: usize, - - /// Identifiers that are imported (and introduced in the header) - imports: Vec<(Ident, Symbol, Region)>, - - /// Identifiers that are in scope, and defined in the current module - pub locals: ScopedIdentIds, -} - -impl Scope { - pub fn new( - home: ModuleId, - initial_ident_ids: IdentIds, - starting_abilities_store: AbilitiesStore, - ) -> Scope { - let imports = Symbol::default_in_scope() - .into_iter() - .map(|(a, (b, c))| (a, b, c)) - .collect(); - - Scope { - home, - exposed_ident_count: initial_ident_ids.len(), - locals: ScopedIdentIds::from_ident_ids(home, initial_ident_ids), - aliases: VecMap::default(), - abilities_store: starting_abilities_store, - imports, - } - } - - pub fn lookup(&self, ident: &Ident, region: Region) -> Result { - self.lookup_str(ident.as_str(), region) - } - - pub fn lookup_str(&self, ident: &str, region: Region) -> Result { - use ContainsIdent::*; - - match self.scope_contains_ident(ident) { - InScope(symbol, _) => Ok(symbol), - NotInScope(_) | NotPresent => { - let error = RuntimeError::LookupNotInScope( - Loc { - region, - value: Ident::from(ident), - }, - self.idents_in_scope().map(|v| v.as_ref().into()).collect(), - ); - - Err(error) - } - } - } - - fn idents_in_scope(&self) -> impl Iterator + '_ { - let it1 = self.locals.idents_in_scope(); - let it2 = self.imports.iter().map(|t| t.0.clone()); - - it2.chain(it1) - } - - /// Check if there is an opaque type alias referenced by `opaque_ref` referenced in the - /// current scope. E.g. `@Age` must reference an opaque `Age` declared in this module, not any - /// other! - pub fn lookup_opaque_ref( - &self, - opaque_ref: &str, - lookup_region: Region, - ) -> Result<(Symbol, &Alias), RuntimeError> { - debug_assert!(opaque_ref.starts_with('@')); - let opaque_str = &opaque_ref[1..]; - let opaque = opaque_str.into(); - - match self.locals.has_in_scope(&opaque) { - Some((symbol, _)) => match self.lookup_opaque_alias(symbol) { - Ok(alias) => Ok((symbol, alias)), - Err(opt_alias_def_region) => { - Err(self.opaque_not_defined_error(opaque, lookup_region, opt_alias_def_region)) - } - }, - None => { - // opaque types can only be wrapped/unwrapped in the scope they are defined in (and below) - let error = if let Some((_, decl_region)) = self.has_imported(opaque_str) { - // specific error for when the opaque is imported, which definitely does not work - RuntimeError::OpaqueOutsideScope { - opaque, - referenced_region: lookup_region, - imported_region: decl_region, - } - } else { - self.opaque_not_defined_error(opaque, lookup_region, None) - }; - - Err(error) - } - } - } - - fn lookup_opaque_alias(&self, symbol: Symbol) -> Result<&Alias, Option> { - match self.aliases.get(&symbol) { - None => Err(None), - - Some(alias) => match alias.kind { - AliasKind::Opaque => Ok(alias), - AliasKind::Structural => Err(Some(alias.header_region())), - }, - } - } - - fn is_opaque(&self, ident_id: IdentId, string: &str) -> Option> { - if string.is_empty() { - return None; - } - - let symbol = Symbol::new(self.home, ident_id); - - if let Some(AliasKind::Opaque) = self.aliases.get(&symbol).map(|alias| alias.kind) { - Some(string.into()) - } else { - None - } - } - - fn opaque_not_defined_error( - &self, - opaque: Ident, - lookup_region: Region, - opt_defined_alias: Option, - ) -> RuntimeError { - // for opaques, we only look at the locals because opaques can only be matched - // on in the module that defines them. - let opaques_in_scope = self - .locals - .ident_ids - .ident_strs() - .filter_map(|(ident_id, string)| self.is_opaque(ident_id, string)) - .collect(); - - RuntimeError::OpaqueNotDefined { - usage: Loc::at(lookup_region, opaque), - opaques_in_scope, - opt_defined_alias, - } - } - - fn has_imported(&self, ident: &str) -> Option<(Symbol, Region)> { - for (import, shadow, original_region) in self.imports.iter() { - if ident == import.as_str() { - return Some((*shadow, *original_region)); - } - } - - None - } - - /// Is an identifier in scope, either in the locals or imports - fn scope_contains_ident(&self, ident: &str) -> ContainsIdent { - // exposed imports are likely to be small - match self.has_imported(ident) { - Some((symbol, region)) => ContainsIdent::InScope(symbol, region), - None => self.locals.contains_ident(ident), - } - } - - fn introduce_help(&mut self, ident: &str, region: Region) -> Result { - match self.scope_contains_ident(ident) { - ContainsIdent::InScope(original_symbol, original_region) => { - // the ident is already in scope; up to the caller how to handle that - // (usually it's shadowing, but it is valid to shadow ability members) - Err((original_symbol, original_region)) - } - ContainsIdent::NotPresent => { - // We know nothing about this ident yet; introduce it to the scope - let ident_id = self.locals.introduce_into_scope(ident, region); - Ok(Symbol::new(self.home, ident_id)) - } - ContainsIdent::NotInScope(existing) => { - // The ident is not in scope, but its name is already in the string interner - if existing.index() < self.exposed_ident_count { - // if the identifier is exposed, use the IdentId we already have for it - // other modules depend on the symbol having that IdentId - let symbol = Symbol::new(self.home, existing); - - self.locals.in_scope.set(existing.index(), true); - self.locals.regions[existing.index()] = region; - - Ok(symbol) - } else { - // create a new IdentId that under the hood uses the same string bytes as an existing one - let ident_id = self.locals.introduce_into_scope_duplicate(existing, region); - - Ok(Symbol::new(self.home, ident_id)) - } - } - } - } - - /// Introduce a new ident to scope. - /// - /// Returns Err if this would shadow an existing ident, including the - /// Symbol and Region of the ident we already had in scope under that name. - /// - /// If this ident shadows an existing one, a new ident is allocated for the shadow. This is - /// done so that all identifiers have unique symbols, which is important in particular when - /// we generate code for value identifiers. - /// If this behavior is undesirable, use [`Self::introduce_without_shadow_symbol`]. - pub fn introduce( - &mut self, - ident: Ident, - region: Region, - ) -> Result, Symbol)> { - self.introduce_str(ident.as_str(), region) - } - - pub fn introduce_str( - &mut self, - ident: &str, - region: Region, - ) -> Result, Symbol)> { - match self.introduce_help(ident, region) { - Ok(symbol) => Ok(symbol), - Err((_, original_region)) => { - let shadow = Loc { - value: Ident::from(ident), - region, - }; - let symbol = self.locals.scopeless_symbol(ident, region); - - Err((original_region, shadow, symbol)) - } - } - } - - /// Like [Self::introduce], but does not introduce a new symbol for the shadowing symbol. - pub fn introduce_without_shadow_symbol( - &mut self, - ident: &Ident, - region: Region, - ) -> Result)> { - match self.introduce_help(ident.as_str(), region) { - Err((_, original_region)) => { - let shadow = Loc { - value: ident.clone(), - region, - }; - Err((original_region, shadow)) - } - Ok(symbol) => Ok(symbol), - } - } - - /// Like [Self::introduce], but handles the case of when an ident matches an ability member - /// name. In such cases a new symbol is created for the ident (since it's expected to be a - /// specialization of the ability member), but the ident is not added to the ident->symbol map. - /// - /// If the ident does not match an ability name, the behavior of this function is exactly that - /// of `introduce`. - #[allow(clippy::type_complexity)] - pub fn introduce_or_shadow_ability_member( - &mut self, - ident: Ident, - region: Region, - ) -> Result<(Symbol, Option), (Region, Loc, Symbol)> { - let ident = &ident; - - match self.introduce_help(ident.as_str(), region) { - Err((original_symbol, original_region)) => { - let shadow_symbol = self.scopeless_symbol(ident, region); - - if self.abilities_store.is_ability_member_name(original_symbol) { - self.abilities_store - .register_specializing_symbol(shadow_symbol, original_symbol); - - Ok((shadow_symbol, Some(original_symbol))) - } else { - // This is an illegal shadow. - let shadow = Loc { - value: ident.clone(), - region, - }; - - Err((original_region, shadow, shadow_symbol)) - } - } - Ok(symbol) => Ok((symbol, None)), - } - } - - /// Create a new symbol, but don't add it to the scope (yet) - /// - /// Used for record guards like { x: Just _ } where the `x` is not added to the scope, - /// but also in other places where we need to create a symbol and we don't have the right - /// scope information yet. An identifier can be introduced later, and will use the same IdentId - pub fn scopeless_symbol(&mut self, ident: &Ident, region: Region) -> Symbol { - self.locals.scopeless_symbol(ident.as_str(), region) - } - - /// Import a Symbol from another module into this module's top-level scope. - /// - /// Returns Err if this would shadow an existing ident, including the - /// Symbol and Region of the ident we already had in scope under that name. - pub fn import( - &mut self, - ident: Ident, - symbol: Symbol, - region: Region, - ) -> Result<(), (Symbol, Region)> { - if let Some((s, r)) = self.has_imported(ident.as_str()) { - return Err((s, r)); - } - - self.imports.push((ident, symbol, region)); - - Ok(()) - } - - pub fn add_alias( - &mut self, - name: Symbol, - region: Region, - vars: Vec>, - typ: Type, - kind: AliasKind, - ) { - let alias = create_alias(name, region, vars, typ, kind); - self.aliases.insert(name, alias); - } - - pub fn lookup_alias(&self, symbol: Symbol) -> Option<&Alias> { - self.aliases.get(&symbol) - } - - pub fn contains_alias(&mut self, name: Symbol) -> bool { - self.aliases.contains_key(&name) - } - - pub fn inner_scope(&mut self, f: F) -> T - where - F: FnOnce(&mut Scope) -> T, - { - // store enough information to roll back to the original outer scope - // - // - abilities_store: ability definitions not allowed in inner scopes - // - locals: everything introduced in the inner scope is marked as not in scope in the rollback - // - aliases: stored in a VecMap, we just discard anything added in an inner scope - // - exposed_ident_count: unchanged - // - home: unchanged - let aliases_count = self.aliases.len(); - let locals_snapshot = self.locals.in_scope.len(); - - let result = f(self); - - self.aliases.truncate(aliases_count); - - // anything added in the inner scope is no longer in scope now - for i in locals_snapshot..self.locals.in_scope.len() { - self.locals.in_scope.set(i, false); - } - - result - } - - pub fn register_debug_idents(&self) { - self.home.register_debug_idents(&self.locals.ident_ids) - } - - /// Generates a unique, new symbol like "$1" or "$5", - /// using the home module as the module_id. - /// - /// This is used, for example, during canonicalization of an Expr::Closure - /// to generate a unique symbol to refer to that closure. - pub fn gen_unique_symbol(&mut self) -> Symbol { - Symbol::new(self.home, self.locals.gen_unique()) - } -} - -pub fn create_alias( - name: Symbol, - region: Region, - vars: Vec>, - typ: Type, - kind: AliasKind, -) -> Alias { - let roc_types::types::VariableDetail { - type_variables, - lambda_set_variables, - recursion_variables, - } = typ.variables_detail(); - - debug_assert!({ - let mut hidden = type_variables; - - for loc_var in vars.iter() { - hidden.remove(&loc_var.value.var); - } - - if !hidden.is_empty() { - panic!( - "Found unbound type variables {:?} \n in type alias {:?} {:?} : {:?}", - hidden, name, &vars, &typ - ) - } - - true - }); - - let lambda_set_variables: Vec<_> = lambda_set_variables - .into_iter() - .map(|v| roc_types::types::LambdaSet(Type::Variable(v))) - .collect(); - - Alias { - region, - type_variables: vars, - lambda_set_variables, - recursion_variables, - typ, - kind, - } -} - -#[derive(Debug)] -enum ContainsIdent { - InScope(Symbol, Region), - NotInScope(IdentId), - NotPresent, -} - -#[derive(Clone, Debug)] -pub struct ScopedIdentIds { - pub ident_ids: IdentIds, - in_scope: BitVec, - regions: Vec, - home: ModuleId, -} - -impl ScopedIdentIds { - fn from_ident_ids(home: ModuleId, ident_ids: IdentIds) -> Self { - let capacity = ident_ids.len(); - - Self { - in_scope: BitVec::repeat(false, capacity), - ident_ids, - regions: vec![Region::zero(); capacity], - home, - } - } - - fn has_in_scope(&self, ident: &Ident) -> Option<(Symbol, Region)> { - match self.contains_ident(ident.as_str()) { - ContainsIdent::InScope(symbol, region) => Some((symbol, region)), - ContainsIdent::NotInScope(_) | ContainsIdent::NotPresent => None, - } - } - - fn contains_ident(&self, ident: &str) -> ContainsIdent { - use ContainsIdent::*; - - let mut result = NotPresent; - - for ident_id in self.ident_ids.get_id_many(ident) { - let index = ident_id.index(); - if self.in_scope[index] { - return InScope(Symbol::new(self.home, ident_id), self.regions[index]); - } else { - result = NotInScope(ident_id) - } - } - - result - } - - fn idents_in_scope(&self) -> impl Iterator + '_ { - self.ident_ids - .ident_strs() - .zip(self.in_scope.iter()) - .filter_map(|((_, string), keep)| { - if *keep { - Some(Ident::from(string)) - } else { - None - } - }) - } - - fn introduce_into_scope(&mut self, ident_name: &str, region: Region) -> IdentId { - let id = self.ident_ids.add_str(ident_name); - - debug_assert_eq!(id.index(), self.in_scope.len()); - debug_assert_eq!(id.index(), self.regions.len()); - - self.in_scope.push(true); - self.regions.push(region); - - id - } - - fn introduce_into_scope_duplicate(&mut self, existing: IdentId, region: Region) -> IdentId { - let id = self.ident_ids.duplicate_ident(existing); - - debug_assert_eq!(id.index(), self.in_scope.len()); - debug_assert_eq!(id.index(), self.regions.len()); - - self.in_scope.push(true); - self.regions.push(region); - - id - } - - /// Adds an IdentId, but does not introduce it to the scope - fn scopeless_symbol(&mut self, ident_name: &str, region: Region) -> Symbol { - let id = self.ident_ids.add_str(ident_name); - - debug_assert_eq!(id.index(), self.in_scope.len()); - debug_assert_eq!(id.index(), self.regions.len()); - - self.in_scope.push(false); - self.regions.push(region); - - Symbol::new(self.home, id) - } - - fn gen_unique(&mut self) -> IdentId { - let id = self.ident_ids.gen_unique(); - - debug_assert_eq!(id.index(), self.in_scope.len()); - debug_assert_eq!(id.index(), self.regions.len()); - - self.in_scope.push(false); - self.regions.push(Region::zero()); - - id - } -} - -#[cfg(test)] -mod test { - use super::*; - use roc_module::symbol::ModuleIds; - use roc_region::all::Position; - - use pretty_assertions::{assert_eq, assert_ne}; - - #[test] - fn scope_contains_introduced() { - let _register_module_debug_names = ModuleIds::default(); - let mut scope = Scope::new( - ModuleId::ATTR, - IdentIds::default(), - AbilitiesStore::default(), - ); - - let region = Region::zero(); - let ident = Ident::from("mezolit"); - - assert!(scope.lookup(&ident, region).is_err()); - - assert!(scope.introduce(ident.clone(), region).is_ok()); - - assert!(scope.lookup(&ident, region).is_ok()); - } - - #[test] - fn second_introduce_shadows() { - let _register_module_debug_names = ModuleIds::default(); - let mut scope = Scope::new( - ModuleId::ATTR, - IdentIds::default(), - AbilitiesStore::default(), - ); - - let region1 = Region::from_pos(Position { offset: 10 }); - let region2 = Region::from_pos(Position { offset: 20 }); - let ident = Ident::from("mezolit"); - - assert!(scope.lookup(&ident, Region::zero()).is_err()); - - let first = scope.introduce(ident.clone(), region1).unwrap(); - let (original_region, _ident, shadow_symbol) = - scope.introduce(ident.clone(), region2).unwrap_err(); - - scope.register_debug_idents(); - - assert_ne!(first, shadow_symbol); - assert_eq!(original_region, region1); - - let lookup = scope.lookup(&ident, Region::zero()).unwrap(); - - assert_eq!(first, lookup); - } - - #[test] - fn inner_scope_does_not_influence_outer() { - let _register_module_debug_names = ModuleIds::default(); - let mut scope = Scope::new( - ModuleId::ATTR, - IdentIds::default(), - AbilitiesStore::default(), - ); - - let region = Region::zero(); - let ident = Ident::from("uránia"); - - assert!(scope.lookup(&ident, region).is_err()); - - scope.inner_scope(|inner| { - assert!(inner.introduce(ident.clone(), region).is_ok()); - }); - - assert!(scope.lookup(&ident, region).is_err()); - } - - #[test] - fn default_idents_in_scope() { - let _register_module_debug_names = ModuleIds::default(); - let scope = Scope::new( - ModuleId::ATTR, - IdentIds::default(), - AbilitiesStore::default(), - ); - - let idents: Vec<_> = scope.idents_in_scope().collect(); - - assert_eq!( - &idents, - &[ - Ident::from("Box"), - Ident::from("Set"), - Ident::from("Dict"), - Ident::from("Str"), - Ident::from("Ok"), - Ident::from("False"), - Ident::from("List"), - Ident::from("True"), - Ident::from("Err"), - ] - ); - } - - #[test] - fn idents_with_inner_scope() { - let _register_module_debug_names = ModuleIds::default(); - let mut scope = Scope::new( - ModuleId::ATTR, - IdentIds::default(), - AbilitiesStore::default(), - ); - - let idents: Vec<_> = scope.idents_in_scope().collect(); - - assert_eq!( - &idents, - &[ - Ident::from("Box"), - Ident::from("Set"), - Ident::from("Dict"), - Ident::from("Str"), - Ident::from("Ok"), - Ident::from("False"), - Ident::from("List"), - Ident::from("True"), - Ident::from("Err"), - ] - ); - - let builtin_count = idents.len(); - - let region = Region::zero(); - - let ident1 = Ident::from("uránia"); - let ident2 = Ident::from("malmok"); - let ident3 = Ident::from("Járnak"); - - scope.introduce(ident1.clone(), region).unwrap(); - scope.introduce(ident2.clone(), region).unwrap(); - scope.introduce(ident3.clone(), region).unwrap(); - - let idents: Vec<_> = scope.idents_in_scope().collect(); - - assert_eq!( - &idents[builtin_count..], - &[ident1.clone(), ident2.clone(), ident3.clone(),] - ); - - scope.inner_scope(|inner| { - let ident4 = Ident::from("Ångström"); - let ident5 = Ident::from("Sirály"); - - inner.introduce(ident4.clone(), region).unwrap(); - inner.introduce(ident5.clone(), region).unwrap(); - - let idents: Vec<_> = inner.idents_in_scope().collect(); - - assert_eq!( - &idents[builtin_count..], - &[ - ident1.clone(), - ident2.clone(), - ident3.clone(), - ident4, - ident5 - ] - ); - }); - - let idents: Vec<_> = scope.idents_in_scope().collect(); - - assert_eq!(&idents[builtin_count..], &[ident1, ident2, ident3,]); - } - - #[test] - fn import_is_in_scope() { - let _register_module_debug_names = ModuleIds::default(); - let mut scope = Scope::new( - ModuleId::ATTR, - IdentIds::default(), - AbilitiesStore::default(), - ); - - let ident = Ident::from("product"); - let symbol = Symbol::LIST_PRODUCT; - let region = Region::zero(); - - assert!(scope.lookup(&ident, region).is_err()); - - assert!(scope.import(ident.clone(), symbol, region).is_ok()); - - assert!(scope.lookup(&ident, region).is_ok()); - - assert!(scope.idents_in_scope().any(|x| x == ident)); - } - - #[test] - fn shadow_of_import() { - let _register_module_debug_names = ModuleIds::default(); - let mut scope = Scope::new( - ModuleId::ATTR, - IdentIds::default(), - AbilitiesStore::default(), - ); - - let ident = Ident::from("product"); - let symbol = Symbol::LIST_PRODUCT; - - let region1 = Region::from_pos(Position { offset: 10 }); - let region2 = Region::from_pos(Position { offset: 20 }); - - scope.import(ident.clone(), symbol, region1).unwrap(); - - let (original_region, _ident, shadow_symbol) = - scope.introduce(ident.clone(), region2).unwrap_err(); - - scope.register_debug_idents(); - - assert_ne!(symbol, shadow_symbol); - assert_eq!(original_region, region1); - - let lookup = scope.lookup(&ident, Region::zero()).unwrap(); - - assert_eq!(symbol, lookup); - } -} diff --git a/compiler/can/src/traverse.rs b/compiler/can/src/traverse.rs deleted file mode 100644 index bd5af6bc14..0000000000 --- a/compiler/can/src/traverse.rs +++ /dev/null @@ -1,530 +0,0 @@ -//! Traversals over the can ast. - -use roc_module::{ident::Lowercase, symbol::Symbol}; -use roc_region::all::{Loc, Region}; -use roc_types::subs::Variable; - -use crate::{ - abilities::AbilitiesStore, - def::{Annotation, Declaration, Def}, - expr::{self, AccessorData, ClosureData, Expr, Field}, - pattern::{DestructType, Pattern, RecordDestruct}, -}; - -macro_rules! visit_list { - ($visitor:ident, $walk:ident, $list:expr) => { - for elem in $list { - $visitor.$walk(elem) - } - }; -} - -pub fn walk_decls(visitor: &mut V, decls: &[Declaration]) { - visit_list!(visitor, visit_decl, decls) -} - -pub fn walk_decl(visitor: &mut V, decl: &Declaration) { - match decl { - Declaration::Declare(def) => { - visitor.visit_def(def); - } - Declaration::DeclareRec(defs, _cycle_mark) => { - visit_list!(visitor, visit_def, defs) - } - Declaration::Builtin(def) => visitor.visit_def(def), - Declaration::InvalidCycle(_cycles) => { - // ignore - } - } -} - -pub fn walk_def(visitor: &mut V, def: &Def) { - let Def { - loc_pattern, - loc_expr, - annotation, - expr_var, - .. - } = def; - - let opt_var = match loc_pattern.value { - Pattern::Identifier(..) | Pattern::AbilityMemberSpecialization { .. } => Some(*expr_var), - _ => loc_pattern.value.opt_var(), - }; - - visitor.visit_pattern(&loc_pattern.value, loc_pattern.region, opt_var); - visitor.visit_expr(&loc_expr.value, loc_expr.region, *expr_var); - if let Some(annot) = &annotation { - visitor.visit_annotation(annot); - } -} - -pub fn walk_expr(visitor: &mut V, expr: &Expr, var: Variable) { - match expr { - Expr::Closure(closure_data) => walk_closure(visitor, closure_data), - Expr::When { - cond_var, - expr_var, - loc_cond, - branches, - region: _, - branches_cond_var: _, - exhaustive: _, - } => { - walk_when(visitor, *cond_var, *expr_var, loc_cond, branches); - } - Expr::Num(..) => { /* terminal */ } - Expr::Int(..) => { /* terminal */ } - Expr::Float(..) => { /* terminal */ } - Expr::Str(..) => { /* terminal */ } - Expr::SingleQuote(..) => { /* terminal */ } - Expr::List { - elem_var, - loc_elems, - } => { - walk_list(visitor, *elem_var, loc_elems); - } - Expr::Var(..) => { /* terminal */ } - Expr::AbilityMember(..) => { /* terminal */ } - Expr::If { - cond_var, - branches, - branch_var, - final_else, - } => walk_if(visitor, *cond_var, branches, *branch_var, final_else), - Expr::LetRec(defs, body, _cycle_mark) => { - defs.iter().for_each(|def| visitor.visit_def(def)); - visitor.visit_expr(&body.value, body.region, var); - } - Expr::LetNonRec(def, body) => { - visitor.visit_def(def); - visitor.visit_expr(&body.value, body.region, var); - } - Expr::Call(f, args, _called_via) => { - let (fn_var, loc_fn, _closure_var, _ret_var) = &**f; - walk_call(visitor, *fn_var, loc_fn, args); - } - Expr::RunLowLevel { - op: _, - args, - ret_var: _, - } => { - args.iter() - .for_each(|(v, e)| visitor.visit_expr(e, Region::zero(), *v)); - } - Expr::ForeignCall { - foreign_symbol: _, - args, - ret_var: _, - } => { - args.iter() - .for_each(|(v, e)| visitor.visit_expr(e, Region::zero(), *v)); - } - Expr::Record { - record_var: _, - fields, - } => { - walk_record_fields(visitor, fields.iter()); - } - Expr::EmptyRecord => { /* terminal */ } - Expr::Access { - field_var, - loc_expr, - field: _, - record_var: _, - ext_var: _, - } => visitor.visit_expr(&loc_expr.value, loc_expr.region, *field_var), - Expr::Accessor(AccessorData { .. }) => { /* terminal */ } - Expr::Update { - record_var: _, - ext_var: _, - symbol: _, - updates, - } => { - walk_record_fields(visitor, updates.iter()); - } - Expr::Tag { - variant_var: _, - ext_var: _, - name: _, - arguments, - } => arguments - .iter() - .for_each(|(v, le)| visitor.visit_expr(&le.value, le.region, *v)), - Expr::ZeroArgumentTag { .. } => { /* terminal */ } - Expr::OpaqueRef { - opaque_var: _, - name: _, - argument, - specialized_def_type: _, - type_arguments: _, - lambda_set_variables: _, - } => { - let (var, le) = &**argument; - visitor.visit_expr(&le.value, le.region, *var); - } - Expr::Expect { - loc_condition, - loc_continuation, - lookups_in_cond: _, - } => { - // TODO: what type does an expect have? bool - visitor.visit_expr(&loc_condition.value, loc_condition.region, Variable::NULL); - visitor.visit_expr( - &loc_continuation.value, - loc_continuation.region, - Variable::NULL, - ); - } - Expr::TypedHole(_) => { /* terminal */ } - Expr::RuntimeError(..) => { /* terminal */ } - } -} - -#[inline(always)] -pub fn walk_closure(visitor: &mut V, clos: &ClosureData) { - let ClosureData { - arguments, - loc_body, - return_type, - .. - } = clos; - - arguments.iter().for_each(|(var, _exhaustive_mark, arg)| { - visitor.visit_pattern(&arg.value, arg.region, Some(*var)) - }); - - visitor.visit_expr(&loc_body.value, loc_body.region, *return_type); -} - -#[inline(always)] -pub fn walk_when( - visitor: &mut V, - cond_var: Variable, - expr_var: Variable, - loc_cond: &Loc, - branches: &[expr::WhenBranch], -) { - visitor.visit_expr(&loc_cond.value, loc_cond.region, cond_var); - - branches - .iter() - .for_each(|branch| walk_when_branch(visitor, branch, expr_var)); -} - -#[inline(always)] -pub fn walk_when_branch( - visitor: &mut V, - branch: &expr::WhenBranch, - expr_var: Variable, -) { - let expr::WhenBranch { - patterns, - value, - guard, - redundant: _, - } = branch; - - patterns - .iter() - .for_each(|pat| visitor.visit_pattern(&pat.value, pat.region, pat.value.opt_var())); - visitor.visit_expr(&value.value, value.region, expr_var); - if let Some(guard) = guard { - visitor.visit_expr(&guard.value, guard.region, Variable::BOOL); - } -} - -#[inline(always)] -pub fn walk_list(visitor: &mut V, elem_var: Variable, loc_elems: &[Loc]) { - loc_elems - .iter() - .for_each(|le| visitor.visit_expr(&le.value, le.region, elem_var)); -} - -#[inline(always)] -pub fn walk_if( - visitor: &mut V, - cond_var: Variable, - branches: &[(Loc, Loc)], - branch_var: Variable, - final_else: &Loc, -) { - branches.iter().for_each(|(cond, body)| { - visitor.visit_expr(&cond.value, cond.region, cond_var); - visitor.visit_expr(&body.value, body.region, branch_var); - }); - visitor.visit_expr(&final_else.value, final_else.region, branch_var); -} - -#[inline(always)] -pub fn walk_call( - visitor: &mut V, - fn_var: Variable, - fn_expr: &Loc, - args: &[(Variable, Loc)], -) { - visitor.visit_expr(&fn_expr.value, fn_expr.region, fn_var); - args.iter() - .for_each(|(v, le)| visitor.visit_expr(&le.value, le.region, *v)); -} - -#[inline(always)] -pub fn walk_record_fields<'a, V: Visitor>( - visitor: &mut V, - fields: impl Iterator, -) { - fields.for_each( - |( - _name, - Field { - var, - loc_expr, - region: _, - }, - )| { visitor.visit_expr(&loc_expr.value, loc_expr.region, *var) }, - ) -} - -pub trait Visitor: Sized { - /// Most default implementations will call [Visitor::should_visit] to decide whether they - /// should descend into a node. Return `false` to skip visiting. - fn should_visit(&mut self, _region: Region) -> bool { - true - } - - fn visit_decls(&mut self, decls: &[Declaration]) { - walk_decls(self, decls); - } - - fn visit_decl(&mut self, decl: &Declaration) { - if self.should_visit(decl.region()) { - walk_decl(self, decl); - } - } - - fn visit_def(&mut self, def: &Def) { - if self.should_visit(def.region()) { - walk_def(self, def); - } - } - - fn visit_annotation(&mut self, _pat: &Annotation) { - // ignore by default - } - - fn visit_expr(&mut self, expr: &Expr, region: Region, var: Variable) { - if self.should_visit(region) { - walk_expr(self, expr, var); - } - } - - fn visit_pattern(&mut self, pattern: &Pattern, region: Region, _opt_var: Option) { - if self.should_visit(region) { - walk_pattern(self, pattern); - } - } - - fn visit_record_destruct(&mut self, destruct: &RecordDestruct, region: Region) { - if self.should_visit(region) { - walk_record_destruct(self, destruct); - } - } -} - -pub fn walk_pattern(visitor: &mut V, pattern: &Pattern) { - use Pattern::*; - - match pattern { - Identifier(..) => { /* terminal */ } - AppliedTag { arguments, .. } => arguments - .iter() - .for_each(|(v, lp)| visitor.visit_pattern(&lp.value, lp.region, Some(*v))), - UnwrappedOpaque { argument, .. } => { - let (v, lp) = &**argument; - visitor.visit_pattern(&lp.value, lp.region, Some(*v)); - } - RecordDestructure { destructs, .. } => destructs - .iter() - .for_each(|d| visitor.visit_record_destruct(&d.value, d.region)), - NumLiteral(..) => { /* terminal */ } - IntLiteral(..) => { /* terminal */ } - FloatLiteral(..) => { /* terminal */ } - StrLiteral(..) => { /* terminal */ } - SingleQuote(..) => { /* terminal */ } - Underscore => { /* terminal */ } - AbilityMemberSpecialization { .. } => { /* terminal */ } - Shadowed(..) => { /* terminal */ } - OpaqueNotInScope(..) => { /* terminal */ } - UnsupportedPattern(..) => { /* terminal */ } - MalformedPattern(..) => { /* terminal */ } - } -} - -pub fn walk_record_destruct(visitor: &mut V, destruct: &RecordDestruct) { - use DestructType::*; - match &destruct.typ { - Required => { /* terminal */ } - Optional(var, expr) => visitor.visit_expr(&expr.value, expr.region, *var), - Guard(var, pat) => visitor.visit_pattern(&pat.value, pat.region, Some(*var)), - } -} - -struct TypeAtVisitor { - region: Region, - typ: Option, -} - -impl Visitor for TypeAtVisitor { - fn should_visit(&mut self, region: Region) -> bool { - region.contains(&self.region) - } - - fn visit_expr(&mut self, expr: &Expr, region: Region, var: Variable) { - if region == self.region { - debug_assert!(self.typ.is_none()); - self.typ = Some(var); - return; - } - - walk_expr(self, expr, var); - } - - fn visit_pattern(&mut self, pat: &Pattern, region: Region, opt_var: Option) { - if region == self.region { - debug_assert!(self.typ.is_none()); - self.typ = opt_var; - return; - } - - walk_pattern(self, pat) - } -} - -/// Attempts to find the type of an expression at `region`, if it exists. -pub fn find_type_at(region: Region, decls: &[Declaration]) -> Option { - let mut visitor = TypeAtVisitor { region, typ: None }; - visitor.visit_decls(decls); - visitor.typ -} - -/// Given an ability Foo has foo : ..., returns (T, foo1) if the symbol at the given region is a -/// symbol foo1 that specializes foo for T. Otherwise if the symbol is foo but the specialization -/// is unknown, (Foo, foo) is returned. Otherwise [None] is returned. -pub fn find_ability_member_and_owning_type_at( - region: Region, - decls: &[Declaration], - abilities_store: &AbilitiesStore, -) -> Option<(Symbol, Symbol)> { - let mut visitor = Finder { - region, - found: None, - abilities_store, - }; - visitor.visit_decls(decls); - return visitor.found; - - struct Finder<'a> { - region: Region, - abilities_store: &'a AbilitiesStore, - found: Option<(Symbol, Symbol)>, - } - - impl Visitor for Finder<'_> { - fn should_visit(&mut self, region: Region) -> bool { - region.contains(&self.region) - } - - fn visit_pattern(&mut self, pattern: &Pattern, region: Region, _opt_var: Option) { - if region == self.region { - if let Pattern::AbilityMemberSpecialization { - ident: spec_symbol, - specializes: _, - } = pattern - { - debug_assert!(self.found.is_none()); - let spec_type = - find_specialization_type_of_symbol(*spec_symbol, self.abilities_store) - .unwrap(); - self.found = Some((spec_type, *spec_symbol)) - } - } - - walk_pattern(self, pattern); - } - - fn visit_expr(&mut self, expr: &Expr, region: Region, var: Variable) { - if region == self.region { - if let &Expr::AbilityMember(member_symbol, specialization_id, _var) = expr { - debug_assert!(self.found.is_none()); - self.found = match self.abilities_store.get_resolved(specialization_id) { - Some(spec_symbol) => { - let spec_type = find_specialization_type_of_symbol( - spec_symbol, - self.abilities_store, - ) - .unwrap(); - Some((spec_type, spec_symbol)) - } - None => { - let parent_ability = self - .abilities_store - .member_def(member_symbol) - .unwrap() - .parent_ability; - Some((parent_ability, member_symbol)) - } - }; - return; - } - } - - walk_expr(self, expr, var); - } - } - - fn find_specialization_type_of_symbol( - symbol: Symbol, - abilities_store: &AbilitiesStore, - ) -> Option { - abilities_store - .iter_specializations() - .find(|(_, ms)| ms.symbol == symbol) - .map(|(spec, _)| spec.1) - } -} - -pub fn symbols_introduced_from_pattern( - pattern: &Loc, -) -> impl Iterator> { - let mut visitor = Collector { - symbols: Vec::new(), - }; - visitor.visit_pattern(&pattern.value, pattern.region, None); - return visitor.symbols.into_iter(); - - struct Collector { - symbols: Vec>, - } - impl Visitor for Collector { - fn visit_pattern(&mut self, pattern: &Pattern, region: Region, _opt_var: Option) { - use Pattern::*; - match pattern { - Identifier(symbol) - | Shadowed(_, _, symbol) - | AbilityMemberSpecialization { ident: symbol, .. } => { - self.symbols.push(Loc::at(region, *symbol)); - } - _ => walk_pattern(self, pattern), - } - } - - fn visit_record_destruct(&mut self, destruct: &RecordDestruct, region: Region) { - // when a record field has a pattern guard, only symbols in the guard are introduced - if let DestructType::Guard(_, subpattern) = &destruct.typ { - self.visit_pattern(&subpattern.value, subpattern.region, None); - } else { - self.symbols.push(Loc::at(region, destruct.symbol)); - } - } - } -} diff --git a/compiler/can/tests/helpers/mod.rs b/compiler/can/tests/helpers/mod.rs deleted file mode 100644 index 04903b266e..0000000000 --- a/compiler/can/tests/helpers/mod.rs +++ /dev/null @@ -1,112 +0,0 @@ -extern crate bumpalo; - -use self::bumpalo::Bump; -use roc_can::env::Env; -use roc_can::expr::Output; -use roc_can::expr::{canonicalize_expr, Expr}; -use roc_can::operator; -use roc_can::scope::Scope; -use roc_collections::all::MutMap; -use roc_module::symbol::{IdentIds, Interns, ModuleId, ModuleIds, Symbol}; -use roc_problem::can::Problem; -use roc_region::all::{Loc, Region}; -use roc_types::subs::{VarStore, Variable}; -use roc_types::types::{AliasVar, Type}; -use std::hash::Hash; - -pub fn test_home() -> ModuleId { - ModuleIds::default().get_or_insert(&"Test".into()) -} - -#[allow(dead_code)] -pub fn can_expr(expr_str: &str) -> CanExprOut { - can_expr_with(&Bump::new(), test_home(), expr_str) -} - -pub struct CanExprOut { - pub loc_expr: Loc, - pub output: Output, - pub problems: Vec, - pub home: ModuleId, - pub interns: Interns, - pub var_store: VarStore, - pub var: Variable, -} - -#[allow(dead_code)] -pub fn can_expr_with(arena: &Bump, home: ModuleId, expr_str: &str) -> CanExprOut { - let loc_expr = roc_parse::test_helpers::parse_loc_with(arena, expr_str).unwrap_or_else(|e| { - panic!( - "can_expr_with() got a parse error when attempting to canonicalize:\n\n{:?} {:?}", - expr_str, e - ) - }); - - let mut var_store = VarStore::default(); - let var = var_store.fresh(); - let module_ids = ModuleIds::default(); - - // Desugar operators (convert them to Apply calls, taking into account - // operator precedence and associativity rules), before doing other canonicalization. - // - // If we did this *during* canonicalization, then each time we - // visited a BinOp node we'd recursively try to apply this to each of its nested - // operators, and then again on *their* nested operators, ultimately applying the - // rules multiple times unnecessarily. - let loc_expr = operator::desugar_expr(arena, &loc_expr); - - let mut scope = Scope::new(home, IdentIds::default(), Default::default()); - scope.add_alias( - Symbol::NUM_INT, - Region::zero(), - vec![Loc::at_zero(AliasVar::unbound( - "a".into(), - Variable::EMPTY_RECORD, - ))], - Type::EmptyRec, - roc_types::types::AliasKind::Structural, - ); - - let dep_idents = IdentIds::exposed_builtins(0); - let mut env = Env::new(home, &dep_idents, &module_ids); - let (loc_expr, output) = canonicalize_expr( - &mut env, - &mut var_store, - &mut scope, - Region::zero(), - &loc_expr.value, - ); - - let mut all_ident_ids = IdentIds::exposed_builtins(1); - all_ident_ids.insert(home, scope.locals.ident_ids); - - let interns = Interns { - module_ids: env.module_ids.clone(), - all_ident_ids, - }; - - CanExprOut { - loc_expr, - output, - problems: env.problems, - home: env.home, - var_store, - interns, - var, - } -} - -#[allow(dead_code)] -pub fn mut_map_from_pairs(pairs: I) -> MutMap -where - I: IntoIterator, - K: Hash + Eq, -{ - let mut answer = MutMap::default(); - - for (key, value) in pairs { - answer.insert(key, value); - } - - answer -} diff --git a/compiler/collections/Cargo.toml b/compiler/collections/Cargo.toml deleted file mode 100644 index 454e0bc8b6..0000000000 --- a/compiler/collections/Cargo.toml +++ /dev/null @@ -1,14 +0,0 @@ -[package] -name = "roc_collections" -version = "0.1.0" -authors = ["The Roc Contributors"] -license = "UPL-1.0" -edition = "2021" - -[dependencies] -im = "15.0.0" -im-rc = "15.0.0" -wyhash = "0.5.0" -bumpalo = { version = "3.8.0", features = ["collections"] } -hashbrown = { version = "0.11.2", features = [ "bumpalo" ] } -bitvec = "1" diff --git a/compiler/collections/src/all.rs b/compiler/collections/src/all.rs deleted file mode 100644 index 391f48e488..0000000000 --- a/compiler/collections/src/all.rs +++ /dev/null @@ -1,222 +0,0 @@ -use bumpalo::collections::String; -use bumpalo::Bump; -use std::hash::{BuildHasherDefault, Hash}; - -pub use wyhash::WyHash; - -#[inline(always)] -pub fn default_hasher() -> BuildHasherDefault { - BuildHasherDefault::default() -} - -pub type BuildHasher = BuildHasherDefault; - -// Versions of HashMap and HashSet from both std and im_rc -// which use the FNV hasher instead of the default SipHash hasher. -// FNV is faster but less secure; that's fine, since this compiler -// doesn't need cryptographically secure hashes, and also is not a -// server concerned about hash flooding attacks! -pub type MutMap = std::collections::HashMap; - -pub type MutSet = std::collections::HashSet; - -pub type ImMap = im_rc::hashmap::HashMap; - -pub type ImSet = im_rc::hashset::HashSet; - -pub type ImEntry<'a, K, V, S> = im_rc::hashmap::Entry<'a, K, V, S>; - -pub type SendMap = im::hashmap::HashMap; - -pub type SendSet = im::hashset::HashSet; - -// pub type BumpMap<'a, K, V> = hashbrown::HashMap>; -// pub type BumpSet<'a, K> = hashbrown::HashSet>; - -pub type BumpMap = hashbrown::HashMap; -pub type BumpSet = hashbrown::HashSet; - -pub trait BumpMapDefault<'a> { - fn new_in(arena: &'a bumpalo::Bump) -> Self; - - fn with_capacity_in(capacity: usize, arena: &'a bumpalo::Bump) -> Self; -} - -impl<'a, K, V> BumpMapDefault<'a> for BumpMap { - fn new_in(_arena: &'a bumpalo::Bump) -> Self { - // hashbrown::HashMap::with_hasher_in(default_hasher(), hashbrown::BumpWrapper(arena)) - hashbrown::HashMap::with_hasher(default_hasher()) - } - - fn with_capacity_in(capacity: usize, _arena: &'a bumpalo::Bump) -> Self { - // hashbrown::HashMap::with_capacity_and_hasher_in( - // capacity, - // default_hasher(), - // hashbrown::BumpWrapper(arena), - // ) - hashbrown::HashMap::with_capacity_and_hasher(capacity, default_hasher()) - } -} - -impl<'a, K> BumpMapDefault<'a> for BumpSet { - fn new_in(_arena: &'a bumpalo::Bump) -> Self { - // hashbrown::HashSet::with_hasher_in(default_hasher(), hashbrown::BumpWrapper(arena)) - hashbrown::HashSet::with_hasher(default_hasher()) - } - - fn with_capacity_in(capacity: usize, _arena: &'a bumpalo::Bump) -> Self { - // hashbrown::HashSet::with_capacity_and_hasher_in( - // capacity, - // default_hasher(), - // hashbrown::BumpWrapper(arena), - // ) - hashbrown::HashSet::with_capacity_and_hasher(capacity, default_hasher()) - } -} - -pub fn arena_join<'a, I>(arena: &'a Bump, strings: &mut I, join_str: &str) -> String<'a> -where - I: Iterator, -{ - let mut buf = String::new_in(arena); - - if let Some(first) = strings.next() { - buf.push_str(first); - - for string in strings { - buf.reserve(join_str.len() + string.len()); - - buf.push_str(join_str); - buf.push_str(string); - } - } - - buf -} - -pub fn insert_all(map: &mut SendMap, elems: I) -where - K: Clone + Eq + Hash, - V: Clone, - I: Iterator, -{ - for (k, v) in elems { - map.insert(k, v); - } -} - -/// Like im's relative_complement, but for MutMap and with references for arguments. -pub fn relative_complement(map: &MutMap, other: &MutMap) -> MutMap -where - K: Clone + Eq + Hash, - V: Clone, -{ - let mut answer = MutMap::default(); - - for (key, value) in map { - // Drop any key that exists in the other map, - // by declining to insert it into the answer. - if !other.contains_key(key) { - answer.insert(key.clone(), value.clone()); - } - } - - answer -} - -/// Like intersection_with, except for MutMap and specialized to return -/// a tuple. Also, only clones the values that will be actually returned, -/// rather than cloning everything. -pub fn get_shared(map1: &MutMap, map2: &MutMap) -> MutMap -where - K: Clone + Eq + Hash, - V: Clone, -{ - let mut answer = MutMap::default(); - - for (key, right_value) in map2 { - match std::collections::HashMap::get(map1, key) { - None => (), - Some(left_value) => { - answer.insert(key.clone(), (left_value.clone(), right_value.clone())); - } - } - } - - answer -} - -/// Like im's union, but for MutMap. -pub fn union(mut map: MutMap, other: &MutMap) -> MutMap -where - K: Clone + Eq + Hash, - V: Clone, -{ - for (key, value) in other.iter() { - // If the key exists in both maps, keep the value in the owned one. - if !map.contains_key(key) { - map.insert(key.clone(), value.clone()); - } - } - - map -} - -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub struct HumanIndex(usize); - -impl HumanIndex { - pub const FIRST: Self = HumanIndex(0); - - pub fn zero_based(i: usize) -> Self { - HumanIndex(i) - } - - pub fn to_zero_based(self) -> usize { - self.0 - } - - pub fn one_based(i: usize) -> Self { - HumanIndex(i - 1) - } - - pub fn ordinal(self) -> std::string::String { - int_to_ordinal(self.0 + 1) - } -} - -fn int_to_ordinal(number: usize) -> std::string::String { - // NOTE: one-based - let remainder10 = number % 10; - let remainder100 = number % 100; - - let ending = match remainder100 { - 11..=13 => "th", - _ => match remainder10 { - 1 => "st", - 2 => "nd", - 3 => "rd", - _ => "th", - }, - }; - - format!("{}{}", number, ending) -} - -#[macro_export] -macro_rules! mut_map { - (@single $($x:tt)*) => (()); - (@count $($rest:expr),*) => (<[()]>::len(&[$(mut_map!(@single $rest)),*])); - - ($($key:expr => $value:expr,)+) => { mut_map!($($key => $value),+) }; - ($($key:expr => $value:expr),*) => { - { - let _cap = mut_map!(@count $($key),*); - let mut _map = ::std::collections::HashMap::with_capacity_and_hasher(_cap, $crate::all::default_hasher()); - $( - let _ = _map.insert($key, $value); - )* - _map - } - }; -} diff --git a/compiler/collections/src/lib.rs b/compiler/collections/src/lib.rs deleted file mode 100644 index 9bce46c2f2..0000000000 --- a/compiler/collections/src/lib.rs +++ /dev/null @@ -1,16 +0,0 @@ -#![warn(clippy::dbg_macro)] -// See github.com/rtfeldman/roc/issues/800 for discussion of the large_enum_variant check. -#![allow(clippy::large_enum_variant)] - -pub mod all; -mod reference_matrix; -mod small_string_interner; -pub mod soa; -mod vec_map; -mod vec_set; - -pub use all::{default_hasher, BumpMap, ImEntry, ImMap, ImSet, MutMap, MutSet, SendMap}; -pub use reference_matrix::{ReferenceMatrix, Sccs, TopologicalSort}; -pub use small_string_interner::SmallStringInterner; -pub use vec_map::VecMap; -pub use vec_set::VecSet; diff --git a/compiler/collections/src/vec_set.rs b/compiler/collections/src/vec_set.rs deleted file mode 100644 index 7dcca27c30..0000000000 --- a/compiler/collections/src/vec_set.rs +++ /dev/null @@ -1,122 +0,0 @@ -use std::iter::FromIterator; - -#[derive(Clone, Debug, PartialEq)] -pub struct VecSet { - elements: Vec, -} - -impl Default for VecSet { - fn default() -> Self { - Self { - elements: Vec::new(), - } - } -} - -impl VecSet { - pub fn with_capacity(capacity: usize) -> Self { - Self { - elements: Vec::with_capacity(capacity), - } - } - - pub fn len(&self) -> usize { - self.elements.len() - } - - pub fn is_empty(&self) -> bool { - self.elements.is_empty() - } - - pub fn swap_remove(&mut self, index: usize) -> T { - self.elements.swap_remove(index) - } - - pub fn insert(&mut self, value: T) -> bool { - if self.elements.contains(&value) { - true - } else { - self.elements.push(value); - - false - } - } - - /// Returns true iff any of the given elements previoously existed in the set. - pub fn insert_all>(&mut self, values: I) -> bool { - let mut any_existed = false; - - for value in values { - any_existed = any_existed || self.insert(value); - } - - any_existed - } - - pub fn contains(&self, value: &T) -> bool { - self.elements.contains(value) - } - - /// Performs a swap_remove if the element was present in the set, - /// then returns whether the value was present in the set. - pub fn remove(&mut self, value: &T) -> bool { - match self.elements.iter().position(|x| x == value) { - None => false, - Some(index) => { - self.elements.swap_remove(index); - - true - } - } - } - - pub fn iter(&self) -> impl Iterator { - self.elements.iter() - } - - pub fn iter_mut(&mut self) -> impl Iterator { - self.elements.iter_mut() - } -} - -impl Extend for VecSet { - fn extend>(&mut self, iter: T) { - let it = iter.into_iter(); - let hint = it.size_hint(); - - match hint { - (0, Some(0)) => { - // done, do nothing - } - (1, Some(1)) | (2, Some(2)) => { - for value in it { - self.insert(value); - } - } - _ => { - self.elements.extend(it); - - self.elements.sort(); - self.elements.dedup(); - } - } - } -} - -impl FromIterator for VecSet { - fn from_iter>(iter: T) -> Self { - let mut set = VecSet::default(); - set.extend(iter); - set - } -} - -impl IntoIterator for VecSet { - type Item = T; - - type IntoIter = std::vec::IntoIter; - - fn into_iter(self) -> Self::IntoIter { - self.elements.into_iter() - } -} diff --git a/compiler/constrain/Cargo.toml b/compiler/constrain/Cargo.toml deleted file mode 100644 index 94ebd2ce4b..0000000000 --- a/compiler/constrain/Cargo.toml +++ /dev/null @@ -1,17 +0,0 @@ -[package] -name = "roc_constrain" -version = "0.1.0" -authors = ["The Roc Contributors"] -license = "UPL-1.0" -edition = "2021" - -[dependencies] -roc_collections = { path = "../collections" } -roc_error_macros = { path = "../../error_macros" } -roc_region = { path = "../region" } -roc_module = { path = "../module" } -roc_parse = { path = "../parse" } -roc_types = { path = "../types" } -roc_can = { path = "../can" } -roc_builtins = { path = "../builtins" } -arrayvec = "0.7.2" diff --git a/compiler/constrain/src/builtins.rs b/compiler/constrain/src/builtins.rs deleted file mode 100644 index 17128e9de1..0000000000 --- a/compiler/constrain/src/builtins.rs +++ /dev/null @@ -1,335 +0,0 @@ -use arrayvec::ArrayVec; -use roc_can::constraint::{Constraint, Constraints}; -use roc_can::expected::Expected::{self, *}; -use roc_can::num::{FloatBound, FloatWidth, IntBound, IntWidth, NumBound, SignDemand}; -use roc_module::symbol::Symbol; -use roc_region::all::Region; -use roc_types::num::NumericRange; -use roc_types::subs::Variable; -use roc_types::types::Type::{self, *}; -use roc_types::types::{AliasKind, Category}; -use roc_types::types::{OptAbleType, Reason}; - -#[must_use] -#[inline(always)] -pub fn add_numeric_bound_constr( - constraints: &mut Constraints, - num_constraints: &mut impl Extend, - num_type: Type, - bound: impl TypedNumericBound, - region: Region, - category: Category, -) -> Type { - let range = bound.numeric_bound(); - let total_num_type = num_type; - - use roc_types::num::{float_width_to_variable, int_width_to_variable}; - - match range { - NumericBound::None => { - // no additional constraints - total_num_type - } - NumericBound::FloatExact(width) => { - let actual_type = Variable(float_width_to_variable(width)); - let expected = Expected::ForReason(Reason::NumericLiteralSuffix, actual_type, region); - let because_suffix = - constraints.equal_types(total_num_type.clone(), expected, category, region); - - num_constraints.extend([because_suffix]); - - total_num_type - } - NumericBound::IntExact(width) => { - let actual_type = Variable(int_width_to_variable(width)); - let expected = Expected::ForReason(Reason::NumericLiteralSuffix, actual_type, region); - let because_suffix = - constraints.equal_types(total_num_type.clone(), expected, category, region); - - num_constraints.extend([because_suffix]); - - total_num_type - } - NumericBound::Range(range) => RangedNumber(Box::new(total_num_type), range), - } -} - -#[inline(always)] -pub fn int_literal( - constraints: &mut Constraints, - num_var: Variable, - precision_var: Variable, - expected: Expected, - region: Region, - bound: IntBound, -) -> Constraint { - let reason = Reason::IntLiteral; - - // Always add the bound first; this improves the resolved type quality in case it's an alias like "U8". - let mut constrs = ArrayVec::<_, 3>::new(); - let num_type = add_numeric_bound_constr( - constraints, - &mut constrs, - Variable(num_var), - bound, - region, - Category::Num, - ); - - constrs.extend([ - constraints.equal_types( - num_type.clone(), - ForReason(reason, num_int(Type::Variable(precision_var)), region), - Category::Int, - region, - ), - constraints.equal_types(num_type, expected, Category::Int, region), - ]); - - // TODO the precision_var is not part of the exists here; for float it is. Which is correct? - let and_constraint = constraints.and_constraint(constrs); - constraints.exists([num_var], and_constraint) -} - -#[inline(always)] -pub fn float_literal( - constraints: &mut Constraints, - num_var: Variable, - precision_var: Variable, - expected: Expected, - region: Region, - bound: FloatBound, -) -> Constraint { - let reason = Reason::FloatLiteral; - - let mut constrs = ArrayVec::<_, 3>::new(); - let num_type = add_numeric_bound_constr( - constraints, - &mut constrs, - Variable(num_var), - bound, - region, - Category::Float, - ); - - constrs.extend([ - constraints.equal_types( - num_type.clone(), - ForReason(reason, num_float(Type::Variable(precision_var)), region), - Category::Float, - region, - ), - constraints.equal_types(num_type, expected, Category::Float, region), - ]); - - let and_constraint = constraints.and_constraint(constrs); - constraints.exists([num_var, precision_var], and_constraint) -} - -#[inline(always)] -pub fn num_literal( - constraints: &mut Constraints, - num_var: Variable, - expected: Expected, - region: Region, - bound: NumBound, -) -> Constraint { - let open_number_type = crate::builtins::num_num(Type::Variable(num_var)); - - let mut constrs = ArrayVec::<_, 2>::new(); - let num_type = add_numeric_bound_constr( - constraints, - &mut constrs, - open_number_type, - bound, - region, - Category::Num, - ); - - constrs.extend([constraints.equal_types(num_type, expected, Category::Num, region)]); - - let and_constraint = constraints.and_constraint(constrs); - constraints.exists([num_var], and_constraint) -} - -#[inline(always)] -pub fn builtin_type(symbol: Symbol, args: Vec) -> Type { - Type::Apply(symbol, args, Region::zero()) -} - -#[inline(always)] -pub fn empty_list_type(var: Variable) -> Type { - list_type(Type::Variable(var)) -} - -#[inline(always)] -pub fn list_type(typ: Type) -> Type { - builtin_type(Symbol::LIST_LIST, vec![typ]) -} - -#[inline(always)] -pub fn str_type() -> Type { - builtin_type(Symbol::STR_STR, Vec::new()) -} - -#[inline(always)] -fn builtin_alias( - symbol: Symbol, - type_arguments: Vec, - actual: Box, - kind: AliasKind, -) -> Type { - Type::Alias { - symbol, - type_arguments, - actual, - lambda_set_variables: vec![], - kind, - } -} - -#[inline(always)] -pub fn num_float(range: Type) -> Type { - builtin_alias( - Symbol::NUM_FRAC, - vec![OptAbleType::unbound(range.clone())], - Box::new(num_num(num_floatingpoint(range))), - AliasKind::Structural, - ) -} - -#[inline(always)] -pub fn num_floatingpoint(range: Type) -> Type { - builtin_alias( - Symbol::NUM_FLOATINGPOINT, - vec![OptAbleType::unbound(range.clone())], - Box::new(range), - AliasKind::Opaque, - ) -} - -#[inline(always)] -pub fn num_u32() -> Type { - builtin_alias( - Symbol::NUM_U32, - vec![], - Box::new(num_int(num_unsigned32())), - AliasKind::Structural, - ) -} - -#[inline(always)] -fn num_unsigned32() -> Type { - builtin_alias( - Symbol::NUM_UNSIGNED32, - vec![], - Box::new(Type::EmptyTagUnion), - AliasKind::Opaque, - ) -} - -#[inline(always)] -pub fn num_binary64() -> Type { - builtin_alias( - Symbol::NUM_BINARY64, - vec![], - Box::new(Type::EmptyTagUnion), - AliasKind::Opaque, - ) -} - -#[inline(always)] -pub fn num_int(range: Type) -> Type { - builtin_alias( - Symbol::NUM_INT, - vec![OptAbleType::unbound(range.clone())], - Box::new(num_num(num_integer(range))), - AliasKind::Structural, - ) -} - -#[inline(always)] -pub fn num_signed64() -> Type { - builtin_alias( - Symbol::NUM_SIGNED64, - vec![], - Box::new(Type::EmptyTagUnion), - AliasKind::Opaque, - ) -} - -#[inline(always)] -pub fn num_integer(range: Type) -> Type { - builtin_alias( - Symbol::NUM_INTEGER, - vec![OptAbleType::unbound(range.clone())], - Box::new(range), - AliasKind::Opaque, - ) -} - -#[inline(always)] -pub fn num_num(typ: Type) -> Type { - builtin_alias( - Symbol::NUM_NUM, - vec![OptAbleType::unbound(typ.clone())], - Box::new(typ), - AliasKind::Opaque, - ) -} - -pub trait TypedNumericBound { - fn numeric_bound(&self) -> NumericBound; -} - -impl TypedNumericBound for IntBound { - fn numeric_bound(&self) -> NumericBound { - match self { - IntBound::None => NumericBound::None, - IntBound::Exact(w) => NumericBound::IntExact(*w), - IntBound::AtLeast { - sign: SignDemand::NoDemand, - width, - } => NumericBound::Range(NumericRange::IntAtLeastEitherSign(*width)), - IntBound::AtLeast { - sign: SignDemand::Signed, - width, - } => NumericBound::Range(NumericRange::IntAtLeastSigned(*width)), - } - } -} - -impl TypedNumericBound for FloatBound { - fn numeric_bound(&self) -> NumericBound { - match self { - FloatBound::None => NumericBound::None, - FloatBound::Exact(w) => NumericBound::FloatExact(*w), - } - } -} - -impl TypedNumericBound for NumBound { - fn numeric_bound(&self) -> NumericBound { - match self { - NumBound::None => NumericBound::None, - &NumBound::AtLeastIntOrFloat { - sign: SignDemand::NoDemand, - width, - } => NumericBound::Range(NumericRange::NumAtLeastEitherSign(width)), - &NumBound::AtLeastIntOrFloat { - sign: SignDemand::Signed, - width, - } => NumericBound::Range(NumericRange::NumAtLeastSigned(width)), - } - } -} - -/// A bound placed on a number because of its literal value. -/// e.g. `-5` cannot be unsigned, and 300 does not fit in a U8 -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum NumericBound { - None, - FloatExact(FloatWidth), - IntExact(IntWidth), - Range(NumericRange), -} diff --git a/compiler/constrain/src/expr.rs b/compiler/constrain/src/expr.rs deleted file mode 100644 index eaf144b9eb..0000000000 --- a/compiler/constrain/src/expr.rs +++ /dev/null @@ -1,2205 +0,0 @@ -use crate::builtins::{ - empty_list_type, float_literal, int_literal, list_type, num_literal, num_u32, str_type, -}; -use crate::pattern::{constrain_pattern, PatternState}; -use roc_can::annotation::IntroducedVariables; -use roc_can::constraint::{Constraint, Constraints, OpportunisticResolve}; -use roc_can::def::{Declaration, Def}; -use roc_can::exhaustive::{sketch_pattern_to_rows, sketch_when_branches, ExhaustiveContext}; -use roc_can::expected::Expected::{self, *}; -use roc_can::expected::PExpected; -use roc_can::expr::Expr::{self, *}; -use roc_can::expr::{AccessorData, AnnotatedMark, ClosureData, Field, WhenBranch}; -use roc_can::pattern::Pattern; -use roc_can::traverse::symbols_introduced_from_pattern; -use roc_collections::all::{HumanIndex, MutMap, SendMap}; -use roc_module::ident::Lowercase; -use roc_module::symbol::{ModuleId, Symbol}; -use roc_region::all::{Loc, Region}; -use roc_types::subs::{IllegalCycleMark, Variable}; -use roc_types::types::Type::{self, *}; -use roc_types::types::{ - AliasKind, AnnotationSource, Category, OptAbleType, PReason, Reason, RecordField, TypeExtension, -}; - -/// This is for constraining Defs -#[derive(Default, Debug)] -pub struct Info { - pub vars: Vec, - pub constraints: Vec, - pub def_types: SendMap>, -} - -impl Info { - pub fn with_capacity(capacity: usize) -> Self { - Info { - vars: Vec::with_capacity(capacity), - constraints: Vec::with_capacity(capacity), - def_types: SendMap::default(), - } - } -} - -pub struct Env { - /// for example `a` in the annotation `identity : a -> a`, we add it to this - /// map so that expressions within that annotation can share these vars. - pub rigids: MutMap, - pub resolutions_to_make: Vec, - pub home: ModuleId, -} - -fn constrain_untyped_args( - constraints: &mut Constraints, - env: &mut Env, - arguments: &[(Variable, AnnotatedMark, Loc)], - closure_type: Type, - return_type: Type, -) -> (Vec, PatternState, Type) { - let mut vars = Vec::with_capacity(arguments.len()); - let mut pattern_types = Vec::with_capacity(arguments.len()); - - let mut pattern_state = PatternState::default(); - - for (pattern_var, annotated_mark, loc_pattern) in arguments { - // Untyped args don't need exhaustiveness checking because they are the source of truth! - let _ = annotated_mark; - - let pattern_type = Type::Variable(*pattern_var); - let pattern_expected = PExpected::NoExpectation(pattern_type.clone()); - - pattern_types.push(pattern_type); - - constrain_pattern( - constraints, - env, - &loc_pattern.value, - loc_pattern.region, - pattern_expected, - &mut pattern_state, - ); - - vars.push(*pattern_var); - } - - let function_type = - Type::Function(pattern_types, Box::new(closure_type), Box::new(return_type)); - - (vars, pattern_state, function_type) -} - -pub fn constrain_expr( - constraints: &mut Constraints, - env: &mut Env, - region: Region, - expr: &Expr, - expected: Expected, -) -> Constraint { - match expr { - &Int(var, precision, _, _, bound) => { - int_literal(constraints, var, precision, expected, region, bound) - } - &Num(var, _, _, bound) => num_literal(constraints, var, expected, region, bound), - &Float(var, precision, _, _, bound) => { - float_literal(constraints, var, precision, expected, region, bound) - } - EmptyRecord => constrain_empty_record(constraints, region, expected), - Expr::Record { record_var, fields } => { - if fields.is_empty() { - constrain_empty_record(constraints, region, expected) - } else { - let mut field_types = SendMap::default(); - let mut field_vars = Vec::with_capacity(fields.len()); - - // Constraints need capacity for each field - // + 1 for the record itself + 1 for record var - let mut rec_constraints = Vec::with_capacity(2 + fields.len()); - - for (label, field) in fields { - let field_var = field.var; - let loc_field_expr = &field.loc_expr; - let (field_type, field_con) = - constrain_field(constraints, env, field_var, &*loc_field_expr); - - field_vars.push(field_var); - field_types.insert(label.clone(), RecordField::Required(field_type)); - - rec_constraints.push(field_con); - } - - let record_type = Type::Record(field_types, TypeExtension::Closed); - - let record_con = constraints.equal_types_with_storage( - record_type, - expected, - Category::Record, - region, - *record_var, - ); - - rec_constraints.push(record_con); - field_vars.push(*record_var); - - let and_constraint = constraints.and_constraint(rec_constraints); - constraints.exists(field_vars, and_constraint) - } - } - Update { - record_var, - ext_var, - symbol, - updates, - } => { - let mut fields: SendMap> = SendMap::default(); - let mut vars = Vec::with_capacity(updates.len() + 2); - let mut cons = Vec::with_capacity(updates.len() + 1); - for (field_name, Field { var, loc_expr, .. }) in updates.clone() { - let (var, tipe, con) = constrain_field_update( - constraints, - env, - var, - loc_expr.region, - field_name.clone(), - &loc_expr, - ); - fields.insert(field_name, RecordField::Required(tipe)); - vars.push(var); - cons.push(con); - } - - let fields_type = - Type::Record(fields, TypeExtension::from_type(Type::Variable(*ext_var))); - let record_type = Type::Variable(*record_var); - - // NOTE from elm compiler: fields_type is separate so that Error propagates better - let fields_con = constraints.equal_types_var( - *record_var, - NoExpectation(fields_type), - Category::Record, - region, - ); - let record_con = - constraints.equal_types_var(*record_var, expected, Category::Record, region); - - vars.push(*record_var); - vars.push(*ext_var); - - let con = constraints.lookup( - *symbol, - ForReason( - Reason::RecordUpdateKeys( - *symbol, - updates - .iter() - .map(|(key, field)| (key.clone(), field.region)) - .collect(), - ), - record_type, - region, - ), - region, - ); - - // ensure constraints are solved in this order, gives better errors - cons.insert(0, fields_con); - cons.insert(1, con); - cons.insert(2, record_con); - - let and_constraint = constraints.and_constraint(cons); - constraints.exists(vars, and_constraint) - } - Str(_) => constraints.equal_types(str_type(), expected, Category::Str, region), - SingleQuote(_) => constraints.equal_types(num_u32(), expected, Category::Character, region), - List { - elem_var, - loc_elems, - } => { - if loc_elems.is_empty() { - let eq = constraints.equal_types( - empty_list_type(*elem_var), - expected, - Category::List, - region, - ); - constraints.exists(vec![*elem_var], eq) - } else { - let list_elem_type = Type::Variable(*elem_var); - let mut list_constraints = Vec::with_capacity(1 + loc_elems.len()); - - for (index, loc_elem) in loc_elems.iter().enumerate() { - let elem_expected = ForReason( - Reason::ElemInList { - index: HumanIndex::zero_based(index), - }, - list_elem_type.clone(), - loc_elem.region, - ); - let constraint = constrain_expr( - constraints, - env, - loc_elem.region, - &loc_elem.value, - elem_expected, - ); - - list_constraints.push(constraint); - } - - list_constraints.push(constraints.equal_types( - list_type(list_elem_type), - expected, - Category::List, - region, - )); - - let and_constraint = constraints.and_constraint(list_constraints); - constraints.exists([*elem_var], and_constraint) - } - } - Call(boxed, loc_args, called_via) => { - let (fn_var, loc_fn, closure_var, ret_var) = &**boxed; - // The expression that evaluates to the function being called, e.g. `foo` in - // (foo) bar baz - let opt_symbol = if let Var(symbol) | AbilityMember(symbol, _, _) = loc_fn.value { - Some(symbol) - } else { - None - }; - - let fn_type = Variable(*fn_var); - let fn_region = loc_fn.region; - let fn_expected = NoExpectation(fn_type); - - let fn_reason = Reason::FnCall { - name: opt_symbol, - arity: loc_args.len() as u8, - }; - - let fn_con = - constrain_expr(constraints, env, loc_fn.region, &loc_fn.value, fn_expected); - - // The function's return type - let ret_type = Variable(*ret_var); - - // type of values captured in the closure - let closure_type = Variable(*closure_var); - - // This will be used in the occurs check - let mut vars = Vec::with_capacity(2 + loc_args.len()); - - vars.push(*fn_var); - vars.push(*ret_var); - vars.push(*closure_var); - - let mut arg_types = Vec::with_capacity(loc_args.len()); - let mut arg_cons = Vec::with_capacity(loc_args.len()); - - for (index, (arg_var, loc_arg)) in loc_args.iter().enumerate() { - let region = loc_arg.region; - let arg_type = Variable(*arg_var); - - let reason = Reason::FnArg { - name: opt_symbol, - arg_index: HumanIndex::zero_based(index), - }; - let expected_arg = ForReason(reason, arg_type.clone(), region); - let arg_con = constrain_expr( - constraints, - env, - loc_arg.region, - &loc_arg.value, - expected_arg, - ); - - vars.push(*arg_var); - arg_types.push(arg_type); - arg_cons.push(arg_con); - } - - let expected_fn_type = ForReason( - fn_reason, - Function(arg_types, Box::new(closure_type), Box::new(ret_type)), - region, - ); - - let category = Category::CallResult(opt_symbol, *called_via); - - let and_cons = [ - fn_con, - constraints.equal_types_var(*fn_var, expected_fn_type, category.clone(), fn_region), - constraints.and_constraint(arg_cons), - constraints.equal_types_var(*ret_var, expected, category, region), - ]; - - let and_constraint = constraints.and_constraint(and_cons); - constraints.exists(vars, and_constraint) - } - Var(symbol) => { - // make lookup constraint to lookup this symbol's type in the environment - constraints.lookup(*symbol, expected, region) - } - &AbilityMember(symbol, specialization_id, specialization_var) => { - // make lookup constraint to lookup this symbol's type in the environment - let store_expected = constraints.equal_types_var( - specialization_var, - expected, - Category::Storage(file!(), line!()), - region, - ); - let lookup_constr = constraints.lookup( - symbol, - Expected::NoExpectation(Type::Variable(specialization_var)), - region, - ); - - // Make sure we attempt to resolve the specialization. - env.resolutions_to_make.push(OpportunisticResolve { - specialization_variable: specialization_var, - specialization_expectation: constraints.push_expected_type( - Expected::NoExpectation(Type::Variable(specialization_var)), - ), - member: symbol, - specialization_id, - }); - - constraints.and_constraint([store_expected, lookup_constr]) - } - Closure(ClosureData { - function_type: fn_var, - closure_type: closure_var, - return_type: ret_var, - arguments, - loc_body: boxed, - captured_symbols, - name, - .. - }) => { - // NOTE defs are treated somewhere else! - let loc_body_expr = &**boxed; - - let ret_var = *ret_var; - let closure_var = *closure_var; - - let closure_type = Type::Variable(closure_var); - let return_type = Type::Variable(ret_var); - let (mut vars, pattern_state, function_type) = constrain_untyped_args( - constraints, - env, - arguments, - closure_type, - return_type.clone(), - ); - - vars.push(ret_var); - vars.push(closure_var); - vars.push(*fn_var); - - let body_type = NoExpectation(return_type); - let ret_constraint = constrain_expr( - constraints, - env, - loc_body_expr.region, - &loc_body_expr.value, - body_type, - ); - - // make sure the captured symbols are sorted! - debug_assert_eq!(captured_symbols.clone(), { - let mut copy = captured_symbols.clone(); - copy.sort(); - copy - }); - - let closure_constraint = constrain_closure_size( - constraints, - *name, - region, - captured_symbols, - closure_var, - &mut vars, - ); - - let pattern_state_constraints = constraints.and_constraint(pattern_state.constraints); - let cons = [ - constraints.let_constraint( - [], - pattern_state.vars, - pattern_state.headers, - pattern_state_constraints, - ret_constraint, - ), - constraints.equal_types_with_storage( - function_type, - expected, - Category::Lambda, - region, - *fn_var, - ), - closure_constraint, - ]; - - constraints.exists_many(vars, cons) - } - - Expect { - loc_condition, - loc_continuation, - lookups_in_cond, - } => { - let expect_bool = |region| { - let bool_type = Type::Variable(Variable::BOOL); - Expected::ForReason(Reason::ExpectCondition, bool_type, region) - }; - - let cond_con = constrain_expr( - constraints, - env, - loc_condition.region, - &loc_condition.value, - expect_bool(loc_condition.region), - ); - - let continuation_con = constrain_expr( - constraints, - env, - loc_continuation.region, - &loc_continuation.value, - expected, - ); - - // + 2 for cond_con and continuation_con - let mut all_constraints = Vec::with_capacity(lookups_in_cond.len() + 2); - - all_constraints.push(cond_con); - all_constraints.push(continuation_con); - - let mut vars = Vec::with_capacity(lookups_in_cond.len()); - - for (symbol, var) in lookups_in_cond.iter() { - vars.push(*var); - - all_constraints.push(constraints.lookup( - *symbol, - NoExpectation(Type::Variable(*var)), - Region::zero(), - )); - } - - constraints.exists_many(vars, all_constraints) - } - - If { - cond_var, - branch_var, - branches, - final_else, - } => { - let expect_bool = |region| { - let bool_type = Type::Variable(Variable::BOOL); - Expected::ForReason(Reason::IfCondition, bool_type, region) - }; - let mut branch_cons = Vec::with_capacity(2 * branches.len() + 3); - - // TODO why does this cond var exist? is it for error messages? - let first_cond_region = branches[0].0.region; - let cond_var_is_bool_con = constraints.equal_types_var( - *cond_var, - expect_bool(first_cond_region), - Category::If, - first_cond_region, - ); - - branch_cons.push(cond_var_is_bool_con); - - match expected { - FromAnnotation(name, arity, ann_source, tipe) => { - let num_branches = branches.len() + 1; - for (index, (loc_cond, loc_body)) in branches.iter().enumerate() { - let cond_con = constrain_expr( - constraints, - env, - loc_cond.region, - &loc_cond.value, - expect_bool(loc_cond.region), - ); - - let then_con = constrain_expr( - constraints, - env, - loc_body.region, - &loc_body.value, - FromAnnotation( - name.clone(), - arity, - AnnotationSource::TypedIfBranch { - index: HumanIndex::zero_based(index), - num_branches, - region: ann_source.region(), - }, - tipe.clone(), - ), - ); - - branch_cons.push(cond_con); - branch_cons.push(then_con); - } - - let else_con = constrain_expr( - constraints, - env, - final_else.region, - &final_else.value, - FromAnnotation( - name, - arity, - AnnotationSource::TypedIfBranch { - index: HumanIndex::zero_based(branches.len()), - num_branches, - region: ann_source.region(), - }, - tipe.clone(), - ), - ); - - let ast_con = constraints.equal_types_var( - *branch_var, - NoExpectation(tipe), - Category::Storage(std::file!(), std::line!()), - region, - ); - - branch_cons.push(ast_con); - branch_cons.push(else_con); - - constraints.exists_many([*cond_var, *branch_var], branch_cons) - } - _ => { - for (index, (loc_cond, loc_body)) in branches.iter().enumerate() { - let cond_con = constrain_expr( - constraints, - env, - loc_cond.region, - &loc_cond.value, - expect_bool(loc_cond.region), - ); - - let then_con = constrain_expr( - constraints, - env, - loc_body.region, - &loc_body.value, - ForReason( - Reason::IfBranch { - index: HumanIndex::zero_based(index), - total_branches: branches.len(), - }, - Type::Variable(*branch_var), - loc_body.region, - ), - ); - - branch_cons.push(cond_con); - branch_cons.push(then_con); - } - let else_con = constrain_expr( - constraints, - env, - final_else.region, - &final_else.value, - ForReason( - Reason::IfBranch { - index: HumanIndex::zero_based(branches.len()), - total_branches: branches.len() + 1, - }, - Type::Variable(*branch_var), - final_else.region, - ), - ); - - branch_cons.push(constraints.equal_types_var( - *branch_var, - expected, - Category::Storage(std::file!(), std::line!()), - region, - )); - branch_cons.push(else_con); - - constraints.exists_many([*cond_var, *branch_var], branch_cons) - } - } - } - When { - cond_var: real_cond_var, - expr_var, - loc_cond, - branches, - branches_cond_var, - exhaustive, - .. - } => { - let branches_cond_var = *branches_cond_var; - let branches_cond_type = Variable(branches_cond_var); - - let body_var = *expr_var; - let body_type = Variable(body_var); - - let branches_region = { - debug_assert!(!branches.is_empty()); - Region::span_across( - &loc_cond.region, - // &branches.first().unwrap().region(), - &branches.last().unwrap().pattern_region(), - ) - }; - - let branch_expr_reason = - |expected: &Expected, index, branch_region| match expected { - FromAnnotation(name, arity, ann_source, _typ) => { - // NOTE deviation from elm. - // - // in elm, `_typ` is used, but because we have this `expr_var` too - // and need to constrain it, this is what works and gives better error messages - FromAnnotation( - name.clone(), - *arity, - AnnotationSource::TypedWhenBranch { - index, - region: ann_source.region(), - }, - body_type.clone(), - ) - } - - _ => ForReason( - Reason::WhenBranch { index }, - body_type.clone(), - branch_region, - ), - }; - - // Our goal is to constrain and introduce variables in all pattern when branch patterns before - // looking at their bodies. - // - // pat1 -> body1 - // *^^^ +~~~~ - // pat2 -> body2 - // *^^^ +~~~~ - // - // * solve first - // + solve second - // - // For a single pattern/body pair, we must introduce variables and symbols defined in the - // pattern before solving the body, since those definitions are effectively let-bound. - // - // But also, we'd like to solve all branch pattern constraints in one swoop before looking at - // the bodies, because the patterns may have presence constraints that expect to be built up - // together. - // - // For this reason, we distinguish the two - and introduce variables in the branch patterns - // as part of the pattern constraint, solving all of those at once, and then solving the body - // constraints. - let mut pattern_vars = Vec::with_capacity(branches.len()); - let mut pattern_headers = SendMap::default(); - let mut pattern_cons = Vec::with_capacity(branches.len() + 2); - let mut branch_cons = Vec::with_capacity(branches.len()); - - for (index, when_branch) in branches.iter().enumerate() { - let expected_pattern = |sub_pattern, sub_region| { - PExpected::ForReason( - PReason::WhenMatch { - index: HumanIndex::zero_based(index), - sub_pattern, - }, - branches_cond_type.clone(), - sub_region, - ) - }; - - let (new_pattern_vars, new_pattern_headers, pattern_con, branch_con) = - constrain_when_branch_help( - constraints, - env, - region, - when_branch, - expected_pattern, - branch_expr_reason( - &expected, - HumanIndex::zero_based(index), - when_branch.value.region, - ), - ); - - pattern_vars.extend(new_pattern_vars); - debug_assert!( - pattern_headers - .clone() - .intersection(new_pattern_headers.clone()) - .is_empty(), - "Two patterns introduce the same symbols - that's a bug!\n{:?}", - pattern_headers.clone().intersection(new_pattern_headers) - ); - pattern_headers.extend(new_pattern_headers); - pattern_cons.push(pattern_con); - - branch_cons.push(branch_con); - } - - // Deviation: elm adds another layer of And nesting - // - // Record the original conditional expression's constraint. - // Each branch's pattern must have the same type - // as the condition expression did. - // - // The return type of each branch must equal the return type of - // the entire when-expression. - - // After solving the condition variable with what's expected from the branch patterns, - // check it against the condition expression. - // - // First, solve the condition type. - let real_cond_var = *real_cond_var; - let real_cond_type = Type::Variable(real_cond_var); - let cond_constraint = constrain_expr( - constraints, - env, - loc_cond.region, - &loc_cond.value, - Expected::NoExpectation(real_cond_type), - ); - pattern_cons.push(cond_constraint); - - // Now check the condition against the type expected by the branches. - let sketched_rows = sketch_when_branches(real_cond_var, branches_region, branches); - let cond_matches_branches_constraint = constraints.exhaustive( - real_cond_var, - loc_cond.region, - Ok(( - loc_cond.value.category(), - Expected::ForReason(Reason::WhenBranches, branches_cond_type, branches_region), - )), - sketched_rows, - ExhaustiveContext::BadCase, - *exhaustive, - ); - pattern_cons.push(cond_matches_branches_constraint); - - // Solve all the pattern constraints together, introducing variables in the pattern as - // need be before solving the bodies. - let pattern_constraints = constraints.and_constraint(pattern_cons); - let body_constraints = constraints.and_constraint(branch_cons); - let when_body_con = constraints.let_constraint( - [], - pattern_vars, - pattern_headers, - pattern_constraints, - body_constraints, - ); - - let result_con = - constraints.equal_types_var(body_var, expected, Category::When, region); - - let total_cons = [when_body_con, result_con]; - let branch_constraints = constraints.and_constraint(total_cons); - - constraints.exists( - [ - exhaustive.variable_for_introduction(), - branches_cond_var, - real_cond_var, - *expr_var, - ], - branch_constraints, - ) - } - Access { - record_var, - ext_var, - field_var, - loc_expr, - field, - } => { - let ext_var = *ext_var; - let ext_type = Type::Variable(ext_var); - let field_var = *field_var; - let field_type = Type::Variable(field_var); - - let mut rec_field_types = SendMap::default(); - - let label = field.clone(); - rec_field_types.insert(label, RecordField::Demanded(field_type)); - - let record_type = Type::Record(rec_field_types, TypeExtension::from_type(ext_type)); - let record_expected = Expected::NoExpectation(record_type); - - let category = Category::Access(field.clone()); - - let record_con = constraints.equal_types_var( - *record_var, - record_expected.clone(), - category.clone(), - region, - ); - - let constraint = - constrain_expr(constraints, env, region, &loc_expr.value, record_expected); - - let eq = constraints.equal_types_var(field_var, expected, category, region); - constraints.exists_many( - [*record_var, field_var, ext_var], - [constraint, eq, record_con], - ) - } - Accessor(AccessorData { - name: closure_name, - function_var, - field, - record_var, - closure_var, - ext_var, - field_var, - }) => { - let ext_var = *ext_var; - let ext_type = Variable(ext_var); - let field_var = *field_var; - let field_type = Variable(field_var); - - let mut field_types = SendMap::default(); - let label = field.clone(); - field_types.insert(label, RecordField::Demanded(field_type.clone())); - let record_type = Type::Record(field_types, TypeExtension::from_type(ext_type)); - - let category = Category::Accessor(field.clone()); - - let record_expected = Expected::NoExpectation(record_type.clone()); - let record_con = - constraints.equal_types_var(*record_var, record_expected, category.clone(), region); - - let lambda_set = Type::ClosureTag { - name: *closure_name, - captures: vec![], - }; - - let closure_type = Type::Variable(*closure_var); - - let function_type = Type::Function( - vec![record_type], - Box::new(closure_type), - Box::new(field_type), - ); - - let cons = [ - constraints.equal_types_var( - *closure_var, - NoExpectation(lambda_set), - category.clone(), - region, - ), - constraints.equal_types(function_type.clone(), expected, category.clone(), region), - constraints.equal_types( - function_type, - NoExpectation(Variable(*function_var)), - category, - region, - ), - record_con, - ]; - - constraints.exists_many( - [*record_var, *function_var, *closure_var, field_var, ext_var], - cons, - ) - } - LetRec(defs, loc_ret, cycle_mark) => { - let body_con = constrain_expr( - constraints, - env, - loc_ret.region, - &loc_ret.value, - expected.clone(), - ); - - constrain_recursive_defs(constraints, env, defs, body_con, *cycle_mark) - } - LetNonRec(def, loc_ret) => { - let mut stack = Vec::with_capacity(1); - - let mut loc_ret = loc_ret; - - stack.push(def); - - while let LetNonRec(def, new_loc_ret) = &loc_ret.value { - stack.push(def); - loc_ret = new_loc_ret; - } - - let mut body_con = constrain_expr( - constraints, - env, - loc_ret.region, - &loc_ret.value, - expected.clone(), - ); - - while let Some(def) = stack.pop() { - body_con = constrain_def(constraints, env, def, body_con) - } - - body_con - } - Tag { - variant_var, - ext_var, - name, - arguments, - } => { - // +2 because we push all the arguments, plus variant_var and ext_var - let num_vars = arguments.len() + 2; - let mut vars = Vec::with_capacity(num_vars); - let mut types = Vec::with_capacity(arguments.len()); - let mut arg_cons = Vec::with_capacity(arguments.len()); - - for (var, loc_expr) in arguments { - let arg_con = constrain_expr( - constraints, - env, - loc_expr.region, - &loc_expr.value, - Expected::NoExpectation(Type::Variable(*var)), - ); - - arg_cons.push(arg_con); - vars.push(*var); - types.push(Type::Variable(*var)); - } - - let union_con = constraints.equal_types_with_storage( - Type::TagUnion( - vec![(name.clone(), types)], - TypeExtension::from_type(Type::Variable(*ext_var)), - ), - expected.clone(), - Category::TagApply { - tag_name: name.clone(), - args_count: arguments.len(), - }, - region, - *variant_var, - ); - - vars.push(*variant_var); - vars.push(*ext_var); - arg_cons.push(union_con); - - constraints.exists_many(vars, arg_cons) - } - ZeroArgumentTag { - variant_var, - ext_var, - name, - closure_name, - } => { - let union_con = constraints.equal_types_with_storage( - Type::FunctionOrTagUnion( - name.clone(), - *closure_name, - TypeExtension::from_type(Type::Variable(*ext_var)), - ), - expected.clone(), - Category::TagApply { - tag_name: name.clone(), - args_count: 0, - }, - region, - *variant_var, - ); - - constraints.exists_many([*variant_var, *ext_var], [union_con]) - } - OpaqueRef { - opaque_var, - name, - argument, - specialized_def_type, - type_arguments, - lambda_set_variables, - } => { - let (arg_var, arg_loc_expr) = &**argument; - let arg_type = Type::Variable(*arg_var); - - let opaque_type = Type::Alias { - symbol: *name, - type_arguments: type_arguments - .iter() - .map(|v| OptAbleType { - typ: Type::Variable(v.var), - opt_ability: v.opt_ability, - }) - .collect(), - lambda_set_variables: lambda_set_variables.clone(), - actual: Box::new(arg_type.clone()), - kind: AliasKind::Opaque, - }; - - // Constrain the argument - let arg_con = constrain_expr( - constraints, - env, - arg_loc_expr.region, - &arg_loc_expr.value, - Expected::NoExpectation(arg_type.clone()), - ); - - // Link the entire wrapped opaque type (with the now-constrained argument) to the - // expected type - let opaque_con = constraints.equal_types_with_storage( - opaque_type, - expected, - Category::OpaqueWrap(*name), - region, - *opaque_var, - ); - - // Link the entire wrapped opaque type (with the now-constrained argument) to the type - // variables of the opaque type - // TODO: better expectation here - let link_type_variables_con = constraints.equal_types( - arg_type, - Expected::NoExpectation((**specialized_def_type).clone()), - Category::OpaqueArg, - arg_loc_expr.region, - ); - - let mut vars = vec![*arg_var, *opaque_var]; - // Also add the fresh variables we created for the type argument and lambda sets - vars.extend(type_arguments.iter().map(|v| v.var)); - vars.extend(lambda_set_variables.iter().map(|v| { - v.0.expect_variable("all lambda sets should be fresh variables here") - })); - - constraints.exists_many(vars, [arg_con, opaque_con, link_type_variables_con]) - } - - RunLowLevel { args, ret_var, op } => { - // This is a modified version of what we do for function calls. - - // This will be used in the occurs check - let mut vars = Vec::with_capacity(1 + args.len()); - - vars.push(*ret_var); - - let mut arg_types = Vec::with_capacity(args.len()); - let mut arg_cons = Vec::with_capacity(args.len()); - - let mut add_arg = |index, arg_type: Type, arg| { - let reason = Reason::LowLevelOpArg { - op: *op, - arg_index: HumanIndex::zero_based(index), - }; - let expected_arg = ForReason(reason, arg_type.clone(), Region::zero()); - let arg_con = constrain_expr(constraints, env, Region::zero(), arg, expected_arg); - - arg_types.push(arg_type); - arg_cons.push(arg_con); - }; - - for (index, (arg_var, arg)) in args.iter().enumerate() { - vars.push(*arg_var); - - add_arg(index, Variable(*arg_var), arg); - } - - let category = Category::LowLevelOpResult(*op); - - // Deviation: elm uses an additional And here - let eq = constraints.equal_types_var(*ret_var, expected, category, region); - arg_cons.push(eq); - constraints.exists_many(vars, arg_cons) - } - ForeignCall { - args, - ret_var, - foreign_symbol, - } => { - // This is a modified version of what we do for function calls. - - // This will be used in the occurs check - let mut vars = Vec::with_capacity(1 + args.len()); - - vars.push(*ret_var); - - let mut arg_types = Vec::with_capacity(args.len()); - let mut arg_cons = Vec::with_capacity(args.len()); - - let mut add_arg = |index, arg_type: Type, arg| { - let reason = Reason::ForeignCallArg { - foreign_symbol: foreign_symbol.clone(), - arg_index: HumanIndex::zero_based(index), - }; - let expected_arg = ForReason(reason, arg_type.clone(), Region::zero()); - let arg_con = constrain_expr(constraints, env, Region::zero(), arg, expected_arg); - - arg_types.push(arg_type); - arg_cons.push(arg_con); - }; - - for (index, (arg_var, arg)) in args.iter().enumerate() { - vars.push(*arg_var); - - add_arg(index, Variable(*arg_var), arg); - } - - let category = Category::ForeignCall; - - // Deviation: elm uses an additional And here - let eq = constraints.equal_types_var(*ret_var, expected, category, region); - arg_cons.push(eq); - constraints.exists_many(vars, arg_cons) - } - TypedHole(var) => { - // store the expected type for this position - constraints.equal_types_var( - *var, - expected, - Category::Storage(std::file!(), std::line!()), - region, - ) - } - RuntimeError(_) => { - // Runtime Errors have no constraints because they're going to crash. - Constraint::True - } - } -} - -/// Constrain a when branch, returning (variables in pattern, symbols introduced in pattern, pattern constraint, body constraint). -/// We want to constraint all pattern constraints in a "when" before body constraints. -#[inline(always)] -fn constrain_when_branch_help( - constraints: &mut Constraints, - env: &mut Env, - region: Region, - when_branch: &WhenBranch, - pattern_expected: impl Fn(HumanIndex, Region) -> PExpected, - expr_expected: Expected, -) -> ( - Vec, - SendMap>, - Constraint, - Constraint, -) { - let ret_constraint = constrain_expr( - constraints, - env, - region, - &when_branch.value.value, - expr_expected, - ); - - let mut state = PatternState { - headers: SendMap::default(), - vars: Vec::with_capacity(2), - constraints: Vec::with_capacity(2), - delayed_is_open_constraints: Vec::new(), - }; - - // TODO investigate for error messages, is it better to unify all branches with a variable, - // then unify that variable with the expectation? - for (i, loc_pattern) in when_branch.patterns.iter().enumerate() { - let pattern_expected = pattern_expected(HumanIndex::zero_based(i), loc_pattern.region); - - constrain_pattern( - constraints, - env, - &loc_pattern.value, - loc_pattern.region, - pattern_expected, - &mut state, - ); - } - - let (pattern_constraints, body_constraints) = if let Some(loc_guard) = &when_branch.guard { - let guard_constraint = constrain_expr( - constraints, - env, - region, - &loc_guard.value, - Expected::ForReason( - Reason::WhenGuard, - Type::Variable(Variable::BOOL), - loc_guard.region, - ), - ); - - // must introduce the headers from the pattern before constraining the guard - state - .constraints - .append(&mut state.delayed_is_open_constraints); - let state_constraints = constraints.and_constraint(state.constraints); - let inner = constraints.let_constraint([], [], [], guard_constraint, ret_constraint); - - (state_constraints, inner) - } else { - state - .constraints - .append(&mut state.delayed_is_open_constraints); - let state_constraints = constraints.and_constraint(state.constraints); - (state_constraints, ret_constraint) - }; - - ( - state.vars, - state.headers, - pattern_constraints, - body_constraints, - ) -} - -fn constrain_field( - constraints: &mut Constraints, - env: &mut Env, - field_var: Variable, - loc_expr: &Loc, -) -> (Type, Constraint) { - let field_type = Variable(field_var); - let field_expected = NoExpectation(field_type.clone()); - let constraint = constrain_expr( - constraints, - env, - loc_expr.region, - &loc_expr.value, - field_expected, - ); - - (field_type, constraint) -} - -#[inline(always)] -fn constrain_empty_record( - constraints: &mut Constraints, - region: Region, - expected: Expected, -) -> Constraint { - constraints.equal_types(Type::EmptyRec, expected, Category::Record, region) -} - -/// Constrain top-level module declarations -#[inline(always)] -pub fn constrain_decls( - constraints: &mut Constraints, - home: ModuleId, - decls: &[Declaration], -) -> Constraint { - let mut constraint = Constraint::SaveTheEnvironment; - - let mut env = Env { - home, - rigids: MutMap::default(), - resolutions_to_make: vec![], - }; - - for decl in decls.iter().rev() { - // Clear the rigids from the previous iteration. - // rigids are not shared between top-level definitions - env.rigids.clear(); - - match decl { - Declaration::Declare(def) | Declaration::Builtin(def) => { - constraint = constrain_def(constraints, &mut env, def, constraint); - } - Declaration::DeclareRec(defs, cycle_mark) => { - constraint = - constrain_recursive_defs(constraints, &mut env, defs, constraint, *cycle_mark); - } - Declaration::InvalidCycle(_) => { - // invalid cycles give a canonicalization error. we skip them here. - continue; - } - } - - debug_assert!( - env.resolutions_to_make.is_empty(), - "Resolutions not attached after def!" - ); - } - - // this assert make the "root" of the constraint wasn't dropped - debug_assert!(constraints.contains_save_the_environment(&constraint)); - - constraint -} - -pub fn constrain_def_pattern( - constraints: &mut Constraints, - env: &mut Env, - loc_pattern: &Loc, - expr_type: Type, -) -> PatternState { - let pattern_expected = PExpected::NoExpectation(expr_type); - - let mut state = PatternState { - headers: SendMap::default(), - vars: Vec::with_capacity(1), - constraints: Vec::with_capacity(1), - delayed_is_open_constraints: vec![], - }; - - constrain_pattern( - constraints, - env, - &loc_pattern.value, - loc_pattern.region, - pattern_expected, - &mut state, - ); - - state -} - -/// Generate constraints for a definition with a type signature -fn constrain_typed_def( - constraints: &mut Constraints, - env: &mut Env, - def: &Def, - body_con: Constraint, - annotation: &roc_can::def::Annotation, -) -> Constraint { - let expr_var = def.expr_var; - let expr_type = Type::Variable(expr_var); - - let mut def_pattern_state = - constrain_def_pattern(constraints, env, &def.loc_pattern, expr_type.clone()); - - def_pattern_state.vars.push(expr_var); - - let arity = annotation.signature.arity(); - let rigids = &env.rigids; - let mut ftv = rigids.clone(); - - let InstantiateRigids { - signature, - new_rigid_variables, - new_infer_variables, - } = instantiate_rigids( - &annotation.signature, - &annotation.introduced_variables, - &def.loc_pattern, - &mut ftv, - &mut def_pattern_state.headers, - ); - - let env = &mut Env { - home: env.home, - resolutions_to_make: vec![], - rigids: ftv, - }; - - let annotation_expected = FromAnnotation( - def.loc_pattern.clone(), - arity, - AnnotationSource::TypedBody { - region: annotation.region, - }, - signature.clone(), - ); - - def_pattern_state.constraints.push(constraints.equal_types( - expr_type, - annotation_expected, - Category::Storage(std::file!(), std::line!()), - Region::span_across(&annotation.region, &def.loc_expr.region), - )); - - // when a def is annotated, and it's body is a closure, treat this - // as a named function (in elm terms) for error messages. - // - // This means we get errors like "the first argument of `f` is weird" - // instead of the more generic "something is wrong with the body of `f`" - match (&def.loc_expr.value, &signature) { - ( - Closure(ClosureData { - function_type: fn_var, - closure_type: closure_var, - return_type: ret_var, - captured_symbols, - arguments, - loc_body, - name, - .. - }), - Type::Function(arg_types, signature_closure_type, ret_type), - ) => { - // NOTE if we ever have problems with the closure, the ignored `_closure_type` - // is probably a good place to start the investigation! - - let region = def.loc_expr.region; - - let loc_body_expr = &**loc_body; - let mut argument_pattern_state = PatternState { - headers: SendMap::default(), - vars: Vec::with_capacity(arguments.len()), - constraints: Vec::with_capacity(1), - delayed_is_open_constraints: vec![], - }; - let mut vars = Vec::with_capacity(argument_pattern_state.vars.capacity() + 1); - let ret_var = *ret_var; - let closure_var = *closure_var; - let ret_type = *ret_type.clone(); - - vars.push(ret_var); - vars.push(closure_var); - - constrain_typed_function_arguments( - constraints, - env, - def, - &mut def_pattern_state, - &mut argument_pattern_state, - arguments, - arg_types, - ); - - let closure_constraint = constrain_closure_size( - constraints, - *name, - region, - captured_symbols, - closure_var, - &mut vars, - ); - - let body_type = FromAnnotation( - def.loc_pattern.clone(), - arguments.len(), - AnnotationSource::TypedBody { - region: annotation.region, - }, - ret_type.clone(), - ); - - let ret_constraint = constrain_expr( - constraints, - env, - loc_body_expr.region, - &loc_body_expr.value, - body_type, - ); - let ret_constraint = attach_resolution_constraints(constraints, env, ret_constraint); - - vars.push(*fn_var); - let defs_constraint = constraints.and_constraint(argument_pattern_state.constraints); - - let signature_closure_type = *signature_closure_type.clone(); - let signature_index = constraints.push_type(signature); - let cons = [ - constraints.let_constraint( - [], - argument_pattern_state.vars, - argument_pattern_state.headers, - defs_constraint, - ret_constraint, - ), - constraints.equal_types_var( - closure_var, - Expected::FromAnnotation( - def.loc_pattern.clone(), - arity, - AnnotationSource::TypedBody { - region: annotation.region, - }, - signature_closure_type, - ), - Category::ClosureSize, - region, - ), - constraints.store_index(signature_index, *fn_var, std::file!(), std::line!()), - constraints.store_index(signature_index, expr_var, std::file!(), std::line!()), - constraints.store(ret_type, ret_var, std::file!(), std::line!()), - closure_constraint, - ]; - - let expr_con = constraints.exists_many(vars, cons); - - constrain_def_make_constraint( - constraints, - new_rigid_variables.into_iter(), - new_infer_variables.into_iter(), - expr_con, - body_con, - def_pattern_state, - ) - } - - _ => { - let annotation_expected = FromAnnotation( - def.loc_pattern.clone(), - arity, - AnnotationSource::TypedBody { - region: annotation.region, - }, - signature.clone(), - ); - - let ret_constraint = constrain_expr( - constraints, - env, - def.loc_expr.region, - &def.loc_expr.value, - annotation_expected, - ); - let ret_constraint = attach_resolution_constraints(constraints, env, ret_constraint); - - let cons = [ - ret_constraint, - // Store type into AST vars. We use Store so errors aren't reported twice - constraints.store(signature, expr_var, std::file!(), std::line!()), - ]; - let expr_con = constraints.and_constraint(cons); - - constrain_def_make_constraint( - constraints, - new_rigid_variables.into_iter(), - new_infer_variables.into_iter(), - expr_con, - body_con, - def_pattern_state, - ) - } - } -} - -fn constrain_typed_function_arguments( - constraints: &mut Constraints, - env: &mut Env, - def: &Def, - def_pattern_state: &mut PatternState, - argument_pattern_state: &mut PatternState, - arguments: &[(Variable, AnnotatedMark, Loc)], - arg_types: &[Type], -) { - // ensure type matches the one in the annotation - let opt_label = if let Pattern::Identifier(label) = def.loc_pattern.value { - Some(label) - } else { - None - }; - - let it = arguments.iter().zip(arg_types.iter()).enumerate(); - for (index, ((pattern_var, annotated_mark, loc_pattern), ann)) in it { - if loc_pattern.value.surely_exhaustive() { - // OPT: we don't need to perform any type-level exhaustiveness checking. - // Check instead only that the pattern unifies with the annotation type. - let pattern_expected = PExpected::ForReason( - PReason::TypedArg { - index: HumanIndex::zero_based(index), - opt_name: opt_label, - }, - ann.clone(), - loc_pattern.region, - ); - - constrain_pattern( - constraints, - env, - &loc_pattern.value, - loc_pattern.region, - pattern_expected, - argument_pattern_state, - ); - - { - // NOTE: because we perform an equality with part of the signature - // this constraint must be to the def_pattern_state's constraints - def_pattern_state.vars.push(*pattern_var); - - let pattern_con = constraints.equal_types_var( - *pattern_var, - Expected::NoExpectation(ann.clone()), - Category::Storage(std::file!(), std::line!()), - loc_pattern.region, - ); - - def_pattern_state.constraints.push(pattern_con); - } - } else { - // We need to check the types, and run exhaustiveness checking. - let &AnnotatedMark { - annotation_var, - exhaustive, - } = annotated_mark; - - def_pattern_state.vars.push(*pattern_var); - def_pattern_state.vars.push(annotation_var); - - { - // First, solve the type that the pattern is expecting to match in this - // position. - let pattern_expected = PExpected::NoExpectation(Type::Variable(*pattern_var)); - constrain_pattern( - constraints, - env, - &loc_pattern.value, - loc_pattern.region, - pattern_expected, - argument_pattern_state, - ); - } - - { - // Store the actual type in a variable. - argument_pattern_state - .constraints - .push(constraints.equal_types_var( - annotation_var, - Expected::NoExpectation(ann.clone()), - Category::Storage(file!(), line!()), - Region::zero(), - )); - } - - { - // let pattern_expected = PExpected::ForReason( - // PReason::TypedArg { - // index: HumanIndex::zero_based(index), - // opt_name: opt_label, - // }, - // ann.clone(), - // loc_pattern.region, - // ); - - // Exhaustiveness-check the type in the pattern against what the - // annotation wants. - let sketched_rows = - sketch_pattern_to_rows(annotation_var, loc_pattern.region, &loc_pattern.value); - let category = loc_pattern.value.category(); - let expected = PExpected::ForReason( - PReason::TypedArg { - index: HumanIndex::zero_based(index), - opt_name: opt_label, - }, - Type::Variable(*pattern_var), - loc_pattern.region, - ); - let exhaustive_constraint = constraints.exhaustive( - annotation_var, - loc_pattern.region, - Err((category, expected)), - sketched_rows, - ExhaustiveContext::BadArg, - exhaustive, - ); - argument_pattern_state - .constraints - .push(exhaustive_constraint) - } - } - } -} - -#[inline(always)] -fn attach_resolution_constraints( - constraints: &mut Constraints, - env: &mut Env, - constraint: Constraint, -) -> Constraint { - let resolution_constrs = - constraints.and_constraint(env.resolutions_to_make.drain(..).map(Constraint::Resolve)); - constraints.and_constraint([constraint, resolution_constrs]) -} - -fn constrain_def( - constraints: &mut Constraints, - env: &mut Env, - def: &Def, - body_con: Constraint, -) -> Constraint { - match &def.annotation { - Some(annotation) => constrain_typed_def(constraints, env, def, body_con, annotation), - None => { - let expr_var = def.expr_var; - let expr_type = Type::Variable(expr_var); - - let mut def_pattern_state = - constrain_def_pattern(constraints, env, &def.loc_pattern, expr_type.clone()); - - def_pattern_state.vars.push(expr_var); - // no annotation, so no extra work with rigids - - let expr_con = constrain_expr( - constraints, - env, - def.loc_expr.region, - &def.loc_expr.value, - NoExpectation(expr_type), - ); - let expr_con = attach_resolution_constraints(constraints, env, expr_con); - - constrain_def_make_constraint( - constraints, - std::iter::empty(), - std::iter::empty(), - expr_con, - body_con, - def_pattern_state, - ) - } - } -} - -/// Create a let-constraint for a non-recursive def. -/// Recursive defs should always use `constrain_recursive_defs`. -pub fn constrain_def_make_constraint( - constraints: &mut Constraints, - annotation_rigid_variables: impl Iterator, - annotation_infer_variables: impl Iterator, - def_expr_con: Constraint, - after_def_con: Constraint, - def_pattern_state: PatternState, -) -> Constraint { - let all_flex_variables = (def_pattern_state.vars.into_iter()).chain(annotation_infer_variables); - - let pattern_constraints = constraints.and_constraint(def_pattern_state.constraints); - let def_pattern_and_body_con = constraints.and_constraint([pattern_constraints, def_expr_con]); - - constraints.let_constraint( - annotation_rigid_variables, - all_flex_variables, - def_pattern_state.headers, - def_pattern_and_body_con, - after_def_con, - ) -} - -fn constrain_closure_size( - constraints: &mut Constraints, - name: Symbol, - region: Region, - captured_symbols: &[(Symbol, Variable)], - closure_var: Variable, - variables: &mut Vec, -) -> Constraint { - debug_assert!(variables.iter().any(|s| *s == closure_var)); - - let mut captured_types = Vec::with_capacity(captured_symbols.len()); - let mut captured_symbols_constraints = Vec::with_capacity(captured_symbols.len()); - - for (symbol, var) in captured_symbols { - // make sure the variable is registered - variables.push(*var); - - // this symbol is captured, so it must be part of the closure type - captured_types.push(Type::Variable(*var)); - - // make the variable equal to the looked-up type of symbol - captured_symbols_constraints.push(constraints.lookup( - *symbol, - Expected::NoExpectation(Type::Variable(*var)), - Region::zero(), - )); - } - - // pick a more efficient representation if we don't actually capture anything - let closure_type = Type::ClosureTag { - name, - captures: captured_types, - }; - - let finalizer = constraints.equal_types_var( - closure_var, - NoExpectation(closure_type), - Category::ClosureSize, - region, - ); - - captured_symbols_constraints.push(finalizer); - - constraints.and_constraint(captured_symbols_constraints) -} - -pub struct InstantiateRigids { - pub signature: Type, - pub new_rigid_variables: Vec, - pub new_infer_variables: Vec, -} - -fn instantiate_rigids( - annotation: &Type, - introduced_vars: &IntroducedVariables, - loc_pattern: &Loc, - ftv: &mut MutMap, // rigids defined before the current annotation - headers: &mut SendMap>, -) -> InstantiateRigids { - let mut annotation = annotation.clone(); - let mut new_rigid_variables: Vec = Vec::new(); - - let mut rigid_substitution: MutMap = MutMap::default(); - for named in introduced_vars.iter_named() { - use std::collections::hash_map::Entry::*; - - match ftv.entry(named.name().clone()) { - Occupied(occupied) => { - let existing_rigid = occupied.get(); - rigid_substitution.insert(named.variable(), *existing_rigid); - } - Vacant(vacant) => { - // It's possible to use this rigid in nested defs - vacant.insert(named.variable()); - new_rigid_variables.push(named.variable()); - } - } - } - - // wildcards are always freshly introduced in this annotation - new_rigid_variables.extend(introduced_vars.wildcards.iter().map(|v| v.value)); - - // lambda set vars are always freshly introduced in this annotation - new_rigid_variables.extend(introduced_vars.lambda_sets.iter().copied()); - - let new_infer_variables: Vec = - introduced_vars.inferred.iter().map(|v| v.value).collect(); - - // Instantiate rigid variables - if !rigid_substitution.is_empty() { - annotation.substitute_variables(&rigid_substitution); - } - - // TODO investigate when we can skip this. It seems to only be required for correctness - // for recursive functions. For non-recursive functions the final type is correct, but - // alias information is sometimes lost - // - // Skipping all of this cloning here would be neat! - let loc_annotation_ref = Loc::at(loc_pattern.region, &annotation); - if let Pattern::Identifier(symbol) = loc_pattern.value { - headers.insert(symbol, Loc::at(loc_pattern.region, annotation.clone())); - } else if let Some(new_headers) = - crate::pattern::headers_from_annotation(&loc_pattern.value, &loc_annotation_ref) - { - headers.extend(new_headers) - } - - InstantiateRigids { - signature: annotation, - new_rigid_variables, - new_infer_variables, - } -} - -fn constrain_recursive_defs( - constraints: &mut Constraints, - env: &mut Env, - defs: &[Def], - body_con: Constraint, - cycle_mark: IllegalCycleMark, -) -> Constraint { - rec_defs_help( - constraints, - env, - defs, - body_con, - Info::with_capacity(defs.len()), - Info::with_capacity(defs.len()), - cycle_mark, - ) -} - -pub fn rec_defs_help( - constraints: &mut Constraints, - env: &mut Env, - defs: &[Def], - body_con: Constraint, - mut rigid_info: Info, - mut flex_info: Info, - cycle_mark: IllegalCycleMark, -) -> Constraint { - for def in defs { - let expr_var = def.expr_var; - let expr_type = Type::Variable(expr_var); - - let mut def_pattern_state = - constrain_def_pattern(constraints, env, &def.loc_pattern, expr_type.clone()); - - def_pattern_state.vars.push(expr_var); - - match &def.annotation { - None => { - let expr_con = constrain_expr( - constraints, - env, - def.loc_expr.region, - &def.loc_expr.value, - NoExpectation(expr_type), - ); - let expr_con = attach_resolution_constraints(constraints, env, expr_con); - - let def_con = expr_con; - - flex_info.vars = def_pattern_state.vars; - flex_info.constraints.push(def_con); - flex_info.def_types.extend(def_pattern_state.headers); - } - - Some(annotation) => { - let arity = annotation.signature.arity(); - let mut ftv = env.rigids.clone(); - - let InstantiateRigids { - signature, - new_rigid_variables, - new_infer_variables, - } = instantiate_rigids( - &annotation.signature, - &annotation.introduced_variables, - &def.loc_pattern, - &mut ftv, - &mut def_pattern_state.headers, - ); - - flex_info.vars.extend(new_infer_variables); - - let annotation_expected = FromAnnotation( - def.loc_pattern.clone(), - arity, - AnnotationSource::TypedBody { - region: annotation.region, - }, - signature.clone(), - ); - - // when a def is annotated, and it's body is a closure, treat this - // as a named function (in elm terms) for error messages. - // - // This means we get errors like "the first argument of `f` is weird" - // instead of the more generic "something is wrong with the body of `f`" - match (&def.loc_expr.value, &signature) { - ( - Closure(ClosureData { - function_type: fn_var, - closure_type: closure_var, - return_type: ret_var, - captured_symbols, - arguments, - loc_body, - name, - .. - }), - Type::Function(arg_types, _closure_type, ret_type), - ) => { - // NOTE if we ever have trouble with closure type unification, the ignored - // `_closure_type` here is a good place to start investigating - - let expected = annotation_expected; - let region = def.loc_expr.region; - - let loc_body_expr = &**loc_body; - let mut state = PatternState { - headers: SendMap::default(), - vars: Vec::with_capacity(arguments.len()), - constraints: Vec::with_capacity(1), - delayed_is_open_constraints: vec![], - }; - let mut vars = Vec::with_capacity(state.vars.capacity() + 1); - let ret_var = *ret_var; - let closure_var = *closure_var; - let ret_type = *ret_type.clone(); - - vars.push(ret_var); - vars.push(closure_var); - - constrain_typed_function_arguments( - constraints, - env, - def, - &mut def_pattern_state, - &mut state, - arguments, - arg_types, - ); - let pattern_types = arguments.iter().map(|a| Type::Variable(a.0)).collect(); - - let closure_constraint = constrain_closure_size( - constraints, - *name, - region, - captured_symbols, - closure_var, - &mut vars, - ); - - let fn_type = Type::Function( - pattern_types, - Box::new(Type::Variable(closure_var)), - Box::new(ret_type.clone()), - ); - let body_type = NoExpectation(ret_type.clone()); - let expr_con = constrain_expr( - constraints, - env, - loc_body_expr.region, - &loc_body_expr.value, - body_type, - ); - let expr_con = attach_resolution_constraints(constraints, env, expr_con); - - vars.push(*fn_var); - - let signature_index = constraints.push_type(signature); - let state_constraints = constraints.and_constraint(state.constraints); - let cons = [ - constraints.let_constraint( - [], - state.vars, - state.headers, - state_constraints, - expr_con, - ), - constraints.equal_types( - fn_type.clone(), - expected.clone(), - Category::Lambda, - region, - ), - // "fn_var is equal to the closure's type" - fn_var is used in code gen - // Store type into AST vars. We use Store so errors aren't reported twice - constraints.store_index( - signature_index, - *fn_var, - std::file!(), - std::line!(), - ), - constraints.store_index( - signature_index, - expr_var, - std::file!(), - std::line!(), - ), - constraints.store(ret_type, ret_var, std::file!(), std::line!()), - closure_constraint, - ]; - - let and_constraint = constraints.and_constraint(cons); - let def_con = constraints.exists(vars, and_constraint); - - rigid_info.vars.extend(&new_rigid_variables); - - rigid_info.constraints.push(constraints.let_constraint( - new_rigid_variables, - def_pattern_state.vars, - [], // no headers introduced (at this level) - def_con, - Constraint::True, - )); - rigid_info.def_types.extend(def_pattern_state.headers); - } - _ => { - let expected = annotation_expected; - - let ret_constraint = constrain_expr( - constraints, - env, - def.loc_expr.region, - &def.loc_expr.value, - expected, - ); - let ret_constraint = - attach_resolution_constraints(constraints, env, ret_constraint); - - let cons = [ - ret_constraint, - // Store type into AST vars. We use Store so errors aren't reported twice - constraints.store(signature, expr_var, std::file!(), std::line!()), - ]; - let def_con = constraints.and_constraint(cons); - - rigid_info.vars.extend(&new_rigid_variables); - - rigid_info.constraints.push(constraints.let_constraint( - new_rigid_variables, - def_pattern_state.vars, - [], // no headers introduced (at this level) - def_con, - Constraint::True, - )); - rigid_info.def_types.extend(def_pattern_state.headers); - } - } - } - } - } - - // Strategy for recursive defs: - // 1. Let-generalize the type annotations we know; these are the source of truth we'll solve - // everything else with. If there are circular type errors here, they will be caught during - // the let-generalization. - // 2. Introduce all symbols of the untyped defs, but don't generalize them yet. Now, solve - // the untyped defs' bodies. This way, when checking something like - // f = \x -> f [x] - // we introduce `f: b -> c`, then constrain the call `f [x]`, - // forcing `b -> c ~ List b -> c` and correctly picking up a recursion error. - // Had we generalized `b -> c`, the call `f [x]` would have been generalized, and this - // error would not be found. - // 3. Now properly let-generalize the untyped body defs, since we now know their types and - // that they don't have circular type errors. - // 4. Solve the bodies of the typed body defs, and check that they agree the types of the type - // annotation. - // 5. Solve the rest of the program that happens after this recursive def block. - - // 2. Solve untyped defs without generalization of their symbols. - let untyped_body_constraints = constraints.and_constraint(flex_info.constraints); - let untyped_def_symbols_constr = constraints.let_constraint( - [], - [], - flex_info.def_types.clone(), - Constraint::True, - untyped_body_constraints, - ); - - // an extra constraint that propagates information to the solver to check for invalid recursion - // and generate a good error message there. - let (loc_symbols, expr_regions): (Vec<_>, Vec<_>) = defs - .iter() - .flat_map(|def| { - symbols_introduced_from_pattern(&def.loc_pattern) - .map(move |loc_symbol| ((loc_symbol.value, loc_symbol.region), def.loc_expr.region)) - }) - .unzip(); - - let cycle_constraint = constraints.check_cycle(loc_symbols, expr_regions, cycle_mark); - - let typed_body_constraints = constraints.and_constraint(rigid_info.constraints); - let typed_body_and_final_constr = - constraints.and_constraint([typed_body_constraints, cycle_constraint, body_con]); - - // 3. Properly generalize untyped defs after solving them. - let inner = constraints.let_constraint( - [], - flex_info.vars, - flex_info.def_types, - untyped_def_symbols_constr, - // 4 + 5. Solve the typed body defs, and the rest of the program. - typed_body_and_final_constr, - ); - - // 1. Let-generalize annotations we know. - constraints.let_constraint( - rigid_info.vars, - [], - rigid_info.def_types, - Constraint::True, - inner, - ) -} - -#[inline(always)] -fn constrain_field_update( - constraints: &mut Constraints, - env: &mut Env, - var: Variable, - region: Region, - field: Lowercase, - loc_expr: &Loc, -) -> (Variable, Type, Constraint) { - let field_type = Type::Variable(var); - let reason = Reason::RecordUpdateValue(field); - let expected = ForReason(reason, field_type.clone(), region); - let con = constrain_expr(constraints, env, loc_expr.region, &loc_expr.value, expected); - - (var, field_type, con) -} diff --git a/compiler/constrain/src/lib.rs b/compiler/constrain/src/lib.rs deleted file mode 100644 index 94067ab076..0000000000 --- a/compiler/constrain/src/lib.rs +++ /dev/null @@ -1,7 +0,0 @@ -#![warn(clippy::dbg_macro)] -// See github.com/rtfeldman/roc/issues/800 for discussion of the large_enum_variant check. -#![allow(clippy::large_enum_variant)] -pub mod builtins; -pub mod expr; -pub mod module; -pub mod pattern; diff --git a/compiler/constrain/src/module.rs b/compiler/constrain/src/module.rs deleted file mode 100644 index 5bba8efb6f..0000000000 --- a/compiler/constrain/src/module.rs +++ /dev/null @@ -1,320 +0,0 @@ -use crate::expr::{constrain_def_make_constraint, constrain_def_pattern, Env}; -use roc_builtins::std::StdLib; -use roc_can::abilities::{AbilitiesStore, MemberTypeInfo, SolvedSpecializations}; -use roc_can::constraint::{Constraint, Constraints}; -use roc_can::def::Declaration; -use roc_can::expected::Expected; -use roc_can::pattern::Pattern; -use roc_collections::all::MutMap; -use roc_error_macros::internal_error; -use roc_module::symbol::{ModuleId, Symbol}; -use roc_region::all::{Loc, Region}; -use roc_types::solved_types::{FreeVars, SolvedType}; -use roc_types::subs::{VarStore, Variable}; -use roc_types::types::{AnnotationSource, Category, Type}; - -/// The types of all exposed values/functions of a collection of modules -#[derive(Clone, Debug, Default)] -pub struct ExposedByModule { - exposed: MutMap, -} - -impl ExposedByModule { - pub fn insert(&mut self, module_id: ModuleId, exposed: ExposedModuleTypes) { - self.exposed.insert(module_id, exposed); - } - - pub fn get(&self, module_id: &ModuleId) -> Option<&ExposedModuleTypes> { - self.exposed.get(module_id) - } - - /// Convenient when you need mutable access to the StorageSubs in the ExposedModuleTypes - pub fn get_mut(&mut self, module_id: &ModuleId) -> Option<&mut ExposedModuleTypes> { - self.exposed.get_mut(module_id) - } - - /// Create a clone of `self` that has just a subset of the modules - /// - /// Useful when we know what modules a particular module imports, and want just - /// the exposed types for those exposed modules. - pub fn retain_modules<'a>(&self, it: impl Iterator) -> Self { - let mut output = Self::default(); - - for module_id in it { - match self.exposed.get(module_id) { - None => { - internal_error!("Module {:?} did not register its exposed values", module_id) - } - Some(exposed_types) => { - output.exposed.insert(*module_id, exposed_types.clone()); - } - } - } - - output - } - - pub fn iter_all(&self) -> impl Iterator { - self.exposed.iter() - } -} - -#[derive(Clone, Debug, Default)] -pub struct ExposedForModule { - pub exposed_by_module: ExposedByModule, - pub imported_values: Vec, -} - -impl ExposedForModule { - pub fn new<'a>( - it: impl Iterator, - exposed_by_module: ExposedByModule, - ) -> Self { - let mut imported_values = Vec::new(); - - for symbol in it { - let module = exposed_by_module.exposed.get(&symbol.module_id()); - if let Some(ExposedModuleTypes { .. }) = module { - imported_values.push(*symbol); - } else { - continue; - } - } - - Self { - imported_values, - exposed_by_module, - } - } -} - -/// The types of all exposed values/functions of a module -#[derive(Clone, Debug)] -pub struct ExposedModuleTypes { - pub stored_vars_by_symbol: Vec<(Symbol, Variable)>, - pub storage_subs: roc_types::subs::StorageSubs, - pub solved_specializations: SolvedSpecializations, -} - -pub fn constrain_module( - constraints: &mut Constraints, - symbols_from_requires: Vec<(Loc, Loc)>, - abilities_store: &AbilitiesStore, - declarations: &[Declaration], - home: ModuleId, -) -> Constraint { - let constraint = crate::expr::constrain_decls(constraints, home, declarations); - let constraint = - constrain_symbols_from_requires(constraints, symbols_from_requires, home, constraint); - let constraint = frontload_ability_constraints(constraints, abilities_store, home, constraint); - - // The module constraint should always save the environment at the end. - debug_assert!(constraints.contains_save_the_environment(&constraint)); - - constraint -} - -fn constrain_symbols_from_requires( - constraints: &mut Constraints, - symbols_from_requires: Vec<(Loc, Loc)>, - home: ModuleId, - constraint: Constraint, -) -> Constraint { - symbols_from_requires - .into_iter() - .fold(constraint, |constraint, (loc_symbol, loc_type)| { - if loc_symbol.value.module_id() == home { - // 1. Required symbols can only be specified in package modules - // 2. Required symbols come from app modules - // But, if we are running e.g. `roc check` on a package module, there is no app - // module, and we will have instead put the required symbols in the package module - // namespace. If this is the case, we want to introduce the symbols as if they had - // the types they are annotated with. - let rigids = Default::default(); - let mut env = Env { - home, - rigids, - resolutions_to_make: vec![], - }; - let pattern = Loc::at_zero(roc_can::pattern::Pattern::Identifier(loc_symbol.value)); - - let def_pattern_state = - constrain_def_pattern(constraints, &mut env, &pattern, loc_type.value); - - debug_assert!(env.resolutions_to_make.is_empty()); - - constrain_def_make_constraint( - constraints, - // No new rigids or flex vars because they are represented in the type - // annotation. - std::iter::empty(), - std::iter::empty(), - Constraint::True, - constraint, - def_pattern_state, - ) - } else { - // Otherwise, this symbol comes from an app module - we want to check that the type - // provided by the app is in fact what the package module requires. - let arity = loc_type.value.arity(); - let provided_eq_requires_constr = constraints.lookup( - loc_symbol.value, - Expected::FromAnnotation( - loc_symbol.map(|&s| Pattern::Identifier(s)), - arity, - AnnotationSource::RequiredSymbol { - region: loc_type.region, - }, - loc_type.value, - ), - loc_type.region, - ); - constraints.and_constraint([provided_eq_requires_constr, constraint]) - } - }) -} - -pub fn frontload_ability_constraints( - constraints: &mut Constraints, - abilities_store: &AbilitiesStore, - home: ModuleId, - mut constraint: Constraint, -) -> Constraint { - for (member_name, member_data) in abilities_store.root_ability_members().iter() { - if let MemberTypeInfo::Local { - signature_var, - variables: vars, - signature, - } = &member_data.typ - { - let signature_var = *signature_var; - let rigids = Default::default(); - let mut env = Env { - home, - rigids, - resolutions_to_make: vec![], - }; - let pattern = Loc::at_zero(roc_can::pattern::Pattern::Identifier(*member_name)); - - let mut def_pattern_state = constrain_def_pattern( - constraints, - &mut env, - &pattern, - Type::Variable(signature_var), - ); - - debug_assert!(env.resolutions_to_make.is_empty()); - - def_pattern_state.vars.push(signature_var); - - let rigid_variables = vars.rigid_vars.iter().chain(vars.able_vars.iter()).copied(); - let infer_variables = vars.flex_vars.iter().copied(); - - def_pattern_state - .constraints - .push(constraints.equal_types_var( - signature_var, - Expected::NoExpectation(signature.clone()), - Category::Storage(file!(), line!()), - Region::zero(), - )); - - constraint = constrain_def_make_constraint( - constraints, - rigid_variables, - infer_variables, - Constraint::True, - constraint, - def_pattern_state, - ); - } - } - constraint -} - -#[derive(Debug, Clone)] -pub struct Import { - pub loc_symbol: Loc, - pub solved_type: SolvedType, -} - -pub fn introduce_builtin_imports( - constraints: &mut Constraints, - imports: Vec, - body_con: Constraint, - var_store: &mut VarStore, -) -> Constraint { - let stdlib = roc_builtins::std::borrow_stdlib(); - let (rigid_vars, def_types) = constrain_builtin_imports(stdlib, imports, var_store); - constraints.let_import_constraint(rigid_vars, def_types, body_con, &[]) -} - -pub fn constrain_builtin_imports( - stdlib: &StdLib, - imports: Vec, - var_store: &mut VarStore, -) -> (Vec, Vec<(Symbol, Loc)>) { - let mut def_types = Vec::new(); - let mut rigid_vars = Vec::new(); - - for symbol in imports { - let mut free_vars = FreeVars::default(); - - let import = match stdlib.types.get(&symbol) { - Some((solved_type, region)) => { - let loc_symbol = Loc { - value: symbol, - region: *region, - }; - - Import { - loc_symbol, - solved_type: solved_type.clone(), - } - } - None => { - continue; - } - }; - - let loc_symbol = import.loc_symbol; - - // an imported symbol can be either an alias or a value - match import.solved_type { - SolvedType::Alias(symbol, _, _, _, _) if symbol == loc_symbol.value => { - // do nothing, in the future the alias definitions should not be in the list of imported values - } - _ => { - let typ = roc_types::solved_types::to_type( - &import.solved_type, - &mut free_vars, - var_store, - ); - - def_types.push(( - loc_symbol.value, - Loc { - region: loc_symbol.region, - value: typ, - }, - )); - - for (_, var) in free_vars.named_vars { - rigid_vars.push(var); - } - - for var in free_vars.wildcards { - rigid_vars.push(var); - } - - // Variables can lose their name during type inference. But the unnamed - // variables are still part of a signature, and thus must be treated as rigids here! - for (_, var) in free_vars.unnamed_vars { - rigid_vars.push(var); - } - } - } - } - - (rigid_vars, def_types) -} diff --git a/compiler/constrain/src/pattern.rs b/compiler/constrain/src/pattern.rs deleted file mode 100644 index 5aa9eea944..0000000000 --- a/compiler/constrain/src/pattern.rs +++ /dev/null @@ -1,597 +0,0 @@ -use crate::builtins; -use crate::expr::{constrain_expr, Env}; -use roc_can::constraint::{Constraint, Constraints}; -use roc_can::expected::{Expected, PExpected}; -use roc_can::pattern::Pattern::{self, *}; -use roc_can::pattern::{DestructType, RecordDestruct}; -use roc_collections::all::{HumanIndex, SendMap}; -use roc_module::ident::Lowercase; -use roc_module::symbol::Symbol; -use roc_region::all::{Loc, Region}; -use roc_types::subs::Variable; -use roc_types::types::{ - AliasKind, Category, OptAbleType, PReason, PatternCategory, Reason, RecordField, Type, - TypeExtension, -}; - -#[derive(Default)] -pub struct PatternState { - pub headers: SendMap>, - pub vars: Vec, - pub constraints: Vec, - pub delayed_is_open_constraints: Vec, -} - -/// If there is a type annotation, the pattern state headers can be optimized by putting the -/// annotation in the headers. Normally -/// -/// x = 4 -/// -/// Would add `x => <42>` to the headers (i.e., symbol points to a type variable). If the -/// definition has an annotation, we instead now add `x => Int`. -pub fn headers_from_annotation( - pattern: &Pattern, - annotation: &Loc<&Type>, -) -> Option>> { - let mut headers = SendMap::default(); - // Check that the annotation structurally agrees with the pattern, preventing e.g. `{ x, y } : Int` - // in such incorrect cases we don't put the full annotation in headers, just a variable, and let - // inference generate a proper error. - let is_structurally_valid = headers_from_annotation_help(pattern, annotation, &mut headers); - - if is_structurally_valid { - Some(headers) - } else { - None - } -} - -fn headers_from_annotation_help( - pattern: &Pattern, - annotation: &Loc<&Type>, - headers: &mut SendMap>, -) -> bool { - match pattern { - Identifier(symbol) - | Shadowed(_, _, symbol) - // TODO(abilities): handle linking the member def to the specialization ident - | AbilityMemberSpecialization { - ident: symbol, - specializes: _, - } => { - let typ = Loc::at(annotation.region, annotation.value.clone()); - headers.insert(*symbol, typ); - true - } - Underscore - | MalformedPattern(_, _) - | UnsupportedPattern(_) - | OpaqueNotInScope(..) - | NumLiteral(..) - | IntLiteral(..) - | FloatLiteral(..) - | SingleQuote(_) - | StrLiteral(_) => true, - - RecordDestructure { destructs, .. } => match annotation.value.shallow_dealias() { - Type::Record(fields, _) => { - for loc_destruct in destructs { - let destruct = &loc_destruct.value; - - // NOTE: We ignore both Guard and optionality when - // determining the type of the assigned def (which is what - // gets added to the header here). - // - // For example, no matter whether it's `{ x } = rec` or - // `{ x ? 0 } = rec` or `{ x: 5 } -> ...` in all cases - // the type of `x` within the binding itself is the same. - if let Some(field_type) = fields.get(&destruct.label) { - headers.insert( - destruct.symbol, - Loc::at(annotation.region, field_type.clone().into_inner()), - ); - } else { - return false; - } - } - true - } - Type::EmptyRec => destructs.is_empty(), - _ => false, - }, - - AppliedTag { - tag_name, - arguments, - .. - } => match annotation.value.shallow_dealias() { - Type::TagUnion(tags, _) => { - if let Some((_, arg_types)) = tags.iter().find(|(name, _)| name == tag_name) { - if !arguments.len() == arg_types.len() { - return false; - } - - arguments - .iter() - .zip(arg_types.iter()) - .all(|(arg_pattern, arg_type)| { - headers_from_annotation_help( - &arg_pattern.1.value, - &Loc::at(annotation.region, arg_type), - headers, - ) - }) - } else { - false - } - } - _ => false, - }, - - UnwrappedOpaque { - whole_var: _, - opaque, - argument, - specialized_def_type: _, - type_arguments: pat_type_arguments, - lambda_set_variables: pat_lambda_set_variables, - } => match &annotation.value { - Type::Alias { - symbol, - kind: AliasKind::Opaque, - actual, - type_arguments, - lambda_set_variables, - } if symbol == opaque - && type_arguments.len() == pat_type_arguments.len() - && lambda_set_variables.len() == pat_lambda_set_variables.len() => - { - let typ = Loc::at(annotation.region, annotation.value.clone()); - headers.insert(*opaque, typ); - - let (_, argument_pat) = &**argument; - headers_from_annotation_help( - &argument_pat.value, - &Loc::at(annotation.region, actual), - headers, - ) - } - _ => false, - }, - } -} - -/// This accepts PatternState (rather than returning it) so that the caller can -/// initialize the Vecs in PatternState using with_capacity -/// based on its knowledge of their lengths. -pub fn constrain_pattern( - constraints: &mut Constraints, - env: &mut Env, - pattern: &Pattern, - region: Region, - expected: PExpected, - state: &mut PatternState, -) { - match pattern { - Underscore => { - // This is an underscore in a position where we destruct a variable, - // like a when expression: - // when x is - // A -> "" - // _ -> "" - // so, we know that "x" (in this case, a tag union) must be open. - if could_be_a_tag_union(expected.get_type_ref()) { - state - .delayed_is_open_constraints - .push(constraints.is_open_type(expected.get_type())); - } - } - UnsupportedPattern(_) | MalformedPattern(_, _) | OpaqueNotInScope(..) => { - // Erroneous patterns don't add any constraints. - } - - Identifier(symbol) | Shadowed(_, _, symbol) => { - if could_be_a_tag_union(expected.get_type_ref()) { - state - .delayed_is_open_constraints - .push(constraints.is_open_type(expected.get_type_ref().clone())); - } - - state.headers.insert( - *symbol, - Loc { - region, - value: expected.get_type(), - }, - ); - } - - AbilityMemberSpecialization { - ident: symbol, - specializes: _, - } => { - if could_be_a_tag_union(expected.get_type_ref()) { - state - .constraints - .push(constraints.is_open_type(expected.get_type_ref().clone())); - } - - state.headers.insert( - *symbol, - Loc { - region, - value: expected.get_type(), - }, - ); - } - - &NumLiteral(var, _, _, bound) => { - state.vars.push(var); - - let num_type = builtins::num_num(Type::Variable(var)); - - let num_type = builtins::add_numeric_bound_constr( - constraints, - &mut state.constraints, - num_type, - bound, - region, - Category::Num, - ); - - state.constraints.push(constraints.equal_pattern_types( - num_type, - expected, - PatternCategory::Num, - region, - )); - } - - &IntLiteral(num_var, precision_var, _, _, bound) => { - // First constraint on the free num var; this improves the resolved type quality in - // case the bound is an alias. - let num_type = builtins::add_numeric_bound_constr( - constraints, - &mut state.constraints, - Type::Variable(num_var), - bound, - region, - Category::Int, - ); - - // Link the free num var with the int var and our expectation. - let int_type = builtins::num_int(Type::Variable(precision_var)); - - state.constraints.push(constraints.equal_types( - num_type, // TODO check me if something breaks! - Expected::NoExpectation(int_type), - Category::Int, - region, - )); - - // Also constrain the pattern against the num var, again to reuse aliases if they're present. - state.constraints.push(constraints.equal_pattern_types( - Type::Variable(num_var), - expected, - PatternCategory::Int, - region, - )); - } - - &FloatLiteral(num_var, precision_var, _, _, bound) => { - // First constraint on the free num var; this improves the resolved type quality in - // case the bound is an alias. - let num_type = builtins::add_numeric_bound_constr( - constraints, - &mut state.constraints, - Type::Variable(num_var), - bound, - region, - Category::Float, - ); - - // Link the free num var with the float var and our expectation. - let float_type = builtins::num_float(Type::Variable(precision_var)); - - state.constraints.push(constraints.equal_types( - num_type.clone(), // TODO check me if something breaks! - Expected::NoExpectation(float_type), - Category::Float, - region, - )); - - // Also constrain the pattern against the num var, again to reuse aliases if they're present. - state.constraints.push(constraints.equal_pattern_types( - num_type, // TODO check me if something breaks! - expected, - PatternCategory::Float, - region, - )); - } - - StrLiteral(_) => { - state.constraints.push(constraints.equal_pattern_types( - builtins::str_type(), - expected, - PatternCategory::Str, - region, - )); - } - - SingleQuote(_) => { - state.constraints.push(constraints.equal_pattern_types( - builtins::num_u32(), - expected, - PatternCategory::Character, - region, - )); - } - - RecordDestructure { - whole_var, - ext_var, - destructs, - } => { - state.vars.push(*whole_var); - state.vars.push(*ext_var); - let ext_type = Type::Variable(*ext_var); - - let mut field_types: SendMap> = SendMap::default(); - - for Loc { - value: - RecordDestruct { - var, - label, - symbol, - typ, - }, - .. - } in destructs - { - let pat_type = Type::Variable(*var); - let expected = PExpected::NoExpectation(pat_type.clone()); - - if !state.headers.contains_key(symbol) { - state - .headers - .insert(*symbol, Loc::at(region, pat_type.clone())); - } - - let field_type = match typ { - DestructType::Guard(guard_var, loc_guard) => { - state.constraints.push(constraints.pattern_presence( - Type::Variable(*guard_var), - PExpected::ForReason( - PReason::PatternGuard, - pat_type.clone(), - loc_guard.region, - ), - PatternCategory::PatternGuard, - region, - )); - state.vars.push(*guard_var); - - constrain_pattern( - constraints, - env, - &loc_guard.value, - loc_guard.region, - expected, - state, - ); - - RecordField::Demanded(pat_type) - } - DestructType::Optional(expr_var, loc_expr) => { - state.constraints.push(constraints.pattern_presence( - Type::Variable(*expr_var), - PExpected::ForReason( - PReason::OptionalField, - pat_type.clone(), - loc_expr.region, - ), - PatternCategory::PatternDefault, - region, - )); - - state.vars.push(*expr_var); - - let expr_expected = Expected::ForReason( - Reason::RecordDefaultField(label.clone()), - pat_type.clone(), - loc_expr.region, - ); - - let expr_con = constrain_expr( - constraints, - env, - loc_expr.region, - &loc_expr.value, - expr_expected, - ); - state.constraints.push(expr_con); - - RecordField::Optional(pat_type) - } - DestructType::Required => { - // No extra constraints necessary. - RecordField::Demanded(pat_type) - } - }; - - field_types.insert(label.clone(), field_type); - - state.vars.push(*var); - } - - let record_type = Type::Record(field_types, TypeExtension::from_type(ext_type)); - - let whole_con = constraints.equal_types( - Type::Variable(*whole_var), - Expected::NoExpectation(record_type), - Category::Storage(std::file!(), std::line!()), - region, - ); - - let record_con = constraints.pattern_presence( - Type::Variable(*whole_var), - expected, - PatternCategory::Record, - region, - ); - - state.constraints.push(whole_con); - state.constraints.push(record_con); - } - AppliedTag { - whole_var, - ext_var, - tag_name, - arguments, - } => { - let mut argument_types = Vec::with_capacity(arguments.len()); - for (index, (pattern_var, loc_pattern)) in arguments.iter().enumerate() { - state.vars.push(*pattern_var); - - let pattern_type = Type::Variable(*pattern_var); - argument_types.push(pattern_type.clone()); - - let expected = PExpected::ForReason( - PReason::TagArg { - tag_name: tag_name.clone(), - index: HumanIndex::zero_based(index), - }, - pattern_type, - region, - ); - constrain_pattern( - constraints, - env, - &loc_pattern.value, - loc_pattern.region, - expected, - state, - ); - } - - let pat_category = PatternCategory::Ctor(tag_name.clone()); - - let whole_con = constraints.includes_tag( - expected.clone().get_type(), - tag_name.clone(), - argument_types.clone(), - pat_category.clone(), - region, - ); - - let tag_con = constraints.pattern_presence( - Type::Variable(*whole_var), - expected, - pat_category, - region, - ); - - state.vars.push(*whole_var); - state.vars.push(*ext_var); - state.constraints.push(whole_con); - state.constraints.push(tag_con); - state - .constraints - .append(&mut state.delayed_is_open_constraints); - } - - UnwrappedOpaque { - whole_var, - opaque, - argument, - specialized_def_type, - type_arguments, - lambda_set_variables, - } => { - // Suppose we are constraining the pattern \@Id who, where Id n := [Id U64 n] - let (arg_pattern_var, loc_arg_pattern) = &**argument; - let arg_pattern_type = Type::Variable(*arg_pattern_var); - - let opaque_type = Type::Alias { - symbol: *opaque, - type_arguments: type_arguments - .iter() - .map(|v| OptAbleType { - typ: Type::Variable(v.var), - opt_ability: v.opt_ability, - }) - .collect(), - lambda_set_variables: lambda_set_variables.clone(), - actual: Box::new(arg_pattern_type.clone()), - kind: AliasKind::Opaque, - }; - - // First, add a constraint for the argument "who" - let arg_pattern_expected = PExpected::NoExpectation(arg_pattern_type.clone()); - constrain_pattern( - constraints, - env, - &loc_arg_pattern.value, - loc_arg_pattern.region, - arg_pattern_expected, - state, - ); - - // Next, link `whole_var` to the opaque type of "@Id who" - let whole_con = constraints.equal_types( - Type::Variable(*whole_var), - Expected::NoExpectation(opaque_type), - Category::Storage(std::file!(), std::line!()), - region, - ); - - // Link the entire wrapped opaque type (with the now-constrained argument) to the type - // variables of the opaque type. - // - // For example, suppose we have `O k := [A k, B k]`, and the pattern `@O (A s) -> s == ""`. - // Previous constraints will have solved `typeof s ~ Str`, and we have the - // `specialized_def_type` being `[A k1, B k1]`, specializing `k` as `k1` for this opaque - // usage. - // We now want to link `typeof s ~ k1`, so to capture this relationship, we link - // the type of `A s` (the arg type) to `[A k1, B k1]` (the specialized opaque type). - // - // This must **always** be a presence constraint, that is enforcing - // `[A k1, B k1] += typeof (A s)`, because we are in a destructure position and not - // all constructors are covered in this branch! - let link_type_variables_con = constraints.pattern_presence( - arg_pattern_type, - PExpected::NoExpectation((**specialized_def_type).clone()), - PatternCategory::Opaque(*opaque), - loc_arg_pattern.region, - ); - - // Next, link `whole_var` (the type of "@Id who") to the expected type - let opaque_pattern_con = constraints.pattern_presence( - Type::Variable(*whole_var), - expected, - PatternCategory::Opaque(*opaque), - region, - ); - - state - .vars - .extend_from_slice(&[*arg_pattern_var, *whole_var]); - // Also add the fresh variables we created for the type argument and lambda sets - state.vars.extend(type_arguments.iter().map(|v| v.var)); - state.vars.extend(lambda_set_variables.iter().map(|v| { - v.0.expect_variable("all lambda sets should be fresh variables here") - })); - - state.constraints.extend_from_slice(&[ - whole_con, - link_type_variables_con, - opaque_pattern_con, - ]); - } - } -} - -fn could_be_a_tag_union(typ: &Type) -> bool { - !matches!(typ, Type::Apply(..) | Type::Function(..) | Type::Record(..)) -} diff --git a/compiler/debug_flags/Cargo.toml b/compiler/debug_flags/Cargo.toml deleted file mode 100644 index 8aecfb22ec..0000000000 --- a/compiler/debug_flags/Cargo.toml +++ /dev/null @@ -1,6 +0,0 @@ -[package] -name = "roc_debug_flags" -version = "0.1.0" -edition = "2021" - -[dependencies] diff --git a/compiler/debug_flags/src/lib.rs b/compiler/debug_flags/src/lib.rs deleted file mode 100644 index 5af06aff4c..0000000000 --- a/compiler/debug_flags/src/lib.rs +++ /dev/null @@ -1,111 +0,0 @@ -//! Flags for debugging the Roc compiler. -//! -//! Lists environment variable flags that can be enabled for verbose debugging features in debug -//! builds of the compiler. -//! -//! For example, I might define the following alias to run cargo with all unifications and -//! expanded type aliases printed: -//! -//! ```bash -//! alias cargo="\ -//! ROC_PRINT_UNIFICATIONS=1 \ -//! ROC_PRETTY_PRINT_ALIAS_CONTENTS=1 \ -//! cargo" -//! ``` -//! -//! More generally, I have the following: -//! -//! ```bash -//! alias cargo="\ -//! ROC_PRETTY_PRINT_ALIAS_CONTENTS=0 \ -//! ROC_PRINT_UNIFICATIONS=0 \ -//! ROC_PRINT_MISMATCHES=0 \ -//! ROC_PRINT_IR_AFTER_SPECIALIZATION=0 \ -//! ROC_PRINT_IR_AFTER_RESET_REUSE=0 \ -//! ROC_PRINT_IR_AFTER_REFCOUNT=0 \ -//! ROC_PRETTY_PRINT_IR_SYMBOLS=0 \ -//! # ...other flags -//! cargo" -//! ``` -//! -//! Now you can turn debug flags on and off as you like. -//! -//! These flags are also set in .cargo/config found at the repository root. You can modify them -//! there to avoid maintaining a separate script. - -#[macro_export] -macro_rules! dbg_do { - ($flag:path, $expr:expr) => { - #[cfg(debug_assertions)] - { - let flag = std::env::var($flag); - if !flag.is_err() && flag.as_deref() != Ok("0") { - $expr - } - } - }; -} - -macro_rules! flags { - ($($(#[doc = $doc:expr])+ $flag:ident)*) => {$( - $(#[doc = $doc])+ - pub static $flag: &str = stringify!($flag); - )*}; -} - -flags! { - // ===Types=== - - /// Expands the contents of aliases during pretty-printing of types. - ROC_PRETTY_PRINT_ALIAS_CONTENTS - - // ===Solve=== - - /// Prints type unifications, before and after they happen. - ROC_PRINT_UNIFICATIONS - - /// Prints all type mismatches hit during type unification. - ROC_PRINT_MISMATCHES - - /// Verifies that after let-generalization of a def, any rigid variables in the type annotation - /// of the def are indeed generalized. - /// - /// Note that rigids need not always be generalized in a def. For example, they may be - /// constrained by a type from a lower rank, as `b` is in the following def: - /// - /// F a : { foo : a } - /// foo = \arg -> - /// x : F b - /// x = arg - /// x.foo - /// - /// Instead, this flag is useful for checking that in general, introduction is correct, when - /// chainging how defs are constrained. - ROC_VERIFY_RIGID_LET_GENERALIZED - - // ===Mono=== - - /// Writes a pretty-printed mono IR to stderr after function specialization. - ROC_PRINT_IR_AFTER_SPECIALIZATION - - /// Writes a pretty-printed mono IR to stderr after insertion of reset/reuse - /// instructions. - ROC_PRINT_IR_AFTER_RESET_REUSE - - /// Writes a pretty-printed mono IR to stderr after insertion of refcount - /// instructions. - ROC_PRINT_IR_AFTER_REFCOUNT - - /// Prints debug information during the alias analysis pass. - ROC_DEBUG_ALIAS_ANALYSIS - - // ===LLVM Gen=== - - /// Prints LLVM function verification output. - ROC_PRINT_LLVM_FN_VERIFICATION - - // ===Load=== - - /// Print load phases as they complete. - ROC_PRINT_LOAD_LOG -} diff --git a/compiler/exhaustive/Cargo.toml b/compiler/exhaustive/Cargo.toml deleted file mode 100644 index a0cbbaf717..0000000000 --- a/compiler/exhaustive/Cargo.toml +++ /dev/null @@ -1,12 +0,0 @@ -[package] -name = "roc_exhaustive" -version = "0.1.0" -authors = ["The Roc Contributors"] -license = "UPL-1.0" -edition = "2021" - -[dependencies] -roc_collections = { path = "../collections" } -roc_region = { path = "../region" } -roc_module = { path = "../module" } -roc_std = { path = "../../roc_std", default-features = false } diff --git a/compiler/exhaustive/src/lib.rs b/compiler/exhaustive/src/lib.rs deleted file mode 100644 index f432dd2270..0000000000 --- a/compiler/exhaustive/src/lib.rs +++ /dev/null @@ -1,460 +0,0 @@ -//! Exhaustiveness checking, based on "Warning for pattern matching" (Luc Maranget, 2007). -//! http://moscova.inria.fr/~maranget/papers/warn/warn.pdf - -use roc_collections::all::{HumanIndex, MutMap}; -use roc_module::{ - ident::{Lowercase, TagIdIntType, TagName}, - symbol::Symbol, -}; -use roc_region::all::Region; - -use self::Pattern::*; - -#[derive(Clone, Debug, PartialEq, Eq, Hash)] -pub struct Union { - pub alternatives: Vec, - pub render_as: RenderAs, -} - -impl Union { - pub fn newtype_wrapper(name: CtorName, arity: usize) -> Self { - let alternatives = vec![Ctor { - name, - tag_id: TagId(0), - arity, - }]; - - Union { - alternatives, - render_as: RenderAs::Tag, - } - } -} - -#[derive(Clone, Debug, PartialEq, Eq, Hash)] -pub enum RenderAs { - Tag, - Opaque, - Record(Vec), - Guard, -} - -#[derive(Clone, Debug, PartialEq, Eq, Hash, Copy)] -pub struct TagId(pub TagIdIntType); - -#[derive(Clone, Debug, PartialEq, Eq, Hash)] -pub enum CtorName { - Tag(TagName), - Opaque(Symbol), -} - -impl CtorName { - pub fn is_tag(&self, tag_name: &TagName) -> bool { - match self { - Self::Tag(test) => test == tag_name, - _ => false, - } - } -} - -#[derive(Clone, Debug, PartialEq, Eq, Hash)] -pub struct Ctor { - pub name: CtorName, - pub tag_id: TagId, - pub arity: usize, -} - -#[derive(Clone, Debug, PartialEq)] -pub enum Pattern { - Anything, - Literal(Literal), - Ctor(Union, TagId, std::vec::Vec), -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum Literal { - Int([u8; 16]), - U128([u8; 16]), - Bit(bool), - Byte(u8), - /// Stores the float bits - Float(u64), - Decimal([u8; 16]), - Str(Box), -} - -/// Error - -#[derive(Clone, Debug, PartialEq)] -pub enum Error { - Incomplete(Region, Context, Vec), - Redundant { - overall_region: Region, - branch_region: Region, - index: HumanIndex, - }, -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum Context { - BadArg, - BadDestruct, - BadCase, -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum Guard { - HasGuard, - NoGuard, -} - -/// Check - -pub fn check( - region: Region, - context: Context, - matrix: Vec>, -) -> Result<(), Vec> { - let mut errors = Vec::new(); - let bad_patterns = is_exhaustive(&matrix, 1); - if !bad_patterns.is_empty() { - // TODO i suspect this is like a concat in in practice? code below can panic - // if this debug_assert! ever fails, the theory is disproven - debug_assert!(bad_patterns.iter().map(|v| v.len()).sum::() == bad_patterns.len()); - let heads = bad_patterns.into_iter().map(|mut v| v.remove(0)).collect(); - errors.push(Error::Incomplete(region, context, heads)); - return Err(errors); - } - Ok(()) -} - -/// EXHAUSTIVE PATTERNS - -/// INVARIANTS: -/// -/// The initial rows "matrix" are all of length 1 -/// The initial count of items per row "n" is also 1 -/// The resulting rows are examples of missing patterns -fn is_exhaustive(matrix: &RefPatternMatrix, n: usize) -> PatternMatrix { - if matrix.is_empty() { - vec![std::iter::repeat(Anything).take(n).collect()] - } else if n == 0 { - vec![] - } else { - let ctors = collect_ctors(matrix); - let num_seen = ctors.len(); - - if num_seen == 0 { - let new_matrix: Vec<_> = matrix - .iter() - .filter_map(|row| specialize_row_by_anything(row)) - .collect(); - let mut rest = is_exhaustive(&new_matrix, n - 1); - - for row in rest.iter_mut() { - row.push(Anything); - } - - rest - } else { - let alts = ctors.iter().next().unwrap().1; - - let alt_list = &alts.alternatives; - let num_alts = alt_list.len(); - - if num_seen < num_alts { - let new_matrix: Vec<_> = matrix - .iter() - .filter_map(|row| specialize_row_by_anything(row)) - .collect(); - let rest: Vec> = is_exhaustive(&new_matrix, n - 1); - - let last: _ = alt_list - .iter() - .filter_map(|r| is_missing(alts.clone(), &ctors, r)); - - let mut result = Vec::new(); - - for last_option in last { - for mut row in rest.clone() { - row.push(last_option.clone()); - - result.push(row); - } - } - - result - } else { - let is_alt_exhaustive = |Ctor { arity, tag_id, .. }| { - let new_matrix: Vec<_> = matrix - .iter() - .filter_map(|r| specialize_row_by_ctor(tag_id, arity, r)) - .collect(); - let rest: Vec> = is_exhaustive(&new_matrix, arity + n - 1); - - let mut result = Vec::with_capacity(rest.len()); - for row in rest { - result.push(recover_ctor(alts.clone(), tag_id, arity, row)); - } - - result - }; - - alt_list - .iter() - .cloned() - .flat_map(is_alt_exhaustive) - .collect() - } - } - } -} - -fn is_missing(union: Union, ctors: &MutMap, ctor: &Ctor) -> Option { - let Ctor { arity, tag_id, .. } = ctor; - - if ctors.contains_key(tag_id) { - None - } else { - let anythings = std::iter::repeat(Anything).take(*arity).collect(); - Some(Pattern::Ctor(union, *tag_id, anythings)) - } -} - -fn recover_ctor( - union: Union, - tag_id: TagId, - arity: usize, - mut patterns: Vec, -) -> Vec { - let mut rest = patterns.split_off(arity); - let args = patterns; - - rest.push(Ctor(union, tag_id, args)); - - rest -} - -/// Check if a new row "vector" is useful given previous rows "matrix" -pub fn is_useful(mut old_matrix: PatternMatrix, mut vector: Row) -> bool { - let mut matrix = Vec::with_capacity(old_matrix.len()); - - // this loop ping-pongs the rows between old_matrix and matrix - 'outer: loop { - match vector.pop() { - _ if old_matrix.is_empty() => { - // No rows are the same as the new vector! The vector is useful! - break true; - } - None => { - // There is nothing left in the new vector, but we still have - // rows that match the same things. This is not a useful vector! - break false; - } - Some(first_pattern) => { - // NOTE: if there are bugs in this code, look at the ordering of the row/matrix - - match first_pattern { - // keep checking rows that start with this Ctor or Anything - Ctor(_, id, args) => { - specialize_row_by_ctor2(id, args.len(), &mut old_matrix, &mut matrix); - - std::mem::swap(&mut old_matrix, &mut matrix); - - vector.extend(args); - } - - Anything => { - // check if all alternatives appear in matrix - match is_complete(&old_matrix) { - Complete::No => { - // This Anything is useful because some Ctors are missing. - // But what if a previous row has an Anything? - // If so, this one is not useful. - for mut row in old_matrix.drain(..) { - if let Some(Anything) = row.pop() { - matrix.push(row); - } - } - - std::mem::swap(&mut old_matrix, &mut matrix); - } - Complete::Yes(alternatives) => { - // All Ctors are covered, so this Anything is not needed for any - // of those. But what if some of those Ctors have subpatterns - // that make them less general? If so, this actually is useful! - for alternative in alternatives { - let Ctor { arity, tag_id, .. } = alternative; - - let mut old_matrix = old_matrix.clone(); - let mut matrix = vec![]; - specialize_row_by_ctor2( - tag_id, - arity, - &mut old_matrix, - &mut matrix, - ); - - let mut vector = vector.clone(); - vector.extend(std::iter::repeat(Anything).take(arity)); - - if is_useful(matrix, vector) { - break 'outer true; - } - } - - break false; - } - } - } - - Literal(literal) => { - // keep checking rows that start with this Literal or Anything - - for mut row in old_matrix.drain(..) { - let head = row.pop(); - let patterns = row; - - match head { - Some(Literal(lit)) => { - if lit == literal { - matrix.push(patterns); - } else { - // do nothing - } - } - Some(Anything) => matrix.push(patterns), - - Some(Ctor(_, _, _)) => panic!( - r#"Compiler bug! After type checking, constructors and literals should never align in pattern match exhaustiveness checks."# - ), - - None => panic!( - "Compiler error! Empty matrices should not get specialized." - ), - } - } - std::mem::swap(&mut old_matrix, &mut matrix); - } - } - } - } - } -} - -/// INVARIANT: (length row == N) ==> (length result == arity + N - 1) -fn specialize_row_by_ctor2( - tag_id: TagId, - arity: usize, - old_matrix: &mut PatternMatrix, - matrix: &mut PatternMatrix, -) { - for mut row in old_matrix.drain(..) { - let head = row.pop(); - let mut patterns = row; - - match head { - Some(Ctor(_, id, args)) => - if id == tag_id { - patterns.extend(args); - matrix.push(patterns); - } else { - // do nothing - } - Some(Anything) => { - // TODO order! - patterns.extend(std::iter::repeat(Anything).take(arity)); - matrix.push(patterns); - } - Some(Literal(_)) => panic!( "Compiler bug! After type checking, constructors and literal should never align in pattern match exhaustiveness checks."), - None => panic!("Compiler error! Empty matrices should not get specialized."), - } - } -} - -/// INVARIANT: (length row == N) ==> (length result == arity + N - 1) -fn specialize_row_by_ctor(tag_id: TagId, arity: usize, row: &RefRow) -> Option { - let mut row = row.to_vec(); - - let head = row.pop(); - let patterns = row; - - match head { - Some(Ctor(_, id, args)) => { - if id == tag_id { - // TODO order! - let mut new_patterns = Vec::new(); - new_patterns.extend(args); - new_patterns.extend(patterns); - Some(new_patterns) - } else { - None - } - } - Some(Anything) => { - // TODO order! - let new_patterns = std::iter::repeat(Anything) - .take(arity) - .chain(patterns) - .collect(); - Some(new_patterns) - } - Some(Literal(_)) => unreachable!( - r#"Compiler bug! After type checking, a constructor can never align with a literal: that should be a type error!"# - ), - None => panic!("Compiler error! Empty matrices should not get specialized."), - } -} - -/// INVARIANT: (length row == N) ==> (length result == N-1) -fn specialize_row_by_anything(row: &RefRow) -> Option { - let mut row = row.to_vec(); - - match row.pop() { - Some(Anything) => Some(row), - _ => None, - } -} - -/// ALL CONSTRUCTORS ARE PRESENT? - -pub enum Complete { - Yes(Vec), - No, -} - -fn is_complete(matrix: &RefPatternMatrix) -> Complete { - let ctors = collect_ctors(matrix); - let length = ctors.len(); - let mut it = ctors.into_iter(); - - match it.next() { - None => Complete::No, - Some((_, Union { alternatives, .. })) => { - if length == alternatives.len() { - Complete::Yes(alternatives) - } else { - Complete::No - } - } - } -} - -/// COLLECT CTORS - -type RefPatternMatrix = [Vec]; -type PatternMatrix = Vec>; -type RefRow = [Pattern]; -type Row = Vec; - -fn collect_ctors(matrix: &RefPatternMatrix) -> MutMap { - let mut ctors = MutMap::default(); - - for row in matrix { - if let Some(Ctor(union, id, _)) = row.get(row.len() - 1) { - ctors.insert(*id, union.clone()); - } - } - - ctors -} diff --git a/compiler/fmt/Cargo.toml b/compiler/fmt/Cargo.toml deleted file mode 100644 index a402a79d81..0000000000 --- a/compiler/fmt/Cargo.toml +++ /dev/null @@ -1,19 +0,0 @@ -[package] -name = "roc_fmt" -version = "0.1.0" -authors = ["The Roc Contributors"] -license = "UPL-1.0" -edition = "2021" - -[dependencies] -roc_collections = { path = "../collections" } -roc_region = { path = "../region" } -roc_module = { path = "../module" } -roc_parse = { path = "../parse" } -bumpalo = { version = "3.8.0", features = ["collections"] } - -[dev-dependencies] -pretty_assertions = "1.0.0" -indoc = "1.0.3" -roc_test_utils = { path = "../../test_utils" } -walkdir = "2.3.2" diff --git a/compiler/fmt/src/annotation.rs b/compiler/fmt/src/annotation.rs deleted file mode 100644 index ef4e6a26bb..0000000000 --- a/compiler/fmt/src/annotation.rs +++ /dev/null @@ -1,601 +0,0 @@ -use crate::{ - collection::{fmt_collection, Braces}, - spaces::{fmt_comments_only, fmt_spaces, NewlineAt, INDENT}, - Buf, -}; -use roc_parse::ast::{ - AssignedField, Collection, Derived, Expr, ExtractSpaces, HasClause, Tag, TypeAnnotation, - TypeHeader, -}; -use roc_parse::ident::UppercaseIdent; -use roc_region::all::Loc; - -/// Does an AST node need parens around it? -/// -/// Usually not, but there are two cases where it may be required -/// -/// 1. In a function type, function types are in parens -/// -/// a -> b, c -> d -/// (a -> b), c -> d -/// -/// 2. In applications, applications are in brackets -/// This is true in patterns, type annotations and expressions -/// -/// Just (Just a) -/// List (List a) -/// reverse (reverse l) -#[derive(PartialEq, Eq, Clone, Copy, Debug)] -pub enum Parens { - NotNeeded, - InFunctionType, - InApply, -} - -/// In an AST node, do we show newlines around it -/// -/// Sometimes, we only want to show comments, at other times -/// we also want to show newlines. By default the formatter -/// takes care of inserting newlines, but sometimes the user's -/// newlines are taken into account. -#[derive(PartialEq, Eq, Clone, Copy, Debug)] -pub enum Newlines { - No, - Yes, -} - -impl Newlines { - pub fn from_bool(yes: bool) -> Self { - if yes { - Self::Yes - } else { - Self::No - } - } -} - -pub trait Formattable { - fn is_multiline(&self) -> bool; - - fn format_with_options<'buf>( - &self, - buf: &mut Buf<'buf>, - _parens: Parens, - _newlines: Newlines, - indent: u16, - ) { - self.format(buf, indent); - } - - fn format<'buf>(&self, buf: &mut Buf<'buf>, indent: u16) { - self.format_with_options(buf, Parens::NotNeeded, Newlines::No, indent); - } -} - -/// A reference to a formattable value is also formattable -impl<'a, T> Formattable for &'a T -where - T: Formattable, -{ - fn is_multiline(&self) -> bool { - (*self).is_multiline() - } - - fn format_with_options<'buf>( - &self, - buf: &mut Buf<'buf>, - parens: Parens, - newlines: Newlines, - indent: u16, - ) { - (*self).format_with_options(buf, parens, newlines, indent) - } - - fn format<'buf>(&self, buf: &mut Buf<'buf>, indent: u16) { - (*self).format(buf, indent) - } -} - -impl<'a, T> Formattable for Collection<'a, T> -where - T: Formattable, -{ - fn is_multiline(&self) -> bool { - // if there are any comments, they must go on their own line - // because otherwise they'd comment out the closing delimiter - !self.final_comments().is_empty() || - // if any of the items in the collection are multiline, - // then the whole collection must be multiline - self.items.iter().any(Formattable::is_multiline) - } -} - -/// A Located formattable value is also formattable -impl Formattable for Loc -where - T: Formattable, -{ - fn is_multiline(&self) -> bool { - self.value.is_multiline() - } - - fn format_with_options<'buf>( - &self, - buf: &mut Buf<'buf>, - parens: Parens, - newlines: Newlines, - indent: u16, - ) { - self.value - .format_with_options(buf, parens, newlines, indent) - } - - fn format<'buf>(&self, buf: &mut Buf<'buf>, indent: u16) { - self.value.format(buf, indent) - } -} - -impl<'a> Formattable for UppercaseIdent<'a> { - fn is_multiline(&self) -> bool { - false - } - - fn format_with_options<'buf>( - &self, - buf: &mut Buf<'buf>, - _parens: Parens, - _newlines: Newlines, - _indent: u16, - ) { - buf.push_str((*self).into()) - } -} - -impl<'a> Formattable for TypeAnnotation<'a> { - fn is_multiline(&self) -> bool { - use roc_parse::ast::TypeAnnotation::*; - - match self { - // Return whether these spaces contain any Newlines - SpaceBefore(_, spaces) | SpaceAfter(_, spaces) => { - debug_assert!(!spaces.is_empty()); - - // "spaces" always contain either a newline or comment, and comments have newlines - true - } - - Wildcard | Inferred | BoundVariable(_) | Malformed(_) => false, - Function(args, result) => { - (&result.value).is_multiline() - || args.iter().any(|loc_arg| (&loc_arg.value).is_multiline()) - } - Apply(_, _, args) => args.iter().any(|loc_arg| loc_arg.value.is_multiline()), - As(lhs, _, _) => lhs.value.is_multiline(), - - Where(annot, has_clauses) => { - annot.is_multiline() || has_clauses.iter().any(|has| has.is_multiline()) - } - - Record { fields, ext } => { - match ext { - Some(ann) if ann.value.is_multiline() => return true, - _ => {} - } - - fields.items.iter().any(|field| field.value.is_multiline()) - } - - TagUnion { tags, ext } => { - match ext { - Some(ann) if ann.value.is_multiline() => return true, - _ => {} - } - - tags.iter().any(|tag| tag.value.is_multiline()) - } - } - } - - fn format_with_options<'buf>( - &self, - buf: &mut Buf<'buf>, - parens: Parens, - newlines: Newlines, - indent: u16, - ) { - use roc_parse::ast::TypeAnnotation::*; - - match self { - Function(arguments, result) => { - let write_parens = parens != Parens::NotNeeded; - - if write_parens { - buf.push('(') - } - - let mut it = arguments.iter().enumerate().peekable(); - let should_add_newlines = newlines == Newlines::Yes; - - while let Some((index, argument)) = it.next() { - let is_first = index == 0; - let is_multiline = &argument.value.is_multiline(); - - if !is_first && !is_multiline && should_add_newlines { - buf.newline(); - } - - (&argument.value).format_with_options( - buf, - Parens::InFunctionType, - Newlines::No, - indent, - ); - - if it.peek().is_some() { - buf.push_str(","); - if !should_add_newlines { - buf.spaces(1); - } - } - } - - if should_add_newlines { - buf.newline(); - buf.indent(indent); - } else { - buf.spaces(1); - } - - buf.push_str("->"); - buf.spaces(1); - - (&result.value).format_with_options( - buf, - Parens::InFunctionType, - Newlines::No, - indent, - ); - - if write_parens { - buf.push(')') - } - } - Apply(pkg, name, arguments) => { - buf.indent(indent); - // NOTE apply is never multiline - let write_parens = parens == Parens::InApply && !arguments.is_empty(); - - if write_parens { - buf.push('(') - } - - if !pkg.is_empty() { - buf.push_str(pkg); - buf.push('.'); - } - - buf.push_str(name); - - for argument in *arguments { - buf.spaces(1); - (&argument.value).format_with_options( - buf, - Parens::InApply, - Newlines::No, - indent, - ); - } - - if write_parens { - buf.push(')') - } - } - BoundVariable(v) => buf.push_str(v), - Wildcard => buf.push('*'), - Inferred => buf.push('_'), - - TagUnion { tags, ext } => { - fmt_collection(buf, indent, Braces::Square, *tags, newlines); - - if let Some(loc_ext_ann) = *ext { - loc_ext_ann.value.format(buf, indent); - } - } - - Record { fields, ext } => { - fmt_collection(buf, indent, Braces::Curly, *fields, newlines); - - if let Some(loc_ext_ann) = *ext { - loc_ext_ann.value.format(buf, indent); - } - } - - As(lhs, _spaces, TypeHeader { name, vars }) => { - // TODO use _spaces? - lhs.value - .format_with_options(buf, Parens::InFunctionType, Newlines::No, indent); - buf.spaces(1); - buf.push_str("as"); - buf.spaces(1); - buf.push_str(name.value); - for var in *vars { - buf.spaces(1); - var.value - .format_with_options(buf, Parens::NotNeeded, Newlines::No, indent); - } - } - - Where(annot, has_clauses) => { - annot.format_with_options(buf, parens, newlines, indent); - buf.spaces(1); - for (i, has) in has_clauses.iter().enumerate() { - buf.push(if i == 0 { '|' } else { ',' }); - buf.spaces(1); - has.format_with_options(buf, parens, newlines, indent); - } - } - - SpaceBefore(ann, spaces) => { - let is_function = matches!(ann, TypeAnnotation::Function(..)); - let next_newlines = if is_function && newlines == Newlines::Yes { - Newlines::Yes - } else { - Newlines::No - }; - - buf.newline(); - buf.indent(indent); - fmt_comments_only(buf, spaces.iter(), NewlineAt::Bottom, indent); - ann.format_with_options(buf, parens, next_newlines, indent) - } - SpaceAfter(ann, spaces) => { - ann.format_with_options(buf, parens, newlines, indent); - fmt_comments_only(buf, spaces.iter(), NewlineAt::Bottom, indent); - } - - Malformed(raw) => buf.push_str(raw), - } - } -} - -/// Fields are subtly different on the type and term level: -/// -/// > type: { x : Int, y : Bool } -/// > term: { x: 100, y: True } -/// -/// So we need two instances, each having the specific separator -impl<'a> Formattable for AssignedField<'a, TypeAnnotation<'a>> { - fn is_multiline(&self) -> bool { - is_multiline_assigned_field_help(self) - } - - fn format_with_options<'buf>( - &self, - buf: &mut Buf<'buf>, - parens: Parens, - newlines: Newlines, - indent: u16, - ) { - // we abuse the `Newlines` type to decide between multiline or single-line layout - format_assigned_field_help(self, buf, parens, indent, 1, newlines == Newlines::Yes); - } -} - -impl<'a> Formattable for AssignedField<'a, Expr<'a>> { - fn is_multiline(&self) -> bool { - is_multiline_assigned_field_help(self) - } - - fn format_with_options<'buf>( - &self, - buf: &mut Buf<'buf>, - parens: Parens, - newlines: Newlines, - indent: u16, - ) { - // we abuse the `Newlines` type to decide between multiline or single-line layout - format_assigned_field_help(self, buf, parens, indent, 0, newlines == Newlines::Yes); - } -} - -fn is_multiline_assigned_field_help(afield: &AssignedField<'_, T>) -> bool { - use self::AssignedField::*; - - match afield { - RequiredValue(_, spaces, ann) | OptionalValue(_, spaces, ann) => { - !spaces.is_empty() || ann.value.is_multiline() - } - LabelOnly(_) => false, - AssignedField::SpaceBefore(_, _) | AssignedField::SpaceAfter(_, _) => true, - Malformed(text) => text.chars().any(|c| c == '\n'), - } -} - -fn format_assigned_field_help<'a, 'buf, T>( - zelf: &AssignedField<'a, T>, - buf: &mut Buf<'buf>, - parens: Parens, - indent: u16, - separator_spaces: usize, - is_multiline: bool, -) where - T: Formattable, -{ - use self::AssignedField::*; - - match zelf { - RequiredValue(name, spaces, ann) => { - if is_multiline { - buf.newline(); - } - - buf.indent(indent); - buf.push_str(name.value); - - if !spaces.is_empty() { - fmt_spaces(buf, spaces.iter(), indent); - } - - buf.spaces(separator_spaces); - buf.push_str(":"); - buf.spaces(1); - ann.value.format(buf, indent); - } - OptionalValue(name, spaces, ann) => { - if is_multiline { - buf.newline(); - buf.indent(indent); - } - - buf.push_str(name.value); - - if !spaces.is_empty() { - fmt_spaces(buf, spaces.iter(), indent); - } - - buf.spaces(separator_spaces); - buf.push('?'); - ann.value.format(buf, indent); - } - LabelOnly(name) => { - if is_multiline { - buf.newline(); - buf.indent(indent); - } - - buf.push_str(name.value); - } - AssignedField::SpaceBefore(sub_field, spaces) => { - fmt_comments_only(buf, spaces.iter(), NewlineAt::Bottom, indent); - format_assigned_field_help( - sub_field, - buf, - parens, - indent, - separator_spaces, - is_multiline, - ); - } - AssignedField::SpaceAfter(sub_field, spaces) => { - format_assigned_field_help( - sub_field, - buf, - parens, - indent, - separator_spaces, - is_multiline, - ); - fmt_comments_only(buf, spaces.iter(), NewlineAt::Bottom, indent); - } - Malformed(raw) => { - buf.push_str(raw); - } - } -} - -impl<'a> Formattable for Tag<'a> { - fn is_multiline(&self) -> bool { - use self::Tag::*; - - match self { - Apply { args, .. } => args.iter().any(|arg| (&arg.value).is_multiline()), - Tag::SpaceBefore(_, _) | Tag::SpaceAfter(_, _) => true, - Malformed(text) => text.chars().any(|c| c == '\n'), - } - } - - fn format_with_options<'buf>( - &self, - buf: &mut Buf<'buf>, - _parens: Parens, - _newlines: Newlines, - indent: u16, - ) { - let is_multiline = self.is_multiline(); - - match self { - Tag::Apply { name, args } => { - buf.indent(indent); - buf.push_str(name.value); - if is_multiline { - let arg_indent = indent + INDENT; - - for arg in *args { - buf.newline(); - arg.format_with_options(buf, Parens::InApply, Newlines::No, arg_indent); - } - } else { - for arg in *args { - buf.spaces(1); - arg.format_with_options(buf, Parens::InApply, Newlines::No, indent); - } - } - } - Tag::SpaceBefore(_, _) | Tag::SpaceAfter(_, _) => unreachable!(), - Tag::Malformed(raw) => { - buf.indent(indent); - buf.push_str(raw); - } - } - } -} - -impl<'a> Formattable for HasClause<'a> { - fn is_multiline(&self) -> bool { - self.var.value.is_multiline() || self.ability.is_multiline() - } - - fn format_with_options<'buf>( - &self, - buf: &mut Buf<'buf>, - parens: Parens, - newlines: Newlines, - indent: u16, - ) { - buf.push_str(self.var.value.extract_spaces().item); - buf.spaces(1); - buf.push_str("has"); - buf.spaces(1); - self.ability - .format_with_options(buf, parens, newlines, indent); - } -} - -impl<'a> Formattable for Derived<'a> { - fn is_multiline(&self) -> bool { - match self { - Derived::SpaceAfter(..) | Derived::SpaceBefore(..) => true, - Derived::Has(derived) => derived.is_multiline(), - } - } - - fn format_with_options<'buf>( - &self, - buf: &mut Buf<'buf>, - parens: Parens, - newlines: Newlines, - indent: u16, - ) { - match self { - Derived::Has(derived) => { - if newlines == Newlines::Yes { - buf.newline(); - buf.indent(indent); - } - buf.push_str("has"); - buf.spaces(1); - fmt_collection(buf, indent, Braces::Square, *derived, newlines); - } - Derived::SpaceBefore(derived, spaces) => { - buf.newline(); - buf.indent(indent); - fmt_comments_only(buf, spaces.iter(), NewlineAt::Bottom, indent); - derived.format_with_options(buf, parens, Newlines::No, indent) - } - Derived::SpaceAfter(derived, spaces) => { - derived.format_with_options(buf, parens, newlines, indent); - fmt_comments_only(buf, spaces.iter(), NewlineAt::Bottom, indent); - } - } - } -} diff --git a/compiler/fmt/src/collection.rs b/compiler/fmt/src/collection.rs deleted file mode 100644 index f68c25c032..0000000000 --- a/compiler/fmt/src/collection.rs +++ /dev/null @@ -1,110 +0,0 @@ -use roc_parse::ast::{Collection, ExtractSpaces}; - -use crate::{ - annotation::{Formattable, Newlines}, - spaces::{count_leading_newlines, fmt_comments_only, NewlineAt, INDENT}, - Buf, -}; - -#[derive(PartialEq, Eq, Clone, Copy, Debug)] -pub enum Braces { - Square, - Curly, -} - -pub fn fmt_collection<'a, 'buf, T: ExtractSpaces<'a> + Formattable>( - buf: &mut Buf<'buf>, - indent: u16, - braces: Braces, - items: Collection<'a, T>, - newline: Newlines, -) where - >::Item: Formattable, -{ - let start = match braces { - Braces::Curly => '{', - Braces::Square => '[', - }; - - let end = match braces { - Braces::Curly => '}', - Braces::Square => ']', - }; - - if items.is_multiline() { - let braces_indent = indent; - let item_indent = braces_indent + INDENT; - if newline == Newlines::Yes { - buf.newline(); - } - buf.indent(braces_indent); - buf.push(start); - - for (index, item) in items.iter().enumerate() { - let item = item.extract_spaces(); - let is_first_item = index == 0; - - buf.newline(); - - if !item.before.is_empty() { - let is_only_newlines = item.before.iter().all(|s| s.is_newline()); - - if !is_first_item - && !is_only_newlines - && count_leading_newlines(item.before.iter()) > 1 - { - buf.newline(); - } - - fmt_comments_only(buf, item.before.iter(), NewlineAt::Bottom, item_indent); - - if !is_only_newlines && count_leading_newlines(item.before.iter().rev()) > 0 { - buf.newline(); - } - } - - item.item.format(buf, item_indent); - - buf.push(','); - - if !item.after.is_empty() { - fmt_comments_only(buf, item.after.iter(), NewlineAt::Top, item_indent); - } - } - - if count_leading_newlines(items.final_comments().iter()) > 1 { - buf.newline(); - } - - fmt_comments_only( - buf, - items.final_comments().iter(), - NewlineAt::Top, - item_indent, - ); - buf.newline(); - buf.indent(braces_indent); - } else { - // is_multiline == false - // there is no comment to add - buf.indent(indent); - buf.push(start); - let mut iter = items.iter().enumerate().peekable(); - while let Some((index, item)) = iter.next() { - if braces == Braces::Curly || index != 0 { - buf.spaces(1); - } - - item.format(buf, indent); - if iter.peek().is_some() { - buf.push(','); - } - } - - if !items.is_empty() && braces == Braces::Curly { - buf.spaces(1); - } - } - - buf.push(end); -} diff --git a/compiler/fmt/src/def.rs b/compiler/fmt/src/def.rs deleted file mode 100644 index 02fcbc1394..0000000000 --- a/compiler/fmt/src/def.rs +++ /dev/null @@ -1,425 +0,0 @@ -use crate::annotation::{Formattable, Newlines, Parens}; -use crate::pattern::fmt_pattern; -use crate::spaces::{fmt_spaces, INDENT}; -use crate::Buf; -use roc_parse::ast::{ - AbilityMember, Def, Defs, Expr, ExtractSpaces, Pattern, TypeAnnotation, TypeDef, TypeHeader, - ValueDef, -}; -use roc_region::all::Loc; - -/// A Located formattable value is also formattable - -impl<'a> Formattable for Defs<'a> { - fn is_multiline(&self) -> bool { - !self.tags.is_empty() - } - - fn format_with_options<'buf>( - &self, - buf: &mut Buf<'buf>, - _parens: Parens, - _newlines: Newlines, - indent: u16, - ) { - for (index, def) in self.defs().enumerate() { - let spaces_before = &self.spaces[self.space_before[index].indices()]; - let spaces_after = &self.spaces[self.space_after[index].indices()]; - - fmt_spaces(buf, spaces_before.iter(), indent); - - match def { - Ok(type_def) => type_def.format(buf, indent), - Err(value_def) => value_def.format(buf, indent), - } - - fmt_spaces(buf, spaces_after.iter(), indent); - } - } -} - -impl<'a> Formattable for TypeDef<'a> { - fn is_multiline(&self) -> bool { - use roc_parse::ast::TypeDef::*; - - match self { - Alias { ann, .. } => ann.is_multiline(), - Opaque { typ, .. } => typ.is_multiline(), - Ability { members, .. } => members.iter().any(|d| d.is_multiline()), - } - } - - fn format_with_options<'buf>( - &self, - buf: &mut Buf<'buf>, - _parens: Parens, - _newlines: Newlines, - indent: u16, - ) { - use roc_parse::ast::TypeDef::*; - - match self { - Alias { - header: TypeHeader { name, vars }, - ann, - } => { - buf.indent(indent); - buf.push_str(name.value); - - for var in *vars { - buf.spaces(1); - fmt_pattern(buf, &var.value, indent, Parens::NotNeeded); - } - - buf.push_str(" :"); - buf.spaces(1); - - ann.format(buf, indent + INDENT) - } - Opaque { - header: TypeHeader { name, vars }, - typ: ann, - derived, - } => { - buf.indent(indent); - buf.push_str(name.value); - - for var in *vars { - buf.spaces(1); - fmt_pattern(buf, &var.value, indent, Parens::NotNeeded); - } - - buf.push_str(" :="); - buf.spaces(1); - - let ann_is_where_clause = - matches!(ann.extract_spaces().item, TypeAnnotation::Where(..)); - - let ann_has_spaces_before = matches!(&ann.value, TypeAnnotation::SpaceBefore(..)); - - // Always put the has-derived clause on a newline if it is itself multiline, or - // the annotation has a where-has clause. - let derived_multiline = if let Some(derived) = derived { - !derived.value.is_empty() && (derived.is_multiline() || ann_is_where_clause) - } else { - false - }; - - let make_multiline = ann.is_multiline() || derived_multiline; - - // If the annotation has spaces before, a newline will already be printed. - if make_multiline && !ann_has_spaces_before { - buf.newline(); - buf.indent(indent + INDENT); - } - - ann.format(buf, indent + INDENT); - - if let Some(derived) = derived { - if !make_multiline { - buf.spaces(1); - } - - derived.format_with_options( - buf, - Parens::NotNeeded, - Newlines::from_bool(make_multiline), - indent + INDENT, - ); - } - } - Ability { - header: TypeHeader { name, vars }, - loc_has: _, - members, - } => { - buf.indent(indent); - buf.push_str(name.value); - for var in *vars { - buf.spaces(1); - fmt_pattern(buf, &var.value, indent, Parens::NotNeeded); - } - - buf.push_str(" has"); - - if !self.is_multiline() { - debug_assert_eq!(members.len(), 1); - buf.push_str(" "); - members[0].format(buf, indent + INDENT); - } else { - for demand in members.iter() { - buf.newline(); - buf.indent(indent + INDENT); - demand.format(buf, indent + INDENT); - } - } - } - } - } -} - -impl<'a> Formattable for ValueDef<'a> { - fn is_multiline(&self) -> bool { - use roc_parse::ast::ValueDef::*; - - match self { - Annotation(loc_pattern, loc_annotation) => { - loc_pattern.is_multiline() || loc_annotation.is_multiline() - } - Body(loc_pattern, loc_expr) => loc_pattern.is_multiline() || loc_expr.is_multiline(), - AnnotatedBody { .. } => true, - Expect(loc_expr) => loc_expr.is_multiline(), - } - } - - fn format_with_options<'buf>( - &self, - buf: &mut Buf<'buf>, - _parens: Parens, - _newlines: Newlines, - indent: u16, - ) { - use roc_parse::ast::ValueDef::*; - match self { - Annotation(loc_pattern, loc_annotation) => { - loc_pattern.format(buf, indent); - - if loc_annotation.is_multiline() { - buf.push_str(" :"); - - let should_outdent = match loc_annotation.value { - TypeAnnotation::SpaceBefore(sub_def, spaces) => match sub_def { - TypeAnnotation::Record { .. } | TypeAnnotation::TagUnion { .. } => { - let is_only_newlines = spaces.iter().all(|s| s.is_newline()); - is_only_newlines && sub_def.is_multiline() - } - _ => false, - }, - TypeAnnotation::Record { .. } | TypeAnnotation::TagUnion { .. } => true, - _ => false, - }; - - if should_outdent { - buf.spaces(1); - match loc_annotation.value { - TypeAnnotation::SpaceBefore(sub_def, _) => { - sub_def.format_with_options( - buf, - Parens::NotNeeded, - Newlines::No, - indent, - ); - } - _ => { - loc_annotation.format_with_options( - buf, - Parens::NotNeeded, - Newlines::No, - indent, - ); - } - } - } else { - loc_annotation.format_with_options( - buf, - Parens::NotNeeded, - Newlines::Yes, - indent + INDENT, - ); - } - } else { - buf.spaces(1); - buf.push_str(":"); - buf.spaces(1); - loc_annotation.format_with_options( - buf, - Parens::NotNeeded, - Newlines::No, - indent, - ); - } - } - Body(loc_pattern, loc_expr) => { - fmt_body(buf, &loc_pattern.value, &loc_expr.value, indent); - } - Expect(condition) => fmt_expect(buf, condition, self.is_multiline(), indent), - AnnotatedBody { - ann_pattern, - ann_type, - comment, - body_pattern, - body_expr, - } => { - let is_type_multiline = ann_type.is_multiline(); - let is_type_function = matches!( - ann_type.value, - TypeAnnotation::Function(..) - | TypeAnnotation::SpaceBefore(TypeAnnotation::Function(..), ..) - | TypeAnnotation::SpaceAfter(TypeAnnotation::Function(..), ..) - ); - - let next_indent = if is_type_multiline { - indent + INDENT - } else { - indent - }; - - ann_pattern.format(buf, indent); - buf.push_str(" :"); - - if is_type_multiline && is_type_function { - ann_type.format_with_options( - buf, - Parens::NotNeeded, - Newlines::Yes, - next_indent, - ); - } else { - buf.spaces(1); - ann_type.format(buf, indent); - } - - if let Some(comment_str) = comment { - buf.push_str(" #"); - buf.spaces(1); - buf.push_str(comment_str.trim()); - } - - buf.newline(); - fmt_body(buf, &body_pattern.value, &body_expr.value, indent); - } - } - } -} - -impl<'a> Formattable for Def<'a> { - fn is_multiline(&self) -> bool { - use roc_parse::ast::Def::*; - - match self { - Type(def) => def.is_multiline(), - Value(def) => def.is_multiline(), - SpaceBefore(sub_def, spaces) | SpaceAfter(sub_def, spaces) => { - spaces.iter().any(|s| s.is_comment()) || sub_def.is_multiline() - } - NotYetImplemented(s) => todo!("{}", s), - } - } - - fn format_with_options<'buf>( - &self, - buf: &mut Buf<'buf>, - parens: Parens, - newlines: Newlines, - indent: u16, - ) { - use roc_parse::ast::Def::*; - - match self { - Type(def) => def.format_with_options(buf, parens, newlines, indent), - Value(def) => def.format_with_options(buf, parens, newlines, indent), - - SpaceBefore(sub_def, spaces) => { - fmt_spaces(buf, spaces.iter(), indent); - sub_def.format(buf, indent); - } - SpaceAfter(sub_def, spaces) => { - sub_def.format(buf, indent); - fmt_spaces(buf, spaces.iter(), indent); - } - NotYetImplemented(s) => todo!("{}", s), - } - } -} - -fn fmt_expect<'a, 'buf>( - buf: &mut Buf<'buf>, - condition: &'a Loc>, - is_multiline: bool, - indent: u16, -) { - let return_indent = if is_multiline { - indent + INDENT - } else { - indent - }; - - buf.push_str("expect"); - condition.format(buf, return_indent); -} - -pub fn fmt_def<'a, 'buf>(buf: &mut Buf<'buf>, def: &Def<'a>, indent: u16) { - def.format(buf, indent); -} - -pub fn fmt_value_def<'a, 'buf>( - buf: &mut Buf<'buf>, - def: &roc_parse::ast::ValueDef<'a>, - indent: u16, -) { - def.format(buf, indent); -} - -pub fn fmt_type_def<'a, 'buf>(buf: &mut Buf<'buf>, def: &roc_parse::ast::TypeDef<'a>, indent: u16) { - def.format(buf, indent); -} - -pub fn fmt_toplevel_defs<'a, 'buf>(buf: &mut Buf<'buf>, defs: &Defs<'a>, indent: u16) { - defs.format(buf, indent); -} - -pub fn fmt_body<'a, 'buf>( - buf: &mut Buf<'buf>, - pattern: &'a Pattern<'a>, - body: &'a Expr<'a>, - indent: u16, -) { - pattern.format_with_options(buf, Parens::InApply, Newlines::No, indent); - buf.push_str(" ="); - if body.is_multiline() { - match body { - Expr::SpaceBefore(sub_def, spaces) => { - let should_outdent = match sub_def { - Expr::Record { .. } | Expr::List { .. } => { - let is_only_newlines = spaces.iter().all(|s| s.is_newline()); - is_only_newlines && sub_def.is_multiline() - } - _ => false, - }; - - if should_outdent { - buf.spaces(1); - sub_def.format_with_options(buf, Parens::NotNeeded, Newlines::Yes, indent); - } else { - body.format_with_options( - buf, - Parens::NotNeeded, - Newlines::Yes, - indent + INDENT, - ); - } - } - _ => { - buf.spaces(1); - body.format_with_options(buf, Parens::NotNeeded, Newlines::Yes, indent); - } - } - } else { - buf.spaces(1); - body.format_with_options(buf, Parens::NotNeeded, Newlines::Yes, indent); - } -} - -impl<'a> Formattable for AbilityMember<'a> { - fn is_multiline(&self) -> bool { - self.name.value.is_multiline() || self.typ.is_multiline() - } - - fn format<'buf>(&self, buf: &mut Buf<'buf>, indent: u16) { - buf.push_str(self.name.value.extract_spaces().item); - buf.spaces(1); - buf.push(':'); - buf.spaces(1); - self.typ.value.format(buf, indent + INDENT); - } -} diff --git a/compiler/fmt/src/expr.rs b/compiler/fmt/src/expr.rs deleted file mode 100644 index d8befe4d2f..0000000000 --- a/compiler/fmt/src/expr.rs +++ /dev/null @@ -1,1306 +0,0 @@ -use crate::annotation::{Formattable, Newlines, Parens}; -use crate::collection::{fmt_collection, Braces}; -use crate::def::fmt_def; -use crate::pattern::fmt_pattern; -use crate::spaces::{count_leading_newlines, fmt_comments_only, fmt_spaces, NewlineAt, INDENT}; -use crate::Buf; -use roc_module::called_via::{self, BinOp}; -use roc_parse::ast::{ - AssignedField, Base, Collection, CommentOrNewline, Expr, ExtractSpaces, Pattern, WhenBranch, -}; -use roc_parse::ast::{StrLiteral, StrSegment}; -use roc_region::all::Loc; - -impl<'a> Formattable for Expr<'a> { - fn is_multiline(&self) -> bool { - use roc_parse::ast::Expr::*; - // TODO cache these answers using a Map, so - // we don't have to traverse subexpressions repeatedly - - match self { - // Return whether these spaces contain any Newlines - SpaceBefore(_sub_expr, spaces) | SpaceAfter(_sub_expr, spaces) => { - debug_assert!(!spaces.is_empty()); - - // "spaces" always contain either a newline or comment, and comments have newlines - true - } - - // These expressions never have newlines - Float(..) - | Num(..) - | NonBase10Int { .. } - | SingleQuote(_) - | Access(_, _) - | AccessorFunction(_) - | Var { .. } - | Underscore { .. } - | MalformedIdent(_, _) - | MalformedClosure - | Tag(_) - | OpaqueRef(_) => false, - - // These expressions always have newlines - Defs(_, _) | When(_, _) => true, - - List(items) => items.iter().any(|loc_expr| loc_expr.is_multiline()), - - Str(literal) => { - use roc_parse::ast::StrLiteral::*; - - match literal { - PlainLine(_) | Line(_) => { - // If this had any newlines, it'd have parsed as Block. - false - } - Block(lines) => { - // Block strings don't *have* to be multiline! - lines.len() > 1 - } - } - } - Apply(loc_expr, args, _) => { - loc_expr.is_multiline() || args.iter().any(|loc_arg| loc_arg.is_multiline()) - } - - Expect(condition, continuation) => { - condition.is_multiline() || continuation.is_multiline() - } - - If(branches, final_else) => { - final_else.is_multiline() - || branches - .iter() - .any(|(c, t)| c.is_multiline() || t.is_multiline()) - } - - BinOps(lefts, loc_right) => { - lefts.iter().any(|(loc_expr, _)| loc_expr.is_multiline()) - || loc_right.is_multiline() - } - - UnaryOp(loc_subexpr, _) - | PrecedenceConflict(roc_parse::ast::PrecedenceConflict { - expr: loc_subexpr, .. - }) => loc_subexpr.is_multiline(), - - ParensAround(subexpr) => subexpr.is_multiline(), - - Closure(loc_patterns, loc_body) => { - // check the body first because it's more likely to be multiline - loc_body.is_multiline() - || loc_patterns - .iter() - .any(|loc_pattern| loc_pattern.is_multiline()) - } - Backpassing(loc_patterns, loc_body, loc_ret) => { - // check the body first because it's more likely to be multiline - loc_body.is_multiline() - || loc_ret.is_multiline() - || loc_patterns - .iter() - .any(|loc_pattern| loc_pattern.is_multiline()) - } - - Record(fields) => fields.iter().any(|loc_field| loc_field.is_multiline()), - RecordUpdate { fields, .. } => fields.iter().any(|loc_field| loc_field.is_multiline()), - } - } - - fn format_with_options<'buf>( - &self, - buf: &mut Buf<'buf>, - parens: Parens, - newlines: Newlines, - indent: u16, - ) { - use self::Expr::*; - - let apply_needs_parens = parens == Parens::InApply; - - match self { - SpaceBefore(sub_expr, spaces) => { - format_spaces(buf, spaces, newlines, indent); - sub_expr.format_with_options(buf, parens, newlines, indent); - } - SpaceAfter(sub_expr, spaces) => { - sub_expr.format_with_options(buf, parens, newlines, indent); - format_spaces(buf, spaces, newlines, indent); - } - ParensAround(sub_expr) => { - if parens == Parens::NotNeeded && !sub_expr_requests_parens(sub_expr) { - sub_expr.format_with_options(buf, Parens::NotNeeded, Newlines::Yes, indent); - } else { - let should_add_newlines = match sub_expr { - Expr::Closure(..) - | Expr::SpaceBefore(..) - | Expr::SpaceAfter(Closure(..), ..) => false, - _ => sub_expr.is_multiline(), - }; - - buf.indent(indent); - buf.push('('); - if should_add_newlines { - buf.newline(); - } - - let next_indent = if starts_with_newline(sub_expr) || should_add_newlines { - match sub_expr { - Expr::Closure(..) | Expr::SpaceAfter(Closure(..), ..) => indent, - _ => indent + INDENT, - } - } else { - indent - }; - - sub_expr.format_with_options( - buf, - Parens::NotNeeded, - Newlines::Yes, - next_indent, - ); - - if !matches!(sub_expr, Expr::SpaceAfter(..)) && should_add_newlines { - buf.newline(); - } - buf.indent(indent); - buf.push(')'); - } - } - Str(literal) => { - fmt_str_literal(buf, *literal, indent); - } - Var { module_name, ident } => { - buf.indent(indent); - if !module_name.is_empty() { - buf.push_str(module_name); - buf.push('.'); - } - - buf.push_str(ident); - } - Underscore(name) => { - buf.indent(indent); - buf.push('_'); - buf.push_str(name); - } - Apply(loc_expr, loc_args, _) => { - buf.indent(indent); - if apply_needs_parens && !loc_args.is_empty() { - buf.push('('); - } - - loc_expr.format_with_options(buf, Parens::InApply, Newlines::Yes, indent); - - let multiline_args = loc_args.iter().any(|loc_arg| loc_arg.is_multiline()); - - let mut found_multiline_expr = false; - let mut iter = loc_args.iter().peekable(); - - while let Some(loc_arg) = iter.next() { - if iter.peek().is_none() { - found_multiline_expr = match loc_arg.value { - SpaceBefore(sub_expr, spaces) => match sub_expr { - Record { .. } | List { .. } => { - let is_only_newlines = spaces.iter().all(|s| s.is_newline()); - is_only_newlines - && !found_multiline_expr - && sub_expr.is_multiline() - } - _ => false, - }, - Record { .. } | List { .. } | Closure { .. } => { - !found_multiline_expr && loc_arg.is_multiline() - } - _ => false, - } - } else { - found_multiline_expr = loc_arg.is_multiline(); - } - } - - let should_outdent_last_arg = found_multiline_expr; - - if multiline_args && !should_outdent_last_arg { - let arg_indent = indent + INDENT; - - for loc_arg in loc_args.iter() { - buf.newline(); - loc_arg.format_with_options(buf, Parens::InApply, Newlines::No, arg_indent); - } - } else if multiline_args && should_outdent_last_arg { - let mut iter = loc_args.iter().peekable(); - while let Some(loc_arg) = iter.next() { - buf.spaces(1); - - if iter.peek().is_none() { - match loc_arg.value { - SpaceBefore(sub_expr, _) => { - sub_expr.format_with_options( - buf, - Parens::InApply, - Newlines::Yes, - indent, - ); - } - _ => { - loc_arg.format_with_options( - buf, - Parens::InApply, - Newlines::Yes, - indent, - ); - } - } - } else { - loc_arg.format_with_options( - buf, - Parens::InApply, - Newlines::Yes, - indent, - ); - } - } - } else { - for loc_arg in loc_args.iter() { - buf.spaces(1); - loc_arg.format_with_options(buf, Parens::InApply, Newlines::Yes, indent); - } - } - - if apply_needs_parens && !loc_args.is_empty() { - buf.push(')'); - } - } - &Num(string) => { - buf.indent(indent); - buf.push_str(string); - } - &Float(string) => { - buf.indent(indent); - buf.push_str(string); - } - Tag(string) | OpaqueRef(string) => { - buf.indent(indent); - buf.push_str(string) - } - SingleQuote(string) => { - buf.push('\''); - buf.push_str(string); - buf.push('\''); - } - &NonBase10Int { - base, - string, - is_negative, - } => { - buf.indent(indent); - if is_negative { - buf.push('-'); - } - - match base { - Base::Hex => buf.push_str("0x"), - Base::Octal => buf.push_str("0o"), - Base::Binary => buf.push_str("0b"), - Base::Decimal => { /* nothing */ } - } - - buf.push_str(string); - } - Record(fields) => { - fmt_record(buf, None, *fields, indent); - } - RecordUpdate { update, fields } => { - fmt_record(buf, Some(*update), *fields, indent); - } - Closure(loc_patterns, loc_ret) => { - fmt_closure(buf, loc_patterns, loc_ret, indent); - } - Backpassing(loc_patterns, loc_body, loc_ret) => { - fmt_backpassing(buf, loc_patterns, loc_body, loc_ret, indent); - } - Defs(defs, ret) => { - // It should theoretically be impossible to *parse* an empty defs list. - // (Canonicalization can remove defs later, but that hasn't happened yet!) - debug_assert!(!defs.is_empty()); - - for loc_def in defs.iter() { - fmt_def(buf, &loc_def.value, indent); - } - - match &ret.value { - SpaceBefore(sub_expr, spaces) => { - let empty_line_before_return = empty_line_before_expr(&ret.value); - let has_inline_comment = with_inline_comment(&ret.value); - - if has_inline_comment { - buf.spaces(1); - format_spaces(buf, spaces, newlines, indent); - - if !empty_line_before_return { - buf.newline(); - } - - sub_expr.format_with_options( - buf, - Parens::NotNeeded, - Newlines::Yes, - indent, - ); - } else { - if !empty_line_before_return { - buf.newline(); - } - - ret.format_with_options(buf, Parens::NotNeeded, Newlines::Yes, indent); - } - } - _ => { - // Even if there were no defs, which theoretically should never happen, - // still print the return value. - ret.format_with_options(buf, Parens::NotNeeded, Newlines::Yes, indent); - } - } - } - Expect(condition, continuation) => { - fmt_expect(buf, condition, continuation, self.is_multiline(), indent); - } - If(branches, final_else) => { - fmt_if(buf, branches, final_else, self.is_multiline(), indent); - } - When(loc_condition, branches) => fmt_when(buf, loc_condition, branches, indent), - List(items) => fmt_collection(buf, indent, Braces::Square, *items, Newlines::No), - BinOps(lefts, right) => fmt_bin_ops(buf, lefts, right, false, parens, indent), - UnaryOp(sub_expr, unary_op) => { - buf.indent(indent); - match &unary_op.value { - called_via::UnaryOp::Negate => { - buf.push('-'); - } - called_via::UnaryOp::Not => { - buf.push('!'); - } - } - - sub_expr.format_with_options(buf, Parens::InApply, newlines, indent); - } - AccessorFunction(key) => { - buf.indent(indent); - buf.push('.'); - buf.push_str(key); - } - Access(expr, key) => { - expr.format_with_options(buf, Parens::InApply, Newlines::Yes, indent); - buf.push('.'); - buf.push_str(key); - } - MalformedIdent(_, _) => {} - MalformedClosure => {} - PrecedenceConflict { .. } => {} - } - } -} - -fn starts_with_newline(expr: &Expr) -> bool { - use roc_parse::ast::Expr::*; - - match expr { - SpaceBefore(_, comment_or_newline) => { - if !comment_or_newline.is_empty() { - // safe because we check the length before - comment_or_newline.get(0).unwrap().is_newline() - } else { - false - } - } - SpaceAfter(_, comment_or_newline) => { - if !(**comment_or_newline).is_empty() { - // safe because we check the length before - comment_or_newline.get(0).unwrap().is_newline() - } else { - false - } - } - _ => false, - } -} - -fn format_str_segment<'a, 'buf>(seg: &StrSegment<'a>, buf: &mut Buf<'buf>, indent: u16) { - use StrSegment::*; - - match seg { - Plaintext(string) => { - buf.push_str_allow_spaces(string); - } - Unicode(loc_str) => { - buf.push_str("\\u("); - buf.push_str(loc_str.value); // e.g. "00A0" in "\u(00A0)" - buf.push(')'); - } - EscapedChar(escaped) => { - buf.push('\\'); - buf.push(escaped.to_parsed_char()); - } - Interpolated(loc_expr) => { - buf.push_str("\\("); - // e.g. (name) in "Hi, \(name)!" - loc_expr.value.format_with_options( - buf, - Parens::NotNeeded, // We already printed parens! - Newlines::No, // Interpolations can never have newlines - indent, - ); - buf.push(')'); - } - } -} - -fn push_op(buf: &mut Buf, op: BinOp) { - match op { - called_via::BinOp::Caret => buf.push('^'), - called_via::BinOp::Star => buf.push('*'), - called_via::BinOp::Slash => buf.push('/'), - called_via::BinOp::DoubleSlash => buf.push_str("//"), - called_via::BinOp::Percent => buf.push('%'), - called_via::BinOp::Plus => buf.push('+'), - called_via::BinOp::Minus => buf.push('-'), - called_via::BinOp::Equals => buf.push_str("=="), - called_via::BinOp::NotEquals => buf.push_str("!="), - called_via::BinOp::LessThan => buf.push('<'), - called_via::BinOp::GreaterThan => buf.push('>'), - called_via::BinOp::LessThanOrEq => buf.push_str("<="), - called_via::BinOp::GreaterThanOrEq => buf.push_str(">="), - called_via::BinOp::And => buf.push_str("&&"), - called_via::BinOp::Or => buf.push_str("||"), - called_via::BinOp::Pizza => buf.push_str("|>"), - called_via::BinOp::Assignment => unreachable!(), - called_via::BinOp::IsAliasType => unreachable!(), - called_via::BinOp::IsOpaqueType => unreachable!(), - called_via::BinOp::Backpassing => unreachable!(), - } -} - -pub fn fmt_str_literal<'buf>(buf: &mut Buf<'buf>, literal: StrLiteral, indent: u16) { - use roc_parse::ast::StrLiteral::*; - - buf.indent(indent); - buf.push('"'); - match literal { - PlainLine(string) => { - buf.push_str_allow_spaces(string); - } - Line(segments) => { - for seg in segments.iter() { - format_str_segment(seg, buf, 0) - } - } - Block(lines) => { - buf.push_str("\"\""); - - if lines.len() > 1 { - // Since we have multiple lines, format this with - // the `"""` symbols on their own lines, and the - buf.newline(); - - for segments in lines.iter() { - for seg in segments.iter() { - format_str_segment(seg, buf, indent); - } - - buf.newline(); - } - } else { - // This is a single-line block string, for example: - // - // """Whee, "quotes" inside quotes!""" - - // This loop will run either 0 or 1 times. - for segments in lines.iter() { - for seg in segments.iter() { - format_str_segment(seg, buf, indent); - } - - // Don't print a newline here, because we either - // just printed 1 or 0 lines. - } - } - - buf.push_str("\"\""); - } - } - buf.push('"'); -} - -fn fmt_bin_ops<'a, 'buf>( - buf: &mut Buf<'buf>, - lefts: &'a [(Loc>, Loc)], - loc_right_side: &'a Loc>, - part_of_multi_line_bin_ops: bool, - apply_needs_parens: Parens, - indent: u16, -) { - let is_multiline = part_of_multi_line_bin_ops - || (&loc_right_side.value).is_multiline() - || lefts.iter().any(|(expr, _)| expr.value.is_multiline()); - - let mut curr_indent = indent; - - for (loc_left_side, loc_bin_op) in lefts { - let bin_op = loc_bin_op.value; - - loc_left_side.format_with_options(buf, apply_needs_parens, Newlines::No, curr_indent); - - if is_multiline { - buf.newline(); - curr_indent = indent + INDENT; - buf.indent(curr_indent); - } else { - buf.spaces(1); - } - - push_op(buf, bin_op); - - buf.spaces(1); - } - - let next_indent = if is_multiline { - indent + INDENT - } else { - indent - }; - - loc_right_side.format_with_options(buf, apply_needs_parens, Newlines::Yes, next_indent); -} - -fn format_spaces<'a, 'buf>( - buf: &mut Buf<'buf>, - spaces: &[CommentOrNewline<'a>], - newlines: Newlines, - indent: u16, -) { - let format_newlines = newlines == Newlines::Yes; - - if format_newlines { - fmt_spaces(buf, spaces.iter(), indent); - } else { - fmt_comments_only(buf, spaces.iter(), NewlineAt::Bottom, indent); - } -} - -fn with_inline_comment<'a>(expr: &'a Expr<'a>) -> bool { - use roc_parse::ast::Expr::*; - - match expr { - SpaceBefore(_, spaces) => match spaces.iter().next() { - Some(CommentOrNewline::LineComment(_)) => true, - Some(_) => false, - None => false, - }, - _ => false, - } -} - -fn empty_line_before_expr<'a>(expr: &'a Expr<'a>) -> bool { - use roc_parse::ast::Expr::*; - - match expr { - SpaceBefore(_, spaces) => { - let mut has_at_least_one_newline = false; - - for comment_or_newline in spaces.iter() { - match comment_or_newline { - CommentOrNewline::Newline => { - if has_at_least_one_newline { - return true; - } else { - has_at_least_one_newline = true; - } - } - CommentOrNewline::LineComment(_) | CommentOrNewline::DocComment(_) => {} - } - } - - false - } - - _ => false, - } -} - -fn is_when_patterns_multiline(when_branch: &WhenBranch) -> bool { - let patterns = when_branch.patterns; - let (first_pattern, rest) = patterns.split_first().unwrap(); - - let is_multiline_patterns = if let Some((last_pattern, inner_patterns)) = rest.split_last() { - !first_pattern.value.extract_spaces().after.is_empty() - || !last_pattern.value.extract_spaces().before.is_empty() - || inner_patterns.iter().any(|p| { - let spaces = p.value.extract_spaces(); - !spaces.before.is_empty() || !spaces.after.is_empty() - }) - } else { - false - }; - - is_multiline_patterns -} - -fn fmt_when<'a, 'buf>( - buf: &mut Buf<'buf>, - loc_condition: &'a Loc>, - branches: &[&'a WhenBranch<'a>], - indent: u16, -) { - let is_multiline_condition = loc_condition.is_multiline(); - buf.indent(indent); - buf.push_str( - "\ - when", - ); - if is_multiline_condition { - let condition_indent = indent + INDENT; - - match &loc_condition.value { - Expr::SpaceBefore(expr_below, spaces_above_expr) => { - fmt_comments_only( - buf, - spaces_above_expr.iter(), - NewlineAt::Top, - condition_indent, - ); - buf.newline(); - match &expr_below { - Expr::SpaceAfter(expr_above, spaces_below_expr) => { - expr_above.format(buf, condition_indent); - fmt_comments_only( - buf, - spaces_below_expr.iter(), - NewlineAt::Top, - condition_indent, - ); - buf.newline(); - } - _ => { - expr_below.format(buf, condition_indent); - } - } - } - _ => { - buf.newline(); - loc_condition.format(buf, condition_indent); - buf.newline(); - } - } - buf.indent(indent); - } else { - buf.spaces(1); - loc_condition.format(buf, indent); - buf.spaces(1); - } - buf.push_str("is"); - buf.newline(); - - let mut it = branches.iter().peekable(); - while let Some(branch) = it.next() { - let expr = &branch.value; - let patterns = &branch.patterns; - let is_multiline_expr = expr.is_multiline(); - let is_multiline_patterns = is_when_patterns_multiline(branch); - - for (index, pattern) in patterns.iter().enumerate() { - if index != 0 { - if is_multiline_patterns { - buf.newline(); - buf.indent(indent + INDENT); - } - - buf.push_str(" |"); - buf.spaces(1); - } - - fmt_pattern(buf, &pattern.value, indent + INDENT, Parens::NotNeeded); - } - - if let Some(guard_expr) = &branch.guard { - buf.push_str(" if"); - buf.spaces(1); - guard_expr.format_with_options(buf, Parens::NotNeeded, Newlines::Yes, indent + INDENT); - } - - buf.push_str(" ->"); - - if is_multiline_expr { - buf.newline(); - } else { - buf.spaces(1); - } - - match expr.value { - Expr::SpaceBefore(nested, spaces) => { - fmt_comments_only(buf, spaces.iter(), NewlineAt::Bottom, indent + (INDENT * 2)); - nested.format_with_options( - buf, - Parens::NotNeeded, - Newlines::Yes, - indent + 2 * INDENT, - ); - } - _ => { - expr.format_with_options( - buf, - Parens::NotNeeded, - Newlines::Yes, - indent + 2 * INDENT, - ); - } - } - - if it.peek().is_some() { - buf.newline(); - } - } -} - -fn fmt_expect<'a, 'buf>( - buf: &mut Buf<'buf>, - condition: &'a Loc>, - continuation: &'a Loc>, - is_multiline: bool, - indent: u16, -) { - let return_indent = if is_multiline { - indent + INDENT - } else { - indent - }; - - buf.push_str("expect"); - condition.format(buf, return_indent); - buf.push('\n'); - continuation.format(buf, return_indent); -} - -fn fmt_if<'a, 'buf>( - buf: &mut Buf<'buf>, - branches: &'a [(Loc>, Loc>)], - final_else: &'a Loc>, - is_multiline: bool, - indent: u16, -) { - // let is_multiline_then = loc_then.is_multiline(); - // let is_multiline_else = final_else.is_multiline(); - // let is_multiline_condition = loc_condition.is_multiline(); - // let is_multiline = is_multiline_then || is_multiline_else || is_multiline_condition; - - let return_indent = if is_multiline { - indent + INDENT - } else { - indent - }; - - for (i, (loc_condition, loc_then)) in branches.iter().enumerate() { - let is_multiline_condition = loc_condition.is_multiline(); - - buf.indent(indent); - - if i > 0 { - buf.push_str("else"); - buf.spaces(1); - } - - buf.push_str("if"); - - if is_multiline_condition { - match &loc_condition.value { - Expr::SpaceBefore(expr_below, spaces_above_expr) => { - fmt_comments_only(buf, spaces_above_expr.iter(), NewlineAt::Top, return_indent); - buf.newline(); - - match &expr_below { - Expr::SpaceAfter(expr_above, spaces_below_expr) => { - expr_above.format(buf, return_indent); - fmt_comments_only( - buf, - spaces_below_expr.iter(), - NewlineAt::Top, - return_indent, - ); - buf.newline(); - } - - _ => { - expr_below.format(buf, return_indent); - } - } - } - - Expr::SpaceAfter(expr_above, spaces_below_expr) => { - buf.newline(); - expr_above.format(buf, return_indent); - fmt_comments_only(buf, spaces_below_expr.iter(), NewlineAt::Top, return_indent); - buf.newline(); - } - - _ => { - buf.newline(); - loc_condition.format(buf, return_indent); - buf.newline(); - } - } - buf.indent(indent); - } else { - buf.spaces(1); - loc_condition.format_with_options(buf, Parens::NotNeeded, Newlines::Yes, indent); - buf.spaces(1); - } - - buf.push_str("then"); - - if is_multiline { - match &loc_then.value { - Expr::SpaceBefore(expr_below, spaces_below) => { - // we want exactly one newline, user-inserted extra newlines are ignored. - buf.newline(); - fmt_comments_only(buf, spaces_below.iter(), NewlineAt::Bottom, return_indent); - - match &expr_below { - Expr::SpaceAfter(expr_above, spaces_above) => { - expr_above.format(buf, return_indent); - - fmt_comments_only( - buf, - spaces_above.iter(), - NewlineAt::Top, - return_indent, - ); - buf.newline(); - } - - _ => { - expr_below.format(buf, return_indent); - } - } - } - _ => { - buf.newline(); - loc_then.format(buf, return_indent); - buf.newline(); - } - } - } else { - buf.push_str(""); - buf.spaces(1); - loc_then.format(buf, return_indent); - } - } - - buf.indent(indent); - if is_multiline { - buf.push_str("else"); - buf.newline(); - } else { - buf.push_str(" else"); - buf.spaces(1); - } - - final_else.format(buf, return_indent); -} - -fn fmt_closure<'a, 'buf>( - buf: &mut Buf<'buf>, - loc_patterns: &'a [Loc>], - loc_ret: &'a Loc>, - indent: u16, -) { - use self::Expr::*; - - buf.indent(indent); - buf.push('\\'); - - let arguments_are_multiline = loc_patterns - .iter() - .any(|loc_pattern| loc_pattern.is_multiline()); - - // If the arguments are multiline, go down a line and indent. - let indent = if arguments_are_multiline { - indent + INDENT - } else { - indent - }; - - let mut it = loc_patterns.iter().peekable(); - - while let Some(loc_pattern) = it.next() { - loc_pattern.format(buf, indent); - - if it.peek().is_some() { - buf.indent(indent); - if arguments_are_multiline { - buf.push(','); - buf.newline(); - } else { - buf.push_str(","); - buf.spaces(1); - } - } - } - - if arguments_are_multiline { - buf.newline(); - buf.indent(indent); - } else { - buf.spaces(1); - } - - buf.push_str("->"); - - let is_multiline = (&loc_ret.value).is_multiline(); - - // If the body is multiline, go down a line and indent. - let body_indent = if is_multiline { - indent + INDENT - } else { - indent - }; - - // the body of the Closure can be on the same line, or - // on a new line. If it's on the same line, insert a space. - - match &loc_ret.value { - SpaceBefore(_, _) => { - // the body starts with (first comment and then) a newline - // do nothing - } - _ => { - // add a space after the `->` - buf.spaces(1); - } - }; - - if is_multiline { - match &loc_ret.value { - SpaceBefore(sub_expr, spaces) => { - let should_outdent = match sub_expr { - Record { .. } | List { .. } => { - let is_only_newlines = spaces.iter().all(|s| s.is_newline()); - is_only_newlines && sub_expr.is_multiline() - } - _ => false, - }; - - if should_outdent { - buf.spaces(1); - sub_expr.format_with_options(buf, Parens::NotNeeded, Newlines::Yes, indent); - } else { - loc_ret.format_with_options(buf, Parens::NotNeeded, Newlines::Yes, body_indent); - } - } - Record { .. } | List { .. } => { - loc_ret.format_with_options(buf, Parens::NotNeeded, Newlines::Yes, indent); - } - _ => { - loc_ret.format_with_options(buf, Parens::NotNeeded, Newlines::Yes, body_indent); - } - } - } else { - loc_ret.format_with_options(buf, Parens::NotNeeded, Newlines::Yes, body_indent); - } -} - -fn fmt_backpassing<'a, 'buf>( - buf: &mut Buf<'buf>, - loc_patterns: &'a [Loc>], - loc_body: &'a Loc>, - loc_ret: &'a Loc>, - indent: u16, -) { - use self::Expr::*; - - let arguments_are_multiline = loc_patterns - .iter() - .any(|loc_pattern| loc_pattern.is_multiline()); - - // If the arguments are multiline, go down a line and indent. - let indent = if arguments_are_multiline { - indent + INDENT - } else { - indent - }; - - let pattern_needs_parens = loc_patterns - .iter() - .any(|p| pattern_needs_parens_when_backpassing(&p.value)); - - if pattern_needs_parens { - buf.indent(indent); - buf.push('('); - } - - let mut it = loc_patterns.iter().peekable(); - - while let Some(loc_pattern) = it.next() { - loc_pattern.format(buf, indent); - - if it.peek().is_some() { - if arguments_are_multiline { - buf.push(','); - buf.newline(); - } else { - buf.push_str(","); - buf.spaces(1); - } - } - } - - if pattern_needs_parens { - buf.push(')'); - } - - if arguments_are_multiline { - buf.newline(); - buf.indent(indent); - } else { - buf.spaces(1); - } - - buf.push_str("<-"); - - let is_multiline = (&loc_ret.value).is_multiline(); - - // If the body is multiline, go down a line and indent. - let body_indent = if is_multiline { - indent + INDENT - } else { - indent - }; - - // the body of the Backpass can be on the same line, or - // on a new line. If it's on the same line, insert a space. - - match &loc_body.value { - SpaceBefore(_, _) => { - // the body starts with (first comment and then) a newline - // do nothing - } - _ => { - // add a space after the `<-` - buf.spaces(1); - } - }; - - loc_body.format_with_options(buf, Parens::NotNeeded, Newlines::Yes, body_indent); - loc_ret.format_with_options(buf, Parens::NotNeeded, Newlines::Yes, indent); -} - -fn pattern_needs_parens_when_backpassing(pat: &Pattern) -> bool { - match pat { - Pattern::Apply(_, _) => true, - Pattern::SpaceBefore(a, _) | Pattern::SpaceAfter(a, _) => { - pattern_needs_parens_when_backpassing(a) - } - _ => false, - } -} - -fn fmt_record<'a, 'buf>( - buf: &mut Buf<'buf>, - update: Option<&'a Loc>>, - fields: Collection<'a, Loc>>>, - indent: u16, -) { - let loc_fields = fields.items; - let final_comments = fields.final_comments(); - buf.indent(indent); - if loc_fields.is_empty() && final_comments.iter().all(|c| c.is_newline()) { - buf.push_str("{}"); - } else { - buf.push('{'); - - match update { - None => {} - // We are presuming this to be a Var() - // If it wasnt a Var() we would not have made - // it this far. For example "{ 4 & hello = 9 }" - // doesnt make sense. - Some(record_var) => { - buf.spaces(1); - record_var.format(buf, indent); - buf.push_str(" &"); - } - } - - let is_multiline = loc_fields.iter().any(|loc_field| loc_field.is_multiline()) - || !final_comments.is_empty(); - - if is_multiline { - let field_indent = indent + INDENT; - for (index, field) in loc_fields.iter().enumerate() { - // comma addition is handled by the `format_field_multiline` function - // since we can have stuff like: - // { x # comment - // , y - // } - // In this case, we have to move the comma before the comment. - - let is_first_item = index == 0; - if let AssignedField::SpaceBefore(_sub_field, spaces) = &field.value { - let is_only_newlines = spaces.iter().all(|s| s.is_newline()); - if !is_first_item - && !is_only_newlines - && count_leading_newlines(spaces.iter()) > 1 - { - buf.newline(); - } - - fmt_comments_only(buf, spaces.iter(), NewlineAt::Top, field_indent); - - if !is_only_newlines && count_leading_newlines(spaces.iter().rev()) > 0 { - buf.newline(); - } - } - - format_field_multiline(buf, &field.value, field_indent, ""); - } - - if count_leading_newlines(final_comments.iter()) > 1 { - buf.newline(); - } - - fmt_comments_only(buf, final_comments.iter(), NewlineAt::Top, field_indent); - - buf.newline(); - } else { - // is_multiline == false - buf.spaces(1); - let field_indent = indent; - let mut iter = loc_fields.iter().peekable(); - while let Some(field) = iter.next() { - field.format_with_options(buf, Parens::NotNeeded, Newlines::No, field_indent); - - if iter.peek().is_some() { - buf.push_str(","); - buf.spaces(1); - } - } - buf.spaces(1); - // if we are here, that means that `final_comments` is empty, thus we don't have - // to add a comment. Anyway, it is not possible to have a single line record with - // a comment in it. - }; - - // closes the initial bracket - buf.indent(indent); - buf.push('}'); - } -} - -fn format_field_multiline<'a, 'buf, T>( - buf: &mut Buf<'buf>, - field: &AssignedField<'a, T>, - indent: u16, - separator_prefix: &str, -) where - T: Formattable, -{ - use self::AssignedField::*; - match field { - RequiredValue(name, spaces, ann) => { - buf.newline(); - buf.indent(indent); - buf.push_str(name.value); - - if !spaces.is_empty() { - fmt_spaces(buf, spaces.iter(), indent); - } - - buf.push_str(separator_prefix); - buf.push_str(":"); - buf.spaces(1); - ann.value.format(buf, indent); - buf.push(','); - } - OptionalValue(name, spaces, ann) => { - buf.newline(); - buf.indent(indent); - buf.push_str(name.value); - - if !spaces.is_empty() { - fmt_spaces(buf, spaces.iter(), indent); - } - - buf.push_str(separator_prefix); - buf.push_str("?"); - buf.spaces(1); - ann.value.format(buf, indent); - buf.push(','); - } - LabelOnly(name) => { - buf.newline(); - buf.indent(indent); - buf.push_str(name.value); - buf.push(','); - } - AssignedField::SpaceBefore(sub_field, _spaces) => { - // We have something like that: - // ``` - // # comment - // field, - // ``` - // we'd like to preserve this - - format_field_multiline(buf, sub_field, indent, separator_prefix); - } - AssignedField::SpaceAfter(sub_field, spaces) => { - // We have something like that: - // ``` - // field # comment - // , otherfield - // ``` - // we'd like to transform it into: - // ``` - // field, - // # comment - // otherfield - // ``` - format_field_multiline(buf, sub_field, indent, separator_prefix); - fmt_comments_only(buf, spaces.iter(), NewlineAt::Top, indent); - } - Malformed(raw) => { - buf.push_str(raw); - } - } -} - -fn sub_expr_requests_parens(expr: &Expr<'_>) -> bool { - match expr { - Expr::BinOps(left_side, _) => { - left_side - .iter() - .any(|(_, loc_bin_op)| match loc_bin_op.value { - BinOp::Caret - | BinOp::Star - | BinOp::Slash - | BinOp::DoubleSlash - | BinOp::Percent - | BinOp::Plus - | BinOp::Minus - | BinOp::Equals - | BinOp::NotEquals - | BinOp::LessThan - | BinOp::GreaterThan - | BinOp::LessThanOrEq - | BinOp::GreaterThanOrEq - | BinOp::And - | BinOp::Or => true, - BinOp::Pizza - | BinOp::Assignment - | BinOp::IsAliasType - | BinOp::IsOpaqueType - | BinOp::Backpassing => false, - }) - } - Expr::If(_, _) => true, - _ => false, - } -} diff --git a/compiler/fmt/src/lib.rs b/compiler/fmt/src/lib.rs deleted file mode 100644 index 1e15f605b8..0000000000 --- a/compiler/fmt/src/lib.rs +++ /dev/null @@ -1,212 +0,0 @@ -#![warn(clippy::dbg_macro)] -// See github.com/rtfeldman/roc/issues/800 for discussion of the large_enum_variant check. -#![allow(clippy::large_enum_variant)] -pub mod annotation; -pub mod collection; -pub mod def; -pub mod expr; -pub mod module; -pub mod pattern; -pub mod spaces; - -use bumpalo::{collections::String, Bump}; -use roc_parse::ast::Module; - -#[derive(Debug)] -pub struct Ast<'a> { - pub module: Module<'a>, - pub defs: roc_parse::ast::Defs<'a>, -} - -#[derive(Debug)] -pub struct Buf<'a> { - text: String<'a>, - spaces_to_flush: usize, - beginning_of_line: bool, -} - -impl<'a> Buf<'a> { - pub fn new_in(arena: &'a Bump) -> Buf<'a> { - Buf { - text: String::new_in(arena), - spaces_to_flush: 0, - beginning_of_line: true, - } - } - - pub fn as_str(&'a self) -> &'a str { - self.text.as_str() - } - - pub fn into_bump_str(self) -> &'a str { - self.text.into_bump_str() - } - - pub fn indent(&mut self, indent: u16) { - if self.beginning_of_line { - for _ in 0..indent { - self.text.push(' '); - } - } - self.beginning_of_line = false; - } - - pub fn push(&mut self, ch: char) { - debug_assert!(!self.beginning_of_line); - debug_assert!(ch != '\n' && ch != ' '); - - self.flush_spaces(); - - self.text.push(ch); - } - - pub fn push_str_allow_spaces(&mut self, s: &str) { - debug_assert!(!self.beginning_of_line); - - self.flush_spaces(); - - self.text.push_str(s); - } - - pub fn push_str(&mut self, s: &str) { - debug_assert!(!self.beginning_of_line); - debug_assert!(!s.contains('\n') && !s.ends_with(' ')); - - if !s.is_empty() { - self.flush_spaces(); - } - - self.text.push_str(s); - } - - pub fn spaces(&mut self, count: usize) { - self.spaces_to_flush += count; - } - - pub fn newline(&mut self) { - self.spaces_to_flush = 0; - self.text.push('\n'); - self.beginning_of_line = true; - } - - fn flush_spaces(&mut self) { - if self.spaces_to_flush > 0 { - for _ in 0..self.spaces_to_flush { - self.text.push(' '); - } - self.spaces_to_flush = 0; - } - } - - /// Ensures the text ends in a newline with no whitespace preceding it. - pub fn fmt_end_of_file(&mut self) { - fmt_text_eof(&mut self.text) - } -} - -/// Ensures the text ends in a newline with no whitespace preceding it. -fn fmt_text_eof(text: &mut bumpalo::collections::String<'_>) { - let mut chars_rev = text.chars().rev(); - let mut last_whitespace = None; - let mut last_whitespace_index = text.len(); - - // Keep going until we either run out of characters or encounter one - // that isn't whitespace. - loop { - match chars_rev.next() { - Some(ch) if ch.is_whitespace() => { - last_whitespace = Some(ch); - last_whitespace_index -= 1; - } - _ => { - break; - } - } - } - - match last_whitespace { - Some('\n') => { - // There may have been more whitespace after this newline; remove it! - text.truncate(last_whitespace_index + '\n'.len_utf8()); - } - Some(_) => { - // There's some whitespace at the end of this file, but the first - // whitespace char after the last non-whitespace char isn't a newline. - // So replace that whitespace char (and everything after it) with a newline. - text.replace_range(last_whitespace_index.., "\n"); - } - None => { - debug_assert!(last_whitespace_index == text.len()); - debug_assert!(!text.ends_with(char::is_whitespace)); - - // This doesn't end in whitespace at all, so add a newline. - text.push('\n'); - } - } -} - -#[test] -fn eof_text_ends_with_newline() { - use bumpalo::{collections::String, Bump}; - - let arena = Bump::new(); - let input = "This should be a newline:\n"; - let mut text = String::from_str_in(input, &arena); - - fmt_text_eof(&mut text); - - // This should be unchanged! - assert_eq!(text.as_str(), input); -} - -#[test] -fn eof_text_ends_with_whitespace() { - use bumpalo::{collections::String, Bump}; - - let arena = Bump::new(); - let input = "This should be a newline: \t"; - let mut text = String::from_str_in(input, &arena); - - fmt_text_eof(&mut text); - - assert_eq!(text.as_str(), "This should be a newline:\n"); -} - -#[test] -fn eof_text_ends_with_whitespace_then_newline() { - use bumpalo::{collections::String, Bump}; - - let arena = Bump::new(); - let input = "This should be a newline: \n"; - let mut text = String::from_str_in(input, &arena); - - fmt_text_eof(&mut text); - - assert_eq!(text.as_str(), "This should be a newline:\n"); -} - -#[test] -fn eof_text_ends_with_no_whitespace() { - use bumpalo::{collections::String, Bump}; - - let arena = Bump::new(); - let input = "This should be a newline:"; - let mut text = String::from_str_in(input, &arena); - - fmt_text_eof(&mut text); - - assert_eq!(text.as_str(), "This should be a newline:\n"); -} - -#[test] -fn eof_text_is_empty() { - use bumpalo::{collections::String, Bump}; - - let arena = Bump::new(); - let input = ""; - let mut text = String::from_str_in(input, &arena); - - fmt_text_eof(&mut text); - - assert_eq!(text.as_str(), "\n"); -} diff --git a/compiler/fmt/src/module.rs b/compiler/fmt/src/module.rs deleted file mode 100644 index 3ee1db8b2c..0000000000 --- a/compiler/fmt/src/module.rs +++ /dev/null @@ -1,407 +0,0 @@ -use crate::annotation::{Formattable, Newlines}; -use crate::collection::{fmt_collection, Braces}; -use crate::expr::fmt_str_literal; -use crate::spaces::{fmt_default_spaces, fmt_spaces, INDENT}; -use crate::Buf; -use roc_parse::ast::{Collection, Module, Spaced}; -use roc_parse::header::{ - AppHeader, ExposedName, HostedHeader, ImportsEntry, InterfaceHeader, ModuleName, PackageEntry, - PackageName, PlatformHeader, PlatformRequires, To, TypedIdent, -}; -use roc_parse::ident::UppercaseIdent; -use roc_region::all::Loc; - -pub fn fmt_module<'a>(buf: &mut Buf<'_>, module: &'a Module<'a>) { - match module { - Module::Interface { header } => { - fmt_interface_header(buf, header); - } - Module::App { header } => { - fmt_app_header(buf, header); - } - Module::Platform { header } => { - fmt_platform_header(buf, header); - } - Module::Hosted { header } => { - fmt_hosted_header(buf, header); - } - } -} - -pub fn fmt_interface_header<'a, 'buf>(buf: &mut Buf<'buf>, header: &'a InterfaceHeader<'a>) { - let indent = INDENT; - - buf.indent(0); - buf.push_str("interface"); - - // module name - fmt_default_spaces(buf, header.after_interface_keyword, indent); - buf.push_str(header.name.value.as_str()); - - // exposes - fmt_default_spaces(buf, header.before_exposes, indent); - buf.indent(indent); - buf.push_str("exposes"); - fmt_default_spaces(buf, header.after_exposes, indent); - fmt_exposes(buf, header.exposes, indent); - - // imports - fmt_default_spaces(buf, header.before_imports, indent); - buf.indent(indent); - buf.push_str("imports"); - fmt_default_spaces(buf, header.after_imports, indent); - fmt_imports(buf, header.imports, indent); -} - -pub fn fmt_hosted_header<'a, 'buf>(buf: &mut Buf<'buf>, header: &'a HostedHeader<'a>) { - let indent = INDENT; - - buf.indent(0); - buf.push_str("hosted"); - - // module name - fmt_default_spaces(buf, header.after_hosted_keyword, indent); - buf.push_str(header.name.value.as_str()); - - // exposes - fmt_default_spaces(buf, header.before_exposes, indent); - buf.indent(indent); - buf.push_str("exposes"); - fmt_default_spaces(buf, header.after_exposes, indent); - fmt_exposes(buf, header.exposes, indent); - - // imports - fmt_default_spaces(buf, header.before_imports, indent); - buf.indent(indent); - buf.push_str("imports"); - fmt_default_spaces(buf, header.after_imports, indent); - fmt_imports(buf, header.imports, indent); - - // generates - fmt_default_spaces(buf, header.before_generates, indent); - buf.indent(indent); - buf.push_str("generates"); - fmt_default_spaces(buf, header.after_generates, indent); - buf.push_str(header.generates.into()); - - // with - fmt_default_spaces(buf, header.before_with, indent); - buf.indent(indent); - buf.push_str("with"); - fmt_default_spaces(buf, header.after_with, indent); - fmt_exposes(buf, header.generates_with, indent); -} - -pub fn fmt_app_header<'a, 'buf>(buf: &mut Buf<'buf>, header: &'a AppHeader<'a>) { - let indent = INDENT; - buf.indent(0); - buf.push_str("app"); - - fmt_default_spaces(buf, header.after_app_keyword, indent); - fmt_str_literal(buf, header.name.value, indent); - - // packages - fmt_default_spaces(buf, header.before_packages, indent); - buf.indent(indent); - buf.push_str("packages"); - fmt_default_spaces(buf, header.after_packages, indent); - fmt_packages(buf, header.packages, indent); - - // imports - fmt_default_spaces(buf, header.before_imports, indent); - buf.indent(indent); - buf.push_str("imports"); - fmt_default_spaces(buf, header.after_imports, indent); - fmt_imports(buf, header.imports, indent); - - // provides - fmt_default_spaces(buf, header.before_provides, indent); - buf.indent(indent); - buf.push_str("provides"); - fmt_default_spaces(buf, header.after_provides, indent); - fmt_provides(buf, header.provides, header.provides_types, indent); - fmt_default_spaces(buf, header.before_to, indent); - buf.indent(indent); - buf.push_str("to"); - fmt_default_spaces(buf, header.after_to, indent); - fmt_to(buf, header.to.value, indent); -} - -pub fn fmt_platform_header<'a, 'buf>(buf: &mut Buf<'buf>, header: &'a PlatformHeader<'a>) { - let indent = INDENT; - - buf.indent(0); - buf.push_str("platform"); - - fmt_default_spaces(buf, header.after_platform_keyword, indent); - fmt_package_name(buf, header.name.value, indent); - - // requires - fmt_default_spaces(buf, header.before_requires, indent); - buf.indent(indent); - buf.push_str("requires"); - fmt_default_spaces(buf, header.after_requires, indent); - fmt_requires(buf, &header.requires, indent); - - // exposes - fmt_default_spaces(buf, header.before_exposes, indent); - buf.indent(indent); - buf.push_str("exposes"); - fmt_default_spaces(buf, header.after_exposes, indent); - fmt_exposes(buf, header.exposes, indent); - - // packages - fmt_default_spaces(buf, header.before_packages, indent); - buf.indent(indent); - buf.push_str("packages"); - fmt_default_spaces(buf, header.after_packages, indent); - fmt_packages(buf, header.packages, indent); - - // imports - fmt_default_spaces(buf, header.before_imports, indent); - buf.indent(indent); - buf.push_str("imports"); - fmt_default_spaces(buf, header.after_imports, indent); - fmt_imports(buf, header.imports, indent); - - // provides - fmt_default_spaces(buf, header.before_provides, indent); - buf.indent(indent); - buf.push_str("provides"); - fmt_default_spaces(buf, header.after_provides, indent); - fmt_provides(buf, header.provides, None, indent); -} - -fn fmt_requires<'a, 'buf>(buf: &mut Buf<'buf>, requires: &PlatformRequires<'a>, indent: u16) { - fmt_collection(buf, indent, Braces::Curly, requires.rigids, Newlines::No); - - buf.push_str(" {"); - buf.spaces(1); - requires.signature.value.format(buf, indent); - buf.push_str(" }"); -} - -impl<'a> Formattable for TypedIdent<'a> { - fn is_multiline(&self) -> bool { - false - } - - fn format<'buf>(&self, buf: &mut Buf<'buf>, indent: u16) { - buf.indent(indent); - buf.push_str(self.ident.value); - fmt_default_spaces(buf, self.spaces_before_colon, indent); - buf.push_str(":"); - buf.spaces(1); - self.ann.value.format(buf, indent); - } -} - -fn fmt_package_name<'buf>(buf: &mut Buf<'buf>, name: PackageName, _indent: u16) { - buf.push('"'); - buf.push_str_allow_spaces(name.0); - buf.push('"'); -} - -impl<'a, T: Formattable> Formattable for Spaced<'a, T> { - fn is_multiline(&self) -> bool { - use Spaced::*; - - match self { - Item(formattable) => formattable.is_multiline(), - SpaceBefore(formattable, spaces) | SpaceAfter(formattable, spaces) => { - !spaces.is_empty() || formattable.is_multiline() - } - } - } - - fn format_with_options<'buf>( - &self, - buf: &mut Buf<'buf>, - parens: crate::annotation::Parens, - newlines: Newlines, - indent: u16, - ) { - match self { - Spaced::Item(item) => { - item.format_with_options(buf, parens, newlines, indent); - } - Spaced::SpaceBefore(item, spaces) => { - fmt_spaces(buf, spaces.iter(), indent); - item.format_with_options(buf, parens, newlines, indent); - } - Spaced::SpaceAfter(item, spaces) => { - item.format_with_options(buf, parens, newlines, indent); - fmt_spaces(buf, spaces.iter(), indent); - } - } - } -} - -fn fmt_imports<'a, 'buf>( - buf: &mut Buf<'buf>, - loc_entries: Collection<'a, Loc>>>, - indent: u16, -) { - fmt_collection( - buf, - indent + INDENT, - Braces::Square, - loc_entries, - Newlines::No, - ) -} - -fn fmt_provides<'a, 'buf>( - buf: &mut Buf<'buf>, - loc_exposed_names: Collection<'a, Loc>>>, - loc_provided_types: Option>>>>, - indent: u16, -) { - fmt_collection(buf, indent, Braces::Square, loc_exposed_names, Newlines::No); - if let Some(loc_provided) = loc_provided_types { - fmt_default_spaces(buf, &[], indent); - fmt_collection( - buf, - indent + INDENT, - Braces::Curly, - loc_provided, - Newlines::No, - ); - } -} - -fn fmt_to<'buf>(buf: &mut Buf<'buf>, to: To, indent: u16) { - match to { - To::ExistingPackage(name) => { - buf.push_str(name); - } - To::NewPackage(package_name) => fmt_package_name(buf, package_name, indent), - } -} - -fn fmt_exposes<'buf, N: Formattable + Copy>( - buf: &mut Buf<'buf>, - loc_entries: Collection<'_, Loc>>, - indent: u16, -) { - fmt_collection( - buf, - indent + INDENT, - Braces::Square, - loc_entries, - Newlines::No, - ) -} - -pub trait FormatName { - fn format<'buf>(&self, buf: &mut Buf<'buf>); -} - -impl<'a> FormatName for &'a str { - fn format<'buf>(&self, buf: &mut Buf<'buf>) { - buf.push_str(self) - } -} - -impl<'a> FormatName for ModuleName<'a> { - fn format<'buf>(&self, buf: &mut Buf<'buf>) { - buf.push_str(self.as_str()); - } -} - -impl<'a> Formattable for ModuleName<'a> { - fn is_multiline(&self) -> bool { - false - } - - fn format<'buf>(&self, buf: &mut Buf<'buf>, _indent: u16) { - buf.push_str(self.as_str()); - } -} - -impl<'a> Formattable for ExposedName<'a> { - fn is_multiline(&self) -> bool { - false - } - - fn format<'buf>(&self, buf: &mut Buf<'buf>, indent: u16) { - buf.indent(indent); - buf.push_str(self.as_str()); - } -} - -impl<'a> FormatName for ExposedName<'a> { - fn format<'buf>(&self, buf: &mut Buf<'buf>) { - buf.push_str(self.as_str()); - } -} - -fn fmt_packages<'a, 'buf>( - buf: &mut Buf<'buf>, - loc_entries: Collection<'a, Loc>>>, - indent: u16, -) { - fmt_collection(buf, indent, Braces::Curly, loc_entries, Newlines::No) -} - -impl<'a> Formattable for PackageEntry<'a> { - fn is_multiline(&self) -> bool { - false - } - - fn format<'buf>(&self, buf: &mut Buf<'buf>, indent: u16) { - fmt_packages_entry(buf, self, indent); - } -} - -impl<'a> Formattable for ImportsEntry<'a> { - fn is_multiline(&self) -> bool { - false - } - - fn format<'buf>(&self, buf: &mut Buf<'buf>, indent: u16) { - fmt_imports_entry(buf, self, indent); - } -} -fn fmt_packages_entry<'a, 'buf>(buf: &mut Buf<'buf>, entry: &PackageEntry<'a>, indent: u16) { - buf.push_str(entry.shorthand); - buf.push(':'); - fmt_default_spaces(buf, entry.spaces_after_shorthand, indent); - fmt_package_name(buf, entry.package_name.value, indent); -} - -fn fmt_imports_entry<'a, 'buf>(buf: &mut Buf<'buf>, entry: &ImportsEntry<'a>, indent: u16) { - use roc_parse::header::ImportsEntry::*; - - buf.indent(indent); - - match entry { - Module(module, loc_exposes_entries) => { - buf.push_str(module.as_str()); - - if !loc_exposes_entries.is_empty() { - buf.push('.'); - - fmt_collection( - buf, - indent, - Braces::Curly, - *loc_exposes_entries, - Newlines::No, - ) - } - } - - Package(pkg, name, entries) => { - buf.push_str(pkg); - buf.push('.'); - buf.push_str(name.as_str()); - - if !entries.is_empty() { - buf.push('.'); - - fmt_collection(buf, indent, Braces::Curly, *entries, Newlines::No) - } - } - } -} diff --git a/compiler/fmt/src/pattern.rs b/compiler/fmt/src/pattern.rs deleted file mode 100644 index 4df623dd5c..0000000000 --- a/compiler/fmt/src/pattern.rs +++ /dev/null @@ -1,198 +0,0 @@ -use crate::annotation::{Formattable, Newlines, Parens}; -use crate::spaces::{fmt_comments_only, fmt_spaces, NewlineAt}; -use crate::Buf; -use roc_parse::ast::{Base, Pattern}; - -pub fn fmt_pattern<'a, 'buf>( - buf: &mut Buf<'buf>, - pattern: &'a Pattern<'a>, - indent: u16, - parens: Parens, -) { - pattern.format_with_options(buf, parens, Newlines::No, indent); -} - -impl<'a> Formattable for Pattern<'a> { - fn is_multiline(&self) -> bool { - // Theory: a pattern should only be multiline when it contains a comment - match self { - Pattern::SpaceBefore(_, spaces) | Pattern::SpaceAfter(_, spaces) => { - debug_assert!(!spaces.is_empty()); - - spaces.iter().any(|s| s.is_comment()) - } - - Pattern::RecordDestructure(fields) => fields.iter().any(|f| f.is_multiline()), - Pattern::RequiredField(_, subpattern) => subpattern.is_multiline(), - - Pattern::OptionalField(_, expr) => expr.is_multiline(), - - Pattern::Identifier(_) - | Pattern::Tag(_) - | Pattern::OpaqueRef(_) - | Pattern::Apply(_, _) - | Pattern::NumLiteral(..) - | Pattern::NonBase10Literal { .. } - | Pattern::FloatLiteral(..) - | Pattern::StrLiteral(_) - | Pattern::SingleQuote(_) - | Pattern::Underscore(_) - | Pattern::Malformed(_) - | Pattern::MalformedIdent(_, _) - | Pattern::QualifiedIdentifier { .. } => false, - } - } - - fn format_with_options<'buf>( - &self, - buf: &mut Buf<'buf>, - parens: Parens, - newlines: Newlines, - indent: u16, - ) { - use self::Pattern::*; - - match self { - Identifier(string) => { - buf.indent(indent); - buf.push_str(string) - } - Tag(name) | OpaqueRef(name) => { - buf.indent(indent); - buf.push_str(name); - } - Apply(loc_pattern, loc_arg_patterns) => { - buf.indent(indent); - // Sometimes, an Apply pattern needs parens around it. - // In particular when an Apply's argument is itself an Apply (> 0) arguments - let parens = !loc_arg_patterns.is_empty() && parens == Parens::InApply; - - if parens { - buf.push('('); - } - - loc_pattern.format_with_options(buf, Parens::InApply, Newlines::No, indent); - - for loc_arg in loc_arg_patterns.iter() { - buf.spaces(1); - loc_arg.format_with_options(buf, Parens::InApply, Newlines::No, indent); - } - - if parens { - buf.push(')'); - } - } - RecordDestructure(loc_patterns) => { - buf.indent(indent); - buf.push_str("{"); - - if !loc_patterns.is_empty() { - buf.spaces(1); - let mut it = loc_patterns.iter().peekable(); - while let Some(loc_pattern) = it.next() { - loc_pattern.format(buf, indent); - - if it.peek().is_some() { - buf.push_str(","); - buf.spaces(1); - } - } - buf.spaces(1); - } - - buf.push_str("}"); - } - - RequiredField(name, loc_pattern) => { - buf.indent(indent); - buf.push_str(name); - buf.push_str(":"); - buf.spaces(1); - loc_pattern.format(buf, indent); - } - - OptionalField(name, loc_pattern) => { - buf.indent(indent); - buf.push_str(name); - buf.push_str(" ?"); - buf.spaces(1); - loc_pattern.format(buf, indent); - } - - &NumLiteral(string) => { - buf.indent(indent); - buf.push_str(string); - } - &NonBase10Literal { - base, - string, - is_negative, - } => { - buf.indent(indent); - if is_negative { - buf.push('-'); - } - - match base { - Base::Hex => buf.push_str("0x"), - Base::Octal => buf.push_str("0o"), - Base::Binary => buf.push_str("0b"), - Base::Decimal => { /* nothing */ } - } - - buf.push_str(string); - } - &FloatLiteral(string) => { - buf.indent(indent); - buf.push_str(string); - } - StrLiteral(literal) => { - todo!("Format string literal: {:?}", literal); - } - SingleQuote(string) => { - buf.push('\''); - buf.push_str(string); - buf.push('\''); - } - Underscore(name) => { - buf.indent(indent); - buf.push('_'); - buf.push_str(name); - } - - // Space - SpaceBefore(sub_pattern, spaces) => { - if !sub_pattern.is_multiline() { - fmt_comments_only(buf, spaces.iter(), NewlineAt::Bottom, indent) - } else { - fmt_spaces(buf, spaces.iter(), indent); - } - sub_pattern.format_with_options(buf, parens, newlines, indent); - } - SpaceAfter(sub_pattern, spaces) => { - sub_pattern.format_with_options(buf, parens, newlines, indent); - // if only_comments { - if !sub_pattern.is_multiline() { - fmt_comments_only(buf, spaces.iter(), NewlineAt::Bottom, indent) - } else { - fmt_spaces(buf, spaces.iter(), indent); - } - } - - // Malformed - Malformed(string) | MalformedIdent(string, _) => { - buf.indent(indent); - buf.push_str(string); - } - QualifiedIdentifier { module_name, ident } => { - buf.indent(indent); - if !module_name.is_empty() { - buf.push_str(module_name); - buf.push('.'); - } - - buf.push_str(ident); - } - } - } -} diff --git a/compiler/fmt/src/spaces.rs b/compiler/fmt/src/spaces.rs deleted file mode 100644 index 5752777dd3..0000000000 --- a/compiler/fmt/src/spaces.rs +++ /dev/null @@ -1,772 +0,0 @@ -use bumpalo::collections::vec::Vec; -use bumpalo::Bump; -use roc_module::called_via::{BinOp, UnaryOp}; -use roc_parse::{ - ast::{ - AbilityMember, AssignedField, Collection, CommentOrNewline, Def, Derived, Expr, Has, - HasClause, Module, Pattern, Spaced, StrLiteral, StrSegment, Tag, TypeAnnotation, TypeDef, - TypeHeader, ValueDef, WhenBranch, - }, - header::{ - AppHeader, ExposedName, HostedHeader, ImportsEntry, InterfaceHeader, ModuleName, - PackageEntry, PackageName, PlatformHeader, PlatformRequires, To, TypedIdent, - }, - ident::UppercaseIdent, -}; -use roc_region::all::{Loc, Region}; - -use crate::{Ast, Buf}; - -/// The number of spaces to indent. -pub const INDENT: u16 = 4; - -pub fn fmt_default_spaces<'a, 'buf>( - buf: &mut Buf<'buf>, - spaces: &[CommentOrNewline<'a>], - indent: u16, -) { - if spaces.is_empty() { - buf.spaces(1); - } else { - fmt_spaces(buf, spaces.iter(), indent); - } -} - -pub fn fmt_spaces<'a, 'buf, I>(buf: &mut Buf<'buf>, spaces: I, indent: u16) -where - I: Iterator>, -{ - use self::CommentOrNewline::*; - - // Only ever print two newlines back to back. - // (Two newlines renders as one blank line.) - let mut consecutive_newlines = 0; - - let mut encountered_comment = false; - - for space in spaces { - match space { - Newline => { - if !encountered_comment && (consecutive_newlines < 2) { - buf.newline(); - - // Don't bother incrementing it if we're already over the limit. - // There's no upside, and it might eventually overflow, - consecutive_newlines += 1; - } - } - LineComment(comment) => { - buf.indent(indent); - fmt_comment(buf, comment); - buf.newline(); - - encountered_comment = true; - } - DocComment(docs) => { - buf.indent(indent); - fmt_docs(buf, docs); - buf.newline(); - - encountered_comment = true; - } - } - } -} - -#[derive(Eq, PartialEq, Debug)] -pub enum NewlineAt { - Top, - Bottom, - Both, - None, -} - -/// Like format_spaces, but remove newlines and keep only comments. -/// The `new_line_at` argument describes how new lines should be inserted -/// at the beginning or at the end of the block -/// in the case of there is some comment in the `spaces` argument. -pub fn fmt_comments_only<'a, 'buf, I>( - buf: &mut Buf<'buf>, - spaces: I, - new_line_at: NewlineAt, - indent: u16, -) where - I: Iterator>, -{ - use self::CommentOrNewline::*; - use NewlineAt::*; - - let mut comment_seen = false; - - for space in spaces { - match space { - Newline => {} - LineComment(comment) => { - if comment_seen || new_line_at == Top || new_line_at == Both { - buf.newline(); - } - buf.indent(indent); - fmt_comment(buf, comment); - comment_seen = true; - } - DocComment(docs) => { - if comment_seen || new_line_at == Top || new_line_at == Both { - buf.newline(); - } - buf.indent(indent); - fmt_docs(buf, docs); - comment_seen = true; - } - } - } - if comment_seen && (new_line_at == Bottom || new_line_at == Both) { - buf.newline(); - } -} - -fn fmt_comment<'buf>(buf: &mut Buf<'buf>, comment: &str) { - buf.push('#'); - if !comment.starts_with(' ') { - buf.spaces(1); - } - buf.push_str(comment.trim_end()); -} - -pub fn count_leading_newlines<'a, I>(data: I) -> u16 -where - I: Iterator>, -{ - let mut count = 0; - let mut allow_counting = false; - - for (index, val) in data.enumerate() { - let is_first = index == 0; - let is_newline = matches!(val, CommentOrNewline::Newline); - - if is_first && is_newline { - allow_counting = true - } - - if is_newline && allow_counting { - count += 1; - } else { - break; - } - } - - count -} - -fn fmt_docs<'buf>(buf: &mut Buf<'buf>, docs: &str) { - buf.push_str("##"); - if !docs.is_empty() { - buf.spaces(1); - } - buf.push_str(docs.trim_end()); -} - -/// RemoveSpaces normalizes the ast to something that we _expect_ to be invariant under formatting. -/// -/// Currently this consists of: -/// * Removing newlines -/// * Removing comments -/// * Removing parens in Exprs -/// -/// Long term, we actuall want this transform to preserve comments (so we can assert they're maintained by formatting) -/// - but there are currently several bugs where they're _not_ preserved. -/// TODO: ensure formatting retains comments -pub trait RemoveSpaces<'a> { - fn remove_spaces(&self, arena: &'a Bump) -> Self; -} - -impl<'a> RemoveSpaces<'a> for Ast<'a> { - fn remove_spaces(&self, arena: &'a Bump) -> Self { - Ast { - module: self.module.remove_spaces(arena), - defs: { - let mut defs = self.defs.clone(); - - for type_def in defs.type_defs.iter_mut() { - *type_def = type_def.remove_spaces(arena); - } - - for value_def in defs.value_defs.iter_mut() { - *value_def = value_def.remove_spaces(arena); - } - - for region_def in defs.regions.iter_mut() { - *region_def = region_def.remove_spaces(arena); - } - - defs - }, - } - } -} - -impl<'a> RemoveSpaces<'a> for Module<'a> { - fn remove_spaces(&self, arena: &'a Bump) -> Self { - match self { - Module::Interface { header } => Module::Interface { - header: InterfaceHeader { - name: header.name.remove_spaces(arena), - exposes: header.exposes.remove_spaces(arena), - imports: header.imports.remove_spaces(arena), - before_header: &[], - after_interface_keyword: &[], - before_exposes: &[], - after_exposes: &[], - before_imports: &[], - after_imports: &[], - }, - }, - Module::App { header } => Module::App { - header: AppHeader { - name: header.name.remove_spaces(arena), - packages: header.packages.remove_spaces(arena), - imports: header.imports.remove_spaces(arena), - provides: header.provides.remove_spaces(arena), - provides_types: header.provides_types.map(|ts| ts.remove_spaces(arena)), - to: header.to.remove_spaces(arena), - before_header: &[], - after_app_keyword: &[], - before_packages: &[], - after_packages: &[], - before_imports: &[], - after_imports: &[], - before_provides: &[], - after_provides: &[], - before_to: &[], - after_to: &[], - }, - }, - Module::Platform { header } => Module::Platform { - header: PlatformHeader { - name: header.name.remove_spaces(arena), - requires: header.requires.remove_spaces(arena), - exposes: header.exposes.remove_spaces(arena), - packages: header.packages.remove_spaces(arena), - imports: header.imports.remove_spaces(arena), - provides: header.provides.remove_spaces(arena), - before_header: &[], - after_platform_keyword: &[], - before_requires: &[], - after_requires: &[], - before_exposes: &[], - after_exposes: &[], - before_packages: &[], - after_packages: &[], - before_imports: &[], - after_imports: &[], - before_provides: &[], - after_provides: &[], - }, - }, - Module::Hosted { header } => Module::Hosted { - header: HostedHeader { - name: header.name.remove_spaces(arena), - exposes: header.exposes.remove_spaces(arena), - imports: header.imports.remove_spaces(arena), - generates: header.generates.remove_spaces(arena), - generates_with: header.generates_with.remove_spaces(arena), - before_header: &[], - after_hosted_keyword: &[], - before_exposes: &[], - after_exposes: &[], - before_imports: &[], - after_imports: &[], - before_generates: &[], - after_generates: &[], - before_with: &[], - after_with: &[], - }, - }, - } - } -} - -impl<'a> RemoveSpaces<'a> for Region { - fn remove_spaces(&self, _arena: &'a Bump) -> Self { - Region::zero() - } -} - -impl<'a> RemoveSpaces<'a> for &'a str { - fn remove_spaces(&self, _arena: &'a Bump) -> Self { - self - } -} - -impl<'a, T: RemoveSpaces<'a> + Copy> RemoveSpaces<'a> for Spaced<'a, T> { - fn remove_spaces(&self, arena: &'a Bump) -> Self { - match *self { - Spaced::Item(a) => Spaced::Item(a.remove_spaces(arena)), - Spaced::SpaceBefore(a, _) => a.remove_spaces(arena), - Spaced::SpaceAfter(a, _) => a.remove_spaces(arena), - } - } -} - -impl<'a> RemoveSpaces<'a> for ExposedName<'a> { - fn remove_spaces(&self, _arena: &'a Bump) -> Self { - *self - } -} - -impl<'a> RemoveSpaces<'a> for ModuleName<'a> { - fn remove_spaces(&self, _arena: &'a Bump) -> Self { - *self - } -} - -impl<'a> RemoveSpaces<'a> for PackageName<'a> { - fn remove_spaces(&self, _arena: &'a Bump) -> Self { - *self - } -} - -impl<'a> RemoveSpaces<'a> for To<'a> { - fn remove_spaces(&self, arena: &'a Bump) -> Self { - match *self { - To::ExistingPackage(a) => To::ExistingPackage(a), - To::NewPackage(a) => To::NewPackage(a.remove_spaces(arena)), - } - } -} - -impl<'a> RemoveSpaces<'a> for TypedIdent<'a> { - fn remove_spaces(&self, arena: &'a Bump) -> Self { - TypedIdent { - ident: self.ident.remove_spaces(arena), - spaces_before_colon: &[], - ann: self.ann.remove_spaces(arena), - } - } -} - -impl<'a> RemoveSpaces<'a> for PlatformRequires<'a> { - fn remove_spaces(&self, arena: &'a Bump) -> Self { - PlatformRequires { - rigids: self.rigids.remove_spaces(arena), - signature: self.signature.remove_spaces(arena), - } - } -} - -impl<'a> RemoveSpaces<'a> for UppercaseIdent<'a> { - fn remove_spaces(&self, _arena: &'a Bump) -> Self { - *self - } -} - -impl<'a> RemoveSpaces<'a> for PackageEntry<'a> { - fn remove_spaces(&self, arena: &'a Bump) -> Self { - PackageEntry { - shorthand: self.shorthand, - spaces_after_shorthand: &[], - package_name: self.package_name.remove_spaces(arena), - } - } -} - -impl<'a> RemoveSpaces<'a> for ImportsEntry<'a> { - fn remove_spaces(&self, arena: &'a Bump) -> Self { - match *self { - ImportsEntry::Module(a, b) => ImportsEntry::Module(a, b.remove_spaces(arena)), - ImportsEntry::Package(a, b, c) => ImportsEntry::Package(a, b, c.remove_spaces(arena)), - } - } -} - -impl<'a, T: RemoveSpaces<'a>> RemoveSpaces<'a> for Option { - fn remove_spaces(&self, arena: &'a Bump) -> Self { - self.as_ref().map(|a| a.remove_spaces(arena)) - } -} - -impl<'a, T: RemoveSpaces<'a> + std::fmt::Debug> RemoveSpaces<'a> for Loc { - fn remove_spaces(&self, arena: &'a Bump) -> Self { - let res = self.value.remove_spaces(arena); - Loc::at(Region::zero(), res) - } -} - -impl<'a, A: RemoveSpaces<'a>, B: RemoveSpaces<'a>> RemoveSpaces<'a> for (A, B) { - fn remove_spaces(&self, arena: &'a Bump) -> Self { - (self.0.remove_spaces(arena), self.1.remove_spaces(arena)) - } -} - -impl<'a, T: RemoveSpaces<'a>> RemoveSpaces<'a> for Collection<'a, T> { - fn remove_spaces(&self, arena: &'a Bump) -> Self { - let mut items = Vec::with_capacity_in(self.items.len(), arena); - for item in self.items { - items.push(item.remove_spaces(arena)); - } - Collection::with_items(items.into_bump_slice()) - } -} - -impl<'a, T: RemoveSpaces<'a> + std::fmt::Debug> RemoveSpaces<'a> for &'a [T] { - fn remove_spaces(&self, arena: &'a Bump) -> Self { - let mut items = Vec::with_capacity_in(self.len(), arena); - for item in *self { - let res = item.remove_spaces(arena); - items.push(res); - } - items.into_bump_slice() - } -} - -impl<'a> RemoveSpaces<'a> for UnaryOp { - fn remove_spaces(&self, _arena: &'a Bump) -> Self { - *self - } -} - -impl<'a> RemoveSpaces<'a> for BinOp { - fn remove_spaces(&self, _arena: &'a Bump) -> Self { - *self - } -} - -impl<'a, T: RemoveSpaces<'a>> RemoveSpaces<'a> for &'a T { - fn remove_spaces(&self, arena: &'a Bump) -> Self { - arena.alloc((*self).remove_spaces(arena)) - } -} - -impl<'a> RemoveSpaces<'a> for TypeDef<'a> { - fn remove_spaces(&self, arena: &'a Bump) -> Self { - use TypeDef::*; - - match *self { - Alias { - header: TypeHeader { name, vars }, - ann, - } => Alias { - header: TypeHeader { - name: name.remove_spaces(arena), - vars: vars.remove_spaces(arena), - }, - ann: ann.remove_spaces(arena), - }, - Opaque { - header: TypeHeader { name, vars }, - typ, - derived, - } => Opaque { - header: TypeHeader { - name: name.remove_spaces(arena), - vars: vars.remove_spaces(arena), - }, - typ: typ.remove_spaces(arena), - derived: derived.remove_spaces(arena), - }, - Ability { - header: TypeHeader { name, vars }, - loc_has, - members, - } => Ability { - header: TypeHeader { - name: name.remove_spaces(arena), - vars: vars.remove_spaces(arena), - }, - loc_has: loc_has.remove_spaces(arena), - members: members.remove_spaces(arena), - }, - } - } -} - -impl<'a> RemoveSpaces<'a> for ValueDef<'a> { - fn remove_spaces(&self, arena: &'a Bump) -> Self { - use ValueDef::*; - - match *self { - Annotation(a, b) => Annotation(a.remove_spaces(arena), b.remove_spaces(arena)), - Body(a, b) => Body( - arena.alloc(a.remove_spaces(arena)), - arena.alloc(b.remove_spaces(arena)), - ), - AnnotatedBody { - ann_pattern, - ann_type, - comment: _, - body_pattern, - body_expr, - } => AnnotatedBody { - ann_pattern: arena.alloc(ann_pattern.remove_spaces(arena)), - ann_type: arena.alloc(ann_type.remove_spaces(arena)), - comment: None, - body_pattern: arena.alloc(body_pattern.remove_spaces(arena)), - body_expr: arena.alloc(body_expr.remove_spaces(arena)), - }, - Expect(a) => Expect(arena.alloc(a.remove_spaces(arena))), - } - } -} - -impl<'a> RemoveSpaces<'a> for Def<'a> { - fn remove_spaces(&self, arena: &'a Bump) -> Self { - match *self { - Def::Type(def) => Def::Type(def.remove_spaces(arena)), - Def::Value(def) => Def::Value(def.remove_spaces(arena)), - Def::NotYetImplemented(a) => Def::NotYetImplemented(a), - Def::SpaceBefore(a, _) | Def::SpaceAfter(a, _) => a.remove_spaces(arena), - } - } -} - -impl<'a> RemoveSpaces<'a> for Has<'a> { - fn remove_spaces(&self, _arena: &'a Bump) -> Self { - Has::Has - } -} - -impl<'a> RemoveSpaces<'a> for AbilityMember<'a> { - fn remove_spaces(&self, arena: &'a Bump) -> Self { - AbilityMember { - name: self.name.remove_spaces(arena), - typ: self.typ.remove_spaces(arena), - } - } -} - -impl<'a> RemoveSpaces<'a> for WhenBranch<'a> { - fn remove_spaces(&self, arena: &'a Bump) -> Self { - WhenBranch { - patterns: self.patterns.remove_spaces(arena), - value: self.value.remove_spaces(arena), - guard: self.guard.remove_spaces(arena), - } - } -} - -impl<'a, T: RemoveSpaces<'a> + Copy + std::fmt::Debug> RemoveSpaces<'a> for AssignedField<'a, T> { - fn remove_spaces(&self, arena: &'a Bump) -> Self { - match *self { - AssignedField::RequiredValue(a, _, c) => AssignedField::RequiredValue( - a.remove_spaces(arena), - arena.alloc([]), - arena.alloc(c.remove_spaces(arena)), - ), - AssignedField::OptionalValue(a, _, c) => AssignedField::OptionalValue( - a.remove_spaces(arena), - arena.alloc([]), - arena.alloc(c.remove_spaces(arena)), - ), - AssignedField::LabelOnly(a) => AssignedField::LabelOnly(a.remove_spaces(arena)), - AssignedField::Malformed(a) => AssignedField::Malformed(a), - AssignedField::SpaceBefore(a, _) => a.remove_spaces(arena), - AssignedField::SpaceAfter(a, _) => a.remove_spaces(arena), - } - } -} - -impl<'a> RemoveSpaces<'a> for StrLiteral<'a> { - fn remove_spaces(&self, arena: &'a Bump) -> Self { - match *self { - StrLiteral::PlainLine(t) => StrLiteral::PlainLine(t), - StrLiteral::Line(t) => StrLiteral::Line(t.remove_spaces(arena)), - StrLiteral::Block(t) => StrLiteral::Block(t.remove_spaces(arena)), - } - } -} - -impl<'a> RemoveSpaces<'a> for StrSegment<'a> { - fn remove_spaces(&self, arena: &'a Bump) -> Self { - match *self { - StrSegment::Plaintext(t) => StrSegment::Plaintext(t), - StrSegment::Unicode(t) => StrSegment::Unicode(t.remove_spaces(arena)), - StrSegment::EscapedChar(c) => StrSegment::EscapedChar(c), - StrSegment::Interpolated(t) => StrSegment::Interpolated(t.remove_spaces(arena)), - } - } -} - -impl<'a> RemoveSpaces<'a> for Expr<'a> { - fn remove_spaces(&self, arena: &'a Bump) -> Self { - match *self { - Expr::Float(a) => Expr::Float(a), - Expr::Num(a) => Expr::Num(a), - Expr::NonBase10Int { - string, - base, - is_negative, - } => Expr::NonBase10Int { - string, - base, - is_negative, - }, - Expr::Str(a) => Expr::Str(a.remove_spaces(arena)), - Expr::Access(a, b) => Expr::Access(arena.alloc(a.remove_spaces(arena)), b), - Expr::AccessorFunction(a) => Expr::AccessorFunction(a), - Expr::List(a) => Expr::List(a.remove_spaces(arena)), - Expr::RecordUpdate { update, fields } => Expr::RecordUpdate { - update: arena.alloc(update.remove_spaces(arena)), - fields: fields.remove_spaces(arena), - }, - Expr::Record(a) => Expr::Record(a.remove_spaces(arena)), - Expr::Var { module_name, ident } => Expr::Var { module_name, ident }, - Expr::Underscore(a) => Expr::Underscore(a), - Expr::Tag(a) => Expr::Tag(a), - Expr::OpaqueRef(a) => Expr::OpaqueRef(a), - Expr::Closure(a, b) => Expr::Closure( - arena.alloc(a.remove_spaces(arena)), - arena.alloc(b.remove_spaces(arena)), - ), - Expr::Defs(a, b) => { - Expr::Defs(a.remove_spaces(arena), arena.alloc(b.remove_spaces(arena))) - } - Expr::Backpassing(a, b, c) => Expr::Backpassing( - arena.alloc(a.remove_spaces(arena)), - arena.alloc(b.remove_spaces(arena)), - arena.alloc(c.remove_spaces(arena)), - ), - Expr::Expect(a, b) => Expr::Expect( - arena.alloc(a.remove_spaces(arena)), - arena.alloc(b.remove_spaces(arena)), - ), - Expr::Apply(a, b, c) => Expr::Apply( - arena.alloc(a.remove_spaces(arena)), - b.remove_spaces(arena), - c, - ), - Expr::BinOps(a, b) => { - Expr::BinOps(a.remove_spaces(arena), arena.alloc(b.remove_spaces(arena))) - } - Expr::UnaryOp(a, b) => { - Expr::UnaryOp(arena.alloc(a.remove_spaces(arena)), b.remove_spaces(arena)) - } - Expr::If(a, b) => Expr::If(a.remove_spaces(arena), arena.alloc(b.remove_spaces(arena))), - Expr::When(a, b) => { - Expr::When(arena.alloc(a.remove_spaces(arena)), b.remove_spaces(arena)) - } - Expr::ParensAround(a) => { - // The formatter can remove redundant parentheses, so also remove these when normalizing for comparison. - a.remove_spaces(arena) - } - Expr::MalformedIdent(a, b) => Expr::MalformedIdent(a, b), - Expr::MalformedClosure => Expr::MalformedClosure, - Expr::PrecedenceConflict(a) => Expr::PrecedenceConflict(a), - Expr::SpaceBefore(a, _) => a.remove_spaces(arena), - Expr::SpaceAfter(a, _) => a.remove_spaces(arena), - Expr::SingleQuote(a) => Expr::Num(a), - } - } -} - -impl<'a> RemoveSpaces<'a> for Pattern<'a> { - fn remove_spaces(&self, arena: &'a Bump) -> Self { - match *self { - Pattern::Identifier(a) => Pattern::Identifier(a), - Pattern::Tag(a) => Pattern::Tag(a), - Pattern::OpaqueRef(a) => Pattern::OpaqueRef(a), - Pattern::Apply(a, b) => Pattern::Apply( - arena.alloc(a.remove_spaces(arena)), - arena.alloc(b.remove_spaces(arena)), - ), - Pattern::RecordDestructure(a) => Pattern::RecordDestructure(a.remove_spaces(arena)), - Pattern::RequiredField(a, b) => { - Pattern::RequiredField(a, arena.alloc(b.remove_spaces(arena))) - } - Pattern::OptionalField(a, b) => { - Pattern::OptionalField(a, arena.alloc(b.remove_spaces(arena))) - } - Pattern::NumLiteral(a) => Pattern::NumLiteral(a), - Pattern::NonBase10Literal { - string, - base, - is_negative, - } => Pattern::NonBase10Literal { - string, - base, - is_negative, - }, - Pattern::FloatLiteral(a) => Pattern::FloatLiteral(a), - Pattern::StrLiteral(a) => Pattern::StrLiteral(a), - Pattern::Underscore(a) => Pattern::Underscore(a), - Pattern::Malformed(a) => Pattern::Malformed(a), - Pattern::MalformedIdent(a, b) => Pattern::MalformedIdent(a, b), - Pattern::QualifiedIdentifier { module_name, ident } => { - Pattern::QualifiedIdentifier { module_name, ident } - } - Pattern::SpaceBefore(a, _) => a.remove_spaces(arena), - Pattern::SpaceAfter(a, _) => a.remove_spaces(arena), - Pattern::SingleQuote(a) => Pattern::NumLiteral(a), - } - } -} - -impl<'a> RemoveSpaces<'a> for TypeAnnotation<'a> { - fn remove_spaces(&self, arena: &'a Bump) -> Self { - match *self { - TypeAnnotation::Function(a, b) => TypeAnnotation::Function( - arena.alloc(a.remove_spaces(arena)), - arena.alloc(b.remove_spaces(arena)), - ), - TypeAnnotation::Apply(a, b, c) => TypeAnnotation::Apply(a, b, c.remove_spaces(arena)), - TypeAnnotation::BoundVariable(a) => TypeAnnotation::BoundVariable(a), - TypeAnnotation::As(a, _, TypeHeader { name, vars }) => TypeAnnotation::As( - arena.alloc(a.remove_spaces(arena)), - &[], - TypeHeader { - name: name.remove_spaces(arena), - vars: vars.remove_spaces(arena), - }, - ), - TypeAnnotation::Record { fields, ext } => TypeAnnotation::Record { - fields: fields.remove_spaces(arena), - ext: ext.remove_spaces(arena), - }, - TypeAnnotation::TagUnion { ext, tags } => TypeAnnotation::TagUnion { - ext: ext.remove_spaces(arena), - tags: tags.remove_spaces(arena), - }, - TypeAnnotation::Inferred => TypeAnnotation::Inferred, - TypeAnnotation::Wildcard => TypeAnnotation::Wildcard, - TypeAnnotation::Where(annot, has_clauses) => TypeAnnotation::Where( - arena.alloc(annot.remove_spaces(arena)), - arena.alloc(has_clauses.remove_spaces(arena)), - ), - TypeAnnotation::SpaceBefore(a, _) => a.remove_spaces(arena), - TypeAnnotation::SpaceAfter(a, _) => a.remove_spaces(arena), - TypeAnnotation::Malformed(a) => TypeAnnotation::Malformed(a), - } - } -} - -impl<'a> RemoveSpaces<'a> for HasClause<'a> { - fn remove_spaces(&self, arena: &'a Bump) -> Self { - HasClause { - var: self.var.remove_spaces(arena), - ability: self.ability.remove_spaces(arena), - } - } -} - -impl<'a> RemoveSpaces<'a> for Tag<'a> { - fn remove_spaces(&self, arena: &'a Bump) -> Self { - match *self { - Tag::Apply { name, args } => Tag::Apply { - name: name.remove_spaces(arena), - args: args.remove_spaces(arena), - }, - Tag::Malformed(a) => Tag::Malformed(a), - Tag::SpaceBefore(a, _) => a.remove_spaces(arena), - Tag::SpaceAfter(a, _) => a.remove_spaces(arena), - } - } -} - -impl<'a> RemoveSpaces<'a> for Derived<'a> { - fn remove_spaces(&self, arena: &'a Bump) -> Self { - match *self { - Derived::Has(derived) => Derived::Has(derived.remove_spaces(arena)), - Derived::SpaceBefore(derived, _) | Derived::SpaceAfter(derived, _) => { - derived.remove_spaces(arena) - } - } - } -} diff --git a/compiler/gen_dev/Cargo.toml b/compiler/gen_dev/Cargo.toml deleted file mode 100644 index ef8f6d1dbd..0000000000 --- a/compiler/gen_dev/Cargo.toml +++ /dev/null @@ -1,38 +0,0 @@ -[package] -name = "roc_gen_dev" -description = "The development backend for the Roc compiler" -version = "0.1.0" -authors = ["The Roc Contributors"] -license = "UPL-1.0" -edition = "2021" - -[dependencies] -roc_collections = { path = "../collections" } -roc_region = { path = "../region" } -roc_module = { path = "../module" } -roc_problem = { path = "../problem" } -roc_types = { path = "../types" } -roc_builtins = { path = "../builtins" } -roc_unify = { path = "../unify" } -roc_solve = { path = "../solve" } -roc_mono = { path = "../mono" } -roc_target = { path = "../roc_target" } -roc_error_macros = { path = "../../error_macros" } -bumpalo = { version = "3.8.0", features = ["collections"] } -target-lexicon = "0.12.3" -# TODO: Deal with the update of object to 0.27. -# It looks like it breaks linking the generated objects. -# Probably just need to specify an extra field that used to be implicit or something. -# When fixed also update the version of object in the linker. -object = { version = "0.26.2", features = ["write"] } -packed_struct = "0.10.0" - -[dev-dependencies] -roc_can = { path = "../can" } -roc_parse = { path = "../parse" } -roc_std = { path = "../../roc_std", default-features = false } -bumpalo = { version = "3.8.0", features = ["collections"] } - -[features] -target-aarch64 = [] -target-x86_64 = [] diff --git a/compiler/gen_dev/README.md b/compiler/gen_dev/README.md deleted file mode 100644 index 6a86279cce..0000000000 --- a/compiler/gen_dev/README.md +++ /dev/null @@ -1,125 +0,0 @@ -# Dev Backend - -The dev backend is focused on generating decent binaries extremely fast. -It goes from Roc's [mono ir](https://github.com/rtfeldman/roc/blob/trunk/compiler/mono/src/ir.rs) to an object file ready to be linked. - -## General Process - -The backend is essentially defined as two recursive match statement over the mono ir. -The first pass is used to do simple linear scan lifetime analysis. -In the future it may be expanded to add a few other quick optimizations. -The second pass is the actual meat of the backend that generates the byte buffer of output binary. -The process is pretty simple, but can get quite complex when you have to deal with memory layouts, function calls, and multiple architectures. - -## Core Abstractions - -This library is built with a number of core traits/generic types that may look quite weird at first glance. -The reason for all of the generics and traits is to allow rust to optimize each target specific backend. -Instead of every needing an `if linux ...` or `if arm ...` statement within the backend, -rust should be abled compile each specific target (`linux-arm`, `darwin-x86_64`, etc) as a static optimized backend without branches on target or dynamic dispatch. - -**Note:** links below are to files, not specific lines. Just look up the specific type in the file. - -### Backend - -[Backend](https://github.com/rtfeldman/roc/blob/trunk/compiler/gen_dev/src/lib.rs) is the core abstraction. -It understands Roc's mono ir and some high level ideas about the generation process. -The main job of Backend is to do high level optimizatons (like lazy literal loading) and parse the mono ir. -Every target specific backend must implement this trait. - -### Backend64Bit - -[Backend64Bit](https://github.com/rtfeldman/roc/blob/trunk/compiler/gen_dev/src/generic64/mod.rs) is more or less what it sounds like. -It is the backend that understands 64 bit architectures. -Currently it is the only backend implementation, but a 32 bit implementation will probably come in the future. -This backend understands that the unit of data movement is 64 bit. -It also knows about things common to all 64 bit architectures (general purpose registers, stack, float regs, etc). - -If you look at the signiture for Backend64Bit, it is actually quite complex. -Backend64Bit is generic over things like the register type, assembler, and calling convention. -This enables to backend to support multiple architectures and operating systems. -For example, the `windows-x86_64` would use the x86 register set, the x86 assembler, and the x86 windows calling convention. -`darwin-x86_64` and `linux-x86_64` would use the same register set and assembler, but they would use the system v amd64 abi calling convention. -Backend64Bit is generic over these types instead of containing these types within it's struct to avoid the cost of dynamic dispatch. - -### Assembler - -[Assembler](https://github.com/rtfeldman/roc/blob/trunk/compiler/gen_dev/src/generic64/mod.rs) is the trait for generating assembly bytes. -It defines a set of RISC-like assembly calls that must be implemented for each architecture. -A lot of these calls may not map one to one with actual assembly instructions for each architecture. -Instead, they are a general abstraction over functionality shared between all architectures. -This will grow regularly as more Roc builtins are added. -Here are example implementations for [arm](https://github.com/rtfeldman/roc/blob/trunk/compiler/gen_dev/src/generic64/aarch64.rs) and [x86_64](https://github.com/rtfeldman/roc/blob/trunk/compiler/gen_dev/src/generic64/x86_64.rs). - -### CallConv - -[CallConv](https://github.com/rtfeldman/roc/blob/trunk/compiler/gen_dev/src/generic64/mod.rs) is the abstraction over calling conventions. -It deals with register and stack specific information related to passing and returning arguments. -Here are example implementations for [arm](https://github.com/rtfeldman/roc/blob/trunk/compiler/gen_dev/src/generic64/aarch64.rs) and [x86_64](https://github.com/rtfeldman/roc/blob/trunk/compiler/gen_dev/src/generic64/x86_64.rs). - -## Adding New Features - -Adding a new builtin to the dev backend can be pretty simple. -Here is [an example](https://github.com/rtfeldman/roc/pull/893/files) of adding `Num.Sub`. - -This is the general procedure I follow with some helpful links: - -1. Find a feature that is just n+1. - For example, since we already have integers, adding a builtin that functions on them should be n+1. - On the other hand, since we don't yet have booleans/conditionals, adding if statements may not yet be n+1. - A good place to look for missing features is in the test files for generation in [test_gen](https://github.com/rtfeldman/roc/tree/trunk/compiler/test_gen). Any test that is not enabled for the `gen-dev` feature still needs to be added to the dev backend. Eventually all features should be enabled for the dev backend. -1. Pick/write the simplest test case you can find for the new feature. - Just add `feature = "gen-dev"` to the `cfg` line for the test case. -1. Uncomment the code to print out procedures [from here](https://github.com/rtfeldman/roc/blob/b03ed18553569314a420d5bf1fb0ead4b6b5ecda/compiler/test_gen/src/helpers/dev.rs#L76) and run the test. - It should fail and print out the mono ir for this test case. - Seeing the actual mono ir tends to be very helpful for complex additions. -1. Generally it will fail in one of the match statements in the [Backend](https://github.com/rtfeldman/roc/blob/trunk/compiler/gen_dev/src/lib.rs) trait. - Add the correct pattern matching and likely new function for your new builtin. - This will break the compile until you add the same function to places that implement the trait, - like [Backend64Bit](https://github.com/rtfeldman/roc/blob/trunk/compiler/gen_dev/src/generic64/mod.rs). -1. Keep following the chain down. - To implement the function in Backend64Bit, you may need to add new assembly calls. - Feel free to ignore backends that aren't x86_64 for now and just add `unimplemented!`. - See the helpful resources section below for guides on figuring out assembly bytes. -1. Hopefully at some point everything compiles and the test is passing. - If so, yay. Now add more tests for the same feature and make sure you didn't miss the edge cases. -1. If things aren't working, reach out on zulip. Get advice, maybe even pair. -1. Make a PR. - -## Helpful Resources - -- [Compiler Explorer](godbolt.org) - - Generates assembly from most languages. - Really good for getting a reference for what is required to do something. - Can answer questions like "how would x be implemented in arm assembly?" -- [objdump](https://www.tutorialspoint.com/unix_commands/objdump.htm) - - Super super useful commandline tool. - Lets you inspect exactly what is generated in a binary. - Can inspect assembly, relocations, and more. - I use this all the time for debugging and inspecting C sample apps. - May write a larger tutorial for this because it can be seriosly helpful. - As a note, when dealing with relocatoins, please make sure to compile with PIC. -- [Online Assembler](https://defuse.ca/online-x86-assembler.htm#disassembly) - - Useful for seeing the actual bytes generated by assembly instructions. - A lot of time it gives on out of multiple options because x86_64 has many ways to do things. - Also, sometimes it doesn't seem to generate things quite as you expect. -- [Alternative Online Assembler](http://shell-storm.org/online/Online-Assembler-and-Disassembler/) - - Like previous but with more architecture options. -- [x86 and amd64 instruction reference](https://www.felixcloutier.com/x86/) - - Great for looking up x86_64 instructions and there bytes. - Definitely missing information if you aren't used to reading it. -- [Intel 64 ISA Reference](https://software.intel.com/content/dam/develop/public/us/en/documents/325383-sdm-vol-2abcd.pdf) - - Super dense manual. - Contains everything you would need to know for x86_64. - Also is like 2000 pages. -- [ARM architecture reference manual](https://developer.arm.com/documentation/ddi0487/latest/) - - Same thing as the intel manual, but for ARM. - It is huge, but quite useful. - Links in the pdf make it easy to jump around and understand our arm enum. -- [A ToC of the 20 part linker essay](https://lwn.net/Articles/276782/) - - Lots of information on linkers by the author of the gold linker. -- If there is anything else basic that you want to know, - there is a good chance it is include in lectures from compiler courses. - Definitely look at some of the free moocs, lectures, or youtube class recordings on the subject. -- If you have any specific questions feel free to ping Brendan Hansknecht on zulip. -- If you have any other resource that you find useful, please add them here. diff --git a/compiler/gen_dev/src/generic64/aarch64.rs b/compiler/gen_dev/src/generic64/aarch64.rs deleted file mode 100644 index e1eb27d8aa..0000000000 --- a/compiler/gen_dev/src/generic64/aarch64.rs +++ /dev/null @@ -1,1228 +0,0 @@ -use crate::generic64::{storage::StorageManager, Assembler, CallConv, RegTrait}; -use crate::Relocation; -use bumpalo::collections::Vec; -use packed_struct::prelude::*; -use roc_error_macros::internal_error; -use roc_module::symbol::Symbol; -use roc_mono::layout::Layout; - -#[derive(Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Debug)] -#[allow(dead_code)] -pub enum AArch64GeneralReg { - X0 = 0, - X1 = 1, - X2 = 2, - X3 = 3, - X4 = 4, - X5 = 5, - X6 = 6, - X7 = 7, - XR = 8, - X9 = 9, - X10 = 10, - X11 = 11, - X12 = 12, - X13 = 13, - X14 = 14, - X15 = 15, - IP0 = 16, - IP1 = 17, - PR = 18, - X19 = 19, - X20 = 20, - X21 = 21, - X22 = 22, - X23 = 23, - X24 = 24, - X25 = 25, - X26 = 26, - X27 = 27, - X28 = 28, - FP = 29, - LR = 30, - /// This can mean Zero or Stack Pointer depending on the context. - ZRSP = 31, -} - -impl RegTrait for AArch64GeneralReg { - fn value(&self) -> u8 { - *self as u8 - } -} - -impl AArch64GeneralReg { - #[inline(always)] - fn id(&self) -> u8 { - *self as u8 - } -} - -#[derive(Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Debug)] -#[allow(dead_code)] -pub enum AArch64FloatReg {} -impl RegTrait for AArch64FloatReg { - fn value(&self) -> u8 { - *self as u8 - } -} - -#[derive(Copy, Clone)] -pub struct AArch64Assembler {} - -// AArch64Call may need to eventually be split by OS, -// but I think with how we use it, they may all be the same. -#[derive(Copy, Clone)] -pub struct AArch64Call {} - -const STACK_ALIGNMENT: u8 = 16; - -impl CallConv for AArch64Call { - const BASE_PTR_REG: AArch64GeneralReg = AArch64GeneralReg::FP; - const STACK_PTR_REG: AArch64GeneralReg = AArch64GeneralReg::ZRSP; - - const GENERAL_PARAM_REGS: &'static [AArch64GeneralReg] = &[ - AArch64GeneralReg::X0, - AArch64GeneralReg::X1, - AArch64GeneralReg::X2, - AArch64GeneralReg::X3, - AArch64GeneralReg::X4, - AArch64GeneralReg::X5, - AArch64GeneralReg::X6, - AArch64GeneralReg::X7, - ]; - const GENERAL_RETURN_REGS: &'static [AArch64GeneralReg] = Self::GENERAL_PARAM_REGS; - const GENERAL_DEFAULT_FREE_REGS: &'static [AArch64GeneralReg] = &[ - // The regs we want to use first should be at the end of this vec. - // We will use pop to get which reg to use next - - // Don't use frame pointer: AArch64GeneralReg::FP, - // Don't user indirect result location: AArch64GeneralReg::XR, - // Don't use platform register: AArch64GeneralReg::PR, - // Don't use link register: AArch64GeneralReg::LR, - // Don't use zero register/stack pointer: AArch64GeneralReg::ZRSP, - - // Use callee saved regs last. - AArch64GeneralReg::X19, - AArch64GeneralReg::X20, - AArch64GeneralReg::X21, - AArch64GeneralReg::X22, - AArch64GeneralReg::X23, - AArch64GeneralReg::X24, - AArch64GeneralReg::X25, - AArch64GeneralReg::X26, - AArch64GeneralReg::X27, - AArch64GeneralReg::X28, - // Use caller saved regs first. - AArch64GeneralReg::X0, - AArch64GeneralReg::X1, - AArch64GeneralReg::X2, - AArch64GeneralReg::X3, - AArch64GeneralReg::X4, - AArch64GeneralReg::X5, - AArch64GeneralReg::X6, - AArch64GeneralReg::X7, - AArch64GeneralReg::X9, - AArch64GeneralReg::X10, - AArch64GeneralReg::X11, - AArch64GeneralReg::X12, - AArch64GeneralReg::X13, - AArch64GeneralReg::X14, - AArch64GeneralReg::X15, - AArch64GeneralReg::IP0, - AArch64GeneralReg::IP1, - ]; - const FLOAT_PARAM_REGS: &'static [AArch64FloatReg] = &[]; - const FLOAT_RETURN_REGS: &'static [AArch64FloatReg] = Self::FLOAT_PARAM_REGS; - const FLOAT_DEFAULT_FREE_REGS: &'static [AArch64FloatReg] = &[]; - - const SHADOW_SPACE_SIZE: u8 = 0; - - #[inline(always)] - fn general_callee_saved(reg: &AArch64GeneralReg) -> bool { - matches!( - reg, - AArch64GeneralReg::X19 - | AArch64GeneralReg::X20 - | AArch64GeneralReg::X21 - | AArch64GeneralReg::X22 - | AArch64GeneralReg::X23 - | AArch64GeneralReg::X24 - | AArch64GeneralReg::X25 - | AArch64GeneralReg::X26 - | AArch64GeneralReg::X27 - | AArch64GeneralReg::X28 - ) - } - #[inline(always)] - fn float_callee_saved(_reg: &AArch64FloatReg) -> bool { - todo!("AArch64 FloatRegs"); - } - - #[inline(always)] - fn setup_stack( - buf: &mut Vec<'_, u8>, - saved_general_regs: &[AArch64GeneralReg], - saved_float_regs: &[AArch64FloatReg], - requested_stack_size: i32, - fn_call_stack_size: i32, - ) -> i32 { - // Full size is upcast to i64 to make sure we don't overflow here. - let full_stack_size = match requested_stack_size - .checked_add(8 * (saved_general_regs.len() + saved_float_regs.len()) as i32 + 8) // The extra 8 is space to store the frame pointer. - .and_then(|size| size.checked_add(fn_call_stack_size)) - { - Some(size) => size, - _ => internal_error!("Ran out of stack space"), - }; - let alignment = if full_stack_size <= 0 { - 0 - } else { - full_stack_size % STACK_ALIGNMENT as i32 - }; - let offset = if alignment == 0 { - 0 - } else { - STACK_ALIGNMENT - alignment as u8 - }; - if let Some(aligned_stack_size) = full_stack_size.checked_add(offset as i32) { - if aligned_stack_size > 0 { - AArch64Assembler::mov_reg64_reg64( - buf, - AArch64GeneralReg::FP, - AArch64GeneralReg::ZRSP, - ); - AArch64Assembler::sub_reg64_reg64_imm32( - buf, - AArch64GeneralReg::ZRSP, - AArch64GeneralReg::ZRSP, - aligned_stack_size, - ); - - // All the following stores could be optimized by using `STP` to store pairs. - let mut offset = aligned_stack_size; - offset -= 8; - AArch64Assembler::mov_stack32_reg64(buf, offset, AArch64GeneralReg::LR); - offset -= 8; - AArch64Assembler::mov_stack32_reg64(buf, offset, AArch64GeneralReg::FP); - - offset = aligned_stack_size - fn_call_stack_size; - for reg in saved_general_regs { - offset -= 8; - AArch64Assembler::mov_base32_reg64(buf, offset, *reg); - } - for reg in saved_float_regs { - offset -= 8; - AArch64Assembler::mov_base32_freg64(buf, offset, *reg); - } - aligned_stack_size - } else { - 0 - } - } else { - internal_error!("Ran out of stack space"); - } - } - - #[inline(always)] - fn cleanup_stack( - buf: &mut Vec<'_, u8>, - saved_general_regs: &[AArch64GeneralReg], - saved_float_regs: &[AArch64FloatReg], - aligned_stack_size: i32, - fn_call_stack_size: i32, - ) { - if aligned_stack_size > 0 { - // All the following stores could be optimized by using `STP` to store pairs. - let mut offset = aligned_stack_size; - offset -= 8; - AArch64Assembler::mov_reg64_stack32(buf, AArch64GeneralReg::LR, offset); - offset -= 8; - AArch64Assembler::mov_reg64_stack32(buf, AArch64GeneralReg::FP, offset); - - offset = aligned_stack_size - fn_call_stack_size; - for reg in saved_general_regs { - offset -= 8; - AArch64Assembler::mov_reg64_base32(buf, *reg, offset); - } - for reg in saved_float_regs { - offset -= 8; - AArch64Assembler::mov_freg64_base32(buf, *reg, offset); - } - AArch64Assembler::add_reg64_reg64_imm32( - buf, - AArch64GeneralReg::ZRSP, - AArch64GeneralReg::ZRSP, - aligned_stack_size, - ); - } - } - - #[inline(always)] - fn load_args<'a>( - _buf: &mut Vec<'a, u8>, - _storage_manager: &mut StorageManager< - 'a, - AArch64GeneralReg, - AArch64FloatReg, - AArch64Assembler, - AArch64Call, - >, - _args: &'a [(Layout<'a>, Symbol)], - _ret_layout: &Layout<'a>, - ) { - todo!("Loading args for AArch64"); - } - - #[inline(always)] - fn store_args<'a>( - _buf: &mut Vec<'a, u8>, - _storage_manager: &mut StorageManager< - 'a, - AArch64GeneralReg, - AArch64FloatReg, - AArch64Assembler, - AArch64Call, - >, - _dst: &Symbol, - _args: &[Symbol], - _arg_layouts: &[Layout<'a>], - _ret_layout: &Layout<'a>, - ) { - todo!("Storing args for AArch64"); - } - - fn return_complex_symbol<'a>( - _buf: &mut Vec<'a, u8>, - _storage_manager: &mut StorageManager< - 'a, - AArch64GeneralReg, - AArch64FloatReg, - AArch64Assembler, - AArch64Call, - >, - _sym: &Symbol, - _layout: &Layout<'a>, - ) { - todo!("Returning complex symbols for AArch64"); - } - - fn load_returned_complex_symbol<'a>( - _buf: &mut Vec<'a, u8>, - _storage_manager: &mut StorageManager< - 'a, - AArch64GeneralReg, - AArch64FloatReg, - AArch64Assembler, - AArch64Call, - >, - _sym: &Symbol, - _layout: &Layout<'a>, - ) { - todo!("Loading returned complex symbols for AArch64"); - } -} - -impl Assembler for AArch64Assembler { - #[inline(always)] - fn abs_reg64_reg64(_buf: &mut Vec<'_, u8>, _dst: AArch64GeneralReg, _src: AArch64GeneralReg) { - todo!("abs_reg64_reg64 for AArch64"); - } - - #[inline(always)] - fn abs_freg64_freg64( - _buf: &mut Vec<'_, u8>, - _relocs: &mut Vec<'_, Relocation>, - _dst: AArch64FloatReg, - _src: AArch64FloatReg, - ) { - todo!("abs_reg64_reg64 for AArch64"); - } - - #[inline(always)] - fn add_reg64_reg64_imm32( - buf: &mut Vec<'_, u8>, - dst: AArch64GeneralReg, - src: AArch64GeneralReg, - imm32: i32, - ) { - if imm32 < 0 { - todo!("immediate addition with values less than 0"); - } else if imm32 < 0xFFF { - add_reg64_reg64_imm12(buf, dst, src, imm32 as u16); - } else { - todo!("immediate additions with values greater than 12bits"); - } - } - #[inline(always)] - fn add_reg64_reg64_reg64( - buf: &mut Vec<'_, u8>, - dst: AArch64GeneralReg, - src1: AArch64GeneralReg, - src2: AArch64GeneralReg, - ) { - add_reg64_reg64_reg64(buf, dst, src1, src2); - } - #[inline(always)] - fn add_freg64_freg64_freg64( - _buf: &mut Vec<'_, u8>, - _dst: AArch64FloatReg, - _src1: AArch64FloatReg, - _src2: AArch64FloatReg, - ) { - todo!("adding floats for AArch64"); - } - - #[inline(always)] - fn call(_buf: &mut Vec<'_, u8>, _relocs: &mut Vec<'_, Relocation>, _fn_name: String) { - todo!("calling functions literal for AArch64"); - } - - #[inline(always)] - fn imul_reg64_reg64_reg64( - _buf: &mut Vec<'_, u8>, - _dst: AArch64GeneralReg, - _src1: AArch64GeneralReg, - _src2: AArch64GeneralReg, - ) { - todo!("register multiplication for AArch64"); - } - - #[inline(always)] - fn jmp_imm32(_buf: &mut Vec<'_, u8>, _offset: i32) -> usize { - todo!("jump instructions for AArch64"); - } - - #[inline(always)] - fn tail_call(buf: &mut Vec<'_, u8>) -> u64 { - Self::jmp_imm32(buf, 0); - buf.len() as u64 - 4 // TODO is 4 the correct offset in ARM? - } - - #[inline(always)] - fn jne_reg64_imm64_imm32( - _buf: &mut Vec<'_, u8>, - _reg: AArch64GeneralReg, - _imm: u64, - _offset: i32, - ) -> usize { - todo!("jump not equal instructions for AArch64"); - } - - #[inline(always)] - fn mov_freg32_imm32( - _buf: &mut Vec<'_, u8>, - _relocs: &mut Vec<'_, Relocation>, - _dst: AArch64FloatReg, - _imm: f32, - ) { - todo!("loading f32 literal for AArch64"); - } - #[inline(always)] - fn mov_freg64_imm64( - _buf: &mut Vec<'_, u8>, - _relocs: &mut Vec<'_, Relocation>, - _dst: AArch64FloatReg, - _imm: f64, - ) { - todo!("loading f64 literal for AArch64"); - } - #[inline(always)] - fn mov_reg64_imm64(buf: &mut Vec<'_, u8>, dst: AArch64GeneralReg, imm: i64) { - let mut remaining = imm as u64; - movz_reg64_imm16(buf, dst, remaining as u16, 0); - remaining >>= 16; - if remaining > 0 { - movk_reg64_imm16(buf, dst, remaining as u16, 1); - } - remaining >>= 16; - if remaining > 0 { - movk_reg64_imm16(buf, dst, remaining as u16, 2); - } - remaining >>= 16; - if remaining > 0 { - movk_reg64_imm16(buf, dst, remaining as u16, 3); - } - } - #[inline(always)] - fn mov_freg64_freg64(_buf: &mut Vec<'_, u8>, _dst: AArch64FloatReg, _src: AArch64FloatReg) { - todo!("moving data between float registers for AArch64"); - } - #[inline(always)] - fn mov_reg64_reg64(buf: &mut Vec<'_, u8>, dst: AArch64GeneralReg, src: AArch64GeneralReg) { - mov_reg64_reg64(buf, dst, src); - } - - #[inline(always)] - fn mov_freg64_base32(_buf: &mut Vec<'_, u8>, _dst: AArch64FloatReg, _offset: i32) { - todo!("loading floating point reg from base offset for AArch64"); - } - #[inline(always)] - fn mov_reg64_base32(buf: &mut Vec<'_, u8>, dst: AArch64GeneralReg, offset: i32) { - if offset < 0 { - todo!("negative base offsets for AArch64"); - } else if offset < (0xFFF << 8) { - debug_assert!(offset % 8 == 0); - ldr_reg64_imm12(buf, dst, AArch64GeneralReg::FP, (offset as u16) >> 3); - } else { - todo!("base offsets over 32k for AArch64"); - } - } - #[inline(always)] - fn mov_base32_freg64(_buf: &mut Vec<'_, u8>, _offset: i32, _src: AArch64FloatReg) { - todo!("saving floating point reg to base offset for AArch64"); - } - #[inline(always)] - fn mov_base32_reg64(buf: &mut Vec<'_, u8>, offset: i32, src: AArch64GeneralReg) { - if offset < 0 { - todo!("negative base offsets for AArch64"); - } else if offset < (0xFFF << 8) { - debug_assert!(offset % 8 == 0); - str_reg64_imm12(buf, src, AArch64GeneralReg::FP, (offset as u16) >> 3); - } else { - todo!("base offsets over 32k for AArch64"); - } - } - - #[inline(always)] - fn mov_reg64_mem64_offset32( - buf: &mut Vec<'_, u8>, - dst: AArch64GeneralReg, - src: AArch64GeneralReg, - offset: i32, - ) { - if offset < 0 { - todo!("negative mem offsets for AArch64"); - } else if offset < (0xFFF << 8) { - debug_assert!(offset % 8 == 0); - ldr_reg64_imm12(buf, dst, src, (offset as u16) >> 3); - } else { - todo!("mem offsets over 32k for AArch64"); - } - } - #[inline(always)] - fn mov_mem64_offset32_reg64( - buf: &mut Vec<'_, u8>, - dst: AArch64GeneralReg, - offset: i32, - src: AArch64GeneralReg, - ) { - if offset < 0 { - todo!("negative mem offsets for AArch64"); - } else if offset < (0xFFF << 8) { - debug_assert!(offset % 8 == 0); - str_reg64_imm12(buf, src, dst, (offset as u16) >> 3); - } else { - todo!("mem offsets over 32k for AArch64"); - } - } - - #[inline(always)] - fn movsx_reg64_base32(buf: &mut Vec<'_, u8>, dst: AArch64GeneralReg, offset: i32, size: u8) { - debug_assert!(size <= 8); - if size == 8 { - Self::mov_reg64_base32(buf, dst, offset); - } else if size == 4 { - todo!("sign extending 4 byte values"); - } else if size == 2 { - todo!("sign extending 2 byte values"); - } else if size == 1 { - todo!("sign extending 1 byte values"); - } else { - internal_error!("Invalid size for sign extension: {}", size); - } - } - #[inline(always)] - fn movzx_reg64_base32(buf: &mut Vec<'_, u8>, dst: AArch64GeneralReg, offset: i32, size: u8) { - debug_assert!(size <= 8); - if size == 8 { - Self::mov_reg64_base32(buf, dst, offset); - } else if size == 4 { - todo!("zero extending 4 byte values"); - } else if size == 2 { - todo!("zero extending 2 byte values"); - } else if size == 1 { - todo!("zero extending 1 byte values"); - } else { - internal_error!("Invalid size for zero extension: {}", size); - } - } - - #[inline(always)] - fn mov_freg64_stack32(_buf: &mut Vec<'_, u8>, _dst: AArch64FloatReg, _offset: i32) { - todo!("loading floating point reg from stack for AArch64"); - } - #[inline(always)] - fn mov_reg64_stack32(buf: &mut Vec<'_, u8>, dst: AArch64GeneralReg, offset: i32) { - if offset < 0 { - todo!("negative stack offsets for AArch64"); - } else if offset < (0xFFF << 8) { - debug_assert!(offset % 8 == 0); - ldr_reg64_imm12(buf, dst, AArch64GeneralReg::ZRSP, (offset as u16) >> 3); - } else { - todo!("stack offsets over 32k for AArch64"); - } - } - #[inline(always)] - fn mov_stack32_freg64(_buf: &mut Vec<'_, u8>, _offset: i32, _src: AArch64FloatReg) { - todo!("saving floating point reg to stack for AArch64"); - } - #[inline(always)] - fn mov_stack32_reg64(buf: &mut Vec<'_, u8>, offset: i32, src: AArch64GeneralReg) { - if offset < 0 { - todo!("negative stack offsets for AArch64"); - } else if offset < (0xFFF << 8) { - debug_assert!(offset % 8 == 0); - str_reg64_imm12(buf, src, AArch64GeneralReg::ZRSP, (offset as u16) >> 3); - } else { - todo!("stack offsets over 32k for AArch64"); - } - } - #[inline(always)] - fn neg_reg64_reg64(_buf: &mut Vec<'_, u8>, _dst: AArch64GeneralReg, _src: AArch64GeneralReg) { - todo!("neg for AArch64"); - } - - #[inline(always)] - fn sub_reg64_reg64_imm32( - buf: &mut Vec<'_, u8>, - dst: AArch64GeneralReg, - src: AArch64GeneralReg, - imm32: i32, - ) { - if imm32 < 0 { - todo!("immediate subtractions with values less than 0"); - } else if imm32 < 0xFFF { - sub_reg64_reg64_imm12(buf, dst, src, imm32 as u16); - } else { - todo!("immediate subtractions with values greater than 12bits"); - } - } - #[inline(always)] - fn sub_reg64_reg64_reg64( - _buf: &mut Vec<'_, u8>, - _dst: AArch64GeneralReg, - _src1: AArch64GeneralReg, - _src2: AArch64GeneralReg, - ) { - todo!("registers subtractions for AArch64"); - } - - #[inline(always)] - fn eq_reg64_reg64_reg64( - _buf: &mut Vec<'_, u8>, - _dst: AArch64GeneralReg, - _src1: AArch64GeneralReg, - _src2: AArch64GeneralReg, - ) { - todo!("registers equality for AArch64"); - } - - #[inline(always)] - fn neq_reg64_reg64_reg64( - _buf: &mut Vec<'_, u8>, - _dst: AArch64GeneralReg, - _src1: AArch64GeneralReg, - _src2: AArch64GeneralReg, - ) { - todo!("registers non-equality for AArch64"); - } - - #[inline(always)] - fn lt_reg64_reg64_reg64( - _buf: &mut Vec<'_, u8>, - _dst: AArch64GeneralReg, - _src1: AArch64GeneralReg, - _src2: AArch64GeneralReg, - ) { - todo!("registers less than for AArch64"); - } - - #[inline(always)] - fn to_float_freg64_reg64( - _buf: &mut Vec<'_, u8>, - _dst: AArch64FloatReg, - _src: AArch64GeneralReg, - ) { - todo!("registers to float for AArch64"); - } - - #[inline(always)] - fn to_float_freg32_reg64( - _buf: &mut Vec<'_, u8>, - _dst: AArch64FloatReg, - _src: AArch64GeneralReg, - ) { - todo!("registers to float for AArch64"); - } - - #[inline(always)] - fn to_float_freg32_freg64( - _buf: &mut Vec<'_, u8>, - _dst: AArch64FloatReg, - _src: AArch64FloatReg, - ) { - todo!("registers to float for AArch64"); - } - - #[inline(always)] - fn to_float_freg64_freg32( - _buf: &mut Vec<'_, u8>, - _dst: AArch64FloatReg, - _src: AArch64FloatReg, - ) { - todo!("registers to float for AArch64"); - } - - #[inline(always)] - fn lte_reg64_reg64_reg64( - _buf: &mut Vec<'_, u8>, - _dst: AArch64GeneralReg, - _src1: AArch64GeneralReg, - _src2: AArch64GeneralReg, - ) { - todo!("registers less than or equal for AArch64"); - } - - #[inline(always)] - fn gte_reg64_reg64_reg64( - _buf: &mut Vec<'_, u8>, - _dst: AArch64GeneralReg, - _src1: AArch64GeneralReg, - _src2: AArch64GeneralReg, - ) { - todo!("registers greater than or equal for AArch64"); - } - - #[inline(always)] - fn ret(buf: &mut Vec<'_, u8>) { - ret_reg64(buf, AArch64GeneralReg::LR) - } -} - -impl AArch64Assembler {} - -// Instructions -// ARM manual section C3 -// https://developer.arm.com/documentation/ddi0487/ga -// Map all instructions to a packed struct. - -trait Aarch64Bytes: PackedStruct { - #[inline(always)] - fn bytes(&self) -> [u8; 4] { - let mut bytes: [u8; 4] = [0, 0, 0, 0]; - - self.pack_to_slice(&mut bytes).unwrap(); - - bytes.reverse(); - - bytes - } -} - -#[derive(PackedStruct, Debug)] -#[packed_struct(endian = "msb")] -pub struct MoveWideImmediate { - sf: bool, - opc: Integer>, - fixed: Integer>, // = 0b100101, - hw: Integer>, - imm16: u16, - reg_d: Integer>, // AArch64GeneralReg -} - -impl Aarch64Bytes for MoveWideImmediate {} - -impl MoveWideImmediate { - #[inline(always)] - fn new(opc: u8, rd: AArch64GeneralReg, imm16: u16, hw: u8, sf: bool) -> Self { - // TODO: revisit this is we change where we want to check the shift - // currently this is done in the assembler above - // assert!(shift % 16 == 0 && shift <= 48); - debug_assert!(hw <= 0b11); - debug_assert!(opc <= 0b11); - - Self { - reg_d: rd.id().into(), - imm16, - hw: hw.into(), - opc: opc.into(), - sf, - fixed: 0b100101.into(), - } - } -} - -#[derive(PackedStruct, Debug)] -#[packed_struct(endian = "msb")] -pub struct ArithmeticImmediate { - sf: bool, - op: bool, // add or subtract - s: bool, - fixed: Integer>, // = 0b100010, - sh: bool, // shift - imm12: Integer>, - reg_n: Integer>, - reg_d: Integer>, -} - -impl Aarch64Bytes for ArithmeticImmediate {} - -impl ArithmeticImmediate { - #[inline(always)] - fn new( - op: bool, - s: bool, - rd: AArch64GeneralReg, - rn: AArch64GeneralReg, - imm12: u16, - sh: bool, - ) -> Self { - debug_assert!(imm12 <= 0xFFF); - - Self { - reg_d: rd.id().into(), - reg_n: rn.id().into(), - imm12: imm12.into(), - sh, - s, - op, - // true for 64 bit addition - // false for 32 bit addition - sf: true, - fixed: 0b100010.into(), - } - } -} - -#[derive(Clone, Copy)] -#[allow(dead_code)] -enum ShiftType { - LSL = 0, - LSR = 1, - ASR = 2, - ROR = 3, -} - -impl ShiftType { - #[inline(always)] - fn id(&self) -> u8 { - *self as u8 - } -} - -#[derive(PackedStruct)] -#[packed_struct(endian = "msb")] -pub struct ArithmeticShifted { - sf: bool, - op: bool, // add or subtract - s: bool, - fixed: Integer>, // = 0b01011, - shift: Integer>, // shift - fixed2: bool, // = 0b0, - reg_m: Integer>, - imm6: Integer>, - reg_n: Integer>, - reg_d: Integer>, -} - -impl Aarch64Bytes for ArithmeticShifted {} - -impl ArithmeticShifted { - #[inline(always)] - fn new( - op: bool, - s: bool, - shift: ShiftType, - imm6: u8, - rm: AArch64GeneralReg, - rn: AArch64GeneralReg, - rd: AArch64GeneralReg, - ) -> Self { - debug_assert!(imm6 <= 0b111111); - - Self { - reg_d: rd.id().into(), - reg_n: rn.id().into(), - imm6: imm6.into(), - reg_m: rm.id().into(), - fixed2: false, - shift: shift.id().into(), - fixed: 0b01011.into(), - s, - op, - // true for 64 bit addition - // false for 32 bit addition - sf: true, - } - } -} - -#[derive(Debug)] -#[allow(dead_code)] -enum LogicalOp { - AND, - BIC, - ORR, - ORN, - EOR, - EON, - ANDS, - BICS, -} - -#[derive(PackedStruct)] -#[packed_struct(endian = "msb")] -pub struct LogicalShiftedRegister { - sf: bool, - op: Integer>, - fixed: Integer>, // = 0b01010, - shift: Integer>, // shift - n: bool, - reg_m: Integer>, - imm6: Integer>, - reg_n: Integer>, - reg_d: Integer>, -} - -impl Aarch64Bytes for LogicalShiftedRegister {} - -impl LogicalShiftedRegister { - #[inline(always)] - fn new( - op: LogicalOp, - shift: ShiftType, - imm6: u8, - rm: AArch64GeneralReg, - rn: AArch64GeneralReg, - rd: AArch64GeneralReg, - ) -> Self { - debug_assert!(imm6 <= 0b111111); - - let (op, n) = match op { - LogicalOp::AND => (0b00, false), - LogicalOp::BIC => (0b00, true), - LogicalOp::ORR => (0b01, false), - LogicalOp::ORN => (0b01, true), - LogicalOp::EOR => (0b10, false), - LogicalOp::EON => (0b10, true), - LogicalOp::ANDS => (0b11, false), - LogicalOp::BICS => (0b11, true), - }; - - Self { - reg_d: rd.id().into(), - reg_n: rn.id().into(), - imm6: imm6.into(), - reg_m: rm.id().into(), - n, - shift: shift.id().into(), - fixed: 0b01010.into(), - op: op.into(), - // true for 64 bit addition - // false for 32 bit addition - sf: true, - } - } -} - -#[derive(PackedStruct)] -pub struct UnconditionalBranchRegister { - fixed: Integer>, - z: bool, - fixed2: bool, - op: Integer>, - fixed3: Integer>, - fixed4: Integer>, - a: bool, - m: bool, - rn: Integer>, - fixed5: Integer>, -} - -impl Aarch64Bytes for UnconditionalBranchRegister {} - -impl UnconditionalBranchRegister { - #[inline(always)] - fn new(op: u8, rn: AArch64GeneralReg) -> Self { - debug_assert!(op <= 0b11); - - Self { - fixed5: 0b00000.into(), - rn: rn.id().into(), - m: false, - a: false, - fixed4: 0b0000.into(), - fixed3: 0b11111.into(), - op: op.into(), - fixed2: false, - z: false, - fixed: 0b1101011.into(), - } - } -} - -// Uses unsigned Offset -// opc = 0b01 means load -// opc = 0b00 means store -#[derive(PackedStruct, Debug)] -#[packed_struct(endian = "msb")] -pub struct LoadStoreRegisterImmediate { - size: Integer>, - fixed: Integer>, // = 0b111, - fixed2: bool, - fixed3: Integer>, - opc: Integer>, - imm12: Integer>, - rn: Integer>, - rt: Integer>, -} - -impl Aarch64Bytes for LoadStoreRegisterImmediate {} - -impl LoadStoreRegisterImmediate { - #[inline(always)] - fn new(size: u8, opc: u8, imm12: u16, rn: AArch64GeneralReg, rt: AArch64GeneralReg) -> Self { - debug_assert!(size <= 0b11); - debug_assert!(imm12 <= 0xFFF); - - Self { - rt: rt.id().into(), - rn: rn.id().into(), - imm12: imm12.into(), - opc: opc.into(), - fixed3: 0b01.into(), - fixed2: false, - fixed: 0b111.into(), - size: size.into(), - } - } - - #[inline(always)] - fn new_load(size: u8, imm12: u16, rn: AArch64GeneralReg, rt: AArch64GeneralReg) -> Self { - Self::new(size, 0b01, imm12, rn, rt) - } - - #[inline(always)] - fn new_store(size: u8, imm12: u16, rn: AArch64GeneralReg, rt: AArch64GeneralReg) -> Self { - Self::new(size, 0b00, imm12, rn, rt) - } -} - -// Below here are the functions for all of the assembly instructions. -// Their names are based on the instruction and operators combined. -// You should call `buf.reserve()` if you push or extend more than once. -// Unit tests are added at the bottom of the file to ensure correct asm generation. -// Please keep these in alphanumeric order. - -/// `ADD Xd, Xn, imm12` -> Add Xn and imm12 and place the result into Xd. -#[inline(always)] -fn add_reg64_reg64_imm12( - buf: &mut Vec<'_, u8>, - dst: AArch64GeneralReg, - src: AArch64GeneralReg, - imm12: u16, -) { - let inst = ArithmeticImmediate::new(false, false, dst, src, imm12, false); - - buf.extend(inst.bytes()); -} - -/// `ADD Xd, Xm, Xn` -> Add Xm and Xn and place the result into Xd. -#[inline(always)] -fn add_reg64_reg64_reg64( - buf: &mut Vec<'_, u8>, - dst: AArch64GeneralReg, - src1: AArch64GeneralReg, - src2: AArch64GeneralReg, -) { - let inst = ArithmeticShifted::new(false, false, ShiftType::LSL, 0, src1, src2, dst); - - buf.extend(inst.bytes()); -} - -/// `LDR Xt, [Xn, #offset]` -> Load Xn + Offset Xt. ZRSP is SP. -/// Note: imm12 is the offest divided by 8. -#[inline(always)] -fn ldr_reg64_imm12( - buf: &mut Vec<'_, u8>, - dst: AArch64GeneralReg, - base: AArch64GeneralReg, - imm12: u16, -) { - let inst = LoadStoreRegisterImmediate::new_load(0b11, imm12, base, dst); - - buf.extend(inst.bytes()); -} - -/// `MOV Xd, Xm` -> Move Xm to Xd. -#[inline(always)] -fn mov_reg64_reg64(buf: &mut Vec<'_, u8>, dst: AArch64GeneralReg, src: AArch64GeneralReg) { - // MOV is equvalent to `ORR Xd, XZR, XM` in AARCH64. - let inst = LogicalShiftedRegister::new( - LogicalOp::ORR, - ShiftType::LSL, - 0, - src, - AArch64GeneralReg::ZRSP, - dst, - ); - - buf.extend(inst.bytes()); -} - -/// `MOVK Xd, imm16` -> Keeps Xd and moves an optionally shifted imm16 to Xd. -#[inline(always)] -fn movk_reg64_imm16(buf: &mut Vec<'_, u8>, dst: AArch64GeneralReg, imm16: u16, hw: u8) { - let inst = MoveWideImmediate::new(0b11, dst, imm16, hw, true); - - buf.extend(inst.bytes()); -} - -/// `MOVZ Xd, imm16` -> Zeros Xd and moves an optionally shifted imm16 to Xd. -#[inline(always)] -fn movz_reg64_imm16(buf: &mut Vec<'_, u8>, dst: AArch64GeneralReg, imm16: u16, hw: u8) { - let inst = MoveWideImmediate::new(0b10, dst, imm16, hw, true); - - buf.extend(inst.bytes()); -} - -/// `STR Xt, [Xn, #offset]` -> Store Xt to Xn + Offset. ZRSP is SP. -/// Note: imm12 is the offest divided by 8. -#[inline(always)] -fn str_reg64_imm12( - buf: &mut Vec<'_, u8>, - src: AArch64GeneralReg, - base: AArch64GeneralReg, - imm12: u16, -) { - let inst = LoadStoreRegisterImmediate::new_store(0b11, imm12, base, src); - - buf.extend(inst.bytes()); -} - -/// `SUB Xd, Xn, imm12` -> Subtract Xn and imm12 and place the result into Xd. -#[inline(always)] -fn sub_reg64_reg64_imm12( - buf: &mut Vec<'_, u8>, - dst: AArch64GeneralReg, - src: AArch64GeneralReg, - imm12: u16, -) { - let inst = ArithmeticImmediate::new(true, false, dst, src, imm12, false); - - buf.extend(inst.bytes()); -} - -/// `RET Xn` -> Return to the address stored in Xn. -#[inline(always)] -fn ret_reg64(buf: &mut Vec<'_, u8>, xn: AArch64GeneralReg) { - let inst = UnconditionalBranchRegister::new(0b10, xn); - - buf.extend(inst.bytes()); -} - -#[cfg(test)] -mod tests { - use super::*; - - const TEST_U16: u16 = 0x1234; - //const TEST_I32: i32 = 0x12345678; - //const TEST_I64: i64 = 0x12345678_9ABCDEF0; - - #[test] - fn test_add_reg64_reg64_reg64() { - let arena = bumpalo::Bump::new(); - let mut buf = bumpalo::vec![in &arena]; - add_reg64_reg64_reg64( - &mut buf, - AArch64GeneralReg::X10, - AArch64GeneralReg::ZRSP, - AArch64GeneralReg::X21, - ); - assert_eq!(&buf, &[0xAA, 0x02, 0x1F, 0x8B]); - } - - #[test] - fn test_add_reg64_reg64_imm12() { - let arena = bumpalo::Bump::new(); - let mut buf = bumpalo::vec![in &arena]; - add_reg64_reg64_imm12( - &mut buf, - AArch64GeneralReg::X10, - AArch64GeneralReg::X21, - 0x123, - ); - assert_eq!(&buf, &[0xAA, 0x8E, 0x04, 0x91]); - } - - #[test] - fn test_ldr_reg64_imm12() { - let arena = bumpalo::Bump::new(); - let mut buf = bumpalo::vec![in &arena]; - ldr_reg64_imm12( - &mut buf, - AArch64GeneralReg::X21, - AArch64GeneralReg::ZRSP, - 0x123, - ); - assert_eq!(&buf, &[0xF5, 0x8F, 0x44, 0xF9]); - } - - #[test] - fn test_mov_reg64_reg64() { - let arena = bumpalo::Bump::new(); - let mut buf = bumpalo::vec![in &arena]; - mov_reg64_reg64(&mut buf, AArch64GeneralReg::X10, AArch64GeneralReg::X21); - assert_eq!(&buf, &[0xEA, 0x03, 0x15, 0xAA]); - } - - #[test] - fn test_movk_reg64_imm16() { - let arena = bumpalo::Bump::new(); - let mut buf = bumpalo::vec![in &arena]; - movk_reg64_imm16(&mut buf, AArch64GeneralReg::X21, TEST_U16, 3); - assert_eq!(&buf, &[0x95, 0x46, 0xE2, 0xF2]); - } - - #[test] - fn test_movz_reg64_imm16() { - let arena = bumpalo::Bump::new(); - let mut buf = bumpalo::vec![in &arena]; - movz_reg64_imm16(&mut buf, AArch64GeneralReg::X21, TEST_U16, 3); - assert_eq!(&buf, &[0x95, 0x46, 0xE2, 0xD2]); - } - - #[test] - fn test_str_reg64_imm12() { - let arena = bumpalo::Bump::new(); - let mut buf = bumpalo::vec![in &arena]; - str_reg64_imm12( - &mut buf, - AArch64GeneralReg::X21, - AArch64GeneralReg::ZRSP, - 0x123, - ); - assert_eq!(&buf, &[0xF5, 0x8F, 0x04, 0xF9]); - } - - #[test] - fn test_sub_reg64_reg64_imm12() { - let arena = bumpalo::Bump::new(); - let mut buf = bumpalo::vec![in &arena]; - sub_reg64_reg64_imm12( - &mut buf, - AArch64GeneralReg::X10, - AArch64GeneralReg::X21, - 0x123, - ); - assert_eq!(&buf, &[0xAA, 0x8E, 0x04, 0xD1]); - } - - #[test] - fn test_ret_reg64() { - let arena = bumpalo::Bump::new(); - let mut buf = bumpalo::vec![in &arena]; - ret_reg64(&mut buf, AArch64GeneralReg::LR); - assert_eq!(&buf, &[0xC0, 0x03, 0x5F, 0xD6]); - } -} diff --git a/compiler/gen_dev/src/generic64/mod.rs b/compiler/gen_dev/src/generic64/mod.rs deleted file mode 100644 index 64dd060cb5..0000000000 --- a/compiler/gen_dev/src/generic64/mod.rs +++ /dev/null @@ -1,1304 +0,0 @@ -use crate::{ - single_register_floats, single_register_int_builtins, single_register_integers, Backend, Env, - Relocation, -}; -use bumpalo::collections::Vec; -use roc_builtins::bitcode::{self, FloatWidth, IntWidth}; -use roc_collections::all::MutMap; -use roc_error_macros::internal_error; -use roc_module::symbol::{Interns, Symbol}; -use roc_mono::code_gen_help::CodeGenHelp; -use roc_mono::ir::{BranchInfo, JoinPointId, Literal, Param, ProcLayout, SelfRecursive, Stmt}; -use roc_mono::layout::{Builtin, Layout, TagIdIntType, UnionLayout}; -use roc_target::TargetInfo; -use std::marker::PhantomData; - -pub(crate) mod aarch64; -pub(crate) mod storage; -pub(crate) mod x86_64; - -use storage::StorageManager; - -// TODO: on all number functions double check and deal with over/underflow. - -pub trait CallConv>: - Sized + Copy -{ - const BASE_PTR_REG: GeneralReg; - const STACK_PTR_REG: GeneralReg; - - const GENERAL_PARAM_REGS: &'static [GeneralReg]; - const GENERAL_RETURN_REGS: &'static [GeneralReg]; - const GENERAL_DEFAULT_FREE_REGS: &'static [GeneralReg]; - - const FLOAT_PARAM_REGS: &'static [FloatReg]; - const FLOAT_RETURN_REGS: &'static [FloatReg]; - const FLOAT_DEFAULT_FREE_REGS: &'static [FloatReg]; - - const SHADOW_SPACE_SIZE: u8; - - fn general_callee_saved(reg: &GeneralReg) -> bool; - #[inline(always)] - fn general_caller_saved(reg: &GeneralReg) -> bool { - !Self::general_callee_saved(reg) - } - fn float_callee_saved(reg: &FloatReg) -> bool; - #[inline(always)] - fn float_caller_saved(reg: &FloatReg) -> bool { - !Self::float_callee_saved(reg) - } - - fn setup_stack<'a>( - buf: &mut Vec<'a, u8>, - general_saved_regs: &[GeneralReg], - float_saved_regs: &[FloatReg], - requested_stack_size: i32, - fn_call_stack_size: i32, - ) -> i32; - fn cleanup_stack<'a>( - buf: &mut Vec<'a, u8>, - general_saved_regs: &[GeneralReg], - float_saved_regs: &[FloatReg], - aligned_stack_size: i32, - fn_call_stack_size: i32, - ); - - /// load_args updates the storage manager to know where every arg is stored. - fn load_args<'a>( - buf: &mut Vec<'a, u8>, - storage_manager: &mut StorageManager<'a, GeneralReg, FloatReg, ASM, Self>, - args: &'a [(Layout<'a>, Symbol)], - // ret_layout is needed because if it is a complex type, we pass a pointer as the first arg. - ret_layout: &Layout<'a>, - ); - - /// store_args stores the args in registers and on the stack for function calling. - /// It also updates the amount of temporary stack space needed in the storage manager. - fn store_args<'a>( - buf: &mut Vec<'a, u8>, - storage_manager: &mut StorageManager<'a, GeneralReg, FloatReg, ASM, Self>, - dst: &Symbol, - args: &[Symbol], - arg_layouts: &[Layout<'a>], - // ret_layout is needed because if it is a complex type, we pass a pointer as the first arg. - ret_layout: &Layout<'a>, - ); - - /// return_complex_symbol returns the specified complex/non-primative symbol. - /// It uses the layout to determine how the data should be returned. - fn return_complex_symbol<'a>( - buf: &mut Vec<'a, u8>, - storage_manager: &mut StorageManager<'a, GeneralReg, FloatReg, ASM, Self>, - sym: &Symbol, - layout: &Layout<'a>, - ); - - /// load_returned_complex_symbol loads a complex symbol that was returned from a function call. - /// It uses the layout to determine how the data should be loaded into the symbol. - fn load_returned_complex_symbol<'a>( - buf: &mut Vec<'a, u8>, - storage_manager: &mut StorageManager<'a, GeneralReg, FloatReg, ASM, Self>, - sym: &Symbol, - layout: &Layout<'a>, - ); -} - -/// Assembler contains calls to the backend assembly generator. -/// These calls do not necessarily map directly to a single assembly instruction. -/// They are higher level in cases where an instruction would not be common and shared between multiple architectures. -/// Thus, some backends will need to use mulitiple instructions to preform a single one of this calls. -/// Generally, I prefer explicit sources, as opposed to dst being one of the sources. Ex: `x = x + y` would be `add x, x, y` instead of `add x, y`. -/// dst should always come before sources. -pub trait Assembler: Sized + Copy { - fn abs_reg64_reg64(buf: &mut Vec<'_, u8>, dst: GeneralReg, src: GeneralReg); - fn abs_freg64_freg64( - buf: &mut Vec<'_, u8>, - relocs: &mut Vec<'_, Relocation>, - dst: FloatReg, - src: FloatReg, - ); - - fn add_reg64_reg64_imm32(buf: &mut Vec<'_, u8>, dst: GeneralReg, src1: GeneralReg, imm32: i32); - fn add_freg64_freg64_freg64( - buf: &mut Vec<'_, u8>, - dst: FloatReg, - src1: FloatReg, - src2: FloatReg, - ); - fn add_reg64_reg64_reg64( - buf: &mut Vec<'_, u8>, - dst: GeneralReg, - src1: GeneralReg, - src2: GeneralReg, - ); - - fn call(buf: &mut Vec<'_, u8>, relocs: &mut Vec<'_, Relocation>, fn_name: String); - - /// Jumps by an offset of offset bytes unconditionally. - /// It should always generate the same number of bytes to enable replacement if offset changes. - /// It returns the base offset to calculate the jump from (generally the instruction after the jump). - fn jmp_imm32(buf: &mut Vec<'_, u8>, offset: i32) -> usize; - - fn tail_call(buf: &mut Vec<'_, u8>) -> u64; - - /// Jumps by an offset of offset bytes if reg is not equal to imm. - /// It should always generate the same number of bytes to enable replacement if offset changes. - /// It returns the base offset to calculate the jump from (generally the instruction after the jump). - fn jne_reg64_imm64_imm32( - buf: &mut Vec<'_, u8>, - reg: GeneralReg, - imm: u64, - offset: i32, - ) -> usize; - - fn mov_freg32_imm32( - buf: &mut Vec<'_, u8>, - relocs: &mut Vec<'_, Relocation>, - dst: FloatReg, - imm: f32, - ); - fn mov_freg64_imm64( - buf: &mut Vec<'_, u8>, - relocs: &mut Vec<'_, Relocation>, - dst: FloatReg, - imm: f64, - ); - fn mov_reg64_imm64(buf: &mut Vec<'_, u8>, dst: GeneralReg, imm: i64); - fn mov_freg64_freg64(buf: &mut Vec<'_, u8>, dst: FloatReg, src: FloatReg); - fn mov_reg64_reg64(buf: &mut Vec<'_, u8>, dst: GeneralReg, src: GeneralReg); - - // base32 is similar to stack based instructions but they reference the base/frame pointer. - fn mov_freg64_base32(buf: &mut Vec<'_, u8>, dst: FloatReg, offset: i32); - fn mov_reg64_base32(buf: &mut Vec<'_, u8>, dst: GeneralReg, offset: i32); - fn mov_base32_freg64(buf: &mut Vec<'_, u8>, offset: i32, src: FloatReg); - fn mov_base32_reg64(buf: &mut Vec<'_, u8>, offset: i32, src: GeneralReg); - - fn mov_reg64_mem64_offset32( - buf: &mut Vec<'_, u8>, - dst: GeneralReg, - src: GeneralReg, - offset: i32, - ); - fn mov_mem64_offset32_reg64( - buf: &mut Vec<'_, u8>, - dst: GeneralReg, - offset: i32, - src: GeneralReg, - ); - - /// Sign extends the data at `offset` with `size` as it copies it to `dst` - /// size must be less than or equal to 8. - fn movsx_reg64_base32(buf: &mut Vec<'_, u8>, dst: GeneralReg, offset: i32, size: u8); - /// Zero extends the data at `offset` with `size` as it copies it to `dst` - /// size must be less than or equal to 8. - fn movzx_reg64_base32(buf: &mut Vec<'_, u8>, dst: GeneralReg, offset: i32, size: u8); - - fn mov_freg64_stack32(buf: &mut Vec<'_, u8>, dst: FloatReg, offset: i32); - fn mov_reg64_stack32(buf: &mut Vec<'_, u8>, dst: GeneralReg, offset: i32); - fn mov_stack32_freg64(buf: &mut Vec<'_, u8>, offset: i32, src: FloatReg); - fn mov_stack32_reg64(buf: &mut Vec<'_, u8>, offset: i32, src: GeneralReg); - - fn neg_reg64_reg64(buf: &mut Vec<'_, u8>, dst: GeneralReg, src: GeneralReg); - fn imul_reg64_reg64_reg64( - buf: &mut Vec<'_, u8>, - dst: GeneralReg, - src1: GeneralReg, - src2: GeneralReg, - ); - - fn sub_reg64_reg64_imm32(buf: &mut Vec<'_, u8>, dst: GeneralReg, src1: GeneralReg, imm32: i32); - fn sub_reg64_reg64_reg64( - buf: &mut Vec<'_, u8>, - dst: GeneralReg, - src1: GeneralReg, - src2: GeneralReg, - ); - - fn eq_reg64_reg64_reg64( - buf: &mut Vec<'_, u8>, - dst: GeneralReg, - src1: GeneralReg, - src2: GeneralReg, - ); - - fn neq_reg64_reg64_reg64( - buf: &mut Vec<'_, u8>, - dst: GeneralReg, - src1: GeneralReg, - src2: GeneralReg, - ); - - fn lt_reg64_reg64_reg64( - buf: &mut Vec<'_, u8>, - dst: GeneralReg, - src1: GeneralReg, - src2: GeneralReg, - ); - - fn to_float_freg32_reg64(buf: &mut Vec<'_, u8>, dst: FloatReg, src: GeneralReg); - - fn to_float_freg64_reg64(buf: &mut Vec<'_, u8>, dst: FloatReg, src: GeneralReg); - - fn to_float_freg32_freg64(buf: &mut Vec<'_, u8>, dst: FloatReg, src: FloatReg); - - fn to_float_freg64_freg32(buf: &mut Vec<'_, u8>, dst: FloatReg, src: FloatReg); - - fn lte_reg64_reg64_reg64( - buf: &mut Vec<'_, u8>, - dst: GeneralReg, - src1: GeneralReg, - src2: GeneralReg, - ); - - fn gte_reg64_reg64_reg64( - buf: &mut Vec<'_, u8>, - dst: GeneralReg, - src1: GeneralReg, - src2: GeneralReg, - ); - - fn ret(buf: &mut Vec<'_, u8>); -} - -pub trait RegTrait: Copy + PartialEq + Eq + std::hash::Hash + std::fmt::Debug + 'static { - fn value(&self) -> u8; -} - -pub struct Backend64Bit< - 'a, - GeneralReg: RegTrait, - FloatReg: RegTrait, - ASM: Assembler, - CC: CallConv, -> { - // TODO: A number of the uses of MutMap could probably be some form of linear mutmap - // They are likely to be small enough that it is faster to use a vec and linearly scan it or keep it sorted and binary search. - phantom_asm: PhantomData, - phantom_cc: PhantomData, - env: &'a Env<'a>, - interns: &'a mut Interns, - helper_proc_gen: CodeGenHelp<'a>, - helper_proc_symbols: Vec<'a, (Symbol, ProcLayout<'a>)>, - buf: Vec<'a, u8>, - relocs: Vec<'a, Relocation>, - proc_name: Option, - is_self_recursive: Option, - - last_seen_map: MutMap>, - layout_map: MutMap>, - free_map: MutMap<*const Stmt<'a>, Vec<'a, Symbol>>, - - literal_map: MutMap, *const Layout<'a>)>, - join_map: MutMap>, - - storage_manager: StorageManager<'a, GeneralReg, FloatReg, ASM, CC>, -} - -/// new creates a new backend that will output to the specific Object. -pub fn new_backend_64bit< - 'a, - GeneralReg: RegTrait, - FloatReg: RegTrait, - ASM: Assembler, - CC: CallConv, ->( - env: &'a Env, - target_info: TargetInfo, - interns: &'a mut Interns, -) -> Backend64Bit<'a, GeneralReg, FloatReg, ASM, CC> { - Backend64Bit { - phantom_asm: PhantomData, - phantom_cc: PhantomData, - env, - interns, - helper_proc_gen: CodeGenHelp::new(env.arena, target_info, env.module_id), - helper_proc_symbols: bumpalo::vec![in env.arena], - proc_name: None, - is_self_recursive: None, - buf: bumpalo::vec![in env.arena], - relocs: bumpalo::vec![in env.arena], - last_seen_map: MutMap::default(), - layout_map: MutMap::default(), - free_map: MutMap::default(), - literal_map: MutMap::default(), - join_map: MutMap::default(), - storage_manager: storage::new_storage_manager(env, target_info), - } -} - -impl< - 'a, - GeneralReg: RegTrait, - FloatReg: RegTrait, - ASM: Assembler, - CC: CallConv, - > Backend<'a> for Backend64Bit<'a, GeneralReg, FloatReg, ASM, CC> -{ - fn env(&self) -> &Env<'a> { - self.env - } - fn interns(&self) -> &Interns { - self.interns - } - fn env_interns_helpers_mut(&mut self) -> (&Env<'a>, &mut Interns, &mut CodeGenHelp<'a>) { - (self.env, self.interns, &mut self.helper_proc_gen) - } - fn helper_proc_gen_mut(&mut self) -> &mut CodeGenHelp<'a> { - &mut self.helper_proc_gen - } - fn helper_proc_symbols_mut(&mut self) -> &mut Vec<'a, (Symbol, ProcLayout<'a>)> { - &mut self.helper_proc_symbols - } - fn helper_proc_symbols(&self) -> &Vec<'a, (Symbol, ProcLayout<'a>)> { - &self.helper_proc_symbols - } - - fn reset(&mut self, name: String, is_self_recursive: SelfRecursive) { - self.proc_name = Some(name); - self.is_self_recursive = Some(is_self_recursive); - self.last_seen_map.clear(); - self.layout_map.clear(); - self.join_map.clear(); - self.free_map.clear(); - self.buf.clear(); - self.storage_manager.reset(); - } - - fn literal_map(&mut self) -> &mut MutMap, *const Layout<'a>)> { - &mut self.literal_map - } - - fn last_seen_map(&mut self) -> &mut MutMap> { - &mut self.last_seen_map - } - - fn layout_map(&mut self) -> &mut MutMap> { - &mut self.layout_map - } - - fn set_free_map(&mut self, map: MutMap<*const Stmt<'a>, Vec<'a, Symbol>>) { - self.free_map = map; - } - - fn free_map(&mut self) -> &mut MutMap<*const Stmt<'a>, Vec<'a, Symbol>> { - &mut self.free_map - } - - fn finalize(&mut self) -> (Vec, Vec) { - let mut out = bumpalo::vec![in self.env.arena]; - - // Setup stack. - let used_general_regs = self.storage_manager.general_used_callee_saved_regs(); - let used_float_regs = self.storage_manager.float_used_callee_saved_regs(); - let aligned_stack_size = CC::setup_stack( - &mut out, - &used_general_regs, - &used_float_regs, - self.storage_manager.stack_size() as i32, - self.storage_manager.fn_call_stack_size() as i32, - ); - let setup_offset = out.len(); - - // Deal with jumps to the return address. - let old_relocs = std::mem::replace(&mut self.relocs, bumpalo::vec![in self.env.arena]); - - // Check if their is an unnessary jump to return right at the end of the function. - let mut end_jmp_size = 0; - for reloc in old_relocs - .iter() - .filter(|reloc| matches!(reloc, Relocation::JmpToReturn { .. })) - { - if let Relocation::JmpToReturn { - inst_loc, - inst_size, - .. - } = reloc - { - if *inst_loc as usize + *inst_size as usize == self.buf.len() { - end_jmp_size = *inst_size as usize; - break; - } - } - } - - // Update jumps to returns. - let ret_offset = self.buf.len() - end_jmp_size; - let mut tmp = bumpalo::vec![in self.env.arena]; - for reloc in old_relocs - .iter() - .filter(|reloc| matches!(reloc, Relocation::JmpToReturn { .. })) - { - if let Relocation::JmpToReturn { - inst_loc, - inst_size, - offset, - } = reloc - { - if *inst_loc as usize + *inst_size as usize != self.buf.len() { - self.update_jmp_imm32_offset(&mut tmp, *inst_loc, *offset, ret_offset as u64); - } - } - } - - // Add function body. - out.extend(&self.buf[..self.buf.len() - end_jmp_size]); - - // Cleanup stack. - CC::cleanup_stack( - &mut out, - &used_general_regs, - &used_float_regs, - aligned_stack_size, - self.storage_manager.fn_call_stack_size() as i32, - ); - ASM::ret(&mut out); - - // Update other relocs to include stack setup offset. - let mut out_relocs = bumpalo::vec![in self.env.arena]; - out_relocs.extend( - old_relocs - .into_iter() - .filter(|reloc| !matches!(reloc, Relocation::JmpToReturn { .. })) - .map(|reloc| match reloc { - Relocation::LocalData { offset, data } => Relocation::LocalData { - offset: offset + setup_offset as u64, - data, - }, - Relocation::LinkedData { offset, name } => Relocation::LinkedData { - offset: offset + setup_offset as u64, - name, - }, - Relocation::LinkedFunction { offset, name } => Relocation::LinkedFunction { - offset: offset + setup_offset as u64, - name, - }, - Relocation::JmpToReturn { .. } => unreachable!(), - }), - ); - (out, out_relocs) - } - - fn load_args(&mut self, args: &'a [(Layout<'a>, Symbol)], ret_layout: &Layout<'a>) { - CC::load_args(&mut self.buf, &mut self.storage_manager, args, ret_layout); - } - - /// Used for generating wrappers for malloc/realloc/free - fn build_wrapped_jmp(&mut self) -> (&'a [u8], u64) { - let mut out = bumpalo::vec![in self.env.arena]; - let offset = ASM::tail_call(&mut out); - - (out.into_bump_slice(), offset) - } - - fn build_fn_call( - &mut self, - dst: &Symbol, - fn_name: String, - args: &[Symbol], - arg_layouts: &[Layout<'a>], - ret_layout: &Layout<'a>, - ) { - if let Some(SelfRecursive::SelfRecursive(id)) = self.is_self_recursive { - if &fn_name == self.proc_name.as_ref().unwrap() && self.join_map.contains_key(&id) { - return self.build_jump(&id, args, arg_layouts, ret_layout); - } - } - // Save used caller saved regs. - self.storage_manager - .push_used_caller_saved_regs_to_stack(&mut self.buf); - - // Put values in param regs or on top of the stack. - CC::store_args( - &mut self.buf, - &mut self.storage_manager, - dst, - args, - arg_layouts, - ret_layout, - ); - - // Call function and generate reloc. - ASM::call(&mut self.buf, &mut self.relocs, fn_name); - - // move return value to dst. - match ret_layout { - single_register_integers!() => { - let dst_reg = self.storage_manager.claim_general_reg(&mut self.buf, dst); - ASM::mov_reg64_reg64(&mut self.buf, dst_reg, CC::GENERAL_RETURN_REGS[0]); - } - single_register_floats!() => { - let dst_reg = self.storage_manager.claim_float_reg(&mut self.buf, dst); - ASM::mov_freg64_freg64(&mut self.buf, dst_reg, CC::FLOAT_RETURN_REGS[0]); - } - _ => { - CC::load_returned_complex_symbol( - &mut self.buf, - &mut self.storage_manager, - dst, - ret_layout, - ); - } - } - } - - fn build_switch( - &mut self, - cond_symbol: &Symbol, - _cond_layout: &Layout<'a>, // cond_layout must be a integer due to potential jump table optimizations. - branches: &'a [(u64, BranchInfo<'a>, Stmt<'a>)], - default_branch: &(BranchInfo<'a>, &'a Stmt<'a>), - ret_layout: &Layout<'a>, - ) { - // Switches are a little complex due to keeping track of jumps. - // In general I am trying to not have to loop over things multiple times or waste memory. - // The basic plan is to make jumps to nowhere and then correct them once we know the correct address. - let cond_reg = self - .storage_manager - .load_to_general_reg(&mut self.buf, cond_symbol); - - let mut base_storage = self.storage_manager.clone(); - let mut max_branch_stack_size = 0; - let mut ret_jumps = bumpalo::vec![in self.env.arena]; - let mut tmp = bumpalo::vec![in self.env.arena]; - for (val, _branch_info, stmt) in branches.iter() { - // TODO: look into branch info and if it matters here. - tmp.clear(); - // Create jump to next branch if cond_sym not equal to value. - // Since we don't know the offset yet, set it to 0 and overwrite later. - let jne_location = self.buf.len(); - let start_offset = ASM::jne_reg64_imm64_imm32(&mut self.buf, cond_reg, *val, 0); - - // Build all statements in this branch. Using storage as from before any branch. - self.storage_manager = base_storage.clone(); - self.build_stmt(stmt, ret_layout); - - // Build unconditional jump to the end of this switch. - // Since we don't know the offset yet, set it to 0 and overwrite later. - let jmp_location = self.buf.len(); - let jmp_offset = ASM::jmp_imm32(&mut self.buf, 0x1234_5678); - ret_jumps.push((jmp_location, jmp_offset)); - - // Overwrite the original jne with the correct offset. - let end_offset = self.buf.len(); - let jne_offset = end_offset - start_offset; - ASM::jne_reg64_imm64_imm32(&mut tmp, cond_reg, *val, jne_offset as i32); - for (i, byte) in tmp.iter().enumerate() { - self.buf[jne_location + i] = *byte; - } - - // Update important storage information to avoid overwrites. - max_branch_stack_size = - std::cmp::max(max_branch_stack_size, self.storage_manager.stack_size()); - base_storage.update_fn_call_stack_size(self.storage_manager.fn_call_stack_size()); - } - self.storage_manager = base_storage; - self.storage_manager - .update_stack_size(max_branch_stack_size); - let (_branch_info, stmt) = default_branch; - self.build_stmt(stmt, ret_layout); - - // Update all return jumps to jump past the default case. - let ret_offset = self.buf.len(); - for (jmp_location, start_offset) in ret_jumps.into_iter() { - self.update_jmp_imm32_offset( - &mut tmp, - jmp_location as u64, - start_offset as u64, - ret_offset as u64, - ); - } - } - - fn build_join( - &mut self, - id: &JoinPointId, - parameters: &'a [Param<'a>], - body: &'a Stmt<'a>, - remainder: &'a Stmt<'a>, - ret_layout: &Layout<'a>, - ) { - // Free everything to the stack to make sure they don't get messed up when looping back to this point. - // TODO: look into a nicer solution. - self.storage_manager.free_all_to_stack(&mut self.buf); - - // Ensure all the joinpoint parameters have storage locations. - // On jumps to the joinpoint, we will overwrite those locations as a way to "pass parameters" to the joinpoint. - self.storage_manager - .setup_joinpoint(&mut self.buf, id, parameters); - - self.join_map.insert(*id, bumpalo::vec![in self.env.arena]); - - // Build remainder of function first. It is what gets run and jumps to join. - self.build_stmt(remainder, ret_layout); - - let join_location = self.buf.len() as u64; - - // Build all statements in body. - self.build_stmt(body, ret_layout); - - // Overwrite the all jumps to the joinpoint with the correct offset. - let mut tmp = bumpalo::vec![in self.env.arena]; - for (jmp_location, start_offset) in self - .join_map - .remove(id) - .unwrap_or_else(|| internal_error!("join point not defined")) - { - tmp.clear(); - self.update_jmp_imm32_offset(&mut tmp, jmp_location, start_offset, join_location); - } - } - - fn build_jump( - &mut self, - id: &JoinPointId, - args: &[Symbol], - arg_layouts: &[Layout<'a>], - _ret_layout: &Layout<'a>, - ) { - self.storage_manager - .setup_jump(&mut self.buf, id, args, arg_layouts); - - let jmp_location = self.buf.len(); - let start_offset = ASM::jmp_imm32(&mut self.buf, 0x1234_5678); - - if let Some(vec) = self.join_map.get_mut(id) { - vec.push((jmp_location as u64, start_offset as u64)) - } else { - internal_error!("Jump: unknown point specified to jump to: {:?}", id); - } - } - - fn build_num_abs(&mut self, dst: &Symbol, src: &Symbol, layout: &Layout<'a>) { - match layout { - Layout::Builtin(Builtin::Int(IntWidth::I64 | IntWidth::U64)) => { - let dst_reg = self.storage_manager.claim_general_reg(&mut self.buf, dst); - let src_reg = self.storage_manager.load_to_general_reg(&mut self.buf, src); - ASM::abs_reg64_reg64(&mut self.buf, dst_reg, src_reg); - } - Layout::Builtin(Builtin::Float(FloatWidth::F64)) => { - let dst_reg = self.storage_manager.claim_float_reg(&mut self.buf, dst); - let src_reg = self.storage_manager.load_to_float_reg(&mut self.buf, src); - ASM::abs_freg64_freg64(&mut self.buf, &mut self.relocs, dst_reg, src_reg); - } - x => todo!("NumAbs: layout, {:?}", x), - } - } - - fn build_num_add(&mut self, dst: &Symbol, src1: &Symbol, src2: &Symbol, layout: &Layout<'a>) { - match layout { - Layout::Builtin(Builtin::Int(IntWidth::I64 | IntWidth::U64)) => { - let dst_reg = self.storage_manager.claim_general_reg(&mut self.buf, dst); - let src1_reg = self - .storage_manager - .load_to_general_reg(&mut self.buf, src1); - let src2_reg = self - .storage_manager - .load_to_general_reg(&mut self.buf, src2); - ASM::add_reg64_reg64_reg64(&mut self.buf, dst_reg, src1_reg, src2_reg); - } - Layout::Builtin(Builtin::Float(FloatWidth::F64)) => { - let dst_reg = self.storage_manager.claim_float_reg(&mut self.buf, dst); - let src1_reg = self.storage_manager.load_to_float_reg(&mut self.buf, src1); - let src2_reg = self.storage_manager.load_to_float_reg(&mut self.buf, src2); - ASM::add_freg64_freg64_freg64(&mut self.buf, dst_reg, src1_reg, src2_reg); - } - x => todo!("NumAdd: layout, {:?}", x), - } - } - - fn build_num_mul(&mut self, dst: &Symbol, src1: &Symbol, src2: &Symbol, layout: &Layout<'a>) { - match layout { - Layout::Builtin(Builtin::Int(IntWidth::I64 | IntWidth::U64)) => { - let dst_reg = self.storage_manager.claim_general_reg(&mut self.buf, dst); - let src1_reg = self - .storage_manager - .load_to_general_reg(&mut self.buf, src1); - let src2_reg = self - .storage_manager - .load_to_general_reg(&mut self.buf, src2); - ASM::imul_reg64_reg64_reg64(&mut self.buf, dst_reg, src1_reg, src2_reg); - } - x => todo!("NumMul: layout, {:?}", x), - } - } - - fn build_num_neg(&mut self, dst: &Symbol, src: &Symbol, layout: &Layout<'a>) { - match layout { - Layout::Builtin(Builtin::Int(IntWidth::I64 | IntWidth::U64)) => { - let dst_reg = self.storage_manager.claim_general_reg(&mut self.buf, dst); - let src_reg = self.storage_manager.load_to_general_reg(&mut self.buf, src); - ASM::neg_reg64_reg64(&mut self.buf, dst_reg, src_reg); - } - x => todo!("NumNeg: layout, {:?}", x), - } - } - - fn build_num_sub(&mut self, dst: &Symbol, src1: &Symbol, src2: &Symbol, layout: &Layout<'a>) { - match layout { - Layout::Builtin(Builtin::Int(IntWidth::I64 | IntWidth::U64)) => { - let dst_reg = self.storage_manager.claim_general_reg(&mut self.buf, dst); - let src1_reg = self - .storage_manager - .load_to_general_reg(&mut self.buf, src1); - let src2_reg = self - .storage_manager - .load_to_general_reg(&mut self.buf, src2); - ASM::sub_reg64_reg64_reg64(&mut self.buf, dst_reg, src1_reg, src2_reg); - } - x => todo!("NumSub: layout, {:?}", x), - } - } - - fn build_eq(&mut self, dst: &Symbol, src1: &Symbol, src2: &Symbol, arg_layout: &Layout<'a>) { - match arg_layout { - Layout::Builtin(single_register_int_builtins!()) => { - let dst_reg = self.storage_manager.claim_general_reg(&mut self.buf, dst); - let src1_reg = self - .storage_manager - .load_to_general_reg(&mut self.buf, src1); - let src2_reg = self - .storage_manager - .load_to_general_reg(&mut self.buf, src2); - ASM::eq_reg64_reg64_reg64(&mut self.buf, dst_reg, src1_reg, src2_reg); - } - x => todo!("NumEq: layout, {:?}", x), - } - } - - fn build_neq(&mut self, dst: &Symbol, src1: &Symbol, src2: &Symbol, arg_layout: &Layout<'a>) { - match arg_layout { - Layout::Builtin(Builtin::Int(IntWidth::I64 | IntWidth::U64)) => { - let dst_reg = self.storage_manager.claim_general_reg(&mut self.buf, dst); - let src1_reg = self - .storage_manager - .load_to_general_reg(&mut self.buf, src1); - let src2_reg = self - .storage_manager - .load_to_general_reg(&mut self.buf, src2); - ASM::neq_reg64_reg64_reg64(&mut self.buf, dst_reg, src1_reg, src2_reg); - } - x => todo!("NumNeq: layout, {:?}", x), - } - } - - fn build_num_lt( - &mut self, - dst: &Symbol, - src1: &Symbol, - src2: &Symbol, - arg_layout: &Layout<'a>, - ) { - match arg_layout { - Layout::Builtin(Builtin::Int(IntWidth::I64 | IntWidth::U64)) => { - let dst_reg = self.storage_manager.claim_general_reg(&mut self.buf, dst); - let src1_reg = self - .storage_manager - .load_to_general_reg(&mut self.buf, src1); - let src2_reg = self - .storage_manager - .load_to_general_reg(&mut self.buf, src2); - ASM::lt_reg64_reg64_reg64(&mut self.buf, dst_reg, src1_reg, src2_reg); - } - x => todo!("NumLt: layout, {:?}", x), - } - } - - fn build_num_to_frac( - &mut self, - dst: &Symbol, - src: &Symbol, - arg_layout: &Layout<'a>, - ret_layout: &Layout<'a>, - ) { - let dst_reg = self.storage_manager.claim_float_reg(&mut self.buf, dst); - match (arg_layout, ret_layout) { - ( - Layout::Builtin(Builtin::Int(IntWidth::I32 | IntWidth::I64)), - Layout::Builtin(Builtin::Float(FloatWidth::F64)), - ) => { - let src_reg = self.storage_manager.load_to_general_reg(&mut self.buf, src); - ASM::to_float_freg64_reg64(&mut self.buf, dst_reg, src_reg); - } - ( - Layout::Builtin(Builtin::Int(IntWidth::I32 | IntWidth::I64)), - Layout::Builtin(Builtin::Float(FloatWidth::F32)), - ) => { - let src_reg = self.storage_manager.load_to_general_reg(&mut self.buf, src); - ASM::to_float_freg32_reg64(&mut self.buf, dst_reg, src_reg); - } - ( - Layout::Builtin(Builtin::Float(FloatWidth::F64)), - Layout::Builtin(Builtin::Float(FloatWidth::F32)), - ) => { - let src_reg = self.storage_manager.load_to_float_reg(&mut self.buf, src); - ASM::to_float_freg32_freg64(&mut self.buf, dst_reg, src_reg); - } - ( - Layout::Builtin(Builtin::Float(FloatWidth::F32)), - Layout::Builtin(Builtin::Float(FloatWidth::F64)), - ) => { - let src_reg = self.storage_manager.load_to_float_reg(&mut self.buf, src); - ASM::to_float_freg64_freg32(&mut self.buf, dst_reg, src_reg); - } - ( - Layout::Builtin(Builtin::Float(FloatWidth::F64)), - Layout::Builtin(Builtin::Float(FloatWidth::F64)), - ) => { - let src_reg = self.storage_manager.load_to_float_reg(&mut self.buf, src); - ASM::mov_freg64_freg64(&mut self.buf, dst_reg, src_reg); - } - ( - Layout::Builtin(Builtin::Float(FloatWidth::F32)), - Layout::Builtin(Builtin::Float(FloatWidth::F32)), - ) => { - let src_reg = self.storage_manager.load_to_float_reg(&mut self.buf, src); - ASM::mov_freg64_freg64(&mut self.buf, dst_reg, src_reg); - } - (a, r) => todo!("NumToFrac: layout, arg {:?}, ret {:?}", a, r), - } - } - - fn build_num_lte( - &mut self, - dst: &Symbol, - src1: &Symbol, - src2: &Symbol, - arg_layout: &Layout<'a>, - ) { - match arg_layout { - Layout::Builtin(single_register_int_builtins!()) => { - let dst_reg = self.storage_manager.claim_general_reg(&mut self.buf, dst); - let src1_reg = self - .storage_manager - .load_to_general_reg(&mut self.buf, src1); - let src2_reg = self - .storage_manager - .load_to_general_reg(&mut self.buf, src2); - ASM::lte_reg64_reg64_reg64(&mut self.buf, dst_reg, src1_reg, src2_reg); - } - x => todo!("NumLte: layout, {:?}", x), - } - } - - fn build_num_gte( - &mut self, - dst: &Symbol, - src1: &Symbol, - src2: &Symbol, - arg_layout: &Layout<'a>, - ) { - match arg_layout { - Layout::Builtin(single_register_int_builtins!()) => { - let dst_reg = self.storage_manager.claim_general_reg(&mut self.buf, dst); - let src1_reg = self - .storage_manager - .load_to_general_reg(&mut self.buf, src1); - let src2_reg = self - .storage_manager - .load_to_general_reg(&mut self.buf, src2); - ASM::gte_reg64_reg64_reg64(&mut self.buf, dst_reg, src1_reg, src2_reg); - } - x => todo!("NumGte: layout, {:?}", x), - } - } - - fn build_list_len(&mut self, dst: &Symbol, list: &Symbol) { - self.storage_manager.list_len(&mut self.buf, dst, list); - } - - fn build_list_get_unsafe( - &mut self, - dst: &Symbol, - list: &Symbol, - index: &Symbol, - ret_layout: &Layout<'a>, - ) { - let (base_offset, _) = self.storage_manager.stack_offset_and_size(list); - let index_reg = self - .storage_manager - .load_to_general_reg(&mut self.buf, index); - let ret_stack_size = ret_layout.stack_size(self.storage_manager.target_info()); - // TODO: This can be optimized with smarter instructions. - // Also can probably be moved into storage manager at least partly. - self.storage_manager.with_tmp_general_reg( - &mut self.buf, - |storage_manager, buf, list_ptr| { - ASM::mov_reg64_base32(buf, list_ptr, base_offset as i32); - storage_manager.with_tmp_general_reg(buf, |storage_manager, buf, tmp| { - ASM::mov_reg64_imm64(buf, tmp, ret_stack_size as i64); - ASM::imul_reg64_reg64_reg64(buf, tmp, tmp, index_reg); - ASM::add_reg64_reg64_reg64(buf, tmp, tmp, list_ptr); - match ret_layout { - single_register_integers!() if ret_stack_size == 8 => { - let dst_reg = storage_manager.claim_general_reg(buf, dst); - ASM::mov_reg64_mem64_offset32(buf, dst_reg, tmp, 0); - } - x => internal_error!("Loading list element with layout: {:?}", x), - } - }); - }, - ); - } - - fn build_list_replace_unsafe( - &mut self, - dst: &Symbol, - args: &'a [Symbol], - arg_layouts: &[Layout<'a>], - ret_layout: &Layout<'a>, - ) { - // We want to delegate to the zig builtin, but it takes some extra parameters. - // Firstly, it takes the alignment of the list. - // Secondly, it takes the stack size of an element. - // Thirdly, it takes a pointer that it will write the output element to. - let list = args[0]; - let list_layout = arg_layouts[0]; - let index = args[1]; - let index_layout = arg_layouts[1]; - let elem = args[2]; - let elem_layout = arg_layouts[2]; - - let u32_layout = &Layout::Builtin(Builtin::Int(IntWidth::U32)); - let list_alignment = list_layout.alignment_bytes(self.storage_manager.target_info()); - self.load_literal( - &Symbol::DEV_TMP, - u32_layout, - &Literal::Int((list_alignment as i128).to_ne_bytes()), - ); - - // Have to pass the input element by pointer, so put it on the stack and load it's address. - self.storage_manager - .ensure_symbol_on_stack(&mut self.buf, &elem); - let u64_layout = &Layout::Builtin(Builtin::Int(IntWidth::U64)); - let (new_elem_offset, _) = self.storage_manager.stack_offset_and_size(&elem); - // Load address of output element into register. - let reg = self - .storage_manager - .claim_general_reg(&mut self.buf, &Symbol::DEV_TMP2); - ASM::add_reg64_reg64_imm32(&mut self.buf, reg, CC::BASE_PTR_REG, new_elem_offset); - - // Load the elements size. - let elem_stack_size = elem_layout.stack_size(self.storage_manager.target_info()); - self.load_literal( - &Symbol::DEV_TMP3, - u64_layout, - &Literal::Int((elem_stack_size as i128).to_ne_bytes()), - ); - - // Setup the return location. - let base_offset = self.storage_manager.claim_stack_area( - dst, - ret_layout.stack_size(self.storage_manager.target_info()), - ); - - let ret_fields = if let Layout::Struct { field_layouts, .. } = ret_layout { - field_layouts - } else { - internal_error!( - "Expected replace to return a struct instead found: {:?}", - ret_layout - ) - }; - - // Only return list and old element. - debug_assert_eq!(ret_fields.len(), 2); - - let (out_list_offset, out_elem_offset) = if ret_fields[0] == elem_layout { - ( - base_offset + ret_fields[0].stack_size(self.storage_manager.target_info()) as i32, - base_offset, - ) - } else { - ( - base_offset, - base_offset + ret_fields[0].stack_size(self.storage_manager.target_info()) as i32, - ) - }; - - // Load address of output element into register. - let reg = self - .storage_manager - .claim_general_reg(&mut self.buf, &Symbol::DEV_TMP4); - ASM::add_reg64_reg64_imm32(&mut self.buf, reg, CC::BASE_PTR_REG, out_elem_offset); - - let lowlevel_args = bumpalo::vec![ - in self.env.arena; - list, - Symbol::DEV_TMP, - index, - Symbol::DEV_TMP2, - Symbol::DEV_TMP3, - Symbol::DEV_TMP4, - ]; - let lowlevel_arg_layouts = bumpalo::vec![ - in self.env.arena; - list_layout, - *u32_layout, - index_layout, - *u64_layout, - *u64_layout, - *u64_layout, - ]; - - self.build_fn_call( - &Symbol::DEV_TMP5, - bitcode::LIST_REPLACE.to_string(), - &lowlevel_args, - &lowlevel_arg_layouts, - &list_layout, - ); - self.free_symbol(&Symbol::DEV_TMP); - self.free_symbol(&Symbol::DEV_TMP2); - self.free_symbol(&Symbol::DEV_TMP3); - self.free_symbol(&Symbol::DEV_TMP4); - - // Copy from list to the output record. - self.storage_manager.copy_symbol_to_stack_offset( - &mut self.buf, - out_list_offset, - &Symbol::DEV_TMP5, - &list_layout, - ); - - self.free_symbol(&Symbol::DEV_TMP5); - } - - fn build_ptr_cast(&mut self, dst: &Symbol, src: &Symbol) { - let dst_reg = self.storage_manager.claim_general_reg(&mut self.buf, dst); - self.storage_manager - .ensure_symbol_on_stack(&mut self.buf, src); - let (offset, _) = self.storage_manager.stack_offset_and_size(src); - ASM::add_reg64_reg64_imm32(&mut self.buf, dst_reg, CC::BASE_PTR_REG, offset); - } - - fn create_struct(&mut self, sym: &Symbol, layout: &Layout<'a>, fields: &'a [Symbol]) { - self.storage_manager - .create_struct(&mut self.buf, sym, layout, fields); - } - - fn load_struct_at_index( - &mut self, - sym: &Symbol, - structure: &Symbol, - index: u64, - field_layouts: &'a [Layout<'a>], - ) { - self.storage_manager - .load_field_at_index(sym, structure, index, field_layouts); - } - - fn load_union_at_index( - &mut self, - sym: &Symbol, - structure: &Symbol, - tag_id: TagIdIntType, - index: u64, - union_layout: &UnionLayout<'a>, - ) { - match union_layout { - UnionLayout::NonRecursive(tag_layouts) | UnionLayout::Recursive(tag_layouts) => { - self.storage_manager.load_field_at_index( - sym, - structure, - index, - tag_layouts[tag_id as usize], - ); - } - x => todo!("loading from union type: {:?}", x), - } - } - - fn get_tag_id(&mut self, sym: &Symbol, structure: &Symbol, union_layout: &UnionLayout<'a>) { - self.storage_manager - .load_union_tag_id(&mut self.buf, sym, structure, union_layout); - } - - fn tag( - &mut self, - sym: &Symbol, - fields: &'a [Symbol], - union_layout: &UnionLayout<'a>, - tag_id: TagIdIntType, - ) { - self.storage_manager - .create_union(&mut self.buf, sym, union_layout, fields, tag_id) - } - - fn load_literal(&mut self, sym: &Symbol, layout: &Layout<'a>, lit: &Literal<'a>) { - match (lit, layout) { - ( - Literal::Int(x), - Layout::Builtin(Builtin::Int( - IntWidth::U8 - | IntWidth::U16 - | IntWidth::U32 - | IntWidth::U64 - | IntWidth::I8 - | IntWidth::I16 - | IntWidth::I32 - | IntWidth::I64, - )), - ) => { - let reg = self.storage_manager.claim_general_reg(&mut self.buf, sym); - let val = *x; - ASM::mov_reg64_imm64(&mut self.buf, reg, i128::from_ne_bytes(val) as i64); - } - (Literal::Float(x), Layout::Builtin(Builtin::Float(FloatWidth::F64))) => { - let reg = self.storage_manager.claim_float_reg(&mut self.buf, sym); - let val = *x; - ASM::mov_freg64_imm64(&mut self.buf, &mut self.relocs, reg, val); - } - (Literal::Float(x), Layout::Builtin(Builtin::Float(FloatWidth::F32))) => { - let reg = self.storage_manager.claim_float_reg(&mut self.buf, sym); - let val = *x as f32; - ASM::mov_freg32_imm32(&mut self.buf, &mut self.relocs, reg, val); - } - (Literal::Str(x), Layout::Builtin(Builtin::Str)) if x.len() < 24 => { - // Load small string. - self.storage_manager.with_tmp_general_reg( - &mut self.buf, - |storage_manager, buf, reg| { - let base_offset = storage_manager.claim_stack_area(sym, 24); - let mut bytes = [0; 24]; - bytes[..x.len()].copy_from_slice(x.as_bytes()); - bytes[23] = (x.len() as u8) | 0b1000_0000; - - let mut num_bytes = [0; 8]; - num_bytes.copy_from_slice(&bytes[..8]); - let num = i64::from_ne_bytes(num_bytes); - ASM::mov_reg64_imm64(buf, reg, num); - ASM::mov_base32_reg64(buf, base_offset, reg); - - num_bytes.copy_from_slice(&bytes[8..16]); - let num = i64::from_ne_bytes(num_bytes); - ASM::mov_reg64_imm64(buf, reg, num); - ASM::mov_base32_reg64(buf, base_offset + 8, reg); - - num_bytes.copy_from_slice(&bytes[16..]); - let num = i64::from_ne_bytes(num_bytes); - ASM::mov_reg64_imm64(buf, reg, num); - ASM::mov_base32_reg64(buf, base_offset + 16, reg); - }, - ); - } - x => todo!("loading literal, {:?}", x), - } - } - - fn free_symbol(&mut self, sym: &Symbol) { - self.join_map.remove(&JoinPointId(*sym)); - self.storage_manager.free_symbol(sym); - } - - fn return_symbol(&mut self, sym: &Symbol, layout: &Layout<'a>) { - if self.storage_manager.is_stored_primitive(sym) { - // Just load it to the correct type of reg as a stand alone value. - match layout { - single_register_integers!() => { - self.storage_manager.load_to_specified_general_reg( - &mut self.buf, - sym, - CC::GENERAL_RETURN_REGS[0], - ); - } - single_register_floats!() => { - self.storage_manager.load_to_specified_float_reg( - &mut self.buf, - sym, - CC::FLOAT_RETURN_REGS[0], - ); - } - _ => { - internal_error!("All primitive valuse should fit in a single register"); - } - } - } else { - CC::return_complex_symbol(&mut self.buf, &mut self.storage_manager, sym, layout) - } - let inst_loc = self.buf.len() as u64; - let offset = ASM::jmp_imm32(&mut self.buf, 0x1234_5678) as u64; - self.relocs.push(Relocation::JmpToReturn { - inst_loc, - inst_size: self.buf.len() as u64 - inst_loc, - offset, - }); - } -} - -/// This impl block is for ir related instructions that need backend specific information. -/// For example, loading a symbol for doing a computation. -impl< - 'a, - FloatReg: RegTrait, - GeneralReg: RegTrait, - ASM: Assembler, - CC: CallConv, - > Backend64Bit<'a, GeneralReg, FloatReg, ASM, CC> -{ - /// Updates a jump instruction to a new offset and returns the number of bytes written. - fn update_jmp_imm32_offset( - &mut self, - tmp: &mut Vec<'a, u8>, - jmp_location: u64, - base_offset: u64, - target_offset: u64, - ) { - tmp.clear(); - let jmp_offset = target_offset as i32 - base_offset as i32; - ASM::jmp_imm32(tmp, jmp_offset); - for (i, byte) in tmp.iter().enumerate() { - self.buf[jmp_location as usize + i] = *byte; - } - } -} - -#[macro_export] -macro_rules! sign_extended_int_builtins { - () => { - Builtin::Int(IntWidth::I8 | IntWidth::I16 | IntWidth::I32 | IntWidth::I64 | IntWidth::I128) - }; -} - -#[macro_export] -macro_rules! zero_extended_int_builtins { - () => { - Builtin::Int(IntWidth::U8 | IntWidth::U16 | IntWidth::U32 | IntWidth::U64 | IntWidth::U128) - }; -} - -#[macro_export] -macro_rules! single_register_int_builtins { - () => { - Builtin::Int( - IntWidth::I8 - | IntWidth::I16 - | IntWidth::I32 - | IntWidth::I64 - | IntWidth::U8 - | IntWidth::U16 - | IntWidth::U32 - | IntWidth::U64, - ) - }; -} - -#[macro_export] -macro_rules! single_register_integers { - () => { - Layout::Builtin(Builtin::Bool | single_register_int_builtins!()) | Layout::RecursivePointer - }; -} - -#[macro_export] -macro_rules! single_register_floats { - () => { - Layout::Builtin(Builtin::Float(FloatWidth::F32 | FloatWidth::F64)) - }; -} - -#[macro_export] -macro_rules! single_register_layouts { - () => { - single_register_integers!() | single_register_floats!() - }; -} diff --git a/compiler/gen_dev/src/generic64/storage.rs b/compiler/gen_dev/src/generic64/storage.rs deleted file mode 100644 index 1334e5ae7c..0000000000 --- a/compiler/gen_dev/src/generic64/storage.rs +++ /dev/null @@ -1,1308 +0,0 @@ -use crate::{ - generic64::{Assembler, CallConv, RegTrait}, - sign_extended_int_builtins, single_register_floats, single_register_int_builtins, - single_register_integers, single_register_layouts, Env, -}; -use bumpalo::collections::Vec; -use roc_builtins::bitcode::{FloatWidth, IntWidth}; -use roc_collections::all::{MutMap, MutSet}; -use roc_error_macros::internal_error; -use roc_module::symbol::Symbol; -use roc_mono::{ - ir::{JoinPointId, Param}, - layout::{Builtin, Layout, TagIdIntType, UnionLayout}, -}; -use roc_target::TargetInfo; -use std::cmp::max; -use std::marker::PhantomData; -use std::rc::Rc; - -use RegStorage::*; -use StackStorage::*; -use Storage::*; - -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -enum RegStorage { - General(GeneralReg), - Float(FloatReg), -} - -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -enum StackStorage { - /// Primitives are 8 bytes or less. That generally live in registers but can move stored on the stack. - /// Their data, when on the stack, must always be 8 byte aligned and will be moved as a block. - /// They are never part of a struct, union, or more complex value. - /// The rest of the bytes should be the sign extension due to how these are loaded. - Primitive { - // Offset from the base pointer in bytes. - base_offset: i32, - // Optional register also holding the value. - reg: Option>, - }, - /// Referenced Primitives are primitives within a complex structures. - /// They have no guarantees about the bits around them and cannot simply be loaded as an 8 byte value. - /// For example, a U8 in a struct must be loaded as a single byte and sign extended. - /// If it was loaded as an 8 byte value, a bunch of garbage data would be loaded with the U8. - /// After loading, they should just be stored in a register, removing the reference. - ReferencedPrimitive { - // Offset from the base pointer in bytes. - base_offset: i32, - // Size on the stack in bytes. - size: u32, - // Whether or not the data is need to be sign extended on load. - // If not, it must be zero extended. - sign_extend: bool, - }, - /// Complex data (lists, unions, structs, str) stored on the stack. - /// Note, this is also used for referencing a value within a struct/union. - /// It has no alignment guarantees. - /// When a primitive value is being loaded from this, it should be moved into a register. - /// To start, the primitive can just be loaded as a ReferencePrimitive. - Complex { - // Offset from the base pointer in bytes. - base_offset: i32, - // Size on the stack in bytes. - size: u32, - // TODO: investigate if storing a reg here for special values is worth it. - // For example, the ptr in list.get/list.set - // Instead, it would probably be better to change the incoming IR to load the pointer once and then use it multiple times. - }, -} - -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -enum Storage { - Reg(RegStorage), - Stack(StackStorage), - NoData, -} - -#[derive(Clone)] -pub struct StorageManager< - 'a, - GeneralReg: RegTrait, - FloatReg: RegTrait, - ASM: Assembler, - CC: CallConv, -> { - phantom_cc: PhantomData, - phantom_asm: PhantomData, - env: &'a Env<'a>, - target_info: TargetInfo, - // Data about where each symbol is stored. - symbol_storage_map: MutMap>, - - // A map from symbol to its owning allocation. - // This is only used for complex data on the stack and its references. - // In the case that subdata is still referenced from an overall structure, - // We can't free the entire structure until the subdata is no longer needed. - // If a symbol has only one reference, we can free it. - allocation_map: MutMap>, - - // The storage for parameters of a join point. - // When jumping to the join point, the parameters should be setup to match this. - join_param_map: MutMap>>, - - // This should probably be smarter than a vec. - // There are certain registers we should always use first. With pushing and popping, this could get mixed. - general_free_regs: Vec<'a, GeneralReg>, - float_free_regs: Vec<'a, FloatReg>, - - // The last major thing we need is a way to decide what reg to free when all of them are full. - // Theoretically we want a basic lru cache for the currently loaded symbols. - // For now just a vec of used registers and the symbols they contain. - general_used_regs: Vec<'a, (GeneralReg, Symbol)>, - float_used_regs: Vec<'a, (FloatReg, Symbol)>, - - // TODO: it probably would be faster to make these a list that linearly scans rather than hashing. - // used callee saved regs must be tracked for pushing and popping at the beginning/end of the function. - general_used_callee_saved_regs: MutSet, - float_used_callee_saved_regs: MutSet, - - free_stack_chunks: Vec<'a, (i32, u32)>, - stack_size: u32, - - // The amount of extra stack space needed to pass args for function calling. - fn_call_stack_size: u32, -} - -pub fn new_storage_manager< - 'a, - GeneralReg: RegTrait, - FloatReg: RegTrait, - ASM: Assembler, - CC: CallConv, ->( - env: &'a Env, - target_info: TargetInfo, -) -> StorageManager<'a, GeneralReg, FloatReg, ASM, CC> { - StorageManager { - phantom_asm: PhantomData, - phantom_cc: PhantomData, - env, - target_info, - symbol_storage_map: MutMap::default(), - allocation_map: MutMap::default(), - join_param_map: MutMap::default(), - general_free_regs: bumpalo::vec![in env.arena], - general_used_regs: bumpalo::vec![in env.arena], - general_used_callee_saved_regs: MutSet::default(), - float_free_regs: bumpalo::vec![in env.arena], - float_used_regs: bumpalo::vec![in env.arena], - float_used_callee_saved_regs: MutSet::default(), - free_stack_chunks: bumpalo::vec![in env.arena], - stack_size: 0, - fn_call_stack_size: 0, - } -} - -impl< - 'a, - FloatReg: RegTrait, - GeneralReg: RegTrait, - ASM: Assembler, - CC: CallConv, - > StorageManager<'a, GeneralReg, FloatReg, ASM, CC> -{ - pub fn reset(&mut self) { - self.symbol_storage_map.clear(); - self.allocation_map.clear(); - self.join_param_map.clear(); - self.general_used_callee_saved_regs.clear(); - self.general_free_regs.clear(); - self.general_used_regs.clear(); - self.general_free_regs - .extend_from_slice(CC::GENERAL_DEFAULT_FREE_REGS); - self.float_used_callee_saved_regs.clear(); - self.float_free_regs.clear(); - self.float_used_regs.clear(); - self.float_free_regs - .extend_from_slice(CC::FLOAT_DEFAULT_FREE_REGS); - self.free_stack_chunks.clear(); - self.stack_size = 0; - self.fn_call_stack_size = 0; - } - - pub fn target_info(&self) -> TargetInfo { - self.target_info - } - - pub fn stack_size(&self) -> u32 { - self.stack_size - } - - pub fn fn_call_stack_size(&self) -> u32 { - self.fn_call_stack_size - } - - pub fn general_used_callee_saved_regs(&self) -> Vec<'a, GeneralReg> { - let mut used_regs = bumpalo::vec![in self.env.arena]; - used_regs.extend(&self.general_used_callee_saved_regs); - used_regs - } - - pub fn float_used_callee_saved_regs(&self) -> Vec<'a, FloatReg> { - let mut used_regs = bumpalo::vec![in self.env.arena]; - used_regs.extend(&self.float_used_callee_saved_regs); - used_regs - } - - /// Returns true if the symbol is storing a primitive value. - pub fn is_stored_primitive(&self, sym: &Symbol) -> bool { - matches!( - self.get_storage_for_sym(sym), - Reg(_) | Stack(Primitive { .. } | ReferencedPrimitive { .. }) - ) - } - - /// Get a general register from the free list. - /// Will free data to the stack if necessary to get the register. - fn get_general_reg(&mut self, buf: &mut Vec<'a, u8>) -> GeneralReg { - if let Some(reg) = self.general_free_regs.pop() { - if CC::general_callee_saved(®) { - self.general_used_callee_saved_regs.insert(reg); - } - reg - } else if !self.general_used_regs.is_empty() { - let (reg, sym) = self.general_used_regs.remove(0); - self.free_to_stack(buf, &sym, General(reg)); - reg - } else { - internal_error!("completely out of general purpose registers"); - } - } - - /// Get a float register from the free list. - /// Will free data to the stack if necessary to get the register. - fn get_float_reg(&mut self, buf: &mut Vec<'a, u8>) -> FloatReg { - if let Some(reg) = self.float_free_regs.pop() { - if CC::float_callee_saved(®) { - self.float_used_callee_saved_regs.insert(reg); - } - reg - } else if !self.float_used_regs.is_empty() { - let (reg, sym) = self.float_used_regs.remove(0); - self.free_to_stack(buf, &sym, Float(reg)); - reg - } else { - internal_error!("completely out of general purpose registers"); - } - } - - /// Claims a general reg for a specific symbol. - /// They symbol should not already have storage. - pub fn claim_general_reg(&mut self, buf: &mut Vec<'a, u8>, sym: &Symbol) -> GeneralReg { - debug_assert_eq!(self.symbol_storage_map.get(sym), None); - let reg = self.get_general_reg(buf); - self.general_used_regs.push((reg, *sym)); - self.symbol_storage_map.insert(*sym, Reg(General(reg))); - reg - } - - /// Claims a float reg for a specific symbol. - /// They symbol should not already have storage. - pub fn claim_float_reg(&mut self, buf: &mut Vec<'a, u8>, sym: &Symbol) -> FloatReg { - debug_assert_eq!(self.symbol_storage_map.get(sym), None); - let reg = self.get_float_reg(buf); - self.float_used_regs.push((reg, *sym)); - self.symbol_storage_map.insert(*sym, Reg(Float(reg))); - reg - } - - /// This claims a temporary general register and enables is used in the passed in function. - /// Temporary registers are not safe across call instructions. - pub fn with_tmp_general_reg, GeneralReg)>( - &mut self, - buf: &mut Vec<'a, u8>, - callback: F, - ) { - let reg = self.get_general_reg(buf); - callback(self, buf, reg); - self.general_free_regs.push(reg); - } - - #[allow(dead_code)] - /// This claims a temporary float register and enables is used in the passed in function. - /// Temporary registers are not safe across call instructions. - pub fn with_tmp_float_reg, FloatReg)>( - &mut self, - buf: &mut Vec<'a, u8>, - callback: F, - ) { - let reg = self.get_float_reg(buf); - callback(self, buf, reg); - self.float_free_regs.push(reg); - } - - /// Loads a symbol into a general reg and returns that register. - /// The symbol must already be stored somewhere. - /// Will fail on values stored in float regs. - /// Will fail for values that don't fit in a single register. - pub fn load_to_general_reg(&mut self, buf: &mut Vec<'a, u8>, sym: &Symbol) -> GeneralReg { - let storage = self.remove_storage_for_sym(sym); - match storage { - Reg(General(reg)) - | Stack(Primitive { - reg: Some(General(reg)), - .. - }) => { - self.symbol_storage_map.insert(*sym, storage); - reg - } - Reg(Float(_)) - | Stack(Primitive { - reg: Some(Float(_)), - .. - }) => { - internal_error!("Cannot load floating point symbol into GeneralReg: {}", sym) - } - Stack(Primitive { - reg: None, - base_offset, - }) => { - debug_assert_eq!(base_offset % 8, 0); - let reg = self.get_general_reg(buf); - ASM::mov_reg64_base32(buf, reg, base_offset); - self.general_used_regs.push((reg, *sym)); - self.symbol_storage_map.insert( - *sym, - Stack(Primitive { - base_offset, - reg: Some(General(reg)), - }), - ); - reg - } - Stack(ReferencedPrimitive { - base_offset, - size, - sign_extend, - }) => { - let reg = self.get_general_reg(buf); - if sign_extend { - ASM::movsx_reg64_base32(buf, reg, base_offset, size as u8); - } else { - ASM::movzx_reg64_base32(buf, reg, base_offset, size as u8); - } - self.general_used_regs.push((reg, *sym)); - self.symbol_storage_map.insert(*sym, Reg(General(reg))); - self.free_reference(sym); - reg - } - Stack(Complex { .. }) => { - internal_error!("Cannot load large values into general registers: {}", sym) - } - NoData => { - internal_error!("Cannot load no data into general registers: {}", sym) - } - } - } - - /// Loads a symbol into a float reg and returns that register. - /// The symbol must already be stored somewhere. - /// Will fail on values stored in general regs. - /// Will fail for values that don't fit in a single register. - pub fn load_to_float_reg(&mut self, buf: &mut Vec<'a, u8>, sym: &Symbol) -> FloatReg { - let storage = self.remove_storage_for_sym(sym); - match storage { - Reg(Float(reg)) - | Stack(Primitive { - reg: Some(Float(reg)), - .. - }) => { - self.symbol_storage_map.insert(*sym, storage); - reg - } - Reg(General(_)) - | Stack(Primitive { - reg: Some(General(_)), - .. - }) => { - internal_error!("Cannot load general symbol into FloatReg: {}", sym) - } - Stack(Primitive { - reg: None, - base_offset, - }) => { - debug_assert_eq!(base_offset % 8, 0); - let reg = self.get_float_reg(buf); - ASM::mov_freg64_base32(buf, reg, base_offset); - self.float_used_regs.push((reg, *sym)); - self.symbol_storage_map.insert( - *sym, - Stack(Primitive { - base_offset, - reg: Some(Float(reg)), - }), - ); - reg - } - Stack(ReferencedPrimitive { - base_offset, size, .. - }) if base_offset % 8 == 0 && size == 8 => { - // The primitive is aligned and the data is exactly 8 bytes, treat it like regular stack. - let reg = self.get_float_reg(buf); - ASM::mov_freg64_base32(buf, reg, base_offset); - self.float_used_regs.push((reg, *sym)); - self.symbol_storage_map.insert(*sym, Reg(Float(reg))); - self.free_reference(sym); - reg - } - Stack(ReferencedPrimitive { .. }) => { - todo!("loading referenced primitives") - } - Stack(Complex { .. }) => { - internal_error!("Cannot load large values into float registers: {}", sym) - } - NoData => { - internal_error!("Cannot load no data into general registers: {}", sym) - } - } - } - - /// Loads the symbol to the specified register. - /// It will fail if the symbol is stored in a float register. - /// This is only made to be used in special cases where exact regs are needed (function args and returns). - /// It will not try to free the register first. - /// This will not track the symbol change (it makes no assumptions about the new reg). - pub fn load_to_specified_general_reg( - &self, - buf: &mut Vec<'a, u8>, - sym: &Symbol, - reg: GeneralReg, - ) { - match self.get_storage_for_sym(sym) { - Reg(General(old_reg)) - | Stack(Primitive { - reg: Some(General(old_reg)), - .. - }) => { - if *old_reg == reg { - return; - } - ASM::mov_reg64_reg64(buf, reg, *old_reg); - } - Reg(Float(_)) - | Stack(Primitive { - reg: Some(Float(_)), - .. - }) => { - internal_error!("Cannot load floating point symbol into GeneralReg: {}", sym) - } - Stack(Primitive { - reg: None, - base_offset, - }) => { - debug_assert_eq!(base_offset % 8, 0); - ASM::mov_reg64_base32(buf, reg, *base_offset); - } - Stack(ReferencedPrimitive { - base_offset, size, .. - }) if base_offset % 8 == 0 && *size == 8 => { - // The primitive is aligned and the data is exactly 8 bytes, treat it like regular stack. - ASM::mov_reg64_base32(buf, reg, *base_offset); - } - Stack(ReferencedPrimitive { .. }) => { - todo!("loading referenced primitives") - } - Stack(Complex { .. }) => { - internal_error!("Cannot load large values into general registers: {}", sym) - } - NoData => { - internal_error!("Cannot load no data into general registers: {}", sym) - } - } - } - - /// Loads the symbol to the specified register. - /// It will fail if the symbol is stored in a general register. - /// This is only made to be used in special cases where exact regs are needed (function args and returns). - /// It will not try to free the register first. - /// This will not track the symbol change (it makes no assumptions about the new reg). - pub fn load_to_specified_float_reg(&self, buf: &mut Vec<'a, u8>, sym: &Symbol, reg: FloatReg) { - match self.get_storage_for_sym(sym) { - Reg(Float(old_reg)) - | Stack(Primitive { - reg: Some(Float(old_reg)), - .. - }) => { - if *old_reg == reg { - return; - } - ASM::mov_freg64_freg64(buf, reg, *old_reg); - } - Reg(General(_)) - | Stack(Primitive { - reg: Some(General(_)), - .. - }) => { - internal_error!("Cannot load general symbol into FloatReg: {}", sym) - } - Stack(Primitive { - reg: None, - base_offset, - }) => { - debug_assert_eq!(base_offset % 8, 0); - ASM::mov_freg64_base32(buf, reg, *base_offset); - } - Stack(ReferencedPrimitive { - base_offset, size, .. - }) if base_offset % 8 == 0 && *size == 8 => { - // The primitive is aligned and the data is exactly 8 bytes, treat it like regular stack. - ASM::mov_freg64_base32(buf, reg, *base_offset); - } - Stack(ReferencedPrimitive { .. }) => { - todo!("loading referenced primitives") - } - Stack(Complex { .. }) => { - internal_error!("Cannot load large values into float registers: {}", sym) - } - NoData => { - internal_error!("Cannot load no data into general registers: {}", sym) - } - } - } - - /// Loads a field from a struct or tag union. - /// This is lazy by default. It will not copy anything around. - pub fn load_field_at_index( - &mut self, - sym: &Symbol, - structure: &Symbol, - index: u64, - field_layouts: &'a [Layout<'a>], - ) { - debug_assert!(index < field_layouts.len() as u64); - // This must be removed and reinserted for ownership and mutability reasons. - let owned_data = self.remove_allocation_for_sym(structure); - self.allocation_map - .insert(*structure, Rc::clone(&owned_data)); - match self.get_storage_for_sym(structure) { - Stack(Complex { base_offset, size }) => { - let (base_offset, size) = (*base_offset, *size); - let mut data_offset = base_offset; - for layout in field_layouts.iter().take(index as usize) { - let field_size = layout.stack_size(self.target_info); - data_offset += field_size as i32; - } - debug_assert!(data_offset < base_offset + size as i32); - let layout = field_layouts[index as usize]; - let size = layout.stack_size(self.target_info); - self.allocation_map.insert(*sym, owned_data); - self.symbol_storage_map.insert( - *sym, - Stack(if is_primitive(&layout) { - ReferencedPrimitive { - base_offset: data_offset, - size, - sign_extend: matches!( - layout, - Layout::Builtin(sign_extended_int_builtins!()) - ), - } - } else { - Complex { - base_offset: data_offset, - size, - } - }), - ); - } - storage => { - internal_error!( - "Cannot load field from data with storage type: {:?}", - storage - ); - } - } - } - - pub fn load_union_tag_id( - &mut self, - _buf: &mut Vec<'a, u8>, - sym: &Symbol, - structure: &Symbol, - union_layout: &UnionLayout<'a>, - ) { - // This must be removed and reinserted for ownership and mutability reasons. - let owned_data = self.remove_allocation_for_sym(structure); - self.allocation_map - .insert(*structure, Rc::clone(&owned_data)); - match union_layout { - UnionLayout::NonRecursive(_) => { - let (union_offset, _) = self.stack_offset_and_size(structure); - - let (data_size, data_alignment) = - union_layout.data_size_and_alignment(self.target_info); - let id_offset = data_size - data_alignment; - let id_builtin = union_layout.tag_id_builtin(); - - let size = id_builtin.stack_size(self.target_info); - self.allocation_map.insert(*sym, owned_data); - self.symbol_storage_map.insert( - *sym, - Stack(ReferencedPrimitive { - base_offset: union_offset + id_offset as i32, - size, - sign_extend: matches!(id_builtin, sign_extended_int_builtins!()), - }), - ); - } - x => todo!("getting tag id of union with layout ({:?})", x), - } - } - - // Loads the dst to be the later 64 bits of a list (its length). - pub fn list_len(&mut self, _buf: &mut Vec<'a, u8>, dst: &Symbol, list: &Symbol) { - let owned_data = self.remove_allocation_for_sym(list); - self.allocation_map.insert(*list, Rc::clone(&owned_data)); - self.allocation_map.insert(*dst, owned_data); - let (list_offset, _) = self.stack_offset_and_size(list); - self.symbol_storage_map.insert( - *dst, - Stack(ReferencedPrimitive { - base_offset: list_offset + 8, - size: 8, - sign_extend: false, - }), - ); - } - - /// Creates a struct on the stack, moving the data in fields into the struct. - pub fn create_struct( - &mut self, - buf: &mut Vec<'a, u8>, - sym: &Symbol, - layout: &Layout<'a>, - fields: &'a [Symbol], - ) { - let struct_size = layout.stack_size(self.target_info); - if struct_size == 0 { - self.symbol_storage_map.insert(*sym, NoData); - return; - } - let base_offset = self.claim_stack_area(sym, struct_size); - - if let Layout::Struct { field_layouts, .. } = layout { - let mut current_offset = base_offset; - for (field, field_layout) in fields.iter().zip(field_layouts.iter()) { - self.copy_symbol_to_stack_offset(buf, current_offset, field, field_layout); - let field_size = field_layout.stack_size(self.target_info); - current_offset += field_size as i32; - } - } else { - // This is a single element struct. Just copy the single field to the stack. - debug_assert_eq!(fields.len(), 1); - self.copy_symbol_to_stack_offset(buf, base_offset, &fields[0], layout); - } - } - - /// Creates a union on the stack, moving the data in fields into the union and tagging it. - pub fn create_union( - &mut self, - buf: &mut Vec<'a, u8>, - sym: &Symbol, - union_layout: &UnionLayout<'a>, - fields: &'a [Symbol], - tag_id: TagIdIntType, - ) { - match union_layout { - UnionLayout::NonRecursive(field_layouts) => { - let (data_size, data_alignment) = - union_layout.data_size_and_alignment(self.target_info); - let id_offset = data_size - data_alignment; - if data_alignment < 8 || data_alignment % 8 != 0 { - todo!("small/unaligned tagging"); - } - let base_offset = self.claim_stack_area(sym, data_size); - let mut current_offset = base_offset; - for (field, field_layout) in - fields.iter().zip(field_layouts[tag_id as usize].iter()) - { - self.copy_symbol_to_stack_offset(buf, current_offset, field, field_layout); - let field_size = field_layout.stack_size(self.target_info); - current_offset += field_size as i32; - } - self.with_tmp_general_reg(buf, |_symbol_storage, buf, reg| { - ASM::mov_reg64_imm64(buf, reg, tag_id as i64); - debug_assert!((base_offset + id_offset as i32) % 8 == 0); - ASM::mov_base32_reg64(buf, base_offset + id_offset as i32, reg); - }); - } - x => todo!("creating unions with layout: {:?}", x), - } - } - - /// Copies a complex symbol on the stack to the arg pointer. - pub fn copy_symbol_to_arg_pointer( - &mut self, - buf: &mut Vec<'a, u8>, - sym: &Symbol, - _layout: &Layout<'a>, - ) { - let ret_reg = self.load_to_general_reg(buf, &Symbol::RET_POINTER); - let (base_offset, size) = self.stack_offset_and_size(sym); - debug_assert!(base_offset % 8 == 0); - debug_assert!(size % 8 == 0); - self.with_tmp_general_reg(buf, |_storage_manager, buf, tmp_reg| { - for i in (0..size as i32).step_by(8) { - ASM::mov_reg64_base32(buf, tmp_reg, base_offset + i); - ASM::mov_mem64_offset32_reg64(buf, ret_reg, i, tmp_reg); - } - }); - } - - /// Copies a symbol to the specified stack offset. This is used for things like filling structs. - /// The offset is not guarenteed to be perfectly aligned, it follows Roc's alignment plan. - /// This means that, for example 2 I32s might be back to back on the stack. - /// Always interact with the stack using aligned 64bit movement. - pub fn copy_symbol_to_stack_offset( - &mut self, - buf: &mut Vec<'a, u8>, - to_offset: i32, - sym: &Symbol, - layout: &Layout<'a>, - ) { - match layout { - Layout::Builtin(Builtin::Int(IntWidth::I64 | IntWidth::U64)) => { - debug_assert_eq!(to_offset % 8, 0); - let reg = self.load_to_general_reg(buf, sym); - ASM::mov_base32_reg64(buf, to_offset, reg); - } - Layout::Builtin(Builtin::Float(FloatWidth::F64)) => { - debug_assert_eq!(to_offset % 8, 0); - let reg = self.load_to_float_reg(buf, sym); - ASM::mov_base32_freg64(buf, to_offset, reg); - } - _ if layout.stack_size(self.target_info) == 0 => {} - _ if layout.safe_to_memcpy() && layout.stack_size(self.target_info) > 8 => { - let (from_offset, size) = self.stack_offset_and_size(sym); - debug_assert!(from_offset % 8 == 0); - debug_assert!(size % 8 == 0); - debug_assert_eq!(size, layout.stack_size(self.target_info)); - self.with_tmp_general_reg(buf, |_storage_manager, buf, reg| { - for i in (0..size as i32).step_by(8) { - ASM::mov_reg64_base32(buf, reg, from_offset + i); - ASM::mov_base32_reg64(buf, to_offset + i, reg); - } - }); - } - x => todo!("copying data to the stack with layout, {:?}", x), - } - } - - #[allow(dead_code)] - /// Ensures that a register is free. If it is not free, data will be moved to make it free. - fn ensure_reg_free( - &mut self, - buf: &mut Vec<'a, u8>, - wanted_reg: RegStorage, - ) { - match wanted_reg { - General(reg) => { - if self.general_free_regs.contains(®) { - return; - } - match self - .general_used_regs - .iter() - .position(|(used_reg, _sym)| reg == *used_reg) - { - Some(position) => { - let (used_reg, sym) = self.general_used_regs.remove(position); - self.free_to_stack(buf, &sym, wanted_reg); - self.general_free_regs.push(used_reg); - } - None => { - internal_error!("wanted register ({:?}) is not used or free", wanted_reg); - } - } - } - Float(reg) => { - if self.float_free_regs.contains(®) { - return; - } - match self - .float_used_regs - .iter() - .position(|(used_reg, _sym)| reg == *used_reg) - { - Some(position) => { - let (used_reg, sym) = self.float_used_regs.remove(position); - self.free_to_stack(buf, &sym, wanted_reg); - self.float_free_regs.push(used_reg); - } - None => { - internal_error!("wanted register ({:?}) is not used or free", wanted_reg); - } - } - } - } - } - - pub fn ensure_symbol_on_stack(&mut self, buf: &mut Vec<'a, u8>, sym: &Symbol) { - match self.remove_storage_for_sym(sym) { - Reg(reg_storage) => { - let base_offset = self.claim_stack_size(8); - match reg_storage { - General(reg) => ASM::mov_base32_reg64(buf, base_offset, reg), - Float(reg) => ASM::mov_base32_freg64(buf, base_offset, reg), - } - self.symbol_storage_map.insert( - *sym, - Stack(Primitive { - base_offset, - reg: Some(reg_storage), - }), - ); - } - x => { - self.symbol_storage_map.insert(*sym, x); - } - } - } - - /// Frees all symbols to the stack setuping up a clean slate. - pub fn free_all_to_stack(&mut self, buf: &mut Vec<'a, u8>) { - let mut free_list = bumpalo::vec![in self.env.arena]; - for (sym, storage) in self.symbol_storage_map.iter() { - match storage { - Reg(reg_storage) - | Stack(Primitive { - reg: Some(reg_storage), - .. - }) => { - free_list.push((*sym, *reg_storage)); - } - _ => {} - } - } - for (sym, reg_storage) in free_list { - match reg_storage { - General(reg) => { - self.general_free_regs.push(reg); - self.general_used_regs.retain(|(r, _)| *r != reg); - } - Float(reg) => { - self.float_free_regs.push(reg); - self.float_used_regs.retain(|(r, _)| *r != reg); - } - } - self.free_to_stack(buf, &sym, reg_storage); - } - } - - /// Frees `wanted_reg` which is currently owned by `sym` by making sure the value is loaded on the stack. - /// Note, used and free regs are expected to be updated outside of this function. - fn free_to_stack( - &mut self, - buf: &mut Vec<'a, u8>, - sym: &Symbol, - wanted_reg: RegStorage, - ) { - match self.remove_storage_for_sym(sym) { - Reg(reg_storage) => { - debug_assert_eq!(reg_storage, wanted_reg); - let base_offset = self.claim_stack_size(8); - match reg_storage { - General(reg) => ASM::mov_base32_reg64(buf, base_offset, reg), - Float(reg) => ASM::mov_base32_freg64(buf, base_offset, reg), - } - self.symbol_storage_map.insert( - *sym, - Stack(Primitive { - base_offset, - reg: None, - }), - ); - } - Stack(Primitive { - reg: Some(reg_storage), - base_offset, - }) => { - debug_assert_eq!(reg_storage, wanted_reg); - self.symbol_storage_map.insert( - *sym, - Stack(Primitive { - base_offset, - reg: None, - }), - ); - } - NoData - | Stack(Complex { .. } | Primitive { reg: None, .. } | ReferencedPrimitive { .. }) => { - internal_error!("Cannot free reg from symbol without a reg: {}", sym) - } - } - } - - /// gets the stack offset and size of the specified symbol. - /// the symbol must already be stored on the stack. - pub fn stack_offset_and_size(&self, sym: &Symbol) -> (i32, u32) { - match self.get_storage_for_sym(sym) { - Stack(Primitive { base_offset, .. }) => (*base_offset, 8), - Stack( - ReferencedPrimitive { - base_offset, size, .. - } - | Complex { base_offset, size }, - ) => (*base_offset, *size), - storage => { - internal_error!( - "Data not on the stack for sym ({}) with storage ({:?})", - sym, - storage - ) - } - } - } - - /// Specifies a symbol is loaded at the specified general register. - pub fn general_reg_arg(&mut self, sym: &Symbol, reg: GeneralReg) { - self.symbol_storage_map.insert(*sym, Reg(General(reg))); - self.general_free_regs.retain(|r| *r != reg); - self.general_used_regs.push((reg, *sym)); - } - - /// Specifies a symbol is loaded at the specified float register. - pub fn float_reg_arg(&mut self, sym: &Symbol, reg: FloatReg) { - self.symbol_storage_map.insert(*sym, Reg(Float(reg))); - self.float_free_regs.retain(|r| *r != reg); - self.float_used_regs.push((reg, *sym)); - } - - /// Specifies a primitive is loaded at the specific base offset. - pub fn primitive_stack_arg(&mut self, sym: &Symbol, base_offset: i32) { - self.symbol_storage_map.insert( - *sym, - Stack(Primitive { - base_offset, - reg: None, - }), - ); - self.allocation_map.insert(*sym, Rc::new((base_offset, 8))); - } - - /// Specifies a complex is loaded at the specific base offset. - pub fn complex_stack_arg(&mut self, sym: &Symbol, base_offset: i32, size: u32) { - self.symbol_storage_map - .insert(*sym, Stack(Complex { base_offset, size })); - self.allocation_map - .insert(*sym, Rc::new((base_offset, size))); - } - - /// Specifies a no data exists. - pub fn no_data_arg(&mut self, sym: &Symbol) { - self.symbol_storage_map.insert(*sym, NoData); - } - - /// Loads the arg pointer symbol to the specified general reg. - pub fn ret_pointer_arg(&mut self, reg: GeneralReg) { - self.symbol_storage_map - .insert(Symbol::RET_POINTER, Reg(General(reg))); - self.general_free_regs.retain(|x| *x != reg); - self.general_used_regs.push((reg, Symbol::RET_POINTER)); - } - - /// updates the stack size to the max of its current value and the tmp size needed. - pub fn update_stack_size(&mut self, tmp_size: u32) { - self.stack_size = max(self.stack_size, tmp_size); - } - - /// updates the function call stack size to the max of its current value and the size need for this call. - pub fn update_fn_call_stack_size(&mut self, tmp_size: u32) { - self.fn_call_stack_size = max(self.fn_call_stack_size, tmp_size); - } - - /// Setups a join point. - /// To do this, each of the join pionts params are given a storage location. - /// Then those locations are stored. - /// Later jumps to the join point can overwrite the stored locations to pass parameters. - pub fn setup_joinpoint( - &mut self, - _buf: &mut Vec<'a, u8>, - id: &JoinPointId, - params: &'a [Param<'a>], - ) { - let mut param_storage = bumpalo::vec![in self.env.arena]; - param_storage.reserve(params.len()); - for Param { - symbol, - borrow, - layout, - } in params - { - if *borrow { - // These probably need to be passed by pointer/reference? - // Otherwise, we probably need to copy back to the param at the end of the joinpoint. - todo!("joinpoints with borrowed parameters"); - } - // Claim a location for every join point parameter to be loaded at. - // Put everything on the stack for simplicity. - match layout { - single_register_layouts!() => { - let base_offset = self.claim_stack_size(8); - self.symbol_storage_map.insert( - *symbol, - Stack(Primitive { - base_offset, - reg: None, - }), - ); - self.allocation_map - .insert(*symbol, Rc::new((base_offset, 8))); - } - _ => { - let stack_size = layout.stack_size(self.target_info); - if stack_size == 0 { - self.symbol_storage_map.insert(*symbol, NoData); - } else { - self.claim_stack_area(symbol, stack_size); - } - } - } - param_storage.push(*self.get_storage_for_sym(symbol)); - } - self.join_param_map.insert(*id, param_storage); - } - - /// Setup jump loads the parameters for the joinpoint. - /// This enables the jump to correctly passe arguments to the joinpoint. - pub fn setup_jump( - &mut self, - buf: &mut Vec<'a, u8>, - id: &JoinPointId, - args: &[Symbol], - arg_layouts: &[Layout<'a>], - ) { - // TODO: remove was use here and for current_storage to deal with borrow checker. - // See if we can do this better. - let param_storage = match self.join_param_map.remove(id) { - Some(storages) => storages, - None => internal_error!("Jump: unknown point specified to jump to: {:?}", id), - }; - for ((sym, layout), wanted_storage) in - args.iter().zip(arg_layouts).zip(param_storage.iter()) - { - // Note: it is possible that the storage we want to move to is in use by one of the args we want to pass. - if self.get_storage_for_sym(sym) == wanted_storage { - continue; - } - match wanted_storage { - Reg(_) => { - internal_error!("Register storage is not allowed for jumping to joinpoint") - } - Stack(Complex { base_offset, .. }) => { - // TODO: This might be better not to call. - // Maybe we want a more memcpy like method to directly get called here. - // That would also be capable of asserting the size. - // Maybe copy stack to stack or something. - self.copy_symbol_to_stack_offset(buf, *base_offset, sym, layout); - } - Stack(Primitive { - base_offset, - reg: None, - }) => match layout { - single_register_integers!() => { - let reg = self.load_to_general_reg(buf, sym); - ASM::mov_base32_reg64(buf, *base_offset, reg); - } - single_register_floats!() => { - let reg = self.load_to_float_reg(buf, sym); - ASM::mov_base32_freg64(buf, *base_offset, reg); - } - _ => { - internal_error!( - "cannot load non-primitive layout ({:?}) to primitive stack location", - layout - ); - } - }, - NoData => {} - Stack(Primitive { reg: Some(_), .. }) => { - internal_error!( - "primitives with register storage are not allowed for jumping to joinpoint" - ) - } - Stack(ReferencedPrimitive { .. }) => { - internal_error!( - "referenced primitive stack storage is not allowed for jumping to joinpoint" - ) - } - } - } - self.join_param_map.insert(*id, param_storage); - } - - /// claim_stack_area is the public wrapper around claim_stack_size. - /// It also deals with updating symbol storage. - /// It returns the base offset of the stack area. - /// It should only be used for complex data and not primitives. - pub fn claim_stack_area(&mut self, sym: &Symbol, size: u32) -> i32 { - let base_offset = self.claim_stack_size(size); - self.symbol_storage_map - .insert(*sym, Stack(Complex { base_offset, size })); - self.allocation_map - .insert(*sym, Rc::new((base_offset, size))); - base_offset - } - - /// claim_stack_size claims `amount` bytes from the stack alignind to 8. - /// This may be free space in the stack or result in increasing the stack size. - /// It returns base pointer relative offset of the new data. - fn claim_stack_size(&mut self, amount: u32) -> i32 { - debug_assert!(amount > 0); - // round value to 8 byte alignment. - let amount = if amount % 8 != 0 { - amount + 8 - (amount % 8) - } else { - amount - }; - if let Some(fitting_chunk) = self - .free_stack_chunks - .iter() - .enumerate() - .filter(|(_, (_, size))| *size >= amount) - .min_by_key(|(_, (_, size))| size) - { - let (pos, (offset, size)) = fitting_chunk; - let (offset, size) = (*offset, *size); - if size == amount { - self.free_stack_chunks.remove(pos); - offset - } else { - let (prev_offset, prev_size) = self.free_stack_chunks[pos]; - self.free_stack_chunks[pos] = (prev_offset + amount as i32, prev_size - amount); - prev_offset - } - } else if let Some(new_size) = self.stack_size.checked_add(amount) { - // Since stack size is u32, but the max offset is i32, if we pass i32 max, we have overflowed. - if new_size > i32::MAX as u32 { - internal_error!("Ran out of stack space"); - } else { - self.stack_size = new_size; - -(self.stack_size as i32) - } - } else { - internal_error!("Ran out of stack space"); - } - } - - pub fn free_symbol(&mut self, sym: &Symbol) { - if self.join_param_map.remove(&JoinPointId(*sym)).is_some() { - // This is a join point and will not be in the storage map. - return; - } - match self.symbol_storage_map.remove(sym) { - // Free stack chunck if this is the last reference to the chunk. - Some(Stack(Primitive { base_offset, .. })) => { - self.free_stack_chunk(base_offset, 8); - } - Some(Stack(Complex { .. } | ReferencedPrimitive { .. })) => { - self.free_reference(sym); - } - _ => {} - } - for i in 0..self.general_used_regs.len() { - let (reg, saved_sym) = self.general_used_regs[i]; - if saved_sym == *sym { - self.general_free_regs.push(reg); - self.general_used_regs.remove(i); - break; - } - } - for i in 0..self.float_used_regs.len() { - let (reg, saved_sym) = self.float_used_regs[i]; - if saved_sym == *sym { - self.float_free_regs.push(reg); - self.float_used_regs.remove(i); - break; - } - } - } - - /// Frees an reference and release an allocation if it is no longer used. - fn free_reference(&mut self, sym: &Symbol) { - let owned_data = self.remove_allocation_for_sym(sym); - if Rc::strong_count(&owned_data) == 1 { - self.free_stack_chunk(owned_data.0, owned_data.1); - } - } - - fn free_stack_chunk(&mut self, base_offset: i32, size: u32) { - let loc = (base_offset, size); - // Note: this position current points to the offset following the specified location. - // If loc was inserted at this position, it would shift the data at this position over by 1. - let pos = self - .free_stack_chunks - .binary_search(&loc) - .unwrap_or_else(|e| e); - - // Check for overlap with previous and next free chunk. - let merge_with_prev = if pos > 0 { - if let Some((prev_offset, prev_size)) = self.free_stack_chunks.get(pos - 1) { - let prev_end = *prev_offset + *prev_size as i32; - if prev_end > base_offset { - internal_error!("Double free? A previously freed stack location overlaps with the currently freed stack location."); - } - prev_end == base_offset - } else { - false - } - } else { - false - }; - let merge_with_next = if let Some((next_offset, _)) = self.free_stack_chunks.get(pos) { - let current_end = base_offset + size as i32; - if current_end > *next_offset { - internal_error!("Double free? A previously freed stack location overlaps with the currently freed stack location."); - } - current_end == *next_offset - } else { - false - }; - - match (merge_with_prev, merge_with_next) { - (true, true) => { - let (prev_offset, prev_size) = self.free_stack_chunks[pos - 1]; - let (_, next_size) = self.free_stack_chunks[pos]; - self.free_stack_chunks[pos - 1] = (prev_offset, prev_size + size + next_size); - self.free_stack_chunks.remove(pos); - } - (true, false) => { - let (prev_offset, prev_size) = self.free_stack_chunks[pos - 1]; - self.free_stack_chunks[pos - 1] = (prev_offset, prev_size + size); - } - (false, true) => { - let (_, next_size) = self.free_stack_chunks[pos]; - self.free_stack_chunks[pos] = (base_offset, next_size + size); - } - (false, false) => self.free_stack_chunks.insert(pos, loc), - } - } - - pub fn push_used_caller_saved_regs_to_stack(&mut self, buf: &mut Vec<'a, u8>) { - let old_general_used_regs = std::mem::replace( - &mut self.general_used_regs, - bumpalo::vec![in self.env.arena], - ); - for (reg, saved_sym) in old_general_used_regs.into_iter() { - if CC::general_caller_saved(®) { - self.general_free_regs.push(reg); - self.free_to_stack(buf, &saved_sym, General(reg)); - } else { - self.general_used_regs.push((reg, saved_sym)); - } - } - let old_float_used_regs = - std::mem::replace(&mut self.float_used_regs, bumpalo::vec![in self.env.arena]); - for (reg, saved_sym) in old_float_used_regs.into_iter() { - if CC::float_caller_saved(®) { - self.float_free_regs.push(reg); - self.free_to_stack(buf, &saved_sym, Float(reg)); - } else { - self.float_used_regs.push((reg, saved_sym)); - } - } - } - - #[allow(dead_code)] - /// Gets the allocated area for a symbol. The index symbol must be defined. - fn get_allocation_for_sym(&self, sym: &Symbol) -> &Rc<(i32, u32)> { - if let Some(allocation) = self.allocation_map.get(sym) { - allocation - } else { - internal_error!("Unknown symbol: {:?}", sym); - } - } - - /// Removes and returns the allocated area for a symbol. They index symbol must be defined. - fn remove_allocation_for_sym(&mut self, sym: &Symbol) -> Rc<(i32, u32)> { - if let Some(allocation) = self.allocation_map.remove(sym) { - allocation - } else { - internal_error!("Unknown symbol: {:?}", sym); - } - } - - /// Gets a value from storage. The index symbol must be defined. - fn get_storage_for_sym(&self, sym: &Symbol) -> &Storage { - if let Some(storage) = self.symbol_storage_map.get(sym) { - storage - } else { - internal_error!("Unknown symbol: {:?}", sym); - } - } - - /// Removes and returns a value from storage. They index symbol must be defined. - fn remove_storage_for_sym(&mut self, sym: &Symbol) -> Storage { - if let Some(storage) = self.symbol_storage_map.remove(sym) { - storage - } else { - internal_error!("Unknown symbol: {:?}", sym); - } - } -} - -fn is_primitive(layout: &Layout<'_>) -> bool { - matches!(layout, single_register_layouts!()) -} diff --git a/compiler/gen_dev/src/generic64/x86_64.rs b/compiler/gen_dev/src/generic64/x86_64.rs deleted file mode 100644 index 410c879209..0000000000 --- a/compiler/gen_dev/src/generic64/x86_64.rs +++ /dev/null @@ -1,2535 +0,0 @@ -use crate::generic64::{storage::StorageManager, Assembler, CallConv, RegTrait}; -use crate::{ - single_register_floats, single_register_int_builtins, single_register_integers, - single_register_layouts, Relocation, -}; -use bumpalo::collections::Vec; -use roc_builtins::bitcode::{FloatWidth, IntWidth}; -use roc_error_macros::internal_error; -use roc_module::symbol::Symbol; -use roc_mono::layout::{Builtin, Layout}; -use roc_target::TargetInfo; - -const TARGET_INFO: TargetInfo = TargetInfo::default_x86_64(); - -// Not sure exactly how I want to represent registers. -// If we want max speed, we would likely make them structs that impl the same trait to avoid ifs. -#[derive(Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Debug)] -pub enum X86_64GeneralReg { - RAX = 0, - RCX = 1, - RDX = 2, - RBX = 3, - RSP = 4, - RBP = 5, - RSI = 6, - RDI = 7, - R8 = 8, - R9 = 9, - R10 = 10, - R11 = 11, - R12 = 12, - R13 = 13, - R14 = 14, - R15 = 15, -} -impl RegTrait for X86_64GeneralReg { - fn value(&self) -> u8 { - *self as u8 - } -} - -#[derive(Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Debug)] -pub enum X86_64FloatReg { - XMM0 = 0, - XMM1 = 1, - XMM2 = 2, - XMM3 = 3, - XMM4 = 4, - XMM5 = 5, - XMM6 = 6, - XMM7 = 7, - XMM8 = 8, - XMM9 = 9, - XMM10 = 10, - XMM11 = 11, - XMM12 = 12, - XMM13 = 13, - XMM14 = 14, - XMM15 = 15, -} -impl RegTrait for X86_64FloatReg { - fn value(&self) -> u8 { - *self as u8 - } -} - -#[derive(Copy, Clone)] -pub struct X86_64Assembler {} -#[derive(Copy, Clone)] -pub struct X86_64WindowsFastcall {} -#[derive(Copy, Clone)] -pub struct X86_64SystemV {} - -const STACK_ALIGNMENT: u8 = 16; - -impl CallConv for X86_64SystemV { - const BASE_PTR_REG: X86_64GeneralReg = X86_64GeneralReg::RBP; - const STACK_PTR_REG: X86_64GeneralReg = X86_64GeneralReg::RSP; - - const GENERAL_PARAM_REGS: &'static [X86_64GeneralReg] = &[ - X86_64GeneralReg::RDI, - X86_64GeneralReg::RSI, - X86_64GeneralReg::RDX, - X86_64GeneralReg::RCX, - X86_64GeneralReg::R8, - X86_64GeneralReg::R9, - ]; - const GENERAL_RETURN_REGS: &'static [X86_64GeneralReg] = - &[X86_64GeneralReg::RAX, X86_64GeneralReg::RDX]; - const GENERAL_DEFAULT_FREE_REGS: &'static [X86_64GeneralReg] = &[ - // The regs we want to use first should be at the end of this vec. - // We will use pop to get which reg to use next - // Use callee saved regs last. - X86_64GeneralReg::RBX, - // Don't use frame pointer: X86_64GeneralReg::RBP, - X86_64GeneralReg::R12, - X86_64GeneralReg::R13, - X86_64GeneralReg::R14, - X86_64GeneralReg::R15, - // Use caller saved regs first. - X86_64GeneralReg::RAX, - X86_64GeneralReg::RCX, - X86_64GeneralReg::RDX, - // Don't use stack pointer: X86_64GeneralReg::RSP, - X86_64GeneralReg::RSI, - X86_64GeneralReg::RDI, - X86_64GeneralReg::R8, - X86_64GeneralReg::R9, - X86_64GeneralReg::R10, - X86_64GeneralReg::R11, - ]; - - const FLOAT_PARAM_REGS: &'static [X86_64FloatReg] = &[ - X86_64FloatReg::XMM0, - X86_64FloatReg::XMM1, - X86_64FloatReg::XMM2, - X86_64FloatReg::XMM3, - X86_64FloatReg::XMM4, - X86_64FloatReg::XMM5, - X86_64FloatReg::XMM6, - X86_64FloatReg::XMM7, - ]; - const FLOAT_RETURN_REGS: &'static [X86_64FloatReg] = - &[X86_64FloatReg::XMM0, X86_64FloatReg::XMM1]; - const FLOAT_DEFAULT_FREE_REGS: &'static [X86_64FloatReg] = &[ - // The regs we want to use first should be at the end of this vec. - // We will use pop to get which reg to use next - // No callee saved regs. - // Use caller saved regs first. - X86_64FloatReg::XMM15, - X86_64FloatReg::XMM14, - X86_64FloatReg::XMM13, - X86_64FloatReg::XMM12, - X86_64FloatReg::XMM11, - X86_64FloatReg::XMM10, - X86_64FloatReg::XMM9, - X86_64FloatReg::XMM8, - X86_64FloatReg::XMM7, - X86_64FloatReg::XMM6, - X86_64FloatReg::XMM5, - X86_64FloatReg::XMM4, - X86_64FloatReg::XMM3, - X86_64FloatReg::XMM2, - X86_64FloatReg::XMM1, - X86_64FloatReg::XMM0, - ]; - const SHADOW_SPACE_SIZE: u8 = 0; - - #[inline(always)] - fn general_callee_saved(reg: &X86_64GeneralReg) -> bool { - matches!( - reg, - X86_64GeneralReg::RBX - | X86_64GeneralReg::RBP - | X86_64GeneralReg::R12 - | X86_64GeneralReg::R13 - | X86_64GeneralReg::R14 - | X86_64GeneralReg::R15 - ) - } - - #[inline(always)] - fn float_callee_saved(_reg: &X86_64FloatReg) -> bool { - false - } - - #[inline(always)] - fn setup_stack<'a>( - buf: &mut Vec<'a, u8>, - saved_general_regs: &[X86_64GeneralReg], - saved_float_regs: &[X86_64FloatReg], - requested_stack_size: i32, - fn_call_stack_size: i32, - ) -> i32 { - x86_64_generic_setup_stack( - buf, - saved_general_regs, - saved_float_regs, - requested_stack_size, - fn_call_stack_size, - ) - } - - #[inline(always)] - fn cleanup_stack<'a>( - buf: &mut Vec<'a, u8>, - saved_general_regs: &[X86_64GeneralReg], - saved_float_regs: &[X86_64FloatReg], - aligned_stack_size: i32, - fn_call_stack_size: i32, - ) { - x86_64_generic_cleanup_stack( - buf, - saved_general_regs, - saved_float_regs, - aligned_stack_size, - fn_call_stack_size, - ) - } - - #[inline(always)] - fn load_args<'a>( - _buf: &mut Vec<'a, u8>, - storage_manager: &mut StorageManager< - 'a, - X86_64GeneralReg, - X86_64FloatReg, - X86_64Assembler, - X86_64SystemV, - >, - args: &'a [(Layout<'a>, Symbol)], - ret_layout: &Layout<'a>, - ) { - let mut arg_offset = Self::SHADOW_SPACE_SIZE as i32 + 16; // 16 is the size of the pushed return address and base pointer. - let mut general_i = 0; - let mut float_i = 0; - if X86_64SystemV::returns_via_arg_pointer(ret_layout) { - storage_manager.ret_pointer_arg(Self::GENERAL_PARAM_REGS[0]); - general_i += 1; - } - for (layout, sym) in args.iter() { - let stack_size = layout.stack_size(TARGET_INFO); - match layout { - single_register_integers!() => { - if general_i < Self::GENERAL_PARAM_REGS.len() { - storage_manager.general_reg_arg(sym, Self::GENERAL_PARAM_REGS[general_i]); - general_i += 1; - } else { - storage_manager.primitive_stack_arg(sym, arg_offset); - arg_offset += 8; - } - } - single_register_floats!() => { - if float_i < Self::FLOAT_PARAM_REGS.len() { - storage_manager.float_reg_arg(sym, Self::FLOAT_PARAM_REGS[float_i]); - float_i += 1; - } else { - storage_manager.primitive_stack_arg(sym, arg_offset); - arg_offset += 8; - } - } - _ if stack_size == 0 => { - storage_manager.no_data_arg(sym); - } - _ if stack_size > 16 => { - // TODO: Double check this. - storage_manager.complex_stack_arg(sym, arg_offset, stack_size); - arg_offset += stack_size as i32; - } - x => { - todo!("Loading args with layout {:?}", x); - } - } - } - } - - #[inline(always)] - fn store_args<'a>( - buf: &mut Vec<'a, u8>, - storage_manager: &mut StorageManager< - 'a, - X86_64GeneralReg, - X86_64FloatReg, - X86_64Assembler, - X86_64SystemV, - >, - dst: &Symbol, - args: &[Symbol], - arg_layouts: &[Layout<'a>], - ret_layout: &Layout<'a>, - ) { - let mut tmp_stack_offset = Self::SHADOW_SPACE_SIZE as i32; - let mut general_i = 0; - let mut float_i = 0; - if Self::returns_via_arg_pointer(ret_layout) { - // Save space on the stack for the result we will be return. - let base_offset = - storage_manager.claim_stack_area(dst, ret_layout.stack_size(TARGET_INFO)); - // Set the first reg to the address base + offset. - let ret_reg = Self::GENERAL_PARAM_REGS[general_i]; - general_i += 1; - X86_64Assembler::add_reg64_reg64_imm32( - buf, - ret_reg, - X86_64GeneralReg::RBP, - base_offset, - ); - } - for (sym, layout) in args.iter().zip(arg_layouts.iter()) { - match layout { - single_register_integers!() => { - if general_i < Self::GENERAL_PARAM_REGS.len() { - storage_manager.load_to_specified_general_reg( - buf, - sym, - Self::GENERAL_PARAM_REGS[general_i], - ); - general_i += 1; - } else { - // Copy to stack using return reg as buffer. - storage_manager.load_to_specified_general_reg( - buf, - sym, - Self::GENERAL_RETURN_REGS[0], - ); - X86_64Assembler::mov_stack32_reg64( - buf, - tmp_stack_offset, - Self::GENERAL_RETURN_REGS[0], - ); - tmp_stack_offset += 8; - } - } - single_register_floats!() => { - if float_i < Self::FLOAT_PARAM_REGS.len() { - storage_manager.load_to_specified_float_reg( - buf, - sym, - Self::FLOAT_PARAM_REGS[float_i], - ); - float_i += 1; - } else { - // Copy to stack using return reg as buffer. - storage_manager.load_to_specified_float_reg( - buf, - sym, - Self::FLOAT_RETURN_REGS[0], - ); - X86_64Assembler::mov_stack32_freg64( - buf, - tmp_stack_offset, - Self::FLOAT_RETURN_REGS[0], - ); - tmp_stack_offset += 8; - } - } - x if x.stack_size(TARGET_INFO) == 0 => {} - x if x.stack_size(TARGET_INFO) > 16 => { - // TODO: Double check this. - // Just copy onto the stack. - // Use return reg as buffer because it will be empty right now. - let (base_offset, size) = storage_manager.stack_offset_and_size(sym); - debug_assert_eq!(base_offset % 8, 0); - for i in (0..size as i32).step_by(8) { - X86_64Assembler::mov_reg64_base32( - buf, - Self::GENERAL_RETURN_REGS[0], - base_offset + i, - ); - X86_64Assembler::mov_stack32_reg64( - buf, - tmp_stack_offset + i, - Self::GENERAL_RETURN_REGS[0], - ); - } - tmp_stack_offset += size as i32; - } - x => { - todo!("calling with arg type, {:?}", x); - } - } - } - storage_manager.update_fn_call_stack_size(tmp_stack_offset as u32); - } - - fn return_complex_symbol<'a>( - buf: &mut Vec<'a, u8>, - storage_manager: &mut StorageManager< - 'a, - X86_64GeneralReg, - X86_64FloatReg, - X86_64Assembler, - X86_64SystemV, - >, - sym: &Symbol, - layout: &Layout<'a>, - ) { - match layout { - single_register_layouts!() => { - internal_error!("single register layouts are not complex symbols"); - } - x if x.stack_size(TARGET_INFO) == 0 => {} - x if !Self::returns_via_arg_pointer(x) => { - let (base_offset, size) = storage_manager.stack_offset_and_size(sym); - debug_assert_eq!(base_offset % 8, 0); - if size <= 8 { - X86_64Assembler::mov_reg64_base32( - buf, - Self::GENERAL_RETURN_REGS[0], - base_offset, - ); - } else if size <= 16 { - X86_64Assembler::mov_reg64_base32( - buf, - Self::GENERAL_RETURN_REGS[0], - base_offset, - ); - X86_64Assembler::mov_reg64_base32( - buf, - Self::GENERAL_RETURN_REGS[1], - base_offset + 8, - ); - } else { - internal_error!( - "types that don't return via arg pointer must be less than 16 bytes" - ); - } - } - _ => { - // This is a large type returned via the arg pointer. - storage_manager.copy_symbol_to_arg_pointer(buf, sym, layout); - // Also set the return reg to the arg pointer. - storage_manager.load_to_specified_general_reg( - buf, - &Symbol::RET_POINTER, - Self::GENERAL_RETURN_REGS[0], - ); - } - } - } - - fn load_returned_complex_symbol<'a>( - buf: &mut Vec<'a, u8>, - storage_manager: &mut StorageManager< - 'a, - X86_64GeneralReg, - X86_64FloatReg, - X86_64Assembler, - X86_64SystemV, - >, - sym: &Symbol, - layout: &Layout<'a>, - ) { - match layout { - single_register_layouts!() => { - internal_error!("single register layouts are not complex symbols"); - } - x if x.stack_size(TARGET_INFO) == 0 => {} - x if !Self::returns_via_arg_pointer(x) => { - let size = layout.stack_size(TARGET_INFO); - let offset = storage_manager.claim_stack_area(sym, size); - if size <= 8 { - X86_64Assembler::mov_base32_reg64(buf, offset, Self::GENERAL_RETURN_REGS[0]); - } else if size <= 16 { - X86_64Assembler::mov_base32_reg64(buf, offset, Self::GENERAL_RETURN_REGS[0]); - X86_64Assembler::mov_base32_reg64( - buf, - offset + 8, - Self::GENERAL_RETURN_REGS[1], - ); - } else { - internal_error!( - "types that don't return via arg pointer must be less than 16 bytes" - ); - } - } - _ => { - // This should have been recieved via an arg pointer. - // That means the value is already loaded onto the stack area we allocated before the call. - // Nothing to do. - } - } - } -} - -impl X86_64SystemV { - fn returns_via_arg_pointer(ret_layout: &Layout) -> bool { - // TODO: This will need to be more complex/extended to fully support the calling convention. - // details here: https://github.com/hjl-tools/x86-psABI/wiki/x86-64-psABI-1.0.pdf - ret_layout.stack_size(TARGET_INFO) > 16 - } -} - -impl CallConv for X86_64WindowsFastcall { - const BASE_PTR_REG: X86_64GeneralReg = X86_64GeneralReg::RBP; - const STACK_PTR_REG: X86_64GeneralReg = X86_64GeneralReg::RSP; - - const GENERAL_PARAM_REGS: &'static [X86_64GeneralReg] = &[ - X86_64GeneralReg::RCX, - X86_64GeneralReg::RDX, - X86_64GeneralReg::R8, - X86_64GeneralReg::R9, - ]; - const GENERAL_RETURN_REGS: &'static [X86_64GeneralReg] = &[X86_64GeneralReg::RAX]; - const GENERAL_DEFAULT_FREE_REGS: &'static [X86_64GeneralReg] = &[ - // The regs we want to use first should be at the end of this vec. - // We will use pop to get which reg to use next - - // Don't use stack pointer: X86_64GeneralReg::RSP, - // Don't use frame pointer: X86_64GeneralReg::RBP, - - // Use callee saved regs last. - X86_64GeneralReg::RBX, - X86_64GeneralReg::RSI, - X86_64GeneralReg::RDI, - X86_64GeneralReg::R12, - X86_64GeneralReg::R13, - X86_64GeneralReg::R14, - X86_64GeneralReg::R15, - // Use caller saved regs first. - X86_64GeneralReg::RAX, - X86_64GeneralReg::RCX, - X86_64GeneralReg::RDX, - X86_64GeneralReg::R8, - X86_64GeneralReg::R9, - X86_64GeneralReg::R10, - X86_64GeneralReg::R11, - ]; - const FLOAT_PARAM_REGS: &'static [X86_64FloatReg] = &[ - X86_64FloatReg::XMM0, - X86_64FloatReg::XMM1, - X86_64FloatReg::XMM2, - X86_64FloatReg::XMM3, - ]; - const FLOAT_RETURN_REGS: &'static [X86_64FloatReg] = &[X86_64FloatReg::XMM0]; - const FLOAT_DEFAULT_FREE_REGS: &'static [X86_64FloatReg] = &[ - // The regs we want to use first should be at the end of this vec. - // We will use pop to get which reg to use next - // Use callee saved regs last. - X86_64FloatReg::XMM15, - X86_64FloatReg::XMM15, - X86_64FloatReg::XMM13, - X86_64FloatReg::XMM12, - X86_64FloatReg::XMM11, - X86_64FloatReg::XMM10, - X86_64FloatReg::XMM9, - X86_64FloatReg::XMM8, - X86_64FloatReg::XMM7, - X86_64FloatReg::XMM6, - // Use caller saved regs first. - X86_64FloatReg::XMM5, - X86_64FloatReg::XMM4, - X86_64FloatReg::XMM3, - X86_64FloatReg::XMM2, - X86_64FloatReg::XMM1, - X86_64FloatReg::XMM0, - ]; - const SHADOW_SPACE_SIZE: u8 = 32; - - #[inline(always)] - fn general_callee_saved(reg: &X86_64GeneralReg) -> bool { - matches!( - reg, - X86_64GeneralReg::RBX - | X86_64GeneralReg::RBP - | X86_64GeneralReg::RSI - | X86_64GeneralReg::RSP - | X86_64GeneralReg::RDI - | X86_64GeneralReg::R12 - | X86_64GeneralReg::R13 - | X86_64GeneralReg::R14 - | X86_64GeneralReg::R15 - ) - } - - #[inline(always)] - fn float_callee_saved(reg: &X86_64FloatReg) -> bool { - matches!( - reg, - X86_64FloatReg::XMM0 - | X86_64FloatReg::XMM1 - | X86_64FloatReg::XMM2 - | X86_64FloatReg::XMM3 - | X86_64FloatReg::XMM4 - | X86_64FloatReg::XMM5 - ) - } - - #[inline(always)] - fn setup_stack<'a>( - buf: &mut Vec<'a, u8>, - saved_general_regs: &[X86_64GeneralReg], - saved_float_regs: &[X86_64FloatReg], - requested_stack_size: i32, - fn_call_stack_size: i32, - ) -> i32 { - x86_64_generic_setup_stack( - buf, - saved_general_regs, - saved_float_regs, - requested_stack_size, - fn_call_stack_size, - ) - } - - #[inline(always)] - fn cleanup_stack<'a>( - buf: &mut Vec<'a, u8>, - saved_general_regs: &[X86_64GeneralReg], - saved_float_regs: &[X86_64FloatReg], - aligned_stack_size: i32, - fn_call_stack_size: i32, - ) { - x86_64_generic_cleanup_stack( - buf, - saved_general_regs, - saved_float_regs, - aligned_stack_size, - fn_call_stack_size, - ) - } - - #[inline(always)] - fn load_args<'a>( - _buf: &mut Vec<'a, u8>, - storage_manager: &mut StorageManager< - 'a, - X86_64GeneralReg, - X86_64FloatReg, - X86_64Assembler, - X86_64WindowsFastcall, - >, - args: &'a [(Layout<'a>, Symbol)], - ret_layout: &Layout<'a>, - ) { - let mut arg_offset = Self::SHADOW_SPACE_SIZE as i32 + 16; // 16 is the size of the pushed return address and base pointer. - let mut i = 0; - if X86_64WindowsFastcall::returns_via_arg_pointer(ret_layout) { - storage_manager.ret_pointer_arg(Self::GENERAL_PARAM_REGS[i]); - i += 1; - } - for (layout, sym) in args.iter() { - if i < Self::GENERAL_PARAM_REGS.len() { - match layout { - single_register_integers!() => { - storage_manager.general_reg_arg(sym, Self::GENERAL_PARAM_REGS[i]); - i += 1; - } - single_register_floats!() => { - storage_manager.float_reg_arg(sym, Self::FLOAT_PARAM_REGS[i]); - i += 1; - } - x if x.stack_size(TARGET_INFO) == 0 => {} - x => { - todo!("Loading args with layout {:?}", x); - } - } - } else { - match layout { - single_register_layouts!() => { - storage_manager.primitive_stack_arg(sym, arg_offset); - arg_offset += 8; - } - x => { - todo!("Loading args with layout {:?}", x); - } - }; - } - } - } - - #[inline(always)] - fn store_args<'a>( - buf: &mut Vec<'a, u8>, - storage_manager: &mut StorageManager< - 'a, - X86_64GeneralReg, - X86_64FloatReg, - X86_64Assembler, - X86_64WindowsFastcall, - >, - dst: &Symbol, - args: &[Symbol], - arg_layouts: &[Layout<'a>], - ret_layout: &Layout<'a>, - ) { - let mut tmp_stack_offset = Self::SHADOW_SPACE_SIZE as i32; - if Self::returns_via_arg_pointer(ret_layout) { - // Save space on the stack for the arg we will return. - storage_manager.claim_stack_area(dst, ret_layout.stack_size(TARGET_INFO)); - todo!("claim first parama reg for the address"); - } - for (i, (sym, layout)) in args.iter().zip(arg_layouts.iter()).enumerate() { - match layout { - single_register_integers!() => { - if i < Self::GENERAL_PARAM_REGS.len() { - storage_manager.load_to_specified_general_reg( - buf, - sym, - Self::GENERAL_PARAM_REGS[i], - ); - } else { - // Copy to stack using return reg as buffer. - storage_manager.load_to_specified_general_reg( - buf, - sym, - Self::GENERAL_RETURN_REGS[0], - ); - X86_64Assembler::mov_stack32_reg64( - buf, - tmp_stack_offset, - Self::GENERAL_RETURN_REGS[0], - ); - tmp_stack_offset += 8; - } - } - single_register_floats!() => { - if i < Self::FLOAT_PARAM_REGS.len() { - storage_manager.load_to_specified_float_reg( - buf, - sym, - Self::FLOAT_PARAM_REGS[i], - ); - } else { - // Copy to stack using return reg as buffer. - storage_manager.load_to_specified_float_reg( - buf, - sym, - Self::FLOAT_RETURN_REGS[0], - ); - X86_64Assembler::mov_stack32_freg64( - buf, - tmp_stack_offset, - Self::FLOAT_RETURN_REGS[0], - ); - tmp_stack_offset += 8; - } - } - x if x.stack_size(TARGET_INFO) == 0 => {} - x => { - todo!("calling with arg type, {:?}", x); - } - } - } - storage_manager.update_fn_call_stack_size(tmp_stack_offset as u32); - } - - fn return_complex_symbol<'a>( - _buf: &mut Vec<'a, u8>, - _storage_manager: &mut StorageManager< - 'a, - X86_64GeneralReg, - X86_64FloatReg, - X86_64Assembler, - X86_64WindowsFastcall, - >, - _sym: &Symbol, - _layout: &Layout<'a>, - ) { - todo!("Returning complex symbols for X86_64"); - } - - fn load_returned_complex_symbol<'a>( - _buf: &mut Vec<'a, u8>, - _storage_manager: &mut StorageManager< - 'a, - X86_64GeneralReg, - X86_64FloatReg, - X86_64Assembler, - X86_64WindowsFastcall, - >, - _sym: &Symbol, - _layout: &Layout<'a>, - ) { - todo!("Loading returned complex symbols for X86_64"); - } -} - -impl X86_64WindowsFastcall { - fn returns_via_arg_pointer(ret_layout: &Layout) -> bool { - // TODO: This is not fully correct there are some exceptions for "vector" types. - // details here: https://docs.microsoft.com/en-us/cpp/build/x64-calling-convention?view=msvc-160#return-values - ret_layout.stack_size(TARGET_INFO) > 8 - } -} - -#[inline(always)] -fn x86_64_generic_setup_stack<'a>( - buf: &mut Vec<'a, u8>, - saved_general_regs: &[X86_64GeneralReg], - saved_float_regs: &[X86_64FloatReg], - requested_stack_size: i32, - fn_call_stack_size: i32, -) -> i32 { - X86_64Assembler::push_reg64(buf, X86_64GeneralReg::RBP); - X86_64Assembler::mov_reg64_reg64(buf, X86_64GeneralReg::RBP, X86_64GeneralReg::RSP); - - let full_stack_size = match requested_stack_size - .checked_add(8 * (saved_general_regs.len() + saved_float_regs.len()) as i32) - .and_then(|size| size.checked_add(fn_call_stack_size)) - { - Some(size) => size, - _ => internal_error!("Ran out of stack space"), - }; - let alignment = if full_stack_size <= 0 { - 0 - } else { - full_stack_size % STACK_ALIGNMENT as i32 - }; - let offset = if alignment == 0 { - 0 - } else { - STACK_ALIGNMENT - alignment as u8 - }; - if let Some(aligned_stack_size) = full_stack_size.checked_add(offset as i32) { - if aligned_stack_size > 0 { - X86_64Assembler::sub_reg64_reg64_imm32( - buf, - X86_64GeneralReg::RSP, - X86_64GeneralReg::RSP, - aligned_stack_size, - ); - - // Put values at the top of the stack to avoid conflicts with previously saved variables. - let mut offset = aligned_stack_size - fn_call_stack_size; - for reg in saved_general_regs { - X86_64Assembler::mov_base32_reg64(buf, -offset, *reg); - offset -= 8; - } - for reg in saved_float_regs { - X86_64Assembler::mov_base32_freg64(buf, -offset, *reg); - offset -= 8; - } - aligned_stack_size - } else { - 0 - } - } else { - internal_error!("Ran out of stack space"); - } -} - -#[inline(always)] -#[allow(clippy::unnecessary_wraps)] -fn x86_64_generic_cleanup_stack<'a>( - buf: &mut Vec<'a, u8>, - saved_general_regs: &[X86_64GeneralReg], - saved_float_regs: &[X86_64FloatReg], - aligned_stack_size: i32, - fn_call_stack_size: i32, -) { - if aligned_stack_size > 0 { - let mut offset = aligned_stack_size - fn_call_stack_size; - for reg in saved_general_regs { - X86_64Assembler::mov_reg64_base32(buf, *reg, -offset); - offset -= 8; - } - for reg in saved_float_regs { - X86_64Assembler::mov_freg64_base32(buf, *reg, -offset); - offset -= 8; - } - X86_64Assembler::add_reg64_reg64_imm32( - buf, - X86_64GeneralReg::RSP, - X86_64GeneralReg::RSP, - aligned_stack_size, - ); - } - //X86_64Assembler::mov_reg64_reg64(buf, X86_64GeneralReg::RSP, X86_64GeneralReg::RBP); - X86_64Assembler::pop_reg64(buf, X86_64GeneralReg::RBP); -} - -impl Assembler for X86_64Assembler { - // These functions should map to the raw assembly functions below. - // In some cases, that means you can just directly call one of the direct assembly functions. - #[inline(always)] - fn abs_reg64_reg64(buf: &mut Vec<'_, u8>, dst: X86_64GeneralReg, src: X86_64GeneralReg) { - mov_reg64_reg64(buf, dst, src); - neg_reg64(buf, dst); - cmovl_reg64_reg64(buf, dst, src); - } - - #[inline(always)] - fn abs_freg64_freg64( - buf: &mut Vec<'_, u8>, - relocs: &mut Vec<'_, Relocation>, - dst: X86_64FloatReg, - src: X86_64FloatReg, - ) { - movsd_freg64_rip_offset32(buf, dst, 0); - - // TODO: make sure this constant only loads once instead of every call to abs - relocs.push(Relocation::LocalData { - offset: buf.len() as u64 - 4, - data: 0x7fffffffffffffffu64.to_le_bytes().to_vec(), - }); - - andpd_freg64_freg64(buf, dst, src); - } - - #[inline(always)] - fn add_reg64_reg64_imm32( - buf: &mut Vec<'_, u8>, - dst: X86_64GeneralReg, - src1: X86_64GeneralReg, - imm32: i32, - ) { - if dst != src1 { - mov_reg64_reg64(buf, dst, src1); - } - - add_reg64_imm32(buf, dst, imm32); - } - #[inline(always)] - fn add_reg64_reg64_reg64( - buf: &mut Vec<'_, u8>, - dst: X86_64GeneralReg, - src1: X86_64GeneralReg, - src2: X86_64GeneralReg, - ) { - if dst == src1 { - add_reg64_reg64(buf, dst, src2); - } else if dst == src2 { - add_reg64_reg64(buf, dst, src1); - } else { - mov_reg64_reg64(buf, dst, src1); - add_reg64_reg64(buf, dst, src2); - } - } - #[inline(always)] - fn add_freg64_freg64_freg64( - buf: &mut Vec<'_, u8>, - dst: X86_64FloatReg, - src1: X86_64FloatReg, - src2: X86_64FloatReg, - ) { - if dst == src1 { - addsd_freg64_freg64(buf, dst, src2); - } else if dst == src2 { - addsd_freg64_freg64(buf, dst, src1); - } else { - movsd_freg64_freg64(buf, dst, src1); - addsd_freg64_freg64(buf, dst, src2); - } - } - - #[inline(always)] - fn call(buf: &mut Vec<'_, u8>, relocs: &mut Vec<'_, Relocation>, fn_name: String) { - buf.extend(&[0xE8, 0x00, 0x00, 0x00, 0x00]); - relocs.push(Relocation::LinkedFunction { - offset: buf.len() as u64 - 4, - name: fn_name, - }); - } - - #[inline(always)] - fn imul_reg64_reg64_reg64( - buf: &mut Vec<'_, u8>, - dst: X86_64GeneralReg, - src1: X86_64GeneralReg, - src2: X86_64GeneralReg, - ) { - if dst != src1 { - mov_reg64_reg64(buf, dst, src1); - } - - imul_reg64_reg64(buf, dst, src2); - } - - #[inline(always)] - fn jmp_imm32(buf: &mut Vec<'_, u8>, offset: i32) -> usize { - jmp_imm32(buf, offset); - buf.len() - } - - #[inline(always)] - fn tail_call(buf: &mut Vec<'_, u8>) -> u64 { - Self::jmp_imm32(buf, 0); - buf.len() as u64 - 4 - } - - #[inline(always)] - fn jne_reg64_imm64_imm32( - buf: &mut Vec<'_, u8>, - reg: X86_64GeneralReg, - imm: u64, - offset: i32, - ) -> usize { - buf.reserve(13); - if imm > i32::MAX as u64 { - todo!("comparison with values greater than i32::max"); - } - cmp_reg64_imm32(buf, reg, imm as i32); - jne_imm32(buf, offset); - buf.len() - } - - #[inline(always)] - fn mov_freg32_imm32( - buf: &mut Vec<'_, u8>, - relocs: &mut Vec<'_, Relocation>, - dst: X86_64FloatReg, - imm: f32, - ) { - movss_freg32_rip_offset32(buf, dst, 0); - relocs.push(Relocation::LocalData { - offset: buf.len() as u64 - 4, - data: imm.to_le_bytes().to_vec(), - }); - } - #[inline(always)] - fn mov_freg64_imm64( - buf: &mut Vec<'_, u8>, - relocs: &mut Vec<'_, Relocation>, - dst: X86_64FloatReg, - imm: f64, - ) { - movsd_freg64_rip_offset32(buf, dst, 0); - relocs.push(Relocation::LocalData { - offset: buf.len() as u64 - 4, - data: imm.to_le_bytes().to_vec(), - }); - } - #[inline(always)] - fn mov_reg64_imm64(buf: &mut Vec<'_, u8>, dst: X86_64GeneralReg, imm: i64) { - mov_reg64_imm64(buf, dst, imm); - } - #[inline(always)] - fn mov_freg64_freg64(buf: &mut Vec<'_, u8>, dst: X86_64FloatReg, src: X86_64FloatReg) { - movsd_freg64_freg64(buf, dst, src); - } - #[inline(always)] - fn mov_reg64_reg64(buf: &mut Vec<'_, u8>, dst: X86_64GeneralReg, src: X86_64GeneralReg) { - mov_reg64_reg64(buf, dst, src); - } - - #[inline(always)] - fn mov_freg64_base32(buf: &mut Vec<'_, u8>, dst: X86_64FloatReg, offset: i32) { - movsd_freg64_base64_offset32(buf, dst, X86_64GeneralReg::RBP, offset) - } - #[inline(always)] - fn mov_reg64_base32(buf: &mut Vec<'_, u8>, dst: X86_64GeneralReg, offset: i32) { - mov_reg64_base64_offset32(buf, dst, X86_64GeneralReg::RBP, offset) - } - #[inline(always)] - fn mov_base32_freg64(buf: &mut Vec<'_, u8>, offset: i32, src: X86_64FloatReg) { - movsd_base64_offset32_freg64(buf, X86_64GeneralReg::RBP, offset, src) - } - #[inline(always)] - fn mov_base32_reg64(buf: &mut Vec<'_, u8>, offset: i32, src: X86_64GeneralReg) { - mov_base64_offset32_reg64(buf, X86_64GeneralReg::RBP, offset, src) - } - - #[inline(always)] - fn mov_reg64_mem64_offset32( - buf: &mut Vec<'_, u8>, - dst: X86_64GeneralReg, - src: X86_64GeneralReg, - offset: i32, - ) { - mov_reg64_base64_offset32(buf, dst, src, offset) - } - #[inline(always)] - fn mov_mem64_offset32_reg64( - buf: &mut Vec<'_, u8>, - dst: X86_64GeneralReg, - offset: i32, - src: X86_64GeneralReg, - ) { - mov_base64_offset32_reg64(buf, dst, offset, src) - } - - #[inline(always)] - fn movsx_reg64_base32(buf: &mut Vec<'_, u8>, dst: X86_64GeneralReg, offset: i32, size: u8) { - debug_assert!(size <= 8); - if size == 8 { - Self::mov_reg64_base32(buf, dst, offset); - } else if size == 4 { - todo!("sign extending 4 byte values"); - } else if size == 2 { - todo!("sign extending 2 byte values"); - } else if size == 1 { - todo!("sign extending 1 byte values"); - } else { - internal_error!("Invalid size for sign extension: {}", size); - } - } - #[inline(always)] - fn movzx_reg64_base32(buf: &mut Vec<'_, u8>, dst: X86_64GeneralReg, offset: i32, size: u8) { - debug_assert!(size <= 8); - if size == 8 { - Self::mov_reg64_base32(buf, dst, offset); - } else if size == 4 { - todo!("zero extending 4 byte values"); - } else if size == 2 { - todo!("zero extending 2 byte values"); - } else if size == 1 { - movzx_reg64_base8_offset32(buf, dst, X86_64GeneralReg::RBP, offset); - } else { - internal_error!("Invalid size for zero extension: {}", size); - } - } - - #[inline(always)] - fn mov_freg64_stack32(buf: &mut Vec<'_, u8>, dst: X86_64FloatReg, offset: i32) { - movsd_freg64_base64_offset32(buf, dst, X86_64GeneralReg::RSP, offset) - } - #[inline(always)] - fn mov_reg64_stack32(buf: &mut Vec<'_, u8>, dst: X86_64GeneralReg, offset: i32) { - mov_reg64_base64_offset32(buf, dst, X86_64GeneralReg::RSP, offset) - } - #[inline(always)] - fn mov_stack32_freg64(buf: &mut Vec<'_, u8>, offset: i32, src: X86_64FloatReg) { - movsd_base64_offset32_freg64(buf, X86_64GeneralReg::RSP, offset, src) - } - #[inline(always)] - fn mov_stack32_reg64(buf: &mut Vec<'_, u8>, offset: i32, src: X86_64GeneralReg) { - mov_base64_offset32_reg64(buf, X86_64GeneralReg::RSP, offset, src) - } - - #[inline(always)] - fn neg_reg64_reg64(buf: &mut Vec<'_, u8>, dst: X86_64GeneralReg, src: X86_64GeneralReg) { - mov_reg64_reg64(buf, dst, src); - neg_reg64(buf, dst); - } - - #[inline(always)] - fn sub_reg64_reg64_imm32( - buf: &mut Vec<'_, u8>, - dst: X86_64GeneralReg, - src1: X86_64GeneralReg, - imm32: i32, - ) { - if dst != src1 { - mov_reg64_reg64(buf, dst, src1); - } - - sub_reg64_imm32(buf, dst, imm32); - } - #[inline(always)] - fn sub_reg64_reg64_reg64( - buf: &mut Vec<'_, u8>, - dst: X86_64GeneralReg, - src1: X86_64GeneralReg, - src2: X86_64GeneralReg, - ) { - if dst != src1 { - mov_reg64_reg64(buf, dst, src1); - } - - sub_reg64_reg64(buf, dst, src2); - } - - #[inline(always)] - fn eq_reg64_reg64_reg64( - buf: &mut Vec<'_, u8>, - dst: X86_64GeneralReg, - src1: X86_64GeneralReg, - src2: X86_64GeneralReg, - ) { - cmp_reg64_reg64(buf, src1, src2); - sete_reg64(buf, dst); - } - - #[inline(always)] - fn neq_reg64_reg64_reg64( - buf: &mut Vec<'_, u8>, - dst: X86_64GeneralReg, - src1: X86_64GeneralReg, - src2: X86_64GeneralReg, - ) { - cmp_reg64_reg64(buf, src1, src2); - setne_reg64(buf, dst); - } - - #[inline(always)] - fn lt_reg64_reg64_reg64( - buf: &mut Vec<'_, u8>, - dst: X86_64GeneralReg, - src1: X86_64GeneralReg, - src2: X86_64GeneralReg, - ) { - cmp_reg64_reg64(buf, src1, src2); - setl_reg64(buf, dst); - } - - #[inline(always)] - fn to_float_freg32_reg64(buf: &mut Vec<'_, u8>, dst: X86_64FloatReg, src: X86_64GeneralReg) { - cvtsi2ss_freg64_reg64(buf, dst, src); - } - - #[inline(always)] - fn to_float_freg32_freg64(buf: &mut Vec<'_, u8>, dst: X86_64FloatReg, src: X86_64FloatReg) { - cvtsd2ss_freg32_freg64(buf, dst, src); - } - - #[inline(always)] - fn to_float_freg64_freg32(buf: &mut Vec<'_, u8>, dst: X86_64FloatReg, src: X86_64FloatReg) { - cvtss2sd_freg64_freg32(buf, dst, src); - } - - #[inline(always)] - fn to_float_freg64_reg64(buf: &mut Vec<'_, u8>, dst: X86_64FloatReg, src: X86_64GeneralReg) { - cvtsi2sd_freg64_reg64(buf, dst, src); - } - - #[inline(always)] - fn lte_reg64_reg64_reg64( - buf: &mut Vec<'_, u8>, - dst: X86_64GeneralReg, - src1: X86_64GeneralReg, - src2: X86_64GeneralReg, - ) { - cmp_reg64_reg64(buf, src1, src2); - setle_reg64(buf, dst); - } - - #[inline(always)] - fn gte_reg64_reg64_reg64( - buf: &mut Vec<'_, u8>, - dst: X86_64GeneralReg, - src1: X86_64GeneralReg, - src2: X86_64GeneralReg, - ) { - cmp_reg64_reg64(buf, src1, src2); - setge_reg64(buf, dst); - } - - #[inline(always)] - fn ret(buf: &mut Vec<'_, u8>) { - ret(buf); - } -} - -impl X86_64Assembler { - #[inline(always)] - fn pop_reg64(buf: &mut Vec<'_, u8>, reg: X86_64GeneralReg) { - pop_reg64(buf, reg); - } - - #[inline(always)] - fn push_reg64(buf: &mut Vec<'_, u8>, reg: X86_64GeneralReg) { - push_reg64(buf, reg); - } -} -const REX: u8 = 0x40; -const REX_W: u8 = REX + 0x8; - -#[inline(always)] -fn add_rm_extension(reg: T, byte: u8) -> u8 { - if reg.value() > 7 { - byte + 1 - } else { - byte - } -} - -#[inline(always)] -fn add_opcode_extension(reg: X86_64GeneralReg, byte: u8) -> u8 { - add_rm_extension(reg, byte) -} - -#[inline(always)] -fn add_reg_extension(reg: T, byte: u8) -> u8 { - if reg.value() > 7 { - byte + 4 - } else { - byte - } -} - -#[inline(always)] -fn binop_reg64_reg64( - op_code: u8, - buf: &mut Vec<'_, u8>, - dst: X86_64GeneralReg, - src: X86_64GeneralReg, -) { - let rex = add_rm_extension(dst, REX_W); - let rex = add_reg_extension(src, rex); - let dst_mod = dst as u8 % 8; - let src_mod = (src as u8 % 8) << 3; - buf.extend(&[rex, op_code, 0xC0 + dst_mod + src_mod]); -} - -#[inline(always)] -fn extended_binop_reg64_reg64( - op_code1: u8, - op_code2: u8, - buf: &mut Vec<'_, u8>, - dst: X86_64GeneralReg, - src: X86_64GeneralReg, -) { - let rex = add_rm_extension(dst, REX_W); - let rex = add_reg_extension(src, rex); - let dst_mod = dst as u8 % 8; - let src_mod = (src as u8 % 8) << 3; - buf.extend(&[rex, op_code1, op_code2, 0xC0 + dst_mod + src_mod]); -} - -// Below here are the functions for all of the assembly instructions. -// Their names are based on the instruction and operators combined. -// You should call `buf.reserve()` if you push or extend more than once. -// Unit tests are added at the bottom of the file to ensure correct asm generation. -// Please keep these in alphanumeric order. -/// `ADD r/m64, imm32` -> Add imm32 sign-extended to 64-bits from r/m64. -#[inline(always)] -fn add_reg64_imm32(buf: &mut Vec<'_, u8>, dst: X86_64GeneralReg, imm: i32) { - // This can be optimized if the immediate is 1 byte. - let rex = add_rm_extension(dst, REX_W); - let dst_mod = dst as u8 % 8; - buf.reserve(7); - buf.extend(&[rex, 0x81, 0xC0 + dst_mod]); - buf.extend(&imm.to_le_bytes()); -} - -/// `ADD r/m64,r64` -> Add r64 to r/m64. -#[inline(always)] -fn add_reg64_reg64(buf: &mut Vec<'_, u8>, dst: X86_64GeneralReg, src: X86_64GeneralReg) { - binop_reg64_reg64(0x01, buf, dst, src); -} - -/// `ADDSD xmm1,xmm2/m64` -> Add the low double-precision floating-point value from xmm2/mem to xmm1 and store the result in xmm1. -#[inline(always)] -fn addsd_freg64_freg64(buf: &mut Vec<'_, u8>, dst: X86_64FloatReg, src: X86_64FloatReg) { - let dst_high = dst as u8 > 7; - let dst_mod = dst as u8 % 8; - let src_high = src as u8 > 7; - let src_mod = src as u8 % 8; - if dst_high || src_high { - buf.extend(&[ - 0xF2, - 0x40 + ((dst_high as u8) << 2) + (src_high as u8), - 0x0F, - 0x58, - 0xC0 + (dst_mod << 3) + (src_mod), - ]) - } else { - buf.extend(&[0xF2, 0x0F, 0x58, 0xC0 + (dst_mod << 3) + (src_mod)]) - } -} - -#[inline(always)] -fn andpd_freg64_freg64(buf: &mut Vec<'_, u8>, dst: X86_64FloatReg, src: X86_64FloatReg) { - let dst_high = dst as u8 > 7; - let dst_mod = dst as u8 % 8; - let src_high = src as u8 > 7; - let src_mod = src as u8 % 8; - - if dst_high || src_high { - buf.extend(&[ - 0x66, - 0x40 + ((dst_high as u8) << 2) + (src_high as u8), - 0x0F, - 0x54, - 0xC0 + (dst_mod << 3) + (src_mod), - ]) - } else { - buf.extend(&[0x66, 0x0F, 0x54, 0xC0 + (dst_mod << 3) + (src_mod)]) - } -} - -/// r/m64 AND imm8 (sign-extended). -#[inline(always)] -fn and_reg64_imm8(buf: &mut Vec<'_, u8>, dst: X86_64GeneralReg, imm: i8) { - let rex = add_rm_extension(dst, REX_W); - let dst_mod = dst as u8 % 8; - buf.extend(&[rex, 0x83, 0xE0 + dst_mod, imm as u8]); -} - -/// `CMOVL r64,r/m64` -> Move if less (SF≠ OF). -#[inline(always)] -fn cmovl_reg64_reg64(buf: &mut Vec<'_, u8>, dst: X86_64GeneralReg, src: X86_64GeneralReg) { - let rex = add_reg_extension(dst, REX_W); - let rex = add_rm_extension(src, rex); - let dst_mod = (dst as u8 % 8) << 3; - let src_mod = src as u8 % 8; - buf.extend(&[rex, 0x0F, 0x4C, 0xC0 + dst_mod + src_mod]); -} - -/// `CMP r/m64,i32` -> Compare i32 to r/m64. -#[inline(always)] -fn cmp_reg64_imm32(buf: &mut Vec<'_, u8>, dst: X86_64GeneralReg, imm: i32) { - let rex = add_rm_extension(dst, REX_W); - let dst_mod = dst as u8 % 8; - buf.reserve(7); - buf.extend(&[rex, 0x81, 0xF8 + dst_mod]); - buf.extend(&imm.to_le_bytes()); -} - -/// `CMP r/m64,r64` -> Compare r64 to r/m64. -#[inline(always)] -fn cmp_reg64_reg64(buf: &mut Vec<'_, u8>, dst: X86_64GeneralReg, src: X86_64GeneralReg) { - binop_reg64_reg64(0x39, buf, dst, src); -} - -/// `TEST r/m64,r64` -> AND r64 with r/m64; set SF, ZF, PF according to result. -#[allow(dead_code)] -#[inline(always)] -fn test_reg64_reg64(buf: &mut Vec<'_, u8>, dst: X86_64GeneralReg, src: X86_64GeneralReg) { - binop_reg64_reg64(0x85, buf, dst, src); -} - -/// `IMUL r64,r/m64` -> Signed Multiply r/m64 to r64. -#[inline(always)] -fn imul_reg64_reg64(buf: &mut Vec<'_, u8>, dst: X86_64GeneralReg, src: X86_64GeneralReg) { - // IMUL is strange, the parameters are reversed from must other binary ops. - // The final encoding is (src, dst) instead of (dst, src). - extended_binop_reg64_reg64(0x0F, 0xAF, buf, src, dst); -} - -/// Jump near, relative, RIP = RIP + 32-bit displacement sign extended to 64-bits. -#[inline(always)] -fn jmp_imm32(buf: &mut Vec<'_, u8>, imm: i32) { - buf.reserve(5); - buf.push(0xE9); - buf.extend(&imm.to_le_bytes()); -} - -/// Jump near if not equal (ZF=0). -#[inline(always)] -fn jne_imm32(buf: &mut Vec<'_, u8>, imm: i32) { - buf.reserve(6); - buf.push(0x0F); - buf.push(0x85); - buf.extend(&imm.to_le_bytes()); -} - -/// `MOV r/m64, imm32` -> Move imm32 sign extended to 64-bits to r/m64. -#[inline(always)] -fn mov_reg64_imm32(buf: &mut Vec<'_, u8>, dst: X86_64GeneralReg, imm: i32) { - let rex = add_rm_extension(dst, REX_W); - let dst_mod = dst as u8 % 8; - buf.reserve(7); - buf.extend(&[rex, 0xC7, 0xC0 + dst_mod]); - buf.extend(&imm.to_le_bytes()); -} - -/// `MOV r64, imm64` -> Move imm64 to r64. -#[inline(always)] -fn mov_reg64_imm64(buf: &mut Vec<'_, u8>, dst: X86_64GeneralReg, imm: i64) { - if imm <= i32::MAX as i64 && imm >= i32::MIN as i64 { - mov_reg64_imm32(buf, dst, imm as i32) - } else { - let rex = add_opcode_extension(dst, REX_W); - let dst_mod = dst as u8 % 8; - buf.reserve(10); - buf.extend(&[rex, 0xB8 + dst_mod]); - buf.extend(&imm.to_le_bytes()); - } -} - -/// `MOV r/m64,r64` -> Move r64 to r/m64. -#[inline(always)] -fn mov_reg64_reg64(buf: &mut Vec<'_, u8>, dst: X86_64GeneralReg, src: X86_64GeneralReg) { - if dst != src { - binop_reg64_reg64(0x89, buf, dst, src); - } -} - -// The following base and stack based operations could be optimized based on how many bytes the offset actually is. - -/// `MOV r/m64,r64` -> Move r64 to r/m64, where m64 references a base + offset. -#[inline(always)] -fn mov_base64_offset32_reg64( - buf: &mut Vec<'_, u8>, - base: X86_64GeneralReg, - offset: i32, - src: X86_64GeneralReg, -) { - let rex = add_rm_extension(base, REX_W); - let rex = add_reg_extension(src, rex); - let src_mod = (src as u8 % 8) << 3; - let base_mod = base as u8 % 8; - buf.reserve(8); - buf.extend(&[rex, 0x89, 0x80 + src_mod + base_mod]); - // Using RSP or R12 requires a secondary index byte. - if base == X86_64GeneralReg::RSP || base == X86_64GeneralReg::R12 { - buf.push(0x24); - } - buf.extend(&offset.to_le_bytes()); -} - -/// `MOV r64,r/m64` -> Move r/m64 to r64, where m64 references a base + offset. -#[inline(always)] -fn mov_reg64_base64_offset32( - buf: &mut Vec<'_, u8>, - dst: X86_64GeneralReg, - base: X86_64GeneralReg, - offset: i32, -) { - let rex = add_rm_extension(base, REX_W); - let rex = add_reg_extension(dst, rex); - let dst_mod = (dst as u8 % 8) << 3; - let base_mod = base as u8 % 8; - buf.reserve(8); - buf.extend(&[rex, 0x8B, 0x80 + dst_mod + base_mod]); - // Using RSP or R12 requires a secondary index byte. - if base == X86_64GeneralReg::RSP || base == X86_64GeneralReg::R12 { - buf.push(0x24); - } - buf.extend(&offset.to_le_bytes()); -} - -/// `MOVZX r64,r/m8` -> Move r/m8 with zero extention to r64, where m8 references a base + offset. -#[inline(always)] -fn movzx_reg64_base8_offset32( - buf: &mut Vec<'_, u8>, - dst: X86_64GeneralReg, - base: X86_64GeneralReg, - offset: i32, -) { - let rex = add_rm_extension(base, REX_W); - let rex = add_reg_extension(dst, rex); - let dst_mod = (dst as u8 % 8) << 3; - let base_mod = base as u8 % 8; - buf.reserve(9); - buf.extend(&[rex, 0x0F, 0xB6, 0x80 + dst_mod + base_mod]); - // Using RSP or R12 requires a secondary index byte. - if base == X86_64GeneralReg::RSP || base == X86_64GeneralReg::R12 { - buf.push(0x24); - } - buf.extend(&offset.to_le_bytes()); -} - -/// `MOVSD xmm1,xmm2` -> Move scalar double-precision floating-point value from xmm2 to xmm1 register. -#[inline(always)] -fn movsd_freg64_freg64(buf: &mut Vec<'_, u8>, dst: X86_64FloatReg, src: X86_64FloatReg) { - if dst == src { - return; - } - let dst_high = dst as u8 > 7; - let dst_mod = dst as u8 % 8; - let src_high = src as u8 > 7; - let src_mod = src as u8 % 8; - if dst_high || src_high { - buf.extend(&[ - 0xF2, - 0x40 + ((dst_high as u8) << 2) + (src_high as u8), - 0x0F, - 0x10, - 0xC0 + (dst_mod << 3) + (src_mod), - ]) - } else { - buf.extend(&[0xF2, 0x0F, 0x10, 0xC0 + (dst_mod << 3) + (src_mod)]) - } -} - -// `MOVSS xmm, m32` -> Load scalar single-precision floating-point value from m32 to xmm register. -#[inline(always)] -fn movss_freg32_rip_offset32(buf: &mut Vec<'_, u8>, dst: X86_64FloatReg, offset: u32) { - let dst_mod = dst as u8 % 8; - if dst as u8 > 7 { - buf.reserve(9); - buf.extend(&[0xF3, 0x44, 0x0F, 0x10, 0x05 + (dst_mod << 3)]); - } else { - buf.reserve(8); - buf.extend(&[0xF3, 0x0F, 0x10, 0x05 + (dst_mod << 3)]); - } - buf.extend(&offset.to_le_bytes()); -} - -// `MOVSD xmm, m64` -> Load scalar double-precision floating-point value from m64 to xmm register. -#[inline(always)] -fn movsd_freg64_rip_offset32(buf: &mut Vec<'_, u8>, dst: X86_64FloatReg, offset: u32) { - let dst_mod = dst as u8 % 8; - if dst as u8 > 7 { - buf.reserve(9); - buf.extend(&[0xF2, 0x44, 0x0F, 0x10, 0x05 + (dst_mod << 3)]); - } else { - buf.reserve(8); - buf.extend(&[0xF2, 0x0F, 0x10, 0x05 + (dst_mod << 3)]); - } - buf.extend(&offset.to_le_bytes()); -} - -/// `MOVSD r/m64,xmm1` -> Move xmm1 to r/m64. where m64 references the base pointer. -#[inline(always)] -fn movsd_base64_offset32_freg64( - buf: &mut Vec<'_, u8>, - base: X86_64GeneralReg, - offset: i32, - src: X86_64FloatReg, -) { - let src_mod = (src as u8 % 8) << 3; - let base_mod = base as u8 % 8; - buf.reserve(10); - buf.push(0xF2); - if src as u8 > 7 || base as u8 > 7 { - buf.push(0x44); - } - buf.extend(&[0x0F, 0x11, 0x80 + src_mod + base_mod]); - // Using RSP or R12 requires a secondary index byte. - if base == X86_64GeneralReg::RSP || base == X86_64GeneralReg::R12 { - buf.push(0x24); - } - buf.extend(&offset.to_le_bytes()); -} - -/// `MOVSD xmm1,r/m64` -> Move r/m64 to xmm1. where m64 references the base pointer. -#[inline(always)] -fn movsd_freg64_base64_offset32( - buf: &mut Vec<'_, u8>, - dst: X86_64FloatReg, - base: X86_64GeneralReg, - offset: i32, -) { - let dst_mod = (dst as u8 % 8) << 3; - let base_mod = base as u8 % 8; - buf.reserve(10); - buf.push(0xF2); - if dst as u8 > 7 || base as u8 > 7 { - buf.push(0x44); - } - buf.extend(&[0x0F, 0x10, 0x80 + dst_mod + base_mod]); - // Using RSP or R12 requires a secondary index byte. - if base == X86_64GeneralReg::RSP || base == X86_64GeneralReg::R12 { - buf.push(0x24); - } - buf.extend(&offset.to_le_bytes()); -} - -/// `NEG r/m64` -> Two's complement negate r/m64. -#[inline(always)] -fn neg_reg64(buf: &mut Vec<'_, u8>, reg: X86_64GeneralReg) { - let rex = add_rm_extension(reg, REX_W); - let reg_mod = reg as u8 % 8; - buf.extend(&[rex, 0xF7, 0xD8 + reg_mod]); -} - -// helper function for `set*` instructions -#[inline(always)] -fn set_reg64_help(op_code: u8, buf: &mut Vec<'_, u8>, reg: X86_64GeneralReg) { - // XOR needs 3 bytes, actual SETE instruction need 3 or 4 bytes - buf.reserve(7); - - // Actually apply the SETE instruction - let reg_mod = reg as u8 % 8; - use X86_64GeneralReg::*; - match reg { - RAX | RCX | RDX | RBX => buf.extend(&[0x0F, op_code, 0xC0 + reg_mod]), - RSP | RBP | RSI | RDI => buf.extend(&[REX, 0x0F, op_code, 0xC0 + reg_mod]), - R8 | R9 | R10 | R11 | R12 | R13 | R14 | R15 => { - buf.extend(&[REX + 1, 0x0F, op_code, 0xC0 + reg_mod]) - } - } - - // We and reg with 1 because the SETE instruction only applies - // to the lower bits of the register - and_reg64_imm8(buf, reg, 1); -} - -#[inline(always)] -fn cvtsi2_help( - buf: &mut Vec<'_, u8>, - op_code1: u8, - op_code2: u8, - dst: T, - src: U, -) { - let rex = add_rm_extension(src, REX_W); - let rex = add_reg_extension(dst, rex); - let mod1 = (dst.value() % 8) << 3; - let mod2 = src.value() % 8; - - buf.extend(&[op_code1, rex, 0x0F, op_code2, 0xC0 + mod1 + mod2]) -} - -#[inline(always)] -fn cvtsx2_help( - buf: &mut Vec<'_, u8>, - op_code1: u8, - op_code2: u8, - dst: T, - src: V, -) { - let mod1 = (dst.value() % 8) << 3; - let mod2 = src.value() % 8; - - buf.extend(&[op_code1, 0x0F, op_code2, 0xC0 + mod1 + mod2]) -} - -/// `SETE r/m64` -> Set Byte on Condition - zero/equal (ZF=1) -#[inline(always)] -fn sete_reg64(buf: &mut Vec<'_, u8>, reg: X86_64GeneralReg) { - set_reg64_help(0x94, buf, reg); -} - -/// `CVTSS2SD xmm` -> Convert one single-precision floating-point value in xmm/m32 to one double-precision floating-point value in xmm. -#[inline(always)] -fn cvtss2sd_freg64_freg32(buf: &mut Vec<'_, u8>, dst: X86_64FloatReg, src: X86_64FloatReg) { - cvtsx2_help(buf, 0xF3, 0x5A, dst, src) -} - -/// `CVTSD2SS xmm` -> Convert one double-precision floating-point value in xmm to one single-precision floating-point value and merge with high bits. -#[inline(always)] -fn cvtsd2ss_freg32_freg64(buf: &mut Vec<'_, u8>, dst: X86_64FloatReg, src: X86_64FloatReg) { - cvtsx2_help(buf, 0xF2, 0x5A, dst, src) -} - -/// `CVTSI2SD r/m64` -> Convert one signed quadword integer from r/m64 to one double-precision floating-point value in xmm. -#[inline(always)] -fn cvtsi2sd_freg64_reg64(buf: &mut Vec<'_, u8>, dst: X86_64FloatReg, src: X86_64GeneralReg) { - cvtsi2_help(buf, 0xF2, 0x2A, dst, src) -} - -/// `CVTSI2SS r/m64` -> Convert one signed quadword integer from r/m64 to one single-precision floating-point value in xmm. -#[allow(dead_code)] -#[inline(always)] -fn cvtsi2ss_freg64_reg64(buf: &mut Vec<'_, u8>, dst: X86_64FloatReg, src: X86_64GeneralReg) { - cvtsi2_help(buf, 0xF3, 0x2A, dst, src) -} - -/// `CVTTSS2SI xmm/m32` -> Convert one single-precision floating-point value from xmm/m32 to one signed quadword integer in r64 using truncation. -#[allow(dead_code)] -#[inline(always)] -fn cvttss2si_reg64_freg64(buf: &mut Vec<'_, u8>, dst: X86_64GeneralReg, src: X86_64FloatReg) { - cvtsi2_help(buf, 0xF3, 0x2C, dst, src) -} - -/// `SETNE r/m64` -> Set byte if not equal (ZF=0). -#[inline(always)] -fn setne_reg64(buf: &mut Vec<'_, u8>, reg: X86_64GeneralReg) { - set_reg64_help(0x95, buf, reg); -} - -/// `SETL r/m64` -> Set byte if less (SF≠ OF). -#[inline(always)] -fn setl_reg64(buf: &mut Vec<'_, u8>, reg: X86_64GeneralReg) { - set_reg64_help(0x9c, buf, reg); -} - -/// `SETLE r/m64` -> Set byte if less or equal (ZF=1 or SF≠ OF). -#[inline(always)] -fn setle_reg64(buf: &mut Vec<'_, u8>, reg: X86_64GeneralReg) { - set_reg64_help(0x9e, buf, reg); -} - -/// `SETGE r/m64` -> Set byte if greater or equal (SF=OF). -#[inline(always)] -fn setge_reg64(buf: &mut Vec<'_, u8>, reg: X86_64GeneralReg) { - set_reg64_help(0x9d, buf, reg); -} - -/// `RET` -> Near return to calling procedure. -#[inline(always)] -fn ret(buf: &mut Vec<'_, u8>) { - buf.push(0xC3); -} - -/// `SUB r/m64, imm32` -> Subtract imm32 sign-extended to 64-bits from r/m64. -#[inline(always)] -fn sub_reg64_imm32(buf: &mut Vec<'_, u8>, dst: X86_64GeneralReg, imm: i32) { - // This can be optimized if the immediate is 1 byte. - let rex = add_rm_extension(dst, REX_W); - let dst_mod = dst as u8 % 8; - buf.reserve(7); - buf.extend(&[rex, 0x81, 0xE8 + dst_mod]); - buf.extend(&imm.to_le_bytes()); -} - -/// `SUB r/m64,r64` -> Sub r64 to r/m64. -#[inline(always)] -fn sub_reg64_reg64(buf: &mut Vec<'_, u8>, dst: X86_64GeneralReg, src: X86_64GeneralReg) { - binop_reg64_reg64(0x29, buf, dst, src); -} - -/// `POP r64` -> Pop top of stack into r64; increment stack pointer. Cannot encode 32-bit operand size. -#[inline(always)] -fn pop_reg64(buf: &mut Vec<'_, u8>, reg: X86_64GeneralReg) { - let reg_mod = reg as u8 % 8; - if reg as u8 > 7 { - let rex = add_opcode_extension(reg, REX); - buf.extend(&[rex, 0x58 + reg_mod]); - } else { - buf.push(0x58 + reg_mod); - } -} - -/// `PUSH r64` -> Push r64, -#[inline(always)] -fn push_reg64(buf: &mut Vec<'_, u8>, reg: X86_64GeneralReg) { - let reg_mod = reg as u8 % 8; - if reg as u8 > 7 { - let rex = add_opcode_extension(reg, REX); - buf.extend(&[rex, 0x50 + reg_mod]); - } else { - buf.push(0x50 + reg_mod); - } -} - -/// `XOR r/m64,r64` -> Xor r64 to r/m64. -#[inline(always)] -#[allow(dead_code)] -fn xor_reg64_reg64(buf: &mut Vec<'_, u8>, dst: X86_64GeneralReg, src: X86_64GeneralReg) { - binop_reg64_reg64(0x31, buf, dst, src); -} - -// When writing tests, it is a good idea to test both a number and unnumbered register. -// This is because R8-R15 often have special instruction prefixes. -#[cfg(test)] -mod tests { - use super::*; - - const TEST_I32: i32 = 0x12345678; - const TEST_I64: i64 = 0x1234_5678_9ABC_DEF0; - - #[test] - fn test_add_reg64_imm32() { - let arena = bumpalo::Bump::new(); - let mut buf = bumpalo::vec![in &arena]; - for (dst, expected) in &[ - (X86_64GeneralReg::RAX, [0x48, 0x81, 0xC0]), - (X86_64GeneralReg::R15, [0x49, 0x81, 0xC7]), - ] { - buf.clear(); - add_reg64_imm32(&mut buf, *dst, TEST_I32); - assert_eq!(expected, &buf[..3]); - assert_eq!(TEST_I32.to_le_bytes(), &buf[3..]); - } - } - - #[test] - fn test_add_reg64_reg64() { - let arena = bumpalo::Bump::new(); - let mut buf = bumpalo::vec![in &arena]; - for ((dst, src), expected) in &[ - ( - (X86_64GeneralReg::RAX, X86_64GeneralReg::RAX), - [0x48, 0x01, 0xC0], - ), - ( - (X86_64GeneralReg::RAX, X86_64GeneralReg::R15), - [0x4C, 0x01, 0xF8], - ), - ( - (X86_64GeneralReg::R15, X86_64GeneralReg::RAX), - [0x49, 0x01, 0xC7], - ), - ( - (X86_64GeneralReg::R15, X86_64GeneralReg::R15), - [0x4D, 0x01, 0xFF], - ), - ] { - buf.clear(); - add_reg64_reg64(&mut buf, *dst, *src); - assert_eq!(expected, &buf[..]); - } - } - - #[test] - fn test_sub_reg64_reg64() { - let arena = bumpalo::Bump::new(); - let mut buf = bumpalo::vec![in &arena]; - for ((dst, src), expected) in &[ - ( - (X86_64GeneralReg::RAX, X86_64GeneralReg::RAX), - [0x48, 0x29, 0xC0], - ), - ( - (X86_64GeneralReg::RAX, X86_64GeneralReg::R15), - [0x4C, 0x29, 0xF8], - ), - ( - (X86_64GeneralReg::R15, X86_64GeneralReg::RAX), - [0x49, 0x29, 0xC7], - ), - ( - (X86_64GeneralReg::R15, X86_64GeneralReg::R15), - [0x4D, 0x29, 0xFF], - ), - ] { - buf.clear(); - sub_reg64_reg64(&mut buf, *dst, *src); - assert_eq!(expected, &buf[..]); - } - } - - #[test] - fn test_addsd_freg64_freg64() { - let arena = bumpalo::Bump::new(); - let mut buf = bumpalo::vec![in &arena]; - for ((dst, src), expected) in &[ - ( - (X86_64FloatReg::XMM0, X86_64FloatReg::XMM0), - vec![0xF2, 0x0F, 0x58, 0xC0], - ), - ( - (X86_64FloatReg::XMM0, X86_64FloatReg::XMM15), - vec![0xF2, 0x41, 0x0F, 0x58, 0xC7], - ), - ( - (X86_64FloatReg::XMM15, X86_64FloatReg::XMM0), - vec![0xF2, 0x44, 0x0F, 0x58, 0xF8], - ), - ( - (X86_64FloatReg::XMM15, X86_64FloatReg::XMM15), - vec![0xF2, 0x45, 0x0F, 0x58, 0xFF], - ), - ] { - buf.clear(); - addsd_freg64_freg64(&mut buf, *dst, *src); - assert_eq!(&expected[..], &buf[..]); - } - } - - #[test] - fn test_andpd_freg64_freg64() { - let arena = bumpalo::Bump::new(); - let mut buf = bumpalo::vec![in &arena]; - - for ((dst, src), expected) in &[ - ( - (X86_64FloatReg::XMM0, X86_64FloatReg::XMM0), - vec![0x66, 0x0F, 0x54, 0xC0], - ), - ( - (X86_64FloatReg::XMM0, X86_64FloatReg::XMM15), - vec![0x66, 0x41, 0x0F, 0x54, 0xC7], - ), - ( - (X86_64FloatReg::XMM15, X86_64FloatReg::XMM0), - vec![0x66, 0x44, 0x0F, 0x54, 0xF8], - ), - ( - (X86_64FloatReg::XMM15, X86_64FloatReg::XMM15), - vec![0x66, 0x45, 0x0F, 0x54, 0xFF], - ), - ] { - buf.clear(); - andpd_freg64_freg64(&mut buf, *dst, *src); - assert_eq!(&expected[..], &buf[..]); - } - } - - #[test] - fn test_xor_reg64_reg64() { - let arena = bumpalo::Bump::new(); - let mut buf = bumpalo::vec![in &arena]; - for ((dst, src), expected) in &[ - ( - (X86_64GeneralReg::RAX, X86_64GeneralReg::RAX), - [0x48, 0x31, 0xC0], - ), - ( - (X86_64GeneralReg::RAX, X86_64GeneralReg::R15), - [0x4C, 0x31, 0xF8], - ), - ( - (X86_64GeneralReg::R15, X86_64GeneralReg::RAX), - [0x49, 0x31, 0xC7], - ), - ( - (X86_64GeneralReg::R15, X86_64GeneralReg::R15), - [0x4D, 0x31, 0xFF], - ), - ] { - buf.clear(); - xor_reg64_reg64(&mut buf, *dst, *src); - assert_eq!(expected, &buf[..]); - } - } - - #[test] - fn test_cmovl_reg64_reg64() { - let arena = bumpalo::Bump::new(); - let mut buf = bumpalo::vec![in &arena]; - for ((dst, src), expected) in &[ - ( - (X86_64GeneralReg::RAX, X86_64GeneralReg::RAX), - [0x48, 0x0F, 0x4C, 0xC0], - ), - ( - (X86_64GeneralReg::RAX, X86_64GeneralReg::R15), - [0x49, 0x0F, 0x4C, 0xC7], - ), - ( - (X86_64GeneralReg::R15, X86_64GeneralReg::RAX), - [0x4C, 0x0F, 0x4C, 0xF8], - ), - ( - (X86_64GeneralReg::R15, X86_64GeneralReg::R15), - [0x4D, 0x0F, 0x4C, 0xFF], - ), - ] { - buf.clear(); - cmovl_reg64_reg64(&mut buf, *dst, *src); - assert_eq!(expected, &buf[..]); - } - } - - #[test] - fn test_cmp_reg64_imm32() { - let arena = bumpalo::Bump::new(); - let mut buf = bumpalo::vec![in &arena]; - for (dst, expected) in &[ - (X86_64GeneralReg::RAX, [0x48, 0x81, 0xF8]), - (X86_64GeneralReg::R15, [0x49, 0x81, 0xFF]), - ] { - buf.clear(); - cmp_reg64_imm32(&mut buf, *dst, TEST_I32); - assert_eq!(expected, &buf[..3]); - assert_eq!(TEST_I32.to_le_bytes(), &buf[3..]); - } - } - - #[test] - fn test_imul_reg64_reg64() { - let arena = bumpalo::Bump::new(); - let mut buf = bumpalo::vec![in &arena]; - for ((dst, src), expected) in &[ - ( - (X86_64GeneralReg::RAX, X86_64GeneralReg::RAX), - [0x48, 0x0F, 0xAF, 0xC0], - ), - ( - (X86_64GeneralReg::RAX, X86_64GeneralReg::R15), - [0x49, 0x0F, 0xAF, 0xC7], - ), - ( - (X86_64GeneralReg::R15, X86_64GeneralReg::RAX), - [0x4C, 0x0F, 0xAF, 0xF8], - ), - ( - (X86_64GeneralReg::R15, X86_64GeneralReg::R15), - [0x4D, 0x0F, 0xAF, 0xFF], - ), - ] { - buf.clear(); - imul_reg64_reg64(&mut buf, *dst, *src); - assert_eq!(expected, &buf[..]); - } - } - - #[test] - fn test_jmp_imm32() { - let arena = bumpalo::Bump::new(); - let mut buf = bumpalo::vec![in &arena]; - jmp_imm32(&mut buf, TEST_I32); - assert_eq!(0xE9, buf[0]); - assert_eq!(TEST_I32.to_le_bytes(), &buf[1..]); - } - - #[test] - fn test_jne_imm32() { - let arena = bumpalo::Bump::new(); - let mut buf = bumpalo::vec![in &arena]; - jne_imm32(&mut buf, TEST_I32); - assert_eq!([0x0F, 0x85], &buf[..2]); - assert_eq!(TEST_I32.to_le_bytes(), &buf[2..]); - } - - #[test] - fn test_mov_reg64_imm32() { - let arena = bumpalo::Bump::new(); - let mut buf = bumpalo::vec![in &arena]; - for (dst, expected) in &[ - (X86_64GeneralReg::RAX, [0x48, 0xC7, 0xC0]), - (X86_64GeneralReg::R15, [0x49, 0xC7, 0xC7]), - ] { - buf.clear(); - mov_reg64_imm32(&mut buf, *dst, TEST_I32); - assert_eq!(expected, &buf[..3]); - assert_eq!(TEST_I32.to_le_bytes(), &buf[3..]); - } - } - - #[test] - fn test_mov_reg64_imm64() { - let arena = bumpalo::Bump::new(); - let mut buf = bumpalo::vec![in &arena]; - for (dst, expected) in &[ - (X86_64GeneralReg::RAX, [0x48, 0xB8]), - (X86_64GeneralReg::R15, [0x49, 0xBF]), - ] { - buf.clear(); - mov_reg64_imm64(&mut buf, *dst, TEST_I64); - assert_eq!(expected, &buf[..2]); - assert_eq!(TEST_I64.to_le_bytes(), &buf[2..]); - } - for (dst, expected) in &[ - (X86_64GeneralReg::RAX, [0x48, 0xC7, 0xC0]), - (X86_64GeneralReg::R15, [0x49, 0xC7, 0xC7]), - ] { - buf.clear(); - mov_reg64_imm64(&mut buf, *dst, TEST_I32 as i64); - assert_eq!(expected, &buf[..3]); - assert_eq!(TEST_I32.to_le_bytes(), &buf[3..]); - } - } - - #[test] - fn test_mov_reg64_reg64() { - let arena = bumpalo::Bump::new(); - let mut buf = bumpalo::vec![in &arena]; - for ((dst, src), expected) in &[ - ((X86_64GeneralReg::RAX, X86_64GeneralReg::RAX), vec![]), - ( - (X86_64GeneralReg::RAX, X86_64GeneralReg::RCX), - vec![0x48, 0x89, 0xC8], - ), - ( - (X86_64GeneralReg::RAX, X86_64GeneralReg::R15), - vec![0x4C, 0x89, 0xF8], - ), - ( - (X86_64GeneralReg::R15, X86_64GeneralReg::RAX), - vec![0x49, 0x89, 0xC7], - ), - ( - (X86_64GeneralReg::R15, X86_64GeneralReg::R14), - vec![0x4D, 0x89, 0xF7], - ), - ] { - buf.clear(); - mov_reg64_reg64(&mut buf, *dst, *src); - assert_eq!(&expected[..], &buf[..]); - } - } - - #[test] - fn test_movsd_freg64_base32() { - let arena = bumpalo::Bump::new(); - let mut buf = bumpalo::vec![in &arena]; - for ((dst, offset), expected) in &[ - ( - (X86_64FloatReg::XMM0, TEST_I32), - vec![0xF2, 0x0F, 0x10, 0x85], - ), - ( - (X86_64FloatReg::XMM15, TEST_I32), - vec![0xF2, 0x44, 0x0F, 0x10, 0xBD], - ), - ] { - buf.clear(); - movsd_freg64_base64_offset32(&mut buf, *dst, X86_64GeneralReg::RBP, *offset); - assert_eq!(expected, &buf[..buf.len() - 4]); - assert_eq!(TEST_I32.to_le_bytes(), &buf[buf.len() - 4..]); - } - } - - #[test] - fn test_movsd_base32_freg64() { - let arena = bumpalo::Bump::new(); - let mut buf = bumpalo::vec![in &arena]; - for ((offset, src), expected) in &[ - ( - (TEST_I32, X86_64FloatReg::XMM0), - vec![0xF2, 0x0F, 0x11, 0x85], - ), - ( - (TEST_I32, X86_64FloatReg::XMM15), - vec![0xF2, 0x44, 0x0F, 0x11, 0xBD], - ), - ] { - buf.clear(); - movsd_base64_offset32_freg64(&mut buf, X86_64GeneralReg::RBP, *offset, *src); - assert_eq!(expected, &buf[..buf.len() - 4]); - assert_eq!(TEST_I32.to_le_bytes(), &buf[buf.len() - 4..]); - } - } - - #[test] - fn test_movsd_freg64_stack32() { - let arena = bumpalo::Bump::new(); - let mut buf = bumpalo::vec![in &arena]; - for ((dst, offset), expected) in &[ - ( - (X86_64FloatReg::XMM0, TEST_I32), - vec![0xF2, 0x0F, 0x10, 0x84, 0x24], - ), - ( - (X86_64FloatReg::XMM15, TEST_I32), - vec![0xF2, 0x44, 0x0F, 0x10, 0xBC, 0x24], - ), - ] { - buf.clear(); - movsd_freg64_base64_offset32(&mut buf, *dst, X86_64GeneralReg::RSP, *offset); - assert_eq!(expected, &buf[..buf.len() - 4]); - assert_eq!(TEST_I32.to_le_bytes(), &buf[buf.len() - 4..]); - } - } - - #[test] - fn test_movsd_stack32_freg64() { - let arena = bumpalo::Bump::new(); - let mut buf = bumpalo::vec![in &arena]; - for ((offset, src), expected) in &[ - ( - (TEST_I32, X86_64FloatReg::XMM0), - vec![0xF2, 0x0F, 0x11, 0x84, 0x24], - ), - ( - (TEST_I32, X86_64FloatReg::XMM15), - vec![0xF2, 0x44, 0x0F, 0x11, 0xBC, 0x24], - ), - ] { - buf.clear(); - movsd_base64_offset32_freg64(&mut buf, X86_64GeneralReg::RSP, *offset, *src); - assert_eq!(expected, &buf[..buf.len() - 4]); - assert_eq!(TEST_I32.to_le_bytes(), &buf[buf.len() - 4..]); - } - } - - #[test] - fn test_mov_reg64_base32() { - let arena = bumpalo::Bump::new(); - let mut buf = bumpalo::vec![in &arena]; - for ((dst, offset), expected) in &[ - ((X86_64GeneralReg::RAX, TEST_I32), [0x48, 0x8B, 0x85]), - ((X86_64GeneralReg::R15, TEST_I32), [0x4C, 0x8B, 0xBD]), - ] { - buf.clear(); - mov_reg64_base64_offset32(&mut buf, *dst, X86_64GeneralReg::RBP, *offset); - assert_eq!(expected, &buf[..3]); - assert_eq!(TEST_I32.to_le_bytes(), &buf[3..]); - } - } - - #[test] - fn test_mov_base32_reg64() { - let arena = bumpalo::Bump::new(); - let mut buf = bumpalo::vec![in &arena]; - for ((offset, src), expected) in &[ - ((TEST_I32, X86_64GeneralReg::RAX), [0x48, 0x89, 0x85]), - ((TEST_I32, X86_64GeneralReg::R15), [0x4C, 0x89, 0xBD]), - ] { - buf.clear(); - mov_base64_offset32_reg64(&mut buf, X86_64GeneralReg::RBP, *offset, *src); - assert_eq!(expected, &buf[..3]); - assert_eq!(TEST_I32.to_le_bytes(), &buf[3..]); - } - } - - #[test] - fn test_movzx_reg64_base8_offset32() { - let arena = bumpalo::Bump::new(); - let mut buf = bumpalo::vec![in &arena]; - for ((dst, src, offset), expected) in &[ - ( - (X86_64GeneralReg::RAX, X86_64GeneralReg::RBP, TEST_I32), - vec![0x48, 0x0F, 0xB6, 0x85], - ), - ( - (X86_64GeneralReg::R15, X86_64GeneralReg::RBP, TEST_I32), - vec![0x4C, 0x0F, 0xB6, 0xBD], - ), - ( - (X86_64GeneralReg::RAX, X86_64GeneralReg::RSP, TEST_I32), - vec![0x48, 0x0F, 0xB6, 0x84, 0x24], - ), - ( - (X86_64GeneralReg::R15, X86_64GeneralReg::RSP, TEST_I32), - vec![0x4C, 0x0F, 0xB6, 0xBC, 0x24], - ), - ] { - buf.clear(); - movzx_reg64_base8_offset32(&mut buf, *dst, *src, *offset); - assert_eq!(expected, &buf[..expected.len()]); - assert_eq!(TEST_I32.to_le_bytes(), &buf[expected.len()..]); - } - } - - #[test] - fn test_mov_reg64_stack32() { - let arena = bumpalo::Bump::new(); - let mut buf = bumpalo::vec![in &arena]; - for ((dst, offset), expected) in &[ - ((X86_64GeneralReg::RAX, TEST_I32), [0x48, 0x8B, 0x84, 0x24]), - ((X86_64GeneralReg::R15, TEST_I32), [0x4C, 0x8B, 0xBC, 0x24]), - ] { - buf.clear(); - mov_reg64_base64_offset32(&mut buf, *dst, X86_64GeneralReg::RSP, *offset); - assert_eq!(expected, &buf[..4]); - assert_eq!(TEST_I32.to_le_bytes(), &buf[4..]); - } - } - - #[test] - fn test_mov_stack32_reg64() { - let arena = bumpalo::Bump::new(); - let mut buf = bumpalo::vec![in &arena]; - for ((offset, src), expected) in &[ - ((TEST_I32, X86_64GeneralReg::RAX), [0x48, 0x89, 0x84, 0x24]), - ((TEST_I32, X86_64GeneralReg::R15), [0x4C, 0x89, 0xBC, 0x24]), - ] { - buf.clear(); - mov_base64_offset32_reg64(&mut buf, X86_64GeneralReg::RSP, *offset, *src); - assert_eq!(expected, &buf[..4]); - assert_eq!(TEST_I32.to_le_bytes(), &buf[4..]); - } - } - - #[test] - fn test_movsd_freg64_freg64() { - let arena = bumpalo::Bump::new(); - let mut buf = bumpalo::vec![in &arena]; - for ((dst, src), expected) in &[ - ((X86_64FloatReg::XMM0, X86_64FloatReg::XMM0), vec![]), - ( - (X86_64FloatReg::XMM0, X86_64FloatReg::XMM15), - vec![0xF2, 0x41, 0x0F, 0x10, 0xC7], - ), - ( - (X86_64FloatReg::XMM15, X86_64FloatReg::XMM0), - vec![0xF2, 0x44, 0x0F, 0x10, 0xF8], - ), - ((X86_64FloatReg::XMM15, X86_64FloatReg::XMM15), vec![]), - ] { - buf.clear(); - movsd_freg64_freg64(&mut buf, *dst, *src); - assert_eq!(&expected[..], &buf[..]); - } - } - - #[test] - fn test_movss_freg32_rip_offset32() { - let arena = bumpalo::Bump::new(); - let mut buf = bumpalo::vec![in &arena]; - for ((dst, offset), expected) in &[ - ( - (X86_64FloatReg::XMM0, TEST_I32), - vec![0xF3, 0x0F, 0x10, 0x05], - ), - ( - (X86_64FloatReg::XMM15, TEST_I32), - vec![0xF3, 0x44, 0x0F, 0x10, 0x3D], - ), - ] { - buf.clear(); - movss_freg32_rip_offset32(&mut buf, *dst, *offset as u32); - assert_eq!(&expected[..], &buf[..(buf.len() - 4)]); - assert_eq!(TEST_I32.to_le_bytes(), &buf[(buf.len() - 4)..]); - } - } - - #[test] - fn test_movsd_freg64_rip_offset32() { - let arena = bumpalo::Bump::new(); - let mut buf = bumpalo::vec![in &arena]; - for ((dst, offset), expected) in &[ - ( - (X86_64FloatReg::XMM0, TEST_I32), - vec![0xF2, 0x0F, 0x10, 0x05], - ), - ( - (X86_64FloatReg::XMM15, TEST_I32), - vec![0xF2, 0x44, 0x0F, 0x10, 0x3D], - ), - ] { - buf.clear(); - movsd_freg64_rip_offset32(&mut buf, *dst, *offset as u32); - assert_eq!(&expected[..], &buf[..(buf.len() - 4)]); - assert_eq!(TEST_I32.to_le_bytes(), &buf[(buf.len() - 4)..]); - } - } - - #[test] - fn test_neg_reg64() { - let arena = bumpalo::Bump::new(); - let mut buf = bumpalo::vec![in &arena]; - for (reg, expected) in &[ - (X86_64GeneralReg::RAX, [0x48, 0xF7, 0xD8]), - (X86_64GeneralReg::R15, [0x49, 0xF7, 0xDF]), - ] { - buf.clear(); - neg_reg64(&mut buf, *reg); - assert_eq!(expected, &buf[..]); - } - } - - #[test] - fn test_cvtsi2_help() { - let arena = bumpalo::Bump::new(); - let mut buf = bumpalo::vec![in &arena]; - let cvtsi2ss_code: u8 = 0x2A; - let cvttss2si_code: u8 = 0x2C; - - for (op_code, reg1, reg2, expected) in &[ - ( - cvtsi2ss_code, - X86_64FloatReg::XMM0, - X86_64GeneralReg::RDI, - [0xF3, 0x48, 0x0F, 0x2A, 0xC7], - ), - ( - cvtsi2ss_code, - X86_64FloatReg::XMM15, - X86_64GeneralReg::RDI, - [0xF3, 0x4C, 0x0F, 0x2A, 0xFF], - ), - ( - cvtsi2ss_code, - X86_64FloatReg::XMM0, - X86_64GeneralReg::RAX, - [0xF3, 0x48, 0x0F, 0x2A, 0xC0], - ), - ( - cvtsi2ss_code, - X86_64FloatReg::XMM0, - X86_64GeneralReg::R15, - [0xF3, 0x49, 0x0F, 0x2A, 0xC7], - ), - ] { - buf.clear(); - cvtsi2_help(&mut buf, 0xF3, *op_code, *reg1, *reg2); - assert_eq!(expected, &buf[..]); - } - - for (op_code, reg1, reg2, expected) in &[ - ( - cvttss2si_code, - X86_64GeneralReg::RAX, - X86_64FloatReg::XMM0, - [0xF3, 0x48, 0x0F, 0x2C, 0xC0], - ), - ( - cvttss2si_code, - X86_64GeneralReg::RAX, - X86_64FloatReg::XMM15, - [0xF3, 0x49, 0x0F, 0x2C, 0xC7], - ), - ( - cvttss2si_code, - X86_64GeneralReg::RAX, - X86_64FloatReg::XMM0, - [0xF3, 0x48, 0x0F, 0x2C, 0xC0], - ), - ( - cvttss2si_code, - X86_64GeneralReg::R15, - X86_64FloatReg::XMM0, - [0xF3, 0x4C, 0x0F, 0x2C, 0xF8], - ), - ] { - buf.clear(); - cvtsi2_help(&mut buf, 0xF3, *op_code, *reg1, *reg2); - assert_eq!(expected, &buf[..]); - } - } - - #[test] - fn test_cvtsx2_help() { - let arena = bumpalo::Bump::new(); - let mut buf = bumpalo::vec![in &arena]; - let cvtss2sd_code: u8 = 0x5A; - - { - let (op_code, reg1, reg2, expected) = &( - cvtss2sd_code, - X86_64FloatReg::XMM1, - X86_64FloatReg::XMM0, - [0xF3, 0x0F, 0x5A, 0xC8], - ); - buf.clear(); - cvtsx2_help(&mut buf, 0xF3, *op_code, *reg1, *reg2); - assert_eq!(expected, &buf[..]); - } - } - - #[test] - fn test_set_reg64_help() { - let arena = bumpalo::Bump::new(); - let mut buf = bumpalo::vec![in &arena]; - - // tests for 7 bytes in the output buffer - let (reg, expected) = ( - X86_64GeneralReg::RAX, - [ - 0x0F, 0x94, 0xC0, // SETE al ; al are the 8 lower weight bits of rax - 0x48, 0x83, 0xE0, 0x01, // AND rax, 1 - ], - ); - buf.clear(); - set_reg64_help(0x94, &mut buf, reg); // sete_reg64 - assert_eq!(expected, &buf[..]); - - // tests for 8 bytes in the output buffer - for (reg, expected) in &[ - ( - X86_64GeneralReg::RSP, - [ - // SETE spl ; spl are the 8 lower weight bits of rsp - 0x40, 0x0F, 0x94, 0xC4, // - // AND rsp, 1 - 0x48, 0x83, 0xE4, 0x01, - ], - ), - ( - X86_64GeneralReg::R15, - [ - // SETE r15b ; r15b are the 8 lower weight bits of r15 - 0x41, 0x0F, 0x94, 0xC7, // - // AND rsp, 1 - 0x49, 0x83, 0xE7, 0x01, - ], - ), - ] { - buf.clear(); - set_reg64_help(0x94, &mut buf, *reg); // sete_reg64 - assert_eq!(expected, &buf[..]); - } - } - - #[test] - fn test_ret() { - let arena = bumpalo::Bump::new(); - let mut buf = bumpalo::vec![in &arena]; - ret(&mut buf); - assert_eq!(&[0xC3], &buf[..]); - } - - #[test] - fn test_sub_reg64_imm32() { - let arena = bumpalo::Bump::new(); - let mut buf = bumpalo::vec![in &arena]; - for (dst, expected) in &[ - (X86_64GeneralReg::RAX, [0x48, 0x81, 0xE8]), - (X86_64GeneralReg::R15, [0x49, 0x81, 0xEF]), - ] { - buf.clear(); - sub_reg64_imm32(&mut buf, *dst, TEST_I32); - assert_eq!(expected, &buf[..3]); - assert_eq!(TEST_I32.to_le_bytes(), &buf[3..]); - } - } - - #[test] - fn test_pop_reg64() { - let arena = bumpalo::Bump::new(); - let mut buf = bumpalo::vec![in &arena]; - for (dst, expected) in &[ - (X86_64GeneralReg::RAX, vec![0x58]), - (X86_64GeneralReg::R15, vec![0x41, 0x5F]), - ] { - buf.clear(); - pop_reg64(&mut buf, *dst); - assert_eq!(&expected[..], &buf[..]); - } - } - - #[test] - fn test_push_reg64() { - let arena = bumpalo::Bump::new(); - let mut buf = bumpalo::vec![in &arena]; - for (src, expected) in &[ - (X86_64GeneralReg::RAX, vec![0x50]), - (X86_64GeneralReg::R15, vec![0x41, 0x57]), - ] { - buf.clear(); - push_reg64(&mut buf, *src); - assert_eq!(&expected[..], &buf[..]); - } - } -} diff --git a/compiler/gen_dev/src/lib.rs b/compiler/gen_dev/src/lib.rs deleted file mode 100644 index 73927170e6..0000000000 --- a/compiler/gen_dev/src/lib.rs +++ /dev/null @@ -1,1008 +0,0 @@ -#![warn(clippy::dbg_macro)] -// See github.com/rtfeldman/roc/issues/800 for discussion of the large_enum_variant check. -#![allow(clippy::large_enum_variant, clippy::upper_case_acronyms)] - -use bumpalo::{collections::Vec, Bump}; -use roc_builtins::bitcode::{self, FloatWidth, IntWidth}; -use roc_collections::all::{MutMap, MutSet}; -use roc_error_macros::internal_error; -use roc_module::ident::ModuleName; -use roc_module::low_level::{LowLevel, LowLevelWrapperType}; -use roc_module::symbol::{Interns, ModuleId, Symbol}; -use roc_mono::code_gen_help::CodeGenHelp; -use roc_mono::ir::{ - BranchInfo, CallType, Expr, JoinPointId, ListLiteralElement, Literal, Param, Proc, ProcLayout, - SelfRecursive, Stmt, -}; -use roc_mono::layout::{ - Builtin, Layout, LayoutId, LayoutIds, TagIdIntType, TagOrClosure, UnionLayout, -}; - -mod generic64; -mod object_builder; -pub use object_builder::build_module; -mod run_roc; - -pub struct Env<'a> { - pub arena: &'a Bump, - pub module_id: ModuleId, - pub exposed_to_host: MutSet, - pub lazy_literals: bool, - pub generate_allocators: bool, -} - -// These relocations likely will need a length. -// They may even need more definition, but this should be at least good enough for how we will use elf. -#[derive(Debug, Clone)] -#[allow(dead_code)] -pub enum Relocation { - LocalData { - offset: u64, - // This should probably technically be a bumpalo::Vec. - // The problem is that it currently is built in a place that can't access the arena. - data: std::vec::Vec, - }, - LinkedFunction { - offset: u64, - name: String, - }, - LinkedData { - offset: u64, - name: String, - }, - JmpToReturn { - inst_loc: u64, - inst_size: u64, - offset: u64, - }, -} - -trait Backend<'a> { - fn env(&self) -> &Env<'a>; - fn interns(&self) -> &Interns; - - // This method is suboptimal, but it seems to be the only way to make rust understand - // that all of these values can be mutable at the same time. By returning them together, - // rust understands that they are part of a single use of mutable self. - fn env_interns_helpers_mut(&mut self) -> (&Env<'a>, &mut Interns, &mut CodeGenHelp<'a>); - - fn symbol_to_string(&self, symbol: Symbol, layout_id: LayoutId) -> String { - layout_id.to_symbol_string(symbol, self.interns()) - } - - fn defined_in_app_module(&self, symbol: Symbol) -> bool { - symbol - .module_string(self.interns()) - .starts_with(ModuleName::APP) - } - - fn helper_proc_gen_mut(&mut self) -> &mut CodeGenHelp<'a>; - - fn helper_proc_symbols_mut(&mut self) -> &mut Vec<'a, (Symbol, ProcLayout<'a>)>; - - fn helper_proc_symbols(&self) -> &Vec<'a, (Symbol, ProcLayout<'a>)>; - - /// reset resets any registers or other values that may be occupied at the end of a procedure. - /// It also passes basic procedure information to the builder for setup of the next function. - fn reset(&mut self, name: String, is_self_recursive: SelfRecursive); - - /// finalize does any setup and cleanup that should happen around the procedure. - /// finalize does setup because things like stack size and jump locations are not know until the function is written. - /// For example, this can store the frame pointer and setup stack space. - /// finalize is run at the end of build_proc when all internal code is finalized. - fn finalize(&mut self) -> (Vec, Vec); - - // load_args is used to let the backend know what the args are. - // The backend should track these args so it can use them as needed. - fn load_args(&mut self, args: &'a [(Layout<'a>, Symbol)], ret_layout: &Layout<'a>); - - /// Used for generating wrappers for malloc/realloc/free - fn build_wrapped_jmp(&mut self) -> (&'a [u8], u64); - - /// build_proc creates a procedure and outputs it to the wrapped object writer. - /// Returns the procedure bytes, its relocations, and the names of the refcounting functions it references. - fn build_proc( - &mut self, - proc: Proc<'a>, - layout_ids: &mut LayoutIds<'a>, - ) -> (Vec, Vec, Vec<'a, (Symbol, String)>) { - let layout_id = layout_ids.get(proc.name, &proc.ret_layout); - let proc_name = self.symbol_to_string(proc.name, layout_id); - self.reset(proc_name, proc.is_self_recursive); - self.load_args(proc.args, &proc.ret_layout); - for (layout, sym) in proc.args { - self.set_layout_map(*sym, layout); - } - self.scan_ast(&proc.body); - self.create_free_map(); - self.build_stmt(&proc.body, &proc.ret_layout); - let mut helper_proc_names = bumpalo::vec![in self.env().arena]; - helper_proc_names.reserve(self.helper_proc_symbols().len()); - for (rc_proc_sym, rc_proc_layout) in self.helper_proc_symbols() { - let name = layout_ids - .get_toplevel(*rc_proc_sym, rc_proc_layout) - .to_symbol_string(*rc_proc_sym, self.interns()); - - helper_proc_names.push((*rc_proc_sym, name)); - } - let (bytes, relocs) = self.finalize(); - (bytes, relocs, helper_proc_names) - } - - /// build_stmt builds a statement and outputs at the end of the buffer. - fn build_stmt(&mut self, stmt: &Stmt<'a>, ret_layout: &Layout<'a>) { - match stmt { - Stmt::Let(sym, expr, layout, following) => { - self.build_expr(sym, expr, layout); - self.set_layout_map(*sym, layout); - self.free_symbols(stmt); - self.build_stmt(following, ret_layout); - } - Stmt::Ret(sym) => { - self.load_literal_symbols(&[*sym]); - self.return_symbol(sym, ret_layout); - self.free_symbols(stmt); - } - Stmt::Refcounting(modify, following) => { - let sym = modify.get_symbol(); - let layout = *self.layout_map().get(&sym).unwrap(); - - // Expand the Refcounting statement into more detailed IR with a function call - // If this layout requires a new RC proc, we get enough info to create a linker symbol - // for it. Here we don't create linker symbols at this time, but in Wasm backend, we do. - let (rc_stmt, new_specializations) = { - let (env, interns, rc_proc_gen) = self.env_interns_helpers_mut(); - let module_id = env.module_id; - let ident_ids = interns.all_ident_ids.get_mut(&module_id).unwrap(); - - rc_proc_gen.expand_refcount_stmt(ident_ids, layout, modify, *following) - }; - - for spec in new_specializations.into_iter() { - self.helper_proc_symbols_mut().push(spec); - } - - self.build_stmt(rc_stmt, ret_layout) - } - Stmt::Switch { - cond_symbol, - cond_layout, - branches, - default_branch, - ret_layout, - } => { - self.load_literal_symbols(&[*cond_symbol]); - self.build_switch( - cond_symbol, - cond_layout, - branches, - default_branch, - ret_layout, - ); - self.free_symbols(stmt); - } - Stmt::Join { - id, - parameters, - body, - remainder, - } => { - for param in parameters.iter() { - self.set_layout_map(param.symbol, ¶m.layout); - } - self.build_join(id, parameters, body, remainder, ret_layout); - self.free_symbols(stmt); - } - Stmt::Jump(id, args) => { - let mut arg_layouts: bumpalo::collections::Vec> = - bumpalo::vec![in self.env().arena]; - arg_layouts.reserve(args.len()); - let layout_map = self.layout_map(); - for arg in *args { - if let Some(layout) = layout_map.get(arg) { - arg_layouts.push(*layout); - } else { - internal_error!("the argument, {:?}, has no know layout", arg); - } - } - self.build_jump(id, args, arg_layouts.into_bump_slice(), ret_layout); - self.free_symbols(stmt); - } - x => todo!("the statement, {:?}", x), - } - } - // build_switch generates a instructions for a switch statement. - fn build_switch( - &mut self, - cond_symbol: &Symbol, - cond_layout: &Layout<'a>, - branches: &'a [(u64, BranchInfo<'a>, Stmt<'a>)], - default_branch: &(BranchInfo<'a>, &'a Stmt<'a>), - ret_layout: &Layout<'a>, - ); - - // build_join generates a instructions for a join statement. - fn build_join( - &mut self, - id: &JoinPointId, - parameters: &'a [Param<'a>], - body: &'a Stmt<'a>, - remainder: &'a Stmt<'a>, - ret_layout: &Layout<'a>, - ); - - // build_jump generates a instructions for a jump statement. - fn build_jump( - &mut self, - id: &JoinPointId, - args: &[Symbol], - arg_layouts: &[Layout<'a>], - ret_layout: &Layout<'a>, - ); - - /// build_expr builds the expressions for the specified symbol. - /// The builder must keep track of the symbol because it may be referred to later. - fn build_expr(&mut self, sym: &Symbol, expr: &Expr<'a>, layout: &Layout<'a>) { - match expr { - Expr::Literal(lit) => { - if self.env().lazy_literals { - self.literal_map().insert(*sym, (lit, layout)); - } else { - self.load_literal(sym, layout, lit); - } - } - Expr::Call(roc_mono::ir::Call { - call_type, - arguments, - }) => { - match call_type { - CallType::ByName { - name: func_sym, - arg_layouts, - ret_layout, - .. - } => { - if let LowLevelWrapperType::CanBeReplacedBy(lowlevel) = - LowLevelWrapperType::from_symbol(*func_sym) - { - self.build_run_low_level( - sym, - &lowlevel, - arguments, - arg_layouts, - ret_layout, - ) - } else if self.defined_in_app_module(*func_sym) { - let layout_id = LayoutIds::default().get(*func_sym, layout); - let fn_name = self.symbol_to_string(*func_sym, layout_id); - // Now that the arguments are needed, load them if they are literals. - self.load_literal_symbols(arguments); - self.build_fn_call(sym, fn_name, arguments, arg_layouts, ret_layout) - } else { - self.build_builtin(sym, *func_sym, arguments, arg_layouts, ret_layout) - } - } - - CallType::LowLevel { op: lowlevel, .. } => { - let mut arg_layouts: bumpalo::collections::Vec> = - bumpalo::vec![in self.env().arena]; - arg_layouts.reserve(arguments.len()); - let layout_map = self.layout_map(); - for arg in *arguments { - if let Some(layout) = layout_map.get(arg) { - arg_layouts.push(*layout); - } else { - internal_error!("the argument, {:?}, has no know layout", arg); - } - } - self.build_run_low_level( - sym, - lowlevel, - arguments, - arg_layouts.into_bump_slice(), - layout, - ) - } - x => todo!("the call type, {:?}", x), - } - } - Expr::Struct(fields) => { - self.load_literal_symbols(fields); - self.create_struct(sym, layout, fields); - } - Expr::StructAtIndex { - index, - field_layouts, - structure, - } => { - self.load_struct_at_index(sym, structure, *index, field_layouts); - } - Expr::UnionAtIndex { - structure, - tag_id, - union_layout, - index, - } => { - self.load_union_at_index(sym, structure, *tag_id, *index, union_layout); - } - Expr::GetTagId { - structure, - union_layout, - } => { - self.get_tag_id(sym, structure, union_layout); - } - Expr::Tag { - tag_layout, - tag_id, - arguments, - .. - } => { - self.load_literal_symbols(arguments); - self.tag(sym, arguments, tag_layout, *tag_id); - } - x => todo!("the expression, {:?}", x), - } - } - - /// build_run_low_level builds the low level opertation and outputs to the specified symbol. - /// The builder must keep track of the symbol because it may be referred to later. - fn build_run_low_level( - &mut self, - sym: &Symbol, - lowlevel: &LowLevel, - args: &'a [Symbol], - arg_layouts: &[Layout<'a>], - ret_layout: &Layout<'a>, - ) { - // Now that the arguments are needed, load them if they are literals. - self.load_literal_symbols(args); - match lowlevel { - LowLevel::NumAbs => { - debug_assert_eq!( - 1, - args.len(), - "NumAbs: expected to have exactly one argument" - ); - debug_assert_eq!( - arg_layouts[0], *ret_layout, - "NumAbs: expected to have the same argument and return layout" - ); - self.build_num_abs(sym, &args[0], ret_layout) - } - LowLevel::NumAdd => { - debug_assert_eq!( - 2, - args.len(), - "NumAdd: expected to have exactly two argument" - ); - debug_assert_eq!( - arg_layouts[0], arg_layouts[1], - "NumAdd: expected all arguments of to have the same layout" - ); - debug_assert_eq!( - arg_layouts[0], *ret_layout, - "NumAdd: expected to have the same argument and return layout" - ); - self.build_num_add(sym, &args[0], &args[1], ret_layout) - } - LowLevel::NumAcos => self.build_fn_call( - sym, - bitcode::NUM_ACOS[FloatWidth::F64].to_string(), - args, - arg_layouts, - ret_layout, - ), - LowLevel::NumAsin => self.build_fn_call( - sym, - bitcode::NUM_ASIN[FloatWidth::F64].to_string(), - args, - arg_layouts, - ret_layout, - ), - LowLevel::NumAtan => self.build_fn_call( - sym, - bitcode::NUM_ATAN[FloatWidth::F64].to_string(), - args, - arg_layouts, - ret_layout, - ), - LowLevel::NumMul => { - debug_assert_eq!( - 2, - args.len(), - "NumMul: expected to have exactly two argument" - ); - debug_assert_eq!( - arg_layouts[0], arg_layouts[1], - "NumMul: expected all arguments of to have the same layout" - ); - debug_assert_eq!( - arg_layouts[0], *ret_layout, - "NumMul: expected to have the same argument and return layout" - ); - self.build_num_mul(sym, &args[0], &args[1], ret_layout) - } - LowLevel::NumNeg => { - debug_assert_eq!( - 1, - args.len(), - "NumNeg: expected to have exactly one argument" - ); - debug_assert_eq!( - arg_layouts[0], *ret_layout, - "NumNeg: expected to have the same argument and return layout" - ); - self.build_num_neg(sym, &args[0], ret_layout) - } - LowLevel::NumPowInt => self.build_fn_call( - sym, - bitcode::NUM_POW_INT[IntWidth::I64].to_string(), - args, - arg_layouts, - ret_layout, - ), - LowLevel::NumSub => { - debug_assert_eq!( - 2, - args.len(), - "NumSub: expected to have exactly two argument" - ); - debug_assert_eq!( - arg_layouts[0], arg_layouts[1], - "NumSub: expected all arguments of to have the same layout" - ); - debug_assert_eq!( - arg_layouts[0], *ret_layout, - "NumSub: expected to have the same argument and return layout" - ); - self.build_num_sub(sym, &args[0], &args[1], ret_layout) - } - LowLevel::Eq => { - debug_assert_eq!(2, args.len(), "Eq: expected to have exactly two argument"); - debug_assert_eq!( - arg_layouts[0], arg_layouts[1], - "Eq: expected all arguments of to have the same layout" - ); - debug_assert_eq!( - Layout::Builtin(Builtin::Bool), - *ret_layout, - "Eq: expected to have return layout of type Bool" - ); - self.build_eq(sym, &args[0], &args[1], &arg_layouts[0]) - } - LowLevel::NotEq => { - debug_assert_eq!( - 2, - args.len(), - "NotEq: expected to have exactly two argument" - ); - debug_assert_eq!( - arg_layouts[0], arg_layouts[1], - "NotEq: expected all arguments of to have the same layout" - ); - debug_assert_eq!( - Layout::Builtin(Builtin::Bool), - *ret_layout, - "NotEq: expected to have return layout of type Bool" - ); - self.build_neq(sym, &args[0], &args[1], &arg_layouts[0]) - } - LowLevel::NumLt => { - debug_assert_eq!( - 2, - args.len(), - "NumLt: expected to have exactly two argument" - ); - debug_assert_eq!( - arg_layouts[0], arg_layouts[1], - "NumLt: expected all arguments of to have the same layout" - ); - debug_assert_eq!( - Layout::Builtin(Builtin::Bool), - *ret_layout, - "NumLt: expected to have return layout of type Bool" - ); - self.build_num_lt(sym, &args[0], &args[1], &arg_layouts[0]) - } - LowLevel::NumToFrac => { - debug_assert_eq!( - 1, - args.len(), - "NumToFrac: expected to have exactly one argument" - ); - - debug_assert!( - matches!( - *ret_layout, - Layout::Builtin(Builtin::Float(FloatWidth::F32 | FloatWidth::F64)), - ), - "NumToFrac: expected to have return layout of type Float" - ); - self.build_num_to_frac(sym, &args[0], &arg_layouts[0], ret_layout) - } - LowLevel::NumLte => { - debug_assert_eq!( - 2, - args.len(), - "NumLte: expected to have exactly two argument" - ); - debug_assert_eq!( - arg_layouts[0], arg_layouts[1], - "NumLte: expected all arguments of to have the same layout" - ); - debug_assert_eq!( - Layout::Builtin(Builtin::Bool), - *ret_layout, - "NumLte: expected to have return layout of type Bool" - ); - self.build_num_lte(sym, &args[0], &args[1], &arg_layouts[0]) - } - LowLevel::NumGte => { - debug_assert_eq!( - 2, - args.len(), - "NumGte: expected to have exactly two argument" - ); - debug_assert_eq!( - arg_layouts[0], arg_layouts[1], - "NumGte: expected all arguments of to have the same layout" - ); - debug_assert_eq!( - Layout::Builtin(Builtin::Bool), - *ret_layout, - "NumGte: expected to have return layout of type Bool" - ); - self.build_num_gte(sym, &args[0], &args[1], &arg_layouts[0]) - } - LowLevel::NumRound => self.build_fn_call( - sym, - bitcode::NUM_ROUND_F64[IntWidth::I64].to_string(), - args, - arg_layouts, - ret_layout, - ), - LowLevel::ListLen => { - debug_assert_eq!( - 1, - args.len(), - "ListLen: expected to have exactly one argument" - ); - self.build_list_len(sym, &args[0]) - } - LowLevel::ListGetUnsafe => { - debug_assert_eq!( - 2, - args.len(), - "ListGetUnsafe: expected to have exactly two arguments" - ); - self.build_list_get_unsafe(sym, &args[0], &args[1], ret_layout) - } - LowLevel::ListReplaceUnsafe => { - debug_assert_eq!( - 3, - args.len(), - "ListReplaceUnsafe: expected to have exactly three arguments" - ); - self.build_list_replace_unsafe(sym, args, arg_layouts, ret_layout) - } - LowLevel::StrConcat => self.build_fn_call( - sym, - bitcode::STR_CONCAT.to_string(), - args, - arg_layouts, - ret_layout, - ), - LowLevel::PtrCast => { - debug_assert_eq!( - 1, - args.len(), - "RefCountGetPtr: expected to have exactly one argument" - ); - self.build_ptr_cast(sym, &args[0]) - } - LowLevel::RefCountDec => self.build_fn_call( - sym, - bitcode::UTILS_DECREF.to_string(), - args, - arg_layouts, - ret_layout, - ), - LowLevel::RefCountInc => self.build_fn_call( - sym, - bitcode::UTILS_INCREF.to_string(), - args, - arg_layouts, - ret_layout, - ), - x => todo!("low level, {:?}", x), - } - } - - /// Builds a builtin functions that do not map directly to a low level - /// If the builtin is simple enough, it will be inlined. - fn build_builtin( - &mut self, - sym: &Symbol, - func_sym: Symbol, - args: &'a [Symbol], - arg_layouts: &[Layout<'a>], - ret_layout: &Layout<'a>, - ) { - self.load_literal_symbols(args); - match func_sym { - Symbol::NUM_IS_ZERO => { - debug_assert_eq!( - 1, - args.len(), - "NumIsZero: expected to have exactly one argument" - ); - debug_assert_eq!( - Layout::Builtin(Builtin::Bool), - *ret_layout, - "NumIsZero: expected to have return layout of type Bool" - ); - - self.load_literal( - &Symbol::DEV_TMP, - &arg_layouts[0], - &Literal::Int(0i128.to_ne_bytes()), - ); - self.build_eq(sym, &args[0], &Symbol::DEV_TMP, &arg_layouts[0]); - self.free_symbol(&Symbol::DEV_TMP) - } - Symbol::LIST_GET | Symbol::LIST_SET | Symbol::LIST_REPLACE => { - // TODO: This is probably simple enough to be worth inlining. - let layout_id = LayoutIds::default().get(func_sym, ret_layout); - let fn_name = self.symbol_to_string(func_sym, layout_id); - // Now that the arguments are needed, load them if they are literals. - self.load_literal_symbols(args); - self.build_fn_call(sym, fn_name, args, arg_layouts, ret_layout) - } - _ => todo!("the function, {:?}", func_sym), - } - } - - /// build_fn_call creates a call site for a function. - /// This includes dealing with things like saving regs and propagating the returned value. - fn build_fn_call( - &mut self, - dst: &Symbol, - fn_name: String, - args: &[Symbol], - arg_layouts: &[Layout<'a>], - ret_layout: &Layout<'a>, - ); - - /// build_num_abs stores the absolute value of src into dst. - fn build_num_abs(&mut self, dst: &Symbol, src: &Symbol, layout: &Layout<'a>); - - /// build_num_add stores the sum of src1 and src2 into dst. - fn build_num_add(&mut self, dst: &Symbol, src1: &Symbol, src2: &Symbol, layout: &Layout<'a>); - - /// build_num_mul stores `src1 * src2` into dst. - fn build_num_mul(&mut self, dst: &Symbol, src1: &Symbol, src2: &Symbol, layout: &Layout<'a>); - - /// build_num_neg stores the negated value of src into dst. - fn build_num_neg(&mut self, dst: &Symbol, src: &Symbol, layout: &Layout<'a>); - - /// build_num_sub stores the `src1 - src2` difference into dst. - fn build_num_sub(&mut self, dst: &Symbol, src1: &Symbol, src2: &Symbol, layout: &Layout<'a>); - - /// build_eq stores the result of `src1 == src2` into dst. - fn build_eq(&mut self, dst: &Symbol, src1: &Symbol, src2: &Symbol, arg_layout: &Layout<'a>); - - /// build_neq stores the result of `src1 != src2` into dst. - fn build_neq(&mut self, dst: &Symbol, src1: &Symbol, src2: &Symbol, arg_layout: &Layout<'a>); - - /// build_num_lt stores the result of `src1 < src2` into dst. - fn build_num_lt(&mut self, dst: &Symbol, src1: &Symbol, src2: &Symbol, arg_layout: &Layout<'a>); - - /// build_num_to_frac convert Number to Frac - fn build_num_to_frac( - &mut self, - dst: &Symbol, - src: &Symbol, - arg_layout: &Layout<'a>, - ret_layout: &Layout<'a>, - ); - - /// build_num_lte stores the result of `src1 <= src2` into dst. - fn build_num_lte( - &mut self, - dst: &Symbol, - src1: &Symbol, - src2: &Symbol, - arg_layout: &Layout<'a>, - ); - - /// build_num_gte stores the result of `src1 >= src2` into dst. - fn build_num_gte( - &mut self, - dst: &Symbol, - src1: &Symbol, - src2: &Symbol, - arg_layout: &Layout<'a>, - ); - - /// build_list_len returns the length of a list. - fn build_list_len(&mut self, dst: &Symbol, list: &Symbol); - - /// build_list_get_unsafe loads the element from the list at the index. - fn build_list_get_unsafe( - &mut self, - dst: &Symbol, - list: &Symbol, - index: &Symbol, - ret_layout: &Layout<'a>, - ); - - /// build_list_replace_unsafe returns the old element and new list with the list having the new element inserted. - fn build_list_replace_unsafe( - &mut self, - dst: &Symbol, - args: &'a [Symbol], - arg_layouts: &[Layout<'a>], - ret_layout: &Layout<'a>, - ); - - /// build_refcount_getptr loads the pointer to the reference count of src into dst. - fn build_ptr_cast(&mut self, dst: &Symbol, src: &Symbol); - - /// literal_map gets the map from symbol to literal and layout, used for lazy loading and literal folding. - fn literal_map(&mut self) -> &mut MutMap, *const Layout<'a>)>; - - fn load_literal_symbols(&mut self, syms: &[Symbol]) { - if self.env().lazy_literals { - for sym in syms { - if let Some((lit, layout)) = self.literal_map().remove(sym) { - // This operation is always safe but complicates lifetimes. - // The map is reset when building a procedure and then used for that single procedure. - // Since the lifetime is shorter than the entire backend, we need to use a pointer. - let (lit, layout) = unsafe { (*lit, *layout) }; - self.load_literal(sym, &layout, &lit); - } - } - } - } - - /// load_literal sets a symbol to be equal to a literal. - fn load_literal(&mut self, sym: &Symbol, layout: &Layout<'a>, lit: &Literal<'a>); - - /// create_struct creates a struct with the elements specified loaded into it as data. - fn create_struct(&mut self, sym: &Symbol, layout: &Layout<'a>, fields: &'a [Symbol]); - - /// load_struct_at_index loads into `sym` the value at `index` in `structure`. - fn load_struct_at_index( - &mut self, - sym: &Symbol, - structure: &Symbol, - index: u64, - field_layouts: &'a [Layout<'a>], - ); - - /// load_union_at_index loads into `sym` the value at `index` for `tag_id`. - fn load_union_at_index( - &mut self, - sym: &Symbol, - structure: &Symbol, - tag_id: TagIdIntType, - index: u64, - union_layout: &UnionLayout<'a>, - ); - - /// get_tag_id loads the tag id from a the union. - fn get_tag_id(&mut self, sym: &Symbol, structure: &Symbol, union_layout: &UnionLayout<'a>); - - /// tag sets the tag for a union. - fn tag( - &mut self, - sym: &Symbol, - args: &'a [Symbol], - tag_layout: &UnionLayout<'a>, - tag_id: TagIdIntType, - ); - - /// return_symbol moves a symbol to the correct return location for the backend and adds a jump to the end of the function. - fn return_symbol(&mut self, sym: &Symbol, layout: &Layout<'a>); - - /// free_symbols will free all symbols for the given statement. - fn free_symbols(&mut self, stmt: &Stmt<'a>) { - if let Some(syms) = self.free_map().remove(&(stmt as *const Stmt<'a>)) { - for sym in syms { - // println!("Freeing symbol: {:?}", sym); - self.free_symbol(&sym); - } - } - } - - /// free_symbol frees any registers or stack space used to hold a symbol. - fn free_symbol(&mut self, sym: &Symbol); - - /// set_last_seen sets the statement a symbol was last seen in. - fn set_last_seen(&mut self, sym: Symbol, stmt: &Stmt<'a>) { - self.last_seen_map().insert(sym, stmt); - } - - /// last_seen_map gets the map from symbol to when it is last seen in the function. - fn last_seen_map(&mut self) -> &mut MutMap>; - - /// set_layout_map sets the layout for a specific symbol. - fn set_layout_map(&mut self, sym: Symbol, layout: &Layout<'a>) { - if let Some(old_layout) = self.layout_map().insert(sym, *layout) { - // Layout map already contains the symbol. We should never need to overwrite. - // If the layout is not the same, that is a bug. - if &old_layout != layout { - internal_error!( - "Overwriting layout for symbol, {:?}: got {:?}, want {:?}", - sym, - layout, - old_layout - ) - } - } - } - - /// layout_map gets the map from symbol to layout. - fn layout_map(&mut self) -> &mut MutMap>; - - fn create_free_map(&mut self) { - let mut free_map = MutMap::default(); - let arena = self.env().arena; - for (sym, stmt) in self.last_seen_map() { - let vals = free_map - .entry(*stmt) - .or_insert_with(|| bumpalo::vec![in arena]); - vals.push(*sym); - } - self.set_free_map(free_map); - } - - /// free_map gets the map statement to the symbols that are free after they run. - fn free_map(&mut self) -> &mut MutMap<*const Stmt<'a>, Vec<'a, Symbol>>; - - /// set_free_map sets the free map to the given map. - fn set_free_map(&mut self, map: MutMap<*const Stmt<'a>, Vec<'a, Symbol>>); - - /// scan_ast runs through the ast and fill the last seen map. - /// This must iterate through the ast in the same way that build_stmt does. i.e. then before else. - fn scan_ast(&mut self, stmt: &Stmt<'a>) { - // Join map keeps track of join point parameters so that we can keep them around while they still might be jumped to. - let mut join_map: MutMap]> = MutMap::default(); - match stmt { - Stmt::Let(sym, expr, _, following) => { - self.set_last_seen(*sym, stmt); - match expr { - Expr::Literal(_) => {} - - Expr::Call(call) => self.scan_ast_call(call, stmt), - - Expr::Tag { arguments, .. } => { - for sym in *arguments { - self.set_last_seen(*sym, stmt); - } - } - Expr::ExprBox { symbol } => { - self.set_last_seen(*symbol, stmt); - } - Expr::ExprUnbox { symbol } => { - self.set_last_seen(*symbol, stmt); - } - Expr::Struct(syms) => { - for sym in *syms { - self.set_last_seen(*sym, stmt); - } - } - Expr::StructAtIndex { structure, .. } => { - self.set_last_seen(*structure, stmt); - } - Expr::GetTagId { structure, .. } => { - self.set_last_seen(*structure, stmt); - } - Expr::UnionAtIndex { structure, .. } => { - self.set_last_seen(*structure, stmt); - } - Expr::Array { elems, .. } => { - for elem in *elems { - if let ListLiteralElement::Symbol(sym) = elem { - self.set_last_seen(*sym, stmt); - } - } - } - Expr::Reuse { - symbol, - arguments, - tag_name, - .. - } => { - self.set_last_seen(*symbol, stmt); - match tag_name { - TagOrClosure::Closure(sym) => { - self.set_last_seen(*sym, stmt); - } - TagOrClosure::Tag(_) => {} - } - for sym in *arguments { - self.set_last_seen(*sym, stmt); - } - } - Expr::Reset { symbol, .. } => { - self.set_last_seen(*symbol, stmt); - } - Expr::EmptyArray => {} - Expr::RuntimeErrorFunction(_) => {} - } - self.scan_ast(following); - } - - Stmt::Switch { - cond_symbol, - branches, - default_branch, - .. - } => { - self.set_last_seen(*cond_symbol, stmt); - for (_, _, branch) in *branches { - self.scan_ast(branch); - } - self.scan_ast(default_branch.1); - } - Stmt::Ret(sym) => { - self.set_last_seen(*sym, stmt); - } - Stmt::Refcounting(modify, following) => { - let sym = modify.get_symbol(); - - self.set_last_seen(sym, stmt); - self.scan_ast(following); - } - Stmt::Join { - parameters, - body: continuation, - remainder, - id: JoinPointId(sym), - .. - } => { - self.set_last_seen(*sym, stmt); - join_map.insert(JoinPointId(*sym), parameters); - for param in *parameters { - self.set_last_seen(param.symbol, stmt); - } - self.scan_ast(remainder); - self.scan_ast(continuation); - } - Stmt::Jump(JoinPointId(sym), symbols) => { - if let Some(parameters) = join_map.get(&JoinPointId(*sym)) { - // Keep the parameters around. They will be overwritten when jumping. - for param in *parameters { - self.set_last_seen(param.symbol, stmt); - } - } - for sym in *symbols { - self.set_last_seen(*sym, stmt); - } - } - - Stmt::Expect { .. } => todo!("expect is not implemented in the wasm backend"), - - Stmt::RuntimeError(_) => {} - } - } - - fn scan_ast_call(&mut self, call: &roc_mono::ir::Call, stmt: &roc_mono::ir::Stmt<'a>) { - let roc_mono::ir::Call { - call_type, - arguments, - } = call; - - for sym in *arguments { - self.set_last_seen(*sym, stmt); - } - - match call_type { - CallType::ByName { .. } => {} - CallType::LowLevel { .. } => {} - CallType::HigherOrder { .. } => {} - CallType::Foreign { .. } => {} - } - } -} diff --git a/compiler/gen_dev/src/object_builder.rs b/compiler/gen_dev/src/object_builder.rs deleted file mode 100644 index 92c01583d1..0000000000 --- a/compiler/gen_dev/src/object_builder.rs +++ /dev/null @@ -1,476 +0,0 @@ -use crate::generic64::{aarch64, new_backend_64bit, x86_64}; -use crate::{Backend, Env, Relocation}; -use bumpalo::collections::Vec; -use object::write::{self, SectionId, SymbolId}; -use object::write::{Object, StandardSection, StandardSegment, Symbol, SymbolSection}; -use object::{ - Architecture, BinaryFormat, Endianness, RelocationEncoding, RelocationKind, SectionKind, - SymbolFlags, SymbolKind, SymbolScope, -}; -use roc_collections::all::MutMap; -use roc_error_macros::internal_error; -use roc_module::symbol; -use roc_module::symbol::Interns; -use roc_mono::ir::{Proc, ProcLayout}; -use roc_mono::layout::LayoutIds; -use roc_target::TargetInfo; -use target_lexicon::{Architecture as TargetArch, BinaryFormat as TargetBF, Triple}; - -// This is used by some code below which is currently commented out. -// See that code for more details! -// const VERSION: &str = env!("CARGO_PKG_VERSION"); - -/// build_module is the high level builder/delegator. -/// It takes the request to build a module and output the object file for the module. -pub fn build_module<'a>( - env: &'a Env, - interns: &'a mut Interns, - target: &Triple, - procedures: MutMap<(symbol::Symbol, ProcLayout<'a>), Proc<'a>>, -) -> Object { - match target { - Triple { - architecture: TargetArch::X86_64, - binary_format: TargetBF::Elf, - .. - } if cfg!(feature = "target-x86_64") => { - let backend = new_backend_64bit::< - x86_64::X86_64GeneralReg, - x86_64::X86_64FloatReg, - x86_64::X86_64Assembler, - x86_64::X86_64SystemV, - >(env, TargetInfo::default_x86_64(), interns); - build_object( - procedures, - backend, - Object::new(BinaryFormat::Elf, Architecture::X86_64, Endianness::Little), - ) - } - Triple { - architecture: TargetArch::X86_64, - binary_format: TargetBF::Macho, - .. - } if cfg!(feature = "target-x86_64") => { - let backend = new_backend_64bit::< - x86_64::X86_64GeneralReg, - x86_64::X86_64FloatReg, - x86_64::X86_64Assembler, - x86_64::X86_64SystemV, - >(env, TargetInfo::default_x86_64(), interns); - build_object( - procedures, - backend, - Object::new( - BinaryFormat::MachO, - Architecture::X86_64, - Endianness::Little, - ), - ) - } - Triple { - architecture: TargetArch::Aarch64(_), - binary_format: TargetBF::Elf, - .. - } if cfg!(feature = "target-aarch64") => { - let backend = new_backend_64bit::< - aarch64::AArch64GeneralReg, - aarch64::AArch64FloatReg, - aarch64::AArch64Assembler, - aarch64::AArch64Call, - >(env, TargetInfo::default_aarch64(), interns); - build_object( - procedures, - backend, - Object::new(BinaryFormat::Elf, Architecture::Aarch64, Endianness::Little), - ) - } - Triple { - architecture: TargetArch::Aarch64(_), - binary_format: TargetBF::Macho, - .. - } if cfg!(feature = "target-aarch64") => { - let backend = new_backend_64bit::< - aarch64::AArch64GeneralReg, - aarch64::AArch64FloatReg, - aarch64::AArch64Assembler, - aarch64::AArch64Call, - >(env, TargetInfo::default_aarch64(), interns); - build_object( - procedures, - backend, - Object::new( - BinaryFormat::MachO, - Architecture::Aarch64, - Endianness::Little, - ), - ) - } - x => unimplemented!("the target, {:?}", x), - } -} - -fn generate_wrapper<'a, B: Backend<'a>>( - backend: &mut B, - output: &mut Object, - wrapper_name: String, - wraps: String, -) { - let text_section = output.section_id(StandardSection::Text); - let proc_symbol = Symbol { - name: wrapper_name.as_bytes().to_vec(), - value: 0, - size: 0, - kind: SymbolKind::Text, - scope: SymbolScope::Dynamic, - weak: false, - section: SymbolSection::Section(text_section), - flags: SymbolFlags::None, - }; - let proc_id = output.add_symbol(proc_symbol); - let (proc_data, offset) = backend.build_wrapped_jmp(); - let proc_offset = output.add_symbol_data(proc_id, text_section, proc_data, 16); - - let name = wraps.as_str().as_bytes(); - // If the symbol is an undefined zig builtin, we need to add it here. - let symbol = Symbol { - name: name.to_vec(), - value: 0, - size: 0, - kind: SymbolKind::Text, - scope: SymbolScope::Dynamic, - weak: true, - section: SymbolSection::Undefined, - flags: SymbolFlags::None, - }; - output.add_symbol(symbol); - if let Some(sym_id) = output.symbol_id(name) { - let reloc = write::Relocation { - offset: offset + proc_offset, - size: 32, - kind: RelocationKind::PltRelative, - encoding: RelocationEncoding::X86Branch, - symbol: sym_id, - addend: -4, - }; - - match output.add_relocation(text_section, reloc) { - Ok(obj) => obj, - Err(e) => internal_error!("{:?}", e), - } - } else { - internal_error!("failed to find fn symbol for {:?}", wraps); - } -} - -fn build_object<'a, B: Backend<'a>>( - procedures: MutMap<(symbol::Symbol, ProcLayout<'a>), Proc<'a>>, - mut backend: B, - mut output: Object, -) -> Object { - let data_section = output.section_id(StandardSection::Data); - - let arena = backend.env().arena; - - /* - // Commented out because we couldn't figure out how to get it to work on mac - see https://github.com/rtfeldman/roc/pull/1323 - let comment = output.add_section(vec![], b".comment".to_vec(), SectionKind::OtherString); - output.append_section_data( - comment, - format!("\0roc dev backend version {} \0", VERSION).as_bytes(), - 1, - ); - */ - - if backend.env().generate_allocators { - generate_wrapper( - &mut backend, - &mut output, - "roc_alloc".into(), - "malloc".into(), - ); - generate_wrapper( - &mut backend, - &mut output, - "roc_realloc".into(), - "realloc".into(), - ); - generate_wrapper( - &mut backend, - &mut output, - "roc_dealloc".into(), - "free".into(), - ); - generate_wrapper( - &mut backend, - &mut output, - "roc_panic".into(), - "roc_builtins.utils.test_panic".into(), - ); - } - - // Setup layout_ids for procedure calls. - let mut layout_ids = LayoutIds::default(); - let mut procs = Vec::with_capacity_in(procedures.len(), arena); - - // Names and linker data for user procedures - for ((sym, layout), proc) in procedures { - build_proc_symbol( - &mut output, - &mut layout_ids, - &mut procs, - &backend, - sym, - layout, - proc, - ) - } - - // Build procedures from user code - let mut relocations = bumpalo::vec![in arena]; - for (fn_name, section_id, proc_id, proc) in procs { - build_proc( - &mut output, - &mut backend, - &mut relocations, - &mut layout_ids, - data_section, - fn_name, - section_id, - proc_id, - proc, - ) - } - - // Generate IR for specialized helper procs (refcounting & equality) - let helper_procs = { - let module_id = backend.env().module_id; - - let (env, interns, helper_proc_gen) = backend.env_interns_helpers_mut(); - - let ident_ids = interns.all_ident_ids.get_mut(&module_id).unwrap(); - let helper_procs = helper_proc_gen.take_procs(); - env.module_id.register_debug_idents(ident_ids); - - helper_procs - }; - - let empty = bumpalo::collections::Vec::new_in(arena); - let helper_symbols_and_layouts = std::mem::replace(backend.helper_proc_symbols_mut(), empty); - let mut helper_names_symbols_procs = Vec::with_capacity_in(helper_procs.len(), arena); - - // Names and linker data for helpers - for ((sym, layout), proc) in helper_symbols_and_layouts.into_iter().zip(helper_procs) { - let layout_id = layout_ids.get_toplevel(sym, &layout); - let fn_name = backend.symbol_to_string(sym, layout_id); - if let Some(proc_id) = output.symbol_id(fn_name.as_bytes()) { - if let SymbolSection::Section(section_id) = output.symbol(proc_id).section { - helper_names_symbols_procs.push((fn_name, section_id, proc_id, proc)); - continue; - } - } else { - // The symbol isn't defined yet and will just be used by other rc procs. - let section_id = output.add_section( - output.segment_name(StandardSegment::Text).to_vec(), - format!(".text.{:x}", sym.as_u64()).as_bytes().to_vec(), - SectionKind::Text, - ); - - let rc_symbol = Symbol { - name: fn_name.as_bytes().to_vec(), - value: 0, - size: 0, - kind: SymbolKind::Text, - scope: SymbolScope::Linkage, - weak: false, - section: SymbolSection::Section(section_id), - flags: SymbolFlags::None, - }; - let proc_id = output.add_symbol(rc_symbol); - helper_names_symbols_procs.push((fn_name, section_id, proc_id, proc)); - continue; - } - internal_error!("failed to create rc fn for symbol {:?}", sym); - } - - // Build helpers - for (fn_name, section_id, proc_id, proc) in helper_names_symbols_procs { - build_proc( - &mut output, - &mut backend, - &mut relocations, - &mut layout_ids, - data_section, - fn_name, - section_id, - proc_id, - proc, - ) - } - - // Relocations for all procedures (user code & helpers) - for (section_id, reloc) in relocations { - match output.add_relocation(section_id, reloc) { - Ok(obj) => obj, - Err(e) => internal_error!("{:?}", e), - } - } - output -} - -fn build_proc_symbol<'a, B: Backend<'a>>( - output: &mut Object, - layout_ids: &mut LayoutIds<'a>, - procs: &mut Vec<'a, (String, SectionId, SymbolId, Proc<'a>)>, - backend: &B, - sym: roc_module::symbol::Symbol, - layout: ProcLayout<'a>, - proc: Proc<'a>, -) { - let layout_id = layout_ids.get_toplevel(sym, &layout); - let base_name = backend.symbol_to_string(sym, layout_id); - - let fn_name = if backend.env().exposed_to_host.contains(&sym) { - format!("roc_{}_exposed", base_name) - } else { - base_name - }; - - let section_id = output.add_section( - output.segment_name(StandardSegment::Text).to_vec(), - format!(".text.{:x}", sym.as_u64()).as_bytes().to_vec(), - SectionKind::Text, - ); - - let proc_symbol = Symbol { - name: fn_name.as_bytes().to_vec(), - value: 0, - size: 0, - kind: SymbolKind::Text, - // TODO: Depending on whether we are building a static or dynamic lib, this should change. - // We should use Dynamic -> anyone, Linkage -> static link, Compilation -> this module only. - scope: if backend.env().exposed_to_host.contains(&sym) { - SymbolScope::Dynamic - } else { - SymbolScope::Linkage - }, - weak: false, - section: SymbolSection::Section(section_id), - flags: SymbolFlags::None, - }; - let proc_id = output.add_symbol(proc_symbol); - procs.push((fn_name, section_id, proc_id, proc)); -} - -#[allow(clippy::too_many_arguments)] -fn build_proc<'a, B: Backend<'a>>( - output: &mut Object, - backend: &mut B, - relocations: &mut Vec<'a, (SectionId, object::write::Relocation)>, - layout_ids: &mut LayoutIds<'a>, - data_section: SectionId, - fn_name: String, - section_id: SectionId, - proc_id: SymbolId, - proc: Proc<'a>, -) { - let mut local_data_index = 0; - let (proc_data, relocs, rc_proc_names) = backend.build_proc(proc, layout_ids); - let proc_offset = output.add_symbol_data(proc_id, section_id, &proc_data, 16); - for reloc in relocs.iter() { - let elfreloc = match reloc { - Relocation::LocalData { offset, data } => { - let data_symbol = write::Symbol { - name: format!("{}.data{}", fn_name, local_data_index) - .as_bytes() - .to_vec(), - value: 0, - size: 0, - kind: SymbolKind::Data, - scope: SymbolScope::Compilation, - weak: false, - section: SymbolSection::Section(data_section), - flags: SymbolFlags::None, - }; - local_data_index += 1; - let data_id = output.add_symbol(data_symbol); - output.add_symbol_data(data_id, data_section, data, 4); - write::Relocation { - offset: offset + proc_offset, - size: 32, - kind: RelocationKind::Relative, - encoding: RelocationEncoding::Generic, - symbol: data_id, - addend: -4, - } - } - Relocation::LinkedData { offset, name } => { - if let Some(sym_id) = output.symbol_id(name.as_bytes()) { - write::Relocation { - offset: offset + proc_offset, - size: 32, - kind: RelocationKind::GotRelative, - encoding: RelocationEncoding::Generic, - symbol: sym_id, - addend: -4, - } - } else { - internal_error!("failed to find data symbol for {:?}", name); - } - } - Relocation::LinkedFunction { offset, name } => { - // If the symbol is an undefined zig builtin, we need to add it here. - if output.symbol_id(name.as_bytes()) == None && name.starts_with("roc_builtins.") { - let builtin_symbol = Symbol { - name: name.as_bytes().to_vec(), - value: 0, - size: 0, - kind: SymbolKind::Text, - scope: SymbolScope::Linkage, - weak: false, - section: SymbolSection::Undefined, - flags: SymbolFlags::None, - }; - output.add_symbol(builtin_symbol); - } - // If the symbol is an undefined reference counting procedure, we need to add it here. - if output.symbol_id(name.as_bytes()) == None { - for (sym, rc_name) in rc_proc_names.iter() { - if name == rc_name { - let section_id = output.add_section( - output.segment_name(StandardSegment::Text).to_vec(), - format!(".text.{:x}", sym.as_u64()).as_bytes().to_vec(), - SectionKind::Text, - ); - - let rc_symbol = Symbol { - name: name.as_bytes().to_vec(), - value: 0, - size: 0, - kind: SymbolKind::Text, - scope: SymbolScope::Linkage, - weak: false, - section: SymbolSection::Section(section_id), - flags: SymbolFlags::None, - }; - output.add_symbol(rc_symbol); - } - } - } - if let Some(sym_id) = output.symbol_id(name.as_bytes()) { - write::Relocation { - offset: offset + proc_offset, - size: 32, - kind: RelocationKind::PltRelative, - encoding: RelocationEncoding::X86Branch, - symbol: sym_id, - addend: -4, - } - } else { - internal_error!("failed to find fn symbol for {:?}", name); - } - } - Relocation::JmpToReturn { .. } => unreachable!(), - }; - relocations.push((section_id, elfreloc)); - } -} diff --git a/compiler/gen_dev/src/run_roc.rs b/compiler/gen_dev/src/run_roc.rs deleted file mode 100644 index 981a60f8ae..0000000000 --- a/compiler/gen_dev/src/run_roc.rs +++ /dev/null @@ -1,31 +0,0 @@ -#[macro_export] -/// run_jit_function_raw runs an unwrapped jit function. -/// The function could throw an exception and break things, or worse, it could not throw an exception and break things. -/// This functions is generally a bad idea with an untrused backend, but is being used for now for development purposes. -macro_rules! run_jit_function_raw { - ($lib: expr, $main_fn_name: expr, $ty:ty, $transform:expr) => {{ - let v: std::vec::Vec = std::vec::Vec::new(); - run_jit_function_raw!($lib, $main_fn_name, $ty, $transform, v) - }}; - - ($lib: expr, $main_fn_name: expr, $ty:ty, $transform:expr, $errors:expr) => {{ - unsafe { - let main: libloading::Symbol $ty> = $lib - .get($main_fn_name.as_bytes()) - .ok() - .ok_or(format!("Unable to JIT compile `{}`", $main_fn_name)) - .expect("errored"); - - let result = main(); - - assert_eq!( - $errors, - std::vec::Vec::new(), - "Encountered errors: {:?}", - $errors - ); - - $transform(result) - } - }}; -} diff --git a/compiler/gen_llvm/Cargo.toml b/compiler/gen_llvm/Cargo.toml deleted file mode 100644 index 8093e436ed..0000000000 --- a/compiler/gen_llvm/Cargo.toml +++ /dev/null @@ -1,22 +0,0 @@ -[package] -name = "roc_gen_llvm" -description = "The LLVM backend for the Roc compiler" -version = "0.1.0" -authors = ["The Roc Contributors"] -license = "UPL-1.0" -edition = "2021" - -[dependencies] -roc_alias_analysis = { path = "../alias_analysis" } -roc_collections = { path = "../collections" } -roc_module = { path = "../module" } -roc_builtins = { path = "../builtins" } -roc_error_macros = { path = "../../error_macros" } -roc_mono = { path = "../mono" } -roc_target = { path = "../roc_target" } -roc_std = { path = "../../roc_std", default-features = false } -roc_debug_flags = { path = "../debug_flags" } -morphic_lib = { path = "../../vendor/morphic_lib" } -bumpalo = { version = "3.8.0", features = ["collections"] } -inkwell = { path = "../../vendor/inkwell" } -target-lexicon = "0.12.3" diff --git a/compiler/gen_llvm/src/lib.rs b/compiler/gen_llvm/src/lib.rs deleted file mode 100644 index bef97f894c..0000000000 --- a/compiler/gen_llvm/src/lib.rs +++ /dev/null @@ -1,9 +0,0 @@ -#![warn(clippy::dbg_macro)] -// See github.com/rtfeldman/roc/issues/800 for discussion of the large_enum_variant check. -#![allow(clippy::large_enum_variant)] -// we actually want to compare against the literal float bits -#![allow(clippy::float_cmp)] - -pub mod llvm; - -pub mod run_roc; diff --git a/compiler/gen_llvm/src/llvm/bitcode.rs b/compiler/gen_llvm/src/llvm/bitcode.rs deleted file mode 100644 index d3b5b5a86f..0000000000 --- a/compiler/gen_llvm/src/llvm/bitcode.rs +++ /dev/null @@ -1,804 +0,0 @@ -/// Helpers for interacting with the zig that generates bitcode -use crate::debug_info_init; -use crate::llvm::build::{ - complex_bitcast_check_size, load_roc_value, struct_from_fields, to_cc_return, CCReturn, Env, - C_CALL_CONV, FAST_CALL_CONV, TAG_DATA_INDEX, -}; -use crate::llvm::convert::basic_type_from_layout; -use crate::llvm::refcounting::{ - decrement_refcount_layout, increment_n_refcount_layout, increment_refcount_layout, -}; -use inkwell::attributes::{Attribute, AttributeLoc}; -use inkwell::types::{BasicType, BasicTypeEnum}; -use inkwell::values::{BasicValue, BasicValueEnum, CallSiteValue, FunctionValue, InstructionValue}; -use inkwell::AddressSpace; -use roc_error_macros::internal_error; -use roc_module::symbol::Symbol; -use roc_mono::layout::{LambdaSet, Layout, LayoutIds, UnionLayout}; - -use super::build::create_entry_block_alloca; - -use std::convert::TryInto; - -pub fn call_bitcode_fn<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - args: &[BasicValueEnum<'ctx>], - fn_name: &str, -) -> BasicValueEnum<'ctx> { - call_bitcode_fn_help(env, args, fn_name) - .try_as_basic_value() - .left() - .unwrap_or_else(|| { - panic!( - "LLVM error: Did not get return value from bitcode function {:?}", - fn_name - ) - }) -} - -pub fn call_list_bitcode_fn<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - args: &[BasicValueEnum<'ctx>], - fn_name: &str, -) -> BasicValueEnum<'ctx> { - use bumpalo::collections::Vec; - - let parent = env - .builder - .get_insert_block() - .and_then(|b| b.get_parent()) - .unwrap(); - - let list_type = super::convert::zig_list_type(env); - let result = create_entry_block_alloca(env, parent, list_type.into(), "list_alloca"); - let mut arguments: Vec = Vec::with_capacity_in(args.len() + 1, env.arena); - - arguments.push(result.into()); - arguments.extend(args); - - call_void_bitcode_fn(env, &arguments, fn_name); - - env.builder.build_load(result, "load_list") -} - -pub fn call_str_bitcode_fn<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - args: &[BasicValueEnum<'ctx>], - fn_name: &str, -) -> BasicValueEnum<'ctx> { - use bumpalo::collections::Vec; - - let parent = env - .builder - .get_insert_block() - .and_then(|b| b.get_parent()) - .unwrap(); - - let str_type = super::convert::zig_str_type(env); - - match env.target_info.ptr_width() { - roc_target::PtrWidth::Bytes4 => { - // 3 machine words actually fit into 2 registers - call_bitcode_fn(env, args, fn_name) - } - roc_target::PtrWidth::Bytes8 => { - let result = - create_entry_block_alloca(env, parent, str_type.into(), "return_str_alloca"); - let mut arguments: Vec = - Vec::with_capacity_in(args.len() + 1, env.arena); - - arguments.push(result.into()); - arguments.extend(args); - - call_void_bitcode_fn(env, &arguments, fn_name); - - result.into() - } - } -} - -pub fn call_void_bitcode_fn<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - args: &[BasicValueEnum<'ctx>], - fn_name: &str, -) -> InstructionValue<'ctx> { - call_bitcode_fn_help(env, args, fn_name) - .try_as_basic_value() - .right() - .unwrap_or_else(|| panic!("LLVM error: Tried to call void bitcode function, but got return value from bitcode function, {:?}", fn_name)) -} - -fn call_bitcode_fn_help<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - args: &[BasicValueEnum<'ctx>], - fn_name: &str, -) -> CallSiteValue<'ctx> { - let it = args.iter().map(|x| (*x).into()); - let arguments = bumpalo::collections::Vec::from_iter_in(it, env.arena); - - let fn_val = env - .module - .get_function(fn_name) - .unwrap_or_else(|| panic!("Unrecognized builtin function: {:?} - if you're working on the Roc compiler, do you need to rebuild the bitcode? See compiler/builtins/bitcode/README.md", fn_name)); - - let call = env.builder.build_call(fn_val, &arguments, "call_builtin"); - - // Attributes that we propagate from the zig builtin parameters, to the arguments we give to the - // call. It is undefined behavior in LLVM to have an attribute on a parameter, and then call - // the function where that parameter is not present. For many (e.g. nonnull) it can be inferred - // but e.g. byval and sret cannot and must be explicitly provided. - let propagate = [ - Attribute::get_named_enum_kind_id("nonnull"), - Attribute::get_named_enum_kind_id("nocapture"), - Attribute::get_named_enum_kind_id("readonly"), - Attribute::get_named_enum_kind_id("noalias"), - Attribute::get_named_enum_kind_id("sret"), - Attribute::get_named_enum_kind_id("byval"), - ]; - - for i in 0..fn_val.count_params() { - let attributes = fn_val.attributes(AttributeLoc::Param(i)); - - for attribute in attributes { - let kind_id = attribute.get_enum_kind_id(); - - if propagate.contains(&kind_id) { - call.add_attribute(AttributeLoc::Param(i), attribute) - } - } - } - - call.set_call_convention(fn_val.get_call_conventions()); - call -} - -pub fn call_bitcode_fn_fixing_for_convention<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - args: &[BasicValueEnum<'ctx>], - return_layout: &Layout<'_>, - fn_name: &str, -) -> BasicValueEnum<'ctx> { - // Calling zig bitcode, so we must follow C calling conventions. - let cc_return = to_cc_return(env, return_layout); - match cc_return { - CCReturn::Return => { - // We'll get a return value - call_bitcode_fn(env, args, fn_name) - } - CCReturn::ByPointer => { - // We need to pass the return value by pointer. - let roc_return_type = basic_type_from_layout(env, return_layout); - - let cc_ptr_return_type = env - .module - .get_function(fn_name) - .unwrap() - .get_type() - .get_param_types()[0] - .into_pointer_type(); - let cc_return_type: BasicTypeEnum<'ctx> = cc_ptr_return_type - .get_element_type() - .try_into() - .expect("Zig bitcode return type is not a basic type!"); - - let cc_return_value_ptr = env.builder.build_alloca(cc_return_type, "return_value"); - let fixed_args: Vec> = [cc_return_value_ptr.into()] - .iter() - .chain(args) - .copied() - .collect(); - call_void_bitcode_fn(env, &fixed_args, fn_name); - - let cc_return_value = env.builder.build_load(cc_return_value_ptr, "read_result"); - if roc_return_type.size_of() == cc_return_type.size_of() { - cc_return_value - } else { - // We need to convert the C-callconv return type, which may be larger than the Roc - // return type, into the Roc return type. - complex_bitcast_check_size( - env, - cc_return_value, - roc_return_type, - "c_value_to_roc_value", - ) - } - } - CCReturn::Void => { - internal_error!("Tried to call valued bitcode function, but it has no return type") - } - } -} - -const ARGUMENT_SYMBOLS: [Symbol; 8] = [ - Symbol::ARG_1, - Symbol::ARG_2, - Symbol::ARG_3, - Symbol::ARG_4, - Symbol::ARG_5, - Symbol::ARG_6, - Symbol::ARG_7, - Symbol::ARG_8, -]; - -pub fn build_has_tag_id<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - function: FunctionValue<'ctx>, - union_layout: UnionLayout<'a>, -) -> FunctionValue<'ctx> { - let fn_name: &str = &format!("{}_has_tag_id", function.get_name().to_string_lossy()); - - // currently the code assumes we're dealing with a non-recursive layout - debug_assert!(matches!(union_layout, UnionLayout::NonRecursive(_))); - - match env.module.get_function(fn_name) { - Some(function_value) => function_value, - None => build_has_tag_id_help(env, union_layout, fn_name), - } -} - -fn build_has_tag_id_help<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - union_layout: UnionLayout<'a>, - fn_name: &str, -) -> FunctionValue<'ctx> { - let i8_ptr_type = env.context.i8_type().ptr_type(AddressSpace::Generic); - let argument_types: &[BasicTypeEnum] = &[env.context.i16_type().into(), i8_ptr_type.into()]; - - let block = env.builder.get_insert_block().expect("to be in a function"); - let di_location = env.builder.get_current_debug_location().unwrap(); - - let output_type = crate::llvm::convert::zig_has_tag_id_type(env); - - let function_value = crate::llvm::refcounting::build_header_help( - env, - fn_name, - output_type.into(), - argument_types, - ); - - // called from zig, must use C calling convention - function_value.set_call_conventions(C_CALL_CONV); - - let kind_id = Attribute::get_named_enum_kind_id("alwaysinline"); - debug_assert!(kind_id > 0); - let attr = env.context.create_enum_attribute(kind_id, 1); - function_value.add_attribute(AttributeLoc::Function, attr); - - let entry = env.context.append_basic_block(function_value, "entry"); - env.builder.position_at_end(entry); - - debug_info_init!(env, function_value); - - let it = function_value.get_param_iter(); - - let arguments = - bumpalo::collections::Vec::from_iter_in(it.take(argument_types.len()), env.arena); - - for (argument, name) in arguments.iter().zip(ARGUMENT_SYMBOLS.iter()) { - argument.set_name(name.as_str(&env.interns)); - } - - match arguments.as_slice() { - [tag_id, tag_value_ptr] => { - let tag_type = basic_type_from_layout(env, &Layout::Union(union_layout)); - - let tag_value = env.builder.build_pointer_cast( - tag_value_ptr.into_pointer_value(), - tag_type.ptr_type(AddressSpace::Generic), - "load_opaque_get_tag_id", - ); - - let actual_tag_id = { - let tag_id_i64 = crate::llvm::build::get_tag_id( - env, - function_value, - &union_layout, - tag_value.into(), - ); - - env.builder.build_int_cast_sign_flag( - tag_id_i64, - env.context.i16_type(), - true, - "to_i16", - ) - }; - - let answer = env.builder.build_int_compare( - inkwell::IntPredicate::EQ, - tag_id.into_int_value(), - actual_tag_id, - "compare", - ); - - let tag_data_ptr = { - let ptr = env - .builder - .build_struct_gep(tag_value, TAG_DATA_INDEX, "get_data_ptr") - .unwrap(); - - env.builder.build_bitcast(ptr, i8_ptr_type, "to_opaque") - }; - - let field_vals = [(0, answer.into()), (1, tag_data_ptr)]; - - let output = struct_from_fields(env, output_type, field_vals.iter().copied()); - - env.builder.build_return(Some(&output)); - - env.builder.position_at_end(block); - env.builder - .set_current_debug_location(env.context, di_location); - - function_value - } - _ => unreachable!(), - } -} - -pub fn build_transform_caller<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - function: FunctionValue<'ctx>, - closure_data_layout: LambdaSet<'a>, - argument_layouts: &[Layout<'a>], - result_layout: Layout<'a>, -) -> FunctionValue<'ctx> { - let fn_name: &str = &format!( - "{}_zig_function_caller", - function.get_name().to_string_lossy() - ); - - match env.module.get_function(fn_name) { - Some(function_value) => function_value, - None => build_transform_caller_help( - env, - function, - closure_data_layout, - argument_layouts, - result_layout, - fn_name, - ), - } -} - -fn build_transform_caller_help<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - roc_function: FunctionValue<'ctx>, - closure_data_layout: LambdaSet<'a>, - argument_layouts: &[Layout<'a>], - result_layout: Layout<'a>, - fn_name: &str, -) -> FunctionValue<'ctx> { - debug_assert!(argument_layouts.len() <= 7); - - let block = env.builder.get_insert_block().expect("to be in a function"); - let di_location = env.builder.get_current_debug_location().unwrap(); - - let arg_type = env.context.i8_type().ptr_type(AddressSpace::Generic); - - let function_value = crate::llvm::refcounting::build_header_help( - env, - fn_name, - env.context.void_type().into(), - &(bumpalo::vec![in env.arena; BasicTypeEnum::PointerType(arg_type); argument_layouts.len() + 2]), - ); - - // called from zig, must use C calling convention - function_value.set_call_conventions(C_CALL_CONV); - - let kind_id = Attribute::get_named_enum_kind_id("alwaysinline"); - debug_assert!(kind_id > 0); - let attr = env.context.create_enum_attribute(kind_id, 1); - function_value.add_attribute(AttributeLoc::Function, attr); - - let entry = env.context.append_basic_block(function_value, "entry"); - env.builder.position_at_end(entry); - - debug_info_init!(env, function_value); - - let mut it = function_value.get_param_iter(); - let closure_ptr = it.next().unwrap().into_pointer_value(); - closure_ptr.set_name(Symbol::ARG_1.as_str(&env.interns)); - - let arguments = - bumpalo::collections::Vec::from_iter_in(it.take(argument_layouts.len()), env.arena); - - for (argument, name) in arguments.iter().zip(ARGUMENT_SYMBOLS[1..].iter()) { - argument.set_name(name.as_str(&env.interns)); - } - - let mut arguments_cast = - bumpalo::collections::Vec::with_capacity_in(arguments.len(), env.arena); - - for (argument_ptr, layout) in arguments.iter().zip(argument_layouts) { - let basic_type = basic_type_from_layout(env, layout).ptr_type(AddressSpace::Generic); - - let argument = if layout.is_passed_by_reference(env.target_info) { - env.builder - .build_pointer_cast( - argument_ptr.into_pointer_value(), - basic_type, - "cast_ptr_to_tag_build_transform_caller_help", - ) - .into() - } else { - let argument_cast = env - .builder - .build_bitcast(*argument_ptr, basic_type, "load_opaque_1") - .into_pointer_value(); - - env.builder.build_load(argument_cast, "load_opaque_2") - }; - - arguments_cast.push(argument); - } - - match closure_data_layout.runtime_representation() { - Layout::Struct { - field_layouts: &[], .. - } => { - // nothing to add - } - other => { - let closure_type = basic_type_from_layout(env, &other).ptr_type(AddressSpace::Generic); - - let closure_cast = env - .builder - .build_bitcast(closure_ptr, closure_type, "load_opaque") - .into_pointer_value(); - - let closure_data = env.builder.build_load(closure_cast, "load_opaque"); - - arguments_cast.push(closure_data); - } - } - - let result = crate::llvm::build::call_roc_function( - env, - roc_function, - &result_layout, - arguments_cast.as_slice(), - ); - - let result_u8_ptr = function_value - .get_nth_param(argument_layouts.len() as u32 + 1) - .unwrap() - .into_pointer_value(); - - crate::llvm::build::store_roc_value_opaque(env, result_layout, result_u8_ptr, result); - env.builder.build_return(None); - - env.builder.position_at_end(block); - env.builder - .set_current_debug_location(env.context, di_location); - - function_value -} - -enum Mode { - Inc, - IncN, - Dec, -} - -/// a function that accepts two arguments: the value to increment, and an amount to increment by -pub fn build_inc_n_wrapper<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - layout_ids: &mut LayoutIds<'a>, - layout: &Layout<'a>, -) -> FunctionValue<'ctx> { - build_rc_wrapper(env, layout_ids, layout, Mode::IncN) -} - -/// a function that accepts two arguments: the value to increment; increments by 1 -pub fn build_inc_wrapper<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - layout_ids: &mut LayoutIds<'a>, - layout: &Layout<'a>, -) -> FunctionValue<'ctx> { - build_rc_wrapper(env, layout_ids, layout, Mode::Inc) -} - -pub fn build_dec_wrapper<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - layout_ids: &mut LayoutIds<'a>, - layout: &Layout<'a>, -) -> FunctionValue<'ctx> { - build_rc_wrapper(env, layout_ids, layout, Mode::Dec) -} - -fn build_rc_wrapper<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - layout_ids: &mut LayoutIds<'a>, - layout: &Layout<'a>, - rc_operation: Mode, -) -> FunctionValue<'ctx> { - let block = env.builder.get_insert_block().expect("to be in a function"); - let di_location = env.builder.get_current_debug_location().unwrap(); - - let symbol = Symbol::GENERIC_RC_REF; - let fn_name = layout_ids - .get(symbol, layout) - .to_symbol_string(symbol, &env.interns); - - let fn_name = match rc_operation { - Mode::IncN => format!("{}_inc_n", fn_name), - Mode::Inc => format!("{}_inc", fn_name), - Mode::Dec => format!("{}_dec", fn_name), - }; - - let function_value = match env.module.get_function(fn_name.as_str()) { - Some(function_value) => function_value, - None => { - let arg_type = env.context.i8_type().ptr_type(AddressSpace::Generic); - - let function_value = match rc_operation { - Mode::Inc | Mode::Dec => crate::llvm::refcounting::build_header_help( - env, - &fn_name, - env.context.void_type().into(), - &[arg_type.into()], - ), - Mode::IncN => crate::llvm::refcounting::build_header_help( - env, - &fn_name, - env.context.void_type().into(), - &[arg_type.into(), env.ptr_int().into()], - ), - }; - - // called from zig, must use C calling convention - function_value.set_call_conventions(C_CALL_CONV); - - let kind_id = Attribute::get_named_enum_kind_id("alwaysinline"); - debug_assert!(kind_id > 0); - let attr = env.context.create_enum_attribute(kind_id, 1); - function_value.add_attribute(AttributeLoc::Function, attr); - - let entry = env.context.append_basic_block(function_value, "entry"); - env.builder.position_at_end(entry); - - debug_info_init!(env, function_value); - - let mut it = function_value.get_param_iter(); - let value_ptr = it.next().unwrap().into_pointer_value(); - - value_ptr.set_name(Symbol::ARG_1.as_str(&env.interns)); - - let value_type = basic_type_from_layout(env, layout).ptr_type(AddressSpace::Generic); - - let value = if layout.is_passed_by_reference(env.target_info) { - env.builder - .build_pointer_cast(value_ptr, value_type, "cast_ptr_to_tag_build_rc_wrapper") - .into() - } else { - let value_cast = env - .builder - .build_bitcast(value_ptr, value_type, "load_opaque") - .into_pointer_value(); - - env.builder.build_load(value_cast, "load_opaque") - }; - - match rc_operation { - Mode::Inc => { - let n = 1; - increment_refcount_layout(env, function_value, layout_ids, n, value, layout); - } - Mode::IncN => { - let n = it.next().unwrap().into_int_value(); - n.set_name(Symbol::ARG_2.as_str(&env.interns)); - - increment_n_refcount_layout(env, function_value, layout_ids, n, value, layout); - } - Mode::Dec => { - decrement_refcount_layout(env, function_value, layout_ids, value, layout); - } - } - - env.builder.build_return(None); - - function_value - } - }; - - env.builder.position_at_end(block); - env.builder - .set_current_debug_location(env.context, di_location); - - function_value -} - -pub fn build_eq_wrapper<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - layout_ids: &mut LayoutIds<'a>, - layout: &Layout<'a>, -) -> FunctionValue<'ctx> { - let block = env.builder.get_insert_block().expect("to be in a function"); - let di_location = env.builder.get_current_debug_location().unwrap(); - - let symbol = Symbol::GENERIC_EQ_REF; - let fn_name = layout_ids - .get(symbol, layout) - .to_symbol_string(symbol, &env.interns); - - let function_value = match env.module.get_function(fn_name.as_str()) { - Some(function_value) => function_value, - None => { - let arg_type = env.context.i8_type().ptr_type(AddressSpace::Generic); - - let function_value = crate::llvm::refcounting::build_header_help( - env, - &fn_name, - env.context.bool_type().into(), - &[arg_type.into(), arg_type.into()], - ); - - // called from zig, must use C calling convention - function_value.set_call_conventions(C_CALL_CONV); - - let kind_id = Attribute::get_named_enum_kind_id("alwaysinline"); - debug_assert!(kind_id > 0); - let attr = env.context.create_enum_attribute(kind_id, 1); - function_value.add_attribute(AttributeLoc::Function, attr); - - let entry = env.context.append_basic_block(function_value, "entry"); - env.builder.position_at_end(entry); - - debug_info_init!(env, function_value); - - let mut it = function_value.get_param_iter(); - let value_ptr1 = it.next().unwrap().into_pointer_value(); - let value_ptr2 = it.next().unwrap().into_pointer_value(); - - value_ptr1.set_name(Symbol::ARG_1.as_str(&env.interns)); - value_ptr2.set_name(Symbol::ARG_2.as_str(&env.interns)); - - let value_type = basic_type_from_layout(env, layout).ptr_type(AddressSpace::Generic); - - let value_cast1 = env - .builder - .build_bitcast(value_ptr1, value_type, "load_opaque") - .into_pointer_value(); - - let value_cast2 = env - .builder - .build_bitcast(value_ptr2, value_type, "load_opaque") - .into_pointer_value(); - - // load_roc_value(env, *element_layout, elem_ptr, "get_elem") - let value1 = load_roc_value(env, *layout, value_cast1, "load_opaque"); - let value2 = load_roc_value(env, *layout, value_cast2, "load_opaque"); - - let result = - crate::llvm::compare::generic_eq(env, layout_ids, value1, value2, layout, layout); - - env.builder.build_return(Some(&result)); - - function_value - } - }; - - env.builder.position_at_end(block); - env.builder - .set_current_debug_location(env.context, di_location); - - function_value -} - -pub fn build_compare_wrapper<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - roc_function: FunctionValue<'ctx>, - closure_data_layout: LambdaSet<'a>, - layout: &Layout<'a>, -) -> FunctionValue<'ctx> { - let block = env.builder.get_insert_block().expect("to be in a function"); - let di_location = env.builder.get_current_debug_location().unwrap(); - - let fn_name: &str = &format!( - "{}_compare_wrapper", - roc_function.get_name().to_string_lossy() - ); - - let function_value = match env.module.get_function(fn_name) { - Some(function_value) => function_value, - None => { - let arg_type = env.context.i8_type().ptr_type(AddressSpace::Generic); - - let function_value = crate::llvm::refcounting::build_header_help( - env, - fn_name, - env.context.i8_type().into(), - &[arg_type.into(), arg_type.into(), arg_type.into()], - ); - - // called from zig, must use C calling convention - function_value.set_call_conventions(C_CALL_CONV); - - // we expose this function to zig; must use c calling convention - function_value.set_call_conventions(C_CALL_CONV); - - let kind_id = Attribute::get_named_enum_kind_id("alwaysinline"); - debug_assert!(kind_id > 0); - let attr = env.context.create_enum_attribute(kind_id, 1); - function_value.add_attribute(AttributeLoc::Function, attr); - - let entry = env.context.append_basic_block(function_value, "entry"); - env.builder.position_at_end(entry); - - debug_info_init!(env, function_value); - - let mut it = function_value.get_param_iter(); - let closure_ptr = it.next().unwrap().into_pointer_value(); - let value_ptr1 = it.next().unwrap().into_pointer_value(); - let value_ptr2 = it.next().unwrap().into_pointer_value(); - - closure_ptr.set_name(Symbol::ARG_1.as_str(&env.interns)); - value_ptr1.set_name(Symbol::ARG_2.as_str(&env.interns)); - value_ptr2.set_name(Symbol::ARG_3.as_str(&env.interns)); - - let value_type = basic_type_from_layout(env, layout); - let value_ptr_type = value_type.ptr_type(AddressSpace::Generic); - - let value_cast1 = env - .builder - .build_bitcast(value_ptr1, value_ptr_type, "load_opaque") - .into_pointer_value(); - - let value_cast2 = env - .builder - .build_bitcast(value_ptr2, value_ptr_type, "load_opaque") - .into_pointer_value(); - - let value1 = env.builder.build_load(value_cast1, "load_opaque"); - let value2 = env.builder.build_load(value_cast2, "load_opaque"); - - let default = [value1.into(), value2.into()]; - - let arguments_cast = match closure_data_layout.runtime_representation() { - Layout::Struct { - field_layouts: &[], .. - } => { - // nothing to add - &default - } - other => { - let closure_type = - basic_type_from_layout(env, &other).ptr_type(AddressSpace::Generic); - - let closure_cast = env - .builder - .build_bitcast(closure_ptr, closure_type, "load_opaque") - .into_pointer_value(); - - let closure_data = env.builder.build_load(closure_cast, "load_opaque"); - - env.arena - .alloc([value1.into(), value2.into(), closure_data.into()]) - as &[_] - } - }; - - let call = env.builder.build_call( - roc_function, - arguments_cast, - "call_user_defined_compare_function", - ); - - let result = call.try_as_basic_value().left().unwrap(); - - // IMPORTANT! we call a user function, so it has the fast calling convention - call.set_call_convention(FAST_CALL_CONV); - - env.builder.build_return(Some(&result)); - - function_value - } - }; - - env.builder.position_at_end(block); - env.builder - .set_current_debug_location(env.context, di_location); - - function_value -} diff --git a/compiler/gen_llvm/src/llvm/build.rs b/compiler/gen_llvm/src/llvm/build.rs deleted file mode 100644 index 7b7a6404ae..0000000000 --- a/compiler/gen_llvm/src/llvm/build.rs +++ /dev/null @@ -1,7756 +0,0 @@ -use crate::llvm::bitcode::{ - call_bitcode_fn, call_bitcode_fn_fixing_for_convention, call_list_bitcode_fn, - call_str_bitcode_fn, call_void_bitcode_fn, -}; -use crate::llvm::build_dict::{ - self, dict_contains, dict_difference, dict_empty, dict_get, dict_insert, dict_intersection, - dict_keys, dict_len, dict_remove, dict_union, dict_values, dict_walk, set_from_list, -}; -use crate::llvm::build_hash::generic_hash; -use crate::llvm::build_list::{ - self, allocate_list, empty_polymorphic_list, list_all, list_any, list_append, list_concat, - list_contains, list_drop_at, list_find_unsafe, list_get_unsafe, list_join, list_keep_errs, - list_keep_if, list_keep_oks, list_len, list_map, list_map2, list_map3, list_map4, - list_map_with_index, list_prepend, list_range, list_repeat, list_replace_unsafe, list_reverse, - list_single, list_sort_with, list_sublist, list_swap, list_symbol_to_c_abi, list_to_c_abi, -}; -use crate::llvm::build_str::{ - str_from_float, str_from_int, str_from_utf8, str_from_utf8_range, str_split, -}; -use crate::llvm::compare::{generic_eq, generic_neq}; -use crate::llvm::convert::{ - self, argument_type_from_layout, basic_type_from_builtin, basic_type_from_layout, - block_of_memory_slices, zig_str_type, -}; -use crate::llvm::refcounting::{ - build_reset, decrement_refcount_layout, increment_refcount_layout, PointerToRefcount, -}; -use bumpalo::collections::Vec; -use bumpalo::Bump; -use inkwell::attributes::{Attribute, AttributeLoc}; -use inkwell::basic_block::BasicBlock; -use inkwell::builder::Builder; -use inkwell::context::Context; -use inkwell::debug_info::{ - AsDIScope, DICompileUnit, DIFlagsConstants, DISubprogram, DebugInfoBuilder, -}; -use inkwell::memory_buffer::MemoryBuffer; -use inkwell::module::{Linkage, Module}; -use inkwell::passes::{PassManager, PassManagerBuilder}; -use inkwell::types::{ - AnyType, BasicMetadataTypeEnum, BasicType, BasicTypeEnum, FunctionType, IntType, StructType, -}; -use inkwell::values::BasicValueEnum::{self, *}; -use inkwell::values::{ - BasicMetadataValueEnum, BasicValue, CallSiteValue, CallableValue, FloatValue, FunctionValue, - InstructionOpcode, InstructionValue, IntValue, PhiValue, PointerValue, StructValue, -}; -use inkwell::OptimizationLevel; -use inkwell::{AddressSpace, IntPredicate}; -use morphic_lib::{ - CalleeSpecVar, FuncName, FuncSpec, FuncSpecSolutions, ModSolutions, UpdateMode, UpdateModeVar, -}; -use roc_builtins::bitcode::{self, FloatWidth, IntWidth, IntrinsicName}; -use roc_builtins::{float_intrinsic, llvm_int_intrinsic}; -use roc_collections::all::{ImMap, MutMap, MutSet}; -use roc_debug_flags::dbg_do; -#[cfg(debug_assertions)] -use roc_debug_flags::ROC_PRINT_LLVM_FN_VERIFICATION; -use roc_error_macros::internal_error; -use roc_module::low_level::LowLevel; -use roc_module::symbol::{Interns, ModuleId, Symbol}; -use roc_mono::ir::{ - BranchInfo, CallType, EntryPoint, HigherOrderLowLevel, JoinPointId, ListLiteralElement, - ModifyRc, OptLevel, ProcLayout, -}; -use roc_mono::layout::{Builtin, LambdaSet, Layout, LayoutIds, TagIdIntType, UnionLayout}; -use roc_std::RocDec; -use roc_target::{PtrWidth, TargetInfo}; -use std::convert::{TryFrom, TryInto}; -use std::path::Path; -use target_lexicon::{Architecture, OperatingSystem, Triple}; - -use super::convert::zig_with_overflow_roc_dec; - -#[inline(always)] -fn print_fn_verification_output() -> bool { - dbg_do!(ROC_PRINT_LLVM_FN_VERIFICATION, { - return true; - }); - false -} - -#[macro_export] -macro_rules! debug_info_init { - ($env:expr, $function_value:expr) => {{ - use inkwell::debug_info::AsDIScope; - - let func_scope = $function_value.get_subprogram().expect("subprogram"); - let lexical_block = $env.dibuilder.create_lexical_block( - /* scope */ func_scope.as_debug_info_scope(), - /* file */ $env.compile_unit.get_file(), - /* line_no */ 0, - /* column_no */ 0, - ); - - let loc = $env.dibuilder.create_debug_location( - $env.context, - /* line */ 0, - /* column */ 0, - /* current_scope */ lexical_block.as_debug_info_scope(), - /* inlined_at */ None, - ); - $env.builder.set_current_debug_location(&$env.context, loc); - }}; -} - -/// Iterate over all functions in an llvm module -pub struct FunctionIterator<'ctx> { - next: Option>, -} - -impl<'ctx> FunctionIterator<'ctx> { - pub fn from_module(module: &inkwell::module::Module<'ctx>) -> Self { - Self { - next: module.get_first_function(), - } - } -} - -impl<'ctx> Iterator for FunctionIterator<'ctx> { - type Item = FunctionValue<'ctx>; - - fn next(&mut self) -> Option { - match self.next { - Some(function) => { - self.next = function.get_next_function(); - - Some(function) - } - None => None, - } - } -} - -#[derive(Default, Debug, Clone, PartialEq)] -pub struct Scope<'a, 'ctx> { - symbols: ImMap, BasicValueEnum<'ctx>)>, - pub top_level_thunks: ImMap, FunctionValue<'ctx>)>, - join_points: ImMap, &'a [PhiValue<'ctx>])>, -} - -impl<'a, 'ctx> Scope<'a, 'ctx> { - fn get(&self, symbol: &Symbol) -> Option<&(Layout<'a>, BasicValueEnum<'ctx>)> { - self.symbols.get(symbol) - } - pub fn insert(&mut self, symbol: Symbol, value: (Layout<'a>, BasicValueEnum<'ctx>)) { - self.symbols.insert(symbol, value); - } - pub fn insert_top_level_thunk( - &mut self, - symbol: Symbol, - layout: &'a ProcLayout<'a>, - function_value: FunctionValue<'ctx>, - ) { - self.top_level_thunks - .insert(symbol, (*layout, function_value)); - } - fn remove(&mut self, symbol: &Symbol) { - self.symbols.remove(symbol); - } - - pub fn retain_top_level_thunks_for_module(&mut self, module_id: ModuleId) { - self.top_level_thunks - .retain(|s, _| s.module_id() == module_id); - } -} - -pub struct Env<'a, 'ctx, 'env> { - pub arena: &'a Bump, - pub context: &'ctx Context, - pub builder: &'env Builder<'ctx>, - pub dibuilder: &'env DebugInfoBuilder<'ctx>, - pub compile_unit: &'env DICompileUnit<'ctx>, - pub module: &'ctx Module<'ctx>, - pub interns: Interns, - pub target_info: TargetInfo, - pub is_gen_test: bool, - pub exposed_to_host: MutSet, -} - -#[repr(u32)] -pub enum PanicTagId { - NullTerminatedString = 0, -} - -impl std::convert::TryFrom for PanicTagId { - type Error = (); - - fn try_from(value: u32) -> Result { - match value { - 0 => Ok(PanicTagId::NullTerminatedString), - _ => Err(()), - } - } -} - -impl<'a, 'ctx, 'env> Env<'a, 'ctx, 'env> { - /// The integer type representing a pointer - /// - /// on 64-bit systems, this is i64 - /// on 32-bit systems, this is i32 - pub fn ptr_int(&self) -> IntType<'ctx> { - let ctx = self.context; - - match self.target_info.ptr_width() { - roc_target::PtrWidth::Bytes4 => ctx.i32_type(), - roc_target::PtrWidth::Bytes8 => ctx.i64_type(), - } - } - - /// The integer type representing twice the width of a pointer - /// - /// on 64-bit systems, this is i128 - /// on 32-bit systems, this is i64 - pub fn twice_ptr_int(&self) -> IntType<'ctx> { - let ctx = self.context; - - match self.target_info.ptr_width() { - roc_target::PtrWidth::Bytes4 => ctx.i64_type(), - roc_target::PtrWidth::Bytes8 => ctx.i128_type(), - } - } - - pub fn small_str_bytes(&self) -> u32 { - self.target_info.ptr_width() as u32 * 3 - } - - pub fn build_intrinsic_call( - &self, - intrinsic_name: &'static str, - args: &[BasicValueEnum<'ctx>], - ) -> CallSiteValue<'ctx> { - let fn_val = self - .module - .get_function(intrinsic_name) - .unwrap_or_else(|| panic!("Unrecognized intrinsic function: {}", intrinsic_name)); - - let mut arg_vals: Vec = - Vec::with_capacity_in(args.len(), self.arena); - - for arg in args.iter() { - arg_vals.push((*arg).into()); - } - - let call = self - .builder - .build_call(fn_val, arg_vals.into_bump_slice(), "call"); - - call.set_call_convention(fn_val.get_call_conventions()); - - call - } - - pub fn call_intrinsic( - &self, - intrinsic_name: &'static str, - args: &[BasicValueEnum<'ctx>], - ) -> BasicValueEnum<'ctx> { - let call = self.build_intrinsic_call(intrinsic_name, args); - - call.try_as_basic_value().left().unwrap_or_else(|| { - panic!( - "LLVM error: Invalid call by name for intrinsic {}", - intrinsic_name - ) - }) - } - - pub fn alignment_type(&self) -> IntType<'ctx> { - self.context.i32_type() - } - - pub fn alignment_const(&self, alignment: u32) -> IntValue<'ctx> { - self.alignment_type().const_int(alignment as u64, false) - } - - pub fn alignment_intvalue(&self, element_layout: &Layout<'a>) -> BasicValueEnum<'ctx> { - let alignment = element_layout.alignment_bytes(self.target_info); - let alignment_iv = self.alignment_const(alignment); - - alignment_iv.into() - } - - pub fn call_alloc( - &self, - number_of_bytes: IntValue<'ctx>, - alignment: u32, - ) -> PointerValue<'ctx> { - let function = self.module.get_function("roc_alloc").unwrap(); - let alignment = self.alignment_const(alignment); - let call = self.builder.build_call( - function, - &[number_of_bytes.into(), alignment.into()], - "roc_alloc", - ); - - call.set_call_convention(C_CALL_CONV); - - call.try_as_basic_value() - .left() - .unwrap() - .into_pointer_value() - // TODO check if alloc returned null; if so, runtime error for OOM! - } - - pub fn call_dealloc(&self, ptr: PointerValue<'ctx>, alignment: u32) -> InstructionValue<'ctx> { - let function = self.module.get_function("roc_dealloc").unwrap(); - let alignment = self.alignment_const(alignment); - let call = - self.builder - .build_call(function, &[ptr.into(), alignment.into()], "roc_dealloc"); - - call.set_call_convention(C_CALL_CONV); - - call.try_as_basic_value().right().unwrap() - } - - pub fn call_memset( - &self, - bytes_ptr: PointerValue<'ctx>, - filler: IntValue<'ctx>, - length: IntValue<'ctx>, - ) -> CallSiteValue<'ctx> { - let false_val = self.context.bool_type().const_int(0, false); - - let intrinsic_name = match self.target_info.ptr_width() { - roc_target::PtrWidth::Bytes8 => LLVM_MEMSET_I64, - roc_target::PtrWidth::Bytes4 => LLVM_MEMSET_I32, - }; - - self.build_intrinsic_call( - intrinsic_name, - &[ - bytes_ptr.into(), - filler.into(), - length.into(), - false_val.into(), - ], - ) - } - - pub fn call_panic(&self, message: PointerValue<'ctx>, tag_id: PanicTagId) { - let function = self.module.get_function("roc_panic").unwrap(); - let tag_id = self - .context - .i32_type() - .const_int(tag_id as u32 as u64, false); - - let call = self - .builder - .build_call(function, &[message.into(), tag_id.into()], "roc_panic"); - - call.set_call_convention(C_CALL_CONV); - } - - pub fn new_debug_info(module: &Module<'ctx>) -> (DebugInfoBuilder<'ctx>, DICompileUnit<'ctx>) { - module.create_debug_info_builder( - true, - /* language */ inkwell::debug_info::DWARFSourceLanguage::C, - /* filename */ "roc_app", - /* directory */ ".", - /* producer */ "my llvm compiler frontend", - /* is_optimized */ false, - /* compiler command line flags */ "", - /* runtime_ver */ 0, - /* split_name */ "", - /* kind */ inkwell::debug_info::DWARFEmissionKind::Full, - /* dwo_id */ 0, - /* split_debug_inling */ false, - /* debug_info_for_profiling */ false, - "", - "", - ) - } - - pub fn new_subprogram(&self, function_name: &str) -> DISubprogram<'ctx> { - let dibuilder = self.dibuilder; - let compile_unit = self.compile_unit; - - let ditype = dibuilder - .create_basic_type( - "type_name", - 0_u64, - 0x00, - inkwell::debug_info::DIFlags::PUBLIC, - ) - .unwrap(); - - let subroutine_type = dibuilder.create_subroutine_type( - compile_unit.get_file(), - /* return type */ Some(ditype.as_type()), - /* parameter types */ &[], - inkwell::debug_info::DIFlags::PUBLIC, - ); - - dibuilder.create_function( - /* scope */ compile_unit.get_file().as_debug_info_scope(), - /* func name */ function_name, - /* linkage_name */ None, - /* file */ compile_unit.get_file(), - /* line_no */ 0, - /* DIType */ subroutine_type, - /* is_local_to_unit */ true, - /* is_definition */ true, - /* scope_line */ 0, - /* flags */ inkwell::debug_info::DIFlags::PUBLIC, - /* is_optimized */ false, - ) - } -} - -pub fn module_from_builtins<'ctx>( - target: &target_lexicon::Triple, - ctx: &'ctx Context, - module_name: &str, -) -> Module<'ctx> { - // In the build script for the builtins module, we compile the builtins into LLVM bitcode - - let bitcode_bytes: &[u8] = if target == &target_lexicon::Triple::host() { - include_bytes!("../../../builtins/bitcode/builtins-host.bc") - } else { - match target { - Triple { - architecture: Architecture::Wasm32, - .. - } => { - include_bytes!("../../../builtins/bitcode/builtins-wasm32.bc") - } - Triple { - architecture: Architecture::X86_32(_), - operating_system: OperatingSystem::Linux, - .. - } => { - include_bytes!("../../../builtins/bitcode/builtins-i386.bc") - } - Triple { - architecture: Architecture::X86_64, - operating_system: OperatingSystem::Linux, - .. - } => { - include_bytes!("../../../builtins/bitcode/builtins-x86_64.bc") - } - _ => panic!( - "The zig builtins are not currently built for this target: {:?}", - target - ), - } - }; - - let memory_buffer = MemoryBuffer::create_from_memory_range(bitcode_bytes, module_name); - - let module = Module::parse_bitcode_from_buffer(&memory_buffer, ctx) - .unwrap_or_else(|err| panic!("Unable to import builtins bitcode. LLVM error: {:?}", err)); - - // Add LLVM intrinsics. - add_intrinsics(ctx, &module); - - module -} - -fn add_float_intrinsic<'ctx, F>( - ctx: &'ctx Context, - module: &Module<'ctx>, - name: &IntrinsicName, - construct_type: F, -) where - F: Fn(inkwell::types::FloatType<'ctx>) -> inkwell::types::FunctionType<'ctx>, -{ - macro_rules! check { - ($width:expr, $typ:expr) => { - let full_name = &name[$width]; - - if let Some(_) = module.get_function(full_name) { - // zig defined this function already - } else { - add_intrinsic(ctx, module, full_name, construct_type($typ)); - } - }; - } - - check!(FloatWidth::F32, ctx.f32_type()); - check!(FloatWidth::F64, ctx.f64_type()); - // check!(IntWidth::F128, ctx.i128_type()); -} - -fn add_int_intrinsic<'ctx, F>( - ctx: &'ctx Context, - module: &Module<'ctx>, - name: &IntrinsicName, - construct_type: F, -) where - F: Fn(inkwell::types::IntType<'ctx>) -> inkwell::types::FunctionType<'ctx>, -{ - macro_rules! check { - ($width:expr, $typ:expr) => { - let full_name = &name[$width]; - - if let Some(_) = module.get_function(full_name) { - // zig defined this function already - } else { - add_intrinsic(ctx, module, full_name, construct_type($typ)); - } - }; - } - - check!(IntWidth::U8, ctx.i8_type()); - check!(IntWidth::U16, ctx.i16_type()); - check!(IntWidth::U32, ctx.i32_type()); - check!(IntWidth::U64, ctx.i64_type()); - check!(IntWidth::U128, ctx.i128_type()); - - check!(IntWidth::I8, ctx.i8_type()); - check!(IntWidth::I16, ctx.i16_type()); - check!(IntWidth::I32, ctx.i32_type()); - check!(IntWidth::I64, ctx.i64_type()); - check!(IntWidth::I128, ctx.i128_type()); -} - -fn add_intrinsics<'ctx>(ctx: &'ctx Context, module: &Module<'ctx>) { - // List of all supported LLVM intrinsics: - // - // https://releases.llvm.org/10.0.0/docs/LangRef.html#standard-c-library-intrinsics - let i1_type = ctx.bool_type(); - let i8_type = ctx.i8_type(); - let i8_ptr_type = i8_type.ptr_type(AddressSpace::Generic); - let i32_type = ctx.i32_type(); - let void_type = ctx.void_type(); - - if let Some(func) = module.get_function("__muloti4") { - func.set_linkage(Linkage::WeakAny); - } - - add_intrinsic( - ctx, - module, - LLVM_SETJMP, - i32_type.fn_type(&[i8_ptr_type.into()], false), - ); - - add_intrinsic( - ctx, - module, - LLVM_LONGJMP, - void_type.fn_type(&[i8_ptr_type.into()], false), - ); - - add_intrinsic( - ctx, - module, - LLVM_FRAME_ADDRESS, - i8_ptr_type.fn_type(&[i32_type.into()], false), - ); - - add_intrinsic( - ctx, - module, - LLVM_STACK_SAVE, - i8_ptr_type.fn_type(&[], false), - ); - - add_float_intrinsic(ctx, module, &LLVM_LOG, |t| t.fn_type(&[t.into()], false)); - add_float_intrinsic(ctx, module, &LLVM_POW, |t| { - t.fn_type(&[t.into(), t.into()], false) - }); - add_float_intrinsic(ctx, module, &LLVM_FABS, |t| t.fn_type(&[t.into()], false)); - add_float_intrinsic(ctx, module, &LLVM_SIN, |t| t.fn_type(&[t.into()], false)); - add_float_intrinsic(ctx, module, &LLVM_COS, |t| t.fn_type(&[t.into()], false)); - add_float_intrinsic(ctx, module, &LLVM_CEILING, |t| { - t.fn_type(&[t.into()], false) - }); - add_float_intrinsic(ctx, module, &LLVM_FLOOR, |t| t.fn_type(&[t.into()], false)); - - add_int_intrinsic(ctx, module, &LLVM_ADD_WITH_OVERFLOW, |t| { - let fields = [t.into(), i1_type.into()]; - ctx.struct_type(&fields, false) - .fn_type(&[t.into(), t.into()], false) - }); - - add_int_intrinsic(ctx, module, &LLVM_SUB_WITH_OVERFLOW, |t| { - let fields = [t.into(), i1_type.into()]; - ctx.struct_type(&fields, false) - .fn_type(&[t.into(), t.into()], false) - }); - - add_int_intrinsic(ctx, module, &LLVM_MUL_WITH_OVERFLOW, |t| { - let fields = [t.into(), i1_type.into()]; - ctx.struct_type(&fields, false) - .fn_type(&[t.into(), t.into()], false) - }); - - add_int_intrinsic(ctx, module, &LLVM_ADD_SATURATED, |t| { - t.fn_type(&[t.into(), t.into()], false) - }); - - add_int_intrinsic(ctx, module, &LLVM_SUB_SATURATED, |t| { - t.fn_type(&[t.into(), t.into()], false) - }); -} - -const LLVM_POW: IntrinsicName = float_intrinsic!("llvm.pow"); -const LLVM_FABS: IntrinsicName = float_intrinsic!("llvm.fabs"); -static LLVM_SQRT: IntrinsicName = float_intrinsic!("llvm.sqrt"); -static LLVM_LOG: IntrinsicName = float_intrinsic!("llvm.log"); - -static LLVM_SIN: IntrinsicName = float_intrinsic!("llvm.sin"); -static LLVM_COS: IntrinsicName = float_intrinsic!("llvm.cos"); -static LLVM_CEILING: IntrinsicName = float_intrinsic!("llvm.ceil"); -static LLVM_FLOOR: IntrinsicName = float_intrinsic!("llvm.floor"); -static LLVM_ROUND: IntrinsicName = float_intrinsic!("llvm.round"); - -static LLVM_MEMSET_I64: &str = "llvm.memset.p0i8.i64"; -static LLVM_MEMSET_I32: &str = "llvm.memset.p0i8.i32"; - -static LLVM_FRAME_ADDRESS: &str = "llvm.frameaddress.p0i8"; -static LLVM_STACK_SAVE: &str = "llvm.stacksave"; - -static LLVM_SETJMP: &str = "llvm.eh.sjlj.setjmp"; -pub static LLVM_LONGJMP: &str = "llvm.eh.sjlj.longjmp"; - -const LLVM_ADD_WITH_OVERFLOW: IntrinsicName = - llvm_int_intrinsic!("llvm.sadd.with.overflow", "llvm.uadd.with.overflow"); -const LLVM_SUB_WITH_OVERFLOW: IntrinsicName = - llvm_int_intrinsic!("llvm.ssub.with.overflow", "llvm.usub.with.overflow"); -const LLVM_MUL_WITH_OVERFLOW: IntrinsicName = - llvm_int_intrinsic!("llvm.smul.with.overflow", "llvm.umul.with.overflow"); - -const LLVM_ADD_SATURATED: IntrinsicName = llvm_int_intrinsic!("llvm.sadd.sat", "llvm.uadd.sat"); -const LLVM_SUB_SATURATED: IntrinsicName = llvm_int_intrinsic!("llvm.ssub.sat", "llvm.usub.sat"); - -fn add_intrinsic<'ctx>( - context: &Context, - module: &Module<'ctx>, - intrinsic_name: &str, - fn_type: FunctionType<'ctx>, -) -> FunctionValue<'ctx> { - add_func( - context, - module, - intrinsic_name, - FunctionSpec::intrinsic(fn_type), - Linkage::External, - ) -} - -pub fn construct_optimization_passes<'a>( - module: &'a Module, - opt_level: OptLevel, -) -> (PassManager>, PassManager>) { - let mpm = PassManager::create(()); - let fpm = PassManager::create(module); - - // remove unused global values (e.g. those defined by zig, but unused in user code) - mpm.add_global_dce_pass(); - - mpm.add_always_inliner_pass(); - - // tail-call elimination is always on - fpm.add_instruction_combining_pass(); - fpm.add_tail_call_elimination_pass(); - - let pmb = PassManagerBuilder::create(); - match opt_level { - OptLevel::Development | OptLevel::Normal => { - pmb.set_optimization_level(OptimizationLevel::None); - } - OptLevel::Size => { - pmb.set_optimization_level(OptimizationLevel::Default); - // TODO: For some usecase, like embedded, it is useful to expose this and tune it. - pmb.set_inliner_with_threshold(50); - } - OptLevel::Optimize => { - pmb.set_optimization_level(OptimizationLevel::Aggressive); - // this threshold seems to do what we want - pmb.set_inliner_with_threshold(275); - } - } - - // Add optimization passes for Size and Optimize. - if matches!(opt_level, OptLevel::Size | OptLevel::Optimize) { - // TODO figure out which of these actually help - - // function passes - - fpm.add_cfg_simplification_pass(); - mpm.add_cfg_simplification_pass(); - - fpm.add_jump_threading_pass(); - mpm.add_jump_threading_pass(); - - fpm.add_memcpy_optimize_pass(); // this one is very important - - fpm.add_licm_pass(); - - // turn invoke into call - mpm.add_prune_eh_pass(); - - // remove unused global values (often the `_wrapper` can be removed) - mpm.add_global_dce_pass(); - - mpm.add_function_inlining_pass(); - } - - pmb.populate_module_pass_manager(&mpm); - pmb.populate_function_pass_manager(&fpm); - - fpm.initialize(); - - // For now, we have just one of each - (mpm, fpm) -} - -fn promote_to_main_function<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - mod_solutions: &'a ModSolutions, - symbol: Symbol, - top_level: ProcLayout<'a>, -) -> (&'static str, FunctionValue<'ctx>) { - let it = top_level.arguments.iter().copied(); - let bytes = roc_alias_analysis::func_name_bytes_help(symbol, it, &top_level.result); - let func_name = FuncName(&bytes); - let func_solutions = mod_solutions.func_solutions(func_name).unwrap(); - - let mut it = func_solutions.specs(); - let func_spec = it.next().unwrap(); - debug_assert!( - it.next().is_none(), - "we expect only one specialization of this symbol" - ); - - // NOTE fake layout; it is only used for debug prints - let roc_main_fn = function_value_by_func_spec(env, *func_spec, symbol, &[], &Layout::UNIT); - - let main_fn_name = "$Test.main"; - - // Add main to the module. - let main_fn = expose_function_to_host_help_c_abi( - env, - main_fn_name, - roc_main_fn, - top_level.arguments, - top_level.result, - main_fn_name, - ); - - (main_fn_name, main_fn) -} - -fn int_with_precision<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - value: i128, - int_width: IntWidth, -) -> IntValue<'ctx> { - use IntWidth::*; - - match int_width { - U128 | I128 => const_i128(env, value), - U64 | I64 => env.context.i64_type().const_int(value as u64, false), - U32 | I32 => env.context.i32_type().const_int(value as u64, false), - U16 | I16 => env.context.i16_type().const_int(value as u64, false), - U8 | I8 => env.context.i8_type().const_int(value as u64, false), - } -} - -fn float_with_precision<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - value: f64, - float_width: FloatWidth, -) -> BasicValueEnum<'ctx> { - match float_width { - FloatWidth::F64 => env.context.f64_type().const_float(value).into(), - FloatWidth::F32 => env.context.f32_type().const_float(value).into(), - FloatWidth::F128 => todo!("F128 is not implemented"), - } -} - -pub fn build_exp_literal<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - parent: FunctionValue<'ctx>, - layout: &Layout<'_>, - literal: &roc_mono::ir::Literal<'a>, -) -> BasicValueEnum<'ctx> { - use roc_mono::ir::Literal::*; - - match literal { - Int(bytes) => match layout { - Layout::Builtin(Builtin::Bool) => env - .context - .bool_type() - .const_int(i128::from_ne_bytes(*bytes) as u64, false) - .into(), - Layout::Builtin(Builtin::Int(int_width)) => { - int_with_precision(env, i128::from_ne_bytes(*bytes), *int_width).into() - } - _ => panic!("Invalid layout for int literal = {:?}", layout), - }, - - U128(bytes) => const_u128(env, u128::from_ne_bytes(*bytes)).into(), - - Float(float) => match layout { - Layout::Builtin(Builtin::Float(float_width)) => { - float_with_precision(env, *float, *float_width) - } - _ => panic!("Invalid layout for float literal = {:?}", layout), - }, - - Decimal(bytes) => { - let (upper_bits, lower_bits) = RocDec::from_ne_bytes(*bytes).as_bits(); - env.context - .i128_type() - .const_int_arbitrary_precision(&[lower_bits, upper_bits as u64]) - .into() - } - Bool(b) => env.context.bool_type().const_int(*b as u64, false).into(), - Byte(b) => env.context.i8_type().const_int(*b as u64, false).into(), - Str(str_literal) => { - if str_literal.len() < env.small_str_bytes() as usize { - match env.small_str_bytes() { - 24 => small_str_ptr_width_8(env, parent, str_literal).into(), - 12 => small_str_ptr_width_4(env, parent, str_literal).into(), - _ => unreachable!("incorrect small_str_bytes"), - } - } else { - let ptr = define_global_str_literal_ptr(env, *str_literal); - let number_of_elements = env.ptr_int().const_int(str_literal.len() as u64, false); - - let alloca = - const_str_alloca_ptr(env, parent, ptr, number_of_elements, number_of_elements); - - match env.target_info.ptr_width() { - PtrWidth::Bytes4 => env.builder.build_load(alloca, "load_const_str"), - PtrWidth::Bytes8 => alloca.into(), - } - } - } - } -} - -fn const_str_alloca_ptr<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - parent: FunctionValue<'ctx>, - ptr: PointerValue<'ctx>, - len: IntValue<'ctx>, - cap: IntValue<'ctx>, -) -> PointerValue<'ctx> { - let typ = zig_str_type(env); - - let value = typ.const_named_struct(&[ptr.into(), len.into(), cap.into()]); - - let alloca = create_entry_block_alloca(env, parent, typ.into(), "const_str_store"); - - env.builder.build_store(alloca, value); - - alloca -} - -fn small_str_ptr_width_8<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - parent: FunctionValue<'ctx>, - str_literal: &str, -) -> PointerValue<'ctx> { - debug_assert_eq!(env.target_info.ptr_width() as u8, 8); - - let mut array = [0u8; 24]; - - array[..str_literal.len()].copy_from_slice(str_literal.as_bytes()); - - array[env.small_str_bytes() as usize - 1] = str_literal.len() as u8 | roc_std::RocStr::MASK; - - let word1 = u64::from_ne_bytes(array[0..8].try_into().unwrap()); - let word2 = u64::from_ne_bytes(array[8..16].try_into().unwrap()); - let word3 = u64::from_ne_bytes(array[16..24].try_into().unwrap()); - - let ptr = env.ptr_int().const_int(word1, false); - let len = env.ptr_int().const_int(word2, false); - let cap = env.ptr_int().const_int(word3, false); - - let address_space = AddressSpace::Generic; - let ptr_type = env.context.i8_type().ptr_type(address_space); - let ptr = env.builder.build_int_to_ptr(ptr, ptr_type, "to_u8_ptr"); - - const_str_alloca_ptr(env, parent, ptr, len, cap) -} - -fn small_str_ptr_width_4<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - parent: FunctionValue<'ctx>, - str_literal: &str, -) -> PointerValue<'ctx> { - debug_assert_eq!(env.target_info.ptr_width() as u8, 4); - - let mut array = [0u8; 12]; - - array[..str_literal.len()].copy_from_slice(str_literal.as_bytes()); - - array[env.small_str_bytes() as usize - 1] = str_literal.len() as u8 | roc_std::RocStr::MASK; - - let word1 = u32::from_ne_bytes(array[0..4].try_into().unwrap()); - let word2 = u32::from_ne_bytes(array[4..8].try_into().unwrap()); - let word3 = u32::from_ne_bytes(array[8..12].try_into().unwrap()); - - let ptr = env.ptr_int().const_int(word1 as u64, false); - let len = env.ptr_int().const_int(word2 as u64, false); - let cap = env.ptr_int().const_int(word3 as u64, false); - - let address_space = AddressSpace::Generic; - let ptr_type = env.context.i8_type().ptr_type(address_space); - let ptr = env.builder.build_int_to_ptr(ptr, ptr_type, "to_u8_ptr"); - - const_str_alloca_ptr(env, parent, ptr, len, cap) -} - -pub fn build_exp_call<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - layout_ids: &mut LayoutIds<'a>, - func_spec_solutions: &FuncSpecSolutions, - scope: &mut Scope<'a, 'ctx>, - parent: FunctionValue<'ctx>, - layout: &Layout<'a>, - call: &roc_mono::ir::Call<'a>, -) -> BasicValueEnum<'ctx> { - let roc_mono::ir::Call { - call_type, - arguments, - } = call; - - match call_type { - CallType::ByName { - name, - specialization_id, - arg_layouts, - ret_layout, - .. - } => { - let mut arg_tuples: Vec = - Vec::with_capacity_in(arguments.len(), env.arena); - - for symbol in arguments.iter() { - arg_tuples.push(load_symbol(scope, symbol)); - } - - let bytes = specialization_id.to_bytes(); - let callee_var = CalleeSpecVar(&bytes); - let func_spec = func_spec_solutions.callee_spec(callee_var).unwrap(); - - roc_call_with_args( - env, - arg_layouts, - ret_layout, - *name, - func_spec, - arg_tuples.into_bump_slice(), - ) - } - - CallType::LowLevel { op, update_mode } => { - let bytes = update_mode.to_bytes(); - let update_var = UpdateModeVar(&bytes); - let update_mode = func_spec_solutions - .update_mode(update_var) - .unwrap_or(UpdateMode::Immutable); - - run_low_level( - env, - layout_ids, - scope, - parent, - layout, - *op, - arguments, - update_mode, - ) - } - - CallType::HigherOrder(higher_order) => { - let bytes = higher_order.passed_function.specialization_id.to_bytes(); - let callee_var = CalleeSpecVar(&bytes); - let func_spec = func_spec_solutions.callee_spec(callee_var).unwrap(); - - run_higher_order_low_level(env, layout_ids, scope, layout, func_spec, higher_order) - } - - CallType::Foreign { - foreign_symbol, - ret_layout, - } => build_foreign_symbol(env, scope, foreign_symbol, arguments, ret_layout), - } -} - -pub const TAG_ID_INDEX: u32 = 1; -pub const TAG_DATA_INDEX: u32 = 0; - -pub fn struct_from_fields<'a, 'ctx, 'env, I>( - env: &Env<'a, 'ctx, 'env>, - struct_type: StructType<'ctx>, - values: I, -) -> StructValue<'ctx> -where - I: Iterator)>, -{ - let mut struct_value = struct_type.const_zero().into(); - - // Insert field exprs into struct_val - for (index, field_val) in values { - let index: u32 = index as u32; - - struct_value = env - .builder - .build_insert_value(struct_value, field_val, index, "insert_record_field") - .unwrap(); - } - - struct_value.into_struct_value() -} - -fn struct_pointer_from_fields<'a, 'ctx, 'env, I>( - env: &Env<'a, 'ctx, 'env>, - struct_type: StructType<'ctx>, - input_pointer: PointerValue<'ctx>, - values: I, -) where - I: Iterator, BasicValueEnum<'ctx>))>, -{ - let struct_ptr = env - .builder - .build_bitcast( - input_pointer, - struct_type.ptr_type(AddressSpace::Generic), - "struct_ptr", - ) - .into_pointer_value(); - - // Insert field exprs into struct_val - for (index, (field_layout, field_value)) in values { - let field_ptr = env - .builder - .build_struct_gep(struct_ptr, index as u32, "field_struct_gep") - .unwrap(); - - store_roc_value(env, field_layout, field_ptr, field_value); - } -} - -pub fn build_exp_expr<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - layout_ids: &mut LayoutIds<'a>, - func_spec_solutions: &FuncSpecSolutions, - scope: &mut Scope<'a, 'ctx>, - parent: FunctionValue<'ctx>, - layout: &Layout<'a>, - expr: &roc_mono::ir::Expr<'a>, -) -> BasicValueEnum<'ctx> { - use roc_mono::ir::Expr::*; - - match expr { - Literal(literal) => build_exp_literal(env, parent, layout, literal), - - Call(call) => build_exp_call( - env, - layout_ids, - func_spec_solutions, - scope, - parent, - layout, - call, - ), - - Struct(sorted_fields) => { - let ctx = env.context; - - // Determine types - let num_fields = sorted_fields.len(); - let mut field_types = Vec::with_capacity_in(num_fields, env.arena); - let mut field_vals = Vec::with_capacity_in(num_fields, env.arena); - - for symbol in sorted_fields.iter() { - // Zero-sized fields have no runtime representation. - // The layout of the struct expects them to be dropped! - let (field_expr, field_layout) = load_symbol_and_layout(scope, symbol); - if !field_layout.is_dropped_because_empty() { - field_types.push(basic_type_from_layout(env, field_layout)); - - if field_layout.is_passed_by_reference(env.target_info) { - let field_value = env.builder.build_load( - field_expr.into_pointer_value(), - "load_tag_to_put_in_struct", - ); - - field_vals.push(field_value); - } else { - field_vals.push(field_expr); - } - } - } - - // Create the struct_type - let struct_type = ctx.struct_type(field_types.into_bump_slice(), false); - - // Insert field exprs into struct_val - struct_from_fields(env, struct_type, field_vals.into_iter().enumerate()).into() - } - - Reuse { - arguments, - tag_layout: union_layout, - tag_id, - symbol, - .. - } => { - let reset = load_symbol(scope, symbol).into_pointer_value(); - build_tag( - env, - scope, - union_layout, - *tag_id, - arguments, - Some(reset), - parent, - ) - } - - Tag { - arguments, - tag_layout: union_layout, - tag_id, - .. - } => build_tag(env, scope, union_layout, *tag_id, arguments, None, parent), - - ExprBox { symbol } => { - let (value, layout) = load_symbol_and_layout(scope, symbol); - let basic_type = basic_type_from_layout(env, layout); - let allocation = reserve_with_refcount_help( - env, - basic_type, - layout.stack_size(env.target_info), - layout.alignment_bytes(env.target_info), - ); - - store_roc_value(env, *layout, allocation, value); - - allocation.into() - } - - ExprUnbox { symbol } => { - let value = load_symbol(scope, symbol); - - debug_assert!(value.is_pointer_value()); - - load_roc_value(env, *layout, value.into_pointer_value(), "load_boxed_value") - } - - Reset { symbol, .. } => { - let (tag_ptr, layout) = load_symbol_and_layout(scope, symbol); - let tag_ptr = tag_ptr.into_pointer_value(); - - // reset is only generated for union values - let union_layout = match layout { - Layout::Union(ul) => ul, - _ => unreachable!(), - }; - - let ctx = env.context; - let then_block = ctx.append_basic_block(parent, "then_reset"); - let else_block = ctx.append_basic_block(parent, "else_decref"); - let cont_block = ctx.append_basic_block(parent, "cont"); - - let refcount_ptr = - PointerToRefcount::from_ptr_to_data(env, tag_pointer_clear_tag_id(env, tag_ptr)); - let is_unique = refcount_ptr.is_1(env); - - env.builder - .build_conditional_branch(is_unique, then_block, else_block); - - { - // reset, when used on a unique reference, eagerly decrements the components of the - // referenced value, and returns the location of the now-invalid cell - env.builder.position_at_end(then_block); - - let reset_function = build_reset(env, layout_ids, *union_layout); - let call = env - .builder - .build_call(reset_function, &[tag_ptr.into()], "call_reset"); - - call.set_call_convention(FAST_CALL_CONV); - - let _ = call.try_as_basic_value(); - - env.builder.build_unconditional_branch(cont_block); - } - { - // If reset is used on a shared, non-reusable reference, it behaves - // like dec and returns NULL, which instructs reuse to behave like ctor - env.builder.position_at_end(else_block); - refcount_ptr.decrement(env, layout); - env.builder.build_unconditional_branch(cont_block); - } - { - env.builder.position_at_end(cont_block); - let phi = env.builder.build_phi(tag_ptr.get_type(), "branch"); - - let null_ptr = tag_ptr.get_type().const_null(); - phi.add_incoming(&[(&tag_ptr, then_block), (&null_ptr, else_block)]); - - phi.as_basic_value() - } - } - - StructAtIndex { - index, structure, .. - } => { - let (value, layout) = load_symbol_and_layout(scope, structure); - - let layout = if let Layout::LambdaSet(lambda_set) = layout { - lambda_set.runtime_representation() - } else { - *layout - }; - - // extract field from a record - match (value, layout) { - (StructValue(argument), Layout::Struct { field_layouts, .. }) => { - debug_assert!(!field_layouts.is_empty()); - - let field_value = env - .builder - .build_extract_value( - argument, - *index as u32, - env.arena - .alloc(format!("struct_field_access_record_{}", index)), - ) - .unwrap(); - - let field_layout = field_layouts[*index as usize]; - use_roc_value(env, field_layout, field_value, "struct_field_tag") - } - ( - PointerValue(argument), - Layout::Union(UnionLayout::NonNullableUnwrapped(fields)), - ) => { - let struct_layout = Layout::struct_no_name_order(fields); - let struct_type = basic_type_from_layout(env, &struct_layout); - - let cast_argument = env - .builder - .build_bitcast( - argument, - struct_type.ptr_type(AddressSpace::Generic), - "cast_rosetree_like", - ) - .into_pointer_value(); - - let ptr = env - .builder - .build_struct_gep( - cast_argument, - *index as u32, - env.arena.alloc(format!("non_nullable_unwrapped_{}", index)), - ) - .unwrap(); - - env.builder.build_load(ptr, "load_rosetree_like") - } - (other, layout) => { - // potential cause: indexing into an unwrapped 1-element record/tag? - unreachable!( - "can only index into struct layout\nValue: {:?}\nLayout: {:?}\nIndex: {:?}", - other, layout, index - ) - } - } - } - - EmptyArray => empty_polymorphic_list(env), - Array { elem_layout, elems } => list_literal(env, parent, scope, elem_layout, elems), - RuntimeErrorFunction(_) => todo!(), - - UnionAtIndex { - tag_id, - structure, - index, - union_layout, - } => { - // cast the argument bytes into the desired shape for this tag - let (argument, _structure_layout) = load_symbol_and_layout(scope, structure); - - match union_layout { - UnionLayout::NonRecursive(tag_layouts) => { - debug_assert!(argument.is_pointer_value()); - - let field_layouts = tag_layouts[*tag_id as usize]; - - let tag_id_type = - basic_type_from_layout(env, &union_layout.tag_id_layout()).into_int_type(); - - lookup_at_index_ptr2( - env, - union_layout, - tag_id_type, - field_layouts, - *index as usize, - argument.into_pointer_value(), - ) - } - UnionLayout::Recursive(tag_layouts) => { - debug_assert!(argument.is_pointer_value()); - - let field_layouts = tag_layouts[*tag_id as usize]; - - let tag_id_type = - basic_type_from_layout(env, &union_layout.tag_id_layout()).into_int_type(); - - let ptr = tag_pointer_clear_tag_id(env, argument.into_pointer_value()); - - lookup_at_index_ptr2( - env, - union_layout, - tag_id_type, - field_layouts, - *index as usize, - ptr, - ) - } - UnionLayout::NonNullableUnwrapped(field_layouts) => { - let struct_layout = Layout::struct_no_name_order(field_layouts); - - let struct_type = basic_type_from_layout(env, &struct_layout); - - lookup_at_index_ptr( - env, - union_layout, - field_layouts, - *index as usize, - argument.into_pointer_value(), - struct_type.into_struct_type(), - ) - } - UnionLayout::NullableWrapped { - nullable_id, - other_tags, - } => { - debug_assert!(argument.is_pointer_value()); - debug_assert_ne!(*tag_id, *nullable_id); - - let tag_index = if *tag_id < *nullable_id { - *tag_id - } else { - tag_id - 1 - }; - - let field_layouts = other_tags[tag_index as usize]; - - let tag_id_type = - basic_type_from_layout(env, &union_layout.tag_id_layout()).into_int_type(); - - let ptr = tag_pointer_clear_tag_id(env, argument.into_pointer_value()); - lookup_at_index_ptr2( - env, - union_layout, - tag_id_type, - field_layouts, - *index as usize, - ptr, - ) - } - UnionLayout::NullableUnwrapped { - nullable_id, - other_fields, - } => { - debug_assert!(argument.is_pointer_value()); - debug_assert_ne!(*tag_id != 0, *nullable_id); - - let field_layouts = other_fields; - let struct_layout = Layout::struct_no_name_order(field_layouts); - - let struct_type = basic_type_from_layout(env, &struct_layout); - - lookup_at_index_ptr( - env, - union_layout, - field_layouts, - // the tag id is not stored - *index as usize, - argument.into_pointer_value(), - struct_type.into_struct_type(), - ) - } - } - } - - GetTagId { - structure, - union_layout, - } => { - // cast the argument bytes into the desired shape for this tag - let (argument, _structure_layout) = load_symbol_and_layout(scope, structure); - - get_tag_id(env, parent, union_layout, argument).into() - } - } -} - -#[allow(clippy::too_many_arguments)] -fn build_wrapped_tag<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - scope: &Scope<'a, 'ctx>, - union_layout: &UnionLayout<'a>, - tag_id: u8, - arguments: &[Symbol], - tag_field_layouts: &[Layout<'a>], - tags: &[&[Layout<'a>]], - reuse_allocation: Option>, - parent: FunctionValue<'ctx>, -) -> BasicValueEnum<'ctx> { - let builder = env.builder; - - let tag_id_layout = union_layout.tag_id_layout(); - - let (field_types, field_values) = build_tag_fields(env, scope, tag_field_layouts, arguments); - - // Create the struct_type - let raw_data_ptr = allocate_tag(env, parent, reuse_allocation, union_layout, tags); - let struct_type = env.context.struct_type(&field_types, false); - - if union_layout.stores_tag_id_as_data(env.target_info) { - let tag_id_ptr = builder - .build_struct_gep(raw_data_ptr, TAG_ID_INDEX, "tag_id_index") - .unwrap(); - - let tag_id_type = basic_type_from_layout(env, &tag_id_layout).into_int_type(); - - env.builder - .build_store(tag_id_ptr, tag_id_type.const_int(tag_id as u64, false)); - - let opaque_struct_ptr = builder - .build_struct_gep(raw_data_ptr, TAG_DATA_INDEX, "tag_data_index") - .unwrap(); - - struct_pointer_from_fields( - env, - struct_type, - opaque_struct_ptr, - field_values.into_iter().enumerate(), - ); - - raw_data_ptr.into() - } else { - struct_pointer_from_fields( - env, - struct_type, - raw_data_ptr, - field_values.into_iter().enumerate(), - ); - - tag_pointer_set_tag_id(env, tag_id, raw_data_ptr).into() - } -} - -pub fn entry_block_alloca_zerofill<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - basic_type: BasicTypeEnum<'ctx>, - name: &str, -) -> PointerValue<'ctx> { - let parent = env - .builder - .get_insert_block() - .unwrap() - .get_parent() - .unwrap(); - - create_entry_block_alloca(env, parent, basic_type, name) -} - -fn build_tag_field_value<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - value: BasicValueEnum<'ctx>, - tag_field_layout: Layout<'a>, -) -> BasicValueEnum<'ctx> { - if let Layout::RecursivePointer = tag_field_layout { - debug_assert!(value.is_pointer_value()); - - // we store recursive pointers as `i64*` - env.builder.build_bitcast( - value, - env.context.i64_type().ptr_type(AddressSpace::Generic), - "cast_recursive_pointer", - ) - } else if tag_field_layout.is_passed_by_reference(env.target_info) { - debug_assert!(value.is_pointer_value()); - - // NOTE: we rely on this being passed to `store_roc_value` so that - // the value is memcpy'd - value - } else { - // this check fails for recursive tag unions, but can be helpful while debugging - // debug_assert_eq!(tag_field_layout, val_layout); - - value - } -} - -fn build_tag_fields<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - scope: &Scope<'a, 'ctx>, - fields: &[Layout<'a>], - arguments: &[Symbol], -) -> ( - Vec<'a, BasicTypeEnum<'ctx>>, - Vec<'a, (Layout<'a>, BasicValueEnum<'ctx>)>, -) { - debug_assert_eq!(fields.len(), arguments.len()); - - let capacity = fields.len(); - let mut field_types = Vec::with_capacity_in(capacity, env.arena); - let mut field_values = Vec::with_capacity_in(capacity, env.arena); - - for (field_symbol, tag_field_layout) in arguments.iter().zip(fields.iter()) { - let field_type = basic_type_from_layout(env, tag_field_layout); - field_types.push(field_type); - - let raw_value = load_symbol(scope, field_symbol); - let field_value = build_tag_field_value(env, raw_value, *tag_field_layout); - - field_values.push((*tag_field_layout, field_value)); - } - - (field_types, field_values) -} - -fn build_tag<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - scope: &Scope<'a, 'ctx>, - union_layout: &UnionLayout<'a>, - tag_id: TagIdIntType, - arguments: &[Symbol], - reuse_allocation: Option>, - parent: FunctionValue<'ctx>, -) -> BasicValueEnum<'ctx> { - let tag_id_layout = union_layout.tag_id_layout(); - let union_size = union_layout.number_of_tags(); - - match union_layout { - UnionLayout::NonRecursive(tags) => { - debug_assert!(union_size > 1); - - let internal_type = block_of_memory_slices(env.context, tags, env.target_info); - - let tag_id_type = basic_type_from_layout(env, &tag_id_layout).into_int_type(); - let wrapper_type = env - .context - .struct_type(&[internal_type, tag_id_type.into()], false); - let result_alloca = entry_block_alloca_zerofill(env, wrapper_type.into(), "opaque_tag"); - - // Determine types - let num_fields = arguments.len() + 1; - let mut field_types = Vec::with_capacity_in(num_fields, env.arena); - let mut field_vals = Vec::with_capacity_in(num_fields, env.arena); - - let tag_field_layouts = &tags[tag_id as usize]; - - for (field_symbol, tag_field_layout) in arguments.iter().zip(tag_field_layouts.iter()) { - let (val, _val_layout) = load_symbol_and_layout(scope, field_symbol); - - // Zero-sized fields have no runtime representation. - // The layout of the struct expects them to be dropped! - if !tag_field_layout.is_dropped_because_empty() { - let field_type = basic_type_from_layout(env, tag_field_layout); - - field_types.push(field_type); - - if let Layout::RecursivePointer = tag_field_layout { - panic!( - r"non-recursive tag unions cannot directly contain a recursive pointer" - ); - } else { - // this check fails for recursive tag unions, but can be helpful while debugging - // debug_assert_eq!(tag_field_layout, val_layout); - - field_vals.push(val); - } - } - } - // store the tag id - let tag_id_ptr = env - .builder - .build_struct_gep(result_alloca, TAG_ID_INDEX, "tag_id_ptr") - .unwrap(); - - let tag_id_intval = tag_id_type.const_int(tag_id as u64, false); - env.builder.build_store(tag_id_ptr, tag_id_intval); - - // Create the struct_type - let struct_type = env - .context - .struct_type(field_types.into_bump_slice(), false); - - let struct_opaque_ptr = env - .builder - .build_struct_gep(result_alloca, TAG_DATA_INDEX, "opaque_data_ptr") - .unwrap(); - let struct_ptr = env.builder.build_pointer_cast( - struct_opaque_ptr, - struct_type.ptr_type(AddressSpace::Generic), - "to_specific", - ); - - // Insert field exprs into struct_val - //let struct_val = - //struct_from_fields(env, struct_type, field_vals.into_iter().enumerate()); - - // Insert field exprs into struct_val - for (index, field_val) in field_vals.iter().copied().enumerate() { - let index: u32 = index as u32; - - let ptr = env - .builder - .build_struct_gep(struct_ptr, index, "get_tag_field_ptr") - .unwrap(); - - let field_layout = tag_field_layouts[index as usize]; - store_roc_value(env, field_layout, ptr, field_val); - } - - // env.builder.build_load(result_alloca, "load_result") - result_alloca.into() - } - UnionLayout::Recursive(tags) => { - debug_assert!(union_size > 1); - - let tag_field_layouts = &tags[tag_id as usize]; - - build_wrapped_tag( - env, - scope, - union_layout, - tag_id as _, - arguments, - tag_field_layouts, - tags, - reuse_allocation, - parent, - ) - } - UnionLayout::NullableWrapped { - nullable_id, - other_tags: tags, - } => { - let tag_field_layouts = { - use std::cmp::Ordering::*; - match tag_id.cmp(&(*nullable_id as _)) { - Equal => { - let layout = Layout::Union(*union_layout); - - return basic_type_from_layout(env, &layout) - .into_pointer_type() - .const_null() - .into(); - } - Less => &tags[tag_id as usize], - Greater => &tags[tag_id as usize - 1], - } - }; - - build_wrapped_tag( - env, - scope, - union_layout, - tag_id as _, - arguments, - tag_field_layouts, - tags, - reuse_allocation, - parent, - ) - } - UnionLayout::NonNullableUnwrapped(fields) => { - debug_assert_eq!(union_size, 1); - debug_assert_eq!(tag_id, 0); - debug_assert_eq!(arguments.len(), fields.len()); - - let (field_types, field_values) = build_tag_fields(env, scope, fields, arguments); - - // Create the struct_type - let data_ptr = - reserve_with_refcount_union_as_block_of_memory(env, *union_layout, &[fields]); - - let struct_type = env - .context - .struct_type(field_types.into_bump_slice(), false); - - struct_pointer_from_fields( - env, - struct_type, - data_ptr, - field_values.into_iter().enumerate(), - ); - - data_ptr.into() - } - UnionLayout::NullableUnwrapped { - nullable_id, - other_fields, - } => { - let tag_struct_type = - block_of_memory_slices(env.context, &[other_fields], env.target_info); - - if tag_id == *nullable_id as _ { - let output_type = tag_struct_type.ptr_type(AddressSpace::Generic); - - return output_type.const_null().into(); - } - - // this tag id is not the nullable one. For the type to be recursive, the other - // constructor must have at least one argument! - debug_assert!(!arguments.is_empty()); - - debug_assert!(union_size == 2); - - // Determine types - let (field_types, field_values) = build_tag_fields(env, scope, other_fields, arguments); - - // Create the struct_type - let data_ptr = - allocate_tag(env, parent, reuse_allocation, union_layout, &[other_fields]); - - let struct_type = env - .context - .struct_type(field_types.into_bump_slice(), false); - - struct_pointer_from_fields( - env, - struct_type, - data_ptr, - field_values.into_iter().enumerate(), - ); - - data_ptr.into() - } - } -} - -fn tag_pointer_set_tag_id<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - tag_id: u8, - pointer: PointerValue<'ctx>, -) -> PointerValue<'ctx> { - // we only have 3 bits, so can encode only 0..7 (or on 32-bit targets, 2 bits to encode 0..3) - debug_assert!((tag_id as u32) < env.target_info.ptr_width() as u32); - - let ptr_int = env.ptr_int(); - - let as_int = env.builder.build_ptr_to_int(pointer, ptr_int, "to_int"); - - let tag_id_intval = ptr_int.const_int(tag_id as u64, false); - let combined = env.builder.build_or(as_int, tag_id_intval, "store_tag_id"); - - env.builder - .build_int_to_ptr(combined, pointer.get_type(), "to_ptr") -} - -pub fn tag_pointer_tag_id_bits_and_mask(target_info: TargetInfo) -> (u64, u64) { - match target_info.ptr_width() { - roc_target::PtrWidth::Bytes8 => (3, 0b0000_0111), - roc_target::PtrWidth::Bytes4 => (2, 0b0000_0011), - } -} - -pub fn tag_pointer_read_tag_id<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - pointer: PointerValue<'ctx>, -) -> IntValue<'ctx> { - let (_, mask) = tag_pointer_tag_id_bits_and_mask(env.target_info); - let ptr_int = env.ptr_int(); - - let as_int = env.builder.build_ptr_to_int(pointer, ptr_int, "to_int"); - let mask_intval = env.ptr_int().const_int(mask, false); - - let masked = env.builder.build_and(as_int, mask_intval, "mask"); - - env.builder - .build_int_cast_sign_flag(masked, env.context.i8_type(), false, "to_u8") -} - -pub fn tag_pointer_clear_tag_id<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - pointer: PointerValue<'ctx>, -) -> PointerValue<'ctx> { - let ptr_int = env.ptr_int(); - - let (tag_id_bits_mask, _) = tag_pointer_tag_id_bits_and_mask(env.target_info); - - let as_int = env.builder.build_ptr_to_int(pointer, ptr_int, "to_int"); - - let mask = { - let a = env.ptr_int().const_all_ones(); - let tag_id_bits = env.ptr_int().const_int(tag_id_bits_mask, false); - env.builder.build_left_shift(a, tag_id_bits, "make_mask") - }; - - let masked = env.builder.build_and(as_int, mask, "masked"); - - env.builder - .build_int_to_ptr(masked, pointer.get_type(), "to_ptr") -} - -fn allocate_tag<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - parent: FunctionValue<'ctx>, - reuse_allocation: Option>, - union_layout: &UnionLayout<'a>, - tags: &[&[Layout<'a>]], -) -> PointerValue<'ctx> { - match reuse_allocation { - Some(ptr) => { - // check if its a null pointer - let is_null_ptr = env.builder.build_is_null(ptr, "is_null_ptr"); - let ctx = env.context; - let then_block = ctx.append_basic_block(parent, "then_allocate_fresh"); - let else_block = ctx.append_basic_block(parent, "else_reuse"); - let cont_block = ctx.append_basic_block(parent, "cont"); - - env.builder - .build_conditional_branch(is_null_ptr, then_block, else_block); - - let raw_ptr = { - env.builder.position_at_end(then_block); - let raw_ptr = - reserve_with_refcount_union_as_block_of_memory(env, *union_layout, tags); - env.builder.build_unconditional_branch(cont_block); - raw_ptr - }; - - let reuse_ptr = { - env.builder.position_at_end(else_block); - - let cleared = tag_pointer_clear_tag_id(env, ptr); - - env.builder.build_unconditional_branch(cont_block); - - cleared - }; - - { - env.builder.position_at_end(cont_block); - let phi = env.builder.build_phi(raw_ptr.get_type(), "branch"); - - phi.add_incoming(&[(&raw_ptr, then_block), (&reuse_ptr, else_block)]); - - phi.as_basic_value().into_pointer_value() - } - } - None => reserve_with_refcount_union_as_block_of_memory(env, *union_layout, tags), - } -} - -pub fn get_tag_id<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - parent: FunctionValue<'ctx>, - union_layout: &UnionLayout<'a>, - argument: BasicValueEnum<'ctx>, -) -> IntValue<'ctx> { - let builder = env.builder; - - let tag_id_layout = union_layout.tag_id_layout(); - let tag_id_int_type = basic_type_from_layout(env, &tag_id_layout).into_int_type(); - - match union_layout { - UnionLayout::NonRecursive(_) => { - debug_assert!(argument.is_pointer_value(), "{:?}", argument); - - let argument_ptr = argument.into_pointer_value(); - get_tag_id_wrapped(env, argument_ptr) - } - UnionLayout::Recursive(_) => { - let argument_ptr = argument.into_pointer_value(); - - if union_layout.stores_tag_id_as_data(env.target_info) { - get_tag_id_wrapped(env, argument_ptr) - } else { - tag_pointer_read_tag_id(env, argument_ptr) - } - } - UnionLayout::NonNullableUnwrapped(_) => tag_id_int_type.const_zero(), - UnionLayout::NullableWrapped { nullable_id, .. } => { - let argument_ptr = argument.into_pointer_value(); - let is_null = env.builder.build_is_null(argument_ptr, "is_null"); - - let ctx = env.context; - let then_block = ctx.append_basic_block(parent, "then"); - let else_block = ctx.append_basic_block(parent, "else"); - let cont_block = ctx.append_basic_block(parent, "cont"); - - let result = builder.build_alloca(tag_id_int_type, "result"); - - env.builder - .build_conditional_branch(is_null, then_block, else_block); - - { - env.builder.position_at_end(then_block); - let tag_id = tag_id_int_type.const_int(*nullable_id as u64, false); - env.builder.build_store(result, tag_id); - env.builder.build_unconditional_branch(cont_block); - } - - { - env.builder.position_at_end(else_block); - - let tag_id = if union_layout.stores_tag_id_as_data(env.target_info) { - get_tag_id_wrapped(env, argument_ptr) - } else { - tag_pointer_read_tag_id(env, argument_ptr) - }; - env.builder.build_store(result, tag_id); - env.builder.build_unconditional_branch(cont_block); - } - - env.builder.position_at_end(cont_block); - - env.builder - .build_load(result, "load_result") - .into_int_value() - } - UnionLayout::NullableUnwrapped { nullable_id, .. } => { - let argument_ptr = argument.into_pointer_value(); - let is_null = env.builder.build_is_null(argument_ptr, "is_null"); - - let then_value = tag_id_int_type.const_int(*nullable_id as u64, false); - let else_value = tag_id_int_type.const_int(!*nullable_id as u64, false); - - env.builder - .build_select(is_null, then_value, else_value, "select_tag_id") - .into_int_value() - } - } -} - -fn lookup_at_index_ptr<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - union_layout: &UnionLayout<'a>, - field_layouts: &[Layout<'_>], - index: usize, - value: PointerValue<'ctx>, - struct_type: StructType<'ctx>, -) -> BasicValueEnum<'ctx> { - let builder = env.builder; - - let ptr = env - .builder - .build_bitcast( - value, - struct_type.ptr_type(AddressSpace::Generic), - "cast_lookup_at_index_ptr", - ) - .into_pointer_value(); - - let elem_ptr = builder - .build_struct_gep(ptr, index as u32, "at_index_struct_gep") - .unwrap(); - - let field_layout = field_layouts[index]; - let result = load_roc_value(env, field_layout, elem_ptr, "load_at_index_ptr_old"); - - if let Some(Layout::RecursivePointer) = field_layouts.get(index as usize) { - // a recursive field is stored as a `i64*`, to use it we must cast it to - // a pointer to the block of memory representation - let actual_type = basic_type_from_layout(env, &Layout::Union(*union_layout)); - debug_assert!(actual_type.is_pointer_type()); - - builder.build_bitcast( - result, - actual_type, - "cast_rec_pointer_lookup_at_index_ptr_old", - ) - } else { - result - } -} - -fn lookup_at_index_ptr2<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - union_layout: &UnionLayout<'a>, - tag_id_type: IntType<'ctx>, - field_layouts: &[Layout<'_>], - index: usize, - value: PointerValue<'ctx>, -) -> BasicValueEnum<'ctx> { - let builder = env.builder; - - let struct_layout = Layout::struct_no_name_order(field_layouts); - let struct_type = basic_type_from_layout(env, &struct_layout); - - let wrapper_type = env - .context - .struct_type(&[struct_type, tag_id_type.into()], false); - - let ptr = env - .builder - .build_bitcast( - value, - wrapper_type.ptr_type(AddressSpace::Generic), - "cast_lookup_at_index_ptr", - ) - .into_pointer_value(); - - let data_ptr = builder - .build_struct_gep(ptr, TAG_DATA_INDEX, "at_index_struct_gep_tag") - .unwrap(); - - let elem_ptr = builder - .build_struct_gep(data_ptr, index as u32, "at_index_struct_gep_data") - .unwrap(); - - let field_layout = field_layouts[index]; - let result = load_roc_value(env, field_layout, elem_ptr, "load_at_index_ptr"); - - if let Some(Layout::RecursivePointer) = field_layouts.get(index as usize) { - // a recursive field is stored as a `i64*`, to use it we must cast it to - // a pointer to the block of memory representation - - let actual_type = basic_type_from_layout(env, &Layout::Union(*union_layout)); - debug_assert!(actual_type.is_pointer_type()); - - builder.build_bitcast( - result, - actual_type, - "cast_rec_pointer_lookup_at_index_ptr_new", - ) - } else { - result - } -} - -pub fn reserve_with_refcount<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - layout: &Layout<'a>, -) -> PointerValue<'ctx> { - let stack_size = layout.stack_size(env.target_info); - let alignment_bytes = layout.alignment_bytes(env.target_info); - - let basic_type = basic_type_from_layout(env, layout); - - reserve_with_refcount_help(env, basic_type, stack_size, alignment_bytes) -} - -fn reserve_with_refcount_union_as_block_of_memory<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - union_layout: UnionLayout<'a>, - fields: &[&[Layout<'a>]], -) -> PointerValue<'ctx> { - let ptr_bytes = env.target_info; - - let block_type = block_of_memory_slices(env.context, fields, env.target_info); - - let basic_type = if union_layout.stores_tag_id_as_data(ptr_bytes) { - let tag_id_type = basic_type_from_layout(env, &union_layout.tag_id_layout()); - - env.context - .struct_type(&[block_type, tag_id_type], false) - .into() - } else { - block_type - }; - - let mut stack_size = fields - .iter() - .map(|tag| tag.iter().map(|l| l.stack_size(env.target_info)).sum()) - .max() - .unwrap_or_default(); - - if union_layout.stores_tag_id_as_data(ptr_bytes) { - stack_size += union_layout.tag_id_layout().stack_size(env.target_info); - } - - let alignment_bytes = fields - .iter() - .flat_map(|tag| tag.iter().map(|l| l.alignment_bytes(env.target_info))) - .max() - .unwrap_or(0); - - reserve_with_refcount_help(env, basic_type, stack_size, alignment_bytes) -} - -fn reserve_with_refcount_help<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - basic_type: impl BasicType<'ctx>, - stack_size: u32, - alignment_bytes: u32, -) -> PointerValue<'ctx> { - let len_type = env.ptr_int(); - - let value_bytes_intvalue = len_type.const_int(stack_size as u64, false); - - allocate_with_refcount_help(env, basic_type, alignment_bytes, value_bytes_intvalue) -} - -pub fn allocate_with_refcount<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - layout: &Layout<'a>, - value: BasicValueEnum<'ctx>, -) -> PointerValue<'ctx> { - let data_ptr = reserve_with_refcount(env, layout); - - // store the value in the pointer - env.builder.build_store(data_ptr, value); - - data_ptr -} - -pub fn allocate_with_refcount_help<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - value_type: impl BasicType<'ctx>, - alignment_bytes: u32, - number_of_data_bytes: IntValue<'ctx>, -) -> PointerValue<'ctx> { - let ptr = call_bitcode_fn( - env, - &[ - number_of_data_bytes.into(), - env.alignment_const(alignment_bytes).into(), - ], - roc_builtins::bitcode::UTILS_ALLOCATE_WITH_REFCOUNT, - ) - .into_pointer_value(); - - let ptr_type = value_type.ptr_type(AddressSpace::Generic); - - env.builder - .build_bitcast(ptr, ptr_type, "alloc_cast_to_desired") - .into_pointer_value() -} - -macro_rules! dict_key_value_layout { - ($dict_layout:expr) => { - match $dict_layout { - Layout::Builtin(Builtin::Dict(key_layout, value_layout)) => (key_layout, value_layout), - _ => unreachable!("invalid dict layout"), - } - }; -} - -macro_rules! list_element_layout { - ($list_layout:expr) => { - match $list_layout { - Layout::Builtin(Builtin::List(list_layout)) => *list_layout, - _ => unreachable!("invalid list layout"), - } - }; -} - -fn list_literal<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - parent: FunctionValue<'ctx>, - scope: &Scope<'a, 'ctx>, - element_layout: &Layout<'a>, - elems: &[ListLiteralElement], -) -> BasicValueEnum<'ctx> { - let ctx = env.context; - let builder = env.builder; - - let element_type = basic_type_from_layout(env, element_layout); - - let list_length = elems.len(); - let list_length_intval = env.ptr_int().const_int(list_length as _, false); - - // TODO re-enable, currently causes morphic segfaults because it tries to update - // constants in-place... - // if element_type.is_int_type() { - if false { - let element_type = element_type.into_int_type(); - let element_width = element_layout.stack_size(env.target_info); - let size = list_length * element_width as usize; - let alignment = element_layout - .alignment_bytes(env.target_info) - .max(env.target_info.ptr_width() as u32); - - let mut is_all_constant = true; - let zero_elements = - (env.target_info.ptr_width() as u8 as f64 / element_width as f64).ceil() as usize; - - // runtime-evaluated elements - let mut runtime_evaluated_elements = Vec::with_capacity_in(list_length, env.arena); - - // set up a global that contains all the literal elements of the array - // any variables or expressions are represented as `undef` - let global = { - let mut global_elements = Vec::with_capacity_in(list_length, env.arena); - - // Add zero bytes that represent the refcount - // - // - if all elements are const, then we store the whole list as a constant. - // It then needs a refcount before the first element. - // - but if the list is not all constants, then we will just copy the constant values, - // and we do not need that refcount at the start - // - // In the latter case, we won't store the zeros in the globals - // (we slice them off again below) - for _ in 0..zero_elements { - global_elements.push(element_type.const_zero()); - } - - // Copy the elements from the list literal into the array - for (index, element) in elems.iter().enumerate() { - match element { - ListLiteralElement::Literal(literal) => { - let val = build_exp_literal(env, parent, element_layout, literal); - global_elements.push(val.into_int_value()); - } - ListLiteralElement::Symbol(symbol) => { - let val = load_symbol(scope, symbol); - - // here we'd like to furthermore check for intval.is_const(). - // if all elements are const for LLVM, we could make the array a constant. - // BUT morphic does not know about this, and could allow us to modify that - // array in-place. That would cause a segfault. So, we'll have to find - // constants ourselves and cannot lean on LLVM here. - - is_all_constant = false; - - runtime_evaluated_elements.push((index, val)); - - global_elements.push(element_type.get_undef()); - } - }; - } - - let const_elements = if is_all_constant { - global_elements.into_bump_slice() - } else { - &global_elements[zero_elements..] - }; - - // use None for the address space (e.g. Const does not work) - let typ = element_type.array_type(const_elements.len() as u32); - let global = env.module.add_global(typ, None, "roc__list_literal"); - - global.set_constant(true); - global.set_alignment(alignment); - global.set_unnamed_addr(true); - global.set_linkage(inkwell::module::Linkage::Private); - - global.set_initializer(&element_type.const_array(const_elements)); - global.as_pointer_value() - }; - - if is_all_constant { - // all elements are constants, so we can use the memory in the constants section directly - // here we make a pointer to the first actual element (skipping the 0 bytes that - // represent the refcount) - let zero = env.ptr_int().const_zero(); - let offset = env.ptr_int().const_int(zero_elements as _, false); - - let ptr = unsafe { - env.builder - .build_in_bounds_gep(global, &[zero, offset], "first_element_pointer") - }; - - super::build_list::store_list(env, ptr, list_length_intval) - } else { - // some of our elements are non-constant, so we must allocate space on the heap - let ptr = allocate_list(env, element_layout, list_length_intval); - - // then, copy the relevant segment from the constant section into the heap - env.builder - .build_memcpy( - ptr, - alignment, - global, - alignment, - env.ptr_int().const_int(size as _, false), - ) - .unwrap(); - - // then replace the `undef`s with the values that we evaluate at runtime - for (index, val) in runtime_evaluated_elements { - let index_val = ctx.i64_type().const_int(index as u64, false); - let elem_ptr = unsafe { builder.build_in_bounds_gep(ptr, &[index_val], "index") }; - - builder.build_store(elem_ptr, val); - } - - super::build_list::store_list(env, ptr, list_length_intval) - } - } else { - let ptr = allocate_list(env, element_layout, list_length_intval); - - // Copy the elements from the list literal into the array - for (index, element) in elems.iter().enumerate() { - let val = match element { - ListLiteralElement::Literal(literal) => { - build_exp_literal(env, parent, element_layout, literal) - } - ListLiteralElement::Symbol(symbol) => load_symbol(scope, symbol), - }; - let index_val = ctx.i64_type().const_int(index as u64, false); - let elem_ptr = unsafe { builder.build_in_bounds_gep(ptr, &[index_val], "index") }; - - store_roc_value(env, *element_layout, elem_ptr, val); - } - - super::build_list::store_list(env, ptr, list_length_intval) - } -} - -pub fn load_roc_value<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - layout: Layout<'a>, - source: PointerValue<'ctx>, - name: &str, -) -> BasicValueEnum<'ctx> { - if layout.is_passed_by_reference(env.target_info) { - let alloca = entry_block_alloca_zerofill(env, basic_type_from_layout(env, &layout), name); - - store_roc_value(env, layout, alloca, source.into()); - - alloca.into() - } else { - env.builder.build_load(source, name) - } -} - -pub fn use_roc_value<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - layout: Layout<'a>, - source: BasicValueEnum<'ctx>, - name: &str, -) -> BasicValueEnum<'ctx> { - if layout.is_passed_by_reference(env.target_info) { - let alloca = entry_block_alloca_zerofill(env, basic_type_from_layout(env, &layout), name); - - env.builder.build_store(alloca, source); - - alloca.into() - } else { - source - } -} - -pub fn store_roc_value_opaque<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - layout: Layout<'a>, - opaque_destination: PointerValue<'ctx>, - value: BasicValueEnum<'ctx>, -) { - let target_type = basic_type_from_layout(env, &layout).ptr_type(AddressSpace::Generic); - let destination = - env.builder - .build_pointer_cast(opaque_destination, target_type, "store_roc_value_opaque"); - - store_roc_value(env, layout, destination, value) -} - -pub fn store_roc_value<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - layout: Layout<'a>, - destination: PointerValue<'ctx>, - value: BasicValueEnum<'ctx>, -) { - if layout.is_passed_by_reference(env.target_info) { - debug_assert!(value.is_pointer_value()); - - let align_bytes = layout.alignment_bytes(env.target_info); - - if align_bytes > 0 { - let size = env - .ptr_int() - .const_int(layout.stack_size(env.target_info) as u64, false); - - env.builder - .build_memcpy( - destination, - align_bytes, - value.into_pointer_value(), - align_bytes, - size, - ) - .unwrap(); - } - } else { - env.builder.build_store(destination, value); - } -} - -pub fn build_exp_stmt<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - layout_ids: &mut LayoutIds<'a>, - func_spec_solutions: &FuncSpecSolutions, - scope: &mut Scope<'a, 'ctx>, - parent: FunctionValue<'ctx>, - stmt: &roc_mono::ir::Stmt<'a>, -) -> BasicValueEnum<'ctx> { - use roc_mono::ir::Stmt::*; - - match stmt { - Let(first_symbol, first_expr, first_layout, mut cont) => { - let mut queue = Vec::new_in(env.arena); - - queue.push((first_symbol, first_expr, first_layout)); - - while let Let(symbol, expr, layout, new_cont) = cont { - queue.push((symbol, expr, layout)); - - cont = new_cont; - } - - let mut stack = Vec::with_capacity_in(queue.len(), env.arena); - - for (symbol, expr, layout) in queue { - debug_assert!(layout != &Layout::RecursivePointer); - - let val = build_exp_expr( - env, - layout_ids, - func_spec_solutions, - scope, - parent, - layout, - expr, - ); - - // Make a new scope which includes the binding we just encountered. - // This should be done *after* compiling the bound expr, since any - // recursive (in the LetRec sense) bindings should already have - // been extracted as procedures. Nothing in here should need to - // access itself! - // scope = scope.clone(); - - scope.insert(*symbol, (*layout, val)); - stack.push(*symbol); - } - - let result = build_exp_stmt(env, layout_ids, func_spec_solutions, scope, parent, cont); - - for symbol in stack { - scope.remove(&symbol); - } - - result - } - Ret(symbol) => { - let (value, layout) = load_symbol_and_layout(scope, symbol); - - match RocReturn::from_layout(env, layout) { - RocReturn::Return => { - if let Some(block) = env.builder.get_insert_block() { - if block.get_terminator().is_none() { - env.builder.build_return(Some(&value)); - } - } - - value - } - RocReturn::ByPointer => { - // we need to write our value into the final argument of the current function - let parameters = parent.get_params(); - let out_parameter = parameters.last().unwrap(); - debug_assert!(out_parameter.is_pointer_value()); - - // store_roc_value(env, *layout, out_parameter.into_pointer_value(), value); - - let destination = out_parameter.into_pointer_value(); - if layout.is_passed_by_reference(env.target_info) { - let align_bytes = layout.alignment_bytes(env.target_info); - - if align_bytes > 0 { - debug_assert!( - value.is_pointer_value(), - "{:?}: {:?}\n{:?}", - parent.get_name(), - value, - layout - ); - - // What we want to do here is - // - // let value_ptr = value.into_pointer_value(); - // if value_ptr.get_first_use().is_some() { - // value_ptr.replace_all_uses_with(destination); - // - // In other words, if the source pointer is used, - // then we just subsitute the source for the input pointer, done. - // - // Only that does not work if the source is not written to. - // A simple example is the identity function - // - // A slightly more complex case that will also make the above not - // work is when the source pointer is only incremented, but not - // written to. Then there is a first_use, but it's still invalid to - // subsitute source with destination - // - // Hence, we explicitly memcpy source to destination, and rely on - // LLVM optimizing away any inefficiencies. - let size = env.ptr_int().const_int( - layout.stack_size_without_alignment(env.target_info) as u64, - false, - ); - - env.builder - .build_memcpy( - destination, - align_bytes, - value.into_pointer_value(), - align_bytes, - size, - ) - .unwrap(); - } - } else { - env.builder.build_store(destination, value); - } - - if let Some(block) = env.builder.get_insert_block() { - match block.get_terminator() { - None => { - env.builder.build_return(None); - } - Some(terminator) => { - terminator.remove_from_basic_block(); - env.builder.build_return(None); - } - } - } - - env.context.i8_type().const_zero().into() - } - } - } - - Switch { - branches, - default_branch, - ret_layout, - cond_layout, - cond_symbol, - } => { - let ret_type = basic_type_from_layout(env, ret_layout); - - let switch_args = SwitchArgsIr { - cond_layout: *cond_layout, - cond_symbol: *cond_symbol, - branches, - default_branch: default_branch.1, - ret_type, - }; - - build_switch_ir( - env, - layout_ids, - func_spec_solutions, - scope, - parent, - switch_args, - ) - } - Join { - id, - parameters, - remainder, - body: continuation, - } => { - let builder = env.builder; - let context = env.context; - - // create new block - let cont_block = context.append_basic_block(parent, "joinpointcont"); - - let mut joinpoint_args = Vec::with_capacity_in(parameters.len(), env.arena); - { - let current = builder.get_insert_block().unwrap(); - builder.position_at_end(cont_block); - - for param in parameters.iter() { - let basic_type = basic_type_from_layout(env, ¶m.layout); - - let phi_type = if param.layout.is_passed_by_reference(env.target_info) { - basic_type.ptr_type(AddressSpace::Generic).into() - } else { - basic_type - }; - - let phi_node = env.builder.build_phi(phi_type, "joinpointarg"); - joinpoint_args.push(phi_node); - } - - builder.position_at_end(current); - } - - // store this join point - let joinpoint_args = joinpoint_args.into_bump_slice(); - scope.join_points.insert(*id, (cont_block, joinpoint_args)); - - // construct the blocks that may jump to this join point - build_exp_stmt( - env, - layout_ids, - func_spec_solutions, - scope, - parent, - remainder, - ); - - let phi_block = builder.get_insert_block().unwrap(); - - // put the cont block at the back - builder.position_at_end(cont_block); - - // bind the values - for (phi_value, param) in joinpoint_args.iter().zip(parameters.iter()) { - let value = phi_value.as_basic_value(); - scope.insert(param.symbol, (param.layout, value)); - } - - // put the continuation in - let result = build_exp_stmt( - env, - layout_ids, - func_spec_solutions, - scope, - parent, - continuation, - ); - - // remove this join point again - scope.join_points.remove(id); - - cont_block.move_after(phi_block).unwrap(); - - result - } - - Jump(join_point, arguments) => { - let builder = env.builder; - let context = env.context; - let (cont_block, argument_phi_values) = scope.join_points.get(join_point).unwrap(); - - let current_block = builder.get_insert_block().unwrap(); - - for (phi_value, argument) in argument_phi_values.iter().zip(arguments.iter()) { - let (value, _) = load_symbol_and_layout(scope, argument); - - phi_value.add_incoming(&[(&value, current_block)]); - } - - builder.build_unconditional_branch(*cont_block); - - // This doesn't currently do anything - context.i64_type().const_zero().into() - } - - Refcounting(modify, cont) => { - use ModifyRc::*; - - match modify { - Inc(symbol, inc_amount) => { - let (value, layout) = load_symbol_and_layout(scope, symbol); - let layout = *layout; - - if layout.contains_refcounted() { - increment_refcount_layout( - env, - parent, - layout_ids, - *inc_amount, - value, - &layout, - ); - } - - build_exp_stmt(env, layout_ids, func_spec_solutions, scope, parent, cont) - } - Dec(symbol) => { - let (value, layout) = load_symbol_and_layout(scope, symbol); - - if layout.contains_refcounted() { - decrement_refcount_layout(env, parent, layout_ids, value, layout); - } - - build_exp_stmt(env, layout_ids, func_spec_solutions, scope, parent, cont) - } - DecRef(symbol) => { - let (value, layout) = load_symbol_and_layout(scope, symbol); - - match layout { - Layout::Builtin(Builtin::Str) => todo!(), - Layout::Builtin(Builtin::List(element_layout)) => { - debug_assert!(value.is_struct_value()); - let alignment = element_layout.alignment_bytes(env.target_info); - - build_list::decref(env, value.into_struct_value(), alignment); - } - Layout::Builtin(Builtin::Dict(key_layout, value_layout)) => { - debug_assert!(value.is_struct_value()); - let alignment = key_layout - .alignment_bytes(env.target_info) - .max(value_layout.alignment_bytes(env.target_info)); - - build_dict::decref(env, value.into_struct_value(), alignment); - } - Layout::Builtin(Builtin::Set(key_layout)) => { - debug_assert!(value.is_struct_value()); - let alignment = key_layout.alignment_bytes(env.target_info); - - build_dict::decref(env, value.into_struct_value(), alignment); - } - - _ if layout.is_refcounted() => { - if value.is_pointer_value() { - let value_ptr = value.into_pointer_value(); - - let then_block = env.context.append_basic_block(parent, "then"); - let done_block = env.context.append_basic_block(parent, "done"); - - let condition = - env.builder.build_is_not_null(value_ptr, "box_is_not_null"); - env.builder - .build_conditional_branch(condition, then_block, done_block); - - { - env.builder.position_at_end(then_block); - let refcount_ptr = - PointerToRefcount::from_ptr_to_data(env, value_ptr); - refcount_ptr.decrement(env, layout); - - env.builder.build_unconditional_branch(done_block); - } - - env.builder.position_at_end(done_block); - } else { - eprint!("we're likely leaking memory; see issue #985 for details"); - } - } - _ => { - // nothing to do - } - } - - build_exp_stmt(env, layout_ids, func_spec_solutions, scope, parent, cont) - } - } - } - - Expect { - condition: cond, - region: _, - lookups: _, - layouts: _, - remainder, - } => { - // do stuff - - let bd = env.builder; - let context = env.context; - - let (cond, _cond_layout) = load_symbol_and_layout(scope, cond); - - let condition = bd.build_int_compare( - IntPredicate::EQ, - cond.into_int_value(), - context.bool_type().const_int(1, false), - "is_true", - ); - - let then_block = context.append_basic_block(parent, "then_block"); - let throw_block = context.append_basic_block(parent, "throw_block"); - - bd.build_conditional_branch(condition, then_block, throw_block); - - { - bd.position_at_end(throw_block); - - match env.target_info.ptr_width() { - roc_target::PtrWidth::Bytes8 => { - let func = env - .module - .get_function(bitcode::UTILS_EXPECT_FAILED) - .unwrap(); - // TODO get the actual line info instead of - // hardcoding as zero! - let callable = CallableValue::try_from(func).unwrap(); - let start_line = context.i32_type().const_int(0, false); - let end_line = context.i32_type().const_int(0, false); - let start_col = context.i16_type().const_int(0, false); - let end_col = context.i16_type().const_int(0, false); - - bd.build_call( - callable, - &[ - start_line.into(), - end_line.into(), - start_col.into(), - end_col.into(), - ], - "call_expect_failed", - ); - - bd.build_unconditional_branch(then_block); - } - roc_target::PtrWidth::Bytes4 => { - // temporary WASM implementation - throw_exception(env, "An expectation failed!"); - } - } - } - - bd.position_at_end(then_block); - - build_exp_stmt( - env, - layout_ids, - func_spec_solutions, - scope, - parent, - remainder, - ) - } - - RuntimeError(error_msg) => { - throw_exception(env, error_msg); - - // unused value (must return a BasicValue) - let zero = env.context.i64_type().const_zero(); - zero.into() - } - } -} - -pub fn load_symbol<'a, 'ctx>(scope: &Scope<'a, 'ctx>, symbol: &Symbol) -> BasicValueEnum<'ctx> { - match scope.get(symbol) { - Some((_, ptr)) => *ptr, - - None => panic!( - "There was no entry for {:?} {} in scope {:?}", - symbol, symbol, scope - ), - } -} - -pub fn load_symbol_and_layout<'a, 'ctx, 'b>( - scope: &'b Scope<'a, 'ctx>, - symbol: &Symbol, -) -> (BasicValueEnum<'ctx>, &'b Layout<'a>) { - match scope.get(symbol) { - Some((layout, ptr)) => (*ptr, layout), - None => panic!("There was no entry for {:?} in scope {:?}", symbol, scope), - } -} - -pub fn load_symbol_and_lambda_set<'a, 'ctx, 'b>( - scope: &'b Scope<'a, 'ctx>, - symbol: &Symbol, -) -> (BasicValueEnum<'ctx>, LambdaSet<'a>) { - match scope.get(symbol) { - Some((Layout::LambdaSet(lambda_set), ptr)) => (*ptr, *lambda_set), - Some((other, ptr)) => panic!("Not a lambda set: {:?}, {:?}", other, ptr), - None => panic!("There was no entry for {:?} in scope {:?}", symbol, scope), - } -} - -/// Cast a value to another value of the same (or smaller?) size -pub fn cast_basic_basic<'ctx>( - builder: &Builder<'ctx>, - from_value: BasicValueEnum<'ctx>, - to_type: BasicTypeEnum<'ctx>, -) -> BasicValueEnum<'ctx> { - complex_bitcast(builder, from_value, to_type, "cast_basic_basic") -} - -pub fn complex_bitcast_struct_struct<'ctx>( - builder: &Builder<'ctx>, - from_value: StructValue<'ctx>, - to_type: StructType<'ctx>, - name: &str, -) -> StructValue<'ctx> { - complex_bitcast(builder, from_value.into(), to_type.into(), name).into_struct_value() -} - -pub fn cast_block_of_memory_to_tag<'ctx>( - builder: &Builder<'ctx>, - from_value: StructValue<'ctx>, - to_type: BasicTypeEnum<'ctx>, -) -> StructValue<'ctx> { - complex_bitcast( - builder, - from_value.into(), - to_type, - "block_of_memory_to_tag", - ) - .into_struct_value() -} - -/// Cast a value to another value of the same (or smaller?) size -pub fn complex_bitcast<'ctx>( - builder: &Builder<'ctx>, - from_value: BasicValueEnum<'ctx>, - to_type: BasicTypeEnum<'ctx>, - name: &str, -) -> BasicValueEnum<'ctx> { - use BasicTypeEnum::*; - - if let (PointerType(_), PointerType(_)) = (from_value.get_type(), to_type) { - // we can't use the more straightforward bitcast in all cases - // it seems like a bitcast only works on integers and pointers - // and crucially does not work not on arrays - return builder.build_bitcast(from_value, to_type, name); - } - - complex_bitcast_from_bigger_than_to(builder, from_value, to_type, name) -} - -/// Check the size of the input and output types. Pretending we have more bytes at a pointer than -/// we actually do can lead to faulty optimizations and weird segfaults/crashes -pub fn complex_bitcast_check_size<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - from_value: BasicValueEnum<'ctx>, - to_type: BasicTypeEnum<'ctx>, - name: &str, -) -> BasicValueEnum<'ctx> { - use BasicTypeEnum::*; - - if let (PointerType(_), PointerType(_)) = (from_value.get_type(), to_type) { - // we can't use the more straightforward bitcast in all cases - // it seems like a bitcast only works on integers and pointers - // and crucially does not work not on arrays - return env.builder.build_bitcast(from_value, to_type, name); - } - - let block = env.builder.get_insert_block().expect("to be in a function"); - let parent = block.get_parent().expect("to be in a function"); - let then_block = env.context.append_basic_block(parent, "then"); - let else_block = env.context.append_basic_block(parent, "else"); - let cont_block = env.context.append_basic_block(parent, "cont"); - - let from_size = from_value.get_type().size_of().unwrap(); - let to_size = to_type.size_of().unwrap(); - - let condition = env.builder.build_int_compare( - IntPredicate::UGT, - from_size, - to_size, - "from_size >= to_size", - ); - - env.builder - .build_conditional_branch(condition, then_block, else_block); - - let then_answer = { - env.builder.position_at_end(then_block); - let result = complex_bitcast_from_bigger_than_to(env.builder, from_value, to_type, name); - env.builder.build_unconditional_branch(cont_block); - result - }; - - let else_answer = { - env.builder.position_at_end(else_block); - let result = complex_bitcast_to_bigger_than_from(env.builder, from_value, to_type, name); - env.builder.build_unconditional_branch(cont_block); - result - }; - - env.builder.position_at_end(cont_block); - - let result = env.builder.build_phi(then_answer.get_type(), "answer"); - - result.add_incoming(&[(&then_answer, then_block), (&else_answer, else_block)]); - - result.as_basic_value() -} - -fn complex_bitcast_from_bigger_than_to<'ctx>( - builder: &Builder<'ctx>, - from_value: BasicValueEnum<'ctx>, - to_type: BasicTypeEnum<'ctx>, - name: &str, -) -> BasicValueEnum<'ctx> { - // store the value in memory - let argument_pointer = builder.build_alloca(from_value.get_type(), "cast_alloca"); - builder.build_store(argument_pointer, from_value); - - // then read it back as a different type - let to_type_pointer = builder - .build_bitcast( - argument_pointer, - to_type.ptr_type(inkwell::AddressSpace::Generic), - name, - ) - .into_pointer_value(); - - builder.build_load(to_type_pointer, "cast_value") -} - -fn complex_bitcast_to_bigger_than_from<'ctx>( - builder: &Builder<'ctx>, - from_value: BasicValueEnum<'ctx>, - to_type: BasicTypeEnum<'ctx>, - name: &str, -) -> BasicValueEnum<'ctx> { - // reserve space in memory with the return type. This way, if the return type is bigger - // than the input type, we don't access invalid memory when later taking a pointer to - // the cast value - let storage = builder.build_alloca(to_type, "cast_alloca"); - - // then cast the pointer to our desired type - let from_type_pointer = builder - .build_bitcast( - storage, - from_value - .get_type() - .ptr_type(inkwell::AddressSpace::Generic), - name, - ) - .into_pointer_value(); - - // store the value in memory - builder.build_store(from_type_pointer, from_value); - - // then read it back as a different type - builder.build_load(storage, "cast_value") -} - -/// get the tag id out of a pointer to a wrapped (i.e. stores the tag id at runtime) layout -fn get_tag_id_wrapped<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - from_value: PointerValue<'ctx>, -) -> IntValue<'ctx> { - let tag_id_ptr = env - .builder - .build_struct_gep(from_value, TAG_ID_INDEX, "tag_id_ptr") - .unwrap(); - - env.builder - .build_load(tag_id_ptr, "load_tag_id") - .into_int_value() -} - -pub fn get_tag_id_non_recursive<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - tag: StructValue<'ctx>, -) -> IntValue<'ctx> { - env.builder - .build_extract_value(tag, TAG_ID_INDEX, "get_tag_id") - .unwrap() - .into_int_value() -} - -struct SwitchArgsIr<'a, 'ctx> { - pub cond_symbol: Symbol, - pub cond_layout: Layout<'a>, - pub branches: &'a [(u64, BranchInfo<'a>, roc_mono::ir::Stmt<'a>)], - pub default_branch: &'a roc_mono::ir::Stmt<'a>, - pub ret_type: BasicTypeEnum<'ctx>, -} - -fn const_i128<'a, 'ctx, 'env>(env: &Env<'a, 'ctx, 'env>, value: i128) -> IntValue<'ctx> { - // truncate the lower 64 bits - let value = value as u128; - let a = value as u64; - - // get the upper 64 bits - let b = (value >> 64) as u64; - - env.context - .i128_type() - .const_int_arbitrary_precision(&[a, b]) -} - -fn const_u128<'a, 'ctx, 'env>(env: &Env<'a, 'ctx, 'env>, value: u128) -> IntValue<'ctx> { - // truncate the lower 64 bits - let value = value as u128; - let a = value as u64; - - // get the upper 64 bits - let b = (value >> 64) as u64; - - env.context - .i128_type() - .const_int_arbitrary_precision(&[a, b]) -} - -fn build_switch_ir<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - layout_ids: &mut LayoutIds<'a>, - func_spec_solutions: &FuncSpecSolutions, - scope: &Scope<'a, 'ctx>, - parent: FunctionValue<'ctx>, - switch_args: SwitchArgsIr<'a, 'ctx>, -) -> BasicValueEnum<'ctx> { - let arena = env.arena; - let builder = env.builder; - let context = env.context; - let SwitchArgsIr { - branches, - cond_symbol, - mut cond_layout, - default_branch, - ret_type, - .. - } = switch_args; - - let mut copy = scope.clone(); - let scope = &mut copy; - - let cond_symbol = &cond_symbol; - let (cond_value, stored_layout) = load_symbol_and_layout(scope, cond_symbol); - - debug_assert_eq!( - basic_type_from_layout(env, &cond_layout), - basic_type_from_layout(env, stored_layout), - "This switch matches on {:?}, but the matched-on symbol {:?} has layout {:?}", - cond_layout, - cond_symbol, - stored_layout - ); - - let cont_block = context.append_basic_block(parent, "cont"); - - // Build the condition - let cond = match cond_layout { - Layout::Builtin(Builtin::Float(float_width)) => { - // float matches are done on the bit pattern - cond_layout = Layout::float_width(float_width); - - let int_type = match float_width { - FloatWidth::F32 => env.context.i32_type(), - FloatWidth::F64 => env.context.i64_type(), - FloatWidth::F128 => env.context.i128_type(), - }; - - builder - .build_bitcast(cond_value, int_type, "") - .into_int_value() - } - Layout::Union(variant) => { - cond_layout = variant.tag_id_layout(); - - get_tag_id(env, parent, &variant, cond_value) - } - Layout::Builtin(_) => cond_value.into_int_value(), - other => todo!("Build switch value from layout: {:?}", other), - }; - - // Build the cases - let mut incoming = Vec::with_capacity_in(branches.len(), arena); - - if let Layout::Builtin(Builtin::Bool) = cond_layout { - match (branches, default_branch) { - ([(0, _, false_branch)], true_branch) | ([(1, _, true_branch)], false_branch) => { - let then_block = context.append_basic_block(parent, "then_block"); - let else_block = context.append_basic_block(parent, "else_block"); - - builder.build_conditional_branch(cond, then_block, else_block); - - { - builder.position_at_end(then_block); - - let branch_val = build_exp_stmt( - env, - layout_ids, - func_spec_solutions, - scope, - parent, - true_branch, - ); - - if then_block.get_terminator().is_none() { - builder.build_unconditional_branch(cont_block); - incoming.push((branch_val, then_block)); - } - } - - { - builder.position_at_end(else_block); - - let branch_val = build_exp_stmt( - env, - layout_ids, - func_spec_solutions, - scope, - parent, - false_branch, - ); - - if else_block.get_terminator().is_none() { - builder.build_unconditional_branch(cont_block); - incoming.push((branch_val, else_block)); - } - } - } - - _ => { - unreachable!() - } - } - } else { - let default_block = context.append_basic_block(parent, "default"); - let mut cases = Vec::with_capacity_in(branches.len(), arena); - - for (int, _, _) in branches.iter() { - // Switch constants must all be same type as switch value! - // e.g. this is incorrect, and will trigger a LLVM warning: - // - // switch i8 %apple1, label %default [ - // i64 2, label %branch2 - // i64 0, label %branch0 - // i64 1, label %branch1 - // ] - // - // they either need to all be i8, or i64 - let condition_int_type = cond.get_type(); - - let int_val = if condition_int_type == context.i128_type() { - const_i128(env, *int as i128) - } else { - condition_int_type.const_int(*int as u64, false) - }; - - let block = context.append_basic_block(parent, format!("branch{}", int).as_str()); - - cases.push((int_val, block)); - } - - builder.build_switch(cond, default_block, &cases); - - for ((_, _, branch_expr), (_, block)) in branches.iter().zip(cases) { - builder.position_at_end(block); - - let branch_val = build_exp_stmt( - env, - layout_ids, - func_spec_solutions, - scope, - parent, - branch_expr, - ); - - if block.get_terminator().is_none() { - builder.build_unconditional_branch(cont_block); - incoming.push((branch_val, block)); - } - } - - // The block for the conditional's default branch. - builder.position_at_end(default_block); - - let default_val = build_exp_stmt( - env, - layout_ids, - func_spec_solutions, - scope, - parent, - default_branch, - ); - - if default_block.get_terminator().is_none() { - builder.build_unconditional_branch(cont_block); - incoming.push((default_val, default_block)); - } - } - - // emit merge block - if incoming.is_empty() { - unsafe { - cont_block.delete().unwrap(); - } - // produce unused garbage value - context.i64_type().const_zero().into() - } else { - builder.position_at_end(cont_block); - - let phi = builder.build_phi(ret_type, "branch"); - - for (branch_val, block) in incoming { - phi.add_incoming(&[(&Into::::into(branch_val), block)]); - } - - phi.as_basic_value() - } -} - -/// Creates a new stack allocation instruction in the entry block of the function. -pub fn create_entry_block_alloca<'a, 'ctx>( - env: &Env<'a, 'ctx, '_>, - parent: FunctionValue<'_>, - basic_type: BasicTypeEnum<'ctx>, - name: &str, -) -> PointerValue<'ctx> { - let builder = env.context.create_builder(); - let entry = parent.get_first_basic_block().unwrap(); - - match entry.get_first_instruction() { - Some(first_instr) => builder.position_before(&first_instr), - None => builder.position_at_end(entry), - } - - builder.build_alloca(basic_type, name) -} - -fn expose_function_to_host<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - symbol: Symbol, - roc_function: FunctionValue<'ctx>, - arguments: &[Layout<'a>], - return_layout: Layout<'a>, -) { - // Assumption: there is only one specialization of a host-exposed function - let ident_string = symbol.as_str(&env.interns); - let c_function_name: String = format!("roc__{}_1_exposed", ident_string); - - expose_function_to_host_help_c_abi( - env, - ident_string, - roc_function, - arguments, - return_layout, - &c_function_name, - ); -} - -fn expose_function_to_host_help_c_abi_generic<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - roc_function: FunctionValue<'ctx>, - arguments: &[Layout<'a>], - return_layout: Layout<'a>, - c_function_name: &str, -) -> FunctionValue<'ctx> { - // NOTE we ingore env.is_gen_test here - - let mut cc_argument_types = Vec::with_capacity_in(arguments.len(), env.arena); - for layout in arguments { - cc_argument_types.push(to_cc_type(env, layout)); - } - - // STEP 1: turn `f : a,b,c -> d` into `f : a,b,c, &d -> {}` - // let mut argument_types = roc_function.get_type().get_param_types(); - let mut argument_types = cc_argument_types; - - match roc_function.get_type().get_return_type() { - None => { - // this function already returns by-pointer - let output_type = roc_function.get_type().get_param_types().pop().unwrap(); - argument_types.insert(0, output_type); - } - Some(return_type) => { - let output_type = return_type.ptr_type(AddressSpace::Generic); - argument_types.insert(0, output_type.into()); - } - } - // This is not actually a function that returns a value but then became - // return-by-pointer do to the calling convention. Instead, here we - // explicitly are forcing the passing of values via the first parameter - // pointer, since they are generic and hence opaque to anything outside roc. - let c_function_spec = FunctionSpec::cconv(env, CCReturn::Void, None, &argument_types); - - let c_function = add_func( - env.context, - env.module, - c_function_name, - c_function_spec, - Linkage::External, - ); - - let subprogram = env.new_subprogram(c_function_name); - c_function.set_subprogram(subprogram); - - // STEP 2: build the exposed function's body - let builder = env.builder; - let context = env.context; - - let entry = context.append_basic_block(c_function, "entry"); - - builder.position_at_end(entry); - - debug_info_init!(env, c_function); - - // drop the first argument, which is the pointer we write the result into - let args_vector = c_function.get_params(); - let mut args = args_vector.as_slice(); - - // drop the output parameter - args = &args[1..]; - - let mut arguments_for_call = Vec::with_capacity_in(args.len(), env.arena); - - let it = args.iter().zip(roc_function.get_type().get_param_types()); - for (arg, fastcc_type) in it { - let arg_type = arg.get_type(); - if arg_type == fastcc_type { - // the C and Fast calling conventions agree - arguments_for_call.push(*arg); - } else { - // not pretty, but seems to cover all our current cases - if arg_type.is_pointer_type() && !fastcc_type.is_pointer_type() { - // bitcast the ptr - let fastcc_ptr = env - .builder - .build_bitcast( - *arg, - fastcc_type.ptr_type(AddressSpace::Generic), - "bitcast_arg", - ) - .into_pointer_value(); - - let loaded = env.builder.build_load(fastcc_ptr, "load_arg"); - arguments_for_call.push(loaded); - } else { - let as_cc_type = env.builder.build_pointer_cast( - arg.into_pointer_value(), - fastcc_type.into_pointer_type(), - "to_cc_type_ptr", - ); - arguments_for_call.push(as_cc_type.into()); - } - } - } - - let arguments_for_call = &arguments_for_call.into_bump_slice(); - - let call_result = { - if env.is_gen_test { - debug_assert_eq!(args.len(), roc_function.get_params().len()); - - let roc_wrapper_function = make_exception_catcher(env, roc_function, return_layout); - debug_assert_eq!( - arguments_for_call.len(), - roc_wrapper_function.get_params().len() - ); - - builder.position_at_end(entry); - - let wrapped_layout = roc_result_layout(env.arena, return_layout, env.target_info); - call_roc_function(env, roc_function, &wrapped_layout, arguments_for_call) - } else { - call_roc_function(env, roc_function, &return_layout, arguments_for_call) - } - }; - - let output_arg_index = 0; - - let output_arg = c_function - .get_nth_param(output_arg_index as u32) - .unwrap() - .into_pointer_value(); - - store_roc_value(env, return_layout, output_arg, call_result); - builder.build_return(None); - - c_function -} - -fn expose_function_to_host_help_c_abi_gen_test<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - ident_string: &str, - roc_function: FunctionValue<'ctx>, - arguments: &[Layout<'a>], - return_layout: Layout<'a>, - c_function_name: &str, -) -> FunctionValue<'ctx> { - // a tagged union to indicate to the test loader that a panic occurred. - // especially when running 32-bit binaries on a 64-bit machine, there - // does not seem to be a smarter solution - let wrapper_return_type = roc_result_type(env, basic_type_from_layout(env, &return_layout)); - - let mut cc_argument_types = Vec::with_capacity_in(arguments.len(), env.arena); - for layout in arguments { - cc_argument_types.push(to_cc_type(env, layout)); - } - - // STEP 1: turn `f : a,b,c -> d` into `f : a,b,c, &d -> {}` if the C abi demands it - let mut argument_types = cc_argument_types; - let return_type = wrapper_return_type; - - let c_function_spec = { - let output_type = return_type.ptr_type(AddressSpace::Generic); - argument_types.push(output_type.into()); - FunctionSpec::cconv(env, CCReturn::Void, None, &argument_types) - }; - - let c_function = add_func( - env.context, - env.module, - c_function_name, - c_function_spec, - Linkage::External, - ); - - let subprogram = env.new_subprogram(c_function_name); - c_function.set_subprogram(subprogram); - - // STEP 2: build the exposed function's body - let builder = env.builder; - let context = env.context; - - let entry = context.append_basic_block(c_function, "entry"); - - builder.position_at_end(entry); - - debug_info_init!(env, c_function); - - // drop the final argument, which is the pointer we write the result into - let args_vector = c_function.get_params(); - let mut args = args_vector.as_slice(); - let args_length = args.len(); - - args = &args[..args.len() - 1]; - - let mut arguments_for_call = Vec::with_capacity_in(args.len(), env.arena); - - let it = args.iter().zip(roc_function.get_type().get_param_types()); - for (arg, fastcc_type) in it { - let arg_type = arg.get_type(); - if arg_type == fastcc_type { - // the C and Fast calling conventions agree - arguments_for_call.push(*arg); - } else { - let cast = complex_bitcast_check_size(env, *arg, fastcc_type, "to_fastcc_type"); - arguments_for_call.push(cast); - } - } - - let arguments_for_call = &arguments_for_call.into_bump_slice(); - - let call_result = { - let last_block = builder.get_insert_block().unwrap(); - - let roc_wrapper_function = make_exception_catcher(env, roc_function, return_layout); - - builder.position_at_end(last_block); - - call_roc_function( - env, - roc_wrapper_function, - &Layout::struct_no_name_order(&[Layout::u64(), return_layout]), - arguments_for_call, - ) - }; - - let output_arg_index = args_length - 1; - - let output_arg = c_function - .get_nth_param(output_arg_index as u32) - .unwrap() - .into_pointer_value(); - - builder.build_store(output_arg, call_result); - builder.build_return(None); - - // STEP 3: build a {} -> u64 function that gives the size of the return type - let size_function_spec = FunctionSpec::cconv( - env, - CCReturn::Return, - Some(env.context.i64_type().as_basic_type_enum()), - &[], - ); - let size_function_name: String = format!("roc__{}_size", ident_string); - - let size_function = add_func( - env.context, - env.module, - size_function_name.as_str(), - size_function_spec, - Linkage::External, - ); - - let subprogram = env.new_subprogram(&size_function_name); - size_function.set_subprogram(subprogram); - - let entry = context.append_basic_block(size_function, "entry"); - - builder.position_at_end(entry); - - debug_info_init!(env, size_function); - - let size: BasicValueEnum = return_type.size_of().unwrap().into(); - builder.build_return(Some(&size)); - - c_function -} - -fn expose_function_to_host_help_c_abi_v2<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - roc_function: FunctionValue<'ctx>, - arguments: &[Layout<'a>], - return_layout: Layout<'a>, - c_function_name: &str, -) -> FunctionValue<'ctx> { - let it = arguments.iter().map(|l| basic_type_from_layout(env, l)); - let argument_types = Vec::from_iter_in(it, env.arena); - let return_type = basic_type_from_layout(env, &return_layout); - - let cc_return = to_cc_return(env, &return_layout); - let roc_return = RocReturn::from_layout(env, &return_layout); - - let c_function_spec = FunctionSpec::cconv(env, cc_return, Some(return_type), &argument_types); - - let c_function = add_func( - env.context, - env.module, - c_function_name, - c_function_spec, - Linkage::External, - ); - - let subprogram = env.new_subprogram(c_function_name); - c_function.set_subprogram(subprogram); - - // STEP 2: build the exposed function's body - let builder = env.builder; - let context = env.context; - - let entry = context.append_basic_block(c_function, "entry"); - builder.position_at_end(entry); - - let params = c_function.get_params(); - - let param_types = Vec::from_iter_in(roc_function.get_type().get_param_types(), env.arena); - - let (params, param_types) = match (&roc_return, &cc_return) { - // Drop the "return pointer" if it exists on the roc function - // and the c function does not return via pointer - (RocReturn::ByPointer, CCReturn::Return) => (¶ms[..], ¶m_types[1..]), - // Drop the return pointer the other way, if the C function returns by pointer but Roc - // doesn't - (RocReturn::Return, CCReturn::ByPointer) => (¶ms[1..], ¶m_types[..]), - _ => (¶ms[..], ¶m_types[..]), - }; - - debug_assert!( - params.len() == param_types.len(), - "when exposing a function to the host, params.len() was {}, but param_types.len() was {}", - params.len(), - param_types.len() - ); - - let it = params.iter().zip(param_types).map(|(arg, fastcc_type)| { - let arg_type = arg.get_type(); - if arg_type == *fastcc_type { - // the C and Fast calling conventions agree - *arg - } else { - complex_bitcast_check_size(env, *arg, *fastcc_type, "to_fastcc_type") - } - }); - - let arguments = Vec::from_iter_in(it, env.arena); - - let value = call_roc_function(env, roc_function, &return_layout, arguments.as_slice()); - - match cc_return { - CCReturn::Return => match roc_return { - RocReturn::Return => { - env.builder.build_return(Some(&value)); - } - RocReturn::ByPointer => { - let loaded = env - .builder - .build_load(value.into_pointer_value(), "load_result"); - env.builder.build_return(Some(&loaded)); - } - }, - CCReturn::ByPointer => { - let out_ptr = c_function.get_nth_param(0).unwrap().into_pointer_value(); - - env.builder.build_store(out_ptr, value); - env.builder.build_return(None); - } - CCReturn::Void => { - env.builder.build_return(None); - } - } - - c_function -} - -fn expose_function_to_host_help_c_abi<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - ident_string: &str, - roc_function: FunctionValue<'ctx>, - arguments: &[Layout<'a>], - return_layout: Layout<'a>, - c_function_name: &str, -) -> FunctionValue<'ctx> { - if env.is_gen_test { - return expose_function_to_host_help_c_abi_gen_test( - env, - ident_string, - roc_function, - arguments, - return_layout, - c_function_name, - ); - } - - // a generic version that writes the result into a passed *u8 pointer - expose_function_to_host_help_c_abi_generic( - env, - roc_function, - arguments, - return_layout, - &format!("{}_generic", c_function_name), - ); - - let c_function = expose_function_to_host_help_c_abi_v2( - env, - roc_function, - arguments, - return_layout, - c_function_name, - ); - - // STEP 3: build a {} -> u64 function that gives the size of the return type - let size_function_spec = FunctionSpec::cconv( - env, - CCReturn::Return, - Some(env.context.i64_type().as_basic_type_enum()), - &[], - ); - let size_function_name: String = format!("roc__{}_size", ident_string); - - let size_function = add_func( - env.context, - env.module, - size_function_name.as_str(), - size_function_spec, - Linkage::External, - ); - - let subprogram = env.new_subprogram(&size_function_name); - size_function.set_subprogram(subprogram); - - let entry = env.context.append_basic_block(size_function, "entry"); - - env.builder.position_at_end(entry); - - debug_info_init!(env, size_function); - - let return_type = if env.is_gen_test { - roc_result_type(env, roc_function.get_type().get_return_type().unwrap()).into() - } else { - // roc_function.get_type().get_return_type().unwrap() - basic_type_from_layout(env, &return_layout) - }; - - let size: BasicValueEnum = return_type.size_of().unwrap().into(); - env.builder.build_return(Some(&size)); - - c_function -} - -pub fn get_sjlj_buffer<'a, 'ctx, 'env>(env: &Env<'a, 'ctx, 'env>) -> PointerValue<'ctx> { - // The size of jump_buf is platform-dependent. - // - AArch64 needs 3 machine-sized words - // - LLVM says the following about the SJLJ intrinsic: - // - // [It is] a five word buffer in which the calling context is saved. - // The front end places the frame pointer in the first word, and the - // target implementation of this intrinsic should place the destination - // address for a llvm.eh.sjlj.longjmp in the second word. - // The following three words are available for use in a target-specific manner. - // - // So, let's create a 5-word buffer. - let word_type = match env.target_info.ptr_width() { - PtrWidth::Bytes4 => env.context.i32_type(), - PtrWidth::Bytes8 => env.context.i64_type(), - }; - let type_ = word_type.array_type(5); - - let global = match env.module.get_global("roc_sjlj_buffer") { - Some(global) => global, - None => env.module.add_global(type_, None, "roc_sjlj_buffer"), - }; - - global.set_initializer(&type_.const_zero()); - - env.builder - .build_bitcast( - global.as_pointer_value(), - env.context.i32_type().ptr_type(AddressSpace::Generic), - "cast_sjlj_buffer", - ) - .into_pointer_value() -} - -pub fn build_setjmp_call<'a, 'ctx, 'env>(env: &Env<'a, 'ctx, 'env>) -> BasicValueEnum<'ctx> { - let jmp_buf = get_sjlj_buffer(env); - if cfg!(target_arch = "aarch64") { - // Due to https://github.com/rtfeldman/roc/issues/2965, we use a setjmp we linked in from Zig - call_bitcode_fn(env, &[jmp_buf.into()], bitcode::UTILS_SETJMP) - } else { - // Anywhere else, use the LLVM intrinsic. - // https://llvm.org/docs/ExceptionHandling.html#llvm-eh-sjlj-setjmp - - let jmp_buf_i8p_arr = env - .builder - .build_bitcast( - jmp_buf, - env.context - .i8_type() - .ptr_type(AddressSpace::Generic) - .array_type(5) - .ptr_type(AddressSpace::Generic), - "jmp_buf [5 x i8*]", - ) - .into_pointer_value(); - - // LLVM asks us to please store the frame pointer in the first word. - let frame_address = env.call_intrinsic( - LLVM_FRAME_ADDRESS, - &[env.context.i32_type().const_zero().into()], - ); - - let zero = env.context.i32_type().const_zero(); - let fa_index = env.context.i32_type().const_zero(); - let fa = unsafe { - env.builder.build_in_bounds_gep( - jmp_buf_i8p_arr, - &[zero, fa_index], - "frame address index", - ) - }; - env.builder.build_store(fa, frame_address); - - // LLVM says that the target implementation of the setjmp intrinsic will put the - // destination address at index 1, and that the remaining three words are for ad-hoc target - // usage. But for whatever reason, on x86, it appears we need a stacksave in those words. - let ss_index = env.context.i32_type().const_int(2, false); - let ss = unsafe { - env.builder - .build_in_bounds_gep(jmp_buf_i8p_arr, &[zero, ss_index], "name") - }; - let stack_save = env.call_intrinsic(LLVM_STACK_SAVE, &[]); - env.builder.build_store(ss, stack_save); - - let jmp_buf_i8p = env.builder.build_bitcast( - jmp_buf, - env.context.i8_type().ptr_type(AddressSpace::Generic), - "jmp_buf i8*", - ); - env.call_intrinsic(LLVM_SETJMP, &[jmp_buf_i8p]) - } -} - -/// Pointer to pointer of the panic message. -pub fn get_panic_msg_ptr<'a, 'ctx, 'env>(env: &Env<'a, 'ctx, 'env>) -> PointerValue<'ctx> { - let ptr_to_u8_ptr = env.context.i8_type().ptr_type(AddressSpace::Generic); - - let global_name = "roc_panic_msg_ptr"; - let global = env.module.get_global(global_name).unwrap_or_else(|| { - let global = env.module.add_global(ptr_to_u8_ptr, None, global_name); - global.set_initializer(&ptr_to_u8_ptr.const_zero()); - global - }); - - global.as_pointer_value() -} - -fn set_jump_and_catch_long_jump<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - parent: FunctionValue<'ctx>, - roc_function: FunctionValue<'ctx>, - arguments: &[BasicValueEnum<'ctx>], - return_layout: Layout<'a>, -) -> BasicValueEnum<'ctx> { - let context = env.context; - let builder = env.builder; - - let return_type = basic_type_from_layout(env, &return_layout); - let call_result_type = roc_result_type(env, return_type.as_basic_type_enum()); - let result_alloca = builder.build_alloca(call_result_type, "result"); - - let then_block = context.append_basic_block(parent, "then_block"); - let catch_block = context.append_basic_block(parent, "catch_block"); - let cont_block = context.append_basic_block(parent, "cont_block"); - - let panicked_u32 = build_setjmp_call(env); - let panicked_bool = env.builder.build_int_compare( - IntPredicate::NE, - panicked_u32.into_int_value(), - panicked_u32.get_type().into_int_type().const_zero(), - "to_bool", - ); - - env.builder - .build_conditional_branch(panicked_bool, catch_block, then_block); - - // all went well - { - builder.position_at_end(then_block); - - let call_result = call_roc_function(env, roc_function, &return_layout, arguments); - - let return_value = make_good_roc_result(env, return_layout, call_result); - - builder.build_store(result_alloca, return_value); - - env.builder.build_unconditional_branch(cont_block); - } - - // something went wrong - { - builder.position_at_end(catch_block); - - let error_msg = { - // u8** - let ptr_int_ptr = get_panic_msg_ptr(env); - - // u8* again - builder.build_load(ptr_int_ptr, "ptr_int") - }; - - let return_value = { - let v1 = call_result_type.const_zero(); - - // flag is non-zero, indicating failure - let flag = context.i64_type().const_int(1, false); - - let v2 = builder - .build_insert_value(v1, flag, 0, "set_error") - .unwrap(); - - let v3 = builder - .build_insert_value(v2, error_msg, 1, "set_exception") - .unwrap(); - v3 - }; - - builder.build_store(result_alloca, return_value); - - env.builder.build_unconditional_branch(cont_block); - } - - env.builder.position_at_end(cont_block); - - builder.build_load(result_alloca, "set_jump_and_catch_long_jump_load_result") -} - -fn make_exception_catcher<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - roc_function: FunctionValue<'ctx>, - return_layout: Layout<'a>, -) -> FunctionValue<'ctx> { - let wrapper_function_name = format!("{}_catcher", roc_function.get_name().to_str().unwrap()); - - let function_value = - make_exception_catching_wrapper(env, roc_function, return_layout, &wrapper_function_name); - - function_value.set_linkage(Linkage::Internal); - - function_value -} - -fn roc_result_layout<'a>( - arena: &'a Bump, - return_layout: Layout<'a>, - target_info: TargetInfo, -) -> Layout<'a> { - let elements = [Layout::u64(), Layout::usize(target_info), return_layout]; - - Layout::struct_no_name_order(arena.alloc(elements)) -} - -fn roc_result_type<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - return_type: BasicTypeEnum<'ctx>, -) -> StructType<'ctx> { - env.context.struct_type( - &[ - env.context.i64_type().into(), - env.context.i8_type().ptr_type(AddressSpace::Generic).into(), - return_type, - ], - false, - ) -} - -fn make_good_roc_result<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - return_layout: Layout<'a>, - return_value: BasicValueEnum<'ctx>, -) -> BasicValueEnum<'ctx> { - let context = env.context; - let builder = env.builder; - - let v1 = roc_result_type(env, basic_type_from_layout(env, &return_layout)).const_zero(); - - let v2 = builder - .build_insert_value(v1, context.i64_type().const_zero(), 0, "set_no_error") - .unwrap(); - - let v3 = if return_layout.is_passed_by_reference(env.target_info) { - let loaded = env.builder.build_load( - return_value.into_pointer_value(), - "load_call_result_passed_by_ptr", - ); - builder - .build_insert_value(v2, loaded, 2, "set_call_result") - .unwrap() - } else { - builder - .build_insert_value(v2, return_value, 2, "set_call_result") - .unwrap() - }; - - v3.into_struct_value().into() -} - -fn make_exception_catching_wrapper<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - roc_function: FunctionValue<'ctx>, - return_layout: Layout<'a>, - wrapper_function_name: &str, -) -> FunctionValue<'ctx> { - // build the C calling convention wrapper - - let context = env.context; - let builder = env.builder; - - let roc_function_type = roc_function.get_type(); - let argument_types = match RocReturn::from_layout(env, &return_layout) { - RocReturn::Return => roc_function_type.get_param_types(), - RocReturn::ByPointer => { - // Our fastcc passes the return pointer as the last parameter. - // Remove the return pointer since we now intend to return the result by value. - let mut types = roc_function_type.get_param_types(); - types.pop(); - - types - } - }; - - let wrapper_return_type = roc_result_type(env, basic_type_from_layout(env, &return_layout)); - - // argument_types.push(wrapper_return_type.ptr_type(AddressSpace::Generic).into()); - - // let wrapper_function_type = env.context.void_type().fn_type(&argument_types, false); - let wrapper_function_spec = FunctionSpec::cconv( - env, - CCReturn::Return, - Some(wrapper_return_type.as_basic_type_enum()), - &argument_types, - ); - - // Add main to the module. - let wrapper_function = add_func( - env.context, - env.module, - wrapper_function_name, - wrapper_function_spec, - Linkage::External, - ); - - let subprogram = env.new_subprogram(wrapper_function_name); - wrapper_function.set_subprogram(subprogram); - - // our exposed main function adheres to the C calling convention - wrapper_function.set_call_conventions(FAST_CALL_CONV); - - // invoke instead of call, so that we can catch any exceptions thrown in Roc code - let arguments = wrapper_function.get_params(); - - let basic_block = context.append_basic_block(wrapper_function, "entry"); - builder.position_at_end(basic_block); - - debug_info_init!(env, wrapper_function); - - let result = set_jump_and_catch_long_jump( - env, - wrapper_function, - roc_function, - &arguments, - return_layout, - ); - - builder.build_return(Some(&result)); - - wrapper_function -} - -pub fn build_proc_headers<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - mod_solutions: &'a ModSolutions, - procedures: MutMap<(Symbol, ProcLayout<'a>), roc_mono::ir::Proc<'a>>, - scope: &mut Scope<'a, 'ctx>, - // alias_analysis_solutions: AliasAnalysisSolutions, -) -> Vec< - 'a, - ( - roc_mono::ir::Proc<'a>, - &'a [(&'a FuncSpecSolutions, FunctionValue<'ctx>)], - ), -> { - // Populate Procs further and get the low-level Expr from the canonical Expr - let mut headers = Vec::with_capacity_in(procedures.len(), env.arena); - for ((symbol, layout), proc) in procedures { - let name_bytes = roc_alias_analysis::func_name_bytes(&proc); - let func_name = FuncName(&name_bytes); - - let func_solutions = mod_solutions.func_solutions(func_name).unwrap(); - - let it = func_solutions.specs(); - let mut function_values = Vec::with_capacity_in(it.size_hint().0, env.arena); - for specialization in it { - let fn_val = build_proc_header(env, *specialization, symbol, &proc); - - if proc.args.is_empty() { - // this is a 0-argument thunk, i.e. a top-level constant definition - // it must be in-scope everywhere in the module! - scope.insert_top_level_thunk(symbol, env.arena.alloc(layout), fn_val); - } - - let func_spec_solutions = func_solutions.spec(specialization).unwrap(); - - function_values.push((func_spec_solutions, fn_val)); - } - headers.push((proc, function_values.into_bump_slice())); - } - - headers -} - -pub fn build_procedures<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - opt_level: OptLevel, - procedures: MutMap<(Symbol, ProcLayout<'a>), roc_mono::ir::Proc<'a>>, - entry_point: EntryPoint<'a>, - debug_output_file: Option<&Path>, -) { - build_procedures_help(env, opt_level, procedures, entry_point, debug_output_file); -} - -pub fn build_procedures_return_main<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - opt_level: OptLevel, - procedures: MutMap<(Symbol, ProcLayout<'a>), roc_mono::ir::Proc<'a>>, - entry_point: EntryPoint<'a>, -) -> (&'static str, FunctionValue<'ctx>) { - let mod_solutions = build_procedures_help( - env, - opt_level, - procedures, - entry_point, - Some(Path::new("/tmp/test.ll")), - ); - - promote_to_main_function(env, mod_solutions, entry_point.symbol, entry_point.layout) -} - -fn build_procedures_help<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - opt_level: OptLevel, - procedures: MutMap<(Symbol, ProcLayout<'a>), roc_mono::ir::Proc<'a>>, - entry_point: EntryPoint<'a>, - debug_output_file: Option<&Path>, -) -> &'a ModSolutions { - let mut layout_ids = roc_mono::layout::LayoutIds::default(); - let mut scope = Scope::default(); - - let it = procedures.iter().map(|x| x.1); - - let solutions = match roc_alias_analysis::spec_program(opt_level, entry_point, it) { - Err(e) => panic!("Error in alias analysis: {}", e), - Ok(solutions) => solutions, - }; - - let solutions = env.arena.alloc(solutions); - - let mod_solutions = solutions - .mod_solutions(roc_alias_analysis::MOD_APP) - .unwrap(); - - // Add all the Proc headers to the module. - // We have to do this in a separate pass first, - // because their bodies may reference each other. - let headers = build_proc_headers(env, mod_solutions, procedures, &mut scope); - - let (_, function_pass) = construct_optimization_passes(env.module, opt_level); - - for (proc, fn_vals) in headers { - for (func_spec_solutions, fn_val) in fn_vals { - let mut current_scope = scope.clone(); - - // only have top-level thunks for this proc's module in scope - // this retain is not needed for correctness, but will cause less confusion when debugging - let home = proc.name.module_id(); - current_scope.retain_top_level_thunks_for_module(home); - - build_proc( - env, - mod_solutions, - &mut layout_ids, - func_spec_solutions, - scope.clone(), - &proc, - *fn_val, - ); - - // call finalize() before any code generation/verification - env.dibuilder.finalize(); - - if fn_val.verify(true) { - function_pass.run_on(fn_val); - } else { - let mode = "NON-OPTIMIZED"; - - eprintln!( - "\n\nFunction {:?} failed LLVM verification in {} build. Its content was:\n", - fn_val.get_name().to_str().unwrap(), - mode, - ); - - fn_val.print_to_stderr(); - - if let Some(app_ll_file) = debug_output_file { - env.module.print_to_file(&app_ll_file).unwrap(); - - panic!( - r"😱 LLVM errors when defining function {:?}; I wrote the full LLVM IR to {:?}", - fn_val.get_name().to_str().unwrap(), - app_ll_file, - ); - } else { - env.module.print_to_stderr(); - - panic!( - "The preceding code was from {:?}, which failed LLVM verification in {} build.", - fn_val.get_name().to_str().unwrap(), - mode, - ) - } - } - } - } - - mod_solutions -} - -fn func_spec_name<'a>( - arena: &'a Bump, - interns: &Interns, - symbol: Symbol, - func_spec: FuncSpec, -) -> bumpalo::collections::String<'a> { - use std::fmt::Write; - - let mut buf = bumpalo::collections::String::with_capacity_in(1, arena); - - let ident_string = symbol.as_str(interns); - let module_string = interns.module_ids.get_name(symbol.module_id()).unwrap(); - write!(buf, "{}_{}_", module_string, ident_string).unwrap(); - - for byte in func_spec.0.iter() { - write!(buf, "{:x?}", byte).unwrap(); - } - - buf -} - -fn build_proc_header<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - func_spec: FuncSpec, - symbol: Symbol, - proc: &roc_mono::ir::Proc<'a>, -) -> FunctionValue<'ctx> { - let args = proc.args; - let arena = env.arena; - - let fn_name = func_spec_name(env.arena, &env.interns, symbol, func_spec); - - let ret_type = basic_type_from_layout(env, &proc.ret_layout); - let mut arg_basic_types = Vec::with_capacity_in(args.len(), arena); - - for (layout, _) in args.iter() { - let arg_type = argument_type_from_layout(env, layout); - - arg_basic_types.push(arg_type); - } - - let roc_return = RocReturn::from_layout(env, &proc.ret_layout); - let fn_spec = FunctionSpec::fastcc(env, roc_return, ret_type, arg_basic_types); - - let fn_val = add_func( - env.context, - env.module, - fn_name.as_str(), - fn_spec, - Linkage::Internal, - ); - - let subprogram = env.new_subprogram(&fn_name); - fn_val.set_subprogram(subprogram); - - if env.exposed_to_host.contains(&symbol) { - let arguments = Vec::from_iter_in(proc.args.iter().map(|(layout, _)| *layout), env.arena); - expose_function_to_host( - env, - symbol, - fn_val, - arguments.into_bump_slice(), - proc.ret_layout, - ); - } - - if false { - let kind_id = Attribute::get_named_enum_kind_id("alwaysinline"); - debug_assert!(kind_id > 0); - let enum_attr = env.context.create_enum_attribute(kind_id, 1); - fn_val.add_attribute(AttributeLoc::Function, enum_attr); - } - - if false { - let kind_id = Attribute::get_named_enum_kind_id("noinline"); - debug_assert!(kind_id > 0); - let enum_attr = env.context.create_enum_attribute(kind_id, 1); - fn_val.add_attribute(AttributeLoc::Function, enum_attr); - } - - fn_val -} - -#[allow(clippy::too_many_arguments)] -pub fn build_closure_caller<'a, 'ctx, 'env>( - env: &'a Env<'a, 'ctx, 'env>, - def_name: &str, - evaluator: FunctionValue<'ctx>, - alias_symbol: Symbol, - arguments: &[Layout<'a>], - return_layout: &Layout<'a>, - lambda_set: LambdaSet<'a>, - result: &Layout<'a>, -) { - let mut argument_types = Vec::with_capacity_in(arguments.len() + 3, env.arena); - - for layout in arguments { - let arg_type = basic_type_from_layout(env, layout); - let arg_ptr_type = arg_type.ptr_type(AddressSpace::Generic); - - argument_types.push(arg_ptr_type.into()); - } - - let closure_argument_type = { - let basic_type = basic_type_from_layout(env, &lambda_set.runtime_representation()); - - basic_type.ptr_type(AddressSpace::Generic) - }; - argument_types.push(closure_argument_type.into()); - - let context = &env.context; - let builder = env.builder; - - let result_type = basic_type_from_layout(env, result); - - let output_type = { result_type.ptr_type(AddressSpace::Generic) }; - argument_types.push(output_type.into()); - - // STEP 1: build function header - - // e.g. `roc__main_1_Fx_caller` - let function_name = format!( - "roc__{}_{}_caller", - def_name, - alias_symbol.as_str(&env.interns) - ); - - let function_spec = FunctionSpec::cconv(env, CCReturn::Void, None, &argument_types); - - let function_value = add_func( - env.context, - env.module, - function_name.as_str(), - function_spec, - Linkage::External, - ); - - // STEP 2: build function body - - let entry = context.append_basic_block(function_value, "entry"); - - builder.position_at_end(entry); - - let mut evaluator_arguments = function_value.get_params(); - - // the final parameter is the output pointer, pop it - let output = evaluator_arguments.pop().unwrap().into_pointer_value(); - - // NOTE this may be incorrect in the long run - // here we load any argument that is a pointer - let closure_layout = lambda_set.runtime_representation(); - let layouts_it = arguments.iter().chain(std::iter::once(&closure_layout)); - for (param, layout) in evaluator_arguments.iter_mut().zip(layouts_it) { - if param.is_pointer_value() && !layout.is_passed_by_reference(env.target_info) { - *param = builder.build_load(param.into_pointer_value(), "load_param"); - } - } - - if env.is_gen_test { - let call_result = set_jump_and_catch_long_jump( - env, - function_value, - evaluator, - &evaluator_arguments, - *return_layout, - ); - - builder.build_store(output, call_result); - } else { - let call_result = call_roc_function(env, evaluator, return_layout, &evaluator_arguments); - - if return_layout.is_passed_by_reference(env.target_info) { - let align_bytes = return_layout.alignment_bytes(env.target_info); - - if align_bytes > 0 { - let size = env - .ptr_int() - .const_int(return_layout.stack_size(env.target_info) as u64, false); - - env.builder - .build_memcpy( - output, - align_bytes, - call_result.into_pointer_value(), - align_bytes, - size, - ) - .unwrap(); - } - } else { - builder.build_store(output, call_result); - } - }; - - builder.build_return(None); - - // STEP 3: build a {} -> u64 function that gives the size of the return type - build_host_exposed_alias_size_help(env, def_name, alias_symbol, Some("result"), result_type); - - // STEP 4: build a {} -> u64 function that gives the size of the closure - build_host_exposed_alias_size( - env, - def_name, - alias_symbol, - lambda_set.runtime_representation(), - ); -} - -fn build_host_exposed_alias_size<'a, 'ctx, 'env>( - env: &'a Env<'a, 'ctx, 'env>, - def_name: &str, - alias_symbol: Symbol, - layout: Layout<'a>, -) { - build_host_exposed_alias_size_help( - env, - def_name, - alias_symbol, - None, - basic_type_from_layout(env, &layout), - ) -} - -fn build_host_exposed_alias_size_help<'a, 'ctx, 'env>( - env: &'a Env<'a, 'ctx, 'env>, - def_name: &str, - alias_symbol: Symbol, - opt_label: Option<&str>, - basic_type: BasicTypeEnum<'ctx>, -) { - let builder = env.builder; - let context = env.context; - - let i64 = env.context.i64_type().as_basic_type_enum(); - let size_function_spec = FunctionSpec::cconv(env, CCReturn::Return, Some(i64), &[]); - let size_function_name: String = if let Some(label) = opt_label { - format!( - "roc__{}_{}_{}_size", - def_name, - alias_symbol.as_str(&env.interns), - label - ) - } else { - format!( - "roc__{}_{}_size", - def_name, - alias_symbol.as_str(&env.interns) - ) - }; - - let size_function = add_func( - env.context, - env.module, - size_function_name.as_str(), - size_function_spec, - Linkage::External, - ); - - let entry = context.append_basic_block(size_function, "entry"); - - builder.position_at_end(entry); - - let size: BasicValueEnum = basic_type.size_of().unwrap().into(); - builder.build_return(Some(&size)); -} - -pub fn build_proc<'a, 'ctx, 'env>( - env: &'a Env<'a, 'ctx, 'env>, - mod_solutions: &'a ModSolutions, - layout_ids: &mut LayoutIds<'a>, - func_spec_solutions: &FuncSpecSolutions, - mut scope: Scope<'a, 'ctx>, - proc: &roc_mono::ir::Proc<'a>, - fn_val: FunctionValue<'ctx>, -) { - use roc_mono::ir::HostExposedLayouts; - use roc_mono::layout::RawFunctionLayout; - let copy = proc.host_exposed_layouts.clone(); - match copy { - HostExposedLayouts::NotHostExposed => {} - HostExposedLayouts::HostExposed { rigids: _, aliases } => { - for (name, (symbol, top_level, layout)) in aliases { - match layout { - RawFunctionLayout::Function(arguments, closure, result) => { - // define closure size and return value size, e.g. - // - // * roc__mainForHost_1_Update_size() -> i64 - // * roc__mainForHost_1_Update_result_size() -> i64 - - let it = top_level.arguments.iter().copied(); - let bytes = - roc_alias_analysis::func_name_bytes_help(symbol, it, &top_level.result); - let func_name = FuncName(&bytes); - let func_solutions = mod_solutions.func_solutions(func_name).unwrap(); - - let mut it = func_solutions.specs(); - let evaluator = match it.next() { - Some(func_spec) => { - debug_assert!( - it.next().is_none(), - "we expect only one specialization of this symbol" - ); - - function_value_by_func_spec( - env, - *func_spec, - symbol, - top_level.arguments, - &top_level.result, - ) - } - None => { - // morphic did not generate a specialization for this function, - // therefore it must actually be unused. - // An example is our closure callers - panic!("morphic did not specialize {:?}", symbol); - } - }; - - let ident_string = proc.name.as_str(&env.interns); - let fn_name: String = format!("{}_1", ident_string); - - build_closure_caller( - env, &fn_name, evaluator, name, arguments, result, closure, result, - ) - } - - RawFunctionLayout::ZeroArgumentThunk(_) => { - // do nothing - } - } - } - } - } - - let args = proc.args; - let context = &env.context; - - // Add a basic block for the entry point - let entry = context.append_basic_block(fn_val, "entry"); - let builder = env.builder; - - builder.position_at_end(entry); - - debug_info_init!(env, fn_val); - - // Add args to scope - for (arg_val, (layout, arg_symbol)) in fn_val.get_param_iter().zip(args) { - arg_val.set_name(arg_symbol.as_str(&env.interns)); - scope.insert(*arg_symbol, (*layout, arg_val)); - } - - let body = build_exp_stmt( - env, - layout_ids, - func_spec_solutions, - &mut scope, - fn_val, - &proc.body, - ); - - // only add a return if codegen did not already add one - if let Some(block) = builder.get_insert_block() { - if block.get_terminator().is_none() { - builder.build_return(Some(&body)); - } - } -} - -pub fn verify_fn(fn_val: FunctionValue<'_>) { - if !fn_val.verify(print_fn_verification_output()) { - unsafe { - fn_val.delete(); - } - - panic!("Invalid generated fn_val.") - } -} - -fn function_value_by_func_spec<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - func_spec: FuncSpec, - symbol: Symbol, - arguments: &[Layout<'a>], - result: &Layout<'a>, -) -> FunctionValue<'ctx> { - let fn_name = func_spec_name(env.arena, &env.interns, symbol, func_spec); - let fn_name = fn_name.as_str(); - - function_value_by_name_help(env, arguments, result, symbol, fn_name) -} - -fn function_value_by_name_help<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - arguments: &[Layout<'a>], - result: &Layout<'a>, - symbol: Symbol, - fn_name: &str, -) -> FunctionValue<'ctx> { - env.module.get_function(fn_name).unwrap_or_else(|| { - if symbol.is_builtin() { - eprintln!( - "Unrecognized builtin function: {:?}\nLayout: {:?}\n", - fn_name, - (arguments, result) - ); - eprintln!("Is the function defined? If so, maybe there is a problem with the layout"); - - panic!( - "Unrecognized builtin function: {:?} (symbol: {:?})", - fn_name, symbol, - ) - } else { - // Unrecognized non-builtin function: - eprintln!( - "Unrecognized non-builtin function: {:?}\n\nSymbol: {:?}\nLayout: {:?}\n", - fn_name, - symbol, - (arguments, result) - ); - eprintln!("Is the function defined? If so, maybe there is a problem with the layout"); - - panic!( - "Unrecognized non-builtin function: {:?} (symbol: {:?})", - fn_name, symbol, - ) - } - }) -} - -#[inline(always)] -fn roc_call_with_args<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - argument_layouts: &[Layout<'a>], - result_layout: &Layout<'a>, - symbol: Symbol, - func_spec: FuncSpec, - arguments: &[BasicValueEnum<'ctx>], -) -> BasicValueEnum<'ctx> { - let fn_val = - function_value_by_func_spec(env, func_spec, symbol, argument_layouts, result_layout); - - call_roc_function(env, fn_val, result_layout, arguments) -} - -pub fn call_roc_function<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - roc_function: FunctionValue<'ctx>, - result_layout: &Layout<'a>, - arguments: &[BasicValueEnum<'ctx>], -) -> BasicValueEnum<'ctx> { - let pass_by_pointer = roc_function.get_type().get_param_types().len() == arguments.len() + 1; - - match RocReturn::from_layout(env, result_layout) { - RocReturn::ByPointer if !pass_by_pointer => { - // WARNING this is a hack!! - let it = arguments.iter().map(|x| (*x).into()); - let mut arguments = Vec::from_iter_in(it, env.arena); - arguments.pop(); - - let result_type = basic_type_from_layout(env, result_layout); - let result_alloca = env.builder.build_alloca(result_type, "result_value"); - - arguments.push(result_alloca.into()); - - debug_assert_eq!( - roc_function.get_type().get_param_types().len(), - arguments.len() - ); - let call = env.builder.build_call(roc_function, &arguments, "call"); - - // roc functions should have the fast calling convention - debug_assert_eq!(roc_function.get_call_conventions(), FAST_CALL_CONV); - call.set_call_convention(FAST_CALL_CONV); - - env.builder.build_load(result_alloca, "load_result") - } - RocReturn::ByPointer => { - let it = arguments.iter().map(|x| (*x).into()); - let mut arguments = Vec::from_iter_in(it, env.arena); - - let result_type = basic_type_from_layout(env, result_layout); - let result_alloca = entry_block_alloca_zerofill(env, result_type, "result_value"); - - arguments.push(result_alloca.into()); - - debug_assert_eq!( - roc_function.get_type().get_param_types().len(), - arguments.len() - ); - let call = env.builder.build_call(roc_function, &arguments, "call"); - - // roc functions should have the fast calling convention - debug_assert_eq!(roc_function.get_call_conventions(), FAST_CALL_CONV); - call.set_call_convention(FAST_CALL_CONV); - - if result_layout.is_passed_by_reference(env.target_info) { - result_alloca.into() - } else { - env.builder - .build_load(result_alloca, "return_by_pointer_load_result") - } - } - RocReturn::Return => { - debug_assert_eq!( - roc_function.get_type().get_param_types().len(), - arguments.len() - ); - let it = arguments.iter().map(|x| (*x).into()); - let arguments = Vec::from_iter_in(it, env.arena); - - let call = env.builder.build_call(roc_function, &arguments, "call"); - - // roc functions should have the fast calling convention - debug_assert_eq!(roc_function.get_call_conventions(), FAST_CALL_CONV); - call.set_call_convention(FAST_CALL_CONV); - - call.try_as_basic_value().left().unwrap_or_else(|| { - panic!( - "LLVM error: Invalid call by name for name {:?}", - roc_function.get_name() - ) - }) - } - } -} - -/// Translates a target_lexicon::Triple to a LLVM calling convention u32 -/// as described in https://llvm.org/doxygen/namespacellvm_1_1CallingConv.html -pub fn get_call_conventions(cc: target_lexicon::CallingConvention) -> u32 { - use target_lexicon::CallingConvention::*; - - // For now, we're returning 0 for the C calling convention on all of these. - // Not sure if we should be picking something more specific! - match cc { - SystemV => C_CALL_CONV, - WasmBasicCAbi => C_CALL_CONV, - WindowsFastcall => C_CALL_CONV, - AppleAarch64 => C_CALL_CONV, - _ => C_CALL_CONV, - } -} - -/// Source: https://llvm.org/doxygen/namespacellvm_1_1CallingConv.html -pub const C_CALL_CONV: u32 = 0; -pub const FAST_CALL_CONV: u32 = 8; -pub const COLD_CALL_CONV: u32 = 9; - -pub struct RocFunctionCall<'ctx> { - pub caller: PointerValue<'ctx>, - pub data: PointerValue<'ctx>, - pub inc_n_data: PointerValue<'ctx>, - pub data_is_owned: IntValue<'ctx>, -} - -#[allow(clippy::too_many_arguments)] -fn roc_function_call<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - layout_ids: &mut LayoutIds<'a>, - transform: FunctionValue<'ctx>, - closure_data: BasicValueEnum<'ctx>, - lambda_set: LambdaSet<'a>, - closure_data_is_owned: bool, - argument_layouts: &[Layout<'a>], - result_layout: Layout<'a>, -) -> RocFunctionCall<'ctx> { - use crate::llvm::bitcode::{build_inc_n_wrapper, build_transform_caller}; - - let closure_data_ptr = env - .builder - .build_alloca(closure_data.get_type(), "closure_data_ptr"); - env.builder.build_store(closure_data_ptr, closure_data); - - let stepper_caller = - build_transform_caller(env, transform, lambda_set, argument_layouts, result_layout) - .as_global_value() - .as_pointer_value(); - - let inc_closure_data = - build_inc_n_wrapper(env, layout_ids, &lambda_set.runtime_representation()) - .as_global_value() - .as_pointer_value(); - - let closure_data_is_owned = env - .context - .bool_type() - .const_int(closure_data_is_owned as u64, false); - - RocFunctionCall { - caller: stepper_caller, - inc_n_data: inc_closure_data, - data_is_owned: closure_data_is_owned, - data: closure_data_ptr, - } -} - -#[allow(clippy::too_many_arguments)] -fn run_higher_order_low_level<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - layout_ids: &mut LayoutIds<'a>, - scope: &Scope<'a, 'ctx>, - return_layout: &Layout<'a>, - func_spec: FuncSpec, - higher_order: &HigherOrderLowLevel<'a>, -) -> BasicValueEnum<'ctx> { - use roc_mono::ir::PassedFunction; - use roc_mono::low_level::HigherOrder::*; - - let HigherOrderLowLevel { - op, - passed_function, - .. - } = higher_order; - - let PassedFunction { - argument_layouts, - return_layout: result_layout, - owns_captured_environment: function_owns_closure_data, - name: function_name, - captured_environment, - .. - } = *passed_function; - - // macros because functions cause lifetime issues related to the `env` or `layout_ids` - macro_rules! function_details { - () => {{ - let function = function_value_by_func_spec( - env, - func_spec, - function_name, - argument_layouts, - return_layout, - ); - - let (closure, closure_layout) = - load_symbol_and_lambda_set(scope, &captured_environment); - - (function, closure, closure_layout) - }}; - } - - macro_rules! list_walk { - ($variant:expr, $xs:expr, $state:expr) => {{ - let (list, list_layout) = load_symbol_and_layout(scope, &$xs); - let (default, default_layout) = load_symbol_and_layout(scope, &$state); - - let (function, closure, closure_layout) = function_details!(); - - match list_layout { - Layout::Builtin(Builtin::List(element_layout)) => { - let argument_layouts = &[*default_layout, **element_layout]; - - let roc_function_call = roc_function_call( - env, - layout_ids, - function, - closure, - closure_layout, - function_owns_closure_data, - argument_layouts, - result_layout, - ); - - crate::llvm::build_list::list_walk_generic( - env, - layout_ids, - roc_function_call, - &result_layout, - list, - element_layout, - default, - default_layout, - $variant, - ) - } - _ => unreachable!("invalid list layout"), - } - }}; - } - match op { - ListMap { xs } => { - // List.map : List before, (before -> after) -> List after - let (list, list_layout) = load_symbol_and_layout(scope, xs); - - let (function, closure, closure_layout) = function_details!(); - - match (list_layout, return_layout) { - ( - Layout::Builtin(Builtin::List(element_layout)), - Layout::Builtin(Builtin::List(result_layout)), - ) => { - let argument_layouts = &[**element_layout]; - - let roc_function_call = roc_function_call( - env, - layout_ids, - function, - closure, - closure_layout, - function_owns_closure_data, - argument_layouts, - **result_layout, - ); - - list_map(env, roc_function_call, list, element_layout, result_layout) - } - _ => unreachable!("invalid list layout"), - } - } - ListMap2 { xs, ys } => { - let (list1, list1_layout) = load_symbol_and_layout(scope, xs); - let (list2, list2_layout) = load_symbol_and_layout(scope, ys); - - let (function, closure, closure_layout) = function_details!(); - - match (list1_layout, list2_layout, return_layout) { - ( - Layout::Builtin(Builtin::List(element1_layout)), - Layout::Builtin(Builtin::List(element2_layout)), - Layout::Builtin(Builtin::List(result_layout)), - ) => { - let argument_layouts = &[**element1_layout, **element2_layout]; - - let roc_function_call = roc_function_call( - env, - layout_ids, - function, - closure, - closure_layout, - function_owns_closure_data, - argument_layouts, - **result_layout, - ); - - list_map2( - env, - layout_ids, - roc_function_call, - list1, - list2, - element1_layout, - element2_layout, - result_layout, - ) - } - _ => unreachable!("invalid list layout"), - } - } - ListMap3 { xs, ys, zs } => { - let (list1, list1_layout) = load_symbol_and_layout(scope, xs); - let (list2, list2_layout) = load_symbol_and_layout(scope, ys); - let (list3, list3_layout) = load_symbol_and_layout(scope, zs); - - let (function, closure, closure_layout) = function_details!(); - - match (list1_layout, list2_layout, list3_layout, return_layout) { - ( - Layout::Builtin(Builtin::List(element1_layout)), - Layout::Builtin(Builtin::List(element2_layout)), - Layout::Builtin(Builtin::List(element3_layout)), - Layout::Builtin(Builtin::List(result_layout)), - ) => { - let argument_layouts = - &[**element1_layout, **element2_layout, **element3_layout]; - - let roc_function_call = roc_function_call( - env, - layout_ids, - function, - closure, - closure_layout, - function_owns_closure_data, - argument_layouts, - **result_layout, - ); - - list_map3( - env, - layout_ids, - roc_function_call, - list1, - list2, - list3, - element1_layout, - element2_layout, - element3_layout, - result_layout, - ) - } - _ => unreachable!("invalid list layout"), - } - } - ListMap4 { xs, ys, zs, ws } => { - let (list1, list1_layout) = load_symbol_and_layout(scope, xs); - let (list2, list2_layout) = load_symbol_and_layout(scope, ys); - let (list3, list3_layout) = load_symbol_and_layout(scope, zs); - let (list4, list4_layout) = load_symbol_and_layout(scope, ws); - - let (function, closure, closure_layout) = function_details!(); - - match ( - list1_layout, - list2_layout, - list3_layout, - list4_layout, - return_layout, - ) { - ( - Layout::Builtin(Builtin::List(element1_layout)), - Layout::Builtin(Builtin::List(element2_layout)), - Layout::Builtin(Builtin::List(element3_layout)), - Layout::Builtin(Builtin::List(element4_layout)), - Layout::Builtin(Builtin::List(result_layout)), - ) => { - let argument_layouts = &[ - **element1_layout, - **element2_layout, - **element3_layout, - **element4_layout, - ]; - - let roc_function_call = roc_function_call( - env, - layout_ids, - function, - closure, - closure_layout, - function_owns_closure_data, - argument_layouts, - **result_layout, - ); - - list_map4( - env, - layout_ids, - roc_function_call, - list1, - list2, - list3, - list4, - element1_layout, - element2_layout, - element3_layout, - element4_layout, - result_layout, - ) - } - _ => unreachable!("invalid list layout"), - } - } - ListMapWithIndex { xs } => { - // List.mapWithIndex : List before, (before, Nat -> after) -> List after - let (list, list_layout) = load_symbol_and_layout(scope, xs); - - let (function, closure, closure_layout) = function_details!(); - - match (list_layout, return_layout) { - ( - Layout::Builtin(Builtin::List(element_layout)), - Layout::Builtin(Builtin::List(result_layout)), - ) => { - let argument_layouts = &[**element_layout, Layout::usize(env.target_info)]; - - let roc_function_call = roc_function_call( - env, - layout_ids, - function, - closure, - closure_layout, - function_owns_closure_data, - argument_layouts, - **result_layout, - ); - - list_map_with_index(env, roc_function_call, list, element_layout, result_layout) - } - _ => unreachable!("invalid list layout"), - } - } - ListKeepIf { xs } => { - // List.keepIf : List elem, (elem -> Bool) -> List elem - let (list, list_layout) = load_symbol_and_layout(scope, xs); - - let (function, closure, closure_layout) = function_details!(); - - match list_layout { - Layout::Builtin(Builtin::List(element_layout)) => { - let argument_layouts = &[**element_layout]; - - let roc_function_call = roc_function_call( - env, - layout_ids, - function, - closure, - closure_layout, - function_owns_closure_data, - argument_layouts, - result_layout, - ); - - list_keep_if(env, layout_ids, roc_function_call, list, element_layout) - } - _ => unreachable!("invalid list layout"), - } - } - ListKeepOks { xs } => { - // List.keepOks : List before, (before -> Result after *) -> List after - let (list, list_layout) = load_symbol_and_layout(scope, xs); - - let (function, closure, closure_layout) = function_details!(); - - match (list_layout, return_layout) { - ( - Layout::Builtin(Builtin::List(before_layout)), - Layout::Builtin(Builtin::List(after_layout)), - ) => { - let argument_layouts = &[**before_layout]; - - let roc_function_call = roc_function_call( - env, - layout_ids, - function, - closure, - closure_layout, - function_owns_closure_data, - argument_layouts, - result_layout, - ); - - list_keep_oks( - env, - layout_ids, - roc_function_call, - &result_layout, - list, - before_layout, - after_layout, - ) - } - (other1, other2) => { - unreachable!("invalid list layouts:\n{:?}\n{:?}", other1, other2) - } - } - } - ListKeepErrs { xs } => { - // List.keepErrs : List before, (before -> Result * after) -> List after - let (list, list_layout) = load_symbol_and_layout(scope, xs); - - let (function, closure, closure_layout) = function_details!(); - - match (list_layout, return_layout) { - ( - Layout::Builtin(Builtin::List(before_layout)), - Layout::Builtin(Builtin::List(after_layout)), - ) => { - let argument_layouts = &[**before_layout]; - - let roc_function_call = roc_function_call( - env, - layout_ids, - function, - closure, - closure_layout, - function_owns_closure_data, - argument_layouts, - result_layout, - ); - - list_keep_errs( - env, - layout_ids, - roc_function_call, - &result_layout, - list, - before_layout, - after_layout, - ) - } - (other1, other2) => { - unreachable!("invalid list layouts:\n{:?}\n{:?}", other1, other2) - } - } - } - ListWalk { xs, state } => { - list_walk!(crate::llvm::build_list::ListWalk::Walk, xs, state) - } - ListWalkUntil { xs, state } => { - list_walk!(crate::llvm::build_list::ListWalk::WalkUntil, xs, state) - } - ListWalkBackwards { xs, state } => { - list_walk!(crate::llvm::build_list::ListWalk::WalkBackwards, xs, state) - } - ListSortWith { xs } => { - // List.sortWith : List a, (a, a -> Ordering) -> List a - let (list, list_layout) = load_symbol_and_layout(scope, xs); - - let (function, closure, closure_layout) = function_details!(); - - match list_layout { - Layout::Builtin(Builtin::List(element_layout)) => { - use crate::llvm::bitcode::build_compare_wrapper; - - let argument_layouts = &[**element_layout, **element_layout]; - - let compare_wrapper = - build_compare_wrapper(env, function, closure_layout, element_layout) - .as_global_value() - .as_pointer_value(); - - let roc_function_call = roc_function_call( - env, - layout_ids, - function, - closure, - closure_layout, - function_owns_closure_data, - argument_layouts, - result_layout, - ); - - list_sort_with( - env, - roc_function_call, - compare_wrapper, - list, - element_layout, - ) - } - _ => unreachable!("invalid list layout"), - } - } - ListAny { xs } => { - let (list, list_layout) = load_symbol_and_layout(scope, xs); - let (function, closure, closure_layout) = function_details!(); - - match list_layout { - Layout::Builtin(Builtin::List(element_layout)) => { - let argument_layouts = &[**element_layout]; - - let roc_function_call = roc_function_call( - env, - layout_ids, - function, - closure, - closure_layout, - function_owns_closure_data, - argument_layouts, - Layout::Builtin(Builtin::Bool), - ); - - list_any(env, roc_function_call, list, element_layout) - } - _ => unreachable!("invalid list layout"), - } - } - ListAll { xs } => { - let (list, list_layout) = load_symbol_and_layout(scope, xs); - let (function, closure, closure_layout) = function_details!(); - - match list_layout { - Layout::Builtin(Builtin::List(element_layout)) => { - let argument_layouts = &[**element_layout]; - - let roc_function_call = roc_function_call( - env, - layout_ids, - function, - closure, - closure_layout, - function_owns_closure_data, - argument_layouts, - Layout::Builtin(Builtin::Bool), - ); - - list_all(env, roc_function_call, list, element_layout) - } - _ => unreachable!("invalid list layout"), - } - } - ListFindUnsafe { xs } => { - let (list, list_layout) = load_symbol_and_layout(scope, xs); - - let (function, closure, closure_layout) = function_details!(); - - match list_layout { - Layout::Builtin(Builtin::List(element_layout)) => { - let argument_layouts = &[**element_layout]; - let roc_function_call = roc_function_call( - env, - layout_ids, - function, - closure, - closure_layout, - function_owns_closure_data, - argument_layouts, - Layout::Builtin(Builtin::Bool), - ); - - list_find_unsafe(env, layout_ids, roc_function_call, list, element_layout) - } - _ => unreachable!("invalid list layout"), - } - } - DictWalk { xs, state } => { - let (dict, dict_layout) = load_symbol_and_layout(scope, xs); - let (default, default_layout) = load_symbol_and_layout(scope, state); - - let (function, closure, closure_layout) = function_details!(); - - match dict_layout { - Layout::Builtin(Builtin::Dict(key_layout, value_layout)) => { - let argument_layouts = &[*default_layout, **key_layout, **value_layout]; - - let roc_function_call = roc_function_call( - env, - layout_ids, - function, - closure, - closure_layout, - function_owns_closure_data, - argument_layouts, - result_layout, - ); - - dict_walk( - env, - roc_function_call, - dict, - default, - key_layout, - value_layout, - default_layout, - ) - } - _ => unreachable!("invalid dict layout"), - } - } - } -} - -#[allow(clippy::too_many_arguments)] -fn run_low_level<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - layout_ids: &mut LayoutIds<'a>, - scope: &Scope<'a, 'ctx>, - parent: FunctionValue<'ctx>, - layout: &Layout<'a>, - op: LowLevel, - args: &[Symbol], - update_mode: UpdateMode, -) -> BasicValueEnum<'ctx> { - use LowLevel::*; - - debug_assert!(!op.is_higher_order()); - - match op { - StrConcat => { - // Str.concat : Str, Str -> Str - debug_assert_eq!(args.len(), 2); - - let string1 = load_symbol(scope, &args[0]); - let string2 = load_symbol(scope, &args[1]); - - call_str_bitcode_fn(env, &[string1, string2], bitcode::STR_CONCAT) - } - StrJoinWith => { - // Str.joinWith : List Str, Str -> Str - debug_assert_eq!(args.len(), 2); - - let list = list_symbol_to_c_abi(env, scope, args[0]); - let string = load_symbol(scope, &args[1]); - - call_str_bitcode_fn(env, &[list.into(), string], bitcode::STR_JOIN_WITH) - } - StrStartsWith => { - // Str.startsWith : Str, Str -> Bool - debug_assert_eq!(args.len(), 2); - - let string = load_symbol(scope, &args[0]); - let prefix = load_symbol(scope, &args[1]); - - call_bitcode_fn(env, &[string, prefix], bitcode::STR_STARTS_WITH) - } - StrStartsWithCodePt => { - // Str.startsWithCodePt : Str, U32 -> Bool - debug_assert_eq!(args.len(), 2); - - let string = load_symbol(scope, &args[0]); - let prefix = load_symbol(scope, &args[1]); - - call_bitcode_fn(env, &[string, prefix], bitcode::STR_STARTS_WITH_CODE_PT) - } - StrEndsWith => { - // Str.startsWith : Str, Str -> Bool - debug_assert_eq!(args.len(), 2); - - let string = load_symbol(scope, &args[0]); - let prefix = load_symbol(scope, &args[1]); - - call_bitcode_fn(env, &[string, prefix], bitcode::STR_ENDS_WITH) - } - StrToNum => { - // Str.toNum : Str -> Result (Num *) {} - debug_assert_eq!(args.len(), 1); - - let number_layout = match layout { - Layout::Struct { field_layouts, .. } => field_layouts[0], // TODO: why is it sometimes a struct? - _ => unreachable!(), - }; - - // match on the return layout to figure out which zig builtin we need - let intrinsic = match number_layout { - Layout::Builtin(Builtin::Int(int_width)) => &bitcode::STR_TO_INT[int_width], - Layout::Builtin(Builtin::Float(float_width)) => &bitcode::STR_TO_FLOAT[float_width], - Layout::Builtin(Builtin::Decimal) => bitcode::DEC_FROM_STR, - _ => unreachable!(), - }; - - let string = load_symbol(scope, &args[0]); - - let result = call_bitcode_fn(env, &[string], intrinsic); - - // zig passes the result as a packed integer sometimes, instead of a struct. So we cast - let expected_type = basic_type_from_layout(env, layout); - let actual_type = result.get_type(); - - if expected_type != actual_type { - complex_bitcast_check_size(env, result, expected_type, "str_to_num_cast") - } else { - result - } - } - StrFromInt => { - // Str.fromInt : Int -> Str - debug_assert_eq!(args.len(), 1); - - let (int, int_layout) = load_symbol_and_layout(scope, &args[0]); - let int = int.into_int_value(); - - let int_width = match int_layout { - Layout::Builtin(Builtin::Int(int_width)) => *int_width, - _ => unreachable!(), - }; - - str_from_int(env, int, int_width) - } - StrFromFloat => { - // Str.fromFloat : Float * -> Str - debug_assert_eq!(args.len(), 1); - - str_from_float(env, scope, args[0]) - } - StrFromUtf8 => { - // Str.fromUtf8 : List U8 -> Result Str Utf8Problem - debug_assert_eq!(args.len(), 1); - - str_from_utf8(env, scope, args[0], update_mode) - } - StrFromUtf8Range => { - debug_assert_eq!(args.len(), 2); - - let count_and_start = load_symbol(scope, &args[1]).into_struct_value(); - - str_from_utf8_range(env, scope, args[0], count_and_start) - } - StrToUtf8 => { - // Str.fromInt : Str -> List U8 - debug_assert_eq!(args.len(), 1); - - let string = load_symbol(scope, &args[0]); - call_list_bitcode_fn(env, &[string], bitcode::STR_TO_UTF8) - } - StrRepeat => { - // Str.repeat : Str, Nat -> Str - debug_assert_eq!(args.len(), 2); - - let string = load_symbol(scope, &args[0]); - let count = load_symbol(scope, &args[1]); - call_str_bitcode_fn(env, &[string, count], bitcode::STR_REPEAT) - } - StrSplit => { - // Str.split : Str, Str -> List Str - debug_assert_eq!(args.len(), 2); - - str_split(env, scope, args[0], args[1]) - } - StrIsEmpty => { - // Str.isEmpty : Str -> Str - debug_assert_eq!(args.len(), 1); - - // the builtin will always return an u64 - let string = load_symbol(scope, &args[0]); - let length = - call_bitcode_fn(env, &[string], bitcode::STR_NUMBER_OF_BYTES).into_int_value(); - - // cast to the appropriate usize of the current build - let byte_count = - env.builder - .build_int_cast_sign_flag(length, env.ptr_int(), false, "len_as_usize"); - - let is_zero = env.builder.build_int_compare( - IntPredicate::EQ, - byte_count, - env.ptr_int().const_zero(), - "str_len_is_zero", - ); - BasicValueEnum::IntValue(is_zero) - } - StrCountGraphemes => { - // Str.countGraphemes : Str -> Int - debug_assert_eq!(args.len(), 1); - - let string = load_symbol(scope, &args[0]); - call_bitcode_fn(env, &[string], bitcode::STR_COUNT_GRAPEHEME_CLUSTERS) - } - StrTrim => { - // Str.trim : Str -> Str - debug_assert_eq!(args.len(), 1); - - let string = load_symbol(scope, &args[0]); - call_str_bitcode_fn(env, &[string], bitcode::STR_TRIM) - } - StrTrimLeft => { - // Str.trim : Str -> Str - debug_assert_eq!(args.len(), 1); - - let string = load_symbol(scope, &args[0]); - call_str_bitcode_fn(env, &[string], bitcode::STR_TRIM_LEFT) - } - StrTrimRight => { - // Str.trim : Str -> Str - debug_assert_eq!(args.len(), 1); - - let string = load_symbol(scope, &args[0]); - call_str_bitcode_fn(env, &[string], bitcode::STR_TRIM_RIGHT) - } - ListLen => { - // List.len : List * -> Int - debug_assert_eq!(args.len(), 1); - - let arg = load_symbol(scope, &args[0]); - - list_len(env.builder, arg.into_struct_value()).into() - } - ListSingle => { - // List.single : a -> List a - debug_assert_eq!(args.len(), 1); - - let (arg, arg_layout) = load_symbol_and_layout(scope, &args[0]); - - list_single(env, arg, arg_layout) - } - ListRepeat => { - // List.repeat : elem, Nat -> List elem - debug_assert_eq!(args.len(), 2); - - let (elem, elem_layout) = load_symbol_and_layout(scope, &args[0]); - let list_len = load_symbol(scope, &args[1]).into_int_value(); - - list_repeat(env, layout_ids, elem, elem_layout, list_len) - } - ListReverse => { - // List.reverse : List elem -> List elem - debug_assert_eq!(args.len(), 1); - - let (list, list_layout) = load_symbol_and_layout(scope, &args[0]); - - let element_layout = list_element_layout!(list_layout); - - list_reverse(env, list, element_layout, update_mode) - } - ListConcat => { - debug_assert_eq!(args.len(), 2); - - let (first_list, list_layout) = load_symbol_and_layout(scope, &args[0]); - - let second_list = load_symbol(scope, &args[1]); - - let element_layout = list_element_layout!(list_layout); - - list_concat(env, first_list, second_list, element_layout) - } - ListContains => { - // List.contains : List elem, elem -> Bool - debug_assert_eq!(args.len(), 2); - - let list = load_symbol(scope, &args[0]); - - let (elem, elem_layout) = load_symbol_and_layout(scope, &args[1]); - - list_contains(env, layout_ids, elem, elem_layout, list) - } - ListRange => { - // List.contains : List elem, elem -> Bool - debug_assert_eq!(args.len(), 2); - - let (low, low_layout) = load_symbol_and_layout(scope, &args[0]); - let high = load_symbol(scope, &args[1]); - - let int_width = match low_layout { - Layout::Builtin(Builtin::Int(int_width)) => *int_width, - _ => unreachable!(), - }; - - list_range(env, int_width, low.into_int_value(), high.into_int_value()) - } - ListAppend => { - // List.append : List elem, elem -> List elem - debug_assert_eq!(args.len(), 2); - - let original_wrapper = load_symbol(scope, &args[0]).into_struct_value(); - let (elem, elem_layout) = load_symbol_and_layout(scope, &args[1]); - - list_append(env, original_wrapper, elem, elem_layout, update_mode) - } - ListSwap => { - // List.swap : List elem, Nat, Nat -> List elem - debug_assert_eq!(args.len(), 3); - - let (list, list_layout) = load_symbol_and_layout(scope, &args[0]); - let original_wrapper = list.into_struct_value(); - - let index_1 = load_symbol(scope, &args[1]); - let index_2 = load_symbol(scope, &args[2]); - - let element_layout = list_element_layout!(list_layout); - list_swap( - env, - original_wrapper, - index_1.into_int_value(), - index_2.into_int_value(), - element_layout, - update_mode, - ) - } - ListSublist => { - // List.sublist : List elem, { start : Nat, len : Nat } -> List elem - // - // As a low-level, record is destructed - // List.sublist : List elem, start : Nat, len : Nat -> List elem - debug_assert_eq!(args.len(), 3); - - let (list, list_layout) = load_symbol_and_layout(scope, &args[0]); - let original_wrapper = list.into_struct_value(); - - let start = load_symbol(scope, &args[1]); - let len = load_symbol(scope, &args[2]); - - let element_layout = list_element_layout!(list_layout); - list_sublist( - env, - layout_ids, - original_wrapper, - start.into_int_value(), - len.into_int_value(), - element_layout, - ) - } - ListDropAt => { - // List.dropAt : List elem, Nat -> List elem - debug_assert_eq!(args.len(), 2); - - let (list, list_layout) = load_symbol_and_layout(scope, &args[0]); - let original_wrapper = list.into_struct_value(); - - let count = load_symbol(scope, &args[1]); - - let element_layout = list_element_layout!(list_layout); - list_drop_at( - env, - layout_ids, - original_wrapper, - count.into_int_value(), - element_layout, - ) - } - ListPrepend => { - // List.prepend : List elem, elem -> List elem - debug_assert_eq!(args.len(), 2); - - let original_wrapper = load_symbol(scope, &args[0]).into_struct_value(); - let (elem, elem_layout) = load_symbol_and_layout(scope, &args[1]); - - list_prepend(env, original_wrapper, elem, elem_layout) - } - ListJoin => { - // List.join : List (List elem) -> List elem - debug_assert_eq!(args.len(), 1); - - let (list, outer_list_layout) = load_symbol_and_layout(scope, &args[0]); - - let inner_list_layout = list_element_layout!(outer_list_layout); - let element_layout = list_element_layout!(inner_list_layout); - - list_join(env, list, element_layout) - } - ListGetUnsafe => { - // List.get : List elem, Nat -> [Ok elem, OutOfBounds]* - debug_assert_eq!(args.len(), 2); - - let (wrapper_struct, list_layout) = load_symbol_and_layout(scope, &args[0]); - let wrapper_struct = wrapper_struct.into_struct_value(); - let elem_index = load_symbol(scope, &args[1]).into_int_value(); - - let element_layout = list_element_layout!(list_layout); - - list_get_unsafe( - env, - layout_ids, - parent, - element_layout, - elem_index, - wrapper_struct, - ) - } - ListReplaceUnsafe => { - let list = load_symbol(scope, &args[0]); - let index = load_symbol(scope, &args[1]); - let (element, element_layout) = load_symbol_and_layout(scope, &args[2]); - - list_replace_unsafe( - env, - layout_ids, - list, - index.into_int_value(), - element, - element_layout, - update_mode, - ) - } - ListIsUnique => { - // List.isUnique : List a -> Bool - debug_assert_eq!(args.len(), 1); - - let list = load_symbol(scope, &args[0]); - let list = list_to_c_abi(env, list).into(); - - call_bitcode_fn(env, &[list], bitcode::LIST_IS_UNIQUE) - } - NumToStr => { - // Num.toStr : Num a -> Str - debug_assert_eq!(args.len(), 1); - - let (num, num_layout) = load_symbol_and_layout(scope, &args[0]); - - match num_layout { - Layout::Builtin(Builtin::Int(int_width)) => { - let int = num.into_int_value(); - - str_from_int(env, int, *int_width) - } - Layout::Builtin(Builtin::Float(_float_width)) => { - str_from_float(env, scope, args[0]) - } - _ => unreachable!(), - } - } - NumAbs | NumNeg | NumRound | NumSqrtUnchecked | NumLogUnchecked | NumSin | NumCos - | NumCeiling | NumFloor | NumToFrac | NumIsFinite | NumAtan | NumAcos | NumAsin - | NumToIntChecked => { - debug_assert_eq!(args.len(), 1); - - let (arg, arg_layout) = load_symbol_and_layout(scope, &args[0]); - - match arg_layout { - Layout::Builtin(arg_builtin) => { - use roc_mono::layout::Builtin::*; - - match arg_builtin { - Int(int_width) => { - let int_type = convert::int_type_from_int_width(env, *int_width); - build_int_unary_op( - env, - arg.into_int_value(), - *int_width, - int_type, - op, - layout, - ) - } - Float(float_width) => build_float_unary_op( - env, - layout, - arg.into_float_value(), - op, - *float_width, - ), - _ => { - unreachable!("Compiler bug: tried to run numeric operation {:?} on invalid builtin layout: ({:?})", op, arg_layout); - } - } - } - _ => { - unreachable!( - "Compiler bug: tried to run numeric operation {:?} on invalid layout: {:?}", - op, arg_layout - ); - } - } - } - NumBytesToU16 => { - debug_assert_eq!(args.len(), 2); - let list = load_symbol(scope, &args[0]); - let position = load_symbol(scope, &args[1]); - call_bitcode_fn( - env, - &[list_to_c_abi(env, list).into(), position], - bitcode::NUM_BYTES_TO_U16, - ) - } - NumBytesToU32 => { - debug_assert_eq!(args.len(), 2); - let list = load_symbol(scope, &args[0]); - let position = load_symbol(scope, &args[1]); - call_bitcode_fn( - env, - &[list_to_c_abi(env, list).into(), position], - bitcode::NUM_BYTES_TO_U32, - ) - } - NumCompare => { - use inkwell::FloatPredicate; - - debug_assert_eq!(args.len(), 2); - - let (lhs_arg, lhs_layout) = load_symbol_and_layout(scope, &args[0]); - let (rhs_arg, rhs_layout) = load_symbol_and_layout(scope, &args[1]); - - match (lhs_layout, rhs_layout) { - (Layout::Builtin(lhs_builtin), Layout::Builtin(rhs_builtin)) - if lhs_builtin == rhs_builtin => - { - use roc_mono::layout::Builtin::*; - - let tag_eq = env.context.i8_type().const_int(0_u64, false); - let tag_gt = env.context.i8_type().const_int(1_u64, false); - let tag_lt = env.context.i8_type().const_int(2_u64, false); - - match lhs_builtin { - Int(int_width) => { - let are_equal = env.builder.build_int_compare( - IntPredicate::EQ, - lhs_arg.into_int_value(), - rhs_arg.into_int_value(), - "int_eq", - ); - - let predicate = if int_width.is_signed() { - IntPredicate::SLT - } else { - IntPredicate::ULT - }; - - let is_less_than = env.builder.build_int_compare( - predicate, - lhs_arg.into_int_value(), - rhs_arg.into_int_value(), - "int_compare", - ); - - let step1 = - env.builder - .build_select(is_less_than, tag_lt, tag_gt, "lt_or_gt"); - - env.builder.build_select( - are_equal, - tag_eq, - step1.into_int_value(), - "lt_or_gt", - ) - } - Float(_) => { - let are_equal = env.builder.build_float_compare( - FloatPredicate::OEQ, - lhs_arg.into_float_value(), - rhs_arg.into_float_value(), - "float_eq", - ); - let is_less_than = env.builder.build_float_compare( - FloatPredicate::OLT, - lhs_arg.into_float_value(), - rhs_arg.into_float_value(), - "float_compare", - ); - - let step1 = - env.builder - .build_select(is_less_than, tag_lt, tag_gt, "lt_or_gt"); - - env.builder.build_select( - are_equal, - tag_eq, - step1.into_int_value(), - "lt_or_gt", - ) - } - - _ => { - unreachable!("Compiler bug: tried to run numeric operation {:?} on invalid builtin layout: ({:?})", op, lhs_layout); - } - } - } - _ => { - unreachable!("Compiler bug: tried to run numeric operation {:?} on invalid layouts. The 2 layouts were: ({:?}) and ({:?})", op, lhs_layout, rhs_layout); - } - } - } - - NumAdd | NumSub | NumMul | NumLt | NumLte | NumGt | NumGte | NumRemUnchecked - | NumIsMultipleOf | NumAddWrap | NumAddChecked | NumAddSaturated | NumDivUnchecked - | NumDivCeilUnchecked | NumPow | NumPowInt | NumSubWrap | NumSubChecked - | NumSubSaturated | NumMulWrap | NumMulChecked => { - debug_assert_eq!(args.len(), 2); - - let (lhs_arg, lhs_layout) = load_symbol_and_layout(scope, &args[0]); - let (rhs_arg, rhs_layout) = load_symbol_and_layout(scope, &args[1]); - - build_num_binop(env, parent, lhs_arg, lhs_layout, rhs_arg, rhs_layout, op) - } - NumBitwiseAnd | NumBitwiseOr | NumBitwiseXor => { - debug_assert_eq!(args.len(), 2); - - let (lhs_arg, lhs_layout) = load_symbol_and_layout(scope, &args[0]); - let (rhs_arg, rhs_layout) = load_symbol_and_layout(scope, &args[1]); - - debug_assert_eq!(lhs_layout, rhs_layout); - let int_width = intwidth_from_layout(*lhs_layout); - - build_int_binop( - env, - parent, - int_width, - lhs_arg.into_int_value(), - rhs_arg.into_int_value(), - op, - ) - } - NumShiftLeftBy | NumShiftRightBy | NumShiftRightZfBy => { - debug_assert_eq!(args.len(), 2); - - let (lhs_arg, lhs_layout) = load_symbol_and_layout(scope, &args[0]); - let (rhs_arg, rhs_layout) = load_symbol_and_layout(scope, &args[1]); - - debug_assert_eq!(lhs_layout, rhs_layout); - let int_width = intwidth_from_layout(*lhs_layout); - - build_int_binop( - env, - parent, - int_width, - lhs_arg.into_int_value(), - rhs_arg.into_int_value(), - op, - ) - } - NumIntCast => { - debug_assert_eq!(args.len(), 1); - - let arg = load_symbol(scope, &args[0]).into_int_value(); - - let to = basic_type_from_layout(env, layout).into_int_type(); - let to_signed = intwidth_from_layout(*layout).is_signed(); - - env.builder - .build_int_cast_sign_flag(arg, to, to_signed, "inc_cast") - .into() - } - NumToFloatCast => { - debug_assert_eq!(args.len(), 1); - - let (arg, arg_layout) = load_symbol_and_layout(scope, &args[0]); - - match arg_layout { - Layout::Builtin(Builtin::Int(width)) => { - // Converting from int to float - let int_val = arg.into_int_value(); - let dest = basic_type_from_layout(env, layout).into_float_type(); - - if width.is_signed() { - env.builder - .build_signed_int_to_float(int_val, dest, "signed_int_to_float") - .into() - } else { - env.builder - .build_unsigned_int_to_float(int_val, dest, "unsigned_int_to_float") - .into() - } - } - Layout::Builtin(Builtin::Float(_)) => { - // Converting from float to float - e.g. F64 to F32, or vice versa - let dest = basic_type_from_layout(env, layout).into_float_type(); - - env.builder - .build_float_cast(arg.into_float_value(), dest, "cast_float_to_float") - .into() - } - Layout::Builtin(Builtin::Decimal) => { - todo!("Support converting Dec values to floats."); - } - other => { - unreachable!("Tried to do a float cast to non-float layout {:?}", other); - } - } - } - NumToFloatChecked => { - // NOTE: There's a NumToIntChecked implementation above, - // which could be useful to look at when implementing this. - todo!("implement checked float conversion"); - } - Eq => { - debug_assert_eq!(args.len(), 2); - - let (lhs_arg, lhs_layout) = load_symbol_and_layout(scope, &args[0]); - let (rhs_arg, rhs_layout) = load_symbol_and_layout(scope, &args[1]); - - generic_eq(env, layout_ids, lhs_arg, rhs_arg, lhs_layout, rhs_layout) - } - NotEq => { - debug_assert_eq!(args.len(), 2); - - let (lhs_arg, lhs_layout) = load_symbol_and_layout(scope, &args[0]); - let (rhs_arg, rhs_layout) = load_symbol_and_layout(scope, &args[1]); - - generic_neq(env, layout_ids, lhs_arg, rhs_arg, lhs_layout, rhs_layout) - } - And => { - // The (&&) operator - debug_assert_eq!(args.len(), 2); - - let lhs_arg = load_symbol(scope, &args[0]); - let rhs_arg = load_symbol(scope, &args[1]); - let bool_val = env.builder.build_and( - lhs_arg.into_int_value(), - rhs_arg.into_int_value(), - "bool_and", - ); - - BasicValueEnum::IntValue(bool_val) - } - Or => { - // The (||) operator - debug_assert_eq!(args.len(), 2); - - let lhs_arg = load_symbol(scope, &args[0]); - let rhs_arg = load_symbol(scope, &args[1]); - let bool_val = env.builder.build_or( - lhs_arg.into_int_value(), - rhs_arg.into_int_value(), - "bool_or", - ); - - BasicValueEnum::IntValue(bool_val) - } - Not => { - // The (!) operator - debug_assert_eq!(args.len(), 1); - - let arg = load_symbol(scope, &args[0]); - let bool_val = env.builder.build_not(arg.into_int_value(), "bool_not"); - - BasicValueEnum::IntValue(bool_val) - } - Hash => { - debug_assert_eq!(args.len(), 2); - let seed = load_symbol(scope, &args[0]); - let (value, layout) = load_symbol_and_layout(scope, &args[1]); - - debug_assert!(seed.is_int_value()); - - generic_hash(env, layout_ids, seed.into_int_value(), value, layout).into() - } - DictSize => { - debug_assert_eq!(args.len(), 1); - dict_len(env, scope, args[0]) - } - DictEmpty => { - debug_assert_eq!(args.len(), 0); - dict_empty(env) - } - DictInsert => { - debug_assert_eq!(args.len(), 3); - - let (dict, _) = load_symbol_and_layout(scope, &args[0]); - let (key, key_layout) = load_symbol_and_layout(scope, &args[1]); - let (value, value_layout) = load_symbol_and_layout(scope, &args[2]); - dict_insert(env, layout_ids, dict, key, key_layout, value, value_layout) - } - DictRemove => { - debug_assert_eq!(args.len(), 2); - - let (dict, dict_layout) = load_symbol_and_layout(scope, &args[0]); - let key = load_symbol(scope, &args[1]); - - let (key_layout, value_layout) = dict_key_value_layout!(dict_layout); - dict_remove(env, layout_ids, dict, key, key_layout, value_layout) - } - DictContains => { - debug_assert_eq!(args.len(), 2); - - let (dict, dict_layout) = load_symbol_and_layout(scope, &args[0]); - let key = load_symbol(scope, &args[1]); - - let (key_layout, value_layout) = dict_key_value_layout!(dict_layout); - dict_contains(env, layout_ids, dict, key, key_layout, value_layout) - } - DictGetUnsafe => { - debug_assert_eq!(args.len(), 2); - - let (dict, dict_layout) = load_symbol_and_layout(scope, &args[0]); - let key = load_symbol(scope, &args[1]); - - let (key_layout, value_layout) = dict_key_value_layout!(dict_layout); - dict_get(env, layout_ids, dict, key, key_layout, value_layout) - } - DictKeys => { - debug_assert_eq!(args.len(), 1); - - let (dict, dict_layout) = load_symbol_and_layout(scope, &args[0]); - - let (key_layout, value_layout) = dict_key_value_layout!(dict_layout); - dict_keys(env, layout_ids, dict, key_layout, value_layout) - } - DictValues => { - debug_assert_eq!(args.len(), 1); - - let (dict, dict_layout) = load_symbol_and_layout(scope, &args[0]); - - let (key_layout, value_layout) = dict_key_value_layout!(dict_layout); - dict_values(env, layout_ids, dict, key_layout, value_layout) - } - DictUnion => { - debug_assert_eq!(args.len(), 2); - - let (dict1, dict_layout) = load_symbol_and_layout(scope, &args[0]); - let (dict2, _) = load_symbol_and_layout(scope, &args[1]); - - let (key_layout, value_layout) = dict_key_value_layout!(dict_layout); - dict_union(env, layout_ids, dict1, dict2, key_layout, value_layout) - } - DictDifference => { - debug_assert_eq!(args.len(), 2); - - let (dict1, dict_layout) = load_symbol_and_layout(scope, &args[0]); - let (dict2, _) = load_symbol_and_layout(scope, &args[1]); - - let (key_layout, value_layout) = dict_key_value_layout!(dict_layout); - dict_difference(env, layout_ids, dict1, dict2, key_layout, value_layout) - } - DictIntersection => { - debug_assert_eq!(args.len(), 2); - - let (dict1, dict_layout) = load_symbol_and_layout(scope, &args[0]); - let (dict2, _) = load_symbol_and_layout(scope, &args[1]); - - let (key_layout, value_layout) = dict_key_value_layout!(dict_layout); - dict_intersection(env, layout_ids, dict1, dict2, key_layout, value_layout) - } - SetFromList => { - debug_assert_eq!(args.len(), 1); - - let (list, list_layout) = load_symbol_and_layout(scope, &args[0]); - - let key_layout = list_element_layout!(list_layout); - set_from_list(env, layout_ids, list, key_layout) - } - SetToDict => { - debug_assert_eq!(args.len(), 1); - - let (set, _set_layout) = load_symbol_and_layout(scope, &args[0]); - - set - } - - ListMap | ListMap2 | ListMap3 | ListMap4 | ListMapWithIndex | ListKeepIf | ListWalk - | ListWalkUntil | ListWalkBackwards | ListKeepOks | ListKeepErrs | ListSortWith - | ListAny | ListAll | ListFindUnsafe | DictWalk => { - unreachable!("these are higher order, and are handled elsewhere") - } - - BoxExpr | UnboxExpr => { - unreachable!("The {:?} operation is turned into mono Expr", op) - } - - PtrCast | RefCountInc | RefCountDec => { - unreachable!("Not used in LLVM backend: {:?}", op); - } - } -} - -/// A type that is valid according to the C ABI -/// -/// As an example, structs that fit inside an integer type should -/// (this does not currently happen here) be coerced to that integer type. -fn to_cc_type<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - layout: &Layout<'a>, -) -> BasicTypeEnum<'ctx> { - match layout { - Layout::Builtin(builtin) => to_cc_type_builtin(env, builtin), - _ => { - // TODO this is almost certainly incorrect for bigger structs - basic_type_from_layout(env, layout) - } - } -} - -fn to_cc_type_builtin<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - builtin: &Builtin<'a>, -) -> BasicTypeEnum<'ctx> { - match builtin { - Builtin::Int(_) | Builtin::Float(_) | Builtin::Bool | Builtin::Decimal => { - basic_type_from_builtin(env, builtin) - } - Builtin::Str | Builtin::List(_) => { - let address_space = AddressSpace::Generic; - let field_types: [BasicTypeEnum; 3] = [ - env.context.i8_type().ptr_type(address_space).into(), - env.ptr_int().into(), - env.ptr_int().into(), - ]; - - let struct_type = env.context.struct_type(&field_types, false); - - struct_type.ptr_type(address_space).into() - } - Builtin::Dict(_, _) | Builtin::Set(_) => { - // TODO verify this is what actually happens - basic_type_from_builtin(env, builtin) - } - } -} - -#[derive(Clone, Copy)] -enum RocReturn { - /// Return as normal - Return, - /// require an extra argument, a pointer - /// where the result is written into returns void - ByPointer, -} - -impl RocReturn { - fn roc_return_by_pointer(target_info: TargetInfo, layout: Layout) -> bool { - match layout { - Layout::Builtin(builtin) => { - use Builtin::*; - - match target_info.ptr_width() { - roc_target::PtrWidth::Bytes4 => false, - - roc_target::PtrWidth::Bytes8 => { - // - matches!(builtin, Str) - } - } - } - Layout::Union(UnionLayout::NonRecursive(_)) => true, - Layout::LambdaSet(lambda_set) => { - RocReturn::roc_return_by_pointer(target_info, lambda_set.runtime_representation()) - } - _ => false, - } - } - - fn from_layout<'a, 'ctx, 'env>(env: &Env<'a, 'ctx, 'env>, layout: &Layout<'a>) -> Self { - if Self::roc_return_by_pointer(env.target_info, *layout) { - RocReturn::ByPointer - } else { - RocReturn::Return - } - } -} - -#[derive(Debug, Clone, Copy)] -pub enum CCReturn { - /// Return as normal - Return, - /// require an extra argument, a pointer - /// where the result is written into - /// returns void - ByPointer, - /// The return type is zero-sized - Void, -} - -#[derive(Debug, Clone, Copy)] -pub struct FunctionSpec<'ctx> { - /// The function type - pub typ: FunctionType<'ctx>, - call_conv: u32, - - /// Index (0-based) of return-by-pointer parameter, if it exists. - /// We only care about this for C-call-conv functions, because this may take - /// ownership of a register due to the convention. For example, on AArch64, - /// values returned-by-pointer use the x8 register. - /// But for internal functions we don't need to worry about that and we don't - /// want the convention, since it might eat a register and cause a spill! - cconv_sret_parameter: Option, -} - -impl<'ctx> FunctionSpec<'ctx> { - fn attach_attributes(&self, ctx: &Context, fn_val: FunctionValue<'ctx>) { - fn_val.set_call_conventions(self.call_conv); - - if let Some(param_index) = self.cconv_sret_parameter { - // Indicate to LLVM that this argument holds the return value of the function. - let sret_attribute_id = Attribute::get_named_enum_kind_id("sret"); - debug_assert!(sret_attribute_id > 0); - let ret_typ = self.typ.get_param_types()[param_index as usize]; - let sret_attribute = - ctx.create_type_attribute(sret_attribute_id, ret_typ.as_any_type_enum()); - fn_val.add_attribute(AttributeLoc::Param(0), sret_attribute); - } - } - - /// C-calling convention - pub fn cconv<'a, 'env>( - env: &Env<'a, 'ctx, 'env>, - cc_return: CCReturn, - return_type: Option>, - argument_types: &[BasicTypeEnum<'ctx>], - ) -> FunctionSpec<'ctx> { - let (typ, opt_sret_parameter) = match cc_return { - CCReturn::ByPointer => { - // turn the output type into a pointer type. Make it the first argument to the function - let output_type = return_type.unwrap().ptr_type(AddressSpace::Generic); - - let mut arguments: Vec<'_, BasicTypeEnum> = - bumpalo::vec![in env.arena; output_type.into()]; - arguments.extend(argument_types); - - let arguments = function_arguments(env, &arguments); - (env.context.void_type().fn_type(&arguments, false), Some(0)) - } - CCReturn::Return => { - let arguments = function_arguments(env, argument_types); - (return_type.unwrap().fn_type(&arguments, false), None) - } - CCReturn::Void => { - let arguments = function_arguments(env, argument_types); - (env.context.void_type().fn_type(&arguments, false), None) - } - }; - - Self { - typ, - call_conv: C_CALL_CONV, - cconv_sret_parameter: opt_sret_parameter, - } - } - - /// Fastcc calling convention - fn fastcc<'a, 'env>( - env: &Env<'a, 'ctx, 'env>, - roc_return: RocReturn, - return_type: BasicTypeEnum<'ctx>, - mut argument_types: Vec>, - ) -> FunctionSpec<'ctx> { - let typ = match roc_return { - RocReturn::Return => { - return_type.fn_type(&function_arguments(env, &argument_types), false) - } - RocReturn::ByPointer => { - argument_types.push(return_type.ptr_type(AddressSpace::Generic).into()); - env.context - .void_type() - .fn_type(&function_arguments(env, &argument_types), false) - } - }; - - Self { - typ, - call_conv: FAST_CALL_CONV, - cconv_sret_parameter: None, - } - } - - pub fn known_fastcc(fn_type: FunctionType<'ctx>) -> FunctionSpec<'ctx> { - Self { - typ: fn_type, - call_conv: FAST_CALL_CONV, - cconv_sret_parameter: None, - } - } - - pub fn intrinsic(fn_type: FunctionType<'ctx>) -> Self { - // LLVM intrinsics always use the C calling convention, because - // they are implemented in C libraries - Self { - typ: fn_type, - call_conv: C_CALL_CONV, - cconv_sret_parameter: None, - } - } -} - -/// According to the C ABI, how should we return a value with the given layout? -pub fn to_cc_return<'a, 'ctx, 'env>(env: &Env<'a, 'ctx, 'env>, layout: &Layout<'a>) -> CCReturn { - let return_size = layout.stack_size(env.target_info); - let pass_result_by_pointer = return_size > 2 * env.target_info.ptr_width() as u32; - - if return_size == 0 { - CCReturn::Void - } else if pass_result_by_pointer { - CCReturn::ByPointer - } else { - CCReturn::Return - } -} - -fn function_arguments<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - arguments: &[BasicTypeEnum<'ctx>], -) -> Vec<'a, BasicMetadataTypeEnum<'ctx>> { - let it = arguments.iter().map(|x| (*x).into()); - Vec::from_iter_in(it, env.arena) -} - -fn build_foreign_symbol<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - scope: &mut Scope<'a, 'ctx>, - foreign: &roc_module::ident::ForeignSymbol, - argument_symbols: &[Symbol], - ret_layout: &Layout<'a>, -) -> BasicValueEnum<'ctx> { - let builder = env.builder; - let context = env.context; - - let fastcc_function_name = format!("{}_fastcc_wrapper", foreign.as_str()); - - let (fastcc_function, arguments) = match env.module.get_function(fastcc_function_name.as_str()) - { - Some(function_value) => { - let mut arguments = Vec::with_capacity_in(argument_symbols.len(), env.arena); - - for symbol in argument_symbols { - let (value, _) = load_symbol_and_layout(scope, symbol); - - arguments.push(value); - } - - (function_value, arguments) - } - None => { - // Here we build two functions: - // - // - an C_CALL_CONV extern that will be provided by the host, e.g. `roc_fx_putLine` - // This is just a type signature that we make available to the linker, - // and can use in the wrapper - // - a FAST_CALL_CONV wrapper that we make here, e.g. `roc_fx_putLine_fastcc_wrapper` - - let return_type = basic_type_from_layout(env, ret_layout); - let roc_return = RocReturn::from_layout(env, ret_layout); - let cc_return = to_cc_return(env, ret_layout); - - let mut cc_argument_types = - Vec::with_capacity_in(argument_symbols.len() + 1, env.arena); - let mut fastcc_argument_types = - Vec::with_capacity_in(argument_symbols.len(), env.arena); - let mut arguments = Vec::with_capacity_in(argument_symbols.len(), env.arena); - - for symbol in argument_symbols { - let (value, layout) = load_symbol_and_layout(scope, symbol); - - cc_argument_types.push(to_cc_type(env, layout)); - - let basic_type = argument_type_from_layout(env, layout); - fastcc_argument_types.push(basic_type); - - arguments.push(value); - } - - let cc_type = - FunctionSpec::cconv(env, cc_return, Some(return_type), &cc_argument_types); - let cc_function = get_foreign_symbol(env, foreign.clone(), cc_type); - - let fastcc_type = - FunctionSpec::fastcc(env, roc_return, return_type, fastcc_argument_types); - - let fastcc_function = add_func( - env.context, - env.module, - &fastcc_function_name, - fastcc_type, - Linkage::Internal, - ); - - let old = builder.get_insert_block().unwrap(); - - let entry = context.append_basic_block(fastcc_function, "entry"); - { - builder.position_at_end(entry); - - let mut fastcc_parameters = fastcc_function.get_params(); - let mut cc_arguments = - Vec::with_capacity_in(fastcc_parameters.len() + 1, env.arena); - - let return_pointer = match roc_return { - RocReturn::Return => env.builder.build_alloca(return_type, "return_value"), - RocReturn::ByPointer => fastcc_parameters.pop().unwrap().into_pointer_value(), - }; - - if let CCReturn::ByPointer = cc_return { - cc_arguments.push(return_pointer.into()); - } - - let it = fastcc_parameters.into_iter().zip(cc_argument_types.iter()); - for (param, cc_type) in it { - if param.get_type() == *cc_type { - cc_arguments.push(param.into()); - } else { - // not pretty, but seems to cover all our current case - if cc_type.is_pointer_type() && !param.get_type().is_pointer_type() { - // we need to pass this value by-reference; put it into an alloca - // and bitcast the reference - - let param_alloca = - env.builder.build_alloca(param.get_type(), "param_alloca"); - env.builder.build_store(param_alloca, param); - - let as_cc_type = env.builder.build_bitcast( - param_alloca, - cc_type.into_pointer_type(), - "to_cc_type_ptr", - ); - - cc_arguments.push(as_cc_type.into()); - } else { - // eprintln!("C type: {:?}", cc_type); - // eprintln!("Fastcc type: {:?}", param.get_type()); - // todo!("C <-> Fastcc interaction that we haven't seen before") - - let as_cc_type = env.builder.build_pointer_cast( - param.into_pointer_value(), - cc_type.into_pointer_type(), - "to_cc_type_ptr", - ); - cc_arguments.push(as_cc_type.into()); - } - } - } - - let call = env.builder.build_call(cc_function, &cc_arguments, "tmp"); - call.set_call_convention(C_CALL_CONV); - - match roc_return { - RocReturn::Return => { - let return_value = match cc_return { - CCReturn::Return => call.try_as_basic_value().left().unwrap(), - - CCReturn::ByPointer => { - env.builder.build_load(return_pointer, "read_result") - } - CCReturn::Void => return_type.const_zero(), - }; - - builder.build_return(Some(&return_value)); - } - RocReturn::ByPointer => { - debug_assert!(matches!(cc_return, CCReturn::ByPointer)); - - builder.build_return(None); - } - } - } - - builder.position_at_end(old); - - (fastcc_function, arguments) - } - }; - - call_roc_function(env, fastcc_function, ret_layout, &arguments) -} - -fn throw_on_overflow<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - parent: FunctionValue<'ctx>, - result: StructValue<'ctx>, // of the form { value: T, has_overflowed: bool } - message: &str, -) -> BasicValueEnum<'ctx> { - let bd = env.builder; - let context = env.context; - - let has_overflowed = bd.build_extract_value(result, 1, "has_overflowed").unwrap(); - - let condition = bd.build_int_compare( - IntPredicate::EQ, - has_overflowed.into_int_value(), - context.bool_type().const_zero(), - "has_not_overflowed", - ); - - let then_block = context.append_basic_block(parent, "then_block"); - let throw_block = context.append_basic_block(parent, "throw_block"); - - bd.build_conditional_branch(condition, then_block, throw_block); - - bd.position_at_end(throw_block); - - throw_exception(env, message); - - bd.position_at_end(then_block); - - bd.build_extract_value(result, 0, "operation_result") - .unwrap() -} - -fn intwidth_from_layout(layout: Layout<'_>) -> IntWidth { - match layout { - Layout::Builtin(Builtin::Int(int_width)) => int_width, - - _ => unreachable!(), - } -} - -fn build_int_binop<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - parent: FunctionValue<'ctx>, - int_width: IntWidth, - lhs: IntValue<'ctx>, - rhs: IntValue<'ctx>, - op: LowLevel, -) -> BasicValueEnum<'ctx> { - use inkwell::IntPredicate::*; - use roc_module::low_level::LowLevel::*; - - let bd = env.builder; - - match op { - NumAdd => { - let result = env - .call_intrinsic( - &LLVM_ADD_WITH_OVERFLOW[int_width], - &[lhs.into(), rhs.into()], - ) - .into_struct_value(); - - throw_on_overflow(env, parent, result, "integer addition overflowed!") - } - NumAddWrap => bd.build_int_add(lhs, rhs, "add_int_wrap").into(), - NumAddChecked => env.call_intrinsic( - &LLVM_ADD_WITH_OVERFLOW[int_width], - &[lhs.into(), rhs.into()], - ), - NumAddSaturated => { - env.call_intrinsic(&LLVM_ADD_SATURATED[int_width], &[lhs.into(), rhs.into()]) - } - NumSub => { - let result = env - .call_intrinsic( - &LLVM_SUB_WITH_OVERFLOW[int_width], - &[lhs.into(), rhs.into()], - ) - .into_struct_value(); - - throw_on_overflow(env, parent, result, "integer subtraction overflowed!") - } - NumSubWrap => bd.build_int_sub(lhs, rhs, "sub_int").into(), - NumSubChecked => env.call_intrinsic( - &LLVM_SUB_WITH_OVERFLOW[int_width], - &[lhs.into(), rhs.into()], - ), - NumSubSaturated => { - env.call_intrinsic(&LLVM_SUB_SATURATED[int_width], &[lhs.into(), rhs.into()]) - } - NumMul => { - let result = env - .call_intrinsic( - &LLVM_MUL_WITH_OVERFLOW[int_width], - &[lhs.into(), rhs.into()], - ) - .into_struct_value(); - - throw_on_overflow(env, parent, result, "integer multiplication overflowed!") - } - NumMulWrap => bd.build_int_mul(lhs, rhs, "mul_int").into(), - NumMulChecked => env.call_intrinsic( - &LLVM_MUL_WITH_OVERFLOW[int_width], - &[lhs.into(), rhs.into()], - ), - NumGt => { - if int_width.is_signed() { - bd.build_int_compare(SGT, lhs, rhs, "gt_int").into() - } else { - bd.build_int_compare(UGT, lhs, rhs, "gt_uint").into() - } - } - NumGte => { - if int_width.is_signed() { - bd.build_int_compare(SGE, lhs, rhs, "gte_int").into() - } else { - bd.build_int_compare(UGE, lhs, rhs, "gte_uint").into() - } - } - NumLt => { - if int_width.is_signed() { - bd.build_int_compare(SLT, lhs, rhs, "lt_int").into() - } else { - bd.build_int_compare(ULT, lhs, rhs, "lt_uint").into() - } - } - NumLte => { - if int_width.is_signed() { - bd.build_int_compare(SLE, lhs, rhs, "lte_int").into() - } else { - bd.build_int_compare(ULE, lhs, rhs, "lte_uint").into() - } - } - NumRemUnchecked => { - if int_width.is_signed() { - bd.build_int_signed_rem(lhs, rhs, "rem_int").into() - } else { - bd.build_int_unsigned_rem(lhs, rhs, "rem_uint").into() - } - } - NumIsMultipleOf => { - // this builds the following construct - // - // if (rhs == 0 || rhs == -1) { - // // lhs is a multiple of rhs iff - // // - // // - rhs == -1 - // // - both rhs and lhs are 0 - // // - // // the -1 case is important for overflow reasons `isize::MIN % -1` crashes in rust - // (rhs == -1) || (lhs == 0) - // } else { - // let rem = lhs % rhs; - // rem == 0 - // } - // - // NOTE we'd like the branches to be swapped for better branch prediction, - // but llvm normalizes to the above ordering in -O3 - let zero = rhs.get_type().const_zero(); - let neg_1 = rhs.get_type().const_int(-1i64 as u64, false); - - let special_block = env.context.append_basic_block(parent, "special_block"); - let default_block = env.context.append_basic_block(parent, "default_block"); - let cont_block = env.context.append_basic_block(parent, "branchcont"); - - bd.build_switch( - rhs, - default_block, - &[(zero, special_block), (neg_1, special_block)], - ); - - let condition_rem = { - bd.position_at_end(default_block); - - let rem = bd.build_int_signed_rem(lhs, rhs, "int_rem"); - let result = bd.build_int_compare(IntPredicate::EQ, rem, zero, "is_zero_rem"); - - bd.build_unconditional_branch(cont_block); - result - }; - - let condition_special = { - bd.position_at_end(special_block); - - let is_zero = bd.build_int_compare(IntPredicate::EQ, lhs, zero, "is_zero_lhs"); - let is_neg_one = - bd.build_int_compare(IntPredicate::EQ, rhs, neg_1, "is_neg_one_rhs"); - - let result = bd.build_or(is_neg_one, is_zero, "cond"); - - bd.build_unconditional_branch(cont_block); - - result - }; - - { - bd.position_at_end(cont_block); - - let phi = bd.build_phi(env.context.bool_type(), "branch"); - - phi.add_incoming(&[ - (&condition_rem, default_block), - (&condition_special, special_block), - ]); - - phi.as_basic_value() - } - } - NumPowInt => call_bitcode_fn( - env, - &[lhs.into(), rhs.into()], - &bitcode::NUM_POW_INT[int_width], - ), - NumDivUnchecked => { - if int_width.is_signed() { - bd.build_int_signed_div(lhs, rhs, "div_int").into() - } else { - bd.build_int_unsigned_div(lhs, rhs, "div_uint").into() - } - } - NumDivCeilUnchecked => call_bitcode_fn( - env, - &[lhs.into(), rhs.into()], - &bitcode::NUM_DIV_CEIL[int_width], - ), - NumBitwiseAnd => bd.build_and(lhs, rhs, "int_bitwise_and").into(), - NumBitwiseXor => bd.build_xor(lhs, rhs, "int_bitwise_xor").into(), - NumBitwiseOr => bd.build_or(lhs, rhs, "int_bitwise_or").into(), - NumShiftLeftBy => { - // NOTE arguments are flipped; - // we write `assert_eq!(0b0000_0001 << 0, 0b0000_0001);` - // as `Num.shiftLeftBy 0 0b0000_0001 - bd.build_left_shift(rhs, lhs, "int_shift_left").into() - } - NumShiftRightBy => { - // NOTE arguments are flipped; - bd.build_right_shift(rhs, lhs, true, "int_shift_right") - .into() - } - NumShiftRightZfBy => { - // NOTE arguments are flipped; - bd.build_right_shift(rhs, lhs, false, "int_shift_right_zf") - .into() - } - - _ => { - unreachable!("Unrecognized int binary operation: {:?}", op); - } - } -} - -pub fn build_num_binop<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - parent: FunctionValue<'ctx>, - lhs_arg: BasicValueEnum<'ctx>, - lhs_layout: &Layout<'a>, - rhs_arg: BasicValueEnum<'ctx>, - rhs_layout: &Layout<'a>, - op: LowLevel, -) -> BasicValueEnum<'ctx> { - match (lhs_layout, rhs_layout) { - (Layout::Builtin(lhs_builtin), Layout::Builtin(rhs_builtin)) - if lhs_builtin == rhs_builtin => - { - use roc_mono::layout::Builtin::*; - - match lhs_builtin { - Int(int_width) => build_int_binop( - env, - parent, - *int_width, - lhs_arg.into_int_value(), - rhs_arg.into_int_value(), - op, - ), - - Float(float_width) => build_float_binop( - env, - parent, - *float_width, - lhs_arg.into_float_value(), - rhs_arg.into_float_value(), - op, - ), - - Decimal => { - build_dec_binop(env, parent, lhs_arg, lhs_layout, rhs_arg, rhs_layout, op) - } - _ => { - unreachable!("Compiler bug: tried to run numeric operation {:?} on invalid builtin layout: ({:?})", op, lhs_layout); - } - } - } - _ => { - unreachable!("Compiler bug: tried to run numeric operation {:?} on invalid layouts. The 2 layouts were: ({:?}) and ({:?})", op, lhs_layout, rhs_layout); - } - } -} - -fn build_float_binop<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - parent: FunctionValue<'ctx>, - float_width: FloatWidth, - lhs: FloatValue<'ctx>, - rhs: FloatValue<'ctx>, - op: LowLevel, -) -> BasicValueEnum<'ctx> { - use inkwell::FloatPredicate::*; - use roc_module::low_level::LowLevel::*; - - let bd = env.builder; - - match op { - NumAdd => { - let builder = env.builder; - let context = env.context; - - let result = bd.build_float_add(lhs, rhs, "add_float"); - - let is_finite = - call_bitcode_fn(env, &[result.into()], &bitcode::NUM_IS_FINITE[float_width]) - .into_int_value(); - - let then_block = context.append_basic_block(parent, "then_block"); - let throw_block = context.append_basic_block(parent, "throw_block"); - - builder.build_conditional_branch(is_finite, then_block, throw_block); - - builder.position_at_end(throw_block); - - throw_exception(env, "float addition overflowed!"); - - builder.position_at_end(then_block); - - result.into() - } - NumAddChecked => { - let context = env.context; - - let result = bd.build_float_add(lhs, rhs, "add_float"); - - let is_finite = - call_bitcode_fn(env, &[result.into()], &bitcode::NUM_IS_FINITE[float_width]) - .into_int_value(); - let is_infinite = bd.build_not(is_finite, "negate"); - - let struct_type = context.struct_type( - &[context.f64_type().into(), context.bool_type().into()], - false, - ); - - let struct_value = { - let v1 = struct_type.const_zero(); - let v2 = bd.build_insert_value(v1, result, 0, "set_result").unwrap(); - let v3 = bd - .build_insert_value(v2, is_infinite, 1, "set_is_infinite") - .unwrap(); - - v3.into_struct_value() - }; - - struct_value.into() - } - NumAddWrap => unreachable!("wrapping addition is not defined on floats"), - NumSub => { - let builder = env.builder; - let context = env.context; - - let result = bd.build_float_sub(lhs, rhs, "sub_float"); - - let is_finite = - call_bitcode_fn(env, &[result.into()], &bitcode::NUM_IS_FINITE[float_width]) - .into_int_value(); - - let then_block = context.append_basic_block(parent, "then_block"); - let throw_block = context.append_basic_block(parent, "throw_block"); - - builder.build_conditional_branch(is_finite, then_block, throw_block); - - builder.position_at_end(throw_block); - - throw_exception(env, "float subtraction overflowed!"); - - builder.position_at_end(then_block); - - result.into() - } - NumSubChecked => { - let context = env.context; - - let result = bd.build_float_sub(lhs, rhs, "sub_float"); - - let is_finite = - call_bitcode_fn(env, &[result.into()], &bitcode::NUM_IS_FINITE[float_width]) - .into_int_value(); - let is_infinite = bd.build_not(is_finite, "negate"); - - let struct_type = context.struct_type( - &[context.f64_type().into(), context.bool_type().into()], - false, - ); - - let struct_value = { - let v1 = struct_type.const_zero(); - let v2 = bd.build_insert_value(v1, result, 0, "set_result").unwrap(); - let v3 = bd - .build_insert_value(v2, is_infinite, 1, "set_is_infinite") - .unwrap(); - - v3.into_struct_value() - }; - - struct_value.into() - } - NumSubWrap => unreachable!("wrapping subtraction is not defined on floats"), - NumMul => { - let builder = env.builder; - let context = env.context; - - let result = bd.build_float_mul(lhs, rhs, "mul_float"); - - let is_finite = - call_bitcode_fn(env, &[result.into()], &bitcode::NUM_IS_FINITE[float_width]) - .into_int_value(); - - let then_block = context.append_basic_block(parent, "then_block"); - let throw_block = context.append_basic_block(parent, "throw_block"); - - builder.build_conditional_branch(is_finite, then_block, throw_block); - - builder.position_at_end(throw_block); - - throw_exception(env, "float multiplication overflowed!"); - - builder.position_at_end(then_block); - - result.into() - } - NumMulChecked => { - let context = env.context; - - let result = bd.build_float_mul(lhs, rhs, "mul_float"); - - let is_finite = - call_bitcode_fn(env, &[result.into()], &bitcode::NUM_IS_FINITE[float_width]) - .into_int_value(); - let is_infinite = bd.build_not(is_finite, "negate"); - - let struct_type = context.struct_type( - &[context.f64_type().into(), context.bool_type().into()], - false, - ); - - let struct_value = { - let v1 = struct_type.const_zero(); - let v2 = bd.build_insert_value(v1, result, 0, "set_result").unwrap(); - let v3 = bd - .build_insert_value(v2, is_infinite, 1, "set_is_infinite") - .unwrap(); - - v3.into_struct_value() - }; - - struct_value.into() - } - NumMulWrap => unreachable!("wrapping multiplication is not defined on floats"), - NumGt => bd.build_float_compare(OGT, lhs, rhs, "float_gt").into(), - NumGte => bd.build_float_compare(OGE, lhs, rhs, "float_gte").into(), - NumLt => bd.build_float_compare(OLT, lhs, rhs, "float_lt").into(), - NumLte => bd.build_float_compare(OLE, lhs, rhs, "float_lte").into(), - NumDivUnchecked => bd.build_float_div(lhs, rhs, "div_float").into(), - NumPow => env.call_intrinsic(&LLVM_POW[float_width], &[lhs.into(), rhs.into()]), - _ => { - unreachable!("Unrecognized int binary operation: {:?}", op); - } - } -} - -fn dec_binop_with_overflow<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - fn_name: &str, - lhs: BasicValueEnum<'ctx>, - rhs: BasicValueEnum<'ctx>, -) -> StructValue<'ctx> { - let lhs = lhs.into_int_value(); - let rhs = rhs.into_int_value(); - - let return_type = zig_with_overflow_roc_dec(env); - let return_alloca = env.builder.build_alloca(return_type, "return_alloca"); - - let int_64 = env.context.i128_type().const_int(64, false); - let int_64_type = env.context.i64_type(); - - let lhs1 = env - .builder - .build_right_shift(lhs, int_64, false, "lhs_left_bits"); - let rhs1 = env - .builder - .build_right_shift(rhs, int_64, false, "rhs_left_bits"); - - call_void_bitcode_fn( - env, - &[ - return_alloca.into(), - env.builder.build_int_cast(lhs, int_64_type, "").into(), - env.builder.build_int_cast(lhs1, int_64_type, "").into(), - env.builder.build_int_cast(rhs, int_64_type, "").into(), - env.builder.build_int_cast(rhs1, int_64_type, "").into(), - ], - fn_name, - ); - - env.builder - .build_load(return_alloca, "load_dec") - .into_struct_value() -} - -pub fn dec_binop_with_unchecked<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - fn_name: &str, - lhs: BasicValueEnum<'ctx>, - rhs: BasicValueEnum<'ctx>, -) -> BasicValueEnum<'ctx> { - let lhs = lhs.into_int_value(); - let rhs = rhs.into_int_value(); - - let int_64 = env.context.i128_type().const_int(64, false); - let int_64_type = env.context.i64_type(); - - let lhs1 = env - .builder - .build_right_shift(lhs, int_64, false, "lhs_left_bits"); - let rhs1 = env - .builder - .build_right_shift(rhs, int_64, false, "rhs_left_bits"); - - call_bitcode_fn( - env, - &[ - env.builder.build_int_cast(lhs, int_64_type, "").into(), - env.builder.build_int_cast(lhs1, int_64_type, "").into(), - env.builder.build_int_cast(rhs, int_64_type, "").into(), - env.builder.build_int_cast(rhs1, int_64_type, "").into(), - ], - fn_name, - ) -} - -fn build_dec_binop<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - parent: FunctionValue<'ctx>, - lhs: BasicValueEnum<'ctx>, - _lhs_layout: &Layout<'a>, - rhs: BasicValueEnum<'ctx>, - _rhs_layout: &Layout<'a>, - op: LowLevel, -) -> BasicValueEnum<'ctx> { - use roc_module::low_level::LowLevel::*; - - match op { - NumAddChecked => call_bitcode_fn(env, &[lhs, rhs], bitcode::DEC_ADD_WITH_OVERFLOW), - NumSubChecked => call_bitcode_fn(env, &[lhs, rhs], bitcode::DEC_SUB_WITH_OVERFLOW), - NumMulChecked => call_bitcode_fn(env, &[lhs, rhs], bitcode::DEC_MUL_WITH_OVERFLOW), - NumAdd => build_dec_binop_throw_on_overflow( - env, - parent, - bitcode::DEC_ADD_WITH_OVERFLOW, - lhs, - rhs, - "decimal addition overflowed", - ), - NumSub => build_dec_binop_throw_on_overflow( - env, - parent, - bitcode::DEC_SUB_WITH_OVERFLOW, - lhs, - rhs, - "decimal subtraction overflowed", - ), - NumMul => build_dec_binop_throw_on_overflow( - env, - parent, - bitcode::DEC_MUL_WITH_OVERFLOW, - lhs, - rhs, - "decimal multiplication overflowed", - ), - NumDivUnchecked => dec_binop_with_unchecked(env, bitcode::DEC_DIV, lhs, rhs), - _ => { - unreachable!("Unrecognized int binary operation: {:?}", op); - } - } -} - -fn build_dec_binop_throw_on_overflow<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - parent: FunctionValue<'ctx>, - operation: &str, - lhs: BasicValueEnum<'ctx>, - rhs: BasicValueEnum<'ctx>, - message: &str, -) -> BasicValueEnum<'ctx> { - let result = dec_binop_with_overflow(env, operation, lhs, rhs); - - let value = throw_on_overflow(env, parent, result, message).into_struct_value(); - - env.builder.build_extract_value(value, 0, "num").unwrap() -} - -fn int_type_signed_min(int_type: IntType) -> IntValue { - let width = int_type.get_bit_width(); - - debug_assert!(width <= 128); - let shift = 128 - width as usize; - - if shift < 64 { - let min = i128::MIN >> shift; - let a = min as u64; - let b = (min >> 64) as u64; - - int_type.const_int_arbitrary_precision(&[b, a]) - } else { - int_type.const_int((i128::MIN >> shift) as u64, false) - } -} - -fn build_int_unary_op<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - arg: IntValue<'ctx>, - arg_width: IntWidth, - arg_int_type: IntType<'ctx>, - op: LowLevel, - return_layout: &Layout<'a>, -) -> BasicValueEnum<'ctx> { - use roc_module::low_level::LowLevel::*; - - let bd = env.builder; - - match op { - NumNeg => { - // integer abs overflows when applied to the minimum value of a signed type - int_neg_raise_on_overflow(env, arg, arg_int_type) - } - NumAbs => { - // integer abs overflows when applied to the minimum value of a signed type - int_abs_raise_on_overflow(env, arg, arg_int_type) - } - NumToFrac => { - // This is an Int, so we need to convert it. - - let target_float_type = match return_layout { - Layout::Builtin(Builtin::Float(float_width)) => { - convert::float_type_from_float_width(env, *float_width) - } - _ => internal_error!("There can only be floats here!"), - }; - - bd.build_cast( - InstructionOpcode::SIToFP, - arg, - target_float_type, - "i64_to_f64", - ) - } - NumToIntChecked => { - // return_layout : Result N [OutOfBounds]* ~ { result: N, out_of_bounds: bool } - - let target_int_width = match return_layout { - Layout::Struct { field_layouts, .. } if field_layouts.len() == 2 => { - debug_assert!(matches!(field_layouts[1], Layout::Builtin(Builtin::Bool))); - match field_layouts[0] { - Layout::Builtin(Builtin::Int(iw)) => iw, - layout => internal_error!( - "There can only be an int layout here, found {:?}!", - layout - ), - } - } - layout => internal_error!( - "There can only be a result layout here, found {:?}!", - layout - ), - }; - - let arg_always_fits_in_target = (arg_width.stack_size() < target_int_width.stack_size() - && ( - // If the arg is unsigned, it will always fit in either a signed or unsigned - // int of a larger width. - !arg_width.is_signed() - || - // Otherwise if the arg is signed, it will always fit in a signed int of a - // larger width. - (target_int_width.is_signed() ) - ) ) - || // Or if the two types are the same, they trivially fit. - arg_width == target_int_width; - - let return_type = - convert::basic_type_from_layout(env, return_layout).into_struct_type(); - - if arg_always_fits_in_target { - // This is guaranteed to succeed so we can just make it an int cast and let LLVM - // optimize it away. - let target_int_type = convert::int_type_from_int_width(env, target_int_width); - let target_int_val: BasicValueEnum<'ctx> = env - .builder - .build_int_cast_sign_flag( - arg, - target_int_type, - target_int_width.is_signed(), - "int_cast", - ) - .into(); - - let r = return_type.const_zero(); - let r = bd - .build_insert_value(r, target_int_val, 0, "converted_int") - .unwrap(); - let r = bd - .build_insert_value(r, env.context.bool_type().const_zero(), 1, "out_of_bounds") - .unwrap(); - - r.into_struct_value().into() - } else { - let bitcode_fn = if !arg_width.is_signed() { - // We are trying to convert from unsigned to signed/unsigned of same or lesser width, e.g. - // u16 -> i16, u16 -> i8, or u16 -> u8. We only need to check that the argument - // value fits in the MAX target type value. - &bitcode::NUM_INT_TO_INT_CHECKING_MAX[target_int_width][arg_width] - } else { - // We are trying to convert from signed to signed/unsigned of same or lesser width, e.g. - // i16 -> u16, i16 -> i8, or i16 -> u8. We need to check that the argument value fits in - // the MAX and MIN target type. - &bitcode::NUM_INT_TO_INT_CHECKING_MAX_AND_MIN[target_int_width][arg_width] - }; - - let result = call_bitcode_fn_fixing_for_convention( - env, - &[arg.into()], - return_layout, - bitcode_fn, - ); - - complex_bitcast_check_size(env, result, return_type.into(), "cast_bitpacked") - } - } - _ => { - unreachable!("Unrecognized int unary operation: {:?}", op); - } - } -} - -fn int_neg_raise_on_overflow<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - arg: IntValue<'ctx>, - int_type: IntType<'ctx>, -) -> BasicValueEnum<'ctx> { - let builder = env.builder; - - let min_val = int_type_signed_min(int_type); - let condition = builder.build_int_compare(IntPredicate::EQ, arg, min_val, "is_min_val"); - - let block = env.builder.get_insert_block().expect("to be in a function"); - let parent = block.get_parent().expect("to be in a function"); - let then_block = env.context.append_basic_block(parent, "then"); - let else_block = env.context.append_basic_block(parent, "else"); - - env.builder - .build_conditional_branch(condition, then_block, else_block); - - builder.position_at_end(then_block); - - throw_exception( - env, - "integer negation overflowed because its argument is the minimum value", - ); - - builder.position_at_end(else_block); - - builder.build_int_neg(arg, "negate_int").into() -} - -fn int_abs_raise_on_overflow<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - arg: IntValue<'ctx>, - int_type: IntType<'ctx>, -) -> BasicValueEnum<'ctx> { - let builder = env.builder; - - let min_val = int_type_signed_min(int_type); - let condition = builder.build_int_compare(IntPredicate::EQ, arg, min_val, "is_min_val"); - - let block = env.builder.get_insert_block().expect("to be in a function"); - let parent = block.get_parent().expect("to be in a function"); - let then_block = env.context.append_basic_block(parent, "then"); - let else_block = env.context.append_basic_block(parent, "else"); - - env.builder - .build_conditional_branch(condition, then_block, else_block); - - builder.position_at_end(then_block); - - throw_exception( - env, - "integer absolute overflowed because its argument is the minimum value", - ); - - builder.position_at_end(else_block); - - int_abs_with_overflow(env, arg, int_type) -} - -fn int_abs_with_overflow<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - arg: IntValue<'ctx>, - int_type: IntType<'ctx>, -) -> BasicValueEnum<'ctx> { - // This is how libc's abs() is implemented - it uses no branching! - // - // abs = \arg -> - // shifted = arg >>> 63 - // - // (xor arg shifted) - shifted - - let bd = env.builder; - let ctx = env.context; - let shifted_name = "abs_shift_right"; - let shifted_alloca = { - let bits_to_shift = int_type.get_bit_width() as u64 - 1; - let shift_val = ctx.i64_type().const_int(bits_to_shift, false); - let shifted = bd.build_right_shift(arg, shift_val, true, shifted_name); - let alloca = bd.build_alloca(int_type, "#int_abs_help"); - - // shifted = arg >>> 63 - bd.build_store(alloca, shifted); - - alloca - }; - - let xored_arg = bd.build_xor( - arg, - bd.build_load(shifted_alloca, shifted_name).into_int_value(), - "xor_arg_shifted", - ); - - BasicValueEnum::IntValue(bd.build_int_sub( - xored_arg, - bd.build_load(shifted_alloca, shifted_name).into_int_value(), - "sub_xored_shifted", - )) -} - -fn build_float_unary_op<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - layout: &Layout<'a>, - arg: FloatValue<'ctx>, - op: LowLevel, - float_width: FloatWidth, // arg width -) -> BasicValueEnum<'ctx> { - use roc_module::low_level::LowLevel::*; - - let bd = env.builder; - - // TODO: Handle different sized floats - match op { - NumNeg => bd.build_float_neg(arg, "negate_float").into(), - NumAbs => env.call_intrinsic(&LLVM_FABS[float_width], &[arg.into()]), - NumSqrtUnchecked => env.call_intrinsic(&LLVM_SQRT[float_width], &[arg.into()]), - NumLogUnchecked => env.call_intrinsic(&LLVM_LOG[float_width], &[arg.into()]), - NumToFrac => { - let return_width = match layout { - Layout::Builtin(Builtin::Float(return_width)) => *return_width, - _ => internal_error!("Layout for returning is not Float : {:?}", layout), - }; - match (float_width, return_width) { - (FloatWidth::F32, FloatWidth::F32) => arg.into(), - (FloatWidth::F32, FloatWidth::F64) => bd.build_cast( - InstructionOpcode::FPExt, - arg, - env.context.f64_type(), - "f32_to_f64", - ), - (FloatWidth::F64, FloatWidth::F32) => bd.build_cast( - InstructionOpcode::FPTrunc, - arg, - env.context.f32_type(), - "f64_to_f32", - ), - (FloatWidth::F64, FloatWidth::F64) => arg.into(), - (FloatWidth::F128, FloatWidth::F128) => arg.into(), - (FloatWidth::F128, _) => { - unimplemented!("I cannot handle F128 with Num.toFrac yet") - } - (_, FloatWidth::F128) => { - unimplemented!("I cannot handle F128 with Num.toFrac yet") - } - } - } - NumCeiling => { - let (return_signed, return_type) = match layout { - Layout::Builtin(Builtin::Int(int_width)) => ( - int_width.is_signed(), - convert::int_type_from_int_width(env, *int_width), - ), - _ => internal_error!("Ceiling return layout is not int: {:?}", layout), - }; - let opcode = if return_signed { - InstructionOpcode::FPToSI - } else { - InstructionOpcode::FPToUI - }; - env.builder.build_cast( - opcode, - env.call_intrinsic(&LLVM_CEILING[float_width], &[arg.into()]), - return_type, - "num_ceiling", - ) - } - NumFloor => { - let (return_signed, return_type) = match layout { - Layout::Builtin(Builtin::Int(int_width)) => ( - int_width.is_signed(), - convert::int_type_from_int_width(env, *int_width), - ), - _ => internal_error!("Ceiling return layout is not int: {:?}", layout), - }; - let opcode = if return_signed { - InstructionOpcode::FPToSI - } else { - InstructionOpcode::FPToUI - }; - env.builder.build_cast( - opcode, - env.call_intrinsic(&LLVM_FLOOR[float_width], &[arg.into()]), - return_type, - "num_floor", - ) - } - NumRound => { - let (return_signed, return_type) = match layout { - Layout::Builtin(Builtin::Int(int_width)) => ( - int_width.is_signed(), - convert::int_type_from_int_width(env, *int_width), - ), - _ => internal_error!("Ceiling return layout is not int: {:?}", layout), - }; - let opcode = if return_signed { - InstructionOpcode::FPToSI - } else { - InstructionOpcode::FPToUI - }; - env.builder.build_cast( - opcode, - env.call_intrinsic(&LLVM_ROUND[float_width], &[arg.into()]), - return_type, - "num_round", - ) - } - NumIsFinite => call_bitcode_fn(env, &[arg.into()], &bitcode::NUM_IS_FINITE[float_width]), - - // trigonometry - NumSin => env.call_intrinsic(&LLVM_SIN[float_width], &[arg.into()]), - NumCos => env.call_intrinsic(&LLVM_COS[float_width], &[arg.into()]), - - NumAtan => call_bitcode_fn(env, &[arg.into()], &bitcode::NUM_ATAN[float_width]), - NumAcos => call_bitcode_fn(env, &[arg.into()], &bitcode::NUM_ACOS[float_width]), - NumAsin => call_bitcode_fn(env, &[arg.into()], &bitcode::NUM_ASIN[float_width]), - - _ => { - unreachable!("Unrecognized int unary operation: {:?}", op); - } - } -} - -fn define_global_str_literal_ptr<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - message: &str, -) -> PointerValue<'ctx> { - let global = define_global_str_literal(env, message); - - let ptr = env - .builder - .build_bitcast( - global, - env.context.i8_type().ptr_type(AddressSpace::Generic), - "to_opaque", - ) - .into_pointer_value(); - - // a pointer to the first actual data (skipping over the refcount) - let ptr = unsafe { - env.builder.build_in_bounds_gep( - ptr, - &[env - .ptr_int() - .const_int(env.target_info.ptr_width() as u64, false)], - "get_rc_ptr", - ) - }; - - ptr -} - -fn define_global_str_literal<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - message: &str, -) -> inkwell::values::GlobalValue<'ctx> { - let module = env.module; - - // hash the name so we don't re-define existing messages - let name = { - use std::collections::hash_map::DefaultHasher; - use std::hash::{Hash, Hasher}; - - let mut hasher = DefaultHasher::new(); - message.hash(&mut hasher); - let hash = hasher.finish(); - - format!("_str_literal_{}", hash) - }; - - match module.get_global(&name) { - Some(current) => current, - - None => { - let size = message.bytes().len() + env.target_info.ptr_width() as usize; - let mut bytes = Vec::with_capacity_in(size, env.arena); - - // insert NULL bytes for the refcount - for _ in 0..env.target_info.ptr_width() as usize { - bytes.push(env.context.i8_type().const_zero()); - } - - // then add the data bytes - for b in message.bytes() { - bytes.push(env.context.i8_type().const_int(b as u64, false)); - } - - // use None for the address space (e.g. Const does not work) - let typ = env.context.i8_type().array_type(bytes.len() as u32); - let global = module.add_global(typ, None, &name); - - global.set_initializer(&env.context.i8_type().const_array(bytes.into_bump_slice())); - - // mimic the `global_string` function; we cannot use it directly because it assumes - // strings are NULL-terminated, which means we can't store the refcount (which is 8 - // NULL bytes) - global.set_constant(true); - global.set_alignment(env.target_info.ptr_width() as u32); - global.set_unnamed_addr(true); - global.set_linkage(inkwell::module::Linkage::Private); - - global - } - } -} - -fn define_global_error_str<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - message: &str, -) -> inkwell::values::GlobalValue<'ctx> { - let module = env.module; - - // hash the name so we don't re-define existing messages - let name = { - use std::collections::hash_map::DefaultHasher; - use std::hash::{Hash, Hasher}; - - let mut hasher = DefaultHasher::new(); - message.hash(&mut hasher); - let hash = hasher.finish(); - - format!("_Error_message_{}", hash) - }; - - match module.get_global(&name) { - Some(current) => current, - None => unsafe { env.builder.build_global_string(message, name.as_str()) }, - } -} - -fn throw_exception<'a, 'ctx, 'env>(env: &Env<'a, 'ctx, 'env>, message: &str) { - let builder = env.builder; - - // define the error message as a global - // (a hash is used such that the same value is not defined repeatedly) - let error_msg_global = define_global_error_str(env, message); - - let cast = env - .builder - .build_bitcast( - error_msg_global.as_pointer_value(), - env.context.i8_type().ptr_type(AddressSpace::Generic), - "cast_void", - ) - .into_pointer_value(); - - env.call_panic(cast, PanicTagId::NullTerminatedString); - - builder.build_unreachable(); -} - -fn get_foreign_symbol<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - foreign_symbol: roc_module::ident::ForeignSymbol, - function_spec: FunctionSpec<'ctx>, -) -> FunctionValue<'ctx> { - let module = env.module; - - match module.get_function(foreign_symbol.as_str()) { - Some(gvalue) => gvalue, - None => { - let foreign_function = add_func( - env.context, - module, - foreign_symbol.as_str(), - function_spec, - Linkage::External, - ); - - foreign_function - } - } -} - -/// Add a function to a module, after asserting that the function is unique. -/// We never want to define the same function twice in the same module! -/// The result can be bugs that are difficult to track down. -pub fn add_func<'ctx>( - ctx: &Context, - module: &Module<'ctx>, - name: &str, - spec: FunctionSpec<'ctx>, - linkage: Linkage, -) -> FunctionValue<'ctx> { - if cfg!(debug_assertions) { - if let Some(func) = module.get_function(name) { - panic!("Attempting to redefine LLVM function {}, which was already defined in this module as:\n\n{:?}", name, func); - } - } - - let fn_val = module.add_function(name, spec.typ, Some(linkage)); - - spec.attach_attributes(ctx, fn_val); - - fn_val -} diff --git a/compiler/gen_llvm/src/llvm/build_dict.rs b/compiler/gen_llvm/src/llvm/build_dict.rs deleted file mode 100644 index a7f4f123df..0000000000 --- a/compiler/gen_llvm/src/llvm/build_dict.rs +++ /dev/null @@ -1,839 +0,0 @@ -use crate::debug_info_init; -use crate::llvm::bitcode::{ - build_dec_wrapper, build_eq_wrapper, build_inc_wrapper, call_bitcode_fn, call_void_bitcode_fn, -}; -use crate::llvm::build::{ - complex_bitcast, load_roc_value, load_symbol, load_symbol_and_layout, Env, RocFunctionCall, - Scope, -}; -use crate::llvm::build_list::{layout_width, pass_as_opaque}; -use crate::llvm::convert::{basic_type_from_layout, zig_dict_type}; -use crate::llvm::refcounting::Mode; -use inkwell::attributes::{Attribute, AttributeLoc}; -use inkwell::context::Context; -use inkwell::types::BasicType; -use inkwell::values::{BasicValue, BasicValueEnum, FunctionValue, IntValue, StructValue}; -use inkwell::AddressSpace; -use roc_builtins::bitcode; -use roc_module::symbol::Symbol; -use roc_mono::layout::{Builtin, Layout, LayoutIds}; -use roc_target::TargetInfo; - -use super::bitcode::call_list_bitcode_fn; -use super::build::store_roc_value; -use super::build_list::list_to_c_abi; - -#[repr(transparent)] -struct Alignment(u8); - -impl Alignment { - fn from_key_value_layout(key: &Layout, value: &Layout, target_info: TargetInfo) -> Alignment { - let key_align = key.alignment_bytes(target_info); - let value_align = value.alignment_bytes(target_info); - - let mut bits = key_align.max(value_align) as u8; - - // alignment must be a power of 2 - debug_assert!(bits.is_power_of_two()); - - let value_before_key_flag = 0b1000_0000; - - if key_align < value_align { - bits |= value_before_key_flag; - } - - Alignment(bits) - } - - fn as_int_value<'ctx>(&self, context: &'ctx Context) -> IntValue<'ctx> { - context.i8_type().const_int(self.0 as u64, false) - } -} - -pub fn dict_len<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - scope: &Scope<'a, 'ctx>, - dict_symbol: Symbol, -) -> BasicValueEnum<'ctx> { - let (_, dict_layout) = load_symbol_and_layout(scope, &dict_symbol); - - match dict_layout { - Layout::Builtin(Builtin::Dict(_, _)) => { - // let dict_as_int = dict_symbol_to_i128(env, scope, dict_symbol); - let dict_as_zig_dict = dict_symbol_to_zig_dict(env, scope, dict_symbol); - - let length_i64 = call_bitcode_fn( - env, - &[pass_dict_c_abi(env, dict_as_zig_dict.into())], - bitcode::DICT_LEN, - ); - - env.builder - .build_int_cast_sign_flag( - length_i64.into_int_value(), - env.ptr_int(), - false, - "to_usize", - ) - .into() - } - _ => unreachable!("Invalid layout given to Dict.len : {:?}", dict_layout), - } -} - -pub fn dict_empty<'a, 'ctx, 'env>(env: &Env<'a, 'ctx, 'env>) -> BasicValueEnum<'ctx> { - // get the RocDict type defined by zig - let roc_dict_type = env.module.get_struct_type("dict.RocDict").unwrap(); - - // we must give a pointer for the bitcode function to write the result into - let result_alloc = env.builder.build_alloca(roc_dict_type, "dict_empty"); - - call_void_bitcode_fn(env, &[result_alloc.into()], bitcode::DICT_EMPTY); - - env.builder.build_load(result_alloc, "load_result") -} - -#[allow(clippy::too_many_arguments)] -pub fn dict_insert<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - layout_ids: &mut LayoutIds<'a>, - dict: BasicValueEnum<'ctx>, - key: BasicValueEnum<'ctx>, - key_layout: &Layout<'a>, - value: BasicValueEnum<'ctx>, - value_layout: &Layout<'a>, -) -> BasicValueEnum<'ctx> { - let builder = env.builder; - - let u8_ptr = env.context.i8_type().ptr_type(AddressSpace::Generic); - - let key_type = basic_type_from_layout(env, key_layout); - let value_type = basic_type_from_layout(env, value_layout); - - let key_ptr = builder.build_alloca(key_type, "key_ptr"); - let value_ptr = builder.build_alloca(value_type, "value_ptr"); - - store_roc_value(env, *key_layout, key_ptr, key); - store_roc_value(env, *value_layout, value_ptr, value); - - let key_width = env - .ptr_int() - .const_int(key_layout.stack_size(env.target_info) as u64, false); - - let value_width = env - .ptr_int() - .const_int(value_layout.stack_size(env.target_info) as u64, false); - - let result_ptr = builder.build_alloca(zig_dict_type(env), "result_ptr"); - - let alignment = Alignment::from_key_value_layout(key_layout, value_layout, env.target_info); - let alignment_iv = alignment.as_int_value(env.context); - - let hash_fn = build_hash_wrapper(env, layout_ids, key_layout); - let eq_fn = build_eq_wrapper(env, layout_ids, key_layout); - - let dec_key_fn = build_dec_wrapper(env, layout_ids, key_layout); - let dec_value_fn = build_dec_wrapper(env, layout_ids, value_layout); - - call_void_bitcode_fn( - env, - &[ - pass_dict_c_abi(env, dict), - alignment_iv.into(), - env.builder.build_bitcast(key_ptr, u8_ptr, "to_u8_ptr"), - key_width.into(), - env.builder.build_bitcast(value_ptr, u8_ptr, "to_u8_ptr"), - value_width.into(), - hash_fn.as_global_value().as_pointer_value().into(), - eq_fn.as_global_value().as_pointer_value().into(), - dec_key_fn.as_global_value().as_pointer_value().into(), - dec_value_fn.as_global_value().as_pointer_value().into(), - result_ptr.into(), - ], - bitcode::DICT_INSERT, - ); - - env.builder.build_load(result_ptr, "load_result") -} - -#[allow(clippy::too_many_arguments)] -pub fn dict_remove<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - layout_ids: &mut LayoutIds<'a>, - dict: BasicValueEnum<'ctx>, - key: BasicValueEnum<'ctx>, - key_layout: &Layout<'a>, - value_layout: &Layout<'a>, -) -> BasicValueEnum<'ctx> { - let builder = env.builder; - - let u8_ptr = env.context.i8_type().ptr_type(AddressSpace::Generic); - - let key_ptr = builder.build_alloca(key.get_type(), "key_ptr"); - - env.builder.build_store(key_ptr, key); - - let key_width = env - .ptr_int() - .const_int(key_layout.stack_size(env.target_info) as u64, false); - - let value_width = env - .ptr_int() - .const_int(value_layout.stack_size(env.target_info) as u64, false); - - let result_ptr = builder.build_alloca(zig_dict_type(env), "result_ptr"); - - let alignment = Alignment::from_key_value_layout(key_layout, value_layout, env.target_info); - let alignment_iv = alignment.as_int_value(env.context); - - let hash_fn = build_hash_wrapper(env, layout_ids, key_layout); - let eq_fn = build_eq_wrapper(env, layout_ids, key_layout); - - let dec_key_fn = build_dec_wrapper(env, layout_ids, key_layout); - let dec_value_fn = build_dec_wrapper(env, layout_ids, value_layout); - - call_void_bitcode_fn( - env, - &[ - pass_dict_c_abi(env, dict), - alignment_iv.into(), - env.builder.build_bitcast(key_ptr, u8_ptr, "to_u8_ptr"), - key_width.into(), - value_width.into(), - hash_fn.as_global_value().as_pointer_value().into(), - eq_fn.as_global_value().as_pointer_value().into(), - dec_key_fn.as_global_value().as_pointer_value().into(), - dec_value_fn.as_global_value().as_pointer_value().into(), - result_ptr.into(), - ], - bitcode::DICT_REMOVE, - ); - - env.builder.build_load(result_ptr, "load_result") -} - -#[allow(clippy::too_many_arguments)] -pub fn dict_contains<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - layout_ids: &mut LayoutIds<'a>, - dict: BasicValueEnum<'ctx>, - key: BasicValueEnum<'ctx>, - key_layout: &Layout<'a>, - value_layout: &Layout<'a>, -) -> BasicValueEnum<'ctx> { - let builder = env.builder; - - let u8_ptr = env.context.i8_type().ptr_type(AddressSpace::Generic); - - let key_ptr = builder.build_alloca(key.get_type(), "key_ptr"); - - env.builder.build_store(key_ptr, key); - - let key_width = env - .ptr_int() - .const_int(key_layout.stack_size(env.target_info) as u64, false); - - let value_width = env - .ptr_int() - .const_int(value_layout.stack_size(env.target_info) as u64, false); - - let alignment = Alignment::from_key_value_layout(key_layout, value_layout, env.target_info); - let alignment_iv = alignment.as_int_value(env.context); - - let hash_fn = build_hash_wrapper(env, layout_ids, key_layout); - let eq_fn = build_eq_wrapper(env, layout_ids, key_layout); - - call_bitcode_fn( - env, - &[ - pass_dict_c_abi(env, dict), - alignment_iv.into(), - env.builder.build_bitcast(key_ptr, u8_ptr, "to_u8_ptr"), - key_width.into(), - value_width.into(), - hash_fn.as_global_value().as_pointer_value().into(), - eq_fn.as_global_value().as_pointer_value().into(), - ], - bitcode::DICT_CONTAINS, - ) -} - -#[allow(clippy::too_many_arguments)] -pub fn dict_get<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - layout_ids: &mut LayoutIds<'a>, - dict: BasicValueEnum<'ctx>, - key: BasicValueEnum<'ctx>, - key_layout: &Layout<'a>, - value_layout: &Layout<'a>, -) -> BasicValueEnum<'ctx> { - let builder = env.builder; - - let u8_ptr = env.context.i8_type().ptr_type(AddressSpace::Generic); - - let key_ptr = builder.build_alloca(key.get_type(), "key_ptr"); - - env.builder.build_store(key_ptr, key); - - let key_width = env - .ptr_int() - .const_int(key_layout.stack_size(env.target_info) as u64, false); - - let value_width = env - .ptr_int() - .const_int(value_layout.stack_size(env.target_info) as u64, false); - - let value_bt = basic_type_from_layout(env, value_layout); - - let alignment = Alignment::from_key_value_layout(key_layout, value_layout, env.target_info); - let alignment_iv = alignment.as_int_value(env.context); - - let hash_fn = build_hash_wrapper(env, layout_ids, key_layout); - let eq_fn = build_eq_wrapper(env, layout_ids, key_layout); - - let inc_value_fn = build_inc_wrapper(env, layout_ids, value_layout); - - // { flag: bool, value: *const u8 } - let result = call_bitcode_fn( - env, - &[ - pass_dict_c_abi(env, dict), - alignment_iv.into(), - env.builder.build_bitcast(key_ptr, u8_ptr, "to_u8_ptr"), - key_width.into(), - value_width.into(), - hash_fn.as_global_value().as_pointer_value().into(), - eq_fn.as_global_value().as_pointer_value().into(), - inc_value_fn.as_global_value().as_pointer_value().into(), - ], - bitcode::DICT_GET, - ) - .into_struct_value(); - - let flag_u8 = env - .builder - .build_extract_value(result, 1, "get_flag") - .unwrap() - .into_int_value(); - - let flag = env - .builder - .build_int_cast(flag_u8, env.context.bool_type(), "to_bool"); - - let value_u8_ptr_int = env - .builder - .build_extract_value(result, 0, "get_value_ptr_int") - .unwrap() - .into_int_value(); - - let ptr_type = value_bt.ptr_type(AddressSpace::Generic); - let value_u8_ptr = env - .builder - .build_int_to_ptr(value_u8_ptr_int, ptr_type, "opaque_value_ptr"); - - let start_block = env.builder.get_insert_block().unwrap(); - let parent = start_block.get_parent().unwrap(); - - let if_not_null = env.context.append_basic_block(parent, "if_not_null"); - let done_block = env.context.append_basic_block(parent, "done"); - - let default = value_bt.const_zero(); - - env.builder - .build_conditional_branch(flag, if_not_null, done_block); - - env.builder.position_at_end(if_not_null); - let value_ptr = env - .builder - .build_bitcast( - value_u8_ptr, - value_bt.ptr_type(AddressSpace::Generic), - "from_opaque", - ) - .into_pointer_value(); - let loaded = env.builder.build_load(value_ptr, "load_value"); - env.builder.build_unconditional_branch(done_block); - - env.builder.position_at_end(done_block); - let result_phi = env.builder.build_phi(value_bt, "result"); - - result_phi.add_incoming(&[(&default, start_block), (&loaded, if_not_null)]); - - let value = result_phi.as_basic_value(); - - let result = env - .context - .struct_type(&[value_bt, env.context.bool_type().into()], false) - .const_zero(); - - let result = env - .builder - .build_insert_value(result, flag, 1, "insert_flag") - .unwrap(); - - env.builder - .build_insert_value(result, value, 0, "insert_value") - .unwrap() - .into_struct_value() - .into() -} - -#[allow(clippy::too_many_arguments)] -pub fn dict_elements_rc<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - layout_ids: &mut LayoutIds<'a>, - dict: BasicValueEnum<'ctx>, - key_layout: &Layout<'a>, - value_layout: &Layout<'a>, - rc_operation: Mode, -) { - let key_width = env - .ptr_int() - .const_int(key_layout.stack_size(env.target_info) as u64, false); - - let value_width = env - .ptr_int() - .const_int(value_layout.stack_size(env.target_info) as u64, false); - - let alignment = Alignment::from_key_value_layout(key_layout, value_layout, env.target_info); - let alignment_iv = alignment.as_int_value(env.context); - - let (key_fn, value_fn) = match rc_operation { - Mode::Inc => ( - build_inc_wrapper(env, layout_ids, key_layout), - build_inc_wrapper(env, layout_ids, value_layout), - ), - Mode::Dec => ( - build_dec_wrapper(env, layout_ids, key_layout), - build_dec_wrapper(env, layout_ids, value_layout), - ), - }; - - call_void_bitcode_fn( - env, - &[ - pass_dict_c_abi(env, dict), - alignment_iv.into(), - key_width.into(), - value_width.into(), - key_fn.as_global_value().as_pointer_value().into(), - value_fn.as_global_value().as_pointer_value().into(), - ], - bitcode::DICT_ELEMENTS_RC, - ); -} - -#[allow(clippy::too_many_arguments)] -pub fn dict_keys<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - layout_ids: &mut LayoutIds<'a>, - dict: BasicValueEnum<'ctx>, - key_layout: &Layout<'a>, - value_layout: &Layout<'a>, -) -> BasicValueEnum<'ctx> { - let key_width = env - .ptr_int() - .const_int(key_layout.stack_size(env.target_info) as u64, false); - - let value_width = env - .ptr_int() - .const_int(value_layout.stack_size(env.target_info) as u64, false); - - let alignment = Alignment::from_key_value_layout(key_layout, value_layout, env.target_info); - let alignment_iv = alignment.as_int_value(env.context); - - let inc_key_fn = build_inc_wrapper(env, layout_ids, key_layout); - - call_list_bitcode_fn( - env, - &[ - pass_dict_c_abi(env, dict), - alignment_iv.into(), - key_width.into(), - value_width.into(), - inc_key_fn.as_global_value().as_pointer_value().into(), - ], - bitcode::DICT_KEYS, - ) -} - -fn pass_dict_c_abi<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - dict: BasicValueEnum<'ctx>, -) -> BasicValueEnum<'ctx> { - match env.target_info.ptr_width() { - roc_target::PtrWidth::Bytes4 => { - let target_type = env.context.custom_width_int_type(96).into(); - - complex_bitcast(env.builder, dict, target_type, "to_i96") - } - roc_target::PtrWidth::Bytes8 => { - let dict_ptr = env.builder.build_alloca(zig_dict_type(env), "dict_ptr"); - env.builder.build_store(dict_ptr, dict); - - dict_ptr.into() - } - } -} - -#[allow(clippy::too_many_arguments)] -pub fn dict_union<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - layout_ids: &mut LayoutIds<'a>, - dict1: BasicValueEnum<'ctx>, - dict2: BasicValueEnum<'ctx>, - key_layout: &Layout<'a>, - value_layout: &Layout<'a>, -) -> BasicValueEnum<'ctx> { - let builder = env.builder; - - let key_width = env - .ptr_int() - .const_int(key_layout.stack_size(env.target_info) as u64, false); - - let value_width = env - .ptr_int() - .const_int(value_layout.stack_size(env.target_info) as u64, false); - - let alignment = Alignment::from_key_value_layout(key_layout, value_layout, env.target_info); - let alignment_iv = alignment.as_int_value(env.context); - - let hash_fn = build_hash_wrapper(env, layout_ids, key_layout); - let eq_fn = build_eq_wrapper(env, layout_ids, key_layout); - - let inc_key_fn = build_inc_wrapper(env, layout_ids, key_layout); - let inc_value_fn = build_inc_wrapper(env, layout_ids, value_layout); - - let output_ptr = builder.build_alloca(zig_dict_type(env), "output_ptr"); - - call_void_bitcode_fn( - env, - &[ - pass_dict_c_abi(env, dict1), - pass_dict_c_abi(env, dict2), - alignment_iv.into(), - key_width.into(), - value_width.into(), - hash_fn.as_global_value().as_pointer_value().into(), - eq_fn.as_global_value().as_pointer_value().into(), - inc_key_fn.as_global_value().as_pointer_value().into(), - inc_value_fn.as_global_value().as_pointer_value().into(), - output_ptr.into(), - ], - bitcode::DICT_UNION, - ); - - env.builder.build_load(output_ptr, "load_output_ptr") -} - -#[allow(clippy::too_many_arguments)] -pub fn dict_difference<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - layout_ids: &mut LayoutIds<'a>, - dict1: BasicValueEnum<'ctx>, - dict2: BasicValueEnum<'ctx>, - key_layout: &Layout<'a>, - value_layout: &Layout<'a>, -) -> BasicValueEnum<'ctx> { - dict_intersect_or_difference( - env, - layout_ids, - dict1, - dict2, - key_layout, - value_layout, - bitcode::DICT_DIFFERENCE, - ) -} - -#[allow(clippy::too_many_arguments)] -pub fn dict_intersection<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - layout_ids: &mut LayoutIds<'a>, - dict1: BasicValueEnum<'ctx>, - dict2: BasicValueEnum<'ctx>, - key_layout: &Layout<'a>, - value_layout: &Layout<'a>, -) -> BasicValueEnum<'ctx> { - dict_intersect_or_difference( - env, - layout_ids, - dict1, - dict2, - key_layout, - value_layout, - bitcode::DICT_INTERSECTION, - ) -} - -#[allow(clippy::too_many_arguments)] -fn dict_intersect_or_difference<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - layout_ids: &mut LayoutIds<'a>, - dict1: BasicValueEnum<'ctx>, - dict2: BasicValueEnum<'ctx>, - key_layout: &Layout<'a>, - value_layout: &Layout<'a>, - op: &str, -) -> BasicValueEnum<'ctx> { - let builder = env.builder; - - let zig_dict_type = env.module.get_struct_type("dict.RocDict").unwrap(); - - let key_width = env - .ptr_int() - .const_int(key_layout.stack_size(env.target_info) as u64, false); - - let value_width = env - .ptr_int() - .const_int(value_layout.stack_size(env.target_info) as u64, false); - - let alignment = Alignment::from_key_value_layout(key_layout, value_layout, env.target_info); - let alignment_iv = alignment.as_int_value(env.context); - - let hash_fn = build_hash_wrapper(env, layout_ids, key_layout); - let eq_fn = build_eq_wrapper(env, layout_ids, key_layout); - - let dec_key_fn = build_dec_wrapper(env, layout_ids, key_layout); - let dec_value_fn = build_dec_wrapper(env, layout_ids, value_layout); - - let output_ptr = builder.build_alloca(zig_dict_type, "output_ptr"); - - call_void_bitcode_fn( - env, - &[ - pass_dict_c_abi(env, dict1), - pass_dict_c_abi(env, dict2), - alignment_iv.into(), - key_width.into(), - value_width.into(), - hash_fn.as_global_value().as_pointer_value().into(), - eq_fn.as_global_value().as_pointer_value().into(), - dec_key_fn.as_global_value().as_pointer_value().into(), - dec_value_fn.as_global_value().as_pointer_value().into(), - output_ptr.into(), - ], - op, - ); - - env.builder.build_load(output_ptr, "load_output_ptr") -} - -#[allow(clippy::too_many_arguments)] -pub fn dict_walk<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - roc_function_call: RocFunctionCall<'ctx>, - dict: BasicValueEnum<'ctx>, - accum: BasicValueEnum<'ctx>, - key_layout: &Layout<'a>, - value_layout: &Layout<'a>, - accum_layout: &Layout<'a>, -) -> BasicValueEnum<'ctx> { - let builder = env.builder; - - let u8_ptr = env.context.i8_type().ptr_type(AddressSpace::Generic); - - let accum_bt = basic_type_from_layout(env, accum_layout); - let accum_ptr = builder.build_alloca(accum_bt, "accum_ptr"); - env.builder.build_store(accum_ptr, accum); - - let alignment = Alignment::from_key_value_layout(key_layout, value_layout, env.target_info); - let alignment_iv = alignment.as_int_value(env.context); - - let output_ptr = builder.build_alloca(accum_bt, "output_ptr"); - - call_void_bitcode_fn( - env, - &[ - pass_dict_c_abi(env, dict), - roc_function_call.caller.into(), - pass_as_opaque(env, roc_function_call.data), - roc_function_call.inc_n_data.into(), - roc_function_call.data_is_owned.into(), - env.builder.build_bitcast(accum_ptr, u8_ptr, "to_opaque"), - alignment_iv.into(), - layout_width(env, key_layout), - layout_width(env, value_layout), - layout_width(env, accum_layout), - env.builder.build_bitcast(output_ptr, u8_ptr, "to_opaque"), - ], - bitcode::DICT_WALK, - ); - - env.builder.build_load(output_ptr, "load_output_ptr") -} - -#[allow(clippy::too_many_arguments)] -pub fn dict_values<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - layout_ids: &mut LayoutIds<'a>, - dict: BasicValueEnum<'ctx>, - key_layout: &Layout<'a>, - value_layout: &Layout<'a>, -) -> BasicValueEnum<'ctx> { - let key_width = env - .ptr_int() - .const_int(key_layout.stack_size(env.target_info) as u64, false); - - let value_width = env - .ptr_int() - .const_int(value_layout.stack_size(env.target_info) as u64, false); - - let alignment = Alignment::from_key_value_layout(key_layout, value_layout, env.target_info); - let alignment_iv = alignment.as_int_value(env.context); - - let inc_value_fn = build_inc_wrapper(env, layout_ids, value_layout); - - call_list_bitcode_fn( - env, - &[ - pass_dict_c_abi(env, dict), - alignment_iv.into(), - key_width.into(), - value_width.into(), - inc_value_fn.as_global_value().as_pointer_value().into(), - ], - bitcode::DICT_VALUES, - ) -} - -#[allow(clippy::too_many_arguments)] -pub fn set_from_list<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - layout_ids: &mut LayoutIds<'a>, - list: BasicValueEnum<'ctx>, - key_layout: &Layout<'a>, -) -> BasicValueEnum<'ctx> { - let builder = env.builder; - - let key_width = env - .ptr_int() - .const_int(key_layout.stack_size(env.target_info) as u64, false); - - let value_width = env.ptr_int().const_zero(); - - let result_alloca = builder.build_alloca(zig_dict_type(env), "result_alloca"); - - let alignment = Alignment::from_key_value_layout(key_layout, &Layout::UNIT, env.target_info); - let alignment_iv = alignment.as_int_value(env.context); - - let hash_fn = build_hash_wrapper(env, layout_ids, key_layout); - let eq_fn = build_eq_wrapper(env, layout_ids, key_layout); - - let dec_key_fn = build_dec_wrapper(env, layout_ids, key_layout); - - call_void_bitcode_fn( - env, - &[ - list_to_c_abi(env, list).into(), - alignment_iv.into(), - key_width.into(), - value_width.into(), - hash_fn.as_global_value().as_pointer_value().into(), - eq_fn.as_global_value().as_pointer_value().into(), - dec_key_fn.as_global_value().as_pointer_value().into(), - result_alloca.into(), - ], - bitcode::SET_FROM_LIST, - ); - - env.builder.build_load(result_alloca, "load_result") -} - -fn build_hash_wrapper<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - layout_ids: &mut LayoutIds<'a>, - layout: &Layout<'a>, -) -> FunctionValue<'ctx> { - let block = env.builder.get_insert_block().expect("to be in a function"); - let di_location = env.builder.get_current_debug_location().unwrap(); - - let symbol = Symbol::GENERIC_HASH_REF; - let fn_name = layout_ids - .get(symbol, layout) - .to_symbol_string(symbol, &env.interns); - - let function_value = match env.module.get_function(fn_name.as_str()) { - Some(function_value) => function_value, - None => { - let seed_type = env.context.i64_type(); - let arg_type = env.context.i8_type().ptr_type(AddressSpace::Generic); - - let function_value = crate::llvm::refcounting::build_header_help( - env, - &fn_name, - seed_type.into(), - &[seed_type.into(), arg_type.into()], - ); - - let kind_id = Attribute::get_named_enum_kind_id("alwaysinline"); - debug_assert!(kind_id > 0); - let attr = env.context.create_enum_attribute(kind_id, 1); - function_value.add_attribute(AttributeLoc::Function, attr); - - let entry = env.context.append_basic_block(function_value, "entry"); - env.builder.position_at_end(entry); - - debug_info_init!(env, function_value); - - let mut it = function_value.get_param_iter(); - let seed_arg = it.next().unwrap().into_int_value(); - let value_ptr = it.next().unwrap().into_pointer_value(); - - seed_arg.set_name(Symbol::ARG_1.as_str(&env.interns)); - value_ptr.set_name(Symbol::ARG_2.as_str(&env.interns)); - - let value_type = basic_type_from_layout(env, layout).ptr_type(AddressSpace::Generic); - - let value_cast = env - .builder - .build_bitcast(value_ptr, value_type, "cast_to_known_type") - .into_pointer_value(); - - let val_arg = load_roc_value(env, *layout, value_cast, "load_opaque"); - - let result = - crate::llvm::build_hash::generic_hash(env, layout_ids, seed_arg, val_arg, layout); - - env.builder.build_return(Some(&result)); - - function_value - } - }; - - env.builder.position_at_end(block); - env.builder - .set_current_debug_location(env.context, di_location); - - function_value -} - -fn dict_symbol_to_zig_dict<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - scope: &Scope<'a, 'ctx>, - symbol: Symbol, -) -> StructValue<'ctx> { - let dict = load_symbol(scope, &symbol); - - complex_bitcast( - env.builder, - dict, - crate::llvm::convert::zig_dict_type(env).into(), - "dict_to_zig_dict", - ) - .into_struct_value() -} - -pub fn decref<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - wrapper_struct: StructValue<'ctx>, - alignment: u32, -) { - let pointer = env - .builder - .build_extract_value(wrapper_struct, Builtin::WRAPPER_PTR, "read_list_ptr") - .unwrap() - .into_pointer_value(); - - crate::llvm::refcounting::decref_pointer_check_null(env, pointer, alignment); -} diff --git a/compiler/gen_llvm/src/llvm/build_hash.rs b/compiler/gen_llvm/src/llvm/build_hash.rs deleted file mode 100644 index b57ec76101..0000000000 --- a/compiler/gen_llvm/src/llvm/build_hash.rs +++ /dev/null @@ -1,874 +0,0 @@ -use crate::debug_info_init; -use crate::llvm::bitcode::call_bitcode_fn; -use crate::llvm::build::tag_pointer_clear_tag_id; -use crate::llvm::build::Env; -use crate::llvm::build::{get_tag_id, FAST_CALL_CONV, TAG_DATA_INDEX}; -use crate::llvm::convert::basic_type_from_layout; -use bumpalo::collections::Vec; -use inkwell::values::{ - BasicValue, BasicValueEnum, FunctionValue, IntValue, PointerValue, StructValue, -}; -use roc_builtins::bitcode; -use roc_module::symbol::Symbol; -use roc_mono::layout::{Builtin, Layout, LayoutIds, UnionLayout}; - -use super::build::use_roc_value; -use super::convert::argument_type_from_union_layout; - -#[derive(Clone, Debug)] -enum WhenRecursive<'a> { - Unreachable, - Loop(UnionLayout<'a>), -} - -pub fn generic_hash<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - layout_ids: &mut LayoutIds<'a>, - seed: IntValue<'ctx>, - val: BasicValueEnum<'ctx>, - layout: &Layout<'a>, -) -> IntValue<'ctx> { - // NOTE: C and Zig use this value for their initial HashMap seed: 0xc70f6907 - build_hash_layout( - env, - layout_ids, - seed, - val, - layout, - WhenRecursive::Unreachable, - ) -} - -fn build_hash_layout<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - layout_ids: &mut LayoutIds<'a>, - seed: IntValue<'ctx>, - val: BasicValueEnum<'ctx>, - layout: &Layout<'a>, - when_recursive: WhenRecursive<'a>, -) -> IntValue<'ctx> { - match layout { - Layout::Builtin(builtin) => { - hash_builtin(env, layout_ids, seed, val, layout, builtin, when_recursive) - } - - Layout::Struct { field_layouts, .. } => build_hash_struct( - env, - layout_ids, - field_layouts, - when_recursive, - seed, - val.into_struct_value(), - ), - - Layout::LambdaSet(lambda_set) => build_hash_layout( - env, - layout_ids, - seed, - val, - &lambda_set.runtime_representation(), - when_recursive, - ), - - Layout::Union(union_layout) => build_hash_tag(env, layout_ids, union_layout, seed, val), - - Layout::Boxed(_inner_layout) => { - // build_hash_box(env, layout_ids, layout, inner_layout, seed, val) - todo!() - } - - Layout::RecursivePointer => match when_recursive { - WhenRecursive::Unreachable => { - unreachable!("recursion pointers should never be hashed directly") - } - WhenRecursive::Loop(union_layout) => { - let layout = Layout::Union(union_layout); - - let bt = basic_type_from_layout(env, &layout); - - // cast the i64 pointer to a pointer to block of memory - let field_cast = env - .builder - .build_bitcast(val, bt, "i64_to_opaque") - .into_pointer_value(); - - build_hash_tag(env, layout_ids, &union_layout, seed, field_cast.into()) - } - }, - } -} - -fn append_hash_layout<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - layout_ids: &mut LayoutIds<'a>, - seed: IntValue<'ctx>, - val: BasicValueEnum<'ctx>, - layout: &Layout<'a>, - when_recursive: WhenRecursive<'a>, -) -> IntValue<'ctx> { - build_hash_layout(env, layout_ids, seed, val, layout, when_recursive) -} - -fn hash_builtin<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - layout_ids: &mut LayoutIds<'a>, - seed: IntValue<'ctx>, - val: BasicValueEnum<'ctx>, - layout: &Layout<'a>, - builtin: &Builtin<'a>, - when_recursive: WhenRecursive<'a>, -) -> IntValue<'ctx> { - let ptr_bytes = env.target_info; - - match builtin { - Builtin::Int(_) | Builtin::Float(_) | Builtin::Bool | Builtin::Decimal => { - let hash_bytes = store_and_use_as_u8_ptr(env, val, layout); - hash_bitcode_fn(env, seed, hash_bytes, layout.stack_size(ptr_bytes)) - } - Builtin::Str => { - // let zig deal with big vs small string - call_bitcode_fn(env, &[seed.into(), val], bitcode::DICT_HASH_STR).into_int_value() - } - - Builtin::Dict(_, _) => { - todo!("Implement hash for Dict") - } - Builtin::Set(_) => { - todo!("Implement Hash for Set") - } - Builtin::List(element_layout) => build_hash_list( - env, - layout_ids, - layout, - element_layout, - when_recursive, - seed, - val.into_struct_value(), - ), - } -} - -fn build_hash_struct<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - layout_ids: &mut LayoutIds<'a>, - field_layouts: &'a [Layout<'a>], - when_recursive: WhenRecursive<'a>, - seed: IntValue<'ctx>, - value: StructValue<'ctx>, -) -> IntValue<'ctx> { - let block = env.builder.get_insert_block().expect("to be in a function"); - let di_location = env.builder.get_current_debug_location().unwrap(); - - let struct_layout = Layout::struct_no_name_order(field_layouts); - - let symbol = Symbol::GENERIC_HASH; - let fn_name = layout_ids - .get(symbol, &struct_layout) - .to_symbol_string(symbol, &env.interns); - - let function = match env.module.get_function(fn_name.as_str()) { - Some(function_value) => function_value, - None => { - let seed_type = env.context.i64_type(); - - let arg_type = basic_type_from_layout(env, &struct_layout); - - let function_value = crate::llvm::refcounting::build_header_help( - env, - &fn_name, - seed_type.into(), - &[seed_type.into(), arg_type], - ); - - build_hash_struct_help( - env, - layout_ids, - function_value, - when_recursive, - field_layouts, - ); - - function_value - } - }; - - env.builder.position_at_end(block); - env.builder - .set_current_debug_location(env.context, di_location); - let call = env - .builder - .build_call(function, &[seed.into(), value.into()], "struct_hash"); - - call.set_call_convention(FAST_CALL_CONV); - - call.try_as_basic_value().left().unwrap().into_int_value() -} - -fn build_hash_struct_help<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - layout_ids: &mut LayoutIds<'a>, - parent: FunctionValue<'ctx>, - when_recursive: WhenRecursive<'a>, - field_layouts: &[Layout<'a>], -) { - let ctx = env.context; - - debug_info_init!(env, parent); - - // Add args to scope - let mut it = parent.get_param_iter(); - let seed = it.next().unwrap().into_int_value(); - let value = it.next().unwrap().into_struct_value(); - - seed.set_name(Symbol::ARG_1.as_str(&env.interns)); - value.set_name(Symbol::ARG_2.as_str(&env.interns)); - - let entry = ctx.append_basic_block(parent, "entry"); - env.builder.position_at_end(entry); - - let result = hash_struct(env, layout_ids, seed, value, when_recursive, field_layouts); - - env.builder.build_return(Some(&result)); -} - -fn hash_struct<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - layout_ids: &mut LayoutIds<'a>, - mut seed: IntValue<'ctx>, - value: StructValue<'ctx>, - when_recursive: WhenRecursive<'a>, - field_layouts: &[Layout<'a>], -) -> IntValue<'ctx> { - let ptr_bytes = env.target_info; - - let layout = Layout::struct_no_name_order(field_layouts); - - // Optimization: if the bit representation of equal values is the same - // just hash the bits. Caveat here is tags: e.g. `Nothing` in `Just a` - // contains garbage bits after the tag (currently) - if false { - // this is a struct of only basic types, so we can just hash its bits - let hash_bytes = store_and_use_as_u8_ptr(env, value.into(), &layout); - hash_bitcode_fn(env, seed, hash_bytes, layout.stack_size(ptr_bytes)) - } else { - for (index, field_layout) in field_layouts.iter().enumerate() { - let field = env - .builder - .build_extract_value(value, index as u32, "hash_field") - .unwrap(); - - let field = use_roc_value(env, *field_layout, field, "store_field_for_hashing"); - - if let Layout::RecursivePointer = field_layout { - match &when_recursive { - WhenRecursive::Unreachable => { - unreachable!("The current layout should not be recursive, but is") - } - WhenRecursive::Loop(union_layout) => { - let field_layout = Layout::Union(*union_layout); - - let bt = basic_type_from_layout(env, &field_layout); - - // cast the i64 pointer to a pointer to block of memory - let field_cast = env - .builder - .build_bitcast(field, bt, "i64_to_opaque") - .into_pointer_value(); - - seed = append_hash_layout( - env, - layout_ids, - seed, - field_cast.into(), - &field_layout, - when_recursive.clone(), - ) - } - } - } else { - seed = append_hash_layout( - env, - layout_ids, - seed, - field, - field_layout, - when_recursive.clone(), - ); - } - } - seed - } -} - -fn build_hash_tag<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - layout_ids: &mut LayoutIds<'a>, - union_layout: &UnionLayout<'a>, - seed: IntValue<'ctx>, - value: BasicValueEnum<'ctx>, -) -> IntValue<'ctx> { - let block = env.builder.get_insert_block().expect("to be in a function"); - let di_location = env.builder.get_current_debug_location().unwrap(); - - let symbol = Symbol::GENERIC_HASH; - let fn_name = layout_ids - .get(symbol, &Layout::Union(*union_layout)) - .to_symbol_string(symbol, &env.interns); - - let function = match env.module.get_function(fn_name.as_str()) { - Some(function_value) => function_value, - None => { - let seed_type = env.context.i64_type(); - - let arg_type = argument_type_from_union_layout(env, union_layout); - - let function_value = crate::llvm::refcounting::build_header_help( - env, - &fn_name, - seed_type.into(), - &[seed_type.into(), arg_type], - ); - - build_hash_tag_help(env, layout_ids, function_value, union_layout); - - function_value - } - }; - - env.builder.position_at_end(block); - env.builder - .set_current_debug_location(env.context, di_location); - let call = env - .builder - .build_call(function, &[seed.into(), value.into()], "struct_hash"); - - call.set_call_convention(FAST_CALL_CONV); - - call.try_as_basic_value().left().unwrap().into_int_value() -} - -fn build_hash_tag_help<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - layout_ids: &mut LayoutIds<'a>, - parent: FunctionValue<'ctx>, - union_layout: &UnionLayout<'a>, -) { - let ctx = env.context; - - debug_info_init!(env, parent); - - // Add args to scope - let mut it = parent.get_param_iter(); - let seed = it.next().unwrap().into_int_value(); - let value = it.next().unwrap(); - - seed.set_name(Symbol::ARG_1.as_str(&env.interns)); - value.set_name(Symbol::ARG_2.as_str(&env.interns)); - - let entry = ctx.append_basic_block(parent, "entry"); - env.builder.position_at_end(entry); - - let result = hash_tag(env, layout_ids, parent, seed, value, union_layout); - - env.builder.build_return(Some(&result)); -} - -fn hash_tag<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - layout_ids: &mut LayoutIds<'a>, - parent: FunctionValue<'ctx>, - seed: IntValue<'ctx>, - tag: BasicValueEnum<'ctx>, - union_layout: &UnionLayout<'a>, -) -> IntValue<'ctx> { - use UnionLayout::*; - - let entry_block = env.builder.get_insert_block().unwrap(); - - let merge_block = env.context.append_basic_block(parent, "merge_block"); - env.builder.position_at_end(merge_block); - - let tag_id_layout = union_layout.tag_id_layout(); - let tag_id_basic_type = basic_type_from_layout(env, &tag_id_layout); - - let merge_phi = env.builder.build_phi(seed.get_type(), "merge_hash"); - - env.builder.position_at_end(entry_block); - match union_layout { - NonRecursive(tags) => { - let current_tag_id = get_tag_id(env, parent, union_layout, tag); - - let mut cases = Vec::with_capacity_in(tags.len(), env.arena); - - for (tag_id, field_layouts) in tags.iter().enumerate() { - let block = env.context.append_basic_block(parent, "tag_id_modify"); - env.builder.position_at_end(block); - - // hash the tag id - let hash_bytes = store_and_use_as_u8_ptr( - env, - tag_id_basic_type - .into_int_type() - .const_int(tag_id as u64, false) - .into(), - &tag_id_layout, - ); - let seed = hash_bitcode_fn( - env, - seed, - hash_bytes, - tag_id_layout.stack_size(env.target_info), - ); - - // hash the tag data - let tag = tag.into_pointer_value(); - let answer = - hash_ptr_to_struct(env, layout_ids, union_layout, field_layouts, seed, tag); - - merge_phi.add_incoming(&[(&answer, block)]); - env.builder.build_unconditional_branch(merge_block); - - cases.push(( - current_tag_id.get_type().const_int(tag_id as u64, false), - block, - )); - } - - env.builder.position_at_end(entry_block); - - match cases.pop() { - Some((_, default)) => { - env.builder.build_switch(current_tag_id, default, &cases); - } - None => { - // we're hashing empty tag unions; this code is effectively unreachable - env.builder.build_unreachable(); - } - } - } - Recursive(tags) => { - let current_tag_id = get_tag_id(env, parent, union_layout, tag); - - let mut cases = Vec::with_capacity_in(tags.len(), env.arena); - - for (tag_id, field_layouts) in tags.iter().enumerate() { - let block = env.context.append_basic_block(parent, "tag_id_modify"); - env.builder.position_at_end(block); - - // hash the tag id - let hash_bytes = store_and_use_as_u8_ptr( - env, - tag_id_basic_type - .into_int_type() - .const_int(tag_id as u64, false) - .into(), - &tag_id_layout, - ); - let seed = hash_bitcode_fn( - env, - seed, - hash_bytes, - tag_id_layout.stack_size(env.target_info), - ); - - // hash the tag data - let tag = tag_pointer_clear_tag_id(env, tag.into_pointer_value()); - let answer = - hash_ptr_to_struct(env, layout_ids, union_layout, field_layouts, seed, tag); - - merge_phi.add_incoming(&[(&answer, block)]); - env.builder.build_unconditional_branch(merge_block); - - cases.push(( - current_tag_id.get_type().const_int(tag_id as u64, false), - block, - )); - } - - env.builder.position_at_end(entry_block); - - let default = cases.pop().unwrap().1; - - env.builder.build_switch(current_tag_id, default, &cases); - } - NullableUnwrapped { other_fields, .. } => { - let tag = tag.into_pointer_value(); - - let is_null = env.builder.build_is_null(tag, "is_null"); - - let hash_null_block = env.context.append_basic_block(parent, "hash_null_block"); - let hash_other_block = env.context.append_basic_block(parent, "hash_other_block"); - - env.builder - .build_conditional_branch(is_null, hash_null_block, hash_other_block); - - { - env.builder.position_at_end(hash_null_block); - - let answer = hash_null(seed); - - merge_phi.add_incoming(&[(&answer, hash_null_block)]); - env.builder.build_unconditional_branch(merge_block); - } - - { - env.builder.position_at_end(hash_other_block); - - let answer = - hash_ptr_to_struct(env, layout_ids, union_layout, other_fields, seed, tag); - - merge_phi.add_incoming(&[(&answer, hash_other_block)]); - env.builder.build_unconditional_branch(merge_block); - } - } - NullableWrapped { - other_tags, - nullable_id, - } => { - let tag = tag.into_pointer_value(); - - let is_null = env.builder.build_is_null(tag, "is_null"); - - let hash_null_block = env.context.append_basic_block(parent, "hash_null_block"); - let hash_other_block = env.context.append_basic_block(parent, "hash_other_block"); - - env.builder - .build_conditional_branch(is_null, hash_null_block, hash_other_block); - - { - env.builder.position_at_end(hash_null_block); - - let answer = hash_null(seed); - - merge_phi.add_incoming(&[(&answer, hash_null_block)]); - env.builder.build_unconditional_branch(merge_block); - } - - { - let mut cases = Vec::with_capacity_in(other_tags.len(), env.arena); - - for (mut tag_id, field_layouts) in other_tags.iter().enumerate() { - if tag_id >= *nullable_id as usize { - tag_id += 1; - } - - let block = env.context.append_basic_block(parent, "tag_id_modify"); - env.builder.position_at_end(block); - - // hash the tag id - let hash_bytes = store_and_use_as_u8_ptr( - env, - tag_id_basic_type - .into_int_type() - .const_int(tag_id as u64, false) - .into(), - &tag_id_layout, - ); - let seed1 = hash_bitcode_fn( - env, - seed, - hash_bytes, - tag_id_layout.stack_size(env.target_info), - ); - - // hash tag data - let tag = tag_pointer_clear_tag_id(env, tag); - let answer = hash_ptr_to_struct( - env, - layout_ids, - union_layout, - field_layouts, - seed1, - tag, - ); - - merge_phi.add_incoming(&[(&answer, block)]); - env.builder.build_unconditional_branch(merge_block); - - cases.push(( - tag_id_basic_type - .into_int_type() - .const_int(tag_id as u64, false), - block, - )); - } - - env.builder.position_at_end(hash_other_block); - - let tag_id = get_tag_id(env, parent, union_layout, tag.into()); - - let default = cases.pop().unwrap().1; - - env.builder.build_switch(tag_id, default, &cases); - } - } - NonNullableUnwrapped(field_layouts) => { - let answer = hash_ptr_to_struct( - env, - layout_ids, - union_layout, - field_layouts, - seed, - tag.into_pointer_value(), - ); - - merge_phi.add_incoming(&[(&answer, entry_block)]); - env.builder.build_unconditional_branch(merge_block); - } - } - - env.builder.position_at_end(merge_block); - - merge_phi.as_basic_value().into_int_value() -} - -fn build_hash_list<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - layout_ids: &mut LayoutIds<'a>, - layout: &Layout<'a>, - element_layout: &Layout<'a>, - when_recursive: WhenRecursive<'a>, - seed: IntValue<'ctx>, - value: StructValue<'ctx>, -) -> IntValue<'ctx> { - let block = env.builder.get_insert_block().expect("to be in a function"); - let di_location = env.builder.get_current_debug_location().unwrap(); - - let symbol = Symbol::GENERIC_HASH; - let fn_name = layout_ids - .get(symbol, layout) - .to_symbol_string(symbol, &env.interns); - - let function = match env.module.get_function(fn_name.as_str()) { - Some(function_value) => function_value, - None => { - let seed_type = env.context.i64_type(); - - let arg_type = basic_type_from_layout(env, layout); - - let function_value = crate::llvm::refcounting::build_header_help( - env, - &fn_name, - seed_type.into(), - &[seed_type.into(), arg_type], - ); - - build_hash_list_help( - env, - layout_ids, - function_value, - when_recursive, - element_layout, - ); - - function_value - } - }; - - env.builder.position_at_end(block); - env.builder - .set_current_debug_location(env.context, di_location); - let call = env - .builder - .build_call(function, &[seed.into(), value.into()], "struct_hash"); - - call.set_call_convention(FAST_CALL_CONV); - - call.try_as_basic_value().left().unwrap().into_int_value() -} - -fn build_hash_list_help<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - layout_ids: &mut LayoutIds<'a>, - parent: FunctionValue<'ctx>, - when_recursive: WhenRecursive<'a>, - element_layout: &Layout<'a>, -) { - let ctx = env.context; - - debug_info_init!(env, parent); - - // Add args to scope - let mut it = parent.get_param_iter(); - let seed = it.next().unwrap().into_int_value(); - let value = it.next().unwrap().into_struct_value(); - - seed.set_name(Symbol::ARG_1.as_str(&env.interns)); - value.set_name(Symbol::ARG_2.as_str(&env.interns)); - - let entry = ctx.append_basic_block(parent, "entry"); - env.builder.position_at_end(entry); - - let result = hash_list( - env, - layout_ids, - parent, - seed, - value, - when_recursive, - element_layout, - ); - - env.builder.build_return(Some(&result)); -} - -fn hash_list<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - layout_ids: &mut LayoutIds<'a>, - parent: FunctionValue<'ctx>, - seed: IntValue<'ctx>, - value: StructValue<'ctx>, - when_recursive: WhenRecursive<'a>, - element_layout: &Layout<'a>, -) -> IntValue<'ctx> { - use crate::llvm::build_list::{incrementing_elem_loop, load_list}; - use inkwell::types::BasicType; - - // hash of a list is the hash of its elements - let done_block = env.context.append_basic_block(parent, "done"); - let loop_block = env.context.append_basic_block(parent, "loop"); - - let element_type = basic_type_from_layout(env, element_layout); - let ptr_type = element_type.ptr_type(inkwell::AddressSpace::Generic); - - let (length, ptr) = load_list(env.builder, value, ptr_type); - - let result = env.builder.build_alloca(env.context.i64_type(), "result"); - env.builder.build_store(result, seed); - - let is_empty = env.builder.build_int_compare( - inkwell::IntPredicate::EQ, - length, - env.ptr_int().const_zero(), - "is_empty", - ); - - env.builder - .build_conditional_branch(is_empty, done_block, loop_block); - - env.builder.position_at_end(loop_block); - - let loop_fn = |_index, element| { - let seed = env - .builder - .build_load(result, "load_current") - .into_int_value(); - - let answer = append_hash_layout( - env, - layout_ids, - seed, - element, - element_layout, - when_recursive.clone(), - ); - - env.builder.build_store(result, answer); - }; - - incrementing_elem_loop( - env, - parent, - *element_layout, - ptr, - length, - "current_index", - loop_fn, - ); - - env.builder.build_unconditional_branch(done_block); - - env.builder.position_at_end(done_block); - - env.builder - .build_load(result, "load_current") - .into_int_value() -} - -fn hash_null(seed: IntValue<'_>) -> IntValue<'_> { - seed -} - -fn hash_ptr_to_struct<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - layout_ids: &mut LayoutIds<'a>, - union_layout: &UnionLayout<'a>, - field_layouts: &'a [Layout<'a>], - seed: IntValue<'ctx>, - tag: PointerValue<'ctx>, -) -> IntValue<'ctx> { - use inkwell::types::BasicType; - - let wrapper_type = argument_type_from_union_layout(env, union_layout); - - // cast the opaque pointer to a pointer of the correct shape - let wrapper_ptr = env - .builder - .build_bitcast(tag, wrapper_type, "hash_ptr_to_struct_opaque_to_correct") - .into_pointer_value(); - - let struct_ptr = env - .builder - .build_struct_gep(wrapper_ptr, TAG_DATA_INDEX, "get_tag_data") - .unwrap(); - - let struct_layout = Layout::struct_no_name_order(field_layouts); - let struct_type = basic_type_from_layout(env, &struct_layout); - let struct_ptr = env - .builder - .build_bitcast( - struct_ptr, - struct_type.ptr_type(inkwell::AddressSpace::Generic), - "cast_tag_data", - ) - .into_pointer_value(); - - let struct_value = env - .builder - .build_load(struct_ptr, "load_struct1") - .into_struct_value(); - - build_hash_struct( - env, - layout_ids, - field_layouts, - WhenRecursive::Loop(*union_layout), - seed, - struct_value, - ) -} - -fn store_and_use_as_u8_ptr<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - value: BasicValueEnum<'ctx>, - layout: &Layout<'a>, -) -> PointerValue<'ctx> { - let basic_type = basic_type_from_layout(env, layout); - let alloc = env.builder.build_alloca(basic_type, "store"); - env.builder.build_store(alloc, value); - - let u8_ptr = env - .context - .i8_type() - .ptr_type(inkwell::AddressSpace::Generic); - - env.builder - .build_bitcast(alloc, u8_ptr, "as_u8_ptr") - .into_pointer_value() -} - -fn hash_bitcode_fn<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - seed: IntValue<'ctx>, - buffer: PointerValue<'ctx>, - width: u32, -) -> IntValue<'ctx> { - let num_bytes = env.ptr_int().const_int(width as u64, false); - - call_bitcode_fn( - env, - &[seed.into(), buffer.into(), num_bytes.into()], - bitcode::DICT_HASH, - ) - .into_int_value() -} diff --git a/compiler/gen_llvm/src/llvm/build_list.rs b/compiler/gen_llvm/src/llvm/build_list.rs deleted file mode 100644 index 57edf0696b..0000000000 --- a/compiler/gen_llvm/src/llvm/build_list.rs +++ /dev/null @@ -1,1370 +0,0 @@ -#![allow(clippy::too_many_arguments)] -use crate::llvm::bitcode::{ - build_dec_wrapper, build_eq_wrapper, build_has_tag_id, build_inc_n_wrapper, build_inc_wrapper, - call_bitcode_fn, call_list_bitcode_fn, call_void_bitcode_fn, -}; -use crate::llvm::build::{ - allocate_with_refcount_help, cast_basic_basic, Env, RocFunctionCall, Scope, -}; -use crate::llvm::convert::basic_type_from_layout; -use crate::llvm::refcounting::increment_refcount_layout; -use inkwell::builder::Builder; -use inkwell::context::Context; -use inkwell::types::{BasicType, BasicTypeEnum, PointerType}; -use inkwell::values::{BasicValueEnum, FunctionValue, IntValue, PointerValue, StructValue}; -use inkwell::{AddressSpace, IntPredicate}; -use morphic_lib::UpdateMode; -use roc_builtins::bitcode::{self, IntWidth}; -use roc_module::symbol::Symbol; -use roc_mono::layout::{Builtin, Layout, LayoutIds}; - -use super::build::{create_entry_block_alloca, load_roc_value, load_symbol, store_roc_value}; - -pub fn list_symbol_to_c_abi<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - scope: &Scope<'a, 'ctx>, - symbol: Symbol, -) -> PointerValue<'ctx> { - let parent = env - .builder - .get_insert_block() - .and_then(|b| b.get_parent()) - .unwrap(); - - let list_type = super::convert::zig_list_type(env); - let list_alloca = create_entry_block_alloca(env, parent, list_type.into(), "list_alloca"); - - let list = load_symbol(scope, &symbol); - env.builder.build_store(list_alloca, list); - - list_alloca -} - -pub fn list_to_c_abi<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - list: BasicValueEnum<'ctx>, -) -> PointerValue<'ctx> { - let parent = env - .builder - .get_insert_block() - .and_then(|b| b.get_parent()) - .unwrap(); - - let list_type = super::convert::zig_list_type(env); - let list_alloca = create_entry_block_alloca(env, parent, list_type.into(), "list_alloca"); - - env.builder.build_store(list_alloca, list); - - list_alloca -} - -pub fn pass_update_mode<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - update_mode: UpdateMode, -) -> BasicValueEnum<'ctx> { - match update_mode { - UpdateMode::Immutable => env.context.i8_type().const_zero().into(), - UpdateMode::InPlace => env.context.i8_type().const_int(1, false).into(), - } -} - -fn pass_element_as_opaque<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - element: BasicValueEnum<'ctx>, - layout: Layout<'a>, -) -> BasicValueEnum<'ctx> { - let element_type = basic_type_from_layout(env, &layout); - let element_ptr = env - .builder - .build_alloca(element_type, "element_to_pass_as_opaque"); - store_roc_value(env, layout, element_ptr, element); - - env.builder.build_bitcast( - element_ptr, - env.context.i8_type().ptr_type(AddressSpace::Generic), - "pass_element_as_opaque", - ) -} - -pub fn layout_width<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - layout: &Layout<'a>, -) -> BasicValueEnum<'ctx> { - env.ptr_int() - .const_int(layout.stack_size(env.target_info) as u64, false) - .into() -} - -pub fn pass_as_opaque<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - ptr: PointerValue<'ctx>, -) -> BasicValueEnum<'ctx> { - env.builder.build_bitcast( - ptr, - env.context.i8_type().ptr_type(AddressSpace::Generic), - "pass_as_opaque", - ) -} - -/// List.single : a -> List a -pub fn list_single<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - element: BasicValueEnum<'ctx>, - element_layout: &Layout<'a>, -) -> BasicValueEnum<'ctx> { - call_list_bitcode_fn( - env, - &[ - env.alignment_intvalue(element_layout), - pass_element_as_opaque(env, element, *element_layout), - layout_width(env, element_layout), - ], - bitcode::LIST_SINGLE, - ) -} - -/// List.repeat : elem, Nat -> List elem -pub fn list_repeat<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - layout_ids: &mut LayoutIds<'a>, - element: BasicValueEnum<'ctx>, - element_layout: &Layout<'a>, - list_len: IntValue<'ctx>, -) -> BasicValueEnum<'ctx> { - let inc_element_fn = build_inc_n_wrapper(env, layout_ids, element_layout); - - call_list_bitcode_fn( - env, - &[ - list_len.into(), - env.alignment_intvalue(element_layout), - pass_element_as_opaque(env, element, *element_layout), - layout_width(env, element_layout), - inc_element_fn.as_global_value().as_pointer_value().into(), - ], - bitcode::LIST_REPEAT, - ) -} - -/// List.join : List (List elem) -> List elem -pub fn list_join<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - outer_list: BasicValueEnum<'ctx>, - element_layout: &Layout<'a>, -) -> BasicValueEnum<'ctx> { - call_list_bitcode_fn( - env, - &[ - list_to_c_abi(env, outer_list).into(), - env.alignment_intvalue(element_layout), - layout_width(env, element_layout), - ], - bitcode::LIST_JOIN, - ) -} - -/// List.reverse : List elem -> List elem -pub fn list_reverse<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - list: BasicValueEnum<'ctx>, - element_layout: &Layout<'a>, - update_mode: UpdateMode, -) -> BasicValueEnum<'ctx> { - call_list_bitcode_fn( - env, - &[ - list_to_c_abi(env, list).into(), - env.alignment_intvalue(element_layout), - layout_width(env, element_layout), - pass_update_mode(env, update_mode), - ], - bitcode::LIST_REVERSE, - ) -} - -pub fn list_get_unsafe<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - layout_ids: &mut LayoutIds<'a>, - parent: FunctionValue<'ctx>, - element_layout: &Layout<'a>, - elem_index: IntValue<'ctx>, - wrapper_struct: StructValue<'ctx>, -) -> BasicValueEnum<'ctx> { - let builder = env.builder; - - let elem_type = basic_type_from_layout(env, element_layout); - let ptr_type = elem_type.ptr_type(AddressSpace::Generic); - // Load the pointer to the array data - let array_data_ptr = load_list_ptr(builder, wrapper_struct, ptr_type); - - // Assume the bounds have already been checked earlier - // (e.g. by List.get or List.first, which wrap List.#getUnsafe) - let elem_ptr = - unsafe { builder.build_in_bounds_gep(array_data_ptr, &[elem_index], "list_get_element") }; - - let result = load_roc_value(env, *element_layout, elem_ptr, "list_get_load_element"); - - increment_refcount_layout(env, parent, layout_ids, 1, result, element_layout); - - result -} - -/// List.append : List elem, elem -> List elem -pub fn list_append<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - original_wrapper: StructValue<'ctx>, - element: BasicValueEnum<'ctx>, - element_layout: &Layout<'a>, - update_mode: UpdateMode, -) -> BasicValueEnum<'ctx> { - call_list_bitcode_fn( - env, - &[ - list_to_c_abi(env, original_wrapper.into()).into(), - env.alignment_intvalue(element_layout), - pass_element_as_opaque(env, element, *element_layout), - layout_width(env, element_layout), - pass_update_mode(env, update_mode), - ], - bitcode::LIST_APPEND, - ) -} - -/// List.prepend : List elem, elem -> List elem -pub fn list_prepend<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - original_wrapper: StructValue<'ctx>, - element: BasicValueEnum<'ctx>, - element_layout: &Layout<'a>, -) -> BasicValueEnum<'ctx> { - call_list_bitcode_fn( - env, - &[ - list_to_c_abi(env, original_wrapper.into()).into(), - env.alignment_intvalue(element_layout), - pass_element_as_opaque(env, element, *element_layout), - layout_width(env, element_layout), - ], - bitcode::LIST_PREPEND, - ) -} - -/// List.swap : List elem, Nat, Nat -> List elem -pub fn list_swap<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - original_wrapper: StructValue<'ctx>, - index_1: IntValue<'ctx>, - index_2: IntValue<'ctx>, - element_layout: &Layout<'a>, - update_mode: UpdateMode, -) -> BasicValueEnum<'ctx> { - call_list_bitcode_fn( - env, - &[ - list_to_c_abi(env, original_wrapper.into()).into(), - env.alignment_intvalue(element_layout), - layout_width(env, element_layout), - index_1.into(), - index_2.into(), - pass_update_mode(env, update_mode), - ], - bitcode::LIST_SWAP, - ) -} - -/// List.sublist : List elem, { start : Nat, len : Nat } -> List elem -pub fn list_sublist<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - layout_ids: &mut LayoutIds<'a>, - original_wrapper: StructValue<'ctx>, - start: IntValue<'ctx>, - len: IntValue<'ctx>, - element_layout: &Layout<'a>, -) -> BasicValueEnum<'ctx> { - let dec_element_fn = build_dec_wrapper(env, layout_ids, element_layout); - call_list_bitcode_fn( - env, - &[ - list_to_c_abi(env, original_wrapper.into()).into(), - env.alignment_intvalue(element_layout), - layout_width(env, element_layout), - start.into(), - len.into(), - dec_element_fn.as_global_value().as_pointer_value().into(), - ], - bitcode::LIST_SUBLIST, - ) -} - -/// List.dropAt : List elem, Nat -> List elem -pub fn list_drop_at<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - layout_ids: &mut LayoutIds<'a>, - original_wrapper: StructValue<'ctx>, - count: IntValue<'ctx>, - element_layout: &Layout<'a>, -) -> BasicValueEnum<'ctx> { - let dec_element_fn = build_dec_wrapper(env, layout_ids, element_layout); - call_list_bitcode_fn( - env, - &[ - list_to_c_abi(env, original_wrapper.into()).into(), - env.alignment_intvalue(element_layout), - layout_width(env, element_layout), - count.into(), - dec_element_fn.as_global_value().as_pointer_value().into(), - ], - bitcode::LIST_DROP_AT, - ) -} - -/// List.replace_unsafe : List elem, Nat, elem -> { list: List elem, value: elem } -pub fn list_replace_unsafe<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - _layout_ids: &mut LayoutIds<'a>, - list: BasicValueEnum<'ctx>, - index: IntValue<'ctx>, - element: BasicValueEnum<'ctx>, - element_layout: &Layout<'a>, - update_mode: UpdateMode, -) -> BasicValueEnum<'ctx> { - let element_type = basic_type_from_layout(env, element_layout); - let element_ptr = env - .builder - .build_alloca(element_type, "output_element_as_opaque"); - - // Assume the bounds have already been checked earlier - // (e.g. by List.replace or List.set, which wrap List.#replaceUnsafe) - let new_list = match update_mode { - UpdateMode::InPlace => call_list_bitcode_fn( - env, - &[ - list_to_c_abi(env, list).into(), - index.into(), - pass_element_as_opaque(env, element, *element_layout), - layout_width(env, element_layout), - pass_as_opaque(env, element_ptr), - ], - bitcode::LIST_REPLACE_IN_PLACE, - ), - UpdateMode::Immutable => call_list_bitcode_fn( - env, - &[ - list_to_c_abi(env, list).into(), - env.alignment_intvalue(element_layout), - index.into(), - pass_element_as_opaque(env, element, *element_layout), - layout_width(env, element_layout), - pass_as_opaque(env, element_ptr), - ], - bitcode::LIST_REPLACE, - ), - }; - - // Load the element and returned list into a struct. - let old_element = env.builder.build_load(element_ptr, "load_element"); - - let result = env - .context - .struct_type( - &[super::convert::zig_list_type(env).into(), element_type], - false, - ) - .const_zero(); - - let result = env - .builder - .build_insert_value(result, new_list, 0, "insert_list") - .unwrap(); - - env.builder - .build_insert_value(result, old_element, 1, "insert_value") - .unwrap() - .into_struct_value() - .into() -} - -fn bounds_check_comparison<'ctx>( - builder: &Builder<'ctx>, - elem_index: IntValue<'ctx>, - len: IntValue<'ctx>, -) -> IntValue<'ctx> { - // Note: Check for index < length as the "true" condition, - // to avoid misprediction. (In practice this should usually pass, - // and CPUs generally default to predicting that a forward jump - // shouldn't be taken; that is, they predict "else" won't be taken.) - builder.build_int_compare(IntPredicate::ULT, elem_index, len, "bounds_check") -} - -/// List.len : List elem -> Int -pub fn list_len<'ctx>( - builder: &Builder<'ctx>, - wrapper_struct: StructValue<'ctx>, -) -> IntValue<'ctx> { - builder - .build_extract_value(wrapper_struct, Builtin::WRAPPER_LEN, "list_len") - .unwrap() - .into_int_value() -} - -pub enum ListWalk { - Walk, - WalkBackwards, - WalkUntil, - WalkBackwardsUntil, -} - -pub fn list_walk_generic<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - layout_ids: &mut LayoutIds<'a>, - roc_function_call: RocFunctionCall<'ctx>, - function_call_return_layout: &Layout<'a>, - list: BasicValueEnum<'ctx>, - element_layout: &Layout<'a>, - default: BasicValueEnum<'ctx>, - default_layout: &Layout<'a>, - variant: ListWalk, -) -> BasicValueEnum<'ctx> { - let builder = env.builder; - - let zig_function = match variant { - ListWalk::Walk => bitcode::LIST_WALK, - ListWalk::WalkBackwards => bitcode::LIST_WALK_BACKWARDS, - ListWalk::WalkUntil => bitcode::LIST_WALK_UNTIL, - ListWalk::WalkBackwardsUntil => todo!(), - }; - - let default_ptr = if default_layout.is_passed_by_reference(env.target_info) { - debug_assert!(default.is_pointer_value()); - default.into_pointer_value() - } else { - let default_ptr = builder.build_alloca(default.get_type(), "default_ptr"); - env.builder.build_store(default_ptr, default); - default_ptr - }; - - let result_ptr = { - let basic_type = basic_type_from_layout(env, default_layout); - env.builder.build_alloca(basic_type, "result") - }; - - match variant { - ListWalk::Walk | ListWalk::WalkBackwards => { - call_void_bitcode_fn( - env, - &[ - list_to_c_abi(env, list).into(), - roc_function_call.caller.into(), - pass_as_opaque(env, roc_function_call.data), - roc_function_call.inc_n_data.into(), - roc_function_call.data_is_owned.into(), - pass_as_opaque(env, default_ptr), - env.alignment_intvalue(element_layout), - layout_width(env, element_layout), - layout_width(env, default_layout), - pass_as_opaque(env, result_ptr), - ], - zig_function, - ); - } - ListWalk::WalkUntil | ListWalk::WalkBackwardsUntil => { - let function = env - .builder - .get_insert_block() - .unwrap() - .get_parent() - .unwrap(); - - let has_tag_id = match function_call_return_layout { - Layout::Union(union_layout) => build_has_tag_id(env, function, *union_layout), - _ => unreachable!(), - }; - - let dec_element_fn = build_dec_wrapper(env, layout_ids, element_layout); - call_void_bitcode_fn( - env, - &[ - list_to_c_abi(env, list).into(), - roc_function_call.caller.into(), - pass_as_opaque(env, roc_function_call.data), - roc_function_call.inc_n_data.into(), - roc_function_call.data_is_owned.into(), - pass_as_opaque(env, default_ptr), - env.alignment_intvalue(element_layout), - layout_width(env, element_layout), - layout_width(env, function_call_return_layout), - layout_width(env, default_layout), - has_tag_id_helper(env, has_tag_id).into(), - dec_element_fn.as_global_value().as_pointer_value().into(), - pass_as_opaque(env, result_ptr), - ], - zig_function, - ); - } - } - - if default_layout.is_passed_by_reference(env.target_info) { - result_ptr.into() - } else { - env.builder.build_load(result_ptr, "load_result") - } -} - -/// List.range : Int a, Int a -> List (Int a) -pub fn list_range<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - int_width: IntWidth, - low: IntValue<'ctx>, - high: IntValue<'ctx>, -) -> BasicValueEnum<'ctx> { - let builder = env.builder; - - let low_ptr = builder.build_alloca(low.get_type(), "low_ptr"); - env.builder.build_store(low_ptr, low); - - let high_ptr = builder.build_alloca(high.get_type(), "high_ptr"); - env.builder.build_store(high_ptr, high); - - let int_width = env - .context - .i8_type() - .const_int(int_width as u64, false) - .into(); - - call_list_bitcode_fn( - env, - &[ - int_width, - pass_as_opaque(env, low_ptr), - pass_as_opaque(env, high_ptr), - ], - bitcode::LIST_RANGE, - ) -} - -/// List.contains : List elem, elem -> Bool -pub fn list_contains<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - layout_ids: &mut LayoutIds<'a>, - element: BasicValueEnum<'ctx>, - element_layout: &Layout<'a>, - list: BasicValueEnum<'ctx>, -) -> BasicValueEnum<'ctx> { - let eq_fn = build_eq_wrapper(env, layout_ids, element_layout) - .as_global_value() - .as_pointer_value() - .into(); - - call_bitcode_fn( - env, - &[ - list_to_c_abi(env, list).into(), - pass_element_as_opaque(env, element, *element_layout), - layout_width(env, element_layout), - eq_fn, - ], - bitcode::LIST_CONTAINS, - ) -} - -/// List.keepIf : List elem, (elem -> Bool) -> List elem -pub fn list_keep_if<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - layout_ids: &mut LayoutIds<'a>, - roc_function_call: RocFunctionCall<'ctx>, - list: BasicValueEnum<'ctx>, - element_layout: &Layout<'a>, -) -> BasicValueEnum<'ctx> { - let inc_element_fn = build_inc_wrapper(env, layout_ids, element_layout); - let dec_element_fn = build_dec_wrapper(env, layout_ids, element_layout); - - call_list_bitcode_fn( - env, - &[ - list_to_c_abi(env, list).into(), - roc_function_call.caller.into(), - pass_as_opaque(env, roc_function_call.data), - roc_function_call.inc_n_data.into(), - roc_function_call.data_is_owned.into(), - env.alignment_intvalue(element_layout), - layout_width(env, element_layout), - inc_element_fn.as_global_value().as_pointer_value().into(), - dec_element_fn.as_global_value().as_pointer_value().into(), - ], - bitcode::LIST_KEEP_IF, - ) -} - -fn empty_list<'a, 'ctx, 'env>(env: &Env<'a, 'ctx, 'env>) -> BasicValueEnum<'ctx> { - let struct_type = super::convert::zig_list_type(env); - - // The pointer should be null (aka zero) and the length should be zero, - // so the whole struct should be a const_zero - BasicValueEnum::StructValue(struct_type.const_zero()) -} - -fn has_tag_id_helper<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - has_tag_id: FunctionValue<'ctx>, -) -> PointerValue<'ctx> { - let u8_t = env.context.i8_type(); - let u16_t = env.context.i16_type(); - - let u8_ptr_t = u8_t.ptr_type(AddressSpace::Generic); - - let struct_t = env - .context - .struct_type(&[u8_t.into(), env.ptr_int().into()], false); - - let has_tag_id_type = struct_t - .fn_type(&[u16_t.into(), u8_ptr_t.into()], false) - .ptr_type(AddressSpace::Generic); - - env.builder.build_pointer_cast( - has_tag_id.as_global_value().as_pointer_value(), - has_tag_id_type, - "has_tag_id_cast", - ) -} - -/// List.keepOks : List before, (before -> Result after *) -> List after -pub fn list_keep_oks<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - layout_ids: &mut LayoutIds<'a>, - roc_function_call: RocFunctionCall<'ctx>, - // Layout of the `Result after *` - result_layout: &Layout<'a>, - list: BasicValueEnum<'ctx>, - before_layout: &Layout<'a>, - after_layout: &Layout<'a>, -) -> BasicValueEnum<'ctx> { - let dec_result_fn = build_dec_wrapper(env, layout_ids, result_layout); - - let function = env - .builder - .get_insert_block() - .unwrap() - .get_parent() - .unwrap(); - - let has_tag_id = match result_layout { - Layout::Union(union_layout) => build_has_tag_id(env, function, *union_layout), - Layout::Builtin(Builtin::Bool) => { - // a `Result whatever []`, so there is nothing to keep - return empty_list(env); - } - _ => unreachable!(), - }; - - call_list_bitcode_fn( - env, - &[ - list_to_c_abi(env, list).into(), - roc_function_call.caller.into(), - pass_as_opaque(env, roc_function_call.data), - roc_function_call.inc_n_data.into(), - roc_function_call.data_is_owned.into(), - env.alignment_intvalue(before_layout), - layout_width(env, before_layout), - layout_width(env, result_layout), - layout_width(env, after_layout), - has_tag_id_helper(env, has_tag_id).into(), - dec_result_fn.as_global_value().as_pointer_value().into(), - ], - bitcode::LIST_KEEP_OKS, - ) -} - -/// List.keepErrs : List before, (before -> Result * after) -> List after -pub fn list_keep_errs<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - layout_ids: &mut LayoutIds<'a>, - roc_function_call: RocFunctionCall<'ctx>, - // Layout of the `Result * err` - result_layout: &Layout<'a>, - list: BasicValueEnum<'ctx>, - before_layout: &Layout<'a>, - after_layout: &Layout<'a>, -) -> BasicValueEnum<'ctx> { - let dec_result_fn = build_dec_wrapper(env, layout_ids, result_layout); - - let function = env - .builder - .get_insert_block() - .unwrap() - .get_parent() - .unwrap(); - - let has_tag_id = match result_layout { - Layout::Union(union_layout) => build_has_tag_id(env, function, *union_layout), - Layout::Builtin(Builtin::Bool) => { - // a `Result whatever []`, so there is nothing to keep - return empty_list(env); - } - _ => unreachable!(), - }; - - call_list_bitcode_fn( - env, - &[ - list_to_c_abi(env, list).into(), - roc_function_call.caller.into(), - pass_as_opaque(env, roc_function_call.data), - roc_function_call.inc_n_data.into(), - roc_function_call.data_is_owned.into(), - env.alignment_intvalue(before_layout), - layout_width(env, before_layout), - layout_width(env, result_layout), - layout_width(env, after_layout), - has_tag_id_helper(env, has_tag_id).into(), - dec_result_fn.as_global_value().as_pointer_value().into(), - ], - bitcode::LIST_KEEP_ERRS, - ) -} - -/// List.sortWith : List a, (a, a -> Ordering) -> List a -pub fn list_sort_with<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - roc_function_call: RocFunctionCall<'ctx>, - compare_wrapper: PointerValue<'ctx>, - list: BasicValueEnum<'ctx>, - element_layout: &Layout<'a>, -) -> BasicValueEnum<'ctx> { - call_list_bitcode_fn( - env, - &[ - list_to_c_abi(env, list).into(), - compare_wrapper.into(), - pass_as_opaque(env, roc_function_call.data), - roc_function_call.inc_n_data.into(), - roc_function_call.data_is_owned.into(), - env.alignment_intvalue(element_layout), - layout_width(env, element_layout), - ], - bitcode::LIST_SORT_WITH, - ) -} - -/// List.mapWithIndex : List before, (before, Nat -> after) -> List after -pub fn list_map_with_index<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - roc_function_call: RocFunctionCall<'ctx>, - list: BasicValueEnum<'ctx>, - element_layout: &Layout<'a>, - return_layout: &Layout<'a>, -) -> BasicValueEnum<'ctx> { - call_list_bitcode_fn( - env, - &[ - list_to_c_abi(env, list).into(), - roc_function_call.caller.into(), - pass_as_opaque(env, roc_function_call.data), - roc_function_call.inc_n_data.into(), - roc_function_call.data_is_owned.into(), - env.alignment_intvalue(element_layout), - layout_width(env, element_layout), - layout_width(env, return_layout), - ], - bitcode::LIST_MAP_WITH_INDEX, - ) -} - -/// List.map : List before, (before -> after) -> List after -pub fn list_map<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - roc_function_call: RocFunctionCall<'ctx>, - list: BasicValueEnum<'ctx>, - element_layout: &Layout<'a>, - return_layout: &Layout<'a>, -) -> BasicValueEnum<'ctx> { - call_list_bitcode_fn( - env, - &[ - list_to_c_abi(env, list).into(), - roc_function_call.caller.into(), - pass_as_opaque(env, roc_function_call.data), - roc_function_call.inc_n_data.into(), - roc_function_call.data_is_owned.into(), - env.alignment_intvalue(element_layout), - layout_width(env, element_layout), - layout_width(env, return_layout), - ], - bitcode::LIST_MAP, - ) -} - -pub fn list_map2<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - layout_ids: &mut LayoutIds<'a>, - roc_function_call: RocFunctionCall<'ctx>, - list1: BasicValueEnum<'ctx>, - list2: BasicValueEnum<'ctx>, - element1_layout: &Layout<'a>, - element2_layout: &Layout<'a>, - return_layout: &Layout<'a>, -) -> BasicValueEnum<'ctx> { - let dec_a = build_dec_wrapper(env, layout_ids, element1_layout); - let dec_b = build_dec_wrapper(env, layout_ids, element2_layout); - - call_list_bitcode_fn( - env, - &[ - list_to_c_abi(env, list1).into(), - list_to_c_abi(env, list2).into(), - roc_function_call.caller.into(), - pass_as_opaque(env, roc_function_call.data), - roc_function_call.inc_n_data.into(), - roc_function_call.data_is_owned.into(), - env.alignment_intvalue(return_layout), - layout_width(env, element1_layout), - layout_width(env, element2_layout), - layout_width(env, return_layout), - dec_a.as_global_value().as_pointer_value().into(), - dec_b.as_global_value().as_pointer_value().into(), - ], - bitcode::LIST_MAP2, - ) -} - -pub fn list_map3<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - layout_ids: &mut LayoutIds<'a>, - roc_function_call: RocFunctionCall<'ctx>, - list1: BasicValueEnum<'ctx>, - list2: BasicValueEnum<'ctx>, - list3: BasicValueEnum<'ctx>, - element1_layout: &Layout<'a>, - element2_layout: &Layout<'a>, - element3_layout: &Layout<'a>, - result_layout: &Layout<'a>, -) -> BasicValueEnum<'ctx> { - let dec_a = build_dec_wrapper(env, layout_ids, element1_layout); - let dec_b = build_dec_wrapper(env, layout_ids, element2_layout); - let dec_c = build_dec_wrapper(env, layout_ids, element3_layout); - - call_list_bitcode_fn( - env, - &[ - list_to_c_abi(env, list1).into(), - list_to_c_abi(env, list2).into(), - list_to_c_abi(env, list3).into(), - roc_function_call.caller.into(), - pass_as_opaque(env, roc_function_call.data), - roc_function_call.inc_n_data.into(), - roc_function_call.data_is_owned.into(), - env.alignment_intvalue(result_layout), - layout_width(env, element1_layout), - layout_width(env, element2_layout), - layout_width(env, element3_layout), - layout_width(env, result_layout), - dec_a.as_global_value().as_pointer_value().into(), - dec_b.as_global_value().as_pointer_value().into(), - dec_c.as_global_value().as_pointer_value().into(), - ], - bitcode::LIST_MAP3, - ) -} - -pub fn list_map4<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - layout_ids: &mut LayoutIds<'a>, - roc_function_call: RocFunctionCall<'ctx>, - list1: BasicValueEnum<'ctx>, - list2: BasicValueEnum<'ctx>, - list3: BasicValueEnum<'ctx>, - list4: BasicValueEnum<'ctx>, - element1_layout: &Layout<'a>, - element2_layout: &Layout<'a>, - element3_layout: &Layout<'a>, - element4_layout: &Layout<'a>, - result_layout: &Layout<'a>, -) -> BasicValueEnum<'ctx> { - let dec_a = build_dec_wrapper(env, layout_ids, element1_layout); - let dec_b = build_dec_wrapper(env, layout_ids, element2_layout); - let dec_c = build_dec_wrapper(env, layout_ids, element3_layout); - let dec_d = build_dec_wrapper(env, layout_ids, element4_layout); - - call_list_bitcode_fn( - env, - &[ - list_to_c_abi(env, list1).into(), - list_to_c_abi(env, list2).into(), - list_to_c_abi(env, list3).into(), - list_to_c_abi(env, list4).into(), - roc_function_call.caller.into(), - pass_as_opaque(env, roc_function_call.data), - roc_function_call.inc_n_data.into(), - roc_function_call.data_is_owned.into(), - env.alignment_intvalue(result_layout), - layout_width(env, element1_layout), - layout_width(env, element2_layout), - layout_width(env, element3_layout), - layout_width(env, element4_layout), - layout_width(env, result_layout), - dec_a.as_global_value().as_pointer_value().into(), - dec_b.as_global_value().as_pointer_value().into(), - dec_c.as_global_value().as_pointer_value().into(), - dec_d.as_global_value().as_pointer_value().into(), - ], - bitcode::LIST_MAP4, - ) -} - -/// List.concat : List elem, List elem -> List elem -pub fn list_concat<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - first_list: BasicValueEnum<'ctx>, - second_list: BasicValueEnum<'ctx>, - element_layout: &Layout<'a>, -) -> BasicValueEnum<'ctx> { - call_list_bitcode_fn( - env, - &[ - list_to_c_abi(env, first_list).into(), - list_to_c_abi(env, second_list).into(), - env.alignment_intvalue(element_layout), - layout_width(env, element_layout), - ], - bitcode::LIST_CONCAT, - ) -} - -/// List.any : List elem, \(elem -> Bool) -> Bool -pub fn list_any<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - roc_function_call: RocFunctionCall<'ctx>, - list: BasicValueEnum<'ctx>, - element_layout: &Layout<'a>, -) -> BasicValueEnum<'ctx> { - call_bitcode_fn( - env, - &[ - list_to_c_abi(env, list).into(), - roc_function_call.caller.into(), - pass_as_opaque(env, roc_function_call.data), - roc_function_call.inc_n_data.into(), - roc_function_call.data_is_owned.into(), - layout_width(env, element_layout), - ], - bitcode::LIST_ANY, - ) -} - -/// List.all : List elem, \(elem -> Bool) -> Bool -pub fn list_all<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - roc_function_call: RocFunctionCall<'ctx>, - list: BasicValueEnum<'ctx>, - element_layout: &Layout<'a>, -) -> BasicValueEnum<'ctx> { - call_bitcode_fn( - env, - &[ - list_to_c_abi(env, list).into(), - roc_function_call.caller.into(), - pass_as_opaque(env, roc_function_call.data), - roc_function_call.inc_n_data.into(), - roc_function_call.data_is_owned.into(), - layout_width(env, element_layout), - ], - bitcode::LIST_ALL, - ) -} - -/// List.findUnsafe : List elem, (elem -> Bool) -> { value: elem, found: bool } -pub fn list_find_unsafe<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - layout_ids: &mut LayoutIds<'a>, - roc_function_call: RocFunctionCall<'ctx>, - list: BasicValueEnum<'ctx>, - element_layout: &Layout<'a>, -) -> BasicValueEnum<'ctx> { - let inc_element_fn = build_inc_wrapper(env, layout_ids, element_layout); - let dec_element_fn = build_dec_wrapper(env, layout_ids, element_layout); - - // { value: *const u8, found: bool } - let result = call_bitcode_fn( - env, - &[ - list_to_c_abi(env, list).into(), - roc_function_call.caller.into(), - pass_as_opaque(env, roc_function_call.data), - roc_function_call.inc_n_data.into(), - roc_function_call.data_is_owned.into(), - layout_width(env, element_layout), - inc_element_fn.as_global_value().as_pointer_value().into(), - dec_element_fn.as_global_value().as_pointer_value().into(), - ], - bitcode::LIST_FIND_UNSAFE, - ) - .into_struct_value(); - - // We promised the caller we'd give them back a struct containing the element - // loaded on the stack, so we do that now. The element can't be loaded directly - // in the Zig definition called above, because we don't know the size of the - // element until user compile time, which is later than the compile time of bitcode defs. - - let value_u8_ptr_int = env - .builder - .build_extract_value(result, 0, "get_value_ptr_int") - .unwrap() - .into_int_value(); - - let found_u8 = env - .builder - .build_extract_value(result, 1, "get_found") - .unwrap() - .into_int_value(); - - let found = env - .builder - .build_int_cast(found_u8, env.context.bool_type(), "found_as_bool"); - - let start_block = env.builder.get_insert_block().unwrap(); - let parent = start_block.get_parent().unwrap(); - - let if_not_null = env.context.append_basic_block(parent, "if_not_null"); - let done_block = env.context.append_basic_block(parent, "done"); - - let value_bt = basic_type_from_layout(env, element_layout); - let default = value_bt.const_zero(); - - env.builder - .build_conditional_branch(found, if_not_null, done_block); - - env.builder.position_at_end(if_not_null); - - let value_ptr = env.builder.build_int_to_ptr( - value_u8_ptr_int, - value_bt.ptr_type(AddressSpace::Generic), - "get_value_ptr", - ); - - let loaded = env.builder.build_load(value_ptr, "load_value"); - env.builder.build_unconditional_branch(done_block); - - env.builder.position_at_end(done_block); - let result_phi = env.builder.build_phi(value_bt, "result"); - - result_phi.add_incoming(&[(&default, start_block), (&loaded, if_not_null)]); - - let value = result_phi.as_basic_value(); - - let result = env - .context - .struct_type(&[value_bt, env.context.bool_type().into()], false) - .const_zero(); - - let result = env - .builder - .build_insert_value(result, value, 0, "insert_value") - .unwrap(); - - env.builder - .build_insert_value(result, found, 1, "insert_found") - .unwrap() - .into_struct_value() - .into() -} - -pub fn decrementing_elem_loop<'ctx, LoopFn>( - builder: &Builder<'ctx>, - ctx: &'ctx Context, - parent: FunctionValue<'ctx>, - ptr: PointerValue<'ctx>, - len: IntValue<'ctx>, - index_name: &str, - mut loop_fn: LoopFn, -) -> PointerValue<'ctx> -where - LoopFn: FnMut(IntValue<'ctx>, BasicValueEnum<'ctx>), -{ - decrementing_index_loop(builder, ctx, parent, len, index_name, |index| { - // The pointer to the element in the list - let elem_ptr = unsafe { builder.build_in_bounds_gep(ptr, &[index], "load_index") }; - - let elem = builder.build_load(elem_ptr, "get_elem"); - - loop_fn(index, elem); - }) -} - -// a for-loop from the back to the front -fn decrementing_index_loop<'ctx, LoopFn>( - builder: &Builder<'ctx>, - ctx: &'ctx Context, - parent: FunctionValue<'ctx>, - end: IntValue<'ctx>, - index_name: &str, - mut loop_fn: LoopFn, -) -> PointerValue<'ctx> -where - LoopFn: FnMut(IntValue<'ctx>), -{ - // constant 1i64 - let one = ctx.i64_type().const_int(1, false); - - // allocate a stack slot for the current index - let index_alloca = builder.build_alloca(ctx.i64_type(), index_name); - - // we assume `end` is the length of the list - // the final index is therefore `end - 1` - let end_index = builder.build_int_sub(end, one, "end_index"); - builder.build_store(index_alloca, end_index); - - let loop_bb = ctx.append_basic_block(parent, "loop"); - builder.build_unconditional_branch(loop_bb); - builder.position_at_end(loop_bb); - - let current_index = builder - .build_load(index_alloca, index_name) - .into_int_value(); - - let next_index = builder.build_int_sub(current_index, one, "nextindex"); - - builder.build_store(index_alloca, next_index); - - // The body of the loop - loop_fn(current_index); - - // #index >= 0 - let condition = builder.build_int_compare( - IntPredicate::SGE, - next_index, - ctx.i64_type().const_zero(), - "bounds_check", - ); - - let after_loop_bb = ctx.append_basic_block(parent, "after_outer_loop_1"); - - builder.build_conditional_branch(condition, loop_bb, after_loop_bb); - builder.position_at_end(after_loop_bb); - - index_alloca -} - -pub fn incrementing_elem_loop<'a, 'ctx, 'env, LoopFn>( - env: &Env<'a, 'ctx, 'env>, - parent: FunctionValue<'ctx>, - element_layout: Layout<'a>, - ptr: PointerValue<'ctx>, - len: IntValue<'ctx>, - index_name: &str, - mut loop_fn: LoopFn, -) -> PointerValue<'ctx> -where - LoopFn: FnMut(IntValue<'ctx>, BasicValueEnum<'ctx>), -{ - let builder = env.builder; - - incrementing_index_loop(env, parent, len, index_name, |index| { - // The pointer to the element in the list - let element_ptr = unsafe { builder.build_in_bounds_gep(ptr, &[index], "load_index") }; - - let elem = load_roc_value( - env, - element_layout, - element_ptr, - "incrementing_element_loop_load", - ); - - loop_fn(index, elem); - }) -} - -// This helper simulates a basic for loop, where -// and index increments up from 0 to some end value -pub fn incrementing_index_loop<'a, 'ctx, 'env, LoopFn>( - env: &Env<'a, 'ctx, 'env>, - parent: FunctionValue<'ctx>, - end: IntValue<'ctx>, - index_name: &str, - mut loop_fn: LoopFn, -) -> PointerValue<'ctx> -where - LoopFn: FnMut(IntValue<'ctx>), -{ - let ctx = env.context; - let builder = env.builder; - - let entry = env.builder.get_insert_block().unwrap(); - - // constant 1i64 - let one = env.ptr_int().const_int(1, false); - - // allocate a stack slot for the current index - let index_alloca = builder.build_alloca(env.ptr_int(), index_name); - builder.build_store(index_alloca, env.ptr_int().const_zero()); - - let loop_bb = ctx.append_basic_block(parent, "loop"); - builder.build_unconditional_branch(loop_bb); - builder.position_at_end(loop_bb); - - let current_index_phi = env.builder.build_phi(env.ptr_int(), "current_index"); - let current_index = current_index_phi.as_basic_value().into_int_value(); - - let next_index = builder.build_int_add(current_index, one, "next_index"); - - current_index_phi.add_incoming(&[(&next_index, loop_bb), (&env.ptr_int().const_zero(), entry)]); - - // The body of the loop - loop_fn(current_index); - - // #index < end - let loop_end_cond = bounds_check_comparison(builder, next_index, end); - - let after_loop_bb = ctx.append_basic_block(parent, "after_outer_loop_2"); - - builder.build_conditional_branch(loop_end_cond, loop_bb, after_loop_bb); - builder.position_at_end(after_loop_bb); - - index_alloca -} - -pub fn build_basic_phi2<'a, 'ctx, 'env, PassFn, FailFn>( - env: &Env<'a, 'ctx, 'env>, - parent: FunctionValue<'ctx>, - comparison: IntValue<'ctx>, - mut build_pass: PassFn, - mut build_fail: FailFn, - ret_type: BasicTypeEnum<'ctx>, -) -> BasicValueEnum<'ctx> -where - PassFn: FnMut() -> BasicValueEnum<'ctx>, - FailFn: FnMut() -> BasicValueEnum<'ctx>, -{ - let builder = env.builder; - let context = env.context; - - // build blocks - let then_block = context.append_basic_block(parent, "then"); - let else_block = context.append_basic_block(parent, "else"); - let cont_block = context.append_basic_block(parent, "branchcont"); - - builder.build_conditional_branch(comparison, then_block, else_block); - - // build then block - builder.position_at_end(then_block); - let then_val = build_pass(); - builder.build_unconditional_branch(cont_block); - - let then_block = builder.get_insert_block().unwrap(); - - // build else block - builder.position_at_end(else_block); - let else_val = build_fail(); - builder.build_unconditional_branch(cont_block); - - let else_block = builder.get_insert_block().unwrap(); - - // emit merge block - builder.position_at_end(cont_block); - - let phi = builder.build_phi(ret_type, "branch"); - - phi.add_incoming(&[(&then_val, then_block), (&else_val, else_block)]); - - phi.as_basic_value() -} - -pub fn empty_polymorphic_list<'a, 'ctx, 'env>(env: &Env<'a, 'ctx, 'env>) -> BasicValueEnum<'ctx> { - let struct_type = super::convert::zig_list_type(env); - - // The pointer should be null (aka zero) and the length should be zero, - // so the whole struct should be a const_zero - BasicValueEnum::StructValue(struct_type.const_zero()) -} - -pub fn load_list<'ctx>( - builder: &Builder<'ctx>, - wrapper_struct: StructValue<'ctx>, - ptr_type: PointerType<'ctx>, -) -> (IntValue<'ctx>, PointerValue<'ctx>) { - let ptr = load_list_ptr(builder, wrapper_struct, ptr_type); - - let length = builder - .build_extract_value(wrapper_struct, Builtin::WRAPPER_LEN, "list_len") - .unwrap() - .into_int_value(); - - (length, ptr) -} - -pub fn load_list_ptr<'ctx>( - builder: &Builder<'ctx>, - wrapper_struct: StructValue<'ctx>, - ptr_type: PointerType<'ctx>, -) -> PointerValue<'ctx> { - // a `*mut u8` pointer - let generic_ptr = builder - .build_extract_value(wrapper_struct, Builtin::WRAPPER_PTR, "read_list_ptr") - .unwrap() - .into_pointer_value(); - - // cast to the expected pointer type - cast_basic_basic(builder, generic_ptr.into(), ptr_type.into()).into_pointer_value() -} - -pub fn allocate_list<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - elem_layout: &Layout<'a>, - number_of_elements: IntValue<'ctx>, -) -> PointerValue<'ctx> { - let builder = env.builder; - - let len_type = env.ptr_int(); - let elem_bytes = elem_layout.stack_size(env.target_info) as u64; - let bytes_per_element = len_type.const_int(elem_bytes, false); - let number_of_data_bytes = - builder.build_int_mul(bytes_per_element, number_of_elements, "data_length"); - - let basic_type = basic_type_from_layout(env, elem_layout); - let alignment_bytes = elem_layout.alignment_bytes(env.target_info); - allocate_with_refcount_help(env, basic_type, alignment_bytes, number_of_data_bytes) -} - -pub fn store_list<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - pointer_to_first_element: PointerValue<'ctx>, - len: IntValue<'ctx>, -) -> BasicValueEnum<'ctx> { - let builder = env.builder; - - let struct_type = super::convert::zig_list_type(env); - - // Store the pointer - let mut struct_val = builder - .build_insert_value( - struct_type.get_undef(), - pass_as_opaque(env, pointer_to_first_element), - Builtin::WRAPPER_PTR, - "insert_ptr_store_list", - ) - .unwrap(); - - // Store the length - struct_val = builder - .build_insert_value(struct_val, len, Builtin::WRAPPER_LEN, "insert_len") - .unwrap(); - - builder.build_bitcast( - struct_val.into_struct_value(), - super::convert::zig_list_type(env), - "cast_collection", - ) -} - -pub fn decref<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - wrapper_struct: StructValue<'ctx>, - alignment: u32, -) { - let (_, pointer) = load_list( - env.builder, - wrapper_struct, - env.context.i8_type().ptr_type(AddressSpace::Generic), - ); - - crate::llvm::refcounting::decref_pointer_check_null(env, pointer, alignment); -} diff --git a/compiler/gen_llvm/src/llvm/build_str.rs b/compiler/gen_llvm/src/llvm/build_str.rs deleted file mode 100644 index 12b24b2dea..0000000000 --- a/compiler/gen_llvm/src/llvm/build_str.rs +++ /dev/null @@ -1,225 +0,0 @@ -use crate::llvm::bitcode::{call_bitcode_fn, call_str_bitcode_fn, call_void_bitcode_fn}; -use crate::llvm::build::{Env, Scope}; -use crate::llvm::build_list::{allocate_list, pass_update_mode, store_list}; -use inkwell::builder::Builder; -use inkwell::values::{BasicValueEnum, IntValue, PointerValue, StructValue}; -use inkwell::AddressSpace; -use morphic_lib::UpdateMode; -use roc_builtins::bitcode::{self, IntWidth}; -use roc_module::symbol::Symbol; -use roc_mono::layout::{Builtin, Layout}; -use roc_target::PtrWidth; - -use super::build::{create_entry_block_alloca, load_symbol}; -use super::build_list::list_symbol_to_c_abi; - -pub static CHAR_LAYOUT: Layout = Layout::u8(); - -/// Str.split : Str, Str -> List Str -pub fn str_split<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - scope: &Scope<'a, 'ctx>, - str_symbol: Symbol, - delimiter_symbol: Symbol, -) -> BasicValueEnum<'ctx> { - let builder = env.builder; - - let string = load_symbol(scope, &str_symbol); - let delimiter = load_symbol(scope, &delimiter_symbol); - - let segment_count = - call_bitcode_fn(env, &[string, delimiter], bitcode::STR_COUNT_SEGMENTS).into_int_value(); - - // a pointer to the elements - let ret_list_ptr = allocate_list(env, &Layout::Builtin(Builtin::Str), segment_count); - - // get the RocStr type defined by zig - let roc_str_type = env.module.get_struct_type("str.RocStr").unwrap(); - - // convert `*mut { *mut u8, i64 }` to `*mut RocStr` - let ret_list_ptr_zig_rocstr = builder.build_bitcast( - ret_list_ptr, - roc_str_type.ptr_type(AddressSpace::Generic), - "convert_to_zig_rocstr", - ); - - call_void_bitcode_fn( - env, - &[ret_list_ptr_zig_rocstr, string, delimiter], - bitcode::STR_STR_SPLIT_IN_PLACE, - ); - - store_list(env, ret_list_ptr, segment_count) -} - -pub fn str_symbol_to_c_abi<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - scope: &Scope<'a, 'ctx>, - symbol: Symbol, -) -> PointerValue<'ctx> { - let string = load_symbol(scope, &symbol); - - str_to_c_abi(env, string) -} - -pub fn str_to_c_abi<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - value: BasicValueEnum<'ctx>, -) -> PointerValue<'ctx> { - let parent = env - .builder - .get_insert_block() - .and_then(|b| b.get_parent()) - .unwrap(); - - let str_type = super::convert::zig_str_type(env); - let string_alloca = create_entry_block_alloca(env, parent, str_type.into(), "str_alloca"); - - env.builder.build_store(string_alloca, value); - - string_alloca -} - -pub fn destructure<'ctx>( - builder: &Builder<'ctx>, - wrapper_struct: StructValue<'ctx>, -) -> (PointerValue<'ctx>, IntValue<'ctx>) { - let length = builder - .build_extract_value(wrapper_struct, Builtin::WRAPPER_LEN, "list_len") - .unwrap() - .into_int_value(); - - // a `*mut u8` pointer - let generic_ptr = builder - .build_extract_value(wrapper_struct, Builtin::WRAPPER_PTR, "read_list_ptr") - .unwrap() - .into_pointer_value(); - - (generic_ptr, length) -} - -/// Str.fromInt : Int -> Str -pub fn str_from_int<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - value: IntValue<'ctx>, - int_width: IntWidth, -) -> BasicValueEnum<'ctx> { - call_str_bitcode_fn(env, &[value.into()], &bitcode::STR_FROM_INT[int_width]) -} - -fn decode_from_utf8_result<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - pointer: PointerValue<'ctx>, -) -> StructValue<'ctx> { - let builder = env.builder; - let ctx = env.context; - - let fields = match env.target_info.ptr_width() { - PtrWidth::Bytes4 | PtrWidth::Bytes8 => [ - env.ptr_int().into(), - super::convert::zig_str_type(env).into(), - env.context.bool_type().into(), - ctx.i8_type().into(), - ], - }; - - let record_type = env.context.struct_type(&fields, false); - - match env.target_info.ptr_width() { - PtrWidth::Bytes4 | PtrWidth::Bytes8 => { - let result_ptr_cast = env - .builder - .build_bitcast( - pointer, - record_type.ptr_type(AddressSpace::Generic), - "to_unnamed", - ) - .into_pointer_value(); - - builder - .build_load(result_ptr_cast, "load_utf8_validate_bytes_result") - .into_struct_value() - } - } -} - -/// Str.fromUtf8 : List U8, { count : Nat, start : Nat } -> { a : Bool, b : Str, c : Nat, d : I8 } -pub fn str_from_utf8_range<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - scope: &Scope<'a, 'ctx>, - list: Symbol, - count_and_start: StructValue<'ctx>, -) -> BasicValueEnum<'ctx> { - let builder = env.builder; - - let result_type = env.module.get_struct_type("str.FromUtf8Result").unwrap(); - let result_ptr = builder.build_alloca(result_type, "alloca_utf8_validate_bytes_result"); - - let count = env - .builder - .build_extract_value(count_and_start, 0, "get_count") - .unwrap(); - - let start = env - .builder - .build_extract_value(count_and_start, 1, "get_start") - .unwrap(); - - call_void_bitcode_fn( - env, - &[ - list_symbol_to_c_abi(env, scope, list).into(), - count, - start, - result_ptr.into(), - ], - bitcode::STR_FROM_UTF8_RANGE, - ); - - decode_from_utf8_result(env, result_ptr).into() -} - -/// Str.fromUtf8 : List U8 -> { a : Bool, b : Str, c : Nat, d : I8 } -pub fn str_from_utf8<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - scope: &Scope<'a, 'ctx>, - list: Symbol, - update_mode: UpdateMode, -) -> BasicValueEnum<'ctx> { - let builder = env.builder; - - let result_type = env.module.get_struct_type("str.FromUtf8Result").unwrap(); - let result_ptr = builder.build_alloca(result_type, "alloca_utf8_validate_bytes_result"); - - call_void_bitcode_fn( - env, - &[ - list_symbol_to_c_abi(env, scope, list).into(), - pass_update_mode(env, update_mode), - result_ptr.into(), - ], - bitcode::STR_FROM_UTF8, - ); - - decode_from_utf8_result(env, result_ptr).into() -} - -/// Str.fromFloat : Int -> Str -pub fn str_from_float<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - scope: &Scope<'a, 'ctx>, - int_symbol: Symbol, -) -> BasicValueEnum<'ctx> { - let float = load_symbol(scope, &int_symbol); - - call_str_bitcode_fn(env, &[float], bitcode::STR_FROM_FLOAT) -} - -/// Str.equal : Str, Str -> Bool -pub fn str_equal<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - value1: BasicValueEnum<'ctx>, - value2: BasicValueEnum<'ctx>, -) -> BasicValueEnum<'ctx> { - call_bitcode_fn(env, &[value1, value2], bitcode::STR_EQUAL) -} diff --git a/compiler/gen_llvm/src/llvm/compare.rs b/compiler/gen_llvm/src/llvm/compare.rs deleted file mode 100644 index 8af0ea575c..0000000000 --- a/compiler/gen_llvm/src/llvm/compare.rs +++ /dev/null @@ -1,1419 +0,0 @@ -use crate::llvm::build::{get_tag_id, tag_pointer_clear_tag_id, Env, FAST_CALL_CONV}; -use crate::llvm::build_list::{list_len, load_list_ptr}; -use crate::llvm::build_str::str_equal; -use crate::llvm::convert::basic_type_from_layout; -use bumpalo::collections::Vec; -use inkwell::types::BasicType; -use inkwell::values::{ - BasicValue, BasicValueEnum, FunctionValue, IntValue, PointerValue, StructValue, -}; -use inkwell::{AddressSpace, FloatPredicate, IntPredicate}; -use roc_builtins::bitcode; -use roc_builtins::bitcode::{FloatWidth, IntWidth}; -use roc_module::symbol::Symbol; -use roc_mono::layout::{Builtin, Layout, LayoutIds, UnionLayout}; - -use super::build::{dec_binop_with_unchecked, load_roc_value, use_roc_value}; -use super::convert::argument_type_from_union_layout; - -#[derive(Clone, Debug)] -enum WhenRecursive<'a> { - Unreachable, - Loop(UnionLayout<'a>), -} - -pub fn generic_eq<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - layout_ids: &mut LayoutIds<'a>, - lhs_val: BasicValueEnum<'ctx>, - rhs_val: BasicValueEnum<'ctx>, - lhs_layout: &Layout<'a>, - rhs_layout: &Layout<'a>, -) -> BasicValueEnum<'ctx> { - build_eq( - env, - layout_ids, - lhs_val, - rhs_val, - lhs_layout, - rhs_layout, - WhenRecursive::Unreachable, - ) -} - -pub fn generic_neq<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - layout_ids: &mut LayoutIds<'a>, - lhs_val: BasicValueEnum<'ctx>, - rhs_val: BasicValueEnum<'ctx>, - lhs_layout: &Layout<'a>, - rhs_layout: &Layout<'a>, -) -> BasicValueEnum<'ctx> { - build_neq( - env, - layout_ids, - lhs_val, - rhs_val, - lhs_layout, - rhs_layout, - WhenRecursive::Unreachable, - ) -} - -fn build_eq_builtin<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - layout_ids: &mut LayoutIds<'a>, - lhs_val: BasicValueEnum<'ctx>, - rhs_val: BasicValueEnum<'ctx>, - builtin: &Builtin<'a>, - when_recursive: WhenRecursive<'a>, -) -> BasicValueEnum<'ctx> { - let int_cmp = |pred, label| { - let int_val = env.builder.build_int_compare( - pred, - lhs_val.into_int_value(), - rhs_val.into_int_value(), - label, - ); - - BasicValueEnum::IntValue(int_val) - }; - - let float_cmp = |pred, label| { - let int_val = env.builder.build_float_compare( - pred, - lhs_val.into_float_value(), - rhs_val.into_float_value(), - label, - ); - - BasicValueEnum::IntValue(int_val) - }; - - match builtin { - Builtin::Int(int_width) => { - use IntWidth::*; - - let name = match int_width { - I128 => "eq_i128", - I64 => "eq_i64", - I32 => "eq_i32", - I16 => "eq_i16", - I8 => "eq_i8", - U128 => "eq_u128", - U64 => "eq_u64", - U32 => "eq_u32", - U16 => "eq_u16", - U8 => "eq_u8", - }; - - int_cmp(IntPredicate::EQ, name) - } - - Builtin::Float(float_width) => { - use FloatWidth::*; - - let name = match float_width { - F128 => "eq_f128", - F64 => "eq_f64", - F32 => "eq_f32", - }; - - float_cmp(FloatPredicate::OEQ, name) - } - - Builtin::Bool => int_cmp(IntPredicate::EQ, "eq_i1"), - Builtin::Decimal => dec_binop_with_unchecked(env, bitcode::DEC_EQ, lhs_val, rhs_val), - - Builtin::Str => str_equal(env, lhs_val, rhs_val), - Builtin::List(elem) => build_list_eq( - env, - layout_ids, - &Layout::Builtin(*builtin), - elem, - lhs_val.into_struct_value(), - rhs_val.into_struct_value(), - when_recursive, - ), - Builtin::Set(_elem) => todo!("equality on Set"), - Builtin::Dict(_key, _value) => todo!("equality on Dict"), - } -} - -fn build_eq<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - layout_ids: &mut LayoutIds<'a>, - lhs_val: BasicValueEnum<'ctx>, - rhs_val: BasicValueEnum<'ctx>, - lhs_layout: &Layout<'a>, - rhs_layout: &Layout<'a>, - when_recursive: WhenRecursive<'a>, -) -> BasicValueEnum<'ctx> { - if lhs_layout != rhs_layout { - panic!( - "Equality of different layouts; did you have a type mismatch?\n{:?} == {:?}", - lhs_layout, rhs_layout - ); - } - - match lhs_layout { - Layout::Builtin(builtin) => { - build_eq_builtin(env, layout_ids, lhs_val, rhs_val, builtin, when_recursive) - } - - Layout::Struct { field_layouts, .. } => build_struct_eq( - env, - layout_ids, - field_layouts, - when_recursive, - lhs_val.into_struct_value(), - rhs_val.into_struct_value(), - ), - - Layout::LambdaSet(_) => unreachable!("cannot compare closures"), - - Layout::Union(union_layout) => build_tag_eq( - env, - layout_ids, - when_recursive, - union_layout, - lhs_val, - rhs_val, - ), - - Layout::Boxed(inner_layout) => build_box_eq( - env, - layout_ids, - when_recursive, - lhs_layout, - inner_layout, - lhs_val, - rhs_val, - ), - - Layout::RecursivePointer => match when_recursive { - WhenRecursive::Unreachable => { - unreachable!("recursion pointers should never be compared directly") - } - - WhenRecursive::Loop(union_layout) => { - let layout = Layout::Union(union_layout); - - let bt = basic_type_from_layout(env, &layout); - - // cast the i64 pointer to a pointer to block of memory - let field1_cast = env - .builder - .build_bitcast(lhs_val, bt, "i64_to_opaque") - .into_pointer_value(); - - let field2_cast = env - .builder - .build_bitcast(rhs_val, bt, "i64_to_opaque") - .into_pointer_value(); - - build_tag_eq( - env, - layout_ids, - WhenRecursive::Loop(union_layout), - &union_layout, - field1_cast.into(), - field2_cast.into(), - ) - } - }, - } -} - -fn build_neq_builtin<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - layout_ids: &mut LayoutIds<'a>, - lhs_val: BasicValueEnum<'ctx>, - rhs_val: BasicValueEnum<'ctx>, - builtin: &Builtin<'a>, - when_recursive: WhenRecursive<'a>, -) -> BasicValueEnum<'ctx> { - let int_cmp = |pred, label| { - let int_val = env.builder.build_int_compare( - pred, - lhs_val.into_int_value(), - rhs_val.into_int_value(), - label, - ); - - BasicValueEnum::IntValue(int_val) - }; - - let float_cmp = |pred, label| { - let int_val = env.builder.build_float_compare( - pred, - lhs_val.into_float_value(), - rhs_val.into_float_value(), - label, - ); - - BasicValueEnum::IntValue(int_val) - }; - - match builtin { - Builtin::Int(int_width) => { - use IntWidth::*; - - let name = match int_width { - I128 => "neq_i128", - I64 => "neq_i64", - I32 => "neq_i32", - I16 => "neq_i16", - I8 => "neq_i8", - U128 => "neq_u128", - U64 => "neq_u64", - U32 => "neq_u32", - U16 => "neq_u16", - U8 => "neq_u8", - }; - - int_cmp(IntPredicate::NE, name) - } - - Builtin::Float(float_width) => { - use FloatWidth::*; - - let name = match float_width { - F128 => "neq_f128", - F64 => "neq_f64", - F32 => "neq_f32", - }; - - float_cmp(FloatPredicate::ONE, name) - } - - Builtin::Bool => int_cmp(IntPredicate::NE, "neq_i1"), - Builtin::Decimal => dec_binop_with_unchecked(env, bitcode::DEC_NEQ, lhs_val, rhs_val), - - Builtin::Str => { - let is_equal = str_equal(env, lhs_val, rhs_val).into_int_value(); - let result: IntValue = env.builder.build_not(is_equal, "negate"); - - result.into() - } - Builtin::List(elem) => { - let is_equal = build_list_eq( - env, - layout_ids, - &Layout::Builtin(*builtin), - elem, - lhs_val.into_struct_value(), - rhs_val.into_struct_value(), - when_recursive, - ) - .into_int_value(); - - let result: IntValue = env.builder.build_not(is_equal, "negate"); - - result.into() - } - Builtin::Set(_elem) => todo!("equality on Set"), - Builtin::Dict(_key, _value) => todo!("equality on Dict"), - } -} - -fn build_neq<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - layout_ids: &mut LayoutIds<'a>, - lhs_val: BasicValueEnum<'ctx>, - rhs_val: BasicValueEnum<'ctx>, - lhs_layout: &Layout<'a>, - rhs_layout: &Layout<'a>, - when_recursive: WhenRecursive<'a>, -) -> BasicValueEnum<'ctx> { - if lhs_layout != rhs_layout { - panic!( - "Inequality of different layouts; did you have a type mismatch?\n{:?} != {:?}", - lhs_layout, rhs_layout - ); - } - - match lhs_layout { - Layout::Builtin(builtin) => { - build_neq_builtin(env, layout_ids, lhs_val, rhs_val, builtin, when_recursive) - } - - Layout::Struct { field_layouts, .. } => { - let is_equal = build_struct_eq( - env, - layout_ids, - field_layouts, - when_recursive, - lhs_val.into_struct_value(), - rhs_val.into_struct_value(), - ) - .into_int_value(); - - let result: IntValue = env.builder.build_not(is_equal, "negate"); - - result.into() - } - - Layout::Union(union_layout) => { - let is_equal = build_tag_eq( - env, - layout_ids, - when_recursive, - union_layout, - lhs_val, - rhs_val, - ) - .into_int_value(); - - let result: IntValue = env.builder.build_not(is_equal, "negate"); - - result.into() - } - - Layout::Boxed(inner_layout) => { - let is_equal = build_box_eq( - env, - layout_ids, - when_recursive, - lhs_layout, - inner_layout, - lhs_val, - rhs_val, - ) - .into_int_value(); - - let result: IntValue = env.builder.build_not(is_equal, "negate"); - - result.into() - } - - Layout::RecursivePointer => { - unreachable!("recursion pointers should never be compared directly") - } - Layout::LambdaSet(_) => unreachable!("cannot compare closure"), - } -} - -fn build_list_eq<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - layout_ids: &mut LayoutIds<'a>, - list_layout: &Layout<'a>, - element_layout: &Layout<'a>, - list1: StructValue<'ctx>, - list2: StructValue<'ctx>, - when_recursive: WhenRecursive<'a>, -) -> BasicValueEnum<'ctx> { - let block = env.builder.get_insert_block().expect("to be in a function"); - let di_location = env.builder.get_current_debug_location().unwrap(); - - let symbol = Symbol::LIST_EQ; - let fn_name = layout_ids - .get(symbol, element_layout) - .to_symbol_string(symbol, &env.interns); - - let function = match env.module.get_function(fn_name.as_str()) { - Some(function_value) => function_value, - None => { - let arg_type = basic_type_from_layout(env, list_layout); - - let function_value = crate::llvm::refcounting::build_header_help( - env, - &fn_name, - env.context.bool_type().into(), - &[arg_type, arg_type], - ); - - build_list_eq_help( - env, - layout_ids, - when_recursive, - function_value, - element_layout, - ); - - function_value - } - }; - - env.builder.position_at_end(block); - env.builder - .set_current_debug_location(env.context, di_location); - let call = env - .builder - .build_call(function, &[list1.into(), list2.into()], "list_eq"); - - call.set_call_convention(FAST_CALL_CONV); - - call.try_as_basic_value().left().unwrap() -} - -fn build_list_eq_help<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - layout_ids: &mut LayoutIds<'a>, - when_recursive: WhenRecursive<'a>, - parent: FunctionValue<'ctx>, - element_layout: &Layout<'a>, -) { - let ctx = env.context; - let builder = env.builder; - - { - use inkwell::debug_info::AsDIScope; - - let func_scope = parent.get_subprogram().unwrap(); - let lexical_block = env.dibuilder.create_lexical_block( - /* scope */ func_scope.as_debug_info_scope(), - /* file */ env.compile_unit.get_file(), - /* line_no */ 0, - /* column_no */ 0, - ); - - let loc = env.dibuilder.create_debug_location( - ctx, - /* line */ 0, - /* column */ 0, - /* current_scope */ lexical_block.as_debug_info_scope(), - /* inlined_at */ None, - ); - builder.set_current_debug_location(ctx, loc); - } - - // Add args to scope - let mut it = parent.get_param_iter(); - let list1 = it.next().unwrap().into_struct_value(); - let list2 = it.next().unwrap().into_struct_value(); - - list1.set_name(Symbol::ARG_1.as_str(&env.interns)); - list2.set_name(Symbol::ARG_2.as_str(&env.interns)); - - let entry = ctx.append_basic_block(parent, "entry"); - env.builder.position_at_end(entry); - - let return_true = ctx.append_basic_block(parent, "return_true"); - let return_false = ctx.append_basic_block(parent, "return_false"); - - // first, check whether the length is equal - - let len1 = list_len(env.builder, list1); - let len2 = list_len(env.builder, list2); - - let length_equal: IntValue = - env.builder - .build_int_compare(IntPredicate::EQ, len1, len2, "bounds_check"); - - let then_block = ctx.append_basic_block(parent, "then"); - - env.builder - .build_conditional_branch(length_equal, then_block, return_false); - - { - // the length is equal; check elements pointwise - env.builder.position_at_end(then_block); - - let builder = env.builder; - let element_type = basic_type_from_layout(env, element_layout); - let ptr_type = element_type.ptr_type(AddressSpace::Generic); - let ptr1 = load_list_ptr(env.builder, list1, ptr_type); - let ptr2 = load_list_ptr(env.builder, list2, ptr_type); - - // we know that len1 == len2 - let end = len1; - - // allocate a stack slot for the current index - let index_alloca = builder.build_alloca(env.ptr_int(), "index"); - builder.build_store(index_alloca, env.ptr_int().const_zero()); - - let loop_bb = ctx.append_basic_block(parent, "loop"); - let body_bb = ctx.append_basic_block(parent, "body"); - let increment_bb = ctx.append_basic_block(parent, "increment"); - - // the "top" of the loop - builder.build_unconditional_branch(loop_bb); - builder.position_at_end(loop_bb); - - let curr_index = builder.build_load(index_alloca, "index").into_int_value(); - - // #index < end - let loop_end_cond = - builder.build_int_compare(IntPredicate::ULT, curr_index, end, "bounds_check"); - - // if we're at the end, and all elements were equal so far, return true - // otherwise check the current elements for equality - builder.build_conditional_branch(loop_end_cond, body_bb, return_true); - - { - // loop body - builder.position_at_end(body_bb); - - let elem1 = { - let elem_ptr = - unsafe { builder.build_in_bounds_gep(ptr1, &[curr_index], "load_index") }; - load_roc_value(env, *element_layout, elem_ptr, "get_elem") - }; - - let elem2 = { - let elem_ptr = - unsafe { builder.build_in_bounds_gep(ptr2, &[curr_index], "load_index") }; - load_roc_value(env, *element_layout, elem_ptr, "get_elem") - }; - - let are_equal = build_eq( - env, - layout_ids, - elem1, - elem2, - element_layout, - element_layout, - when_recursive, - ) - .into_int_value(); - - // if the elements are equal, increment the index and check the next element - // otherwise, return false - builder.build_conditional_branch(are_equal, increment_bb, return_false); - } - - { - env.builder.position_at_end(increment_bb); - - // constant 1isize - let one = env.ptr_int().const_int(1, false); - - let next_index = builder.build_int_add(curr_index, one, "nextindex"); - - builder.build_store(index_alloca, next_index); - - // jump back to the top of the loop - builder.build_unconditional_branch(loop_bb); - } - } - - { - env.builder.position_at_end(return_true); - env.builder - .build_return(Some(&env.context.bool_type().const_int(1, false))); - } - - { - env.builder.position_at_end(return_false); - env.builder - .build_return(Some(&env.context.bool_type().const_int(0, false))); - } -} - -fn build_struct_eq<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - layout_ids: &mut LayoutIds<'a>, - field_layouts: &'a [Layout<'a>], - when_recursive: WhenRecursive<'a>, - struct1: StructValue<'ctx>, - struct2: StructValue<'ctx>, -) -> BasicValueEnum<'ctx> { - let block = env.builder.get_insert_block().expect("to be in a function"); - let di_location = env.builder.get_current_debug_location().unwrap(); - - let struct_layout = Layout::struct_no_name_order(field_layouts); - - let symbol = Symbol::GENERIC_EQ; - let fn_name = layout_ids - .get(symbol, &struct_layout) - .to_symbol_string(symbol, &env.interns); - - let function = match env.module.get_function(fn_name.as_str()) { - Some(function_value) => function_value, - None => { - let arg_type = basic_type_from_layout(env, &struct_layout); - - let function_value = crate::llvm::refcounting::build_header_help( - env, - &fn_name, - env.context.bool_type().into(), - &[arg_type, arg_type], - ); - - build_struct_eq_help( - env, - layout_ids, - function_value, - when_recursive, - field_layouts, - ); - - function_value - } - }; - - env.builder.position_at_end(block); - env.builder - .set_current_debug_location(env.context, di_location); - let call = env - .builder - .build_call(function, &[struct1.into(), struct2.into()], "struct_eq"); - - call.set_call_convention(FAST_CALL_CONV); - - call.try_as_basic_value().left().unwrap() -} - -fn build_struct_eq_help<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - layout_ids: &mut LayoutIds<'a>, - parent: FunctionValue<'ctx>, - when_recursive: WhenRecursive<'a>, - field_layouts: &[Layout<'a>], -) { - let ctx = env.context; - let builder = env.builder; - - { - use inkwell::debug_info::AsDIScope; - - let func_scope = parent.get_subprogram().unwrap(); - let lexical_block = env.dibuilder.create_lexical_block( - /* scope */ func_scope.as_debug_info_scope(), - /* file */ env.compile_unit.get_file(), - /* line_no */ 0, - /* column_no */ 0, - ); - - let loc = env.dibuilder.create_debug_location( - ctx, - /* line */ 0, - /* column */ 0, - /* current_scope */ lexical_block.as_debug_info_scope(), - /* inlined_at */ None, - ); - builder.set_current_debug_location(ctx, loc); - } - - // Add args to scope - let mut it = parent.get_param_iter(); - let struct1 = it.next().unwrap().into_struct_value(); - let struct2 = it.next().unwrap().into_struct_value(); - - struct1.set_name(Symbol::ARG_1.as_str(&env.interns)); - struct2.set_name(Symbol::ARG_2.as_str(&env.interns)); - - let entry = ctx.append_basic_block(parent, "entry"); - let start = ctx.append_basic_block(parent, "start"); - env.builder.position_at_end(entry); - env.builder.build_unconditional_branch(start); - - let return_true = ctx.append_basic_block(parent, "return_true"); - let return_false = ctx.append_basic_block(parent, "return_false"); - - let mut current = start; - - for (index, field_layout) in field_layouts.iter().enumerate() { - env.builder.position_at_end(current); - - let field1 = env - .builder - .build_extract_value(struct1, index as u32, "eq_field") - .unwrap(); - - let field2 = env - .builder - .build_extract_value(struct2, index as u32, "eq_field") - .unwrap(); - - let are_equal = if let Layout::RecursivePointer = field_layout { - match &when_recursive { - WhenRecursive::Unreachable => { - unreachable!("The current layout should not be recursive, but is") - } - WhenRecursive::Loop(union_layout) => { - let field_layout = Layout::Union(*union_layout); - - let bt = basic_type_from_layout(env, &field_layout); - - // cast the i64 pointer to a pointer to block of memory - let field1_cast = env - .builder - .build_bitcast(field1, bt, "i64_to_opaque") - .into_pointer_value(); - - let field2_cast = env - .builder - .build_bitcast(field2, bt, "i64_to_opaque") - .into_pointer_value(); - - build_eq( - env, - layout_ids, - field1_cast.into(), - field2_cast.into(), - &field_layout, - &field_layout, - WhenRecursive::Loop(*union_layout), - ) - .into_int_value() - } - } - } else { - build_eq( - env, - layout_ids, - use_roc_value(env, *field_layout, field1, "field1"), - use_roc_value(env, *field_layout, field2, "field2"), - field_layout, - field_layout, - when_recursive.clone(), - ) - .into_int_value() - }; - - current = ctx.append_basic_block(parent, &format!("eq_step_{}", index)); - - env.builder - .build_conditional_branch(are_equal, current, return_false); - } - - env.builder.position_at_end(current); - env.builder.build_unconditional_branch(return_true); - - { - env.builder.position_at_end(return_true); - env.builder - .build_return(Some(&env.context.bool_type().const_int(1, false))); - } - - { - env.builder.position_at_end(return_false); - env.builder - .build_return(Some(&env.context.bool_type().const_int(0, false))); - } -} - -fn build_tag_eq<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - layout_ids: &mut LayoutIds<'a>, - when_recursive: WhenRecursive<'a>, - union_layout: &UnionLayout<'a>, - tag1: BasicValueEnum<'ctx>, - tag2: BasicValueEnum<'ctx>, -) -> BasicValueEnum<'ctx> { - let block = env.builder.get_insert_block().expect("to be in a function"); - let di_location = env.builder.get_current_debug_location().unwrap(); - - let tag_layout = Layout::Union(*union_layout); - let symbol = Symbol::GENERIC_EQ; - let fn_name = layout_ids - .get(symbol, &tag_layout) - .to_symbol_string(symbol, &env.interns); - - let function = match env.module.get_function(fn_name.as_str()) { - Some(function_value) => function_value, - None => { - let arg_type = argument_type_from_union_layout(env, union_layout); - - let function_value = crate::llvm::refcounting::build_header_help( - env, - &fn_name, - env.context.bool_type().into(), - &[arg_type, arg_type], - ); - - build_tag_eq_help( - env, - layout_ids, - when_recursive, - function_value, - union_layout, - ); - - function_value - } - }; - - env.builder.position_at_end(block); - env.builder - .set_current_debug_location(env.context, di_location); - let call = env - .builder - .build_call(function, &[tag1.into(), tag2.into()], "tag_eq"); - - call.set_call_convention(FAST_CALL_CONV); - - call.try_as_basic_value().left().unwrap() -} - -fn build_tag_eq_help<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - layout_ids: &mut LayoutIds<'a>, - when_recursive: WhenRecursive<'a>, - parent: FunctionValue<'ctx>, - union_layout: &UnionLayout<'a>, -) { - let ctx = env.context; - let builder = env.builder; - - { - use inkwell::debug_info::AsDIScope; - - let func_scope = parent.get_subprogram().unwrap(); - let lexical_block = env.dibuilder.create_lexical_block( - /* scope */ func_scope.as_debug_info_scope(), - /* file */ env.compile_unit.get_file(), - /* line_no */ 0, - /* column_no */ 0, - ); - - let loc = env.dibuilder.create_debug_location( - ctx, - /* line */ 0, - /* column */ 0, - /* current_scope */ lexical_block.as_debug_info_scope(), - /* inlined_at */ None, - ); - builder.set_current_debug_location(ctx, loc); - } - - // Add args to scope - let mut it = parent.get_param_iter(); - let tag1 = it.next().unwrap(); - let tag2 = it.next().unwrap(); - - tag1.set_name(Symbol::ARG_1.as_str(&env.interns)); - tag2.set_name(Symbol::ARG_2.as_str(&env.interns)); - - let entry = ctx.append_basic_block(parent, "entry"); - - let return_true = ctx.append_basic_block(parent, "return_true"); - let return_false = ctx.append_basic_block(parent, "return_false"); - - { - env.builder.position_at_end(return_false); - env.builder - .build_return(Some(&env.context.bool_type().const_int(0, false))); - } - - { - env.builder.position_at_end(return_true); - env.builder - .build_return(Some(&env.context.bool_type().const_int(1, false))); - } - - env.builder.position_at_end(entry); - - use UnionLayout::*; - - match union_layout { - NonRecursive(&[]) => { - // we're comparing empty tag unions; this code is effectively unreachable - env.builder.build_unreachable(); - } - NonRecursive(tags) => { - let ptr_equal = env.builder.build_int_compare( - IntPredicate::EQ, - env.builder - .build_ptr_to_int(tag1.into_pointer_value(), env.ptr_int(), "pti"), - env.builder - .build_ptr_to_int(tag2.into_pointer_value(), env.ptr_int(), "pti"), - "compare_pointers", - ); - - let compare_tag_ids = ctx.append_basic_block(parent, "compare_tag_ids"); - - env.builder - .build_conditional_branch(ptr_equal, return_true, compare_tag_ids); - - env.builder.position_at_end(compare_tag_ids); - - let id1 = get_tag_id(env, parent, union_layout, tag1); - let id2 = get_tag_id(env, parent, union_layout, tag2); - - // clear the tag_id so we get a pointer to the actual data - let tag1 = tag1.into_pointer_value(); - let tag2 = tag2.into_pointer_value(); - - let compare_tag_fields = ctx.append_basic_block(parent, "compare_tag_fields"); - - let same_tag = - env.builder - .build_int_compare(IntPredicate::EQ, id1, id2, "compare_tag_id"); - - env.builder - .build_conditional_branch(same_tag, compare_tag_fields, return_false); - - env.builder.position_at_end(compare_tag_fields); - - // switch on all the tag ids - - let mut cases = Vec::with_capacity_in(tags.len(), env.arena); - - for (tag_id, field_layouts) in tags.iter().enumerate() { - let block = env.context.append_basic_block(parent, "tag_id_modify"); - env.builder.position_at_end(block); - - let answer = eq_ptr_to_struct( - env, - layout_ids, - union_layout, - Some(when_recursive.clone()), - field_layouts, - tag1, - tag2, - ); - - env.builder.build_return(Some(&answer)); - - cases.push((id1.get_type().const_int(tag_id as u64, false), block)); - } - - env.builder.position_at_end(compare_tag_fields); - - match cases.pop() { - Some((_, default)) => { - env.builder.build_switch(id1, default, &cases); - } - None => { - // we're comparing empty tag unions; this code is effectively unreachable - env.builder.build_unreachable(); - } - } - } - Recursive(tags) => { - let ptr_equal = env.builder.build_int_compare( - IntPredicate::EQ, - env.builder - .build_ptr_to_int(tag1.into_pointer_value(), env.ptr_int(), "pti"), - env.builder - .build_ptr_to_int(tag2.into_pointer_value(), env.ptr_int(), "pti"), - "compare_pointers", - ); - - let compare_tag_ids = ctx.append_basic_block(parent, "compare_tag_ids"); - - env.builder - .build_conditional_branch(ptr_equal, return_true, compare_tag_ids); - - env.builder.position_at_end(compare_tag_ids); - - let id1 = get_tag_id(env, parent, union_layout, tag1); - let id2 = get_tag_id(env, parent, union_layout, tag2); - - // clear the tag_id so we get a pointer to the actual data - let tag1 = tag_pointer_clear_tag_id(env, tag1.into_pointer_value()); - let tag2 = tag_pointer_clear_tag_id(env, tag2.into_pointer_value()); - - let compare_tag_fields = ctx.append_basic_block(parent, "compare_tag_fields"); - - let same_tag = - env.builder - .build_int_compare(IntPredicate::EQ, id1, id2, "compare_tag_id"); - - env.builder - .build_conditional_branch(same_tag, compare_tag_fields, return_false); - - env.builder.position_at_end(compare_tag_fields); - - // switch on all the tag ids - - let mut cases = Vec::with_capacity_in(tags.len(), env.arena); - - for (tag_id, field_layouts) in tags.iter().enumerate() { - let block = env.context.append_basic_block(parent, "tag_id_modify"); - env.builder.position_at_end(block); - - let answer = eq_ptr_to_struct( - env, - layout_ids, - union_layout, - None, - field_layouts, - tag1, - tag2, - ); - - env.builder.build_return(Some(&answer)); - - cases.push((id1.get_type().const_int(tag_id as u64, false), block)); - } - - env.builder.position_at_end(compare_tag_fields); - - let default = cases.pop().unwrap().1; - - env.builder.build_switch(id1, default, &cases); - } - NullableUnwrapped { other_fields, .. } => { - let ptr_equal = env.builder.build_int_compare( - IntPredicate::EQ, - env.builder - .build_ptr_to_int(tag1.into_pointer_value(), env.ptr_int(), "pti"), - env.builder - .build_ptr_to_int(tag2.into_pointer_value(), env.ptr_int(), "pti"), - "compare_pointers", - ); - - let check_for_null = ctx.append_basic_block(parent, "check_for_null"); - let compare_other = ctx.append_basic_block(parent, "compare_other"); - - env.builder - .build_conditional_branch(ptr_equal, return_true, check_for_null); - - // check for NULL - - env.builder.position_at_end(check_for_null); - - let is_null_1 = env - .builder - .build_is_null(tag1.into_pointer_value(), "is_null"); - - let is_null_2 = env - .builder - .build_is_null(tag2.into_pointer_value(), "is_null"); - - let either_null = env.builder.build_or(is_null_1, is_null_2, "either_null"); - - // logic: the pointers are not the same, if one is NULL, the other one is not - // therefore the two tags are not equal - env.builder - .build_conditional_branch(either_null, return_false, compare_other); - - // compare the non-null case - - env.builder.position_at_end(compare_other); - - let answer = eq_ptr_to_struct( - env, - layout_ids, - union_layout, - None, - other_fields, - tag1.into_pointer_value(), - tag2.into_pointer_value(), - ); - - env.builder.build_return(Some(&answer)); - } - NullableWrapped { other_tags, .. } => { - let ptr_equal = env.builder.build_int_compare( - IntPredicate::EQ, - env.builder - .build_ptr_to_int(tag1.into_pointer_value(), env.ptr_int(), "pti"), - env.builder - .build_ptr_to_int(tag2.into_pointer_value(), env.ptr_int(), "pti"), - "compare_pointers", - ); - - let check_for_null = ctx.append_basic_block(parent, "check_for_null"); - let compare_other = ctx.append_basic_block(parent, "compare_other"); - - env.builder - .build_conditional_branch(ptr_equal, return_true, check_for_null); - - // check for NULL - - env.builder.position_at_end(check_for_null); - - let is_null_1 = env - .builder - .build_is_null(tag1.into_pointer_value(), "is_null"); - - let is_null_2 = env - .builder - .build_is_null(tag2.into_pointer_value(), "is_null"); - - // Logic: - // - // NULL and NULL => equal - // NULL and not => not equal - // not and NULL => not equal - // not and not => more work required - - let i8_type = env.context.i8_type(); - - let sum = env.builder.build_int_add( - env.builder - .build_int_cast_sign_flag(is_null_1, i8_type, false, "to_u8"), - env.builder - .build_int_cast_sign_flag(is_null_2, i8_type, false, "to_u8"), - "sum_is_null", - ); - - env.builder.build_switch( - sum, - compare_other, - &[ - (i8_type.const_int(2, false), return_true), - (i8_type.const_int(1, false), return_false), - ], - ); - - // compare the non-null case - - env.builder.position_at_end(compare_other); - - let id1 = get_tag_id(env, parent, union_layout, tag1); - let id2 = get_tag_id(env, parent, union_layout, tag2); - - // clear the tag_id so we get a pointer to the actual data - let tag1 = tag_pointer_clear_tag_id(env, tag1.into_pointer_value()); - let tag2 = tag_pointer_clear_tag_id(env, tag2.into_pointer_value()); - - let compare_tag_fields = ctx.append_basic_block(parent, "compare_tag_fields"); - - let same_tag = - env.builder - .build_int_compare(IntPredicate::EQ, id1, id2, "compare_tag_id"); - - env.builder - .build_conditional_branch(same_tag, compare_tag_fields, return_false); - - env.builder.position_at_end(compare_tag_fields); - - // switch on all the tag ids - - let tags = other_tags; - let mut cases = Vec::with_capacity_in(tags.len(), env.arena); - - for (tag_id, field_layouts) in tags.iter().enumerate() { - let block = env.context.append_basic_block(parent, "tag_id_modify"); - env.builder.position_at_end(block); - - let answer = eq_ptr_to_struct( - env, - layout_ids, - union_layout, - None, - field_layouts, - tag1, - tag2, - ); - - env.builder.build_return(Some(&answer)); - - cases.push((id1.get_type().const_int(tag_id as u64, false), block)); - } - - env.builder.position_at_end(compare_tag_fields); - - let default = cases.pop().unwrap().1; - - env.builder.build_switch(id1, default, &cases); - } - NonNullableUnwrapped(field_layouts) => { - let ptr_equal = env.builder.build_int_compare( - IntPredicate::EQ, - env.builder - .build_ptr_to_int(tag1.into_pointer_value(), env.ptr_int(), "pti"), - env.builder - .build_ptr_to_int(tag2.into_pointer_value(), env.ptr_int(), "pti"), - "compare_pointers", - ); - - let compare_fields = ctx.append_basic_block(parent, "compare_fields"); - - env.builder - .build_conditional_branch(ptr_equal, return_true, compare_fields); - - env.builder.position_at_end(compare_fields); - - let answer = eq_ptr_to_struct( - env, - layout_ids, - union_layout, - None, - field_layouts, - tag1.into_pointer_value(), - tag2.into_pointer_value(), - ); - - env.builder.build_return(Some(&answer)); - } - } -} - -fn eq_ptr_to_struct<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - layout_ids: &mut LayoutIds<'a>, - union_layout: &UnionLayout<'a>, - opt_when_recursive: Option>, - field_layouts: &'a [Layout<'a>], - tag1: PointerValue<'ctx>, - tag2: PointerValue<'ctx>, -) -> IntValue<'ctx> { - let struct_layout = Layout::struct_no_name_order(field_layouts); - - let wrapper_type = basic_type_from_layout(env, &struct_layout); - debug_assert!(wrapper_type.is_struct_type()); - - // cast the opaque pointer to a pointer of the correct shape - let struct1_ptr = env - .builder - .build_bitcast( - tag1, - wrapper_type.ptr_type(AddressSpace::Generic), - "opaque_to_correct", - ) - .into_pointer_value(); - - let struct2_ptr = env - .builder - .build_bitcast( - tag2, - wrapper_type.ptr_type(AddressSpace::Generic), - "opaque_to_correct", - ) - .into_pointer_value(); - - let struct1 = env - .builder - .build_load(struct1_ptr, "load_struct1") - .into_struct_value(); - - let struct2 = env - .builder - .build_load(struct2_ptr, "load_struct2") - .into_struct_value(); - - build_struct_eq( - env, - layout_ids, - field_layouts, - opt_when_recursive.unwrap_or(WhenRecursive::Loop(*union_layout)), - struct1, - struct2, - ) - .into_int_value() -} - -/// ---- - -fn build_box_eq<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - layout_ids: &mut LayoutIds<'a>, - when_recursive: WhenRecursive<'a>, - box_layout: &Layout<'a>, - inner_layout: &Layout<'a>, - tag1: BasicValueEnum<'ctx>, - tag2: BasicValueEnum<'ctx>, -) -> BasicValueEnum<'ctx> { - let block = env.builder.get_insert_block().expect("to be in a function"); - let di_location = env.builder.get_current_debug_location().unwrap(); - - let symbol = Symbol::GENERIC_EQ; - let fn_name = layout_ids - .get(symbol, box_layout) - .to_symbol_string(symbol, &env.interns); - - let function = match env.module.get_function(fn_name.as_str()) { - Some(function_value) => function_value, - None => { - let arg_type = basic_type_from_layout(env, box_layout); - - let function_value = crate::llvm::refcounting::build_header_help( - env, - &fn_name, - env.context.bool_type().into(), - &[arg_type, arg_type], - ); - - build_box_eq_help( - env, - layout_ids, - when_recursive, - function_value, - inner_layout, - ); - - function_value - } - }; - - env.builder.position_at_end(block); - env.builder - .set_current_debug_location(env.context, di_location); - let call = env - .builder - .build_call(function, &[tag1.into(), tag2.into()], "tag_eq"); - - call.set_call_convention(FAST_CALL_CONV); - - call.try_as_basic_value().left().unwrap() -} - -fn build_box_eq_help<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - layout_ids: &mut LayoutIds<'a>, - when_recursive: WhenRecursive<'a>, - parent: FunctionValue<'ctx>, - inner_layout: &Layout<'a>, -) { - let ctx = env.context; - let builder = env.builder; - - { - use inkwell::debug_info::AsDIScope; - - let func_scope = parent.get_subprogram().unwrap(); - let lexical_block = env.dibuilder.create_lexical_block( - /* scope */ func_scope.as_debug_info_scope(), - /* file */ env.compile_unit.get_file(), - /* line_no */ 0, - /* column_no */ 0, - ); - - let loc = env.dibuilder.create_debug_location( - ctx, - /* line */ 0, - /* column */ 0, - /* current_scope */ lexical_block.as_debug_info_scope(), - /* inlined_at */ None, - ); - builder.set_current_debug_location(ctx, loc); - } - - // Add args to scope - let mut it = parent.get_param_iter(); - let box1 = it.next().unwrap(); - let box2 = it.next().unwrap(); - - box1.set_name(Symbol::ARG_1.as_str(&env.interns)); - box2.set_name(Symbol::ARG_2.as_str(&env.interns)); - - let return_true = ctx.append_basic_block(parent, "return_true"); - env.builder.position_at_end(return_true); - env.builder - .build_return(Some(&env.context.bool_type().const_all_ones())); - - let entry = ctx.append_basic_block(parent, "entry"); - env.builder.position_at_end(entry); - - let ptr_equal = env.builder.build_int_compare( - IntPredicate::EQ, - env.builder - .build_ptr_to_int(box1.into_pointer_value(), env.ptr_int(), "pti"), - env.builder - .build_ptr_to_int(box2.into_pointer_value(), env.ptr_int(), "pti"), - "compare_pointers", - ); - - let compare_inner_value = ctx.append_basic_block(parent, "compare_inner_value"); - - env.builder - .build_conditional_branch(ptr_equal, return_true, compare_inner_value); - - env.builder.position_at_end(compare_inner_value); - - // clear the tag_id so we get a pointer to the actual data - let box1 = box1.into_pointer_value(); - let box2 = box2.into_pointer_value(); - - let value1 = env.builder.build_load(box1, "load_box1"); - let value2 = env.builder.build_load(box2, "load_box2"); - - let is_equal = build_eq( - env, - layout_ids, - value1, - value2, - inner_layout, - inner_layout, - when_recursive, - ); - - env.builder.build_return(Some(&is_equal)); -} diff --git a/compiler/gen_llvm/src/llvm/convert.rs b/compiler/gen_llvm/src/llvm/convert.rs deleted file mode 100644 index 67e2d21933..0000000000 --- a/compiler/gen_llvm/src/llvm/convert.rs +++ /dev/null @@ -1,286 +0,0 @@ -use crate::llvm::build::Env; -use bumpalo::collections::Vec; -use inkwell::context::Context; -use inkwell::types::{BasicType, BasicTypeEnum, FloatType, IntType, StructType}; -use inkwell::AddressSpace; -use roc_builtins::bitcode::{FloatWidth, IntWidth}; -use roc_mono::layout::{Builtin, Layout, UnionLayout}; -use roc_target::TargetInfo; - -fn basic_type_from_record<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - fields: &[Layout<'_>], -) -> BasicTypeEnum<'ctx> { - let mut field_types = Vec::with_capacity_in(fields.len(), env.arena); - - for field_layout in fields.iter() { - field_types.push(basic_type_from_layout(env, field_layout)); - } - - env.context - .struct_type(field_types.into_bump_slice(), false) - .as_basic_type_enum() -} - -pub fn basic_type_from_layout<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - layout: &Layout<'_>, -) -> BasicTypeEnum<'ctx> { - use Layout::*; - - match layout { - Struct { - field_layouts: sorted_fields, - .. - } => basic_type_from_record(env, sorted_fields), - LambdaSet(lambda_set) => basic_type_from_layout(env, &lambda_set.runtime_representation()), - Boxed(inner_layout) => { - let inner_type = basic_type_from_layout(env, inner_layout); - - inner_type.ptr_type(AddressSpace::Generic).into() - } - Union(union_layout) => basic_type_from_union_layout(env, union_layout), - RecursivePointer => env - .context - .i64_type() - .ptr_type(AddressSpace::Generic) - .as_basic_type_enum(), - - Builtin(builtin) => basic_type_from_builtin(env, builtin), - } -} - -pub fn basic_type_from_union_layout<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - union_layout: &UnionLayout<'_>, -) -> BasicTypeEnum<'ctx> { - use UnionLayout::*; - - let tag_id_type = basic_type_from_layout(env, &union_layout.tag_id_layout()); - - match union_layout { - NonRecursive(tags) => { - let data = block_of_memory_slices(env.context, tags, env.target_info); - - env.context.struct_type(&[data, tag_id_type], false).into() - } - Recursive(tags) - | NullableWrapped { - other_tags: tags, .. - } => { - let data = block_of_memory_slices(env.context, tags, env.target_info); - - if union_layout.stores_tag_id_as_data(env.target_info) { - env.context - .struct_type(&[data, tag_id_type], false) - .ptr_type(AddressSpace::Generic) - .into() - } else { - data.ptr_type(AddressSpace::Generic).into() - } - } - NullableUnwrapped { other_fields, .. } => { - let block = block_of_memory_slices(env.context, &[other_fields], env.target_info); - block.ptr_type(AddressSpace::Generic).into() - } - NonNullableUnwrapped(fields) => { - let block = block_of_memory_slices(env.context, &[fields], env.target_info); - block.ptr_type(AddressSpace::Generic).into() - } - } -} - -pub fn basic_type_from_builtin<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - builtin: &Builtin<'_>, -) -> BasicTypeEnum<'ctx> { - use Builtin::*; - - let context = env.context; - - match builtin { - Int(int_width) => int_type_from_int_width(env, *int_width).as_basic_type_enum(), - Float(float_width) => float_type_from_float_width(env, *float_width).as_basic_type_enum(), - Bool => context.bool_type().as_basic_type_enum(), - Decimal => context.i128_type().as_basic_type_enum(), - Dict(_, _) => zig_dict_type(env).into(), - Set(_) => zig_dict_type(env).into(), - List(_) => zig_list_type(env).into(), - Str => zig_str_type(env).into(), - } -} - -/// Turn a layout into a BasicType that we use in LLVM function arguments. -/// -/// This makes it possible to pass values as something different from how they are typically stored. -/// Current differences -/// -/// - tag unions are passed by-reference. That means that -/// * `f : [Some I64, None] -> I64` is typed `{ { i64, i8 }, i64 }* -> i64` -/// * `f : { x : [Some I64, None] } -> I64 is typed `{ { { i64, i8 }, i64 } } -> i64` -/// -/// Ideas exist to have (bigger than 2 register) records also be passed by-reference, but this -/// is not currently implemented -pub fn argument_type_from_layout<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - layout: &Layout<'_>, -) -> BasicTypeEnum<'ctx> { - use Layout::*; - - match layout { - LambdaSet(lambda_set) => { - argument_type_from_layout(env, &lambda_set.runtime_representation()) - } - Union(union_layout) => argument_type_from_union_layout(env, union_layout), - Builtin(_) => { - let base = basic_type_from_layout(env, layout); - - if layout.is_passed_by_reference(env.target_info) { - base.ptr_type(AddressSpace::Generic).into() - } else { - base - } - } - other => basic_type_from_layout(env, other), - } -} - -/// Non-recursive tag unions are stored on the stack, but passed by-reference -pub fn argument_type_from_union_layout<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - union_layout: &UnionLayout<'_>, -) -> BasicTypeEnum<'ctx> { - let heap_type = basic_type_from_union_layout(env, union_layout); - - if let UnionLayout::NonRecursive(_) = union_layout { - heap_type.ptr_type(AddressSpace::Generic).into() - } else { - heap_type - } -} - -pub fn int_type_from_int_width<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - int_width: IntWidth, -) -> IntType<'ctx> { - use IntWidth::*; - - match int_width { - U128 | I128 => env.context.i128_type(), - U64 | I64 => env.context.i64_type(), - U32 | I32 => env.context.i32_type(), - U16 | I16 => env.context.i16_type(), - U8 | I8 => env.context.i8_type(), - } -} - -pub fn float_type_from_float_width<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - float_width: FloatWidth, -) -> FloatType<'ctx> { - use FloatWidth::*; - - match float_width { - F128 => todo!("F128 is not implemented"), - F64 => env.context.f64_type(), - F32 => env.context.f32_type(), - } -} - -pub fn block_of_memory_slices<'ctx>( - context: &'ctx Context, - layouts: &[&[Layout<'_>]], - target_info: TargetInfo, -) -> BasicTypeEnum<'ctx> { - let mut union_size = 0; - for tag in layouts { - let mut total = 0; - for layout in tag.iter() { - total += layout.stack_size(target_info); - } - - union_size = union_size.max(total); - } - - block_of_memory_help(context, union_size) -} - -pub fn block_of_memory<'ctx>( - context: &'ctx Context, - layout: &Layout<'_>, - target_info: TargetInfo, -) -> BasicTypeEnum<'ctx> { - // TODO make this dynamic - let mut union_size = layout.stack_size(target_info); - - if let Layout::Union(UnionLayout::NonRecursive { .. }) = layout { - union_size -= target_info.ptr_width() as u32; - } - - block_of_memory_help(context, union_size) -} - -fn block_of_memory_help(context: &Context, union_size: u32) -> BasicTypeEnum<'_> { - // The memory layout of Union is a bit tricky. - // We have tags with different memory layouts, that are part of the same type. - // For llvm, all tags must have the same memory layout. - // - // So, we convert all tags to a layout of bytes of some size. - // It turns out that encoding to i64 for as many elements as possible is - // a nice optimization, the remainder is encoded as bytes. - - let num_i64 = union_size / 8; - let num_i8 = union_size % 8; - - let i8_array_type = context.i8_type().array_type(num_i8).as_basic_type_enum(); - let i64_array_type = context.i64_type().array_type(num_i64).as_basic_type_enum(); - - if num_i64 == 0 { - // The object fits perfectly in some number of i8s - context.struct_type(&[i8_array_type], false).into() - } else if num_i8 == 0 { - // The object fits perfectly in some number of i64s - // (i.e. the size is a multiple of 8 bytes) - context.struct_type(&[i64_array_type], false).into() - } else { - // There are some trailing bytes at the end - let i8_array_type = context.i8_type().array_type(num_i8).as_basic_type_enum(); - - context - .struct_type(&[i64_array_type, i8_array_type], false) - .into() - } -} - -/// The int type that the C ABI turns our RocList/RocStr into -pub fn str_list_int(ctx: &Context, target_info: TargetInfo) -> IntType<'_> { - match target_info.ptr_width() { - roc_target::PtrWidth::Bytes4 => ctx.i64_type(), - roc_target::PtrWidth::Bytes8 => ctx.i128_type(), - } -} - -pub fn zig_dict_type<'a, 'ctx, 'env>(env: &Env<'a, 'ctx, 'env>) -> StructType<'ctx> { - env.module.get_struct_type("dict.RocDict").unwrap() -} - -pub fn zig_list_type<'a, 'ctx, 'env>(env: &Env<'a, 'ctx, 'env>) -> StructType<'ctx> { - env.module.get_struct_type("list.RocList").unwrap() -} - -pub fn zig_str_type<'a, 'ctx, 'env>(env: &Env<'a, 'ctx, 'env>) -> StructType<'ctx> { - env.module.get_struct_type("str.RocStr").unwrap() -} - -pub fn zig_has_tag_id_type<'a, 'ctx, 'env>(env: &Env<'a, 'ctx, 'env>) -> StructType<'ctx> { - let u8_ptr_t = env.context.i8_type().ptr_type(AddressSpace::Generic); - - env.context - .struct_type(&[env.context.bool_type().into(), u8_ptr_t.into()], false) -} - -pub fn zig_with_overflow_roc_dec<'a, 'ctx, 'env>(env: &Env<'a, 'ctx, 'env>) -> StructType<'ctx> { - env.module - .get_struct_type("utils.WithOverflow(dec.RocDec)") - .unwrap() -} diff --git a/compiler/gen_llvm/src/llvm/externs.rs b/compiler/gen_llvm/src/llvm/externs.rs deleted file mode 100644 index b3c428958b..0000000000 --- a/compiler/gen_llvm/src/llvm/externs.rs +++ /dev/null @@ -1,241 +0,0 @@ -use crate::llvm::bitcode::call_void_bitcode_fn; -use crate::llvm::build::{add_func, get_panic_msg_ptr, C_CALL_CONV}; -use crate::llvm::build::{CCReturn, Env, FunctionSpec}; -use inkwell::module::Linkage; -use inkwell::types::BasicType; -use inkwell::values::BasicValue; -use inkwell::AddressSpace; -use roc_builtins::bitcode; - -use super::build::{get_sjlj_buffer, LLVM_LONGJMP}; - -/// Define functions for roc_alloc, roc_realloc, and roc_dealloc -/// which use libc implementations (malloc, realloc, and free) -pub fn add_default_roc_externs(env: &Env<'_, '_, '_>) { - let ctx = env.context; - let module = env.module; - let builder = env.builder; - - let usize_type = env.ptr_int(); - let i8_ptr_type = ctx.i8_type().ptr_type(AddressSpace::Generic); - - // roc_alloc - { - // The type of this function (but not the implementation) should have - // already been defined by the builtins, which rely on it. - let fn_val = module.get_function("roc_alloc").unwrap(); - let mut params = fn_val.get_param_iter(); - let size_arg = params.next().unwrap(); - let _alignment_arg = params.next().unwrap(); - - debug_assert!(params.next().is_none()); - - // Add a basic block for the entry point - let entry = ctx.append_basic_block(fn_val, "entry"); - - builder.position_at_end(entry); - - // Call libc malloc() - let retval = builder - .build_array_malloc(ctx.i8_type(), size_arg.into_int_value(), "call_malloc") - .unwrap(); - - builder.build_return(Some(&retval)); - - if cfg!(debug_assertions) { - crate::llvm::build::verify_fn(fn_val); - } - } - - // roc_memcpy - { - // The type of this function (but not the implementation) should have - // already been defined by the builtins, which rely on it. - let fn_val = module.get_function("roc_memcpy").unwrap(); - let mut params = fn_val.get_param_iter(); - let dest_arg = params.next().unwrap(); - let dest_alignment = 1; - let src_arg = params.next().unwrap(); - let src_alignment = 1; - let bytes_arg = params.next().unwrap(); - - debug_assert!(params.next().is_none()); - - // Add a basic block for the entry point - let entry = ctx.append_basic_block(fn_val, "entry"); - - builder.position_at_end(entry); - - // Call libc memcpy() - let _retval = builder - .build_memcpy( - dest_arg.into_pointer_value(), - dest_alignment, - src_arg.into_pointer_value(), - src_alignment, - bytes_arg.into_int_value(), - ) - .unwrap(); - - builder.build_return(None); - - if cfg!(debug_assertions) { - crate::llvm::build::verify_fn(fn_val); - } - } - - // roc_realloc - { - let libc_realloc_val = { - let fn_spec = FunctionSpec::cconv( - env, - CCReturn::Return, - Some(i8_ptr_type.as_basic_type_enum()), - &[ - // ptr: *void - i8_ptr_type.into(), - // size: usize - usize_type.into(), - ], - ); - let fn_val = add_func(env.context, module, "realloc", fn_spec, Linkage::External); - - let mut params = fn_val.get_param_iter(); - let ptr_arg = params.next().unwrap(); - let size_arg = params.next().unwrap(); - - debug_assert!(params.next().is_none()); - - ptr_arg.set_name("ptr"); - size_arg.set_name("size"); - - if cfg!(debug_assertions) { - crate::llvm::build::verify_fn(fn_val); - } - - fn_val - }; - - // The type of this function (but not the implementation) should have - // already been defined by the builtins, which rely on it. - let fn_val = module.get_function("roc_realloc").unwrap(); - let mut params = fn_val.get_param_iter(); - let ptr_arg = params.next().unwrap(); - let new_size_arg = params.next().unwrap(); - let _old_size_arg = params.next().unwrap(); - let _alignment_arg = params.next().unwrap(); - - debug_assert!(params.next().is_none()); - - // Add a basic block for the entry point - let entry = ctx.append_basic_block(fn_val, "entry"); - - builder.position_at_end(entry); - - // Call libc realloc() - let call = builder.build_call( - libc_realloc_val, - &[ptr_arg.into(), new_size_arg.into()], - "call_libc_realloc", - ); - - call.set_call_convention(C_CALL_CONV); - - let retval = call.try_as_basic_value().left().unwrap(); - - builder.build_return(Some(&retval)); - - if cfg!(debug_assertions) { - crate::llvm::build::verify_fn(fn_val); - } - } - - // roc_dealloc - { - // The type of this function (but not the implementation) should have - // already been defined by the builtins, which rely on it. - let fn_val = module.get_function("roc_dealloc").unwrap(); - let mut params = fn_val.get_param_iter(); - let ptr_arg = params.next().unwrap(); - let _alignment_arg = params.next().unwrap(); - - debug_assert!(params.next().is_none()); - - // Add a basic block for the entry point - let entry = ctx.append_basic_block(fn_val, "entry"); - - builder.position_at_end(entry); - - // Call libc free() - builder.build_free(ptr_arg.into_pointer_value()); - - builder.build_return(None); - - if cfg!(debug_assertions) { - crate::llvm::build::verify_fn(fn_val); - } - } - - if env.is_gen_test { - add_sjlj_roc_panic(env) - } -} - -pub fn add_sjlj_roc_panic(env: &Env<'_, '_, '_>) { - let ctx = env.context; - let module = env.module; - let builder = env.builder; - - // roc_panic - { - // The type of this function (but not the implementation) should have - // already been defined by the builtins, which rely on it. - let fn_val = module.get_function("roc_panic").unwrap(); - let mut params = fn_val.get_param_iter(); - let ptr_arg = params.next().unwrap(); - - // in debug mode, this is assumed to be NullTerminatedString - let _tag_id_arg = params.next().unwrap(); - - debug_assert!(params.next().is_none()); - - let subprogram = env.new_subprogram("roc_panic"); - fn_val.set_subprogram(subprogram); - - env.dibuilder.finalize(); - - // Add a basic block for the entry point - let entry = ctx.append_basic_block(fn_val, "entry"); - - builder.position_at_end(entry); - - // write our error message pointer - env.builder.build_store(get_panic_msg_ptr(env), ptr_arg); - - build_longjmp_call(env); - - builder.build_unreachable(); - - if cfg!(debug_assertions) { - crate::llvm::build::verify_fn(fn_val); - } - } -} - -pub fn build_longjmp_call(env: &Env) { - let jmp_buf = get_sjlj_buffer(env); - if cfg!(target_arch = "aarch64") { - // Call the Zig-linked longjmp: `void longjmp(i32*, i32)` - let tag = env.context.i32_type().const_int(1, false); - let _call = - call_void_bitcode_fn(env, &[jmp_buf.into(), tag.into()], bitcode::UTILS_LONGJMP); - } else { - // Call the LLVM-intrinsic longjmp: `void @llvm.eh.sjlj.longjmp(i8* %setjmp_buf)` - let jmp_buf_i8p = env.builder.build_bitcast( - jmp_buf, - env.context.i8_type().ptr_type(AddressSpace::Generic), - "jmp_buf i8*", - ); - let _call = env.build_intrinsic_call(LLVM_LONGJMP, &[jmp_buf_i8p]); - } -} diff --git a/compiler/gen_llvm/src/llvm/mod.rs b/compiler/gen_llvm/src/llvm/mod.rs deleted file mode 100644 index 72ae3851aa..0000000000 --- a/compiler/gen_llvm/src/llvm/mod.rs +++ /dev/null @@ -1,10 +0,0 @@ -pub mod bitcode; -pub mod build; -pub mod build_dict; -pub mod build_hash; -pub mod build_list; -pub mod build_str; -pub mod compare; -pub mod convert; -pub mod externs; -pub mod refcounting; diff --git a/compiler/gen_llvm/src/llvm/refcounting.rs b/compiler/gen_llvm/src/llvm/refcounting.rs deleted file mode 100644 index efc9d9bfa5..0000000000 --- a/compiler/gen_llvm/src/llvm/refcounting.rs +++ /dev/null @@ -1,1882 +0,0 @@ -use crate::debug_info_init; -use crate::llvm::bitcode::call_void_bitcode_fn; -use crate::llvm::build::{ - add_func, cast_basic_basic, get_tag_id, tag_pointer_clear_tag_id, use_roc_value, Env, - FAST_CALL_CONV, TAG_DATA_INDEX, TAG_ID_INDEX, -}; -use crate::llvm::build_list::{incrementing_elem_loop, list_len, load_list}; -use crate::llvm::convert::basic_type_from_layout; -use bumpalo::collections::Vec; -use inkwell::basic_block::BasicBlock; -use inkwell::module::Linkage; -use inkwell::types::{AnyTypeEnum, BasicMetadataTypeEnum, BasicType, BasicTypeEnum}; -use inkwell::values::{ - BasicValue, BasicValueEnum, FunctionValue, IntValue, PointerValue, StructValue, -}; -use inkwell::{AddressSpace, IntPredicate}; -use roc_module::symbol::Interns; -use roc_module::symbol::Symbol; -use roc_mono::layout::{Builtin, Layout, LayoutIds, UnionLayout}; - -use super::build::{load_roc_value, FunctionSpec}; -use super::convert::{argument_type_from_layout, argument_type_from_union_layout}; - -pub struct PointerToRefcount<'ctx> { - value: PointerValue<'ctx>, -} - -impl<'ctx> PointerToRefcount<'ctx> { - /// # Safety - /// - /// the invariant is that the given pointer really points to the refcount, - /// not the data, and only is the start of the allocated buffer if the - /// alignment works out that way. - pub unsafe fn from_ptr<'a, 'env>(env: &Env<'a, 'ctx, 'env>, ptr: PointerValue<'ctx>) -> Self { - // must make sure it's a pointer to usize - let refcount_type = env.ptr_int(); - - let value = env - .builder - .build_bitcast( - ptr, - refcount_type.ptr_type(AddressSpace::Generic), - "to_refcount_ptr", - ) - .into_pointer_value(); - - Self { value } - } - - pub fn from_ptr_to_data<'a, 'env>( - env: &Env<'a, 'ctx, 'env>, - data_ptr: PointerValue<'ctx>, - ) -> Self { - let builder = env.builder; - // pointer to usize - let refcount_type = env.ptr_int(); - let refcount_ptr_type = refcount_type.ptr_type(AddressSpace::Generic); - - let ptr_as_usize_ptr = builder - .build_bitcast(data_ptr, refcount_ptr_type, "as_usize_ptr") - .into_pointer_value(); - - // get a pointer to index -1 - let index_intvalue = refcount_type.const_int(-1_i64 as u64, false); - let refcount_ptr = unsafe { - builder.build_in_bounds_gep(ptr_as_usize_ptr, &[index_intvalue], "get_rc_ptr") - }; - - Self { - value: refcount_ptr, - } - } - - fn from_list_wrapper(env: &Env<'_, 'ctx, '_>, list_wrapper: StructValue<'ctx>) -> Self { - let data_ptr = env - .builder - .build_extract_value(list_wrapper, Builtin::WRAPPER_PTR, "read_list_ptr") - .unwrap() - .into_pointer_value(); - - Self::from_ptr_to_data(env, data_ptr) - } - - pub fn is_1<'a, 'env>(&self, env: &Env<'a, 'ctx, 'env>) -> IntValue<'ctx> { - let current = self.get_refcount(env); - let one = match env.target_info.ptr_width() { - roc_target::PtrWidth::Bytes4 => { - env.context.i32_type().const_int(i32::MIN as u64, false) - } - roc_target::PtrWidth::Bytes8 => { - env.context.i64_type().const_int(i64::MIN as u64, false) - } - }; - - env.builder - .build_int_compare(IntPredicate::EQ, current, one, "is_one") - } - - fn get_refcount<'a, 'env>(&self, env: &Env<'a, 'ctx, 'env>) -> IntValue<'ctx> { - env.builder - .build_load(self.value, "get_refcount") - .into_int_value() - } - - pub fn set_refcount<'a, 'env>(&self, env: &Env<'a, 'ctx, 'env>, refcount: IntValue<'ctx>) { - env.builder.build_store(self.value, refcount); - } - - fn modify<'a, 'env>( - &self, - mode: CallMode<'ctx>, - layout: &Layout<'a>, - env: &Env<'a, 'ctx, 'env>, - ) { - match mode { - CallMode::Inc(inc_amount) => self.increment(inc_amount, env), - CallMode::Dec => self.decrement(env, layout), - } - } - - fn increment<'a, 'env>(&self, amount: IntValue<'ctx>, env: &Env<'a, 'ctx, 'env>) { - incref_pointer(env, self.value, amount); - } - - pub fn decrement<'a, 'env>(&self, env: &Env<'a, 'ctx, 'env>, layout: &Layout<'a>) { - let alignment = layout - .allocation_alignment_bytes(env.target_info) - .max(env.target_info.ptr_width() as u32); - - let context = env.context; - let block = env.builder.get_insert_block().expect("to be in a function"); - let di_location = env.builder.get_current_debug_location().unwrap(); - - let fn_name = &format!("decrement_refcounted_ptr_{}", alignment); - - let function = match env.module.get_function(fn_name) { - Some(function_value) => function_value, - None => { - // inc and dec return void - let fn_type = context.void_type().fn_type( - &[env.ptr_int().ptr_type(AddressSpace::Generic).into()], - false, - ); - - let function_value = add_func( - env.context, - env.module, - fn_name, - FunctionSpec::known_fastcc(fn_type), - Linkage::Internal, - ); - - let subprogram = env.new_subprogram(fn_name); - function_value.set_subprogram(subprogram); - - Self::build_decrement_function_body(env, function_value, alignment); - - function_value - } - }; - - let refcount_ptr = self.value; - - env.builder.position_at_end(block); - env.builder - .set_current_debug_location(env.context, di_location); - - let call = env - .builder - .build_call(function, &[refcount_ptr.into()], fn_name); - - call.set_call_convention(FAST_CALL_CONV); - } - - fn build_decrement_function_body<'a, 'env>( - env: &Env<'a, 'ctx, 'env>, - parent: FunctionValue<'ctx>, - alignment: u32, - ) { - let builder = env.builder; - let ctx = env.context; - - let entry = ctx.append_basic_block(parent, "entry"); - builder.position_at_end(entry); - - debug_info_init!(env, parent); - - decref_pointer( - env, - parent.get_nth_param(0).unwrap().into_pointer_value(), - alignment, - ); - - builder.build_return(None); - } -} - -fn incref_pointer<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - pointer: PointerValue<'ctx>, - amount: IntValue<'ctx>, -) { - call_void_bitcode_fn( - env, - &[ - env.builder.build_bitcast( - pointer, - env.ptr_int().ptr_type(AddressSpace::Generic), - "to_isize_ptr", - ), - amount.into(), - ], - roc_builtins::bitcode::UTILS_INCREF, - ); -} - -fn decref_pointer<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - pointer: PointerValue<'ctx>, - alignment: u32, -) { - let alignment = env.context.i32_type().const_int(alignment as _, false); - call_void_bitcode_fn( - env, - &[ - env.builder.build_bitcast( - pointer, - env.ptr_int().ptr_type(AddressSpace::Generic), - "to_isize_ptr", - ), - alignment.into(), - ], - roc_builtins::bitcode::UTILS_DECREF, - ); -} - -/// Assumes a pointer to the refcount -pub fn decref_pointer_check_null<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - pointer: PointerValue<'ctx>, - alignment: u32, -) { - let alignment = env.context.i32_type().const_int(alignment as _, false); - call_void_bitcode_fn( - env, - &[ - env.builder.build_bitcast( - pointer, - env.context.i8_type().ptr_type(AddressSpace::Generic), - "to_i8_ptr", - ), - alignment.into(), - ], - roc_builtins::bitcode::UTILS_DECREF_CHECK_NULL, - ); -} - -fn modify_refcount_struct<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - layout_ids: &mut LayoutIds<'a>, - layouts: &'a [Layout<'a>], - mode: Mode, - when_recursive: &WhenRecursive<'a>, -) -> FunctionValue<'ctx> { - let block = env.builder.get_insert_block().expect("to be in a function"); - let di_location = env.builder.get_current_debug_location().unwrap(); - - let layout = Layout::struct_no_name_order(layouts); - - let (_, fn_name) = function_name_from_mode( - layout_ids, - &env.interns, - "increment_struct", - "decrement_struct", - &layout, - mode, - ); - - let function = match env.module.get_function(fn_name.as_str()) { - Some(function_value) => function_value, - None => { - let basic_type = basic_type_from_layout(env, &layout); - let function_value = build_header(env, basic_type, mode, &fn_name); - - modify_refcount_struct_help( - env, - layout_ids, - mode, - when_recursive, - layouts, - function_value, - ); - - function_value - } - }; - - env.builder.position_at_end(block); - env.builder - .set_current_debug_location(env.context, di_location); - - function -} - -#[allow(clippy::too_many_arguments)] -fn modify_refcount_struct_help<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - layout_ids: &mut LayoutIds<'a>, - mode: Mode, - when_recursive: &WhenRecursive<'a>, - layouts: &[Layout<'a>], - fn_val: FunctionValue<'ctx>, -) { - let builder = env.builder; - let ctx = env.context; - - // Add a basic block for the entry point - let entry = ctx.append_basic_block(fn_val, "entry"); - - builder.position_at_end(entry); - - debug_info_init!(env, fn_val); - - // Add args to scope - let arg_symbol = Symbol::ARG_1; - let arg_val = fn_val.get_param_iter().next().unwrap(); - - arg_val.set_name(arg_symbol.as_str(&env.interns)); - - let parent = fn_val; - - let wrapper_struct = arg_val.into_struct_value(); - - for (i, field_layout) in layouts.iter().enumerate() { - if field_layout.contains_refcounted() { - let raw_value = env - .builder - .build_extract_value(wrapper_struct, i as u32, "decrement_struct_field") - .unwrap(); - - let field_value = use_roc_value( - env, - *field_layout, - raw_value, - "load_struct_tag_field_for_decrement", - ); - - modify_refcount_layout_help( - env, - parent, - layout_ids, - mode.to_call_mode(fn_val), - when_recursive, - field_value, - field_layout, - ); - } - } - // this function returns void - builder.build_return(None); -} - -pub fn increment_refcount_layout<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - parent: FunctionValue<'ctx>, - layout_ids: &mut LayoutIds<'a>, - inc_amount: u64, - value: BasicValueEnum<'ctx>, - layout: &Layout<'a>, -) { - let amount = env.ptr_int().const_int(inc_amount, false); - increment_n_refcount_layout(env, parent, layout_ids, amount, value, layout); -} - -pub fn increment_n_refcount_layout<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - parent: FunctionValue<'ctx>, - layout_ids: &mut LayoutIds<'a>, - amount: IntValue<'ctx>, - value: BasicValueEnum<'ctx>, - layout: &Layout<'a>, -) { - modify_refcount_layout( - env, - parent, - layout_ids, - CallMode::Inc(amount), - value, - layout, - ); -} - -pub fn decrement_refcount_layout<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - parent: FunctionValue<'ctx>, - layout_ids: &mut LayoutIds<'a>, - value: BasicValueEnum<'ctx>, - layout: &Layout<'a>, -) { - modify_refcount_layout(env, parent, layout_ids, CallMode::Dec, value, layout); -} - -fn modify_refcount_builtin<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - layout_ids: &mut LayoutIds<'a>, - mode: Mode, - when_recursive: &WhenRecursive<'a>, - layout: &Layout<'a>, - builtin: &Builtin<'a>, -) -> Option> { - use Builtin::*; - - match builtin { - List(element_layout) => { - let function = modify_refcount_list( - env, - layout_ids, - mode, - when_recursive, - layout, - element_layout, - ); - - Some(function) - } - Set(element_layout) => { - let key_layout = element_layout; - let value_layout = &Layout::UNIT; - - let function = modify_refcount_dict( - env, - layout_ids, - mode, - when_recursive, - layout, - key_layout, - value_layout, - ); - - Some(function) - } - Dict(key_layout, value_layout) => { - let function = modify_refcount_dict( - env, - layout_ids, - mode, - when_recursive, - layout, - key_layout, - value_layout, - ); - - Some(function) - } - - Str => Some(modify_refcount_str(env, layout_ids, mode, layout)), - - _ => { - debug_assert!(!builtin.is_refcounted()); - None - } - } -} - -fn modify_refcount_layout<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - parent: FunctionValue<'ctx>, - layout_ids: &mut LayoutIds<'a>, - call_mode: CallMode<'ctx>, - value: BasicValueEnum<'ctx>, - layout: &Layout<'a>, -) { - modify_refcount_layout_help( - env, - parent, - layout_ids, - call_mode, - &WhenRecursive::Unreachable, - value, - layout, - ); -} - -#[derive(Clone, Debug, PartialEq)] -enum WhenRecursive<'a> { - Unreachable, - Loop(UnionLayout<'a>), -} - -fn modify_refcount_layout_help<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - parent: FunctionValue<'ctx>, - layout_ids: &mut LayoutIds<'a>, - call_mode: CallMode<'ctx>, - when_recursive: &WhenRecursive<'a>, - value: BasicValueEnum<'ctx>, - layout: &Layout<'a>, -) { - let mode = match call_mode { - CallMode::Inc(_) => Mode::Inc, - CallMode::Dec => Mode::Dec, - }; - - let function = match modify_refcount_layout_build_function( - env, - parent, - layout_ids, - mode, - when_recursive, - layout, - ) { - Some(f) => f, - None => return, - }; - - match layout { - Layout::RecursivePointer => match when_recursive { - WhenRecursive::Unreachable => { - unreachable!("recursion pointers should never be hashed directly") - } - WhenRecursive::Loop(union_layout) => { - let layout = Layout::Union(*union_layout); - - let bt = basic_type_from_layout(env, &layout); - - // cast the i64 pointer to a pointer to block of memory - let field_cast = env - .builder - .build_bitcast(value, bt, "i64_to_opaque") - .into_pointer_value(); - - call_help(env, function, call_mode, field_cast.into()); - } - }, - _ => { - call_help(env, function, call_mode, value); - } - } -} - -fn call_help<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - function: FunctionValue<'ctx>, - call_mode: CallMode<'ctx>, - value: BasicValueEnum<'ctx>, -) -> inkwell::values::CallSiteValue<'ctx> { - let call = match call_mode { - CallMode::Inc(inc_amount) => { - env.builder - .build_call(function, &[value.into(), inc_amount.into()], "increment") - } - CallMode::Dec => env - .builder - .build_call(function, &[value.into()], "decrement"), - }; - - call.set_call_convention(FAST_CALL_CONV); - - call -} - -fn modify_refcount_layout_build_function<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - parent: FunctionValue<'ctx>, - layout_ids: &mut LayoutIds<'a>, - mode: Mode, - when_recursive: &WhenRecursive<'a>, - layout: &Layout<'a>, -) -> Option> { - use Layout::*; - - match layout { - Builtin(builtin) => { - modify_refcount_builtin(env, layout_ids, mode, when_recursive, layout, builtin) - } - - Boxed(inner) => { - let function = modify_refcount_boxed(env, layout_ids, mode, inner); - - Some(function) - } - - Union(variant) => { - use UnionLayout::*; - - match variant { - NonRecursive(&[]) => { - // void type, nothing to refcount here - None - } - - NonRecursive(tags) => { - let function = - modify_refcount_union(env, layout_ids, mode, when_recursive, tags); - - Some(function) - } - - _ => { - let function = build_rec_union( - env, - layout_ids, - mode, - &WhenRecursive::Loop(*variant), - *variant, - ); - - Some(function) - } - } - } - - Struct { field_layouts, .. } => { - let function = - modify_refcount_struct(env, layout_ids, field_layouts, mode, when_recursive); - - Some(function) - } - - Layout::RecursivePointer => match when_recursive { - WhenRecursive::Unreachable => { - unreachable!("recursion pointers cannot be in/decremented directly") - } - WhenRecursive::Loop(union_layout) => { - let layout = Layout::Union(*union_layout); - - let function = modify_refcount_layout_build_function( - env, - parent, - layout_ids, - mode, - when_recursive, - &layout, - )?; - - Some(function) - } - }, - LambdaSet(lambda_set) => modify_refcount_layout_build_function( - env, - parent, - layout_ids, - mode, - when_recursive, - &lambda_set.runtime_representation(), - ), - } -} - -fn modify_refcount_list<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - layout_ids: &mut LayoutIds<'a>, - mode: Mode, - when_recursive: &WhenRecursive<'a>, - layout: &Layout<'a>, - element_layout: &Layout<'a>, -) -> FunctionValue<'ctx> { - let block = env.builder.get_insert_block().expect("to be in a function"); - let di_location = env.builder.get_current_debug_location().unwrap(); - - let (_, fn_name) = function_name_from_mode( - layout_ids, - &env.interns, - "increment_list", - "decrement_list", - layout, - mode, - ); - - let function = match env.module.get_function(fn_name.as_str()) { - Some(function_value) => function_value, - None => { - let basic_type = argument_type_from_layout(env, layout); - let function_value = build_header(env, basic_type, mode, &fn_name); - - modify_refcount_list_help( - env, - layout_ids, - mode, - when_recursive, - layout, - element_layout, - function_value, - ); - - function_value - } - }; - - env.builder.position_at_end(block); - env.builder - .set_current_debug_location(env.context, di_location); - - function -} - -fn mode_to_call_mode(function: FunctionValue<'_>, mode: Mode) -> CallMode<'_> { - match mode { - Mode::Dec => CallMode::Dec, - Mode::Inc => CallMode::Inc(function.get_nth_param(1).unwrap().into_int_value()), - } -} - -fn modify_refcount_list_help<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - layout_ids: &mut LayoutIds<'a>, - mode: Mode, - when_recursive: &WhenRecursive<'a>, - layout: &Layout<'a>, - element_layout: &Layout<'a>, - fn_val: FunctionValue<'ctx>, -) { - let builder = env.builder; - let ctx = env.context; - - // Add a basic block for the entry point - let entry = ctx.append_basic_block(fn_val, "entry"); - - builder.position_at_end(entry); - - debug_info_init!(env, fn_val); - - // Add args to scope - let arg_symbol = Symbol::ARG_1; - let arg_val = fn_val.get_param_iter().next().unwrap(); - - arg_val.set_name(arg_symbol.as_str(&env.interns)); - - let parent = fn_val; - let original_wrapper = arg_val.into_struct_value(); - - let len = list_len(builder, original_wrapper); - - let is_non_empty = builder.build_int_compare( - IntPredicate::UGT, - len, - env.ptr_int().const_zero(), - "len > 0", - ); - - // build blocks - let modification_block = ctx.append_basic_block(parent, "modification_block"); - let cont_block = ctx.append_basic_block(parent, "modify_rc_list_cont"); - - builder.build_conditional_branch(is_non_empty, modification_block, cont_block); - - builder.position_at_end(modification_block); - - if element_layout.contains_refcounted() { - let ptr_type = basic_type_from_layout(env, element_layout).ptr_type(AddressSpace::Generic); - - let (len, ptr) = load_list(env.builder, original_wrapper, ptr_type); - - let loop_fn = |_index, element| { - modify_refcount_layout_help( - env, - parent, - layout_ids, - mode.to_call_mode(fn_val), - when_recursive, - element, - element_layout, - ); - }; - - incrementing_elem_loop( - env, - parent, - *element_layout, - ptr, - len, - "modify_rc_index", - loop_fn, - ); - } - - let refcount_ptr = PointerToRefcount::from_list_wrapper(env, original_wrapper); - let call_mode = mode_to_call_mode(fn_val, mode); - refcount_ptr.modify(call_mode, layout, env); - - builder.build_unconditional_branch(cont_block); - - builder.position_at_end(cont_block); - - // this function returns void - builder.build_return(None); -} - -fn modify_refcount_str<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - layout_ids: &mut LayoutIds<'a>, - mode: Mode, - layout: &Layout<'a>, -) -> FunctionValue<'ctx> { - let block = env.builder.get_insert_block().expect("to be in a function"); - let di_location = env.builder.get_current_debug_location().unwrap(); - - let (_, fn_name) = function_name_from_mode( - layout_ids, - &env.interns, - "increment_str", - "decrement_str", - layout, - mode, - ); - - let function = match env.module.get_function(fn_name.as_str()) { - Some(function_value) => function_value, - None => { - let basic_type = argument_type_from_layout(env, layout); - let function_value = build_header(env, basic_type, mode, &fn_name); - - modify_refcount_str_help(env, mode, layout, function_value); - - function_value - } - }; - - env.builder.position_at_end(block); - env.builder - .set_current_debug_location(env.context, di_location); - - function -} - -fn modify_refcount_str_help<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - mode: Mode, - layout: &Layout<'a>, - fn_val: FunctionValue<'ctx>, -) { - let builder = env.builder; - let ctx = env.context; - - // Add a basic block for the entry point - let entry = ctx.append_basic_block(fn_val, "entry"); - - builder.position_at_end(entry); - - debug_info_init!(env, fn_val); - - // Add args to scope - let arg_symbol = Symbol::ARG_1; - let arg_val = fn_val.get_param_iter().next().unwrap(); - - arg_val.set_name(arg_symbol.as_str(&env.interns)); - - let parent = fn_val; - - let arg_val = if Layout::Builtin(Builtin::Str).is_passed_by_reference(env.target_info) { - env.builder - .build_load(arg_val.into_pointer_value(), "load_str_to_stack") - } else { - // it's already a struct, just do nothing - debug_assert!(arg_val.is_struct_value()); - arg_val - }; - let str_wrapper = arg_val.into_struct_value(); - - let capacity = builder - .build_extract_value(str_wrapper, Builtin::WRAPPER_CAPACITY, "read_str_capacity") - .unwrap() - .into_int_value(); - - // Small strings have 1 as the first bit of capacity, making them negative. - // Thus, to check for big and non empty, just needs a signed len > 0. - let is_big_and_non_empty = builder.build_int_compare( - IntPredicate::SGT, - capacity, - env.ptr_int().const_zero(), - "is_big_str", - ); - - // the block we'll always jump to when we're done - let cont_block = ctx.append_basic_block(parent, "modify_rc_str_cont"); - let modification_block = ctx.append_basic_block(parent, "modify_rc"); - - builder.build_conditional_branch(is_big_and_non_empty, modification_block, cont_block); - builder.position_at_end(modification_block); - - let refcount_ptr = PointerToRefcount::from_list_wrapper(env, str_wrapper); - let call_mode = mode_to_call_mode(fn_val, mode); - refcount_ptr.modify(call_mode, layout, env); - - builder.build_unconditional_branch(cont_block); - - builder.position_at_end(cont_block); - - // this function returns void - builder.build_return(None); -} - -fn modify_refcount_boxed<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - layout_ids: &mut LayoutIds<'a>, - mode: Mode, - inner_layout: &'a Layout<'a>, -) -> FunctionValue<'ctx> { - let block = env.builder.get_insert_block().expect("to be in a function"); - let di_location = env.builder.get_current_debug_location().unwrap(); - - let boxed_layout = env.arena.alloc(Layout::Boxed(inner_layout)); - - let (_, fn_name) = function_name_from_mode( - layout_ids, - &env.interns, - "increment_boxed", - "decrement_boxed", - boxed_layout, - mode, - ); - - let function = match env.module.get_function(fn_name.as_str()) { - Some(function_value) => function_value, - None => { - let basic_type = basic_type_from_layout(env, boxed_layout); - let function_value = build_header(env, basic_type, mode, &fn_name); - - modify_refcount_box_help(env, mode, inner_layout, function_value); - - function_value - } - }; - - env.builder.position_at_end(block); - env.builder - .set_current_debug_location(env.context, di_location); - - function -} - -fn modify_refcount_box_help<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - mode: Mode, - inner_layout: &Layout<'a>, - fn_val: FunctionValue<'ctx>, -) { - let builder = env.builder; - let ctx = env.context; - - // Add a basic block for the entry point - let entry = ctx.append_basic_block(fn_val, "entry"); - - builder.position_at_end(entry); - - debug_info_init!(env, fn_val); - - // Add args to scope - let arg_symbol = Symbol::ARG_1; - let arg_val = fn_val.get_param_iter().next().unwrap(); - arg_val.set_name(arg_symbol.as_str(&env.interns)); - - let boxed = arg_val.into_pointer_value(); - let refcount_ptr = PointerToRefcount::from_ptr_to_data(env, boxed); - let call_mode = mode_to_call_mode(fn_val, mode); - let boxed_layout = Layout::Boxed(inner_layout); - refcount_ptr.modify(call_mode, &boxed_layout, env); - - // this function returns void - builder.build_return(None); -} - -#[allow(clippy::too_many_arguments)] -fn modify_refcount_dict<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - layout_ids: &mut LayoutIds<'a>, - mode: Mode, - when_recursive: &WhenRecursive<'a>, - layout: &Layout<'a>, - key_layout: &Layout<'a>, - value_layout: &Layout<'a>, -) -> FunctionValue<'ctx> { - let block = env.builder.get_insert_block().expect("to be in a function"); - let di_location = env.builder.get_current_debug_location().unwrap(); - - let (_, fn_name) = function_name_from_mode( - layout_ids, - &env.interns, - "increment_dict", - "decrement_dict", - layout, - mode, - ); - - let function = match env.module.get_function(fn_name.as_str()) { - Some(function_value) => function_value, - None => { - let basic_type = basic_type_from_layout(env, layout); - let function_value = build_header(env, basic_type, mode, &fn_name); - - modify_refcount_dict_help( - env, - layout_ids, - mode, - when_recursive, - layout, - key_layout, - value_layout, - function_value, - ); - - function_value - } - }; - - env.builder.position_at_end(block); - env.builder - .set_current_debug_location(env.context, di_location); - - function -} - -#[allow(clippy::too_many_arguments)] -fn modify_refcount_dict_help<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - layout_ids: &mut LayoutIds<'a>, - mode: Mode, - when_recursive: &WhenRecursive<'a>, - layout: &Layout<'a>, - key_layout: &Layout<'a>, - value_layout: &Layout<'a>, - fn_val: FunctionValue<'ctx>, -) { - debug_assert_eq!( - when_recursive, - &WhenRecursive::Unreachable, - "TODO pipe when_recursive through the dict key/value inc/dec" - ); - - let builder = env.builder; - let ctx = env.context; - - // Add a basic block for the entry point - let entry = ctx.append_basic_block(fn_val, "entry"); - - builder.position_at_end(entry); - - debug_info_init!(env, fn_val); - - // Add args to scope - let arg_symbol = Symbol::ARG_1; - let arg_val = fn_val.get_param_iter().next().unwrap(); - - arg_val.set_name(arg_symbol.as_str(&env.interns)); - - let parent = fn_val; - - let wrapper_struct = arg_val.into_struct_value(); - - let len = builder - .build_extract_value(wrapper_struct, 1, "read_dict_len") - .unwrap() - .into_int_value(); - - // the block we'll always jump to when we're done - let cont_block = ctx.append_basic_block(parent, "modify_rc_dict_cont"); - let modification_block = ctx.append_basic_block(parent, "modify_rc"); - - let is_non_empty = builder.build_int_compare( - IntPredicate::SGT, - len, - env.ptr_int().const_zero(), - "is_non_empty", - ); - - builder.build_conditional_branch(is_non_empty, modification_block, cont_block); - builder.position_at_end(modification_block); - - if key_layout.contains_refcounted() || value_layout.contains_refcounted() { - crate::llvm::build_dict::dict_elements_rc( - env, - layout_ids, - wrapper_struct.into(), - key_layout, - value_layout, - mode, - ); - } - - let data_ptr = env - .builder - .build_extract_value(wrapper_struct, 0, "get_data_ptr") - .unwrap() - .into_pointer_value(); - - let refcount_ptr = PointerToRefcount::from_ptr_to_data(env, data_ptr); - let call_mode = mode_to_call_mode(fn_val, mode); - refcount_ptr.modify(call_mode, layout, env); - - builder.build_unconditional_branch(cont_block); - - builder.position_at_end(cont_block); - - // this function returns void - builder.build_return(None); -} - -/// Build an increment or decrement function for a specific layout -fn build_header<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - arg_type: BasicTypeEnum<'ctx>, - mode: Mode, - fn_name: &str, -) -> FunctionValue<'ctx> { - match mode { - Mode::Inc => build_header_help( - env, - fn_name, - env.context.void_type().into(), - &[arg_type, env.ptr_int().into()], - ), - Mode::Dec => build_header_help(env, fn_name, env.context.void_type().into(), &[arg_type]), - } -} - -/// Build an increment or decrement function for a specific layout -pub fn build_header_help<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - fn_name: &str, - return_type: AnyTypeEnum<'ctx>, - arguments: &[BasicTypeEnum<'ctx>], -) -> FunctionValue<'ctx> { - use inkwell::types::AnyTypeEnum::*; - - let it = arguments.iter().map(|x| BasicMetadataTypeEnum::from(*x)); - let vec = Vec::from_iter_in(it, env.arena); - let arguments = vec.as_slice(); - - let fn_type = match return_type { - ArrayType(t) => t.fn_type(arguments, false), - FloatType(t) => t.fn_type(arguments, false), - FunctionType(_) => unreachable!("functions cannot return functions"), - IntType(t) => t.fn_type(arguments, false), - PointerType(t) => t.fn_type(arguments, false), - StructType(t) => t.fn_type(arguments, false), - VectorType(t) => t.fn_type(arguments, false), - VoidType(t) => t.fn_type(arguments, false), - }; - - let fn_val = add_func( - env.context, - env.module, - fn_name, - FunctionSpec::known_fastcc(fn_type), - Linkage::Private, - ); - - let subprogram = env.new_subprogram(fn_name); - fn_val.set_subprogram(subprogram); - - env.dibuilder.finalize(); - - fn_val -} - -#[derive(Clone, Copy)] -pub enum Mode { - Inc, - Dec, -} - -impl Mode { - fn to_call_mode(self, function: FunctionValue<'_>) -> CallMode<'_> { - match self { - Mode::Inc => { - let amount = function.get_nth_param(1).unwrap().into_int_value(); - - CallMode::Inc(amount) - } - Mode::Dec => CallMode::Dec, - } - } -} - -#[derive(Clone, Copy)] -enum CallMode<'ctx> { - Inc(IntValue<'ctx>), - Dec, -} - -fn build_rec_union<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - layout_ids: &mut LayoutIds<'a>, - mode: Mode, - when_recursive: &WhenRecursive<'a>, - union_layout: UnionLayout<'a>, -) -> FunctionValue<'ctx> { - let layout = Layout::Union(union_layout); - - let (_, fn_name) = function_name_from_mode( - layout_ids, - &env.interns, - "increment_rec_union", - "decrement_rec_union", - &layout, - mode, - ); - - let function = match env.module.get_function(fn_name.as_str()) { - Some(function_value) => function_value, - None => { - let block = env.builder.get_insert_block().expect("to be in a function"); - let di_location = env.builder.get_current_debug_location().unwrap(); - - let basic_type = basic_type_from_layout(env, &layout); - let function_value = build_header(env, basic_type, mode, &fn_name); - - build_rec_union_help( - env, - layout_ids, - mode, - when_recursive, - union_layout, - function_value, - ); - - env.builder.position_at_end(block); - env.builder - .set_current_debug_location(env.context, di_location); - - function_value - } - }; - - function -} - -#[allow(clippy::too_many_arguments)] -fn build_rec_union_help<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - layout_ids: &mut LayoutIds<'a>, - mode: Mode, - when_recursive: &WhenRecursive<'a>, - union_layout: UnionLayout<'a>, - fn_val: FunctionValue<'ctx>, -) { - let tags = union_layout_tags(env.arena, &union_layout); - debug_assert!(!tags.is_empty()); - - let context = &env.context; - let builder = env.builder; - - // Add a basic block for the entry point - let entry = context.append_basic_block(fn_val, "entry"); - - builder.position_at_end(entry); - - debug_info_init!(env, fn_val); - - // Add args to scope - let arg_symbol = Symbol::ARG_1; - - let arg_val = fn_val.get_param_iter().next().unwrap(); - - arg_val.set_name(arg_symbol.as_str(&env.interns)); - - let parent = fn_val; - - debug_assert!(arg_val.is_pointer_value()); - let current_tag_id = get_tag_id(env, fn_val, &union_layout, arg_val); - let value_ptr = if union_layout.stores_tag_id_in_pointer(env.target_info) { - tag_pointer_clear_tag_id(env, arg_val.into_pointer_value()) - } else { - arg_val.into_pointer_value() - }; - - let should_recurse_block = env.context.append_basic_block(parent, "should_recurse"); - - let ctx = env.context; - if union_layout.is_nullable() { - let is_null = env.builder.build_is_null(value_ptr, "is_null"); - - let then_block = ctx.append_basic_block(parent, "then"); - - env.builder - .build_conditional_branch(is_null, then_block, should_recurse_block); - - { - env.builder.position_at_end(then_block); - env.builder.build_return(None); - } - } else { - env.builder.build_unconditional_branch(should_recurse_block); - } - - env.builder.position_at_end(should_recurse_block); - - // to increment/decrement the cons-cell itself - let refcount_ptr = PointerToRefcount::from_ptr_to_data(env, value_ptr); - let call_mode = mode_to_call_mode(fn_val, mode); - - let layout = Layout::Union(union_layout); - - match mode { - Mode::Inc => { - // inc is cheap; we never recurse - refcount_ptr.modify(call_mode, &layout, env); - env.builder.build_return(None); - } - - Mode::Dec => { - let do_recurse_block = env.context.append_basic_block(parent, "do_recurse"); - let no_recurse_block = env.context.append_basic_block(parent, "no_recurse"); - - builder.build_conditional_branch( - refcount_ptr.is_1(env), - do_recurse_block, - no_recurse_block, - ); - - { - env.builder.position_at_end(no_recurse_block); - - refcount_ptr.modify(call_mode, &layout, env); - env.builder.build_return(None); - } - - { - env.builder.position_at_end(do_recurse_block); - - build_rec_union_recursive_decrement( - env, - layout_ids, - when_recursive, - parent, - fn_val, - union_layout, - tags, - value_ptr, - current_tag_id, - refcount_ptr, - do_recurse_block, - DecOrReuse::Dec, - ) - } - } - } -} - -enum DecOrReuse { - Dec, - Reuse, -} - -fn fields_need_no_refcounting(field_layouts: &[Layout]) -> bool { - !field_layouts - .iter() - .any(|x| x.is_refcounted() || x.contains_refcounted()) -} - -#[allow(clippy::too_many_arguments)] -fn build_rec_union_recursive_decrement<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - layout_ids: &mut LayoutIds<'a>, - when_recursive: &WhenRecursive<'a>, - parent: FunctionValue<'ctx>, - decrement_fn: FunctionValue<'ctx>, - union_layout: UnionLayout<'a>, - tags: &[&[Layout<'a>]], - value_ptr: PointerValue<'ctx>, - current_tag_id: IntValue<'ctx>, - refcount_ptr: PointerToRefcount<'ctx>, - match_block: BasicBlock<'ctx>, - decrement_or_reuse: DecOrReuse, -) { - let mode = Mode::Dec; - let call_mode = mode_to_call_mode(decrement_fn, mode); - let builder = env.builder; - - // next, make a jump table for all possible values of the tag_id - let mut cases = Vec::with_capacity_in(tags.len(), env.arena); - - let tag_id_int_type = - basic_type_from_layout(env, &union_layout.tag_id_layout()).into_int_type(); - - for (tag_id, field_layouts) in tags.iter().enumerate() { - // if none of the fields are or contain anything refcounted, just move on - if fields_need_no_refcounting(field_layouts) { - continue; - } - - let block = env.context.append_basic_block(parent, "tag_id_decrement"); - - env.builder.position_at_end(block); - - let wrapper_type = - basic_type_from_layout(env, &Layout::struct_no_name_order(field_layouts)); - - // cast the opaque pointer to a pointer of the correct shape - let struct_ptr = env - .builder - .build_bitcast( - value_ptr, - wrapper_type.ptr_type(AddressSpace::Generic), - "opaque_to_correct_recursive_decrement", - ) - .into_pointer_value(); - - // defer actually performing the refcount modifications until after the current cell has - // been decremented, see below - let mut deferred_rec = Vec::new_in(env.arena); - let mut deferred_nonrec = Vec::new_in(env.arena); - - for (i, field_layout) in field_layouts.iter().enumerate() { - if let Layout::RecursivePointer = field_layout { - // this field has type `*i64`, but is really a pointer to the data we want - let elem_pointer = env - .builder - .build_struct_gep(struct_ptr, i as u32, "gep_recursive_pointer") - .unwrap(); - - let ptr_as_i64_ptr = env - .builder - .build_load(elem_pointer, "load_recursive_pointer"); - - debug_assert!(ptr_as_i64_ptr.is_pointer_value()); - - // therefore we must cast it to our desired type - let union_type = basic_type_from_layout(env, &Layout::Union(union_layout)); - let recursive_field_ptr = cast_basic_basic(env.builder, ptr_as_i64_ptr, union_type); - - deferred_rec.push(recursive_field_ptr); - } else if field_layout.contains_refcounted() { - let elem_pointer = env - .builder - .build_struct_gep(struct_ptr, i as u32, "gep_recursive_pointer") - .unwrap(); - - let field = - load_roc_value(env, *field_layout, elem_pointer, "decrement_struct_field"); - - deferred_nonrec.push((field, field_layout)); - } - } - - // OPTIMIZATION - // - // We really would like `inc/dec` to be tail-recursive; it gives roughly a 2X speedup on linked - // lists. To achieve it, we must first load all fields that we want to inc/dec (done above) - // and store them on the stack, then modify (and potentially free) the current cell, then - // actually inc/dec the fields. - - match decrement_or_reuse { - DecOrReuse::Reuse => {} - DecOrReuse::Dec => { - refcount_ptr.modify(call_mode, &Layout::Union(union_layout), env); - } - } - - for (field, field_layout) in deferred_nonrec { - modify_refcount_layout_help( - env, - parent, - layout_ids, - mode.to_call_mode(decrement_fn), - when_recursive, - field, - field_layout, - ); - } - - for ptr in deferred_rec { - // recursively decrement the field - let call = call_help(env, decrement_fn, mode.to_call_mode(decrement_fn), ptr); - call.set_tail_call(true); - } - - // this function returns void - builder.build_return(None); - - cases.push((tag_id_int_type.const_int(tag_id as u64, false), block)); - } - - env.builder.position_at_end(match_block); - - cases.reverse(); - - if matches!( - union_layout, - UnionLayout::NullableUnwrapped { .. } | UnionLayout::NonNullableUnwrapped { .. } - ) { - debug_assert_eq!(cases.len(), 1); - - // in this case, don't switch, because the `else` branch below would try to read the (nonexistent) tag id - let (_, only_branch) = cases.pop().unwrap(); - env.builder.build_unconditional_branch(only_branch); - } else { - let default_block = env.context.append_basic_block(parent, "switch_default"); - - // switch on it - env.builder - .build_switch(current_tag_id, default_block, &cases); - - { - env.builder.position_at_end(default_block); - - // increment/decrement the cons-cell itself - if let DecOrReuse::Dec = decrement_or_reuse { - refcount_ptr.modify(call_mode, &Layout::Union(union_layout), env); - } - } - - // this function returns void - builder.build_return(None); - } -} - -fn union_layout_tags<'a>( - arena: &'a bumpalo::Bump, - union_layout: &UnionLayout<'a>, -) -> &'a [&'a [Layout<'a>]] { - use UnionLayout::*; - - match union_layout { - NullableWrapped { - other_tags: tags, .. - } => *tags, - NullableUnwrapped { other_fields, .. } => arena.alloc([*other_fields]), - NonNullableUnwrapped(fields) => arena.alloc([*fields]), - Recursive(tags) => tags, - NonRecursive(tags) => tags, - } -} - -pub fn build_reset<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - layout_ids: &mut LayoutIds<'a>, - union_layout: UnionLayout<'a>, -) -> FunctionValue<'ctx> { - let mode = Mode::Dec; - - let layout_id = layout_ids.get(Symbol::DEC, &Layout::Union(union_layout)); - let fn_name = layout_id.to_symbol_string(Symbol::DEC, &env.interns); - let fn_name = format!("{}_reset", fn_name); - - let when_recursive = WhenRecursive::Loop(union_layout); - let dec_function = build_rec_union(env, layout_ids, Mode::Dec, &when_recursive, union_layout); - - let function = match env.module.get_function(fn_name.as_str()) { - Some(function_value) => function_value, - None => { - let block = env.builder.get_insert_block().expect("to be in a function"); - let di_location = env.builder.get_current_debug_location().unwrap(); - - let basic_type = basic_type_from_layout(env, &Layout::Union(union_layout)); - let function_value = build_header(env, basic_type, mode, &fn_name); - - build_reuse_rec_union_help( - env, - layout_ids, - &when_recursive, - union_layout, - function_value, - dec_function, - ); - - env.builder.position_at_end(block); - env.builder - .set_current_debug_location(env.context, di_location); - - function_value - } - }; - - function -} - -#[allow(clippy::too_many_arguments)] -fn build_reuse_rec_union_help<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - layout_ids: &mut LayoutIds<'a>, - when_recursive: &WhenRecursive<'a>, - union_layout: UnionLayout<'a>, - reset_function: FunctionValue<'ctx>, - dec_function: FunctionValue<'ctx>, -) { - let tags = union_layout_tags(env.arena, &union_layout); - - debug_assert!(!tags.is_empty()); - - let context = &env.context; - let builder = env.builder; - - // Add a basic block for the entry point - let entry = context.append_basic_block(reset_function, "entry"); - - builder.position_at_end(entry); - - debug_info_init!(env, reset_function); - - // Add args to scope - let arg_symbol = Symbol::ARG_1; - - let arg_val = reset_function.get_param_iter().next().unwrap(); - - arg_val.set_name(arg_symbol.as_str(&env.interns)); - - let parent = reset_function; - - debug_assert!(arg_val.is_pointer_value()); - let current_tag_id = get_tag_id(env, reset_function, &union_layout, arg_val); - let value_ptr = tag_pointer_clear_tag_id(env, arg_val.into_pointer_value()); - - // to increment/decrement the cons-cell itself - let refcount_ptr = PointerToRefcount::from_ptr_to_data(env, value_ptr); - let call_mode = CallMode::Dec; - - let should_recurse_block = env.context.append_basic_block(parent, "should_recurse"); - - let ctx = env.context; - if union_layout.is_nullable() { - let is_null = env.builder.build_is_null(value_ptr, "is_null"); - - let then_block = ctx.append_basic_block(parent, "then"); - - env.builder - .build_conditional_branch(is_null, then_block, should_recurse_block); - - { - env.builder.position_at_end(then_block); - env.builder.build_return(None); - } - } else { - env.builder.build_unconditional_branch(should_recurse_block); - } - - env.builder.position_at_end(should_recurse_block); - - let layout = Layout::Union(union_layout); - - let do_recurse_block = env.context.append_basic_block(parent, "do_recurse"); - let no_recurse_block = env.context.append_basic_block(parent, "no_recurse"); - - builder.build_conditional_branch(refcount_ptr.is_1(env), do_recurse_block, no_recurse_block); - - { - env.builder.position_at_end(no_recurse_block); - - refcount_ptr.modify(call_mode, &layout, env); - env.builder.build_return(None); - } - - { - env.builder.position_at_end(do_recurse_block); - - build_rec_union_recursive_decrement( - env, - layout_ids, - when_recursive, - parent, - dec_function, - union_layout, - tags, - value_ptr, - current_tag_id, - refcount_ptr, - do_recurse_block, - DecOrReuse::Reuse, - ) - } -} - -fn function_name_from_mode<'a>( - layout_ids: &mut LayoutIds<'a>, - interns: &Interns, - if_inc: &'static str, - if_dec: &'static str, - layout: &Layout<'a>, - mode: Mode, -) -> (&'static str, String) { - // NOTE this is not a typo, we always determine the layout ID - // using the DEC symbol. Anything that is incrementing must also be - // decremented, so `dec` is used on more layouts. That can cause the - // layout ids of the inc and dec versions to be different, which is - // rather confusing, so now `inc_x` always corresponds to `dec_x` - let layout_id = layout_ids.get(Symbol::DEC, layout); - match mode { - Mode::Inc => (if_inc, layout_id.to_symbol_string(Symbol::INC, interns)), - Mode::Dec => (if_dec, layout_id.to_symbol_string(Symbol::DEC, interns)), - } -} - -fn modify_refcount_union<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - layout_ids: &mut LayoutIds<'a>, - mode: Mode, - when_recursive: &WhenRecursive<'a>, - fields: &'a [&'a [Layout<'a>]], -) -> FunctionValue<'ctx> { - let union_layout = UnionLayout::NonRecursive(fields); - let layout = Layout::Union(union_layout); - - let block = env.builder.get_insert_block().expect("to be in a function"); - let di_location = env.builder.get_current_debug_location().unwrap(); - - let (_, fn_name) = function_name_from_mode( - layout_ids, - &env.interns, - "increment_union", - "decrement_union", - &layout, - mode, - ); - - let function = match env.module.get_function(fn_name.as_str()) { - Some(function_value) => function_value, - None => { - let basic_type = argument_type_from_union_layout(env, &union_layout); - let function_value = build_header(env, basic_type, mode, &fn_name); - - modify_refcount_union_help( - env, - layout_ids, - mode, - when_recursive, - fields, - function_value, - ); - - function_value - } - }; - - env.builder.position_at_end(block); - env.builder - .set_current_debug_location(env.context, di_location); - - function -} - -fn modify_refcount_union_help<'a, 'ctx, 'env>( - env: &Env<'a, 'ctx, 'env>, - layout_ids: &mut LayoutIds<'a>, - mode: Mode, - when_recursive: &WhenRecursive<'a>, - tags: &[&[Layout<'a>]], - fn_val: FunctionValue<'ctx>, -) { - debug_assert!(!tags.is_empty()); - - let context = &env.context; - let builder = env.builder; - - // Add a basic block for the entry point - let entry = context.append_basic_block(fn_val, "entry"); - - builder.position_at_end(entry); - - debug_info_init!(env, fn_val); - - // Add args to scope - let arg_symbol = Symbol::ARG_1; - let arg_ptr = fn_val.get_param_iter().next().unwrap().into_pointer_value(); - - arg_ptr.set_name(arg_symbol.as_str(&env.interns)); - - let parent = fn_val; - - let before_block = env.builder.get_insert_block().expect("to be in a function"); - - // read the tag_id - let tag_id_ptr = env - .builder - .build_struct_gep(arg_ptr, TAG_ID_INDEX, "tag_id_ptr") - .unwrap(); - - let tag_id = env - .builder - .build_load(tag_id_ptr, "load_tag_id") - .into_int_value(); - - let tag_id_u8 = - env.builder - .build_int_cast_sign_flag(tag_id, env.context.i8_type(), false, "tag_id_u8"); - - // next, make a jump table for all possible values of the tag_id - let mut cases = Vec::with_capacity_in(tags.len(), env.arena); - - let merge_block = env - .context - .append_basic_block(parent, "modify_rc_union_merge"); - - for (tag_id, field_layouts) in tags.iter().enumerate() { - // if none of the fields are or contain anything refcounted, just move on - if !field_layouts - .iter() - .any(|x| x.is_refcounted() || x.contains_refcounted()) - { - continue; - } - - let block = env.context.append_basic_block(parent, "tag_id_modify"); - env.builder.position_at_end(block); - - let wrapper_type = - basic_type_from_layout(env, &Layout::struct_no_name_order(field_layouts)); - - debug_assert!(wrapper_type.is_struct_type()); - let opaque_tag_data_ptr = env - .builder - .build_struct_gep(arg_ptr, TAG_DATA_INDEX, "field_ptr") - .unwrap(); - - let cast_tag_data_pointer = env.builder.build_pointer_cast( - opaque_tag_data_ptr, - wrapper_type.ptr_type(AddressSpace::Generic), - "cast_to_concrete_tag", - ); - - for (i, field_layout) in field_layouts.iter().enumerate() { - if let Layout::RecursivePointer = field_layout { - let recursive_union_layout = match when_recursive { - WhenRecursive::Unreachable => { - panic!("non-recursive tag unions cannot contain naked recursion pointers!"); - } - WhenRecursive::Loop(recursive_union_layout) => recursive_union_layout, - }; - - // This field is a pointer to the recursive pointer. - let field_ptr = env - .builder - .build_struct_gep(cast_tag_data_pointer, i as u32, "modify_tag_field") - .unwrap(); - - // This is the actual pointer to the recursive data. - let field_value = env.builder.build_load(field_ptr, "load_recursive_pointer"); - - debug_assert!(field_value.is_pointer_value()); - - // therefore we must cast it to our desired type - let union_type = - basic_type_from_layout(env, &Layout::Union(*recursive_union_layout)); - let recursive_ptr_field_value = - cast_basic_basic(env.builder, field_value, union_type); - - modify_refcount_layout_help( - env, - parent, - layout_ids, - mode.to_call_mode(fn_val), - when_recursive, - recursive_ptr_field_value, - &Layout::RecursivePointer, - ) - } else if field_layout.contains_refcounted() { - let field_ptr = env - .builder - .build_struct_gep(cast_tag_data_pointer, i as u32, "modify_tag_field") - .unwrap(); - - let field_value = if field_layout.is_passed_by_reference(env.target_info) { - field_ptr.into() - } else { - env.builder.build_load(field_ptr, "field_value") - }; - - modify_refcount_layout_help( - env, - parent, - layout_ids, - mode.to_call_mode(fn_val), - when_recursive, - field_value, - field_layout, - ); - } - } - - env.builder.build_unconditional_branch(merge_block); - - cases.push((env.context.i8_type().const_int(tag_id as u64, false), block)); - } - - env.builder.position_at_end(before_block); - - env.builder.build_switch(tag_id_u8, merge_block, &cases); - - env.builder.position_at_end(merge_block); - - // this function returns void - builder.build_return(None); -} diff --git a/compiler/gen_llvm/src/run_roc.rs b/compiler/gen_llvm/src/run_roc.rs deleted file mode 100644 index 34f893f68e..0000000000 --- a/compiler/gen_llvm/src/run_roc.rs +++ /dev/null @@ -1,173 +0,0 @@ -use std::ffi::CString; -use std::mem::MaybeUninit; -use std::os::raw::c_char; - -/// This must have the same size as the repr() of RocCallResult! -pub const ROC_CALL_RESULT_DISCRIMINANT_SIZE: usize = std::mem::size_of::(); - -#[repr(C)] -pub struct RocCallResult { - tag: u64, - error_msg: *mut c_char, - value: MaybeUninit, -} - -impl From> for Result { - fn from(call_result: RocCallResult) -> Self { - match call_result.tag { - 0 => Ok(unsafe { call_result.value.assume_init() }), - _ => Err({ - let raw = unsafe { CString::from_raw(call_result.error_msg) }; - - let result = format!("{:?}", raw); - - // make sure rust does not try to free the Roc string - std::mem::forget(raw); - - result - }), - } - } -} - -#[macro_export] -macro_rules! run_jit_function { - ($lib: expr, $main_fn_name: expr, $ty:ty, $transform:expr) => {{ - let v: String = String::new(); - run_jit_function!($lib, $main_fn_name, $ty, $transform, v) - }}; - - ($lib: expr, $main_fn_name: expr, $ty:ty, $transform:expr, $errors:expr) => {{ - run_jit_function!($lib, $main_fn_name, $ty, $transform, $errors, &[]) - }}; - ($lib: expr, $main_fn_name: expr, $ty:ty, $transform:expr, $errors:expr, $expect_failures:expr) => {{ - use inkwell::context::Context; - use roc_builtins::bitcode; - use roc_gen_llvm::run_roc::RocCallResult; - use std::mem::MaybeUninit; - - #[derive(Debug, Copy, Clone)] - #[repr(C)] - struct Failure { - start_line: u32, - end_line: u32, - start_col: u16, - end_col: u16, - } - - unsafe { - let main: libloading::Symbol)> = $lib - .get($main_fn_name.as_bytes()) - .ok() - .ok_or(format!("Unable to JIT compile `{}`", $main_fn_name)) - .expect("errored"); - - #[repr(C)] - struct Failures { - failures: *const Failure, - count: usize, - } - - impl Drop for Failures { - fn drop(&mut self) { - use std::alloc::{dealloc, Layout}; - use std::mem; - - unsafe { - let layout = Layout::from_size_align_unchecked( - mem::size_of::(), - mem::align_of::(), - ); - - dealloc(self.failures as *mut u8, layout); - } - } - } - - let get_expect_failures: libloading::Symbol Failures> = $lib - .get(bitcode::UTILS_GET_EXPECT_FAILURES.as_bytes()) - .ok() - .ok_or(format!( - "Unable to JIT compile `{}`", - bitcode::UTILS_GET_EXPECT_FAILURES - )) - .expect("errored"); - let mut main_result = MaybeUninit::uninit(); - - main(main_result.as_mut_ptr()); - let failures = get_expect_failures(); - - if failures.count > 0 { - // TODO tell the user about the failures! - let failures = - unsafe { core::slice::from_raw_parts(failures.failures, failures.count) }; - - panic!("Failed with {} failures. Failures: ", failures.len()); - } - - match main_result.assume_init().into() { - Ok(success) => { - // only if there are no exceptions thrown, check for errors - assert!($errors.is_empty(), "Encountered errors:\n{}", $errors); - - $transform(success) - } - Err(error_msg) => panic!("Roc failed with message: {}", error_msg), - } - } - }}; -} - -/// In the repl, we don't know the type that is returned; it it's large enough to not fit in 2 -/// registers (i.e. size bigger than 16 bytes on 64-bit systems), then we use this macro. -/// It explicitly allocates a buffer that the roc main function can write its result into. -#[macro_export] -macro_rules! run_jit_function_dynamic_type { - ($lib: expr, $main_fn_name: expr, $bytes:expr, $transform:expr) => {{ - let v: String = String::new(); - run_jit_function_dynamic_type!($lib, $main_fn_name, $bytes, $transform, v) - }}; - - ($lib: expr, $main_fn_name: expr, $bytes:expr, $transform:expr, $errors:expr) => {{ - use inkwell::context::Context; - use roc_gen_llvm::run_roc::RocCallResult; - - unsafe { - let main: libloading::Symbol = $lib - .get($main_fn_name.as_bytes()) - .ok() - .ok_or(format!("Unable to JIT compile `{}`", $main_fn_name)) - .expect("errored"); - - let size = std::mem::size_of::>() + $bytes; - let layout = std::alloc::Layout::array::(size).unwrap(); - let result = std::alloc::alloc(layout); - main(result); - - let flag = *result; - - if flag == 0 { - $transform(result.add(std::mem::size_of::>()) as usize) - } else { - use std::ffi::CString; - use std::os::raw::c_char; - - // first field is a char pointer (to the error message) - // read value, and transmute to a pointer - let ptr_as_int = *(result as *const u64).offset(1); - let ptr = std::mem::transmute::(ptr_as_int); - - // make CString (null-terminated) - let raw = CString::from_raw(ptr); - - let result = format!("{:?}", raw); - - // make sure rust doesn't try to free the Roc constant string - std::mem::forget(raw); - - eprintln!("{}", result); - panic!("Roc hit an error"); - } - } - }}; -} diff --git a/compiler/gen_wasm/Cargo.toml b/compiler/gen_wasm/Cargo.toml deleted file mode 100644 index 5989fb6aa2..0000000000 --- a/compiler/gen_wasm/Cargo.toml +++ /dev/null @@ -1,16 +0,0 @@ -[package] -name = "roc_gen_wasm" -version = "0.1.0" -edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -bumpalo = { version = "3.8.0", features = ["collections"] } -roc_builtins = { path = "../builtins" } -roc_collections = { path = "../collections" } -roc_module = { path = "../module" } -roc_mono = { path = "../mono" } -roc_target = { path = "../roc_target" } -roc_std = { path = "../../roc_std", default-features = false } -roc_error_macros = { path = "../../error_macros" } diff --git a/compiler/gen_wasm/README.md b/compiler/gen_wasm/README.md deleted file mode 100644 index b99ff43fca..0000000000 --- a/compiler/gen_wasm/README.md +++ /dev/null @@ -1,253 +0,0 @@ -# Development backend for WebAssembly - -## Plan - -- Initial bringup - - [x] Get a wasm backend working for some of the number tests. - - [x] Use a separate `gen_wasm` directory for now, to avoid trying to do bringup and integration at the same time. -- Get the fundamentals working - - - [x] Come up with a way to do control flow - - [x] Flesh out the details of value representations between local variables and stack memory - - [x] Set up a way to write tests with any return value rather than just i64 and f64 - - [x] Implement stack memory - - [x] Push and pop stack frames - - [x] Deal with returning structs - - [x] Distinguish which variables go in locals, own stack frame, caller stack frame, etc. - - [x] Ensure early Return statements don't skip stack cleanup - - [x] Model the stack machine as a storage mechanism, to make generated code "less bad" - - [x] Switch vectors to `bumpalo::Vec` where possible - - [ ] Implement relocations - - Requires knowing the _byte_ offset of each call site. This is awkward as the backend builds a `Vec` rather than a `Vec`. It may be worth serialising each instruction as it is inserted. - -- Refactor for code sharing with CPU backends - - - [ ] Extract a trait from `WasmBackend` that looks as similar as possible to `Backend`, to prepare for code sharing - - [ ] Refactor to actually share code between `WasmBackend` and `Backend` if it seems feasible - -- Integration - - Move wasm files to `gen_dev/src/wasm` - - Share tests between wasm and x64, with some way of saying which tests work on which backends, and dispatching to different eval helpers based on that. - - Get `build_module` in object_builder.rs to dispatch to the wasm generator (adding some Wasm options to the `Triple` struct) - - Get `build_module` to write to a file, or maybe return `Vec`, instead of returning an Object structure - -## Structured control flow - -One of the security features of WebAssembly is that it does not allow unrestricted "jumps" to anywhere you like. It does not have an instruction for that. All of the [control instructions][control-inst] can only implement "structured" control flow, and have names like `if`, `loop`, `block` that you'd normally associate with high-level languages. There are branch (`br`) instructions that can jump to labelled blocks within the same function, but the blocks have to be nested in sensible ways. - -[control-inst]: https://webassembly.github.io/spec/core/syntax/instructions.html#control-instructions - -This way of representing control flow is similar to parts of the Roc AST like `When`, `If` and `LetRec`. But Mono IR converts this to jumps and join points, which are more of a Control Flow Graph than a tree. We need to map back from graph to a tree again in the Wasm backend. - -Our solution is to wrap all joinpoint/jump graphs in an outer `loop`, with nested `block`s inside it. - -### Possible future optimisations - -There are other algorithms available that may result in more optimised control flow. We are not focusing on that for our development backend, but here are some notes for future reference. - -The WebAssembly compiler toolkit `binaryen` has an [API for control-flow graphs][cfg-api]. We're not using `binaryen` right now. It's a C++ library, though it does have a (very thin and somewhat hard-to-use) [Rust wrapper][binaryen-rs]. Binaryen's control-flow graph API implements the "Relooper" algorithm developed by the Emscripten project and described in [this paper](https://github.com/emscripten-core/emscripten/blob/main/docs/paper.pdf). - -> By the way, apparently "binaryen" rhymes with "Targaryen", the family name from the "Game of Thrones" TV series - -There is also an improvement on Relooper called ["Stackifier"](https://medium.com/leaningtech/solving-the-structured-control-flow-problem-once-and-for-all-5123117b1ee2). It can reorder the joinpoints and jumps to make code more efficient. (It is also has things Roc wouldn't need but C++ does, like support for "irreducible" graphs that include `goto`). - -[cfg-api]: https://github.com/WebAssembly/binaryen/wiki/Compiling-to-WebAssembly-with-Binaryen#cfg-api -[binaryen-rs]: https://crates.io/crates/binaryen - -## Stack machine vs register machine - -Wasm's instruction set is based on a stack-machine VM. Whereas CPU instructions have named registers that they operate on, Wasm has no named registers at all. The instructions don't contain register names. Instructions can only operate on whatever data is at the top of the stack. - -For example the instruction `i64.add` takes two operands. It pops the top two arguments off the VM stack and pushes the result back. - -In the [spec][spec-instructions], every instruction has a type signature! This is not something you would see for CPU instructions. The type signature for i64.add is `[i64 i64] → [i64]` because it pushes two i64's and pops an i64. - -[spec-instructions]: https://webassembly.github.io/spec/core/appendix/index-instructions.html - -This means that WebAssembly has a concept of type checking. When you load a .wasm file as a chunk of bytes into a Wasm runtime (like a browser or [wasmer](https://wasmer.io/)), the runtime will first _validate_ those bytes. They have some fast way of checking whether the types being pushed and popped are consistent. So if you try to do the i64.add instruction when you have floats on the stack, it will fail validation. - -Note that the instruction makes no mention of any source or destination registers, because there is no such thing. It just pops two values and pushes one. (This architecture choice helps to keep WebAssembly programs quite compact. There are no extra bytes specifying source and destination registers.) - -Implications of the stack machine for Roc: - -- There is no such thing as register allocation, since there are no registers! There is no reason to maintain hashmaps of what registers are free or not. And there is no need to do a pass over the IR to find the "last seen" occurrence of a symbol in the IR. That means we don't need the `Backend` methods `scan_ast`, `scan_ast_call`, `set_last_seen`, `last_seen_map`, `free_map`, `free_symbols`, `free_symbol`, `set_free_map`. - -- There is no random access to the stack. All instructions operate on the data at the _top_ of the stack. There is no instruction that says "get the value at index 17 in the stack". If such an instruction did exist, it wouldn't be a stack machine. And there is no way to "free up some of the slots in the stack". You have to consume the stuff at the top, then the stuff further down. However Wasm has a concept of local variables, which do allow random access. See below. - -## Local variables - -WebAssembly functions can have any number of local variables. They are declared at the beginning of the function, along with their types (just like C). WebAssembly has 4 value types: `i32`, `i64`, `f32`, `f64`. - -In this backend, each symbol in the Mono IR gets one WebAssembly local. To illustrate, let's translate a simple Roc example to WebAssembly text format. -The WebAssembly code below is completely unoptimised and uses far more locals than necessary. But that does help to illustrate the concept of locals. - -``` -app "test" provides [main] to "./platform" - -main = - 1 + 2 + 4 -``` - - -### Direct translation of Mono IR - -The Mono IR contains two functions, `Num.add` and `main`, so we generate two corresponding WebAssembly functions. -Since it has a Symbol for every expression, the simplest thing is to create a local for each one. -The code ends up being quite bloated, with lots of `local.set` and `local.get` instructions. - -I've added comments on each line to show what is on the stack and in the locals at each point in the program. - -``` - (func (;0;) (param i64 i64) (result i64) ; declare function index 0 (Num.add) with two i64 parameters and an i64 result - local.get 0 ; load param 0 stack=[param0] - local.get 1 ; load param 1 stack=[param0, param1] - i64.add ; pop two values, add, and push result stack=[param0 + param1] - return) ; return the value at the top of the stack - - (func (;1;) (result i64) ; declare function index 1 (main) with no parameters and an i64 result - (local i64 i64 i64 i64) ; declare 4 local variables, all with type i64, one for each symbol in the Mono IR - i64.const 1 ; stack=[1] - local.set 0 ; stack=[] local0=1 - i64.const 2 ; stack=[2] local0=1 - local.set 1 ; stack=[] local0=1 local1=2 - local.get 0 ; stack=[1] local0=1 local1=2 - local.get 1 ; stack=[1,2] local0=1 local1=2 - call 0 ; stack=[3] local0=1 local1=2 - local.set 2 ; stack=[] local0=1 local1=2 local2=3 - i64.const 4 ; stack=[4] local0=1 local1=2 local2=3 - local.set 3 ; stack=[] local0=1 local1=2 local2=3 local3=4 - local.get 2 ; stack=[3] local0=1 local1=2 local2=3 local3=4 - local.get 3 ; stack=[3,4] local0=1 local1=2 local2=3 local3=4 - call 0 ; stack=[7] local0=1 local1=2 local2=3 local3=4 - return) ; return the value at the top of the stack -``` - -### Handwritten equivalent - -This code doesn't actually require any locals at all. -(It also doesn't need the `return` instructions, but that's less of a problem.) - -``` - (func (;0;) (param i64 i64) (result i64) - local.get 0 - local.get 1 - i64.add) - (func (;1;) (result i64) - i64.const 1 - i64.const 2 - call 0 - i64.const 4 - call 0) -``` - -### Reducing sets and gets - -For our example code, we don't need any locals because the WebAssembly virtual machine effectively _stores_ intermediate results in a stack. Since it's already storing those values, there is no need for us to create locals. If you compare the two versions, you'll see that the `local.set` and `local.get` instructions have simply been deleted and the other instructions are in the same order. - -But sometimes we really do need locals! We may need to use the same symbol twice, or values could end up on the stack in the wrong order and need to be swapped around by setting a local and getting it again. - -The hard part is knowing when we need a local, and when we don't. For that, the `WasmBackend` needs to have some understanding of the stack machine. - -To help with this, the `CodeBuilder` maintains a vector that represents the stack. For every instruction the backend generates, `CodeBuilder` simulates the right number of pushes and pops for that instruction, so that we always know the state of the VM stack at every point in the program. - -When the `WasmBackend` generates code for a `Let` statement, it can "label" the top of the stack with the relevant `Symbol`. Then at any later point in the program, when we need to retrieve a list of symbols in a certain order, we can check whether they already happen to be at the top of the stack in that order (as they were in our example above.) - -In practice it should be very common for values to appear on the VM stack in the right order, because in the Mono IR, statements occur in dependency order! We should only generate locals when the dependency graph is a little more complicated, and we actually need them. - -``` - ┌─────────────────┐ ┌─────────────┐ - │ │ │ │ - │ ├─────► Storage ├──────┐ - │ │ │ │ │ - │ │ └─────────────┘ │ - │ │ Manage state about │ - │ │ how/where symbol │ Delegate part of - │ WasmBackend │ values are stored │ state management - │ │ │ for values on - │ │ │ the VM stack - │ │ │ - │ │ Generate ┌────────▼──────┐ - │ │ instructions │ │ - │ ├─────────────────► CodeBuilder │ - │ │ │ │ - └─────────────────┘ └───────────────┘ -``` - -## Memory - -WebAssembly programs have a "linear memory" for storing data, which is a block of memory assigned to it by the host. You can assign a min and max size to the memory, and the WebAssembly program can request 64kB pages from the host, just like a "normal" program would request pages from the OS. Addresses start at zero and go up to whatever the current size is. Zero is a perfectly normal address like any other, and dereferencing it is not a segfault. But addresses beyond the current memory size are out of bounds and dereferencing them will cause a panic. - -The program has full read/write access to the memory and can divide it into whatever sections it wants. Most programs will want to do the traditional split of static memory, stack memory and heap memory. - -The WebAssembly module structure includes a data section that will be copied into the linear memory at a specified offset on initialisation, so you can use that for string literals etc. But the division of the rest of memory into "stack" and "heap" areas is not a first-class concept. It is up to the compiler to generate instructions to do whatever it wants with that memory. - -## Stack machine vs stack memory - -**There are two entirely different meanings of the word "stack" that are relevant to the WebAssembly backend.** It's unfortunate that the word "stack" is so overloaded. I guess it's just a useful data structure. The worst thing is that both of them tend to be referred to as just "the stack"! We need more precise terms. - -When we are talking about the instruction set, I'll use the term _machine stack_ or _VM stack_. This is the implicit data structure that WebAssembly instructions operate on. In the examples above, it's where `i64.add` gets its arguments and stores its result. I think of it as an abstraction over CPU registers, that WebAssembly uses in order to be portable and compact. - -When we are talking about how we store values in _memory_, I'll use the term _stack memory_ rather than just "the stack". It feels clunky but it's the best I can think of. - -Of course our program can use another area of memory as a heap as well. WebAssembly doesn't mind how you divide up your memory. It just gives you some memory and some instructions for loading and storing. - -## Calling conventions & stack memory - -In WebAssembly you call a function by pushing arguments to the stack and then issuing a `call` instruction, which specifies a function index. The VM knows how many values to pop off the stack by examining the _type_ of the function. In our example earlier, `Num.add` had the type `[i64 i64] → [i64]` so it expects to find two i64's on the stack and pushes one i64 back as the result. Remember, the runtime engine will validate the module before running it, and if your generated code is trying to call a function at a point in the program where the wrong value types are on the stack, it will fail validation. - -Function arguments are restricted to the four value types, `i32`, `i64`, `f32` and `f64`. If those are all we need, then there is _no need for any stack memory_, stack pointer, etc. We saw this in our example earlier. We just said `call 0`. We didn't need any instructions to create a stack frame with a return address, and there was no "jump" instruction. Essentially, WebAssembly has a first-class concept of function calls, so you don't build it up from lower-level primitives. You could think of this as an abstraction over calling conventions. - -That's all great for primitive values but what happens when we want to pass more complex data structures between functions? - -Well, remember, "stack memory" is not a special kind of memory in WebAssembly, and is separate from the VM stack. It's just an area of our memory where we implement a stack data structure. But there are some conventions that it makes sense to follow so that we can easily link to Wasm code generated from Zig or other languages. - -### Observations from compiled C code - -- `global 0` is used as the stack pointer, and its value is normally copied to a `local` as well (presumably because locals tend to be assigned to CPU registers) -- Stack memory grows downwards -- If a C function returns a struct, the compiled WebAssembly function has no return value, but instead has an extra _argument_. The argument is an `i32` pointer to space allocated in the caller's stack, that the called function can write to. -- There is no maximum number of arguments for a WebAssembly function, and arguments are not passed via _stack memory_. This makes sense because the _VM stack_ has no size limit. It's like having a CPU with an unlimited number of registers. -- Stack memory is only used for allocating local variables, not for passing arguments. And it's only used for values that cannot be stored in one of WebAssembly's primitive values (`i32`, `i64`, `f32`, `f64`). - -These observations are based on experiments compiling C to WebAssembly via the Emscripten toolchain (which is built on top of clang). It's also in line with what the WebAssembly project describes [here](https://github.com/WebAssembly/design/blob/main/Rationale.md#locals). - -## Modules vs Instances - -What's the difference between a Module and an Instance in WebAssembly? - -Well, if I compare it to running a program on Linux, it's like the difference between an ELF binary and the executable image in memory that you get when you _load_ that ELF file. The ELF file is essentially a _specification_ for how to create the executable image. In order to start executing the program, the OS has to actually allocate a stack and a heap, and load the text and data. If you run multiple copies of the same program, they will each have their own memory and their own execution state. (More detail [here](https://wiki.osdev.org/ELF#Loading_ELF_Binaries)). - -The Module is like the ELF file, and the Instance is like the executable image. - -The Module is a _specification_ for how to create an Instance of the program. The Module says how much memory the program needs, but the Instance actually _contains_ that memory. In order to run the Wasm program, the VM needs to create an instance, allocate some memory for it, and copy the data section into that memory. If you run many copies of the same Wasm program, you will have one Module but many Instances. Each instance will have its own separate area of memory, and its own execution state. - -## Modules, object files, and linking - -A WebAssembly module is equivalent to an executable file. It doesn't normally need relocations since at the WebAssembly layer, there is no Address Space Layout Randomisation. If it has relocations then it's an object file. - -The [official spec](https://webassembly.github.io/spec/core/binary/modules.html#sections) lists the sections that are part of the final module. It doesn't mention any sections for relocations or symbol names, but it does support "custom" sections. Conventions to use those for linking are documented in the WebAssembly `tool-conventions` repo [here](https://github.com/WebAssembly/tool-conventions/blob/main/Linking.md) and it mentions that LLVM is using those conventions. - -## Linking host-to-app calls - -We implement a few linking operations in the Wasm backend. The most important are host-to-app calls. - -In the host .wasm file, `roc__mainForHost_1_exposed` is defined as a Wasm Import, as if it were an external JavaScript function. But when we link the host and app, we need to make it an internal function instead. - -There are a few important facts to note about the Wasm binary format: -- Function calls refer to the callee by its function index in the file. -- If we move a function from one index to another, all of its call sites need to be updated. So we want to minimise this to make linking fast. -- If we _remove_ a function, then all functions above it will implicitly have their indices shifted down by 1! This is not good for speed. We should try to _swap_ rather than remove. -- JavaScript imports always get the lower indices. - -With that background, here are the linking steps for a single app function that gets called by the host: -- Remove `roc__mainForHost_1_exposed` from the imports, updating all call sites to the new index, which is somewhere in the app. -- Swap the _last_ JavaScript import into the slot where `roc__mainForHost_1_exposed` was, updating all of its call sites in the host. -- Insert an internally-defined dummy function at the index where the last JavaScript import used to be. - -The diagram below illustrates this process. - -> The diagram has a tiny number of functions just to make it easier to draw! Our mock host for integration tests has 48 imports and 648 defined functions. - -  - -![Diagram showing how host-to-app calls are linked.](./docs/host-to-app-calls.svg) diff --git a/compiler/gen_wasm/src/backend.rs b/compiler/gen_wasm/src/backend.rs deleted file mode 100644 index ad52783d0a..0000000000 --- a/compiler/gen_wasm/src/backend.rs +++ /dev/null @@ -1,1767 +0,0 @@ -use bumpalo::collections::{String, Vec}; - -use code_builder::Align; -use roc_builtins::bitcode::{FloatWidth, IntWidth}; -use roc_collections::all::MutMap; -use roc_error_macros::internal_error; -use roc_module::low_level::{LowLevel, LowLevelWrapperType}; -use roc_module::symbol::{Interns, Symbol}; -use roc_mono::code_gen_help::{CodeGenHelp, HelperOp, REFCOUNT_MAX}; -use roc_mono::ir::{ - BranchInfo, CallType, Expr, JoinPointId, ListLiteralElement, Literal, ModifyRc, Param, Proc, - ProcLayout, Stmt, -}; -use roc_mono::layout::{Builtin, Layout, LayoutIds, TagIdIntType, UnionLayout}; -use roc_std::RocDec; - -use crate::layout::{CallConv, ReturnMethod, WasmLayout}; -use crate::low_level::{call_higher_order_lowlevel, LowLevelCall}; -use crate::storage::{Storage, StoredValue, StoredValueKind}; -use crate::wasm_module::linking::{self, DataSymbol, WasmObjectSymbol}; -use crate::wasm_module::sections::{ - ConstExpr, DataMode, DataSegment, Export, Global, GlobalType, Import, ImportDesc, Limits, - MemorySection, -}; -use crate::wasm_module::{ - code_builder, CodeBuilder, ExportType, LocalId, Signature, SymInfo, ValueType, WasmModule, -}; -use crate::{ - copy_memory, round_up_to_alignment, CopyMemoryConfig, Env, DEBUG_LOG_SETTINGS, MEMORY_NAME, - PTR_SIZE, PTR_TYPE, TARGET_INFO, -}; - -#[derive(Clone, Copy, Debug)] -pub enum ProcSource { - Roc, - Helper, - /// Wrapper function for higher-order calls from Zig to Roc - HigherOrderWrapper(usize), -} - -#[derive(Debug)] -pub struct ProcLookupData<'a> { - pub name: Symbol, - pub layout: ProcLayout<'a>, - pub source: ProcSource, -} - -pub struct WasmBackend<'a> { - pub env: &'a Env<'a>, - interns: &'a mut Interns, - - // Module-level data - module: WasmModule<'a>, - layout_ids: LayoutIds<'a>, - pub fn_index_offset: u32, - called_preload_fns: Vec<'a, u32>, - pub proc_lookup: Vec<'a, ProcLookupData<'a>>, - host_lookup: Vec<'a, (&'a str, u32)>, - helper_proc_gen: CodeGenHelp<'a>, - - // Function-level data - pub code_builder: CodeBuilder<'a>, - pub storage: Storage<'a>, - - /// how many blocks deep are we (used for jumps) - block_depth: u32, - joinpoint_label_map: MutMap)>, -} - -impl<'a> WasmBackend<'a> { - #[allow(clippy::too_many_arguments)] - pub fn new( - env: &'a Env<'a>, - interns: &'a mut Interns, - layout_ids: LayoutIds<'a>, - proc_lookup: Vec<'a, ProcLookupData<'a>>, - host_to_app_map: Vec<'a, (&'a str, u32)>, - mut module: WasmModule<'a>, - fn_index_offset: u32, - helper_proc_gen: CodeGenHelp<'a>, - ) -> Self { - // TODO: get this from a CLI parameter with some default - const STACK_SIZE: u32 = 1024 * 1024; - Self::set_memory_layout(env, &mut module, STACK_SIZE); - - Self::export_globals(&mut module); - - // We don't want to import any Memory or Tables - module.import.imports.retain(|import| { - !matches!( - import.description, - ImportDesc::Mem { .. } | ImportDesc::Table { .. } - ) - }); - - module.link_host_to_app_calls(host_to_app_map); - - module.code.code_builders.reserve(proc_lookup.len()); - - let symbol_prefix = "roc_"; // The app only links to roc_builtins.*, roc_alloc, etc. - let host_lookup = module.linking.name_index_map(env.arena, symbol_prefix); - - WasmBackend { - env, - interns, - - // Module-level data - module, - - layout_ids, - fn_index_offset, - called_preload_fns: Vec::with_capacity_in(2, env.arena), - proc_lookup, - host_lookup, - helper_proc_gen, - - // Function-level data - block_depth: 0, - joinpoint_label_map: MutMap::default(), - code_builder: CodeBuilder::new(env.arena), - storage: Storage::new(env.arena), - } - } - - /// A Wasm module's memory is all in one contiguous block, unlike native executables. - /// The standard layout is: constant data, then stack, then heap. - /// Since they're all in one block, they can't grow independently. Only the highest one can grow. - /// Also, there's no "invalid region" below the stack, so stack overflow will overwrite constants! - /// TODO: Detect stack overflow in function prologue... at least in Roc code... - fn set_memory_layout(env: &'a Env<'a>, module: &mut WasmModule<'a>, stack_size: u32) { - let mut stack_heap_boundary = module.data.end_addr + stack_size; - stack_heap_boundary = round_up_to_alignment!(stack_heap_boundary, MemorySection::PAGE_SIZE); - - // Stack pointer - // This should be an imported global in the host - // In the final binary, it's an internally defined global - let sp_type = GlobalType { - value_type: ValueType::I32, - is_mutable: true, - }; - { - // Check that __stack_pointer is the only imported global - // If there were more, we'd have to relocate them, and we don't - let imported_globals = Vec::from_iter_in( - module - .import - .imports - .iter() - .filter(|import| matches!(import.description, ImportDesc::Global { .. })), - env.arena, - ); - if imported_globals.len() != 1 - || imported_globals[0] - != &(Import { - module: "env", - name: "__stack_pointer", - description: ImportDesc::Global { ty: sp_type }, - }) - { - panic!("I can't link this host file. I expected it to have one imported Global called env.__stack_pointer") - } - } - module - .import - .imports - .retain(|import| !matches!(import.description, ImportDesc::Global { .. })); - module.global.append(Global { - ty: sp_type, - init: ConstExpr::I32(stack_heap_boundary as i32), - }); - - // Set the initial size of the memory - module.memory = - MemorySection::new(env.arena, stack_heap_boundary + MemorySection::PAGE_SIZE); - - // Export the memory so that JS can interact with it - module.export.append(Export { - name: MEMORY_NAME, - ty: ExportType::Mem, - index: 0, - }); - - // Set the constant that malloc uses to know where the heap begins - module.relocate_internal_symbol("__heap_base", stack_heap_boundary); - } - - /// If the host has some `extern` global variables, we need to create them in the final binary - /// and make them visible to JavaScript by exporting them - fn export_globals(module: &mut WasmModule<'a>) { - for (sym_index, sym) in module.linking.symbol_table.iter().enumerate() { - match sym { - SymInfo::Data(DataSymbol::Imported { name, .. }) if *name != "__heap_base" => { - let global_value_addr = module.data.end_addr; - module.data.end_addr += PTR_SIZE; - - module.reloc_code.apply_relocs_u32( - &mut module.code.preloaded_bytes, - module.code.preloaded_reloc_offset, - sym_index as u32, - global_value_addr, - ); - - let global_index = module.global.count; - module.global.append(Global { - ty: GlobalType { - value_type: ValueType::I32, - is_mutable: false, - }, - init: ConstExpr::I32(global_value_addr as i32), - }); - - module.export.append(Export { - name, - ty: ExportType::Global, - index: global_index, - }); - } - _ => {} - } - } - } - - pub fn get_helpers(&mut self) -> Vec<'a, Proc<'a>> { - self.helper_proc_gen.take_procs() - } - - pub fn register_helper_proc( - &mut self, - symbol: Symbol, - layout: ProcLayout<'a>, - source: ProcSource, - ) -> u32 { - let proc_index = self.proc_lookup.len(); - let wasm_fn_index = self.fn_index_offset + proc_index as u32; - - let name = self - .layout_ids - .get_toplevel(symbol, &layout) - .to_symbol_string(symbol, self.interns); - let name = String::from_str_in(&name, self.env.arena).into_bump_str(); - - self.proc_lookup.push(ProcLookupData { - name: symbol, - layout, - source, - }); - - let linker_symbol = SymInfo::Function(WasmObjectSymbol::ExplicitlyNamed { - flags: 0, - index: wasm_fn_index, - name, - }); - self.module.linking.symbol_table.push(linker_symbol); - - wasm_fn_index - } - - pub fn finalize(mut self) -> (WasmModule<'a>, Vec<'a, u32>) { - self.maybe_call_host_main(); - let fn_table_size = 1 + self.module.element.max_table_index(); - self.module.table.function_table.limits = Limits::MinMax(fn_table_size, fn_table_size); - (self.module, self.called_preload_fns) - } - - /// If the host has a `main` function then we need to insert a `_start` to call it. - /// This is something linkers do, and this backend is also a linker! - fn maybe_call_host_main(&mut self) { - let main_symbol_index = if let Some(i) = self.module.linking.find_internal_symbol("main") { - i as usize - } else { - return; - }; - - let main_fn_index: u32 = match &self.module.linking.symbol_table[main_symbol_index] { - SymInfo::Function(WasmObjectSymbol::ExplicitlyNamed { flags, index, .. }) - if flags & linking::WASM_SYM_BINDING_LOCAL == 0 => - { - *index - } - _ => { - return; - } - }; - - self.module.add_function_signature(Signature { - param_types: bumpalo::vec![in self.env.arena], - ret_type: None, - }); - - self.module.export.append(Export { - name: "_start", - ty: ExportType::Func, - index: self.fn_index_offset + self.module.code.code_builders.len() as u32, - }); - - self.code_builder.i32_const(0); // argc=0 - self.code_builder.i32_const(0); // argv=NULL - self.code_builder.call(main_fn_index, 2, true); - self.code_builder.drop_(); - self.code_builder.build_fn_header_and_footer(&[], 0, None); - self.reset(); - - self.called_preload_fns.push(main_fn_index); - } - - /// Register the debug names of Symbols in a global lookup table - /// so that they have meaningful names when you print them. - /// Particularly useful after generating IR for refcount procedures - #[cfg(debug_assertions)] - pub fn register_symbol_debug_names(&self) { - let module_id = self.env.module_id; - let ident_ids = self.interns.all_ident_ids.get(&module_id).unwrap(); - self.env.module_id.register_debug_idents(ident_ids); - } - - #[cfg(not(debug_assertions))] - pub fn register_symbol_debug_names(&self) {} - - pub fn get_fn_table_index(&mut self, fn_index: u32) -> i32 { - self.module.element.get_fn_table_index(fn_index) - } - - /// Create an IR Symbol for an anonymous value (such as ListLiteral) - pub fn create_symbol(&mut self, debug_name: &str) -> Symbol { - let ident_ids = self - .interns - .all_ident_ids - .get_mut(&self.env.module_id) - .unwrap(); - - let ident_id = ident_ids.add_str(debug_name); - Symbol::new(self.env.module_id, ident_id) - } - - /// Reset function-level data - fn reset(&mut self) { - // Push the completed CodeBuilder into the module and swap it for a new empty one - let mut swap_code_builder = CodeBuilder::new(self.env.arena); - std::mem::swap(&mut swap_code_builder, &mut self.code_builder); - self.module.code.code_builders.push(swap_code_builder); - - self.storage.clear(); - self.joinpoint_label_map.clear(); - assert_eq!(self.block_depth, 0); - } - - /********************************************************** - - PROCEDURE - - ***********************************************************/ - - pub fn build_proc(&mut self, proc: &Proc<'a>) { - if DEBUG_LOG_SETTINGS.proc_start_end { - println!("\ngenerating procedure {:?}\n", proc.name); - } - - self.append_proc_debug_name(proc.name); - - self.start_proc(proc); - - self.stmt(&proc.body); - - self.finalize_proc(); - self.reset(); - - if DEBUG_LOG_SETTINGS.proc_start_end { - println!("\nfinished generating {:?}\n", proc.name); - } - } - - fn start_proc(&mut self, proc: &Proc<'a>) { - use ReturnMethod::*; - let ret_layout = WasmLayout::new(&proc.ret_layout); - - let ret_type = match ret_layout.return_method(CallConv::C) { - Primitive(ty, _) => Some(ty), - NoReturnValue => None, - WriteToPointerArg => { - self.storage.arg_types.push(PTR_TYPE); - None - } - ZigPackedStruct => { - internal_error!("C calling convention does not return Zig packed structs") - } - }; - - // Create a block so we can exit the function without skipping stack frame "pop" code. - // We never use the `return` instruction. Instead, we break from this block. - self.start_block(); - - for (layout, symbol) in proc.args { - self.storage - .allocate(*layout, *symbol, StoredValueKind::Parameter); - } - - if let Some(ty) = ret_type { - let ret_var = self.storage.create_anonymous_local(ty); - self.storage.return_var = Some(ret_var); - } - - self.module.add_function_signature(Signature { - param_types: self.storage.arg_types.clone(), - ret_type, - }); - } - - fn finalize_proc(&mut self) { - // end the block from start_proc, to ensure all paths pop stack memory (if any) - self.end_block(); - - if let Some(ret_var) = self.storage.return_var { - self.code_builder.get_local(ret_var); - } - - // Write local declarations and stack frame push/pop code - self.code_builder.build_fn_header_and_footer( - &self.storage.local_types, - self.storage.stack_frame_size, - self.storage.stack_frame_pointer, - ); - - if DEBUG_LOG_SETTINGS.storage_map { - println!("\nStorage:"); - for (sym, storage) in self.storage.symbol_storage_map.iter() { - println!("{:?} => {:?}", sym, storage); - } - } - } - - fn append_proc_debug_name(&mut self, sym: Symbol) { - let proc_index = self - .proc_lookup - .iter() - .position(|ProcLookupData { name, .. }| *name == sym) - .unwrap(); - let wasm_fn_index = self.fn_index_offset + proc_index as u32; - - let name = String::from_str_in(sym.as_str(self.interns), self.env.arena).into_bump_str(); - self.module.names.append_function(wasm_fn_index, name); - } - - /// Build a wrapper around a Roc procedure so that it can be called from our higher-order Zig builtins. - /// - /// The generic Zig code passes *pointers* to all of the argument values (e.g. on the heap in a List). - /// Numbers up to 64 bits are passed by value, so we need to load them from the provided pointer. - /// Everything else is passed by reference, so we can just pass the pointer through. - /// - /// NOTE: If the builtins expected the return pointer first and closure data last, we could eliminate the wrapper - /// when all args are pass-by-reference and non-zero size. But currently we need it to swap those around. - pub fn build_higher_order_wrapper( - &mut self, - wrapper_lookup_idx: usize, - inner_lookup_idx: usize, - ) { - use Align::*; - use ValueType::*; - - let ProcLookupData { - name: wrapper_name, - layout: wrapper_proc_layout, - .. - } = self.proc_lookup[wrapper_lookup_idx]; - let wrapper_arg_layouts = wrapper_proc_layout.arguments; - - // Our convention is that the last arg of the wrapper is the heap return pointer - let heap_return_ptr_id = LocalId(wrapper_arg_layouts.len() as u32 - 1); - let inner_ret_layout = match wrapper_arg_layouts.last() { - Some(Layout::Boxed(inner)) => WasmLayout::new(inner), - x => internal_error!("Higher-order wrapper: invalid return layout {:?}", x), - }; - - let mut n_inner_wasm_args = 0; - let ret_type_and_size = match inner_ret_layout.return_method(CallConv::C) { - ReturnMethod::NoReturnValue => None, - ReturnMethod::Primitive(ty, size) => { - // If the inner function returns a primitive, load the address to store it at - // After the call, it will be under the call result in the value stack - self.code_builder.get_local(heap_return_ptr_id); - Some((ty, size)) - } - ReturnMethod::WriteToPointerArg => { - // If the inner function writes to a return pointer, load its address - self.code_builder.get_local(heap_return_ptr_id); - n_inner_wasm_args += 1; - None - } - x => internal_error!("A Roc function should never use ReturnMethod {:?}", x), - }; - - // Load all the arguments for the inner function - for (i, wrapper_arg) in wrapper_arg_layouts.iter().enumerate() { - let is_closure_data = i == 0; // Skip closure data (first for wrapper, last for inner) - let is_return_pointer = i == wrapper_arg_layouts.len() - 1; // Skip return pointer (may not be an arg for inner. And if it is, swaps from end to start) - if is_closure_data || is_return_pointer || wrapper_arg.stack_size(TARGET_INFO) == 0 { - continue; - } - n_inner_wasm_args += 1; - - // Load wrapper argument. They're all pointers. - self.code_builder.get_local(LocalId(i as u32)); - - // Dereference any primitive-valued arguments - match wrapper_arg { - Layout::Boxed(inner_arg) => match inner_arg { - Layout::Builtin(Builtin::Int(IntWidth::U8 | IntWidth::I8)) => { - self.code_builder.i32_load8_u(Bytes1, 0); - } - Layout::Builtin(Builtin::Int(IntWidth::U16 | IntWidth::I16)) => { - self.code_builder.i32_load16_u(Bytes2, 0); - } - Layout::Builtin(Builtin::Int(IntWidth::U32 | IntWidth::I32)) => { - self.code_builder.i32_load(Bytes4, 0); - } - Layout::Builtin(Builtin::Int(IntWidth::U64 | IntWidth::I64)) => { - self.code_builder.i64_load(Bytes8, 0); - } - Layout::Builtin(Builtin::Float(FloatWidth::F32)) => { - self.code_builder.f32_load(Bytes4, 0); - } - Layout::Builtin(Builtin::Float(FloatWidth::F64)) => { - self.code_builder.f64_load(Bytes8, 0); - } - Layout::Builtin(Builtin::Bool) => { - self.code_builder.i32_load8_u(Bytes1, 0); - } - _ => { - // Any other layout is a pointer, which we've already loaded. Nothing to do! - } - }, - x => internal_error!("Higher-order wrapper: expected a Box layout, got {:?}", x), - } - } - - // If the inner function has closure data, it's the last arg of the inner fn - let closure_data_layout = wrapper_arg_layouts[0]; - if closure_data_layout.stack_size(TARGET_INFO) > 0 { - self.code_builder.get_local(LocalId(0)); - } - - // Call the wrapped inner function - let inner_wasm_fn_index = self.fn_index_offset + inner_lookup_idx as u32; - let has_return_val = ret_type_and_size.is_some(); - self.code_builder - .call(inner_wasm_fn_index, n_inner_wasm_args, has_return_val); - - // If the inner function returns a primitive, store it to the address we loaded at the very beginning - if let Some((ty, size)) = ret_type_and_size { - match (ty, size) { - (I64, 8) => self.code_builder.i64_store(Bytes8, 0), - (I32, 4) => self.code_builder.i32_store(Bytes4, 0), - (I32, 2) => self.code_builder.i32_store16(Bytes2, 0), - (I32, 1) => self.code_builder.i32_store8(Bytes1, 0), - (F32, 4) => self.code_builder.f32_store(Bytes4, 0), - (F64, 8) => self.code_builder.f64_store(Bytes8, 0), - _ => { - internal_error!("Cannot store {:?} with alignment of {:?}", ty, size); - } - } - } - - // Write empty function header (local variables array with zero length) - self.code_builder.build_fn_header_and_footer(&[], 0, None); - - self.module.add_function_signature(Signature { - param_types: bumpalo::vec![in self.env.arena; I32; wrapper_arg_layouts.len()], - ret_type: None, - }); - - self.append_proc_debug_name(wrapper_name); - self.reset(); - } - - /********************************************************** - - STATEMENTS - - ***********************************************************/ - - fn stmt(&mut self, stmt: &Stmt<'a>) { - match stmt { - Stmt::Let(_, _, _, _) => self.stmt_let(stmt), - - Stmt::Ret(sym) => self.stmt_ret(*sym), - - Stmt::Switch { - cond_symbol, - cond_layout, - branches, - default_branch, - ret_layout: _, - } => self.stmt_switch(*cond_symbol, cond_layout, branches, default_branch), - - Stmt::Join { - id, - parameters, - body, - remainder, - } => self.stmt_join(*id, parameters, body, remainder), - - Stmt::Jump(id, arguments) => self.stmt_jump(*id, arguments), - - Stmt::Refcounting(modify, following) => self.stmt_refcounting(modify, following), - - Stmt::Expect { .. } => todo!("expect is not implemented in the wasm backend"), - - Stmt::RuntimeError(msg) => self.stmt_runtime_error(msg), - } - } - - fn start_block(&mut self) { - // Wasm blocks can have result types, but we don't use them. - // You need the right type on the stack when you jump from an inner block to an outer one. - // The rules are confusing, and implementing them would add complexity and slow down code gen. - // Instead we use local variables to move a value from an inner block to an outer one. - self.block_depth += 1; - self.code_builder.block(); - } - - fn start_loop(&mut self) { - self.block_depth += 1; - self.code_builder.loop_(); - } - - fn end_block(&mut self) { - self.block_depth -= 1; - self.code_builder.end(); - } - - fn stmt_let(&mut self, stmt: &Stmt<'a>) { - let mut current_stmt = stmt; - while let Stmt::Let(sym, expr, layout, following) = current_stmt { - if DEBUG_LOG_SETTINGS.let_stmt_ir { - println!("let {:?} = {}", sym, expr.to_pretty(200)); // ignore `following`! Too confusing otherwise. - } - - let kind = match following { - Stmt::Ret(ret_sym) if *sym == *ret_sym => StoredValueKind::ReturnValue, - _ => StoredValueKind::Variable, - }; - - self.stmt_let_store_expr(*sym, layout, expr, kind); - - current_stmt = *following; - } - - self.stmt(current_stmt); - } - - fn stmt_let_store_expr( - &mut self, - sym: Symbol, - layout: &Layout<'a>, - expr: &Expr<'a>, - kind: StoredValueKind, - ) { - let sym_storage = self.storage.allocate(*layout, sym, kind); - - self.expr(sym, expr, layout, &sym_storage); - - // If this value is stored in the VM stack, we need code_builder to track it - // (since every instruction can change the VM stack) - if let Some(StoredValue::VirtualMachineStack { vm_state, .. }) = - self.storage.symbol_storage_map.get_mut(&sym) - { - *vm_state = self.code_builder.set_top_symbol(sym); - } - } - - fn stmt_ret(&mut self, sym: Symbol) { - use crate::storage::StoredValue::*; - - let storage = self.storage.symbol_storage_map.get(&sym).unwrap(); - - match storage { - StackMemory { - location, - size, - alignment_bytes, - .. - } => { - let (from_ptr, from_offset) = - location.local_and_offset(self.storage.stack_frame_pointer); - copy_memory( - &mut self.code_builder, - CopyMemoryConfig { - from_ptr, - from_offset, - to_ptr: LocalId(0), - to_offset: 0, - size: *size, - alignment_bytes: *alignment_bytes, - }, - ); - } - - _ => { - self.storage.load_symbols(&mut self.code_builder, &[sym]); - - // If we have a return value, store it to the return variable - // This avoids complications with block result types when returning from nested blocks - if let Some(ret_var) = self.storage.return_var { - self.code_builder.set_local(ret_var); - } - } - } - // jump to the "stack frame pop" code at the end of the function - self.code_builder.br(self.block_depth - 1); - } - - fn stmt_switch( - &mut self, - cond_symbol: Symbol, - cond_layout: &Layout<'a>, - branches: &'a [(u64, BranchInfo<'a>, Stmt<'a>)], - default_branch: &(BranchInfo<'a>, &'a Stmt<'a>), - ) { - // NOTE currently implemented as a series of conditional jumps - // We may be able to improve this in the future with `Select` - // or `BrTable` - - // Ensure the condition value is not stored only in the VM stack - // Otherwise we can't reach it from inside the block - let cond_storage = self.storage.get(&cond_symbol).to_owned(); - self.storage - .ensure_value_has_local(&mut self.code_builder, cond_symbol, cond_storage); - - // create a block for each branch except the default - for _ in 0..branches.len() { - self.start_block() - } - - let is_bool = matches!(cond_layout, Layout::Builtin(Builtin::Bool)); - let cond_type = WasmLayout::new(cond_layout).arg_types(CallConv::C)[0]; - - // then, we jump whenever the value under scrutiny is equal to the value of a branch - for (i, (value, _, _)) in branches.iter().enumerate() { - // put the cond_symbol on the top of the stack - self.storage - .load_symbols(&mut self.code_builder, &[cond_symbol]); - - if is_bool { - // We already have a bool, don't need to compare against a const to get one - if *value == 0 { - self.code_builder.i32_eqz(); - } - } else { - match cond_type { - ValueType::I32 => { - self.code_builder.i32_const(*value as i32); - self.code_builder.i32_eq(); - } - ValueType::I64 => { - self.code_builder.i64_const(*value as i64); - self.code_builder.i64_eq(); - } - ValueType::F32 => { - self.code_builder.f32_const(f32::from_bits(*value as u32)); - self.code_builder.f32_eq(); - } - ValueType::F64 => { - self.code_builder.f64_const(f64::from_bits(*value as u64)); - self.code_builder.f64_eq(); - } - } - } - - // "break" out of `i` surrounding blocks - self.code_builder.br_if(i as u32); - } - - // if we never jumped because a value matched, we're in the default case - self.stmt(default_branch.1); - - // now put in the actual body of each branch in order - // (the first branch would have broken out of 1 block, - // hence we must generate its code first) - for (_, _, branch) in branches.iter() { - self.end_block(); - - self.stmt(branch); - } - } - - fn stmt_join( - &mut self, - id: JoinPointId, - parameters: &'a [Param<'a>], - body: &'a Stmt<'a>, - remainder: &'a Stmt<'a>, - ) { - // make locals for join pointer parameters - let mut jp_param_storages = Vec::with_capacity_in(parameters.len(), self.env.arena); - for parameter in parameters.iter() { - let mut param_storage = self.storage.allocate( - parameter.layout, - parameter.symbol, - StoredValueKind::Variable, - ); - param_storage = self.storage.ensure_value_has_local( - &mut self.code_builder, - parameter.symbol, - param_storage, - ); - jp_param_storages.push(param_storage); - } - - self.start_block(); - - self.joinpoint_label_map - .insert(id, (self.block_depth, jp_param_storages)); - - self.stmt(remainder); - - self.end_block(); - self.start_loop(); - - self.stmt(body); - - // ends the loop - self.end_block(); - } - - fn stmt_jump(&mut self, id: JoinPointId, arguments: &'a [Symbol]) { - let (target, param_storages) = self.joinpoint_label_map[&id].clone(); - - for (arg_symbol, param_storage) in arguments.iter().zip(param_storages.iter()) { - let arg_storage = self.storage.get(arg_symbol).clone(); - self.storage.clone_value( - &mut self.code_builder, - param_storage, - &arg_storage, - *arg_symbol, - ); - } - - // jump - let levels = self.block_depth - target; - self.code_builder.br(levels); - } - - fn stmt_refcounting(&mut self, modify: &ModifyRc, following: &'a Stmt<'a>) { - let value = modify.get_symbol(); - let layout = self.storage.symbol_layouts[&value]; - - let ident_ids = self - .interns - .all_ident_ids - .get_mut(&self.env.module_id) - .unwrap(); - - let (rc_stmt, new_specializations) = self - .helper_proc_gen - .expand_refcount_stmt(ident_ids, layout, modify, following); - - if false { - self.register_symbol_debug_names(); - println!("## rc_stmt:\n{}\n{:?}", rc_stmt.to_pretty(200), rc_stmt); - } - - // If any new specializations were created, register their symbol data - for (spec_sym, spec_layout) in new_specializations.into_iter() { - self.register_helper_proc(spec_sym, spec_layout, ProcSource::Helper); - } - - self.stmt(rc_stmt); - } - - fn stmt_runtime_error(&mut self, msg: &'a str) { - // Create a zero-terminated version of the message string - let mut bytes = Vec::with_capacity_in(msg.len() + 1, self.env.arena); - bytes.extend_from_slice(msg.as_bytes()); - bytes.push(0); - - // Store it in the app's data section - let elements_addr = self.store_bytes_in_data_section(&bytes); - - // Pass its address to roc_panic - let tag_id = 0; - self.code_builder.i32_const(elements_addr as i32); - self.code_builder.i32_const(tag_id); - self.call_zig_builtin_after_loading_args("roc_panic", 2, false); - - self.code_builder.unreachable_(); - } - - /********************************************************** - - EXPRESSIONS - - ***********************************************************/ - - fn expr(&mut self, sym: Symbol, expr: &Expr<'a>, layout: &Layout<'a>, storage: &StoredValue) { - match expr { - Expr::Literal(lit) => self.expr_literal(lit, storage), - - Expr::Call(roc_mono::ir::Call { - call_type, - arguments, - }) => self.expr_call(call_type, arguments, sym, layout, storage), - - Expr::Struct(fields) => self.expr_struct(sym, layout, storage, fields), - - Expr::StructAtIndex { - index, - field_layouts, - structure, - } => self.expr_struct_at_index(sym, storage, *index, field_layouts, *structure), - - Expr::Array { elems, elem_layout } => self.expr_array(sym, storage, elem_layout, elems), - - Expr::EmptyArray => self.expr_empty_array(sym, storage), - - Expr::Tag { - tag_layout: union_layout, - tag_id, - arguments, - .. - } => self.expr_tag(union_layout, *tag_id, arguments, sym, storage, None), - - Expr::GetTagId { - structure, - union_layout, - } => self.expr_get_tag_id(*structure, union_layout, sym, storage), - - Expr::UnionAtIndex { - structure, - tag_id, - union_layout, - index, - } => self.expr_union_at_index(*structure, *tag_id, union_layout, *index, sym), - - Expr::ExprBox { .. } | Expr::ExprUnbox { .. } => { - todo!("Expression `{}`", expr.to_pretty(100)) - } - - Expr::Reuse { - tag_layout, - tag_id, - arguments, - symbol: reused, - .. - } => self.expr_tag(tag_layout, *tag_id, arguments, sym, storage, Some(*reused)), - - Expr::Reset { symbol: arg, .. } => self.expr_reset(*arg, sym, storage), - - Expr::RuntimeErrorFunction(_) => { - todo!("Expression `{}`", expr.to_pretty(100)) - } - } - } - - /******************************************************************* - * Literals - *******************************************************************/ - - fn expr_literal(&mut self, lit: &Literal<'a>, storage: &StoredValue) { - let invalid_error = - || internal_error!("Literal value {:?} has invalid storage {:?}", lit, storage); - - match storage { - StoredValue::VirtualMachineStack { value_type, .. } => { - match (lit, value_type) { - (Literal::Float(x), ValueType::F64) => self.code_builder.f64_const(*x as f64), - (Literal::Float(x), ValueType::F32) => self.code_builder.f32_const(*x as f32), - (Literal::Int(x), ValueType::I64) => { - self.code_builder.i64_const(i128::from_ne_bytes(*x) as i64) - } - (Literal::Int(x), ValueType::I32) => { - self.code_builder.i32_const(i128::from_ne_bytes(*x) as i32) - } - (Literal::Bool(x), ValueType::I32) => self.code_builder.i32_const(*x as i32), - (Literal::Byte(x), ValueType::I32) => self.code_builder.i32_const(*x as i32), - _ => invalid_error(), - }; - } - - StoredValue::StackMemory { location, .. } => { - let mut write128 = |lower_bits, upper_bits| { - let (local_id, offset) = - location.local_and_offset(self.storage.stack_frame_pointer); - - self.code_builder.get_local(local_id); - self.code_builder.i64_const(lower_bits); - self.code_builder.i64_store(Align::Bytes8, offset); - - self.code_builder.get_local(local_id); - self.code_builder.i64_const(upper_bits); - self.code_builder.i64_store(Align::Bytes8, offset + 8); - }; - - match lit { - Literal::Decimal(bytes) => { - let (upper_bits, lower_bits) = RocDec::from_ne_bytes(*bytes).as_bits(); - write128(lower_bits as i64, upper_bits); - } - Literal::Int(x) => { - let lower_bits = (i128::from_ne_bytes(*x) & 0xffff_ffff_ffff_ffff) as i64; - let upper_bits = (i128::from_ne_bytes(*x) >> 64) as i64; - write128(lower_bits, upper_bits); - } - Literal::Float(_) => { - // Also not implemented in LLVM backend (nor in Rust!) - todo!("f128 type"); - } - Literal::Str(string) => { - let (local_id, offset) = - location.local_and_offset(self.storage.stack_frame_pointer); - - let len = string.len(); - if len < 12 { - // Construct the bytes of the small string - let mut bytes = [0; 12]; - bytes[0..len].clone_from_slice(string.as_bytes()); - bytes[11] = 0x80 | (len as u8); - - // Transform into two integers, to minimise number of instructions - let bytes_split: &([u8; 8], [u8; 4]) = - unsafe { std::mem::transmute(&bytes) }; - let int64 = i64::from_le_bytes(bytes_split.0); - let int32 = i32::from_le_bytes(bytes_split.1); - - // Write the integers to memory - self.code_builder.get_local(local_id); - self.code_builder.i64_const(int64); - self.code_builder.i64_store(Align::Bytes4, offset); - self.code_builder.get_local(local_id); - self.code_builder.i32_const(int32); - self.code_builder.i32_store(Align::Bytes4, offset + 8); - } else { - let bytes = string.as_bytes(); - let elements_addr = self.store_bytes_in_data_section(bytes); - - // ptr - self.code_builder.get_local(local_id); - self.code_builder.i32_const(elements_addr as i32); - self.code_builder.i32_store(Align::Bytes4, offset); - - // len - self.code_builder.get_local(local_id); - self.code_builder.i32_const(string.len() as i32); - self.code_builder.i32_store(Align::Bytes4, offset + 4); - - // capacity - self.code_builder.get_local(local_id); - self.code_builder.i32_const(string.len() as i32); - self.code_builder.i32_store(Align::Bytes4, offset + 8); - }; - } - _ => invalid_error(), - } - } - - _ => invalid_error(), - }; - } - - /// Create a string constant in the module data section - /// Return the data we need for code gen: linker symbol index and memory address - fn store_bytes_in_data_section(&mut self, bytes: &[u8]) -> u32 { - // Place the segment at a 4-byte aligned offset - let segment_addr = round_up_to_alignment!(self.module.data.end_addr, PTR_SIZE); - let elements_addr = segment_addr + PTR_SIZE; - let length_with_refcount = 4 + bytes.len(); - self.module.data.end_addr = segment_addr + length_with_refcount as u32; - - let mut segment = DataSegment { - mode: DataMode::active_at(segment_addr), - init: Vec::with_capacity_in(length_with_refcount, self.env.arena), - }; - - // Prefix the string bytes with "infinite" refcount - let refcount_max_bytes: [u8; 4] = (REFCOUNT_MAX as i32).to_le_bytes(); - segment.init.extend_from_slice(&refcount_max_bytes); - segment.init.extend_from_slice(bytes); - - self.module.data.append_segment(segment); - - elements_addr - } - - /******************************************************************* - * Call expressions - *******************************************************************/ - - fn expr_call( - &mut self, - call_type: &CallType<'a>, - arguments: &'a [Symbol], - ret_sym: Symbol, - ret_layout: &Layout<'a>, - ret_storage: &StoredValue, - ) { - match call_type { - CallType::ByName { - name: func_sym, - arg_layouts, - ret_layout: result, - .. - } => { - let proc_layout = ProcLayout { - arguments: arg_layouts, - result: **result, - }; - self.expr_call_by_name( - *func_sym, - &proc_layout, - arguments, - ret_sym, - ret_layout, - ret_storage, - ) - } - - CallType::LowLevel { op: lowlevel, .. } => { - self.expr_call_low_level(*lowlevel, arguments, ret_sym, ret_layout, ret_storage) - } - - CallType::HigherOrder(higher_order_lowlevel) => { - call_higher_order_lowlevel(self, ret_sym, ret_layout, *higher_order_lowlevel) - } - - CallType::Foreign { .. } => todo!("CallType::Foreign"), - } - } - - fn expr_call_by_name( - &mut self, - func_sym: Symbol, - proc_layout: &ProcLayout<'a>, - arguments: &'a [Symbol], - ret_sym: Symbol, - ret_layout: &Layout<'a>, - ret_storage: &StoredValue, - ) { - let wasm_layout = WasmLayout::new(ret_layout); - - // If this function is just a lowlevel wrapper, then inline it - if let LowLevelWrapperType::CanBeReplacedBy(lowlevel) = - LowLevelWrapperType::from_symbol(func_sym) - { - return self.expr_call_low_level(lowlevel, arguments, ret_sym, ret_layout, ret_storage); - } - - let (num_wasm_args, has_return_val, ret_zig_packed_struct) = - self.storage.load_symbols_for_call( - self.env.arena, - &mut self.code_builder, - arguments, - ret_sym, - &wasm_layout, - CallConv::C, - ); - debug_assert!(!ret_zig_packed_struct); - - let roc_proc_index = self - .proc_lookup - .iter() - .position(|lookup| lookup.name == func_sym && &lookup.layout == proc_layout) - .unwrap_or_else(|| { - internal_error!( - "Could not find procedure {:?} with proc_layout:\n{:#?}\nKnown procedures:\n{:#?}", - func_sym, - proc_layout, - self.proc_lookup - ); - }); - - let wasm_fn_index = self.fn_index_offset + roc_proc_index as u32; - - self.code_builder - .call(wasm_fn_index, num_wasm_args, has_return_val); - } - - fn expr_call_low_level( - &mut self, - lowlevel: LowLevel, - arguments: &'a [Symbol], - ret_symbol: Symbol, - ret_layout: &Layout<'a>, - ret_storage: &StoredValue, - ) { - let low_level_call = LowLevelCall { - lowlevel, - arguments, - ret_symbol, - ret_layout: ret_layout.to_owned(), - ret_storage: ret_storage.to_owned(), - }; - low_level_call.generate(self); - } - - /// Generate a call instruction to a Zig builtin function. - /// And if we haven't seen it before, add an Import and linker data for it. - /// Zig calls use LLVM's "fast" calling convention rather than our usual C ABI. - pub fn call_zig_builtin_after_loading_args( - &mut self, - name: &'a str, - num_wasm_args: usize, - has_return_val: bool, - ) { - let (_, fn_index) = self - .host_lookup - .iter() - .find(|(fn_name, _)| *fn_name == name) - .unwrap_or_else(|| { - panic!( - "I can't find the builtin function `{}` in the preprocessed host file.", - name - ) - }); - - self.called_preload_fns.push(*fn_index); - self.code_builder - .call(*fn_index, num_wasm_args, has_return_val); - } - - /// Call a helper procedure that implements `==` for a data structure (not numbers or Str) - /// If this is the first call for this Layout, it will generate the IR for the procedure. - /// Call stack is expr_call_low_level -> LowLevelCall::generate -> call_eq_specialized - /// It's a bit circuitous, but the alternative is to give low_level.rs `pub` access to - /// interns, helper_proc_gen, and expr(). That just seemed all wrong. - pub fn call_eq_specialized( - &mut self, - arguments: &'a [Symbol], - arg_layout: &Layout<'a>, - ret_symbol: Symbol, - ret_storage: &StoredValue, - ) { - let ident_ids = self - .interns - .all_ident_ids - .get_mut(&self.env.module_id) - .unwrap(); - - // Get an IR expression for the call to the specialized procedure - let (specialized_call_expr, new_specializations) = self - .helper_proc_gen - .call_specialized_equals(ident_ids, arg_layout, arguments); - - // If any new specializations were created, register their symbol data - for (spec_sym, spec_layout) in new_specializations.into_iter() { - self.register_helper_proc(spec_sym, spec_layout, ProcSource::Helper); - } - - // Generate Wasm code for the IR call expression - self.expr( - ret_symbol, - self.env.arena.alloc(specialized_call_expr), - &Layout::Builtin(Builtin::Bool), - ret_storage, - ); - } - - /******************************************************************* - * Structs - *******************************************************************/ - - fn expr_struct( - &mut self, - sym: Symbol, - layout: &Layout<'a>, - storage: &StoredValue, - fields: &'a [Symbol], - ) { - if matches!(layout, Layout::Struct { .. }) { - match storage { - StoredValue::StackMemory { location, size, .. } => { - if *size > 0 { - let (local_id, struct_offset) = - location.local_and_offset(self.storage.stack_frame_pointer); - let mut field_offset = struct_offset; - for field in fields.iter() { - field_offset += self.storage.copy_value_to_memory( - &mut self.code_builder, - local_id, - field_offset, - *field, - ); - } - } else { - // Zero-size struct. No code to emit. - // These values are purely conceptual, they only exist internally in the compiler - } - } - _ => internal_error!("Cannot create struct {:?} with storage {:?}", sym, storage), - }; - } else if !fields.is_empty() { - // Struct expression but not Struct layout => single element. Copy it. - let field_storage = self.storage.get(&fields[0]).to_owned(); - self.storage - .clone_value(&mut self.code_builder, storage, &field_storage, fields[0]); - } else { - // Empty record. Nothing to do. - } - } - - fn expr_struct_at_index( - &mut self, - sym: Symbol, - storage: &StoredValue, - index: u64, - field_layouts: &'a [Layout<'a>], - structure: Symbol, - ) { - self.storage - .ensure_value_has_local(&mut self.code_builder, sym, storage.to_owned()); - let (local_id, mut offset) = match self.storage.get(&structure) { - StoredValue::StackMemory { location, .. } => { - location.local_and_offset(self.storage.stack_frame_pointer) - } - - StoredValue::Local { - value_type, - local_id, - .. - } => { - debug_assert!(matches!(value_type, ValueType::I32)); - (*local_id, 0) - } - - StoredValue::VirtualMachineStack { .. } => { - internal_error!("ensure_value_has_local didn't work") - } - }; - for field in field_layouts.iter().take(index as usize) { - offset += field.stack_size(TARGET_INFO); - } - self.storage - .copy_value_from_memory(&mut self.code_builder, sym, local_id, offset); - } - - /******************************************************************* - * Arrays - *******************************************************************/ - - fn expr_array( - &mut self, - sym: Symbol, - storage: &StoredValue, - elem_layout: &Layout<'a>, - elems: &'a [ListLiteralElement<'a>], - ) { - if let StoredValue::StackMemory { location, .. } = storage { - let size = elem_layout.stack_size(TARGET_INFO) * (elems.len() as u32); - - // Allocate heap space and store its address in a local variable - let heap_local_id = self.storage.create_anonymous_local(PTR_TYPE); - let heap_alignment = elem_layout.alignment_bytes(TARGET_INFO); - self.allocate_with_refcount(Some(size), heap_alignment, 1); - self.code_builder.set_local(heap_local_id); - - let (stack_local_id, stack_offset) = - location.local_and_offset(self.storage.stack_frame_pointer); - - // elements pointer - self.code_builder.get_local(stack_local_id); - self.code_builder.get_local(heap_local_id); - self.code_builder.i32_store(Align::Bytes4, stack_offset); - - // length of the list - self.code_builder.get_local(stack_local_id); - self.code_builder.i32_const(elems.len() as i32); - self.code_builder.i32_store(Align::Bytes4, stack_offset + 4); - - let mut elem_offset = 0; - - for (i, elem) in elems.iter().enumerate() { - let elem_sym = match elem { - ListLiteralElement::Literal(lit) => { - // This has no Symbol but our storage methods expect one. - // Let's just pretend it was defined in a `Let`. - let debug_name = format!("{:?}_{}", sym, i); - let elem_sym = self.create_symbol(&debug_name); - let expr = Expr::Literal(*lit); - - self.stmt_let_store_expr( - elem_sym, - elem_layout, - &expr, - StoredValueKind::Variable, - ); - - elem_sym - } - - ListLiteralElement::Symbol(elem_sym) => *elem_sym, - }; - - elem_offset += self.storage.copy_value_to_memory( - &mut self.code_builder, - heap_local_id, - elem_offset, - elem_sym, - ); - } - } else { - internal_error!("Unexpected storage for Array {:?}: {:?}", sym, storage) - } - } - - fn expr_empty_array(&mut self, sym: Symbol, storage: &StoredValue) { - if let StoredValue::StackMemory { location, .. } = storage { - let (local_id, offset) = location.local_and_offset(self.storage.stack_frame_pointer); - - // This is a minor cheat. - // What we want to write to stack memory is { elements: null, length: 0 } - // But instead of two 32-bit stores, we can do a single 64-bit store. - self.code_builder.get_local(local_id); - self.code_builder.i64_const(0); - self.code_builder.i64_store(Align::Bytes4, offset); - } else { - internal_error!("Unexpected storage for {:?}", sym) - } - } - - /******************************************************************* - * Tag Unions - *******************************************************************/ - - fn expr_tag( - &mut self, - union_layout: &UnionLayout<'a>, - tag_id: TagIdIntType, - arguments: &'a [Symbol], - symbol: Symbol, - stored: &StoredValue, - maybe_reused: Option, - ) { - if union_layout.tag_is_null(tag_id) { - self.code_builder.i32_const(0); - return; - } - - let stores_tag_id_as_data = union_layout.stores_tag_id_as_data(TARGET_INFO); - let stores_tag_id_in_pointer = union_layout.stores_tag_id_in_pointer(TARGET_INFO); - let (data_size, data_alignment) = union_layout.data_size_and_alignment(TARGET_INFO); - - // We're going to use the pointer many times, so put it in a local variable - let stored_with_local = - self.storage - .ensure_value_has_local(&mut self.code_builder, symbol, stored.to_owned()); - - let (local_id, data_offset) = match stored_with_local { - StoredValue::StackMemory { location, .. } => { - location.local_and_offset(self.storage.stack_frame_pointer) - } - StoredValue::Local { local_id, .. } => { - // Tag is stored as a heap pointer. - if let Some(reused) = maybe_reused { - // Reuse an existing heap allocation - self.storage.load_symbols(&mut self.code_builder, &[reused]); - } else { - // Call the allocator to get a memory address. - self.allocate_with_refcount(Some(data_size), data_alignment, 1); - } - self.code_builder.set_local(local_id); - (local_id, 0) - } - StoredValue::VirtualMachineStack { .. } => { - internal_error!("{:?} should have a local variable", symbol) - } - }; - - // Write the field values to memory - let mut field_offset = data_offset; - for field_symbol in arguments.iter() { - field_offset += self.storage.copy_value_to_memory( - &mut self.code_builder, - local_id, - field_offset, - *field_symbol, - ); - } - - // Store the tag ID (if any) - if stores_tag_id_as_data { - let id_offset = data_offset + data_size - data_alignment; - - let id_align = union_layout.tag_id_builtin().alignment_bytes(TARGET_INFO); - let id_align = Align::from(id_align); - - self.code_builder.get_local(local_id); - - match id_align { - Align::Bytes1 => { - self.code_builder.i32_const(tag_id as i32); - self.code_builder.i32_store8(id_align, id_offset); - } - Align::Bytes2 => { - self.code_builder.i32_const(tag_id as i32); - self.code_builder.i32_store16(id_align, id_offset); - } - Align::Bytes4 => { - self.code_builder.i32_const(tag_id as i32); - self.code_builder.i32_store(id_align, id_offset); - } - Align::Bytes8 => { - self.code_builder.i64_const(tag_id as i64); - self.code_builder.i64_store(id_align, id_offset); - } - } - } else if stores_tag_id_in_pointer { - self.code_builder.get_local(local_id); - self.code_builder.i32_const(tag_id as i32); - self.code_builder.i32_or(); - self.code_builder.set_local(local_id); - } - } - - fn expr_get_tag_id( - &mut self, - structure: Symbol, - union_layout: &UnionLayout<'a>, - tag_id_symbol: Symbol, - stored_value: &StoredValue, - ) { - use UnionLayout::*; - - let block_result_id = match union_layout { - NonRecursive(_) => None, - Recursive(_) => None, - NonNullableUnwrapped(_) => { - self.code_builder.i32_const(0); - return; - } - NullableWrapped { nullable_id, .. } => { - let stored_with_local = self.storage.ensure_value_has_local( - &mut self.code_builder, - tag_id_symbol, - stored_value.to_owned(), - ); - let local_id = match stored_with_local { - StoredValue::Local { local_id, .. } => local_id, - _ => internal_error!("ensure_value_has_local didn't work"), - }; - - // load pointer - self.storage - .load_symbols(&mut self.code_builder, &[structure]); - - // null check - self.code_builder.i32_eqz(); - self.code_builder.if_(); - self.code_builder.i32_const(*nullable_id as i32); - self.code_builder.set_local(local_id); - self.code_builder.else_(); - Some(local_id) - } - NullableUnwrapped { nullable_id, .. } => { - self.code_builder.i32_const(!(*nullable_id) as i32); - self.code_builder.i32_const(*nullable_id as i32); - self.storage - .load_symbols(&mut self.code_builder, &[structure]); - self.code_builder.select(); - None - } - }; - - if union_layout.stores_tag_id_as_data(TARGET_INFO) { - let (data_size, data_alignment) = union_layout.data_size_and_alignment(TARGET_INFO); - let id_offset = data_size - data_alignment; - - let id_align = union_layout.tag_id_builtin().alignment_bytes(TARGET_INFO); - let id_align = Align::from(id_align); - - self.storage - .load_symbols(&mut self.code_builder, &[structure]); - - match union_layout.tag_id_builtin() { - Builtin::Bool | Builtin::Int(IntWidth::U8) => { - self.code_builder.i32_load8_u(id_align, id_offset) - } - Builtin::Int(IntWidth::U16) => self.code_builder.i32_load16_u(id_align, id_offset), - Builtin::Int(IntWidth::U32) => self.code_builder.i32_load(id_align, id_offset), - Builtin::Int(IntWidth::U64) => self.code_builder.i64_load(id_align, id_offset), - x => internal_error!("Unexpected layout for tag union id {:?}", x), - } - } else if union_layout.stores_tag_id_in_pointer(TARGET_INFO) { - self.storage - .load_symbols(&mut self.code_builder, &[structure]); - self.code_builder.i32_const(3); - self.code_builder.i32_and(); - } - - if let Some(local_id) = block_result_id { - self.code_builder.set_local(local_id); - self.code_builder.end(); - } - } - - fn expr_union_at_index( - &mut self, - structure: Symbol, - tag_id: TagIdIntType, - union_layout: &UnionLayout<'a>, - index: u64, - symbol: Symbol, - ) { - use UnionLayout::*; - - debug_assert!(!union_layout.tag_is_null(tag_id)); - - let tag_index = tag_id as usize; - let field_layouts = match union_layout { - NonRecursive(tags) => tags[tag_index], - Recursive(tags) => tags[tag_index], - NonNullableUnwrapped(layouts) => *layouts, - NullableWrapped { - other_tags, - nullable_id, - } => { - let index = if tag_index > *nullable_id as usize { - tag_index - 1 - } else { - tag_index - }; - other_tags[index] - } - NullableUnwrapped { other_fields, .. } => *other_fields, - }; - - let field_offset: u32 = field_layouts - .iter() - .take(index as usize) - .map(|field_layout| field_layout.stack_size(TARGET_INFO)) - .sum(); - - // Get pointer and offset to the tag's data - let structure_storage = self.storage.get(&structure).to_owned(); - let stored_with_local = self.storage.ensure_value_has_local( - &mut self.code_builder, - structure, - structure_storage, - ); - let (tag_local_id, tag_offset) = match stored_with_local { - StoredValue::StackMemory { location, .. } => { - location.local_and_offset(self.storage.stack_frame_pointer) - } - StoredValue::Local { local_id, .. } => (local_id, 0), - StoredValue::VirtualMachineStack { .. } => { - internal_error!("{:?} should have a local variable", structure) - } - }; - - let stores_tag_id_in_pointer = union_layout.stores_tag_id_in_pointer(TARGET_INFO); - - let from_ptr = if stores_tag_id_in_pointer { - let ptr = self.storage.create_anonymous_local(ValueType::I32); - self.code_builder.get_local(tag_local_id); - self.code_builder.i32_const(-4); // 11111111...1100 - self.code_builder.i32_and(); - self.code_builder.set_local(ptr); - ptr - } else { - tag_local_id - }; - - let from_offset = tag_offset + field_offset; - self.storage - .copy_value_from_memory(&mut self.code_builder, symbol, from_ptr, from_offset); - } - - /******************************************************************* - * Refcounting & Heap allocation - *******************************************************************/ - - /// Allocate heap space and write an initial refcount - /// If the data size is known at compile time, pass it in comptime_data_size. - /// If size is only known at runtime, push *data* size to the VM stack first. - /// Leaves the *data* address on the VM stack - fn allocate_with_refcount( - &mut self, - comptime_data_size: Option, - alignment_bytes: u32, - initial_refcount: u32, - ) { - // Add extra bytes for the refcount - let extra_bytes = alignment_bytes.max(PTR_SIZE); - - if let Some(data_size) = comptime_data_size { - // Data size known at compile time and passed as an argument - self.code_builder - .i32_const((data_size + extra_bytes) as i32); - } else { - // Data size known only at runtime and is on top of VM stack - self.code_builder.i32_const(extra_bytes as i32); - self.code_builder.i32_add(); - } - - // Provide a constant for the alignment argument - self.code_builder.i32_const(alignment_bytes as i32); - - // Call the foreign function. (Zig and C calling conventions are the same for this signature) - self.call_zig_builtin_after_loading_args("roc_alloc", 2, true); - - // Save the allocation address to a temporary local variable - let local_id = self.storage.create_anonymous_local(ValueType::I32); - self.code_builder.tee_local(local_id); - - // Write the initial refcount - let refcount_offset = extra_bytes - PTR_SIZE; - let encoded_refcount = (initial_refcount as i32) - 1 + i32::MIN; - self.code_builder.i32_const(encoded_refcount); - self.code_builder.i32_store(Align::Bytes4, refcount_offset); - - // Put the data address on the VM stack - self.code_builder.get_local(local_id); - self.code_builder.i32_const(extra_bytes as i32); - self.code_builder.i32_add(); - } - - fn expr_reset(&mut self, argument: Symbol, ret_symbol: Symbol, ret_storage: &StoredValue) { - let ident_ids = self - .interns - .all_ident_ids - .get_mut(&self.env.module_id) - .unwrap(); - - // Get an IR expression for the call to the specialized procedure - let layout = self.storage.symbol_layouts[&argument]; - let (specialized_call_expr, new_specializations) = self - .helper_proc_gen - .call_reset_refcount(ident_ids, layout, argument); - - // If any new specializations were created, register their symbol data - for (spec_sym, spec_layout) in new_specializations.into_iter() { - self.register_helper_proc(spec_sym, spec_layout, ProcSource::Helper); - } - - // Generate Wasm code for the IR call expression - self.expr( - ret_symbol, - self.env.arena.alloc(specialized_call_expr), - &Layout::Builtin(Builtin::Bool), - ret_storage, - ); - } - - /// Generate a refcount helper procedure and return a pointer (table index) to it - /// This allows it to be indirectly called from Zig code - pub fn get_refcount_fn_ptr(&mut self, layout: Layout<'a>, op: HelperOp) -> i32 { - let ident_ids = self - .interns - .all_ident_ids - .get_mut(&self.env.module_id) - .unwrap(); - - let (proc_symbol, new_specializations) = self - .helper_proc_gen - .gen_refcount_proc(ident_ids, layout, op); - - // If any new specializations were created, register their symbol data - for (spec_sym, spec_layout) in new_specializations.into_iter() { - self.register_helper_proc(spec_sym, spec_layout, ProcSource::Helper); - } - - let proc_index = self - .proc_lookup - .iter() - .position(|lookup| lookup.name == proc_symbol && lookup.layout.arguments[0] == layout) - .unwrap(); - - let wasm_fn_index = self.fn_index_offset + proc_index as u32; - self.get_fn_table_index(wasm_fn_index) - } -} diff --git a/compiler/gen_wasm/src/layout.rs b/compiler/gen_wasm/src/layout.rs deleted file mode 100644 index 36335bb78e..0000000000 --- a/compiler/gen_wasm/src/layout.rs +++ /dev/null @@ -1,212 +0,0 @@ -use roc_builtins::bitcode::{FloatWidth, IntWidth}; -use roc_mono::layout::{Layout, UnionLayout}; - -use crate::wasm_module::ValueType; -use crate::{PTR_SIZE, PTR_TYPE, TARGET_INFO}; - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum ReturnMethod { - /// This layout is returned from a Wasm function "normally" as a Primitive - Primitive(ValueType, u32), - /// This layout is returned by writing to a pointer passed as the first argument - WriteToPointerArg, - /// This layout is empty and requires no return value or argument (e.g. refcount helpers) - NoReturnValue, - /// This layout is returned as a packed struct in an integer. Only used by Zig, not C. - ZigPackedStruct, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum StackMemoryFormat { - /// Record, Str, List, Dict, etc. - DataStructure, - Int128, - Float128, - Decimal, -} - -// See README for background information on Wasm locals, memory and function calls -#[derive(Debug, Clone)] -pub enum WasmLayout { - // Primitive number value, without any stack memory. - // For example, Roc i8 is represented as Primitive(ValueType::I32, 1) - Primitive(ValueType, u32), - - // Local pointer to stack memory - StackMemory { - size: u32, - alignment_bytes: u32, - format: StackMemoryFormat, - }, -} - -impl WasmLayout { - pub fn new(layout: &Layout) -> Self { - use roc_mono::layout::Builtin::*; - use UnionLayout::*; - use ValueType::*; - - let (size, alignment_bytes) = layout.stack_size_and_alignment(TARGET_INFO); - - match layout { - Layout::Builtin(Int(int_width)) => { - use IntWidth::*; - - match int_width { - I32 | U32 | I16 | U16 | I8 | U8 => Self::Primitive(ValueType::I32, size), - I64 | U64 => Self::Primitive(ValueType::I64, size), - I128 | U128 => Self::StackMemory { - size, - alignment_bytes, - format: StackMemoryFormat::Int128, - }, - } - } - - Layout::Builtin(Bool) => Self::Primitive(I32, size), - - Layout::Builtin(Float(float_width)) => { - use FloatWidth::*; - - match float_width { - F32 => Self::Primitive(ValueType::F32, size), - F64 => Self::Primitive(ValueType::F64, size), - F128 => Self::StackMemory { - size, - alignment_bytes, - format: StackMemoryFormat::Float128, - }, - } - } - - Layout::Builtin(Decimal) => Self::StackMemory { - size, - alignment_bytes, - format: StackMemoryFormat::Decimal, - }, - - Layout::LambdaSet(lambda_set) => WasmLayout::new(&lambda_set.runtime_representation()), - - Layout::Builtin(Str | Dict(_, _) | Set(_) | List(_)) - | Layout::Struct { .. } - | Layout::Union(NonRecursive(_)) => Self::StackMemory { - size, - alignment_bytes, - format: StackMemoryFormat::DataStructure, - }, - - Layout::Union( - Recursive(_) - | NonNullableUnwrapped(_) - | NullableWrapped { .. } - | NullableUnwrapped { .. }, - ) - | Layout::Boxed(_) - | Layout::RecursivePointer => Self::Primitive(PTR_TYPE, PTR_SIZE), - } - } - - /// The `ValueType`s to use for this layout when calling a Wasm function - /// One Roc argument can become 0, 1, or 2 Wasm arguments - pub fn arg_types(&self, conv: CallConv) -> &'static [ValueType] { - use ValueType::*; - - match self { - // 1 Roc argument => 1 Wasm argument (same for all calling conventions) - Self::Primitive(I32, _) => &[I32], - Self::Primitive(I64, _) => &[I64], - Self::Primitive(F32, _) => &[F32], - Self::Primitive(F64, _) => &[F64], - - // 1 Roc argument => 0-2 Wasm arguments (depending on size and calling convention) - Self::StackMemory { size, format, .. } => conv.stack_memory_arg_types(*size, *format), - } - } - - pub fn return_method(&self, conv: CallConv) -> ReturnMethod { - match self { - Self::Primitive(ty, size) => ReturnMethod::Primitive(*ty, *size), - Self::StackMemory { size, format, .. } => { - conv.stack_memory_return_method(*size, *format) - } - } - } -} - -#[derive(Debug, Clone, Copy)] -pub enum CallConv { - /// The C calling convention, as defined here: - /// https://github.com/WebAssembly/tool-conventions/blob/main/BasicCABI.md - C, - /// The calling convention that Zig 0.9 generates for Wasm when we *ask* it - /// for the .C calling convention, due to bugs in the Zig compiler. - Zig, -} - -impl CallConv { - /// The Wasm argument types to use when passing structs or 128-bit numbers - pub fn stack_memory_arg_types( - &self, - size: u32, - format: StackMemoryFormat, - ) -> &'static [ValueType] { - use StackMemoryFormat::*; - use ValueType::*; - - match format { - Int128 | Float128 | Decimal => &[I64, I64], - - DataStructure => { - if size == 0 { - // Zero-size Roc values like `{}` => no Wasm arguments - return &[]; - } - match self { - CallConv::C => { - &[I32] // Always pass structs by reference (pointer to stack memory) - } - - CallConv::Zig => { - if size <= 4 { - &[I32] // Small struct: pass by value - } else if size <= 8 { - &[I64] // Small struct: pass by value - } else if size <= 12 { - &[I64, I32] // Medium struct: pass by value, as two Wasm arguments - } else if size <= 16 { - &[I64, I64] // Medium struct: pass by value, as two Wasm arguments - } else { - &[I32] // Large struct: pass by reference - } - } - } - } - } - } - - pub fn stack_memory_return_method(&self, size: u32, format: StackMemoryFormat) -> ReturnMethod { - use ReturnMethod::*; - use StackMemoryFormat::*; - - match format { - Int128 | Float128 | Decimal => WriteToPointerArg, - - DataStructure => { - if size == 0 { - return NoReturnValue; - } - match self { - CallConv::C => WriteToPointerArg, - - CallConv::Zig => { - if size <= 8 { - ZigPackedStruct - } else { - WriteToPointerArg - } - } - } - } - } - } -} diff --git a/compiler/gen_wasm/src/lib.rs b/compiler/gen_wasm/src/lib.rs deleted file mode 100644 index 81226668bc..0000000000 --- a/compiler/gen_wasm/src/lib.rs +++ /dev/null @@ -1,268 +0,0 @@ -mod backend; -mod layout; -mod low_level; -mod storage; -pub mod wasm_module; - -// Helpers for interfacing to a Wasm module from outside -pub mod wasm32_result; -pub mod wasm32_sized; - -use bumpalo::collections::Vec; -use bumpalo::{self, Bump}; - -use roc_collections::all::{MutMap, MutSet}; -use roc_module::low_level::LowLevelWrapperType; -use roc_module::symbol::{Interns, ModuleId, Symbol}; -use roc_mono::code_gen_help::CodeGenHelp; -use roc_mono::ir::{Proc, ProcLayout}; -use roc_mono::layout::LayoutIds; -use roc_target::TargetInfo; -use wasm_module::parse::ParseError; - -use crate::backend::{ProcLookupData, ProcSource, WasmBackend}; -use crate::wasm_module::{Align, CodeBuilder, LocalId, ValueType, WasmModule}; - -const TARGET_INFO: TargetInfo = TargetInfo::default_wasm32(); -const PTR_SIZE: u32 = { - let value = TARGET_INFO.ptr_width() as u32; - - // const assert that our pointer width is actually 4 - // the code relies on the pointer width being exactly 4 - assert!(value == 4); - - value -}; -const PTR_TYPE: ValueType = ValueType::I32; - -pub const STACK_POINTER_GLOBAL_ID: u32 = 0; -pub const FRAME_ALIGNMENT_BYTES: i32 = 16; -pub const MEMORY_NAME: &str = "memory"; -pub const BUILTINS_IMPORT_MODULE_NAME: &str = "env"; -pub const STACK_POINTER_NAME: &str = "__stack_pointer"; - -pub struct Env<'a> { - pub arena: &'a Bump, - pub module_id: ModuleId, - pub exposed_to_host: MutSet, -} - -/// Parse the preprocessed host binary -/// If successful, the module can be passed to build_app_binary -pub fn parse_host<'a>(arena: &'a Bump, host_bytes: &[u8]) -> Result, ParseError> { - WasmModule::preload(arena, host_bytes) -} - -/// Generate a Wasm module in binary form, ready to write to a file. Entry point from roc_build. -/// env environment data from previous compiler stages -/// interns names of functions and variables (as memory-efficient interned strings) -/// host_module parsed module from a Wasm object file containing all of the non-Roc code -/// procedures Roc code in monomorphized intermediate representation -pub fn build_app_binary<'a>( - env: &'a Env<'a>, - interns: &'a mut Interns, - host_module: WasmModule<'a>, - procedures: MutMap<(Symbol, ProcLayout<'a>), Proc<'a>>, -) -> std::vec::Vec { - let (mut wasm_module, called_preload_fns, _) = - build_app_module(env, interns, host_module, procedures); - - wasm_module.remove_dead_preloads(env.arena, called_preload_fns); - - let mut buffer = std::vec::Vec::with_capacity(wasm_module.size()); - wasm_module.serialize(&mut buffer); - buffer -} - -/// Generate an unserialized Wasm module -/// Shared by all consumers of gen_wasm: roc_build, roc_repl_wasm, and test_gen -/// (roc_repl_wasm and test_gen will add more generated code for a wrapper function -/// that defines a common interface to `main`, independent of return type.) -pub fn build_app_module<'a>( - env: &'a Env<'a>, - interns: &'a mut Interns, - host_module: WasmModule<'a>, - procedures: MutMap<(Symbol, ProcLayout<'a>), Proc<'a>>, -) -> (WasmModule<'a>, Vec<'a, u32>, u32) { - let layout_ids = LayoutIds::default(); - let mut procs = Vec::with_capacity_in(procedures.len(), env.arena); - let mut proc_lookup = Vec::with_capacity_in(procedures.len() * 2, env.arena); - let mut host_to_app_map = Vec::with_capacity_in(env.exposed_to_host.len(), env.arena); - let mut maybe_main_fn_index = None; - - // Adjust Wasm function indices to account for functions from the object file - let fn_index_offset: u32 = - host_module.import.function_count() as u32 + host_module.code.preloaded_count; - - // Pre-pass over the procedure names & layouts - // Filter out procs we're going to inline & gather some data for lookups - let mut fn_index: u32 = fn_index_offset; - for ((sym, proc_layout), proc) in procedures.into_iter() { - if matches!( - LowLevelWrapperType::from_symbol(sym), - LowLevelWrapperType::CanBeReplacedBy(_) - ) { - continue; - } - procs.push(proc); - - if env.exposed_to_host.contains(&sym) { - maybe_main_fn_index = Some(fn_index); - - // Assumption: there is only one specialization of a host-exposed function - let ident_string = sym.as_str(interns); - let c_function_name = bumpalo::format!(in env.arena, "roc__{}_1_exposed", ident_string); - host_to_app_map.push((c_function_name.into_bump_str(), fn_index)); - } - - proc_lookup.push(ProcLookupData { - name: sym, - layout: proc_layout, - source: ProcSource::Roc, - }); - - fn_index += 1; - } - - let mut backend = WasmBackend::new( - env, - interns, - layout_ids, - proc_lookup, - host_to_app_map, - host_module, - fn_index_offset, - CodeGenHelp::new(env.arena, TargetInfo::default_wasm32(), env.module_id), - ); - - if DEBUG_LOG_SETTINGS.user_procs_ir { - println!("## procs"); - for proc in procs.iter() { - println!("{}", proc.to_pretty(200)); - // println!("{:?}", proc); - } - } - - // Generate procs from user code - for proc in procs.iter() { - backend.build_proc(proc); - } - - // Generate specialized helpers for refcounting & equality - let helper_procs = backend.get_helpers(); - - backend.register_symbol_debug_names(); - - if DEBUG_LOG_SETTINGS.helper_procs_ir { - println!("## helper_procs"); - for proc in helper_procs.iter() { - println!("{}", proc.to_pretty(200)); - // println!("{:#?}", proc); - } - } - - // Generate Wasm for helpers and Zig/Roc wrappers - let sources = Vec::from_iter_in( - backend - .proc_lookup - .iter() - .map(|ProcLookupData { source, .. }| *source), - env.arena, - ); - let mut helper_iter = helper_procs.iter(); - for (idx, source) in sources.iter().enumerate() { - use ProcSource::*; - match source { - Roc => { /* already generated */ } - Helper => backend.build_proc(helper_iter.next().unwrap()), - HigherOrderWrapper(inner_idx) => backend.build_higher_order_wrapper(idx, *inner_idx), - } - } - - let (module, called_preload_fns) = backend.finalize(); - let main_function_index = maybe_main_fn_index.unwrap(); - - (module, called_preload_fns, main_function_index) -} - -pub struct CopyMemoryConfig { - from_ptr: LocalId, - from_offset: u32, - to_ptr: LocalId, - to_offset: u32, - size: u32, - alignment_bytes: u32, -} - -pub fn copy_memory(code_builder: &mut CodeBuilder, config: CopyMemoryConfig) { - if config.from_ptr == config.to_ptr && config.from_offset == config.to_offset { - return; - } - if config.size == 0 { - return; - } - - let alignment = Align::from(config.alignment_bytes); - let mut i = 0; - while config.size - i >= 8 { - code_builder.get_local(config.to_ptr); - code_builder.get_local(config.from_ptr); - code_builder.i64_load(alignment, i + config.from_offset); - code_builder.i64_store(alignment, i + config.to_offset); - i += 8; - } - if config.size - i >= 4 { - code_builder.get_local(config.to_ptr); - code_builder.get_local(config.from_ptr); - code_builder.i32_load(alignment, i + config.from_offset); - code_builder.i32_store(alignment, i + config.to_offset); - i += 4; - } - while config.size - i > 0 { - code_builder.get_local(config.to_ptr); - code_builder.get_local(config.from_ptr); - code_builder.i32_load8_u(alignment, i + config.from_offset); - code_builder.i32_store8(alignment, i + config.to_offset); - i += 1; - } -} - -/// Round up to alignment_bytes (which must be a power of 2) -#[macro_export] -macro_rules! round_up_to_alignment { - ($unaligned: expr, $alignment_bytes: expr) => { - if $alignment_bytes <= 1 { - $unaligned - } else if $alignment_bytes.count_ones() != 1 { - internal_error!( - "Cannot align to {} bytes. Not a power of 2.", - $alignment_bytes - ); - } else { - let mut aligned = $unaligned; - aligned += $alignment_bytes - 1; // if lower bits are non-zero, push it over the next boundary - aligned &= !$alignment_bytes + 1; // mask with a flag that has upper bits 1, lower bits 0 - aligned - } - }; -} - -pub struct WasmDebugLogSettings { - proc_start_end: bool, - user_procs_ir: bool, - helper_procs_ir: bool, - let_stmt_ir: bool, - instructions: bool, - storage_map: bool, - pub keep_test_binary: bool, -} - -pub const DEBUG_LOG_SETTINGS: WasmDebugLogSettings = WasmDebugLogSettings { - proc_start_end: false && cfg!(debug_assertions), - user_procs_ir: false && cfg!(debug_assertions), - helper_procs_ir: false && cfg!(debug_assertions), - let_stmt_ir: false && cfg!(debug_assertions), - instructions: false && cfg!(debug_assertions), - storage_map: false && cfg!(debug_assertions), - keep_test_binary: false && cfg!(debug_assertions), -}; diff --git a/compiler/gen_wasm/src/low_level.rs b/compiler/gen_wasm/src/low_level.rs deleted file mode 100644 index baf2d8edac..0000000000 --- a/compiler/gen_wasm/src/low_level.rs +++ /dev/null @@ -1,1267 +0,0 @@ -use bumpalo::collections::Vec; -use roc_builtins::bitcode::{self, FloatWidth, IntWidth}; -use roc_error_macros::internal_error; -use roc_module::low_level::LowLevel; -use roc_module::symbol::Symbol; -use roc_mono::code_gen_help::HelperOp; -use roc_mono::ir::{HigherOrderLowLevel, PassedFunction, ProcLayout}; -use roc_mono::layout::{Builtin, Layout, UnionLayout}; -use roc_mono::low_level::HigherOrder; - -use crate::backend::{ProcLookupData, ProcSource, WasmBackend}; -use crate::layout::{CallConv, StackMemoryFormat, WasmLayout}; -use crate::storage::{StackMemoryLocation, StoredValue}; -use crate::wasm_module::{Align, ValueType}; -use crate::TARGET_INFO; - -/// Number types used for Wasm code gen -/// Unlike other enums, this contains no details about layout or storage. -/// Its purpose is to help simplify the arms of the main lowlevel `match` below. -/// -/// Note: Wasm I32 is used for Roc I8, I16, I32, U8, U16, and U32, since it's -/// the smallest integer supported in the Wasm instruction set. -/// We may choose different instructions for signed and unsigned integers, -/// but they share the same Wasm value type. -#[derive(Clone, Copy, Debug, PartialEq)] -enum CodeGenNumType { - I32, // Supported in Wasm instruction set - I64, // Supported in Wasm instruction set - F32, // Supported in Wasm instruction set - F64, // Supported in Wasm instruction set - I128, // Bytes in memory, needs Zig builtins - F128, // Bytes in memory, needs Zig builtins - Decimal, // Bytes in memory, needs Zig builtins -} - -impl CodeGenNumType { - pub fn for_symbol(backend: &WasmBackend<'_>, symbol: Symbol) -> Self { - Self::from(backend.storage.get(&symbol)) - } -} - -impl From> for CodeGenNumType { - fn from(layout: Layout) -> CodeGenNumType { - use CodeGenNumType::*; - - let not_num_error = - || internal_error!("Tried to perform a Num low-level operation on {:?}", layout); - match layout { - Layout::Builtin(builtin) => match builtin { - Builtin::Int(int_width) => match int_width { - IntWidth::U8 => I32, - IntWidth::U16 => I32, - IntWidth::U32 => I32, - IntWidth::U64 => I64, - IntWidth::U128 => I128, - IntWidth::I8 => I32, - IntWidth::I16 => I32, - IntWidth::I32 => I32, - IntWidth::I64 => I64, - IntWidth::I128 => I128, - }, - Builtin::Float(float_width) => match float_width { - FloatWidth::F32 => F32, - FloatWidth::F64 => F64, - FloatWidth::F128 => F128, - }, - Builtin::Decimal => Decimal, - _ => not_num_error(), - }, - _ => not_num_error(), - } - } -} - -impl From for CodeGenNumType { - fn from(value_type: ValueType) -> CodeGenNumType { - match value_type { - ValueType::I32 => CodeGenNumType::I32, - ValueType::I64 => CodeGenNumType::I64, - ValueType::F32 => CodeGenNumType::F32, - ValueType::F64 => CodeGenNumType::F64, - } - } -} - -impl From for CodeGenNumType { - fn from(format: StackMemoryFormat) -> CodeGenNumType { - match format { - StackMemoryFormat::Int128 => CodeGenNumType::I128, - StackMemoryFormat::Float128 => CodeGenNumType::F128, - StackMemoryFormat::Decimal => CodeGenNumType::Decimal, - StackMemoryFormat::DataStructure => { - internal_error!("Tried to perform a Num low-level operation on a data structure") - } - } - } -} - -impl From for CodeGenNumType { - fn from(wasm_layout: WasmLayout) -> CodeGenNumType { - match wasm_layout { - WasmLayout::Primitive(value_type, _) => CodeGenNumType::from(value_type), - WasmLayout::StackMemory { format, .. } => CodeGenNumType::from(format), - } - } -} - -impl From<&StoredValue> for CodeGenNumType { - fn from(stored: &StoredValue) -> CodeGenNumType { - use StoredValue::*; - match stored { - VirtualMachineStack { value_type, .. } => CodeGenNumType::from(*value_type), - Local { value_type, .. } => CodeGenNumType::from(*value_type), - StackMemory { format, .. } => CodeGenNumType::from(*format), - } - } -} - -fn integer_symbol_is_signed(backend: &WasmBackend<'_>, symbol: Symbol) -> bool { - return match backend.storage.symbol_layouts[&symbol] { - Layout::Builtin(Builtin::Int(int_width)) => int_width.is_signed(), - x => internal_error!("Expected integer, found {:?}", x), - }; -} - -pub struct LowLevelCall<'a> { - pub lowlevel: LowLevel, - pub arguments: &'a [Symbol], - pub ret_symbol: Symbol, - pub ret_layout: Layout<'a>, - pub ret_storage: StoredValue, -} - -impl<'a> LowLevelCall<'a> { - /// Load symbol values for a Zig call or numerical operation - /// For numerical ops, this just pushes the arguments to the Wasm VM's value stack - /// It implements the calling convention used by Zig for both numbers and structs - /// Result is the type signature of the call - fn load_args(&self, backend: &mut WasmBackend<'a>) -> (usize, bool, bool) { - backend.storage.load_symbols_for_call( - backend.env.arena, - &mut backend.code_builder, - self.arguments, - self.ret_symbol, - &WasmLayout::new(&self.ret_layout), - CallConv::Zig, - ) - } - - fn load_args_and_call_zig(&self, backend: &mut WasmBackend<'a>, name: &'a str) { - let (num_wasm_args, has_return_val, ret_zig_packed_struct) = self.load_args(backend); - backend.call_zig_builtin_after_loading_args(name, num_wasm_args, has_return_val); - - if ret_zig_packed_struct { - match self.ret_storage { - StoredValue::StackMemory { - size, - alignment_bytes, - .. - } => { - // The address of the return value was already loaded before the call - let align = Align::from(alignment_bytes); - if size > 4 { - backend.code_builder.i64_store(align, 0); - } else { - backend.code_builder.i32_store(align, 0); - } - } - _ => { - internal_error!("Zig packed struct should always be stored to StackMemory") - } - } - } - } - - /// Wrap an integer whose Wasm representation is i32 - /// This may seem like deliberately introducing an error! - /// But we want all targets to behave the same, and hash algos rely on wrapping. - /// Discussion: https://github.com/rtfeldman/roc/pull/2117#discussion_r760723063 - fn wrap_i32(&self, backend: &mut WasmBackend<'a>) { - let invalid = - || internal_error!("Expected integer <= 32 bits, found {:?}", self.ret_layout); - - let (shift, is_signed) = match self.ret_layout { - Layout::Builtin(Builtin::Int(int_width)) => match int_width { - IntWidth::U8 => (24, false), - IntWidth::U16 => (16, false), - IntWidth::I8 => (24, true), - IntWidth::I16 => (16, true), - IntWidth::I32 | IntWidth::U32 => return, - _ => invalid(), - }, - _ => invalid(), - }; - - backend.code_builder.i32_const(shift); - backend.code_builder.i32_shl(); - backend.code_builder.i32_const(shift); - if is_signed { - backend.code_builder.i32_shr_s(); - } else { - backend.code_builder.i32_shr_u(); - } - } - - /// Main entrypoint from WasmBackend - pub fn generate(&self, backend: &mut WasmBackend<'a>) { - use CodeGenNumType::*; - use LowLevel::*; - - let panic_ret_type = || { - internal_error!( - "Invalid return layout for {:?}: {:?}", - self.lowlevel, - self.ret_layout - ) - }; - - match self.lowlevel { - // Str - StrConcat => self.load_args_and_call_zig(backend, bitcode::STR_CONCAT), - StrJoinWith => self.load_args_and_call_zig(backend, bitcode::STR_JOIN_WITH), - StrIsEmpty => match backend.storage.get(&self.arguments[0]) { - StoredValue::StackMemory { location, .. } => { - let (local_id, offset) = - location.local_and_offset(backend.storage.stack_frame_pointer); - backend.code_builder.get_local(local_id); - backend.code_builder.i32_load8_u(Align::Bytes1, offset + 11); - backend.code_builder.i32_const(0x80); - backend.code_builder.i32_eq(); - } - _ => internal_error!("invalid storage for Str"), - }, - StrStartsWith => self.load_args_and_call_zig(backend, bitcode::STR_STARTS_WITH), - StrStartsWithCodePt => { - self.load_args_and_call_zig(backend, bitcode::STR_STARTS_WITH_CODE_PT) - } - StrEndsWith => self.load_args_and_call_zig(backend, bitcode::STR_ENDS_WITH), - StrSplit => { - // LLVM implementation (build_str.rs) does the following - // 1. Call bitcode::STR_COUNT_SEGMENTS - // 2. Allocate a `List Str` - // 3. Call bitcode::STR_STR_SPLIT_IN_PLACE - // 4. Write the elements and length of the List - // To do this here, we need full access to WasmBackend, or we could make a Zig wrapper - todo!("{:?}", self.lowlevel); - } - StrCountGraphemes => { - self.load_args_and_call_zig(backend, bitcode::STR_COUNT_GRAPEHEME_CLUSTERS) - } - StrToNum => { - let number_layout = match self.ret_layout { - Layout::Struct { field_layouts, .. } => field_layouts[0], - _ => { - internal_error!("Unexpected mono layout {:?} for StrToNum", self.ret_layout) - } - }; - // match on the return layout to figure out which zig builtin we need - let intrinsic = match number_layout { - Layout::Builtin(Builtin::Int(int_width)) => &bitcode::STR_TO_INT[int_width], - Layout::Builtin(Builtin::Float(float_width)) => { - &bitcode::STR_TO_FLOAT[float_width] - } - Layout::Builtin(Builtin::Decimal) => bitcode::DEC_FROM_STR, - rest => internal_error!("Unexpected layout {:?} for StrToNum", rest), - }; - - self.load_args_and_call_zig(backend, intrinsic); - } - StrFromInt => { - // This does not get exposed in user space. We switched to NumToStr instead. - // We can probably just leave this as NotImplemented. We may want remove this LowLevel. - // see: https://github.com/rtfeldman/roc/pull/2108 - todo!("{:?}", self.lowlevel); - } - StrFromFloat => { - // linker errors for __ashlti3, __fixunsdfti, __multi3, __udivti3, __umodti3 - // https://gcc.gnu.org/onlinedocs/gccint/Integer-library-routines.html - // https://gcc.gnu.org/onlinedocs/gccint/Soft-float-library-routines.html - todo!("{:?}", self.lowlevel); - } - StrFromUtf8 => self.load_args_and_call_zig(backend, bitcode::STR_FROM_UTF8), - StrTrimLeft => self.load_args_and_call_zig(backend, bitcode::STR_TRIM_LEFT), - StrTrimRight => self.load_args_and_call_zig(backend, bitcode::STR_TRIM_RIGHT), - StrFromUtf8Range => self.load_args_and_call_zig(backend, bitcode::STR_FROM_UTF8_RANGE), - StrToUtf8 => self.load_args_and_call_zig(backend, bitcode::STR_TO_UTF8), - StrRepeat => self.load_args_and_call_zig(backend, bitcode::STR_REPEAT), - StrTrim => self.load_args_and_call_zig(backend, bitcode::STR_TRIM), - - // List - ListLen => match backend.storage.get(&self.arguments[0]) { - StoredValue::StackMemory { location, .. } => { - let (local_id, offset) = - location.local_and_offset(backend.storage.stack_frame_pointer); - backend.code_builder.get_local(local_id); - backend.code_builder.i32_load(Align::Bytes4, offset + 4); - } - _ => internal_error!("invalid storage for List"), - }, - - ListIsUnique => self.load_args_and_call_zig(backend, bitcode::LIST_IS_UNIQUE), - - ListMap | ListMap2 | ListMap3 | ListMap4 | ListMapWithIndex | ListKeepIf | ListWalk - | ListWalkUntil | ListWalkBackwards | ListKeepOks | ListKeepErrs | ListSortWith - | ListAny | ListAll | ListFindUnsafe | DictWalk => { - internal_error!("HigherOrder lowlevels should not be handled here") - } - - ListGetUnsafe | ListReplaceUnsafe | ListSingle | ListRepeat | ListReverse - | ListConcat | ListContains | ListAppend | ListPrepend | ListJoin | ListRange - | ListSublist | ListDropAt | ListSwap => { - todo!("{:?}", self.lowlevel); - } - - DictSize | DictEmpty | DictInsert | DictRemove | DictContains | DictGetUnsafe - | DictKeys | DictValues | DictUnion | DictIntersection | DictDifference - | SetFromList | SetToDict => { - todo!("{:?}", self.lowlevel); - } - - // Num - NumAdd => { - self.load_args(backend); - match CodeGenNumType::from(self.ret_layout) { - CodeGenNumType::I32 => backend.code_builder.i32_add(), - CodeGenNumType::I64 => backend.code_builder.i64_add(), - CodeGenNumType::F32 => backend.code_builder.f32_add(), - CodeGenNumType::F64 => backend.code_builder.f64_add(), - CodeGenNumType::I128 => todo!("{:?}", self.lowlevel), - CodeGenNumType::F128 => todo!("{:?}", self.lowlevel), - CodeGenNumType::Decimal => { - self.load_args_and_call_zig(backend, bitcode::DEC_ADD_WITH_OVERFLOW) - } - } - } - - NumAddWrap => { - self.load_args(backend); - match CodeGenNumType::from(self.ret_layout) { - I32 => { - backend.code_builder.i32_add(); - self.wrap_i32(backend); - } - I64 => backend.code_builder.i64_add(), - F32 => backend.code_builder.f32_add(), - F64 => backend.code_builder.f64_add(), - Decimal => self.load_args_and_call_zig(backend, bitcode::DEC_ADD_WITH_OVERFLOW), - x => todo!("{:?} for {:?}", self.lowlevel, x), - } - } - NumToStr => todo!("{:?}", self.lowlevel), - NumAddChecked => todo!("{:?}", self.lowlevel), - NumAddSaturated => todo!("{:?}", self.lowlevel), - NumSub => { - self.load_args(backend); - match CodeGenNumType::from(self.ret_layout) { - I32 => backend.code_builder.i32_sub(), - I64 => backend.code_builder.i64_sub(), - F32 => backend.code_builder.f32_sub(), - F64 => backend.code_builder.f64_sub(), - Decimal => self.load_args_and_call_zig(backend, bitcode::DEC_SUB_WITH_OVERFLOW), - x => todo!("{:?} for {:?}", self.lowlevel, x), - } - } - NumSubWrap => { - self.load_args(backend); - match CodeGenNumType::from(self.ret_layout) { - I32 => { - backend.code_builder.i32_sub(); - self.wrap_i32(backend); - } - I64 => backend.code_builder.i64_sub(), - F32 => backend.code_builder.f32_sub(), - F64 => backend.code_builder.f64_sub(), - Decimal => self.load_args_and_call_zig(backend, bitcode::DEC_SUB_WITH_OVERFLOW), - x => todo!("{:?} for {:?}", self.lowlevel, x), - } - } - NumSubChecked => todo!("{:?}", self.lowlevel), - NumSubSaturated => todo!("{:?}", self.lowlevel), - NumMul => { - self.load_args(backend); - match CodeGenNumType::from(self.ret_layout) { - I32 => backend.code_builder.i32_mul(), - I64 => backend.code_builder.i64_mul(), - F32 => backend.code_builder.f32_mul(), - F64 => backend.code_builder.f64_mul(), - Decimal => self.load_args_and_call_zig(backend, bitcode::DEC_MUL_WITH_OVERFLOW), - x => todo!("{:?} for {:?}", self.lowlevel, x), - } - } - NumMulWrap => { - self.load_args(backend); - match CodeGenNumType::from(self.ret_layout) { - I32 => { - backend.code_builder.i32_mul(); - self.wrap_i32(backend); - } - I64 => backend.code_builder.i64_mul(), - F32 => backend.code_builder.f32_mul(), - F64 => backend.code_builder.f64_mul(), - Decimal => self.load_args_and_call_zig(backend, bitcode::DEC_MUL_WITH_OVERFLOW), - x => todo!("{:?} for {:?}", self.lowlevel, x), - } - } - NumMulChecked => todo!("{:?}", self.lowlevel), - NumGt => { - self.load_args(backend); - match CodeGenNumType::for_symbol(backend, self.arguments[0]) { - I32 => { - if integer_symbol_is_signed(backend, self.arguments[0]) { - backend.code_builder.i32_gt_s() - } else { - backend.code_builder.i32_gt_u() - } - } - I64 => { - if integer_symbol_is_signed(backend, self.arguments[0]) { - backend.code_builder.i64_gt_s() - } else { - backend.code_builder.i64_gt_u() - } - } - F32 => backend.code_builder.f32_gt(), - F64 => backend.code_builder.f64_gt(), - x => todo!("{:?} for {:?}", self.lowlevel, x), - } - } - NumGte => { - self.load_args(backend); - match CodeGenNumType::for_symbol(backend, self.arguments[0]) { - I32 => { - if integer_symbol_is_signed(backend, self.arguments[0]) { - backend.code_builder.i32_ge_s() - } else { - backend.code_builder.i32_ge_u() - } - } - I64 => { - if integer_symbol_is_signed(backend, self.arguments[0]) { - backend.code_builder.i64_ge_s() - } else { - backend.code_builder.i64_ge_u() - } - } - F32 => backend.code_builder.f32_ge(), - F64 => backend.code_builder.f64_ge(), - x => todo!("{:?} for {:?}", self.lowlevel, x), - } - } - NumLt => { - self.load_args(backend); - match CodeGenNumType::for_symbol(backend, self.arguments[0]) { - I32 => { - if integer_symbol_is_signed(backend, self.arguments[0]) { - backend.code_builder.i32_lt_s() - } else { - backend.code_builder.i32_lt_u() - } - } - I64 => { - if integer_symbol_is_signed(backend, self.arguments[0]) { - backend.code_builder.i64_lt_s() - } else { - backend.code_builder.i64_lt_u() - } - } - F32 => backend.code_builder.f32_lt(), - F64 => backend.code_builder.f64_lt(), - x => todo!("{:?} for {:?}", self.lowlevel, x), - } - } - NumLte => { - self.load_args(backend); - match CodeGenNumType::for_symbol(backend, self.arguments[0]) { - I32 => { - if integer_symbol_is_signed(backend, self.arguments[0]) { - backend.code_builder.i32_le_s() - } else { - backend.code_builder.i32_le_u() - } - } - I64 => { - if integer_symbol_is_signed(backend, self.arguments[0]) { - backend.code_builder.i64_le_s() - } else { - backend.code_builder.i64_le_u() - } - } - F32 => backend.code_builder.f32_le(), - F64 => backend.code_builder.f64_le(), - x => todo!("{:?} for {:?}", self.lowlevel, x), - } - } - NumCompare => todo!("{:?}", self.lowlevel), - NumDivUnchecked => { - self.load_args(backend); - match CodeGenNumType::for_symbol(backend, self.arguments[0]) { - I32 => backend.code_builder.i32_div_s(), - I64 => backend.code_builder.i64_div_s(), - F32 => backend.code_builder.f32_div(), - F64 => backend.code_builder.f64_div(), - Decimal => self.load_args_and_call_zig(backend, bitcode::DEC_DIV), - x => todo!("{:?} for {:?}", self.lowlevel, x), - } - } - NumDivCeilUnchecked => todo!("{:?}", self.lowlevel), - NumRemUnchecked => { - self.load_args(backend); - match CodeGenNumType::for_symbol(backend, self.arguments[0]) { - I32 => backend.code_builder.i32_rem_s(), - I64 => backend.code_builder.i64_rem_s(), - _ => todo!("{:?} for {:?}", self.lowlevel, self.ret_layout), - } - } - NumIsMultipleOf => todo!("{:?}", self.lowlevel), - NumAbs => { - self.load_args(backend); - match CodeGenNumType::for_symbol(backend, self.arguments[0]) { - I32 => { - let code_builder = &mut backend.code_builder; - let arg_storage = backend.storage.get(&self.arguments[0]).to_owned(); - backend.storage.ensure_value_has_local( - code_builder, - self.arguments[0], - arg_storage, - ); - backend.storage.load_symbols(code_builder, self.arguments); - code_builder.i32_const(0); - backend.storage.load_symbols(code_builder, self.arguments); - code_builder.i32_sub(); - backend.storage.load_symbols(code_builder, self.arguments); - code_builder.i32_const(0); - code_builder.i32_ge_s(); - code_builder.select(); - } - I64 => { - let code_builder = &mut backend.code_builder; - let arg_storage = backend.storage.get(&self.arguments[0]).to_owned(); - backend.storage.ensure_value_has_local( - code_builder, - self.arguments[0], - arg_storage, - ); - backend.storage.load_symbols(code_builder, self.arguments); - code_builder.i64_const(0); - backend.storage.load_symbols(code_builder, self.arguments); - code_builder.i64_sub(); - backend.storage.load_symbols(code_builder, self.arguments); - code_builder.i64_const(0); - code_builder.i64_ge_s(); - code_builder.select(); - } - F32 => backend.code_builder.f32_abs(), - F64 => backend.code_builder.f64_abs(), - _ => todo!("{:?} for {:?}", self.lowlevel, self.ret_layout), - } - } - NumNeg => { - self.load_args(backend); - match CodeGenNumType::from(self.ret_layout) { - I32 => { - backend.code_builder.i32_const(0); - backend - .storage - .load_symbols(&mut backend.code_builder, self.arguments); - backend.code_builder.i32_sub(); - } - I64 => { - backend.code_builder.i64_const(0); - backend - .storage - .load_symbols(&mut backend.code_builder, self.arguments); - backend.code_builder.i64_sub(); - } - F32 => backend.code_builder.f32_neg(), - F64 => backend.code_builder.f64_neg(), - _ => todo!("{:?} for {:?}", self.lowlevel, self.ret_layout), - } - } - NumSin => todo!("{:?}", self.lowlevel), - NumCos => todo!("{:?}", self.lowlevel), - NumSqrtUnchecked => todo!("{:?}", self.lowlevel), - NumLogUnchecked => todo!("{:?}", self.lowlevel), - NumToFrac => { - self.load_args(backend); - let ret_type = CodeGenNumType::from(self.ret_layout); - let arg_type = CodeGenNumType::for_symbol(backend, self.arguments[0]); - match (ret_type, arg_type) { - (F32, I32) => backend.code_builder.f32_convert_s_i32(), - (F32, I64) => backend.code_builder.f32_convert_s_i64(), - (F32, F32) => {} - (F32, F64) => backend.code_builder.f32_demote_f64(), - - (F64, I32) => backend.code_builder.f64_convert_s_i32(), - (F64, I64) => backend.code_builder.f64_convert_s_i64(), - (F64, F32) => backend.code_builder.f64_promote_f32(), - (F64, F64) => {} - - _ => todo!("{:?}: {:?} -> {:?}", self.lowlevel, arg_type, ret_type), - } - } - NumPow => todo!("{:?}", self.lowlevel), - NumRound => { - self.load_args(backend); - let arg_type = CodeGenNumType::for_symbol(backend, self.arguments[0]); - let ret_type = CodeGenNumType::from(self.ret_layout); - - let width = match ret_type { - CodeGenNumType::I32 => IntWidth::I32, - CodeGenNumType::I64 => IntWidth::I64, - CodeGenNumType::I128 => todo!("{:?} for I128", self.lowlevel), - _ => internal_error!("Invalid return type for round: {:?}", ret_type), - }; - - match arg_type { - F32 => self.load_args_and_call_zig(backend, &bitcode::NUM_ROUND_F32[width]), - F64 => self.load_args_and_call_zig(backend, &bitcode::NUM_ROUND_F64[width]), - _ => internal_error!("Invalid argument type for round: {:?}", arg_type), - } - } - NumCeiling | NumFloor => { - self.load_args(backend); - let arg_type = CodeGenNumType::for_symbol(backend, self.arguments[0]); - let ret_type = CodeGenNumType::from(self.ret_layout); - match (arg_type, self.lowlevel) { - (F32, NumCeiling) => { - backend.code_builder.f32_ceil(); - } - (F64, NumCeiling) => { - backend.code_builder.f64_ceil(); - } - (F32, NumFloor) => { - backend.code_builder.f32_floor(); - } - (F64, NumFloor) => { - backend.code_builder.f64_floor(); - } - _ => internal_error!("Invalid argument type for ceiling: {:?}", arg_type), - } - match (ret_type, arg_type) { - // TODO: unsigned truncation - (I32, F32) => backend.code_builder.i32_trunc_s_f32(), - (I32, F64) => backend.code_builder.i32_trunc_s_f64(), - (I64, F32) => backend.code_builder.i64_trunc_s_f32(), - (I64, F64) => backend.code_builder.i64_trunc_s_f64(), - (I128, _) => todo!("{:?} for I128", self.lowlevel), - _ => panic_ret_type(), - } - } - NumPowInt => { - self.load_args(backend); - let base_type = CodeGenNumType::for_symbol(backend, self.arguments[0]); - let exponent_type = CodeGenNumType::for_symbol(backend, self.arguments[1]); - let ret_type = CodeGenNumType::from(self.ret_layout); - - debug_assert!(base_type == exponent_type); - debug_assert!(exponent_type == ret_type); - - let width = match ret_type { - CodeGenNumType::I32 => IntWidth::I32, - CodeGenNumType::I64 => IntWidth::I64, - CodeGenNumType::I128 => todo!("{:?} for I128", self.lowlevel), - _ => internal_error!("Invalid return type for pow: {:?}", ret_type), - }; - - self.load_args_and_call_zig(backend, &bitcode::NUM_POW_INT[width]) - } - - NumIsFinite => num_is_finite(backend, self.arguments[0]), - - NumAtan => match self.ret_layout { - Layout::Builtin(Builtin::Float(width)) => { - self.load_args_and_call_zig(backend, &bitcode::NUM_ATAN[width]); - } - _ => panic_ret_type(), - }, - NumAcos => match self.ret_layout { - Layout::Builtin(Builtin::Float(width)) => { - self.load_args_and_call_zig(backend, &bitcode::NUM_ACOS[width]); - } - _ => panic_ret_type(), - }, - NumAsin => match self.ret_layout { - Layout::Builtin(Builtin::Float(width)) => { - self.load_args_and_call_zig(backend, &bitcode::NUM_ASIN[width]); - } - _ => panic_ret_type(), - }, - NumBytesToU16 => todo!("{:?}", self.lowlevel), - NumBytesToU32 => todo!("{:?}", self.lowlevel), - NumBitwiseAnd => { - self.load_args(backend); - match CodeGenNumType::from(self.ret_layout) { - I32 => backend.code_builder.i32_and(), - I64 => backend.code_builder.i64_and(), - I128 => todo!("{:?} for I128", self.lowlevel), - _ => panic_ret_type(), - } - } - NumBitwiseXor => { - self.load_args(backend); - match CodeGenNumType::from(self.ret_layout) { - I32 => backend.code_builder.i32_xor(), - I64 => backend.code_builder.i64_xor(), - I128 => todo!("{:?} for I128", self.lowlevel), - _ => panic_ret_type(), - } - } - NumBitwiseOr => { - self.load_args(backend); - match CodeGenNumType::from(self.ret_layout) { - I32 => backend.code_builder.i32_or(), - I64 => backend.code_builder.i64_or(), - I128 => todo!("{:?} for I128", self.lowlevel), - _ => panic_ret_type(), - } - } - NumShiftLeftBy => { - // Swap order of arguments - backend.storage.load_symbols( - &mut backend.code_builder, - &[self.arguments[1], self.arguments[0]], - ); - match CodeGenNumType::from(self.ret_layout) { - I32 => backend.code_builder.i32_shl(), - I64 => backend.code_builder.i64_shl(), - I128 => todo!("{:?} for I128", self.lowlevel), - _ => panic_ret_type(), - } - } - NumShiftRightBy => { - backend.storage.load_symbols( - &mut backend.code_builder, - &[self.arguments[1], self.arguments[0]], - ); - match CodeGenNumType::from(self.ret_layout) { - I32 => backend.code_builder.i32_shr_s(), - I64 => backend.code_builder.i64_shr_s(), - I128 => todo!("{:?} for I128", self.lowlevel), - _ => panic_ret_type(), - } - } - NumShiftRightZfBy => { - backend.storage.load_symbols( - &mut backend.code_builder, - &[self.arguments[1], self.arguments[0]], - ); - match CodeGenNumType::from(self.ret_layout) { - I32 => backend.code_builder.i32_shr_u(), - I64 => backend.code_builder.i64_shr_u(), - I128 => todo!("{:?} for I128", self.lowlevel), - _ => panic_ret_type(), - } - } - NumIntCast => { - self.load_args(backend); - let ret_type = CodeGenNumType::from(self.ret_layout); - let arg_type = CodeGenNumType::for_symbol(backend, self.arguments[0]); - match (ret_type, arg_type) { - (I32, I32) => {} - (I32, I64) => backend.code_builder.i32_wrap_i64(), - (I32, F32) => backend.code_builder.i32_trunc_s_f32(), - (I32, F64) => backend.code_builder.i32_trunc_s_f64(), - - (I64, I32) => backend.code_builder.i64_extend_s_i32(), - (I64, I64) => {} - (I64, F32) => backend.code_builder.i64_trunc_s_f32(), - (I64, F64) => backend.code_builder.i64_trunc_s_f64(), - - (F32, I32) => backend.code_builder.f32_convert_s_i32(), - (F32, I64) => backend.code_builder.f32_convert_s_i64(), - (F32, F32) => {} - (F32, F64) => backend.code_builder.f32_demote_f64(), - - (F64, I32) => backend.code_builder.f64_convert_s_i32(), - (F64, I64) => backend.code_builder.f64_convert_s_i64(), - (F64, F32) => backend.code_builder.f64_promote_f32(), - (F64, F64) => {} - - _ => todo!("{:?}: {:?} -> {:?}", self.lowlevel, arg_type, ret_type), - } - } - NumToFloatCast => { - todo!("implement toF32 and toF64"); - } - NumToIntChecked => { - todo!() - } - NumToFloatChecked => { - todo!("implement toF32Checked and toF64Checked"); - } - And => { - self.load_args(backend); - backend.code_builder.i32_and(); - } - Or => { - self.load_args(backend); - backend.code_builder.i32_or(); - } - Not => { - self.load_args(backend); - backend.code_builder.i32_eqz(); - } - RefCountInc => self.load_args_and_call_zig(backend, bitcode::UTILS_INCREF), - RefCountDec => self.load_args_and_call_zig(backend, bitcode::UTILS_DECREF), - - PtrCast => { - let code_builder = &mut backend.code_builder; - backend.storage.load_symbols(code_builder, self.arguments); - } - - Hash => todo!("{:?}", self.lowlevel), - - Eq | NotEq => self.eq_or_neq(backend), - - BoxExpr | UnboxExpr => { - unreachable!("The {:?} operation is turned into mono Expr", self.lowlevel) - } - } - } - - /// Equality and inequality - /// These can operate on any data type (except functions) so they're more complex than other operators. - fn eq_or_neq(&self, backend: &mut WasmBackend<'a>) { - let arg_layout = backend.storage.symbol_layouts[&self.arguments[0]]; - let other_arg_layout = backend.storage.symbol_layouts[&self.arguments[1]]; - debug_assert!( - arg_layout == other_arg_layout, - "Cannot do `==` comparison on different types" - ); - - let invert_result = matches!(self.lowlevel, LowLevel::NotEq); - - match arg_layout { - Layout::Builtin( - Builtin::Int(_) | Builtin::Float(_) | Builtin::Bool | Builtin::Decimal, - ) => self.eq_or_neq_number(backend), - - Layout::Builtin(Builtin::Str) => { - self.load_args_and_call_zig(backend, bitcode::STR_EQUAL); - if invert_result { - backend.code_builder.i32_eqz(); - } - } - - // Empty record is always equal to empty record. - // There are no runtime arguments to check, so just emit true or false. - Layout::Struct { field_layouts, .. } if field_layouts.is_empty() => { - backend.code_builder.i32_const(!invert_result as i32); - } - - // Void is always equal to void. This is the type for the contents of the empty list in `[] == []` - // This instruction will never execute, but we need an i32 for module validation - Layout::Union(UnionLayout::NonRecursive(tags)) if tags.is_empty() => { - backend.code_builder.i32_const(!invert_result as i32); - } - - Layout::Builtin(Builtin::Dict(_, _) | Builtin::Set(_) | Builtin::List(_)) - | Layout::Struct { .. } - | Layout::Union(_) - | Layout::LambdaSet(_) => { - // Don't want Zig calling convention here, we're calling internal Roc functions - backend - .storage - .load_symbols(&mut backend.code_builder, self.arguments); - - backend.call_eq_specialized( - self.arguments, - &arg_layout, - self.ret_symbol, - &self.ret_storage, - ); - - if invert_result { - backend.code_builder.i32_eqz(); - } - } - - Layout::Boxed(_) => todo!(), - - Layout::RecursivePointer => { - internal_error!( - "Tried to apply `==` to RecursivePointer values {:?}", - self.arguments, - ) - } - } - } - - fn eq_or_neq_number(&self, backend: &mut WasmBackend<'a>) { - use StoredValue::*; - - match backend.storage.get(&self.arguments[0]).to_owned() { - VirtualMachineStack { value_type, .. } | Local { value_type, .. } => { - self.load_args(backend); - match self.lowlevel { - LowLevel::Eq => match value_type { - ValueType::I32 => backend.code_builder.i32_eq(), - ValueType::I64 => backend.code_builder.i64_eq(), - ValueType::F32 => backend.code_builder.f32_eq(), - ValueType::F64 => backend.code_builder.f64_eq(), - }, - LowLevel::NotEq => match value_type { - ValueType::I32 => backend.code_builder.i32_ne(), - ValueType::I64 => backend.code_builder.i64_ne(), - ValueType::F32 => backend.code_builder.f32_ne(), - ValueType::F64 => backend.code_builder.f64_ne(), - }, - _ => internal_error!("{:?} ended up in Equality code", self.lowlevel), - } - } - StackMemory { - format, - location: location0, - .. - } => { - if let StackMemory { - location: location1, - .. - } = backend.storage.get(&self.arguments[1]).to_owned() - { - self.eq_num128(backend, format, [location0, location1]); - if matches!(self.lowlevel, LowLevel::NotEq) { - backend.code_builder.i32_eqz(); - } - } - } - } - } - - /// Equality for 12-bit numbers. Checks if they're finite and contain the same bytes - /// Takes care of loading the arguments - fn eq_num128( - &self, - backend: &mut WasmBackend<'a>, - format: StackMemoryFormat, - locations: [StackMemoryLocation; 2], - ) { - match format { - StackMemoryFormat::Decimal => { - // Both args are finite - num_is_finite(backend, self.arguments[0]); - num_is_finite(backend, self.arguments[1]); - backend.code_builder.i32_and(); - - // AND they have the same bytes - Self::eq_num128_bytes(backend, locations); - backend.code_builder.i32_and(); - } - - StackMemoryFormat::Int128 => Self::eq_num128_bytes(backend, locations), - - StackMemoryFormat::Float128 => todo!("equality for f128"), - - StackMemoryFormat::DataStructure => { - internal_error!("Data structure equality is handled elsewhere") - } - } - } - - /// Check that two 128-bit numbers contain the same bytes - /// Loads *half* an argument at a time - /// (Don't call "load arguments" or "load symbols" helpers before this, it'll just waste instructions) - fn eq_num128_bytes(backend: &mut WasmBackend<'a>, locations: [StackMemoryLocation; 2]) { - let (local0, offset0) = locations[0].local_and_offset(backend.storage.stack_frame_pointer); - let (local1, offset1) = locations[1].local_and_offset(backend.storage.stack_frame_pointer); - - // Load & compare the first half of each argument - backend.code_builder.get_local(local0); - backend.code_builder.i64_load(Align::Bytes8, offset0); - backend.code_builder.get_local(local1); - backend.code_builder.i64_load(Align::Bytes8, offset1); - backend.code_builder.i64_eq(); - - // Load & compare the second half of each argument - backend.code_builder.get_local(local0); - backend.code_builder.i64_load(Align::Bytes8, offset0 + 8); - backend.code_builder.get_local(local1); - backend.code_builder.i64_load(Align::Bytes8, offset1 + 8); - backend.code_builder.i64_eq(); - - // First half matches AND second half matches - backend.code_builder.i32_and(); - } -} - -/// Helper for NumIsFinite op, and also part of Eq/NotEq -fn num_is_finite(backend: &mut WasmBackend<'_>, argument: Symbol) { - use StoredValue::*; - let stored = backend.storage.get(&argument).to_owned(); - match stored { - VirtualMachineStack { value_type, .. } | Local { value_type, .. } => { - backend - .storage - .load_symbols(&mut backend.code_builder, &[argument]); - match value_type { - // Integers are always finite. Just return True. - ValueType::I32 | ValueType::I64 => backend.code_builder.i32_const(1), - ValueType::F32 => { - backend.code_builder.i32_reinterpret_f32(); - backend.code_builder.i32_const(0x7f80_0000); - backend.code_builder.i32_and(); - backend.code_builder.i32_const(0x7f80_0000); - backend.code_builder.i32_ne(); - } - ValueType::F64 => { - backend.code_builder.i64_reinterpret_f64(); - backend.code_builder.i64_const(0x7ff0_0000_0000_0000); - backend.code_builder.i64_and(); - backend.code_builder.i64_const(0x7ff0_0000_0000_0000); - backend.code_builder.i64_ne(); - } - } - } - StackMemory { - format, location, .. - } => { - let (local_id, offset) = location.local_and_offset(backend.storage.stack_frame_pointer); - - match format { - // Integers and fixed-point numbers are always finite. Just return True. - StackMemoryFormat::Int128 | StackMemoryFormat::Decimal => { - backend.code_builder.i32_const(1) - } - - // f128 is not supported anywhere else but it's easy to support it here, so why not... - StackMemoryFormat::Float128 => { - backend.code_builder.get_local(local_id); - backend.code_builder.i64_load(Align::Bytes4, offset + 8); - backend.code_builder.i64_const(0x7fff_0000_0000_0000); - backend.code_builder.i64_and(); - backend.code_builder.i64_const(0x7fff_0000_0000_0000); - backend.code_builder.i64_ne(); - } - - StackMemoryFormat::DataStructure => { - internal_error!("Tried to perform NumIsFinite on a data structure") - } - } - } - } -} - -pub fn call_higher_order_lowlevel<'a>( - backend: &mut WasmBackend<'a>, - return_sym: Symbol, - return_layout: &Layout<'a>, - higher_order: &'a HigherOrderLowLevel<'a>, -) { - use HigherOrder::*; - - let HigherOrderLowLevel { - op, - passed_function, - .. - } = higher_order; - - let PassedFunction { - name: fn_name, - argument_layouts, - return_layout: result_layout, - owns_captured_environment, - captured_environment, - .. - } = passed_function; - - let closure_data_layout = match backend.storage.symbol_layouts[captured_environment] { - Layout::LambdaSet(lambda_set) => lambda_set.runtime_representation(), - Layout::Struct { - field_layouts: &[], .. - } => Layout::UNIT, - x => internal_error!("Closure data has an invalid layout\n{:?}", x), - }; - let closure_data_exists: bool = closure_data_layout != Layout::UNIT; - - // We create a wrapper around the passed function, which just unboxes the arguments. - // This allows Zig builtins to have a generic pointer-based interface. - let source = { - let passed_proc_layout = ProcLayout { - arguments: argument_layouts, - result: *result_layout, - }; - let passed_proc_index = backend - .proc_lookup - .iter() - .position(|ProcLookupData { name, layout, .. }| { - name == fn_name && layout == &passed_proc_layout - }) - .unwrap(); - ProcSource::HigherOrderWrapper(passed_proc_index) - }; - let wrapper_sym = backend.create_symbol(&format!("#wrap#{:?}", fn_name)); - let wrapper_layout = { - let mut wrapper_arg_layouts: Vec> = - Vec::with_capacity_in(argument_layouts.len() + 1, backend.env.arena); - - let n_non_closure_args = if closure_data_exists { - argument_layouts.len() - 1 - } else { - argument_layouts.len() - }; - - wrapper_arg_layouts.push(closure_data_layout); - wrapper_arg_layouts.extend( - argument_layouts - .iter() - .take(n_non_closure_args) - .map(Layout::Boxed), - ); - wrapper_arg_layouts.push(Layout::Boxed(result_layout)); - - ProcLayout { - arguments: wrapper_arg_layouts.into_bump_slice(), - result: Layout::UNIT, - } - }; - - let wrapper_fn_idx = backend.register_helper_proc(wrapper_sym, wrapper_layout, source); - let wrapper_fn_ptr = backend.get_fn_table_index(wrapper_fn_idx); - let inc_fn_ptr = match closure_data_layout { - Layout::Struct { - field_layouts: &[], .. - } => { - // Our code gen would ignore the Unit arg, but the Zig builtin passes a pointer for it! - // That results in an exception (type signature mismatch in indirect call). - // The workaround is to use I32 layout, treating the (ignored) pointer as an integer. - backend.get_refcount_fn_ptr(Layout::Builtin(Builtin::Int(IntWidth::I32)), HelperOp::Inc) - } - _ => backend.get_refcount_fn_ptr(closure_data_layout, HelperOp::Inc), - }; - - match op { - ListMap { xs } => list_map_n( - bitcode::LIST_MAP, - backend, - &[*xs], - return_sym, - *return_layout, - wrapper_fn_ptr, - inc_fn_ptr, - closure_data_exists, - *captured_environment, - *owns_captured_environment, - ), - - ListMap2 { xs, ys } => list_map_n( - bitcode::LIST_MAP2, - backend, - &[*xs, *ys], - return_sym, - *return_layout, - wrapper_fn_ptr, - inc_fn_ptr, - closure_data_exists, - *captured_environment, - *owns_captured_environment, - ), - - ListMap3 { xs, ys, zs } => list_map_n( - bitcode::LIST_MAP3, - backend, - &[*xs, *ys, *zs], - return_sym, - *return_layout, - wrapper_fn_ptr, - inc_fn_ptr, - closure_data_exists, - *captured_environment, - *owns_captured_environment, - ), - - ListMap4 { xs, ys, zs, ws } => list_map_n( - bitcode::LIST_MAP4, - backend, - &[*xs, *ys, *zs, *ws], - return_sym, - *return_layout, - wrapper_fn_ptr, - inc_fn_ptr, - closure_data_exists, - *captured_environment, - *owns_captured_environment, - ), - - ListMapWithIndex { .. } - | ListKeepIf { .. } - | ListWalk { .. } - | ListWalkUntil { .. } - | ListWalkBackwards { .. } - | ListKeepOks { .. } - | ListKeepErrs { .. } - | ListSortWith { .. } - | ListAny { .. } - | ListAll { .. } - | ListFindUnsafe { .. } - | DictWalk { .. } => todo!("{:?}", op), - } -} - -fn unwrap_list_elem_layout(list_layout: Layout<'_>) -> &Layout<'_> { - match list_layout { - Layout::Builtin(Builtin::List(x)) => x, - e => internal_error!("expected List layout, got {:?}", e), - } -} - -#[allow(clippy::too_many_arguments)] -fn list_map_n<'a>( - zig_fn_name: &'static str, - backend: &mut WasmBackend<'a>, - arg_symbols: &[Symbol], - return_sym: Symbol, - return_layout: Layout<'a>, - wrapper_fn_ptr: i32, - inc_fn_ptr: i32, - closure_data_exists: bool, - captured_environment: Symbol, - owns_captured_environment: bool, -) { - let arg_elem_layouts = Vec::from_iter_in( - arg_symbols - .iter() - .map(|sym| *unwrap_list_elem_layout(backend.storage.symbol_layouts[sym])), - backend.env.arena, - ); - - let elem_ret = unwrap_list_elem_layout(return_layout); - let (elem_ret_size, elem_ret_align) = elem_ret.stack_size_and_alignment(TARGET_INFO); - - let cb = &mut backend.code_builder; - - backend.storage.load_symbols(cb, &[return_sym]); - - for s in arg_symbols { - backend.storage.load_symbol_zig(cb, *s); - } - cb.i32_const(wrapper_fn_ptr); - if closure_data_exists { - backend.storage.load_symbols(cb, &[captured_environment]); - } else { - // load_symbols assumes that a zero-size arg should be eliminated in code gen, - // but that's a specialization that our Zig code doesn't have! Pass a null pointer. - cb.i32_const(0); - } - cb.i32_const(inc_fn_ptr); - cb.i32_const(owns_captured_environment as i32); - cb.i32_const(elem_ret_align as i32); - for el in arg_elem_layouts.iter() { - cb.i32_const(el.stack_size(TARGET_INFO) as i32); - } - cb.i32_const(elem_ret_size as i32); - - // If we have lists of different lengths, we may need to decrement - let num_wasm_args = if arg_elem_layouts.len() > 1 { - for el in arg_elem_layouts.iter() { - let ptr = backend.get_refcount_fn_ptr(*el, HelperOp::Dec); - backend.code_builder.i32_const(ptr); - } - 7 + arg_elem_layouts.len() * 4 - } else { - 7 + arg_elem_layouts.len() * 3 - }; - - let has_return_val = false; - backend.call_zig_builtin_after_loading_args(zig_fn_name, num_wasm_args, has_return_val); -} diff --git a/compiler/gen_wasm/src/storage.rs b/compiler/gen_wasm/src/storage.rs deleted file mode 100644 index c41b4d44b8..0000000000 --- a/compiler/gen_wasm/src/storage.rs +++ /dev/null @@ -1,709 +0,0 @@ -use bumpalo::collections::Vec; -use bumpalo::Bump; - -use roc_collections::all::MutMap; -use roc_error_macros::internal_error; -use roc_module::symbol::Symbol; -use roc_mono::layout::Layout; - -use crate::layout::{CallConv, ReturnMethod, StackMemoryFormat, WasmLayout}; -use crate::wasm_module::{Align, CodeBuilder, LocalId, ValueType, VmSymbolState}; -use crate::{copy_memory, round_up_to_alignment, CopyMemoryConfig, PTR_TYPE}; - -pub enum StoredValueKind { - Parameter, - Variable, - ReturnValue, -} - -#[derive(Debug, Clone)] -pub enum StackMemoryLocation { - FrameOffset(u32), - PointerArg(LocalId), -} - -impl StackMemoryLocation { - pub fn local_and_offset(&self, stack_frame_pointer: Option) -> (LocalId, u32) { - match self { - Self::PointerArg(local_id) => (*local_id, 0), - Self::FrameOffset(offset) => (stack_frame_pointer.unwrap(), *offset), - } - } -} - -#[derive(Debug, Clone)] -pub enum StoredValue { - /// A value stored implicitly in the VM stack (primitives only) - VirtualMachineStack { - vm_state: VmSymbolState, - value_type: ValueType, - size: u32, - }, - - /// A local variable in the Wasm function (primitives only) - Local { - local_id: LocalId, - value_type: ValueType, - size: u32, - }, - - /// A Struct, or other non-primitive value, stored in stack memory - StackMemory { - location: StackMemoryLocation, - size: u32, - alignment_bytes: u32, - format: StackMemoryFormat, - }, -} - -impl StoredValue { - /// Value types to pass to Wasm functions - /// One Roc value can become 0, 1, or 2 Wasm arguments - pub fn arg_types(&self, conv: CallConv) -> &'static [ValueType] { - use ValueType::*; - match self { - // Simple numbers: 1 Roc argument => 1 Wasm argument - Self::VirtualMachineStack { value_type, .. } | Self::Local { value_type, .. } => { - match value_type { - I32 => &[I32], - I64 => &[I64], - F32 => &[F32], - F64 => &[F64], - } - } - // Stack memory values: 1 Roc argument => 0-2 Wasm arguments - Self::StackMemory { size, format, .. } => conv.stack_memory_arg_types(*size, *format), - } - } -} - -/// Helper structure for WasmBackend, to keep track of how values are stored, -/// including the VM stack, local variables, and linear memory -#[derive(Debug)] -pub struct Storage<'a> { - pub return_var: Option, - pub arg_types: Vec<'a, ValueType>, - pub local_types: Vec<'a, ValueType>, - pub symbol_layouts: MutMap>, - pub symbol_storage_map: MutMap, - pub stack_frame_pointer: Option, - pub stack_frame_size: i32, -} - -impl<'a> Storage<'a> { - pub fn new(arena: &'a Bump) -> Self { - Storage { - return_var: None, - arg_types: Vec::with_capacity_in(8, arena), - local_types: Vec::with_capacity_in(32, arena), - symbol_layouts: MutMap::default(), - symbol_storage_map: MutMap::default(), - stack_frame_pointer: None, - stack_frame_size: 0, - } - } - - pub fn clear(&mut self) { - self.return_var = None; - self.arg_types.clear(); - self.local_types.clear(); - self.symbol_layouts.clear(); - self.symbol_storage_map.clear(); - self.stack_frame_pointer = None; - self.stack_frame_size = 0; - } - - /// Internal use only. See `allocate` or `create_anonymous_local` - fn get_next_local_id(&self) -> LocalId { - LocalId((self.arg_types.len() + self.local_types.len()) as u32) - } - - pub fn create_anonymous_local(&mut self, value_type: ValueType) -> LocalId { - let id = self.get_next_local_id(); - self.local_types.push(value_type); - id - } - - /// Allocate storage for a Roc value - /// - /// Wasm primitives (i32, i64, f32, f64) are allocated "storage" on the VM stack. - /// This is really just a way to model how the stack machine works as a sort of - /// temporary storage. It doesn't result in any code generation. - /// For some values, this initial storage allocation may need to be upgraded later - /// to a Local. See `load_symbols`. - /// - /// Structs and Tags are stored in memory rather than in Wasm primitives. - /// They are allocated a certain offset and size in the stack frame. - pub fn allocate( - &mut self, - layout: Layout<'a>, - symbol: Symbol, - kind: StoredValueKind, - ) -> StoredValue { - let next_local_id = self.get_next_local_id(); - let wasm_layout = WasmLayout::new(&layout); - self.symbol_layouts.insert(symbol, layout); - - let storage = match wasm_layout { - WasmLayout::Primitive(value_type, size) => match kind { - StoredValueKind::Parameter => { - self.arg_types.push(value_type); - StoredValue::Local { - local_id: next_local_id, - value_type, - size, - } - } - _ => StoredValue::VirtualMachineStack { - vm_state: VmSymbolState::NotYetPushed, - value_type, - size, - }, - }, - - WasmLayout::StackMemory { - size, - alignment_bytes, - format, - } => { - let location = match kind { - StoredValueKind::Parameter => { - if size > 0 { - self.arg_types.push(PTR_TYPE); - StackMemoryLocation::PointerArg(next_local_id) - } else { - // An argument with zero size is purely conceptual, and will not exist in Wasm. - // However we need to track the symbol, so we treat it like a local variable. - StackMemoryLocation::FrameOffset(0) - } - } - - StoredValueKind::Variable => { - if self.stack_frame_pointer.is_none() && size > 0 { - self.stack_frame_pointer = Some(next_local_id); - self.local_types.push(PTR_TYPE); - } - - let offset = - round_up_to_alignment!(self.stack_frame_size, alignment_bytes as i32); - - self.stack_frame_size = offset + (size as i32); - - StackMemoryLocation::FrameOffset(offset as u32) - } - - StoredValueKind::ReturnValue => StackMemoryLocation::PointerArg(LocalId(0)), - }; - - StoredValue::StackMemory { - location, - size, - alignment_bytes, - format, - } - } - }; - - self.symbol_storage_map.insert(symbol, storage.clone()); - - storage - } - - /// Get storage info for a given symbol - pub fn get(&self, sym: &Symbol) -> &StoredValue { - self.symbol_storage_map.get(sym).unwrap_or_else(|| { - internal_error!( - "Symbol {:?} not found in function scope:\n{:?}", - sym, - self.symbol_storage_map - ) - }) - } - - /// Load a single symbol using the C Calling Convention - /// *Private* because external code should always load symbols in bulk (see load_symbols) - fn load_symbol_ccc(&mut self, code_builder: &mut CodeBuilder, sym: Symbol) { - let storage = self.get(&sym).to_owned(); - match storage { - StoredValue::VirtualMachineStack { - vm_state, - value_type, - size, - } => { - let next_local_id = self.get_next_local_id(); - let maybe_next_vm_state = code_builder.load_symbol(sym, vm_state, next_local_id); - match maybe_next_vm_state { - // The act of loading the value changed the VM state, so update it - Some(next_vm_state) => { - self.symbol_storage_map.insert( - sym, - StoredValue::VirtualMachineStack { - vm_state: next_vm_state, - value_type, - size, - }, - ); - } - None => { - // Loading the value required creating a new local, because - // it was not in a convenient position in the VM stack. - self.local_types.push(value_type); - self.symbol_storage_map.insert( - sym, - StoredValue::Local { - local_id: next_local_id, - value_type, - size, - }, - ); - } - } - } - - StoredValue::Local { local_id, .. } => { - code_builder.get_local(local_id); - code_builder.set_top_symbol(sym); - } - - StoredValue::StackMemory { - location, - format, - size, - .. - } => { - if size == 0 { - return; - } - - let (local_id, offset) = location.local_and_offset(self.stack_frame_pointer); - - code_builder.get_local(local_id); - - if format == StackMemoryFormat::DataStructure { - if offset != 0 { - code_builder.i32_const(offset as i32); - code_builder.i32_add(); - } - } else { - // It's one of the 128-bit numbers, all of which we load as two i64's - // (Mark the same Symbol twice. Shouldn't matter except for debugging.) - code_builder.i64_load(Align::Bytes8, offset); - code_builder.set_top_symbol(sym); - - code_builder.get_local(local_id); - code_builder.i64_load(Align::Bytes8, offset + 8); - } - - code_builder.set_top_symbol(sym); - } - } - } - - // TODO: expose something higher level instead, shared among higher-order calls - pub fn load_symbol_zig(&mut self, code_builder: &mut CodeBuilder, arg: Symbol) { - if let StoredValue::StackMemory { - location, - size, - alignment_bytes, - format: StackMemoryFormat::DataStructure, - } = self.get(&arg) - { - if *size == 0 { - // do nothing - } else if *size > 16 { - self.load_symbol_ccc(code_builder, arg); - } else { - let (local_id, offset) = location.local_and_offset(self.stack_frame_pointer); - code_builder.get_local(local_id); - let align = Align::from(*alignment_bytes); - - if *size == 1 { - code_builder.i32_load8_u(align, offset); - } else if *size == 2 { - code_builder.i32_load16_u(align, offset); - } else if *size <= 4 { - code_builder.i32_load(align, offset); - } else if *size <= 8 { - code_builder.i64_load(align, offset); - } else if *size <= 12 { - code_builder.i64_load(align, offset); - code_builder.get_local(local_id); - code_builder.i32_load(align, offset + 8); - } else { - code_builder.i64_load(align, offset); - code_builder.get_local(local_id); - code_builder.i64_load(align, offset + 8); - } - } - } else { - self.load_symbol_ccc(code_builder, arg); - } - } - - /// stack memory values are returned by pointer. e.g. a roc function - /// - /// add : I128, I128 -> I128 - /// - /// is given the wasm type - /// - /// add : (i32, i64, i64, i64, i64) -> nil - /// - /// The returned value is written to the address passed as the first argument - fn load_return_address_ccc(&mut self, code_builder: &mut CodeBuilder, sym: Symbol) { - let storage = self.get(&sym).to_owned(); - match storage { - StoredValue::VirtualMachineStack { .. } | StoredValue::Local { .. } => { - internal_error!("these storage types are not returned by writing to a pointer") - } - StoredValue::StackMemory { location, size, .. } => { - if size == 0 { - return; - } - - let (local_id, offset) = location.local_and_offset(self.stack_frame_pointer); - - code_builder.get_local(local_id); - if offset != 0 { - code_builder.i32_const(offset as i32); - code_builder.i32_add(); - } - code_builder.set_top_symbol(sym); - } - } - } - - /// Load symbols to the top of the VM stack - /// Avoid calling this method in a loop with one symbol at a time! It will work, - /// but it generates very inefficient Wasm code. - pub fn load_symbols(&mut self, code_builder: &mut CodeBuilder, symbols: &[Symbol]) { - if code_builder.verify_stack_match(symbols) { - // The symbols were already at the top of the stack, do nothing! - // This should be quite common due to the structure of the Mono IR - return; - } - for sym in symbols.iter() { - self.load_symbol_ccc(code_builder, *sym); - } - } - - /// Load symbols for a function call - pub fn load_symbols_for_call( - &mut self, - arena: &'a Bump, - code_builder: &mut CodeBuilder, - arguments: &[Symbol], - return_symbol: Symbol, - return_layout: &WasmLayout, - call_conv: CallConv, - ) -> (usize, bool, bool) { - use ReturnMethod::*; - - let mut num_wasm_args = 0; - let mut symbols_to_load = Vec::with_capacity_in(arguments.len() * 2 + 1, arena); - - let return_method = return_layout.return_method(call_conv); - let has_return_val = match return_method { - Primitive(..) => true, - NoReturnValue => false, - WriteToPointerArg => { - num_wasm_args += 1; - symbols_to_load.push(return_symbol); - false - } - ZigPackedStruct => { - // Workaround for Zig's incorrect implementation of the C calling convention. - // We need to copy the packed struct into the stack frame - // Load the address before the call so that afterward, it will be 2nd on the value stack, - // ready for the store instruction. - symbols_to_load.push(return_symbol); - true - } - }; - - for arg in arguments { - let stored = self.symbol_storage_map.get(arg).unwrap(); - let arg_types = stored.arg_types(call_conv); - num_wasm_args += arg_types.len(); - match arg_types.len() { - 0 => {} - 1 => symbols_to_load.push(*arg), - 2 => symbols_to_load.extend_from_slice(&[*arg, *arg]), - n => internal_error!("Cannot have {} Wasm arguments for 1 Roc argument", n), - } - } - - // If the symbols were already at the top of the stack, do nothing! - // Should be common for simple cases, due to the structure of the Mono IR - if !code_builder.verify_stack_match(&symbols_to_load) { - if matches!(return_method, WriteToPointerArg | ZigPackedStruct) { - self.load_return_address_ccc(code_builder, return_symbol); - }; - - for arg in arguments { - match call_conv { - CallConv::C => self.load_symbol_ccc(code_builder, *arg), - CallConv::Zig => self.load_symbol_zig(code_builder, *arg), - } - } - } - - ( - num_wasm_args, - has_return_val, - return_method == ZigPackedStruct, - ) - } - - /// Generate code to copy a StoredValue to an arbitrary memory location - /// (defined by a pointer and offset). - pub fn copy_value_to_memory( - &mut self, - code_builder: &mut CodeBuilder, - to_ptr: LocalId, - to_offset: u32, - from_symbol: Symbol, - ) -> u32 { - let from_storage = self.get(&from_symbol).to_owned(); - match from_storage { - StoredValue::StackMemory { - location, - size, - alignment_bytes, - .. - } => { - let (from_ptr, from_offset) = location.local_and_offset(self.stack_frame_pointer); - copy_memory( - code_builder, - CopyMemoryConfig { - from_ptr, - from_offset, - to_ptr, - to_offset, - size, - alignment_bytes, - }, - ); - size - } - - StoredValue::VirtualMachineStack { - value_type, size, .. - } - | StoredValue::Local { - value_type, size, .. - } => { - use crate::wasm_module::Align::*; - code_builder.get_local(to_ptr); - self.load_symbols(code_builder, &[from_symbol]); - match (value_type, size) { - (ValueType::I64, 8) => code_builder.i64_store(Bytes8, to_offset), - (ValueType::I32, 4) => code_builder.i32_store(Bytes4, to_offset), - (ValueType::I32, 2) => code_builder.i32_store16(Bytes2, to_offset), - (ValueType::I32, 1) => code_builder.i32_store8(Bytes1, to_offset), - (ValueType::F32, 4) => code_builder.f32_store(Bytes4, to_offset), - (ValueType::F64, 8) => code_builder.f64_store(Bytes8, to_offset), - _ => { - internal_error!( - "Cannot store {:?} with alignment of {:?}", - value_type, - size - ); - } - } - size - } - } - } - - /// Generate code to copy a StoredValue from an arbitrary memory location - /// (defined by a pointer and offset). - pub fn copy_value_from_memory( - &mut self, - code_builder: &mut CodeBuilder, - to_symbol: Symbol, - from_ptr: LocalId, - from_offset: u32, - ) -> u32 { - let to_storage = self.get(&to_symbol).to_owned(); - match to_storage { - StoredValue::StackMemory { - location, - size, - alignment_bytes, - .. - } => { - if self.stack_frame_pointer.is_none() { - self.stack_frame_pointer = Some(self.get_next_local_id()); - } - - let (to_ptr, to_offset) = location.local_and_offset(self.stack_frame_pointer); - copy_memory( - code_builder, - CopyMemoryConfig { - from_ptr, - from_offset, - to_ptr, - to_offset, - size, - alignment_bytes, - }, - ); - size - } - - StoredValue::VirtualMachineStack { - value_type, size, .. - } - | StoredValue::Local { - value_type, size, .. - } => { - use crate::wasm_module::Align::*; - - code_builder.get_local(from_ptr); - match (value_type, size) { - (ValueType::I64, 8) => code_builder.i64_load(Bytes8, from_offset), - (ValueType::I32, 4) => code_builder.i32_load(Bytes4, from_offset), - (ValueType::I32, 2) => code_builder.i32_load16_s(Bytes2, from_offset), - (ValueType::I32, 1) => code_builder.i32_load8_s(Bytes1, from_offset), - (ValueType::F32, 4) => code_builder.f32_load(Bytes4, from_offset), - (ValueType::F64, 8) => code_builder.f64_load(Bytes8, from_offset), - _ => { - internal_error!( - "Cannot store {:?} with alignment of {:?}", - value_type, - size - ); - } - }; - - if let StoredValue::Local { local_id, .. } = to_storage { - code_builder.set_local(local_id); - } - - size - } - } - } - - /// Generate code to copy from one StoredValue to another - /// Copies the _entire_ value. For struct fields etc., see `copy_value_to_memory` - pub fn clone_value( - &mut self, - code_builder: &mut CodeBuilder, - to: &StoredValue, - from: &StoredValue, - from_symbol: Symbol, - ) { - use StoredValue::*; - - match (to, from) { - ( - Local { - local_id: to_local_id, - value_type: to_value_type, - size: to_size, - }, - VirtualMachineStack { - value_type: from_value_type, - size: from_size, - .. - }, - ) => { - debug_assert!(to_value_type == from_value_type); - debug_assert!(to_size == from_size); - self.load_symbols(code_builder, &[from_symbol]); - code_builder.set_local(*to_local_id); - self.symbol_storage_map.insert(from_symbol, to.clone()); - } - - ( - Local { - local_id: to_local_id, - value_type: to_value_type, - size: to_size, - }, - Local { - local_id: from_local_id, - value_type: from_value_type, - size: from_size, - }, - ) => { - debug_assert!(to_value_type == from_value_type); - debug_assert!(to_size == from_size); - code_builder.get_local(*from_local_id); - code_builder.set_local(*to_local_id); - } - - ( - StackMemory { - location: to_location, - size: to_size, - alignment_bytes: to_alignment_bytes, - .. - }, - StackMemory { - location: from_location, - size: from_size, - alignment_bytes: from_alignment_bytes, - .. - }, - ) => { - let (from_ptr, from_offset) = - from_location.local_and_offset(self.stack_frame_pointer); - let (to_ptr, to_offset) = to_location.local_and_offset(self.stack_frame_pointer); - debug_assert!(*to_size == *from_size); - debug_assert!(*to_alignment_bytes == *from_alignment_bytes); - copy_memory( - code_builder, - CopyMemoryConfig { - from_ptr, - from_offset, - to_ptr, - to_offset, - size: *from_size, - alignment_bytes: *from_alignment_bytes, - }, - ); - } - - _ => { - internal_error!("Cannot copy storage from {:?} to {:?}", from, to); - } - } - } - - /// Ensure a StoredValue has an associated local (which could be the frame pointer!) - /// - /// This is useful when a value needs to be accessed from a more deeply-nested block. - /// In that case we want to make sure it's not just stored in the VM stack, because - /// blocks can't access the VM stack from outer blocks, but they can access locals. - /// (In the case of structs in stack memory, we just use the stack frame pointer local) - pub fn ensure_value_has_local( - &mut self, - code_builder: &mut CodeBuilder, - symbol: Symbol, - storage: StoredValue, - ) -> StoredValue { - if let StoredValue::VirtualMachineStack { - vm_state, - value_type, - size, - } = storage - { - let next_local_id = self.get_next_local_id(); - code_builder.store_symbol_to_local(symbol, vm_state, next_local_id); - - self.local_types.push(value_type); - let new_storage = StoredValue::Local { - local_id: next_local_id, - value_type, - size, - }; - - self.symbol_storage_map.insert(symbol, new_storage.clone()); - new_storage - } else { - storage - } - } -} diff --git a/compiler/gen_wasm/src/wasm32_result.rs b/compiler/gen_wasm/src/wasm32_result.rs deleted file mode 100644 index 601429761e..0000000000 --- a/compiler/gen_wasm/src/wasm32_result.rs +++ /dev/null @@ -1,250 +0,0 @@ -/* -Generate a wrapper function to expose a generic interface from a Wasm module for any result type. -The wrapper function ensures the value is written to memory and returns its address as i32. -The user needs to analyse the Wasm module's memory to decode the result. -*/ - -use bumpalo::{collections::Vec, Bump}; -use roc_builtins::bitcode::{FloatWidth, IntWidth}; -use roc_mono::layout::{Builtin, Layout, UnionLayout}; -use roc_target::TargetInfo; - -use crate::wasm32_sized::Wasm32Sized; -use crate::wasm_module::{ - linking::SymInfo, linking::WasmObjectSymbol, Align, CodeBuilder, Export, ExportType, LocalId, - Signature, ValueType, WasmModule, -}; -use roc_std::{RocDec, RocList, RocOrder, RocStr}; - -/// Type-driven wrapper generation -pub trait Wasm32Result { - fn insert_wrapper<'a>( - arena: &'a Bump, - module: &mut WasmModule<'a>, - wrapper_name: &'static str, - main_function_index: u32, - ) { - insert_wrapper_metadata(arena, module, wrapper_name); - let mut code_builder = CodeBuilder::new(arena); - Self::build_wrapper_body(&mut code_builder, main_function_index); - module.code.code_builders.push(code_builder); - } - - fn build_wrapper_body(code_builder: &mut CodeBuilder, main_function_index: u32); -} - -/// Layout-driven wrapper generation -pub fn insert_wrapper_for_layout<'a>( - arena: &'a Bump, - module: &mut WasmModule<'a>, - wrapper_name: &'static str, - main_fn_index: u32, - layout: &Layout<'a>, -) { - let mut stack_data_structure = || { - let size = layout.stack_size(TargetInfo::default_wasm32()); - if size == 0 { - <() as Wasm32Result>::insert_wrapper(arena, module, wrapper_name, main_fn_index); - } else { - insert_wrapper_metadata(arena, module, wrapper_name); - let mut code_builder = CodeBuilder::new(arena); - build_wrapper_body_stack_memory(&mut code_builder, main_fn_index, size as usize); - module.code.code_builders.push(code_builder); - } - }; - - match layout { - Layout::Builtin(Builtin::Int(IntWidth::U8 | IntWidth::I8)) => { - i8::insert_wrapper(arena, module, wrapper_name, main_fn_index); - } - Layout::Builtin(Builtin::Int(IntWidth::U16 | IntWidth::I16)) => { - i16::insert_wrapper(arena, module, wrapper_name, main_fn_index); - } - Layout::Builtin(Builtin::Int(IntWidth::U32 | IntWidth::I32)) => { - i32::insert_wrapper(arena, module, wrapper_name, main_fn_index); - } - Layout::Builtin(Builtin::Int(IntWidth::U64 | IntWidth::I64)) => { - i64::insert_wrapper(arena, module, wrapper_name, main_fn_index); - } - Layout::Builtin(Builtin::Float(FloatWidth::F32)) => { - f32::insert_wrapper(arena, module, wrapper_name, main_fn_index); - } - Layout::Builtin(Builtin::Float(FloatWidth::F64)) => { - f64::insert_wrapper(arena, module, wrapper_name, main_fn_index); - } - Layout::Builtin(Builtin::Bool) => { - bool::insert_wrapper(arena, module, wrapper_name, main_fn_index); - } - Layout::Union(UnionLayout::NonRecursive(_)) => stack_data_structure(), - Layout::Union(_) => { - i32::insert_wrapper(arena, module, wrapper_name, main_fn_index); - } - _ => stack_data_structure(), - } -} - -fn insert_wrapper_metadata<'a>( - arena: &'a Bump, - module: &mut WasmModule<'a>, - wrapper_name: &'static str, -) { - let index = (module.import.function_count() as u32) - + module.code.linking_dummy_count - + module.code.preloaded_count - + module.code.code_builders.len() as u32; - - module.add_function_signature(Signature { - param_types: Vec::with_capacity_in(0, arena), - ret_type: Some(ValueType::I32), - }); - - module.export.append(Export { - name: wrapper_name, - ty: ExportType::Func, - index, - }); - - let linker_symbol = SymInfo::Function(WasmObjectSymbol::ExplicitlyNamed { - flags: 0, - index, - name: wrapper_name, - }); - module.linking.symbol_table.push(linker_symbol); -} - -macro_rules! build_wrapper_body_primitive { - ($store_instruction: ident, $align: expr) => { - fn build_wrapper_body(code_builder: &mut CodeBuilder, main_function_index: u32) { - let frame_pointer_id = LocalId(0); - let frame_pointer = Some(frame_pointer_id); - let local_types = &[ValueType::I32]; - let frame_size = 8; - - code_builder.get_local(frame_pointer_id); - code_builder.call(main_function_index, 0, true); - code_builder.$store_instruction($align, 0); - code_builder.get_local(frame_pointer_id); - - code_builder.build_fn_header_and_footer(local_types, frame_size, frame_pointer); - } - }; -} - -macro_rules! wasm_result_primitive { - ($type_name: ident, $store_instruction: ident, $align: expr) => { - impl Wasm32Result for $type_name { - build_wrapper_body_primitive!($store_instruction, $align); - } - }; -} - -fn build_wrapper_body_stack_memory( - code_builder: &mut CodeBuilder, - main_function_index: u32, - size: usize, -) { - let local_id = LocalId(0); - let local_types = &[ValueType::I32]; - let frame_pointer = Some(local_id); - - code_builder.get_local(local_id); - code_builder.call(main_function_index, 0, true); - code_builder.get_local(local_id); - code_builder.build_fn_header_and_footer(local_types, size as i32, frame_pointer); -} - -macro_rules! wasm_result_stack_memory { - ($type_name: ident) => { - impl Wasm32Result for $type_name { - fn build_wrapper_body(code_builder: &mut CodeBuilder, main_function_index: u32) { - build_wrapper_body_stack_memory( - code_builder, - main_function_index, - $type_name::ACTUAL_WIDTH, - ) - } - } - }; -} - -wasm_result_primitive!(bool, i32_store8, Align::Bytes1); -wasm_result_primitive!(RocOrder, i32_store8, Align::Bytes1); - -wasm_result_primitive!(u8, i32_store8, Align::Bytes1); -wasm_result_primitive!(i8, i32_store8, Align::Bytes1); -wasm_result_primitive!(u16, i32_store16, Align::Bytes2); -wasm_result_primitive!(i16, i32_store16, Align::Bytes2); -wasm_result_primitive!(u32, i32_store, Align::Bytes4); -wasm_result_primitive!(i32, i32_store, Align::Bytes4); -wasm_result_primitive!(u64, i64_store, Align::Bytes8); -wasm_result_primitive!(i64, i64_store, Align::Bytes8); -wasm_result_primitive!(usize, i32_store, Align::Bytes4); - -wasm_result_primitive!(f32, f32_store, Align::Bytes4); -wasm_result_primitive!(f64, f64_store, Align::Bytes8); - -wasm_result_stack_memory!(u128); -wasm_result_stack_memory!(i128); -wasm_result_stack_memory!(RocDec); - -impl Wasm32Result for RocStr { - fn build_wrapper_body(code_builder: &mut CodeBuilder, main_function_index: u32) { - build_wrapper_body_stack_memory(code_builder, main_function_index, 12) - } -} - -impl Wasm32Result for RocList { - fn build_wrapper_body(code_builder: &mut CodeBuilder, main_function_index: u32) { - build_wrapper_body_stack_memory(code_builder, main_function_index, 12) - } -} - -impl Wasm32Result for &'_ T { - build_wrapper_body_primitive!(i32_store, Align::Bytes4); -} - -impl Wasm32Result for [T; N] -where - T: Wasm32Result + Wasm32Sized, -{ - fn build_wrapper_body(code_builder: &mut CodeBuilder, main_function_index: u32) { - build_wrapper_body_stack_memory(code_builder, main_function_index, N * T::ACTUAL_WIDTH) - } -} - -impl Wasm32Result for () { - fn build_wrapper_body(code_builder: &mut CodeBuilder, main_function_index: u32) { - code_builder.call(main_function_index, 0, false); - code_builder.get_global(0); - code_builder.build_fn_header_and_footer(&[], 0, None); - } -} - -impl Wasm32Result for (T, U) -where - T: Wasm32Result + Wasm32Sized, - U: Wasm32Result + Wasm32Sized, -{ - fn build_wrapper_body(code_builder: &mut CodeBuilder, main_function_index: u32) { - build_wrapper_body_stack_memory( - code_builder, - main_function_index, - T::ACTUAL_WIDTH + U::ACTUAL_WIDTH, - ) - } -} - -impl Wasm32Result for (T, U, V) -where - T: Wasm32Result + Wasm32Sized, - U: Wasm32Result + Wasm32Sized, - V: Wasm32Result + Wasm32Sized, -{ - fn build_wrapper_body(code_builder: &mut CodeBuilder, main_function_index: u32) { - build_wrapper_body_stack_memory( - code_builder, - main_function_index, - T::ACTUAL_WIDTH + U::ACTUAL_WIDTH + V::ACTUAL_WIDTH, - ) - } -} diff --git a/compiler/gen_wasm/src/wasm32_sized.rs b/compiler/gen_wasm/src/wasm32_sized.rs deleted file mode 100644 index 914f97392b..0000000000 --- a/compiler/gen_wasm/src/wasm32_sized.rs +++ /dev/null @@ -1,78 +0,0 @@ -use roc_std::{RocDec, RocList, RocOrder, RocStr}; - -pub trait Wasm32Sized: Sized { - const SIZE_OF_WASM: usize; - const ALIGN_OF_WASM: usize; - const ACTUAL_WIDTH: usize = if (Self::SIZE_OF_WASM % Self::ALIGN_OF_WASM) == 0 { - Self::SIZE_OF_WASM - } else { - Self::SIZE_OF_WASM + (Self::ALIGN_OF_WASM - (Self::SIZE_OF_WASM % Self::ALIGN_OF_WASM)) - }; -} - -macro_rules! wasm32_sized_primitive { - ($($type_name:ident ,)+) => { - $( - impl Wasm32Sized for $type_name { - const SIZE_OF_WASM: usize = core::mem::size_of::<$type_name>(); - const ALIGN_OF_WASM: usize = core::mem::align_of::<$type_name>(); - } - )* - } -} - -wasm32_sized_primitive!( - u8, i8, u16, i16, u32, i32, u64, i64, u128, i128, f32, f64, bool, RocDec, RocOrder, -); - -impl Wasm32Sized for () { - const SIZE_OF_WASM: usize = 0; - const ALIGN_OF_WASM: usize = 0; -} - -impl Wasm32Sized for RocStr { - const SIZE_OF_WASM: usize = 12; - const ALIGN_OF_WASM: usize = 4; -} - -impl Wasm32Sized for RocList { - const SIZE_OF_WASM: usize = 12; - const ALIGN_OF_WASM: usize = 4; -} - -impl Wasm32Sized for &'_ T { - const SIZE_OF_WASM: usize = 4; - const ALIGN_OF_WASM: usize = 4; -} - -impl Wasm32Sized for [T; N] { - const SIZE_OF_WASM: usize = N * T::SIZE_OF_WASM; - const ALIGN_OF_WASM: usize = T::ALIGN_OF_WASM; -} - -impl Wasm32Sized for usize { - const SIZE_OF_WASM: usize = 4; - const ALIGN_OF_WASM: usize = 4; -} - -impl Wasm32Sized for (T, U) { - const SIZE_OF_WASM: usize = T::SIZE_OF_WASM + U::SIZE_OF_WASM; - const ALIGN_OF_WASM: usize = max2(T::SIZE_OF_WASM, U::SIZE_OF_WASM); -} - -impl Wasm32Sized for (T, U, V) { - const SIZE_OF_WASM: usize = T::SIZE_OF_WASM + U::SIZE_OF_WASM + V::SIZE_OF_WASM; - const ALIGN_OF_WASM: usize = max3(T::SIZE_OF_WASM, U::SIZE_OF_WASM, V::SIZE_OF_WASM); -} - -const fn max2(a: usize, b: usize) -> usize { - if a > b { - a - } else { - b - } -} - -const fn max3(a: usize, b: usize, c: usize) -> usize { - max2(max2(a, b), c) -} diff --git a/compiler/gen_wasm/src/wasm_module/code_builder.rs b/compiler/gen_wasm/src/wasm_module/code_builder.rs deleted file mode 100644 index 6c09e9ca2f..0000000000 --- a/compiler/gen_wasm/src/wasm_module/code_builder.rs +++ /dev/null @@ -1,897 +0,0 @@ -use bumpalo::collections::vec::Vec; -use bumpalo::Bump; -use core::panic; -use roc_error_macros::internal_error; - -use roc_module::symbol::Symbol; - -use super::opcodes::{OpCode, OpCode::*}; -use super::serialize::{SerialBuffer, Serialize}; -use crate::{ - round_up_to_alignment, DEBUG_LOG_SETTINGS, FRAME_ALIGNMENT_BYTES, STACK_POINTER_GLOBAL_ID, -}; - -macro_rules! log_instruction { - ($($x: expr),+) => { - if DEBUG_LOG_SETTINGS.instructions { println!($($x,)*); } - }; -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct LocalId(pub u32); - -/// Wasm value type. (Rust representation matches Wasm encoding) -#[repr(u8)] -#[derive(PartialEq, Eq, Clone, Copy, Debug)] -pub enum ValueType { - I32 = 0x7f, - I64 = 0x7e, - F32 = 0x7d, - F64 = 0x7c, -} - -impl Serialize for ValueType { - fn serialize(&self, buffer: &mut T) { - buffer.append_u8(*self as u8); - } -} - -impl From for ValueType { - fn from(x: u8) -> Self { - match x { - 0x7f => Self::I32, - 0x7e => Self::I64, - 0x7d => Self::F32, - 0x7c => Self::F64, - _ => internal_error!("Invalid ValueType 0x{:02x}", x), - } - } -} - -const BLOCK_NO_RESULT: u8 = 0x40; - -/// A control block in our model of the VM -/// Child blocks cannot "see" values from their parent block -struct VmBlock<'a> { - /// opcode indicating what kind of block this is - opcode: OpCode, - /// the stack of values for this block - value_stack: Vec<'a, Symbol>, -} - -impl std::fmt::Debug for VmBlock<'_> { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_fmt(format_args!("{:?} {:?}", self.opcode, self.value_stack)) - } -} - -/// Wasm memory alignment for load/store instructions. -/// Rust representation matches Wasm encoding. -/// It's an error to specify alignment higher than the "natural" alignment of the instruction -#[repr(u8)] -#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)] -pub enum Align { - Bytes1 = 0, - Bytes2 = 1, - Bytes4 = 2, - Bytes8 = 3, -} - -impl Align { - /// Calculate the largest possible alignment for a load/store at a given stack frame offset - /// Assumes the stack frame is aligned to at least 8 bytes - pub fn from_stack_offset(max_align: Align, offset: u32) -> Align { - if (max_align == Align::Bytes8) && (offset & 7 == 0) { - return Align::Bytes8; - } - if (max_align >= Align::Bytes4) && (offset & 3 == 0) { - return Align::Bytes4; - } - if (max_align >= Align::Bytes2) && (offset & 1 == 0) { - return Align::Bytes2; - } - Align::Bytes1 - } -} - -impl From for Align { - fn from(x: u32) -> Align { - match x { - 1 => Align::Bytes1, - 2 => Align::Bytes2, - 4 => Align::Bytes4, - _ => { - if x.count_ones() == 1 { - Align::Bytes8 // Max value supported by any Wasm instruction - } else { - internal_error!("Cannot align to {} bytes", x); - } - } - } - } -} - -#[derive(Debug, Clone, PartialEq, Copy)] -pub enum VmSymbolState { - /// Value doesn't exist yet - NotYetPushed, - - /// Value has been pushed onto the VM stack but not yet popped - /// Remember where it was pushed, in case we need to insert another instruction there later - Pushed { pushed_at: usize }, - - /// Value has been pushed and popped, so it's not on the VM stack any more. - /// If we want to use it again later, we will have to create a local for it, - /// by going back to insert a local.tee instruction at pushed_at - Popped { pushed_at: usize }, -} - -// An instruction (local.set or local.tee) to be inserted into the function code -#[derive(Debug)] -struct Insertion { - at: usize, - start: usize, - end: usize, -} - -macro_rules! instruction_no_args { - ($method_name: ident, $opcode: expr, $pops: expr, $push: expr) => { - pub fn $method_name(&mut self) { - self.inst($opcode, $pops, $push); - } - }; -} - -macro_rules! instruction_memargs { - ($method_name: ident, $opcode: expr, $pops: expr, $push: expr) => { - pub fn $method_name(&mut self, align: Align, offset: u32) { - self.inst_mem($opcode, $pops, $push, align, offset); - } - }; -} - -#[derive(Debug)] -pub struct CodeBuilder<'a> { - pub arena: &'a Bump, - - /// The main container for the instructions - code: Vec<'a, u8>, - - /// Instruction bytes to be inserted into the code when finalizing the function - /// (Used for setting locals when we realise they are used multiple times) - insert_bytes: Vec<'a, u8>, - - /// Code locations where the insert_bytes should go - insertions: Vec<'a, Insertion>, - - /// Bytes for local variable declarations and stack-frame setup code. - /// We can't write this until we've finished the main code. But it goes - /// before it in the final output, so we need a separate vector. - preamble: Vec<'a, u8>, - - /// Encoded bytes for the inner length of the function, locals + code. - /// ("inner" because it doesn't include its own length!) - /// Again, we can't write this until we've finished the code and preamble, - /// but it goes before them in the binary, so it's a separate vector. - inner_length: Vec<'a, u8>, - - /// Our simulation model of the Wasm stack machine - /// Nested blocks of instructions. A child block can't "see" the stack of its parent block - vm_block_stack: Vec<'a, VmBlock<'a>>, -} - -impl<'a> Serialize for CodeBuilder<'a> { - fn serialize(&self, buffer: &mut T) { - self.serialize_without_relocs(buffer); - } -} - -#[allow(clippy::new_without_default)] -impl<'a> CodeBuilder<'a> { - pub fn new(arena: &'a Bump) -> Self { - let mut vm_block_stack = Vec::with_capacity_in(8, arena); - let function_block = VmBlock { - opcode: BLOCK, - value_stack: Vec::with_capacity_in(8, arena), - }; - vm_block_stack.push(function_block); - - CodeBuilder { - arena, - code: Vec::with_capacity_in(1024, arena), - insertions: Vec::with_capacity_in(32, arena), - insert_bytes: Vec::with_capacity_in(64, arena), - preamble: Vec::with_capacity_in(32, arena), - inner_length: Vec::with_capacity_in(5, arena), - vm_block_stack, - } - } - - /// Build a dummy function with just a single `unreachable` instruction - pub fn dummy(arena: &'a Bump) -> Self { - let mut builder = Self::new(arena); - builder.unreachable_(); - builder.build_fn_header_and_footer(&[], 0, None); - builder - } - - /********************************************************** - - SYMBOLS - - The Wasm VM stores temporary values in its stack machine. - We track which stack positions correspond to IR Symbols, - because it helps to generate more efficient code. - - ***********************************************************/ - - fn current_stack(&self) -> &Vec<'a, Symbol> { - let block = self.vm_block_stack.last().unwrap(); - &block.value_stack - } - - fn current_stack_mut(&mut self) -> &mut Vec<'a, Symbol> { - let block = self.vm_block_stack.last_mut().unwrap(); - &mut block.value_stack - } - - /// Set the Symbol that is at the top of the VM stack right now - /// We will use this later when we need to load the Symbol - pub fn set_top_symbol(&mut self, sym: Symbol) -> VmSymbolState { - let current_stack = &mut self.vm_block_stack.last_mut().unwrap().value_stack; - let pushed_at = self.code.len(); - let top_symbol: &mut Symbol = current_stack - .last_mut() - .unwrap_or_else(|| internal_error!("Empty stack when trying to set Symbol {:?}", sym)); - *top_symbol = sym; - - VmSymbolState::Pushed { pushed_at } - } - - /// Verify if a sequence of symbols is at the top of the stack - pub fn verify_stack_match(&self, symbols: &[Symbol]) -> bool { - let current_stack = self.current_stack(); - let n_symbols = symbols.len(); - let stack_depth = current_stack.len(); - if n_symbols > stack_depth { - return false; - } - let offset = stack_depth - n_symbols; - - for (i, sym) in symbols.iter().enumerate() { - if current_stack[offset + i] != *sym { - return false; - } - } - true - } - - fn add_insertion(&mut self, insert_at: usize, opcode: OpCode, immediate: u32) { - let start = self.insert_bytes.len(); - - self.insert_bytes.push(opcode as u8); - self.insert_bytes.encode_u32(immediate); - - self.insertions.push(Insertion { - at: insert_at, - start, - end: self.insert_bytes.len(), - }); - - log_instruction!( - "**insert {:?} {} at byte offset {}**", - opcode, - immediate, - insert_at - ); - } - - /// Load a Symbol that is stored in the VM stack - /// If it's already at the top of the stack, no code will be generated. - /// Otherwise, local.set and local.get instructions will be inserted, using the LocalId provided. - /// - /// If the return value is `Some(s)`, `s` should be stored by the caller, and provided in the next call. - /// If the return value is `None`, the Symbol is no longer stored in the VM stack, but in a local. - /// (In this case, the caller must remember to declare the local in the function header.) - pub fn load_symbol( - &mut self, - symbol: Symbol, - vm_state: VmSymbolState, - next_local_id: LocalId, - ) -> Option { - use VmSymbolState::*; - - match vm_state { - NotYetPushed => { - internal_error!("Symbol {:?} has no value yet. Nothing to load.", symbol) - } - - Pushed { pushed_at } => { - match self.current_stack().last() { - Some(top_symbol) if *top_symbol == symbol => { - // We're lucky, the symbol is already on top of the current block's stack. - // No code to generate! (This reduces code size by up to 25% in tests.) - // Just let the caller know what happened - Some(Popped { pushed_at }) - } - _ => { - // Symbol is not on top of the stack. - // We should have saved it to a local, so go back and do that now. - self.store_pushed_symbol_to_local( - symbol, - vm_state, - pushed_at, - next_local_id, - ); - - // Recover the value again at the current position - self.get_local(next_local_id); - self.set_top_symbol(symbol); - - // This Symbol is no longer stored in the VM stack, but in a local - None - } - } - } - - Popped { pushed_at } => { - // This Symbol is being used for a second time - // Insert a local.tee where it was pushed, so we don't interfere with the first usage - self.add_insertion(pushed_at, TEELOCAL, next_local_id.0); - - // Insert a local.get at the current position - self.get_local(next_local_id); - self.set_top_symbol(symbol); - - // This symbol has been promoted to a Local - // Tell the caller it no longer has a VirtualMachineSymbolState - None - } - } - } - - /// Go back and store a Symbol in a local variable, without loading it at the current position - pub fn store_symbol_to_local( - &mut self, - symbol: Symbol, - vm_state: VmSymbolState, - next_local_id: LocalId, - ) { - use VmSymbolState::*; - - match vm_state { - NotYetPushed => { - // Nothing to do - } - Pushed { pushed_at } => { - self.store_pushed_symbol_to_local(symbol, vm_state, pushed_at, next_local_id) - } - Popped { pushed_at } => { - self.add_insertion(pushed_at, TEELOCAL, next_local_id.0); - } - } - } - - fn store_pushed_symbol_to_local( - &mut self, - symbol: Symbol, - vm_state: VmSymbolState, - pushed_at: usize, - local_id: LocalId, - ) { - debug_assert!(matches!(vm_state, VmSymbolState::Pushed { .. })); - - // Update our stack model at the position where we're going to set the SETLOCAL - let mut found = false; - for block in self.vm_block_stack.iter_mut() { - if let Some(found_index) = block.value_stack.iter().position(|&s| s == symbol) { - block.value_stack.remove(found_index); - found = true; - } - } - - // Go back to the code position where it was pushed, and save it to a local - if found { - self.add_insertion(pushed_at, SETLOCAL, local_id.0); - } else { - if DEBUG_LOG_SETTINGS.instructions { - println!( - "{:?} has been popped implicitly. Leaving it on the stack.", - symbol - ); - } - self.add_insertion(pushed_at, TEELOCAL, local_id.0); - } - } - - /********************************************************** - - FUNCTION HEADER - - ***********************************************************/ - - /// Generate bytes to declare the function's local variables - fn build_local_declarations(&mut self, local_types: &[ValueType]) { - // reserve one byte for num_batches - self.preamble.push(0); - - if local_types.is_empty() { - return; - } - - // Write declarations in batches of the same ValueType - let mut num_batches: u32 = 0; - let mut batch_type = local_types[0]; - let mut batch_size = 0; - for t in local_types { - if *t == batch_type { - batch_size += 1; - } else { - self.preamble.encode_u32(batch_size); - self.preamble.push(batch_type as u8); - batch_type = *t; - batch_size = 1; - num_batches += 1; - } - } - self.preamble.encode_u32(batch_size); - self.preamble.push(batch_type as u8); - num_batches += 1; - - // Go back and write the number of batches at the start - if num_batches < 128 { - self.preamble[0] = num_batches as u8; - } else { - // We need more than 1 byte to encode num_batches! - // This is a ridiculous edge case, so just pad to 5 bytes for simplicity - let old_len = self.preamble.len(); - self.preamble.resize(old_len + 4, 0); - self.preamble.copy_within(1..old_len, 5); - self.preamble.overwrite_padded_u32(0, num_batches); - } - } - - /// Generate instruction bytes to grab a frame of stack memory on entering the function - fn build_stack_frame_push(&mut self, frame_size: i32, frame_pointer: LocalId) { - // Can't use the usual instruction methods because they push to self.code. - // This is the only case where we push instructions somewhere different. - self.preamble.push(GETGLOBAL as u8); - self.preamble.encode_u32(STACK_POINTER_GLOBAL_ID); - self.preamble.push(I32CONST as u8); - self.preamble.encode_i32(frame_size); - self.preamble.push(I32SUB as u8); - self.preamble.push(TEELOCAL as u8); - self.preamble.encode_u32(frame_pointer.0); - self.preamble.push(SETGLOBAL as u8); - self.preamble.encode_u32(STACK_POINTER_GLOBAL_ID); - } - - /// Generate instruction bytes to release a frame of stack memory on leaving the function - fn build_stack_frame_pop(&mut self, frame_size: i32, frame_pointer: LocalId) { - self.get_local(frame_pointer); - self.i32_const(frame_size); - self.i32_add(); - self.set_global(STACK_POINTER_GLOBAL_ID); - } - - /// Build the function header: local declarations, stack frame push/pop code, and function length - /// After this, all bytes have been generated (but not yet serialized) and we know the final size. - pub fn build_fn_header_and_footer( - &mut self, - local_types: &[ValueType], - frame_size: i32, - frame_pointer: Option, - ) { - self.build_local_declarations(local_types); - - if frame_size != 0 { - if let Some(frame_ptr_id) = frame_pointer { - let aligned_size = round_up_to_alignment!(frame_size, FRAME_ALIGNMENT_BYTES); - self.build_stack_frame_push(aligned_size, frame_ptr_id); - self.build_stack_frame_pop(aligned_size, frame_ptr_id); // footer - } - } - - self.code.push(END as u8); - - let inner_len = self.preamble.len() + self.code.len() + self.insert_bytes.len(); - self.inner_length.encode_u32(inner_len as u32); - - // Sort insertions. They are not created in order of assignment, but in order of *second* usage. - self.insertions.sort_by_key(|ins| ins.at); - } - - /********************************************************** - - SERIALIZE - - ***********************************************************/ - - pub fn size(&self) -> usize { - self.inner_length.len() + self.preamble.len() + self.code.len() + self.insert_bytes.len() - } - - /// Serialize all byte vectors in the right order - /// Also update relocation offsets relative to the base offset (code section body start) - pub fn serialize_without_relocs(&self, buffer: &mut T) { - buffer.append_slice(&self.inner_length); - buffer.append_slice(&self.preamble); - - let mut code_pos = 0; - for Insertion { at, start, end } in self.insertions.iter() { - buffer.append_slice(&self.code[code_pos..(*at)]); - buffer.append_slice(&self.insert_bytes[*start..*end]); - code_pos = *at; - } - - buffer.append_slice(&self.code[code_pos..self.code.len()]); - } - - /********************************************************** - - INSTRUCTION HELPER METHODS - - ***********************************************************/ - - /// Base method for generating instructions - /// Emits the opcode and simulates VM stack push/pop - fn inst_base(&mut self, opcode: OpCode, pops: usize, push: bool) { - let current_stack = self.current_stack_mut(); - let stack_size = current_stack.len(); - - debug_assert!( - stack_size >= pops, - "Wasm value stack underflow. Tried to pop {} but only {} available", - pops, - stack_size - ); - - let new_len = stack_size - pops as usize; - current_stack.truncate(new_len); - if push { - current_stack.push(Symbol::WASM_TMP); - } - self.code.push(opcode as u8); - } - - /// Plain instruction without any immediates - fn inst(&mut self, opcode: OpCode, pops: usize, push: bool) { - self.inst_base(opcode, pops, push); - log_instruction!( - "{:10}\t\t{:?}", - format!("{:?}", opcode), - self.vm_block_stack - ); - } - - /// Block instruction - fn inst_block(&mut self, opcode: OpCode, pops: usize) { - self.inst_base(opcode, pops, false); - - // We don't support block result types. Too hard to track types through arbitrary control flow. - self.code.push(BLOCK_NO_RESULT); - - // Start a new block with a fresh value stack - self.vm_block_stack.push(VmBlock { - opcode, - value_stack: Vec::with_capacity_in(8, self.arena), - }); - - log_instruction!("{:10}\t{:?}", format!("{:?}", opcode), &self.vm_block_stack); - } - - fn inst_imm32(&mut self, opcode: OpCode, pops: usize, push: bool, immediate: u32) { - self.inst_base(opcode, pops, push); - self.code.encode_u32(immediate); - log_instruction!( - "{:10}\t{}\t{:?}", - format!("{:?}", opcode), - immediate, - self.vm_block_stack - ); - } - - fn inst_mem(&mut self, opcode: OpCode, pops: usize, push: bool, align: Align, offset: u32) { - self.inst_base(opcode, pops, push); - self.code.push(align as u8); - self.code.encode_u32(offset); - log_instruction!( - "{:10} {:?} {}\t{:?}", - format!("{:?}", opcode), - align, - offset, - self.vm_block_stack - ); - } - - /********************************************************** - - INSTRUCTION METHODS - - One method for each Wasm instruction (in same order as the spec) - macros are for compactness & readability for the most common cases - Patterns that don't repeat very much don't have macros - - ***********************************************************/ - - instruction_no_args!(unreachable_, UNREACHABLE, 0, false); - instruction_no_args!(nop, NOP, 0, false); - - pub fn block(&mut self) { - self.inst_block(BLOCK, 0); - } - pub fn loop_(&mut self) { - self.inst_block(LOOP, 0); - } - pub fn if_(&mut self) { - self.inst_block(IF, 1); - } - pub fn else_(&mut self) { - // Reuse the 'then' block but clear its value stack - self.current_stack_mut().clear(); - self.inst(ELSE, 0, false); - } - - pub fn end(&mut self) { - // We need to drop any unused values from the VM stack in order to pass Wasm validation. - // This happens, for example, in test `gen_tags::if_guard_exhaustiveness` - let n_unused = self - .vm_block_stack - .last() - .map(|block| block.value_stack.len()) - .unwrap_or(0); - - for _ in 0..n_unused { - self.drop_(); - } - - self.inst_base(END, 0, false); - self.vm_block_stack.pop(); - - log_instruction!("END \t\t{:?}", &self.vm_block_stack); - } - pub fn br(&mut self, levels: u32) { - self.inst_imm32(BR, 0, false, levels); - } - pub fn br_if(&mut self, levels: u32) { - // In dynamic execution, br_if can pop 2 values if condition is true and the target block has a result. - // But our stack model is for *static* analysis and we need it to be correct at the next instruction, - // where the branch was not taken. So we only pop 1 value, the condition. - self.inst_imm32(BRIF, 1, false, levels); - } - #[allow(dead_code)] - fn br_table() { - todo!("br instruction"); - } - - instruction_no_args!(return_, RETURN, 0, false); - - pub fn call(&mut self, function_index: u32, n_args: usize, has_return_val: bool) { - self.inst_base(CALL, n_args, has_return_val); - self.code.encode_padded_u32(function_index); - - log_instruction!( - "{:10}\t{}\t{:?}", - format!("{:?}", CALL), - function_index, - self.vm_block_stack - ); - } - - #[allow(dead_code)] - fn call_indirect() { - unimplemented!( - "There is no plan to implement call_indirect. Roc doesn't use function pointers" - ); - } - - instruction_no_args!(drop_, DROP, 1, false); - instruction_no_args!(select, SELECT, 3, true); - - pub fn get_local(&mut self, id: LocalId) { - self.inst_imm32(GETLOCAL, 0, true, id.0); - } - pub fn set_local(&mut self, id: LocalId) { - self.inst_imm32(SETLOCAL, 1, false, id.0); - } - pub fn tee_local(&mut self, id: LocalId) { - self.inst_imm32(TEELOCAL, 0, false, id.0); - } - pub fn get_global(&mut self, id: u32) { - self.inst_imm32(GETGLOBAL, 0, true, id); - } - pub fn set_global(&mut self, id: u32) { - self.inst_imm32(SETGLOBAL, 1, false, id); - } - - instruction_memargs!(i32_load, I32LOAD, 1, true); - instruction_memargs!(i64_load, I64LOAD, 1, true); - instruction_memargs!(f32_load, F32LOAD, 1, true); - instruction_memargs!(f64_load, F64LOAD, 1, true); - instruction_memargs!(i32_load8_s, I32LOAD8S, 1, true); - instruction_memargs!(i32_load8_u, I32LOAD8U, 1, true); - instruction_memargs!(i32_load16_s, I32LOAD16S, 1, true); - instruction_memargs!(i32_load16_u, I32LOAD16U, 1, true); - instruction_memargs!(i64_load8_s, I64LOAD8S, 1, true); - instruction_memargs!(i64_load8_u, I64LOAD8U, 1, true); - instruction_memargs!(i64_load16_s, I64LOAD16S, 1, true); - instruction_memargs!(i64_load16_u, I64LOAD16U, 1, true); - instruction_memargs!(i64_load32_s, I64LOAD32S, 1, true); - instruction_memargs!(i64_load32_u, I64LOAD32U, 1, true); - instruction_memargs!(i32_store, I32STORE, 2, false); - instruction_memargs!(i64_store, I64STORE, 2, false); - instruction_memargs!(f32_store, F32STORE, 2, false); - instruction_memargs!(f64_store, F64STORE, 2, false); - instruction_memargs!(i32_store8, I32STORE8, 2, false); - instruction_memargs!(i32_store16, I32STORE16, 2, false); - instruction_memargs!(i64_store8, I64STORE8, 2, false); - instruction_memargs!(i64_store16, I64STORE16, 2, false); - instruction_memargs!(i64_store32, I64STORE32, 2, false); - - pub fn memory_size(&mut self) { - self.inst(CURRENTMEMORY, 0, true); - self.code.push(0); - } - pub fn memory_grow(&mut self) { - self.inst(GROWMEMORY, 1, true); - self.code.push(0); - } - - fn log_const(&self, opcode: OpCode, x: T) - where - T: std::fmt::Debug + std::fmt::Display, - { - log_instruction!( - "{:10}\t{}\t{:?}", - format!("{:?}", opcode), - x, - self.vm_block_stack - ); - } - pub fn i32_const(&mut self, x: i32) { - self.inst_base(I32CONST, 0, true); - self.code.encode_i32(x); - self.log_const(I32CONST, x); - } - pub fn i64_const(&mut self, x: i64) { - self.inst_base(I64CONST, 0, true); - self.code.encode_i64(x); - self.log_const(I64CONST, x); - } - pub fn f32_const(&mut self, x: f32) { - self.inst_base(F32CONST, 0, true); - self.code.encode_f32(x); - self.log_const(F32CONST, x); - } - pub fn f64_const(&mut self, x: f64) { - self.inst_base(F64CONST, 0, true); - self.code.encode_f64(x); - self.log_const(F64CONST, x); - } - - // TODO: Consider creating unified methods for numerical ops like 'eq' and 'add', - // passing the ValueType as an argument. Could simplify lowlevel code gen. - instruction_no_args!(i32_eqz, I32EQZ, 1, true); - instruction_no_args!(i32_eq, I32EQ, 2, true); - instruction_no_args!(i32_ne, I32NE, 2, true); - instruction_no_args!(i32_lt_s, I32LTS, 2, true); - instruction_no_args!(i32_lt_u, I32LTU, 2, true); - instruction_no_args!(i32_gt_s, I32GTS, 2, true); - instruction_no_args!(i32_gt_u, I32GTU, 2, true); - instruction_no_args!(i32_le_s, I32LES, 2, true); - instruction_no_args!(i32_le_u, I32LEU, 2, true); - instruction_no_args!(i32_ge_s, I32GES, 2, true); - instruction_no_args!(i32_ge_u, I32GEU, 2, true); - instruction_no_args!(i64_eqz, I64EQZ, 1, true); - instruction_no_args!(i64_eq, I64EQ, 2, true); - instruction_no_args!(i64_ne, I64NE, 2, true); - instruction_no_args!(i64_lt_s, I64LTS, 2, true); - instruction_no_args!(i64_lt_u, I64LTU, 2, true); - instruction_no_args!(i64_gt_s, I64GTS, 2, true); - instruction_no_args!(i64_gt_u, I64GTU, 2, true); - instruction_no_args!(i64_le_s, I64LES, 2, true); - instruction_no_args!(i64_le_u, I64LEU, 2, true); - instruction_no_args!(i64_ge_s, I64GES, 2, true); - instruction_no_args!(i64_ge_u, I64GEU, 2, true); - instruction_no_args!(f32_eq, F32EQ, 2, true); - instruction_no_args!(f32_ne, F32NE, 2, true); - instruction_no_args!(f32_lt, F32LT, 2, true); - instruction_no_args!(f32_gt, F32GT, 2, true); - instruction_no_args!(f32_le, F32LE, 2, true); - instruction_no_args!(f32_ge, F32GE, 2, true); - instruction_no_args!(f64_eq, F64EQ, 2, true); - instruction_no_args!(f64_ne, F64NE, 2, true); - instruction_no_args!(f64_lt, F64LT, 2, true); - instruction_no_args!(f64_gt, F64GT, 2, true); - instruction_no_args!(f64_le, F64LE, 2, true); - instruction_no_args!(f64_ge, F64GE, 2, true); - instruction_no_args!(i32_clz, I32CLZ, 1, true); - instruction_no_args!(i32_ctz, I32CTZ, 1, true); - instruction_no_args!(i32_popcnt, I32POPCNT, 1, true); - instruction_no_args!(i32_add, I32ADD, 2, true); - instruction_no_args!(i32_sub, I32SUB, 2, true); - instruction_no_args!(i32_mul, I32MUL, 2, true); - instruction_no_args!(i32_div_s, I32DIVS, 2, true); - instruction_no_args!(i32_div_u, I32DIVU, 2, true); - instruction_no_args!(i32_rem_s, I32REMS, 2, true); - instruction_no_args!(i32_rem_u, I32REMU, 2, true); - instruction_no_args!(i32_and, I32AND, 2, true); - instruction_no_args!(i32_or, I32OR, 2, true); - instruction_no_args!(i32_xor, I32XOR, 2, true); - instruction_no_args!(i32_shl, I32SHL, 2, true); - instruction_no_args!(i32_shr_s, I32SHRS, 2, true); - instruction_no_args!(i32_shr_u, I32SHRU, 2, true); - instruction_no_args!(i32_rotl, I32ROTL, 2, true); - instruction_no_args!(i32_rotr, I32ROTR, 2, true); - instruction_no_args!(i64_clz, I64CLZ, 1, true); - instruction_no_args!(i64_ctz, I64CTZ, 1, true); - instruction_no_args!(i64_popcnt, I64POPCNT, 1, true); - instruction_no_args!(i64_add, I64ADD, 2, true); - instruction_no_args!(i64_sub, I64SUB, 2, true); - instruction_no_args!(i64_mul, I64MUL, 2, true); - instruction_no_args!(i64_div_s, I64DIVS, 2, true); - instruction_no_args!(i64_div_u, I64DIVU, 2, true); - instruction_no_args!(i64_rem_s, I64REMS, 2, true); - instruction_no_args!(i64_rem_u, I64REMU, 2, true); - instruction_no_args!(i64_and, I64AND, 2, true); - instruction_no_args!(i64_or, I64OR, 2, true); - instruction_no_args!(i64_xor, I64XOR, 2, true); - instruction_no_args!(i64_shl, I64SHL, 2, true); - instruction_no_args!(i64_shr_s, I64SHRS, 2, true); - instruction_no_args!(i64_shr_u, I64SHRU, 2, true); - instruction_no_args!(i64_rotl, I64ROTL, 2, true); - instruction_no_args!(i64_rotr, I64ROTR, 2, true); - instruction_no_args!(f32_abs, F32ABS, 1, true); - instruction_no_args!(f32_neg, F32NEG, 1, true); - instruction_no_args!(f32_ceil, F32CEIL, 1, true); - instruction_no_args!(f32_floor, F32FLOOR, 1, true); - instruction_no_args!(f32_trunc, F32TRUNC, 1, true); - instruction_no_args!(f32_nearest, F32NEAREST, 1, true); - instruction_no_args!(f32_sqrt, F32SQRT, 1, true); - instruction_no_args!(f32_add, F32ADD, 2, true); - instruction_no_args!(f32_sub, F32SUB, 2, true); - instruction_no_args!(f32_mul, F32MUL, 2, true); - instruction_no_args!(f32_div, F32DIV, 2, true); - instruction_no_args!(f32_min, F32MIN, 2, true); - instruction_no_args!(f32_max, F32MAX, 2, true); - instruction_no_args!(f32_copysign, F32COPYSIGN, 2, true); - instruction_no_args!(f64_abs, F64ABS, 1, true); - instruction_no_args!(f64_neg, F64NEG, 1, true); - instruction_no_args!(f64_ceil, F64CEIL, 1, true); - instruction_no_args!(f64_floor, F64FLOOR, 1, true); - instruction_no_args!(f64_trunc, F64TRUNC, 1, true); - instruction_no_args!(f64_nearest, F64NEAREST, 1, true); - instruction_no_args!(f64_sqrt, F64SQRT, 1, true); - instruction_no_args!(f64_add, F64ADD, 2, true); - instruction_no_args!(f64_sub, F64SUB, 2, true); - instruction_no_args!(f64_mul, F64MUL, 2, true); - instruction_no_args!(f64_div, F64DIV, 2, true); - instruction_no_args!(f64_min, F64MIN, 2, true); - instruction_no_args!(f64_max, F64MAX, 2, true); - instruction_no_args!(f64_copysign, F64COPYSIGN, 2, true); - instruction_no_args!(i32_wrap_i64, I32WRAPI64, 1, true); - instruction_no_args!(i32_trunc_s_f32, I32TRUNCSF32, 1, true); - instruction_no_args!(i32_trunc_u_f32, I32TRUNCUF32, 1, true); - instruction_no_args!(i32_trunc_s_f64, I32TRUNCSF64, 1, true); - instruction_no_args!(i32_trunc_u_f64, I32TRUNCUF64, 1, true); - instruction_no_args!(i64_extend_s_i32, I64EXTENDSI32, 1, true); - instruction_no_args!(i64_extend_u_i32, I64EXTENDUI32, 1, true); - instruction_no_args!(i64_trunc_s_f32, I64TRUNCSF32, 1, true); - instruction_no_args!(i64_trunc_u_f32, I64TRUNCUF32, 1, true); - instruction_no_args!(i64_trunc_s_f64, I64TRUNCSF64, 1, true); - instruction_no_args!(i64_trunc_u_f64, I64TRUNCUF64, 1, true); - instruction_no_args!(f32_convert_s_i32, F32CONVERTSI32, 1, true); - instruction_no_args!(f32_convert_u_i32, F32CONVERTUI32, 1, true); - instruction_no_args!(f32_convert_s_i64, F32CONVERTSI64, 1, true); - instruction_no_args!(f32_convert_u_i64, F32CONVERTUI64, 1, true); - instruction_no_args!(f32_demote_f64, F32DEMOTEF64, 1, true); - instruction_no_args!(f64_convert_s_i32, F64CONVERTSI32, 1, true); - instruction_no_args!(f64_convert_u_i32, F64CONVERTUI32, 1, true); - instruction_no_args!(f64_convert_s_i64, F64CONVERTSI64, 1, true); - instruction_no_args!(f64_convert_u_i64, F64CONVERTUI64, 1, true); - instruction_no_args!(f64_promote_f32, F64PROMOTEF32, 1, true); - instruction_no_args!(i32_reinterpret_f32, I32REINTERPRETF32, 1, true); - instruction_no_args!(i64_reinterpret_f64, I64REINTERPRETF64, 1, true); - instruction_no_args!(f32_reinterpret_i32, F32REINTERPRETI32, 1, true); - instruction_no_args!(f64_reinterpret_i64, F64REINTERPRETI64, 1, true); -} diff --git a/compiler/gen_wasm/src/wasm_module/dead_code.rs b/compiler/gen_wasm/src/wasm_module/dead_code.rs deleted file mode 100644 index dcd54ec9e3..0000000000 --- a/compiler/gen_wasm/src/wasm_module/dead_code.rs +++ /dev/null @@ -1,235 +0,0 @@ -use bumpalo::collections::vec::Vec; -use bumpalo::Bump; - -use super::opcodes::OpCode; -use super::parse::{Parse, ParseError, SkipBytes}; -use super::serialize::{SerialBuffer, Serialize}; -use super::CodeBuilder; - -/* - -DEAD CODE ELIMINATION - -Or, more specifically, "dead function replacement" - -- On pre-loading the object file: - - Analyse its call graph by finding all `call` instructions in the Code section, - and checking which function index they refer to. Store this in a `PreloadsCallGraph` -- While compiling Roc code: - - Run the backend as usual, adding more data into various sections of the Wasm module - - Whenever a call to a builtin or platform function is made, record its index. - These are the "live" preloaded functions that we are not allowed to eliminate. -- Call graph analysis: - - Starting with the live preloaded functions, trace their call graphs using the info we - collected earlier in `PreloadsCallGraph`. Mark all function indices in the call graph as "live". -- Dead function replacement: - - We actually don't want to just *delete* dead functions, because that would change the indices - of the live functions, invalidating all references to them, such as `call` instructions. - - Instead, during serialization, we replace its body with a single `unreachable` instruction -*/ - -#[derive(Debug)] -pub struct PreloadsCallGraph<'a> { - num_preloads: usize, - /// Byte offset where each function body can be found - code_offsets: Vec<'a, u32>, - /// Vector with one entry per *call*, containing the called function's index - calls: Vec<'a, u32>, - /// Vector with one entry per *function*, indicating its offset in `calls` - calls_offsets: Vec<'a, u32>, -} - -impl<'a> PreloadsCallGraph<'a> { - pub fn new(arena: &'a Bump, import_fn_count: usize, fn_count: usize) -> Self { - let num_preloads = import_fn_count + fn_count; - - let mut code_offsets = Vec::with_capacity_in(num_preloads, arena); - let calls = Vec::with_capacity_in(2 * num_preloads, arena); - let mut calls_offsets = Vec::with_capacity_in(1 + num_preloads, arena); - - // Imported functions have zero code length and no calls - code_offsets.extend(std::iter::repeat(0).take(import_fn_count)); - calls_offsets.extend(std::iter::repeat(0).take(import_fn_count)); - - PreloadsCallGraph { - num_preloads, - code_offsets, - calls, - calls_offsets, - } - } -} - -/// Parse a Code section, collecting metadata that we can use to figure out -/// which functions are actually called, and which are not. -/// This would normally be done in a linker optimisation, but we want to be able to -/// use this backend without a linker. -pub fn parse_preloads_call_graph<'a>( - arena: &'a Bump, - code_section_body: &[u8], - imported_fn_signatures: &[u32], - defined_fn_signatures: &[u32], - indirect_callees: &[u32], -) -> Result, ParseError> { - let mut call_graph = PreloadsCallGraph::new( - arena, - imported_fn_signatures.len(), - defined_fn_signatures.len(), - ); - - // Function type signatures, used for indirect calls - let mut signatures = Vec::with_capacity_in( - imported_fn_signatures.len() + defined_fn_signatures.len(), - arena, - ); - signatures.extend_from_slice(imported_fn_signatures); - signatures.extend_from_slice(defined_fn_signatures); - - // Iterate over the bytes of the Code section - let mut cursor: usize = 0; - while cursor < code_section_body.len() { - // Record the start of a function - call_graph.code_offsets.push(cursor as u32); - call_graph.calls_offsets.push(call_graph.calls.len() as u32); - - let func_size = u32::parse((), code_section_body, &mut cursor)?; - let func_end = cursor + func_size as usize; - - // Skip over local variable declarations - let local_groups_count = u32::parse((), code_section_body, &mut cursor)?; - for _ in 0..local_groups_count { - u32::parse((), code_section_body, &mut cursor)?; - cursor += 1; // ValueType - } - - // Parse `call` and `call_indirect` instructions, skip over everything else - while cursor < func_end { - let opcode_byte: u8 = code_section_body[cursor]; - if opcode_byte == OpCode::CALL as u8 { - cursor += 1; - let call_index = u32::parse((), code_section_body, &mut cursor)?; - call_graph.calls.push(call_index as u32); - } else if opcode_byte == OpCode::CALLINDIRECT as u8 { - cursor += 1; - // Insert all indirect callees with a matching type signature - let sig = u32::parse((), code_section_body, &mut cursor)?; - call_graph.calls.extend( - indirect_callees - .iter() - .filter(|f| signatures[**f as usize] == sig), - ); - u32::skip_bytes(code_section_body, &mut cursor)?; // table_idx - } else { - OpCode::skip_bytes(code_section_body, &mut cursor)?; - } - } - } - - // Extra entries to mark the end of the last function - call_graph.code_offsets.push(cursor as u32); - call_graph.calls_offsets.push(call_graph.calls.len() as u32); - - Ok(call_graph) -} - -/// Trace the dependencies of a list of functions -/// We've already collected call_graph saying which functions call each other -/// Now we need to trace the dependency graphs of a specific subset of them -/// Result is the full set of builtins and platform functions used in the app. -/// The rest are "dead code" and can be eliminated. -pub fn trace_call_graph<'a, Indices: IntoIterator>( - arena: &'a Bump, - call_graph: &PreloadsCallGraph<'a>, - exported_fns: &[u32], - called_from_app: Indices, -) -> Vec<'a, u32> { - let num_preloads = call_graph.num_preloads; - - // All functions that get called from the app, directly or indirectly - let mut live_fn_indices = Vec::with_capacity_in(num_preloads, arena); - - // Current & next batch of functions whose call graphs we want to trace through the call_graph - // (2 separate vectors so that we're not iterating over the same one we're changing) - // If the max call depth is N then we will do N traces or less - let mut current_trace = Vec::with_capacity_in(num_preloads, arena); - let mut next_trace = Vec::with_capacity_in(num_preloads, arena); - - // Start with preloaded functions called from the app or exported directly to Wasm host - current_trace.extend(called_from_app); - current_trace.extend( - exported_fns - .iter() - .filter(|idx| **idx < num_preloads as u32), - ); - current_trace.sort_unstable(); - current_trace.dedup(); - - // Fast per-function lookup table to see if its dependencies have already been traced - let mut already_traced = Vec::from_iter_in(std::iter::repeat(false).take(num_preloads), arena); - - loop { - live_fn_indices.extend_from_slice(¤t_trace); - - for func_idx in current_trace.iter() { - let i = *func_idx as usize; - already_traced[i] = true; - let calls_start = call_graph.calls_offsets[i] as usize; - let calls_end = call_graph.calls_offsets[i + 1] as usize; - let called_indices: &[u32] = &call_graph.calls[calls_start..calls_end]; - for called_idx in called_indices { - if !already_traced[*called_idx as usize] { - next_trace.push(*called_idx); - } - } - } - if next_trace.is_empty() { - break; - } - next_trace.sort_unstable(); - next_trace.dedup(); - - current_trace.clone_from(&next_trace); - next_trace.clear(); - } - - live_fn_indices -} - -/// Copy used functions from preloaded object file into our Code section -/// Replace unused functions with very small dummies, to avoid changing any indices -pub fn copy_preloads_shrinking_dead_fns<'a, T: SerialBuffer>( - arena: &'a Bump, - buffer: &mut T, - call_graph: &PreloadsCallGraph<'a>, - preloaded_bytes: &[u8], - import_fn_count: usize, - mut live_preload_indices: Vec<'a, u32>, -) { - let preload_idx_start = import_fn_count; - - // Create a dummy function with just a single `unreachable` instruction - let builder = CodeBuilder::dummy(arena); - let mut dummy_bytes = Vec::with_capacity_in(builder.size(), arena); - builder.serialize(&mut dummy_bytes); - - live_preload_indices.sort_unstable(); - live_preload_indices.dedup(); - - let mut live_iter = live_preload_indices - .into_iter() - .skip_while(|f| (*f as usize) < preload_idx_start); - let mut next_live_idx = live_iter.next(); - for i in preload_idx_start..call_graph.num_preloads { - match next_live_idx { - Some(live) if live as usize == i => { - next_live_idx = live_iter.next(); - let live_body_start = call_graph.code_offsets[i] as usize; - let live_body_end = call_graph.code_offsets[i + 1] as usize; - buffer.append_slice(&preloaded_bytes[live_body_start..live_body_end]); - } - _ => { - buffer.append_slice(&dummy_bytes); - } - } - } -} diff --git a/compiler/gen_wasm/src/wasm_module/mod.rs b/compiler/gen_wasm/src/wasm_module/mod.rs deleted file mode 100644 index a7b1b49c46..0000000000 --- a/compiler/gen_wasm/src/wasm_module/mod.rs +++ /dev/null @@ -1,347 +0,0 @@ -pub mod code_builder; -mod dead_code; -pub mod linking; -pub mod opcodes; -pub mod parse; -pub mod sections; -pub mod serialize; - -use bumpalo::{collections::Vec, Bump}; -pub use code_builder::{Align, CodeBuilder, LocalId, ValueType, VmSymbolState}; -pub use linking::{OffsetRelocType, RelocationEntry, SymInfo}; -pub use sections::{ConstExpr, Export, ExportType, Global, GlobalType, Signature}; - -use self::linking::{LinkingSection, RelocationSection}; -use self::parse::{Parse, ParseError}; -use self::sections::{ - CodeSection, DataSection, ElementSection, ExportSection, FunctionSection, GlobalSection, - ImportDesc, ImportSection, MemorySection, NameSection, OpaqueSection, Section, SectionId, - TableSection, TypeSection, -}; -use self::serialize::{SerialBuffer, Serialize}; - -/// A representation of the WebAssembly binary file format -/// https://webassembly.github.io/spec/core/binary/modules.html -#[derive(Debug)] -pub struct WasmModule<'a> { - pub types: TypeSection<'a>, - pub import: ImportSection<'a>, - pub function: FunctionSection<'a>, - pub table: TableSection, - pub memory: MemorySection<'a>, - pub global: GlobalSection<'a>, - pub export: ExportSection<'a>, - pub start: OpaqueSection<'a>, - pub element: ElementSection<'a>, - pub code: CodeSection<'a>, - pub data: DataSection<'a>, - pub linking: LinkingSection<'a>, - pub reloc_code: RelocationSection<'a>, - pub reloc_data: RelocationSection<'a>, - pub names: NameSection<'a>, -} - -impl<'a> WasmModule<'a> { - pub const WASM_VERSION: u32 = 1; - - /// Create entries in the Type and Function sections for a function signature - pub fn add_function_signature(&mut self, signature: Signature<'a>) { - let index = self.types.insert(signature); - self.function.add_sig(index); - } - - /// Serialize the module to bytes - pub fn serialize(&self, buffer: &mut T) { - buffer.append_u8(0); - buffer.append_slice("asm".as_bytes()); - buffer.write_unencoded_u32(Self::WASM_VERSION); - - self.types.serialize(buffer); - self.import.serialize(buffer); - self.function.serialize(buffer); - self.table.serialize(buffer); - self.memory.serialize(buffer); - self.global.serialize(buffer); - self.export.serialize(buffer); - self.start.serialize(buffer); - self.element.serialize(buffer); - self.code.serialize(buffer); - self.data.serialize(buffer); - self.names.serialize(buffer); - } - - /// Module size in bytes (assuming no linker data) - /// May be slightly overestimated. Intended for allocating buffer capacity. - pub fn size(&self) -> usize { - self.types.size() - + self.import.size() - + self.function.size() - + self.table.size() - + self.memory.size() - + self.global.size() - + self.export.size() - + self.start.size() - + self.element.size() - + self.code.size() - + self.data.size() - + self.names.size() - } - - pub fn preload(arena: &'a Bump, bytes: &[u8]) -> Result { - let is_valid_magic_number = &bytes[0..4] == "\0asm".as_bytes(); - let is_valid_version = bytes[4..8] == Self::WASM_VERSION.to_le_bytes(); - if !is_valid_magic_number || !is_valid_version { - return Err(ParseError { - offset: 0, - message: "This file is not a WebAssembly binary. The file header is not valid." - .into(), - }); - } - - let mut cursor: usize = 8; - - let types = TypeSection::parse(arena, bytes, &mut cursor)?; - let import = ImportSection::parse(arena, bytes, &mut cursor)?; - let function = FunctionSection::parse(arena, bytes, &mut cursor)?; - let table = TableSection::parse((), bytes, &mut cursor)?; - let memory = MemorySection::parse(arena, bytes, &mut cursor)?; - let global = GlobalSection::parse(arena, bytes, &mut cursor)?; - let export = ExportSection::parse(arena, bytes, &mut cursor)?; - let start = OpaqueSection::parse((arena, SectionId::Start), bytes, &mut cursor)?; - let element = ElementSection::parse(arena, bytes, &mut cursor)?; - let indirect_callees = element.indirect_callees(arena); - - let imported_fn_signatures = import.function_signatures(arena); - let code = CodeSection::parse( - arena, - bytes, - &mut cursor, - &imported_fn_signatures, - &function.signatures, - &indirect_callees, - )?; - - let data = DataSection::parse(arena, bytes, &mut cursor)?; - - let linking = LinkingSection::parse(arena, bytes, &mut cursor)?; - let reloc_code = RelocationSection::parse((arena, "reloc.CODE"), bytes, &mut cursor)?; - let reloc_data = RelocationSection::parse((arena, "reloc.DATA"), bytes, &mut cursor)?; - let names = NameSection::parse(arena, bytes, &mut cursor)?; - - let mut module_errors = String::new(); - if types.is_empty() { - module_errors.push_str("Missing Type section\n"); - } - if function.signatures.is_empty() { - module_errors.push_str("Missing Function section\n"); - } - if code.preloaded_bytes.is_empty() { - module_errors.push_str("Missing Code section\n"); - } - if linking.symbol_table.is_empty() { - module_errors.push_str("Missing \"linking\" Custom section\n"); - } - if reloc_code.entries.is_empty() { - module_errors.push_str("Missing \"reloc.CODE\" Custom section\n"); - } - if global.count != 0 { - let global_err_msg = - format!("All globals in a relocatable Wasm module should be imported, but found {} internally defined", global.count); - module_errors.push_str(&global_err_msg); - } - - if !module_errors.is_empty() { - return Err(ParseError { - offset: 0, - message: format!("{}\n{}\n{}", - "The host file has the wrong structure. I need a relocatable WebAssembly binary file.", - "If you're using wasm-ld, try the --relocatable option.", - module_errors, - ) - }); - } - - Ok(WasmModule { - types, - import, - function, - table, - memory, - global, - export, - start, - element, - code, - data, - linking, - reloc_code, - reloc_data, - names, - }) - } - - pub fn remove_dead_preloads>( - &mut self, - arena: &'a Bump, - called_preload_fns: T, - ) { - let exported_fn_iter = self - .export - .exports - .iter() - .filter(|ex| ex.ty == ExportType::Func) - .map(|ex| ex.index); - let exported_fn_indices = Vec::from_iter_in(exported_fn_iter, arena); - - self.code.remove_dead_preloads( - arena, - self.import.function_count(), - &exported_fn_indices, - called_preload_fns, - ) - } - - pub fn get_exported_global_u32(&self, name: &str) -> Option { - self.export - .exports - .iter() - .find(|ex| ex.name == name) - .and_then(|ex| self.global.parse_u32_at_index(ex.index).ok()) - } - - pub fn relocate_internal_symbol(&mut self, sym_name: &str, value: u32) -> u32 { - let sym_index = self - .linking - .find_internal_symbol(sym_name) - .unwrap_or_else(|| panic!("Linking failed! Can't find host symbol `{}`", sym_name)); - - self.reloc_code.apply_relocs_u32( - &mut self.code.preloaded_bytes, - self.code.preloaded_reloc_offset, - sym_index, - value, - ); - - sym_index - } - - /// Linking steps for host-to-app functions like `roc__mainForHost_1_exposed` - /// (See further explanation in the gen_wasm README) - /// - Remove the target function from the ImportSection. It's not a JS import but the host declared it as one. - /// - Update all of its call sites to the new index in the app - /// - Swap the _last_ JavaScript import into the slot we just vacated - /// - Update all call sites for the swapped JS function - /// - Update the FunctionSection to show the correct type signature for the swapped JS function - /// - Insert a dummy function in the CodeSection, at the same index as the swapped JS function - pub fn link_host_to_app_calls(&mut self, host_to_app_map: Vec<'a, (&'a str, u32)>) { - for (app_fn_name, app_fn_index) in host_to_app_map.into_iter() { - // Find the host import, and the last imported function to swap with it. - // Not all imports are functions, so the function index and import index may be different - // (We could support imported globals if we relocated them, although we don't at the time of this comment) - let mut host_fn = None; - let mut swap_fn = None; - self.import - .imports - .iter() - .enumerate() - .filter(|(_import_index, import)| { - matches!(import.description, ImportDesc::Func { .. }) - }) - .enumerate() - .for_each(|(fn_index, (import_index, import))| { - swap_fn = Some((import_index, fn_index)); - if import.name == app_fn_name { - host_fn = Some((import_index, fn_index)); - } - }); - - let (host_import_index, host_fn_index) = match host_fn { - Some(x) => x, - None => { - // The Wasm host doesn't call our app function, so it must be called from JS. Export it. - self.export.append(Export { - name: app_fn_name, - ty: ExportType::Func, - index: app_fn_index, - }); - continue; - } - }; - let (swap_import_index, swap_fn_index) = swap_fn.unwrap(); - - // Note: swap_remove will not work, because some imports may not be functions. - let swap_import = self.import.imports.remove(swap_import_index); - if swap_import_index != host_import_index { - self.import.imports[host_import_index] = swap_import; - } - - // Find the host's symbol for the function we're linking - let host_sym_index = self - .linking - .find_imported_function_symbol(host_fn_index as u32) - .unwrap_or_else(|| { - panic!( - "Linking failed! Can't find fn #{} ({}) in host symbol table", - host_fn_index, app_fn_name - ) - }); - - // Update calls to use the app function instead of the host import - self.reloc_code.apply_relocs_u32( - &mut self.code.preloaded_bytes, - self.code.preloaded_reloc_offset, - host_sym_index, - app_fn_index, - ); - - if swap_import_index != host_import_index { - // get the name using the old host import index because we already swapped it! - let swap_fn_name = self.import.imports[host_import_index].name; - - // Find the symbol for the swapped JS import - let swap_sym_index = self - .linking - .find_imported_function_symbol(swap_fn_index as u32) - .unwrap_or_else(|| { - panic!( - "Linking failed! Can't find fn #{} ({}) in host symbol table", - swap_fn_index, swap_fn_name - ) - }); - - // Update calls to the swapped JS import - self.reloc_code.apply_relocs_u32( - &mut self.code.preloaded_bytes, - self.code.preloaded_reloc_offset, - swap_sym_index, - host_fn_index as u32, - ); - - // Update the name in the debug info - let (_, debug_name) = self - .names - .function_names - .iter_mut() - .find(|(i, _)| *i as usize == host_fn_index) - .unwrap(); - debug_name.clone_from(&swap_fn_name); - } - - // Remember to insert a dummy function at the beginning of the code section - // to compensate for having one less import, so that function indices don't change. - self.code.linking_dummy_count += 1; - - // Insert any type signature for the dummy. Signature index 0 will do. - self.function.signatures.insert(0, 0); - - // Update the debug name for the dummy - let (_, debug_name) = self - .names - .function_names - .iter_mut() - .find(|(i, _)| *i as usize == swap_fn_index) - .unwrap(); - debug_name.clone_from(&"linking_dummy"); - } - } -} diff --git a/compiler/gen_wasm/src/wasm_module/opcodes.rs b/compiler/gen_wasm/src/wasm_module/opcodes.rs deleted file mode 100644 index e5674dc2b2..0000000000 --- a/compiler/gen_wasm/src/wasm_module/opcodes.rs +++ /dev/null @@ -1,310 +0,0 @@ -use super::parse::{Parse, ParseError, SkipBytes}; - -#[repr(u8)] -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum OpCode { - UNREACHABLE = 0x00, - NOP = 0x01, - BLOCK = 0x02, - LOOP = 0x03, - IF = 0x04, - ELSE = 0x05, - END = 0x0b, - BR = 0x0c, - BRIF = 0x0d, - BRTABLE = 0x0e, - RETURN = 0x0f, - CALL = 0x10, - CALLINDIRECT = 0x11, - DROP = 0x1a, - SELECT = 0x1b, - GETLOCAL = 0x20, - SETLOCAL = 0x21, - TEELOCAL = 0x22, - GETGLOBAL = 0x23, - SETGLOBAL = 0x24, - I32LOAD = 0x28, - I64LOAD = 0x29, - F32LOAD = 0x2a, - F64LOAD = 0x2b, - I32LOAD8S = 0x2c, - I32LOAD8U = 0x2d, - I32LOAD16S = 0x2e, - I32LOAD16U = 0x2f, - I64LOAD8S = 0x30, - I64LOAD8U = 0x31, - I64LOAD16S = 0x32, - I64LOAD16U = 0x33, - I64LOAD32S = 0x34, - I64LOAD32U = 0x35, - I32STORE = 0x36, - I64STORE = 0x37, - F32STORE = 0x38, - F64STORE = 0x39, - I32STORE8 = 0x3a, - I32STORE16 = 0x3b, - I64STORE8 = 0x3c, - I64STORE16 = 0x3d, - I64STORE32 = 0x3e, - CURRENTMEMORY = 0x3f, - GROWMEMORY = 0x40, - I32CONST = 0x41, - I64CONST = 0x42, - F32CONST = 0x43, - F64CONST = 0x44, - I32EQZ = 0x45, - I32EQ = 0x46, - I32NE = 0x47, - I32LTS = 0x48, - I32LTU = 0x49, - I32GTS = 0x4a, - I32GTU = 0x4b, - I32LES = 0x4c, - I32LEU = 0x4d, - I32GES = 0x4e, - I32GEU = 0x4f, - I64EQZ = 0x50, - I64EQ = 0x51, - I64NE = 0x52, - I64LTS = 0x53, - I64LTU = 0x54, - I64GTS = 0x55, - I64GTU = 0x56, - I64LES = 0x57, - I64LEU = 0x58, - I64GES = 0x59, - I64GEU = 0x5a, - - F32EQ = 0x5b, - F32NE = 0x5c, - F32LT = 0x5d, - F32GT = 0x5e, - F32LE = 0x5f, - F32GE = 0x60, - - F64EQ = 0x61, - F64NE = 0x62, - F64LT = 0x63, - F64GT = 0x64, - F64LE = 0x65, - F64GE = 0x66, - - I32CLZ = 0x67, - I32CTZ = 0x68, - I32POPCNT = 0x69, - I32ADD = 0x6a, - I32SUB = 0x6b, - I32MUL = 0x6c, - I32DIVS = 0x6d, - I32DIVU = 0x6e, - I32REMS = 0x6f, - I32REMU = 0x70, - I32AND = 0x71, - I32OR = 0x72, - I32XOR = 0x73, - I32SHL = 0x74, - I32SHRS = 0x75, - I32SHRU = 0x76, - I32ROTL = 0x77, - I32ROTR = 0x78, - - I64CLZ = 0x79, - I64CTZ = 0x7a, - I64POPCNT = 0x7b, - I64ADD = 0x7c, - I64SUB = 0x7d, - I64MUL = 0x7e, - I64DIVS = 0x7f, - I64DIVU = 0x80, - I64REMS = 0x81, - I64REMU = 0x82, - I64AND = 0x83, - I64OR = 0x84, - I64XOR = 0x85, - I64SHL = 0x86, - I64SHRS = 0x87, - I64SHRU = 0x88, - I64ROTL = 0x89, - I64ROTR = 0x8a, - F32ABS = 0x8b, - F32NEG = 0x8c, - F32CEIL = 0x8d, - F32FLOOR = 0x8e, - F32TRUNC = 0x8f, - F32NEAREST = 0x90, - F32SQRT = 0x91, - F32ADD = 0x92, - F32SUB = 0x93, - F32MUL = 0x94, - F32DIV = 0x95, - F32MIN = 0x96, - F32MAX = 0x97, - F32COPYSIGN = 0x98, - F64ABS = 0x99, - F64NEG = 0x9a, - F64CEIL = 0x9b, - F64FLOOR = 0x9c, - F64TRUNC = 0x9d, - F64NEAREST = 0x9e, - F64SQRT = 0x9f, - F64ADD = 0xa0, - F64SUB = 0xa1, - F64MUL = 0xa2, - F64DIV = 0xa3, - F64MIN = 0xa4, - F64MAX = 0xa5, - F64COPYSIGN = 0xa6, - - I32WRAPI64 = 0xa7, - I32TRUNCSF32 = 0xa8, - I32TRUNCUF32 = 0xa9, - I32TRUNCSF64 = 0xaa, - I32TRUNCUF64 = 0xab, - I64EXTENDSI32 = 0xac, - I64EXTENDUI32 = 0xad, - I64TRUNCSF32 = 0xae, - I64TRUNCUF32 = 0xaf, - I64TRUNCSF64 = 0xb0, - I64TRUNCUF64 = 0xb1, - F32CONVERTSI32 = 0xb2, - F32CONVERTUI32 = 0xb3, - F32CONVERTSI64 = 0xb4, - F32CONVERTUI64 = 0xb5, - F32DEMOTEF64 = 0xb6, - F64CONVERTSI32 = 0xb7, - F64CONVERTUI32 = 0xb8, - F64CONVERTSI64 = 0xb9, - F64CONVERTUI64 = 0xba, - F64PROMOTEF32 = 0xbb, - - I32REINTERPRETF32 = 0xbc, - I64REINTERPRETF64 = 0xbd, - F32REINTERPRETI32 = 0xbe, - F64REINTERPRETI64 = 0xbf, -} - -/// The format of the *immediate* operands of an operator -/// Immediates appear directly in the byte stream after the opcode, -/// rather than being popped off the value stack. These are the possible forms. -#[derive(Debug)] -enum OpImmediates { - NoImmediate, - Byte1, - Bytes4, - Bytes8, - Leb32x1, - Leb64x1, - Leb32x2, - BrTable, -} - -fn immediates_for(op: OpCode) -> Result { - use OpCode::*; - use OpImmediates::*; - - let imm = match op { - UNREACHABLE => NoImmediate, - NOP => NoImmediate, - BLOCK | LOOP | IF => Byte1, - ELSE => NoImmediate, - END => NoImmediate, - BR | BRIF => Leb32x1, - BRTABLE => BrTable, - RETURN => NoImmediate, - CALL => Leb32x1, - CALLINDIRECT => Leb32x2, - DROP => NoImmediate, - SELECT => NoImmediate, - GETLOCAL | SETLOCAL | TEELOCAL => Leb32x1, - GETGLOBAL | SETGLOBAL => Leb32x1, - - I32LOAD | I64LOAD | F32LOAD | F64LOAD | I32LOAD8S | I32LOAD8U | I32LOAD16S | I32LOAD16U - | I64LOAD8S | I64LOAD8U | I64LOAD16S | I64LOAD16U | I64LOAD32S | I64LOAD32U | I32STORE - | I64STORE | F32STORE | F64STORE | I32STORE8 | I32STORE16 | I64STORE8 | I64STORE16 - | I64STORE32 => Leb32x2, - - CURRENTMEMORY | GROWMEMORY => Byte1, - - I32CONST => Leb32x1, - I64CONST => Leb64x1, - F32CONST => Bytes4, - F64CONST => Bytes8, - - I32EQZ | I32EQ | I32NE | I32LTS | I32LTU | I32GTS | I32GTU | I32LES | I32LEU | I32GES - | I32GEU | I64EQZ | I64EQ | I64NE | I64LTS | I64LTU | I64GTS | I64GTU | I64LES | I64LEU - | I64GES | I64GEU | F32EQ | F32NE | F32LT | F32GT | F32LE | F32GE | F64EQ | F64NE - | F64LT | F64GT | F64LE | F64GE | I32CLZ | I32CTZ | I32POPCNT | I32ADD | I32SUB - | I32MUL | I32DIVS | I32DIVU | I32REMS | I32REMU | I32AND | I32OR | I32XOR | I32SHL - | I32SHRS | I32SHRU | I32ROTL | I32ROTR | I64CLZ | I64CTZ | I64POPCNT | I64ADD | I64SUB - | I64MUL | I64DIVS | I64DIVU | I64REMS | I64REMU | I64AND | I64OR | I64XOR | I64SHL - | I64SHRS | I64SHRU | I64ROTL | I64ROTR | F32ABS | F32NEG | F32CEIL | F32FLOOR - | F32TRUNC | F32NEAREST | F32SQRT | F32ADD | F32SUB | F32MUL | F32DIV | F32MIN | F32MAX - | F32COPYSIGN | F64ABS | F64NEG | F64CEIL | F64FLOOR | F64TRUNC | F64NEAREST | F64SQRT - | F64ADD | F64SUB | F64MUL | F64DIV | F64MIN | F64MAX | F64COPYSIGN | I32WRAPI64 - | I32TRUNCSF32 | I32TRUNCUF32 | I32TRUNCSF64 | I32TRUNCUF64 | I64EXTENDSI32 - | I64EXTENDUI32 | I64TRUNCSF32 | I64TRUNCUF32 | I64TRUNCSF64 | I64TRUNCUF64 - | F32CONVERTSI32 | F32CONVERTUI32 | F32CONVERTSI64 | F32CONVERTUI64 | F32DEMOTEF64 - | F64CONVERTSI32 | F64CONVERTUI32 | F64CONVERTSI64 | F64CONVERTUI64 | F64PROMOTEF32 - | I32REINTERPRETF32 | I64REINTERPRETF64 | F32REINTERPRETI32 | F64REINTERPRETI64 => { - NoImmediate - } - - // Catch-all in case of an invalid cast from u8 to OpCode while parsing binary - // (rustc keeps this code, I verified in Compiler Explorer) - #[allow(unreachable_patterns)] - _ => return Err(format!("Unknown Wasm instruction 0x{:02x}", op as u8)), - }; - - Ok(imm) -} - -impl SkipBytes for OpCode { - fn skip_bytes(bytes: &[u8], cursor: &mut usize) -> Result<(), ParseError> { - use OpImmediates::*; - - let opcode_byte: u8 = bytes[*cursor]; - - let opcode: OpCode = unsafe { std::mem::transmute(opcode_byte) }; - // will return Err if transmute was invalid - let immediates = immediates_for(opcode).map_err(|message| ParseError { - message, - offset: *cursor, - })?; - - match immediates { - NoImmediate => { - *cursor += 1; - } - Byte1 => { - *cursor += 1 + 1; - } - Bytes4 => { - *cursor += 1 + 4; - } - Bytes8 => { - *cursor += 1 + 8; - } - Leb32x1 => { - *cursor += 1; - u32::skip_bytes(bytes, cursor)?; - } - Leb64x1 => { - *cursor += 1; - u64::skip_bytes(bytes, cursor)?; - } - Leb32x2 => { - *cursor += 1; - u32::skip_bytes(bytes, cursor)?; - u32::skip_bytes(bytes, cursor)?; - } - BrTable => { - *cursor += 1; - let n_labels = 1 + u32::parse((), bytes, cursor)?; - for _ in 0..n_labels { - u32::skip_bytes(bytes, cursor)?; - } - } - } - Ok(()) - } -} diff --git a/compiler/gen_wasm/src/wasm_module/parse.rs b/compiler/gen_wasm/src/wasm_module/parse.rs deleted file mode 100644 index 968c65c6c7..0000000000 --- a/compiler/gen_wasm/src/wasm_module/parse.rs +++ /dev/null @@ -1,241 +0,0 @@ -use super::serialize::MAX_SIZE_ENCODED_U32; -use bumpalo::collections::vec::Vec; -use bumpalo::Bump; - -/// Parse serialized bytes into a data structure -/// Specific parsers may need contextual data from other parts of the .wasm file -pub trait Parse: Sized { - fn parse(ctx: ParseContext, bytes: &[u8], cursor: &mut usize) -> Result; -} - -#[derive(Debug)] -pub struct ParseError { - pub offset: usize, - pub message: String, -} - -/// Decode an unsigned 32-bit integer from the provided buffer in LEB-128 format -/// Return the integer itself and the offset after it ends -fn decode_u32(bytes: &[u8]) -> Result<(u32, usize), ()> { - let mut value = 0; - let mut shift = 0; - for (i, byte) in bytes.iter().take(MAX_SIZE_ENCODED_U32).enumerate() { - value += ((byte & 0x7f) as u32) << shift; - if (byte & 0x80) == 0 { - return Ok((value, i + 1)); - } - shift += 7; - } - Err(()) -} - -impl Parse<()> for u32 { - fn parse(_ctx: (), bytes: &[u8], cursor: &mut usize) -> Result { - match decode_u32(&bytes[*cursor..]) { - Ok((value, len)) => { - *cursor += len; - Ok(value) - } - Err(()) => Err(ParseError { - offset: *cursor, - message: format!( - "Failed to decode u32 as LEB-128 from bytes: {:2x?}", - &bytes[*cursor..][..MAX_SIZE_ENCODED_U32] - ), - }), - } - } -} - -impl Parse<()> for u8 { - fn parse(_ctx: (), bytes: &[u8], cursor: &mut usize) -> Result { - let byte = bytes[*cursor]; - *cursor += 1; - Ok(byte) - } -} - -/// Decode a signed 32-bit integer from the provided buffer in LEB-128 format -/// Return the integer itself and the offset after it ends -fn decode_i32(bytes: &[u8]) -> Result<(i32, usize), ()> { - let mut value = 0; - let mut shift = 0; - for (i, byte) in bytes.iter().take(MAX_SIZE_ENCODED_U32).enumerate() { - value |= ((byte & 0x7f) as i32) << shift; - if (byte & 0x80) == 0 { - let is_negative = byte & 0x40 != 0; - if shift < MAX_SIZE_ENCODED_U32 && is_negative { - value |= -1 << shift; - } - return Ok((value, i + 1)); - } - shift += 7; - } - Err(()) -} - -impl Parse<()> for i32 { - fn parse(_ctx: (), bytes: &[u8], cursor: &mut usize) -> Result { - match decode_i32(&bytes[*cursor..]) { - Ok((value, len)) => { - *cursor += len; - Ok(value) - } - Err(()) => Err(ParseError { - offset: *cursor, - message: format!( - "Failed to decode i32 as LEB-128 from bytes: {:2x?}", - &bytes[*cursor..][..MAX_SIZE_ENCODED_U32] - ), - }), - } - } -} - -impl<'a> Parse<&'a Bump> for &'a str { - fn parse(arena: &'a Bump, bytes: &[u8], cursor: &mut usize) -> Result { - let len = u32::parse((), bytes, cursor)?; - let end = *cursor + len as usize; - let bytes: &[u8] = &bytes[*cursor..end]; - let copy = arena.alloc_slice_copy(bytes); - let s = unsafe { std::str::from_utf8_unchecked(copy) }; - *cursor = end; - Ok(s) - } -} - -pub fn parse_variable_size_items<'a, T>( - arena: &'a Bump, - bytes: &[u8], - cursor: &mut usize, -) -> Result, ParseError> -where - T: Parse<&'a Bump>, -{ - let len = u32::parse((), bytes, cursor)?; - let mut vector: Vec<'a, T> = Vec::with_capacity_in(len as usize, arena); - for _ in 0..len { - let item = T::parse(arena, bytes, cursor)?; - vector.push(item); - } - Ok(vector) -} - -pub fn parse_fixed_size_items<'a, T>( - arena: &'a Bump, - bytes: &[u8], - cursor: &mut usize, -) -> Result, ParseError> -where - T: Parse<()>, -{ - let len = u32::parse((), bytes, cursor)?; - let mut vector: Vec<'a, T> = Vec::with_capacity_in(len as usize, arena); - for _ in 0..len { - let item = T::parse((), bytes, cursor)?; - vector.push(item); - } - Ok(vector) -} - -/// Skip over serialized bytes for a type -/// This may, or may not, require looking at the byte values -pub trait SkipBytes: Sized { - fn skip_bytes(bytes: &[u8], cursor: &mut usize) -> Result<(), ParseError>; -} - -impl SkipBytes for u32 { - fn skip_bytes(bytes: &[u8], cursor: &mut usize) -> Result<(), ParseError> { - const MAX_LEN: usize = 5; - for (i, byte) in bytes.iter().enumerate().skip(*cursor).take(MAX_LEN) { - if byte & 0x80 == 0 { - *cursor = i + 1; - return Ok(()); - } - } - Err(ParseError { - offset: *cursor, - message: "Invalid LEB encoding".into(), - }) - } -} - -impl SkipBytes for u64 { - fn skip_bytes(bytes: &[u8], cursor: &mut usize) -> Result<(), ParseError> { - const MAX_LEN: usize = 10; - for (i, byte) in bytes.iter().enumerate().skip(*cursor).take(MAX_LEN) { - if byte & 0x80 == 0 { - *cursor = i + 1; - return Ok(()); - } - } - Err(ParseError { - offset: *cursor, - message: "Invalid LEB encoding".into(), - }) - } -} - -impl SkipBytes for u8 { - fn skip_bytes(_bytes: &[u8], cursor: &mut usize) -> Result<(), ParseError> { - *cursor += 1; - Ok(()) - } -} - -/// Note: This is just for skipping over Wasm bytes. We don't actually care about String vs str! -impl SkipBytes for String { - fn skip_bytes(bytes: &[u8], cursor: &mut usize) -> Result<(), ParseError> { - let len = u32::parse((), bytes, cursor)?; - - if false { - let str_bytes = &bytes[*cursor..(*cursor + len as usize)]; - println!( - "Skipping string {:?}", - std::str::from_utf8(str_bytes).unwrap() - ); - } - - *cursor += len as usize; - Ok(()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::wasm_module::parse::decode_u32; - - #[test] - fn test_decode_u32() { - assert_eq!(decode_u32(&[0]), Ok((0, 1))); - assert_eq!(decode_u32(&[64]), Ok((64, 1))); - assert_eq!(decode_u32(&[0x7f]), Ok((0x7f, 1))); - assert_eq!(decode_u32(&[0x80, 0x01]), Ok((0x80, 2))); - assert_eq!(decode_u32(&[0xff, 0x7f]), Ok((0x3fff, 2))); - assert_eq!(decode_u32(&[0x80, 0x80, 0x01]), Ok((0x4000, 3))); - assert_eq!( - decode_u32(&[0xff, 0xff, 0xff, 0xff, 0x0f]), - Ok((u32::MAX, MAX_SIZE_ENCODED_U32)) - ); - assert!(matches!(decode_u32(&[0x80; 6]), Err(_))); - assert!(matches!(decode_u32(&[0x80; 2]), Err(_))); - assert!(matches!(decode_u32(&[]), Err(_))); - } - - #[test] - fn test_parse_u32_sequence() { - let bytes = &[0, 0x80, 0x01, 0xff, 0xff, 0xff, 0xff, 0x0f]; - let expected = [0, 128, u32::MAX]; - let mut cursor = 0; - - assert_eq!(u32::parse((), bytes, &mut cursor).unwrap(), expected[0]); - assert_eq!(cursor, 1); - - assert_eq!(u32::parse((), bytes, &mut cursor).unwrap(), expected[1]); - assert_eq!(cursor, 3); - - assert_eq!(u32::parse((), bytes, &mut cursor).unwrap(), expected[2]); - assert_eq!(cursor, 8); - } -} diff --git a/compiler/ident/Cargo.toml b/compiler/ident/Cargo.toml deleted file mode 100644 index 70987df20e..0000000000 --- a/compiler/ident/Cargo.toml +++ /dev/null @@ -1,6 +0,0 @@ -[package] -name = "roc_ident" -version = "0.1.0" -authors = ["The Roc Contributors"] -license = "UPL-1.0" -edition = "2021" diff --git a/compiler/ident/src/lib.rs b/compiler/ident/src/lib.rs deleted file mode 100644 index eded3502e7..0000000000 --- a/compiler/ident/src/lib.rs +++ /dev/null @@ -1,396 +0,0 @@ -#![warn(clippy::dbg_macro)] - -use core::cmp::Ordering; -use core::convert::From; -use core::{fmt, mem, ptr, slice}; -use std::alloc::{alloc, dealloc, Layout}; -use std::os::raw::c_char; - -/// A string which can store identifiers using the small string optimization. -/// It relies on the invariant that it cannot store null characters to store -/// an extra character; if the last byte is 0, that means it's a large string. -/// -/// Because the msbyte of the length is always 0, this can only store up to -/// 2^56 bytes on a 64-bit target, or 2^28 bytes in a 32-bit target. That's -/// way more than enough for an identifier! -/// -/// If it's a small string, that discriminant byte is used to store the length, -/// except it stores it as (255 - length) so that it will be in the range -/// 192 - 255 (all of which are invalid UTF-8 when in the final position of -/// a UTF-8 string). This design works on little-endian targets, but a different -/// design for storing length might be necessary on big-endian targets. - -#[repr(C)] -pub struct IdentStr { - elements: *const u8, - length: usize, -} - -impl IdentStr { - // Reserve 1 byte for the discriminant - const SMALL_STR_BYTES: usize = std::mem::size_of::() - 1; - - #[inline(always)] - pub const fn len(&self) -> usize { - let bytes = self.length.to_ne_bytes(); - let last_byte = bytes[mem::size_of::() - 1]; - - // We always perform this subtraction so that the following - // conditionals can all be cmov instructions. - let small_str_variable_len = (u8::MAX - last_byte) as usize; - - // The numbers 192 - 255 (0xC0 - 0xFF) are not valid as the final - // byte of a UTF-8 string. Hence they are unused and we can use them - // to store the length of a small string! - // - // Reference: https://en.wikipedia.org/wiki/UTF-8#Codepage_layout - if last_byte >= 0xC0 { - small_str_variable_len - } else if last_byte == 0 { - // This is a big string, so return its length. - self.length - } else { - // This is a valid UTF-8 character, meaning the entire struct must - // be in use for storing characters. - mem::size_of::() - } - } - - pub const fn is_empty(&self) -> bool { - self.length == 0 - } - - pub const fn is_small_str(&self) -> bool { - let bytes = self.length.to_ne_bytes(); - let last_byte = bytes[mem::size_of::() - 1]; - - last_byte != 0 - } - - pub fn get(&self, index: usize) -> Option<&u8> { - self.as_bytes().get(index) - } - - pub fn get_bytes(&self) -> *const u8 { - if self.is_small_str() { - self.get_small_str_ptr() - } else { - self.elements - } - } - - fn get_small_str_ptr(&self) -> *const u8 { - (self as *const IdentStr).cast() - } - - #[inline(always)] - const fn small_str_from_bytes(slice: &[u8]) -> Self { - assert!(slice.len() <= Self::SMALL_STR_BYTES); - - let len = slice.len(); - let mut bytes = [0; mem::size_of::()]; - - // Copy the bytes from the slice into bytes. - // while because for/Iterator does not work in const context - let mut i = 0; - while i < len { - bytes[i] = slice[i]; - i += 1; - } - - // Write length and small string bit to last byte of length. - bytes[Self::SMALL_STR_BYTES] = u8::MAX - len as u8; - - unsafe { mem::transmute::<[u8; mem::size_of::()], Self>(bytes) } - } - - #[allow(clippy::should_implement_trait)] - pub fn from_str(str: &str) -> Self { - let slice = str.as_bytes(); - let len = slice.len(); - - match len.cmp(&mem::size_of::()) { - Ordering::Less => Self::small_str_from_bytes(slice), - Ordering::Equal => { - // This fits in a small string, and is exactly long enough to - // take up the entire available struct - let mut bytes = [0; mem::size_of::()]; - - // Copy the bytes from the slice into the answer - bytes.copy_from_slice(slice); - - unsafe { mem::transmute::<[u8; mem::size_of::()], Self>(bytes) } - } - Ordering::Greater => { - // This needs a big string - let align = mem::align_of::(); - let elements = unsafe { - let layout = Layout::from_size_align_unchecked(len, align); - alloc(layout) - }; - - // Turn the new elements into a slice, and copy the existing - // slice's bytes into it. - unsafe { - let dest_slice = slice::from_raw_parts_mut(elements, len); - - dest_slice.copy_from_slice(slice); - } - - Self { - length: len, - elements, - } - } - } - } - - #[inline(always)] - pub fn as_slice(&self) -> &[u8] { - use core::slice::from_raw_parts; - - if self.is_empty() { - &[] - } else if self.is_small_str() { - unsafe { from_raw_parts(self.get_small_str_ptr(), self.len()) } - } else { - unsafe { from_raw_parts(self.elements, self.length) } - } - } - - #[inline(always)] - pub fn as_str(&self) -> &str { - let slice = self.as_slice(); - - unsafe { core::str::from_utf8_unchecked(slice) } - } - - /// Write a CStr (null-terminated) representation of this IdentStr into - /// the given buffer. - /// - /// # Safety - /// This assumes the given buffer has enough space, so make sure you only - /// pass in a pointer to an allocation that's at least as long as this Str! - pub unsafe fn write_c_str(&self, buf: *mut c_char) { - let bytes = self.as_bytes(); - ptr::copy_nonoverlapping(bytes.as_ptr().cast(), buf, bytes.len()); - - // null-terminate - *buf.add(self.len()) = 0; - } -} - -impl Default for IdentStr { - fn default() -> Self { - Self { - length: 0, - elements: core::ptr::null_mut(), - } - } -} - -impl std::ops::Deref for IdentStr { - type Target = str; - - fn deref(&self) -> &Self::Target { - self.as_str() - } -} - -impl From<&str> for IdentStr { - fn from(str: &str) -> Self { - Self::from_str(str) - } -} - -impl From for IdentStr { - fn from(string: String) -> Self { - if string.len() <= Self::SMALL_STR_BYTES { - Self::from_str(string.as_str()) - } else { - // Take over the string's heap allocation - let length = string.len(); - let elements = string.as_ptr(); - - // Make sure the existing string doesn't get dropped. - std::mem::forget(string); - - Self { elements, length } - } - } -} - -impl fmt::Debug for IdentStr { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - // IdentStr { is_small_str: false, storage: Refcounted(3), elements: [1,2,3,4] } - f.debug_struct("IdentStr") - //.field("is_small_str", &self.is_small_str()) - .field("string", &self.as_str()) - //.field("elements", &self.as_slice()) - .finish() - } -} - -impl fmt::Display for IdentStr { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - // IdentStr { is_small_str: false, storage: Refcounted(3), elements: [1,2,3,4] } - f.write_str(self.as_str()) - } -} - -unsafe impl std::marker::Sync for IdentStr {} -unsafe impl std::marker::Send for IdentStr {} - -impl PartialEq for IdentStr { - fn eq(&self, other: &Self) -> bool { - self.as_slice() == other.as_slice() - } -} - -impl Eq for IdentStr {} - -impl PartialOrd for IdentStr { - fn partial_cmp(&self, other: &Self) -> Option { - self.as_str().partial_cmp(other.as_str()) - } -} - -impl Ord for IdentStr { - fn cmp(&self, other: &Self) -> std::cmp::Ordering { - self.as_str().cmp(other.as_str()) - } -} - -impl std::hash::Hash for IdentStr { - fn hash(&self, hasher: &mut H) - where - H: std::hash::Hasher, - { - self.as_str().hash(hasher) - } -} - -impl Clone for IdentStr { - fn clone(&self) -> Self { - Self::from_str(self.as_str()) - } -} - -impl Drop for IdentStr { - fn drop(&mut self) { - if !self.is_empty() && !self.is_small_str() { - let align = mem::align_of::(); - unsafe { - let layout = Layout::from_size_align_unchecked(self.length, align); - dealloc(self.elements as *mut _, layout); - } - } - } -} - -#[test] -fn default() { - let answer = IdentStr::default(); - - assert_eq!(answer.len(), 0); - assert_eq!(answer, answer); - assert_eq!(answer.clone(), answer); - assert_eq!(answer, answer); - assert_eq!(answer.as_str(), ""); - assert_eq!(answer.as_str(), ""); -} - -#[test] -fn big_str() { - for &string in &[ - "0123456789abcdefg", - "0123456789abcdefgh", - "0123456789abcdefghi", - ] { - let answer = IdentStr::from(string); - - assert_eq!(answer.len(), string.len()); - assert_eq!(answer, answer); - assert_eq!(answer.clone(), answer); - assert_eq!(answer.clone(), answer.clone()); - assert_eq!(answer.as_str(), string); - assert_eq!(answer.clone().as_str(), string); - } -} - -#[cfg(target_pointer_width = "64")] -#[test] -fn small_var_length() { - for &string in &[ - "", - "0", - "01", - "012", - "0123", - "01234", - "012345", - "0123456", - "01234567", - "012345678", - "0123456789", - "0123456789a", - "0123456789ab", - "0123456789abc", - "0123456789abcd", - "0123456789abcde ", - ] { - let answer = IdentStr::from(string); - - assert_eq!(answer.len(), string.len()); - assert_eq!(answer, answer); - assert_eq!(answer.clone(), answer); - assert_eq!(answer.clone(), answer.clone()); - assert_eq!(answer.as_str(), string); - assert_eq!(answer.clone().as_str(), string); - } -} - -#[cfg(target_pointer_width = "32")] -#[test] -fn small_var_length() { - for &string in &[ - "", "0", "01", "012", "0123", "01234", "012345", "0123456", "01234567", - ] { - let answer = IdentStr::from(string); - - assert_eq!(answer.len(), string.len()); - assert_eq!(answer, answer); - assert_eq!(answer.clone(), answer); - assert_eq!(answer.clone(), answer.clone()); - assert_eq!(answer.as_str(), string); - assert_eq!(answer.clone().as_str(), string); - } -} - -#[cfg(target_pointer_width = "64")] -#[test] -fn small_max_length() { - let string = "0123456789abcdef"; - let answer = IdentStr::from(string); - - assert_eq!(answer.len(), string.len()); - assert_eq!(answer, answer); - assert_eq!(answer.clone(), answer); - assert_eq!(answer, answer); - assert_eq!(answer.as_str(), string); - assert_eq!(answer.as_str(), string); -} - -#[cfg(target_pointer_width = "32")] -#[test] -fn small_max_length() { - let string = "01234567"; - let answer = IdentStr::from(string); - - assert_eq!(answer.len(), string.len()); - assert_eq!(answer, answer); - assert_eq!(answer.clone(), answer); - assert_eq!(answer.clone(), answer.clone()); - assert_eq!(answer.as_str(), string); - assert_eq!(answer.clone().as_str(), string); -} diff --git a/compiler/late_solve/Cargo.toml b/compiler/late_solve/Cargo.toml deleted file mode 100644 index 97e5b3ac31..0000000000 --- a/compiler/late_solve/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "roc_late_solve" -version = "0.1.0" -authors = ["The Roc Contributors"] -license = "UPL-1.0" -edition = "2021" - -[dependencies] -roc_types = { path = "../types" } -roc_can = { path = "../can" } -roc_unify = { path = "../unify" } -roc_solve = { path = "../solve" } -bumpalo = { version = "3.8.0", features = ["collections"] } diff --git a/compiler/late_solve/src/lib.rs b/compiler/late_solve/src/lib.rs deleted file mode 100644 index 3870be3334..0000000000 --- a/compiler/late_solve/src/lib.rs +++ /dev/null @@ -1,52 +0,0 @@ -//! Crate roc_late_solve exposes type unification and solving primitives from the perspective of -//! the compiler backend. - -use bumpalo::Bump; -use roc_can::abilities::AbilitiesStore; -use roc_solve::solve::{compact_lambda_sets_of_vars, Phase, Pools}; -use roc_types::subs::{Subs, Variable}; -use roc_unify::unify::{unify as unify_unify, Mode, Unified}; - -#[derive(Debug)] -pub struct UnificationFailed; - -/// Unifies two variables and performs lambda set compaction. -/// Ranks and other ability demands are disregarded. -pub fn unify( - arena: &Bump, - subs: &mut Subs, - abilities_store: &AbilitiesStore, - left: Variable, - right: Variable, -) -> Result<(), UnificationFailed> { - let unified = unify_unify(subs, left, right, Mode::EQ); - match unified { - Unified::Success { - vars: _, - must_implement_ability: _, - lambda_sets_to_specialize, - } => { - let mut pools = Pools::default(); - compact_lambda_sets_of_vars( - subs, - arena, - &mut pools, - abilities_store, - lambda_sets_to_specialize, - Phase::Late, - ); - // Pools are only used to keep track of variable ranks for generalization purposes. - // Since we break generalization during monomorphization, `pools` is irrelevant - // here. We only need it for `compact_lambda_sets_of_vars`, which is also used in a - // solving context where pools are relevant. - - Ok(()) - } - Unified::Failure(..) | Unified::BadType(..) => Err(UnificationFailed), - } -} - -pub use roc_solve::solve::instantiate_rigids; - -pub use roc_solve::ability::resolve_ability_specialization; -pub use roc_solve::ability::Resolved; diff --git a/compiler/load/Cargo.toml b/compiler/load/Cargo.toml deleted file mode 100644 index ec3fae6f05..0000000000 --- a/compiler/load/Cargo.toml +++ /dev/null @@ -1,24 +0,0 @@ -[package] -name = "roc_load" -version = "0.1.0" -authors = ["The Roc Contributors"] -license = "UPL-1.0" -edition = "2021" - -[dependencies] -roc_load_internal = { path = "../load_internal" } -bumpalo = { version = "3.8.0", features = ["collections"] } -roc_target = { path = "../roc_target" } -roc_constrain= { path = "../constrain" } -roc_types = { path = "../types" } -roc_module = { path = "../module" } -roc_collections = { path = "../collections" } -roc_reporting = { path = "../../reporting" } - -[build-dependencies] -roc_load_internal = { path = "../load_internal" } -roc_builtins = { path = "../builtins" } -roc_module = { path = "../module" } -roc_reporting = { path = "../../reporting" } -roc_target = { path = "../roc_target" } -bumpalo = { version = "3.8.0", features = ["collections"] } diff --git a/compiler/load/build.rs b/compiler/load/build.rs deleted file mode 100644 index 0ec18fa578..0000000000 --- a/compiler/load/build.rs +++ /dev/null @@ -1,55 +0,0 @@ -use std::path::PathBuf; - -use bumpalo::Bump; -use roc_load_internal::file::Threading; -use roc_module::symbol::ModuleId; - -const MODULES: &[(ModuleId, &str)] = &[ - (ModuleId::BOOL, "Bool.roc"), - (ModuleId::RESULT, "Result.roc"), - (ModuleId::NUM, "Num.roc"), - (ModuleId::LIST, "List.roc"), - (ModuleId::STR, "Str.roc"), - (ModuleId::DICT, "Dict.roc"), - (ModuleId::SET, "Set.roc"), - (ModuleId::BOX, "Box.roc"), - (ModuleId::ENCODE, "Encode.roc"), - (ModuleId::JSON, "Json.roc"), -]; - -fn main() { - for (module_id, filename) in MODULES { - write_subs_for_module(*module_id, filename); - } -} - -fn write_subs_for_module(module_id: ModuleId, filename: &str) { - // Tell Cargo that if the given file changes, to rerun this build script. - println!("cargo:rerun-if-changed=../builtins/roc/{}", filename); - - let arena = Bump::new(); - let src_dir = PathBuf::from("."); - let source = roc_builtins::roc::module_source(module_id); - let target_info = roc_target::TargetInfo::default_x86_64(); - - let res_module = roc_load_internal::file::load_and_typecheck_str( - &arena, - PathBuf::from(filename), - source, - &src_dir, - Default::default(), - target_info, - roc_reporting::report::RenderTarget::ColorTerminal, - Threading::AllAvailable, - ); - - let module = res_module.unwrap(); - let subs = module.solved.inner(); - let exposed_vars_by_symbol: Vec<_> = module.exposed_to_host.into_iter().collect(); - - let mut output_path = PathBuf::from(std::env::var("OUT_DIR").unwrap()); - output_path.extend(&[filename]); - output_path.set_extension("dat"); - let mut file = std::fs::File::create(&output_path).unwrap(); - subs.serialize(&exposed_vars_by_symbol, &mut file).unwrap(); -} diff --git a/compiler/load/src/lib.rs b/compiler/load/src/lib.rs deleted file mode 100644 index 9548d95889..0000000000 --- a/compiler/load/src/lib.rs +++ /dev/null @@ -1,217 +0,0 @@ -pub use roc_load_internal::file::Threading; - -use bumpalo::Bump; -use roc_collections::all::MutMap; -use roc_constrain::module::ExposedByModule; -use roc_module::symbol::{ModuleId, Symbol}; -use roc_reporting::report::RenderTarget; -use roc_target::TargetInfo; -use roc_types::subs::{Subs, Variable}; -use std::path::{Path, PathBuf}; - -pub use roc_load_internal::docs; -pub use roc_load_internal::file::{ - LoadResult, LoadStart, LoadedModule, LoadingProblem, MonomorphizedModule, Phase, -}; - -#[allow(clippy::too_many_arguments)] -fn load<'a>( - arena: &'a Bump, - load_start: LoadStart<'a>, - src_dir: &Path, - exposed_types: ExposedByModule, - goal_phase: Phase, - target_info: TargetInfo, - render: RenderTarget, - threading: Threading, -) -> Result, LoadingProblem<'a>> { - let cached_subs = read_cached_subs(); - - roc_load_internal::file::load( - arena, - load_start, - src_dir, - exposed_types, - goal_phase, - target_info, - cached_subs, - render, - threading, - ) -} - -/// Load using only a single thread; used when compiling to webassembly -pub fn load_single_threaded<'a>( - arena: &'a Bump, - load_start: LoadStart<'a>, - src_dir: &Path, - exposed_types: ExposedByModule, - goal_phase: Phase, - target_info: TargetInfo, - render: RenderTarget, -) -> Result, LoadingProblem<'a>> { - let cached_subs = read_cached_subs(); - - roc_load_internal::file::load_single_threaded( - arena, - load_start, - src_dir, - exposed_types, - goal_phase, - target_info, - cached_subs, - render, - ) -} - -#[allow(clippy::too_many_arguments)] -pub fn load_and_monomorphize_from_str<'a>( - arena: &'a Bump, - filename: PathBuf, - src: &'a str, - src_dir: &Path, - exposed_types: ExposedByModule, - target_info: TargetInfo, - render: RenderTarget, - threading: Threading, -) -> Result, LoadingProblem<'a>> { - use LoadResult::*; - - let load_start = LoadStart::from_str(arena, filename, src)?; - - match load( - arena, - load_start, - src_dir, - exposed_types, - Phase::MakeSpecializations, - target_info, - render, - threading, - )? { - Monomorphized(module) => Ok(module), - TypeChecked(_) => unreachable!(""), - } -} - -pub fn load_and_monomorphize<'a>( - arena: &'a Bump, - filename: PathBuf, - src_dir: &Path, - exposed_types: ExposedByModule, - target_info: TargetInfo, - render: RenderTarget, - threading: Threading, -) -> Result, LoadingProblem<'a>> { - use LoadResult::*; - - let load_start = LoadStart::from_path(arena, filename, render)?; - - match load( - arena, - load_start, - src_dir, - exposed_types, - Phase::MakeSpecializations, - target_info, - render, - threading, - )? { - Monomorphized(module) => Ok(module), - TypeChecked(_) => unreachable!(""), - } -} - -pub fn load_and_typecheck<'a>( - arena: &'a Bump, - filename: PathBuf, - src_dir: &Path, - exposed_types: ExposedByModule, - target_info: TargetInfo, - render: RenderTarget, - threading: Threading, -) -> Result> { - use LoadResult::*; - - let load_start = LoadStart::from_path(arena, filename, render)?; - - match load( - arena, - load_start, - src_dir, - exposed_types, - Phase::SolveTypes, - target_info, - render, - threading, - )? { - Monomorphized(_) => unreachable!(""), - TypeChecked(module) => Ok(module), - } -} - -pub fn load_and_typecheck_str<'a>( - arena: &'a Bump, - filename: PathBuf, - source: &'a str, - src_dir: &Path, - exposed_types: ExposedByModule, - target_info: TargetInfo, - render: RenderTarget, -) -> Result> { - use LoadResult::*; - - let load_start = LoadStart::from_str(arena, filename, source)?; - - // NOTE: this function is meant for tests, and so we use single-threaded - // solving so we don't use too many threads per-test. That gives higher - // throughput for the test run overall - match load_single_threaded( - arena, - load_start, - src_dir, - exposed_types, - Phase::SolveTypes, - target_info, - render, - )? { - Monomorphized(_) => unreachable!(""), - TypeChecked(module) => Ok(module), - } -} - -const BOOL: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/Bool.dat")) as &[_]; -const RESULT: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/Result.dat")) as &[_]; -const LIST: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/List.dat")) as &[_]; -const STR: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/Str.dat")) as &[_]; -const DICT: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/Dict.dat")) as &[_]; -const SET: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/Set.dat")) as &[_]; -const BOX: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/Box.dat")) as &[_]; -const NUM: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/Num.dat")) as &[_]; - -fn deserialize_help(bytes: &[u8]) -> (Subs, Vec<(Symbol, Variable)>) { - let (subs, slice) = Subs::deserialize(bytes); - - (subs, slice.to_vec()) -} - -fn read_cached_subs() -> MutMap)> { - let mut output = MutMap::default(); - - // Wasm seems to re-order definitions between build time and runtime, but only in release mode. - // That is very strange, but we can solve it separately - if !cfg!(target_family = "wasm") { - output.insert(ModuleId::BOOL, deserialize_help(BOOL)); - output.insert(ModuleId::RESULT, deserialize_help(RESULT)); - output.insert(ModuleId::NUM, deserialize_help(NUM)); - - output.insert(ModuleId::LIST, deserialize_help(LIST)); - output.insert(ModuleId::STR, deserialize_help(STR)); - output.insert(ModuleId::DICT, deserialize_help(DICT)); - - output.insert(ModuleId::SET, deserialize_help(SET)); - output.insert(ModuleId::BOX, deserialize_help(BOX)); - } - - output -} diff --git a/compiler/load_internal/Cargo.toml b/compiler/load_internal/Cargo.toml deleted file mode 100644 index 1e171f226b..0000000000 --- a/compiler/load_internal/Cargo.toml +++ /dev/null @@ -1,34 +0,0 @@ -[package] -name = "roc_load_internal" -version = "0.1.0" -authors = ["The Roc Contributors"] -license = "UPL-1.0" -edition = "2021" - -[dependencies] -roc_collections = { path = "../collections" } -roc_error_macros = { path = "../../error_macros" } -roc_region = { path = "../region" } -roc_module = { path = "../module" } -roc_types = { path = "../types" } -roc_can = { path = "../can" } -roc_constrain = { path = "../constrain" } -roc_builtins = { path = "../builtins" } -roc_problem = { path = "../problem" } -roc_unify = { path = "../unify" } -roc_parse = { path = "../parse" } -roc_solve = { path = "../solve" } -roc_mono = { path = "../mono" } -roc_target = { path = "../roc_target" } -roc_reporting = { path = "../../reporting" } -roc_debug_flags = { path = "../debug_flags" } -ven_pretty = { path = "../../vendor/pretty" } -bumpalo = { version = "3.8.0", features = ["collections"] } -parking_lot = "0.12" -crossbeam = "0.8.1" - -[dev-dependencies] -pretty_assertions = "1.0.0" -maplit = "1.0.2" -indoc = "1.0.3" -roc_test_utils = { path = "../../test_utils" } diff --git a/compiler/load_internal/src/docs.rs b/compiler/load_internal/src/docs.rs deleted file mode 100644 index c721e19f87..0000000000 --- a/compiler/load_internal/src/docs.rs +++ /dev/null @@ -1,457 +0,0 @@ -use crate::docs::DocEntry::DetachedDoc; -use crate::docs::TypeAnnotation::{Apply, BoundVariable, Function, NoTypeAnn, Record, TagUnion}; -use crate::file::LoadedModule; -use roc_can::scope::Scope; -use roc_module::ident::ModuleName; -use roc_module::symbol::IdentIds; -use roc_parse::ast::AssignedField; -use roc_parse::ast::{self, ExtractSpaces, TypeHeader}; -use roc_parse::ast::{CommentOrNewline, TypeDef, ValueDef}; - -// Documentation generation requirements - -#[derive(Debug)] -pub struct Documentation { - pub name: String, - pub version: String, - pub docs: String, - pub modules: Vec, -} - -#[derive(Debug)] -pub struct ModuleDocumentation { - pub name: String, - pub entries: Vec, - pub scope: Scope, -} - -#[derive(Debug, Clone)] -pub enum DocEntry { - DocDef(DocDef), - DetachedDoc(String), -} - -#[derive(Debug, Clone)] -pub struct DocDef { - pub name: String, - pub type_vars: Vec, - pub type_annotation: TypeAnnotation, - pub docs: Option, -} - -#[derive(Debug, Clone)] -pub enum TypeAnnotation { - TagUnion { - tags: Vec, - extension: Box, - }, - Function { - args: Vec, - output: Box, - }, - ObscuredTagUnion, - ObscuredRecord, - BoundVariable(String), - Apply { - name: String, - parts: Vec, - }, - Record { - fields: Vec, - extension: Box, - }, - Ability { - members: Vec, - }, - Wildcard, - NoTypeAnn, -} - -#[derive(Debug, Clone)] -pub enum RecordField { - RecordField { - name: String, - type_annotation: TypeAnnotation, - }, - OptionalField { - name: String, - type_annotation: TypeAnnotation, - }, - LabelOnly { - name: String, - }, -} - -#[derive(Debug, Clone)] -pub struct AbilityMember { - pub name: String, - pub type_annotation: TypeAnnotation, - pub able_variables: Vec<(String, TypeAnnotation)>, - pub docs: Option, -} - -#[derive(Debug, Clone)] -pub struct Tag { - pub name: String, - pub values: Vec, -} - -pub fn generate_module_docs( - scope: Scope, - module_name: ModuleName, - parsed_defs: &roc_parse::ast::Defs, -) -> ModuleDocumentation { - let entries = generate_entry_docs(&scope.locals.ident_ids, parsed_defs); - - ModuleDocumentation { - name: module_name.as_str().to_string(), - scope, - entries, - } -} - -fn detached_docs_from_comments_and_new_lines<'a>( - comments_or_new_lines: &'a [roc_parse::ast::CommentOrNewline<'a>], -) -> Vec { - let mut detached_docs: Vec = Vec::new(); - - let mut docs = String::new(); - - for comment_or_new_line in comments_or_new_lines.iter() { - match comment_or_new_line { - CommentOrNewline::DocComment(doc_str) => { - docs.push_str(doc_str); - docs.push('\n'); - } - - CommentOrNewline::LineComment(_) | CommentOrNewline::Newline => { - if !docs.is_empty() { - detached_docs.push(docs.clone()); - } - - docs = String::new(); - } - } - } - - detached_docs -} - -fn generate_entry_docs<'a>( - ident_ids: &'a IdentIds, - defs: &roc_parse::ast::Defs<'a>, -) -> Vec { - use roc_parse::ast::Pattern; - - let mut acc = Vec::with_capacity(defs.tags.len()); - let mut before_comments_or_new_lines = None; - - for (index, either_index) in defs.tags.iter().enumerate() { - let spaces_before = &defs.spaces[defs.space_before[index].indices()]; - - for detached_doc in detached_docs_from_comments_and_new_lines(spaces_before) { - acc.push(DetachedDoc(detached_doc)); - } - - match either_index.split() { - Err(value_index) => match &defs.value_defs[value_index.index()] { - ValueDef::Annotation(loc_pattern, loc_ann) => { - if let Pattern::Identifier(identifier) = loc_pattern.value { - // Check if the definition is exposed - if ident_ids.get_id(&identifier.into()).is_some() { - let name = identifier.to_string(); - let doc_def = DocDef { - name, - type_annotation: type_to_docs(false, loc_ann.value), - type_vars: Vec::new(), - docs: before_comments_or_new_lines - .and_then(comments_or_new_lines_to_docs), - }; - acc.push(DocEntry::DocDef(doc_def)); - } - } - } - - ValueDef::AnnotatedBody { - ann_pattern, - ann_type, - .. - } => { - if let Pattern::Identifier(identifier) = ann_pattern.value { - // Check if the definition is exposed - if ident_ids.get_id(&identifier.into()).is_some() { - let doc_def = DocDef { - name: identifier.to_string(), - type_annotation: type_to_docs(false, ann_type.value), - type_vars: Vec::new(), - docs: before_comments_or_new_lines - .and_then(comments_or_new_lines_to_docs), - }; - acc.push(DocEntry::DocDef(doc_def)); - } - } - } - - ValueDef::Body(_, _) => (), - - ValueDef::Expect(c) => todo!("documentation for tests {:?}", c), - }, - Ok(type_index) => match &defs.type_defs[type_index.index()] { - TypeDef::Alias { - header: TypeHeader { name, vars }, - ann, - } => { - let mut type_vars = Vec::new(); - - for var in vars.iter() { - if let Pattern::Identifier(ident_name) = var.value { - type_vars.push(ident_name.to_string()); - } - } - - let doc_def = DocDef { - name: name.value.to_string(), - type_annotation: type_to_docs(false, ann.value), - type_vars, - docs: before_comments_or_new_lines.and_then(comments_or_new_lines_to_docs), - }; - acc.push(DocEntry::DocDef(doc_def)); - } - - TypeDef::Opaque { - header: TypeHeader { name, vars }, - .. - } => { - let mut type_vars = Vec::new(); - - for var in vars.iter() { - if let Pattern::Identifier(ident_name) = var.value { - type_vars.push(ident_name.to_string()); - } - } - - let doc_def = DocDef { - name: name.value.to_string(), - type_annotation: TypeAnnotation::NoTypeAnn, - type_vars, - docs: before_comments_or_new_lines.and_then(comments_or_new_lines_to_docs), - }; - acc.push(DocEntry::DocDef(doc_def)); - } - - TypeDef::Ability { - header: TypeHeader { name, vars }, - members, - .. - } => { - let mut type_vars = Vec::new(); - - for var in vars.iter() { - if let Pattern::Identifier(ident_name) = var.value { - type_vars.push(ident_name.to_string()); - } - } - - let members = members - .iter() - .map(|mem| { - let extracted = mem.name.value.extract_spaces(); - let (type_annotation, able_variables) = - ability_member_type_to_docs(mem.typ.value); - - AbilityMember { - name: extracted.item.to_string(), - type_annotation, - able_variables, - docs: comments_or_new_lines_to_docs(extracted.before), - } - }) - .collect(); - - let doc_def = DocDef { - name: name.value.to_string(), - type_annotation: TypeAnnotation::Ability { members }, - type_vars, - docs: before_comments_or_new_lines.and_then(comments_or_new_lines_to_docs), - }; - acc.push(DocEntry::DocDef(doc_def)); - } - }, - } - - before_comments_or_new_lines = Some(&defs.spaces[defs.space_after[index].indices()]); - } - - acc -} - -fn type_to_docs(in_func_type_ann: bool, type_annotation: ast::TypeAnnotation) -> TypeAnnotation { - match type_annotation { - ast::TypeAnnotation::TagUnion { tags, ext } => { - let mut tags_to_render: Vec = Vec::new(); - - for tag in tags.iter() { - if let Some(tag_ann) = tag_to_doc(in_func_type_ann, tag.value) { - tags_to_render.push(tag_ann); - } - } - - let extension = match ext { - None => NoTypeAnn, - Some(ext_type_ann) => type_to_docs(in_func_type_ann, ext_type_ann.value), - }; - - TagUnion { - tags: tags_to_render, - extension: Box::new(extension), - } - } - ast::TypeAnnotation::BoundVariable(var_name) => BoundVariable(var_name.to_string()), - ast::TypeAnnotation::Apply(module_name, type_name, type_ann_parts) => { - let mut name = String::new(); - - if !module_name.is_empty() { - name.push_str(module_name); - name.push('.'); - } - - name.push_str(type_name); - - let mut parts: Vec = Vec::new(); - - for type_ann_part in type_ann_parts { - parts.push(type_to_docs(in_func_type_ann, type_ann_part.value)); - } - - Apply { name, parts } - } - ast::TypeAnnotation::Record { fields, ext } => { - let mut doc_fields = Vec::new(); - - for field in fields.items { - if let Some(doc_field) = record_field_to_doc(in_func_type_ann, field.value) { - doc_fields.push(doc_field); - } - } - let extension = match ext { - None => NoTypeAnn, - Some(ext_type_ann) => type_to_docs(in_func_type_ann, ext_type_ann.value), - }; - - Record { - fields: doc_fields, - extension: Box::new(extension), - } - } - ast::TypeAnnotation::SpaceBefore(&sub_type_ann, _) => { - type_to_docs(in_func_type_ann, sub_type_ann) - } - ast::TypeAnnotation::SpaceAfter(&sub_type_ann, _) => { - type_to_docs(in_func_type_ann, sub_type_ann) - } - ast::TypeAnnotation::Function(ast_arg_anns, output_ann) => { - let mut doc_arg_anns = Vec::new(); - - for ast_arg_ann in ast_arg_anns { - doc_arg_anns.push(type_to_docs(true, ast_arg_ann.value)); - } - - Function { - args: doc_arg_anns, - output: Box::new(type_to_docs(true, output_ann.value)), - } - } - ast::TypeAnnotation::Wildcard => TypeAnnotation::Wildcard, - _ => NoTypeAnn, - } -} - -fn ability_member_type_to_docs( - type_annotation: ast::TypeAnnotation, -) -> (TypeAnnotation, Vec<(String, TypeAnnotation)>) { - match type_annotation { - ast::TypeAnnotation::Where(ta, has_clauses) => { - let ta = type_to_docs(false, ta.value); - let has_clauses = has_clauses - .iter() - .map(|hc| { - let ast::HasClause { var, ability } = hc.value; - ( - var.value.extract_spaces().item.to_string(), - type_to_docs(false, ability.value), - ) - }) - .collect(); - - (ta, has_clauses) - } - _ => (type_to_docs(false, type_annotation), vec![]), - } -} - -fn record_field_to_doc( - in_func_ann: bool, - field: ast::AssignedField<'_, ast::TypeAnnotation>, -) -> Option { - match field { - AssignedField::RequiredValue(name, _, type_ann) => Some(RecordField::RecordField { - name: name.value.to_string(), - type_annotation: type_to_docs(in_func_ann, type_ann.value), - }), - AssignedField::SpaceBefore(&sub_field, _) => record_field_to_doc(in_func_ann, sub_field), - AssignedField::SpaceAfter(&sub_field, _) => record_field_to_doc(in_func_ann, sub_field), - AssignedField::OptionalValue(name, _, type_ann) => Some(RecordField::OptionalField { - name: name.value.to_string(), - type_annotation: type_to_docs(in_func_ann, type_ann.value), - }), - AssignedField::LabelOnly(label) => Some(RecordField::LabelOnly { - name: label.value.to_string(), - }), - AssignedField::Malformed(_) => None, - } -} - -// The Option here represents if it is malformed. -fn tag_to_doc(in_func_ann: bool, tag: ast::Tag) -> Option { - match tag { - ast::Tag::Apply { name, args } => Some(Tag { - name: name.value.to_string(), - values: { - let mut type_vars = Vec::new(); - - for arg in args { - type_vars.push(type_to_docs(in_func_ann, arg.value)); - } - - type_vars - }, - }), - ast::Tag::SpaceBefore(&sub_tag, _) => tag_to_doc(in_func_ann, sub_tag), - ast::Tag::SpaceAfter(&sub_tag, _) => tag_to_doc(in_func_ann, sub_tag), - ast::Tag::Malformed(_) => None, - } -} - -fn comments_or_new_lines_to_docs<'a>( - comments_or_new_lines: &'a [roc_parse::ast::CommentOrNewline<'a>], -) -> Option { - let mut docs = String::new(); - - for comment_or_new_line in comments_or_new_lines.iter() { - match comment_or_new_line { - CommentOrNewline::DocComment(doc_str) => { - docs.push_str(doc_str); - docs.push('\n'); - } - CommentOrNewline::Newline | CommentOrNewline::LineComment(_) => { - docs = String::new(); - } - } - } - - if docs.is_empty() { - None - } else { - Some(docs) - } -} diff --git a/compiler/load_internal/src/file.rs b/compiler/load_internal/src/file.rs deleted file mode 100644 index 1c2d52b066..0000000000 --- a/compiler/load_internal/src/file.rs +++ /dev/null @@ -1,4821 +0,0 @@ -use crate::docs::ModuleDocumentation; -use bumpalo::Bump; -use crossbeam::channel::{bounded, Sender}; -use crossbeam::deque::{Injector, Stealer, Worker}; -use crossbeam::thread; -use parking_lot::Mutex; -use roc_builtins::roc::module_source; -use roc_builtins::std::borrow_stdlib; -use roc_can::abilities::{AbilitiesStore, SolvedSpecializations}; -use roc_can::constraint::{Constraint as ConstraintSoa, Constraints}; -use roc_can::def::Declaration; -use roc_can::expr::PendingDerives; -use roc_can::module::{canonicalize_module_defs, Module}; -use roc_collections::{default_hasher, BumpMap, MutMap, MutSet, VecMap, VecSet}; -use roc_constrain::module::{ - constrain_builtin_imports, constrain_module, ExposedByModule, ExposedForModule, - ExposedModuleTypes, -}; -use roc_debug_flags::dbg_do; -#[cfg(debug_assertions)] -use roc_debug_flags::{ - ROC_PRINT_IR_AFTER_REFCOUNT, ROC_PRINT_IR_AFTER_RESET_REUSE, ROC_PRINT_IR_AFTER_SPECIALIZATION, - ROC_PRINT_LOAD_LOG, -}; -use roc_error_macros::internal_error; -use roc_module::ident::{Ident, ModuleName, QualifiedModuleName}; -use roc_module::symbol::{ - IdentIds, IdentIdsByModule, Interns, ModuleId, ModuleIds, PQModuleName, PackageModuleIds, - PackageQualified, Symbol, -}; -use roc_mono::ir::{ - CapturedSymbols, EntryPoint, ExternalSpecializations, PartialProc, Proc, ProcLayout, Procs, - UpdateModeIds, -}; -use roc_mono::layout::{Layout, LayoutCache, LayoutProblem}; -use roc_parse::ast::{self, Defs, ExtractSpaces, Spaced, StrLiteral, TypeAnnotation}; -use roc_parse::header::{ExposedName, ImportsEntry, PackageEntry, PlatformHeader, To, TypedIdent}; -use roc_parse::header::{HeaderFor, ModuleNameEnum, PackageName}; -use roc_parse::ident::UppercaseIdent; -use roc_parse::module::module_defs; -use roc_parse::parser::{FileError, Parser, SyntaxError}; -use roc_region::all::{LineInfo, Loc, Region}; -use roc_reporting::report::RenderTarget; -use roc_solve::module::SolvedModule; -use roc_solve::solve; -use roc_target::TargetInfo; -use roc_types::solved_types::Solved; -use roc_types::subs::{Subs, VarStore, Variable}; -use roc_types::types::{Alias, AliasKind}; -use std::collections::hash_map::Entry::{Occupied, Vacant}; -use std::collections::HashMap; -use std::io; -use std::iter; -use std::ops::ControlFlow; -use std::path::{Path, PathBuf}; -use std::str::from_utf8_unchecked; -use std::sync::Arc; -use std::{env, fs}; - -use crate::work::Dependencies; -pub use crate::work::Phase; - -#[cfg(target_family = "wasm")] -use crate::wasm_system_time::{Duration, SystemTime}; -#[cfg(not(target_family = "wasm"))] -use std::time::{Duration, SystemTime}; - -/// Default name for the binary generated for an app, if an invalid one was specified. -const DEFAULT_APP_OUTPUT_PATH: &str = "app"; - -/// Filename extension for normal Roc modules -const ROC_FILE_EXTENSION: &str = "roc"; - -/// Roc-Config file name -const PKG_CONFIG_FILE_NAME: &str = "Package-Config"; - -/// The . in between module names like Foo.Bar.Baz -const MODULE_SEPARATOR: char = '.'; - -const EXPANDED_STACK_SIZE: usize = 8 * 1024 * 1024; - -const PRELUDE_TYPES: [(&str, Symbol); 33] = [ - ("Num", Symbol::NUM_NUM), - ("Int", Symbol::NUM_INT), - ("Frac", Symbol::NUM_FRAC), - ("Integer", Symbol::NUM_INTEGER), - ("FloatingPoint", Symbol::NUM_FLOATINGPOINT), - ("Binary32", Symbol::NUM_BINARY32), - ("Binary64", Symbol::NUM_BINARY64), - ("Signed128", Symbol::NUM_SIGNED128), - ("Signed64", Symbol::NUM_SIGNED64), - ("Signed32", Symbol::NUM_SIGNED32), - ("Signed16", Symbol::NUM_SIGNED16), - ("Signed8", Symbol::NUM_SIGNED8), - ("Unsigned128", Symbol::NUM_UNSIGNED128), - ("Unsigned64", Symbol::NUM_UNSIGNED64), - ("Unsigned32", Symbol::NUM_UNSIGNED32), - ("Unsigned16", Symbol::NUM_UNSIGNED16), - ("Unsigned8", Symbol::NUM_UNSIGNED8), - ("Natural", Symbol::NUM_NATURAL), - ("Decimal", Symbol::NUM_DECIMAL), - ("Nat", Symbol::NUM_NAT), - ("I8", Symbol::NUM_I8), - ("I16", Symbol::NUM_I16), - ("I32", Symbol::NUM_I32), - ("I64", Symbol::NUM_I64), - ("I128", Symbol::NUM_I128), - ("U8", Symbol::NUM_U8), - ("U16", Symbol::NUM_U16), - ("U32", Symbol::NUM_U32), - ("U64", Symbol::NUM_U64), - ("U128", Symbol::NUM_U128), - ("F32", Symbol::NUM_F32), - ("F64", Symbol::NUM_F64), - ("Dec", Symbol::NUM_DEC), -]; - -macro_rules! log { - ($($arg:tt)*) => (dbg_do!(ROC_PRINT_LOAD_LOG, println!($($arg)*))) -} - -/// Struct storing various intermediate stages by their ModuleId -#[derive(Debug)] -struct ModuleCache<'a> { - module_names: MutMap>, - - /// Phases - headers: MutMap>, - parsed: MutMap>, - aliases: MutMap>, - abilities: MutMap, - constrained: MutMap, - typechecked: MutMap>, - found_specializations: MutMap>, - external_specializations_requested: MutMap>, - - /// Various information - imports: MutMap>, - top_level_thunks: MutMap>, - documentation: MutMap, - can_problems: MutMap>, - type_problems: MutMap>, - - sources: MutMap, -} - -impl Default for ModuleCache<'_> { - fn default() -> Self { - let mut module_names = MutMap::default(); - - macro_rules! insert_builtins { - ($($name:ident,)*) => {$( - module_names.insert( - ModuleId::$name, - PQModuleName::Unqualified(ModuleName::from(ModuleName::$name)), - ); - )*} - } - - insert_builtins! { - RESULT, - LIST, - STR, - DICT, - SET, - BOOL, - NUM, - BOX, - ENCODE, - JSON, - } - - Self { - module_names, - headers: Default::default(), - parsed: Default::default(), - aliases: Default::default(), - abilities: Default::default(), - constrained: Default::default(), - typechecked: Default::default(), - found_specializations: Default::default(), - external_specializations_requested: Default::default(), - imports: Default::default(), - top_level_thunks: Default::default(), - documentation: Default::default(), - can_problems: Default::default(), - type_problems: Default::default(), - sources: Default::default(), - } - } -} - -type SharedIdentIdsByModule = Arc>; - -fn start_phase<'a>( - module_id: ModuleId, - phase: Phase, - arena: &'a Bump, - state: &mut State<'a>, -) -> Vec> { - // we blindly assume all dependencies are met - - use crate::work::PrepareStartPhase::*; - match state.dependencies.prepare_start_phase(module_id, phase) { - Continue => { - // fall through - } - Done => { - // no more work to do - return vec![]; - } - Recurse(new) => { - return new - .into_iter() - .flat_map(|(module_id, phase)| start_phase(module_id, phase, arena, state)) - .collect() - } - } - - let task = { - match phase { - Phase::LoadHeader => { - let opt_dep_name = state.module_cache.module_names.get(&module_id); - - match opt_dep_name { - None => { - panic!("Module {:?} is not in module_cache.module_names", module_id) - } - Some(dep_name) => { - let module_name = dep_name.clone(); - - BuildTask::LoadModule { - module_name, - // Provide mutexes of ModuleIds and IdentIds by module, - // so other modules can populate them as they load. - module_ids: Arc::clone(&state.arc_modules), - shorthands: Arc::clone(&state.arc_shorthands), - ident_ids_by_module: Arc::clone(&state.ident_ids_by_module), - } - } - } - } - Phase::Parse => { - // parse the file - let header = state.module_cache.headers.remove(&module_id).unwrap(); - - BuildTask::Parse { header } - } - Phase::CanonicalizeAndConstrain => { - // canonicalize the file - let parsed = state.module_cache.parsed.remove(&module_id).unwrap(); - - let deps_by_name = &parsed.deps_by_name; - let num_deps = deps_by_name.len(); - let mut dep_idents: IdentIdsByModule = IdentIds::exposed_builtins(num_deps); - - let State { - ident_ids_by_module, - .. - } = &state; - - { - let ident_ids_by_module = (*ident_ids_by_module).lock(); - - // Populate dep_idents with each of their IdentIds, - // which we'll need during canonicalization to translate - // identifier strings into IdentIds, which we need to build Symbols. - // We only include the modules we care about (the ones we import). - // - // At the end of this loop, dep_idents contains all the information to - // resolve a symbol from another module: if it's in here, that means - // we have both imported the module and the ident was exported by that mdoule. - for dep_id in deps_by_name.values() { - // We already verified that these are all present, - // so unwrapping should always succeed here. - let idents = ident_ids_by_module.get(dep_id).unwrap(); - - dep_idents.insert(*dep_id, idents.clone()); - } - } - - // Clone the module_ids we'll need for canonicalization. - // This should be small, and cloning it should be quick. - // We release the lock as soon as we're done cloning, so we don't have - // to lock the global module_ids while canonicalizing any given module. - let qualified_module_ids = Arc::clone(&state.arc_modules); - let qualified_module_ids = { (*qualified_module_ids).lock().clone() }; - - let module_ids = qualified_module_ids.into_module_ids(); - - let exposed_symbols = state - .exposed_symbols_by_module - .get(&module_id) - .expect("Could not find listener ID in exposed_symbols_by_module") - .clone(); - - let mut aliases = MutMap::default(); - let mut abilities_store = AbilitiesStore::default(); - - for imported in parsed.imported_modules.keys() { - match state.module_cache.aliases.get(imported) { - None => unreachable!( - r"imported module {:?} did not register its aliases, so {:?} cannot use them", - imported, parsed.module_id, - ), - Some(new) => { - aliases.extend(new.iter().filter_map(|(s, (exposed, a))| { - // only pass this on if it's exposed, or the alias is a transitive import - if *exposed || s.module_id() != *imported { - Some((*s, a.clone())) - } else { - None - } - })); - } - } - - match state.module_cache.abilities.get(imported) { - None => unreachable!( - r"imported module {:?} did not register its abilities, so {:?} cannot use them", - imported, parsed.module_id, - ), - Some(import_store) => { - let exposed_symbols = state - .exposed_symbols_by_module - .get(imported) - .unwrap_or_else(|| { - internal_error!( - "Could not find exposed symbols of imported {:?}", - imported - ) - }); - - abilities_store - .union(import_store.closure_from_imported(exposed_symbols)); - } - } - } - - let skip_constraint_gen = { - // Give this its own scope to make sure that the Guard from the lock() is dropped - // immediately after contains_key returns - state.cached_subs.lock().contains_key(&module_id) - }; - - BuildTask::CanonicalizeAndConstrain { - parsed, - dep_idents, - exposed_symbols, - module_ids, - aliases, - abilities_store, - skip_constraint_gen, - } - } - - Phase::SolveTypes => { - let constrained = state.module_cache.constrained.remove(&module_id).unwrap(); - - let ConstrainedModule { - module, - ident_ids, - module_timing, - constraints, - constraint, - var_store, - imported_modules, - declarations, - dep_idents, - pending_derives, - .. - } = constrained; - - BuildTask::solve_module( - module, - ident_ids, - module_timing, - constraints, - constraint, - pending_derives, - var_store, - imported_modules, - &state.exposed_types, - dep_idents, - declarations, - state.cached_subs.clone(), - ) - } - Phase::FindSpecializations => { - let typechecked = state.module_cache.typechecked.remove(&module_id).unwrap(); - - let TypeCheckedModule { - layout_cache, - module_id, - module_timing, - solved_subs, - decls, - ident_ids, - abilities_store, - } = typechecked; - - let mut imported_module_thunks = bumpalo::collections::Vec::new_in(arena); - - if let Some(imports) = state.module_cache.imports.get(&module_id) { - for imported in imports.iter() { - imported_module_thunks.extend( - state.module_cache.top_level_thunks[imported] - .iter() - .copied(), - ); - } - } - - BuildTask::BuildPendingSpecializations { - layout_cache, - module_id, - module_timing, - solved_subs, - imported_module_thunks: imported_module_thunks.into_bump_slice(), - decls, - ident_ids, - exposed_to_host: state.exposed_to_host.clone(), - abilities_store, - } - } - Phase::MakeSpecializations => { - let found_specializations = state - .module_cache - .found_specializations - .remove(&module_id) - .unwrap(); - - let specializations_we_must_make = state - .module_cache - .external_specializations_requested - .remove(&module_id) - .unwrap_or_default(); - - let FoundSpecializationsModule { - module_id, - ident_ids, - subs, - procs_base, - layout_cache, - module_timing, - abilities_store, - } = found_specializations; - - BuildTask::MakeSpecializations { - module_id, - ident_ids, - subs, - procs_base, - layout_cache, - specializations_we_must_make, - module_timing, - abilities_store, - } - } - } - }; - - vec![task] -} - -#[derive(Debug)] -pub struct LoadedModule { - pub module_id: ModuleId, - pub interns: Interns, - pub solved: Solved, - pub can_problems: MutMap>, - pub type_problems: MutMap>, - pub declarations_by_id: MutMap>, - pub exposed_to_host: MutMap, - pub dep_idents: IdentIdsByModule, - pub exposed_aliases: MutMap, - pub exposed_values: Vec, - pub sources: MutMap)>, - pub timings: MutMap, - pub documentation: MutMap, - pub abilities_store: AbilitiesStore, -} - -impl LoadedModule { - pub fn total_problems(&self) -> usize { - let mut total = 0; - - for problems in self.can_problems.values() { - total += problems.len(); - } - - for problems in self.type_problems.values() { - total += problems.len(); - } - - total - } - - pub fn exposed_values_str(&self) -> Vec<&str> { - self.exposed_values - .iter() - .map(|symbol| symbol.as_str(&self.interns)) - .collect() - } -} - -#[derive(Debug)] -pub enum BuildProblem<'a> { - FileNotFound(&'a Path), -} - -#[derive(Debug)] -struct ModuleHeader<'a> { - module_id: ModuleId, - module_name: ModuleNameEnum<'a>, - module_path: PathBuf, - is_root_module: bool, - exposed_ident_ids: IdentIds, - deps_by_name: MutMap, ModuleId>, - packages: MutMap<&'a str, PackageName<'a>>, - imported_modules: MutMap, - package_qualified_imported_modules: MutSet>, - exposes: Vec, - exposed_imports: MutMap, - parse_state: roc_parse::state::State<'a>, - header_for: HeaderFor<'a>, - symbols_from_requires: Vec<(Loc, Loc>)>, - module_timing: ModuleTiming, -} - -#[derive(Debug)] -struct ConstrainedModule { - module: Module, - declarations: Vec, - imported_modules: MutMap, - constraints: Constraints, - constraint: ConstraintSoa, - ident_ids: IdentIds, - var_store: VarStore, - dep_idents: IdentIdsByModule, - module_timing: ModuleTiming, - // Rather than adding pending derives as constraints, hand them directly to solve because they - // must be solved at the end of a module. - pending_derives: PendingDerives, -} - -#[derive(Debug)] -pub struct TypeCheckedModule<'a> { - pub module_id: ModuleId, - pub layout_cache: LayoutCache<'a>, - pub module_timing: ModuleTiming, - pub solved_subs: Solved, - pub decls: Vec, - pub ident_ids: IdentIds, - pub abilities_store: AbilitiesStore, -} - -#[derive(Debug)] -struct FoundSpecializationsModule<'a> { - module_id: ModuleId, - ident_ids: IdentIds, - layout_cache: LayoutCache<'a>, - procs_base: ProcsBase<'a>, - subs: Subs, - module_timing: ModuleTiming, - abilities_store: AbilitiesStore, -} - -#[derive(Debug)] -pub struct MonomorphizedModule<'a> { - pub module_id: ModuleId, - pub interns: Interns, - pub subs: Subs, - pub output_path: Box, - pub platform_path: Box, - pub can_problems: MutMap>, - pub type_problems: MutMap>, - pub procedures: MutMap<(Symbol, ProcLayout<'a>), Proc<'a>>, - pub entry_point: EntryPoint<'a>, - pub exposed_to_host: ExposedToHost, - pub sources: MutMap)>, - pub timings: MutMap, -} - -#[derive(Clone, Debug, Default)] -pub struct ExposedToHost { - /// usually `mainForHost` - pub values: MutMap, - /// exposed closure types, typically `Fx` - pub closure_types: Vec, -} - -impl<'a> MonomorphizedModule<'a> { - pub fn total_problems(&self) -> usize { - let mut total = 0; - - for problems in self.can_problems.values() { - total += problems.len(); - } - - for problems in self.type_problems.values() { - total += problems.len(); - } - - total - } -} - -#[derive(Debug)] -struct ParsedModule<'a> { - module_id: ModuleId, - module_path: PathBuf, - src: &'a str, - module_timing: ModuleTiming, - deps_by_name: MutMap, ModuleId>, - imported_modules: MutMap, - exposed_ident_ids: IdentIds, - exposed_imports: MutMap, - parsed_defs: Defs<'a>, - module_name: ModuleNameEnum<'a>, - symbols_from_requires: Vec<(Loc, Loc>)>, - header_for: HeaderFor<'a>, -} - -/// A message sent out _from_ a worker thread, -/// representing a result of work done, or a request for further work -#[derive(Debug)] -enum Msg<'a> { - Many(Vec>), - Header(ModuleHeader<'a>), - Parsed(ParsedModule<'a>), - CanonicalizedAndConstrained(CanAndCon), - SolvedTypes { - module_id: ModuleId, - ident_ids: IdentIds, - solved_module: SolvedModule, - solved_subs: Solved, - decls: Vec, - dep_idents: IdentIdsByModule, - module_timing: ModuleTiming, - abilities_store: AbilitiesStore, - }, - FinishedAllTypeChecking { - solved_subs: Solved, - exposed_vars_by_symbol: Vec<(Symbol, Variable)>, - exposed_aliases_by_symbol: MutMap, - dep_idents: IdentIdsByModule, - documentation: MutMap, - abilities_store: AbilitiesStore, - }, - FoundSpecializations { - module_id: ModuleId, - ident_ids: IdentIds, - layout_cache: LayoutCache<'a>, - procs_base: ProcsBase<'a>, - solved_subs: Solved, - module_timing: ModuleTiming, - abilities_store: AbilitiesStore, - }, - MadeSpecializations { - module_id: ModuleId, - ident_ids: IdentIds, - layout_cache: LayoutCache<'a>, - external_specializations_requested: BumpMap, - procedures: MutMap<(Symbol, ProcLayout<'a>), Proc<'a>>, - update_mode_ids: UpdateModeIds, - module_timing: ModuleTiming, - subs: Subs, - }, - - /// The task is to only typecheck AND monomorphize modules - /// all modules are now monomorphized, we are done - FinishedAllSpecialization { - subs: Subs, - exposed_to_host: ExposedToHost, - }, - - FailedToParse(FileError<'a, SyntaxError<'a>>), - FailedToReadFile { - filename: PathBuf, - error: io::ErrorKind, - }, -} - -#[derive(Debug)] -struct CanAndCon { - constrained_module: ConstrainedModule, - canonicalization_problems: Vec, - module_docs: Option, -} - -#[derive(Debug)] -enum PlatformPath<'a> { - NotSpecified, - Valid(To<'a>), - RootIsInterface, - RootIsHosted, - RootIsPkgConfig, -} - -#[derive(Debug)] -struct PlatformData { - module_id: ModuleId, - provides: Symbol, -} - -#[derive(Debug)] -struct State<'a> { - pub root_id: ModuleId, - pub root_subs: Option, - pub platform_data: Option, - pub goal_phase: Phase, - pub exposed_types: ExposedByModule, - pub output_path: Option<&'a str>, - pub platform_path: PlatformPath<'a>, - pub target_info: TargetInfo, - - pub module_cache: ModuleCache<'a>, - pub dependencies: Dependencies<'a>, - pub procedures: MutMap<(Symbol, ProcLayout<'a>), Proc<'a>>, - pub exposed_to_host: ExposedToHost, - - /// This is the "final" list of IdentIds, after canonicalization and constraint gen - /// have completed for a given module. - pub constrained_ident_ids: IdentIdsByModule, - - /// From now on, these will be used by multiple threads; time to make an Arc>! - pub arc_modules: Arc>>, - pub arc_shorthands: Arc>>>, - - pub ident_ids_by_module: SharedIdentIdsByModule, - - pub declarations_by_id: MutMap>, - - pub exposed_symbols_by_module: MutMap>, - - pub timings: MutMap, - - // Each thread gets its own layout cache. When one "pending specializations" - // pass completes, it returns its layout cache so another thread can use it. - // We don't bother trying to union them all together to maximize cache hits, - // since the unioning process could potentially take longer than the savings. - // (Granted, this has not been attempted or measured!) - pub layout_caches: std::vec::Vec>, - - pub render: RenderTarget, - - // cached subs (used for builtin modules, could include packages in the future too) - cached_subs: CachedSubs, -} - -type CachedSubs = Arc)>>>; - -impl<'a> State<'a> { - #[allow(clippy::too_many_arguments)] - fn new( - root_id: ModuleId, - target_info: TargetInfo, - goal_phase: Phase, - exposed_types: ExposedByModule, - arc_modules: Arc>>, - ident_ids_by_module: SharedIdentIdsByModule, - cached_subs: MutMap)>, - render: RenderTarget, - number_of_workers: usize, - ) -> Self { - let arc_shorthands = Arc::new(Mutex::new(MutMap::default())); - - Self { - root_id, - root_subs: None, - target_info, - platform_data: None, - goal_phase, - output_path: None, - platform_path: PlatformPath::NotSpecified, - module_cache: ModuleCache::default(), - dependencies: Dependencies::default(), - procedures: MutMap::default(), - exposed_to_host: ExposedToHost::default(), - exposed_types, - arc_modules, - arc_shorthands, - constrained_ident_ids: IdentIds::exposed_builtins(0), - ident_ids_by_module, - declarations_by_id: MutMap::default(), - exposed_symbols_by_module: MutMap::default(), - timings: MutMap::default(), - layout_caches: std::vec::Vec::with_capacity(number_of_workers), - cached_subs: Arc::new(Mutex::new(cached_subs)), - render, - } - } -} - -#[derive(Debug)] -pub struct ModuleTiming { - pub read_roc_file: Duration, - pub parse_header: Duration, - pub parse_body: Duration, - pub canonicalize: Duration, - pub constrain: Duration, - pub solve: Duration, - pub find_specializations: Duration, - pub make_specializations: Duration, - // TODO pub monomorphize: Duration, - /// Total duration will always be more than the sum of the other fields, due - /// to things like state lookups in between phases, waiting on other threads, etc. - start_time: SystemTime, - end_time: SystemTime, -} - -impl ModuleTiming { - pub fn new(start_time: SystemTime) -> Self { - ModuleTiming { - read_roc_file: Duration::default(), - parse_header: Duration::default(), - parse_body: Duration::default(), - canonicalize: Duration::default(), - constrain: Duration::default(), - solve: Duration::default(), - find_specializations: Duration::default(), - make_specializations: Duration::default(), - start_time, - end_time: start_time, // just for now; we'll overwrite this at the end - } - } - - pub fn total(&self) -> Duration { - self.end_time.duration_since(self.start_time).unwrap() - } - - /// Subtract all the other fields from total_start_to_finish - pub fn other(&self) -> Duration { - let Self { - read_roc_file, - parse_header, - parse_body, - canonicalize, - constrain, - solve, - find_specializations, - make_specializations, - start_time, - end_time, - } = self; - - let calculate = |t: Result| -> Option { - t.ok()? - .checked_sub(*make_specializations)? - .checked_sub(*find_specializations)? - .checked_sub(*solve)? - .checked_sub(*constrain)? - .checked_sub(*canonicalize)? - .checked_sub(*parse_body)? - .checked_sub(*parse_header)? - .checked_sub(*read_roc_file) - }; - - calculate(end_time.duration_since(*start_time)).unwrap_or_default() - } -} - -/// A message sent _to_ a worker thread, describing the work to be done -#[derive(Debug)] -#[allow(dead_code)] -enum BuildTask<'a> { - LoadModule { - module_name: PQModuleName<'a>, - module_ids: Arc>>, - shorthands: Arc>>>, - ident_ids_by_module: SharedIdentIdsByModule, - }, - Parse { - header: ModuleHeader<'a>, - }, - CanonicalizeAndConstrain { - parsed: ParsedModule<'a>, - module_ids: ModuleIds, - dep_idents: IdentIdsByModule, - exposed_symbols: VecSet, - aliases: MutMap, - abilities_store: AbilitiesStore, - skip_constraint_gen: bool, - }, - Solve { - module: Module, - ident_ids: IdentIds, - imported_builtins: Vec, - exposed_for_module: ExposedForModule, - module_timing: ModuleTiming, - constraints: Constraints, - constraint: ConstraintSoa, - pending_derives: PendingDerives, - var_store: VarStore, - declarations: Vec, - dep_idents: IdentIdsByModule, - cached_subs: CachedSubs, - }, - BuildPendingSpecializations { - module_timing: ModuleTiming, - layout_cache: LayoutCache<'a>, - solved_subs: Solved, - imported_module_thunks: &'a [Symbol], - module_id: ModuleId, - ident_ids: IdentIds, - decls: Vec, - exposed_to_host: ExposedToHost, - abilities_store: AbilitiesStore, - }, - MakeSpecializations { - module_id: ModuleId, - ident_ids: IdentIds, - subs: Subs, - procs_base: ProcsBase<'a>, - layout_cache: LayoutCache<'a>, - specializations_we_must_make: Vec, - module_timing: ModuleTiming, - abilities_store: AbilitiesStore, - }, -} - -enum WorkerMsg { - Shutdown, - TaskAdded, -} - -#[derive(Debug)] -pub enum LoadingProblem<'a> { - FileProblem { - filename: PathBuf, - error: io::ErrorKind, - }, - ParsingFailed(FileError<'a, SyntaxError<'a>>), - UnexpectedHeader(String), - - MsgChannelDied, - ErrJoiningWorkerThreads, - TriedToImportAppModule, - - /// a formatted report - FormattedReport(String), -} - -pub enum Phases { - /// Parse, canonicalize, check types - TypeCheck, - /// Parse, canonicalize, check types, monomorphize - Monomorphize, -} - -type MsgSender<'a> = Sender>; - -/// Add a task to the queue, and notify all the listeners. -fn enqueue_task<'a>( - injector: &Injector>, - listeners: &[Sender], - task: BuildTask<'a>, -) -> Result<(), LoadingProblem<'a>> { - injector.push(task); - - for listener in listeners { - listener - .send(WorkerMsg::TaskAdded) - .map_err(|_| LoadingProblem::MsgChannelDied)?; - } - - Ok(()) -} - -#[allow(clippy::too_many_arguments)] -pub fn load_and_typecheck_str<'a>( - arena: &'a Bump, - filename: PathBuf, - source: &'a str, - src_dir: &Path, - exposed_types: ExposedByModule, - target_info: TargetInfo, - render: RenderTarget, - threading: Threading, -) -> Result> { - use LoadResult::*; - - let load_start = LoadStart::from_str(arena, filename, source)?; - - // this function is used specifically in the case - // where we want to regenerate the cached data - let cached_subs = MutMap::default(); - - match load( - arena, - load_start, - src_dir, - exposed_types, - Phase::SolveTypes, - target_info, - cached_subs, - render, - threading, - )? { - Monomorphized(_) => unreachable!(""), - TypeChecked(module) => Ok(module), - } -} - -#[derive(Clone, Copy)] -pub enum PrintTarget { - ColorTerminal, - Generic, -} - -pub struct LoadStart<'a> { - arc_modules: Arc>>, - ident_ids_by_module: SharedIdentIdsByModule, - root_id: ModuleId, - root_msg: Msg<'a>, -} - -impl<'a> LoadStart<'a> { - pub fn from_path( - arena: &'a Bump, - filename: PathBuf, - render: RenderTarget, - ) -> Result> { - let arc_modules = Arc::new(Mutex::new(PackageModuleIds::default())); - let root_exposed_ident_ids = IdentIds::exposed_builtins(0); - let ident_ids_by_module = Arc::new(Mutex::new(root_exposed_ident_ids)); - - // Load the root module synchronously; we can't proceed until we have its id. - let (root_id, root_msg) = { - let root_start_time = SystemTime::now(); - - let res_loaded = load_filename( - arena, - filename, - true, - None, - Arc::clone(&arc_modules), - Arc::clone(&ident_ids_by_module), - root_start_time, - ); - - match res_loaded { - Ok(good) => good, - - Err(LoadingProblem::ParsingFailed(problem)) => { - let module_ids = Arc::try_unwrap(arc_modules) - .unwrap_or_else(|_| { - panic!("There were still outstanding Arc references to module_ids") - }) - .into_inner() - .into_module_ids(); - - // if parsing failed, this module did not add any identifiers - let root_exposed_ident_ids = IdentIds::exposed_builtins(0); - let buf = to_parse_problem_report( - problem, - module_ids, - root_exposed_ident_ids, - render, - ); - return Err(LoadingProblem::FormattedReport(buf)); - } - Err(LoadingProblem::FileProblem { filename, error }) => { - let buf = to_file_problem_report(&filename, error); - return Err(LoadingProblem::FormattedReport(buf)); - } - Err(e) => return Err(e), - } - }; - - Ok(LoadStart { - arc_modules, - ident_ids_by_module, - root_id, - root_msg, - }) - } - - pub fn from_str( - arena: &'a Bump, - filename: PathBuf, - src: &'a str, - ) -> Result> { - let arc_modules = Arc::new(Mutex::new(PackageModuleIds::default())); - let root_exposed_ident_ids = IdentIds::exposed_builtins(0); - let ident_ids_by_module = Arc::new(Mutex::new(root_exposed_ident_ids)); - - // Load the root module synchronously; we can't proceed until we have its id. - let (root_id, root_msg) = { - let root_start_time = SystemTime::now(); - - load_from_str( - arena, - filename, - src, - Arc::clone(&arc_modules), - Arc::clone(&ident_ids_by_module), - root_start_time, - )? - }; - - Ok(LoadStart { - arc_modules, - ident_ids_by_module, - root_id, - root_msg, - }) - } -} - -pub enum LoadResult<'a> { - TypeChecked(LoadedModule), - Monomorphized(MonomorphizedModule<'a>), -} - -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -pub enum Threading { - Single, - AllAvailable, - AtMost(usize), -} - -/// The loading process works like this, starting from the given filename (e.g. "main.roc"): -/// -/// 1. Open the file. -/// 2. Parse the module's header. -/// 3. For each of its imports, send a message on the channel to the coordinator thread, which -/// will repeat this process to load that module - starting with step 1. -/// 4. Add everything we were able to import unqualified to the module's default scope. -/// 5. Parse the module's defs. -/// 6. Canonicalize the module. -/// 7. Before type checking, block on waiting for type checking to complete on all imports. -/// (Since Roc doesn't allow cyclic dependencies, this cannot deadlock.) -/// 8. Type check the module and create type annotations for its top-level declarations. -/// 9. Report the completed type annotation to the coordinator thread, so other modules -/// that are waiting in step 7 can unblock. -/// -/// The loaded_modules argument specifies which modules have already been loaded. -/// It typically contains *at least* the standard modules, but is empty when loading -/// the standard modules themselves. -/// -/// If we're just type-checking everything (e.g. running `roc check` at the command line), -/// we can stop there. However, if we're generating code, then there are additional steps. -/// -/// 10. After reporting the completed type annotation, we have all the information necessary -/// to monomorphize. However, since we want to monomorphize in parallel without -/// duplicating work, we do monomorphization in two steps. First, we go through and -/// determine all the specializations this module *wants*. We compute the hashes -/// and report them to the coordinator thread, along with the mono::expr::Expr values of -/// the current function's body. At this point, we have not yet begun to assemble Procs; -/// all we've done is send a list of requetsted specializations to the coordinator. -/// 11. The coordinator works through the specialization requests in parallel, adding them -/// to a global map once they're finished. Performing one specialization may result -/// in requests for others; these are added to the queue and worked through as normal. -/// This process continues until *both* all modules have reported that they've finished -/// adding specialization requests to the queue, *and* the queue is empty (including -/// of any requests that were added in the course of completing other requests). Now -/// we have a map of specializations, and everything was assembled in parallel with -/// no unique specialization ever getting assembled twice (meaning no wasted effort). -/// 12. Now that we have our final map of specializations, we can proceed to code gen! -/// As long as the specializations are stored in a per-ModuleId map, we can also -/// parallelize this code gen. (e.g. in dev builds, building separate LLVM modules -/// and then linking them together, and possibly caching them by the hash of their -/// specializations, so if none of their specializations changed, we don't even need -/// to rebuild the module and can link in the cached one directly.) -#[allow(clippy::too_many_arguments)] -pub fn load<'a>( - arena: &'a Bump, - load_start: LoadStart<'a>, - src_dir: &Path, - exposed_types: ExposedByModule, - goal_phase: Phase, - target_info: TargetInfo, - cached_subs: MutMap)>, - render: RenderTarget, - threading: Threading, -) -> Result, LoadingProblem<'a>> { - enum Threads { - Single, - Many(usize), - } - - let threads = { - if cfg!(target_family = "wasm") { - // When compiling to wasm, we cannot spawn extra threads - // so we have a single-threaded implementation - Threads::Single - } else { - match std::thread::available_parallelism().map(|v| v.get()) { - Err(_) => Threads::Single, - Ok(0) => unreachable!("NonZeroUsize"), - Ok(1) => Threads::Single, - Ok(reported) => match threading { - Threading::Single => Threads::Single, - Threading::AllAvailable => Threads::Many(reported), - Threading::AtMost(at_most) => Threads::Many(Ord::min(reported, at_most)), - }, - } - } - }; - - match threads { - Threads::Single => load_single_threaded( - arena, - load_start, - src_dir, - exposed_types, - goal_phase, - target_info, - cached_subs, - render, - ), - Threads::Many(threads) => load_multi_threaded( - arena, - load_start, - src_dir, - exposed_types, - goal_phase, - target_info, - cached_subs, - render, - threads, - ), - } -} - -/// Load using only a single thread; used when compiling to webassembly -#[allow(clippy::too_many_arguments)] -pub fn load_single_threaded<'a>( - arena: &'a Bump, - load_start: LoadStart<'a>, - src_dir: &Path, - exposed_types: ExposedByModule, - goal_phase: Phase, - target_info: TargetInfo, - cached_subs: MutMap)>, - render: RenderTarget, -) -> Result, LoadingProblem<'a>> { - let LoadStart { - arc_modules, - ident_ids_by_module, - root_id, - root_msg, - .. - } = load_start; - - let (msg_tx, msg_rx) = bounded(1024); - - msg_tx - .send(root_msg) - .map_err(|_| LoadingProblem::MsgChannelDied)?; - - let number_of_workers = 1; - let mut state = State::new( - root_id, - target_info, - goal_phase, - exposed_types, - arc_modules, - ident_ids_by_module, - cached_subs, - render, - number_of_workers, - ); - - // We'll add tasks to this, and then worker threads will take tasks from it. - let injector = Injector::new(); - - let (worker_msg_tx, worker_msg_rx) = bounded(1024); - let worker_listener = worker_msg_tx; - let worker_listeners = arena.alloc([worker_listener]); - - let worker = Worker::new_fifo(); - let stealer = worker.stealer(); - let stealers = &[stealer]; - - // now we just manually interleave stepping the state "thread" and the worker "thread" - loop { - match state_thread_step(arena, state, worker_listeners, &injector, &msg_tx, &msg_rx) { - Ok(ControlFlow::Break(done)) => return Ok(done), - Ok(ControlFlow::Continue(new_state)) => { - state = new_state; - } - Err(e) => return Err(e), - } - - // then check if the worker can step - let control_flow = worker_task_step( - arena, - &worker, - &injector, - stealers, - &worker_msg_rx, - &msg_tx, - src_dir, - target_info, - ); - - match control_flow { - Ok(ControlFlow::Break(())) => panic!("the worker should not break!"), - Ok(ControlFlow::Continue(())) => { - // progress was made - } - Err(e) => return Err(e), - } - } -} - -fn state_thread_step<'a>( - arena: &'a Bump, - state: State<'a>, - worker_listeners: &'a [Sender], - injector: &Injector>, - msg_tx: &crossbeam::channel::Sender>, - msg_rx: &crossbeam::channel::Receiver>, -) -> Result, State<'a>>, LoadingProblem<'a>> { - match msg_rx.try_recv() { - Ok(msg) => { - match msg { - Msg::FinishedAllTypeChecking { - solved_subs, - exposed_vars_by_symbol, - exposed_aliases_by_symbol, - dep_idents, - documentation, - abilities_store, - } => { - // We're done! There should be no more messages pending. - debug_assert!(msg_rx.is_empty()); - - let exposed_aliases_by_symbol = exposed_aliases_by_symbol - .into_iter() - .map(|(k, (_, v))| (k, v)) - .collect(); - - let typechecked = finish( - state, - solved_subs, - exposed_aliases_by_symbol, - exposed_vars_by_symbol, - dep_idents, - documentation, - abilities_store, - ); - - Ok(ControlFlow::Break(LoadResult::TypeChecked(typechecked))) - } - Msg::FinishedAllSpecialization { - subs, - exposed_to_host, - } => { - // We're done! There should be no more messages pending. - debug_assert!(msg_rx.is_empty()); - - let monomorphized = finish_specialization(state, subs, exposed_to_host)?; - - Ok(ControlFlow::Break(LoadResult::Monomorphized(monomorphized))) - } - Msg::FailedToReadFile { filename, error } => { - let buf = to_file_problem_report(&filename, error); - Err(LoadingProblem::FormattedReport(buf)) - } - - Msg::FailedToParse(problem) => { - let module_ids = (*state.arc_modules).lock().clone().into_module_ids(); - let buf = to_parse_problem_report( - problem, - module_ids, - state.constrained_ident_ids, - state.render, - ); - Err(LoadingProblem::FormattedReport(buf)) - } - msg => { - // This is where most of the main thread's work gets done. - // Everything up to this point has been setting up the threading - // system which lets this logic work efficiently. - let arc_modules = state.arc_modules.clone(); - - let render = state.render; - - let res_state = update( - state, - msg, - msg_tx.clone(), - injector, - worker_listeners, - arena, - ); - - match res_state { - Ok(new_state) => Ok(ControlFlow::Continue(new_state)), - Err(LoadingProblem::ParsingFailed(problem)) => { - let module_ids = Arc::try_unwrap(arc_modules) - .unwrap_or_else(|_| { - panic!( - r"There were still outstanding Arc references to module_ids" - ) - }) - .into_inner() - .into_module_ids(); - - // if parsing failed, this module did not add anything to IdentIds - let root_exposed_ident_ids = IdentIds::exposed_builtins(0); - let buf = to_parse_problem_report( - problem, - module_ids, - root_exposed_ident_ids, - render, - ); - Err(LoadingProblem::FormattedReport(buf)) - } - Err(e) => Err(e), - } - } - } - } - Err(err) => match err { - crossbeam::channel::TryRecvError::Empty => Ok(ControlFlow::Continue(state)), - crossbeam::channel::TryRecvError::Disconnected => Err(LoadingProblem::MsgChannelDied), - }, - } -} - -#[allow(clippy::too_many_arguments)] -fn load_multi_threaded<'a>( - arena: &'a Bump, - load_start: LoadStart<'a>, - src_dir: &Path, - exposed_types: ExposedByModule, - goal_phase: Phase, - target_info: TargetInfo, - cached_subs: MutMap)>, - render: RenderTarget, - available_threads: usize, -) -> Result, LoadingProblem<'a>> { - let LoadStart { - arc_modules, - ident_ids_by_module, - root_id, - root_msg, - .. - } = load_start; - - let (msg_tx, msg_rx) = bounded(1024); - msg_tx - .send(root_msg) - .map_err(|_| LoadingProblem::MsgChannelDied)?; - - // Reserve one CPU for the main thread, and let all the others be eligible - // to spawn workers. - let available_workers = available_threads - 1; - - let num_workers = match env::var("ROC_NUM_WORKERS") { - Ok(env_str) => env_str - .parse::() - .unwrap_or(available_workers) - .min(available_workers), - Err(_) => available_workers, - }; - - assert!( - num_workers >= 1, - "`load_multi_threaded` needs at least one worker" - ); - - let mut state = State::new( - root_id, - target_info, - goal_phase, - exposed_types, - arc_modules, - ident_ids_by_module, - cached_subs, - render, - num_workers, - ); - - // an arena for every worker, stored in an arena-allocated bumpalo vec to make the lifetimes work - let arenas = std::iter::repeat_with(Bump::new).take(num_workers); - let worker_arenas = arena.alloc(bumpalo::collections::Vec::from_iter_in(arenas, arena)); - - // We'll add tasks to this, and then worker threads will take tasks from it. - let injector = Injector::new(); - - // We need to allocate worker *queues* on the main thread and then move them - // into the worker threads, because those workers' stealers need to be - // shared between all threads, and this coordination work is much easier - // on the main thread. - let mut worker_queues = bumpalo::collections::Vec::with_capacity_in(num_workers, arena); - let mut stealers = bumpalo::collections::Vec::with_capacity_in(num_workers, arena); - - for _ in 0..num_workers { - let worker = Worker::new_fifo(); - - stealers.push(worker.stealer()); - worker_queues.push(worker); - } - - // Get a reference to the completed stealers, so we can send that - // reference to each worker. (Slices are Sync, but bumpalo Vecs are not.) - let stealers = stealers.into_bump_slice(); - - let it = worker_arenas.iter_mut(); - { - thread::scope(|thread_scope| { - let mut worker_listeners = - bumpalo::collections::Vec::with_capacity_in(num_workers, arena); - - for worker_arena in it { - let msg_tx = msg_tx.clone(); - let worker = worker_queues.pop().unwrap(); - - let (worker_msg_tx, worker_msg_rx) = bounded(1024); - worker_listeners.push(worker_msg_tx); - - // We only want to move a *reference* to the main task queue's - // injector in the thread, not the injector itself - // (since other threads need to reference it too). - let injector = &injector; - - // Record this thread's handle so the main thread can join it later. - let res_join_handle = thread_scope - .builder() - .stack_size(EXPANDED_STACK_SIZE) - .spawn(move |_| { - // will process messages until we run out - worker_task( - worker_arena, - worker, - injector, - stealers, - worker_msg_rx, - msg_tx, - src_dir, - target_info, - ) - }); - - res_join_handle.unwrap(); - } - - // We've now distributed one worker queue to each thread. - // There should be no queues left to distribute! - debug_assert!(worker_queues.is_empty()); - drop(worker_queues); - - // Grab a reference to these Senders outside the loop, so we can share - // it across each iteration of the loop. - let worker_listeners = worker_listeners.into_bump_slice(); - let msg_tx = msg_tx.clone(); - - macro_rules! shut_down_worker_threads { - () => { - for listener in worker_listeners { - listener - .send(WorkerMsg::Shutdown) - .map_err(|_| LoadingProblem::MsgChannelDied)?; - } - }; - } - - // The root module will have already queued up messages to process, - // and processing those messages will in turn queue up more messages. - loop { - match state_thread_step(arena, state, worker_listeners, &injector, &msg_tx, &msg_rx) - { - Ok(ControlFlow::Break(load_result)) => { - shut_down_worker_threads!(); - - return Ok(load_result); - } - Ok(ControlFlow::Continue(new_state)) => { - state = new_state; - continue; - } - Err(e) => { - shut_down_worker_threads!(); - - return Err(e); - } - } - } - }) - } - .unwrap() -} - -#[allow(clippy::too_many_arguments)] -fn worker_task_step<'a>( - worker_arena: &'a Bump, - worker: &Worker>, - injector: &Injector>, - stealers: &[Stealer>], - worker_msg_rx: &crossbeam::channel::Receiver, - msg_tx: &MsgSender<'a>, - src_dir: &Path, - target_info: TargetInfo, -) -> Result, LoadingProblem<'a>> { - match worker_msg_rx.try_recv() { - Ok(msg) => { - match msg { - WorkerMsg::Shutdown => { - // We've finished all our work. It's time to - // shut down the thread, so when the main thread - // blocks on joining with all the worker threads, - // it can finally exit too! - Ok(ControlFlow::Break(())) - } - WorkerMsg::TaskAdded => { - // Find a task - either from this thread's queue, - // or from the main queue, or from another worker's - // queue - and run it. - // - // There might be no tasks to work on! That could - // happen if another thread is working on a task - // which will later result in more tasks being - // added. In that case, do nothing, and keep waiting - // until we receive a Shutdown message. - if let Some(task) = find_task(worker, injector, stealers) { - let result = - run_task(task, worker_arena, src_dir, msg_tx.clone(), target_info); - - match result { - Ok(()) => {} - Err(LoadingProblem::MsgChannelDied) => { - panic!("Msg channel closed unexpectedly.") - } - Err(LoadingProblem::ParsingFailed(problem)) => { - msg_tx.send(Msg::FailedToParse(problem)).unwrap(); - } - Err(LoadingProblem::FileProblem { filename, error }) => { - msg_tx - .send(Msg::FailedToReadFile { filename, error }) - .unwrap(); - } - Err(other) => { - return Err(other); - } - } - } - - Ok(ControlFlow::Continue(())) - } - } - } - Err(err) => match err { - crossbeam::channel::TryRecvError::Empty => Ok(ControlFlow::Continue(())), - crossbeam::channel::TryRecvError::Disconnected => Ok(ControlFlow::Break(())), - }, - } -} - -#[allow(clippy::too_many_arguments)] -fn worker_task<'a>( - worker_arena: &'a Bump, - worker: Worker>, - injector: &Injector>, - stealers: &[Stealer>], - worker_msg_rx: crossbeam::channel::Receiver, - msg_tx: MsgSender<'a>, - src_dir: &Path, - target_info: TargetInfo, -) -> Result<(), LoadingProblem<'a>> { - // Keep listening until we receive a Shutdown msg - for msg in worker_msg_rx.iter() { - match msg { - WorkerMsg::Shutdown => { - // We've finished all our work. It's time to - // shut down the thread, so when the main thread - // blocks on joining with all the worker threads, - // it can finally exit too! - return Ok(()); - } - WorkerMsg::TaskAdded => { - // Find a task - either from this thread's queue, - // or from the main queue, or from another worker's - // queue - and run it. - // - // There might be no tasks to work on! That could - // happen if another thread is working on a task - // which will later result in more tasks being - // added. In that case, do nothing, and keep waiting - // until we receive a Shutdown message. - if let Some(task) = find_task(&worker, injector, stealers) { - let result = run_task(task, worker_arena, src_dir, msg_tx.clone(), target_info); - - match result { - Ok(()) => {} - Err(LoadingProblem::MsgChannelDied) => { - panic!("Msg channel closed unexpectedly.") - } - Err(LoadingProblem::ParsingFailed(problem)) => { - msg_tx.send(Msg::FailedToParse(problem)).unwrap(); - } - Err(LoadingProblem::FileProblem { filename, error }) => { - msg_tx - .send(Msg::FailedToReadFile { filename, error }) - .unwrap(); - } - Err(other) => { - return Err(other); - } - } - } - } - } - } - - Ok(()) -} - -fn start_tasks<'a>( - arena: &'a Bump, - state: &mut State<'a>, - work: MutSet<(ModuleId, Phase)>, - injector: &Injector>, - worker_listeners: &'a [Sender], -) -> Result<(), LoadingProblem<'a>> { - for (module_id, phase) in work { - for task in start_phase(module_id, phase, arena, state) { - enqueue_task(injector, worker_listeners, task)? - } - } - - Ok(()) -} - -macro_rules! debug_print_ir { - ($state:expr, $flag:path) => { - dbg_do!($flag, { - let procs_string = $state - .procedures - .values() - .map(|proc| proc.to_pretty(200)) - .collect::>(); - - let result = procs_string.join("\n"); - - eprintln!("{}", result); - }) - }; -} - -/// Report modules that are imported, but from which nothing is used -fn report_unused_imported_modules<'a>( - state: &mut State<'a>, - module_id: ModuleId, - constrained_module: &ConstrainedModule, -) { - let mut unused_imported_modules = constrained_module.imported_modules.clone(); - - for symbol in constrained_module.module.referenced_values.iter() { - unused_imported_modules.remove(&symbol.module_id()); - } - - for symbol in constrained_module.module.referenced_types.iter() { - unused_imported_modules.remove(&symbol.module_id()); - } - - let existing = match state.module_cache.can_problems.entry(module_id) { - Vacant(entry) => entry.insert(std::vec::Vec::new()), - Occupied(entry) => entry.into_mut(), - }; - - for (unused, region) in unused_imported_modules.drain() { - if !unused.is_builtin() { - existing.push(roc_problem::can::Problem::UnusedImport(unused, region)); - } - } -} - -fn update<'a>( - mut state: State<'a>, - msg: Msg<'a>, - msg_tx: MsgSender<'a>, - injector: &Injector>, - worker_listeners: &'a [Sender], - arena: &'a Bump, -) -> Result, LoadingProblem<'a>> { - use self::Msg::*; - - match msg { - Many(messages) => { - // enqueue all these message - for msg in messages { - msg_tx - .send(msg) - .map_err(|_| LoadingProblem::MsgChannelDied)?; - } - - Ok(state) - } - Header(header) => { - use HeaderFor::*; - - log!("loaded header for {:?}", header.module_id); - let home = header.module_id; - - let mut work = MutSet::default(); - - { - let mut shorthands = (*state.arc_shorthands).lock(); - - for (shorthand, package_name) in header.packages.iter() { - shorthands.insert(shorthand, *package_name); - } - - if let PkgConfig { - config_shorthand, .. - } = header.header_for - { - work.extend(state.dependencies.notify_package(config_shorthand)); - } - } - - match header.header_for { - App { to_platform } => { - debug_assert!(matches!(state.platform_path, PlatformPath::NotSpecified)); - state.platform_path = PlatformPath::Valid(to_platform); - } - PkgConfig { main_for_host, .. } => { - debug_assert!(matches!(state.platform_data, None)); - - state.platform_data = Some(PlatformData { - module_id: header.module_id, - provides: main_for_host, - }); - - if header.is_root_module { - debug_assert!(matches!(state.platform_path, PlatformPath::NotSpecified)); - state.platform_path = PlatformPath::RootIsPkgConfig; - } - } - Builtin { .. } | Interface => { - if header.is_root_module { - debug_assert!(matches!(state.platform_path, PlatformPath::NotSpecified)); - state.platform_path = PlatformPath::RootIsInterface; - } - } - Hosted { .. } => { - if header.is_root_module { - debug_assert!(matches!(state.platform_path, PlatformPath::NotSpecified)); - state.platform_path = PlatformPath::RootIsHosted; - } - } - } - - // store an ID to name mapping, so we know the file to read when fetching dependencies' headers - for (name, id) in header.deps_by_name.iter() { - state.module_cache.module_names.insert(*id, name.clone()); - } - - // This was a dependency. Write it down and keep processing messages. - let mut exposed_symbols: VecSet = VecSet::with_capacity(header.exposes.len()); - - // TODO can we avoid this loop by storing them as a Set in Header to begin with? - for symbol in header.exposes.iter() { - exposed_symbols.insert(*symbol); - } - - // NOTE we currently re-parse the headers when a module is imported twice. - // We need a proper solution that marks a phase as in-progress so it's not repeated - // debug_assert!(!state.exposed_symbols_by_module.contains_key(&home)); - - state - .exposed_symbols_by_module - .insert(home, exposed_symbols); - - // add the prelude - let mut header = header; - - if ![ModuleId::RESULT, ModuleId::BOOL].contains(&header.module_id) { - header - .package_qualified_imported_modules - .insert(PackageQualified::Unqualified(ModuleId::RESULT)); - - header - .imported_modules - .insert(ModuleId::RESULT, Region::zero()); - - header.exposed_imports.insert( - Ident::from("Result"), - (Symbol::RESULT_RESULT, Region::zero()), - ); - } - - if ![ModuleId::NUM, ModuleId::BOOL, ModuleId::RESULT].contains(&header.module_id) { - header - .package_qualified_imported_modules - .insert(PackageQualified::Unqualified(ModuleId::NUM)); - - header - .imported_modules - .insert(ModuleId::NUM, Region::zero()); - - for (type_name, symbol) in PRELUDE_TYPES { - header - .exposed_imports - .insert(Ident::from(type_name), (symbol, Region::zero())); - } - } - - if header.module_id != ModuleId::BOOL { - header - .package_qualified_imported_modules - .insert(PackageQualified::Unqualified(ModuleId::BOOL)); - - header - .imported_modules - .insert(ModuleId::BOOL, Region::zero()); - - header - .exposed_imports - .insert(Ident::from("Bool"), (Symbol::BOOL_BOOL, Region::zero())); - } - - if !header.module_id.is_builtin() { - header - .package_qualified_imported_modules - .insert(PackageQualified::Unqualified(ModuleId::BOX)); - - header - .imported_modules - .insert(ModuleId::BOX, Region::zero()); - - header - .package_qualified_imported_modules - .insert(PackageQualified::Unqualified(ModuleId::STR)); - - header - .imported_modules - .insert(ModuleId::STR, Region::zero()); - - header - .package_qualified_imported_modules - .insert(PackageQualified::Unqualified(ModuleId::DICT)); - - header - .imported_modules - .insert(ModuleId::DICT, Region::zero()); - - header - .package_qualified_imported_modules - .insert(PackageQualified::Unqualified(ModuleId::SET)); - - header - .imported_modules - .insert(ModuleId::SET, Region::zero()); - - header - .package_qualified_imported_modules - .insert(PackageQualified::Unqualified(ModuleId::LIST)); - - header - .imported_modules - .insert(ModuleId::LIST, Region::zero()); - } - - state - .module_cache - .imports - .entry(header.module_id) - .or_default() - .extend( - header - .package_qualified_imported_modules - .iter() - .map(|x| *x.as_inner()), - ); - - work.extend(state.dependencies.add_module( - header.module_id, - &header.package_qualified_imported_modules, - state.goal_phase, - )); - - state.module_cache.headers.insert(header.module_id, header); - - start_tasks(arena, &mut state, work, injector, worker_listeners)?; - - let work = state.dependencies.notify(home, Phase::LoadHeader); - - start_tasks(arena, &mut state, work, injector, worker_listeners)?; - - Ok(state) - } - Parsed(parsed) => { - state - .module_cache - .sources - .insert(parsed.module_id, (parsed.module_path.clone(), parsed.src)); - - // If this was an app module, set the output path to be - // the module's declared "name". - // - // e.g. for `app "blah"` we should generate an output file named "blah" - match &parsed.module_name { - ModuleNameEnum::App(output_str) => match output_str { - StrLiteral::PlainLine(path) => { - state.output_path = Some(path); - } - _ => { - todo!("TODO gracefully handle a malformed string literal after `app` keyword."); - } - }, - ModuleNameEnum::PkgConfig - | ModuleNameEnum::Interface(_) - | ModuleNameEnum::Hosted(_) => {} - } - - let module_id = parsed.module_id; - - state.module_cache.parsed.insert(parsed.module_id, parsed); - - let work = state.dependencies.notify(module_id, Phase::Parse); - - start_tasks(arena, &mut state, work, injector, worker_listeners)?; - - Ok(state) - } - - CanonicalizedAndConstrained(CanAndCon { - constrained_module, - canonicalization_problems, - module_docs, - }) => { - let module_id = constrained_module.module.module_id; - log!("generated constraints for {:?}", module_id); - state - .module_cache - .can_problems - .insert(module_id, canonicalization_problems); - - if let Some(docs) = module_docs { - state.module_cache.documentation.insert(module_id, docs); - } - - report_unused_imported_modules(&mut state, module_id, &constrained_module); - - state - .module_cache - .aliases - .insert(module_id, constrained_module.module.aliases.clone()); - - state - .module_cache - .abilities - .insert(module_id, constrained_module.module.abilities_store.clone()); - - state - .module_cache - .constrained - .insert(module_id, constrained_module); - - let work = state - .dependencies - .notify(module_id, Phase::CanonicalizeAndConstrain); - - start_tasks(arena, &mut state, work, injector, worker_listeners)?; - - Ok(state) - } - SolvedTypes { - module_id, - ident_ids, - solved_module, - solved_subs, - decls, - dep_idents, - mut module_timing, - abilities_store, - } => { - log!("solved types for {:?}", module_id); - module_timing.end_time = SystemTime::now(); - - state - .module_cache - .type_problems - .insert(module_id, solved_module.problems); - - let work = state.dependencies.notify(module_id, Phase::SolveTypes); - - // if there is a platform, the Package-Config module provides host-exposed, - // otherwise the App module exposes host-exposed - let is_host_exposed = match state.platform_data { - None => module_id == state.root_id, - Some(ref platform_data) => module_id == platform_data.module_id, - }; - - if is_host_exposed { - state.exposed_to_host.values.extend( - solved_module - .exposed_vars_by_symbol - .iter() - .filter_map(|(k, v)| { - if abilities_store.is_specialization_name(*k) { - None - } else { - Some((*k, *v)) - } - }), - ); - - state - .exposed_to_host - .closure_types - .extend(solved_module.aliases.keys().copied()); - } - - if is_host_exposed && state.goal_phase == Phase::SolveTypes { - debug_assert!(work.is_empty()); - debug_assert!(state.dependencies.solved_all()); - - state.timings.insert(module_id, module_timing); - - let documentation = { - let mut empty = MutMap::default(); - std::mem::swap(&mut empty, &mut state.module_cache.documentation); - - empty - }; - - msg_tx - .send(Msg::FinishedAllTypeChecking { - solved_subs, - exposed_vars_by_symbol: solved_module.exposed_vars_by_symbol, - exposed_aliases_by_symbol: solved_module.aliases, - dep_idents, - documentation, - abilities_store, - }) - .map_err(|_| LoadingProblem::MsgChannelDied)?; - - // bookkeeping - state.declarations_by_id.insert(module_id, decls); - state.constrained_ident_ids.insert(module_id, ident_ids); - - // As far as type-checking goes, once we've solved - // the originally requested module, we're all done! - return Ok(state); - } else { - state.exposed_types.insert( - module_id, - ExposedModuleTypes { - stored_vars_by_symbol: solved_module.stored_vars_by_symbol, - storage_subs: solved_module.storage_subs, - solved_specializations: solved_module.solved_specializations, - }, - ); - - if state.goal_phase > Phase::SolveTypes { - let layout_cache = state - .layout_caches - .pop() - .unwrap_or_else(|| LayoutCache::new(state.target_info)); - - let typechecked = TypeCheckedModule { - module_id, - layout_cache, - module_timing, - solved_subs, - decls, - ident_ids, - abilities_store, - }; - - state - .module_cache - .typechecked - .insert(module_id, typechecked); - } else { - state.constrained_ident_ids.insert(module_id, ident_ids); - state.timings.insert(module_id, module_timing); - } - - start_tasks(arena, &mut state, work, injector, worker_listeners)?; - } - - Ok(state) - } - FoundSpecializations { - module_id, - procs_base, - solved_subs, - ident_ids, - layout_cache, - module_timing, - abilities_store, - } => { - log!("found specializations for {:?}", module_id); - - let subs = solved_subs.into_inner(); - - state - .module_cache - .top_level_thunks - .entry(module_id) - .or_default() - .extend(procs_base.module_thunks.iter().copied()); - - let found_specializations_module = FoundSpecializationsModule { - module_id, - ident_ids, - layout_cache, - procs_base, - subs, - module_timing, - abilities_store, - }; - - state - .module_cache - .found_specializations - .insert(module_id, found_specializations_module); - - let work = state - .dependencies - .notify(module_id, Phase::FindSpecializations); - - start_tasks(arena, &mut state, work, injector, worker_listeners)?; - - Ok(state) - } - MadeSpecializations { - module_id, - mut ident_ids, - mut update_mode_ids, - subs, - procedures, - external_specializations_requested, - module_timing, - layout_cache, - .. - } => { - log!("made specializations for {:?}", module_id); - - // in the future, layouts will be in SoA form and we'll want to hold on to this data - let _ = layout_cache; - - state.procedures.extend(procedures); - state.timings.insert(module_id, module_timing); - - let work = state - .dependencies - .notify(module_id, Phase::MakeSpecializations); - - if work.is_empty() - && state.dependencies.solved_all() - && state.goal_phase == Phase::MakeSpecializations - { - debug_print_ir!(state, ROC_PRINT_IR_AFTER_SPECIALIZATION); - - Proc::insert_reset_reuse_operations( - arena, - module_id, - &mut ident_ids, - &mut update_mode_ids, - &mut state.procedures, - ); - - debug_print_ir!(state, ROC_PRINT_IR_AFTER_RESET_REUSE); - - Proc::insert_refcount_operations( - arena, - module_id, - &mut ident_ids, - &mut update_mode_ids, - &mut state.procedures, - ); - - debug_print_ir!(state, ROC_PRINT_IR_AFTER_REFCOUNT); - - // This is not safe with the new non-recursive RC updates that we do for tag unions - // - // Proc::optimize_refcount_operations( - // arena, - // module_id, - // &mut ident_ids, - // &mut state.procedures, - // ); - - state.constrained_ident_ids.insert(module_id, ident_ids); - - for (module_id, requested) in external_specializations_requested { - let existing = match state - .module_cache - .external_specializations_requested - .entry(module_id) - { - Vacant(entry) => entry.insert(vec![]), - Occupied(entry) => entry.into_mut(), - }; - - existing.push(requested); - } - - // use the subs of the root module; - // this is used in the repl to find the type of `main` - let subs = if module_id == state.root_id { - subs - } else { - state.root_subs.clone().unwrap() - }; - - msg_tx - .send(Msg::FinishedAllSpecialization { - subs, - // TODO thread through mono problems - exposed_to_host: state.exposed_to_host.clone(), - }) - .map_err(|_| LoadingProblem::MsgChannelDied)?; - - // As far as type-checking goes, once we've solved - // the originally requested module, we're all done! - return Ok(state); - } else { - // record the subs of the root module; - // this is used in the repl to find the type of `main` - if module_id == state.root_id { - state.root_subs = Some(subs); - } - - state.constrained_ident_ids.insert(module_id, ident_ids); - - for (module_id, requested) in external_specializations_requested { - let existing = match state - .module_cache - .external_specializations_requested - .entry(module_id) - { - Vacant(entry) => entry.insert(vec![]), - Occupied(entry) => entry.into_mut(), - }; - - existing.push(requested); - } - - start_tasks(arena, &mut state, work, injector, worker_listeners)?; - } - - Ok(state) - } - Msg::FinishedAllTypeChecking { .. } => { - unreachable!(); - } - Msg::FinishedAllSpecialization { .. } => { - unreachable!(); - } - Msg::FailedToParse(_) => { - unreachable!(); - } - Msg::FailedToReadFile { .. } => { - unreachable!(); - } - } -} - -fn finish_specialization( - state: State, - subs: Subs, - exposed_to_host: ExposedToHost, -) -> Result { - if false { - println!( - "total Type clones: {} ", - roc_types::types::get_type_clone_count() - ); - } - let module_ids = Arc::try_unwrap(state.arc_modules) - .unwrap_or_else(|_| panic!("There were still outstanding Arc references to module_ids")) - .into_inner() - .into_module_ids(); - - let interns = Interns { - module_ids, - all_ident_ids: state.constrained_ident_ids, - }; - - let State { - procedures, - module_cache, - output_path, - platform_path, - platform_data, - .. - } = state; - - let ModuleCache { - type_problems, - can_problems, - sources, - .. - } = module_cache; - - let sources: MutMap)> = sources - .into_iter() - .map(|(id, (path, src))| (id, (path, src.into()))) - .collect(); - - let path_to_platform = { - use PlatformPath::*; - let package_name = match platform_path { - Valid(To::ExistingPackage(shorthand)) => { - match (*state.arc_shorthands).lock().get(shorthand) { - Some(p_or_p) => *p_or_p, - None => unreachable!(), - } - } - Valid(To::NewPackage(p_or_p)) => p_or_p, - other => { - let buf = to_missing_platform_report(state.root_id, other); - return Err(LoadingProblem::FormattedReport(buf)); - } - }; - - package_name.0 - }; - - let platform_path = path_to_platform.into(); - - let entry_point = { - let symbol = match platform_data { - None => { - debug_assert_eq!(exposed_to_host.values.len(), 1); - *exposed_to_host.values.iter().next().unwrap().0 - } - Some(PlatformData { provides, .. }) => provides, - }; - - match procedures.keys().find(|(s, _)| *s == symbol) { - Some((_, layout)) => EntryPoint { - layout: *layout, - symbol, - }, - None => { - // the entry point is not specialized. This can happen if the repl output - // is a function value - EntryPoint { - layout: roc_mono::ir::ProcLayout { - arguments: &[], - result: Layout::struct_no_name_order(&[]), - }, - symbol, - } - } - } - }; - - Ok(MonomorphizedModule { - can_problems, - type_problems, - output_path: output_path.unwrap_or(DEFAULT_APP_OUTPUT_PATH).into(), - platform_path, - exposed_to_host, - module_id: state.root_id, - subs, - interns, - procedures, - entry_point, - sources, - timings: state.timings, - }) -} - -fn finish( - state: State, - solved: Solved, - exposed_aliases_by_symbol: MutMap, - exposed_vars_by_symbol: Vec<(Symbol, Variable)>, - dep_idents: IdentIdsByModule, - documentation: MutMap, - abilities_store: AbilitiesStore, -) -> LoadedModule { - let module_ids = Arc::try_unwrap(state.arc_modules) - .unwrap_or_else(|_| panic!("There were still outstanding Arc references to module_ids")) - .into_inner() - .into_module_ids(); - - let interns = Interns { - module_ids, - all_ident_ids: state.constrained_ident_ids, - }; - - let sources = state - .module_cache - .sources - .into_iter() - .map(|(id, (path, src))| (id, (path, src.into()))) - .collect(); - - let exposed_values = exposed_vars_by_symbol.iter().map(|x| x.0).collect(); - - LoadedModule { - module_id: state.root_id, - interns, - solved, - can_problems: state.module_cache.can_problems, - type_problems: state.module_cache.type_problems, - declarations_by_id: state.declarations_by_id, - dep_idents, - exposed_aliases: exposed_aliases_by_symbol, - exposed_values, - exposed_to_host: exposed_vars_by_symbol.into_iter().collect(), - sources, - timings: state.timings, - documentation, - abilities_store, - } -} - -/// Load a PkgConfig.roc file -fn load_pkg_config<'a>( - arena: &'a Bump, - src_dir: &Path, - shorthand: &'a str, - app_module_id: ModuleId, - module_ids: Arc>>, - ident_ids_by_module: SharedIdentIdsByModule, -) -> Result, LoadingProblem<'a>> { - let module_start_time = SystemTime::now(); - - let filename = PathBuf::from(src_dir); - - let file_io_start = SystemTime::now(); - let file = fs::read(&filename); - let file_io_duration = file_io_start.elapsed().unwrap(); - - match file { - Ok(bytes_vec) => { - let parse_start = SystemTime::now(); - let bytes = arena.alloc(bytes_vec); - let parse_state = roc_parse::state::State::new(bytes); - let parsed = roc_parse::module::parse_header(arena, parse_state.clone()); - let parse_header_duration = parse_start.elapsed().unwrap(); - - // Insert the first entries for this module's timings - let mut pkg_module_timing = ModuleTiming::new(module_start_time); - - pkg_module_timing.read_roc_file = file_io_duration; - pkg_module_timing.parse_header = parse_header_duration; - - match parsed { - Ok((ast::Module::Interface { header }, _parse_state)) => { - Err(LoadingProblem::UnexpectedHeader(format!( - "expected platform/package module, got Interface with header\n{:?}", - header - ))) - } - Ok((ast::Module::Hosted { header }, _parse_state)) => { - Err(LoadingProblem::UnexpectedHeader(format!( - "expected platform/package module, got Hosted module with header\n{:?}", - header - ))) - } - Ok((ast::Module::App { header }, _parse_state)) => { - Err(LoadingProblem::UnexpectedHeader(format!( - "expected platform/package module, got App with header\n{:?}", - header - ))) - } - Ok((ast::Module::Platform { header }, parser_state)) => { - // make a Package-Config module that ultimately exposes `main` to the host - let pkg_config_module_msg = fabricate_pkg_config_module( - arena, - shorthand, - Some(app_module_id), - filename, - parser_state, - module_ids.clone(), - ident_ids_by_module, - &header, - pkg_module_timing, - ) - .1; - - Ok(pkg_config_module_msg) - } - Err(fail) => Err(LoadingProblem::ParsingFailed( - fail.map_problem(SyntaxError::Header) - .into_file_error(filename), - )), - } - } - - Err(err) => Err(LoadingProblem::FileProblem { - filename, - error: err.kind(), - }), - } -} - -fn load_builtin_module_help<'a>( - arena: &'a Bump, - filename: &str, - src_bytes: &'a str, -) -> (HeaderInfo<'a>, roc_parse::state::State<'a>) { - let is_root_module = false; - let opt_shorthand = None; - - let filename = PathBuf::from(filename); - - let parse_state = roc_parse::state::State::new(src_bytes.as_bytes()); - let parsed = roc_parse::module::parse_header(arena, parse_state.clone()); - - match parsed { - Ok((ast::Module::Interface { header }, parse_state)) => { - let info = HeaderInfo { - loc_name: Loc { - region: header.name.region, - value: ModuleNameEnum::Interface(header.name.value), - }, - filename, - is_root_module, - opt_shorthand, - packages: &[], - exposes: unspace(arena, header.exposes.items), - imports: unspace(arena, header.imports.items), - extra: HeaderFor::Builtin { - generates_with: &[], - }, - }; - - (info, parse_state) - } - Ok(_) => panic!("invalid header format for builtin module"), - Err(e) => panic!( - "Hit a parse error in the header of {:?}:\n{:?}", - filename, e - ), - } -} - -fn load_builtin_module<'a>( - arena: &'a Bump, - module_ids: Arc>>, - ident_ids_by_module: SharedIdentIdsByModule, - module_timing: ModuleTiming, - module_id: ModuleId, - module_name: &str, -) -> (ModuleId, Msg<'a>) { - let src_bytes = module_source(module_id); - - let (info, parse_state) = load_builtin_module_help(arena, module_name, src_bytes); - - send_header( - info, - parse_state, - module_ids, - ident_ids_by_module, - module_timing, - ) -} - -/// Load a module by its module name, rather than by its filename -fn load_module<'a>( - arena: &'a Bump, - src_dir: &Path, - module_name: PQModuleName<'a>, - module_ids: Arc>>, - arc_shorthands: Arc>>>, - ident_ids_by_module: SharedIdentIdsByModule, -) -> Result<(ModuleId, Msg<'a>), LoadingProblem<'a>> { - let module_start_time = SystemTime::now(); - - let parse_start = SystemTime::now(); - let parse_header_duration = parse_start.elapsed().unwrap(); - - // Insert the first entries for this module's timings - let mut module_timing = ModuleTiming::new(module_start_time); - - module_timing.read_roc_file = Default::default(); - module_timing.parse_header = parse_header_duration; - - macro_rules! load_builtins { - ($($name:literal, $module_id:path)*) => { - match module_name.as_inner().as_str() { - $( - $name => { - return Ok(load_builtin_module( - arena, - module_ids, - ident_ids_by_module, - module_timing, - $module_id, - concat!($name, ".roc") - )); - } - )* - _ => { /* fall through */ } - }} - } - - load_builtins! { - "Result", ModuleId::RESULT - "List", ModuleId::LIST - "Str", ModuleId::STR - "Dict", ModuleId::DICT - "Set", ModuleId::SET - "Num", ModuleId::NUM - "Bool", ModuleId::BOOL - "Box", ModuleId::BOX - "Encode", ModuleId::ENCODE - "Json", ModuleId::JSON - } - - let (filename, opt_shorthand) = module_name_to_path(src_dir, module_name, arc_shorthands); - - load_filename( - arena, - filename, - false, - opt_shorthand, - module_ids, - ident_ids_by_module, - module_start_time, - ) -} - -fn module_name_to_path<'a>( - src_dir: &Path, - module_name: PQModuleName<'a>, - arc_shorthands: Arc>>>, -) -> (PathBuf, Option<&'a str>) { - let mut filename = PathBuf::new(); - - filename.push(src_dir); - - let opt_shorthand; - match module_name { - PQModuleName::Unqualified(name) => { - opt_shorthand = None; - // Convert dots in module name to directories - for part in name.split(MODULE_SEPARATOR) { - filename.push(part); - } - } - PQModuleName::Qualified(shorthand, name) => { - opt_shorthand = Some(shorthand); - let shorthands = arc_shorthands.lock(); - - match shorthands.get(shorthand) { - Some(PackageName(path)) => { - filename.push(path); - } - None => unreachable!("there is no shorthand named {:?}", shorthand), - } - - // Convert dots in module name to directories - for part in name.split(MODULE_SEPARATOR) { - filename.push(part); - } - } - } - - // End with .roc - filename.set_extension(ROC_FILE_EXTENSION); - - (filename, opt_shorthand) -} - -/// Find a task according to the following algorithm: -/// -/// 1. Look in a local Worker queue. If it has a task, pop it off the queue and return it. -/// 2. If that queue was empty, ask the global queue for a task. -/// 3. If the global queue is also empty, iterate through each Stealer (each Worker queue has a -/// corresponding Stealer, which can steal from it. Stealers can be shared across threads.) -/// -/// Based on https://docs.rs/crossbeam/0.7.3/crossbeam/deque/index.html#examples -fn find_task(local: &Worker, global: &Injector, stealers: &[Stealer]) -> Option { - // Pop a task from the local queue, if not empty. - local.pop().or_else(|| { - // Otherwise, we need to look for a task elsewhere. - iter::repeat_with(|| { - // Try stealing a task from the global queue. - global - .steal() - // Or try stealing a task from one of the other threads. - .or_else(|| stealers.iter().map(|s| s.steal()).collect()) - }) - // Loop while no task was stolen and any steal operation needs to be retried. - .find(|s| !s.is_retry()) - // Extract the stolen task, if there is one. - .and_then(|s| s.success()) - }) -} - -#[allow(clippy::too_many_arguments)] -fn parse_header<'a>( - arena: &'a Bump, - read_file_duration: Duration, - filename: PathBuf, - is_root_module: bool, - opt_shorthand: Option<&'a str>, - module_ids: Arc>>, - ident_ids_by_module: SharedIdentIdsByModule, - src_bytes: &'a [u8], - start_time: SystemTime, -) -> Result<(ModuleId, Msg<'a>), LoadingProblem<'a>> { - let parse_start = SystemTime::now(); - let parse_state = roc_parse::state::State::new(src_bytes); - let parsed = roc_parse::module::parse_header(arena, parse_state.clone()); - let parse_header_duration = parse_start.elapsed().unwrap(); - - // Insert the first entries for this module's timings - let mut module_timing = ModuleTiming::new(start_time); - - module_timing.read_roc_file = read_file_duration; - module_timing.parse_header = parse_header_duration; - - match parsed { - Ok((ast::Module::Interface { header }, parse_state)) => { - let info = HeaderInfo { - loc_name: Loc { - region: header.name.region, - value: ModuleNameEnum::Interface(header.name.value), - }, - filename, - is_root_module, - opt_shorthand, - packages: &[], - exposes: unspace(arena, header.exposes.items), - imports: unspace(arena, header.imports.items), - extra: HeaderFor::Interface, - }; - - Ok(send_header( - info, - parse_state, - module_ids, - ident_ids_by_module, - module_timing, - )) - } - Ok((ast::Module::Hosted { header }, parse_state)) => { - let info = HeaderInfo { - loc_name: Loc { - region: header.name.region, - value: ModuleNameEnum::Hosted(header.name.value), - }, - filename, - is_root_module, - opt_shorthand, - packages: &[], - exposes: unspace(arena, header.exposes.items), - imports: unspace(arena, header.imports.items), - extra: HeaderFor::Hosted { - generates: header.generates, - generates_with: unspace(arena, header.generates_with.items), - }, - }; - - Ok(send_header( - info, - parse_state, - module_ids, - ident_ids_by_module, - module_timing, - )) - } - Ok((ast::Module::App { header }, parse_state)) => { - let mut pkg_config_dir = filename.clone(); - pkg_config_dir.pop(); - - let packages = unspace(arena, header.packages.items); - - let mut exposes = bumpalo::collections::Vec::new_in(arena); - exposes.extend(unspace(arena, header.provides.items)); - - if let Some(provided_types) = header.provides_types { - for provided_type in unspace(arena, provided_types.items) { - let string: &str = provided_type.value.into(); - let exposed_name = ExposedName::new(string); - - exposes.push(Loc::at(provided_type.region, exposed_name)); - } - } - - let exposes = exposes.into_bump_slice(); - - let info = HeaderInfo { - loc_name: Loc { - region: header.name.region, - value: ModuleNameEnum::App(header.name.value), - }, - filename, - is_root_module, - opt_shorthand, - packages, - exposes, - imports: unspace(arena, header.imports.items), - extra: HeaderFor::App { - to_platform: header.to.value, - }, - }; - - let (module_id, app_module_header_msg) = send_header( - info, - parse_state, - module_ids.clone(), - ident_ids_by_module.clone(), - module_timing, - ); - - match header.to.value { - To::ExistingPackage(existing_package) => { - let opt_base_package = packages.iter().find_map(|loc_package_entry| { - let Loc { value, .. } = loc_package_entry; - - if value.shorthand == existing_package { - Some(value) - } else { - None - } - }); - - if let Some(PackageEntry { - shorthand, - package_name: - Loc { - value: package_name, - .. - }, - .. - }) = opt_base_package - { - let package = package_name.0; - - // check whether we can find a Package-Config.roc file - let mut pkg_config_roc = pkg_config_dir; - pkg_config_roc.push(package); - pkg_config_roc.push(PKG_CONFIG_FILE_NAME); - pkg_config_roc.set_extension(ROC_FILE_EXTENSION); - - if pkg_config_roc.as_path().exists() { - let load_pkg_config_msg = load_pkg_config( - arena, - &pkg_config_roc, - shorthand, - module_id, - module_ids, - ident_ids_by_module, - )?; - - Ok(( - module_id, - Msg::Many(vec![app_module_header_msg, load_pkg_config_msg]), - )) - } else { - Err(LoadingProblem::FileProblem { - filename: pkg_config_roc, - error: io::ErrorKind::NotFound, - }) - } - } else { - panic!("could not find base") - } - } - To::NewPackage(_package_name) => Ok((module_id, app_module_header_msg)), - } - } - Ok((ast::Module::Platform { header }, parse_state)) => { - Ok(fabricate_pkg_config_module( - arena, - "", // Use a shorthand of "" - it will be fine for `roc check` and bindgen - None, - filename, - parse_state, - module_ids.clone(), - ident_ids_by_module, - &header, - module_timing, - )) - } - - Err(fail) => Err(LoadingProblem::ParsingFailed( - fail.map_problem(SyntaxError::Header) - .into_file_error(filename), - )), - } -} - -/// Load a module by its filename -#[allow(clippy::too_many_arguments)] -fn load_filename<'a>( - arena: &'a Bump, - filename: PathBuf, - is_root_module: bool, - opt_shorthand: Option<&'a str>, - module_ids: Arc>>, - ident_ids_by_module: SharedIdentIdsByModule, - module_start_time: SystemTime, -) -> Result<(ModuleId, Msg<'a>), LoadingProblem<'a>> { - let file_io_start = SystemTime::now(); - let file = fs::read(&filename); - let file_io_duration = file_io_start.elapsed().unwrap(); - - match file { - Ok(bytes) => parse_header( - arena, - file_io_duration, - filename, - is_root_module, - opt_shorthand, - module_ids, - ident_ids_by_module, - arena.alloc(bytes), - module_start_time, - ), - Err(err) => Err(LoadingProblem::FileProblem { - filename, - error: err.kind(), - }), - } -} - -/// Load a module from a str -/// the `filename` is never read, but used for the module name -#[allow(clippy::too_many_arguments)] -fn load_from_str<'a>( - arena: &'a Bump, - filename: PathBuf, - src: &'a str, - module_ids: Arc>>, - ident_ids_by_module: SharedIdentIdsByModule, - module_start_time: SystemTime, -) -> Result<(ModuleId, Msg<'a>), LoadingProblem<'a>> { - let file_io_start = SystemTime::now(); - let file_io_duration = file_io_start.elapsed().unwrap(); - - parse_header( - arena, - file_io_duration, - filename, - false, - None, - module_ids, - ident_ids_by_module, - src.as_bytes(), - module_start_time, - ) -} - -#[derive(Debug)] -struct HeaderInfo<'a> { - loc_name: Loc>, - filename: PathBuf, - is_root_module: bool, - opt_shorthand: Option<&'a str>, - packages: &'a [Loc>], - exposes: &'a [Loc>], - imports: &'a [Loc>], - extra: HeaderFor<'a>, -} - -#[allow(clippy::too_many_arguments)] -fn send_header<'a>( - info: HeaderInfo<'a>, - parse_state: roc_parse::state::State<'a>, - module_ids: Arc>>, - ident_ids_by_module: SharedIdentIdsByModule, - module_timing: ModuleTiming, -) -> (ModuleId, Msg<'a>) { - use ModuleNameEnum::*; - - let HeaderInfo { - loc_name, - filename, - is_root_module, - opt_shorthand, - packages, - exposes, - imports, - extra, - } = info; - - let declared_name: ModuleName = match &loc_name.value { - PkgConfig => unreachable!(), - App(_) => ModuleName::APP.into(), - Interface(module_name) | Hosted(module_name) => { - // TODO check to see if module_name is consistent with filename. - // If it isn't, report a problem! - - module_name.as_str().into() - } - }; - - let mut imported: Vec<(QualifiedModuleName, Vec, Region)> = - Vec::with_capacity(imports.len()); - let mut imported_modules: MutMap = MutMap::default(); - let mut scope_size = 0; - - for loc_entry in imports { - let (qualified_module_name, exposed) = exposed_from_import(&loc_entry.value); - - scope_size += exposed.len(); - - imported.push((qualified_module_name, exposed, loc_entry.region)); - } - - let num_exposes = exposes.len(); - let mut deps_by_name: MutMap = - HashMap::with_capacity_and_hasher(num_exposes, default_hasher()); - let mut exposed: Vec = Vec::with_capacity(num_exposes); - - // Make sure the module_ids has ModuleIds for all our deps, - // then record those ModuleIds in can_module_ids for later. - let mut scope: MutMap = - HashMap::with_capacity_and_hasher(scope_size, default_hasher()); - let home: ModuleId; - - let ident_ids = { - // Lock just long enough to perform the minimal operations necessary. - let mut module_ids = (*module_ids).lock(); - let mut ident_ids_by_module = (*ident_ids_by_module).lock(); - - let name = match opt_shorthand { - Some(shorthand) => PQModuleName::Qualified(shorthand, declared_name), - None => PQModuleName::Unqualified(declared_name), - }; - home = module_ids.get_or_insert(&name); - - // Ensure this module has an entry in the exposed_ident_ids map. - ident_ids_by_module.get_or_insert(home); - - // For each of our imports, add an entry to deps_by_name - // - // e.g. for `imports [pf.Foo.{ bar }]`, add `Foo` to deps_by_name - // - // Also build a list of imported_values_to_expose (like `bar` above.) - for (qualified_module_name, exposed_idents, region) in imported.into_iter() { - let cloned_module_name = qualified_module_name.module.clone(); - let pq_module_name = match qualified_module_name.opt_package { - None => match opt_shorthand { - Some(shorthand) => { - PQModuleName::Qualified(shorthand, qualified_module_name.module) - } - None => PQModuleName::Unqualified(qualified_module_name.module), - }, - Some(package) => PQModuleName::Qualified(package, cloned_module_name), - }; - - let module_id = module_ids.get_or_insert(&pq_module_name); - imported_modules.insert(module_id, region); - - deps_by_name.insert(pq_module_name, module_id); - - // Add the new exposed idents to the dep module's IdentIds, so - // once that module later gets loaded, its lookups will resolve - // to the same symbols as the ones we're using here. - let ident_ids = ident_ids_by_module.get_or_insert(module_id); - - for ident in exposed_idents { - let ident_id = ident_ids.get_or_insert(&ident); - let symbol = Symbol::new(module_id, ident_id); - - // Since this value is exposed, add it to our module's default scope. - debug_assert!(!scope.contains_key(&ident.clone())); - - scope.insert(ident, (symbol, region)); - } - } - - let ident_ids = ident_ids_by_module.get_mut(&home).unwrap(); - - // Generate IdentIds entries for all values this module exposes. - // This way, when we encounter them in Defs later, they already - // have an IdentIds entry. - // - // We must *not* add them to scope yet, or else the Defs will - // incorrectly think they're shadowing them! - for loc_exposed in exposes.iter() { - // Use get_or_insert here because the ident_ids may already - // created an IdentId for this, when it was imported exposed - // in a dependent module. - // - // For example, if module A has [B.{ foo }], then - // when we get here for B, `foo` will already have - // an IdentId. We must reuse that! - let ident_id = ident_ids.get_or_insert(&loc_exposed.value.as_str().into()); - let symbol = Symbol::new(home, ident_id); - - exposed.push(symbol); - } - - if cfg!(debug_assertions) { - home.register_debug_idents(ident_ids); - } - - ident_ids.clone() - }; - - let package_entries = packages - .iter() - .map(|pkg| { - let pkg = pkg.value; - (pkg.shorthand, pkg.package_name.value) - }) - .collect::>(); - - // Send the deps to the coordinator thread for processing, - // then continue on to parsing and canonicalizing defs. - // - // We always need to send these, even if deps is empty, - // because the coordinator thread needs to receive this message - // to decrement its "pending" count. - let mut package_qualified_imported_modules = MutSet::default(); - for (pq_module_name, module_id) in &deps_by_name { - match pq_module_name { - PackageQualified::Unqualified(_) => { - package_qualified_imported_modules - .insert(PackageQualified::Unqualified(*module_id)); - } - PackageQualified::Qualified(shorthand, _) => { - package_qualified_imported_modules - .insert(PackageQualified::Qualified(shorthand, *module_id)); - } - } - } - - // make sure when we run the bulitin modules in /compiler/builtins/roc that we - // mark these modules as Builtin. Otherwise the builtin functions are not instantiated - // and we just have a bunch of definitions with runtime errors in their bodies - let extra = { - match extra { - HeaderFor::Interface if home.is_builtin() => HeaderFor::Builtin { - generates_with: &[], - }, - _ => extra, - } - }; - - ( - home, - Msg::Header(ModuleHeader { - module_id: home, - module_path: filename, - is_root_module, - exposed_ident_ids: ident_ids, - module_name: loc_name.value, - packages: package_entries, - imported_modules, - package_qualified_imported_modules, - deps_by_name, - exposes: exposed, - parse_state, - exposed_imports: scope, - symbols_from_requires: Vec::new(), - header_for: extra, - module_timing, - }), - ) -} - -#[derive(Debug)] -struct PlatformHeaderInfo<'a> { - filename: PathBuf, - is_root_module: bool, - shorthand: &'a str, - opt_app_module_id: Option, - packages: &'a [Loc>], - provides: &'a [Loc>], - requires: &'a [Loc>], - requires_types: &'a [Loc>], - imports: &'a [Loc>], -} - -// TODO refactor so more logic is shared with `send_header` -#[allow(clippy::too_many_arguments)] -fn send_header_two<'a>( - info: PlatformHeaderInfo<'a>, - parse_state: roc_parse::state::State<'a>, - module_ids: Arc>>, - ident_ids_by_module: SharedIdentIdsByModule, - module_timing: ModuleTiming, -) -> (ModuleId, Msg<'a>) { - let PlatformHeaderInfo { - filename, - shorthand, - is_root_module, - opt_app_module_id, - packages, - provides, - requires, - requires_types, - imports, - } = info; - - let declared_name: ModuleName = "".into(); - let mut symbols_from_requires = Vec::with_capacity(requires.len()); - - let mut imported: Vec<(QualifiedModuleName, Vec, Region)> = - Vec::with_capacity(imports.len()); - let mut imported_modules: MutMap = MutMap::default(); - - let num_exposes = provides.len(); - let mut deps_by_name: MutMap = - HashMap::with_capacity_and_hasher(num_exposes, default_hasher()); - - // Add standard imports, if there is an app module. - // (There might not be, e.g. when running `roc check Package-Config.roc` or - // when generating bindings.) - if let Some(app_module_id) = opt_app_module_id { - imported_modules.insert(app_module_id, Region::zero()); - deps_by_name.insert( - PQModuleName::Unqualified(ModuleName::APP.into()), - app_module_id, - ); - } - - let mut scope_size = 0; - - for loc_entry in imports { - let (qualified_module_name, exposed) = exposed_from_import(&loc_entry.value); - - scope_size += exposed.len(); - - imported.push((qualified_module_name, exposed, loc_entry.region)); - } - - let mut exposed: Vec = Vec::with_capacity(num_exposes); - - // Make sure the module_ids has ModuleIds for all our deps, - // then record those ModuleIds in can_module_ids for later. - let mut scope: MutMap = - HashMap::with_capacity_and_hasher(scope_size, default_hasher()); - let home: ModuleId; - - let mut ident_ids = { - // Lock just long enough to perform the minimal operations necessary. - let mut module_ids = (*module_ids).lock(); - let mut ident_ids_by_module = (*ident_ids_by_module).lock(); - - let name = PQModuleName::Qualified(shorthand, declared_name); - home = module_ids.get_or_insert(&name); - - // Ensure this module has an entry in the exposed_ident_ids map. - ident_ids_by_module.get_or_insert(home); - - // For each of our imports, add an entry to deps_by_name - // - // e.g. for `imports [pf.Foo.{ bar }]`, add `Foo` to deps_by_name - // - // Also build a list of imported_values_to_expose (like `bar` above.) - for (qualified_module_name, exposed_idents, region) in imported.into_iter() { - let cloned_module_name = qualified_module_name.module.clone(); - let pq_module_name = match qualified_module_name.opt_package { - None => PQModuleName::Qualified(shorthand, qualified_module_name.module), - Some(package) => PQModuleName::Qualified(package, cloned_module_name.clone()), - }; - - let module_id = module_ids.get_or_insert(&pq_module_name); - imported_modules.insert(module_id, region); - - deps_by_name.insert(pq_module_name, module_id); - - // Add the new exposed idents to the dep module's IdentIds, so - // once that module later gets loaded, its lookups will resolve - // to the same symbols as the ones we're using here. - let ident_ids = ident_ids_by_module.get_or_insert(module_id); - - for ident in exposed_idents { - let ident_id = ident_ids.get_or_insert(&ident); - let symbol = Symbol::new(module_id, ident_id); - - // Since this value is exposed, add it to our module's default scope. - debug_assert!(!scope.contains_key(&ident.clone())); - - scope.insert(ident, (symbol, region)); - } - } - - { - // If we don't have an app module id (e.g. because we're doing - // `roc check Package-Config.roc` or because we're doing bindgen), - // insert the `requires` symbols into the platform module's IdentIds. - // - // Otherwise, get them from the app module's IdentIds, because it - // should already have a symbol for each `requires` entry, and we - // want to make sure we're referencing the same symbols! - let module_id = opt_app_module_id.unwrap_or(home); - let ident_ids = ident_ids_by_module.get_or_insert(module_id); - - for entry in requires { - let entry = entry.value; - let ident: Ident = entry.ident.value.into(); - let ident_id = ident_ids.get_or_insert(&ident); - let symbol = Symbol::new(module_id, ident_id); - - // Since this value is exposed, add it to our module's default scope. - debug_assert!(!scope.contains_key(&ident.clone())); - - scope.insert(ident, (symbol, entry.ident.region)); - symbols_from_requires.push((Loc::at(entry.ident.region, symbol), entry.ann)); - } - - for entry in requires_types { - let string: &str = entry.value.into(); - let ident: Ident = string.into(); - let ident_id = ident_ids.get_or_insert(&ident); - let symbol = Symbol::new(module_id, ident_id); - - // Since this value is exposed, add it to our module's default scope. - debug_assert!(!scope.contains_key(&ident.clone())); - scope.insert(ident, (symbol, entry.region)); - } - } - - let ident_ids = ident_ids_by_module.get_mut(&home).unwrap(); - - // Generate IdentIds entries for all values this module exposes. - // This way, when we encounter them in Defs later, they already - // have an IdentIds entry. - // - // We must *not* add them to scope yet, or else the Defs will - // incorrectly think they're shadowing them! - for loc_exposed in provides.iter() { - // Use get_or_insert here because the ident_ids may already - // created an IdentId for this, when it was imported exposed - // in a dependent module. - // - // For example, if module A has [B.{ foo }], then - // when we get here for B, `foo` will already have - // an IdentId. We must reuse that! - let ident_id = ident_ids.get_or_insert(&loc_exposed.value.as_str().into()); - let symbol = Symbol::new(home, ident_id); - - exposed.push(symbol); - } - - if cfg!(debug_assertions) { - home.register_debug_idents(ident_ids); - } - - ident_ids.clone() - }; - - let package_entries = packages - .iter() - .map(|pkg| (pkg.value.shorthand, pkg.value.package_name.value)) - .collect::>(); - - // Send the deps to the coordinator thread for processing, - // then continue on to parsing and canonicalizing defs. - // - // We always need to send these, even if deps is empty, - // because the coordinator thread needs to receive this message - // to decrement its "pending" count. - let module_name = ModuleNameEnum::PkgConfig; - - let main_for_host = { - let ident_str: Ident = provides[0].value.as_str().into(); - let ident_id = ident_ids.get_or_insert(&ident_str); - - Symbol::new(home, ident_id) - }; - - let extra = HeaderFor::PkgConfig { - config_shorthand: shorthand, - platform_main_type: requires[0].value, - main_for_host, - }; - - let mut package_qualified_imported_modules = MutSet::default(); - for (pq_module_name, module_id) in &deps_by_name { - match pq_module_name { - PackageQualified::Unqualified(_) => { - package_qualified_imported_modules - .insert(PackageQualified::Unqualified(*module_id)); - } - PackageQualified::Qualified(shorthand, _) => { - package_qualified_imported_modules - .insert(PackageQualified::Qualified(shorthand, *module_id)); - } - } - } - - ( - home, - Msg::Header(ModuleHeader { - module_id: home, - module_path: filename, - is_root_module, - exposed_ident_ids: ident_ids, - module_name, - packages: package_entries, - imported_modules, - package_qualified_imported_modules, - deps_by_name, - exposes: exposed, - parse_state, - exposed_imports: scope, - module_timing, - symbols_from_requires, - header_for: extra, - }), - ) -} - -impl<'a> BuildTask<'a> { - // TODO trim down these arguments - possibly by moving Constraint into Module - #[allow(clippy::too_many_arguments)] - fn solve_module( - mut module: Module, - ident_ids: IdentIds, - module_timing: ModuleTiming, - constraints: Constraints, - constraint: ConstraintSoa, - pending_derives: PendingDerives, - var_store: VarStore, - imported_modules: MutMap, - exposed_types: &ExposedByModule, - dep_idents: IdentIdsByModule, - declarations: Vec, - cached_subs: CachedSubs, - ) -> Self { - let exposed_by_module = exposed_types.retain_modules(imported_modules.keys()); - - let abilities_store = &mut module.abilities_store; - - for module in imported_modules.keys() { - let exposed = exposed_by_module - .get(module) - .unwrap_or_else(|| internal_error!("No exposed types for {:?}", module)); - let ExposedModuleTypes { - solved_specializations, - .. - } = exposed; - for ((member, typ), specialization) in solved_specializations.iter() { - abilities_store.register_specialization_for_type( - *member, - *typ, - specialization.clone(), - ); - } - } - - let exposed_for_module = - ExposedForModule::new(module.referenced_values.iter(), exposed_by_module); - - let imported_builtins = module - .referenced_values - .iter() - .filter(|s| s.is_builtin()) - .copied() - .collect(); - - // Next, solve this module in the background. - Self::Solve { - module, - ident_ids, - imported_builtins, - exposed_for_module, - constraints, - constraint, - pending_derives, - var_store, - declarations, - dep_idents, - module_timing, - cached_subs, - } - } -} - -fn add_imports( - subs: &mut Subs, - abilities_store: &mut AbilitiesStore, - mut exposed_for_module: ExposedForModule, - def_types: &mut Vec<(Symbol, Loc)>, - rigid_vars: &mut Vec, -) -> Vec { - let mut import_variables = Vec::new(); - - for symbol in exposed_for_module.imported_values { - let module_id = symbol.module_id(); - match exposed_for_module.exposed_by_module.get_mut(&module_id) { - Some(ExposedModuleTypes { - stored_vars_by_symbol, - storage_subs, - solved_specializations: _, - }) => { - let variable = match stored_vars_by_symbol.iter().find(|(s, _)| *s == symbol) { - None => { - // Today we define builtins in each module that uses them - // so even though they have a different module name from - // the surrounding module, they are not technically imported - debug_assert!(symbol.is_builtin()); - continue; - } - Some((_, x)) => *x, - }; - - let copied_import = storage_subs.export_variable_to(subs, variable); - - def_types.push(( - symbol, - Loc::at_zero(roc_types::types::Type::Variable(copied_import.variable)), - )); - - // not a typo; rigids are turned into flex during type inference, but when imported we must - // consider them rigid variables - rigid_vars.extend(copied_import.rigid); - rigid_vars.extend(copied_import.flex); - - // Rigid vars bound to abilities are also treated like rigids. - rigid_vars.extend(copied_import.rigid_able); - rigid_vars.extend(copied_import.flex_able); - - import_variables.extend(copied_import.registered); - - if abilities_store.is_ability_member_name(symbol) { - abilities_store.resolved_imported_member_var(symbol, copied_import.variable); - } - } - None => { - internal_error!("Imported module {:?} is not available", module_id) - } - } - } - - import_variables -} - -#[allow(clippy::complexity)] -fn run_solve_solve( - imported_builtins: Vec, - exposed_for_module: ExposedForModule, - mut constraints: Constraints, - constraint: ConstraintSoa, - pending_derives: PendingDerives, - mut var_store: VarStore, - module: Module, -) -> ( - Solved, - SolvedSpecializations, - Vec<(Symbol, Variable)>, - Vec, - AbilitiesStore, -) { - let Module { - exposed_symbols, - aliases, - rigid_variables, - mut abilities_store, - .. - } = module; - - let (mut rigid_vars, mut def_types) = - constrain_builtin_imports(borrow_stdlib(), imported_builtins, &mut var_store); - - let mut subs = Subs::new_from_varstore(var_store); - - let import_variables = add_imports( - &mut subs, - &mut abilities_store, - exposed_for_module, - &mut def_types, - &mut rigid_vars, - ); - - let actual_constraint = - constraints.let_import_constraint(rigid_vars, def_types, constraint, &import_variables); - - let mut solve_aliases = default_aliases(); - - for (name, (_, alias)) in aliases.iter() { - solve_aliases.insert(*name, alias.clone()); - } - - let (solved_subs, solved_specializations, exposed_vars_by_symbol, problems, abilities_store) = { - let (solved_subs, solved_env, problems, abilities_store) = roc_solve::module::run_solve( - &constraints, - actual_constraint, - rigid_variables, - subs, - solve_aliases, - abilities_store, - pending_derives, - ); - - let module_id = module.module_id; - // Figure out what specializations belong to this module - let solved_specializations: SolvedSpecializations = abilities_store - .iter_specializations() - .filter(|((member, typ), _)| { - // This module solved this specialization if either the member or the type comes from the - // module. - member.module_id() == module_id || typ.module_id() == module_id - }) - .map(|(key, specialization)| (key, specialization.clone())) - .collect(); - - let is_specialization_symbol = - |sym| solved_specializations.values().any(|ms| ms.symbol == sym); - - // Expose anything that is explicitly exposed by the header, or is a specialization of an - // ability. - let exposed_vars_by_symbol: Vec<_> = solved_env - .vars_by_symbol() - .filter(|(k, _)| exposed_symbols.contains(k) || is_specialization_symbol(*k)) - .collect(); - - ( - solved_subs, - solved_specializations, - exposed_vars_by_symbol, - problems, - abilities_store, - ) - }; - - ( - solved_subs, - solved_specializations, - exposed_vars_by_symbol, - problems, - abilities_store, - ) -} - -#[allow(clippy::too_many_arguments)] -fn run_solve<'a>( - module: Module, - ident_ids: IdentIds, - mut module_timing: ModuleTiming, - imported_builtins: Vec, - exposed_for_module: ExposedForModule, - constraints: Constraints, - constraint: ConstraintSoa, - pending_derives: PendingDerives, - var_store: VarStore, - decls: Vec, - dep_idents: IdentIdsByModule, - cached_subs: CachedSubs, -) -> Msg<'a> { - let solve_start = SystemTime::now(); - - let module_id = module.module_id; - - // TODO remove when we write builtins in roc - let aliases = module.aliases.clone(); - - let (solved_subs, solved_specializations, exposed_vars_by_symbol, problems, abilities_store) = { - if module_id.is_builtin() { - match cached_subs.lock().remove(&module_id) { - None => run_solve_solve( - imported_builtins, - exposed_for_module, - constraints, - constraint, - pending_derives, - var_store, - module, - ), - Some((subs, exposed_vars_by_symbol)) => { - ( - Solved(subs), - // TODO(abilities) cache abilities for builtins - VecMap::default(), - exposed_vars_by_symbol.to_vec(), - vec![], - // TODO(abilities) cache abilities for builtins - AbilitiesStore::default(), - ) - } - } - } else { - run_solve_solve( - imported_builtins, - exposed_for_module, - constraints, - constraint, - pending_derives, - var_store, - module, - ) - } - }; - - let mut solved_subs = solved_subs; - let (storage_subs, stored_vars_by_symbol) = - roc_solve::module::exposed_types_storage_subs(&mut solved_subs, &exposed_vars_by_symbol); - - let solved_module = SolvedModule { - exposed_vars_by_symbol, - problems, - aliases, - stored_vars_by_symbol, - solved_specializations, - storage_subs, - }; - - // Record the final timings - let solve_end = SystemTime::now(); - module_timing.solve = solve_end.duration_since(solve_start).unwrap(); - - // Send the subs to the main thread for processing, - Msg::SolvedTypes { - module_id, - solved_subs, - ident_ids, - decls, - dep_idents, - solved_module, - module_timing, - abilities_store, - } -} - -fn unspace<'a, T: Copy>(arena: &'a Bump, items: &[Loc>]) -> &'a [Loc] { - bumpalo::collections::Vec::from_iter_in( - items - .iter() - .map(|item| Loc::at(item.region, item.value.extract_spaces().item)), - arena, - ) - .into_bump_slice() -} - -#[allow(clippy::too_many_arguments)] -fn fabricate_pkg_config_module<'a>( - arena: &'a Bump, - shorthand: &'a str, - opt_app_module_id: Option, - filename: PathBuf, - parse_state: roc_parse::state::State<'a>, - module_ids: Arc>>, - ident_ids_by_module: SharedIdentIdsByModule, - header: &PlatformHeader<'a>, - module_timing: ModuleTiming, -) -> (ModuleId, Msg<'a>) { - // If we have an app module, then it's the root module; - // otherwise, we must be the root. - let is_root_module = opt_app_module_id.is_none(); - - let info = PlatformHeaderInfo { - filename, - is_root_module, - shorthand, - opt_app_module_id, - packages: &[], - provides: unspace(arena, header.provides.items), - requires: &*arena.alloc([Loc::at( - header.requires.signature.region, - header.requires.signature.extract_spaces().item, - )]), - requires_types: unspace(arena, header.requires.rigids.items), - imports: unspace(arena, header.imports.items), - }; - - send_header_two( - info, - parse_state, - module_ids, - ident_ids_by_module, - module_timing, - ) -} - -#[allow(clippy::too_many_arguments)] -#[allow(clippy::unnecessary_wraps)] -fn canonicalize_and_constrain<'a>( - arena: &'a Bump, - module_ids: &ModuleIds, - dep_idents: IdentIdsByModule, - exposed_symbols: VecSet, - aliases: MutMap, - imported_abilities_state: AbilitiesStore, - parsed: ParsedModule<'a>, - skip_constraint_gen: bool, -) -> CanAndCon { - let canonicalize_start = SystemTime::now(); - - let ParsedModule { - module_id, - module_name, - header_for, - exposed_ident_ids, - parsed_defs, - exposed_imports, - imported_modules, - mut module_timing, - symbols_from_requires, - .. - } = parsed; - - // _before has an underscore because it's unused in --release builds - let _before = roc_types::types::get_type_clone_count(); - - let parsed_defs_for_docs = parsed_defs.clone(); - let parsed_defs = arena.alloc(parsed_defs); - - let mut var_store = VarStore::default(); - let module_output = canonicalize_module_defs( - arena, - parsed_defs, - &header_for, - module_id, - module_ids, - exposed_ident_ids, - &dep_idents, - aliases, - imported_abilities_state, - exposed_imports, - &exposed_symbols, - &symbols_from_requires, - &mut var_store, - ); - - // _after has an underscore because it's unused in --release builds - let _after = roc_types::types::get_type_clone_count(); - - log!( - "canonicalize of {:?} cloned Type {} times ({} -> {})", - module_id, - _after - _before, - _before, - _after - ); - - let canonicalize_end = SystemTime::now(); - - module_timing.canonicalize = canonicalize_end.duration_since(canonicalize_start).unwrap(); - - // Generate documentation information - // TODO: store timing information? - let module_docs = match module_name { - ModuleNameEnum::PkgConfig => None, - ModuleNameEnum::App(_) => None, - ModuleNameEnum::Interface(name) | ModuleNameEnum::Hosted(name) => { - let docs = crate::docs::generate_module_docs( - module_output.scope.clone(), - name.as_str().into(), - &parsed_defs_for_docs, - ); - - Some(docs) - } - }; - - // _before has an underscore because it's unused in --release builds - let _before = roc_types::types::get_type_clone_count(); - - let mut constraints = Constraints::new(); - - let constraint = if skip_constraint_gen { - roc_can::constraint::Constraint::True - } else { - constrain_module( - &mut constraints, - module_output.symbols_from_requires, - &module_output.scope.abilities_store, - &module_output.declarations, - module_id, - ) - }; - - // _after has an underscore because it's unused in --release builds - let _after = roc_types::types::get_type_clone_count(); - - log!( - "constraint gen of {:?} cloned Type {} times ({} -> {})", - module_id, - _after - _before, - _before, - _after - ); - - // scope has imported aliases, but misses aliases from inner scopes - // module_output.aliases does have those aliases, so we combine them - let mut aliases: MutMap = module_output - .aliases - .into_iter() - .map(|(k, v)| (k, (true, v))) - .collect(); - for (name, alias) in module_output.scope.aliases { - match aliases.entry(name) { - Occupied(_) => { - // do nothing - } - Vacant(vacant) => { - if !name.is_builtin() || name.module_id() == ModuleId::ENCODE { - vacant.insert((false, alias)); - } - } - } - } - - let module = Module { - module_id, - exposed_imports: module_output.exposed_imports, - exposed_symbols, - referenced_values: module_output.referenced_values, - referenced_types: module_output.referenced_types, - aliases, - rigid_variables: module_output.rigid_variables, - abilities_store: module_output.scope.abilities_store, - }; - - let constrained_module = ConstrainedModule { - module, - declarations: module_output.declarations, - imported_modules, - var_store, - constraints, - constraint, - ident_ids: module_output.scope.locals.ident_ids, - dep_idents, - module_timing, - pending_derives: module_output.pending_derives, - }; - - CanAndCon { - constrained_module, - canonicalization_problems: module_output.problems, - module_docs, - } -} - -fn parse<'a>(arena: &'a Bump, header: ModuleHeader<'a>) -> Result, LoadingProblem<'a>> { - let mut module_timing = header.module_timing; - let parse_start = SystemTime::now(); - let source = header.parse_state.original_bytes(); - let parse_state = header.parse_state; - let parsed_defs = match module_defs().parse(arena, parse_state) { - Ok((_, success, _state)) => success, - Err((_, fail, state)) => { - return Err(LoadingProblem::ParsingFailed( - fail.into_file_error(header.module_path, &state), - )); - } - }; - - // Record the parse end time once, to avoid checking the time a second time - // immediately afterward (for the beginning of canonicalization). - let parse_end = SystemTime::now(); - - module_timing.parse_body = parse_end.duration_since(parse_start).unwrap(); - - let imported_modules = header.imported_modules; - - // SAFETY: By this point we've already incrementally verified that there - // are no UTF-8 errors in these bytes. If there had been any UTF-8 errors, - // we'd have bailed out before now. - let src = unsafe { from_utf8_unchecked(source) }; - - let ModuleHeader { - module_id, - module_name, - deps_by_name, - exposed_ident_ids, - exposed_imports, - module_path, - header_for, - symbols_from_requires, - .. - } = header; - - let parsed = ParsedModule { - module_id, - module_name, - module_path, - src, - module_timing, - deps_by_name, - imported_modules, - exposed_ident_ids, - exposed_imports, - parsed_defs, - symbols_from_requires, - header_for, - }; - - Ok(Msg::Parsed(parsed)) -} - -fn exposed_from_import<'a>(entry: &ImportsEntry<'a>) -> (QualifiedModuleName<'a>, Vec) { - use roc_parse::header::ImportsEntry::*; - - match entry { - Module(module_name, exposes) => { - let mut exposed = Vec::with_capacity(exposes.len()); - - for loc_entry in exposes.iter() { - exposed.push(ident_from_exposed(&loc_entry.value)); - } - - let qualified_module_name = QualifiedModuleName { - opt_package: None, - module: module_name.as_str().into(), - }; - - (qualified_module_name, exposed) - } - - Package(package_name, module_name, exposes) => { - let mut exposed = Vec::with_capacity(exposes.len()); - - for loc_entry in exposes.iter() { - exposed.push(ident_from_exposed(&loc_entry.value)); - } - - let qualified_module_name = QualifiedModuleName { - opt_package: Some(package_name), - module: module_name.as_str().into(), - }; - - (qualified_module_name, exposed) - } - } -} - -fn ident_from_exposed(entry: &Spaced<'_, ExposedName<'_>>) -> Ident { - entry.extract_spaces().item.as_str().into() -} - -#[allow(clippy::too_many_arguments)] -fn make_specializations<'a>( - arena: &'a Bump, - home: ModuleId, - mut ident_ids: IdentIds, - mut subs: Subs, - procs_base: ProcsBase<'a>, - mut layout_cache: LayoutCache<'a>, - specializations_we_must_make: Vec, - mut module_timing: ModuleTiming, - target_info: TargetInfo, - mut abilities_store: AbilitiesStore, -) -> Msg<'a> { - let make_specializations_start = SystemTime::now(); - let mut update_mode_ids = UpdateModeIds::new(); - // do the thing - let mut mono_env = roc_mono::ir::Env { - arena, - subs: &mut subs, - home, - ident_ids: &mut ident_ids, - target_info, - update_mode_ids: &mut update_mode_ids, - // call_specialization_counter=0 is reserved - call_specialization_counter: 1, - abilities_store: &mut abilities_store, - }; - - let mut procs = Procs::new_in(arena); - - for (symbol, partial_proc) in procs_base.partial_procs.into_iter() { - procs.partial_procs.insert(symbol, partial_proc); - } - - procs.module_thunks = procs_base.module_thunks; - procs.runtime_errors = procs_base.runtime_errors; - procs.imported_module_thunks = procs_base.imported_module_thunks; - - // TODO: for now this final specialization pass is sequential, - // with no parallelization at all. We should try to parallelize - // this, but doing so will require a redesign of Procs. - procs = roc_mono::ir::specialize_all( - &mut mono_env, - procs, - specializations_we_must_make, - procs_base.host_specializations, - &mut layout_cache, - ); - - let external_specializations_requested = procs.externals_we_need.clone(); - let procedures = procs.get_specialized_procs_without_rc(&mut mono_env); - - // Turn `Bytes.Decode.IdentId(238)` into `Bytes.Decode.238`, we rely on this in mono tests - mono_env.home.register_debug_idents(mono_env.ident_ids); - - let make_specializations_end = SystemTime::now(); - module_timing.make_specializations = make_specializations_end - .duration_since(make_specializations_start) - .unwrap(); - - Msg::MadeSpecializations { - module_id: home, - ident_ids, - layout_cache, - procedures, - update_mode_ids, - subs, - external_specializations_requested, - module_timing, - } -} - -#[derive(Clone, Debug)] -struct ProcsBase<'a> { - partial_procs: BumpMap>, - module_thunks: &'a [Symbol], - /// A host-exposed function must be specialized; it's a seed for subsequent specializations - host_specializations: roc_mono::ir::HostSpecializations, - runtime_errors: BumpMap, - imported_module_thunks: &'a [Symbol], -} - -#[allow(clippy::too_many_arguments)] -fn build_pending_specializations<'a>( - arena: &'a Bump, - solved_subs: Solved, - imported_module_thunks: &'a [Symbol], - home: ModuleId, - mut ident_ids: IdentIds, - decls: Vec, - mut module_timing: ModuleTiming, - mut layout_cache: LayoutCache<'a>, - target_info: TargetInfo, - // TODO remove - exposed_to_host: ExposedToHost, - mut abilities_store: AbilitiesStore, -) -> Msg<'a> { - let find_specializations_start = SystemTime::now(); - - let mut module_thunks = bumpalo::collections::Vec::new_in(arena); - - let mut procs_base = ProcsBase { - partial_procs: BumpMap::default(), - module_thunks: &[], - host_specializations: roc_mono::ir::HostSpecializations::new(), - runtime_errors: BumpMap::default(), - imported_module_thunks, - }; - - let mut update_mode_ids = UpdateModeIds::new(); - let mut subs = solved_subs.into_inner(); - let mut mono_env = roc_mono::ir::Env { - arena, - subs: &mut subs, - home, - ident_ids: &mut ident_ids, - target_info, - update_mode_ids: &mut update_mode_ids, - // call_specialization_counter=0 is reserved - call_specialization_counter: 1, - abilities_store: &mut abilities_store, - }; - - // Add modules' decls to Procs - for decl in decls { - use roc_can::def::Declaration::*; - - match decl { - Declare(def) | Builtin(def) => add_def_to_module( - &mut layout_cache, - &mut procs_base, - &mut module_thunks, - &mut mono_env, - def, - &exposed_to_host.values, - false, - ), - DeclareRec(defs, cycle_mark) if !cycle_mark.is_illegal(mono_env.subs) => { - for def in defs { - add_def_to_module( - &mut layout_cache, - &mut procs_base, - &mut module_thunks, - &mut mono_env, - def, - &exposed_to_host.values, - true, - ) - } - } - InvalidCycle(_) | DeclareRec(..) => { - // do nothing? - // this may mean the loc_symbols are not defined during codegen; is that a problem? - } - } - } - - procs_base.module_thunks = module_thunks.into_bump_slice(); - - let find_specializations_end = SystemTime::now(); - module_timing.find_specializations = find_specializations_end - .duration_since(find_specializations_start) - .unwrap(); - - Msg::FoundSpecializations { - module_id: home, - solved_subs: roc_types::solved_types::Solved(subs), - ident_ids, - layout_cache, - procs_base, - module_timing, - abilities_store, - } -} - -fn add_def_to_module<'a>( - layout_cache: &mut LayoutCache<'a>, - procs: &mut ProcsBase<'a>, - module_thunks: &mut bumpalo::collections::Vec<'a, Symbol>, - mono_env: &mut roc_mono::ir::Env<'a, '_>, - def: roc_can::def::Def, - exposed_to_host: &MutMap, - is_recursive: bool, -) { - use roc_can::expr::ClosureData; - use roc_can::expr::Expr::*; - use roc_can::pattern::Pattern::*; - - match def.loc_pattern.value { - Identifier(symbol) - | AbilityMemberSpecialization { - ident: symbol, - specializes: _, - } => { - let is_host_exposed = exposed_to_host.contains_key(&symbol); - - match def.loc_expr.value { - Closure(ClosureData { - function_type: annotation, - return_type: ret_var, - arguments: loc_args, - loc_body, - captured_symbols, - name, - .. - }) => { - // this is a top-level definition, it should not capture anything - debug_assert!( - captured_symbols.is_empty(), - "{:?}", - (symbol, name, symbol.module_id(), &captured_symbols) - ); - - // If this is an exposed symbol, we need to - // register it as such. Otherwise, since it - // never gets called by Roc code, it will never - // get specialized! - if is_host_exposed { - let layout_result = - layout_cache.raw_from_var(mono_env.arena, annotation, mono_env.subs); - - // cannot specialize when e.g. main's type contains type variables - if let Err(e) = layout_result { - match e { - LayoutProblem::Erroneous => { - let message = "top level function has erroneous type"; - procs.runtime_errors.insert(symbol, message); - return; - } - LayoutProblem::UnresolvedTypeVar(v) => { - let message = format!( - "top level function has unresolved type variable {:?}", - v - ); - procs - .runtime_errors - .insert(symbol, mono_env.arena.alloc(message)); - return; - } - } - } - - procs.host_specializations.insert_host_exposed( - mono_env.subs, - symbol, - def.annotation, - annotation, - ); - } - - let partial_proc = PartialProc::from_named_function( - mono_env, - annotation, - loc_args, - *loc_body, - CapturedSymbols::None, - is_recursive, - ret_var, - ); - - procs.partial_procs.insert(symbol, partial_proc); - } - body => { - // mark this symbols as a top-level thunk before any other work on the procs - module_thunks.push(symbol); - - let annotation = def.expr_var; - - // If this is an exposed symbol, we need to - // register it as such. Otherwise, since it - // never gets called by Roc code, it will never - // get specialized! - if is_host_exposed { - let layout_result = - layout_cache.raw_from_var(mono_env.arena, annotation, mono_env.subs); - - // cannot specialize when e.g. main's type contains type variables - if let Err(e) = layout_result { - match e { - LayoutProblem::Erroneous => { - let message = "top level function has erroneous type"; - procs.runtime_errors.insert(symbol, message); - return; - } - LayoutProblem::UnresolvedTypeVar(v) => { - let message = format!( - "top level function has unresolved type variable {:?}", - v - ); - procs - .runtime_errors - .insert(symbol, mono_env.arena.alloc(message)); - return; - } - } - } - - procs.host_specializations.insert_host_exposed( - mono_env.subs, - symbol, - def.annotation, - annotation, - ); - } - - let proc = PartialProc { - annotation, - // This is a 0-arity thunk, so it has no arguments. - pattern_symbols: &[], - // This is a top-level definition, so it cannot capture anything - captured_symbols: CapturedSymbols::None, - body, - body_var: def.expr_var, - // This is a 0-arity thunk, so it cannot be recursive - is_self_recursive: false, - }; - - procs.partial_procs.insert(symbol, proc); - } - }; - } - other => { - todo!("TODO gracefully handle Declare({:?})", other); - } - } -} - -fn run_task<'a>( - task: BuildTask<'a>, - arena: &'a Bump, - src_dir: &Path, - msg_tx: MsgSender<'a>, - target_info: TargetInfo, -) -> Result<(), LoadingProblem<'a>> { - use BuildTask::*; - - let msg = match task { - LoadModule { - module_name, - module_ids, - shorthands, - ident_ids_by_module, - } => load_module( - arena, - src_dir, - module_name, - module_ids, - shorthands, - ident_ids_by_module, - ) - .map(|(_, msg)| msg), - Parse { header } => parse(arena, header), - CanonicalizeAndConstrain { - parsed, - module_ids, - dep_idents, - exposed_symbols, - aliases, - abilities_store, - skip_constraint_gen, - } => { - let can_and_con = canonicalize_and_constrain( - arena, - &module_ids, - dep_idents, - exposed_symbols, - aliases, - abilities_store, - parsed, - skip_constraint_gen, - ); - - Ok(Msg::CanonicalizedAndConstrained(can_and_con)) - } - Solve { - module, - module_timing, - imported_builtins, - exposed_for_module, - constraints, - constraint, - pending_derives, - var_store, - ident_ids, - declarations, - dep_idents, - cached_subs, - } => Ok(run_solve( - module, - ident_ids, - module_timing, - imported_builtins, - exposed_for_module, - constraints, - constraint, - pending_derives, - var_store, - declarations, - dep_idents, - cached_subs, - )), - BuildPendingSpecializations { - module_id, - ident_ids, - decls, - module_timing, - layout_cache, - solved_subs, - imported_module_thunks, - exposed_to_host, - abilities_store, - } => Ok(build_pending_specializations( - arena, - solved_subs, - imported_module_thunks, - module_id, - ident_ids, - decls, - module_timing, - layout_cache, - target_info, - exposed_to_host, - abilities_store, - )), - MakeSpecializations { - module_id, - ident_ids, - subs, - procs_base, - layout_cache, - specializations_we_must_make, - module_timing, - abilities_store, - } => Ok(make_specializations( - arena, - module_id, - ident_ids, - subs, - procs_base, - layout_cache, - specializations_we_must_make, - module_timing, - target_info, - abilities_store, - )), - }?; - - msg_tx - .send(msg) - .map_err(|_| LoadingProblem::MsgChannelDied)?; - - Ok(()) -} - -fn to_file_problem_report(filename: &Path, error: io::ErrorKind) -> String { - use roc_reporting::report::{Report, RocDocAllocator, Severity, DEFAULT_PALETTE}; - use ven_pretty::DocAllocator; - - let src_lines: Vec<&str> = Vec::new(); - - let mut module_ids = ModuleIds::default(); - - let module_id = module_ids.get_or_insert(&"find module name somehow?".into()); - - let interns = Interns::default(); - - // Report parsing and canonicalization problems - let alloc = RocDocAllocator::new(&src_lines, module_id, &interns); - - let report = match error { - io::ErrorKind::NotFound => { - let doc = alloc.stack([ - alloc.reflow(r"I am looking for this file, but it's not there:"), - alloc - .parser_suggestion(filename.to_str().unwrap()) - .indent(4), - alloc.concat([ - alloc.reflow(r"Is the file supposed to be there? "), - alloc.reflow("Maybe there is a typo in the file name?"), - ]), - ]); - - Report { - filename: "UNKNOWN.roc".into(), - doc, - title: "FILE NOT FOUND".to_string(), - severity: Severity::RuntimeError, - } - } - io::ErrorKind::PermissionDenied => { - let doc = alloc.stack([ - alloc.reflow(r"I don't have the required permissions to read this file:"), - alloc - .parser_suggestion(filename.to_str().unwrap()) - .indent(4), - alloc - .concat([alloc.reflow(r"Is it the right file? Maybe change its permissions?")]), - ]); - - Report { - filename: "UNKNOWN.roc".into(), - doc, - title: "FILE PERMISSION DENIED".to_string(), - severity: Severity::RuntimeError, - } - } - _ => { - let error = std::io::Error::from(error); - let formatted = format!("{}", error); - let doc = alloc.concat([ - alloc.reflow(r"I tried to read this file, but ran into a "), - alloc.text(formatted), - alloc.reflow(r" problem."), - ]); - - Report { - filename: "UNKNOWN.roc".into(), - doc, - title: "FILE PROBLEM".to_string(), - severity: Severity::RuntimeError, - } - } - }; - - let mut buf = String::new(); - let palette = DEFAULT_PALETTE; - report.render_color_terminal(&mut buf, &alloc, &palette); - - buf -} - -fn to_parse_problem_report<'a>( - problem: FileError<'a, SyntaxError<'a>>, - mut module_ids: ModuleIds, - all_ident_ids: IdentIdsByModule, - render: RenderTarget, -) -> String { - use roc_reporting::report::{parse_problem, RocDocAllocator, DEFAULT_PALETTE}; - - // TODO this is not in fact safe - let src = unsafe { from_utf8_unchecked(problem.problem.bytes) }; - let src_lines = src.lines().collect::>(); - // let mut src_lines: Vec<&str> = problem.prefix.lines().collect(); - // src_lines.extend(src.lines().skip(1)); - - let module_id = module_ids.get_or_insert(&"find module name somehow?".into()); - - let interns = Interns { - module_ids, - all_ident_ids, - }; - - // Report parsing and canonicalization problems - let alloc = RocDocAllocator::new(&src_lines, module_id, &interns); - - let starting_line = 0; - - let lines = LineInfo::new(src); - - let report = parse_problem( - &alloc, - &lines, - problem.filename.clone(), - starting_line, - problem, - ); - - let mut buf = String::new(); - let palette = DEFAULT_PALETTE; - - report.render(render, &mut buf, &alloc, &palette); - - buf -} - -fn to_missing_platform_report(module_id: ModuleId, other: PlatformPath) -> String { - use roc_reporting::report::{Report, RocDocAllocator, Severity, DEFAULT_PALETTE}; - use ven_pretty::DocAllocator; - use PlatformPath::*; - - // Report parsing and canonicalization problems - let interns = Interns::default(); - let alloc = RocDocAllocator::new(&[], module_id, &interns); - - let report = { - match other { - Valid(_) => unreachable!(), - NotSpecified => { - let doc = alloc.stack([ - alloc.reflow("I could not find a platform based on your input file."), - alloc.reflow(r"Does the module header contain an entry that looks like this:"), - alloc - .parser_suggestion(" packages { pf: \"platform\" }") - .indent(4), - alloc.reflow("See also TODO."), - ]); - - Report { - filename: "UNKNOWN.roc".into(), - doc, - title: "NO PLATFORM".to_string(), - severity: Severity::RuntimeError, - } - } - RootIsInterface => { - let doc = alloc.stack([ - alloc.reflow(r"The input file is an interface module, but only app modules can be ran."), - alloc.concat([ - alloc.reflow(r"I will still parse and typecheck the input file and its dependencies, "), - alloc.reflow(r"but won't output any executable."), - ]) - ]); - - Report { - filename: "UNKNOWN.roc".into(), - doc, - title: "NO PLATFORM".to_string(), - severity: Severity::RuntimeError, - } - } - RootIsHosted => { - let doc = alloc.stack([ - alloc.reflow(r"The input file is a hosted module, but only app modules can be ran."), - alloc.concat([ - alloc.reflow(r"I will still parse and typecheck the input file and its dependencies, "), - alloc.reflow(r"but won't output any executable."), - ]) - ]); - - Report { - filename: "UNKNOWN.roc".into(), - doc, - title: "NO PLATFORM".to_string(), - severity: Severity::RuntimeError, - } - } - RootIsPkgConfig => { - let doc = alloc.stack([ - alloc.reflow(r"The input file is a package config file, but only app modules can be ran."), - alloc.concat([ - alloc.reflow(r"I will still parse and typecheck the input file and its dependencies, "), - alloc.reflow(r"but won't output any executable."), - ]) - ]); - - Report { - filename: "UNKNOWN.roc".into(), - doc, - title: "NO PLATFORM".to_string(), - severity: Severity::RuntimeError, - } - } - } - }; - - let palette = DEFAULT_PALETTE; - let mut buf = String::new(); - report.render_color_terminal(&mut buf, &alloc, &palette); - - buf -} - -/// Builtin aliases that are not covered by type checker optimizations -/// -/// Types like `F64` and `I32` are hardcoded into Subs and therefore we don't define them here. -/// Generic number types (Num, Int, Float, etc.) are treated as `DelayedAlias`es resolved during -/// type solving. -/// All that remains are Signed8, Signed16, etc. -fn default_aliases() -> roc_solve::solve::Aliases { - use roc_types::types::Type; - - let mut solve_aliases = roc_solve::solve::Aliases::default(); - - let mut zero_opaque = |alias_name: Symbol| { - let alias = Alias { - region: Region::zero(), - type_variables: vec![], - lambda_set_variables: Default::default(), - recursion_variables: Default::default(), - typ: Type::EmptyTagUnion, - kind: AliasKind::Opaque, - }; - - solve_aliases.insert(alias_name, alias); - }; - - zero_opaque(Symbol::NUM_SIGNED8); - zero_opaque(Symbol::NUM_SIGNED16); - zero_opaque(Symbol::NUM_SIGNED32); - zero_opaque(Symbol::NUM_SIGNED64); - zero_opaque(Symbol::NUM_SIGNED128); - - zero_opaque(Symbol::NUM_UNSIGNED8); - zero_opaque(Symbol::NUM_UNSIGNED16); - zero_opaque(Symbol::NUM_UNSIGNED32); - zero_opaque(Symbol::NUM_UNSIGNED64); - zero_opaque(Symbol::NUM_UNSIGNED128); - - zero_opaque(Symbol::NUM_BINARY32); - zero_opaque(Symbol::NUM_BINARY64); - - solve_aliases -} diff --git a/compiler/load_internal/src/lib.rs b/compiler/load_internal/src/lib.rs deleted file mode 100644 index 474a9bc679..0000000000 --- a/compiler/load_internal/src/lib.rs +++ /dev/null @@ -1,9 +0,0 @@ -#![warn(clippy::dbg_macro)] -// See github.com/rtfeldman/roc/issues/800 for discussion of the large_enum_variant check. -#![allow(clippy::large_enum_variant)] -pub mod docs; -pub mod file; -mod work; - -#[cfg(target_family = "wasm")] -mod wasm_system_time; diff --git a/compiler/load_internal/src/wasm_system_time.rs b/compiler/load_internal/src/wasm_system_time.rs deleted file mode 100644 index 4fa9728871..0000000000 --- a/compiler/load_internal/src/wasm_system_time.rs +++ /dev/null @@ -1,42 +0,0 @@ -#![cfg(target_family = "wasm")] -/* -For the Web REPL (repl_www), we build the compiler as a Wasm module. -SystemTime is the only thing in the compiler that would need a special implementation for this. -There is a WASI implementation for it, but we are targeting the browser, not WASI! -It's possible to write browser versions of WASI's low-level ABI but we'd rather avoid it. -Instead we use these dummy implementations, which should just disappear at compile time. -*/ - -#[derive(Debug, Clone, Copy)] -pub struct SystemTime; - -impl SystemTime { - pub fn now() -> Self { - SystemTime - } - pub fn duration_since(&self, _: SystemTime) -> Result { - Ok(Duration) - } - pub fn elapsed(&self) -> Result { - Ok(Duration) - } -} - -#[derive(Debug, Clone, Copy)] -pub struct Duration; - -impl Duration { - pub fn checked_sub(&self, _: Duration) -> Option { - Some(Duration) - } -} - -impl Default for Duration { - fn default() -> Self { - Duration - } -} - -impl std::ops::AddAssign for Duration { - fn add_assign(&mut self, _: Duration) {} -} diff --git a/compiler/load_internal/src/work.rs b/compiler/load_internal/src/work.rs deleted file mode 100644 index f26209163d..0000000000 --- a/compiler/load_internal/src/work.rs +++ /dev/null @@ -1,290 +0,0 @@ -use roc_collections::all::{MutMap, MutSet}; -use roc_module::symbol::{ModuleId, PackageQualified}; - -use std::collections::hash_map::Entry; - -/// NOTE the order of definition of the phases is used by the ord instance -/// make sure they are ordered from first to last! -#[derive(PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Debug)] -pub enum Phase { - LoadHeader, - Parse, - CanonicalizeAndConstrain, - SolveTypes, - FindSpecializations, - MakeSpecializations, -} - -/// NOTE keep up to date manually, from ParseAndGenerateConstraints to the highest phase we support -const PHASES: [Phase; 6] = [ - Phase::LoadHeader, - Phase::Parse, - Phase::CanonicalizeAndConstrain, - Phase::SolveTypes, - Phase::FindSpecializations, - Phase::MakeSpecializations, -]; - -#[derive(Debug)] -enum Status { - NotStarted, - Pending, - Done, -} - -#[derive(Clone, Debug, PartialEq, Eq, Hash)] -enum Job<'a> { - Step(ModuleId, Phase), - ResolveShorthand(&'a str), -} - -#[derive(Default, Debug)] -pub struct Dependencies<'a> { - waiting_for: MutMap, MutSet>>, - notifies: MutMap, MutSet>>, - status: MutMap, Status>, -} - -impl<'a> Dependencies<'a> { - /// Add all the dependencies for a module, return (module, phase) pairs that can make progress - pub fn add_module( - &mut self, - module_id: ModuleId, - dependencies: &MutSet>, - goal_phase: Phase, - ) -> MutSet<(ModuleId, Phase)> { - use Phase::*; - - let mut output = MutSet::default(); - - for dep in dependencies.iter() { - let has_package_dependency = self.add_package_dependency(dep, Phase::LoadHeader); - - let dep = *dep.as_inner(); - - if !has_package_dependency { - // loading can start immediately on this dependency - output.insert((dep, Phase::LoadHeader)); - } - - // to parse and generate constraints, the headers of all dependencies must be loaded! - // otherwise, we don't know whether an imported symbol is actually exposed - self.add_dependency_help(module_id, dep, Phase::Parse, Phase::LoadHeader); - - // to canonicalize a module, all its dependencies must be canonicalized - self.add_dependency(module_id, dep, Phase::CanonicalizeAndConstrain); - - // to typecheck a module, all its dependencies must be type checked already - self.add_dependency(module_id, dep, Phase::SolveTypes); - - if goal_phase >= FindSpecializations { - self.add_dependency(module_id, dep, Phase::FindSpecializations); - } - - if goal_phase >= MakeSpecializations { - self.add_dependency(dep, module_id, Phase::MakeSpecializations); - } - } - - // add dependencies for self - // phase i + 1 of a file always depends on phase i being completed - { - let mut i = 0; - while PHASES[i] < goal_phase { - self.add_dependency_help(module_id, module_id, PHASES[i + 1], PHASES[i]); - i += 1; - } - } - - self.add_to_status(module_id, goal_phase); - - output - } - - fn add_to_status(&mut self, module_id: ModuleId, goal_phase: Phase) { - for phase in PHASES.iter() { - if *phase > goal_phase { - break; - } - - if let Entry::Vacant(entry) = self.status.entry(Job::Step(module_id, *phase)) { - entry.insert(Status::NotStarted); - } - } - } - - /// Propagate a notification, return (module, phase) pairs that can make progress - pub fn notify(&mut self, module_id: ModuleId, phase: Phase) -> MutSet<(ModuleId, Phase)> { - self.notify_help(Job::Step(module_id, phase)) - } - - /// Propagate a notification, return (module, phase) pairs that can make progress - pub fn notify_package(&mut self, shorthand: &'a str) -> MutSet<(ModuleId, Phase)> { - self.notify_help(Job::ResolveShorthand(shorthand)) - } - - fn notify_help(&mut self, key: Job<'a>) -> MutSet<(ModuleId, Phase)> { - self.status.insert(key.clone(), Status::Done); - - let mut output = MutSet::default(); - - if let Some(to_notify) = self.notifies.get(&key) { - for notify_key in to_notify { - let mut is_empty = false; - if let Some(waiting_for_pairs) = self.waiting_for.get_mut(notify_key) { - waiting_for_pairs.remove(&key); - is_empty = waiting_for_pairs.is_empty(); - } - - if is_empty { - self.waiting_for.remove(notify_key); - - if let Job::Step(module, phase) = *notify_key { - output.insert((module, phase)); - } - } - } - } - - self.notifies.remove(&key); - - output - } - - fn add_package_dependency( - &mut self, - module: &PackageQualified<'a, ModuleId>, - next_phase: Phase, - ) -> bool { - match module { - PackageQualified::Unqualified(_) => { - // no dependency, we can just start loading the file - false - } - PackageQualified::Qualified(shorthand, module_id) => { - let job = Job::ResolveShorthand(shorthand); - let next_step = Job::Step(*module_id, next_phase); - match self.status.get(&job) { - None | Some(Status::NotStarted) | Some(Status::Pending) => { - // this shorthand is not resolved, add a dependency - { - let entry = self - .waiting_for - .entry(next_step.clone()) - .or_insert_with(Default::default); - - entry.insert(job.clone()); - } - - { - let entry = self.notifies.entry(job).or_insert_with(Default::default); - - entry.insert(next_step); - } - - true - } - Some(Status::Done) => { - // shorthand is resolved; no dependency - false - } - } - } - } - } - - /// A waits for B, and B will notify A when it completes the phase - fn add_dependency(&mut self, a: ModuleId, b: ModuleId, phase: Phase) { - self.add_dependency_help(a, b, phase, phase); - } - - /// phase_a of module a is waiting for phase_b of module_b - fn add_dependency_help(&mut self, a: ModuleId, b: ModuleId, phase_a: Phase, phase_b: Phase) { - // no need to wait if the dependency is already done! - if let Some(Status::Done) = self.status.get(&Job::Step(b, phase_b)) { - return; - } - - let key = Job::Step(a, phase_a); - let value = Job::Step(b, phase_b); - match self.waiting_for.get_mut(&key) { - Some(existing) => { - existing.insert(value); - } - None => { - let mut set = MutSet::default(); - set.insert(value); - self.waiting_for.insert(key, set); - } - } - - let key = Job::Step(b, phase_b); - let value = Job::Step(a, phase_a); - match self.notifies.get_mut(&key) { - Some(existing) => { - existing.insert(value); - } - None => { - let mut set = MutSet::default(); - set.insert(value); - self.notifies.insert(key, set); - } - } - } - - pub fn solved_all(&self) -> bool { - debug_assert_eq!(self.notifies.is_empty(), self.waiting_for.is_empty()); - - for status in self.status.values() { - match status { - Status::Done => { - continue; - } - _ => { - return false; - } - } - } - - true - } - - pub fn prepare_start_phase(&mut self, module_id: ModuleId, phase: Phase) -> PrepareStartPhase { - match self.status.get_mut(&Job::Step(module_id, phase)) { - Some(current @ Status::NotStarted) => { - // start this phase! - *current = Status::Pending; - PrepareStartPhase::Continue - } - Some(Status::Pending) => { - // don't start this task again! - PrepareStartPhase::Done - } - Some(Status::Done) => { - // don't start this task again, but tell those waiting for it they can continue - let new = self.notify(module_id, phase); - - PrepareStartPhase::Recurse(new) - } - None => match phase { - Phase::LoadHeader => { - // this is fine, mark header loading as pending - self.status - .insert(Job::Step(module_id, Phase::LoadHeader), Status::Pending); - - PrepareStartPhase::Continue - } - _ => unreachable!( - "Pair {:?} is not in dependencies.status, that should never happen!", - (module_id, phase) - ), - }, - } - } -} - -pub enum PrepareStartPhase { - Continue, - Done, - Recurse(MutSet<(ModuleId, Phase)>), -} diff --git a/compiler/load_internal/tests/fixtures/build/app_with_deps/AStar.roc b/compiler/load_internal/tests/fixtures/build/app_with_deps/AStar.roc deleted file mode 100644 index 6b25695226..0000000000 --- a/compiler/load_internal/tests/fixtures/build/app_with_deps/AStar.roc +++ /dev/null @@ -1,111 +0,0 @@ -interface AStar - exposes [initialModel, reconstructPath, updateCost, cheapestOpen, astar, findPath] - imports [] - - -# a port of https://github.com/krisajenkins/elm-astar/blob/2.1.3/src/AStar/Generalised.elm - -Model position : - { evaluated : Set position - , openSet : Set position - , costs : Map.Map position F64 - , cameFrom : Map.Map position position - } - - -initialModel : position -> Model position -initialModel = \start -> - { evaluated : Set.empty - , openSet : Set.single start - , costs : Dict.single start 0.0 - , cameFrom : Map.empty - } - - -cheapestOpen : (position -> F64), Model position -> Result position [KeyNotFound]* -cheapestOpen = \costFunction, model -> - - folder = \resSmallestSoFar, position -> - when Map.get model.costs position is - Err e -> - Err e - - Ok cost -> - positionCost = costFunction position - - when resSmallestSoFar is - Err _ -> Ok { position, cost: cost + positionCost } - Ok smallestSoFar -> - if positionCost + cost < smallestSoFar.cost then - Ok { position, cost: cost + positionCost } - - else - Ok smallestSoFar - - Set.walk model.openSet (Err KeyNotFound) folder - |> Result.map (\x -> x.position) - - - -reconstructPath : Map position position, position -> List position -reconstructPath = \cameFrom, goal -> - when Map.get cameFrom goal is - Err KeyNotFound -> - [] - - Ok next -> - List.append (reconstructPath cameFrom next) goal - -updateCost : position, position, Model position -> Model position -updateCost = \current, neighbour, model -> - newCameFrom = Map.insert model.cameFrom neighbour current - - newCosts = Map.insert model.costs neighbour distanceTo - - distanceTo = reconstructPath newCameFrom neighbour - |> List.len - |> Num.toFrac - - newModel = { model & costs : newCosts , cameFrom : newCameFrom } - - when Map.get model.costs neighbour is - Err KeyNotFound -> - newModel - - Ok previousDistance -> - if distanceTo < previousDistance then - newModel - - else - model - - -findPath : { costFunction: (position, position -> F64), moveFunction: (position -> Set position), start : position, end : position } -> Result (List position) [KeyNotFound]* -findPath = \{ costFunction, moveFunction, start, end } -> - astar costFunction moveFunction end (initialModel start) - - -astar : (position, position -> F64), (position -> Set position), position, Model position -> [Err [KeyNotFound]*, Ok (List position)]* -astar = \costFn, moveFn, goal, model -> - when cheapestOpen (\position -> costFn goal position) model is - Err _ -> - Err KeyNotFound - - Ok current -> - if current == goal then - Ok (reconstructPath model.cameFrom goal) - - else - - modelPopped = { model & openSet : Set.remove model.openSet current, evaluated : Set.insert model.evaluated current } - - neighbours = moveFn current - - newNeighbours = Set.difference neighbours modelPopped.evaluated - - modelWithNeighbours = { modelPopped & openSet : Set.union modelPopped.openSet newNeighbours } - - modelWithCosts = Set.walk newNeighbours modelWithNeighbours (\md, nb -> updateCost current nb md) - - astar costFn moveFn goal modelWithCosts - diff --git a/compiler/load_internal/tests/fixtures/build/app_with_deps/Primary.roc b/compiler/load_internal/tests/fixtures/build/app_with_deps/Primary.roc deleted file mode 100644 index b68b3d39e2..0000000000 --- a/compiler/load_internal/tests/fixtures/build/app_with_deps/Primary.roc +++ /dev/null @@ -1,30 +0,0 @@ -interface Primary - exposes [blah2, blah3, str, alwaysThree, identity, z, w, succeed, withDefault, yay] - imports [Dep1, Dep2.{ two, foo }, Dep3.Blah.{ bar }, Res] - -blah2 = Dep2.two -blah3 = bar - -str = Dep1.str - -alwaysThree = \_ -> Dep1.three - -identity = \a -> a - -z = identity (alwaysThree {}) - -w : Dep1.Identity {} -w = Identity {} - -succeed : a -> Dep1.Identity a -succeed = \x -> Identity x - -withDefault = Res.withDefault - -yay : Res.Res {} err -yay = - ok = Ok "foo" - - f = \_ -> {} - - Res.map ok f diff --git a/compiler/load_internal/tests/fixtures/build/app_with_deps/Quicksort.roc b/compiler/load_internal/tests/fixtures/build/app_with_deps/Quicksort.roc deleted file mode 100644 index dc8b56f4ee..0000000000 --- a/compiler/load_internal/tests/fixtures/build/app_with_deps/Quicksort.roc +++ /dev/null @@ -1,49 +0,0 @@ -app "quicksort" provides [swap, partition, partitionHelp, quicksort] to "./platform" - -quicksort : List (Num a), Nat, Nat -> List (Num a) -quicksort = \list, low, high -> - when partition low high list is - Pair partitionIndex partitioned -> - partitioned - |> quicksort low (partitionIndex - 1) - |> quicksort (partitionIndex + 1) high - - -swap : Nat, Nat, List a -> List a -swap = \i, j, list -> - when Pair (List.get list i) (List.get list j) is - Pair (Ok atI) (Ok atJ) -> - list - |> List.set i atJ - |> List.set j atI - - _ -> - [] - - -partition : Nat, Nat, List (Num a) -> [Pair Nat (List (Num a))] -partition = \low, high, initialList -> - when List.get initialList high is - Ok pivot -> - when partitionHelp (low - 1) low initialList high pivot is - Pair newI newList -> - Pair (newI + 1) (swap (newI + 1) high newList) - - Err _ -> - Pair (low - 1) initialList - - -partitionHelp : Nat, Nat, List (Num a), Nat, (Num a) -> [Pair Nat (List (Num a))] -partitionHelp = \i, j, list, high, pivot -> - if j < high then - when List.get list j is - Ok value -> - if value <= pivot then - partitionHelp (i + 1) (j + 1) (swap (i + 1) j list) high pivot - else - partitionHelp i (j + 1) list high pivot - - Err _ -> - Pair i list - else - Pair i list diff --git a/compiler/load_internal/tests/fixtures/build/app_with_deps/QuicksortOneDef.roc b/compiler/load_internal/tests/fixtures/build/app_with_deps/QuicksortOneDef.roc deleted file mode 100644 index 9a44e3653b..0000000000 --- a/compiler/load_internal/tests/fixtures/build/app_with_deps/QuicksortOneDef.roc +++ /dev/null @@ -1,57 +0,0 @@ -app "quicksort" provides [quicksort] to "./platform" - -quicksort = \originalList -> - quicksortHelp : List (Num a), Nat, Nat -> List (Num a) - quicksortHelp = \list, low, high -> - if low < high then - when partition low high list is - Pair partitionIndex partitioned -> - partitioned - |> quicksortHelp low (partitionIndex - 1) - |> quicksortHelp (partitionIndex + 1) high - else - list - - - swap : Nat, Nat, List a -> List a - swap = \i, j, list -> - when Pair (List.get list i) (List.get list j) is - Pair (Ok atI) (Ok atJ) -> - list - |> List.set i atJ - |> List.set j atI - - _ -> - [] - - partition : Nat, Nat, List (Num a) -> [Pair Nat (List (Num a))] - partition = \low, high, initialList -> - when List.get initialList high is - Ok pivot -> - when partitionHelp (low - 1) low initialList high pivot is - Pair newI newList -> - Pair (newI + 1) (swap (newI + 1) high newList) - - Err _ -> - Pair (low - 1) initialList - - - partitionHelp : Nat, Nat, List (Num a), Nat, (Num a) -> [Pair Nat (List (Num a))] - partitionHelp = \i, j, list, high, pivot -> - if j < high then - when List.get list j is - Ok value -> - if value <= pivot then - partitionHelp (i + 1) (j + 1) (swap (i + 1) j list) high pivot - else - partitionHelp i (j + 1) list high pivot - - Err _ -> - Pair i list - else - Pair i list - - - - n = List.len originalList - quicksortHelp originalList 0 (n - 1) diff --git a/compiler/load_internal/tests/fixtures/build/interface_with_deps/AStar.roc b/compiler/load_internal/tests/fixtures/build/interface_with_deps/AStar.roc deleted file mode 100644 index dd81f5f633..0000000000 --- a/compiler/load_internal/tests/fixtures/build/interface_with_deps/AStar.roc +++ /dev/null @@ -1,111 +0,0 @@ -interface AStar - exposes [initialModel, reconstructPath, updateCost, cheapestOpen, astar, findPath] - imports [] - - -# a port of https://github.com/krisajenkins/elm-astar/blob/2.1.3/src/AStar/Generalised.elm - -Model position : - { evaluated : Set position - , openSet : Set position - , costs : Dict.Dict position F64 - , cameFrom : Dict.Dict position position - } - - -initialModel : position -> Model position -initialModel = \start -> - { evaluated : Set.empty - , openSet : Set.single start - , costs : Dict.single start 0.0 - , cameFrom : Dict.empty - } - - -cheapestOpen : (position -> F64), Model position -> Result position [KeyNotFound]* -cheapestOpen = \costFunction, model -> - - folder = \resSmallestSoFar, position -> - when Dict.get model.costs position is - Err e -> - Err e - - Ok cost -> - positionCost = costFunction position - - when resSmallestSoFar is - Err _ -> Ok { position, cost: cost + positionCost } - Ok smallestSoFar -> - if positionCost + cost < smallestSoFar.cost then - Ok { position, cost: cost + positionCost } - - else - Ok smallestSoFar - - Set.walk model.openSet (Err KeyNotFound) folder - |> Result.map (\x -> x.position) - - - -reconstructPath : Dict position position, position -> List position -reconstructPath = \cameFrom, goal -> - when Dict.get cameFrom goal is - Err KeyNotFound -> - [] - - Ok next -> - List.append (reconstructPath cameFrom next) goal - -updateCost : position, position, Model position -> Model position -updateCost = \current, neighbour, model -> - newCameFrom = Dict.insert model.cameFrom neighbour current - - newCosts = Dict.insert model.costs neighbour distanceTo - - distanceTo = reconstructPath newCameFrom neighbour - |> List.len - |> Num.toFrac - - newModel = { model & costs : newCosts , cameFrom : newCameFrom } - - when Dict.get model.costs neighbour is - Err KeyNotFound -> - newModel - - Ok previousDistance -> - if distanceTo < previousDistance then - newModel - - else - model - - -findPath : { costFunction: (position, position -> F64), moveFunction: (position -> Set position), start : position, end : position } -> Result (List position) [KeyNotFound]* -findPath = \{ costFunction, moveFunction, start, end } -> - astar costFunction moveFunction end (initialModel start) - - -astar : (position, position -> F64), (position -> Set position), position, Model position -> [Err [KeyNotFound]*, Ok (List position)]* -astar = \costFn, moveFn, goal, model -> - when cheapestOpen (\position -> costFn goal position) model is - Err _ -> - Err KeyNotFound - - Ok current -> - if current == goal then - Ok (reconstructPath model.cameFrom goal) - - else - - modelPopped = { model & openSet : Set.remove model.openSet current, evaluated : Set.insert model.evaluated current } - - neighbours = moveFn current - - newNeighbours = Set.difference neighbours modelPopped.evaluated - - modelWithNeighbours = { modelPopped & openSet : Set.union modelPopped.openSet newNeighbours } - - modelWithCosts = Set.walk newNeighbours modelWithNeighbours (\md, nb -> updateCost current nb md) - - astar costFn moveFn goal modelWithCosts - diff --git a/compiler/load_internal/tests/fixtures/build/interface_with_deps/Dep3/Blah.roc b/compiler/load_internal/tests/fixtures/build/interface_with_deps/Dep3/Blah.roc deleted file mode 100644 index da94d8c6a7..0000000000 --- a/compiler/load_internal/tests/fixtures/build/interface_with_deps/Dep3/Blah.roc +++ /dev/null @@ -1,10 +0,0 @@ -interface Dep3.Blah - exposes [one, two, foo, bar] - imports [] - -one = 1 - -two = 2 - -foo = "foo from Dep3" -bar = "bar from Dep3" diff --git a/compiler/load_internal/tests/fixtures/build/interface_with_deps/Primary.roc b/compiler/load_internal/tests/fixtures/build/interface_with_deps/Primary.roc deleted file mode 100644 index b68b3d39e2..0000000000 --- a/compiler/load_internal/tests/fixtures/build/interface_with_deps/Primary.roc +++ /dev/null @@ -1,30 +0,0 @@ -interface Primary - exposes [blah2, blah3, str, alwaysThree, identity, z, w, succeed, withDefault, yay] - imports [Dep1, Dep2.{ two, foo }, Dep3.Blah.{ bar }, Res] - -blah2 = Dep2.two -blah3 = bar - -str = Dep1.str - -alwaysThree = \_ -> Dep1.three - -identity = \a -> a - -z = identity (alwaysThree {}) - -w : Dep1.Identity {} -w = Identity {} - -succeed : a -> Dep1.Identity a -succeed = \x -> Identity x - -withDefault = Res.withDefault - -yay : Res.Res {} err -yay = - ok = Ok "foo" - - f = \_ -> {} - - Res.map ok f diff --git a/compiler/load_internal/tests/test_load.rs b/compiler/load_internal/tests/test_load.rs deleted file mode 100644 index af351b7490..0000000000 --- a/compiler/load_internal/tests/test_load.rs +++ /dev/null @@ -1,880 +0,0 @@ -#[macro_use] -extern crate indoc; -#[macro_use] -extern crate pretty_assertions; -#[macro_use] -extern crate maplit; - -extern crate bumpalo; -extern crate roc_collections; -extern crate roc_load_internal; -extern crate roc_module; - -mod helpers; - -#[cfg(test)] -mod test_load { - use crate::helpers::fixtures_dir; - use bumpalo::Bump; - use roc_can::def::Declaration::*; - use roc_can::def::Def; - use roc_constrain::module::ExposedByModule; - use roc_load_internal::file::Threading; - use roc_load_internal::file::{LoadResult, LoadStart, LoadedModule, LoadingProblem, Phase}; - use roc_module::ident::ModuleName; - use roc_module::symbol::{Interns, ModuleId}; - use roc_problem::can::Problem; - use roc_region::all::LineInfo; - use roc_reporting::report::can_problem; - use roc_reporting::report::RenderTarget; - use roc_reporting::report::RocDocAllocator; - use roc_target::TargetInfo; - use roc_types::pretty_print::name_and_print_var; - use roc_types::pretty_print::DebugPrint; - use roc_types::subs::Subs; - use std::collections::HashMap; - use std::path::{Path, PathBuf}; - - fn load_and_typecheck<'a>( - arena: &'a Bump, - filename: PathBuf, - src_dir: &Path, - exposed_types: ExposedByModule, - target_info: TargetInfo, - ) -> Result> { - use LoadResult::*; - - let load_start = LoadStart::from_path(arena, filename, RenderTarget::Generic)?; - - match roc_load_internal::file::load( - arena, - load_start, - src_dir, - exposed_types, - Phase::SolveTypes, - target_info, - Default::default(), // these tests will re-compile the builtins - RenderTarget::Generic, - Threading::Single, - )? { - Monomorphized(_) => unreachable!(""), - TypeChecked(module) => Ok(module), - } - } - - const TARGET_INFO: roc_target::TargetInfo = roc_target::TargetInfo::default_x86_64(); - - // HELPERS - - fn format_can_problems( - problems: Vec, - home: ModuleId, - interns: &Interns, - filename: PathBuf, - src: &str, - ) -> String { - use ven_pretty::DocAllocator; - - let src_lines: Vec<&str> = src.split('\n').collect(); - let lines = LineInfo::new(src); - let alloc = RocDocAllocator::new(&src_lines, home, interns); - let reports = problems - .into_iter() - .map(|problem| can_problem(&alloc, &lines, filename.clone(), problem).pretty(&alloc)); - - let mut buf = String::new(); - alloc - .stack(reports) - .append(alloc.line()) - .1 - .render_raw(70, &mut roc_reporting::report::CiWrite::new(&mut buf)) - .unwrap(); - buf - } - - fn multiple_modules(subdir: &str, files: Vec<(&str, &str)>) -> Result { - let arena = Bump::new(); - let arena = &arena; - - match multiple_modules_help(subdir, arena, files) { - Err(io_error) => panic!("IO trouble: {:?}", io_error), - Ok(Err(LoadingProblem::FormattedReport(buf))) => Err(buf), - Ok(Err(loading_problem)) => Err(format!("{:?}", loading_problem)), - Ok(Ok(mut loaded_module)) => { - let home = loaded_module.module_id; - let (filepath, src) = loaded_module.sources.get(&home).unwrap(); - - let can_problems = loaded_module.can_problems.remove(&home).unwrap_or_default(); - if !can_problems.is_empty() { - return Err(format_can_problems( - can_problems, - home, - &loaded_module.interns, - filepath.clone(), - src, - )); - } - - assert!(loaded_module - .type_problems - .remove(&home) - .unwrap_or_default() - .is_empty(),); - - Ok(loaded_module) - } - } - } - - fn multiple_modules_help<'a>( - subdir: &str, - arena: &'a Bump, - mut files: Vec<(&str, &str)>, - ) -> Result>, std::io::Error> - { - use std::fs::{self, File}; - use std::io::Write; - - let mut file_handles: Vec<_> = Vec::new(); - - // Use a deterministic temporary directory. - // We can't have all tests use "tmp" because tests run in parallel, - // so append the test name to the tmp path. - let tmp = format!("tmp/{}", subdir); - let dir = roc_test_utils::TmpDir::new(&tmp); - - let app_module = files.pop().unwrap(); - - for (name, source) in files { - let mut filename = PathBuf::from(name); - filename.set_extension("roc"); - let file_path = dir.path().join(filename.clone()); - - // Create any necessary intermediate directories (e.g. /platform) - fs::create_dir_all(file_path.parent().unwrap())?; - - let mut file = File::create(file_path)?; - writeln!(file, "{}", source)?; - file_handles.push(file); - } - - let result = { - let (name, source) = app_module; - - let filename = PathBuf::from(name); - let file_path = dir.path().join(filename); - let full_file_path = file_path.clone(); - let mut file = File::create(file_path)?; - writeln!(file, "{}", source)?; - file_handles.push(file); - - load_and_typecheck( - arena, - full_file_path, - dir.path(), - Default::default(), - TARGET_INFO, - ) - }; - - Ok(result) - } - - fn load_fixture( - dir_name: &str, - module_name: &str, - subs_by_module: ExposedByModule, - ) -> LoadedModule { - let src_dir = fixtures_dir().join(dir_name); - let filename = src_dir.join(format!("{}.roc", module_name)); - let arena = Bump::new(); - let loaded = load_and_typecheck( - &arena, - filename, - src_dir.as_path(), - subs_by_module, - TARGET_INFO, - ); - let mut loaded_module = match loaded { - Ok(x) => x, - Err(roc_load_internal::file::LoadingProblem::FormattedReport(report)) => { - println!("{}", report); - panic!("{}", report); - } - Err(e) => panic!("{:?}", e), - }; - - let home = loaded_module.module_id; - - assert_eq!( - loaded_module.can_problems.remove(&home).unwrap_or_default(), - Vec::new() - ); - assert!(loaded_module - .type_problems - .remove(&home) - .unwrap_or_default() - .is_empty()); - - let expected_name = loaded_module - .interns - .module_ids - .get_name(loaded_module.module_id) - .expect("Test ModuleID not found in module_ids"); - - // App module names are hardcoded and not based on anything user-specified - if expected_name.as_str() != ModuleName::APP { - assert_eq!(&expected_name.as_str(), &module_name); - } - - loaded_module - } - - fn expect_def( - interns: &Interns, - subs: &mut Subs, - home: ModuleId, - def: &Def, - expected_types: &mut HashMap<&str, &str>, - ) { - for (symbol, expr_var) in &def.pattern_vars { - let actual_str = - name_and_print_var(*expr_var, subs, home, interns, DebugPrint::NOTHING); - let fully_qualified = symbol.fully_qualified(interns, home).to_string(); - let expected_type = expected_types - .remove(fully_qualified.as_str()) - .unwrap_or_else(|| { - panic!("Defs included an unexpected symbol: {:?}", fully_qualified) - }); - - assert_eq!((&symbol, expected_type), (&symbol, actual_str.as_str())); - } - } - - fn expect_types(mut loaded_module: LoadedModule, mut expected_types: HashMap<&str, &str>) { - let home = loaded_module.module_id; - let mut subs = loaded_module.solved.into_inner(); - - assert_eq!( - loaded_module.can_problems.remove(&home).unwrap_or_default(), - Vec::new() - ); - assert!(loaded_module - .type_problems - .remove(&home) - .unwrap_or_default() - .is_empty()); - - for decl in loaded_module.declarations_by_id.remove(&home).unwrap() { - match decl { - Declare(def) => expect_def( - &loaded_module.interns, - &mut subs, - home, - &def, - &mut expected_types, - ), - DeclareRec(defs, cycle_mark) => { - assert!(!cycle_mark.is_illegal(&subs)); - for def in defs { - expect_def( - &loaded_module.interns, - &mut subs, - home, - &def, - &mut expected_types, - ); - } - } - Builtin(_) => {} - cycle @ InvalidCycle(_) => { - panic!("Unexpected cyclic def in module declarations: {:?}", cycle); - } - }; - } - - assert_eq!( - expected_types, - HashMap::default(), - "Some expected types were not found in the defs" - ); - } - - // TESTS - - #[test] - fn import_transitive_alias() { - // this had a bug where NodeColor was HostExposed, and it's `actual_var` conflicted - // with variables in the importee - let modules = vec![ - ( - "RBTree", - indoc!( - r#" - interface RBTree exposes [RedBlackTree, empty] imports [] - - # The color of a node. Leaves are considered Black. - NodeColor : [Red, Black] - - RedBlackTree k v : [Node NodeColor k v (RedBlackTree k v) (RedBlackTree k v), Empty] - - # Create an empty dictionary. - empty : RedBlackTree k v - empty = - Empty - "# - ), - ), - ( - "Main", - indoc!( - r#" - interface Other exposes [empty] imports [RBTree] - - empty : RBTree.RedBlackTree I64 I64 - empty = RBTree.empty - "# - ), - ), - ]; - - assert!(multiple_modules("import_transitive_alias", modules).is_ok()); - } - - #[test] - fn interface_with_deps() { - let subs_by_module = Default::default(); - let src_dir = fixtures_dir().join("interface_with_deps"); - let filename = src_dir.join("Primary.roc"); - let arena = Bump::new(); - let loaded = load_and_typecheck( - &arena, - filename, - src_dir.as_path(), - subs_by_module, - TARGET_INFO, - ); - - let mut loaded_module = loaded.expect("Test module failed to load"); - let home = loaded_module.module_id; - - assert_eq!( - loaded_module.can_problems.remove(&home).unwrap_or_default(), - Vec::new() - ); - assert!(loaded_module - .type_problems - .remove(&home) - .unwrap_or_default() - .is_empty(),); - - let def_count: usize = loaded_module - .declarations_by_id - .remove(&loaded_module.module_id) - .unwrap() - .into_iter() - .map(|decl| decl.def_count()) - .sum(); - - let expected_name = loaded_module - .interns - .module_ids - .get_name(loaded_module.module_id) - .expect("Test ModuleID not found in module_ids"); - - assert_eq!(expected_name.as_str(), "Primary"); - assert_eq!(def_count, 10); - } - - #[test] - fn load_unit() { - let subs_by_module = Default::default(); - let loaded_module = load_fixture("no_deps", "Unit", subs_by_module); - - expect_types( - loaded_module, - hashmap! { - "unit" => "Unit", - }, - ); - } - - #[test] - fn import_alias() { - let subs_by_module = Default::default(); - let loaded_module = load_fixture("interface_with_deps", "ImportAlias", subs_by_module); - - expect_types( - loaded_module, - hashmap! { - "unit" => "Dep1.Unit", - }, - ); - } - - #[test] - fn test_load_and_typecheck() { - let subs_by_module = Default::default(); - let loaded_module = load_fixture("interface_with_deps", "WithBuiltins", subs_by_module); - - expect_types( - loaded_module, - hashmap! { - "floatTest" => "F64", - "divisionFn" => "Float a, Float a -> Float a", - "x" => "Float *", - "divisionTest" => "F64", - "intTest" => "I64", - "constantNum" => "Num *", - "divisionTest" => "F64", - "divDep1ByDep2" => "Float *", - "fromDep2" => "Float *", - }, - ); - } - - #[test] - fn iface_quicksort() { - let subs_by_module = Default::default(); - let loaded_module = load_fixture("interface_with_deps", "Quicksort", subs_by_module); - - expect_types( - loaded_module, - hashmap! { - "swap" => "Nat, Nat, List a -> List a", - "partition" => "Nat, Nat, List (Num a) -> [Pair Nat (List (Num a))]", - "partitionHelp" => "Nat, Nat, List (Num a), Nat, Num a -> [Pair Nat (List (Num a))]", - "quicksort" => "List (Num a), Nat, Nat -> List (Num a)", - }, - ); - } - - #[test] - fn quicksort_one_def() { - let subs_by_module = Default::default(); - let loaded_module = load_fixture("app_with_deps", "QuicksortOneDef", subs_by_module); - - expect_types( - loaded_module, - hashmap! { - "quicksort" => "List (Num a) -> List (Num a)", - }, - ); - } - - #[test] - fn app_quicksort() { - let subs_by_module = Default::default(); - let loaded_module = load_fixture("app_with_deps", "Quicksort", subs_by_module); - - expect_types( - loaded_module, - hashmap! { - "swap" => "Nat, Nat, List a -> List a", - "partition" => "Nat, Nat, List (Num a) -> [Pair Nat (List (Num a))]", - "partitionHelp" => "Nat, Nat, List (Num a), Nat, Num a -> [Pair Nat (List (Num a))]", - "quicksort" => "List (Num a), Nat, Nat -> List (Num a)", - }, - ); - } - - #[test] - fn load_astar() { - let subs_by_module = Default::default(); - let loaded_module = load_fixture("interface_with_deps", "AStar", subs_by_module); - - expect_types( - loaded_module, - hashmap! { - "findPath" => "{ costFunction : position, position -> F64, end : position, moveFunction : position -> Set position, start : position } -> Result (List position) [KeyNotFound]*", - "initialModel" => "position -> Model position", - "reconstructPath" => "Dict position position, position -> List position", - "updateCost" => "position, position, Model position -> Model position", - "cheapestOpen" => "(position -> F64), Model position -> Result position [KeyNotFound]*", - "astar" => "(position, position -> F64), (position -> Set position), position, Model position -> [Err [KeyNotFound]*, Ok (List position)]*", - }, - ); - } - - #[test] - fn load_principal_types() { - let subs_by_module = Default::default(); - let loaded_module = load_fixture("no_deps", "Principal", subs_by_module); - - expect_types( - loaded_module, - hashmap! { - "intVal" => "Str", - "identity" => "a -> a", - }, - ); - } - - #[test] - fn iface_dep_types() { - let subs_by_module = Default::default(); - let loaded_module = load_fixture("interface_with_deps", "Primary", subs_by_module); - - expect_types( - loaded_module, - hashmap! { - "blah2" => "Float *", - "blah3" => "Str", - "str" => "Str", - "alwaysThree" => "* -> Float *", - "identity" => "a -> a", - "z" => "Float *", - "w" => "Dep1.Identity {}", - "succeed" => "a -> Dep1.Identity a", - "yay" => "Res.Res {} err", - "withDefault" => "Res.Res a err, a -> a", - }, - ); - } - - #[test] - fn app_dep_types() { - let subs_by_module = Default::default(); - let loaded_module = load_fixture("app_with_deps", "Primary", subs_by_module); - - expect_types( - loaded_module, - hashmap! { - "blah2" => "Float *", - "blah3" => "Str", - "str" => "Str", - "alwaysThree" => "* -> Float *", - "identity" => "a -> a", - "z" => "Float *", - "w" => "Dep1.Identity {}", - "succeed" => "a -> Dep1.Identity a", - "yay" => "Res.Res {} err", - "withDefault" => "Res.Res a err, a -> a", - }, - ); - } - - #[test] - fn imported_dep_regression() { - let subs_by_module = Default::default(); - let loaded_module = load_fixture("interface_with_deps", "OneDep", subs_by_module); - - expect_types( - loaded_module, - hashmap! { - "str" => "Str", - }, - ); - } - - #[test] - fn parse_problem() { - let modules = vec![( - "Main", - indoc!( - r#" - interface Main exposes [main] imports [] - - main = [ - "# - ), - )]; - - match multiple_modules("parse_problem", modules) { - Err(report) => assert_eq!( - report, - indoc!( - " - ── UNFINISHED LIST ──────────────────────────────────── tmp/parse_problem/Main ─ - - I cannot find the end of this list: - - 3│ main = [ - ^ - - You could change it to something like [1, 2, 3] or even just []. - Anything where there is an open and a close square bracket, and where - the elements of the list are separated by commas. - - Note: I may be confused by indentation" - ) - ), - Ok(_) => unreachable!("we expect failure here"), - } - } - - #[test] - #[should_panic(expected = "FILE NOT FOUND")] - fn file_not_found() { - let subs_by_module = Default::default(); - let loaded_module = load_fixture("interface_with_deps", "invalid$name", subs_by_module); - - expect_types( - loaded_module, - hashmap! { - "str" => "Str", - }, - ); - } - - #[test] - #[should_panic(expected = "FILE NOT FOUND")] - fn imported_file_not_found() { - let subs_by_module = Default::default(); - let loaded_module = load_fixture("no_deps", "MissingDep", subs_by_module); - - expect_types( - loaded_module, - hashmap! { - "str" => "Str", - }, - ); - } - - #[test] - fn platform_does_not_exist() { - let modules = vec![( - "Main", - indoc!( - r#" - app "example" - packages { pf: "./zzz-does-not-exist" } - imports [] - provides [main] to pf - - main = "" - "# - ), - )]; - - match multiple_modules("platform_does_not_exist", modules) { - Err(report) => { - assert!(report.contains("FILE NOT FOUND"), "report=({})", report); - assert!( - report.contains("zzz-does-not-exist/Package-Config.roc"), - "report=({})", - report - ); - } - Ok(_) => unreachable!("we expect failure here"), - } - } - - #[test] - fn platform_parse_error() { - let modules = vec![ - ( - "platform/Package-Config.roc", - indoc!( - r#" - platform "hello-c" - requires {} { main : Str } - exposes [] - packages {} - imports [] - provides [mainForHost] - blah 1 2 3 # causing a parse error on purpose - - mainForHost : Str - "# - ), - ), - ( - "Main", - indoc!( - r#" - app "hello-world" - packages { pf: "platform" } - imports [] - provides [main] to pf - - main = "Hello, World!\n" - "# - ), - ), - ]; - - match multiple_modules("platform_parse_error", modules) { - Err(report) => { - assert!(report.contains("NOT END OF FILE")); - assert!(report.contains("blah 1 2 3 # causing a parse error on purpose")); - } - Ok(_) => unreachable!("we expect failure here"), - } - } - - #[test] - // See https://github.com/rtfeldman/roc/issues/2413 - fn platform_exposes_main_return_by_pointer_issue() { - let modules = vec![ - ( - "platform/Package-Config.roc", - indoc!( - r#" - platform "hello-world" - requires {} { main : { content: Str, other: Str } } - exposes [] - packages {} - imports [] - provides [mainForHost] - - mainForHost : { content: Str, other: Str } - mainForHost = main - "# - ), - ), - ( - "Main", - indoc!( - r#" - app "hello-world" - packages { pf: "platform" } - imports [] - provides [main] to pf - - main = { content: "Hello, World!\n", other: "" } - "# - ), - ), - ]; - - assert!(multiple_modules("platform_exposes_main_return_by_pointer_issue", modules).is_ok()); - } - - #[test] - fn opaque_wrapped_unwrapped_outside_defining_module() { - let modules = vec![ - ( - "Age", - indoc!( - r#" - interface Age exposes [Age] imports [] - - Age := U32 - "# - ), - ), - ( - "Main", - indoc!( - r#" - interface Main exposes [twenty, readAge] imports [Age.{ Age }] - - twenty = @Age 20 - - readAge = \@Age n -> n - "# - ), - ), - ]; - - let err = multiple_modules("opaque_wrapped_unwrapped_outside_defining_module", modules) - .unwrap_err(); - assert_eq!( - err, - indoc!( - r#" - ── OPAQUE TYPE DECLARED OUTSIDE SCOPE ─ ...rapped_outside_defining_module/Main ─ - - The unwrapped opaque type Age referenced here: - - 3│ twenty = @Age 20 - ^^^^ - - is imported from another module: - - 1│ interface Main exposes [twenty, readAge] imports [Age.{ Age }] - ^^^^^^^^^^^ - - Note: Opaque types can only be wrapped and unwrapped in the module they are defined in! - - ── OPAQUE TYPE DECLARED OUTSIDE SCOPE ─ ...rapped_outside_defining_module/Main ─ - - The unwrapped opaque type Age referenced here: - - 5│ readAge = \@Age n -> n - ^^^^ - - is imported from another module: - - 1│ interface Main exposes [twenty, readAge] imports [Age.{ Age }] - ^^^^^^^^^^^ - - Note: Opaque types can only be wrapped and unwrapped in the module they are defined in! - - ── UNUSED IMPORT ─── tmp/opaque_wrapped_unwrapped_outside_defining_module/Main ─ - - Nothing from Age is used in this module. - - 1│ interface Main exposes [twenty, readAge] imports [Age.{ Age }] - ^^^^^^^^^^^ - - Since Age isn't used, you don't need to import it. - "# - ), - "\n{}", - err - ); - } - - #[test] - fn issue_2863_module_type_does_not_exist() { - let modules = vec![ - ( - "platform/Package-Config.roc", - indoc!( - r#" - platform "testplatform" - requires {} { main : Str } - exposes [] - packages {} - imports [] - provides [mainForHost] - - mainForHost : Str - mainForHost = main - "# - ), - ), - ( - "Main", - indoc!( - r#" - app "test" - packages { pf: "platform" } - provides [main] to pf - - main : DoesNotExist - main = 1 - "# - ), - ), - ]; - - match multiple_modules("issue_2863_module_type_does_not_exist", modules) { - Err(report) => { - assert_eq!( - report, - indoc!( - " - ── UNRECOGNIZED NAME ────────── tmp/issue_2863_module_type_does_not_exist/Main ─ - - Nothing is named `DoesNotExist` in this scope. - - 5│ main : DoesNotExist - ^^^^^^^^^^^^ - - Did you mean one of these? - - Dict - Result - List - Box - " - ) - ) - } - Ok(_) => unreachable!("we expect failure here"), - } - } -} diff --git a/compiler/module/Cargo.toml b/compiler/module/Cargo.toml deleted file mode 100644 index 26db73eeb1..0000000000 --- a/compiler/module/Cargo.toml +++ /dev/null @@ -1,16 +0,0 @@ -[package] -name = "roc_module" -version = "0.1.0" -authors = ["The Roc Contributors"] -edition = "2021" -license = "UPL-1.0" - -[dependencies] -roc_region = { path = "../region" } -roc_ident = { path = "../ident" } -roc_collections = { path = "../collections" } -roc_error_macros = {path = "../../error_macros"} -bumpalo = { version = "3.8.0", features = ["collections"] } -lazy_static = "1.4.0" -static_assertions = "1.1.0" -snafu = { version = "0.6.10", features = ["backtraces"] } diff --git a/compiler/module/src/called_via.rs b/compiler/module/src/called_via.rs deleted file mode 100644 index bc314e0eec..0000000000 --- a/compiler/module/src/called_via.rs +++ /dev/null @@ -1,161 +0,0 @@ -use self::BinOp::*; -use std::cmp::Ordering; -use std::fmt; - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum CalledVia { - /// Calling with space, e.g. (foo bar) - Space, - - /// Calling with an operator, e.g. (bar |> foo) or (1 + 2) - BinOp(BinOp), - - /// Calling with a unary operator, e.g. (!foo bar baz) or (-foo bar baz) - UnaryOp(UnaryOp), - - /// This call is the result of desugaring string interpolation, - /// e.g. "\(first) \(last)" is transformed into Str.concat (Str.concat first " ") last. - StringInterpolation, -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum UnaryOp { - /// (-), e.g. (-x) - Negate, - /// (!), e.g. (!x) - Not, -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum BinOp { - // highest precedence - Caret, - Star, - Slash, - DoubleSlash, - Percent, - Plus, - Minus, - Equals, - NotEquals, - LessThan, - GreaterThan, - LessThanOrEq, - GreaterThanOrEq, - And, - Or, - Pizza, - Assignment, - IsAliasType, - IsOpaqueType, - Backpassing, - // lowest precedence -} - -impl BinOp { - /// how wide this operator is when typed out - pub fn width(self) -> u16 { - match self { - Caret | Star | Slash | Percent | Plus | Minus | LessThan | GreaterThan => 1, - DoubleSlash | Equals | NotEquals | LessThanOrEq | GreaterThanOrEq | And | Or - | Pizza => 2, - Assignment | IsAliasType | IsOpaqueType | Backpassing => unreachable!(), - } - } -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum ArgSide { - Left, - Right, -} - -#[derive(Copy, Clone, Debug, PartialEq)] -pub enum Associativity { - /// left-associative operators: - /// - /// arithmetic: * / // % + - - /// application: |> - LeftAssociative, - - /// right-associative operators: - /// - /// exponentiation: ^ - /// boolean: && || - /// application: <| - RightAssociative, - - /// non-associative operators: - /// - /// comparison: == > >= < <= - NonAssociative, -} - -impl BinOp { - pub fn associativity(self) -> Associativity { - use self::Associativity::*; - - match self { - Pizza | Star | Slash | DoubleSlash | Percent | Plus | Minus => LeftAssociative, - And | Or | Caret => RightAssociative, - Equals | NotEquals | LessThan | GreaterThan | LessThanOrEq | GreaterThanOrEq => { - NonAssociative - } - Assignment | IsAliasType | IsOpaqueType | Backpassing => unreachable!(), - } - } - - fn precedence(self) -> u8 { - match self { - Caret => 7, - Star | Slash | DoubleSlash | Percent => 6, - Plus | Minus => 5, - Equals | NotEquals | LessThan | GreaterThan | LessThanOrEq | GreaterThanOrEq => 4, - And => 3, - Or => 2, - Pizza => 1, - Assignment | IsAliasType | IsOpaqueType | Backpassing => unreachable!(), - } - } -} - -impl PartialOrd for BinOp { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -impl Ord for BinOp { - fn cmp(&self, other: &Self) -> Ordering { - self.precedence().cmp(&other.precedence()) - } -} - -impl std::fmt::Display for BinOp { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let as_str = match self { - Caret => "^", - Star => "*", - Slash => "/", - DoubleSlash => "//", - Percent => "%", - Plus => "+", - Minus => "-", - Equals => "==", - NotEquals => "!=", - LessThan => "<", - GreaterThan => ">", - LessThanOrEq => "<=", - GreaterThanOrEq => ">=", - And => "&&", - Or => "||", - Pizza => "|>", - Assignment => "=", - IsAliasType => ":", - IsOpaqueType => ":=", - Backpassing => "<-", - }; - - write!(f, "{}", as_str) - } -} diff --git a/compiler/module/src/ident.rs b/compiler/module/src/ident.rs deleted file mode 100644 index 928f04f71d..0000000000 --- a/compiler/module/src/ident.rs +++ /dev/null @@ -1,298 +0,0 @@ -pub use roc_ident::IdentStr; -use std::fmt; - -/// This could be uppercase or lowercase, qualified or unqualified. -#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Default)] -pub struct Ident(pub IdentStr); - -impl Ident { - pub fn as_inline_str(&self) -> &IdentStr { - &self.0 - } - - #[inline(always)] - pub fn as_str(&self) -> &str { - self.0.as_str() - } -} - -pub struct QualifiedModuleName<'a> { - pub opt_package: Option<&'a str>, - pub module: ModuleName, -} - -#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] -pub struct ModuleName(IdentStr); - -impl std::ops::Deref for ModuleName { - type Target = str; - - fn deref(&self) -> &Self::Target { - self.0.as_str() - } -} - -/// An uncapitalized identifier, such as a field name or local variable -#[derive(Clone, Default, PartialEq, Eq, Hash, PartialOrd, Ord)] -pub struct Lowercase(IdentStr); - -/// A capitalized identifier, such as a tag name or module name -#[derive(Clone, Default, PartialEq, Eq, Hash, PartialOrd, Ord)] -pub struct Uppercase(IdentStr); - -/// A string representing a foreign (linked-in) symbol -#[derive(Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Debug)] -pub struct ForeignSymbol(IdentStr); - -pub type TagIdIntType = u16; - -/// Tags have no module, but tend to be short strings (since they're -/// never qualified), so we store them as ident strings. -/// -/// This is allows canonicalization to happen in parallel without locks. -/// If tags had a Symbol representation, then each module would have to -/// deal with contention on a global mutex around translating tag strings -/// into integers. (Record field labels work the same way, for the same reason.) -#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] -pub struct TagName(pub Uppercase); - -roc_error_macros::assert_sizeof_non_wasm!(TagName, 16); -roc_error_macros::assert_sizeof_wasm!(TagName, 8); - -impl TagName { - pub fn as_ident_str(&self) -> IdentStr { - self.0.as_ident_str().clone() - } -} - -impl ModuleName { - // NOTE: After adding one of these, go to `impl ModuleId` and - // add a corresponding ModuleId to there! - pub const APP: &'static str = "#UserApp"; // app modules have this hardcoded name - pub const BOOL: &'static str = "Bool"; - pub const STR: &'static str = "Str"; - pub const NUM: &'static str = "Num"; - pub const LIST: &'static str = "List"; - pub const DICT: &'static str = "Dict"; - pub const SET: &'static str = "Set"; - pub const RESULT: &'static str = "Result"; - pub const BOX: &'static str = "Box"; - pub const ENCODE: &'static str = "Encode"; - pub const JSON: &'static str = "Json"; - - pub fn as_str(&self) -> &str { - self.0.as_str() - } - - pub fn as_ident_str(&self) -> &IdentStr { - &self.0 - } - - pub fn is_empty(&self) -> bool { - self.0.is_empty() - } -} - -impl AsRef for ModuleName { - #[inline(always)] - fn as_ref(&self) -> &str { - self.as_str() - } -} - -impl<'a> From<&'a str> for ModuleName { - fn from(string: &'a str) -> Self { - Self(string.into()) - } -} - -impl<'a> From for ModuleName { - fn from(string: IdentStr) -> Self { - Self(string.as_str().into()) - } -} - -impl From> for ModuleName { - fn from(string: Box) -> Self { - Self((string.as_ref()).into()) - } -} - -impl From for ModuleName { - fn from(string: String) -> Self { - Self(string.into()) - } -} - -impl From for Box { - fn from(name: ModuleName) -> Self { - name.0.to_string().into() - } -} - -impl fmt::Display for ModuleName { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - self.0.fmt(f) - } -} - -impl ForeignSymbol { - pub fn as_str(&self) -> &str { - self.0.as_str() - } - - pub fn as_inline_str(&self) -> &IdentStr { - &self.0 - } -} - -impl<'a> From<&'a str> for ForeignSymbol { - fn from(string: &'a str) -> Self { - Self(string.into()) - } -} - -impl<'a> From for ForeignSymbol { - fn from(string: String) -> Self { - Self(string.into()) - } -} - -impl Uppercase { - pub fn as_str(&self) -> &str { - self.0.as_str() - } - - pub fn as_ident_str(&self) -> &IdentStr { - &self.0 - } -} - -impl<'a> From<&'a str> for Uppercase { - fn from(string: &'a str) -> Self { - Self(string.into()) - } -} - -impl<'a> From for Uppercase { - fn from(string: String) -> Self { - Self(string.into()) - } -} - -impl Lowercase { - pub fn as_str(&self) -> &str { - self.0.as_str() - } -} - -impl<'a> From<&'a str> for Lowercase { - fn from(string: &'a str) -> Self { - Self(string.into()) - } -} - -impl<'a> From<&'a Lowercase> for &'a str { - fn from(lowercase: &'a Lowercase) -> Self { - lowercase.as_str() - } -} - -impl<'a> From for Lowercase { - fn from(string: String) -> Self { - Self(string.into()) - } -} - -impl AsRef for Ident { - #[inline(always)] - fn as_ref(&self) -> &str { - self.0.as_str() - } -} - -impl<'a> From<&'a str> for Ident { - fn from(string: &'a str) -> Self { - Self(string.into()) - } -} - -impl From> for Ident { - fn from(string: Box) -> Self { - Self((string.as_ref()).into()) - } -} - -impl From for Ident { - fn from(string: String) -> Self { - Self(string.into()) - } -} - -impl From for Ident { - fn from(string: IdentStr) -> Self { - Self(string) - } -} - -impl From for IdentStr { - fn from(ident: Ident) -> Self { - ident.0 - } -} - -impl<'a> From<&'a Ident> for &'a IdentStr { - fn from(ident: &'a Ident) -> Self { - &ident.0 - } -} - -impl From for Box { - fn from(ident: Ident) -> Self { - ident.0.to_string().into() - } -} - -impl fmt::Display for Ident { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - self.0.fmt(f) - } -} - -/// Rather than displaying as this: -/// -/// Lowercase("foo") -/// -/// ...instead display as this: -/// -/// 'foo' -impl fmt::Debug for Lowercase { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "'{}'", self.0) - } -} - -impl fmt::Display for Lowercase { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - self.0.fmt(f) - } -} - -/// Rather than displaying as this: -/// -/// Uppercase("Foo") -/// -/// ...instead display as this: -/// -/// 'Foo' -impl fmt::Debug for Uppercase { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "'{}'", self.0) - } -} - -impl fmt::Display for Uppercase { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - self.0.fmt(f) - } -} diff --git a/compiler/module/src/lib.rs b/compiler/module/src/lib.rs deleted file mode 100644 index 044f697a07..0000000000 --- a/compiler/module/src/lib.rs +++ /dev/null @@ -1,12 +0,0 @@ -#![warn(clippy::dbg_macro)] -// See github.com/rtfeldman/roc/issues/800 for discussion of the large_enum_variant check. -#![allow(clippy::large_enum_variant, clippy::upper_case_acronyms)] - -pub mod called_via; -pub mod ident; -pub mod low_level; -pub mod module_err; -pub mod symbol; - -#[macro_use] -extern crate lazy_static; diff --git a/compiler/module/src/low_level.rs b/compiler/module/src/low_level.rs deleted file mode 100644 index 7af1880cdf..0000000000 --- a/compiler/module/src/low_level.rs +++ /dev/null @@ -1,338 +0,0 @@ -use crate::symbol::Symbol; - -/// Low-level operations that get translated directly into e.g. LLVM instructions. -/// These are always wrapped when exposed to end users, and can only make it -/// into an Expr when added directly by can::builtins -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub enum LowLevel { - StrConcat, - StrJoinWith, - StrIsEmpty, - StrStartsWith, - StrStartsWithCodePt, - StrEndsWith, - StrSplit, - StrCountGraphemes, - StrFromInt, - StrFromUtf8, - StrFromUtf8Range, - StrToUtf8, - StrRepeat, - StrFromFloat, - StrTrim, - StrTrimLeft, - StrTrimRight, - StrToNum, - ListLen, - ListGetUnsafe, - ListSingle, - ListRepeat, - ListReplaceUnsafe, - ListReverse, - ListConcat, - ListContains, - ListAppend, - ListPrepend, - ListJoin, - ListRange, - ListMap, - ListMap2, - ListMap3, - ListMap4, - ListMapWithIndex, - ListKeepIf, - ListWalk, - ListWalkUntil, - ListWalkBackwards, - ListKeepOks, - ListKeepErrs, - ListSortWith, - ListSublist, - ListDropAt, - ListSwap, - ListAny, - ListAll, - ListFindUnsafe, - ListIsUnique, - DictSize, - DictEmpty, - DictInsert, - DictRemove, - DictContains, - DictGetUnsafe, - DictKeys, - DictValues, - DictUnion, - DictIntersection, - DictDifference, - DictWalk, - SetFromList, - SetToDict, - NumAdd, - NumAddWrap, - NumAddChecked, - NumAddSaturated, - NumSub, - NumSubWrap, - NumSubChecked, - NumSubSaturated, - NumMul, - NumMulWrap, - NumMulChecked, - NumGt, - NumGte, - NumLt, - NumLte, - NumCompare, - NumDivUnchecked, - NumDivCeilUnchecked, - NumRemUnchecked, - NumIsMultipleOf, - NumAbs, - NumNeg, - NumSin, - NumCos, - NumSqrtUnchecked, - NumLogUnchecked, - NumRound, - NumToFrac, - NumPow, - NumCeiling, - NumPowInt, - NumFloor, - NumIsFinite, - NumAtan, - NumAcos, - NumAsin, - NumBytesToU16, - NumBytesToU32, - NumBitwiseAnd, - NumBitwiseXor, - NumBitwiseOr, - NumShiftLeftBy, - NumShiftRightBy, - NumShiftRightZfBy, - NumIntCast, - NumToFloatCast, - NumToIntChecked, - NumToFloatChecked, - NumToStr, - Eq, - NotEq, - And, - Or, - Not, - Hash, - PtrCast, - RefCountInc, - RefCountDec, - BoxExpr, - UnboxExpr, -} - -macro_rules! higher_order { - () => { - ListMap - | ListMap2 - | ListMap3 - | ListMap4 - | ListMapWithIndex - | ListKeepIf - | ListWalk - | ListWalkUntil - | ListWalkBackwards - | ListKeepOks - | ListKeepErrs - | ListSortWith - | ListAny - | ListAll - | ListFindUnsafe - | DictWalk - }; -} - -impl LowLevel { - /// is one of the arguments always a function? - /// An example is List.map. - pub fn is_higher_order(&self) -> bool { - use LowLevel::*; - - matches!(self, higher_order!()) - } - - pub fn function_argument_position(&self) -> usize { - use LowLevel::*; - - match self { - ListMap => 1, - ListMap2 => 2, - ListMap3 => 3, - ListMap4 => 4, - ListMapWithIndex => 1, - ListKeepIf => 1, - ListWalk => 2, - ListWalkUntil => 2, - ListWalkBackwards => 2, - ListKeepOks => 1, - ListKeepErrs => 1, - ListSortWith => 1, - ListAny => 1, - ListAll => 1, - ListFindUnsafe => 1, - DictWalk => 2, - _ => unreachable!(), - } - } -} - -/// Some wrapper functions can just be replaced by lowlevels in the backend for performance. -/// For example, Num.add should be an instruction, not a function call. -/// Variant names are chosen to help explain what to do when adding new lowlevels -pub enum LowLevelWrapperType { - /// This wrapper function contains no logic and we can remove it in code gen - CanBeReplacedBy(LowLevel), - /// This wrapper function contains important logic and we cannot remove it in code gen - WrapperIsRequired, - NotALowLevelWrapper, -} - -impl LowLevelWrapperType { - pub fn from_symbol(symbol: Symbol) -> LowLevelWrapperType { - use LowLevel::*; - use LowLevelWrapperType::*; - - match symbol { - Symbol::STR_CONCAT => CanBeReplacedBy(StrConcat), - Symbol::STR_JOIN_WITH => CanBeReplacedBy(StrJoinWith), - Symbol::STR_IS_EMPTY => CanBeReplacedBy(StrIsEmpty), - Symbol::STR_STARTS_WITH => CanBeReplacedBy(StrStartsWith), - Symbol::STR_STARTS_WITH_CODE_PT => CanBeReplacedBy(StrStartsWithCodePt), - Symbol::STR_ENDS_WITH => CanBeReplacedBy(StrEndsWith), - Symbol::STR_SPLIT => CanBeReplacedBy(StrSplit), - Symbol::STR_COUNT_GRAPHEMES => CanBeReplacedBy(StrCountGraphemes), - Symbol::STR_FROM_UTF8 => WrapperIsRequired, - Symbol::STR_FROM_UTF8_RANGE => WrapperIsRequired, - Symbol::STR_TO_UTF8 => CanBeReplacedBy(StrToUtf8), - Symbol::STR_REPEAT => CanBeReplacedBy(StrRepeat), - Symbol::STR_TRIM => CanBeReplacedBy(StrTrim), - Symbol::STR_TRIM_LEFT => CanBeReplacedBy(StrTrimLeft), - Symbol::STR_TRIM_RIGHT => CanBeReplacedBy(StrTrimRight), - Symbol::STR_TO_DEC => WrapperIsRequired, - Symbol::STR_TO_F64 => WrapperIsRequired, - Symbol::STR_TO_F32 => WrapperIsRequired, - Symbol::STR_TO_NAT => WrapperIsRequired, - Symbol::STR_TO_U128 => WrapperIsRequired, - Symbol::STR_TO_I128 => WrapperIsRequired, - Symbol::STR_TO_U64 => WrapperIsRequired, - Symbol::STR_TO_I64 => WrapperIsRequired, - Symbol::STR_TO_U32 => WrapperIsRequired, - Symbol::STR_TO_I32 => WrapperIsRequired, - Symbol::STR_TO_U16 => WrapperIsRequired, - Symbol::STR_TO_I16 => WrapperIsRequired, - Symbol::STR_TO_U8 => WrapperIsRequired, - Symbol::STR_TO_I8 => WrapperIsRequired, - Symbol::LIST_LEN => CanBeReplacedBy(ListLen), - Symbol::LIST_GET => WrapperIsRequired, - Symbol::LIST_REPLACE => WrapperIsRequired, - Symbol::LIST_SINGLE => CanBeReplacedBy(ListSingle), - Symbol::LIST_REPEAT => CanBeReplacedBy(ListRepeat), - Symbol::LIST_REVERSE => CanBeReplacedBy(ListReverse), - Symbol::LIST_CONCAT => CanBeReplacedBy(ListConcat), - Symbol::LIST_CONTAINS => CanBeReplacedBy(ListContains), - Symbol::LIST_APPEND => CanBeReplacedBy(ListAppend), - Symbol::LIST_PREPEND => CanBeReplacedBy(ListPrepend), - Symbol::LIST_JOIN => CanBeReplacedBy(ListJoin), - Symbol::LIST_RANGE => CanBeReplacedBy(ListRange), - Symbol::LIST_MAP => WrapperIsRequired, - Symbol::LIST_MAP2 => WrapperIsRequired, - Symbol::LIST_MAP3 => WrapperIsRequired, - Symbol::LIST_MAP4 => WrapperIsRequired, - Symbol::LIST_MAP_WITH_INDEX => WrapperIsRequired, - Symbol::LIST_KEEP_IF => WrapperIsRequired, - Symbol::LIST_WALK => WrapperIsRequired, - Symbol::LIST_WALK_UNTIL => WrapperIsRequired, - Symbol::LIST_WALK_BACKWARDS => WrapperIsRequired, - Symbol::LIST_KEEP_OKS => WrapperIsRequired, - Symbol::LIST_KEEP_ERRS => WrapperIsRequired, - Symbol::LIST_SORT_WITH => WrapperIsRequired, - Symbol::LIST_SUBLIST => CanBeReplacedBy(ListSublist), - Symbol::LIST_DROP_AT => CanBeReplacedBy(ListDropAt), - Symbol::LIST_SWAP => CanBeReplacedBy(ListSwap), - Symbol::LIST_ANY => WrapperIsRequired, - Symbol::LIST_ALL => WrapperIsRequired, - Symbol::LIST_FIND => WrapperIsRequired, - Symbol::DICT_LEN => CanBeReplacedBy(DictSize), - Symbol::DICT_EMPTY => CanBeReplacedBy(DictEmpty), - Symbol::DICT_INSERT => CanBeReplacedBy(DictInsert), - Symbol::DICT_REMOVE => CanBeReplacedBy(DictRemove), - Symbol::DICT_CONTAINS => CanBeReplacedBy(DictContains), - Symbol::DICT_GET => WrapperIsRequired, - Symbol::DICT_KEYS => CanBeReplacedBy(DictKeys), - Symbol::DICT_VALUES => CanBeReplacedBy(DictValues), - Symbol::DICT_UNION => CanBeReplacedBy(DictUnion), - Symbol::DICT_INTERSECTION => CanBeReplacedBy(DictIntersection), - Symbol::DICT_DIFFERENCE => CanBeReplacedBy(DictDifference), - Symbol::DICT_WALK => WrapperIsRequired, - Symbol::SET_FROM_LIST => CanBeReplacedBy(SetFromList), - Symbol::NUM_ADD => CanBeReplacedBy(NumAdd), - Symbol::NUM_ADD_WRAP => CanBeReplacedBy(NumAddWrap), - Symbol::NUM_ADD_CHECKED => WrapperIsRequired, - Symbol::NUM_ADD_SATURATED => CanBeReplacedBy(NumAddSaturated), - Symbol::NUM_SUB => CanBeReplacedBy(NumSub), - Symbol::NUM_SUB_WRAP => CanBeReplacedBy(NumSubWrap), - Symbol::NUM_SUB_CHECKED => WrapperIsRequired, - Symbol::NUM_SUB_SATURATED => CanBeReplacedBy(NumSubSaturated), - Symbol::NUM_MUL => CanBeReplacedBy(NumMul), - Symbol::NUM_MUL_WRAP => CanBeReplacedBy(NumMulWrap), - Symbol::NUM_MUL_CHECKED => WrapperIsRequired, - Symbol::NUM_GT => CanBeReplacedBy(NumGt), - Symbol::NUM_GTE => CanBeReplacedBy(NumGte), - Symbol::NUM_LT => CanBeReplacedBy(NumLt), - Symbol::NUM_LTE => CanBeReplacedBy(NumLte), - Symbol::NUM_COMPARE => CanBeReplacedBy(NumCompare), - Symbol::NUM_DIV_FRAC => CanBeReplacedBy(NumDivUnchecked), - Symbol::NUM_DIV_FRAC_CHECKED => WrapperIsRequired, - Symbol::NUM_DIV_CEIL => CanBeReplacedBy(NumDivCeilUnchecked), - Symbol::NUM_DIV_CEIL_CHECKED => WrapperIsRequired, - Symbol::NUM_REM => CanBeReplacedBy(NumRemUnchecked), - Symbol::NUM_REM_CHECKED => WrapperIsRequired, - Symbol::NUM_IS_MULTIPLE_OF => CanBeReplacedBy(NumIsMultipleOf), - Symbol::NUM_ABS => CanBeReplacedBy(NumAbs), - Symbol::NUM_NEG => CanBeReplacedBy(NumNeg), - Symbol::NUM_SIN => CanBeReplacedBy(NumSin), - Symbol::NUM_COS => CanBeReplacedBy(NumCos), - Symbol::NUM_SQRT => CanBeReplacedBy(NumSqrtUnchecked), - Symbol::NUM_SQRT_CHECKED => WrapperIsRequired, - Symbol::NUM_LOG => CanBeReplacedBy(NumLogUnchecked), - Symbol::NUM_LOG_CHECKED => WrapperIsRequired, - Symbol::NUM_ROUND => CanBeReplacedBy(NumRound), - Symbol::NUM_TO_FRAC => CanBeReplacedBy(NumToFrac), - Symbol::NUM_POW => CanBeReplacedBy(NumPow), - Symbol::NUM_CEILING => CanBeReplacedBy(NumCeiling), - Symbol::NUM_POW_INT => CanBeReplacedBy(NumPowInt), - Symbol::NUM_FLOOR => CanBeReplacedBy(NumFloor), - Symbol::NUM_TO_STR => CanBeReplacedBy(NumToStr), - // => CanBeReplacedBy(NumIsFinite), - Symbol::NUM_ATAN => CanBeReplacedBy(NumAtan), - Symbol::NUM_ACOS => CanBeReplacedBy(NumAcos), - Symbol::NUM_ASIN => CanBeReplacedBy(NumAsin), - Symbol::NUM_BYTES_TO_U16 => WrapperIsRequired, - Symbol::NUM_BYTES_TO_U32 => WrapperIsRequired, - Symbol::NUM_BITWISE_AND => CanBeReplacedBy(NumBitwiseAnd), - Symbol::NUM_BITWISE_XOR => CanBeReplacedBy(NumBitwiseXor), - Symbol::NUM_BITWISE_OR => CanBeReplacedBy(NumBitwiseOr), - Symbol::NUM_SHIFT_LEFT => CanBeReplacedBy(NumShiftLeftBy), - Symbol::NUM_SHIFT_RIGHT => CanBeReplacedBy(NumShiftRightBy), - Symbol::NUM_SHIFT_RIGHT_ZERO_FILL => CanBeReplacedBy(NumShiftRightZfBy), - Symbol::NUM_INT_CAST => CanBeReplacedBy(NumIntCast), - Symbol::BOOL_EQ => CanBeReplacedBy(Eq), - Symbol::BOOL_NEQ => CanBeReplacedBy(NotEq), - Symbol::BOOL_AND => CanBeReplacedBy(And), - Symbol::BOOL_OR => CanBeReplacedBy(Or), - Symbol::BOOL_NOT => CanBeReplacedBy(Not), - // => CanBeReplacedBy(Hash), - // => CanBeReplacedBy(ExpectTrue), - _ => NotALowLevelWrapper, - } - } -} diff --git a/compiler/module/src/symbol.rs b/compiler/module/src/symbol.rs deleted file mode 100644 index 7240a30f88..0000000000 --- a/compiler/module/src/symbol.rs +++ /dev/null @@ -1,1350 +0,0 @@ -use crate::ident::{Ident, ModuleName}; -use crate::module_err::{IdentIdNotFound, ModuleIdNotFound, ModuleResult}; -use roc_collections::{default_hasher, MutMap, SendMap, SmallStringInterner, VecMap}; -use roc_ident::IdentStr; -use roc_region::all::Region; -use snafu::OptionExt; -use std::collections::HashMap; -use std::num::NonZeroU32; -use std::{fmt, u32}; - -// the packed(4) is needed for faster equality comparisons. With it, the structure is -// treated as a single u64, and comparison is one instruction -// -// example::eq_sym64: -// cmp rdi, rsi -// sete al -// ret -// -// while without it we get 2 extra instructions -// -// example::eq_sym64: -// xor edi, edx -// xor esi, ecx -// or esi, edi -// sete al -// ret -// -// #[repr(packed)] gives you #[repr(packed(1))], and then all your reads are unaligned -// so we set the alignment to (the natural) 4 -#[derive(Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] -#[repr(packed(4))] -pub struct Symbol { - ident_id: u32, - module_id: NonZeroU32, -} - -/// An Option will use the 0 that is not used by the NonZeroU32 module_id field to encode -/// the Nothing case. An Option hence takes no more space than a Symbol. -#[allow(dead_code)] -const SYMBOL_HAS_NICHE: () = - assert!(std::mem::size_of::() == std::mem::size_of::>()); - -// When this is `true` (which it normally should be), Symbol's Debug::fmt implementation -// attempts to pretty print debug symbols using interns recorded using -// register_debug_idents calls (which should be made in debug mode). -// Set it to false if you want to see the raw ModuleId and IdentId ints, -// but please set it back to true before checking in the result! -#[cfg(debug_assertions)] -const PRETTY_PRINT_DEBUG_SYMBOLS: bool = true; - -pub const BUILTIN_ABILITIES: &[Symbol] = &[Symbol::ENCODE_ENCODING]; - -/// In Debug builds only, Symbol has a name() method that lets -/// you look up its name in a global intern table. This table is -/// behind a mutex, so it is neither populated nor available in release builds. -impl Symbol { - // NOTE: the define_builtins! macro adds a bunch of constants to this impl, - // - // e.g. pub const NUM_NUM: Symbol = … - - pub const fn new(module_id: ModuleId, ident_id: IdentId) -> Symbol { - // The bit layout of the inside of a Symbol is: - // - // |------ 32 bits -----|------ 32 bits -----| - // | ident_id | module_id | - // |--------------------|--------------------| - // - // module_id comes second because we need to query it more often, - // and this way we can get it by truncating the u64 to u32, - // whereas accessing the first slot requires a bit shift first. - - Self { - module_id: module_id.0, - ident_id: ident_id.0, - } - } - - pub const fn module_id(self) -> ModuleId { - ModuleId(self.module_id) - } - - pub const fn ident_id(self) -> IdentId { - IdentId(self.ident_id) - } - - pub const fn is_builtin(self) -> bool { - self.module_id().is_builtin() - } - - pub fn is_builtin_ability(self) -> bool { - BUILTIN_ABILITIES.contains(&self) - } - - pub fn module_string<'a>(&self, interns: &'a Interns) -> &'a ModuleName { - interns - .module_ids - .get_name(self.module_id()) - .unwrap_or_else(|| { - panic!( - "module_string could not find IdentIds for module {:?} in {:?}", - self.module_id(), - interns - ) - }) - } - - pub fn as_str(self, interns: &Interns) -> &str { - let ident_ids = interns - .all_ident_ids - .get(&self.module_id()) - .unwrap_or_else(|| { - panic!( - "ident_string could not find IdentIds for module {:?} in {:?}", - self.module_id(), - interns - ) - }); - - ident_ids.get_name(self.ident_id()).unwrap_or_else(|| { - panic!( - "ident_string's IdentIds did not contain an entry for {} in module {:?}", - self.ident_id().0, - self.module_id() - ) - }) - } - - pub const fn as_u64(self) -> u64 { - u64::from_ne_bytes(self.to_ne_bytes()) - } - - pub fn fully_qualified(self, interns: &Interns, home: ModuleId) -> ModuleName { - let module_id = self.module_id(); - - if module_id == home { - ModuleName::from(self.as_str(interns)) - } else { - // TODO do this without format! to avoid allocation for short strings - format!( - "{}.{}", - self.module_string(interns).as_str(), - self.as_str(interns) - ) - .into() - } - } - - pub const fn to_ne_bytes(self) -> [u8; 8] { - unsafe { std::mem::transmute(self) } - } - - #[cfg(debug_assertions)] - pub fn contains(self, needle: &str) -> bool { - format!("{:?}", self).contains(needle) - } -} - -/// Rather than displaying as this: -/// -/// Symbol("Foo.bar") -/// -/// ...instead display as this: -/// -/// `Foo.bar` -impl fmt::Debug for Symbol { - #[cfg(debug_assertions)] - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - if PRETTY_PRINT_DEBUG_SYMBOLS { - let module_id = self.module_id(); - let ident_id = self.ident_id(); - - match DEBUG_IDENT_IDS_BY_MODULE_ID.lock() { - Ok(names) => match &names.get(&(module_id.to_zero_indexed() as u32)) { - Some(ident_ids) => match ident_ids.get_name(ident_id) { - Some(ident_str) => write!(f, "`{:?}.{}`", module_id, ident_str), - None => fallback_debug_fmt(*self, f), - }, - None => fallback_debug_fmt(*self, f), - }, - Err(err) => { - // Print and return Err rather than panicking, because this - // might be used in a panic error message, and if we panick - // while we're already panicking it'll kill the process - // without printing any of the errors! - println!("DEBUG INFO: Failed to acquire lock for Debug reading from DEBUG_IDENT_IDS_BY_MODULE_ID, presumably because a thread panicked: {:?}", err); - - fallback_debug_fmt(*self, f) - } - } - } else { - fallback_debug_fmt(*self, f) - } - } - - #[cfg(not(debug_assertions))] - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - fallback_debug_fmt(*self, f) - } -} - -impl fmt::Display for Symbol { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let module_id = self.module_id(); - let ident_id = self.ident_id(); - - match ident_id { - IdentId(value) => write!(f, "{:?}.{:?}", module_id, value), - } - } -} - -impl From for u64 { - fn from(symbol: Symbol) -> Self { - symbol.as_u64() - } -} - -fn fallback_debug_fmt(symbol: Symbol, f: &mut fmt::Formatter) -> fmt::Result { - let module_id = symbol.module_id(); - let ident_id = symbol.ident_id(); - - write!(f, "`{:?}.{:?}`", module_id, ident_id) -} - -// TODO this is only here to prevent clippy from complaining about an unused -// #[macro_use] on lazy_statc in --release builds, because as of January 2020, -// we only use lazy_static in the debug configuration. If we ever start using -// lazy_static in release builds, this do-nothing macro invocation will be safe to delete! -// -// There's probably also a way to get clippy to stop complaining about the unused -// #[macro_use] but it didn't seem worth the effort since probably someday we'll -// end up using it in release builds anyway. Right? ...Right? -lazy_static! {} - -#[cfg(debug_assertions)] -lazy_static! { - /// This is used in Debug builds only, to let us have a Debug instance - /// which displays not only the Module ID, but also the Module Name which - /// corresponds to that ID. - /// - static ref DEBUG_MODULE_ID_NAMES: std::sync::Mutex>> = - // This stores a u32 key instead of a ModuleId key so that if there's - // a problem with ModuleId's Debug implementation, logging this for diagnostic - // purposes won't recursively trigger ModuleId's Debug instance in the course of printing - // this out. - std::sync::Mutex::new(roc_collections::all::MutMap::default()); -} - -#[derive(Debug, Default)] -pub struct Interns { - pub module_ids: ModuleIds, - pub all_ident_ids: IdentIdsByModule, -} - -impl Interns { - pub fn module_id(&mut self, name: &ModuleName) -> ModuleId { - self.module_ids.get_or_insert(name) - } - - pub fn module_name(&self, module_id: ModuleId) -> &ModuleName { - self.module_ids.get_name(module_id).unwrap_or_else(|| { - panic!( - "Unable to find interns entry for module_id {:?} in Interns {:?}", - module_id, self - ) - }) - } - - pub fn symbol(&self, module_id: ModuleId, ident: IdentStr) -> Symbol { - let ident: Ident = ident.into(); - - match self.all_ident_ids.get(&module_id) { - Some(ident_ids) => match ident_ids.get_id(&ident) { - Some(ident_id) => Symbol::new(module_id, ident_id), - None => { - panic!("Interns::symbol could not find ident entry for {:?} for module {:?} in Interns {:?}", ident, module_id, self); - } - }, - None => { - panic!( - "Interns::symbol could not find entry for module {:?} in Interns {:?}", - module_id, self - ); - } - } - } - - pub fn from_index(module_id: ModuleId, ident_id: u32) -> Symbol { - Symbol::new(module_id, IdentId(ident_id)) - } -} - -pub fn get_module_ident_ids<'a>( - all_ident_ids: &'a IdentIdsByModule, - module_id: &ModuleId, -) -> ModuleResult<&'a IdentIds> { - all_ident_ids - .get(module_id) - .with_context(|| ModuleIdNotFound { - module_id: format!("{:?}", module_id), - all_ident_ids: format!("{:?}", all_ident_ids), - }) -} - -pub fn get_module_ident_ids_mut<'a>( - all_ident_ids: &'a mut IdentIdsByModule, - module_id: &ModuleId, -) -> ModuleResult<&'a mut IdentIds> { - all_ident_ids - .get_mut(module_id) - .with_context(|| ModuleIdNotFound { - module_id: format!("{:?}", module_id), - all_ident_ids: "I could not return all_ident_ids here because of borrowing issues.", - }) -} - -#[cfg(debug_assertions)] -lazy_static! { - /// This is used in Debug builds only, to let us have a Debug instance - /// which displays not only the Module ID, but also the Module Name which - /// corresponds to that ID. - static ref DEBUG_IDENT_IDS_BY_MODULE_ID: std::sync::Mutex> = - // This stores a u32 key instead of a ModuleId key so that if there's - // a problem with ModuleId's Debug implementation, logging this for diagnostic - // purposes won't recursively trigger ModuleId's Debug instance in the course of printing - // this out. - std::sync::Mutex::new(roc_collections::all::MutMap::default()); -} - -/// A globally unique ID that gets assigned to each module as it is loaded. -#[derive(Copy, Clone, PartialEq, Eq, Hash)] -pub struct ModuleId(NonZeroU32); - -impl ModuleId { - // NOTE: the define_builtins! macro adds a bunch of constants to this impl, - // - // e.g. pub const NUM: ModuleId = … - - const fn from_zero_indexed(mut id: usize) -> Self { - id += 1; - - // only happens on overflow - debug_assert!(id != 0); - - ModuleId(unsafe { NonZeroU32::new_unchecked(id as u32) }) - } - - const fn to_zero_indexed(self) -> usize { - (self.0.get() - 1) as usize - } - - #[cfg(debug_assertions)] - pub fn register_debug_idents(self, ident_ids: &IdentIds) { - let mut all = DEBUG_IDENT_IDS_BY_MODULE_ID.lock().expect("Failed to acquire lock for Debug interning into DEBUG_MODULE_ID_NAMES, presumably because a thread panicked."); - - all.insert(self.to_zero_indexed() as u32, ident_ids.clone()); - } - - #[cfg(not(debug_assertions))] - pub fn register_debug_idents(self, _ident_ids: &IdentIds) { - // This is a no-op that should get DCE'd - } - - pub fn to_ident_str(self, interns: &Interns) -> &ModuleName { - interns - .module_ids - .get_name(self) - .unwrap_or_else(|| panic!("Could not find ModuleIds for {:?}", self)) - } -} - -impl fmt::Debug for ModuleId { - /// In debug builds, whenever we create a new ModuleId, we record is name in - /// a global interning table so that Debug can look it up later. That table - /// needs a global mutex, so we don't do this in release builds. This means - /// the Debug impl in release builds only shows the number, not the name (which - /// it does not have available, due to having never stored it in the mutexed intern table.) - #[cfg(debug_assertions)] - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - // Originally, this printed both name and numeric ID, but the numeric ID - // didn't seem to add anything useful. Feel free to temporarily re-add it - // if it's helpful in debugging! - let names = - DEBUG_MODULE_ID_NAMES - .lock() - .expect("Failed to acquire lock for Debug reading from DEBUG_MODULE_ID_NAMES, presumably because a thread panicked."); - - if PRETTY_PRINT_DEBUG_SYMBOLS { - match names.get(&(self.to_zero_indexed() as u32)) { - Some(str_ref) => write!(f, "{}", str_ref.clone()), - None => { - panic!( - "Could not find a Debug name for module ID {} in {:?}", - self.0, names, - ); - } - } - } else { - write!(f, "{}", self.0) - } - } - - /// In release builds, all we have access to is the number, so only display that. - #[cfg(not(debug_assertions))] - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - self.0.fmt(f) - } -} - -/// pf.Task -/// 1. build mapping from short name to package -/// 2. when adding new modules from package we need to register them in some other map (this module id goes with short name) (shortname, module-name) -> moduleId -/// 3. pass this around to other modules getting headers parsed. when parsing interfaces we need to use this map to reference shortnames -/// 4. throw away short names. stash the module id in the can env under the resolved module name -/// 5. test: - -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub enum PackageQualified<'a, T> { - Unqualified(T), - Qualified(&'a str, T), -} - -/// Package-qualified module name -pub type PQModuleName<'a> = PackageQualified<'a, ModuleName>; - -impl<'a, T> PackageQualified<'a, T> { - pub fn as_inner(&self) -> &T { - match self { - PackageQualified::Unqualified(name) => name, - PackageQualified::Qualified(_, name) => name, - } - } -} - -#[derive(Debug, Clone)] -pub struct PackageModuleIds<'a> { - by_name: MutMap, ModuleId>, - by_id: Vec>, -} - -impl<'a> PackageModuleIds<'a> { - pub fn get_or_insert(&mut self, module_name: &PQModuleName<'a>) -> ModuleId { - match self.by_name.get(module_name) { - Some(id) => *id, - None => { - let by_id = &mut self.by_id; - let module_id = ModuleId::from_zero_indexed(by_id.len()); - - by_id.push(module_name.clone()); - - self.by_name.insert(module_name.clone(), module_id); - - if cfg!(debug_assertions) { - Self::insert_debug_name(module_id, module_name); - } - - module_id - } - } - } - - pub fn into_module_ids(self) -> ModuleIds { - let by_name: MutMap = self - .by_name - .into_iter() - .map(|(pqname, module_id)| (pqname.as_inner().clone(), module_id)) - .collect(); - - let by_id: Vec = self - .by_id - .into_iter() - .map(|pqname| pqname.as_inner().clone()) - .collect(); - - ModuleIds { by_name, by_id } - } - - #[cfg(debug_assertions)] - fn insert_debug_name(module_id: ModuleId, module_name: &PQModuleName) { - let mut names = DEBUG_MODULE_ID_NAMES.lock().expect("Failed to acquire lock for Debug interning into DEBUG_MODULE_ID_NAMES, presumably because a thread panicked."); - - names - .entry(module_id.to_zero_indexed() as u32) - .or_insert_with(|| match module_name { - PQModuleName::Unqualified(module) => module.as_str().into(), - PQModuleName::Qualified(package, module) => { - let name = format!("{}.{}", package, module.as_str()).into(); - name - } - }); - } - - #[cfg(not(debug_assertions))] - fn insert_debug_name(_module_id: ModuleId, _module_name: &PQModuleName) { - // By design, this is a no-op in release builds! - } - - pub fn get_id(&self, module_name: &PQModuleName<'a>) -> Option<&ModuleId> { - self.by_name.get(module_name) - } - - pub fn get_name(&self, id: ModuleId) -> Option<&PQModuleName> { - self.by_id.get(id.to_zero_indexed()) - } - - pub fn available_modules(&self) -> impl Iterator { - self.by_id.iter() - } -} - -/// Stores a mapping between ModuleId and InlinableString. -/// -/// Each module name is stored twice, for faster lookups. -/// Since these are interned strings, this shouldn't result in many total allocations in practice. -#[derive(Debug, Clone)] -pub struct ModuleIds { - by_name: MutMap, - /// Each ModuleId is an index into this Vec - by_id: Vec, -} - -impl ModuleIds { - pub fn get_or_insert(&mut self, module_name: &ModuleName) -> ModuleId { - match self.by_name.get(module_name) { - Some(id) => *id, - None => { - let by_id = &mut self.by_id; - let module_id = ModuleId::from_zero_indexed(by_id.len()); - - by_id.push(module_name.clone()); - - self.by_name.insert(module_name.clone(), module_id); - - if cfg!(debug_assertions) { - Self::insert_debug_name(module_id, module_name); - } - - module_id - } - } - } - - #[cfg(debug_assertions)] - fn insert_debug_name(module_id: ModuleId, module_name: &ModuleName) { - let mut names = DEBUG_MODULE_ID_NAMES.lock().expect("Failed to acquire lock for Debug interning into DEBUG_MODULE_ID_NAMES, presumably because a thread panicked."); - - // TODO make sure modules are never added more than once! - names - .entry(module_id.to_zero_indexed() as u32) - .or_insert_with(|| module_name.as_str().to_string().into()); - } - - #[cfg(not(debug_assertions))] - fn insert_debug_name(_module_id: ModuleId, _module_name: &ModuleName) { - // By design, this is a no-op in release builds! - } - - pub fn get_id(&self, module_name: &ModuleName) -> Option<&ModuleId> { - self.by_name.get(module_name) - } - - pub fn get_name(&self, id: ModuleId) -> Option<&ModuleName> { - self.by_id.get(id.to_zero_indexed()) - } - - pub fn available_modules(&self) -> impl Iterator { - self.by_id.iter() - } -} - -/// An ID that is assigned to interned string identifiers within a module. -/// By turning these strings into numbers, post-canonicalization processes -/// like unification and optimization can run a lot faster. -/// -/// This ID is unique within a given module, not globally - so to turn this back into -/// a string, you would need a ModuleId, an IdentId, and a Map>. -#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] -pub struct IdentId(u32); - -impl IdentId { - pub const fn index(self) -> usize { - self.0 as usize - } -} - -/// Stores a mapping between Ident and IdentId. -#[derive(Clone, Debug, Default, PartialEq, Eq)] -pub struct IdentIds { - pub interner: SmallStringInterner, -} - -impl IdentIds { - pub fn ident_strs(&self) -> impl Iterator { - self.interner - .iter() - .enumerate() - .map(|(index, ident)| (IdentId(index as u32), ident)) - } - - pub fn add_ident(&mut self, ident_name: &Ident) -> IdentId { - self.add_str(ident_name.as_str()) - } - - pub fn add_str(&mut self, ident_name: &str) -> IdentId { - IdentId(self.interner.insert(ident_name) as u32) - } - - pub fn duplicate_ident(&mut self, ident_id: IdentId) -> IdentId { - IdentId(self.interner.duplicate(ident_id.0 as usize) as u32) - } - - pub fn get_or_insert(&mut self, name: &Ident) -> IdentId { - match self.get_id(name) { - Some(id) => id, - None => self.add_str(name.as_str()), - } - } - - // necessary when the name of a value is changed in the editor - // TODO fix when same ident_name is present multiple times, see issue #2548 - pub fn update_key(&mut self, old_name: &str, new_name: &str) -> Result { - match self.interner.find_and_update(old_name, new_name) { - Some(index) => Ok(IdentId(index as u32)), - None => Err(format!("The identifier {:?} is not in IdentIds", old_name)), - } - } - - /// Generates a unique, new name that's just a strigified integer - /// (e.g. "1" or "5"), using an internal counter. Since valid Roc variable - /// names cannot begin with a number, this has no chance of colliding - /// with actual user-defined variables. - /// - /// This is used, for example, during canonicalization of an Expr::Closure - /// to generate a unique symbol to refer to that closure. - pub fn gen_unique(&mut self) -> IdentId { - IdentId(self.interner.insert_index_str() as u32) - } - - #[inline(always)] - pub fn get_id(&self, ident_name: &Ident) -> Option { - self.interner - .find_index(ident_name.as_str()) - .map(|i| IdentId(i as u32)) - } - - #[inline(always)] - pub fn get_id_many<'a>(&'a self, ident_name: &'a str) -> impl Iterator + 'a { - self.interner - .find_indices(ident_name) - .map(|i| IdentId(i as u32)) - } - - pub fn get_name(&self, id: IdentId) -> Option<&str> { - self.interner.try_get(id.0 as usize) - } - - pub fn get_name_str_res(&self, ident_id: IdentId) -> ModuleResult<&str> { - self.get_name(ident_id).with_context(|| IdentIdNotFound { - ident_id, - ident_ids_str: format!("{:?}", self), - }) - } - - pub fn len(&self) -> usize { - self.interner.len() - } - - pub fn is_empty(&self) -> bool { - self.interner.is_empty() - } -} - -#[derive(Debug, Default)] -pub struct IdentIdsByModule(VecMap); - -impl IdentIdsByModule { - pub fn get_or_insert(&mut self, module_id: ModuleId) -> &mut IdentIds { - self.0.get_or_insert(module_id, IdentIds::default) - } - - pub fn get_mut(&mut self, key: &ModuleId) -> Option<&mut IdentIds> { - self.0.get_mut(key) - } - - pub fn get(&self, key: &ModuleId) -> Option<&IdentIds> { - self.0.get(key) - } - - pub fn insert(&mut self, key: ModuleId, value: IdentIds) -> Option { - self.0.insert(key, value) - } - - pub fn keys(&self) -> impl Iterator { - self.0.keys() - } - - pub fn len(&self) -> usize { - self.0.len() - } - - pub fn is_empty(&self) -> bool { - self.0.is_empty() - } -} - -// BUILTINS - -const fn offset_helper(mut array: [u32; N]) -> [u32; N] { - let mut sum = 0u32; - - let mut i = 0; - while i < N { - // In rust 1.60 change to: (array[i], sum) = (sum, sum + array[i]); - let temp = array[i]; - array[i] = sum; - sum += temp; - - i += 1; - } - - array -} - -const fn byte_slice_equality(a: &[u8], b: &[u8]) -> bool { - if a.len() != b.len() { - return false; - } - - let mut i = 0; - while i < a.len() { - if a[i] != b[i] { - return false; - } - - i += 1; - } - - true -} - -const fn find_duplicates(array: [&str; N]) -> Option<(usize, usize)> { - let mut i = 0; - while i < N { - let needle = array[i]; - let mut j = i + 1; - while j < N { - if byte_slice_equality(needle.as_bytes(), array[j].as_bytes()) { - return Some((i, j)); - } - - j += 1; - } - - i += 1; - } - - None -} - -const fn check_indices(array: [u32; N]) -> Option<(u32, usize)> { - let mut i = 0; - while i < N { - if array[i] as usize != i { - return Some((array[i], i)); - } - - i += 1; - } - - None -} - -macro_rules! define_builtins { - { - $( - $module_id:literal $module_const:ident: $module_name:literal => { - $( - $ident_id:literal $ident_const:ident: $ident_name:literal $($imported:ident)? - )+ - } - )+ - num_modules: $total:literal - } => { - impl IdentIds { - pub fn exposed_builtins(extra_capacity: usize) -> IdentIdsByModule { - let mut exposed_idents_by_module = VecMap::with_capacity(extra_capacity + $total); - - $( - let module_id = ModuleId::$module_const; - debug_assert!(!exposed_idents_by_module.contains_key(&module_id), r"Error setting up Builtins: when setting up module {} {:?} - the module ID {} is already present in the map. Check the map for duplicate module IDs!", $module_id, $module_name, $module_id); - - let ident_ids = { - const TOTAL : usize = [ $($ident_name),+ ].len(); - const NAMES : [ &str; TOTAL] = [ $($ident_name),+ ]; - const LENGTHS: [ u16; TOTAL] = [ $($ident_name.len() as u16),+ ]; - const OFFSETS: [ u32; TOTAL] = offset_helper([ $($ident_name.len() as u32),+ ]); - const BUFFER: &str = concat!($($ident_name),+); - - const LENGTH_CHECK: Option<(u32, usize)> = check_indices([ $($ident_id),+ ]); - const DUPLICATE_CHECK: Option<(usize, usize)> = find_duplicates(NAMES); - - if cfg!(debug_assertions) { - match LENGTH_CHECK { - None => (), - Some((given, expected)) => panic!( - "Symbol {} : {} should have index {} based on the insertion order, try {} : {} instead", - given, NAMES[expected], expected, expected, NAMES[expected], - ), - } - }; - - if cfg!(debug_assertions) { - match DUPLICATE_CHECK { - None => (), - Some((first, second)) => panic!( - "Symbol {} : {} is duplicated at position {}, try removing the duplicate", - first, NAMES[first], second - ), - } - }; - - // Safety: all lengths are non-negative and smaller than 2^15 - let interner = unsafe { - SmallStringInterner::from_parts ( - BUFFER.as_bytes().to_vec(), - LENGTHS.to_vec(), - OFFSETS.to_vec(), - )}; - - IdentIds{ interner } - }; - - if cfg!(debug_assertions) { - let name = PQModuleName::Unqualified($module_name.into()); - PackageModuleIds::insert_debug_name(module_id, &name); - module_id.register_debug_idents(&ident_ids); - } - - - exposed_idents_by_module.insert( - module_id, - ident_ids - ); - )+ - - debug_assert!(exposed_idents_by_module.len() == $total, "Error setting up Builtins: `total:` is set to the wrong amount. It was set to {} but {} modules were set up.", $total, exposed_idents_by_module.len()); - - IdentIdsByModule(exposed_idents_by_module) - } - } - - impl ModuleId { - pub const fn is_builtin(self) -> bool { - // This is a builtin ModuleId iff it's below the - // total number of builtin modules, since they - // take up the first $total ModuleId numbers. - self.to_zero_indexed() < $total - } - - $( - pub const $module_const: ModuleId = ModuleId::from_zero_indexed($module_id); - )+ - } - - impl Default for ModuleIds { - fn default() -> Self { - // +1 because the user will be compiling at least 1 non-builtin module! - let capacity = $total + 1; - - let mut by_name = HashMap::with_capacity_and_hasher(capacity, default_hasher()); - let mut by_id = Vec::with_capacity(capacity); - - let mut insert_both = |id: ModuleId, name_str: &'static str| { - let name: ModuleName = name_str.into(); - - if cfg!(debug_assertions) { - Self::insert_debug_name(id, &name); - } - - by_name.insert(name.clone(), id); - by_id.push(name); - }; - - $( - insert_both(ModuleId::$module_const, $module_name); - )+ - - ModuleIds { by_name, by_id } - } - } - - impl<'a> Default for PackageModuleIds<'a> { - fn default() -> Self { - // +1 because the user will be compiling at least 1 non-builtin module! - let capacity = $total + 1; - - let mut by_name = HashMap::with_capacity_and_hasher(capacity, default_hasher()); - let mut by_id = Vec::with_capacity(capacity); - - let mut insert_both = |id: ModuleId, name_str: &'static str| { - let raw_name: IdentStr = name_str.into(); - let name = PQModuleName::Unqualified(raw_name.into()); - - if cfg!(debug_assertions) { - Self::insert_debug_name(id, &name); - } - - by_name.insert(name.clone(), id); - by_id.push(name); - }; - - $( - insert_both(ModuleId::$module_const, $module_name); - )+ - - PackageModuleIds { by_name, by_id } - } - } - - impl Symbol { - $( - $( - pub const $ident_const: Symbol = Symbol::new(ModuleId::$module_const, IdentId($ident_id)); - )+ - )+ - - /// The default idents that should be in scope, - /// and what symbols they should resolve to. - /// - /// This is for type aliases like `Int` and `Str` and such. - pub fn default_in_scope() -> SendMap { - let mut scope = SendMap::default(); - - $( - $( - $( - // TODO is there a cleaner way to do this? - // The goal is to make sure that we only - // actually import things into scope if - // they are tagged as "imported" in define_builtins! - let $imported = true; - - if $imported { - scope.insert($ident_name.into(), (Symbol::new(ModuleId::$module_const, IdentId($ident_id)), Region::zero())); - } - )? - )+ - )+ - - scope - } - } - }; -} - -// NOTE: Some of these builtins have a # in their names. -// This is because they are for compiler use only, and should not cause -// namespace conflicts with userspace! -define_builtins! { - 0 ATTR: "#Attr" => { - 0 UNDERSCORE: "_" // the _ used in pattern matches. This is Symbol 0. - 1 ATTR_ATTR: "Attr" // the #Attr.Attr type alias, used in uniqueness types. - 2 ARG_1: "#arg1" - 3 ARG_2: "#arg2" - 4 ARG_3: "#arg3" - 5 ARG_4: "#arg4" - 6 ARG_5: "#arg5" - 7 ARG_6: "#arg6" - 8 ARG_7: "#arg7" - 9 ARG_8: "#arg8" - 10 INC: "#inc" // internal function that increments the refcount - 11 DEC: "#dec" // internal function that increments the refcount - 12 ARG_CLOSURE: "#arg_closure" // symbol used to store the closure record - 13 LIST_EQ: "#list_eq" // internal function that checks list equality - - 14 GENERIC_HASH: "#generic_hash" // hash of arbitrary layouts - 15 GENERIC_HASH_REF: "#generic_hash_by_ref" // hash of arbitrary layouts, passed as an opaque pointer - - 16 GENERIC_EQ_REF: "#generic_eq_by_ref" // equality of arbitrary layouts, passed as an opaque pointer - 17 GENERIC_RC_REF: "#generic_rc_by_ref" // refcount of arbitrary layouts, passed as an opaque pointer - - 18 GENERIC_EQ: "#generic_eq" // internal function that checks generic equality - - // a user-defined function that we need to capture in a closure - // see e.g. Set.walk - 19 USER_FUNCTION: "#user_function" - - // A caller (wrapper) that we pass to zig for it to be able to call Roc functions - 20 ZIG_FUNCTION_CALLER: "#zig_function_caller" - - // a caller (wrapper) for comparison - 21 GENERIC_COMPARE_REF: "#generic_compare_ref" - - // used to initialize parameters in borrow.rs - 22 EMPTY_PARAM: "#empty_param" - - // used by the dev backend to store the pointer to where to store large return types - 23 RET_POINTER: "#ret_pointer" - - // used in wasm dev backend to mark temporary values in the VM stack - 24 WASM_TMP: "#wasm_tmp" - - // the _ used in mono when a specialized symbol is deleted - 25 REMOVED_SPECIALIZATION: "#removed_specialization" - - // used in dev backend - 26 DEV_TMP: "#dev_tmp" - 27 DEV_TMP2: "#dev_tmp2" - 28 DEV_TMP3: "#dev_tmp3" - 29 DEV_TMP4: "#dev_tmp4" - 30 DEV_TMP5: "#dev_tmp5" - } - 1 NUM: "Num" => { - 0 NUM_NUM: "Num" // the Num.Num type alias - 1 NUM_I128: "I128" // the Num.I128 type alias - 2 NUM_U128: "U128" // the Num.U128 type alias - 3 NUM_I64: "I64" // the Num.I64 type alias - 4 NUM_U64: "U64" // the Num.U64 type alias - 5 NUM_I32: "I32" // the Num.I32 type alias - 6 NUM_U32: "U32" // the Num.U32 type alias - 7 NUM_I16: "I16" // the Num.I16 type alias - 8 NUM_U16: "U16" // the Num.U16 type alias - 9 NUM_I8: "I8" // the Num.I8 type alias - 10 NUM_U8: "U8" // the Num.U8 type alias - 11 NUM_INTEGER: "Integer" // Int : Num Integer - 12 NUM_F64: "F64" // the Num.F64 type alias - 13 NUM_F32: "F32" // the Num.F32 type alias - 14 NUM_FLOATINGPOINT: "FloatingPoint" // Float : Num FloatingPoint - 15 NUM_MAX_F32: "maxF32" - 16 NUM_MIN_F32: "minF32" - 17 NUM_ABS: "abs" - 18 NUM_NEG: "neg" - 19 NUM_ADD: "add" - 20 NUM_SUB: "sub" - 21 NUM_MUL: "mul" - 22 NUM_LT: "isLt" - 23 NUM_LTE: "isLte" - 24 NUM_GT: "isGt" - 25 NUM_GTE: "isGte" - 26 NUM_TO_FRAC: "toFrac" - 27 NUM_SIN: "sin" - 28 NUM_COS: "cos" - 29 NUM_TAN: "tan" - 30 NUM_IS_ZERO: "isZero" - 31 NUM_IS_EVEN: "isEven" - 32 NUM_IS_ODD: "isOdd" - 33 NUM_IS_POSITIVE: "isPositive" - 34 NUM_IS_NEGATIVE: "isNegative" - 35 NUM_REM: "rem" - 36 NUM_REM_CHECKED: "remChecked" - 37 NUM_DIV_FRAC: "div" - 38 NUM_DIV_FRAC_CHECKED: "divChecked" - 39 NUM_DIV_TRUNC: "divTrunc" - 40 NUM_DIV_TRUNC_CHECKED: "divTruncChecked" - 41 NUM_SQRT: "sqrt" - 42 NUM_SQRT_CHECKED: "sqrtChecked" - 43 NUM_LOG: "log" - 44 NUM_LOG_CHECKED: "logChecked" - 45 NUM_ROUND: "round" - 46 NUM_COMPARE: "compare" - 47 NUM_POW: "pow" - 48 NUM_CEILING: "ceiling" - 49 NUM_POW_INT: "powInt" - 50 NUM_FLOOR: "floor" - 51 NUM_ADD_WRAP: "addWrap" - 52 NUM_ADD_CHECKED: "addChecked" - 53 NUM_ADD_SATURATED: "addSaturated" - 54 NUM_ATAN: "atan" - 55 NUM_ACOS: "acos" - 56 NUM_ASIN: "asin" - 57 NUM_SIGNED128: "Signed128" - 58 NUM_SIGNED64: "Signed64" - 59 NUM_SIGNED32: "Signed32" - 60 NUM_SIGNED16: "Signed16" - 61 NUM_SIGNED8: "Signed8" - 62 NUM_UNSIGNED128: "Unsigned128" - 63 NUM_UNSIGNED64: "Unsigned64" - 64 NUM_UNSIGNED32: "Unsigned32" - 65 NUM_UNSIGNED16: "Unsigned16" - 66 NUM_UNSIGNED8: "Unsigned8" - 67 NUM_BINARY64: "Binary64" - 68 NUM_BINARY32: "Binary32" - 69 NUM_BITWISE_AND: "bitwiseAnd" - 70 NUM_BITWISE_XOR: "bitwiseXor" - 71 NUM_BITWISE_OR: "bitwiseOr" - 72 NUM_SHIFT_LEFT: "shiftLeftBy" - 73 NUM_SHIFT_RIGHT: "shiftRightBy" - 74 NUM_SHIFT_RIGHT_ZERO_FILL: "shiftRightZfBy" - 75 NUM_SUB_WRAP: "subWrap" - 76 NUM_SUB_CHECKED: "subChecked" - 77 NUM_SUB_SATURATED: "subSaturated" - 78 NUM_MUL_WRAP: "mulWrap" - 79 NUM_MUL_CHECKED: "mulChecked" - 80 NUM_INT: "Int" - 81 NUM_FRAC: "Frac" - 82 NUM_NATURAL: "Natural" - 83 NUM_NAT: "Nat" - 84 NUM_INT_CAST: "intCast" - 85 NUM_IS_MULTIPLE_OF: "isMultipleOf" - 86 NUM_DECIMAL: "Decimal" - 87 NUM_DEC: "Dec" // the Num.Dectype alias - 88 NUM_BYTES_TO_U16: "bytesToU16" - 89 NUM_BYTES_TO_U32: "bytesToU32" - 90 NUM_CAST_TO_NAT: "#castToNat" - 91 NUM_DIV_CEIL: "divCeil" - 92 NUM_DIV_CEIL_CHECKED: "divCeilChecked" - 93 NUM_TO_STR: "toStr" - 94 NUM_MIN_I8: "minI8" - 95 NUM_MAX_I8: "maxI8" - 96 NUM_MIN_U8: "minU8" - 97 NUM_MAX_U8: "maxU8" - 98 NUM_MIN_I16: "minI16" - 99 NUM_MAX_I16: "maxI16" - 100 NUM_MIN_U16: "minU16" - 101 NUM_MAX_U16: "maxU16" - 102 NUM_MIN_I32: "minI32" - 103 NUM_MAX_I32: "maxI32" - 104 NUM_MIN_U32: "minU32" - 105 NUM_MAX_U32: "maxU32" - 106 NUM_MIN_I64: "minI64" - 107 NUM_MAX_I64: "maxI64" - 108 NUM_MIN_U64: "minU64" - 109 NUM_MAX_U64: "maxU64" - 110 NUM_MIN_I128: "minI128" - 111 NUM_MAX_I128: "maxI128" - 112 NUM_TO_I8: "toI8" - 113 NUM_TO_I8_CHECKED: "toI8Checked" - 114 NUM_TO_I16: "toI16" - 115 NUM_TO_I16_CHECKED: "toI16Checked" - 116 NUM_TO_I32: "toI32" - 117 NUM_TO_I32_CHECKED: "toI32Checked" - 118 NUM_TO_I64: "toI64" - 119 NUM_TO_I64_CHECKED: "toI64Checked" - 120 NUM_TO_I128: "toI128" - 121 NUM_TO_I128_CHECKED: "toI128Checked" - 122 NUM_TO_U8: "toU8" - 123 NUM_TO_U8_CHECKED: "toU8Checked" - 124 NUM_TO_U16: "toU16" - 125 NUM_TO_U16_CHECKED: "toU16Checked" - 126 NUM_TO_U32: "toU32" - 127 NUM_TO_U32_CHECKED: "toU32Checked" - 128 NUM_TO_U64: "toU64" - 129 NUM_TO_U64_CHECKED: "toU64Checked" - 130 NUM_TO_U128: "toU128" - 131 NUM_TO_U128_CHECKED: "toU128Checked" - 132 NUM_TO_NAT: "toNat" - 133 NUM_TO_NAT_CHECKED: "toNatChecked" - 134 NUM_TO_F32: "toF32" - 135 NUM_TO_F32_CHECKED: "toF32Checked" - 136 NUM_TO_F64: "toF64" - 137 NUM_TO_F64_CHECKED: "toF64Checked" - 138 NUM_MAX_F64: "maxF64" - 139 NUM_MIN_F64: "minF64" - } - 2 BOOL: "Bool" => { - 0 BOOL_BOOL: "Bool" // the Bool.Bool type alias - 1 BOOL_FALSE: "False" imported // Bool.Bool = [False, True] - // NB: not strictly needed; used for finding tag names in error suggestions - 2 BOOL_TRUE: "True" imported // Bool.Bool = [False, True] - // NB: not strictly needed; used for finding tag names in error suggestions - 3 BOOL_AND: "and" - 4 BOOL_OR: "or" - 5 BOOL_NOT: "not" - 6 BOOL_XOR: "xor" - 7 BOOL_EQ: "isEq" - 8 BOOL_NEQ: "isNotEq" - } - 3 STR: "Str" => { - 0 STR_STR: "Str" imported // the Str.Str type alias - 1 STR_IS_EMPTY: "isEmpty" - 2 STR_APPEND: "#append" // unused - 3 STR_CONCAT: "concat" - 4 STR_JOIN_WITH: "joinWith" - 5 STR_SPLIT: "split" - 6 STR_COUNT_GRAPHEMES: "countGraphemes" - 7 STR_STARTS_WITH: "startsWith" - 8 STR_ENDS_WITH: "endsWith" - 9 STR_FROM_UTF8: "fromUtf8" - 10 STR_UT8_PROBLEM: "Utf8Problem" // the Utf8Problem type alias - 11 STR_UT8_BYTE_PROBLEM: "Utf8ByteProblem" // the Utf8ByteProblem type alias - 12 STR_TO_UTF8: "toUtf8" - 13 STR_STARTS_WITH_CODE_PT: "startsWithCodePt" - 14 STR_ALIAS_ANALYSIS_STATIC: "#aliasAnalysisStatic" // string with the static lifetime - 15 STR_FROM_UTF8_RANGE: "fromUtf8Range" - 16 STR_REPEAT: "repeat" - 17 STR_TRIM: "trim" - 18 STR_TRIM_LEFT: "trimLeft" - 19 STR_TRIM_RIGHT: "trimRight" - 20 STR_TO_DEC: "toDec" - 21 STR_TO_F64: "toF64" - 22 STR_TO_F32: "toF32" - 23 STR_TO_NAT: "toNat" - 24 STR_TO_U128: "toU128" - 25 STR_TO_I128: "toI128" - 26 STR_TO_U64: "toU64" - 27 STR_TO_I64: "toI64" - 28 STR_TO_U32: "toU32" - 29 STR_TO_I32: "toI32" - 30 STR_TO_U16: "toU16" - 31 STR_TO_I16: "toI16" - 32 STR_TO_U8: "toU8" - 33 STR_TO_I8: "toI8" - } - 4 LIST: "List" => { - 0 LIST_LIST: "List" imported // the List.List type alias - 1 LIST_IS_EMPTY: "isEmpty" - 2 LIST_GET: "get" - 3 LIST_SET: "set" - 4 LIST_APPEND: "append" - 5 LIST_MAP: "map" - 6 LIST_LEN: "len" - 7 LIST_WALK_BACKWARDS: "walkBackwards" - 8 LIST_CONCAT: "concat" - 9 LIST_FIRST: "first" - 10 LIST_SINGLE: "single" - 11 LIST_REPEAT: "repeat" - 12 LIST_REVERSE: "reverse" - 13 LIST_PREPEND: "prepend" - 14 LIST_JOIN: "join" - 15 LIST_KEEP_IF: "keepIf" - 16 LIST_CONTAINS: "contains" - 17 LIST_SUM: "sum" - 18 LIST_WALK: "walk" - 19 LIST_LAST: "last" - 20 LIST_KEEP_OKS: "keepOks" - 21 LIST_KEEP_ERRS: "keepErrs" - 22 LIST_MAP_WITH_INDEX: "mapWithIndex" - 23 LIST_MAP2: "map2" - 24 LIST_MAP3: "map3" - 25 LIST_PRODUCT: "product" - 26 LIST_WALK_UNTIL: "walkUntil" - 27 LIST_RANGE: "range" - 28 LIST_SORT_WITH: "sortWith" - 29 LIST_DROP: "drop" - 30 LIST_SWAP: "swap" - 31 LIST_DROP_AT: "dropAt" - 32 LIST_DROP_LAST: "dropLast" - 33 LIST_MIN: "min" - 34 LIST_MIN_LT: "#minlt" - 35 LIST_MAX: "max" - 36 LIST_MAX_GT: "#maxGt" - 37 LIST_MAP4: "map4" - 38 LIST_DROP_FIRST: "dropFirst" - 39 LIST_JOIN_MAP: "joinMap" - 40 LIST_JOIN_MAP_CONCAT: "#joinMapConcat" - 41 LIST_ANY: "any" - 42 LIST_TAKE_FIRST: "takeFirst" - 43 LIST_TAKE_LAST: "takeLast" - 44 LIST_FIND: "find" - 45 LIST_FIND_RESULT: "#find_result" // symbol used in the definition of List.find - 46 LIST_SUBLIST: "sublist" - 47 LIST_INTERSPERSE: "intersperse" - 48 LIST_INTERSPERSE_CLOS: "#intersperseClos" - 49 LIST_SPLIT: "split" - 50 LIST_SPLIT_CLOS: "#splitClos" - 51 LIST_ALL: "all" - 52 LIST_DROP_IF: "dropIf" - 53 LIST_DROP_IF_PREDICATE: "#dropIfPred" - 54 LIST_SORT_ASC: "sortAsc" - 55 LIST_SORT_DESC: "sortDesc" - 56 LIST_SORT_DESC_COMPARE: "#sortDescCompare" - 57 LIST_REPLACE: "replace" - 58 LIST_IS_UNIQUE: "#isUnique" - } - 5 RESULT: "Result" => { - 0 RESULT_RESULT: "Result" // the Result.Result type alias - 1 RESULT_OK: "Ok" imported // Result.Result a e = [Ok a, Err e] - // NB: not strictly needed; used for finding tag names in error suggestions - 2 RESULT_ERR: "Err" imported // Result.Result a e = [Ok a, Err e] - // NB: not strictly needed; used for finding tag names in error suggestions - 3 RESULT_MAP: "map" - 4 RESULT_MAP_ERR: "mapErr" - 5 RESULT_WITH_DEFAULT: "withDefault" - 6 RESULT_AFTER: "after" - 7 RESULT_IS_OK: "isOk" - 8 RESULT_IS_ERR: "isErr" - } - 6 DICT: "Dict" => { - 0 DICT_DICT: "Dict" imported // the Dict.Dict type alias - 1 DICT_EMPTY: "empty" - 2 DICT_SINGLE: "single" - 3 DICT_GET: "get" - 4 DICT_GET_RESULT: "#get_result" // symbol used in the definition of Dict.get - 5 DICT_WALK: "walk" - 6 DICT_INSERT: "insert" - 7 DICT_LEN: "len" - - 8 DICT_REMOVE: "remove" - 9 DICT_CONTAINS: "contains" - 10 DICT_KEYS: "keys" - 11 DICT_VALUES: "values" - - 12 DICT_UNION: "union" - 13 DICT_INTERSECTION: "intersection" - 14 DICT_DIFFERENCE: "difference" - } - 7 SET: "Set" => { - 0 SET_SET: "Set" imported // the Set.Set type alias - 1 SET_EMPTY: "empty" - 2 SET_SINGLE: "single" - 3 SET_LEN: "len" - 4 SET_INSERT: "insert" - 5 SET_REMOVE: "remove" - 6 SET_UNION: "union" - 7 SET_DIFFERENCE: "difference" - 8 SET_INTERSECTION: "intersection" - 9 SET_TO_LIST: "toList" - 10 SET_FROM_LIST: "fromList" - 11 SET_WALK: "walk" - 12 SET_WALK_USER_FUNCTION: "#walk_user_function" - 13 SET_CONTAINS: "contains" - 14 SET_TO_DICT: "toDict" - } - 8 BOX: "Box" => { - 0 BOX_BOX_TYPE: "Box" imported // the Box.Box opaque type - 1 BOX_BOX_FUNCTION: "box" // Box.box - 2 BOX_UNBOX: "unbox" - } - 9 ENCODE: "Encode" => { - 0 ENCODE_ENCODER: "Encoder" - 1 ENCODE_ENCODING: "Encoding" - 2 ENCODE_TO_ENCODER: "toEncoder" - 3 ENCODE_ENCODERFORMATTING: "EncoderFormatting" - 4 ENCODE_U8: "u8" - 5 ENCODE_U16: "u16" - 6 ENCODE_U32: "u32" - 7 ENCODE_U64: "u64" - 8 ENCODE_U128: "u128" - 9 ENCODE_I8: "i8" - 10 ENCODE_I16: "i16" - 11 ENCODE_I32: "i32" - 12 ENCODE_I64: "i64" - 13 ENCODE_I128: "i128" - 14 ENCODE_F32: "f32" - 15 ENCODE_F64: "f64" - 16 ENCODE_DEC: "dec" - 17 ENCODE_BOOL: "bool" - 18 ENCODE_STRING: "string" - 19 ENCODE_LIST: "list" - 20 ENCODE_CUSTOM: "custom" - 21 ENCODE_APPEND_WITH: "appendWith" - 22 ENCODE_APPEND: "append" - 23 ENCODE_TO_BYTES: "toBytes" - } - 10 JSON: "Json" => { - 0 JSON_JSON: "Json" - } - - num_modules: 11 // Keep this count up to date by hand! (TODO: see the mut_map! macro for how we could determine this count correctly in the macro) -} diff --git a/compiler/mono/Cargo.toml b/compiler/mono/Cargo.toml deleted file mode 100644 index 04aa87b880..0000000000 --- a/compiler/mono/Cargo.toml +++ /dev/null @@ -1,25 +0,0 @@ -[package] -name = "roc_mono" -version = "0.1.0" -authors = ["The Roc Contributors"] -license = "UPL-1.0" -edition = "2021" - -[dependencies] -roc_collections = { path = "../collections" } -roc_exhaustive = { path = "../exhaustive" } -roc_region = { path = "../region" } -roc_module = { path = "../module" } -roc_types = { path = "../types" } -roc_can = { path = "../can" } -roc_late_solve = { path = "../late_solve" } -roc_std = { path = "../../roc_std", default-features = false } -roc_problem = { path = "../problem" } -roc_builtins = { path = "../builtins" } -roc_target = { path = "../roc_target" } -roc_error_macros = {path="../../error_macros"} -roc_debug_flags = {path="../debug_flags"} -ven_pretty = { path = "../../vendor/pretty" } -bumpalo = { version = "3.8.0", features = ["collections"] } -hashbrown = { version = "0.11.2", features = [ "bumpalo" ] } -static_assertions = "1.1.0" diff --git a/compiler/mono/src/borrow.rs b/compiler/mono/src/borrow.rs deleted file mode 100644 index b9851a4b9e..0000000000 --- a/compiler/mono/src/borrow.rs +++ /dev/null @@ -1,1046 +0,0 @@ -use crate::ir::{Expr, HigherOrderLowLevel, JoinPointId, Param, Proc, ProcLayout, Stmt}; -use crate::layout::Layout; -use bumpalo::collections::Vec; -use bumpalo::Bump; -use roc_collections::all::{MutMap, MutSet}; -use roc_collections::ReferenceMatrix; -use roc_module::low_level::LowLevel; -use roc_module::symbol::Symbol; - -pub(crate) const OWNED: bool = false; -pub(crate) const BORROWED: bool = true; - -/// For reference-counted types (lists, (big) strings, recursive tags), owning a value -/// means incrementing its reference count. Hence, we prefer borrowing for these types -fn should_borrow_layout(layout: &Layout) -> bool { - layout.is_refcounted() -} - -pub fn infer_borrow<'a>( - arena: &'a Bump, - procs: &MutMap<(Symbol, ProcLayout<'a>), Proc<'a>>, -) -> ParamMap<'a> { - // intern the layouts - - let mut param_map = { - let (declaration_to_index, total_number_of_params) = DeclarationToIndex::new(arena, procs); - - ParamMap { - declaration_to_index, - join_points: MutMap::default(), - declarations: bumpalo::vec![in arena; Param::EMPTY; total_number_of_params], - } - }; - - for (key, proc) in procs { - param_map.visit_proc(arena, proc, *key); - } - - let mut env = BorrowInfState { - current_proc: Symbol::ATTR_ATTR, - param_set: MutSet::default(), - owned: MutMap::default(), - modified: false, - arena, - }; - - // next we first partition the functions into strongly connected components, then do a - // topological sort on these components, finally run the fix-point borrow analysis on each - // component (in top-sorted order, from primitives (std-lib) to main) - - let mut matrix = ReferenceMatrix::new(procs.len()); - - for (row, proc) in procs.values().enumerate() { - let mut call_info = CallInfo { - keys: Vec::new_in(arena), - }; - call_info_stmt(arena, &proc.body, &mut call_info); - - for key in call_info.keys.iter() { - // the same symbol can be in `keys` multiple times (with different layouts) - for (col, (k, _)) in procs.keys().enumerate() { - if k == key { - matrix.set_row_col(row, col, true); - } - } - } - } - - let sccs = matrix.strongly_connected_components_all(); - - for group in sccs.groups() { - // This is a fixed-point analysis - // - // all functions initiall own all their parameters - // through a series of checks and heuristics, some arguments are set to borrowed - // when that doesn't lead to conflicts the change is kept, otherwise it may be reverted - // - // when the signatures no longer change, the analysis stops and returns the signatures - loop { - for index in group.iter_ones() { - let (key, proc) = &procs.iter().nth(index).unwrap(); - let param_offset = param_map.get_param_offset(key.0, key.1); - env.collect_proc(&mut param_map, proc, param_offset); - } - - if !env.modified { - // if there were no modifications, we're done - break; - } else { - // otherwise see if there are changes after another iteration - env.modified = false; - } - } - } - - param_map -} - -#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)] -pub struct ParamOffset(usize); - -impl From for usize { - fn from(id: ParamOffset) -> Self { - id.0 as usize - } -} - -#[derive(Debug)] -struct DeclarationToIndex<'a> { - elements: Vec<'a, ((Symbol, ProcLayout<'a>), ParamOffset)>, -} - -impl<'a> DeclarationToIndex<'a> { - fn new(arena: &'a Bump, procs: &MutMap<(Symbol, ProcLayout<'a>), Proc<'a>>) -> (Self, usize) { - let mut declaration_to_index = Vec::with_capacity_in(procs.len(), arena); - - let mut i = 0; - for key in procs.keys().copied() { - declaration_to_index.push((key, ParamOffset(i))); - - i += key.1.arguments.len(); - } - - declaration_to_index.sort_unstable_by_key(|t| t.0 .0); - - ( - DeclarationToIndex { - elements: declaration_to_index, - }, - i, - ) - } - - fn get_param_offset( - &self, - needle_symbol: Symbol, - needle_layout: ProcLayout<'a>, - ) -> ParamOffset { - if let Ok(middle_index) = self - .elements - .binary_search_by_key(&needle_symbol, |t| t.0 .0) - { - // first, iterate backward until we hit a different symbol - let backward = self.elements[..middle_index].iter().rev(); - - for ((symbol, proc_layout), param_offset) in backward { - if *symbol != needle_symbol { - break; - } else if *proc_layout == needle_layout { - return *param_offset; - } - } - - // if not found, iterate forward until we find our combo - let forward = self.elements[middle_index..].iter(); - - for ((symbol, proc_layout), param_offset) in forward { - if *symbol != needle_symbol { - break; - } else if *proc_layout == needle_layout { - return *param_offset; - } - } - } - unreachable!( - "symbol/layout {:?} {:?} combo must be in DeclarationToIndex", - needle_symbol, needle_layout - ) - } -} - -#[derive(Debug)] -pub struct ParamMap<'a> { - /// Map a (Symbol, ProcLayout) pair to the starting index in the `declarations` array - declaration_to_index: DeclarationToIndex<'a>, - /// the parameters of all functions in a single flat array. - /// - /// - the map above gives the index of the first parameter for the function - /// - the length of the ProcLayout's argument field gives the total number of parameters - /// - /// These can be read by taking a slice into this array, and can also be updated in-place - declarations: Vec<'a, Param<'a>>, - join_points: MutMap]>, -} - -impl<'a> ParamMap<'a> { - pub fn get_param_offset(&self, symbol: Symbol, layout: ProcLayout<'a>) -> ParamOffset { - self.declaration_to_index.get_param_offset(symbol, layout) - } - - pub fn get_symbol(&self, symbol: Symbol, layout: ProcLayout<'a>) -> Option<&[Param<'a>]> { - // let index: usize = self.declaration_to_index[&(symbol, layout)].into(); - let index: usize = self.get_param_offset(symbol, layout).into(); - - self.declarations.get(index..index + layout.arguments.len()) - } - - pub fn get_join_point(&self, id: JoinPointId) -> &'a [Param<'a>] { - match self.join_points.get(&id) { - Some(slice) => slice, - None => unreachable!("join point not in param map: {:?}", id), - } - } - - pub fn iter_symbols(&'a self) -> impl Iterator { - self.declaration_to_index.elements.iter().map(|t| &t.0 .0) - } -} - -impl<'a> ParamMap<'a> { - fn init_borrow_params(arena: &'a Bump, ps: &'a [Param<'a>]) -> &'a [Param<'a>] { - Vec::from_iter_in( - ps.iter().map(|p| Param { - borrow: p.layout.is_refcounted(), - layout: p.layout, - symbol: p.symbol, - }), - arena, - ) - .into_bump_slice() - } - - fn init_borrow_args(arena: &'a Bump, ps: &'a [(Layout<'a>, Symbol)]) -> &'a [Param<'a>] { - Vec::from_iter_in( - ps.iter().map(|(layout, symbol)| Param { - borrow: should_borrow_layout(layout), - layout: *layout, - symbol: *symbol, - }), - arena, - ) - .into_bump_slice() - } - - fn init_borrow_args_always_owned( - arena: &'a Bump, - ps: &'a [(Layout<'a>, Symbol)], - ) -> &'a [Param<'a>] { - Vec::from_iter_in( - ps.iter().map(|(layout, symbol)| Param { - borrow: false, - layout: *layout, - symbol: *symbol, - }), - arena, - ) - .into_bump_slice() - } - - fn visit_proc(&mut self, arena: &'a Bump, proc: &Proc<'a>, key: (Symbol, ProcLayout<'a>)) { - if proc.must_own_arguments { - self.visit_proc_always_owned(arena, proc, key); - return; - } - - let index: usize = self.get_param_offset(key.0, key.1).into(); - - for (i, param) in Self::init_borrow_args(arena, proc.args) - .iter() - .copied() - .enumerate() - { - self.declarations[index + i] = param; - } - - self.visit_stmt(arena, proc.name, &proc.body); - } - - fn visit_proc_always_owned( - &mut self, - arena: &'a Bump, - proc: &Proc<'a>, - key: (Symbol, ProcLayout<'a>), - ) { - let index: usize = self.get_param_offset(key.0, key.1).into(); - - for (i, param) in Self::init_borrow_args_always_owned(arena, proc.args) - .iter() - .copied() - .enumerate() - { - self.declarations[index + i] = param; - } - - self.visit_stmt(arena, proc.name, &proc.body); - } - - fn visit_stmt(&mut self, arena: &'a Bump, _fnid: Symbol, stmt: &Stmt<'a>) { - use Stmt::*; - - let mut stack = bumpalo::vec![in arena; stmt]; - - while let Some(stmt) = stack.pop() { - match stmt { - Join { - id: j, - parameters: xs, - remainder: v, - body: b, - } => { - self.join_points - .insert(*j, Self::init_borrow_params(arena, xs)); - - stack.push(v); - stack.push(b); - } - Let(_, _, _, cont) => { - stack.push(cont); - } - - Expect { remainder, .. } => stack.push(remainder), - - Switch { - branches, - default_branch, - .. - } => { - stack.extend(branches.iter().map(|b| &b.2)); - stack.push(default_branch.1); - } - Refcounting(_, _) => unreachable!("these have not been introduced yet"), - - Ret(_) | Jump(_, _) | RuntimeError(_) => { - // these are terminal, do nothing - } - } - } - } -} - -// Apply the inferred borrow annotations stored in ParamMap to a block of mutually recursive procs - -struct BorrowInfState<'a> { - current_proc: Symbol, - param_set: MutSet, - owned: MutMap>, - modified: bool, - arena: &'a Bump, -} - -impl<'a> BorrowInfState<'a> { - pub fn own_var(&mut self, x: Symbol) { - let current = self.owned.get_mut(&self.current_proc).unwrap(); - - if current.insert(x) { - // entered if key was not yet present. If so, the set is modified, - // hence we set this flag - self.modified = true; - } - } - - /// if the extracted value is owned, then the surrounding structure must be too - fn if_is_owned_then_own(&mut self, extracted: Symbol, structure: Symbol) { - match self.owned.get_mut(&self.current_proc) { - None => unreachable!( - "the current procedure symbol {:?} is not in the owned map", - self.current_proc - ), - Some(set) => { - if set.contains(&extracted) && set.insert(structure) { - // entered if key was not yet present. If so, the set is modified, - // hence we set this flag - self.modified = true; - } - } - } - } - - fn is_owned(&self, x: Symbol) -> bool { - match self.owned.get(&self.current_proc) { - None => unreachable!( - "the current procedure symbol {:?} is not in the owned map", - self.current_proc - ), - Some(set) => set.contains(&x), - } - } - - fn update_param_map_help(&mut self, ps: &[Param<'a>]) -> &'a [Param<'a>] { - let mut new_ps = Vec::with_capacity_in(ps.len(), self.arena); - new_ps.extend(ps.iter().map(|p| { - if !p.borrow { - *p - } else if self.is_owned(p.symbol) { - self.modified = true; - let mut p = *p; - p.borrow = false; - - p - } else { - *p - } - })); - - new_ps.into_bump_slice() - } - - fn update_param_map_declaration( - &mut self, - param_map: &mut ParamMap<'a>, - start: ParamOffset, - length: usize, - ) { - let index: usize = start.into(); - let ps = &mut param_map.declarations[index..][..length]; - - for p in ps.iter_mut() { - if !p.borrow { - // do nothing - } else if self.is_owned(p.symbol) { - self.modified = true; - p.borrow = false; - } else { - // do nothing - } - } - } - - fn update_param_map_join_point(&mut self, param_map: &mut ParamMap<'a>, id: JoinPointId) { - let ps = param_map.join_points[&id]; - let new_ps = self.update_param_map_help(ps); - param_map.join_points.insert(id, new_ps); - } - - /// This looks at an application `f x1 x2 x3` - /// If the parameter (based on the definition of `f`) is owned, - /// then the argument must also be owned - fn own_args_using_params(&mut self, xs: &[Symbol], ps: &[Param<'a>]) { - debug_assert_eq!(xs.len(), ps.len()); - - for (x, p) in xs.iter().zip(ps.iter()) { - if !p.borrow { - self.own_var(*x); - } - } - } - - /// This looks at an application `f x1 x2 x3` - /// If the parameter (based on the definition of `f`) is owned, - /// then the argument must also be owned - fn own_args_using_bools(&mut self, xs: &[Symbol], ps: &[bool]) { - debug_assert_eq!(xs.len(), ps.len()); - - for (x, borrow) in xs.iter().zip(ps.iter()) { - if !borrow { - self.own_var(*x); - } - } - } - - /// For each xs[i], if xs[i] is owned, then mark ps[i] as owned. - /// We use this action to preserve tail calls. That is, if we have - /// a tail call `f xs`, if the i-th parameter is borrowed, but `xs[i]` is owned - /// we would have to insert a `dec xs[i]` after `f xs` and consequently - /// "break" the tail call. - fn own_params_using_args(&mut self, xs: &[Symbol], ps: &[Param<'a>]) { - debug_assert_eq!(xs.len(), ps.len()); - - for (x, p) in xs.iter().zip(ps.iter()) { - if self.is_owned(*x) { - self.own_var(p.symbol); - } - } - } - - /// Mark `xs[i]` as owned if it is one of the parameters `ps`. - /// We use this action to mark function parameters that are being "packed" inside constructors. - /// This is a heuristic, and is not related with the effectiveness of the reset/reuse optimization. - /// It is useful for code such as - /// - /// > def f (x y : obj) := - /// > let z := ctor_1 x y; - /// > ret z - fn own_args_if_param(&mut self, xs: &[Symbol]) { - for x in xs.iter() { - // TODO may also be asking for the index here? see Lean - if self.param_set.contains(x) { - self.own_var(*x); - } - } - } - - /// This looks at the assignment - /// - /// let z = e in ... - /// - /// and determines whether z and which of the symbols used in e - /// must be taken as owned parameters - fn collect_call(&mut self, param_map: &mut ParamMap<'a>, z: Symbol, e: &crate::ir::Call<'a>) { - use crate::ir::CallType::*; - - let crate::ir::Call { - call_type, - arguments, - } = e; - - match call_type { - ByName { - name, - ret_layout, - arg_layouts, - .. - } => { - let top_level = ProcLayout::new(self.arena, arg_layouts, **ret_layout); - - // get the borrow signature of the applied function - let ps = param_map - .get_symbol(*name, top_level) - .expect("function is defined"); - - // the return value will be owned - self.own_var(z); - - // if the function exects an owned argument (ps), the argument must be owned (args) - debug_assert_eq!( - arguments.len(), - ps.len(), - "{:?} has {} parameters, but was applied to {} arguments", - name, - ps.len(), - arguments.len() - ); - - self.own_args_using_params(arguments, ps); - } - - LowLevel { op, .. } => { - debug_assert!(!op.is_higher_order()); - - self.own_var(z); - - let ps = lowlevel_borrow_signature(self.arena, *op); - - self.own_args_using_bools(arguments, ps); - } - - HigherOrder(HigherOrderLowLevel { - op, - passed_function, - .. - }) => { - use crate::low_level::HigherOrder::*; - - let closure_layout = ProcLayout { - arguments: passed_function.argument_layouts, - result: passed_function.return_layout, - }; - - let function_ps = match param_map.get_symbol(passed_function.name, closure_layout) { - Some(function_ps) => function_ps, - None => unreachable!(), - }; - - match op { - ListMap { xs } - | ListKeepIf { xs } - | ListKeepOks { xs } - | ListKeepErrs { xs } - | ListAny { xs } - | ListAll { xs } - | ListFindUnsafe { xs } => { - // own the list if the function wants to own the element - if !function_ps[0].borrow { - self.own_var(*xs); - } - } - ListMapWithIndex { xs } => { - // List.mapWithIndex : List before, (before, Nat -> after) -> List after - // own the list if the function wants to own the element (before, index 0) - if !function_ps[0].borrow { - self.own_var(*xs); - } - } - ListMap2 { xs, ys } => { - // own the lists if the function wants to own the element - if !function_ps[0].borrow { - self.own_var(*xs); - } - - if !function_ps[1].borrow { - self.own_var(*ys); - } - } - ListMap3 { xs, ys, zs } => { - // own the lists if the function wants to own the element - if !function_ps[0].borrow { - self.own_var(*xs); - } - if !function_ps[1].borrow { - self.own_var(*ys); - } - if !function_ps[2].borrow { - self.own_var(*zs); - } - } - ListMap4 { xs, ys, zs, ws } => { - // own the lists if the function wants to own the element - if !function_ps[0].borrow { - self.own_var(*xs); - } - if !function_ps[1].borrow { - self.own_var(*ys); - } - if !function_ps[2].borrow { - self.own_var(*zs); - } - if !function_ps[3].borrow { - self.own_var(*ws); - } - } - ListSortWith { xs } => { - // always own the input list - self.own_var(*xs); - } - ListWalk { xs, state } - | ListWalkUntil { xs, state } - | ListWalkBackwards { xs, state } - | DictWalk { xs, state } => { - // own the default value if the function wants to own it - if !function_ps[0].borrow { - self.own_var(*state); - } - - // own the data structure if the function wants to own the element - if !function_ps[1].borrow { - self.own_var(*xs); - } - } - } - - // own the closure environment if the function needs to own it - let function_env_position = op.function_arity(); - if let Some(false) = function_ps.get(function_env_position).map(|p| p.borrow) { - self.own_var(passed_function.captured_environment); - } - } - - Foreign { .. } => { - // very unsure what demand ForeignCall should place upon its arguments - self.own_var(z); - - let ps = foreign_borrow_signature(self.arena, arguments.len()); - - self.own_args_using_bools(arguments, ps); - } - } - } - - fn collect_expr(&mut self, param_map: &mut ParamMap<'a>, z: Symbol, e: &Expr<'a>) { - use Expr::*; - - match e { - Array { elems: xs, .. } => { - let xs = Vec::from_iter_in(xs.iter().filter_map(|e| e.to_symbol()), self.arena); - self.own_var(z); - - // if the used symbol is an argument to the current function, - // the function must take it as an owned parameter - self.own_args_if_param(&xs); - } - Tag { arguments: xs, .. } | Struct(xs) => { - self.own_var(z); - - // if the used symbol is an argument to the current function, - // the function must take it as an owned parameter - self.own_args_if_param(xs); - } - - ExprBox { symbol: x } => { - self.own_var(z); - - // if the used symbol is an argument to the current function, - // the function must take it as an owned parameter - self.own_args_if_param(&[*x]); - } - - ExprUnbox { symbol: x } => { - // if the boxed value is owned, the box is - self.if_is_owned_then_own(*x, z); - - // if the extracted value is owned, the structure must be too - self.if_is_owned_then_own(z, *x); - } - - Reset { symbol: x, .. } => { - self.own_var(z); - self.own_var(*x); - } - Reuse { - symbol: x, - arguments: ys, - .. - } => { - self.own_var(z); - self.own_var(*x); - self.own_args_if_param(ys); - } - EmptyArray => { - self.own_var(z); - } - - Call(call) => self.collect_call(param_map, z, call), - - Literal(_) | RuntimeErrorFunction(_) => {} - - StructAtIndex { structure: x, .. } => { - // if the structure (record/tag/array) is owned, the extracted value is - self.if_is_owned_then_own(*x, z); - - // if the extracted value is owned, the structure must be too - self.if_is_owned_then_own(z, *x); - } - - UnionAtIndex { structure: x, .. } => { - // if the structure (record/tag/array) is owned, the extracted value is - self.if_is_owned_then_own(*x, z); - - // if the extracted value is owned, the structure must be too - self.if_is_owned_then_own(z, *x); - } - - GetTagId { structure: x, .. } => { - // if the structure (record/tag/array) is owned, the extracted value is - self.if_is_owned_then_own(*x, z); - - // if the extracted value is owned, the structure must be too - self.if_is_owned_then_own(z, *x); - } - } - } - - #[allow(clippy::many_single_char_names)] - fn preserve_tail_call( - &mut self, - param_map: &mut ParamMap<'a>, - x: Symbol, - v: &Expr<'a>, - b: &Stmt<'a>, - ) { - if let ( - Expr::Call(crate::ir::Call { - call_type: - crate::ir::CallType::ByName { - name: g, - arg_layouts, - ret_layout, - .. - }, - arguments: ys, - .. - }), - Stmt::Ret(z), - ) = (v, b) - { - let top_level = ProcLayout::new(self.arena, arg_layouts, **ret_layout); - - if self.current_proc == *g && x == *z { - // anonymous functions (for which the ps may not be known) - // can never be tail-recursive, so this is fine - if let Some(ps) = param_map.get_symbol(*g, top_level) { - self.own_params_using_args(ys, ps) - } - } - } - } - - fn update_param_set(&mut self, ps: &[Param<'a>]) { - for p in ps.iter() { - self.param_set.insert(p.symbol); - } - } - - fn update_param_set_symbols(&mut self, ps: &[Symbol]) { - for p in ps.iter() { - self.param_set.insert(*p); - } - } - - fn collect_stmt(&mut self, param_map: &mut ParamMap<'a>, stmt: &Stmt<'a>) { - use Stmt::*; - - match stmt { - Join { - id: j, - parameters: ys, - remainder: v, - body: b, - } => { - let old = self.param_set.clone(); - self.update_param_set(ys); - self.collect_stmt(param_map, v); - self.param_set = old; - self.update_param_map_join_point(param_map, *j); - - self.collect_stmt(param_map, b); - } - - Let(x, v, _, mut b) => { - let mut stack = Vec::new_in(self.arena); - - stack.push((*x, v)); - - while let Stmt::Let(symbol, expr, _, tail) = b { - b = tail; - stack.push((*symbol, expr)); - } - - self.collect_stmt(param_map, b); - - let mut it = stack.into_iter().rev(); - - // collect the final expr, and see if we need to preserve a tail call - let (x, v) = it.next().unwrap(); - self.collect_expr(param_map, x, v); - self.preserve_tail_call(param_map, x, v, b); - - for (x, v) in it { - self.collect_expr(param_map, x, v); - } - } - - Jump(j, ys) => { - let ps = param_map.get_join_point(*j); - - // for making sure the join point can reuse - self.own_args_using_params(ys, ps); - - // for making sure the tail call is preserved - self.own_params_using_args(ys, ps); - } - Switch { - branches, - default_branch, - .. - } => { - for (_, _, b) in branches.iter() { - self.collect_stmt(param_map, b); - } - self.collect_stmt(param_map, default_branch.1); - } - - Expect { remainder, .. } => { - self.collect_stmt(param_map, remainder); - } - - Refcounting(_, _) => unreachable!("these have not been introduced yet"), - - Ret(_) | RuntimeError(_) => { - // these are terminal, do nothing - } - } - } - - fn collect_proc( - &mut self, - param_map: &mut ParamMap<'a>, - proc: &Proc<'a>, - param_offset: ParamOffset, - ) { - let old = self.param_set.clone(); - - let ys = Vec::from_iter_in(proc.args.iter().map(|t| t.1), self.arena).into_bump_slice(); - self.update_param_set_symbols(ys); - self.current_proc = proc.name; - - // ensure that current_proc is in the owned map - self.owned.entry(proc.name).or_default(); - - self.collect_stmt(param_map, &proc.body); - self.update_param_map_declaration(param_map, param_offset, proc.args.len()); - - self.param_set = old; - } -} - -pub fn foreign_borrow_signature(arena: &Bump, arity: usize) -> &[bool] { - // NOTE this means that Roc is responsible for cleaning up resources; - // the host cannot (currently) take ownership - let all = bumpalo::vec![in arena; BORROWED; arity]; - all.into_bump_slice() -} - -pub fn lowlevel_borrow_signature(arena: &Bump, op: LowLevel) -> &[bool] { - use LowLevel::*; - - // TODO is true or false more efficient for non-refcounted layouts? - let irrelevant = OWNED; - let function = irrelevant; - let closure_data = irrelevant; - let owned = OWNED; - let borrowed = BORROWED; - - // Here we define the borrow signature of low-level operations - // - // - arguments with non-refcounted layouts (ints, floats) are `irrelevant` - // - arguments that we may want to update destructively must be Owned - // - other refcounted arguments are Borrowed - match op { - ListLen | StrIsEmpty | StrCountGraphemes => arena.alloc_slice_copy(&[borrowed]), - ListReplaceUnsafe => arena.alloc_slice_copy(&[owned, irrelevant, irrelevant]), - ListGetUnsafe => arena.alloc_slice_copy(&[borrowed, irrelevant]), - ListConcat => arena.alloc_slice_copy(&[owned, owned]), - StrConcat => arena.alloc_slice_copy(&[owned, borrowed]), - StrTrim => arena.alloc_slice_copy(&[owned]), - StrTrimLeft => arena.alloc_slice_copy(&[owned]), - StrTrimRight => arena.alloc_slice_copy(&[owned]), - StrSplit => arena.alloc_slice_copy(&[borrowed, borrowed]), - StrToNum => arena.alloc_slice_copy(&[borrowed]), - ListSingle => arena.alloc_slice_copy(&[irrelevant]), - // List.repeat : elem, Nat -> List.elem - ListRepeat => arena.alloc_slice_copy(&[borrowed, irrelevant]), - ListReverse => arena.alloc_slice_copy(&[owned]), - ListPrepend => arena.alloc_slice_copy(&[owned, owned]), - StrJoinWith => arena.alloc_slice_copy(&[borrowed, borrowed]), - ListJoin => arena.alloc_slice_copy(&[irrelevant]), - ListMap | ListMapWithIndex => arena.alloc_slice_copy(&[owned, function, closure_data]), - ListMap2 => arena.alloc_slice_copy(&[owned, owned, function, closure_data]), - ListMap3 => arena.alloc_slice_copy(&[owned, owned, owned, function, closure_data]), - ListMap4 => arena.alloc_slice_copy(&[owned, owned, owned, owned, function, closure_data]), - ListKeepIf | ListKeepOks | ListKeepErrs | ListAny | ListAll => { - arena.alloc_slice_copy(&[owned, function, closure_data]) - } - ListContains => arena.alloc_slice_copy(&[borrowed, irrelevant]), - ListRange => arena.alloc_slice_copy(&[irrelevant, irrelevant]), - ListWalk | ListWalkUntil | ListWalkBackwards => { - arena.alloc_slice_copy(&[owned, owned, function, closure_data]) - } - ListSortWith => arena.alloc_slice_copy(&[owned, function, closure_data]), - ListFindUnsafe => arena.alloc_slice_copy(&[owned, function, closure_data]), - - // TODO when we have lists with capacity (if ever) - // List.append should own its first argument - ListAppend => arena.alloc_slice_copy(&[owned, owned]), - ListSublist => arena.alloc_slice_copy(&[owned, irrelevant, irrelevant]), - ListDropAt => arena.alloc_slice_copy(&[owned, irrelevant]), - ListSwap => arena.alloc_slice_copy(&[owned, irrelevant, irrelevant]), - - Eq | NotEq => arena.alloc_slice_copy(&[borrowed, borrowed]), - - And | Or | NumAdd | NumAddWrap | NumAddChecked | NumAddSaturated | NumSub | NumSubWrap - | NumSubChecked | NumSubSaturated | NumMul | NumMulWrap | NumMulChecked | NumGt - | NumGte | NumLt | NumLte | NumCompare | NumDivUnchecked | NumDivCeilUnchecked - | NumRemUnchecked | NumIsMultipleOf | NumPow | NumPowInt | NumBitwiseAnd - | NumBitwiseXor | NumBitwiseOr | NumShiftLeftBy | NumShiftRightBy | NumShiftRightZfBy => { - arena.alloc_slice_copy(&[irrelevant, irrelevant]) - } - - NumToStr | NumAbs | NumNeg | NumSin | NumCos | NumSqrtUnchecked | NumLogUnchecked - | NumRound | NumCeiling | NumFloor | NumToFrac | Not | NumIsFinite | NumAtan | NumAcos - | NumAsin | NumIntCast | NumToIntChecked | NumToFloatCast | NumToFloatChecked => { - arena.alloc_slice_copy(&[irrelevant]) - } - NumBytesToU16 => arena.alloc_slice_copy(&[borrowed, irrelevant]), - NumBytesToU32 => arena.alloc_slice_copy(&[borrowed, irrelevant]), - StrStartsWith | StrEndsWith => arena.alloc_slice_copy(&[owned, borrowed]), - StrStartsWithCodePt => arena.alloc_slice_copy(&[borrowed, irrelevant]), - StrFromUtf8 => arena.alloc_slice_copy(&[owned]), - StrFromUtf8Range => arena.alloc_slice_copy(&[borrowed, irrelevant]), - StrToUtf8 => arena.alloc_slice_copy(&[owned]), - StrRepeat => arena.alloc_slice_copy(&[borrowed, irrelevant]), - StrFromInt | StrFromFloat => arena.alloc_slice_copy(&[irrelevant]), - Hash => arena.alloc_slice_copy(&[borrowed, irrelevant]), - DictSize => arena.alloc_slice_copy(&[borrowed]), - DictEmpty => &[], - DictInsert => arena.alloc_slice_copy(&[owned, owned, owned]), - DictRemove => arena.alloc_slice_copy(&[owned, borrowed]), - DictContains => arena.alloc_slice_copy(&[borrowed, borrowed]), - DictGetUnsafe => arena.alloc_slice_copy(&[borrowed, borrowed]), - DictKeys | DictValues => arena.alloc_slice_copy(&[borrowed]), - DictUnion | DictDifference | DictIntersection => arena.alloc_slice_copy(&[owned, borrowed]), - - // borrow function argument so we don't have to worry about RC of the closure - DictWalk => arena.alloc_slice_copy(&[owned, owned, function, closure_data]), - - SetFromList => arena.alloc_slice_copy(&[owned]), - SetToDict => arena.alloc_slice_copy(&[owned]), - - ListIsUnique => arena.alloc_slice_copy(&[borrowed]), - - BoxExpr | UnboxExpr => { - unreachable!("These lowlevel operations are turned into mono Expr's") - } - - PtrCast | RefCountInc | RefCountDec => { - unreachable!("Only inserted *after* borrow checking: {:?}", op); - } - } -} - -struct CallInfo<'a> { - keys: Vec<'a, Symbol>, -} - -fn call_info_call<'a>(call: &crate::ir::Call<'a>, info: &mut CallInfo<'a>) { - use crate::ir::CallType::*; - - match call.call_type { - ByName { name, .. } => { - info.keys.push(name); - } - Foreign { .. } => {} - LowLevel { .. } => {} - HigherOrder(_) => {} - } -} - -fn call_info_stmt<'a>(arena: &'a Bump, stmt: &Stmt<'a>, info: &mut CallInfo<'a>) { - use Stmt::*; - - let mut stack = bumpalo::vec![in arena; stmt]; - - while let Some(stmt) = stack.pop() { - match stmt { - Join { - remainder: v, - body: b, - .. - } => { - stack.push(v); - stack.push(b); - } - Let(_, expr, _, cont) => { - if let Expr::Call(call) = expr { - call_info_call(call, info); - } - stack.push(cont); - } - Switch { - branches, - default_branch, - .. - } => { - stack.extend(branches.iter().map(|b| &b.2)); - stack.push(default_branch.1); - } - - Expect { remainder, .. } => stack.push(remainder), - - Refcounting(_, _) => unreachable!("these have not been introduced yet"), - - Ret(_) | Jump(_, _) | RuntimeError(_) => { - // these are terminal, do nothing - } - } - } -} diff --git a/compiler/mono/src/code_gen_help/equality.rs b/compiler/mono/src/code_gen_help/equality.rs deleted file mode 100644 index 374daeb6d2..0000000000 --- a/compiler/mono/src/code_gen_help/equality.rs +++ /dev/null @@ -1,811 +0,0 @@ -use bumpalo::collections::vec::Vec; -use roc_module::low_level::LowLevel; -use roc_module::symbol::{IdentIds, Symbol}; - -use crate::ir::{ - BranchInfo, Call, CallType, Expr, JoinPointId, Literal, Param, Stmt, UpdateModeId, -}; -use crate::layout::{Builtin, Layout, TagIdIntType, UnionLayout}; - -use super::{let_lowlevel, CodeGenHelp, Context, LAYOUT_BOOL}; - -const ARG_1: Symbol = Symbol::ARG_1; -const ARG_2: Symbol = Symbol::ARG_2; - -pub fn eq_generic<'a>( - root: &mut CodeGenHelp<'a>, - ident_ids: &mut IdentIds, - ctx: &mut Context<'a>, - layout: Layout<'a>, -) -> Stmt<'a> { - let eq_todo = || todo!("Specialized `==` operator for `{:?}`", layout); - - let main_body = match layout { - Layout::Builtin(Builtin::Int(_) | Builtin::Float(_) | Builtin::Bool | Builtin::Decimal) => { - unreachable!( - "No generated proc for `==`. Use direct code gen for {:?}", - layout - ) - } - Layout::Builtin(Builtin::Str) => { - unreachable!("No generated helper proc for `==` on Str. Use Zig function.") - } - Layout::Builtin(Builtin::Dict(_, _) | Builtin::Set(_)) => eq_todo(), - Layout::Builtin(Builtin::List(elem_layout)) => eq_list(root, ident_ids, ctx, elem_layout), - Layout::Struct { field_layouts, .. } => eq_struct(root, ident_ids, ctx, field_layouts), - Layout::Union(union_layout) => eq_tag_union(root, ident_ids, ctx, union_layout), - Layout::Boxed(inner_layout) => eq_boxed(root, ident_ids, ctx, inner_layout), - Layout::LambdaSet(_) => unreachable!("`==` is not defined on functions"), - Layout::RecursivePointer => { - unreachable!( - "Can't perform `==` on RecursivePointer. Should have been replaced by a tag union." - ) - } - }; - - Stmt::Let( - Symbol::BOOL_TRUE, - Expr::Literal(Literal::Int(1i128.to_ne_bytes())), - LAYOUT_BOOL, - root.arena.alloc(Stmt::Let( - Symbol::BOOL_FALSE, - Expr::Literal(Literal::Int(0i128.to_ne_bytes())), - LAYOUT_BOOL, - root.arena.alloc(main_body), - )), - ) -} - -fn if_pointers_equal_return_true<'a>( - root: &CodeGenHelp<'a>, - ident_ids: &mut IdentIds, - operands: [Symbol; 2], - following: &'a Stmt<'a>, -) -> Stmt<'a> { - let ptr1_addr = root.create_symbol(ident_ids, "addr1"); - let ptr2_addr = root.create_symbol(ident_ids, "addr2"); - let ptr_eq = root.create_symbol(ident_ids, "eq_addr"); - - Stmt::Let( - ptr1_addr, - Expr::Call(Call { - call_type: CallType::LowLevel { - op: LowLevel::PtrCast, - update_mode: UpdateModeId::BACKEND_DUMMY, - }, - arguments: root.arena.alloc([operands[0]]), - }), - root.layout_isize, - root.arena.alloc(Stmt::Let( - ptr2_addr, - Expr::Call(Call { - call_type: CallType::LowLevel { - op: LowLevel::PtrCast, - update_mode: UpdateModeId::BACKEND_DUMMY, - }, - arguments: root.arena.alloc([operands[1]]), - }), - root.layout_isize, - root.arena.alloc(Stmt::Let( - ptr_eq, - Expr::Call(Call { - call_type: CallType::LowLevel { - op: LowLevel::Eq, - update_mode: UpdateModeId::BACKEND_DUMMY, - }, - arguments: root.arena.alloc([ptr1_addr, ptr2_addr]), - }), - LAYOUT_BOOL, - root.arena.alloc(Stmt::Switch { - cond_symbol: ptr_eq, - cond_layout: LAYOUT_BOOL, - branches: root.arena.alloc([( - 1, - BranchInfo::None, - Stmt::Ret(Symbol::BOOL_TRUE), - )]), - default_branch: (BranchInfo::None, following), - ret_layout: LAYOUT_BOOL, - }), - )), - )), - ) -} - -fn if_false_return_false<'a>( - root: &CodeGenHelp<'a>, - symbol: Symbol, - following: &'a Stmt<'a>, -) -> Stmt<'a> { - Stmt::Switch { - cond_symbol: symbol, - cond_layout: LAYOUT_BOOL, - branches: root - .arena - .alloc([(0, BranchInfo::None, Stmt::Ret(Symbol::BOOL_FALSE))]), - default_branch: (BranchInfo::None, following), - ret_layout: LAYOUT_BOOL, - } -} - -fn eq_struct<'a>( - root: &mut CodeGenHelp<'a>, - ident_ids: &mut IdentIds, - ctx: &mut Context<'a>, - field_layouts: &'a [Layout<'a>], -) -> Stmt<'a> { - let mut else_stmt = Stmt::Ret(Symbol::BOOL_TRUE); - for (i, layout) in field_layouts.iter().enumerate().rev() { - let field1_sym = root.create_symbol(ident_ids, &format!("field_1_{}", i)); - let field1_expr = Expr::StructAtIndex { - index: i as u64, - field_layouts, - structure: ARG_1, - }; - let field1_stmt = |next| Stmt::Let(field1_sym, field1_expr, *layout, next); - - let field2_sym = root.create_symbol(ident_ids, &format!("field_2_{}", i)); - let field2_expr = Expr::StructAtIndex { - index: i as u64, - field_layouts, - structure: ARG_2, - }; - let field2_stmt = |next| Stmt::Let(field2_sym, field2_expr, *layout, next); - - let eq_call_expr = root - .call_specialized_op( - ident_ids, - ctx, - *layout, - root.arena.alloc([field1_sym, field2_sym]), - ) - .unwrap(); - - let eq_call_name = format!("eq_call_{}", i); - let eq_call_sym = root.create_symbol(ident_ids, &eq_call_name); - let eq_call_stmt = |next| Stmt::Let(eq_call_sym, eq_call_expr, LAYOUT_BOOL, next); - - else_stmt = field1_stmt(root.arena.alloc( - // - field2_stmt(root.arena.alloc( - // - eq_call_stmt(root.arena.alloc( - // - if_false_return_false(root, eq_call_sym, root.arena.alloc(else_stmt)), - )), - )), - )) - } - - if_pointers_equal_return_true(root, ident_ids, [ARG_1, ARG_2], root.arena.alloc(else_stmt)) -} - -fn eq_tag_union<'a>( - root: &mut CodeGenHelp<'a>, - ident_ids: &mut IdentIds, - ctx: &mut Context<'a>, - union_layout: UnionLayout<'a>, -) -> Stmt<'a> { - use UnionLayout::*; - - let parent_rec_ptr_layout = ctx.recursive_union; - if !matches!(union_layout, NonRecursive(_)) { - ctx.recursive_union = Some(union_layout); - } - - let body = match union_layout { - NonRecursive(tags) => eq_tag_union_help(root, ident_ids, ctx, union_layout, tags, None), - - Recursive(tags) => eq_tag_union_help(root, ident_ids, ctx, union_layout, tags, None), - - NonNullableUnwrapped(field_layouts) => { - let tags = root.arena.alloc([field_layouts]); - eq_tag_union_help(root, ident_ids, ctx, union_layout, tags, None) - } - - NullableWrapped { - other_tags, - nullable_id, - } => eq_tag_union_help( - root, - ident_ids, - ctx, - union_layout, - other_tags, - Some(nullable_id), - ), - - NullableUnwrapped { - other_fields, - nullable_id, - } => eq_tag_union_help( - root, - ident_ids, - ctx, - union_layout, - root.arena.alloc([other_fields]), - Some(nullable_id as TagIdIntType), - ), - }; - - ctx.recursive_union = parent_rec_ptr_layout; - - body -} - -fn eq_tag_union_help<'a>( - root: &mut CodeGenHelp<'a>, - ident_ids: &mut IdentIds, - ctx: &mut Context<'a>, - union_layout: UnionLayout<'a>, - tag_layouts: &'a [&'a [Layout<'a>]], - nullable_id: Option, -) -> Stmt<'a> { - let tailrec_loop = JoinPointId(root.create_symbol(ident_ids, "tailrec_loop")); - let is_non_recursive = matches!(union_layout, UnionLayout::NonRecursive(_)); - let operands = if is_non_recursive { - [ARG_1, ARG_2] - } else { - [ - root.create_symbol(ident_ids, "a"), - root.create_symbol(ident_ids, "b"), - ] - }; - - let tag_id_layout = union_layout.tag_id_layout(); - - let tag_id_a = root.create_symbol(ident_ids, "tag_id_a"); - let tag_id_a_stmt = |next| { - Stmt::Let( - tag_id_a, - Expr::GetTagId { - structure: operands[0], - union_layout, - }, - tag_id_layout, - next, - ) - }; - - let tag_id_b = root.create_symbol(ident_ids, "tag_id_b"); - let tag_id_b_stmt = |next| { - Stmt::Let( - tag_id_b, - Expr::GetTagId { - structure: operands[1], - union_layout, - }, - tag_id_layout, - next, - ) - }; - - let tag_ids_eq = root.create_symbol(ident_ids, "tag_ids_eq"); - let tag_ids_expr = Expr::Call(Call { - call_type: CallType::LowLevel { - op: LowLevel::Eq, - update_mode: UpdateModeId::BACKEND_DUMMY, - }, - arguments: root.arena.alloc([tag_id_a, tag_id_b]), - }); - let tag_ids_eq_stmt = |next| Stmt::Let(tag_ids_eq, tag_ids_expr, LAYOUT_BOOL, next); - - let if_equal_ids_branches = - root.arena - .alloc([(0, BranchInfo::None, Stmt::Ret(Symbol::BOOL_FALSE))]); - - // - // Switch statement by tag ID - // - - let mut tag_branches = Vec::with_capacity_in(tag_layouts.len(), root.arena); - - // If there's a null tag, check it first. We might not need to load any data from memory. - if let Some(id) = nullable_id { - tag_branches.push((id as u64, BranchInfo::None, Stmt::Ret(Symbol::BOOL_TRUE))) - } - - let mut tag_id: TagIdIntType = 0; - for field_layouts in tag_layouts.iter().take(tag_layouts.len() - 1) { - if let Some(null_id) = nullable_id { - if tag_id == null_id as TagIdIntType { - tag_id += 1; - } - } - - let tag_stmt = eq_tag_fields( - root, - ident_ids, - ctx, - tailrec_loop, - union_layout, - field_layouts, - operands, - tag_id, - ); - tag_branches.push((tag_id as u64, BranchInfo::None, tag_stmt)); - - tag_id += 1; - } - - let tag_switch_stmt = Stmt::Switch { - cond_symbol: tag_id_a, - cond_layout: tag_id_layout, - branches: tag_branches.into_bump_slice(), - default_branch: ( - BranchInfo::None, - root.arena.alloc(eq_tag_fields( - root, - ident_ids, - ctx, - tailrec_loop, - union_layout, - tag_layouts.last().unwrap(), - operands, - tag_id, - )), - ), - ret_layout: LAYOUT_BOOL, - }; - - let if_equal_ids_stmt = Stmt::Switch { - cond_symbol: tag_ids_eq, - cond_layout: LAYOUT_BOOL, - branches: if_equal_ids_branches, - default_branch: (BranchInfo::None, root.arena.alloc(tag_switch_stmt)), - ret_layout: LAYOUT_BOOL, - }; - - // - // combine all the statments - // - let compare_values = tag_id_a_stmt(root.arena.alloc( - // - tag_id_b_stmt(root.arena.alloc( - // - tag_ids_eq_stmt(root.arena.alloc( - // - if_equal_ids_stmt, - )), - )), - )); - - let compare_ptr_or_value = - if_pointers_equal_return_true(root, ident_ids, operands, root.arena.alloc(compare_values)); - - if is_non_recursive { - compare_ptr_or_value - } else { - let loop_params_iter = operands.iter().map(|arg| Param { - symbol: *arg, - borrow: true, - layout: Layout::Union(union_layout), - }); - - let loop_start = Stmt::Jump(tailrec_loop, root.arena.alloc([ARG_1, ARG_2])); - - Stmt::Join { - id: tailrec_loop, - parameters: root.arena.alloc_slice_fill_iter(loop_params_iter), - body: root.arena.alloc(compare_ptr_or_value), - remainder: root.arena.alloc(loop_start), - } - } -} - -#[allow(clippy::too_many_arguments)] -fn eq_tag_fields<'a>( - root: &mut CodeGenHelp<'a>, - ident_ids: &mut IdentIds, - ctx: &mut Context<'a>, - tailrec_loop: JoinPointId, - union_layout: UnionLayout<'a>, - field_layouts: &'a [Layout<'a>], - operands: [Symbol; 2], - tag_id: TagIdIntType, -) -> Stmt<'a> { - // Find a RecursivePointer to use in the tail recursion loop - // (If there are more than one, the others will use non-tail recursion) - let rec_ptr_index = field_layouts - .iter() - .position(|field| matches!(field, Layout::RecursivePointer)); - - let (tailrec_index, innermost_stmt) = match rec_ptr_index { - None => { - // This tag has no RecursivePointers. Set tailrec_index out of range. - (field_layouts.len(), Stmt::Ret(Symbol::BOOL_TRUE)) - } - - Some(i) => { - // Implement tail recursion on this RecursivePointer, - // in the innermost `else` clause after all other fields have been checked - let field1_sym = root.create_symbol(ident_ids, &format!("field_1_{}_{}", tag_id, i)); - let field2_sym = root.create_symbol(ident_ids, &format!("field_2_{}_{}", tag_id, i)); - - let field1_expr = Expr::UnionAtIndex { - union_layout, - tag_id, - index: i as u64, - structure: operands[0], - }; - - let field2_expr = Expr::UnionAtIndex { - union_layout, - tag_id, - index: i as u64, - structure: operands[1], - }; - - let inner = Stmt::Let( - field1_sym, - field1_expr, - field_layouts[i], - root.arena.alloc( - // - Stmt::Let( - field2_sym, - field2_expr, - field_layouts[i], - root.arena.alloc( - // - Stmt::Jump(tailrec_loop, root.arena.alloc([field1_sym, field2_sym])), - ), - ), - ), - ); - - (i, inner) - } - }; - - let mut stmt = innermost_stmt; - for (i, layout) in field_layouts.iter().enumerate().rev() { - if i == tailrec_index { - continue; // the tail-recursive field is handled elsewhere - } - - let field1_sym = root.create_symbol(ident_ids, &format!("field_1_{}_{}", tag_id, i)); - let field2_sym = root.create_symbol(ident_ids, &format!("field_2_{}_{}", tag_id, i)); - - let field1_expr = Expr::UnionAtIndex { - union_layout, - tag_id, - index: i as u64, - structure: operands[0], - }; - - let field2_expr = Expr::UnionAtIndex { - union_layout, - tag_id, - index: i as u64, - structure: operands[1], - }; - - let eq_call_expr = root - .call_specialized_op( - ident_ids, - ctx, - *layout, - root.arena.alloc([field1_sym, field2_sym]), - ) - .unwrap(); - - let eq_call_name = format!("eq_call_{}", i); - let eq_call_sym = root.create_symbol(ident_ids, &eq_call_name); - - stmt = Stmt::Let( - field1_sym, - field1_expr, - field_layouts[i], - root.arena.alloc( - // - Stmt::Let( - field2_sym, - field2_expr, - field_layouts[i], - root.arena.alloc( - // - Stmt::Let( - eq_call_sym, - eq_call_expr, - LAYOUT_BOOL, - root.arena.alloc( - // - if_false_return_false( - root, - eq_call_sym, - root.arena.alloc( - // - stmt, - ), - ), - ), - ), - ), - ), - ), - ) - } - stmt -} - -fn eq_boxed<'a>( - _root: &mut CodeGenHelp<'a>, - _ident_ids: &mut IdentIds, - _ctx: &mut Context<'a>, - _inner_layout: &'a Layout<'a>, -) -> Stmt<'a> { - todo!() -} - -/// List equality -/// We can't use `ListGetUnsafe` because it increments the refcount, and we don't want that. -/// Another way to dereference a heap pointer is to use `Expr::UnionAtIndex`. -/// To achieve this we use `PtrCast` to cast the element pointer to a "Box" layout. -/// Then we can increment the Box pointer in a loop, dereferencing it each time. -/// (An alternative approach would be to create a new lowlevel like ListPeekUnsafe.) -fn eq_list<'a>( - root: &mut CodeGenHelp<'a>, - ident_ids: &mut IdentIds, - ctx: &mut Context<'a>, - elem_layout: &Layout<'a>, -) -> Stmt<'a> { - use LowLevel::*; - let layout_isize = root.layout_isize; - let arena = root.arena; - - // A "Box" layout (heap pointer to a single list element) - let box_union_layout = UnionLayout::NonNullableUnwrapped(root.arena.alloc([*elem_layout])); - let box_layout = Layout::Union(box_union_layout); - - // Compare lengths - - let len_1 = root.create_symbol(ident_ids, "len_1"); - let len_2 = root.create_symbol(ident_ids, "len_2"); - let len_1_stmt = |next| let_lowlevel(arena, layout_isize, len_1, ListLen, &[ARG_1], next); - let len_2_stmt = |next| let_lowlevel(arena, layout_isize, len_2, ListLen, &[ARG_2], next); - - let eq_len = root.create_symbol(ident_ids, "eq_len"); - let eq_len_stmt = |next| let_lowlevel(arena, LAYOUT_BOOL, eq_len, Eq, &[len_1, len_2], next); - - // if lengths are equal... - - // get element pointers - let elements_1 = root.create_symbol(ident_ids, "elements_1"); - let elements_2 = root.create_symbol(ident_ids, "elements_2"); - let elements_1_expr = Expr::StructAtIndex { - index: 0, - field_layouts: root.arena.alloc([box_layout, layout_isize]), - structure: ARG_1, - }; - let elements_2_expr = Expr::StructAtIndex { - index: 0, - field_layouts: root.arena.alloc([box_layout, layout_isize]), - structure: ARG_2, - }; - let elements_1_stmt = |next| Stmt::Let(elements_1, elements_1_expr, box_layout, next); - let elements_2_stmt = |next| Stmt::Let(elements_2, elements_2_expr, box_layout, next); - - // Cast to integers - let start_1 = root.create_symbol(ident_ids, "start_1"); - let start_2 = root.create_symbol(ident_ids, "start_2"); - let start_1_stmt = - |next| let_lowlevel(arena, layout_isize, start_1, PtrCast, &[elements_1], next); - let start_2_stmt = - |next| let_lowlevel(arena, layout_isize, start_2, PtrCast, &[elements_2], next); - - // - // Loop initialisation - // - - // let size = literal int - let size = root.create_symbol(ident_ids, "size"); - let size_expr = Expr::Literal(Literal::Int( - (elem_layout.stack_size(root.target_info) as i128).to_ne_bytes(), - )); - let size_stmt = |next| Stmt::Let(size, size_expr, layout_isize, next); - - // let list_size = len_1 * size - let list_size = root.create_symbol(ident_ids, "list_size"); - let list_size_stmt = - |next| let_lowlevel(arena, layout_isize, list_size, NumMul, &[len_1, size], next); - - // let end_1 = start_1 + list_size - let end_1 = root.create_symbol(ident_ids, "end_1"); - let end_1_stmt = |next| { - let_lowlevel( - arena, - layout_isize, - end_1, - NumAdd, - &[start_1, list_size], - next, - ) - }; - - // - // Loop name & parameters - // - - let elems_loop = JoinPointId(root.create_symbol(ident_ids, "elems_loop")); - let addr1 = root.create_symbol(ident_ids, "addr1"); - let addr2 = root.create_symbol(ident_ids, "addr2"); - - let param_addr1 = Param { - symbol: addr1, - borrow: false, - layout: layout_isize, - }; - - let param_addr2 = Param { - symbol: addr2, - borrow: false, - layout: layout_isize, - }; - - // - // if we haven't reached the end yet... - // - - // Cast integers to box pointers - let box1 = root.create_symbol(ident_ids, "box1"); - let box2 = root.create_symbol(ident_ids, "box2"); - let box1_stmt = |next| let_lowlevel(arena, box_layout, box1, PtrCast, &[addr1], next); - let box2_stmt = |next| let_lowlevel(arena, box_layout, box2, PtrCast, &[addr2], next); - - // Dereference the box pointers to get the current elements - let elem1 = root.create_symbol(ident_ids, "elem1"); - let elem2 = root.create_symbol(ident_ids, "elem2"); - let elem1_expr = Expr::UnionAtIndex { - structure: box1, - union_layout: box_union_layout, - tag_id: 0, - index: 0, - }; - let elem2_expr = Expr::UnionAtIndex { - structure: box2, - union_layout: box_union_layout, - tag_id: 0, - index: 0, - }; - let elem1_stmt = |next| Stmt::Let(elem1, elem1_expr, *elem_layout, next); - let elem2_stmt = |next| Stmt::Let(elem2, elem2_expr, *elem_layout, next); - - // Compare the two current elements - let eq_elems = root.create_symbol(ident_ids, "eq_elems"); - let eq_elems_args = root.arena.alloc([elem1, elem2]); - let eq_elems_expr = root - .call_specialized_op(ident_ids, ctx, *elem_layout, eq_elems_args) - .unwrap(); - - let eq_elems_stmt = |next| Stmt::Let(eq_elems, eq_elems_expr, LAYOUT_BOOL, next); - - // If current elements are equal, loop back again - let next_1 = root.create_symbol(ident_ids, "next_1"); - let next_2 = root.create_symbol(ident_ids, "next_2"); - let next_1_stmt = - |next| let_lowlevel(arena, layout_isize, next_1, NumAdd, &[addr1, size], next); - let next_2_stmt = - |next| let_lowlevel(arena, layout_isize, next_2, NumAdd, &[addr2, size], next); - - let jump_back = Stmt::Jump(elems_loop, root.arena.alloc([next_1, next_2])); - - // - // Control flow - // - - let is_end = root.create_symbol(ident_ids, "is_end"); - let is_end_stmt = - |next| let_lowlevel(arena, LAYOUT_BOOL, is_end, NumGte, &[addr1, end_1], next); - - let if_elems_not_equal = if_false_return_false( - root, - eq_elems, - // else - root.arena.alloc( - // - next_1_stmt(root.arena.alloc( - // - next_2_stmt(root.arena.alloc( - // - jump_back, - )), - )), - ), - ); - - let if_end_of_list = Stmt::Switch { - cond_symbol: is_end, - cond_layout: LAYOUT_BOOL, - ret_layout: LAYOUT_BOOL, - branches: root - .arena - .alloc([(1, BranchInfo::None, Stmt::Ret(Symbol::BOOL_TRUE))]), - default_branch: ( - BranchInfo::None, - root.arena.alloc( - // - box1_stmt(root.arena.alloc( - // - box2_stmt(root.arena.alloc( - // - elem1_stmt(root.arena.alloc( - // - elem2_stmt(root.arena.alloc( - // - eq_elems_stmt(root.arena.alloc( - // - if_elems_not_equal, - )), - )), - )), - )), - )), - ), - ), - }; - - let joinpoint_loop = Stmt::Join { - id: elems_loop, - parameters: root.arena.alloc([param_addr1, param_addr2]), - body: root.arena.alloc( - // - is_end_stmt( - // - root.arena.alloc(if_end_of_list), - ), - ), - remainder: root - .arena - .alloc(Stmt::Jump(elems_loop, root.arena.alloc([start_1, start_2]))), - }; - - let if_different_lengths = if_false_return_false( - root, - eq_len, - // else - root.arena.alloc( - // - elements_1_stmt(root.arena.alloc( - // - elements_2_stmt(root.arena.alloc( - // - start_1_stmt(root.arena.alloc( - // - start_2_stmt(root.arena.alloc( - // - size_stmt(root.arena.alloc( - // - list_size_stmt(root.arena.alloc( - // - end_1_stmt(root.arena.alloc( - // - joinpoint_loop, - )), - )), - )), - )), - )), - )), - )), - ), - ); - - let pointers_else = len_1_stmt(root.arena.alloc( - // - len_2_stmt(root.arena.alloc( - // - eq_len_stmt(root.arena.alloc( - // - if_different_lengths, - )), - )), - )); - - if_pointers_equal_return_true( - root, - ident_ids, - [ARG_1, ARG_2], - root.arena.alloc(pointers_else), - ) -} diff --git a/compiler/mono/src/code_gen_help/mod.rs b/compiler/mono/src/code_gen_help/mod.rs deleted file mode 100644 index dadaafd9f1..0000000000 --- a/compiler/mono/src/code_gen_help/mod.rs +++ /dev/null @@ -1,548 +0,0 @@ -use bumpalo::collections::vec::Vec; -use bumpalo::Bump; -use roc_module::low_level::LowLevel; -use roc_module::symbol::{IdentIds, ModuleId, Symbol}; -use roc_target::TargetInfo; - -use crate::ir::{ - Call, CallSpecId, CallType, Expr, HostExposedLayouts, JoinPointId, ModifyRc, Proc, ProcLayout, - SelfRecursive, Stmt, UpdateModeId, -}; -use crate::layout::{Builtin, Layout, UnionLayout}; - -mod equality; -mod refcount; - -const LAYOUT_BOOL: Layout = Layout::Builtin(Builtin::Bool); -const LAYOUT_UNIT: Layout = Layout::UNIT; - -const ARG_1: Symbol = Symbol::ARG_1; -const ARG_2: Symbol = Symbol::ARG_2; - -/// "Infinite" reference count, for static values -/// Ref counts are encoded as negative numbers where isize::MIN represents 1 -pub const REFCOUNT_MAX: usize = 0; - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum HelperOp { - Inc, - Dec, - DecRef(JoinPointId), - Reset, - Eq, -} - -impl HelperOp { - fn is_decref(&self) -> bool { - matches!(self, Self::DecRef(_)) - } -} - -#[derive(Debug)] -struct Specialization<'a> { - op: HelperOp, - layout: Layout<'a>, - symbol: Symbol, - proc: Option>, -} - -#[derive(Debug)] -pub struct Context<'a> { - new_linker_data: Vec<'a, (Symbol, ProcLayout<'a>)>, - recursive_union: Option>, - op: HelperOp, -} - -/// Generate specialized helper procs for code gen -/// ---------------------------------------------- -/// -/// Some low level operations need specialized helper procs to traverse data structures at runtime. -/// This includes refcounting, hashing, and equality checks. -/// -/// For example, when checking List equality, we need to visit each element and compare them. -/// Depending on the type of the list elements, we may need to recurse deeper into each element. -/// For tag unions, we may need branches for different tag IDs, etc. -/// -/// This module creates specialized helper procs for all such operations and types used in the program. -/// -/// The backend drives the process, in two steps: -/// 1) When it sees the relevant node, it calls CodeGenHelp to get the replacement IR. -/// CodeGenHelp returns IR for a call to the helper proc, and remembers the specialization. -/// 2) After the backend has generated code for all user procs, it takes the IR for all of the -/// specialized helpers procs, and generates target code for them too. -/// -pub struct CodeGenHelp<'a> { - arena: &'a Bump, - home: ModuleId, - target_info: TargetInfo, - layout_isize: Layout<'a>, - union_refcount: UnionLayout<'a>, - specializations: Vec<'a, Specialization<'a>>, - debug_recursion_depth: usize, -} - -impl<'a> CodeGenHelp<'a> { - pub fn new(arena: &'a Bump, target_info: TargetInfo, home: ModuleId) -> Self { - let layout_isize = Layout::isize(target_info); - - // Refcount is a boxed isize. TODO: use the new Box layout when dev backends support it - let union_refcount = UnionLayout::NonNullableUnwrapped(arena.alloc([layout_isize])); - - CodeGenHelp { - arena, - home, - target_info, - layout_isize, - union_refcount, - specializations: Vec::with_capacity_in(16, arena), - debug_recursion_depth: 0, - } - } - - pub fn take_procs(&mut self) -> Vec<'a, Proc<'a>> { - let procs_iter = self - .specializations - .drain(0..) - .map(|spec| spec.proc.unwrap()); - Vec::from_iter_in(procs_iter, self.arena) - } - - // ============================================================================ - // - // CALL GENERATED PROCS - // - // ============================================================================ - - /// Expand a `Refcounting` node to a `Let` node that calls a specialized helper proc. - /// The helper procs themselves are to be generated later with `generate_procs` - pub fn expand_refcount_stmt( - &mut self, - ident_ids: &mut IdentIds, - layout: Layout<'a>, - modify: &ModifyRc, - following: &'a Stmt<'a>, - ) -> (&'a Stmt<'a>, Vec<'a, (Symbol, ProcLayout<'a>)>) { - if !refcount::is_rc_implemented_yet(&layout) { - // Just a warning, so we can decouple backend development from refcounting development. - // When we are closer to completion, we can change it to a panic. - println!( - "WARNING! MEMORY LEAK! Refcounting not yet implemented for Layout {:?}", - layout - ); - return (following, Vec::new_in(self.arena)); - } - - let op = match modify { - ModifyRc::Inc(..) => HelperOp::Inc, - ModifyRc::Dec(_) => HelperOp::Dec, - ModifyRc::DecRef(_) => { - let jp_decref = JoinPointId(self.create_symbol(ident_ids, "jp_decref")); - HelperOp::DecRef(jp_decref) - } - }; - - let mut ctx = Context { - new_linker_data: Vec::new_in(self.arena), - recursive_union: None, - op, - }; - - let rc_stmt = refcount::refcount_stmt(self, ident_ids, &mut ctx, layout, modify, following); - (rc_stmt, ctx.new_linker_data) - } - - pub fn call_reset_refcount( - &mut self, - ident_ids: &mut IdentIds, - layout: Layout<'a>, - argument: Symbol, - ) -> (Expr<'a>, Vec<'a, (Symbol, ProcLayout<'a>)>) { - let mut ctx = Context { - new_linker_data: Vec::new_in(self.arena), - recursive_union: None, - op: HelperOp::Reset, - }; - - let proc_name = self.find_or_create_proc(ident_ids, &mut ctx, layout); - - let arguments = self.arena.alloc([argument]); - let ret_layout = self.arena.alloc(layout); - let arg_layouts = self.arena.alloc([layout]); - let expr = Expr::Call(Call { - call_type: CallType::ByName { - name: proc_name, - ret_layout, - arg_layouts, - specialization_id: CallSpecId::BACKEND_DUMMY, - }, - arguments, - }); - - (expr, ctx.new_linker_data) - } - - /// Generate a refcount increment procedure, *without* a Call expression. - /// *This method should be rarely used* - only when the proc is to be called from Zig. - /// Otherwise you want to generate the Proc and the Call together, using another method. - pub fn gen_refcount_proc( - &mut self, - ident_ids: &mut IdentIds, - layout: Layout<'a>, - op: HelperOp, - ) -> (Symbol, Vec<'a, (Symbol, ProcLayout<'a>)>) { - let mut ctx = Context { - new_linker_data: Vec::new_in(self.arena), - recursive_union: None, - op, - }; - - let proc_name = self.find_or_create_proc(ident_ids, &mut ctx, layout); - - (proc_name, ctx.new_linker_data) - } - - /// Replace a generic `Lowlevel::Eq` call with a specialized helper proc. - /// The helper procs themselves are to be generated later with `generate_procs` - pub fn call_specialized_equals( - &mut self, - ident_ids: &mut IdentIds, - layout: &Layout<'a>, - arguments: &'a [Symbol], - ) -> (Expr<'a>, Vec<'a, (Symbol, ProcLayout<'a>)>) { - let mut ctx = Context { - new_linker_data: Vec::new_in(self.arena), - recursive_union: None, - op: HelperOp::Eq, - }; - - let expr = self - .call_specialized_op(ident_ids, &mut ctx, *layout, arguments) - .unwrap(); - - (expr, ctx.new_linker_data) - } - - // ============================================================================ - // - // CALL SPECIALIZED OP - // - // ============================================================================ - - fn call_specialized_op( - &mut self, - ident_ids: &mut IdentIds, - ctx: &mut Context<'a>, - called_layout: Layout<'a>, - arguments: &'a [Symbol], - ) -> Option> { - use HelperOp::*; - - // debug_assert!(self.debug_recursion_depth < 100); - self.debug_recursion_depth += 1; - - let layout = if matches!(called_layout, Layout::RecursivePointer) { - let union_layout = ctx.recursive_union.unwrap(); - Layout::Union(union_layout) - } else { - called_layout - }; - - if layout_needs_helper_proc(&layout, ctx.op) { - let proc_name = self.find_or_create_proc(ident_ids, ctx, layout); - - let (ret_layout, arg_layouts): (&'a Layout<'a>, &'a [Layout<'a>]) = { - let arg = self.replace_rec_ptr(ctx, layout); - match ctx.op { - Dec | DecRef(_) => (&LAYOUT_UNIT, self.arena.alloc([arg])), - Reset => (self.arena.alloc(layout), self.arena.alloc([layout])), - Inc => (&LAYOUT_UNIT, self.arena.alloc([arg, self.layout_isize])), - Eq => (&LAYOUT_BOOL, self.arena.alloc([arg, arg])), - } - }; - - Some(Expr::Call(Call { - call_type: CallType::ByName { - name: proc_name, - ret_layout, - arg_layouts, - specialization_id: CallSpecId::BACKEND_DUMMY, - }, - arguments, - })) - } else if ctx.op == HelperOp::Eq { - Some(Expr::Call(Call { - call_type: CallType::LowLevel { - op: LowLevel::Eq, - update_mode: UpdateModeId::BACKEND_DUMMY, - }, - arguments, - })) - } else { - None - } - } - - fn find_or_create_proc( - &mut self, - ident_ids: &mut IdentIds, - ctx: &mut Context<'a>, - layout: Layout<'a>, - ) -> Symbol { - use HelperOp::*; - - let layout = self.replace_rec_ptr(ctx, layout); - - let found = self - .specializations - .iter() - .find(|spec| spec.op == ctx.op && spec.layout == layout); - - if let Some(spec) = found { - return spec.symbol; - } - - // Procs can be recursive, so we need to create the symbol before the body is complete - // But with nested recursion, that means Symbols and Procs can end up in different orders. - // We want the same order, especially for function indices in Wasm. So create an empty slot and fill it in later. - let (proc_symbol, proc_layout) = self.create_proc_symbol(ident_ids, ctx, &layout); - ctx.new_linker_data.push((proc_symbol, proc_layout)); - let spec_index = self.specializations.len(); - self.specializations.push(Specialization { - op: ctx.op, - layout, - symbol: proc_symbol, - proc: None, - }); - - // Recursively generate the body of the Proc and sub-procs - let (ret_layout, body) = match ctx.op { - Inc | Dec | DecRef(_) => ( - LAYOUT_UNIT, - refcount::refcount_generic(self, ident_ids, ctx, layout, Symbol::ARG_1), - ), - Reset => ( - layout, - refcount::refcount_reset_proc_body(self, ident_ids, ctx, layout, Symbol::ARG_1), - ), - Eq => ( - LAYOUT_BOOL, - equality::eq_generic(self, ident_ids, ctx, layout), - ), - }; - - let args: &'a [(Layout<'a>, Symbol)] = { - let roc_value = (layout, ARG_1); - match ctx.op { - Inc => { - let inc_amount = (self.layout_isize, ARG_2); - self.arena.alloc([roc_value, inc_amount]) - } - Dec | DecRef(_) | Reset => self.arena.alloc([roc_value]), - Eq => self.arena.alloc([roc_value, (layout, ARG_2)]), - } - }; - - self.specializations[spec_index].proc = Some(Proc { - name: proc_symbol, - args, - body, - closure_data_layout: None, - ret_layout, - is_self_recursive: SelfRecursive::NotSelfRecursive, - must_own_arguments: false, - host_exposed_layouts: HostExposedLayouts::NotHostExposed, - }); - - proc_symbol - } - - fn create_proc_symbol( - &self, - ident_ids: &mut IdentIds, - ctx: &mut Context<'a>, - layout: &Layout<'a>, - ) -> (Symbol, ProcLayout<'a>) { - let debug_name = format!( - "#help{}_{:?}_{:?}", - self.specializations.len(), - ctx.op, - layout - ) - .replace("Builtin", ""); - let proc_symbol: Symbol = self.create_symbol(ident_ids, &debug_name); - - let proc_layout = match ctx.op { - HelperOp::Inc => ProcLayout { - arguments: self.arena.alloc([*layout, self.layout_isize]), - result: LAYOUT_UNIT, - }, - HelperOp::Dec => ProcLayout { - arguments: self.arena.alloc([*layout]), - result: LAYOUT_UNIT, - }, - HelperOp::Reset => ProcLayout { - arguments: self.arena.alloc([*layout]), - result: *layout, - }, - HelperOp::DecRef(_) => unreachable!("No generated Proc for DecRef"), - HelperOp::Eq => ProcLayout { - arguments: self.arena.alloc([*layout, *layout]), - result: LAYOUT_BOOL, - }, - }; - - (proc_symbol, proc_layout) - } - - fn create_symbol(&self, ident_ids: &mut IdentIds, debug_name: &str) -> Symbol { - let ident_id = ident_ids.add_str(debug_name); - Symbol::new(self.home, ident_id) - } - - // When creating or looking up Specializations, we need to replace RecursivePointer - // with the particular Union layout it represents at this point in the tree. - // For example if a program uses `RoseTree a : [Tree a (List (RoseTree a))]` - // then it could have both `RoseTree I64` and `RoseTree Str`. In this case it - // needs *two* specializations for `List(RecursivePointer)`, not just one. - fn replace_rec_ptr(&self, ctx: &Context<'a>, layout: Layout<'a>) -> Layout<'a> { - match layout { - Layout::Builtin(Builtin::Dict(k, v)) => Layout::Builtin(Builtin::Dict( - self.arena.alloc(self.replace_rec_ptr(ctx, *k)), - self.arena.alloc(self.replace_rec_ptr(ctx, *v)), - )), - - Layout::Builtin(Builtin::Set(k)) => Layout::Builtin(Builtin::Set( - self.arena.alloc(self.replace_rec_ptr(ctx, *k)), - )), - - Layout::Builtin(Builtin::List(v)) => Layout::Builtin(Builtin::List( - self.arena.alloc(self.replace_rec_ptr(ctx, *v)), - )), - - Layout::Builtin(_) => layout, - - Layout::Struct { - field_layouts, - field_order_hash, - } => { - let new_fields_iter = field_layouts.iter().map(|f| self.replace_rec_ptr(ctx, *f)); - Layout::Struct { - field_layouts: self.arena.alloc_slice_fill_iter(new_fields_iter), - field_order_hash, - } - } - - Layout::Union(UnionLayout::NonRecursive(tags)) => { - let mut new_tags = Vec::with_capacity_in(tags.len(), self.arena); - for fields in tags { - let mut new_fields = Vec::with_capacity_in(fields.len(), self.arena); - for field in fields.iter() { - new_fields.push(self.replace_rec_ptr(ctx, *field)) - } - new_tags.push(new_fields.into_bump_slice()); - } - Layout::Union(UnionLayout::NonRecursive(new_tags.into_bump_slice())) - } - - Layout::Union(_) => { - // we always fully unroll recursive types. That means tha when we find a - // recursive tag union we can replace it with the layout - layout - } - - Layout::Boxed(inner) => self.replace_rec_ptr(ctx, *inner), - - Layout::LambdaSet(lambda_set) => { - self.replace_rec_ptr(ctx, lambda_set.runtime_representation()) - } - - // This line is the whole point of the function - Layout::RecursivePointer => Layout::Union(ctx.recursive_union.unwrap()), - } - } - - fn union_tail_recursion_fields( - &self, - union: UnionLayout<'a>, - ) -> (bool, Vec<'a, Option>) { - use UnionLayout::*; - match union { - NonRecursive(_) => return (false, bumpalo::vec![in self.arena]), - - Recursive(tags) => self.union_tail_recursion_fields_help(tags), - - NonNullableUnwrapped(field_layouts) => { - self.union_tail_recursion_fields_help(&[field_layouts]) - } - - NullableWrapped { - other_tags: tags, .. - } => self.union_tail_recursion_fields_help(tags), - - NullableUnwrapped { other_fields, .. } => { - self.union_tail_recursion_fields_help(&[other_fields]) - } - } - } - - fn union_tail_recursion_fields_help( - &self, - tags: &[&'a [Layout<'a>]], - ) -> (bool, Vec<'a, Option>) { - let mut can_use_tailrec = false; - let mut tailrec_indices = Vec::with_capacity_in(tags.len(), self.arena); - - for fields in tags.iter() { - let found_index = fields - .iter() - .position(|f| matches!(f, Layout::RecursivePointer)); - tailrec_indices.push(found_index); - can_use_tailrec |= found_index.is_some(); - } - - (can_use_tailrec, tailrec_indices) - } -} - -fn let_lowlevel<'a>( - arena: &'a Bump, - result_layout: Layout<'a>, - result: Symbol, - op: LowLevel, - arguments: &[Symbol], - next: &'a Stmt<'a>, -) -> Stmt<'a> { - Stmt::Let( - result, - Expr::Call(Call { - call_type: CallType::LowLevel { - op, - update_mode: UpdateModeId::BACKEND_DUMMY, - }, - arguments: arena.alloc_slice_copy(arguments), - }), - result_layout, - next, - ) -} - -fn layout_needs_helper_proc(layout: &Layout, op: HelperOp) -> bool { - match layout { - Layout::Builtin(Builtin::Int(_) | Builtin::Float(_) | Builtin::Bool | Builtin::Decimal) => { - false - } - Layout::Builtin(Builtin::Str) => { - // Str type can use either Zig functions or generated IR, since it's not generic. - // Eq uses a Zig function, refcount uses generated IR. - // Both are fine, they were just developed at different times. - matches!(op, HelperOp::Inc | HelperOp::Dec | HelperOp::DecRef(_)) - } - Layout::Builtin(Builtin::Dict(_, _) | Builtin::Set(_) | Builtin::List(_)) => true, - Layout::Struct { .. } => true, // note: we do generate a helper for Unit, with just a Stmt::Ret - Layout::Union(UnionLayout::NonRecursive(tags)) => !tags.is_empty(), - Layout::Union(_) => true, - Layout::LambdaSet(_) => true, - Layout::RecursivePointer => false, - Layout::Boxed(_) => true, - } -} diff --git a/compiler/mono/src/code_gen_help/refcount.rs b/compiler/mono/src/code_gen_help/refcount.rs deleted file mode 100644 index e3a6282ce0..0000000000 --- a/compiler/mono/src/code_gen_help/refcount.rs +++ /dev/null @@ -1,1467 +0,0 @@ -use bumpalo::collections::vec::Vec; -use roc_builtins::bitcode::IntWidth; -use roc_module::low_level::{LowLevel, LowLevel::*}; -use roc_module::symbol::{IdentIds, Symbol}; -use roc_target::PtrWidth; - -use crate::code_gen_help::let_lowlevel; -use crate::ir::{ - BranchInfo, Call, CallType, Expr, JoinPointId, Literal, ModifyRc, Param, Stmt, UpdateModeId, -}; -use crate::layout::{Builtin, Layout, TagIdIntType, UnionLayout}; - -use super::{CodeGenHelp, Context, HelperOp}; - -const LAYOUT_BOOL: Layout = Layout::Builtin(Builtin::Bool); -const LAYOUT_UNIT: Layout = Layout::UNIT; -const LAYOUT_U32: Layout = Layout::Builtin(Builtin::Int(IntWidth::U32)); - -// TODO: Replace usages with root.union_refcount -const LAYOUT_PTR: Layout = Layout::RecursivePointer; - -pub fn refcount_stmt<'a>( - root: &mut CodeGenHelp<'a>, - ident_ids: &mut IdentIds, - ctx: &mut Context<'a>, - layout: Layout<'a>, - modify: &ModifyRc, - following: &'a Stmt<'a>, -) -> &'a Stmt<'a> { - let arena = root.arena; - - match modify { - ModifyRc::Inc(structure, amount) => { - let layout_isize = root.layout_isize; - - // Define a constant for the amount to increment - let amount_sym = root.create_symbol(ident_ids, "amount"); - let amount_expr = Expr::Literal(Literal::Int((*amount as i128).to_ne_bytes())); - let amount_stmt = |next| Stmt::Let(amount_sym, amount_expr, layout_isize, next); - - // Call helper proc, passing the Roc structure and constant amount - let call_result_empty = root.create_symbol(ident_ids, "call_result_empty"); - let call_expr = root - .call_specialized_op( - ident_ids, - ctx, - layout, - arena.alloc([*structure, amount_sym]), - ) - .unwrap(); - - let call_stmt = Stmt::Let(call_result_empty, call_expr, LAYOUT_UNIT, following); - arena.alloc(amount_stmt(arena.alloc(call_stmt))) - } - - ModifyRc::Dec(structure) => { - // Call helper proc, passing the Roc structure - let call_result_empty = root.create_symbol(ident_ids, "call_result_empty"); - let call_expr = root - .call_specialized_op(ident_ids, ctx, layout, arena.alloc([*structure])) - .unwrap(); - let call_stmt = Stmt::Let(call_result_empty, call_expr, LAYOUT_UNIT, following); - arena.alloc(call_stmt) - } - - ModifyRc::DecRef(structure) => { - match layout { - // Str has no children, so we might as well do what we normally do and call the helper. - Layout::Builtin(Builtin::Str) => { - ctx.op = HelperOp::Dec; - refcount_stmt(root, ident_ids, ctx, layout, modify, following) - } - - // Struct and non-recursive Unions are stack-only, so DecRef is a no-op - Layout::Struct { .. } => following, - Layout::Union(UnionLayout::NonRecursive(_)) => following, - - // Inline the refcounting code instead of making a function. Don't iterate fields, - // and replace any return statements with jumps to the `following` statement. - _ => match ctx.op { - HelperOp::DecRef(jp_decref) => { - let rc_stmt = refcount_generic(root, ident_ids, ctx, layout, *structure); - let join = Stmt::Join { - id: jp_decref, - parameters: &[], - body: following, - remainder: arena.alloc(rc_stmt), - }; - arena.alloc(join) - } - _ => unreachable!(), - }, - } - } - } -} - -pub fn refcount_generic<'a>( - root: &mut CodeGenHelp<'a>, - ident_ids: &mut IdentIds, - ctx: &mut Context<'a>, - layout: Layout<'a>, - structure: Symbol, -) -> Stmt<'a> { - debug_assert!(is_rc_implemented_yet(&layout)); - let rc_todo = || todo!("Please update is_rc_implemented_yet for `{:?}`", layout); - - match layout { - Layout::Builtin(Builtin::Int(_) | Builtin::Float(_) | Builtin::Bool | Builtin::Decimal) => { - // Generate a dummy function that immediately returns Unit - // Some higher-order Zig builtins *always* call an RC function on List elements. - rc_return_stmt(root, ident_ids, ctx) - } - Layout::Builtin(Builtin::Str) => refcount_str(root, ident_ids, ctx), - Layout::Builtin(Builtin::List(elem_layout)) => { - refcount_list(root, ident_ids, ctx, &layout, elem_layout, structure) - } - Layout::Builtin(Builtin::Dict(_, _) | Builtin::Set(_)) => rc_todo(), - Layout::Struct { field_layouts, .. } => { - refcount_struct(root, ident_ids, ctx, field_layouts, structure) - } - Layout::Union(union_layout) => { - refcount_union(root, ident_ids, ctx, union_layout, structure) - } - Layout::LambdaSet(lambda_set) => { - let runtime_layout = lambda_set.runtime_representation(); - refcount_generic(root, ident_ids, ctx, runtime_layout, structure) - } - Layout::RecursivePointer => unreachable!( - "We should never call a refcounting helper on a RecursivePointer layout directly" - ), - Layout::Boxed(_) => rc_todo(), - } -} - -pub fn refcount_reset_proc_body<'a>( - root: &mut CodeGenHelp<'a>, - ident_ids: &mut IdentIds, - ctx: &mut Context<'a>, - layout: Layout<'a>, - structure: Symbol, -) -> Stmt<'a> { - let rc_ptr = root.create_symbol(ident_ids, "rc_ptr"); - let rc = root.create_symbol(ident_ids, "rc"); - let refcount_1 = root.create_symbol(ident_ids, "refcount_1"); - let is_unique = root.create_symbol(ident_ids, "is_unique"); - - let union_layout = match layout { - Layout::Union(u) => u, - _ => unimplemented!("Reset is only implemented for UnionLayout"), - }; - - // Whenever we recurse into a child layout we will want to Decrement - ctx.op = HelperOp::Dec; - ctx.recursive_union = Some(union_layout); - - // Reset structure is unique. Decrement its children and return a pointer to the allocation. - let then_stmt = { - use UnionLayout::*; - - let tag_layouts; - let mut null_id = None; - match union_layout { - NonRecursive(tags) => { - tag_layouts = tags; - } - Recursive(tags) => { - tag_layouts = tags; - } - NonNullableUnwrapped(field_layouts) => { - tag_layouts = root.arena.alloc([field_layouts]); - } - NullableWrapped { - other_tags: tags, - nullable_id, - } => { - null_id = Some(nullable_id); - tag_layouts = tags; - } - NullableUnwrapped { - other_fields, - nullable_id, - } => { - null_id = Some(nullable_id as TagIdIntType); - tag_layouts = root.arena.alloc([other_fields]); - } - }; - - let tag_id_layout = union_layout.tag_id_layout(); - - let tag_id_sym = root.create_symbol(ident_ids, "tag_id"); - let tag_id_stmt = |next| { - Stmt::Let( - tag_id_sym, - Expr::GetTagId { - structure, - union_layout, - }, - tag_id_layout, - next, - ) - }; - - let rc_contents_stmt = refcount_union_contents( - root, - ident_ids, - ctx, - union_layout, - tag_layouts, - null_id, - structure, - tag_id_sym, - tag_id_layout, - Stmt::Ret(structure), - ); - - tag_id_stmt(root.arena.alloc( - // - rc_contents_stmt, - )) - }; - - // Reset structure is not unique. Decrement it and return a NULL pointer. - let else_stmt = { - let decrement_unit = root.create_symbol(ident_ids, "decrement_unit"); - let decrement_expr = root - .call_specialized_op(ident_ids, ctx, layout, root.arena.alloc([structure])) - .unwrap(); - let decrement_stmt = |next| Stmt::Let(decrement_unit, decrement_expr, LAYOUT_UNIT, next); - - // Zero - let zero = root.create_symbol(ident_ids, "zero"); - let zero_expr = Expr::Literal(Literal::Int(0i128.to_ne_bytes())); - let zero_stmt = |next| Stmt::Let(zero, zero_expr, root.layout_isize, next); - - // Null pointer with union layout - let null = root.create_symbol(ident_ids, "null"); - let null_stmt = - |next| let_lowlevel(root.arena, root.layout_isize, null, PtrCast, &[zero], next); - - decrement_stmt(root.arena.alloc( - // - zero_stmt(root.arena.alloc( - // - null_stmt(root.arena.alloc( - // - Stmt::Ret(null), - )), - )), - )) - }; - - let if_stmt = Stmt::Switch { - cond_symbol: is_unique, - cond_layout: LAYOUT_BOOL, - branches: root.arena.alloc([(1, BranchInfo::None, then_stmt)]), - default_branch: (BranchInfo::None, root.arena.alloc(else_stmt)), - ret_layout: layout, - }; - - // Uniqueness test - let is_unique_stmt = { - let_lowlevel( - root.arena, - LAYOUT_BOOL, - is_unique, - Eq, - &[rc, refcount_1], - root.arena.alloc(if_stmt), - ) - }; - - // Constant for unique refcount - let refcount_1_encoded = match root.target_info.ptr_width() { - PtrWidth::Bytes4 => i32::MIN as i128, - PtrWidth::Bytes8 => i64::MIN as i128, - } - .to_ne_bytes(); - let refcount_1_expr = Expr::Literal(Literal::Int(refcount_1_encoded)); - let refcount_1_stmt = Stmt::Let( - refcount_1, - refcount_1_expr, - root.layout_isize, - root.arena.alloc(is_unique_stmt), - ); - - // Refcount value - let rc_expr = Expr::UnionAtIndex { - structure: rc_ptr, - tag_id: 0, - union_layout: root.union_refcount, - index: 0, - }; - let rc_stmt = Stmt::Let( - rc, - rc_expr, - root.layout_isize, - root.arena.alloc(refcount_1_stmt), - ); - - // Refcount pointer - let rc_ptr_stmt = { - rc_ptr_from_data_ptr( - root, - ident_ids, - structure, - rc_ptr, - union_layout.stores_tag_id_in_pointer(root.target_info), - root.arena.alloc(rc_stmt), - ) - }; - - rc_ptr_stmt -} - -// Check if refcounting is implemented yet. In the long term, this will be deleted. -// In the short term, it helps us to skip refcounting and let it leak, so we can make -// progress incrementally. Kept in sync with generate_procs using assertions. -pub fn is_rc_implemented_yet(layout: &Layout) -> bool { - use UnionLayout::*; - - match layout { - Layout::Builtin(Builtin::Dict(..) | Builtin::Set(_)) => false, - Layout::Builtin(Builtin::List(elem_layout)) => is_rc_implemented_yet(elem_layout), - Layout::Builtin(_) => true, - Layout::Struct { field_layouts, .. } => field_layouts.iter().all(is_rc_implemented_yet), - Layout::Union(union_layout) => match union_layout { - NonRecursive(tags) => tags - .iter() - .all(|fields| fields.iter().all(is_rc_implemented_yet)), - Recursive(tags) => tags - .iter() - .all(|fields| fields.iter().all(is_rc_implemented_yet)), - NonNullableUnwrapped(fields) => fields.iter().all(is_rc_implemented_yet), - NullableWrapped { other_tags, .. } => other_tags - .iter() - .all(|fields| fields.iter().all(is_rc_implemented_yet)), - NullableUnwrapped { other_fields, .. } => { - other_fields.iter().all(is_rc_implemented_yet) - } - }, - Layout::LambdaSet(lambda_set) => { - is_rc_implemented_yet(&lambda_set.runtime_representation()) - } - Layout::RecursivePointer => true, - Layout::Boxed(_) => false, - } -} - -fn rc_return_stmt<'a>( - root: &CodeGenHelp<'a>, - ident_ids: &mut IdentIds, - ctx: &mut Context<'a>, -) -> Stmt<'a> { - if let HelperOp::DecRef(jp_decref) = ctx.op { - Stmt::Jump(jp_decref, &[]) - } else { - let unit = root.create_symbol(ident_ids, "unit"); - let ret_stmt = root.arena.alloc(Stmt::Ret(unit)); - Stmt::Let(unit, Expr::Struct(&[]), LAYOUT_UNIT, ret_stmt) - } -} - -fn refcount_args<'a>(root: &CodeGenHelp<'a>, ctx: &Context<'a>, structure: Symbol) -> &'a [Symbol] { - if ctx.op == HelperOp::Inc { - // second argument is always `amount`, passed down through the call stack - root.arena.alloc([structure, Symbol::ARG_2]) - } else { - root.arena.alloc([structure]) - } -} - -// Subtract a constant from a pointer to find the refcount -// Also does some type casting, so that we have different Symbols and Layouts -// for the 'pointer' and 'integer' versions of the address. -// This helps to avoid issues with the backends Symbol->Layout mapping. -pub fn rc_ptr_from_data_ptr<'a>( - root: &CodeGenHelp<'a>, - ident_ids: &mut IdentIds, - structure: Symbol, - rc_ptr_sym: Symbol, - mask_lower_bits: bool, - following: &'a Stmt<'a>, -) -> Stmt<'a> { - use std::ops::Neg; - - // Typecast the structure pointer to an integer - // Backends expect a number Layout to choose the right "subtract" instruction - let addr_sym = root.create_symbol(ident_ids, "addr"); - let addr_expr = Expr::Call(Call { - call_type: CallType::LowLevel { - op: LowLevel::PtrCast, - update_mode: UpdateModeId::BACKEND_DUMMY, - }, - arguments: root.arena.alloc([structure]), - }); - let addr_stmt = |next| Stmt::Let(addr_sym, addr_expr, root.layout_isize, next); - - // Mask for lower bits (for tag union id) - let mask_sym = root.create_symbol(ident_ids, "mask"); - let mask_expr = Expr::Literal(Literal::Int( - (root.target_info.ptr_width() as i128).neg().to_ne_bytes(), - )); - let mask_stmt = |next| Stmt::Let(mask_sym, mask_expr, root.layout_isize, next); - - let masked_sym = root.create_symbol(ident_ids, "masked"); - let and_expr = Expr::Call(Call { - call_type: CallType::LowLevel { - op: LowLevel::And, - update_mode: UpdateModeId::BACKEND_DUMMY, - }, - arguments: root.arena.alloc([addr_sym, mask_sym]), - }); - let and_stmt = |next| Stmt::Let(masked_sym, and_expr, root.layout_isize, next); - - // Pointer size constant - let ptr_size_sym = root.create_symbol(ident_ids, "ptr_size"); - let ptr_size_expr = Expr::Literal(Literal::Int( - (root.target_info.ptr_width() as i128).to_ne_bytes(), - )); - let ptr_size_stmt = |next| Stmt::Let(ptr_size_sym, ptr_size_expr, root.layout_isize, next); - - // Refcount address - let rc_addr_sym = root.create_symbol(ident_ids, "rc_addr"); - let sub_expr = Expr::Call(Call { - call_type: CallType::LowLevel { - op: LowLevel::NumSub, - update_mode: UpdateModeId::BACKEND_DUMMY, - }, - arguments: root.arena.alloc([ - if mask_lower_bits { - masked_sym - } else { - addr_sym - }, - ptr_size_sym, - ]), - }); - let sub_stmt = |next| Stmt::Let(rc_addr_sym, sub_expr, root.layout_isize, next); - - // Typecast the refcount address from integer to pointer - let cast_expr = Expr::Call(Call { - call_type: CallType::LowLevel { - op: LowLevel::PtrCast, - update_mode: UpdateModeId::BACKEND_DUMMY, - }, - arguments: root.arena.alloc([rc_addr_sym]), - }); - let cast_stmt = |next| Stmt::Let(rc_ptr_sym, cast_expr, LAYOUT_PTR, next); - - if mask_lower_bits { - addr_stmt(root.arena.alloc( - // - mask_stmt(root.arena.alloc( - // - and_stmt(root.arena.alloc( - // - ptr_size_stmt(root.arena.alloc( - // - sub_stmt(root.arena.alloc( - // - cast_stmt(root.arena.alloc( - // - following, - )), - )), - )), - )), - )), - )) - } else { - addr_stmt(root.arena.alloc( - // - ptr_size_stmt(root.arena.alloc( - // - sub_stmt(root.arena.alloc( - // - cast_stmt(root.arena.alloc( - // - following, - )), - )), - )), - )) - } -} - -fn modify_refcount<'a>( - root: &CodeGenHelp<'a>, - ident_ids: &mut IdentIds, - ctx: &mut Context<'a>, - rc_ptr: Symbol, - alignment: u32, - following: &'a Stmt<'a>, -) -> Stmt<'a> { - // Call the relevant Zig lowlevel to actually modify the refcount - let zig_call_result = root.create_symbol(ident_ids, "zig_call_result"); - match ctx.op { - HelperOp::Inc => { - let zig_call_expr = Expr::Call(Call { - call_type: CallType::LowLevel { - op: LowLevel::RefCountInc, - update_mode: UpdateModeId::BACKEND_DUMMY, - }, - arguments: root.arena.alloc([rc_ptr, Symbol::ARG_2]), - }); - Stmt::Let(zig_call_result, zig_call_expr, LAYOUT_UNIT, following) - } - - HelperOp::Dec | HelperOp::DecRef(_) => { - let alignment_sym = root.create_symbol(ident_ids, "alignment"); - let alignment_expr = Expr::Literal(Literal::Int((alignment as i128).to_ne_bytes())); - let alignment_stmt = |next| Stmt::Let(alignment_sym, alignment_expr, LAYOUT_U32, next); - - let zig_call_expr = Expr::Call(Call { - call_type: CallType::LowLevel { - op: LowLevel::RefCountDec, - update_mode: UpdateModeId::BACKEND_DUMMY, - }, - arguments: root.arena.alloc([rc_ptr, alignment_sym]), - }); - let zig_call_stmt = Stmt::Let(zig_call_result, zig_call_expr, LAYOUT_UNIT, following); - - alignment_stmt(root.arena.alloc( - // - zig_call_stmt, - )) - } - - _ => unreachable!(), - } -} - -/// Generate a procedure to modify the reference count of a Str -fn refcount_str<'a>( - root: &CodeGenHelp<'a>, - ident_ids: &mut IdentIds, - ctx: &mut Context<'a>, -) -> Stmt<'a> { - let string = Symbol::ARG_1; - let layout_isize = root.layout_isize; - let field_layouts = root.arena.alloc([LAYOUT_PTR, layout_isize, layout_isize]); - - // Get the last word as a signed int - let last_word = root.create_symbol(ident_ids, "last_word"); - let last_word_expr = Expr::StructAtIndex { - index: 2, - field_layouts, - structure: string, - }; - let last_word_stmt = |next| Stmt::Let(last_word, last_word_expr, layout_isize, next); - - // Zero - let zero = root.create_symbol(ident_ids, "zero"); - let zero_expr = Expr::Literal(Literal::Int(0i128.to_ne_bytes())); - let zero_stmt = |next| Stmt::Let(zero, zero_expr, layout_isize, next); - - // is_big_str = (last_word >= 0); - // Treat last word as isize so that the small string flag is the same as the sign bit - let is_big_str = root.create_symbol(ident_ids, "is_big_str"); - let is_big_str_expr = Expr::Call(Call { - call_type: CallType::LowLevel { - op: LowLevel::NumGte, - update_mode: UpdateModeId::BACKEND_DUMMY, - }, - arguments: root.arena.alloc([last_word, zero]), - }); - let is_big_str_stmt = |next| Stmt::Let(is_big_str, is_big_str_expr, LAYOUT_BOOL, next); - - // Get the pointer to the string elements - let elements = root.create_symbol(ident_ids, "elements"); - let elements_expr = Expr::StructAtIndex { - index: 0, - field_layouts, - structure: string, - }; - let elements_stmt = |next| Stmt::Let(elements, elements_expr, layout_isize, next); - - // A pointer to the refcount value itself - let rc_ptr = root.create_symbol(ident_ids, "rc_ptr"); - let alignment = root.target_info.ptr_width() as u32; - - let ret_unit_stmt = rc_return_stmt(root, ident_ids, ctx); - let mod_rc_stmt = modify_refcount( - root, - ident_ids, - ctx, - rc_ptr, - alignment, - root.arena.alloc(ret_unit_stmt), - ); - - // Generate an `if` to skip small strings but modify big strings - let then_branch = elements_stmt(root.arena.alloc( - // - rc_ptr_from_data_ptr( - root, - ident_ids, - elements, - rc_ptr, - false, - root.arena.alloc( - // - mod_rc_stmt, - ), - ), - )); - - let if_stmt = Stmt::Switch { - cond_symbol: is_big_str, - cond_layout: LAYOUT_BOOL, - branches: root.arena.alloc([(1, BranchInfo::None, then_branch)]), - default_branch: ( - BranchInfo::None, - root.arena.alloc(rc_return_stmt(root, ident_ids, ctx)), - ), - ret_layout: LAYOUT_UNIT, - }; - - // Combine the statements in sequence - last_word_stmt(root.arena.alloc( - // - zero_stmt(root.arena.alloc( - // - is_big_str_stmt(root.arena.alloc( - // - if_stmt, - )), - )), - )) -} - -fn refcount_list<'a>( - root: &mut CodeGenHelp<'a>, - ident_ids: &mut IdentIds, - ctx: &mut Context<'a>, - layout: &Layout, - elem_layout: &'a Layout, - structure: Symbol, -) -> Stmt<'a> { - let layout_isize = root.layout_isize; - let arena = root.arena; - - // A "Box" layout (heap pointer to a single list element) - let box_union_layout = UnionLayout::NonNullableUnwrapped(arena.alloc([*elem_layout])); - let box_layout = Layout::Union(box_union_layout); - - // - // Check if the list is empty - // - - let len = root.create_symbol(ident_ids, "len"); - let len_stmt = |next| let_lowlevel(arena, layout_isize, len, ListLen, &[structure], next); - - // Zero - let zero = root.create_symbol(ident_ids, "zero"); - let zero_expr = Expr::Literal(Literal::Int(0i128.to_ne_bytes())); - let zero_stmt = |next| Stmt::Let(zero, zero_expr, layout_isize, next); - - // let is_empty = lowlevel Eq len zero - let is_empty = root.create_symbol(ident_ids, "is_empty"); - let is_empty_expr = Expr::Call(Call { - call_type: CallType::LowLevel { - op: LowLevel::Eq, - update_mode: UpdateModeId::BACKEND_DUMMY, - }, - arguments: root.arena.alloc([len, zero]), - }); - let is_empty_stmt = |next| Stmt::Let(is_empty, is_empty_expr, LAYOUT_BOOL, next); - - // get elements pointer - let elements = root.create_symbol(ident_ids, "elements"); - let elements_expr = Expr::StructAtIndex { - index: 0, - field_layouts: arena.alloc([box_layout, layout_isize, layout_isize]), - structure, - }; - let elements_stmt = |next| Stmt::Let(elements, elements_expr, box_layout, next); - - // - // modify refcount of the list and its elements - // (elements first, to avoid use-after-free for Dec) - // - - let rc_ptr = root.create_symbol(ident_ids, "rc_ptr"); - let alignment = layout.alignment_bytes(root.target_info); - - let ret_stmt = rc_return_stmt(root, ident_ids, ctx); - let modify_list = modify_refcount( - root, - ident_ids, - ctx, - rc_ptr, - alignment, - arena.alloc(ret_stmt), - ); - - let get_rc_and_modify_list = rc_ptr_from_data_ptr( - root, - ident_ids, - elements, - rc_ptr, - false, - arena.alloc(modify_list), - ); - - let modify_elems_and_list = if elem_layout.is_refcounted() && !ctx.op.is_decref() { - refcount_list_elems( - root, - ident_ids, - ctx, - elem_layout, - LAYOUT_UNIT, - box_union_layout, - len, - elements, - get_rc_and_modify_list, - ) - } else { - get_rc_and_modify_list - }; - - // - // Do nothing if the list is empty - // - - let non_empty_branch = root.arena.alloc( - // - elements_stmt(root.arena.alloc( - // - modify_elems_and_list, - )), - ); - - let if_stmt = Stmt::Switch { - cond_symbol: is_empty, - cond_layout: LAYOUT_BOOL, - branches: root - .arena - .alloc([(1, BranchInfo::None, rc_return_stmt(root, ident_ids, ctx))]), - default_branch: (BranchInfo::None, non_empty_branch), - ret_layout: LAYOUT_UNIT, - }; - - len_stmt(arena.alloc( - // - zero_stmt(arena.alloc( - // - is_empty_stmt(arena.alloc( - // - if_stmt, - )), - )), - )) -} - -#[allow(clippy::too_many_arguments)] -fn refcount_list_elems<'a>( - root: &mut CodeGenHelp<'a>, - ident_ids: &mut IdentIds, - ctx: &mut Context<'a>, - elem_layout: &Layout<'a>, - ret_layout: Layout<'a>, - box_union_layout: UnionLayout<'a>, - length: Symbol, - elements: Symbol, - following: Stmt<'a>, -) -> Stmt<'a> { - use LowLevel::*; - let layout_isize = root.layout_isize; - let arena = root.arena; - - // Cast to integer - let start = root.create_symbol(ident_ids, "start"); - let start_stmt = |next| let_lowlevel(arena, layout_isize, start, PtrCast, &[elements], next); - - // - // Loop initialisation - // - - // let size = literal int - let elem_size = root.create_symbol(ident_ids, "elem_size"); - let elem_size_expr = Expr::Literal(Literal::Int( - (elem_layout.stack_size(root.target_info) as i128).to_ne_bytes(), - )); - let elem_size_stmt = |next| Stmt::Let(elem_size, elem_size_expr, layout_isize, next); - - // let list_size = len * size - let list_size = root.create_symbol(ident_ids, "list_size"); - let list_size_stmt = |next| { - let_lowlevel( - arena, - layout_isize, - list_size, - NumMul, - &[length, elem_size], - next, - ) - }; - - // let end = start + list_size - let end = root.create_symbol(ident_ids, "end"); - let end_stmt = |next| let_lowlevel(arena, layout_isize, end, NumAdd, &[start, list_size], next); - - // - // Loop name & parameter - // - - let elems_loop = JoinPointId(root.create_symbol(ident_ids, "elems_loop")); - let addr = root.create_symbol(ident_ids, "addr"); - - let param_addr = Param { - symbol: addr, - borrow: false, - layout: layout_isize, - }; - - // - // if we haven't reached the end yet... - // - - // Cast integer to box pointer - let box_ptr = root.create_symbol(ident_ids, "box"); - let box_layout = Layout::Union(box_union_layout); - let box_stmt = |next| let_lowlevel(arena, box_layout, box_ptr, PtrCast, &[addr], next); - - // Dereference the box pointer to get the current element - let elem = root.create_symbol(ident_ids, "elem"); - let elem_expr = Expr::UnionAtIndex { - structure: box_ptr, - union_layout: box_union_layout, - tag_id: 0, - index: 0, - }; - let elem_stmt = |next| Stmt::Let(elem, elem_expr, *elem_layout, next); - - // - // Modify element refcount - // - - let mod_elem_unit = root.create_symbol(ident_ids, "mod_elem_unit"); - let mod_elem_args = refcount_args(root, ctx, elem); - let mod_elem_expr = root - .call_specialized_op(ident_ids, ctx, *elem_layout, mod_elem_args) - .unwrap(); - let mod_elem_stmt = |next| Stmt::Let(mod_elem_unit, mod_elem_expr, LAYOUT_UNIT, next); - - // - // Next loop iteration - // - let next_addr = root.create_symbol(ident_ids, "next_addr"); - let next_addr_stmt = |next| { - let_lowlevel( - arena, - layout_isize, - next_addr, - NumAdd, - &[addr, elem_size], - next, - ) - }; - - // - // Control flow - // - - let is_end = root.create_symbol(ident_ids, "is_end"); - let is_end_stmt = |next| let_lowlevel(arena, LAYOUT_BOOL, is_end, NumGte, &[addr, end], next); - - let if_end_of_list = Stmt::Switch { - cond_symbol: is_end, - cond_layout: LAYOUT_BOOL, - ret_layout, - branches: root.arena.alloc([(1, BranchInfo::None, following)]), - default_branch: ( - BranchInfo::None, - arena.alloc(box_stmt(arena.alloc( - // - elem_stmt(arena.alloc( - // - mod_elem_stmt(arena.alloc( - // - next_addr_stmt(arena.alloc( - // - Stmt::Jump(elems_loop, arena.alloc([next_addr])), - )), - )), - )), - ))), - ), - }; - - let joinpoint_loop = Stmt::Join { - id: elems_loop, - parameters: arena.alloc([param_addr]), - body: arena.alloc( - // - is_end_stmt( - // - arena.alloc(if_end_of_list), - ), - ), - remainder: root - .arena - .alloc(Stmt::Jump(elems_loop, arena.alloc([start]))), - }; - - start_stmt(arena.alloc( - // - elem_size_stmt(arena.alloc( - // - list_size_stmt(arena.alloc( - // - end_stmt(arena.alloc( - // - joinpoint_loop, - )), - )), - )), - )) -} - -fn refcount_struct<'a>( - root: &mut CodeGenHelp<'a>, - ident_ids: &mut IdentIds, - ctx: &mut Context<'a>, - field_layouts: &'a [Layout<'a>], - structure: Symbol, -) -> Stmt<'a> { - let mut stmt = rc_return_stmt(root, ident_ids, ctx); - - for (i, field_layout) in field_layouts.iter().enumerate().rev() { - if field_layout.contains_refcounted() { - let field_val = root.create_symbol(ident_ids, &format!("field_val_{}", i)); - let field_val_expr = Expr::StructAtIndex { - index: i as u64, - field_layouts, - structure, - }; - let field_val_stmt = |next| Stmt::Let(field_val, field_val_expr, *field_layout, next); - - let mod_unit = root.create_symbol(ident_ids, &format!("mod_field_{}", i)); - let mod_args = refcount_args(root, ctx, field_val); - let mod_expr = root - .call_specialized_op(ident_ids, ctx, *field_layout, mod_args) - .unwrap(); - let mod_stmt = |next| Stmt::Let(mod_unit, mod_expr, LAYOUT_UNIT, next); - - stmt = field_val_stmt(root.arena.alloc( - // - mod_stmt(root.arena.alloc( - // - stmt, - )), - )) - } - } - - stmt -} - -fn refcount_union<'a>( - root: &mut CodeGenHelp<'a>, - ident_ids: &mut IdentIds, - ctx: &mut Context<'a>, - union: UnionLayout<'a>, - structure: Symbol, -) -> Stmt<'a> { - use UnionLayout::*; - - let parent_rec_ptr_layout = ctx.recursive_union; - if !matches!(union, NonRecursive(_)) { - ctx.recursive_union = Some(union); - } - - let body = match union { - NonRecursive(tags) => refcount_union_nonrec(root, ident_ids, ctx, union, tags, structure), - - Recursive(tags) => { - let (is_tailrec, tail_idx) = root.union_tail_recursion_fields(union); - if is_tailrec && !ctx.op.is_decref() { - refcount_union_tailrec(root, ident_ids, ctx, union, tags, None, tail_idx, structure) - } else { - refcount_union_rec(root, ident_ids, ctx, union, tags, None, structure) - } - } - - NonNullableUnwrapped(field_layouts) => { - // We don't do tail recursion on NonNullableUnwrapped. - // Its RecursionPointer is always nested inside a List, Option, or other sub-layout, since - // a direct RecursionPointer is only possible if there's at least one non-recursive variant. - // This nesting makes it harder to do tail recursion, so we just don't. - let tags = root.arena.alloc([field_layouts]); - refcount_union_rec(root, ident_ids, ctx, union, tags, None, structure) - } - - NullableWrapped { - other_tags: tags, - nullable_id, - } => { - let null_id = Some(nullable_id); - let (is_tailrec, tail_idx) = root.union_tail_recursion_fields(union); - if is_tailrec && !ctx.op.is_decref() { - refcount_union_tailrec( - root, ident_ids, ctx, union, tags, null_id, tail_idx, structure, - ) - } else { - refcount_union_rec(root, ident_ids, ctx, union, tags, null_id, structure) - } - } - - NullableUnwrapped { - other_fields, - nullable_id, - } => { - let null_id = Some(nullable_id as TagIdIntType); - let tags = root.arena.alloc([other_fields]); - let (is_tailrec, tail_idx) = root.union_tail_recursion_fields(union); - if is_tailrec && !ctx.op.is_decref() { - refcount_union_tailrec( - root, ident_ids, ctx, union, tags, null_id, tail_idx, structure, - ) - } else { - refcount_union_rec(root, ident_ids, ctx, union, tags, null_id, structure) - } - } - }; - - ctx.recursive_union = parent_rec_ptr_layout; - - body -} - -fn refcount_union_nonrec<'a>( - root: &mut CodeGenHelp<'a>, - ident_ids: &mut IdentIds, - ctx: &mut Context<'a>, - union_layout: UnionLayout<'a>, - tag_layouts: &'a [&'a [Layout<'a>]], - structure: Symbol, -) -> Stmt<'a> { - let tag_id_layout = union_layout.tag_id_layout(); - - let tag_id_sym = root.create_symbol(ident_ids, "tag_id"); - let tag_id_stmt = |next| { - Stmt::Let( - tag_id_sym, - Expr::GetTagId { - structure, - union_layout, - }, - tag_id_layout, - next, - ) - }; - - let continuation = rc_return_stmt(root, ident_ids, ctx); - - let switch_stmt = refcount_union_contents( - root, - ident_ids, - ctx, - union_layout, - tag_layouts, - None, - structure, - tag_id_sym, - tag_id_layout, - continuation, - ); - - tag_id_stmt(root.arena.alloc( - // - switch_stmt, - )) -} - -#[allow(clippy::too_many_arguments)] -fn refcount_union_contents<'a>( - root: &mut CodeGenHelp<'a>, - ident_ids: &mut IdentIds, - ctx: &mut Context<'a>, - union_layout: UnionLayout<'a>, - tag_layouts: &'a [&'a [Layout<'a>]], - null_id: Option, - structure: Symbol, - tag_id_sym: Symbol, - tag_id_layout: Layout<'a>, - next_stmt: Stmt<'a>, -) -> Stmt<'a> { - let jp_contents_modified = JoinPointId(root.create_symbol(ident_ids, "jp_contents_modified")); - let mut tag_branches = Vec::with_capacity_in(tag_layouts.len() + 1, root.arena); - - if let Some(id) = null_id { - let ret = rc_return_stmt(root, ident_ids, ctx); - tag_branches.push((id as u64, BranchInfo::None, ret)); - } - - let mut tag_id: TagIdIntType = 0; - for field_layouts in tag_layouts.iter() { - match null_id { - Some(id) if id == tag_id => { - tag_id += 1; - } - _ => {} - } - - // After refcounting the fields, jump to modify the union itself - // (Order is important, to avoid use-after-free for Dec) - let following = Stmt::Jump(jp_contents_modified, &[]); - - let fields_stmt = refcount_tag_fields( - root, - ident_ids, - ctx, - union_layout, - field_layouts, - structure, - tag_id, - following, - ); - - tag_branches.push((tag_id as u64, BranchInfo::None, fields_stmt)); - - tag_id += 1; - } - - let default_stmt: Stmt<'a> = tag_branches.pop().unwrap().2; - - let tag_id_switch = Stmt::Switch { - cond_symbol: tag_id_sym, - cond_layout: tag_id_layout, - branches: tag_branches.into_bump_slice(), - default_branch: (BranchInfo::None, root.arena.alloc(default_stmt)), - ret_layout: LAYOUT_UNIT, - }; - - Stmt::Join { - id: jp_contents_modified, - parameters: &[], - body: root.arena.alloc(next_stmt), - remainder: root.arena.alloc(tag_id_switch), - } -} - -fn refcount_union_rec<'a>( - root: &mut CodeGenHelp<'a>, - ident_ids: &mut IdentIds, - ctx: &mut Context<'a>, - union_layout: UnionLayout<'a>, - tag_layouts: &'a [&'a [Layout<'a>]], - null_id: Option, - structure: Symbol, -) -> Stmt<'a> { - let tag_id_layout = union_layout.tag_id_layout(); - - let tag_id_sym = root.create_symbol(ident_ids, "tag_id"); - let tag_id_stmt = |next| { - Stmt::Let( - tag_id_sym, - Expr::GetTagId { - structure, - union_layout, - }, - tag_id_layout, - next, - ) - }; - - let rc_structure_stmt = { - let rc_ptr = root.create_symbol(ident_ids, "rc_ptr"); - - let alignment = Layout::Union(union_layout).alignment_bytes(root.target_info); - let ret_stmt = rc_return_stmt(root, ident_ids, ctx); - let modify_structure_stmt = modify_refcount( - root, - ident_ids, - ctx, - rc_ptr, - alignment, - root.arena.alloc(ret_stmt), - ); - - rc_ptr_from_data_ptr( - root, - ident_ids, - structure, - rc_ptr, - union_layout.stores_tag_id_in_pointer(root.target_info), - root.arena.alloc(modify_structure_stmt), - ) - }; - - let rc_contents_then_structure = if ctx.op.is_decref() { - rc_structure_stmt - } else { - refcount_union_contents( - root, - ident_ids, - ctx, - union_layout, - tag_layouts, - null_id, - structure, - tag_id_sym, - tag_id_layout, - rc_structure_stmt, - ) - }; - - if ctx.op.is_decref() && null_id.is_none() { - rc_contents_then_structure - } else { - tag_id_stmt(root.arena.alloc( - // - rc_contents_then_structure, - )) - } -} - -// Refcount a recursive union using tail-call elimination to limit stack growth -#[allow(clippy::too_many_arguments)] -fn refcount_union_tailrec<'a>( - root: &mut CodeGenHelp<'a>, - ident_ids: &mut IdentIds, - ctx: &mut Context<'a>, - union_layout: UnionLayout<'a>, - tag_layouts: &'a [&'a [Layout<'a>]], - null_id: Option, - tailrec_indices: Vec<'a, Option>, - initial_structure: Symbol, -) -> Stmt<'a> { - let tailrec_loop = JoinPointId(root.create_symbol(ident_ids, "tailrec_loop")); - let current = root.create_symbol(ident_ids, "current"); - let next_ptr = root.create_symbol(ident_ids, "next_ptr"); - let layout = Layout::Union(union_layout); - - let tag_id_layout = union_layout.tag_id_layout(); - - let tag_id_sym = root.create_symbol(ident_ids, "tag_id"); - let tag_id_stmt = |next| { - Stmt::Let( - tag_id_sym, - Expr::GetTagId { - structure: current, - union_layout, - }, - tag_id_layout, - next, - ) - }; - - // Do refcounting on the structure itself - // In the control flow, this comes *after* refcounting the fields - // It receives a `next` parameter to pass through to the outer joinpoint - let rc_structure_stmt = { - let rc_ptr = root.create_symbol(ident_ids, "rc_ptr"); - let next_addr = root.create_symbol(ident_ids, "next_addr"); - - let exit_stmt = rc_return_stmt(root, ident_ids, ctx); - let jump_to_loop = Stmt::Jump(tailrec_loop, root.arena.alloc([next_ptr])); - - let loop_or_exit = Stmt::Switch { - cond_symbol: next_addr, - cond_layout: root.layout_isize, - branches: root.arena.alloc([(0, BranchInfo::None, exit_stmt)]), - default_branch: (BranchInfo::None, root.arena.alloc(jump_to_loop)), - ret_layout: LAYOUT_UNIT, - }; - let loop_or_exit_based_on_next_addr = { - let_lowlevel( - root.arena, - root.layout_isize, - next_addr, - PtrCast, - &[next_ptr], - root.arena.alloc(loop_or_exit), - ) - }; - - let alignment = layout.alignment_bytes(root.target_info); - let modify_structure_stmt = modify_refcount( - root, - ident_ids, - ctx, - rc_ptr, - alignment, - root.arena.alloc(loop_or_exit_based_on_next_addr), - ); - - rc_ptr_from_data_ptr( - root, - ident_ids, - current, - rc_ptr, - union_layout.stores_tag_id_in_pointer(root.target_info), - root.arena.alloc(modify_structure_stmt), - ) - }; - - let rc_contents_then_structure = { - let jp_modify_union = JoinPointId(root.create_symbol(ident_ids, "jp_modify_union")); - let mut tag_branches = Vec::with_capacity_in(tag_layouts.len() + 1, root.arena); - - // If this is null, there is no refcount, no `next`, no fields. Just return. - if let Some(id) = null_id { - let ret = rc_return_stmt(root, ident_ids, ctx); - tag_branches.push((id as u64, BranchInfo::None, ret)); - } - - let mut tag_id: TagIdIntType = 0; - for (field_layouts, opt_tailrec_index) in tag_layouts.iter().zip(tailrec_indices) { - match null_id { - Some(id) if id == tag_id => { - tag_id += 1; - } - _ => {} - } - - // After refcounting the fields, jump to modify the union itself. - // The loop param is a pointer to the next union. It gets passed through two jumps. - let (non_tailrec_fields, jump_to_modify_union) = - if let Some(tailrec_index) = opt_tailrec_index { - let mut filtered = Vec::with_capacity_in(field_layouts.len() - 1, root.arena); - let mut tail_stmt = None; - for (i, field) in field_layouts.iter().enumerate() { - if i != tailrec_index { - filtered.push(*field); - } else { - let field_val = - root.create_symbol(ident_ids, &format!("field_{}_{}", tag_id, i)); - let field_val_expr = Expr::UnionAtIndex { - union_layout, - tag_id, - index: i as u64, - structure: current, - }; - let jump_params = root.arena.alloc([field_val]); - let jump = root.arena.alloc(Stmt::Jump(jp_modify_union, jump_params)); - tail_stmt = Some(Stmt::Let(field_val, field_val_expr, *field, jump)); - } - } - - (filtered.into_bump_slice(), tail_stmt.unwrap()) - } else { - let zero = root.create_symbol(ident_ids, "zero"); - let zero_expr = Expr::Literal(Literal::Int(0i128.to_ne_bytes())); - let zero_stmt = |next| Stmt::Let(zero, zero_expr, root.layout_isize, next); - - let null = root.create_symbol(ident_ids, "null"); - let null_stmt = - |next| let_lowlevel(root.arena, layout, null, PtrCast, &[zero], next); - - let tail_stmt = zero_stmt(root.arena.alloc( - // - null_stmt(root.arena.alloc( - // - Stmt::Jump(jp_modify_union, root.arena.alloc([null])), - )), - )); - - (*field_layouts, tail_stmt) - }; - - let fields_stmt = refcount_tag_fields( - root, - ident_ids, - ctx, - union_layout, - non_tailrec_fields, - current, - tag_id, - jump_to_modify_union, - ); - - tag_branches.push((tag_id as u64, BranchInfo::None, fields_stmt)); - - tag_id += 1; - } - - let default_stmt: Stmt<'a> = tag_branches.pop().unwrap().2; - - let tag_id_switch = Stmt::Switch { - cond_symbol: tag_id_sym, - cond_layout: tag_id_layout, - branches: tag_branches.into_bump_slice(), - default_branch: (BranchInfo::None, root.arena.alloc(default_stmt)), - ret_layout: LAYOUT_UNIT, - }; - - let jp_param = Param { - symbol: next_ptr, - borrow: true, - layout, - }; - - Stmt::Join { - id: jp_modify_union, - parameters: root.arena.alloc([jp_param]), - body: root.arena.alloc(rc_structure_stmt), - remainder: root.arena.alloc(tag_id_switch), - } - }; - - let loop_body = tag_id_stmt(root.arena.alloc( - // - rc_contents_then_structure, - )); - - let loop_init = Stmt::Jump(tailrec_loop, root.arena.alloc([initial_structure])); - let loop_param = Param { - symbol: current, - borrow: true, - layout: Layout::Union(union_layout), - }; - - Stmt::Join { - id: tailrec_loop, - parameters: root.arena.alloc([loop_param]), - body: root.arena.alloc(loop_body), - remainder: root.arena.alloc(loop_init), - } -} - -#[allow(clippy::too_many_arguments)] -fn refcount_tag_fields<'a>( - root: &mut CodeGenHelp<'a>, - ident_ids: &mut IdentIds, - ctx: &mut Context<'a>, - union_layout: UnionLayout<'a>, - field_layouts: &'a [Layout<'a>], - structure: Symbol, - tag_id: TagIdIntType, - following: Stmt<'a>, -) -> Stmt<'a> { - let mut stmt = following; - - for (i, field_layout) in field_layouts.iter().enumerate().rev() { - if field_layout.contains_refcounted() { - let field_val = root.create_symbol(ident_ids, &format!("field_{}_{}", tag_id, i)); - let field_val_expr = Expr::UnionAtIndex { - union_layout, - tag_id, - index: i as u64, - structure, - }; - let field_val_stmt = |next| Stmt::Let(field_val, field_val_expr, *field_layout, next); - - let mod_unit = root.create_symbol(ident_ids, &format!("mod_field_{}_{}", tag_id, i)); - let mod_args = refcount_args(root, ctx, field_val); - let mod_expr = root - .call_specialized_op(ident_ids, ctx, *field_layout, mod_args) - .unwrap(); - let mod_stmt = |next| Stmt::Let(mod_unit, mod_expr, LAYOUT_UNIT, next); - - stmt = field_val_stmt(root.arena.alloc( - // - mod_stmt(root.arena.alloc( - // - stmt, - )), - )) - } - } - - stmt -} diff --git a/compiler/mono/src/copy.rs b/compiler/mono/src/copy.rs deleted file mode 100644 index 9b9c7e65bb..0000000000 --- a/compiler/mono/src/copy.rs +++ /dev/null @@ -1,791 +0,0 @@ -use bumpalo::collections::Vec; -use bumpalo::Bump; -use roc_can::{ - def::Def, - expr::{AccessorData, ClosureData, Expr, Field, WhenBranch}, -}; -use roc_types::{ - subs::{ - self, AliasVariables, Descriptor, OptVariable, RecordFields, Subs, SubsSlice, UnionLambdas, - UnionTags, Variable, VariableSubsSlice, - }, - types::Uls, -}; - -/// Deep copies the type variables in the type hosted by [`var`] into [`expr`]. -/// Returns [`None`] if the expression does not need to be copied. -pub fn deep_copy_type_vars_into_expr<'a>( - arena: &'a Bump, - subs: &mut Subs, - var: Variable, - expr: &Expr, -) -> Option<(Variable, Expr)> { - // Always deal with the root, so that aliases propagate correctly. - let var = subs.get_root_key_without_compacting(var); - - let substitutions = deep_copy_type_vars(arena, subs, var); - - if substitutions.is_empty() { - return None; - } - - let new_var = substitutions - .iter() - .find_map(|&(original, new)| if original == var { Some(new) } else { None }) - .expect("Variable marked as cloned, but it isn't"); - - return Some((new_var, help(subs, expr, &substitutions))); - - fn help(subs: &Subs, expr: &Expr, substitutions: &[(Variable, Variable)]) -> Expr { - use Expr::*; - - macro_rules! sub { - ($var:expr) => {{ - // Always deal with the root, so that aliases propagate correctly. - let root = subs.get_root_key_without_compacting($var); - substitutions - .iter() - .find_map(|&(original, new)| if original == root { Some(new) } else { None }) - .unwrap_or($var) - }}; - } - - let go_help = |e: &Expr| help(subs, e, substitutions); - - match expr { - Num(var, str, val, bound) => Num(sub!(*var), str.clone(), val.clone(), *bound), - Int(v1, v2, str, val, bound) => { - Int(sub!(*v1), sub!(*v2), str.clone(), val.clone(), *bound) - } - Float(v1, v2, str, val, bound) => { - Float(sub!(*v1), sub!(*v2), str.clone(), *val, *bound) - } - Str(str) => Str(str.clone()), - SingleQuote(char) => SingleQuote(*char), - List { - elem_var, - loc_elems, - } => List { - elem_var: sub!(*elem_var), - loc_elems: loc_elems.iter().map(|le| le.map(go_help)).collect(), - }, - Var(sym) => Var(*sym), - &AbilityMember(sym, specialization, specialization_var) => { - AbilityMember(sym, specialization, specialization_var) - } - When { - loc_cond, - cond_var, - expr_var, - region, - branches, - branches_cond_var, - exhaustive, - } => When { - loc_cond: Box::new(loc_cond.map(go_help)), - cond_var: sub!(*cond_var), - expr_var: sub!(*expr_var), - region: *region, - branches: branches - .iter() - .map( - |WhenBranch { - patterns, - value, - guard, - redundant, - }| WhenBranch { - patterns: patterns.clone(), - value: value.map(go_help), - guard: guard.as_ref().map(|le| le.map(go_help)), - redundant: *redundant, - }, - ) - .collect(), - branches_cond_var: sub!(*branches_cond_var), - exhaustive: *exhaustive, - }, - If { - cond_var, - branch_var, - branches, - final_else, - } => If { - cond_var: sub!(*cond_var), - branch_var: sub!(*branch_var), - branches: branches - .iter() - .map(|(c, e)| (c.map(go_help), e.map(go_help))) - .collect(), - final_else: Box::new(final_else.map(go_help)), - }, - - LetRec(defs, body, cycle_mark) => LetRec( - defs.iter() - .map( - |Def { - loc_pattern, - loc_expr, - expr_var, - pattern_vars, - annotation, - }| Def { - loc_pattern: loc_pattern.clone(), - loc_expr: loc_expr.map(go_help), - expr_var: sub!(*expr_var), - pattern_vars: pattern_vars - .iter() - .map(|(s, v)| (*s, sub!(*v))) - .collect(), - annotation: annotation.clone(), - }, - ) - .collect(), - Box::new(body.map(go_help)), - *cycle_mark, - ), - LetNonRec(def, body) => { - let Def { - loc_pattern, - loc_expr, - expr_var, - pattern_vars, - annotation, - } = &**def; - let def = Def { - loc_pattern: loc_pattern.clone(), - loc_expr: loc_expr.map(go_help), - expr_var: sub!(*expr_var), - pattern_vars: pattern_vars.iter().map(|(s, v)| (*s, sub!(*v))).collect(), - annotation: annotation.clone(), - }; - LetNonRec(Box::new(def), Box::new(body.map(go_help))) - } - - Call(f, args, called_via) => { - let (fn_var, fn_expr, clos_var, ret_var) = &**f; - Call( - Box::new(( - sub!(*fn_var), - fn_expr.map(go_help), - sub!(*clos_var), - sub!(*ret_var), - )), - args.iter() - .map(|(var, expr)| (sub!(*var), expr.map(go_help))) - .collect(), - *called_via, - ) - } - RunLowLevel { op, args, ret_var } => RunLowLevel { - op: *op, - args: args - .iter() - .map(|(var, expr)| (sub!(*var), go_help(expr))) - .collect(), - ret_var: sub!(*ret_var), - }, - ForeignCall { - foreign_symbol, - args, - ret_var, - } => ForeignCall { - foreign_symbol: foreign_symbol.clone(), - args: args - .iter() - .map(|(var, expr)| (sub!(*var), go_help(expr))) - .collect(), - ret_var: sub!(*ret_var), - }, - - Closure(ClosureData { - function_type, - closure_type, - return_type, - name, - captured_symbols, - recursive, - arguments, - loc_body, - }) => Closure(ClosureData { - function_type: sub!(*function_type), - closure_type: sub!(*closure_type), - return_type: sub!(*return_type), - name: *name, - captured_symbols: captured_symbols - .iter() - .map(|(s, v)| (*s, sub!(*v))) - .collect(), - recursive: *recursive, - arguments: arguments - .iter() - .map(|(v, mark, pat)| (sub!(*v), *mark, pat.clone())) - .collect(), - loc_body: Box::new(loc_body.map(go_help)), - }), - - Record { record_var, fields } => Record { - record_var: sub!(*record_var), - fields: fields - .iter() - .map( - |( - k, - Field { - var, - region, - loc_expr, - }, - )| { - ( - k.clone(), - Field { - var: sub!(*var), - region: *region, - loc_expr: Box::new(loc_expr.map(go_help)), - }, - ) - }, - ) - .collect(), - }, - - EmptyRecord => EmptyRecord, - - Access { - record_var, - ext_var, - field_var, - loc_expr, - field, - } => Access { - record_var: sub!(*record_var), - ext_var: sub!(*ext_var), - field_var: sub!(*field_var), - loc_expr: Box::new(loc_expr.map(go_help)), - field: field.clone(), - }, - - Accessor(AccessorData { - name, - function_var, - record_var, - closure_var, - ext_var, - field_var, - field, - }) => Accessor(AccessorData { - name: *name, - function_var: sub!(*function_var), - record_var: sub!(*record_var), - closure_var: sub!(*closure_var), - ext_var: sub!(*ext_var), - field_var: sub!(*field_var), - field: field.clone(), - }), - - Update { - record_var, - ext_var, - symbol, - updates, - } => Update { - record_var: sub!(*record_var), - ext_var: sub!(*ext_var), - symbol: *symbol, - updates: updates - .iter() - .map( - |( - k, - Field { - var, - region, - loc_expr, - }, - )| { - ( - k.clone(), - Field { - var: sub!(*var), - region: *region, - loc_expr: Box::new(loc_expr.map(go_help)), - }, - ) - }, - ) - .collect(), - }, - - Tag { - variant_var, - ext_var, - name, - arguments, - } => Tag { - variant_var: sub!(*variant_var), - ext_var: sub!(*ext_var), - name: name.clone(), - arguments: arguments - .iter() - .map(|(v, e)| (sub!(*v), e.map(go_help))) - .collect(), - }, - - ZeroArgumentTag { - closure_name, - variant_var, - ext_var, - name, - } => ZeroArgumentTag { - closure_name: *closure_name, - variant_var: sub!(*variant_var), - ext_var: sub!(*ext_var), - name: name.clone(), - }, - - OpaqueRef { - opaque_var, - name, - argument, - specialized_def_type, - type_arguments, - lambda_set_variables, - } => OpaqueRef { - opaque_var: sub!(*opaque_var), - name: *name, - argument: Box::new((sub!(argument.0), argument.1.map(go_help))), - // These shouldn't matter for opaques during mono, because they are only used for reporting - // and pretty-printing to the user. During mono we decay immediately into the argument. - // NB: if there are bugs, check if not substituting here is the problem! - specialized_def_type: specialized_def_type.clone(), - type_arguments: type_arguments.clone(), - lambda_set_variables: lambda_set_variables.clone(), - }, - - Expect { - loc_condition, - loc_continuation, - lookups_in_cond, - } => Expect { - loc_condition: Box::new(loc_condition.map(go_help)), - loc_continuation: Box::new(loc_continuation.map(go_help)), - lookups_in_cond: lookups_in_cond.to_vec(), - }, - - TypedHole(v) => TypedHole(sub!(*v)), - - RuntimeError(err) => RuntimeError(err.clone()), - } - } -} - -/// Deep copies the type variables in [`var`], returning a map of original -> new type variable for -/// all type variables copied. -fn deep_copy_type_vars<'a>( - arena: &'a Bump, - subs: &mut Subs, - var: Variable, -) -> Vec<'a, (Variable, Variable)> { - // Always deal with the root, so that unified variables are treated the same. - let var = subs.get_root_key_without_compacting(var); - - let mut copied = Vec::with_capacity_in(16, arena); - - let cloned_var = help(arena, subs, &mut copied, var); - - // we have tracked all visited variables, and can now traverse them - // in one go (without looking at the UnificationTable) and clear the copy field - let mut result = Vec::with_capacity_in(copied.len(), arena); - for var in copied { - subs.modify(var, |descriptor| { - if let Some(copy) = descriptor.copy.into_variable() { - result.push((var, copy)); - descriptor.copy = OptVariable::NONE; - } else { - debug_assert!(false, "{:?} marked as copied but it wasn't", var); - } - }) - } - - debug_assert!(result.contains(&(var, cloned_var))); - - return result; - - #[must_use] - fn help(arena: &Bump, subs: &mut Subs, visited: &mut Vec, var: Variable) -> Variable { - use roc_types::subs::Content::*; - use roc_types::subs::FlatType::*; - - // Always deal with the root, so that unified variables are treated the same. - let var = subs.get_root_key_without_compacting(var); - - let desc = subs.get(var); - - // Unlike `deep_copy_var` in solve, here we are cloning *all* flex and rigid vars. - // So we only want to short-circuit if we've already done the cloning work for a particular - // var. - if let Some(copy) = desc.copy.into_variable() { - return copy; - } - - let content = desc.content; - - let copy_descriptor = Descriptor { - content: Error, // we'll update this below - rank: desc.rank, - mark: desc.mark, - copy: OptVariable::NONE, - }; - - let copy = subs.fresh(copy_descriptor); - subs.set_copy(var, copy.into()); - - visited.push(var); - - macro_rules! descend_slice { - ($slice:expr) => { - for var_index in $slice { - let var = subs[var_index]; - let _ = help(arena, subs, visited, var); - } - }; - } - - macro_rules! descend_var { - ($var:expr) => {{ - help(arena, subs, visited, $var) - }}; - } - - macro_rules! clone_var_slice { - ($slice:expr) => {{ - let new_arguments = VariableSubsSlice::reserve_into_subs(subs, $slice.len()); - for (target_index, var_index) in (new_arguments.indices()).zip($slice) { - let var = subs[var_index]; - let copy_var = subs.get_copy(var).into_variable().unwrap_or(var); - subs.variables[target_index] = copy_var; - } - new_arguments - }}; - } - - macro_rules! perform_clone { - ($do_clone:expr) => {{ - // It may the case that while deep-copying nested variables of this type, we - // ended up copying the type itself (notably if it was self-referencing, in a - // recursive type). In that case, short-circuit with the known copy. - // if let Some(copy) = subs.get_ref(var).copy.into_variable() { - // return copy; - // } - // Perform the clone. - $do_clone - }}; - } - - // Now we recursively copy the content of the variable. - // We have already marked the variable as copied, so we - // will not repeat this work or crawl this variable again. - let new_content = match content { - // The vars for which we want to do something interesting. - FlexVar(opt_name) => FlexVar(opt_name), - FlexAbleVar(opt_name, ability) => FlexAbleVar(opt_name, ability), - RigidVar(name) => RigidVar(name), - RigidAbleVar(name, ability) => RigidAbleVar(name, ability), - - // Everything else is a mechanical descent. - Structure(flat_type) => match flat_type { - EmptyRecord | EmptyTagUnion | Erroneous(_) => Structure(flat_type), - Apply(symbol, arguments) => { - descend_slice!(arguments); - - perform_clone!({ - let new_arguments = clone_var_slice!(arguments); - Structure(Apply(symbol, new_arguments)) - }) - } - Func(arguments, closure_var, ret_var) => { - descend_slice!(arguments); - - let new_closure_var = descend_var!(closure_var); - let new_ret_var = descend_var!(ret_var); - - perform_clone!({ - let new_arguments = clone_var_slice!(arguments); - Structure(Func(new_arguments, new_closure_var, new_ret_var)) - }) - } - Record(fields, ext_var) => { - let new_ext_var = descend_var!(ext_var); - - descend_slice!(fields.variables()); - - perform_clone!({ - let new_variables = clone_var_slice!(fields.variables()); - let new_fields = { - RecordFields { - length: fields.length, - field_names_start: fields.field_names_start, - variables_start: new_variables.start, - field_types_start: fields.field_types_start, - } - }; - - Structure(Record(new_fields, new_ext_var)) - }) - } - TagUnion(tags, ext_var) => { - let new_ext_var = descend_var!(ext_var); - - for variables_slice_index in tags.variables() { - let variables_slice = subs[variables_slice_index]; - descend_slice!(variables_slice); - } - - perform_clone!({ - let new_variable_slices = - SubsSlice::reserve_variable_slices(subs, tags.len()); - let it = (new_variable_slices.indices()).zip(tags.variables()); - for (target_index, index) in it { - let slice = subs[index]; - let new_variables = clone_var_slice!(slice); - subs.variable_slices[target_index] = new_variables; - } - - let new_union_tags = - UnionTags::from_slices(tags.labels(), new_variable_slices); - - Structure(TagUnion(new_union_tags, new_ext_var)) - }) - } - RecursiveTagUnion(rec_var, tags, ext_var) => { - let new_ext_var = descend_var!(ext_var); - let new_rec_var = descend_var!(rec_var); - - for variables_slice_index in tags.variables() { - let variables_slice = subs[variables_slice_index]; - descend_slice!(variables_slice); - } - - perform_clone!({ - let new_variable_slices = - SubsSlice::reserve_variable_slices(subs, tags.len()); - let it = (new_variable_slices.indices()).zip(tags.variables()); - for (target_index, index) in it { - let slice = subs[index]; - let new_variables = clone_var_slice!(slice); - subs.variable_slices[target_index] = new_variables; - } - - let new_union_tags = - UnionTags::from_slices(tags.labels(), new_variable_slices); - - Structure(RecursiveTagUnion(new_rec_var, new_union_tags, new_ext_var)) - }) - } - FunctionOrTagUnion(tag_name, symbol, ext_var) => { - let new_ext_var = descend_var!(ext_var); - perform_clone!(Structure(FunctionOrTagUnion(tag_name, symbol, new_ext_var))) - } - }, - - RecursionVar { - opt_name, - structure, - } => { - let new_structure = descend_var!(structure); - - perform_clone!({ - RecursionVar { - opt_name, - structure: new_structure, - } - }) - } - - Alias(symbol, arguments, real_type_var, kind) => { - let new_real_type_var = descend_var!(real_type_var); - descend_slice!(arguments.all_variables()); - - perform_clone!({ - let new_variables = clone_var_slice!(arguments.all_variables()); - let new_arguments = AliasVariables { - variables_start: new_variables.start, - ..arguments - }; - - Alias(symbol, new_arguments, new_real_type_var, kind) - }) - } - - LambdaSet(subs::LambdaSet { - solved, - recursion_var, - unspecialized, - }) => { - let new_rec_var = recursion_var.map(|var| descend_var!(var)); - for variables_slice_index in solved.variables() { - let variables_slice = subs[variables_slice_index]; - descend_slice!(variables_slice); - } - for uls_index in unspecialized { - let Uls(var, _, _) = subs[uls_index]; - descend_var!(var); - } - - perform_clone!({ - let new_variable_slices = - SubsSlice::reserve_variable_slices(subs, solved.len()); - let it = (new_variable_slices.indices()).zip(solved.variables()); - for (target_index, index) in it { - let slice = subs[index]; - let new_variables = clone_var_slice!(slice); - subs.variable_slices[target_index] = new_variables; - } - - let new_solved = - UnionLambdas::from_slices(solved.labels(), new_variable_slices); - - let new_unspecialized = SubsSlice::reserve_uls_slice(subs, unspecialized.len()); - for (target_index, uls_index) in - (new_unspecialized.into_iter()).zip(unspecialized.into_iter()) - { - let Uls(var, sym, region) = subs[uls_index]; - let copy_var = subs.get_copy(var).into_variable().unwrap_or(var); - subs[target_index] = Uls(copy_var, sym, region); - } - - LambdaSet(subs::LambdaSet { - solved: new_solved, - recursion_var: new_rec_var, - unspecialized: new_unspecialized, - }) - }) - } - - RangedNumber(typ, range) => { - let new_typ = descend_var!(typ); - - perform_clone!(RangedNumber(new_typ, range)) - } - Error => Error, - }; - - subs.set_content(copy, new_content); - - copy - } -} - -#[cfg(test)] -mod test { - use super::deep_copy_type_vars; - use bumpalo::Bump; - use roc_error_macros::internal_error; - use roc_module::symbol::Symbol; - use roc_types::subs::{ - Content, Content::*, Descriptor, Mark, OptVariable, Rank, Subs, SubsIndex, Variable, - }; - - #[cfg(test)] - fn new_var(subs: &mut Subs, content: Content) -> Variable { - subs.fresh(Descriptor { - content, - rank: Rank::toplevel(), - mark: Mark::NONE, - copy: OptVariable::NONE, - }) - } - - #[test] - fn copy_flex_var() { - let mut subs = Subs::new(); - let arena = Bump::new(); - - let field_name = SubsIndex::push_new(&mut subs.field_names, "a".into()); - let var = new_var(&mut subs, FlexVar(Some(field_name))); - - let mut copies = deep_copy_type_vars(&arena, &mut subs, var); - - assert_eq!(copies.len(), 1); - let (original, new) = copies.pop().unwrap(); - assert_ne!(original, new); - - assert_eq!(original, var); - match subs.get_content_without_compacting(new) { - FlexVar(Some(name)) => { - assert_eq!(subs[*name].as_str(), "a"); - } - it => unreachable!("{:?}", it), - } - } - - #[test] - fn copy_rigid_var() { - let mut subs = Subs::new(); - let arena = Bump::new(); - - let field_name = SubsIndex::push_new(&mut subs.field_names, "a".into()); - let var = new_var(&mut subs, RigidVar(field_name)); - - let mut copies = deep_copy_type_vars(&arena, &mut subs, var); - - assert_eq!(copies.len(), 1); - let (original, new) = copies.pop().unwrap(); - assert_ne!(original, new); - - assert_eq!(original, var); - match subs.get_content_without_compacting(new) { - RigidVar(name) => { - assert_eq!(subs[*name].as_str(), "a"); - } - it => unreachable!("{:?}", it), - } - } - - #[test] - fn copy_flex_able_var() { - let mut subs = Subs::new(); - let arena = Bump::new(); - - let field_name = SubsIndex::push_new(&mut subs.field_names, "a".into()); - let var = new_var(&mut subs, FlexAbleVar(Some(field_name), Symbol::UNDERSCORE)); - - let mut copies = deep_copy_type_vars(&arena, &mut subs, var); - - assert_eq!(copies.len(), 1); - let (original, new) = copies.pop().unwrap(); - assert_ne!(original, new); - - assert_eq!(original, var); - match subs.get_content_without_compacting(new) { - FlexAbleVar(Some(name), Symbol::UNDERSCORE) => { - assert_eq!(subs[*name].as_str(), "a"); - } - it => unreachable!("{:?}", it), - } - } - - #[test] - fn copy_rigid_able_var() { - let mut subs = Subs::new(); - let arena = Bump::new(); - - let field_name = SubsIndex::push_new(&mut subs.field_names, "a".into()); - let var = new_var(&mut subs, RigidAbleVar(field_name, Symbol::UNDERSCORE)); - - let mut copies = deep_copy_type_vars(&arena, &mut subs, var); - - assert_eq!(copies.len(), 1); - let (original, new) = copies.pop().unwrap(); - assert_ne!(original, new); - - assert_eq!(original, var); - match subs.get_content_without_compacting(new) { - RigidAbleVar(name, Symbol::UNDERSCORE) => { - assert_eq!(subs[*name].as_str(), "a"); - } - it => internal_error!("{:?}", it), - } - } -} diff --git a/compiler/mono/src/decision_tree.rs b/compiler/mono/src/decision_tree.rs deleted file mode 100644 index c7847f52d9..0000000000 --- a/compiler/mono/src/decision_tree.rs +++ /dev/null @@ -1,2187 +0,0 @@ -use crate::ir::{ - BranchInfo, DestructType, Env, Expr, JoinPointId, Literal, Param, Pattern, Procs, Stmt, -}; -use crate::layout::{Builtin, Layout, LayoutCache, TagIdIntType, UnionLayout}; -use roc_builtins::bitcode::{FloatWidth, IntWidth}; -use roc_collections::all::{MutMap, MutSet}; -use roc_exhaustive::{Ctor, CtorName, RenderAs, TagId, Union}; -use roc_module::ident::TagName; -use roc_module::low_level::LowLevel; -use roc_module::symbol::Symbol; - -/// COMPILE CASES - -type Label = u64; -const RECORD_TAG_NAME: &str = "#Record"; - -/// Users of this module will mainly interact with this function. It takes -/// some normal branches and gives out a decision tree that has "labels" at all -/// the leafs and a dictionary that maps these "labels" to the code that should -/// run. -fn compile<'a>(raw_branches: Vec<(Guard<'a>, Pattern<'a>, u64)>) -> DecisionTree<'a> { - let formatted = raw_branches - .into_iter() - .map(|(guard, pattern, index)| Branch { - goal: index, - guard, - patterns: vec![(Vec::new(), pattern)], - }) - .collect(); - - to_decision_tree(formatted) -} - -#[derive(Clone, Debug, PartialEq)] -pub enum Guard<'a> { - NoGuard, - Guard { - /// pattern - pattern: Pattern<'a>, - /// after assigning to symbol, the stmt jumps to this label - id: JoinPointId, - stmt: Stmt<'a>, - }, -} - -impl<'a> Guard<'a> { - fn is_none(&self) -> bool { - self == &Guard::NoGuard - } -} - -#[derive(Clone, Debug, PartialEq)] -enum DecisionTree<'a> { - Match(Label), - Decision { - path: Vec, - edges: Vec<(GuardedTest<'a>, DecisionTree<'a>)>, - default: Option>>, - }, -} - -#[derive(Clone, Debug, PartialEq)] -enum GuardedTest<'a> { - // e.g. `_ if True -> ...` - GuardedNoTest { - /// pattern - pattern: Pattern<'a>, - /// after assigning to symbol, the stmt jumps to this label - id: JoinPointId, - /// body - stmt: Stmt<'a>, - }, - TestNotGuarded { - test: Test<'a>, - }, - Placeholder, -} - -#[derive(Clone, Debug, PartialEq)] -#[allow(clippy::enum_variant_names)] -enum Test<'a> { - IsCtor { - tag_id: TagIdIntType, - ctor_name: CtorName, - union: roc_exhaustive::Union, - arguments: Vec<(Pattern<'a>, Layout<'a>)>, - }, - IsInt([u8; 16], IntWidth), - IsFloat(u64, FloatWidth), - IsDecimal([u8; 16]), - IsStr(Box), - IsBit(bool), - IsByte { - tag_id: TagIdIntType, - num_alts: usize, - }, -} - -use std::hash::{Hash, Hasher}; -impl<'a> Hash for Test<'a> { - fn hash(&self, state: &mut H) { - use Test::*; - - match self { - IsCtor { tag_id, .. } => { - state.write_u8(0); - tag_id.hash(state); - // The point of this custom implementation is to not hash the tag arguments - } - IsInt(v, width) => { - state.write_u8(1); - v.hash(state); - width.hash(state); - } - IsFloat(v, width) => { - state.write_u8(2); - v.hash(state); - width.hash(state); - } - IsStr(v) => { - state.write_u8(3); - v.hash(state); - } - IsBit(v) => { - state.write_u8(4); - v.hash(state); - } - IsByte { tag_id, num_alts } => { - state.write_u8(5); - tag_id.hash(state); - num_alts.hash(state); - } - IsDecimal(v) => { - // TODO: Is this okay? - state.write_u8(6); - v.hash(state); - } - } - } -} - -impl<'a> Hash for GuardedTest<'a> { - fn hash(&self, state: &mut H) { - match self { - GuardedTest::GuardedNoTest { id, .. } => { - state.write_u8(1); - id.hash(state); - } - GuardedTest::TestNotGuarded { test } => { - state.write_u8(0); - test.hash(state); - } - GuardedTest::Placeholder => { - state.write_u8(2); - } - } - } -} - -// ACTUALLY BUILD DECISION TREES - -#[derive(Clone, Debug, PartialEq)] -struct Branch<'a> { - goal: Label, - guard: Guard<'a>, - patterns: Vec<(Vec, Pattern<'a>)>, -} - -fn to_decision_tree(raw_branches: Vec) -> DecisionTree { - let branches: Vec<_> = raw_branches.into_iter().map(flatten_patterns).collect(); - - debug_assert!(!branches.is_empty()); - - match check_for_match(&branches) { - Match::Exact(goal) => DecisionTree::Match(goal), - - Match::GuardOnly => { - // the first branch has no more tests to do, but it has an if-guard - - let mut branches = branches; - let first = branches.remove(0); - - match first.guard { - Guard::NoGuard => unreachable!(), - - Guard::Guard { id, stmt, pattern } => { - let guarded_test = GuardedTest::GuardedNoTest { id, stmt, pattern }; - - // the guard test does not have a path - let path = vec![]; - - // we expect none of the patterns need tests, those decisions should have been made already - debug_assert!(first - .patterns - .iter() - .all(|(_, pattern)| !needs_tests(pattern))); - - let default = if branches.is_empty() { - None - } else { - Some(Box::new(to_decision_tree(branches))) - }; - - DecisionTree::Decision { - path, - edges: vec![(guarded_test, DecisionTree::Match(first.goal))], - default, - } - } - } - } - - Match::None => { - // must clone here to release the borrow on `branches` - let path = pick_path(&branches).clone(); - - let bs = branches.clone(); - let (edges, fallback) = gather_edges(branches, &path); - - let mut decision_edges: Vec<_> = edges - .into_iter() - .map(|(test, branches)| { - if bs == branches { - panic!(); - } else { - (test, to_decision_tree(branches)) - } - }) - .collect(); - - match (decision_edges.as_slice(), fallback.as_slice()) { - ([(_test, _decision_tree)], []) => { - // only one test with no fallback: we will always enter this branch - - // get the `_decision_tree` without cloning - decision_edges.pop().unwrap().1 - } - (_, []) => break_out_guard(path, decision_edges, None), - ([], _) => { - // should be guaranteed by the patterns - debug_assert!(!fallback.is_empty()); - to_decision_tree(fallback) - } - (_, _) => break_out_guard( - path, - decision_edges, - Some(Box::new(to_decision_tree(fallback))), - ), - } - } - } -} - -/// Give a guard it's own Decision -fn break_out_guard<'a>( - path: Vec, - mut edges: Vec<(GuardedTest<'a>, DecisionTree<'a>)>, - default: Option>>, -) -> DecisionTree<'a> { - match edges - .iter() - .position(|(t, _)| matches!(t, GuardedTest::Placeholder)) - { - None => DecisionTree::Decision { - path, - edges, - default, - }, - Some(index) => { - let (a, b) = edges.split_at_mut(index + 1); - - let new_default = break_out_guard(path.clone(), b.to_vec(), default); - - let mut left = a.to_vec(); - let guard = left.pop().unwrap(); - - let help = DecisionTree::Decision { - path: path.clone(), - edges: vec![guard], - default: Some(Box::new(new_default)), - }; - - DecisionTree::Decision { - path, - edges: left, - default: Some(Box::new(help)), - } - } - } -} - -fn guarded_tests_are_complete(tests: &[GuardedTest]) -> bool { - let length = tests.len(); - debug_assert!(length > 0); - - let no_guard = tests - .iter() - .all(|t| matches!(t, GuardedTest::TestNotGuarded { .. })); - - match tests.last().unwrap() { - GuardedTest::Placeholder => false, - GuardedTest::GuardedNoTest { .. } => false, - GuardedTest::TestNotGuarded { test } => no_guard && tests_are_complete_help(test, length), - } -} - -fn tests_are_complete_help(last_test: &Test, number_of_tests: usize) -> bool { - match last_test { - Test::IsCtor { union, .. } => number_of_tests == union.alternatives.len(), - Test::IsByte { num_alts, .. } => number_of_tests == *num_alts, - Test::IsBit(_) => number_of_tests == 2, - Test::IsInt(_, _) => false, - Test::IsFloat(_, _) => false, - Test::IsDecimal(_) => false, - Test::IsStr(_) => false, - } -} - -fn flatten_patterns(branch: Branch) -> Branch { - let mut result = Vec::with_capacity(branch.patterns.len()); - - for path_pattern in branch.patterns { - flatten(path_pattern, &mut result); - } - - Branch { - patterns: result, - ..branch - } -} - -fn flatten<'a>( - path_pattern: (Vec, Pattern<'a>), - path_patterns: &mut Vec<(Vec, Pattern<'a>)>, -) { - match path_pattern.1 { - Pattern::AppliedTag { - union, - arguments, - tag_id, - tag_name, - layout, - } if union.alternatives.len() == 1 && !layout.is_nullable() => { - // TODO ^ do we need to check that guard.is_none() here? - - let path = path_pattern.0; - // Theory: unbox doesn't have any value for us, because one-element tag unions - // don't store the tag anyway. - if arguments.len() == 1 { - // NOTE here elm will unbox, but we don't use that - path_patterns.push(( - path, - Pattern::AppliedTag { - union, - arguments, - tag_id, - tag_name, - layout, - }, - )); - } else { - for (index, (arg_pattern, _)) in arguments.iter().enumerate() { - let mut new_path = path.clone(); - new_path.push(PathInstruction { - index: index as u64, - tag_id, - }); - - flatten((new_path, arg_pattern.clone()), path_patterns); - } - } - } - - _ => { - path_patterns.push(path_pattern); - } - } -} - -/// SUCCESSFULLY MATCH - -/// If the first branch has no more "decision points" we can finally take that -/// path. If that is the case we give the resulting label and a mapping from free -/// variables to "how to get their value". So a pattern like (Just (x,_)) will give -/// us something like ("x" => value.0.0) - -enum Match { - Exact(Label), - GuardOnly, - None, -} - -fn check_for_match(branches: &[Branch]) -> Match { - match branches.get(0) { - Some(Branch { - goal, - patterns, - guard, - }) if patterns.iter().all(|(_, pattern)| !needs_tests(pattern)) => { - if guard.is_none() { - Match::Exact(*goal) - } else { - Match::GuardOnly - } - } - _ => Match::None, - } -} - -/// GATHER OUTGOING EDGES - -// my understanding: branches that we could jump to based on the pattern at the current path -fn gather_edges<'a>( - branches: Vec>, - path: &[PathInstruction], -) -> (Vec<(GuardedTest<'a>, Vec>)>, Vec>) { - let relevant_tests = tests_at_path(path, &branches); - - let check = guarded_tests_are_complete(&relevant_tests); - - let all_edges = relevant_tests - .into_iter() - .map(|t| edges_for(path, &branches, t)) - .collect(); - - let fallbacks = if check { - vec![] - } else { - branches - .into_iter() - .filter(|b| is_irrelevant_to(path, b)) - .collect() - }; - - (all_edges, fallbacks) -} - -/// FIND RELEVANT TESTS - -fn tests_at_path<'a>( - selected_path: &[PathInstruction], - branches: &[Branch<'a>], -) -> Vec> { - // NOTE the ordering of the result is important! - - let mut all_tests = Vec::new(); - - for branch in branches { - all_tests.extend(test_at_path(selected_path, branch)); - } - - // The rust HashMap also uses equality, here we really want to use the custom hash function - // defined on Test to determine whether a test is unique. So we have to do the hashing - // explicitly - - use std::collections::hash_map::DefaultHasher; - - let mut visited = MutSet::default(); - let mut unique = Vec::new(); - - for test in all_tests { - let hash = { - let mut hasher = DefaultHasher::new(); - test.hash(&mut hasher); - hasher.finish() - }; - - if !visited.contains(&hash) { - visited.insert(hash); - unique.push(test); - } - } - - unique -} - -fn test_at_path<'a>( - selected_path: &[PathInstruction], - branch: &Branch<'a>, -) -> Option> { - use Pattern::*; - use Test::*; - - match branch - .patterns - .iter() - .find(|(path, _)| path == selected_path) - { - None => None, - Some((_, pattern)) => { - let test = match pattern { - Identifier(_) | Underscore => { - if let Guard::Guard { .. } = &branch.guard { - // no tests for this pattern remain, but we cannot discard it yet - // because it has a guard! - return Some(GuardedTest::Placeholder); - } else { - return None; - } - } - - RecordDestructure(destructs, _) => { - // not rendered, so pick the easiest - let union = Union { - render_as: RenderAs::Tag, - alternatives: vec![Ctor { - tag_id: TagId(0), - name: CtorName::Tag(TagName(RECORD_TAG_NAME.into())), - arity: destructs.len(), - }], - }; - - let mut arguments = std::vec::Vec::new(); - - for destruct in destructs { - match &destruct.typ { - DestructType::Guard(guard) => { - arguments.push((guard.clone(), destruct.layout)); - } - DestructType::Required(_) => { - arguments.push((Pattern::Underscore, destruct.layout)); - } - } - } - - IsCtor { - tag_id: 0, - ctor_name: CtorName::Tag(TagName(RECORD_TAG_NAME.into())), - union, - arguments, - } - } - - NewtypeDestructure { - tag_name, - arguments, - } => { - let tag_id = 0; - let union = - Union::newtype_wrapper(CtorName::Tag(tag_name.clone()), arguments.len()); - - IsCtor { - tag_id, - ctor_name: CtorName::Tag(tag_name.clone()), - union, - arguments: arguments.to_vec(), - } - } - - AppliedTag { - tag_name, - tag_id, - arguments, - union, - .. - } => IsCtor { - tag_id: *tag_id, - ctor_name: CtorName::Tag(tag_name.clone()), - union: union.clone(), - arguments: arguments.to_vec(), - }, - - OpaqueUnwrap { opaque, argument } => { - let union = Union { - render_as: RenderAs::Tag, - alternatives: vec![Ctor { - tag_id: TagId(0), - name: CtorName::Opaque(*opaque), - arity: 1, - }], - }; - - IsCtor { - tag_id: 0, - ctor_name: CtorName::Opaque(*opaque), - union, - arguments: vec![(**argument).clone()], - } - } - - BitLiteral { value, .. } => IsBit(*value), - EnumLiteral { tag_id, union, .. } => IsByte { - tag_id: *tag_id as _, - num_alts: union.alternatives.len(), - }, - IntLiteral(v, precision) => IsInt(*v, *precision), - FloatLiteral(v, precision) => IsFloat(*v, *precision), - DecimalLiteral(v) => IsDecimal(*v), - StrLiteral(v) => IsStr(v.clone()), - }; - - let guarded_test = GuardedTest::TestNotGuarded { test }; - - Some(guarded_test) - } - } -} - -/// BUILD EDGES - -// understanding: if the test is successful, where could we go? -fn edges_for<'a>( - path: &[PathInstruction], - branches: &[Branch<'a>], - test: GuardedTest<'a>, -) -> (GuardedTest<'a>, Vec>) { - let mut new_branches = Vec::new(); - - // if we test for a guard, skip all branches until one that has a guard - - let it = match test { - GuardedTest::GuardedNoTest { .. } | GuardedTest::Placeholder => { - let index = branches - .iter() - .position(|b| !b.guard.is_none()) - .expect("if testing for a guard, one branch must have a guard"); - - branches[index..].iter() - } - GuardedTest::TestNotGuarded { .. } => branches.iter(), - }; - - for branch in it { - new_branches.extend(to_relevant_branch(&test, path, branch)); - } - - (test, new_branches) -} - -fn to_relevant_branch<'a>( - guarded_test: &GuardedTest<'a>, - path: &[PathInstruction], - branch: &Branch<'a>, -) -> Option> { - // TODO remove clone - match extract(path, branch.patterns.clone()) { - Extract::NotFound => Some(branch.clone()), - Extract::Found { - start, - found_pattern: pattern, - end, - } => match guarded_test { - GuardedTest::Placeholder | GuardedTest::GuardedNoTest { .. } => { - // if there is no test, the pattern should not require any - debug_assert!( - matches!(pattern, Pattern::Identifier(_) | Pattern::Underscore,), - "{:?}", - pattern, - ); - - Some(branch.clone()) - } - GuardedTest::TestNotGuarded { test } => { - to_relevant_branch_help(test, path, start, end, branch, pattern) - } - }, - } -} - -fn to_relevant_branch_help<'a>( - test: &Test<'a>, - path: &[PathInstruction], - mut start: Vec<(Vec, Pattern<'a>)>, - end: Vec<(Vec, Pattern<'a>)>, - branch: &Branch<'a>, - pattern: Pattern<'a>, -) -> Option> { - use Pattern::*; - use Test::*; - - match pattern { - Identifier(_) | Underscore => Some(branch.clone()), - - RecordDestructure(destructs, _) => match test { - IsCtor { - ctor_name: test_name, - tag_id, - .. - } => { - debug_assert!(test_name == &CtorName::Tag(TagName(RECORD_TAG_NAME.into()))); - let sub_positions = destructs.into_iter().enumerate().map(|(index, destruct)| { - let pattern = match destruct.typ { - DestructType::Guard(guard) => guard.clone(), - DestructType::Required(_) => Pattern::Underscore, - }; - - let mut new_path = path.to_vec(); - new_path.push(PathInstruction { - index: index as u64, - tag_id: *tag_id, - }); - - (new_path, pattern) - }); - start.extend(sub_positions); - start.extend(end); - - Some(Branch { - goal: branch.goal, - guard: branch.guard.clone(), - patterns: start, - }) - } - _ => None, - }, - - OpaqueUnwrap { opaque, argument } => match test { - IsCtor { - ctor_name: test_opaque_tag_name, - tag_id, - .. - } => { - debug_assert_eq!(test_opaque_tag_name, &CtorName::Opaque(opaque)); - - let (argument, _) = *argument; - - let mut new_path = path.to_vec(); - new_path.push(PathInstruction { - index: 0, - tag_id: *tag_id, - }); - - start.push((new_path, argument)); - start.extend(end); - Some(Branch { - goal: branch.goal, - guard: branch.guard.clone(), - patterns: start, - }) - } - _ => None, - }, - - NewtypeDestructure { - tag_name, - arguments, - .. - } => match test { - IsCtor { - ctor_name: test_name, - tag_id: test_id, - .. - } if test_name.is_tag(&tag_name) => { - let tag_id = 0; - debug_assert_eq!(tag_id, *test_id); - - let sub_positions = - arguments - .into_iter() - .enumerate() - .map(|(index, (pattern, _))| { - let mut new_path = path.to_vec(); - new_path.push(PathInstruction { - index: index as u64, - tag_id, - }); - (new_path, pattern) - }); - start.extend(sub_positions); - start.extend(end); - - Some(Branch { - goal: branch.goal, - guard: branch.guard.clone(), - patterns: start, - }) - } - - _ => None, - }, - - AppliedTag { - tag_name, - tag_id, - arguments, - layout, - .. - } => { - match test { - IsCtor { - ctor_name: test_name, - tag_id: test_id, - .. - } if test_name.is_tag(&tag_name) => { - debug_assert_eq!(tag_id, *test_id); - - // the test matches the constructor of this pattern - match layout { - UnionLayout::NonRecursive( - [[Layout::Struct { - field_layouts: [_], .. - }]], - ) => { - // a one-element record equivalent - // Theory: Unbox doesn't have any value for us - debug_assert_eq!(arguments.len(), 1); - let arg = arguments[0].clone(); - { - // NOTE here elm unboxes, but we ignore that - // Path::Unbox(Box::new(path.clone())) - start.push((path.to_vec(), arg.0)); - start.extend(end); - } - } - UnionLayout::NonRecursive([_]) | UnionLayout::NonNullableUnwrapped(_) => { - let sub_positions = - arguments - .into_iter() - .enumerate() - .map(|(index, (pattern, _))| { - let mut new_path = path.to_vec(); - new_path.push(PathInstruction { - index: index as u64, - tag_id, - }); - (new_path, pattern) - }); - start.extend(sub_positions); - start.extend(end); - } - UnionLayout::NonRecursive(_) - | UnionLayout::Recursive(_) - | UnionLayout::NullableWrapped { .. } - | UnionLayout::NullableUnwrapped { .. } => { - let sub_positions = - arguments - .into_iter() - .enumerate() - .map(|(index, (pattern, _))| { - let mut new_path = path.to_vec(); - new_path.push(PathInstruction { - index: index as u64, - tag_id, - }); - (new_path, pattern) - }); - start.extend(sub_positions); - start.extend(end); - } - } - - Some(Branch { - goal: branch.goal, - guard: branch.guard.clone(), - patterns: start, - }) - } - _ => None, - } - } - StrLiteral(string) => match test { - IsStr(test_str) if string == *test_str => { - start.extend(end); - Some(Branch { - goal: branch.goal, - guard: branch.guard.clone(), - patterns: start, - }) - } - _ => None, - }, - - IntLiteral(int, p1) => match test { - IsInt(is_int, p2) if int == *is_int => { - debug_assert_eq!(p1, *p2); - start.extend(end); - Some(Branch { - goal: branch.goal, - guard: branch.guard.clone(), - patterns: start, - }) - } - _ => None, - }, - - FloatLiteral(float, p1) => match test { - IsFloat(test_float, p2) if float == *test_float => { - debug_assert_eq!(p1, *p2); - start.extend(end); - Some(Branch { - goal: branch.goal, - guard: branch.guard.clone(), - patterns: start, - }) - } - _ => None, - }, - - DecimalLiteral(dec) => match test { - IsDecimal(test_dec) if dec.eq(test_dec) => { - start.extend(end); - Some(Branch { - goal: branch.goal, - guard: branch.guard.clone(), - patterns: start, - }) - } - _ => None, - }, - - BitLiteral { value: bit, .. } => match test { - IsBit(test_bit) if bit == *test_bit => { - start.extend(end); - Some(Branch { - goal: branch.goal, - guard: branch.guard.clone(), - patterns: start, - }) - } - _ => None, - }, - - EnumLiteral { tag_id, .. } => match test { - IsByte { - tag_id: test_id, .. - } if tag_id == *test_id as _ => { - start.extend(end); - Some(Branch { - goal: branch.goal, - guard: branch.guard.clone(), - patterns: start, - }) - } - - _ => None, - }, - } -} - -enum Extract<'a> { - NotFound, - Found { - start: Vec<(Vec, Pattern<'a>)>, - found_pattern: Pattern<'a>, - end: Vec<(Vec, Pattern<'a>)>, - }, -} - -fn extract<'a>( - selected_path: &[PathInstruction], - path_patterns: Vec<(Vec, Pattern<'a>)>, -) -> Extract<'a> { - let mut start = Vec::new(); - - // TODO potential ordering problem - let mut it = path_patterns.into_iter(); - while let Some(current) = it.next() { - if current.0 == selected_path { - return Extract::Found { - start, - found_pattern: current.1, - end: it.collect::>(), - }; - } else { - start.push(current); - } - } - - Extract::NotFound -} - -/// FIND IRRELEVANT BRANCHES - -fn is_irrelevant_to<'a>(selected_path: &[PathInstruction], branch: &Branch<'a>) -> bool { - match branch - .patterns - .iter() - .find(|(path, _)| path == selected_path) - { - None => true, - Some((_, pattern)) => branch.guard.is_none() && !needs_tests(pattern), - } -} - -fn needs_tests(pattern: &Pattern) -> bool { - use Pattern::*; - - match pattern { - Identifier(_) | Underscore => false, - - RecordDestructure(_, _) - | NewtypeDestructure { .. } - | AppliedTag { .. } - | OpaqueUnwrap { .. } - | BitLiteral { .. } - | EnumLiteral { .. } - | IntLiteral(_, _) - | FloatLiteral(_, _) - | DecimalLiteral(_) - | StrLiteral(_) => true, - } -} - -/// PICK A PATH - -fn pick_path<'a>(branches: &'a [Branch]) -> &'a Vec { - let mut all_paths = Vec::with_capacity(branches.len()); - - // is choice path - for branch in branches { - for (path, pattern) in &branch.patterns { - // NOTE we no longer check for the guard here - // if !branch.guard.is_none() || needs_tests(&pattern) { - if needs_tests(pattern) { - all_paths.push(path); - } else { - // do nothing - } - } - } - - let mut by_small_defaults = bests_by_small_defaults(branches, all_paths.into_iter()); - - if by_small_defaults.len() == 1 { - by_small_defaults.remove(0) - } else { - debug_assert!(!by_small_defaults.is_empty()); - let mut result = bests_by_small_branching_factor(branches, by_small_defaults.into_iter()); - - match result.pop() { - None => unreachable!("bests_by will always return at least one value in the vec"), - Some(path) => path, - } - } -} - -fn bests_by_small_branching_factor<'a, I>( - branches: &[Branch], - mut all_paths: I, -) -> Vec<&'a Vec> -where - I: Iterator>, -{ - match all_paths.next() { - None => panic!("Cannot choose the best of zero paths. This should never happen."), - Some(first_path) => { - let mut min_weight = small_branching_factor(branches, first_path); - let mut min_paths = vec![first_path]; - - for path in all_paths { - let weight = small_branching_factor(branches, path); - - use std::cmp::Ordering; - match weight.cmp(&min_weight) { - Ordering::Equal => { - min_paths.push(path); - } - Ordering::Less => { - min_weight = weight; - min_paths.clear(); - min_paths.push(path); - } - Ordering::Greater => {} - } - } - - min_paths - } - } -} - -fn bests_by_small_defaults<'a, I>( - branches: &[Branch], - mut all_paths: I, -) -> Vec<&'a Vec> -where - I: Iterator>, -{ - match all_paths.next() { - None => panic!("Cannot choose the best of zero paths. This should never happen."), - Some(first_path) => { - let mut min_weight = small_defaults(branches, first_path); - let mut min_paths = vec![first_path]; - - for path in all_paths { - let weight = small_defaults(branches, path); - - use std::cmp::Ordering; - match weight.cmp(&min_weight) { - Ordering::Equal => { - min_paths.push(path); - } - Ordering::Less => { - min_weight = weight; - min_paths.clear(); - min_paths.push(path); - } - Ordering::Greater => {} - } - } - - min_paths - } - } -} - -/// PATH PICKING HEURISTICS - -fn small_defaults(branches: &[Branch], path: &[PathInstruction]) -> usize { - branches - .iter() - .filter(|b| is_irrelevant_to(path, b)) - .map(|_| 1) - .sum() -} - -fn small_branching_factor(branches: &[Branch], path: &[PathInstruction]) -> usize { - // a specialized version of gather_edges that just counts the number of options - - let relevant_tests = tests_at_path(path, branches); - - let check = guarded_tests_are_complete(&relevant_tests); - - let fallbacks = if check { - false - } else { - branches.iter().any(|b| is_irrelevant_to(path, b)) - }; - - relevant_tests.len() + (if !fallbacks { 0 } else { 1 }) -} - -#[derive(Clone, Debug, PartialEq)] -enum Decider<'a, T> { - Leaf(T), - Guarded { - /// after assigning to symbol, the stmt jumps to this label - id: JoinPointId, - stmt: Stmt<'a>, - pattern: Pattern<'a>, - - success: Box>, - failure: Box>, - }, - Chain { - test_chain: Vec<(Vec, Test<'a>)>, - success: Box>, - failure: Box>, - }, - FanOut { - path: Vec, - tests: Vec<(Test<'a>, Decider<'a, T>)>, - fallback: Box>, - }, -} - -#[derive(Clone, Debug, PartialEq)] -enum Choice<'a> { - Inline(Stmt<'a>), - Jump(Label), -} - -type StoresVec<'a> = bumpalo::collections::Vec<'a, (Symbol, Layout<'a>, Expr<'a>)>; - -pub fn optimize_when<'a>( - env: &mut Env<'a, '_>, - procs: &mut Procs<'a>, - layout_cache: &mut LayoutCache<'a>, - cond_symbol: Symbol, - cond_layout: Layout<'a>, - ret_layout: Layout<'a>, - opt_branches: bumpalo::collections::Vec<'a, (Pattern<'a>, Guard<'a>, Stmt<'a>)>, -) -> Stmt<'a> { - let (patterns, _indexed_branches) = opt_branches - .into_iter() - .enumerate() - .map(|(index, (pattern, guard, branch))| { - let has_guard = !guard.is_none(); - ( - (guard, pattern.clone(), index as u64), - (index as u64, branch, pattern, has_guard), - ) - }) - .unzip(); - - let indexed_branches: Vec<_> = _indexed_branches; - - let decision_tree = compile(patterns); - let decider = tree_to_decider(decision_tree); - - // for each target (branch body), count in how many ways it can be reached - let mut target_counts = bumpalo::vec![in env.arena; 0; indexed_branches.len()]; - count_targets(&mut target_counts, &decider); - - let mut choices = MutMap::default(); - let mut jumps = Vec::new(); - - for (index, mut branch, pattern, has_guard) in indexed_branches.into_iter() { - // bind the fields referenced in the pattern. For guards this happens separately, so - // the pattern variables are defined when evaluating the guard. - if !has_guard { - branch = - crate::ir::store_pattern(env, procs, layout_cache, &pattern, cond_symbol, branch); - } - - let ((branch_index, choice), opt_jump) = create_choices(&target_counts, index, branch); - - if let Some((index, body)) = opt_jump { - let id = JoinPointId(env.unique_symbol()); - jumps.push((index, id, body)); - } - - choices.insert(branch_index, choice); - } - - let choice_decider = insert_choices(&choices, decider); - - let mut stmt = decide_to_branching( - env, - procs, - layout_cache, - cond_symbol, - cond_layout, - ret_layout, - choice_decider, - &jumps, - ); - - for (_, id, body) in jumps.into_iter() { - stmt = Stmt::Join { - id, - parameters: &[], - body: env.arena.alloc(body), - remainder: env.arena.alloc(stmt), - }; - } - - stmt -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -struct PathInstruction { - index: u64, - tag_id: TagIdIntType, -} - -fn path_to_expr_help<'a>( - env: &mut Env<'a, '_>, - mut symbol: Symbol, - path: &[PathInstruction], - mut layout: Layout<'a>, -) -> (Symbol, StoresVec<'a>, Layout<'a>) { - let mut stores = bumpalo::collections::Vec::new_in(env.arena); - - // let instructions = reverse_path(path); - let instructions = path; - let mut it = instructions.iter().peekable(); - - while let Some(PathInstruction { index, tag_id }) = it.next() { - let index = *index; - - match &layout { - Layout::Union(union_layout) => { - let inner_expr = Expr::UnionAtIndex { - tag_id: *tag_id, - structure: symbol, - index, - union_layout: *union_layout, - }; - - let inner_layout = union_layout.layout_at(*tag_id as TagIdIntType, index as usize); - - symbol = env.unique_symbol(); - stores.push((symbol, inner_layout, inner_expr)); - - layout = inner_layout; - } - - Layout::Struct { field_layouts, .. } => { - debug_assert!(field_layouts.len() > 1); - - let inner_expr = Expr::StructAtIndex { - index, - field_layouts, - structure: symbol, - }; - - let inner_layout = field_layouts[index as usize]; - - symbol = env.unique_symbol(); - stores.push((symbol, inner_layout, inner_expr)); - - layout = inner_layout; - } - - _ => { - // this MUST be an index into a single-element (hence unwrapped) record - - debug_assert_eq!(index, 0, "{:?}", &layout); - debug_assert_eq!(*tag_id, 0); - debug_assert!(it.peek().is_none()); - - break; - } - } - } - - (symbol, stores, layout) -} - -fn test_to_equality<'a>( - env: &mut Env<'a, '_>, - cond_symbol: Symbol, - cond_layout: &Layout<'a>, - path: &[PathInstruction], - test: Test<'a>, -) -> (StoresVec<'a>, Symbol, Symbol, Option>) { - let (rhs_symbol, mut stores, test_layout) = - path_to_expr_help(env, cond_symbol, path, *cond_layout); - - match test { - Test::IsCtor { tag_id, union, .. } => { - let path_symbol = rhs_symbol; - // the IsCtor check should never be generated for tag unions of size 1 - // (e.g. record pattern guard matches) - debug_assert!(union.alternatives.len() > 1); - - match test_layout { - Layout::Union(union_layout) => { - let lhs = Expr::Literal(Literal::Int((tag_id as i128).to_ne_bytes())); - - let rhs = Expr::GetTagId { - structure: path_symbol, - union_layout, - }; - - let lhs_symbol = env.unique_symbol(); - let rhs_symbol = env.unique_symbol(); - - let tag_id_layout = union_layout.tag_id_layout(); - - stores.push((lhs_symbol, tag_id_layout, lhs)); - stores.push((rhs_symbol, tag_id_layout, rhs)); - - ( - stores, - lhs_symbol, - rhs_symbol, - Some(ConstructorKnown::OnlyPass { - scrutinee: path_symbol, - layout: *cond_layout, - tag_id, - }), - ) - } - _ => unreachable!("{:?}", (cond_layout, union)), - } - } - - Test::IsInt(test_int, precision) => { - // TODO don't downcast i128 here - debug_assert!(i128::from_ne_bytes(test_int) <= i64::MAX as i128); - let lhs = Expr::Literal(Literal::Int(test_int)); - let lhs_symbol = env.unique_symbol(); - stores.push((lhs_symbol, Layout::int_width(precision), lhs)); - - (stores, lhs_symbol, rhs_symbol, None) - } - - Test::IsFloat(test_int, precision) => { - // TODO maybe we can actually use i64 comparison here? - let test_float = f64::from_bits(test_int as u64); - let lhs = Expr::Literal(Literal::Float(test_float)); - let lhs_symbol = env.unique_symbol(); - stores.push((lhs_symbol, Layout::float_width(precision), lhs)); - - (stores, lhs_symbol, rhs_symbol, None) - } - - Test::IsDecimal(test_dec) => { - let lhs = Expr::Literal(Literal::Decimal(test_dec)); - let lhs_symbol = env.unique_symbol(); - stores.push((lhs_symbol, *cond_layout, lhs)); - - (stores, lhs_symbol, rhs_symbol, None) - } - - Test::IsByte { - tag_id: test_byte, .. - } => { - debug_assert!(test_byte <= (u8::MAX as u16)); - - let lhs = Expr::Literal(Literal::Byte(test_byte as u8)); - let lhs_symbol = env.unique_symbol(); - stores.push((lhs_symbol, Layout::u8(), lhs)); - - (stores, lhs_symbol, rhs_symbol, None) - } - - Test::IsBit(test_bit) => { - let lhs = Expr::Literal(Literal::Bool(test_bit)); - let lhs_symbol = env.unique_symbol(); - stores.push((lhs_symbol, Layout::Builtin(Builtin::Bool), lhs)); - - (stores, lhs_symbol, rhs_symbol, None) - } - - Test::IsStr(test_str) => { - let lhs = Expr::Literal(Literal::Str(env.arena.alloc(test_str))); - let lhs_symbol = env.unique_symbol(); - - stores.push((lhs_symbol, Layout::Builtin(Builtin::Str), lhs)); - - (stores, lhs_symbol, rhs_symbol, None) - } - } -} - -type Tests<'a> = std::vec::Vec<( - bumpalo::collections::Vec<'a, (Symbol, Layout<'a>, Expr<'a>)>, - Symbol, - Symbol, - Option>, -)>; - -fn stores_and_condition<'a>( - env: &mut Env<'a, '_>, - cond_symbol: Symbol, - cond_layout: &Layout<'a>, - test_chain: Vec<(Vec, Test<'a>)>, -) -> Tests<'a> { - let mut tests: Tests = Vec::with_capacity(test_chain.len()); - - // Assumption: there is at most 1 guard, and it is the outer layer. - for (path, test) in test_chain { - tests.push(test_to_equality(env, cond_symbol, cond_layout, &path, test)) - } - - tests -} - -fn compile_test<'a>( - env: &mut Env<'a, '_>, - ret_layout: Layout<'a>, - stores: bumpalo::collections::Vec<'a, (Symbol, Layout<'a>, Expr<'a>)>, - lhs: Symbol, - rhs: Symbol, - fail: &'a Stmt<'a>, - cond: Stmt<'a>, -) -> Stmt<'a> { - compile_test_help( - env, - ConstructorKnown::Neither, - ret_layout, - stores, - lhs, - rhs, - fail, - cond, - ) -} - -#[allow(clippy::too_many_arguments)] -fn compile_test_help<'a>( - env: &mut Env<'a, '_>, - branch_info: ConstructorKnown<'a>, - ret_layout: Layout<'a>, - stores: bumpalo::collections::Vec<'a, (Symbol, Layout<'a>, Expr<'a>)>, - lhs: Symbol, - rhs: Symbol, - fail: &'a Stmt<'a>, - mut cond: Stmt<'a>, -) -> Stmt<'a> { - // if test_symbol then cond else fail - let test_symbol = env.unique_symbol(); - let arena = env.arena; - - let (pass_info, fail_info) = { - use ConstructorKnown::*; - match branch_info { - Both { - scrutinee, - layout, - pass, - fail, - } => { - let pass_info = BranchInfo::Constructor { - scrutinee, - layout, - tag_id: pass, - }; - let fail_info = BranchInfo::Constructor { - scrutinee, - layout, - tag_id: fail, - }; - - (pass_info, fail_info) - } - - OnlyPass { - scrutinee, - layout, - tag_id, - } => { - let pass_info = BranchInfo::Constructor { - scrutinee, - layout, - tag_id, - }; - - (pass_info, BranchInfo::None) - } - - Neither => (BranchInfo::None, BranchInfo::None), - } - }; - - let branches = env.arena.alloc([(1u64, pass_info, cond)]); - let default_branch = (fail_info, &*env.arena.alloc(fail.clone())); - - cond = Stmt::Switch { - cond_symbol: test_symbol, - cond_layout: Layout::Builtin(Builtin::Bool), - ret_layout, - branches, - default_branch, - }; - - let op = LowLevel::Eq; - let test = Expr::Call(crate::ir::Call { - call_type: crate::ir::CallType::LowLevel { - op, - update_mode: env.next_update_mode_id(), - }, - arguments: arena.alloc([lhs, rhs]), - }); - - // write to the test symbol - cond = Stmt::Let( - test_symbol, - test, - Layout::Builtin(Builtin::Bool), - arena.alloc(cond), - ); - - // stores are in top-to-bottom order, so we have to add them in reverse - for (symbol, layout, expr) in stores.into_iter().rev() { - cond = Stmt::Let(symbol, expr, layout, arena.alloc(cond)); - } - - cond -} - -fn compile_tests<'a>( - env: &mut Env<'a, '_>, - ret_layout: Layout<'a>, - tests: Tests<'a>, - fail: &'a Stmt<'a>, - mut cond: Stmt<'a>, -) -> Stmt<'a> { - for (new_stores, lhs, rhs, opt_constructor_info) in tests.into_iter() { - match opt_constructor_info { - None => { - cond = compile_test(env, ret_layout, new_stores, lhs, rhs, fail, cond); - } - Some(cinfo) => { - cond = compile_test_help(env, cinfo, ret_layout, new_stores, lhs, rhs, fail, cond); - } - } - } - cond -} - -#[derive(Debug)] -enum ConstructorKnown<'a> { - Both { - scrutinee: Symbol, - layout: Layout<'a>, - pass: TagIdIntType, - fail: TagIdIntType, - }, - OnlyPass { - scrutinee: Symbol, - layout: Layout<'a>, - tag_id: TagIdIntType, - }, - Neither, -} - -impl<'a> ConstructorKnown<'a> { - fn from_test_chain( - cond_symbol: Symbol, - cond_layout: &Layout<'a>, - test_chain: &[(Vec, Test)], - ) -> Self { - match test_chain { - [(path, test)] => match test { - Test::IsCtor { tag_id, union, .. } if path.is_empty() => { - if union.alternatives.len() == 2 { - // excluded middle: we also know the tag_id in the fail branch - ConstructorKnown::Both { - layout: *cond_layout, - scrutinee: cond_symbol, - pass: *tag_id, - fail: (*tag_id == 0) as _, - } - } else { - ConstructorKnown::OnlyPass { - layout: *cond_layout, - scrutinee: cond_symbol, - tag_id: *tag_id, - } - } - } - _ => ConstructorKnown::Neither, - }, - _ => ConstructorKnown::Neither, - } - } -} - -// TODO procs and layout are currently unused, but potentially required -// for defining optional fields? -// if not, do remove -#[allow(clippy::too_many_arguments, clippy::needless_collect)] -fn decide_to_branching<'a>( - env: &mut Env<'a, '_>, - procs: &mut Procs<'a>, - layout_cache: &mut LayoutCache<'a>, - cond_symbol: Symbol, - cond_layout: Layout<'a>, - ret_layout: Layout<'a>, - decider: Decider<'a, Choice<'a>>, - jumps: &[(u64, JoinPointId, Stmt<'a>)], -) -> Stmt<'a> { - use Choice::*; - use Decider::*; - - let arena = env.arena; - - match decider { - Leaf(Jump(label)) => { - let index = jumps - .binary_search_by_key(&label, |r| r.0) - .expect("jump not in list of jumps"); - - Stmt::Jump(jumps[index].1, &[]) - } - Leaf(Inline(expr)) => expr, - Guarded { - id, - stmt, - pattern, - success, - failure, - } => { - // the guard is the final thing that we check, so needs to be layered on first! - let test_symbol = env.unique_symbol(); - let arena = env.arena; - - let pass_expr = decide_to_branching( - env, - procs, - layout_cache, - cond_symbol, - cond_layout, - ret_layout, - *success, - jumps, - ); - - let fail_expr = decide_to_branching( - env, - procs, - layout_cache, - cond_symbol, - cond_layout, - ret_layout, - *failure, - jumps, - ); - - let decide = crate::ir::cond( - env, - test_symbol, - Layout::Builtin(Builtin::Bool), - pass_expr, - fail_expr, - ret_layout, - ); - - // calculate the guard value - let param = Param { - symbol: test_symbol, - layout: Layout::Builtin(Builtin::Bool), - borrow: false, - }; - - let join = Stmt::Join { - id, - parameters: arena.alloc([param]), - remainder: arena.alloc(stmt), - body: arena.alloc(decide), - }; - - crate::ir::store_pattern(env, procs, layout_cache, &pattern, cond_symbol, join) - } - Chain { - test_chain, - success, - failure, - } => { - // generate a (nested) if-then-else - - let pass_expr = decide_to_branching( - env, - procs, - layout_cache, - cond_symbol, - cond_layout, - ret_layout, - *success, - jumps, - ); - - let fail_expr = decide_to_branching( - env, - procs, - layout_cache, - cond_symbol, - cond_layout, - ret_layout, - *failure, - jumps, - ); - - let chain_branch_info = - ConstructorKnown::from_test_chain(cond_symbol, &cond_layout, &test_chain); - - let tests = stores_and_condition(env, cond_symbol, &cond_layout, test_chain); - - let number_of_tests = tests.len() as i64; - - debug_assert!(number_of_tests > 0); - - let fail = env.arena.alloc(fail_expr); - if number_of_tests == 1 { - // if there is just one test, compile to a simple if-then-else - - let (new_stores, lhs, rhs, _cinfo) = tests.into_iter().next().unwrap(); - - compile_test_help( - env, - chain_branch_info, - ret_layout, - new_stores, - lhs, - rhs, - fail, - pass_expr, - ) - } else { - // otherwise, we use a join point so the code for the `else` case - // is only generated once. - let fail_jp_id = JoinPointId(env.unique_symbol()); - let jump = arena.alloc(Stmt::Jump(fail_jp_id, &[])); - - let test_stmt = compile_tests(env, ret_layout, tests, jump, pass_expr); - - Stmt::Join { - id: fail_jp_id, - parameters: &[], - body: fail, - remainder: arena.alloc(test_stmt), - } - } - } - FanOut { - path, - tests, - fallback, - } => { - // the cond_layout can change in the process. E.g. if the cond is a Tag, we actually - // switch on the tag discriminant (currently an i64 value) - // NOTE the tag discriminant is not actually loaded, `cond` can point to a tag - let (inner_cond_symbol, cond_stores_vec, inner_cond_layout) = - path_to_expr_help(env, cond_symbol, &path, cond_layout); - - let default_branch = decide_to_branching( - env, - procs, - layout_cache, - cond_symbol, - cond_layout, - ret_layout, - *fallback, - jumps, - ); - - let mut branches = bumpalo::collections::Vec::with_capacity_in(tests.len(), env.arena); - - let mut tag_id_sum: i64 = (0..tests.len() as i64 + 1).sum(); - let mut union_size: i64 = -1; - - for (test, decider) in tests { - let branch = decide_to_branching( - env, - procs, - layout_cache, - cond_symbol, - cond_layout, - ret_layout, - decider, - jumps, - ); - - let tag = match test { - Test::IsInt(v, _) => i128::from_ne_bytes(v) as u64, - Test::IsFloat(v, _) => v as u64, - Test::IsBit(v) => v as u64, - Test::IsByte { tag_id, .. } => tag_id as u64, - Test::IsCtor { tag_id, .. } => tag_id as u64, - other => todo!("other {:?}", other), - }; - - // branch info is only useful for refcounted values - let branch_info = if let Test::IsCtor { tag_id, union, .. } = test { - tag_id_sum -= tag_id as i64; - union_size = union.alternatives.len() as i64; - - BranchInfo::Constructor { - scrutinee: inner_cond_symbol, - layout: inner_cond_layout, - tag_id, - } - } else { - tag_id_sum = -1; - BranchInfo::None - }; - - branches.push((tag, branch_info, branch)); - } - - // determine if the switch is exhaustive - let default_branch_info = if tag_id_sum > 0 && union_size > 0 { - BranchInfo::Constructor { - scrutinee: inner_cond_symbol, - layout: inner_cond_layout, - tag_id: tag_id_sum as u8 as _, - } - } else { - BranchInfo::None - }; - - // We have learned more about the exact layout of the cond (based on the path) - // but tests are still relative to the original cond symbol - let mut switch = if let Layout::Union(union_layout) = inner_cond_layout { - let tag_id_symbol = env.unique_symbol(); - - let temp = Stmt::Switch { - cond_layout: union_layout.tag_id_layout(), - cond_symbol: tag_id_symbol, - branches: branches.into_bump_slice(), - default_branch: (default_branch_info, env.arena.alloc(default_branch)), - ret_layout, - }; - - let expr = Expr::GetTagId { - structure: inner_cond_symbol, - union_layout, - }; - - Stmt::Let( - tag_id_symbol, - expr, - union_layout.tag_id_layout(), - env.arena.alloc(temp), - ) - } else { - Stmt::Switch { - cond_layout: inner_cond_layout, - cond_symbol: inner_cond_symbol, - branches: branches.into_bump_slice(), - default_branch: (default_branch_info, env.arena.alloc(default_branch)), - ret_layout, - } - }; - - for (symbol, layout, expr) in cond_stores_vec.into_iter().rev() { - switch = Stmt::Let(symbol, expr, layout, env.arena.alloc(switch)); - } - - // make a jump table based on the tests - switch - } - } -} - -/* -fn boolean_all<'a>(arena: &'a Bump, tests: Vec<(Expr<'a>, Expr<'a>, Layout<'a>)>) -> Expr<'a> { - let mut expr = Expr::Bool(true); - - for (lhs, rhs, layout) in tests.into_iter().rev() { - let test = Expr::RunLowLevel( - LowLevel::Eq, - bumpalo::vec![in arena; (lhs, layout.clone()), (rhs, layout.clone())].into_bump_slice(), - ); - - expr = Expr::RunLowLevel( - LowLevel::And, - arena.alloc([ - (test, Layout::Builtin(Builtin::Int1)), - (expr, Layout::Builtin(Builtin::Int1)), - ]), - ); - } - - expr -} -*/ - -/// TREE TO DECIDER -/// -/// Decision trees may have some redundancies, so we convert them to a Decider -/// which has special constructs to avoid code duplication when possible. - -/// If a test always succeeds, we don't need to branch on it -/// this saves on work and jumps -fn test_always_succeeds(test: &Test) -> bool { - match test { - Test::IsCtor { union, .. } => union.alternatives.len() == 1, - _ => false, - } -} - -fn tree_to_decider(tree: DecisionTree) -> Decider { - use Decider::*; - use DecisionTree::*; - - match tree { - Match(target) => Leaf(target), - - Decision { - path, - mut edges, - default, - } => match default { - None => match edges.len() { - 0 => panic!("compiler bug, somehow created an empty decision tree"), - 1 => { - let (_, sub_tree) = edges.remove(0); - - tree_to_decider(sub_tree) - } - 2 => { - let (_, failure_tree) = edges.remove(1); - let (guarded_test, success_tree) = edges.remove(0); - - chain_decider(path, guarded_test, failure_tree, success_tree) - } - - _ => { - let fallback = edges.remove(edges.len() - 1).1; - - fanout_decider(path, fallback, edges) - } - }, - - Some(last) => match edges.len() { - 0 => tree_to_decider(*last), - 1 => { - let failure_tree = *last; - let (guarded_test, success_tree) = edges.remove(0); - - chain_decider(path, guarded_test, failure_tree, success_tree) - } - - _ => { - let fallback = *last; - - fanout_decider(path, fallback, edges) - } - }, - }, - } -} - -fn fanout_decider<'a>( - path: Vec, - fallback: DecisionTree<'a>, - edges: Vec<(GuardedTest<'a>, DecisionTree<'a>)>, -) -> Decider<'a, u64> { - let fallback_decider = tree_to_decider(fallback); - let necessary_tests = edges - .into_iter() - .map(|(test, tree)| fanout_decider_help(tree, test)) - .collect(); - - Decider::FanOut { - path, - tests: necessary_tests, - fallback: Box::new(fallback_decider), - } -} - -fn fanout_decider_help<'a>( - dectree: DecisionTree<'a>, - guarded_test: GuardedTest<'a>, -) -> (Test<'a>, Decider<'a, u64>) { - match guarded_test { - GuardedTest::Placeholder | GuardedTest::GuardedNoTest { .. } => { - unreachable!("this would not end up in a switch") - } - GuardedTest::TestNotGuarded { test } => { - let decider = tree_to_decider(dectree); - (test, decider) - } - } -} - -fn chain_decider<'a>( - path: Vec, - guarded_test: GuardedTest<'a>, - failure_tree: DecisionTree<'a>, - success_tree: DecisionTree<'a>, -) -> Decider<'a, u64> { - match guarded_test { - GuardedTest::GuardedNoTest { id, stmt, pattern } => { - let failure = Box::new(tree_to_decider(failure_tree)); - let success = Box::new(tree_to_decider(success_tree)); - - Decider::Guarded { - id, - stmt, - pattern, - success, - failure: failure.clone(), - } - } - GuardedTest::TestNotGuarded { test } => { - if test_always_succeeds(&test) { - tree_to_decider(success_tree) - } else { - to_chain(path, test, success_tree, failure_tree) - } - } - - GuardedTest::Placeholder => { - // ? - tree_to_decider(success_tree) - } - } -} - -fn to_chain<'a>( - path: Vec, - test: Test<'a>, - success_tree: DecisionTree<'a>, - failure_tree: DecisionTree<'a>, -) -> Decider<'a, u64> { - use Decider::*; - - let failure = tree_to_decider(failure_tree); - - match tree_to_decider(success_tree) { - Chain { - mut test_chain, - success, - failure: sub_failure, - } if failure == *sub_failure => { - test_chain.push((path, test)); - - Chain { - test_chain, - success, - failure: Box::new(failure), - } - } - - success => Chain { - test_chain: vec![(path, test)], - success: Box::new(success), - failure: Box::new(failure), - }, - } -} - -/// INSERT CHOICES -/// -/// If a target appears exactly once in a Decider, the corresponding expression -/// can be inlined. Whether things are inlined or jumps is called a "choice". - -fn count_targets(targets: &mut bumpalo::collections::Vec, initial: &Decider) { - use Decider::*; - - let mut stack = vec![initial]; - - while let Some(decision_tree) = stack.pop() { - match decision_tree { - Leaf(target) => { - targets[*target as usize] += 1; - } - - Guarded { - success, failure, .. - } => { - stack.push(success); - stack.push(failure); - } - - Chain { - success, failure, .. - } => { - stack.push(success); - stack.push(failure); - } - - FanOut { - tests, fallback, .. - } => { - stack.push(fallback); - - for (_, decider) in tests { - stack.push(decider); - } - } - } - } -} - -#[allow(clippy::type_complexity)] -fn create_choices<'a>( - target_counts: &bumpalo::collections::Vec<'a, u64>, - target: u64, - branch: Stmt<'a>, -) -> ((u64, Choice<'a>), Option<(u64, Stmt<'a>)>) { - match target_counts.get(target as usize) { - None => unreachable!( - "this should never happen: {:?} not in {:?}", - target, target_counts - ), - Some(1) => ((target, Choice::Inline(branch)), None), - Some(_) => ((target, Choice::Jump(target)), Some((target, branch))), - } -} - -fn insert_choices<'a>( - choice_dict: &MutMap>, - decider: Decider<'a, u64>, -) -> Decider<'a, Choice<'a>> { - use Decider::*; - match decider { - Leaf(target) => { - // TODO remove clone - // Only targes that appear once are Inline, so it's safe to remove them from the dict. - Leaf(choice_dict[&target].clone()) - } - - Guarded { - id, - stmt, - pattern, - success, - failure, - } => Guarded { - id, - stmt, - pattern, - success: Box::new(insert_choices(choice_dict, *success)), - failure: Box::new(insert_choices(choice_dict, *failure)), - }, - - Chain { - test_chain, - success, - failure, - } => Chain { - test_chain, - success: Box::new(insert_choices(choice_dict, *success)), - failure: Box::new(insert_choices(choice_dict, *failure)), - }, - - FanOut { - path, - tests, - fallback, - } => FanOut { - path, - tests: tests - .into_iter() - .map(|(test, nested)| (test, insert_choices(choice_dict, nested))) - .collect(), - fallback: Box::new(insert_choices(choice_dict, *fallback)), - }, - } -} diff --git a/compiler/mono/src/inc_dec.rs b/compiler/mono/src/inc_dec.rs deleted file mode 100644 index 278d8a9a76..0000000000 --- a/compiler/mono/src/inc_dec.rs +++ /dev/null @@ -1,1437 +0,0 @@ -use crate::borrow::{ParamMap, BORROWED, OWNED}; -use crate::ir::{ - CallType, Expr, HigherOrderLowLevel, JoinPointId, ModifyRc, Param, Proc, ProcLayout, Stmt, - UpdateModeIds, -}; -use crate::layout::Layout; -use bumpalo::collections::Vec; -use bumpalo::Bump; -use roc_collections::all::{MutMap, MutSet}; -use roc_module::symbol::{IdentIds, ModuleId, Symbol}; - -/// Data and Function ownership relation for higher-order lowlevels -/// -/// Normally, the borrowing algorithm figures out how to own/borrow data so that -/// the borrows match up. But that fails for our higher-order lowlevels, because -/// they are rigid (cannot add extra inc/dec in there dynamically) and opaque to -/// the borrow inference. -/// -/// So, we must fix this up ourselves. This code is deliberately verbose to make -/// it easier to understand without full context. -/// -/// If we take `List.map list f` as an example, then there are two orders of freedom: -/// -/// - `list` can be either owned or borrowed by `List.map` -/// - `f` can require either owned or borrowed elements from `list` -/// -/// Hence we get the four options below: the data (`list` in the example) is owned or borrowed by -/// the higher-order function, and the function argument (`f`) either owns or borrows the elements. -#[allow(clippy::enum_variant_names)] -#[derive(Debug, Clone, Copy)] -#[repr(u8)] -enum DataFunction { - /// `list` is owned, and `f` expects owned values. That means that when we run the map, all - /// list elements will be consumed (because they are passed to `f`, which takes owned values) - /// Because we own the whole list, and must consume it, we need to `decref` the list. - /// `decref` just decrements the container, and will never recursively decrement elements - DataOwnedFunctionOwns, - /// `list` is owned, and `f` expects borrowed values. After running `f` for each element, the - /// elements are not consumed, and neither is the list. We must consume it though, because we - /// own the `list`. Therefore we need to perform a `dec` - DataOwnedFunctionBorrows, - /// `list` is borrowed, `f` borrows, so the trivial implementation is correct: just map `f` - /// over elements of `list`, and don't do anything else. - DataBorrowedFunctionBorrows, - /// The trickiest case: we only borrow the `list`, but the mapped function `f` needs owned - /// values. There are two options - /// - /// - define some `fBorrow` that takes a borrowed value, `inc`'s it (similar to `.clone()` on - /// an `Rc` in rust) and then passes the (now owned) value to `f`, then rewrite the call - /// to `List.map list fBorrow` - /// - `inc` the list (which recursively increments the elements), map `f` over the list, - /// consuming one RC token on the elements, finally `decref` the list. - /// - /// For simplicity, we use the second option right now, but the first option is probably - /// preferable long-term. - DataBorrowedFunctionOwns, -} - -impl DataFunction { - fn new(vars: &VarMap, lowlevel_argument: Symbol, passed_function_argument: Param) -> Self { - use DataFunction::*; - - let data_borrowed = !vars[&lowlevel_argument].consume; - let function_borrows = passed_function_argument.borrow; - - match (data_borrowed, function_borrows) { - (BORROWED, BORROWED) => DataBorrowedFunctionBorrows, - (BORROWED, OWNED) => DataBorrowedFunctionOwns, - (OWNED, BORROWED) => DataOwnedFunctionBorrows, - (OWNED, OWNED) => DataOwnedFunctionOwns, - } - } -} - -pub fn free_variables(stmt: &Stmt<'_>) -> MutSet { - let (mut occurring, bound) = occurring_variables(stmt); - - for ref s in bound { - occurring.remove(s); - } - - occurring -} - -pub fn occurring_variables(stmt: &Stmt<'_>) -> (MutSet, MutSet) { - let mut stack = std::vec![stmt]; - let mut result = MutSet::default(); - let mut bound_variables = MutSet::default(); - - while let Some(stmt) = stack.pop() { - use Stmt::*; - - match stmt { - Let(symbol, expr, _, cont) => { - occurring_variables_expr(expr, &mut result); - result.insert(*symbol); - bound_variables.insert(*symbol); - stack.push(cont); - } - - Ret(symbol) => { - result.insert(*symbol); - } - - Refcounting(modify, cont) => { - let symbol = modify.get_symbol(); - result.insert(symbol); - stack.push(cont); - } - - Expect { - condition, - remainder, - .. - } => { - result.insert(*condition); - stack.push(remainder); - } - - Jump(_, arguments) => { - result.extend(arguments.iter().copied()); - } - - Join { - parameters, - body: continuation, - remainder, - .. - } => { - result.extend(parameters.iter().map(|p| p.symbol)); - - stack.push(continuation); - stack.push(remainder); - } - - Switch { - cond_symbol, - branches, - default_branch, - .. - } => { - result.insert(*cond_symbol); - - stack.extend(branches.iter().map(|(_, _, s)| s)); - stack.push(default_branch.1); - } - - RuntimeError(_) => {} - } - } - - (result, bound_variables) -} - -fn occurring_variables_call(call: &crate::ir::Call<'_>, result: &mut MutSet) { - // NOTE though the function name does occur, it is a static constant in the program - // for liveness, it should not be included here. - result.extend(call.arguments.iter().copied()); -} - -pub fn occurring_variables_expr(expr: &Expr<'_>, result: &mut MutSet) { - use Expr::*; - - match expr { - StructAtIndex { - structure: symbol, .. - } => { - result.insert(*symbol); - } - - Call(call) => occurring_variables_call(call, result), - - Array { - elems: arguments, .. - } => result.extend(arguments.iter().filter_map(|e| e.to_symbol())), - - Tag { arguments, .. } | Struct(arguments) => { - result.extend(arguments.iter().copied()); - } - Reuse { - symbol, arguments, .. - } => { - result.extend(arguments.iter().copied()); - result.insert(*symbol); - } - Reset { symbol: x, .. } => { - result.insert(*x); - } - - ExprBox { symbol } | ExprUnbox { symbol } => { - result.insert(*symbol); - } - - EmptyArray | RuntimeErrorFunction(_) | Literal(_) => {} - - GetTagId { - structure: symbol, .. - } => { - result.insert(*symbol); - } - - UnionAtIndex { - structure: symbol, .. - } => { - result.insert(*symbol); - } - } -} - -/* Insert explicit RC instructions. So, it assumes the input code does not contain `inc` nor `dec` instructions. - This transformation is applied before lower level optimizations - that introduce the instructions `release` and `set` -*/ - -#[derive(Clone, Debug, Copy)] -struct VarInfo { - reference: bool, // true if the variable may be a reference (aka pointer) at runtime - persistent: bool, // true if the variable is statically known to be marked a Persistent at runtime - consume: bool, // true if the variable RC must be "consumed" - reset: bool, // true if the variable is the result of a Reset operation -} - -type VarMap = MutMap; -pub type LiveVarSet = MutSet; -pub type JPLiveVarMap = MutMap; - -#[derive(Clone, Debug)] -struct Context<'a> { - arena: &'a Bump, - vars: VarMap, - jp_live_vars: JPLiveVarMap, // map: join point => live variables - param_map: &'a ParamMap<'a>, -} - -fn update_live_vars<'a>(expr: &Expr<'a>, v: &LiveVarSet) -> LiveVarSet { - let mut v = v.clone(); - - occurring_variables_expr(expr, &mut v); - - v -} - -/// `isFirstOcc xs x i = true` if `xs[i]` is the first occurrence of `xs[i]` in `xs` -fn is_first_occurrence(xs: &[Symbol], i: usize) -> bool { - match xs.get(i) { - None => unreachable!(), - Some(s) => i == xs.iter().position(|v| s == v).unwrap(), - } -} - -/// Return `n`, the number of times `x` is consumed. -/// - `ys` is a sequence of instruction parameters where we search for `x`. -/// - `consumeParamPred i = true` if parameter `i` is consumed. -fn get_num_consumptions(x: Symbol, ys: &[Symbol], consume_param_pred: F) -> usize -where - F: Fn(usize) -> bool, -{ - let mut n = 0; - - for (i, y) in ys.iter().enumerate() { - if x == *y && consume_param_pred(i) { - n += 1; - } - } - n -} - -/// Return true if `x` also occurs in `ys` in a position that is not consumed. -/// That is, it is also passed as a borrow reference. -fn is_borrow_param_help(x: Symbol, ys: &[Symbol], consume_param_pred: F) -> bool -where - F: Fn(usize) -> bool, -{ - ys.iter() - .enumerate() - .any(|(i, y)| x == *y && !consume_param_pred(i)) -} - -fn is_borrow_param(x: Symbol, ys: &[Symbol], ps: &[Param]) -> bool { - // default to owned arguments - let is_owned = |i: usize| match ps.get(i) { - Some(param) => !param.borrow, - None => unreachable!("or?"), - }; - is_borrow_param_help(x, ys, is_owned) -} - -// We do not need to consume the projection of a variable that is not consumed -fn consume_expr(m: &VarMap, e: &Expr<'_>) -> bool { - match e { - Expr::StructAtIndex { structure: x, .. } => match m.get(x) { - Some(info) => info.consume, - None => true, - }, - Expr::UnionAtIndex { structure: x, .. } => match m.get(x) { - Some(info) => info.consume, - None => true, - }, - _ => true, - } -} - -impl<'a> Context<'a> { - pub fn new(arena: &'a Bump, param_map: &'a ParamMap<'a>) -> Self { - let mut vars = MutMap::default(); - - for symbol in param_map.iter_symbols() { - vars.insert( - *symbol, - VarInfo { - reference: false, // assume function symbols are global constants - persistent: true, // assume function symbols are global constants - consume: false, // no need to consume this variable - reset: false, // reset symbols cannot be passed as function arguments - }, - ); - } - - Self { - arena, - vars, - jp_live_vars: MutMap::default(), - param_map, - } - } - - fn get_var_info(&self, symbol: Symbol) -> VarInfo { - match self.vars.get(&symbol) { - Some(info) => *info, - None => { - eprintln!( - "Symbol {:?} {} has no info in self.vars", - symbol, - symbol, // self.vars - ); - - VarInfo { - persistent: true, - reference: false, - consume: false, - reset: false, - } - } - } - } - - fn add_inc(&self, symbol: Symbol, inc_amount: u64, stmt: &'a Stmt<'a>) -> &'a Stmt<'a> { - debug_assert!(inc_amount > 0); - - let info = self.get_var_info(symbol); - - if info.persistent { - // persistent values are never reference counted - return stmt; - } - - // if this symbol is never a reference, don't emit - if !info.reference { - return stmt; - } - - let modify = ModifyRc::Inc(symbol, inc_amount); - self.arena.alloc(Stmt::Refcounting(modify, stmt)) - } - - fn add_dec(&self, symbol: Symbol, stmt: &'a Stmt<'a>) -> &'a Stmt<'a> { - let info = self.get_var_info(symbol); - - if info.persistent { - // persistent values are never reference counted - return stmt; - } - - // if this symbol is never a reference, don't emit - if !info.reference { - return stmt; - } - - let modify = if info.reset { - ModifyRc::DecRef(symbol) - } else { - ModifyRc::Dec(symbol) - }; - - self.arena.alloc(Stmt::Refcounting(modify, stmt)) - } - - fn add_inc_before_consume_all( - &self, - xs: &[Symbol], - b: &'a Stmt<'a>, - live_vars_after: &LiveVarSet, - ) -> &'a Stmt<'a> { - self.add_inc_before_help(xs, |_: usize| true, b, live_vars_after) - } - - fn add_inc_before_help( - &self, - xs: &[Symbol], - consume_param_pred: F, - mut b: &'a Stmt<'a>, - live_vars_after: &LiveVarSet, - ) -> &'a Stmt<'a> - where - F: Fn(usize) -> bool + Clone, - { - for (i, x) in xs.iter().enumerate() { - let info = self.get_var_info(*x); - if !info.reference || !is_first_occurrence(xs, i) { - // do nothing - } else { - let num_consumptions = get_num_consumptions(*x, xs, consume_param_pred.clone()); // number of times the argument is used - - // `x` is not a variable that must be consumed by the current procedure - let need_not_consume = !info.consume; - - // `x` is live after executing instruction - let is_live_after = live_vars_after.contains(x); - - // `x` is used in a position that is passed as a borrow reference - let is_borrowed = is_borrow_param_help(*x, xs, consume_param_pred.clone()); - - let num_incs = if need_not_consume || is_live_after || is_borrowed { - num_consumptions - } else { - num_consumptions - 1 - }; - - if num_incs >= 1 { - b = self.add_inc(*x, num_incs as u64, b) - } - } - } - b - } - - fn add_inc_before( - &self, - xs: &[Symbol], - ps: &[Param], - b: &'a Stmt<'a>, - live_vars_after: &LiveVarSet, - ) -> &'a Stmt<'a> { - // default to owned arguments - let pred = |i: usize| match ps.get(i) { - Some(param) => !param.borrow, - None => unreachable!("or?"), - }; - self.add_inc_before_help(xs, pred, b, live_vars_after) - } - - fn add_dec_if_needed( - &self, - x: Symbol, - b: &'a Stmt<'a>, - b_live_vars: &LiveVarSet, - ) -> &'a Stmt<'a> { - if self.must_consume(x) && !b_live_vars.contains(&x) { - self.add_dec(x, b) - } else { - b - } - } - - fn must_consume(&self, x: Symbol) -> bool { - let info = self.get_var_info(x); - info.reference && info.consume - } - - fn add_dec_after_application( - &self, - xs: &[Symbol], - ps: &[Param], - mut b: &'a Stmt<'a>, - b_live_vars: &LiveVarSet, - ) -> &'a Stmt<'a> { - for (i, x) in xs.iter().enumerate() { - // We must add a `dec` if `x` must be consumed, it is alive after the application, - // and it has been borrowed by the application. - // Remark: `x` may occur multiple times in the application (e.g., `f x y x`). - // This is why we check whether it is the first occurrence. - if self.must_consume(*x) - && is_first_occurrence(xs, i) - && is_borrow_param(*x, xs, ps) - && !b_live_vars.contains(x) - { - b = self.add_dec(*x, b); - } - } - - b - } - - fn add_dec_after_lowlevel( - &self, - xs: &[Symbol], - ps: &[bool], - mut b: &'a Stmt<'a>, - b_live_vars: &LiveVarSet, - ) -> &'a Stmt<'a> { - for (i, (x, is_borrow)) in xs.iter().zip(ps.iter()).enumerate() { - /* We must add a `dec` if `x` must be consumed, it is alive after the application, - and it has been borrowed by the application. - Remark: `x` may occur multiple times in the application (e.g., `f x y x`). - This is why we check whether it is the first occurrence. */ - - if self.must_consume(*x) - && is_first_occurrence(xs, i) - && *is_borrow - && !b_live_vars.contains(x) - { - b = self.add_dec(*x, b); - } - } - - b - } - - #[allow(clippy::too_many_arguments)] - fn visit_call<'i>( - &self, - codegen: &mut CodegenTools<'i>, - z: Symbol, - call_type: crate::ir::CallType<'a>, - arguments: &'a [Symbol], - l: Layout<'a>, - b: &'a Stmt<'a>, - b_live_vars: &LiveVarSet, - ) -> &'a Stmt<'a> { - use crate::ir::CallType::*; - - match &call_type { - LowLevel { op, .. } => { - let ps = crate::borrow::lowlevel_borrow_signature(self.arena, *op); - let b = self.add_dec_after_lowlevel(arguments, ps, b, b_live_vars); - - let v = Expr::Call(crate::ir::Call { - call_type, - arguments, - }); - - &*self.arena.alloc(Stmt::Let(z, v, l, b)) - } - - HigherOrder(lowlevel) => { - self.visit_higher_order_lowlevel(codegen, z, lowlevel, arguments, l, b, b_live_vars) - } - - Foreign { .. } => { - let ps = crate::borrow::foreign_borrow_signature(self.arena, arguments.len()); - let b = self.add_dec_after_lowlevel(arguments, ps, b, b_live_vars); - - let v = Expr::Call(crate::ir::Call { - call_type, - arguments, - }); - - &*self.arena.alloc(Stmt::Let(z, v, l, b)) - } - - ByName { - name, - ret_layout, - arg_layouts, - .. - } => { - let top_level = ProcLayout::new(self.arena, arg_layouts, **ret_layout); - - // get the borrow signature - let ps = self - .param_map - .get_symbol(*name, top_level) - .expect("function is defined"); - - let v = Expr::Call(crate::ir::Call { - call_type, - arguments, - }); - - let b = self.add_dec_after_application(arguments, ps, b, b_live_vars); - let b = self.arena.alloc(Stmt::Let(z, v, l, b)); - - self.add_inc_before(arguments, ps, b, b_live_vars) - } - } - } - - #[allow(clippy::too_many_arguments)] - fn visit_higher_order_lowlevel<'i>( - &self, - codegen: &mut CodegenTools<'i>, - z: Symbol, - lowlevel: &'a crate::ir::HigherOrderLowLevel, - arguments: &'a [Symbol], - l: Layout<'a>, - b: &'a Stmt<'a>, - b_live_vars: &LiveVarSet, - ) -> &'a Stmt<'a> { - use crate::low_level::HigherOrder::*; - use DataFunction::*; - - let HigherOrderLowLevel { - op, - passed_function, - .. - } = lowlevel; - - macro_rules! create_call { - ($borrows:expr) => { - create_holl_call(self.arena, lowlevel, $borrows, arguments) - }; - } - - let function_layout = ProcLayout { - arguments: passed_function.argument_layouts, - result: passed_function.return_layout, - }; - - let function_ps = match self - .param_map - .get_symbol(passed_function.name, function_layout) - { - Some(function_ps) => function_ps, - None => unreachable!(), - }; - - macro_rules! handle_ownerships_post { - ($stmt:expr, $args:expr) => {{ - let mut stmt = $stmt; - - for (argument, function_ps) in $args.iter().copied() { - let ownership = DataFunction::new(&self.vars, argument, function_ps); - - match ownership { - DataOwnedFunctionOwns | DataBorrowedFunctionOwns => { - // elements have been consumed, must still consume the list itself - let rest = self.arena.alloc($stmt); - let rc = Stmt::Refcounting(ModifyRc::DecRef(argument), rest); - - stmt = self.arena.alloc(rc); - } - DataOwnedFunctionBorrows => { - // must consume list and elements - let rest = self.arena.alloc($stmt); - let rc = Stmt::Refcounting(ModifyRc::Dec(argument), rest); - - stmt = self.arena.alloc(rc); - } - DataBorrowedFunctionBorrows => { - // list borrows, function borrows, so there is nothing to do - } - } - } - - stmt - }}; - } - - macro_rules! handle_ownerships_pre { - ($stmt:expr, $args:expr) => {{ - let mut stmt = self.arena.alloc($stmt); - - for (argument, function_ps) in $args.iter().copied() { - let ownership = DataFunction::new(&self.vars, argument, function_ps); - - match ownership { - DataBorrowedFunctionOwns => { - // the data is borrowed; - // increment it to own the values so the function can use them - let rc = Stmt::Refcounting(ModifyRc::Inc(argument, 1), stmt); - - stmt = self.arena.alloc(rc); - } - DataOwnedFunctionOwns | DataOwnedFunctionBorrows => { - // we actually own the data; nothing to do - } - DataBorrowedFunctionBorrows => { - // list borrows, function borrows, so there is nothing to do - } - } - } - - stmt - }}; - } - - // incrementing/consuming the closure (if needed) is done by the zig implementation. - // We don't want to touch the RC on the roc side, so treat these as borrowed. - const FUNCTION: bool = BORROWED; - const CLOSURE_DATA: bool = BORROWED; - - let borrows = [FUNCTION, CLOSURE_DATA]; - let after_arguments = &arguments[op.function_index()..]; - - match *op { - ListMap { xs } - | ListKeepIf { xs } - | ListKeepOks { xs } - | ListKeepErrs { xs } - | ListAny { xs } - | ListAll { xs } - | ListFindUnsafe { xs } => { - let ownerships = [(xs, function_ps[0])]; - - let b = self.add_dec_after_lowlevel(after_arguments, &borrows, b, b_live_vars); - - let b = handle_ownerships_post!(b, ownerships); - - let v = create_call!(function_ps.get(1)); - - handle_ownerships_pre!(Stmt::Let(z, v, l, b), ownerships) - } - ListMap2 { xs, ys } => { - let ownerships = [(xs, function_ps[0]), (ys, function_ps[1])]; - - let b = self.add_dec_after_lowlevel(after_arguments, &borrows, b, b_live_vars); - - let b = handle_ownerships_post!(b, ownerships); - - let v = create_call!(function_ps.get(2)); - - handle_ownerships_pre!(Stmt::Let(z, v, l, b), ownerships) - } - ListMap3 { xs, ys, zs } => { - let ownerships = [ - (xs, function_ps[0]), - (ys, function_ps[1]), - (zs, function_ps[2]), - ]; - - let b = self.add_dec_after_lowlevel(after_arguments, &borrows, b, b_live_vars); - - let b = handle_ownerships_post!(b, ownerships); - - let v = create_call!(function_ps.get(3)); - - handle_ownerships_pre!(Stmt::Let(z, v, l, b), ownerships) - } - ListMap4 { xs, ys, zs, ws } => { - let ownerships = [ - (xs, function_ps[0]), - (ys, function_ps[1]), - (zs, function_ps[2]), - (ws, function_ps[3]), - ]; - - let b = self.add_dec_after_lowlevel(after_arguments, &borrows, b, b_live_vars); - - let b = handle_ownerships_post!(b, ownerships); - - let v = create_call!(function_ps.get(3)); - - handle_ownerships_pre!(Stmt::Let(z, v, l, b), ownerships) - } - ListMapWithIndex { xs } => { - let ownerships = [(xs, function_ps[0])]; - - let b = self.add_dec_after_lowlevel(after_arguments, &borrows, b, b_live_vars); - - let b = handle_ownerships_post!(b, ownerships); - - let v = create_call!(function_ps.get(2)); - - handle_ownerships_pre!(Stmt::Let(z, v, l, b), ownerships) - } - ListSortWith { xs } => { - // NOTE: we may apply the function to the same argument multiple times. - // for that to be valid, the function must borrow its argument. This is not - // enforced at the moment - // - // we also don't check that both arguments have the same ownership characteristics - let ownerships = [(xs, function_ps[0])]; - - let b = self.add_dec_after_lowlevel(after_arguments, &borrows, b, b_live_vars); - - // list-sort will sort in-place; that really changes how RC should work - let b = { - let ownership = DataFunction::new(&self.vars, xs, function_ps[0]); - - match ownership { - DataOwnedFunctionOwns => { - // if non-unique, elements have been consumed, must still consume the list itself - let rc = Stmt::Refcounting(ModifyRc::DecRef(xs), b); - - let condition_stmt = branch_on_list_uniqueness( - self.arena, - codegen, - xs, - l, - b.clone(), - self.arena.alloc(rc), - ); - - &*self.arena.alloc(condition_stmt) - } - DataOwnedFunctionBorrows => { - // must consume list and elements - let rc = Stmt::Refcounting(ModifyRc::Dec(xs), b); - - let condition_stmt = branch_on_list_uniqueness( - self.arena, - codegen, - xs, - l, - b.clone(), - self.arena.alloc(rc), - ); - - &*self.arena.alloc(condition_stmt) - } - DataBorrowedFunctionOwns => { - // elements have been consumed, must still consume the list itself - let rest = self.arena.alloc(b); - let rc = Stmt::Refcounting(ModifyRc::DecRef(xs), rest); - - &*self.arena.alloc(rc) - } - DataBorrowedFunctionBorrows => { - // list borrows, function borrows, so there is nothing to do - b - } - } - }; - - let v = create_call!(function_ps.get(2)); - - handle_ownerships_pre!(Stmt::Let(z, v, l, b), ownerships) - } - ListWalk { xs, state: _ } - | ListWalkUntil { xs, state: _ } - | ListWalkBackwards { xs, state: _ } - | DictWalk { xs, state: _ } => { - let ownerships = [ - // borrow data structure based on second argument of the folded function - (xs, function_ps[1]), - ]; - // borrow the default based on first argument of the folded function - // (state, function_ps[0]) - - let b = self.add_dec_after_lowlevel(after_arguments, &borrows, b, b_live_vars); - - let b = handle_ownerships_post!(b, ownerships); - - let v = create_call!(function_ps.get(2)); - - handle_ownerships_pre!(Stmt::Let(z, v, l, b), ownerships) - } - } - } - - #[allow(clippy::many_single_char_names)] - fn visit_variable_declaration<'i>( - &self, - codegen: &mut CodegenTools<'i>, - z: Symbol, - v: Expr<'a>, - l: Layout<'a>, - b: &'a Stmt<'a>, - b_live_vars: &LiveVarSet, - ) -> (&'a Stmt<'a>, LiveVarSet) { - use Expr::*; - - let mut live_vars = update_live_vars(&v, b_live_vars); - live_vars.remove(&z); - - let new_b = match v { - Reuse { arguments: ys, .. } | Tag { arguments: ys, .. } | Struct(ys) => self - .add_inc_before_consume_all( - ys, - self.arena.alloc(Stmt::Let(z, v, l, b)), - b_live_vars, - ), - - Array { elems, .. } => { - let ys = Vec::from_iter_in(elems.iter().filter_map(|e| e.to_symbol()), self.arena); - self.add_inc_before_consume_all( - &ys, - self.arena.alloc(Stmt::Let(z, v, l, b)), - b_live_vars, - ) - } - - Call(crate::ir::Call { - call_type, - arguments, - }) => self.visit_call(codegen, z, call_type, arguments, l, b, b_live_vars), - - StructAtIndex { structure: x, .. } => { - let b = self.add_dec_if_needed(x, b, b_live_vars); - let info_x = self.get_var_info(x); - let b = if info_x.consume { - self.add_inc(z, 1, b) - } else { - b - }; - - self.arena.alloc(Stmt::Let(z, v, l, b)) - } - - GetTagId { structure: x, .. } => { - let b = self.add_dec_if_needed(x, b, b_live_vars); - let info_x = self.get_var_info(x); - let b = if info_x.consume { - self.add_inc(z, 1, b) - } else { - b - }; - - self.arena.alloc(Stmt::Let(z, v, l, b)) - } - - UnionAtIndex { structure: x, .. } => { - let b = self.add_dec_if_needed(x, b, b_live_vars); - let info_x = self.get_var_info(x); - let b = if info_x.consume { - self.add_inc(z, 1, b) - } else { - b - }; - - self.arena.alloc(Stmt::Let(z, v, l, b)) - } - - ExprBox { symbol: x } => { - // mimics Tag - self.add_inc_before_consume_all( - &[x], - self.arena.alloc(Stmt::Let(z, v, l, b)), - b_live_vars, - ) - } - - ExprUnbox { symbol: x } => { - // mimics UnionAtIndex - let b = self.add_dec_if_needed(x, b, b_live_vars); - let info_x = self.get_var_info(x); - let b = if info_x.consume { - self.add_inc(z, 1, b) - } else { - b - }; - - self.arena.alloc(Stmt::Let(z, v, l, b)) - } - - EmptyArray | Literal(_) | Reset { .. } | RuntimeErrorFunction(_) => { - // EmptyArray is always stack-allocated - // function pointers are persistent - self.arena.alloc(Stmt::Let(z, v, l, b)) - } - }; - - (new_b, live_vars) - } - - fn update_var_info(&self, symbol: Symbol, layout: &Layout<'a>, expr: &Expr<'a>) -> Self { - // is this value a constant? - // TODO do function pointers also fall into this category? - let persistent = false; - - // must this value be consumed? - let consume = consume_expr(&self.vars, expr); - - let reset = matches!(expr, Expr::Reset { .. }); - - self.update_var_info_help(symbol, layout, persistent, consume, reset) - } - - fn update_var_info_help( - &self, - symbol: Symbol, - layout: &Layout<'a>, - persistent: bool, - consume: bool, - reset: bool, - ) -> Self { - // should we perform incs and decs on this value? - let reference = layout.contains_refcounted(); - - let info = VarInfo { - reference, - persistent, - consume, - reset, - }; - - let mut ctx = self.clone(); - - ctx.vars.insert(symbol, info); - - ctx - } - - fn update_var_info_with_params(&self, ps: &[Param]) -> Self { - let mut ctx = self.clone(); - - for p in ps.iter() { - let info = VarInfo { - reference: p.layout.contains_refcounted(), - consume: !p.borrow, - persistent: false, - reset: false, - }; - ctx.vars.insert(p.symbol, info); - } - - ctx - } - - // Add `dec` instructions for parameters that are - // - // - references - // - not alive in `b` - // - not borrow. - // - // That is, we must make sure these parameters are consumed. - fn add_dec_for_dead_params( - &self, - ps: &[Param<'a>], - mut b: &'a Stmt<'a>, - b_live_vars: &LiveVarSet, - ) -> &'a Stmt<'a> { - for p in ps.iter() { - if !p.borrow && p.layout.contains_refcounted() && !b_live_vars.contains(&p.symbol) { - b = self.add_dec(p.symbol, b) - } - } - - b - } - - fn add_dec_for_alt( - &self, - case_live_vars: &LiveVarSet, - alt_live_vars: &LiveVarSet, - mut b: &'a Stmt<'a>, - ) -> &'a Stmt<'a> { - for x in case_live_vars.iter() { - if !alt_live_vars.contains(x) && self.must_consume(*x) { - b = self.add_dec(*x, b); - } - } - - b - } - - fn visit_stmt<'i>( - &self, - codegen: &mut CodegenTools<'i>, - stmt: &'a Stmt<'a>, - ) -> (&'a Stmt<'a>, LiveVarSet) { - use Stmt::*; - - // let-chains can be very long, especially for large (list) literals - // in (rust) debug mode, this function can overflow the stack for such values - // so we have to write an explicit loop. - { - let mut cont = stmt; - let mut triples = Vec::new_in(self.arena); - while let Stmt::Let(symbol, expr, layout, new_cont) = cont { - triples.push((symbol, expr, layout)); - cont = new_cont; - } - - if !triples.is_empty() { - let mut ctx = self.clone(); - for (symbol, expr, layout) in triples.iter() { - ctx = ctx.update_var_info(**symbol, layout, expr); - } - let (mut b, mut b_live_vars) = ctx.visit_stmt(codegen, cont); - for (symbol, expr, layout) in triples.into_iter().rev() { - let pair = ctx.visit_variable_declaration( - codegen, - *symbol, - (*expr).clone(), - *layout, - b, - &b_live_vars, - ); - - b = pair.0; - b_live_vars = pair.1; - } - - return (b, b_live_vars); - } - } - - match stmt { - Let(symbol, expr, layout, cont) => { - let ctx = self.update_var_info(*symbol, layout, expr); - let (b, b_live_vars) = ctx.visit_stmt(codegen, cont); - ctx.visit_variable_declaration( - codegen, - *symbol, - expr.clone(), - *layout, - b, - &b_live_vars, - ) - } - - Join { - id: j, - parameters: _, - remainder: b, - body: v, - } => { - // get the parameters with borrow signature - let xs = self.param_map.get_join_point(*j); - - let (v, v_live_vars) = { - let ctx = self.update_var_info_with_params(xs); - ctx.visit_stmt(codegen, v) - }; - - let mut ctx = self.clone(); - let v = ctx.add_dec_for_dead_params(xs, v, &v_live_vars); - - update_jp_live_vars(*j, xs, v, &mut ctx.jp_live_vars); - - let (b, b_live_vars) = ctx.visit_stmt(codegen, b); - - ( - ctx.arena.alloc(Join { - id: *j, - parameters: xs, - remainder: b, - body: v, - }), - b_live_vars, - ) - } - - Ret(x) => { - let info = self.get_var_info(*x); - - let mut live_vars = MutSet::default(); - live_vars.insert(*x); - - if info.reference && !info.consume { - (self.add_inc(*x, 1, stmt), live_vars) - } else { - (stmt, live_vars) - } - } - - Jump(j, xs) => { - let empty = MutSet::default(); - let j_live_vars = match self.jp_live_vars.get(j) { - Some(vars) => vars, - None => &empty, - }; - // TODO use borrow signature here? - let ps = self.param_map.get_join_point(*j); - - let b = self.add_inc_before(xs, ps, stmt, j_live_vars); - - let b_live_vars = collect_stmt(b, &self.jp_live_vars, MutSet::default()); - - (b, b_live_vars) - } - - Switch { - cond_symbol, - cond_layout, - branches, - default_branch, - ret_layout, - } => { - let case_live_vars = collect_stmt(stmt, &self.jp_live_vars, MutSet::default()); - - let branches = Vec::from_iter_in( - branches.iter().map(|(label, info, branch)| { - // TODO should we use ctor info like Lean? - let ctx = self.clone(); - let (b, alt_live_vars) = ctx.visit_stmt(codegen, branch); - let b = ctx.add_dec_for_alt(&case_live_vars, &alt_live_vars, b); - - (*label, info.clone(), b.clone()) - }), - self.arena, - ) - .into_bump_slice(); - - let default_branch = { - // TODO should we use ctor info like Lean? - let ctx = self.clone(); - let (b, alt_live_vars) = ctx.visit_stmt(codegen, default_branch.1); - - ( - default_branch.0.clone(), - ctx.add_dec_for_alt(&case_live_vars, &alt_live_vars, b), - ) - }; - - let switch = self.arena.alloc(Switch { - cond_symbol: *cond_symbol, - branches, - default_branch, - cond_layout: *cond_layout, - ret_layout: *ret_layout, - }); - - (switch, case_live_vars) - } - - Expect { remainder, .. } => self.visit_stmt(codegen, remainder), - - RuntimeError(_) | Refcounting(_, _) => (stmt, MutSet::default()), - } - } -} - -fn branch_on_list_uniqueness<'a, 'i>( - arena: &'a Bump, - codegen: &mut CodegenTools<'i>, - list_symbol: Symbol, - return_layout: Layout<'a>, - then_branch_stmt: Stmt<'a>, - else_branch_stmt: &'a Stmt<'a>, -) -> Stmt<'a> { - let condition_symbol = Symbol::new(codegen.home, codegen.ident_ids.add_str("listIsUnique")); - - let when_stmt = Stmt::if_then_else( - arena, - condition_symbol, - return_layout, - then_branch_stmt, - else_branch_stmt, - ); - - let stmt = arena.alloc(when_stmt); - - // define the condition - - let condition_call_type = CallType::LowLevel { - op: roc_module::low_level::LowLevel::ListIsUnique, - update_mode: codegen.update_mode_ids.next_id(), - }; - - let condition_call = crate::ir::Call { - call_type: condition_call_type, - arguments: arena.alloc([list_symbol]), - }; - - Stmt::Let( - condition_symbol, - Expr::Call(condition_call), - Layout::bool(), - stmt, - ) -} - -fn create_holl_call<'a>( - arena: &'a Bump, - holl: &'a crate::ir::HigherOrderLowLevel, - param: Option<&Param>, - arguments: &'a [Symbol], -) -> Expr<'a> { - let call = crate::ir::Call { - call_type: if let Some(OWNED) = param.map(|p| p.borrow) { - let mut passed_function = holl.passed_function; - passed_function.owns_captured_environment = true; - - let higher_order = HigherOrderLowLevel { - op: holl.op, - closure_env_layout: holl.closure_env_layout, - update_mode: holl.update_mode, - passed_function, - }; - - CallType::HigherOrder(arena.alloc(higher_order)) - } else { - debug_assert!(!holl.passed_function.owns_captured_environment); - CallType::HigherOrder(holl) - }, - arguments, - }; - - Expr::Call(call) -} - -pub fn collect_stmt( - stmt: &Stmt<'_>, - jp_live_vars: &JPLiveVarMap, - mut vars: LiveVarSet, -) -> LiveVarSet { - use Stmt::*; - - match stmt { - Let(symbol, expr, _, cont) => { - vars = collect_stmt(cont, jp_live_vars, vars); - vars.remove(symbol); - let mut result = MutSet::default(); - occurring_variables_expr(expr, &mut result); - vars.extend(result); - - vars - } - - Ret(symbol) => { - vars.insert(*symbol); - vars - } - - Refcounting(modify, cont) => { - let symbol = modify.get_symbol(); - vars.insert(symbol); - collect_stmt(cont, jp_live_vars, vars) - } - - Expect { - condition, - remainder, - .. - } => { - vars.insert(*condition); - collect_stmt(remainder, jp_live_vars, vars) - } - - Join { - id: j, - parameters, - remainder: b, - body: v, - } => { - let mut j_live_vars = collect_stmt(v, jp_live_vars, MutSet::default()); - for param in parameters.iter() { - j_live_vars.remove(¶m.symbol); - } - - let mut jp_live_vars = jp_live_vars.clone(); - jp_live_vars.insert(*j, j_live_vars); - - collect_stmt(b, &jp_live_vars, vars) - } - - Jump(id, arguments) => { - vars.extend(arguments.iter().copied()); - - // NOTE deviation from Lean - // we fall through when no join point is available - if let Some(jvars) = jp_live_vars.get(id) { - vars.extend(jvars); - } - - vars - } - - Switch { - cond_symbol, - branches, - default_branch, - .. - } => { - vars.insert(*cond_symbol); - - for (_, _info, branch) in branches.iter() { - vars.extend(collect_stmt(branch, jp_live_vars, vars.clone())); - } - - vars.extend(collect_stmt(default_branch.1, jp_live_vars, vars.clone())); - - vars - } - - RuntimeError(_) => vars, - } -} - -fn update_jp_live_vars(j: JoinPointId, ys: &[Param], v: &Stmt<'_>, m: &mut JPLiveVarMap) { - let j_live_vars = MutSet::default(); - let mut j_live_vars = collect_stmt(v, m, j_live_vars); - - for param in ys { - j_live_vars.remove(¶m.symbol); - } - - m.insert(j, j_live_vars); -} - -struct CodegenTools<'i> { - home: ModuleId, - ident_ids: &'i mut IdentIds, - update_mode_ids: &'i mut UpdateModeIds, -} - -pub fn visit_procs<'a, 'i>( - arena: &'a Bump, - home: ModuleId, - ident_ids: &'i mut IdentIds, - update_mode_ids: &'i mut UpdateModeIds, - param_map: &'a ParamMap<'a>, - procs: &mut MutMap<(Symbol, ProcLayout<'a>), Proc<'a>>, -) { - let ctx = Context::new(arena, param_map); - - let mut codegen = CodegenTools { - home, - ident_ids, - update_mode_ids, - }; - - for (key, proc) in procs.iter_mut() { - visit_proc(arena, &mut codegen, param_map, &ctx, proc, key.1); - } -} - -fn visit_proc<'a, 'i>( - arena: &'a Bump, - codegen: &mut CodegenTools<'i>, - param_map: &'a ParamMap<'a>, - ctx: &Context<'a>, - proc: &mut Proc<'a>, - layout: ProcLayout<'a>, -) { - let params = match param_map.get_symbol(proc.name, layout) { - Some(slice) => slice, - None => Vec::from_iter_in( - proc.args.iter().cloned().map(|(layout, symbol)| Param { - symbol, - borrow: false, - layout, - }), - arena, - ) - .into_bump_slice(), - }; - - let stmt = arena.alloc(proc.body.clone()); - let ctx = ctx.update_var_info_with_params(params); - let (b, b_live_vars) = ctx.visit_stmt(codegen, stmt); - let b = ctx.add_dec_for_dead_params(params, b, &b_live_vars); - - proc.body = b.clone(); -} diff --git a/compiler/mono/src/ir.rs b/compiler/mono/src/ir.rs deleted file mode 100644 index e3df06e39f..0000000000 --- a/compiler/mono/src/ir.rs +++ /dev/null @@ -1,9512 +0,0 @@ -#![allow(clippy::manual_map)] - -use crate::layout::{ - Builtin, ClosureRepresentation, LambdaSet, Layout, LayoutCache, LayoutProblem, - RawFunctionLayout, TagIdIntType, TagOrClosure, UnionLayout, WrappedVariant, -}; -use bumpalo::collections::{CollectIn, Vec}; -use bumpalo::Bump; -use roc_builtins::bitcode::{FloatWidth, IntWidth}; -use roc_can::abilities::{AbilitiesStore, SpecializationId}; -use roc_can::expr::{AnnotatedMark, ClosureData, IntValue}; -use roc_collections::all::{default_hasher, BumpMap, BumpMapDefault, MutMap}; -use roc_collections::VecMap; -use roc_debug_flags::dbg_do; -#[cfg(debug_assertions)] -use roc_debug_flags::{ - ROC_PRINT_IR_AFTER_REFCOUNT, ROC_PRINT_IR_AFTER_RESET_REUSE, ROC_PRINT_IR_AFTER_SPECIALIZATION, -}; -use roc_error_macros::todo_abilities; -use roc_exhaustive::{Ctor, CtorName, Guard, RenderAs, TagId}; -use roc_late_solve::{ - instantiate_rigids, resolve_ability_specialization, Resolved, UnificationFailed, -}; -use roc_module::ident::{ForeignSymbol, Lowercase, TagName}; -use roc_module::low_level::LowLevel; -use roc_module::symbol::{IdentIds, ModuleId, Symbol}; -use roc_problem::can::{RuntimeError, ShadowKind}; -use roc_region::all::{Loc, Region}; -use roc_std::RocDec; -use roc_target::TargetInfo; -use roc_types::subs::{ - Content, ExhaustiveMark, FlatType, RedundantMark, StorageSubs, Subs, Variable, - VariableSubsSlice, -}; -use std::collections::HashMap; -use ven_pretty::{BoxAllocator, DocAllocator, DocBuilder}; - -#[inline(always)] -pub fn pretty_print_ir_symbols() -> bool { - dbg_do!(ROC_PRINT_IR_AFTER_SPECIALIZATION, { - return true; - }); - dbg_do!(ROC_PRINT_IR_AFTER_RESET_REUSE, { - return true; - }); - dbg_do!(ROC_PRINT_IR_AFTER_REFCOUNT, { - return true; - }); - false -} - -// if your changes cause this number to go down, great! -// please change it to the lower number. -// if it went up, maybe check that the change is really required - -roc_error_macros::assert_sizeof_wasm!(Literal, 24); -roc_error_macros::assert_sizeof_wasm!(Expr, 48); -roc_error_macros::assert_sizeof_wasm!(Stmt, 120); -roc_error_macros::assert_sizeof_wasm!(ProcLayout, 32); -roc_error_macros::assert_sizeof_wasm!(Call, 36); -roc_error_macros::assert_sizeof_wasm!(CallType, 28); - -roc_error_macros::assert_sizeof_non_wasm!(Literal, 3 * 8); -roc_error_macros::assert_sizeof_non_wasm!(Expr, 10 * 8); -roc_error_macros::assert_sizeof_non_wasm!(Stmt, 19 * 8); -roc_error_macros::assert_sizeof_non_wasm!(ProcLayout, 6 * 8); -roc_error_macros::assert_sizeof_non_wasm!(Call, 7 * 8); -roc_error_macros::assert_sizeof_non_wasm!(CallType, 5 * 8); - -macro_rules! return_on_layout_error { - ($env:expr, $layout_result:expr) => { - match $layout_result { - Ok(cached) => cached, - Err(error) => return_on_layout_error_help!($env, error), - } - }; -} - -macro_rules! return_on_layout_error_help { - ($env:expr, $error:expr) => {{ - match $error { - LayoutProblem::UnresolvedTypeVar(_) => { - return Stmt::RuntimeError($env.arena.alloc(format!( - "UnresolvedTypeVar {} line {}", - file!(), - line!() - ))); - } - LayoutProblem::Erroneous => { - return Stmt::RuntimeError($env.arena.alloc(format!( - "Erroneous {} line {}", - file!(), - line!() - ))); - } - } - }}; -} - -#[derive(Debug, Clone, Copy)] -pub enum OptLevel { - Development, - Normal, - Size, - Optimize, -} - -#[derive(Debug, Clone, Copy)] -pub struct EntryPoint<'a> { - pub symbol: Symbol, - pub layout: ProcLayout<'a>, -} - -#[derive(Clone, Copy, Debug)] -pub struct PartialProcId(usize); - -#[derive(Clone, Debug)] -pub struct PartialProcs<'a> { - /// maps a function name (symbol) to an index - symbols: Vec<'a, Symbol>, - - /// An entry (a, b) means `a` directly references the lambda value of `b`, - /// i.e. this came from a `let a = b in ...` where `b` was defined as a - /// lambda earlier. - references: Vec<'a, (Symbol, Symbol)>, - - partial_procs: Vec<'a, PartialProc<'a>>, -} - -impl<'a> PartialProcs<'a> { - fn new_in(arena: &'a Bump) -> Self { - Self { - symbols: Vec::new_in(arena), - references: Vec::new_in(arena), - partial_procs: Vec::new_in(arena), - } - } - fn contains_key(&self, symbol: Symbol) -> bool { - self.symbol_to_id(symbol).is_some() - } - - fn symbol_to_id(&self, mut symbol: Symbol) -> Option { - while let Some(real_symbol) = self - .references - .iter() - .find(|(alias, _)| *alias == symbol) - .map(|(_, real)| real) - { - symbol = *real_symbol; - } - - self.symbols - .iter() - .position(|s| *s == symbol) - .map(PartialProcId) - } - - fn get_symbol(&self, symbol: Symbol) -> Option<&PartialProc<'a>> { - let id = self.symbol_to_id(symbol)?; - - Some(self.get_id(id)) - } - - fn get_id(&self, id: PartialProcId) -> &PartialProc<'a> { - &self.partial_procs[id.0] - } - - pub fn insert(&mut self, symbol: Symbol, partial_proc: PartialProc<'a>) -> PartialProcId { - debug_assert!( - !self.contains_key(symbol), - "The {:?} is inserted as a partial proc twice: that's a bug!", - symbol, - ); - - let id = PartialProcId(self.symbols.len()); - - self.symbols.push(symbol); - self.partial_procs.push(partial_proc); - - id - } - - pub fn insert_alias(&mut self, alias: Symbol, real_symbol: Symbol) { - debug_assert!( - !self.contains_key(alias), - "{:?} is inserted as a partial proc twice: that's a bug!", - alias, - ); - debug_assert!( - self.contains_key(real_symbol), - "{:?} is not a partial proc or another alias: that's a bug!", - real_symbol, - ); - - self.references.push((alias, real_symbol)); - } -} - -#[derive(Clone, Debug)] -pub struct PartialProc<'a> { - pub annotation: Variable, - pub pattern_symbols: &'a [Symbol], - pub captured_symbols: CapturedSymbols<'a>, - pub body: roc_can::expr::Expr, - pub body_var: Variable, - pub is_self_recursive: bool, -} - -impl<'a> PartialProc<'a> { - #[allow(clippy::too_many_arguments)] - pub fn from_named_function( - env: &mut Env<'a, '_>, - annotation: Variable, - loc_args: std::vec::Vec<(Variable, AnnotatedMark, Loc)>, - loc_body: Loc, - captured_symbols: CapturedSymbols<'a>, - is_self_recursive: bool, - ret_var: Variable, - ) -> PartialProc<'a> { - let number_of_arguments = loc_args.len(); - - match patterns_to_when(env, loc_args, ret_var, loc_body) { - Ok((_, pattern_symbols, body)) => { - // a named closure. Since these aren't specialized by the surrounding - // context, we can't add pending specializations for them yet. - // (If we did, all named polymorphic functions would immediately error - // on trying to convert a flex var to a Layout.) - let pattern_symbols = pattern_symbols.into_bump_slice(); - PartialProc { - annotation, - pattern_symbols, - captured_symbols, - body: body.value, - body_var: ret_var, - is_self_recursive, - } - } - - Err(error) => { - let mut pattern_symbols = Vec::with_capacity_in(number_of_arguments, env.arena); - - for _ in 0..number_of_arguments { - pattern_symbols.push(env.unique_symbol()); - } - - PartialProc { - annotation, - pattern_symbols: pattern_symbols.into_bump_slice(), - captured_symbols: CapturedSymbols::None, - body: roc_can::expr::Expr::RuntimeError(error.value), - body_var: ret_var, - is_self_recursive: false, - } - } - } - } -} - -#[derive(Clone, Copy, Debug)] -struct AbilityMember(Symbol); - -/// A table of aliases of ability member symbols. -#[derive(Clone, Debug)] -struct AbilityAliases(BumpMap); - -impl AbilityAliases { - fn new_in(arena: &Bump) -> Self { - Self(BumpMap::new_in(arena)) - } - - fn insert(&mut self, symbol: Symbol, member: AbilityMember) { - self.0.insert(symbol, member); - } - - fn get(&self, symbol: Symbol) -> Option<&AbilityMember> { - self.0.get(&symbol) - } -} - -#[derive(Clone, Copy, Debug, PartialEq)] -pub enum CapturedSymbols<'a> { - None, - Captured(&'a [(Symbol, Variable)]), -} - -impl<'a> CapturedSymbols<'a> { - fn captures(&self) -> bool { - match self { - CapturedSymbols::None => false, - CapturedSymbols::Captured(_) => true, - } - } -} - -impl<'a> Default for CapturedSymbols<'a> { - fn default() -> Self { - CapturedSymbols::None - } -} - -#[derive(Clone, Debug, PartialEq)] -pub struct Proc<'a> { - pub name: Symbol, - pub args: &'a [(Layout<'a>, Symbol)], - pub body: Stmt<'a>, - pub closure_data_layout: Option>, - pub ret_layout: Layout<'a>, - pub is_self_recursive: SelfRecursive, - pub must_own_arguments: bool, - pub host_exposed_layouts: HostExposedLayouts<'a>, -} - -#[derive(Clone, Debug, PartialEq)] -pub enum HostExposedLayouts<'a> { - NotHostExposed, - HostExposed { - rigids: BumpMap>, - aliases: BumpMap, RawFunctionLayout<'a>)>, - }, -} - -#[derive(Clone, Debug, PartialEq)] -pub enum SelfRecursive { - NotSelfRecursive, - SelfRecursive(JoinPointId), -} - -#[derive(Clone, Copy, Debug, PartialEq)] -pub enum Parens { - NotNeeded, - InTypeParam, - InFunction, -} - -impl<'a> Proc<'a> { - pub fn to_doc<'b, D, A>(&'b self, alloc: &'b D, _parens: Parens) -> DocBuilder<'b, D, A> - where - D: DocAllocator<'b, A>, - D::Doc: Clone, - A: Clone, - { - let args_doc = self - .args - .iter() - .map(|(_, symbol)| symbol_to_doc(alloc, *symbol)); - - if pretty_print_ir_symbols() { - alloc - .text("procedure : ") - .append(symbol_to_doc(alloc, self.name)) - .append(" ") - .append(self.ret_layout.to_doc(alloc, Parens::NotNeeded)) - .append(alloc.hardline()) - .append(alloc.text("procedure = ")) - .append(symbol_to_doc(alloc, self.name)) - .append(" (") - .append(alloc.intersperse(args_doc, ", ")) - .append("):") - .append(alloc.hardline()) - .append(self.body.to_doc(alloc).indent(4)) - } else { - alloc - .text("procedure ") - .append(symbol_to_doc(alloc, self.name)) - .append(" (") - .append(alloc.intersperse(args_doc, ", ")) - .append("):") - .append(alloc.hardline()) - .append(self.body.to_doc(alloc).indent(4)) - } - } - - pub fn to_pretty(&self, width: usize) -> String { - let allocator = BoxAllocator; - let mut w = std::vec::Vec::new(); - self.to_doc::<_, ()>(&allocator, Parens::NotNeeded) - .1 - .render(width, &mut w) - .unwrap(); - w.push(b'\n'); - String::from_utf8(w).unwrap() - } - - pub fn insert_refcount_operations<'i>( - arena: &'a Bump, - home: ModuleId, - ident_ids: &'i mut IdentIds, - update_mode_ids: &'i mut UpdateModeIds, - procs: &mut MutMap<(Symbol, ProcLayout<'a>), Proc<'a>>, - ) { - let borrow_params = arena.alloc(crate::borrow::infer_borrow(arena, procs)); - - crate::inc_dec::visit_procs( - arena, - home, - ident_ids, - update_mode_ids, - borrow_params, - procs, - ); - } - - pub fn insert_reset_reuse_operations<'i>( - arena: &'a Bump, - home: ModuleId, - ident_ids: &'i mut IdentIds, - update_mode_ids: &'i mut UpdateModeIds, - procs: &mut MutMap<(Symbol, ProcLayout<'a>), Proc<'a>>, - ) { - for (_, proc) in procs.iter_mut() { - let new_proc = crate::reset_reuse::insert_reset_reuse( - arena, - home, - ident_ids, - update_mode_ids, - proc.clone(), - ); - *proc = new_proc; - } - } - - fn make_tail_recursive(&mut self, env: &mut Env<'a, '_>) { - let mut args = Vec::with_capacity_in(self.args.len(), env.arena); - let mut proc_args = Vec::with_capacity_in(self.args.len(), env.arena); - - for (layout, symbol) in self.args { - let new = env.unique_symbol(); - args.push((*layout, *symbol, new)); - proc_args.push((*layout, new)); - } - - use self::SelfRecursive::*; - if let SelfRecursive(id) = self.is_self_recursive { - let transformed = crate::tail_recursion::make_tail_recursive( - env.arena, - id, - self.name, - self.body.clone(), - args.into_bump_slice(), - ); - - if let Some(with_tco) = transformed { - self.body = with_tco; - self.args = proc_args.into_bump_slice(); - } - } - } -} - -/// A host-exposed function must be specialized; it's a seed for subsequent specializations -#[derive(Clone, Debug)] -pub struct HostSpecializations { - /// Not a bumpalo vec because bumpalo is not thread safe - /// Separate array so we can search for membership quickly - symbols: std::vec::Vec, - storage_subs: StorageSubs, - /// For each symbol, what types to specialize it for, points into the storage_subs - types_to_specialize: std::vec::Vec, - /// Variables for an exposed alias - exposed_aliases: std::vec::Vec>, -} - -impl Default for HostSpecializations { - fn default() -> Self { - Self::new() - } -} - -impl HostSpecializations { - pub fn new() -> Self { - Self { - symbols: std::vec::Vec::new(), - storage_subs: StorageSubs::new(Subs::default()), - types_to_specialize: std::vec::Vec::new(), - exposed_aliases: std::vec::Vec::new(), - } - } - - pub fn insert_host_exposed( - &mut self, - env_subs: &mut Subs, - symbol: Symbol, - opt_annotation: Option, - variable: Variable, - ) { - let variable = self.storage_subs.extend_with_variable(env_subs, variable); - - let mut host_exposed_aliases = std::vec::Vec::new(); - - if let Some(annotation) = opt_annotation { - host_exposed_aliases.extend(annotation.introduced_variables.host_exposed_aliases); - } - - match self.symbols.iter().position(|s| *s == symbol) { - None => { - self.symbols.push(symbol); - self.types_to_specialize.push(variable); - self.exposed_aliases.push(host_exposed_aliases); - } - Some(_) => { - // we assume that only one specialization of a function is directly exposed to the - // host. Other host-exposed symbols may (transitively) specialize this symbol, - // but then the existing specialization mechanism will find those specializations - panic!("A host-exposed symbol can only be exposed once"); - } - } - - debug_assert_eq!(self.types_to_specialize.len(), self.exposed_aliases.len()); - } - - fn decompose( - self, - ) -> ( - StorageSubs, - impl Iterator)>, - ) { - let it1 = self.symbols.into_iter(); - - let it2 = self.types_to_specialize.into_iter(); - let it3 = self.exposed_aliases.into_iter(); - - ( - self.storage_subs, - it1.zip(it2).zip(it3).map(|((a, b), c)| (a, b, c)), - ) - } -} - -/// Specializations of this module's symbols that other modules need -#[derive(Clone, Debug)] -pub struct ExternalSpecializations { - /// Not a bumpalo vec because bumpalo is not thread safe - /// Separate array so we can search for membership quickly - symbols: std::vec::Vec, - storage_subs: StorageSubs, - /// For each symbol, what types to specialize it for, points into the storage_subs - types_to_specialize: std::vec::Vec>, -} - -impl Default for ExternalSpecializations { - fn default() -> Self { - Self::new() - } -} - -impl ExternalSpecializations { - pub fn new() -> Self { - Self { - symbols: std::vec::Vec::new(), - storage_subs: StorageSubs::new(Subs::default()), - types_to_specialize: std::vec::Vec::new(), - } - } - - fn insert_external(&mut self, symbol: Symbol, env_subs: &mut Subs, variable: Variable) { - let variable = self.storage_subs.extend_with_variable(env_subs, variable); - - match self.symbols.iter().position(|s| *s == symbol) { - None => { - self.symbols.push(symbol); - self.types_to_specialize.push(vec![variable]); - } - Some(index) => { - let types_to_specialize = &mut self.types_to_specialize[index]; - types_to_specialize.push(variable); - } - } - } - - fn decompose( - self, - ) -> ( - StorageSubs, - impl Iterator)>, - ) { - ( - self.storage_subs, - self.symbols - .into_iter() - .zip(self.types_to_specialize.into_iter()), - ) - } -} - -#[derive(Clone, Debug)] -pub struct Suspended<'a> { - pub store: StorageSubs, - pub symbols: Vec<'a, Symbol>, - pub layouts: Vec<'a, ProcLayout<'a>>, - pub variables: Vec<'a, Variable>, -} - -impl<'a> Suspended<'a> { - fn new_in(arena: &'a Bump) -> Self { - Self { - store: StorageSubs::new(Subs::new_from_varstore(Default::default())), - symbols: Vec::new_in(arena), - layouts: Vec::new_in(arena), - variables: Vec::new_in(arena), - } - } - - fn specialization( - &mut self, - subs: &mut Subs, - symbol: Symbol, - proc_layout: ProcLayout<'a>, - variable: Variable, - ) { - // de-duplicate - for (i, s) in self.symbols.iter().enumerate() { - if *s == symbol { - let existing = &self.layouts[i]; - if &proc_layout == existing { - // symbol + layout combo exists - return; - } - } - } - - self.symbols.push(symbol); - self.layouts.push(proc_layout); - - let variable = self.store.extend_with_variable(subs, variable); - - self.variables.push(variable); - } -} - -#[derive(Clone, Debug)] -enum PendingSpecializations<'a> { - /// We are finding specializations we need. This is a separate step so - /// that we can give specializations we need to modules higher up in the dependency chain, so - /// that they can start making specializations too - Finding(Suspended<'a>), - /// We are making specializations. If any new one comes up, we can just make it immediately - Making, -} - -#[derive(Clone, Debug, Default)] -struct Specialized<'a> { - symbols: std::vec::Vec, - proc_layouts: std::vec::Vec>, - procedures: std::vec::Vec>, -} - -impl<'a> Specialized<'a> { - fn len(&self) -> usize { - self.symbols.len() - } - - #[allow(dead_code)] - fn is_empty(&self) -> bool { - self.symbols.is_empty() - } - - fn into_iter_assert_done(self) -> impl Iterator, Proc<'a>)> { - self.symbols - .into_iter() - .zip(self.proc_layouts.into_iter()) - .zip(self.procedures.into_iter()) - .filter_map(|((s, l), in_progress)| { - if let Symbol::REMOVED_SPECIALIZATION = s { - None - } else { - match in_progress { - InProgressProc::InProgress => panic!("Function is not done specializing"), - InProgressProc::Done(proc) => Some((s, l, proc)), - } - } - }) - } - - fn is_specialized(&self, symbol: Symbol, layout: &ProcLayout<'a>) -> bool { - for (i, s) in self.symbols.iter().enumerate() { - if *s == symbol && &self.proc_layouts[i] == layout { - return true; - } - } - - false - } - - fn mark_in_progress(&mut self, symbol: Symbol, layout: ProcLayout<'a>) { - for (i, s) in self.symbols.iter().enumerate() { - if *s == symbol && self.proc_layouts[i] == layout { - match &self.procedures[i] { - InProgressProc::InProgress => { - return; - } - InProgressProc::Done(_) => { - panic!("marking in progress, but this proc is already done!") - } - } - } - } - - // the key/layout combo was not found; insert it - self.symbols.push(symbol); - self.proc_layouts.push(layout); - self.procedures.push(InProgressProc::InProgress); - } - - fn remove_specialized(&mut self, symbol: Symbol, layout: &ProcLayout<'a>) -> bool { - let mut index = None; - - for (i, s) in self.symbols.iter().enumerate() { - if *s == symbol && &self.proc_layouts[i] == layout { - index = Some(i); - } - } - - if let Some(index) = index { - self.symbols[index] = Symbol::REMOVED_SPECIALIZATION; - - true - } else { - false - } - } - - fn insert_specialized(&mut self, symbol: Symbol, layout: ProcLayout<'a>, proc: Proc<'a>) { - for (i, s) in self.symbols.iter().enumerate() { - if *s == symbol && self.proc_layouts[i] == layout { - match &self.procedures[i] { - InProgressProc::InProgress => { - self.procedures[i] = InProgressProc::Done(proc); - return; - } - InProgressProc::Done(_) => { - // overwrite existing! this is important in practice - // TODO investigate why we generate the wrong proc in some cases and then - // correct later - self.procedures[i] = InProgressProc::Done(proc); - return; - } - } - } - } - - // the key/layout combo was not found; insert it - self.symbols.push(symbol); - self.proc_layouts.push(layout); - self.procedures.push(InProgressProc::Done(proc)); - } -} - -/// Uniquely determines the specialization of a polymorphic (non-proc) value symbol. -/// Two specializations are equivalent if their [`SpecializationMark`]s are equal. -#[derive(PartialEq, Eq, Debug, Clone, Copy)] -struct SpecializationMark<'a> { - /// The layout of the symbol itself. - layout: Layout<'a>, - - /// If this symbol is a closure def, we must also keep track of what function it specializes, - /// because the [`layout`] field will only keep track of its closure and lambda set - which can - /// be the same for two different function specializations. For example, - /// - /// id = if True then \x -> x else \y -> y - /// { a: id "", b: id 1u8 } - /// - /// The lambda set and captures of `id` is the same in both usages inside the record, but the - /// reified specializations of `\x -> x` and `\y -> y` must be for Str and U8. - /// - /// Note that this field is not relevant for anything that is not a function. - function_mark: Option>, -} - -/// When walking a function body, we may encounter specialized usages of polymorphic symbols. For -/// example -/// -/// myTag = A -/// use1 : [A, B] -/// use1 = myTag -/// use2 : [A, B, C] -/// use2 = myTag -/// -/// We keep track of the specializations of `myTag` and create fresh symbols when there is more -/// than one, so that a unique def can be created for each. -#[derive(Default, Debug, Clone)] -struct SymbolSpecializations<'a>( - // THEORY: - // 1. the number of symbols in a def is very small - // 2. the number of specializations of a symbol in a def is even smaller (almost always only one) - // So, a linear VecMap is preferrable. Use a two-layered one to make (1) extraction of defs easy - // and (2) reads of a certain symbol be determined by its first occurrence, not its last. - VecMap, (Variable, Symbol)>>, -); - -impl<'a> SymbolSpecializations<'a> { - /// Gets a specialization for a symbol, or creates a new one. - #[inline(always)] - fn get_or_insert( - &mut self, - env: &mut Env<'a, '_>, - layout_cache: &mut LayoutCache<'a>, - symbol: Symbol, - specialization_var: Variable, - ) -> Symbol { - let arena = env.arena; - let subs: &Subs = env.subs; - - let layout = match layout_cache.from_var(arena, specialization_var, subs) { - Ok(layout) => layout, - // This can happen when the def symbol has a type error. In such cases just use the - // def symbol, which is erroring. - Err(_) => return symbol, - }; - - let is_closure = matches!( - subs.get_content_without_compacting(specialization_var), - Content::Structure(FlatType::Func(..)) - ); - let function_mark = if is_closure { - let fn_layout = match layout_cache.raw_from_var(arena, specialization_var, subs) { - Ok(layout) => layout, - // This can happen when the def symbol has a type error. In such cases just use the - // def symbol, which is erroring. - Err(_) => return symbol, - }; - Some(fn_layout) - } else { - None - }; - - let specialization_mark = SpecializationMark { - layout, - function_mark, - }; - - let symbol_specializations = self.0.get_or_insert(symbol, Default::default); - - // For the first specialization, always reuse the current symbol. The vast majority of defs - // only have one instance type, so this preserves readability of the IR. - // TODO: turn me off and see what breaks. - let needs_fresh_symbol = !symbol_specializations.is_empty(); - - let mut make_specialized_symbol = || { - if needs_fresh_symbol { - env.unique_symbol() - } else { - symbol - } - }; - - let (_var, specialized_symbol) = symbol_specializations - .get_or_insert(specialization_mark, || { - (specialization_var, make_specialized_symbol()) - }); - - *specialized_symbol - } - - /// Inserts a known specialization for a symbol. Returns the overwritten specialization, if any. - pub fn get_or_insert_known( - &mut self, - symbol: Symbol, - mark: SpecializationMark<'a>, - specialization_var: Variable, - specialization_symbol: Symbol, - ) -> Option<(Variable, Symbol)> { - self.0 - .get_or_insert(symbol, Default::default) - .insert(mark, (specialization_var, specialization_symbol)) - } - - /// Removes all specializations for a symbol, returning the type and symbol of each specialization. - pub fn remove( - &mut self, - symbol: Symbol, - ) -> impl ExactSizeIterator, (Variable, Symbol))> { - self.0 - .remove(&symbol) - .map(|(_, specializations)| specializations) - .unwrap_or_default() - .into_iter() - } - - /// Expects and removes at most a single specialization symbol for the given requested symbol. - /// A symbol may have no specializations if it is never referenced in a body, so it is possible - /// for this to return None. - pub fn remove_single(&mut self, symbol: Symbol) -> Option { - let mut specializations = self.remove(symbol); - - debug_assert!( - specializations.len() < 2, - "Symbol {:?} has multiple specializations", - symbol - ); - - specializations.next().map(|(_, (_, symbol))| symbol) - } - - pub fn is_empty(&self) -> bool { - self.0.is_empty() - } -} - -#[derive(Clone, Debug)] -pub struct Procs<'a> { - pub partial_procs: PartialProcs<'a>, - ability_member_aliases: AbilityAliases, - pub imported_module_thunks: &'a [Symbol], - pub module_thunks: &'a [Symbol], - pending_specializations: PendingSpecializations<'a>, - specialized: Specialized<'a>, - pub runtime_errors: BumpMap, - pub externals_we_need: BumpMap, - symbol_specializations: SymbolSpecializations<'a>, -} - -impl<'a> Procs<'a> { - pub fn new_in(arena: &'a Bump) -> Self { - Self { - partial_procs: PartialProcs::new_in(arena), - ability_member_aliases: AbilityAliases::new_in(arena), - imported_module_thunks: &[], - module_thunks: &[], - pending_specializations: PendingSpecializations::Finding(Suspended::new_in(arena)), - specialized: Specialized::default(), - runtime_errors: BumpMap::new_in(arena), - externals_we_need: BumpMap::new_in(arena), - symbol_specializations: Default::default(), - } - } -} - -#[derive(Clone, Debug, PartialEq)] -pub enum InProgressProc<'a> { - InProgress, - Done(Proc<'a>), -} - -impl<'a> Procs<'a> { - fn is_imported_module_thunk(&self, symbol: Symbol) -> bool { - self.imported_module_thunks.iter().any(|x| *x == symbol) - } - - fn is_module_thunk(&self, symbol: Symbol) -> bool { - self.module_thunks.iter().any(|x| *x == symbol) - } - - fn get_partial_proc<'b>(&'b self, symbol: Symbol) -> Option<&'b PartialProc<'a>> { - self.partial_procs.get_symbol(symbol) - } - - pub fn get_specialized_procs_without_rc( - self, - env: &mut Env<'a, '_>, - ) -> MutMap<(Symbol, ProcLayout<'a>), Proc<'a>> { - let mut result = MutMap::with_capacity_and_hasher(self.specialized.len(), default_hasher()); - - for (symbol, layout, mut proc) in self.specialized.into_iter_assert_done() { - proc.make_tail_recursive(env); - - let key = (symbol, layout); - result.insert(key, proc); - } - - result - } - - // TODO trim these down - #[allow(clippy::too_many_arguments)] - fn insert_anonymous( - &mut self, - env: &mut Env<'a, '_>, - symbol: Symbol, - annotation: Variable, - loc_args: std::vec::Vec<(Variable, AnnotatedMark, Loc)>, - loc_body: Loc, - captured_symbols: CapturedSymbols<'a>, - ret_var: Variable, - layout_cache: &mut LayoutCache<'a>, - ) -> Result, RuntimeError> { - let raw_layout = layout_cache - .raw_from_var(env.arena, annotation, env.subs) - .unwrap_or_else(|err| panic!("TODO turn fn_var into a RuntimeError {:?}", err)); - - let top_level = ProcLayout::from_raw(env.arena, raw_layout); - - // anonymous functions cannot reference themselves, therefore cannot be tail-recursive - // EXCEPT when the closure conversion makes it tail-recursive. - let is_self_recursive = match top_level.arguments.last() { - Some(Layout::LambdaSet(lambda_set)) => lambda_set.contains(symbol), - _ => false, - }; - - match patterns_to_when(env, loc_args, ret_var, loc_body) { - Ok((_, pattern_symbols, body)) => { - // an anonymous closure. These will always be specialized already - // by the surrounding context, so we can add pending specializations - // for them immediately. - - let already_specialized = self.specialized.is_specialized(symbol, &top_level); - - let layout = top_level; - - // if we've already specialized this one, no further work is needed. - if !already_specialized { - if self.is_module_thunk(symbol) { - debug_assert!(layout.arguments.is_empty()); - } - - match &mut self.pending_specializations { - PendingSpecializations::Finding(suspended) => { - // register the pending specialization, so this gets code genned later - suspended.specialization(env.subs, symbol, layout, annotation); - - match self.partial_procs.symbol_to_id(symbol) { - Some(occupied) => { - let existing = self.partial_procs.get_id(occupied); - // if we're adding the same partial proc twice, they must be the actual same! - // - // NOTE we can't skip extra work! we still need to make the specialization for this - // invocation. The content of the `annotation` can be different, even if the variable - // number is the same - debug_assert_eq!(annotation, existing.annotation); - debug_assert_eq!(captured_symbols, existing.captured_symbols); - debug_assert_eq!(is_self_recursive, existing.is_self_recursive); - - // the partial proc is already in there, do nothing - } - None => { - let pattern_symbols = pattern_symbols.into_bump_slice(); - - let partial_proc = PartialProc { - annotation, - pattern_symbols, - captured_symbols, - body: body.value, - body_var: ret_var, - is_self_recursive, - }; - - self.partial_procs.insert(symbol, partial_proc); - } - } - } - PendingSpecializations::Making => { - // Mark this proc as in-progress, so if we're dealing with - // mutually recursive functions, we don't loop forever. - // (We had a bug around this before this system existed!) - self.specialized.mark_in_progress(symbol, layout); - - let outside_layout = layout; - - let partial_proc_id = if let Some(partial_proc_id) = - self.partial_procs.symbol_to_id(symbol) - { - let existing = self.partial_procs.get_id(partial_proc_id); - // if we're adding the same partial proc twice, they must be the actual same! - // - // NOTE we can't skip extra work! we still need to make the specialization for this - // invocation. The content of the `annotation` can be different, even if the variable - // number is the same - debug_assert_eq!(annotation, existing.annotation); - debug_assert_eq!(captured_symbols, existing.captured_symbols); - debug_assert_eq!(is_self_recursive, existing.is_self_recursive); - - partial_proc_id - } else { - let pattern_symbols = pattern_symbols.into_bump_slice(); - - let partial_proc = PartialProc { - annotation, - pattern_symbols, - captured_symbols, - body: body.value, - body_var: ret_var, - is_self_recursive, - }; - - self.partial_procs.insert(symbol, partial_proc) - }; - - match specialize_variable( - env, - self, - symbol, - layout_cache, - annotation, - &[], - partial_proc_id, - ) { - Ok((proc, layout)) => { - let top_level = ProcLayout::from_raw(env.arena, layout); - - debug_assert_eq!( - outside_layout, top_level, - "different raw layouts for {:?}", - proc.name - ); - - if self.is_module_thunk(proc.name) { - debug_assert!(top_level.arguments.is_empty()); - } - - self.specialized.insert_specialized(symbol, top_level, proc); - } - Err(error) => { - panic!("TODO generate a RuntimeError message for {:?}", error); - } - } - } - } - } - - Ok(layout) - } - Err(loc_error) => Err(loc_error.value), - } - } - - fn insert_passed_by_name( - &mut self, - env: &mut Env<'a, '_>, - fn_var: Variable, - name: Symbol, - layout: ProcLayout<'a>, - layout_cache: &mut LayoutCache<'a>, - ) { - // If we've already specialized this one, no further work is needed. - if self.specialized.is_specialized(name, &layout) { - return; - } - - // If this is an imported symbol, let its home module make this specialization - if env.is_imported_symbol(name) { - add_needed_external(self, env, fn_var, name); - return; - } - - // register the pending specialization, so this gets code genned later - if self.module_thunks.contains(&name) { - debug_assert!(layout.arguments.is_empty()); - } - - // This should only be called when pending_specializations is Some. - // Otherwise, it's being called in the wrong pass! - match &mut self.pending_specializations { - PendingSpecializations::Finding(suspended) => { - suspended.specialization(env.subs, name, layout, fn_var); - } - PendingSpecializations::Making => { - let symbol = name; - - let partial_proc_id = match self.partial_procs.symbol_to_id(symbol) { - Some(p) => p, - None => panic!("no partial_proc for {:?} in module {:?}", symbol, env.home), - }; - - // Mark this proc as in-progress, so if we're dealing with - // mutually recursive functions, we don't loop forever. - // (We had a bug around this before this system existed!) - self.specialized.mark_in_progress(symbol, layout); - - // See https://github.com/rtfeldman/roc/issues/1600 - // - // The annotation variable is the generic/lifted/top-level annotation. - // It is connected to the variables of the function's body - // - // fn_var is the variable representing the type that we actually need for the - // function right here. - // - // For some reason, it matters that we unify with the original variable. Extracting - // that variable into a SolvedType and then introducing it again severs some - // connection that turns out to be important - match specialize_variable( - env, - self, - symbol, - layout_cache, - fn_var, - Default::default(), - partial_proc_id, - ) { - Ok((proc, _ignore_layout)) => { - // the `layout` is a function pointer, while `_ignore_layout` can be a - // closure. We only specialize functions, storing this value with a closure - // layout will give trouble. - let arguments = - Vec::from_iter_in(proc.args.iter().map(|(l, _)| *l), env.arena) - .into_bump_slice(); - - let proper_layout = ProcLayout { - arguments, - result: proc.ret_layout, - }; - - // NOTE: some function are specialized to have a closure, but don't actually - // need any closure argument. Here is where we correct this sort of thing, - // by trusting the layout of the Proc, not of what we specialize for - self.specialized.remove_specialized(symbol, &layout); - self.specialized - .insert_specialized(symbol, proper_layout, proc); - } - Err(error) => { - panic!("TODO generate a RuntimeError message for {:?}", error); - } - } - } - } - } -} - -#[derive(Default)] -pub struct Specializations<'a> { - by_symbol: MutMap, Proc<'a>>>, -} - -impl<'a> Specializations<'a> { - pub fn insert(&mut self, symbol: Symbol, layout: Layout<'a>, proc: Proc<'a>) { - let procs_by_layout = self - .by_symbol - .entry(symbol) - .or_insert_with(|| HashMap::with_capacity_and_hasher(1, default_hasher())); - - // If we already have an entry for this, it should be no different - // from what we're about to insert. - debug_assert!( - !procs_by_layout.contains_key(&layout) || procs_by_layout.get(&layout) == Some(&proc) - ); - - procs_by_layout.insert(layout, proc); - } - - pub fn len(&self) -> usize { - self.by_symbol.len() - } - - pub fn is_empty(&self) -> bool { - self.by_symbol.is_empty() - } -} - -pub struct Env<'a, 'i> { - pub arena: &'a Bump, - pub subs: &'i mut Subs, - pub home: ModuleId, - pub ident_ids: &'i mut IdentIds, - pub target_info: TargetInfo, - pub update_mode_ids: &'i mut UpdateModeIds, - pub call_specialization_counter: u32, - pub abilities_store: &'i mut AbilitiesStore, -} - -impl<'a, 'i> Env<'a, 'i> { - pub fn unique_symbol(&mut self) -> Symbol { - let ident_id = self.ident_ids.gen_unique(); - - Symbol::new(self.home, ident_id) - } - - pub fn next_update_mode_id(&mut self) -> UpdateModeId { - self.update_mode_ids.next_id() - } - - pub fn next_call_specialization_id(&mut self) -> CallSpecId { - let id = CallSpecId { - id: self.call_specialization_counter, - }; - - self.call_specialization_counter += 1; - - id - } - - pub fn is_imported_symbol(&self, symbol: Symbol) -> bool { - symbol.module_id() != self.home - } - - /// Unifies two variables and performs lambda set compaction. - /// Use this rather than [roc_unify::unify] directly! - fn unify(&mut self, left: Variable, right: Variable) -> Result<(), UnificationFailed> { - roc_late_solve::unify(self.arena, self.subs, self.abilities_store, left, right) - } -} - -#[derive(Clone, Debug, PartialEq, Copy, Eq, Hash)] -pub struct JoinPointId(pub Symbol); - -#[derive(Clone, Copy, Debug, PartialEq)] -pub struct Param<'a> { - pub symbol: Symbol, - pub borrow: bool, - pub layout: Layout<'a>, -} - -impl<'a> Param<'a> { - pub const EMPTY: Self = Param { - symbol: Symbol::EMPTY_PARAM, - borrow: false, - layout: Layout::UNIT, - }; -} - -pub fn cond<'a>( - env: &mut Env<'a, '_>, - cond_symbol: Symbol, - cond_layout: Layout<'a>, - pass: Stmt<'a>, - fail: Stmt<'a>, - ret_layout: Layout<'a>, -) -> Stmt<'a> { - let branches = env.arena.alloc([(1u64, BranchInfo::None, pass)]); - let default_branch = (BranchInfo::None, &*env.arena.alloc(fail)); - - Stmt::Switch { - cond_symbol, - cond_layout, - ret_layout, - branches, - default_branch, - } -} - -pub type Stores<'a> = &'a [(Symbol, Layout<'a>, Expr<'a>)]; -#[derive(Clone, Debug, PartialEq)] -pub enum Stmt<'a> { - Let(Symbol, Expr<'a>, Layout<'a>, &'a Stmt<'a>), - Switch { - /// This *must* stand for an integer, because Switch potentially compiles to a jump table. - cond_symbol: Symbol, - cond_layout: Layout<'a>, - /// The u64 in the tuple will be compared directly to the condition Expr. - /// If they are equal, this branch will be taken. - branches: &'a [(u64, BranchInfo<'a>, Stmt<'a>)], - /// If no other branches pass, this default branch will be taken. - default_branch: (BranchInfo<'a>, &'a Stmt<'a>), - /// Each branch must return a value of this type. - ret_layout: Layout<'a>, - }, - Ret(Symbol), - Refcounting(ModifyRc, &'a Stmt<'a>), - Expect { - condition: Symbol, - region: Region, - lookups: &'a [Symbol], - layouts: &'a [Layout<'a>], - /// what happens after the expect - remainder: &'a Stmt<'a>, - }, - /// a join point `join f = in remainder` - Join { - id: JoinPointId, - parameters: &'a [Param<'a>], - /// body of the join point - /// what happens after _jumping to_ the join point - body: &'a Stmt<'a>, - /// what happens after _defining_ the join point - remainder: &'a Stmt<'a>, - }, - Jump(JoinPointId, &'a [Symbol]), - RuntimeError(&'a str), -} - -/// in the block below, symbol `scrutinee` is assumed be be of shape `tag_id` -#[derive(Clone, Debug, PartialEq)] -pub enum BranchInfo<'a> { - None, - Constructor { - scrutinee: Symbol, - layout: Layout<'a>, - tag_id: TagIdIntType, - }, -} - -impl<'a> BranchInfo<'a> { - pub fn to_doc<'b, D, A>(&'b self, alloc: &'b D) -> DocBuilder<'b, D, A> - where - D: DocAllocator<'b, A>, - D::Doc: Clone, - A: Clone, - { - use BranchInfo::*; - - match self { - Constructor { - tag_id, - scrutinee, - layout: _, - } if pretty_print_ir_symbols() => alloc - .hardline() - .append(" BranchInfo: { scrutinee: ") - .append(symbol_to_doc(alloc, *scrutinee)) - .append(", tag_id: ") - .append(format!("{}", tag_id)) - .append("} "), - - _ => { - if pretty_print_ir_symbols() { - alloc.text(" ") - } else { - alloc.text("") - } - } - } - } -} - -#[derive(Clone, Copy, Debug, PartialEq)] -pub enum ModifyRc { - /// Increment a reference count - Inc(Symbol, u64), - /// Decrement a reference count - Dec(Symbol), - /// A DecRef is a non-recursive reference count decrement - /// e.g. If we Dec a list of lists, then if the reference count of the outer list is one, - /// a Dec will recursively decrement all elements, then free the memory of the outer list. - /// A DecRef would just free the outer list. - /// That is dangerous because you may not free the elements, but in our Zig builtins, - /// sometimes we know we already dealt with the elements (e.g. by copying them all over - /// to a new list) and so we can just do a DecRef, which is much cheaper in such a case. - DecRef(Symbol), -} - -impl ModifyRc { - pub fn to_doc<'a, D, A>(self, alloc: &'a D) -> DocBuilder<'a, D, A> - where - D: DocAllocator<'a, A>, - D::Doc: Clone, - A: Clone, - { - use ModifyRc::*; - - match self { - Inc(symbol, 1) => alloc - .text("inc ") - .append(symbol_to_doc(alloc, symbol)) - .append(";"), - Inc(symbol, n) => alloc - .text("inc ") - .append(alloc.text(format!("{} ", n))) - .append(symbol_to_doc(alloc, symbol)) - .append(";"), - Dec(symbol) => alloc - .text("dec ") - .append(symbol_to_doc(alloc, symbol)) - .append(";"), - DecRef(symbol) => alloc - .text("decref ") - .append(symbol_to_doc(alloc, symbol)) - .append(";"), - } - } - - pub fn get_symbol(&self) -> Symbol { - use ModifyRc::*; - - match self { - Inc(symbol, _) => *symbol, - Dec(symbol) => *symbol, - DecRef(symbol) => *symbol, - } - } -} - -#[derive(Clone, Copy, Debug, PartialEq)] -pub enum Literal<'a> { - // Literals - /// stored as raw bytes rather than a number to avoid an alignment bump - Int([u8; 16]), - /// stored as raw bytes rather than a number to avoid an alignment bump - U128([u8; 16]), - Float(f64), - /// stored as raw bytes rather than a number to avoid an alignment bump - Decimal([u8; 16]), - Str(&'a str), - /// Closed tag unions containing exactly two (0-arity) tags compile to Expr::Bool, - /// so they can (at least potentially) be emitted as 1-bit machine bools. - /// - /// So [True, False] compiles to this, and so do [A, B] and [Foo, Bar]. - /// However, a union like [True, False, Other Int] would not. - Bool(bool), - /// Closed tag unions containing between 3 and 256 tags (all of 0 arity) - /// compile to bytes, e.g. [Blue, Black, Red, Green, White] - Byte(u8), -} - -#[derive(Clone, Copy, Debug, PartialEq)] -pub enum ListLiteralElement<'a> { - Literal(Literal<'a>), - Symbol(Symbol), -} - -impl<'a> ListLiteralElement<'a> { - pub fn to_symbol(&self) -> Option { - match self { - Self::Symbol(s) => Some(*s), - _ => None, - } - } -} - -#[derive(Clone, Debug, PartialEq)] -pub struct Call<'a> { - pub call_type: CallType<'a>, - pub arguments: &'a [Symbol], -} - -impl<'a> Call<'a> { - pub fn to_doc<'b, D, A>(&'b self, alloc: &'b D) -> DocBuilder<'b, D, A> - where - D: DocAllocator<'b, A>, - D::Doc: Clone, - A: Clone, - { - use CallType::*; - - let arguments = self.arguments; - - match self.call_type { - CallType::ByName { name, .. } => { - let it = std::iter::once(name) - .chain(arguments.iter().copied()) - .map(|s| symbol_to_doc(alloc, s)); - - alloc.text("CallByName ").append(alloc.intersperse(it, " ")) - } - LowLevel { op: lowlevel, .. } => { - let it = arguments.iter().map(|s| symbol_to_doc(alloc, *s)); - - alloc - .text(format!("lowlevel {:?} ", lowlevel)) - .append(alloc.intersperse(it, " ")) - } - HigherOrder(higher_order) => { - let it = arguments.iter().map(|s| symbol_to_doc(alloc, *s)); - - alloc - .text(format!("lowlevel {:?} ", higher_order.op)) - .append(alloc.intersperse(it, " ")) - } - Foreign { - ref foreign_symbol, .. - } => { - let it = arguments.iter().map(|s| symbol_to_doc(alloc, *s)); - - alloc - .text(format!("foreign {:?} ", foreign_symbol.as_str())) - .append(alloc.intersperse(it, " ")) - } - } - } -} - -#[derive(Clone, Copy, Debug, PartialEq)] -pub struct CallSpecId { - id: u32, -} - -impl CallSpecId { - pub fn to_bytes(self) -> [u8; 4] { - self.id.to_ne_bytes() - } - - /// Dummy value for generating refcount helper procs in the backends - /// This happens *after* specialization so it's safe - pub const BACKEND_DUMMY: Self = Self { id: 0 }; -} - -#[derive(Clone, Copy, Debug, PartialEq)] -pub struct UpdateModeId { - id: u32, -} - -impl UpdateModeId { - pub fn to_bytes(self) -> [u8; 4] { - self.id.to_ne_bytes() - } - - /// Dummy value for generating refcount helper procs in the backends - /// This happens *after* alias analysis so it's safe - pub const BACKEND_DUMMY: Self = Self { id: 0 }; -} - -#[derive(Clone, Copy, Debug, PartialEq)] -pub struct UpdateModeIds { - next: u32, -} - -impl UpdateModeIds { - pub const fn new() -> Self { - Self { next: 0 } - } - - pub fn next_id(&mut self) -> UpdateModeId { - let id = UpdateModeId { id: self.next }; - self.next += 1; - id - } -} - -#[derive(Clone, Debug, PartialEq)] -pub enum CallType<'a> { - ByName { - name: Symbol, - ret_layout: &'a Layout<'a>, - arg_layouts: &'a [Layout<'a>], - specialization_id: CallSpecId, - }, - Foreign { - foreign_symbol: ForeignSymbol, - ret_layout: &'a Layout<'a>, - }, - LowLevel { - op: LowLevel, - update_mode: UpdateModeId, - }, - HigherOrder(&'a HigherOrderLowLevel<'a>), -} - -#[derive(Copy, Clone, Debug, PartialEq)] -pub struct PassedFunction<'a> { - /// name of the top-level function that is passed as an argument - /// e.g. in `List.map xs Num.abs` this would be `Num.abs` - pub name: Symbol, - - pub argument_layouts: &'a [Layout<'a>], - pub return_layout: Layout<'a>, - - pub specialization_id: CallSpecId, - - /// Symbol of the environment captured by the function argument - pub captured_environment: Symbol, - - pub owns_captured_environment: bool, -} - -#[derive(Clone, Debug, PartialEq)] -pub struct HigherOrderLowLevel<'a> { - pub op: crate::low_level::HigherOrder, - - /// TODO I _think_ we can get rid of this, perhaps only keeping track of - /// the layout of the closure argument, if any - pub closure_env_layout: Option>, - - /// update mode of the higher order lowlevel itself - pub update_mode: UpdateModeId, - - pub passed_function: PassedFunction<'a>, -} - -#[derive(Clone, Debug, PartialEq)] -pub enum Expr<'a> { - Literal(Literal<'a>), - - // Functions - Call(Call<'a>), - - Tag { - tag_layout: UnionLayout<'a>, - tag_name: TagOrClosure, - tag_id: TagIdIntType, - arguments: &'a [Symbol], - }, - Struct(&'a [Symbol]), - - StructAtIndex { - index: u64, - field_layouts: &'a [Layout<'a>], - structure: Symbol, - }, - - GetTagId { - structure: Symbol, - union_layout: UnionLayout<'a>, - }, - - UnionAtIndex { - structure: Symbol, - tag_id: TagIdIntType, - union_layout: UnionLayout<'a>, - index: u64, - }, - - Array { - elem_layout: Layout<'a>, - elems: &'a [ListLiteralElement<'a>], - }, - EmptyArray, - - ExprBox { - symbol: Symbol, - }, - - ExprUnbox { - symbol: Symbol, - }, - - Reuse { - symbol: Symbol, - update_tag_id: bool, - update_mode: UpdateModeId, - // normal Tag fields - tag_layout: UnionLayout<'a>, - tag_name: TagOrClosure, - tag_id: TagIdIntType, - arguments: &'a [Symbol], - }, - Reset { - symbol: Symbol, - update_mode: UpdateModeId, - }, - - RuntimeErrorFunction(&'a str), -} - -impl<'a> Literal<'a> { - pub fn to_doc<'b, D, A>(&'b self, alloc: &'b D) -> DocBuilder<'b, D, A> - where - D: DocAllocator<'b, A>, - D::Doc: Clone, - A: Clone, - { - use Literal::*; - - match self { - Int(bytes) => alloc.text(format!("{}i64", i128::from_ne_bytes(*bytes))), - U128(bytes) => alloc.text(format!("{}u128", u128::from_ne_bytes(*bytes))), - Float(lit) => alloc.text(format!("{}f64", lit)), - Decimal(bytes) => alloc.text(format!("{}dec", RocDec::from_ne_bytes(*bytes))), - Bool(lit) => alloc.text(format!("{}", lit)), - Byte(lit) => alloc.text(format!("{}u8", lit)), - Str(lit) => alloc.text(format!("{:?}", lit)), - } - } -} - -pub(crate) fn symbol_to_doc_string(symbol: Symbol) -> String { - use roc_module::ident::ModuleName; - - if pretty_print_ir_symbols() { - format!("{:?}", symbol) - } else { - let text = format!("{}", symbol); - - if text.starts_with(ModuleName::APP) { - let name: String = text.trim_start_matches(ModuleName::APP).into(); - format!("Test{}", name) - } else { - text - } - } -} - -fn symbol_to_doc<'b, D, A>(alloc: &'b D, symbol: Symbol) -> DocBuilder<'b, D, A> -where - D: DocAllocator<'b, A>, - D::Doc: Clone, - A: Clone, -{ - alloc.text(symbol_to_doc_string(symbol)) -} - -fn join_point_to_doc<'b, D, A>(alloc: &'b D, symbol: JoinPointId) -> DocBuilder<'b, D, A> -where - D: DocAllocator<'b, A>, - D::Doc: Clone, - A: Clone, -{ - symbol_to_doc(alloc, symbol.0) -} - -impl<'a> Expr<'a> { - pub fn to_doc<'b, D, A>(&'b self, alloc: &'b D) -> DocBuilder<'b, D, A> - where - D: DocAllocator<'b, A>, - D::Doc: Clone, - A: Clone, - { - use Expr::*; - - match self { - Literal(lit) => lit.to_doc(alloc), - - Call(call) => call.to_doc(alloc), - - Tag { - tag_name, - arguments, - .. - } => { - let doc_tag = match tag_name { - TagOrClosure::Tag(TagName(s)) => alloc.text(s.as_str()), - TagOrClosure::Closure(s) => alloc - .text("ClosureTag(") - .append(symbol_to_doc(alloc, *s)) - .append(")"), - }; - - let it = arguments.iter().map(|s| symbol_to_doc(alloc, *s)); - - doc_tag - .append(alloc.space()) - .append(alloc.intersperse(it, " ")) - } - Reuse { - symbol, - tag_name, - arguments, - update_mode, - .. - } => { - let doc_tag = match tag_name { - TagOrClosure::Tag(TagName(s)) => alloc.text(s.as_str()), - TagOrClosure::Closure(s) => alloc - .text("ClosureTag(") - .append(symbol_to_doc(alloc, *s)) - .append(")"), - }; - - let it = arguments.iter().map(|s| symbol_to_doc(alloc, *s)); - - alloc - .text("Reuse ") - .append(symbol_to_doc(alloc, *symbol)) - .append(alloc.space()) - .append(format!("{:?}", update_mode)) - .append(alloc.space()) - .append(doc_tag) - .append(alloc.space()) - .append(alloc.intersperse(it, " ")) - } - Reset { - symbol, - update_mode, - } => alloc.text(format!( - "Reset {{ symbol: {:?}, id: {} }}", - symbol, update_mode.id - )), - - Struct(args) => { - let it = args.iter().map(|s| symbol_to_doc(alloc, *s)); - - alloc - .text("Struct {") - .append(alloc.intersperse(it, ", ")) - .append(alloc.text("}")) - } - Array { elems, .. } => { - let it = elems.iter().map(|e| match e { - ListLiteralElement::Literal(l) => l.to_doc(alloc), - ListLiteralElement::Symbol(s) => symbol_to_doc(alloc, *s), - }); - - alloc - .text("Array [") - .append(alloc.intersperse(it, ", ")) - .append(alloc.text("]")) - } - EmptyArray => alloc.text("Array []"), - - StructAtIndex { - index, structure, .. - } => alloc - .text(format!("StructAtIndex {} ", index)) - .append(symbol_to_doc(alloc, *structure)), - - RuntimeErrorFunction(s) => alloc.text(format!("ErrorFunction {}", s)), - - GetTagId { structure, .. } => alloc - .text("GetTagId ") - .append(symbol_to_doc(alloc, *structure)), - - ExprBox { symbol, .. } => alloc.text("Box ").append(symbol_to_doc(alloc, *symbol)), - - ExprUnbox { symbol, .. } => alloc.text("Unbox ").append(symbol_to_doc(alloc, *symbol)), - - UnionAtIndex { - tag_id, - structure, - index, - .. - } => alloc - .text(format!("UnionAtIndex (Id {}) (Index {}) ", tag_id, index)) - .append(symbol_to_doc(alloc, *structure)), - } - } - - pub fn to_pretty(&self, width: usize) -> String { - let allocator = BoxAllocator; - let mut w = std::vec::Vec::new(); - self.to_doc::<_, ()>(&allocator) - .1 - .render(width, &mut w) - .unwrap(); - w.push(b'\n'); - String::from_utf8(w).unwrap() - } -} - -impl<'a> Stmt<'a> { - pub fn new( - env: &mut Env<'a, '_>, - can_expr: roc_can::expr::Expr, - var: Variable, - procs: &mut Procs<'a>, - layout_cache: &mut LayoutCache<'a>, - ) -> Self { - from_can(env, var, can_expr, procs, layout_cache) - } - - pub fn to_doc<'b, D, A>(&'b self, alloc: &'b D) -> DocBuilder<'b, D, A> - where - D: DocAllocator<'b, A>, - D::Doc: Clone, - A: Clone, - { - use Stmt::*; - - match self { - Let(symbol, expr, layout, cont) => alloc - .text("let ") - .append(symbol_to_doc(alloc, *symbol)) - .append(" : ") - .append(layout.to_doc(alloc, Parens::NotNeeded)) - .append(" = ") - .append(expr.to_doc(alloc)) - .append(";") - .append(alloc.hardline()) - .append(cont.to_doc(alloc)), - - Refcounting(modify, cont) => modify - .to_doc(alloc) - .append(alloc.hardline()) - .append(cont.to_doc(alloc)), - - Expect { condition, .. } => alloc - .text("expect ") - .append(symbol_to_doc(alloc, *condition)), - - Ret(symbol) => alloc - .text("ret ") - .append(symbol_to_doc(alloc, *symbol)) - .append(";"), - - Switch { - cond_symbol, - branches, - default_branch, - .. - } => { - match branches { - [(1, info, pass)] => { - let fail = default_branch.1; - alloc - .text("if ") - .append(symbol_to_doc(alloc, *cond_symbol)) - .append(" then") - .append(info.to_doc(alloc)) - .append(alloc.hardline()) - .append(pass.to_doc(alloc).indent(4)) - .append(alloc.hardline()) - .append(alloc.text("else")) - .append(default_branch.0.to_doc(alloc)) - .append(alloc.hardline()) - .append(fail.to_doc(alloc).indent(4)) - } - - _ => { - let default_doc = alloc - .text("default:") - .append(alloc.hardline()) - .append(default_branch.1.to_doc(alloc).indent(4)) - .indent(4); - - let branches_docs = branches - .iter() - .map(|(tag, _info, expr)| { - alloc - .text(format!("case {}:", tag)) - .append(alloc.hardline()) - .append(expr.to_doc(alloc).indent(4)) - .indent(4) - }) - .chain(std::iter::once(default_doc)); - // - alloc - .text("switch ") - .append(symbol_to_doc(alloc, *cond_symbol)) - .append(":") - .append(alloc.hardline()) - .append(alloc.intersperse( - branches_docs, - alloc.hardline().append(alloc.hardline()), - )) - .append(alloc.hardline()) - } - } - } - - RuntimeError(s) => alloc.text(format!("Error {}", s)), - - Join { - id, - parameters, - body: continuation, - remainder, - } => { - let it = parameters.iter().map(|p| symbol_to_doc(alloc, p.symbol)); - - alloc.intersperse( - vec![ - alloc - .text("joinpoint ") - .append(join_point_to_doc(alloc, *id)) - .append(" ".repeat(parameters.len().min(1))) - .append(alloc.intersperse(it, alloc.space())) - .append(":"), - continuation.to_doc(alloc).indent(4), - alloc.text("in"), - remainder.to_doc(alloc), - ], - alloc.hardline(), - ) - } - Jump(id, arguments) => { - let it = arguments.iter().map(|s| symbol_to_doc(alloc, *s)); - - alloc - .text("jump ") - .append(join_point_to_doc(alloc, *id)) - .append(" ".repeat(arguments.len().min(1))) - .append(alloc.intersperse(it, alloc.space())) - .append(";") - } - } - } - - pub fn to_pretty(&self, width: usize) -> String { - let allocator = BoxAllocator; - let mut w = std::vec::Vec::new(); - self.to_doc::<_, ()>(&allocator) - .1 - .render(width, &mut w) - .unwrap(); - w.push(b'\n'); - String::from_utf8(w).unwrap() - } - - pub fn is_terminal(&self) -> bool { - use Stmt::*; - - match self { - Switch { .. } => { - // TODO is this the reason Lean only looks at the outermost `when`? - true - } - Ret(_) => true, - Jump(_, _) => true, - _ => false, - } - } - - pub fn if_then_else( - arena: &'a Bump, - condition_symbol: Symbol, - return_layout: Layout<'a>, - then_branch_stmt: Stmt<'a>, - else_branch_stmt: &'a Stmt<'a>, - ) -> Self { - let then_branch_info = BranchInfo::Constructor { - scrutinee: condition_symbol, - layout: Layout::bool(), - tag_id: 1, - }; - let then_branch = (1u64, then_branch_info, then_branch_stmt); - - let else_branch_info = BranchInfo::Constructor { - scrutinee: condition_symbol, - layout: Layout::bool(), - tag_id: 0, - }; - let else_branch = (else_branch_info, else_branch_stmt); - - Stmt::Switch { - cond_symbol: condition_symbol, - cond_layout: Layout::bool(), - branches: &*arena.alloc([then_branch]), - default_branch: else_branch, - ret_layout: return_layout, - } - } -} - -fn from_can_let<'a>( - env: &mut Env<'a, '_>, - procs: &mut Procs<'a>, - layout_cache: &mut LayoutCache<'a>, - def: Box, - cont: Box>, - variable: Variable, - opt_assigned_and_hole: Option<(Symbol, &'a Stmt<'a>)>, -) -> Stmt<'a> { - use roc_can::expr::Expr::*; - - macro_rules! lower_rest { - ($variable:expr, $expr:expr) => { - lower_rest!(env, procs, layout_cache, $variable, $expr) - }; - ($env:expr, $procs:expr, $layout_cache:expr, $variable:expr, $expr:expr) => { - match opt_assigned_and_hole { - None => from_can($env, $variable, $expr, $procs, $layout_cache), - Some((assigned, hole)) => with_hole( - $env, - $expr, - $variable, - $procs, - $layout_cache, - assigned, - hole, - ), - } - }; - } - - if let roc_can::pattern::Pattern::Identifier(symbol) = &def.loc_pattern.value { - return match def.loc_expr.value { - Closure(closure_data) => { - register_capturing_closure(env, procs, layout_cache, *symbol, closure_data); - - lower_rest!(variable, cont.value) - } - Accessor(accessor_data) => { - let fresh_record_symbol = env.unique_symbol(); - register_noncapturing_closure( - env, - procs, - *symbol, - accessor_data.to_closure_data(fresh_record_symbol), - ); - - lower_rest!(variable, cont.value) - } - Var(original) | AbilityMember(original, _, _) => { - // a variable is aliased, e.g. - // - // foo = bar - // - // or - // - // foo = RBTRee.empty - - // TODO: right now we need help out rustc with the closure types; - // it isn't able to infer the right lifetime bounds. See if we - // can remove the annotations in the future. - let build_rest = - |env: &mut Env<'a, '_>, - procs: &mut Procs<'a>, - layout_cache: &mut LayoutCache<'a>| { - lower_rest!(env, procs, layout_cache, variable, cont.value) - }; - - return handle_variable_aliasing( - env, - procs, - layout_cache, - def.expr_var, - *symbol, - original, - build_rest, - ); - } - LetNonRec(nested_def, nested_cont) => { - use roc_can::expr::Expr::*; - // We must transform - // - // let answer = 1337 - // in - // let unused = - // let nested = 17 - // in - // nested - // in - // answer - // - // into - // - // let answer = 1337 - // in - // let nested = 17 - // in - // let unused = nested - // in - // answer - - let new_def = roc_can::def::Def { - loc_pattern: def.loc_pattern, - loc_expr: *nested_cont, - pattern_vars: def.pattern_vars, - annotation: def.annotation, - expr_var: def.expr_var, - }; - - let new_inner = LetNonRec(Box::new(new_def), cont); - - let new_outer = LetNonRec(nested_def, Box::new(Loc::at_zero(new_inner))); - - lower_rest!(variable, new_outer) - } - LetRec(nested_defs, nested_cont, cycle_mark) => { - use roc_can::expr::Expr::*; - // We must transform - // - // let answer = 1337 - // in - // let unused = - // let nested = \{} -> nested {} - // in - // nested - // in - // answer - // - // into - // - // let answer = 1337 - // in - // let nested = \{} -> nested {} - // in - // let unused = nested - // in - // answer - - let new_def = roc_can::def::Def { - loc_pattern: def.loc_pattern, - loc_expr: *nested_cont, - pattern_vars: def.pattern_vars, - annotation: def.annotation, - expr_var: def.expr_var, - }; - - let new_inner = LetNonRec(Box::new(new_def), cont); - - let new_outer = LetRec(nested_defs, Box::new(Loc::at_zero(new_inner)), cycle_mark); - - lower_rest!(variable, new_outer) - } - _ => { - let rest = lower_rest!(variable, cont.value); - - // Remove all the requested symbol specializations now, since this is the - // def site and hence we won't need them any higher up. - let mut needed_specializations = procs.symbol_specializations.remove(*symbol); - - match needed_specializations.len() { - 0 => { - // We don't need any specializations, that means this symbol is never - // referenced. - with_hole( - env, - def.loc_expr.value, - def.expr_var, - procs, - layout_cache, - *symbol, - env.arena.alloc(rest), - ) - } - - // We do need specializations - 1 => { - let (_specialization_mark, (var, specialized_symbol)) = - needed_specializations.next().unwrap(); - - // Unify the expr_var with the requested specialization once. - let _res = env.unify(var, def.expr_var); - - with_hole( - env, - def.loc_expr.value, - def.expr_var, - procs, - layout_cache, - specialized_symbol, - env.arena.alloc(rest), - ) - } - _n => { - let mut stmt = rest; - - // Need to eat the cost and create a specialized version of the body for - // each specialization. - for (_specialization_mark, (var, specialized_symbol)) in - needed_specializations - { - use crate::copy::deep_copy_type_vars_into_expr; - - let (new_def_expr_var, specialized_expr) = deep_copy_type_vars_into_expr( - env.arena, - env.subs, - def.expr_var, - &def.loc_expr.value, - ) - .expect( - "expr marked as having specializations, but it has no type variables!", - ); - - let _res = env.unify(var, new_def_expr_var); - - stmt = with_hole( - env, - specialized_expr, - new_def_expr_var, - procs, - layout_cache, - specialized_symbol, - env.arena.alloc(stmt), - ); - } - - stmt - } - } - } - }; - } - - // this may be a destructure pattern - let (mono_pattern, assignments) = - match from_can_pattern(env, procs, layout_cache, &def.loc_pattern.value) { - Ok(v) => v, - Err(_) => todo!(), - }; - - // convert the continuation - let mut stmt = lower_rest!(variable, cont.value); - - // layer on any default record fields - for (symbol, variable, expr) in assignments { - let specialization_symbol = procs - .symbol_specializations - .remove_single(symbol) - // Can happen when the symbol was never used under this body, and hence has no - // requested specialization. - .unwrap_or(symbol); - - let hole = env.arena.alloc(stmt); - stmt = with_hole( - env, - expr, - variable, - procs, - layout_cache, - specialization_symbol, - hole, - ); - } - - if let roc_can::expr::Expr::Var(outer_symbol) = def.loc_expr.value { - store_pattern(env, procs, layout_cache, &mono_pattern, outer_symbol, stmt) - } else { - let outer_symbol = env.unique_symbol(); - stmt = store_pattern(env, procs, layout_cache, &mono_pattern, outer_symbol, stmt); - - // convert the def body, store in outer_symbol - with_hole( - env, - def.loc_expr.value, - def.expr_var, - procs, - layout_cache, - outer_symbol, - env.arena.alloc(stmt), - ) - } -} - -/// turn record/tag patterns into a when expression, e.g. -/// -/// foo = \{ x } -> body -/// -/// becomes -/// -/// foo = \r -> when r is { x } -> body -/// -/// conversion of one-pattern when expressions will do the most optimal thing -#[allow(clippy::type_complexity)] -fn patterns_to_when<'a>( - env: &mut Env<'a, '_>, - patterns: std::vec::Vec<(Variable, AnnotatedMark, Loc)>, - body_var: Variable, - body: Loc, -) -> Result<(Vec<'a, Variable>, Vec<'a, Symbol>, Loc), Loc> { - let mut arg_vars = Vec::with_capacity_in(patterns.len(), env.arena); - let mut symbols = Vec::with_capacity_in(patterns.len(), env.arena); - let mut body = Ok(body); - - // patterns that are not yet in a when (e.g. in let or function arguments) must be irrefutable - // to pass type checking. So the order in which we add them to the body does not matter: there - // are only stores anyway, no branches. - // - // NOTE this fails if the pattern contains rigid variables, - // see https://github.com/rtfeldman/roc/issues/786 - // this must be fixed when moving exhaustiveness checking to the new canonical AST - for (pattern_var, annotated_mark, pattern) in patterns.into_iter() { - if annotated_mark.exhaustive.is_non_exhaustive(env.subs) { - // Even if the body was Ok, replace it with this Err. - // If it was already an Err, leave it at that Err, so the first - // RuntimeError we encountered remains the first. - let value = RuntimeError::UnsupportedPattern(pattern.region); - body = body.and({ - Err(Loc { - region: pattern.region, - value, - }) - }); - } else if let Ok(unwrapped_body) = body { - let (new_symbol, new_body) = - pattern_to_when(env, pattern_var, pattern, body_var, unwrapped_body); - - symbols.push(new_symbol); - arg_vars.push(pattern_var); - - body = Ok(new_body) - } - } - - match body { - Ok(body) => Ok((arg_vars, symbols, body)), - Err(loc_error) => Err(loc_error), - } -} - -/// turn irrefutable patterns into when. For example -/// -/// foo = \{ x } -> body -/// -/// Assuming the above program typechecks, the pattern match cannot fail -/// (it is irrefutable). It becomes -/// -/// foo = \r -> -/// when r is -/// { x } -> body -/// -/// conversion of one-pattern when expressions will do the most optimal thing -fn pattern_to_when<'a>( - env: &mut Env<'a, '_>, - pattern_var: Variable, - pattern: Loc, - body_var: Variable, - body: Loc, -) -> (Symbol, Loc) { - use roc_can::expr::Expr::*; - use roc_can::expr::WhenBranch; - use roc_can::pattern::Pattern::*; - - match &pattern.value { - Identifier(symbol) => (*symbol, body), - Underscore => { - // for underscore we generate a dummy Symbol - (env.unique_symbol(), body) - } - Shadowed(region, loc_ident, new_symbol) => { - let error = roc_problem::can::RuntimeError::Shadowing { - original_region: *region, - shadow: loc_ident.clone(), - kind: ShadowKind::Variable, - }; - (*new_symbol, Loc::at_zero(RuntimeError(error))) - } - - UnsupportedPattern(region) => { - // create the runtime error here, instead of delegating to When. - // UnsupportedPattern should then never occur in When - let error = roc_problem::can::RuntimeError::UnsupportedPattern(*region); - (env.unique_symbol(), Loc::at_zero(RuntimeError(error))) - } - - MalformedPattern(problem, region) => { - // create the runtime error here, instead of delegating to When. - let error = roc_problem::can::RuntimeError::MalformedPattern(*problem, *region); - (env.unique_symbol(), Loc::at_zero(RuntimeError(error))) - } - - OpaqueNotInScope(loc_ident) => { - // create the runtime error here, instead of delegating to When. - // TODO(opaques) should be `RuntimeError::OpaqueNotDefined` - let error = roc_problem::can::RuntimeError::UnsupportedPattern(loc_ident.region); - (env.unique_symbol(), Loc::at_zero(RuntimeError(error))) - } - - AppliedTag { .. } | RecordDestructure { .. } | UnwrappedOpaque { .. } => { - let symbol = env.unique_symbol(); - - let wrapped_body = When { - cond_var: pattern_var, - expr_var: body_var, - region: Region::zero(), - loc_cond: Box::new(Loc::at_zero(Var(symbol))), - branches: vec![WhenBranch { - patterns: vec![pattern], - value: body, - guard: None, - // If this type-checked, it's non-redundant - redundant: RedundantMark::known_non_redundant(), - }], - branches_cond_var: pattern_var, - // If this type-checked, it's exhaustive - exhaustive: ExhaustiveMark::known_exhaustive(), - }; - - (symbol, Loc::at_zero(wrapped_body)) - } - - IntLiteral(..) - | NumLiteral(..) - | FloatLiteral(..) - | StrLiteral(..) - | roc_can::pattern::Pattern::SingleQuote(..) => { - // These patters are refutable, and thus should never occur outside a `when` expression - // They should have been replaced with `UnsupportedPattern` during canonicalization - unreachable!("refutable pattern {:?} where irrefutable pattern is expected. This should never happen!", pattern.value) - } - - AbilityMemberSpecialization { .. } => { - unreachable!( - "Ability member specialization {:?} should never appear in a when!", - pattern.value - ) - } - } -} - -fn specialize_suspended<'a>( - env: &mut Env<'a, '_>, - procs: &mut Procs<'a>, - layout_cache: &mut LayoutCache<'a>, - suspended: Suspended<'a>, -) { - let offset_variable = StorageSubs::merge_into(suspended.store, env.subs); - - for (i, (symbol, var)) in suspended - .symbols - .iter() - .zip(suspended.variables.iter()) - .enumerate() - { - let name = *symbol; - let outside_layout = suspended.layouts[i]; - - let var = offset_variable(*var); - - // TODO define our own Entry for Specialized? - let partial_proc = if procs.specialized.is_specialized(name, &outside_layout) { - // already specialized, just continue - continue; - } else { - match procs.partial_procs.symbol_to_id(name) { - Some(v) => { - // Mark this proc as in-progress, so if we're dealing with - // mutually recursive functions, we don't loop forever. - // (We had a bug around this before this system existed!) - procs.specialized.mark_in_progress(name, outside_layout); - - v - } - None => { - // TODO this assumes the specialization is done by another module - // make sure this does not become a problem down the road! - continue; - } - } - }; - - match specialize_variable(env, procs, name, layout_cache, var, &[], partial_proc) { - Ok((proc, layout)) => { - // TODO thiscode is duplicated elsewhere - let top_level = ProcLayout::from_raw(env.arena, layout); - - if procs.is_module_thunk(proc.name) { - debug_assert!( - top_level.arguments.is_empty(), - "{:?} from {:?}", - name, - layout - ); - } - - debug_assert_eq!(outside_layout, top_level, " in {:?}", name); - procs.specialized.insert_specialized(name, top_level, proc); - } - Err(SpecializeFailure { - attempted_layout, .. - }) => { - let proc = generate_runtime_error_function(env, name, attempted_layout); - - let top_level = ProcLayout::from_raw(env.arena, attempted_layout); - - procs.specialized.insert_specialized(name, top_level, proc); - } - } - } -} - -pub fn specialize_all<'a>( - env: &mut Env<'a, '_>, - mut procs: Procs<'a>, - externals_others_need: std::vec::Vec, - specializations_for_host: HostSpecializations, - layout_cache: &mut LayoutCache<'a>, -) -> Procs<'a> { - for externals in externals_others_need { - specialize_external_specializations(env, &mut procs, layout_cache, externals); - } - - // When calling from_can, pending_specializations should be unavailable. - // This must be a single pass, and we must not add any more entries to it! - let pending_specializations = std::mem::replace( - &mut procs.pending_specializations, - PendingSpecializations::Making, - ); - - match pending_specializations { - PendingSpecializations::Making => {} - PendingSpecializations::Finding(suspended) => { - specialize_suspended(env, &mut procs, layout_cache, suspended) - } - } - - specialize_host_specializations(env, &mut procs, layout_cache, specializations_for_host); - - debug_assert!( - procs.symbol_specializations.is_empty(), - "{:?}", - &procs.symbol_specializations - ); - - procs -} - -fn specialize_host_specializations<'a>( - env: &mut Env<'a, '_>, - procs: &mut Procs<'a>, - layout_cache: &mut LayoutCache<'a>, - host_specializations: HostSpecializations, -) { - let (store, it) = host_specializations.decompose(); - - let offset_variable = StorageSubs::merge_into(store, env.subs); - - for (symbol, variable, host_exposed_aliases) in it { - specialize_external_help( - env, - procs, - layout_cache, - symbol, - offset_variable(variable), - &host_exposed_aliases, - ) - } -} - -fn specialize_external_specializations<'a>( - env: &mut Env<'a, '_>, - procs: &mut Procs<'a>, - layout_cache: &mut LayoutCache<'a>, - externals_others_need: ExternalSpecializations, -) { - let (store, it) = externals_others_need.decompose(); - - let offset_variable = StorageSubs::merge_into(store, env.subs); - - for (symbol, solved_types) in it { - for store_variable in solved_types { - // historical note: we used to deduplicate with a hash here, - // but the cost of that hash is very high. So for now we make - // duplicate specializations, and the insertion into a hash map - // below will deduplicate them. - - specialize_external_help( - env, - procs, - layout_cache, - symbol, - offset_variable(store_variable), - &[], - ) - } - } -} - -fn specialize_external_help<'a>( - env: &mut Env<'a, '_>, - procs: &mut Procs<'a>, - layout_cache: &mut LayoutCache<'a>, - name: Symbol, - variable: Variable, - host_exposed_aliases: &[(Symbol, Variable)], -) { - let partial_proc_id = match procs.partial_procs.symbol_to_id(name) { - Some(v) => v, - None => { - panic!("Cannot find a partial proc for {:?}", name); - } - }; - - let specialization_result = specialize_variable( - env, - procs, - name, - layout_cache, - variable, - host_exposed_aliases, - partial_proc_id, - ); - - match specialization_result { - Ok((proc, layout)) => { - let top_level = ProcLayout::from_raw(env.arena, layout); - - if procs.is_module_thunk(name) { - debug_assert!(top_level.arguments.is_empty()); - } - - procs.specialized.insert_specialized(name, top_level, proc); - } - Err(SpecializeFailure { attempted_layout }) => { - let proc = generate_runtime_error_function(env, name, attempted_layout); - - let top_level = ProcLayout::from_raw(env.arena, attempted_layout); - - procs.specialized.insert_specialized(name, top_level, proc); - } - } -} - -fn generate_runtime_error_function<'a>( - env: &mut Env<'a, '_>, - name: Symbol, - layout: RawFunctionLayout<'a>, -) -> Proc<'a> { - let mut msg = bumpalo::collections::string::String::with_capacity_in(80, env.arena); - use std::fmt::Write; - write!( - &mut msg, - "The {:?} function could not be generated, likely due to a type error.", - name - ) - .unwrap(); - - eprintln!( - "emitted runtime error function {:?} for layout {:?}", - &msg, layout - ); - - let runtime_error = Stmt::RuntimeError(msg.into_bump_str()); - - let (args, ret_layout) = match layout { - RawFunctionLayout::Function(arg_layouts, lambda_set, ret_layout) => { - let mut args = Vec::with_capacity_in(arg_layouts.len(), env.arena); - - for arg in arg_layouts { - args.push((*arg, env.unique_symbol())); - } - - args.push((Layout::LambdaSet(lambda_set), Symbol::ARG_CLOSURE)); - - (args.into_bump_slice(), *ret_layout) - } - RawFunctionLayout::ZeroArgumentThunk(ret_layout) => (&[] as &[_], ret_layout), - }; - - Proc { - name, - args, - body: runtime_error, - closure_data_layout: None, - ret_layout, - is_self_recursive: SelfRecursive::NotSelfRecursive, - must_own_arguments: false, - host_exposed_layouts: HostExposedLayouts::NotHostExposed, - } -} - -fn specialize_external<'a>( - env: &mut Env<'a, '_>, - procs: &mut Procs<'a>, - proc_name: Symbol, - layout_cache: &mut LayoutCache<'a>, - fn_var: Variable, - host_exposed_variables: &[(Symbol, Variable)], - partial_proc_id: PartialProcId, -) -> Result, LayoutProblem> { - let partial_proc = procs.partial_procs.get_id(partial_proc_id); - let captured_symbols = partial_proc.captured_symbols; - - // unify the called function with the specialized signature, then specialize the function body - let snapshot = env.subs.snapshot(); - let cache_snapshot = layout_cache.snapshot(); - - let _unified = env.unify(partial_proc.annotation, fn_var); - - // This will not hold for programs with type errors - // let is_valid = matches!(unified, roc_unify::unify::Unified::Success(_)); - // debug_assert!(is_valid, "unificaton failure for {:?}", proc_name); - - // if this is a closure, add the closure record argument - let pattern_symbols = match partial_proc.captured_symbols { - CapturedSymbols::None => partial_proc.pattern_symbols, - CapturedSymbols::Captured([]) => partial_proc.pattern_symbols, - CapturedSymbols::Captured(_) => { - let mut temp = - Vec::from_iter_in(partial_proc.pattern_symbols.iter().copied(), env.arena); - temp.push(Symbol::ARG_CLOSURE); - temp.into_bump_slice() - } - }; - - let specialized = - build_specialized_proc_from_var(env, layout_cache, proc_name, pattern_symbols, fn_var)?; - - // determine the layout of aliases/rigids exposed to the host - let host_exposed_layouts = if host_exposed_variables.is_empty() { - HostExposedLayouts::NotHostExposed - } else { - let mut aliases = BumpMap::new_in(env.arena); - - for (symbol, variable) in host_exposed_variables { - let layout = layout_cache - .raw_from_var(env.arena, *variable, env.subs) - .unwrap(); - - let name = env.unique_symbol(); - - match layout { - RawFunctionLayout::Function(argument_layouts, lambda_set, return_layout) => { - let assigned = env.unique_symbol(); - - let mut argument_symbols = - Vec::with_capacity_in(argument_layouts.len(), env.arena); - let mut proc_arguments = - Vec::with_capacity_in(argument_layouts.len() + 1, env.arena); - let mut top_level_arguments = - Vec::with_capacity_in(argument_layouts.len() + 1, env.arena); - - for layout in argument_layouts { - let symbol = env.unique_symbol(); - - proc_arguments.push((*layout, symbol)); - - argument_symbols.push(symbol); - top_level_arguments.push(*layout); - } - - // the proc needs to take an extra closure argument - let lambda_set_layout = Layout::LambdaSet(lambda_set); - proc_arguments.push((lambda_set_layout, Symbol::ARG_CLOSURE)); - - // this should also be reflected in the TopLevel signature - top_level_arguments.push(lambda_set_layout); - - let hole = env.arena.alloc(Stmt::Ret(assigned)); - - let body = match_on_lambda_set( - env, - lambda_set, - Symbol::ARG_CLOSURE, - argument_symbols.into_bump_slice(), - argument_layouts, - return_layout, - assigned, - hole, - ); - - let proc = Proc { - name, - args: proc_arguments.into_bump_slice(), - body, - closure_data_layout: None, - ret_layout: *return_layout, - is_self_recursive: SelfRecursive::NotSelfRecursive, - must_own_arguments: false, - host_exposed_layouts: HostExposedLayouts::NotHostExposed, - }; - - let top_level = ProcLayout::new( - env.arena, - top_level_arguments.into_bump_slice(), - *return_layout, - ); - - procs.specialized.insert_specialized(name, top_level, proc); - - aliases.insert(*symbol, (name, top_level, layout)); - } - RawFunctionLayout::ZeroArgumentThunk(_) => { - unreachable!("so far"); - } - } - } - - HostExposedLayouts::HostExposed { - rigids: BumpMap::new_in(env.arena), - aliases, - } - }; - - let recursivity = if partial_proc.is_self_recursive { - SelfRecursive::SelfRecursive(JoinPointId(env.unique_symbol())) - } else { - SelfRecursive::NotSelfRecursive - }; - - let body = partial_proc.body.clone(); - - let mut specialized_body = from_can(env, partial_proc.body_var, body, procs, layout_cache); - - match specialized { - SpecializedLayout::FunctionPointerBody { - ret_layout, - closure: opt_closure_layout, - } => { - // this is a function body like - // - // foo = Num.add - // - // we need to expand this to - // - // foo = \x,y -> Num.add x y - - // reset subs, so we don't get type errors when specializing for a different signature - layout_cache.rollback_to(cache_snapshot); - env.subs.rollback_to(snapshot); - - let closure_data_layout = match opt_closure_layout { - Some(lambda_set) => Layout::LambdaSet(lambda_set), - None => Layout::UNIT, - }; - - // I'm not sure how to handle the closure case, does it ever occur? - debug_assert!(matches!(captured_symbols, CapturedSymbols::None)); - - let proc = Proc { - name: proc_name, - args: &[], - body: specialized_body, - closure_data_layout: Some(closure_data_layout), - ret_layout, - is_self_recursive: recursivity, - must_own_arguments: false, - host_exposed_layouts, - }; - - Ok(proc) - } - SpecializedLayout::FunctionBody { - arguments: proc_args, - closure: opt_closure_layout, - ret_layout, - } => { - // unpack the closure symbols, if any - match (opt_closure_layout, captured_symbols) { - (Some(closure_layout), CapturedSymbols::Captured(captured)) => { - // debug_assert!(!captured.is_empty()); - - // An argument from the closure list may have taken on a specialized symbol - // name during the evaluation of the def body. If this is the case, load the - // specialized name rather than the original captured name! - let mut get_specialized_name = |symbol| { - procs - .symbol_specializations - .remove_single(symbol) - .unwrap_or(symbol) - }; - - match closure_layout.layout_for_member(proc_name) { - ClosureRepresentation::Union { - alphabetic_order_fields: field_layouts, - union_layout, - tag_id, - .. - } => { - debug_assert!(matches!(union_layout, UnionLayout::NonRecursive(_))); - debug_assert_eq!(field_layouts.len(), captured.len()); - - // captured variables are in symbol-alphabetic order, but now we want - // them ordered by their alignment requirements - let mut combined = Vec::from_iter_in( - captured.iter().map(|(x, _)| x).zip(field_layouts.iter()), - env.arena, - ); - - let ptr_bytes = env.target_info; - - combined.sort_by(|(_, layout1), (_, layout2)| { - let size1 = layout1.alignment_bytes(ptr_bytes); - let size2 = layout2.alignment_bytes(ptr_bytes); - - size2.cmp(&size1) - }); - - for (index, (symbol, layout)) in combined.iter().enumerate() { - let expr = Expr::UnionAtIndex { - tag_id, - structure: Symbol::ARG_CLOSURE, - index: index as u64, - union_layout, - }; - - let symbol = get_specialized_name(**symbol); - - specialized_body = Stmt::Let( - symbol, - expr, - **layout, - env.arena.alloc(specialized_body), - ); - } - } - ClosureRepresentation::AlphabeticOrderStruct(field_layouts) => { - // captured variables are in symbol-alphabetic order, but now we want - // them ordered by their alignment requirements - let mut combined = Vec::from_iter_in( - captured.iter().map(|(x, _)| x).zip(field_layouts.iter()), - env.arena, - ); - - let ptr_bytes = env.target_info; - - combined.sort_by(|(_, layout1), (_, layout2)| { - let size1 = layout1.alignment_bytes(ptr_bytes); - let size2 = layout2.alignment_bytes(ptr_bytes); - - size2.cmp(&size1) - }); - - debug_assert_eq!( - captured.len(), - field_layouts.len(), - "{:?} captures {:?} but has layout {:?}", - proc_name, - &captured, - &field_layouts - ); - - for (index, (symbol, layout)) in combined.iter().enumerate() { - let expr = Expr::StructAtIndex { - index: index as _, - field_layouts, - structure: Symbol::ARG_CLOSURE, - }; - - let symbol = get_specialized_name(**symbol); - - specialized_body = Stmt::Let( - symbol, - expr, - **layout, - env.arena.alloc(specialized_body), - ); - } - // let symbol = captured[0].0; - // - // substitute_in_exprs( - // env.arena, - // &mut specialized_body, - // symbol, - // Symbol::ARG_CLOSURE, - // ); - } - - ClosureRepresentation::Other(layout) => match layout { - Layout::Builtin(Builtin::Bool) => { - // just ignore this value - // IDEA don't pass this value in the future - } - Layout::Builtin(Builtin::Int(IntWidth::U8)) => { - // just ignore this value - // IDEA don't pass this value in the future - } - other => { - // NOTE other values always should be wrapped in a 1-element record - unreachable!( - "{:?} is not a valid closure data representation", - other - ) - } - }, - } - } - (None, CapturedSymbols::None) | (None, CapturedSymbols::Captured([])) => {} - _ => unreachable!("to closure or not to closure?"), - } - - let proc_args: Vec<_> = proc_args - .iter() - .map(|&(layout, symbol)| { - // Grab the specialization symbol, if it exists. - let symbol = procs - .symbol_specializations - .remove_single(symbol) - .unwrap_or(symbol); - - (layout, symbol) - }) - .collect_in(env.arena); - - // reset subs, so we don't get type errors when specializing for a different signature - layout_cache.rollback_to(cache_snapshot); - env.subs.rollback_to(snapshot); - - let closure_data_layout = match opt_closure_layout { - Some(lambda_set) => Some(Layout::LambdaSet(lambda_set)), - None => None, - }; - - let proc = Proc { - name: proc_name, - args: proc_args.into_bump_slice(), - body: specialized_body, - closure_data_layout, - ret_layout, - is_self_recursive: recursivity, - must_own_arguments: false, - host_exposed_layouts, - }; - - Ok(proc) - } - } -} - -#[derive(Debug)] -enum SpecializedLayout<'a> { - /// A body like `foo = \a,b,c -> ...` - FunctionBody { - arguments: &'a [(Layout<'a>, Symbol)], - closure: Option>, - ret_layout: Layout<'a>, - }, - /// A body like `foo = Num.add` - FunctionPointerBody { - closure: Option>, - ret_layout: Layout<'a>, - }, -} - -#[allow(clippy::type_complexity)] -fn build_specialized_proc_from_var<'a>( - env: &mut Env<'a, '_>, - layout_cache: &mut LayoutCache<'a>, - proc_name: Symbol, - pattern_symbols: &[Symbol], - fn_var: Variable, -) -> Result, LayoutProblem> { - match layout_cache.raw_from_var(env.arena, fn_var, env.subs)? { - RawFunctionLayout::Function(pattern_layouts, closure_layout, ret_layout) => { - let mut pattern_layouts_vec = Vec::with_capacity_in(pattern_layouts.len(), env.arena); - pattern_layouts_vec.extend_from_slice(pattern_layouts); - - build_specialized_proc( - env.arena, - proc_name, - pattern_symbols, - pattern_layouts_vec, - Some(closure_layout), - *ret_layout, - ) - } - RawFunctionLayout::ZeroArgumentThunk(ret_layout) => { - // a top-level constant 0-argument thunk - build_specialized_proc( - env.arena, - proc_name, - pattern_symbols, - Vec::new_in(env.arena), - None, - ret_layout, - ) - } - } -} - -#[allow(clippy::type_complexity)] -fn build_specialized_proc<'a>( - arena: &'a Bump, - proc_name: Symbol, - pattern_symbols: &[Symbol], - pattern_layouts: Vec<'a, Layout<'a>>, - lambda_set: Option>, - ret_layout: Layout<'a>, -) -> Result, LayoutProblem> { - use SpecializedLayout::*; - - let mut proc_args = Vec::with_capacity_in(pattern_layouts.len(), arena); - - let pattern_layouts_len = pattern_layouts.len(); - - for (arg_layout, arg_name) in pattern_layouts.into_iter().zip(pattern_symbols.iter()) { - proc_args.push((arg_layout, *arg_name)); - } - - // Given - // - // foo = - // x = 42 - // - // f = \{} -> x - // - // We desugar that into - // - // f = \{}, x -> x - // - // foo = - // x = 42 - // - // f_closure = { ptr: f, closure: x } - // - // then - - match lambda_set { - Some(lambda_set) if pattern_symbols.last() == Some(&Symbol::ARG_CLOSURE) => { - // here we define the lifted (now top-level) f function. Its final argument is `Symbol::ARG_CLOSURE`, - // it stores the closure structure (just an integer in this case) - proc_args.push((Layout::LambdaSet(lambda_set), Symbol::ARG_CLOSURE)); - - debug_assert_eq!( - pattern_layouts_len + 1, - pattern_symbols.len(), - "Tried to zip two vecs with different lengths in {:?}!", - proc_name, - ); - - let proc_args = proc_args.into_bump_slice(); - - Ok(FunctionBody { - arguments: proc_args, - closure: Some(lambda_set), - ret_layout, - }) - } - Some(lambda_set) => { - // a function that returns a function, but is not itself a closure - // e.g. f = Num.add - - // make sure there is not arg_closure argument without a closure layout - debug_assert!(pattern_symbols.last() != Some(&Symbol::ARG_CLOSURE)); - - use std::cmp::Ordering; - match pattern_layouts_len.cmp(&pattern_symbols.len()) { - Ordering::Equal => { - let proc_args = proc_args.into_bump_slice(); - - Ok(FunctionBody { - arguments: proc_args, - closure: None, - ret_layout, - }) - } - Ordering::Greater => { - if pattern_symbols.is_empty() { - let ret_layout = Layout::LambdaSet(lambda_set); - Ok(FunctionPointerBody { - closure: None, - ret_layout, - }) - } else { - // so far, the problem when hitting this branch was always somewhere else - // I think this branch should not be reachable in a bugfree compiler - panic!( - "more arguments (according to the layout) than argument symbols for {:?}", - proc_name - ) - } - } - Ordering::Less => panic!( - "more argument symbols than arguments (according to the layout) for {:?}", - proc_name - ), - } - } - None => { - // else we're making a normal function, no closure problems to worry about - // we'll just assert some things - - // make sure there is not arg_closure argument without a closure layout - debug_assert!(pattern_symbols.last() != Some(&Symbol::ARG_CLOSURE)); - - use std::cmp::Ordering; - match pattern_layouts_len.cmp(&pattern_symbols.len()) { - Ordering::Equal => { - let proc_args = proc_args.into_bump_slice(); - - Ok(FunctionBody { - arguments: proc_args, - closure: None, - ret_layout, - }) - } - Ordering::Greater => { - if pattern_symbols.is_empty() { - Ok(FunctionPointerBody { - closure: None, - ret_layout, - }) - } else { - // so far, the problem when hitting this branch was always somewhere else - // I think this branch should not be reachable in a bugfree compiler - panic!( - "more arguments (according to the layout) than argument symbols for {:?}", - proc_name - ) - } - } - Ordering::Less => panic!( - "more argument symbols than arguments (according to the layout) for {:?}", - proc_name - ), - } - } - } -} - -#[derive(Debug)] -struct SpecializeFailure<'a> { - /// The layout we attempted to create - attempted_layout: RawFunctionLayout<'a>, -} - -type SpecializeSuccess<'a> = (Proc<'a>, RawFunctionLayout<'a>); - -fn specialize_variable<'a>( - env: &mut Env<'a, '_>, - procs: &mut Procs<'a>, - proc_name: Symbol, - layout_cache: &mut LayoutCache<'a>, - fn_var: Variable, - host_exposed_aliases: &[(Symbol, Variable)], - partial_proc_id: PartialProcId, -) -> Result, SpecializeFailure<'a>> { - specialize_variable_help( - env, - procs, - proc_name, - layout_cache, - |_| fn_var, - host_exposed_aliases, - partial_proc_id, - ) -} - -fn specialize_variable_help<'a, F>( - env: &mut Env<'a, '_>, - procs: &mut Procs<'a>, - proc_name: Symbol, - layout_cache: &mut LayoutCache<'a>, - fn_var_thunk: F, - host_exposed_variables: &[(Symbol, Variable)], - partial_proc_id: PartialProcId, -) -> Result, SpecializeFailure<'a>> -where - F: FnOnce(&mut Env<'a, '_>) -> Variable, -{ - // add the specializations that other modules require of us - - let snapshot = env.subs.snapshot(); - let cache_snapshot = layout_cache.snapshot(); - - // important: evaluate after the snapshot has been created! - let fn_var = fn_var_thunk(env); - - // for debugging only - let raw = layout_cache - .raw_from_var(env.arena, fn_var, env.subs) - .unwrap_or_else(|err| panic!("TODO handle invalid function {:?}", err)); - - let raw = if procs.is_module_thunk(proc_name) { - match raw { - RawFunctionLayout::Function(_, lambda_set, _) => { - RawFunctionLayout::ZeroArgumentThunk(Layout::LambdaSet(lambda_set)) - } - _ => raw, - } - } else { - raw - }; - - // make sure rigid variables in the annotation are converted to flex variables - let annotation_var = procs.partial_procs.get_id(partial_proc_id).annotation; - instantiate_rigids(env.subs, annotation_var); - - let specialized = specialize_external( - env, - procs, - proc_name, - layout_cache, - fn_var, - host_exposed_variables, - partial_proc_id, - ); - - match specialized { - Ok(proc) => { - // when successful, the layout after unification should be the layout before unification - // debug_assert_eq!( - // attempted_layout, - // layout_cache - // .from_var(env.arena, fn_var, env.subs) - // .unwrap_or_else(|err| panic!("TODO handle invalid function {:?}", err)) - // ); - - env.subs.rollback_to(snapshot); - layout_cache.rollback_to(cache_snapshot); - - Ok((proc, raw)) - } - Err(error) => { - env.subs.rollback_to(snapshot); - layout_cache.rollback_to(cache_snapshot); - - // earlier we made this information available where we handle the failure - // but we didn't do anything useful with it. So it's here if we ever need it again - let _ = error; - - Err(SpecializeFailure { - attempted_layout: raw, - }) - } - } -} - -#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] -pub struct ProcLayout<'a> { - pub arguments: &'a [Layout<'a>], - pub result: Layout<'a>, -} - -impl<'a> ProcLayout<'a> { - pub fn new(arena: &'a Bump, old_arguments: &'a [Layout<'a>], result: Layout<'a>) -> Self { - let mut arguments = Vec::with_capacity_in(old_arguments.len(), arena); - - for old in old_arguments { - let other = old; - arguments.push(*other); - } - - let other = result; - let new_result = other; - - ProcLayout { - arguments: arguments.into_bump_slice(), - result: new_result, - } - } - - pub fn from_raw(arena: &'a Bump, raw: RawFunctionLayout<'a>) -> Self { - match raw { - RawFunctionLayout::Function(arguments, lambda_set, result) => { - let arguments = lambda_set.extend_argument_list(arena, arguments); - ProcLayout::new(arena, arguments, *result) - } - RawFunctionLayout::ZeroArgumentThunk(result) => ProcLayout::new(arena, &[], result), - } - } -} - -fn specialize_naked_symbol<'a>( - env: &mut Env<'a, '_>, - variable: Variable, - procs: &mut Procs<'a>, - layout_cache: &mut LayoutCache<'a>, - assigned: Symbol, - hole: &'a Stmt<'a>, - symbol: Symbol, -) -> Stmt<'a> { - if procs.is_module_thunk(symbol) { - let fn_var = variable; - - // This is a top-level declaration, which will code gen to a 0-arity thunk. - let result = call_by_name( - env, - procs, - fn_var, - symbol, - std::vec::Vec::new(), - layout_cache, - assigned, - env.arena.alloc(Stmt::Ret(assigned)), - ); - - return result; - } else if env.is_imported_symbol(symbol) { - match layout_cache.from_var(env.arena, variable, env.subs) { - Err(e) => panic!("invalid layout {:?}", e), - Ok(_) => { - // this is a 0-arity thunk - let result = call_by_name( - env, - procs, - variable, - symbol, - std::vec::Vec::new(), - layout_cache, - assigned, - env.arena.alloc(match hole { - Stmt::Jump(id, _) => Stmt::Jump(*id, env.arena.alloc([assigned])), - Stmt::Ret(_) => Stmt::Ret(assigned), - _ => unreachable!(), - }), - ); - - return result; - } - } - } - - let result = match hole { - Stmt::Jump(id, _) => Stmt::Jump(*id, env.arena.alloc([symbol])), - _ => Stmt::Ret(symbol), - }; - - // if the symbol is a function symbol, ensure it is properly specialized! - let original = symbol; - - let opt_fn_var = Some(variable); - - // if this is a function symbol, ensure that it's properly specialized! - specialize_symbol( - env, - procs, - layout_cache, - opt_fn_var, - symbol, - result, - original, - ) -} - -fn try_make_literal<'a>( - env: &mut Env<'a, '_>, - can_expr: &roc_can::expr::Expr, -) -> Option> { - use roc_can::expr::Expr::*; - - match can_expr { - Int(_, precision, _, int, _bound) => { - match num_argument_to_int_or_float(env.subs, env.target_info, *precision, false) { - IntOrFloat::Int(_) => Some(match *int { - IntValue::I128(n) => Literal::Int(n), - IntValue::U128(n) => Literal::U128(n), - }), - _ => unreachable!("unexpected float precision for integer"), - } - } - - Float(_, precision, float_str, float, _bound) => { - match num_argument_to_int_or_float(env.subs, env.target_info, *precision, true) { - IntOrFloat::Float(_) => Some(Literal::Float(*float)), - IntOrFloat::DecimalFloatType => { - let dec = match RocDec::from_str(float_str) { - Some(d) => d, - None => panic!( - r"Invalid decimal for float literal = {}. TODO: Make this a nice, user-friendly error message", - float_str - ), - }; - - Some(Literal::Decimal(dec.to_ne_bytes())) - } - _ => unreachable!("unexpected float precision for integer"), - } - } - - // TODO investigate lifetime trouble - // Str(string) => Some(Literal::Str(env.arena.alloc(string))), - Num(var, num_str, num, _bound) => { - // first figure out what kind of number this is - match num_argument_to_int_or_float(env.subs, env.target_info, *var, false) { - IntOrFloat::Int(_) => Some(match *num { - IntValue::I128(n) => Literal::Int(n), - IntValue::U128(n) => Literal::U128(n), - }), - IntOrFloat::Float(_) => Some(match *num { - IntValue::I128(n) => Literal::Float(i128::from_ne_bytes(n) as f64), - IntValue::U128(n) => Literal::Float(u128::from_ne_bytes(n) as f64), - }), - IntOrFloat::DecimalFloatType => { - let dec = match RocDec::from_str(num_str) { - Some(d) => d, - None => panic!( - r"Invalid decimal for float literal = {}. TODO: Make this a nice, user-friendly error message", - num_str - ), - }; - - Some(Literal::Decimal(dec.to_ne_bytes())) - } - } - } - _ => None, - } -} - -pub fn with_hole<'a>( - env: &mut Env<'a, '_>, - can_expr: roc_can::expr::Expr, - variable: Variable, - procs: &mut Procs<'a>, - layout_cache: &mut LayoutCache<'a>, - assigned: Symbol, - hole: &'a Stmt<'a>, -) -> Stmt<'a> { - use roc_can::expr::Expr::*; - - let arena = env.arena; - - match can_expr { - Int(_, precision, _, int, _bound) => { - match num_argument_to_int_or_float(env.subs, env.target_info, precision, false) { - IntOrFloat::Int(precision) => Stmt::Let( - assigned, - Expr::Literal(match int { - IntValue::I128(n) => Literal::Int(n), - IntValue::U128(n) => Literal::U128(n), - }), - Layout::Builtin(Builtin::Int(precision)), - hole, - ), - _ => unreachable!("unexpected float precision for integer"), - } - } - - Float(_, precision, float_str, float, _bound) => { - match num_argument_to_int_or_float(env.subs, env.target_info, precision, true) { - IntOrFloat::Float(precision) => Stmt::Let( - assigned, - Expr::Literal(Literal::Float(float)), - Layout::Builtin(Builtin::Float(precision)), - hole, - ), - IntOrFloat::DecimalFloatType => { - let dec = match RocDec::from_str(&float_str) { - Some(d) => d, - None => panic!("Invalid decimal for float literal = {}. TODO: Make this a nice, user-friendly error message", float_str), - }; - Stmt::Let( - assigned, - Expr::Literal(Literal::Decimal(dec.to_ne_bytes())), - Layout::Builtin(Builtin::Decimal), - hole, - ) - } - _ => unreachable!("unexpected float precision for integer"), - } - } - - Str(string) => Stmt::Let( - assigned, - Expr::Literal(Literal::Str(arena.alloc(string))), - Layout::Builtin(Builtin::Str), - hole, - ), - - SingleQuote(character) => Stmt::Let( - assigned, - Expr::Literal(Literal::Int((character as i128).to_ne_bytes())), - Layout::int_width(IntWidth::I32), - hole, - ), - - Num(var, num_str, num, _bound) => { - // first figure out what kind of number this is - match num_argument_to_int_or_float(env.subs, env.target_info, var, false) { - IntOrFloat::Int(precision) => Stmt::Let( - assigned, - Expr::Literal(match num { - IntValue::I128(n) => Literal::Int(n), - IntValue::U128(n) => Literal::U128(n), - }), - Layout::int_width(precision), - hole, - ), - IntOrFloat::Float(precision) => Stmt::Let( - assigned, - Expr::Literal(match num { - IntValue::I128(n) => Literal::Float(i128::from_ne_bytes(n) as f64), - IntValue::U128(n) => Literal::Float(u128::from_ne_bytes(n) as f64), - }), - Layout::float_width(precision), - hole, - ), - IntOrFloat::DecimalFloatType => { - let dec = match RocDec::from_str(&num_str) { - Some(d) => d, - None => panic!("Invalid decimal for float literal = {}. TODO: Make this a nice, user-friendly error message", num_str), - }; - Stmt::Let( - assigned, - Expr::Literal(Literal::Decimal(dec.to_ne_bytes())), - Layout::Builtin(Builtin::Decimal), - hole, - ) - } - } - } - LetNonRec(def, cont) => from_can_let( - env, - procs, - layout_cache, - def, - cont, - variable, - Some((assigned, hole)), - ), - LetRec(defs, cont, _cycle_mark) => { - // because Roc is strict, only functions can be recursive! - for def in defs.into_iter() { - if let roc_can::pattern::Pattern::Identifier(symbol) = &def.loc_pattern.value { - if let Closure(closure_data) = def.loc_expr.value { - register_noncapturing_closure(env, procs, *symbol, closure_data); - - continue; - } - } - unreachable!("recursive value does not have Identifier pattern") - } - - with_hole( - env, - cont.value, - variable, - procs, - layout_cache, - assigned, - hole, - ) - } - Var(mut symbol) => { - // If this symbol is a raw value, find the real name we gave to its specialized usage. - if let ReuseSymbol::Value(_symbol) = - can_reuse_symbol(env, procs, &roc_can::expr::Expr::Var(symbol), variable) - { - let real_symbol = - procs - .symbol_specializations - .get_or_insert(env, layout_cache, symbol, variable); - symbol = real_symbol; - } - - specialize_naked_symbol(env, variable, procs, layout_cache, assigned, hole, symbol) - } - AbilityMember(_member, specialization_id, _) => { - let specialization_symbol = env - .abilities_store - .get_resolved(specialization_id) - .expect("Specialization was never made!"); - - specialize_naked_symbol( - env, - variable, - procs, - layout_cache, - assigned, - hole, - specialization_symbol, - ) - } - Tag { - variant_var, - name: tag_name, - arguments: args, - .. - } => { - let arena = env.arena; - - debug_assert!(!matches!( - env.subs.get_content_without_compacting(variant_var), - Content::Structure(FlatType::Func(_, _, _)) - )); - convert_tag_union( - env, - variant_var, - assigned, - hole, - tag_name, - procs, - layout_cache, - args, - arena, - ) - } - - ZeroArgumentTag { - variant_var, - name: tag_name, - ext_var, - closure_name, - } => { - let arena = env.arena; - - let content = env.subs.get_content_without_compacting(variant_var); - - if let Content::Structure(FlatType::Func(arg_vars, _, ret_var)) = content { - let ret_var = *ret_var; - let arg_vars = *arg_vars; - - tag_union_to_function( - env, - arg_vars, - ret_var, - tag_name, - closure_name, - ext_var, - procs, - variant_var, - layout_cache, - assigned, - hole, - ) - } else { - convert_tag_union( - env, - variant_var, - assigned, - hole, - tag_name, - procs, - layout_cache, - std::vec::Vec::new(), - arena, - ) - } - } - - OpaqueRef { argument, .. } => { - let (arg_var, loc_arg_expr) = *argument; - - match can_reuse_symbol(env, procs, &loc_arg_expr.value, arg_var) { - // Opaques decay to their argument. - ReuseSymbol::Value(symbol) => { - let real_name = procs.symbol_specializations.get_or_insert( - env, - layout_cache, - symbol, - arg_var, - ); - let mut result = hole.clone(); - substitute_in_exprs(arena, &mut result, assigned, real_name); - result - } - _ => with_hole( - env, - loc_arg_expr.value, - arg_var, - procs, - layout_cache, - assigned, - hole, - ), - } - } - - Record { - record_var, - mut fields, - .. - } => { - let sorted_fields = match crate::layout::sort_record_fields( - env.arena, - record_var, - env.subs, - env.target_info, - ) { - Ok(fields) => fields, - Err(_) => return Stmt::RuntimeError("Can't create record with improper layout"), - }; - - let mut field_symbols = Vec::with_capacity_in(fields.len(), env.arena); - let mut can_fields = Vec::with_capacity_in(fields.len(), env.arena); - - #[allow(clippy::enum_variant_names)] - enum Field { - // TODO: rename this since it can handle unspecialized expressions now too - Function(Symbol, Variable), - ValueSymbol, - Field(roc_can::expr::Field), - } - - for (label, variable, _) in sorted_fields.into_iter() { - // TODO how should function pointers be handled here? - use ReuseSymbol::*; - match fields.remove(&label) { - Some(field) => { - match can_reuse_symbol(env, procs, &field.loc_expr.value, field.var) { - Imported(symbol) - | LocalFunction(symbol) - | UnspecializedExpr(symbol) => { - field_symbols.push(symbol); - can_fields.push(Field::Function(symbol, variable)); - } - Value(symbol) => { - let reusable = procs.symbol_specializations.get_or_insert( - env, - layout_cache, - symbol, - field.var, - ); - field_symbols.push(reusable); - can_fields.push(Field::ValueSymbol); - } - NotASymbol => { - field_symbols.push(env.unique_symbol()); - can_fields.push(Field::Field(field)); - } - } - } - None => { - // this field was optional, but not given - continue; - } - } - } - - // creating a record from the var will unpack it if it's just a single field. - let layout = layout_cache - .from_var(env.arena, record_var, env.subs) - .unwrap_or_else(|err| panic!("TODO turn fn_var into a RuntimeError {:?}", err)); - - let field_symbols = field_symbols.into_bump_slice(); - - let mut stmt = if let [only_field] = field_symbols { - let mut hole = hole.clone(); - substitute_in_exprs(env.arena, &mut hole, assigned, *only_field); - hole - } else { - Stmt::Let(assigned, Expr::Struct(field_symbols), layout, hole) - }; - - for (opt_field, symbol) in can_fields.into_iter().rev().zip(field_symbols.iter().rev()) - { - match opt_field { - Field::ValueSymbol => { - // this symbol is already defined; nothing to do - } - Field::Function(symbol, variable) => { - stmt = specialize_symbol( - env, - procs, - layout_cache, - Some(variable), - symbol, - stmt, - symbol, - ); - } - Field::Field(field) => { - stmt = with_hole( - env, - field.loc_expr.value, - field.var, - procs, - layout_cache, - *symbol, - env.arena.alloc(stmt), - ); - } - } - } - - stmt - } - - EmptyRecord => let_empty_struct(assigned, hole), - - Expect { .. } => unreachable!("I think this is unreachable"), - - If { - cond_var, - branch_var, - branches, - final_else, - } => { - match ( - layout_cache.from_var(env.arena, branch_var, env.subs), - layout_cache.from_var(env.arena, cond_var, env.subs), - ) { - (Ok(ret_layout), Ok(cond_layout)) => { - // if the hole is a return, then we don't need to merge the two - // branches together again, we can just immediately return - let is_terminated = matches!(hole, Stmt::Ret(_)); - - if is_terminated { - let terminator = hole; - - let mut stmt = with_hole( - env, - final_else.value, - branch_var, - procs, - layout_cache, - assigned, - terminator, - ); - - for (loc_cond, loc_then) in branches.into_iter().rev() { - let branching_symbol = env.unique_symbol(); - - let then = with_hole( - env, - loc_then.value, - branch_var, - procs, - layout_cache, - assigned, - terminator, - ); - - stmt = cond(env, branching_symbol, cond_layout, then, stmt, ret_layout); - - // add condition - stmt = with_hole( - env, - loc_cond.value, - cond_var, - procs, - layout_cache, - branching_symbol, - env.arena.alloc(stmt), - ); - } - stmt - } else { - let assigned_in_jump = env.unique_symbol(); - let id = JoinPointId(env.unique_symbol()); - - let terminator = env - .arena - .alloc(Stmt::Jump(id, env.arena.alloc([assigned_in_jump]))); - - let mut stmt = with_hole( - env, - final_else.value, - branch_var, - procs, - layout_cache, - assigned_in_jump, - terminator, - ); - - for (loc_cond, loc_then) in branches.into_iter().rev() { - let branching_symbol = possible_reuse_symbol_or_specialize( - env, - procs, - layout_cache, - &loc_cond.value, - cond_var, - ); - - let then = with_hole( - env, - loc_then.value, - branch_var, - procs, - layout_cache, - assigned_in_jump, - terminator, - ); - - stmt = cond(env, branching_symbol, cond_layout, then, stmt, ret_layout); - - // add condition - stmt = assign_to_symbol( - env, - procs, - layout_cache, - cond_var, - loc_cond, - branching_symbol, - stmt, - ); - } - - let layout = layout_cache - .from_var(env.arena, branch_var, env.subs) - .unwrap_or_else(|err| { - panic!("TODO turn fn_var into a RuntimeError {:?}", err) - }); - - let param = Param { - symbol: assigned, - layout, - borrow: false, - }; - - Stmt::Join { - id, - parameters: env.arena.alloc([param]), - remainder: env.arena.alloc(stmt), - body: hole, - } - } - } - (Err(_), _) => Stmt::RuntimeError("invalid ret_layout"), - (_, Err(_)) => Stmt::RuntimeError("invalid cond_layout"), - } - } - - When { - cond_var, - expr_var, - region: _, - loc_cond, - branches, - branches_cond_var: _, - exhaustive, - } => { - let cond_symbol = possible_reuse_symbol_or_specialize( - env, - procs, - layout_cache, - &loc_cond.value, - cond_var, - ); - - let id = JoinPointId(env.unique_symbol()); - - let mut stmt = from_can_when( - env, - cond_var, - expr_var, - cond_symbol, - branches, - exhaustive, - layout_cache, - procs, - Some(id), - ); - - // define the `when` condition - stmt = assign_to_symbol( - env, - procs, - layout_cache, - cond_var, - *loc_cond, - cond_symbol, - stmt, - ); - - let layout = layout_cache - .from_var(env.arena, expr_var, env.subs) - .unwrap_or_else(|err| panic!("TODO turn fn_var into a RuntimeError {:?}", err)); - - let param = Param { - symbol: assigned, - layout, - borrow: false, - }; - - Stmt::Join { - id, - parameters: env.arena.alloc([param]), - remainder: env.arena.alloc(stmt), - body: env.arena.alloc(hole), - } - } - - List { - loc_elems, - elem_var, - .. - } if loc_elems.is_empty() => { - // because an empty list has an unknown element type, it is handled differently - let opt_elem_layout = layout_cache.from_var(env.arena, elem_var, env.subs); - - match opt_elem_layout { - Ok(elem_layout) => { - let expr = Expr::EmptyArray; - Stmt::Let( - assigned, - expr, - Layout::Builtin(Builtin::List(env.arena.alloc(elem_layout))), - hole, - ) - } - Err(LayoutProblem::UnresolvedTypeVar(_)) => { - let expr = Expr::EmptyArray; - Stmt::Let( - assigned, - expr, - Layout::Builtin(Builtin::List(&Layout::VOID)), - hole, - ) - } - Err(LayoutProblem::Erroneous) => panic!("list element is error type"), - } - } - - List { - elem_var, - loc_elems, - } => { - let mut arg_symbols = Vec::with_capacity_in(loc_elems.len(), env.arena); - let mut elements = Vec::with_capacity_in(loc_elems.len(), env.arena); - - let mut symbol_exprs = Vec::with_capacity_in(loc_elems.len(), env.arena); - - for arg_expr in loc_elems.into_iter() { - if let Some(literal) = try_make_literal(env, &arg_expr.value) { - elements.push(ListLiteralElement::Literal(literal)); - } else { - let symbol = possible_reuse_symbol_or_specialize( - env, - procs, - layout_cache, - &arg_expr.value, - elem_var, - ); - - elements.push(ListLiteralElement::Symbol(symbol)); - arg_symbols.push(symbol); - symbol_exprs.push(arg_expr); - } - } - let arg_symbols = arg_symbols.into_bump_slice(); - - let elem_layout = layout_cache - .from_var(env.arena, elem_var, env.subs) - .unwrap_or_else(|err| panic!("TODO turn fn_var into a RuntimeError {:?}", err)); - - let expr = Expr::Array { - elem_layout, - elems: elements.into_bump_slice(), - }; - - let stmt = Stmt::Let( - assigned, - expr, - Layout::Builtin(Builtin::List(env.arena.alloc(elem_layout))), - hole, - ); - - let iter = symbol_exprs - .into_iter() - .rev() - .map(|e| (elem_var, e)) - .zip(arg_symbols.iter().rev()); - - assign_to_symbols(env, procs, layout_cache, iter, stmt) - } - - Access { - record_var, - field_var, - field, - loc_expr, - .. - } => { - let sorted_fields = match crate::layout::sort_record_fields( - env.arena, - record_var, - env.subs, - env.target_info, - ) { - Ok(fields) => fields, - Err(_) => return Stmt::RuntimeError("Can't access record with improper layout"), - }; - - let mut index = None; - let mut field_layouts = Vec::with_capacity_in(sorted_fields.len(), env.arena); - - let mut current = 0; - for (label, _, opt_field_layout) in sorted_fields.into_iter() { - match opt_field_layout { - Err(_) => { - // this was an optional field, and now does not exist! - // do not increment `current`! - } - Ok(field_layout) => { - field_layouts.push(field_layout); - - if label == field { - index = Some(current); - } - - current += 1; - } - } - } - - let record_symbol = possible_reuse_symbol_or_specialize( - env, - procs, - layout_cache, - &loc_expr.value, - record_var, - ); - - let mut stmt = match field_layouts.as_slice() { - [_] => { - let mut hole = hole.clone(); - substitute_in_exprs(env.arena, &mut hole, assigned, record_symbol); - - hole - } - _ => { - let expr = Expr::StructAtIndex { - index: index.expect("field not in its own type") as u64, - field_layouts: field_layouts.into_bump_slice(), - structure: record_symbol, - }; - - let layout = layout_cache - .from_var(env.arena, field_var, env.subs) - .unwrap_or_else(|err| { - panic!("TODO turn fn_var into a RuntimeError {:?}", err) - }); - - Stmt::Let(assigned, expr, layout, hole) - } - }; - - stmt = assign_to_symbol( - env, - procs, - layout_cache, - record_var, - *loc_expr, - record_symbol, - stmt, - ); - - stmt - } - - Accessor(accessor_data) => { - let field_var = accessor_data.field_var; - let fresh_record_symbol = env.unique_symbol(); - - let ClosureData { - name, - function_type, - arguments, - loc_body, - .. - } = accessor_data.to_closure_data(fresh_record_symbol); - - match procs.insert_anonymous( - env, - name, - function_type, - arguments, - *loc_body, - CapturedSymbols::None, - field_var, - layout_cache, - ) { - Ok(_) => { - let raw_layout = return_on_layout_error!( - env, - layout_cache.raw_from_var(env.arena, function_type, env.subs) - ); - - match raw_layout { - RawFunctionLayout::Function(_, lambda_set, _) => { - construct_closure_data(env, lambda_set, name, &[], assigned, hole) - } - RawFunctionLayout::ZeroArgumentThunk(_) => unreachable!(), - } - } - - Err(_error) => Stmt::RuntimeError( - "TODO convert anonymous function error to a RuntimeError string", - ), - } - } - - Update { - record_var, - symbol: structure, - updates, - .. - } => { - use FieldType::*; - - enum FieldType<'a> { - CopyExisting(u64), - UpdateExisting(&'a roc_can::expr::Field), - } - - // Strategy: turn a record update into the creation of a new record. - // This has the benefit that we don't need to do anything special for reference - // counting - - let sorted_fields = match crate::layout::sort_record_fields( - env.arena, - record_var, - env.subs, - env.target_info, - ) { - Ok(fields) => fields, - Err(_) => return Stmt::RuntimeError("Can't update record with improper layout"), - }; - - let mut field_layouts = Vec::with_capacity_in(sorted_fields.len(), env.arena); - - let mut symbols = Vec::with_capacity_in(sorted_fields.len(), env.arena); - let mut fields = Vec::with_capacity_in(sorted_fields.len(), env.arena); - - let mut current = 0; - for (label, _, opt_field_layout) in sorted_fields.into_iter() { - match opt_field_layout { - Err(_) => { - debug_assert!(!updates.contains_key(&label)); - // this was an optional field, and now does not exist! - // do not increment `current`! - } - Ok(field_layout) => { - field_layouts.push(field_layout); - - if let Some(field) = updates.get(&label) { - let field_symbol = possible_reuse_symbol_or_specialize( - env, - procs, - layout_cache, - &field.loc_expr.value, - field.var, - ); - - fields.push(UpdateExisting(field)); - symbols.push(field_symbol); - } else { - fields.push(CopyExisting(current)); - symbols.push(env.unique_symbol()); - } - - current += 1; - } - } - } - let symbols = symbols.into_bump_slice(); - - let record_layout = layout_cache - .from_var(env.arena, record_var, env.subs) - .unwrap_or_else(|err| panic!("TODO turn fn_var into a RuntimeError {:?}", err)); - - let field_layouts = match &record_layout { - Layout::Struct { field_layouts, .. } => *field_layouts, - other => arena.alloc([*other]), - }; - - debug_assert_eq!(field_layouts.len(), symbols.len()); - debug_assert_eq!(fields.len(), symbols.len()); - - if symbols.len() == 1 { - // TODO we can probably special-case this more, skippiing the generation of - // UpdateExisting - let mut stmt = hole.clone(); - - let what_to_do = &fields[0]; - - match what_to_do { - UpdateExisting(field) => { - substitute_in_exprs(env.arena, &mut stmt, assigned, symbols[0]); - - stmt = assign_to_symbol( - env, - procs, - layout_cache, - field.var, - *field.loc_expr.clone(), - symbols[0], - stmt, - ); - } - CopyExisting(_) => { - unreachable!( - r"when a record has just one field and is updated, it must update that one field" - ); - } - } - - stmt - } else { - let expr = Expr::Struct(symbols); - let mut stmt = Stmt::Let(assigned, expr, record_layout, hole); - - let it = field_layouts.iter().zip(symbols.iter()).zip(fields); - - for ((field_layout, symbol), what_to_do) in it { - match what_to_do { - UpdateExisting(field) => { - stmt = assign_to_symbol( - env, - procs, - layout_cache, - field.var, - *field.loc_expr.clone(), - *symbol, - stmt, - ); - } - CopyExisting(index) => { - let record_needs_specialization = - procs.ability_member_aliases.get(structure).is_some(); - let specialized_structure_sym = if record_needs_specialization { - // We need to specialize the record now; create a new one for it. - // TODO: reuse this symbol for all updates - env.unique_symbol() - } else { - // The record is already good. - structure - }; - - let access_expr = Expr::StructAtIndex { - structure: specialized_structure_sym, - index, - field_layouts, - }; - stmt = - Stmt::Let(*symbol, access_expr, *field_layout, arena.alloc(stmt)); - - if record_needs_specialization { - stmt = specialize_symbol( - env, - procs, - layout_cache, - Some(record_var), - specialized_structure_sym, - stmt, - structure, - ); - } - } - } - } - stmt - } - } - - Closure(ClosureData { - function_type, - return_type, - name, - arguments, - captured_symbols, - loc_body: boxed_body, - .. - }) => { - let loc_body = *boxed_body; - - let raw = layout_cache.raw_from_var(env.arena, function_type, env.subs); - - match return_on_layout_error!(env, raw) { - RawFunctionLayout::ZeroArgumentThunk(_) => { - unreachable!("a closure syntactically always must have at least one argument") - } - RawFunctionLayout::Function(_argument_layouts, lambda_set, _ret_layout) => { - let mut captured_symbols = Vec::from_iter_in(captured_symbols, env.arena); - captured_symbols.sort(); - let captured_symbols = captured_symbols.into_bump_slice(); - - let inserted = procs.insert_anonymous( - env, - name, - function_type, - arguments, - loc_body, - CapturedSymbols::Captured(captured_symbols), - return_type, - layout_cache, - ); - - if let Err(runtime_error) = inserted { - return Stmt::RuntimeError(env.arena.alloc(format!( - "RuntimeError {} line {} {:?}", - file!(), - line!(), - runtime_error, - ))); - } else { - drop(inserted); - } - - // define the closure data - - let symbols = - Vec::from_iter_in(captured_symbols.iter(), env.arena).into_bump_slice(); - - construct_closure_data( - env, - lambda_set, - name, - symbols.iter().copied(), - assigned, - hole, - ) - } - } - } - - Call(boxed, loc_args, _) => { - let (fn_var, loc_expr, _lambda_set_var, _ret_var) = *boxed; - - // even if a call looks like it's by name, it may in fact be by-pointer. - // E.g. in `(\f, x -> f x)` the call is in fact by pointer. - // So we check the function name against the list of partial procedures, - // the procedures that we have lifted to the top-level and can call by name - // if it's in there, it's a call by name, otherwise it's a call by pointer - let is_known = |key| { - // a proc in this module, or an imported symbol - procs.partial_procs.contains_key(key) - || (env.is_imported_symbol(key) && !procs.is_imported_module_thunk(key)) - }; - - match loc_expr.value { - roc_can::expr::Expr::Var(proc_name) if is_known(proc_name) => { - // a call by a known name - call_by_name( - env, - procs, - fn_var, - proc_name, - loc_args, - layout_cache, - assigned, - hole, - ) - } - roc_can::expr::Expr::AbilityMember(member, specialization_id, _) => { - let specialization_proc_name = - late_resolve_ability_specialization(env, member, specialization_id, fn_var); - - call_by_name( - env, - procs, - fn_var, - specialization_proc_name, - loc_args, - layout_cache, - assigned, - hole, - ) - } - _ => { - // Call by pointer - the closure was anonymous, e.g. - // - // ((\a -> a) 5) - // - // It might even be the anonymous result of a conditional: - // - // ((if x > 0 then \a -> a else \_ -> 0) 5) - // - // It could be named too: - // - // ((if x > 0 then foo else bar) 5) - // - // also this occurs for functions passed in as arguments, e.g. - // - // (\f, x -> f x) - - let arg_symbols = Vec::from_iter_in( - loc_args.iter().map(|(var, arg_expr)| { - possible_reuse_symbol_or_specialize( - env, - procs, - layout_cache, - &arg_expr.value, - *var, - ) - }), - arena, - ) - .into_bump_slice(); - - let full_layout = return_on_layout_error!( - env, - layout_cache.raw_from_var(env.arena, fn_var, env.subs) - ); - - // if the function expression (loc_expr) is already a symbol, - // re-use that symbol, and don't define its value again - let mut result; - use ReuseSymbol::*; - match can_reuse_symbol(env, procs, &loc_expr.value, fn_var) { - LocalFunction(_) => { - unreachable!("if this was known to be a function, we would not be here") - } - Imported(thunk_name) => { - debug_assert!(procs.is_imported_module_thunk(thunk_name)); - - add_needed_external(procs, env, fn_var, thunk_name); - - let function_symbol = env.unique_symbol(); - - match full_layout { - RawFunctionLayout::Function( - arg_layouts, - lambda_set, - ret_layout, - ) => { - let closure_data_symbol = function_symbol; - - result = match_on_lambda_set( - env, - lambda_set, - closure_data_symbol, - arg_symbols, - arg_layouts, - ret_layout, - assigned, - hole, - ); - - result = force_thunk( - env, - thunk_name, - Layout::LambdaSet(lambda_set), - function_symbol, - env.arena.alloc(result), - ); - } - RawFunctionLayout::ZeroArgumentThunk(_) => { - unreachable!("calling a non-closure layout") - } - } - } - Value(function_symbol) => { - let function_symbol = procs.symbol_specializations.get_or_insert( - env, - layout_cache, - function_symbol, - fn_var, - ); - - match full_layout { - RawFunctionLayout::Function( - arg_layouts, - lambda_set, - ret_layout, - ) => { - let closure_data_symbol = function_symbol; - - result = match_on_lambda_set( - env, - lambda_set, - closure_data_symbol, - arg_symbols, - arg_layouts, - ret_layout, - assigned, - hole, - ); - } - RawFunctionLayout::ZeroArgumentThunk(_) => { - unreachable!("calling a non-closure layout") - } - } - } - UnspecializedExpr(symbol) => { - match procs.ability_member_aliases.get(symbol).unwrap() { - &self::AbilityMember(member) => { - let resolved_proc = resolve_ability_specialization(env.subs, env.abilities_store, member, fn_var).expect("Recorded as an ability member, but it doesn't have a specialization"); - - let resolved_proc = match resolved_proc { - Resolved::Specialization(symbol) => symbol, - Resolved::NeedsGenerated => { - todo_abilities!("Generate impls for structural types") - } - }; - - // a call by a known name - return call_by_name( - env, - procs, - fn_var, - resolved_proc, - loc_args, - layout_cache, - assigned, - hole, - ); - } - } - } - NotASymbol => { - // the expression is not a symbol. That means it's an expression - // evaluating to a function value. - - match full_layout { - RawFunctionLayout::Function( - arg_layouts, - lambda_set, - ret_layout, - ) => { - let closure_data_symbol = env.unique_symbol(); - - result = match_on_lambda_set( - env, - lambda_set, - closure_data_symbol, - arg_symbols, - arg_layouts, - ret_layout, - assigned, - hole, - ); - - result = with_hole( - env, - loc_expr.value, - fn_var, - procs, - layout_cache, - closure_data_symbol, - env.arena.alloc(result), - ); - } - RawFunctionLayout::ZeroArgumentThunk(_) => { - unreachable!( - "{:?} cannot be called in the source language", - full_layout - ) - } - } - } - } - let iter = loc_args.into_iter().rev().zip(arg_symbols.iter().rev()); - assign_to_symbols(env, procs, layout_cache, iter, result) - } - } - } - - ForeignCall { - foreign_symbol, - args, - ret_var, - } => { - let mut arg_symbols = Vec::with_capacity_in(args.len(), env.arena); - - for (var, arg_expr) in args.iter() { - arg_symbols.push(possible_reuse_symbol_or_specialize( - env, - procs, - layout_cache, - arg_expr, - *var, - )); - } - let arg_symbols = arg_symbols.into_bump_slice(); - - // layout of the return type - let layout = - return_on_layout_error!(env, layout_cache.from_var(env.arena, ret_var, env.subs)); - - let call = self::Call { - call_type: CallType::Foreign { - foreign_symbol, - ret_layout: env.arena.alloc(layout), - }, - arguments: arg_symbols, - }; - - let result = build_call(env, call, assigned, layout, hole); - - let iter = args - .into_iter() - .rev() - .map(|(a, b)| (a, Loc::at_zero(b))) - .zip(arg_symbols.iter().rev()); - assign_to_symbols(env, procs, layout_cache, iter, result) - } - - RunLowLevel { op, args, ret_var } => { - let mut arg_symbols = Vec::with_capacity_in(args.len(), env.arena); - - for (var, arg_expr) in args.iter() { - arg_symbols.push(possible_reuse_symbol_or_specialize( - env, - procs, - layout_cache, - arg_expr, - *var, - )); - } - let arg_symbols = arg_symbols.into_bump_slice(); - - // layout of the return type - let layout = - return_on_layout_error!(env, layout_cache.from_var(env.arena, ret_var, env.subs)); - - macro_rules! match_on_closure_argument { - ( $ho:ident, [$($x:ident),* $(,)?]) => {{ - let closure_index = op.function_argument_position(); - let closure_data_symbol = arg_symbols[closure_index]; - let closure_data_var = args[closure_index].0; - - let closure_data_layout = return_on_layout_error!( - env, - layout_cache.raw_from_var(env.arena, closure_data_var, env.subs) - ); - - let top_level = ProcLayout::from_raw(env.arena, closure_data_layout); - - let arena = env.arena; - - let arg_layouts = top_level.arguments; - let ret_layout = top_level.result; - - match closure_data_layout { - RawFunctionLayout::Function(_, lambda_set, _) => { - lowlevel_match_on_lambda_set( - env, - lambda_set, - op, - closure_data_symbol, - |(top_level_function, closure_data, closure_env_layout, specialization_id, update_mode)| { - let passed_function = PassedFunction { - name: top_level_function, - captured_environment: closure_data_symbol, - owns_captured_environment: false, - specialization_id, - argument_layouts: arg_layouts, - return_layout: ret_layout, - }; - - - let higher_order = HigherOrderLowLevel { - op: crate::low_level::HigherOrder::$ho { $($x,)* }, - closure_env_layout, - update_mode, - passed_function, - }; - - self::Call { - call_type: CallType::HigherOrder(arena.alloc(higher_order)), - arguments: arena.alloc([$($x,)* top_level_function, closure_data]), - } - }, - layout, - assigned, - hole, - ) - } - RawFunctionLayout::ZeroArgumentThunk(_) => unreachable!("match_on_closure_argument received a zero-argument thunk"), - } - }}; - } - - macro_rules! walk { - ($oh:ident) => {{ - debug_assert_eq!(arg_symbols.len(), 3); - - const LIST_INDEX: usize = 0; - const DEFAULT_INDEX: usize = 1; - const CLOSURE_INDEX: usize = 2; - - let xs = arg_symbols[LIST_INDEX]; - let state = arg_symbols[DEFAULT_INDEX]; - - let stmt = match_on_closure_argument!($oh, [xs, state]); - - // because of a hack to implement List.product and List.sum, we need to also - // assign to symbols here. Normally the arguments to a lowlevel function are - // all symbols anyway, but because of this hack the closure symbol can be an - // actual closure, and the default is either the number 1 or 0 - // this can be removed when we define builtin modules as proper modules - - let stmt = assign_to_symbol( - env, - procs, - layout_cache, - args[LIST_INDEX].0, - Loc::at_zero(args[LIST_INDEX].1.clone()), - arg_symbols[LIST_INDEX], - stmt, - ); - - let stmt = assign_to_symbol( - env, - procs, - layout_cache, - args[DEFAULT_INDEX].0, - Loc::at_zero(args[DEFAULT_INDEX].1.clone()), - arg_symbols[DEFAULT_INDEX], - stmt, - ); - - assign_to_symbol( - env, - procs, - layout_cache, - args[CLOSURE_INDEX].0, - Loc::at_zero(args[CLOSURE_INDEX].1.clone()), - arg_symbols[CLOSURE_INDEX], - stmt, - ) - }}; - } - - use LowLevel::*; - match op { - ListMap => { - debug_assert_eq!(arg_symbols.len(), 2); - let xs = arg_symbols[0]; - match_on_closure_argument!(ListMap, [xs]) - } - - ListMapWithIndex => { - debug_assert_eq!(arg_symbols.len(), 2); - let xs = arg_symbols[0]; - match_on_closure_argument!(ListMapWithIndex, [xs]) - } - ListKeepIf => { - debug_assert_eq!(arg_symbols.len(), 2); - let xs = arg_symbols[0]; - let stmt = match_on_closure_argument!(ListKeepIf, [xs]); - - // See the comment in `walk!`. We use List.keepIf to implement - // other builtins, where the closure can be an actual closure rather - // than a symbol. - assign_to_symbol( - env, - procs, - layout_cache, - args[1].0, // the closure - Loc::at_zero(args[1].1.clone()), - arg_symbols[1], - stmt, - ) - } - ListAny => { - debug_assert_eq!(arg_symbols.len(), 2); - let xs = arg_symbols[0]; - match_on_closure_argument!(ListAny, [xs]) - } - ListAll => { - debug_assert_eq!(arg_symbols.len(), 2); - let xs = arg_symbols[0]; - match_on_closure_argument!(ListAll, [xs]) - } - - ListKeepOks => { - debug_assert_eq!(arg_symbols.len(), 2); - let xs = arg_symbols[0]; - match_on_closure_argument!(ListKeepOks, [xs]) - } - ListKeepErrs => { - debug_assert_eq!(arg_symbols.len(), 2); - let xs = arg_symbols[0]; - match_on_closure_argument!(ListKeepErrs, [xs]) - } - ListSortWith => { - debug_assert_eq!(arg_symbols.len(), 2); - let xs = arg_symbols[0]; - match_on_closure_argument!(ListSortWith, [xs]) - } - ListWalk => walk!(ListWalk), - ListWalkUntil => walk!(ListWalkUntil), - ListWalkBackwards => walk!(ListWalkBackwards), - DictWalk => walk!(DictWalk), - ListMap2 => { - debug_assert_eq!(arg_symbols.len(), 3); - - let xs = arg_symbols[0]; - let ys = arg_symbols[1]; - - match_on_closure_argument!(ListMap2, [xs, ys]) - } - ListMap3 => { - debug_assert_eq!(arg_symbols.len(), 4); - - let xs = arg_symbols[0]; - let ys = arg_symbols[1]; - let zs = arg_symbols[2]; - - match_on_closure_argument!(ListMap3, [xs, ys, zs]) - } - ListMap4 => { - debug_assert_eq!(arg_symbols.len(), 5); - - let xs = arg_symbols[0]; - let ys = arg_symbols[1]; - let zs = arg_symbols[2]; - let ws = arg_symbols[3]; - - match_on_closure_argument!(ListMap4, [xs, ys, zs, ws]) - } - ListFindUnsafe => { - debug_assert_eq!(arg_symbols.len(), 2); - let xs = arg_symbols[0]; - match_on_closure_argument!(ListFindUnsafe, [xs]) - } - BoxExpr => { - debug_assert_eq!(arg_symbols.len(), 1); - let x = arg_symbols[0]; - - Stmt::Let(assigned, Expr::ExprBox { symbol: x }, layout, hole) - } - UnboxExpr => { - debug_assert_eq!(arg_symbols.len(), 1); - let x = arg_symbols[0]; - - Stmt::Let(assigned, Expr::ExprUnbox { symbol: x }, layout, hole) - } - _ => { - let call = self::Call { - call_type: CallType::LowLevel { - op, - update_mode: env.next_update_mode_id(), - }, - arguments: arg_symbols, - }; - - let result = build_call(env, call, assigned, layout, hole); - - let iter = args - .into_iter() - .rev() - .map(|(a, b)| (a, Loc::at_zero(b))) - .zip(arg_symbols.iter().rev()); - assign_to_symbols(env, procs, layout_cache, iter, result) - } - } - } - TypedHole(_) => Stmt::RuntimeError("Hit a blank"), - RuntimeError(e) => Stmt::RuntimeError(env.arena.alloc(format!("{:?}", e))), - } -} - -#[inline(always)] -fn late_resolve_ability_specialization<'a>( - env: &mut Env<'a, '_>, - member: Symbol, - specialization_id: SpecializationId, - specialization_var: Variable, -) -> Symbol { - if let Some(spec_symbol) = env.abilities_store.get_resolved(specialization_id) { - // Fast path: specialization is monomorphic, was found during solving. - spec_symbol - } else if let Content::Structure(FlatType::Func(_, lambda_set, _)) = - env.subs.get_content_without_compacting(specialization_var) - { - // Fast path: the member is a function, so the lambda set will tell us the - // specialization. - use roc_types::subs::LambdaSet; - let LambdaSet { - solved, - unspecialized, - recursion_var: _, - } = env.subs.get_lambda_set(*lambda_set); - debug_assert!(unspecialized.is_empty()); - let mut iter_lambda_set = solved.iter_all(); - debug_assert_eq!(iter_lambda_set.len(), 1); - let spec_symbol_index = iter_lambda_set.next().unwrap().0; - env.subs[spec_symbol_index] - } else { - // Otherwise, resolve by checking the able var. - let specialization = resolve_ability_specialization( - env.subs, - env.abilities_store, - member, - specialization_var, - ) - .expect("Ability specialization is unknown - code generation cannot proceed!"); - - match specialization { - Resolved::Specialization(symbol) => symbol, - Resolved::NeedsGenerated => { - todo_abilities!("Generate impls for structural types") - } - } - } -} - -#[allow(clippy::too_many_arguments)] -fn construct_closure_data<'a, I>( - env: &mut Env<'a, '_>, - // procs: &mut Procs<'a>, - lambda_set: LambdaSet<'a>, - name: Symbol, - symbols: I, - assigned: Symbol, - hole: &'a Stmt<'a>, -) -> Stmt<'a> -where - I: IntoIterator, - I::IntoIter: ExactSizeIterator, -{ - let lambda_set_layout = Layout::LambdaSet(lambda_set); - let symbols = symbols.into_iter(); - - let result = match lambda_set.layout_for_member(name) { - ClosureRepresentation::Union { - tag_id, - alphabetic_order_fields: field_layouts, - closure_name: tag_name, - union_layout, - } => { - // captured variables are in symbol-alphabetic order, but now we want - // them ordered by their alignment requirements - let mut combined = Vec::with_capacity_in(symbols.len(), env.arena); - for ((symbol, _variable), layout) in symbols.zip(field_layouts.iter()) { - combined.push((*symbol, layout)) - } - - let ptr_bytes = env.target_info; - - combined.sort_by(|(_, layout1), (_, layout2)| { - let size1 = layout1.alignment_bytes(ptr_bytes); - let size2 = layout2.alignment_bytes(ptr_bytes); - - size2.cmp(&size1) - }); - - let symbols = - Vec::from_iter_in(combined.iter().map(|(a, _)| *a), env.arena).into_bump_slice(); - - let expr = Expr::Tag { - tag_id, - tag_layout: union_layout, - tag_name: tag_name.into(), - arguments: symbols, - }; - - Stmt::Let(assigned, expr, lambda_set_layout, env.arena.alloc(hole)) - } - ClosureRepresentation::AlphabeticOrderStruct(field_layouts) => { - debug_assert_eq!(field_layouts.len(), symbols.len()); - - // captured variables are in symbol-alphabetic order, but now we want - // them ordered by their alignment requirements - let mut combined = Vec::with_capacity_in(symbols.len(), env.arena); - for ((symbol, _variable), layout) in symbols.zip(field_layouts.iter()) { - combined.push((*symbol, layout)) - } - - let ptr_bytes = env.target_info; - - combined.sort_by(|(_, layout1), (_, layout2)| { - let size1 = layout1.alignment_bytes(ptr_bytes); - let size2 = layout2.alignment_bytes(ptr_bytes); - - size2.cmp(&size1) - }); - - let symbols = - Vec::from_iter_in(combined.iter().map(|(a, _)| *a), env.arena).into_bump_slice(); - let field_layouts = - Vec::from_iter_in(combined.iter().map(|(_, b)| **b), env.arena).into_bump_slice(); - - debug_assert_eq!( - Layout::struct_no_name_order(field_layouts), - lambda_set.runtime_representation() - ); - - let expr = Expr::Struct(symbols); - - Stmt::Let(assigned, expr, lambda_set_layout, hole) - } - ClosureRepresentation::Other(Layout::Builtin(Builtin::Bool)) => { - debug_assert_eq!(symbols.len(), 0); - - debug_assert_eq!(lambda_set.set.len(), 2); - let tag_id = name != lambda_set.set[0].0; - let expr = Expr::Literal(Literal::Bool(tag_id)); - - Stmt::Let(assigned, expr, lambda_set_layout, hole) - } - ClosureRepresentation::Other(Layout::Builtin(Builtin::Int(IntWidth::U8))) => { - debug_assert_eq!(symbols.len(), 0); - - debug_assert!(lambda_set.set.len() > 2); - let tag_id = lambda_set.set.iter().position(|(s, _)| *s == name).unwrap() as u8; - let expr = Expr::Literal(Literal::Byte(tag_id)); - - Stmt::Let(assigned, expr, lambda_set_layout, hole) - } - _ => unreachable!(), - }; - - result -} - -#[allow(clippy::too_many_arguments)] -fn convert_tag_union<'a>( - env: &mut Env<'a, '_>, - variant_var: Variable, - assigned: Symbol, - hole: &'a Stmt<'a>, - tag_name: TagName, - procs: &mut Procs<'a>, - layout_cache: &mut LayoutCache<'a>, - args: std::vec::Vec<(Variable, Loc)>, - arena: &'a Bump, -) -> Stmt<'a> { - use crate::layout::UnionVariant::*; - let res_variant = - crate::layout::union_sorted_tags(env.arena, variant_var, env.subs, env.target_info); - let variant = match res_variant { - Ok(cached) => cached, - Err(LayoutProblem::UnresolvedTypeVar(_)) => { - return Stmt::RuntimeError(env.arena.alloc(format!( - "UnresolvedTypeVar {} line {}", - file!(), - line!() - ))) - } - Err(LayoutProblem::Erroneous) => { - return Stmt::RuntimeError(env.arena.alloc(format!( - "Erroneous {} line {}", - file!(), - line!() - ))); - } - }; - - match variant { - Never => unreachable!( - "The `[]` type has no constructors, source var {:?}", - variant_var - ), - Unit | UnitWithArguments => Stmt::Let(assigned, Expr::Struct(&[]), Layout::UNIT, hole), - BoolUnion { ttrue, .. } => Stmt::Let( - assigned, - Expr::Literal(Literal::Bool(&tag_name == ttrue.expect_tag_ref())), - Layout::Builtin(Builtin::Bool), - hole, - ), - ByteUnion(tag_names) => { - let opt_tag_id = tag_names - .iter() - .position(|key| key.expect_tag_ref() == &tag_name); - - match opt_tag_id { - Some(tag_id) => Stmt::Let( - assigned, - Expr::Literal(Literal::Byte(tag_id as u8)), - Layout::Builtin(Builtin::Int(IntWidth::U8)), - hole, - ), - None => Stmt::RuntimeError("tag must be in its own type"), - } - } - - Newtype { - arguments: field_layouts, - .. - } => { - let field_symbols_temp = sorted_field_symbols(env, procs, layout_cache, args); - - let mut field_symbols = Vec::with_capacity_in(field_layouts.len(), env.arena); - field_symbols.extend(field_symbols_temp.iter().map(|r| r.1)); - let field_symbols = field_symbols.into_bump_slice(); - - // Layout will unpack this unwrapped tack if it only has one (non-zero-sized) field - let layout = layout_cache - .from_var(env.arena, variant_var, env.subs) - .unwrap_or_else(|err| panic!("TODO turn fn_var into a RuntimeError {:?}", err)); - - // even though this was originally a Tag, we treat it as a Struct from now on - let stmt = if let [only_field] = field_symbols { - let mut hole = hole.clone(); - substitute_in_exprs(env.arena, &mut hole, assigned, *only_field); - hole - } else { - Stmt::Let(assigned, Expr::Struct(field_symbols), layout, hole) - }; - - let iter = field_symbols_temp.into_iter().map(|(_, _, data)| data); - assign_to_symbols(env, procs, layout_cache, iter, stmt) - } - Wrapped(variant) => { - let (tag_id, _) = variant.tag_name_to_id(&tag_name); - - let field_symbols_temp = sorted_field_symbols(env, procs, layout_cache, args); - - let field_symbols; - - // we must derive the union layout from the whole_var, building it up - // from `layouts` would unroll recursive tag unions, and that leads to - // problems down the line because we hash layouts and an unrolled - // version is not the same as the minimal version. - let union_layout = match return_on_layout_error!( - env, - layout_cache.from_var(env.arena, variant_var, env.subs) - ) { - Layout::Union(ul) => ul, - _ => unreachable!(), - }; - - use WrappedVariant::*; - let (tag, union_layout) = match variant { - Recursive { sorted_tag_layouts } => { - debug_assert!(sorted_tag_layouts.len() > 1); - - field_symbols = { - let mut temp = Vec::with_capacity_in(field_symbols_temp.len() + 1, arena); - - temp.extend(field_symbols_temp.iter().map(|r| r.1)); - - temp.into_bump_slice() - }; - - let mut layouts: Vec<&'a [Layout<'a>]> = - Vec::with_capacity_in(sorted_tag_layouts.len(), env.arena); - - for (_, arg_layouts) in sorted_tag_layouts.into_iter() { - layouts.push(arg_layouts); - } - - let tag = Expr::Tag { - tag_layout: union_layout, - tag_name: tag_name.into(), - tag_id: tag_id as _, - arguments: field_symbols, - }; - - (tag, union_layout) - } - NonNullableUnwrapped { - tag_name: wrapped_tag_name, - .. - } => { - debug_assert_eq!(TagOrClosure::Tag(tag_name.clone()), wrapped_tag_name); - - field_symbols = { - let mut temp = Vec::with_capacity_in(field_symbols_temp.len(), arena); - - temp.extend(field_symbols_temp.iter().map(|r| r.1)); - - temp.into_bump_slice() - }; - - let tag = Expr::Tag { - tag_layout: union_layout, - tag_name: tag_name.into(), - tag_id: tag_id as _, - arguments: field_symbols, - }; - - (tag, union_layout) - } - NonRecursive { sorted_tag_layouts } => { - field_symbols = { - let mut temp = Vec::with_capacity_in(field_symbols_temp.len(), arena); - - temp.extend(field_symbols_temp.iter().map(|r| r.1)); - - temp.into_bump_slice() - }; - - let mut layouts: Vec<&'a [Layout<'a>]> = - Vec::with_capacity_in(sorted_tag_layouts.len(), env.arena); - - for (_, arg_layouts) in sorted_tag_layouts.into_iter() { - layouts.push(arg_layouts); - } - - let tag = Expr::Tag { - tag_layout: union_layout, - tag_name: tag_name.into(), - tag_id: tag_id as _, - arguments: field_symbols, - }; - - (tag, union_layout) - } - NullableWrapped { - sorted_tag_layouts, .. - } => { - field_symbols = { - let mut temp = Vec::with_capacity_in(field_symbols_temp.len() + 1, arena); - - temp.extend(field_symbols_temp.iter().map(|r| r.1)); - - temp.into_bump_slice() - }; - - let mut layouts: Vec<&'a [Layout<'a>]> = - Vec::with_capacity_in(sorted_tag_layouts.len(), env.arena); - - for (_, arg_layouts) in sorted_tag_layouts.into_iter() { - layouts.push(arg_layouts); - } - - let tag = Expr::Tag { - tag_layout: union_layout, - tag_name: tag_name.into(), - tag_id: tag_id as _, - arguments: field_symbols, - }; - - (tag, union_layout) - } - NullableUnwrapped { .. } => { - field_symbols = { - let mut temp = Vec::with_capacity_in(field_symbols_temp.len() + 1, arena); - - temp.extend(field_symbols_temp.iter().map(|r| r.1)); - - temp.into_bump_slice() - }; - - let tag = Expr::Tag { - tag_layout: union_layout, - tag_name: tag_name.into(), - tag_id: tag_id as _, - arguments: field_symbols, - }; - - (tag, union_layout) - } - }; - - let stmt = Stmt::Let(assigned, tag, Layout::Union(union_layout), hole); - let iter = field_symbols_temp - .into_iter() - .map(|x| x.2 .0) - .rev() - .zip(field_symbols.iter().rev()); - - assign_to_symbols(env, procs, layout_cache, iter, stmt) - } - } -} - -#[allow(clippy::too_many_arguments)] -fn tag_union_to_function<'a>( - env: &mut Env<'a, '_>, - argument_variables: VariableSubsSlice, - return_variable: Variable, - tag_name: TagName, - proc_symbol: Symbol, - ext_var: Variable, - procs: &mut Procs<'a>, - whole_var: Variable, - layout_cache: &mut LayoutCache<'a>, - assigned: Symbol, - hole: &'a Stmt<'a>, -) -> Stmt<'a> { - let mut loc_pattern_args = vec![]; - let mut loc_expr_args = vec![]; - - for index in argument_variables { - let arg_var = env.subs[index]; - - let arg_symbol = env.unique_symbol(); - - let loc_pattern = Loc::at_zero(roc_can::pattern::Pattern::Identifier(arg_symbol)); - - let loc_expr = Loc::at_zero(roc_can::expr::Expr::Var(arg_symbol)); - - loc_pattern_args.push((arg_var, AnnotatedMark::known_exhaustive(), loc_pattern)); - loc_expr_args.push((arg_var, loc_expr)); - } - - let loc_body = Loc::at_zero(roc_can::expr::Expr::Tag { - variant_var: return_variable, - name: tag_name, - arguments: loc_expr_args, - ext_var, - }); - - let inserted = procs.insert_anonymous( - env, - proc_symbol, - whole_var, - loc_pattern_args, - loc_body, - CapturedSymbols::None, - return_variable, - layout_cache, - ); - - match inserted { - Ok(_layout) => { - // only need to construct closure data - let raw_layout = return_on_layout_error!( - env, - layout_cache.raw_from_var(env.arena, whole_var, env.subs) - ); - - match raw_layout { - RawFunctionLayout::Function(_, lambda_set, _) => { - construct_closure_data(env, lambda_set, proc_symbol, &[], assigned, hole) - } - RawFunctionLayout::ZeroArgumentThunk(_) => unreachable!(), - } - } - - Err(runtime_error) => Stmt::RuntimeError(env.arena.alloc(format!( - "RuntimeError {} line {} {:?}", - file!(), - line!(), - runtime_error, - ))), - } -} - -#[allow(clippy::type_complexity)] -fn sorted_field_symbols<'a>( - env: &mut Env<'a, '_>, - procs: &mut Procs<'a>, - layout_cache: &mut LayoutCache<'a>, - mut args: std::vec::Vec<(Variable, Loc)>, -) -> Vec< - 'a, - ( - u32, - Symbol, - ((Variable, Loc), &'a Symbol), - ), -> { - let mut field_symbols_temp = Vec::with_capacity_in(args.len(), env.arena); - - for (var, mut arg) in args.drain(..) { - // Layout will unpack this unwrapped tag if it only has one (non-zero-sized) field - let layout = match layout_cache.from_var(env.arena, var, env.subs) { - Ok(cached) => cached, - Err(LayoutProblem::UnresolvedTypeVar(_)) => { - // this argument has type `forall a. a`, which is isomorphic to - // the empty type (Void, Never, the empty tag union `[]`) - // Note it does not catch the use of `[]` currently. - use roc_can::expr::Expr; - arg.value = Expr::RuntimeError(RuntimeError::VoidValue); - Layout::UNIT - } - Err(LayoutProblem::Erroneous) => { - // something went very wrong - panic!("TODO turn fn_var into a RuntimeError") - } - }; - - let alignment = layout.alignment_bytes(env.target_info); - - let symbol = possible_reuse_symbol_or_specialize(env, procs, layout_cache, &arg.value, var); - field_symbols_temp.push((alignment, symbol, ((var, arg), &*env.arena.alloc(symbol)))); - } - field_symbols_temp.sort_by(|a, b| b.0.cmp(&a.0)); - - field_symbols_temp -} - -/// Insert a closure that does capture symbols (because it is top-level) to the list of partial procs -fn register_noncapturing_closure<'a>( - env: &mut Env<'a, '_>, - procs: &mut Procs<'a>, - closure_name: Symbol, - closure_data: ClosureData, -) { - let ClosureData { - function_type, - return_type, - recursive, - arguments, - loc_body: boxed_body, - captured_symbols, - .. - } = closure_data; - - // Extract Procs, but discard the resulting Expr::Load. - // That Load looks up the pointer, which we won't use here! - - let loc_body = *boxed_body; - - let is_self_recursive = !matches!(recursive, roc_can::expr::Recursive::NotRecursive); - - // this should be a top-level declaration, and hence have no captured symbols - // if we ever do hit this (and it's not a bug), we should make sure to put the - // captured symbols into a CapturedSymbols and give it to PartialProc::from_named_function - debug_assert!(captured_symbols.is_empty()); - - let partial_proc = PartialProc::from_named_function( - env, - function_type, - arguments, - loc_body, - CapturedSymbols::None, - is_self_recursive, - return_type, - ); - - procs.partial_procs.insert(closure_name, partial_proc); -} - -/// Insert a closure that may capture symbols to the list of partial procs -fn register_capturing_closure<'a>( - env: &mut Env<'a, '_>, - procs: &mut Procs<'a>, - layout_cache: &mut LayoutCache<'a>, - closure_name: Symbol, - closure_data: ClosureData, -) { - // the function surrounding the closure definition may be specialized multiple times, - // hence in theory this partial proc may be added multiple times. That would be wasteful - // so we check whether this partial proc is already there. - // - // (the `gen_primitives::task_always_twice` test has this behavior) - if !procs.partial_procs.contains_key(closure_name) { - let ClosureData { - function_type, - return_type, - closure_type, - recursive, - arguments, - loc_body: boxed_body, - captured_symbols, - .. - } = closure_data; - let loc_body = *boxed_body; - - let is_self_recursive = !matches!(recursive, roc_can::expr::Recursive::NotRecursive); - - let captured_symbols = match *env.subs.get_content_without_compacting(function_type) { - Content::Structure(FlatType::Func(_, closure_var, _)) => { - match LambdaSet::from_var(env.arena, env.subs, closure_var, env.target_info) { - Ok(lambda_set) => { - if let Layout::Struct { - field_layouts: &[], .. - } = lambda_set.runtime_representation() - { - CapturedSymbols::None - } else { - let mut temp = Vec::from_iter_in(captured_symbols, env.arena); - temp.sort(); - CapturedSymbols::Captured(temp.into_bump_slice()) - } - } - Err(_) => { - // just allow this. see https://github.com/rtfeldman/roc/issues/1585 - if captured_symbols.is_empty() { - CapturedSymbols::None - } else { - let mut temp = Vec::from_iter_in(captured_symbols, env.arena); - temp.sort(); - CapturedSymbols::Captured(temp.into_bump_slice()) - } - } - } - } - _ => { - // This is a value (zero-argument thunk); it cannot capture any variables. - debug_assert!( - captured_symbols.is_empty(), - "{:?} with layout {:?} {:?} {:?}", - &captured_symbols, - layout_cache.raw_from_var(env.arena, function_type, env.subs), - env.subs, - (function_type, closure_type), - ); - CapturedSymbols::None - } - }; - - let partial_proc = PartialProc::from_named_function( - env, - function_type, - arguments, - loc_body, - captured_symbols, - is_self_recursive, - return_type, - ); - - procs.partial_procs.insert(closure_name, partial_proc); - } -} - -pub fn from_can<'a>( - env: &mut Env<'a, '_>, - variable: Variable, - can_expr: roc_can::expr::Expr, - procs: &mut Procs<'a>, - layout_cache: &mut LayoutCache<'a>, -) -> Stmt<'a> { - use roc_can::expr::Expr::*; - - match can_expr { - When { - cond_var, - expr_var, - region: _, - loc_cond, - branches, - branches_cond_var: _, - exhaustive, - } => { - let cond_symbol = possible_reuse_symbol_or_specialize( - env, - procs, - layout_cache, - &loc_cond.value, - cond_var, - ); - - let stmt = from_can_when( - env, - cond_var, - expr_var, - cond_symbol, - branches, - exhaustive, - layout_cache, - procs, - None, - ); - - // define the `when` condition - assign_to_symbol( - env, - procs, - layout_cache, - cond_var, - *loc_cond, - cond_symbol, - stmt, - ) - } - If { - cond_var, - branch_var, - branches, - final_else, - } => { - let ret_layout = layout_cache - .from_var(env.arena, branch_var, env.subs) - .expect("invalid ret_layout"); - let cond_layout = layout_cache - .from_var(env.arena, cond_var, env.subs) - .expect("invalid cond_layout"); - - let mut stmt = from_can(env, branch_var, final_else.value, procs, layout_cache); - - for (loc_cond, loc_then) in branches.into_iter().rev() { - let branching_symbol = possible_reuse_symbol_or_specialize( - env, - procs, - layout_cache, - &loc_cond.value, - cond_var, - ); - let then = from_can(env, branch_var, loc_then.value, procs, layout_cache); - - stmt = cond(env, branching_symbol, cond_layout, then, stmt, ret_layout); - - stmt = assign_to_symbol( - env, - procs, - layout_cache, - cond_var, - loc_cond, - branching_symbol, - stmt, - ); - } - - stmt - } - - Expect { - loc_condition, - loc_continuation, - lookups_in_cond, - } => { - let rest = from_can(env, variable, loc_continuation.value, procs, layout_cache); - let cond_symbol = env.unique_symbol(); - - let lookups = Vec::from_iter_in(lookups_in_cond.iter().map(|t| t.0), env.arena); - - let mut layouts = Vec::with_capacity_in(lookups_in_cond.len(), env.arena); - - for (_, var) in lookups_in_cond { - let res_layout = layout_cache.from_var(env.arena, var, env.subs); - let layout = return_on_layout_error!(env, res_layout); - layouts.push(layout); - } - - let mut stmt = Stmt::Expect { - condition: cond_symbol, - region: loc_condition.region, - lookups: lookups.into_bump_slice(), - layouts: layouts.into_bump_slice(), - remainder: env.arena.alloc(rest), - }; - - stmt = with_hole( - env, - loc_condition.value, - variable, - procs, - layout_cache, - cond_symbol, - env.arena.alloc(stmt), - ); - - stmt - } - - LetRec(defs, cont, _cycle_mark) => { - // because Roc is strict, only functions can be recursive! - for def in defs.into_iter() { - if let roc_can::pattern::Pattern::Identifier(symbol) = &def.loc_pattern.value { - // Now that we know for sure it's a closure, get an owned - // version of these variant args so we can use them properly. - match def.loc_expr.value { - Closure(closure_data) => { - register_capturing_closure( - env, - procs, - layout_cache, - *symbol, - closure_data, - ); - - continue; - } - _ => unreachable!("recursive value is not a function"), - } - } - unreachable!("recursive value does not have Identifier pattern") - } - - from_can(env, variable, cont.value, procs, layout_cache) - } - LetNonRec(def, cont) => from_can_let(env, procs, layout_cache, def, cont, variable, None), - _ => { - let symbol = env.unique_symbol(); - let hole = env.arena.alloc(Stmt::Ret(symbol)); - with_hole(env, can_expr, variable, procs, layout_cache, symbol, hole) - } - } -} - -fn to_opt_branches<'a>( - env: &mut Env<'a, '_>, - procs: &mut Procs<'a>, - branches: std::vec::Vec, - exhaustive_mark: ExhaustiveMark, - layout_cache: &mut LayoutCache<'a>, -) -> std::vec::Vec<( - Pattern<'a>, - Option>, - roc_can::expr::Expr, -)> { - debug_assert!(!branches.is_empty()); - - let mut loc_branches = std::vec::Vec::new(); - let mut opt_branches = std::vec::Vec::new(); - - for when_branch in branches { - let exhaustive_guard = if when_branch.guard.is_some() { - Guard::HasGuard - } else { - Guard::NoGuard - }; - - if when_branch.redundant.is_redundant(env.subs) { - // Don't codegen this branch since it's redundant. - continue; - } - - for loc_pattern in when_branch.patterns { - match from_can_pattern(env, procs, layout_cache, &loc_pattern.value) { - Ok((mono_pattern, assignments)) => { - loc_branches.push(( - Loc::at(loc_pattern.region, mono_pattern.clone()), - exhaustive_guard, - )); - - let mut loc_expr = when_branch.value.clone(); - let region = loc_pattern.region; - for (symbol, variable, expr) in assignments.into_iter().rev() { - let def = roc_can::def::Def { - annotation: None, - expr_var: variable, - loc_expr: Loc::at(region, expr), - loc_pattern: Loc::at( - region, - roc_can::pattern::Pattern::Identifier(symbol), - ), - pattern_vars: std::iter::once((symbol, variable)).collect(), - }; - let new_expr = - roc_can::expr::Expr::LetNonRec(Box::new(def), Box::new(loc_expr)); - loc_expr = Loc::at(region, new_expr); - } - - // TODO remove clone? - opt_branches.push((mono_pattern, when_branch.guard.clone(), loc_expr.value)); - } - Err(runtime_error) => { - loc_branches.push(( - Loc::at(loc_pattern.region, Pattern::Underscore), - exhaustive_guard, - )); - - // TODO remove clone? - opt_branches.push(( - Pattern::Underscore, - when_branch.guard.clone(), - roc_can::expr::Expr::RuntimeError(runtime_error), - )); - } - } - } - } - - if exhaustive_mark.is_non_exhaustive(env.subs) { - // In contrast to elm (currently), we still do codegen even if a pattern is non-exhaustive. - // So we not only report exhaustiveness errors, but also correct them - opt_branches.push(( - Pattern::Underscore, - None, - roc_can::expr::Expr::RuntimeError(roc_problem::can::RuntimeError::NonExhaustivePattern), - )); - } - - opt_branches -} - -#[allow(clippy::too_many_arguments)] -fn from_can_when<'a>( - env: &mut Env<'a, '_>, - cond_var: Variable, - expr_var: Variable, - cond_symbol: Symbol, - branches: std::vec::Vec, - exhaustive_mark: ExhaustiveMark, - layout_cache: &mut LayoutCache<'a>, - procs: &mut Procs<'a>, - join_point: Option, -) -> Stmt<'a> { - if branches.is_empty() { - // A when-expression with no branches is a runtime error. - // We can't know what to return! - return Stmt::RuntimeError("Hit a 0-branch when expression"); - } - let opt_branches = to_opt_branches(env, procs, branches, exhaustive_mark, layout_cache); - - let cond_layout = - return_on_layout_error!(env, layout_cache.from_var(env.arena, cond_var, env.subs)); - - let ret_layout = - return_on_layout_error!(env, layout_cache.from_var(env.arena, expr_var, env.subs)); - - let arena = env.arena; - let it = opt_branches - .into_iter() - .map(|(pattern, opt_guard, can_expr)| { - let branch_stmt = match join_point { - None => from_can(env, expr_var, can_expr, procs, layout_cache), - Some(id) => { - let symbol = env.unique_symbol(); - let arguments = bumpalo::vec![in env.arena; symbol].into_bump_slice(); - let jump = env.arena.alloc(Stmt::Jump(id, arguments)); - - with_hole(env, can_expr, expr_var, procs, layout_cache, symbol, jump) - } - }; - - use crate::decision_tree::Guard; - if let Some(loc_expr) = opt_guard { - let id = JoinPointId(env.unique_symbol()); - let symbol = env.unique_symbol(); - let jump = env.arena.alloc(Stmt::Jump(id, env.arena.alloc([symbol]))); - - let guard_stmt = with_hole( - env, - loc_expr.value, - cond_var, - procs, - layout_cache, - symbol, - jump, - ); - - ( - pattern.clone(), - Guard::Guard { - id, - pattern, - stmt: guard_stmt, - }, - branch_stmt, - ) - } else { - (pattern, Guard::NoGuard, branch_stmt) - } - }); - let mono_branches = Vec::from_iter_in(it, arena); - - crate::decision_tree::optimize_when( - env, - procs, - layout_cache, - cond_symbol, - cond_layout, - ret_layout, - mono_branches, - ) -} - -fn substitute(substitutions: &BumpMap, s: Symbol) -> Option { - match substitutions.get(&s) { - Some(new) => { - debug_assert!(!substitutions.contains_key(new)); - Some(*new) - } - None => None, - } -} - -fn substitute_in_exprs<'a>(arena: &'a Bump, stmt: &mut Stmt<'a>, from: Symbol, to: Symbol) { - let mut subs = BumpMap::with_capacity_in(1, arena); - subs.insert(from, to); - - // TODO clean this up - let ref_stmt = arena.alloc(stmt.clone()); - if let Some(new) = substitute_in_stmt_help(arena, ref_stmt, &subs) { - *stmt = new.clone(); - } -} - -fn substitute_in_stmt_help<'a>( - arena: &'a Bump, - stmt: &'a Stmt<'a>, - subs: &BumpMap, -) -> Option<&'a Stmt<'a>> { - use Stmt::*; - - match stmt { - Let(symbol, expr, layout, cont) => { - let opt_cont = substitute_in_stmt_help(arena, cont, subs); - let opt_expr = substitute_in_expr(arena, expr, subs); - - if opt_expr.is_some() || opt_cont.is_some() { - let cont = opt_cont.unwrap_or(cont); - let expr = opt_expr.unwrap_or_else(|| expr.clone()); - - Some(arena.alloc(Let(*symbol, expr, *layout, cont))) - } else { - None - } - } - Join { - id, - parameters, - remainder, - body: continuation, - } => { - let opt_remainder = substitute_in_stmt_help(arena, remainder, subs); - let opt_continuation = substitute_in_stmt_help(arena, continuation, subs); - - if opt_remainder.is_some() || opt_continuation.is_some() { - let remainder = opt_remainder.unwrap_or(remainder); - let continuation = opt_continuation.unwrap_or(*continuation); - - Some(arena.alloc(Join { - id: *id, - parameters, - remainder, - body: continuation, - })) - } else { - None - } - } - Switch { - cond_symbol, - cond_layout, - branches, - default_branch, - ret_layout, - } => { - let opt_default = substitute_in_stmt_help(arena, default_branch.1, subs); - - let mut did_change = false; - - let opt_branches = Vec::from_iter_in( - branches.iter().map(|(label, info, branch)| { - match substitute_in_stmt_help(arena, branch, subs) { - None => None, - Some(branch) => { - did_change = true; - Some((*label, info.clone(), branch.clone())) - } - } - }), - arena, - ); - - if opt_default.is_some() || did_change { - let default_branch = ( - default_branch.0.clone(), - opt_default.unwrap_or(default_branch.1), - ); - - let branches = if did_change { - let new = Vec::from_iter_in( - opt_branches.into_iter().zip(branches.iter()).map( - |(opt_branch, branch)| match opt_branch { - None => branch.clone(), - Some(new_branch) => new_branch, - }, - ), - arena, - ); - - new.into_bump_slice() - } else { - branches - }; - - Some(arena.alloc(Switch { - cond_symbol: *cond_symbol, - cond_layout: *cond_layout, - default_branch, - branches, - ret_layout: *ret_layout, - })) - } else { - None - } - } - Ret(s) => match substitute(subs, *s) { - Some(s) => Some(arena.alloc(Ret(s))), - None => None, - }, - Refcounting(modify, cont) => { - // TODO should we substitute in the ModifyRc? - match substitute_in_stmt_help(arena, cont, subs) { - Some(cont) => Some(arena.alloc(Refcounting(*modify, cont))), - None => None, - } - } - - Expect { - condition, - region, - lookups, - layouts, - remainder, - } => { - // TODO should we substitute in the ModifyRc? - match substitute_in_stmt_help(arena, remainder, subs) { - Some(cont) => Some(arena.alloc(Expect { - condition: *condition, - region: *region, - lookups, - layouts, - remainder: cont, - })), - None => None, - } - } - - Jump(id, args) => { - let mut did_change = false; - let new_args = Vec::from_iter_in( - args.iter().map(|s| match substitute(subs, *s) { - None => *s, - Some(s) => { - did_change = true; - s - } - }), - arena, - ); - - if did_change { - let args = new_args.into_bump_slice(); - - Some(arena.alloc(Jump(*id, args))) - } else { - None - } - } - - RuntimeError(_) => None, - } -} - -fn substitute_in_call<'a>( - arena: &'a Bump, - call: &'a Call<'a>, - subs: &BumpMap, -) -> Option> { - let Call { - call_type, - arguments, - } = call; - - let opt_call_type = match call_type { - CallType::ByName { - name, - arg_layouts, - ret_layout, - specialization_id, - } => substitute(subs, *name).map(|new| CallType::ByName { - name: new, - arg_layouts, - ret_layout: *ret_layout, - specialization_id: *specialization_id, - }), - CallType::Foreign { .. } => None, - CallType::LowLevel { .. } => None, - CallType::HigherOrder { .. } => None, - }; - - let mut did_change = false; - let new_args = Vec::from_iter_in( - arguments.iter().map(|s| match substitute(subs, *s) { - None => *s, - Some(s) => { - did_change = true; - s - } - }), - arena, - ); - - if did_change || opt_call_type.is_some() { - let call_type = opt_call_type.unwrap_or_else(|| call_type.clone()); - - let arguments = new_args.into_bump_slice(); - - Some(self::Call { - call_type, - arguments, - }) - } else { - None - } -} - -fn substitute_in_expr<'a>( - arena: &'a Bump, - expr: &'a Expr<'a>, - subs: &BumpMap, -) -> Option> { - use Expr::*; - - match expr { - Literal(_) | EmptyArray | RuntimeErrorFunction(_) => None, - - Call(call) => substitute_in_call(arena, call, subs).map(Expr::Call), - - Tag { - tag_layout, - tag_name, - tag_id, - arguments: args, - } => { - let mut did_change = false; - let new_args = Vec::from_iter_in( - args.iter().map(|s| match substitute(subs, *s) { - None => *s, - Some(s) => { - did_change = true; - s - } - }), - arena, - ); - - if did_change { - let arguments = new_args.into_bump_slice(); - - Some(Tag { - tag_layout: *tag_layout, - tag_name: tag_name.clone(), - tag_id: *tag_id, - arguments, - }) - } else { - None - } - } - - Reuse { .. } | Reset { .. } => unreachable!("reset/reuse have not been introduced yet"), - - Struct(args) => { - let mut did_change = false; - let new_args = Vec::from_iter_in( - args.iter().map(|s| match substitute(subs, *s) { - None => *s, - Some(s) => { - did_change = true; - s - } - }), - arena, - ); - - if did_change { - let args = new_args.into_bump_slice(); - - Some(Struct(args)) - } else { - None - } - } - - Array { - elems: args, - elem_layout, - } => { - let mut did_change = false; - let new_args = Vec::from_iter_in( - args.iter().map(|e| { - if let ListLiteralElement::Symbol(s) = e { - match substitute(subs, *s) { - None => ListLiteralElement::Symbol(*s), - Some(s) => { - did_change = true; - ListLiteralElement::Symbol(s) - } - } - } else { - *e - } - }), - arena, - ); - - if did_change { - let args = new_args.into_bump_slice(); - - Some(Array { - elem_layout: *elem_layout, - elems: args, - }) - } else { - None - } - } - - ExprBox { symbol } => { - substitute(subs, *symbol).map(|new_symbol| ExprBox { symbol: new_symbol }) - } - - ExprUnbox { symbol } => { - substitute(subs, *symbol).map(|new_symbol| ExprUnbox { symbol: new_symbol }) - } - - StructAtIndex { - index, - structure, - field_layouts, - } => match substitute(subs, *structure) { - Some(structure) => Some(StructAtIndex { - index: *index, - field_layouts: *field_layouts, - structure, - }), - None => None, - }, - - GetTagId { - structure, - union_layout, - } => match substitute(subs, *structure) { - Some(structure) => Some(GetTagId { - structure, - union_layout: *union_layout, - }), - None => None, - }, - - UnionAtIndex { - structure, - tag_id, - index, - union_layout, - } => match substitute(subs, *structure) { - Some(structure) => Some(UnionAtIndex { - structure, - tag_id: *tag_id, - index: *index, - union_layout: *union_layout, - }), - None => None, - }, - } -} - -#[allow(clippy::too_many_arguments)] -pub fn store_pattern<'a>( - env: &mut Env<'a, '_>, - procs: &mut Procs<'a>, - layout_cache: &mut LayoutCache<'a>, - can_pat: &Pattern<'a>, - outer_symbol: Symbol, - stmt: Stmt<'a>, -) -> Stmt<'a> { - match store_pattern_help(env, procs, layout_cache, can_pat, outer_symbol, stmt) { - StorePattern::Productive(new) => new, - StorePattern::NotProductive(new) => new, - } -} - -enum StorePattern<'a> { - /// we bound new symbols - Productive(Stmt<'a>), - /// no new symbols were bound in this pattern - NotProductive(Stmt<'a>), -} - -/// It is crucial for correct RC insertion that we don't create dead variables! -#[allow(clippy::too_many_arguments)] -fn store_pattern_help<'a>( - env: &mut Env<'a, '_>, - procs: &mut Procs<'a>, - layout_cache: &mut LayoutCache<'a>, - can_pat: &Pattern<'a>, - outer_symbol: Symbol, - mut stmt: Stmt<'a>, -) -> StorePattern<'a> { - use Pattern::*; - - match can_pat { - Identifier(symbol) => { - // An identifier in a pattern can define at most one specialization! - // Remove any requested specializations for this name now, since this is the definition site. - let specialization_symbol = procs - .symbol_specializations - .remove_single(*symbol) - // Can happen when the symbol was never used under this body, and hence has no - // requested specialization. - .unwrap_or(*symbol); - - substitute_in_exprs(env.arena, &mut stmt, specialization_symbol, outer_symbol); - } - Underscore => { - // do nothing - return StorePattern::NotProductive(stmt); - } - IntLiteral(_, _) - | FloatLiteral(_, _) - | DecimalLiteral(_) - | EnumLiteral { .. } - | BitLiteral { .. } - | StrLiteral(_) => { - return StorePattern::NotProductive(stmt); - } - NewtypeDestructure { arguments, .. } => match arguments.as_slice() { - [(pattern, _layout)] => { - return store_pattern_help(env, procs, layout_cache, pattern, outer_symbol, stmt); - } - _ => { - let mut fields = Vec::with_capacity_in(arguments.len(), env.arena); - fields.extend(arguments.iter().map(|x| x.1)); - - let layout = Layout::struct_no_name_order(fields.into_bump_slice()); - - return store_newtype_pattern( - env, - procs, - layout_cache, - outer_symbol, - &layout, - arguments, - stmt, - ); - } - }, - AppliedTag { - arguments, - layout, - tag_id, - .. - } => { - return store_tag_pattern( - env, - procs, - layout_cache, - outer_symbol, - *layout, - arguments, - *tag_id, - stmt, - ); - } - OpaqueUnwrap { argument, .. } => { - let (pattern, _layout) = &**argument; - return store_pattern_help(env, procs, layout_cache, pattern, outer_symbol, stmt); - } - - RecordDestructure(destructs, [_single_field]) => { - for destruct in destructs { - match &destruct.typ { - DestructType::Required(symbol) => { - let specialization_symbol = procs - .symbol_specializations - .remove_single(*symbol) - // Can happen when the symbol was never used under this body, and hence has no - // requested specialization. - .unwrap_or(*symbol); - - substitute_in_exprs( - env.arena, - &mut stmt, - specialization_symbol, - outer_symbol, - ); - } - DestructType::Guard(guard_pattern) => { - return store_pattern_help( - env, - procs, - layout_cache, - guard_pattern, - outer_symbol, - stmt, - ); - } - } - } - } - RecordDestructure(destructs, sorted_fields) => { - let mut is_productive = false; - for (index, destruct) in destructs.iter().enumerate().rev() { - match store_record_destruct( - env, - procs, - layout_cache, - destruct, - index as u64, - outer_symbol, - sorted_fields, - stmt, - ) { - StorePattern::Productive(new) => { - is_productive = true; - stmt = new; - } - StorePattern::NotProductive(new) => { - stmt = new; - } - } - } - - if !is_productive { - return StorePattern::NotProductive(stmt); - } - } - } - - StorePattern::Productive(stmt) -} - -#[allow(clippy::too_many_arguments)] -fn store_tag_pattern<'a>( - env: &mut Env<'a, '_>, - procs: &mut Procs<'a>, - layout_cache: &mut LayoutCache<'a>, - structure: Symbol, - union_layout: UnionLayout<'a>, - arguments: &[(Pattern<'a>, Layout<'a>)], - tag_id: TagIdIntType, - mut stmt: Stmt<'a>, -) -> StorePattern<'a> { - use Pattern::*; - - let mut is_productive = false; - - for (index, (argument, arg_layout)) in arguments.iter().enumerate().rev() { - let mut arg_layout = *arg_layout; - - if let Layout::RecursivePointer = arg_layout { - arg_layout = Layout::Union(union_layout); - } - - let load = Expr::UnionAtIndex { - index: index as u64, - structure, - tag_id, - union_layout, - }; - - match argument { - Identifier(symbol) => { - // Pattern can define only one specialization - let symbol = procs - .symbol_specializations - .remove_single(*symbol) - .unwrap_or(*symbol); - - // store immediately in the given symbol - stmt = Stmt::Let(symbol, load, arg_layout, env.arena.alloc(stmt)); - is_productive = true; - } - Underscore => { - // ignore - } - IntLiteral(_, _) - | FloatLiteral(_, _) - | DecimalLiteral(_) - | EnumLiteral { .. } - | BitLiteral { .. } - | StrLiteral(_) => {} - _ => { - // store the field in a symbol, and continue matching on it - let symbol = env.unique_symbol(); - - // first recurse, continuing to unpack symbol - match store_pattern_help(env, procs, layout_cache, argument, symbol, stmt) { - StorePattern::Productive(new) => { - is_productive = true; - stmt = new; - // only if we bind one of its (sub)fields to a used name should we - // extract the field - stmt = Stmt::Let(symbol, load, arg_layout, env.arena.alloc(stmt)); - } - StorePattern::NotProductive(new) => { - // do nothing - stmt = new; - } - } - } - } - } - - if is_productive { - StorePattern::Productive(stmt) - } else { - StorePattern::NotProductive(stmt) - } -} - -#[allow(clippy::too_many_arguments)] -fn store_newtype_pattern<'a>( - env: &mut Env<'a, '_>, - procs: &mut Procs<'a>, - layout_cache: &mut LayoutCache<'a>, - structure: Symbol, - layout: &Layout<'a>, - arguments: &[(Pattern<'a>, Layout<'a>)], - mut stmt: Stmt<'a>, -) -> StorePattern<'a> { - use Pattern::*; - - let mut arg_layouts = Vec::with_capacity_in(arguments.len(), env.arena); - let mut is_productive = false; - - for (_, layout) in arguments { - arg_layouts.push(*layout); - } - - for (index, (argument, arg_layout)) in arguments.iter().enumerate().rev() { - let mut arg_layout = *arg_layout; - - if let Layout::RecursivePointer = arg_layout { - arg_layout = *layout; - } - - let load = Expr::StructAtIndex { - index: index as u64, - field_layouts: arg_layouts.clone().into_bump_slice(), - structure, - }; - - match argument { - Identifier(symbol) => { - // store immediately in the given symbol, removing it specialization if it had any - let specialization_symbol = procs - .symbol_specializations - .remove_single(*symbol) - // Can happen when the symbol was never used under this body, and hence has no - // requested specialization. - .unwrap_or(*symbol); - - stmt = Stmt::Let( - specialization_symbol, - load, - arg_layout, - env.arena.alloc(stmt), - ); - is_productive = true; - } - Underscore => { - // ignore - } - IntLiteral(_, _) - | FloatLiteral(_, _) - | DecimalLiteral(_) - | EnumLiteral { .. } - | BitLiteral { .. } - | StrLiteral(_) => {} - _ => { - // store the field in a symbol, and continue matching on it - let symbol = env.unique_symbol(); - - // first recurse, continuing to unpack symbol - match store_pattern_help(env, procs, layout_cache, argument, symbol, stmt) { - StorePattern::Productive(new) => { - is_productive = true; - stmt = new; - // only if we bind one of its (sub)fields to a used name should we - // extract the field - stmt = Stmt::Let(symbol, load, arg_layout, env.arena.alloc(stmt)); - } - StorePattern::NotProductive(new) => { - // do nothing - stmt = new; - } - } - } - } - } - - if is_productive { - StorePattern::Productive(stmt) - } else { - StorePattern::NotProductive(stmt) - } -} - -#[allow(clippy::too_many_arguments)] -fn store_record_destruct<'a>( - env: &mut Env<'a, '_>, - procs: &mut Procs<'a>, - layout_cache: &mut LayoutCache<'a>, - destruct: &RecordDestruct<'a>, - index: u64, - outer_symbol: Symbol, - sorted_fields: &'a [Layout<'a>], - mut stmt: Stmt<'a>, -) -> StorePattern<'a> { - use Pattern::*; - - let load = Expr::StructAtIndex { - index, - field_layouts: sorted_fields, - structure: outer_symbol, - }; - - match &destruct.typ { - DestructType::Required(symbol) => { - // A destructure can define at most one specialization! - // Remove any requested specializations for this name now, since this is the definition site. - let specialization_symbol = procs - .symbol_specializations - .remove_single(*symbol) - // Can happen when the symbol was never used under this body, and hence has no - // requested specialization. - .unwrap_or(*symbol); - - stmt = Stmt::Let( - specialization_symbol, - load, - destruct.layout, - env.arena.alloc(stmt), - ); - } - DestructType::Guard(guard_pattern) => match &guard_pattern { - Identifier(symbol) => { - let specialization_symbol = procs - .symbol_specializations - .remove_single(*symbol) - // Can happen when the symbol was never used under this body, and hence has no - // requested specialization. - .unwrap_or(*symbol); - - stmt = Stmt::Let( - specialization_symbol, - load, - destruct.layout, - env.arena.alloc(stmt), - ); - } - Underscore => { - // important that this is special-cased to do nothing: mono record patterns will extract all the - // fields, but those not bound in the source code are guarded with the underscore - // pattern. So given some record `{ x : a, y : b }`, a match - // - // { x } -> ... - // - // is actually - // - // { x, y: _ } -> ... - // - // internally. But `y` is never used, so we must make sure it't not stored/loaded. - return StorePattern::NotProductive(stmt); - } - IntLiteral(_, _) - | FloatLiteral(_, _) - | DecimalLiteral(_) - | EnumLiteral { .. } - | BitLiteral { .. } - | StrLiteral(_) => { - return StorePattern::NotProductive(stmt); - } - - _ => { - let symbol = env.unique_symbol(); - - match store_pattern_help(env, procs, layout_cache, guard_pattern, symbol, stmt) { - StorePattern::Productive(new) => { - stmt = new; - stmt = Stmt::Let(symbol, load, destruct.layout, env.arena.alloc(stmt)); - } - StorePattern::NotProductive(stmt) => return StorePattern::NotProductive(stmt), - } - } - }, - } - - StorePattern::Productive(stmt) -} - -/// We want to re-use symbols that are not function symbols -/// for any other expression, we create a new symbol, and will -/// later make sure it gets assigned the correct value. - -#[derive(Debug)] -enum ReuseSymbol { - Imported(Symbol), - LocalFunction(Symbol), - Value(Symbol), - UnspecializedExpr(Symbol), - NotASymbol, -} - -fn can_reuse_symbol<'a>( - env: &mut Env<'a, '_>, - procs: &Procs<'a>, - expr: &roc_can::expr::Expr, - expr_var: Variable, -) -> ReuseSymbol { - use roc_can::expr::Expr::*; - use ReuseSymbol::*; - - let symbol = match expr { - AbilityMember(member, specialization_id, _) => { - late_resolve_ability_specialization(env, *member, *specialization_id, expr_var) - } - Var(symbol) => *symbol, - _ => return NotASymbol, - }; - - let arguments = [ - Symbol::ARG_1, - Symbol::ARG_2, - Symbol::ARG_3, - Symbol::ARG_4, - Symbol::ARG_5, - Symbol::ARG_6, - Symbol::ARG_7, - ]; - - if arguments.contains(&symbol) { - Value(symbol) - } else if env.is_imported_symbol(symbol) { - Imported(symbol) - } else if procs.partial_procs.contains_key(symbol) { - LocalFunction(symbol) - } else if procs.ability_member_aliases.get(symbol).is_some() { - UnspecializedExpr(symbol) - } else { - Value(symbol) - } -} - -fn possible_reuse_symbol_or_specialize<'a>( - env: &mut Env<'a, '_>, - procs: &mut Procs<'a>, - layout_cache: &mut LayoutCache<'a>, - expr: &roc_can::expr::Expr, - var: Variable, -) -> Symbol { - match can_reuse_symbol(env, procs, expr, var) { - ReuseSymbol::Value(symbol) => { - procs - .symbol_specializations - .get_or_insert(env, layout_cache, symbol, var) - } - _ => env.unique_symbol(), - } -} - -fn handle_variable_aliasing<'a, BuildRest>( - env: &mut Env<'a, '_>, - procs: &mut Procs<'a>, - layout_cache: &mut LayoutCache<'a>, - variable: Variable, - left: Symbol, - right: Symbol, - build_rest: BuildRest, -) -> Stmt<'a> -where - BuildRest: FnOnce(&mut Env<'a, '_>, &mut Procs<'a>, &mut LayoutCache<'a>) -> Stmt<'a>, -{ - // 1. Handle references to ability members - we could be aliasing an ability member, or another - // alias to an ability member. - { - if env.abilities_store.is_ability_member_name(right) { - procs - .ability_member_aliases - .insert(left, AbilityMember(right)); - return build_rest(env, procs, layout_cache); - } - if let Some(&ability_member) = procs.ability_member_aliases.get(right) { - procs.ability_member_aliases.insert(left, ability_member); - return build_rest(env, procs, layout_cache); - } - } - - // 2. Handle references to a known proc - again, we may be either aliasing the proc, or another - // alias to a proc. - if procs.partial_procs.contains_key(right) { - // This is an alias to a function defined in this module. - // Attach the alias, then build the rest of the module, so that we reference and specialize - // the correct proc. - procs.partial_procs.insert_alias(left, right); - return build_rest(env, procs, layout_cache); - } - - // Otherwise we're dealing with an alias whose usages will tell us what specializations we - // need. So let's figure those out first. - let result = build_rest(env, procs, layout_cache); - - // The specializations we wanted of the symbol on the LHS of this alias. - let needed_specializations_of_left = procs.symbol_specializations.remove(left); - - if procs.is_imported_module_thunk(right) { - // if this is an imported symbol, then we must make sure it is - // specialized, and wrap the original in a function pointer. - let mut result = result; - for (_, (variable, left)) in needed_specializations_of_left { - add_needed_external(procs, env, variable, right); - - let res_layout = layout_cache.from_var(env.arena, variable, env.subs); - let layout = return_on_layout_error!(env, res_layout); - - result = force_thunk(env, right, layout, left, env.arena.alloc(result)); - } - result - } else if env.is_imported_symbol(right) { - // if this is an imported symbol, then we must make sure it is - // specialized, and wrap the original in a function pointer. - add_needed_external(procs, env, variable, right); - - // then we must construct its closure; since imported symbols have no closure, we use the empty struct - let_empty_struct(left, env.arena.alloc(result)) - } else { - // Otherwise, we are referencing a non-proc value. - - // We need to lift all specializations of "left" to be specializations of "right". - let mut scratchpad_update_specializations = std::vec::Vec::new(); - - let left_had_specialization_symbols = needed_specializations_of_left.len() > 0; - - for (specialization_mark, (specialized_var, specialized_sym)) in - needed_specializations_of_left - { - let old_specialized_sym = procs.symbol_specializations.get_or_insert_known( - right, - specialization_mark, - specialized_var, - specialized_sym, - ); - - if let Some((_, old_specialized_sym)) = old_specialized_sym { - scratchpad_update_specializations.push((old_specialized_sym, specialized_sym)); - } - } - - let mut result = result; - if left_had_specialization_symbols { - // If the symbol is specialized, only the specializations need to be updated. - for (old_specialized_sym, specialized_sym) in - scratchpad_update_specializations.into_iter() - { - substitute_in_exprs(env.arena, &mut result, old_specialized_sym, specialized_sym); - } - } else { - substitute_in_exprs(env.arena, &mut result, left, right); - } - - result - } -} - -fn force_thunk<'a>( - env: &mut Env<'a, '_>, - thunk_name: Symbol, - layout: Layout<'a>, - assigned: Symbol, - hole: &'a Stmt<'a>, -) -> Stmt<'a> { - let call = self::Call { - call_type: CallType::ByName { - name: thunk_name, - ret_layout: env.arena.alloc(layout), - arg_layouts: &[], - specialization_id: env.next_call_specialization_id(), - }, - arguments: &[], - }; - - build_call(env, call, assigned, layout, env.arena.alloc(hole)) -} - -fn let_empty_struct<'a>(assigned: Symbol, hole: &'a Stmt<'a>) -> Stmt<'a> { - Stmt::Let(assigned, Expr::Struct(&[]), Layout::UNIT, hole) -} - -/// If the symbol is a function or polymorphic value, make sure it is properly specialized -fn specialize_symbol<'a>( - env: &mut Env<'a, '_>, - procs: &mut Procs<'a>, - layout_cache: &mut LayoutCache<'a>, - arg_var: Option, - symbol: Symbol, - result: Stmt<'a>, - original: Symbol, -) -> Stmt<'a> { - match procs.get_partial_proc(original) { - None => { - match arg_var { - Some(arg_var) if env.is_imported_symbol(original) => { - let raw = match layout_cache.raw_from_var(env.arena, arg_var, env.subs) { - Ok(v) => v, - Err(e) => return_on_layout_error_help!(env, e), - }; - - if procs.is_imported_module_thunk(original) { - let layout = match raw { - RawFunctionLayout::ZeroArgumentThunk(layout) => layout, - RawFunctionLayout::Function(_, lambda_set, _) => { - Layout::LambdaSet(lambda_set) - } - }; - - let raw = RawFunctionLayout::ZeroArgumentThunk(layout); - let top_level = ProcLayout::from_raw(env.arena, raw); - - procs.insert_passed_by_name( - env, - arg_var, - original, - top_level, - layout_cache, - ); - - force_thunk(env, original, layout, symbol, env.arena.alloc(result)) - } else { - let top_level = ProcLayout::from_raw(env.arena, raw); - procs.insert_passed_by_name( - env, - arg_var, - original, - top_level, - layout_cache, - ); - - let_empty_struct(symbol, env.arena.alloc(result)) - } - } - - _ => { - // danger: a foreign symbol may not be specialized! - debug_assert!( - !env.is_imported_symbol(original), - "symbol {:?} while processing module {:?}", - original, - (env.home, &arg_var), - ); - result - } - } - } - - Some(partial_proc) => { - let arg_var = arg_var.unwrap_or(partial_proc.annotation); - // this symbol is a function, that is used by-name (e.g. as an argument to another - // function). Register it with the current variable, then create a function pointer - // to it in the IR. - let res_layout = return_on_layout_error!( - env, - layout_cache.raw_from_var(env.arena, arg_var, env.subs) - ); - - // we have three kinds of functions really. Plain functions, closures by capture, - // and closures by unification. Here we record whether this function captures - // anything. - let captures = partial_proc.captured_symbols.captures(); - let captured = partial_proc.captured_symbols; - - match res_layout { - RawFunctionLayout::Function(_, lambda_set, _) => { - // define the function pointer - let function_ptr_layout = ProcLayout::from_raw(env.arena, res_layout); - - if captures { - // this is a closure by capture, meaning it itself captures local variables. - procs.insert_passed_by_name( - env, - arg_var, - original, - function_ptr_layout, - layout_cache, - ); - - let closure_data = symbol; - - let symbols = match captured { - CapturedSymbols::Captured(captured_symbols) => { - Vec::from_iter_in(captured_symbols.iter(), env.arena) - .into_bump_slice() - } - CapturedSymbols::None => unreachable!(), - }; - - construct_closure_data( - env, - lambda_set, - original, - symbols.iter().copied(), - closure_data, - env.arena.alloc(result), - ) - } else if procs.is_module_thunk(original) { - // this is a 0-argument thunk - - // TODO suspicious - // let layout = Layout::Closure(argument_layouts, lambda_set, ret_layout); - // panic!("suspicious"); - let layout = Layout::LambdaSet(lambda_set); - let top_level = ProcLayout::new(env.arena, &[], layout); - procs.insert_passed_by_name( - env, - arg_var, - original, - top_level, - layout_cache, - ); - - force_thunk(env, original, layout, symbol, env.arena.alloc(result)) - } else { - procs.insert_passed_by_name( - env, - arg_var, - original, - function_ptr_layout, - layout_cache, - ); - - // even though this function may not itself capture, - // unification may still cause it to have an extra argument - construct_closure_data( - env, - lambda_set, - original, - &[], - symbol, - env.arena.alloc(result), - ) - } - } - RawFunctionLayout::ZeroArgumentThunk(ret_layout) => { - // this is a 0-argument thunk - let top_level = ProcLayout::new(env.arena, &[], ret_layout); - procs.insert_passed_by_name(env, arg_var, original, top_level, layout_cache); - - force_thunk(env, original, ret_layout, symbol, env.arena.alloc(result)) - } - } - } - } -} - -fn assign_to_symbol<'a>( - env: &mut Env<'a, '_>, - procs: &mut Procs<'a>, - layout_cache: &mut LayoutCache<'a>, - arg_var: Variable, - loc_arg: Loc, - symbol: Symbol, - result: Stmt<'a>, -) -> Stmt<'a> { - use ReuseSymbol::*; - match can_reuse_symbol(env, procs, &loc_arg.value, arg_var) { - Imported(original) | LocalFunction(original) | UnspecializedExpr(original) => { - // for functions we must make sure they are specialized correctly - specialize_symbol( - env, - procs, - layout_cache, - Some(arg_var), - symbol, - result, - original, - ) - } - Value(_symbol) => result, - NotASymbol => with_hole( - env, - loc_arg.value, - arg_var, - procs, - layout_cache, - symbol, - env.arena.alloc(result), - ), - } -} - -fn assign_to_symbols<'a, I>( - env: &mut Env<'a, '_>, - procs: &mut Procs<'a>, - layout_cache: &mut LayoutCache<'a>, - iter: I, - mut result: Stmt<'a>, -) -> Stmt<'a> -where - I: Iterator), &'a Symbol)>, -{ - for ((arg_var, loc_arg), symbol) in iter { - result = assign_to_symbol(env, procs, layout_cache, arg_var, loc_arg, *symbol, result); - } - - result -} - -fn add_needed_external<'a>( - procs: &mut Procs<'a>, - env: &mut Env<'a, '_>, - fn_var: Variable, - name: Symbol, -) { - // call of a function that is not in this module - use hashbrown::hash_map::Entry::{Occupied, Vacant}; - - let existing = match procs.externals_we_need.entry(name.module_id()) { - Vacant(entry) => entry.insert(ExternalSpecializations::new()), - Occupied(entry) => entry.into_mut(), - }; - - existing.insert_external(name, env.subs, fn_var); -} - -fn build_call<'a>( - _env: &mut Env<'a, '_>, - call: Call<'a>, - assigned: Symbol, - return_layout: Layout<'a>, - hole: &'a Stmt<'a>, -) -> Stmt<'a> { - Stmt::Let(assigned, Expr::Call(call), return_layout, hole) -} - -/// See https://github.com/rtfeldman/roc/issues/1549 -/// -/// What happened is that a function has a type error, but the arguments are not processed. -/// That means specializations were missing. Normally that is not a problem, but because -/// of our closure strategy, internal functions can "leak". That's what happened here. -/// -/// The solution is to evaluate the arguments as normal, and only when calling the function give an error -fn evaluate_arguments_then_runtime_error<'a>( - env: &mut Env<'a, '_>, - procs: &mut Procs<'a>, - layout_cache: &mut LayoutCache<'a>, - msg: String, - loc_args: std::vec::Vec<(Variable, Loc)>, -) -> Stmt<'a> { - let arena = env.arena; - - // eventually we will throw this runtime error - let result = Stmt::RuntimeError(env.arena.alloc(msg)); - - // but, we also still evaluate and specialize the arguments to give better error messages - let arg_symbols = Vec::from_iter_in( - loc_args.iter().map(|(var, arg_expr)| { - possible_reuse_symbol_or_specialize(env, procs, layout_cache, &arg_expr.value, *var) - }), - arena, - ) - .into_bump_slice(); - - let iter = loc_args.into_iter().rev().zip(arg_symbols.iter().rev()); - assign_to_symbols(env, procs, layout_cache, iter, result) -} - -#[allow(clippy::too_many_arguments)] -fn call_by_name<'a>( - env: &mut Env<'a, '_>, - procs: &mut Procs<'a>, - fn_var: Variable, - proc_name: Symbol, - loc_args: std::vec::Vec<(Variable, Loc)>, - layout_cache: &mut LayoutCache<'a>, - assigned: Symbol, - hole: &'a Stmt<'a>, -) -> Stmt<'a> { - // Register a pending_specialization for this function - match layout_cache.raw_from_var(env.arena, fn_var, env.subs) { - Err(LayoutProblem::UnresolvedTypeVar(var)) => { - let msg = format!( - "Hit an unresolved type variable {:?} when creating a layout for {:?} (var {:?})", - var, proc_name, fn_var - ); - - evaluate_arguments_then_runtime_error(env, procs, layout_cache, msg, loc_args) - } - Err(LayoutProblem::Erroneous) => { - let msg = format!( - "Hit an erroneous type when creating a layout for {:?}", - proc_name - ); - - evaluate_arguments_then_runtime_error(env, procs, layout_cache, msg, loc_args) - } - Ok(RawFunctionLayout::Function(arg_layouts, lambda_set, ret_layout)) => { - if procs.is_module_thunk(proc_name) { - if loc_args.is_empty() { - call_by_name_module_thunk( - env, - procs, - fn_var, - proc_name, - env.arena.alloc(Layout::LambdaSet(lambda_set)), - layout_cache, - assigned, - hole, - ) - } else { - // here we turn a call to a module thunk into forcing of that thunk - // the thunk represents the closure environment for the body, so we then match - // on the closure environment to perform the call that the body represents. - // - // Example: - // - // > main = parseA "foo" "bar" - // > parseA = Str.concat - - let closure_data_symbol = env.unique_symbol(); - - let arena = env.arena; - let arg_symbols = Vec::from_iter_in( - loc_args.iter().map(|(arg_var, arg_expr)| { - possible_reuse_symbol_or_specialize( - env, - procs, - layout_cache, - &arg_expr.value, - *arg_var, - ) - }), - arena, - ) - .into_bump_slice(); - - debug_assert_eq!(arg_symbols.len(), arg_layouts.len()); - - let result = match_on_lambda_set( - env, - lambda_set, - closure_data_symbol, - arg_symbols, - arg_layouts, - ret_layout, - assigned, - hole, - ); - - let result = call_by_name_module_thunk( - env, - procs, - fn_var, - proc_name, - env.arena.alloc(Layout::LambdaSet(lambda_set)), - layout_cache, - closure_data_symbol, - env.arena.alloc(result), - ); - - let iter = loc_args.into_iter().rev().zip(arg_symbols.iter().rev()); - assign_to_symbols(env, procs, layout_cache, iter, result) - } - } else { - call_by_name_help( - env, - procs, - fn_var, - proc_name, - loc_args, - lambda_set, - arg_layouts, - ret_layout, - layout_cache, - assigned, - hole, - ) - } - } - Ok(RawFunctionLayout::ZeroArgumentThunk(ret_layout)) => { - if procs.is_module_thunk(proc_name) { - // here we turn a call to a module thunk into forcing of that thunk - call_by_name_module_thunk( - env, - procs, - fn_var, - proc_name, - env.arena.alloc(ret_layout), - layout_cache, - assigned, - hole, - ) - } else if env.is_imported_symbol(proc_name) { - add_needed_external(procs, env, fn_var, proc_name); - force_thunk(env, proc_name, ret_layout, assigned, hole) - } else { - panic!("most likely we're trying to call something that is not a function"); - } - } - } -} - -#[allow(clippy::too_many_arguments)] -fn call_by_name_help<'a>( - env: &mut Env<'a, '_>, - procs: &mut Procs<'a>, - fn_var: Variable, - proc_name: Symbol, - loc_args: std::vec::Vec<(Variable, Loc)>, - lambda_set: LambdaSet<'a>, - argument_layouts: &'a [Layout<'a>], - ret_layout: &'a Layout<'a>, - layout_cache: &mut LayoutCache<'a>, - assigned: Symbol, - hole: &'a Stmt<'a>, -) -> Stmt<'a> { - let original_fn_var = fn_var; - let arena = env.arena; - - // the arguments given to the function, stored in symbols - let mut field_symbols = Vec::with_capacity_in(loc_args.len(), arena); - field_symbols.extend(loc_args.iter().map(|(arg_var, arg_expr)| { - possible_reuse_symbol_or_specialize(env, procs, layout_cache, &arg_expr.value, *arg_var) - })); - - // If required, add an extra argument to the layout that is the captured environment - // afterwards, we MUST make sure the number of arguments in the layout matches the - // number of arguments actually passed. - let top_level_layout = { - let argument_layouts = lambda_set.extend_argument_list(env.arena, argument_layouts); - ProcLayout::new(env.arena, argument_layouts, *ret_layout) - }; - - // the variables of the given arguments - let mut pattern_vars = Vec::with_capacity_in(loc_args.len(), arena); - for (var, _) in &loc_args { - match layout_cache.from_var(env.arena, *var, env.subs) { - Ok(_) => { - pattern_vars.push(*var); - } - Err(_) => { - // One of this function's arguments code gens to a runtime error, - // so attempting to call it will immediately crash. - return Stmt::RuntimeError("TODO runtime error for invalid layout"); - } - } - } - - // If we've already specialized this one, no further work is needed. - if procs - .specialized - .is_specialized(proc_name, &top_level_layout) - { - debug_assert_eq!( - argument_layouts.len(), - field_symbols.len(), - "see call_by_name for background (scroll down a bit), function is {:?}", - proc_name, - ); - - let field_symbols = field_symbols.into_bump_slice(); - - let call = self::Call { - call_type: CallType::ByName { - name: proc_name, - ret_layout, - arg_layouts: argument_layouts, - specialization_id: env.next_call_specialization_id(), - }, - arguments: field_symbols, - }; - - let result = build_call(env, call, assigned, *ret_layout, hole); - - let iter = loc_args.into_iter().rev().zip(field_symbols.iter().rev()); - assign_to_symbols(env, procs, layout_cache, iter, result) - } else if env.is_imported_symbol(proc_name) { - add_needed_external(procs, env, original_fn_var, proc_name); - - debug_assert_ne!(proc_name.module_id(), ModuleId::ATTR); - - if procs.is_imported_module_thunk(proc_name) { - force_thunk( - env, - proc_name, - Layout::LambdaSet(lambda_set), - assigned, - hole, - ) - } else if field_symbols.is_empty() { - // this is a case like `Str.concat`, an imported standard function, applied to zero arguments - - // imported symbols cannot capture anything - let captured = &[]; - - construct_closure_data(env, lambda_set, proc_name, captured, assigned, hole) - } else { - debug_assert_eq!( - argument_layouts.len(), - field_symbols.len(), - "see call_by_name for background (scroll down a bit), function is {:?}", - proc_name, - ); - - let field_symbols = field_symbols.into_bump_slice(); - - let call = self::Call { - call_type: CallType::ByName { - name: proc_name, - ret_layout, - arg_layouts: argument_layouts, - specialization_id: env.next_call_specialization_id(), - }, - arguments: field_symbols, - }; - - let result = build_call(env, call, assigned, *ret_layout, hole); - - let iter = loc_args.into_iter().rev().zip(field_symbols.iter().rev()); - assign_to_symbols(env, procs, layout_cache, iter, result) - } - } else { - // When requested (that is, when procs.pending_specializations is `Some`), - // store a pending specialization rather than specializing immediately. - // - // We do this so that we can do specialization in two passes: first, - // build the mono_expr with all the specialized calls in place (but - // no specializations performed yet), and then second, *after* - // de-duplicating requested specializations (since multiple modules - // which could be getting monomorphized in parallel might request - // the same specialization independently), we work through the - // queue of pending specializations to complete each specialization - // exactly once. - if procs.is_module_thunk(proc_name) { - debug_assert!(top_level_layout.arguments.is_empty()); - } - - match &mut procs.pending_specializations { - PendingSpecializations::Finding(suspended) => { - debug_assert!(!env.is_imported_symbol(proc_name)); - - // register the pending specialization, so this gets code genned later - suspended.specialization(env.subs, proc_name, top_level_layout, fn_var); - - debug_assert_eq!( - argument_layouts.len(), - field_symbols.len(), - "see call_by_name for background (scroll down a bit), function is {:?}", - proc_name, - ); - - let has_closure = argument_layouts.len() != top_level_layout.arguments.len(); - let closure_argument = env.unique_symbol(); - - if has_closure { - field_symbols.push(closure_argument); - } - - let field_symbols = field_symbols.into_bump_slice(); - - let call = self::Call { - call_type: CallType::ByName { - name: proc_name, - ret_layout, - arg_layouts: top_level_layout.arguments, - specialization_id: env.next_call_specialization_id(), - }, - arguments: field_symbols, - }; - - let result = build_call(env, call, assigned, *ret_layout, hole); - - // NOTE: the zip omits the closure symbol, if it exists, - // because loc_args then is shorter than field_symbols - debug_assert!([0, 1].contains(&(field_symbols.len() - loc_args.len()))); - let iter = loc_args.into_iter().zip(field_symbols.iter()).rev(); - let result = assign_to_symbols(env, procs, layout_cache, iter, result); - - if has_closure { - let partial_proc = procs.partial_procs.get_symbol(proc_name).unwrap(); - - let captured = match partial_proc.captured_symbols { - CapturedSymbols::None => &[], - CapturedSymbols::Captured(slice) => slice, - }; - - construct_closure_data( - env, - lambda_set, - proc_name, - captured.iter(), - closure_argument, - env.arena.alloc(result), - ) - } else { - result - } - } - PendingSpecializations::Making => { - let opt_partial_proc = procs.partial_procs.symbol_to_id(proc_name); - - let field_symbols = field_symbols.into_bump_slice(); - - match opt_partial_proc { - Some(partial_proc) => { - // Mark this proc as in-progress, so if we're dealing with - // mutually recursive functions, we don't loop forever. - // (We had a bug around this before this system existed!) - procs - .specialized - .mark_in_progress(proc_name, top_level_layout); - - match specialize_variable( - env, - procs, - proc_name, - layout_cache, - fn_var, - &[], - partial_proc, - ) { - Ok((proc, layout)) => { - // now we just call our freshly-specialized function - call_specialized_proc( - env, - procs, - proc_name, - proc, - lambda_set, - layout, - field_symbols, - loc_args, - layout_cache, - assigned, - hole, - ) - } - Err(SpecializeFailure { attempted_layout }) => { - let proc = generate_runtime_error_function( - env, - proc_name, - attempted_layout, - ); - - call_specialized_proc( - env, - procs, - proc_name, - proc, - lambda_set, - attempted_layout, - field_symbols, - loc_args, - layout_cache, - assigned, - hole, - ) - } - } - } - - None => { - unreachable!("Proc name {:?} is invalid", proc_name) - } - } - } - } - } -} - -#[allow(clippy::too_many_arguments)] -fn call_by_name_module_thunk<'a>( - env: &mut Env<'a, '_>, - procs: &mut Procs<'a>, - fn_var: Variable, - proc_name: Symbol, - ret_layout: &'a Layout<'a>, - layout_cache: &mut LayoutCache<'a>, - assigned: Symbol, - hole: &'a Stmt<'a>, -) -> Stmt<'a> { - let top_level_layout = ProcLayout::new(env.arena, &[], *ret_layout); - - let inner_layout = *ret_layout; - - // If we've already specialized this one, no further work is needed. - let already_specialized = procs - .specialized - .is_specialized(proc_name, &top_level_layout); - - if already_specialized { - force_thunk(env, proc_name, inner_layout, assigned, hole) - } else { - // When requested (that is, when procs.pending_specializations is `Some`), - // store a pending specialization rather than specializing immediately. - // - // We do this so that we can do specialization in two passes: first, - // build the mono_expr with all the specialized calls in place (but - // no specializations performed yet), and then second, *after* - // de-duplicating requested specializations (since multiple modules - // which could be getting monomorphized in parallel might request - // the same specialization independently), we work through the - // queue of pending specializations to complete each specialization - // exactly once. - if procs.is_module_thunk(proc_name) { - debug_assert!(top_level_layout.arguments.is_empty()); - } - - match &mut procs.pending_specializations { - PendingSpecializations::Finding(suspended) => { - debug_assert!(!env.is_imported_symbol(proc_name)); - - // register the pending specialization, so this gets code genned later - suspended.specialization(env.subs, proc_name, top_level_layout, fn_var); - - force_thunk(env, proc_name, inner_layout, assigned, hole) - } - PendingSpecializations::Making => { - let opt_partial_proc = procs.partial_procs.symbol_to_id(proc_name); - - match opt_partial_proc { - Some(partial_proc) => { - // Mark this proc as in-progress, so if we're dealing with - // mutually recursive functions, we don't loop forever. - // (We had a bug around this before this system existed!) - procs - .specialized - .mark_in_progress(proc_name, top_level_layout); - - match specialize_variable( - env, - procs, - proc_name, - layout_cache, - fn_var, - &[], - partial_proc, - ) { - Ok((proc, raw_layout)) => { - debug_assert!( - raw_layout.is_zero_argument_thunk(), - "but actually {:?}", - raw_layout - ); - - let was_present = procs - .specialized - .remove_specialized(proc_name, &top_level_layout); - debug_assert!(was_present); - - procs.specialized.insert_specialized( - proc_name, - top_level_layout, - proc, - ); - - force_thunk(env, proc_name, inner_layout, assigned, hole) - } - Err(SpecializeFailure { attempted_layout }) => { - let proc = generate_runtime_error_function( - env, - proc_name, - attempted_layout, - ); - - let was_present = procs - .specialized - .remove_specialized(proc_name, &top_level_layout); - debug_assert!(was_present); - - procs.specialized.insert_specialized( - proc_name, - top_level_layout, - proc, - ); - - force_thunk(env, proc_name, inner_layout, assigned, hole) - } - } - } - - None => { - unreachable!("Proc name {:?} is invalid", proc_name) - } - } - } - } - } -} - -#[allow(clippy::too_many_arguments)] -fn call_specialized_proc<'a>( - env: &mut Env<'a, '_>, - procs: &mut Procs<'a>, - proc_name: Symbol, - proc: Proc<'a>, - lambda_set: LambdaSet<'a>, - layout: RawFunctionLayout<'a>, - field_symbols: &'a [Symbol], - loc_args: std::vec::Vec<(Variable, Loc)>, - layout_cache: &mut LayoutCache<'a>, - assigned: Symbol, - hole: &'a Stmt<'a>, -) -> Stmt<'a> { - let function_layout = ProcLayout::from_raw(env.arena, layout); - - procs - .specialized - .insert_specialized(proc_name, function_layout, proc); - - if field_symbols.is_empty() { - debug_assert!(loc_args.is_empty()); - - // This happens when we return a function, e.g. - // - // foo = Num.add - // - // Even though the layout (and type) are functions, - // there are no arguments. This confuses our IR, - // and we have to fix it here. - match layout { - RawFunctionLayout::Function(_, lambda_set, _) => { - // when the body is a closure, the function will return the closure environment - let call = self::Call { - call_type: CallType::ByName { - name: proc_name, - ret_layout: env.arena.alloc(function_layout.result), - arg_layouts: function_layout.arguments, - specialization_id: env.next_call_specialization_id(), - }, - arguments: field_symbols, - }; - - // the closure argument is already added here (to get the right specialization) - // but now we need to remove it because the `match_on_lambda_set` will add it again - build_call(env, call, assigned, Layout::LambdaSet(lambda_set), hole) - } - RawFunctionLayout::ZeroArgumentThunk(_) => { - unreachable!() - } - } - } else { - let iter = loc_args.into_iter().rev().zip(field_symbols.iter().rev()); - - match procs - .partial_procs - .get_symbol(proc_name) - .map(|pp| &pp.captured_symbols) - { - Some(&CapturedSymbols::Captured(captured_symbols)) => { - let symbols = - Vec::from_iter_in(captured_symbols.iter(), env.arena).into_bump_slice(); - - let closure_data_symbol = env.unique_symbol(); - - // the closure argument is already added here (to get the right specialization) - // but now we need to remove it because the `match_on_lambda_set` will add it again - let mut argument_layouts = - Vec::from_iter_in(function_layout.arguments.iter().copied(), env.arena); - argument_layouts.pop().unwrap(); - - debug_assert_eq!(argument_layouts.len(), field_symbols.len(),); - - let new_hole = match_on_lambda_set( - env, - lambda_set, - closure_data_symbol, - field_symbols, - argument_layouts.into_bump_slice(), - env.arena.alloc(function_layout.result), - assigned, - hole, - ); - - let result = construct_closure_data( - env, - lambda_set, - proc_name, - symbols.iter().copied(), - closure_data_symbol, - env.arena.alloc(new_hole), - ); - - assign_to_symbols(env, procs, layout_cache, iter, result) - } - _ => { - debug_assert_eq!( - function_layout.arguments.len(), - field_symbols.len(), - "function {:?} with layout {:#?} expects {:?} arguments, but is applied to {:?}", - proc_name, - function_layout, - function_layout.arguments.len(), - field_symbols.len(), - ); - - let call = self::Call { - call_type: CallType::ByName { - name: proc_name, - ret_layout: env.arena.alloc(function_layout.result), - arg_layouts: function_layout.arguments, - specialization_id: env.next_call_specialization_id(), - }, - arguments: field_symbols, - }; - - let result = build_call(env, call, assigned, function_layout.result, hole); - - assign_to_symbols(env, procs, layout_cache, iter, result) - } - } - } -} - -/// A pattern, including possible problems (e.g. shadowing) so that -/// codegen can generate a runtime error if this pattern is reached. -#[derive(Clone, Debug, PartialEq)] -pub enum Pattern<'a> { - Identifier(Symbol), - Underscore, - IntLiteral([u8; 16], IntWidth), - FloatLiteral(u64, FloatWidth), - DecimalLiteral([u8; 16]), - BitLiteral { - value: bool, - tag_name: TagName, - union: roc_exhaustive::Union, - }, - EnumLiteral { - tag_id: u8, - tag_name: TagName, - union: roc_exhaustive::Union, - }, - StrLiteral(Box), - - RecordDestructure(Vec<'a, RecordDestruct<'a>>, &'a [Layout<'a>]), - NewtypeDestructure { - tag_name: TagName, - arguments: Vec<'a, (Pattern<'a>, Layout<'a>)>, - }, - AppliedTag { - tag_name: TagName, - tag_id: TagIdIntType, - arguments: Vec<'a, (Pattern<'a>, Layout<'a>)>, - layout: UnionLayout<'a>, - union: roc_exhaustive::Union, - }, - OpaqueUnwrap { - opaque: Symbol, - argument: Box<(Pattern<'a>, Layout<'a>)>, - }, -} - -#[derive(Clone, Debug, PartialEq)] -pub struct RecordDestruct<'a> { - pub label: Lowercase, - pub variable: Variable, - pub layout: Layout<'a>, - pub typ: DestructType<'a>, -} - -#[derive(Clone, Debug, PartialEq)] -pub enum DestructType<'a> { - Required(Symbol), - Guard(Pattern<'a>), -} - -#[derive(Clone, Debug, PartialEq)] -pub struct WhenBranch<'a> { - pub patterns: Vec<'a, Pattern<'a>>, - pub value: Expr<'a>, - pub guard: Option>, -} - -#[allow(clippy::type_complexity)] -fn from_can_pattern<'a>( - env: &mut Env<'a, '_>, - procs: &mut Procs<'a>, - layout_cache: &mut LayoutCache<'a>, - can_pattern: &roc_can::pattern::Pattern, -) -> Result< - ( - Pattern<'a>, - Vec<'a, (Symbol, Variable, roc_can::expr::Expr)>, - ), - RuntimeError, -> { - let mut assignments = Vec::new_in(env.arena); - let pattern = from_can_pattern_help(env, procs, layout_cache, can_pattern, &mut assignments)?; - - Ok((pattern, assignments)) -} - -fn from_can_pattern_help<'a>( - env: &mut Env<'a, '_>, - procs: &mut Procs<'a>, - layout_cache: &mut LayoutCache<'a>, - can_pattern: &roc_can::pattern::Pattern, - assignments: &mut Vec<'a, (Symbol, Variable, roc_can::expr::Expr)>, -) -> Result, RuntimeError> { - use roc_can::pattern::Pattern::*; - - match can_pattern { - Underscore => Ok(Pattern::Underscore), - Identifier(symbol) => Ok(Pattern::Identifier(*symbol)), - AbilityMemberSpecialization { ident, .. } => Ok(Pattern::Identifier(*ident)), - IntLiteral(_, precision_var, _, int, _bound) => { - match num_argument_to_int_or_float(env.subs, env.target_info, *precision_var, false) { - IntOrFloat::Int(precision) => match *int { - IntValue::I128(n) | IntValue::U128(n) => Ok(Pattern::IntLiteral(n, precision)), - }, - other => { - panic!( - "Invalid precision for int pattern: {:?} has {:?}", - can_pattern, other - ) - } - } - } - FloatLiteral(_, precision_var, float_str, float, _bound) => { - // TODO: Can I reuse num_argument_to_int_or_float here if I pass in true? - match num_argument_to_int_or_float(env.subs, env.target_info, *precision_var, true) { - IntOrFloat::Int(_) => { - panic!("Invalid precision for float pattern {:?}", precision_var) - } - IntOrFloat::Float(precision) => { - Ok(Pattern::FloatLiteral(f64::to_bits(*float), precision)) - } - IntOrFloat::DecimalFloatType => { - let dec = match RocDec::from_str(float_str) { - Some(d) => d, - None => panic!( - r"Invalid decimal for float literal = {}. TODO: Make this a nice, user-friendly error message", - float_str - ), - }; - Ok(Pattern::DecimalLiteral(dec.to_ne_bytes())) - } - } - } - StrLiteral(v) => Ok(Pattern::StrLiteral(v.clone())), - SingleQuote(c) => Ok(Pattern::IntLiteral( - (*c as i128).to_ne_bytes(), - IntWidth::I32, - )), - Shadowed(region, ident, _new_symbol) => Err(RuntimeError::Shadowing { - original_region: *region, - shadow: ident.clone(), - kind: ShadowKind::Variable, - }), - UnsupportedPattern(region) => Err(RuntimeError::UnsupportedPattern(*region)), - MalformedPattern(_problem, region) => { - // TODO preserve malformed problem information here? - Err(RuntimeError::UnsupportedPattern(*region)) - } - OpaqueNotInScope(loc_ident) => { - // TODO(opaques) should be `RuntimeError::OpaqueNotDefined` - Err(RuntimeError::UnsupportedPattern(loc_ident.region)) - } - NumLiteral(var, num_str, num, _bound) => { - match num_argument_to_int_or_float(env.subs, env.target_info, *var, false) { - IntOrFloat::Int(precision) => Ok(match num { - IntValue::I128(num) | IntValue::U128(num) => { - Pattern::IntLiteral(*num, precision) - } - }), - IntOrFloat::Float(precision) => { - // TODO: this may be lossy - let num = match *num { - IntValue::I128(n) => f64::to_bits(i128::from_ne_bytes(n) as f64), - IntValue::U128(n) => f64::to_bits(u128::from_ne_bytes(n) as f64), - }; - Ok(Pattern::FloatLiteral(num, precision)) - } - IntOrFloat::DecimalFloatType => { - let dec = match RocDec::from_str(num_str) { - Some(d) => d, - None => panic!("Invalid decimal for float literal = {}. TODO: Make this a nice, user-friendly error message", num_str), - }; - Ok(Pattern::DecimalLiteral(dec.to_ne_bytes())) - } - } - } - - AppliedTag { - whole_var, - tag_name, - arguments, - .. - } => { - use crate::layout::UnionVariant::*; - use roc_exhaustive::Union; - - let res_variant = - crate::layout::union_sorted_tags(env.arena, *whole_var, env.subs, env.target_info) - .map_err(Into::into); - - let variant = match res_variant { - Ok(cached) => cached, - Err(LayoutProblem::UnresolvedTypeVar(_)) => { - return Err(RuntimeError::UnresolvedTypeVar) - } - Err(LayoutProblem::Erroneous) => return Err(RuntimeError::ErroneousType), - }; - - let result = match variant { - Never => unreachable!( - "there is no pattern of type `[]`, union var {:?}", - *whole_var - ), - Unit | UnitWithArguments => Pattern::EnumLiteral { - tag_id: 0, - tag_name: tag_name.clone(), - union: Union { - render_as: RenderAs::Tag, - alternatives: vec![Ctor { - tag_id: TagId(0), - name: CtorName::Tag(tag_name.clone()), - arity: 0, - }], - }, - }, - BoolUnion { ttrue, ffalse } => { - let (ttrue, ffalse) = (ttrue.expect_tag(), ffalse.expect_tag()); - Pattern::BitLiteral { - value: tag_name == &ttrue, - tag_name: tag_name.clone(), - union: Union { - render_as: RenderAs::Tag, - alternatives: vec![ - Ctor { - tag_id: TagId(0), - name: CtorName::Tag(ffalse), - arity: 0, - }, - Ctor { - tag_id: TagId(1), - name: CtorName::Tag(ttrue), - arity: 0, - }, - ], - }, - } - } - ByteUnion(tag_names) => { - let tag_id = tag_names - .iter() - .position(|key| tag_name == key.expect_tag_ref()) - .expect("tag must be in its own type"); - - let mut ctors = std::vec::Vec::with_capacity(tag_names.len()); - for (i, tag_name) in tag_names.into_iter().enumerate() { - ctors.push(Ctor { - tag_id: TagId(i as _), - name: CtorName::Tag(tag_name.expect_tag()), - arity: 0, - }) - } - - let union = roc_exhaustive::Union { - render_as: RenderAs::Tag, - alternatives: ctors, - }; - - Pattern::EnumLiteral { - tag_id: tag_id as u8, - tag_name: tag_name.clone(), - union, - } - } - Newtype { - arguments: field_layouts, - .. - } => { - let mut arguments = arguments.clone(); - - arguments.sort_by(|arg1, arg2| { - let size1 = layout_cache - .from_var(env.arena, arg1.0, env.subs) - .map(|x| x.alignment_bytes(env.target_info)) - .unwrap_or(0); - - let size2 = layout_cache - .from_var(env.arena, arg2.0, env.subs) - .map(|x| x.alignment_bytes(env.target_info)) - .unwrap_or(0); - - size2.cmp(&size1) - }); - - let mut mono_args = Vec::with_capacity_in(arguments.len(), env.arena); - for ((_, loc_pat), layout) in arguments.iter().zip(field_layouts.iter()) { - mono_args.push(( - from_can_pattern_help( - env, - procs, - layout_cache, - &loc_pat.value, - assignments, - )?, - *layout, - )); - } - - Pattern::NewtypeDestructure { - tag_name: tag_name.clone(), - arguments: mono_args, - } - } - Wrapped(variant) => { - let (tag_id, argument_layouts) = variant.tag_name_to_id(tag_name); - let number_of_tags = variant.number_of_tags(); - let mut ctors = std::vec::Vec::with_capacity(number_of_tags); - - let arguments = { - let mut temp = arguments.clone(); - - temp.sort_by(|arg1, arg2| { - let layout1 = - layout_cache.from_var(env.arena, arg1.0, env.subs).unwrap(); - let layout2 = - layout_cache.from_var(env.arena, arg2.0, env.subs).unwrap(); - - let size1 = layout1.alignment_bytes(env.target_info); - let size2 = layout2.alignment_bytes(env.target_info); - - size2.cmp(&size1) - }); - - temp - }; - - // we must derive the union layout from the whole_var, building it up - // from `layouts` would unroll recursive tag unions, and that leads to - // problems down the line because we hash layouts and an unrolled - // version is not the same as the minimal version. - let layout = match layout_cache.from_var(env.arena, *whole_var, env.subs) { - Ok(Layout::Union(ul)) => ul, - _ => unreachable!(), - }; - - use WrappedVariant::*; - match variant { - NonRecursive { - sorted_tag_layouts: ref tags, - } => { - debug_assert!(tags.len() > 1); - - for (i, (tag_name, args)) in tags.iter().enumerate() { - ctors.push(Ctor { - tag_id: TagId(i as _), - name: CtorName::Tag(tag_name.expect_tag_ref().clone()), - arity: args.len(), - }) - } - - let union = roc_exhaustive::Union { - render_as: RenderAs::Tag, - alternatives: ctors, - }; - - let mut mono_args = Vec::with_capacity_in(arguments.len(), env.arena); - - debug_assert_eq!( - arguments.len(), - argument_layouts.len(), - "The {:?} tag got {} arguments, but its layout expects {}!", - tag_name, - arguments.len(), - argument_layouts.len(), - ); - let it = argument_layouts.iter(); - - for ((_, loc_pat), layout) in arguments.iter().zip(it) { - mono_args.push(( - from_can_pattern_help( - env, - procs, - layout_cache, - &loc_pat.value, - assignments, - )?, - *layout, - )); - } - - Pattern::AppliedTag { - tag_name: tag_name.clone(), - tag_id: tag_id as _, - arguments: mono_args, - union, - layout, - } - } - - Recursive { - sorted_tag_layouts: ref tags, - } => { - debug_assert!(tags.len() > 1); - - for (i, (tag_name, args)) in tags.iter().enumerate() { - ctors.push(Ctor { - tag_id: TagId(i as _), - name: CtorName::Tag(tag_name.expect_tag_ref().clone()), - // don't include tag discriminant in arity - arity: args.len() - 1, - }) - } - - let union = roc_exhaustive::Union { - render_as: RenderAs::Tag, - alternatives: ctors, - }; - - let mut mono_args = Vec::with_capacity_in(arguments.len(), env.arena); - - debug_assert_eq!(arguments.len(), argument_layouts.len()); - let it = argument_layouts.iter(); - - for ((_, loc_pat), layout) in arguments.iter().zip(it) { - mono_args.push(( - from_can_pattern_help( - env, - procs, - layout_cache, - &loc_pat.value, - assignments, - )?, - *layout, - )); - } - - Pattern::AppliedTag { - tag_name: tag_name.clone(), - tag_id: tag_id as _, - arguments: mono_args, - union, - layout, - } - } - - NonNullableUnwrapped { - tag_name: w_tag_name, - fields, - } => { - debug_assert_eq!(w_tag_name.expect_tag_ref(), tag_name); - - ctors.push(Ctor { - tag_id: TagId(0), - name: CtorName::Tag(tag_name.clone()), - arity: fields.len(), - }); - - let union = roc_exhaustive::Union { - render_as: RenderAs::Tag, - alternatives: ctors, - }; - - let mut mono_args = Vec::with_capacity_in(arguments.len(), env.arena); - - debug_assert_eq!(arguments.len(), argument_layouts.len()); - let it = argument_layouts.iter(); - - for ((_, loc_pat), layout) in arguments.iter().zip(it) { - mono_args.push(( - from_can_pattern_help( - env, - procs, - layout_cache, - &loc_pat.value, - assignments, - )?, - *layout, - )); - } - - Pattern::AppliedTag { - tag_name: tag_name.clone(), - tag_id: tag_id as _, - arguments: mono_args, - union, - layout, - } - } - - NullableWrapped { - sorted_tag_layouts: ref tags, - nullable_id, - nullable_name, - } => { - debug_assert!(!tags.is_empty()); - - let mut i = 0; - for (tag_name, args) in tags.iter() { - if i == nullable_id as usize { - ctors.push(Ctor { - tag_id: TagId(i as _), - name: CtorName::Tag(nullable_name.expect_tag_ref().clone()), - // don't include tag discriminant in arity - arity: 0, - }); - - i += 1; - } - - ctors.push(Ctor { - tag_id: TagId(i as _), - name: CtorName::Tag(tag_name.expect_tag_ref().clone()), - // don't include tag discriminant in arity - arity: args.len() - 1, - }); - - i += 1; - } - - if i == nullable_id as usize { - ctors.push(Ctor { - tag_id: TagId(i as _), - name: CtorName::Tag(nullable_name.expect_tag_ref().clone()), - // don't include tag discriminant in arity - arity: 0, - }); - } - - let union = roc_exhaustive::Union { - render_as: RenderAs::Tag, - alternatives: ctors, - }; - - let mut mono_args = Vec::with_capacity_in(arguments.len(), env.arena); - - let it = if tag_name == nullable_name.expect_tag_ref() { - [].iter() - } else { - argument_layouts.iter() - }; - - for ((_, loc_pat), layout) in arguments.iter().zip(it) { - mono_args.push(( - from_can_pattern_help( - env, - procs, - layout_cache, - &loc_pat.value, - assignments, - )?, - *layout, - )); - } - - Pattern::AppliedTag { - tag_name: tag_name.clone(), - tag_id: tag_id as _, - arguments: mono_args, - union, - layout, - } - } - - NullableUnwrapped { - other_fields, - nullable_id, - nullable_name, - other_name: _, - } => { - debug_assert!(!other_fields.is_empty()); - - ctors.push(Ctor { - tag_id: TagId(nullable_id as _), - name: CtorName::Tag(nullable_name.expect_tag_ref().clone()), - arity: 0, - }); - - ctors.push(Ctor { - tag_id: TagId(!nullable_id as _), - name: CtorName::Tag(nullable_name.expect_tag_ref().clone()), - // FIXME drop tag - arity: other_fields.len() - 1, - }); - - let union = roc_exhaustive::Union { - render_as: RenderAs::Tag, - alternatives: ctors, - }; - - let mut mono_args = Vec::with_capacity_in(arguments.len(), env.arena); - - let it = if tag_name == nullable_name.expect_tag_ref() { - [].iter() - } else { - // FIXME drop tag - argument_layouts.iter() - }; - - for ((_, loc_pat), layout) in arguments.iter().zip(it) { - mono_args.push(( - from_can_pattern_help( - env, - procs, - layout_cache, - &loc_pat.value, - assignments, - )?, - *layout, - )); - } - - Pattern::AppliedTag { - tag_name: tag_name.clone(), - tag_id: tag_id as _, - arguments: mono_args, - union, - layout, - } - } - } - } - }; - - Ok(result) - } - - UnwrappedOpaque { - opaque, argument, .. - } => { - let (arg_var, loc_arg_pattern) = &(**argument); - let arg_layout = layout_cache - .from_var(env.arena, *arg_var, env.subs) - .unwrap(); - let mono_arg_pattern = from_can_pattern_help( - env, - procs, - layout_cache, - &loc_arg_pattern.value, - assignments, - )?; - Ok(Pattern::OpaqueUnwrap { - opaque: *opaque, - argument: Box::new((mono_arg_pattern, arg_layout)), - }) - } - - RecordDestructure { - whole_var, - destructs, - .. - } => { - // sorted fields based on the type - let sorted_fields = - crate::layout::sort_record_fields(env.arena, *whole_var, env.subs, env.target_info) - .map_err(RuntimeError::from)?; - - // sorted fields based on the destruct - let mut mono_destructs = Vec::with_capacity_in(destructs.len(), env.arena); - let mut destructs_by_label = BumpMap::with_capacity_in(destructs.len(), env.arena); - destructs_by_label.extend(destructs.iter().map(|x| (&x.value.label, x))); - - let mut field_layouts = Vec::with_capacity_in(sorted_fields.len(), env.arena); - - // next we step through both sequences of fields. The outer loop is the sequence based - // on the type, since not all fields need to actually be destructured in the source - // language. - // - // However in mono patterns, we do destruct all patterns (but use Underscore) when - // in the source the field is not matche in the source language. - // - // Optional fields somewhat complicate the matter here - - for (label, variable, res_layout) in sorted_fields.into_iter() { - match res_layout { - Ok(field_layout) => { - // the field is non-optional according to the type - - match destructs_by_label.remove(&label) { - Some(destruct) => { - // this field is destructured by the pattern - mono_destructs.push(from_can_record_destruct( - env, - procs, - layout_cache, - &destruct.value, - field_layout, - assignments, - )?); - } - None => { - // this field is not destructured by the pattern - // put in an underscore - mono_destructs.push(RecordDestruct { - label: label.clone(), - variable, - layout: field_layout, - typ: DestructType::Guard(Pattern::Underscore), - }); - } - } - - // the layout of this field is part of the layout of the record - field_layouts.push(field_layout); - } - Err(field_layout) => { - // the field is optional according to the type - match destructs_by_label.remove(&label) { - Some(destruct) => { - // this field is destructured by the pattern - match &destruct.value.typ { - roc_can::pattern::DestructType::Optional(_, loc_expr) => { - // if we reach this stage, the optional field is not present - // so we push the default assignment into the branch - assignments.push(( - destruct.value.symbol, - variable, - loc_expr.value.clone(), - )); - } - _ => unreachable!( - "only optional destructs can be optional fields" - ), - }; - } - None => { - // this field is not destructured by the pattern - // put in an underscore - mono_destructs.push(RecordDestruct { - label: label.clone(), - variable, - layout: field_layout, - typ: DestructType::Guard(Pattern::Underscore), - }); - } - } - } - } - } - - for (_, destruct) in destructs_by_label.drain() { - // this destruct is not in the type, but is in the pattern - // it must be an optional field, and we will use the default - match &destruct.value.typ { - roc_can::pattern::DestructType::Optional(field_var, loc_expr) => { - // TODO these don't match up in the uniqueness inference; when we remove - // that, reinstate this assert! - // - // dbg!(&env.subs.get_content_without_compacting(*field_var)); - // dbg!(&env.subs.get_content_without_compacting(destruct.var).content); - // debug_assert_eq!( - // env.subs.get_root_key_without_compacting(*field_var), - // env.subs.get_root_key_without_compacting(destruct.value.var) - // ); - assignments.push(( - destruct.value.symbol, - // destruct.value.var, - *field_var, - loc_expr.value.clone(), - )); - } - _ => unreachable!("only optional destructs can be optional fields"), - } - } - - Ok(Pattern::RecordDestructure( - mono_destructs, - field_layouts.into_bump_slice(), - )) - } - } -} - -fn from_can_record_destruct<'a>( - env: &mut Env<'a, '_>, - procs: &mut Procs<'a>, - layout_cache: &mut LayoutCache<'a>, - can_rd: &roc_can::pattern::RecordDestruct, - field_layout: Layout<'a>, - assignments: &mut Vec<'a, (Symbol, Variable, roc_can::expr::Expr)>, -) -> Result, RuntimeError> { - Ok(RecordDestruct { - label: can_rd.label.clone(), - variable: can_rd.var, - layout: field_layout, - typ: match &can_rd.typ { - roc_can::pattern::DestructType::Required => DestructType::Required(can_rd.symbol), - roc_can::pattern::DestructType::Optional(_, _) => { - // if we reach this stage, the optional field is present - DestructType::Required(can_rd.symbol) - } - roc_can::pattern::DestructType::Guard(_, loc_pattern) => DestructType::Guard( - from_can_pattern_help(env, procs, layout_cache, &loc_pattern.value, assignments)?, - ), - }, - }) -} - -#[derive(Debug)] -pub enum IntOrFloat { - Int(IntWidth), - Float(FloatWidth), - DecimalFloatType, -} - -/// Given the `a` in `Num a`, determines whether it's an int or a float -pub fn num_argument_to_int_or_float( - subs: &Subs, - target_info: TargetInfo, - var: Variable, - known_to_be_float: bool, -) -> IntOrFloat { - match subs.get_content_without_compacting(var) { - Content::FlexVar(_) | Content::RigidVar(_) if known_to_be_float => { - IntOrFloat::Float(FloatWidth::F64) - } - Content::FlexVar(_) | Content::RigidVar(_) => IntOrFloat::Int(IntWidth::I64), // We default (Num *) to I64 - - Content::Alias(Symbol::NUM_INTEGER, args, _, _) => { - debug_assert!(args.len() == 1); - - // Recurse on the second argument - let var = subs[args.all_variables().into_iter().next().unwrap()]; - num_argument_to_int_or_float(subs, target_info, var, false) - } - - other @ Content::Alias(symbol, args, _, _) => { - if let Some(int_width) = IntWidth::try_from_symbol(*symbol) { - return IntOrFloat::Int(int_width); - } - - if let Some(float_width) = FloatWidth::try_from_symbol(*symbol) { - return IntOrFloat::Float(float_width); - } - - match *symbol { - Symbol::NUM_FLOATINGPOINT => { - debug_assert!(args.len() == 1); - - // Recurse on the second argument - let var = subs[args.all_variables().into_iter().next().unwrap()]; - num_argument_to_int_or_float(subs, target_info, var, true) - } - - Symbol::NUM_DECIMAL => IntOrFloat::DecimalFloatType, - - Symbol::NUM_NAT | Symbol::NUM_NATURAL => { - let int_width = match target_info.ptr_width() { - roc_target::PtrWidth::Bytes4 => IntWidth::U32, - roc_target::PtrWidth::Bytes8 => IntWidth::U64, - }; - - IntOrFloat::Int(int_width) - } - - _ => panic!( - "Unrecognized Num type argument for var {:?} with Content: {:?}", - var, other - ), - } - } - - other => { - panic!( - "Unrecognized Num type argument for var {:?} with Content: {:?}", - var, other - ); - } - } -} - -type ToLowLevelCallArguments<'a> = (Symbol, Symbol, Option>, CallSpecId, UpdateModeId); - -/// Use the lambda set to figure out how to make a lowlevel call -#[allow(clippy::too_many_arguments)] -fn lowlevel_match_on_lambda_set<'a, ToLowLevelCall>( - env: &mut Env<'a, '_>, - lambda_set: LambdaSet<'a>, - op: LowLevel, - closure_data_symbol: Symbol, - to_lowlevel_call: ToLowLevelCall, - return_layout: Layout<'a>, - assigned: Symbol, - hole: &'a Stmt<'a>, -) -> Stmt<'a> -where - ToLowLevelCall: Fn(ToLowLevelCallArguments<'a>) -> Call<'a> + Copy, -{ - match lambda_set.runtime_representation() { - Layout::Union(union_layout) => { - let closure_tag_id_symbol = env.unique_symbol(); - - let result = lowlevel_union_lambda_set_to_switch( - env, - lambda_set.set, - closure_tag_id_symbol, - union_layout.tag_id_layout(), - closure_data_symbol, - lambda_set.is_represented(), - to_lowlevel_call, - return_layout, - assigned, - hole, - ); - - // extract & assign the closure_tag_id_symbol - let expr = Expr::GetTagId { - structure: closure_data_symbol, - union_layout, - }; - - Stmt::Let( - closure_tag_id_symbol, - expr, - union_layout.tag_id_layout(), - env.arena.alloc(result), - ) - } - Layout::Struct { .. } => match lambda_set.set.get(0) { - Some((function_symbol, _)) => { - let call_spec_id = env.next_call_specialization_id(); - let update_mode = env.next_update_mode_id(); - let call = to_lowlevel_call(( - *function_symbol, - closure_data_symbol, - lambda_set.is_represented(), - call_spec_id, - update_mode, - )); - - build_call(env, call, assigned, return_layout, env.arena.alloc(hole)) - } - None => { - eprintln!( - "a function passed to `{:?}` LowLevel call has an empty lambda set! - The most likely reason is that some symbol you use is not in scope. - ", - op - ); - - hole.clone() - } - }, - Layout::Builtin(Builtin::Bool) => { - let closure_tag_id_symbol = closure_data_symbol; - - lowlevel_enum_lambda_set_to_switch( - env, - lambda_set.set, - closure_tag_id_symbol, - Layout::Builtin(Builtin::Bool), - closure_data_symbol, - lambda_set.is_represented(), - to_lowlevel_call, - return_layout, - assigned, - hole, - ) - } - Layout::Builtin(Builtin::Int(IntWidth::U8)) => { - let closure_tag_id_symbol = closure_data_symbol; - - lowlevel_enum_lambda_set_to_switch( - env, - lambda_set.set, - closure_tag_id_symbol, - Layout::Builtin(Builtin::Int(IntWidth::U8)), - closure_data_symbol, - lambda_set.is_represented(), - to_lowlevel_call, - return_layout, - assigned, - hole, - ) - } - other => todo!("{:?}", other), - } -} - -#[allow(clippy::too_many_arguments)] -fn lowlevel_union_lambda_set_to_switch<'a, ToLowLevelCall>( - env: &mut Env<'a, '_>, - lambda_set: &'a [(Symbol, &'a [Layout<'a>])], - closure_tag_id_symbol: Symbol, - closure_tag_id_layout: Layout<'a>, - closure_data_symbol: Symbol, - closure_env_layout: Option>, - to_lowlevel_call: ToLowLevelCall, - return_layout: Layout<'a>, - assigned: Symbol, - hole: &'a Stmt<'a>, -) -> Stmt<'a> -where - ToLowLevelCall: Fn(ToLowLevelCallArguments<'a>) -> Call<'a> + Copy, -{ - debug_assert!(!lambda_set.is_empty()); - - let join_point_id = JoinPointId(env.unique_symbol()); - - let mut branches = Vec::with_capacity_in(lambda_set.len(), env.arena); - - for (i, (function_symbol, _)) in lambda_set.iter().enumerate() { - let assigned = env.unique_symbol(); - - let hole = Stmt::Jump(join_point_id, env.arena.alloc([assigned])); - - let call_spec_id = env.next_call_specialization_id(); - let update_mode = env.next_update_mode_id(); - let call = to_lowlevel_call(( - *function_symbol, - closure_data_symbol, - closure_env_layout, - call_spec_id, - update_mode, - )); - let stmt = build_call(env, call, assigned, return_layout, env.arena.alloc(hole)); - - branches.push((i as u64, BranchInfo::None, stmt)); - } - - let default_branch = { - let (_, info, stmt) = branches.pop().unwrap(); - - (info, &*env.arena.alloc(stmt)) - }; - - let switch = Stmt::Switch { - cond_symbol: closure_tag_id_symbol, - cond_layout: closure_tag_id_layout, - branches: branches.into_bump_slice(), - default_branch, - ret_layout: return_layout, - }; - - let param = Param { - symbol: assigned, - layout: return_layout, - borrow: false, - }; - - Stmt::Join { - id: join_point_id, - parameters: &*env.arena.alloc([param]), - body: hole, - remainder: env.arena.alloc(switch), - } -} - -/// Use the lambda set to figure out how to make a call-by-name -#[allow(clippy::too_many_arguments)] -fn match_on_lambda_set<'a>( - env: &mut Env<'a, '_>, - lambda_set: LambdaSet<'a>, - closure_data_symbol: Symbol, - argument_symbols: &'a [Symbol], - argument_layouts: &'a [Layout<'a>], - return_layout: &'a Layout<'a>, - assigned: Symbol, - hole: &'a Stmt<'a>, -) -> Stmt<'a> { - match lambda_set.runtime_representation() { - Layout::Union(union_layout) => { - let closure_tag_id_symbol = env.unique_symbol(); - - let result = union_lambda_set_to_switch( - env, - lambda_set, - Layout::Union(union_layout), - closure_tag_id_symbol, - union_layout.tag_id_layout(), - closure_data_symbol, - argument_symbols, - argument_layouts, - return_layout, - assigned, - hole, - ); - - // extract & assign the closure_tag_id_symbol - let expr = Expr::GetTagId { - structure: closure_data_symbol, - union_layout, - }; - - Stmt::Let( - closure_tag_id_symbol, - expr, - union_layout.tag_id_layout(), - env.arena.alloc(result), - ) - } - Layout::Struct { - field_layouts, - field_order_hash, - } => { - let function_symbol = lambda_set.set[0].0; - - union_lambda_set_branch_help( - env, - function_symbol, - lambda_set, - closure_data_symbol, - Layout::Struct { - field_layouts, - field_order_hash, - }, - argument_symbols, - argument_layouts, - return_layout, - assigned, - hole, - ) - } - Layout::Builtin(Builtin::Bool) => { - let closure_tag_id_symbol = closure_data_symbol; - - enum_lambda_set_to_switch( - env, - lambda_set.set, - closure_tag_id_symbol, - Layout::Builtin(Builtin::Bool), - closure_data_symbol, - argument_symbols, - argument_layouts, - return_layout, - assigned, - hole, - ) - } - Layout::Builtin(Builtin::Int(IntWidth::U8)) => { - let closure_tag_id_symbol = closure_data_symbol; - - enum_lambda_set_to_switch( - env, - lambda_set.set, - closure_tag_id_symbol, - Layout::Builtin(Builtin::Int(IntWidth::U8)), - closure_data_symbol, - argument_symbols, - argument_layouts, - return_layout, - assigned, - hole, - ) - } - other => todo!("{:?}", other), - } -} - -#[allow(clippy::too_many_arguments)] -fn union_lambda_set_to_switch<'a>( - env: &mut Env<'a, '_>, - lambda_set: LambdaSet<'a>, - closure_layout: Layout<'a>, - closure_tag_id_symbol: Symbol, - closure_tag_id_layout: Layout<'a>, - closure_data_symbol: Symbol, - argument_symbols: &'a [Symbol], - argument_layouts: &'a [Layout<'a>], - return_layout: &'a Layout<'a>, - assigned: Symbol, - hole: &'a Stmt<'a>, -) -> Stmt<'a> { - if lambda_set.set.is_empty() { - // NOTE this can happen if there is a type error somewhere. Since the lambda set is empty, - // there is really nothing we can do here. We generate a runtime error here which allows - // code gen to proceed. We then assume that we hit another (more descriptive) error before - // hitting this one - - let msg = "a Lambda Set isempty. Most likely there is a type error in your program."; - return Stmt::RuntimeError(msg); - } - - let join_point_id = JoinPointId(env.unique_symbol()); - - let mut branches = Vec::with_capacity_in(lambda_set.set.len(), env.arena); - - for (i, (function_symbol, _)) in lambda_set.set.iter().enumerate() { - let stmt = union_lambda_set_branch( - env, - lambda_set, - join_point_id, - *function_symbol, - closure_data_symbol, - closure_layout, - argument_symbols, - argument_layouts, - return_layout, - ); - branches.push((i as u64, BranchInfo::None, stmt)); - } - - let default_branch = { - let (_, info, stmt) = branches.pop().unwrap(); - - (info, &*env.arena.alloc(stmt)) - }; - - let switch = Stmt::Switch { - cond_symbol: closure_tag_id_symbol, - cond_layout: closure_tag_id_layout, - branches: branches.into_bump_slice(), - default_branch, - ret_layout: *return_layout, - }; - - let param = Param { - symbol: assigned, - layout: *return_layout, - borrow: false, - }; - - Stmt::Join { - id: join_point_id, - parameters: &*env.arena.alloc([param]), - body: hole, - remainder: env.arena.alloc(switch), - } -} - -#[allow(clippy::too_many_arguments)] -fn union_lambda_set_branch<'a>( - env: &mut Env<'a, '_>, - lambda_set: LambdaSet<'a>, - join_point_id: JoinPointId, - function_symbol: Symbol, - closure_data_symbol: Symbol, - closure_data_layout: Layout<'a>, - argument_symbols_slice: &'a [Symbol], - argument_layouts_slice: &'a [Layout<'a>], - return_layout: &'a Layout<'a>, -) -> Stmt<'a> { - let result_symbol = env.unique_symbol(); - - let hole = Stmt::Jump(join_point_id, env.arena.alloc([result_symbol])); - - union_lambda_set_branch_help( - env, - function_symbol, - lambda_set, - closure_data_symbol, - closure_data_layout, - argument_symbols_slice, - argument_layouts_slice, - return_layout, - result_symbol, - env.arena.alloc(hole), - ) -} - -#[allow(clippy::too_many_arguments)] -fn union_lambda_set_branch_help<'a>( - env: &mut Env<'a, '_>, - function_symbol: Symbol, - lambda_set: LambdaSet<'a>, - closure_data_symbol: Symbol, - closure_data_layout: Layout<'a>, - argument_symbols_slice: &'a [Symbol], - argument_layouts_slice: &'a [Layout<'a>], - return_layout: &'a Layout<'a>, - assigned: Symbol, - hole: &'a Stmt<'a>, -) -> Stmt<'a> { - let (argument_layouts, argument_symbols) = match closure_data_layout { - Layout::Struct { - field_layouts: &[], .. - } - | Layout::Builtin(Builtin::Bool) - | Layout::Builtin(Builtin::Int(IntWidth::U8)) => { - (argument_layouts_slice, argument_symbols_slice) - } - _ if lambda_set.member_does_not_need_closure_argument(function_symbol) => { - // sometimes unification causes a function that does not itself capture anything - // to still get a lambda set that does store information. We must not pass a closure - // argument in this case - - (argument_layouts_slice, argument_symbols_slice) - } - _ => { - // extend layouts with the layout of the closure environment - let mut argument_layouts = - Vec::with_capacity_in(argument_layouts_slice.len() + 1, env.arena); - argument_layouts.extend(argument_layouts_slice); - argument_layouts.push(Layout::LambdaSet(lambda_set)); - - // extend symbols with the symbol of the closure environment - let mut argument_symbols = - Vec::with_capacity_in(argument_symbols_slice.len() + 1, env.arena); - argument_symbols.extend(argument_symbols_slice); - argument_symbols.push(closure_data_symbol); - - ( - argument_layouts.into_bump_slice(), - argument_symbols.into_bump_slice(), - ) - } - }; - - // build the call - let call = self::Call { - call_type: CallType::ByName { - name: function_symbol, - ret_layout: return_layout, - arg_layouts: argument_layouts, - specialization_id: env.next_call_specialization_id(), - }, - arguments: argument_symbols, - }; - - build_call(env, call, assigned, *return_layout, hole) -} - -#[allow(clippy::too_many_arguments)] -fn enum_lambda_set_to_switch<'a>( - env: &mut Env<'a, '_>, - lambda_set: &'a [(Symbol, &'a [Layout<'a>])], - closure_tag_id_symbol: Symbol, - closure_tag_id_layout: Layout<'a>, - closure_data_symbol: Symbol, - argument_symbols: &'a [Symbol], - argument_layouts: &'a [Layout<'a>], - return_layout: &'a Layout<'a>, - assigned: Symbol, - hole: &'a Stmt<'a>, -) -> Stmt<'a> { - debug_assert!(!lambda_set.is_empty()); - - let join_point_id = JoinPointId(env.unique_symbol()); - - let mut branches = Vec::with_capacity_in(lambda_set.len(), env.arena); - - let closure_layout = closure_tag_id_layout; - - for (i, (function_symbol, _)) in lambda_set.iter().enumerate() { - let stmt = enum_lambda_set_branch( - env, - join_point_id, - *function_symbol, - closure_data_symbol, - closure_layout, - argument_symbols, - argument_layouts, - return_layout, - ); - branches.push((i as u64, BranchInfo::None, stmt)); - } - - let default_branch = { - let (_, info, stmt) = branches.pop().unwrap(); - - (info, &*env.arena.alloc(stmt)) - }; - - let switch = Stmt::Switch { - cond_symbol: closure_tag_id_symbol, - cond_layout: closure_tag_id_layout, - branches: branches.into_bump_slice(), - default_branch, - ret_layout: *return_layout, - }; - - let param = Param { - symbol: assigned, - layout: *return_layout, - borrow: false, - }; - - Stmt::Join { - id: join_point_id, - parameters: &*env.arena.alloc([param]), - body: hole, - remainder: env.arena.alloc(switch), - } -} - -#[allow(clippy::too_many_arguments)] -fn enum_lambda_set_branch<'a>( - env: &mut Env<'a, '_>, - join_point_id: JoinPointId, - function_symbol: Symbol, - closure_data_symbol: Symbol, - closure_data_layout: Layout<'a>, - argument_symbols_slice: &'a [Symbol], - argument_layouts_slice: &'a [Layout<'a>], - return_layout: &'a Layout<'a>, -) -> Stmt<'a> { - let result_symbol = env.unique_symbol(); - - let hole = Stmt::Jump(join_point_id, env.arena.alloc([result_symbol])); - - let assigned = result_symbol; - - let (argument_layouts, argument_symbols) = match closure_data_layout { - Layout::Struct { - field_layouts: &[], .. - } - | Layout::Builtin(Builtin::Bool) - | Layout::Builtin(Builtin::Int(IntWidth::U8)) => { - (argument_layouts_slice, argument_symbols_slice) - } - _ => { - // extend layouts with the layout of the closure environment - let mut argument_layouts = - Vec::with_capacity_in(argument_layouts_slice.len() + 1, env.arena); - argument_layouts.extend(argument_layouts_slice); - argument_layouts.push(closure_data_layout); - - // extend symbols with the symbol of the closure environment - let mut argument_symbols = - Vec::with_capacity_in(argument_symbols_slice.len() + 1, env.arena); - argument_symbols.extend(argument_symbols_slice); - argument_symbols.push(closure_data_symbol); - - ( - argument_layouts.into_bump_slice(), - argument_symbols.into_bump_slice(), - ) - } - }; - - let call = self::Call { - call_type: CallType::ByName { - name: function_symbol, - ret_layout: return_layout, - arg_layouts: argument_layouts, - specialization_id: env.next_call_specialization_id(), - }, - arguments: argument_symbols, - }; - build_call(env, call, assigned, *return_layout, env.arena.alloc(hole)) -} - -#[allow(clippy::too_many_arguments)] -fn lowlevel_enum_lambda_set_to_switch<'a, ToLowLevelCall>( - env: &mut Env<'a, '_>, - lambda_set: &'a [(Symbol, &'a [Layout<'a>])], - closure_tag_id_symbol: Symbol, - closure_tag_id_layout: Layout<'a>, - closure_data_symbol: Symbol, - closure_env_layout: Option>, - to_lowlevel_call: ToLowLevelCall, - return_layout: Layout<'a>, - assigned: Symbol, - hole: &'a Stmt<'a>, -) -> Stmt<'a> -where - ToLowLevelCall: Fn(ToLowLevelCallArguments<'a>) -> Call<'a> + Copy, -{ - debug_assert!(!lambda_set.is_empty()); - - let join_point_id = JoinPointId(env.unique_symbol()); - - let mut branches = Vec::with_capacity_in(lambda_set.len(), env.arena); - - for (i, (function_symbol, _)) in lambda_set.iter().enumerate() { - let result_symbol = env.unique_symbol(); - - let hole = Stmt::Jump(join_point_id, env.arena.alloc([result_symbol])); - - let call_spec_id = env.next_call_specialization_id(); - let update_mode = env.next_update_mode_id(); - let call = to_lowlevel_call(( - *function_symbol, - closure_data_symbol, - closure_env_layout, - call_spec_id, - update_mode, - )); - let stmt = build_call( - env, - call, - result_symbol, - return_layout, - env.arena.alloc(hole), - ); - - branches.push((i as u64, BranchInfo::None, stmt)); - } - - let default_branch = { - let (_, info, stmt) = branches.pop().unwrap(); - - (info, &*env.arena.alloc(stmt)) - }; - - let switch = Stmt::Switch { - cond_symbol: closure_tag_id_symbol, - cond_layout: closure_tag_id_layout, - branches: branches.into_bump_slice(), - default_branch, - ret_layout: return_layout, - }; - - let param = Param { - symbol: assigned, - layout: return_layout, - borrow: false, - }; - - Stmt::Join { - id: join_point_id, - parameters: &*env.arena.alloc([param]), - body: hole, - remainder: env.arena.alloc(switch), - } -} diff --git a/compiler/mono/src/layout.rs b/compiler/mono/src/layout.rs deleted file mode 100644 index 6663498d1a..0000000000 --- a/compiler/mono/src/layout.rs +++ /dev/null @@ -1,3082 +0,0 @@ -use crate::ir::Parens; -use bumpalo::collections::Vec; -use bumpalo::Bump; -use roc_builtins::bitcode::{FloatWidth, IntWidth}; -use roc_collections::all::{default_hasher, MutMap}; -use roc_error_macros::{internal_error, todo_abilities}; -use roc_module::ident::{Lowercase, TagName}; -use roc_module::symbol::{Interns, Symbol}; -use roc_problem::can::RuntimeError; -use roc_target::{PtrWidth, TargetInfo}; -use roc_types::pretty_print::ResolvedLambdaSet; -use roc_types::subs::{ - self, Content, FlatType, Label, RecordFields, Subs, UnionTags, UnsortedUnionLabels, Variable, -}; -use roc_types::types::{gather_fields_unsorted_iter, RecordField, RecordFieldsError}; -use std::cmp::Ordering; -use std::collections::hash_map::{DefaultHasher, Entry}; -use std::collections::HashMap; -use std::hash::{Hash, Hasher}; -use ven_pretty::{DocAllocator, DocBuilder}; - -// if your changes cause this number to go down, great! -// please change it to the lower number. -// if it went up, maybe check that the change is really required -roc_error_macros::assert_sizeof_aarch64!(Builtin, 3 * 8); -roc_error_macros::assert_sizeof_aarch64!(Layout, 4 * 8); -roc_error_macros::assert_sizeof_aarch64!(UnionLayout, 3 * 8); -roc_error_macros::assert_sizeof_aarch64!(LambdaSet, 3 * 8); - -roc_error_macros::assert_sizeof_wasm!(Builtin, 3 * 4); -roc_error_macros::assert_sizeof_wasm!(Layout, 6 * 4); -roc_error_macros::assert_sizeof_wasm!(UnionLayout, 3 * 4); -roc_error_macros::assert_sizeof_wasm!(LambdaSet, 3 * 4); - -roc_error_macros::assert_sizeof_default!(Builtin, 3 * 8); -roc_error_macros::assert_sizeof_default!(Layout, 4 * 8); -roc_error_macros::assert_sizeof_default!(UnionLayout, 3 * 8); -roc_error_macros::assert_sizeof_default!(LambdaSet, 3 * 8); - -pub type TagIdIntType = u16; -pub const MAX_ENUM_SIZE: usize = (std::mem::size_of::() * 8) as usize; -const GENERATE_NULLABLE: bool = true; - -#[derive(Debug, Clone)] -pub enum LayoutProblem { - UnresolvedTypeVar(Variable), - Erroneous, -} - -impl From for RuntimeError { - fn from(lp: LayoutProblem) -> Self { - match lp { - LayoutProblem::UnresolvedTypeVar(_) => RuntimeError::UnresolvedTypeVar, - LayoutProblem::Erroneous => RuntimeError::ErroneousType, - } - } -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] -pub enum RawFunctionLayout<'a> { - Function(&'a [Layout<'a>], LambdaSet<'a>, &'a Layout<'a>), - ZeroArgumentThunk(Layout<'a>), -} - -impl<'a> RawFunctionLayout<'a> { - pub fn is_zero_argument_thunk(&self) -> bool { - matches!(self, RawFunctionLayout::ZeroArgumentThunk(_)) - } - - fn new_help<'b>( - env: &mut Env<'a, 'b>, - var: Variable, - content: Content, - ) -> Result { - use roc_types::subs::Content::*; - match content { - FlexVar(_) | RigidVar(_) => Err(LayoutProblem::UnresolvedTypeVar(var)), - FlexAbleVar(_, _) | RigidAbleVar(_, _) => todo_abilities!("Not reachable yet"), - RecursionVar { structure, .. } => { - let structure_content = env.subs.get_content_without_compacting(structure); - Self::new_help(env, structure, *structure_content) - } - LambdaSet(lset) => Self::layout_from_lambda_set(env, lset), - Structure(flat_type) => Self::layout_from_flat_type(env, flat_type), - RangedNumber(typ, _) => Self::from_var(env, typ), - - // Ints - Alias(Symbol::NUM_I128, args, _, _) => { - debug_assert!(args.is_empty()); - Ok(Self::ZeroArgumentThunk(Layout::i128())) - } - Alias(Symbol::NUM_I64, args, _, _) => { - debug_assert!(args.is_empty()); - Ok(Self::ZeroArgumentThunk(Layout::i64())) - } - Alias(Symbol::NUM_I32, args, _, _) => { - debug_assert!(args.is_empty()); - Ok(Self::ZeroArgumentThunk(Layout::i32())) - } - Alias(Symbol::NUM_I16, args, _, _) => { - debug_assert!(args.is_empty()); - Ok(Self::ZeroArgumentThunk(Layout::i16())) - } - Alias(Symbol::NUM_I8, args, _, _) => { - debug_assert!(args.is_empty()); - Ok(Self::ZeroArgumentThunk(Layout::i8())) - } - - // I think unsigned and signed use the same layout - Alias(Symbol::NUM_U128, args, _, _) => { - debug_assert!(args.is_empty()); - Ok(Self::ZeroArgumentThunk(Layout::u128())) - } - Alias(Symbol::NUM_U64, args, _, _) => { - debug_assert!(args.is_empty()); - Ok(Self::ZeroArgumentThunk(Layout::u64())) - } - Alias(Symbol::NUM_U32, args, _, _) => { - debug_assert!(args.is_empty()); - Ok(Self::ZeroArgumentThunk(Layout::u32())) - } - Alias(Symbol::NUM_U16, args, _, _) => { - debug_assert!(args.is_empty()); - Ok(Self::ZeroArgumentThunk(Layout::u16())) - } - Alias(Symbol::NUM_U8, args, _, _) => { - debug_assert!(args.is_empty()); - Ok(Self::ZeroArgumentThunk(Layout::u8())) - } - - // Floats - Alias(Symbol::NUM_F64, args, _, _) => { - debug_assert!(args.is_empty()); - Ok(Self::ZeroArgumentThunk(Layout::f64())) - } - Alias(Symbol::NUM_F32, args, _, _) => { - debug_assert!(args.is_empty()); - Ok(Self::ZeroArgumentThunk(Layout::f32())) - } - - // Nat - Alias(Symbol::NUM_NAT, args, _, _) => { - debug_assert!(args.is_empty()); - Ok(Self::ZeroArgumentThunk(Layout::usize(env.target_info))) - } - - Alias(symbol, _, _, _) if symbol.is_builtin() => Ok(Self::ZeroArgumentThunk( - Layout::new_help(env, var, content)?, - )), - - Alias(_, _, var, _) => Self::from_var(env, var), - Error => Err(LayoutProblem::Erroneous), - } - } - - fn layout_from_lambda_set( - _env: &mut Env<'a, '_>, - _lset: subs::LambdaSet, - ) -> Result { - unreachable!() - // Lambda set is just a tag union from the layout's perspective. - // Self::layout_from_flat_type(env, lset.as_tag_union()) - } - - fn layout_from_flat_type( - env: &mut Env<'a, '_>, - flat_type: FlatType, - ) -> Result { - use roc_types::subs::FlatType::*; - - let arena = env.arena; - - match flat_type { - Func(args, closure_var, ret_var) => { - let mut fn_args = Vec::with_capacity_in(args.len(), arena); - - for index in args.into_iter() { - let arg_var = env.subs[index]; - fn_args.push(Layout::from_var(env, arg_var)?); - } - - let ret = Layout::from_var(env, ret_var)?; - - let fn_args = fn_args.into_bump_slice(); - let ret = arena.alloc(ret); - - let lambda_set = - LambdaSet::from_var(env.arena, env.subs, closure_var, env.target_info)?; - - Ok(Self::Function(fn_args, lambda_set, ret)) - } - TagUnion(tags, ext) if tags.is_newtype_wrapper(env.subs) => { - debug_assert!(ext_var_is_empty_tag_union(env.subs, ext)); - let slice_index = tags.variables().into_iter().next().unwrap(); - let slice = env.subs[slice_index]; - let var_index = slice.into_iter().next().unwrap(); - let var = env.subs[var_index]; - - Self::from_var(env, var) - } - Record(fields, ext) if fields.len() == 1 => { - debug_assert!(ext_var_is_empty_record(env.subs, ext)); - - let var_index = fields.iter_variables().next().unwrap(); - let var = env.subs[var_index]; - - Self::from_var(env, var) - } - _ => { - let layout = layout_from_flat_type(env, flat_type)?; - Ok(Self::ZeroArgumentThunk(layout)) - } - } - } - - /// Returns Err(()) if given an error, or Ok(Layout) if given a non-erroneous Structure. - /// Panics if given a FlexVar or RigidVar, since those should have been - /// monomorphized away already! - fn from_var(env: &mut Env<'a, '_>, var: Variable) -> Result { - if env.is_seen(var) { - unreachable!("The initial variable of a signature cannot be seen already") - } else { - let content = env.subs.get_content_without_compacting(var); - Self::new_help(env, var, *content) - } - } -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] -pub struct FieldOrderHash(u64); - -impl FieldOrderHash { - // NB: This should really be a proper "zero" hash via `DefaultHasher::new().finish()`, but Rust - // stdlib hashers are not (yet) compile-time-computable. - const ZERO_FIELD_HASH: Self = Self(0); - const IRRELEVANT_NON_ZERO_FIELD_HASH: Self = Self(1); - - pub fn from_ordered_fields(fields: &[&Lowercase]) -> Self { - if fields.is_empty() { - // HACK: we must make sure this is always equivalent to a `ZERO_FIELD_HASH`. - return Self::ZERO_FIELD_HASH; - } - - let mut hasher = DefaultHasher::new(); - fields.iter().for_each(|field| field.hash(&mut hasher)); - Self(hasher.finish()) - } -} - -/// Types for code gen must be monomorphic. No type variables allowed! -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] -pub enum Layout<'a> { - Builtin(Builtin<'a>), - Struct { - /// Two different struct types can have the same layout, for example - /// { a: U8, b: I64 } - /// { a: I64, b: U8 } - /// both have the layout {I64, U8}. Not distinguishing the order of record fields can cause - /// us problems during monomorphization when we specialize the same type in different ways, - /// so keep a hash of the record order for disambiguation. This still of course may result - /// in collisions, but it's unlikely. - /// - /// See also https://github.com/rtfeldman/roc/issues/2535. - field_order_hash: FieldOrderHash, - field_layouts: &'a [Layout<'a>], - }, - Boxed(&'a Layout<'a>), - Union(UnionLayout<'a>), - LambdaSet(LambdaSet<'a>), - RecursivePointer, -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] -pub enum UnionLayout<'a> { - /// A non-recursive tag union - /// e.g. `Result a e : [Ok a, Err e]` - NonRecursive(&'a [&'a [Layout<'a>]]), - /// A recursive tag union (general case) - /// e.g. `Expr : [Sym Str, Add Expr Expr]` - Recursive(&'a [&'a [Layout<'a>]]), - /// A recursive tag union with just one constructor - /// Optimization: No need to store a tag ID (the payload is "unwrapped") - /// e.g. `RoseTree a : [Tree a (List (RoseTree a))]` - NonNullableUnwrapped(&'a [Layout<'a>]), - /// A recursive tag union that has an empty variant - /// Optimization: Represent the empty variant as null pointer => no memory usage & fast comparison - /// It has more than one other variant, so they need tag IDs (payloads are "wrapped") - /// e.g. `FingerTree a : [Empty, Single a, More (Some a) (FingerTree (Tuple a)) (Some a)]` - /// see also: https://youtu.be/ip92VMpf_-A?t=164 - /// - /// nullable_id refers to the index of the tag that is represented at runtime as NULL. - /// For example, in `FingerTree a : [Empty, Single a, More (Some a) (FingerTree (Tuple a)) (Some a)]`, - /// the ids would be Empty = 0, More = 1, Single = 2, because that's how those tags are - /// ordered alphabetically. Since the Empty tag will be represented at runtime as NULL, - /// and since Empty's tag id is 0, here nullable_id would be 0. - NullableWrapped { - nullable_id: u16, - other_tags: &'a [&'a [Layout<'a>]], - }, - /// A recursive tag union with only two variants, where one is empty. - /// Optimizations: Use null for the empty variant AND don't store a tag ID for the other variant. - /// e.g. `ConsList a : [Nil, Cons a (ConsList a)]` - /// - /// nullable_id is a bool because it's only ever 0 or 1, but (as with the NullableWrapped - /// variant), it reprsents the index of the tag that will be represented at runtime as NULL. - /// - /// So for example, in `ConsList a : [Nil, Cons a (ConsList a)]`, Nil is tag id 1 and - /// Cons is tag id 0 because Nil comes alphabetically after Cons. Here, Nil will be - /// represented as NULL at runtime, so nullable_id is 1 - which is to say, `true`, because - /// `(1 as bool)` is `true`. - NullableUnwrapped { - nullable_id: bool, - other_fields: &'a [Layout<'a>], - }, -} - -impl<'a> UnionLayout<'a> { - pub fn to_doc(self, alloc: &'a D, _parens: Parens) -> DocBuilder<'a, D, A> - where - D: DocAllocator<'a, A>, - D::Doc: Clone, - A: Clone, - { - use UnionLayout::*; - - match self { - NonRecursive(tags) => { - let tags_doc = tags.iter().map(|fields| { - alloc.text("C ").append(alloc.intersperse( - fields.iter().map(|x| x.to_doc(alloc, Parens::InTypeParam)), - " ", - )) - }); - - alloc - .text("[") - .append(alloc.intersperse(tags_doc, ", ")) - .append(alloc.text("]")) - } - Recursive(tags) => { - let tags_doc = tags.iter().map(|fields| { - alloc.text("C ").append(alloc.intersperse( - fields.iter().map(|x| x.to_doc(alloc, Parens::InTypeParam)), - " ", - )) - }); - alloc - .text("[") - .append(alloc.intersperse(tags_doc, ", ")) - .append(alloc.text("]")) - } - NonNullableUnwrapped(fields) => { - let fields_doc = alloc.text("C ").append(alloc.intersperse( - fields.iter().map(|x| x.to_doc(alloc, Parens::InTypeParam)), - " ", - )); - alloc - .text("[") - .append(fields_doc) - .append(alloc.text("]")) - } - NullableUnwrapped { - nullable_id, - other_fields, - } => { - let fields_doc = alloc.text("C ").append( - alloc.intersperse( - other_fields - .iter() - .map(|x| x.to_doc(alloc, Parens::InTypeParam)), - " ", - ), - ); - let tags_doc = if nullable_id { - alloc.concat(vec![alloc.text(", "), fields_doc]) - } else { - alloc.concat(vec![fields_doc, alloc.text(", ")]) - }; - alloc - .text("[") - .append(tags_doc) - .append(alloc.text("]")) - } - _ => alloc.text("TODO"), - } - } - - pub fn layout_at(self, tag_id: TagIdIntType, index: usize) -> Layout<'a> { - let result = match self { - UnionLayout::NonRecursive(tag_layouts) => { - let field_layouts = tag_layouts[tag_id as usize]; - - // this cannot be recursive; return immediately - return field_layouts[index]; - } - UnionLayout::Recursive(tag_layouts) => { - let field_layouts = tag_layouts[tag_id as usize]; - - field_layouts[index] - } - UnionLayout::NonNullableUnwrapped(field_layouts) => field_layouts[index], - UnionLayout::NullableWrapped { - nullable_id, - other_tags, - } => { - debug_assert_ne!(nullable_id, tag_id); - - let tag_index = if tag_id < nullable_id { - tag_id - } else { - tag_id - 1 - }; - - let field_layouts = other_tags[tag_index as usize]; - field_layouts[index] - } - - UnionLayout::NullableUnwrapped { - nullable_id, - other_fields, - } => { - debug_assert_ne!(nullable_id, tag_id != 0); - - other_fields[index as usize] - } - }; - - if let Layout::RecursivePointer = result { - Layout::Union(self) - } else { - result - } - } - - pub fn number_of_tags(&'a self) -> usize { - match self { - UnionLayout::NonRecursive(tags) | UnionLayout::Recursive(tags) => tags.len(), - - UnionLayout::NullableWrapped { other_tags, .. } => other_tags.len() + 1, - UnionLayout::NonNullableUnwrapped(_) => 1, - UnionLayout::NullableUnwrapped { .. } => 2, - } - } - - pub fn discriminant_size(num_tags: usize) -> IntWidth { - if num_tags <= u8::MAX as usize { - IntWidth::U8 - } else if num_tags <= u16::MAX as usize { - IntWidth::U16 - } else { - panic!("tag union is too big") - } - } - - pub fn tag_id_builtin(&self) -> Builtin<'a> { - match self { - UnionLayout::NonRecursive(tags) => { - let union_size = tags.len(); - Builtin::Int(Self::discriminant_size(union_size)) - } - UnionLayout::Recursive(tags) => { - let union_size = tags.len(); - - Builtin::Int(Self::discriminant_size(union_size)) - } - - UnionLayout::NullableWrapped { other_tags, .. } => { - Builtin::Int(Self::discriminant_size(other_tags.len() + 1)) - } - UnionLayout::NonNullableUnwrapped(_) => Builtin::Bool, - UnionLayout::NullableUnwrapped { .. } => Builtin::Bool, - } - } - - pub fn tag_id_layout(&self) -> Layout<'a> { - Layout::Builtin(self.tag_id_builtin()) - } - - fn stores_tag_id_in_pointer_bits(tags: &[&[Layout<'a>]], target_info: TargetInfo) -> bool { - tags.len() < target_info.ptr_width() as usize - } - - pub fn tag_id_pointer_bits_and_mask(target_info: TargetInfo) -> (usize, usize) { - match target_info.ptr_width() { - PtrWidth::Bytes8 => (3, 0b0000_0111), - PtrWidth::Bytes4 => (2, 0b0000_0011), - } - } - - // i.e. it is not implicit and not stored in the pointer bits - pub fn stores_tag_id_as_data(&self, target_info: TargetInfo) -> bool { - match self { - UnionLayout::NonRecursive(_) => true, - UnionLayout::Recursive(tags) - | UnionLayout::NullableWrapped { - other_tags: tags, .. - } => !Self::stores_tag_id_in_pointer_bits(tags, target_info), - UnionLayout::NonNullableUnwrapped(_) | UnionLayout::NullableUnwrapped { .. } => false, - } - } - - pub fn stores_tag_id_in_pointer(&self, target_info: TargetInfo) -> bool { - match self { - UnionLayout::NonRecursive(_) => false, - UnionLayout::Recursive(tags) - | UnionLayout::NullableWrapped { - other_tags: tags, .. - } => Self::stores_tag_id_in_pointer_bits(tags, target_info), - UnionLayout::NonNullableUnwrapped(_) | UnionLayout::NullableUnwrapped { .. } => false, - } - } - - pub fn tag_is_null(&self, tag_id: TagIdIntType) -> bool { - match self { - UnionLayout::NonRecursive(_) - | UnionLayout::NonNullableUnwrapped(_) - | UnionLayout::Recursive(_) => false, - UnionLayout::NullableWrapped { nullable_id, .. } => *nullable_id == tag_id, - UnionLayout::NullableUnwrapped { nullable_id, .. } => *nullable_id == (tag_id != 0), - } - } - - pub fn is_nullable(&self) -> bool { - match self { - UnionLayout::NonRecursive(_) - | UnionLayout::Recursive(_) - | UnionLayout::NonNullableUnwrapped { .. } => false, - UnionLayout::NullableWrapped { .. } | UnionLayout::NullableUnwrapped { .. } => true, - } - } - - fn tags_alignment_bytes(tags: &[&[Layout]], target_info: TargetInfo) -> u32 { - tags.iter() - .map(|field_layouts| { - Layout::struct_no_name_order(field_layouts).alignment_bytes(target_info) - }) - .max() - .unwrap_or(0) - } - - pub fn allocation_alignment_bytes(&self, target_info: TargetInfo) -> u32 { - let allocation = match self { - UnionLayout::NonRecursive(tags) => Self::tags_alignment_bytes(tags, target_info), - UnionLayout::Recursive(tags) => Self::tags_alignment_bytes(tags, target_info), - UnionLayout::NonNullableUnwrapped(field_layouts) => { - Layout::struct_no_name_order(field_layouts).alignment_bytes(target_info) - } - UnionLayout::NullableWrapped { other_tags, .. } => { - Self::tags_alignment_bytes(other_tags, target_info) - } - UnionLayout::NullableUnwrapped { other_fields, .. } => { - Layout::struct_no_name_order(other_fields).alignment_bytes(target_info) - } - }; - - // because we store a refcount, the alignment must be at least the size of a pointer - allocation.max(target_info.ptr_width() as u32) - } - - /// Size of the data in memory, whether it's stack or heap (for non-null tag ids) - pub fn data_size_and_alignment(&self, target_info: TargetInfo) -> (u32, u32) { - let id_data_layout = if self.stores_tag_id_as_data(target_info) { - Some(self.tag_id_layout()) - } else { - None - }; - - self.data_size_and_alignment_help_match(id_data_layout, target_info) - } - - /// Size of the data before the tag_id, if it exists. - /// Returns None if the tag_id is not stored as data in the layout. - pub fn data_size_without_tag_id(&self, target_info: TargetInfo) -> Option { - if !self.stores_tag_id_as_data(target_info) { - return None; - }; - - Some(self.data_size_and_alignment_help_match(None, target_info).0) - } - - fn data_size_and_alignment_help_match( - &self, - id_data_layout: Option, - target_info: TargetInfo, - ) -> (u32, u32) { - match self { - Self::NonRecursive(tags) => { - Self::data_size_and_alignment_help(tags, id_data_layout, target_info) - } - Self::Recursive(tags) => { - Self::data_size_and_alignment_help(tags, id_data_layout, target_info) - } - Self::NonNullableUnwrapped(fields) => { - Self::data_size_and_alignment_help(&[fields], id_data_layout, target_info) - } - Self::NullableWrapped { other_tags, .. } => { - Self::data_size_and_alignment_help(other_tags, id_data_layout, target_info) - } - Self::NullableUnwrapped { other_fields, .. } => { - Self::data_size_and_alignment_help(&[other_fields], id_data_layout, target_info) - } - } - } - - fn data_size_and_alignment_help( - variant_field_layouts: &[&[Layout]], - id_data_layout: Option, - target_info: TargetInfo, - ) -> (u32, u32) { - let mut size = 0; - let mut alignment_bytes = 0; - - for field_layouts in variant_field_layouts { - let mut data = Layout::struct_no_name_order(field_layouts); - - let fields_and_id; - if let Some(id_layout) = id_data_layout { - fields_and_id = [data, id_layout]; - data = Layout::struct_no_name_order(&fields_and_id); - } - - let (variant_size, variant_alignment) = data.stack_size_and_alignment(target_info); - alignment_bytes = alignment_bytes.max(variant_alignment); - size = size.max(variant_size); - } - - (size, alignment_bytes) - } - - /// Very important to use this when doing a memcpy! - fn stack_size_without_alignment(&self, target_info: TargetInfo) -> u32 { - match self { - UnionLayout::NonRecursive(tags) => { - let id_layout = self.tag_id_layout(); - - let mut size = 0; - - for field_layouts in tags.iter() { - let fields = Layout::struct_no_name_order(field_layouts); - let fields_and_id = [fields, id_layout]; - - let data = Layout::struct_no_name_order(&fields_and_id); - size = size.max(data.stack_size_without_alignment(target_info)); - } - - size - } - UnionLayout::Recursive(_) - | UnionLayout::NonNullableUnwrapped(_) - | UnionLayout::NullableWrapped { .. } - | UnionLayout::NullableUnwrapped { .. } => target_info.ptr_width() as u32, - } - } -} - -/// Custom type so we can get the numeric representation of a symbol in tests (so `#UserApp.3` -/// instead of `UserApp.foo`). The pretty name is not reliable when running many tests -/// concurrently. The number does not change and will give a reliable output. -struct SetElement<'a> { - symbol: Symbol, - layout: &'a [Layout<'a>], -} - -impl std::fmt::Debug for SetElement<'_> { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let symbol_string = crate::ir::symbol_to_doc_string(self.symbol); - - write!(f, "( {}, {:?})", symbol_string, self.layout) - } -} - -impl std::fmt::Debug for LambdaSet<'_> { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - struct Helper<'a> { - set: &'a [(Symbol, &'a [Layout<'a>])], - } - - impl std::fmt::Debug for Helper<'_> { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let entries = self.set.iter().map(|x| SetElement { - symbol: x.0, - layout: x.1, - }); - - f.debug_list().entries(entries).finish() - } - } - - f.debug_struct("LambdaSet") - .field("set", &Helper { set: self.set }) - .field("representation", &self.representation) - .finish() - } -} - -#[derive(Clone, Copy, PartialEq, Eq, Hash)] -pub struct LambdaSet<'a> { - /// collection of function names and their closure arguments - pub set: &'a [(Symbol, &'a [Layout<'a>])], - /// how the closure will be represented at runtime - representation: &'a Layout<'a>, -} - -/// representation of the closure *for a particular function* -#[derive(Debug)] -pub enum ClosureRepresentation<'a> { - /// the closure is represented as a union. Includes the tag ID! - Union { - alphabetic_order_fields: &'a [Layout<'a>], - closure_name: Symbol, - tag_id: TagIdIntType, - union_layout: UnionLayout<'a>, - }, - /// The closure is represented as a struct. The layouts are sorted - /// alphabetically by the identifier that is captured. - /// - /// We MUST sort these according to their stack size before code gen! - AlphabeticOrderStruct(&'a [Layout<'a>]), - /// the representation is anything but a union - Other(Layout<'a>), -} - -impl<'a> LambdaSet<'a> { - pub fn runtime_representation(&self) -> Layout<'a> { - *self.representation - } - - /// Does the lambda set contain the given symbol? - pub fn contains(&self, symbol: Symbol) -> bool { - self.set.iter().any(|(s, _)| *s == symbol) - } - - pub fn is_represented(&self) -> Option> { - if let Layout::Struct { - field_layouts: &[], .. - } = self.representation - { - None - } else { - Some(*self.representation) - } - } - - pub fn member_does_not_need_closure_argument(&self, function_symbol: Symbol) -> bool { - match self.layout_for_member(function_symbol) { - ClosureRepresentation::Union { - alphabetic_order_fields, - .. - } => alphabetic_order_fields.is_empty(), - ClosureRepresentation::AlphabeticOrderStruct(fields) => fields.is_empty(), - ClosureRepresentation::Other(_) => false, - } - } - - pub fn layout_for_member(&self, function_symbol: Symbol) -> ClosureRepresentation<'a> { - debug_assert!( - self.set.iter().any(|(s, _)| *s == function_symbol), - "function symbol not in set" - ); - - match self.representation { - Layout::Union(union) => { - // here we rely on the fact that a union in a closure would be stored in a one-element record. - // a closure representation that is itself union must be a of the shape `Closure1 ... | Closure2 ...` - match union { - UnionLayout::NonRecursive(_) => { - // get the fields from the set, where they are sorted in alphabetic order - // (and not yet sorted by their alignment) - let (index, (_, fields)) = self - .set - .iter() - .enumerate() - .find(|(_, (s, _))| *s == function_symbol) - .unwrap(); - - ClosureRepresentation::Union { - tag_id: index as TagIdIntType, - alphabetic_order_fields: fields, - closure_name: function_symbol, - union_layout: *union, - } - } - UnionLayout::Recursive(_) => todo!("recursive closures"), - UnionLayout::NonNullableUnwrapped(_) => todo!("recursive closures"), - UnionLayout::NullableWrapped { - nullable_id: _, - other_tags: _, - } => todo!("recursive closures"), - UnionLayout::NullableUnwrapped { - nullable_id: _, - other_fields: _, - } => todo!("recursive closures"), - } - } - Layout::Struct { .. } => { - // get the fields from the set, where they are sorted in alphabetic order - // (and not yet sorted by their alignment) - let (_, fields) = self - .set - .iter() - .find(|(s, _)| *s == function_symbol) - .unwrap(); - - ClosureRepresentation::AlphabeticOrderStruct(fields) - } - _ => ClosureRepresentation::Other(*self.representation), - } - } - - pub fn extend_argument_list( - &self, - arena: &'a Bump, - argument_layouts: &'a [Layout<'a>], - ) -> &'a [Layout<'a>] { - if let [] = self.set { - // TERRIBLE HACK for builting functions - argument_layouts - } else { - match self.representation { - Layout::Struct { - field_layouts: &[], .. - } => { - // this function does not have anything in its closure, and the lambda set is a - // singleton, so we pass no extra argument - argument_layouts - } - Layout::Builtin(Builtin::Bool) - | Layout::Builtin(Builtin::Int(IntWidth::I8 | IntWidth::U8)) => { - // we don't pass this along either - argument_layouts - } - _ => { - let mut arguments = Vec::with_capacity_in(argument_layouts.len() + 1, arena); - arguments.extend(argument_layouts); - arguments.push(Layout::LambdaSet(*self)); - - arguments.into_bump_slice() - } - } - } - } - - pub fn from_var( - arena: &'a Bump, - subs: &Subs, - closure_var: Variable, - target_info: TargetInfo, - ) -> Result { - match roc_types::pretty_print::resolve_lambda_set(subs, closure_var) { - ResolvedLambdaSet::Set(mut lambdas) => { - // sort the tags; make sure ordering stays intact! - lambdas.sort(); - - let mut set = Vec::with_capacity_in(lambdas.len(), arena); - - let mut env = Env { - arena, - subs, - seen: Vec::new_in(arena), - target_info, - }; - - for (function_symbol, variables) in lambdas.iter() { - let mut arguments = Vec::with_capacity_in(variables.len(), arena); - - for var in variables { - arguments.push(Layout::from_var(&mut env, *var)?); - } - - set.push((*function_symbol, arguments.into_bump_slice())); - } - - let representation = - arena.alloc(Self::make_representation(arena, subs, lambdas, target_info)); - - Ok(LambdaSet { - set: set.into_bump_slice(), - representation, - }) - } - ResolvedLambdaSet::Unbound => { - // The lambda set is unbound which means it must be unused. Just give it the empty lambda set. - // See also https://github.com/rtfeldman/roc/issues/3163. - Ok(LambdaSet { - set: &[], - representation: arena.alloc(Layout::UNIT), - }) - } - } - } - - fn make_representation( - arena: &'a Bump, - subs: &Subs, - tags: std::vec::Vec<(Symbol, std::vec::Vec)>, - target_info: TargetInfo, - ) -> Layout<'a> { - // otherwise, this is a closure with a payload - let variant = union_sorted_tags_help(arena, tags, None, subs, target_info); - - use UnionVariant::*; - match variant { - Never => Layout::VOID, - BoolUnion { .. } => Layout::bool(), - ByteUnion { .. } => Layout::u8(), - Unit | UnitWithArguments => { - // no useful information to store - Layout::UNIT - } - Newtype { - arguments: layouts, .. - } => Layout::struct_no_name_order(layouts.into_bump_slice()), - Wrapped(variant) => { - use WrappedVariant::*; - - match variant { - NonRecursive { - sorted_tag_layouts: tags, - } => { - debug_assert!(tags.len() > 1); - - // if the closed-over value is actually a layout, it should be wrapped in a 1-element record - debug_assert!(matches!(tags[0].0, TagOrClosure::Closure(_))); - - let mut tag_arguments = Vec::with_capacity_in(tags.len(), arena); - - for (_, tag_args) in tags.iter() { - tag_arguments.push(&tag_args[0..]); - } - Layout::Union(UnionLayout::NonRecursive(tag_arguments.into_bump_slice())) - } - - _ => panic!("handle recursive layouts"), - } - } - } - } - - pub fn stack_size(&self, target_info: TargetInfo) -> u32 { - self.representation.stack_size(target_info) - } - pub fn contains_refcounted(&self) -> bool { - self.representation.contains_refcounted() - } - pub fn safe_to_memcpy(&self) -> bool { - self.representation.safe_to_memcpy() - } - - pub fn alignment_bytes(&self, target_info: TargetInfo) -> u32 { - self.representation.alignment_bytes(target_info) - } -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] -pub enum Builtin<'a> { - Int(IntWidth), - Float(FloatWidth), - Bool, - Decimal, - Str, - Dict(&'a Layout<'a>, &'a Layout<'a>), - Set(&'a Layout<'a>), - List(&'a Layout<'a>), -} - -pub struct Env<'a, 'b> { - target_info: TargetInfo, - arena: &'a Bump, - seen: Vec<'a, Variable>, - subs: &'b Subs, -} - -impl<'a, 'b> Env<'a, 'b> { - fn is_seen(&self, var: Variable) -> bool { - let var = self.subs.get_root_key_without_compacting(var); - - self.seen.iter().rev().any(|x| x == &var) - } - - fn insert_seen(&mut self, var: Variable) { - let var = self.subs.get_root_key_without_compacting(var); - - self.seen.push(var); - } - - fn remove_seen(&mut self, var: Variable) -> bool { - let var = self.subs.get_root_key_without_compacting(var); - - if let Some(index) = self.seen.iter().rposition(|x| x == &var) { - self.seen.remove(index); - true - } else { - false - } - } -} - -pub const fn round_up_to_alignment(width: u32, alignment: u32) -> u32 { - if alignment != 0 && width % alignment > 0 { - width + alignment - (width % alignment) - } else { - width - } -} - -#[inline(always)] -pub fn is_unresolved_var(subs: &Subs, var: Variable) -> bool { - use Content::*; - let content = subs.get_content_without_compacting(var); - matches!( - content, - FlexVar(..) | RigidVar(..) | FlexAbleVar(..) | RigidAbleVar(..), - ) -} - -impl<'a> Layout<'a> { - pub const VOID: Self = Layout::Union(UnionLayout::NonRecursive(&[])); - pub const UNIT: Self = Layout::Struct { - field_layouts: &[], - field_order_hash: FieldOrderHash::ZERO_FIELD_HASH, - }; - - fn new_help<'b>( - env: &mut Env<'a, 'b>, - var: Variable, - content: Content, - ) -> Result { - use roc_types::subs::Content::*; - match content { - FlexVar(_) | RigidVar(_) => Err(LayoutProblem::UnresolvedTypeVar(var)), - FlexAbleVar(_, _) | RigidAbleVar(_, _) => todo_abilities!("Not reachable yet"), - RecursionVar { structure, .. } => { - let structure_content = env.subs.get_content_without_compacting(structure); - Self::new_help(env, structure, *structure_content) - } - LambdaSet(lset) => layout_from_lambda_set(env, lset), - Structure(flat_type) => layout_from_flat_type(env, flat_type), - - Alias(symbol, _args, actual_var, _) => { - if let Some(int_width) = IntWidth::try_from_symbol(symbol) { - return Ok(Layout::Builtin(Builtin::Int(int_width))); - } - - if let Some(float_width) = FloatWidth::try_from_symbol(symbol) { - return Ok(Layout::Builtin(Builtin::Float(float_width))); - } - - match symbol { - Symbol::NUM_DECIMAL => return Ok(Layout::Builtin(Builtin::Decimal)), - - Symbol::NUM_NAT | Symbol::NUM_NATURAL => { - return Ok(Layout::usize(env.target_info)) - } - - Symbol::NUM_NUM | Symbol::NUM_INT | Symbol::NUM_INTEGER - if is_unresolved_var(env.subs, actual_var) => - { - // default to i64 - return Ok(Layout::i64()); - } - - Symbol::NUM_FRAC | Symbol::NUM_FLOATINGPOINT - if is_unresolved_var(env.subs, actual_var) => - { - // default to f64 - return Ok(Layout::f64()); - } - - _ => Self::from_var(env, actual_var), - } - } - - RangedNumber(typ, _) => Self::from_var(env, typ), - - Error => Err(LayoutProblem::Erroneous), - } - } - - /// Returns Err(()) if given an error, or Ok(Layout) if given a non-erroneous Structure. - /// Panics if given a FlexVar or RigidVar, since those should have been - /// monomorphized away already! - fn from_var(env: &mut Env<'a, '_>, var: Variable) -> Result { - if env.is_seen(var) { - Ok(Layout::RecursivePointer) - } else { - let content = env.subs.get_content_without_compacting(var); - Self::new_help(env, var, *content) - } - } - - pub fn safe_to_memcpy(&self) -> bool { - use Layout::*; - - match self { - Builtin(builtin) => builtin.safe_to_memcpy(), - Struct { field_layouts, .. } => field_layouts - .iter() - .all(|field_layout| field_layout.safe_to_memcpy()), - Union(variant) => { - use UnionLayout::*; - - match variant { - NonRecursive(tags) => tags - .iter() - .all(|tag_layout| tag_layout.iter().all(|field| field.safe_to_memcpy())), - Recursive(_) - | NullableWrapped { .. } - | NullableUnwrapped { .. } - | NonNullableUnwrapped(_) => { - // a recursive union will always contain a pointer, and is thus not safe to memcpy - false - } - } - } - LambdaSet(lambda_set) => lambda_set.runtime_representation().safe_to_memcpy(), - Boxed(_) | RecursivePointer => { - // We cannot memcpy pointers, because then we would have the same pointer in multiple places! - false - } - } - } - - pub fn is_dropped_because_empty(&self) -> bool { - // For this calculation, we don't need an accurate - // stack size, we just need to know whether it's zero, - // so it's fine to use a pointer size of 1. - false // TODO this should use is_zero_sized once doing so doesn't break things! - } - - /// Like stack_size, but doesn't require target info because - /// whether something is zero sized is not target-dependent. - #[allow(dead_code)] - fn is_zero_sized(&self) -> bool { - match self { - // There are no zero-sized builtins - Layout::Builtin(_) => false, - // Functions are never zero-sized - Layout::LambdaSet(_) => false, - // Empty structs, or structs with all zero-sized fields, are zero-sized - Layout::Struct { field_layouts, .. } => field_layouts.iter().all(Self::is_zero_sized), - // A Box that points to nothing should be unwrapped - Layout::Boxed(content) => content.is_zero_sized(), - Layout::Union(union_layout) => match union_layout { - UnionLayout::NonRecursive(tags) - | UnionLayout::Recursive(tags) - | UnionLayout::NullableWrapped { - other_tags: tags, .. - } => tags - .iter() - .all(|payloads| payloads.iter().all(Self::is_zero_sized)), - UnionLayout::NonNullableUnwrapped(tags) - | UnionLayout::NullableUnwrapped { - other_fields: tags, .. - } => tags.iter().all(Self::is_zero_sized), - }, - // Recursive pointers are considered zero-sized because - // if you have a recursive data structure where everything - // else but the recutsive pointer is zero-sized, then - // the whole thing is unnecessary at runtime and should - // be zero-sized. - Layout::RecursivePointer => true, - } - } - - pub fn is_passed_by_reference(&self, target_info: TargetInfo) -> bool { - match self { - Layout::Builtin(builtin) => { - use Builtin::*; - - match target_info.ptr_width() { - PtrWidth::Bytes4 => { - // more things fit into a register - false - } - PtrWidth::Bytes8 => { - // currently, only Str is passed by-reference internally - matches!(builtin, Str) - } - } - } - Layout::Union(UnionLayout::NonRecursive(_)) => true, - Layout::LambdaSet(lambda_set) => lambda_set - .runtime_representation() - .is_passed_by_reference(target_info), - _ => false, - } - } - - pub fn stack_size(&self, target_info: TargetInfo) -> u32 { - let width = self.stack_size_without_alignment(target_info); - let alignment = self.alignment_bytes(target_info); - - round_up_to_alignment(width, alignment) - } - - pub fn stack_size_and_alignment(&self, target_info: TargetInfo) -> (u32, u32) { - let width = self.stack_size_without_alignment(target_info); - let alignment = self.alignment_bytes(target_info); - - let size = round_up_to_alignment(width, alignment); - (size, alignment) - } - - /// Very important to use this when doing a memcpy! - pub fn stack_size_without_alignment(&self, target_info: TargetInfo) -> u32 { - use Layout::*; - - match self { - Builtin(builtin) => builtin.stack_size(target_info), - Struct { field_layouts, .. } => { - let mut sum = 0; - - for field_layout in *field_layouts { - sum += field_layout.stack_size(target_info); - } - - sum - } - Union(variant) => variant.stack_size_without_alignment(target_info), - LambdaSet(lambda_set) => lambda_set - .runtime_representation() - .stack_size_without_alignment(target_info), - RecursivePointer => target_info.ptr_width() as u32, - Boxed(_) => target_info.ptr_width() as u32, - } - } - - pub fn alignment_bytes(&self, target_info: TargetInfo) -> u32 { - match self { - Layout::Struct { field_layouts, .. } => field_layouts - .iter() - .map(|x| x.alignment_bytes(target_info)) - .max() - .unwrap_or(0), - - Layout::Union(variant) => { - use UnionLayout::*; - - match variant { - NonRecursive(tags) => { - let max_alignment = tags - .iter() - .flat_map(|layouts| { - layouts - .iter() - .map(|layout| layout.alignment_bytes(target_info)) - }) - .max(); - - let tag_id_builtin = variant.tag_id_builtin(); - match max_alignment { - Some(align) => round_up_to_alignment( - align.max(tag_id_builtin.alignment_bytes(target_info)), - tag_id_builtin.alignment_bytes(target_info), - ), - None => { - // none of the tags had any payload, but the tag id still contains information - tag_id_builtin.alignment_bytes(target_info) - } - } - } - Recursive(_) - | NullableWrapped { .. } - | NullableUnwrapped { .. } - | NonNullableUnwrapped(_) => target_info.ptr_width() as u32, - } - } - Layout::LambdaSet(lambda_set) => lambda_set - .runtime_representation() - .alignment_bytes(target_info), - Layout::Builtin(builtin) => builtin.alignment_bytes(target_info), - Layout::RecursivePointer => target_info.ptr_width() as u32, - Layout::Boxed(_) => target_info.ptr_width() as u32, - } - } - - pub fn allocation_alignment_bytes(&self, target_info: TargetInfo) -> u32 { - let ptr_width = target_info.ptr_width() as u32; - - match self { - Layout::Builtin(builtin) => builtin.allocation_alignment_bytes(target_info), - Layout::Struct { .. } => self.alignment_bytes(target_info).max(ptr_width), - Layout::Union(union_layout) => union_layout.allocation_alignment_bytes(target_info), - Layout::LambdaSet(lambda_set) => lambda_set - .runtime_representation() - .allocation_alignment_bytes(target_info), - Layout::RecursivePointer => unreachable!("should be looked up to get an actual layout"), - Layout::Boxed(inner) => inner.allocation_alignment_bytes(target_info), - } - } - - pub fn is_refcounted(&self) -> bool { - use self::Builtin::*; - use Layout::*; - - match self { - Union(UnionLayout::NonRecursive(_)) => false, - - Union(_) => true, - - RecursivePointer => true, - - Builtin(List(_)) | Builtin(Str) => true, - - _ => false, - } - } - - /// Even if a value (say, a record) is not itself reference counted, - /// it may contains values/fields that are. Therefore when this record - /// goes out of scope, the refcount on those values/fields must be decremented. - pub fn contains_refcounted(&self) -> bool { - use Layout::*; - - match self { - Builtin(builtin) => builtin.is_refcounted(), - Struct { field_layouts, .. } => field_layouts.iter().any(|f| f.contains_refcounted()), - Union(variant) => { - use UnionLayout::*; - - match variant { - NonRecursive(fields) => fields - .iter() - .flat_map(|ls| ls.iter()) - .any(|f| f.contains_refcounted()), - Recursive(_) - | NullableWrapped { .. } - | NullableUnwrapped { .. } - | NonNullableUnwrapped(_) => true, - } - } - LambdaSet(lambda_set) => lambda_set.runtime_representation().contains_refcounted(), - RecursivePointer => true, - Boxed(_) => true, - } - } - - pub fn to_doc(self, alloc: &'a D, parens: Parens) -> DocBuilder<'a, D, A> - where - D: DocAllocator<'a, A>, - D::Doc: Clone, - A: Clone, - { - use Layout::*; - - match self { - Builtin(builtin) => builtin.to_doc(alloc, parens), - Struct { field_layouts, .. } => { - let fields_doc = field_layouts.iter().map(|x| x.to_doc(alloc, parens)); - - alloc - .text("{") - .append(alloc.intersperse(fields_doc, ", ")) - .append(alloc.text("}")) - } - Union(union_layout) => union_layout.to_doc(alloc, parens), - LambdaSet(lambda_set) => lambda_set.runtime_representation().to_doc(alloc, parens), - RecursivePointer => alloc.text("*self"), - Boxed(inner) => alloc - .text("Boxed(") - .append(inner.to_doc(alloc, parens)) - .append(")"), - } - } - - /// Used to build a `Layout::Struct` where the field name order is irrelevant. - pub fn struct_no_name_order(field_layouts: &'a [Layout]) -> Self { - if field_layouts.is_empty() { - Self::UNIT - } else { - Self::Struct { - field_layouts, - field_order_hash: FieldOrderHash::IRRELEVANT_NON_ZERO_FIELD_HASH, - } - } - } -} - -/// Avoid recomputing Layout from Variable multiple times. -/// We use `ena` for easy snapshots and rollbacks of the cache. -/// During specialization, a type variable `a` can be specialized to different layouts, -/// e.g. `identity : a -> a` could be specialized to `Bool -> Bool` or `Str -> Str`. -/// Therefore in general it's invalid to store a map from variables to layouts -/// But if we're careful when to invalidate certain keys, we still get some benefit -#[derive(Debug)] -pub struct LayoutCache<'a> { - pub target_info: TargetInfo, - _marker: std::marker::PhantomData<&'a u8>, -} - -#[derive(Debug, Clone)] -pub enum CachedLayout<'a> { - Cached(Layout<'a>), - NotCached, - Problem(LayoutProblem), -} - -impl<'a> LayoutCache<'a> { - pub fn new(target_info: TargetInfo) -> Self { - Self { - target_info, - _marker: Default::default(), - } - } - - pub fn from_var( - &mut self, - arena: &'a Bump, - var: Variable, - subs: &Subs, - ) -> Result, LayoutProblem> { - // Store things according to the root Variable, to avoid duplicate work. - let var = subs.get_root_key_without_compacting(var); - - let mut env = Env { - arena, - subs, - seen: Vec::new_in(arena), - target_info: self.target_info, - }; - - Layout::from_var(&mut env, var) - } - - pub fn raw_from_var( - &mut self, - arena: &'a Bump, - var: Variable, - subs: &Subs, - ) -> Result, LayoutProblem> { - // Store things according to the root Variable, to avoid duplicate work. - let var = subs.get_root_key_without_compacting(var); - - let mut env = Env { - arena, - subs, - seen: Vec::new_in(arena), - target_info: self.target_info, - }; - RawFunctionLayout::from_var(&mut env, var) - } - - pub fn snapshot(&mut self) -> SnapshotKeyPlaceholder { - SnapshotKeyPlaceholder - } - - pub fn rollback_to(&mut self, _snapshot: SnapshotKeyPlaceholder) {} -} - -// placeholder for the type ven_ena::unify::Snapshot>> -pub struct SnapshotKeyPlaceholder; - -impl<'a> Layout<'a> { - pub fn int_width(width: IntWidth) -> Layout<'a> { - Layout::Builtin(Builtin::Int(width)) - } - - pub fn float_width(width: FloatWidth) -> Layout<'a> { - Layout::Builtin(Builtin::Float(width)) - } - - pub fn f64() -> Layout<'a> { - Layout::Builtin(Builtin::Float(FloatWidth::F64)) - } - - pub fn f32() -> Layout<'a> { - Layout::Builtin(Builtin::Float(FloatWidth::F32)) - } - - pub fn usize(target_info: TargetInfo) -> Layout<'a> { - match target_info.ptr_width() { - roc_target::PtrWidth::Bytes4 => Self::u32(), - roc_target::PtrWidth::Bytes8 => Self::u64(), - } - } - - pub fn isize(target_info: TargetInfo) -> Layout<'a> { - match target_info.ptr_width() { - roc_target::PtrWidth::Bytes4 => Self::i32(), - roc_target::PtrWidth::Bytes8 => Self::i64(), - } - } - - pub fn bool() -> Layout<'a> { - Layout::Builtin(Builtin::Bool) - } - - pub const fn u8() -> Layout<'a> { - Layout::Builtin(Builtin::Int(IntWidth::U8)) - } - - pub fn u16() -> Layout<'a> { - Layout::Builtin(Builtin::Int(IntWidth::U16)) - } - - pub fn u32() -> Layout<'a> { - Layout::Builtin(Builtin::Int(IntWidth::U32)) - } - - pub fn u64() -> Layout<'a> { - Layout::Builtin(Builtin::Int(IntWidth::U64)) - } - - pub fn u128() -> Layout<'a> { - Layout::Builtin(Builtin::Int(IntWidth::U128)) - } - - pub fn i8() -> Layout<'a> { - Layout::Builtin(Builtin::Int(IntWidth::I8)) - } - - pub fn i16() -> Layout<'a> { - Layout::Builtin(Builtin::Int(IntWidth::I16)) - } - - pub fn i32() -> Layout<'a> { - Layout::Builtin(Builtin::Int(IntWidth::I32)) - } - - pub fn i64() -> Layout<'a> { - Layout::Builtin(Builtin::Int(IntWidth::I64)) - } - - pub fn i128() -> Layout<'a> { - Layout::Builtin(Builtin::Int(IntWidth::I128)) - } - - pub fn default_integer() -> Layout<'a> { - Layout::i64() - } - - pub fn default_float() -> Layout<'a> { - Layout::f64() - } -} - -impl<'a> Builtin<'a> { - const I1_SIZE: u32 = std::mem::size_of::() as u32; - const DECIMAL_SIZE: u32 = std::mem::size_of::() as u32; - - /// Number of machine words in an empty one of these - pub const STR_WORDS: u32 = 3; - pub const DICT_WORDS: u32 = 3; - pub const SET_WORDS: u32 = Builtin::DICT_WORDS; // Set is an alias for Dict with {} for value - pub const LIST_WORDS: u32 = 3; - - /// Layout of collection wrapper for List and Str - a struct of (pointer, length, capacity). - pub const WRAPPER_PTR: u32 = 0; - pub const WRAPPER_LEN: u32 = 1; - pub const WRAPPER_CAPACITY: u32 = 2; - - pub fn stack_size(&self, target_info: TargetInfo) -> u32 { - use Builtin::*; - - let ptr_width = target_info.ptr_width() as u32; - - match self { - Int(int) => int.stack_size(), - Float(float) => float.stack_size(), - Bool => Builtin::I1_SIZE, - Decimal => Builtin::DECIMAL_SIZE, - Str => Builtin::STR_WORDS * ptr_width, - Dict(_, _) => Builtin::DICT_WORDS * ptr_width, - Set(_) => Builtin::SET_WORDS * ptr_width, - List(_) => Builtin::LIST_WORDS * ptr_width, - } - } - - pub fn alignment_bytes(&self, target_info: TargetInfo) -> u32 { - use std::mem::align_of; - use Builtin::*; - - let ptr_width = target_info.ptr_width() as u32; - - // for our data structures, what counts is the alignment of the `( ptr, len )` tuple, and - // since both of those are one pointer size, the alignment of that structure is a pointer - // size - match self { - Int(int_width) => int_width.alignment_bytes(target_info), - Float(float_width) => float_width.alignment_bytes(target_info), - Bool => align_of::() as u32, - Decimal => IntWidth::I128.alignment_bytes(target_info), - Dict(_, _) => ptr_width, - Set(_) => ptr_width, - // we often treat these as i128 (64-bit systems) - // or i64 (32-bit systems). - // - // In webassembly, For that to be safe - // they must be aligned to allow such access - List(_) => ptr_width, - Str => ptr_width, - } - } - - pub fn safe_to_memcpy(&self) -> bool { - use Builtin::*; - - match self { - Int(_) | Float(_) | Bool | Decimal => true, - - Str | Dict(_, _) | Set(_) | List(_) => false, - } - } - - // Question: does is_refcounted exactly correspond with the "safe to memcpy" property? - pub fn is_refcounted(&self) -> bool { - use Builtin::*; - - match self { - Int(_) | Float(_) | Bool | Decimal => false, - List(_) => true, - - Str | Dict(_, _) | Set(_) => true, - } - } - - pub fn to_doc(self, alloc: &'a D, _parens: Parens) -> DocBuilder<'a, D, A> - where - D: DocAllocator<'a, A>, - D::Doc: Clone, - A: Clone, - { - use Builtin::*; - - match self { - Int(int_width) => { - use IntWidth::*; - - match int_width { - I128 => alloc.text("I128"), - I64 => alloc.text("I64"), - I32 => alloc.text("I32"), - I16 => alloc.text("I16"), - I8 => alloc.text("I8"), - U128 => alloc.text("U128"), - U64 => alloc.text("U64"), - U32 => alloc.text("U32"), - U16 => alloc.text("U16"), - U8 => alloc.text("U8"), - } - } - - Float(float_width) => { - use FloatWidth::*; - - match float_width { - F128 => alloc.text("Float128"), - F64 => alloc.text("Float64"), - F32 => alloc.text("Float32"), - } - } - - Bool => alloc.text("Int1"), - Decimal => alloc.text("Decimal"), - - Str => alloc.text("Str"), - List(layout) => alloc - .text("List ") - .append(layout.to_doc(alloc, Parens::InTypeParam)), - Set(layout) => alloc - .text("Set ") - .append(layout.to_doc(alloc, Parens::InTypeParam)), - Dict(key_layout, value_layout) => alloc - .text("Dict ") - .append(key_layout.to_doc(alloc, Parens::InTypeParam)) - .append(" ") - .append(value_layout.to_doc(alloc, Parens::InTypeParam)), - } - } - - pub fn allocation_alignment_bytes(&self, target_info: TargetInfo) -> u32 { - let ptr_width = target_info.ptr_width() as u32; - - let allocation = match self { - Builtin::Str => ptr_width, - Builtin::Dict(k, v) => k - .alignment_bytes(target_info) - .max(v.alignment_bytes(target_info)) - .max(ptr_width), - Builtin::Set(k) => k.alignment_bytes(target_info).max(ptr_width), - Builtin::List(e) => e.alignment_bytes(target_info).max(ptr_width), - // The following are usually not heap-allocated, but they might be when inside a Box. - Builtin::Int(int_width) => int_width.alignment_bytes(target_info).max(ptr_width), - Builtin::Float(float_width) => float_width.alignment_bytes(target_info).max(ptr_width), - Builtin::Bool => (core::mem::align_of::() as u32).max(ptr_width), - Builtin::Decimal => IntWidth::I128.alignment_bytes(target_info).max(ptr_width), - }; - - allocation.max(ptr_width) - } -} - -fn layout_from_lambda_set<'a>( - env: &mut Env<'a, '_>, - lset: subs::LambdaSet, -) -> Result, LayoutProblem> { - // Lambda set is just a tag union from the layout's perspective. - let subs::LambdaSet { - solved, - recursion_var, - unspecialized, - } = lset; - - if !unspecialized.is_empty() { - internal_error!( - "unspecialized lambda sets remain during layout generation for {:?}", - roc_types::subs::SubsFmtContent(&Content::LambdaSet(lset), env.subs) - ); - } - - match recursion_var.into_variable() { - None => { - let labels = solved.unsorted_lambdas(env.subs); - Ok(layout_from_union(env, &labels)) - } - Some(rec_var) => { - let labels = solved.unsorted_lambdas(env.subs); - layout_from_recursive_union(env, rec_var, &labels) - } - } -} - -fn layout_from_flat_type<'a>( - env: &mut Env<'a, '_>, - flat_type: FlatType, -) -> Result, LayoutProblem> { - use roc_types::subs::FlatType::*; - - let arena = env.arena; - let subs = env.subs; - let target_info = env.target_info; - - match flat_type { - Apply(symbol, args) => { - let args = Vec::from_iter_in(args.into_iter().map(|index| subs[index]), arena); - - match symbol { - // Ints - Symbol::NUM_NAT => { - debug_assert_eq!(args.len(), 0); - Ok(Layout::usize(env.target_info)) - } - - Symbol::NUM_I128 => { - debug_assert_eq!(args.len(), 0); - Ok(Layout::i128()) - } - Symbol::NUM_I64 => { - debug_assert_eq!(args.len(), 0); - Ok(Layout::i64()) - } - Symbol::NUM_I32 => { - debug_assert_eq!(args.len(), 0); - Ok(Layout::i32()) - } - Symbol::NUM_I16 => { - debug_assert_eq!(args.len(), 0); - Ok(Layout::i16()) - } - Symbol::NUM_I8 => { - debug_assert_eq!(args.len(), 0); - Ok(Layout::i8()) - } - - Symbol::NUM_U128 => { - debug_assert_eq!(args.len(), 0); - Ok(Layout::u128()) - } - Symbol::NUM_U64 => { - debug_assert_eq!(args.len(), 0); - Ok(Layout::u64()) - } - Symbol::NUM_U32 => { - debug_assert_eq!(args.len(), 0); - Ok(Layout::u32()) - } - Symbol::NUM_U16 => { - debug_assert_eq!(args.len(), 0); - Ok(Layout::u16()) - } - Symbol::NUM_U8 => { - debug_assert_eq!(args.len(), 0); - Ok(Layout::u8()) - } - - // Floats - Symbol::NUM_DEC => { - debug_assert_eq!(args.len(), 0); - Ok(Layout::Builtin(Builtin::Decimal)) - } - Symbol::NUM_F64 => { - debug_assert_eq!(args.len(), 0); - Ok(Layout::f64()) - } - Symbol::NUM_F32 => { - debug_assert_eq!(args.len(), 0); - Ok(Layout::f32()) - } - - Symbol::NUM_NUM => { - // Num.Num should only ever have 1 argument, e.g. Num.Num Int.Integer - debug_assert_eq!(args.len(), 1); - - let var = args[0]; - let content = subs.get_content_without_compacting(var); - - layout_from_num_content(content, target_info) - } - - Symbol::STR_STR => Ok(Layout::Builtin(Builtin::Str)), - Symbol::LIST_LIST => list_layout_from_elem(env, args[0]), - Symbol::DICT_DICT => dict_layout_from_key_value(env, args[0], args[1]), - Symbol::SET_SET => dict_layout_from_key_value(env, args[0], Variable::EMPTY_RECORD), - Symbol::BOX_BOX_TYPE => { - // Num.Num should only ever have 1 argument, e.g. Num.Num Int.Integer - debug_assert_eq!(args.len(), 1); - - let inner_var = args[0]; - let inner_layout = Layout::from_var(env, inner_var)?; - - Ok(Layout::Boxed(env.arena.alloc(inner_layout))) - } - _ => { - panic!( - "TODO layout_from_flat_type for Apply({:?}, {:?})", - symbol, args - ); - } - } - } - Func(_, closure_var, _) => { - let lambda_set = - LambdaSet::from_var(env.arena, env.subs, closure_var, env.target_info)?; - - Ok(Layout::LambdaSet(lambda_set)) - } - Record(fields, ext_var) => { - // extract any values from the ext_var - - let mut sortables = Vec::with_capacity_in(fields.len(), arena); - let it = match fields.unsorted_iterator(subs, ext_var) { - Ok(it) => it, - Err(RecordFieldsError) => return Err(LayoutProblem::Erroneous), - }; - - for (label, field) in it { - match field { - RecordField::Required(field_var) | RecordField::Demanded(field_var) => { - sortables.push((label, Layout::from_var(env, field_var)?)); - } - RecordField::Optional(_) => { - // drop optional fields - } - } - } - - sortables.sort_by(|(label1, layout1), (label2, layout2)| { - cmp_fields(label1, layout1, label2, layout2, target_info) - }); - - let ordered_field_names = - Vec::from_iter_in(sortables.iter().map(|(label, _)| *label), arena); - let field_order_hash = - FieldOrderHash::from_ordered_fields(ordered_field_names.as_slice()); - - if sortables.len() == 1 { - // If the record has only one field that isn't zero-sized, - // unwrap it. - Ok(sortables.pop().unwrap().1) - } else { - let layouts = Vec::from_iter_in(sortables.into_iter().map(|t| t.1), arena); - - Ok(Layout::Struct { - field_order_hash, - field_layouts: layouts.into_bump_slice(), - }) - } - } - TagUnion(tags, ext_var) => { - let (tags, ext_var) = tags.unsorted_tags_and_ext(subs, ext_var); - - debug_assert!(ext_var_is_empty_tag_union(subs, ext_var)); - - Ok(layout_from_union(env, &tags)) - } - FunctionOrTagUnion(tag_name, _, ext_var) => { - debug_assert!( - ext_var_is_empty_tag_union(subs, ext_var), - "If ext_var wasn't empty, this wouldn't be a FunctionOrTagUnion!" - ); - - let union_tags = UnionTags::from_tag_name_index(tag_name); - let (tags, _) = union_tags.unsorted_tags_and_ext(subs, ext_var); - - Ok(layout_from_union(env, &tags)) - } - RecursiveTagUnion(rec_var, tags, ext_var) => { - let (tags, ext_var) = tags.unsorted_tags_and_ext(subs, ext_var); - - debug_assert!(ext_var_is_empty_tag_union(subs, ext_var)); - - layout_from_recursive_union(env, rec_var, &tags) - } - EmptyTagUnion => Ok(Layout::VOID), - Erroneous(_) => Err(LayoutProblem::Erroneous), - EmptyRecord => Ok(Layout::UNIT), - } -} - -pub type SortedField<'a> = (Lowercase, Variable, Result, Layout<'a>>); - -pub fn sort_record_fields<'a>( - arena: &'a Bump, - var: Variable, - subs: &Subs, - target_info: TargetInfo, -) -> Result>, LayoutProblem> { - let mut env = Env { - arena, - subs, - seen: Vec::new_in(arena), - target_info, - }; - - let (it, _) = match gather_fields_unsorted_iter(subs, RecordFields::empty(), var) { - Ok(it) => it, - Err(_) => return Err(LayoutProblem::Erroneous), - }; - - let it = it - .into_iter() - .map(|(field, field_type)| (field.clone(), field_type)); - - sort_record_fields_help(&mut env, it) -} - -fn sort_record_fields_help<'a>( - env: &mut Env<'a, '_>, - fields_map: impl Iterator)>, -) -> Result>, LayoutProblem> { - let target_info = env.target_info; - - // Sort the fields by label - let mut sorted_fields = Vec::with_capacity_in(fields_map.size_hint().0, env.arena); - - for (label, field) in fields_map { - match field { - RecordField::Demanded(v) | RecordField::Required(v) => { - let layout = Layout::from_var(env, v)?; - sorted_fields.push((label, v, Ok(layout))); - } - RecordField::Optional(v) => { - let layout = Layout::from_var(env, v)?; - sorted_fields.push((label, v, Err(layout))); - } - }; - } - - sorted_fields.sort_by( - |(label1, _, res_layout1), (label2, _, res_layout2)| match res_layout1 { - Ok(layout1) | Err(layout1) => match res_layout2 { - Ok(layout2) | Err(layout2) => { - cmp_fields(label1, layout1, label2, layout2, target_info) - } - }, - }, - ); - - Ok(sorted_fields) -} - -#[derive(Clone, Debug, PartialEq, Eq, Hash)] -pub enum TagOrClosure { - Tag(TagName), - Closure(Symbol), -} - -impl TagOrClosure { - pub fn expect_tag(self) -> TagName { - match self { - Self::Tag(t) => t, - _ => internal_error!("not a tag"), - } - } - pub fn expect_tag_ref(&self) -> &TagName { - match self { - Self::Tag(t) => t, - _ => internal_error!("not a tag"), - } - } -} - -impl From for TagOrClosure { - fn from(t: TagName) -> Self { - Self::Tag(t) - } -} - -impl From for TagOrClosure { - fn from(s: Symbol) -> Self { - Self::Closure(s) - } -} - -#[derive(Clone, Debug, PartialEq, Eq, Hash)] -pub enum UnionVariant<'a> { - Never, - Unit, - UnitWithArguments, - BoolUnion { - ttrue: TagOrClosure, - ffalse: TagOrClosure, - }, - ByteUnion(Vec<'a, TagOrClosure>), - Newtype { - tag_name: TagOrClosure, - arguments: Vec<'a, Layout<'a>>, - }, - Wrapped(WrappedVariant<'a>), -} - -#[derive(Clone, Debug, PartialEq, Eq, Hash)] -pub enum WrappedVariant<'a> { - Recursive { - sorted_tag_layouts: Vec<'a, (TagOrClosure, &'a [Layout<'a>])>, - }, - NonRecursive { - sorted_tag_layouts: Vec<'a, (TagOrClosure, &'a [Layout<'a>])>, - }, - NullableWrapped { - nullable_id: TagIdIntType, - nullable_name: TagOrClosure, - sorted_tag_layouts: Vec<'a, (TagOrClosure, &'a [Layout<'a>])>, - }, - NonNullableUnwrapped { - tag_name: TagOrClosure, - fields: &'a [Layout<'a>], - }, - NullableUnwrapped { - nullable_id: bool, - nullable_name: TagOrClosure, - other_name: TagOrClosure, - other_fields: &'a [Layout<'a>], - }, -} - -impl<'a> WrappedVariant<'a> { - pub fn tag_name_to_id(&self, tag_name: &TagName) -> (TagIdIntType, &'a [Layout<'a>]) { - use WrappedVariant::*; - - match self { - Recursive { sorted_tag_layouts } | NonRecursive { sorted_tag_layouts } => { - let (tag_id, (_, argument_layouts)) = sorted_tag_layouts - .iter() - .enumerate() - .find(|(_, (key, _))| key.expect_tag_ref() == tag_name) - .expect("tag name is not in its own type"); - - debug_assert!(tag_id < 256); - (tag_id as TagIdIntType, *argument_layouts) - } - NullableWrapped { - nullable_id, - nullable_name, - sorted_tag_layouts, - } => { - // assumption: the nullable_name is not included in sorted_tag_layouts - - if tag_name == nullable_name.expect_tag_ref() { - (*nullable_id as TagIdIntType, &[] as &[_]) - } else { - let (mut tag_id, (_, argument_layouts)) = sorted_tag_layouts - .iter() - .enumerate() - .find(|(_, (key, _))| key.expect_tag_ref() == tag_name) - .expect("tag name is not in its own type"); - - if tag_id >= *nullable_id as usize { - tag_id += 1; - } - - debug_assert!(tag_id < 256); - (tag_id as TagIdIntType, *argument_layouts) - } - } - NullableUnwrapped { - nullable_id, - nullable_name, - other_name, - other_fields, - } => { - if tag_name == nullable_name.expect_tag_ref() { - (*nullable_id as TagIdIntType, &[] as &[_]) - } else { - debug_assert_eq!(other_name.expect_tag_ref(), tag_name); - - (!*nullable_id as TagIdIntType, *other_fields) - } - } - NonNullableUnwrapped { fields, .. } => (0, fields), - } - } - - pub fn number_of_tags(&'a self) -> usize { - use WrappedVariant::*; - - match self { - Recursive { sorted_tag_layouts } | NonRecursive { sorted_tag_layouts } => { - sorted_tag_layouts.len() - } - NullableWrapped { - sorted_tag_layouts, .. - } => { - // assumption: the nullable_name is not included in sorted_tag_layouts - - sorted_tag_layouts.len() + 1 - } - NullableUnwrapped { .. } => 2, - NonNullableUnwrapped { .. } => 1, - } - } -} - -pub fn union_sorted_tags<'a>( - arena: &'a Bump, - var: Variable, - subs: &Subs, - target_info: TargetInfo, -) -> Result, LayoutProblem> { - let var = - if let Content::RecursionVar { structure, .. } = subs.get_content_without_compacting(var) { - *structure - } else { - var - }; - - let mut tags_vec = std::vec::Vec::new(); - let result = match roc_types::pretty_print::chase_ext_tag_union(subs, var, &mut tags_vec) { - Ok(()) - // Admit type variables in the extension for now. This may come from things that never got - // monomorphized, like in - // x : [A]* - // x = A - // x - // In such cases it's fine to drop the variable. We may be proven wrong in the future... - | Err((_, Content::FlexVar(_) | Content::RigidVar(_))) - | Err((_, Content::RecursionVar { .. })) => { - let opt_rec_var = get_recursion_var(subs, var); - union_sorted_tags_help(arena, tags_vec, opt_rec_var, subs, target_info) - } - Err((_, Content::Error)) => return Err(LayoutProblem::Erroneous), - Err(other) => panic!("invalid content in tag union variable: {:?}", other), - }; - - Ok(result) -} - -fn get_recursion_var(subs: &Subs, var: Variable) -> Option { - match subs.get_content_without_compacting(var) { - Content::Structure(FlatType::RecursiveTagUnion(rec_var, _, _)) => Some(*rec_var), - Content::Alias(_, _, actual, _) => get_recursion_var(subs, *actual), - _ => None, - } -} - -fn is_recursive_tag_union(layout: &Layout) -> bool { - matches!( - layout, - Layout::Union( - UnionLayout::NullableUnwrapped { .. } - | UnionLayout::Recursive(_) - | UnionLayout::NullableWrapped { .. } - | UnionLayout::NonNullableUnwrapped { .. }, - ) - ) -} - -fn union_sorted_tags_help_new<'a, L>( - env: &mut Env<'a, '_>, - tags_list: &[(&'_ L, &[Variable])], - opt_rec_var: Option, -) -> UnionVariant<'a> -where - L: Label + Ord + Clone + Into, -{ - // sort up front; make sure the ordering stays intact! - let mut tags_list = Vec::from_iter_in(tags_list.iter(), env.arena); - tags_list.sort_unstable_by(|(a, _), (b, _)| a.cmp(b)); - - match tags_list.len() { - 0 => { - // trying to instantiate a type with no values - UnionVariant::Never - } - 1 => { - let &(tag_name, arguments) = tags_list.remove(0); - let tag_name = tag_name.clone().into(); - - // just one tag in the union (but with arguments) can be a struct - let mut layouts = Vec::with_capacity_in(tags_list.len(), env.arena); - - for &var in arguments { - match Layout::from_var(env, var) { - Ok(layout) => { - layouts.push(layout); - } - Err(LayoutProblem::UnresolvedTypeVar(_)) => { - // If we encounter an unbound type var (e.g. `Ok *`) - // then it's zero-sized; In the future we may drop this argument - // completely, but for now we represent it with the empty tag union - layouts.push(Layout::VOID) - } - Err(LayoutProblem::Erroneous) => { - // An erroneous type var will code gen to a runtime - // error, so we don't need to store any data for it. - } - } - } - - layouts.sort_by(|layout1, layout2| { - let size1 = layout1.alignment_bytes(env.target_info); - let size2 = layout2.alignment_bytes(env.target_info); - - size2.cmp(&size1) - }); - - if layouts.is_empty() { - UnionVariant::Unit - } else if opt_rec_var.is_some() { - UnionVariant::Wrapped(WrappedVariant::NonNullableUnwrapped { - tag_name, - fields: layouts.into_bump_slice(), - }) - } else { - UnionVariant::Newtype { - tag_name, - arguments: layouts, - } - } - } - num_tags => { - // default path - let mut answer: Vec<(TagOrClosure, &[Layout])> = - Vec::with_capacity_in(tags_list.len(), env.arena); - let mut has_any_arguments = false; - - let mut nullable: Option<(TagIdIntType, TagOrClosure)> = None; - - // only recursive tag unions can be nullable - let is_recursive = opt_rec_var.is_some(); - if is_recursive && GENERATE_NULLABLE { - for (index, (name, variables)) in tags_list.iter().enumerate() { - if variables.is_empty() { - nullable = Some((index as TagIdIntType, (*name).clone().into())); - break; - } - } - } - - for (index, &(tag_name, arguments)) in tags_list.into_iter().enumerate() { - // reserve space for the tag discriminant - if matches!(nullable, Some((i, _)) if i as usize == index) { - debug_assert!(arguments.is_empty()); - continue; - } - - let mut arg_layouts = Vec::with_capacity_in(arguments.len() + 1, env.arena); - - for &var in arguments { - match Layout::from_var(env, var) { - Ok(layout) => { - has_any_arguments = true; - - // make sure to not unroll recursive types! - let self_recursion = opt_rec_var.is_some() - && env.subs.get_root_key_without_compacting(var) - == env - .subs - .get_root_key_without_compacting(opt_rec_var.unwrap()) - && is_recursive_tag_union(&layout); - - if self_recursion { - arg_layouts.push(Layout::RecursivePointer); - } else { - arg_layouts.push(layout); - } - } - Err(LayoutProblem::UnresolvedTypeVar(_)) => { - // If we encounter an unbound type var (e.g. `Ok *`) - // then it's zero-sized; In the future we may drop this argument - // completely, but for now we represent it with the empty tag union - arg_layouts.push(Layout::VOID); - } - Err(LayoutProblem::Erroneous) => { - // An erroneous type var will code gen to a runtime - // error, so we don't need to store any data for it. - } - } - } - - arg_layouts.sort_by(|layout1, layout2| { - let size1 = layout1.alignment_bytes(env.target_info); - let size2 = layout2.alignment_bytes(env.target_info); - - size2.cmp(&size1) - }); - - answer.push((tag_name.clone().into(), arg_layouts.into_bump_slice())); - } - - match num_tags { - 2 if !has_any_arguments => { - // type can be stored in a boolean - - // tags_vec is sorted, and answer is sorted the same way - let ttrue = answer.remove(1).0; - let ffalse = answer.remove(0).0; - - UnionVariant::BoolUnion { ffalse, ttrue } - } - 3..=MAX_ENUM_SIZE if !has_any_arguments => { - // type can be stored in a byte - // needs the sorted tag names to determine the tag_id - let mut tag_names = Vec::with_capacity_in(answer.len(), env.arena); - - for (tag_name, _) in answer { - tag_names.push(tag_name); - } - - UnionVariant::ByteUnion(tag_names) - } - _ => { - let variant = if let Some((nullable_id, nullable_name)) = nullable { - if answer.len() == 1 { - let (other_name, other_arguments) = answer.drain(..).next().unwrap(); - let nullable_id = nullable_id != 0; - - WrappedVariant::NullableUnwrapped { - nullable_id, - nullable_name, - other_name, - other_fields: other_arguments, - } - } else { - WrappedVariant::NullableWrapped { - nullable_id, - nullable_name, - sorted_tag_layouts: answer, - } - } - } else if is_recursive { - debug_assert!(answer.len() > 1); - WrappedVariant::Recursive { - sorted_tag_layouts: answer, - } - } else { - WrappedVariant::NonRecursive { - sorted_tag_layouts: answer, - } - }; - - UnionVariant::Wrapped(variant) - } - } - } - } -} - -pub fn union_sorted_tags_help<'a, L>( - arena: &'a Bump, - mut tags_vec: std::vec::Vec<(L, std::vec::Vec)>, - opt_rec_var: Option, - subs: &Subs, - target_info: TargetInfo, -) -> UnionVariant<'a> -where - L: Into + Ord + Clone, -{ - // sort up front; make sure the ordering stays intact! - tags_vec.sort_unstable_by(|(a, _), (b, _)| a.cmp(b)); - - let mut env = Env { - arena, - subs, - seen: Vec::new_in(arena), - target_info, - }; - - match tags_vec.len() { - 0 => { - // trying to instantiate a type with no values - UnionVariant::Never - } - 1 => { - let (tag_name, arguments) = tags_vec.remove(0); - - // just one tag in the union (but with arguments) can be a struct - let mut layouts = Vec::with_capacity_in(tags_vec.len(), arena); - let mut contains_zero_sized = false; - - for var in arguments { - match Layout::from_var(&mut env, var) { - Ok(layout) => { - // Drop any zero-sized arguments like {} - if !layout.is_dropped_because_empty() { - layouts.push(layout); - } else { - contains_zero_sized = true; - } - } - Err(LayoutProblem::UnresolvedTypeVar(_)) => { - // If we encounter an unbound type var (e.g. `Ok *`) - // then it's zero-sized; In the future we may drop this argument - // completely, but for now we represent it with the empty tag union - layouts.push(Layout::VOID) - } - Err(LayoutProblem::Erroneous) => { - // An erroneous type var will code gen to a runtime - // error, so we don't need to store any data for it. - } - } - } - - layouts.sort_by(|layout1, layout2| { - let size1 = layout1.alignment_bytes(target_info); - let size2 = layout2.alignment_bytes(target_info); - - size2.cmp(&size1) - }); - - if layouts.is_empty() { - if contains_zero_sized { - UnionVariant::UnitWithArguments - } else { - UnionVariant::Unit - } - } else if opt_rec_var.is_some() { - UnionVariant::Wrapped(WrappedVariant::NonNullableUnwrapped { - tag_name: tag_name.into(), - fields: layouts.into_bump_slice(), - }) - } else { - UnionVariant::Newtype { - tag_name: tag_name.into(), - arguments: layouts, - } - } - } - num_tags => { - // default path - let mut answer = Vec::with_capacity_in(tags_vec.len(), arena); - let mut has_any_arguments = false; - - let mut nullable = None; - - // only recursive tag unions can be nullable - let is_recursive = opt_rec_var.is_some(); - if is_recursive && GENERATE_NULLABLE { - for (index, (name, variables)) in tags_vec.iter().enumerate() { - if variables.is_empty() { - nullable = Some((index as TagIdIntType, name.clone())); - break; - } - } - } - - for (index, (tag_name, arguments)) in tags_vec.into_iter().enumerate() { - // reserve space for the tag discriminant - if matches!(nullable, Some((i, _)) if i as usize == index) { - debug_assert!(arguments.is_empty()); - continue; - } - - let mut arg_layouts = Vec::with_capacity_in(arguments.len() + 1, arena); - - for var in arguments { - match Layout::from_var(&mut env, var) { - Ok(layout) => { - has_any_arguments = true; - - // make sure to not unroll recursive types! - let self_recursion = opt_rec_var.is_some() - && subs.get_root_key_without_compacting(var) - == subs.get_root_key_without_compacting(opt_rec_var.unwrap()) - && is_recursive_tag_union(&layout); - - if self_recursion { - arg_layouts.push(Layout::RecursivePointer); - } else { - arg_layouts.push(layout); - } - } - Err(LayoutProblem::UnresolvedTypeVar(_)) => { - // If we encounter an unbound type var (e.g. `Ok *`) - // then it's zero-sized; In the future we may drop this argument - // completely, but for now we represent it with the empty struct tag - // union - arg_layouts.push(Layout::VOID); - } - Err(LayoutProblem::Erroneous) => { - // An erroneous type var will code gen to a runtime - // error, so we don't need to store any data for it. - } - } - } - - arg_layouts.sort_by(|layout1, layout2| { - let size1 = layout1.alignment_bytes(target_info); - let size2 = layout2.alignment_bytes(target_info); - - size2.cmp(&size1) - }); - - answer.push((tag_name.into(), arg_layouts.into_bump_slice())); - } - - match num_tags { - 2 if !has_any_arguments => { - // type can be stored in a boolean - - // tags_vec is sorted, and answer is sorted the same way - let ttrue = answer.remove(1).0; - let ffalse = answer.remove(0).0; - - UnionVariant::BoolUnion { ffalse, ttrue } - } - 3..=MAX_ENUM_SIZE if !has_any_arguments => { - // type can be stored in a byte - // needs the sorted tag names to determine the tag_id - let mut tag_names = Vec::with_capacity_in(answer.len(), arena); - - for (tag_name, _) in answer { - tag_names.push(tag_name); - } - - UnionVariant::ByteUnion(tag_names) - } - _ => { - let variant = if let Some((nullable_id, nullable_name)) = nullable { - if answer.len() == 1 { - let (other_name, other_arguments) = answer.drain(..).next().unwrap(); - let nullable_id = nullable_id != 0; - - WrappedVariant::NullableUnwrapped { - nullable_id, - nullable_name: nullable_name.into(), - other_name, - other_fields: other_arguments, - } - } else { - WrappedVariant::NullableWrapped { - nullable_id, - nullable_name: nullable_name.into(), - sorted_tag_layouts: answer, - } - } - } else if is_recursive { - debug_assert!(answer.len() > 1); - WrappedVariant::Recursive { - sorted_tag_layouts: answer, - } - } else { - WrappedVariant::NonRecursive { - sorted_tag_layouts: answer, - } - }; - - UnionVariant::Wrapped(variant) - } - } - } - } -} - -fn layout_from_newtype<'a, L: Label>( - env: &mut Env<'a, '_>, - tags: &UnsortedUnionLabels, -) -> Layout<'a> { - debug_assert!(tags.is_newtype_wrapper(env.subs)); - - let (_tag_name, var) = tags.get_newtype(env.subs); - - match Layout::from_var(env, var) { - Ok(layout) => layout, - Err(LayoutProblem::UnresolvedTypeVar(_)) => { - // If we encounter an unbound type var (e.g. `Ok *`) - // then it's zero-sized; In the future we may drop this argument - // completely, but for now we represent it with the empty tag union - Layout::VOID - } - Err(LayoutProblem::Erroneous) => { - // An erroneous type var will code gen to a runtime - // error, so we don't need to store any data for it. - todo!() - } - } -} - -fn layout_from_union<'a, L>(env: &mut Env<'a, '_>, tags: &UnsortedUnionLabels) -> Layout<'a> -where - L: Label + Ord + Into, -{ - use UnionVariant::*; - - if tags.is_newtype_wrapper(env.subs) { - return layout_from_newtype(env, tags); - } - - let tags_vec = &tags.tags; - - let opt_rec_var = None; - let variant = union_sorted_tags_help_new(env, tags_vec, opt_rec_var); - - match variant { - Never => Layout::VOID, - Unit | UnitWithArguments => Layout::UNIT, - BoolUnion { .. } => Layout::bool(), - ByteUnion(_) => Layout::u8(), - Newtype { - arguments: field_layouts, - .. - } => { - let answer1 = if field_layouts.len() == 1 { - field_layouts[0] - } else { - Layout::struct_no_name_order(field_layouts.into_bump_slice()) - }; - - answer1 - } - Wrapped(variant) => { - use WrappedVariant::*; - - match variant { - NonRecursive { - sorted_tag_layouts: tags, - } => { - let mut tag_layouts = Vec::with_capacity_in(tags.len(), env.arena); - tag_layouts.extend(tags.iter().map(|r| r.1)); - - Layout::Union(UnionLayout::NonRecursive(tag_layouts.into_bump_slice())) - } - - Recursive { - sorted_tag_layouts: tags, - } => { - let mut tag_layouts = Vec::with_capacity_in(tags.len(), env.arena); - tag_layouts.extend(tags.iter().map(|r| r.1)); - - debug_assert!(tag_layouts.len() > 1); - Layout::Union(UnionLayout::Recursive(tag_layouts.into_bump_slice())) - } - - NullableWrapped { - nullable_id, - nullable_name: _, - sorted_tag_layouts: tags, - } => { - let mut tag_layouts = Vec::with_capacity_in(tags.len(), env.arena); - tag_layouts.extend(tags.iter().map(|r| r.1)); - - Layout::Union(UnionLayout::NullableWrapped { - nullable_id, - other_tags: tag_layouts.into_bump_slice(), - }) - } - - NullableUnwrapped { .. } => todo!(), - NonNullableUnwrapped { .. } => todo!(), - } - } - } -} - -fn layout_from_recursive_union<'a, L>( - env: &mut Env<'a, '_>, - rec_var: Variable, - tags: &UnsortedUnionLabels, -) -> Result, LayoutProblem> -where - L: Label + Ord + Into, -{ - let arena = env.arena; - let subs = env.subs; - let target_info = env.target_info; - - // some observations - // - // * recursive tag unions are always recursive - // * therefore at least one tag has a pointer (non-zero sized) field - // * they must (to be instantiated) have 2 or more tags - // - // That means none of the optimizations for enums or single tag tag unions apply - - let rec_var = subs.get_root_key_without_compacting(rec_var); - let tags_vec = &tags.tags; - let mut tag_layouts = Vec::with_capacity_in(tags_vec.len(), arena); - - let mut nullable = None; - - if GENERATE_NULLABLE { - for (index, (_name, variables)) in tags_vec.iter().enumerate() { - if variables.is_empty() { - nullable = Some(index as TagIdIntType); - break; - } - } - } - - env.insert_seen(rec_var); - for (index, &(_name, variables)) in tags_vec.iter().enumerate() { - if matches!(nullable, Some(i) if i == index as TagIdIntType) { - // don't add the nullable case - continue; - } - - let mut tag_layout = Vec::with_capacity_in(variables.len() + 1, arena); - - for &var in variables { - // TODO does this cause problems with mutually recursive unions? - if rec_var == subs.get_root_key_without_compacting(var) { - tag_layout.push(Layout::RecursivePointer); - continue; - } - - tag_layout.push(Layout::from_var(env, var)?); - } - - tag_layout.sort_by(|layout1, layout2| { - let size1 = layout1.alignment_bytes(target_info); - let size2 = layout2.alignment_bytes(target_info); - - size2.cmp(&size1) - }); - - tag_layouts.push(tag_layout.into_bump_slice()); - } - env.remove_seen(rec_var); - - let union_layout = if let Some(tag_id) = nullable { - match tag_layouts.into_bump_slice() { - [one] => { - let nullable_id = tag_id != 0; - - UnionLayout::NullableUnwrapped { - nullable_id, - other_fields: one, - } - } - many => UnionLayout::NullableWrapped { - nullable_id: tag_id, - other_tags: many, - }, - } - } else if tag_layouts.len() == 1 { - // drop the tag id - UnionLayout::NonNullableUnwrapped(tag_layouts.pop().unwrap()) - } else { - UnionLayout::Recursive(tag_layouts.into_bump_slice()) - }; - - Ok(Layout::Union(union_layout)) -} - -#[cfg(debug_assertions)] -pub fn ext_var_is_empty_record(subs: &Subs, ext_var: Variable) -> bool { - // the ext_var is empty - let fields = match roc_types::types::gather_fields(subs, RecordFields::empty(), ext_var) { - Ok(fields) => fields, - Err(_) => return false, - }; - - fields.fields.is_empty() -} - -#[cfg(not(debug_assertions))] -pub fn ext_var_is_empty_record(_subs: &Subs, _ext_var: Variable) -> bool { - // This should only ever be used in debug_assert! macros - unreachable!(); -} - -#[cfg(debug_assertions)] -pub fn ext_var_is_empty_tag_union(subs: &Subs, ext_var: Variable) -> bool { - // the ext_var is empty - let mut ext_fields = std::vec::Vec::new(); - match roc_types::pretty_print::chase_ext_tag_union(subs, ext_var, &mut ext_fields) { - Ok(()) | Err((_, Content::FlexVar(_) | Content::RigidVar(_))) => ext_fields.is_empty(), - Err(content) => panic!("invalid content in ext_var: {:?}", content), - } -} - -#[cfg(not(debug_assertions))] -pub fn ext_var_is_empty_tag_union(_: &Subs, _: Variable) -> bool { - // This should only ever be used in debug_assert! macros - unreachable!(); -} - -fn layout_from_num_content<'a>( - content: &Content, - target_info: TargetInfo, -) -> Result, LayoutProblem> { - use roc_types::subs::Content::*; - use roc_types::subs::FlatType::*; - - match content { - RecursionVar { .. } => panic!("recursion var in num"), - FlexVar(_) | RigidVar(_) => { - // If a Num makes it all the way through type checking with an unbound - // type variable, then assume it's a 64-bit integer. - // - // (e.g. for (5 + 5) assume both 5s are 64-bit integers.) - Ok(Layout::default_integer()) - } - FlexAbleVar(_, _) | RigidAbleVar(_, _) => todo_abilities!("Not reachable yet"), - Structure(Apply(symbol, args)) => match *symbol { - // Ints - Symbol::NUM_NAT => Ok(Layout::usize(target_info)), - - Symbol::NUM_INTEGER => Ok(Layout::i64()), - Symbol::NUM_I128 => Ok(Layout::i128()), - Symbol::NUM_I64 => Ok(Layout::i64()), - Symbol::NUM_I32 => Ok(Layout::i32()), - Symbol::NUM_I16 => Ok(Layout::i16()), - Symbol::NUM_I8 => Ok(Layout::i8()), - - Symbol::NUM_U128 => Ok(Layout::u128()), - Symbol::NUM_U64 => Ok(Layout::u64()), - Symbol::NUM_U32 => Ok(Layout::u32()), - Symbol::NUM_U16 => Ok(Layout::u16()), - Symbol::NUM_U8 => Ok(Layout::u8()), - - // Floats - Symbol::NUM_FLOATINGPOINT => Ok(Layout::f64()), - Symbol::NUM_F64 => Ok(Layout::f64()), - Symbol::NUM_F32 => Ok(Layout::f32()), - - // Dec - Symbol::NUM_DEC => Ok(Layout::Builtin(Builtin::Decimal)), - - _ => { - panic!( - "Invalid Num.Num type application: Apply({:?}, {:?})", - symbol, args - ); - } - }, - Alias(_, _, _, _) => { - todo!("TODO recursively resolve type aliases in num_from_content"); - } - Structure(_) | RangedNumber(..) | LambdaSet(_) => { - panic!("Invalid Num.Num type application: {:?}", content); - } - Error => Err(LayoutProblem::Erroneous), - } -} - -fn dict_layout_from_key_value<'a>( - env: &mut Env<'a, '_>, - key_var: Variable, - value_var: Variable, -) -> Result, LayoutProblem> { - let is_variable = |content| matches!(content, &Content::FlexVar(_) | &Content::RigidVar(_)); - - let key_content = env.subs.get_content_without_compacting(key_var); - let value_content = env.subs.get_content_without_compacting(value_var); - - let key_layout = if is_variable(key_content) { - Layout::VOID - } else { - // NOTE: cannot re-use Content, because it may be recursive - // then some state is not correctly kept, we have to go through from_var - Layout::from_var(env, key_var)? - }; - - let value_layout = if is_variable(value_content) { - Layout::VOID - } else { - // NOTE: cannot re-use Content, because it may be recursive - // then some state is not correctly kept, we have to go through from_var - Layout::from_var(env, value_var)? - }; - - // This is a normal list. - Ok(Layout::Builtin(Builtin::Dict( - env.arena.alloc(key_layout), - env.arena.alloc(value_layout), - ))) -} - -pub fn list_layout_from_elem<'a>( - env: &mut Env<'a, '_>, - element_var: Variable, -) -> Result, LayoutProblem> { - let is_variable = |content| matches!(content, &Content::FlexVar(_) | &Content::RigidVar(_)); - - let element_content = env.subs.get_content_without_compacting(element_var); - - let element_layout = if is_variable(element_content) { - // If this was still a (List *) then it must have been an empty list - Layout::VOID - } else { - // NOTE: cannot re-use Content, because it may be recursive - // then some state is not correctly kept, we have to go through from_var - Layout::from_var(env, element_var)? - }; - - Ok(Layout::Builtin(Builtin::List( - env.arena.alloc(element_layout), - ))) -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct LayoutId(u32); - -impl LayoutId { - // Returns something like "foo#1" when given a symbol that interns to "foo" - // and a LayoutId of 1. - pub fn to_symbol_string(self, symbol: Symbol, interns: &Interns) -> String { - let ident_string = symbol.as_str(interns); - let module_string = interns.module_ids.get_name(symbol.module_id()).unwrap(); - format!("{}_{}_{}", module_string, ident_string, self.0) - } -} - -struct IdsByLayout<'a> { - by_id: MutMap, u32>, - toplevels_by_id: MutMap, u32>, - next_id: u32, -} - -impl<'a> IdsByLayout<'a> { - #[inline(always)] - fn insert_layout(&mut self, layout: Layout<'a>) -> LayoutId { - match self.by_id.entry(layout) { - Entry::Vacant(vacant) => { - let answer = self.next_id; - vacant.insert(answer); - self.next_id += 1; - - LayoutId(answer) - } - Entry::Occupied(occupied) => LayoutId(*occupied.get()), - } - } - - #[inline(always)] - fn singleton_layout(layout: Layout<'a>) -> (Self, LayoutId) { - let mut by_id = HashMap::with_capacity_and_hasher(1, default_hasher()); - by_id.insert(layout, 1); - - let ids_by_layout = IdsByLayout { - by_id, - toplevels_by_id: Default::default(), - next_id: 2, - }; - - (ids_by_layout, LayoutId(1)) - } - - #[inline(always)] - fn insert_toplevel(&mut self, layout: crate::ir::ProcLayout<'a>) -> LayoutId { - match self.toplevels_by_id.entry(layout) { - Entry::Vacant(vacant) => { - let answer = self.next_id; - vacant.insert(answer); - self.next_id += 1; - - LayoutId(answer) - } - Entry::Occupied(occupied) => LayoutId(*occupied.get()), - } - } - - #[inline(always)] - fn singleton_toplevel(layout: crate::ir::ProcLayout<'a>) -> (Self, LayoutId) { - let mut toplevels_by_id = HashMap::with_capacity_and_hasher(1, default_hasher()); - toplevels_by_id.insert(layout, 1); - - let ids_by_layout = IdsByLayout { - by_id: Default::default(), - toplevels_by_id, - next_id: 2, - }; - - (ids_by_layout, LayoutId(1)) - } -} - -#[derive(Default)] -pub struct LayoutIds<'a> { - by_symbol: MutMap>, -} - -impl<'a> LayoutIds<'a> { - /// Returns a LayoutId which is unique for the given symbol and layout. - /// If given the same symbol and same layout, returns the same LayoutId. - #[inline(always)] - pub fn get<'b>(&mut self, symbol: Symbol, layout: &'b Layout<'a>) -> LayoutId { - match self.by_symbol.entry(symbol) { - Entry::Vacant(vacant) => { - let (ids_by_layout, layout_id) = IdsByLayout::singleton_layout(*layout); - - vacant.insert(ids_by_layout); - - layout_id - } - Entry::Occupied(mut occupied_ids) => occupied_ids.get_mut().insert_layout(*layout), - } - } - - /// Returns a LayoutId which is unique for the given symbol and layout. - /// If given the same symbol and same layout, returns the same LayoutId. - #[inline(always)] - pub fn get_toplevel<'b>( - &mut self, - symbol: Symbol, - layout: &'b crate::ir::ProcLayout<'a>, - ) -> LayoutId { - match self.by_symbol.entry(symbol) { - Entry::Vacant(vacant) => { - let (ids_by_layout, layout_id) = IdsByLayout::singleton_toplevel(*layout); - - vacant.insert(ids_by_layout); - - layout_id - } - Entry::Occupied(mut occupied_ids) => occupied_ids.get_mut().insert_toplevel(*layout), - } - } -} - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn width_and_alignment_union_empty_struct() { - let lambda_set = LambdaSet { - set: &[(Symbol::LIST_MAP, &[])], - representation: &Layout::UNIT, - }; - - let a = &[Layout::UNIT] as &[_]; - let b = &[Layout::LambdaSet(lambda_set)] as &[_]; - let tt = [a, b]; - - let layout = Layout::Union(UnionLayout::NonRecursive(&tt)); - - let target_info = TargetInfo::default_x86_64(); - assert_eq!(layout.stack_size(target_info), 1); - assert_eq!(layout.alignment_bytes(target_info), 1); - } - - #[test] - fn memcpy_size_result_u32_unit() { - let ok_tag = &[Layout::Builtin(Builtin::Int(IntWidth::U32))]; - let err_tag = &[Layout::UNIT]; - let tags = [ok_tag as &[_], err_tag as &[_]]; - let union_layout = UnionLayout::NonRecursive(&tags as &[_]); - let layout = Layout::Union(union_layout); - - let target_info = TargetInfo::default_x86_64(); - assert_eq!(layout.stack_size_without_alignment(target_info), 5); - } -} - -/// Compare two fields when sorting them for code gen. -/// This is called by both code gen and bindgen, so that -/// their field orderings agree. -#[inline(always)] -pub fn cmp_fields( - label1: &L, - layout1: &Layout<'_>, - label2: &L, - layout2: &Layout<'_>, - target_info: TargetInfo, -) -> Ordering { - let size1 = layout1.alignment_bytes(target_info); - let size2 = layout2.alignment_bytes(target_info); - - size2.cmp(&size1).then(label1.cmp(label2)) -} diff --git a/compiler/mono/src/layout_soa.rs b/compiler/mono/src/layout_soa.rs deleted file mode 100644 index 93a57d2bd0..0000000000 --- a/compiler/mono/src/layout_soa.rs +++ /dev/null @@ -1,907 +0,0 @@ -use crate::layout::{ext_var_is_empty_record, ext_var_is_empty_tag_union}; -use roc_builtins::bitcode::{FloatWidth, IntWidth}; -use roc_collections::all::MutMap; -use roc_module::symbol::Symbol; -use roc_target::TargetInfo; -use roc_types::subs::{self, Content, FlatType, Subs, Variable}; -use roc_types::types::RecordField; -use std::collections::hash_map::Entry; - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct Index { - index: u32, - _marker: std::marker::PhantomData, -} - -impl Index { - pub const fn new(index: u32) -> Self { - Self { - index, - _marker: std::marker::PhantomData, - } - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct Slice { - start: u32, - length: u16, - _marker: std::marker::PhantomData, -} - -impl Slice { - pub const fn new(start: u32, length: u16) -> Self { - Self { - start, - length, - _marker: std::marker::PhantomData, - } - } - - pub const fn len(&self) -> usize { - self.length as _ - } - - pub const fn is_empty(&self) -> bool { - self.length == 0 - } - - pub const fn indices(&self) -> std::ops::Range { - self.start as usize..(self.start as usize + self.length as usize) - } - - pub fn into_iter(&self) -> impl Iterator> { - self.indices().map(|i| Index::new(i as _)) - } -} - -trait Reserve { - fn reserve(layouts: &mut Layouts, length: usize) -> Self; -} - -impl Reserve for Slice { - fn reserve(layouts: &mut Layouts, length: usize) -> Self { - let start = layouts.layouts.len() as u32; - - let it = std::iter::repeat(Layout::Reserved).take(length); - layouts.layouts.extend(it); - - Self { - start, - length: length as u16, - _marker: Default::default(), - } - } -} - -impl Reserve for Slice> { - fn reserve(layouts: &mut Layouts, length: usize) -> Self { - let start = layouts.layout_slices.len() as u32; - - let empty: Slice = Slice::new(0, 0); - let it = std::iter::repeat(empty).take(length); - layouts.layout_slices.extend(it); - - Self { - start, - length: length as u16, - _marker: Default::default(), - } - } -} - -static_assertions::assert_eq_size!([u8; 12], Layout); - -pub struct Layouts { - layouts: Vec, - layout_slices: Vec>, - // function_layouts: Vec<(Slice, Index)>, - lambda_sets: Vec, - symbols: Vec, - recursion_variable_to_structure_variable_map: MutMap>, - target_info: TargetInfo, -} - -pub struct FunctionLayout { - /// last element is the result, prior elements the arguments - arguments_and_result: Slice, - pub lambda_set: Index, -} - -impl FunctionLayout { - pub fn from_var( - layouts: &mut Layouts, - subs: &Subs, - var: Variable, - ) -> Result { - // so we can set some things/clean up - Self::from_var_help(layouts, subs, var) - } - - fn from_var_help( - layouts: &mut Layouts, - subs: &Subs, - var: Variable, - ) -> Result { - let content = &subs.get_content_without_compacting(var); - Self::from_content(layouts, subs, var, content) - } - - fn from_content( - layouts: &mut Layouts, - subs: &Subs, - var: Variable, - content: &Content, - ) -> Result { - use LayoutError::*; - - match content { - Content::FlexVar(_) - | Content::RigidVar(_) - | Content::FlexAbleVar(_, _) - | Content::RigidAbleVar(_, _) => Err(UnresolvedVariable(var)), - Content::RecursionVar { .. } => Err(TypeError(())), - Content::LambdaSet(lset) => Self::from_lambda_set(layouts, subs, *lset), - Content::Structure(flat_type) => Self::from_flat_type(layouts, subs, flat_type), - Content::Alias(_, _, actual, _) => Self::from_var_help(layouts, subs, *actual), - Content::RangedNumber(actual, _) => Self::from_var_help(layouts, subs, *actual), - Content::Error => Err(TypeError(())), - } - } - - fn from_lambda_set( - _layouts: &mut Layouts, - _subs: &Subs, - _lset: subs::LambdaSet, - ) -> Result { - todo!(); - } - - fn from_flat_type( - layouts: &mut Layouts, - subs: &Subs, - flat_type: &FlatType, - ) -> Result { - use LayoutError::*; - - match flat_type { - FlatType::Func(arguments, lambda_set, result) => { - let slice = Slice::reserve(layouts, arguments.len() + 1); - - let variable_slice = &subs.variables[arguments.indices()]; - let it = slice.indices().zip(variable_slice); - for (target_index, var) in it { - let layout = Layout::from_var_help(layouts, subs, *var)?; - layouts.layouts[target_index] = layout; - } - - let result_layout = Layout::from_var_help(layouts, subs, *result)?; - let result_index: Index = Index::new(slice.start + slice.len() as u32 - 1); - layouts.layouts[result_index.index as usize] = result_layout; - - let lambda_set = LambdaSet::from_var(layouts, subs, *lambda_set)?; - let lambda_set_index = Index::new(layouts.lambda_sets.len() as u32); - layouts.lambda_sets.push(lambda_set); - - Ok(Self { - arguments_and_result: slice, - lambda_set: lambda_set_index, - }) - } - - FlatType::Erroneous(_) => Err(TypeError(())), - - _ => todo!(), - } - } - - pub fn argument_slice(&self) -> Slice { - let mut result = self.arguments_and_result; - result.length -= 1; - - result - } - pub fn result_index(&self) -> Index { - Index::new(self.arguments_and_result.start + self.arguments_and_result.length as u32 - 1) - } -} - -/// Idea: don't include the symbols for the first 3 cases in --optimize mode -pub enum LambdaSet { - Empty { - symbol: Index, - }, - Single { - symbol: Index, - layout: Index, - }, - Struct { - symbol: Index, - layouts: Slice, - }, - Union { - symbols: Slice, - layouts: Slice>, - }, -} - -impl LambdaSet { - pub fn from_var( - layouts: &mut Layouts, - subs: &Subs, - var: Variable, - ) -> Result { - // so we can set some things/clean up - Self::from_var_help(layouts, subs, var) - } - - fn from_var_help( - layouts: &mut Layouts, - subs: &Subs, - var: Variable, - ) -> Result { - let content = &subs.get_content_without_compacting(var); - Self::from_content(layouts, subs, var, content) - } - - fn from_content( - layouts: &mut Layouts, - subs: &Subs, - var: Variable, - content: &Content, - ) -> Result { - use LayoutError::*; - - match content { - Content::FlexVar(_) - | Content::RigidVar(_) - | Content::FlexAbleVar(_, _) - | Content::RigidAbleVar(_, _) => Err(UnresolvedVariable(var)), - Content::RecursionVar { .. } => { - unreachable!("lambda sets cannot currently be recursive") - } - Content::LambdaSet(lset) => Self::from_lambda_set(layouts, subs, *lset), - Content::Structure(_flat_type) => unreachable!(), - Content::Alias(_, _, actual, _) => Self::from_var_help(layouts, subs, *actual), - Content::RangedNumber(actual, _) => Self::from_var_help(layouts, subs, *actual), - Content::Error => Err(TypeError(())), - } - } - - fn from_lambda_set( - layouts: &mut Layouts, - subs: &Subs, - lset: subs::LambdaSet, - ) -> Result { - let subs::LambdaSet { - solved, - recursion_var: _, - unspecialized: _, - } = lset; - - // TODO: handle unspecialized - - debug_assert!( - !solved.is_empty(), - "lambda set must contain atleast the function itself" - ); - - let lambda_names = solved.labels(); - let closure_names = Self::get_closure_names(layouts, subs, lambda_names); - - let variables = solved.variables(); - if variables.len() == 1 { - let symbol = subs.closure_names[lambda_names.start as usize]; - let symbol_index = Index::new(layouts.symbols.len() as u32); - layouts.symbols.push(symbol); - let variable_slice = subs.variable_slices[variables.start as usize]; - - match variable_slice.len() { - 0 => Ok(LambdaSet::Empty { - symbol: symbol_index, - }), - 1 => { - let var = subs.variables[variable_slice.start as usize]; - let layout = Layout::from_var(layouts, subs, var)?; - - let index = Index::new(layouts.layouts.len() as u32); - layouts.layouts.push(layout); - - Ok(LambdaSet::Single { - symbol: symbol_index, - layout: index, - }) - } - _ => { - let slice = Layout::from_variable_slice(layouts, subs, variable_slice)?; - - Ok(LambdaSet::Struct { - symbol: symbol_index, - layouts: slice, - }) - } - } - } else { - let layouts = Layout::from_slice_variable_slice(layouts, subs, solved.variables())?; - - Ok(LambdaSet::Union { - symbols: closure_names, - layouts, - }) - } - } - - fn get_closure_names( - layouts: &mut Layouts, - subs: &Subs, - subs_slice: roc_types::subs::SubsSlice, - ) -> Slice { - let slice = Slice::new(layouts.symbols.len() as u32, subs_slice.len() as u16); - - let symbols = &subs.closure_names[subs_slice.indices()]; - - for symbol in symbols { - layouts.symbols.push(*symbol); - } - - slice - } -} - -#[derive(Debug, Clone, Copy, PartialEq)] -pub enum Layout { - // theory: we can zero out memory to reserve space for many layouts - Reserved, - - // Question: where to store signedness information? - Int(IntWidth), - Float(FloatWidth), - Decimal, - - Str, - Dict(Index<(Layout, Layout)>), - Set(Index), - List(Index), - - Struct(Slice), - - UnionNonRecursive(Slice>), - - Boxed(Index), - UnionRecursive(Slice>), - // UnionNonNullableUnwrapped(Slice), - // UnionNullableWrapper { - // data: NullableUnionIndex, - // tag_id: u16, - // }, - // - // UnionNullableUnwrappedTrue(Slice), - // UnionNullableUnwrappedFalse(Slice), - - // RecursivePointer, -} - -fn round_up_to_alignment(unaligned: u16, alignment_bytes: u16) -> u16 { - let unaligned = unaligned as i32; - let alignment_bytes = alignment_bytes as i32; - if alignment_bytes <= 1 { - return unaligned as u16; - } - if alignment_bytes.count_ones() != 1 { - panic!( - "Cannot align to {} bytes. Not a power of 2.", - alignment_bytes - ); - } - let mut aligned = unaligned; - aligned += alignment_bytes - 1; // if lower bits are non-zero, push it over the next boundary - aligned &= -alignment_bytes; // mask with a flag that has upper bits 1, lower bits 0 - - aligned as u16 -} - -impl Layouts { - const VOID_INDEX: Index = Index::new(0); - const VOID_TUPLE: Index<(Layout, Layout)> = Index::new(0); - const UNIT_INDEX: Index = Index::new(2); - - pub fn new(target_info: TargetInfo) -> Self { - let mut layouts = Vec::with_capacity(64); - - layouts.push(Layout::VOID); - layouts.push(Layout::VOID); - layouts.push(Layout::UNIT); - - // sanity check - debug_assert_eq!(layouts[Self::VOID_INDEX.index as usize], Layout::VOID); - debug_assert_eq!(layouts[Self::VOID_TUPLE.index as usize + 1], Layout::VOID); - debug_assert_eq!(layouts[Self::UNIT_INDEX.index as usize], Layout::UNIT); - - Layouts { - layouts: Vec::default(), - layout_slices: Vec::default(), - lambda_sets: Vec::default(), - symbols: Vec::default(), - recursion_variable_to_structure_variable_map: MutMap::default(), - target_info, - } - } - - /// sort a slice according to elements' alignment - fn sort_slice_by_alignment(&mut self, layout_slice: Slice) { - let slice = &mut self.layouts[layout_slice.indices()]; - - // SAFETY: the align_of function does not mutate the layouts vector - // this unsafety is required to circumvent the borrow checker - let sneaky_slice = - unsafe { std::slice::from_raw_parts_mut(slice.as_mut_ptr(), slice.len()) }; - - sneaky_slice.sort_by(|layout1, layout2| { - let align1 = self.align_of_layout(*layout1); - let align2 = self.align_of_layout(*layout2); - - // we want the biggest alignment first - align2.cmp(&align1) - }); - } - - fn usize(&self) -> Layout { - let usize_int_width = match self.target_info.ptr_width() { - roc_target::PtrWidth::Bytes4 => IntWidth::U32, - roc_target::PtrWidth::Bytes8 => IntWidth::U64, - }; - - Layout::Int(usize_int_width) - } - - fn align_of_layout_index(&self, index: Index) -> u16 { - let layout = self.layouts[index.index as usize]; - - self.align_of_layout(layout) - } - - fn align_of_layout(&self, layout: Layout) -> u16 { - let usize_int_width = match self.target_info.ptr_width() { - roc_target::PtrWidth::Bytes4 => IntWidth::U32, - roc_target::PtrWidth::Bytes8 => IntWidth::U64, - }; - - let ptr_alignment = usize_int_width.alignment_bytes(self.target_info) as u16; - - match layout { - Layout::Reserved => unreachable!(), - Layout::Int(int_width) => int_width.alignment_bytes(self.target_info) as u16, - Layout::Float(float_width) => float_width.alignment_bytes(self.target_info) as u16, - Layout::Decimal => IntWidth::U128.alignment_bytes(self.target_info) as u16, - Layout::Str | Layout::Dict(_) | Layout::Set(_) | Layout::List(_) => ptr_alignment, - Layout::Struct(slice) => self.align_of_layout_slice(slice), - Layout::Boxed(_) | Layout::UnionRecursive(_) => ptr_alignment, - Layout::UnionNonRecursive(slices) => { - let tag_id_align = IntWidth::I64.alignment_bytes(self.target_info) as u16; - - self.align_of_layout_slices(slices).max(tag_id_align) - } -// Layout::UnionNonNullableUnwrapped(_) => todo!(), -// Layout::UnionNullableWrapper { data, tag_id } => todo!(), -// Layout::UnionNullableUnwrappedTrue(_) => todo!(), -// Layout::UnionNullableUnwrappedFalse(_) => todo!(), -// Layout::RecursivePointer => todo!(), - } - } - - /// Invariant: the layouts are sorted from biggest to smallest alignment - fn align_of_layout_slice(&self, slice: Slice) -> u16 { - match slice.into_iter().next() { - None => 0, - Some(first_index) => self.align_of_layout_index(first_index), - } - } - - fn align_of_layout_slices(&self, slice: Slice>) -> u16 { - slice - .into_iter() - .map(|index| self.layout_slices[index.index as usize]) - .map(|slice| self.align_of_layout_slice(slice)) - .max() - .unwrap_or_default() - } - - /// Invariant: the layouts are sorted from biggest to smallest alignment - fn size_of_layout_slice(&self, slice: Slice) -> u16 { - match slice.into_iter().next() { - None => 0, - Some(first_index) => { - let alignment = self.align_of_layout_index(first_index); - - let mut sum = 0; - - for index in slice.into_iter() { - sum += self.size_of_layout_index(index); - } - - round_up_to_alignment(sum, alignment) - } - } - } - - pub fn size_of_layout_index(&self, index: Index) -> u16 { - let layout = self.layouts[index.index as usize]; - - self.size_of_layout(layout) - } - - pub fn size_of_layout(&self, layout: Layout) -> u16 { - let usize_int_width = match self.target_info.ptr_width() { - roc_target::PtrWidth::Bytes4 => IntWidth::U32, - roc_target::PtrWidth::Bytes8 => IntWidth::U64, - }; - - let ptr_width = usize_int_width.stack_size() as u16; - - match layout { - Layout::Reserved => unreachable!(), - Layout::Int(int_width) => int_width.stack_size() as _, - Layout::Float(float_width) => float_width as _, - Layout::Decimal => (std::mem::size_of::()) as _, - Layout::Str | Layout::Dict(_) | Layout::Set(_) | Layout::List(_) => 2 * ptr_width, - Layout::Struct(slice) => self.size_of_layout_slice(slice), - Layout::Boxed(_) | Layout::UnionRecursive(_) => ptr_width, - Layout::UnionNonRecursive(slices) if slices.is_empty() => 0, - Layout::UnionNonRecursive(slices) => { - let tag_id = IntWidth::I64; - - let max_slice_size = slices - .into_iter() - .map(|index| self.layout_slices[index.index as usize]) - .map(|slice| self.align_of_layout_slice(slice)) - .max() - .unwrap_or_default(); - - tag_id.stack_size() as u16 + max_slice_size - } -// Layout::UnionNonNullableUnwrapped(_) => todo!(), -// Layout::UnionNullableWrapper { data, tag_id } => todo!(), -// Layout::UnionNullableUnwrappedTrue(_) => todo!(), -// Layout::UnionNullableUnwrappedFalse(_) => todo!(), -// Layout::RecursivePointer => todo!(), - } - } -} - -pub enum LayoutError { - UnresolvedVariable(Variable), - TypeError(()), -} - -impl Layout { - pub const UNIT: Self = Self::Struct(Slice::new(0, 0)); - pub const VOID: Self = Self::UnionNonRecursive(Slice::new(0, 0)); - - pub const EMPTY_LIST: Self = Self::List(Layouts::VOID_INDEX); - pub const EMPTY_DICT: Self = Self::Dict(Layouts::VOID_TUPLE); - pub const EMPTY_SET: Self = Self::Set(Layouts::VOID_INDEX); - - pub fn from_var( - layouts: &mut Layouts, - subs: &Subs, - var: Variable, - ) -> Result { - // so we can set some things/clean up - Self::from_var_help(layouts, subs, var) - } - - fn from_var_help( - layouts: &mut Layouts, - subs: &Subs, - var: Variable, - ) -> Result { - let content = &subs.get_content_without_compacting(var); - Self::from_content(layouts, subs, var, content) - } - - /// Used in situations where an unspecialized variable is not a problem, - /// and we can substitute with `[]`, the empty tag union. - /// e.g. an empty list literal has type `List *`. We can still generate code - /// in those cases by just picking any concrete type for the list element, - /// and we pick the empty tag union in practice. - fn from_var_help_or_void( - layouts: &mut Layouts, - subs: &Subs, - var: Variable, - ) -> Result { - let content = &subs.get_content_without_compacting(var); - - match content { - Content::FlexVar(_) | Content::RigidVar(_) => Ok(Layout::VOID), - - _ => Self::from_content(layouts, subs, var, content), - } - } - - fn from_content( - layouts: &mut Layouts, - subs: &Subs, - var: Variable, - content: &Content, - ) -> Result { - use LayoutError::*; - - match content { - Content::FlexVar(_) - | Content::RigidVar(_) - | Content::FlexAbleVar(_, _) - | Content::RigidAbleVar(_, _) => Err(UnresolvedVariable(var)), - Content::RecursionVar { - structure, - opt_name: _, - } => { - let structure = subs.get_root_key_without_compacting(*structure); - - let entry = layouts - .recursion_variable_to_structure_variable_map - .entry(structure); - - match entry { - Entry::Vacant(vacant) => { - let reserved = Index::new(layouts.layouts.len() as _); - layouts.layouts.push(Layout::Reserved); - - vacant.insert(reserved); - - let layout = Layout::from_var(layouts, subs, structure)?; - - layouts.layouts[reserved.index as usize] = layout; - - Ok(Layout::Boxed(reserved)) - } - Entry::Occupied(occupied) => { - let index = occupied.get(); - - Ok(Layout::Boxed(*index)) - } - } - } - // Lambda set layout is same as tag union - Content::LambdaSet(lset) => Self::from_lambda_set(layouts, subs, *lset), - Content::Structure(flat_type) => Self::from_flat_type(layouts, subs, flat_type), - Content::Alias(symbol, _, actual, _) => { - let symbol = *symbol; - - if let Some(int_width) = IntWidth::try_from_symbol(symbol) { - return Ok(Layout::Int(int_width)); - } - - if let Some(float_width) = FloatWidth::try_from_symbol(symbol) { - return Ok(Layout::Float(float_width)); - } - - match symbol { - Symbol::NUM_DECIMAL => Ok(Layout::Decimal), - - Symbol::NUM_NAT | Symbol::NUM_NATURAL => Ok(layouts.usize()), - - _ => { - // at this point we throw away alias information - Self::from_var_help(layouts, subs, *actual) - } - } - } - Content::RangedNumber(typ, _) => Self::from_var_help(layouts, subs, *typ), - Content::Error => Err(TypeError(())), - } - } - - fn from_lambda_set( - layouts: &mut Layouts, - subs: &Subs, - lset: subs::LambdaSet, - ) -> Result { - let subs::LambdaSet { - solved, - recursion_var, - unspecialized: _, - } = lset; - - // TODO: handle unspecialized lambda set - - match recursion_var.into_variable() { - Some(rec_var) => { - let rec_var = subs.get_root_key_without_compacting(rec_var); - - let cached = layouts - .recursion_variable_to_structure_variable_map - .get(&rec_var); - - if let Some(layout_index) = cached { - match layouts.layouts[layout_index.index as usize] { - Layout::Reserved => { - // we have to do the work here to fill this reserved variable in - } - other => { - return Ok(other); - } - } - } - - let slices = Self::from_slice_variable_slice(layouts, subs, solved.variables())?; - - Ok(Layout::UnionRecursive(slices)) - } - None => { - let slices = Self::from_slice_variable_slice(layouts, subs, solved.variables())?; - - Ok(Layout::UnionNonRecursive(slices)) - } - } - } - - fn from_flat_type( - layouts: &mut Layouts, - subs: &Subs, - flat_type: &FlatType, - ) -> Result { - use LayoutError::*; - - match flat_type { - FlatType::Apply(Symbol::LIST_LIST, arguments) => { - debug_assert_eq!(arguments.len(), 1); - - let element_var = subs.variables[arguments.start as usize]; - let element_layout = Self::from_var_help_or_void(layouts, subs, element_var)?; - - let element_index = Index::new(layouts.layouts.len() as _); - layouts.layouts.push(element_layout); - - Ok(Layout::List(element_index)) - } - - FlatType::Apply(Symbol::DICT_DICT, arguments) => { - debug_assert_eq!(arguments.len(), 2); - - let key_var = subs.variables[arguments.start as usize]; - let value_var = subs.variables[arguments.start as usize + 1]; - - let key_layout = Self::from_var_help_or_void(layouts, subs, key_var)?; - let value_layout = Self::from_var_help_or_void(layouts, subs, value_var)?; - - let index = Index::new(layouts.layouts.len() as _); - layouts.layouts.push(key_layout); - layouts.layouts.push(value_layout); - - Ok(Layout::Dict(index)) - } - - FlatType::Apply(Symbol::SET_SET, arguments) => { - debug_assert_eq!(arguments.len(), 1); - - let element_var = subs.variables[arguments.start as usize]; - let element_layout = Self::from_var_help_or_void(layouts, subs, element_var)?; - - let element_index = Index::new(layouts.layouts.len() as _); - layouts.layouts.push(element_layout); - - Ok(Layout::Set(element_index)) - } - - FlatType::Apply(symbol, _) => { - unreachable!("Symbol {:?} does not have a layout", symbol) - } - - FlatType::Func(_arguments, lambda_set, _result) => { - // in this case, a function (pointer) is represented by the environment it - // captures: the lambda set - - Self::from_var_help(layouts, subs, *lambda_set) - } - FlatType::Record(fields, ext) => { - debug_assert!(ext_var_is_empty_record(subs, *ext)); - - let mut slice = Slice::reserve(layouts, fields.len()); - - let mut non_optional_fields = 0; - let it = slice.indices().zip(fields.iter_all()); - for (target_index, (_, field_index, var_index)) in it { - match subs.record_fields[field_index.index as usize] { - RecordField::Optional(_) => { - // do nothing - } - RecordField::Required(_) | RecordField::Demanded(_) => { - let var = subs.variables[var_index.index as usize]; - let layout = Layout::from_var_help(layouts, subs, var)?; - - layouts.layouts[target_index] = layout; - - non_optional_fields += 1; - } - } - } - - // we have some wasted space in the case of optional fields; so be it - slice.length = non_optional_fields; - - layouts.sort_slice_by_alignment(slice); - - Ok(Layout::Struct(slice)) - } - FlatType::TagUnion(union_tags, ext) => { - debug_assert!(ext_var_is_empty_tag_union(subs, *ext)); - - let slices = - Self::from_slice_variable_slice(layouts, subs, union_tags.variables())?; - - Ok(Layout::UnionNonRecursive(slices)) - } - - FlatType::FunctionOrTagUnion(_, _, ext) => { - debug_assert!(ext_var_is_empty_tag_union(subs, *ext)); - - // at this point we know this is a tag - Ok(Layout::UNIT) - } - FlatType::RecursiveTagUnion(rec_var, union_tags, ext) => { - debug_assert!(ext_var_is_empty_tag_union(subs, *ext)); - - let rec_var = subs.get_root_key_without_compacting(*rec_var); - - let cached = layouts - .recursion_variable_to_structure_variable_map - .get(&rec_var); - - if let Some(layout_index) = cached { - match layouts.layouts[layout_index.index as usize] { - Layout::Reserved => { - // we have to do the work here to fill this reserved variable in - } - other => { - return Ok(other); - } - } - } - - let slices = - Self::from_slice_variable_slice(layouts, subs, union_tags.variables())?; - - Ok(Layout::UnionRecursive(slices)) - } - FlatType::Erroneous(_) => Err(TypeError(())), - FlatType::EmptyRecord => Ok(Layout::UNIT), - FlatType::EmptyTagUnion => Ok(Layout::VOID), - } - } - - fn from_slice_variable_slice( - layouts: &mut Layouts, - subs: &Subs, - slice_variable_slice: roc_types::subs::SubsSlice, - ) -> Result>, LayoutError> { - let slice = Slice::reserve(layouts, slice_variable_slice.len()); - - let variable_slices = &subs.variable_slices[slice_variable_slice.indices()]; - let it = slice.indices().zip(variable_slices); - for (target_index, variable_slice) in it { - let layout_slice = Layout::from_variable_slice(layouts, subs, *variable_slice)?; - layouts.layout_slices[target_index] = layout_slice; - } - - Ok(slice) - } - - fn from_variable_slice( - layouts: &mut Layouts, - subs: &Subs, - variable_subs_slice: roc_types::subs::VariableSubsSlice, - ) -> Result, LayoutError> { - let slice = Slice::reserve(layouts, variable_subs_slice.len()); - - let variable_slice = &subs.variables[variable_subs_slice.indices()]; - let it = slice.indices().zip(variable_slice); - for (target_index, var) in it { - let layout = Layout::from_var_help(layouts, subs, *var)?; - layouts.layouts[target_index] = layout; - } - - layouts.sort_slice_by_alignment(slice); - - Ok(slice) - } -} diff --git a/compiler/mono/src/lib.rs b/compiler/mono/src/lib.rs deleted file mode 100644 index aa8dfba4c8..0000000000 --- a/compiler/mono/src/lib.rs +++ /dev/null @@ -1,19 +0,0 @@ -#![warn(clippy::dbg_macro)] -// See github.com/rtfeldman/roc/issues/800 for discussion of the large_enum_variant check. -#![allow(clippy::large_enum_variant, clippy::upper_case_acronyms)] - -pub mod borrow; -pub mod code_gen_help; -mod copy; -pub mod inc_dec; -pub mod ir; -pub mod layout; -pub mod layout_soa; -pub mod low_level; -pub mod reset_reuse; -pub mod tail_recursion; - -// Temporary, while we can build up test cases and optimize the exhaustiveness checking. -// For now, following this warning's advice will lead to nasty type inference errors. -//#[allow(clippy::ptr_arg)] -pub mod decision_tree; diff --git a/compiler/mono/src/low_level.rs b/compiler/mono/src/low_level.rs deleted file mode 100644 index 7660c05627..0000000000 --- a/compiler/mono/src/low_level.rs +++ /dev/null @@ -1,209 +0,0 @@ -use roc_module::symbol::Symbol; - -#[derive(Clone, Copy, Debug, PartialEq)] -pub enum HigherOrder { - ListMap { - xs: Symbol, - }, - ListMap2 { - xs: Symbol, - ys: Symbol, - }, - ListMap3 { - xs: Symbol, - ys: Symbol, - zs: Symbol, - }, - ListMap4 { - xs: Symbol, - ys: Symbol, - zs: Symbol, - ws: Symbol, - }, - ListMapWithIndex { - xs: Symbol, - }, - ListKeepIf { - xs: Symbol, - }, - ListWalk { - xs: Symbol, - state: Symbol, - }, - ListWalkUntil { - xs: Symbol, - state: Symbol, - }, - ListWalkBackwards { - xs: Symbol, - state: Symbol, - }, - ListKeepOks { - xs: Symbol, - }, - ListKeepErrs { - xs: Symbol, - }, - ListSortWith { - xs: Symbol, - }, - ListAny { - xs: Symbol, - }, - ListAll { - xs: Symbol, - }, - ListFindUnsafe { - xs: Symbol, - }, - DictWalk { - xs: Symbol, - state: Symbol, - }, -} - -impl HigherOrder { - pub fn function_arity(&self) -> usize { - match self { - HigherOrder::ListMap { .. } => 1, - HigherOrder::ListMap2 { .. } => 2, - HigherOrder::ListMap3 { .. } => 3, - HigherOrder::ListMap4 { .. } => 4, - HigherOrder::ListMapWithIndex { .. } => 2, - HigherOrder::ListKeepIf { .. } => 1, - HigherOrder::ListWalk { .. } => 2, - HigherOrder::ListWalkUntil { .. } => 2, - HigherOrder::ListWalkBackwards { .. } => 2, - HigherOrder::ListKeepOks { .. } => 1, - HigherOrder::ListKeepErrs { .. } => 1, - HigherOrder::ListSortWith { .. } => 2, - HigherOrder::ListFindUnsafe { .. } => 1, - HigherOrder::DictWalk { .. } => 2, - HigherOrder::ListAny { .. } => 1, - HigherOrder::ListAll { .. } => 1, - } - } - - /// Index in the array of arguments of the symbol that is the closure data - /// (captured environment for this function) - pub const fn closure_data_index(&self) -> usize { - use HigherOrder::*; - - match self { - ListMap { .. } - | ListMapWithIndex { .. } - | ListSortWith { .. } - | ListKeepIf { .. } - | ListKeepOks { .. } - | ListKeepErrs { .. } - | ListAny { .. } - | ListAll { .. } - | ListFindUnsafe { .. } => 2, - ListMap2 { .. } => 3, - ListMap3 { .. } => 4, - ListMap4 { .. } => 5, - ListWalk { .. } | ListWalkUntil { .. } | ListWalkBackwards { .. } | DictWalk { .. } => { - 3 - } - } - } - - /// Index of the function symbol in the argument list - pub const fn function_index(&self) -> usize { - self.closure_data_index() - 1 - } -} - -#[allow(dead_code)] -enum FirstOrder { - StrConcat, - StrJoinWith, - StrIsEmpty, - StrStartsWith, - StrStartsWithCodePt, - StrEndsWith, - StrSplit, - StrCountGraphemes, - StrFromInt, - StrFromUtf8, - StrFromUtf8Range, - StrToUtf8, - StrRepeat, - StrFromFloat, - ListLen, - ListGetUnsafe, - ListSet, - ListSublist, - ListDropAt, - ListSingle, - ListRepeat, - ListReverse, - ListConcat, - ListContains, - ListAppend, - ListPrepend, - ListJoin, - ListRange, - ListSwap, - DictSize, - DictEmpty, - DictInsert, - DictRemove, - DictContains, - DictGetUnsafe, - DictKeys, - DictValues, - DictUnion, - DictIntersection, - DictDifference, - SetFromList, - NumAdd, - NumAddWrap, - NumAddChecked, - NumSub, - NumSubWrap, - NumSubChecked, - NumMul, - NumMulWrap, - NumMulChecked, - NumGt, - NumGte, - NumLt, - NumLte, - NumCompare, - NumDivUnchecked, - NumRemUnchecked, - NumIsMultipleOf, - NumAbs, - NumNeg, - NumSin, - NumCos, - NumSqrtUnchecked, - NumLogUnchecked, - NumRound, - NumToFrac, - NumPow, - NumCeiling, - NumPowInt, - NumFloor, - NumIsFinite, - NumAtan, - NumAcos, - NumAsin, - NumBitwiseAnd, - NumBitwiseXor, - NumBitwiseOr, - NumShiftLeftBy, - NumShiftRightBy, - NumBytesToU16, - NumBytesToU32, - NumShiftRightZfBy, - NumIntCast, - NumFloatCast, - Eq, - NotEq, - And, - Or, - Not, - Hash, -} diff --git a/compiler/mono/src/reset_reuse.rs b/compiler/mono/src/reset_reuse.rs deleted file mode 100644 index 189a1b8481..0000000000 --- a/compiler/mono/src/reset_reuse.rs +++ /dev/null @@ -1,722 +0,0 @@ -//! This module inserts reset/reuse statements into the mono IR. These statements provide an -//! opportunity to reduce memory pressure by reusing memory slots of non-shared values. From the -//! introduction of the relevant paper: -//! -//! > [We] have added two additional instructions to our IR: `let y = reset x` and -//! > `let z = (reuse y in ctor_i w)`. The two instructions are used together; if `x` -//! > is a shared value, then `y` is set to a special reference, and the reuse instruction -//! > just allocates a new constructor value `ctor_i w`. If `x` is not shared, then reset -//! > decrements the reference counters of the components of `x`, and `y` is set to `x`. -//! > Then, reuse reuses the memory cell used by `x` to store the constructor value `ctor_i w`. -//! -//! See also -//! - [Counting Immutable Beans](https://arxiv.org/pdf/1908.05647.pdf) (Ullrich and Moura, 2020) -//! - [The lean implementation](https://github.com/leanprover/lean4/blob/master/src/Lean/Compiler/IR/ResetReuse.lean) - -use crate::inc_dec::{collect_stmt, occurring_variables_expr, JPLiveVarMap, LiveVarSet}; -use crate::ir::{ - BranchInfo, Call, Expr, ListLiteralElement, Proc, Stmt, UpdateModeId, UpdateModeIds, -}; -use crate::layout::{Layout, TagIdIntType, UnionLayout}; -use bumpalo::collections::Vec; -use bumpalo::Bump; -use roc_collections::all::MutSet; -use roc_module::symbol::{IdentIds, ModuleId, Symbol}; - -pub fn insert_reset_reuse<'a, 'i>( - arena: &'a Bump, - home: ModuleId, - ident_ids: &'i mut IdentIds, - update_mode_ids: &'i mut UpdateModeIds, - mut proc: Proc<'a>, -) -> Proc<'a> { - let mut env = Env { - arena, - home, - ident_ids, - update_mode_ids, - jp_live_vars: Default::default(), - }; - - let new_body = function_r(&mut env, arena.alloc(proc.body)); - proc.body = new_body.clone(); - - proc -} - -#[derive(Debug)] -struct CtorInfo<'a> { - id: TagIdIntType, - layout: UnionLayout<'a>, -} - -fn may_reuse(tag_layout: UnionLayout, tag_id: TagIdIntType, other: &CtorInfo) -> bool { - if tag_layout != other.layout { - return false; - } - - // we should not get here if the tag we matched on is represented as NULL - debug_assert!(!tag_layout.tag_is_null(other.id as _)); - - // furthermore, we can only use the memory if the tag we're creating is non-NULL - !tag_layout.tag_is_null(tag_id) -} - -#[derive(Debug)] -struct Env<'a, 'i> { - arena: &'a Bump, - - /// required for creating new `Symbol`s - home: ModuleId, - ident_ids: &'i mut IdentIds, - update_mode_ids: &'i mut UpdateModeIds, - - jp_live_vars: JPLiveVarMap, -} - -impl<'a, 'i> Env<'a, 'i> { - fn unique_symbol(&mut self) -> Symbol { - let ident_id = self.ident_ids.gen_unique(); - - Symbol::new(self.home, ident_id) - } -} - -fn function_s<'a, 'i>( - env: &mut Env<'a, 'i>, - w: Opportunity, - c: &CtorInfo<'a>, - stmt: &'a Stmt<'a>, -) -> &'a Stmt<'a> { - use Stmt::*; - - let arena = env.arena; - - match stmt { - Let(symbol, expr, layout, continuation) => match expr { - Expr::Tag { - tag_layout, - tag_id, - tag_name, - arguments, - } if may_reuse(*tag_layout, *tag_id, c) => { - // for now, always overwrite the tag ID just to be sure - let update_tag_id = true; - - let new_expr = Expr::Reuse { - symbol: w.symbol, - update_mode: w.update_mode, - update_tag_id, - tag_layout: *tag_layout, - tag_id: *tag_id, - tag_name: tag_name.clone(), - arguments, - }; - let new_stmt = Let(*symbol, new_expr, *layout, continuation); - - arena.alloc(new_stmt) - } - _ => { - let rest = function_s(env, w, c, continuation); - let new_stmt = Let(*symbol, expr.clone(), *layout, rest); - - arena.alloc(new_stmt) - } - }, - Join { - id, - parameters, - body, - remainder, - } => { - let id = *id; - let body: &Stmt = *body; - let new_body = function_s(env, w, c, body); - - let new_join = if std::ptr::eq(body, new_body) || body == new_body { - // the join point body will consume w - Join { - id, - parameters, - body: new_body, - remainder, - } - } else { - let new_remainder = function_s(env, w, c, remainder); - - Join { - id, - parameters, - body, - remainder: new_remainder, - } - }; - - arena.alloc(new_join) - } - Switch { - cond_symbol, - cond_layout, - branches, - default_branch, - ret_layout, - } => { - let mut new_branches = Vec::with_capacity_in(branches.len(), arena); - new_branches.extend(branches.iter().map(|(tag, info, body)| { - let new_body = function_s(env, w, c, body); - - (*tag, info.clone(), new_body.clone()) - })); - - let new_default = function_s(env, w, c, default_branch.1); - - let new_switch = Switch { - cond_symbol: *cond_symbol, - cond_layout: *cond_layout, - branches: new_branches.into_bump_slice(), - default_branch: (default_branch.0.clone(), new_default), - ret_layout: *ret_layout, - }; - - arena.alloc(new_switch) - } - Refcounting(op, continuation) => { - let continuation: &Stmt = *continuation; - let new_continuation = function_s(env, w, c, continuation); - - if std::ptr::eq(continuation, new_continuation) || continuation == new_continuation { - stmt - } else { - let new_refcounting = Refcounting(*op, new_continuation); - - arena.alloc(new_refcounting) - } - } - - Expect { - condition, - region, - lookups, - layouts, - remainder, - } => { - let continuation: &Stmt = *remainder; - let new_continuation = function_s(env, w, c, continuation); - - if std::ptr::eq(continuation, new_continuation) || continuation == new_continuation { - stmt - } else { - let new_refcounting = Expect { - condition: *condition, - region: *region, - lookups, - layouts, - remainder: new_continuation, - }; - - arena.alloc(new_refcounting) - } - } - - Ret(_) | Jump(_, _) | RuntimeError(_) => stmt, - } -} - -#[derive(Clone, Copy)] -struct Opportunity { - symbol: Symbol, - update_mode: UpdateModeId, -} - -fn try_function_s<'a, 'i>( - env: &mut Env<'a, 'i>, - x: Symbol, - c: &CtorInfo<'a>, - stmt: &'a Stmt<'a>, -) -> &'a Stmt<'a> { - let w = Opportunity { - symbol: env.unique_symbol(), - update_mode: env.update_mode_ids.next_id(), - }; - - let new_stmt = function_s(env, w, c, stmt); - - if std::ptr::eq(stmt, new_stmt) || stmt == new_stmt { - stmt - } else { - insert_reset(env, w, x, c.layout, new_stmt) - } -} - -fn insert_reset<'a>( - env: &mut Env<'a, '_>, - w: Opportunity, - x: Symbol, - union_layout: UnionLayout<'a>, - mut stmt: &'a Stmt<'a>, -) -> &'a Stmt<'a> { - use crate::ir::Expr::*; - - let mut stack = vec![]; - - while let Stmt::Let(symbol, expr, expr_layout, rest) = stmt { - match &expr { - StructAtIndex { .. } | GetTagId { .. } | UnionAtIndex { .. } => { - stack.push((symbol, expr, expr_layout)); - stmt = rest; - } - - ExprBox { .. } | ExprUnbox { .. } => { - // TODO - break; - } - - Literal(_) - | Call(_) - | Tag { .. } - | Struct(_) - | Array { .. } - | EmptyArray - | Reuse { .. } - | Reset { .. } - | RuntimeErrorFunction(_) => break, - } - } - - let reset_expr = Expr::Reset { - symbol: x, - update_mode: w.update_mode, - }; - - let layout = Layout::Union(union_layout); - - stmt = env - .arena - .alloc(Stmt::Let(w.symbol, reset_expr, layout, stmt)); - - for (symbol, expr, expr_layout) in stack.into_iter().rev() { - stmt = env - .arena - .alloc(Stmt::Let(*symbol, expr.clone(), *expr_layout, stmt)); - } - - stmt -} - -fn function_d_finalize<'a, 'i>( - env: &mut Env<'a, 'i>, - x: Symbol, - c: &CtorInfo<'a>, - output: (&'a Stmt<'a>, bool), -) -> &'a Stmt<'a> { - let (stmt, x_live_in_stmt) = output; - if x_live_in_stmt { - stmt - } else { - try_function_s(env, x, c, stmt) - } -} - -fn function_d_main<'a, 'i>( - env: &mut Env<'a, 'i>, - x: Symbol, - c: &CtorInfo<'a>, - stmt: &'a Stmt<'a>, -) -> (&'a Stmt<'a>, bool) { - use Stmt::*; - - let arena = env.arena; - - match stmt { - Let(symbol, expr, layout, continuation) => { - match expr { - Expr::Tag { arguments, .. } if arguments.iter().any(|s| *s == x) => { - // If the scrutinee `x` (the one that is providing memory) is being - // stored in a constructor, then reuse will probably not be able to reuse memory at runtime. - // It may work only if the new cell is consumed, but we ignore this case. - (stmt, true) - } - _ => { - let (b, found) = function_d_main(env, x, c, continuation); - - // NOTE the &b != continuation is not found in the Lean source, but is required - // otherwise we observe the same symbol being reset twice - let mut result = MutSet::default(); - if found - || { - occurring_variables_expr(expr, &mut result); - !result.contains(&x) - } - || &b != continuation - { - let let_stmt = Let(*symbol, expr.clone(), *layout, b); - - (arena.alloc(let_stmt), found) - } else { - let b = try_function_s(env, x, c, b); - let let_stmt = Let(*symbol, expr.clone(), *layout, b); - - (arena.alloc(let_stmt), found) - } - } - } - } - Switch { - cond_symbol, - cond_layout, - branches, - default_branch, - ret_layout, - } => { - if has_live_var(&env.jp_live_vars, stmt, x) { - // if `x` is live in `stmt`, we recursively process each branch - let mut new_branches = Vec::with_capacity_in(branches.len(), arena); - - for (tag, info, body) in branches.iter() { - let temp = function_d_main(env, x, c, body); - let new_body = function_d_finalize(env, x, c, temp); - - new_branches.push((*tag, info.clone(), new_body.clone())); - } - - let new_default = { - let (info, body) = default_branch; - let temp = function_d_main(env, x, c, body); - let new_body = function_d_finalize(env, x, c, temp); - - (info.clone(), new_body) - }; - - let new_switch = Switch { - cond_symbol: *cond_symbol, - cond_layout: *cond_layout, - branches: new_branches.into_bump_slice(), - default_branch: new_default, - ret_layout: *ret_layout, - }; - - (arena.alloc(new_switch), true) - } else { - (stmt, false) - } - } - Refcounting(modify_rc, continuation) => { - let (b, found) = function_d_main(env, x, c, continuation); - - if found || modify_rc.get_symbol() != x { - let refcounting = Refcounting(*modify_rc, b); - - (arena.alloc(refcounting), found) - } else { - let b = try_function_s(env, x, c, b); - let refcounting = Refcounting(*modify_rc, b); - - (arena.alloc(refcounting), found) - } - } - - Expect { - condition, - region, - lookups, - layouts, - remainder, - } => { - let (b, found) = function_d_main(env, x, c, remainder); - - if found || *condition != x { - let refcounting = Expect { - condition: *condition, - region: *region, - lookups, - layouts, - remainder: b, - }; - - (arena.alloc(refcounting), found) - } else { - let b = try_function_s(env, x, c, b); - - let refcounting = Expect { - condition: *condition, - region: *region, - lookups, - layouts, - remainder: b, - }; - - (arena.alloc(refcounting), found) - } - } - Join { - id, - parameters, - body, - remainder, - } => { - env.jp_live_vars.insert(*id, LiveVarSet::default()); - - let body_live_vars = collect_stmt(body, &env.jp_live_vars, LiveVarSet::default()); - - env.jp_live_vars.insert(*id, body_live_vars); - - let (b, found) = function_d_main(env, x, c, remainder); - - let (v, _found) = function_d_main(env, x, c, body); - - env.jp_live_vars.remove(id); - - // If `found' == true`, then `Dmain b` must also have returned `(b, true)` since - // we assume the IR does not have dead join points. So, if `x` is live in `j` (i.e., `v`), - // then it must also live in `b` since `j` is reachable from `b` with a `jmp`. - // On the other hand, `x` may be live in `b` but dead in `j` (i.e., `v`). -/ - let new_join = Join { - id: *id, - parameters, - body: v, - remainder: b, - }; - - (arena.alloc(new_join), found) - } - Ret(_) | Jump(_, _) | RuntimeError(_) => (stmt, has_live_var(&env.jp_live_vars, stmt, x)), - } -} - -fn function_d<'a, 'i>( - env: &mut Env<'a, 'i>, - x: Symbol, - c: &CtorInfo<'a>, - stmt: &'a Stmt<'a>, -) -> &'a Stmt<'a> { - let temp = function_d_main(env, x, c, stmt); - - function_d_finalize(env, x, c, temp) -} - -fn function_r_branch_body<'a, 'i>( - env: &mut Env<'a, 'i>, - info: &BranchInfo<'a>, - body: &'a Stmt<'a>, -) -> &'a Stmt<'a> { - let temp = function_r(env, body); - - match info { - BranchInfo::None => temp, - BranchInfo::Constructor { - scrutinee, - layout, - tag_id, - } => match layout { - Layout::Union(UnionLayout::NonRecursive(_)) => temp, - Layout::Union(union_layout) if !union_layout.tag_is_null(*tag_id) => { - let ctor_info = CtorInfo { - layout: *union_layout, - id: *tag_id, - }; - function_d(env, *scrutinee, &ctor_info, temp) - } - _ => temp, - }, - } -} - -fn function_r<'a, 'i>(env: &mut Env<'a, 'i>, stmt: &'a Stmt<'a>) -> &'a Stmt<'a> { - use Stmt::*; - - let arena = env.arena; - - match stmt { - Switch { - cond_symbol, - cond_layout, - branches, - default_branch, - ret_layout, - } => { - let mut new_branches = Vec::with_capacity_in(branches.len(), arena); - - for (tag, info, body) in branches.iter() { - let new_body = function_r_branch_body(env, info, body); - - new_branches.push((*tag, info.clone(), new_body.clone())); - } - - let new_default = { - let (info, body) = default_branch; - - let new_body = function_r_branch_body(env, info, body); - - (info.clone(), new_body) - }; - - let new_switch = Switch { - cond_symbol: *cond_symbol, - cond_layout: *cond_layout, - branches: new_branches.into_bump_slice(), - default_branch: new_default, - ret_layout: *ret_layout, - }; - - arena.alloc(new_switch) - } - - Join { - id, - parameters, - body, - remainder, - } => { - env.jp_live_vars.insert(*id, LiveVarSet::default()); - - let body_live_vars = collect_stmt(body, &env.jp_live_vars, LiveVarSet::default()); - - env.jp_live_vars.insert(*id, body_live_vars); - - let b = function_r(env, remainder); - - let v = function_r(env, body); - - env.jp_live_vars.remove(id); - - let join = Join { - id: *id, - parameters, - body: v, - remainder: b, - }; - - arena.alloc(join) - } - - Let(symbol, expr, layout, continuation) => { - let b = function_r(env, continuation); - - arena.alloc(Let(*symbol, expr.clone(), *layout, b)) - } - Refcounting(modify_rc, continuation) => { - let b = function_r(env, continuation); - - arena.alloc(Refcounting(*modify_rc, b)) - } - - Expect { - condition, - region, - lookups, - layouts, - remainder, - } => { - let b = function_r(env, remainder); - - let expect = Expect { - condition: *condition, - region: *region, - lookups, - layouts, - remainder: b, - }; - - arena.alloc(expect) - } - - Ret(_) | Jump(_, _) | RuntimeError(_) => { - // terminals - stmt - } - } -} - -fn has_live_var<'a>(jp_live_vars: &JPLiveVarMap, stmt: &'a Stmt<'a>, needle: Symbol) -> bool { - use Stmt::*; - - match stmt { - Let(s, e, _, c) => { - debug_assert_ne!(*s, needle); - has_live_var_expr(e, needle) || has_live_var(jp_live_vars, c, needle) - } - Switch { cond_symbol, .. } if *cond_symbol == needle => true, - Switch { - branches, - default_branch, - .. - } => { - has_live_var(jp_live_vars, default_branch.1, needle) - || branches - .iter() - .any(|(_, _, body)| has_live_var(jp_live_vars, body, needle)) - } - Ret(s) => *s == needle, - Refcounting(modify_rc, cont) => { - modify_rc.get_symbol() == needle || has_live_var(jp_live_vars, cont, needle) - } - Expect { - condition, - remainder, - .. - } => *condition == needle || has_live_var(jp_live_vars, remainder, needle), - Join { - id, - parameters, - body, - remainder, - } => { - debug_assert!(parameters.iter().all(|p| p.symbol != needle)); - - let mut jp_live_vars = jp_live_vars.clone(); - - jp_live_vars.insert(*id, LiveVarSet::default()); - - let body_live_vars = collect_stmt(body, &jp_live_vars, LiveVarSet::default()); - - if body_live_vars.contains(&needle) { - return true; - } - - jp_live_vars.insert(*id, body_live_vars); - - has_live_var(&jp_live_vars, remainder, needle) - } - Jump(id, arguments) => { - arguments.iter().any(|s| *s == needle) || jp_live_vars[id].contains(&needle) - } - RuntimeError(_) => false, - } -} - -fn has_live_var_expr<'a>(expr: &'a Expr<'a>, needle: Symbol) -> bool { - match expr { - Expr::Literal(_) => false, - Expr::Call(call) => has_live_var_call(call, needle), - Expr::Array { elems: fields, .. } => { - for element in fields.iter() { - if let ListLiteralElement::Symbol(s) = element { - if *s == needle { - return true; - } - } - } - - false - } - Expr::Tag { - arguments: fields, .. - } - | Expr::Struct(fields) => fields.iter().any(|s| *s == needle), - Expr::StructAtIndex { structure, .. } - | Expr::GetTagId { structure, .. } - | Expr::UnionAtIndex { structure, .. } => *structure == needle, - Expr::EmptyArray => false, - Expr::Reuse { - symbol, arguments, .. - } => needle == *symbol || arguments.iter().any(|s| *s == needle), - Expr::Reset { symbol, .. } => needle == *symbol, - Expr::ExprBox { symbol, .. } => needle == *symbol, - Expr::ExprUnbox { symbol, .. } => needle == *symbol, - Expr::RuntimeErrorFunction(_) => false, - } -} - -fn has_live_var_call<'a>(call: &'a Call<'a>, needle: Symbol) -> bool { - call.arguments.iter().any(|s| *s == needle) -} diff --git a/compiler/mono/src/tail_recursion.rs b/compiler/mono/src/tail_recursion.rs deleted file mode 100644 index e73735a626..0000000000 --- a/compiler/mono/src/tail_recursion.rs +++ /dev/null @@ -1,215 +0,0 @@ -#![allow(clippy::manual_map)] - -use crate::ir::{CallType, Expr, JoinPointId, Param, Stmt}; -use crate::layout::Layout; -use bumpalo::collections::Vec; -use bumpalo::Bump; -use roc_module::symbol::Symbol; - -/// Make tail calls into loops (using join points) -/// -/// e.g. -/// -/// > factorial n accum = if n == 1 then accum else factorial (n - 1) (n * accum) -/// -/// becomes -/// -/// ```elm -/// factorial n1 accum1 = -/// let joinpoint j n accum = -/// if n == 1 then -/// accum -/// else -/// jump j (n - 1) (n * accum) -/// -/// in -/// jump j n1 accum1 -/// ``` -/// -/// This will effectively compile into a loop in llvm, and -/// won't grow the call stack for each iteration -pub fn make_tail_recursive<'a>( - arena: &'a Bump, - id: JoinPointId, - needle: Symbol, - stmt: Stmt<'a>, - args: &'a [(Layout<'a>, Symbol, Symbol)], -) -> Option> { - let allocated = arena.alloc(stmt); - match insert_jumps(arena, allocated, id, needle) { - None => None, - Some(new) => { - // jumps were inserted, we must now add a join point - - let params = Vec::from_iter_in( - args.iter().map(|(layout, symbol, _)| Param { - symbol: *symbol, - layout: *layout, - borrow: true, - }), - arena, - ) - .into_bump_slice(); - - // TODO could this be &[]? - let args = Vec::from_iter_in(args.iter().map(|t| t.2), arena).into_bump_slice(); - - let jump = arena.alloc(Stmt::Jump(id, args)); - - let join = Stmt::Join { - id, - remainder: jump, - parameters: params, - body: new, - }; - - Some(join) - } - } -} - -fn insert_jumps<'a>( - arena: &'a Bump, - stmt: &'a Stmt<'a>, - goal_id: JoinPointId, - needle: Symbol, -) -> Option<&'a Stmt<'a>> { - use Stmt::*; - - match stmt { - Let( - symbol, - Expr::Call(crate::ir::Call { - call_type: CallType::ByName { name: fsym, .. }, - arguments, - .. - }), - _, - Stmt::Ret(rsym), - ) if needle == *fsym && symbol == rsym => { - // replace the call and return with a jump - - let jump = Stmt::Jump(goal_id, arguments); - - Some(arena.alloc(jump)) - } - - Let(symbol, expr, layout, cont) => { - let opt_cont = insert_jumps(arena, cont, goal_id, needle); - - if opt_cont.is_some() { - let cont = opt_cont.unwrap_or(cont); - - Some(arena.alloc(Let(*symbol, expr.clone(), *layout, cont))) - } else { - None - } - } - - Join { - id, - parameters, - remainder, - body: continuation, - } => { - let opt_remainder = insert_jumps(arena, remainder, goal_id, needle); - let opt_continuation = insert_jumps(arena, continuation, goal_id, needle); - - if opt_remainder.is_some() || opt_continuation.is_some() { - let remainder = opt_remainder.unwrap_or(remainder); - let continuation = opt_continuation.unwrap_or(*continuation); - - Some(arena.alloc(Join { - id: *id, - parameters, - remainder, - body: continuation, - })) - } else { - None - } - } - Switch { - cond_symbol, - cond_layout, - branches, - default_branch, - ret_layout, - } => { - let opt_default = insert_jumps(arena, default_branch.1, goal_id, needle); - - let mut did_change = false; - - let opt_branches = Vec::from_iter_in( - branches.iter().map(|(label, info, branch)| { - match insert_jumps(arena, branch, goal_id, needle) { - None => None, - Some(branch) => { - did_change = true; - Some((*label, info.clone(), branch.clone())) - } - } - }), - arena, - ); - - if opt_default.is_some() || did_change { - let default_branch = ( - default_branch.0.clone(), - opt_default.unwrap_or(default_branch.1), - ); - - let branches = if did_change { - let new = Vec::from_iter_in( - opt_branches.into_iter().zip(branches.iter()).map( - |(opt_branch, branch)| match opt_branch { - None => branch.clone(), - Some(new_branch) => new_branch, - }, - ), - arena, - ); - - new.into_bump_slice() - } else { - branches - }; - - Some(arena.alloc(Switch { - cond_symbol: *cond_symbol, - cond_layout: *cond_layout, - default_branch, - branches, - ret_layout: *ret_layout, - })) - } else { - None - } - } - Refcounting(modify, cont) => match insert_jumps(arena, cont, goal_id, needle) { - Some(cont) => Some(arena.alloc(Refcounting(*modify, cont))), - None => None, - }, - - Expect { - condition, - region, - lookups, - layouts, - remainder, - } => match insert_jumps(arena, remainder, goal_id, needle) { - Some(cont) => Some(arena.alloc(Expect { - condition: *condition, - region: *region, - lookups, - layouts, - remainder: cont, - })), - None => None, - }, - - Ret(_) => None, - Jump(_, _) => None, - RuntimeError(_) => None, - } -} diff --git a/compiler/parse/Cargo.toml b/compiler/parse/Cargo.toml deleted file mode 100644 index 00e9ce63c9..0000000000 --- a/compiler/parse/Cargo.toml +++ /dev/null @@ -1,28 +0,0 @@ -[package] -name = "roc_parse" -version = "0.1.0" -authors = ["The Roc Contributors"] -license = "UPL-1.0" -edition = "2021" - -[features] -"parse_debug_trace" = [] - -[dependencies] -roc_collections = { path = "../collections" } -roc_region = { path = "../region" } -roc_module = { path = "../module" } -bumpalo = { version = "3.8.0", features = ["collections"] } -encode_unicode = "0.3.6" - -[dev-dependencies] -criterion = { version = "0.3.5", features = ["html_reports"] } -pretty_assertions = "1.0.0" -indoc = "1.0.3" -quickcheck = "1.0.3" -quickcheck_macros = "1.0.0" -roc_test_utils = { path = "../../test_utils" } - -[[bench]] -name = "bench_parse" -harness = false diff --git a/compiler/parse/benches/bench_parse.rs b/compiler/parse/benches/bench_parse.rs deleted file mode 100644 index bfba0a6260..0000000000 --- a/compiler/parse/benches/bench_parse.rs +++ /dev/null @@ -1,37 +0,0 @@ -use bumpalo::Bump; -use criterion::{black_box, criterion_group, criterion_main, Criterion}; -use roc_parse::{module, module::module_defs, parser::Parser, state::State}; - -pub fn criterion_benchmark(c: &mut Criterion) { - let mut path = std::env::current_dir() - .unwrap() - .parent() - .unwrap() - .parent() - .unwrap() - .to_owned(); - path.push("examples"); - path.push("false-interpreter"); - path.push("False.roc"); - - let src = std::fs::read_to_string(&path).unwrap(); - - c.bench_function("parse false-interpreter", |b| { - b.iter(|| { - let arena = Bump::new(); - - let (_actual, state) = - module::parse_header(&arena, State::new(src.as_bytes())).unwrap(); - - let res = module_defs() - .parse(&arena, state) - .map(|tuple| tuple.1) - .unwrap(); - - black_box(res.len()); - }) - }); -} - -criterion_group!(benches, criterion_benchmark); -criterion_main!(benches); diff --git a/compiler/parse/fuzz/Cargo.lock b/compiler/parse/fuzz/Cargo.lock deleted file mode 100644 index 78597e9fca..0000000000 --- a/compiler/parse/fuzz/Cargo.lock +++ /dev/null @@ -1,182 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -[[package]] -name = "arbitrary" -version = "0.4.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db55d72333851e17d572bec876e390cd3b11eb1ef53ae821dd9f3b653d2b4569" - -[[package]] -name = "bitmaps" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "031043d04099746d8db04daf1fa424b2bc8bd69d92b25962dcde24da39ab64a2" -dependencies = [ - "typenum", -] - -[[package]] -name = "bumpalo" -version = "3.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e8c087f005730276d1096a652e92a8bacee2e2472bcc9715a74d2bec38b5820" - -[[package]] -name = "cc" -version = "1.0.61" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed67cbde08356238e75fc4656be4749481eeffb09e19f320a25237d5221c985d" - -[[package]] -name = "encode_unicode" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" - -[[package]] -name = "im" -version = "14.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "696059c87b83c5a258817ecd67c3af915e3ed141891fc35a1e79908801cf0ce7" -dependencies = [ - "bitmaps", - "rand_core 0.5.1", - "rand_xoshiro", - "sized-chunks", - "typenum", - "version_check", -] - -[[package]] -name = "im-rc" -version = "14.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "303f7e6256d546e01979071417432425f15c1891fb309a5f2d724ee908fabd6e" -dependencies = [ - "bitmaps", - "rand_core 0.5.1", - "rand_xoshiro", - "sized-chunks", - "typenum", - "version_check", -] - -[[package]] -name = "inlinable_string" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb6ee2a7da03bfc3b66ca47c92c2e392fcc053ea040a85561749b026f7aad09a" - -[[package]] -name = "lazy_static" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" - -[[package]] -name = "libfuzzer-sys" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee8c42ab62f43795ed77a965ed07994c5584cdc94fd0ebf14b22ac1524077acc" -dependencies = [ - "arbitrary", - "cc", -] - -[[package]] -name = "rand_core" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" - -[[package]] -name = "rand_core" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" - -[[package]] -name = "rand_xoshiro" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9fcdd2e881d02f1d9390ae47ad8e5696a9e4be7b547a1da2afbc61973217004" -dependencies = [ - "rand_core 0.5.1", -] - -[[package]] -name = "roc_collections" -version = "0.1.0" -dependencies = [ - "bumpalo", - "im", - "im-rc", - "wyhash", -] - -[[package]] -name = "roc_module" -version = "0.1.0" -dependencies = [ - "bumpalo", - "inlinable_string", - "lazy_static", - "roc_collections", - "roc_region", -] - -[[package]] -name = "roc_parse" -version = "0.1.0" -dependencies = [ - "bumpalo", - "encode_unicode", - "inlinable_string", - "roc_collections", - "roc_module", - "roc_region", -] - -[[package]] -name = "roc_parse-fuzz" -version = "0.0.0" -dependencies = [ - "bumpalo", - "libfuzzer-sys", - "roc_parse", -] - -[[package]] -name = "roc_region" -version = "0.1.0" - -[[package]] -name = "sized-chunks" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d59044ea371ad781ff976f7b06480b9f0180e834eda94114f2afb4afc12b7718" -dependencies = [ - "bitmaps", - "typenum", -] - -[[package]] -name = "typenum" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "373c8a200f9e67a0c95e62a4f52fbf80c23b4381c05a17845531982fa99e6b33" - -[[package]] -name = "version_check" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed" - -[[package]] -name = "wyhash" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "782a50f48ac4336916227cd199c61c7b42f38d0ad705421b49eb12c74c53ae00" -dependencies = [ - "rand_core 0.4.2", -] diff --git a/compiler/parse/fuzz/Cargo.toml b/compiler/parse/fuzz/Cargo.toml deleted file mode 100644 index 54889585a9..0000000000 --- a/compiler/parse/fuzz/Cargo.toml +++ /dev/null @@ -1,38 +0,0 @@ -[package] -name = "roc_parse-fuzz" -version = "0.0.0" -authors = ["Automatically generated"] -publish = false -edition = "2021" - -[package.metadata] -cargo-fuzz = true - -[dependencies] -libfuzzer-sys = "0.3" -bumpalo = { version = "3.6.1", features = ["collections"] } - -[dependencies.roc_parse] -path = ".." - -# Prevent this from interfering with workspaces -[workspace] -members = ["."] - -[[bin]] -name = "fuzz_expr" -path = "fuzz_targets/fuzz_expr.rs" -test = false -doc = false - -[[bin]] -name = "fuzz_defs" -path = "fuzz_targets/fuzz_defs.rs" -test = false -doc = false - -[[bin]] -name = "fuzz_header" -path = "fuzz_targets/fuzz_header.rs" -test = false -doc = false diff --git a/compiler/parse/fuzz/README.md b/compiler/parse/fuzz/README.md deleted file mode 100644 index c31c048a6d..0000000000 --- a/compiler/parse/fuzz/README.md +++ /dev/null @@ -1,11 +0,0 @@ -To setup fuzzing you will need to install cargo-fuzz and run with rust nightly: - -``` -$ cargo install cargo-fuzz -$ cargo +nightly fuzz run -j -- -dict=dict.txt -``` - -The different targets can be found by running `cargo fuzz list`. - -When a bug is found, it will be reported with commands to run it again and look for a minimized version. -If you are going to file a bug, please minimize the input before filing the bug. diff --git a/compiler/parse/fuzz/dict.txt b/compiler/parse/fuzz/dict.txt deleted file mode 100644 index c2e458bbda..0000000000 --- a/compiler/parse/fuzz/dict.txt +++ /dev/null @@ -1,35 +0,0 @@ -"if" -"then" -"else" -"when" -"as" -"is" -"expect" - -"app" -"platform" -"provides" -"requires" -"exposes" -"imports" -"effects" -"interface" - -"|>" -"==" -"!=" -"&&" -"||" -"+" -"*" -"-" -"//" -"/" -"<=" -"<" -">=" -">" -"^" -"%" - -"->" \ No newline at end of file diff --git a/compiler/parse/fuzz/fuzz_targets/fuzz_defs.rs b/compiler/parse/fuzz/fuzz_targets/fuzz_defs.rs deleted file mode 100644 index f02bfc587c..0000000000 --- a/compiler/parse/fuzz/fuzz_targets/fuzz_defs.rs +++ /dev/null @@ -1,11 +0,0 @@ -#![no_main] -use bumpalo::Bump; -use libfuzzer_sys::fuzz_target; -use roc_parse::test_helpers::parse_defs_with; - -fuzz_target!(|data: &[u8]| { - if let Ok(input) = std::str::from_utf8(data) { - let arena = Bump::new(); - let _actual = parse_defs_with(&arena, input.trim()); - } -}); diff --git a/compiler/parse/fuzz/fuzz_targets/fuzz_expr.rs b/compiler/parse/fuzz/fuzz_targets/fuzz_expr.rs deleted file mode 100644 index d130a0c621..0000000000 --- a/compiler/parse/fuzz/fuzz_targets/fuzz_expr.rs +++ /dev/null @@ -1,11 +0,0 @@ -#![no_main] -use bumpalo::Bump; -use libfuzzer_sys::fuzz_target; -use roc_parse::test_helpers::parse_expr_with; - -fuzz_target!(|data: &[u8]| { - if let Ok(input) = std::str::from_utf8(data) { - let arena = Bump::new(); - let _actual = parse_expr_with(&arena, input.trim()); - } -}); diff --git a/compiler/parse/fuzz/fuzz_targets/fuzz_header.rs b/compiler/parse/fuzz/fuzz_targets/fuzz_header.rs deleted file mode 100644 index e04f338c87..0000000000 --- a/compiler/parse/fuzz/fuzz_targets/fuzz_header.rs +++ /dev/null @@ -1,11 +0,0 @@ -#![no_main] -use bumpalo::Bump; -use libfuzzer_sys::fuzz_target; -use roc_parse::test_helpers::parse_header_with; - -fuzz_target!(|data: &[u8]| { - if let Ok(input) = std::str::from_utf8(data) { - let arena = Bump::new(); - let _actual = parse_header_with(&arena, input.trim()); - } -}); diff --git a/compiler/parse/src/ast.rs b/compiler/parse/src/ast.rs deleted file mode 100644 index 6ba4a5b509..0000000000 --- a/compiler/parse/src/ast.rs +++ /dev/null @@ -1,1162 +0,0 @@ -use std::fmt::Debug; - -use crate::header::{AppHeader, HostedHeader, InterfaceHeader, PlatformHeader}; -use crate::ident::Ident; -use bumpalo::collections::{String, Vec}; -use bumpalo::Bump; -use roc_collections::soa::{EitherIndex, Index, Slice}; -use roc_module::called_via::{BinOp, CalledVia, UnaryOp}; -use roc_region::all::{Loc, Position, Region}; - -#[derive(Debug)] -pub struct Spaces<'a, T> { - pub before: &'a [CommentOrNewline<'a>], - pub item: T, - pub after: &'a [CommentOrNewline<'a>], -} - -#[derive(Copy, Clone, PartialEq)] -pub enum Spaced<'a, T> { - Item(T), - - // Spaces - SpaceBefore(&'a Spaced<'a, T>, &'a [CommentOrNewline<'a>]), - SpaceAfter(&'a Spaced<'a, T>, &'a [CommentOrNewline<'a>]), -} - -impl<'a, T> Spaced<'a, T> { - /// A `Spaced` is multiline if it has newlines or comments before or after the item, since - /// comments induce newlines! - pub fn is_multiline(&self) -> bool { - match self { - Spaced::Item(_) => false, - Spaced::SpaceBefore(_, spaces) | Spaced::SpaceAfter(_, spaces) => { - debug_assert!(!spaces.is_empty()); - true - } - } - } -} - -impl<'a, T: Debug> Debug for Spaced<'a, T> { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::Item(item) => item.fmt(f), - Self::SpaceBefore(item, space) => f - .debug_tuple("SpaceBefore") - .field(item) - .field(space) - .finish(), - Self::SpaceAfter(item, space) => f - .debug_tuple("SpaceAfter") - .field(item) - .field(space) - .finish(), - } - } -} - -pub trait ExtractSpaces<'a>: Sized + Copy { - type Item; - fn extract_spaces(&self) -> Spaces<'a, Self::Item>; -} - -impl<'a, T: ExtractSpaces<'a>> ExtractSpaces<'a> for &'a T { - type Item = T::Item; - fn extract_spaces(&self) -> Spaces<'a, Self::Item> { - (*self).extract_spaces() - } -} - -impl<'a, T: ExtractSpaces<'a>> ExtractSpaces<'a> for Loc { - type Item = T::Item; - fn extract_spaces(&self) -> Spaces<'a, Self::Item> { - let spaces = self.value.extract_spaces(); - Spaces { - before: spaces.before, - item: spaces.item, - after: spaces.after, - } - } -} - -#[derive(Clone, Debug, PartialEq)] -pub enum Module<'a> { - Interface { header: InterfaceHeader<'a> }, - App { header: AppHeader<'a> }, - Platform { header: PlatformHeader<'a> }, - Hosted { header: HostedHeader<'a> }, -} - -#[derive(Clone, Copy, Debug, PartialEq)] -pub struct WhenBranch<'a> { - pub patterns: &'a [Loc>], - pub value: Loc>, - pub guard: Option>>, -} - -#[derive(Clone, Copy, Debug, PartialEq)] -pub struct WhenPattern<'a> { - pub pattern: Loc>, - pub guard: Option>>, -} - -#[derive(Clone, Copy, Debug, PartialEq)] -pub enum StrSegment<'a> { - Plaintext(&'a str), // e.g. "foo" - Unicode(Loc<&'a str>), // e.g. "00A0" in "\u(00A0)" - EscapedChar(EscapedChar), // e.g. '\n' in "Hello!\n" - Interpolated(Loc<&'a Expr<'a>>), // e.g. (name) in "Hi, \(name)!" -} - -#[derive(Clone, Copy, Debug, PartialEq)] -pub enum EscapedChar { - Newline, // \n - Tab, // \t - Quote, // \" - Backslash, // \\ - CarriageReturn, // \r -} - -impl EscapedChar { - /// Returns the char that would have been originally parsed to - pub fn to_parsed_char(self) -> char { - use EscapedChar::*; - - match self { - Backslash => '\\', - Quote => '"', - CarriageReturn => 'r', - Tab => 't', - Newline => 'n', - } - } -} - -#[derive(Clone, Copy, Debug, PartialEq)] -pub enum StrLiteral<'a> { - /// The most common case: a plain string with no escapes or interpolations - PlainLine(&'a str), - Line(&'a [StrSegment<'a>]), - Block(&'a [&'a [StrSegment<'a>]]), -} - -/// A parsed expression. This uses lifetimes extensively for two reasons: -/// -/// 1. It uses Bump::alloc for all allocations, which returns a reference. -/// 2. It often stores references into the input string instead of allocating. -/// -/// This dramatically reduces allocations during parsing. Once parsing is done, -/// we move on to canonicalization, which often needs to allocate more because -/// it's doing things like turning local variables into fully qualified symbols. -/// Once canonicalization is done, the arena and the input string get dropped. -#[derive(Clone, Copy, Debug, PartialEq)] -pub enum Expr<'a> { - // Number Literals - Float(&'a str), - Num(&'a str), - NonBase10Int { - string: &'a str, - base: Base, - is_negative: bool, - }, - - // String Literals - Str(StrLiteral<'a>), // string without escapes in it - /// Look up exactly one field on a record, e.g. (expr).foo. - Access(&'a Expr<'a>, &'a str), - /// e.g. `.foo` - AccessorFunction(&'a str), - /// eg 'b' - SingleQuote(&'a str), - - // Collection Literals - List(Collection<'a, &'a Loc>>), - - RecordUpdate { - update: &'a Loc>, - fields: Collection<'a, Loc>>>, - }, - - Record(Collection<'a, Loc>>>), - - // Lookups - Var { - module_name: &'a str, // module_name will only be filled if the original Roc code stated something like `5 + SomeModule.myVar`, module_name will be blank if it was `5 + myVar` - ident: &'a str, - }, - - Underscore(&'a str), - - // Tags - Tag(&'a str), - - // Reference to an opaque type, e.g. @Opaq - OpaqueRef(&'a str), - - // Pattern Matching - Closure(&'a [Loc>], &'a Loc>), - /// Multiple defs in a row - Defs(&'a [&'a Loc>], &'a Loc>), - Backpassing(&'a [Loc>], &'a Loc>, &'a Loc>), - Expect(&'a Loc>, &'a Loc>), - - // Application - /// To apply by name, do Apply(Var(...), ...) - /// To apply a tag by name, do Apply(Tag(...), ...) - Apply(&'a Loc>, &'a [&'a Loc>], CalledVia), - BinOps(&'a [(Loc>, Loc)], &'a Loc>), - UnaryOp(&'a Loc>, Loc), - - // Conditionals - If(&'a [(Loc>, Loc>)], &'a Loc>), - When( - /// The condition - &'a Loc>, - /// A | B if bool -> expression - /// | if -> - /// Vec, because there may be many patterns, and the guard - /// is Option because each branch may be preceded by - /// a guard (".. if .."). - &'a [&'a WhenBranch<'a>], - ), - - // Blank Space (e.g. comments, spaces, newlines) before or after an expression. - // We preserve this for the formatter; canonicalization ignores it. - SpaceBefore(&'a Expr<'a>, &'a [CommentOrNewline<'a>]), - SpaceAfter(&'a Expr<'a>, &'a [CommentOrNewline<'a>]), - ParensAround(&'a Expr<'a>), - - // Problems - MalformedIdent(&'a str, crate::ident::BadIdent), - MalformedClosure, - // Both operators were non-associative, e.g. (True == False == False). - // We should tell the author to disambiguate by grouping them with parens. - PrecedenceConflict(&'a PrecedenceConflict<'a>), -} - -#[derive(Clone, Copy, Debug, PartialEq)] -pub struct PrecedenceConflict<'a> { - pub whole_region: Region, - pub binop1_position: Position, - pub binop2_position: Position, - pub binop1: BinOp, - pub binop2: BinOp, - pub expr: &'a Loc>, -} - -#[derive(Debug, Clone, Copy, PartialEq)] -pub struct TypeHeader<'a> { - pub name: Loc<&'a str>, - pub vars: &'a [Loc>], -} - -impl<'a> TypeHeader<'a> { - pub fn region(&self) -> Region { - Region::across_all( - [self.name.region] - .iter() - .chain(self.vars.iter().map(|v| &v.region)), - ) - } -} - -/// The `has` keyword associated with ability definitions. -#[derive(Debug, Clone, Copy, PartialEq)] -pub enum Has<'a> { - Has, - SpaceBefore(&'a Has<'a>, &'a [CommentOrNewline<'a>]), - SpaceAfter(&'a Has<'a>, &'a [CommentOrNewline<'a>]), -} - -/// An ability demand is a value defining the ability; for example `hash : a -> U64 | a has Hash` -/// for a `Hash` ability. -#[derive(Debug, Clone, Copy, PartialEq)] -pub struct AbilityMember<'a> { - pub name: Loc>, - pub typ: Loc>, -} - -impl AbilityMember<'_> { - pub fn region(&self) -> Region { - Region::across_all([self.name.region, self.typ.region].iter()) - } -} - -#[derive(Debug, Clone, Copy, PartialEq)] -pub enum TypeDef<'a> { - /// A type alias. This is like a standalone annotation, except the pattern - /// must be a capitalized Identifier, e.g. - /// - /// Foo : Bar Baz - Alias { - header: TypeHeader<'a>, - ann: Loc>, - }, - - /// An opaque type, wrapping its inner type. E.g. Age := U64. - Opaque { - header: TypeHeader<'a>, - typ: Loc>, - derived: Option>>, - }, - - /// An ability definition. E.g. - /// Hash has - /// hash : a -> U64 | a has Hash - Ability { - header: TypeHeader<'a>, - loc_has: Loc>, - members: &'a [AbilityMember<'a>], - }, -} - -#[derive(Debug, Clone, Copy, PartialEq)] -pub enum ValueDef<'a> { - // TODO in canonicalization, validate the pattern; only certain patterns - // are allowed in annotations. - Annotation(Loc>, Loc>), - - // TODO in canonicalization, check to see if there are any newlines after the - // annotation; if not, and if it's followed by a Body, then the annotation - // applies to that expr! (TODO: verify that the pattern for both annotation and body match.) - // No need to track that relationship in any data structure. - Body(&'a Loc>, &'a Loc>), - - AnnotatedBody { - ann_pattern: &'a Loc>, - ann_type: &'a Loc>, - comment: Option<&'a str>, - body_pattern: &'a Loc>, - body_expr: &'a Loc>, - }, - - Expect(&'a Loc>), -} - -#[derive(Debug, Clone, PartialEq, Default)] -pub struct Defs<'a> { - pub tags: std::vec::Vec, ValueDef<'a>>>, - pub regions: std::vec::Vec, - pub space_before: std::vec::Vec>>, - pub space_after: std::vec::Vec>>, - pub spaces: std::vec::Vec>, - pub type_defs: std::vec::Vec>, - pub value_defs: std::vec::Vec>, -} - -impl<'a> Defs<'a> { - pub fn defs(&self) -> impl Iterator, &ValueDef<'a>>> { - self.tags.iter().map(|tag| match tag.split() { - Ok(type_index) => Ok(&self.type_defs[type_index.index()]), - Err(value_index) => Err(&self.value_defs[value_index.index()]), - }) - } - - pub fn last(&self) -> Option, &ValueDef<'a>>> { - self.tags.last().map(|tag| match tag.split() { - Ok(type_index) => Ok(&self.type_defs[type_index.index()]), - Err(value_index) => Err(&self.value_defs[value_index.index()]), - }) - } - - /// NOTE assumes the def itself is pushed already! - fn push_def_help( - &mut self, - tag: EitherIndex, ValueDef<'a>>, - region: Region, - spaces_before: &[CommentOrNewline<'a>], - spaces_after: &[CommentOrNewline<'a>], - ) { - self.tags.push(tag); - - self.regions.push(region); - - let before = Slice::extend_new(&mut self.spaces, spaces_before.iter().copied()); - self.space_before.push(before); - - let after = Slice::extend_new(&mut self.spaces, spaces_after.iter().copied()); - self.space_after.push(after); - } - - pub fn push_value_def( - &mut self, - value_def: ValueDef<'a>, - region: Region, - spaces_before: &[CommentOrNewline<'a>], - spaces_after: &[CommentOrNewline<'a>], - ) { - let value_def_index = Index::push_new(&mut self.value_defs, value_def); - let tag = EitherIndex::from_right(value_def_index); - self.push_def_help(tag, region, spaces_before, spaces_after) - } - - pub fn replace_with_value_def( - &mut self, - index: usize, - value_def: ValueDef<'a>, - region: Region, - ) { - let value_def_index = Index::push_new(&mut self.value_defs, value_def); - let tag = EitherIndex::from_right(value_def_index); - - self.tags[index] = tag; - self.regions[index] = region; - } - - pub fn push_type_def( - &mut self, - type_def: TypeDef<'a>, - region: Region, - spaces_before: &[CommentOrNewline<'a>], - spaces_after: &[CommentOrNewline<'a>], - ) { - let type_def_index = Index::push_new(&mut self.type_defs, type_def); - let tag = EitherIndex::from_left(type_def_index); - self.push_def_help(tag, region, spaces_before, spaces_after) - } -} - -#[derive(Debug, Clone, Copy, PartialEq)] -pub enum Def<'a> { - Type(TypeDef<'a>), - Value(ValueDef<'a>), - - // Blank Space (e.g. comments, spaces, newlines) before or after a def. - // We preserve this for the formatter; canonicalization ignores it. - SpaceBefore(&'a Def<'a>, &'a [CommentOrNewline<'a>]), - SpaceAfter(&'a Def<'a>, &'a [CommentOrNewline<'a>]), - - NotYetImplemented(&'static str), -} - -impl<'a> Def<'a> { - pub fn unroll_spaces_before(&self) -> (&'a [CommentOrNewline<'a>], &Def) { - let (spaces, def): (&'a [_], &Def) = match self { - Def::SpaceBefore(def, spaces) => (spaces, def), - def => (&[], def), - }; - debug_assert!(!matches!(def, Def::SpaceBefore(_, _))); - (spaces, def) - } - - pub fn unroll_def(&self) -> Result<&TypeDef<'a>, &ValueDef<'a>> { - let mut def = self; - loop { - match def { - Def::Type(type_def) => return Ok(type_def), - Def::Value(value_def) => return Err(value_def), - Def::SpaceBefore(def1, _) | Def::SpaceAfter(def1, _) => def = def1, - Def::NotYetImplemented(s) => todo!("{}", s), - } - } - } -} - -impl<'a> From> for Def<'a> { - fn from(def: TypeDef<'a>) -> Self { - Self::Type(def) - } -} - -impl<'a> From> for Def<'a> { - fn from(def: ValueDef<'a>) -> Self { - Self::Value(def) - } -} - -/// Should always be a zero-argument `Apply`; we'll check this in canonicalization -pub type AbilityName<'a> = Loc>; - -#[derive(Debug, Copy, Clone, PartialEq)] -pub struct HasClause<'a> { - pub var: Loc>, - pub ability: AbilityName<'a>, -} - -#[derive(Debug, Copy, Clone, PartialEq)] -pub enum Derived<'a> { - /// `has [Eq, Hash]` - Has(Collection<'a, AbilityName<'a>>), - - // We preserve this for the formatter; canonicalization ignores it. - SpaceBefore(&'a Derived<'a>, &'a [CommentOrNewline<'a>]), - SpaceAfter(&'a Derived<'a>, &'a [CommentOrNewline<'a>]), -} - -impl Derived<'_> { - pub fn collection(&self) -> &Collection { - let mut it = self; - loop { - match it { - Self::SpaceBefore(inner, _) | Self::SpaceAfter(inner, _) => { - it = inner; - } - Self::Has(collection) => return collection, - } - } - } - - pub fn is_empty(&self) -> bool { - self.collection().is_empty() - } -} - -#[derive(Debug, Copy, Clone, PartialEq)] -pub enum TypeAnnotation<'a> { - /// A function. The types of its arguments, then the type of its return value. - Function(&'a [Loc>], &'a Loc>), - - /// Applying a type to some arguments (e.g. Map.Map String Int) - Apply(&'a str, &'a str, &'a [Loc>]), - - /// A bound type variable, e.g. `a` in `(a -> a)` - BoundVariable(&'a str), - - /// Inline type alias, e.g. `as List a` in `[Cons a (List a), Nil] as List a` - As( - &'a Loc>, - &'a [CommentOrNewline<'a>], - TypeHeader<'a>, - ), - - Record { - fields: Collection<'a, Loc>>>, - /// The row type variable in an open record, e.g. the `r` in `{ name: Str }r`. - /// This is None if it's a closed record annotation like `{ name: Str }`. - ext: Option<&'a Loc>>, - }, - - /// A tag union, e.g. `[ - TagUnion { - /// The row type variable in an open tag union, e.g. the `a` in `[Foo, Bar]a`. - /// This is None if it's a closed tag union like `[Foo, Bar]`. - ext: Option<&'a Loc>>, - tags: Collection<'a, Loc>>, - }, - - /// '_', indicating the compiler should infer the type - Inferred, - - /// The `*` type variable, e.g. in (List *) - Wildcard, - - /// A "where" clause demanding abilities designated by a `|`, e.g. `a -> U64 | a has Hash` - Where(&'a Loc>, &'a [Loc>]), - - // We preserve this for the formatter; canonicalization ignores it. - SpaceBefore(&'a TypeAnnotation<'a>, &'a [CommentOrNewline<'a>]), - SpaceAfter(&'a TypeAnnotation<'a>, &'a [CommentOrNewline<'a>]), - - /// A malformed type annotation, which will code gen to a runtime error - Malformed(&'a str), -} - -#[derive(Debug, Clone, Copy, PartialEq)] -pub enum Tag<'a> { - Apply { - name: Loc<&'a str>, - args: &'a [Loc>], - }, - - // We preserve this for the formatter; canonicalization ignores it. - SpaceBefore(&'a Tag<'a>, &'a [CommentOrNewline<'a>]), - SpaceAfter(&'a Tag<'a>, &'a [CommentOrNewline<'a>]), - - /// A malformed tag, which will code gen to a runtime error - Malformed(&'a str), -} - -#[derive(Debug, Clone, Copy, PartialEq)] -pub enum AssignedField<'a, Val> { - // A required field with a label, e.g. `{ name: "blah" }` or `{ name : Str }` - RequiredValue(Loc<&'a str>, &'a [CommentOrNewline<'a>], &'a Loc), - - // An optional field with a label, e.g. `{ name ? "blah" }` - // - // NOTE: This only comes up in type annotations (e.g. `name ? Str`) - // and in destructuring patterns (e.g. `{ name ? "blah" }`) - OptionalValue(Loc<&'a str>, &'a [CommentOrNewline<'a>], &'a Loc), - - // A label with no value, e.g. `{ name }` (this is sugar for { name: name }) - LabelOnly(Loc<&'a str>), - - // We preserve this for the formatter; canonicalization ignores it. - SpaceBefore(&'a AssignedField<'a, Val>, &'a [CommentOrNewline<'a>]), - SpaceAfter(&'a AssignedField<'a, Val>, &'a [CommentOrNewline<'a>]), - - /// A malformed assigned field, which will code gen to a runtime error - Malformed(&'a str), -} - -#[derive(Clone, Copy, Debug, PartialEq)] -pub enum CommentOrNewline<'a> { - Newline, - LineComment(&'a str), - DocComment(&'a str), -} - -impl<'a> CommentOrNewline<'a> { - pub fn is_comment(&self) -> bool { - use CommentOrNewline::*; - match self { - Newline => false, - LineComment(_) => true, - DocComment(_) => true, - } - } - - pub fn is_newline(&self) -> bool { - use CommentOrNewline::*; - match self { - Newline => true, - LineComment(_) => false, - DocComment(_) => false, - } - } - - pub fn to_string_repr(&self) -> std::string::String { - use CommentOrNewline::*; - match self { - Newline => "\n".to_owned(), - LineComment(comment_str) => format!("#{}", comment_str), - DocComment(comment_str) => format!("##{}", comment_str), - } - } -} - -#[derive(Clone, Copy, Debug, PartialEq)] -pub enum Pattern<'a> { - // Identifier - Identifier(&'a str), - - Tag(&'a str), - - OpaqueRef(&'a str), - - Apply(&'a Loc>, &'a [Loc>]), - - /// This is Located rather than Located so we can record comments - /// around the destructured names, e.g. { x ### x does stuff ###, y } - /// In practice, these patterns will always be Identifier - RecordDestructure(Collection<'a, Loc>>), - - /// A required field pattern, e.g. { x: Just 0 } -> ... - /// Can only occur inside of a RecordDestructure - RequiredField(&'a str, &'a Loc>), - - /// An optional field pattern, e.g. { x ? Just 0 } -> ... - /// Can only occur inside of a RecordDestructure - OptionalField(&'a str, &'a Loc>), - - // Literal - NumLiteral(&'a str), - NonBase10Literal { - string: &'a str, - base: Base, - is_negative: bool, - }, - FloatLiteral(&'a str), - StrLiteral(StrLiteral<'a>), - Underscore(&'a str), - SingleQuote(&'a str), - - // Space - SpaceBefore(&'a Pattern<'a>, &'a [CommentOrNewline<'a>]), - SpaceAfter(&'a Pattern<'a>, &'a [CommentOrNewline<'a>]), - - // Malformed - Malformed(&'a str), - MalformedIdent(&'a str, crate::ident::BadIdent), - QualifiedIdentifier { - module_name: &'a str, - ident: &'a str, - }, -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] -pub enum Base { - Octal, - Binary, - Hex, - Decimal, -} - -impl<'a> Pattern<'a> { - pub fn from_ident(arena: &'a Bump, ident: Ident<'a>) -> Pattern<'a> { - match ident { - Ident::Tag(string) => Pattern::Tag(string), - Ident::OpaqueRef(string) => Pattern::OpaqueRef(string), - Ident::Access { module_name, parts } => { - if parts.len() == 1 { - // This is valid iff there is no module. - let ident = parts.iter().next().unwrap(); - - if module_name.is_empty() { - Pattern::Identifier(ident) - } else { - Pattern::QualifiedIdentifier { module_name, ident } - } - } else { - // This is definitely malformed. - let mut buf = - String::with_capacity_in(module_name.len() + (2 * parts.len()), arena); - let mut any_parts_printed = if module_name.is_empty() { - false - } else { - buf.push_str(module_name); - - true - }; - - for part in parts.iter() { - if any_parts_printed { - buf.push('.'); - } else { - any_parts_printed = true; - } - - buf.push_str(part); - } - - Pattern::Malformed(buf.into_bump_str()) - } - } - Ident::AccessorFunction(string) => Pattern::Malformed(string), - Ident::Malformed(string, _problem) => Pattern::Malformed(string), - } - } - - /// Check that patterns are equivalent, meaning they have the same shape, but may have - /// different locations/whitespace - pub fn equivalent(&self, other: &Self) -> bool { - use Pattern::*; - - match (self, other) { - (Identifier(x), Identifier(y)) => x == y, - (Tag(x), Tag(y)) => x == y, - (Apply(constructor_x, args_x), Apply(constructor_y, args_y)) => { - let equivalent_args = args_x - .iter() - .zip(args_y.iter()) - .all(|(p, q)| p.value.equivalent(&q.value)); - - constructor_x.value.equivalent(&constructor_y.value) && equivalent_args - } - (RecordDestructure(fields_x), RecordDestructure(fields_y)) => fields_x - .iter() - .zip(fields_y.iter()) - .all(|(p, q)| p.value.equivalent(&q.value)), - (RequiredField(x, inner_x), RequiredField(y, inner_y)) => { - x == y && inner_x.value.equivalent(&inner_y.value) - } - (OptionalField(x, _), OptionalField(y, _)) - | (OptionalField(x, _), Identifier(y)) - | (Identifier(x), OptionalField(y, _)) => { - // optional record fields can be annotated as: - // { x, y } : { x : Int, y ? Bool } - // { x, y ? False } = rec - x == y - } - // Literal - (NumLiteral(x), NumLiteral(y)) => x == y, - ( - NonBase10Literal { - string: string_x, - base: base_x, - is_negative: is_negative_x, - }, - NonBase10Literal { - string: string_y, - base: base_y, - is_negative: is_negative_y, - }, - ) => string_x == string_y && base_x == base_y && is_negative_x == is_negative_y, - (FloatLiteral(x), FloatLiteral(y)) => x == y, - (StrLiteral(x), StrLiteral(y)) => x == y, - (Underscore(x), Underscore(y)) => x == y, - - // Space - (SpaceBefore(x, _), SpaceBefore(y, _)) => x.equivalent(y), - (SpaceAfter(x, _), SpaceAfter(y, _)) => x.equivalent(y), - - // Malformed - (Malformed(x), Malformed(y)) => x == y, - ( - QualifiedIdentifier { - module_name: a, - ident: x, - }, - QualifiedIdentifier { - module_name: b, - ident: y, - }, - ) => (a == b) && (x == y), - - // Different constructors - _ => false, - } - } -} -#[derive(Copy, Clone)] -pub struct Collection<'a, T> { - pub items: &'a [T], - // Use a pointer to a slice (rather than just a slice), in order to avoid bloating - // Ast variants. The final_comments field is rarely accessed in the hot path, so - // this shouldn't matter much for perf. - // Use an Option, so it's possible to initialize without allocating. - final_comments: Option<&'a &'a [CommentOrNewline<'a>]>, -} - -impl<'a, T> Collection<'a, T> { - pub fn empty() -> Collection<'a, T> { - Collection { - items: &[], - final_comments: None, - } - } - - pub const fn with_items(items: &'a [T]) -> Collection<'a, T> { - Collection { - items, - final_comments: None, - } - } - - pub fn with_items_and_comments( - arena: &'a Bump, - items: &'a [T], - comments: &'a [CommentOrNewline<'a>], - ) -> Collection<'a, T> { - Collection { - items, - final_comments: if comments.is_empty() { - None - } else { - Some(arena.alloc(comments)) - }, - } - } - - pub fn replace_items(&self, new_items: &'a [V]) -> Collection<'a, V> { - Collection { - items: new_items, - final_comments: self.final_comments, - } - } - - pub fn ptrify_items(&self, arena: &'a Bump) -> Collection<'a, &'a T> { - let mut allocated = Vec::with_capacity_in(self.len(), arena); - - for parsed_elem in self.items { - allocated.push(parsed_elem); - } - - self.replace_items(allocated.into_bump_slice()) - } - - pub fn map_items(&self, arena: &'a Bump, f: impl Fn(&'a T) -> V) -> Collection<'a, V> { - let mut allocated = Vec::with_capacity_in(self.len(), arena); - - for parsed_elem in self.items { - allocated.push(f(parsed_elem)); - } - - self.replace_items(allocated.into_bump_slice()) - } - - pub fn map_items_result( - &self, - arena: &'a Bump, - f: impl Fn(&T) -> Result, - ) -> Result, E> { - let mut allocated = Vec::with_capacity_in(self.len(), arena); - - for parsed_elem in self.items { - allocated.push(f(parsed_elem)?); - } - - Ok(self.replace_items(allocated.into_bump_slice())) - } - - pub fn final_comments(&self) -> &'a [CommentOrNewline<'a>] { - if let Some(final_comments) = self.final_comments { - *final_comments - } else { - &[] - } - } - - pub fn iter(&self) -> impl Iterator { - self.items.iter() - } - - pub fn len(&self) -> usize { - self.items.len() - } - - pub fn is_empty(&self) -> bool { - self.items.is_empty() - } -} - -impl<'a, T: PartialEq> PartialEq for Collection<'a, T> { - fn eq(&self, other: &Self) -> bool { - self.items == other.items && self.final_comments() == other.final_comments() - } -} - -impl<'a, T: Debug> Debug for Collection<'a, T> { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - if self.final_comments().is_empty() { - f.debug_list().entries(self.items.iter()).finish() - } else { - f.debug_struct("Collection") - .field("items", &self.items) - .field("final_comments", &self.final_comments()) - .finish() - } - } -} - -pub trait Spaceable<'a> { - fn before(&'a self, _: &'a [CommentOrNewline<'a>]) -> Self; - fn after(&'a self, _: &'a [CommentOrNewline<'a>]) -> Self; - - fn with_spaces_before(&'a self, spaces: &'a [CommentOrNewline<'a>], region: Region) -> Loc - where - Self: Sized, - { - Loc { - region, - value: self.before(spaces), - } - } - - fn with_spaces_after(&'a self, spaces: &'a [CommentOrNewline<'a>], region: Region) -> Loc - where - Self: Sized, - { - Loc { - region, - value: self.after(spaces), - } - } -} - -impl<'a, T> Spaceable<'a> for Spaced<'a, T> { - fn before(&'a self, spaces: &'a [CommentOrNewline<'a>]) -> Self { - Spaced::SpaceBefore(self, spaces) - } - fn after(&'a self, spaces: &'a [CommentOrNewline<'a>]) -> Self { - Spaced::SpaceAfter(self, spaces) - } -} - -impl<'a> Spaceable<'a> for Expr<'a> { - fn before(&'a self, spaces: &'a [CommentOrNewline<'a>]) -> Self { - Expr::SpaceBefore(self, spaces) - } - fn after(&'a self, spaces: &'a [CommentOrNewline<'a>]) -> Self { - Expr::SpaceAfter(self, spaces) - } -} - -impl<'a> Spaceable<'a> for Pattern<'a> { - fn before(&'a self, spaces: &'a [CommentOrNewline<'a>]) -> Self { - Pattern::SpaceBefore(self, spaces) - } - fn after(&'a self, spaces: &'a [CommentOrNewline<'a>]) -> Self { - Pattern::SpaceAfter(self, spaces) - } -} - -impl<'a> Spaceable<'a> for TypeAnnotation<'a> { - fn before(&'a self, spaces: &'a [CommentOrNewline<'a>]) -> Self { - TypeAnnotation::SpaceBefore(self, spaces) - } - fn after(&'a self, spaces: &'a [CommentOrNewline<'a>]) -> Self { - TypeAnnotation::SpaceAfter(self, spaces) - } -} - -impl<'a, Val> Spaceable<'a> for AssignedField<'a, Val> { - fn before(&'a self, spaces: &'a [CommentOrNewline<'a>]) -> Self { - AssignedField::SpaceBefore(self, spaces) - } - fn after(&'a self, spaces: &'a [CommentOrNewline<'a>]) -> Self { - AssignedField::SpaceAfter(self, spaces) - } -} - -impl<'a> Spaceable<'a> for Tag<'a> { - fn before(&'a self, spaces: &'a [CommentOrNewline<'a>]) -> Self { - Tag::SpaceBefore(self, spaces) - } - fn after(&'a self, spaces: &'a [CommentOrNewline<'a>]) -> Self { - Tag::SpaceAfter(self, spaces) - } -} - -impl<'a> Spaceable<'a> for Def<'a> { - fn before(&'a self, spaces: &'a [CommentOrNewline<'a>]) -> Self { - Def::SpaceBefore(self, spaces) - } - fn after(&'a self, spaces: &'a [CommentOrNewline<'a>]) -> Self { - Def::SpaceAfter(self, spaces) - } -} - -impl<'a> Spaceable<'a> for Has<'a> { - fn before(&'a self, spaces: &'a [CommentOrNewline<'a>]) -> Self { - Has::SpaceBefore(self, spaces) - } - fn after(&'a self, spaces: &'a [CommentOrNewline<'a>]) -> Self { - Has::SpaceAfter(self, spaces) - } -} - -impl<'a> Spaceable<'a> for Derived<'a> { - fn before(&'a self, spaces: &'a [CommentOrNewline<'a>]) -> Self { - Derived::SpaceBefore(self, spaces) - } - fn after(&'a self, spaces: &'a [CommentOrNewline<'a>]) -> Self { - Derived::SpaceAfter(self, spaces) - } -} - -impl<'a> Expr<'a> { - pub fn loc_ref(&'a self, region: Region) -> Loc<&'a Self> { - Loc { - region, - value: self, - } - } - - pub fn loc(self, region: Region) -> Loc { - Loc { - region, - value: self, - } - } - - pub fn is_tag(&self) -> bool { - matches!(self, Expr::Tag(_)) - } -} - -macro_rules! impl_extract_spaces { - ($t:ident $(< $($generic_args:ident),* >)?) => { - - impl<'a, $($($generic_args: Copy),*)?> ExtractSpaces<'a> for $t<'a, $($($generic_args),*)?> { - type Item = Self; - fn extract_spaces(&self) -> Spaces<'a, Self::Item> { - match self { - $t::SpaceBefore(item, before) => { - match item { - $t::SpaceBefore(_, _) => todo!(), - $t::SpaceAfter(item, after) => { - Spaces { - before, - item: **item, - after, - } - } - _ => { - Spaces { - before, - item: **item, - after: &[], - } - } - } - }, - $t::SpaceAfter(item, after) => { - match item { - $t::SpaceBefore(item, before) => { - Spaces { - before, - item: **item, - after, - } - } - $t::SpaceAfter(_, _) => todo!(), - _ => { - Spaces { - before: &[], - item: **item, - after, - } - } - } - }, - _ => { - Spaces { - before: &[], - item: *self, - after: &[], - } - } - } - } - } - }; -} - -impl_extract_spaces!(Expr); -impl_extract_spaces!(Pattern); -impl_extract_spaces!(Tag); -impl_extract_spaces!(AssignedField); -impl_extract_spaces!(TypeAnnotation); - -impl<'a, T: Copy> ExtractSpaces<'a> for Spaced<'a, T> { - type Item = T; - - fn extract_spaces(&self) -> Spaces<'a, T> { - match self { - Spaced::SpaceBefore(item, before) => match item { - Spaced::SpaceBefore(_, _) => todo!(), - Spaced::SpaceAfter(item, after) => { - if let Spaced::Item(item) = item { - Spaces { - before, - item: *item, - after, - } - } else { - todo!(); - } - } - Spaced::Item(item) => Spaces { - before, - item: *item, - after: &[], - }, - }, - Spaced::SpaceAfter(item, after) => match item { - Spaced::SpaceBefore(item, before) => { - if let Spaced::Item(item) = item { - Spaces { - before, - item: *item, - after, - } - } else { - todo!(); - } - } - Spaced::SpaceAfter(_, _) => todo!(), - Spaced::Item(item) => Spaces { - before: &[], - item: *item, - after, - }, - }, - Spaced::Item(item) => Spaces { - before: &[], - item: *item, - after: &[], - }, - } - } -} diff --git a/compiler/parse/src/blankspace.rs b/compiler/parse/src/blankspace.rs deleted file mode 100644 index 78f4f41fab..0000000000 --- a/compiler/parse/src/blankspace.rs +++ /dev/null @@ -1,596 +0,0 @@ -use crate::ast::CommentOrNewline; -use crate::ast::Spaceable; -use crate::parser::SpaceProblem; -use crate::parser::{self, and, backtrackable, BadInputError, Parser, Progress::*}; -use crate::state::State; -use bumpalo::collections::vec::Vec; -use bumpalo::Bump; -use roc_region::all::Loc; -use roc_region::all::Position; - -pub fn space0_around_ee<'a, P, S, E>( - parser: P, - min_indent: u32, - indent_before_problem: fn(Position) -> E, - indent_after_problem: fn(Position) -> E, -) -> impl Parser<'a, Loc, E> -where - S: Spaceable<'a>, - S: 'a, - P: Parser<'a, Loc, E>, - P: 'a, - E: 'a + SpaceProblem, -{ - parser::map_with_arena( - and( - space0_e(min_indent, indent_before_problem), - and(parser, space0_e(min_indent, indent_after_problem)), - ), - spaces_around_help, - ) -} - -pub fn space0_before_optional_after<'a, P, S, E>( - parser: P, - min_indent: u32, - indent_before_problem: fn(Position) -> E, - indent_after_problem: fn(Position) -> E, -) -> impl Parser<'a, Loc, E> -where - S: Spaceable<'a>, - S: 'a, - P: Parser<'a, Loc, E>, - P: 'a, - E: 'a + SpaceProblem, -{ - parser::map_with_arena( - and( - space0_e(min_indent, indent_before_problem), - and( - parser, - one_of![ - backtrackable(space0_e(min_indent, indent_after_problem)), - succeed!(&[] as &[_]), - ], - ), - ), - spaces_around_help, - ) -} - -fn spaces_around_help<'a, S>( - arena: &'a Bump, - tuples: ( - &'a [CommentOrNewline<'a>], - (Loc, &'a [CommentOrNewline<'a>]), - ), -) -> Loc -where - S: Spaceable<'a>, - S: 'a, -{ - let (spaces_before, (loc_val, spaces_after)) = tuples; - - if spaces_before.is_empty() { - if spaces_after.is_empty() { - loc_val - } else { - arena - .alloc(loc_val.value) - .with_spaces_after(spaces_after, loc_val.region) - } - } else if spaces_after.is_empty() { - arena - .alloc(loc_val.value) - .with_spaces_before(spaces_before, loc_val.region) - } else { - let wrapped_expr = arena - .alloc(loc_val.value) - .with_spaces_after(spaces_after, loc_val.region); - - arena - .alloc(wrapped_expr.value) - .with_spaces_before(spaces_before, wrapped_expr.region) - } -} - -pub fn space0_before_e<'a, P, S, E>( - parser: P, - min_indent: u32, - indent_problem: fn(Position) -> E, -) -> impl Parser<'a, Loc, E> -where - S: Spaceable<'a>, - S: 'a, - P: Parser<'a, Loc, E>, - P: 'a, - E: 'a + SpaceProblem, -{ - parser::map_with_arena( - and!(space0_e(min_indent, indent_problem), parser), - |arena: &'a Bump, (space_list, loc_expr): (&'a [CommentOrNewline<'a>], Loc)| { - if space_list.is_empty() { - loc_expr - } else { - arena - .alloc(loc_expr.value) - .with_spaces_before(space_list, loc_expr.region) - } - }, - ) -} - -pub fn space0_after_e<'a, P, S, E>( - parser: P, - min_indent: u32, - indent_problem: fn(Position) -> E, -) -> impl Parser<'a, Loc, E> -where - S: Spaceable<'a>, - S: 'a, - P: Parser<'a, Loc, E>, - P: 'a, - E: 'a + SpaceProblem, -{ - parser::map_with_arena( - and!(parser, space0_e(min_indent, indent_problem)), - |arena: &'a Bump, (loc_expr, space_list): (Loc, &'a [CommentOrNewline<'a>])| { - if space_list.is_empty() { - loc_expr - } else { - arena - .alloc(loc_expr.value) - .with_spaces_after(space_list, loc_expr.region) - } - }, - ) -} - -pub fn check_indent<'a, E>( - min_indent: u32, - indent_problem: fn(Position) -> E, -) -> impl Parser<'a, (), E> -where - E: 'a, -{ - move |_, state: State<'a>| { - if state.column() >= min_indent { - Ok((NoProgress, (), state)) - } else { - Err((NoProgress, indent_problem(state.pos()), state)) - } - } -} - -pub fn space0_e<'a, E>( - min_indent: u32, - indent_problem: fn(Position) -> E, -) -> impl Parser<'a, &'a [CommentOrNewline<'a>], E> -where - E: 'a + SpaceProblem, -{ - spaces_help_help(min_indent, indent_problem) -} - -#[inline(always)] -fn spaces_help_help<'a, E>( - min_indent: u32, - indent_problem: fn(Position) -> E, -) -> impl Parser<'a, &'a [CommentOrNewline<'a>], E> -where - E: 'a + SpaceProblem, -{ - move |arena, state: State<'a>| match fast_eat_spaces(&state) { - FastSpaceState::HasTab(position) => Err(( - MadeProgress, - E::space_problem(BadInputError::HasTab, position), - state, - )), - FastSpaceState::Good { - newlines, - consumed, - column, - } => { - if consumed == 0 { - Ok((NoProgress, &[] as &[_], state)) - } else if column < min_indent { - Err((MadeProgress, indent_problem(state.pos()), state)) - } else { - let comments_and_newlines = Vec::with_capacity_in(newlines, arena); - let mut spaces = eat_spaces(state, false, comments_and_newlines); - - if spaces.multiline { - spaces.state.indent_column = spaces.state.column(); - } - - Ok(( - MadeProgress, - spaces.comments_and_newlines.into_bump_slice(), - spaces.state, - )) - } - } - } -} - -enum FastSpaceState { - Good { - newlines: usize, - consumed: usize, - column: u32, - }, - HasTab(Position), -} - -fn fast_eat_spaces(state: &State) -> FastSpaceState { - use FastSpaceState::*; - - let mut newlines = 0; - let mut line_start = state.line_start.offset as usize; - let base_offset = state.pos().offset as usize; - - let mut index = base_offset; - let bytes = state.original_bytes(); - let length = bytes.len(); - - 'outer: while index < length { - match bytes[index] { - b' ' => { - index += 1; - } - b'\n' => { - newlines += 1; - index += 1; - line_start = index; - } - b'\r' => { - index += 1; - line_start = index; - } - b'\t' => { - return HasTab(Position::new(index as u32)); - } - b'#' => { - index += 1; - - // try to use SIMD instructions explicitly - #[cfg(target_arch = "x86_64")] - { - use std::arch::x86_64::*; - - // a bytestring with the three characters we're looking for (the rest is ignored) - let needle = b"\r\n\t============="; - let needle = unsafe { _mm_loadu_si128(needle.as_ptr() as *const _) }; - - while index < length { - let remaining = length - index; - let length = if remaining < 16 { remaining as i32 } else { 16 }; - - // the source bytes we'll be looking at - let haystack = - unsafe { _mm_loadu_si128(bytes.as_ptr().add(index) as *const _) }; - - // use first 3 characters of needle, first `length` characters of haystack - // finds the first index where one of the `needle` characters occurs - // or 16 when none of the needle characters occur - let first_special_char = unsafe { - _mm_cmpestri(needle, 3, haystack, length, _SIDD_CMP_EQUAL_ANY) - }; - - // we've made `first_special_char` characters of progress - index += usize::min(first_special_char as usize, remaining); - - // if we found a special char, let the outer loop handle it - if first_special_char != 16 { - continue 'outer; - } - } - } - - #[cfg(not(target_arch = "x86_64"))] - { - while index < length { - match bytes[index] { - b'\n' | b'\t' | b'\r' => { - continue 'outer; - } - - _ => { - index += 1; - } - } - } - } - } - _ => break, - } - } - - Good { - newlines, - consumed: index - base_offset, - column: (index - line_start) as u32, - } -} - -struct SpaceState<'a> { - state: State<'a>, - multiline: bool, - comments_and_newlines: Vec<'a, CommentOrNewline<'a>>, -} - -fn eat_spaces<'a>( - mut state: State<'a>, - mut multiline: bool, - mut comments_and_newlines: Vec<'a, CommentOrNewline<'a>>, -) -> SpaceState<'a> { - for c in state.bytes() { - match c { - b' ' => { - state = state.advance(1); - } - b'\n' => { - state = state.advance_newline(); - multiline = true; - comments_and_newlines.push(CommentOrNewline::Newline); - } - b'\r' => { - state = state.advance_newline(); - } - b'\t' => unreachable!(), - - b'#' => { - state = state.advance(1); - return eat_line_comment(state, multiline, comments_and_newlines); - } - _ => break, - } - } - - SpaceState { - state, - multiline, - comments_and_newlines, - } -} - -fn eat_line_comment<'a>( - mut state: State<'a>, - mut multiline: bool, - mut comments_and_newlines: Vec<'a, CommentOrNewline<'a>>, -) -> SpaceState<'a> { - let mut index = state.pos().offset as usize; - let bytes = state.original_bytes(); - let length = bytes.len(); - - 'outer: loop { - let is_doc_comment = if let Some(b'#') = bytes.get(index) { - match bytes.get(index + 1) { - Some(b' ') => { - state = state.advance(2); - index += 2; - - true - } - Some(b'\n') => { - // consume the second # and the \n - state = state.advance(1); - state = state.advance_newline(); - index += 2; - - comments_and_newlines.push(CommentOrNewline::DocComment("")); - multiline = true; - - for c in state.bytes() { - match c { - b' ' => { - state = state.advance(1); - } - b'\n' => { - state = state.advance_newline(); - multiline = true; - comments_and_newlines.push(CommentOrNewline::Newline); - } - b'\r' => { - state = state.advance_newline(); - } - b'\t' => unreachable!(), - b'#' => { - state = state.advance(1); - index += 1; - continue 'outer; - } - _ => break, - } - - index += 1; - } - - return SpaceState { - state, - multiline, - comments_and_newlines, - }; - } - None => { - // consume the second # - state = state.advance(1); - - return SpaceState { - state, - multiline, - comments_and_newlines, - }; - } - - Some(_) => false, - } - } else { - false - }; - - let loop_start = index; - - #[cfg(target_arch = "x86_64")] - { - use std::arch::x86_64::*; - - // a bytestring with the three characters we're looking for (the rest is ignored) - let needle = b"\r\n\t============="; - let needle = unsafe { _mm_loadu_si128(needle.as_ptr() as *const _) }; - - while index < length { - let remaining = length - index; - let chunk = if remaining < 16 { remaining as i32 } else { 16 }; - - // the source bytes we'll be looking at - let haystack = unsafe { _mm_loadu_si128(bytes.as_ptr().add(index) as *const _) }; - - // use first 3 characters of needle, first chunk` characters of haystack - // finds the first index where one of the `needle` characters occurs - // or 16 when none of the needle characters occur - let first_special_char = - unsafe { _mm_cmpestri(needle, 3, haystack, chunk, _SIDD_CMP_EQUAL_ANY) }; - - // we've made `first_special_char` characters of progress - let progress = usize::min(first_special_char as usize, remaining); - index += progress; - state = state.advance(progress); - - if first_special_char != 16 { - match bytes[index] { - b'\t' => unreachable!(), - b'\n' => { - let comment = - unsafe { std::str::from_utf8_unchecked(&bytes[loop_start..index]) }; - - if is_doc_comment { - comments_and_newlines.push(CommentOrNewline::DocComment(comment)); - } else { - comments_and_newlines.push(CommentOrNewline::LineComment(comment)); - } - state = state.advance_newline(); - multiline = true; - - index += 1; - while index < length { - match bytes[index] { - b' ' => { - state = state.advance(1); - } - b'\n' => { - state = state.advance_newline(); - multiline = true; - comments_and_newlines.push(CommentOrNewline::Newline); - } - b'\r' => { - state = state.advance_newline(); - } - b'\t' => unreachable!(), - b'#' => { - state = state.advance(1); - index += 1; - continue 'outer; - } - _ => break, - } - - index += 1; - } - - return SpaceState { - state, - multiline, - comments_and_newlines, - }; - } - b'\r' => { - state = state.advance_newline(); - index += 1; - } - odd_character => { - unreachable!( - "unexpected_character {} {}", - odd_character, odd_character as char - ) - } - } - } - } - } - - #[cfg(not(target_arch = "x86_64"))] - while index < length { - match bytes[index] { - b'\t' => unreachable!(), - b'\n' => { - let comment = - unsafe { std::str::from_utf8_unchecked(&bytes[loop_start..index]) }; - - if is_doc_comment { - comments_and_newlines.push(CommentOrNewline::DocComment(comment)); - } else { - comments_and_newlines.push(CommentOrNewline::LineComment(comment)); - } - state = state.advance_newline(); - multiline = true; - - index += 1; - while index < length { - match bytes[index] { - b' ' => { - state = state.advance(1); - } - b'\n' => { - state = state.advance_newline(); - multiline = true; - comments_and_newlines.push(CommentOrNewline::Newline); - } - b'\r' => { - state = state.advance_newline(); - } - b'\t' => unreachable!(), - b'#' => { - state = state.advance(1); - index += 1; - continue 'outer; - } - _ => break, - } - - index += 1; - } - - return SpaceState { - state, - multiline, - comments_and_newlines, - }; - } - b'\r' => { - state = state.advance_newline(); - } - _ => { - state = state.advance(1); - } - } - - index += 1; - } - - // We made it to the end of the bytes. This means there's a comment without a trailing newline. - let comment = unsafe { std::str::from_utf8_unchecked(&bytes[loop_start..index]) }; - - if is_doc_comment { - comments_and_newlines.push(CommentOrNewline::DocComment(comment)); - } else { - comments_and_newlines.push(CommentOrNewline::LineComment(comment)); - } - - return SpaceState { - state, - multiline, - comments_and_newlines, - }; - } -} diff --git a/compiler/parse/src/expr.rs b/compiler/parse/src/expr.rs deleted file mode 100644 index 20e2ce0ef4..0000000000 --- a/compiler/parse/src/expr.rs +++ /dev/null @@ -1,3262 +0,0 @@ -use crate::ast::{ - AssignedField, Collection, CommentOrNewline, Def, Defs, Derived, Expr, ExtractSpaces, Has, - Pattern, Spaceable, TypeAnnotation, TypeDef, TypeHeader, ValueDef, -}; -use crate::blankspace::{ - space0_after_e, space0_around_ee, space0_before_e, space0_before_optional_after, space0_e, -}; -use crate::ident::{lowercase_ident, parse_ident, Ident}; -use crate::keyword; -use crate::parser::{ - self, backtrackable, optional, sep_by1, sep_by1_e, specialize, specialize_ref, then, - trailing_sep_by0, word1, word2, EExpect, EExpr, EIf, EInParens, ELambda, EList, ENumber, - EPattern, ERecord, EString, EType, EWhen, Either, ParseResult, Parser, -}; -use crate::pattern::{loc_closure_param, loc_has_parser}; -use crate::state::State; -use crate::type_annotation; -use bumpalo::collections::Vec; -use bumpalo::Bump; -use roc_collections::soa::Slice; -use roc_module::called_via::{BinOp, CalledVia, UnaryOp}; -use roc_region::all::{Loc, Position, Region}; - -use crate::parser::Progress::{self, *}; - -fn expr_end<'a>() -> impl Parser<'a, (), EExpr<'a>> { - |_arena, state: State<'a>| { - if state.has_reached_end() { - Ok((NoProgress, (), state)) - } else { - Err((NoProgress, EExpr::BadExprEnd(state.pos()), state)) - } - } -} - -pub fn test_parse_expr<'a>( - min_indent: u32, - arena: &'a bumpalo::Bump, - state: State<'a>, -) -> Result>, EExpr<'a>> { - let parser = skip_second!( - space0_before_e( - move |a, s| parse_loc_expr(min_indent, a, s), - min_indent, - EExpr::IndentStart, - ), - expr_end() - ); - - match parser.parse(arena, state) { - Ok((_, expression, _)) => Ok(expression), - Err((_, fail, _)) => Err(fail), - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct ExprParseOptions { - /// Check for and accept multi-backpassing syntax - /// This is usually true, but false within list/record literals - /// because the comma separating backpassing arguments conflicts - /// with the comma separating literal elements - accept_multi_backpassing: bool, - - /// Check for the `->` token, and raise an error if found - /// This is usually true, but false in if-guards - /// - /// > Just foo if foo == 2 -> ... - check_for_arrow: bool, -} - -impl Default for ExprParseOptions { - fn default() -> Self { - ExprParseOptions { - accept_multi_backpassing: true, - check_for_arrow: true, - } - } -} - -pub fn expr_help<'a>(min_indent: u32) -> impl Parser<'a, Expr<'a>, EExpr<'a>> { - move |arena, state: State<'a>| { - parse_loc_expr(min_indent, arena, state).map(|(a, b, c)| (a, b.value, c)) - } -} - -fn loc_expr_in_parens_help<'a>(min_indent: u32) -> impl Parser<'a, Loc>, EInParens<'a>> { - move |arena, state| { - let (_, loc_expr, state) = loc_expr_in_parens_help_help(min_indent).parse(arena, state)?; - - Ok(( - MadeProgress, - Loc { - region: loc_expr.region, - value: Expr::ParensAround(arena.alloc(loc_expr.value)), - }, - state, - )) - } -} - -fn loc_expr_in_parens_help_help<'a>( - min_indent: u32, -) -> impl Parser<'a, Loc>, EInParens<'a>> { - between!( - word1(b'(', EInParens::Open), - space0_around_ee( - specialize_ref(EInParens::Expr, move |arena, state| parse_loc_expr( - min_indent, arena, state - )), - min_indent, - EInParens::IndentOpen, - EInParens::IndentEnd, - ), - word1(b')', EInParens::End) - ) -} - -fn loc_expr_in_parens_etc_help<'a>(min_indent: u32) -> impl Parser<'a, Loc>, EExpr<'a>> { - move |arena, state: State<'a>| { - let parser = loc!(and!( - specialize(EExpr::InParens, loc_expr_in_parens_help(min_indent)), - one_of![record_field_access_chain(), |a, s| Ok(( - NoProgress, - Vec::new_in(a), - s - ))] - )); - - let ( - _, - Loc { - mut region, - value: (loc_expr, field_accesses), - }, - state, - ) = parser.parse(arena, state)?; - - let mut value = loc_expr.value; - - // if there are field accesses, include the parentheses in the region - // otherwise, don't include the parentheses - if field_accesses.is_empty() { - region = loc_expr.region; - } else { - for field in field_accesses { - // Wrap the previous answer in the new one, so we end up - // with a nested Expr. That way, `foo.bar.baz` gets represented - // in the AST as if it had been written (foo.bar).baz all along. - value = Expr::Access(arena.alloc(value), field); - } - } - - let loc_expr = Loc::at(region, value); - - Ok((MadeProgress, loc_expr, state)) - } -} - -fn record_field_access_chain<'a>() -> impl Parser<'a, Vec<'a, &'a str>, EExpr<'a>> { - |arena, state| match record_field_access().parse(arena, state) { - Ok((_, initial, state)) => { - let mut accesses = Vec::with_capacity_in(1, arena); - - accesses.push(initial); - - let mut loop_state = state; - loop { - match record_field_access().parse(arena, loop_state) { - Ok((_, next, state)) => { - accesses.push(next); - loop_state = state; - } - Err((MadeProgress, fail, state)) => return Err((MadeProgress, fail, state)), - Err((NoProgress, _, state)) => return Ok((MadeProgress, accesses, state)), - } - } - } - Err((MadeProgress, fail, state)) => Err((MadeProgress, fail, state)), - Err((NoProgress, _, state)) => Err((NoProgress, EExpr::Access(state.pos()), state)), - } -} - -fn record_field_access<'a>() -> impl Parser<'a, &'a str, EExpr<'a>> { - skip_first!( - word1(b'.', EExpr::Access), - specialize(|_, pos| EExpr::Access(pos), lowercase_ident()) - ) -} - -/// In some contexts we want to parse the `_` as an expression, so it can then be turned into a -/// pattern later -fn parse_loc_term_or_underscore<'a>( - min_indent: u32, - options: ExprParseOptions, - arena: &'a Bump, - state: State<'a>, -) -> ParseResult<'a, Loc>, EExpr<'a>> { - one_of!( - loc_expr_in_parens_etc_help(min_indent), - loc!(specialize(EExpr::If, if_expr_help(min_indent, options))), - loc!(specialize(EExpr::Str, string_literal_help())), - loc!(specialize(EExpr::SingleQuote, single_quote_literal_help())), - loc!(specialize(EExpr::Number, positive_number_literal_help())), - loc!(specialize(EExpr::Lambda, closure_help(min_indent, options))), - loc!(underscore_expression()), - loc!(record_literal_help(min_indent)), - loc!(specialize(EExpr::List, list_literal_help(min_indent))), - loc!(map_with_arena!( - assign_or_destructure_identifier(), - ident_to_expr - )), - ) - .parse(arena, state) -} - -fn parse_loc_term<'a>( - min_indent: u32, - options: ExprParseOptions, - arena: &'a Bump, - state: State<'a>, -) -> ParseResult<'a, Loc>, EExpr<'a>> { - one_of!( - loc_expr_in_parens_etc_help(min_indent), - loc!(specialize(EExpr::Str, string_literal_help())), - loc!(specialize(EExpr::SingleQuote, single_quote_literal_help())), - loc!(specialize(EExpr::Number, positive_number_literal_help())), - loc!(specialize(EExpr::Lambda, closure_help(min_indent, options))), - loc!(record_literal_help(min_indent)), - loc!(specialize(EExpr::List, list_literal_help(min_indent))), - loc!(map_with_arena!( - assign_or_destructure_identifier(), - ident_to_expr - )), - ) - .parse(arena, state) -} - -fn underscore_expression<'a>() -> impl Parser<'a, Expr<'a>, EExpr<'a>> { - move |arena: &'a Bump, state: State<'a>| { - let start = state.pos(); - - let (_, _, next_state) = word1(b'_', EExpr::Underscore).parse(arena, state)?; - - let lowercase_ident_expr = { specialize(move |_, _| EExpr::End(start), lowercase_ident()) }; - - let (_, output, final_state) = optional(lowercase_ident_expr).parse(arena, next_state)?; - - match output { - Some(name) => Ok((MadeProgress, Expr::Underscore(name), final_state)), - None => Ok((MadeProgress, Expr::Underscore(""), final_state)), - } - } -} - -fn loc_possibly_negative_or_negated_term<'a>( - min_indent: u32, - options: ExprParseOptions, -) -> impl Parser<'a, Loc>, EExpr<'a>> { - one_of![ - |arena, state: State<'a>| { - let initial = state.clone(); - - let (_, (loc_op, loc_expr), state) = and!(loc!(unary_negate()), |a, s| parse_loc_term( - min_indent, options, a, s - )) - .parse(arena, state)?; - - let loc_expr = numeric_negate_expression(arena, initial, loc_op, loc_expr, &[]); - - Ok((MadeProgress, loc_expr, state)) - }, - // this will parse negative numbers, which the unary negate thing up top doesn't (for now) - loc!(specialize(EExpr::Number, number_literal_help())), - loc!(map_with_arena!( - and!(loc!(word1(b'!', EExpr::Start)), |a, s| { - parse_loc_term(min_indent, options, a, s) - }), - |arena: &'a Bump, (loc_op, loc_expr): (Loc<_>, _)| { - Expr::UnaryOp(arena.alloc(loc_expr), Loc::at(loc_op.region, UnaryOp::Not)) - } - )), - |arena, state| { parse_loc_term_or_underscore(min_indent, options, arena, state) } - ] -} - -fn fail_expr_start_e<'a, T: 'a>() -> impl Parser<'a, T, EExpr<'a>> { - |_arena, state: State<'a>| Err((NoProgress, EExpr::Start(state.pos()), state)) -} - -fn unary_negate<'a>() -> impl Parser<'a, (), EExpr<'a>> { - move |_arena: &'a Bump, state: State<'a>| { - // a minus is unary iff - // - // - it is preceded by whitespace (spaces, newlines, comments) - // - it is not followed by whitespace - let followed_by_whitespace = state - .bytes() - .get(1) - .map(|c| c.is_ascii_whitespace() || *c == b'#') - .unwrap_or(false); - - if state.bytes().starts_with(b"-") && !followed_by_whitespace { - // the negate is only unary if it is not followed by whitespace - let state = state.advance(1); - Ok((MadeProgress, (), state)) - } else { - // this is not a negated expression - Err((NoProgress, EExpr::UnaryNot(state.pos()), state)) - } - } -} - -fn parse_expr_start<'a>( - min_indent: u32, - options: ExprParseOptions, - start_column: u32, - arena: &'a Bump, - state: State<'a>, -) -> ParseResult<'a, Loc>, EExpr<'a>> { - one_of![ - loc!(specialize(EExpr::If, if_expr_help(min_indent, options))), - loc!(specialize( - EExpr::When, - when::expr_help(min_indent, options) - )), - loc!(specialize(EExpr::Expect, expect_help(min_indent, options))), - loc!(specialize(EExpr::Lambda, closure_help(min_indent, options))), - loc!(move |a, s| parse_expr_operator_chain(min_indent, options, start_column, a, s)), - fail_expr_start_e() - ] - .parse(arena, state) -} - -fn parse_expr_operator_chain<'a>( - min_indent: u32, - options: ExprParseOptions, - start_column: u32, - arena: &'a Bump, - state: State<'a>, -) -> ParseResult<'a, Expr<'a>, EExpr<'a>> { - let (_, expr, state) = - loc_possibly_negative_or_negated_term(min_indent, options).parse(arena, state)?; - - let initial = state.clone(); - let end = state.pos(); - - match space0_e(min_indent, EExpr::IndentEnd).parse(arena, state) { - Err((_, _, state)) => Ok((MadeProgress, expr.value, state)), - Ok((_, spaces_before_op, state)) => { - let expr_state = ExprState { - operators: Vec::new_in(arena), - arguments: Vec::new_in(arena), - expr, - spaces_after: spaces_before_op, - initial, - end, - }; - - parse_expr_end(min_indent, options, start_column, expr_state, arena, state) - } - } -} - -#[derive(Debug)] -struct ExprState<'a> { - operators: Vec<'a, (Loc>, Loc)>, - arguments: Vec<'a, &'a Loc>>, - expr: Loc>, - spaces_after: &'a [CommentOrNewline<'a>], - initial: State<'a>, - end: Position, -} - -impl<'a> ExprState<'a> { - fn consume_spaces(&mut self, arena: &'a Bump) { - if !self.spaces_after.is_empty() { - if let Some(last) = self.arguments.pop() { - let new = last.value.with_spaces_after(self.spaces_after, last.region); - - self.arguments.push(arena.alloc(new)); - } else { - let region = self.expr.region; - - let mut value = Expr::Num(""); - std::mem::swap(&mut self.expr.value, &mut value); - - self.expr = arena - .alloc(value) - .with_spaces_after(self.spaces_after, region); - }; - - self.spaces_after = &[]; - } - } - - fn validate_assignment_or_backpassing( - mut self, - arena: &'a Bump, - loc_op: Loc, - argument_error: F, - ) -> Result>, EExpr<'a>> - where - F: Fn(Region, Position) -> EExpr<'a>, - { - if !self.operators.is_empty() { - // this `=` or `<-` likely occurred inline; treat it as an invalid operator - let opchar = match loc_op.value { - BinOp::Assignment => "=", - BinOp::Backpassing => "<-", - _ => unreachable!(), - }; - - let fail = EExpr::BadOperator(opchar, loc_op.region.start()); - - Err(fail) - } else if !self.expr.value.is_tag() && !self.arguments.is_empty() { - let region = Region::across_all(self.arguments.iter().map(|v| &v.region)); - - Err(argument_error(region, loc_op.region.start())) - } else { - self.consume_spaces(arena); - Ok(to_call(arena, self.arguments, self.expr)) - } - } - - fn validate_is_type_def( - mut self, - arena: &'a Bump, - loc_op: Loc, - kind: AliasOrOpaque, - ) -> Result<(Loc>, Vec<'a, &'a Loc>>), EExpr<'a>> { - debug_assert_eq!( - loc_op.value, - match kind { - AliasOrOpaque::Alias => BinOp::IsAliasType, - AliasOrOpaque::Opaque => BinOp::IsOpaqueType, - } - ); - - if !self.operators.is_empty() { - // this `:`/`:=` likely occurred inline; treat it as an invalid operator - let op = match kind { - AliasOrOpaque::Alias => ":", - AliasOrOpaque::Opaque => ":=", - }; - let fail = EExpr::BadOperator(op, loc_op.region.start()); - - Err(fail) - } else { - self.consume_spaces(arena); - Ok((self.expr, self.arguments)) - } - } -} - -#[allow(clippy::unnecessary_wraps)] -fn parse_expr_final<'a>( - expr_state: ExprState<'a>, - arena: &'a Bump, - state: State<'a>, -) -> ParseResult<'a, Expr<'a>, EExpr<'a>> { - let right_arg = to_call(arena, expr_state.arguments, expr_state.expr); - - let expr = if expr_state.operators.is_empty() { - right_arg.value - } else { - Expr::BinOps( - expr_state.operators.into_bump_slice(), - arena.alloc(right_arg), - ) - }; - - Ok((MadeProgress, expr, state)) -} - -fn to_call<'a>( - arena: &'a Bump, - mut arguments: Vec<'a, &'a Loc>>, - loc_expr1: Loc>, -) -> Loc> { - if arguments.is_empty() { - loc_expr1 - } else { - let last = arguments.last().map(|x| x.region).unwrap_or_default(); - let region = Region::span_across(&loc_expr1.region, &last); - - let spaces = if let Some(last) = arguments.last_mut() { - let spaces = last.value.extract_spaces(); - - if spaces.after.is_empty() { - &[] - } else { - let inner = if !spaces.before.is_empty() { - arena.alloc(spaces.item).before(spaces.before) - } else { - spaces.item - }; - *last = arena.alloc(Loc::at(last.region, inner)); - - spaces.after - } - } else { - &[] - }; - - let mut apply = Expr::Apply( - arena.alloc(loc_expr1), - arguments.into_bump_slice(), - CalledVia::Space, - ); - - if !spaces.is_empty() { - apply = arena.alloc(apply).after(spaces) - } - - Loc::at(region, apply) - } -} - -fn numeric_negate_expression<'a, T>( - arena: &'a Bump, - state: State<'a>, - loc_op: Loc, - expr: Loc>, - spaces: &'a [CommentOrNewline<'a>], -) -> Loc> { - debug_assert_eq!(state.bytes().get(0), Some(&b'-')); - // for overflow reasons, we must make the unary minus part of the number literal. - let start = state.pos(); - let region = Region::new(start, expr.region.end()); - - let new_expr = match expr.value { - Expr::Num(string) => { - let new_string = - unsafe { std::str::from_utf8_unchecked(&state.bytes()[..string.len() + 1]) }; - - Expr::Num(new_string) - } - Expr::Float(string) => { - let new_string = - unsafe { std::str::from_utf8_unchecked(&state.bytes()[..string.len() + 1]) }; - - Expr::Float(new_string) - } - Expr::NonBase10Int { - string, - base, - is_negative, - } => { - // don't include the minus sign here; it will not be parsed right - Expr::NonBase10Int { - is_negative: !is_negative, - string, - base, - } - } - _ => Expr::UnaryOp(arena.alloc(expr), Loc::at(loc_op.region, UnaryOp::Negate)), - }; - - let new_loc_expr = Loc::at(region, new_expr); - - if spaces.is_empty() { - new_loc_expr - } else { - arena - .alloc(new_loc_expr.value) - .with_spaces_before(spaces, new_loc_expr.region) - } -} - -fn append_body_definition<'a>( - arena: &'a Bump, - defs: &mut Vec<'a, &'a Loc>>, - spaces: &'a [CommentOrNewline<'a>], - loc_pattern: Loc>, - loc_def_body: Loc>, -) { - let region = Region::span_across(&loc_pattern.region, &loc_def_body.region); - - if spaces.len() <= 1 { - let last = defs.pop(); - match last.map(|d| d.value.unroll_spaces_before()) { - Some((before_ann_spaces, Def::Value(ValueDef::Annotation(ann_pattern, ann_type)))) => { - return append_body_definition_help( - arena, - defs, - region, - before_ann_spaces, - spaces, - loc_pattern, - loc_def_body, - ann_pattern, - ann_type, - ); - } - Some(( - before_ann_spaces, - Def::Type(TypeDef::Alias { - header, - ann: ann_type, - }), - )) => { - // This is a case like - // UserId x : [UserId Int] - // UserId x = UserId 42 - // We optimistically parsed the first line as an alias; we now turn it - // into an annotation. - let loc_name = arena.alloc(header.name.map(|x| Pattern::Tag(x))); - let ann_pattern = Pattern::Apply(loc_name, header.vars); - let vars_region = Region::across_all(header.vars.iter().map(|v| &v.region)); - let region_ann_pattern = Region::span_across(&loc_name.region, &vars_region); - let loc_ann_pattern = Loc::at(region_ann_pattern, ann_pattern); - - return append_body_definition_help( - arena, - defs, - region, - before_ann_spaces, - spaces, - loc_pattern, - loc_def_body, - arena.alloc(loc_ann_pattern), - ann_type, - ); - } - _ => { - defs.extend(last); - } - } - } - - // the previous and current def can't be joined up - let mut loc_def = Loc::at( - region, - Def::Value(ValueDef::Body( - arena.alloc(loc_pattern), - &*arena.alloc(loc_def_body), - )), - ); - - if !spaces.is_empty() { - loc_def = arena - .alloc(loc_def.value) - .with_spaces_before(spaces, loc_def.region); - } - - defs.push(arena.alloc(loc_def)); -} - -#[allow(clippy::too_many_arguments)] -fn append_body_definition_help<'a>( - arena: &'a Bump, - defs: &mut Vec<'a, &'a Loc>>, - region: Region, - before_ann_spaces: &'a [CommentOrNewline<'a>], - before_body_spaces: &'a [CommentOrNewline<'a>], - loc_pattern_body: Loc>, - loc_def_body: Loc>, - loc_pattern_ann: &'a Loc>, - loc_ann: &'a Loc>, -) { - let comment = match before_body_spaces.get(0) { - Some(CommentOrNewline::LineComment(s)) => Some(*s), - Some(CommentOrNewline::DocComment(s)) => Some(*s), - _ => None, - }; - - let mut loc_def = Loc::at( - region, - Def::Value(ValueDef::AnnotatedBody { - ann_pattern: loc_pattern_ann, - ann_type: loc_ann, - comment, - body_pattern: arena.alloc(loc_pattern_body), - body_expr: &*arena.alloc(loc_def_body), - }), - ); - - if !before_ann_spaces.is_empty() { - loc_def = arena - .alloc(loc_def.value) - .with_spaces_before(before_ann_spaces, loc_def.region); - } - - defs.push(arena.alloc(loc_def)); -} - -fn append_annotation_definition<'a>( - arena: &'a Bump, - defs: &mut Vec<'a, &'a Loc>>, - spaces: &'a [CommentOrNewline<'a>], - loc_pattern: Loc>, - loc_ann: Loc>, - - // If this turns out to be an alias - kind: AliasOrOpaque, - opt_derived: Option>>, -) { - let region = Region::span_across(&loc_pattern.region, &loc_ann.region); - - // the previous and current def can't be joined up - match &loc_pattern.value { - Pattern::Apply( - Loc { - value: Pattern::Tag(name), - .. - }, - alias_arguments, - ) => append_alias_or_opaque_definition( - arena, - defs, - region, - spaces, - Loc::at(loc_pattern.region, name), - alias_arguments, - loc_ann, - kind, - opt_derived, - ), - Pattern::Tag(name) => append_alias_or_opaque_definition( - arena, - defs, - region, - spaces, - Loc::at(loc_pattern.region, name), - &[], - loc_ann, - kind, - opt_derived, - ), - _ => { - let mut loc_def = Loc::at( - region, - Def::Value(ValueDef::Annotation(loc_pattern, loc_ann)), - ); - if !spaces.is_empty() { - loc_def = arena - .alloc(loc_def.value) - .with_spaces_before(spaces, loc_def.region); - } - - defs.push(arena.alloc(loc_def)); - } - } -} - -fn append_expect_definition<'a>( - arena: &'a Bump, - defs: &mut Vec<'a, &'a Loc>>, - start: Position, - spaces: &'a [CommentOrNewline<'a>], - loc_expect_body: Loc>, -) { - let def: Def = ValueDef::Expect(arena.alloc(loc_expect_body)).into(); - - let end = loc_expect_body.region.end(); - let region = Region::new(start, end); - - let mut loc_def = Loc::at(region, def); - - if !spaces.is_empty() { - loc_def = arena - .alloc(loc_def.value) - .with_spaces_before(spaces, loc_def.region); - } - - defs.push(arena.alloc(loc_def)); -} - -#[allow(clippy::too_many_arguments)] -fn append_alias_or_opaque_definition<'a>( - arena: &'a Bump, - defs: &mut Vec<'a, &'a Loc>>, - region: Region, - spaces: &'a [CommentOrNewline<'a>], - name: Loc<&'a str>, - pattern_arguments: &'a [Loc>], - loc_ann: Loc>, - kind: AliasOrOpaque, - opt_derived: Option>>, -) { - let header = TypeHeader { - name, - vars: pattern_arguments, - }; - - let type_def = match kind { - AliasOrOpaque::Alias => TypeDef::Alias { - header, - ann: loc_ann, - }, - AliasOrOpaque::Opaque => TypeDef::Opaque { - header, - typ: loc_ann, - derived: opt_derived, - }, - }; - let mut loc_def = Loc::at(region, Def::Type(type_def)); - - if !spaces.is_empty() { - loc_def = arena - .alloc(loc_def.value) - .with_spaces_before(spaces, loc_def.region); - } - - defs.push(arena.alloc(loc_def)); -} - -#[derive(Debug)] -struct DefState<'a> { - defs: Vec<'a, &'a Loc>>, - spaces_after: &'a [CommentOrNewline<'a>], -} - -fn parse_toplevel_defs_end<'a>( - _options: ExprParseOptions, - start_column: u32, - mut defs: Defs<'a>, - arena: &'a Bump, - state: State<'a>, -) -> ParseResult<'a, Defs<'a>, EExpr<'a>> { - let min_indent = start_column; - - let mut global_state = state; - - loop { - let state = global_state; - let initial = state.clone(); - - let mut spaces_before_current = &[] as &[_]; - - let state = match space0_e(min_indent, EExpr::IndentStart).parse(arena, state) { - Err((MadeProgress, _, s)) => { - return Err((MadeProgress, EExpr::DefMissingFinalExpr(s.pos()), s)); - } - Ok((_, spaces, state)) => { - spaces_before_current = spaces; - state - } - Err((NoProgress, _, state)) => state, - }; - - let start = state.pos(); - - match space0_after_e( - crate::pattern::loc_pattern_help(min_indent), - min_indent, - EPattern::IndentEnd, - ) - .parse(arena, state.clone()) - { - Err((NoProgress, _, _)) => { - match crate::parser::keyword_e(crate::keyword::EXPECT, EExpect::Expect) - .parse(arena, state) - { - Err((_, _, _)) => { - // a hacky way to get expression-based error messages. TODO fix this - return Ok((NoProgress, defs, initial)); - } - Ok((_, _, state)) => { - let parse_def_expr = space0_before_e( - move |a, s| parse_loc_expr(min_indent + 1, a, s), - min_indent, - EExpr::IndentEnd, - ); - - let (_, loc_def_expr, state) = parse_def_expr.parse(arena, state)?; - - let end = loc_def_expr.region.end(); - let region = Region::new(start, end); - - let value_def = ValueDef::Expect(arena.alloc(loc_def_expr)); - defs.push_value_def(value_def, region, spaces_before_current, &[]); - - global_state = state; - continue; - } - } - } - Err((MadeProgress, _, _)) => { - // a hacky way to get expression-based error messages. TODO fix this - return Ok((NoProgress, defs, initial)); - } - Ok((_, loc_pattern, state)) => { - // First let's check whether this is an ability definition. - let opt_tag_and_args: Option<(&str, Region, &[Loc])> = - match loc_pattern.value { - Pattern::Apply( - Loc { - value: Pattern::Tag(name), - region, - }, - args, - ) => Some((name, *region, args)), - Pattern::Tag(name) => Some((name, loc_pattern.region, &[])), - _ => None, - }; - - if let Some((name, name_region, args)) = opt_tag_and_args { - if let Ok((_, loc_has, state)) = - loc_has_parser(min_indent).parse(arena, state.clone()) - { - let (_, (type_def, def_region), state) = finish_parsing_ability_def_help( - start_column, - Loc::at(name_region, name), - args, - loc_has, - arena, - state, - )?; - - defs.push_type_def(type_def, def_region, spaces_before_current, &[]); - - global_state = state; - continue; - } - } - - // Otherwise, this is a def or alias. - match operator().parse(arena, state) { - Ok((_, BinOp::Assignment, state)) => { - let parse_def_expr = space0_before_e( - move |a, s| parse_loc_expr(min_indent + 1, a, s), - min_indent, - EExpr::IndentEnd, - ); - - let (_, loc_def_expr, state) = parse_def_expr.parse(arena, state)?; - - { - let region = - Region::span_across(&loc_pattern.region, &loc_def_expr.region); - - if spaces_before_current.len() <= 1 { - let comment = match spaces_before_current.get(0) { - Some(CommentOrNewline::LineComment(s)) => Some(*s), - Some(CommentOrNewline::DocComment(s)) => Some(*s), - _ => None, - }; - - match defs.last() { - Some(Err(ValueDef::Annotation(ann_pattern, ann_type))) => { - // join this body with the preceding annotation - - let value_def = ValueDef::AnnotatedBody { - ann_pattern: arena.alloc(*ann_pattern), - ann_type: arena.alloc(*ann_type), - comment, - body_pattern: arena.alloc(loc_pattern), - body_expr: &*arena.alloc(loc_def_expr), - }; - - let region = - Region::span_across(&ann_pattern.region, ®ion); - - defs.replace_with_value_def( - defs.tags.len() - 1, - value_def, - region, - ) - } - Some(Ok(TypeDef::Alias { - header, - ann: ann_type, - })) => { - // This is a case like - // UserId x : [UserId Int] - // UserId x = UserId 42 - // We optimistically parsed the first line as an alias; we now turn it - // into an annotation. - - let loc_name = - arena.alloc(header.name.map(|x| Pattern::Tag(x))); - let ann_pattern = Pattern::Apply(loc_name, header.vars); - - let vars_region = Region::across_all( - header.vars.iter().map(|v| &v.region), - ); - let region_ann_pattern = - Region::span_across(&loc_name.region, &vars_region); - let loc_ann_pattern = - Loc::at(region_ann_pattern, ann_pattern); - - let value_def = ValueDef::AnnotatedBody { - ann_pattern: arena.alloc(loc_ann_pattern), - ann_type: arena.alloc(*ann_type), - comment, - body_pattern: arena.alloc(loc_pattern), - body_expr: &*arena.alloc(loc_def_expr), - }; - - let region = - Region::span_across(&header.name.region, ®ion); - - defs.replace_with_value_def( - defs.tags.len() - 1, - value_def, - region, - ) - } - _ => { - // the previous and current def can't be joined up - let value_def = ValueDef::Body( - arena.alloc(loc_pattern), - &*arena.alloc(loc_def_expr), - ); - - defs.push_value_def( - value_def, - region, - spaces_before_current, - &[], - ) - } - } - } else { - // the previous and current def can't be joined up - let value_def = ValueDef::Body( - arena.alloc(loc_pattern), - &*arena.alloc(loc_def_expr), - ); - - defs.push_value_def(value_def, region, spaces_before_current, &[]) - } - }; - - global_state = state; - continue; - } - Ok((_, BinOp::IsAliasType, state)) => { - let (_, ann_type, state) = - alias_signature_with_space_before(min_indent + 1) - .parse(arena, state)?; - - let region = Region::span_across(&loc_pattern.region, &ann_type.region); - - // the previous and current def can't be joined up - match &loc_pattern.value { - Pattern::Apply( - Loc { - value: Pattern::Tag(name), - .. - }, - alias_arguments, - ) => { - let name = Loc::at(loc_pattern.region, *name); - let header = TypeHeader { - name, - vars: alias_arguments, - }; - - let type_def = TypeDef::Alias { - header, - ann: ann_type, - }; - - defs.push_type_def(type_def, region, spaces_before_current, &[]); - } - Pattern::Tag(name) => { - let name = Loc::at(loc_pattern.region, *name); - let pattern_arguments: &'a [Loc>] = &[]; - let header = TypeHeader { - name, - vars: pattern_arguments, - }; - - let type_def = TypeDef::Alias { - header, - ann: ann_type, - }; - - defs.push_type_def(type_def, region, spaces_before_current, &[]); - } - _ => { - let value_def = ValueDef::Annotation(loc_pattern, ann_type); - defs.push_value_def(value_def, region, spaces_before_current, &[]); - } - }; - - global_state = state; - continue; - } - Ok((_, BinOp::IsOpaqueType, state)) => { - let (_, (signature, derived), state) = - opaque_signature_with_space_before(min_indent + 1) - .parse(arena, state)?; - - let region = Region::span_across(&loc_pattern.region, &signature.region); - - // the previous and current def can't be joined up - match &loc_pattern.value { - Pattern::Apply( - Loc { - value: Pattern::Tag(name), - .. - }, - alias_arguments, - ) => { - let name = Loc::at(loc_pattern.region, *name); - let header = TypeHeader { - name, - vars: alias_arguments, - }; - - let type_def = TypeDef::Opaque { - header, - typ: signature, - derived, - }; - - defs.push_type_def(type_def, region, spaces_before_current, &[]); - } - Pattern::Tag(name) => { - let name = Loc::at(loc_pattern.region, *name); - let pattern_arguments: &'a [Loc>] = &[]; - let header = TypeHeader { - name, - vars: pattern_arguments, - }; - - let type_def = TypeDef::Opaque { - header, - typ: signature, - derived, - }; - - defs.push_type_def(type_def, region, spaces_before_current, &[]); - } - _ => { - let value_def = ValueDef::Annotation(loc_pattern, signature); - defs.push_value_def(value_def, region, spaces_before_current, &[]); - } - }; - - global_state = state; - continue; - } - - _ => return Ok((MadeProgress, defs, initial)), - } - } - } - } -} - -fn parse_defs_end<'a>( - options: ExprParseOptions, - start_column: u32, - mut def_state: DefState<'a>, - arena: &'a Bump, - state: State<'a>, -) -> ParseResult<'a, DefState<'a>, EExpr<'a>> { - let min_indent = start_column; - let initial = state.clone(); - - let state = match space0_e(min_indent, EExpr::IndentStart).parse(arena, state) { - Err((MadeProgress, _, s)) => { - return Err((MadeProgress, EExpr::DefMissingFinalExpr(s.pos()), s)); - } - Ok((_, spaces, state)) => { - def_state.spaces_after = spaces; - state - } - Err((NoProgress, _, state)) => state, - }; - - let start = state.pos(); - let column = state.column(); - - match space0_after_e( - crate::pattern::loc_pattern_help(min_indent), - min_indent, - EPattern::IndentEnd, - ) - .parse(arena, state.clone()) - { - Err((NoProgress, _, _)) => { - match crate::parser::keyword_e(crate::keyword::EXPECT, EExpect::Expect) - .parse(arena, state) - { - Err((_, _, _)) => { - // a hacky way to get expression-based error messages. TODO fix this - Ok((NoProgress, def_state, initial)) - } - Ok((_, _, state)) => { - let parse_def_expr = space0_before_e( - move |a, s| parse_loc_expr(min_indent + 1, a, s), - min_indent, - EExpr::IndentEnd, - ); - - let (_, loc_def_expr, state) = parse_def_expr.parse(arena, state)?; - - append_expect_definition( - arena, - &mut def_state.defs, - start, - def_state.spaces_after, - loc_def_expr, - ); - - parse_defs_end(options, column, def_state, arena, state) - } - } - } - Err((MadeProgress, _, _)) => { - // a hacky way to get expression-based error messages. TODO fix this - Ok((NoProgress, def_state, initial)) - } - Ok((_, loc_pattern, state)) => { - // First let's check whether this is an ability definition. - let opt_tag_and_args: Option<(&str, Region, &[Loc])> = match loc_pattern.value - { - Pattern::Apply( - Loc { - value: Pattern::Tag(name), - region, - }, - args, - ) => Some((name, *region, args)), - Pattern::Tag(name) => Some((name, loc_pattern.region, &[])), - _ => None, - }; - - if let Some((name, name_region, args)) = opt_tag_and_args { - if let Ok((_, loc_has, state)) = - loc_has_parser(min_indent).parse(arena, state.clone()) - { - let (_, loc_def, state) = finish_parsing_ability_def( - start_column, - Loc::at(name_region, name), - args, - loc_has, - def_state.spaces_after, - arena, - state, - )?; - - def_state.defs.push(loc_def); - - return parse_defs_end(options, column, def_state, arena, state); - } - } - - // Otherwise, this is a def or alias. - match operator().parse(arena, state) { - Ok((_, BinOp::Assignment, state)) => { - let parse_def_expr = space0_before_e( - move |a, s| parse_loc_expr(min_indent + 1, a, s), - min_indent, - EExpr::IndentEnd, - ); - - let (_, loc_def_expr, state) = parse_def_expr.parse(arena, state)?; - - append_body_definition( - arena, - &mut def_state.defs, - def_state.spaces_after, - loc_pattern, - loc_def_expr, - ); - - parse_defs_end(options, column, def_state, arena, state) - } - Ok((_, BinOp::IsAliasType, state)) => { - let (_, ann_type, state) = - alias_signature_with_space_before(min_indent + 1).parse(arena, state)?; - - append_annotation_definition( - arena, - &mut def_state.defs, - def_state.spaces_after, - loc_pattern, - ann_type, - AliasOrOpaque::Alias, - None, - ); - - parse_defs_end(options, column, def_state, arena, state) - } - Ok((_, BinOp::IsOpaqueType, state)) => { - let (_, (signature, derived), state) = - opaque_signature_with_space_before(min_indent + 1).parse(arena, state)?; - - append_annotation_definition( - arena, - &mut def_state.defs, - def_state.spaces_after, - loc_pattern, - signature, - AliasOrOpaque::Opaque, - derived, - ); - - parse_defs_end(options, column, def_state, arena, state) - } - - _ => Ok((MadeProgress, def_state, initial)), - } - } - } -} - -fn parse_defs_expr<'a>( - options: ExprParseOptions, - start_column: u32, - def_state: DefState<'a>, - arena: &'a Bump, - state: State<'a>, -) -> ParseResult<'a, Expr<'a>, EExpr<'a>> { - let min_indent = start_column; - - match parse_defs_end(options, start_column, def_state, arena, state) { - Err(bad) => Err(bad), - Ok((_, def_state, state)) => { - // this is no def, because there is no `=` or `:`; parse as an expr - let parse_final_expr = space0_before_e( - move |a, s| parse_loc_expr(min_indent, a, s), - min_indent, - EExpr::IndentEnd, - ); - - match parse_final_expr.parse(arena, state) { - Err((_, fail, state)) => { - return Err(( - MadeProgress, - EExpr::DefMissingFinalExpr2(arena.alloc(fail), state.pos()), - state, - )); - } - Ok((_, loc_ret, state)) => { - return Ok(( - MadeProgress, - Expr::Defs(def_state.defs.into_bump_slice(), arena.alloc(loc_ret)), - state, - )); - } - } - } - } -} - -fn alias_signature_with_space_before<'a>( - min_indent: u32, -) -> impl Parser<'a, Loc>, EExpr<'a>> { - specialize( - EExpr::Type, - space0_before_e( - type_annotation::located(min_indent + 1, false), - min_indent + 1, - EType::TIndentStart, - ), - ) -} - -fn opaque_signature_with_space_before<'a>( - min_indent: u32, -) -> impl Parser<'a, (Loc>, Option>>), EExpr<'a>> { - and!( - specialize( - EExpr::Type, - space0_before_e( - type_annotation::located_opaque_signature(min_indent, true), - min_indent, - EType::TIndentStart, - ), - ), - optional(specialize( - EExpr::Type, - space0_before_e( - type_annotation::has_derived(min_indent), - min_indent, - EType::TIndentStart, - ), - )) - ) -} - -#[derive(Copy, Clone, PartialEq, Eq)] -enum AliasOrOpaque { - Alias, - Opaque, -} - -#[allow(clippy::too_many_arguments)] -fn finish_parsing_alias_or_opaque<'a>( - min_indent: u32, - options: ExprParseOptions, - start_column: u32, - expr_state: ExprState<'a>, - loc_op: Loc, - arena: &'a Bump, - state: State<'a>, - spaces_after_operator: &'a [CommentOrNewline<'a>], - kind: AliasOrOpaque, -) -> ParseResult<'a, Expr<'a>, EExpr<'a>> { - let expr_region = expr_state.expr.region; - let indented_more = start_column + 1; - - let (expr, arguments) = expr_state - .validate_is_type_def(arena, loc_op, kind) - .map_err(|fail| (MadeProgress, fail, state.clone()))?; - - let (loc_def, state) = match &expr.value { - Expr::Tag(name) => { - let mut type_arguments = Vec::with_capacity_in(arguments.len(), arena); - - for argument in arguments { - match expr_to_pattern_help(arena, &argument.value) { - Ok(good) => { - type_arguments.push(Loc::at(argument.region, good)); - } - Err(_) => panic!(), - } - } - - let (loc_def, state) = match kind { - AliasOrOpaque::Alias => { - let (_, signature, state) = - alias_signature_with_space_before(indented_more).parse(arena, state)?; - - let def_region = Region::span_across(&expr.region, &signature.region); - - let header = TypeHeader { - name: Loc::at(expr.region, name), - vars: type_arguments.into_bump_slice(), - }; - - let def = TypeDef::Alias { - header, - ann: signature, - }; - - (Loc::at(def_region, Def::Type(def)), state) - } - - AliasOrOpaque::Opaque => { - let (_, (signature, derived), state) = - opaque_signature_with_space_before(indented_more).parse(arena, state)?; - - let def_region = Region::span_across(&expr.region, &signature.region); - - let header = TypeHeader { - name: Loc::at(expr.region, name), - vars: type_arguments.into_bump_slice(), - }; - - let def = TypeDef::Opaque { - header, - typ: signature, - derived, - }; - - (Loc::at(def_region, Def::Type(def)), state) - } - }; - - (&*arena.alloc(loc_def), state) - } - - _ => { - let call = to_call(arena, arguments, expr); - - match expr_to_pattern_help(arena, &call.value) { - Ok(good) => { - let parser = specialize( - EExpr::Type, - space0_before_e( - type_annotation::located(indented_more, false), - min_indent, - EType::TIndentStart, - ), - ); - - match parser.parse(arena, state) { - Err((_, fail, state)) => return Err((MadeProgress, fail, state)), - Ok((_, mut ann_type, state)) => { - // put the spaces from after the operator in front of the call - if !spaces_after_operator.is_empty() { - ann_type = arena - .alloc(ann_type.value) - .with_spaces_before(spaces_after_operator, ann_type.region); - } - - let def_region = Region::span_across(&call.region, &ann_type.region); - - let alias = ValueDef::Annotation(Loc::at(expr_region, good), ann_type); - - (&*arena.alloc(Loc::at(def_region, alias.into())), state) - } - } - } - Err(_) => { - // this `:`/`:=` likely occurred inline; treat it as an invalid operator - let op = match kind { - AliasOrOpaque::Alias => ":", - AliasOrOpaque::Opaque => ":=", - }; - let fail = EExpr::BadOperator(op, loc_op.region.start()); - - return Err((MadeProgress, fail, state)); - } - } - } - }; - - let def_state = DefState { - defs: bumpalo::vec![in arena; loc_def], - spaces_after: &[], - }; - - parse_defs_expr(options, start_column, def_state, arena, state) -} - -mod ability { - use super::*; - use crate::{ - ast::{AbilityMember, Spaceable, Spaced}, - parser::EAbility, - }; - - /// Parses a single ability demand line; see `parse_demand`. - fn parse_demand_help<'a>( - start_column: u32, - ) -> impl Parser<'a, AbilityMember<'a>, EAbility<'a>> { - map!( - and!( - specialize(|_, pos| EAbility::DemandName(pos), loc!(lowercase_ident())), - skip_first!( - and!( - // TODO: do we get anything from picking up spaces here? - space0_e(start_column, EAbility::DemandName), - word1(b':', EAbility::DemandColon) - ), - specialize( - EAbility::Type, - // Require the type to be more indented than the name - type_annotation::located(start_column + 1, true) - ) - ) - ), - |(name, typ): (Loc<&'a str>, Loc>)| { - AbilityMember { - name: name.map_owned(Spaced::Item), - typ, - } - } - ) - } - - pub enum IndentLevel { - PendingMin(u32), - Exact(u32), - } - - /// Parses an ability demand like `hash : a -> U64 | a has Hash`, in the context of a larger - /// ability definition. - /// This is basically the same as parsing a free-floating annotation, but with stricter rules. - pub fn parse_demand<'a>( - indent: IndentLevel, - ) -> impl Parser<'a, (u32, AbilityMember<'a>), EAbility<'a>> { - move |arena, state: State<'a>| { - let initial = state.clone(); - - // Put no restrictions on the indent after the spaces; we'll check it manually. - match space0_e(0, EAbility::DemandName).parse(arena, state) { - Err((MadeProgress, fail, _)) => Err((NoProgress, fail, initial)), - Err((NoProgress, fail, _)) => Err((NoProgress, fail, initial)), - - Ok((_progress, spaces, state)) => { - match indent { - IndentLevel::PendingMin(min_indent) if state.column() < min_indent => { - let indent_difference = state.column() as i32 - min_indent as i32; - Err(( - MadeProgress, - EAbility::DemandAlignment(indent_difference, state.pos()), - initial, - )) - } - IndentLevel::Exact(wanted) if state.column() < wanted => { - // This demand is not indented correctly - let indent_difference = state.column() as i32 - wanted as i32; - Err(( - // Rollback because the deindent may be because there is a next - // expression - NoProgress, - EAbility::DemandAlignment(indent_difference, state.pos()), - initial, - )) - } - IndentLevel::Exact(wanted) if state.column() > wanted => { - // This demand is not indented correctly - let indent_difference = state.column() as i32 - wanted as i32; - Err(( - MadeProgress, - EAbility::DemandAlignment(indent_difference, state.pos()), - initial, - )) - } - _ => { - let indent_column = state.column(); - - let parser = parse_demand_help(indent_column); - - match parser.parse(arena, state) { - Err((MadeProgress, fail, state)) => { - Err((MadeProgress, fail, state)) - } - Err((NoProgress, fail, _)) => { - // We made progress relative to the entire ability definition, - // so this is an error. - Err((MadeProgress, fail, initial)) - } - - Ok((_, mut demand, state)) => { - // Tag spaces onto the parsed demand name - if !spaces.is_empty() { - demand.name = arena - .alloc(demand.name.value) - .with_spaces_before(spaces, demand.name.region); - } - - Ok((MadeProgress, (indent_column, demand), state)) - } - } - } - } - } - } - } - } -} - -fn finish_parsing_ability_def<'a>( - start_column: u32, - name: Loc<&'a str>, - args: &'a [Loc>], - loc_has: Loc>, - spaces_before: &'a [CommentOrNewline<'a>], - arena: &'a Bump, - state: State<'a>, -) -> ParseResult<'a, &'a Loc>, EExpr<'a>> { - let (_, (type_def, def_region), state) = - finish_parsing_ability_def_help(start_column, name, args, loc_has, arena, state)?; - - let def = Def::Type(type_def); - - let loc_def = &*(if spaces_before.is_empty() { - arena.alloc(Loc::at(def_region, def)) - } else { - arena.alloc( - arena - .alloc(def) - .with_spaces_before(spaces_before, def_region), - ) - }); - - Ok((MadeProgress, loc_def, state)) -} - -fn finish_parsing_ability_def_help<'a>( - start_column: u32, - name: Loc<&'a str>, - args: &'a [Loc>], - loc_has: Loc>, - arena: &'a Bump, - state: State<'a>, -) -> ParseResult<'a, (TypeDef<'a>, Region), EExpr<'a>> { - let mut demands = Vec::with_capacity_in(2, arena); - - let min_indent_for_demand = start_column + 1; - - // Parse the first demand. This will determine the indentation level all the - // other demands must observe. - let (_, (demand_indent_level, first_demand), mut state) = - ability::parse_demand(ability::IndentLevel::PendingMin(min_indent_for_demand)) - .parse(arena, state) - .map_err(|(progress, err, state)| { - (progress, EExpr::Ability(err, state.pos()), state) - })?; - demands.push(first_demand); - - let demand_indent = ability::IndentLevel::Exact(demand_indent_level); - let demand_parser = ability::parse_demand(demand_indent); - - loop { - match demand_parser.parse(arena, state.clone()) { - Ok((_, (_indent, demand), next_state)) => { - state = next_state; - demands.push(demand); - } - Err((MadeProgress, problem, old_state)) => { - return Err(( - MadeProgress, - EExpr::Ability(problem, old_state.pos()), - old_state, - )); - } - Err((NoProgress, _, old_state)) => { - state = old_state; - break; - } - } - } - - let def_region = Region::span_across(&name.region, &demands.last().unwrap().typ.region); - let type_def = TypeDef::Ability { - header: TypeHeader { name, vars: args }, - loc_has, - members: demands.into_bump_slice(), - }; - - Ok((MadeProgress, (type_def, def_region), state)) -} - -fn finish_parsing_ability<'a>( - start_column: u32, - options: ExprParseOptions, - name: Loc<&'a str>, - args: &'a [Loc>], - loc_has: Loc>, - arena: &'a Bump, - state: State<'a>, -) -> ParseResult<'a, Expr<'a>, EExpr<'a>> { - let (_, loc_def, state) = - finish_parsing_ability_def(start_column, name, args, loc_has, &[], arena, state)?; - - let def_state = DefState { - defs: bumpalo::vec![in arena; loc_def], - spaces_after: &[], - }; - - parse_defs_expr(options, start_column, def_state, arena, state) -} - -fn parse_expr_operator<'a>( - min_indent: u32, - options: ExprParseOptions, - start_column: u32, - mut expr_state: ExprState<'a>, - loc_op: Loc, - arena: &'a Bump, - state: State<'a>, -) -> ParseResult<'a, Expr<'a>, EExpr<'a>> { - let (_, spaces_after_operator, state) = - space0_e(min_indent, EExpr::IndentEnd).parse(arena, state)?; - - // a `-` is unary if it is preceded by a space and not followed by a space - - let op = loc_op.value; - let op_start = loc_op.region.start(); - let op_end = loc_op.region.end(); - let new_start = state.pos(); - match op { - BinOp::Minus if expr_state.end != op_start && op_end == new_start => { - // negative terms - - let (_, negated_expr, state) = parse_loc_term(min_indent, options, arena, state)?; - let new_end = state.pos(); - - let arg = numeric_negate_expression( - arena, - expr_state.initial, - loc_op, - negated_expr, - expr_state.spaces_after, - ); - - expr_state.initial = state.clone(); - - let (spaces, state) = match space0_e(min_indent, EExpr::IndentEnd).parse(arena, state) { - Err((_, _, state)) => (&[] as &[_], state), - Ok((_, spaces, state)) => (spaces, state), - }; - - expr_state.arguments.push(arena.alloc(arg)); - expr_state.spaces_after = spaces; - expr_state.end = new_end; - - parse_expr_end(min_indent, options, start_column, expr_state, arena, state) - } - BinOp::Assignment => { - let expr_region = expr_state.expr.region; - let indented_more = start_column + 1; - - let call = expr_state - .validate_assignment_or_backpassing(arena, loc_op, EExpr::ElmStyleFunction) - .map_err(|fail| (MadeProgress, fail, state.clone()))?; - - let (loc_def, state) = { - match expr_to_pattern_help(arena, &call.value) { - Ok(good) => { - let (_, mut body, state) = parse_loc_expr(indented_more, arena, state)?; - - // put the spaces from after the operator in front of the call - if !spaces_after_operator.is_empty() { - body = arena - .alloc(body.value) - .with_spaces_before(spaces_after_operator, body.region); - } - - let body_region = Region::span_across(&call.region, &body.region); - - let alias = ValueDef::Body( - arena.alloc(Loc::at(expr_region, good)), - arena.alloc(body), - ) - .into(); - - (&*arena.alloc(Loc::at(body_region, alias)), state) - } - Err(_) => { - // this `=` likely occurred inline; treat it as an invalid operator - let fail = EExpr::BadOperator(arena.alloc("="), loc_op.region.start()); - - return Err((MadeProgress, fail, state)); - } - } - }; - - let def_state = DefState { - defs: bumpalo::vec![in arena; loc_def], - spaces_after: &[], - }; - - parse_defs_expr(options, start_column, def_state, arena, state) - } - BinOp::Backpassing => { - let expr_region = expr_state.expr.region; - let indented_more = start_column + 1; - - let call = expr_state - .validate_assignment_or_backpassing(arena, loc_op, |_, pos| { - EExpr::BadOperator("<-", pos) - }) - .map_err(|fail| (MadeProgress, fail, state.clone()))?; - - let (loc_pattern, loc_body, state) = { - match expr_to_pattern_help(arena, &call.value) { - Ok(good) => { - let (_, mut ann_type, state) = parse_loc_expr(indented_more, arena, state)?; - - // put the spaces from after the operator in front of the call - if !spaces_after_operator.is_empty() { - ann_type = arena - .alloc(ann_type.value) - .with_spaces_before(spaces_after_operator, ann_type.region); - } - - (Loc::at(expr_region, good), ann_type, state) - } - Err(_) => { - // this `=` likely occurred inline; treat it as an invalid operator - let fail = EExpr::BadOperator("=", loc_op.region.start()); - - return Err((MadeProgress, fail, state)); - } - } - }; - - let parse_cont = space0_before_e( - move |a, s| parse_loc_expr(min_indent, a, s), - min_indent, - EExpr::IndentEnd, - ); - - let (_, loc_cont, state) = parse_cont.parse(arena, state)?; - - let ret = Expr::Backpassing( - arena.alloc([loc_pattern]), - arena.alloc(loc_body), - arena.alloc(loc_cont), - ); - - Ok((MadeProgress, ret, state)) - } - BinOp::IsAliasType | BinOp::IsOpaqueType => finish_parsing_alias_or_opaque( - min_indent, - options, - start_column, - expr_state, - loc_op, - arena, - state, - spaces_after_operator, - match op { - BinOp::IsAliasType => AliasOrOpaque::Alias, - BinOp::IsOpaqueType => AliasOrOpaque::Opaque, - _ => unreachable!(), - }, - ), - _ => match loc_possibly_negative_or_negated_term(min_indent, options).parse(arena, state) { - Err((MadeProgress, f, s)) => Err((MadeProgress, f, s)), - Ok((_, mut new_expr, state)) => { - let new_end = state.pos(); - - expr_state.initial = state.clone(); - - // put the spaces from after the operator in front of the new_expr - if !spaces_after_operator.is_empty() { - new_expr = arena - .alloc(new_expr.value) - .with_spaces_before(spaces_after_operator, new_expr.region); - } - - match space0_e(min_indent, EExpr::IndentEnd).parse(arena, state) { - Err((_, _, state)) => { - let args = std::mem::replace(&mut expr_state.arguments, Vec::new_in(arena)); - - let call = to_call(arena, args, expr_state.expr); - - expr_state.operators.push((call, loc_op)); - expr_state.expr = new_expr; - expr_state.end = new_end; - expr_state.spaces_after = &[]; - - parse_expr_final(expr_state, arena, state) - } - Ok((_, spaces, state)) => { - let args = std::mem::replace(&mut expr_state.arguments, Vec::new_in(arena)); - - let call = to_call(arena, args, expr_state.expr); - - expr_state.operators.push((call, loc_op)); - expr_state.expr = new_expr; - expr_state.end = new_end; - expr_state.spaces_after = spaces; - - // TODO new start? - parse_expr_end(min_indent, options, start_column, expr_state, arena, state) - } - } - } - Err((NoProgress, expr, e)) => { - todo!("{:?} {:?}", expr, e) - } - }, - } -} - -fn parse_expr_end<'a>( - min_indent: u32, - options: ExprParseOptions, - start_column: u32, - mut expr_state: ExprState<'a>, - arena: &'a Bump, - state: State<'a>, -) -> ParseResult<'a, Expr<'a>, EExpr<'a>> { - let parser = skip_first!( - crate::blankspace::check_indent(min_indent, EExpr::IndentEnd), - move |a, s| parse_loc_term(min_indent, options, a, s) - ); - - match parser.parse(arena, state.clone()) { - Err((MadeProgress, f, s)) => Err((MadeProgress, f, s)), - Ok(( - _, - has @ Loc { - value: - Expr::Var { - module_name: "", - ident: "has", - }, - .. - }, - state, - )) if matches!(expr_state.expr.value, Expr::Tag(..)) => { - // This is an ability definition, `Ability arg1 ... has ...`. - - let name = expr_state.expr.map_owned(|e| match e { - Expr::Tag(name) => name, - _ => unreachable!(), - }); - - let mut arguments = Vec::with_capacity_in(expr_state.arguments.len(), arena); - for argument in expr_state.arguments { - match expr_to_pattern_help(arena, &argument.value) { - Ok(good) => { - arguments.push(Loc::at(argument.region, good)); - } - Err(_) => { - let start = argument.region.start(); - let err = &*arena.alloc(EPattern::Start(start)); - return Err(( - MadeProgress, - EExpr::Pattern(err, argument.region.start()), - state, - )); - } - } - } - - // Attach any spaces to the `has` keyword - let has = if !expr_state.spaces_after.is_empty() { - arena - .alloc(Has::Has) - .with_spaces_before(expr_state.spaces_after, has.region) - } else { - Loc::at(has.region, Has::Has) - }; - - finish_parsing_ability( - start_column, - options, - name, - arguments.into_bump_slice(), - has, - arena, - state, - ) - } - Ok((_, mut arg, state)) => { - let new_end = state.pos(); - - // now that we have `function arg1 ... argn`, attach the spaces to the `argn` - if !expr_state.spaces_after.is_empty() { - arg = arena - .alloc(arg.value) - .with_spaces_before(expr_state.spaces_after, arg.region); - - expr_state.spaces_after = &[]; - } - expr_state.initial = state.clone(); - - match space0_e(min_indent, EExpr::IndentEnd).parse(arena, state) { - Err((_, _, state)) => { - expr_state.arguments.push(arena.alloc(arg)); - expr_state.end = new_end; - expr_state.spaces_after = &[]; - - parse_expr_final(expr_state, arena, state) - } - Ok((_, new_spaces, state)) => { - expr_state.arguments.push(arena.alloc(arg)); - expr_state.end = new_end; - expr_state.spaces_after = new_spaces; - - parse_expr_end(min_indent, options, start_column, expr_state, arena, state) - } - } - } - Err((NoProgress, _, _)) => { - let before_op = state.clone(); - // try an operator - match loc!(operator()).parse(arena, state) { - Err((MadeProgress, f, s)) => Err((MadeProgress, f, s)), - Ok((_, loc_op, state)) => { - expr_state.consume_spaces(arena); - expr_state.initial = before_op; - parse_expr_operator( - min_indent, - options, - start_column, - expr_state, - loc_op, - arena, - state, - ) - } - Err((NoProgress, _, mut state)) => { - // try multi-backpassing - if options.accept_multi_backpassing && state.bytes().starts_with(b",") { - state = state.advance(1); - - let (_, mut patterns, state) = specialize_ref( - EExpr::Pattern, - crate::parser::sep_by0( - word1(b',', EPattern::Start), - space0_around_ee( - crate::pattern::loc_pattern_help(min_indent), - min_indent, - EPattern::Start, - EPattern::IndentEnd, - ), - ), - ) - .parse(arena, state)?; - - expr_state.consume_spaces(arena); - let call = to_call(arena, expr_state.arguments, expr_state.expr); - - let loc_pattern = Loc::at( - call.region, - expr_to_pattern_help(arena, &call.value).unwrap(), - ); - - patterns.insert(0, loc_pattern); - - match word2(b'<', b'-', EExpr::BackpassArrow).parse(arena, state) { - Err((_, fail, state)) => Err((MadeProgress, fail, state)), - Ok((_, _, state)) => { - let min_indent = start_column; - - let parse_body = space0_before_e( - move |a, s| parse_loc_expr(min_indent + 1, a, s), - min_indent, - EExpr::IndentEnd, - ); - - let (_, loc_body, state) = parse_body.parse(arena, state)?; - - let parse_cont = space0_before_e( - move |a, s| parse_loc_expr(min_indent, a, s), - min_indent, - EExpr::IndentEnd, - ); - - let (_, loc_cont, state) = parse_cont.parse(arena, state)?; - - let ret = Expr::Backpassing( - patterns.into_bump_slice(), - arena.alloc(loc_body), - arena.alloc(loc_cont), - ); - - Ok((MadeProgress, ret, state)) - } - } - } else if options.check_for_arrow && state.bytes().starts_with(b"->") { - Err((MadeProgress, EExpr::BadOperator("->", state.pos()), state)) - } else { - // roll back space parsing - let state = expr_state.initial.clone(); - - parse_expr_final(expr_state, arena, state) - } - } - } - } - } -} - -pub fn parse_loc_expr<'a>( - min_indent: u32, - arena: &'a Bump, - state: State<'a>, -) -> ParseResult<'a, Loc>, EExpr<'a>> { - parse_loc_expr_with_options( - min_indent, - ExprParseOptions { - accept_multi_backpassing: true, - ..Default::default() - }, - arena, - state, - ) -} - -pub fn parse_loc_expr_no_multi_backpassing<'a>( - min_indent: u32, - arena: &'a Bump, - state: State<'a>, -) -> ParseResult<'a, Loc>, EExpr<'a>> { - parse_loc_expr_with_options( - min_indent, - ExprParseOptions { - accept_multi_backpassing: false, - ..Default::default() - }, - arena, - state, - ) -} - -fn parse_loc_expr_with_options<'a>( - min_indent: u32, - options: ExprParseOptions, - arena: &'a Bump, - state: State<'a>, -) -> ParseResult<'a, Loc>, EExpr<'a>> { - let column = state.column(); - parse_expr_start(min_indent, options, column, arena, state) -} - -/// If the given Expr would parse the same way as a valid Pattern, convert it. -/// Example: (foo) could be either an Expr::Var("foo") or Pattern::Identifier("foo") -fn expr_to_pattern_help<'a>(arena: &'a Bump, expr: &Expr<'a>) -> Result, ()> { - match expr { - Expr::Var { module_name, ident } => { - if module_name.is_empty() { - Ok(Pattern::Identifier(ident)) - } else { - Ok(Pattern::QualifiedIdentifier { module_name, ident }) - } - } - Expr::Underscore(opt_name) => Ok(Pattern::Underscore(opt_name)), - Expr::Tag(value) => Ok(Pattern::Tag(value)), - Expr::OpaqueRef(value) => Ok(Pattern::OpaqueRef(value)), - Expr::Apply(loc_val, loc_args, _) => { - let region = loc_val.region; - let value = expr_to_pattern_help(arena, &loc_val.value)?; - let val_pattern = arena.alloc(Loc { region, value }); - - let mut arg_patterns = Vec::with_capacity_in(loc_args.len(), arena); - - for loc_arg in loc_args.iter() { - let region = loc_arg.region; - let value = expr_to_pattern_help(arena, &loc_arg.value)?; - - arg_patterns.push(Loc { region, value }); - } - - let pattern = Pattern::Apply(val_pattern, arg_patterns.into_bump_slice()); - - Ok(pattern) - } - - Expr::SpaceBefore(sub_expr, spaces) => Ok(Pattern::SpaceBefore( - arena.alloc(expr_to_pattern_help(arena, sub_expr)?), - spaces, - )), - Expr::SpaceAfter(sub_expr, spaces) => Ok(Pattern::SpaceAfter( - arena.alloc(expr_to_pattern_help(arena, sub_expr)?), - spaces, - )), - - Expr::ParensAround(sub_expr) => expr_to_pattern_help(arena, sub_expr), - - Expr::Record(fields) => { - let patterns = fields.map_items_result(arena, |loc_assigned_field| { - let region = loc_assigned_field.region; - let value = assigned_expr_field_to_pattern_help(arena, &loc_assigned_field.value)?; - Ok(Loc { region, value }) - })?; - - Ok(Pattern::RecordDestructure(patterns)) - } - - &Expr::Float(string) => Ok(Pattern::FloatLiteral(string)), - &Expr::Num(string) => Ok(Pattern::NumLiteral(string)), - Expr::NonBase10Int { - string, - base, - is_negative, - } => Ok(Pattern::NonBase10Literal { - string, - base: *base, - is_negative: *is_negative, - }), - // These would not have parsed as patterns - Expr::AccessorFunction(_) - | Expr::Access(_, _) - | Expr::List { .. } - | Expr::Closure(_, _) - | Expr::Backpassing(_, _, _) - | Expr::BinOps { .. } - | Expr::Defs(_, _) - | Expr::If(_, _) - | Expr::When(_, _) - | Expr::Expect(_, _) - | Expr::MalformedClosure - | Expr::PrecedenceConflict { .. } - | Expr::RecordUpdate { .. } - | Expr::UnaryOp(_, _) => Err(()), - - Expr::Str(string) => Ok(Pattern::StrLiteral(*string)), - Expr::SingleQuote(string) => Ok(Pattern::SingleQuote(*string)), - Expr::MalformedIdent(string, _problem) => Ok(Pattern::Malformed(string)), - } -} - -fn assigned_expr_field_to_pattern_help<'a>( - arena: &'a Bump, - assigned_field: &AssignedField<'a, Expr<'a>>, -) -> Result, ()> { - // the assigned fields always store spaces, but this slice is often empty - Ok(match assigned_field { - AssignedField::RequiredValue(name, spaces, value) => { - let pattern = expr_to_pattern_help(arena, &value.value)?; - let result = arena.alloc(Loc { - region: value.region, - value: pattern, - }); - if spaces.is_empty() { - Pattern::RequiredField(name.value, result) - } else { - Pattern::SpaceAfter( - arena.alloc(Pattern::RequiredField(name.value, result)), - spaces, - ) - } - } - AssignedField::OptionalValue(name, spaces, value) => { - let result = arena.alloc(Loc { - region: value.region, - value: value.value, - }); - if spaces.is_empty() { - Pattern::OptionalField(name.value, result) - } else { - Pattern::SpaceAfter( - arena.alloc(Pattern::OptionalField(name.value, result)), - spaces, - ) - } - } - AssignedField::LabelOnly(name) => Pattern::Identifier(name.value), - AssignedField::SpaceBefore(nested, spaces) => Pattern::SpaceBefore( - arena.alloc(assigned_expr_field_to_pattern_help(arena, nested)?), - spaces, - ), - AssignedField::SpaceAfter(nested, spaces) => Pattern::SpaceAfter( - arena.alloc(assigned_expr_field_to_pattern_help(arena, nested)?), - spaces, - ), - AssignedField::Malformed(string) => Pattern::Malformed(string), - }) -} - -pub fn toplevel_defs<'a>(min_indent: u32) -> impl Parser<'a, Defs<'a>, EExpr<'a>> { - move |arena, state: State<'a>| { - let (_, initial_space, state) = - space0_e(min_indent, EExpr::IndentEnd).parse(arena, state)?; - - let start_column = state.column(); - - let options = ExprParseOptions { - accept_multi_backpassing: false, - check_for_arrow: true, - }; - - let mut output = Defs::default(); - let before = Slice::extend_new(&mut output.spaces, initial_space.iter().copied()); - - let (_, mut output, state) = - parse_toplevel_defs_end(options, start_column, output, arena, state)?; - - let (_, final_space, state) = - space0_e(start_column, EExpr::IndentEnd).parse(arena, state)?; - - if !output.tags.is_empty() { - // add surrounding whitespace - let after = Slice::extend_new(&mut output.spaces, final_space.iter().copied()); - - debug_assert!(output.space_before[0].is_empty()); - output.space_before[0] = before; - - let last = output.tags.len() - 1; - debug_assert!(output.space_after[last].is_empty() || after.is_empty()); - output.space_after[last] = after; - } - - Ok((MadeProgress, output, state)) - } -} - -pub fn defs<'a>(min_indent: u32) -> impl Parser<'a, Vec<'a, Loc>>, EExpr<'a>> { - move |arena, state: State<'a>| { - let def_state = DefState { - defs: Vec::new_in(arena), - spaces_after: &[], - }; - - let (_, initial_space, state) = - space0_e(min_indent, EExpr::IndentEnd).parse(arena, state)?; - - let start_column = state.column(); - - let options = ExprParseOptions { - accept_multi_backpassing: false, - check_for_arrow: true, - }; - - let (_, def_state, state) = parse_defs_end(options, start_column, def_state, arena, state)?; - - let (_, final_space, state) = - space0_e(start_column, EExpr::IndentEnd).parse(arena, state)?; - - let mut output = Vec::with_capacity_in(def_state.defs.len(), arena); - - if !def_state.defs.is_empty() { - let first = 0; - let last = def_state.defs.len() - 1; - - for (i, ref_def) in def_state.defs.into_iter().enumerate() { - let mut def = *ref_def; - - if i == first { - def = arena - .alloc(def.value) - .with_spaces_before(initial_space, def.region) - } - - if i == last { - def = arena - .alloc(def.value) - .with_spaces_after(final_space, def.region) - } - - output.push(def); - } - } - - Ok((MadeProgress, output, state)) - } -} - -// PARSER HELPERS - -fn closure_help<'a>( - min_indent: u32, - options: ExprParseOptions, -) -> impl Parser<'a, Expr<'a>, ELambda<'a>> { - map_with_arena!( - skip_first!( - // All closures start with a '\' - e.g. (\x -> x + 1) - word1(b'\\', ELambda::Start), - // Once we see the '\', we're committed to parsing this as a closure. - // It may turn out to be malformed, but it is definitely a closure. - and!( - // Parse the params - // Params are comma-separated - sep_by1_e( - word1(b',', ELambda::Comma), - space0_around_ee( - specialize(ELambda::Pattern, loc_closure_param(min_indent)), - min_indent, - ELambda::IndentArg, - ELambda::IndentArrow - ), - ELambda::Arg, - ), - skip_first!( - // Parse the -> which separates params from body - word2(b'-', b'>', ELambda::Arrow), - // Parse the body - space0_before_e( - specialize_ref(ELambda::Body, move |arena, state| { - parse_loc_expr_with_options(min_indent, options, arena, state) - }), - min_indent, - ELambda::IndentBody - ) - ) - ) - ), - |arena: &'a Bump, (params, loc_body)| { - let params: Vec<'a, Loc>> = params; - let params: &'a [Loc>] = params.into_bump_slice(); - - Expr::Closure(params, arena.alloc(loc_body)) - } - ) -} - -mod when { - use super::*; - use crate::ast::WhenBranch; - - /// Parser for when expressions. - pub fn expr_help<'a>( - min_indent: u32, - options: ExprParseOptions, - ) -> impl Parser<'a, Expr<'a>, EWhen<'a>> { - then( - and!( - when_with_indent(), - skip_second!( - space0_around_ee( - specialize_ref(EWhen::Condition, move |arena, state| { - parse_loc_expr_with_options(min_indent, options, arena, state) - }), - min_indent, - EWhen::IndentCondition, - EWhen::IndentIs, - ), - parser::keyword_e(keyword::IS, EWhen::Is) - ) - ), - move |arena, state, progress, (case_indent, loc_condition)| { - if case_indent < min_indent { - return Err(( - progress, - // TODO maybe pass case_indent here? - EWhen::PatternAlignment(5, state.pos()), - state, - )); - } - - // Everything in the branches must be indented at least as much as the case itself. - let min_indent = case_indent; - - let (p1, branches, state) = branches(min_indent, options).parse(arena, state)?; - - Ok(( - progress.or(p1), - Expr::When(arena.alloc(loc_condition), branches.into_bump_slice()), - state, - )) - }, - ) - } - - /// Parsing when with indentation. - fn when_with_indent<'a>() -> impl Parser<'a, u32, EWhen<'a>> { - move |arena, state: State<'a>| { - parser::keyword_e(keyword::WHEN, EWhen::When) - .parse(arena, state) - .map(|(progress, (), state)| (progress, state.indent_column, state)) - } - } - - fn branches<'a>( - min_indent: u32, - options: ExprParseOptions, - ) -> impl Parser<'a, Vec<'a, &'a WhenBranch<'a>>, EWhen<'a>> { - move |arena, state: State<'a>| { - let when_indent = state.indent_column; - - let mut branches: Vec<'a, &'a WhenBranch<'a>> = Vec::with_capacity_in(2, arena); - - // 1. Parse the first branch and get its indentation level. (It must be >= min_indent.) - // 2. Parse the other branches. Their indentation levels must be == the first branch's. - - let (_, ((pattern_indent_level, loc_first_patterns), loc_first_guard), mut state): ( - _, - ((_, _), _), - State<'a>, - ) = branch_alternatives(min_indent, options, None).parse(arena, state)?; - - let original_indent = pattern_indent_level; - - state.indent_column = pattern_indent_level; - - // Parse the first "->" and the expression after it. - let (_, loc_first_expr, mut state) = - branch_result(original_indent + 1).parse(arena, state)?; - - // Record this as the first branch, then optionally parse additional branches. - branches.push(arena.alloc(WhenBranch { - patterns: loc_first_patterns.into_bump_slice(), - value: loc_first_expr, - guard: loc_first_guard, - })); - - let branch_parser = map!( - and!( - then( - branch_alternatives(min_indent, options, Some(pattern_indent_level)), - move |_arena, state, _, ((indent_column, loc_patterns), loc_guard)| { - if pattern_indent_level == indent_column { - Ok((MadeProgress, (loc_patterns, loc_guard), state)) - } else { - let indent = pattern_indent_level - indent_column; - Err(( - MadeProgress, - EWhen::PatternAlignment(indent, state.pos()), - state, - )) - } - }, - ), - branch_result(original_indent + 1) - ), - |((patterns, guard), expr)| { - let patterns: Vec<'a, _> = patterns; - WhenBranch { - patterns: patterns.into_bump_slice(), - value: expr, - guard, - } - } - ); - - while !state.bytes().is_empty() { - match branch_parser.parse(arena, state) { - Ok((_, next_output, next_state)) => { - state = next_state; - - branches.push(arena.alloc(next_output)); - } - Err((MadeProgress, problem, old_state)) => { - return Err((MadeProgress, problem, old_state)); - } - Err((NoProgress, _, old_state)) => { - state = old_state; - - break; - } - } - } - - let mut state = state; - state.indent_column = when_indent; - - Ok((MadeProgress, branches, state)) - } - } - - /// Parsing alternative patterns in when branches. - fn branch_alternatives<'a>( - min_indent: u32, - options: ExprParseOptions, - pattern_indent_level: Option, - ) -> impl Parser<'a, ((u32, Vec<'a, Loc>>), Option>>), EWhen<'a>> { - let options = ExprParseOptions { - check_for_arrow: false, - ..options - }; - and!( - branch_alternatives_help(min_indent, pattern_indent_level), - one_of![ - map!( - skip_first!( - parser::keyword_e(keyword::IF, EWhen::IfToken), - // TODO we should require space before the expression but not after - space0_around_ee( - specialize_ref(EWhen::IfGuard, move |arena, state| { - parse_loc_expr_with_options(min_indent + 1, options, arena, state) - }), - min_indent, - EWhen::IndentIfGuard, - EWhen::IndentArrow, - ) - ), - Some - ), - |_, s| Ok((NoProgress, None, s)) - ] - ) - } - - fn branch_single_alternative<'a>( - min_indent: u32, - ) -> impl Parser<'a, Loc>, EWhen<'a>> { - move |arena, state| { - let (_, spaces, state) = - backtrackable(space0_e(min_indent, EWhen::IndentPattern)).parse(arena, state)?; - - let (_, loc_pattern, state) = space0_after_e( - specialize(EWhen::Pattern, crate::pattern::loc_pattern_help(min_indent)), - min_indent, - EWhen::IndentPattern, - ) - .parse(arena, state)?; - - Ok(( - MadeProgress, - if spaces.is_empty() { - loc_pattern - } else { - arena - .alloc(loc_pattern.value) - .with_spaces_before(spaces, loc_pattern.region) - }, - state, - )) - } - } - - fn branch_alternatives_help<'a>( - min_indent: u32, - pattern_indent_level: Option, - ) -> impl Parser<'a, (u32, Vec<'a, Loc>>), EWhen<'a>> { - move |arena, state: State<'a>| { - let initial = state.clone(); - - // put no restrictions on the indent after the spaces; we'll check it manually - match space0_e(0, EWhen::IndentPattern).parse(arena, state) { - Err((MadeProgress, fail, _)) => Err((NoProgress, fail, initial)), - Err((NoProgress, fail, _)) => Err((NoProgress, fail, initial)), - Ok((_progress, spaces, state)) => { - match pattern_indent_level { - Some(wanted) if state.column() > wanted => { - // this branch is indented too much - Err((NoProgress, EWhen::IndentPattern(state.pos()), initial)) - } - Some(wanted) if state.column() < wanted => { - let indent = wanted - state.column(); - Err(( - NoProgress, - EWhen::PatternAlignment(indent, state.pos()), - initial, - )) - } - _ => { - let pattern_indent = - min_indent.max(pattern_indent_level.unwrap_or(min_indent)); - // the region is not reliable for the indent column in the case of - // parentheses around patterns - let pattern_indent_column = state.column(); - - let parser = sep_by1( - word1(b'|', EWhen::Bar), - branch_single_alternative(pattern_indent + 1), - ); - - match parser.parse(arena, state) { - Err((MadeProgress, fail, state)) => { - Err((MadeProgress, fail, state)) - } - Err((NoProgress, fail, _)) => { - // roll back space parsing if the pattern made no progress - Err((NoProgress, fail, initial)) - } - - Ok((_, mut loc_patterns, state)) => { - // tag spaces onto the first parsed pattern - if !spaces.is_empty() { - if let Some(first) = loc_patterns.get_mut(0) { - *first = arena - .alloc(first.value) - .with_spaces_before(spaces, first.region); - } - } - - Ok((MadeProgress, (pattern_indent_column, loc_patterns), state)) - } - } - } - } - } - } - } - } - - /// Parsing the righthandside of a branch in a when conditional. - fn branch_result<'a>(indent: u32) -> impl Parser<'a, Loc>, EWhen<'a>> { - skip_first!( - word2(b'-', b'>', EWhen::Arrow), - space0_before_e( - specialize_ref(EWhen::Branch, move |arena, state| parse_loc_expr( - indent, arena, state - )), - indent, - EWhen::IndentBranch, - ) - ) - } -} - -fn if_branch<'a>(min_indent: u32) -> impl Parser<'a, (Loc>, Loc>), EIf<'a>> { - move |arena, state| { - // NOTE: only parse spaces before the expression - let (_, cond, state) = space0_around_ee( - specialize_ref(EIf::Condition, move |arena, state| { - parse_loc_expr(min_indent, arena, state) - }), - min_indent, - EIf::IndentCondition, - EIf::IndentThenToken, - ) - .parse(arena, state) - .map_err(|(_, f, s)| (MadeProgress, f, s))?; - - let (_, _, state) = parser::keyword_e(keyword::THEN, EIf::Then) - .parse(arena, state) - .map_err(|(_, f, s)| (MadeProgress, f, s))?; - - let (_, then_branch, state) = space0_around_ee( - specialize_ref(EIf::ThenBranch, move |arena, state| { - parse_loc_expr(min_indent, arena, state) - }), - min_indent, - EIf::IndentThenBranch, - EIf::IndentElseToken, - ) - .parse(arena, state) - .map_err(|(_, f, s)| (MadeProgress, f, s))?; - - let (_, _, state) = parser::keyword_e(keyword::ELSE, EIf::Else) - .parse(arena, state) - .map_err(|(_, f, s)| (MadeProgress, f, s))?; - - Ok((MadeProgress, (cond, then_branch), state)) - } -} - -fn expect_help<'a>( - min_indent: u32, - options: ExprParseOptions, -) -> impl Parser<'a, Expr<'a>, EExpect<'a>> { - move |arena: &'a Bump, state: State<'a>| { - let start_column = state.column(); - - let (_, _, state) = - parser::keyword_e(keyword::EXPECT, EExpect::Expect).parse(arena, state)?; - - let (_, condition, state) = space0_before_e( - specialize_ref(EExpect::Condition, move |arena, state| { - parse_loc_expr_with_options(start_column + 1, options, arena, state) - }), - start_column + 1, - EExpect::IndentCondition, - ) - .parse(arena, state) - .map_err(|(_, f, s)| (MadeProgress, f, s))?; - - let parse_cont = specialize_ref( - EExpect::Continuation, - space0_before_e( - move |a, s| parse_loc_expr(min_indent, a, s), - min_indent, - EExpr::IndentEnd, - ), - ); - - let (_, loc_cont, state) = parse_cont.parse(arena, state)?; - - let expr = Expr::Expect(arena.alloc(condition), arena.alloc(loc_cont)); - - Ok((MadeProgress, expr, state)) - } -} - -fn if_expr_help<'a>( - min_indent: u32, - options: ExprParseOptions, -) -> impl Parser<'a, Expr<'a>, EIf<'a>> { - move |arena: &'a Bump, state| { - let (_, _, state) = parser::keyword_e(keyword::IF, EIf::If).parse(arena, state)?; - - let mut branches = Vec::with_capacity_in(1, arena); - - let mut loop_state = state; - - let state_final_else = loop { - let (_, (cond, then_branch), state) = if_branch(min_indent).parse(arena, loop_state)?; - - branches.push((cond, then_branch)); - - // try to parse another `if` - // NOTE this drops spaces between the `else` and the `if` - let optional_if = and!( - backtrackable(space0_e(min_indent, EIf::IndentIf)), - parser::keyword_e(keyword::IF, EIf::If) - ); - - match optional_if.parse(arena, state) { - Err((_, _, state)) => break state, - Ok((_, _, state)) => { - loop_state = state; - continue; - } - } - }; - - let (_, else_branch, state) = space0_before_e( - specialize_ref(EIf::ElseBranch, move |arena, state| { - parse_loc_expr_with_options(min_indent, options, arena, state) - }), - min_indent, - EIf::IndentElseBranch, - ) - .parse(arena, state_final_else) - .map_err(|(_, f, s)| (MadeProgress, f, s))?; - - let expr = Expr::If(branches.into_bump_slice(), arena.alloc(else_branch)); - - Ok((MadeProgress, expr, state)) - } -} - -/// This is a helper function for parsing function args. -/// The rules for (-) are special-cased, and they come up in function args. -/// -/// They work like this: -/// -/// x - y # "x minus y" -/// x-y # "x minus y" -/// x- y # "x minus y" (probably written in a rush) -/// x -y # "call x, passing (-y)" -/// -/// Since operators have higher precedence than function application, -/// any time we encounter a '-' it is unary iff it is both preceded by spaces -/// and is *not* followed by a whitespace character. - -/// When we parse an ident like `foo ` it could be any of these: -/// -/// 1. A standalone variable with trailing whitespace (e.g. because an operator is next) -/// 2. The beginning of a function call (e.g. `foo bar baz`) -/// 3. The beginning of a definition (e.g. `foo =`) -/// 4. The beginning of a type annotation (e.g. `foo :`) -/// 5. A reserved keyword (e.g. `if ` or `case `), meaning we should do something else. - -fn assign_or_destructure_identifier<'a>() -> impl Parser<'a, Ident<'a>, EExpr<'a>> { - crate::ident::parse_ident -} - -#[allow(dead_code)] -fn with_indent<'a, E, T, P>(parser: P) -> impl Parser<'a, u32, E> -where - P: Parser<'a, T, E>, - E: 'a, -{ - move |arena, state: State<'a>| { - let indent_column = state.indent_column; - - let (progress, _, state) = parser.parse(arena, state)?; - - Ok((progress, indent_column, state)) - } -} - -fn ident_to_expr<'a>(arena: &'a Bump, src: Ident<'a>) -> Expr<'a> { - match src { - Ident::Tag(string) => Expr::Tag(string), - Ident::OpaqueRef(string) => Expr::OpaqueRef(string), - Ident::Access { module_name, parts } => { - let mut iter = parts.iter(); - - // The first value in the iterator is the variable name, - // e.g. `foo` in `foo.bar.baz` - let mut answer = match iter.next() { - Some(ident) => Expr::Var { module_name, ident }, - None => { - panic!("Parsed an Ident::Access with no parts"); - } - }; - - // The remaining items in the iterator are record field accesses, - // e.g. `bar` in `foo.bar.baz`, followed by `baz` - for field in iter { - // Wrap the previous answer in the new one, so we end up - // with a nested Expr. That way, `foo.bar.baz` gets represented - // in the AST as if it had been written (foo.bar).baz all along. - answer = Expr::Access(arena.alloc(answer), field); - } - - answer - } - Ident::AccessorFunction(string) => Expr::AccessorFunction(string), - Ident::Malformed(string, problem) => Expr::MalformedIdent(string, problem), - } -} - -fn list_literal_help<'a>(min_indent: u32) -> impl Parser<'a, Expr<'a>, EList<'a>> { - move |arena, state| { - let (_, elements, state) = collection_trailing_sep_e!( - word1(b'[', EList::Open), - specialize_ref( - EList::Expr, - move |a, s| parse_loc_expr_no_multi_backpassing(min_indent, a, s) - ), - word1(b',', EList::End), - word1(b']', EList::End), - min_indent, - EList::Open, - EList::IndentEnd, - Expr::SpaceBefore - ) - .parse(arena, state)?; - - let elements = elements.ptrify_items(arena); - let expr = Expr::List(elements); - - Ok((MadeProgress, expr, state)) - } -} - -fn record_field_help<'a>( - min_indent: u32, -) -> impl Parser<'a, AssignedField<'a, Expr<'a>>, ERecord<'a>> { - use AssignedField::*; - - move |arena, state: State<'a>| { - // You must have a field name, e.g. "email" - let (progress, loc_label, state) = - specialize(|_, pos| ERecord::Field(pos), loc!(lowercase_ident())) - .parse(arena, state)?; - debug_assert_eq!(progress, MadeProgress); - - let (_, spaces, state) = space0_e(min_indent, ERecord::IndentColon).parse(arena, state)?; - - // Having a value is optional; both `{ email }` and `{ email: blah }` work. - // (This is true in both literals and types.) - let (_, opt_loc_val, state) = optional(and!( - either!( - word1(b':', ERecord::Colon), - word1(b'?', ERecord::QuestionMark) - ), - space0_before_e( - specialize_ref(ERecord::Expr, move |a, s| { - parse_loc_expr_no_multi_backpassing(min_indent, a, s) - }), - min_indent, - ERecord::IndentEnd, - ) - )) - .parse(arena, state)?; - - let answer = match opt_loc_val { - Some((Either::First(_), loc_val)) => { - RequiredValue(loc_label, spaces, arena.alloc(loc_val)) - } - - Some((Either::Second(_), loc_val)) => { - OptionalValue(loc_label, spaces, arena.alloc(loc_val)) - } - - // If no value was provided, record it as a Var. - // Canonicalize will know what to do with a Var later. - None => { - if !spaces.is_empty() { - SpaceAfter(arena.alloc(LabelOnly(loc_label)), spaces) - } else { - LabelOnly(loc_label) - } - } - }; - - Ok((MadeProgress, answer, state)) - } -} - -fn record_updateable_identifier<'a>() -> impl Parser<'a, Expr<'a>, ERecord<'a>> { - specialize( - |_, pos| ERecord::Updateable(pos), - map_with_arena!(parse_ident, ident_to_expr), - ) -} - -fn record_help<'a>( - min_indent: u32, -) -> impl Parser< - 'a, - ( - Option>>, - Loc<( - Vec<'a, Loc>>>, - &'a [CommentOrNewline<'a>], - )>, - ), - ERecord<'a>, -> { - skip_first!( - word1(b'{', ERecord::Open), - and!( - // You can optionally have an identifier followed by an '&' to - // make this a record update, e.g. { Foo.user & username: "blah" }. - optional(skip_second!( - space0_around_ee( - // We wrap the ident in an Expr here, - // so that we have a Spaceable value to work with, - // and then in canonicalization verify that it's an Expr::Var - // (and not e.g. an `Expr::Access`) and extract its string. - loc!(record_updateable_identifier()), - min_indent, - ERecord::IndentEnd, - ERecord::IndentAmpersand, - ), - word1(b'&', ERecord::Ampersand) - )), - loc!(skip_first!( - // We specifically allow space characters inside here, so that - // `{ }` can be successfully parsed as an empty record, and then - // changed by the formatter back into `{}`. - zero_or_more!(word1(b' ', ERecord::End)), - skip_second!( - and!( - trailing_sep_by0( - word1(b',', ERecord::End), - space0_before_optional_after( - loc!(record_field_help(min_indent)), - min_indent, - ERecord::IndentEnd, - ERecord::IndentEnd - ), - ), - // Allow outdented closing braces - space0_e(0, ERecord::IndentEnd) - ), - word1(b'}', ERecord::End) - ) - )) - ) - ) -} - -fn record_literal_help<'a>(min_indent: u32) -> impl Parser<'a, Expr<'a>, EExpr<'a>> { - then( - loc!(specialize(EExpr::Record, record_help(min_indent))), - move |arena, state, _, loc_record| { - let (opt_update, loc_assigned_fields_with_comments) = loc_record.value; - - // This is a record literal, not a destructure. - let mut value = match opt_update { - Some(update) => Expr::RecordUpdate { - update: &*arena.alloc(update), - fields: Collection::with_items_and_comments( - arena, - loc_assigned_fields_with_comments.value.0.into_bump_slice(), - arena.alloc(loc_assigned_fields_with_comments.value.1), - ), - }, - None => Expr::Record(Collection::with_items_and_comments( - arena, - loc_assigned_fields_with_comments.value.0.into_bump_slice(), - loc_assigned_fields_with_comments.value.1, - )), - }; - - // there can be field access, e.g. `{ x : 4 }.x` - let (_, accesses, state) = optional(record_field_access_chain()).parse(arena, state)?; - - if let Some(fields) = accesses { - for field in fields { - // Wrap the previous answer in the new one, so we end up - // with a nested Expr. That way, `foo.bar.baz` gets represented - // in the AST as if it had been written (foo.bar).baz all along. - value = Expr::Access(arena.alloc(value), field); - } - } - - Ok((MadeProgress, value, state)) - }, - ) -} - -fn string_literal_help<'a>() -> impl Parser<'a, Expr<'a>, EString<'a>> { - map!(crate::string_literal::parse(), Expr::Str) -} - -fn single_quote_literal_help<'a>() -> impl Parser<'a, Expr<'a>, EString<'a>> { - map!( - crate::string_literal::parse_single_quote(), - Expr::SingleQuote - ) -} - -fn positive_number_literal_help<'a>() -> impl Parser<'a, Expr<'a>, ENumber> { - map!( - crate::number_literal::positive_number_literal(), - |literal| { - use crate::number_literal::NumLiteral::*; - - match literal { - Num(s) => Expr::Num(s), - Float(s) => Expr::Float(s), - NonBase10Int { - string, - base, - is_negative, - } => Expr::NonBase10Int { - string, - base, - is_negative, - }, - } - } - ) -} - -fn number_literal_help<'a>() -> impl Parser<'a, Expr<'a>, ENumber> { - map!(crate::number_literal::number_literal(), |literal| { - use crate::number_literal::NumLiteral::*; - - match literal { - Num(s) => Expr::Num(s), - Float(s) => Expr::Float(s), - NonBase10Int { - string, - base, - is_negative, - } => Expr::NonBase10Int { - string, - base, - is_negative, - }, - } - }) -} - -const BINOP_CHAR_SET: &[u8] = b"+-/*=.<>:&|^?%!"; - -const BINOP_CHAR_MASK: [bool; 125] = { - let mut result = [false; 125]; - - let mut i = 0; - while i < BINOP_CHAR_SET.len() { - let index = BINOP_CHAR_SET[i] as usize; - - result[index] = true; - - i += 1; - } - - result -}; - -fn operator<'a>() -> impl Parser<'a, BinOp, EExpr<'a>> { - |_, state| operator_help(EExpr::Start, EExpr::BadOperator, state) -} - -#[inline(always)] -fn operator_help<'a, F, G, E>( - to_expectation: F, - to_error: G, - mut state: State<'a>, -) -> ParseResult<'a, BinOp, E> -where - F: Fn(Position) -> E, - G: Fn(&'a str, Position) -> E, - E: 'a, -{ - let chomped = chomp_ops(state.bytes()); - - macro_rules! good { - ($op:expr, $width:expr) => {{ - state = state.advance($width); - - Ok((MadeProgress, $op, state)) - }}; - } - - macro_rules! bad_made_progress { - ($op:expr) => {{ - Err((MadeProgress, to_error($op, state.pos()), state)) - }}; - } - - match chomped { - "" => Err((NoProgress, to_expectation(state.pos()), state)), - "+" => good!(BinOp::Plus, 1), - "-" => good!(BinOp::Minus, 1), - "*" => good!(BinOp::Star, 1), - "/" => good!(BinOp::Slash, 1), - "%" => good!(BinOp::Percent, 1), - "^" => good!(BinOp::Caret, 1), - ">" => good!(BinOp::GreaterThan, 1), - "<" => good!(BinOp::LessThan, 1), - "." => { - // a `.` makes no progress, so it does not interfere with `.foo` access(or) - Err((NoProgress, to_error(".", state.pos()), state)) - } - "=" => good!(BinOp::Assignment, 1), - ":=" => good!(BinOp::IsOpaqueType, 2), - ":" => good!(BinOp::IsAliasType, 1), - "|>" => good!(BinOp::Pizza, 2), - "==" => good!(BinOp::Equals, 2), - "!=" => good!(BinOp::NotEquals, 2), - ">=" => good!(BinOp::GreaterThanOrEq, 2), - "<=" => good!(BinOp::LessThanOrEq, 2), - "&&" => good!(BinOp::And, 2), - "||" => good!(BinOp::Or, 2), - "//" => good!(BinOp::DoubleSlash, 2), - "->" => { - // makes no progress, so it does not interfere with `_ if isGood -> ...` - Err((NoProgress, to_error("->", state.pos()), state)) - } - "<-" => good!(BinOp::Backpassing, 2), - _ => bad_made_progress!(chomped), - } -} - -fn chomp_ops(bytes: &[u8]) -> &str { - let mut chomped = 0; - - for c in bytes.iter() { - if let Some(true) = BINOP_CHAR_MASK.get(*c as usize) { - chomped += 1; - } else { - break; - } - } - - unsafe { - // Safe because BINOP_CHAR_SET only contains ascii chars - std::str::from_utf8_unchecked(&bytes[..chomped]) - } -} diff --git a/compiler/parse/src/header.rs b/compiler/parse/src/header.rs deleted file mode 100644 index fcd534e0d6..0000000000 --- a/compiler/parse/src/header.rs +++ /dev/null @@ -1,298 +0,0 @@ -use crate::ast::{Collection, CommentOrNewline, Spaced, StrLiteral, TypeAnnotation}; -use crate::blankspace::space0_e; -use crate::ident::{lowercase_ident, UppercaseIdent}; -use crate::parser::Progress::*; -use crate::parser::{specialize, word1, EPackageEntry, EPackageName, Parser}; -use crate::state::State; -use crate::string_literal; -use bumpalo::collections::Vec; -use roc_module::symbol::Symbol; -use roc_region::all::Loc; - -#[derive(Debug)] -pub enum HeaderFor<'a> { - App { - to_platform: To<'a>, - }, - Hosted { - generates: UppercaseIdent<'a>, - generates_with: &'a [Loc>], - }, - /// Only created during canonicalization, never actually parsed from source - Builtin { - generates_with: &'a [Symbol], - }, - PkgConfig { - /// usually `pf` - config_shorthand: &'a str, - /// the type scheme of the main function (required by the platform) - /// (currently unused) - #[allow(dead_code)] - platform_main_type: TypedIdent<'a>, - /// provided symbol to host (commonly `mainForHost`) - main_for_host: roc_module::symbol::Symbol, - }, - Interface, -} - -#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash)] -pub enum Version<'a> { - Exact(&'a str), - Range { - min: &'a str, - min_comparison: VersionComparison, - max: &'a str, - max_comparison: VersionComparison, - }, -} - -#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash)] -pub enum VersionComparison { - AllowsEqual, - DisallowsEqual, -} - -#[derive(Copy, Clone, PartialEq, Debug)] -pub struct PackageName<'a>(pub &'a str); - -#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash)] -pub struct ModuleName<'a>(&'a str); - -impl<'a> From> for &'a str { - fn from(name: ModuleName<'a>) -> Self { - name.0 - } -} - -impl<'a> ModuleName<'a> { - pub const fn new(name: &'a str) -> Self { - ModuleName(name) - } - - pub const fn as_str(&'a self) -> &'a str { - self.0 - } -} - -#[derive(Debug)] -pub enum ModuleNameEnum<'a> { - /// A filename - App(StrLiteral<'a>), - Interface(ModuleName<'a>), - Hosted(ModuleName<'a>), - PkgConfig, -} - -#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash)] -pub struct ExposedName<'a>(&'a str); - -impl<'a> From> for &'a str { - fn from(name: ExposedName<'a>) -> Self { - name.0 - } -} - -impl<'a> ExposedName<'a> { - pub const fn new(name: &'a str) -> Self { - ExposedName(name) - } - - pub fn as_str(&'a self) -> &'a str { - self.0 - } -} - -#[derive(Clone, Debug, PartialEq)] -pub struct InterfaceHeader<'a> { - pub name: Loc>, - pub exposes: Collection<'a, Loc>>>, - pub imports: Collection<'a, Loc>>>, - - // Potential comments and newlines - these will typically all be empty. - pub before_header: &'a [CommentOrNewline<'a>], - pub after_interface_keyword: &'a [CommentOrNewline<'a>], - pub before_exposes: &'a [CommentOrNewline<'a>], - pub after_exposes: &'a [CommentOrNewline<'a>], - pub before_imports: &'a [CommentOrNewline<'a>], - pub after_imports: &'a [CommentOrNewline<'a>], -} - -#[derive(Clone, Debug, PartialEq)] -pub struct HostedHeader<'a> { - pub name: Loc>, - pub exposes: Collection<'a, Loc>>>, - pub imports: Collection<'a, Loc>>>, - pub generates: UppercaseIdent<'a>, - pub generates_with: Collection<'a, Loc>>>, - - // Potential comments and newlines - these will typically all be empty. - pub before_header: &'a [CommentOrNewline<'a>], - pub after_hosted_keyword: &'a [CommentOrNewline<'a>], - pub before_exposes: &'a [CommentOrNewline<'a>], - pub after_exposes: &'a [CommentOrNewline<'a>], - pub before_imports: &'a [CommentOrNewline<'a>], - pub after_imports: &'a [CommentOrNewline<'a>], - pub before_generates: &'a [CommentOrNewline<'a>], - pub after_generates: &'a [CommentOrNewline<'a>], - pub before_with: &'a [CommentOrNewline<'a>], - pub after_with: &'a [CommentOrNewline<'a>], -} - -#[derive(Copy, Clone, Debug, PartialEq)] -pub enum To<'a> { - ExistingPackage(&'a str), - NewPackage(PackageName<'a>), -} - -#[derive(Clone, Debug, PartialEq)] -pub struct AppHeader<'a> { - pub name: Loc>, - pub packages: Collection<'a, Loc>>>, - pub imports: Collection<'a, Loc>>>, - pub provides: Collection<'a, Loc>>>, - pub provides_types: Option>>>>, - pub to: Loc>, - - // Potential comments and newlines - these will typically all be empty. - pub before_header: &'a [CommentOrNewline<'a>], - pub after_app_keyword: &'a [CommentOrNewline<'a>], - pub before_packages: &'a [CommentOrNewline<'a>], - pub after_packages: &'a [CommentOrNewline<'a>], - pub before_imports: &'a [CommentOrNewline<'a>], - pub after_imports: &'a [CommentOrNewline<'a>], - pub before_provides: &'a [CommentOrNewline<'a>], - pub after_provides: &'a [CommentOrNewline<'a>], - pub before_to: &'a [CommentOrNewline<'a>], - pub after_to: &'a [CommentOrNewline<'a>], -} - -#[derive(Clone, Debug, PartialEq)] -pub struct PackageHeader<'a> { - pub name: Loc>, - pub exposes: Vec<'a, Loc>>>, - pub packages: Vec<'a, (Loc<&'a str>, Loc>)>, - pub imports: Vec<'a, Loc>>, - - // Potential comments and newlines - these will typically all be empty. - pub before_header: &'a [CommentOrNewline<'a>], - pub after_package_keyword: &'a [CommentOrNewline<'a>], - pub before_exposes: &'a [CommentOrNewline<'a>], - pub after_exposes: &'a [CommentOrNewline<'a>], - pub before_packages: &'a [CommentOrNewline<'a>], - pub after_packages: &'a [CommentOrNewline<'a>], - pub before_imports: &'a [CommentOrNewline<'a>], - pub after_imports: &'a [CommentOrNewline<'a>], -} - -#[derive(Clone, Debug, PartialEq)] -pub struct PlatformRequires<'a> { - pub rigids: Collection<'a, Loc>>>, - pub signature: Loc>>, -} - -#[derive(Clone, Debug, PartialEq)] -pub struct PlatformHeader<'a> { - pub name: Loc>, - pub requires: PlatformRequires<'a>, - pub exposes: Collection<'a, Loc>>>, - pub packages: Collection<'a, Loc>>>, - pub imports: Collection<'a, Loc>>>, - pub provides: Collection<'a, Loc>>>, - - // Potential comments and newlines - these will typically all be empty. - pub before_header: &'a [CommentOrNewline<'a>], - pub after_platform_keyword: &'a [CommentOrNewline<'a>], - pub before_requires: &'a [CommentOrNewline<'a>], - pub after_requires: &'a [CommentOrNewline<'a>], - pub before_exposes: &'a [CommentOrNewline<'a>], - pub after_exposes: &'a [CommentOrNewline<'a>], - pub before_packages: &'a [CommentOrNewline<'a>], - pub after_packages: &'a [CommentOrNewline<'a>], - pub before_imports: &'a [CommentOrNewline<'a>], - pub after_imports: &'a [CommentOrNewline<'a>], - pub before_provides: &'a [CommentOrNewline<'a>], - pub after_provides: &'a [CommentOrNewline<'a>], -} - -#[derive(Copy, Clone, Debug, PartialEq)] -pub enum ImportsEntry<'a> { - /// e.g. `Task` or `Task.{ Task, after }` - Module( - ModuleName<'a>, - Collection<'a, Loc>>>, - ), - - /// e.g. `pf.Task` or `pf.Task.{ after }` or `pf.{ Task.{ Task, after } }` - Package( - &'a str, - ModuleName<'a>, - Collection<'a, Loc>>>, - ), -} - -/// e.g. -/// -/// printLine : Str -> Effect {} -#[derive(Copy, Clone, Debug, PartialEq)] -pub struct TypedIdent<'a> { - pub ident: Loc<&'a str>, - pub spaces_before_colon: &'a [CommentOrNewline<'a>], - pub ann: Loc>, -} - -#[derive(Copy, Clone, Debug, PartialEq)] -pub struct PackageEntry<'a> { - pub shorthand: &'a str, - pub spaces_after_shorthand: &'a [CommentOrNewline<'a>], - pub package_name: Loc>, -} - -pub fn package_entry<'a>() -> impl Parser<'a, Spaced<'a, PackageEntry<'a>>, EPackageEntry<'a>> { - move |arena, state| { - // You may optionally have a package shorthand, - // e.g. "uc" in `uc: roc/unicode 1.0.0` - // - // (Indirect dependencies don't have a shorthand.) - let min_indent = 1; - - let (_, opt_shorthand, state) = maybe!(and!( - skip_second!( - specialize(|_, pos| EPackageEntry::Shorthand(pos), lowercase_ident()), - word1(b':', EPackageEntry::Colon) - ), - space0_e(min_indent, EPackageEntry::IndentPackage) - )) - .parse(arena, state)?; - - let (_, package_or_path, state) = - loc!(specialize(EPackageEntry::BadPackage, package_name())).parse(arena, state)?; - - let entry = match opt_shorthand { - Some((shorthand, spaces_after_shorthand)) => PackageEntry { - shorthand, - spaces_after_shorthand, - package_name: package_or_path, - }, - None => PackageEntry { - shorthand: "", - spaces_after_shorthand: &[], - package_name: package_or_path, - }, - }; - - Ok((MadeProgress, Spaced::Item(entry), state)) - } -} - -pub fn package_name<'a>() -> impl Parser<'a, PackageName<'a>, EPackageName<'a>> { - move |arena, state: State<'a>| { - let pos = state.pos(); - specialize(EPackageName::BadPath, string_literal::parse()) - .parse(arena, state) - .and_then(|(progress, text, next_state)| match text { - StrLiteral::PlainLine(text) => Ok((progress, PackageName(text), next_state)), - StrLiteral::Line(_) => Err((progress, EPackageName::Escapes(pos), next_state)), - StrLiteral::Block(_) => Err((progress, EPackageName::Multiline(pos), next_state)), - }) - } -} diff --git a/compiler/parse/src/ident.rs b/compiler/parse/src/ident.rs deleted file mode 100644 index 131cf4c465..0000000000 --- a/compiler/parse/src/ident.rs +++ /dev/null @@ -1,546 +0,0 @@ -use crate::parser::Progress::{self, *}; -use crate::parser::{BadInputError, EExpr, ParseResult, Parser}; -use crate::state::State; -use bumpalo::collections::vec::Vec; -use bumpalo::Bump; -use roc_region::all::Position; - -/// A tag, for example. Must start with an uppercase letter -/// and then contain only letters and numbers afterwards - no dots allowed! -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub struct UppercaseIdent<'a>(&'a str); - -impl<'a> From<&'a str> for UppercaseIdent<'a> { - fn from(string: &'a str) -> Self { - UppercaseIdent(string) - } -} - -impl<'a> From> for &'a str { - fn from(ident: UppercaseIdent<'a>) -> Self { - ident.0 - } -} - -impl<'a> From<&'a UppercaseIdent<'a>> for &'a str { - fn from(ident: &'a UppercaseIdent<'a>) -> Self { - ident.0 - } -} - -/// The parser accepts all of these in any position where any one of them could -/// appear. This way, canonicalization can give more helpful error messages like -/// "you can't redefine this tag!" if you wrote `Foo = ...` or -/// "you can only define unqualified constants" if you wrote `Foo.bar = ...` -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum Ident<'a> { - /// Foo or Bar - Tag(&'a str), - /// @Foo or @Bar - OpaqueRef(&'a str), - /// foo or foo.bar or Foo.Bar.baz.qux - Access { - module_name: &'a str, - parts: &'a [&'a str], - }, - /// .foo { foo: 42 } - AccessorFunction(&'a str), - /// .Foo or foo. or something like foo.Bar - Malformed(&'a str, BadIdent), -} - -impl<'a> Ident<'a> { - pub fn len(&self) -> usize { - use self::Ident::*; - - match self { - Tag(string) | OpaqueRef(string) => string.len(), - Access { module_name, parts } => { - let mut len = if module_name.is_empty() { - 0 - } else { - module_name.len() + 1 - // +1 for the dot - }; - - for part in parts.iter() { - len += part.len() + 1 // +1 for the dot - } - - len - 1 - } - AccessorFunction(string) => string.len(), - Malformed(string, _) => string.len(), - } - } - - pub fn is_empty(&self) -> bool { - self.len() == 0 - } -} - -/// This could be: -/// -/// * A record field, e.g. "email" in `.email` or in `email:` -/// * A named pattern match, e.g. "foo" in `foo =` or `foo ->` or `\foo ->` -pub fn lowercase_ident<'a>() -> impl Parser<'a, &'a str, ()> { - move |_, state: State<'a>| match chomp_lowercase_part(state.bytes()) { - Err(progress) => Err((progress, (), state)), - Ok(ident) => { - if crate::keyword::KEYWORDS.iter().any(|kw| &ident == kw) { - Err((NoProgress, (), state)) - } else { - let width = ident.len(); - Ok((MadeProgress, ident, state.advance(width))) - } - } - } -} - -pub fn tag_name<'a>() -> impl Parser<'a, &'a str, ()> { - move |arena, state: State<'a>| uppercase_ident().parse(arena, state) -} - -/// This could be: -/// -/// * A module name -/// * A type name -/// * A tag -pub fn uppercase<'a>() -> impl Parser<'a, UppercaseIdent<'a>, ()> { - move |_, state: State<'a>| match chomp_uppercase_part(state.bytes()) { - Err(progress) => Err((progress, (), state)), - Ok(ident) => { - let width = ident.len(); - Ok((MadeProgress, ident.into(), state.advance(width))) - } - } -} - -/// This could be: -/// -/// * A module name -/// * A type name -/// * A tag -pub fn uppercase_ident<'a>() -> impl Parser<'a, &'a str, ()> { - move |_, state: State<'a>| match chomp_uppercase_part(state.bytes()) { - Err(progress) => Err((progress, (), state)), - Ok(ident) => { - let width = ident.len(); - Ok((MadeProgress, ident, state.advance(width))) - } - } -} - -pub fn unqualified_ident<'a>() -> impl Parser<'a, &'a str, ()> { - move |_, state: State<'a>| match chomp_part(|c| c.is_alphabetic(), state.bytes()) { - Err(progress) => Err((progress, (), state)), - Ok(ident) => { - if crate::keyword::KEYWORDS.iter().any(|kw| &ident == kw) { - Err((MadeProgress, (), state)) - } else { - let width = ident.len(); - Ok((MadeProgress, ident, state.advance(width))) - } - } - } -} - -macro_rules! advance_state { - ($state:expr, $n:expr) => { - Ok($state.advance($n)) - }; -} - -pub fn parse_ident<'a>(arena: &'a Bump, state: State<'a>) -> ParseResult<'a, Ident<'a>, EExpr<'a>> { - let initial = state.clone(); - - match parse_ident_help(arena, state) { - Ok((progress, ident, state)) => { - if let Ident::Access { module_name, parts } = ident { - if module_name.is_empty() { - if let Some(first) = parts.first() { - for keyword in crate::keyword::KEYWORDS.iter() { - if first == keyword { - return Err((NoProgress, EExpr::Start(initial.pos()), initial)); - } - } - } - } - } - - Ok((progress, ident, state)) - } - Err((NoProgress, _, state)) => Err((NoProgress, EExpr::Start(state.pos()), state)), - Err((MadeProgress, fail, state)) => match fail { - BadIdent::Start(pos) => Err((NoProgress, EExpr::Start(pos), state)), - BadIdent::Space(e, pos) => Err((NoProgress, EExpr::Space(e, pos), state)), - _ => malformed_identifier(initial.bytes(), fail, state), - }, - } -} - -fn malformed_identifier<'a>( - initial_bytes: &'a [u8], - problem: BadIdent, - mut state: State<'a>, -) -> ParseResult<'a, Ident<'a>, EExpr<'a>> { - let chomped = chomp_malformed(state.bytes()); - let delta = initial_bytes.len() - state.bytes().len(); - let parsed_str = unsafe { std::str::from_utf8_unchecked(&initial_bytes[..chomped + delta]) }; - - state = state.advance(chomped); - - Ok((MadeProgress, Ident::Malformed(parsed_str, problem), state)) -} - -/// skip forward to the next non-identifier character -pub fn chomp_malformed(bytes: &[u8]) -> usize { - use encode_unicode::CharExt; - let mut chomped = 0; - while let Ok((ch, width)) = char::from_utf8_slice_start(&bytes[chomped..]) { - // We can't use ch.is_alphanumeric() here because that passes for - // things that are "numeric" but not ASCII digits, like `¾` - if ch == '.' || ch == '_' || ch.is_alphabetic() || ch.is_ascii_digit() { - chomped += width; - continue; - } else { - break; - } - } - - chomped -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum BadIdent { - Start(Position), - Space(BadInputError, Position), - - Underscore(Position), - QualifiedTag(Position), - WeirdAccessor(Position), - WeirdDotAccess(Position), - WeirdDotQualified(Position), - StrayDot(Position), - BadOpaqueRef(Position), -} - -fn chomp_lowercase_part(buffer: &[u8]) -> Result<&str, Progress> { - chomp_part(|c: char| c.is_lowercase(), buffer) -} - -fn chomp_uppercase_part(buffer: &[u8]) -> Result<&str, Progress> { - chomp_part(|c: char| c.is_uppercase(), buffer) -} - -#[inline(always)] -fn chomp_part(leading_is_good: F, buffer: &[u8]) -> Result<&str, Progress> -where - F: Fn(char) -> bool, -{ - use encode_unicode::CharExt; - - let mut chomped = 0; - - if let Ok((ch, width)) = char::from_utf8_slice_start(&buffer[chomped..]) { - if leading_is_good(ch) { - chomped += width; - } else { - return Err(NoProgress); - } - } - - while let Ok((ch, width)) = char::from_utf8_slice_start(&buffer[chomped..]) { - if ch.is_alphabetic() || ch.is_ascii_digit() { - chomped += width; - } else { - // we're done - break; - } - } - - if chomped == 0 { - Err(NoProgress) - } else { - let name = unsafe { std::str::from_utf8_unchecked(&buffer[..chomped]) }; - - Ok(name) - } -} - -/// a `.foo` accessor function -fn chomp_accessor(buffer: &[u8], pos: Position) -> Result<&str, BadIdent> { - // assumes the leading `.` has been chomped already - use encode_unicode::CharExt; - - match chomp_lowercase_part(buffer) { - Ok(name) => { - let chomped = name.len(); - - if let Ok(('.', _)) = char::from_utf8_slice_start(&buffer[chomped..]) { - Err(BadIdent::WeirdAccessor(pos)) - } else { - Ok(name) - } - } - Err(_) => { - // we've already made progress with the initial `.` - Err(BadIdent::StrayDot(pos.bump_column(1))) - } - } -} - -/// a `@Token` opaque -fn chomp_opaque_ref(buffer: &[u8], pos: Position) -> Result<&str, BadIdent> { - // assumes the leading `@` has NOT been chomped already - debug_assert_eq!(buffer.get(0), Some(&b'@')); - use encode_unicode::CharExt; - - let bad_ident = BadIdent::BadOpaqueRef; - - match chomp_uppercase_part(&buffer[1..]) { - Ok(name) => { - let width = 1 + name.len(); - - if let Ok(('.', _)) = char::from_utf8_slice_start(&buffer[width..]) { - Err(bad_ident(pos.bump_column(width as u32))) - } else { - let value = unsafe { std::str::from_utf8_unchecked(&buffer[..width]) }; - Ok(value) - } - } - Err(_) => Err(bad_ident(pos.bump_column(1))), - } -} - -fn chomp_identifier_chain<'a>( - arena: &'a Bump, - buffer: &'a [u8], - pos: Position, -) -> Result<(u32, Ident<'a>), (u32, BadIdent)> { - use encode_unicode::CharExt; - - let first_is_uppercase; - let mut chomped = 0; - - match char::from_utf8_slice_start(&buffer[chomped..]) { - Ok((ch, width)) => match ch { - '.' => match chomp_accessor(&buffer[1..], pos) { - Ok(accessor) => { - let bytes_parsed = 1 + accessor.len(); - - return Ok((bytes_parsed as u32, Ident::AccessorFunction(accessor))); - } - Err(fail) => return Err((1, fail)), - }, - '@' => match chomp_opaque_ref(buffer, pos) { - Ok(tagname) => { - let bytes_parsed = tagname.len(); - - let ident = Ident::OpaqueRef; - - return Ok((bytes_parsed as u32, ident(tagname))); - } - Err(fail) => return Err((1, fail)), - }, - c if c.is_alphabetic() => { - // fall through - chomped += width; - first_is_uppercase = c.is_uppercase(); - } - _ => { - return Err((0, BadIdent::Start(pos))); - } - }, - Err(_) => return Err((0, BadIdent::Start(pos))), - } - - while let Ok((ch, width)) = char::from_utf8_slice_start(&buffer[chomped..]) { - if ch.is_alphabetic() || ch.is_ascii_digit() { - chomped += width; - } else { - // we're done - break; - } - } - - if let Ok(('.', _)) = char::from_utf8_slice_start(&buffer[chomped..]) { - let module_name = if first_is_uppercase { - match chomp_module_chain(&buffer[chomped..]) { - Ok(width) => { - chomped += width as usize; - unsafe { std::str::from_utf8_unchecked(&buffer[..chomped]) } - } - Err(MadeProgress) => todo!(), - Err(NoProgress) => unsafe { std::str::from_utf8_unchecked(&buffer[..chomped]) }, - } - } else { - "" - }; - - let mut parts = Vec::with_capacity_in(4, arena); - - if !first_is_uppercase { - let first_part = unsafe { std::str::from_utf8_unchecked(&buffer[..chomped]) }; - parts.push(first_part); - } - - match chomp_access_chain(&buffer[chomped..], &mut parts) { - Ok(width) => { - chomped += width as usize; - - let ident = Ident::Access { - module_name, - parts: parts.into_bump_slice(), - }; - - Ok((chomped as u32, ident)) - } - Err(0) if !module_name.is_empty() => Err(( - chomped as u32, - BadIdent::QualifiedTag(pos.bump_column(chomped as u32)), - )), - Err(1) if parts.is_empty() => Err(( - chomped as u32 + 1, - BadIdent::WeirdDotQualified(pos.bump_column(chomped as u32 + 1)), - )), - Err(width) => Err(( - chomped as u32 + width, - BadIdent::WeirdDotAccess(pos.bump_column(chomped as u32 + width)), - )), - } - } else if let Ok(('_', _)) = char::from_utf8_slice_start(&buffer[chomped..]) { - // we don't allow underscores in the middle of an identifier - // but still parse them (and generate a malformed identifier) - // to give good error messages for this case - Err(( - chomped as u32 + 1, - BadIdent::Underscore(pos.bump_column(chomped as u32 + 1)), - )) - } else if first_is_uppercase { - // just one segment, starting with an uppercase letter; that's a tag - let value = unsafe { std::str::from_utf8_unchecked(&buffer[..chomped]) }; - Ok((chomped as u32, Ident::Tag(value))) - } else { - // just one segment, starting with a lowercase letter; that's a normal identifier - let value = unsafe { std::str::from_utf8_unchecked(&buffer[..chomped]) }; - let ident = Ident::Access { - module_name: "", - parts: arena.alloc([value]), - }; - Ok((chomped as u32, ident)) - } -} - -fn chomp_module_chain(buffer: &[u8]) -> Result { - let mut chomped = 0; - - while let Some(b'.') = buffer.get(chomped) { - match &buffer.get(chomped + 1..) { - Some(slice) => match chomp_uppercase_part(slice) { - Ok(name) => { - chomped += name.len() + 1; - } - Err(MadeProgress) => return Err(MadeProgress), - Err(NoProgress) => break, - }, - None => return Err(MadeProgress), - } - } - - if chomped == 0 { - Err(NoProgress) - } else { - Ok(chomped as u32) - } -} - -pub fn concrete_type<'a>() -> impl Parser<'a, (&'a str, &'a str), ()> { - move |_, state: State<'a>| match chomp_concrete_type(state.bytes()) { - Err(progress) => Err((progress, (), state)), - Ok((module_name, type_name, width)) => { - Ok((MadeProgress, (module_name, type_name), state.advance(width))) - } - } -} - -// parse a type name like `Result` or `Result.Result` -fn chomp_concrete_type(buffer: &[u8]) -> Result<(&str, &str, usize), Progress> { - let first = crate::ident::chomp_uppercase_part(buffer)?; - - if let Some(b'.') = buffer.get(first.len()) { - match crate::ident::chomp_module_chain(&buffer[first.len()..]) { - Err(_) => Err(MadeProgress), - Ok(rest) => { - let width = first.len() + rest as usize; - - // we must explicitly check here for a trailing `.` - if let Some(b'.') = buffer.get(width) { - return Err(MadeProgress); - } - - let slice = &buffer[..width]; - - match slice.iter().rev().position(|c| *c == b'.') { - None => Ok(("", first, first.len())), - Some(rev_index) => { - let index = slice.len() - rev_index; - let module_name = - unsafe { std::str::from_utf8_unchecked(&slice[..index - 1]) }; - let type_name = unsafe { std::str::from_utf8_unchecked(&slice[index..]) }; - - Ok((module_name, type_name, width)) - } - } - } - } - } else { - Ok(("", first, first.len())) - } -} - -fn chomp_access_chain<'a>(buffer: &'a [u8], parts: &mut Vec<'a, &'a str>) -> Result { - let mut chomped = 0; - - while let Some(b'.') = buffer.get(chomped) { - match &buffer.get(chomped + 1..) { - Some(slice) => match chomp_lowercase_part(slice) { - Ok(name) => { - let value = unsafe { - std::str::from_utf8_unchecked( - &buffer[chomped + 1..chomped + 1 + name.len()], - ) - }; - parts.push(value); - - chomped += name.len() + 1; - } - Err(_) => return Err(chomped as u32 + 1), - }, - None => return Err(chomped as u32 + 1), - } - } - - if chomped == 0 { - Err(0) - } else { - Ok(chomped as u32) - } -} - -fn parse_ident_help<'a>( - arena: &'a Bump, - mut state: State<'a>, -) -> ParseResult<'a, Ident<'a>, BadIdent> { - match chomp_identifier_chain(arena, state.bytes(), state.pos()) { - Ok((width, ident)) => { - state = advance_state!(state, width as usize)?; - Ok((MadeProgress, ident, state)) - } - Err((0, fail)) => Err((NoProgress, fail, state)), - Err((width, fail)) => { - state = advance_state!(state, width as usize)?; - Err((MadeProgress, fail, state)) - } - } -} diff --git a/compiler/parse/src/keyword.rs b/compiler/parse/src/keyword.rs deleted file mode 100644 index 4d83a06eb2..0000000000 --- a/compiler/parse/src/keyword.rs +++ /dev/null @@ -1,9 +0,0 @@ -pub const IF: &str = "if"; -pub const THEN: &str = "then"; -pub const ELSE: &str = "else"; -pub const WHEN: &str = "when"; -pub const AS: &str = "as"; -pub const IS: &str = "is"; -pub const EXPECT: &str = "expect"; - -pub const KEYWORDS: [&str; 7] = [IF, THEN, ELSE, WHEN, AS, IS, EXPECT]; diff --git a/compiler/parse/src/lib.rs b/compiler/parse/src/lib.rs deleted file mode 100644 index a6954f8edd..0000000000 --- a/compiler/parse/src/lib.rs +++ /dev/null @@ -1,20 +0,0 @@ -#![warn(clippy::dbg_macro)] -// See github.com/rtfeldman/roc/issues/800 for discussion of the large_enum_variant check. -#![allow(clippy::large_enum_variant)] - -#[macro_use] -pub mod parser; -pub mod ast; -pub mod blankspace; -pub mod expr; -pub mod header; -pub mod ident; -pub mod keyword; -pub mod module; -pub mod number_literal; -pub mod pattern; -pub mod problems; -pub mod state; -pub mod string_literal; -pub mod test_helpers; -pub mod type_annotation; diff --git a/compiler/parse/src/module.rs b/compiler/parse/src/module.rs deleted file mode 100644 index e5bc7c259d..0000000000 --- a/compiler/parse/src/module.rs +++ /dev/null @@ -1,908 +0,0 @@ -use crate::ast::{Collection, CommentOrNewline, Def, Defs, Module, Spaced}; -use crate::blankspace::{space0_around_ee, space0_before_e, space0_e}; -use crate::header::{ - package_entry, package_name, AppHeader, ExposedName, HostedHeader, ImportsEntry, - InterfaceHeader, ModuleName, PackageEntry, PlatformHeader, PlatformRequires, To, TypedIdent, -}; -use crate::ident::{self, lowercase_ident, unqualified_ident, uppercase, UppercaseIdent}; -use crate::parser::Progress::{self, *}; -use crate::parser::{ - backtrackable, optional, specialize, specialize_region, word1, EExposes, EGenerates, - EGeneratesWith, EHeader, EImports, EPackages, EProvides, ERequires, ETypedIdent, Parser, - SourceError, SpaceProblem, SyntaxError, -}; -use crate::state::State; -use crate::string_literal; -use crate::type_annotation; -use bumpalo::collections::Vec; -use roc_region::all::{Loc, Position}; - -fn end_of_file<'a>() -> impl Parser<'a, (), SyntaxError<'a>> { - |_arena, state: State<'a>| { - if state.has_reached_end() { - Ok((NoProgress, (), state)) - } else { - Err((NoProgress, SyntaxError::NotEndOfFile(state.pos()), state)) - } - } -} - -#[inline(always)] -pub fn module_defs<'a>() -> impl Parser<'a, Defs<'a>, SyntaxError<'a>> { - let min_indent = 0; - skip_second!( - specialize_region( - |e, r| SyntaxError::Expr(e, r.start()), - crate::expr::toplevel_defs(min_indent), - ), - end_of_file() - ) -} - -#[inline(always)] -pub fn module_defs_help<'a>() -> impl Parser<'a, Vec<'a, Loc>>, SyntaxError<'a>> { - // force that we parse until the end of the input - let min_indent = 0; - skip_second!( - specialize_region( - |e, r| SyntaxError::Expr(e, r.start()), - crate::expr::defs(min_indent), - ), - end_of_file() - ) -} - -pub fn parse_header<'a>( - arena: &'a bumpalo::Bump, - state: State<'a>, -) -> Result<(Module<'a>, State<'a>), SourceError<'a, EHeader<'a>>> { - match header().parse(arena, state) { - Ok((_, module, state)) => Ok((module, state)), - Err((_, fail, state)) => Err(SourceError::new(fail, &state)), - } -} - -fn header<'a>() -> impl Parser<'a, Module<'a>, EHeader<'a>> { - use crate::parser::keyword_e; - - type Clos<'b> = Box<(dyn FnOnce(&'b [CommentOrNewline]) -> Module<'b> + 'b)>; - - map!( - and!( - space0_e(0, EHeader::IndentStart), - one_of![ - map!( - skip_first!(keyword_e("interface", EHeader::Start), interface_header()), - |mut header: InterfaceHeader<'a>| -> Clos<'a> { - Box::new(|spaces| { - header.before_header = spaces; - Module::Interface { header } - }) - } - ), - map!( - skip_first!(keyword_e("app", EHeader::Start), app_header()), - |mut header: AppHeader<'a>| -> Clos<'a> { - Box::new(|spaces| { - header.before_header = spaces; - Module::App { header } - }) - } - ), - map!( - skip_first!(keyword_e("platform", EHeader::Start), platform_header()), - |mut header: PlatformHeader<'a>| -> Clos<'a> { - Box::new(|spaces| { - header.before_header = spaces; - Module::Platform { header } - }) - } - ), - map!( - skip_first!(keyword_e("hosted", EHeader::Start), hosted_header()), - |mut header: HostedHeader<'a>| -> Clos<'a> { - Box::new(|spaces| { - header.before_header = spaces; - Module::Hosted { header } - }) - } - ) - ] - ), - |(spaces, make_header): (&'a [CommentOrNewline], Clos<'a>)| { make_header(spaces) } - ) -} - -#[inline(always)] -fn interface_header<'a>() -> impl Parser<'a, InterfaceHeader<'a>, EHeader<'a>> { - |arena, state| { - let min_indent = 1; - - let (_, after_interface_keyword, state) = - space0_e(min_indent, EHeader::IndentStart).parse(arena, state)?; - let (_, name, state) = loc!(module_name_help(EHeader::ModuleName)).parse(arena, state)?; - - let (_, ((before_exposes, after_exposes), exposes), state) = - specialize(EHeader::Exposes, exposes_values()).parse(arena, state)?; - let (_, ((before_imports, after_imports), imports), state) = - specialize(EHeader::Imports, imports()).parse(arena, state)?; - - let header = InterfaceHeader { - name, - exposes, - imports, - before_header: &[] as &[_], - after_interface_keyword, - before_exposes, - after_exposes, - before_imports, - after_imports, - }; - - Ok((MadeProgress, header, state)) - } -} - -#[inline(always)] -fn hosted_header<'a>() -> impl Parser<'a, HostedHeader<'a>, EHeader<'a>> { - |arena, state| { - let min_indent = 1; - - let (_, after_hosted_keyword, state) = - space0_e(min_indent, EHeader::IndentStart).parse(arena, state)?; - let (_, name, state) = loc!(module_name_help(EHeader::ModuleName)).parse(arena, state)?; - - let (_, ((before_exposes, after_exposes), exposes), state) = - specialize(EHeader::Exposes, exposes_values()).parse(arena, state)?; - let (_, ((before_imports, after_imports), imports), state) = - specialize(EHeader::Imports, imports()).parse(arena, state)?; - let (_, ((before_generates, after_generates), generates), state) = - specialize(EHeader::Generates, generates()).parse(arena, state)?; - let (_, ((before_with, after_with), generates_with), state) = - specialize(EHeader::GeneratesWith, generates_with()).parse(arena, state)?; - - let header = HostedHeader { - name, - exposes, - imports, - generates, - generates_with, - before_header: &[] as &[_], - after_hosted_keyword, - before_exposes, - after_exposes, - before_imports, - after_imports, - before_generates, - after_generates, - before_with, - after_with, - }; - - Ok((MadeProgress, header, state)) - } -} - -fn chomp_module_name(buffer: &[u8]) -> Result<&str, Progress> { - use encode_unicode::CharExt; - - let mut chomped = 0; - - if let Ok((first_letter, width)) = char::from_utf8_slice_start(&buffer[chomped..]) { - if first_letter.is_uppercase() { - chomped += width; - } else { - return Err(Progress::NoProgress); - } - } - - while let Ok((ch, width)) = char::from_utf8_slice_start(&buffer[chomped..]) { - // After the first character, only these are allowed: - // - // * Unicode alphabetic chars - you might include `鹏` if that's clear to your readers - // * ASCII digits - e.g. `1` but not `¾`, both of which pass .is_numeric() - // * A '.' separating module parts - if ch.is_alphabetic() || ch.is_ascii_digit() { - chomped += width; - } else if ch == '.' { - chomped += width; - - if let Ok((first_letter, width)) = char::from_utf8_slice_start(&buffer[chomped..]) { - if first_letter.is_uppercase() { - chomped += width; - } else if first_letter == '{' { - // the .{ starting a `Foo.{ bar, baz }` importing clauses - chomped -= width; - break; - } else { - return Err(Progress::MadeProgress); - } - } - } else { - // we're done - break; - } - } - - let name = unsafe { std::str::from_utf8_unchecked(&buffer[..chomped]) }; - - Ok(name) -} - -#[inline(always)] -fn module_name<'a>() -> impl Parser<'a, ModuleName<'a>, ()> { - |_, mut state: State<'a>| match chomp_module_name(state.bytes()) { - Ok(name) => { - let width = name.len(); - state = state.advance(width); - - Ok((MadeProgress, ModuleName::new(name), state)) - } - Err(progress) => Err((progress, (), state)), - } -} - -#[inline(always)] -fn app_header<'a>() -> impl Parser<'a, AppHeader<'a>, EHeader<'a>> { - |arena, state| { - let min_indent = 1; - - let (_, after_app_keyword, state) = - space0_e(min_indent, EHeader::IndentStart).parse(arena, state)?; - let (_, name, state) = loc!(crate::parser::specialize( - EHeader::AppName, - string_literal::parse() - )) - .parse(arena, state)?; - - let (_, opt_pkgs, state) = - maybe!(specialize(EHeader::Packages, packages())).parse(arena, state)?; - let (_, opt_imports, state) = - maybe!(specialize(EHeader::Imports, imports())).parse(arena, state)?; - let (_, provides, state) = - specialize(EHeader::Provides, provides_to()).parse(arena, state)?; - - let (before_packages, after_packages, packages) = match opt_pkgs { - Some(pkgs) => { - let pkgs: Packages<'a> = pkgs; // rustc must be told the type here - - ( - pkgs.before_packages_keyword, - pkgs.after_packages_keyword, - pkgs.entries, - ) - } - None => (&[] as _, &[] as _, Collection::empty()), - }; - - // rustc must be told the type here - #[allow(clippy::type_complexity)] - let opt_imports: Option<( - (&'a [CommentOrNewline<'a>], &'a [CommentOrNewline<'a>]), - Collection<'a, Loc>>>, - )> = opt_imports; - - let ((before_imports, after_imports), imports) = - opt_imports.unwrap_or_else(|| ((&[] as _, &[] as _), Collection::empty())); - let provides: ProvidesTo<'a> = provides; // rustc must be told the type here - - let header = AppHeader { - name, - packages, - imports, - provides: provides.entries, - provides_types: provides.types, - to: provides.to, - before_header: &[] as &[_], - after_app_keyword, - before_packages, - after_packages, - before_imports, - after_imports, - before_provides: provides.before_provides_keyword, - after_provides: provides.after_provides_keyword, - before_to: provides.before_to_keyword, - after_to: provides.after_to_keyword, - }; - - Ok((MadeProgress, header, state)) - } -} - -#[inline(always)] -fn platform_header<'a>() -> impl Parser<'a, PlatformHeader<'a>, EHeader<'a>> { - |arena, state| { - let min_indent = 1; - - let (_, after_platform_keyword, state) = - space0_e(min_indent, EHeader::IndentStart).parse(arena, state)?; - let (_, name, state) = - loc!(specialize(EHeader::PlatformName, package_name())).parse(arena, state)?; - - let (_, ((before_requires, after_requires), requires), state) = - specialize(EHeader::Requires, requires()).parse(arena, state)?; - - let (_, ((before_exposes, after_exposes), exposes), state) = - specialize(EHeader::Exposes, exposes_modules()).parse(arena, state)?; - - let (_, packages, state) = specialize(EHeader::Packages, packages()).parse(arena, state)?; - - let (_, ((before_imports, after_imports), imports), state) = - specialize(EHeader::Imports, imports()).parse(arena, state)?; - - let (_, ((before_provides, after_provides), (provides, _provides_type)), state) = - specialize(EHeader::Provides, provides_without_to()).parse(arena, state)?; - - let header = PlatformHeader { - name, - requires, - exposes, - packages: packages.entries, - imports, - provides, - before_header: &[] as &[_], - after_platform_keyword, - before_requires, - after_requires, - before_exposes, - after_exposes, - before_packages: packages.before_packages_keyword, - after_packages: packages.after_packages_keyword, - before_imports, - after_imports, - before_provides, - after_provides, - }; - - Ok((MadeProgress, header, state)) - } -} - -#[derive(Debug)] -struct ProvidesTo<'a> { - entries: Collection<'a, Loc>>>, - types: Option>>>>, - to: Loc>, - - before_provides_keyword: &'a [CommentOrNewline<'a>], - after_provides_keyword: &'a [CommentOrNewline<'a>], - before_to_keyword: &'a [CommentOrNewline<'a>], - after_to_keyword: &'a [CommentOrNewline<'a>], -} - -fn provides_to_package<'a>() -> impl Parser<'a, To<'a>, EProvides<'a>> { - one_of![ - specialize( - |_, pos| EProvides::Identifier(pos), - map!(lowercase_ident(), To::ExistingPackage) - ), - specialize(EProvides::Package, map!(package_name(), To::NewPackage)) - ] -} - -#[inline(always)] -fn provides_to<'a>() -> impl Parser<'a, ProvidesTo<'a>, EProvides<'a>> { - let min_indent = 1; - - map!( - and!( - provides_without_to(), - and!( - spaces_around_keyword( - min_indent, - "to", - EProvides::To, - EProvides::IndentTo, - EProvides::IndentListStart - ), - loc!(provides_to_package()) - ) - ), - |( - ((before_provides_keyword, after_provides_keyword), (entries, provides_types)), - ((before_to_keyword, after_to_keyword), to), - )| { - ProvidesTo { - entries, - types: provides_types, - to, - before_provides_keyword, - after_provides_keyword, - before_to_keyword, - after_to_keyword, - } - } - ) -} - -#[inline(always)] -fn provides_without_to<'a>() -> impl Parser< - 'a, - ( - (&'a [CommentOrNewline<'a>], &'a [CommentOrNewline<'a>]), - ( - Collection<'a, Loc>>>, - Option>>>>, - ), - ), - EProvides<'a>, -> { - let min_indent = 1; - and!( - spaces_around_keyword( - min_indent, - "provides", - EProvides::Provides, - EProvides::IndentProvides, - EProvides::IndentListStart - ), - and!( - collection_trailing_sep_e!( - word1(b'[', EProvides::ListStart), - exposes_entry(EProvides::Identifier), - word1(b',', EProvides::ListEnd), - word1(b']', EProvides::ListEnd), - min_indent, - EProvides::Open, - EProvides::IndentListEnd, - Spaced::SpaceBefore - ), - // Optionally - optional(provides_types()) - ) - ) -} - -#[inline(always)] -fn provides_types<'a>( -) -> impl Parser<'a, Collection<'a, Loc>>>, EProvides<'a>> { - let min_indent = 1; - - skip_first!( - // We only support spaces here, not newlines, because this is not intended - // to be the design forever. Someday it will hopefully work like Elm, - // where platform authors can provide functions like Browser.sandbox which - // present an API based on ordinary-looking type variables. - zero_or_more!(word1( - b' ', - // HACK: If this errors, EProvides::Provides is not an accurate reflection - // of what went wrong. However, this is both skipped and zero_or_more, - // so this error should never be visible to anyone in practice! - EProvides::Provides - )), - collection_trailing_sep_e!( - word1(b'{', EProvides::ListStart), - provides_type_entry(EProvides::Identifier), - word1(b',', EProvides::ListEnd), - word1(b'}', EProvides::ListEnd), - min_indent, - EProvides::Open, - EProvides::IndentListEnd, - Spaced::SpaceBefore - ) - ) -} - -fn provides_type_entry<'a, F, E>( - to_expectation: F, -) -> impl Parser<'a, Loc>>, E> -where - F: Fn(Position) -> E, - F: Copy, - E: 'a, -{ - loc!(map!( - specialize(|_, pos| to_expectation(pos), ident::uppercase()), - Spaced::Item - )) -} - -fn exposes_entry<'a, F, E>( - to_expectation: F, -) -> impl Parser<'a, Loc>>, E> -where - F: Fn(Position) -> E, - F: Copy, - E: 'a, -{ - loc!(map!( - specialize(|_, pos| to_expectation(pos), unqualified_ident()), - |n| Spaced::Item(ExposedName::new(n)) - )) -} - -#[inline(always)] -fn requires<'a>() -> impl Parser< - 'a, - ( - (&'a [CommentOrNewline<'a>], &'a [CommentOrNewline<'a>]), - PlatformRequires<'a>, - ), - ERequires<'a>, -> { - let min_indent = 0; - and!( - spaces_around_keyword( - min_indent, - "requires", - ERequires::Requires, - ERequires::IndentRequires, - ERequires::IndentListStart - ), - platform_requires() - ) -} - -#[inline(always)] -fn platform_requires<'a>() -> impl Parser<'a, PlatformRequires<'a>, ERequires<'a>> { - map!( - and!( - skip_second!(requires_rigids(0), space0_e(0, ERequires::ListStart)), - requires_typed_ident() - ), - |(rigids, signature)| { PlatformRequires { rigids, signature } } - ) -} - -#[inline(always)] -fn requires_rigids<'a>( - min_indent: u32, -) -> impl Parser<'a, Collection<'a, Loc>>>, ERequires<'a>> { - collection_trailing_sep_e!( - word1(b'{', ERequires::ListStart), - specialize( - |_, pos| ERequires::Rigid(pos), - loc!(map!(ident::uppercase(), Spaced::Item)) - ), - word1(b',', ERequires::ListEnd), - word1(b'}', ERequires::ListEnd), - min_indent, - ERequires::Open, - ERequires::IndentListEnd, - Spaced::SpaceBefore - ) -} - -#[inline(always)] -fn requires_typed_ident<'a>() -> impl Parser<'a, Loc>>, ERequires<'a>> { - skip_first!( - word1(b'{', ERequires::ListStart), - skip_second!( - space0_around_ee( - specialize(ERequires::TypedIdent, loc!(typed_ident()),), - 0, - ERequires::ListStart, - ERequires::ListEnd - ), - word1(b'}', ERequires::ListStart) - ) - ) -} - -#[inline(always)] -fn exposes_values<'a>() -> impl Parser< - 'a, - ( - (&'a [CommentOrNewline<'a>], &'a [CommentOrNewline<'a>]), - Collection<'a, Loc>>>, - ), - EExposes, -> { - let min_indent = 1; - - and!( - spaces_around_keyword( - min_indent, - "exposes", - EExposes::Exposes, - EExposes::IndentExposes, - EExposes::IndentListStart - ), - collection_trailing_sep_e!( - word1(b'[', EExposes::ListStart), - exposes_entry(EExposes::Identifier), - word1(b',', EExposes::ListEnd), - word1(b']', EExposes::ListEnd), - min_indent, - EExposes::Open, - EExposes::IndentListEnd, - Spaced::SpaceBefore - ) - ) -} - -fn spaces_around_keyword<'a, E>( - min_indent: u32, - keyword: &'static str, - expectation: fn(Position) -> E, - indent_problem1: fn(Position) -> E, - indent_problem2: fn(Position) -> E, -) -> impl Parser<'a, (&'a [CommentOrNewline<'a>], &'a [CommentOrNewline<'a>]), E> -where - E: 'a + SpaceProblem, -{ - and!( - skip_second!( - backtrackable(space0_e(min_indent, indent_problem1)), - crate::parser::keyword_e(keyword, expectation) - ), - space0_e(min_indent, indent_problem2) - ) -} - -#[inline(always)] -fn exposes_modules<'a>() -> impl Parser< - 'a, - ( - (&'a [CommentOrNewline<'a>], &'a [CommentOrNewline<'a>]), - Collection<'a, Loc>>>, - ), - EExposes, -> { - let min_indent = 1; - - and!( - spaces_around_keyword( - min_indent, - "exposes", - EExposes::Exposes, - EExposes::IndentExposes, - EExposes::IndentListStart - ), - collection_trailing_sep_e!( - word1(b'[', EExposes::ListStart), - exposes_module(EExposes::Identifier), - word1(b',', EExposes::ListEnd), - word1(b']', EExposes::ListEnd), - min_indent, - EExposes::Open, - EExposes::IndentListEnd, - Spaced::SpaceBefore - ) - ) -} - -fn exposes_module<'a, F, E>( - to_expectation: F, -) -> impl Parser<'a, Loc>>, E> -where - F: Fn(Position) -> E, - F: Copy, - E: 'a, -{ - loc!(map!( - specialize(|_, pos| to_expectation(pos), module_name()), - Spaced::Item - )) -} - -#[derive(Debug)] -struct Packages<'a> { - entries: Collection<'a, Loc>>>, - before_packages_keyword: &'a [CommentOrNewline<'a>], - after_packages_keyword: &'a [CommentOrNewline<'a>], -} - -#[inline(always)] -fn packages<'a>() -> impl Parser<'a, Packages<'a>, EPackages<'a>> { - let min_indent = 1; - - map!( - and!( - spaces_around_keyword( - min_indent, - "packages", - EPackages::Packages, - EPackages::IndentPackages, - EPackages::IndentListStart - ), - collection_trailing_sep_e!( - word1(b'{', EPackages::ListStart), - specialize(EPackages::PackageEntry, loc!(package_entry())), - word1(b',', EPackages::ListEnd), - word1(b'}', EPackages::ListEnd), - min_indent, - EPackages::Open, - EPackages::IndentListEnd, - Spaced::SpaceBefore - ) - ), - |((before_packages_keyword, after_packages_keyword), entries): ( - (_, _), - Collection<'a, _> - )| { - Packages { - entries, - before_packages_keyword, - after_packages_keyword, - } - } - ) -} - -#[inline(always)] -fn generates<'a>() -> impl Parser< - 'a, - ( - (&'a [CommentOrNewline<'a>], &'a [CommentOrNewline<'a>]), - UppercaseIdent<'a>, - ), - EGenerates, -> { - let min_indent = 1; - - and!( - spaces_around_keyword( - min_indent, - "generates", - EGenerates::Generates, - EGenerates::IndentGenerates, - EGenerates::IndentTypeStart - ), - specialize(|(), pos| EGenerates::Identifier(pos), uppercase()) - ) -} - -#[inline(always)] -fn generates_with<'a>() -> impl Parser< - 'a, - ( - (&'a [CommentOrNewline<'a>], &'a [CommentOrNewline<'a>]), - Collection<'a, Loc>>>, - ), - EGeneratesWith, -> { - let min_indent = 1; - - and!( - spaces_around_keyword( - min_indent, - "with", - EGeneratesWith::With, - EGeneratesWith::IndentWith, - EGeneratesWith::IndentListStart - ), - collection_trailing_sep_e!( - word1(b'[', EGeneratesWith::ListStart), - exposes_entry(EGeneratesWith::Identifier), - word1(b',', EGeneratesWith::ListEnd), - word1(b']', EGeneratesWith::ListEnd), - min_indent, - EGeneratesWith::Open, - EGeneratesWith::IndentListEnd, - Spaced::SpaceBefore - ) - ) -} - -#[inline(always)] -fn imports<'a>() -> impl Parser< - 'a, - ( - (&'a [CommentOrNewline<'a>], &'a [CommentOrNewline<'a>]), - Collection<'a, Loc>>>, - ), - EImports, -> { - let min_indent = 1; - - and!( - spaces_around_keyword( - min_indent, - "imports", - EImports::Imports, - EImports::IndentImports, - EImports::IndentListStart - ), - collection_trailing_sep_e!( - word1(b'[', EImports::ListStart), - loc!(imports_entry()), - word1(b',', EImports::ListEnd), - word1(b']', EImports::ListEnd), - min_indent, - EImports::Open, - EImports::IndentListEnd, - Spaced::SpaceBefore - ) - ) -} - -#[inline(always)] -fn typed_ident<'a>() -> impl Parser<'a, Spaced<'a, TypedIdent<'a>>, ETypedIdent<'a>> { - // e.g. - // - // printLine : Str -> Effect {} - let min_indent = 0; - - map!( - and!( - and!( - loc!(specialize( - |_, pos| ETypedIdent::Identifier(pos), - lowercase_ident() - )), - space0_e(min_indent, ETypedIdent::IndentHasType) - ), - skip_first!( - word1(b':', ETypedIdent::HasType), - space0_before_e( - specialize( - ETypedIdent::Type, - type_annotation::located(min_indent, true) - ), - min_indent, - ETypedIdent::IndentType, - ) - ) - ), - |((ident, spaces_before_colon), ann)| { - Spaced::Item(TypedIdent { - ident, - spaces_before_colon, - ann, - }) - } - ) -} - -fn shortname<'a>() -> impl Parser<'a, &'a str, EImports> { - specialize(|_, pos| EImports::Shorthand(pos), lowercase_ident()) -} - -fn module_name_help<'a, F, E>(to_expectation: F) -> impl Parser<'a, ModuleName<'a>, E> -where - F: Fn(Position) -> E, - E: 'a, - F: 'a, -{ - specialize(move |_, pos| to_expectation(pos), module_name()) -} - -#[inline(always)] -fn imports_entry<'a>() -> impl Parser<'a, Spaced<'a, ImportsEntry<'a>>, EImports> { - let min_indent = 1; - - type Temp<'a> = ( - (Option<&'a str>, ModuleName<'a>), - Option>>>>, - ); - - map_with_arena!( - and!( - and!( - // e.g. `pf.` - maybe!(skip_second!( - shortname(), - word1(b'.', EImports::ShorthandDot) - )), - // e.g. `Task` - module_name_help(EImports::ModuleName) - ), - // e.g. `.{ Task, after}` - maybe!(skip_first!( - word1(b'.', EImports::ExposingDot), - collection_trailing_sep_e!( - word1(b'{', EImports::SetStart), - exposes_entry(EImports::Identifier), - word1(b',', EImports::SetEnd), - word1(b'}', EImports::SetEnd), - min_indent, - EImports::Open, - EImports::IndentSetEnd, - Spaced::SpaceBefore - ) - )) - ), - |_arena, ((opt_shortname, module_name), opt_values): Temp<'a>| { - let exposed_values = opt_values.unwrap_or_else(Collection::empty); - - let entry = match opt_shortname { - Some(shortname) => ImportsEntry::Package(shortname, module_name, exposed_values), - - None => ImportsEntry::Module(module_name, exposed_values), - }; - - Spaced::Item(entry) - } - ) -} diff --git a/compiler/parse/src/parser.rs b/compiler/parse/src/parser.rs deleted file mode 100644 index 3c25922ba9..0000000000 --- a/compiler/parse/src/parser.rs +++ /dev/null @@ -1,1667 +0,0 @@ -use crate::state::State; -use bumpalo::collections::vec::Vec; -use bumpalo::Bump; -use roc_region::all::{Loc, Position, Region}; -use Progress::*; - -#[derive(Debug, PartialEq, Eq)] -pub enum Either { - First(First), - Second(Second), -} - -pub type ParseResult<'a, Output, Error> = - Result<(Progress, Output, State<'a>), (Progress, Error, State<'a>)>; - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum Progress { - MadeProgress, - NoProgress, -} - -impl Progress { - pub fn from_lengths(before: usize, after: usize) -> Self { - Self::from_consumed(before - after) - } - pub fn from_consumed(chars_consumed: usize) -> Self { - Self::progress_when(chars_consumed != 0) - } - - pub fn progress_when(made_progress: bool) -> Self { - if made_progress { - Progress::MadeProgress - } else { - Progress::NoProgress - } - } - - pub fn or(&self, other: Self) -> Self { - if (*self == MadeProgress) || (other == MadeProgress) { - MadeProgress - } else { - NoProgress - } - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum SyntaxError<'a> { - Unexpected(Region), - OutdentedTooFar, - TooManyLines, - Eof(Region), - InvalidPattern, - BadUtf8, - ReservedKeyword(Region), - ArgumentsBeforeEquals(Region), - NotYetImplemented(String), - Todo, - Type(EType<'a>), - Pattern(EPattern<'a>), - Expr(EExpr<'a>, Position), - Header(EHeader<'a>), - Space(BadInputError), - NotEndOfFile(Position), -} -pub trait SpaceProblem { - fn space_problem(e: BadInputError, pos: Position) -> Self; -} - -macro_rules! impl_space_problem { - ($($name:ident $(< $lt:tt >)?),*) => { - $( - impl $(< $lt >)? SpaceProblem for $name $(< $lt >)? { - fn space_problem(e: BadInputError, pos: Position) -> Self { - Self::Space(e, pos) - } - } - )* - }; -} - -impl_space_problem! { - EExpect<'a>, - EExposes, - EExpr<'a>, - EGenerates, - EGeneratesWith, - EHeader<'a>, - EIf<'a>, - EImports, - EInParens<'a>, - ELambda<'a>, - EList<'a>, - EPackageEntry<'a>, - EPackages<'a>, - EPattern<'a>, - EProvides<'a>, - ERecord<'a>, - ERequires<'a>, - EString<'a>, - EType<'a>, - ETypeInParens<'a>, - ETypeRecord<'a>, - ETypeTagUnion<'a>, - ETypedIdent<'a>, - EWhen<'a>, - EAbility<'a>, - PInParens<'a>, - PRecord<'a> -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum EHeader<'a> { - Provides(EProvides<'a>, Position), - Exposes(EExposes, Position), - Imports(EImports, Position), - Requires(ERequires<'a>, Position), - Packages(EPackages<'a>, Position), - Generates(EGenerates, Position), - GeneratesWith(EGeneratesWith, Position), - - Space(BadInputError, Position), - Start(Position), - ModuleName(Position), - AppName(EString<'a>, Position), - PlatformName(EPackageName<'a>, Position), - IndentStart(Position), -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum EProvides<'a> { - Provides(Position), - Open(Position), - To(Position), - IndentProvides(Position), - IndentTo(Position), - IndentListStart(Position), - IndentListEnd(Position), - IndentPackage(Position), - ListStart(Position), - ListEnd(Position), - Identifier(Position), - Package(EPackageName<'a>, Position), - Space(BadInputError, Position), -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum EExposes { - Exposes(Position), - Open(Position), - IndentExposes(Position), - IndentListStart(Position), - IndentListEnd(Position), - ListStart(Position), - ListEnd(Position), - Identifier(Position), - Space(BadInputError, Position), -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum ERequires<'a> { - Requires(Position), - Open(Position), - IndentRequires(Position), - IndentListStart(Position), - IndentListEnd(Position), - ListStart(Position), - ListEnd(Position), - TypedIdent(ETypedIdent<'a>, Position), - Rigid(Position), - Space(BadInputError, Position), -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum ETypedIdent<'a> { - Space(BadInputError, Position), - HasType(Position), - IndentHasType(Position), - Name(Position), - Type(EType<'a>, Position), - IndentType(Position), - Identifier(Position), -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum EPackages<'a> { - Open(Position), - Space(BadInputError, Position), - Packages(Position), - IndentPackages(Position), - ListStart(Position), - ListEnd(Position), - IndentListStart(Position), - IndentListEnd(Position), - PackageEntry(EPackageEntry<'a>, Position), -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum EPackageName<'a> { - BadPath(EString<'a>, Position), - Escapes(Position), - Multiline(Position), -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum EPackageEntry<'a> { - BadPackage(EPackageName<'a>, Position), - Shorthand(Position), - Colon(Position), - IndentPackage(Position), - Space(BadInputError, Position), -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum EImports { - Open(Position), - Imports(Position), - IndentImports(Position), - IndentListStart(Position), - IndentListEnd(Position), - ListStart(Position), - ListEnd(Position), - Identifier(Position), - ExposingDot(Position), - ShorthandDot(Position), - Shorthand(Position), - ModuleName(Position), - Space(BadInputError, Position), - IndentSetStart(Position), - IndentSetEnd(Position), - SetStart(Position), - SetEnd(Position), -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum EGenerates { - Open(Position), - Generates(Position), - IndentGenerates(Position), - Identifier(Position), - Space(BadInputError, Position), - IndentTypeStart(Position), - IndentTypeEnd(Position), -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum EGeneratesWith { - Open(Position), - With(Position), - IndentWith(Position), - IndentListStart(Position), - IndentListEnd(Position), - ListStart(Position), - ListEnd(Position), - Identifier(Position), - Space(BadInputError, Position), -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum BadInputError { - HasTab, - /// - TooManyLines, - /// - /// - BadUtf8, -} - -pub fn bad_input_to_syntax_error<'a>(bad_input: BadInputError) -> SyntaxError<'a> { - use crate::parser::BadInputError::*; - match bad_input { - HasTab => SyntaxError::NotYetImplemented("call error on tabs".to_string()), - TooManyLines => SyntaxError::TooManyLines, - BadUtf8 => SyntaxError::BadUtf8, - } -} - -impl<'a, T> SourceError<'a, T> { - pub fn new(problem: T, state: &State<'a>) -> Self { - Self { - problem, - bytes: state.original_bytes(), - } - } - - pub fn map_problem(self, f: impl FnOnce(T) -> E) -> SourceError<'a, E> { - SourceError { - problem: f(self.problem), - bytes: self.bytes, - } - } - - pub fn into_file_error(self, filename: std::path::PathBuf) -> FileError<'a, T> { - FileError { - problem: self, - filename, - } - } -} - -impl<'a> SyntaxError<'a> { - pub fn into_source_error(self, state: &State<'a>) -> SourceError<'a, SyntaxError<'a>> { - SourceError { - problem: self, - bytes: state.original_bytes(), - } - } - - pub fn into_file_error( - self, - filename: std::path::PathBuf, - state: &State<'a>, - ) -> FileError<'a, SyntaxError<'a>> { - self.into_source_error(state).into_file_error(filename) - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum EExpr<'a> { - Start(Position), - End(Position), - BadExprEnd(Position), - Space(BadInputError, Position), - - Dot(Position), - Access(Position), - UnaryNot(Position), - UnaryNegate(Position), - BadOperator(&'a str, Position), - - DefMissingFinalExpr(Position), - DefMissingFinalExpr2(&'a EExpr<'a>, Position), - Type(EType<'a>, Position), - Pattern(&'a EPattern<'a>, Position), - Ability(EAbility<'a>, Position), - IndentDefBody(Position), - IndentEquals(Position), - IndentAnnotation(Position), - Equals(Position), - Colon(Position), - DoubleColon(Position), - Ident(Position), - ElmStyleFunction(Region, Position), - MalformedPattern(Position), - QualifiedTag(Position), - BackpassComma(Position), - BackpassArrow(Position), - - When(EWhen<'a>, Position), - If(EIf<'a>, Position), - - Expect(EExpect<'a>, Position), - - Lambda(ELambda<'a>, Position), - Underscore(Position), - - InParens(EInParens<'a>, Position), - Record(ERecord<'a>, Position), - Str(EString<'a>, Position), - SingleQuote(EString<'a>, Position), - Number(ENumber, Position), - List(EList<'a>, Position), - - IndentStart(Position), - IndentEnd(Position), -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum ENumber { - End, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum EString<'a> { - Open(Position), - - CodePtOpen(Position), - CodePtEnd(Position), - - Space(BadInputError, Position), - EndlessSingle(Position), - EndlessMulti(Position), - UnknownEscape(Position), - Format(&'a EExpr<'a>, Position), - FormatEnd(Position), -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum ERecord<'a> { - End(Position), - Open(Position), - - Updateable(Position), - Field(Position), - Colon(Position), - QuestionMark(Position), - Bar(Position), - Ampersand(Position), - - // TODO remove - Expr(&'a EExpr<'a>, Position), - - Space(BadInputError, Position), - - IndentOpen(Position), - IndentColon(Position), - IndentBar(Position), - IndentAmpersand(Position), - IndentEnd(Position), -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum EInParens<'a> { - End(Position), - Open(Position), - /// - Expr(&'a EExpr<'a>, Position), - - /// - Space(BadInputError, Position), - /// - IndentOpen(Position), - IndentEnd(Position), -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum ELambda<'a> { - Space(BadInputError, Position), - Start(Position), - Arrow(Position), - Comma(Position), - Arg(Position), - // TODO make EEXpr - Pattern(EPattern<'a>, Position), - Body(&'a EExpr<'a>, Position), - IndentArrow(Position), - IndentBody(Position), - IndentArg(Position), -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum EList<'a> { - Open(Position), - End(Position), - Space(BadInputError, Position), - - Expr(&'a EExpr<'a>, Position), - - IndentOpen(Position), - IndentEnd(Position), -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum EWhen<'a> { - Space(BadInputError, Position), - When(Position), - Is(Position), - Pattern(EPattern<'a>, Position), - Arrow(Position), - Bar(Position), - - IfToken(Position), - IfGuard(&'a EExpr<'a>, Position), - - Condition(&'a EExpr<'a>, Position), - Branch(&'a EExpr<'a>, Position), - - IndentIs(Position), - IndentCondition(Position), - IndentPattern(Position), - IndentArrow(Position), - IndentBranch(Position), - IndentIfGuard(Position), - PatternAlignment(u32, Position), -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum EAbility<'a> { - Space(BadInputError, Position), - Type(EType<'a>, Position), - - DemandAlignment(i32, Position), - DemandName(Position), - DemandColon(Position), -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum EIf<'a> { - Space(BadInputError, Position), - If(Position), - Then(Position), - Else(Position), - // TODO make EEXpr - Condition(&'a EExpr<'a>, Position), - ThenBranch(&'a EExpr<'a>, Position), - ElseBranch(&'a EExpr<'a>, Position), - - IndentCondition(Position), - IndentIf(Position), - IndentThenToken(Position), - IndentElseToken(Position), - IndentThenBranch(Position), - IndentElseBranch(Position), -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum EExpect<'a> { - Space(BadInputError, Position), - Expect(Position), - Condition(&'a EExpr<'a>, Position), - Continuation(&'a EExpr<'a>, Position), - IndentCondition(Position), -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum EPattern<'a> { - Record(PRecord<'a>, Position), - Underscore(Position), - - Start(Position), - End(Position), - Space(BadInputError, Position), - - PInParens(PInParens<'a>, Position), - NumLiteral(ENumber, Position), - - IndentStart(Position), - IndentEnd(Position), - AsIndentStart(Position), -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum PRecord<'a> { - End(Position), - Open(Position), - - Field(Position), - Colon(Position), - Optional(Position), - - Pattern(&'a EPattern<'a>, Position), - Expr(&'a EExpr<'a>, Position), - - Space(BadInputError, Position), - - IndentOpen(Position), - IndentColon(Position), - IndentOptional(Position), - IndentEnd(Position), -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum PInParens<'a> { - End(Position), - Open(Position), - Pattern(&'a EPattern<'a>, Position), - - Space(BadInputError, Position), - IndentOpen(Position), - IndentEnd(Position), -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum EType<'a> { - Space(BadInputError, Position), - - TRecord(ETypeRecord<'a>, Position), - TTagUnion(ETypeTagUnion<'a>, Position), - TInParens(ETypeInParens<'a>, Position), - TApply(ETypeApply, Position), - TInlineAlias(ETypeInlineAlias, Position), - TBadTypeVariable(Position), - TWildcard(Position), - TInferred(Position), - /// - TStart(Position), - TEnd(Position), - TFunctionArgument(Position), - TWhereBar(Position), - THasClause(Position), - /// - TIndentStart(Position), - TIndentEnd(Position), - TAsIndentStart(Position), -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum ETypeRecord<'a> { - End(Position), - Open(Position), - - Field(Position), - Colon(Position), - Optional(Position), - Type(&'a EType<'a>, Position), - - Space(BadInputError, Position), - - IndentOpen(Position), - IndentColon(Position), - IndentOptional(Position), - IndentEnd(Position), -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum ETypeTagUnion<'a> { - End(Position), - Open(Position), - - Type(&'a EType<'a>, Position), - - Space(BadInputError, Position), - - IndentOpen(Position), - IndentEnd(Position), -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum ETypeInParens<'a> { - End(Position), - Open(Position), - /// - Type(&'a EType<'a>, Position), - - /// - Space(BadInputError, Position), - /// - IndentOpen(Position), - IndentEnd(Position), -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum ETypeApply { - /// - StartNotUppercase(Position), - End(Position), - Space(BadInputError, Position), - /// - DoubleDot(Position), - TrailingDot(Position), - StartIsNumber(Position), -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum ETypeInlineAlias { - NotAnAlias(Position), - Qualified(Position), - ArgumentNotLowercase(Position), -} - -#[derive(Debug)] -pub struct SourceError<'a, T> { - pub problem: T, - pub bytes: &'a [u8], -} - -#[derive(Debug)] -pub struct FileError<'a, T> { - pub problem: SourceError<'a, T>, - pub filename: std::path::PathBuf, -} - -pub trait Parser<'a, Output, Error> { - fn parse(&self, _: &'a Bump, _: State<'a>) -> ParseResult<'a, Output, Error>; - - #[cfg(not(feature = "parse_debug_trace"))] - fn trace(self, _message: &'static str) -> Self - where - Self: Sized, - Output: std::fmt::Debug, - Error: std::fmt::Debug, - { - self - } - - #[cfg(feature = "parse_debug_trace")] - fn trace(self, message: &'static str) -> Traced<'a, Output, Error, Self> - where - Self: Sized, - Output: std::fmt::Debug, - Error: std::fmt::Debug, - { - Traced { - parser: self, - message, - _phantom: Default::default(), - } - } -} - -impl<'a, F, Output, Error> Parser<'a, Output, Error> for F -where - Error: 'a, - F: Fn(&'a Bump, State<'a>) -> ParseResult<'a, Output, Error>, -{ - fn parse(&self, arena: &'a Bump, state: State<'a>) -> ParseResult<'a, Output, Error> { - self(arena, state) - } -} - -#[cfg(feature = "parse_debug_trace")] -pub struct Traced<'a, O, E, P: Parser<'a, O, E>> { - parser: P, - message: &'static str, - _phantom: std::marker::PhantomData<&'a (O, E)>, -} - -#[cfg(feature = "parse_debug_trace")] -impl<'a, O: std::fmt::Debug, E: std::fmt::Debug, P: Parser<'a, O, E>> Parser<'a, O, E> - for Traced<'a, O, E, P> -where - E: 'a, -{ - fn parse(&self, arena: &'a Bump, state: State<'a>) -> ParseResult<'a, O, E> { - use std::cell::RefCell; - - thread_local! { - pub static INDENT: RefCell = RefCell::new(0); - } - - // This should be enough for anyone. Right? RIGHT? - let indent_text = - "| ; : ! | ; : ! | ; : ! | ; : ! | ; : ! | ; : ! | ; : ! | ; : ! | ; : ! "; - - let cur_indent = INDENT.with(|i| *i.borrow()); - - println!( - "{:>5?}: {}{:<50}", - state.pos(), - &indent_text[..cur_indent * 2], - self.message - ); - - INDENT.with(|i| *i.borrow_mut() += 1); - let res = self.parser.parse(arena, state); - INDENT.with(|i| *i.borrow_mut() = cur_indent); - - let (progress, value, state) = match &res { - Ok((progress, result, state)) => (progress, Ok(result), state), - Err((progress, error, state)) => (progress, Err(error), state), - }; - - println!( - "{:<5?}: {}{:<50} {:<15} {:?}", - state.pos(), - &indent_text[..cur_indent * 2], - self.message, - format!("{:?}", progress), - value - ); - - res - } -} - -pub fn allocated<'a, P, Val, Error>(parser: P) -> impl Parser<'a, &'a Val, Error> -where - Error: 'a, - P: Parser<'a, Val, Error>, - Val: 'a, -{ - move |arena, state: State<'a>| { - let (progress, answer, state) = parser.parse(arena, state)?; - - Ok((progress, &*arena.alloc(answer), state)) - } -} - -pub fn and_then<'a, P1, P2, F, Before, After, Error>( - parser: P1, - transform: F, -) -> impl Parser<'a, After, Error> -where - P1: Parser<'a, Before, Error>, - P2: Parser<'a, After, Error>, - F: Fn(Progress, Before) -> P2, - Error: 'a, -{ - move |arena, state| { - parser - .parse(arena, state) - .and_then(|(progress, output, next_state)| { - transform(progress, output).parse(arena, next_state) - }) - } -} - -pub fn then<'a, P1, F, Before, After, E>(parser: P1, transform: F) -> impl Parser<'a, After, E> -where - P1: Parser<'a, Before, E>, - After: 'a, - E: 'a, - F: Fn(&'a Bump, State<'a>, Progress, Before) -> ParseResult<'a, After, E>, -{ - move |arena, state| { - parser - .parse(arena, state) - .and_then(|(progress, output, next_state)| { - transform(arena, next_state, progress, output) - }) - } -} - -pub fn keyword_e<'a, ToError, E>(keyword: &'static str, if_error: ToError) -> impl Parser<'a, (), E> -where - ToError: Fn(Position) -> E, - E: 'a, -{ - move |_, mut state: State<'a>| { - let width = keyword.len(); - - if !state.bytes().starts_with(keyword.as_bytes()) { - return Err((NoProgress, if_error(state.pos()), state)); - } - - // the next character should not be an identifier character - // to prevent treating `whence` or `iffy` as keywords - match state.bytes().get(width) { - Some(next) if *next == b' ' || *next == b'#' || *next == b'\n' => { - state = state.advance(width); - Ok((MadeProgress, (), state)) - } - None => { - state = state.advance(width); - Ok((MadeProgress, (), state)) - } - Some(_) => Err((NoProgress, if_error(state.pos()), state)), - } - } -} - -/// Parse zero or more values separated by a delimiter (e.g. a comma) whose -/// values are discarded -pub fn sep_by0<'a, P, D, Val, Error>( - delimiter: D, - parser: P, -) -> impl Parser<'a, Vec<'a, Val>, Error> -where - D: Parser<'a, (), Error>, - P: Parser<'a, Val, Error>, - Error: 'a, -{ - move |arena, state: State<'a>| { - let start_bytes_len = state.bytes().len(); - - match parser.parse(arena, state) { - Ok((elem_progress, first_output, next_state)) => { - // in practice, we want elements to make progress - debug_assert_eq!(elem_progress, MadeProgress); - - let mut state = next_state; - let mut buf = Vec::with_capacity_in(1, arena); - - buf.push(first_output); - - loop { - match delimiter.parse(arena, state) { - Ok((_, (), next_state)) => { - // If the delimiter passed, check the element parser. - match parser.parse(arena, next_state) { - Ok((element_progress, next_output, next_state)) => { - // in practice, we want elements to make progress - debug_assert_eq!(element_progress, MadeProgress); - - state = next_state; - buf.push(next_output); - } - Err((_, fail, state)) => { - // If the delimiter parsed, but the following - // element did not, that's a fatal error. - let progress = Progress::from_lengths( - start_bytes_len, - state.bytes().len(), - ); - - return Err((progress, fail, state)); - } - } - } - Err((delim_progress, fail, old_state)) => match delim_progress { - MadeProgress => return Err((MadeProgress, fail, old_state)), - NoProgress => return Ok((NoProgress, buf, old_state)), - }, - } - } - } - Err((element_progress, fail, new_state)) => match element_progress { - MadeProgress => Err((MadeProgress, fail, new_state)), - NoProgress => Ok((NoProgress, Vec::new_in(arena), new_state)), - }, - } - } -} - -/// Parse zero or more values separated by a delimiter (e.g. a comma) -/// with an optional trailing delimiter whose values are discarded -pub fn trailing_sep_by0<'a, P, D, Val, Error>( - delimiter: D, - parser: P, -) -> impl Parser<'a, Vec<'a, Val>, Error> -where - D: Parser<'a, (), Error>, - P: Parser<'a, Val, Error>, - Error: 'a, -{ - move |arena, state: State<'a>| { - let start_bytes_len = state.bytes().len(); - - match parser.parse(arena, state) { - Ok((progress, first_output, next_state)) => { - // in practice, we want elements to make progress - debug_assert_eq!(progress, MadeProgress); - let mut state = next_state; - let mut buf = Vec::with_capacity_in(1, arena); - - buf.push(first_output); - - loop { - match delimiter.parse(arena, state) { - Ok((_, (), next_state)) => { - // If the delimiter passed, check the element parser. - match parser.parse(arena, next_state) { - Ok((element_progress, next_output, next_state)) => { - // in practice, we want elements to make progress - debug_assert_eq!(element_progress, MadeProgress); - - state = next_state; - buf.push(next_output); - } - Err((_, _fail, old_state)) => { - // If the delimiter parsed, but the following - // element did not, that means we saw a trailing comma - let progress = Progress::from_lengths( - start_bytes_len, - old_state.bytes().len(), - ); - return Ok((progress, buf, old_state)); - } - } - } - Err((delim_progress, fail, old_state)) => match delim_progress { - MadeProgress => return Err((MadeProgress, fail, old_state)), - NoProgress => return Ok((NoProgress, buf, old_state)), - }, - } - } - } - Err((element_progress, fail, new_state)) => match element_progress { - MadeProgress => Err((MadeProgress, fail, new_state)), - NoProgress => Ok((NoProgress, Vec::new_in(arena), new_state)), - }, - } - } -} - -/// Parse one or more values separated by a delimiter (e.g. a comma) whose -/// values are discarded -pub fn sep_by1<'a, P, D, Val, Error>( - delimiter: D, - parser: P, -) -> impl Parser<'a, Vec<'a, Val>, Error> -where - D: Parser<'a, (), Error>, - P: Parser<'a, Val, Error>, - Error: 'a, -{ - move |arena, state: State<'a>| { - let start_bytes_len = state.bytes().len(); - - match parser.parse(arena, state) { - Ok((progress, first_output, next_state)) => { - debug_assert_eq!(progress, MadeProgress); - let mut state = next_state; - let mut buf = Vec::with_capacity_in(1, arena); - - buf.push(first_output); - - loop { - match delimiter.parse(arena, state) { - Ok((_, (), next_state)) => { - // If the delimiter passed, check the element parser. - match parser.parse(arena, next_state) { - Ok((_, next_output, next_state)) => { - state = next_state; - buf.push(next_output); - } - Err((_, fail, state)) => { - return Err((MadeProgress, fail, state)); - } - } - } - Err((delim_progress, fail, old_state)) => { - match delim_progress { - MadeProgress => { - // fail if the delimiter made progress - return Err((MadeProgress, fail, old_state)); - } - NoProgress => { - let progress = Progress::from_lengths( - start_bytes_len, - old_state.bytes().len(), - ); - return Ok((progress, buf, old_state)); - } - } - } - } - } - } - Err((fail_progress, fail, new_state)) => Err((fail_progress, fail, new_state)), - } - } -} - -/// Parse one or more values separated by a delimiter (e.g. a comma) whose -/// values are discarded -pub fn sep_by1_e<'a, P, V, D, Val, Error>( - delimiter: D, - parser: P, - to_element_error: V, -) -> impl Parser<'a, Vec<'a, Val>, Error> -where - D: Parser<'a, (), Error>, - P: Parser<'a, Val, Error>, - V: Fn(Position) -> Error, - Error: 'a, -{ - move |arena, state: State<'a>| { - let start_bytes_len = state.bytes().len(); - - match parser.parse(arena, state) { - Ok((progress, first_output, next_state)) => { - debug_assert_eq!(progress, MadeProgress); - let mut state = next_state; - let mut buf = Vec::with_capacity_in(1, arena); - - buf.push(first_output); - - loop { - match delimiter.parse(arena, state) { - Ok((_, (), next_state)) => { - // If the delimiter passed, check the element parser. - match parser.parse(arena, next_state) { - Ok((_, next_output, next_state)) => { - state = next_state; - buf.push(next_output); - } - Err((MadeProgress, fail, state)) => { - return Err((MadeProgress, fail, state)); - } - Err((NoProgress, _fail, state)) => { - return Err((NoProgress, to_element_error(state.pos()), state)); - } - } - } - Err((delim_progress, fail, old_state)) => { - match delim_progress { - MadeProgress => { - // fail if the delimiter made progress - return Err((MadeProgress, fail, old_state)); - } - NoProgress => { - let progress = Progress::from_lengths( - start_bytes_len, - old_state.bytes().len(), - ); - return Ok((progress, buf, old_state)); - } - } - } - } - } - } - - Err((MadeProgress, fail, state)) => Err((MadeProgress, fail, state)), - Err((NoProgress, _fail, state)) => { - Err((NoProgress, to_element_error(state.pos()), state)) - } - } - } -} - -pub fn fail_when_progress( - progress: Progress, - fail: E, - value: T, - state: State<'_>, -) -> ParseResult<'_, T, E> { - match progress { - MadeProgress => Err((MadeProgress, fail, state)), - NoProgress => Ok((NoProgress, value, state)), - } -} - -pub fn optional<'a, P, T, E>(parser: P) -> impl Parser<'a, Option, E> -where - P: Parser<'a, T, E>, - E: 'a, -{ - move |arena: &'a Bump, state: State<'a>| { - // We have to clone this because if the optional parser fails, - // we need to revert back to the original state. - let original_state = state.clone(); - - match parser.parse(arena, state) { - Ok((progress, out1, state)) => Ok((progress, Some(out1), state)), - Err((_, _, _)) => { - // NOTE this will backtrack - // TODO can we get rid of some of the potential backtracking? - Ok((NoProgress, None, original_state)) - } - } - } -} - -// MACRO COMBINATORS -// -// Using some combinators together results in combinatorial type explosion -// which makes things take forever to compile. Using macros instead avoids this! - -#[macro_export] -macro_rules! loc { - ($parser:expr) => { - move |arena, state: $crate::state::State<'a>| { - use roc_region::all::{Loc, Region}; - - let start = state.pos(); - - match $parser.parse(arena, state) { - Ok((progress, value, state)) => { - let end = state.pos(); - let region = Region::new(start, end); - - Ok((progress, Loc { region, value }, state)) - } - Err(err) => Err(err), - } - } - }; -} - -/// If the first one parses, ignore its output and move on to parse with the second one. -#[macro_export] -macro_rules! skip_first { - ($p1:expr, $p2:expr) => { - move |arena, state: $crate::state::State<'a>| { - let original_state = state.clone(); - - match $p1.parse(arena, state) { - Ok((p1, _, state)) => match $p2.parse(arena, state) { - Ok((p2, out2, state)) => Ok((p1.or(p2), out2, state)), - Err((p2, fail, _)) => Err((p1.or(p2), fail, original_state)), - }, - Err((progress, fail, _)) => Err((progress, fail, original_state)), - } - } - }; -} - -/// If the first one parses, parse the second one; if it also parses, use the -/// output from the first one. -#[macro_export] -macro_rules! skip_second { - ($p1:expr, $p2:expr) => { - move |arena, state: $crate::state::State<'a>| { - let original_state = state.clone(); - - match $p1.parse(arena, state) { - Ok((p1, out1, state)) => match $p2.parse(arena, state) { - Ok((p2, _, state)) => Ok((p1.or(p2), out1, state)), - Err((p2, fail, _)) => Err((p1.or(p2), fail, original_state)), - }, - Err((progress, fail, _)) => Err((progress, fail, original_state)), - } - } - }; -} - -/// Parse zero or more elements between two braces (e.g. square braces). -/// Elements can be optionally surrounded by spaces, and are separated by a -/// delimiter (e.g comma-separated). Braces and delimiters get discarded. -#[macro_export] -macro_rules! collection { - ($opening_brace:expr, $elem:expr, $delimiter:expr, $closing_brace:expr, $min_indent:expr) => { - skip_first!( - $opening_brace, - skip_first!( - // We specifically allow space characters inside here, so that - // `[ ]` can be successfully parsed as an empty list, and then - // changed by the formatter back into `[]`. - // - // We don't allow newlines or comments in the middle of empty - // roc_collections because those are normally stored in an Expr, - // and there's no Expr in which to store them in an empty collection! - // - // We could change the AST to add extra storage specifically to - // support empty literals containing newlines or comments, but this - // does not seem worth even the tiniest regression in compiler performance. - zero_or_more!($crate::parser::ascii_char(b' ')), - skip_second!( - $crate::parser::sep_by0( - $delimiter, - $crate::blankspace::space0_around($elem, $min_indent) - ), - $closing_brace - ) - ) - ) - }; -} - -#[macro_export] -macro_rules! collection_trailing_sep_e { - ($opening_brace:expr, $elem:expr, $delimiter:expr, $closing_brace:expr, $min_indent:expr, $open_problem:expr, $indent_problem:expr, $space_before:expr) => { - skip_first!( - $opening_brace, - |arena, state| { - let (_, spaces, state) = space0_e($min_indent, $indent_problem) - .parse(arena, state)?; - - let (_, (mut parsed_elems, mut final_comments), state) = - and!( - $crate::parser::trailing_sep_by0( - $delimiter, - $crate::blankspace::space0_before_optional_after( - $elem, - $min_indent, - $indent_problem, - $indent_problem - ) - ), - $crate::blankspace::space0_e( - // we use min_indent=0 because we want to parse incorrectly indented closing braces - // and later fix these up in the formatter. - 0 /* min_indent */, - $indent_problem) - ).parse(arena, state)?; - - let (_,_, state) = - if parsed_elems.is_empty() { - one_of_with_error![$open_problem; $closing_brace].parse(arena, state)? - } else { - $closing_brace.parse(arena, state)? - }; - - if !spaces.is_empty() { - if let Some(first) = parsed_elems.first_mut() { - first.value = $space_before(arena.alloc(first.value), spaces) - } else { - assert!(final_comments.is_empty()); - final_comments = spaces; - } - } - - let collection = $crate::ast::Collection::with_items_and_comments( - arena, - parsed_elems.into_bump_slice(), - final_comments); - - Ok((MadeProgress, collection, state)) - } - ) - }; -} - -#[macro_export] -macro_rules! succeed { - ($value:expr) => { - move |_arena: &'a bumpalo::Bump, state: $crate::state::State<'a>| { - Ok((NoProgress, $value, state)) - } - }; -} - -#[macro_export] -macro_rules! and { - ($p1:expr, $p2:expr) => { - move |arena: &'a bumpalo::Bump, state: $crate::state::State<'a>| { - // We have to clone this because if the first parser passes and then - // the second one fails, we need to revert back to the original state. - let original_state = state.clone(); - - match $p1.parse(arena, state) { - Ok((p1, out1, state)) => match $p2.parse(arena, state) { - Ok((p2, out2, state)) => Ok((p1.or(p2), (out1, out2), state)), - Err((p2, fail, _)) => Err((p1.or(p2), fail, original_state)), - }, - Err((progress, fail, state)) => Err((progress, fail, state)), - } - } - }; -} - -#[macro_export] -macro_rules! one_of { - ($p1:expr, $p2:expr) => { - move |arena: &'a bumpalo::Bump, state: $crate::state::State<'a>| { - - match $p1.parse(arena, state) { - valid @ Ok(_) => valid, - Err((MadeProgress, fail, state)) => Err((MadeProgress, fail, state)), - Err((NoProgress, _, state)) => $p2.parse( arena, state), - } - } - }; - - ($p1:expr, $($others:expr),+) => { - one_of!($p1, one_of!($($others),+)) - }; - ($p1:expr, $($others:expr),+ $(,)?) => { - one_of!($p1, $($others),+) - }; -} - -#[macro_export] -macro_rules! maybe { - ($p1:expr) => { - move |arena: &'a bumpalo::Bump, state: $crate::state::State<'a>| match $p1 - .parse(arena, state) - { - Ok((progress, value, state)) => Ok((progress, Some(value), state)), - Err((MadeProgress, fail, state)) => Err((MadeProgress, fail, state)), - Err((NoProgress, _, state)) => Ok((NoProgress, None, state)), - } - }; -} - -#[macro_export] -macro_rules! one_of_with_error { - ($toerror:expr; $p1:expr) => { - move |arena: &'a bumpalo::Bump, state: $crate::state::State<'a>| { - - match $p1.parse(arena, state) { - valid @ Ok(_) => valid, - Err((MadeProgress, fail, state)) => Err((MadeProgress, fail, state )), - Err((NoProgress, _, state)) => Err((MadeProgress, $toerror(state.pos()), state)), - } - } - }; - - ($toerror:expr; $p1:expr, $($others:expr),+) => { - one_of_with_error!($toerror, $p1, one_of_with_error!($($others),+)) - }; -} - -pub fn specialize<'a, F, P, T, X, Y>(map_error: F, parser: P) -> impl Parser<'a, T, Y> -where - F: Fn(X, Position) -> Y, - P: Parser<'a, T, X>, - Y: 'a, -{ - move |a, s| match parser.parse(a, s) { - Ok(t) => Ok(t), - Err((p, error, s)) => Err((p, map_error(error, s.pos()), s)), - } -} - -/// Like `specialize`, except the error function receives a Region representing the begin/end of the error -pub fn specialize_region<'a, F, P, T, X, Y>(map_error: F, parser: P) -> impl Parser<'a, T, Y> -where - F: Fn(X, Region) -> Y, - P: Parser<'a, T, X>, - Y: 'a, -{ - move |a, s: State<'a>| { - let start = s.pos(); - match parser.parse(a, s) { - Ok(t) => Ok(t), - Err((p, error, s)) => Err((p, map_error(error, Region::new(start, s.pos())), s)), - } - } -} - -pub fn specialize_ref<'a, F, P, T, X, Y>(map_error: F, parser: P) -> impl Parser<'a, T, Y> -where - F: Fn(&'a X, Position) -> Y, - P: Parser<'a, T, X>, - Y: 'a, - X: 'a, -{ - move |a, s| match parser.parse(a, s) { - Ok(t) => Ok(t), - Err((p, error, s)) => Err((p, map_error(a.alloc(error), s.pos()), s)), - } -} - -pub fn word1<'a, ToError, E>(word: u8, to_error: ToError) -> impl Parser<'a, (), E> -where - ToError: Fn(Position) -> E, - E: 'a, -{ - debug_assert_ne!(word, b'\n'); - - move |_arena: &'a Bump, state: State<'a>| match state.bytes().get(0) { - Some(x) if *x == word => { - let state = state.advance(1); - Ok((MadeProgress, (), state)) - } - _ => Err((NoProgress, to_error(state.pos()), state)), - } -} - -pub fn word2<'a, ToError, E>(word_1: u8, word_2: u8, to_error: ToError) -> impl Parser<'a, (), E> -where - ToError: Fn(Position) -> E, - E: 'a, -{ - debug_assert_ne!(word_1, b'\n'); - debug_assert_ne!(word_2, b'\n'); - - let needle = [word_1, word_2]; - - move |_arena: &'a Bump, state: State<'a>| { - if state.bytes().starts_with(&needle) { - let state = state.advance(2); - Ok((MadeProgress, (), state)) - } else { - Err((NoProgress, to_error(state.pos()), state)) - } - } -} - -pub fn word3<'a, ToError, E>( - word_1: u8, - word_2: u8, - word_3: u8, - to_error: ToError, -) -> impl Parser<'a, (), E> -where - ToError: Fn(Position) -> E, - E: 'a, -{ - debug_assert_ne!(word_1, b'\n'); - debug_assert_ne!(word_2, b'\n'); - debug_assert_ne!(word_3, b'\n'); - - let needle = [word_1, word_2, word_3]; - - move |_arena: &'a Bump, state: State<'a>| { - if state.bytes().starts_with(&needle) { - let state = state.advance(3); - Ok((MadeProgress, (), state)) - } else { - Err((NoProgress, to_error(state.pos()), state)) - } - } -} - -#[macro_export] -macro_rules! word1_check_indent { - ($word:expr, $word_problem:expr, $min_indent:expr, $indent_problem:expr) => { - and!( - word1($word, $word_problem), - crate::parser::check_indent($min_indent, $indent_problem) - ) - }; -} - -#[macro_export] -macro_rules! map { - ($parser:expr, $transform:expr) => { - move |arena, state| { - $parser - .parse(arena, state) - .map(|(progress, output, next_state)| (progress, $transform(output), next_state)) - } - }; -} - -#[macro_export] -macro_rules! map_with_arena { - ($parser:expr, $transform:expr) => { - move |arena, state| { - $parser - .parse(arena, state) - .map(|(progress, output, next_state)| { - (progress, $transform(arena, output), next_state) - }) - } - }; -} - -#[macro_export] -macro_rules! zero_or_more { - ($parser:expr) => { - move |arena, state: State<'a>| { - use bumpalo::collections::Vec; - - let start_bytes_len = state.bytes().len(); - - match $parser.parse(arena, state) { - Ok((_, first_output, next_state)) => { - let mut state = next_state; - let mut buf = Vec::with_capacity_in(1, arena); - - buf.push(first_output); - - loop { - match $parser.parse(arena, state) { - Ok((_, next_output, next_state)) => { - state = next_state; - buf.push(next_output); - } - Err((fail_progress, fail, old_state)) => { - match fail_progress { - MadeProgress => { - // made progress on an element and then failed; that's an error - return Err((MadeProgress, fail, old_state)); - } - NoProgress => { - // the next element failed with no progress - // report whether we made progress before - let progress = Progress::from_lengths(start_bytes_len, old_state.bytes().len()); - return Ok((progress, buf, old_state)); - } - } - } - } - } - } - Err((fail_progress, fail, new_state)) => { - match fail_progress { - MadeProgress => { - // made progress on an element and then failed; that's an error - Err((MadeProgress, fail, new_state)) - } - NoProgress => { - // the first element failed (with no progress), but that's OK - // because we only need to parse 0 elements - Ok((NoProgress, Vec::new_in(arena), new_state)) - } - } - } - } - } - }; -} - -#[macro_export] -macro_rules! one_or_more { - ($parser:expr, $to_error:expr) => { - move |arena, state: State<'a>| { - use bumpalo::collections::Vec; - - match $parser.parse(arena, state) { - Ok((_, first_output, next_state)) => { - let mut state = next_state; - let mut buf = Vec::with_capacity_in(1, arena); - - buf.push(first_output); - - loop { - match $parser.parse(arena, state) { - Ok((_, next_output, next_state)) => { - state = next_state; - buf.push(next_output); - } - Err((NoProgress, _, old_state)) => { - return Ok((MadeProgress, buf, old_state)); - } - Err((MadeProgress, fail, old_state)) => { - return Err((MadeProgress, fail, old_state)); - } - } - } - } - Err((progress, _, new_state)) => { - Err((progress, $to_error(new_state.pos), new_state)) - } - } - } - }; -} - -#[macro_export] -macro_rules! debug { - ($parser:expr) => { - move |arena, state: $crate::state::State<'a>| dbg!($parser.parse(arena, state)) - }; -} - -#[macro_export] -macro_rules! either { - ($p1:expr, $p2:expr) => { - move |arena: &'a bumpalo::Bump, state: $crate::state::State<'a>| match $p1 - .parse(arena, state) - { - Ok((progress, output, state)) => { - Ok((progress, $crate::parser::Either::First(output), state)) - } - Err((NoProgress, _, state)) => match $p2.parse(arena, state) { - Ok((progress, output, state)) => { - Ok((progress, $crate::parser::Either::Second(output), state)) - } - Err((progress, fail, state)) => Err((progress, fail, state)), - }, - Err((MadeProgress, fail, state)) => Err((MadeProgress, fail, state)), - } - }; -} - -/// Parse everything between two braces (e.g. parentheses), skipping both braces -/// and keeping only whatever was parsed in between them. -#[macro_export] -macro_rules! between { - ($opening_brace:expr, $parser:expr, $closing_brace:expr) => { - skip_first!($opening_brace, skip_second!($parser, $closing_brace)) - }; -} - -/// For some reason, some usages won't compile unless they use this instead of the macro version -#[inline(always)] -pub fn and<'a, P1, P2, A, B, E>(p1: P1, p2: P2) -> impl Parser<'a, (A, B), E> -where - P1: Parser<'a, A, E>, - P2: Parser<'a, B, E>, - P1: 'a, - P2: 'a, - A: 'a, - B: 'a, - E: 'a, -{ - and!(p1, p2) -} - -/// For some reason, some usages won't compile unless they use this instead of the macro version -#[inline(always)] -pub fn loc<'a, P, Val, Error>(parser: P) -> impl Parser<'a, Loc, Error> -where - P: Parser<'a, Val, Error>, - Error: 'a, -{ - loc!(parser) -} - -/// For some reason, some usages won't compile unless they use this instead of the macro version -#[inline(always)] -pub fn map_with_arena<'a, P, F, Before, After, E>( - parser: P, - transform: F, -) -> impl Parser<'a, After, E> -where - P: Parser<'a, Before, E>, - P: 'a, - F: Fn(&'a Bump, Before) -> After, - F: 'a, - Before: 'a, - After: 'a, - E: 'a, -{ - map_with_arena!(parser, transform) -} - -pub fn backtrackable<'a, P, Val, Error>(parser: P) -> impl Parser<'a, Val, Error> -where - P: Parser<'a, Val, Error>, - Error: 'a, -{ - move |arena: &'a Bump, state: State<'a>| { - let old_state = state.clone(); - - match parser.parse(arena, state) { - Ok((_, a, s1)) => Ok((NoProgress, a, s1)), - Err((_, f, _)) => Err((NoProgress, f, old_state)), - } - } -} diff --git a/compiler/parse/src/pattern.rs b/compiler/parse/src/pattern.rs deleted file mode 100644 index df09c1cb73..0000000000 --- a/compiler/parse/src/pattern.rs +++ /dev/null @@ -1,472 +0,0 @@ -use crate::ast::{Has, Pattern}; -use crate::blankspace::{space0_around_ee, space0_before_e, space0_e}; -use crate::ident::{lowercase_ident, parse_ident, Ident}; -use crate::parser::Progress::{self, *}; -use crate::parser::{ - backtrackable, optional, specialize, specialize_ref, then, word1, EPattern, PInParens, PRecord, - ParseResult, Parser, -}; -use crate::state::State; -use bumpalo::collections::string::String; -use bumpalo::collections::Vec; -use bumpalo::Bump; -use roc_region::all::{Loc, Region}; - -/// Different patterns are supported in different circumstances. -/// For example, when branches can pattern match on number literals, but -/// assignments and function args can't. Underscore is supported in function -/// arg patterns and in when branch patterns, but not in assignments. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum PatternType { - TopLevelDef, - DefExpr, - FunctionArg, - WhenBranch, -} - -pub fn loc_closure_param<'a>(min_indent: u32) -> impl Parser<'a, Loc>, EPattern<'a>> { - move |arena, state| parse_closure_param(arena, state, min_indent) -} - -fn parse_closure_param<'a>( - arena: &'a Bump, - state: State<'a>, - min_indent: u32, -) -> ParseResult<'a, Loc>, EPattern<'a>> { - one_of!( - // An ident is the most common param, e.g. \foo -> ... - loc_ident_pattern_help(min_indent, true), - // Underscore is also common, e.g. \_ -> ... - loc!(underscore_pattern_help()), - // You can destructure records in params, e.g. \{ x, y } -> ... - loc!(specialize( - EPattern::Record, - crate::pattern::record_pattern_help(min_indent) - )), - // If you wrap it in parens, you can match any arbitrary pattern at all. - // e.g. \User.UserId userId -> ... - specialize(EPattern::PInParens, loc_pattern_in_parens_help(min_indent)) - ) - .parse(arena, state) -} - -pub fn loc_pattern_help<'a>(min_indent: u32) -> impl Parser<'a, Loc>, EPattern<'a>> { - one_of!( - specialize(EPattern::PInParens, loc_pattern_in_parens_help(min_indent)), - loc!(underscore_pattern_help()), - loc_ident_pattern_help(min_indent, true), - loc!(specialize( - EPattern::Record, - crate::pattern::record_pattern_help(min_indent) - )), - loc!(number_pattern_help()), - loc!(string_pattern_help()), - loc!(single_quote_pattern_help()), - ) -} - -fn loc_tag_pattern_args_help<'a>( - min_indent: u32, -) -> impl Parser<'a, Vec<'a, Loc>>, EPattern<'a>> { - zero_or_more!(loc_tag_pattern_arg(min_indent, false)) -} - -/// Like `loc_tag_pattern_args_help`, but stops if a "has" keyword is seen (indicating an ability). -fn loc_type_def_tag_pattern_args_help<'a>( - min_indent: u32, -) -> impl Parser<'a, Vec<'a, Loc>>, EPattern<'a>> { - zero_or_more!(loc_tag_pattern_arg(min_indent, true)) -} - -fn loc_tag_pattern_arg<'a>( - min_indent: u32, - stop_on_has_kw: bool, -) -> impl Parser<'a, Loc>, EPattern<'a>> { - // Don't parse operators, because they have a higher precedence than function application. - // If we encounter one, we're done parsing function args! - move |arena, original_state: State<'a>| { - let (_, spaces, state) = backtrackable(space0_e(min_indent, EPattern::IndentStart)) - .parse(arena, original_state.clone())?; - - let (_, loc_pat, state) = loc_parse_tag_pattern_arg(min_indent, arena, state)?; - - let Loc { region, value } = loc_pat; - - if stop_on_has_kw && matches!(value, Pattern::Identifier("has")) { - Err(( - NoProgress, - EPattern::End(original_state.pos()), - original_state, - )) - } else { - Ok(( - MadeProgress, - if spaces.is_empty() { - Loc::at(region, value) - } else { - Loc::at(region, Pattern::SpaceBefore(arena.alloc(value), spaces)) - }, - state, - )) - } - } -} - -pub fn loc_has_parser<'a>(min_indent: u32) -> impl Parser<'a, Loc>, EPattern<'a>> { - then( - loc_tag_pattern_arg(min_indent, false), - |_arena, state, progress, pattern| { - if matches!(pattern.value, Pattern::Identifier("has")) { - Ok((progress, Loc::at(pattern.region, Has::Has), state)) - } else { - Err((progress, EPattern::End(state.pos()), state)) - } - }, - ) -} - -fn loc_parse_tag_pattern_arg<'a>( - min_indent: u32, - arena: &'a Bump, - state: State<'a>, -) -> ParseResult<'a, Loc>, EPattern<'a>> { - one_of!( - specialize(EPattern::PInParens, loc_pattern_in_parens_help(min_indent)), - loc!(underscore_pattern_help()), - // Make sure `Foo Bar 1` is parsed as `Foo (Bar) 1`, and not `Foo (Bar 1)` - loc_ident_pattern_help(min_indent, false), - loc!(specialize( - EPattern::Record, - crate::pattern::record_pattern_help(min_indent) - )), - loc!(string_pattern_help()), - loc!(single_quote_pattern_help()), - loc!(number_pattern_help()) - ) - .parse(arena, state) -} - -fn loc_pattern_in_parens_help<'a>( - min_indent: u32, -) -> impl Parser<'a, Loc>, PInParens<'a>> { - between!( - word1(b'(', PInParens::Open), - space0_around_ee( - move |arena, state| specialize_ref(PInParens::Pattern, loc_pattern_help(min_indent)) - .parse(arena, state), - min_indent, - PInParens::IndentOpen, - PInParens::IndentEnd, - ), - word1(b')', PInParens::End) - ) -} - -fn number_pattern_help<'a>() -> impl Parser<'a, Pattern<'a>, EPattern<'a>> { - specialize( - EPattern::NumLiteral, - map!(crate::number_literal::number_literal(), |literal| { - use crate::number_literal::NumLiteral::*; - - match literal { - Num(s) => Pattern::NumLiteral(s), - Float(s) => Pattern::FloatLiteral(s), - NonBase10Int { - string, - base, - is_negative, - } => Pattern::NonBase10Literal { - string, - base, - is_negative, - }, - } - }), - ) -} - -fn string_pattern_help<'a>() -> impl Parser<'a, Pattern<'a>, EPattern<'a>> { - specialize( - |_, pos| EPattern::Start(pos), - map!(crate::string_literal::parse(), Pattern::StrLiteral), - ) -} - -fn single_quote_pattern_help<'a>() -> impl Parser<'a, Pattern<'a>, EPattern<'a>> { - specialize( - |_, pos| EPattern::Start(pos), - map!( - crate::string_literal::parse_single_quote(), - Pattern::SingleQuote - ), - ) -} - -fn loc_ident_pattern_help<'a>( - min_indent: u32, - can_have_arguments: bool, -) -> impl Parser<'a, Loc>, EPattern<'a>> { - move |arena: &'a Bump, state: State<'a>| { - let original_state = state.clone(); - - let (_, loc_ident, state) = - specialize(|_, pos| EPattern::Start(pos), loc!(parse_ident)).parse(arena, state)?; - - match loc_ident.value { - Ident::Tag(tag) => { - let loc_tag = Loc { - region: loc_ident.region, - value: Pattern::Tag(tag), - }; - - // Make sure `Foo Bar 1` is parsed as `Foo (Bar) 1`, and not `Foo (Bar 1)` - if can_have_arguments { - let (_, loc_args, state) = - loc_type_def_tag_pattern_args_help(min_indent).parse(arena, state)?; - - if loc_args.is_empty() { - Ok((MadeProgress, loc_tag, state)) - } else { - let region = Region::across_all( - std::iter::once(&loc_ident.region) - .chain(loc_args.iter().map(|loc_arg| &loc_arg.region)), - ); - let value = - Pattern::Apply(&*arena.alloc(loc_tag), loc_args.into_bump_slice()); - - Ok((MadeProgress, Loc { region, value }, state)) - } - } else { - Ok((MadeProgress, loc_tag, state)) - } - } - Ident::OpaqueRef(name) => { - let loc_pat = Loc { - region: loc_ident.region, - value: Pattern::OpaqueRef(name), - }; - - // Make sure `@Foo Bar 1` is parsed as `@Foo (Bar) 1`, and not `@Foo (Bar 1)` - if can_have_arguments { - let (_, loc_args, state) = - loc_tag_pattern_args_help(min_indent).parse(arena, state)?; - - if loc_args.is_empty() { - Ok((MadeProgress, loc_pat, state)) - } else { - let region = Region::across_all( - std::iter::once(&loc_ident.region) - .chain(loc_args.iter().map(|loc_arg| &loc_arg.region)), - ); - let value = - Pattern::Apply(&*arena.alloc(loc_pat), loc_args.into_bump_slice()); - - Ok((MadeProgress, Loc { region, value }, state)) - } - } else { - Ok((MadeProgress, loc_pat, state)) - } - } - Ident::Access { module_name, parts } => { - // Plain identifiers (e.g. `foo`) are allowed in patterns, but - // more complex ones (e.g. `Foo.bar` or `foo.bar.baz`) are not. - if crate::keyword::KEYWORDS.contains(&parts[0]) { - Err(( - NoProgress, - EPattern::End(original_state.pos()), - original_state, - )) - } else if module_name.is_empty() && parts.len() == 1 { - Ok(( - MadeProgress, - Loc { - region: loc_ident.region, - value: Pattern::Identifier(parts[0]), - }, - state, - )) - } else { - let malformed_str = if module_name.is_empty() { - parts.join(".") - } else { - format!("{}.{}", module_name, parts.join(".")) - }; - Ok(( - MadeProgress, - Loc { - region: loc_ident.region, - value: Pattern::Malformed( - String::from_str_in(&malformed_str, arena).into_bump_str(), - ), - }, - state, - )) - } - } - Ident::AccessorFunction(string) => Ok(( - MadeProgress, - Loc { - region: loc_ident.region, - value: Pattern::Malformed(string), - }, - state, - )), - Ident::Malformed(malformed, problem) => { - debug_assert!(!malformed.is_empty()); - - Ok(( - MadeProgress, - Loc { - region: loc_ident.region, - value: Pattern::MalformedIdent(malformed, problem), - }, - state, - )) - } - } - } -} - -fn underscore_pattern_help<'a>() -> impl Parser<'a, Pattern<'a>, EPattern<'a>> { - move |arena: &'a Bump, state: State<'a>| { - let (_, _, next_state) = word1(b'_', EPattern::Underscore).parse(arena, state)?; - - let (_, output, final_state) = - optional(lowercase_ident_pattern).parse(arena, next_state)?; - - match output { - Some(name) => Ok((MadeProgress, Pattern::Underscore(name), final_state)), - None => Ok((MadeProgress, Pattern::Underscore(""), final_state)), - } - } -} - -fn lowercase_ident_pattern<'a>( - arena: &'a Bump, - state: State<'a>, -) -> ParseResult<'a, &'a str, EPattern<'a>> { - let pos = state.pos(); - - specialize(move |_, _| EPattern::End(pos), lowercase_ident()).parse(arena, state) -} - -#[inline(always)] -fn record_pattern_help<'a>(min_indent: u32) -> impl Parser<'a, Pattern<'a>, PRecord<'a>> { - move |arena, state| { - let (_, fields, state) = collection_trailing_sep_e!( - // word1_check_indent!(b'{', PRecord::Open, min_indent, PRecord::IndentOpen), - word1(b'{', PRecord::Open), - record_pattern_field(min_indent), - word1(b',', PRecord::End), - // word1_check_indent!(b'}', PRecord::End, min_indent, PRecord::IndentEnd), - word1(b'}', PRecord::End), - min_indent, - PRecord::Open, - PRecord::IndentEnd, - Pattern::SpaceBefore - ) - .parse(arena, state)?; - - let result = Pattern::RecordDestructure(fields); - - Ok((MadeProgress, result, state)) - } -} - -fn record_pattern_field<'a>(min_indent: u32) -> impl Parser<'a, Loc>, PRecord<'a>> { - use crate::parser::Either::*; - - move |arena, state: State<'a>| { - // You must have a field name, e.g. "email" - // using the initial pos is important for error reporting - let pos = state.pos(); - let (progress, loc_label, state) = loc!(specialize( - move |_, _| PRecord::Field(pos), - lowercase_ident() - )) - .parse(arena, state)?; - debug_assert_eq!(progress, MadeProgress); - - let (_, spaces, state) = space0_e(min_indent, PRecord::IndentEnd).parse(arena, state)?; - - // Having a value is optional; both `{ email }` and `{ email: blah }` work. - // (This is true in both literals and types.) - let (_, opt_loc_val, state) = optional(either!( - word1(b':', PRecord::Colon), - word1(b'?', PRecord::Optional) - )) - .parse(arena, state)?; - - match opt_loc_val { - Some(First(_)) => { - let val_parser = specialize_ref(PRecord::Pattern, loc_pattern_help(min_indent)); - let (_, loc_val, state) = - space0_before_e(val_parser, min_indent, PRecord::IndentColon) - .parse(arena, state)?; - - let Loc { - value: label, - region, - } = loc_label; - - let region = Region::span_across(®ion, &loc_val.region); - - Ok(( - MadeProgress, - Loc::at( - region, - Pattern::RequiredField( - label, - // TODO spaces are dropped here - // arena.alloc(arena.alloc(value).with_spaces_before(spaces, region)), - arena.alloc(loc_val), - ), - ), - state, - )) - } - Some(Second(_)) => { - let val_parser = specialize_ref(PRecord::Expr, move |a, s| { - crate::expr::parse_loc_expr_no_multi_backpassing(min_indent, a, s) - }); - - let (_, loc_val, state) = - space0_before_e(val_parser, min_indent, PRecord::IndentColon) - .parse(arena, state)?; - - let Loc { - value: label, - region, - } = loc_label; - - let region = Region::span_across(®ion, &loc_val.region); - - Ok(( - MadeProgress, - Loc::at( - region, - Pattern::OptionalField( - label, - // TODO spaces are dropped - // arena.alloc(arena.alloc(value).with_spaces_before(spaces, region)), - arena.alloc(loc_val), - ), - ), - state, - )) - } - // If no value was provided, record it as a Var. - // Canonicalize will know what to do with a Var later. - None => { - let Loc { value, region } = loc_label; - let value = if !spaces.is_empty() { - Pattern::SpaceAfter(arena.alloc(Pattern::Identifier(value)), spaces) - } else { - Pattern::Identifier(value) - }; - - Ok((MadeProgress, Loc::at(region, value), state)) - } - } - } -} diff --git a/compiler/parse/src/state.rs b/compiler/parse/src/state.rs deleted file mode 100644 index 4e936d7589..0000000000 --- a/compiler/parse/src/state.rs +++ /dev/null @@ -1,101 +0,0 @@ -use roc_region::all::{Position, Region}; -use std::fmt; - -/// A position in a source file. -#[derive(Clone)] -pub struct State<'a> { - /// The raw input bytes from the file. - /// Beware: original_bytes[0] always points the the start of the file. - /// Use bytes()[0] to access the current byte the parser is inspecting - original_bytes: &'a [u8], - - /// Offset in original_bytes that the parser is currently inspecting - offset: usize, - - /// Position of the start of the current line - pub(crate) line_start: Position, - - /// Current indentation level, in columns - /// (so no indent is col 1 - this saves an arithmetic operation.) - pub(crate) indent_column: u32, -} - -impl<'a> State<'a> { - pub fn new(bytes: &'a [u8]) -> State<'a> { - State { - original_bytes: bytes, - offset: 0, - line_start: Position::zero(), - indent_column: 0, - } - } - - pub fn original_bytes(&self) -> &'a [u8] { - self.original_bytes - } - - pub(crate) fn bytes(&self) -> &'a [u8] { - &self.original_bytes[self.offset..] - } - - pub fn column(&self) -> u32 { - self.pos().offset - self.line_start.offset - } - - #[must_use] - #[inline(always)] - pub(crate) const fn advance(mut self, offset: usize) -> State<'a> { - self.offset += offset; - self - } - - #[must_use] - #[inline(always)] - pub(crate) const fn advance_newline(mut self) -> State<'a> { - self.offset += 1; - self.line_start = self.pos(); - self - } - - /// Returns the current position - pub const fn pos(&self) -> Position { - Position::new(self.offset as u32) - } - - /// Returns whether the parser has reached the end of the input - pub const fn has_reached_end(&self) -> bool { - self.offset == self.original_bytes.len() - } - - /// Returns a Region corresponding to the current state, but - /// with the the end column advanced by the given amount. This is - /// useful when parsing something "manually" (using input.chars()) - /// and thus wanting a Region while not having access to loc(). - pub fn len_region(&self, length: u32) -> Region { - Region::new(self.pos(), self.pos().bump_column(length)) - } -} - -impl<'a> fmt::Debug for State<'a> { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "State {{")?; - - match std::str::from_utf8(self.bytes()) { - Ok(string) => write!(f, "\n\tbytes: [utf8] {:?}", string)?, - Err(_) => write!(f, "\n\tbytes: [invalid utf8] {:?}", self.bytes())?, - } - - write!(f, "\n\t(offset): {:?},", self.pos())?; - write!(f, "\n\tindent_column: {}", self.indent_column)?; - write!(f, "\n}}") - } -} - -#[test] -fn state_size() { - // State should always be under 8 machine words, so it fits in a typical - // cache line. - let state_size = std::mem::size_of::(); - let maximum = std::mem::size_of::() * 8; - assert!(state_size <= maximum, "{:?} <= {:?}", state_size, maximum); -} diff --git a/compiler/parse/src/string_literal.rs b/compiler/parse/src/string_literal.rs deleted file mode 100644 index 4d77dc523f..0000000000 --- a/compiler/parse/src/string_literal.rs +++ /dev/null @@ -1,396 +0,0 @@ -use crate::ast::{EscapedChar, StrLiteral, StrSegment}; -use crate::expr; -use crate::parser::Progress::*; -use crate::parser::{allocated, loc, specialize_ref, word1, BadInputError, EString, Parser}; -use crate::state::State; -use bumpalo::collections::vec::Vec; -use bumpalo::Bump; - -/// One or more ASCII hex digits. (Useful when parsing unicode escape codes, -/// which must consist entirely of ASCII hex digits.) -fn ascii_hex_digits<'a>() -> impl Parser<'a, &'a str, EString<'a>> { - move |arena, state: State<'a>| { - let mut buf = bumpalo::collections::String::new_in(arena); - - for &byte in state.bytes().iter() { - if (byte as char).is_ascii_hexdigit() { - buf.push(byte as char); - } else if buf.is_empty() { - // We didn't find any hex digits! - return Err((NoProgress, EString::CodePtEnd(state.pos()), state)); - } else { - let state = state.advance(buf.len()); - - return Ok((MadeProgress, buf.into_bump_str(), state)); - } - } - - Err((NoProgress, EString::CodePtEnd(state.pos()), state)) - } -} - -macro_rules! advance_state { - ($state:expr, $n:expr) => { - Ok($state.advance($n)) - }; -} - -pub fn parse_single_quote<'a>() -> impl Parser<'a, &'a str, EString<'a>> { - move |arena: &'a Bump, mut state: State<'a>| { - if state.bytes().starts_with(b"\'") { - // we will be parsing a single-quote-string - } else { - return Err((NoProgress, EString::Open(state.pos()), state)); - } - - // early return did not hit, just advance one byte - state = advance_state!(state, 1)?; - - // Handle back slaches in byte literal - // - starts with a backslash and used as an escape character. ex: '\n', '\t' - // - single quote floating (un closed single quote) should be an error - match state.bytes().first() { - Some(b'\\') => { - state = advance_state!(state, 1)?; - match state.bytes().first() { - Some(&ch) => { - state = advance_state!(state, 1)?; - if (ch == b'n' || ch == b'r' || ch == b't' || ch == b'\'' || ch == b'\\') - && (state.bytes().first() == Some(&b'\'')) - { - state = advance_state!(state, 1)?; - let test = match ch { - b'n' => '\n', - b't' => '\t', - b'r' => '\r', - // since we checked the current char between the single quotes we - // know they are valid UTF-8, allowing us to use 'from_u32_unchecked' - _ => unsafe { char::from_u32_unchecked(ch as u32) }, - }; - - return Ok((MadeProgress, &*arena.alloc_str(&test.to_string()), state)); - } - // invalid error, backslah escaping something we do not recognize - return Err((NoProgress, EString::CodePtEnd(state.pos()), state)); - } - None => { - // no close quote found - return Err((NoProgress, EString::CodePtEnd(state.pos()), state)); - } - } - } - Some(_) => { - // do nothing for other characters, handled below - } - None => return Err((NoProgress, EString::CodePtEnd(state.pos()), state)), - } - - let mut bytes = state.bytes().iter(); - let mut end_index = 1; - - // Copy paste problem in mono - - loop { - match bytes.next() { - Some(b'\'') => { - break; - } - Some(_) => end_index += 1, - None => { - return Err((NoProgress, EString::Open(state.pos()), state)); - } - } - } - - if end_index == 1 { - // no progress was made - // this case is a double single quote, ex: '' - // not supporting empty single quotes - return Err((NoProgress, EString::Open(state.pos()), state)); - } - - if end_index > (std::mem::size_of::() + 1) { - // bad case: too big to fit into u32 - return Err((NoProgress, EString::Open(state.pos()), state)); - } - - // happy case -> we have some bytes that will fit into a u32 - // ending up w/ a slice of bytes that we want to convert into an integer - let raw_bytes = &state.bytes()[0..end_index - 1]; - - state = advance_state!(state, end_index)?; - match std::str::from_utf8(raw_bytes) { - Ok(string) => Ok((MadeProgress, string, state)), - Err(_) => { - // invalid UTF-8 - return Err((NoProgress, EString::CodePtEnd(state.pos()), state)); - } - } - } -} - -pub fn parse<'a>() -> impl Parser<'a, StrLiteral<'a>, EString<'a>> { - use StrLiteral::*; - - move |arena: &'a Bump, mut state: State<'a>| { - let is_multiline; - let mut bytes; - - if state.bytes().starts_with(b"\"\"\"") { - // we will be parsing a multi-string - is_multiline = true; - bytes = state.bytes()[3..].iter(); - state = advance_state!(state, 3)?; - } else if state.bytes().starts_with(b"\"") { - // we will be parsing a single-string - is_multiline = false; - bytes = state.bytes()[1..].iter(); - state = advance_state!(state, 1)?; - } else { - return Err((NoProgress, EString::Open(state.pos()), state)); - } - - // At the parsing stage we keep the entire raw string, because the formatter - // needs the raw string. (For example, so it can "remember" whether you - // wrote \u{...} or the actual unicode character itself.) - // - // Since we're keeping the entire raw string, all we need to track is - // how many characters we've parsed. So far, that's 1 (the opening `"`). - let mut segment_parsed_bytes = 0; - let mut segments = Vec::new_in(arena); - - macro_rules! escaped_char { - ($ch:expr) => { - // Record the escaped char. - segments.push(StrSegment::EscapedChar($ch)); - - // Advance past the segment we just added - state = advance_state!(state, segment_parsed_bytes)?; - - // Reset the segment - segment_parsed_bytes = 0; - }; - } - - macro_rules! end_segment { - ($transform:expr) => { - // Don't push anything if the string would be empty. - if segment_parsed_bytes > 1 { - // This function is always called after we just parsed - // something which signalled that we should end the - // current segment - so use segment_parsed_bytes - 1 here, - // to exclude that char we just parsed. - let string_bytes = &state.bytes()[0..(segment_parsed_bytes - 1)]; - - match std::str::from_utf8(string_bytes) { - Ok(string) => { - state = advance_state!(state, string.len())?; - - segments.push($transform(string)); - } - Err(_) => { - return Err(( - MadeProgress, - EString::Space(BadInputError::BadUtf8, state.pos()), - state, - )); - } - } - } - - // Depending on where this macro is used, in some - // places this is unused. - #[allow(unused_assignments)] - { - // This function is always called after we just parsed - // something which signalled that we should end the - // current segment. - segment_parsed_bytes = 1; - } - }; - } - - while let Some(&byte) = bytes.next() { - // This is for the byte we just grabbed from the iterator. - segment_parsed_bytes += 1; - - match byte { - b'"' => { - if segment_parsed_bytes == 1 && segments.is_empty() { - // special case of the empty string - if is_multiline { - if bytes.as_slice().starts_with(b"\"\"") { - return Ok((MadeProgress, Block(&[]), advance_state!(state, 3)?)); - } else { - // this quote is in a block string - continue; - } - } else { - // This is the end of the string! - // Advance 1 for the close quote - return Ok((MadeProgress, PlainLine(""), advance_state!(state, 1)?)); - } - } else { - // the string is non-empty, which means we need to convert any previous segments - // and the current segment into a string literal - if is_multiline { - if bytes.as_slice().starts_with(b"\"\"") { - end_segment!(StrSegment::Plaintext); - - let expr = if segments.len() == 1 { - // We had exactly one segment, so this is a candidate - // to be StrLiteral::Plaintext - match segments.pop().unwrap() { - StrSegment::Plaintext(string) => { - StrLiteral::PlainLine(string) - } - other => StrLiteral::Line(arena.alloc([other])), - } - } else { - Block(arena.alloc([segments.into_bump_slice()])) - }; - - return Ok((MadeProgress, expr, advance_state!(state, 3)?)); - } else { - // this quote is in a block string - continue; - } - } else { - end_segment!(StrSegment::Plaintext); - - let expr = if segments.len() == 1 { - // We had exactly one segment, so this is a candidate - // to be StrLiteral::Plaintext - match segments.pop().unwrap() { - StrSegment::Plaintext(string) => StrLiteral::PlainLine(string), - other => StrLiteral::Line(arena.alloc([other])), - } - } else { - Line(segments.into_bump_slice()) - }; - - // Advance the state 1 to account for the closing `"` - return Ok((MadeProgress, expr, advance_state!(state, 1)?)); - } - }; - } - b'\n' => { - if is_multiline { - continue; - } else { - // This is a single-line string, which cannot have newlines! - // Treat this as an unclosed string literal, and consume - // all remaining chars. This will mask all other errors, but - // it should make it easiest to debug; the file will be a giant - // error starting from where the open quote appeared. - return Err((MadeProgress, EString::EndlessSingle(state.pos()), state)); - } - } - b'\\' => { - // We're about to begin an escaped segment of some sort! - // - // Record the current segment so we can begin a new one. - // End it right before the `\` char we just parsed. - end_segment!(StrSegment::Plaintext); - - // This is for the byte we're about to parse. - segment_parsed_bytes += 1; - - // This is the start of a new escape. Look at the next byte - // to figure out what type of escape it is. - match bytes.next() { - Some(b'(') => { - // Advance past the `\(` before using the expr parser - state = advance_state!(state, 2)?; - - let original_byte_count = state.bytes().len(); - - // This is an interpolated variable. - // Parse an arbitrary expression, then give a - // canonicalization error if that expression variant - // is not allowed inside a string interpolation. - let (_progress, loc_expr, new_state) = skip_second!( - specialize_ref(EString::Format, loc(allocated(expr::expr_help(0)))), - word1(b')', EString::FormatEnd) - ) - .parse(arena, state)?; - - // Advance the iterator past the expr we just parsed. - for _ in 0..(original_byte_count - new_state.bytes().len()) { - bytes.next(); - } - - segments.push(StrSegment::Interpolated(loc_expr)); - - // Reset the segment - segment_parsed_bytes = 0; - state = new_state; - } - Some(b'u') => { - // Advance past the `\u` before using the expr parser - state = advance_state!(state, 2)?; - - let original_byte_count = state.bytes().len(); - - // Parse the hex digits, surrounded by parens, then - // give a canonicalization error if the digits form - // an invalid unicode code point. - let (_progress, loc_digits, new_state) = between!( - word1(b'(', EString::CodePtOpen), - loc(ascii_hex_digits()), - word1(b')', EString::CodePtEnd) - ) - .parse(arena, state)?; - - // Advance the iterator past the expr we just parsed. - for _ in 0..(original_byte_count - new_state.bytes().len()) { - bytes.next(); - } - - segments.push(StrSegment::Unicode(loc_digits)); - - // Reset the segment - segment_parsed_bytes = 0; - state = new_state; - } - Some(b'\\') => { - escaped_char!(EscapedChar::Backslash); - } - Some(b'"') => { - escaped_char!(EscapedChar::Quote); - } - Some(b'r') => { - escaped_char!(EscapedChar::CarriageReturn); - } - Some(b't') => { - escaped_char!(EscapedChar::Tab); - } - Some(b'n') => { - escaped_char!(EscapedChar::Newline); - } - _ => { - // Invalid escape! A backslash must be followed - // by either an open paren or else one of the - // escapable characters (\n, \t, \", \\, etc) - return Err((MadeProgress, EString::UnknownEscape(state.pos()), state)); - } - } - } - _ => { - // All other characters need no special handling. - } - } - } - - // We ran out of characters before finding a closed quote - Err(( - MadeProgress, - if is_multiline { - EString::EndlessMulti(state.pos()) - } else { - EString::EndlessSingle(state.pos()) - }, - state, - )) - } -} diff --git a/compiler/parse/src/test_helpers.rs b/compiler/parse/src/test_helpers.rs deleted file mode 100644 index 0128465506..0000000000 --- a/compiler/parse/src/test_helpers.rs +++ /dev/null @@ -1,45 +0,0 @@ -use crate::ast; -use crate::ast::Def; -use crate::module::module_defs_help; -// use crate::module::module_defs; -use crate::parser::Parser; -use crate::parser::SourceError; -use crate::parser::SyntaxError; -use crate::state::State; -use bumpalo::Bump; -use roc_region::all::Loc; -use roc_region::all::Position; - -pub fn parse_expr_with<'a>( - arena: &'a Bump, - input: &'a str, -) -> Result, SyntaxError<'a>> { - parse_loc_with(arena, input) - .map(|loc_expr| loc_expr.value) - .map_err(|e| e.problem) -} - -#[allow(dead_code)] -pub fn parse_loc_with<'a>( - arena: &'a Bump, - input: &'a str, -) -> Result>, SourceError<'a, SyntaxError<'a>>> { - let state = State::new(input.trim().as_bytes()); - - match crate::expr::test_parse_expr(0, arena, state.clone()) { - Ok(loc_expr) => Ok(loc_expr), - Err(fail) => Err(SyntaxError::Expr(fail, Position::default()).into_source_error(&state)), - } -} - -pub fn parse_defs_with<'a>( - arena: &'a Bump, - input: &'a str, -) -> Result>>, SyntaxError<'a>> { - let state = State::new(input.trim().as_bytes()); - - match module_defs_help().parse(arena, state) { - Ok(tuple) => Ok(tuple.1), - Err(tuple) => Err(tuple.1), - } -} diff --git a/compiler/parse/src/type_annotation.rs b/compiler/parse/src/type_annotation.rs deleted file mode 100644 index 35f9ae837f..0000000000 --- a/compiler/parse/src/type_annotation.rs +++ /dev/null @@ -1,686 +0,0 @@ -use crate::ast::{ - AssignedField, CommentOrNewline, Derived, HasClause, Pattern, Spaced, Tag, TypeAnnotation, - TypeHeader, -}; -use crate::blankspace::{space0_around_ee, space0_before_e, space0_e}; -use crate::ident::lowercase_ident; -use crate::keyword; -use crate::parser::then; -use crate::parser::{ - allocated, backtrackable, optional, specialize, specialize_ref, word1, word2, word3, EType, - ETypeApply, ETypeInParens, ETypeInlineAlias, ETypeRecord, ETypeTagUnion, ParseResult, Parser, - Progress::{self, *}, -}; -use crate::state::State; -use bumpalo::collections::vec::Vec; -use bumpalo::Bump; -use roc_region::all::{Loc, Position, Region}; - -pub fn located<'a>( - min_indent: u32, - is_trailing_comma_valid: bool, -) -> impl Parser<'a, Loc>, EType<'a>> { - expression(min_indent, is_trailing_comma_valid, false) -} - -pub fn located_opaque_signature<'a>( - min_indent: u32, - is_trailing_comma_valid: bool, -) -> impl Parser<'a, Loc>, EType<'a>> { - expression(min_indent, is_trailing_comma_valid, true) -} - -#[inline(always)] -fn tag_union_type<'a>( - min_indent: u32, - stop_at_surface_has: bool, -) -> impl Parser<'a, TypeAnnotation<'a>, ETypeTagUnion<'a>> { - move |arena, state| { - let (_, tags, state) = collection_trailing_sep_e!( - word1(b'[', ETypeTagUnion::Open), - loc!(tag_type(min_indent, false)), - word1(b',', ETypeTagUnion::End), - word1(b']', ETypeTagUnion::End), - min_indent, - ETypeTagUnion::Open, - ETypeTagUnion::IndentEnd, - Tag::SpaceBefore - ) - .parse(arena, state)?; - - // This could be an open tag union, e.g. `[Foo, Bar]a` - let (_, ext, state) = optional(allocated(specialize_ref( - ETypeTagUnion::Type, - term(min_indent, stop_at_surface_has), - ))) - .parse(arena, state)?; - - let result = TypeAnnotation::TagUnion { tags, ext }; - - Ok((MadeProgress, result, state)) - } -} - -fn check_type_alias( - p: Progress, - annot: Loc, -) -> impl Parser { - move |arena, state| match annot.value { - TypeAnnotation::Apply("", tag_name, vars) => { - let mut var_names = Vec::new_in(arena); - var_names.reserve(vars.len()); - for var in vars { - if let TypeAnnotation::BoundVariable(v) = var.value { - var_names.push(Loc::at(var.region, Pattern::Identifier(v))); - } else { - return Err(( - p, - ETypeInlineAlias::ArgumentNotLowercase(var.region.start()), - state, - )); - } - } - - let name_start = annot.region.start(); - let name_region = - Region::between(name_start, name_start.bump_column(tag_name.len() as u32)); - - let header = TypeHeader { - name: Loc::at(name_region, tag_name), - vars: var_names.into_bump_slice(), - }; - - Ok((p, header, state)) - } - TypeAnnotation::Apply(_, _, _) => { - Err((p, ETypeInlineAlias::Qualified(annot.region.start()), state)) - } - _ => Err((p, ETypeInlineAlias::NotAnAlias(annot.region.start()), state)), - } -} - -fn parse_type_alias_after_as<'a>(min_indent: u32) -> impl Parser<'a, TypeHeader<'a>, EType<'a>> { - move |arena, state| { - space0_before_e(term(min_indent, false), min_indent, EType::TAsIndentStart) - .parse(arena, state) - .and_then(|(p, annot, state)| { - specialize(EType::TInlineAlias, check_type_alias(p, annot)).parse(arena, state) - }) - } -} - -fn fail_type_start<'a, T: 'a>() -> impl Parser<'a, T, EType<'a>> { - |_arena, state: State<'a>| Err((NoProgress, EType::TStart(state.pos()), state)) -} - -fn term<'a>( - min_indent: u32, - stop_at_surface_has: bool, -) -> impl Parser<'a, Loc>, EType<'a>> { - map_with_arena!( - and!( - one_of!( - loc_wildcard(), - loc_inferred(), - specialize(EType::TInParens, loc_type_in_parens(min_indent)), - loc!(specialize( - EType::TRecord, - record_type(min_indent, stop_at_surface_has) - )), - loc!(specialize( - EType::TTagUnion, - tag_union_type(min_indent, stop_at_surface_has) - )), - loc!(applied_type(min_indent, stop_at_surface_has)), - loc!(parse_type_variable(stop_at_surface_has)), - fail_type_start(), - ), - // Inline alias notation, e.g. [Nil, Cons a (List a)] as List a - one_of![ - map!( - and!( - skip_second!( - backtrackable(space0_e(min_indent, EType::TIndentEnd)), - crate::parser::keyword_e(keyword::AS, EType::TEnd) - ), - parse_type_alias_after_as(min_indent) - ), - Some - ), - |_, state| Ok((NoProgress, None, state)) - ] - ), - |arena: &'a Bump, - (loc_ann, opt_as): (Loc>, Option<(&'a [_], TypeHeader<'a>)>)| { - match opt_as { - Some((spaces, alias)) => { - let alias_vars_region = - Region::across_all(alias.vars.iter().map(|v| &v.region)); - let region = Region::span_across(&loc_ann.region, &alias_vars_region); - let value = TypeAnnotation::As(arena.alloc(loc_ann), spaces, alias); - - Loc { region, value } - } - - None => loc_ann, - } - } - ) - .trace("type_annotation:term") -} - -/// The `*` type variable, e.g. in (List *) Wildcard, -fn loc_wildcard<'a>() -> impl Parser<'a, Loc>, EType<'a>> { - map!(loc!(word1(b'*', EType::TWildcard)), |loc_val: Loc<()>| { - loc_val.map(|_| TypeAnnotation::Wildcard) - }) -} - -/// The `_` indicating an inferred type, e.g. in (List _) -fn loc_inferred<'a>() -> impl Parser<'a, Loc>, EType<'a>> { - map!(loc!(word1(b'_', EType::TInferred)), |loc_val: Loc<()>| { - loc_val.map(|_| TypeAnnotation::Inferred) - }) -} - -fn loc_applied_arg<'a>( - min_indent: u32, - stop_at_surface_has: bool, -) -> impl Parser<'a, Loc>, EType<'a>> { - use crate::ast::Spaceable; - - map_with_arena!( - and!( - backtrackable(space0_e(min_indent, EType::TIndentStart)), - one_of!( - loc_wildcard(), - loc_inferred(), - specialize(EType::TInParens, loc_type_in_parens(min_indent)), - loc!(specialize( - EType::TRecord, - record_type(min_indent, stop_at_surface_has) - )), - loc!(specialize( - EType::TTagUnion, - tag_union_type(min_indent, stop_at_surface_has) - )), - loc!(specialize(EType::TApply, parse_concrete_type)), - loc!(parse_type_variable(stop_at_surface_has)) - ) - ), - |arena: &'a Bump, (spaces, argument): (&'a [_], Loc>)| { - if spaces.is_empty() { - argument - } else { - let Loc { region, value } = argument; - arena.alloc(value).with_spaces_before(spaces, region) - } - } - ) -} - -fn loc_type_in_parens<'a>( - min_indent: u32, -) -> impl Parser<'a, Loc>, ETypeInParens<'a>> { - between!( - word1(b'(', ETypeInParens::Open), - space0_around_ee( - move |arena, state| specialize_ref( - ETypeInParens::Type, - expression(min_indent, true, false) - ) - .parse(arena, state), - min_indent, - ETypeInParens::IndentOpen, - ETypeInParens::IndentEnd, - ), - word1(b')', ETypeInParens::IndentEnd) - ) -} - -#[inline(always)] -fn tag_type<'a>( - min_indent: u32, - stop_at_surface_has: bool, -) -> impl Parser<'a, Tag<'a>, ETypeTagUnion<'a>> { - move |arena, state: State<'a>| { - let (_, name, state) = loc!(parse_tag_name(ETypeTagUnion::End)).parse(arena, state)?; - - let (_, args, state) = specialize_ref( - ETypeTagUnion::Type, - loc_applied_args_e(min_indent, stop_at_surface_has), - ) - .parse(arena, state)?; - - let result = Tag::Apply { - name, - args: args.into_bump_slice(), - }; - - Ok((MadeProgress, result, state)) - } -} - -fn parse_tag_name<'a, F, E>(to_problem: F) -> impl Parser<'a, &'a str, E> -where - F: Fn(Position) -> E, - E: 'a, -{ - move |arena, state: State<'a>| match crate::ident::tag_name().parse(arena, state) { - Ok(good) => Ok(good), - Err((progress, _, state)) => Err((progress, to_problem(state.pos()), state)), - } -} - -fn record_type_field<'a>( - min_indent: u32, -) -> impl Parser<'a, AssignedField<'a, TypeAnnotation<'a>>, ETypeRecord<'a>> { - use crate::parser::Either::*; - use AssignedField::*; - - (move |arena, state: State<'a>| { - // You must have a field name, e.g. "email" - // using the initial pos is important for error reporting - let pos = state.pos(); - let (progress, loc_label, state) = loc!(specialize( - move |_, _| ETypeRecord::Field(pos), - lowercase_ident() - )) - .parse(arena, state)?; - debug_assert_eq!(progress, MadeProgress); - - let (_, spaces, state) = - space0_e(min_indent, ETypeRecord::IndentEnd).parse(arena, state)?; - - // Having a value is optional; both `{ email }` and `{ email: blah }` work. - // (This is true in both literals and types.) - let (_, opt_loc_val, state) = optional(either!( - word1(b':', ETypeRecord::Colon), - word1(b'?', ETypeRecord::Optional) - )) - .parse(arena, state)?; - - let val_parser = specialize_ref(ETypeRecord::Type, expression(min_indent, true, false)); - - match opt_loc_val { - Some(First(_)) => { - let (_, loc_val, state) = - space0_before_e(val_parser, min_indent, ETypeRecord::IndentColon) - .parse(arena, state)?; - - Ok(( - MadeProgress, - RequiredValue(loc_label, spaces, arena.alloc(loc_val)), - state, - )) - } - Some(Second(_)) => { - let (_, loc_val, state) = - space0_before_e(val_parser, min_indent, ETypeRecord::IndentOptional) - .parse(arena, state)?; - - Ok(( - MadeProgress, - OptionalValue(loc_label, spaces, arena.alloc(loc_val)), - state, - )) - } - // If no value was provided, record it as a Var. - // Canonicalize will know what to do with a Var later. - None => { - let value = if !spaces.is_empty() { - SpaceAfter(arena.alloc(LabelOnly(loc_label)), spaces) - } else { - LabelOnly(loc_label) - }; - - Ok((MadeProgress, value, state)) - } - } - }) - .trace("type_annotation:record_type_field") -} - -#[inline(always)] -fn record_type<'a>( - min_indent: u32, - stop_at_surface_has: bool, -) -> impl Parser<'a, TypeAnnotation<'a>, ETypeRecord<'a>> { - use crate::type_annotation::TypeAnnotation::*; - - (move |arena, state| { - let (_, fields, state) = collection_trailing_sep_e!( - // word1_check_indent!(b'{', TRecord::Open, min_indent, TRecord::IndentOpen), - word1(b'{', ETypeRecord::Open), - loc!(record_type_field(min_indent)), - word1(b',', ETypeRecord::End), - // word1_check_indent!(b'}', TRecord::End, min_indent, TRecord::IndentEnd), - word1(b'}', ETypeRecord::End), - min_indent, - ETypeRecord::Open, - ETypeRecord::IndentEnd, - AssignedField::SpaceBefore - ) - .parse(arena, state)?; - - let field_term = specialize_ref(ETypeRecord::Type, term(min_indent, stop_at_surface_has)); - let (_, ext, state) = optional(allocated(field_term)).parse(arena, state)?; - - let result = Record { fields, ext }; - - Ok((MadeProgress, result, state)) - }) - .trace("type_annotation:record_type") -} - -fn applied_type<'a>( - min_indent: u32, - stop_at_surface_has: bool, -) -> impl Parser<'a, TypeAnnotation<'a>, EType<'a>> { - map!( - and!( - specialize(EType::TApply, parse_concrete_type), - // Optionally parse space-separated arguments for the constructor, - // e.g. `Str Float` in `Map Str Float` - loc_applied_args_e(min_indent, stop_at_surface_has) - ), - |(ctor, args): (TypeAnnotation<'a>, Vec<'a, Loc>>)| { - match &ctor { - TypeAnnotation::Apply(module_name, name, _) => { - if args.is_empty() { - // ctor is already an Apply with no args, so return it directly. - ctor - } else { - TypeAnnotation::Apply(module_name, name, args.into_bump_slice()) - } - } - TypeAnnotation::Malformed(_) => ctor, - _ => unreachable!(), - } - } - ) - .trace("type_annotation:applied_type") -} - -fn loc_applied_args_e<'a>( - min_indent: u32, - stop_at_surface_has: bool, -) -> impl Parser<'a, Vec<'a, Loc>>, EType<'a>> { - zero_or_more!(loc_applied_arg(min_indent, stop_at_surface_has)) -} - -fn has_clause<'a>(min_indent: u32) -> impl Parser<'a, Loc>, EType<'a>> { - map!( - // Suppose we are trying to parse "a has Hash" - and!( - space0_around_ee( - // Parse "a", with appropriate spaces - specialize( - |_, pos| EType::TBadTypeVariable(pos), - loc!(map!(lowercase_ident(), Spaced::Item)), - ), - min_indent, - EType::TIndentStart, - EType::TIndentEnd - ), - then( - // Parse "has"; we don't care about this keyword - word3(b'h', b'a', b's', EType::THasClause), - // Parse "Hash"; this may be qualified from another module like "Hash.Hash" - |arena, state, _progress, _output| { - space0_before_e( - specialize(EType::TApply, loc!(parse_concrete_type)), - state.column() + 1, - EType::TIndentStart, - ) - .parse(arena, state) - } - ) - ), - |(var, ability): (Loc>, Loc>)| { - let region = Region::span_across(&var.region, &ability.region); - let has_clause = HasClause { var, ability }; - Loc::at(region, has_clause) - } - ) -} - -/// Parse a chain of `has` clauses, e.g. " | a has Hash, b has Eq". -/// Returns the clauses and spaces before the starting "|", if there were any. -fn has_clause_chain<'a>( - min_indent: u32, -) -> impl Parser<'a, (&'a [CommentOrNewline<'a>], &'a [Loc>]), EType<'a>> { - move |arena, state: State<'a>| { - let (_, (spaces_before, ()), state) = and!( - space0_e(min_indent, EType::TIndentStart), - word1(b'|', EType::TWhereBar) - ) - .parse(arena, state)?; - - let min_demand_indent = state.column() + 1; - // Parse the first clause (there must be one), then the rest - let (_, first_clause, state) = has_clause(min_demand_indent).parse(arena, state)?; - - let (_, mut clauses, state) = zero_or_more!(skip_first!( - word1(b',', EType::THasClause), - has_clause(min_demand_indent) - )) - .parse(arena, state)?; - - // Usually the number of clauses shouldn't be too large, so this is okay - clauses.insert(0, first_clause); - - Ok(( - MadeProgress, - (spaces_before, clauses.into_bump_slice()), - state, - )) - } -} - -/// Parse a has-derived clause, e.g. `has [Eq, Hash]`. -pub fn has_derived<'a>(min_indent: u32) -> impl Parser<'a, Loc>, EType<'a>> { - skip_first!( - // Parse "has"; we don't care about this keyword - word3(b'h', b'a', b's', EType::THasClause), - // Parse "Hash"; this may be qualified from another module like "Hash.Hash" - space0_before_e( - loc!(map!( - collection_trailing_sep_e!( - word1(b'[', EType::TStart), - specialize(EType::TApply, loc!(parse_concrete_type)), - word1(b',', EType::TEnd), - word1(b']', EType::TEnd), - min_indent + 1, - EType::TStart, - EType::TIndentEnd, - TypeAnnotation::SpaceBefore - ), - Derived::Has - )), - min_indent + 1, - EType::TIndentEnd - ) - ) -} - -fn expression<'a>( - min_indent: u32, - is_trailing_comma_valid: bool, - stop_at_surface_has: bool, -) -> impl Parser<'a, Loc>, EType<'a>> { - (move |arena, state: State<'a>| { - let (p1, first, state) = space0_before_e( - term(min_indent, stop_at_surface_has), - min_indent, - EType::TIndentStart, - ) - .parse(arena, state)?; - - let result = and![ - zero_or_more!(skip_first!( - word1(b',', EType::TFunctionArgument), - one_of![ - space0_around_ee( - term(min_indent, stop_at_surface_has), - min_indent, - EType::TIndentStart, - EType::TIndentEnd - ), - |_, state: State<'a>| Err(( - NoProgress, - EType::TFunctionArgument(state.pos()), - state - )) - ] - )) - .trace("type_annotation:expression:rest_args"), - // TODO this space0 is dropped, so newlines just before the function arrow when there - // is only one argument are not seen by the formatter. Can we do better? - skip_second!( - space0_e(min_indent, EType::TIndentStart), - word2(b'-', b'>', EType::TStart) - ) - .trace("type_annotation:expression:arrow") - ] - .parse(arena, state.clone()); - - let (progress, annot, state) = match result { - Ok((p2, (rest, _dropped_spaces), state)) => { - let (p3, return_type, state) = space0_before_e( - term(min_indent, stop_at_surface_has), - min_indent, - EType::TIndentStart, - ) - .parse(arena, state)?; - - let region = Region::span_across(&first.region, &return_type.region); - - // prepare arguments - let mut arguments = Vec::with_capacity_in(rest.len() + 1, arena); - arguments.push(first); - arguments.extend(rest); - let output = arena.alloc(arguments); - - let result = Loc { - region, - value: TypeAnnotation::Function(output, arena.alloc(return_type)), - }; - let progress = p1.or(p2).or(p3); - (progress, result, state) - } - Err(err) => { - if !is_trailing_comma_valid { - let (_, comma, _) = optional(skip_first!( - space0_e(min_indent, EType::TIndentStart), - word1(b',', EType::TStart) - )) - .trace("check trailing comma") - .parse(arena, state.clone())?; - - if comma.is_some() { - // If the surrounding scope has declared that a trailing comma is not a valid state - // for a type annotation - and we found one anyway - return an error so that we can - // produce a more useful error message, knowing that the user was probably writing a - // function type and messed up the syntax somehow. - return Err(err); - } - } - - // We ran into trouble parsing the function bits; just return the single term - (p1, first, state) - } - }; - - // Finally, try to parse a where clause if there is one. - // The where clause must be at least as deep as where the type annotation started. - let min_where_clause_indent = min_indent; - match has_clause_chain(min_where_clause_indent).parse(arena, state.clone()) { - Ok((where_progress, (spaces_before, has_chain), state)) => { - use crate::ast::Spaceable; - - let region = Region::span_across(&annot.region, &has_chain.last().unwrap().region); - let type_annot = if !spaces_before.is_empty() { - let spaced = arena - .alloc(annot.value) - .with_spaces_before(spaces_before, annot.region); - &*arena.alloc(spaced) - } else { - &*arena.alloc(annot) - }; - let where_annot = TypeAnnotation::Where(type_annot, has_chain); - Ok(( - where_progress.or(progress), - Loc::at(region, where_annot), - state, - )) - } - Err(_) => { - // Ran into a problem parsing a where clause; don't suppose there is one. - Ok((progress, annot, state)) - } - } - }) - .trace("type_annotation:expression") -} - -/// Parse a basic type annotation that's a combination of variables -/// (which are lowercase and unqualified, e.g. `a` in `List a`), -/// type applications (which are uppercase and optionally qualified, e.g. -/// `Int`, or the `List` in `List a` or the qualified application `Set.Set Float`), -/// and function types like `(a -> b)`. -/// -/// Type annotations can also contain records, parentheses, and the `*` character, -/// but this function is not responsible for parsing those. -// Function(&'a [TypeAnnotation<'a>], &'a TypeAnnotation<'a>), - -// /// Applying a type to some arguments (e.g. Map.Map String Int) -// Apply(&'a [&'a str], &'a str, &'a [&'a TypeAnnotation<'a>]), - -// /// A bound type variable, e.g. `a` in `(a -> a)` -// BoundVariable(&'a str), - -fn parse_concrete_type<'a>( - arena: &'a Bump, - state: State<'a>, -) -> ParseResult<'a, TypeAnnotation<'a>, ETypeApply> { - let initial_bytes = state.bytes(); - - match crate::ident::concrete_type().parse(arena, state) { - Ok((_, (module_name, type_name), state)) => { - let answer = TypeAnnotation::Apply(module_name, type_name, &[]); - - Ok((MadeProgress, answer, state)) - } - Err((NoProgress, _, state)) => Err((NoProgress, ETypeApply::End(state.pos()), state)), - Err((MadeProgress, _, mut state)) => { - // we made some progress, but ultimately failed. - // that means a malformed type name - let chomped = crate::ident::chomp_malformed(state.bytes()); - let delta = initial_bytes.len() - state.bytes().len(); - let parsed_str = - unsafe { std::str::from_utf8_unchecked(&initial_bytes[..chomped + delta]) }; - - state = state.advance(chomped); - - Ok((MadeProgress, TypeAnnotation::Malformed(parsed_str), state)) - } - } -} - -fn parse_type_variable<'a>( - stop_at_surface_has: bool, -) -> impl Parser<'a, TypeAnnotation<'a>, EType<'a>> { - move |arena, state: State<'a>| match crate::ident::lowercase_ident().parse(arena, state) { - Ok((_, name, state)) => { - if name == "has" && stop_at_surface_has { - Err((NoProgress, EType::TEnd(state.pos()), state)) - } else { - let answer = TypeAnnotation::BoundVariable(name); - - Ok((MadeProgress, answer, state)) - } - } - Err((progress, _, state)) => Err((progress, EType::TBadTypeVariable(state.pos()), state)), - } -} diff --git a/compiler/parse/tests/snapshots/pass/ability_demand_signature_is_multiline.expr.result-ast b/compiler/parse/tests/snapshots/pass/ability_demand_signature_is_multiline.expr.result-ast deleted file mode 100644 index 89c0bd7b00..0000000000 --- a/compiler/parse/tests/snapshots/pass/ability_demand_signature_is_multiline.expr.result-ast +++ /dev/null @@ -1,44 +0,0 @@ -Defs( - [ - @0-36 Type( - Ability { - header: TypeHeader { - name: @0-4 "Hash", - vars: [], - }, - loc_has: @5-8 Has, - members: [ - AbilityMember { - name: @11-15 SpaceBefore( - "hash", - [ - Newline, - ], - ), - typ: @18-36 Function( - [ - @18-19 BoundVariable( - "a", - ), - ], - @33-36 Apply( - "", - "U64", - [], - ), - ), - }, - ], - }, - ), - ], - @38-39 SpaceBefore( - Num( - "1", - ), - [ - Newline, - Newline, - ], - ), -) diff --git a/compiler/parse/tests/snapshots/pass/ability_demand_signature_is_multiline.expr.roc b/compiler/parse/tests/snapshots/pass/ability_demand_signature_is_multiline.expr.roc deleted file mode 100644 index 5e218feb6e..0000000000 --- a/compiler/parse/tests/snapshots/pass/ability_demand_signature_is_multiline.expr.roc +++ /dev/null @@ -1,5 +0,0 @@ -Hash has - hash : a - -> U64 - -1 diff --git a/compiler/parse/tests/snapshots/pass/ability_multi_line.expr.result-ast b/compiler/parse/tests/snapshots/pass/ability_multi_line.expr.result-ast deleted file mode 100644 index c650e71f0e..0000000000 --- a/compiler/parse/tests/snapshots/pass/ability_multi_line.expr.result-ast +++ /dev/null @@ -1,64 +0,0 @@ -Defs( - [ - @0-45 Type( - Ability { - header: TypeHeader { - name: @0-4 "Hash", - vars: [], - }, - loc_has: @5-8 Has, - members: [ - AbilityMember { - name: @11-15 SpaceBefore( - "hash", - [ - Newline, - ], - ), - typ: @18-26 Function( - [ - @18-19 BoundVariable( - "a", - ), - ], - @23-26 Apply( - "", - "U64", - [], - ), - ), - }, - AbilityMember { - name: @29-34 SpaceBefore( - "hash2", - [ - Newline, - ], - ), - typ: @37-45 Function( - [ - @37-38 BoundVariable( - "a", - ), - ], - @42-45 Apply( - "", - "U64", - [], - ), - ), - }, - ], - }, - ), - ], - @47-48 SpaceBefore( - Num( - "1", - ), - [ - Newline, - Newline, - ], - ), -) diff --git a/compiler/parse/tests/snapshots/pass/ability_multi_line.expr.roc b/compiler/parse/tests/snapshots/pass/ability_multi_line.expr.roc deleted file mode 100644 index 795c19bad6..0000000000 --- a/compiler/parse/tests/snapshots/pass/ability_multi_line.expr.roc +++ /dev/null @@ -1,5 +0,0 @@ -Hash has - hash : a -> U64 - hash2 : a -> U64 - -1 diff --git a/compiler/parse/tests/snapshots/pass/ability_single_line.expr.result-ast b/compiler/parse/tests/snapshots/pass/ability_single_line.expr.result-ast deleted file mode 100644 index f97c020789..0000000000 --- a/compiler/parse/tests/snapshots/pass/ability_single_line.expr.result-ast +++ /dev/null @@ -1,51 +0,0 @@ -Defs( - [ - @0-37 Type( - Ability { - header: TypeHeader { - name: @0-4 "Hash", - vars: [], - }, - loc_has: @5-8 Has, - members: [ - AbilityMember { - name: @9-13 "hash", - typ: @16-37 Where( - @16-24 Function( - [ - @16-17 BoundVariable( - "a", - ), - ], - @21-24 Apply( - "", - "U64", - [], - ), - ), - [ - @27-37 HasClause { - var: @27-28 "a", - ability: @33-37 Apply( - "", - "Hash", - [], - ), - }, - ], - ), - }, - ], - }, - ), - ], - @39-40 SpaceBefore( - Num( - "1", - ), - [ - Newline, - Newline, - ], - ), -) diff --git a/compiler/parse/tests/snapshots/pass/ability_single_line.expr.roc b/compiler/parse/tests/snapshots/pass/ability_single_line.expr.roc deleted file mode 100644 index c3d8e53fab..0000000000 --- a/compiler/parse/tests/snapshots/pass/ability_single_line.expr.roc +++ /dev/null @@ -1,3 +0,0 @@ -Hash has hash : a -> U64 | a has Hash - -1 diff --git a/compiler/parse/tests/snapshots/pass/ability_two_in_a_row.expr.result-ast b/compiler/parse/tests/snapshots/pass/ability_two_in_a_row.expr.result-ast deleted file mode 100644 index dc6057152e..0000000000 --- a/compiler/parse/tests/snapshots/pass/ability_two_in_a_row.expr.result-ast +++ /dev/null @@ -1,93 +0,0 @@ -Defs( - [ - @0-33 Type( - Ability { - header: TypeHeader { - name: @0-3 "Ab1", - vars: [], - }, - loc_has: @4-7 Has, - members: [ - AbilityMember { - name: @8-11 "ab1", - typ: @14-33 Where( - @14-21 Function( - [ - @14-15 BoundVariable( - "a", - ), - ], - @19-21 Record { - fields: [], - ext: None, - }, - ), - [ - @24-33 HasClause { - var: @24-25 "a", - ability: @30-33 Apply( - "", - "Ab1", - [], - ), - }, - ], - ), - }, - ], - }, - ), - @35-68 SpaceBefore( - Type( - Ability { - header: TypeHeader { - name: @35-38 "Ab2", - vars: [], - }, - loc_has: @39-42 Has, - members: [ - AbilityMember { - name: @43-46 "ab2", - typ: @49-68 Where( - @49-56 Function( - [ - @49-50 BoundVariable( - "a", - ), - ], - @54-56 Record { - fields: [], - ext: None, - }, - ), - [ - @59-68 HasClause { - var: @59-60 "a", - ability: @65-68 Apply( - "", - "Ab2", - [], - ), - }, - ], - ), - }, - ], - }, - ), - [ - Newline, - Newline, - ], - ), - ], - @70-71 SpaceBefore( - Num( - "1", - ), - [ - Newline, - Newline, - ], - ), -) diff --git a/compiler/parse/tests/snapshots/pass/ability_two_in_a_row.expr.roc b/compiler/parse/tests/snapshots/pass/ability_two_in_a_row.expr.roc deleted file mode 100644 index f747842c70..0000000000 --- a/compiler/parse/tests/snapshots/pass/ability_two_in_a_row.expr.roc +++ /dev/null @@ -1,5 +0,0 @@ -Ab1 has ab1 : a -> {} | a has Ab1 - -Ab2 has ab2 : a -> {} | a has Ab2 - -1 diff --git a/compiler/parse/tests/snapshots/pass/annotated_record_destructure.expr.result-ast b/compiler/parse/tests/snapshots/pass/annotated_record_destructure.expr.result-ast deleted file mode 100644 index 3b87b36828..0000000000 --- a/compiler/parse/tests/snapshots/pass/annotated_record_destructure.expr.result-ast +++ /dev/null @@ -1,64 +0,0 @@ -Defs( - [ - @15-49 Value( - AnnotatedBody { - ann_pattern: @0-8 RecordDestructure( - [ - @2-3 Identifier( - "x", - ), - @5-7 Identifier( - "y", - ), - ], - ), - ann_type: @11-14 Apply( - "", - "Foo", - [], - ), - comment: None, - body_pattern: @15-23 RecordDestructure( - [ - @17-18 Identifier( - "x", - ), - @20-21 Identifier( - "y", - ), - ], - ), - body_expr: @26-49 Record( - [ - @28-37 RequiredValue( - @28-29 "x", - [], - @32-37 Str( - PlainLine( - "foo", - ), - ), - ), - @39-47 RequiredValue( - @39-40 "y", - [], - @43-47 Float( - "3.14", - ), - ), - ], - ), - }, - ), - ], - @51-52 SpaceBefore( - Var { - module_name: "", - ident: "x", - }, - [ - Newline, - Newline, - ], - ), -) diff --git a/compiler/parse/tests/snapshots/pass/annotated_tag_destructure.expr.result-ast b/compiler/parse/tests/snapshots/pass/annotated_tag_destructure.expr.result-ast deleted file mode 100644 index eec49d33ac..0000000000 --- a/compiler/parse/tests/snapshots/pass/annotated_tag_destructure.expr.result-ast +++ /dev/null @@ -1,65 +0,0 @@ -Defs( - [ - @26-46 Value( - AnnotatedBody { - ann_pattern: @0-8 Apply( - @0-6 Tag( - "UserId", - ), - [ - @7-8 Identifier( - "x", - ), - ], - ), - ann_type: @11-25 TagUnion { - ext: None, - tags: [ - @13-23 Apply { - name: @13-19 "UserId", - args: [ - @20-23 Apply( - "", - "I64", - [], - ), - ], - }, - ], - }, - comment: None, - body_pattern: @26-34 Apply( - @26-32 Tag( - "UserId", - ), - [ - @33-34 Identifier( - "x", - ), - ], - ), - body_expr: @37-46 Apply( - @37-43 Tag( - "UserId", - ), - [ - @44-46 Num( - "42", - ), - ], - Space, - ), - }, - ), - ], - @48-49 SpaceBefore( - Var { - module_name: "", - ident: "x", - }, - [ - Newline, - Newline, - ], - ), -) diff --git a/compiler/parse/tests/snapshots/pass/basic_docs.expr.result-ast b/compiler/parse/tests/snapshots/pass/basic_docs.expr.result-ast deleted file mode 100644 index e457bb7705..0000000000 --- a/compiler/parse/tests/snapshots/pass/basic_docs.expr.result-ast +++ /dev/null @@ -1,45 +0,0 @@ -SpaceBefore( - Defs( - [ - @107-112 Value( - Body( - @107-108 Identifier( - "x", - ), - @111-112 Num( - "5", - ), - ), - ), - ], - @114-116 SpaceBefore( - Num( - "42", - ), - [ - Newline, - Newline, - ], - ), - ), - [ - DocComment( - "first line of docs", - ), - DocComment( - " second line", - ), - DocComment( - " third line", - ), - DocComment( - "fourth line", - ), - DocComment( - "", - ), - DocComment( - "sixth line after doc new line", - ), - ], -) diff --git a/compiler/parse/tests/snapshots/pass/destructure_tag_assignment.expr.result-ast b/compiler/parse/tests/snapshots/pass/destructure_tag_assignment.expr.result-ast deleted file mode 100644 index 95c8fc9192..0000000000 --- a/compiler/parse/tests/snapshots/pass/destructure_tag_assignment.expr.result-ast +++ /dev/null @@ -1,40 +0,0 @@ -Defs( - [ - @0-36 Value( - Body( - @0-5 Apply( - @0-5 Tag( - "Email", - ), - [ - @6-9 Identifier( - "str", - ), - ], - ), - @12-36 Apply( - @12-17 Tag( - "Email", - ), - [ - @18-36 Str( - PlainLine( - "blah@example.com", - ), - ), - ], - Space, - ), - ), - ), - ], - @37-40 SpaceBefore( - Var { - module_name: "", - ident: "str", - }, - [ - Newline, - ], - ), -) diff --git a/compiler/parse/tests/snapshots/pass/empty_app_header.header.result-ast b/compiler/parse/tests/snapshots/pass/empty_app_header.header.result-ast deleted file mode 100644 index d0ba5ec7b0..0000000000 --- a/compiler/parse/tests/snapshots/pass/empty_app_header.header.result-ast +++ /dev/null @@ -1,24 +0,0 @@ -App { - header: AppHeader { - name: @4-14 PlainLine( - "test-app", - ), - packages: [], - imports: [], - provides: [], - provides_types: None, - to: @53-57 ExistingPackage( - "blah", - ), - before_header: [], - after_app_keyword: [], - before_packages: [], - after_packages: [], - before_imports: [], - after_imports: [], - before_provides: [], - after_provides: [], - before_to: [], - after_to: [], - }, -} diff --git a/compiler/parse/tests/snapshots/pass/empty_hosted_header.header.result-ast b/compiler/parse/tests/snapshots/pass/empty_hosted_header.header.result-ast deleted file mode 100644 index 8cf0174c92..0000000000 --- a/compiler/parse/tests/snapshots/pass/empty_hosted_header.header.result-ast +++ /dev/null @@ -1,23 +0,0 @@ -Hosted { - header: HostedHeader { - name: @7-10 ModuleName( - "Foo", - ), - exposes: [], - imports: [], - generates: UppercaseIdent( - "Bar", - ), - generates_with: [], - before_header: [], - after_hosted_keyword: [], - before_exposes: [], - after_exposes: [], - before_imports: [], - after_imports: [], - before_generates: [], - after_generates: [], - before_with: [], - after_with: [], - }, -} diff --git a/compiler/parse/tests/snapshots/pass/empty_interface_header.header.result-ast b/compiler/parse/tests/snapshots/pass/empty_interface_header.header.result-ast deleted file mode 100644 index 1bd388d31d..0000000000 --- a/compiler/parse/tests/snapshots/pass/empty_interface_header.header.result-ast +++ /dev/null @@ -1,15 +0,0 @@ -Interface { - header: InterfaceHeader { - name: @10-13 ModuleName( - "Foo", - ), - exposes: [], - imports: [], - before_header: [], - after_interface_keyword: [], - before_exposes: [], - after_exposes: [], - before_imports: [], - after_imports: [], - }, -} diff --git a/compiler/parse/tests/snapshots/pass/empty_platform_header.header.result-ast b/compiler/parse/tests/snapshots/pass/empty_platform_header.header.result-ast deleted file mode 100644 index 736414d3a8..0000000000 --- a/compiler/parse/tests/snapshots/pass/empty_platform_header.header.result-ast +++ /dev/null @@ -1,34 +0,0 @@ -Platform { - header: PlatformHeader { - name: @9-25 PackageName( - "rtfeldman/blah", - ), - requires: PlatformRequires { - rigids: [], - signature: @40-49 TypedIdent { - ident: @40-44 "main", - spaces_before_colon: [], - ann: @47-49 Record { - fields: [], - ext: None, - }, - }, - }, - exposes: [], - packages: [], - imports: [], - provides: [], - before_header: [], - after_platform_keyword: [], - before_requires: [], - after_requires: [], - before_exposes: [], - after_exposes: [], - before_packages: [], - after_packages: [], - before_imports: [], - after_imports: [], - before_provides: [], - after_provides: [], - }, -} diff --git a/compiler/parse/tests/snapshots/pass/full_app_header.header.result-ast b/compiler/parse/tests/snapshots/pass/full_app_header.header.result-ast deleted file mode 100644 index 76545b43e9..0000000000 --- a/compiler/parse/tests/snapshots/pass/full_app_header.header.result-ast +++ /dev/null @@ -1,50 +0,0 @@ -App { - header: AppHeader { - name: @4-15 PlainLine( - "quicksort", - ), - packages: [ - @31-47 PackageEntry { - shorthand: "pf", - spaces_after_shorthand: [], - package_name: @35-47 PackageName( - "./platform", - ), - }, - ], - imports: [ - @64-75 Package( - "foo", - ModuleName( - "Bar.Baz", - ), - [], - ), - ], - provides: [ - @93-102 ExposedName( - "quicksort", - ), - ], - provides_types: None, - to: @108-110 ExistingPackage( - "pf", - ), - before_header: [], - after_app_keyword: [], - before_packages: [ - Newline, - ], - after_packages: [], - before_imports: [ - Newline, - ], - after_imports: [], - before_provides: [ - Newline, - ], - after_provides: [], - before_to: [], - after_to: [], - }, -} diff --git a/compiler/parse/tests/snapshots/pass/full_app_header_trailing_commas.header.result-ast b/compiler/parse/tests/snapshots/pass/full_app_header_trailing_commas.header.result-ast deleted file mode 100644 index 8840febceb..0000000000 --- a/compiler/parse/tests/snapshots/pass/full_app_header_trailing_commas.header.result-ast +++ /dev/null @@ -1,75 +0,0 @@ -App { - header: AppHeader { - name: @4-15 PlainLine( - "quicksort", - ), - packages: [ - @31-47 PackageEntry { - shorthand: "pf", - spaces_after_shorthand: [], - package_name: @35-47 PackageName( - "./platform", - ), - }, - ], - imports: [ - @65-141 Package( - "foo", - ModuleName( - "Bar", - ), - Collection { - items: [ - @83-86 SpaceBefore( - ExposedName( - "Baz", - ), - [ - Newline, - ], - ), - @96-104 SpaceBefore( - ExposedName( - "FortyTwo", - ), - [ - Newline, - ], - ), - ], - final_comments: [ - Newline, - LineComment( - " I'm a happy comment", - ), - ], - }, - ), - ], - provides: [ - @159-168 ExposedName( - "quicksort", - ), - ], - provides_types: None, - to: @175-177 ExistingPackage( - "pf", - ), - before_header: [], - after_app_keyword: [], - before_packages: [ - Newline, - ], - after_packages: [], - before_imports: [ - Newline, - ], - after_imports: [], - before_provides: [ - Newline, - ], - after_provides: [], - before_to: [], - after_to: [], - }, -} diff --git a/compiler/parse/tests/snapshots/pass/function_effect_types.header.result-ast b/compiler/parse/tests/snapshots/pass/function_effect_types.header.result-ast deleted file mode 100644 index 39d72bce49..0000000000 --- a/compiler/parse/tests/snapshots/pass/function_effect_types.header.result-ast +++ /dev/null @@ -1,71 +0,0 @@ -Platform { - header: PlatformHeader { - name: @9-14 PackageName( - "cli", - ), - requires: PlatformRequires { - rigids: [], - signature: @32-49 TypedIdent { - ident: @32-36 "main", - spaces_before_colon: [], - ann: @39-49 Apply( - "", - "Task", - [ - @44-46 Record { - fields: [], - ext: None, - }, - @47-49 TagUnion { - ext: None, - tags: [], - }, - ], - ), - }, - }, - exposes: [], - packages: [], - imports: [ - @110-123 Module( - ModuleName( - "Task", - ), - [ - @117-121 ExposedName( - "Task", - ), - ], - ), - ], - provides: [ - @141-152 ExposedName( - "mainForHost", - ), - ], - before_header: [], - after_platform_keyword: [], - before_requires: [ - Newline, - ], - after_requires: [], - before_exposes: [ - LineComment( - " TODO FIXME", - ), - ], - after_exposes: [], - before_packages: [ - Newline, - ], - after_packages: [], - before_imports: [ - Newline, - ], - after_imports: [], - before_provides: [ - Newline, - ], - after_provides: [], - }, -} diff --git a/compiler/parse/tests/snapshots/pass/if_def.expr.result-ast b/compiler/parse/tests/snapshots/pass/if_def.expr.result-ast deleted file mode 100644 index 61962aa8bc..0000000000 --- a/compiler/parse/tests/snapshots/pass/if_def.expr.result-ast +++ /dev/null @@ -1,23 +0,0 @@ -Defs( - [ - @0-6 Value( - Body( - @0-4 Identifier( - "iffy", - ), - @5-6 Num( - "5", - ), - ), - ), - ], - @8-10 SpaceBefore( - Num( - "42", - ), - [ - Newline, - Newline, - ], - ), -) diff --git a/compiler/parse/tests/snapshots/pass/interface_with_newline.header.result-ast b/compiler/parse/tests/snapshots/pass/interface_with_newline.header.result-ast deleted file mode 100644 index fe67388046..0000000000 --- a/compiler/parse/tests/snapshots/pass/interface_with_newline.header.result-ast +++ /dev/null @@ -1,15 +0,0 @@ -Interface { - header: InterfaceHeader { - name: @10-11 ModuleName( - "T", - ), - exposes: [], - imports: [], - before_header: [], - after_interface_keyword: [], - before_exposes: [], - after_exposes: [], - before_imports: [], - after_imports: [], - }, -} diff --git a/compiler/parse/tests/snapshots/pass/list_closing_same_indent_no_trailing_comma.expr.result-ast b/compiler/parse/tests/snapshots/pass/list_closing_same_indent_no_trailing_comma.expr.result-ast deleted file mode 100644 index ffe535c03f..0000000000 --- a/compiler/parse/tests/snapshots/pass/list_closing_same_indent_no_trailing_comma.expr.result-ast +++ /dev/null @@ -1,44 +0,0 @@ -Defs( - [ - @0-26 Value( - Body( - @0-7 Malformed( - "my_list", - ), - @10-26 List( - Collection { - items: [ - @16-17 SpaceBefore( - Num( - "0", - ), - [ - Newline, - ], - ), - @23-24 SpaceBefore( - Num( - "1", - ), - [ - Newline, - ], - ), - ], - final_comments: [ - Newline, - ], - }, - ), - ), - ), - ], - @27-29 SpaceBefore( - Num( - "42", - ), - [ - Newline, - ], - ), -) diff --git a/compiler/parse/tests/snapshots/pass/list_closing_same_indent_no_trailing_comma.expr.roc b/compiler/parse/tests/snapshots/pass/list_closing_same_indent_no_trailing_comma.expr.roc deleted file mode 100644 index 24b7269dec..0000000000 --- a/compiler/parse/tests/snapshots/pass/list_closing_same_indent_no_trailing_comma.expr.roc +++ /dev/null @@ -1,5 +0,0 @@ -my_list = [ - 0, - 1 -] -42 diff --git a/compiler/parse/tests/snapshots/pass/list_closing_same_indent_with_trailing_comma.expr.result-ast b/compiler/parse/tests/snapshots/pass/list_closing_same_indent_with_trailing_comma.expr.result-ast deleted file mode 100644 index e526c773d2..0000000000 --- a/compiler/parse/tests/snapshots/pass/list_closing_same_indent_with_trailing_comma.expr.result-ast +++ /dev/null @@ -1,44 +0,0 @@ -Defs( - [ - @0-27 Value( - Body( - @0-7 Malformed( - "my_list", - ), - @10-27 List( - Collection { - items: [ - @16-17 SpaceBefore( - Num( - "0", - ), - [ - Newline, - ], - ), - @23-24 SpaceBefore( - Num( - "1", - ), - [ - Newline, - ], - ), - ], - final_comments: [ - Newline, - ], - }, - ), - ), - ), - ], - @28-30 SpaceBefore( - Num( - "42", - ), - [ - Newline, - ], - ), -) diff --git a/compiler/parse/tests/snapshots/pass/list_closing_same_indent_with_trailing_comma.expr.roc b/compiler/parse/tests/snapshots/pass/list_closing_same_indent_with_trailing_comma.expr.roc deleted file mode 100644 index f6e475d2df..0000000000 --- a/compiler/parse/tests/snapshots/pass/list_closing_same_indent_with_trailing_comma.expr.roc +++ /dev/null @@ -1,5 +0,0 @@ -my_list = [ - 0, - 1, -] -42 diff --git a/compiler/parse/tests/snapshots/pass/minimal_app_header.header.result-ast b/compiler/parse/tests/snapshots/pass/minimal_app_header.header.result-ast deleted file mode 100644 index 225de00938..0000000000 --- a/compiler/parse/tests/snapshots/pass/minimal_app_header.header.result-ast +++ /dev/null @@ -1,26 +0,0 @@ -App { - header: AppHeader { - name: @4-14 PlainLine( - "test-app", - ), - packages: [], - imports: [], - provides: [], - provides_types: None, - to: @30-38 NewPackage( - PackageName( - "./blah", - ), - ), - before_header: [], - after_app_keyword: [], - before_packages: [], - after_packages: [], - before_imports: [], - after_imports: [], - before_provides: [], - after_provides: [], - before_to: [], - after_to: [], - }, -} diff --git a/compiler/parse/tests/snapshots/pass/mixed_docs.expr.result-ast b/compiler/parse/tests/snapshots/pass/mixed_docs.expr.result-ast deleted file mode 100644 index 9b61bffafa..0000000000 --- a/compiler/parse/tests/snapshots/pass/mixed_docs.expr.result-ast +++ /dev/null @@ -1,39 +0,0 @@ -SpaceBefore( - Defs( - [ - @113-118 Value( - Body( - @113-114 Identifier( - "x", - ), - @117-118 Num( - "5", - ), - ), - ), - ], - @120-122 SpaceBefore( - Num( - "42", - ), - [ - Newline, - Newline, - ], - ), - ), - [ - LineComment( - "## not docs!", - ), - DocComment( - "docs, but with a problem", - ), - DocComment( - "(namely that this is a mix of docs and regular comments)", - ), - LineComment( - " not docs", - ), - ], -) diff --git a/compiler/parse/tests/snapshots/pass/module_def_newline.module.result-ast b/compiler/parse/tests/snapshots/pass/module_def_newline.module.result-ast deleted file mode 100644 index 63fd8f31d3..0000000000 --- a/compiler/parse/tests/snapshots/pass/module_def_newline.module.result-ast +++ /dev/null @@ -1,54 +0,0 @@ -Defs { - tags: [ - Index(2147483648), - ], - regions: [ - @0-24, - ], - space_before: [ - Slice(start = 0, length = 0), - ], - space_after: [ - Slice(start = 0, length = 1), - ], - spaces: [ - Newline, - ], - type_defs: [], - value_defs: [ - Body( - @0-4 Identifier( - "main", - ), - @11-24 SpaceBefore( - Defs( - [ - @11-17 Value( - Body( - @11-12 Identifier( - "i", - ), - @15-17 Num( - "64", - ), - ), - ), - ], - @23-24 SpaceBefore( - Var { - module_name: "", - ident: "i", - }, - [ - Newline, - Newline, - ], - ), - ), - [ - Newline, - ], - ), - ), - ], -} diff --git a/compiler/parse/tests/snapshots/pass/multiline_type_signature.expr.result-ast b/compiler/parse/tests/snapshots/pass/multiline_type_signature.expr.result-ast deleted file mode 100644 index d3519069ff..0000000000 --- a/compiler/parse/tests/snapshots/pass/multiline_type_signature.expr.result-ast +++ /dev/null @@ -1,29 +0,0 @@ -Defs( - [ - @0-10 Value( - Annotation( - @0-1 Identifier( - "f", - ), - @8-10 SpaceBefore( - Record { - fields: [], - ext: None, - }, - [ - Newline, - ], - ), - ), - ), - ], - @12-14 SpaceBefore( - Num( - "42", - ), - [ - Newline, - Newline, - ], - ), -) diff --git a/compiler/parse/tests/snapshots/pass/multiline_type_signature_with_comment.expr.result-ast b/compiler/parse/tests/snapshots/pass/multiline_type_signature_with_comment.expr.result-ast deleted file mode 100644 index d989a361e4..0000000000 --- a/compiler/parse/tests/snapshots/pass/multiline_type_signature_with_comment.expr.result-ast +++ /dev/null @@ -1,31 +0,0 @@ -Defs( - [ - @0-19 Value( - Annotation( - @0-1 Identifier( - "f", - ), - @17-19 SpaceBefore( - Record { - fields: [], - ext: None, - }, - [ - LineComment( - " comment", - ), - ], - ), - ), - ), - ], - @21-23 SpaceBefore( - Num( - "42", - ), - [ - Newline, - Newline, - ], - ), -) diff --git a/compiler/parse/tests/snapshots/pass/multiple_fields.expr.result-ast b/compiler/parse/tests/snapshots/pass/multiple_fields.expr.result-ast deleted file mode 100644 index bb7b50e47a..0000000000 --- a/compiler/parse/tests/snapshots/pass/multiple_fields.expr.result-ast +++ /dev/null @@ -1,13 +0,0 @@ -Access( - Access( - Access( - Var { - module_name: "", - ident: "rec", - }, - "abc", - ), - "def", - ), - "ghi", -) diff --git a/compiler/parse/tests/snapshots/pass/nested_def_annotation.module.result-ast b/compiler/parse/tests/snapshots/pass/nested_def_annotation.module.result-ast deleted file mode 100644 index 2ff84db367..0000000000 --- a/compiler/parse/tests/snapshots/pass/nested_def_annotation.module.result-ast +++ /dev/null @@ -1,111 +0,0 @@ -Defs { - tags: [ - Index(2147483648), - ], - regions: [ - @0-115, - ], - space_before: [ - Slice(start = 0, length = 0), - ], - space_after: [ - Slice(start = 0, length = 1), - ], - spaces: [ - Newline, - ], - type_defs: [], - value_defs: [ - Body( - @0-4 Identifier( - "main", - ), - @11-115 SpaceBefore( - Defs( - [ - @43-93 Value( - AnnotatedBody { - ann_pattern: @11-23 Identifier( - "wrappedNotEq", - ), - ann_type: @26-38 Function( - [ - @26-27 BoundVariable( - "a", - ), - @29-30 BoundVariable( - "a", - ), - ], - @34-38 Apply( - "", - "Bool", - [], - ), - ), - comment: None, - body_pattern: @43-55 Identifier( - "wrappedNotEq", - ), - body_expr: @58-93 Closure( - [ - @59-63 Identifier( - "num1", - ), - @65-69 Identifier( - "num2", - ), - ], - @81-93 SpaceBefore( - BinOps( - [ - ( - @81-85 Var { - module_name: "", - ident: "num1", - }, - @86-88 NotEquals, - ), - ], - @89-93 Var { - module_name: "", - ident: "num2", - }, - ), - [ - Newline, - ], - ), - ), - }, - ), - ], - @99-115 SpaceBefore( - Apply( - @99-111 Var { - module_name: "", - ident: "wrappedNotEq", - }, - [ - @112-113 Num( - "2", - ), - @114-115 Num( - "3", - ), - ], - Space, - ), - [ - Newline, - Newline, - ], - ), - ), - [ - Newline, - ], - ), - ), - ], -} diff --git a/compiler/parse/tests/snapshots/pass/nested_module.header.result-ast b/compiler/parse/tests/snapshots/pass/nested_module.header.result-ast deleted file mode 100644 index 50f7b410ed..0000000000 --- a/compiler/parse/tests/snapshots/pass/nested_module.header.result-ast +++ /dev/null @@ -1,15 +0,0 @@ -Interface { - header: InterfaceHeader { - name: @10-21 ModuleName( - "Foo.Bar.Baz", - ), - exposes: [], - imports: [], - before_header: [], - after_interface_keyword: [], - before_exposes: [], - after_exposes: [], - before_imports: [], - after_imports: [], - }, -} diff --git a/compiler/parse/tests/snapshots/pass/newline_after_equals.expr.result-ast b/compiler/parse/tests/snapshots/pass/newline_after_equals.expr.result-ast deleted file mode 100644 index a62f3cc138..0000000000 --- a/compiler/parse/tests/snapshots/pass/newline_after_equals.expr.result-ast +++ /dev/null @@ -1,28 +0,0 @@ -Defs( - [ - @0-9 Value( - Body( - @0-1 Identifier( - "x", - ), - @8-9 SpaceBefore( - Num( - "5", - ), - [ - Newline, - ], - ), - ), - ), - ], - @11-13 SpaceBefore( - Num( - "42", - ), - [ - Newline, - Newline, - ], - ), -) diff --git a/compiler/parse/tests/snapshots/pass/newline_and_spaces_before_less_than.expr.result-ast b/compiler/parse/tests/snapshots/pass/newline_and_spaces_before_less_than.expr.result-ast deleted file mode 100644 index 2fd5007e3d..0000000000 --- a/compiler/parse/tests/snapshots/pass/newline_and_spaces_before_less_than.expr.result-ast +++ /dev/null @@ -1,38 +0,0 @@ -Defs( - [ - @0-13 Value( - Body( - @0-1 Identifier( - "x", - ), - @4-13 BinOps( - [ - ( - @4-5 SpaceAfter( - Num( - "1", - ), - [ - Newline, - ], - ), - @10-11 LessThan, - ), - ], - @12-13 Num( - "2", - ), - ), - ), - ), - ], - @15-17 SpaceBefore( - Num( - "42", - ), - [ - Newline, - Newline, - ], - ), -) diff --git a/compiler/parse/tests/snapshots/pass/nonempty_hosted_header.header.result-ast b/compiler/parse/tests/snapshots/pass/nonempty_hosted_header.header.result-ast deleted file mode 100644 index c5b245f35f..0000000000 --- a/compiler/parse/tests/snapshots/pass/nonempty_hosted_header.header.result-ast +++ /dev/null @@ -1,130 +0,0 @@ -Hosted { - header: HostedHeader { - name: @7-10 ModuleName( - "Foo", - ), - exposes: Collection { - items: [ - @45-50 SpaceBefore( - ExposedName( - "Stuff", - ), - [ - Newline, - ], - ), - @64-70 SpaceBefore( - ExposedName( - "Things", - ), - [ - Newline, - ], - ), - @84-97 SpaceBefore( - ExposedName( - "somethingElse", - ), - [ - Newline, - ], - ), - ], - final_comments: [ - Newline, - ], - }, - imports: Collection { - items: [ - @143-147 SpaceBefore( - Module( - ModuleName( - "Blah", - ), - [], - ), - [ - Newline, - ], - ), - @161-182 SpaceBefore( - Module( - ModuleName( - "Baz", - ), - [ - @167-172 ExposedName( - "stuff", - ), - @174-180 ExposedName( - "things", - ), - ], - ), - [ - Newline, - ], - ), - ], - final_comments: [ - Newline, - ], - }, - generates: UppercaseIdent( - "Bar", - ), - generates_with: Collection { - items: [ - @239-242 SpaceBefore( - ExposedName( - "map", - ), - [ - Newline, - ], - ), - @256-261 SpaceBefore( - ExposedName( - "after", - ), - [ - Newline, - ], - ), - @275-279 SpaceBefore( - ExposedName( - "loop", - ), - [ - Newline, - ], - ), - ], - final_comments: [ - Newline, - ], - }, - before_header: [], - after_hosted_keyword: [], - before_exposes: [ - Newline, - ], - after_exposes: [ - Newline, - ], - before_imports: [ - Newline, - ], - after_imports: [ - Newline, - ], - before_generates: [ - Newline, - ], - after_generates: [], - before_with: [], - after_with: [ - Newline, - ], - }, -} diff --git a/compiler/parse/tests/snapshots/pass/nonempty_platform_header.header.result-ast b/compiler/parse/tests/snapshots/pass/nonempty_platform_header.header.result-ast deleted file mode 100644 index 38b5d6f691..0000000000 --- a/compiler/parse/tests/snapshots/pass/nonempty_platform_header.header.result-ast +++ /dev/null @@ -1,60 +0,0 @@ -Platform { - header: PlatformHeader { - name: @9-21 PackageName( - "foo/barbaz", - ), - requires: PlatformRequires { - rigids: [ - @36-41 UppercaseIdent( - "Model", - ), - ], - signature: @45-54 TypedIdent { - ident: @45-49 "main", - spaces_before_colon: [], - ann: @52-54 Record { - fields: [], - ext: None, - }, - }, - }, - exposes: [], - packages: [ - @87-99 PackageEntry { - shorthand: "foo", - spaces_after_shorthand: [], - package_name: @92-99 PackageName( - "./foo", - ), - }, - ], - imports: [], - provides: [ - @132-143 ExposedName( - "mainForHost", - ), - ], - before_header: [], - after_platform_keyword: [], - before_requires: [ - Newline, - ], - after_requires: [], - before_exposes: [ - Newline, - ], - after_exposes: [], - before_packages: [ - Newline, - ], - after_packages: [], - before_imports: [ - Newline, - ], - after_imports: [], - before_provides: [ - Newline, - ], - after_provides: [], - }, -} diff --git a/compiler/parse/tests/snapshots/pass/not_docs.expr.result-ast b/compiler/parse/tests/snapshots/pass/not_docs.expr.result-ast deleted file mode 100644 index 0df9d829d3..0000000000 --- a/compiler/parse/tests/snapshots/pass/not_docs.expr.result-ast +++ /dev/null @@ -1,39 +0,0 @@ -SpaceBefore( - Defs( - [ - @46-51 Value( - Body( - @46-47 Identifier( - "x", - ), - @50-51 Num( - "5", - ), - ), - ), - ], - @53-55 SpaceBefore( - Num( - "42", - ), - [ - Newline, - Newline, - ], - ), - ), - [ - LineComment( - "######", - ), - LineComment( - "## not docs!", - ), - LineComment( - "#still not docs", - ), - LineComment( - "#####", - ), - ], -) diff --git a/compiler/parse/tests/snapshots/pass/one_def.expr.result-ast b/compiler/parse/tests/snapshots/pass/one_def.expr.result-ast deleted file mode 100644 index a8af60df37..0000000000 --- a/compiler/parse/tests/snapshots/pass/one_def.expr.result-ast +++ /dev/null @@ -1,30 +0,0 @@ -SpaceBefore( - Defs( - [ - @18-21 Value( - Body( - @18-19 Identifier( - "x", - ), - @20-21 Num( - "5", - ), - ), - ), - ], - @23-25 SpaceBefore( - Num( - "42", - ), - [ - Newline, - Newline, - ], - ), - ), - [ - LineComment( - " leading comment", - ), - ], -) diff --git a/compiler/parse/tests/snapshots/pass/one_spaced_def.expr.result-ast b/compiler/parse/tests/snapshots/pass/one_spaced_def.expr.result-ast deleted file mode 100644 index 7e47e88173..0000000000 --- a/compiler/parse/tests/snapshots/pass/one_spaced_def.expr.result-ast +++ /dev/null @@ -1,30 +0,0 @@ -SpaceBefore( - Defs( - [ - @18-23 Value( - Body( - @18-19 Identifier( - "x", - ), - @22-23 Num( - "5", - ), - ), - ), - ], - @25-27 SpaceBefore( - Num( - "42", - ), - [ - Newline, - Newline, - ], - ), - ), - [ - LineComment( - " leading comment", - ), - ], -) diff --git a/compiler/parse/tests/snapshots/pass/opaque_has_abilities.expr.result-ast b/compiler/parse/tests/snapshots/pass/opaque_has_abilities.expr.result-ast deleted file mode 100644 index da465ced6d..0000000000 --- a/compiler/parse/tests/snapshots/pass/opaque_has_abilities.expr.result-ast +++ /dev/null @@ -1,137 +0,0 @@ -Defs( - [ - @0-7 Type( - Opaque { - header: TypeHeader { - name: @0-1 "A", - vars: [], - }, - typ: @5-7 Apply( - "", - "U8", - [], - ), - derived: Some( - @12-22 Has( - [ - @13-15 Apply( - "", - "Eq", - [], - ), - @17-21 Apply( - "", - "Hash", - [], - ), - ], - ), - ), - }, - ), - @24-44 SpaceBefore( - Type( - Opaque { - header: TypeHeader { - name: @24-25 "A", - vars: [], - }, - typ: @29-44 Where( - @29-30 BoundVariable( - "a", - ), - [ - @33-44 HasClause { - var: @33-34 "a", - ability: @39-44 Apply( - "", - "Other", - [], - ), - }, - ], - ), - derived: Some( - @49-59 Has( - [ - @50-52 Apply( - "", - "Eq", - [], - ), - @54-58 Apply( - "", - "Hash", - [], - ), - ], - ), - ), - }, - ), - [ - Newline, - Newline, - ], - ), - @61-81 SpaceBefore( - Type( - Opaque { - header: TypeHeader { - name: @61-62 "A", - vars: [], - }, - typ: @66-81 Where( - @66-67 BoundVariable( - "a", - ), - [ - @70-81 HasClause { - var: @70-71 "a", - ability: @76-81 Apply( - "", - "Other", - [], - ), - }, - ], - ), - derived: Some( - @91-101 SpaceBefore( - Has( - [ - @92-94 Apply( - "", - "Eq", - [], - ), - @96-100 Apply( - "", - "Hash", - [], - ), - ], - ), - [ - Newline, - ], - ), - ), - }, - ), - [ - Newline, - Newline, - ], - ), - ], - @103-104 SpaceBefore( - Num( - "0", - ), - [ - Newline, - Newline, - ], - ), -) diff --git a/compiler/parse/tests/snapshots/pass/opaque_has_abilities.expr.roc b/compiler/parse/tests/snapshots/pass/opaque_has_abilities.expr.roc deleted file mode 100644 index 605ae5a29c..0000000000 --- a/compiler/parse/tests/snapshots/pass/opaque_has_abilities.expr.roc +++ /dev/null @@ -1,8 +0,0 @@ -A := U8 has [Eq, Hash] - -A := a | a has Other has [Eq, Hash] - -A := a | a has Other - has [Eq, Hash] - -0 diff --git a/compiler/parse/tests/snapshots/pass/opaque_simple.module.result-ast b/compiler/parse/tests/snapshots/pass/opaque_simple.module.result-ast deleted file mode 100644 index d1464898db..0000000000 --- a/compiler/parse/tests/snapshots/pass/opaque_simple.module.result-ast +++ /dev/null @@ -1,32 +0,0 @@ -Defs { - tags: [ - Index(0), - ], - regions: [ - @0-9, - ], - space_before: [ - Slice(start = 0, length = 0), - ], - space_after: [ - Slice(start = 0, length = 1), - ], - spaces: [ - Newline, - ], - type_defs: [ - Opaque { - header: TypeHeader { - name: @0-3 "Age", - vars: [], - }, - typ: @7-9 Apply( - "", - "U8", - [], - ), - derived: None, - }, - ], - value_defs: [], -} diff --git a/compiler/parse/tests/snapshots/pass/opaque_with_type_arguments.module.result-ast b/compiler/parse/tests/snapshots/pass/opaque_with_type_arguments.module.result-ast deleted file mode 100644 index cbcdf745f7..0000000000 --- a/compiler/parse/tests/snapshots/pass/opaque_with_type_arguments.module.result-ast +++ /dev/null @@ -1,61 +0,0 @@ -Defs { - tags: [ - Index(0), - ], - regions: [ - @0-53, - ], - space_before: [ - Slice(start = 0, length = 0), - ], - space_after: [ - Slice(start = 0, length = 1), - ], - spaces: [ - Newline, - ], - type_defs: [ - Opaque { - header: TypeHeader { - name: @0-10 "Bookmark", - vars: [ - @9-10 Identifier( - "a", - ), - ], - }, - typ: @14-53 Record { - fields: [ - @16-28 RequiredValue( - @16-23 "chapter", - [], - @25-28 Apply( - "", - "Str", - [], - ), - ), - @30-41 RequiredValue( - @30-36 "stanza", - [], - @38-41 Apply( - "", - "Str", - [], - ), - ), - @43-51 RequiredValue( - @43-48 "notes", - [], - @50-51 BoundVariable( - "a", - ), - ), - ], - ext: None, - }, - derived: None, - }, - ], - value_defs: [], -} diff --git a/compiler/parse/tests/snapshots/pass/outdented_app_with_record.expr.result-ast b/compiler/parse/tests/snapshots/pass/outdented_app_with_record.expr.result-ast deleted file mode 100644 index 4df43e0d86..0000000000 --- a/compiler/parse/tests/snapshots/pass/outdented_app_with_record.expr.result-ast +++ /dev/null @@ -1,62 +0,0 @@ -Defs( - [ - @0-29 Value( - Body( - @0-1 Identifier( - "x", - ), - @4-29 Apply( - @4-7 Var { - module_name: "", - ident: "foo", - }, - [ - @9-28 ParensAround( - Apply( - @9-12 Var { - module_name: "", - ident: "baz", - }, - [ - @13-28 Record( - Collection { - items: [ - @17-26 SpaceBefore( - RequiredValue( - @17-20 "bar", - [], - @22-26 Var { - module_name: "", - ident: "blah", - }, - ), - [ - Newline, - ], - ), - ], - final_comments: [ - Newline, - ], - }, - ), - ], - Space, - ), - ), - ], - Space, - ), - ), - ), - ], - @30-31 SpaceBefore( - Var { - module_name: "", - ident: "x", - }, - [ - Newline, - ], - ), -) diff --git a/compiler/parse/tests/snapshots/pass/outdented_list.expr.result-ast b/compiler/parse/tests/snapshots/pass/outdented_list.expr.result-ast deleted file mode 100644 index d7db49650a..0000000000 --- a/compiler/parse/tests/snapshots/pass/outdented_list.expr.result-ast +++ /dev/null @@ -1,43 +0,0 @@ -Defs( - [ - @0-17 Value( - Body( - @0-1 Identifier( - "a", - ), - @4-17 List( - Collection { - items: [ - @8-9 SpaceBefore( - Num( - "1", - ), - [ - Newline, - ], - ), - @11-12 Num( - "2", - ), - @14-15 Num( - "3", - ), - ], - final_comments: [ - Newline, - ], - }, - ), - ), - ), - ], - @18-19 SpaceBefore( - Var { - module_name: "", - ident: "a", - }, - [ - Newline, - ], - ), -) diff --git a/compiler/parse/tests/snapshots/pass/outdented_record.expr.result-ast b/compiler/parse/tests/snapshots/pass/outdented_record.expr.result-ast deleted file mode 100644 index 119d0dc5be..0000000000 --- a/compiler/parse/tests/snapshots/pass/outdented_record.expr.result-ast +++ /dev/null @@ -1,51 +0,0 @@ -Defs( - [ - @0-23 Value( - Body( - @0-1 Identifier( - "x", - ), - @4-23 Apply( - @4-7 Var { - module_name: "", - ident: "foo", - }, - [ - @8-23 Record( - Collection { - items: [ - @12-21 SpaceBefore( - RequiredValue( - @12-15 "bar", - [], - @17-21 Var { - module_name: "", - ident: "blah", - }, - ), - [ - Newline, - ], - ), - ], - final_comments: [ - Newline, - ], - }, - ), - ], - Space, - ), - ), - ), - ], - @24-25 SpaceBefore( - Var { - module_name: "", - ident: "x", - }, - [ - Newline, - ], - ), -) diff --git a/compiler/parse/tests/snapshots/pass/parse_alias.expr.result-ast b/compiler/parse/tests/snapshots/pass/parse_alias.expr.result-ast deleted file mode 100644 index 7db0abac4a..0000000000 --- a/compiler/parse/tests/snapshots/pass/parse_alias.expr.result-ast +++ /dev/null @@ -1,40 +0,0 @@ -Defs( - [ - @0-26 Type( - Alias { - header: TypeHeader { - name: @0-4 "Blah", - vars: [ - @5-6 Identifier( - "a", - ), - @7-8 Identifier( - "b", - ), - ], - }, - ann: @11-26 Apply( - "Foo.Bar", - "Baz", - [ - @23-24 BoundVariable( - "x", - ), - @25-26 BoundVariable( - "y", - ), - ], - ), - }, - ), - ], - @28-30 SpaceBefore( - Num( - "42", - ), - [ - Newline, - Newline, - ], - ), -) diff --git a/compiler/parse/tests/snapshots/pass/pattern_with_space_in_parens.expr.roc b/compiler/parse/tests/snapshots/pass/pattern_with_space_in_parens.expr.roc deleted file mode 100644 index c397c8ce35..0000000000 --- a/compiler/parse/tests/snapshots/pass/pattern_with_space_in_parens.expr.roc +++ /dev/null @@ -1,2 +0,0 @@ -when Delmin (Del rx) 0 is - Delmin (Del ry ) _ -> Node Black 0 False ry diff --git a/compiler/parse/tests/snapshots/pass/plus_if.expr.result-ast b/compiler/parse/tests/snapshots/pass/plus_if.expr.result-ast deleted file mode 100644 index e8a3539470..0000000000 --- a/compiler/parse/tests/snapshots/pass/plus_if.expr.result-ast +++ /dev/null @@ -1,25 +0,0 @@ -BinOps( - [ - ( - @0-1 Num( - "1", - ), - @2-3 Star, - ), - ], - @4-25 If( - [ - ( - @7-11 Tag( - "True", - ), - @17-18 Num( - "1", - ), - ), - ], - @24-25 Num( - "1", - ), - ), -) diff --git a/compiler/parse/tests/snapshots/pass/plus_if.expr.roc b/compiler/parse/tests/snapshots/pass/plus_if.expr.roc deleted file mode 100644 index 50a84b0a4d..0000000000 --- a/compiler/parse/tests/snapshots/pass/plus_if.expr.roc +++ /dev/null @@ -1 +0,0 @@ -1 * if True then 1 else 1 diff --git a/compiler/parse/tests/snapshots/pass/provides_type.header.result-ast b/compiler/parse/tests/snapshots/pass/provides_type.header.result-ast deleted file mode 100644 index 1991b3bd8f..0000000000 --- a/compiler/parse/tests/snapshots/pass/provides_type.header.result-ast +++ /dev/null @@ -1,59 +0,0 @@ -App { - header: AppHeader { - name: @4-10 PlainLine( - "test", - ), - packages: [ - @26-42 PackageEntry { - shorthand: "pf", - spaces_after_shorthand: [], - package_name: @30-42 PackageName( - "./platform", - ), - }, - ], - imports: [ - @59-70 Package( - "foo", - ModuleName( - "Bar.Baz", - ), - [], - ), - ], - provides: [ - @88-97 ExposedName( - "quicksort", - ), - ], - provides_types: Some( - [ - @102-107 UppercaseIdent( - "Flags", - ), - @109-114 UppercaseIdent( - "Model", - ), - ], - ), - to: @121-123 ExistingPackage( - "pf", - ), - before_header: [], - after_app_keyword: [], - before_packages: [ - Newline, - ], - after_packages: [], - before_imports: [ - Newline, - ], - after_imports: [], - before_provides: [ - Newline, - ], - after_provides: [], - before_to: [], - after_to: [], - }, -} diff --git a/compiler/parse/tests/snapshots/pass/record_destructure_def.expr.result-ast b/compiler/parse/tests/snapshots/pass/record_destructure_def.expr.result-ast deleted file mode 100644 index b2cbb9bbca..0000000000 --- a/compiler/parse/tests/snapshots/pass/record_destructure_def.expr.result-ast +++ /dev/null @@ -1,52 +0,0 @@ -SpaceBefore( - Defs( - [ - @18-30 Value( - Body( - @18-26 RecordDestructure( - [ - @20-21 Identifier( - "x", - ), - @23-25 Identifier( - "y", - ), - ], - ), - @29-30 Num( - "5", - ), - ), - ), - @31-36 SpaceBefore( - Value( - Body( - @31-32 Identifier( - "y", - ), - @35-36 Num( - "6", - ), - ), - ), - [ - Newline, - ], - ), - ], - @38-40 SpaceBefore( - Num( - "42", - ), - [ - Newline, - Newline, - ], - ), - ), - [ - LineComment( - " leading comment", - ), - ], -) diff --git a/compiler/parse/tests/snapshots/pass/record_with_if.expr.result-ast b/compiler/parse/tests/snapshots/pass/record_with_if.expr.result-ast deleted file mode 100644 index 5301d17702..0000000000 --- a/compiler/parse/tests/snapshots/pass/record_with_if.expr.result-ast +++ /dev/null @@ -1,30 +0,0 @@ -Record( - [ - @1-26 RequiredValue( - @1-2 "x", - [], - @5-26 If( - [ - ( - @8-12 Tag( - "True", - ), - @18-19 Num( - "1", - ), - ), - ], - @25-26 Num( - "2", - ), - ), - ), - @28-32 RequiredValue( - @28-29 "y", - [], - @31-32 Num( - "3", - ), - ), - ], -) diff --git a/compiler/parse/tests/snapshots/pass/record_with_if.expr.roc b/compiler/parse/tests/snapshots/pass/record_with_if.expr.roc deleted file mode 100644 index 7c186e5d31..0000000000 --- a/compiler/parse/tests/snapshots/pass/record_with_if.expr.roc +++ /dev/null @@ -1 +0,0 @@ -{x : if True then 1 else 2, y: 3 } \ No newline at end of file diff --git a/compiler/parse/tests/snapshots/pass/requires_type.header.result-ast b/compiler/parse/tests/snapshots/pass/requires_type.header.result-ast deleted file mode 100644 index 35b2f64345..0000000000 --- a/compiler/parse/tests/snapshots/pass/requires_type.header.result-ast +++ /dev/null @@ -1,67 +0,0 @@ -Platform { - header: PlatformHeader { - name: @9-21 PackageName( - "test/types", - ), - requires: PlatformRequires { - rigids: [ - @37-42 UppercaseIdent( - "Flags", - ), - @44-49 UppercaseIdent( - "Model", - ), - ], - signature: @55-77 TypedIdent { - ident: @55-59 "main", - spaces_before_colon: [], - ann: @62-77 Apply( - "", - "App", - [ - @66-71 Apply( - "", - "Flags", - [], - ), - @72-77 Apply( - "", - "Model", - [], - ), - ], - ), - }, - }, - exposes: [], - packages: [], - imports: [], - provides: [ - @141-152 ExposedName( - "mainForHost", - ), - ], - before_header: [], - after_platform_keyword: [], - before_requires: [ - Newline, - ], - after_requires: [], - before_exposes: [ - Newline, - ], - after_exposes: [], - before_packages: [ - Newline, - ], - after_packages: [], - before_imports: [ - Newline, - ], - after_imports: [], - before_provides: [ - Newline, - ], - after_provides: [], - }, -} diff --git a/compiler/parse/tests/snapshots/pass/two_spaced_def.expr.result-ast b/compiler/parse/tests/snapshots/pass/two_spaced_def.expr.result-ast deleted file mode 100644 index 27cab84f39..0000000000 --- a/compiler/parse/tests/snapshots/pass/two_spaced_def.expr.result-ast +++ /dev/null @@ -1,45 +0,0 @@ -SpaceBefore( - Defs( - [ - @18-23 Value( - Body( - @18-19 Identifier( - "x", - ), - @22-23 Num( - "5", - ), - ), - ), - @24-29 SpaceBefore( - Value( - Body( - @24-25 Identifier( - "y", - ), - @28-29 Num( - "6", - ), - ), - ), - [ - Newline, - ], - ), - ], - @31-33 SpaceBefore( - Num( - "42", - ), - [ - Newline, - Newline, - ], - ), - ), - [ - LineComment( - " leading comment", - ), - ], -) diff --git a/compiler/parse/tests/snapshots/pass/type_decl_with_underscore.expr.result-ast b/compiler/parse/tests/snapshots/pass/type_decl_with_underscore.expr.result-ast deleted file mode 100644 index 4575da7337..0000000000 --- a/compiler/parse/tests/snapshots/pass/type_decl_with_underscore.expr.result-ast +++ /dev/null @@ -1,40 +0,0 @@ -Defs( - [ - @0-30 Value( - Annotation( - @0-7 Identifier( - "doStuff", - ), - @10-30 Function( - [ - @10-16 Apply( - "", - "UserId", - [], - ), - ], - @20-30 Apply( - "", - "Task", - [ - @25-28 Apply( - "", - "Str", - [], - ), - @29-30 Inferred, - ], - ), - ), - ), - ), - ], - @31-33 SpaceBefore( - Num( - "42", - ), - [ - Newline, - ], - ), -) diff --git a/compiler/parse/tests/snapshots/pass/when_with_alternative_patterns.expr.result-ast b/compiler/parse/tests/snapshots/pass/when_with_alternative_patterns.expr.result-ast deleted file mode 100644 index 62dd86481e..0000000000 --- a/compiler/parse/tests/snapshots/pass/when_with_alternative_patterns.expr.result-ast +++ /dev/null @@ -1,59 +0,0 @@ -When( - @5-6 Var { - module_name: "", - ident: "x", - }, - [ - WhenBranch { - patterns: [ - @11-17 SpaceBefore( - StrLiteral( - PlainLine( - "blah", - ), - ), - [ - Newline, - ], - ), - @20-26 StrLiteral( - PlainLine( - "blop", - ), - ), - ], - value: @30-31 Num( - "1", - ), - guard: None, - }, - WhenBranch { - patterns: [ - @33-38 SpaceBefore( - StrLiteral( - PlainLine( - "foo", - ), - ), - [ - Newline, - ], - ), - @43-48 SpaceBefore( - StrLiteral( - PlainLine( - "bar", - ), - ), - [ - Newline, - ], - ), - ], - value: @52-53 Num( - "2", - ), - guard: None, - }, - ], -) diff --git a/compiler/parse/tests/snapshots/pass/when_with_alternative_patterns.expr.roc b/compiler/parse/tests/snapshots/pass/when_with_alternative_patterns.expr.roc deleted file mode 100644 index 2e4d050fe4..0000000000 --- a/compiler/parse/tests/snapshots/pass/when_with_alternative_patterns.expr.roc +++ /dev/null @@ -1,4 +0,0 @@ -when x is - "blah" | "blop" -> 1 - "foo" | - "bar" -> 2 diff --git a/compiler/parse/tests/snapshots/pass/where_clause_function.expr.result-ast b/compiler/parse/tests/snapshots/pass/where_clause_function.expr.result-ast deleted file mode 100644 index d404a9cd7a..0000000000 --- a/compiler/parse/tests/snapshots/pass/where_clause_function.expr.result-ast +++ /dev/null @@ -1,50 +0,0 @@ -Defs( - [ - @0-27 Value( - Annotation( - @0-1 Identifier( - "f", - ), - @4-27 Where( - @4-16 Function( - [ - @4-5 BoundVariable( - "a", - ), - ], - @10-16 Function( - [ - @10-11 BoundVariable( - "b", - ), - ], - @15-16 BoundVariable( - "c", - ), - ), - ), - [ - @20-27 HasClause { - var: @20-21 "a", - ability: @26-27 Apply( - "", - "A", - [], - ), - }, - ], - ), - ), - ), - ], - @29-30 SpaceBefore( - Var { - module_name: "", - ident: "f", - }, - [ - Newline, - Newline, - ], - ), -) diff --git a/compiler/parse/tests/snapshots/pass/where_clause_function.expr.roc b/compiler/parse/tests/snapshots/pass/where_clause_function.expr.roc deleted file mode 100644 index ede845156a..0000000000 --- a/compiler/parse/tests/snapshots/pass/where_clause_function.expr.roc +++ /dev/null @@ -1,3 +0,0 @@ -f : a -> (b -> c) | a has A - -f diff --git a/compiler/parse/tests/snapshots/pass/where_clause_multiple_has.expr.result-ast b/compiler/parse/tests/snapshots/pass/where_clause_multiple_has.expr.result-ast deleted file mode 100644 index 753ced4c9f..0000000000 --- a/compiler/parse/tests/snapshots/pass/where_clause_multiple_has.expr.result-ast +++ /dev/null @@ -1,66 +0,0 @@ -Defs( - [ - @0-48 Value( - Annotation( - @0-1 Identifier( - "f", - ), - @4-48 Where( - @4-16 Function( - [ - @4-5 BoundVariable( - "a", - ), - ], - @10-16 Function( - [ - @10-11 BoundVariable( - "b", - ), - ], - @15-16 BoundVariable( - "c", - ), - ), - ), - [ - @20-27 HasClause { - var: @20-21 "a", - ability: @26-27 Apply( - "", - "A", - [], - ), - }, - @29-37 HasClause { - var: @29-30 "b", - ability: @35-37 Apply( - "", - "Eq", - [], - ), - }, - @39-48 HasClause { - var: @39-40 "c", - ability: @45-48 Apply( - "", - "Ord", - [], - ), - }, - ], - ), - ), - ), - ], - @50-51 SpaceBefore( - Var { - module_name: "", - ident: "f", - }, - [ - Newline, - Newline, - ], - ), -) diff --git a/compiler/parse/tests/snapshots/pass/where_clause_multiple_has.expr.roc b/compiler/parse/tests/snapshots/pass/where_clause_multiple_has.expr.roc deleted file mode 100644 index a56e9fb184..0000000000 --- a/compiler/parse/tests/snapshots/pass/where_clause_multiple_has.expr.roc +++ /dev/null @@ -1,3 +0,0 @@ -f : a -> (b -> c) | a has A, b has Eq, c has Ord - -f diff --git a/compiler/parse/tests/snapshots/pass/where_clause_multiple_has_across_newlines.expr.result-ast b/compiler/parse/tests/snapshots/pass/where_clause_multiple_has_across_newlines.expr.result-ast deleted file mode 100644 index 2a5ad71478..0000000000 --- a/compiler/parse/tests/snapshots/pass/where_clause_multiple_has_across_newlines.expr.result-ast +++ /dev/null @@ -1,81 +0,0 @@ -Defs( - [ - @0-67 Value( - Annotation( - @0-1 Identifier( - "f", - ), - @4-67 Where( - @4-16 SpaceBefore( - Function( - [ - @4-5 BoundVariable( - "a", - ), - ], - @10-16 Function( - [ - @10-11 BoundVariable( - "b", - ), - ], - @15-16 BoundVariable( - "c", - ), - ), - ), - [ - Newline, - ], - ), - [ - @24-34 HasClause { - var: @24-25 "a", - ability: @30-34 Apply( - "", - "Hash", - [], - ), - }, - @42-50 HasClause { - var: @42-43 SpaceBefore( - "b", - [ - Newline, - ], - ), - ability: @48-50 Apply( - "", - "Eq", - [], - ), - }, - @58-67 HasClause { - var: @58-59 SpaceBefore( - "c", - [ - Newline, - ], - ), - ability: @64-67 Apply( - "", - "Ord", - [], - ), - }, - ], - ), - ), - ), - ], - @69-70 SpaceBefore( - Var { - module_name: "", - ident: "f", - }, - [ - Newline, - Newline, - ], - ), -) diff --git a/compiler/parse/tests/snapshots/pass/where_clause_multiple_has_across_newlines.expr.roc b/compiler/parse/tests/snapshots/pass/where_clause_multiple_has_across_newlines.expr.roc deleted file mode 100644 index a5e89f075f..0000000000 --- a/compiler/parse/tests/snapshots/pass/where_clause_multiple_has_across_newlines.expr.roc +++ /dev/null @@ -1,6 +0,0 @@ -f : a -> (b -> c) - | a has Hash, - b has Eq, - c has Ord - -f diff --git a/compiler/parse/tests/snapshots/pass/where_clause_non_function.expr.result-ast b/compiler/parse/tests/snapshots/pass/where_clause_non_function.expr.result-ast deleted file mode 100644 index 841d963271..0000000000 --- a/compiler/parse/tests/snapshots/pass/where_clause_non_function.expr.result-ast +++ /dev/null @@ -1,36 +0,0 @@ -Defs( - [ - @0-15 Value( - Annotation( - @0-1 Identifier( - "f", - ), - @4-15 Where( - @4-5 BoundVariable( - "a", - ), - [ - @8-15 HasClause { - var: @8-9 "a", - ability: @14-15 Apply( - "", - "A", - [], - ), - }, - ], - ), - ), - ), - ], - @17-18 SpaceBefore( - Var { - module_name: "", - ident: "f", - }, - [ - Newline, - Newline, - ], - ), -) diff --git a/compiler/parse/tests/snapshots/pass/where_clause_non_function.expr.roc b/compiler/parse/tests/snapshots/pass/where_clause_non_function.expr.roc deleted file mode 100644 index eb3374f992..0000000000 --- a/compiler/parse/tests/snapshots/pass/where_clause_non_function.expr.roc +++ /dev/null @@ -1,3 +0,0 @@ -f : a | a has A - -f diff --git a/compiler/parse/tests/snapshots/pass/where_clause_on_newline.expr.result-ast b/compiler/parse/tests/snapshots/pass/where_clause_on_newline.expr.result-ast deleted file mode 100644 index d554d5cf2b..0000000000 --- a/compiler/parse/tests/snapshots/pass/where_clause_on_newline.expr.result-ast +++ /dev/null @@ -1,50 +0,0 @@ -Defs( - [ - @0-29 Value( - Annotation( - @0-1 Identifier( - "f", - ), - @4-29 Where( - @4-12 SpaceBefore( - Function( - [ - @4-5 BoundVariable( - "a", - ), - ], - @9-12 Apply( - "", - "U64", - [], - ), - ), - [ - Newline, - ], - ), - [ - @19-29 HasClause { - var: @19-20 "a", - ability: @25-29 Apply( - "", - "Hash", - [], - ), - }, - ], - ), - ), - ), - ], - @31-32 SpaceBefore( - Var { - module_name: "", - ident: "f", - }, - [ - Newline, - Newline, - ], - ), -) diff --git a/compiler/parse/tests/snapshots/pass/where_clause_on_newline.expr.roc b/compiler/parse/tests/snapshots/pass/where_clause_on_newline.expr.roc deleted file mode 100644 index 7f29c770d3..0000000000 --- a/compiler/parse/tests/snapshots/pass/where_clause_on_newline.expr.roc +++ /dev/null @@ -1,4 +0,0 @@ -f : a -> U64 - | a has Hash - -f diff --git a/compiler/parse/tests/test_parse.rs b/compiler/parse/tests/test_parse.rs deleted file mode 100644 index c55773c28a..0000000000 --- a/compiler/parse/tests/test_parse.rs +++ /dev/null @@ -1,859 +0,0 @@ -#[macro_use] -extern crate pretty_assertions; -#[macro_use] -extern crate indoc; -extern crate bumpalo; -extern crate quickcheck; - -#[macro_use(quickcheck)] -extern crate quickcheck_macros; - -extern crate roc_module; -extern crate roc_parse; - -#[cfg(test)] -mod test_parse { - use bumpalo::collections::vec::Vec; - use bumpalo::{self, Bump}; - use roc_parse::ast::Expr::{self, *}; - use roc_parse::ast::StrLiteral::*; - use roc_parse::ast::StrSegment::*; - use roc_parse::ast::{self, EscapedChar}; - use roc_parse::module::module_defs; - use roc_parse::parser::{Parser, SyntaxError}; - use roc_parse::state::State; - use roc_parse::test_helpers::parse_expr_with; - use roc_region::all::{Loc, Region}; - use roc_test_utils::assert_multiline_str_eq; - use std::{f64, i64}; - - macro_rules! parse_snapshot_kind { - (expr => $arena:expr, $input:expr) => { - parse_expr_with($arena, $input.trim()) - }; - (header => $arena:expr, $input:expr) => { - roc_parse::module::parse_header($arena, State::new($input.trim().as_bytes())) - .map(|tuple| tuple.0) - }; - (module => $arena:expr, $input:expr) => { - module_defs() - .parse($arena, State::new($input.as_bytes())) - .map(|tuple| tuple.1) - }; - } - - macro_rules! should_pass { - (pass) => { - true - }; - (fail) => { - false - }; - } - - macro_rules! snapshot_tests { - ( - $($pass_or_fail:ident / $test_name:ident . $kind:ident),* - $(,)? - ) => { - #[test] - fn no_extra_snapshot_test_files() { - let tests = &[ - $(concat!( - stringify!($pass_or_fail), - "/", - stringify!($test_name), - ".", - stringify!($kind) - )),*, - ].iter().map(|t| *t).collect::>(); - - fn list(dir: &std::path::Path) -> std::vec::Vec { - std::fs::read_dir(dir).unwrap().map(|f| f.unwrap().file_name().to_str().unwrap().to_string()).collect::>() - } - - let mut base = std::path::PathBuf::from("tests"); - base.push("snapshots"); - let pass_or_fail_names = list(&base); - let mut extra_test_files = std::collections::HashSet::new(); - for res in pass_or_fail_names { - assert!(res == "pass" || res == "fail", "a pass or fail filename was neither \"pass\" nor \"fail\", but rather: {:?}", res); - let res_dir = base.join(&res); - for file in list(&res_dir) { - let test = if let Some(test) = file.strip_suffix(".roc") { - test - } else if let Some(test) = file.strip_suffix(".result-ast") { - test - } else { - panic!("unexpected file found in tests/snapshots: {}", file); - }; - let test_name = format!("{}/{}", &res, test); - if !tests.contains(test_name.as_str()) { - extra_test_files.insert(test_name); - } - } - } - - if extra_test_files.len() > 0 { - eprintln!("Found extra test files:"); - for file in extra_test_files { - eprintln!("{}", file); - } - panic!("Add entries for these in the `snapshot_tests!` macro in test_parse.rs"); - } - } - - $( - #[test] - fn $test_name() { - snapshot_test(should_pass!($pass_or_fail), stringify!($test_name), stringify!($kind), |input| { - let arena = Bump::new(); - let result = parse_snapshot_kind!($kind => &arena, input); - result - .map(|actual_ast| format!("{:#?}\n", actual_ast)) - .map_err(|error| format!("{:?}", error)) - }); - } - )* - }; - } - - // see tests/snapshots to see test input(.roc) and expected output(.result-ast) - snapshot_tests! { - fail/type_argument_no_arrow.expr, - fail/type_double_comma.expr, - pass/plus_if.expr, - pass/list_closing_indent_not_enough.expr, - pass/ability_single_line.expr, - pass/ability_multi_line.expr, - pass/ability_demand_signature_is_multiline.expr, - pass/ability_two_in_a_row.expr, - pass/add_var_with_spaces.expr, - pass/add_with_spaces.expr, - pass/annotated_record_destructure.expr, - pass/annotated_tag_destructure.expr, - pass/apply_tag.expr, - pass/apply_parenthetical_tag_args.expr, - pass/apply_three_args.expr, - pass/apply_two_args.expr, - pass/apply_unary_negation.expr, - pass/apply_unary_not.expr, - pass/basic_apply.expr, - pass/basic_docs.expr, - pass/basic_field.expr, - pass/basic_tag.expr, - pass/basic_var.expr, - pass/closure_with_underscores.expr, - pass/comment_after_def.module, - pass/comment_after_op.expr, - pass/comment_before_op.expr, - pass/comment_inside_empty_list.expr, - pass/comment_with_non_ascii.expr, - pass/destructure_tag_assignment.expr, - pass/empty_app_header.header, - pass/empty_interface_header.header, - pass/empty_hosted_header.header, - pass/nonempty_hosted_header.header, - pass/empty_list.expr, - pass/empty_platform_header.header, - pass/empty_record.expr, - pass/empty_string.expr, - pass/equals_with_spaces.expr, - pass/equals.expr, - pass/expect.expr, - pass/record_type_with_function.expr, - pass/float_with_underscores.expr, - pass/full_app_header_trailing_commas.header, - pass/full_app_header.header, - pass/function_effect_types.header, - pass/highest_float.expr, - pass/highest_int.expr, - pass/if_def.expr, - pass/int_with_underscore.expr, - pass/interface_with_newline.header, - pass/lowest_float.expr, - pass/list_closing_same_indent_no_trailing_comma.expr, - pass/list_closing_same_indent_with_trailing_comma.expr, - pass/lowest_int.expr, - pass/malformed_ident_due_to_underscore.expr, - pass/malformed_pattern_field_access.expr, // See https://github.com/rtfeldman/roc/issues/399 - pass/malformed_pattern_module_name.expr, // See https://github.com/rtfeldman/roc/issues/399 - pass/minimal_app_header.header, - pass/minus_twelve_minus_five.expr, - pass/mixed_docs.expr, - pass/module_def_newline.module, - pass/multi_backpassing.expr, - pass/multi_char_string.expr, - pass/multiline_type_signature_with_comment.expr, - pass/multiline_type_signature.expr, - pass/multiple_fields.expr, - pass/multiple_operators.expr, - pass/neg_inf_float.expr, - pass/negative_float.expr, - pass/negative_int.expr, - pass/nested_def_annotation.module, - pass/nested_if.expr, - pass/nested_module.header, - pass/newline_after_equals.expr, // Regression test for https://github.com/rtfeldman/roc/issues/51 - pass/newline_after_mul.expr, - pass/newline_after_sub.expr, - pass/newline_and_spaces_before_less_than.expr, - pass/newline_before_add.expr, - pass/newline_before_sub.expr, - pass/newline_inside_empty_list.expr, - pass/newline_singleton_list.expr, - pass/nonempty_platform_header.header, - pass/not_docs.expr, - pass/number_literal_suffixes.expr, - pass/one_backpassing.expr, - pass/one_char_string.expr, - pass/one_def.expr, - pass/one_minus_two.expr, - pass/one_plus_two.expr, - pass/one_spaced_def.expr, - pass/opaque_has_abilities.expr, - pass/opaque_simple.module, - pass/opaque_with_type_arguments.module, - pass/opaque_reference_expr.expr, - pass/opaque_reference_expr_with_arguments.expr, - pass/opaque_reference_pattern.expr, - pass/opaque_reference_pattern_with_arguments.expr, - pass/ops_with_newlines.expr, - pass/outdented_list.expr, - pass/outdented_record.expr, - pass/outdented_app_with_record.expr, - pass/packed_singleton_list.expr, - pass/parenthetical_apply.expr, - pass/parenthetical_basic_field.expr, - pass/parenthetical_field_qualified_var.expr, - pass/parenthetical_var.expr, - pass/parse_alias.expr, - pass/parse_as_ann.expr, - pass/pattern_with_space_in_parens.expr, // https://github.com/rtfeldman/roc/issues/929 - pass/pos_inf_float.expr, - pass/positive_float.expr, - pass/positive_int.expr, - pass/provides_type.header, - pass/qualified_field.expr, - pass/qualified_tag.expr, - pass/qualified_var.expr, - pass/record_destructure_def.expr, - pass/record_func_type_decl.expr, - pass/record_update.expr, - pass/record_with_if.expr, - pass/requires_type.header, - pass/single_arg_closure.expr, - pass/single_underscore_closure.expr, - pass/space_only_after_minus.expr, - pass/spaced_singleton_list.expr, - pass/spaces_inside_empty_list.expr, - pass/standalone_module_defs.module, - pass/string_without_escape.expr, - pass/sub_var_with_spaces.expr, - pass/sub_with_spaces.expr, - pass/tag_pattern.expr, - pass/ten_times_eleven.expr, - pass/three_arg_closure.expr, - pass/two_arg_closure.expr, - pass/two_backpassing.expr, - pass/two_branch_when.expr, - pass/two_spaced_def.expr, - pass/type_decl_with_underscore.expr, - pass/unary_negation_access.expr, // Regression test for https://github.com/rtfeldman/roc/issues/509 - pass/unary_negation_arg.expr, - pass/unary_negation_with_parens.expr, - pass/unary_negation.expr, - pass/unary_not_with_parens.expr, - pass/unary_not.expr, - pass/underscore_backpassing.expr, - pass/var_else.expr, - pass/var_if.expr, - pass/var_is.expr, - pass/var_minus_two.expr, - pass/var_then.expr, - pass/var_when.expr, - pass/when_if_guard.expr, - pass/when_in_parens_indented.expr, - pass/when_in_parens.expr, - pass/when_with_alternative_patterns.expr, - pass/when_with_function_application.expr, - pass/when_with_negative_numbers.expr, - pass/when_with_numbers.expr, - pass/when_with_records.expr, - pass/where_clause_function.expr, - pass/where_clause_non_function.expr, - pass/where_clause_multiple_has.expr, - pass/where_clause_multiple_has_across_newlines.expr, - pass/where_clause_on_newline.expr, - pass/zero_float.expr, - pass/zero_int.expr, - } - - fn snapshot_test( - should_pass: bool, - name: &str, - ty: &str, - func: impl Fn(&str) -> Result, - ) { - let mut parent = std::path::PathBuf::from("tests"); - parent.push("snapshots"); - parent.push(if should_pass { "pass" } else { "fail" }); - let input_path = parent.join(&format!("{}.{}.roc", name, ty)); - let result_path = parent.join(&format!("{}.{}.result-ast", name, ty)); - - let input = std::fs::read_to_string(&input_path).unwrap_or_else(|err| { - panic!( - "Could not find a snapshot test result at {:?} - {:?}", - input_path, err - ) - }); - - let result = func(&input); - - let actual_result = if should_pass { - result.expect("The source code for this test did not successfully parse!") - } else { - result.expect_err( - "The source code for this test successfully parsed, but it was not expected to!", - ) - }; - - if std::env::var("ROC_PARSER_SNAPSHOT_TEST_OVERWRITE").is_ok() { - std::fs::write(&result_path, actual_result).unwrap(); - } else { - let expected_result = std::fs::read_to_string(&result_path).unwrap_or_else(|e| { - panic!( - "Error opening test output file {}:\n\ - {:?} - Supposing the file is missing, consider running the tests with:\n\ - `env ROC_PARSER_SNAPSHOT_TEST_OVERWRITE=1 cargo test ...`\n\ - and committing the file that creates.", - result_path.display(), - e - ); - }); - - assert_multiline_str_eq!(expected_result, actual_result); - } - } - - fn assert_parses_to<'a>(input: &'a str, expected_expr: Expr<'a>) { - let arena = Bump::new(); - let actual = parse_expr_with(&arena, input.trim()); - assert_eq!(Ok(expected_expr), actual); - } - - fn assert_parsing_fails(input: &str, _reason: SyntaxError) { - let arena = Bump::new(); - let actual = parse_expr_with(&arena, input); - - assert!(actual.is_err()); - } - - fn assert_segments Vec<'_, ast::StrSegment<'_>>>(input: &str, to_expected: E) { - let arena = Bump::new(); - let actual = parse_expr_with(&arena, arena.alloc(input)); - let expected_slice = to_expected(&arena); - let expected_expr = Expr::Str(Line(&expected_slice)); - - assert_eq!(Ok(expected_expr), actual); - } - - fn parses_with_escaped_char< - I: Fn(&str) -> String, - E: Fn(EscapedChar, &Bump) -> Vec<'_, ast::StrSegment<'_>>, - >( - to_input: I, - to_expected: E, - ) { - let arena = Bump::new(); - - // Try parsing with each of the escaped chars Roc supports - for (string, escaped) in &[ - ("\\\\", EscapedChar::Backslash), - ("\\n", EscapedChar::Newline), - ("\\r", EscapedChar::CarriageReturn), - ("\\t", EscapedChar::Tab), - ("\\\"", EscapedChar::Quote), - ] { - let actual = parse_expr_with(&arena, arena.alloc(to_input(string))); - let expected_slice = to_expected(*escaped, &arena); - let expected_expr = Expr::Str(Line(&expected_slice)); - - assert_eq!(Ok(expected_expr), actual); - } - } - - // BACKSLASH ESCAPES - - #[test] - fn string_with_escaped_char_at_end() { - parses_with_escaped_char( - |esc| format!(r#""abcd{}""#, esc), - |esc, arena| bumpalo::vec![in arena; Plaintext("abcd"), EscapedChar(esc)], - ); - } - - #[test] - fn string_with_escaped_char_in_front() { - parses_with_escaped_char( - |esc| format!(r#""{}abcd""#, esc), - |esc, arena| bumpalo::vec![in arena; EscapedChar(esc), Plaintext("abcd")], - ); - } - - #[test] - fn string_with_escaped_char_in_middle() { - parses_with_escaped_char( - |esc| format!(r#""ab{}cd""#, esc), - |esc, arena| bumpalo::vec![in arena; Plaintext("ab"), EscapedChar(esc), Plaintext("cd")], - ); - } - - #[test] - fn string_with_multiple_escaped_chars() { - parses_with_escaped_char( - |esc| format!(r#""{}abc{}de{}fghi{}""#, esc, esc, esc, esc), - |esc, arena| bumpalo::vec![in arena; EscapedChar(esc), Plaintext("abc"), EscapedChar(esc), Plaintext("de"), EscapedChar(esc), Plaintext("fghi"), EscapedChar(esc)], - ); - } - - // UNICODE ESCAPES - - #[test] - fn unicode_escape_in_middle() { - assert_segments(r#""Hi, \u(123)!""#, |arena| { - bumpalo::vec![in arena; - Plaintext("Hi, "), - Unicode(Loc::new(8, 11, "123")), - Plaintext("!") - ] - }); - } - - #[test] - fn unicode_escape_in_front() { - assert_segments(r#""\u(1234) is a unicode char""#, |arena| { - bumpalo::vec![in arena; - Unicode(Loc::new(4, 8, "1234")), - Plaintext(" is a unicode char") - ] - }); - } - - #[test] - fn unicode_escape_in_back() { - assert_segments(r#""this is unicode: \u(1)""#, |arena| { - bumpalo::vec![in arena; - Plaintext("this is unicode: "), - Unicode(Loc::new(21, 22, "1")) - ] - }); - } - - #[test] - fn unicode_escape_multiple() { - assert_segments(r#""\u(a1) this is \u(2Bcd) unicode \u(ef97)""#, |arena| { - bumpalo::vec![in arena; - Unicode(Loc::new(4, 6, "a1")), - Plaintext(" this is "), - Unicode(Loc::new(19, 23, "2Bcd")), - Plaintext(" unicode "), - Unicode(Loc::new(36, 40, "ef97")) - ] - }); - } - - // INTERPOLATION - - #[test] - fn string_with_interpolation_in_middle() { - assert_segments(r#""Hi, \(name)!""#, |arena| { - let expr = arena.alloc(Var { - module_name: "", - ident: "name", - }); - - bumpalo::vec![in arena; - Plaintext("Hi, "), - Interpolated(Loc::new(7, 11, expr)), - Plaintext("!") - ] - }); - } - - #[test] - fn string_with_interpolation_in_front() { - assert_segments(r#""\(name), hi!""#, |arena| { - let expr = arena.alloc(Var { - module_name: "", - ident: "name", - }); - - bumpalo::vec![in arena; - Interpolated(Loc::new(3, 7, expr)), - Plaintext(", hi!") - ] - }); - } - - #[test] - fn string_with_interpolation_in_back() { - assert_segments(r#""Hello \(name)""#, |arena| { - let expr = arena.alloc(Var { - module_name: "", - ident: "name", - }); - - bumpalo::vec![in arena; - Plaintext("Hello "), - Interpolated(Loc::new(9, 13, expr)) - ] - }); - } - - #[test] - fn string_with_multiple_interpolations() { - assert_segments(r#""Hi, \(name)! How is \(project) going?""#, |arena| { - let expr1 = arena.alloc(Var { - module_name: "", - ident: "name", - }); - - let expr2 = arena.alloc(Var { - module_name: "", - ident: "project", - }); - - bumpalo::vec![in arena; - Plaintext("Hi, "), - Interpolated(Loc::new(7, 11, expr1)), - Plaintext("! How is "), - Interpolated(Loc::new(23, 30, expr2)), - Plaintext(" going?") - ] - }); - } - - #[test] - fn empty_source_file() { - assert_parsing_fails("", SyntaxError::Eof(Region::zero())); - } - - #[quickcheck] - fn all_i64_values_parse(num: i64) { - assert_parses_to(num.to_string().as_str(), Num(num.to_string().as_str())); - } - - #[quickcheck] - fn all_f64_values_parse(mut num: f64) { - // NaN, Infinity, -Infinity (these would all parse as tags in Roc) - if !num.is_finite() { - num = 0.0; - } - - // These can potentially be whole numbers. `Display` omits the decimal point for those, - // causing them to no longer be parsed as fractional numbers by Roc. - // Using `Debug` instead of `Display` ensures they always have a decimal point. - let float_string = format!("{:?}", num); - - assert_parses_to(float_string.as_str(), Float(float_string.as_str())); - } - - // SINGLE QUOTE LITERAL - #[test] - fn single_quote() { - assert_parses_to("'b'", Expr::SingleQuote("b")); - } - - // RECORD LITERALS - - // #[test] - // fn type_signature_def() { - // let arena = Bump::new(); - // let newline = bumpalo::vec![in &arena; Newline]; - // let newlines = bumpalo::vec![in &arena; Newline, Newline]; - // let applied_ann = TypeAnnotation::Apply("", "Int", &[]); - // let signature = Def::Annotation( - // Located::new(0, 0, 0, 3, Identifier("foo")), - // Located::new(0, 0, 6, 9, applied_ann), - // ); - // let def = Def::Body( - // arena.alloc(Located::new(1, 1, 0, 3, Identifier("foo"))), - // arena.alloc(Located::new(1, 1, 6, 7, Num("4"))), - // ); - // let spaced_def = Def::SpaceBefore(arena.alloc(def), &newline); - // let loc_def = &*arena.alloc(Located::new(1, 1, 0, 7, spaced_def)); - - // let loc_ann = &*arena.alloc(Located::new(0, 0, 0, 3, signature)); - // let defs = &[loc_ann, loc_def]; - // let ret = Expr::SpaceBefore(arena.alloc(Num("42")), &newlines); - // let loc_ret = Located::new(3, 3, 0, 2, ret); - // let expected = Defs(defs, arena.alloc(loc_ret)); - - // assert_parses_to( - // indoc!( - // r#" - // foo : Int - // foo = 4 - - // 42 - // "# - // ), - // expected, - // ); - // } - - // #[test] - // fn type_signature_function_def() { - // use TypeAnnotation; - // let arena = Bump::new(); - // let newline = bumpalo::vec![in &arena; Newline]; - // let newlines = bumpalo::vec![in &arena; Newline, Newline]; - - // let int_type = TypeAnnotation::Apply("", "Int", &[]); - // let float_type = TypeAnnotation::Apply("", "Float", &[]); - // let bool_type = TypeAnnotation::Apply("", "Bool", &[]); - - // let arguments = bumpalo::vec![in &arena; - // Located::new(0, 0, 6, 9, int_type), - // Located::new(0, 0, 11, 16, float_type) - // ]; - // let return_type = Located::new(0, 0, 20, 24, bool_type); - // let fn_ann = TypeAnnotation::Function(&arguments, &return_type); - // let signature = Def::Annotation( - // Located::new(0, 0, 0, 3, Identifier("foo")), - // Located::new(0, 0, 20, 24, fn_ann), - // ); - - // let args = bumpalo::vec![in &arena; - // Located::new(1,1,7,8, Identifier("x")), - // Located::new(1,1,10,11, Underscore) - // ]; - // let body = Located::new(1, 1, 15, 17, Num("42")); - - // let closure = Expr::Closure(&args, &body); - - // let def = Def::Body( - // arena.alloc(Located::new(1, 1, 0, 3, Identifier("foo"))), - // arena.alloc(Located::new(1, 1, 6, 17, closure)), - // ); - // let spaced = Def::SpaceBefore(arena.alloc(def), newline.into_bump_slice()); - // let loc_def = &*arena.alloc(Located::new(1, 1, 0, 17, spaced)); - - // let loc_ann = &*arena.alloc(Located::new(0, 0, 0, 3, signature)); - // let defs = &[loc_ann, loc_def]; - // let ret = Expr::SpaceBefore(arena.alloc(Num("42")), newlines.into_bump_slice()); - // let loc_ret = Located::new(3, 3, 0, 2, ret); - // let expected = Defs(defs, arena.alloc(loc_ret)); - - // assert_parses_to( - // indoc!( - // r#" - // foo : Int, Float -> Bool - // foo = \x, _ -> 42 - - // 42 - // "# - // ), - // expected, - // ); - // } - - // #[test] - // fn ann_open_union() { - // let arena = Bump::new(); - // let newline = bumpalo::vec![in &arena; Newline]; - // let newlines = bumpalo::vec![in &arena; Newline, Newline]; - // let tag1 = Tag::Apply { - // name: Located::new(0, 0, 8, 12, "True"), - // args: &[], - // }; - // let tag2arg = Located::new(0, 0, 22, 27, TypeAnnotation::Apply("", "Thing", &[])); - // let tag2args = bumpalo::vec![in &arena; tag2arg]; - // let tag2 = Tag::Apply { - // name: Located::new(0, 0, 14, 21, "Perhaps"), - // args: tag2args.into_bump_slice(), - // }; - // let tags = bumpalo::vec![in &arena; - // Located::new(0, 0, 8, 12, tag1), - // Located::new(0, 0, 14, 27, tag2) - // ]; - // let loc_wildcard = Located::new(0, 0, 29, 30, TypeAnnotation::Wildcard); - // let applied_ann = TypeAnnotation::TagUnion { - // tags: tags.into_bump_slice(), - // ext: Some(arena.alloc(loc_wildcard)), - // }; - // let signature = Def::Annotation( - // Located::new(0, 0, 0, 3, Identifier("foo")), - // Located::new(0, 0, 6, 30, applied_ann), - // ); - // let def = Def::Body( - // arena.alloc(Located::new(1, 1, 0, 3, Identifier("foo"))), - // arena.alloc(Located::new(1, 1, 6, 10, Expr::Tag("True"))), - // ); - // let spaced_def = Def::SpaceBefore(arena.alloc(def), newline.into_bump_slice()); - // let loc_def = &*arena.alloc(Located::new(1, 1, 0, 10, spaced_def)); - - // let loc_ann = &*arena.alloc(Located::new(0, 0, 0, 3, signature)); - // let defs = &[loc_ann, loc_def]; - // let ret = Expr::SpaceBefore(arena.alloc(Num("42")), newlines.into_bump_slice()); - // let loc_ret = Located::new(3, 3, 0, 2, ret); - // let expected = Defs(defs, arena.alloc(loc_ret)); - - // assert_parses_to( - // indoc!( - // r#" - // foo : [True, Perhaps Thing]* - // foo = True - - // 42 - // "# - // ), - // expected, - // ); - // } - - // #[test] - // fn ann_closed_union() { - // let arena = Bump::new(); - // let newline = bumpalo::vec![in &arena; Newline]; - // let newlines = bumpalo::vec![in &arena; Newline, Newline]; - // let tag1 = Tag::Apply { - // name: Located::new(0, 0, 8, 12, "True"), - // args: &[], - // }; - // let tag2arg = Located::new(0, 0, 22, 27, TypeAnnotation::Apply("", "Thing", &[])); - // let tag2args = bumpalo::vec![in &arena; tag2arg]; - // let tag2 = Tag::Apply { - // name: Located::new(0, 0, 14, 21, "Perhaps"), - // args: tag2args.into_bump_slice(), - // }; - // let tags = bumpalo::vec![in &arena; - // Located::new(0, 0, 8, 12, tag1), - // Located::new(0, 0, 14, 27, tag2) - // ]; - // let applied_ann = TypeAnnotation::TagUnion { - // tags: tags.into_bump_slice(), - // ext: None, - // }; - // let signature = Def::Annotation( - // Located::new(0, 0, 0, 3, Identifier("foo")), - // Located::new(0, 0, 6, 29, applied_ann), - // ); - // let def = Def::Body( - // arena.alloc(Located::new(1, 1, 0, 3, Identifier("foo"))), - // arena.alloc(Located::new(1, 1, 6, 10, Expr::Tag("True"))), - // ); - // let spaced_def = Def::SpaceBefore(arena.alloc(def), newline.into_bump_slice()); - // let loc_def = &*arena.alloc(Located::new(1, 1, 0, 10, spaced_def)); - - // let loc_ann = &*arena.alloc(Located::new(0, 0, 0, 3, signature)); - // let defs = &[loc_ann, loc_def]; - // let ret = Expr::SpaceBefore(arena.alloc(Num("42")), newlines.into_bump_slice()); - // let loc_ret = Located::new(3, 3, 0, 2, ret); - // let expected = Defs(defs, arena.alloc(loc_ret)); - - // assert_parses_to( - // indoc!( - // r#" - // foo : [True, Perhaps Thing] - // foo = True - - // 42 - // "# - // ), - // expected, - // ); - // } - - #[test] - fn repro_keyword_bug() { - // Reproducing this bug requires a bizarre set of things to all be true: - // - // * Must be parsing a *module* def (nested expr defs don't repro this) - // * That top-level module def contains a def inside it - // * That inner def is defining a function - // * The name of the inner def begins with a keyword (`if`, `then`, `else`, `when`, `is`) - // - // If all of these are true, then lookups on that def get skipped over by the parser. - // If any one of the above is false, then everything works. - - let arena = Bump::new(); - let src = indoc!( - r#" - foo = \list -> - isTest = \_ -> 5 - List.map list isTest - "# - ); - let actual = module_defs() - .parse(&arena, State::new(src.as_bytes())) - .map(|tuple| tuple.1); - - // It should occur twice in the debug output - once for the pattern, - // and then again for the lookup. - let occurrences = format!("{:?}", actual).split("isTest").count() - 1; - - assert_eq!(occurrences, 2); - } - - #[test] - fn outdenting_newline_after_else() { - let arena = &Bump::new(); - - // highlights a problem with the else branch demanding a newline after its expression - let src = indoc!( - r#" - main = - v = \y -> if x then y else z - - 1 - "# - ); - - let state = State::new(src.as_bytes()); - let parser = module_defs(); - let parsed = parser.parse(arena, state); - match parsed { - Ok((_, _, _state)) => { - // dbg!(_state); - } - Err((_, _fail, _state)) => { - // dbg!(_fail, _state); - panic!("Failed to parse!"); - } - } - } - - #[test] - fn parse_expr_size() { - assert_eq!(std::mem::size_of::(), 40); - } - - // PARSE ERROR - - // TODO this should be parse error, but isn't! - // #[test] - // fn trailing_paren() { - // assert_parses_to( - // indoc!( - // r#" - // r = "foo" - // s = { left : "foo" } - - // when 0 is - // 1 -> { x: s.left, y: s.left } - // 0 -> { x: s.left, y: r } - // ) - // "# - // ), - // Str(PlainLine("")), - // ); - // } - - // TODO test for non-ASCII variables - // - // TODO verify that when a string literal contains a newline before the - // closing " it correctly updates both the line *and* column in the State. -} diff --git a/compiler/problem/Cargo.toml b/compiler/problem/Cargo.toml deleted file mode 100644 index 9aa102ff5f..0000000000 --- a/compiler/problem/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "roc_problem" -version = "0.1.0" -authors = ["The Roc Contributors"] -license = "UPL-1.0" -edition = "2021" - -[dependencies] -roc_collections = { path = "../collections" } -roc_region = { path = "../region" } -roc_module = { path = "../module" } -roc_types = { path = "../types" } -roc_parse = { path = "../parse" } diff --git a/compiler/problem/src/can.rs b/compiler/problem/src/can.rs deleted file mode 100644 index 6f8a28e3df..0000000000 --- a/compiler/problem/src/can.rs +++ /dev/null @@ -1,306 +0,0 @@ -use roc_collections::all::MutSet; -use roc_module::called_via::BinOp; -use roc_module::ident::{Ident, Lowercase, ModuleName, TagName}; -use roc_module::symbol::{ModuleId, Symbol}; -use roc_parse::ast::Base; -use roc_parse::pattern::PatternType; -use roc_region::all::{Loc, Region}; -use roc_types::types::AliasKind; - -#[derive(Clone, Copy, Debug, PartialEq)] -pub struct CycleEntry { - pub symbol: Symbol, - pub symbol_region: Region, - pub expr_region: Region, -} - -#[derive(Clone, Debug, PartialEq)] -pub enum BadPattern { - UnderscoreInDef, - Unsupported(PatternType), -} - -#[derive(Clone, Copy, Debug, PartialEq)] -pub enum ShadowKind { - Variable, - Alias, - Opaque, - Ability, -} - -/// Problems that can occur in the course of canonicalization. -#[derive(Clone, Debug, PartialEq)] -pub enum Problem { - UnusedDef(Symbol, Region), - UnusedImport(ModuleId, Region), - ExposedButNotDefined(Symbol), - UnknownGeneratesWith(Loc), - /// First symbol is the name of the closure with that argument - /// Second symbol is the name of the argument that is unused - UnusedArgument(Symbol, Symbol, Region), - PrecedenceProblem(PrecedenceProblem), - // Example: (5 = 1 + 2) is an unsupported pattern in an assignment; Int patterns aren't allowed in assignments! - UnsupportedPattern(BadPattern, Region), - Shadowing { - original_region: Region, - shadow: Loc, - kind: ShadowKind, - }, - CyclicAlias(Symbol, Region, Vec), - BadRecursion(Vec), - PhantomTypeArgument { - typ: Symbol, - variable_region: Region, - variable_name: Lowercase, - }, - UnboundTypeVariable { - typ: Symbol, - num_unbound: usize, - one_occurrence: Region, - kind: AliasKind, - }, - DuplicateRecordFieldValue { - field_name: Lowercase, - record_region: Region, - field_region: Region, - replaced_region: Region, - }, - DuplicateRecordFieldType { - field_name: Lowercase, - record_region: Region, - field_region: Region, - replaced_region: Region, - }, - InvalidOptionalValue { - field_name: Lowercase, - record_region: Region, - field_region: Region, - }, - - DuplicateTag { - tag_name: TagName, - tag_union_region: Region, - tag_region: Region, - replaced_region: Region, - }, - RuntimeError(RuntimeError), - SignatureDefMismatch { - annotation_pattern: Region, - def_pattern: Region, - }, - InvalidAliasRigid { - alias_name: Symbol, - region: Region, - }, - InvalidInterpolation(Region), - InvalidHexadecimal(Region), - InvalidUnicodeCodePt(Region), - NestedDatatype { - alias: Symbol, - def_region: Region, - differing_recursion_region: Region, - }, - InvalidExtensionType { - region: Region, - kind: ExtensionTypeKind, - }, - AbilityHasTypeVariables { - name: Symbol, - variables_region: Region, - }, - HasClauseIsNotAbility { - region: Region, - }, - IllegalHasClause { - region: Region, - }, - AbilityMemberMissingHasClause { - member: Symbol, - ability: Symbol, - region: Region, - }, - AbilityMemberMultipleBoundVars { - member: Symbol, - ability: Symbol, - span_has_clauses: Region, - bound_var_names: Vec, - }, - AbilityNotOnToplevel { - region: Region, - }, - AbilityUsedAsType(Lowercase, Symbol, Region), - NestedSpecialization(Symbol, Region), - IllegalDerive(Region), -} - -#[derive(Clone, Debug, PartialEq)] -pub enum ExtensionTypeKind { - Record, - TagUnion, -} - -#[derive(Clone, Debug, PartialEq)] -pub enum PrecedenceProblem { - BothNonAssociative(Region, Loc, Loc), -} - -/// Enum to store the various types of errors that can cause parsing an integer to fail. -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum IntErrorKind { - /// Value being parsed is empty. - /// - /// Among other causes, this variant will be constructed when parsing an empty string. - /// In roc, this can happen with non-base-10 literals, e.g. `0x` or `0b` without any digits - Empty, - /// Contains an invalid digit. - /// - /// Among other causes, this variant will be constructed when parsing a string that - /// contains a letter. - InvalidDigit, - /// Integer is too large to store in target integer type. - Overflow, - /// Integer is too small to store in target integer type. - Underflow, - /// This is an integer, but it has a float numeric suffix. - FloatSuffix, - /// The integer literal overflows the width of the suffix associated with it. - OverflowsSuffix { - suffix_type: &'static str, - max_value: u128, - }, - /// The integer literal underflows the width of the suffix associated with it. - UnderflowsSuffix { - suffix_type: &'static str, - min_value: i128, - }, -} - -/// Enum to store the various types of errors that can cause parsing a float to fail. -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum FloatErrorKind { - /// Probably an invalid digit - Error, - /// the literal is too small for f64 - NegativeInfinity, - /// the literal is too large for f64 - PositiveInfinity, - /// This is a float, but it has an integer numeric suffix. - IntSuffix, -} - -#[derive(Clone, Debug, PartialEq)] -pub enum RuntimeError { - Shadowing { - original_region: Region, - shadow: Loc, - kind: ShadowKind, - }, - InvalidOptionalValue { - field_name: Lowercase, - record_region: Region, - field_region: Region, - }, - // Example: (5 = 1 + 2) is an unsupported pattern in an assignment; Int patterns aren't allowed in assignments! - UnsupportedPattern(Region), - // Example: when 1 is 1.X -> 32 - MalformedPattern(MalformedPatternProblem, Region), - - UnresolvedTypeVar, - ErroneousType, - - LookupNotInScope(Loc, MutSet>), - OpaqueNotDefined { - usage: Loc, - opaques_in_scope: MutSet>, - opt_defined_alias: Option, - }, - OpaqueOutsideScope { - opaque: Ident, - referenced_region: Region, - imported_region: Region, - }, - OpaqueNotApplied(Loc), - OpaqueAppliedToMultipleArgs(Region), - ValueNotExposed { - module_name: ModuleName, - ident: Ident, - region: Region, - exposed_values: Vec, - }, - /// A module was referenced, but hasn't been imported anywhere in the program - /// - /// An example would be: - /// ```roc - /// app "hello" - /// packages { pf: "platform" } - /// imports [pf.Stdout] - /// provides [main] to pf - /// - /// main : Task.Task {} [] // Task isn't imported! - /// main = Stdout.line "I'm a Roc application!" - /// ``` - ModuleNotImported { - /// The name of the module that was referenced - module_name: ModuleName, - /// A list of modules which *have* been imported - imported_modules: MutSet>, - /// Where the problem occurred - region: Region, - /// Whether or not the module exists at all - /// - /// This is used to suggest that the user import the module, as opposed to fix a - /// typo in the spelling. For example, if the user typed `Task`, and the platform - /// exposes a `Task` module that hasn't been imported, we can sugguest that they - /// add the import statement. - /// - /// On the other hand, if the user typed `Tesk`, they might want to check their - /// spelling. - /// - /// If unsure, this should be set to `false` - module_exists: bool, - }, - InvalidPrecedence(PrecedenceProblem, Region), - MalformedIdentifier(Box, roc_parse::ident::BadIdent, Region), - MalformedTypeName(Box, Region), - MalformedClosure(Region), - InvalidRecordUpdate { - region: Region, - }, - InvalidFloat(FloatErrorKind, Region, Box), - InvalidInt(IntErrorKind, Base, Region, Box), - CircularDef(Vec), - - NonExhaustivePattern, - - InvalidInterpolation(Region), - InvalidHexadecimal(Region), - InvalidUnicodeCodePt(Region), - - /// When the author specifies a type annotation but no implementation - NoImplementationNamed { - def_symbol: Symbol, - }, - NoImplementation, - - /// cases where the `[]` value (or equivalently, `forall a. a`) pops up - VoidValue, - - ExposedButNotDefined(Symbol), - - /// where '' - EmptySingleQuote(Region), - /// where 'aa' - MultipleCharsInSingleQuote(Region), -} - -#[derive(Clone, Copy, Debug, PartialEq)] -pub enum MalformedPatternProblem { - MalformedInt, - MalformedFloat, - MalformedBase(Base), - Unknown, - QualifiedIdentifier, - BadIdent(roc_parse::ident::BadIdent), - EmptySingleQuote, - MultipleCharsInSingleQuote, -} diff --git a/compiler/problem/src/lib.rs b/compiler/problem/src/lib.rs deleted file mode 100644 index 9da9d7b105..0000000000 --- a/compiler/problem/src/lib.rs +++ /dev/null @@ -1,4 +0,0 @@ -#![warn(clippy::dbg_macro)] -// See github.com/rtfeldman/roc/issues/800 for discussion of the large_enum_variant check. -#![allow(clippy::large_enum_variant)] -pub mod can; diff --git a/compiler/region/Cargo.toml b/compiler/region/Cargo.toml deleted file mode 100644 index d80e07b9a0..0000000000 --- a/compiler/region/Cargo.toml +++ /dev/null @@ -1,9 +0,0 @@ -[package] -name = "roc_region" -version = "0.1.0" -authors = ["The Roc Contributors"] -license = "UPL-1.0" -edition = "2021" - -[dependencies] -static_assertions = "1.1.0" diff --git a/compiler/region/src/all.rs b/compiler/region/src/all.rs deleted file mode 100644 index 3eed5060d5..0000000000 --- a/compiler/region/src/all.rs +++ /dev/null @@ -1,455 +0,0 @@ -use std::fmt::{self, Debug}; - -#[derive(Copy, Clone, Eq, PartialEq, PartialOrd, Ord, Hash, Default)] -pub struct Region { - start: Position, - end: Position, -} - -impl Region { - pub const fn zero() -> Self { - Region { - start: Position::zero(), - end: Position::zero(), - } - } - - pub const fn new(start: Position, end: Position) -> Self { - Self { start, end } - } - - pub fn contains(&self, other: &Self) -> bool { - self.start <= other.start && self.end >= other.end - } - - pub fn is_empty(&self) -> bool { - self.start == self.end - } - - pub fn span_across(start: &Region, end: &Region) -> Self { - Region { - start: start.start, - end: end.end, - } - } - - pub fn across_all<'a, I>(regions: I) -> Self - where - I: IntoIterator, - { - let mut it = regions.into_iter(); - - if let Some(first) = it.next() { - let mut result = *first; - - for r in it { - result = Self::span_across(&result, r); - } - - result - } else { - Self::zero() - } - } - - pub const fn from_pos(pos: Position) -> Self { - Region { - start: pos, - end: pos.bump_column(1), - } - } - - pub const fn start(&self) -> Position { - self.start - } - - pub const fn end(&self) -> Position { - self.end - } - - pub const fn between(start: Position, end: Position) -> Self { - Self::new(start, end) - } -} - -// Region is used all over the place. Avoid increasing its size! -static_assertions::assert_eq_size!([u8; 8], Region); - -impl fmt::Debug for Region { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - if self.start == Position::zero() && self.end == Position::zero() { - // In tests, it's super common to set all Located values to 0. - // Also in tests, we don't want to bother printing the locations - // because it makes failed assertions much harder to read. - write!(f, "…") - } else { - write!(f, "@{}-{}", self.start.offset, self.end.offset,) - } - } -} - -#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] -pub struct Position { - pub offset: u32, -} - -impl Position { - pub const fn zero() -> Position { - Position { offset: 0 } - } - - pub const fn new(offset: u32) -> Position { - Position { offset } - } - - #[must_use] - pub const fn bump_column(self, count: u32) -> Self { - Self { - offset: self.offset + count as u32, - } - } - - #[must_use] - pub fn bump_invisible(self, count: u32) -> Self { - Self { - offset: self.offset + count as u32, - } - } - - #[must_use] - pub fn bump_newline(self) -> Self { - Self { - offset: self.offset + 1, - } - } - - #[must_use] - pub const fn sub(self, count: u32) -> Self { - Self { - offset: self.offset - count as u32, - } - } -} - -impl Debug for Position { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "@{}", self.offset) - } -} - -#[derive(Debug, Copy, Clone, Eq, PartialEq, PartialOrd, Ord, Hash, Default)] -pub struct LineColumn { - pub line: u32, - pub column: u32, -} - -impl LineColumn { - pub const fn zero() -> Self { - LineColumn { line: 0, column: 0 } - } - - #[must_use] - pub const fn bump_column(self, count: u32) -> Self { - Self { - line: self.line, - column: self.column + count, - } - } -} - -#[derive(Copy, Clone, Eq, PartialEq, PartialOrd, Ord, Hash, Default)] -pub struct LineColumnRegion { - pub start: LineColumn, - pub end: LineColumn, -} - -impl LineColumnRegion { - pub const fn new(start: LineColumn, end: LineColumn) -> Self { - LineColumnRegion { start, end } - } - - pub const fn zero() -> Self { - LineColumnRegion { - start: LineColumn::zero(), - end: LineColumn::zero(), - } - } - - pub fn contains(&self, other: &Self) -> bool { - use std::cmp::Ordering::*; - match self.start.line.cmp(&other.start.line) { - Greater => false, - Equal => match self.end.line.cmp(&other.end.line) { - Less => false, - Equal => { - self.start.column <= other.start.column && self.end.column >= other.end.column - } - Greater => self.start.column >= other.start.column, - }, - Less => match self.end.line.cmp(&other.end.line) { - Less => false, - Equal => self.end.column >= other.end.column, - Greater => true, - }, - } - } - - pub const fn from_pos(pos: LineColumn) -> Self { - Self { - start: pos, - end: pos.bump_column(1), - } - } - - pub fn is_empty(&self) -> bool { - self.end.line == self.start.line && self.start.column == self.end.column - } - - pub fn span_across(start: &LineColumnRegion, end: &LineColumnRegion) -> Self { - LineColumnRegion { - start: start.start, - end: end.end, - } - } - - pub fn across_all<'a, I>(regions: I) -> Self - where - I: IntoIterator, - { - let mut it = regions.into_iter(); - - if let Some(first) = it.next() { - let mut result = *first; - - for r in it { - result = Self::span_across(&result, r); - } - - result - } else { - Self::zero() - } - } - - pub fn lines_between(&self, other: &LineColumnRegion) -> u32 { - if self.end.line <= other.start.line { - other.start.line - self.end.line - } else if self.start.line >= other.end.line { - self.start.line - other.end.line - } else { - // intersection - 0 - } - } - - pub const fn start(&self) -> LineColumn { - self.start - } - - pub const fn end(&self) -> LineColumn { - self.end - } -} - -impl fmt::Debug for LineColumnRegion { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - if self.start.line == 0 - && self.start.column == 0 - && self.end.line == 0 - && self.end.column == 0 - { - // In tests, it's super common to set all Located values to 0. - // Also in tests, we don't want to bother printing the locations - // because it makes failed assertions much harder to read. - write!(f, "…") - } else { - write!( - f, - "|L {}-{}, C {}-{}|", - self.start.line, self.end.line, self.start.column, self.end.column, - ) - } - } -} - -#[derive(Clone, Eq, Copy, PartialEq, PartialOrd, Ord, Hash)] -pub struct Loc { - pub region: Region, - pub value: T, -} - -impl Loc { - pub const fn new(start: u32, end: u32, value: T) -> Loc { - let region = Region::new(Position::new(start), Position::new(end)); - Loc { region, value } - } - - pub const fn at(region: Region, value: T) -> Loc { - Loc { region, value } - } - - pub const fn at_zero(value: T) -> Loc { - let region = Region::zero(); - Loc { region, value } - } -} - -impl Loc { - pub fn with_value(&self, value: U) -> Loc { - Loc { - region: self.region, - value, - } - } - - pub fn map(&self, transform: F) -> Loc - where - F: (FnOnce(&T) -> U), - { - Loc { - region: self.region, - value: transform(&self.value), - } - } - - pub fn map_owned(self, transform: F) -> Loc - where - F: (FnOnce(T) -> U), - { - Loc { - region: self.region, - value: transform(self.value), - } - } -} - -impl fmt::Debug for Loc -where - T: fmt::Debug, -{ - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let region = self.region; - - if region.start == Position::zero() && region.end == Position::zero() { - // In tests, it's super common to set all Located values to 0. - // Also in tests, we don't want to bother printing the locations - // because it makes failed assertions much harder to read. - self.value.fmt(f) - } else if f.alternate() { - write!(f, "{:?} {:#?}", region, self.value) - } else { - write!(f, "{:?} {:?}", region, self.value) - } - } -} - -pub struct LineInfo { - line_offsets: Vec, -} - -impl LineInfo { - pub fn new(src: &str) -> LineInfo { - let mut line_offsets = vec![0]; - line_offsets.extend(src.match_indices('\n').map(|(offset, _)| offset as u32 + 1)); - LineInfo { line_offsets } - } - - pub fn convert_offset(&self, offset: u32) -> LineColumn { - let search = self.line_offsets.binary_search(&offset); - let line = match search { - Ok(i) => i, - Err(i) => i - 1, - }; - let column = offset - self.line_offsets[line]; - LineColumn { - line: line as u32, - column: column as u32, - } - } - - pub fn convert_pos(&self, pos: Position) -> LineColumn { - self.convert_offset(pos.offset) - } - - pub fn convert_region(&self, region: Region) -> LineColumnRegion { - LineColumnRegion { - start: self.convert_pos(region.start()), - end: self.convert_pos(region.end()), - } - } - - pub fn convert_line_column(&self, lc: LineColumn) -> Position { - let offset = self.line_offsets[lc.line as usize] + lc.column; - Position::new(offset) - } - - pub fn convert_line_column_region(&self, lc_region: LineColumnRegion) -> Region { - let start = self.convert_line_column(lc_region.start); - let end = self.convert_line_column(lc_region.end); - Region::new(start, end) - } -} - -#[test] -fn test_line_info() { - fn char_at_line<'a>(lines: &[&'a str], line_column: LineColumn) -> &'a str { - let line = line_column.line as usize; - let line_text = if line < lines.len() { lines[line] } else { "" }; - let column = line_column.column as usize; - if column == line_text.len() { - "\n" - } else { - &line_text[column..column + 1] - } - } - - fn check_correctness(lines: &[&str]) { - let mut input = String::new(); - for (i, line) in lines.iter().enumerate() { - if i > 0 { - input.push('\n'); - } - input.push_str(line); - } - let info = LineInfo::new(&input); - - let mut last: Option = None; - - for offset in 0..=input.len() { - let expected = if offset < input.len() { - &input[offset..offset + 1] - } else { - "\n" // HACK! pretend there's an extra newline on the end, strictly so we can do the comparison - }; - println!( - "checking {:?} {:?}, expecting {:?}", - input, offset, expected - ); - let line_column = info.convert_offset(offset as u32); - assert!( - Some(line_column) > last, - "{:?} > {:?}", - Some(line_column), - last - ); - assert_eq!(expected, char_at_line(lines, line_column)); - last = Some(line_column); - } - - assert_eq!( - info.convert_offset(input.len() as u32), - LineColumn { - line: lines.len().saturating_sub(1) as u32, - column: lines.last().map(|l| l.len()).unwrap_or(0) as u32, - } - ) - } - - check_correctness(&["", "abc", "def", "", "gi"]); - - check_correctness(&[]); - - check_correctness(&["a"]); - - check_correctness(&["", ""]); -} diff --git a/compiler/region/src/lib.rs b/compiler/region/src/lib.rs deleted file mode 100644 index 885d50b458..0000000000 --- a/compiler/region/src/lib.rs +++ /dev/null @@ -1,5 +0,0 @@ -#![warn(clippy::dbg_macro)] -// See github.com/rtfeldman/roc/issues/800 for discussion of the large_enum_variant check. -#![allow(clippy::large_enum_variant)] - -pub mod all; diff --git a/compiler/roc_target/Cargo.toml b/compiler/roc_target/Cargo.toml deleted file mode 100644 index cbef2efa5a..0000000000 --- a/compiler/roc_target/Cargo.toml +++ /dev/null @@ -1,11 +0,0 @@ -[package] -name = "roc_target" -version = "0.1.0" -authors = ["The Roc Contributors"] -license = "UPL-1.0" -edition = "2021" - -[dependencies] -target-lexicon = "0.12.3" -strum = "0.24.0" -strum_macros = "0.24" diff --git a/compiler/roc_target/src/lib.rs b/compiler/roc_target/src/lib.rs deleted file mode 100644 index 1106a45805..0000000000 --- a/compiler/roc_target/src/lib.rs +++ /dev/null @@ -1,103 +0,0 @@ -#![warn(clippy::dbg_macro)] -// See github.com/rtfeldman/roc/issues/800 for discussion of the large_enum_variant check. -#![allow(clippy::large_enum_variant)] - -use strum_macros::EnumIter; - -#[derive(Debug, Clone, Copy)] -pub struct TargetInfo { - pub architecture: Architecture, -} - -impl TargetInfo { - pub const fn ptr_width(&self) -> PtrWidth { - self.architecture.ptr_width() - } - - pub const fn ptr_size(&self) -> usize { - match self.ptr_width() { - PtrWidth::Bytes4 => 4, - PtrWidth::Bytes8 => 8, - } - } - - pub const fn ptr_alignment_bytes(&self) -> usize { - self.architecture.ptr_alignment_bytes() - } - - pub const fn default_aarch64() -> Self { - TargetInfo { - architecture: Architecture::Aarch64, - } - } - - pub const fn default_x86_64() -> Self { - TargetInfo { - architecture: Architecture::X86_64, - } - } - - pub const fn default_wasm32() -> Self { - TargetInfo { - architecture: Architecture::Wasm32, - } - } -} - -impl From<&target_lexicon::Triple> for TargetInfo { - fn from(triple: &target_lexicon::Triple) -> Self { - let architecture = Architecture::from(triple.architecture); - - Self { architecture } - } -} - -impl From for TargetInfo { - fn from(architecture: Architecture) -> Self { - Self { architecture } - } -} - -#[repr(u8)] -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum PtrWidth { - Bytes4 = 4, - Bytes8 = 8, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, EnumIter)] -pub enum Architecture { - X86_64, - X86_32, - Aarch64, - Aarch32, - Wasm32, -} - -impl Architecture { - pub const fn ptr_width(&self) -> PtrWidth { - use Architecture::*; - - match self { - X86_64 | Aarch64 => PtrWidth::Bytes8, - X86_32 | Aarch32 | Wasm32 => PtrWidth::Bytes4, - } - } - - pub const fn ptr_alignment_bytes(&self) -> usize { - self.ptr_width() as usize - } -} - -impl From for Architecture { - fn from(target: target_lexicon::Architecture) -> Self { - match target { - target_lexicon::Architecture::X86_64 => Architecture::X86_64, - target_lexicon::Architecture::X86_32(_) => Architecture::X86_32, - target_lexicon::Architecture::Aarch64(_) => Architecture::Aarch64, - target_lexicon::Architecture::Arm(_) => Architecture::Aarch32, - target_lexicon::Architecture::Wasm32 => Architecture::Wasm32, - _ => unreachable!("unsupported architecture"), - } - } -} diff --git a/compiler/solve/Cargo.toml b/compiler/solve/Cargo.toml deleted file mode 100644 index 3044589259..0000000000 --- a/compiler/solve/Cargo.toml +++ /dev/null @@ -1,35 +0,0 @@ -[package] -name = "roc_solve" -version = "0.1.0" -authors = ["The Roc Contributors"] -license = "UPL-1.0" -edition = "2021" - -[dependencies] -roc_collections = { path = "../collections" } -roc_error_macros = { path = "../../error_macros" } -roc_exhaustive = { path = "../exhaustive" } -roc_region = { path = "../region" } -roc_module = { path = "../module" } -roc_types = { path = "../types" } -roc_can = { path = "../can" } -roc_problem = { path = "../problem" } -roc_unify = { path = "../unify" } -roc_debug_flags = { path = "../debug_flags" } -arrayvec = "0.7.2" -bumpalo = { version = "3.8.0", features = ["collections"] } - -[dev-dependencies] -roc_load = { path = "../load" } -roc_builtins = { path = "../builtins" } -roc_problem = { path = "../problem" } -roc_parse = { path = "../parse" } -roc_solve = { path = "../solve" } -roc_target = { path = "../roc_target" } -roc_reporting = { path = "../../reporting" } -pretty_assertions = "1.0.0" -indoc = "1.0.3" -tempfile = "3.2.0" -bumpalo = { version = "3.8.0", features = ["collections"] } -regex = "1.5.5" -lazy_static = "1.4.0" diff --git a/compiler/solve/src/ability.rs b/compiler/solve/src/ability.rs deleted file mode 100644 index a4b1349dbc..0000000000 --- a/compiler/solve/src/ability.rs +++ /dev/null @@ -1,659 +0,0 @@ -use roc_can::abilities::AbilitiesStore; -use roc_can::expr::PendingDerives; -use roc_collections::VecMap; -use roc_error_macros::internal_error; -use roc_module::symbol::Symbol; -use roc_region::all::{Loc, Region}; -use roc_types::subs::{Content, FlatType, GetSubsSlice, Rank, Subs, Variable}; -use roc_types::types::{AliasKind, Category, ErrorType, PatternCategory}; -use roc_unify::unify::MustImplementConstraints; -use roc_unify::unify::{MustImplementAbility, Obligated}; - -use crate::solve::{instantiate_rigids, type_to_var}; -use crate::solve::{Aliases, Pools, TypeError}; - -#[derive(Debug, Clone)] -pub enum AbilityImplError { - /// Promote this to an error that the type does not fully implement an ability - IncompleteAbility, - /// Promote this error to a `TypeError::BadExpr` from elsewhere - BadExpr(Region, Category, Variable), - /// Promote this error to a `TypeError::BadPattern` from elsewhere - BadPattern(Region, PatternCategory, Variable), -} - -#[derive(PartialEq, Debug, Clone)] -pub enum UnderivableReason { - NotABuiltin, - /// The surface type is not derivable - SurfaceNotDerivable, - /// A nested type is not derivable - NestedNotDerivable(ErrorType), -} - -#[derive(PartialEq, Debug, Clone)] -pub enum Unfulfilled { - /// Incomplete custom implementation for an ability by an opaque type. - Incomplete { - typ: Symbol, - ability: Symbol, - missing_members: Vec>, - }, - /// Cannot derive implementation of an ability for a structural type. - AdhocUnderivable { - typ: ErrorType, - ability: Symbol, - reason: UnderivableReason, - }, - /// Cannot derive implementation of an ability for an opaque type. - OpaqueUnderivable { - typ: ErrorType, - ability: Symbol, - opaque: Symbol, - derive_region: Region, - reason: UnderivableReason, - }, -} - -/// Indexes a deriving of an ability for an opaque type. -#[derive(Debug, PartialEq, Clone, Copy)] -pub struct DeriveKey { - pub opaque: Symbol, - pub ability: Symbol, -} - -/// Indexes a custom implementation of an ability for an opaque type. -#[derive(Debug, PartialEq, Clone, Copy)] -struct ImplKey { - opaque: Symbol, - ability: Symbol, -} - -#[derive(Debug)] -pub struct PendingDerivesTable( - /// derive key -> (opaque type var to use for checking, derive region) - VecMap, -); - -impl PendingDerivesTable { - pub fn new(subs: &mut Subs, aliases: &mut Aliases, pending_derives: PendingDerives) -> Self { - let mut table = VecMap::with_capacity(pending_derives.len()); - - for (opaque, (typ, derives)) in pending_derives.into_iter() { - for Loc { - value: ability, - region, - } in derives - { - debug_assert!( - ability.is_builtin_ability(), - "Not a builtin - should have been caught during can" - ); - let derive_key = DeriveKey { opaque, ability }; - - // Neither rank nor pools should matter here. - let opaque_var = - type_to_var(subs, Rank::toplevel(), &mut Pools::default(), aliases, &typ); - let real_var = match subs.get_content_without_compacting(opaque_var) { - Content::Alias(_, _, real_var, AliasKind::Opaque) => real_var, - _ => internal_error!("Non-opaque in derives table"), - }; - - let old = table.insert(derive_key, (*real_var, region)); - debug_assert!(old.is_none()); - } - } - - Self(table) - } -} - -#[derive(Debug)] -pub struct DeferredObligations { - /// Obligations, to be filled in during solving of a module. - obligations: Vec<(MustImplementConstraints, AbilityImplError)>, - /// Derives that module-defined opaques claim to have. - pending_derives: PendingDerivesTable, - /// Derives that are claimed, but have also been determined to have - /// specializations. Maps to the first member specialization of the same - /// ability. - dominated_derives: VecMap, -} - -impl DeferredObligations { - pub fn new(pending_derives: PendingDerivesTable) -> Self { - Self { - obligations: Default::default(), - pending_derives, - dominated_derives: Default::default(), - } - } - - pub fn add(&mut self, must_implement: MustImplementConstraints, on_error: AbilityImplError) { - self.obligations.push((must_implement, on_error)); - } - - pub fn dominate(&mut self, key: DeriveKey, impl_region: Region) { - // Only builtin abilities can be derived, and hence dominated. - if self.pending_derives.0.contains_key(&key) && !self.dominated_derives.contains_key(&key) { - self.dominated_derives.insert(key, impl_region); - } - } - - // Rules for checking ability implementations: - // - Ad-hoc derives for structural types are checked on-the-fly - // - Opaque derives are registered as "pending" when we check a module - // - Opaque derives are always checked and registered at the end to make sure opaque - // specializations are found first - // - If an opaque O both derives and specializes an ability A - // - The specialization is recorded in the abilities store (this is done in solve/solve) - // - The derive is checked, but will not be recorded in the abilities store (this is done here) - // - Obligations for O to implement A will defer to whether the specialization is complete - pub fn check_all( - self, - subs: &mut Subs, - abilities_store: &AbilitiesStore, - ) -> (Vec, Vec) { - let mut problems = vec![]; - - let Self { - obligations, - pending_derives, - dominated_derives, - } = self; - - let mut obligation_cache = ObligationCache { - abilities_store, - pending_derives: &pending_derives, - dominated_derives: &dominated_derives, - - impl_cache: VecMap::with_capacity(obligations.len()), - derive_cache: VecMap::with_capacity(pending_derives.0.len()), - }; - - let mut legal_derives = Vec::with_capacity(pending_derives.0.len()); - - // First, check all derives. - for (&derive_key, &(opaque_real_var, derive_region)) in pending_derives.0.iter() { - obligation_cache.check_derive(subs, derive_key, opaque_real_var, derive_region); - let result = obligation_cache.derive_cache.get(&derive_key).unwrap(); - match result { - Ok(()) => legal_derives.push(derive_key), - Err(problem) => problems.push(TypeError::UnfulfilledAbility(problem.clone())), - } - } - - for (derive_key, impl_region) in dominated_derives.iter() { - let derive_region = pending_derives.0.get(derive_key).unwrap().1; - - problems.push(TypeError::DominatedDerive { - opaque: derive_key.opaque, - ability: derive_key.ability, - derive_region, - impl_region: *impl_region, - }); - } - - // Keep track of which types that have an incomplete ability were reported as part of - // another type error (from an expression or pattern). If we reported an error for a type - // that doesn't implement an ability in that context, we don't want to repeat the error - // message. - let mut reported_in_context = vec![]; - let mut incomplete_not_in_context = vec![]; - - for (constraints, on_error) in obligations.into_iter() { - let must_implement = constraints.get_unique(); - - let mut get_unfulfilled = |must_implement: &[MustImplementAbility]| { - must_implement - .iter() - .filter_map(|mia| { - obligation_cache - .check_one(subs, *mia) - .as_ref() - .err() - .cloned() - }) - .collect::>() - }; - - use AbilityImplError::*; - match on_error { - IncompleteAbility => { - // These aren't attached to another type error, so if these must_implement - // constraints aren't met, we'll emit a generic "this type doesn't implement an - // ability" error message at the end. We only want to do this if it turns out - // the "must implement" constraint indeed wasn't part of a more specific type - // error. - incomplete_not_in_context.extend(must_implement); - } - BadExpr(region, category, var) => { - let unfulfilled = get_unfulfilled(&must_implement); - - if !unfulfilled.is_empty() { - // Demote the bad variable that exposed this problem to an error, both so - // that we have an ErrorType to report and so that codegen knows to deal - // with the error later. - let (error_type, _moar_ghosts_n_stuff) = subs.var_to_error_type(var); - problems.push(TypeError::BadExprMissingAbility( - region, - category, - error_type, - unfulfilled, - )); - reported_in_context.extend(must_implement); - } - } - BadPattern(region, category, var) => { - let unfulfilled = get_unfulfilled(&must_implement); - - if !unfulfilled.is_empty() { - // Demote the bad variable that exposed this problem to an error, both so - // that we have an ErrorType to report and so that codegen knows to deal - // with the error later. - let (error_type, _moar_ghosts_n_stuff) = subs.var_to_error_type(var); - problems.push(TypeError::BadPatternMissingAbility( - region, - category, - error_type, - unfulfilled, - )); - reported_in_context.extend(must_implement); - } - } - } - } - - // Go through and attach generic "type does not implement ability" errors, if they were not - // part of a larger context. - for mia in incomplete_not_in_context.into_iter() { - if let Err(unfulfilled) = obligation_cache.check_one(subs, mia) { - if !reported_in_context.contains(&mia) { - problems.push(TypeError::UnfulfilledAbility(unfulfilled.clone())); - } - } - } - - (problems, legal_derives) - } -} - -type ObligationResult = Result<(), Unfulfilled>; - -struct ObligationCache<'a> { - abilities_store: &'a AbilitiesStore, - dominated_derives: &'a VecMap, - pending_derives: &'a PendingDerivesTable, - - impl_cache: VecMap, - derive_cache: VecMap, -} - -enum ReadCache { - Impl, - Derive, -} - -impl ObligationCache<'_> { - fn check_one(&mut self, subs: &mut Subs, mia: MustImplementAbility) -> ObligationResult { - let MustImplementAbility { typ, ability } = mia; - - match typ { - Obligated::Adhoc(var) => self.check_adhoc(subs, var, ability), - Obligated::Opaque(opaque) => self.check_opaque_and_read(subs, opaque, ability).clone(), - } - } - - fn check_adhoc(&mut self, subs: &mut Subs, var: Variable, ability: Symbol) -> ObligationResult { - // Not worth caching ad-hoc checks because variables are unlikely to be the same between - // independent queries. - - let opt_can_derive_builtin = match ability { - Symbol::ENCODE_ENCODING => Some(self.can_derive_encoding(subs, var)), - _ => None, - }; - - let opt_underivable = match opt_can_derive_builtin { - Some(Ok(())) => { - // can derive! - None - } - Some(Err(failure_var)) => Some(if failure_var == var { - UnderivableReason::SurfaceNotDerivable - } else { - let (error_type, _skeletons) = subs.var_to_error_type(failure_var); - UnderivableReason::NestedNotDerivable(error_type) - }), - None => Some(UnderivableReason::NotABuiltin), - }; - - if let Some(underivable_reason) = opt_underivable { - let (error_type, _skeletons) = subs.var_to_error_type(var); - - Err(Unfulfilled::AdhocUnderivable { - typ: error_type, - ability, - reason: underivable_reason, - }) - } else { - Ok(()) - } - } - - fn check_opaque(&mut self, subs: &mut Subs, opaque: Symbol, ability: Symbol) -> ReadCache { - let impl_key = ImplKey { opaque, ability }; - let derive_key = DeriveKey { opaque, ability }; - - match self.pending_derives.0.get(&derive_key) { - Some(&(opaque_real_var, derive_region)) => { - if self.dominated_derives.contains_key(&derive_key) { - // We have a derive, but also a custom implementation. The custom - // implementation takes priority because we'll use that for codegen. - // We'll report an error for the conflict, and whether the derive is - // legal will be checked out-of-band. - self.check_impl(impl_key); - ReadCache::Impl - } else { - // Only a derive - self.check_derive(subs, derive_key, opaque_real_var, derive_region); - ReadCache::Derive - } - } - // Only an impl - None => { - self.check_impl(impl_key); - ReadCache::Impl - } - } - } - - fn check_opaque_and_read( - &mut self, - subs: &mut Subs, - opaque: Symbol, - ability: Symbol, - ) -> &ObligationResult { - match self.check_opaque(subs, opaque, ability) { - ReadCache::Impl => self.impl_cache.get(&ImplKey { opaque, ability }).unwrap(), - ReadCache::Derive => self - .derive_cache - .get(&DeriveKey { opaque, ability }) - .unwrap(), - } - } - - fn check_impl(&mut self, impl_key: ImplKey) { - if self.impl_cache.get(&impl_key).is_some() { - return; - } - - let ImplKey { opaque, ability } = impl_key; - - let members_of_ability = self.abilities_store.members_of_ability(ability).unwrap(); - let mut missing_members = Vec::new(); - for &member in members_of_ability { - if self - .abilities_store - .get_specialization(member, opaque) - .is_none() - { - let root_data = self.abilities_store.member_def(member).unwrap(); - missing_members.push(Loc::at(root_data.region, member)); - } - } - - let obligation_result = if !missing_members.is_empty() { - Err(Unfulfilled::Incomplete { - typ: opaque, - ability, - missing_members, - }) - } else { - Ok(()) - }; - - self.impl_cache.insert(impl_key, obligation_result); - } - - fn check_derive( - &mut self, - subs: &mut Subs, - derive_key: DeriveKey, - opaque_real_var: Variable, - derive_region: Region, - ) { - if self.derive_cache.get(&derive_key).is_some() { - return; - } - - // The opaque may be recursive, so make sure we stop if we see it again. - // We need to enforce that on both the impl and derive cache. - let fake_fulfilled = Ok(()); - // If this opaque both derives and specializes, we may already know whether the - // specialization fulfills or not. Since specializations take priority over derives, we - // want to keep that result around. - let impl_key = ImplKey { - opaque: derive_key.opaque, - ability: derive_key.ability, - }; - let opt_specialization_result = self.impl_cache.insert(impl_key, fake_fulfilled.clone()); - let is_dominated = self.dominated_derives.contains_key(&derive_key); - debug_assert!( - opt_specialization_result.is_none() || is_dominated, - "This derive also has a specialization but it's not marked as dominated!" - ); - - let old_deriving = self.derive_cache.insert(derive_key, fake_fulfilled.clone()); - debug_assert!( - old_deriving.is_none(), - "Already knew deriving result, but here anyway" - ); - - // Now we check whether the structural type behind the opaque is derivable, since that's - // what we'll need to generate an implementation for during codegen. - let real_var_result = self.check_adhoc(subs, opaque_real_var, derive_key.ability); - - let root_result = real_var_result.map_err(|err| match err { - // Promote the failure, which should be related to a structural type not being - // derivable for the ability, to a failure regarding the opaque in particular. - Unfulfilled::AdhocUnderivable { - typ, - ability, - reason, - } => Unfulfilled::OpaqueUnderivable { - typ, - ability, - reason, - opaque: derive_key.opaque, - derive_region, - }, - _ => internal_error!("unexpected underivable result"), - }); - - // Remove the derive result because the specialization check should take priority. - let check_has_fake = self.impl_cache.remove(&impl_key); - debug_assert_eq!(check_has_fake.map(|(_, b)| b), Some(fake_fulfilled.clone())); - - if let Some(specialization_result) = opt_specialization_result { - self.impl_cache.insert(impl_key, specialization_result); - } - - // Make sure we fix-up with the correct result of the check. - let check_has_fake = self.derive_cache.insert(derive_key, root_result); - debug_assert_eq!(check_has_fake, Some(fake_fulfilled)); - } - - // If we have a lot of these, consider using a visitor. - // It will be very similar for most types (can't derive functions, can't derive unbound type - // variables, can only derive opaques if they have an impl, etc). - fn can_derive_encoding(&mut self, subs: &mut Subs, var: Variable) -> Result<(), Variable> { - let mut stack = vec![var]; - let mut seen_recursion_vars = vec![]; - - macro_rules! push_var_slice { - ($slice:expr) => { - stack.extend(subs.get_subs_slice($slice)) - }; - } - - while let Some(var) = stack.pop() { - if seen_recursion_vars.contains(&var) { - continue; - } - - let content = subs.get_content_without_compacting(var); - - use Content::*; - use FlatType::*; - match content { - FlexVar(_) | RigidVar(_) => return Err(var), - FlexAbleVar(_, ability) | RigidAbleVar(_, ability) => { - if *ability != Symbol::ENCODE_ENCODING { - return Err(var); - } - // Any concrete type this variables is instantiated with will also gain a "does - // implement" check so this is okay. - } - RecursionVar { - structure, - opt_name: _, - } => { - seen_recursion_vars.push(var); - stack.push(*structure); - } - Structure(flat_type) => match flat_type { - Apply( - Symbol::LIST_LIST | Symbol::SET_SET | Symbol::DICT_DICT | Symbol::STR_STR, - vars, - ) => push_var_slice!(*vars), - Apply(..) => return Err(var), - Func(..) => { - return Err(var); - } - Record(fields, var) => { - push_var_slice!(fields.variables()); - stack.push(*var); - } - TagUnion(tags, ext_var) => { - for i in tags.variables() { - push_var_slice!(subs[i]); - } - stack.push(*ext_var); - } - FunctionOrTagUnion(_, _, var) => stack.push(*var), - RecursiveTagUnion(rec_var, tags, ext_var) => { - seen_recursion_vars.push(*rec_var); - for i in tags.variables() { - push_var_slice!(subs[i]); - } - stack.push(*ext_var); - } - EmptyRecord | EmptyTagUnion => { - // yes - } - Erroneous(_) => return Err(var), - }, - Alias(name, _, _, AliasKind::Opaque) => { - let opaque = *name; - if self - .check_opaque_and_read(subs, opaque, Symbol::ENCODE_ENCODING) - .is_err() - { - return Err(var); - } - } - Alias(_, arguments, real_type_var, _) => { - push_var_slice!(arguments.all_variables()); - stack.push(*real_type_var); - } - RangedNumber(..) => { - // yes, all numbers can - } - LambdaSet(..) => return Err(var), - Error => { - return Err(var); - } - } - } - - Ok(()) - } -} - -/// Determines what type implements an ability member of a specialized signature, given the -/// [MustImplementAbility] constraints of the signature. -pub fn type_implementing_specialization( - specialization_must_implement_constraints: &MustImplementConstraints, - ability: Symbol, -) -> Option { - debug_assert!({ - specialization_must_implement_constraints - .clone() - .get_unique() - .into_iter() - .filter(|mia| mia.ability == ability) - .count() - } < 2, - "Multiple variables bound to an ability - this is ambiguous and should have been caught in canonicalization: {:?}", - specialization_must_implement_constraints - ); - - specialization_must_implement_constraints - .iter_for_ability(ability) - .next() - .map(|mia| mia.typ) -} - -/// Result of trying to resolve an ability specialization. -#[derive(Clone, Copy)] -pub enum Resolved { - /// A user-defined specialization should be used. - Specialization(Symbol), - /// A specialization must be generated. - NeedsGenerated, -} - -pub fn resolve_ability_specialization( - subs: &mut Subs, - abilities_store: &AbilitiesStore, - ability_member: Symbol, - specialization_var: Variable, -) -> Option { - use roc_unify::unify::{unify, Mode}; - - let member_def = abilities_store - .member_def(ability_member) - .expect("Not an ability member symbol"); - - // Figure out the ability we're resolving in a temporary subs snapshot. - let snapshot = subs.snapshot(); - - let signature_var = member_def - .signature_var() - .unwrap_or_else(|| internal_error!("Signature var not resolved for {:?}", ability_member)); - - instantiate_rigids(subs, signature_var); - let (_vars, must_implement_ability, _lambda_sets_to_specialize) = - unify(subs, specialization_var, signature_var, Mode::EQ).expect_success( - "If resolving a specialization, the specialization must be known to typecheck.", - ); - - subs.rollback_to(snapshot); - - let obligated = - type_implementing_specialization(&must_implement_ability, member_def.parent_ability)?; - - let resolved = match obligated { - Obligated::Opaque(symbol) => { - let specialization = abilities_store.get_specialization(ability_member, symbol)?; - - Resolved::Specialization(specialization.symbol) - } - Obligated::Adhoc(_) => { - // TODO: more rules need to be validated here, like is this a builtin ability? - Resolved::NeedsGenerated - } - }; - - Some(resolved) -} diff --git a/compiler/solve/src/lib.rs b/compiler/solve/src/lib.rs deleted file mode 100644 index d0e42eaf42..0000000000 --- a/compiler/solve/src/lib.rs +++ /dev/null @@ -1,7 +0,0 @@ -#![warn(clippy::dbg_macro)] -// See github.com/rtfeldman/roc/issues/800 for discussion of the large_enum_variant check. -#![allow(clippy::large_enum_variant)] - -pub mod ability; -pub mod module; -pub mod solve; diff --git a/compiler/solve/src/module.rs b/compiler/solve/src/module.rs deleted file mode 100644 index 17da080633..0000000000 --- a/compiler/solve/src/module.rs +++ /dev/null @@ -1,93 +0,0 @@ -use crate::solve::{self, Aliases}; -use roc_can::abilities::{AbilitiesStore, SolvedSpecializations}; -use roc_can::constraint::{Constraint as ConstraintSoa, Constraints}; -use roc_can::expr::PendingDerives; -use roc_can::module::RigidVariables; -use roc_collections::all::MutMap; -use roc_module::symbol::Symbol; -use roc_types::solved_types::Solved; -use roc_types::subs::{StorageSubs, Subs, Variable}; -use roc_types::types::Alias; - -#[derive(Debug)] -pub struct SolvedModule { - pub problems: Vec, - - /// all aliases and their definitions. this has to include non-exposed aliases - /// because exposed aliases can depend on non-exposed ones) - pub aliases: MutMap, - - /// Used when the goal phase is TypeChecking, and - /// to create the types for HostExposed. This - /// has some overlap with the StorageSubs fields, - /// so maybe we can get rid of this at some point - /// - /// Contains both variables of symbols that are explicitly exposed by the header, - /// and the variables of any solved ability specializations we have. - pub exposed_vars_by_symbol: Vec<(Symbol, Variable)>, - - /// Used when importing this module into another module - pub stored_vars_by_symbol: Vec<(Symbol, Variable)>, - pub storage_subs: StorageSubs, - pub solved_specializations: SolvedSpecializations, -} - -pub fn run_solve( - constraints: &Constraints, - constraint: ConstraintSoa, - rigid_variables: RigidVariables, - mut subs: Subs, - mut aliases: Aliases, - mut abilities_store: AbilitiesStore, - pending_derives: PendingDerives, -) -> ( - Solved, - solve::Env, - Vec, - AbilitiesStore, -) { - for (var, name) in rigid_variables.named { - subs.rigid_var(var, name); - } - - for (var, (name, ability)) in rigid_variables.able { - subs.rigid_able_var(var, name, ability); - } - - for var in rigid_variables.wildcards { - subs.rigid_var(var, "*".into()); - } - - // Now that the module is parsed, canonicalized, and constrained, - // we need to type check it. - let mut problems = Vec::new(); - - // Run the solver to populate Subs. - let (solved_subs, solved_env) = solve::run( - constraints, - &mut problems, - subs, - &mut aliases, - &constraint, - pending_derives, - &mut abilities_store, - ); - - (solved_subs, solved_env, problems, abilities_store) -} - -pub fn exposed_types_storage_subs( - solved_subs: &mut Solved, - exposed_vars_by_symbol: &[(Symbol, Variable)], -) -> (StorageSubs, Vec<(Symbol, Variable)>) { - let subs = solved_subs.inner_mut(); - let mut storage_subs = StorageSubs::new(Subs::new()); - let mut stored_vars_by_symbol = Vec::with_capacity(exposed_vars_by_symbol.len()); - - for (symbol, var) in exposed_vars_by_symbol.iter() { - let new_var = storage_subs.import_variable_from(subs, *var).variable; - stored_vars_by_symbol.push((*symbol, new_var)); - } - - (storage_subs, stored_vars_by_symbol) -} diff --git a/compiler/solve/src/solve.rs b/compiler/solve/src/solve.rs deleted file mode 100644 index 296d3f43b3..0000000000 --- a/compiler/solve/src/solve.rs +++ /dev/null @@ -1,3772 +0,0 @@ -use crate::ability::{ - resolve_ability_specialization, type_implementing_specialization, AbilityImplError, - DeferredObligations, DeriveKey, PendingDerivesTable, Resolved, Unfulfilled, -}; -use bumpalo::Bump; -use roc_can::abilities::{AbilitiesStore, MemberSpecialization}; -use roc_can::constraint::Constraint::{self, *}; -use roc_can::constraint::{Constraints, Cycle, LetConstraint, OpportunisticResolve}; -use roc_can::expected::{Expected, PExpected}; -use roc_can::expr::PendingDerives; -use roc_collections::all::MutMap; -use roc_collections::{VecMap, VecSet}; -use roc_debug_flags::dbg_do; -#[cfg(debug_assertions)] -use roc_debug_flags::ROC_VERIFY_RIGID_LET_GENERALIZED; -use roc_error_macros::internal_error; -use roc_module::ident::TagName; -use roc_module::symbol::Symbol; -use roc_problem::can::CycleEntry; -use roc_region::all::{Loc, Region}; -use roc_types::solved_types::Solved; -use roc_types::subs::{ - self, AliasVariables, Content, Descriptor, FlatType, GetSubsSlice, LambdaSet, Mark, - OptVariable, Rank, RecordFields, Subs, SubsIndex, SubsSlice, UlsOfVar, UnionLabels, - UnionLambdas, UnionTags, Variable, VariableSubsSlice, -}; -use roc_types::types::Type::{self, *}; -use roc_types::types::{ - gather_fields_unsorted_iter, AliasCommon, AliasKind, Category, ErrorType, OptAbleType, - OptAbleVar, PatternCategory, Reason, TypeExtension, Uls, -}; -use roc_unify::unify::{unify, Mode, Obligated, Unified::*}; - -// Type checking system adapted from Elm by Evan Czaplicki, BSD-3-Clause Licensed -// https://github.com/elm/compiler -// Thank you, Evan! - -// A lot of energy was put into making type inference fast. That means it's pretty intimidating. -// -// Fundamentally, type inference assigns very general types based on syntax, and then tries to -// make all the pieces fit together. For instance when writing -// -// > f x -// -// We know that `f` is a function, and thus must have some type `a -> b`. -// `x` is just a variable, that gets the type `c` -// -// Next comes constraint generation. For `f x` to be well-typed, -// it must be the case that `c = a`, So a constraint `Eq(c, a)` is generated. -// But `Eq` is a bit special: `c` does not need to equal `a` exactly, but they need to be equivalent. -// This allows for instance the use of aliases. `c` could be an alias, and so looks different from -// `a`, but they still represent the same type. -// -// Then we get to solving, which happens in this file. -// -// When we hit an `Eq` constraint, then we check whether the two involved types are in fact -// equivalent using unification, and when they are, we can substitute one for the other. -// -// When all constraints are processed, and no unification errors have occurred, then the program -// is type-correct. Otherwise the errors are reported. -// -// Now, coming back to efficiency, this type checker uses *ranks* to optimize -// The rank tracks the number of let-bindings a variable is "under". Top-level definitions -// have rank 1. A let in a top-level definition gets rank 2, and so on. -// -// This has to do with generalization of type variables. This is described here -// -// http://okmij.org/ftp/ML/generalization.html#levels -// -// The problem is that when doing inference naively, this program would fail to typecheck -// -// f = -// id = \x -> x -// -// { a: id 1, b: id "foo" } -// -// Because `id` is applied to an integer, the type `Int -> Int` is inferred, which then gives a -// type error for `id "foo"`. -// -// Thus instead the inferred type for `id` is generalized (see the `generalize` function) to `a -> a`. -// Ranks are used to limit the number of type variables considered for generalization. Only those inside -// of the let (so those used in inferring the type of `\x -> x`) are considered. - -/// What phase in the compiler is reaching out to solve types. -/// This is important to distinguish subtle differences in the behavior of the solving algorithm. -#[derive(Clone, Copy)] -pub enum Phase { - /// The regular type-solving phase. - /// Here we can assume that some information is still unknown, and react to that. - Solve, - /// Calls into solve during later phases of compilation, namely monomorphization. - /// Here we expect all information is known. - Late, -} - -#[derive(Debug, Clone)] -pub enum TypeError { - BadExpr(Region, Category, ErrorType, Expected), - BadPattern(Region, PatternCategory, ErrorType, PExpected), - CircularType(Region, Symbol, ErrorType), - CircularDef(Vec), - BadType(roc_types::types::Problem), - UnexposedLookup(Symbol), - UnfulfilledAbility(Unfulfilled), - BadExprMissingAbility(Region, Category, ErrorType, Vec), - BadPatternMissingAbility(Region, PatternCategory, ErrorType, Vec), - Exhaustive(roc_exhaustive::Error), - StructuralSpecialization { - region: Region, - typ: ErrorType, - ability: Symbol, - member: Symbol, - }, - DominatedDerive { - opaque: Symbol, - ability: Symbol, - derive_region: Region, - impl_region: Region, - }, -} - -use roc_types::types::Alias; - -#[derive(Debug, Clone, Copy)] -struct DelayedAliasVariables { - start: u32, - type_variables_len: u8, - lambda_set_variables_len: u8, - recursion_variables_len: u8, -} - -impl DelayedAliasVariables { - fn recursion_variables(self, variables: &mut [OptAbleVar]) -> &mut [OptAbleVar] { - let start = self.start as usize - + (self.type_variables_len + self.lambda_set_variables_len) as usize; - let length = self.recursion_variables_len as usize; - - &mut variables[start..][..length] - } - - fn lambda_set_variables(self, variables: &mut [OptAbleVar]) -> &mut [OptAbleVar] { - let start = self.start as usize + self.type_variables_len as usize; - let length = self.lambda_set_variables_len as usize; - - &mut variables[start..][..length] - } - - fn type_variables(self, variables: &mut [OptAbleVar]) -> &mut [OptAbleVar] { - let start = self.start as usize; - let length = self.type_variables_len as usize; - - &mut variables[start..][..length] - } -} - -#[derive(Debug, Default)] -pub struct Aliases { - aliases: Vec<(Symbol, Type, DelayedAliasVariables, AliasKind)>, - variables: Vec, -} - -impl Aliases { - pub fn insert(&mut self, symbol: Symbol, alias: Alias) { - let alias_variables = - { - let start = self.variables.len() as _; - - self.variables.extend( - alias - .type_variables - .iter() - .map(|x| OptAbleVar::from(&x.value)), - ); - - self.variables.extend(alias.lambda_set_variables.iter().map( - |x| match x.as_inner() { - Type::Variable(v) => OptAbleVar::unbound(*v), - _ => unreachable!("lambda set type is not a variable"), - }, - )); - - let recursion_variables_len = alias.recursion_variables.len() as _; - self.variables.extend( - alias - .recursion_variables - .iter() - .copied() - .map(OptAbleVar::unbound), - ); - - DelayedAliasVariables { - start, - type_variables_len: alias.type_variables.len() as _, - lambda_set_variables_len: alias.lambda_set_variables.len() as _, - recursion_variables_len, - } - }; - - self.aliases - .push((symbol, alias.typ, alias_variables, alias.kind)); - } - - fn instantiate_result_result( - subs: &mut Subs, - rank: Rank, - pools: &mut Pools, - alias_variables: AliasVariables, - ) -> Variable { - let tag_names_slice = Subs::RESULT_TAG_NAMES; - - let err_slice = SubsSlice::new(alias_variables.variables_start + 1, 1); - let ok_slice = SubsSlice::new(alias_variables.variables_start, 1); - - let variable_slices = - SubsSlice::extend_new(&mut subs.variable_slices, [err_slice, ok_slice]); - - let union_tags = UnionTags::from_slices(tag_names_slice, variable_slices); - let ext_var = Variable::EMPTY_TAG_UNION; - let flat_type = FlatType::TagUnion(union_tags, ext_var); - let content = Content::Structure(flat_type); - - register(subs, rank, pools, content) - } - - /// Build an alias of the form `Num range := range` - fn build_num_opaque( - subs: &mut Subs, - rank: Rank, - pools: &mut Pools, - symbol: Symbol, - range_var: Variable, - ) -> Variable { - let content = Content::Alias( - symbol, - AliasVariables::insert_into_subs(subs, [range_var], []), - range_var, - AliasKind::Opaque, - ); - - register(subs, rank, pools, content) - } - - fn instantiate_builtin_aliases_real_var( - &mut self, - subs: &mut Subs, - rank: Rank, - pools: &mut Pools, - symbol: Symbol, - alias_variables: AliasVariables, - ) -> Option<(Variable, AliasKind)> { - match symbol { - Symbol::RESULT_RESULT => { - let var = Self::instantiate_result_result(subs, rank, pools, alias_variables); - - Some((var, AliasKind::Structural)) - } - Symbol::NUM_NUM | Symbol::NUM_INTEGER | Symbol::NUM_FLOATINGPOINT => { - // Num range := range | Integer range := range | FloatingPoint range := range - let range_var = subs.variables[alias_variables.variables_start as usize]; - Some((range_var, AliasKind::Opaque)) - } - Symbol::NUM_INT => { - // Int range : Num (Integer range) - // - // build `Integer range := range` - let integer_content_var = Self::build_num_opaque( - subs, - rank, - pools, - Symbol::NUM_INTEGER, - subs.variables[alias_variables.variables_start as usize], - ); - - // build `Num (Integer range) := Integer range` - let num_content_var = - Self::build_num_opaque(subs, rank, pools, Symbol::NUM_NUM, integer_content_var); - - Some((num_content_var, AliasKind::Structural)) - } - Symbol::NUM_FRAC => { - // Frac range : Num (FloatingPoint range) - // - // build `FloatingPoint range := range` - let fpoint_content_var = Self::build_num_opaque( - subs, - rank, - pools, - Symbol::NUM_FLOATINGPOINT, - subs.variables[alias_variables.variables_start as usize], - ); - - // build `Num (FloatingPoint range) := FloatingPoint range` - let num_content_var = - Self::build_num_opaque(subs, rank, pools, Symbol::NUM_NUM, fpoint_content_var); - - Some((num_content_var, AliasKind::Structural)) - } - _ => None, - } - } - - fn instantiate_real_var( - &mut self, - subs: &mut Subs, - rank: Rank, - pools: &mut Pools, - arena: &bumpalo::Bump, - symbol: Symbol, - alias_variables: AliasVariables, - ) -> (Variable, AliasKind) { - // hardcoded instantiations for builtin aliases - if let Some((var, kind)) = Self::instantiate_builtin_aliases_real_var( - self, - subs, - rank, - pools, - symbol, - alias_variables, - ) { - return (var, kind); - } - - let (typ, delayed_variables, &mut kind) = - match self.aliases.iter_mut().find(|(s, _, _, _)| *s == symbol) { - None => internal_error!( - "Alias not registered in delayed aliases! {:?}", - &self.aliases - ), - Some((_, typ, delayed_variables, kind)) => (typ, delayed_variables, kind), - }; - - let mut substitutions: MutMap<_, _> = Default::default(); - - for OptAbleVar { - var: rec_var, - opt_ability, - } in delayed_variables - .recursion_variables(&mut self.variables) - .iter_mut() - { - debug_assert!(opt_ability.is_none()); - let new_var = subs.fresh_unnamed_flex_var(); - substitutions.insert(*rec_var, new_var); - *rec_var = new_var; - } - - let old_type_variables = delayed_variables.type_variables(&mut self.variables); - let new_type_variables = &subs.variables[alias_variables.type_variables().indices()]; - - for (old, new) in old_type_variables.iter_mut().zip(new_type_variables) { - // if constraint gen duplicated a type these variables could be the same - // (happens very often in practice) - if old.var != *new { - substitutions.insert(old.var, *new); - - old.var = *new; - } - } - - let old_lambda_set_variables = delayed_variables.lambda_set_variables(&mut self.variables); - let new_lambda_set_variables = - &subs.variables[alias_variables.lambda_set_variables().indices()]; - - for (old, new) in old_lambda_set_variables - .iter_mut() - .zip(new_lambda_set_variables) - { - debug_assert!(old.opt_ability.is_none()); - if old.var != *new { - substitutions.insert(old.var, *new); - old.var = *new; - } - } - - if !substitutions.is_empty() { - typ.substitute_variables(&substitutions); - } - - let mut t = Type::EmptyRec; - - std::mem::swap(typ, &mut t); - - // assumption: an alias does not (transitively) syntactically contain itself - // (if it did it would have to be a recursive tag union, which we should have fixed up - // during canonicalization) - let alias_variable = type_to_variable(subs, rank, pools, arena, self, &t); - - { - match self.aliases.iter_mut().find(|(s, _, _, _)| *s == symbol) { - None => unreachable!(), - Some((_, typ, _, _)) => { - // swap typ back - std::mem::swap(typ, &mut t); - } - } - } - - (alias_variable, kind) - } -} - -#[derive(Clone, Debug, Default)] -pub struct Env { - symbols: Vec, - variables: Vec, -} - -impl Env { - pub fn vars_by_symbol(&self) -> impl Iterator + '_ { - let it1 = self.symbols.iter().copied(); - let it2 = self.variables.iter().copied(); - - it1.zip(it2) - } - - #[inline(always)] - fn get_var_by_symbol(&self, symbol: &Symbol) -> Option { - self.symbols - .iter() - .position(|s| s == symbol) - .map(|index| self.variables[index]) - } - - #[inline(always)] - fn insert_symbol_var_if_vacant(&mut self, symbol: Symbol, var: Variable) { - match self.symbols.iter().position(|s| *s == symbol) { - None => { - // symbol is not in vars_by_symbol yet; insert it - self.symbols.push(symbol); - self.variables.push(var); - } - Some(_) => { - // do nothing - } - } - } -} - -const DEFAULT_POOLS: usize = 8; - -#[derive(Clone, Debug)] -pub struct Pools(Vec>); - -impl Default for Pools { - fn default() -> Self { - Pools::new(DEFAULT_POOLS) - } -} - -impl Pools { - pub fn new(num_pools: usize) -> Self { - Pools(vec![Vec::new(); num_pools]) - } - - pub fn len(&self) -> usize { - self.0.len() - } - - pub fn is_empty(&self) -> bool { - self.0.is_empty() - } - - pub fn get_mut(&mut self, rank: Rank) -> &mut Vec { - match self.0.get_mut(rank.into_usize()) { - Some(reference) => reference, - None => panic!("Compiler bug: could not find pool at rank {}", rank), - } - } - - pub fn get(&self, rank: Rank) -> &Vec { - match self.0.get(rank.into_usize()) { - Some(reference) => reference, - None => panic!("Compiler bug: could not find pool at rank {}", rank), - } - } - - pub fn iter(&self) -> std::slice::Iter<'_, Vec> { - self.0.iter() - } - - pub fn split_last(mut self) -> (Vec, Vec>) { - let last = self - .0 - .pop() - .unwrap_or_else(|| panic!("Attempted to split_last() on non-empty Pools")); - - (last, self.0) - } - - pub fn extend_to(&mut self, n: usize) { - for _ in self.len()..n { - self.0.push(Vec::new()); - } - } -} - -#[derive(Clone)] -struct State { - env: Env, - mark: Mark, -} - -pub fn run( - constraints: &Constraints, - problems: &mut Vec, - mut subs: Subs, - aliases: &mut Aliases, - constraint: &Constraint, - pending_derives: PendingDerives, - abilities_store: &mut AbilitiesStore, -) -> (Solved, Env) { - let env = run_in_place( - constraints, - problems, - &mut subs, - aliases, - constraint, - pending_derives, - abilities_store, - ); - - (Solved(subs), env) -} - -/// Modify an existing subs in-place instead -fn run_in_place( - constraints: &Constraints, - problems: &mut Vec, - subs: &mut Subs, - aliases: &mut Aliases, - constraint: &Constraint, - pending_derives: PendingDerives, - abilities_store: &mut AbilitiesStore, -) -> Env { - let mut pools = Pools::default(); - - let state = State { - env: Env::default(), - mark: Mark::NONE.next(), - }; - let rank = Rank::toplevel(); - let arena = Bump::new(); - - let pending_derives = PendingDerivesTable::new(subs, aliases, pending_derives); - let mut deferred_obligations = DeferredObligations::new(pending_derives); - - // Because we don't know what ability specializations are available until the entire module is - // solved, we must wait to solve unspecialized lambda sets then. - let mut deferred_uls_to_resolve = UlsOfVar::default(); - - let state = solve( - &arena, - constraints, - state, - rank, - &mut pools, - problems, - aliases, - subs, - constraint, - abilities_store, - &mut deferred_obligations, - &mut deferred_uls_to_resolve, - ); - - // Now that the module has been solved, we can run through and check all - // types claimed to implement abilities. This will also tell us what derives - // are legal, which we need to register. - let (obligation_problems, _derived) = deferred_obligations.check_all(subs, abilities_store); - problems.extend(obligation_problems); - - compact_lambda_sets_of_vars( - subs, - &arena, - &mut pools, - abilities_store, - deferred_uls_to_resolve, - Phase::Solve, - ); - - state.env -} - -enum Work<'a> { - Constraint { - env: &'a Env, - rank: Rank, - constraint: &'a Constraint, - }, - CheckForInfiniteTypes(LocalDefVarsVec<(Symbol, Loc)>), - /// The ret_con part of a let constraint that does NOT introduces rigid and/or flex variables - LetConNoVariables { - env: &'a Env, - rank: Rank, - let_con: &'a LetConstraint, - - /// The variables used to store imported types in the Subs. - /// The `Contents` are copied from the source module, but to - /// mimic `type_to_var`, we must add these variables to `Pools` - /// at the correct rank - pool_variables: &'a [Variable], - }, - /// The ret_con part of a let constraint that introduces rigid and/or flex variables - /// - /// These introduced variables must be generalized, hence this variant - /// is more complex than `LetConNoVariables`. - LetConIntroducesVariables { - env: &'a Env, - rank: Rank, - let_con: &'a LetConstraint, - - /// The variables used to store imported types in the Subs. - /// The `Contents` are copied from the source module, but to - /// mimic `type_to_var`, we must add these variables to `Pools` - /// at the correct rank - pool_variables: &'a [Variable], - }, -} - -#[allow(clippy::too_many_arguments)] -fn solve( - arena: &Bump, - constraints: &Constraints, - mut state: State, - rank: Rank, - pools: &mut Pools, - problems: &mut Vec, - aliases: &mut Aliases, - subs: &mut Subs, - constraint: &Constraint, - abilities_store: &mut AbilitiesStore, - deferred_obligations: &mut DeferredObligations, - deferred_uls_to_resolve: &mut UlsOfVar, -) -> State { - let initial = Work::Constraint { - env: &Env::default(), - rank, - constraint, - }; - - let mut stack = vec![initial]; - - while let Some(work_item) = stack.pop() { - let (env, rank, constraint) = match work_item { - Work::Constraint { - env, - rank, - constraint, - } => { - // the default case; actually solve this constraint - (env, rank, constraint) - } - Work::CheckForInfiniteTypes(def_vars) => { - // after a LetCon, we must check if any of the variables that we introduced - // loop back to themselves after solving the ret_constraint - for (symbol, loc_var) in def_vars.iter() { - check_for_infinite_type(subs, problems, *symbol, *loc_var); - } - - continue; - } - Work::LetConNoVariables { - env, - rank, - let_con, - pool_variables, - } => { - // NOTE be extremely careful with shadowing here - let offset = let_con.defs_and_ret_constraint.index(); - let ret_constraint = &constraints.constraints[offset + 1]; - - // Add a variable for each def to new_vars_by_env. - let local_def_vars = LocalDefVarsVec::from_def_types( - constraints, - rank, - pools, - aliases, - subs, - let_con.def_types, - ); - - pools.get_mut(rank).extend(pool_variables); - - let mut new_env = env.clone(); - for (symbol, loc_var) in local_def_vars.iter() { - check_ability_specialization( - arena, - subs, - pools, - rank, - abilities_store, - problems, - deferred_obligations, - deferred_uls_to_resolve, - *symbol, - *loc_var, - ); - - new_env.insert_symbol_var_if_vacant(*symbol, loc_var.value); - } - - stack.push(Work::CheckForInfiniteTypes(local_def_vars)); - stack.push(Work::Constraint { - env: arena.alloc(new_env), - rank, - constraint: ret_constraint, - }); - - continue; - } - Work::LetConIntroducesVariables { - env, - rank, - let_con, - pool_variables, - } => { - // NOTE be extremely careful with shadowing here - let offset = let_con.defs_and_ret_constraint.index(); - let ret_constraint = &constraints.constraints[offset + 1]; - - let next_rank = rank.next(); - - let mark = state.mark; - let saved_env = state.env; - - let young_mark = mark; - let visit_mark = young_mark.next(); - let final_mark = visit_mark.next(); - - // Add a variable for each def to local_def_vars. - let local_def_vars = LocalDefVarsVec::from_def_types( - constraints, - next_rank, - pools, - aliases, - subs, - let_con.def_types, - ); - - pools.get_mut(next_rank).extend(pool_variables); - - debug_assert_eq!( - // Check that no variable ended up in a higher rank than the next rank.. that - // would mean we generalized one level more than we need to! - { - let offenders = pools - .get(next_rank) - .iter() - .filter(|var| { - subs.get_rank(**var).into_usize() > next_rank.into_usize() - }) - .collect::>(); - - let result = offenders.len(); - - if result > 0 { - dbg!(&subs, &offenders, &let_con.def_types); - } - - result - }, - 0 - ); - - // pop pool - generalize(subs, young_mark, visit_mark, next_rank, pools); - debug_assert!(pools.get(next_rank).is_empty()); - - // check that things went well - dbg_do!(ROC_VERIFY_RIGID_LET_GENERALIZED, { - let rigid_vars = &constraints.variables[let_con.rigid_vars.indices()]; - - // NOTE the `subs.redundant` check does not come from elm. - // It's unclear whether this is a bug with our implementation - // (something is redundant that shouldn't be) - // or that it just never came up in elm. - let mut it = rigid_vars - .iter() - .filter(|&var| !subs.redundant(*var) && subs.get_rank(*var) != Rank::NONE) - .peekable(); - - if it.peek().is_some() { - let failing: Vec<_> = it.collect(); - println!("Rigids {:?}", &rigid_vars); - println!("Failing {:?}", failing); - debug_assert!(false); - } - }); - - let mut new_env = env.clone(); - for (symbol, loc_var) in local_def_vars.iter() { - check_ability_specialization( - arena, - subs, - pools, - rank, - abilities_store, - problems, - deferred_obligations, - deferred_uls_to_resolve, - *symbol, - *loc_var, - ); - - new_env.insert_symbol_var_if_vacant(*symbol, loc_var.value); - } - - // Note that this vars_by_symbol is the one returned by the - // previous call to solve() - let state_for_ret_con = State { - env: saved_env, - mark: final_mark, - }; - - // Now solve the body, using the new vars_by_symbol which includes - // the assignments' name-to-variable mappings. - stack.push(Work::CheckForInfiniteTypes(local_def_vars)); - stack.push(Work::Constraint { - env: arena.alloc(new_env), - rank, - constraint: ret_constraint, - }); - - state = state_for_ret_con; - - continue; - } - }; - - state = match constraint { - True => state, - SaveTheEnvironment => { - let mut copy = state; - - copy.env = env.clone(); - - copy - } - Eq(roc_can::constraint::Eq(type_index, expectation_index, category_index, region)) => { - let category = &constraints.categories[category_index.index()]; - - let actual = - either_type_index_to_var(constraints, subs, rank, pools, aliases, *type_index); - - let expectation = &constraints.expectations[expectation_index.index()]; - let expected = type_to_var(subs, rank, pools, aliases, expectation.get_type_ref()); - - match unify(subs, actual, expected, Mode::EQ) { - Success { - vars, - must_implement_ability, - lambda_sets_to_specialize, - } => { - introduce(subs, rank, pools, &vars); - if !must_implement_ability.is_empty() { - deferred_obligations.add( - must_implement_ability, - AbilityImplError::BadExpr(*region, category.clone(), actual), - ); - } - deferred_uls_to_resolve.union(lambda_sets_to_specialize); - - state - } - Failure(vars, actual_type, expected_type, _bad_impls) => { - introduce(subs, rank, pools, &vars); - - let problem = TypeError::BadExpr( - *region, - category.clone(), - actual_type, - expectation.clone().replace(expected_type), - ); - - problems.push(problem); - - state - } - BadType(vars, problem) => { - introduce(subs, rank, pools, &vars); - - problems.push(TypeError::BadType(problem)); - - state - } - } - } - Store(source_index, target, _filename, _linenr) => { - // a special version of Eq that is used to store types in the AST. - // IT DOES NOT REPORT ERRORS! - let actual = either_type_index_to_var( - constraints, - subs, - rank, - pools, - aliases, - *source_index, - ); - let target = *target; - - match unify(subs, actual, target, Mode::EQ) { - Success { - vars, - // ERROR NOT REPORTED - must_implement_ability: _, - lambda_sets_to_specialize, - } => { - introduce(subs, rank, pools, &vars); - - deferred_uls_to_resolve.union(lambda_sets_to_specialize); - - state - } - Failure(vars, _actual_type, _expected_type, _bad_impls) => { - introduce(subs, rank, pools, &vars); - - // ERROR NOT REPORTED - - state - } - BadType(vars, _) => { - introduce(subs, rank, pools, &vars); - - // ERROR NOT REPORTED - - state - } - } - } - Lookup(symbol, expectation_index, region) => { - match env.get_var_by_symbol(symbol) { - Some(var) => { - // Deep copy the vars associated with this symbol before unifying them. - // Otherwise, suppose we have this: - // - // identity = \a -> a - // - // x = identity 5 - // - // When we call (identity 5), it's important that we not unify - // on identity's original vars. If we do, the type of `identity` will be - // mutated to be `Int -> Int` instead of `a -> `, which would be incorrect; - // the type of `identity` is more general than that! - // - // Instead, we want to unify on a *copy* of its vars. If the copy unifies - // successfully (in this case, to `Int -> Int`), we can use that to - // infer the type of this lookup (in this case, `Int`) without ever - // having mutated the original. - // - // If this Lookup is targeting a value in another module, - // then we copy from that module's Subs into our own. If the value - // is being looked up in this module, then we use our Subs as both - // the source and destination. - let actual = deep_copy_var_in(subs, rank, pools, var, arena); - let expectation = &constraints.expectations[expectation_index.index()]; - - let expected = - type_to_var(subs, rank, pools, aliases, expectation.get_type_ref()); - - match unify(subs, actual, expected, Mode::EQ) { - Success { - vars, - must_implement_ability, - lambda_sets_to_specialize, - } => { - introduce(subs, rank, pools, &vars); - if !must_implement_ability.is_empty() { - deferred_obligations.add( - must_implement_ability, - AbilityImplError::BadExpr( - *region, - Category::Lookup(*symbol), - actual, - ), - ); - } - deferred_uls_to_resolve.union(lambda_sets_to_specialize); - - state - } - - Failure(vars, actual_type, expected_type, _bad_impls) => { - introduce(subs, rank, pools, &vars); - - let problem = TypeError::BadExpr( - *region, - Category::Lookup(*symbol), - actual_type, - expectation.clone().replace(expected_type), - ); - - problems.push(problem); - - state - } - BadType(vars, problem) => { - introduce(subs, rank, pools, &vars); - - problems.push(TypeError::BadType(problem)); - - state - } - } - } - None => { - problems.push(TypeError::UnexposedLookup(*symbol)); - - state - } - } - } - And(slice) => { - let it = constraints.constraints[slice.indices()].iter().rev(); - for sub_constraint in it { - stack.push(Work::Constraint { - env, - rank, - constraint: sub_constraint, - }) - } - - state - } - Pattern(type_index, expectation_index, category_index, region) - | PatternPresence(type_index, expectation_index, category_index, region) => { - let category = &constraints.pattern_categories[category_index.index()]; - - let actual = - either_type_index_to_var(constraints, subs, rank, pools, aliases, *type_index); - - let expectation = &constraints.pattern_expectations[expectation_index.index()]; - let expected = type_to_var(subs, rank, pools, aliases, expectation.get_type_ref()); - - let mode = match constraint { - PatternPresence(..) => Mode::PRESENT, - _ => Mode::EQ, - }; - - match unify(subs, actual, expected, mode) { - Success { - vars, - must_implement_ability, - lambda_sets_to_specialize, - } => { - introduce(subs, rank, pools, &vars); - if !must_implement_ability.is_empty() { - deferred_obligations.add( - must_implement_ability, - AbilityImplError::BadPattern(*region, category.clone(), actual), - ); - } - deferred_uls_to_resolve.union(lambda_sets_to_specialize); - - state - } - Failure(vars, actual_type, expected_type, _bad_impls) => { - introduce(subs, rank, pools, &vars); - - let problem = TypeError::BadPattern( - *region, - category.clone(), - actual_type, - expectation.clone().replace(expected_type), - ); - - problems.push(problem); - - state - } - BadType(vars, problem) => { - introduce(subs, rank, pools, &vars); - - problems.push(TypeError::BadType(problem)); - - state - } - } - } - Let(index, pool_slice) => { - let let_con = &constraints.let_constraints[index.index()]; - - let offset = let_con.defs_and_ret_constraint.index(); - let defs_constraint = &constraints.constraints[offset]; - let ret_constraint = &constraints.constraints[offset + 1]; - - let flex_vars = &constraints.variables[let_con.flex_vars.indices()]; - let rigid_vars = &constraints.variables[let_con.rigid_vars.indices()]; - - let pool_variables = &constraints.variables[pool_slice.indices()]; - - if matches!(&ret_constraint, True) && let_con.rigid_vars.is_empty() { - debug_assert!(pool_variables.is_empty()); - - introduce(subs, rank, pools, flex_vars); - - // If the return expression is guaranteed to solve, - // solve the assignments themselves and move on. - stack.push(Work::Constraint { - env, - rank, - constraint: defs_constraint, - }); - - state - } else if let_con.rigid_vars.is_empty() && let_con.flex_vars.is_empty() { - // items are popped from the stack in reverse order. That means that we'll - // first solve then defs_constraint, and then (eventually) the ret_constraint. - // - // Note that the LetConSimple gets the current env and rank, - // and not the env/rank from after solving the defs_constraint - stack.push(Work::LetConNoVariables { - env, - rank, - let_con, - pool_variables, - }); - stack.push(Work::Constraint { - env, - rank, - constraint: defs_constraint, - }); - - state - } else { - // work in the next pool to localize header - let next_rank = rank.next(); - - // introduce variables - for &var in rigid_vars.iter().chain(flex_vars.iter()) { - subs.set_rank(var, next_rank); - } - - // determine the next pool - if next_rank.into_usize() < pools.len() { - // Nothing to do, we already accounted for the next rank, no need to - // adjust the pools - } else { - // we should be off by one at this point - debug_assert_eq!(next_rank.into_usize(), 1 + pools.len()); - pools.extend_to(next_rank.into_usize()); - } - - let pool: &mut Vec = pools.get_mut(next_rank); - - // Replace the contents of this pool with rigid_vars and flex_vars - pool.clear(); - pool.reserve(rigid_vars.len() + flex_vars.len()); - pool.extend(rigid_vars.iter()); - pool.extend(flex_vars.iter()); - - // run solver in next pool - - // items are popped from the stack in reverse order. That means that we'll - // first solve then defs_constraint, and then (eventually) the ret_constraint. - // - // Note that the LetConSimple gets the current env and rank, - // and not the env/rank from after solving the defs_constraint - stack.push(Work::LetConIntroducesVariables { - env, - rank, - let_con, - pool_variables, - }); - stack.push(Work::Constraint { - env, - rank: next_rank, - constraint: defs_constraint, - }); - - state - } - } - IsOpenType(type_index) => { - let actual = - either_type_index_to_var(constraints, subs, rank, pools, aliases, *type_index); - - open_tag_union(subs, actual); - - state - } - IncludesTag(index) => { - let includes_tag = &constraints.includes_tags[index.index()]; - - let roc_can::constraint::IncludesTag { - type_index, - tag_name, - types, - pattern_category, - region, - } = includes_tag; - - let typ = &constraints.types[type_index.index()]; - let tys = &constraints.types[types.indices()]; - let pattern_category = &constraints.pattern_categories[pattern_category.index()]; - - let actual = type_to_var(subs, rank, pools, aliases, typ); - let tag_ty = Type::TagUnion( - vec![(tag_name.clone(), tys.to_vec())], - TypeExtension::Closed, - ); - let includes = type_to_var(subs, rank, pools, aliases, &tag_ty); - - match unify(subs, actual, includes, Mode::PRESENT) { - Success { - vars, - must_implement_ability, - lambda_sets_to_specialize, - } => { - introduce(subs, rank, pools, &vars); - if !must_implement_ability.is_empty() { - deferred_obligations.add( - must_implement_ability, - AbilityImplError::BadPattern( - *region, - pattern_category.clone(), - actual, - ), - ); - } - deferred_uls_to_resolve.union(lambda_sets_to_specialize); - - state - } - Failure(vars, actual_type, expected_to_include_type, _bad_impls) => { - introduce(subs, rank, pools, &vars); - - let problem = TypeError::BadPattern( - *region, - pattern_category.clone(), - expected_to_include_type, - PExpected::NoExpectation(actual_type), - ); - problems.push(problem); - - state - } - BadType(vars, problem) => { - introduce(subs, rank, pools, &vars); - - problems.push(TypeError::BadType(problem)); - - state - } - } - } - &Exhaustive(eq, sketched_rows, context, exhaustive_mark) => { - // A few cases: - // 1. Either condition or branch types already have a type error. In this case just - // propagate it. - // 2. Types are correct, but there are redundancies. In this case we want - // exhaustiveness checking to pull those out. - // 3. Condition and branch types are "almost equal", that is one or the other is - // only missing a few more tags. In this case we want to run - // exhaustiveness checking both ways, to see which one is missing tags. - // 4. Condition and branch types aren't "almost equal", this is just a normal type - // error. - - let (real_var, real_region, expected_type, category_and_expected) = match eq { - Ok(eq) => { - let roc_can::constraint::Eq(real_var, expected, category, real_region) = - constraints.eq[eq.index()]; - let expected = &constraints.expectations[expected.index()]; - ( - real_var, - real_region, - expected.get_type_ref(), - Ok((category, expected)), - ) - } - Err(peq) => { - let roc_can::constraint::PatternEq( - real_var, - expected, - category, - real_region, - ) = constraints.pattern_eq[peq.index()]; - let expected = &constraints.pattern_expectations[expected.index()]; - ( - real_var, - real_region, - expected.get_type_ref(), - Err((category, expected)), - ) - } - }; - - let real_var = - either_type_index_to_var(constraints, subs, rank, pools, aliases, real_var); - - let branches_var = type_to_var(subs, rank, pools, aliases, expected_type); - - let real_content = subs.get_content_without_compacting(real_var); - let branches_content = subs.get_content_without_compacting(branches_var); - let already_have_error = matches!( - (real_content, branches_content), - ( - Content::Error | Content::Structure(FlatType::Erroneous(_)), - _ - ) | ( - _, - Content::Error | Content::Structure(FlatType::Erroneous(_)) - ) - ); - - let snapshot = subs.snapshot(); - let outcome = unify(subs, real_var, branches_var, Mode::EQ); - - let should_check_exhaustiveness; - match outcome { - Success { - vars, - must_implement_ability, - lambda_sets_to_specialize, - } => { - subs.commit_snapshot(snapshot); - - introduce(subs, rank, pools, &vars); - if !must_implement_ability.is_empty() { - internal_error!("Didn't expect ability vars to land here"); - } - - deferred_uls_to_resolve.union(lambda_sets_to_specialize); - - // Case 1: unify error types, but don't check exhaustiveness. - // Case 2: run exhaustiveness to check for redundant branches. - should_check_exhaustiveness = !already_have_error; - } - Failure(..) => { - // Rollback and check for almost-equality. - subs.rollback_to(snapshot); - - let almost_eq_snapshot = subs.snapshot(); - // TODO: turn this on for bidirectional exhaustiveness checking - // open_tag_union(subs, real_var); - open_tag_union(subs, branches_var); - let almost_eq = matches!( - unify(subs, real_var, branches_var, Mode::EQ), - Success { .. } - ); - - subs.rollback_to(almost_eq_snapshot); - - if almost_eq { - // Case 3: almost equal, check exhaustiveness. - should_check_exhaustiveness = true; - } else { - // Case 4: incompatible types, report type error. - // Re-run first failed unification to get the type diff. - match unify(subs, real_var, branches_var, Mode::EQ) { - Failure(vars, actual_type, expected_type, _bad_impls) => { - introduce(subs, rank, pools, &vars); - - // Figure out the problem - it might be pattern or value - // related. - let problem = match category_and_expected { - Ok((category, expected)) => { - let real_category = - constraints.categories[category.index()].clone(); - TypeError::BadExpr( - real_region, - real_category, - actual_type, - expected.replace_ref(expected_type), - ) - } - - Err((category, expected)) => { - let real_category = constraints.pattern_categories - [category.index()] - .clone(); - TypeError::BadPattern( - real_region, - real_category, - expected_type, - expected.replace_ref(actual_type), - ) - } - }; - - problems.push(problem); - should_check_exhaustiveness = false; - } - _ => internal_error!("Must be failure"), - } - } - } - BadType(vars, problem) => { - subs.commit_snapshot(snapshot); - - introduce(subs, rank, pools, &vars); - - problems.push(TypeError::BadType(problem)); - - should_check_exhaustiveness = false; - } - } - - let sketched_rows = constraints.sketched_rows[sketched_rows.index()].clone(); - - if should_check_exhaustiveness { - use roc_can::exhaustive::{check, ExhaustiveSummary}; - - let ExhaustiveSummary { - errors, - exhaustive, - redundancies, - } = check(subs, sketched_rows, context); - - // Store information about whether the "when" is exhaustive, and - // which (if any) of its branches are redundant. Codegen may use - // this for branch-fixing and redundant elimination. - if !exhaustive { - exhaustive_mark.set_non_exhaustive(subs); - } - for redundant_mark in redundancies { - redundant_mark.set_redundant(subs); - } - - // Store the errors. - problems.extend(errors.into_iter().map(TypeError::Exhaustive)); - } - - state - } - &Resolve(OpportunisticResolve { - specialization_variable, - specialization_expectation, - member, - specialization_id, - }) => { - if let Some(Resolved::Specialization(specialization)) = - resolve_ability_specialization( - subs, - abilities_store, - member, - specialization_variable, - ) - { - abilities_store.insert_resolved(specialization_id, specialization); - - // We must now refine the current type state to account for this specialization. - let lookup_constr = arena.alloc(Constraint::Lookup( - specialization, - specialization_expectation, - Region::zero(), - )); - stack.push(Work::Constraint { - env, - rank, - constraint: lookup_constr, - }); - } - - state - } - CheckCycle(cycle, cycle_mark) => { - let Cycle { - def_names, - expr_regions, - } = &constraints.cycles[cycle.index()]; - let symbols = &constraints.loc_symbols[def_names.indices()]; - - // If the type of a symbol is not a function, that's an error. - // Roc is strict, so only functions can be mutually recursive. - let any_is_bad = { - use Content::*; - - symbols.iter().any(|(s, _)| { - let var = env.get_var_by_symbol(s).expect("Symbol not solved!"); - let content = subs.get_content_without_compacting(var); - !matches!(content, Error | Structure(FlatType::Func(..))) - }) - }; - - if any_is_bad { - // expr regions are stored in loc_symbols (that turned out to be convenient). - // The symbol is just a dummy, and should not be used - let expr_regions = &constraints.loc_symbols[expr_regions.indices()]; - - let cycle = symbols - .iter() - .zip(expr_regions.iter()) - .map(|(&(symbol, symbol_region), &(_, expr_region))| CycleEntry { - symbol, - symbol_region, - expr_region, - }) - .collect(); - - problems.push(TypeError::CircularDef(cycle)); - - cycle_mark.set_illegal(subs); - } - - state - } - }; - } - - state -} - -fn open_tag_union(subs: &mut Subs, var: Variable) { - let mut stack = vec![var]; - while let Some(var) = stack.pop() { - use {Content::*, FlatType::*}; - - let desc = subs.get(var); - if let Structure(TagUnion(tags, ext)) = desc.content { - if let Structure(EmptyTagUnion) = subs.get_content_without_compacting(ext) { - let new_ext = subs.fresh_unnamed_flex_var(); - subs.set_rank(new_ext, desc.rank); - let new_union = Structure(TagUnion(tags, new_ext)); - subs.set_content(var, new_union); - } - - // Also open up all nested tag unions. - let all_vars = tags.variables().into_iter(); - stack.extend(all_vars.flat_map(|slice| subs[slice]).map(|var| subs[var])); - } - - // Today, an "open" constraint doesn't affect any types - // other than tag unions. Recursive tag unions are constructed - // at a later time (during occurs checks after tag unions are - // resolved), so that's not handled here either. - // NB: Handle record types here if we add presence constraints - // to their type inference as well. - } -} - -/// If a symbol claims to specialize an ability member, check that its solved type in fact -/// does specialize the ability, and record the specialization. -#[allow(clippy::too_many_arguments)] -// Aggressive but necessary - there aren't many usages. -#[inline(always)] -fn check_ability_specialization( - arena: &Bump, - subs: &mut Subs, - pools: &mut Pools, - rank: Rank, - abilities_store: &mut AbilitiesStore, - problems: &mut Vec, - deferred_obligations: &mut DeferredObligations, - deferred_uls_to_resolve: &mut UlsOfVar, - symbol: Symbol, - symbol_loc_var: Loc, -) { - // If the symbol specializes an ability member, we need to make sure that the - // inferred type for the specialization actually aligns with the expected - // implementation. - if let Some((ability_member, root_data)) = abilities_store.root_name_and_def(symbol) { - let root_signature_var = root_data.signature_var().unwrap_or_else(|| { - internal_error!("Signature var not resolved for {:?}", ability_member) - }); - let parent_ability = root_data.parent_ability; - - // Check if they unify - if they don't, then the claimed specialization isn't really one, - // and that's a type error! - // This also fixes any latent type variables that need to be specialized to exactly what - // the ability signature expects. - - // We need to freshly instantiate the root signature so that all unifications are reflected - // in the specialization type, but not the original signature type. - let root_signature_var = - deep_copy_var_in(subs, Rank::toplevel(), pools, root_signature_var, arena); - let snapshot = subs.snapshot(); - let unified = unify(subs, symbol_loc_var.value, root_signature_var, Mode::EQ); - - match unified { - Success { - vars, - must_implement_ability, - lambda_sets_to_specialize, - } => { - let specialization_type = - type_implementing_specialization(&must_implement_ability, parent_ability); - - match specialization_type { - Some(Obligated::Opaque(opaque)) => { - // This is a specialization for an opaque - that's allowed. - - subs.commit_snapshot(snapshot); - introduce(subs, rank, pools, &vars); - - let (other_lambda_sets_to_specialize, specialization_lambda_sets) = - find_specialization_lambda_sets( - subs, - opaque, - ability_member, - lambda_sets_to_specialize, - ); - deferred_uls_to_resolve.union(other_lambda_sets_to_specialize); - - let specialization_region = symbol_loc_var.region; - let specialization = - MemberSpecialization::new(symbol, specialization_lambda_sets); - abilities_store.register_specialization_for_type( - ability_member, - opaque, - specialization, - ); - - // Make sure we check that the opaque has specialized all members of the - // ability, after we finish solving the module. - deferred_obligations - .add(must_implement_ability, AbilityImplError::IncompleteAbility); - // This specialization dominates any derives that might be present. - deferred_obligations.dominate( - DeriveKey { - opaque, - ability: parent_ability, - }, - specialization_region, - ); - } - Some(Obligated::Adhoc(var)) => { - // This is a specialization of a structural type - never allowed. - - // Commit so that `var` persists in subs. - subs.commit_snapshot(snapshot); - - let (typ, _problems) = subs.var_to_error_type(var); - - let problem = TypeError::StructuralSpecialization { - region: symbol_loc_var.region, - typ, - ability: parent_ability, - member: ability_member, - }; - - problems.push(problem); - } - None => { - // This can happen when every ability constriant on a type variable went - // through only another type variable. That means this def is not specialized - // for one concrete type - we won't admit this. - - // Rollback the snapshot so we unlink the root signature with the specialization, - // so we can have two separate error types. - subs.rollback_to(snapshot); - - let (expected_type, _problems) = subs.var_to_error_type(root_signature_var); - let (actual_type, _problems) = subs.var_to_error_type(symbol_loc_var.value); - - let reason = Reason::GeneralizedAbilityMemberSpecialization { - member_name: ability_member, - def_region: root_data.region, - }; - - let problem = TypeError::BadExpr( - symbol_loc_var.region, - Category::AbilityMemberSpecialization(ability_member), - actual_type, - Expected::ForReason(reason, expected_type, symbol_loc_var.region), - ); - - problems.push(problem); - } - } - } - - Failure(vars, actual_type, expected_type, unimplemented_abilities) => { - subs.commit_snapshot(snapshot); - introduce(subs, rank, pools, &vars); - - let reason = Reason::InvalidAbilityMemberSpecialization { - member_name: ability_member, - def_region: root_data.region, - unimplemented_abilities, - }; - - let problem = TypeError::BadExpr( - symbol_loc_var.region, - Category::AbilityMemberSpecialization(ability_member), - actual_type, - Expected::ForReason(reason, expected_type, symbol_loc_var.region), - ); - - problems.push(problem); - } - BadType(vars, problem) => { - subs.commit_snapshot(snapshot); - introduce(subs, rank, pools, &vars); - - problems.push(TypeError::BadType(problem)); - } - } - } -} - -/// Finds the lambda sets in an ability member specialization. -/// -/// Suppose we have -/// -/// Default has default : {} -[[] + a:default:1]-> a | a has Default -/// -/// A := {} -/// default = \{} -[[closA]]-> @A {} -/// -/// Now after solving the `default` specialization we have unified it with the ability signature, -/// yielding -/// -/// {} -[[closA] + A:default:1]-> A -/// -/// But really, what we want is to only keep around the original lambda sets, and associate -/// `A:default:1` to resolve to the lambda set `[[closA]]`. There might be other unspecialized lambda -/// sets in the lambda sets for this implementation, which we need to account for as well; that is, -/// it may really be `[[closA] + v123:otherAbilityMember:4 + ...]`. -#[inline(always)] -fn find_specialization_lambda_sets( - subs: &mut Subs, - opaque: Symbol, - ability_member: Symbol, - uls: UlsOfVar, -) -> (UlsOfVar, VecMap) { - // unspecialized lambda sets that don't belong to our specialization, and should be resolved - // later. - let mut leftover_uls = UlsOfVar::default(); - let mut specialization_lambda_sets: VecMap = VecMap::with_capacity(uls.len()); - - for (spec_var, lambda_sets) in uls.drain() { - if !matches!(subs.get_content_without_compacting(spec_var), Content::Alias(name, _, _, AliasKind::Opaque) if *name == opaque) - { - // These lambda sets aren't resolved to the current specialization, they need to be - // solved at a later time. - leftover_uls.extend(spec_var, lambda_sets); - continue; - } - - for lambda_set in lambda_sets { - let &LambdaSet { - solved, - recursion_var, - unspecialized, - } = match subs.get_content_without_compacting(lambda_set) { - Content::LambdaSet(lambda_set) => lambda_set, - _ => internal_error!("Not a lambda set"), - }; - - // Figure out the unspecailized lambda set that corresponds to our specialization - // (`A:default:1` in the example), and those that need to stay part of the lambda set. - let mut split_index_and_region = None; - let uls_slice = subs.get_subs_slice(unspecialized).to_owned(); - for (i, &Uls(var, _sym, region)) in uls_slice.iter().enumerate() { - if var == spec_var { - debug_assert!(split_index_and_region.is_none()); - debug_assert!(_sym == ability_member, "unspecialized lambda set var is the same as the specialization, but points to a different ability member"); - split_index_and_region = Some((i, region)); - } - } - - let (split_index, specialized_lset_region) = - split_index_and_region.expect("no unspecialization lambda set found"); - let (uls_before, uls_after) = - (&uls_slice[0..split_index], &uls_slice[split_index + 1..]); - - let new_unspecialized = SubsSlice::extend_new( - &mut subs.unspecialized_lambda_sets, - uls_before.iter().chain(uls_after.iter()).copied(), - ); - - let new_lambda_set_content = Content::LambdaSet(LambdaSet { - solved, - recursion_var, - unspecialized: new_unspecialized, - }); - subs.set_content(lambda_set, new_lambda_set_content); - - let old_specialized = - specialization_lambda_sets.insert(specialized_lset_region, lambda_set); - debug_assert!( - old_specialized.is_none(), - "Specialization of lambda set already exists" - ); - } - } - - (leftover_uls, specialization_lambda_sets) -} - -pub fn compact_lambda_sets_of_vars( - subs: &mut Subs, - arena: &Bump, - pools: &mut Pools, - abilities_store: &AbilitiesStore, - uls_of_var: UlsOfVar, - phase: Phase, -) { - let mut seen = VecSet::default(); - for (_, lambda_sets) in uls_of_var.drain() { - for lset in lambda_sets { - let root_lset = subs.get_root_key_without_compacting(lset); - if seen.contains(&root_lset) { - continue; - } - - compact_lambda_set(subs, arena, pools, abilities_store, root_lset, phase); - seen.insert(root_lset); - } - } -} - -fn compact_lambda_set( - subs: &mut Subs, - arena: &Bump, - pools: &mut Pools, - abilities_store: &AbilitiesStore, - this_lambda_set: Variable, - phase: Phase, -) { - let LambdaSet { - solved, - recursion_var, - unspecialized, - } = subs.get_lambda_set(this_lambda_set); - let target_rank = subs.get_rank(this_lambda_set); - - if unspecialized.is_empty() { - return; - } - - let mut new_unspecialized = vec![]; - let mut specialized_to_unify_with = Vec::with_capacity(1); - for uls_index in unspecialized.into_iter() { - let uls @ Uls(var, member, region) = subs[uls_index]; - - use Content::*; - let opaque = match subs.get_content_without_compacting(var) { - FlexAbleVar(_, _) => { - /* not specialized yet */ - new_unspecialized.push(uls); - continue; - } - Structure(_) | Alias(_, _, _, AliasKind::Structural) => { - // TODO: figure out a convention for references to structural types in the - // unspecialized lambda set. This may very well happen, for example - // - // Default has default : {} -> a | a has Default - // - // {a, b} = default {} - // # ^^^^^^^ {} -[{a: t1, b: t2}:default:1] - new_unspecialized.push(uls); - continue; - } - Alias(opaque, _, _, AliasKind::Opaque) => opaque, - Error => { - /* skip */ - continue; - } - RigidVar(..) - | RigidAbleVar(..) - | FlexVar(..) - | RecursionVar { .. } - | LambdaSet(..) - | RangedNumber(_, _) => { - internal_error!("unexpected") - } - }; - - let opt_specialization = abilities_store.get_specialization(member, *opaque); - let specialized_lambda_set = match (phase, opt_specialization) { - (Phase::Solve, None) => { - // doesn't specialize, we'll have reported an error for this - continue; - } - (Phase::Late, None) => { - internal_error!( - "expected to know a specialization for {:?}#{:?}, but it wasn't found", - opaque, - member - ); - } - (_, Some(specialization)) => *specialization - .specialization_lambda_sets - .get(®ion) - .expect("lambda set region not resolved"), - }; - - // Ensure the specialization lambda set is already compacted. - if subs.get_root_key(specialized_lambda_set) != subs.get_root_key(this_lambda_set) { - compact_lambda_set( - subs, - arena, - pools, - abilities_store, - specialized_lambda_set, - phase, - ); - } - - // Ensure the specialization lambda set we'll unify with is not a generalized one, but one - // at the rank of the lambda set being compacted. - let copy_specialized_lambda_set = - deep_copy_var_in(subs, target_rank, pools, specialized_lambda_set, arena); - - specialized_to_unify_with.push(copy_specialized_lambda_set); - } - - let new_unspecialized_slice = - SubsSlice::extend_new(&mut subs.unspecialized_lambda_sets, new_unspecialized); - let partial_compacted_lambda_set = Content::LambdaSet(LambdaSet { - solved, - recursion_var, - unspecialized: new_unspecialized_slice, - }); - subs.set_content(this_lambda_set, partial_compacted_lambda_set); - - for other_specialized in specialized_to_unify_with.into_iter() { - let (vars, must_implement_ability, lambda_sets_to_specialize) = - unify(subs, this_lambda_set, other_specialized, Mode::EQ) - .expect_success("lambda sets don't unify"); - - introduce(subs, subs.get_rank(this_lambda_set), pools, &vars); - - debug_assert!( - must_implement_ability.is_empty(), - "didn't expect abilities instantiated in this position" - ); - debug_assert!( - lambda_sets_to_specialize.is_empty(), - "didn't expect more lambda sets in this position" - ); - } -} - -#[derive(Debug)] -enum LocalDefVarsVec { - Stack(arrayvec::ArrayVec), - Heap(Vec), -} - -impl LocalDefVarsVec { - #[inline(always)] - fn with_length(length: usize) -> Self { - if length <= 32 { - Self::Stack(Default::default()) - } else { - Self::Heap(Default::default()) - } - } - - fn push(&mut self, element: T) { - match self { - LocalDefVarsVec::Stack(vec) => vec.push(element), - LocalDefVarsVec::Heap(vec) => vec.push(element), - } - } - - fn iter(&self) -> impl Iterator { - match self { - LocalDefVarsVec::Stack(vec) => vec.iter(), - LocalDefVarsVec::Heap(vec) => vec.iter(), - } - } -} - -impl LocalDefVarsVec<(Symbol, Loc)> { - fn from_def_types( - constraints: &Constraints, - rank: Rank, - pools: &mut Pools, - aliases: &mut Aliases, - subs: &mut Subs, - def_types_slice: roc_can::constraint::DefTypes, - ) -> Self { - let types_slice = &constraints.types[def_types_slice.types.indices()]; - let loc_symbols_slice = &constraints.loc_symbols[def_types_slice.loc_symbols.indices()]; - - let mut local_def_vars = Self::with_length(types_slice.len()); - - for (&(symbol, region), typ) in (loc_symbols_slice.iter()).zip(types_slice) { - let var = type_to_var(subs, rank, pools, aliases, typ); - - local_def_vars.push((symbol, Loc { value: var, region })); - } - - local_def_vars - } -} - -use std::cell::RefCell; -use std::ops::ControlFlow; -std::thread_local! { - /// Scratchpad arena so we don't need to allocate a new one all the time - static SCRATCHPAD: RefCell> = RefCell::new(Some(bumpalo::Bump::with_capacity(4 * 1024))); -} - -fn take_scratchpad() -> bumpalo::Bump { - SCRATCHPAD.with(|f| f.take().unwrap()) -} - -fn put_scratchpad(scratchpad: bumpalo::Bump) { - SCRATCHPAD.with(|f| { - f.replace(Some(scratchpad)); - }); -} - -fn either_type_index_to_var( - constraints: &Constraints, - subs: &mut Subs, - rank: Rank, - pools: &mut Pools, - aliases: &mut Aliases, - either_type_index: roc_collections::soa::EitherIndex, -) -> Variable { - match either_type_index.split() { - Ok(type_index) => { - let typ = &constraints.types[type_index.index()]; - - type_to_var(subs, rank, pools, aliases, typ) - } - Err(var_index) => { - // we cheat, and store the variable directly in the index - unsafe { Variable::from_index(var_index.index() as _) } - } - } -} - -pub(crate) fn type_to_var( - subs: &mut Subs, - rank: Rank, - pools: &mut Pools, - aliases: &mut Aliases, - typ: &Type, -) -> Variable { - if let Type::Variable(var) = typ { - *var - } else { - let mut arena = take_scratchpad(); - - let var = type_to_variable(subs, rank, pools, &arena, aliases, typ); - - arena.reset(); - put_scratchpad(arena); - - var - } -} - -enum RegisterVariable { - /// Based on the Type, we already know what variable this will be - Direct(Variable), - /// This Type needs more complicated Content. We reserve a Variable - /// for it, but put a placeholder Content in subs - Deferred, -} - -impl RegisterVariable { - fn from_type( - subs: &mut Subs, - rank: Rank, - pools: &mut Pools, - arena: &'_ bumpalo::Bump, - typ: &Type, - ) -> Self { - use RegisterVariable::*; - - match typ { - Type::Variable(var) => Direct(*var), - EmptyRec => Direct(Variable::EMPTY_RECORD), - EmptyTagUnion => Direct(Variable::EMPTY_TAG_UNION), - Type::DelayedAlias(AliasCommon { symbol, .. }) => { - if let Some(reserved) = Variable::get_reserved(*symbol) { - if rank.is_none() { - // reserved variables are stored with rank NONE - return Direct(reserved); - } else { - // for any other rank, we need to copy; it takes care of adjusting the rank - let copied = deep_copy_var_in(subs, rank, pools, reserved, arena); - return Direct(copied); - } - } - - Deferred - } - Type::Alias { symbol, .. } => { - if let Some(reserved) = Variable::get_reserved(*symbol) { - if rank.is_none() { - // reserved variables are stored with rank NONE - return Direct(reserved); - } else { - // for any other rank, we need to copy; it takes care of adjusting the rank - let copied = deep_copy_var_in(subs, rank, pools, reserved, arena); - return Direct(copied); - } - } - - Deferred - } - _ => Deferred, - } - } - - #[inline(always)] - fn with_stack<'a>( - subs: &mut Subs, - rank: Rank, - pools: &mut Pools, - arena: &'_ bumpalo::Bump, - typ: &'a Type, - stack: &mut bumpalo::collections::Vec<'_, TypeToVar<'a>>, - ) -> Variable { - match Self::from_type(subs, rank, pools, arena, typ) { - Self::Direct(var) => var, - Self::Deferred => { - let var = subs.fresh_unnamed_flex_var(); - stack.push(TypeToVar::Defer(typ, var)); - var - } - } - } -} - -#[derive(Debug)] -enum TypeToVar<'a> { - Defer(&'a Type, Variable), -} - -fn type_to_variable<'a>( - subs: &mut Subs, - rank: Rank, - pools: &mut Pools, - arena: &'a bumpalo::Bump, - aliases: &mut Aliases, - typ: &Type, -) -> Variable { - use bumpalo::collections::Vec; - - let mut stack = Vec::with_capacity_in(8, arena); - - macro_rules! helper { - ($typ:expr) => {{ - match RegisterVariable::from_type(subs, rank, pools, arena, $typ) { - RegisterVariable::Direct(var) => var, - RegisterVariable::Deferred => { - let var = subs.fresh_unnamed_flex_var(); - stack.push(TypeToVar::Defer($typ, var)); - var - } - } - }}; - } - - let result = helper!(typ); - - while let Some(TypeToVar::Defer(typ, destination)) = stack.pop() { - match typ { - Variable(_) | EmptyRec | EmptyTagUnion => { - unreachable!("This variant should never be deferred!") - } - RangedNumber(typ, range) => { - let ty_var = helper!(typ); - let content = Content::RangedNumber(ty_var, *range); - - register_with_known_var(subs, destination, rank, pools, content) - } - Apply(symbol, arguments, _) => { - let new_arguments = VariableSubsSlice::reserve_into_subs(subs, arguments.len()); - for (target_index, var_index) in (new_arguments.indices()).zip(arguments) { - let var = helper!(var_index); - subs.variables[target_index] = var; - } - - let flat_type = FlatType::Apply(*symbol, new_arguments); - let content = Content::Structure(flat_type); - - register_with_known_var(subs, destination, rank, pools, content) - } - - ClosureTag { name, captures } => { - let union_lambdas = - create_union_lambda(subs, rank, pools, arena, *name, captures, &mut stack); - - let content = Content::LambdaSet(subs::LambdaSet { - solved: union_lambdas, - // We may figure out the lambda set is recursive during solving, but it never - // is to begin with. - recursion_var: OptVariable::NONE, - unspecialized: SubsSlice::default(), - }); - - register_with_known_var(subs, destination, rank, pools, content) - } - UnspecializedLambdaSet(uls) => { - let unspecialized = SubsSlice::extend_new( - &mut subs.unspecialized_lambda_sets, - std::iter::once(*uls), - ); - - let content = Content::LambdaSet(subs::LambdaSet { - unspecialized, - solved: UnionLabels::default(), - recursion_var: OptVariable::NONE, - }); - - register_with_known_var(subs, destination, rank, pools, content) - } - // This case is important for the rank of boolean variables - Function(arguments, closure_type, ret_type) => { - let new_arguments = VariableSubsSlice::reserve_into_subs(subs, arguments.len()); - for (target_index, var_index) in (new_arguments.indices()).zip(arguments) { - let var = helper!(var_index); - subs.variables[target_index] = var; - } - - let ret_var = helper!(ret_type); - let closure_var = helper!(closure_type); - let content = - Content::Structure(FlatType::Func(new_arguments, closure_var, ret_var)); - - register_with_known_var(subs, destination, rank, pools, content) - } - Record(fields, ext) => { - // An empty fields is inefficient (but would be correct) - // If hit, try to turn the value into an EmptyRecord in canonicalization - debug_assert!(!fields.is_empty() || !ext.is_closed()); - - let mut field_vars = Vec::with_capacity_in(fields.len(), arena); - - for (field, field_type) in fields { - let field_var = { - use roc_types::types::RecordField::*; - match &field_type { - Optional(t) => Optional(helper!(t)), - Required(t) => Required(helper!(t)), - Demanded(t) => Demanded(helper!(t)), - } - }; - - field_vars.push((field.clone(), field_var)); - } - - let temp_ext_var = match ext { - TypeExtension::Open(ext) => helper!(ext), - TypeExtension::Closed => Variable::EMPTY_RECORD, - }; - - let (it, new_ext_var) = - gather_fields_unsorted_iter(subs, RecordFields::empty(), temp_ext_var) - .expect("Something ended up weird in this record type"); - - let it = it - .into_iter() - .map(|(field, field_type)| (field.clone(), field_type)); - - field_vars.extend(it); - insertion_sort_by(&mut field_vars, RecordFields::compare); - - let record_fields = RecordFields::insert_into_subs(subs, field_vars); - - let content = Content::Structure(FlatType::Record(record_fields, new_ext_var)); - - register_with_known_var(subs, destination, rank, pools, content) - } - - TagUnion(tags, ext) => { - // An empty tags is inefficient (but would be correct) - // If hit, try to turn the value into an EmptyTagUnion in canonicalization - debug_assert!(!tags.is_empty() || !ext.is_closed()); - - let (union_tags, ext) = - type_to_union_tags(subs, rank, pools, arena, tags, ext, &mut stack); - let content = Content::Structure(FlatType::TagUnion(union_tags, ext)); - - register_with_known_var(subs, destination, rank, pools, content) - } - FunctionOrTagUnion(tag_name, symbol, ext) => { - let temp_ext_var = match ext { - TypeExtension::Open(ext) => helper!(ext), - TypeExtension::Closed => Variable::EMPTY_TAG_UNION, - }; - - let (it, ext) = roc_types::types::gather_tags_unsorted_iter( - subs, - UnionTags::default(), - temp_ext_var, - ); - - for _ in it { - unreachable!("we assert that the ext var is empty; otherwise we'd already know it was a tag union!"); - } - - let slice = SubsIndex::new(subs.tag_names.len() as u32); - subs.tag_names.push(tag_name.clone()); - - let content = Content::Structure(FlatType::FunctionOrTagUnion(slice, *symbol, ext)); - - register_with_known_var(subs, destination, rank, pools, content) - } - RecursiveTagUnion(rec_var, tags, ext) => { - // An empty tags is inefficient (but would be correct) - // If hit, try to turn the value into an EmptyTagUnion in canonicalization - debug_assert!(!tags.is_empty() || !ext.is_closed()); - - let (union_tags, ext) = - type_to_union_tags(subs, rank, pools, arena, tags, ext, &mut stack); - let content = - Content::Structure(FlatType::RecursiveTagUnion(*rec_var, union_tags, ext)); - - let tag_union_var = destination; - register_with_known_var(subs, tag_union_var, rank, pools, content); - - register_with_known_var( - subs, - *rec_var, - rank, - pools, - Content::RecursionVar { - opt_name: None, - structure: tag_union_var, - }, - ); - - tag_union_var - } - - Type::DelayedAlias(AliasCommon { - symbol, - type_arguments, - lambda_set_variables, - }) => { - let alias_variables = { - let length = type_arguments.len() + lambda_set_variables.len(); - let new_variables = VariableSubsSlice::reserve_into_subs(subs, length); - - for (target_index, arg_type) in (new_variables.indices()).zip(type_arguments) { - let copy_var = helper!(arg_type); - subs.variables[target_index] = copy_var; - } - - let it = (new_variables.indices().skip(type_arguments.len())) - .zip(lambda_set_variables); - for (target_index, ls) in it { - let copy_var = helper!(&ls.0); - subs.variables[target_index] = copy_var; - } - - AliasVariables { - variables_start: new_variables.start, - type_variables_len: type_arguments.len() as _, - all_variables_len: length as _, - } - }; - - let (alias_variable, kind) = aliases.instantiate_real_var( - subs, - rank, - pools, - arena, - *symbol, - alias_variables, - ); - - let content = Content::Alias(*symbol, alias_variables, alias_variable, kind); - - register_with_known_var(subs, destination, rank, pools, content) - } - - Type::Alias { - symbol, - type_arguments, - actual, - lambda_set_variables, - kind, - } => { - debug_assert!(Variable::get_reserved(*symbol).is_none()); - - let alias_variables = { - let length = type_arguments.len() + lambda_set_variables.len(); - let new_variables = VariableSubsSlice::reserve_into_subs(subs, length); - - for (target_index, OptAbleType { typ, opt_ability }) in - (new_variables.indices()).zip(type_arguments) - { - let copy_var = match opt_ability { - None => helper!(typ), - Some(ability) => { - // If this type argument is marked as being bound to an ability, we must - // now correctly instantiate it as so. - match RegisterVariable::from_type(subs, rank, pools, arena, typ) { - RegisterVariable::Direct(var) => { - use Content::*; - match *subs.get_content_without_compacting(var) { - FlexVar(opt_name) => subs - .set_content(var, FlexAbleVar(opt_name, *ability)), - RigidVar(..) => internal_error!("Rigid var in type arg for {:?} - this is a bug in the solver, or our understanding", actual), - RigidAbleVar(..) | FlexAbleVar(..) => internal_error!("Able var in type arg for {:?} - this is a bug in the solver, or our understanding", actual), - _ => { - // TODO associate the type to the bound ability, and check - // that it correctly implements the ability. - } - } - var - } - RegisterVariable::Deferred => { - // TODO associate the type to the bound ability, and check - // that it correctly implements the ability. - let var = subs.fresh_unnamed_flex_var(); - stack.push(TypeToVar::Defer(typ, var)); - var - } - } - } - }; - subs.variables[target_index] = copy_var; - } - - let it = (new_variables.indices().skip(type_arguments.len())) - .zip(lambda_set_variables); - for (target_index, ls) in it { - let copy_var = helper!(&ls.0); - subs.variables[target_index] = copy_var; - } - - AliasVariables { - variables_start: new_variables.start, - type_variables_len: type_arguments.len() as _, - all_variables_len: length as _, - } - }; - - let alias_variable = if let Symbol::RESULT_RESULT = *symbol { - roc_result_to_var(subs, rank, pools, arena, actual, &mut stack) - } else { - helper!(actual) - }; - let content = Content::Alias(*symbol, alias_variables, alias_variable, *kind); - - register_with_known_var(subs, destination, rank, pools, content) - } - HostExposedAlias { - name: symbol, - type_arguments, - actual: alias_type, - actual_var, - lambda_set_variables, - .. - } => { - let alias_variables = { - let length = type_arguments.len() + lambda_set_variables.len(); - let new_variables = VariableSubsSlice::reserve_into_subs(subs, length); - - for (target_index, arg_type) in (new_variables.indices()).zip(type_arguments) { - let copy_var = helper!(arg_type); - subs.variables[target_index] = copy_var; - } - - AliasVariables { - variables_start: new_variables.start, - type_variables_len: type_arguments.len() as _, - all_variables_len: length as _, - } - }; - - // cannot use helper! here because this variable may be involved in unification below - let alias_variable = - type_to_variable(subs, rank, pools, arena, aliases, alias_type); - // TODO(opaques): I think host-exposed aliases should always be structural - // (when does it make sense to give a host an opaque type?) - let content = Content::Alias( - *symbol, - alias_variables, - alias_variable, - AliasKind::Structural, - ); - // let result = register(subs, rank, pools, content); - let result = register_with_known_var(subs, destination, rank, pools, content); - - // We only want to unify the actual_var with the alias once - // if it's already redirected (and therefore, redundant) - // don't do it again - if !subs.redundant(*actual_var) { - let descriptor = subs.get(result); - subs.union(result, *actual_var, descriptor); - } - - result - } - Erroneous(problem) => { - let problem_index = SubsIndex::push_new(&mut subs.problems, problem.clone()); - let content = Content::Structure(FlatType::Erroneous(problem_index)); - - register_with_known_var(subs, destination, rank, pools, content) - } - }; - } - - result -} - -#[inline(always)] -fn roc_result_to_var<'a>( - subs: &mut Subs, - rank: Rank, - pools: &mut Pools, - arena: &'_ bumpalo::Bump, - result_type: &'a Type, - stack: &mut bumpalo::collections::Vec<'_, TypeToVar<'a>>, -) -> Variable { - match result_type { - Type::TagUnion(tags, ext) => { - debug_assert!(ext.is_closed()); - debug_assert!(tags.len() == 2); - - if let [(err, err_args), (ok, ok_args)] = &tags[..] { - debug_assert_eq!(err, &subs.tag_names[0]); - debug_assert_eq!(ok, &subs.tag_names[1]); - - if let ([err_type], [ok_type]) = (err_args.as_slice(), ok_args.as_slice()) { - let err_var = - RegisterVariable::with_stack(subs, rank, pools, arena, err_type, stack); - let ok_var = - RegisterVariable::with_stack(subs, rank, pools, arena, ok_type, stack); - - let start = subs.variables.len() as u32; - let err_slice = SubsSlice::new(start, 1); - let ok_slice = SubsSlice::new(start + 1, 1); - - subs.variables.push(err_var); - subs.variables.push(ok_var); - - let variables = SubsSlice::new(subs.variable_slices.len() as _, 2); - subs.variable_slices.push(err_slice); - subs.variable_slices.push(ok_slice); - - let union_tags = UnionTags::from_slices(Subs::RESULT_TAG_NAMES, variables); - let ext = Variable::EMPTY_TAG_UNION; - - let content = Content::Structure(FlatType::TagUnion(union_tags, ext)); - - return register(subs, rank, pools, content); - } - } - - unreachable!("invalid arguments to Result.Result; canonicalization should catch this!") - } - _ => unreachable!("not a valid type inside a Result.Result alias"), - } -} - -fn insertion_sort_by(arr: &mut [T], mut compare: F) -where - F: FnMut(&T, &T) -> std::cmp::Ordering, -{ - for i in 1..arr.len() { - let val = &arr[i]; - let mut j = i; - let pos = arr[..i] - .binary_search_by(|x| compare(x, val)) - .unwrap_or_else(|pos| pos); - // Swap all elements until specific position. - while j > pos { - arr.swap(j - 1, j); - j -= 1; - } - } -} - -fn sorted_no_duplicates(slice: &[(TagName, T)]) -> bool { - match slice.split_first() { - None => true, - Some(((first, _), rest)) => { - let mut current = first; - - for (next, _) in rest { - if current >= next { - return false; - } else { - current = next; - } - } - - true - } - } -} - -fn sort_and_deduplicate(tag_vars: &mut bumpalo::collections::Vec<(TagName, T)>) { - insertion_sort_by(tag_vars, |(a, _), (b, _)| a.cmp(b)); - - // deduplicate, keeping the right-most occurrence of a tag name - let mut i = 0; - - while i < tag_vars.len() { - match (tag_vars.get(i), tag_vars.get(i + 1)) { - (Some((t1, _)), Some((t2, _))) => { - if t1 == t2 { - tag_vars.remove(i); - } else { - i += 1; - } - } - _ => break, - } - } -} - -/// Find whether the current run of tag names is in the subs.tag_names array already. If so, -/// we take a SubsSlice to the existing tag names, so we don't have to add/clone those tag names -/// and keep subs memory consumption low -fn find_tag_name_run(slice: &[(TagName, T)], subs: &mut Subs) -> Option> { - use std::cmp::Ordering; - - let tag_name = &slice.get(0)?.0; - - let mut result = None; - - // the `SubsSlice` that inserting `slice` into subs would give - let bigger_slice = SubsSlice::new(subs.tag_names.len() as _, slice.len() as _); - - match subs.tag_name_cache.get_mut(tag_name) { - Some(occupied) => { - let subs_slice = *occupied; - - let prefix_slice = SubsSlice::new(subs_slice.start, slice.len() as _); - - if slice.len() == 1 { - return Some(prefix_slice); - } - - match slice.len().cmp(&subs_slice.len()) { - Ordering::Less => { - // we might have a prefix - let tag_names = &subs.tag_names[subs_slice.start as usize..]; - - for (from_subs, (from_slice, _)) in tag_names.iter().zip(slice.iter()) { - if from_subs != from_slice { - return None; - } - } - - result = Some(prefix_slice); - } - Ordering::Equal => { - let tag_names = &subs.tag_names[subs_slice.indices()]; - - for (from_subs, (from_slice, _)) in tag_names.iter().zip(slice.iter()) { - if from_subs != from_slice { - return None; - } - } - - result = Some(subs_slice); - } - Ordering::Greater => { - // switch to the bigger slice that is not inserted yet, but will be soon - *occupied = bigger_slice; - } - } - } - None => { - subs.tag_name_cache.push(tag_name, bigger_slice); - } - } - - result -} - -#[inline(always)] -fn register_tag_arguments<'a>( - subs: &mut Subs, - rank: Rank, - pools: &mut Pools, - arena: &'_ bumpalo::Bump, - stack: &mut bumpalo::collections::Vec<'_, TypeToVar<'a>>, - arguments: &'a [Type], -) -> VariableSubsSlice { - if arguments.is_empty() { - VariableSubsSlice::default() - } else { - let new_variables = VariableSubsSlice::reserve_into_subs(subs, arguments.len()); - let it = new_variables.indices().zip(arguments); - - for (target_index, argument) in it { - let var = RegisterVariable::with_stack(subs, rank, pools, arena, argument, stack); - subs.variables[target_index] = var; - } - - new_variables - } -} - -/// Assumes that the tags are sorted and there are no duplicates! -fn insert_tags_fast_path<'a>( - subs: &mut Subs, - rank: Rank, - pools: &mut Pools, - arena: &'_ bumpalo::Bump, - tags: &'a [(TagName, Vec)], - stack: &mut bumpalo::collections::Vec<'_, TypeToVar<'a>>, -) -> UnionTags { - if let [(TagName(tag_name), arguments)] = tags { - let variable_slice = - register_tag_arguments(subs, rank, pools, arena, stack, arguments.as_slice()); - let new_variable_slices = - SubsSlice::extend_new(&mut subs.variable_slices, [variable_slice]); - - macro_rules! subs_tag_name { - ($tag_name_slice:expr) => { - return UnionTags::from_slices($tag_name_slice, new_variable_slices) - }; - } - - match tag_name.as_str() { - "Ok" => subs_tag_name!(Subs::TAG_NAME_OK.as_slice()), - "Err" => subs_tag_name!(Subs::TAG_NAME_ERR.as_slice()), - "InvalidNumStr" => subs_tag_name!(Subs::TAG_NAME_INVALID_NUM_STR.as_slice()), - "BadUtf8" => subs_tag_name!(Subs::TAG_NAME_BAD_UTF_8.as_slice()), - "OutOfBounds" => subs_tag_name!(Subs::TAG_NAME_OUT_OF_BOUNDS.as_slice()), - _other => {} - } - } - - let new_variable_slices = SubsSlice::reserve_variable_slices(subs, tags.len()); - match find_tag_name_run(tags, subs) { - Some(new_tag_names) => { - let it = (new_variable_slices.indices()).zip(tags); - - for (variable_slice_index, (_, arguments)) in it { - subs.variable_slices[variable_slice_index] = - register_tag_arguments(subs, rank, pools, arena, stack, arguments.as_slice()); - } - - UnionTags::from_slices(new_tag_names, new_variable_slices) - } - None => { - let new_tag_names = SubsSlice::reserve_tag_names(subs, tags.len()); - - let it = (new_variable_slices.indices()) - .zip(new_tag_names.indices()) - .zip(tags); - - for ((variable_slice_index, tag_name_index), (tag_name, arguments)) in it { - subs.variable_slices[variable_slice_index] = - register_tag_arguments(subs, rank, pools, arena, stack, arguments.as_slice()); - - subs.tag_names[tag_name_index] = tag_name.clone(); - } - - UnionTags::from_slices(new_tag_names, new_variable_slices) - } - } -} - -fn insert_tags_slow_path<'a>( - subs: &mut Subs, - rank: Rank, - pools: &mut Pools, - arena: &'_ bumpalo::Bump, - tags: &'a [(TagName, Vec)], - mut tag_vars: bumpalo::collections::Vec<(TagName, VariableSubsSlice)>, - stack: &mut bumpalo::collections::Vec<'_, TypeToVar<'a>>, -) -> UnionTags { - for (tag, tag_argument_types) in tags { - let tag_argument_types: &[Type] = tag_argument_types.as_slice(); - let new_slice = VariableSubsSlice::reserve_into_subs(subs, tag_argument_types.len()); - - for (i, arg) in (new_slice.indices()).zip(tag_argument_types) { - let var = RegisterVariable::with_stack(subs, rank, pools, arena, arg, stack); - subs.variables[i] = var; - } - - tag_vars.push((tag.clone(), new_slice)); - } - - sort_and_deduplicate(&mut tag_vars); - - UnionTags::insert_slices_into_subs(subs, tag_vars) -} - -fn type_to_union_tags<'a>( - subs: &mut Subs, - rank: Rank, - pools: &mut Pools, - arena: &'_ bumpalo::Bump, - tags: &'a [(TagName, Vec)], - ext: &'a TypeExtension, - stack: &mut bumpalo::collections::Vec<'_, TypeToVar<'a>>, -) -> (UnionTags, Variable) { - use bumpalo::collections::Vec; - - let sorted = tags.len() == 1 || sorted_no_duplicates(tags); - - match ext { - TypeExtension::Closed => { - let ext = Variable::EMPTY_TAG_UNION; - - let union_tags = if sorted { - insert_tags_fast_path(subs, rank, pools, arena, tags, stack) - } else { - let tag_vars = Vec::with_capacity_in(tags.len(), arena); - insert_tags_slow_path(subs, rank, pools, arena, tags, tag_vars, stack) - }; - - (union_tags, ext) - } - TypeExtension::Open(ext) => { - let mut tag_vars = Vec::with_capacity_in(tags.len(), arena); - - let temp_ext_var = RegisterVariable::with_stack(subs, rank, pools, arena, ext, stack); - let (it, ext) = roc_types::types::gather_tags_unsorted_iter( - subs, - UnionTags::default(), - temp_ext_var, - ); - - tag_vars.extend(it.map(|(n, v)| (n.clone(), v))); - - let union_tags = if tag_vars.is_empty() && sorted { - insert_tags_fast_path(subs, rank, pools, arena, tags, stack) - } else { - insert_tags_slow_path(subs, rank, pools, arena, tags, tag_vars, stack) - }; - - (union_tags, ext) - } - } -} - -fn create_union_lambda<'a>( - subs: &mut Subs, - rank: Rank, - pools: &mut Pools, - arena: &'_ bumpalo::Bump, - closure: Symbol, - capture_types: &'a [Type], - stack: &mut bumpalo::collections::Vec<'_, TypeToVar<'a>>, -) -> UnionLambdas { - let variable_slice = register_tag_arguments(subs, rank, pools, arena, stack, capture_types); - let new_variable_slices = SubsSlice::extend_new(&mut subs.variable_slices, [variable_slice]); - - let lambda_name_slice = SubsSlice::extend_new(&mut subs.closure_names, [closure]); - - UnionLambdas::from_slices(lambda_name_slice, new_variable_slices) -} - -fn check_for_infinite_type( - subs: &mut Subs, - problems: &mut Vec, - symbol: Symbol, - loc_var: Loc, -) { - let var = loc_var.value; - - while let Err((recursive, _chain)) = subs.occurs(var) { - // try to make a union recursive, see if that helps - match subs.get_content_without_compacting(recursive) { - &Content::Structure(FlatType::TagUnion(tags, ext_var)) => { - subs.mark_tag_union_recursive(recursive, tags, ext_var); - } - &Content::LambdaSet(subs::LambdaSet { - solved, - recursion_var: _, - unspecialized, - }) => { - subs.mark_lambda_set_recursive(recursive, solved, unspecialized); - } - - _other => circular_error(subs, problems, symbol, &loc_var), - } - } -} - -fn circular_error( - subs: &mut Subs, - problems: &mut Vec, - symbol: Symbol, - loc_var: &Loc, -) { - let var = loc_var.value; - let (error_type, _) = subs.var_to_error_type(var); - let problem = TypeError::CircularType(loc_var.region, symbol, error_type); - - subs.set_content(var, Content::Error); - - problems.push(problem); -} - -fn generalize( - subs: &mut Subs, - young_mark: Mark, - visit_mark: Mark, - young_rank: Rank, - pools: &mut Pools, -) { - let young_vars = std::mem::take(pools.get_mut(young_rank)); - let rank_table = pool_to_rank_table(subs, young_mark, young_rank, young_vars); - - // Get the ranks right for each entry. - // Start at low ranks so we only have to pass over the information once. - for (index, table) in rank_table.iter().enumerate() { - for &var in table.iter() { - adjust_rank(subs, young_mark, visit_mark, Rank::from(index), var); - } - } - - let (mut last_pool, all_but_last_pool) = rank_table.split_last(); - - // For variables that have rank lowerer than young_rank, register them in - // the appropriate old pool if they are not redundant. - for vars in all_but_last_pool { - for var in vars { - if !subs.redundant(var) { - let rank = subs.get_rank(var); - - pools.get_mut(rank).push(var); - } - } - } - - // For variables with rank young_rank, if rank < young_rank: register in old pool, - // otherwise generalize - for var in last_pool.drain(..) { - if !subs.redundant(var) { - let desc_rank = subs.get_rank(var); - - if desc_rank < young_rank { - pools.get_mut(desc_rank).push(var); - } else { - subs.set_rank(var, Rank::NONE); - } - } - } - - // re-use the last_vector (which likely has a good capacity for future runs - *pools.get_mut(young_rank) = last_pool; -} - -/// Sort the variables into buckets by rank. -#[inline] -fn pool_to_rank_table( - subs: &mut Subs, - young_mark: Mark, - young_rank: Rank, - mut young_vars: Vec, -) -> Pools { - let mut pools = Pools::new(young_rank.into_usize() + 1); - - // the vast majority of young variables have young_rank - let mut i = 0; - while i < young_vars.len() { - let var = subs.get_root_key(young_vars[i]); - - subs.set_mark_unchecked(var, young_mark); - let rank = subs.get_rank_unchecked(var); - - if rank != young_rank { - debug_assert!(rank.into_usize() < young_rank.into_usize() + 1); - - pools.get_mut(rank).push(var); - - // swap an element in; don't increment i - young_vars.swap_remove(i); - } else { - i += 1; - } - } - - std::mem::swap(pools.get_mut(young_rank), &mut young_vars); - - pools -} - -/// Adjust variable ranks such that ranks never increase as you move deeper. -/// This way the outermost rank is representative of the entire structure. -fn adjust_rank( - subs: &mut Subs, - young_mark: Mark, - visit_mark: Mark, - group_rank: Rank, - var: Variable, -) -> Rank { - let var = subs.get_root_key(var); - - let desc_rank = subs.get_rank_unchecked(var); - let desc_mark = subs.get_mark_unchecked(var); - - if desc_mark == young_mark { - let content = { - let ptr = subs.get_content_unchecked(var) as *const _; - unsafe { &*ptr } - }; - - // Mark the variable as visited before adjusting content, as it may be cyclic. - subs.set_mark_unchecked(var, visit_mark); - - let max_rank = adjust_rank_content(subs, young_mark, visit_mark, group_rank, content); - - subs.set_rank_unchecked(var, max_rank); - subs.set_mark_unchecked(var, visit_mark); - - max_rank - } else if desc_mark == visit_mark { - // we have already visited this variable - // (probably two variables had the same root) - desc_rank - } else { - let min_rank = group_rank.min(desc_rank); - - // TODO from elm-compiler: how can min_rank ever be group_rank? - subs.set_rank_unchecked(var, min_rank); - subs.set_mark_unchecked(var, visit_mark); - - min_rank - } -} - -fn adjust_rank_content( - subs: &mut Subs, - young_mark: Mark, - visit_mark: Mark, - group_rank: Rank, - content: &Content, -) -> Rank { - use roc_types::subs::Content::*; - use roc_types::subs::FlatType::*; - - match content { - FlexVar(_) | RigidVar(_) | FlexAbleVar(_, _) | RigidAbleVar(_, _) | Error => group_rank, - - RecursionVar { .. } => group_rank, - - Structure(flat_type) => { - match flat_type { - Apply(_, args) => { - let mut rank = Rank::toplevel(); - - for var_index in args.into_iter() { - let var = subs[var_index]; - rank = rank.max(adjust_rank(subs, young_mark, visit_mark, group_rank, var)); - } - - rank - } - - Func(arg_vars, closure_var, ret_var) => { - let mut rank = adjust_rank(subs, young_mark, visit_mark, group_rank, *ret_var); - - // TODO investigate further. - // - // My theory is that because the closure_var contains variables already - // contained in the signature only, it does not need to be part of the rank - // calculuation - if true { - rank = rank.max(adjust_rank( - subs, - young_mark, - visit_mark, - group_rank, - *closure_var, - )); - } - - for index in arg_vars.into_iter() { - let var = subs[index]; - rank = rank.max(adjust_rank(subs, young_mark, visit_mark, group_rank, var)); - } - - rank - } - - EmptyRecord => { - // from elm-compiler: THEORY: an empty record never needs to get generalized - Rank::toplevel() - } - - EmptyTagUnion => Rank::toplevel(), - - Record(fields, ext_var) => { - let mut rank = adjust_rank(subs, young_mark, visit_mark, group_rank, *ext_var); - - for index in fields.iter_variables() { - let var = subs[index]; - rank = rank.max(adjust_rank(subs, young_mark, visit_mark, group_rank, var)); - } - - rank - } - - TagUnion(tags, ext_var) => { - let mut rank = adjust_rank(subs, young_mark, visit_mark, group_rank, *ext_var); - // For performance reasons, we only keep one representation of empty tag unions - // in subs. That representation exists at rank 0, which we don't always want to - // reflect the whole tag union as, because doing so may over-generalize free - // type variables. - // Normally this is not a problem because of the loop below that maximizes the - // rank from nested types in the union. But suppose we have the simple tag - // union - // [Z]{} - // there are no nested types in the tags, and the empty tag union is at rank 0, - // so we promote the tag union to rank 0. Now if we introduce the presence - // constraint - // [Z]{} += [S a] - // we'll wind up with [Z, S a]{}, but it will be at rank 0, and "a" will get - // over-generalized. Really, the empty tag union should be introduced at - // whatever current group rank we're at, and so that's how we encode it here. - if *ext_var == Variable::EMPTY_TAG_UNION && rank.is_none() { - rank = group_rank; - } - - for (_, index) in tags.iter_all() { - let slice = subs[index]; - for var_index in slice { - let var = subs[var_index]; - rank = rank - .max(adjust_rank(subs, young_mark, visit_mark, group_rank, var)); - } - } - - rank - } - - FunctionOrTagUnion(_, _, ext_var) => { - adjust_rank(subs, young_mark, visit_mark, group_rank, *ext_var) - } - - RecursiveTagUnion(rec_var, tags, ext_var) => { - let mut rank = adjust_rank(subs, young_mark, visit_mark, group_rank, *ext_var); - - for (_, index) in tags.iter_all() { - let slice = subs[index]; - for var_index in slice { - let var = subs[var_index]; - rank = rank - .max(adjust_rank(subs, young_mark, visit_mark, group_rank, var)); - } - } - - // The recursion var may have a higher rank than the tag union itself, if it is - // erroneous and escapes into a region where it is let-generalized before it is - // constrained back down to the rank it originated from. - // - // For example, see the `recursion_var_specialization_error` reporting test - - // there, we have - // - // Job a : [Job (List (Job a)) a] - // - // job : Job Str - // - // when job is - // Job lst _ -> lst == "" - // - // In this case, `lst` is generalized and has a higher rank for the type - // `(List (Job a)) as a` - notice that only the recursion var `a` is active - // here, not the entire recursive tag union. In the body of this branch, `lst` - // becomes a type error, but the nested recursion var `a` is left untouched, - // because it is nested under the of `lst`, not the surface type that becomes - // an error. - // - // Had this not become a type error, `lst` would then be constrained against - // `job`, and its rank would get pulled back down. So, this can only happen in - // the presence of type errors. - // - // In all other cases, the recursion var has the same rank as the tag union itself - // all types it uses are also in the tags already, so it cannot influence the - // rank. - if cfg!(debug_assertions) - && !matches!( - subs.get_content_without_compacting(*rec_var), - Content::Error | Content::FlexVar(..) - ) - { - let rec_var_rank = - adjust_rank(subs, young_mark, visit_mark, group_rank, *rec_var); - - debug_assert!( - rank >= rec_var_rank, - "rank was {:?} but recursion var <{:?}>{:?} has higher rank {:?}", - rank, - rec_var, - subs.get_content_without_compacting(*rec_var), - rec_var_rank - ); - } - - rank - } - - Erroneous(_) => group_rank, - } - } - - Alias(_, args, real_var, _) => { - let mut rank = Rank::toplevel(); - - for var_index in args.all_variables() { - let var = subs[var_index]; - rank = rank.max(adjust_rank(subs, young_mark, visit_mark, group_rank, var)); - } - - // from elm-compiler: THEORY: anything in the real_var would be Rank::toplevel() - // this theory is not true in Roc! aliases of function types capture the closure var - rank = rank.max(adjust_rank( - subs, young_mark, visit_mark, group_rank, *real_var, - )); - - rank - } - - LambdaSet(subs::LambdaSet { - solved, - recursion_var, - unspecialized, - }) => { - let mut rank = group_rank; - - for (_, index) in solved.iter_all() { - let slice = subs[index]; - for var_index in slice { - let var = subs[var_index]; - rank = rank.max(adjust_rank(subs, young_mark, visit_mark, group_rank, var)); - } - } - - for uls_index in *unspecialized { - let Uls(var, _, _) = subs[uls_index]; - rank = rank.max(adjust_rank(subs, young_mark, visit_mark, group_rank, var)); - } - - if let (true, Some(rec_var)) = (cfg!(debug_assertions), recursion_var.into_variable()) { - // THEORY: unlike the situation for recursion vars under recursive tag unions, - // recursive vars inside lambda sets can't escape into higher let-generalized regions - // because lambda sets aren't user-facing. - // - // So the recursion var should be fully accounted by everything else in the lambda set - // (since it appears in the lambda set), and if the rank is higher, it's either a - // bug or our theory is wrong and indeed they can escape into higher regions. - let rec_var_rank = adjust_rank(subs, young_mark, visit_mark, group_rank, rec_var); - - debug_assert!( - rank >= rec_var_rank, - "rank was {:?} but recursion var <{:?}>{:?} has higher rank {:?}", - rank, - rec_var, - subs.get_content_without_compacting(rec_var), - rec_var_rank - ); - } - - rank - } - - RangedNumber(typ, _) => adjust_rank(subs, young_mark, visit_mark, group_rank, *typ), - } -} - -/// Introduce some variables to Pools at the given rank. -/// Also, set each of their ranks in Subs to be the given rank. -fn introduce(subs: &mut Subs, rank: Rank, pools: &mut Pools, vars: &[Variable]) { - let pool: &mut Vec = pools.get_mut(rank); - - for &var in vars.iter() { - subs.set_rank(var, rank); - } - - pool.extend(vars); -} - -/// Function that converts rigids variables to flex variables -/// this is used during the monomorphization process -pub fn instantiate_rigids(subs: &mut Subs, var: Variable) { - let rank = Rank::NONE; - - instantiate_rigids_help(subs, rank, var); - - // NOTE subs.restore(var) is done at the end of instantiate_rigids_help -} - -fn instantiate_rigids_help(subs: &mut Subs, max_rank: Rank, initial: Variable) { - let mut visited = vec![]; - let mut stack = vec![initial]; - - macro_rules! var_slice { - ($variable_subs_slice:expr) => {{ - let slice = $variable_subs_slice; - &subs.variables[slice.indices()] - }}; - } - - while let Some(var) = stack.pop() { - visited.push(var); - - if subs.get_copy(var).is_some() { - continue; - } - - subs.modify(var, |desc| { - desc.rank = Rank::NONE; - desc.mark = Mark::NONE; - desc.copy = OptVariable::from(var); - }); - - use Content::*; - use FlatType::*; - - match subs.get_content_without_compacting(var) { - RigidVar(name) => { - // what it's all about: convert the rigid var into a flex var - let name = *name; - - // NOTE: we must write to the mutually borrowed `desc` value here - // using `subs.set` does not work (unclear why, really) - // but get_ref_mut approach saves a lookup, so the weirdness is worth it - subs.modify(var, |d| { - *d = Descriptor { - content: FlexVar(Some(name)), - rank: max_rank, - mark: Mark::NONE, - copy: OptVariable::NONE, - } - }) - } - &RigidAbleVar(name, ability) => { - // Same as `RigidVar` above - subs.modify(var, |d| { - *d = Descriptor { - content: FlexAbleVar(Some(name), ability), - rank: max_rank, - mark: Mark::NONE, - copy: OptVariable::NONE, - } - }) - } - FlexVar(_) | FlexAbleVar(_, _) | Error => (), - - RecursionVar { structure, .. } => { - stack.push(*structure); - } - - Structure(flat_type) => match flat_type { - Apply(_, args) => { - stack.extend(var_slice!(*args)); - } - - Func(arg_vars, closure_var, ret_var) => { - let arg_vars = *arg_vars; - let ret_var = *ret_var; - let closure_var = *closure_var; - - stack.extend(var_slice!(arg_vars)); - - stack.push(ret_var); - stack.push(closure_var); - } - - EmptyRecord => (), - EmptyTagUnion => (), - - Record(fields, ext_var) => { - let fields = *fields; - let ext_var = *ext_var; - stack.extend(var_slice!(fields.variables())); - - stack.push(ext_var); - } - TagUnion(tags, ext_var) => { - let tags = *tags; - let ext_var = *ext_var; - - for slice_index in tags.variables() { - let slice = subs.variable_slices[slice_index.index as usize]; - stack.extend(var_slice!(slice)); - } - - stack.push(ext_var); - } - FunctionOrTagUnion(_, _, ext_var) => { - stack.push(*ext_var); - } - - RecursiveTagUnion(rec_var, tags, ext_var) => { - let tags = *tags; - let ext_var = *ext_var; - let rec_var = *rec_var; - - for slice_index in tags.variables() { - let slice = subs.variable_slices[slice_index.index as usize]; - stack.extend(var_slice!(slice)); - } - - stack.push(ext_var); - stack.push(rec_var); - } - - Erroneous(_) => (), - }, - Alias(_, args, var, _) => { - let var = *var; - let args = *args; - - stack.extend(var_slice!(args.all_variables())); - - stack.push(var); - } - LambdaSet(subs::LambdaSet { - solved, - recursion_var, - unspecialized, - }) => { - for slice_index in solved.variables() { - let slice = subs.variable_slices[slice_index.index as usize]; - stack.extend(var_slice!(slice)); - } - - if let Some(rec_var) = recursion_var.into_variable() { - stack.push(rec_var); - } - - for Uls(var, _, _) in subs.get_subs_slice(*unspecialized) { - stack.push(*var); - } - } - &RangedNumber(typ, _) => { - stack.push(typ); - } - } - } - - // we have tracked all visited variables, and can now traverse them - // in one go (without looking at the UnificationTable) and clear the copy field - for var in visited { - subs.modify(var, |descriptor| { - if descriptor.copy.is_some() { - descriptor.rank = Rank::NONE; - descriptor.mark = Mark::NONE; - descriptor.copy = OptVariable::NONE; - } - }); - } -} - -fn deep_copy_var_in( - subs: &mut Subs, - rank: Rank, - pools: &mut Pools, - var: Variable, - arena: &Bump, -) -> Variable { - let mut visited = bumpalo::collections::Vec::with_capacity_in(256, arena); - - let pool = pools.get_mut(rank); - - let var = subs.get_root_key(var); - match deep_copy_var_decision(subs, rank, var) { - ControlFlow::Break(copy) => copy, - ControlFlow::Continue(copy) => { - deep_copy_var_help(subs, rank, pool, &mut visited, var, copy); - - // we have tracked all visited variables, and can now traverse them - // in one go (without looking at the UnificationTable) and clear the copy field - for var in visited { - subs.set_copy_unchecked(var, OptVariable::NONE); - } - - copy - } - } -} - -#[inline] -fn has_trivial_copy(subs: &Subs, root_var: Variable) -> Option { - let existing_copy = subs.get_copy_unchecked(root_var); - - if let Some(copy) = existing_copy.into_variable() { - Some(copy) - } else if subs.get_rank_unchecked(root_var) != Rank::NONE { - Some(root_var) - } else { - None - } -} - -#[inline] -fn deep_copy_var_decision( - subs: &mut Subs, - max_rank: Rank, - var: Variable, -) -> ControlFlow { - let var = subs.get_root_key(var); - if let Some(copy) = has_trivial_copy(subs, var) { - ControlFlow::Break(copy) - } else { - let copy_descriptor = Descriptor { - content: Content::Structure(FlatType::EmptyTagUnion), - rank: max_rank, - mark: Mark::NONE, - copy: OptVariable::NONE, - }; - - let copy = subs.fresh(copy_descriptor); - - // Link the original variable to the new variable. This lets us - // avoid making multiple copies of the variable we are instantiating. - // - // Need to do this before recursively copying to avoid looping. - subs.set_mark_unchecked(var, Mark::NONE); - subs.set_copy_unchecked(var, copy.into()); - - ControlFlow::Continue(copy) - } -} - -fn deep_copy_var_help( - subs: &mut Subs, - max_rank: Rank, - pool: &mut Vec, - visited: &mut bumpalo::collections::Vec<'_, Variable>, - initial_source: Variable, - initial_copy: Variable, -) -> Variable { - use roc_types::subs::Content::*; - use roc_types::subs::FlatType::*; - - struct DeepCopyVarWork { - source: Variable, - copy: Variable, - } - - let initial = DeepCopyVarWork { - source: initial_source, - copy: initial_copy, - }; - let mut stack = vec![initial]; - - macro_rules! work { - ($variable:expr) => {{ - let var = subs.get_root_key($variable); - match deep_copy_var_decision(subs, max_rank, var) { - ControlFlow::Break(copy) => copy, - ControlFlow::Continue(copy) => { - stack.push(DeepCopyVarWork { source: var, copy }); - - copy - } - } - }}; - } - - macro_rules! copy_sequence { - ($length:expr, $variables:expr) => {{ - let new_variables = SubsSlice::reserve_into_subs(subs, $length as _); - for (target_index, var_index) in (new_variables.indices()).zip($variables) { - let var = subs[var_index]; - let copy_var = work!(var); - subs.variables[target_index] = copy_var; - } - - new_variables - }}; - } - - macro_rules! copy_union { - ($tags:expr) => {{ - let new_variable_slices = SubsSlice::reserve_variable_slices(subs, $tags.len()); - - let it = (new_variable_slices.indices()).zip($tags.variables()); - for (target_index, index) in it { - let slice = subs[index]; - - let new_variables = copy_sequence!(slice.len(), slice); - subs.variable_slices[target_index] = new_variables; - } - - UnionLabels::from_slices($tags.labels(), new_variable_slices) - }}; - } - - while let Some(DeepCopyVarWork { source: var, copy }) = stack.pop() { - visited.push(var); - pool.push(copy); - - let content = *subs.get_content_unchecked(var); - - // Now we recursively copy the content of the variable. - // We have already marked the variable as copied, so we - // will not repeat this work or crawl this variable again. - match content { - Structure(flat_type) => { - let new_flat_type = match flat_type { - Apply(symbol, arguments) => { - let new_arguments = copy_sequence!(arguments.len(), arguments); - - Apply(symbol, new_arguments) - } - - Func(arguments, closure_var, ret_var) => { - let new_ret_var = work!(ret_var); - let new_closure_var = work!(closure_var); - - let new_arguments = copy_sequence!(arguments.len(), arguments); - - Func(new_arguments, new_closure_var, new_ret_var) - } - - same @ EmptyRecord | same @ EmptyTagUnion | same @ Erroneous(_) => same, - - Record(fields, ext_var) => { - let record_fields = { - let new_variables = - copy_sequence!(fields.len(), fields.iter_variables()); - - RecordFields { - length: fields.length, - field_names_start: fields.field_names_start, - variables_start: new_variables.start, - field_types_start: fields.field_types_start, - } - }; - - Record(record_fields, work!(ext_var)) - } - - TagUnion(tags, ext_var) => { - let union_tags = copy_union!(tags); - - TagUnion(union_tags, work!(ext_var)) - } - - FunctionOrTagUnion(tag_name, symbol, ext_var) => { - FunctionOrTagUnion(tag_name, symbol, work!(ext_var)) - } - - RecursiveTagUnion(rec_var, tags, ext_var) => { - let union_tags = copy_union!(tags); - - RecursiveTagUnion(work!(rec_var), union_tags, work!(ext_var)) - } - }; - - subs.set_content_unchecked(copy, Structure(new_flat_type)); - } - - FlexVar(_) | FlexAbleVar(_, _) | Error => { - subs.set_content_unchecked(copy, content); - } - - RecursionVar { - opt_name, - structure, - } => { - let content = RecursionVar { - opt_name, - structure: work!(structure), - }; - - subs.set_content_unchecked(copy, content); - } - - RigidVar(name) => { - subs.set_content_unchecked(copy, FlexVar(Some(name))); - } - - RigidAbleVar(name, ability) => { - subs.set_content_unchecked(copy, FlexAbleVar(Some(name), ability)); - } - - Alias(symbol, arguments, real_type_var, kind) => { - let new_variables = - copy_sequence!(arguments.all_variables_len, arguments.all_variables()); - - let new_arguments = AliasVariables { - variables_start: new_variables.start, - ..arguments - }; - - let new_real_type_var = work!(real_type_var); - let new_content = Alias(symbol, new_arguments, new_real_type_var, kind); - - subs.set_content_unchecked(copy, new_content); - } - - LambdaSet(subs::LambdaSet { - solved, - recursion_var, - unspecialized, - }) => { - let lambda_set_var = copy; - - let new_solved = copy_union!(solved); - let new_rec_var = recursion_var.map(|v| work!(v)); - let new_unspecialized = SubsSlice::reserve_uls_slice(subs, unspecialized.len()); - - for (new_uls_index, uls_index) in - (new_unspecialized.into_iter()).zip(unspecialized.into_iter()) - { - let Uls(var, sym, region) = subs[uls_index]; - let new_var = work!(var); - - deep_copy_uls_precondition(subs, var, new_var); - - subs[new_uls_index] = Uls(new_var, sym, region); - - subs.uls_of_var.add(new_var, lambda_set_var); - } - - subs.set_content_unchecked( - lambda_set_var, - LambdaSet(subs::LambdaSet { - solved: new_solved, - recursion_var: new_rec_var, - unspecialized: new_unspecialized, - }), - ); - } - - RangedNumber(typ, range) => { - let new_content = RangedNumber(work!(typ), range); - - subs.set_content_unchecked(copy, new_content); - } - } - } - - initial_copy -} - -#[inline(always)] -fn deep_copy_uls_precondition(subs: &Subs, original_var: Variable, new_var: Variable) { - if cfg!(debug_assertions) { - let content = subs.get_content_without_compacting(original_var); - - debug_assert!( - matches!( - content, - Content::FlexAbleVar(..) | Content::RigidAbleVar(..) - ), - "var in unspecialized lamba set is not bound to an ability, it is {:?}", - roc_types::subs::SubsFmtContent(content, subs) - ); - debug_assert!( - original_var != new_var, - "unspecialized lamba set var was not instantiated" - ); - } -} - -#[inline(always)] -fn register(subs: &mut Subs, rank: Rank, pools: &mut Pools, content: Content) -> Variable { - let descriptor = Descriptor { - content, - rank, - mark: Mark::NONE, - copy: OptVariable::NONE, - }; - - let var = subs.fresh(descriptor); - - pools.get_mut(rank).push(var); - - var -} - -fn register_with_known_var( - subs: &mut Subs, - var: Variable, - rank: Rank, - pools: &mut Pools, - content: Content, -) -> Variable { - let descriptor = Descriptor { - content, - rank, - mark: Mark::NONE, - copy: OptVariable::NONE, - }; - - subs.set(var, descriptor); - - pools.get_mut(rank).push(var); - - var -} diff --git a/compiler/solve/tests/helpers/mod.rs b/compiler/solve/tests/helpers/mod.rs deleted file mode 100644 index 9d7f565100..0000000000 --- a/compiler/solve/tests/helpers/mod.rs +++ /dev/null @@ -1,41 +0,0 @@ -extern crate bumpalo; - -/// Used in the with_larger_debug_stack() function, for tests that otherwise -/// run out of stack space in debug builds (but don't in --release builds) -#[allow(dead_code)] -const EXPANDED_STACK_SIZE: usize = 8 * 1024 * 1024; - -/// Without this, some tests pass in `cargo test --release` but fail without -/// the --release flag because they run out of stack space. This increases -/// stack size for debug builds only, while leaving the stack space at the default -/// amount for release builds. -#[allow(dead_code)] -#[cfg(debug_assertions)] -pub fn with_larger_debug_stack(run_test: F) -where - F: FnOnce(), - F: Send, - F: 'static, -{ - std::thread::Builder::new() - .stack_size(EXPANDED_STACK_SIZE) - .spawn(run_test) - .expect("Error while spawning expanded dev stack size thread") - .join() - .expect("Error while joining expanded dev stack size thread") -} - -/// In --release builds, don't increase the stack size. Run the test normally. -/// This way, we find out if any of our tests are blowing the stack even after -/// optimizations in release builds. -#[allow(dead_code)] -#[cfg(not(debug_assertions))] -#[inline(always)] -pub fn with_larger_debug_stack(run_test: F) -where - F: FnOnce(), - F: Send, - F: 'static, -{ - run_test() -} diff --git a/compiler/solve/tests/solve_expr.rs b/compiler/solve/tests/solve_expr.rs deleted file mode 100644 index e5630ebcc1..0000000000 --- a/compiler/solve/tests/solve_expr.rs +++ /dev/null @@ -1,6820 +0,0 @@ -#[macro_use] -extern crate pretty_assertions; -#[macro_use] -extern crate indoc; - -extern crate bumpalo; - -mod helpers; - -#[cfg(test)] -mod solve_expr { - use crate::helpers::with_larger_debug_stack; - use lazy_static::lazy_static; - use regex::Regex; - use roc_can::traverse::{find_ability_member_and_owning_type_at, find_type_at}; - use roc_load::LoadedModule; - use roc_module::symbol::{Interns, ModuleId}; - use roc_problem::can::Problem; - use roc_region::all::{LineColumn, LineColumnRegion, LineInfo, Region}; - use roc_reporting::report::{can_problem, type_problem, RocDocAllocator}; - use roc_solve::solve::TypeError; - use roc_types::pretty_print::{name_and_print_var, DebugPrint}; - use std::path::PathBuf; - - // HELPERS - - lazy_static! { - static ref RE_TYPE_QUERY: Regex = - Regex::new(r#"(?P\^+)(?:\{-(?P\d+)\})?"#).unwrap(); - } - - #[derive(Debug, Clone, Copy)] - struct TypeQuery(Region); - - fn parse_queries(src: &str) -> Vec { - let line_info = LineInfo::new(src); - let mut queries = vec![]; - for (i, line) in src.lines().enumerate() { - for capture in RE_TYPE_QUERY.captures_iter(line) { - let wher = capture.name("where").unwrap(); - let subtract_col = capture - .name("sub") - .and_then(|m| str::parse(m.as_str()).ok()) - .unwrap_or(0); - let (start, end) = (wher.start() as u32, wher.end() as u32); - let (start, end) = (start - subtract_col, end - subtract_col); - let last_line = i as u32 - 1; - let start_lc = LineColumn { - line: last_line, - column: start, - }; - let end_lc = LineColumn { - line: last_line, - column: end, - }; - let lc_region = LineColumnRegion::new(start_lc, end_lc); - let region = line_info.convert_line_column_region(lc_region); - - queries.push(TypeQuery(region)); - } - } - queries - } - - fn run_load_and_infer(src: &str) -> Result<(LoadedModule, String), std::io::Error> { - use bumpalo::Bump; - use tempfile::tempdir; - - let arena = &Bump::new(); - - let module_src; - let temp; - if src.starts_with("app") { - // this is already a module - module_src = src; - } else { - // this is an expression, promote it to a module - temp = promote_expr_to_module(src); - module_src = &temp; - } - - let exposed_types = Default::default(); - let loaded = { - let dir = tempdir()?; - let filename = PathBuf::from("Test.roc"); - let file_path = dir.path().join(filename); - let result = roc_load::load_and_typecheck_str( - arena, - file_path, - module_src, - dir.path(), - exposed_types, - roc_target::TargetInfo::default_x86_64(), - roc_reporting::report::RenderTarget::Generic, - ); - - dir.close()?; - - result - }; - - let loaded = loaded.expect("failed to load module"); - Ok((loaded, module_src.to_string())) - } - - fn format_problems( - src: &str, - home: ModuleId, - interns: &Interns, - can_problems: Vec, - type_problems: Vec, - ) -> (String, String) { - let filename = PathBuf::from("test.roc"); - let src_lines: Vec<&str> = src.split('\n').collect(); - let lines = LineInfo::new(src); - let alloc = RocDocAllocator::new(&src_lines, home, interns); - - let mut can_reports = vec![]; - let mut type_reports = vec![]; - - for problem in can_problems { - let report = can_problem(&alloc, &lines, filename.clone(), problem.clone()); - can_reports.push(report.pretty(&alloc)); - } - - for problem in type_problems { - if let Some(report) = type_problem(&alloc, &lines, filename.clone(), problem.clone()) { - type_reports.push(report.pretty(&alloc)); - } - } - - let mut can_reports_buf = String::new(); - let mut type_reports_buf = String::new(); - use roc_reporting::report::CiWrite; - alloc - .stack(can_reports) - .1 - .render_raw(70, &mut CiWrite::new(&mut can_reports_buf)) - .unwrap(); - alloc - .stack(type_reports) - .1 - .render_raw(70, &mut CiWrite::new(&mut type_reports_buf)) - .unwrap(); - - (can_reports_buf, type_reports_buf) - } - - fn infer_eq_help(src: &str) -> Result<(String, String, String), std::io::Error> { - let ( - LoadedModule { - module_id: home, - mut can_problems, - mut type_problems, - interns, - mut solved, - mut exposed_to_host, - abilities_store, - .. - }, - src, - ) = run_load_and_infer(src)?; - - let mut can_problems = can_problems.remove(&home).unwrap_or_default(); - let type_problems = type_problems.remove(&home).unwrap_or_default(); - - // Disregard UnusedDef problems, because those are unavoidable when - // returning a function from the test expression. - can_problems.retain(|prob| !matches!(prob, roc_problem::can::Problem::UnusedDef(_, _))); - - let (can_problems, type_problems) = - format_problems(&src, home, &interns, can_problems, type_problems); - - let subs = solved.inner_mut(); - - exposed_to_host.retain(|s, _| !abilities_store.is_specialization_name(*s)); - - debug_assert!(exposed_to_host.len() == 1); - let (_symbol, variable) = exposed_to_host.into_iter().next().unwrap(); - let actual_str = name_and_print_var(variable, subs, home, &interns, DebugPrint::NOTHING); - - Ok((type_problems, can_problems, actual_str)) - } - - fn promote_expr_to_module(src: &str) -> String { - let mut buffer = String::from(indoc!( - r#" - app "test" - imports [] - provides [main] to "./platform" - - main = - "# - )); - - for line in src.lines() { - // indent the body! - buffer.push_str(" "); - buffer.push_str(line); - buffer.push('\n'); - } - - buffer - } - - fn infer_eq(src: &str, expected: &str) { - let (_, can_problems, actual) = infer_eq_help(src).unwrap(); - - assert!( - can_problems.is_empty(), - "Canonicalization problems: {}", - can_problems - ); - - assert_eq!(actual, expected.to_string()); - } - - fn infer_eq_without_problem(src: &str, expected: &str) { - let (type_problems, can_problems, actual) = infer_eq_help(src).unwrap(); - - assert!( - can_problems.is_empty(), - "Canonicalization problems: {}", - can_problems - ); - - if !type_problems.is_empty() { - // fail with an assert, but print the problems normally so rust doesn't try to diff - // an empty vec with the problems. - panic!( - "expected:\n{:?}\ninferred:\n{:?}\nproblems:\n{}", - expected, actual, type_problems, - ); - } - assert_eq!(actual, expected.to_string()); - } - - fn infer_queries_help(src: &str, expected: &[&'static str], print_only_under_alias: bool) { - let ( - LoadedModule { - module_id: home, - mut can_problems, - mut type_problems, - mut declarations_by_id, - mut solved, - interns, - abilities_store, - .. - }, - src, - ) = run_load_and_infer(src).unwrap(); - - let decls = declarations_by_id.remove(&home).unwrap(); - let subs = solved.inner_mut(); - - let can_problems = can_problems.remove(&home).unwrap_or_default(); - let type_problems = type_problems.remove(&home).unwrap_or_default(); - - let (can_problems, type_problems) = - format_problems(&src, home, &interns, can_problems, type_problems); - - assert!( - can_problems.is_empty(), - "Canonicalization problems: {}", - can_problems - ); - assert!(type_problems.is_empty(), "Type problems: {}", type_problems); - - let queries = parse_queries(&src); - assert!(!queries.is_empty(), "No queries provided!"); - - let mut solved_queries = Vec::with_capacity(queries.len()); - for TypeQuery(region) in queries.into_iter() { - let start = region.start().offset; - let end = region.end().offset; - let text = &src[start as usize..end as usize]; - let var = find_type_at(region, &decls) - .unwrap_or_else(|| panic!("No type for {:?} ({:?})!", &text, region)); - - let actual_str = name_and_print_var( - var, - subs, - home, - &interns, - DebugPrint { - print_lambda_sets: true, - print_only_under_alias, - }, - ); - - let elaborated = - match find_ability_member_and_owning_type_at(region, &decls, &abilities_store) { - Some((spec_type, spec_symbol)) => { - format!( - "{}#{}({}) : {}", - spec_type.as_str(&interns), - text, - spec_symbol.ident_id().index(), - actual_str - ) - } - None => { - format!("{} : {}", text, actual_str) - } - }; - - solved_queries.push(elaborated); - } - - assert_eq!(solved_queries, expected) - } - - macro_rules! infer_queries { - ($program:expr, $queries:expr $(,)?) => { - infer_queries_help($program, $queries, false) - }; - ($program:expr, $queries:expr, print_only_under_alias=true $(,)?) => { - infer_queries_help($program, $queries, true) - }; - } - - fn check_inferred_abilities<'a, I>(src: &'a str, expected_specializations: I) - where - I: IntoIterator, - { - let LoadedModule { - module_id: home, - mut can_problems, - mut type_problems, - interns, - abilities_store, - .. - } = run_load_and_infer(src).unwrap().0; - - let can_problems = can_problems.remove(&home).unwrap_or_default(); - let type_problems = type_problems.remove(&home).unwrap_or_default(); - - assert_eq!(can_problems, Vec::new(), "Canonicalization problems: "); - - if !type_problems.is_empty() { - eprintln!("{:?}", type_problems); - panic!(); - } - - let known_specializations = abilities_store.iter_specializations(); - use std::collections::HashSet; - let pretty_specializations = known_specializations - .into_iter() - .map(|((member, typ), _)| { - let member_data = abilities_store.member_def(member).unwrap(); - let member_str = member.as_str(&interns); - let ability_str = member_data.parent_ability.as_str(&interns); - ( - format!("{}:{}", ability_str, member_str), - typ.as_str(&interns), - ) - }) - .collect::>(); - - for (parent, specialization) in expected_specializations.into_iter() { - let has_the_one = pretty_specializations - .iter() - // references are annoying so we do this - .any(|(p, s)| p == parent && s == &specialization); - assert!( - has_the_one, - "{:#?} not in {:#?}", - (parent, specialization), - pretty_specializations, - ); - } - } - - #[test] - fn int_literal() { - infer_eq("5", "Num *"); - } - - #[test] - fn float_literal() { - infer_eq("0.5", "Float *"); - } - - #[test] - fn dec_literal() { - infer_eq( - indoc!( - r#" - val : Dec - val = 1.2 - - val - "# - ), - "Dec", - ); - } - - #[test] - fn string_literal() { - infer_eq( - indoc!( - r#" - "type inference!" - "# - ), - "Str", - ); - } - - #[test] - fn empty_string() { - infer_eq( - indoc!( - r#" - "" - "# - ), - "Str", - ); - } - - #[test] - fn string_starts_with() { - infer_eq_without_problem( - indoc!( - r#" - Str.startsWith - "# - ), - "Str, Str -> Bool", - ); - } - - #[test] - fn string_from_int() { - infer_eq_without_problem( - indoc!( - r#" - Num.toStr - "# - ), - "Num * -> Str", - ); - } - - #[test] - fn string_from_utf8() { - infer_eq_without_problem( - indoc!( - r#" - Str.fromUtf8 - "# - ), - "List U8 -> Result Str [BadUtf8 Utf8ByteProblem Nat]*", - ); - } - - // #[test] - // fn block_string_literal() { - // infer_eq( - // indoc!( - // r#" - // """type - // inference!""" - // "# - // ), - // "Str", - // ); - // } - - // LIST - - #[test] - fn empty_list() { - infer_eq( - indoc!( - r#" - [] - "# - ), - "List *", - ); - } - - #[test] - fn list_of_lists() { - infer_eq( - indoc!( - r#" - [[]] - "# - ), - "List (List *)", - ); - } - - #[test] - fn triple_nested_list() { - infer_eq( - indoc!( - r#" - [[[]]] - "# - ), - "List (List (List *))", - ); - } - - #[test] - fn nested_empty_list() { - infer_eq( - indoc!( - r#" - [[], [[]]] - "# - ), - "List (List (List *))", - ); - } - - #[test] - fn concat_different_types() { - infer_eq( - indoc!( - r#" - empty = [] - one = List.concat [1] empty - str = List.concat ["blah"] empty - - empty - "# - ), - "List *", - ); - } - - #[test] - fn list_of_one_int() { - infer_eq( - indoc!( - r#" - [42] - "# - ), - "List (Num *)", - ); - } - - #[test] - fn triple_nested_int_list() { - infer_eq( - indoc!( - r#" - [[[5]]] - "# - ), - "List (List (List (Num *)))", - ); - } - - #[test] - fn list_of_ints() { - infer_eq( - indoc!( - r#" - [1, 2, 3] - "# - ), - "List (Num *)", - ); - } - - #[test] - fn nested_list_of_ints() { - infer_eq( - indoc!( - r#" - [[1], [2, 3]] - "# - ), - "List (List (Num *))", - ); - } - - #[test] - fn list_of_one_string() { - infer_eq( - indoc!( - r#" - ["cowabunga"] - "# - ), - "List Str", - ); - } - - #[test] - fn triple_nested_string_list() { - infer_eq( - indoc!( - r#" - [[["foo"]]] - "# - ), - "List (List (List Str))", - ); - } - - #[test] - fn list_of_strings() { - infer_eq( - indoc!( - r#" - ["foo", "bar"] - "# - ), - "List Str", - ); - } - - // INTERPOLATED STRING - - #[test] - fn infer_interpolated_string() { - infer_eq( - indoc!( - r#" - whatItIs = "great" - - "type inference is \(whatItIs)!" - "# - ), - "Str", - ); - } - - #[test] - fn infer_interpolated_var() { - infer_eq( - indoc!( - r#" - whatItIs = "great" - - str = "type inference is \(whatItIs)!" - - whatItIs - "# - ), - "Str", - ); - } - - #[test] - fn infer_interpolated_field() { - infer_eq( - indoc!( - r#" - rec = { whatItIs: "great" } - - str = "type inference is \(rec.whatItIs)!" - - rec - "# - ), - "{ whatItIs : Str }", - ); - } - - // LIST MISMATCH - - #[test] - fn mismatch_heterogeneous_list() { - infer_eq( - indoc!( - r#" - ["foo", 5] - "# - ), - "List ", - ); - } - - #[test] - fn mismatch_heterogeneous_nested_list() { - infer_eq( - indoc!( - r#" - [["foo", 5]] - "# - ), - "List (List )", - ); - } - - #[test] - fn mismatch_heterogeneous_nested_empty_list() { - infer_eq( - indoc!( - r#" - [[1], [[]]] - "# - ), - "List ", - ); - } - - // CLOSURE - - #[test] - fn always_return_empty_record() { - infer_eq( - indoc!( - r#" - \_ -> {} - "# - ), - "* -> {}", - ); - } - - #[test] - fn two_arg_return_int() { - infer_eq( - indoc!( - r#" - \_, _ -> 42 - "# - ), - "*, * -> Num *", - ); - } - - #[test] - fn three_arg_return_string() { - infer_eq( - indoc!( - r#" - \_, _, _ -> "test!" - "# - ), - "*, *, * -> Str", - ); - } - - // DEF - - #[test] - fn def_empty_record() { - infer_eq( - indoc!( - r#" - foo = {} - - foo - "# - ), - "{}", - ); - } - - #[test] - fn def_string() { - infer_eq( - indoc!( - r#" - str = "thing" - - str - "# - ), - "Str", - ); - } - - #[test] - fn def_1_arg_closure() { - infer_eq( - indoc!( - r#" - fn = \_ -> {} - - fn - "# - ), - "* -> {}", - ); - } - - #[test] - fn applied_tag() { - infer_eq_without_problem( - indoc!( - r#" - List.map ["a", "b"] \elem -> Foo elem - "# - ), - "List [Foo Str]*", - ) - } - - // Tests (TagUnion, Func) - #[test] - fn applied_tag_function() { - infer_eq_without_problem( - indoc!( - r#" - foo = Foo - - foo "hi" - "# - ), - "[Foo Str]*", - ) - } - - // Tests (TagUnion, Func) - #[test] - fn applied_tag_function_list_map() { - infer_eq_without_problem( - indoc!( - r#" - List.map ["a", "b"] Foo - "# - ), - "List [Foo Str]*", - ) - } - - // Tests (TagUnion, Func) - #[test] - fn applied_tag_function_list() { - infer_eq_without_problem( - indoc!( - r#" - [\x -> Bar x, Foo] - "# - ), - "List (a -> [Bar a, Foo a]*)", - ) - } - - // Tests (Func, TagUnion) - #[test] - fn applied_tag_function_list_other_way() { - infer_eq_without_problem( - indoc!( - r#" - [Foo, \x -> Bar x] - "# - ), - "List (a -> [Bar a, Foo a]*)", - ) - } - - // Tests (Func, TagUnion) - #[test] - fn applied_tag_function_record() { - infer_eq_without_problem( - indoc!( - r#" - foo = Foo - - { - x: [foo, Foo], - y: [foo, \x -> Foo x], - z: [foo, \x,y -> Foo x y] - } - "# - ), - "{ x : List [Foo]*, y : List (a -> [Foo a]*), z : List (b, c -> [Foo b c]*) }", - ) - } - - // Tests (TagUnion, Func) - #[test] - fn applied_tag_function_with_annotation() { - infer_eq_without_problem( - indoc!( - r#" - x : List [Foo I64] - x = List.map [1, 2] Foo - - x - "# - ), - "List [Foo I64]", - ) - } - - #[test] - fn def_2_arg_closure() { - infer_eq( - indoc!( - r#" - func = \_, _ -> 42 - - func - "# - ), - "*, * -> Num *", - ); - } - - #[test] - fn def_3_arg_closure() { - infer_eq( - indoc!( - r#" - f = \_, _, _ -> "test!" - - f - "# - ), - "*, *, * -> Str", - ); - } - - #[test] - fn def_multiple_functions() { - infer_eq( - indoc!( - r#" - a = \_, _, _ -> "test!" - - b = a - - b - "# - ), - "*, *, * -> Str", - ); - } - - #[test] - fn def_multiple_strings() { - infer_eq( - indoc!( - r#" - a = "test!" - - b = a - - b - "# - ), - "Str", - ); - } - - #[test] - fn def_multiple_ints() { - infer_eq( - indoc!( - r#" - c = b - - b = a - - a = 42 - - c - "# - ), - "Num *", - ); - } - - #[test] - fn def_returning_closure() { - infer_eq( - indoc!( - r#" - f = \z -> z - g = \z -> z - - (\x -> - a = f x - b = g x - x - ) - "# - ), - "a -> a", - ); - } - - // CALLING FUNCTIONS - - #[test] - fn call_returns_int() { - infer_eq( - indoc!( - r#" - alwaysFive = \_ -> 5 - - alwaysFive "stuff" - "# - ), - "Num *", - ); - } - - #[test] - fn identity_returns_given_type() { - infer_eq( - indoc!( - r#" - identity = \a -> a - - identity "hi" - "# - ), - "Str", - ); - } - - #[test] - fn identity_infers_principal_type() { - infer_eq( - indoc!( - r#" - identity = \x -> x - - y = identity 5 - - identity - "# - ), - "a -> a", - ); - } - - #[test] - fn identity_works_on_incompatible_types() { - infer_eq( - indoc!( - r#" - identity = \a -> a - - x = identity 5 - y = identity "hi" - - x - "# - ), - "Num *", - ); - } - - #[test] - fn call_returns_list() { - infer_eq( - indoc!( - r#" - enlist = \val -> [val] - - enlist 5 - "# - ), - "List (Num *)", - ); - } - - #[test] - fn indirect_always() { - infer_eq( - indoc!( - r#" - always = \val -> (\_ -> val) - alwaysFoo = always "foo" - - alwaysFoo 42 - "# - ), - "Str", - ); - } - - #[test] - fn pizza_desugar() { - infer_eq( - indoc!( - r#" - 1 |> (\a -> a) - "# - ), - "Num *", - ); - } - - #[test] - fn pizza_desugar_two_arguments() { - infer_eq( - indoc!( - r#" - always2 = \a, _ -> a - - 1 |> always2 "foo" - "# - ), - "Num *", - ); - } - - #[test] - fn anonymous_identity() { - infer_eq( - indoc!( - r#" - (\a -> a) 3.14 - "# - ), - "Float *", - ); - } - - #[test] - fn identity_of_identity() { - infer_eq( - indoc!( - r#" - (\val -> val) (\val -> val) - "# - ), - "a -> a", - ); - } - - #[test] - fn recursive_identity() { - infer_eq( - indoc!( - r#" - identity = \val -> val - - identity identity - "# - ), - "a -> a", - ); - } - - #[test] - fn identity_function() { - infer_eq( - indoc!( - r#" - \val -> val - "# - ), - "a -> a", - ); - } - - #[test] - fn use_apply() { - infer_eq( - indoc!( - r#" - identity = \a -> a - apply = \f, x -> f x - - apply identity 5 - "# - ), - "Num *", - ); - } - - #[test] - fn apply_function() { - infer_eq( - indoc!( - r#" - \f, x -> f x - "# - ), - "(a -> b), a -> b", - ); - } - - // #[test] - // TODO FIXME this should pass, but instead fails to canonicalize - // fn use_flip() { - // infer_eq( - // indoc!( - // r#" - // flip = \f -> (\a b -> f b a) - // neverendingInt = \f int -> f int - // x = neverendingInt (\a -> a) 5 - - // flip neverendingInt - // "# - // ), - // "(Num *, (a -> a)) -> Num *", - // ); - // } - - #[test] - fn flip_function() { - infer_eq( - indoc!( - r#" - \f -> (\a, b -> f b a) - "# - ), - "(a, b -> c) -> (b, a -> c)", - ); - } - - #[test] - fn always_function() { - infer_eq( - indoc!( - r#" - \val -> \_ -> val - "# - ), - "a -> (* -> a)", - ); - } - - #[test] - fn pass_a_function() { - infer_eq( - indoc!( - r#" - \f -> f {} - "# - ), - "({} -> a) -> a", - ); - } - - // OPERATORS - - // #[test] - // fn div_operator() { - // infer_eq( - // indoc!( - // r#" - // \l r -> l / r - // "# - // ), - // "F64, F64 -> F64", - // ); - // } - - // #[test] - // fn basic_float_division() { - // infer_eq( - // indoc!( - // r#" - // 1 / 2 - // "# - // ), - // "F64", - // ); - // } - - // #[test] - // fn basic_int_division() { - // infer_eq( - // indoc!( - // r#" - // 1 // 2 - // "# - // ), - // "Num *", - // ); - // } - - // #[test] - // fn basic_addition() { - // infer_eq( - // indoc!( - // r#" - // 1 + 2 - // "# - // ), - // "Num *", - // ); - // } - - // #[test] - // fn basic_circular_type() { - // infer_eq( - // indoc!( - // r#" - // \x -> x x - // "# - // ), - // "", - // ); - // } - - // #[test] - // fn y_combinator_has_circular_type() { - // assert_eq!( - // infer(indoc!(r#" - // \f -> (\x -> f x x) (\x -> f x x) - // "#)), - // Erroneous(Problem::CircularType) - // ); - // } - - // #[test] - // fn no_higher_ranked_types() { - // // This should error because it can't type of alwaysFive - // infer_eq( - // indoc!( - // r#" - // \always -> [always [], always ""] - // "# - // ), - // "", - // ); - // } - - #[test] - fn always_with_list() { - infer_eq( - indoc!( - r#" - alwaysFive = \_ -> 5 - - [alwaysFive "foo", alwaysFive []] - "# - ), - "List (Num *)", - ); - } - - #[test] - fn if_with_int_literals() { - infer_eq( - indoc!( - r#" - if True then - 42 - else - 24 - "# - ), - "Num *", - ); - } - - #[test] - fn when_with_int_literals() { - infer_eq( - indoc!( - r#" - when 1 is - 1 -> 2 - 3 -> 4 - "# - ), - "Num *", - ); - } - - // RECORDS - - #[test] - fn empty_record() { - infer_eq("{}", "{}"); - } - - #[test] - fn one_field_record() { - infer_eq("{ x: 5 }", "{ x : Num * }"); - } - - #[test] - fn two_field_record() { - infer_eq("{ x: 5, y : 3.14 }", "{ x : Num *, y : Float * }"); - } - - #[test] - fn record_literal_accessor() { - infer_eq("{ x: 5, y : 3.14 }.x", "Num *"); - } - - #[test] - fn record_arg() { - infer_eq("\\rec -> rec.x", "{ x : a }* -> a"); - } - - #[test] - fn record_with_bound_var() { - infer_eq( - indoc!( - r#" - fn = \rec -> - x = rec.x - - rec - - fn - "# - ), - "{ x : a }b -> { x : a }b", - ); - } - - #[test] - fn using_type_signature() { - infer_eq( - indoc!( - r#" - bar : custom -> custom - bar = \x -> x - - bar - "# - ), - "custom -> custom", - ); - } - - #[test] - fn type_signature_without_body() { - infer_eq( - indoc!( - r#" - foo: Str -> {} - - foo "hi" - "# - ), - "{}", - ); - } - - #[test] - fn type_signature_without_body_rigid() { - infer_eq( - indoc!( - r#" - foo : Num * -> custom - - foo 2 - "# - ), - "custom", - ); - } - - #[test] - fn accessor_function() { - infer_eq(".foo", "{ foo : a }* -> a"); - } - - #[test] - fn type_signature_without_body_record() { - infer_eq( - indoc!( - r#" - { x, y } : { x : ({} -> custom), y : {} } - - x - "# - ), - "{} -> custom", - ); - } - - #[test] - fn empty_record_pattern() { - infer_eq( - indoc!( - r#" - # technically, an empty record can be destructured - {} = {} - thunk = \{} -> 42 - - xEmpty = if thunk {} == 42 then { x: {} } else { x: {} } - - when xEmpty is - { x: {} } -> {} - "# - ), - "{}", - ); - } - - #[test] - fn record_type_annotation() { - // check that a closed record remains closed - infer_eq( - indoc!( - r#" - foo : { x : custom } -> custom - foo = \{ x } -> x - - foo - "# - ), - "{ x : custom } -> custom", - ); - } - - #[test] - fn record_update() { - infer_eq( - indoc!( - r#" - user = { year: "foo", name: "Sam" } - - { user & year: "foo" } - "# - ), - "{ name : Str, year : Str }", - ); - } - - #[test] - fn bare_tag() { - infer_eq( - indoc!( - r#" - Foo - "# - ), - "[Foo]*", - ); - } - - #[test] - fn single_tag_pattern() { - infer_eq( - indoc!( - r#" - \Foo -> 42 - "# - ), - "[Foo] -> Num *", - ); - } - - #[test] - fn two_tag_pattern() { - infer_eq( - indoc!( - r#" - \x -> - when x is - True -> 1 - False -> 0 - "# - ), - "[False, True] -> Num *", - ); - } - - #[test] - fn tag_application() { - infer_eq( - indoc!( - r#" - Foo "happy" 2020 - "# - ), - "[Foo Str (Num *)]*", - ); - } - - #[test] - fn record_extraction() { - infer_eq( - indoc!( - r#" - f = \x -> - when x is - { a, b: _ } -> a - - f - "# - ), - "{ a : a, b : * }* -> a", - ); - } - - #[test] - fn record_field_pattern_match_with_guard() { - infer_eq( - indoc!( - r#" - when { x: 5 } is - { x: 4 } -> 4 - "# - ), - "Num *", - ); - } - - #[test] - fn tag_union_pattern_match() { - infer_eq( - indoc!( - r#" - \Foo x -> Foo x - "# - ), - "[Foo a] -> [Foo a]*", - ); - } - - #[test] - fn tag_union_pattern_match_ignored_field() { - infer_eq( - indoc!( - r#" - \Foo x _ -> Foo x "y" - "# - ), - "[Foo a *] -> [Foo a Str]*", - ); - } - - #[test] - fn tag_with_field() { - infer_eq( - indoc!( - r#" - when Foo "blah" is - Foo x -> x - "# - ), - "Str", - ); - } - - #[test] - fn qualified_annotation_num_integer() { - infer_eq( - indoc!( - r#" - int : Num.Num (Num.Integer Num.Signed64) - - int - "# - ), - "I64", - ); - } - #[test] - fn qualified_annotated_num_integer() { - infer_eq( - indoc!( - r#" - int : Num.Num (Num.Integer Num.Signed64) - int = 5 - - int - "# - ), - "I64", - ); - } - #[test] - fn annotation_num_integer() { - infer_eq( - indoc!( - r#" - int : Num (Integer Signed64) - - int - "# - ), - "I64", - ); - } - #[test] - fn annotated_num_integer() { - infer_eq( - indoc!( - r#" - int : Num (Integer Signed64) - int = 5 - - int - "# - ), - "I64", - ); - } - - #[test] - fn qualified_annotation_using_i128() { - infer_eq( - indoc!( - r#" - int : Num.I128 - - int - "# - ), - "I128", - ); - } - #[test] - fn qualified_annotated_using_i128() { - infer_eq( - indoc!( - r#" - int : Num.I128 - int = 5 - - int - "# - ), - "I128", - ); - } - #[test] - fn annotation_using_i128() { - infer_eq( - indoc!( - r#" - int : I128 - - int - "# - ), - "I128", - ); - } - #[test] - fn annotated_using_i128() { - infer_eq( - indoc!( - r#" - int : I128 - int = 5 - - int - "# - ), - "I128", - ); - } - - #[test] - fn qualified_annotation_using_u128() { - infer_eq( - indoc!( - r#" - int : Num.U128 - - int - "# - ), - "U128", - ); - } - #[test] - fn qualified_annotated_using_u128() { - infer_eq( - indoc!( - r#" - int : Num.U128 - int = 5 - - int - "# - ), - "U128", - ); - } - #[test] - fn annotation_using_u128() { - infer_eq( - indoc!( - r#" - int : U128 - - int - "# - ), - "U128", - ); - } - #[test] - fn annotated_using_u128() { - infer_eq( - indoc!( - r#" - int : U128 - int = 5 - - int - "# - ), - "U128", - ); - } - - #[test] - fn qualified_annotation_using_i64() { - infer_eq( - indoc!( - r#" - int : Num.I64 - - int - "# - ), - "I64", - ); - } - #[test] - fn qualified_annotated_using_i64() { - infer_eq( - indoc!( - r#" - int : Num.I64 - int = 5 - - int - "# - ), - "I64", - ); - } - #[test] - fn annotation_using_i64() { - infer_eq( - indoc!( - r#" - int : I64 - - int - "# - ), - "I64", - ); - } - #[test] - fn annotated_using_i64() { - infer_eq( - indoc!( - r#" - int : I64 - int = 5 - - int - "# - ), - "I64", - ); - } - - #[test] - fn qualified_annotation_using_u64() { - infer_eq( - indoc!( - r#" - int : Num.U64 - - int - "# - ), - "U64", - ); - } - #[test] - fn qualified_annotated_using_u64() { - infer_eq( - indoc!( - r#" - int : Num.U64 - int = 5 - - int - "# - ), - "U64", - ); - } - #[test] - fn annotation_using_u64() { - infer_eq( - indoc!( - r#" - int : U64 - - int - "# - ), - "U64", - ); - } - #[test] - fn annotated_using_u64() { - infer_eq( - indoc!( - r#" - int : U64 - int = 5 - - int - "# - ), - "U64", - ); - } - - #[test] - fn qualified_annotation_using_i32() { - infer_eq( - indoc!( - r#" - int : Num.I32 - - int - "# - ), - "I32", - ); - } - #[test] - fn qualified_annotated_using_i32() { - infer_eq( - indoc!( - r#" - int : Num.I32 - int = 5 - - int - "# - ), - "I32", - ); - } - #[test] - fn annotation_using_i32() { - infer_eq( - indoc!( - r#" - int : I32 - - int - "# - ), - "I32", - ); - } - #[test] - fn annotated_using_i32() { - infer_eq( - indoc!( - r#" - int : I32 - int = 5 - - int - "# - ), - "I32", - ); - } - - #[test] - fn qualified_annotation_using_u32() { - infer_eq( - indoc!( - r#" - int : Num.U32 - - int - "# - ), - "U32", - ); - } - #[test] - fn qualified_annotated_using_u32() { - infer_eq( - indoc!( - r#" - int : Num.U32 - int = 5 - - int - "# - ), - "U32", - ); - } - #[test] - fn annotation_using_u32() { - infer_eq( - indoc!( - r#" - int : U32 - - int - "# - ), - "U32", - ); - } - #[test] - fn annotated_using_u32() { - infer_eq( - indoc!( - r#" - int : U32 - int = 5 - - int - "# - ), - "U32", - ); - } - - #[test] - fn qualified_annotation_using_i16() { - infer_eq( - indoc!( - r#" - int : Num.I16 - - int - "# - ), - "I16", - ); - } - #[test] - fn qualified_annotated_using_i16() { - infer_eq( - indoc!( - r#" - int : Num.I16 - int = 5 - - int - "# - ), - "I16", - ); - } - #[test] - fn annotation_using_i16() { - infer_eq( - indoc!( - r#" - int : I16 - - int - "# - ), - "I16", - ); - } - #[test] - fn annotated_using_i16() { - infer_eq( - indoc!( - r#" - int : I16 - int = 5 - - int - "# - ), - "I16", - ); - } - - #[test] - fn qualified_annotation_using_u16() { - infer_eq( - indoc!( - r#" - int : Num.U16 - - int - "# - ), - "U16", - ); - } - #[test] - fn qualified_annotated_using_u16() { - infer_eq( - indoc!( - r#" - int : Num.U16 - int = 5 - - int - "# - ), - "U16", - ); - } - #[test] - fn annotation_using_u16() { - infer_eq( - indoc!( - r#" - int : U16 - - int - "# - ), - "U16", - ); - } - #[test] - fn annotated_using_u16() { - infer_eq( - indoc!( - r#" - int : U16 - int = 5 - - int - "# - ), - "U16", - ); - } - - #[test] - fn qualified_annotation_using_i8() { - infer_eq( - indoc!( - r#" - int : Num.I8 - - int - "# - ), - "I8", - ); - } - #[test] - fn qualified_annotated_using_i8() { - infer_eq( - indoc!( - r#" - int : Num.I8 - int = 5 - - int - "# - ), - "I8", - ); - } - #[test] - fn annotation_using_i8() { - infer_eq( - indoc!( - r#" - int : I8 - - int - "# - ), - "I8", - ); - } - #[test] - fn annotated_using_i8() { - infer_eq( - indoc!( - r#" - int : I8 - int = 5 - - int - "# - ), - "I8", - ); - } - - #[test] - fn qualified_annotation_using_u8() { - infer_eq( - indoc!( - r#" - int : Num.U8 - - int - "# - ), - "U8", - ); - } - #[test] - fn qualified_annotated_using_u8() { - infer_eq( - indoc!( - r#" - int : Num.U8 - int = 5 - - int - "# - ), - "U8", - ); - } - #[test] - fn annotation_using_u8() { - infer_eq( - indoc!( - r#" - int : U8 - - int - "# - ), - "U8", - ); - } - #[test] - fn annotated_using_u8() { - infer_eq( - indoc!( - r#" - int : U8 - int = 5 - - int - "# - ), - "U8", - ); - } - - #[test] - fn qualified_annotation_num_floatingpoint() { - infer_eq( - indoc!( - r#" - float : Num.Num (Num.FloatingPoint Num.Binary64) - - float - "# - ), - "F64", - ); - } - #[test] - fn qualified_annotated_num_floatingpoint() { - infer_eq( - indoc!( - r#" - float : Num.Num (Num.FloatingPoint Num.Binary64) - float = 5.5 - - float - "# - ), - "F64", - ); - } - #[test] - fn annotation_num_floatingpoint() { - infer_eq( - indoc!( - r#" - float : Num (FloatingPoint Binary64) - - float - "# - ), - "F64", - ); - } - #[test] - fn annotated_num_floatingpoint() { - infer_eq( - indoc!( - r#" - float : Num (FloatingPoint Binary64) - float = 5.5 - - float - "# - ), - "F64", - ); - } - - #[test] - fn qualified_annotation_f64() { - infer_eq( - indoc!( - r#" - float : Num.F64 - - float - "# - ), - "F64", - ); - } - #[test] - fn qualified_annotated_f64() { - infer_eq( - indoc!( - r#" - float : Num.F64 - float = 5.5 - - float - "# - ), - "F64", - ); - } - #[test] - fn annotation_f64() { - infer_eq( - indoc!( - r#" - float : F64 - - float - "# - ), - "F64", - ); - } - #[test] - fn annotated_f64() { - infer_eq( - indoc!( - r#" - float : F64 - float = 5.5 - - float - "# - ), - "F64", - ); - } - - #[test] - fn qualified_annotation_f32() { - infer_eq( - indoc!( - r#" - float : Num.F32 - - float - "# - ), - "F32", - ); - } - #[test] - fn qualified_annotated_f32() { - infer_eq( - indoc!( - r#" - float : Num.F32 - float = 5.5 - - float - "# - ), - "F32", - ); - } - #[test] - fn annotation_f32() { - infer_eq( - indoc!( - r#" - float : F32 - - float - "# - ), - "F32", - ); - } - #[test] - fn annotated_f32() { - infer_eq( - indoc!( - r#" - float : F32 - float = 5.5 - - float - "# - ), - "F32", - ); - } - - #[test] - fn fake_result_ok() { - infer_eq( - indoc!( - r#" - Res a e : [Okay a, Error e] - - ok : Res I64 * - ok = Okay 5 - - ok - "# - ), - "Res I64 *", - ); - } - - #[test] - fn fake_result_err() { - infer_eq( - indoc!( - r#" - Res a e : [Okay a, Error e] - - err : Res * Str - err = Error "blah" - - err - "# - ), - "Res * Str", - ); - } - - #[test] - fn basic_result_ok() { - infer_eq( - indoc!( - r#" - ok : Result I64 * - ok = Ok 5 - - ok - "# - ), - "Result I64 *", - ); - } - - #[test] - fn basic_result_err() { - infer_eq( - indoc!( - r#" - err : Result * Str - err = Err "blah" - - err - "# - ), - "Result * Str", - ); - } - - #[test] - fn basic_result_conditional() { - infer_eq( - indoc!( - r#" - ok : Result I64 * - ok = Ok 5 - - err : Result * Str - err = Err "blah" - - if 1 > 0 then - ok - else - err - "# - ), - "Result I64 Str", - ); - } - - // #[test] - // fn annotation_using_num_used() { - // // There was a problem where `I64`, because it is only an annotation - // // wasn't added to the vars_by_symbol. - // infer_eq_without_problem( - // indoc!( - // r#" - // int : I64 - - // p = (\x -> x) int - - // p - // "# - // ), - // "I64", - // ); - // } - - #[test] - fn num_identity() { - infer_eq_without_problem( - indoc!( - r#" - numIdentity : Num.Num a -> Num.Num a - numIdentity = \x -> x - - y = numIdentity 3.14 - - { numIdentity, x : numIdentity 42, y } - "# - ), - "{ numIdentity : Num a -> Num a, x : Num b, y : Float * }", - ); - } - - #[test] - fn when_with_annotation() { - infer_eq_without_problem( - indoc!( - r#" - x : Num.Num (Num.Integer Num.Signed64) - x = - when 2 is - 3 -> 4 - _ -> 5 - - x - "# - ), - "I64", - ); - } - - // TODO add more realistic function when able - #[test] - fn integer_sum() { - infer_eq_without_problem( - indoc!( - r#" - f = \n -> - when n is - 0 -> 0 - _ -> f n - - f - "# - ), - "Num * -> Num *", - ); - } - - #[test] - fn identity_map() { - infer_eq_without_problem( - indoc!( - r#" - map : (a -> b), [Identity a] -> [Identity b] - map = \f, identity -> - when identity is - Identity v -> Identity (f v) - map - "# - ), - "(a -> b), [Identity a] -> [Identity b]", - ); - } - - #[test] - fn to_bit() { - infer_eq_without_problem( - indoc!( - r#" - toBit = \bool -> - when bool is - True -> 1 - False -> 0 - - toBit - "# - ), - "[False, True] -> Num *", - ); - } - - // this test is related to a bug where ext_var would have an incorrect rank. - // This match has duplicate cases, but we ignore that. - #[test] - fn to_bit_record() { - infer_eq( - indoc!( - r#" - foo = \rec -> - when rec is - { x: _ } -> "1" - { y: _ } -> "2" - - foo - "# - ), - "{ x : *, y : * }* -> Str", - ); - } - - #[test] - fn from_bit() { - infer_eq_without_problem( - indoc!( - r#" - fromBit = \int -> - when int is - 0 -> False - _ -> True - - fromBit - "# - ), - "Num * -> [False, True]*", - ); - } - - #[test] - fn result_map_explicit() { - infer_eq_without_problem( - indoc!( - r#" - map : (a -> b), [Err e, Ok a] -> [Err e, Ok b] - map = \f, result -> - when result is - Ok v -> Ok (f v) - Err e -> Err e - - map - "# - ), - "(a -> b), [Err e, Ok a] -> [Err e, Ok b]", - ); - } - - #[test] - fn result_map_alias() { - infer_eq_without_problem( - indoc!( - r#" - Res e a : [Ok a, Err e] - - map : (a -> b), Res e a -> Res e b - map = \f, result -> - when result is - Ok v -> Ok (f v) - Err e -> Err e - - map - "# - ), - "(a -> b), Res e a -> Res e b", - ); - } - - #[test] - fn record_from_load() { - infer_eq_without_problem( - indoc!( - r#" - foo = \{ x } -> x - - foo { x: 5 } - "# - ), - "Num *", - ); - } - - #[test] - fn defs_from_load() { - infer_eq_without_problem( - indoc!( - r#" - alwaysThreePointZero = \_ -> 3.0 - - answer = 42 - - identity = \a -> a - - threePointZero = identity (alwaysThreePointZero {}) - - threePointZero - "# - ), - "Float *", - ); - } - - #[test] - fn use_as_in_signature() { - infer_eq_without_problem( - indoc!( - r#" - foo : Str.Str as Foo -> Foo - foo = \_ -> "foo" - - foo - "# - ), - "Foo -> Foo", - ); - } - - #[test] - fn use_alias_in_let() { - infer_eq_without_problem( - indoc!( - r#" - Foo : Str.Str - - foo : Foo -> Foo - foo = \_ -> "foo" - - foo - "# - ), - "Foo -> Foo", - ); - } - - #[test] - fn use_alias_with_argument_in_let() { - infer_eq_without_problem( - indoc!( - r#" - Foo a : { foo : a } - - v : Foo (Num.Num (Num.Integer Num.Signed64)) - v = { foo: 42 } - - v - "# - ), - "Foo I64", - ); - } - - #[test] - fn identity_alias() { - infer_eq_without_problem( - indoc!( - r#" - Foo a : { foo : a } - - id : Foo a -> Foo a - id = \x -> x - - id - "# - ), - "Foo a -> Foo a", - ); - } - - #[test] - fn linked_list_empty() { - infer_eq_without_problem( - indoc!( - r#" - empty : [Cons a (ConsList a), Nil] as ConsList a - empty = Nil - - empty - "# - ), - "ConsList a", - ); - } - - #[test] - fn linked_list_singleton() { - infer_eq_without_problem( - indoc!( - r#" - singleton : a -> [Cons a (ConsList a), Nil] as ConsList a - singleton = \x -> Cons x Nil - - singleton - "# - ), - "a -> ConsList a", - ); - } - - #[test] - fn peano_length() { - infer_eq_without_problem( - indoc!( - r#" - Peano : [S Peano, Z] - - length : Peano -> Num.Num (Num.Integer Num.Signed64) - length = \peano -> - when peano is - Z -> 0 - S v -> length v - - length - "# - ), - "Peano -> I64", - ); - } - - #[test] - fn peano_map() { - infer_eq_without_problem( - indoc!( - r#" - map : [S Peano, Z] as Peano -> Peano - map = \peano -> - when peano is - Z -> Z - S v -> S (map v) - - map - "# - ), - "Peano -> Peano", - ); - } - - #[test] - fn infer_linked_list_map() { - infer_eq_without_problem( - indoc!( - r#" - map = \f, list -> - when list is - Nil -> Nil - Cons x xs -> - a = f x - b = map f xs - - Cons a b - - map - "# - ), - "(a -> b), [Cons a c, Nil] as c -> [Cons b d, Nil]* as d", - ); - } - - #[test] - fn typecheck_linked_list_map() { - infer_eq_without_problem( - indoc!( - r#" - ConsList a : [Cons a (ConsList a), Nil] - - map : (a -> b), ConsList a -> ConsList b - map = \f, list -> - when list is - Nil -> Nil - Cons x xs -> - Cons (f x) (map f xs) - - map - "# - ), - "(a -> b), ConsList a -> ConsList b", - ); - } - - #[test] - fn mismatch_in_alias_args_gets_reported() { - infer_eq( - indoc!( - r#" - Foo a : a - - r : Foo {} - r = {} - - s : Foo Str.Str - s = "bar" - - when {} is - _ -> s - _ -> r - "# - ), - "", - ); - } - - #[test] - fn mismatch_in_apply_gets_reported() { - infer_eq( - indoc!( - r#" - r : { x : (Num.Num (Num.Integer Signed64)) } - r = { x : 1 } - - s : { left : { x : Num.Num (Num.FloatingPoint Num.Binary64) } } - s = { left: { x : 3.14 } } - - when 0 is - 1 -> s.left - 0 -> r - "# - ), - "", - ); - } - - #[test] - fn mismatch_in_tag_gets_reported() { - infer_eq( - indoc!( - r#" - r : [Ok Str.Str] - r = Ok 1 - - s : { left: [Ok {}] } - s = { left: Ok 3.14 } - - when 0 is - 1 -> s.left - 0 -> r - "# - ), - "", - ); - } - - // TODO As intended, this fails, but it fails with the wrong error! - // - // #[test] - // fn nums() { - // infer_eq_without_problem( - // indoc!( - // r#" - // s : Num * - // s = 3.1 - - // s - // "# - // ), - // "", - // ); - // } - - #[test] - fn peano_map_alias() { - infer_eq( - indoc!( - r#" - app "test" provides [main] to "./platform" - - Peano : [S Peano, Z] - - map : Peano -> Peano - map = \peano -> - when peano is - Z -> Z - S rest -> S (map rest) - - main = - map - "# - ), - "Peano -> Peano", - ); - } - - #[test] - fn unit_alias() { - infer_eq( - indoc!( - r#" - Unit : [Unit] - - unit : Unit - unit = Unit - - unit - "# - ), - "Unit", - ); - } - - #[test] - fn rigid_in_letnonrec() { - infer_eq_without_problem( - indoc!( - r#" - ConsList a : [Cons a (ConsList a), Nil] - - toEmpty : ConsList a -> ConsList a - toEmpty = \_ -> - result : ConsList a - result = Nil - - result - - toEmpty - "# - ), - "ConsList a -> ConsList a", - ); - } - - #[test] - fn rigid_in_letrec_ignored() { - // re-enable when we don't capture local things that don't need to be! - infer_eq_without_problem( - indoc!( - r#" - ConsList a : [Cons a (ConsList a), Nil] - - toEmpty : ConsList a -> ConsList a - toEmpty = \_ -> - result : ConsList a - result = Nil - - toEmpty result - - toEmpty - "# - ), - "ConsList a -> ConsList a", - ); - } - - #[test] - fn rigid_in_letrec() { - infer_eq_without_problem( - indoc!( - r#" - app "test" provides [main] to "./platform" - - ConsList a : [Cons a (ConsList a), Nil] - - toEmpty : ConsList a -> ConsList a - toEmpty = \_ -> - result : ConsList a - result = Nil - - toEmpty result - - main = - toEmpty - "# - ), - "ConsList a -> ConsList a", - ); - } - - #[test] - fn let_record_pattern_with_annotation() { - infer_eq_without_problem( - indoc!( - r#" - { x, y } : { x : Str.Str, y : Num.Num (Num.FloatingPoint Num.Binary64) } - { x, y } = { x : "foo", y : 3.14 } - - x - "# - ), - "Str", - ); - } - - #[test] - fn let_record_pattern_with_annotation_alias() { - infer_eq( - indoc!( - r#" - Foo : { x : Str.Str, y : Num.Num (Num.FloatingPoint Num.Binary64) } - - { x, y } : Foo - { x, y } = { x : "foo", y : 3.14 } - - x - "# - ), - "Str", - ); - } - - #[test] - fn peano_map_infer() { - infer_eq( - indoc!( - r#" - app "test" provides [main] to "./platform" - - map = - \peano -> - when peano is - Z -> Z - S rest -> map rest |> S - - - main = - map - "# - ), - "[S a, Z] as a -> [S b, Z]* as b", - ); - } - - #[test] - fn peano_map_infer_nested() { - infer_eq( - indoc!( - r#" - map = \peano -> - when peano is - Z -> Z - S rest -> - map rest |> S - - - map - "# - ), - "[S a, Z] as a -> [S b, Z]* as b", - ); - } - - #[test] - fn let_record_pattern_with_alias_annotation() { - infer_eq_without_problem( - indoc!( - r#" - Foo : { x : Str.Str, y : Num.Num (Num.FloatingPoint Num.Binary64) } - - { x, y } : Foo - { x, y } = { x : "foo", y : 3.14 } - - x - "# - ), - "Str", - ); - } - - #[test] - fn let_tag_pattern_with_annotation() { - infer_eq_without_problem( - indoc!( - r#" - UserId x : [UserId I64] - UserId x = UserId 42 - - x - "# - ), - "I64", - ); - } - - #[test] - fn typecheck_record_linked_list_map() { - infer_eq_without_problem( - indoc!( - r#" - ConsList q : [Cons { x: q, xs: ConsList q }, Nil] - - map : (a -> b), ConsList a -> ConsList b - map = \f, list -> - when list is - Nil -> Nil - Cons { x, xs } -> - Cons { x: f x, xs : map f xs } - - map - "# - ), - "(a -> b), ConsList a -> ConsList b", - ); - } - - #[test] - fn infer_record_linked_list_map() { - infer_eq_without_problem( - indoc!( - r#" - map = \f, list -> - when list is - Nil -> Nil - Cons { x, xs } -> - Cons { x: f x, xs : map f xs } - - map - "# - ), - "(a -> b), [Cons { x : a, xs : c }*, Nil] as c -> [Cons { x : b, xs : d }, Nil]* as d", - ); - } - - #[test] - fn typecheck_mutually_recursive_tag_union_2() { - infer_eq_without_problem( - indoc!( - r#" - ListA a b : [Cons a (ListB b a), Nil] - ListB a b : [Cons a (ListA b a), Nil] - - ConsList q : [Cons q (ConsList q), Nil] - - toAs : (b -> a), ListA a b -> ConsList a - toAs = \f, lista -> - when lista is - Nil -> Nil - Cons a listb -> - when listb is - Nil -> Nil - Cons b newLista -> - Cons a (Cons (f b) (toAs f newLista)) - - toAs - "# - ), - "(b -> a), ListA a b -> ConsList a", - ); - } - - #[test] - fn typecheck_mutually_recursive_tag_union_listabc() { - infer_eq_without_problem( - indoc!( - r#" - ListA a : [Cons a (ListB a)] - ListB a : [Cons a (ListC a)] - ListC a : [Cons a (ListA a), Nil] - - val : ListC Num.I64 - val = Cons 1 (Cons 2 (Cons 3 Nil)) - - val - "# - ), - "ListC I64", - ); - } - - #[test] - fn infer_mutually_recursive_tag_union() { - infer_eq_without_problem( - indoc!( - r#" - toAs = \f, lista -> - when lista is - Nil -> Nil - Cons a listb -> - when listb is - Nil -> Nil - Cons b newLista -> - Cons a (Cons (f b) (toAs f newLista)) - - toAs - "# - ), - "(a -> b), [Cons c [Cons a d, Nil], Nil] as d -> [Cons c [Cons b e]*, Nil]* as e", - ); - } - - #[test] - fn solve_list_get() { - infer_eq_without_problem( - indoc!( - r#" - List.get ["a"] 0 - "# - ), - "Result Str [OutOfBounds]*", - ); - } - - #[test] - fn type_more_general_than_signature() { - infer_eq_without_problem( - indoc!( - r#" - partition : Nat, Nat, List (Int a) -> [Pair Nat (List (Int a))] - partition = \low, high, initialList -> - when List.get initialList high is - Ok _ -> - Pair 0 [] - - Err _ -> - Pair (low - 1) initialList - - partition - "# - ), - "Nat, Nat, List (Int a) -> [Pair Nat (List (Int a))]", - ); - } - - #[test] - fn quicksort_partition() { - with_larger_debug_stack(|| { - infer_eq_without_problem( - indoc!( - r#" - swap : Nat, Nat, List a -> List a - swap = \i, j, list -> - when Pair (List.get list i) (List.get list j) is - Pair (Ok atI) (Ok atJ) -> - list - |> List.set i atJ - |> List.set j atI - - _ -> - list - - partition : Nat, Nat, List (Int a) -> [Pair Nat (List (Int a))] - partition = \low, high, initialList -> - when List.get initialList high is - Ok pivot -> - go = \i, j, list -> - if j < high then - when List.get list j is - Ok value -> - if value <= pivot then - go (i + 1) (j + 1) (swap (i + 1) j list) - else - go i (j + 1) list - - Err _ -> - Pair i list - else - Pair i list - - when go (low - 1) low initialList is - Pair newI newList -> - Pair (newI + 1) (swap (newI + 1) high newList) - - Err _ -> - Pair (low - 1) initialList - - partition - "# - ), - "Nat, Nat, List (Int a) -> [Pair Nat (List (Int a))]", - ); - }); - } - - #[test] - fn identity_list() { - infer_eq_without_problem( - indoc!( - r#" - idList : List a -> List a - idList = \list -> list - - foo : List I64 -> List I64 - foo = \initialList -> idList initialList - - - foo - "# - ), - "List I64 -> List I64", - ); - } - - #[test] - fn list_get() { - infer_eq_without_problem( - indoc!( - r#" - List.get [10, 9, 8, 7] 1 - "# - ), - "Result (Num *) [OutOfBounds]*", - ); - - infer_eq_without_problem( - indoc!( - r#" - List.get - "# - ), - "List a, Nat -> Result a [OutOfBounds]*", - ); - } - - #[test] - fn use_rigid_twice() { - infer_eq_without_problem( - indoc!( - r#" - id1 : q -> q - id1 = \x -> x - - id2 : q -> q - id2 = \x -> x - - { id1, id2 } - "# - ), - "{ id1 : q -> q, id2 : a -> a }", - ); - } - - #[test] - fn map_insert() { - infer_eq_without_problem( - indoc!( - r#" - Dict.insert - "# - ), - "Dict a b, a, b -> Dict a b", - ); - } - - #[test] - fn num_to_frac() { - infer_eq_without_problem( - indoc!( - r#" - Num.toFrac - "# - ), - "Num * -> Float *", - ); - } - - #[test] - fn pow() { - infer_eq_without_problem( - indoc!( - r#" - Num.pow - "# - ), - "Float a, Float a -> Float a", - ); - } - - #[test] - fn ceiling() { - infer_eq_without_problem( - indoc!( - r#" - Num.ceiling - "# - ), - "Float * -> Int *", - ); - } - - #[test] - fn floor() { - infer_eq_without_problem( - indoc!( - r#" - Num.floor - "# - ), - "Float * -> Int *", - ); - } - - #[test] - fn div() { - infer_eq_without_problem( - indoc!( - r#" - Num.div - "# - ), - "Float a, Float a -> Float a", - ) - } - - #[test] - fn div_checked() { - infer_eq_without_problem( - indoc!( - r#" - Num.divChecked - "# - ), - "Float a, Float a -> Result (Float a) [DivByZero]*", - ) - } - - #[test] - fn div_ceil() { - infer_eq_without_problem( - indoc!( - r#" - Num.divCeil - "# - ), - "Int a, Int a -> Int a", - ); - } - - #[test] - fn div_ceil_checked() { - infer_eq_without_problem( - indoc!( - r#" - Num.divCeilChecked - "# - ), - "Int a, Int a -> Result (Int a) [DivByZero]*", - ); - } - - #[test] - fn div_trunc() { - infer_eq_without_problem( - indoc!( - r#" - Num.divTrunc - "# - ), - "Int a, Int a -> Int a", - ); - } - - #[test] - fn div_trunc_checked() { - infer_eq_without_problem( - indoc!( - r#" - Num.divTruncChecked - "# - ), - "Int a, Int a -> Result (Int a) [DivByZero]*", - ); - } - - #[test] - fn atan() { - infer_eq_without_problem( - indoc!( - r#" - Num.atan - "# - ), - "Float a -> Float a", - ); - } - - #[test] - fn min_i128() { - infer_eq_without_problem( - indoc!( - r#" - Num.minI128 - "# - ), - "I128", - ); - } - - #[test] - fn max_i128() { - infer_eq_without_problem( - indoc!( - r#" - Num.maxI128 - "# - ), - "I128", - ); - } - - #[test] - fn min_i64() { - infer_eq_without_problem( - indoc!( - r#" - Num.minI64 - "# - ), - "I64", - ); - } - - #[test] - fn max_i64() { - infer_eq_without_problem( - indoc!( - r#" - Num.maxI64 - "# - ), - "I64", - ); - } - - #[test] - fn min_u64() { - infer_eq_without_problem( - indoc!( - r#" - Num.minU64 - "# - ), - "U64", - ); - } - - #[test] - fn max_u64() { - infer_eq_without_problem( - indoc!( - r#" - Num.maxU64 - "# - ), - "U64", - ); - } - - #[test] - fn min_i32() { - infer_eq_without_problem( - indoc!( - r#" - Num.minI32 - "# - ), - "I32", - ); - } - - #[test] - fn max_i32() { - infer_eq_without_problem( - indoc!( - r#" - Num.maxI32 - "# - ), - "I32", - ); - } - - #[test] - fn min_u32() { - infer_eq_without_problem( - indoc!( - r#" - Num.minU32 - "# - ), - "U32", - ); - } - - #[test] - fn max_u32() { - infer_eq_without_problem( - indoc!( - r#" - Num.maxU32 - "# - ), - "U32", - ); - } - - #[test] - fn reconstruct_path() { - infer_eq_without_problem( - indoc!( - r#" - reconstructPath : Dict position position, position -> List position - reconstructPath = \cameFrom, goal -> - when Dict.get cameFrom goal is - Err KeyNotFound -> - [] - - Ok next -> - List.append (reconstructPath cameFrom next) goal - - reconstructPath - "# - ), - "Dict position position, position -> List position", - ); - } - - #[test] - fn use_correct_ext_record() { - // Related to a bug solved in 81fbab0b3fe4765bc6948727e603fc2d49590b1c - infer_eq_without_problem( - indoc!( - r#" - f = \r -> - g = r.q - h = r.p - - 42 - - f - "# - ), - "{ p : *, q : * }* -> Num *", - ); - } - - #[test] - fn use_correct_ext_tag_union() { - // related to a bug solved in 08c82bf151a85e62bce02beeed1e14444381069f - infer_eq_without_problem( - indoc!( - r#" - app "test" imports [Result.{ Result }] provides [main] to "./platform" - - boom = \_ -> boom {} - - Model position : { openSet : Set position } - - cheapestOpen : Model position -> Result position [KeyNotFound]* - cheapestOpen = \model -> - - folder = \resSmallestSoFar, position -> - when resSmallestSoFar is - Err _ -> resSmallestSoFar - Ok smallestSoFar -> - if position == smallestSoFar.position then resSmallestSoFar - - else - Ok { position, cost: 0.0 } - - Set.walk model.openSet (Ok { position: boom {}, cost: 0.0 }) folder - |> Result.map (\x -> x.position) - - astar : Model position -> Result position [KeyNotFound]* - astar = \model -> cheapestOpen model - - main = - astar - "# - ), - "Model position -> Result position [KeyNotFound]*", - ); - } - - #[test] - fn when_with_or_pattern_and_guard() { - infer_eq_without_problem( - indoc!( - r#" - \x -> - when x is - 2 | 3 -> 0 - a if a < 20 -> 1 - 3 | 4 if False -> 2 - _ -> 3 - "# - ), - "Num * -> Num *", - ); - } - - #[test] - fn sorting() { - // based on https://github.com/elm/compiler/issues/2057 - // Roc seems to do this correctly, tracking to make sure it stays that way - infer_eq_without_problem( - indoc!( - r#" - sort : ConsList cm -> ConsList cm - sort = - \xs -> - f : cm, cm -> Order - f = \_, _ -> LT - - sortWith f xs - - sortBy : (x -> cmpl), ConsList x -> ConsList x - sortBy = - \_, list -> - cmp : x, x -> Order - cmp = \_, _ -> LT - - sortWith cmp list - - always = \x, _ -> x - - sortWith : (foobar, foobar -> Order), ConsList foobar -> ConsList foobar - sortWith = - \_, list -> - f = \arg -> - g arg - - g = \bs -> - when bs is - bx -> f bx - - always Nil (f list) - - Order : [LT, GT, EQ] - ConsList a : [Nil, Cons a (ConsList a)] - - { x: sortWith, y: sort, z: sortBy } - "# - ), - "{ x : (foobar, foobar -> Order), ConsList foobar -> ConsList foobar, y : ConsList cm -> ConsList cm, z : (x -> cmpl), ConsList x -> ConsList x }" - ); - } - - // Like in elm, this test now fails. Polymorphic recursion (even with an explicit signature) - // yields a type error. - // - // We should at some point investigate why that is. Elm did support polymorphic recursion in - // earlier versions. - // - // #[test] - // fn wrapper() { - // // based on https://github.com/elm/compiler/issues/1964 - // // Roc seems to do this correctly, tracking to make sure it stays that way - // infer_eq_without_problem( - // indoc!( - // r#" - // Type a : [TypeCtor (Type (Wrapper a))] - // - // Wrapper a : [Wrapper a] - // - // Opaque : [Opaque] - // - // encodeType1 : Type a -> Opaque - // encodeType1 = \thing -> - // when thing is - // TypeCtor v0 -> - // encodeType1 v0 - // - // encodeType1 - // "# - // ), - // "Type a -> Opaque", - // ); - // } - - #[test] - fn rigids() { - infer_eq_without_problem( - indoc!( - r#" - f : List a -> List a - f = \input -> - # let-polymorphism at work - x : List b - x = [] - - when List.get input 0 is - Ok val -> List.append x val - Err _ -> input - f - "# - ), - "List a -> List a", - ); - } - - #[cfg(debug_assertions)] - #[test] - #[should_panic] - fn rigid_record_quantification() { - // the ext here is qualified on the outside (because we have rank 1 types, not rank 2). - // That means e.g. `f : { bar : String, foo : I64 } -> Bool }` is a valid argument, but - // that function could not be applied to the `{ foo : I64 }` list. Therefore, this function - // is not allowed. - // - // should hit a debug_assert! in debug mode, and produce a type error in release mode - infer_eq_without_problem( - indoc!( - r#" - test : ({ foo : I64 }ext -> Bool), { foo : I64 } -> Bool - test = \fn, a -> fn a - - test - "# - ), - "should fail", - ); - } - - // OPTIONAL RECORD FIELDS - - #[test] - fn optional_field_unifies_with_missing() { - infer_eq_without_problem( - indoc!( - r#" - negatePoint : { x : I64, y : I64, z ? Num c } -> { x : I64, y : I64, z : Num c } - - negatePoint { x: 1, y: 2 } - "# - ), - "{ x : I64, y : I64, z : Num c }", - ); - } - - #[test] - fn open_optional_field_unifies_with_missing() { - infer_eq_without_problem( - indoc!( - r#" - negatePoint : { x : I64, y : I64, z ? Num c }r -> { x : I64, y : I64, z : Num c }r - - a = negatePoint { x: 1, y: 2 } - b = negatePoint { x: 1, y: 2, blah : "hi" } - - { a, b } - "# - ), - "{ a : { x : I64, y : I64, z : Num c }, b : { blah : Str, x : I64, y : I64, z : Num a } }", - ); - } - - #[test] - fn optional_field_unifies_with_present() { - infer_eq_without_problem( - indoc!( - r#" - negatePoint : { x : Num a, y : Num b, z ? c } -> { x : Num a, y : Num b, z : c } - - negatePoint { x: 1, y: 2.1, z: 0x3 } - "# - ), - "{ x : Num a, y : Float *, z : Int * }", - ); - } - - #[test] - fn open_optional_field_unifies_with_present() { - infer_eq_without_problem( - indoc!( - r#" - negatePoint : { x : Num a, y : Num b, z ? c }r -> { x : Num a, y : Num b, z : c }r - - a = negatePoint { x: 1, y: 2.1 } - b = negatePoint { x: 1, y: 2.1, blah : "hi" } - - { a, b } - "# - ), - "{ a : { x : Num a, y : Float *, z : c }, b : { blah : Str, x : Num b, y : Float *, z : d } }", - ); - } - - #[test] - fn optional_field_function() { - infer_eq_without_problem( - indoc!( - r#" - \{ x, y ? 0 } -> x + y - "# - ), - "{ x : Num a, y ? Num a }* -> Num a", - ); - } - - #[test] - fn optional_field_let() { - infer_eq_without_problem( - indoc!( - r#" - { x, y ? 0 } = { x: 32 } - - x + y - "# - ), - "Num *", - ); - } - - #[test] - fn optional_field_when() { - infer_eq_without_problem( - indoc!( - r#" - \r -> - when r is - { x, y ? 0 } -> x + y - "# - ), - "{ x : Num a, y ? Num a }* -> Num a", - ); - } - - #[test] - fn optional_field_let_with_signature() { - infer_eq_without_problem( - indoc!( - r#" - \rec -> - { x, y } : { x : I64, y ? Bool }* - { x, y ? False } = rec - - { x, y } - "# - ), - "{ x : I64, y ? Bool }* -> { x : I64, y : Bool }", - ); - } - - #[test] - fn list_walk_backwards() { - infer_eq_without_problem( - indoc!( - r#" - List.walkBackwards - "# - ), - "List a, b, (b, a -> b) -> b", - ); - } - - #[test] - fn list_walk_backwards_example() { - infer_eq_without_problem( - indoc!( - r#" - empty : List I64 - empty = - [] - - List.walkBackwards empty 0 (\a, b -> a + b) - "# - ), - "I64", - ); - } - - #[test] - fn list_drop_at() { - infer_eq_without_problem( - indoc!( - r#" - List.dropAt - "# - ), - "List a, Nat -> List a", - ); - } - - #[test] - fn str_trim() { - infer_eq_without_problem( - indoc!( - r#" - Str.trim - "# - ), - "Str -> Str", - ); - } - - #[test] - fn str_trim_left() { - infer_eq_without_problem( - indoc!( - r#" - Str.trimLeft - "# - ), - "Str -> Str", - ); - } - - #[test] - fn list_take_first() { - infer_eq_without_problem( - indoc!( - r#" - List.takeFirst - "# - ), - "List a, Nat -> List a", - ); - } - - #[test] - fn list_take_last() { - infer_eq_without_problem( - indoc!( - r#" - List.takeLast - "# - ), - "List a, Nat -> List a", - ); - } - - #[test] - fn list_sublist() { - infer_eq_without_problem( - indoc!( - r#" - List.sublist - "# - ), - "List a, { len : Nat, start : Nat } -> List a", - ); - } - - #[test] - fn list_split() { - infer_eq_without_problem( - indoc!("List.split"), - "List a, Nat -> { before : List a, others : List a }", - ); - } - - #[test] - fn list_drop_last() { - infer_eq_without_problem( - indoc!( - r#" - List.dropLast - "# - ), - "List a -> List a", - ); - } - - #[test] - fn list_intersperse() { - infer_eq_without_problem( - indoc!( - r#" - List.intersperse - "# - ), - "List a, a -> List a", - ); - } - #[test] - fn function_that_captures_nothing_is_not_captured() { - // we should make sure that a function that doesn't capture anything it not itself captured - // such functions will be lifted to the top-level, and are thus globally available! - infer_eq_without_problem( - indoc!( - r#" - f = \x -> x + 1 - - g = \y -> f y - - g - "# - ), - "Num a -> Num a", - ); - } - - #[test] - fn double_named_rigids() { - infer_eq_without_problem( - indoc!( - r#" - app "test" provides [main] to "./platform" - - - main : List x - main = - empty : List x - empty = [] - - empty - "# - ), - "List x", - ); - } - - #[test] - fn double_tag_application() { - infer_eq_without_problem( - indoc!( - r#" - app "test" provides [main] to "./platform" - - - main = - if 1 == 1 then - Foo (Bar) 1 - else - Foo Bar 1 - "# - ), - "[Foo [Bar]* (Num *)]*", - ); - - infer_eq_without_problem("Foo Bar 1", "[Foo [Bar]* (Num *)]*"); - } - - #[test] - fn double_tag_application_pattern() { - infer_eq_without_problem( - indoc!( - r#" - app "test" provides [main] to "./platform" - - Bar : [Bar] - Foo : [Foo Bar I64, Empty] - - foo : Foo - foo = Foo Bar 1 - - main = - when foo is - Foo Bar 1 -> - Foo Bar 2 - - x -> - x - "# - ), - "[Empty, Foo Bar I64]", - ); - } - - #[test] - fn recursive_function_with_rigid() { - infer_eq_without_problem( - indoc!( - r#" - app "test" provides [main] to "./platform" - - State a : { count : I64, x : a } - - foo : State a -> I64 - foo = \state -> - if state.count == 0 then - 0 - else - 1 + foo { count: state.count - 1, x: state.x } - - main : I64 - main = - foo { count: 3, x: {} } - "# - ), - "I64", - ); - } - - #[test] - fn rbtree_empty() { - infer_eq_without_problem( - indoc!( - r#" - app "test" provides [main] to "./platform" - - # The color of a node. Leaves are considered Black. - NodeColor : [Red, Black] - - RBTree k v : [Node NodeColor k v (RBTree k v) (RBTree k v), Empty] - - # Create an empty dictionary. - empty : RBTree k v - empty = - Empty - - foo : RBTree I64 I64 - foo = empty - - main : RBTree I64 I64 - main = - foo - "# - ), - "RBTree I64 I64", - ); - } - - #[test] - fn rbtree_insert() { - // exposed an issue where pattern variables were not introduced - // at the correct level in the constraint - // - // see 22592eff805511fbe1da63849771ee5f367a6a16 - infer_eq_without_problem( - indoc!( - r#" - app "test" provides [main] to "./platform" - - RBTree k : [Node k (RBTree k), Empty] - - balance : RBTree k -> RBTree k - balance = \left -> - when left is - Node _ Empty -> Empty - - _ -> Empty - - main : RBTree {} - main = - balance Empty - "# - ), - "RBTree {}", - ); - } - - #[test] - fn rbtree_full_remove_min() { - infer_eq_without_problem( - indoc!( - r#" - app "test" provides [main] to "./platform" - - NodeColor : [Red, Black] - - RBTree k v : [Node NodeColor k v (RBTree k v) (RBTree k v), Empty] - - moveRedLeft : RBTree k v -> RBTree k v - moveRedLeft = \dict -> - when dict is - # Node clr k v (Node lClr lK lV lLeft lRight) (Node rClr rK rV ((Node Red rlK rlV rlL rlR) as rLeft) rRight) -> - # Node clr k v (Node lClr lK lV lLeft lRight) (Node rClr rK rV rLeft rRight) -> - Node clr k v (Node _ lK lV lLeft lRight) (Node _ rK rV rLeft rRight) -> - when rLeft is - Node Red rlK rlV rlL rlR -> - Node - Red - rlK - rlV - (Node Black k v (Node Red lK lV lLeft lRight) rlL) - (Node Black rK rV rlR rRight) - - _ -> - when clr is - Black -> - Node - Black - k - v - (Node Red lK lV lLeft lRight) - (Node Red rK rV rLeft rRight) - - Red -> - Node - Black - k - v - (Node Red lK lV lLeft lRight) - (Node Red rK rV rLeft rRight) - - _ -> - dict - - balance : NodeColor, k, v, RBTree k v, RBTree k v -> RBTree k v - balance = \color, key, value, left, right -> - when right is - Node Red rK rV rLeft rRight -> - when left is - Node Red lK lV lLeft lRight -> - Node - Red - key - value - (Node Black lK lV lLeft lRight) - (Node Black rK rV rLeft rRight) - - _ -> - Node color rK rV (Node Red key value left rLeft) rRight - - _ -> - when left is - Node Red lK lV (Node Red llK llV llLeft llRight) lRight -> - Node - Red - lK - lV - (Node Black llK llV llLeft llRight) - (Node Black key value lRight right) - - _ -> - Node color key value left right - - - Key k : Num k - - removeHelpEQGT : Key k, RBTree (Key k) v -> RBTree (Key k) v - removeHelpEQGT = \targetKey, dict -> - when dict is - Node color key value left right -> - if targetKey == key then - when getMin right is - Node _ minKey minValue _ _ -> - balance color minKey minValue left (removeMin right) - - Empty -> - Empty - else - balance color key value left (removeHelp targetKey right) - - Empty -> - Empty - - getMin : RBTree k v -> RBTree k v - getMin = \dict -> - when dict is - # Node _ _ _ ((Node _ _ _ _ _) as left) _ -> - Node _ _ _ left _ -> - when left is - Node _ _ _ _ _ -> getMin left - _ -> dict - - _ -> - dict - - - moveRedRight : RBTree k v -> RBTree k v - moveRedRight = \dict -> - when dict is - Node clr k v (Node lClr lK lV (Node Red llK llV llLeft llRight) lRight) (Node rClr rK rV rLeft rRight) -> - Node - Red - lK - lV - (Node Black llK llV llLeft llRight) - (Node Black k v lRight (Node Red rK rV rLeft rRight)) - - Node clr k v (Node lClr lK lV lLeft lRight) (Node rClr rK rV rLeft rRight) -> - when clr is - Black -> - Node - Black - k - v - (Node Red lK lV lLeft lRight) - (Node Red rK rV rLeft rRight) - - Red -> - Node - Black - k - v - (Node Red lK lV lLeft lRight) - (Node Red rK rV rLeft rRight) - - _ -> - dict - - - removeHelpPrepEQGT : Key k, RBTree (Key k) v, NodeColor, (Key k), v, RBTree (Key k) v, RBTree (Key k) v -> RBTree (Key k) v - removeHelpPrepEQGT = \_, dict, color, key, value, left, right -> - when left is - Node Red lK lV lLeft lRight -> - Node - color - lK - lV - lLeft - (Node Red key value lRight right) - - _ -> - when right is - Node Black _ _ (Node Black _ _ _ _) _ -> - moveRedRight dict - - Node Black _ _ Empty _ -> - moveRedRight dict - - _ -> - dict - - - removeMin : RBTree k v -> RBTree k v - removeMin = \dict -> - when dict is - Node color key value left right -> - when left is - Node lColor _ _ lLeft _ -> - when lColor is - Black -> - when lLeft is - Node Red _ _ _ _ -> - Node color key value (removeMin left) right - - _ -> - when moveRedLeft dict is # here 1 - Node nColor nKey nValue nLeft nRight -> - balance nColor nKey nValue (removeMin nLeft) nRight - - Empty -> - Empty - - _ -> - Node color key value (removeMin left) right - - _ -> - Empty - _ -> - Empty - - removeHelp : Key k, RBTree (Key k) v -> RBTree (Key k) v - removeHelp = \targetKey, dict -> - when dict is - Empty -> - Empty - - Node color key value left right -> - if targetKey < key then - when left is - Node Black _ _ lLeft _ -> - when lLeft is - Node Red _ _ _ _ -> - Node color key value (removeHelp targetKey left) right - - _ -> - when moveRedLeft dict is # here 2 - Node nColor nKey nValue nLeft nRight -> - balance nColor nKey nValue (removeHelp targetKey nLeft) nRight - - Empty -> - Empty - - _ -> - Node color key value (removeHelp targetKey left) right - else - removeHelpEQGT targetKey (removeHelpPrepEQGT targetKey dict color key value left right) - - - main : RBTree I64 I64 - main = - removeHelp 1 Empty - "# - ), - "RBTree I64 I64", - ); - } - - #[test] - fn rbtree_remove_min_1() { - infer_eq_without_problem( - indoc!( - r#" - app "test" provides [main] to "./platform" - - RBTree k : [Node k (RBTree k) (RBTree k), Empty] - - removeHelp : Num k, RBTree (Num k) -> RBTree (Num k) - removeHelp = \targetKey, dict -> - when dict is - Empty -> - Empty - - Node key left right -> - if targetKey < key then - when left is - Node _ lLeft _ -> - when lLeft is - Node _ _ _ -> - Empty - - _ -> Empty - - _ -> - Node key (removeHelp targetKey left) right - else - Empty - - - main : RBTree I64 - main = - removeHelp 1 Empty - "# - ), - "RBTree I64", - ); - } - - #[test] - fn rbtree_foobar() { - infer_eq_without_problem( - indoc!( - r#" - app "test" provides [main] to "./platform" - - NodeColor : [Red, Black] - - RBTree k v : [Node NodeColor k v (RBTree k v) (RBTree k v), Empty] - - removeHelp : Num k, RBTree (Num k) v -> RBTree (Num k) v - removeHelp = \targetKey, dict -> - when dict is - Empty -> - Empty - - Node color key value left right -> - if targetKey < key then - when left is - Node Black _ _ lLeft _ -> - when lLeft is - Node Red _ _ _ _ -> - Node color key value (removeHelp targetKey left) right - - _ -> - when moveRedLeft dict is # here 2 - Node nColor nKey nValue nLeft nRight -> - balance nColor nKey nValue (removeHelp targetKey nLeft) nRight - - Empty -> - Empty - - _ -> - Node color key value (removeHelp targetKey left) right - else - removeHelpEQGT targetKey (removeHelpPrepEQGT targetKey dict color key value left right) - - Key k : Num k - - balance : NodeColor, k, v, RBTree k v, RBTree k v -> RBTree k v - - moveRedLeft : RBTree k v -> RBTree k v - - removeHelpPrepEQGT : Key k, RBTree (Key k) v, NodeColor, (Key k), v, RBTree (Key k) v, RBTree (Key k) v -> RBTree (Key k) v - - removeHelpEQGT : Key k, RBTree (Key k) v -> RBTree (Key k) v - removeHelpEQGT = \targetKey, dict -> - when dict is - Node color key value left right -> - if targetKey == key then - when getMin right is - Node _ minKey minValue _ _ -> - balance color minKey minValue left (removeMin right) - - Empty -> - Empty - else - balance color key value left (removeHelp targetKey right) - - Empty -> - Empty - - getMin : RBTree k v -> RBTree k v - - removeMin : RBTree k v -> RBTree k v - - main : RBTree I64 I64 - main = - removeHelp 1 Empty - "# - ), - "RBTree I64 I64", - ); - } - - #[test] - fn quicksort_partition_help() { - infer_eq_without_problem( - indoc!( - r#" - app "test" provides [partitionHelp] to "./platform" - - swap : Nat, Nat, List a -> List a - swap = \i, j, list -> - when Pair (List.get list i) (List.get list j) is - Pair (Ok atI) (Ok atJ) -> - list - |> List.set i atJ - |> List.set j atI - - _ -> - [] - - partitionHelp : Nat, Nat, List (Num a), Nat, (Num a) -> [Pair Nat (List (Num a))] - partitionHelp = \i, j, list, high, pivot -> - if j < high then - when List.get list j is - Ok value -> - if value <= pivot then - partitionHelp (i + 1) (j + 1) (swap (i + 1) j list) high pivot - else - partitionHelp i (j + 1) list high pivot - - Err _ -> - Pair i list - else - Pair i list - "# - ), - "Nat, Nat, List (Num a), Nat, Num a -> [Pair Nat (List (Num a))]", - ); - } - - #[test] - fn rbtree_old_balance_simplified() { - infer_eq_without_problem( - indoc!( - r#" - app "test" provides [main] to "./platform" - - RBTree k : [Node k (RBTree k) (RBTree k), Empty] - - balance : k, RBTree k -> RBTree k - balance = \key, left -> - Node key left Empty - - main : RBTree I64 - main = - balance 0 Empty - "# - ), - "RBTree I64", - ); - } - - #[test] - fn rbtree_balance_simplified() { - infer_eq_without_problem( - indoc!( - r#" - app "test" provides [main] to "./platform" - - RBTree k : [Node k (RBTree k) (RBTree k), Empty] - - node = \x,y,z -> Node x y z - - balance : k, RBTree k -> RBTree k - balance = \key, left -> - node key left Empty - - main : RBTree I64 - main = - balance 0 Empty - "# - ), - "RBTree I64", - ); - } - - #[test] - fn rbtree_balance() { - infer_eq_without_problem( - indoc!( - r#" - app "test" provides [main] to "./platform" - - NodeColor : [Red, Black] - - RBTree k v : [Node NodeColor k v (RBTree k v) (RBTree k v), Empty] - - balance : NodeColor, k, v, RBTree k v, RBTree k v -> RBTree k v - balance = \color, key, value, left, right -> - when right is - Node Red rK rV rLeft rRight -> - when left is - Node Red lK lV lLeft lRight -> - Node - Red - key - value - (Node Black lK lV lLeft lRight) - (Node Black rK rV rLeft rRight) - - _ -> - Node color rK rV (Node Red key value left rLeft) rRight - - _ -> - when left is - Node Red lK lV (Node Red llK llV llLeft llRight) lRight -> - Node - Red - lK - lV - (Node Black llK llV llLeft llRight) - (Node Black key value lRight right) - - _ -> - Node color key value left right - - main : RBTree I64 I64 - main = - balance Red 0 0 Empty Empty - "# - ), - "RBTree I64 I64", - ); - } - - #[test] - fn pattern_rigid_problem() { - infer_eq_without_problem( - indoc!( - r#" - app "test" provides [main] to "./platform" - - RBTree k : [Node k (RBTree k) (RBTree k), Empty] - - balance : k, RBTree k -> RBTree k - balance = \key, left -> - when left is - Node _ _ lRight -> - Node key lRight Empty - - _ -> - Empty - - - main : RBTree I64 - main = - balance 0 Empty - "# - ), - "RBTree I64", - ); - } - - #[test] - fn expr_to_str() { - infer_eq_without_problem( - indoc!( - r#" - app "test" provides [main] to "./platform" - - Expr : [Add Expr Expr, Val I64, Var I64] - - printExpr : Expr -> Str - printExpr = \e -> - when e is - Add a b -> - "Add (" - |> Str.concat (printExpr a) - |> Str.concat ") (" - |> Str.concat (printExpr b) - |> Str.concat ")" - Val v -> Num.toStr v - Var v -> "Var " |> Str.concat (Num.toStr v) - - main : Str - main = printExpr (Var 3) - "# - ), - "Str", - ); - } - - #[test] - fn int_type_let_polymorphism() { - infer_eq_without_problem( - indoc!( - r#" - app "test" provides [main] to "./platform" - - x = 4 - - f : U8 -> U32 - f = \z -> Num.intCast z - - y = f x - - main = - x - "# - ), - "Num *", - ); - } - - #[test] - fn rigid_type_variable_problem() { - // see https://github.com/rtfeldman/roc/issues/1162 - infer_eq_without_problem( - indoc!( - r#" - app "test" provides [main] to "./platform" - - RBTree k : [Node k (RBTree k) (RBTree k), Empty] - - balance : a, RBTree a -> RBTree a - balance = \key, left -> - when left is - Node _ _ lRight -> - Node key lRight Empty - - _ -> - Empty - - - main : RBTree {} - main = - balance {} Empty - "# - ), - "RBTree {}", - ); - } - - #[test] - fn inference_var_inside_arrow() { - infer_eq_without_problem( - indoc!( - r#" - id : _ -> _ - id = \x -> x - id - "# - ), - "a -> a", - ) - } - - #[test] - fn inference_var_inside_ctor() { - infer_eq_without_problem( - indoc!( - r#" - canIGo : _ -> Result.Result _ _ - canIGo = \color -> - when color is - "green" -> Ok "go!" - "yellow" -> Err (SlowIt "whoa, let's slow down!") - "red" -> Err (StopIt "absolutely not") - _ -> Err (UnknownColor "this is a weird stoplight") - canIGo - "# - ), - "Str -> Result Str [SlowIt Str, StopIt Str, UnknownColor Str]*", - ) - } - - #[test] - fn inference_var_inside_ctor_linked() { - infer_eq_without_problem( - indoc!( - r#" - swapRcd: {x: _, y: _} -> {x: _, y: _} - swapRcd = \{x, y} -> {x: y, y: x} - swapRcd - "# - ), - "{ x : a, y : b } -> { x : b, y : a }", - ) - } - - #[test] - fn inference_var_link_with_rigid() { - infer_eq_without_problem( - indoc!( - r#" - swapRcd: {x: tx, y: ty} -> {x: _, y: _} - swapRcd = \{x, y} -> {x: y, y: x} - swapRcd - "# - ), - "{ x : tx, y : ty } -> { x : ty, y : tx }", - ) - } - - #[test] - fn inference_var_inside_tag_ctor() { - infer_eq_without_problem( - indoc!( - r#" - badComics: Bool -> [CowTools _, Thagomizer _] - badComics = \c -> - when c is - True -> CowTools "The Far Side" - False -> Thagomizer "The Far Side" - badComics - "# - ), - "Bool -> [CowTools Str, Thagomizer Str]", - ) - } - - #[test] - fn inference_var_tag_union_ext() { - // TODO: we should really be inferring [Blue, Orange]a -> [Lavender, Peach]a here. - // See https://github.com/rtfeldman/roc/issues/2053 - infer_eq_without_problem( - indoc!( - r#" - pastelize: _ -> [Lavender, Peach]_ - pastelize = \color -> - when color is - Blue -> Lavender - Orange -> Peach - col -> col - pastelize - "# - ), - "[Blue, Lavender, Orange, Peach]a -> [Blue, Lavender, Orange, Peach]a", - ) - } - - #[test] - fn inference_var_rcd_union_ext() { - infer_eq_without_problem( - indoc!( - r#" - setRocEmail : _ -> { name: Str, email: Str }_ - setRocEmail = \person -> - { person & email: "\(person.name)@roclang.com" } - setRocEmail - "# - ), - "{ email : Str, name : Str }a -> { email : Str, name : Str }a", - ) - } - - #[test] - fn issue_2217() { - infer_eq_without_problem( - indoc!( - r#" - LinkedList elem : [Empty, Prepend (LinkedList elem) elem] - - fromList : List elem -> LinkedList elem - fromList = \elems -> List.walk elems Empty Prepend - - fromList - "# - ), - "List elem -> LinkedList elem", - ) - } - - #[test] - fn issue_2217_inlined() { - infer_eq_without_problem( - indoc!( - r#" - fromList : List elem -> [Empty, Prepend (LinkedList elem) elem] as LinkedList elem - fromList = \elems -> List.walk elems Empty Prepend - - fromList - "# - ), - "List elem -> LinkedList elem", - ) - } - - #[test] - fn infer_union_input_position1() { - infer_eq_without_problem( - indoc!( - r#" - \tag -> - when tag is - A -> X - B -> Y - "# - ), - "[A, B] -> [X, Y]*", - ) - } - - #[test] - fn infer_union_input_position2() { - infer_eq_without_problem( - indoc!( - r#" - \tag -> - when tag is - A -> X - B -> Y - _ -> Z - "# - ), - "[A, B]* -> [X, Y, Z]*", - ) - } - - #[test] - fn infer_union_input_position3() { - infer_eq_without_problem( - indoc!( - r#" - \tag -> - when tag is - A M -> X - A N -> Y - "# - ), - "[A [M, N]] -> [X, Y]*", - ) - } - - #[test] - fn infer_union_input_position4() { - infer_eq_without_problem( - indoc!( - r#" - \tag -> - when tag is - A M -> X - A N -> Y - A _ -> Z - "# - ), - "[A [M, N]*] -> [X, Y, Z]*", - ) - } - - #[test] - fn infer_union_input_position5() { - infer_eq_without_problem( - indoc!( - r#" - \tag -> - when tag is - A (M J) -> X - A (N K) -> X - "# - ), - "[A [M [J], N [K]]] -> [X]*", - ) - } - - #[test] - fn infer_union_input_position6() { - infer_eq_without_problem( - indoc!( - r#" - \tag -> - when tag is - A M -> X - B -> X - A N -> X - "# - ), - "[A [M, N], B] -> [X]*", - ) - } - - #[test] - fn infer_union_input_position7() { - infer_eq_without_problem( - indoc!( - r#" - \tag -> - when tag is - A -> X - t -> t - "# - ), - // TODO: we could be a bit smarter by subtracting "A" as a possible - // tag in the union known by t, which would yield the principal type - // [A,]a -> [X]a - "[A, X]a -> [A, X]a", - ) - } - - #[test] - fn infer_union_input_position8() { - infer_eq_without_problem( - indoc!( - r#" - \opt -> - when opt is - Some ({tag: A}) -> 1 - Some ({tag: B}) -> 1 - None -> 0 - "# - ), - "[None, Some { tag : [A, B] }*] -> Num *", - ) - } - - #[test] - fn infer_union_input_position9() { - infer_eq_without_problem( - indoc!( - r#" - opt : [Some Str, None] - opt = Some "" - rcd = { opt } - - when rcd is - { opt: Some s } -> s - { opt: None } -> "?" - "# - ), - "Str", - ) - } - - #[test] - fn infer_union_input_position10() { - infer_eq_without_problem( - indoc!( - r#" - \r -> - when r is - { x: Blue, y ? 3 } -> y - { x: Red, y ? 5 } -> y - "# - ), - "{ x : [Blue, Red], y ? Num a }* -> Num a", - ) - } - - #[test] - // Issue #2299 - fn infer_union_argument_position() { - infer_eq_without_problem( - indoc!( - r#" - \UserId id -> id + 1 - "# - ), - "[UserId (Num a)] -> Num a", - ) - } - - #[test] - fn infer_union_def_position() { - infer_eq_without_problem( - indoc!( - r#" - \email -> - Email str = email - Str.isEmpty str - "# - ), - "[Email Str] -> Bool", - ) - } - - #[test] - fn numeric_literal_suffixes() { - infer_eq_without_problem( - indoc!( - r#" - { - u8: 123u8, - u16: 123u16, - u32: 123u32, - u64: 123u64, - u128: 123u128, - - i8: 123i8, - i16: 123i16, - i32: 123i32, - i64: 123i64, - i128: 123i128, - - nat: 123nat, - - bu8: 0b11u8, - bu16: 0b11u16, - bu32: 0b11u32, - bu64: 0b11u64, - bu128: 0b11u128, - - bi8: 0b11i8, - bi16: 0b11i16, - bi32: 0b11i32, - bi64: 0b11i64, - bi128: 0b11i128, - - bnat: 0b11nat, - - dec: 123.0dec, - f32: 123.0f32, - f64: 123.0f64, - - fdec: 123dec, - ff32: 123f32, - ff64: 123f64, - } - "# - ), - r#"{ bi128 : I128, bi16 : I16, bi32 : I32, bi64 : I64, bi8 : I8, bnat : Nat, bu128 : U128, bu16 : U16, bu32 : U32, bu64 : U64, bu8 : U8, dec : Dec, f32 : F32, f64 : F64, fdec : Dec, ff32 : F32, ff64 : F64, i128 : I128, i16 : I16, i32 : I32, i64 : I64, i8 : I8, nat : Nat, u128 : U128, u16 : U16, u32 : U32, u64 : U64, u8 : U8 }"#, - ) - } - - #[test] - fn numeric_literal_suffixes_in_pattern() { - infer_eq_without_problem( - indoc!( - r#" - { - u8: (\n -> - when n is - 123u8 -> n - _ -> n), - u16: (\n -> - when n is - 123u16 -> n - _ -> n), - u32: (\n -> - when n is - 123u32 -> n - _ -> n), - u64: (\n -> - when n is - 123u64 -> n - _ -> n), - u128: (\n -> - when n is - 123u128 -> n - _ -> n), - - i8: (\n -> - when n is - 123i8 -> n - _ -> n), - i16: (\n -> - when n is - 123i16 -> n - _ -> n), - i32: (\n -> - when n is - 123i32 -> n - _ -> n), - i64: (\n -> - when n is - 123i64 -> n - _ -> n), - i128: (\n -> - when n is - 123i128 -> n - _ -> n), - - nat: (\n -> - when n is - 123nat -> n - _ -> n), - - bu8: (\n -> - when n is - 0b11u8 -> n - _ -> n), - bu16: (\n -> - when n is - 0b11u16 -> n - _ -> n), - bu32: (\n -> - when n is - 0b11u32 -> n - _ -> n), - bu64: (\n -> - when n is - 0b11u64 -> n - _ -> n), - bu128: (\n -> - when n is - 0b11u128 -> n - _ -> n), - - bi8: (\n -> - when n is - 0b11i8 -> n - _ -> n), - bi16: (\n -> - when n is - 0b11i16 -> n - _ -> n), - bi32: (\n -> - when n is - 0b11i32 -> n - _ -> n), - bi64: (\n -> - when n is - 0b11i64 -> n - _ -> n), - bi128: (\n -> - when n is - 0b11i128 -> n - _ -> n), - - bnat: (\n -> - when n is - 0b11nat -> n - _ -> n), - - dec: (\n -> - when n is - 123.0dec -> n - _ -> n), - f32: (\n -> - when n is - 123.0f32 -> n - _ -> n), - f64: (\n -> - when n is - 123.0f64 -> n - _ -> n), - - fdec: (\n -> - when n is - 123dec -> n - _ -> n), - ff32: (\n -> - when n is - 123f32 -> n - _ -> n), - ff64: (\n -> - when n is - 123f64 -> n - _ -> n), - } - "# - ), - r#"{ bi128 : I128 -> I128, bi16 : I16 -> I16, bi32 : I32 -> I32, bi64 : I64 -> I64, bi8 : I8 -> I8, bnat : Nat -> Nat, bu128 : U128 -> U128, bu16 : U16 -> U16, bu32 : U32 -> U32, bu64 : U64 -> U64, bu8 : U8 -> U8, dec : Dec -> Dec, f32 : F32 -> F32, f64 : F64 -> F64, fdec : Dec -> Dec, ff32 : F32 -> F32, ff64 : F64 -> F64, i128 : I128 -> I128, i16 : I16 -> I16, i32 : I32 -> I32, i64 : I64 -> I64, i8 : I8 -> I8, nat : Nat -> Nat, u128 : U128 -> U128, u16 : U16 -> U16, u32 : U32 -> U32, u64 : U64 -> U64, u8 : U8 -> U8 }"#, - ) - } - - #[test] - fn issue_2458() { - infer_eq_without_problem( - indoc!( - r#" - Foo a : [Blah (Result (Bar a) { val: a })] - Bar a : Foo a - - v : Bar U8 - v = Blah (Ok (Blah (Err { val: 1 }))) - - v - "# - ), - "Bar U8", - ) - } - - #[test] - fn issue_2458_swapped_order() { - infer_eq_without_problem( - indoc!( - r#" - Bar a : Foo a - Foo a : [Blah (Result (Bar a) { val: a })] - - v : Bar U8 - v = Blah (Ok (Blah (Err { val: 1 }))) - - v - "# - ), - "Bar U8", - ) - } - - // https://github.com/rtfeldman/roc/issues/2379 - #[test] - fn copy_vars_referencing_copied_vars() { - infer_eq_without_problem( - indoc!( - r#" - Job : [Job [Command] (List Job)] - - job : Job - - job - "# - ), - "Job", - ) - } - - #[test] - fn generalize_and_specialize_recursion_var() { - infer_eq_without_problem( - indoc!( - r#" - Job a : [Job (List (Job a)) a] - - job : Job Str - - when job is - Job lst s -> P lst s - "# - ), - "[P (List [Job (List a) Str] as a) Str]*", - ) - } - - #[test] - fn to_int() { - infer_eq_without_problem( - indoc!( - r#" - { - toI8: Num.toI8, - toI16: Num.toI16, - toI32: Num.toI32, - toI64: Num.toI64, - toI128: Num.toI128, - toNat: Num.toNat, - toU8: Num.toU8, - toU16: Num.toU16, - toU32: Num.toU32, - toU64: Num.toU64, - toU128: Num.toU128, - } - "# - ), - r#"{ toI128 : Int * -> I128, toI16 : Int * -> I16, toI32 : Int * -> I32, toI64 : Int * -> I64, toI8 : Int * -> I8, toNat : Int * -> Nat, toU128 : Int * -> U128, toU16 : Int * -> U16, toU32 : Int * -> U32, toU64 : Int * -> U64, toU8 : Int * -> U8 }"#, - ) - } - - #[test] - fn to_float() { - infer_eq_without_problem( - indoc!( - r#" - { - toF32: Num.toF32, - toF64: Num.toF64, - } - "# - ), - r#"{ toF32 : Num * -> F32, toF64 : Num * -> F64 }"#, - ) - } - - #[test] - fn opaque_wrap_infer() { - infer_eq_without_problem( - indoc!( - r#" - Age := U32 - - @Age 21 - "# - ), - r#"Age"#, - ) - } - - #[test] - fn opaque_wrap_check() { - infer_eq_without_problem( - indoc!( - r#" - Age := U32 - - a : Age - a = @Age 21 - - a - "# - ), - r#"Age"#, - ) - } - - #[test] - fn opaque_wrap_polymorphic_infer() { - infer_eq_without_problem( - indoc!( - r#" - Id n := [Id U32 n] - - @Id (Id 21 "sasha") - "# - ), - r#"Id Str"#, - ) - } - - #[test] - fn opaque_wrap_polymorphic_check() { - infer_eq_without_problem( - indoc!( - r#" - Id n := [Id U32 n] - - a : Id Str - a = @Id (Id 21 "sasha") - - a - "# - ), - r#"Id Str"#, - ) - } - - #[test] - fn opaque_wrap_polymorphic_from_multiple_branches_infer() { - infer_eq_without_problem( - indoc!( - r#" - Id n := [Id U32 n] - condition : Bool - - if condition - then @Id (Id 21 (Y "sasha")) - else @Id (Id 21 (Z "felix")) - "# - ), - r#"Id [Y Str, Z Str]*"#, - ) - } - - #[test] - fn opaque_wrap_polymorphic_from_multiple_branches_check() { - infer_eq_without_problem( - indoc!( - r#" - Id n := [Id U32 n] - condition : Bool - - v : Id [Y Str, Z Str] - v = - if condition - then @Id (Id 21 (Y "sasha")) - else @Id (Id 21 (Z "felix")) - - v - "# - ), - r#"Id [Y Str, Z Str]"#, - ) - } - - #[test] - fn opaque_unwrap_infer() { - infer_eq_without_problem( - indoc!( - r#" - Age := U32 - - \@Age n -> n - "# - ), - r#"Age -> U32"#, - ) - } - - #[test] - fn opaque_unwrap_check() { - infer_eq_without_problem( - indoc!( - r#" - Age := U32 - - v : Age -> U32 - v = \@Age n -> n - v - "# - ), - r#"Age -> U32"#, - ) - } - - #[test] - fn opaque_unwrap_polymorphic_infer() { - infer_eq_without_problem( - indoc!( - r#" - Id n := [Id U32 n] - - \@Id (Id _ n) -> n - "# - ), - r#"Id a -> a"#, - ) - } - - #[test] - fn opaque_unwrap_polymorphic_check() { - infer_eq_without_problem( - indoc!( - r#" - Id n := [Id U32 n] - - v : Id a -> a - v = \@Id (Id _ n) -> n - - v - "# - ), - r#"Id a -> a"#, - ) - } - - #[test] - fn opaque_unwrap_polymorphic_specialized_infer() { - infer_eq_without_problem( - indoc!( - r#" - Id n := [Id U32 n] - - strToBool : Str -> Bool - - \@Id (Id _ n) -> strToBool n - "# - ), - r#"Id Str -> Bool"#, - ) - } - - #[test] - fn opaque_unwrap_polymorphic_specialized_check() { - infer_eq_without_problem( - indoc!( - r#" - Id n := [Id U32 n] - - strToBool : Str -> Bool - - v : Id Str -> Bool - v = \@Id (Id _ n) -> strToBool n - - v - "# - ), - r#"Id Str -> Bool"#, - ) - } - - #[test] - fn opaque_unwrap_polymorphic_from_multiple_branches_infer() { - infer_eq_without_problem( - indoc!( - r#" - Id n := [Id U32 n] - - \id -> - when id is - @Id (Id _ A) -> "" - @Id (Id _ B) -> "" - @Id (Id _ (C { a: "" })) -> "" - @Id (Id _ (C { a: _ })) -> "" # any other string, for exhautiveness - "# - ), - r#"Id [A, B, C { a : Str }*] -> Str"#, - ) - } - - #[test] - fn opaque_unwrap_polymorphic_from_multiple_branches_check() { - infer_eq_without_problem( - indoc!( - r#" - Id n := [Id U32 n] - - f : Id [A, B, C { a : Str }e] -> Str - f = \id -> - when id is - @Id (Id _ A) -> "" - @Id (Id _ B) -> "" - @Id (Id _ (C { a: "" })) -> "" - @Id (Id _ (C { a: _ })) -> "" # any other string, for exhautiveness - - f - "# - ), - r#"Id [A, B, C { a : Str }e] -> Str"#, - ) - } - - #[test] - fn lambda_set_within_alias_is_quantified() { - infer_eq_without_problem( - indoc!( - r#" - app "test" provides [effectAlways] to "./platform" - - Effect a := {} -> a - - effectAlways : a -> Effect a - effectAlways = \x -> - inner = \{} -> x - - @Effect inner - "# - ), - r#"a -> Effect a"#, - ) - } - - #[test] - fn generalized_accessor_function_applied() { - infer_eq_without_problem( - indoc!( - r#" - returnFoo = .foo - - returnFoo { foo: "foo" } - "# - ), - "Str", - ) - } - - #[test] - fn record_extension_variable_is_alias() { - infer_eq_without_problem( - indoc!( - r#" - Other a b : { y: a, z: b } - - f : { x : Str }(Other Str Str) - f - "# - ), - r#"{ x : Str, y : Str, z : Str }"#, - ) - } - - #[test] - fn tag_extension_variable_is_alias() { - infer_eq_without_problem( - indoc!( - r#" - Other : [B, C] - - f : [A]Other - f - "# - ), - r#"[A, B, C]"#, - ) - } - - #[test] - // https://github.com/rtfeldman/roc/issues/2702 - fn tag_inclusion_behind_opaque() { - infer_eq_without_problem( - indoc!( - r#" - Outer k := [Empty, Wrapped k] - - insert : Outer k, k -> Outer k - insert = \m, var -> - when m is - @Outer Empty -> @Outer (Wrapped var) - @Outer (Wrapped _) -> @Outer (Wrapped var) - - insert - "# - ), - r#"Outer k, k -> Outer k"#, - ) - } - - #[test] - fn tag_inclusion_behind_opaque_infer() { - infer_eq_without_problem( - indoc!( - r#" - Outer k := [Empty, Wrapped k] - - when (@Outer Empty) is - @Outer Empty -> @Outer (Wrapped "") - @Outer (Wrapped k) -> @Outer (Wrapped k) - "# - ), - r#"Outer Str"#, - ) - } - - #[test] - fn tag_inclusion_behind_opaque_infer_single_ctor() { - infer_eq_without_problem( - indoc!( - r#" - Outer := [A, B] - - when (@Outer A) is - @Outer A -> @Outer A - @Outer B -> @Outer B - "# - ), - r#"Outer"#, - ) - } - - #[test] - fn issue_2583_specialize_errors_behind_unified_branches() { - infer_eq_without_problem( - indoc!( - r#" - if True then List.first [] else Str.toI64 "" - "# - ), - "Result I64 [InvalidNumStr, ListWasEmpty]*", - ) - } - - #[test] - fn lots_of_type_variables() { - infer_eq_without_problem( - indoc!( - r#" - fun = \a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z,aa,bb -> {a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z,aa,bb} - fun - "# - ), - "a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y, z, aa, bb -> { a : a, aa : aa, b : b, bb : bb, c : c, d : d, e : e, f : f, g : g, h : h, i : i, j : j, k : k, l : l, m : m, n : n, o : o, p : p, q : q, r : r, s : s, t : t, u : u, v : v, w : w, x : x, y : y, z : z }", - ) - } - - #[test] - fn exposed_ability_name() { - infer_eq_without_problem( - indoc!( - r#" - app "test" provides [hash] to "./platform" - - Hash has hash : a -> U64 | a has Hash - "# - ), - "a -> U64 | a has Hash", - ) - } - - #[test] - fn single_ability_single_member_specializations() { - check_inferred_abilities( - indoc!( - r#" - app "test" provides [hash] to "./platform" - - Hash has hash : a -> U64 | a has Hash - - Id := U64 - - hash = \@Id n -> n - "# - ), - [("Hash:hash", "Id")], - ) - } - - #[test] - fn single_ability_multiple_members_specializations() { - check_inferred_abilities( - indoc!( - r#" - app "test" provides [hash, hash32] to "./platform" - - Hash has - hash : a -> U64 | a has Hash - hash32 : a -> U32 | a has Hash - - Id := U64 - - hash = \@Id n -> n - hash32 = \@Id n -> Num.toU32 n - "# - ), - [("Hash:hash", "Id"), ("Hash:hash32", "Id")], - ) - } - - #[test] - fn multiple_abilities_multiple_members_specializations() { - check_inferred_abilities( - indoc!( - r#" - app "test" provides [hash, hash32, eq, le] to "./platform" - - Hash has - hash : a -> U64 | a has Hash - hash32 : a -> U32 | a has Hash - - Ord has - eq : a, a -> Bool | a has Ord - le : a, a -> Bool | a has Ord - - Id := U64 - - hash = \@Id n -> n - hash32 = \@Id n -> Num.toU32 n - - eq = \@Id m, @Id n -> m == n - le = \@Id m, @Id n -> m < n - "# - ), - [ - ("Hash:hash", "Id"), - ("Hash:hash32", "Id"), - ("Ord:eq", "Id"), - ("Ord:le", "Id"), - ], - ) - } - - #[test] - fn ability_checked_specialization_with_typed_body() { - check_inferred_abilities( - indoc!( - r#" - app "test" provides [hash] to "./platform" - - Hash has - hash : a -> U64 | a has Hash - - Id := U64 - - hash : Id -> U64 - hash = \@Id n -> n - "# - ), - [("Hash:hash", "Id")], - ) - } - - #[test] - fn ability_checked_specialization_with_annotation_only() { - check_inferred_abilities( - indoc!( - r#" - app "test" provides [hash] to "./platform" - - Hash has - hash : a -> U64 | a has Hash - - Id := U64 - - hash : Id -> U64 - "# - ), - [("Hash:hash", "Id")], - ) - } - - #[test] - fn ability_specialization_called() { - infer_eq_without_problem( - indoc!( - r#" - app "test" provides [zero] to "./platform" - - Hash has - hash : a -> U64 | a has Hash - - Id := U64 - - hash = \@Id n -> n - - zero = hash (@Id 0) - "# - ), - "U64", - ) - } - - #[test] - fn alias_ability_member() { - infer_eq_without_problem( - indoc!( - r#" - app "test" provides [thething] to "./platform" - - Hash has - hash : a -> U64 | a has Hash - - thething = - itis = hash - itis - "# - ), - "a -> U64 | a has Hash", - ) - } - - #[test] - fn when_branch_and_body_flipflop() { - infer_eq_without_problem( - indoc!( - r#" - func = \record -> - when record.tag is - A -> { record & tag: B } - B -> { record & tag: A } - - func - "# - ), - "{ tag : [A, B] }a -> { tag : [A, B] }a", - ) - } - - #[test] - fn ability_constrained_in_non_member_check() { - infer_eq_without_problem( - indoc!( - r#" - app "test" provides [hashEq] to "./platform" - - Hash has - hash : a -> U64 | a has Hash - - hashEq : a, a -> Bool | a has Hash - hashEq = \x, y -> hash x == hash y - "# - ), - "a, a -> Bool | a has Hash", - ) - } - - #[test] - fn ability_constrained_in_non_member_infer() { - infer_eq_without_problem( - indoc!( - r#" - app "test" provides [hashEq] to "./platform" - - Hash has - hash : a -> U64 | a has Hash - - hashEq = \x, y -> hash x == hash y - "# - ), - "a, b -> Bool | a has Hash, b has Hash", - ) - } - - #[test] - fn ability_constrained_in_non_member_infer_usage() { - infer_eq_without_problem( - indoc!( - r#" - app "test" provides [result] to "./platform" - - Hash has - hash : a -> U64 | a has Hash - - hashEq = \x, y -> hash x == hash y - - Id := U64 - hash = \@Id n -> n - - result = hashEq (@Id 100) (@Id 101) - "# - ), - "Bool", - ) - } - - #[test] - fn ability_constrained_in_non_member_multiple_specializations() { - infer_eq_without_problem( - indoc!( - r#" - app "test" provides [result] to "./platform" - - Hash has - hash : a -> U64 | a has Hash - - mulHashes = \x, y -> hash x * hash y - - Id := U64 - hash = \@Id n -> n - - Three := {} - hash = \@Three _ -> 3 - - result = mulHashes (@Id 100) (@Three {}) - "# - ), - "U64", - ) - } - - #[test] - fn intermediate_branch_types() { - infer_queries!( - indoc!( - r#" - app "test" provides [foo] to "./platform" - - foo : Bool -> Str - foo = \ob -> - # ^^ - when ob is - # ^^ - True -> "A" - # ^^^^ - False -> "B" - # ^^^^^ - "# - ), - &[ - "ob : Bool", - "ob : Bool", - "True : [False, True]", - "False : [False, True]", - ], - ) - } - - #[test] - fn nested_open_tag_union() { - infer_eq_without_problem( - indoc!( - r#" - app "test" provides [go] to "./platform" - - Expr : [ - Wrap Expr, - Val I64, - ] - - go : Expr -> Expr - go = \e -> - when P e is - P (Wrap (Val _)) -> Wrap e - - # This branch should force the first argument to `P` and - # the first argument to `Wrap` to be an open tag union. - # This tests checks that we don't regress on that. - P y1 -> Wrap y1 - "# - ), - indoc!(r#"Expr -> Expr"#), - ) - } - - #[test] - fn opaque_and_alias_unify() { - infer_eq_without_problem( - indoc!( - r#" - app "test" provides [always] to "./platform" - - Effect a := {} -> a - - Task a err : Effect (Result a err) - - always : a -> Task a * - always = \x -> @Effect (\{} -> Ok x) - "# - ), - "a -> Task a *", - ); - } - - #[test] - fn export_rigid_to_lower_rank() { - infer_eq_without_problem( - indoc!( - r#" - app "test" provides [foo] to "./platform" - - F a : { foo : a } - - foo = \arg -> - x : F b - x = arg - x.foo - "# - ), - "F b -> b", - ); - } - - #[test] - fn alias_in_opaque() { - infer_eq_without_problem( - indoc!( - r#" - app "test" provides [foo] to "./platform" - - MyError : [Error] - - MyResult := Result U8 MyError - - foo = @MyResult (Err Error) - "# - ), - "MyResult", - ) - } - - #[test] - fn alias_propagates_able_var() { - infer_eq_without_problem( - indoc!( - r#" - app "test" provides [zeroEncoder] to "./platform" - - Encoder fmt := List U8, fmt -> List U8 | fmt has Format - - Format has it : fmt -> {} | fmt has Format - - zeroEncoder = @Encoder \lst, _ -> lst - "# - ), - "Encoder a | a has Format", - ) - } - - #[test] - fn encoder() { - infer_queries!( - indoc!( - r#" - app "test" provides [myU8Bytes] to "./platform" - - Encoder fmt := List U8, fmt -> List U8 | fmt has Format - - Encoding has - toEncoder : val -> Encoder fmt | val has Encoding, fmt has Format - - Format has - u8 : U8 -> Encoder fmt | fmt has Format - - appendWith : List U8, Encoder fmt, fmt -> List U8 | fmt has Format - appendWith = \lst, (@Encoder doFormat), fmt -> doFormat lst fmt - - toBytes : val, fmt -> List U8 | val has Encoding, fmt has Format - toBytes = \val, fmt -> appendWith [] (toEncoder val) fmt - - - Linear := {} - - # impl Format for Linear - u8 = \n -> @Encoder (\lst, @Linear {} -> List.append lst n) - #^^{-1} - - MyU8 := U8 - - # impl Encoding for MyU8 - toEncoder = \@MyU8 n -> u8 n - #^^^^^^^^^{-1} - - myU8Bytes = toBytes (@MyU8 15) (@Linear {}) - #^^^^^^^^^{-1} - "# - ), - &[ - "Linear#u8(22) : U8 -[[u8(22)]]-> Encoder Linear", - "MyU8#toEncoder(23) : MyU8 -[[toEncoder(23)]]-> Encoder fmt | fmt has Format", - "myU8Bytes : List U8", - ], - ) - } - - #[test] - fn decoder() { - infer_queries!( - indoc!( - r#" - app "test" provides [myU8] to "./platform" - - DecodeError : [TooShort, Leftover (List U8)] - - Decoder val fmt := List U8, fmt -> { result: Result val DecodeError, rest: List U8 } | fmt has DecoderFormatting - - Decoding has - decoder : Decoder val fmt | val has Decoding, fmt has DecoderFormatting - - DecoderFormatting has - u8 : Decoder U8 fmt | fmt has DecoderFormatting - - decodeWith : List U8, Decoder val fmt, fmt -> { result: Result val DecodeError, rest: List U8 } | fmt has DecoderFormatting - decodeWith = \lst, (@Decoder doDecode), fmt -> doDecode lst fmt - - fromBytes : List U8, fmt -> Result val DecodeError - | fmt has DecoderFormatting, val has Decoding - fromBytes = \lst, fmt -> - when decodeWith lst decoder fmt is - { result, rest } -> - when result is - Ok val -> if List.isEmpty rest then Ok val else Err (Leftover rest) - Err e -> Err e - - - Linear := {} - - # impl DecoderFormatting for Linear - u8 = @Decoder \lst, @Linear {} -> - #^^{-1} - when List.first lst is - Ok n -> { result: Ok n, rest: List.dropFirst lst } - Err _ -> { result: Err TooShort, rest: [] } - - MyU8 := U8 - - # impl Decoding for MyU8 - decoder = @Decoder \lst, fmt -> - #^^^^^^^{-1} - when decodeWith lst u8 fmt is - { result, rest } -> - { result: Result.map result (\n -> @MyU8 n), rest } - - myU8 : Result MyU8 _ - myU8 = fromBytes [15] (@Linear {}) - #^^^^{-1} - "# - ), - &[ - "Linear#u8(27) : Decoder U8 Linear", - "MyU8#decoder(28) : Decoder MyU8 fmt | fmt has DecoderFormatting", - "myU8 : Result MyU8 DecodeError", - ], - ) - } - - #[test] - fn task_wildcard_wildcard() { - infer_eq_without_problem( - indoc!( - r#" - app "test" provides [tforever] to "./platform" - - Effect a := {} -> a - - eforever : Effect a -> Effect b - - Task a err : Effect (Result a err) - - tforever : Task val err -> Task * * - tforever = \task -> eforever task - "# - ), - "Task val err -> Task * *", - ); - } - - #[test] - fn static_specialization() { - infer_queries!( - indoc!( - r#" - app "test" provides [main] to "./platform" - - Default has default : {} -> a | a has Default - - A := {} - default = \{} -> @A {} - - main = - a : A - a = default {} - # ^^^^^^^ - a - "# - ), - &["A#default(5) : {} -[[default(5)]]-> A"], - ) - } - - #[test] - fn stdlib_encode_json() { - infer_eq_without_problem( - indoc!( - r#" - app "test" - imports [Encode.{ toEncoder }, Json] - provides [main] to "./platform" - - HelloWorld := {} - - toEncoder = \@HelloWorld {} -> - Encode.custom \bytes, fmt -> - bytes - |> Encode.appendWith (Encode.string "Hello, World!\n") fmt - - main = - when Str.fromUtf8 (Encode.toBytes (@HelloWorld {}) Json.format) is - Ok s -> s - _ -> "" - "# - ), - "Str", - ) - } - - #[test] - fn encode_record() { - infer_queries!( - indoc!( - r#" - app "test" - imports [Encode.{ toEncoder }] - provides [main] to "./platform" - - main = toEncoder { a: "" } - # ^^^^^^^^^ - "# - ), - &[ - "Encoding#toEncoder(2) : { a : Str } -[[] + { a : Str }:toEncoder(2):1]-> Encoder fmt | fmt has EncoderFormatting", - ], - ) - } - - #[test] - fn encode_record_with_nested_custom_impl() { - infer_queries!( - indoc!( - r#" - app "test" - imports [Encode.{ toEncoder, Encoding, custom }] - provides [main] to "./platform" - - A := {} - toEncoder = \@A _ -> custom \b, _ -> b - - main = toEncoder { a: @A {} } - # ^^^^^^^^^ - "# - ), - &["Encoding#toEncoder(2) : { a : A } -[[] + { a : A }:toEncoder(2):1]-> Encoder fmt | fmt has EncoderFormatting"], - ) - } - - #[test] - fn resolve_lambda_set_generalized_ability_alias() { - infer_queries!( - indoc!( - r#" - app "test" provides [main] to "./platform" - - Id has id : a -> a | a has Id - - A := {} - id = \@A {} -> @A {} - #^^{-1} - - main = - alias1 = id - # ^^ - alias2 = alias1 - # ^^^^^^ - - a : A - a = alias2 (@A {}) - # ^^^^^^ - - a - "# - ), - &[ - "A#id(5) : A -[[id(5)]]-> A", - "Id#id(4) : a -[[] + a:id(4):1]-> a | a has Id", - "alias1 : a -[[] + a:id(4):1]-> a | a has Id", - "alias2 : A -[[id(5)]]-> A", - ], - ) - } - - #[test] - fn resolve_lambda_set_ability_chain() { - infer_queries!( - indoc!( - r#" - app "test" provides [main] to "./platform" - - Id1 has id1 : a -> a | a has Id1 - Id2 has id2 : a -> a | a has Id2 - - A := {} - id1 = \@A {} -> @A {} - #^^^{-1} - - id2 = \@A {} -> id1 (@A {}) - #^^^{-1} ^^^ - - main = - a : A - a = id2 (@A {}) - # ^^^ - - a - "# - ), - &[ - "A#id1(8) : A -[[id1(8)]]-> A", - // - "A#id2(9) : A -[[id2(9)]]-> A", - "A#id1(8) : A -[[id1(8)]]-> A", - // - "A#id2(9) : A -[[id2(9)]]-> A", - ], - ) - } - - #[test] - fn resolve_lambda_set_branches_ability_vs_non_ability() { - infer_queries!( - indoc!( - r#" - app "test" provides [main] to "./platform" - - Id has id : a -> a | a has Id - - A := {} - id = \@A {} -> @A {} - #^^{-1} - - idNotAbility = \x -> x - #^^^^^^^^^^^^{-1} - - main = - choice : [T, U] - - idChoice = - #^^^^^^^^{-1} - when choice is - T -> id - U -> idNotAbility - - idChoice (@A {}) - #^^^^^^^^{-1} - "# - ), - &[ - "A#id(5) : A -[[id(5)]]-> A", - "idNotAbility : a -[[idNotAbility(6)]]-> a", - "idChoice : a -[[idNotAbility(6)] + a:id(4):1]-> a | a has Id", - "idChoice : A -[[id(5), idNotAbility(6)]]-> A", - ], - ) - } - - #[test] - fn resolve_lambda_set_branches_same_ability() { - infer_queries!( - indoc!( - r#" - app "test" provides [main] to "./platform" - - Id has id : a -> a | a has Id - - A := {} - id = \@A {} -> @A {} - #^^{-1} - - main = - choice : [T, U] - - idChoice = - #^^^^^^^^{-1} - when choice is - T -> id - U -> id - - idChoice (@A {}) - #^^^^^^^^{-1} - "# - ), - &[ - "A#id(5) : A -[[id(5)]]-> A", - "idChoice : a -[[] + a:id(4):1]-> a | a has Id", - "idChoice : A -[[id(5)]]-> A", - ], - ) - } - - #[test] - fn resolve_unspecialized_lambda_set_behind_alias() { - infer_queries!( - indoc!( - r#" - app "test" provides [main] to "./platform" - - Thunk a : {} -> a - - Id has id : a -> Thunk a | a has Id - - A := {} - id = \@A {} -> \{} -> @A {} - #^^{-1} - - main = - alias = id - # ^^ - - a : A - a = (alias (@A {})) {} - # ^^^^^ - - a - "# - ), - &[ - "A#id(7) : {} -[[id(7)]]-> ({} -[[8(8)]]-> {})", - "Id#id(6) : {} -[[id(7)]]-> ({} -[[8(8)]]-> {})", - "alias : {} -[[id(7)]]-> ({} -[[8(8)]]-> {})", - ], - print_only_under_alias = true, - ) - } - - #[test] - fn resolve_unspecialized_lambda_set_behind_opaque() { - infer_queries!( - indoc!( - r#" - app "test" provides [main] to "./platform" - - Thunk a := {} -> a - - Id has id : a -> Thunk a | a has Id - - A := {} - id = \@A {} -> @Thunk (\{} -> @A {}) - #^^{-1} - - main = - thunk = id (@A {}) - @Thunk it = thunk - it {} - #^^{-1} - "# - ), - &[ - "A#id(7) : {} -[[id(7)]]-> ({} -[[8(8)]]-> {})", - "it : {} -[[8(8)]]-> {}", - ], - print_only_under_alias = true, - ) - } - - #[test] - fn resolve_two_unspecialized_lambda_sets_in_one_lambda_set() { - infer_queries!( - indoc!( - r#" - app "test" provides [main] to "./platform" - - Thunk a : {} -> a - - Id has id : a -> Thunk a | a has Id - - A := {} - id = \@A {} -> \{} -> @A {} - #^^{-1} - - main = - a : A - a = (id (@A {})) {} - # ^^ - - a - "# - ), - &[ - "A#id(7) : {} -[[id(7)]]-> ({} -[[8(8)]]-> {})", - "A#id(7) : {} -[[id(7)]]-> ({} -[[8(8)]]-> {})", - ], - print_only_under_alias = true, - ) - } - - #[test] - fn resolve_recursive_ability_lambda_set() { - infer_queries!( - indoc!( - r#" - app "test" provides [main] to "./platform" - - Diverge has diverge : a -> a | a has Diverge - - A := {} - diverge = \@A {} -> diverge (@A {}) - #^^^^^^^{-1} ^^^^^^^ - - main = - a : A - a = diverge (@A {}) - # ^^^^^^^ - - a - "# - ), - &[ - "A#diverge(5) : A -[[diverge(5)]]-> A", - "Diverge#diverge(4) : A -[[diverge(5)]]-> A", - // - "A#diverge(5) : A -[[diverge(5)]]-> A", - ], - ) - } - - #[test] - fn resolve_mutually_recursive_ability_lambda_sets() { - infer_queries!( - indoc!( - r#" - app "test" provides [main] to "./platform" - - Bounce has - ping : a -> a | a has Bounce - pong : a -> a | a has Bounce - - A := {} - - ping = \@A {} -> pong (@A {}) - #^^^^{-1} ^^^^ - - pong = \@A {} -> ping (@A {}) - #^^^^{-1} ^^^^ - - main = - a : A - a = ping (@A {}) - # ^^^^ - - a - "# - ), - &[ - "A#ping(7) : A -[[ping(7)]]-> A", - "Bounce#pong(6) : A -[[pong(8)]]-> A", - // - "A#pong(8) : A -[[pong(8)]]-> A", - "A#ping(7) : A -[[ping(7)]]-> A", - // - "A#ping(7) : A -[[ping(7)]]-> A", - ], - ) - } -} diff --git a/compiler/str/Cargo.toml b/compiler/str/Cargo.toml deleted file mode 100644 index a59efae3c4..0000000000 --- a/compiler/str/Cargo.toml +++ /dev/null @@ -1,26 +0,0 @@ -[package] -name = "roc_str" -version = "0.1.0" -authors = ["The Roc Contributors"] -license = "UPL-1.0" -edition = "2021" - -[dependencies] -roc_collections = { path = "../collections" } -roc_region = { path = "../region" } -roc_module = { path = "../module" } -roc_types = { path = "../types" } -roc_can = { path = "../can" } -roc_unify = { path = "../unify" } -roc_problem = { path = "../problem" } -bumpalo = { version = "3.6.1", features = ["collections"] } - -[dev-dependencies] -roc_constrain = { path = "../constrain" } -roc_builtins = { path = "../builtins" } -roc_parse = { path = "../parse" } -roc_solve = { path = "../solve" } -pretty_assertions = "0.5.1" -indoc = "0.3.3" -quickcheck = "0.8" -quickcheck_macros = "0.8" diff --git a/compiler/str/README.md b/compiler/str/README.md deleted file mode 100644 index 8fc9890e34..0000000000 --- a/compiler/str/README.md +++ /dev/null @@ -1,209 +0,0 @@ -# `Str` - -This is the in-memory representation for `Str`. To explain how `Str` is laid out in memory, it's helpful to start with how `List` is laid out. - -## Empty list - -An empty `List Str` is essentially this Rust type with all 0s in memory: - -```rust -struct List { - pointer: *Str, // pointers are the same size as `usize` - length: usize -} -``` - -On a 64-bit system, this `struct` would take up 16B in memory. On a 32-bit system, it would take up 8B. - -Here's what the fields mean: - -* `pointer` is the memory address of the heap-allocated memory containing the `Bool` elements. For an empty list, the pointer is null (that is, 0). -* `length` is the number of `Bool` elements in the list. For an empty list, this is also 0. - -## Nonempty list - -Now let's say we define a `List Str` with two elements in it, like so: `["foo", "bar"]`. - -First we'd have the `struct` above, with both `length` and `capacity` set to 2. Then, we'd have some memory allocated on the heap, and `pointer` would store that memory's address. - -Here's how that heap memory would be laid out on a 64-bit system. It's a total of 48 bytes. - -``` -|------16B------|------16B------|---8B---|---8B---| - string #1 string #2 refcount unused -``` - -Just like how `List` is a `struct` that takes up `2 * usize` bytes in memory, `Str` takes up the same amount of memory - namely, 16B on a 64-bit system. That's why each of the two strings take up 16B of this heap-allocated memory. (Those structs may also point to other heap memory, but they could also be empty strings! Either way we just store the structs in the list, which take up 16B.) - -We'll get to what the refcount is for shortly, but first let's talk about the memory layout. The refcount is a `usize` integer, so 8B on our 64-bit system. Why is there 8B of unused memory after it? - -This is because of memory alignment. Whenever a system loads some memory from a memory address, it's much more efficient if the address is a multiple of the number of bytes it wants to get. So if we want to load a 16B string struct, we want its address to be a multiple of 16. - -When we're allocating memory on the heap, the way we specify what alignment we want is to say how big each element is, and how many of them we want. In this case, we say we want 16B elements, and we want 3 of them. Then we use the first 16B slot to store the 8B refcount, and the 8B after it are unused. - -This is memory-inefficient, but it's the price we pay for having all the 16B strings stored in addresses that are multiples of 16. It'd be worse for performance if we tried to pack everything tightly, so we accept the memory inefficiency as a cost of achieving better overall execution speed. - -> Note: if we happened to have 8B elements instead of 16B elements, the alignment would be 8 anyway and we'd have no unused memory. - -## Reference counting - -Let's go back to the refcount - short for "reference count." - -The refcount is a `usize` integer which counts how many times this `List` has been shared. For example, if we named this list `myList` and then wrote `[myList, myList, myList]` then we'd increment that refcount 3 times because `myList` is now being shared three more times. - -If we were to later call `List.pop` on that list, and the result was an in-place mutation that removed one of the `myList` entries, we'd decrement the refcount. If we did that again and again until the refcount got all the way down to 0, meaning nothing is using it anymore, then we'd deallocate these 48B of heap memory because nobody is using them anymore. - -In some cases, the compiler can detect that no reference counting is necessary. In that scenario, it doesn't bother allocating extra space for the refcount; instead, it inserts an instruction to allocate the memory at the appropriate place, another to free it later, and that's it. - -## Pointing to the first element - -The fact that the reference count may or may not be present could creat a tricky situation for some `List` operations. - -For example, should `List.get 0` return the first 16B of the heap-allocated bytes, or the second 16B? If there's a reference count in the first 16B, it should return the second 16B. If there's no refcount, it should return the first 16B. - -To solve this, the pointer in the List struct *always* points to the first element in the list. That means to access the reference count, it does negative pointer arithmetic to get the address at 16B *preceding* the memory address it has stored in its pointer field. - -## Growing lists - -If uniqueness typing tells us that a list is Unique, we know two things about it: - -1. It doesn't need a refcount, because nothing else ever references it. -2. It can be mutated in-place. - -One of the in-place mutations that can happen to a list is that its length can increase. For example, if I call `List.append list1 list2`, and `list1` is unique, then we'll attempt to append `list2`'s contents in-place into `list1`. - -Calling `List.append` on a Shared list results in allocating a new chunk of heap memory large enough to hold both lists (with a fresh refcount, since nothing is referencing the new memory yet), then copying the contents of both lists into the new memory, and finally decrementing the refcount of the old memory. - -Calling `List.append` on a Unique list can potentially be done in-place instead. - -First, `List.append` repurposes the `usize` slot normally used to store refcount, and stores a `capacity` counter in there instead of a refcount. (After all, unique lists don't need to be refcounted.) A list's capacity refers to how many elements the list *can* hold given the memory it has allocated to it, which is always guaranteed to be at least as many as its length. - -When calling `List.append list1 list2` on a unique `list1`, first we'll check to see if `list1.capacity <= list1.length + list2.length`. If it is, then we can copy in the new values without needing to allocate more memory for `list1`. - -If there is not enough capacity to fit both lists, then we can try to call [`realloc`](https://docs.microsoft.com/en-us/cpp/c-runtime-library/reference/realloc?view=vs-2019) to hopefully extend the size of our allocated memory. If `realloc` succeeds (meaning there happened to be enough free memory right after our current allocation), then we update `capacity` to reflect the new amount of space, and move on. - -> **Note:** The reason we store capacity right after the last element in the list is because of how memory cache lines work. Whenever we need to access `capacity`, it's because we're about to increase the length of the list, which means that we will most certainly be writing to the memory location right after its last element. That in turn means that we'll need to have that memory location in cache, which in turn means that looking up the `capacity` there is guaranteed not to cause a cache miss. (It's possible that writing the new capacity value to a later address could cause a cache miss, but this strategy minimizes the chance of that happening.) An alternate design would be where we store the capacity right before the first element in the list. In that design we wouldn't have to re-write the capacity value at the end of the list every time we grew it, but we'd be much more likely to incur more cache misses that way - because we're working at the end of the list, not at the beginning. Cache misses are many times more expensive than an extra write to a memory address that's in cache already, not to mention the potential extra load instruction to add the length to the memory address of the first element (instead of subtracting 1 from that address), so we optimize for minimizing the highly expensive cache misses by always paying a tiny additional cost when increasing the length of the list, as well as a potential even tinier cost (zero, if the length already happens to be in a register) when looking up its capacity or refcount. - -If `realloc` fails, then we have to fall back on the same "allocate new memory and copy everything" strategy that we do with shared lists. - -When you have a way to anticipate that a list will want to grow incrementally to a certain size, you can avoid such extra allocations by using `List.reserve` to guarantee more capacity up front. (`List.reserve` works like Rust's [`Vec::reserve`](https://doc.rust-lang.org/std/vec/struct.Vec.html#method.reserve).) - -> **Note:** Calling `List.reserve 0 myList` will have no effect on a Unique list, but on a Shared list it will clone `myList` and return a Unique one. If you want to do a bunch of in-place mutations on a list, but it's currently Shared, calling `List.reserve 0` on it to get a Unique clone could actually be an effective performance optimization! - -## Capacity and alignment - -Some lists may end up beginning with excess capacity due to memory alignment requirements. Since the refcount is `usize`, all lists need a minimum of that alignment. For example, on a 64-bit system, a `List Bool` has an alignment of 8B even though bools can fit in 1B. - -This means the list `[True, True, False]` would have a memory layout like this): - -``` -|--------------8B--------------|--1B--|--1B--|--1B--|-----5B-----| - either refcount or capacity bool1 bool2 bool3 unused -``` - -As such, if this list is Unique, it would start out with a length of 3 and a capacity of 8. - -Since each bool value is a byte, it's okay for them to be packed side-by-side even though the overall alignment of the list elements is 8. This is fine because each of their individual memory addresses will end up being a multiple of their size in bytes. - -Note that unlike in the `List Str` example before, there wouldn't be any unused memory between the refcount (or capacity, depending on whether the list was shared or unique) and the first element in the list. That will always be the case when the size of the refcount is no bigger than the alignment of the list's elements. - -## Distinguishing between refcount and capacity in the host - -If I'm a platform author, and I receive a `List` from the application, it's important that I be able to tell whether I'm dealing with a refcount or a capacity. (The uniqueness type information will have been erased by this time, because some applications will return a Unique list while others return a Shared list, so I need to be able to tell using runtime information only which is which.) This way, I can know to either increment the refcount, or to feel free to mutate it in-place using the capacity value. - -We use a very simple system to distinguish the two: if the high bit in that `usize` value is 0, then it's capacity. If it's 1, then it's a refcount. - -This has a couple of implications: - -* `capacity` actually has a maximum of `isize::MAX`, not `usize::MAX` - because if its high bit flips to 1, then now suddenly it's considered a refcount by the host. As it happens, capacity's maximum value is naturally `isize::MAX` anyway, so there's no downside here. -* `refcount` actually begins at `isize::MIN` and increments towards 0, rather than beginning at 0 and incrementing towards a larger number. When a decrement instruction is executed and the refcount is `isize::MIN`, it gets freed instead. Since all refcounts do is count up and down, and they only ever compare the refcount to a fixed constant, there's no real performance cost to comparing to `isize::MIN` instead of to 0. So this has no significant downside either. - -Using this representation, hosts can trivially distinguish any list they receive as being either refcounted or having a capacity value, without any runtime cost in either the refcounted case or the capacity case. - -### Saturated reference count - -What happens if the reference count overflows? As in, we try to reference the same list more than `isize` times? - -In this situation, the reference count becomes unreliable. Suppose we try to increment it 3 more times after it's already been incremented `isize` times, and since we can't store any further numbers without flipping the high bit from 1 to 0 (meaning it will become a capacity value instead of a refcount), we leave it at -1. If we later decrement it `isize` times, we'll be down to `isize::MIN` and will free the memory, even though 3 things are still referencing that memory! - -This would be a total disaster, so what we do instead is that we decide to leak the memory. Once the reference count hits -1, we neither increment nor decrement it ever again, which in turn means we will never free it. So -1 is a special reference count meaning "this memory has become unreclaimable, and must never be freed." - -This has the downside of being potentially wasteful of the program's memory, but it's less detrimental to user experience than a crash, and it doesn't impact correctness at all. - -## Summary of Lists - -Lists are a `2 * usize` struct which contains a length and a pointer. - -That pointer is a memory address (null in the case of an empty list) which points to the first element in a sequential array of memory. - -If that pointer is shared in multiple places, then there will be a `usize` reference count stored right before the first element of the list. There may be unused memory after the refcount if `usize` is smaller than the alignment of one of the list's elements. - -Refcounts get incremented each time a list gets shared somewhere, and decremented each time that shared value is no longer referenced by anything else (for example, by going out of scope). Once there are no more references, the list's heap memory can be safely freed. If a reference count gets all the way up to `usize`, then it will never be decremented again and the memory will never be freed. - -Whenever a list grows, it will grow in-place if it's Unique and there is enough capacity. (Capacity is stored where a refcount would be in a Shared list.) If there isn't enough capacity - even after trying `realloc` - or if the list is Shared, then instead new heap memory will be allocated, all the necessary elements will get copied into it, and the original list's refcount will be decremented. - -## Strings - -Strings have several things in common with lists: - -* They are a `2 * usize` struct, sometimes with a non-null pointer to some heap memory -* They have a length and a capacity, and they can grow in basically the same way -* They are reference counted in basically the same way - -However, they also have two things going on that lists do not: - -* The Small String Optimization -* Literals stored in read-only memory - -## The Small String Optimization - -In practice, a lot of strings are pretty small. For example, the string `"Richard Feldman"` can be stored in 15 UTF-8 bytes. If we stored that string the same way we store a list, then on a 64-bit system we'd need a 16B struct, which would include a pointer to 24B of heap memory (including the refcount/capacity and one unused byte for alignment). - -That's a total of 48B to store 15B of data, when we could have fit the whole string into the original 16B we needed for the struct, with one byte left over. - -The Small String Optimization is where we store strings directly in the struct, assuming they can fit in there. We reserve one of those bytes to indicate whether this is a Small String or a larger one that actually uses a pointer. - -## String Memory Layout - -How do we tell small strings apart from nonsmall strings? - -We make use of the fact that lengths (for both strings *and* lists) are `usize` values which have a maximum value of `isize::MAX` rather than `usize::MAX`. This is because `List.get` compiles down to an array access operation, and LLVM uses `isize` indices for those because they do signed arithmetic on the pointer in case the caller wants to add a negative number to the address. (We don't want to, as it happens, but that's what the low-level API supports, so we are bound by its limitations.) - -Since the string's length is a `usize` value with a maximum of `isize::MAX`, we can be sure that its most significant bit will always be 0, not 1. (If it were a 1, that would be a negative `isize`!) We can use this fact to use that spare bit as a flag indicating whether the string is small: if that bit is a 1, it's a small string; otherwise, it's a nonsmall string. - -This makes calculating the length of the string a multi-step process: - -1. Get the length field out of the struct. -2. Look at its highest bit. If that bit is 0, return the length as-is. -3. If the bit is 1, then this is a small string, and its length is packed into the highest byte of the `usize` length field we're currently examining. Take that byte and bit shift it by 1 (to drop the `1` flag we used to indicate this is a small string), cast the resulting byte to `usize`, and that's our length. (Actually we bit shift by 4, not 1, because we only need 4 bits to store a length of 0-16, and the leftover 3 bits can potentially be useful in the future.) - -Using this strategy with a [conditional move instruction](https://stackoverflow.com/questions/14131096/why-is-a-conditional-move-not-vulnerable-for-branch-prediction-failure), we can always get the length of a `Str` in 2-3 cheap instructions on a single `usize` value, without any chance of a branch misprediction. - -Thus, the layout of a small string on a 64-bit big-endian architecture would be: - -``` -|-----------usize length field----------|-----------usize pointer field---------| -|-1B-|-1B-|-1B-|-1B-|-1B-|-1B-|-1B-|-1B-|-1B-|-1B-|-1B-|-1B-|-1B-|-1B-|-1B-|-1B-| - len 'R' 'i' 'c' 'h' 'a' 'r' 'd' ' ' 'F' 'e' 'l' 'd' 'm' 'a' 'n' -``` - -The `len` value here would be the number 15, plus a 1 (to flag that this is a small string) that would always get bit-shifted away. The capacity of a small Unique string is always equal to `2 * usize`, because that's how much you can fit without promoting to a nonsmall string. - -## Endianness - -The preceding memory layout example works on a big-endian architecture, but most CPUs are little-endian. That means the high bit where we want to store the flag (the 0 or 1 -that would make an `isize` either negative or positive) will actually be the `usize`'s last byte rather than its first byte. - -That means we'd have to move swap the order of the struct's length and pointer fields. Here's how the string `"Roc string"` would be stored on a little-endian system: - -``` -|-----------usize pointer field---------|-----------usize length field----------| -|-1B-|-1B-|-1B-|-1B-|-1B-|-1B-|-1B-|-1B-|-1B-|-1B-|-1B-|-1B-|-1B-|-1B-|-1B-|-1B-| - 'R' 'o' 'c' ' ' 's' 't' 'r' 'i' 'n' 'g' 0 0 0 0 0 len -``` - -Here, `len` would have the same format as before (including the extra 1 in the same position, which we'd bit shift away) except that it'd store a length of 10 instead of 15. - -Notice that the leftover bytes are stored as zeroes. This is handy because it means we can convert small Roc strings into C strings (which are 0-terminated) for free as long as they have at least one unused byte. Also notice that `usize pointer field` and `usize length field` have been swapped compared to the preceding example! - -## Storing string literals in read-only memory diff --git a/compiler/str/src/lib.rs b/compiler/str/src/lib.rs deleted file mode 100644 index 02387e17ea..0000000000 --- a/compiler/str/src/lib.rs +++ /dev/null @@ -1,66 +0,0 @@ -#![crate_type = "lib"] -#![no_std] - -/// Str does Roc-style collection reference counting, which means the collection -/// may or may not be safe to mutate in-place, may or may not be reference counted, -/// and may or may not need to be freed when no longer in use. Whether each of -/// these is true for a given collection can be determined by inspecting -/// that collection at runtime. -/// -/// Details: -/// -/// 1. If the collection is empty, it does not allocate on the heap. -/// 2. If it is nonempty, its pointer points to the first element of a "backing array" on the heap. -/// 3. There is an extra `isize` right before that backing array (still on the heap) which stores the -/// "flexible reference count" ("flexcount" for short). -/// 4. The flexcount can refer to one of three things, depending on whether it is positive, -/// negative, or zero. -/// 5. If the flexcount is positive, then it's a capacity. The capacity refers to the number of -/// collection elements in the backing array. This collection can be mutated in-place, until it -/// runs out of capacity. At that point, it will need a new backing array. Once it goes out of -/// scope, the backing array should be freed by the system allocator - but free() must be passed -/// a pointer to the flexcount slot, not to element 0 (because the flexcount slot is where the -/// original allocation began). Capacity will always be at least 1, because otherwise we would -/// not have allocated on the heap in the first place. -/// 6. If the flexcount is 0, then this collection resides in readonly memory. That means it cannot -/// be mutated in-place (and attempting to do so will segfault), and it must not be attempted to -/// be freed. It exists in memory forever! -/// 7. If the flexcount is negative, then it is a reference count. Treat the collection as immutable, just like -/// if the flexcount were 0, except free it when there are no more references to it. Instead of the reference count -/// starting at 0 or 1 and incrementing when new references are added, this refcount starts with all bits being 1 (so, isize::MIN) and -/// increments towards 0 when new references are added. When a reference is removed, if all bits are 1, then it should be freed. If so many new references are added that it gets incremented all the way from isize::MAX to 0, then, as is best practice when running out of reference counts, it will leak. (Leaking memory is typically less bad than crashing, and this should essentially never happen.) This happens automatically because when the flexcount is 0, it's assumed that the collection is in readonly memory and should not be freed - which is nice because it means there is no extra conditional required to implement this edge case. -/// 8. If a collection has a refcount of isize::MIN (meaning nothing else references it), it may or may not be safe to convert it to a capacity, -/// depending on whether it contains other refcounted collections. For example, a Str -/// is a collection of all bytes, so if it has a refcount of all 1 bits, it can be safely -/// converted to a capacity (the initial capacity should be equal to the collection's length), -/// after which point it can be safely mutated in-place. However, a refcounted List of Lists with a refcount of isize::MIN will not be safe to convert to a capacity, unless the inner Lists also happen to have refcounts of isize::MIN. This is because mutate-in-place operations like removing an element from a list do not check for refcounts in the elements they remove, which means removing an element from the newly mutable-in-place list would cause memory leaks in its refcounted contents. (They'd have been removed, but their reference counts would not have been adjusted accordingly.) -/// -/// Note that because of these runtime conditionals, modifying and freeing Roc collections are both -/// cheaper operations in generated Roc code than in host code. Since the Roc compiler knows -/// statically whether a collection is refcounted, unique, or readonly, it does not bother with -/// these checks at runtime. A host, however, cannot have that information statically (since it may be different -/// for different applications), and so must check at runtime instead. -struct Str { - bytes: [16, u8]; -} - -#[no_mangle] -pub fn empty_() -> Str { - Str { - bytes : [0; 16] - } -} - -#[no_mangle] -pub fn len_(string: Str) -> usize { - let disc = discriminant(str); - - if disc == 0 { - // It's a - } -} - -#[inline(always)] -fn discriminant(string: &Str) -> u8 { - // cast the first 8 bytes to be u64, return its lsbyte -} diff --git a/compiler/test_gen/Cargo.toml b/compiler/test_gen/Cargo.toml deleted file mode 100644 index 2322900e08..0000000000 --- a/compiler/test_gen/Cargo.toml +++ /dev/null @@ -1,58 +0,0 @@ -[package] -name = "test_gen" -version = "0.1.0" -authors = ["The Roc Contributors"] -license = "UPL-1.0" -edition = "2021" - -[[test]] -name = "test_gen" -path = "src/tests.rs" - -[build-dependencies] -roc_builtins = { path = "../builtins" } - -[dev-dependencies] -roc_gen_llvm = { path = "../gen_llvm" } -roc_gen_dev = { path = "../gen_dev" } -roc_gen_wasm = { path = "../gen_wasm" } -roc_collections = { path = "../collections" } -roc_region = { path = "../region" } -roc_module = { path = "../module" } -roc_problem = { path = "../problem" } -roc_types = { path = "../types" } -roc_builtins = { path = "../builtins" } -roc_constrain = { path = "../constrain" } -roc_unify = { path = "../unify" } -roc_solve = { path = "../solve" } -roc_mono = { path = "../mono" } -roc_reporting = { path = "../../reporting" } -roc_load = { path = "../load" } -roc_can = { path = "../can" } -roc_parse = { path = "../parse" } -roc_build = { path = "../build", features = ["target-aarch64", "target-x86_64", "target-wasm32"] } -roc_target = { path = "../roc_target" } -roc_std = { path = "../../roc_std" } -bumpalo = { version = "3.8.0", features = ["collections"] } -either = "1.6.1" -libc = "0.2.106" -inkwell = { path = "../../vendor/inkwell" } -target-lexicon = "0.12.3" -libloading = "0.7.1" -wasmer-wasi = "2.2.1" -tempfile = "3.2.0" -indoc = "1.0.3" - -# Wasmer singlepass compiler only works on x86_64. -[target.'cfg(target_arch = "x86_64")'.dev-dependencies] -wasmer = { version = "2.2.1", default-features = false, features = ["singlepass", "universal"] } - -[target.'cfg(not(target_arch = "x86_64"))'.dev-dependencies] -wasmer = { version = "2.2.1", default-features = false, features = ["cranelift", "universal"] } - -[features] -default = ["gen-llvm"] -gen-llvm = [] -gen-dev = [] -gen-wasm = [] -wasm-cli-run = [] diff --git a/compiler/test_gen/README.md b/compiler/test_gen/README.md deleted file mode 100644 index cbdb841cdd..0000000000 --- a/compiler/test_gen/README.md +++ /dev/null @@ -1,28 +0,0 @@ -# Running our CodeGen tests - -Our code generation tests are all in this crate. Feature flags are used to run the tests with a specific backend. For convenience, some aliases are added in `.cargo/config`: - -```toml -[alias] -test-gen-llvm = "test -p test_gen" -test-gen-dev = "test -p test_gen --no-default-features --features gen-dev" -test-gen-wasm = "test -p test_gen --no-default-features --features gen-wasm" -``` - -So we can run: - -``` -cargo test-gen-llvm -``` - -To run the gen tests with the LLVM backend. To filter tests, append a filter like so: - -``` -> cargo test-gen-wasm wasm_str::small - Finished test [unoptimized + debuginfo] target(s) in 0.13s - Running src/tests.rs (target/debug/deps/test_gen-b4ad63a9dd50f050) - -running 2 tests -test wasm_str::small_str_literal ... ok -test wasm_str::small_str_zeroed_literal ... ok -``` diff --git a/compiler/test_gen/build.rs b/compiler/test_gen/build.rs deleted file mode 100644 index 39e0416945..0000000000 --- a/compiler/test_gen/build.rs +++ /dev/null @@ -1,153 +0,0 @@ -use roc_builtins::bitcode; -use std::env; -use std::ffi::OsStr; -use std::path::{Path, PathBuf}; -use std::process::Command; - -const PLATFORM_FILENAME: &str = "wasm_test_platform"; -const OUT_DIR_VAR: &str = "TEST_GEN_OUT"; - -fn main() { - println!("cargo:rerun-if-changed=build.rs"); - if feature_is_enabled("gen-wasm") { - build_wasm(); - } -} - -fn build_wasm() { - let source_path = format!("src/helpers/{}.c", PLATFORM_FILENAME); - println!("cargo:rerun-if-changed={}", source_path); - - let out_dir = env::var("OUT_DIR").unwrap(); - println!("cargo:rustc-env={}={}", OUT_DIR_VAR, out_dir); - - // Zig can produce *either* an object containing relocations OR an object containing libc code - // But we want both, so we have to compile twice with different flags, then link them - - // Create an object file with relocations - let platform_path = build_wasm_platform(&out_dir, &source_path); - - // Compile again to get libc path - let (libc_path, compiler_rt_path) = build_wasm_libc_compilerrt(&out_dir, &source_path); - let mut libc_pathbuf = PathBuf::from(&libc_path); - libc_pathbuf.pop(); - let libc_dir = libc_pathbuf.to_str().unwrap(); - - let args = &[ - "wasm-ld", - bitcode::BUILTINS_WASM32_OBJ_PATH, - &platform_path, - &compiler_rt_path, - "-L", - libc_dir, - "-lc", - "-o", - &format!("{}/{}.o", out_dir, PLATFORM_FILENAME), - "--no-entry", - "--relocatable", - ]; - - let zig = zig_executable(); - - // println!("{} {}", zig, args.join(" ")); - - run_command(&zig, args); -} - -fn zig_executable() -> String { - match std::env::var("ROC_ZIG") { - Ok(path) => path, - Err(_) => "zig".into(), - } -} - -fn build_wasm_platform(out_dir: &str, source_path: &str) -> String { - let platform_path = format!("{}/{}.o", out_dir, PLATFORM_FILENAME); - - run_command( - &zig_executable(), - &[ - "build-lib", - "-target", - "wasm32-wasi", - "-lc", - source_path, - &format!("-femit-bin={}", &platform_path), - ], - ); - - platform_path -} - -fn build_wasm_libc_compilerrt(out_dir: &str, source_path: &str) -> (String, String) { - let zig_cache_dir = format!("{}/zig-cache-wasm32", out_dir); - - run_command( - &zig_executable(), - &[ - "build-lib", - "-dynamic", // ensure libc code is actually generated (not just linked against header) - "-target", - "wasm32-wasi", - "-lc", - source_path, - "-femit-bin=/dev/null", - "--global-cache-dir", - &zig_cache_dir, - ], - ); - - let libc_path = run_command("find", &[&zig_cache_dir, "-name", "libc.a"]) - .split('\n') - .next() - .unwrap() - .into(); - let compiler_rt_path = run_command("find", &[&zig_cache_dir, "-name", "compiler_rt.o"]) - .split('\n') - .next() - .unwrap() - .into(); - - (libc_path, compiler_rt_path) -} - -fn feature_is_enabled(feature_name: &str) -> bool { - let cargo_env_var = format!( - "CARGO_FEATURE_{}", - feature_name.replace('-', "_").to_uppercase() - ); - env::var(cargo_env_var).is_ok() -} - -fn run_command(command_str: &str, args: &[&str]) -> String { - let output_result = Command::new(OsStr::new(&command_str)) - .current_dir(Path::new(".")) - .args(args) - .output(); - - let fail = |err: String| { - panic!( - "\n\nFailed command:\n\t{} {}\n\n{}", - command_str, - args.join(" "), - err - ); - }; - - match output_result { - Ok(output) => match output.status.success() { - true => std::str::from_utf8(&output.stdout) - .unwrap() - .trim() - .to_string(), - false => { - let error_str = match std::str::from_utf8(&output.stderr) { - Ok(stderr) => stderr.to_string(), - Err(_) => format!("Failed to run \"{}\"", command_str), - }; - fail(error_str) - } - }, - Err(reason) => fail(reason.to_string()), - } -} diff --git a/compiler/test_gen/src/gen_abilities.rs b/compiler/test_gen/src/gen_abilities.rs deleted file mode 100644 index 307008edf2..0000000000 --- a/compiler/test_gen/src/gen_abilities.rs +++ /dev/null @@ -1,325 +0,0 @@ -#[cfg(feature = "gen-llvm")] -use crate::helpers::llvm::assert_evals_to; - -#[cfg(feature = "gen-dev")] -use crate::helpers::dev::assert_evals_to; - -#[cfg(feature = "gen-wasm")] -use crate::helpers::wasm::assert_evals_to; - -#[cfg(test)] -use indoc::indoc; - -#[cfg(all(test, feature = "gen-llvm"))] -use roc_std::RocList; - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn hash_specialization() { - assert_evals_to!( - indoc!( - r#" - app "test" provides [main] to "./platform" - - Hash has - hash : a -> U64 | a has Hash - - Id := U64 - - hash = \@Id n -> n - - main = hash (@Id 1234) - "# - ), - 1234, - u64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn hash_specialization_multiple_add() { - assert_evals_to!( - indoc!( - r#" - app "test" provides [main] to "./platform" - - Hash has - hash : a -> U64 | a has Hash - - Id := U64 - - hash = \@Id n -> n - - One := {} - - hash = \@One _ -> 1 - - main = hash (@Id 1234) + hash (@One {}) - "# - ), - 1235, - u64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn alias_member_specialization() { - assert_evals_to!( - indoc!( - r#" - app "test" provides [main] to "./platform" - - Hash has - hash : a -> U64 | a has Hash - - Id := U64 - - hash = \@Id n -> n - - main = - aliasedHash = hash - aliasedHash (@Id 1234) - "# - ), - 1234, - u64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn ability_constrained_in_non_member_usage() { - assert_evals_to!( - indoc!( - r#" - app "test" provides [result] to "./platform" - - Hash has - hash : a -> U64 | a has Hash - - mulHashes : a, a -> U64 | a has Hash - mulHashes = \x, y -> hash x * hash y - - Id := U64 - hash = \@Id n -> n - - result = mulHashes (@Id 5) (@Id 7) - "# - ), - 35, - u64 - ) -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn ability_constrained_in_non_member_usage_inferred() { - assert_evals_to!( - indoc!( - r#" - app "test" provides [result] to "./platform" - - Hash has - hash : a -> U64 | a has Hash - - mulHashes = \x, y -> hash x * hash y - - Id := U64 - hash = \@Id n -> n - - result = mulHashes (@Id 5) (@Id 7) - "# - ), - 35, - u64 - ) -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn ability_constrained_in_non_member_multiple_specializations() { - assert_evals_to!( - indoc!( - r#" - app "test" provides [result] to "./platform" - - Hash has - hash : a -> U64 | a has Hash - - mulHashes : a, b -> U64 | a has Hash, b has Hash - mulHashes = \x, y -> hash x * hash y - - Id := U64 - hash = \@Id n -> n - - Three := {} - hash = \@Three _ -> 3 - - result = mulHashes (@Id 100) (@Three {}) - "# - ), - 300, - u64 - ) -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn ability_constrained_in_non_member_multiple_specializations_inferred() { - assert_evals_to!( - indoc!( - r#" - app "test" provides [result] to "./platform" - - Hash has - hash : a -> U64 | a has Hash - - mulHashes = \x, y -> hash x * hash y - - Id := U64 - hash = \@Id n -> n - - Three := {} - hash = \@Three _ -> 3 - - result = mulHashes (@Id 100) (@Three {}) - "# - ), - 300, - u64 - ) -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn ability_used_as_type_still_compiles() { - assert_evals_to!( - indoc!( - r#" - app "test" provides [result] to "./platform" - - Hash has - hash : a -> U64 | a has Hash - - mulHashes : Hash, Hash -> U64 - mulHashes = \x, y -> hash x * hash y - - Id := U64 - hash = \@Id n -> n - - Three := {} - hash = \@Three _ -> 3 - - result = mulHashes (@Id 100) (@Three {}) - "# - ), - 300, - u64 - ) -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn encode() { - assert_evals_to!( - indoc!( - r#" - app "test" provides [myU8Bytes] to "./platform" - - Encoder fmt := List U8, fmt -> List U8 | fmt has Format - - Encoding has - toEncoder : val -> Encoder fmt | val has Encoding, fmt has Format - - Format has - u8 : U8 -> Encoder fmt | fmt has Format - - appendWith : List U8, Encoder fmt, fmt -> List U8 | fmt has Format - appendWith = \lst, (@Encoder doFormat), fmt -> doFormat lst fmt - - toBytes : val, fmt -> List U8 | val has Encoding, fmt has Format - toBytes = \val, fmt -> appendWith [] (toEncoder val) fmt - - - Linear := {} - - # impl Format for Linear - u8 = \n -> @Encoder (\lst, @Linear {} -> List.append lst n) - - Rgba := { r : U8, g : U8, b : U8, a : U8 } - - # impl Encoding for Rgba - toEncoder = \@Rgba {r, g, b, a} -> - @Encoder \lst, fmt -> lst - |> appendWith (u8 r) fmt - |> appendWith (u8 g) fmt - |> appendWith (u8 b) fmt - |> appendWith (u8 a) fmt - - myU8Bytes = toBytes (@Rgba { r: 106, g: 90, b: 205, a: 255 }) (@Linear {}) - "# - ), - RocList::from_slice(&[106, 90, 205, 255]), - RocList - ) -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn decode() { - assert_evals_to!( - indoc!( - r#" - app "test" provides [myU8] to "./platform" - - DecodeError : [TooShort, Leftover (List U8)] - - Decoder val fmt := List U8, fmt -> { result: Result val DecodeError, rest: List U8 } | fmt has DecoderFormatting - - Decoding has - decoder : Decoder val fmt | val has Decoding, fmt has DecoderFormatting - - DecoderFormatting has - u8 : Decoder U8 fmt | fmt has DecoderFormatting - - decodeWith : List U8, Decoder val fmt, fmt -> { result: Result val DecodeError, rest: List U8 } | fmt has DecoderFormatting - decodeWith = \lst, (@Decoder doDecode), fmt -> doDecode lst fmt - - fromBytes : List U8, fmt -> Result val DecodeError - | fmt has DecoderFormatting, val has Decoding - fromBytes = \lst, fmt -> - when decodeWith lst decoder fmt is - { result, rest } -> - Result.after result \val -> - if List.isEmpty rest - then Ok val - else Err (Leftover rest) - - - Linear := {} - - # impl DecoderFormatting for Linear - u8 = @Decoder \lst, @Linear {} -> - when List.first lst is - Ok n -> { result: Ok n, rest: List.dropFirst lst } - Err _ -> { result: Err TooShort, rest: [] } - - MyU8 := U8 - - # impl Decoding for MyU8 - decoder = @Decoder \lst, fmt -> - { result, rest } = decodeWith lst u8 fmt - { result: Result.map result (\n -> @MyU8 n), rest } - - myU8 = - when fromBytes [15] (@Linear {}) is - Ok (@MyU8 n) -> n - _ -> 27u8 - "# - ), - 15, - u8 - ); -} diff --git a/compiler/test_gen/src/gen_dict.rs b/compiler/test_gen/src/gen_dict.rs deleted file mode 100644 index 3628248d10..0000000000 --- a/compiler/test_gen/src/gen_dict.rs +++ /dev/null @@ -1,545 +0,0 @@ -#![cfg(feature = "gen-llvm")] - -#[cfg(feature = "gen-llvm")] -use crate::helpers::llvm::assert_evals_to; - -// #[cfg(feature = "gen-dev")] -// use crate::helpers::dev::assert_evals_to; - -// #[cfg(feature = "gen-wasm")] -// use crate::helpers::wasm::assert_evals_to; - -use indoc::indoc; -use roc_std::{RocList, RocStr}; - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn dict_empty_len() { - assert_evals_to!( - indoc!( - r#" - Dict.len Dict.empty - "# - ), - 0, - usize - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn dict_insert_empty() { - assert_evals_to!( - indoc!( - r#" - Dict.insert Dict.empty 42 32 - |> Dict.len - "# - ), - 1, - usize - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn dict_empty_contains() { - assert_evals_to!( - indoc!( - r#" - empty : Dict I64 F64 - empty = Dict.empty - - Dict.contains empty 42 - "# - ), - false, - bool - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn dict_nonempty_contains() { - assert_evals_to!( - indoc!( - r#" - empty : Dict I64 F64 - empty = Dict.insert Dict.empty 42 1.23 - - Dict.contains empty 42 - "# - ), - true, - bool - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn dict_empty_remove() { - assert_evals_to!( - indoc!( - r#" - empty : Dict I64 F64 - empty = Dict.empty - - empty - |> Dict.remove 42 - |> Dict.len - "# - ), - 0, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn dict_nonempty_remove() { - assert_evals_to!( - indoc!( - r#" - empty : Dict I64 F64 - empty = Dict.insert Dict.empty 42 1.23 - - empty - |> Dict.remove 42 - |> Dict.len - "# - ), - 0, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn dict_nonempty_get() { - assert_evals_to!( - indoc!( - r#" - empty : Dict I64 F64 - empty = Dict.insert Dict.empty 42 1.23 - - withDefault = \x, def -> - when x is - Ok v -> v - Err _ -> def - - empty - |> Dict.insert 42 1.23 - |> Dict.get 42 - |> withDefault 0 - "# - ), - 1.23, - f64 - ); - - assert_evals_to!( - indoc!( - r#" - withDefault = \x, def -> - when x is - Ok v -> v - Err _ -> def - - Dict.empty - |> Dict.insert 42 1.23 - |> Dict.get 43 - |> withDefault 0 - "# - ), - 0.0, - f64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn keys() { - assert_evals_to!( - indoc!( - r#" - myDict : Dict I64 I64 - myDict = - Dict.empty - |> Dict.insert 0 100 - |> Dict.insert 1 100 - |> Dict.insert 2 100 - - - Dict.keys myDict - "# - ), - RocList::from_slice(&[0, 1, 2]), - RocList - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn values() { - assert_evals_to!( - indoc!( - r#" - myDict : Dict I64 I64 - myDict = - Dict.empty - |> Dict.insert 0 100 - |> Dict.insert 1 200 - |> Dict.insert 2 300 - - - Dict.values myDict - "# - ), - RocList::from_slice(&[100, 200, 300]), - RocList - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn from_list_with_fold_simple() { - assert_evals_to!( - indoc!( - r#" - myDict : Dict I64 I64 - myDict = - [1,2,3] - |> List.walk Dict.empty (\accum, value -> Dict.insert accum value value) - - Dict.values myDict - "# - ), - RocList::from_slice(&[2, 3, 1]), - RocList - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn from_list_with_fold_reallocates() { - assert_evals_to!( - indoc!( - r#" - range : I64, I64, List I64-> List I64 - range = \low, high, accum -> - if low < high then - range (low + 1) high (List.append accum low) - else - accum - - myDict : Dict I64 I64 - myDict = - # 25 elements (8 + 16 + 1) is guaranteed to overflow/reallocate at least twice - range 0 25 [] - |> List.walk Dict.empty (\accum, value -> Dict.insert accum value value) - - Dict.values myDict - "# - ), - RocList::from_slice(&[ - 4, 5, 20, 0, 7, 3, 1, 21, 10, 6, 13, 9, 14, 19, 2, 15, 12, 17, 16, 18, 22, 8, 11, 24, - 23 - ]), - RocList - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn small_str_keys() { - assert_evals_to!( - indoc!( - r#" - myDict : Dict Str I64 - myDict = - Dict.empty - |> Dict.insert "a" 100 - |> Dict.insert "b" 100 - |> Dict.insert "c" 100 - - - Dict.keys myDict - "# - ), - RocList::from_slice(&[RocStr::from("c"), RocStr::from("a"), RocStr::from("b"),],), - RocList - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn big_str_keys() { - assert_evals_to!( - indoc!( - r#" - myDict : Dict Str I64 - myDict = - Dict.empty - |> Dict.insert "Leverage agile frameworks to provide a robust" 100 - |> Dict.insert "synopsis for high level overviews. Iterative approaches" 200 - |> Dict.insert "to corporate strategy foster collaborative thinking to" 300 - - Dict.keys myDict - "# - ), - RocList::from_slice(&[ - RocStr::from("Leverage agile frameworks to provide a robust"), - RocStr::from("to corporate strategy foster collaborative thinking to"), - RocStr::from("synopsis for high level overviews. Iterative approaches"), - ]), - RocList - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn big_str_values() { - assert_evals_to!( - indoc!( - r#" - myDict : Dict I64 Str - myDict = - Dict.empty - |> Dict.insert 100 "Leverage agile frameworks to provide a robust" - |> Dict.insert 200 "synopsis for high level overviews. Iterative approaches" - |> Dict.insert 300 "to corporate strategy foster collaborative thinking to" - - Dict.values myDict - "# - ), - RocList::from_slice(&[ - RocStr::from("Leverage agile frameworks to provide a robust"), - RocStr::from("to corporate strategy foster collaborative thinking to"), - RocStr::from("synopsis for high level overviews. Iterative approaches"), - ]), - RocList - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn unit_values() { - assert_evals_to!( - indoc!( - r#" - myDict : Dict I64 {} - myDict = - Dict.empty - |> Dict.insert 0 {} - |> Dict.insert 1 {} - |> Dict.insert 2 {} - |> Dict.insert 3 {} - - Dict.len myDict - "# - ), - 4, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn single() { - assert_evals_to!( - indoc!( - r#" - myDict : Dict I64 {} - myDict = - Dict.single 0 {} - - Dict.len myDict - "# - ), - 1, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn union() { - assert_evals_to!( - indoc!( - r#" - myDict : Dict I64 {} - myDict = - Dict.union (Dict.single 0 {}) (Dict.single 1 {}) - - Dict.len myDict - "# - ), - 2, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn union_prefer_first() { - assert_evals_to!( - indoc!( - r#" - myDict : Dict I64 I64 - myDict = - Dict.union (Dict.single 0 100) (Dict.single 0 200) - - Dict.values myDict - "# - ), - RocList::from_slice(&[100]), - RocList - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn intersection() { - assert_evals_to!( - indoc!( - r#" - dict1 : Dict I64 {} - dict1 = - Dict.empty - |> Dict.insert 1 {} - |> Dict.insert 2 {} - |> Dict.insert 3 {} - |> Dict.insert 4 {} - |> Dict.insert 5 {} - - dict2 : Dict I64 {} - dict2 = - Dict.empty - |> Dict.insert 0 {} - |> Dict.insert 2 {} - |> Dict.insert 4 {} - - Dict.intersection dict1 dict2 - |> Dict.len - "# - ), - 2, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn intersection_prefer_first() { - assert_evals_to!( - indoc!( - r#" - dict1 : Dict I64 I64 - dict1 = - Dict.empty - |> Dict.insert 1 1 - |> Dict.insert 2 2 - |> Dict.insert 3 3 - |> Dict.insert 4 4 - |> Dict.insert 5 5 - - dict2 : Dict I64 I64 - dict2 = - Dict.empty - |> Dict.insert 0 100 - |> Dict.insert 2 200 - |> Dict.insert 4 300 - - Dict.intersection dict1 dict2 - |> Dict.values - "# - ), - RocList::from_slice(&[4, 2]), - RocList - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn difference() { - assert_evals_to!( - indoc!( - r#" - dict1 : Dict I64 {} - dict1 = - Dict.empty - |> Dict.insert 1 {} - |> Dict.insert 2 {} - |> Dict.insert 3 {} - |> Dict.insert 4 {} - |> Dict.insert 5 {} - - dict2 : Dict I64 {} - dict2 = - Dict.empty - |> Dict.insert 0 {} - |> Dict.insert 2 {} - |> Dict.insert 4 {} - - Dict.difference dict1 dict2 - |> Dict.len - "# - ), - 3, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn difference_prefer_first() { - assert_evals_to!( - indoc!( - r#" - dict1 : Dict I64 I64 - dict1 = - Dict.empty - |> Dict.insert 1 1 - |> Dict.insert 2 2 - |> Dict.insert 3 3 - |> Dict.insert 4 4 - |> Dict.insert 5 5 - - dict2 : Dict I64 I64 - dict2 = - Dict.empty - |> Dict.insert 0 100 - |> Dict.insert 2 200 - |> Dict.insert 4 300 - - Dict.difference dict1 dict2 - |> Dict.values - "# - ), - RocList::from_slice(&[5, 3, 1]), - RocList - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn walk_sum_keys() { - assert_evals_to!( - indoc!( - r#" - dict1 : Dict I64 I64 - dict1 = - Dict.empty - |> Dict.insert 1 1 - |> Dict.insert 2 2 - |> Dict.insert 3 3 - |> Dict.insert 4 4 - |> Dict.insert 5 5 - - Dict.walk dict1 0 \k, _, a -> k + a - "# - ), - 15, - i64 - ); -} diff --git a/compiler/test_gen/src/gen_list.rs b/compiler/test_gen/src/gen_list.rs deleted file mode 100644 index 30dd6b73a6..0000000000 --- a/compiler/test_gen/src/gen_list.rs +++ /dev/null @@ -1,2898 +0,0 @@ -#[cfg(feature = "gen-llvm")] -use crate::helpers::llvm::assert_evals_to; - -#[cfg(feature = "gen-llvm")] -use crate::helpers::llvm::expect_runtime_error_panic; - -// #[cfg(feature = "gen-dev")] -// use crate::helpers::dev::assert_evals_to; - -#[cfg(feature = "gen-wasm")] -use crate::helpers::wasm::assert_evals_to; - -#[allow(unused_imports)] -use crate::helpers::with_larger_debug_stack; -//use crate::assert_wasm_evals_to as assert_evals_to; -#[allow(unused_imports)] -use indoc::indoc; -#[allow(unused_imports)] -use roc_std::{RocList, RocStr}; - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] -fn roc_list_construction() { - let list = RocList::from_slice(&[1i64; 23]); - assert_eq!(&list, &list); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn empty_list_literal() { - assert_evals_to!("[]", RocList::::from_slice(&[]), RocList); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn list_literal_empty_record() { - assert_evals_to!("[{}]", RocList::from_slice(&[()]), RocList<()>); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn int_singleton_list_literal() { - assert_evals_to!("[1, 2]", RocList::from_slice(&[1, 2]), RocList); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn int_list_literal() { - assert_evals_to!("[12, 9]", RocList::from_slice(&[12, 9]), RocList); - assert_evals_to!( - "[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1]", - RocList::from_slice(&[1i64; 23]), - RocList - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn bool_list_literal() { - // NOTE: make sure to explicitly declare the elements to be of type bool, or - // use both True and False; only using one of them causes the list to in practice be - // of type `List [True]` or `List [False]`, those are tag unions with one constructor - // and not fields, and don't have a runtime representation. - assert_evals_to!( - indoc!( - r#" - false : Bool - false = False - - [false] - "# - ), - RocList::from_slice(&[false; 1]), - RocList - ); - - assert_evals_to!( - "[True, False, True]", - RocList::from_slice(&[true, false, true]), - RocList - ); - - assert_evals_to!( - indoc!( - r#" - false : Bool - false = False - - [false] - "# - ), - RocList::from_slice(&[false; 1]), - RocList - ); - - assert_evals_to!( - indoc!( - r#" - true : Bool - true = True - - List.repeat true 23 - "# - ), - RocList::from_slice(&[true; 23]), - RocList - ); - - assert_evals_to!( - indoc!( - r#" - true : Bool - true = True - - List.repeat { x: true, y: true } 23 - "# - ), - RocList::from_slice(&[[true, true]; 23]), - RocList<[bool; 2]> - ); - - assert_evals_to!( - indoc!( - r#" - true : Bool - true = True - - List.repeat { x: true, y: true, a: true, b: true, c: true, d : true, e: true, f: true } 23 - "# - ), - RocList::from_slice(&[[true, true, true, true, true, true, true, true]; 23]), - RocList<[bool; 8]> - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn variously_sized_list_literals() { - assert_evals_to!("[]", RocList::::from_slice(&[]), RocList); - assert_evals_to!("[1]", RocList::from_slice(&[1]), RocList); - assert_evals_to!("[1, 2]", RocList::from_slice(&[1, 2]), RocList); - assert_evals_to!("[1, 2, 3]", RocList::from_slice(&[1, 2, 3]), RocList); - assert_evals_to!( - "[1, 2, 3, 4]", - RocList::from_slice(&[1, 2, 3, 4]), - RocList - ); - assert_evals_to!( - "[1, 2, 3, 4, 5]", - RocList::from_slice(&[1, 2, 3, 4, 5]), - RocList - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn list_append() { - assert_evals_to!( - "List.append [1] 2", - RocList::from_slice(&[1, 2]), - RocList - ); - assert_evals_to!( - "List.append [1, 1] 2", - RocList::from_slice(&[1, 1, 2]), - RocList - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn list_take_first() { - assert_evals_to!( - "List.takeFirst [1, 2, 3] 2", - RocList::from_slice(&[1, 2]), - RocList - ); - assert_evals_to!( - "List.takeFirst [1, 2, 3] 0", - RocList::::from_slice(&[]), - RocList - ); - assert_evals_to!( - "List.takeFirst [] 1", - RocList::::from_slice(&[]), - RocList - ); - assert_evals_to!( - "List.takeFirst [1,2] 5", - RocList::from_slice(&[1, 2]), - RocList - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn list_take_last() { - assert_evals_to!( - "List.takeLast [1, 2, 3] 2", - RocList::from_slice(&[2, 3]), - RocList - ); - assert_evals_to!( - "List.takeLast [1, 2, 3] 0", - RocList::::from_slice(&[]), - RocList - ); - assert_evals_to!( - "List.takeLast [] 1", - RocList::::from_slice(&[]), - RocList - ); - assert_evals_to!( - "List.takeLast [1,2] 5", - RocList::from_slice(&[1, 2]), - RocList - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn list_sublist() { - assert_evals_to!( - "List.sublist [1, 2, 3] { start: 0 , len: 2 } ", - RocList::from_slice(&[1, 2]), - RocList - ); - assert_evals_to!( - "List.sublist [1, 2, 3] { start: 1 , len: 2 } ", - RocList::from_slice(&[2, 3]), - RocList - ); - assert_evals_to!( - "List.sublist [1, 2, 3] { start: 2 , len: 2 } ", - RocList::from_slice(&[3]), - RocList - ); - assert_evals_to!( - "List.sublist [1, 2, 3] { start: 3 , len: 2 } ", - RocList::::from_slice(&[]), - RocList - ); - assert_evals_to!( - "List.sublist [] { start: 1 , len: 1 } ", - RocList::::from_slice(&[]), - RocList - ); - assert_evals_to!( - "List.sublist [1, 2, 3] { start: 1 , len: 0 } ", - RocList::::from_slice(&[]), - RocList - ); - assert_evals_to!( - "List.sublist [1, 2, 3] { start: 0 , len: 5 } ", - RocList::from_slice(&[1, 2, 3]), - RocList - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn list_split() { - assert_evals_to!( - r#" - list = List.split [1, 2, 3] 0 - list.before - "#, - RocList::::from_slice(&[]), - RocList - ); - assert_evals_to!( - r#" - list = List.split [1, 2, 3] 0 - list.others - "#, - RocList::from_slice(&[1, 2, 3]), - RocList - ); - - assert_evals_to!( - "List.split [1, 2, 3] 1", - (RocList::from_slice(&[1]), RocList::from_slice(&[2, 3]),), - (RocList, RocList,) - ); - assert_evals_to!( - "List.split [1, 2, 3] 3", - ( - RocList::from_slice(&[1, 2, 3]), - RocList::::from_slice(&[]), - ), - (RocList, RocList,) - ); - assert_evals_to!( - "List.split [1, 2, 3] 4", - ( - RocList::from_slice(&[1, 2, 3]), - RocList::::from_slice(&[]), - ), - (RocList, RocList,) - ); - assert_evals_to!( - "List.split [] 1", - ( - RocList::::from_slice(&[]), - RocList::::from_slice(&[]), - ), - (RocList, RocList,) - ); -} -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn list_drop() { - assert_evals_to!( - "List.drop [1,2,3] 2", - RocList::from_slice(&[3]), - RocList - ); - assert_evals_to!( - "List.drop [] 1", - RocList::::from_slice(&[]), - RocList - ); - assert_evals_to!( - "List.drop [1,2] 5", - RocList::::from_slice(&[]), - RocList - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn list_drop_at() { - assert_evals_to!( - "List.dropAt [1, 2, 3] 0", - RocList::from_slice(&[2, 3]), - RocList - ); - assert_evals_to!( - "List.dropAt [0, 0, 0] 3", - RocList::from_slice(&[0, 0, 0]), - RocList - ); - assert_evals_to!( - "List.dropAt [] 1", - RocList::::from_slice(&[]), - RocList - ); - assert_evals_to!( - "List.dropAt [0] 0", - RocList::::from_slice(&[]), - RocList - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn list_intersperse() { - assert_evals_to!( - indoc!( - r#" - List.intersperse [0, 0, 0] 1 - "# - ), - RocList::from_slice(&[0, 1, 0, 1, 0]), - RocList - ); - assert_evals_to!( - indoc!( - r#" - List.intersperse [] 1 - "# - ), - RocList::::from_slice(&[]), - RocList - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn list_drop_at_shared() { - assert_evals_to!( - indoc!( - r#" - list : List I64 - list = [if True then 4 else 4, 5, 6] - - { newList: List.dropAt list 0, original: list } - "# - ), - ( - // new_list - RocList::from_slice(&[5, 6]), - // original - RocList::from_slice(&[4, 5, 6]), - ), - (RocList, RocList,) - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn list_drop_if_empty_list_of_int() { - assert_evals_to!( - indoc!( - r#" - empty : List I64 - empty = [] - - List.dropIf empty \_ -> True - "# - ), - RocList::::from_slice(&[]), - RocList - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn list_drop_if_empty_list() { - assert_evals_to!( - indoc!( - r#" - alwaysTrue : I64 -> Bool - alwaysTrue = \_ -> True - - List.dropIf [] alwaysTrue - "# - ), - RocList::::from_slice(&[]), - RocList - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn list_drop_if_always_false_for_non_empty_list() { - assert_evals_to!( - indoc!( - r#" - List.dropIf [1,2,3,4,5,6,7,8] (\_ -> False) - "# - ), - RocList::from_slice(&[1, 2, 3, 4, 5, 6, 7, 8]), - RocList - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn list_drop_if_always_true_for_non_empty_list() { - assert_evals_to!( - indoc!( - r#" - List.dropIf [1,2,3,4,5,6,7,8] (\_ -> True) - "# - ), - RocList::::from_slice(&[]), - RocList - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn list_drop_if_geq3() { - assert_evals_to!( - indoc!( - r#" - List.dropIf [1,2,3,4,5,6,7,8] (\n -> n >= 3) - "# - ), - RocList::from_slice(&[1, 2]), - RocList - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn list_drop_if_string_eq() { - assert_evals_to!( - indoc!( - r#" - List.dropIf ["x", "y", "x"] (\s -> s == "y") - "# - ), - RocList::from_slice(&[RocStr::from("x"), RocStr::from("x")]), - RocList - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn list_drop_last() { - assert_evals_to!( - "List.dropLast [1, 2, 3]", - RocList::from_slice(&[1, 2]), - RocList - ); - assert_evals_to!( - "List.dropLast []", - RocList::::from_slice(&[]), - RocList - ); - assert_evals_to!( - "List.dropLast [0]", - RocList::::from_slice(&[]), - RocList - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn list_drop_last_mutable() { - assert_evals_to!( - indoc!( - r#" - list : List I64 - list = [if True then 4 else 4, 5, 6] - - { newList: List.dropLast list, original: list } - "# - ), - ( - // new_list - RocList::from_slice(&[4, 5]), - // original - RocList::from_slice(&[4, 5, 6]), - ), - (RocList, RocList,) - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn list_drop_first() { - assert_evals_to!( - "List.dropFirst [1, 2, 3]", - RocList::from_slice(&[2, 3]), - RocList - ); - assert_evals_to!( - "List.dropFirst []", - RocList::::from_slice(&[]), - RocList - ); - assert_evals_to!( - "List.dropFirst [0]", - RocList::::from_slice(&[]), - RocList - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn list_swap() { - assert_evals_to!( - "List.swap [] 0 1", - RocList::::from_slice(&[]), - RocList - ); - assert_evals_to!("List.swap [0] 1 2", RocList::from_slice(&[0]), RocList); - assert_evals_to!( - "List.swap [1, 2] 0 1", - RocList::from_slice(&[2, 1]), - RocList - ); - assert_evals_to!( - "List.swap [1, 2] 1 0", - RocList::from_slice(&[2, 1]), - RocList - ); - assert_evals_to!( - "List.swap [0, 1, 2, 3, 4, 5] 2 4", - RocList::from_slice(&[0, 1, 4, 3, 2, 5]), - RocList - ); - assert_evals_to!( - "List.swap [0, 1, 2] 1 3", - RocList::from_slice(&[0, 1, 2]), - RocList - ); - assert_evals_to!( - "List.swap [1, 2, 3] 1 1", - RocList::from_slice(&[1, 2, 3]), - RocList - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn list_append_to_empty_list() { - assert_evals_to!("List.append [] 3", RocList::from_slice(&[3]), RocList); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn list_append_to_empty_list_of_int() { - assert_evals_to!( - indoc!( - r#" - initThrees : List I64 - initThrees = - [] - - List.append (List.append initThrees 3) 3 - "# - ), - RocList::from_slice(&[3, 3]), - RocList - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn list_append_bools() { - assert_evals_to!( - "List.append [True, False] True", - RocList::from_slice(&[true, false, true]), - RocList - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn list_append_longer_list() { - assert_evals_to!( - "List.append [11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22] 23", - RocList::from_slice(&[11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23]), - RocList - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn list_prepend() { - assert_evals_to!("List.prepend [] 1", RocList::from_slice(&[1]), RocList); - assert_evals_to!( - "List.prepend [2] 1", - RocList::from_slice(&[1, 2]), - RocList - ); - - assert_evals_to!( - indoc!( - r#" - init : List I64 - init = - [] - - List.prepend (List.prepend init 4) 6 - "# - ), - RocList::from_slice(&[6, 4]), - RocList - ); - - assert_evals_to!( - indoc!( - r#" - init : List Str - init = - ["foo"] - - List.prepend init "bar" - "# - ), - RocList::from_slice(&[RocStr::from("bar"), RocStr::from("foo"),]), - RocList - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn list_prepend_bools() { - assert_evals_to!( - "List.prepend [True, False] True", - RocList::from_slice(&[true, true, false]), - RocList - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn list_prepend_big_list() { - assert_evals_to!( - "List.prepend [10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 100, 100, 100, 100] 9", - RocList::from_slice(&[ - 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 100, 100, 100, 100 - ]), - RocList - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn list_walk_backwards_empty_all_inline() { - assert_evals_to!( - indoc!( - r#" - List.walkBackwards [0x1] 0 \state, elem -> state + elem - "# - ), - 1, - i64 - ); - - assert_evals_to!( - indoc!( - r#" - empty : List I64 - empty = - [] - - List.walkBackwards empty 0 \state, elem -> state + elem - "# - ), - 0, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn list_walk_backwards_with_str() { - assert_evals_to!( - r#"List.walkBackwards ["x", "y", "z"] "<" Str.concat"#, - RocStr::from(" - when b is - Zero -> { r & zeroes: r.zeroes + 1 } - One -> { r & ones: r.ones + 1 } - - finalCounts = List.walkBackwards byte initialCounts acc - - finalCounts.ones * 10 + finalCounts.zeroes - "# - ), - 35, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn list_walk_with_str() { - assert_evals_to!( - r#"List.walk ["x", "y", "z"] "<" Str.concat"#, - RocStr::from(" Continue (a + b)"#, - 3, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn list_walk_implements_position() { - assert_evals_to!( - r#" - Option a : [Some a, None] - - find : List a, a -> Option Nat - find = \list, needle -> - findHelp list needle - |> .v - - findHelp = \list, needle -> - List.walkUntil list { n: 0, v: None } \{ n, v }, element -> - if element == needle then - Stop { n, v: Some n } - else - Continue { n: n + 1, v } - - when find [1, 2, 3] 3 is - None -> 0 - Some v -> v - "#, - 2, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn list_walk_until_even_prefix_sum() { - assert_evals_to!( - r#" - helper = \a, b -> - if Num.isEven b then - Continue (a + b) - - else - Stop a - - List.walkUntil [2, 4, 8, 9] 0 helper"#, - 2 + 4 + 8, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn list_keep_if_empty_list_of_int() { - assert_evals_to!( - indoc!( - r#" - empty : List I64 - empty = - [] - - List.keepIf empty \_ -> True - "# - ), - RocList::::from_slice(&[]), - RocList - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn list_keep_if_empty_list() { - assert_evals_to!( - indoc!( - r#" - alwaysTrue : I64 -> Bool - alwaysTrue = \_ -> - True - - - List.keepIf [] alwaysTrue - "# - ), - RocList::::from_slice(&[]), - RocList - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn list_keep_if_always_true_for_non_empty_list() { - assert_evals_to!( - indoc!( - r#" - alwaysTrue : I64 -> Bool - alwaysTrue = \_ -> - True - - oneThroughEight : List I64 - oneThroughEight = - [1,2,3,4,5,6,7,8] - - List.keepIf oneThroughEight alwaysTrue - "# - ), - RocList::from_slice(&[1, 2, 3, 4, 5, 6, 7, 8]), - RocList - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn list_keep_if_always_false_for_non_empty_list() { - assert_evals_to!( - indoc!( - r#" - alwaysFalse : I64 -> Bool - alwaysFalse = \_ -> - False - - List.keepIf [1,2,3,4,5,6,7,8] alwaysFalse - "# - ), - RocList::::from_slice(&[]), - RocList - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn list_keep_if_one() { - assert_evals_to!( - indoc!( - r#" - intIsLessThanThree : I64 -> Bool - intIsLessThanThree = \i -> - i < 3 - - List.keepIf [1,2,3,4,5,6,7,8] intIsLessThanThree - "# - ), - RocList::from_slice(&[1, 2]), - RocList - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn list_keep_if_str_is_hello() { - assert_evals_to!( - indoc!( - r#" - List.keepIf ["x", "y", "x"] (\x -> x == "x") - "# - ), - RocList::from_slice(&[RocStr::from("x"), RocStr::from("x")]), - RocList - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn list_map_on_empty_list_with_int_layout() { - assert_evals_to!( - indoc!( - r#" - empty : List I64 - empty = - [] - - List.map empty (\x -> x) - "# - ), - RocList::::from_slice(&[]), - RocList - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn list_map_on_non_empty_list() { - assert_evals_to!( - indoc!( - r#" - nonEmpty : List I64 - nonEmpty = - [1] - - List.map nonEmpty (\x -> x) - "# - ), - RocList::from_slice(&[1]), - RocList - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn list_map_changes_input() { - assert_evals_to!( - indoc!( - r#" - nonEmpty : List I64 - nonEmpty = - [1] - - List.map nonEmpty (\x -> x + 1) - "# - ), - RocList::from_slice(&[2]), - RocList - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn list_map_on_big_list() { - assert_evals_to!( - indoc!( - r#" - nonEmpty : List I64 - nonEmpty = - [1, 2, 3, 4, 5, 1, 2, 3, 4, 5, 1, 2, 3, 4, 5, 1, 2, 3, 4, 5, 1, 2, 3, 4, 5] - - List.map nonEmpty (\x -> x * 2) - "# - ), - RocList::from_slice(&[ - 2, 4, 6, 8, 10, 2, 4, 6, 8, 10, 2, 4, 6, 8, 10, 2, 4, 6, 8, 10, 2, 4, 6, 8, 10 - ]), - RocList - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn list_map_with_type_change() { - assert_evals_to!( - indoc!( - r#" - nonEmpty : List I64 - nonEmpty = - [1, 1, -4, 1, 2] - - - List.map nonEmpty (\x -> x > 0) - "# - ), - RocList::from_slice(&[true, true, false, true, true]), - RocList - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn list_map_using_defined_function() { - assert_evals_to!( - indoc!( - r#" - nonEmpty : List I64 - nonEmpty = - [2, 2, -4, 2, 3] - - greaterThanOne : I64 -> Bool - greaterThanOne = \i -> - i > 1 - - List.map nonEmpty greaterThanOne - "# - ), - RocList::from_slice(&[true, true, false, true, true]), - RocList - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn list_map_all_inline() { - assert_evals_to!( - indoc!( - r#" - List.map [] (\x -> x > 0) - "# - ), - RocList::::from_slice(&[]), - RocList - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn list_map_closure() { - assert_evals_to!( - indoc!( - r#" - float : F64 - float = 1.23 - - single : List F64 - single = - [0] - - List.map single (\x -> x + float) - "# - ), - RocList::from_slice(&[1.23]), - RocList - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn list_map4_group() { - assert_evals_to!( - indoc!( - r#" - List.map4 [1,2,3] [3,2,1] [2,1,3] [3,1,2] (\a, b, c, d -> Group a b c d) - "# - ), - RocList::from_slice(&[[1, 3, 2, 3], [2, 2, 1, 1], [3, 1, 3, 2]]), - RocList<[i64; 4]> - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn list_map4_different_length() { - assert_evals_to!( - indoc!( - r#" - List.map4 - ["h", "i", "j", "k"] - ["o", "p", "q"] - ["l", "m"] - ["a"] - (\a, b, c, d -> Str.concat a (Str.concat b (Str.concat c d))) - "# - ), - RocList::from_slice(&[RocStr::from("hola"),]), - RocList - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn list_map3_group() { - assert_evals_to!( - indoc!( - r#" - List.map3 [1,2,3] [3,2,1] [2,1,3] (\a, b, c -> Group a b c) - "# - ), - RocList::from_slice(&[(1, 3, 2), (2, 2, 1), (3, 1, 3)]), - RocList<(i64, i64, i64)> - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn list_map3_different_length() { - assert_evals_to!( - indoc!( - r#" - List.map3 - ["a", "b", "d"] - ["b", "x"] - ["c"] - (\a, b, c -> Str.concat a (Str.concat b c)) - "# - ), - RocList::from_slice(&[RocStr::from("abc"),]), - RocList - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn list_map2_pair() { - assert_evals_to!( - indoc!( - r#" - f = (\a,b -> Pair a b) - List.map2 [1,2,3] [3,2,1] f - "# - ), - RocList::from_slice(&[(1, 3), (2, 2), (3, 1)]), - RocList<(i64, i64)> - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn list_map2_different_lengths() { - assert_evals_to!( - indoc!( - r#" - List.map2 - ["a", "b", "lllllllllllllooooooooongnggg"] - ["b"] - (\a, b -> Str.concat a b) - "# - ), - RocList::from_slice(&[RocStr::from("ab"),]), - RocList - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn list_join_empty_list() { - assert_evals_to!( - "List.join []", - RocList::::from_slice(&[]), - RocList - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn list_join_one_list() { - assert_evals_to!( - "List.join [[1, 2, 3]]", - RocList::from_slice(&[1, 2, 3]), - RocList - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn list_join_two_non_empty_lists() { - assert_evals_to!( - "List.join [[1, 2, 3] , [4 ,5, 6]]", - RocList::from_slice(&[1, 2, 3, 4, 5, 6]), - RocList - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn list_join_two_non_empty_lists_of_float() { - assert_evals_to!( - "List.join [[1.2, 1.1], [2.1, 2.2]]", - RocList::from_slice(&[1.2, 1.1, 2.1, 2.2]), - RocList - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn list_join_to_big_list() { - assert_evals_to!( - indoc!( - r#" - List.join - [ - [1.2, 1.1], - [2.1, 2.2], - [3.0, 4.0, 5.0, 6.1, 9.0], - [3.0, 4.0, 5.0, 6.1, 9.0], - [3.0, 4.0, 5.0, 6.1, 9.0], - [3.0, 4.0, 5.0, 6.1, 9.0], - [3.0, 4.0, 5.0, 6.1, 9.0] - ] - "# - ), - RocList::from_slice(&[ - 1.2, 1.1, 2.1, 2.2, 3.0, 4.0, 5.0, 6.1, 9.0, 3.0, 4.0, 5.0, 6.1, 9.0, 3.0, 4.0, 5.0, - 6.1, 9.0, 3.0, 4.0, 5.0, 6.1, 9.0, 3.0, 4.0, 5.0, 6.1, 9.0 - ]), - RocList - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn list_join_defined_empty_list() { - assert_evals_to!( - indoc!( - r#" - empty : List F64 - empty = - [] - - List.join [[0.2, 11.11], empty] - "# - ), - RocList::from_slice(&[0.2, 11.11]), - RocList - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn list_join_all_empty_lists() { - assert_evals_to!( - "List.join [[], [], []]", - RocList::::from_slice(&[]), - RocList - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn list_join_one_empty_list() { - assert_evals_to!( - "List.join [[1.2, 1.1], []]", - RocList::from_slice(&[1.2, 1.1]), - RocList - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn list_single() { - assert_evals_to!("List.single 1", RocList::from_slice(&[1]), RocList); - assert_evals_to!("List.single 5.6", RocList::from_slice(&[5.6]), RocList); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn list_repeat() { - assert_evals_to!( - "List.repeat 1 5", - RocList::from_slice(&[1, 1, 1, 1, 1]), - RocList - ); - assert_evals_to!( - "List.repeat 2 4", - RocList::from_slice(&[2, 2, 2, 2]), - RocList - ); - - assert_evals_to!( - "List.repeat [] 2", - RocList::from_slice(&[RocList::::default(), RocList::default()]), - RocList> - ); - - assert_evals_to!( - indoc!( - r#" - noStrs : List Str - noStrs = - [] - - List.repeat noStrs 2 - "# - ), - RocList::from_slice(&[RocList::::default(), RocList::default()]), - RocList> - ); - - assert_evals_to!( - "List.repeat 4 15", - RocList::from_slice(&[4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4]), - RocList - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn list_reverse() { - assert_evals_to!( - "List.reverse [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]", - RocList::from_slice(&[12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1]), - RocList - ); - assert_evals_to!( - "List.reverse [1, 2, 3]", - RocList::from_slice(&[3, 2, 1]), - RocList - ); - assert_evals_to!("List.reverse [4]", RocList::from_slice(&[4]), RocList); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn list_reverse_empty_list_of_int() { - assert_evals_to!( - indoc!( - r#" - emptyList : List I64 - emptyList = - [] - - List.reverse emptyList - "# - ), - RocList::::from_slice(&[]), - RocList - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn list_reverse_empty_list() { - assert_evals_to!( - "List.reverse []", - RocList::::from_slice(&[]), - RocList - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn list_concat() { - assert_evals_to!( - indoc!( - r#" - firstList : List I64 - firstList = - [] - - secondList : List I64 - secondList = - [] - - List.concat firstList secondList - "# - ), - RocList::::from_slice(&[]), - RocList - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn list_concat_two_empty_lists() { - assert_evals_to!( - "List.concat [] []", - RocList::::from_slice(&[]), - RocList - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn list_concat_two_empty_lists_of_int() { - assert_evals_to!( - indoc!( - r#" - firstList : List I64 - firstList = - [] - - secondList : List I64 - secondList = - [] - - List.concat firstList secondList - "# - ), - RocList::::from_slice(&[]), - RocList - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn list_concat_second_list_is_empty() { - assert_evals_to!( - "List.concat [12, 13] []", - RocList::from_slice(&[12, 13]), - RocList - ); - assert_evals_to!( - "List.concat [34, 43] [64, 55, 66]", - RocList::from_slice(&[34, 43, 64, 55, 66]), - RocList - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn list_concat_first_list_is_empty() { - assert_evals_to!( - "List.concat [] [23, 24]", - RocList::from_slice(&[23, 24]), - RocList - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn list_concat_two_non_empty_lists() { - assert_evals_to!( - "List.concat [1, 2] [3, 4]", - RocList::from_slice(&[1, 2, 3, 4]), - RocList - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn list_concat_two_bigger_non_empty_lists() { - assert_evals_to!( - "List.concat [1.1, 2.2] [3.3, 4.4, 5.5]", - RocList::from_slice(&[1.1, 2.2, 3.3, 4.4, 5.5]), - RocList - ); -} - -#[allow(dead_code)] -#[cfg(any(feature = "gen-llvm"))] -fn assert_concat_worked(num_elems1: i64, num_elems2: i64) { - let vec1: Vec = (0..num_elems1) - .map(|i| 12345 % (i + num_elems1 + num_elems2 + 1)) - .collect(); - let vec2: Vec = (0..num_elems2) - .map(|i| 54321 % (i + num_elems1 + num_elems2 + 1)) - .collect(); - let slice_str1 = format!("{:?}", vec1); - let slice_str2 = format!("{:?}", vec2); - let mut expected = vec1; - - expected.extend(vec2); - - assert_evals_to!( - &format!("List.concat {} {}", slice_str1, slice_str2), - RocList::from_slice(&expected), - RocList - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn list_concat_empty_list() { - assert_concat_worked(0, 0); - assert_concat_worked(1, 0); - assert_concat_worked(2, 0); - assert_concat_worked(3, 0); - assert_concat_worked(4, 0); - assert_concat_worked(7, 0); - assert_concat_worked(8, 0); - assert_concat_worked(9, 0); - assert_concat_worked(25, 0); - assert_concat_worked(150, 0); - assert_concat_worked(0, 1); - assert_concat_worked(0, 2); - assert_concat_worked(0, 3); - assert_concat_worked(0, 4); - assert_concat_worked(0, 7); - assert_concat_worked(0, 8); - assert_concat_worked(0, 9); - assert_concat_worked(0, 25); - assert_concat_worked(0, 150); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn list_concat_nonempty_lists() { - assert_concat_worked(1, 1); - assert_concat_worked(1, 2); - assert_concat_worked(1, 3); - assert_concat_worked(2, 3); - assert_concat_worked(2, 1); - assert_concat_worked(2, 2); - assert_concat_worked(3, 1); - assert_concat_worked(3, 2); - assert_concat_worked(2, 3); - assert_concat_worked(3, 3); - assert_concat_worked(4, 4); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn list_concat_large() { - with_larger_debug_stack(|| { - // these values produce mono ASTs so large that - // it can cause a stack overflow. This has been solved - // for current code, but may become a problem again in the future. - assert_concat_worked(150, 150); - assert_concat_worked(129, 350); - assert_concat_worked(350, 129); - }) -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn empty_list_len() { - assert_evals_to!("List.len []", 0, usize); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn basic_int_list_len() { - assert_evals_to!("List.len [12, 9, 6, 3]", 4, usize); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn loaded_int_list_len() { - assert_evals_to!( - indoc!( - r#" - nums = [2, 4, 6] - - List.len nums - "# - ), - 3, - usize - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn fn_int_list_len() { - assert_evals_to!( - indoc!( - r#" - getLen = \list -> List.len list - - nums = [2, 4, 6, 8] - - getLen nums - "# - ), - 4, - usize - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn int_list_is_empty() { - assert_evals_to!("List.isEmpty [12, 9, 6, 3]", false, bool); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn empty_list_is_empty() { - assert_evals_to!("List.isEmpty []", true, bool); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn first_int_list() { - assert_evals_to!( - indoc!( - r#" - when List.first [12, 9, 6, 3] is - Ok val -> val - Err _ -> -1 - "# - ), - 12, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -#[ignore] -fn first_wildcard_empty_list() { - assert_evals_to!( - indoc!( - r#" - when List.first [] is - Ok _ -> 5 - Err _ -> -1 - "# - ), - -1, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn first_empty_list() { - assert_evals_to!( - indoc!( - r#" - when List.first [] is - Ok val -> val - Err _ -> -1 - "# - ), - -1, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn last_int_list() { - assert_evals_to!( - indoc!( - r#" - when List.last [12, 9, 6, 3] is - Ok val -> val - Err _ -> -1 - "# - ), - 3, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -#[ignore] -fn last_wildcard_empty_list() { - assert_evals_to!( - indoc!( - r#" - when List.last [] is - Ok _ -> 5 - Err _ -> -1 - "# - ), - -1, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn last_empty_list() { - assert_evals_to!( - indoc!( - r#" - when List.last [] is - Ok val -> val - Err _ -> -1 - "# - ), - -1, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn get_empty_list() { - assert_evals_to!( - indoc!( - r#" - when List.get [] 0 is - Ok val -> val - Err _ -> -1 - "# - ), - -1, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -#[ignore] -fn get_wildcard_empty_list() { - assert_evals_to!( - indoc!( - r#" - when List.get [] 0 is - Ok _ -> 5 - Err _ -> -1 - "# - ), - -1, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn get_int_list_ok() { - assert_evals_to!( - indoc!( - r#" - when List.get [12, 9, 6] 1 is - Ok val -> val - Err _ -> -1 - "# - ), - 9, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn get_int_list_oob() { - assert_evals_to!( - indoc!( - r#" - when List.get [12, 9, 6] 1000 is - Ok val -> val - Err _ -> -1 - "# - ), - -1, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn replace_unique_int_list() { - assert_evals_to!( - indoc!( - r#" - record = List.replace [12, 9, 7, 1, 5] 2 33 - record.list - "# - ), - RocList::from_slice(&[12, 9, 33, 1, 5]), - RocList - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn replace_unique_int_list_out_of_bounds() { - assert_evals_to!( - indoc!( - r#" - record = List.replace [12, 9, 7, 1, 5] 5 33 - record.value - "# - ), - 33, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn replace_unique_int_list_get_old_value() { - assert_evals_to!( - indoc!( - r#" - record = List.replace [12, 9, 7, 1, 5] 2 33 - record.value - "# - ), - 7, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn replace_unique_get_large_value() { - assert_evals_to!( - indoc!( - r#" - list : List { a : U64, b: U64, c: U64, d: U64 } - list = [{ a: 1, b: 2, c: 3, d: 4 }, { a: 5, b: 6, c: 7, d: 8 }, { a: 9, b: 10, c: 11, d: 12 }] - record = List.replace list 1 { a: 13, b: 14, c: 15, d: 16 } - record.value - "# - ), - (5, 6, 7, 8), - (u64, u64, u64, u64) - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn replace_shared_int_list() { - assert_evals_to!( - indoc!( - r#" - wrapper = \shared -> - # This should not mutate the original - replaced = (List.replace shared 1 7.7).list - x = - when List.get replaced 1 is - Ok num -> num - Err _ -> 0 - - y = - when List.get shared 1 is - Ok num -> num - Err _ -> 0 - - { x, y } - - wrapper [2.1, 4.3] - "# - ), - (7.7, 4.3), - (f64, f64) - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn get_set_unique_int_list() { - assert_evals_to!( - indoc!( - r#" - when List.get (List.set [12, 9, 7, 3] 1 42) 1 is - Ok val -> val - Err _ -> -1 - "# - ), - 42, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn set_unique_int_list() { - assert_evals_to!( - "List.set [12, 9, 7, 1, 5] 2 33", - RocList::from_slice(&[12, 9, 33, 1, 5]), - RocList - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn set_unique_list_oob() { - assert_evals_to!( - "List.set [3, 17, 4.1] 1337 9.25", - RocList::from_slice(&[3.0, 17.0, 4.1]), - RocList - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn set_shared_int_list() { - assert_evals_to!( - indoc!( - r#" - wrapper = \shared -> - # This should not mutate the original - x = - when List.get (List.set shared 1 7.7) 1 is - Ok num -> num - Err _ -> 0 - - y = - when List.get shared 1 is - Ok num -> num - Err _ -> 0 - - { x, y } - - wrapper [2.1, 4.3] - "# - ), - (7.7, 4.3), - (f64, f64) - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn set_shared_list_oob() { - assert_evals_to!( - indoc!( - r#" - shared = [2, 4] - - # This List.set is out of bounds, and should have no effect - x = - when List.get (List.set shared 422 0) 1 is - Ok num -> num - Err _ -> 0 - - y = - when List.get shared 1 is - Ok num -> num - Err _ -> 0 - - { x, y } - "# - ), - (4, 4), - (i64, i64) - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn get_unique_int_list() { - assert_evals_to!( - indoc!( - r#" - unique = [2, 4] - - when List.get unique 1 is - Ok num -> num - Err _ -> -1 - "# - ), - 4, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn gen_wrap_len() { - assert_evals_to!( - indoc!( - r#" - wrapLen = \list -> - [List.len list] - - wrapLen [1, 7, 9] - "# - ), - RocList::from_slice(&[3]), - RocList - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn gen_wrap_first() { - assert_evals_to!( - indoc!( - r#" - wrapFirst = \list -> - [List.first list] - - wrapFirst [1, 2] - "# - ), - RocList::from_slice(&[1]), - RocList - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn gen_duplicate() { - assert_evals_to!( - indoc!( - r#" - # Duplicate the first element into the second index - dupe = \list -> - when List.first list is - Ok elem -> - List.set list 1 elem - - _ -> - [] - - dupe [1, 2] - "# - ), - RocList::from_slice(&[1, 1]), - RocList - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn gen_swap() { - assert_evals_to!( - indoc!( - r#" - app "quicksort" provides [main] to "./platform" - - - swap : Nat, Nat, List a -> List a - swap = \i, j, list -> - when Pair (List.get list i) (List.get list j) is - Pair (Ok atI) (Ok atJ) -> - list - |> List.set i atJ - |> List.set j atI - - _ -> - [] - - main = - swap 0 1 [1, 2] - "# - ), - RocList::from_slice(&[2, 1]), - RocList - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn gen_quicksort() { - with_larger_debug_stack(|| { - assert_evals_to!( - indoc!( - r#" - quicksort : List (Num a) -> List (Num a) - quicksort = \list -> - n = List.len list - quicksortHelp list 0 (n - 1) - - - quicksortHelp : List (Num a), Nat, Nat -> List (Num a) - quicksortHelp = \list, low, high -> - if low < high then - when partition low high list is - Pair partitionIndex partitioned -> - partitioned - |> quicksortHelp low (Num.subSaturated partitionIndex 1) - |> quicksortHelp (partitionIndex + 1) high - else - list - - - swap : Nat, Nat, List a -> List a - swap = \i, j, list -> - when Pair (List.get list i) (List.get list j) is - Pair (Ok atI) (Ok atJ) -> - list - |> List.set i atJ - |> List.set j atI - - _ -> - [] - - partition : Nat, Nat, List (Num a) -> [Pair Nat (List (Num a))] - partition = \low, high, initialList -> - when List.get initialList high is - Ok pivot -> - when partitionHelp low low initialList high pivot is - Pair newI newList -> - Pair newI (swap newI high newList) - - Err _ -> - Pair low initialList - - - partitionHelp : Nat, Nat, List (Num a), Nat, (Num a) -> [Pair Nat (List (Num a))] - partitionHelp = \i, j, list, high, pivot -> - if j < high then - when List.get list j is - Ok value -> - if value <= pivot then - partitionHelp (i + 1) (j + 1) (swap i j list) high pivot - else - partitionHelp i (j + 1) list high pivot - - Err _ -> - Pair i list - else - Pair i list - - quicksort [7, 4, 21, 19] - "# - ), - RocList::from_slice(&[4, 7, 19, 21]), - RocList - ); - }) -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn quicksort() { - with_larger_debug_stack(|| { - assert_evals_to!( - indoc!( - r#" - quicksort : List (Num a) -> List (Num a) - quicksort = \list -> - quicksortHelp list 0 (List.len list - 1) - - - quicksortHelp : List (Num a), Nat, Nat -> List (Num a) - quicksortHelp = \list, low, high -> - if low < high then - when partition low high list is - Pair partitionIndex partitioned -> - partitioned - |> quicksortHelp low (Num.subSaturated partitionIndex 1) - |> quicksortHelp (partitionIndex + 1) high - else - list - - - swap : Nat, Nat, List a -> List a - swap = \i, j, list -> - when Pair (List.get list i) (List.get list j) is - Pair (Ok atI) (Ok atJ) -> - list - |> List.set i atJ - |> List.set j atI - - _ -> - [] - - partition : Nat, Nat, List (Num a) -> [Pair Nat (List (Num a))] - partition = \low, high, initialList -> - when List.get initialList high is - Ok pivot -> - when partitionHelp low low initialList high pivot is - Pair newI newList -> - Pair newI (swap newI high newList) - - Err _ -> - Pair low initialList - - - partitionHelp : Nat, Nat, List (Num a), Nat, Num a -> [Pair Nat (List (Num a))] - partitionHelp = \i, j, list, high, pivot -> - # if j < high then - if False then - when List.get list j is - Ok value -> - if value <= pivot then - partitionHelp (i + 1) (j + 1) (swap i j list) high pivot - else - partitionHelp i (j + 1) list high pivot - - Err _ -> - Pair i list - else - Pair i list - - - - quicksort [7, 4, 21, 19] - "# - ), - RocList::from_slice(&[19, 7, 4, 21]), - RocList - ); - }) -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn quicksort_singleton() { - with_larger_debug_stack(|| { - assert_evals_to!( - indoc!( - r#" - quicksort : List (Num a) -> List (Num a) - quicksort = \list -> - quicksortHelp list 0 (List.len list - 1) - - - quicksortHelp : List (Num a), Nat, Nat -> List (Num a) - quicksortHelp = \list, low, high -> - if low < high then - when partition low high list is - Pair partitionIndex partitioned -> - partitioned - |> quicksortHelp low (Num.subSaturated partitionIndex 1) - |> quicksortHelp (partitionIndex + 1) high - else - list - - - swap : Nat, Nat, List a -> List a - swap = \i, j, list -> - when Pair (List.get list i) (List.get list j) is - Pair (Ok atI) (Ok atJ) -> - list - |> List.set i atJ - |> List.set j atI - - _ -> - [] - - partition : Nat, Nat, List (Num a) -> [Pair Nat (List (Num a))] - partition = \low, high, initialList -> - when List.get initialList high is - Ok pivot -> - when partitionHelp low low initialList high pivot is - Pair newI newList -> - Pair newI (swap newI high newList) - - Err _ -> - Pair low initialList - - - partitionHelp : Nat, Nat, List (Num a), Nat, Num a -> [Pair Nat (List (Num a))] - partitionHelp = \i, j, list, high, pivot -> - if j < high then - when List.get list j is - Ok value -> - if value <= pivot then - partitionHelp (i + 1) (j + 1) (swap i j list) high pivot - else - partitionHelp i (j + 1) list high pivot - - Err _ -> - Pair i list - else - Pair i list - - - - when List.first (quicksort [0x1]) is - _ -> 4 - "# - ), - 4, - i64 - ); - }) -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn empty_list_increment_decrement() { - assert_evals_to!( - indoc!( - r#" - x : List I64 - x = [] - - List.len x + List.len x - "# - ), - 0, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn list_literal_increment_decrement() { - assert_evals_to!( - indoc!( - r#" - x : List I64 - x = [1,2,3] - - List.len x + List.len x - "# - ), - 6, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn list_pass_to_function() { - assert_evals_to!( - indoc!( - r#" - x : List I64 - x = [1,2,3] - - id : List I64 -> List I64 - id = \y -> y - - id x - "# - ), - RocList::from_slice(&[1, 2, 3]), - RocList - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn list_pass_to_set() { - assert_evals_to!( - indoc!( - r#" - x : List I64 - x = [1,2,3] - - id : List I64 -> List I64 - id = \y -> List.set y 0 0 - - id x - "# - ), - RocList::from_slice(&[0, 2, 3]), - RocList - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn list_wrap_in_tag() { - assert_evals_to!( - indoc!( - r#" - id : List I64 -> [Pair (List I64) I64] - id = \y -> Pair y 4 - - when id [1,2,3] is - Pair v _ -> v - "# - ), - RocList::from_slice(&[1, 2, 3]), - RocList - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn list_contains_int() { - assert_evals_to!(indoc!("List.contains [1,2,3] 1"), true, bool); - - assert_evals_to!(indoc!("List.contains [1,2,3] 4"), false, bool); - - assert_evals_to!(indoc!("List.contains [] 4"), false, bool); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn list_contains_str() { - assert_evals_to!(indoc!(r#"List.contains ["foo", "bar"] "bar""#), true, bool); - - assert_evals_to!( - indoc!(r#"List.contains ["foo", "bar"] "spam""#), - false, - bool - ); - - assert_evals_to!(indoc!(r#"List.contains [] "spam""#), false, bool); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn list_manual_range() { - assert_evals_to!( - indoc!( - r#" - range : I64, I64, List I64-> List I64 - range = \low, high, accum -> - if low < high then - range (low + 1) high (List.append accum low) - else - accum - - range 0 5 [42] - "# - ), - RocList::from_slice(&[42, 0, 1, 2, 3, 4]), - RocList - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn list_min() { - assert_evals_to!( - indoc!( - r#" - when List.min [] is - Ok val -> val - Err _ -> -1 - "# - ), - -1, - i64 - ); - assert_evals_to!( - indoc!( - r#" - when List.min [3, 1, 2] is - Ok val -> val - Err _ -> -1 - "# - ), - 1, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn list_max() { - assert_evals_to!( - indoc!( - r#" - when List.max [] is - Ok val -> val - Err _ -> -1 - "# - ), - -1, - i64 - ); - assert_evals_to!( - indoc!( - r#" - when List.max [3, 1, 2] is - Ok val -> val - Err _ -> -1 - "# - ), - 3, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn list_sum() { - assert_evals_to!("List.sum []", 0, i64); - assert_evals_to!("List.sum [1, 2, 3]", 6, i64); - assert_evals_to!("List.sum [1.1, 2.2, 3.3]", 6.6, f64); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn list_product() { - assert_evals_to!("List.product []", 1, i64); - assert_evals_to!("List.product [1, 2, 3]", 6, i64); - assert_evals_to!("List.product [1.1, 2.2, 3.3]", 1.1 * 2.2 * 3.3, f64); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn list_keep_void() { - assert_evals_to!( - "List.keepOks [] (\\x -> x)", - RocList::from_slice(&[]), - RocList<()> - ); - - assert_evals_to!( - "List.keepErrs [] (\\x -> x)", - RocList::from_slice(&[]), - RocList<()> - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn list_keep_oks() { - assert_evals_to!( - "List.keepOks [Ok {}, Ok {}] (\\x -> x)", - RocList::from_slice(&[(), ()]), - RocList<()> - ); - assert_evals_to!( - "List.keepOks [1,2] (\\x -> Ok x)", - RocList::from_slice(&[1, 2]), - RocList - ); - assert_evals_to!( - "List.keepOks [1,2] (\\x -> Num.remChecked x 2)", - RocList::from_slice(&[1, 0]), - RocList - ); - assert_evals_to!( - "List.keepOks [Ok 1, Err 2] (\\x -> x)", - RocList::from_slice(&[1]), - RocList - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn list_keep_errs() { - assert_evals_to!( - "List.keepErrs [Err {}, Err {}] (\\x -> x)", - RocList::from_slice(&[(), ()]), - RocList<()> - ); - assert_evals_to!( - "List.keepErrs [1,2] (\\x -> Err x)", - RocList::from_slice(&[1, 2]), - RocList - ); - assert_evals_to!( - indoc!( - r#" - List.keepErrs [0,1,2] (\x -> Num.remChecked x 0 |> Result.mapErr (\_ -> 32)) - "# - ), - RocList::from_slice(&[32, 32, 32]), - RocList - ); - - assert_evals_to!( - "List.keepErrs [Ok 1, Err 2] (\\x -> x)", - RocList::from_slice(&[2]), - RocList - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn list_map_with_index() { - assert_evals_to!( - "List.mapWithIndex [0,0,0] (\\x, index -> Num.intCast index + x)", - RocList::from_slice(&[0, 1, 2]), - RocList - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -#[should_panic(expected = r#"Roc failed with message: "integer addition overflowed!"#)] -fn cleanup_because_exception() { - assert_evals_to!( - indoc!( - r#" - x = [1,2] - - five : I64 - five = 5 - - five + Num.maxI64 + 3 + (Num.intCast (List.len x)) - "# - ), - 9, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn list_range() { - assert_evals_to!( - "List.range 0 -1", - RocList::::from_slice(&[]), - RocList - ); - assert_evals_to!("List.range 0 0", RocList::from_slice(&[0]), RocList); - assert_evals_to!( - "List.range 0 5", - RocList::from_slice(&[0, 1, 2, 3, 4]), - RocList - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn list_sort_with() { - assert_evals_to!( - "List.sortWith [] Num.compare", - RocList::::from_slice(&[]), - RocList - ); - assert_evals_to!( - "List.sortWith [4,3,2,1] Num.compare", - RocList::from_slice(&[1, 2, 3, 4]), - RocList - ); - assert_evals_to!( - "List.sortWith [1,2,3,4] (\\a,b -> Num.compare b a)", - RocList::from_slice(&[4, 3, 2, 1]), - RocList - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn list_sort_asc() { - assert_evals_to!( - "List.sortAsc []", - RocList::::from_slice(&[]), - RocList - ); - assert_evals_to!( - "List.sortAsc [4,3,2,1]", - RocList::from_slice(&[1, 2, 3, 4]), - RocList - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn list_sort_desc() { - assert_evals_to!( - "List.sortDesc []", - RocList::::from_slice(&[]), - RocList - ); - assert_evals_to!( - "List.sortDesc [1,2,3,4]", - RocList::from_slice(&[4, 3, 2, 1]), - RocList - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn list_any() { - assert_evals_to!("List.any [] (\\e -> e > 3)", false, bool); - assert_evals_to!("List.any [1, 2, 3] (\\e -> e > 3)", false, bool); - assert_evals_to!("List.any [1, 2, 4] (\\e -> e > 3)", true, bool); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -#[should_panic(expected = r#"Roc failed with message: "UnresolvedTypeVar"#)] -fn list_any_empty_with_unknown_element_type() { - // Segfaults with invalid memory reference. Running this as a stand-alone - // Roc program, generates the following error message: - // - // Application crashed with message - // UnresolvedTypeVar compiler/mono/src/ir.rs line 3775 - // Shutting down - // - // TODO: eventually we should insert the empty type for unresolved type - // variables, since that means they're unbound. - expect_runtime_error_panic!("List.any [] (\\_ -> True)"); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn list_all() { - assert_evals_to!("List.all [] (\\e -> e > 3)", true, bool); - assert_evals_to!("List.all [1, 2, 3] (\\e -> e > 3)", false, bool); - assert_evals_to!("List.all [1, 2, 4] (\\e -> e > 3)", false, bool); - assert_evals_to!("List.all [1, 2, 3] (\\e -> e >= 1)", true, bool); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -#[should_panic(expected = r#"Roc failed with message: "UnresolvedTypeVar"#)] -fn list_all_empty_with_unknown_element_type() { - // Segfaults with invalid memory reference. Running this as a stand-alone - // Roc program, generates the following error message: - // - // Application crashed with message - // UnresolvedTypeVar compiler/mono/src/ir.rs line 3775 - // Shutting down - // - // TODO: eventually we should insert the empty type for unresolved type - // variables, since that means they're unbound. - expect_runtime_error_panic!("List.all [] (\\_ -> True)"); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -#[should_panic(expected = r#"Roc failed with message: "invalid ret_layout""#)] -fn lists_with_incompatible_type_param_in_if() { - assert_evals_to!( - indoc!( - r#" - list1 = [{}] - - list2 = [""] - - x = if True then list1 else list2 - - "" - "# - ), - RocStr::default(), - RocStr - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn map_with_index_multi_record() { - // see https://github.com/rtfeldman/roc/issues/1700 - assert_evals_to!( - indoc!( - r#" - List.mapWithIndex [{ x: {}, y: {} }] \_, _ -> {} - "# - ), - RocList::from_slice(&[((), ())]), - RocList<((), ())> - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn empty_list_of_function_type() { - // see https://github.com/rtfeldman/roc/issues/1732 - assert_evals_to!( - indoc!( - r#" - myList : List (Str -> Str) - myList = [] - - myClosure : Str -> Str - myClosure = \_ -> "bar" - - choose = - if False then - myList - else - [myClosure] - - when List.get choose 0 is - Ok f -> f "foo" - Err _ -> "bad!" - "# - ), - RocStr::from("bar"), - RocStr - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn list_join_map() { - assert_evals_to!( - indoc!( - r#" - List.joinMap ["guava,apple,pear", "bailey,cyrus"] (\s -> Str.split s ",") - "# - ), - RocList::from_slice(&[ - RocStr::from("guava"), - RocStr::from("apple"), - RocStr::from("pear"), - RocStr::from("bailey"), - RocStr::from("cyrus"), - ]), - RocList - ) -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn list_join_map_empty() { - assert_evals_to!( - indoc!( - r#" - List.joinMap [] (\s -> Str.split s ",") - "# - ), - RocList::from_slice(&[]), - RocList - ) -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn list_find() { - assert_evals_to!( - indoc!( - r#" - when List.find ["a", "bc", "def"] (\s -> Str.countGraphemes s > 1) is - Ok v -> v - Err _ -> "not found" - "# - ), - RocStr::from("bc"), - RocStr - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn list_find_not_found() { - assert_evals_to!( - indoc!( - r#" - when List.find ["a", "bc", "def"] (\s -> Str.countGraphemes s > 5) is - Ok v -> v - Err _ -> "not found" - "# - ), - RocStr::from("not found"), - RocStr - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn list_find_empty_typed_list() { - assert_evals_to!( - indoc!( - r#" - when List.find [] (\s -> Str.countGraphemes s > 5) is - Ok v -> v - Err _ -> "not found" - "# - ), - RocStr::from("not found"), - RocStr - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -#[ignore = "Fails because monomorphization can't be done if we don't have a concrete element type!"] -fn list_find_empty_layout() { - assert_evals_to!( - indoc!( - r#" - List.find [] (\_ -> True) - "# - ), - 0, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn monomorphized_lists() { - assert_evals_to!( - indoc!( - r#" - l = [1, 2, 3] - - f : List U8, List U16 -> Nat - f = \_, _ -> 18 - - f l l - "# - ), - 18, - u64 - ) -} diff --git a/compiler/test_gen/src/gen_num.rs b/compiler/test_gen/src/gen_num.rs deleted file mode 100644 index d3b871ea4d..0000000000 --- a/compiler/test_gen/src/gen_num.rs +++ /dev/null @@ -1,3394 +0,0 @@ -#[cfg(feature = "gen-llvm")] -use crate::helpers::llvm::assert_evals_to; - -#[cfg(feature = "gen-dev")] -use crate::helpers::dev::assert_evals_to; - -#[cfg(feature = "gen-wasm")] -use crate::helpers::wasm::assert_evals_to; - -// use crate::assert_wasm_evals_to as assert_evals_to; -#[allow(unused_imports)] -use indoc::indoc; -#[allow(unused_imports)] -use roc_std::{RocDec, RocOrder}; - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))] -fn nat_alias() { - assert_evals_to!( - indoc!( - r#" - i : Num.Nat - i = 1 - - i - "# - ), - 1, - usize - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn i128_signed_int_alias() { - assert_evals_to!( - indoc!( - r#" - i : I128 - i = 128 - - i - "# - ), - 128, - i128 - ); -} -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))] -fn i64_signed_int_alias() { - assert_evals_to!( - indoc!( - r#" - app "test" provides [main] to "./platform" - - main = - i : I64 - i = 64 - - i - "# - ), - 64, - i64 - ); -} -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))] -fn i32_signed_int_alias() { - assert_evals_to!( - indoc!( - r#" - i : I32 - i = 32 - - i - "# - ), - 32, - i32 - ); -} -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))] -fn i16_signed_int_alias() { - assert_evals_to!( - indoc!( - r#" - i : I16 - i = 16 - - i - "# - ), - 16, - i16 - ); -} -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))] -fn i8_signed_int_alias() { - assert_evals_to!( - indoc!( - r#" - i : I8 - i = 8 - - i - "# - ), - 8, - i8 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn i128_hex_int_alias() { - assert_evals_to!( - indoc!( - r#" - f : I128 - f = 0x123 - - f - "# - ), - 0x123, - i128 - ); -} -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))] -fn i64_hex_int_alias() { - assert_evals_to!( - indoc!( - r#" - f : I64 - f = 0x123 - - f - "# - ), - 0x123, - i64 - ); -} -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))] -fn i32_hex_int_alias() { - assert_evals_to!( - indoc!( - r#" - f : I32 - f = 0x123 - - f - "# - ), - 0x123, - i32 - ); -} -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))] -fn i16_hex_int_alias() { - assert_evals_to!( - indoc!( - r#" - f : I16 - f = 0x123 - - f - "# - ), - 0x123, - i16 - ); -} -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))] -fn i8_hex_int_alias() { - assert_evals_to!( - indoc!( - r#" - f : I8 - f = 0xA - - f - "# - ), - 0xA, - i8 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn u128_signed_int_alias() { - assert_evals_to!( - indoc!( - r#" - i : U128 - i = 128 - - i - "# - ), - 128, - u128 - ); -} -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))] -fn u64_signed_int_alias() { - assert_evals_to!( - indoc!( - r#" - i : U64 - i = 64 - - i - "# - ), - 64, - u64 - ); -} -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))] -fn u32_signed_int_alias() { - assert_evals_to!( - indoc!( - r#" - i : U32 - i = 32 - - i - "# - ), - 32, - u32 - ); -} -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))] -fn u16_signed_int_alias() { - assert_evals_to!( - indoc!( - r#" - i : U16 - i = 16 - - i - "# - ), - 16, - u16 - ); -} -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))] -fn u8_signed_int_alias() { - assert_evals_to!( - indoc!( - r#" - i : U8 - i = 8 - - i - "# - ), - 8, - u8 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn u128_hex_int_alias() { - assert_evals_to!( - indoc!( - r#" - f : U128 - f = 0x123 - - f - "# - ), - 0x123, - i128 - ); -} -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))] -fn u64_hex_int_alias() { - assert_evals_to!( - indoc!( - r#" - f : U64 - f = 0x123 - - f - "# - ), - 0x123, - u64 - ); -} -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))] -fn u32_hex_int_alias() { - assert_evals_to!( - indoc!( - r#" - f : U32 - f = 0x123 - - f - "# - ), - 0x123, - u32 - ); -} -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))] -fn u16_hex_int_alias() { - assert_evals_to!( - indoc!( - r#" - f : U16 - f = 0x123 - - f - "# - ), - 0x123, - u16 - ); -} -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))] -fn u8_hex_int_alias() { - assert_evals_to!( - indoc!( - r#" - f : U8 - f = 0xA - - f - "# - ), - 0xA, - u8 - ); -} - -#[test] -fn character_literal() { - assert_evals_to!( - indoc!( - r#" - x = 'A' - - x - "# - ), - 65, - u32 - ); -} - -#[test] -fn character_literal_back_slash() { - assert_evals_to!( - indoc!( - r#" - x = '\\' - - x - "# - ), - 92, - u32 - ); -} - -#[test] -fn character_literal_single_quote() { - assert_evals_to!( - indoc!( - r#" - x = '\'' - - x - "# - ), - 39, - u32 - ); -} - -#[test] -fn character_literal_new_line() { - assert_evals_to!( - indoc!( - r#" - x = '\n' - - x - "# - ), - 10, - u32 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn dec_float_alias() { - assert_evals_to!( - indoc!( - r#" - x : Dec - x = 2.1 - - x - "# - ), - RocDec::from_str_to_i128_unsafe("2.1"), - i128 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))] -fn f64_float_alias() { - assert_evals_to!( - indoc!( - r#" - f : F64 - f = 3.6 - - f - "# - ), - 3.6, - f64 - ); -} -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn f32_float_alias() { - assert_evals_to!( - indoc!( - r#" - f : F32 - f = 3.6 - - f - "# - ), - 3.6, - f32 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn f64_sqrt() { - assert_evals_to!( - indoc!( - r#" - when Num.sqrtChecked 100 is - Ok val -> val - Err _ -> -1 - "# - ), - 10.0, - f64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn f64_log() { - assert_evals_to!( - indoc!( - r#" - Num.log 7.38905609893 - "# - ), - 1.999999999999912, - f64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn f64_log_checked_one() { - assert_evals_to!( - indoc!( - r#" - when Num.logChecked 1 is - Ok val -> val - Err _ -> -1 - "# - ), - 0.0, - f64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn f64_sqrt_zero() { - assert_evals_to!( - indoc!( - r#" - when Num.sqrtChecked 0 is - Ok val -> val - Err _ -> -1 - "# - ), - 0.0, - f64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn f64_sqrt_checked_negative() { - assert_evals_to!( - indoc!( - r#" - when Num.sqrtChecked -1 is - Err _ -> 42 - Ok val -> val - "# - ), - 42.0, - f64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn f64_log_checked_zero() { - assert_evals_to!( - indoc!( - r#" - when Num.logChecked 0 is - Err _ -> 42 - Ok val -> val - "# - ), - 42.0, - f64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn f64_log_negative() { - assert_evals_to!( - indoc!( - r#" - Num.log -1 - "# - ), - true, - f64, - |f: f64| f.is_nan() - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))] -fn f64_round() { - assert_evals_to!("Num.round 3.6", 4, i64); - assert_evals_to!("Num.round 3.4", 3, i64); - assert_evals_to!("Num.round 2.5", 3, i64); - assert_evals_to!("Num.round -2.3", -2, i64); - assert_evals_to!("Num.round -2.5", -3, i64); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))] -fn f64_abs() { - assert_evals_to!("Num.abs -4.7", 4.7, f64); - assert_evals_to!("Num.abs 5.8", 5.8, f64); - - #[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] - { - assert_evals_to!("Num.abs Num.maxF64", f64::MAX, f64); - assert_evals_to!("Num.abs Num.minF64", f64::MAX, f64); - } -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn i64_abs() { - assert_evals_to!("Num.abs -6", 6, i64); - assert_evals_to!("Num.abs 7", 7, i64); - assert_evals_to!("Num.abs 0", 0, i64); - assert_evals_to!("Num.abs -0", 0, i64); - assert_evals_to!("Num.abs -1", 1, i64); - assert_evals_to!("Num.abs 1", 1, i64); - assert_evals_to!("Num.abs 9_000_000_000_000", 9_000_000_000_000, i64); - assert_evals_to!("Num.abs -9_000_000_000_000", 9_000_000_000_000, i64); - assert_evals_to!("Num.abs Num.maxI64", i64::MAX, i64); - assert_evals_to!("Num.abs (Num.minI64 + 1)", -(i64::MIN + 1), i64); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -#[should_panic( - expected = r#"Roc failed with message: "integer absolute overflowed because its argument is the minimum value"# -)] -fn abs_min_int_overflow() { - assert_evals_to!( - indoc!( - r#" - Num.abs Num.minI64 - "# - ), - 0, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))] -fn gen_if_fn() { - assert_evals_to!( - indoc!( - r#" - limitedNegate = \num -> - x = - if num == 1 then - -1 - else if num == -1 then - 1 - else - num - x - - limitedNegate 1 - "# - ), - -1, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn gen_float_eq() { - assert_evals_to!( - indoc!( - r#" - 1.0 == 1.0 - "# - ), - true, - bool - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn gen_add_dec() { - assert_evals_to!( - indoc!( - r#" - x : Dec - x = 2.1 - - y : Dec - y = 3.1 - - z : Dec - z = x + y - - z - "# - ), - RocDec::from_str_to_i128_unsafe("5.2"), - i128 - ); -} -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))] -fn gen_add_f64() { - assert_evals_to!( - indoc!( - r#" - 1.1 + 2.4 + 3 - "# - ), - 6.5, - f64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))] -fn gen_wrap_add_nums() { - assert_evals_to!( - indoc!( - r#" - add2 = \num1, num2 -> num1 + num2 - - add2 4 5 - "# - ), - 9, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn gen_div_f64() { - assert_evals_to!( - indoc!( - r#" - 48 / 2 - "# - ), - 24.0, - f64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn gen_div_checked_f64() { - assert_evals_to!( - indoc!( - r#" - when Num.divChecked 48 2 is - Ok val -> val - Err _ -> -1 - "# - ), - 24.0, - f64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn gen_div_checked_by_zero_f64() { - assert_evals_to!( - indoc!( - r#" - when Num.divChecked 47 0 is - Ok val -> val - Err _ -> -1 - "# - ), - -1.0, - f64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn gen_div_dec() { - assert_evals_to!( - indoc!( - r#" - x : Dec - x = 10 - - y : Dec - y = 3 - - x / y - "# - ), - RocDec::from_str_to_i128_unsafe("3.333333333333333333"), - i128 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn gen_div_checked_dec() { - assert_evals_to!( - indoc!( - r#" - x : Dec - x = 10 - - y : Dec - y = 3 - - when Num.divChecked x y is - Ok val -> val - Err _ -> -1 - "# - ), - RocDec::from_str_to_i128_unsafe("3.333333333333333333"), - i128 - ); -} -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn gen_div_checked_by_zero_dec() { - assert_evals_to!( - indoc!( - r#" - x : Dec - x = 10 - - y : Dec - y = 0 - - when Num.divChecked x y is - Ok val -> val - Err _ -> -1 - "# - ), - RocDec::from_str_to_i128_unsafe("-1"), - i128 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))] -fn gen_int_eq() { - assert_evals_to!( - indoc!( - r#" - 4 == 4 - "# - ), - true, - bool - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] -fn gen_int_neq() { - assert_evals_to!( - indoc!( - r#" - 4 != 5 - "# - ), - true, - bool - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] -fn gen_int_less_than() { - assert_evals_to!( - indoc!( - r#" - 4 < 5 - "# - ), - true, - bool - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn gen_dec_eq() { - assert_evals_to!( - indoc!( - r#" - x : Dec - x = 4 - - y : Dec - y = 4 - - x == y - "# - ), - true, - bool - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn gen_dec_neq() { - assert_evals_to!( - indoc!( - r#" - x : Dec - x = 4 - - y : Dec - y = 5 - - x != y - "# - ), - true, - bool - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn gen_wrap_int_neq() { - assert_evals_to!( - indoc!( - r#" - wrappedNotEq : a, a -> Bool - wrappedNotEq = \num1, num2 -> - num1 != num2 - - wrappedNotEq 2 3 - "# - ), - true, - bool - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))] -fn gen_add_i64() { - assert_evals_to!( - indoc!( - r#" - 1 + 2 + 3 - "# - ), - 6, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn gen_sub_dec() { - assert_evals_to!( - indoc!( - r#" - x : Dec - x = 1.5 - - y : Dec - y = 2.4 - - z : Dec - z = 3 - - (x - y) - z - "# - ), - RocDec::from_str_to_i128_unsafe("-3.9"), - i128 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn gen_sub_f64() { - assert_evals_to!( - indoc!( - r#" - 1.5 - 2.4 - 3 - "# - ), - -3.9, - f64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))] -fn gen_sub_i64() { - assert_evals_to!( - indoc!( - r#" - 1 - 2 - 3 - "# - ), - -4, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn gen_mul_dec() { - assert_evals_to!( - indoc!( - r#" - x : Dec - x = 2 - - y : Dec - y = 4 - - z : Dec - z = 6 - - x * y * z - "# - ), - RocDec::from_str_to_i128_unsafe("48.0"), - i128 - ); -} -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))] -fn gen_mul_i64() { - assert_evals_to!( - indoc!( - r#" - 2 * 4 * 6 - "# - ), - 48, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn gen_div_i64() { - assert_evals_to!( - indoc!( - r#" - 1000 // 10 - "# - ), - 100, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn gen_div_checked_i64() { - assert_evals_to!( - indoc!( - r#" - when Num.divTruncChecked 1000 10 is - Ok val -> val - Err _ -> -1 - "# - ), - 100, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn gen_div_checked_by_zero_i64() { - assert_evals_to!( - indoc!( - r#" - when Num.divTruncChecked 1000 0 is - Err DivByZero -> 99 - _ -> -24 - "# - ), - 99, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn gen_rem_i64() { - assert_evals_to!( - indoc!( - r#" - Num.rem 8 3 - "# - ), - 2, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn gen_rem_checked_div_by_zero_i64() { - assert_evals_to!( - indoc!( - r#" - when Num.remChecked 8 0 is - Err DivByZero -> 4 - Ok _ -> -23 - "# - ), - 4, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] -fn gen_is_zero_i64() { - assert_evals_to!("Num.isZero 0", true, bool); - assert_evals_to!("Num.isZero 1", false, bool); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn gen_is_positive_i64() { - assert_evals_to!("Num.isPositive 0", false, bool); - assert_evals_to!("Num.isPositive 1", true, bool); - assert_evals_to!("Num.isPositive -5", false, bool); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn gen_is_negative_i64() { - assert_evals_to!("Num.isNegative 0", false, bool); - assert_evals_to!("Num.isNegative 3", false, bool); - assert_evals_to!("Num.isNegative -2", true, bool); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn gen_is_positive_f64() { - assert_evals_to!("Num.isPositive 0.0", false, bool); - assert_evals_to!("Num.isPositive 4.7", true, bool); - assert_evals_to!("Num.isPositive -8.5", false, bool); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn gen_is_negative_f64() { - assert_evals_to!("Num.isNegative 0.0", false, bool); - assert_evals_to!("Num.isNegative 9.9", false, bool); - assert_evals_to!("Num.isNegative -4.4", true, bool); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn gen_is_zero_f64() { - assert_evals_to!("Num.isZero 0", true, bool); - assert_evals_to!("Num.isZero 0_0", true, bool); - assert_evals_to!("Num.isZero 0.0", true, bool); - assert_evals_to!("Num.isZero 1", false, bool); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn gen_is_odd() { - assert_evals_to!("Num.isOdd 4", false, bool); - assert_evals_to!("Num.isOdd 5", true, bool); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn gen_is_even() { - assert_evals_to!("Num.isEven 6", true, bool); - assert_evals_to!("Num.isEven 7", false, bool); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn sin() { - assert_evals_to!("Num.sin 0", 0.0, f64); - assert_evals_to!("Num.sin 1.41421356237", 0.9877659459922529, f64); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn cos() { - assert_evals_to!("Num.cos 0", 1.0, f64); - assert_evals_to!("Num.cos 3.14159265359", -1.0, f64); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn tan() { - assert_evals_to!("Num.tan 0", 0.0, f64); - assert_evals_to!("Num.tan 1", 1.557407724654902, f64); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn bitwise_and() { - assert_evals_to!("Num.bitwiseAnd 20 20", 20, i64); - assert_evals_to!("Num.bitwiseAnd 25 10", 8, i64); - assert_evals_to!("Num.bitwiseAnd 200 0", 0, i64); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn bitwise_xor() { - assert_evals_to!("Num.bitwiseXor 20 20", 0, i64); - assert_evals_to!("Num.bitwiseXor 15 14", 1, i64); - assert_evals_to!("Num.bitwiseXor 7 15", 8, i64); - assert_evals_to!("Num.bitwiseXor 200 0", 200, i64); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn bitwise_or() { - assert_evals_to!("Num.bitwiseOr 1 1", 1, i64); - assert_evals_to!("Num.bitwiseOr 1 2", 3, i64); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn lt_u8() { - assert_evals_to!("1u8 < 2u8", true, bool); - assert_evals_to!("1u8 < 1u8", false, bool); - assert_evals_to!("2u8 < 1u8", false, bool); - assert_evals_to!("0u8 < 0u8", false, bool); - assert_evals_to!("128u8 < 0u8", false, bool); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn lte_u8() { - assert_evals_to!("1u8 <= 1u8", true, bool); - assert_evals_to!("2u8 <= 1u8", false, bool); - assert_evals_to!("1u8 <= 2u8", true, bool); - assert_evals_to!("0u8 <= 0u8", true, bool); - assert_evals_to!("128u8 <= 0u8", false, bool); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn gt_u8() { - assert_evals_to!("2u8 > 1u8", true, bool); - assert_evals_to!("2u8 > 2u8", false, bool); - assert_evals_to!("1u8 > 1u8", false, bool); - assert_evals_to!("0u8 > 0u8", false, bool); - assert_evals_to!("0u8 > 128u8", false, bool); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn gte_u8() { - assert_evals_to!("1u8 >= 1u8", true, bool); - assert_evals_to!("1u8 >= 2u8", false, bool); - assert_evals_to!("2u8 >= 1u8", true, bool); - assert_evals_to!("0u8 >= 0u8", true, bool); - assert_evals_to!("0u8 >= 128u8", false, bool); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn lt_u64() { - assert_evals_to!("1u64 < 2u64", true, bool); - assert_evals_to!("1u64 < 1u64", false, bool); - assert_evals_to!("2u64 < 1u64", false, bool); - assert_evals_to!("0u64 < 0u64", false, bool); - assert_evals_to!("9223372036854775808u64 < 0u64", false, bool); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn lte_u64() { - assert_evals_to!("1u64 <= 1u64", true, bool); - assert_evals_to!("2u64 <= 1u64", false, bool); - assert_evals_to!("1u64 <= 2u64", true, bool); - assert_evals_to!("0u64 <= 0u64", true, bool); - assert_evals_to!("9223372036854775808u64 <= 0u64", false, bool); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn gt_u64() { - assert_evals_to!("2u64 > 1u64", true, bool); - assert_evals_to!("2u64 > 2u64", false, bool); - assert_evals_to!("1u64 > 1u64", false, bool); - assert_evals_to!("0u64 > 0u64", false, bool); - assert_evals_to!("0u64 > 9223372036854775808u64", false, bool); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn gte_u64() { - assert_evals_to!("1u64 >= 1u64", true, bool); - assert_evals_to!("1u64 >= 2u64", false, bool); - assert_evals_to!("2u64 >= 1u64", true, bool); - assert_evals_to!("0u64 >= 0u64", true, bool); - assert_evals_to!("0u64 >= 9223372036854775808u64", false, bool); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn lt_i64() { - assert_evals_to!("1 < 2", true, bool); - assert_evals_to!("1 < 1", false, bool); - assert_evals_to!("2 < 1", false, bool); - assert_evals_to!("0 < 0", false, bool); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn lte_i64() { - assert_evals_to!("1 <= 1", true, bool); - assert_evals_to!("2 <= 1", false, bool); - assert_evals_to!("1 <= 2", true, bool); - assert_evals_to!("0 <= 0", true, bool); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn gt_i64() { - assert_evals_to!("2 > 1", true, bool); - assert_evals_to!("2 > 2", false, bool); - assert_evals_to!("1 > 1", false, bool); - assert_evals_to!("0 > 0", false, bool); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn gte_i64() { - assert_evals_to!("1 >= 1", true, bool); - assert_evals_to!("1 >= 2", false, bool); - assert_evals_to!("2 >= 1", true, bool); - assert_evals_to!("0 >= 0", true, bool); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn lt_f64() { - assert_evals_to!("1.1 < 1.2", true, bool); - assert_evals_to!("1.1 < 1.1", false, bool); - assert_evals_to!("1.2 < 1.1", false, bool); - assert_evals_to!("0.0 < 0.0", false, bool); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn lte_f64() { - assert_evals_to!("1.1 <= 1.1", true, bool); - assert_evals_to!("1.2 <= 1.1", false, bool); - assert_evals_to!("1.1 <= 1.2", true, bool); - assert_evals_to!("0.0 <= 0.0", true, bool); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn gt_f64() { - assert_evals_to!("2.2 > 1.1", true, bool); - assert_evals_to!("2.2 > 2.2", false, bool); - assert_evals_to!("1.1 > 2.2", false, bool); - assert_evals_to!("0.0 > 0.0", false, bool); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn gte_f64() { - assert_evals_to!("1.1 >= 1.1", true, bool); - assert_evals_to!("1.1 >= 1.2", false, bool); - assert_evals_to!("1.2 >= 1.1", true, bool); - assert_evals_to!("0.0 >= 0.0", true, bool); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))] -fn gen_order_of_arithmetic_ops() { - assert_evals_to!( - indoc!( - r#" - 1 + 3 * 7 - 2 - "# - ), - 20, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn gen_order_of_arithmetic_ops_complex_float() { - assert_evals_to!( - indoc!( - r#" - 3 - 48 * 2.0 - "# - ), - -93.0, - f64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))] -fn if_guard_bind_variable_false() { - assert_evals_to!( - indoc!( - r#" - wrapper = \{} -> - when 10 is - x if x == 5 -> 0 - _ -> 42 - - wrapper {} - "# - ), - 42, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))] -fn if_guard_bind_variable_true() { - assert_evals_to!( - indoc!( - r#" - wrapper = \{} -> - when 10 is - x if x == 10 -> 42 - _ -> 0 - - wrapper {} - "# - ), - 42, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))] -fn tail_call_elimination() { - assert_evals_to!( - indoc!( - r#" - sum = \n, accum -> - when n is - 0 -> accum - _ -> sum (n - 1) (n + accum) - - sum 1_000_000 0 - "# - ), - 500000500000, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-dev"))] -fn int_negate_dev() { - // TODO - // dev backend yet to have `Num.maxI64` or `Num.minI64`. - // add the "gen-dev" feature to the test below after implementing them both. - assert_evals_to!("Num.neg 123", -123, i64); - assert_evals_to!("Num.neg -123", 123, i64); - assert_evals_to!("Num.neg 0", 0, i64); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn int_negate() { - assert_evals_to!("Num.neg 123", -123, i64); - assert_evals_to!("Num.neg Num.maxI64", -i64::MAX, i64); - assert_evals_to!("Num.neg (Num.minI64 + 1)", i64::MAX, i64); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -#[should_panic( - expected = r#"Roc failed with message: "integer negation overflowed because its argument is the minimum value"# -)] -fn neg_min_int_overflow() { - assert_evals_to!( - indoc!( - r#" - Num.neg Num.minI64 - "# - ), - 0, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn gen_wrap_int_neg() { - assert_evals_to!( - indoc!( - r#" - wrappedNeg = \num -> -num - - wrappedNeg 3 - "# - ), - -3, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))] -fn gen_basic_fn() { - assert_evals_to!( - indoc!( - r#" - always42 : Num.Num (Num.Integer Num.Signed64) -> Num.Num (Num.Integer Num.Signed64) - always42 = \_ -> 42 - - always42 5 - "# - ), - 42, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] -fn int_to_float() { - assert_evals_to!("Num.toFrac 0x9", 9.0, f64); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] -fn num_to_frac() { - assert_evals_to!("Num.toFrac 9", 9.0, f64); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-dev"))] -fn num_to_frac_f64_to_f32() { - assert_evals_to!( - indoc!( - r#" - f64 : F64 - f64 = 9.0 - - f32 : F32 - f32 = Num.toFrac f64 - f32 - "# - ), - 9.0, - f32 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-dev"))] -fn num_to_frac_f32_to_f32() { - assert_evals_to!( - indoc!( - r#" - - arg : F32 - arg = 9.0 - - ret : F32 - ret = Num.toFrac arg - ret - "# - ), - 9.0, - f32 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] -fn num_to_frac_f64_to_f64() { - assert_evals_to!( - indoc!( - r#" - - arg : F64 - arg = 9.0 - - ret : F64 - ret = Num.toFrac arg - ret - "# - ), - 9.0, - f64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] -fn num_to_frac_f32_to_f64() { - assert_evals_to!( - indoc!( - r#" - - f32 : F32 - f32 = 9.0 - - f64 : F64 - f64 = Num.toFrac f32 - f64 - "# - ), - 9.0, - f64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] -fn float_to_float() { - assert_evals_to!("Num.toFrac 0.5", 0.5, f64); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn int_compare() { - assert_evals_to!("Num.compare 0 1", RocOrder::Lt, RocOrder); - assert_evals_to!("Num.compare 1 1", RocOrder::Eq, RocOrder); - assert_evals_to!("Num.compare 1 0", RocOrder::Gt, RocOrder); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn float_compare() { - assert_evals_to!("Num.compare 0.01 3.14", RocOrder::Lt, RocOrder); - assert_evals_to!("Num.compare 3.14 3.14", RocOrder::Eq, RocOrder); - assert_evals_to!("Num.compare 3.14 0.01", RocOrder::Gt, RocOrder); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn pow() { - assert_evals_to!("Num.pow 2.0 2.0", 4.0, f64); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn ceiling() { - assert_evals_to!("Num.ceiling 1.1", 2, i64); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn floor() { - assert_evals_to!("Num.floor 1.9", 1, i64); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))] -fn pow_int() { - assert_evals_to!("Num.powInt 2 3", 8, i64); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))] -fn atan() { - assert_evals_to!("Num.atan 10", 1.4711276743037347, f64); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -#[should_panic(expected = r#"Roc failed with message: "integer addition overflowed!"#)] -fn int_add_overflow() { - assert_evals_to!( - indoc!( - r#" - 9_223_372_036_854_775_807 + 1 - "# - ), - 0, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn int_add_checked() { - assert_evals_to!( - indoc!( - r#" - when Num.addChecked 1 2 is - Ok v -> v - _ -> -1 - "# - ), - 3, - i64 - ); - - assert_evals_to!( - indoc!( - r#" - when Num.addChecked 9_223_372_036_854_775_807 1 is - Err Overflow -> -1 - Ok v -> v - "# - ), - -1, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn int_add_wrap() { - assert_evals_to!( - indoc!( - r#" - Num.addWrap 9_223_372_036_854_775_807 1 - "# - ), - std::i64::MIN, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn float_add_checked_pass() { - assert_evals_to!( - indoc!( - r#" - when Num.addChecked 1.0 0.0 is - Ok v -> v - Err Overflow -> -1.0 - "# - ), - 1.0, - f64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn float_add_checked_fail() { - assert_evals_to!( - indoc!( - r#" - when Num.addChecked 1.7976931348623157e308 1.7976931348623157e308 is - Err Overflow -> -1 - Ok v -> v - "# - ), - -1.0, - f64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -#[should_panic(expected = r#"Roc failed with message: "float addition overflowed!"#)] -fn float_add_overflow() { - assert_evals_to!( - indoc!( - r#" - 1.7976931348623157e308 + 1.7976931348623157e308 - "# - ), - 0.0, - f64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -#[should_panic(expected = r#"Roc failed with message: "integer subtraction overflowed!"#)] -fn int_sub_overflow() { - assert_evals_to!( - indoc!( - r#" - -9_223_372_036_854_775_808 - 1 - "# - ), - 0, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn int_sub_wrap() { - assert_evals_to!( - indoc!( - r#" - Num.subWrap -9_223_372_036_854_775_808 1 - "# - ), - std::i64::MAX, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -#[should_panic(expected = r#"Roc failed with message: "float subtraction overflowed!"#)] -fn float_sub_overflow() { - assert_evals_to!( - indoc!( - r#" - -1.7976931348623157e308 - 1.7976931348623157e308 - "# - ), - 0.0, - f64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn int_sub_checked() { - assert_evals_to!( - indoc!( - r#" - when Num.subChecked 5 2 is - Ok v -> v - _ -> -1 - "# - ), - 3, - i64 - ); - - assert_evals_to!( - indoc!( - r#" - when Num.subChecked Num.minI64 1 is - Err Overflow -> -1 - Ok v -> v - "# - ), - -1, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn float_sub_checked() { - assert_evals_to!( - indoc!( - r#" - when Num.subChecked 1.0 0.0 is - Ok v -> v - Err Overflow -> -1.0 - "# - ), - 1.0, - f64 - ); - - assert_evals_to!( - indoc!( - r#" - when Num.subChecked -1.7976931348623157e308 1.7976931348623157e308 is - Err Overflow -> -1 - Ok v -> v - "# - ), - -1.0, - f64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -#[should_panic(expected = r#"Roc failed with message: "integer multiplication overflowed!"#)] -fn int_positive_mul_overflow() { - assert_evals_to!( - indoc!( - r#" - 9_223_372_036_854_775_807 * 2 - "# - ), - 0, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -#[should_panic(expected = r#"Roc failed with message: "integer multiplication overflowed!"#)] -fn int_negative_mul_overflow() { - assert_evals_to!( - indoc!( - r#" - (-9_223_372_036_854_775_808) * 2 - "# - ), - 0, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -#[should_panic(expected = r#"Roc failed with message: "float multiplication overflowed!"#)] -fn float_positive_mul_overflow() { - assert_evals_to!( - indoc!( - r#" - 1.7976931348623157e308 * 2 - "# - ), - 0.0, - f64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -#[should_panic(expected = r#"Roc failed with message: "float multiplication overflowed!"#)] -fn float_negative_mul_overflow() { - assert_evals_to!( - indoc!( - r#" - -1.7976931348623157e308 * 2 - "# - ), - 0.0, - f64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn int_mul_wrap() { - assert_evals_to!( - indoc!( - r#" - Num.mulWrap Num.maxI64 2 - "# - ), - -2, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn int_mul_checked() { - assert_evals_to!( - indoc!( - r#" - when Num.mulChecked 20 2 is - Ok v -> v - _ -> -1 - "# - ), - 40, - i64 - ); - - assert_evals_to!( - indoc!( - r#" - when Num.mulChecked Num.maxI64 2 is - Err Overflow -> -1 - Ok v -> v - "# - ), - -1, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn float_mul_checked() { - assert_evals_to!( - indoc!( - r#" - when Num.mulChecked 20.0 2.0 is - Ok v -> v - Err Overflow -> -1.0 - "# - ), - 40.0, - f64 - ); - - assert_evals_to!( - indoc!( - r#" - when Num.mulChecked 1.7976931348623157e308 2 is - Err Overflow -> -1 - Ok v -> v - "# - ), - -1.0, - f64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn shift_left_by() { - assert_evals_to!("Num.shiftLeftBy 0 0b0000_0001", 0b0000_0001, i64); - assert_evals_to!("Num.shiftLeftBy 1 0b0000_0001", 0b0000_0010, i64); - assert_evals_to!("Num.shiftLeftBy 2 0b0000_0011", 0b0000_1100, i64); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn shift_right_by() { - // Sign Extended Right Shift - - let is_wasm = cfg!(feature = "gen-wasm"); - let is_llvm_release_mode = cfg!(feature = "gen-llvm") && !cfg!(debug_assertions); - - // FIXME (Brian) Something funny happening with 8-bit binary literals in tests - if !is_wasm { - assert_evals_to!( - "Num.shiftRightBy 2 (Num.toI8 0b1100_0000u8)", - 0b1111_0000u8 as i8, - i8 - ); - assert_evals_to!("Num.shiftRightBy 2 0b0100_0000i8", 0b0001_0000i8, i8); - assert_evals_to!("Num.shiftRightBy 1 0b1110_0000u8", 0b1111_0000u8, u8); - assert_evals_to!("Num.shiftRightBy 2 0b1100_0000u8", 0b1111_0000u8, u8); - assert_evals_to!("Num.shiftRightBy 12 0b0100_0000u8", 0b0000_0000u8, u8); - - // LLVM in release mode returns 0 instead of -1 for some reason - if !is_llvm_release_mode { - assert_evals_to!("Num.shiftRightBy 12 0b1000_0000u8", 0b1111_1111u8, u8); - } - } - assert_evals_to!("Num.shiftRightBy 0 12", 12, i64); - assert_evals_to!("Num.shiftRightBy 1 12", 6, i64); - assert_evals_to!("Num.shiftRightBy 1 -12", -6, i64); - assert_evals_to!("Num.shiftRightBy 8 12", 0, i64); - assert_evals_to!("Num.shiftRightBy 8 -12", -1, i64); - assert_evals_to!("Num.shiftRightBy -1 12", 0, i64); - assert_evals_to!("Num.shiftRightBy 0 0", 0, i64); - assert_evals_to!("Num.shiftRightBy 1 0", 0, i64); - - assert_evals_to!("Num.shiftRightBy 0 12i32", 12, i32); - assert_evals_to!("Num.shiftRightBy 1 12i32", 6, i32); - assert_evals_to!("Num.shiftRightBy 1 -12i32", -6, i32); - assert_evals_to!("Num.shiftRightBy 8 12i32", 0, i32); - assert_evals_to!("Num.shiftRightBy 8 -12i32", -1, i32); - - assert_evals_to!("Num.shiftRightBy 0 12i8", 12, i8); - assert_evals_to!("Num.shiftRightBy 1 12i8", 6, i8); - assert_evals_to!("Num.shiftRightBy 1 -12i8", -6, i8); - assert_evals_to!("Num.shiftRightBy 8 12i8", 0, i8); - - if !is_llvm_release_mode { - assert_evals_to!("Num.shiftRightBy -1 0", 0, i64); - assert_evals_to!("Num.shiftRightBy -1 -12", -1, i64); - assert_evals_to!("Num.shiftRightBy 8 -12i8", -1, i8); - } -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn shift_right_zf_by() { - // Logical Right Shift - assert_evals_to!( - "Num.shiftRightZfBy 2 (Num.toI8 0b1100_0000u8)", - 0b0011_0000i8, - i8 - ); - assert_evals_to!("Num.shiftRightZfBy 2 0b1100_0000u8", 0b0011_0000u8, u8); - assert_evals_to!("Num.shiftRightZfBy 1 0b0000_0010u8", 0b0000_0001u8, u8); - assert_evals_to!("Num.shiftRightZfBy 2 0b0000_1100u8", 0b0000_0011u8, u8); - assert_evals_to!("Num.shiftRightZfBy 12 0b1000_0000u8", 0b0000_0000u8, u8); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn min_i128() { - assert_evals_to!( - indoc!( - r#" - Num.minI128 - "# - ), - i128::MIN, - i128 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn max_i128() { - assert_evals_to!( - indoc!( - r#" - Num.maxI128 - "# - ), - i128::MAX, - i128 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn min_i64() { - assert_evals_to!( - indoc!( - r#" - Num.minI64 - "# - ), - i64::MIN, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn max_i64() { - assert_evals_to!( - indoc!( - r#" - Num.maxI64 - "# - ), - i64::MAX, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn min_u64() { - assert_evals_to!( - indoc!( - r#" - Num.minU64 - "# - ), - u64::MIN, - u64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn max_u64() { - assert_evals_to!( - indoc!( - r#" - Num.maxU64 - "# - ), - u64::MAX, - u64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn min_i32() { - assert_evals_to!( - indoc!( - r#" - Num.minI32 - "# - ), - i32::MIN, - i32 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn max_i32() { - assert_evals_to!( - indoc!( - r#" - Num.maxI32 - "# - ), - i32::MAX, - i32 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn min_u32() { - assert_evals_to!( - indoc!( - r#" - Num.minU32 - "# - ), - u32::MIN, - u32 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn max_u32() { - assert_evals_to!( - indoc!( - r#" - Num.maxU32 - "# - ), - u32::MAX, - u32 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn min_i16() { - assert_evals_to!( - indoc!( - r#" - Num.minI16 - "# - ), - i16::MIN, - i16 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn max_i16() { - assert_evals_to!( - indoc!( - r#" - Num.maxI16 - "# - ), - i16::MAX, - i16 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn min_u16() { - assert_evals_to!( - indoc!( - r#" - Num.minU16 - "# - ), - u16::MIN, - u16 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn max_u16() { - assert_evals_to!( - indoc!( - r#" - Num.maxU16 - "# - ), - u16::MAX, - u16 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn min_i8() { - assert_evals_to!( - indoc!( - r#" - Num.minI8 - "# - ), - i8::MIN, - i8 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn max_i8() { - assert_evals_to!( - indoc!( - r#" - Num.maxI8 - "# - ), - i8::MAX, - i8 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn min_u8() { - assert_evals_to!( - indoc!( - r#" - Num.minU8 - "# - ), - u8::MIN, - u8 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn max_u8() { - assert_evals_to!( - indoc!( - r#" - Num.maxU8 - "# - ), - u8::MAX, - u8 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn max_f64() { - assert_evals_to!( - indoc!( - r#" - Num.maxF64 - "# - ), - f64::MAX, - f64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn min_f64() { - assert_evals_to!( - indoc!( - r#" - Num.minF64 - "# - ), - f64::MIN, - f64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn max_f32() { - assert_evals_to!( - indoc!( - r#" - Num.maxF32 - "# - ), - f32::MAX, - f32 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn min_f32() { - assert_evals_to!( - indoc!( - r#" - Num.minF32 - "# - ), - f32::MIN, - f32 - ); -} - -macro_rules! num_conversion_tests { - ($($fn:expr, $typ:ty, ($($test_name:ident, $input:expr, $output:expr $(, [$($support_gen:literal),*])? )*))*) => {$($( - #[test] - #[cfg(any(feature = "gen-llvm", $($(feature = $support_gen)*)?))] - fn $test_name() { - let input = format!("{} {}", $fn, $input); - assert_evals_to!(&input, $output, $typ) - } - )*)*} -} - -num_conversion_tests! { - "Num.toI8", i8, ( - to_i8_same_width, "15u8", 15, ["gen-wasm"] - to_i8_truncate, "115i32", 115, ["gen-wasm"] - to_i8_truncate_wraps, "500i32", -12, ["gen-wasm"] - ) - "Num.toI16", i16, ( - to_i16_same_width, "15u16", 15, ["gen-wasm"] - to_i16_extend, "15i8", 15, ["gen-wasm"] - to_i16_truncate, "115i32", 115, ["gen-wasm"] - to_i16_truncate_wraps, "60000i32", -5536, ["gen-wasm"] - ) - "Num.toI32", i32, ( - to_i32_same_width, "15u32", 15, ["gen-wasm"] - to_i32_extend, "15i8", 15, ["gen-wasm"] - to_i32_truncate, "115i64", 115, ["gen-wasm"] - to_i32_truncate_wraps, "5000000000i64", 705032704, ["gen-wasm"] - ) - "Num.toI64", i64, ( - to_i64_same_width, "15u64", 15, ["gen-wasm"] - to_i64_extend, "15i8", 15, ["gen-wasm"] - to_i64_truncate, "115i128", 115 - to_i64_truncate_wraps, "10_000_000_000_000_000_000i128", -8446744073709551616 - ) - "Num.toI128", i128, ( - to_i128_same_width, "15u128", 15 - to_i128_extend, "15i8", 15 - ) - "Num.toU8", u8, ( - to_u8_same_width, "15i8", 15, ["gen-wasm"] - to_u8_truncate, "115i32", 115, ["gen-wasm"] - to_u8_truncate_wraps, "500i32", 244, ["gen-wasm"] - ) - "Num.toU16", u16, ( - to_u16_same_width, "15i16", 15, ["gen-wasm"] - to_u16_extend, "15i8", 15, ["gen-wasm"] - to_u16_truncate, "115i32", 115, ["gen-wasm"] - to_u16_truncate_wraps, "600000000i32", 17920, ["gen-wasm"] - ) - "Num.toU32", u32, ( - to_u32_same_width, "15i32", 15, ["gen-wasm"] - to_u32_extend, "15i8", 15, ["gen-wasm"] - to_u32_truncate, "115i64", 115, ["gen-wasm"] - to_u32_truncate_wraps, "5000000000000000000i64", 1156841472, ["gen-wasm"] - ) - "Num.toU64", u64, ( - to_u64_same_width, "15i64", 15, ["gen-wasm"] - to_u64_extend, "15i8", 15, ["gen-wasm"] - to_u64_truncate, "115i128", 115 - to_u64_truncate_wraps, "10_000_000_000_000_000_000_000i128", 1864712049423024128 - ) - "Num.toU128", u128, ( - to_u128_same_width, "15i128", 15 - to_u128_extend, "15i8", 15 - ) - "Num.toNat", usize, ( - to_nat_same_width, "15i64", 15, ["gen-wasm"] - to_nat_extend, "15i8", 15, ["gen-wasm"] - to_nat_truncate, "115i128", 115 - to_nat_truncate_wraps, "10_000_000_000_000_000_000_000i128", 1864712049423024128 - ) - "Num.toF32", f32, ( - to_f32_from_i8, "15i8", 15.0 - to_f32_from_i16, "15i16", 15.0 - to_f32_from_i32, "15i32", 15.0 - to_f32_from_i64, "15i64", 15.0 - to_f32_from_i128, "15i128", 15.0 - to_f32_from_u8, "15u8", 15.0 - to_f32_from_u16, "15u16", 15.0 - to_f32_from_u32, "15u32", 15.0 - to_f32_from_u64, "15u64", 15.0 - to_f32_from_u128, "15u128", 15.0 - to_f32_from_nat, "15nat", 15.0 - to_f32_from_f32, "1.5f32", 1.5 - to_f32_from_f64, "1.5f64", 1.5 - ) - "Num.toF64", f64, ( - to_f64_from_i8, "15i8", 15.0 - to_f64_from_i16, "15i16", 15.0 - to_f64_from_i32, "15i32", 15.0 - to_f64_from_i64, "15i64", 15.0 - to_f64_from_i128, "15i128", 15.0 - to_f64_from_u8, "15u8", 15.0 - to_f64_from_u16, "15u16", 15.0 - to_f64_from_u32, "15u32", 15.0 - to_f64_from_u64, "15u64", 15.0 - to_f64_from_u128, "15u128", 15.0 - to_f64_from_nat, "15nat", 15.0 - to_f64_from_f32, "1.5f32", 1.5 - to_f64_from_f64, "1.5f64", 1.5 - ) -} - -macro_rules! to_int_checked_tests { - ($($fn:expr, $typ:ty, ($($test_name:ident, $input:expr, $output:expr)*))*) => {$($( - #[test] - #[cfg(any(feature = "gen-llvm"))] - fn $test_name() { - let sentinel = 23; - // Some n = Ok n, None = OutOfBounds - let expected = match $output.into() { - None => sentinel, - Some(n) => { - assert_ne!(n, sentinel); - n - } - }; - let input = format!("Result.withDefault ({} {}) {}", $fn, $input, sentinel); - assert_evals_to!(&input, expected, $typ) - } - )*)*} -} - -to_int_checked_tests! { - "Num.toI8Checked", i8, ( - to_i8_checked_same, "15i8", 15 - to_i8_checked_same_width_unsigned_fits, "15u8", 15 - to_i8_checked_same_width_unsigned_oob, "128u8", None - to_i8_checked_larger_width_signed_fits_pos, "15i16", 15 - to_i8_checked_larger_width_signed_oob_pos, "128i16", None - to_i8_checked_larger_width_signed_fits_neg, "-15i16", -15 - to_i8_checked_larger_width_signed_oob_neg, "-129i16", None - to_i8_checked_larger_width_unsigned_fits_pos, "15u16", 15 - to_i8_checked_larger_width_unsigned_oob_pos, "128u16", None - ) - "Num.toI16Checked", i16, ( - to_i16_checked_smaller_width_pos, "15i8", 15 - to_i16_checked_smaller_width_neg, "-15i8", -15 - to_i16_checked_same, "15i16", 15 - to_i16_checked_same_width_unsigned_fits, "15u16", 15 - to_i16_checked_same_width_unsigned_oob, "32768u16", None - to_i16_checked_larger_width_signed_fits_pos, "15i32", 15 - to_i16_checked_larger_width_signed_oob_pos, "32768i32", None - to_i16_checked_larger_width_signed_fits_neg, "-15i32", -15 - to_i16_checked_larger_width_signed_oob_neg, "-32769i32", None - to_i16_checked_larger_width_unsigned_fits_pos, "15u32", 15 - to_i16_checked_larger_width_unsigned_oob_pos, "32768u32", None - ) - "Num.toI32Checked", i32, ( - to_i32_checked_smaller_width_pos, "15i8", 15 - to_i32_checked_smaller_width_neg, "-15i8", -15 - to_i32_checked_same, "15i32", 15 - to_i32_checked_same_width_unsigned_fits, "15u32", 15 - to_i32_checked_same_width_unsigned_oob, "2147483648u32", None - to_i32_checked_larger_width_signed_fits_pos, "15i64", 15 - to_i32_checked_larger_width_signed_oob_pos, "2147483648i64", None - to_i32_checked_larger_width_signed_fits_neg, "-15i64", -15 - to_i32_checked_larger_width_signed_oob_neg, "-2147483649i64", None - to_i32_checked_larger_width_unsigned_fits_pos, "15u64", 15 - to_i32_checked_larger_width_unsigned_oob_pos, "2147483648u64", None - ) - "Num.toI64Checked", i64, ( - to_i64_checked_smaller_width_pos, "15i8", 15 - to_i64_checked_smaller_width_neg, "-15i8", -15 - to_i64_checked_same, "15i64", 15 - to_i64_checked_same_width_unsigned_fits, "15u64", 15 - to_i64_checked_same_width_unsigned_oob, "9223372036854775808u64", None - to_i64_checked_larger_width_signed_fits_pos, "15i128", 15 - to_i64_checked_larger_width_signed_oob_pos, "9223372036854775808i128", None - to_i64_checked_larger_width_signed_fits_neg, "-15i128", -15 - to_i64_checked_larger_width_signed_oob_neg, "-9223372036854775809i128", None - to_i64_checked_larger_width_unsigned_fits_pos, "15u128", 15 - to_i64_checked_larger_width_unsigned_oob_pos, "9223372036854775808u128", None - ) - "Num.toI128Checked", i128, ( - to_i128_checked_smaller_width_pos, "15i8", 15 - to_i128_checked_smaller_width_neg, "-15i8", -15 - to_i128_checked_same, "15i128", 15 - to_i128_checked_same_width_unsigned_fits, "15u128", 15 - to_i128_checked_same_width_unsigned_oob, "170141183460469231731687303715884105728u128", None - ) - "Num.toU8Checked", u8, ( - to_u8_checked_same, "15u8", 15 - to_u8_checked_same_width_signed_fits, "15i8", 15 - to_u8_checked_same_width_signed_oob, "-1i8", None - to_u8_checked_larger_width_signed_fits_pos, "15i16", 15 - to_u8_checked_larger_width_signed_oob_pos, "256i16", None - to_u8_checked_larger_width_signed_oob_neg, "-1i16", None - to_u8_checked_larger_width_unsigned_fits_pos, "15u16", 15 - to_u8_checked_larger_width_unsigned_oob_pos, "256u16", None - ) - "Num.toU16Checked", u16, ( - to_u16_checked_smaller_width_pos, "15i8", 15 - to_u16_checked_smaller_width_neg_oob, "-15i8", None - to_u16_checked_same, "15u16", 15 - to_u16_checked_same_width_signed_fits, "15i16", 15 - to_u16_checked_same_width_signed_oob, "-1i16", None - to_u16_checked_larger_width_signed_fits_pos, "15i32", 15 - to_u16_checked_larger_width_signed_oob_pos, "65536i32", None - to_u16_checked_larger_width_signed_oob_neg, "-1i32", None - to_u16_checked_larger_width_unsigned_fits_pos, "15u32", 15 - to_u16_checked_larger_width_unsigned_oob_pos, "65536u32", None - ) - "Num.toU32Checked", u32, ( - to_u32_checked_smaller_width_pos, "15i8", 15 - to_u32_checked_smaller_width_neg_oob, "-15i8", None - to_u32_checked_same, "15u32", 15 - to_u32_checked_same_width_signed_fits, "15i32", 15 - to_u32_checked_same_width_signed_oob, "-1i32", None - to_u32_checked_larger_width_signed_fits_pos, "15i64", 15 - to_u32_checked_larger_width_signed_oob_pos, "4294967296i64", None - to_u32_checked_larger_width_signed_oob_neg, "-1i64", None - to_u32_checked_larger_width_unsigned_fits_pos, "15u64", 15 - to_u32_checked_larger_width_unsigned_oob_pos, "4294967296u64", None - ) - "Num.toU64Checked", u64, ( - to_u64_checked_smaller_width_pos, "15i8", 15 - to_u64_checked_smaller_width_neg_oob, "-15i8", None - to_u64_checked_same, "15u64", 15 - to_u64_checked_same_width_signed_fits, "15i64", 15 - to_u64_checked_same_width_signed_oob, "-1i64", None - to_u64_checked_larger_width_signed_fits_pos, "15i128", 15 - to_u64_checked_larger_width_signed_oob_pos, "18446744073709551616i128", None - to_u64_checked_larger_width_signed_oob_neg, "-1i128", None - to_u64_checked_larger_width_unsigned_fits_pos, "15u128", 15 - to_u64_checked_larger_width_unsigned_oob_pos, "18446744073709551616u128", None - ) - "Num.toU128Checked", u128, ( - to_u128_checked_smaller_width_pos, "15i8", 15 - to_u128_checked_smaller_width_neg_oob, "-15i8", None - to_u128_checked_same, "15u128", 15 - to_u128_checked_same_width_signed_fits, "15i128", 15 - to_u128_checked_same_width_signed_oob, "-1i128", None - ) - "Num.toNatChecked", usize, ( - to_nat_checked_smaller_width_pos, "15i8", 15 - to_nat_checked_smaller_width_neg_oob, "-15i8", None - to_nat_checked_same, "15u64", 15 - to_nat_checked_same_width_signed_fits, "15i64", 15 - to_nat_checked_same_width_signed_oob, "-1i64", None - to_nat_checked_larger_width_signed_fits_pos, "15i128", 15 - to_nat_checked_larger_width_signed_oob_pos, "18446744073709551616i128", None - to_nat_checked_larger_width_signed_oob_neg, "-1i128", None - to_nat_checked_larger_width_unsigned_fits_pos, "15u128", 15 - to_nat_checked_larger_width_unsigned_oob_pos, "18446744073709551616u128", None - ) -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn is_multiple_of() { - // true - assert_evals_to!("Num.isMultipleOf 5 1", true, bool); - assert_evals_to!("Num.isMultipleOf 5 -1", true, bool); - assert_evals_to!("Num.isMultipleOf 0 0", true, bool); - assert_evals_to!("Num.isMultipleOf 0 1", true, bool); - assert_evals_to!("Num.isMultipleOf 0 -1", true, bool); - // false - assert_evals_to!("Num.isMultipleOf 5 2", false, bool); - assert_evals_to!("Num.isMultipleOf 5 0", false, bool); - - // overflow - assert_evals_to!("Num.isMultipleOf -9223372036854775808 -1", true, bool); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn bytes_to_u16_clearly_out_of_bounds() { - assert_evals_to!( - indoc!( - r#" - bytes = Str.toUtf8 "hello" - when Num.bytesToU16 bytes 234 is - Ok v -> v - Err OutOfBounds -> 1 - "# - ), - 1, - u16 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn bytes_to_u16_subtly_out_of_bounds() { - assert_evals_to!( - indoc!( - r#" - bytes = Str.toUtf8 "hello" - when Num.bytesToU16 bytes 4 is - Ok v -> v - Err OutOfBounds -> 1 - "# - ), - 1, - u16 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn bytes_to_u32_clearly_out_of_bounds() { - assert_evals_to!( - indoc!( - r#" - bytes = Str.toUtf8 "hello" - when Num.bytesToU32 bytes 234 is - Ok v -> v - Err OutOfBounds -> 1 - "# - ), - 1, - u32 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn bytes_to_u32_subtly_out_of_bounds() { - assert_evals_to!( - indoc!( - r#" - bytes = Str.toUtf8 "hello" - when Num.bytesToU32 bytes 2 is - Ok v -> v - Err OutOfBounds -> 1 - "# - ), - 1, - u32 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn bytes_to_u16_max_u8s() { - assert_evals_to!( - indoc!( - r#" - when Num.bytesToU16 [255, 255] 0 is - Ok v -> v - Err OutOfBounds -> 1 - "# - ), - 65535, - u16 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn bytes_to_u16_min_u8s() { - assert_evals_to!( - indoc!( - r#" - when Num.bytesToU16 [0, 0] 0 is - Ok v -> v - Err OutOfBounds -> 1 - "# - ), - 0, - u16 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn bytes_to_u16_random_u8s() { - assert_evals_to!( - indoc!( - r#" - when Num.bytesToU16 [164, 215] 0 is - Ok v -> v - Err OutOfBounds -> 1 - "# - ), - 55_204, - u16 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn bytes_to_u32_min_u8s() { - assert_evals_to!( - indoc!( - r#" - when Num.bytesToU32 [0, 0, 0, 0] 0 is - Ok v -> v - Err OutOfBounds -> 1 - "# - ), - 0, - u32 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn bytes_to_u32_max_u8s() { - assert_evals_to!( - indoc!( - r#" - when Num.bytesToU32 [255, 255, 255, 255] 0 is - Ok v -> v - Err OutOfBounds -> 1 - "# - ), - 4_294_967_295, - u32 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn bytes_to_u32_random_u8s() { - assert_evals_to!( - indoc!( - r#" - when Num.bytesToU32 [252, 124, 128, 121] 0 is - Ok v -> v - Err OutOfBounds -> 1 - "# - ), - 2_038_463_740, - u32 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn when_on_i32() { - assert_evals_to!( - indoc!( - r#" - app "test" provides [main] to "./platform" - - x : I32 - x = 0 - - main : I32 - main = - when x is - 0 -> 42 - _ -> -1 - "# - ), - 42, - i32 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn when_on_i16() { - assert_evals_to!( - indoc!( - r#" - app "test" provides [main] to "./platform" - - x : I16 - x = 0 - - main : I16 - main = - when x is - 0 -> 42 - _ -> -1 - "# - ), - 42, - i16 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn num_to_str() { - use roc_std::RocStr; - - assert_evals_to!(r#"Num.toStr 1234"#, RocStr::from("1234"), RocStr); - assert_evals_to!(r#"Num.toStr 0"#, RocStr::from("0"), RocStr); - assert_evals_to!(r#"Num.toStr -1"#, RocStr::from("-1"), RocStr); - - let max = format!("{}", i64::MAX); - assert_evals_to!( - r#"Num.toStr Num.maxI64"#, - RocStr::from(max.as_str()), - RocStr - ); - - let min = format!("{}", i64::MIN); - assert_evals_to!( - r#"Num.toStr Num.minI64"#, - RocStr::from(min.as_str()), - RocStr - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn num_to_str_u8() { - use roc_std::RocStr; - - assert_evals_to!(r#"Num.toStr 0u8"#, RocStr::from("0"), RocStr); - assert_evals_to!(r#"Num.toStr 1u8"#, RocStr::from("1"), RocStr); - assert_evals_to!(r#"Num.toStr 10u8"#, RocStr::from("10"), RocStr); - - let max = format!("{}", u8::MAX); - assert_evals_to!(r#"Num.toStr Num.maxU8"#, RocStr::from(max.as_str()), RocStr); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn num_to_str_u16() { - use roc_std::RocStr; - - assert_evals_to!(r#"Num.toStr 0u16"#, RocStr::from("0"), RocStr); - assert_evals_to!(r#"Num.toStr 1u16"#, RocStr::from("1"), RocStr); - assert_evals_to!(r#"Num.toStr 10u16"#, RocStr::from("10"), RocStr); - - let max = format!("{}", u16::MAX); - assert_evals_to!( - r#"Num.toStr Num.maxU16"#, - RocStr::from(max.as_str()), - RocStr - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn num_to_str_u32() { - use roc_std::RocStr; - - assert_evals_to!(r#"Num.toStr 0u32"#, RocStr::from("0"), RocStr); - assert_evals_to!(r#"Num.toStr 1u32"#, RocStr::from("1"), RocStr); - assert_evals_to!(r#"Num.toStr 10u32"#, RocStr::from("10"), RocStr); - - let max = format!("{}", u32::MAX); - assert_evals_to!( - r#"Num.toStr Num.maxU32"#, - RocStr::from(max.as_str()), - RocStr - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn num_to_str_u64() { - use roc_std::RocStr; - - assert_evals_to!(r#"Num.toStr 0u64"#, RocStr::from("0"), RocStr); - assert_evals_to!(r#"Num.toStr 1u64"#, RocStr::from("1"), RocStr); - assert_evals_to!(r#"Num.toStr 10u64"#, RocStr::from("10"), RocStr); - - let max = format!("{}", u64::MAX); - assert_evals_to!( - r#"Num.toStr Num.maxU64"#, - RocStr::from(max.as_str()), - RocStr - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn num_to_str_i8() { - use roc_std::RocStr; - - assert_evals_to!(r#"Num.toStr -10i8"#, RocStr::from("-10"), RocStr); - assert_evals_to!(r#"Num.toStr -1i8"#, RocStr::from("-1"), RocStr); - assert_evals_to!(r#"Num.toStr 0i8"#, RocStr::from("0"), RocStr); - assert_evals_to!(r#"Num.toStr 1i8"#, RocStr::from("1"), RocStr); - assert_evals_to!(r#"Num.toStr 10i8"#, RocStr::from("10"), RocStr); - - let max = format!("{}", i8::MAX); - assert_evals_to!(r#"Num.toStr Num.maxI8"#, RocStr::from(max.as_str()), RocStr); - - let max = format!("{}", i8::MIN); - assert_evals_to!(r#"Num.toStr Num.minI8"#, RocStr::from(max.as_str()), RocStr); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn num_to_str_i16() { - use roc_std::RocStr; - - assert_evals_to!(r#"Num.toStr -10i16"#, RocStr::from("-10"), RocStr); - assert_evals_to!(r#"Num.toStr -1i16"#, RocStr::from("-1"), RocStr); - assert_evals_to!(r#"Num.toStr 0i16"#, RocStr::from("0"), RocStr); - assert_evals_to!(r#"Num.toStr 1i16"#, RocStr::from("1"), RocStr); - assert_evals_to!(r#"Num.toStr 10i16"#, RocStr::from("10"), RocStr); - - let max = format!("{}", i16::MAX); - assert_evals_to!( - r#"Num.toStr Num.maxI16"#, - RocStr::from(max.as_str()), - RocStr - ); - - let max = format!("{}", i16::MIN); - assert_evals_to!( - r#"Num.toStr Num.minI16"#, - RocStr::from(max.as_str()), - RocStr - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn num_to_str_i32() { - use roc_std::RocStr; - - assert_evals_to!(r#"Num.toStr -10i32"#, RocStr::from("-10"), RocStr); - assert_evals_to!(r#"Num.toStr -1i32"#, RocStr::from("-1"), RocStr); - assert_evals_to!(r#"Num.toStr 0i32"#, RocStr::from("0"), RocStr); - assert_evals_to!(r#"Num.toStr 1i32"#, RocStr::from("1"), RocStr); - assert_evals_to!(r#"Num.toStr 10i32"#, RocStr::from("10"), RocStr); - - let max = format!("{}", i32::MAX); - assert_evals_to!( - r#"Num.toStr Num.maxI32"#, - RocStr::from(max.as_str()), - RocStr - ); - - let max = format!("{}", i32::MIN); - assert_evals_to!( - r#"Num.toStr Num.minI32"#, - RocStr::from(max.as_str()), - RocStr - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn num_to_str_i64() { - use roc_std::RocStr; - - assert_evals_to!(r#"Num.toStr -10i64"#, RocStr::from("-10"), RocStr); - assert_evals_to!(r#"Num.toStr -1i64"#, RocStr::from("-1"), RocStr); - assert_evals_to!(r#"Num.toStr 0i64"#, RocStr::from("0"), RocStr); - assert_evals_to!(r#"Num.toStr 1i64"#, RocStr::from("1"), RocStr); - assert_evals_to!(r#"Num.toStr 10i64"#, RocStr::from("10"), RocStr); - - let max = format!("{}", i64::MAX); - assert_evals_to!( - r#"Num.toStr Num.maxI64"#, - RocStr::from(max.as_str()), - RocStr - ); - - let max = format!("{}", i64::MIN); - assert_evals_to!( - r#"Num.toStr Num.minI64"#, - RocStr::from(max.as_str()), - RocStr - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn u8_addition_greater_than_i8() { - assert_evals_to!( - indoc!( - r#" - x : U8 - x = 100 - y : U8 - y = 100 - x + y - "# - ), - 200, - u8 - ) -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn u8_sub_greater_than_i8() { - assert_evals_to!( - indoc!( - r#" - x : U8 - x = 255 - y : U8 - y = 55 - x - y - "# - ), - 200, - u8 - ) -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn u8_mul_greater_than_i8() { - assert_evals_to!( - indoc!( - r#" - x : U8 - x = 40 - y : U8 - y = 5 - x * y - "# - ), - 200, - u8 - ) -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn add_saturated() { - assert_evals_to!( - indoc!( - r#" - x : U8 - x = 200 - y : U8 - y = 200 - Num.addSaturated x y - "# - ), - 255, - u8 - ) -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn sub_saturated() { - assert_evals_to!( - indoc!( - r#" - x : U8 - x = 10 - y : U8 - y = 20 - Num.subSaturated x y - "# - ), - 0, - u8 - ) -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn monomorphized_ints() { - assert_evals_to!( - indoc!( - r#" - x = 100 - - f : U8, U32 -> Nat - f = \_, _ -> 18 - - f x x - "# - ), - 18, - u64 - ) -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn monomorphized_floats() { - assert_evals_to!( - indoc!( - r#" - x = 100.0 - - f : F32, F64 -> Nat - f = \_, _ -> 18 - - f x x - "# - ), - 18, - u64 - ) -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn monomorphized_ints_names_dont_conflict() { - assert_evals_to!( - indoc!( - r#" - f : U8 -> Nat - f = \_ -> 9 - x = - n = 100 - f n - - y = - n = 100 - f n - - x + y - "# - ), - 18, - u64 - ) -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn monomorphized_ints_aliased() { - assert_evals_to!( - indoc!( - r#" - app "test" provides [main] to "./platform" - - main = - y = 100 - w1 = y - w2 = y - - f1 : U8, U32 -> U8 - f1 = \_, _ -> 1 - - f2 : U32, U8 -> U8 - f2 = \_, _ -> 2 - - f1 w1 w2 + f2 w1 w2 - "# - ), - 3, - u8 - ) -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn to_float_f32() { - assert_evals_to!( - indoc!( - r#" - n : U8 - n = 100 - - f : F32 - f = Num.toFrac n - f - "# - ), - 100., - f32 - ) -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn to_float_f64() { - assert_evals_to!( - indoc!( - r#" - n : U8 - n = 100 - - f : F64 - f = Num.toFrac n - f - "# - ), - 100., - f64 - ) -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -// https://github.com/rtfeldman/roc/issues/2696 -fn upcast_of_int_is_zext() { - assert_evals_to!( - indoc!( - r#" - Num.toU16 0b1000_0000u8 - "# - ), - 128, - u16 - ) -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -// https://github.com/rtfeldman/roc/issues/2696 -fn upcast_of_int_checked_is_zext() { - assert_evals_to!( - indoc!( - r#" - when Num.toU16Checked 0b1000_0000u8 is - Ok 128u16 -> 1u8 - _ -> 0u8 - "# - ), - 1, - u8 - ) -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn modulo_of_unsigned() { - assert_evals_to!( - indoc!( - r#" - 0b1111_1111u8 % 64 - "# - ), - 63, - u8 - ) -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn div_of_unsigned() { - assert_evals_to!( - indoc!( - r#" - 0b1111_1111u8 // 2 - "# - ), - 127, - u8 - ) -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn dec_float_suffix() { - assert_evals_to!( - indoc!( - r#" - 123.0dec - "# - ), - RocDec::from_str_to_i128_unsafe("123.0"), - i128 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn dec_no_decimal() { - assert_evals_to!( - indoc!( - r#" - 3dec - "# - ), - RocDec::from_str_to_i128_unsafe("3.0"), - i128 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn ceiling_to_u32() { - assert_evals_to!( - indoc!( - r#" - n : U32 - n = Num.ceiling 124.5 - n - "# - ), - 125, - u32 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn floor_to_u32() { - assert_evals_to!( - indoc!( - r#" - n : U32 - n = Num.floor 124.5 - n - "# - ), - 124, - u32 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn round_to_u32() { - assert_evals_to!( - indoc!( - r#" - n : U32 - n = Num.round 124.49 - n - "# - ), - 124, - u32 - ); -} diff --git a/compiler/test_gen/src/gen_primitives.rs b/compiler/test_gen/src/gen_primitives.rs deleted file mode 100644 index e777418559..0000000000 --- a/compiler/test_gen/src/gen_primitives.rs +++ /dev/null @@ -1,3472 +0,0 @@ -#[cfg(feature = "gen-llvm")] -use crate::helpers::llvm::assert_evals_to; -#[cfg(feature = "gen-llvm")] -use crate::helpers::llvm::assert_non_opt_evals_to; - -#[cfg(feature = "gen-dev")] -use crate::helpers::dev::assert_evals_to; -// #[cfg(feature = "gen-dev")] -// use crate::helpers::dev::assert_evals_to as assert_llvm_evals_to; -// #[cfg(feature = "gen-dev")] -// use crate::helpers::dev::assert_evals_to as assert_non_opt_evals_to; - -#[cfg(feature = "gen-wasm")] -use crate::helpers::wasm::assert_evals_to; -#[cfg(feature = "gen-wasm")] -use crate::helpers::wasm::assert_evals_to as assert_non_opt_evals_to; -// #[cfg(feature = "gen-wasm")] -// use crate::helpers::wasm::assert_evals_to as assert_llvm_evals_to; -// #[cfg(feature = "gen-wasm")] -// use crate::helpers::wasm::assert_evals_to as assert_non_opt_evals_to; - -use indoc::indoc; -#[allow(unused_imports)] -use roc_std::RocStr; - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))] -fn basic_int() { - assert_evals_to!("123", 123, i64); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))] -fn basic_float() { - assert_evals_to!("1234.0", 1234.0, f64); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn branch_first_float() { - assert_evals_to!( - indoc!( - r#" - when 1.23 is - 1.23 -> 12 - _ -> 34 - "# - ), - 12, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn branch_second_float() { - assert_evals_to!( - indoc!( - r#" - when 2.34 is - 1.23 -> 63 - _ -> 48 - "# - ), - 48, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn branch_third_float() { - assert_evals_to!( - indoc!( - r#" - when 10.0 is - 1.0 -> 63 - 2.0 -> 48 - _ -> 112 - "# - ), - 112, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))] -fn branch_first_int() { - assert_evals_to!( - indoc!( - r#" - when 1 is - 1 -> 12 - _ -> 34 - "# - ), - 12, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))] -fn branch_second_int() { - assert_evals_to!( - indoc!( - r#" - when 2 is - 1 -> 63 - _ -> 48 - "# - ), - 48, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-dev"))] -fn branch_third_int() { - assert_evals_to!( - indoc!( - r#" - when 10 is - 1 -> 63 - 2 -> 48 - _ -> 112 - "# - ), - 112, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))] -fn branch_store_variable() { - assert_evals_to!( - indoc!( - r#" - when 0 is - 1 -> 12 - a -> a - "# - ), - 0, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-dev"))] -fn when_one_element_tag() { - assert_evals_to!( - indoc!( - r#" - x : [Pair (Int a) (Int a)] - x = Pair 0x2 0x3 - - when x is - Pair l r -> l + r - "# - ), - 5, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn when_two_element_tag_first() { - assert_evals_to!( - indoc!( - r#" - x : [A (Int *), B (Int *)] - x = A 0x2 - - when x is - A v -> v - B v -> v - "# - ), - 2, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn when_two_element_tag_second() { - assert_evals_to!( - indoc!( - r#" - x : [A (Int *), B (Int *)] - x = B 0x3 - - when x is - A v -> v - B v -> v - "# - ), - 3, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))] -fn gen_when_one_branch() { - assert_evals_to!( - indoc!( - r#" - when 1.23 is - _ -> 23 - "# - ), - 23, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn gen_large_when_int() { - assert_evals_to!( - indoc!( - r#" - foo = \num -> - when num is - 0 -> 200 - -3 -> 111 # TODO adding more negative numbers reproduces parsing bugs here - 3 -> 789 - 1 -> 123 - 2 -> 456 - _ -> 1000 - - foo -3 - "# - ), - 111, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-wasm"))] -fn gen_large_when_float() { - assert_evals_to!( - indoc!( - r#" - foo = \num -> - when num is - 0.5 -> 200.1 - -3.6 -> 111.2 # TODO adding more negative numbers reproduces parsing bugs here - 3.6 -> 789.5 - 1.7 -> 123.3 - 2.8 -> 456.4 - _ -> 1000.6 - - foo -3.6 - "# - ), - 111.2, - f64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))] -fn or_pattern() { - assert_evals_to!( - indoc!( - r#" - when 2 is - 1 | 2 -> 42 - _ -> 1 - "# - ), - 42, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))] -fn apply_identity() { - assert_evals_to!( - indoc!( - r#" - identity = \a -> a - - identity 5 - "# - ), - 5, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn apply_unnamed_identity() { - assert_evals_to!( - indoc!( - r#" - wrapper = \{} -> - (\a -> a) 5 - - wrapper {} - "# - ), - 5, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn return_unnamed_fn() { - assert_evals_to!( - indoc!( - r#" - wrapper = \{} -> - alwaysFloatIdentity : Int * -> (Frac a -> Frac a) - alwaysFloatIdentity = \_ -> - (\a -> a) - - (alwaysFloatIdentity 2) 1.23 - - wrapper {} - "# - ), - 1.23, - f64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn gen_when_fn() { - assert_evals_to!( - indoc!( - r#" - limitedNegate = \num -> - when num is - 1 -> -1 - -1 -> 1 - _ -> num - - limitedNegate 1 - "# - ), - -1, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))] -fn gen_basic_def() { - assert_evals_to!( - indoc!( - r#" - answer = 42 - - answer - "# - ), - 42, - i64 - ); - - assert_evals_to!( - indoc!( - r#" - float = 1.23 - - float - "# - ), - 1.23, - f64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn gen_multiple_defs() { - assert_evals_to!( - indoc!( - r#" - answer = 42 - - float = 1.23 - - if float > 3 then answer else answer - "# - ), - 42, - i64 - ); - - assert_evals_to!( - indoc!( - r#" - answer = 42 - - float = 1.23 - - if answer > 3 then float else float - "# - ), - 1.23, - f64 - ); -} - -// These tests caught a bug in how Defs are converted to the mono IR -// but they have UnusedDef or UnusedArgument problems, and don't run any more -// #[test] -// #[cfg(any(feature = "gen-llvm"))] -// fn gen_chained_defs() { -// assert_evals_to!( -// indoc!( -// r#" -// x = i1 -// i3 = i2 -// i1 = 1337 -// i2 = i1 -// y = 12.4 -// -// i3 -// "# -// ), -// 1337, -// i64 -// ); -// } -// -// #[test] -// #[cfg(any(feature = "gen-llvm"))] -// fn gen_nested_defs_old() { -// assert_evals_to!( -// indoc!( -// r#" -// x = 5 -// -// answer = -// i3 = i2 -// -// nested = -// a = 1.0 -// b = 5 -// -// i1 -// -// i1 = 1337 -// i2 = i1 -// -// -// nested -// -// # None of this should affect anything, even though names -// # overlap with the previous nested defs -// unused = -// nested = 17 -// -// i1 = 84.2 -// -// nested -// -// y = 12.4 -// -// answer -// "# -// ), -// 1337, -// i64 -// ); -// } -// -// #[test] -// #[cfg(any(feature = "gen-llvm"))] -// fn let_x_in_x() { -// assert_evals_to!( -// indoc!( -// r#" -// x = 5 -// -// answer = -// 1337 -// -// unused = -// nested = 17 -// nested -// -// answer -// "# -// ), -// 1337, -// i64 -// ); -// } - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))] -fn factorial() { - assert_evals_to!( - indoc!( - r#" - factorial = \n, accum -> - when n is - 0 -> - accum - - _ -> - factorial (n - 1) (n * accum) - - factorial 10 1 - "# - ), - 3628800, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn peano1() { - assert_non_opt_evals_to!( - indoc!( - r#" - Peano : [S Peano, Z] - - three : Peano - three = S (S (S Z)) - - when three is - Z -> 2 - S _ -> 1 - "# - ), - 1, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn peano2() { - assert_non_opt_evals_to!( - indoc!( - r#" - Peano : [S Peano, Z] - - three : Peano - three = S (S (S Z)) - - when three is - S (S _) -> 1 - S (_) -> 0 - Z -> 0 - "# - ), - 1, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))] -fn top_level_constant() { - assert_evals_to!( - indoc!( - r#" - app "test" provides [main] to "./platform" - - float = 1.2315 - - main = - float + float - "# - ), - 1.2315 + 1.2315, - f64 - ); -} - -#[test] -#[ignore] -#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))] -fn top_level_destructure() { - assert_evals_to!( - indoc!( - r#" - app "test" provides [main] to "./platform" - - {a, b} = { a: 1, b: 2 } - - main = - - a + b - "# - ), - 3, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn linked_list_len_0() { - assert_non_opt_evals_to!( - indoc!( - r#" - app "test" provides [main] to "./platform" - - LinkedList a : [Nil, Cons a (LinkedList a)] - - len : LinkedList a -> Int * - len = \list -> - when list is - Nil -> 0 - Cons _ rest -> 1 + len rest - - main = - nil : LinkedList F64 - nil = Nil - - len nil - "# - ), - 0, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn linked_list_len_twice_0() { - assert_non_opt_evals_to!( - indoc!( - r#" - app "test" provides [main] to "./platform" - - LinkedList a : [Nil, Cons a (LinkedList a)] - - nil : LinkedList I64 - nil = Nil - - length : LinkedList a -> Int * - length = \list -> - when list is - Nil -> 0 - Cons _ rest -> 1 + length rest - - main = - length nil + length nil - "# - ), - 0, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn linked_list_len_1() { - assert_non_opt_evals_to!( - indoc!( - r#" - app "test" provides [main] to "./platform" - - LinkedList a : [Nil, Cons a (LinkedList a)] - - one : LinkedList (Int *) - one = Cons 1 Nil - - length : LinkedList a -> Int * - length = \list -> - when list is - Nil -> 0 - Cons _ rest -> 1 + length rest - - main = - length one - "# - ), - 1, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn linked_list_len_twice_1() { - assert_non_opt_evals_to!( - indoc!( - r#" - app "test" provides [main] to "./platform" - - LinkedList a : [Nil, Cons a (LinkedList a)] - - one : LinkedList (Int *) - one = Cons 1 Nil - - length : LinkedList a -> Int * - length = \list -> - when list is - Nil -> 0 - Cons _ rest -> 1 + length rest - - main = - length one + length one - "# - ), - 2, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn linked_list_len_3() { - assert_non_opt_evals_to!( - indoc!( - r#" - app "test" provides [main] to "./platform" - - LinkedList a : [Nil, Cons a (LinkedList a)] - - three : LinkedList (Int *) - three = Cons 3 (Cons 2 (Cons 1 Nil)) - - length : LinkedList a -> Int * - length = \list -> - when list is - Nil -> 0 - Cons _ rest -> 1 + length rest - - - main = - length three - "# - ), - 3, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn linked_list_sum_num_a() { - assert_non_opt_evals_to!( - indoc!( - r#" - app "test" provides [main] to "./platform" - - LinkedList a : [Nil, Cons a (LinkedList a)] - - three : LinkedList (Int *) - three = Cons 3 (Cons 2 (Cons 1 Nil)) - - - sum : LinkedList (Num a) -> Num a - sum = \list -> - when list is - Nil -> 0 - Cons x rest -> x + sum rest - - main = - sum three - "# - ), - 3 + 2 + 1, - i64 - ) -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn linked_list_sum_int() { - assert_non_opt_evals_to!( - indoc!( - r#" - app "test" provides [main] to "./platform" - - LinkedList a : [Nil, Cons a (LinkedList a)] - - zero : LinkedList (Int *) - zero = Nil - - sum : LinkedList (Int a) -> Int a - sum = \list -> - when list is - Nil -> 0 - Cons x rest -> x + sum rest - - main = - sum zero - "# - ), - 0, - i64 - ) -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn linked_list_map() { - assert_non_opt_evals_to!( - indoc!( - r#" - app "test" provides [main] to "./platform" - - LinkedList a : [Nil, Cons a (LinkedList a)] - - three : LinkedList (Int *) - three = Cons 3 (Cons 2 (Cons 1 Nil)) - - sum : LinkedList (Num a) -> Num a - sum = \list -> - when list is - Nil -> 0 - Cons x rest -> x + sum rest - - map : (a -> b), LinkedList a -> LinkedList b - map = \f, list -> - when list is - Nil -> Nil - Cons x rest -> Cons (f x) (map f rest) - - main = - sum (map (\_ -> 1) three) - "# - ), - 3, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn when_nested_maybe() { - assert_evals_to!( - indoc!( - r#" - Maybe a : [Nothing, Just a] - - x : Maybe (Maybe (Int a)) - x = Just (Just 41) - - when x is - Just (Just v) -> v + 0x1 - _ -> 0x1 - "# - ), - 42, - i64 - ); - - assert_evals_to!( - indoc!( - r#" - Maybe a : [Nothing, Just a] - - x : Maybe (Maybe (Int *)) - x = Just Nothing - - when x is - Just (Just v) -> v + 0x1 - Just Nothing -> 0x2 - Nothing -> 0x1 - "# - ), - 2, - i64 - ); - - assert_evals_to!( - indoc!( - r#" - Maybe a : [Nothing, Just a] - - x : Maybe (Maybe (Int *)) - x = Nothing - - when x is - Just (Just v) -> v + 0x1 - Just Nothing -> 0x2 - Nothing -> 0x1 - "# - ), - 1, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn when_peano() { - assert_non_opt_evals_to!( - indoc!( - r#" - Peano : [S Peano, Z] - - three : Peano - three = S (S (S Z)) - - when three is - S (S _) -> 1 - S (_) -> 2 - Z -> 3 - "# - ), - 1, - i64 - ); - - assert_non_opt_evals_to!( - indoc!( - r#" - Peano : [S Peano, Z] - - three : Peano - three = S Z - - when three is - S (S _) -> 1 - S (_) -> 2 - Z -> 3 - "# - ), - 2, - i64 - ); - - assert_non_opt_evals_to!( - indoc!( - r#" - Peano : [S Peano, Z] - - three : Peano - three = Z - - when three is - S (S _) -> 1 - S (_) -> 2 - Z -> 3 - "# - ), - 3, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -#[should_panic(expected = "Roc failed with message: ")] -fn overflow_frees_list() { - assert_evals_to!( - indoc!( - r#" - myList = [1,2,3] - - # integer overflow; must use the list so it is defined before the overflow - # the list will then be freed in a cleanup block - n : I64 - n = 9_223_372_036_854_775_807 + (Num.intCast (List.len myList)) - - index : Nat - index = Num.intCast n - - List.get myList index - "# - ), - 3, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -#[should_panic(expected = "Roc failed with message: ")] -fn undefined_variable() { - assert_evals_to!( - indoc!( - r#" - if True then - x + z - else - y + z - "# - ), - 3, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -#[should_panic(expected = "Roc failed with message: ")] -fn annotation_without_body() { - assert_evals_to!( - indoc!( - r#" - foo : Int * - - - foo - "# - ), - 3, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))] -fn simple_closure() { - assert_evals_to!( - indoc!( - r#" - app "test" provides [main] to "./platform" - - x = 42 - - f = \{} -> x - - - main = - f {} - "# - ), - 42, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn nested_closure() { - assert_evals_to!( - indoc!( - r#" - app "test" provides [main] to "./platform" - - foo = \{} -> - x = 41 - y = 1 - f = \{} -> x + y - f - - main = - g = foo {} - g {} - "# - ), - 42, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn closure_in_list() { - assert_evals_to!( - indoc!( - r#" - app "test" provides [main] to "./platform" - - foo = \{} -> - x = 41 - - f = \{} -> x - - [f] - - main = - items = foo {} - - List.len items - "# - ), - 1, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn specialize_closure() { - use roc_std::RocList; - - assert_evals_to!( - indoc!( - r#" - app "test" provides [main] to "./platform" - - foo = \{} -> - x = 41 - y = [1] - - f = \{} -> x - g = \{} -> x + Num.intCast (List.len y) - - [f, g] - - apply = \f -> f {} - - main = - items = foo {} - - List.map items apply - "# - ), - RocList::from_slice(&[41, 42]), - RocList - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn io_poc_effect() { - assert_non_opt_evals_to!( - indoc!( - r#" - app "test" provides [main] to "./platform" - - Effect a := {} -> a - - succeed : a -> Effect a - succeed = \x -> @Effect \{} -> x - - runEffect : Effect a -> a - runEffect = \@Effect thunk -> thunk {} - - foo : Effect F64 - foo = - succeed 1.23 - - main : F64 - main = - runEffect foo - - "# - ), - 1.23, - f64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn io_poc_desugared() { - assert_evals_to!( - indoc!( - r#" - app "test" provides [main] to "./platform" - - # succeed : a -> ({} -> a) - succeed = \x -> \_ -> x - - foo : Str -> F64 - foo = - succeed 1.23 - - # runEffect : ({} -> a) -> a - runEffect = \thunk -> thunk "" - - main : F64 - main = - runEffect foo - "# - ), - 1.23, - f64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn return_wrapped_function_pointer() { - assert_non_opt_evals_to!( - indoc!( - r#" - app "test" provides [main] to "./platform" - - Effect a := {} -> a - - foo : Effect {} - foo = @Effect \{} -> {} - - main : Effect {} - main = foo - "# - ), - 1, - i64, - |_| 1 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn return_wrapped_function_pointer_b() { - assert_non_opt_evals_to!( - indoc!( - r#" - app "test" provides [main] to "./platform" - - - foo : { x: (I64 -> Str) } - foo = { x: (\_ -> "foobar") } - - main : { x: (I64 -> Str) } - main = foo - "# - ), - 1, - i64, - |_| 1 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn return_wrapped_closure() { - assert_non_opt_evals_to!( - indoc!( - r#" - app "test" provides [main] to "./platform" - - Effect a := {} -> a - - foo : Effect {} - foo = - x = 5 - - @Effect (\{} -> if x > 3 then {} else {}) - - main : Effect {} - main = foo - "# - ), - 1, - i64, - |_| 1 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn linked_list_is_singleton() { - assert_non_opt_evals_to!( - indoc!( - r#" - app "test" provides [main] to "./platform" - - ConsList a : [Cons a (ConsList a), Nil] - - empty : ConsList a - empty = Nil - - isSingleton : ConsList a -> Bool - isSingleton = \list -> - when list is - Cons _ Nil -> - True - - _ -> - False - - main : Bool - main = - myList : ConsList I64 - myList = empty - - isSingleton myList - "# - ), - false, - bool - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn linked_list_is_empty_1() { - assert_non_opt_evals_to!( - indoc!( - r#" - app "test" provides [main] to "./platform" - - ConsList a : [Cons a (ConsList a), Nil] - - empty : ConsList a - empty = Nil - - isEmpty : ConsList a -> Bool - isEmpty = \list -> - when list is - Cons _ _ -> - False - - Nil -> - True - - main : Bool - main = - myList : ConsList (Int *) - myList = empty - - isEmpty myList - "# - ), - true, - bool - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn linked_list_is_empty_2() { - assert_non_opt_evals_to!( - indoc!( - r#" - app "test" provides [main] to "./platform" - - ConsList a : [Cons a (ConsList a), Nil] - - isEmpty : ConsList a -> Bool - isEmpty = \list -> - when list is - Cons _ _ -> - False - - Nil -> - True - - main : Bool - main = - myList : ConsList I64 - myList = Cons 0x1 Nil - - isEmpty myList - "# - ), - false, - bool - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn linked_list_singleton() { - // verifies only that valid llvm is produced - assert_non_opt_evals_to!( - indoc!( - r#" - app "test" provides [main] to "./platform" - - ConsList a : [Cons a (ConsList a), Nil] - - main : ConsList I64 - main = Cons 0x1 Nil - "# - ), - 0, - i64, - |_| 0 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn recursive_function_with_rigid() { - assert_non_opt_evals_to!( - indoc!( - r#" - app "test" provides [main] to "./platform" - - State a : { count : I64, x : a } - - foo : State a -> Int * - foo = \state -> - if state.count == 0 then - 0 - else - 1 + foo { count: state.count - 1, x: state.x } - - main : Int * - main = - foo { count: 3, x: {} } - "# - ), - 3, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn rbtree_insert() { - assert_non_opt_evals_to!( - indoc!( - r#" - app "test" provides [main] to "./platform" - - NodeColor : [Red, Black] - - RedBlackTree k v : [Node NodeColor k v (RedBlackTree k v) (RedBlackTree k v), Empty] - - Key k : Num k - - insert : Key k, v, RedBlackTree (Key k) v -> RedBlackTree (Key k) v - insert = \key, value, dict -> - when insertHelp key value dict is - Node Red k v l r -> - Node Black k v l r - - x -> - x - - insertHelp : (Key k), v, RedBlackTree (Key k) v -> RedBlackTree (Key k) v - insertHelp = \key, value, dict -> - when dict is - Empty -> - # New nodes are always red. If it violates the rules, it will be fixed - # when balancing. - Node Red key value Empty Empty - - Node nColor nKey nValue nLeft nRight -> - when Num.compare key nKey is - LT -> - balance nColor nKey nValue (insertHelp key value nLeft) nRight - - EQ -> - Node nColor nKey value nLeft nRight - - GT -> - balance nColor nKey nValue nLeft (insertHelp key value nRight) - - balance : NodeColor, k, v, RedBlackTree k v, RedBlackTree k v -> RedBlackTree k v - balance = \color, key, value, left, right -> - when right is - Node Red rK rV rLeft rRight -> - when left is - Node Red lK lV lLeft lRight -> - Node - Red - key - value - (Node Black lK lV lLeft lRight) - (Node Black rK rV rLeft rRight) - - _ -> - Node color rK rV (Node Red key value left rLeft) rRight - - _ -> - when left is - Node Red lK lV (Node Red llK llV llLeft llRight) lRight -> - Node - Red - lK - lV - (Node Black llK llV llLeft llRight) - (Node Black key value lRight right) - - _ -> - Node color key value left right - - show : RedBlackTree I64 {} -> Str - show = \tree -> - when tree is - Empty -> "Empty" - Node _ _ _ _ _ -> "Node" - - - main : Str - main = - show (insert 0 {} Empty) - "# - ), - RocStr::from("Node"), - RocStr - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn rbtree_balance_3() { - assert_non_opt_evals_to!( - indoc!( - r#" - app "test" provides [main] to "./platform" - - RedBlackTree k : [Node k (RedBlackTree k) (RedBlackTree k), Empty] - - balance : k, RedBlackTree k -> RedBlackTree k - balance = \key, left -> - Node key left Empty - - main : RedBlackTree (Int *) - main = - balance 0 Empty - "# - ), - false, - *const i64, - |x: *const i64| x.is_null() - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -#[ignore] -fn rbtree_layout_issue() { - // there is a flex var in here somewhere that blows up layout creation - assert_non_opt_evals_to!( - indoc!( - r#" - app "test" provides [main] to "./platform" - - NodeColor : [Red, Black] - - RedBlackTree k v : [Node NodeColor k v (RedBlackTree k v) (RedBlackTree k v), Empty] - - # balance : NodeColor, k, v, RedBlackTree k v -> RedBlackTree k v - balance = \color, key, value, right -> - when right is - Node Red _ _ rLeft rRight -> - Node color key value rLeft rRight - - - _ -> - Empty - - show : RedBlackTree * * -> Str - show = \tree -> - when tree is - Empty -> "Empty" - Node _ _ _ _ _ -> "Node" - - zero : I64 - zero = 0 - - main : Str - main = show (balance Red zero zero Empty) - "# - ), - RocStr::from("Empty"), - RocStr - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -#[ignore] -fn rbtree_balance_mono_problem() { - // because of how the function is written, only `Red` is used and so in the function's - // type, the first argument is a unit and dropped. Apparently something is weird with - // constraint generation where the specialization required by `main` does not fix the - // problem. As a result, the first argument is dropped and we run into issues down the line - // - // concretely, the `rRight` symbol will not be defined - assert_non_opt_evals_to!( - indoc!( - r#" - app "test" provides [main] to "./platform" - - NodeColor : [Red, Black] - - RedBlackTree k v : [Node NodeColor k v (RedBlackTree k v) (RedBlackTree k v), Empty] - - # balance : NodeColor, k, v, RedBlackTree k v, RedBlackTree k v -> RedBlackTree k v - balance = \color, key, value, left, right -> - when right is - Node Red rK rV rLeft rRight -> - when left is - Node Red lK lV lLeft lRight -> - Node - Red - key - value - (Node Black lK lV lLeft lRight) - (Node Black rK rV rLeft rRight) - - _ -> - Node color rK rV (Node Red key value left rLeft) rRight - - _ -> - Empty - - show : RedBlackTree * * -> Str - show = \tree -> - when tree is - Empty -> "Empty" - Node _ _ _ _ _ -> "Node" - - - main : Str - main = show (balance Red 0 0 Empty Empty) - "# - ), - RocStr::from("Empty"), - RocStr - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn rbtree_balance_full() { - assert_non_opt_evals_to!( - indoc!( - r#" - app "test" provides [main] to "./platform" - - NodeColor : [Red, Black] - - RedBlackTree k v : [Node NodeColor k v (RedBlackTree k v) (RedBlackTree k v), Empty] - - balance : NodeColor, k, v, RedBlackTree k v, RedBlackTree k v -> RedBlackTree k v - balance = \color, key, value, left, right -> - when right is - Node Red rK rV rLeft rRight -> - when left is - Node Red lK lV lLeft lRight -> - Node - Red - key - value - (Node Black lK lV lLeft lRight) - (Node Black rK rV rLeft rRight) - - _ -> - Node color rK rV (Node Red key value left rLeft) rRight - - _ -> - when left is - Node Red lK lV (Node Red llK llV llLeft llRight) lRight -> - Node - Red - lK - lV - (Node Black llK llV llLeft llRight) - (Node Black key value lRight right) - - _ -> - Node color key value left right - - main : RedBlackTree F64 F64 - main = - balance Red 0 0 Empty Empty - "# - ), - true, - usize, - |x| x != 0 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn nested_pattern_match_two_ways() { - // exposed an issue in the ordering of pattern match checks when ran with `--release` mode - assert_non_opt_evals_to!( - indoc!( - r#" - app "test" provides [main] to "./platform" - - ConsList a : [Cons a (ConsList a), Nil] - - balance : ConsList (Int *) -> Int * - balance = \right -> - when right is - Cons 1 foo -> - when foo is - Cons 1 _ -> 3 - _ -> 3 - _ -> 3 - - main : Int * - main = - when balance Nil is - _ -> 3 - "# - ), - 3, - i64 - ); - - assert_non_opt_evals_to!( - indoc!( - r#" - app "test" provides [main] to "./platform" - - ConsList a : [Cons a (ConsList a), Nil] - - balance : ConsList (Int *) -> Int * - balance = \right -> - when right is - Cons 1 (Cons 1 _) -> 3 - _ -> 3 - - main : Int * - main = - when balance Nil is - _ -> 3 - "# - ), - 3, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn linked_list_guarded_double_pattern_match() { - // the important part here is that the first case (with the nested Cons) does not match - // TODO this also has undefined behavior - assert_non_opt_evals_to!( - indoc!( - r#" - app "test" provides [main] to "./platform" - - ConsList a : [Cons a (ConsList a), Nil] - - balance : ConsList (Int *) -> Int * - balance = \right -> - when right is - Cons 1 foo -> - when foo is - Cons 1 _ -> 3 - _ -> 3 - _ -> 3 - - main : Int * - main = - when balance Nil is - _ -> 3 - "# - ), - 3, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn linked_list_double_pattern_match() { - assert_non_opt_evals_to!( - indoc!( - r#" - app "test" provides [main] to "./platform" - - ConsList a : [Cons a (ConsList a), Nil] - - foo : ConsList (Int a) -> Int a - foo = \list -> - when list is - Cons _ (Cons x _) -> x - _ -> 0 - - main : Int * - main = - foo (Cons 1 (Cons 32 Nil)) - "# - ), - 32, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn binary_tree_double_pattern_match() { - assert_non_opt_evals_to!( - indoc!( - r#" - app "test" provides [main] to "./platform" - - BTree : [Node BTree BTree, Leaf I64] - - foo : BTree -> I64 - foo = \btree -> - when btree is - Node (Node (Leaf x) _) _ -> x - _ -> 1 - - main : I64 - main = - foo (Node (Node (Leaf 32) (Leaf 2)) (Leaf 3)) - "# - ), - 32, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn unified_empty_closure_bool() { - // none of the Closure tags will have a payload - // this was not handled correctly in the past - assert_non_opt_evals_to!( - indoc!( - r#" - app "test" provides [main] to "./platform" - - foo = \{} -> - when A is - A -> (\_ -> 1.23) - B -> (\_ -> 1.23) - - main : Frac * - main = - (foo {}) 0 - "# - ), - 1.23, - f64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn unified_empty_closure_byte() { - // none of the Closure tags will have a payload - // this was not handled correctly in the past - assert_non_opt_evals_to!( - indoc!( - r#" - app "test" provides [main] to "./platform" - - foo = \{} -> - when A is - A -> (\_ -> 1.23) - B -> (\_ -> 1.23) - C -> (\_ -> 1.23) - - main : Frac * - main = - (foo {}) 0 - "# - ), - 1.23, - f64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn task_always_twice() { - assert_non_opt_evals_to!( - indoc!( - r#" - app "test" provides [main] to "./platform" - - Effect a := {} -> a - - effectAlways : a -> Effect a - effectAlways = \x -> - inner = \{} -> x - - @Effect inner - - effectAfter : Effect a, (a -> Effect b) -> Effect b - effectAfter = \(@Effect thunk), transform -> transform (thunk {}) - - Task a err : Effect (Result a err) - - always : a -> Task a * - always = \x -> effectAlways (Ok x) - - fail : err -> Task * err - fail = \x -> effectAlways (Err x) - - after : Task a err, (a -> Task b err) -> Task b err - after = \task, transform -> - effectAfter task \res -> - when res is - Ok x -> transform x - Err e -> fail e - - main : Task {} (Frac *) - main = after (always "foo") (\_ -> always {}) - - "# - ), - 0, - i64, - |_| 0 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn wildcard_rigid() { - assert_non_opt_evals_to!( - indoc!( - r#" - app "test" provides [main] to "./platform" - - Effect a := {} -> a - - Task a err : Effect (Result a err) - - # this failed because of the `*`, but worked with `err` - always : a -> Task a * - always = \x -> - inner = \{} -> (Ok x) - - @Effect inner - - - main : Task {} (Frac *) - main = always {} - "# - ), - 0, - i64, - |_| 0 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn alias_of_alias_with_type_arguments() { - assert_non_opt_evals_to!( - indoc!( - r#" - app "test" provides [main] to "./platform" - - Effect a := a - - Task a err : Effect (Result a err) - - always : a -> Task a * - always = \x -> - inner = (Ok x) - - @Effect inner - - - main : Task {} (Frac *) - main = always {} - "# - ), - 0, - i64, - |_| 0 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -#[ignore] -fn todo_bad_error_message() { - assert_non_opt_evals_to!( - indoc!( - r#" - app "test" provides [main] to "./platform" - - Effect a := {} -> a - - effectAlways : a -> Effect a - effectAlways = \x -> - inner = \{} -> x - - @Effect inner - - effectAfter : Effect a, (a -> Effect b) -> Effect b - effectAfter = \(@Effect thunk), transform -> transform (thunk {}) - - Task a err : Effect (Result a err) - - always : a -> Task a (Frac *) - always = \x -> effectAlways (Ok x) - - # the problem is that this restricts to `Task {} *` - fail : err -> Task {} err - fail = \x -> effectAlways (Err x) - - after : Task a err, (a -> Task b err) -> Task b err - after = \task, transform -> - effectAfter task \res -> - when res is - Ok x -> transform x - # but here it must be `forall b. Task b {}` - Err e -> fail e - - main : Task {} (Frac *) - main = - after (always "foo") (\_ -> always {}) - "# - ), - 0, - i64, - |_| 0 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn hof_conditional() { - // exposed issue with the if condition being just a symbol - assert_evals_to!( - indoc!( - r#" - passTrue = \f -> f True - - passTrue (\trueVal -> if trueVal then False else True) - "# - ), - 0, - u8 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -#[should_panic( - expected = "Roc failed with message: \"Shadowing { original_region: @55-56, shadow: @88-89 Ident" -)] -fn pattern_shadowing() { - assert_evals_to!( - indoc!( - r#" - x = 4 - - when 4 is - x -> x - "# - ), - 0, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -#[ignore] -#[should_panic(expected = "")] -fn unsupported_pattern_str_interp() { - assert_evals_to!( - indoc!( - r#" - { x: 4 } = { x : 4 } - - x - "# - ), - 0, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -#[ignore] -fn fingertree_basic() { - assert_non_opt_evals_to!( - indoc!( - r#" - app "test" provides [main] to "./platform" - - Some a : [One a, Two a a, Three a a a] - - Tuple a : [Pair a a, Triple a a a] - - # a FingerTree implementation - Seq a : [Nil, Unit a, More (Some a) (Seq (Tuple a)) (Some a)] - - # cons : a, Seq a -> Seq a - cons = \x, s -> - when s is - Nil -> Unit x - Unit y -> More (One x) Nil (One y) - More some q u -> - when some is - One y -> More (Two x y) q u - Two y z -> More (Three x y z) q u - Three y z w -> More (Two x y) (consTuple (Pair z w) q) u - - consTuple : Tuple a, Seq (Tuple a) -> Seq (Tuple a) - consTuple = \a, b -> cons a b - - main : Bool - main = - when cons 0x1 Nil is - Unit 1 -> True - _ -> False - "# - ), - true, - bool - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn case_or_pattern() { - // the `0` branch body should only be generated once in the future - // it is currently duplicated - assert_evals_to!( - indoc!( - r#" - x : [Red, Green, Blue] - x = Red - - when x is - Red | Green -> 0 - Blue -> 1 - "# - ), - 0, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -#[ignore] -fn rosetree_basic() { - assert_non_opt_evals_to!( - indoc!( - r#" - app "test" provides [main] to "./platform" - - Tree a : [Tree a (List (Tree a))] - - singleton : a -> Tree a - singleton = \x -> Tree x [] - - main : Bool - main = - x : Tree F64 - x = singleton 3 - when x is - Tree 3.0 _ -> True - _ -> False - "# - ), - true, - bool - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn case_jump() { - // the decision tree will generate a jump to the `1` branch here - assert_evals_to!( - indoc!( - r#" - app "test" provides [main] to "./platform" - - ConsList a : [Cons a (ConsList a), Nil] - - x : ConsList I64 - x = Nil - - main = - when Pair x x is - Pair Nil _ -> 1 - Pair _ Nil -> 2 - Pair (Cons a _) (Cons b _) -> a + b + 3 - "# - ), - 1, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn nullable_eval_cfold() { - // the decision tree will generate a jump to the `1` branch here - assert_evals_to!( - indoc!( - r#" - app "test" provides [main] to "./platform" - - Expr : [Var, Val I64, Add Expr Expr, Mul Expr Expr] - - mkExpr : I64, I64 -> Expr - mkExpr = \n , v -> - when n is - 0 -> if v == 0 then Var else Val v - _ -> Add (mkExpr (n-1) (v+1)) (mkExpr (n-1) (max (v-1) 0)) - - max : I64, I64 -> I64 - max = \a, b -> if a > b then a else b - - eval : Expr -> I64 - eval = \e -> - when e is - Var -> 0 - Val v -> v - Add l r -> eval l + eval r - Mul l r -> eval l * eval r - - main : I64 - main = eval (mkExpr 3 1) - "# - ), - 11, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn nested_switch() { - // exposed bug with passing the right symbol/layout down into switch branch generation - assert_evals_to!( - indoc!( - r#" - app "test" provides [main] to "./platform" - - Expr : [ZAdd Expr Expr, Val I64, Var I64] - - eval : Expr -> I64 - eval = \e -> - when e is - Var _ -> 0 - Val v -> v - ZAdd l r -> eval l + eval r - - constFolding : Expr -> Expr - constFolding = \e -> - when e is - ZAdd e1 e2 -> - when Pair e1 e2 is - Pair (Val a) (Val b) -> Val (a+b) - Pair (Val a) (ZAdd x (Val b)) -> ZAdd (Val (a+b)) x - Pair _ _ -> ZAdd e1 e2 - - - _ -> e - - - expr : Expr - expr = ZAdd (Val 3) (ZAdd (Val 4) (Val 5)) - - main : I64 - main = eval (constFolding expr) - "# - ), - 12, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn count_deriv_x() { - // exposed bug with basing the block_of_memory on a specific (smaller) tag layout - assert_evals_to!( - indoc!( - r#" - app "test" provides [main] to "./platform" - - Expr : [Ln Expr, Pow Expr Expr, Var Str] - - count : Expr -> I64 - count = \expr -> - when expr is - (Var _) -> 1 - (Pow f g) -> count f + count g - (Ln f) -> count f - - main : I64 - main = count (Var "x") - "# - ), - 1, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn deriv_pow() { - // exposed bug with ordering of variable declarations before switch - assert_evals_to!( - indoc!( - r#" - app "test" provides [main] to "./platform" - - Expr : [Ln Expr, Pow Expr Expr, Var Str, Val I64] - - count : Expr -> I64 - count = \expr -> - when expr is - (Var _) -> 1 - (Val n) -> n - (Pow f g) -> count f + count g - (Ln f) -> count f - - pow : Expr, Expr -> Expr - pow = \a,b -> - when Pair a b is - Pair (Val _) (Val _) -> Val -1 - Pair _ (Val 0) -> Val 1 - Pair f (Val 1) -> f - Pair (Val 0) _ -> Val 0 - Pair f g -> Pow f g - - main : I64 - main = count (pow (Var "x") (Var "x")) - "# - ), - 2, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn multiple_increment() { - // the `leaf` value will be incremented multiple times at once - assert_evals_to!( - indoc!( - r#" - app "test" provides [main] to "./platform" - - - Color : [Red, Black] - - Tree a b : [Leaf, Node Color (Tree a b) a b (Tree a b)] - - Map : Tree I64 Bool - - main : I64 - main = - leaf : Map - leaf = Leaf - - m : Map - m = Node Black (Node Black leaf 10 False leaf) 11 False (Node Black leaf 12 False (Node Red leaf 13 False leaf)) - - when m is - Leaf -> 0 - Node _ _ _ _ _ -> 1 - "# - ), - 1, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn switch_fuse_rc_non_exhaustive() { - assert_evals_to!( - indoc!( - r#" - app "test" provides [main] to "./platform" - - Foo : [A I64 Foo, B I64 Foo, C I64 Foo, Empty] - - sum : Foo, I64 -> I64 - sum = \foo, accum -> - when foo is - A x resta -> sum resta (x + accum) - B x restb -> sum restb (x + accum) - # Empty -> accum - # C x restc -> sum restc (x + accum) - _ -> accum - - main : I64 - main = - A 1 (B 2 (C 3 Empty)) - |> sum 0 - "# - ), - 3, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn switch_fuse_rc_exhaustive() { - assert_evals_to!( - indoc!( - r#" - app "test" provides [main] to "./platform" - - Foo : [A I64 Foo, B I64 Foo, C I64 Foo, Empty] - - sum : Foo, I64 -> I64 - sum = \foo, accum -> - when foo is - A x resta -> sum resta (x + accum) - B x restb -> sum restb (x + accum) - C x restc -> sum restc (x + accum) - Empty -> accum - - main : I64 - main = - A 1 (B 2 (C 3 Empty)) - |> sum 0 - "# - ), - 6, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn build_then_apply_closure() { - assert_evals_to!( - indoc!( - r#" - app "test" provides [main] to "./platform" - - main : Str - main = - x = "long string that is malloced" - - (\_ -> x) {} - "# - ), - RocStr::from("long string that is malloced"), - RocStr - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn expanded_result() { - assert_evals_to!( - indoc!( - r#" - app "test" provides [main] to "./platform" - - a : Result I64 Str - a = Ok 4 - - after = \x, f -> - when x is - Ok v -> f v - Err e -> Err e - - main : I64 - main = - helper = after a (\x -> Ok x) - - when helper is - Ok v -> v - Err _ -> 0 - - "# - ), - 4, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -#[ignore] -fn backpassing_result() { - assert_evals_to!( - indoc!( - r#" - app "test" provides [main] to "./platform" - - a : Result I64 Str - a = Ok 1 - - f = \x -> Ok (x + 1) - g = \y -> Ok (y * 2) - - main : I64 - main = - helper = - x <- Result.after a - y <- Result.after (f x) - z <- Result.after (g y) - - Ok z - - helper - |> Result.withDefault 0 - - "# - ), - 4, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -#[should_panic(expected = "Shadowing { original_region: @55-56, shadow: @72-73 Ident")] -fn function_malformed_pattern() { - assert_evals_to!( - indoc!( - r#" - x = 3 - - (\x -> x) 42 - "# - ), - 3, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -#[should_panic(expected = "Hit an erroneous type when creating a layout for")] -fn call_invalid_layout() { - assert_evals_to!( - indoc!( - r#" - f : I64 -> I64 - f = \x -> x - - f {} - "# - ), - 3, - i64, - |x| x, - true - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn increment_or_double_closure() { - assert_evals_to!( - indoc!( - r#" - app "test" provides [main] to "./platform" - - - apply : (a -> a), a -> a - apply = \f, x -> f x - - main = - one : I64 - one = 1 - - two : I64 - two = 2 - - b : Bool - b = True - - increment : I64 -> I64 - increment = \x -> x + one - - double : I64 -> I64 - double = \x -> if b then x * two else x - - f = (if True then increment else double) - - apply f 42 - "# - ), - 43, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn module_thunk_is_function() { - assert_evals_to!( - indoc!( - r#" - app "test" provides [main] to "./platform" - - main = helper "foo" "bar" - helper = Str.concat - "# - ), - RocStr::from("foobar"), - RocStr - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -#[should_panic(expected = "Roc failed with message: ")] -fn hit_unresolved_type_variable() { - assert_evals_to!( - indoc!( - r#" - app "test" provides [main] to "./platform" - - main : Str - main = - (accept Bool.isEq) "B" - - - accept : * -> (b -> b) - accept = \_ -> - \input -> input - "# - ), - RocStr::from("B"), - RocStr - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))] -fn pattern_match_empty_record() { - assert_evals_to!( - indoc!( - r#" - app "test" provides [main] to "./platform" - - main : I64 - main = - when {} is - {} -> 0 - - "# - ), - 0, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn pattern_match_unit_tag() { - assert_evals_to!( - indoc!( - r#" - app "test" provides [main] to "./platform" - - unit : [Unit] - unit = Unit - - main : I64 - main = - when unit is - Unit -> 0 - - "# - ), - 0, - i64 - ); -} - -// see for why this is disabled on wasm32 https://github.com/rtfeldman/roc/issues/1687 -#[cfg(not(feature = "wasm-cli-run"))] -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn mirror_llvm_alignment_padding() { - // see https://github.com/rtfeldman/roc/issues/1569 - assert_evals_to!( - indoc!( - r#" - app "test" provides [main] to "./platform" - - main : Str - main = - p1 = {name : "test1", test: 1 == 1 } - - List.map [p1, p1] (\{ test } -> if test then "pass" else "fail") - |> Str.joinWith "\n" - - "# - ), - RocStr::from("pass\npass"), - RocStr - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn lambda_set_bool() { - assert_evals_to!( - indoc!( - r#" - app "test" provides [main] to "./platform" - - p1 = (\u -> u == 97) - p2 = (\u -> u == 98) - - main : I64 - main = - oneOfResult = List.map [p1, p2] (\p -> p 42) - - when oneOfResult is - _ -> 32 - - "# - ), - 32, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn lambda_set_byte() { - assert_evals_to!( - indoc!( - r#" - app "test" provides [main] to "./platform" - - p1 = (\u -> u == 97) - p2 = (\u -> u == 98) - p3 = (\u -> u == 99) - - main : I64 - main = - oneOfResult = List.map [p1, p2, p3] (\p -> p 42) - - when oneOfResult is - _ -> 32 - - "# - ), - 32, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn lambda_set_struct_byte() { - assert_evals_to!( - indoc!( - r#" - app "test" provides [main] to "./platform" - - - main : I64 - main = - r : [Red, Green, Blue] - r = Red - - p1 = (\u -> r == u) - foobarbaz = (\p -> p Green) - oneOfResult = List.map [p1, p1] foobarbaz - - when oneOfResult is - _ -> 32 - - "# - ), - 32, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn lambda_set_enum_byte_byte() { - assert_evals_to!( - indoc!( - r#" - app "test" provides [main] to "./platform" - - - main : I64 - main = - r : [Red, Green, Blue] - r = Red - - g : [Red, Green, Blue] - g = Green - - p1 = (\u -> r == u) - p2 = (\u -> g == u) - oneOfResult = List.map [p1, p2] (\p -> p Green) - - when oneOfResult is - _ -> 32 - - "# - ), - 32, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn list_walk_until() { - // see https://github.com/rtfeldman/roc/issues/1576 - assert_evals_to!( - indoc!( - r#" - app "test" provides [main] to "./platform" - - - satisfyA : {} -> List {} - satisfyA = \_ -> [] - - oneOfResult = - List.walkUntil [satisfyA] [] \_, _ -> Stop [] - - main = - when oneOfResult is - _ -> 32 - "# - ), - 32, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn int_literal_not_specialized_with_annotation() { - // see https://github.com/rtfeldman/roc/issues/1600 - assert_evals_to!( - indoc!( - r#" - app "test" provides [main] to "./platform" - - main = - satisfy : (U8 -> Str) -> Str - satisfy = \_ -> "foo" - - myEq : a, a -> Str - myEq = \_, _ -> "bar" - - p1 : Num * -> Str - p1 = (\u -> myEq u 64) - - when satisfy p1 is - _ -> 32 - "# - ), - 32, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn int_literal_not_specialized_no_annotation() { - // see https://github.com/rtfeldman/roc/issues/1600 - assert_evals_to!( - indoc!( - r#" - app "test" provides [main] to "./platform" - - main = - satisfy : (U8 -> Str) -> Str - satisfy = \_ -> "foo" - - myEq : a, a -> Str - myEq = \_, _ -> "bar" - - p1 = (\u -> myEq u 64) - - when satisfy p1 is - _ -> 32 - "# - ), - 32, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn unresolved_tvar_when_capture_is_unused() { - // see https://github.com/rtfeldman/roc/issues/1585 - assert_evals_to!( - indoc!( - r#" - app "test" provides [main] to "./platform" - - main : I64 - main = - r : Bool - r = False - - p1 = (\_ -> r == (1 == 1)) - oneOfResult = List.map [p1] (\p -> p Green) - - when oneOfResult is - _ -> 32 - - "# - ), - 32, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -#[should_panic(expected = "Roc failed with message: ")] -fn value_not_exposed_hits_panic() { - assert_evals_to!( - indoc!( - r#" - app "test" provides [main] to "./platform" - - main : I64 - main = - Str.toInt 32 - "# - ), - 32, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn mix_function_and_closure() { - // see https://github.com/rtfeldman/roc/pull/1706 - assert_evals_to!( - indoc!( - r#" - app "test" provides [main] to "./platform" - - # foo does not capture any variables - # but through unification will get a lambda set that does store information - # we must handle that correctly - foo = \x -> x - - bar = \y -> \_ -> y - - main : Str - main = - (if 1 == 1 then foo else (bar "nope nope nope")) "hello world" - "# - ), - RocStr::from("hello world"), - RocStr - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn mix_function_and_closure_level_of_indirection() { - // see https://github.com/rtfeldman/roc/pull/1706 - assert_evals_to!( - indoc!( - r#" - app "test" provides [main] to "./platform" - - foo = \x -> x - - bar = \y -> \_ -> y - - f = (if 1 == 1 then foo else (bar "nope nope nope")) - - main : Str - main = - f "hello world" - "# - ), - RocStr::from("hello world"), - RocStr - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -#[cfg_attr(debug_assertions, ignore)] // this test stack-overflows the compiler in debug mode -fn do_pass_bool_byte_closure_layout() { - // see https://github.com/rtfeldman/roc/pull/1706 - // the distinction is actually important, dropping that info means some functions just get - // skipped - assert_evals_to!( - indoc!( - r#" - app "test" provides [main] to "./platform" - - ## PARSER - - Parser a : List U8 -> List [Pair a (List U8)] - - - ## ANY - - # If successful, the any parser consumes one character - - any: Parser U8 - any = \inp -> - when List.first inp is - Ok u -> [Pair u (List.drop inp 1)] - _ -> [] - - - - ## SATISFY - - satisfy : (U8 -> Bool) -> Parser U8 - satisfy = \predicate -> - \input -> - walker = \accum, (Pair u rest) -> - if predicate u then - Stop [Pair u rest] - - else - Stop accum - - List.walkUntil (any input) [] walker - - - - oneOf : List (Parser a) -> Parser a - oneOf = \parserList -> - \input -> - walker = \accum, p -> - output = p input - if List.len output == 1 then - Stop output - - else - Continue accum - - List.walkUntil parserList [] walker - - - satisfyA = satisfy (\u -> u == 97) # recognize 97 - satisfyB = satisfy (\u -> u == 98) # recognize 98 - - test1 = if List.len ((oneOf [satisfyA, satisfyB]) [97, 98, 99, 100] ) == 1 then "PASS" else "FAIL" - test2 = if List.len ((oneOf [satisfyA, satisfyB]) [98, 99, 100, 97] ) == 1 then "PASS" else "FAIL" - test3 = if List.len ((oneOf [satisfyB , satisfyA]) [98, 99, 100, 97] ) == 1 then "PASS" else "FAIL" - test4 = if List.len ((oneOf [satisfyA, satisfyB]) [99, 100, 101] ) == 0 then "PASS" else "FAIL" - - - main : Str - main = [test1, test2, test3, test4] |> Str.joinWith ", " - "# - ), - RocStr::from("PASS, PASS, PASS, PASS"), - RocStr - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn nested_rigid_list() { - assert_evals_to!( - indoc!( - r#" - app "test" provides [main] to "./platform" - - foo : List a -> List a - foo = \list -> - p2 : List a - p2 = list - - p2 - - main = - when foo [] is - _ -> "hello world" - "# - ), - RocStr::from("hello world"), - RocStr - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-dev"))] -fn nested_rigid_alias() { - assert_evals_to!( - indoc!( - r#" - app "test" provides [main] to "./platform" - - Identity a := a - - foo : Identity a -> Identity a - foo = \list -> - p2 : Identity a - p2 = list - - p2 - - main = - when foo (@Identity "foo") is - _ -> "hello world" - "# - ), - RocStr::from("hello world"), - RocStr - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-dev"))] -fn nested_rigid_tag_union() { - assert_evals_to!( - indoc!( - r#" - app "test" provides [main] to "./platform" - - foo : [Identity a] -> [Identity a] - foo = \list -> - p2 : [Identity a] - p2 = list - - p2 - - main = - when foo (Identity "foo") is - _ -> "hello world" - "# - ), - RocStr::from("hello world"), - RocStr - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn call_that_needs_closure_parameter() { - // here both p2 is lifted to the top-level, which means that `list` must be - // passed to it from `manyAux`. - assert_evals_to!( - indoc!( - r#" - Step state a : [Loop state, Done a] - - manyAux : List a -> [Pair (Step (List a) (List a))] - manyAux = \list -> - p2 = \_ -> Pair (Done list) - - p2 "foo" - - manyAuxTest = (manyAux []) == Pair (Loop [97]) - - runTest = \t -> if t then "PASS" else "FAIL" - - runTest manyAuxTest - "# - ), - RocStr::from("FAIL"), - RocStr - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn alias_defined_out_of_order() { - assert_evals_to!( - indoc!( - r#" - app "test" provides [main] to "./platform" - - main : Foo - main = "foo" - - Foo : Bar - Bar : Str - - "# - ), - RocStr::from("foo"), - RocStr - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn recursively_build_effect() { - assert_evals_to!( - indoc!( - r#" - app "test" provides [main] to "./platform" - - greeting = - hi = "Hello" - name = "World" - - "\(hi), \(name)!" - - main = - when nestHelp 4 is - _ -> greeting - - nestHelp : I64 -> XEffect {} - nestHelp = \m -> - when m is - 0 -> - always {} - - _ -> - always {} |> after \_ -> nestHelp (m - 1) - - - XEffect a := {} -> a - - always : a -> XEffect a - always = \x -> @XEffect (\{} -> x) - - after : XEffect a, (a -> XEffect b) -> XEffect b - after = \(@XEffect e), toB -> - @XEffect \{} -> - when toB (e {}) is - @XEffect e2 -> - e2 {} - "# - ), - RocStr::from("Hello, World!"), - RocStr - ); -} - -#[test] -#[ignore = "TODO; currently generates bad code because `a` isn't specialized inside the closure."] -#[cfg(any(feature = "gen-llvm"))] -fn polymophic_expression_captured_inside_closure() { - assert_evals_to!( - indoc!( - r#" - app "test" provides [main] to "./platform" - - asU8 : U8 -> U8 - asU8 = \_ -> 30 - - main = - a = 15 - f = \{} -> - asU8 a - - f {} - "# - ), - 30, - u8 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn issue_2322() { - assert_evals_to!( - indoc!( - r#" - double = \x -> x * 2 - doubleBind = \x -> (\_ -> double x) - doubleThree = doubleBind 3 - doubleThree {} - "# - ), - 6, - i64 - ) -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn box_and_unbox_string() { - assert_evals_to!( - indoc!( - r#" - Str.concat "Leverage " "agile frameworks to provide a robust synopsis for high level overviews" - |> Box.box - |> Box.unbox - "# - ), - RocStr::from( - "Leverage agile frameworks to provide a robust synopsis for high level overviews" - ), - RocStr - ) -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn box_and_unbox_num() { - assert_evals_to!( - indoc!( - r#" - Box.unbox (Box.box (123u8)) - "# - ), - 123, - u8 - ) -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn box_and_unbox_record() { - assert_evals_to!( - indoc!( - r#" - Box.unbox (Box.box { a: 15u8, b: 27u8 }) - "# - ), - (15, 27), - (u8, u8) - ) -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn box_and_unbox_tag_union() { - assert_evals_to!( - indoc!( - r#" - v : [A U8, B U8] # usually stack allocated - v = B 27u8 - - Box.unbox (Box.box v) - "# - ), - (27, 1), - (u8, u8) - ) -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn closure_called_in_its_defining_scope() { - assert_evals_to!( - indoc!( - r#" - app "test" provides [main] to "./platform" - - main : Str - main = - g : Str - g = "hello world" - - getG : {} -> Str - getG = \{} -> g - - getG {} - "# - ), - RocStr::from("hello world"), - RocStr - ) -} - -#[test] -#[ignore] -#[cfg(any(feature = "gen-llvm"))] -fn issue_2894() { - assert_evals_to!( - indoc!( - r#" - app "test" provides [main] to "./platform" - - main : U32 - main = - g : { x : U32 } - g = { x: 1u32 } - - getG : {} -> { x : U32 } - getG = \{} -> g - - h : {} -> U32 - h = \{} -> (getG {}).x - - h {} - "# - ), - 1u32, - u32 - ) -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn polymorphic_def_used_in_closure() { - assert_evals_to!( - indoc!( - r#" - a : I64 -> _ - a = \g -> - f = { r: g, h: 32 } - - h1 : U64 - h1 = (\{} -> f.h) {} - h1 - a 1 - "# - ), - 32, - u64 - ) -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn polymorphic_lambda_set_usage() { - assert_evals_to!( - indoc!( - r#" - id1 = \x -> x - id2 = \y -> y - id = if True then id1 else id2 - - id 9u8 - "# - ), - 9, - u8 - ) -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn polymorphic_lambda_set_multiple_specializations() { - assert_evals_to!( - indoc!( - r#" - id1 = \x -> x - id2 = \y -> y - id = if True then id1 else id2 - - (id 9u8) + Num.toU8 (id 16u16) - "# - ), - 25, - u8 - ) -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn list_map2_conslist() { - // this had an RC problem, https://github.com/rtfeldman/roc/issues/2968 - assert_evals_to!( - indoc!( - r#" - ConsList a : [Nil, Cons a (ConsList a)] - - x : List (ConsList Str) - x = List.map2 [] [Nil] Cons - - when List.first x is - _ -> "" - "# - ), - RocStr::default(), - RocStr - ) -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn polymorphic_lambda_captures_polymorphic_value() { - assert_evals_to!( - indoc!( - r#" - x = 2 - - f1 = \_ -> x - - f = if True then f1 else f1 - f {} - "# - ), - 2, - u64 - ) -} diff --git a/compiler/test_gen/src/gen_refcount.rs b/compiler/test_gen/src/gen_refcount.rs deleted file mode 100644 index e707495919..0000000000 --- a/compiler/test_gen/src/gen_refcount.rs +++ /dev/null @@ -1,433 +0,0 @@ -#[cfg(feature = "gen-wasm")] -use crate::helpers::{wasm::assert_refcounts, RefCount::*}; - -#[allow(unused_imports)] -use indoc::indoc; - -#[allow(unused_imports)] -use roc_std::{RocList, RocStr}; - -// A "good enough" representation of a pointer for these tests, because -// we ignore the return value. As long as it's the right stack size, it's fine. -#[allow(dead_code)] -type Pointer = usize; - -#[test] -#[cfg(any(feature = "gen-wasm"))] -fn str_inc() { - assert_refcounts!( - indoc!( - r#" - s = Str.concat "A long enough string " "to be heap-allocated" - - [s, s, s] - "# - ), - RocList, - &[ - Live(3), // s - Live(1) // result - ] - ); -} - -#[test] -#[cfg(any(feature = "gen-wasm"))] -fn str_dealloc() { - assert_refcounts!( - indoc!( - r#" - s = Str.concat "A long enough string " "to be heap-allocated" - - Str.isEmpty s - "# - ), - bool, - &[Deallocated] - ); -} - -#[test] -#[cfg(any(feature = "gen-wasm"))] -fn list_int_inc() { - assert_refcounts!( - indoc!( - r#" - list = [0x111, 0x222, 0x333] - [list, list, list] - "# - ), - RocList>, - &[ - Live(3), // list - Live(1) // result - ] - ); -} - -#[test] -#[cfg(any(feature = "gen-wasm"))] -fn list_int_dealloc() { - assert_refcounts!( - indoc!( - r#" - list = [0x111, 0x222, 0x333] - List.len [list, list, list] - "# - ), - usize, - &[ - Deallocated, // list - Deallocated // result - ] - ); -} - -#[test] -#[cfg(any(feature = "gen-wasm"))] -fn list_str_inc() { - assert_refcounts!( - indoc!( - r#" - s = Str.concat "A long enough string " "to be heap-allocated" - list = [s, s, s] - [list, list] - "# - ), - RocList>, - &[ - Live(6), // s - Live(2), // list - Live(1) // result - ] - ); -} - -#[test] -#[cfg(any(feature = "gen-wasm"))] -fn list_str_dealloc() { - assert_refcounts!( - indoc!( - r#" - s = Str.concat "A long enough string " "to be heap-allocated" - list = [s, s, s] - List.len [list, list] - "# - ), - usize, - &[ - Deallocated, // s - Deallocated, // list - Deallocated // result - ] - ); -} - -#[test] -#[cfg(any(feature = "gen-wasm"))] -fn struct_inc() { - assert_refcounts!( - indoc!( - r#" - s = Str.concat "A long enough string " "to be heap-allocated" - r1 : { a: I64, b: Str, c: Str } - r1 = { a: 123, b: s, c: s } - { y: r1, z: r1 } - "# - ), - [(i64, RocStr, RocStr); 2], - &[Live(4)] // s - ); -} - -#[test] -#[cfg(any(feature = "gen-wasm"))] -fn struct_dealloc() { - assert_refcounts!( - indoc!( - r#" - s = Str.concat "A long enough string " "to be heap-allocated" - r1 : { a: I64, b: Str, c: Str } - r1 = { a: 123, b: s, c: s } - r2 = { x: 456, y: r1, z: r1 } - r2.x - "# - ), - i64, - &[Deallocated] // s - ); -} - -#[test] -#[cfg(any(feature = "gen-wasm"))] -fn union_nonrecursive_inc() { - type TwoStr = (RocStr, RocStr, i64); - - assert_refcounts!( - indoc!( - r#" - TwoOrNone a: [Two a a, None] - - s = Str.concat "A long enough string " "to be heap-allocated" - - two : TwoOrNone Str - two = Two s s - - four : TwoOrNone (TwoOrNone Str) - four = Two two two - - four - "# - ), - (TwoStr, TwoStr, i64), - &[Live(4)] - ); -} - -#[test] -#[cfg(any(feature = "gen-wasm"))] -fn union_nonrecursive_dec() { - assert_refcounts!( - indoc!( - r#" - TwoOrNone a: [Two a a, None] - - s = Str.concat "A long enough string " "to be heap-allocated" - - two : TwoOrNone Str - two = Two s s - - when two is - Two x _ -> x - None -> "" - "# - ), - RocStr, - &[Live(1)] // s - ); -} - -#[test] -#[cfg(any(feature = "gen-wasm"))] -fn union_recursive_inc() { - assert_refcounts!( - indoc!( - r#" - Expr : [Sym Str, Add Expr Expr] - - s = Str.concat "heap_allocated" "_symbol_name" - - x : Expr - x = Sym s - - e : Expr - e = Add x x - - Pair e e - "# - ), - (Pointer, Pointer), - &[ - Live(4), // s - Live(4), // sym - Live(2), // e - ] - ); -} - -#[test] -#[cfg(any(feature = "gen-wasm"))] -fn union_recursive_dec() { - assert_refcounts!( - indoc!( - r#" - Expr : [Sym Str, Add Expr Expr] - - s = Str.concat "heap_allocated" "_symbol_name" - - x : Expr - x = Sym s - - e : Expr - e = Add x x - - when e is - Add y _ -> y - Sym _ -> e - "# - ), - Pointer, - &[ - Live(1), // s - Live(1), // sym - Deallocated // e - ] - ); -} - -#[test] -#[cfg(any(feature = "gen-wasm"))] -fn refcount_different_rosetrees_inc() { - // Requires two different Inc procedures for `List (Rose I64)` and `List (Rose Str)` - // even though both appear in the mono Layout as `List(RecursivePointer)` - assert_refcounts!( - indoc!( - r#" - Rose a : [Rose a (List (Rose a))] - - s = Str.concat "A long enough string " "to be heap-allocated" - - i1 : Rose I64 - i1 = Rose 999 [] - - s1 : Rose Str - s1 = Rose s [] - - i2 : Rose I64 - i2 = Rose 0 [i1, i1, i1] - - s2 : Rose Str - s2 = Rose "" [s1, s1] - - Tuple i2 s2 - "# - ), - (Pointer, Pointer), - &[ - Live(2), // s - Live(3), // i1 - Live(2), // s1 - Live(1), // [i1, i1] - Live(1), // i2 - Live(1), // [s1, s1] - Live(1) // s2 - ] - ); -} - -#[test] -#[cfg(any(feature = "gen-wasm"))] -fn refcount_different_rosetrees_dec() { - // Requires two different Dec procedures for `List (Rose I64)` and `List (Rose Str)` - // even though both appear in the mono Layout as `List(RecursivePointer)` - assert_refcounts!( - indoc!( - r#" - Rose a : [Rose a (List (Rose a))] - - s = Str.concat "A long enough string " "to be heap-allocated" - - i1 : Rose I64 - i1 = Rose 999 [] - - s1 : Rose Str - s1 = Rose s [] - - i2 : Rose I64 - i2 = Rose 0 [i1, i1] - - s2 : Rose Str - s2 = Rose "" [s1, s1] - - when (Tuple i2 s2) is - Tuple (Rose x _) _ -> x - "# - ), - i64, - &[ - Deallocated, // s - Deallocated, // i1 - Deallocated, // s1 - Deallocated, // [i1, i1] - Deallocated, // i2 - Deallocated, // [s1, s1] - Deallocated, // s2 - ] - ); -} - -#[test] -#[cfg(any(feature = "gen-wasm"))] -fn union_linked_list_inc() { - assert_refcounts!( - indoc!( - r#" - LinkedList a : [Nil, Cons a (LinkedList a)] - - s = Str.concat "A long enough string " "to be heap-allocated" - - linked : LinkedList Str - linked = Cons s (Cons s (Cons s Nil)) - - Tuple linked linked - "# - ), - (Pointer, Pointer), - &[ - Live(6), // s - Live(2), // Cons - Live(2), // Cons - Live(2), // Cons - ] - ); -} - -#[test] -#[cfg(any(feature = "gen-wasm"))] -fn union_linked_list_dec() { - assert_refcounts!( - indoc!( - r#" - LinkedList a : [Nil, Cons a (LinkedList a)] - - s = Str.concat "A long enough string " "to be heap-allocated" - - linked : LinkedList Str - linked = Cons s (Cons s (Cons s Nil)) - - when linked is - Cons x _ -> x - Nil -> "" - "# - ), - RocStr, - &[ - Live(1), // s - Deallocated, // Cons - Deallocated, // Cons - Deallocated, // Cons - ] - ); -} - -#[test] -#[cfg(any(feature = "gen-wasm"))] -fn union_linked_list_long_dec() { - assert_refcounts!( - indoc!( - r#" - app "test" provides [main] to "./platform" - - LinkedList a : [Nil, Cons a (LinkedList a)] - - prependOnes = \n, tail -> - if n == 0 then - tail - else - prependOnes (n-1) (Cons 1 tail) - - main = - n = 1_000 - - linked : LinkedList I64 - linked = prependOnes n Nil - - when linked is - Cons x _ -> x - Nil -> -1 - "# - ), - i64, - &[Deallocated; 1_000] - ); -} diff --git a/compiler/test_gen/src/gen_result.rs b/compiler/test_gen/src/gen_result.rs deleted file mode 100644 index 296a23bab7..0000000000 --- a/compiler/test_gen/src/gen_result.rs +++ /dev/null @@ -1,270 +0,0 @@ -#![cfg(feature = "gen-llvm")] - -#[cfg(feature = "gen-llvm")] -use crate::helpers::llvm::assert_evals_to; - -// #[cfg(feature = "gen-dev")] -// use crate::helpers::dev::assert_evals_to; - -// #[cfg(feature = "gen-wasm")] -// use crate::helpers::wasm::assert_evals_to; - -use indoc::indoc; - -#[allow(unused_imports)] -use roc_std::{RocResult, RocStr}; - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn with_default() { - assert_evals_to!( - indoc!( - r#" - result : Result I64 {} - result = Ok 2 - - Result.withDefault result 0 - "# - ), - 2, - i64 - ); - - assert_evals_to!( - indoc!( - r#" - result : Result I64 {} - result = Err {} - - Result.withDefault result 0 - "# - ), - 0, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn result_map() { - assert_evals_to!( - indoc!( - r#" - result : Result I64 {} - result = Ok 2 - - result - |> Result.map (\x -> x + 1) - |> Result.withDefault 0 - "# - ), - 3, - i64 - ); - - assert_evals_to!( - indoc!( - r#" - result : Result I64 {} - result = Err {} - - result - |> Result.map (\x -> x + 1) - |> Result.withDefault 0 - "# - ), - 0, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn result_map_err() { - assert_evals_to!( - indoc!( - r#" - result : Result {} I64 - result = Err 2 - - when Result.mapErr result (\x -> x + 1) is - Err n -> n - Ok _ -> 0 - "# - ), - 3, - i64 - ); - - assert_evals_to!( - indoc!( - r#" - result : Result {} I64 - result = Ok {} - - when Result.mapErr result (\x -> x + 1) is - Err n -> n - Ok _ -> 0 - "# - ), - 0, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn err_type_var() { - assert_evals_to!( - indoc!( - r#" - Result.map (Ok 3) (\x -> x + 1) - |> Result.withDefault -1 - "# - ), - 4, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn err_type_var_annotation() { - assert_evals_to!( - indoc!( - r#" - ok : Result I64 * - ok = Ok 3 - - Result.map ok (\x -> x + 1) - |> Result.withDefault -1 - "# - ), - 4, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn err_empty_tag_union() { - assert_evals_to!( - indoc!( - r#" - ok : Result I64 [] - ok = Ok 3 - - Result.map ok (\x -> x + 1) - |> Result.withDefault -1 - "# - ), - 4, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn is_ok() { - assert_evals_to!( - indoc!( - r#" - result : Result I64 {} - result = Ok 2 - - Result.isOk result - "# - ), - true, - bool - ); - - assert_evals_to!( - indoc!( - r#" - result : Result I64 {} - result = Err {} - - Result.isOk result - "# - ), - false, - bool - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn is_err() { - assert_evals_to!( - indoc!( - r#" - result : Result I64 {} - result = Ok 2 - - Result.isErr result - "# - ), - false, - bool - ); - - assert_evals_to!( - indoc!( - r#" - result : Result I64 {} - result = Err {} - - Result.isErr result - "# - ), - true, - bool - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn roc_result_ok() { - assert_evals_to!( - indoc!( - r#" - result : Result I64 {} - result = Ok 42 - - result - "# - ), - RocResult::ok(42), - RocResult - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn roc_result_err() { - assert_evals_to!( - indoc!( - r#" - result : Result I64 Str - result = Err "foo" - - result - "# - ), - RocResult::err(RocStr::from("foo")), - RocResult - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn issue_2583_specialize_errors_behind_unified_branches() { - assert_evals_to!( - r#" - if True then List.first [15] else Str.toI64 "" - "#, - RocResult::ok(15i64), - RocResult - ) -} diff --git a/compiler/test_gen/src/gen_set.rs b/compiler/test_gen/src/gen_set.rs deleted file mode 100644 index e2e091b71c..0000000000 --- a/compiler/test_gen/src/gen_set.rs +++ /dev/null @@ -1,280 +0,0 @@ -#![cfg(feature = "gen-llvm")] - -#[cfg(feature = "gen-llvm")] -use crate::helpers::llvm::assert_evals_to; - -// #[cfg(feature = "gen-dev")] -// use crate::helpers::dev::assert_evals_to; - -// #[cfg(feature = "gen-wasm")] -// use crate::helpers::wasm::assert_evals_to; - -use indoc::indoc; -use roc_std::RocList; - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn empty_len() { - assert_evals_to!( - indoc!( - r#" - Set.len Set.empty - "# - ), - 0, - usize - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn single_len() { - assert_evals_to!( - indoc!( - r#" - Set.len (Set.single 42) - "# - ), - 1, - usize - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn single_to_list() { - assert_evals_to!( - indoc!( - r#" - Set.toList (Set.single 42) - "# - ), - RocList::from_slice(&[42]), - RocList - ); - - assert_evals_to!( - indoc!( - r#" - Set.toList (Set.single 1) - "# - ), - RocList::from_slice(&[1]), - RocList - ); - - assert_evals_to!( - indoc!( - r#" - Set.toList (Set.single 1.0) - "# - ), - RocList::from_slice(&[1.0]), - RocList - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn insert() { - assert_evals_to!( - indoc!( - r#" - Set.empty - |> Set.insert 0 - |> Set.insert 1 - |> Set.insert 2 - |> Set.toList - "# - ), - RocList::from_slice(&[0, 1, 2]), - RocList - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn remove() { - assert_evals_to!( - indoc!( - r#" - Set.empty - |> Set.insert 0 - |> Set.insert 1 - |> Set.remove 1 - |> Set.remove 2 - |> Set.toList - "# - ), - RocList::from_slice(&[0]), - RocList - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn union() { - assert_evals_to!( - indoc!( - r#" - set1 : Set I64 - set1 = Set.fromList [1,2] - - set2 : Set I64 - set2 = Set.fromList [1,3,4] - - Set.union set1 set2 - |> Set.toList - "# - ), - RocList::from_slice(&[4, 2, 3, 1]), - RocList - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn difference() { - assert_evals_to!( - indoc!( - r#" - set1 : Set I64 - set1 = Set.fromList [1,2] - - set2 : Set I64 - set2 = Set.fromList [1,3,4] - - Set.difference set1 set2 - |> Set.toList - "# - ), - RocList::from_slice(&[2]), - RocList - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn intersection() { - assert_evals_to!( - indoc!( - r#" - set1 : Set I64 - set1 = Set.fromList [1,2] - - set2 : Set I64 - set2 = Set.fromList [1,3,4] - - Set.intersection set1 set2 - |> Set.toList - "# - ), - RocList::from_slice(&[1]), - RocList - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn walk_sum() { - assert_evals_to!( - indoc!( - r#" - Set.walk (Set.fromList [1,2,3]) 0 (\x, y -> x + y) - "# - ), - 6, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn contains() { - assert_evals_to!( - indoc!( - r#" - Set.contains (Set.fromList [1,3,4]) 4 - "# - ), - true, - bool - ); - - assert_evals_to!( - indoc!( - r#" - Set.contains (Set.fromList [1,3,4]) 2 - "# - ), - false, - bool - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn from_list() { - assert_evals_to!( - indoc!( - r#" - [1,2,2,3,1,4] - |> Set.fromList - |> Set.toList - "# - ), - RocList::from_slice(&[4, 2, 3, 1]), - RocList - ); - - assert_evals_to!( - indoc!( - r#" - empty : List I64 - empty = [] - - empty - |> Set.fromList - |> Set.toList - "# - ), - RocList::::default(), - RocList - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn from_list_void() { - assert_evals_to!( - indoc!( - r#" - [] - |> Set.fromList - |> Set.toList - "# - ), - RocList::::default(), - RocList - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn from_list_result() { - assert_evals_to!( - indoc!( - r#" - x : Result Str {} - x = Ok "foo" - - [x] - |> Set.fromList - |> Set.toList - |> List.len - "# - ), - 1, - i64 - ); -} diff --git a/compiler/test_gen/src/gen_str.rs b/compiler/test_gen/src/gen_str.rs deleted file mode 100644 index dcb4ec08d7..0000000000 --- a/compiler/test_gen/src/gen_str.rs +++ /dev/null @@ -1,1605 +0,0 @@ -#![cfg(not(feature = "gen-wasm"))] - -#[cfg(feature = "gen-llvm")] -use crate::helpers::llvm::assert_evals_to; -#[cfg(feature = "gen-llvm")] -use crate::helpers::llvm::assert_llvm_evals_to; - -#[cfg(feature = "gen-dev")] -use crate::helpers::dev::assert_evals_to; -#[cfg(feature = "gen-dev")] -use crate::helpers::dev::assert_evals_to as assert_llvm_evals_to; - -#[allow(unused_imports)] -use indoc::indoc; -#[allow(unused_imports)] -use roc_std::{RocList, RocResult, RocStr}; - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn str_split_empty_delimiter() { - assert_evals_to!( - indoc!( - r#" - List.len (Str.split "hello" "") - "# - ), - 1, - i64 - ); - - assert_evals_to!( - indoc!( - r#" - when List.first (Str.split "JJJ" "") is - Ok str -> - Str.countGraphemes str - - _ -> - 1729 - - "# - ), - 3, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn str_split_bigger_delimiter_small_str() { - assert_evals_to!( - indoc!( - r#" - List.len (Str.split "hello" "JJJJ there") - "# - ), - 1, - i64 - ); - - assert_evals_to!( - indoc!( - r#" - when List.first (Str.split "JJJ" "JJJJ there") is - Ok str -> - Str.countGraphemes str - - _ -> - 1729 - - "# - ), - 3, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn str_split_str_concat_repeated() { - assert_evals_to!( - indoc!( - r#" - when List.first (Str.split "JJJJJ" "JJJJ there") is - Ok str -> - str - |> Str.concat str - |> Str.concat str - |> Str.concat str - |> Str.concat str - - _ -> - "Not Str!" - - "# - ), - RocStr::from("JJJJJJJJJJJJJJJJJJJJJJJJJ"), - RocStr - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn str_split_small_str_bigger_delimiter() { - assert_evals_to!( - indoc!(r#"Str.split "JJJ" "0123456789abcdefghi""#), - RocList::from_slice(&[RocStr::from("JJJ")]), - RocList - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn str_split_big_str_small_delimiter() { - assert_evals_to!( - indoc!( - r#" - Str.split "01234567789abcdefghi?01234567789abcdefghi" "?" - "# - ), - RocList::from_slice(&[ - RocStr::from("01234567789abcdefghi"), - RocStr::from("01234567789abcdefghi") - ]), - RocList - ); - - assert_evals_to!( - indoc!( - r#" - Str.split "01234567789abcdefghi 3ch 01234567789abcdefghi" "3ch" - "# - ), - RocList::from_slice(&[ - RocStr::from("01234567789abcdefghi "), - RocStr::from(" 01234567789abcdefghi") - ]), - RocList - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn str_split_small_str_small_delimiter() { - assert_evals_to!( - indoc!( - r#" - Str.split "J!J!J" "!" - "# - ), - RocList::from_slice(&[RocStr::from("J"), RocStr::from("J"), RocStr::from("J")]), - RocList - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn str_split_bigger_delimiter_big_strs() { - assert_evals_to!( - indoc!( - r#" - Str.split - "string to split is shorter" - "than the delimiter which happens to be very very long" - "# - ), - RocList::from_slice(&[RocStr::from("string to split is shorter")]), - RocList - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn str_split_empty_strs() { - assert_evals_to!( - indoc!( - r#" - Str.split "" "" - "# - ), - RocList::from_slice(&[RocStr::from("")]), - RocList - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn str_split_minimal_example() { - assert_evals_to!( - indoc!( - r#" - Str.split "a," "," - "# - ), - RocList::from_slice(&[RocStr::from("a"), RocStr::from("")]), - RocList - ) -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn str_split_small_str_big_delimiter() { - assert_evals_to!( - indoc!( - r#" - Str.split - "1---- ---- ---- ---- ----2---- ---- ---- ---- ----" - "---- ---- ---- ---- ----" - |> List.len - "# - ), - 3, - i64 - ); - - assert_evals_to!( - indoc!( - r#" - Str.split - "1---- ---- ---- ---- ----2---- ---- ---- ---- ----" - "---- ---- ---- ---- ----" - "# - ), - RocList::from_slice(&[RocStr::from("1"), RocStr::from("2"), RocStr::from("")]), - RocList - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn str_split_small_str_20_char_delimiter() { - assert_evals_to!( - indoc!( - r#" - Str.split - "3|-- -- -- -- -- -- |4|-- -- -- -- -- -- |" - "|-- -- -- -- -- -- |" - "# - ), - RocList::from_slice(&[RocStr::from("3"), RocStr::from("4"), RocStr::from("")]), - RocList - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn str_concat_big_to_big() { - assert_evals_to!( - indoc!( - r#" - Str.concat - "First string that is fairly long. Longer strings make for different errors. " - "Second string that is also fairly long. Two long strings test things that might not appear with short strings." - "# - ), - RocStr::from("First string that is fairly long. Longer strings make for different errors. Second string that is also fairly long. Two long strings test things that might not appear with short strings."), - RocStr - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-dev"))] -fn small_str_literal() { - assert_llvm_evals_to!( - "\"JJJJJJJJJJJJJJJJJJJJJJJ\"", - [ - b'J', - b'J', - b'J', - b'J', - b'J', - b'J', - b'J', - b'J', - b'J', - b'J', - b'J', - b'J', - b'J', - b'J', - b'J', - b'J', - b'J', - b'J', - b'J', - b'J', - b'J', - b'J', - b'J', - 0b1000_0000 | 23 - ], - [u8; 24] - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-dev"))] -fn small_str_zeroed_literal() { - // Verifies that we zero out unused bytes in the string. - // This is important so that string equality tests don't randomly - // fail due to unused memory being there! - assert_llvm_evals_to!( - "\"J\"", - [ - 0x4a, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0b1000_0001 - ], - [u8; 24] - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-dev"))] -fn small_str_concat_empty_first_arg() { - assert_llvm_evals_to!( - r#"Str.concat "" "JJJJJJJJJJJJJJJ""#, - [ - b'J', - b'J', - b'J', - b'J', - b'J', - b'J', - b'J', - b'J', - b'J', - b'J', - b'J', - b'J', - b'J', - b'J', - b'J', - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0b1000_0000 | 15 - ], - [u8; 24] - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-dev"))] -fn small_str_concat_empty_second_arg() { - assert_llvm_evals_to!( - r#"Str.concat "JJJJJJJJJJJJJJJ" """#, - [ - b'J', - b'J', - b'J', - b'J', - b'J', - b'J', - b'J', - b'J', - b'J', - b'J', - b'J', - b'J', - b'J', - b'J', - b'J', - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0b1000_0000 | 15 - ], - [u8; 24] - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn small_str_concat_small_to_big() { - assert_evals_to!( - r#"Str.concat "abc" " this is longer than 15 chars""#, - RocStr::from("abc this is longer than 15 chars"), - RocStr - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-dev"))] -fn small_str_concat_small_to_small_staying_small() { - assert_llvm_evals_to!( - r#"Str.concat "J" "JJJJJJJJJJJJJJ""#, - [ - b'J', - b'J', - b'J', - b'J', - b'J', - b'J', - b'J', - b'J', - b'J', - b'J', - b'J', - b'J', - b'J', - b'J', - b'J', - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0b1000_0000 | 15 - ], - [u8; 24] - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-dev"))] -fn small_str_concat_small_to_small_overflow_to_big() { - assert_evals_to!( - r#"Str.concat "abcdefghijklm" "nopqrstuvwxyz""#, - RocStr::from("abcdefghijklmnopqrstuvwxyz"), - RocStr - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-dev"))] -fn str_concat_empty() { - assert_evals_to!(r#"Str.concat "" """#, RocStr::default(), RocStr); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn small_str_is_empty() { - assert_evals_to!(r#"Str.isEmpty "abc""#, false, bool); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn big_str_is_empty() { - assert_evals_to!( - r#"Str.isEmpty "this is more than 15 chars long""#, - false, - bool - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn empty_str_is_empty() { - assert_evals_to!(r#"Str.isEmpty """#, true, bool); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn str_starts_with() { - assert_evals_to!(r#"Str.startsWith "hello world" "hell""#, true, bool); - assert_evals_to!(r#"Str.startsWith "hello world" """#, true, bool); - assert_evals_to!(r#"Str.startsWith "nope" "hello world""#, false, bool); - assert_evals_to!(r#"Str.startsWith "hell" "hello world""#, false, bool); - assert_evals_to!(r#"Str.startsWith "" "hello world""#, false, bool); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn str_starts_with_code_point() { - assert_evals_to!( - &format!(r#"Str.startsWithCodePt "foobar" {}"#, 'f' as u32), - true, - bool - ); - assert_evals_to!( - &format!(r#"Str.startsWithCodePt "zoobar" {}"#, 'f' as u32), - false, - bool - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn str_ends_with() { - assert_evals_to!(r#"Str.endsWith "hello world" "world""#, true, bool); - assert_evals_to!(r#"Str.endsWith "nope" "hello world""#, false, bool); - assert_evals_to!(r#"Str.endsWith "" "hello world""#, false, bool); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn str_count_graphemes_small_str() { - assert_evals_to!(r#"Str.countGraphemes "å🤔""#, 2, usize); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn str_count_graphemes_three_js() { - assert_evals_to!(r#"Str.countGraphemes "JJJ""#, 3, usize); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn str_count_graphemes_big_str() { - assert_evals_to!( - r#"Str.countGraphemes "6🤔å🤔e¥🤔çppkd🙃1jdal🦯asdfa∆ltråø˚waia8918.,🏅jjc""#, - 45, - usize - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn str_starts_with_same_big_str() { - assert_evals_to!( - r#"Str.startsWith "123456789123456789" "123456789123456789""#, - true, - bool - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn str_starts_with_different_big_str() { - assert_evals_to!( - r#"Str.startsWith "12345678912345678910" "123456789123456789""#, - true, - bool - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn str_starts_with_same_small_str() { - assert_evals_to!(r#"Str.startsWith "1234" "1234""#, true, bool); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn str_starts_with_different_small_str() { - assert_evals_to!(r#"Str.startsWith "1234" "12""#, true, bool); -} -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn str_starts_with_false_small_str() { - assert_evals_to!(r#"Str.startsWith "1234" "23""#, false, bool); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn str_from_utf8_pass_single_ascii() { - assert_evals_to!( - indoc!( - r#" - when Str.fromUtf8 [97] is - Ok val -> val - Err _ -> "" - "# - ), - roc_std::RocStr::from("a"), - roc_std::RocStr - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn str_from_utf8_pass_many_ascii() { - assert_evals_to!( - indoc!( - r#" - when Str.fromUtf8 [97, 98, 99, 0x7E] is - Ok val -> val - Err _ -> "" - "# - ), - roc_std::RocStr::from("abc~"), - roc_std::RocStr - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn str_from_utf8_pass_single_unicode() { - assert_evals_to!( - indoc!( - r#" - when Str.fromUtf8 [0xE2, 0x88, 0x86] is - Ok val -> val - Err _ -> "" - "# - ), - roc_std::RocStr::from("∆"), - roc_std::RocStr - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn str_from_utf8_pass_many_unicode() { - assert_evals_to!( - indoc!( - r#" - when Str.fromUtf8 [0xE2, 0x88, 0x86, 0xC5, 0x93, 0xC2, 0xAC] is - Ok val -> val - Err _ -> "" - "# - ), - roc_std::RocStr::from("∆œ¬"), - roc_std::RocStr - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn str_from_utf8_pass_single_grapheme() { - assert_evals_to!( - indoc!( - r#" - when Str.fromUtf8 [0xF0, 0x9F, 0x92, 0x96] is - Ok val -> val - Err _ -> "" - "# - ), - roc_std::RocStr::from("💖"), - roc_std::RocStr - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn str_from_utf8_pass_many_grapheme() { - assert_evals_to!( - indoc!( - r#" - when Str.fromUtf8 [0xF0, 0x9F, 0x92, 0x96, 0xF0, 0x9F, 0xA4, 0xA0, 0xF0, 0x9F, 0x9A, 0x80] is - Ok val -> val - Err _ -> "" - "# - ), - roc_std::RocStr::from("💖🤠🚀"), - roc_std::RocStr - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn str_from_utf8_pass_all() { - assert_evals_to!( - indoc!( - r#" - when Str.fromUtf8 [0xF0, 0x9F, 0x92, 0x96, 98, 0xE2, 0x88, 0x86] is - Ok val -> val - Err _ -> "" - "# - ), - roc_std::RocStr::from("💖b∆"), - roc_std::RocStr - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn str_from_utf8_fail_invalid_start_byte() { - assert_evals_to!( - indoc!( - r#" - when Str.fromUtf8 [97, 98, 0x80, 99] is - Err (BadUtf8 InvalidStartByte byteIndex) -> - if byteIndex == 2 then - "a" - else - "b" - _ -> "" - "# - ), - roc_std::RocStr::from("a"), - roc_std::RocStr - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn str_from_utf8_fail_unexpected_end_of_sequence() { - assert_evals_to!( - indoc!( - r#" - when Str.fromUtf8 [97, 98, 99, 0xC2] is - Err (BadUtf8 UnexpectedEndOfSequence byteIndex) -> - if byteIndex == 3 then - "a" - else - "b" - _ -> "" - "# - ), - roc_std::RocStr::from("a"), - roc_std::RocStr - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn str_from_utf8_fail_expected_continuation() { - assert_evals_to!( - indoc!( - r#" - when Str.fromUtf8 [97, 98, 99, 0xC2, 0x00] is - Err (BadUtf8 ExpectedContinuation byteIndex) -> - if byteIndex == 3 then - "a" - else - "b" - _ -> "" - "# - ), - roc_std::RocStr::from("a"), - roc_std::RocStr - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn str_from_utf8_fail_overlong_encoding() { - assert_evals_to!( - indoc!( - r#" - when Str.fromUtf8 [97, 0xF0, 0x80, 0x80, 0x80] is - Err (BadUtf8 OverlongEncoding byteIndex) -> - if byteIndex == 1 then - "a" - else - "b" - _ -> "" - "# - ), - roc_std::RocStr::from("a"), - roc_std::RocStr - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn str_from_utf8_fail_codepoint_too_large() { - assert_evals_to!( - indoc!( - r#" - when Str.fromUtf8 [97, 0xF4, 0x90, 0x80, 0x80] is - Err (BadUtf8 CodepointTooLarge byteIndex) -> - if byteIndex == 1 then - "a" - else - "b" - _ -> "" - "# - ), - roc_std::RocStr::from("a"), - roc_std::RocStr - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn str_from_utf8_fail_surrogate_half() { - assert_evals_to!( - indoc!( - r#" - when Str.fromUtf8 [97, 98, 0xED, 0xA0, 0x80] is - Err (BadUtf8 EncodesSurrogateHalf byteIndex) -> - if byteIndex == 2 then - "a" - else - "b" - _ -> "" - "# - ), - roc_std::RocStr::from("a"), - roc_std::RocStr - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn str_equality() { - assert_evals_to!(r#""a" == "a""#, true, bool); - assert_evals_to!( - r#""loremipsumdolarsitamet" == "loremipsumdolarsitamet""#, - true, - bool - ); - assert_evals_to!(r#""a" != "b""#, true, bool); - assert_evals_to!(r#""a" == "b""#, false, bool); -} - -#[test] -fn str_clone() { - use roc_std::RocStr; - let long = RocStr::from("loremipsumdolarsitamet"); - let short = RocStr::from("x"); - let empty = RocStr::from(""); - - debug_assert_eq!(long.clone(), long); - debug_assert_eq!(short.clone(), short); - debug_assert_eq!(empty.clone(), empty); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn nested_recursive_literal() { - assert_evals_to!( - indoc!( - r#" - Expr : [Add Expr Expr, Val I64, Var I64] - - expr : Expr - expr = Add (Add (Val 3) (Val 1)) (Add (Val 1) (Var 1)) - - printExpr : Expr -> Str - printExpr = \e -> - when e is - Add a b -> - "Add (" - |> Str.concat (printExpr a) - |> Str.concat ") (" - |> Str.concat (printExpr b) - |> Str.concat ")" - Val v -> "Val " |> Str.concat (Num.toStr v) - Var v -> "Var " |> Str.concat (Num.toStr v) - - printExpr expr - "# - ), - RocStr::from("Add (Add (Val 3) (Val 1)) (Add (Val 1) (Var 1))"), - RocStr - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn str_join_comma_small() { - assert_evals_to!( - r#"Str.joinWith ["1", "2"] ", " "#, - RocStr::from("1, 2"), - RocStr - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn str_join_comma_big() { - assert_evals_to!( - r#"Str.joinWith ["10000000", "2000000", "30000000"] ", " "#, - RocStr::from("10000000, 2000000, 30000000"), - RocStr - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn str_join_comma_single() { - assert_evals_to!(r#"Str.joinWith ["1"] ", " "#, RocStr::from("1"), RocStr); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn str_to_utf8() { - assert_evals_to!( - r#"Str.toUtf8 "hello""#, - RocList::from_slice(&[104, 101, 108, 108, 111]), - RocList - ); - assert_evals_to!( - r#"Str.toUtf8 "this is a long string""#, - RocList::from_slice(&[ - 116, 104, 105, 115, 32, 105, 115, 32, 97, 32, 108, 111, 110, 103, 32, 115, 116, 114, - 105, 110, 103 - ]), - RocList - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn str_from_utf8_range() { - assert_evals_to!( - indoc!( - r#" - bytes = Str.toUtf8 "hello" - when Str.fromUtf8Range bytes { count: 5, start: 0 } is - Ok utf8String -> utf8String - _ -> "" - "# - ), - RocStr::from("hello"), - RocStr - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn str_from_utf8_range_slice() { - assert_evals_to!( - indoc!( - r#" - bytes = Str.toUtf8 "hello" - when Str.fromUtf8Range bytes { count: 4, start: 1 } is - Ok utf8String -> utf8String - _ -> "" - "# - ), - RocStr::from("ello"), - RocStr - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn str_from_utf8_range_slice_not_end() { - assert_evals_to!( - indoc!( - r#" - bytes = Str.toUtf8 "hello" - when Str.fromUtf8Range bytes { count: 3, start: 1 } is - Ok utf8String -> utf8String - _ -> "" - "# - ), - RocStr::from("ell"), - RocStr - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn str_from_utf8_range_order_does_not_matter() { - assert_evals_to!( - indoc!( - r#" - bytes = Str.toUtf8 "hello" - when Str.fromUtf8Range bytes { start: 1, count: 3 } is - Ok utf8String -> utf8String - _ -> "" - "# - ), - RocStr::from("ell"), - RocStr - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn str_from_utf8_range_out_of_bounds_start_value() { - assert_evals_to!( - indoc!( - r#" - bytes = Str.toUtf8 "hello" - when Str.fromUtf8Range bytes { start: 7, count: 3 } is - Ok _ -> "" - Err (BadUtf8 _ _) -> "" - Err OutOfBounds -> "out of bounds" - "# - ), - RocStr::from("out of bounds"), - RocStr - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn str_from_utf8_range_count_too_high() { - assert_evals_to!( - indoc!( - r#" - bytes = Str.toUtf8 "hello" - when Str.fromUtf8Range bytes { start: 0, count: 6 } is - Ok _ -> "" - Err (BadUtf8 _ _) -> "" - Err OutOfBounds -> "out of bounds" - "# - ), - RocStr::from("out of bounds"), - RocStr - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn str_from_utf8_range_count_too_high_for_start() { - assert_evals_to!( - indoc!( - r#" - bytes = Str.toUtf8 "hello" - when Str.fromUtf8Range bytes { start: 4, count: 3 } is - Ok _ -> "" - Err (BadUtf8 _ _) -> "" - Err OutOfBounds -> "out of bounds" - "# - ), - RocStr::from("out of bounds"), - RocStr - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn str_repeat_small_stays_small() { - assert_evals_to!( - indoc!(r#"Str.repeat "Roc" 3"#), - RocStr::from("RocRocRoc"), - RocStr - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn str_repeat_small_becomes_big() { - assert_evals_to!( - indoc!(r#"Str.repeat "less than 23 characters" 2"#), - RocStr::from("less than 23 charactersless than 23 characters"), - RocStr - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn str_repeat_big() { - assert_evals_to!( - indoc!(r#"Str.repeat "more than 23 characters now" 2"#), - RocStr::from("more than 23 characters nowmore than 23 characters now"), - RocStr - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn str_repeat_empty_string() { - let a = indoc!(r#"Str.repeat "" 3"#); - let b = RocStr::from(""); - assert_evals_to!(a, b, RocStr); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn str_repeat_zero_times() { - assert_evals_to!(indoc!(r#"Str.repeat "Roc" 0"#), RocStr::from(""), RocStr); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn str_trim_empty_string() { - assert_evals_to!(indoc!(r#"Str.trim """#), RocStr::from(""), RocStr); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn str_trim_small_blank_string() { - assert_evals_to!(indoc!(r#"Str.trim " ""#), RocStr::from(""), RocStr); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn str_trim_small_to_small() { - assert_evals_to!( - indoc!(r#"Str.trim " hello world ""#), - RocStr::from("hello world"), - RocStr - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn str_trim_large_to_large_unique() { - assert_evals_to!( - indoc!(r#"Str.trim (Str.concat " " "hello world from a large string ")"#), - RocStr::from("hello world from a large string"), - RocStr - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn str_trim_large_to_small_unique() { - assert_evals_to!( - indoc!(r#"Str.trim (Str.concat " " "hello world ")"#), - RocStr::from("hello world"), - RocStr - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn str_trim_large_to_large_shared() { - assert_evals_to!( - indoc!( - r#" - original : Str - original = " hello world world " - - { trimmed: Str.trim original, original: original } - "# - ), - ( - RocStr::from(" hello world world "), - RocStr::from("hello world world"), - ), - (RocStr, RocStr) - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn str_trim_large_to_small_shared() { - assert_evals_to!( - indoc!( - r#" - original : Str - original = " hello world " - - { trimmed: Str.trim original, original: original } - "# - ), - ( - RocStr::from(" hello world "), - RocStr::from("hello world"), - ), - (RocStr, RocStr) - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn str_trim_small_to_small_shared() { - assert_evals_to!( - indoc!( - r#" - original : Str - original = " hello world " - - { trimmed: Str.trim original, original: original } - "# - ), - (RocStr::from(" hello world "), RocStr::from("hello world"),), - (RocStr, RocStr) - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn str_trim_left_small_blank_string() { - assert_evals_to!(indoc!(r#"Str.trimLeft " ""#), RocStr::from(""), RocStr); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn str_trim_left_small_to_small() { - assert_evals_to!( - indoc!(r#"Str.trimLeft " hello world ""#), - RocStr::from("hello world "), - RocStr - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn str_trim_left_large_to_large_unique() { - assert_evals_to!( - indoc!(r#"Str.trimLeft (Str.concat " " "hello world from a large string ")"#), - RocStr::from("hello world from a large string "), - RocStr - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn str_trim_left_large_to_small_unique() { - assert_evals_to!( - indoc!(r#"Str.trimLeft (Str.concat " " "hello world ")"#), - RocStr::from("hello world "), - RocStr - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn str_trim_left_large_to_large_shared() { - assert_evals_to!( - indoc!( - r#" - original : Str - original = " hello world world " - - { trimmed: Str.trimLeft original, original: original } - "# - ), - ( - RocStr::from(" hello world world "), - RocStr::from("hello world world "), - ), - (RocStr, RocStr) - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn str_trim_left_large_to_small_shared() { - assert_evals_to!( - indoc!( - r#" - original : Str - original = " hello world " - - { trimmed: Str.trimLeft original, original: original } - "# - ), - ( - RocStr::from(" hello world "), - RocStr::from("hello world "), - ), - (RocStr, RocStr) - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn str_trim_left_small_to_small_shared() { - assert_evals_to!( - indoc!( - r#" - original : Str - original = " hello world " - - { trimmed: Str.trimLeft original, original: original } - "# - ), - (RocStr::from(" hello world "), RocStr::from("hello world "),), - (RocStr, RocStr) - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn str_trim_right_small_blank_string() { - assert_evals_to!(indoc!(r#"Str.trimRight " ""#), RocStr::from(""), RocStr); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn str_trim_right_small_to_small() { - assert_evals_to!( - indoc!(r#"Str.trimRight " hello world ""#), - RocStr::from(" hello world"), - RocStr - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn str_trim_right_large_to_large_unique() { - assert_evals_to!( - indoc!(r#"Str.trimRight (Str.concat " hello world from a large string" " ")"#), - RocStr::from(" hello world from a large string"), - RocStr - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn str_trim_right_large_to_small_unique() { - assert_evals_to!( - indoc!(r#"Str.trimRight (Str.concat " hello world" " ")"#), - RocStr::from(" hello world"), - RocStr - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn str_trim_right_large_to_large_shared() { - assert_evals_to!( - indoc!( - r#" - original : Str - original = " hello world world " - - { trimmed: Str.trimRight original, original: original } - "# - ), - ( - RocStr::from(" hello world world "), - RocStr::from(" hello world world"), - ), - (RocStr, RocStr) - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn str_trim_right_large_to_small_shared() { - assert_evals_to!( - indoc!( - r#" - original : Str - original = " hello world " - - { trimmed: Str.trimRight original, original: original } - "# - ), - ( - RocStr::from(" hello world "), - RocStr::from(" hello world"), - ), - (RocStr, RocStr) - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn str_trim_right_small_to_small_shared() { - assert_evals_to!( - indoc!( - r#" - original : Str - original = " hello world " - - { trimmed: Str.trimRight original, original: original } - "# - ), - (RocStr::from(" hello world "), RocStr::from(" hello world"),), - (RocStr, RocStr) - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn str_to_nat() { - assert_evals_to!( - indoc!( - r#" - when Str.toNat "1" is - Ok n -> n - Err _ -> 0 - - "# - ), - 1, - usize - ); -} - -#[test] -#[ignore = "TODO: figure out why returning i128 across FFI boundary is an issue"] -#[cfg(any(feature = "gen-llvm"))] -fn str_to_i128() { - assert_evals_to!( - indoc!( - r#" - when Str.toI128 "1" is - Ok n -> n - Err _ -> 0 - - "# - ), - 1, - i128 - ); -} - -#[test] -#[ignore = "TODO: figure out why returning i128 across FFI boundary is an issue"] -#[cfg(any(feature = "gen-llvm"))] -fn str_to_u128() { - assert_evals_to!( - indoc!( - r#" - when Str.toU128 "1" is - Ok n -> n - Err _ -> 0 - - "# - ), - 1, - u128 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn str_to_i64() { - assert_evals_to!( - indoc!( - r#" - when Str.toI64 "1" is - Ok n -> n - Err _ -> 0 - - "# - ), - 1, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn str_to_u64() { - assert_evals_to!( - r#"Str.toU64 "1""#, - RocResult::ok(1u64), - RocResult - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn str_to_i32() { - assert_evals_to!( - indoc!( - r#" - when Str.toI32 "1" is - Ok n -> n - Err _ -> 0 - - "# - ), - 1, - i32 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn str_to_u32() { - assert_evals_to!( - r#"Str.toU32 "1""#, - RocResult::ok(1u32), - RocResult - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn str_to_i16() { - assert_evals_to!( - indoc!( - r#" - when Str.toI16 "1" is - Ok n -> n - Err _ -> 0 - - "# - ), - 1, - i16 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn str_to_u16() { - assert_evals_to!( - indoc!( - r#" - when Str.toU16 "1" is - Ok n -> n - Err _ -> 0 - - "# - ), - 1, - u16 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn str_to_i8() { - assert_evals_to!( - indoc!( - r#" - when Str.toI8 "1" is - Ok n -> n - Err _ -> 0 - - "# - ), - 1, - i8 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn str_to_u8() { - assert_evals_to!( - indoc!( - r#" - when Str.toU8 "1" is - Ok n -> n - Err _ -> 0 - - "# - ), - 1, - u8 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn str_to_f64() { - assert_evals_to!( - indoc!( - r#" - when Str.toF64 "1.0" is - Ok n -> n - Err _ -> 0 - - "# - ), - 1.0, - f64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn str_to_f32() { - assert_evals_to!( - indoc!( - r#" - when Str.toF32 "1.0" is - Ok n -> n - Err _ -> 0 - - "# - ), - 1.0, - f32 - ); -} - -#[test] -#[ignore = "TODO: figure out why returning i128 across FFI boundary is an issue"] -#[cfg(any(feature = "gen-llvm"))] -fn str_to_dec() { - use roc_std::RocDec; - - assert_evals_to!( - indoc!( - r#" - when Str.toDec "1.0" is - Ok n -> n - Err _ -> 0 - - "# - ), - RocDec::from_str("1.0").unwrap(), - RocDec - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn issue_2811() { - assert_evals_to!( - indoc!( - r#" - x = Command { tool: "bash" } - Command c = x - c.tool - "# - ), - RocStr::from("bash"), - RocStr - ); -} diff --git a/compiler/test_gen/src/gen_tags.rs b/compiler/test_gen/src/gen_tags.rs deleted file mode 100644 index 1ff2358b55..0000000000 --- a/compiler/test_gen/src/gen_tags.rs +++ /dev/null @@ -1,1705 +0,0 @@ -#[cfg(feature = "gen-llvm")] -use crate::helpers::llvm::assert_evals_to; - -#[cfg(feature = "gen-dev")] -use crate::helpers::dev::assert_evals_to; - -#[cfg(feature = "gen-wasm")] -use crate::helpers::wasm::assert_evals_to; - -#[cfg(test)] -use indoc::indoc; -#[cfg(test)] -use roc_std::{RocList, RocStr}; - -#[test] -fn width_and_alignment_u8_u8() { - use roc_mono::layout::Layout; - use roc_mono::layout::UnionLayout; - - let t = &[Layout::u8()] as &[_]; - let tt = [t, t]; - - let layout = Layout::Union(UnionLayout::NonRecursive(&tt)); - - let target_info = roc_target::TargetInfo::default_x86_64(); - assert_eq!(layout.alignment_bytes(target_info), 1); - assert_eq!(layout.stack_size(target_info), 2); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] -fn applied_tag_nothing_ir() { - assert_evals_to!( - indoc!( - r#" - Maybe a : [Just a, Nothing] - - x : Maybe I64 - x = Nothing - - x - "# - ), - 1, - (i64, u8), - |(_, tag)| tag - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] -fn applied_tag_nothing() { - assert_evals_to!( - indoc!( - r#" - Maybe a : [Just a, Nothing] - - x : Maybe I64 - x = Nothing - - x - "# - ), - 1, - (i64, u8), - |(_, tag)| tag - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] -fn applied_tag_just() { - assert_evals_to!( - indoc!( - r#" - Maybe a : [Just a, Nothing] - - y : Maybe I64 - y = Just 0x4 - - y - "# - ), - (0x4, 0), - (i64, u8) - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] -fn applied_tag_just_ir() { - assert_evals_to!( - indoc!( - r#" - Maybe a : [Just a, Nothing] - - y : Maybe I64 - y = Just 0x4 - - y - "# - ), - (0x4, 0), - (i64, u8) - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn applied_tag_just_enum() { - assert_evals_to!( - indoc!( - r#" - Fruit : [Orange, Apple, Banana] - Maybe a : [Just a, Nothing] - - orange : Fruit - orange = Orange - - y : Maybe Fruit - y = Just orange - - y - "# - ), - (2, 0), - (u8, u8) - ); -} - -// #[test] -// #[cfg(any(feature = "gen-llvm"))] -// fn raw_result() { -// assert_evals_to!( -// indoc!( -// r#" -// x : Result I64 I64 -// x = Err 41 - -// x -// "# -// ), -// 0, -// i8 -// ); -// } -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn true_is_true() { - assert_evals_to!( - indoc!( - r#" - bool : [True, False] - bool = True - - bool - "# - ), - true, - bool - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn false_is_false() { - assert_evals_to!( - indoc!( - r#" - bool : [True, False] - bool = False - - bool - "# - ), - false, - bool - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn basic_enum() { - assert_evals_to!( - indoc!( - r#" - Fruit : [Apple, Orange, Banana] - - apple : Fruit - apple = Apple - - orange : Fruit - orange = Orange - - apple == orange - "# - ), - false, - bool - ); -} - -// #[test] -// #[cfg(any(feature = "gen-llvm"))] -// fn linked_list_empty() { -// assert_evals_to!( -// indoc!( -// r#" -// LinkedList a : [Cons a (LinkedList a), Nil] -// -// empty : LinkedList I64 -// empty = Nil -// -// 1 -// "# -// ), -// 1, -// i64 -// ); -// } -// -// #[test] -// #[cfg(any(feature = "gen-llvm"))] -// fn linked_list_singleton() { -// assert_evals_to!( -// indoc!( -// r#" -// LinkedList a : [Cons a (LinkedList a), Nil] -// -// singleton : LinkedList I64 -// singleton = Cons 0x1 Nil -// -// 1 -// "# -// ), -// 1, -// i64 -// ); -// } -// -// #[test] -// #[cfg(any(feature = "gen-llvm"))] -// fn linked_list_is_empty() { -// assert_evals_to!( -// indoc!( -// r#" -// LinkedList a : [Cons a (LinkedList a), Nil] -// -// isEmpty : LinkedList a -> Bool -// isEmpty = \list -> -// when list is -// Nil -> True -// Cons _ _ -> False -// -// isEmpty (Cons 4 Nil) -// "# -// ), -// false, -// bool -// ); -// } -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn even_odd() { - assert_evals_to!( - indoc!( - r#" - even = \n -> - when n is - 0 -> True - 1 -> False - _ -> odd (n - 1) - - odd = \n -> - when n is - 0 -> False - 1 -> True - _ -> even (n - 1) - - odd 5 && even 42 - "# - ), - true, - bool - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn gen_literal_true() { - assert_evals_to!( - indoc!( - r#" - if True then -1 else 1 - "# - ), - -1, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn gen_if_float() { - assert_evals_to!( - indoc!( - r#" - if True then -1.0 else 1.0 - "# - ), - -1.0, - f64 - ); -} -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] -fn when_on_nothing() { - assert_evals_to!( - indoc!( - r#" - x : [Nothing, Just I64] - x = Nothing - - when x is - Nothing -> 0x2 - Just _ -> 0x1 - "# - ), - 2, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] -fn when_on_just() { - assert_evals_to!( - indoc!( - r#" - x : [Nothing, Just I64] - x = Just 41 - - when x is - Just v -> v + 0x1 - Nothing -> 0x1 - "# - ), - 42, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] -fn when_on_result() { - assert_evals_to!( - indoc!( - r#" - x : Result I64 I64 - x = Err 41 - - when x is - Err v -> v + 1 - Ok _ -> 1 - "# - ), - 42, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] -fn when_on_these() { - assert_evals_to!( - indoc!( - r#" - These a b : [This a, That b, These a b] - - x : These I64 I64 - x = These 0x3 0x2 - - when x is - These a b -> a + b - That v -> v - This v -> v - "# - ), - 5, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] -fn match_on_two_values() { - // this will produce a Chain internally - assert_evals_to!( - indoc!( - r#" - when Pair 2 3 is - Pair 4 3 -> 9 - Pair a b -> a + b - "# - ), - 5, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] -fn pair_with_underscore() { - assert_evals_to!( - indoc!( - r#" - when Pair 2 3 is - Pair 4 _ -> 1 - Pair 3 _ -> 2 - Pair a b -> a + b - "# - ), - 5, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] -fn result_with_underscore() { - // This test revealed an issue with hashing Test values - assert_evals_to!( - indoc!( - r#" - x : Result I64 I64 - x = Ok 2 - - when x is - Ok 3 -> 1 - Ok _ -> 2 - Err _ -> 3 - "# - ), - 2, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn maybe_is_just_not_nested() { - assert_evals_to!( - indoc!( - r#" - app "test" provides [main] to "./platform" - - Maybe a : [Just a, Nothing] - - isJust : Maybe a -> Bool - isJust = \list -> - when list is - Nothing -> False - Just _ -> True - - main = - isJust (Just 42) - "# - ), - true, - bool - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn maybe_is_just_nested() { - assert_evals_to!( - indoc!( - r#" - Maybe a : [Just a, Nothing] - - isJust : Maybe a -> Bool - isJust = \list -> - when list is - Nothing -> False - Just _ -> True - - isJust (Just 42) - "# - ), - true, - bool - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn nested_pattern_match() { - assert_evals_to!( - indoc!( - r#" - Maybe a : [Nothing, Just a] - - x : Maybe (Maybe I64) - x = Just (Just 41) - - when x is - Just (Just v) -> v + 0x1 - _ -> 0x1 - "# - ), - 42, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn if_guard_vanilla() { - assert_evals_to!( - indoc!( - r#" - when "fooz" is - s if s == "foo" -> 0 - s -> List.len (Str.toUtf8 s) - "# - ), - 4, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -#[ignore] -fn when_on_single_value_tag() { - // this fails because the switched-on symbol is not defined - assert_evals_to!( - indoc!( - r#" - when Identity 0 is - Identity 0 -> 0 - Identity s -> s - "# - ), - 6, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn if_guard_multiple() { - assert_evals_to!( - indoc!( - r#" - f = \n -> - when Identity n 0 is - Identity x _ if x == 0 -> x + 0 - Identity x _ if x == 1 -> x + 0 - Identity x _ if x == 2 -> x + 0 - Identity x _ -> x - x - - { a: f 0, b: f 1, c: f 2, d: f 4 } - "# - ), - [0, 1, 2, 0], - [i64; 4] - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn if_guard_constructor_switch() { - assert_evals_to!( - indoc!( - r#" - when Identity 32 0 is - Identity 41 _ -> 0 - Identity s 0 if s == 32 -> 3 - # Identity s 0 -> s - Identity z _ -> z - "# - ), - 3, - i64 - ); - - assert_evals_to!( - indoc!( - r#" - when Identity 42 "" is - Identity 41 _ -> 0 - Identity 42 _ if 3 == 3 -> 1 - Identity z _ -> z - "# - ), - 1, - i64 - ); - - assert_evals_to!( - indoc!( - r#" - when Identity 42 "" is - Identity 41 _ -> 0 - Identity 42 _ if 3 != 3 -> 1 - Identity z _ -> z - "# - ), - 42, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn if_guard_constructor_chain() { - assert_evals_to!( - indoc!( - r#" - when Identity 43 0 is - Identity 42 _ if 3 == 3 -> 43 - # Identity 42 _ -> 1 - Identity z _ -> z - "# - ), - 43, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn if_guard_pattern_false() { - assert_evals_to!( - indoc!( - r#" - wrapper = \{} -> - when 2 is - 2 if False -> 0 - _ -> 42 - - wrapper {} - "# - ), - 42, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn if_guard_switch() { - assert_evals_to!( - indoc!( - r#" - wrapper = \{} -> - when 2 is - 2 | 3 if False -> 0 - _ -> 42 - - wrapper {} - "# - ), - 42, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn if_guard_pattern_true() { - assert_evals_to!( - indoc!( - r#" - wrapper = \{} -> - when 2 is - 2 if True -> 42 - _ -> 0 - - wrapper {} - "# - ), - 42, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn if_guard_exhaustiveness() { - assert_evals_to!( - indoc!( - r#" - wrapper = \{} -> - when 2 is - _ if False -> 0 - _ -> 42 - - wrapper {} - "# - ), - 42, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn when_on_enum() { - assert_evals_to!( - indoc!( - r#" - Fruit : [Apple, Orange, Banana] - - apple : Fruit - apple = Apple - - when apple is - Apple -> 1 - Banana -> 2 - Orange -> 3 - "# - ), - 1, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn pattern_matching_unit() { - assert_evals_to!( - indoc!( - r#" - Unit : [Unit] - - f : Unit -> I64 - f = \Unit -> 42 - - f Unit - "# - ), - 42, - i64 - ); - - assert_evals_to!( - indoc!( - r#" - Unit : [Unit] - - x : Unit - x = Unit - - when x is - Unit -> 42 - "# - ), - 42, - i64 - ); - - assert_evals_to!( - indoc!( - r#" - f : {} -> I64 - f = \{} -> 42 - - f {} - "# - ), - 42, - i64 - ); - - assert_evals_to!( - indoc!( - r#" - when {} is - {} -> 42 - "# - ), - 42, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn one_element_tag() { - assert_evals_to!( - indoc!( - r#" - x : [Pair I64] - x = Pair 2 - - x - "# - ), - 2, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn nested_tag_union() { - assert_evals_to!( - indoc!( - r#" - app "test" provides [main] to "./platform" - - Maybe a : [Nothing, Just a] - - x : Maybe (Maybe I64) - x = Just (Just 41) - - main = - x - "# - ), - ((41, 0), 0), - ((i64, u8), u8) - ); -} -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn unit_type() { - assert_evals_to!( - indoc!( - r#" - Unit : [Unit] - - v : Unit - v = Unit - - v - "# - ), - (), - () - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn join_point_if() { - assert_evals_to!( - indoc!( - r#" - x = - if True then 1 else 2 - - x - "# - ), - 1, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn join_point_when() { - assert_evals_to!( - indoc!( - r#" - wrapper = \{} -> - x : [Red, White, Blue] - x = Blue - - y = - when x is - Red -> 1 - White -> 2 - Blue -> 3.1 - - y - - wrapper {} - "# - ), - 3.1, - f64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn join_point_with_cond_expr() { - assert_evals_to!( - indoc!( - r#" - wrapper = \{} -> - y = - when 1 + 2 is - 3 -> 3 - 1 -> 1 - _ -> 0 - - y - - wrapper {} - "# - ), - 3, - i64 - ); - - assert_evals_to!( - indoc!( - r#" - y = - if 1 + 2 > 0 then - 3 - else - 0 - - y - "# - ), - 3, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn alignment_in_single_tag_construction() { - assert_evals_to!(indoc!("Three (1 == 1) 32"), (32i64, true), (i64, bool)); - - assert_evals_to!( - indoc!("Three (1 == 1) (if True then Red else if True then Green else Blue) 32"), - (32i64, true, 2u8), - (i64, bool, u8) - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn alignment_in_single_tag_pattern_match() { - assert_evals_to!( - indoc!( - r"# - x = Three (1 == 1) 32 - - when x is - Three bool int -> - { bool, int } - #" - ), - (32i64, true), - (i64, bool) - ); - - assert_evals_to!( - indoc!( - r"# - x = Three (1 == 1) (if True then Red else if True then Green else Blue) 32 - - when x is - Three bool color int -> - { bool, color, int } - #" - ), - (32i64, true, 2u8), - (i64, bool, u8) - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn alignment_in_multi_tag_construction_two() { - assert_evals_to!( - indoc!( - r"# - x : [Three Bool I64, Empty] - x = Three (1 == 1) 32 - - x - - #" - ), - ((32i64, true), 1), - ((i64, bool), u8) - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn alignment_in_multi_tag_construction_three() { - assert_evals_to!( - indoc!( - r"# - x : [Three Bool [Red, Green, Blue] I64, Empty] - x = Three (1 == 1) (if True then Red else if True then Green else Blue) 32 - - x - #" - ), - ((32i64, true, 2u8), 1), - ((i64, bool, u8), u8) - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn alignment_in_multi_tag_pattern_match() { - assert_evals_to!( - indoc!( - r"# - x : [Three Bool I64, Empty] - x = Three (1 == 1) 32 - - when x is - Three bool int -> - { bool, int } - - Empty -> - { bool: False, int: 0 } - #" - ), - (32i64, true), - (i64, bool) - ); - - assert_evals_to!( - indoc!( - r"# - x : [Three Bool [Red, Green, Blue] I64, Empty] - x = Three (1 == 1) (if True then Red else if True then Green else Blue) 32 - - when x is - Three bool color int -> - { bool, color, int } - Empty -> - { bool: False, color: Red, int: 0 } - #" - ), - (32i64, true, 2u8), - (i64, bool, u8) - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -#[ignore] -fn phantom_polymorphic() { - // see https://github.com/rtfeldman/roc/issues/786 and below - assert_evals_to!( - indoc!( - r"# - Point coordinate : [Point coordinate I64 I64] - - World := {} - - zero : Point World - zero = Point (@World {}) 0 0 - - add : Point a -> Point a - add = \(Point c x y) -> (Point c x y) - - add zero - #" - ), - (0, 0), - (i64, i64) - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -#[ignore] -fn phantom_polymorphic_record() { - // see https://github.com/rtfeldman/roc/issues/786 - // also seemed to hit an issue where we check whether `add` - // has a Closure layout while the type is not fully specialized yet - assert_evals_to!( - indoc!( - r#" - app "test" provides [main] to "./platform" - - Point coordinate : { coordinate : coordinate, x : I64, y : I64 } - - zero : Point I64 - zero = { coordinate : 0, x : 0, y : 0 } - - add : Point a -> Point a - add = \{ coordinate } -> { coordinate, x: 0 + 0, y: 0 } - - main = add zero - "# - ), - (0, 0), - (i64, i64) - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn result_never() { - assert_evals_to!( - indoc!( - r"# - res : Result I64 [] - res = Ok 4 - - # we should provide this in the stdlib - never : [] -> a - - when res is - Ok v -> v - Err empty -> never empty - #" - ), - 4, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn nested_recursive_literal() { - assert_evals_to!( - indoc!( - r"# - Expr : [Add Expr Expr, Val I64, Var I64] - - e : Expr - e = Add (Add (Val 3) (Val 1)) (Add (Val 1) (Var 1)) - - e - #" - ), - 0, - usize, - |_| 0 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn newtype_wrapper() { - assert_evals_to!( - indoc!( - r#" - app "test" provides [main] to "./platform" - - ConsList a : [Nil, Cons a (ConsList a)] - - foo : ConsList I64 -> ConsList I64 - foo = \t -> - when Delmin (Del t 0.0) is - Delmin (Del ry _) -> Cons 42 ry - - main = foo Nil - "# - ), - 42, - &i64, - |x: &i64| *x - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn applied_tag_function() { - assert_evals_to!( - indoc!( - r#" - x : List [Foo Str] - x = List.map ["a", "b"] Foo - - x - "# - ), - RocList::from_slice(&[RocStr::from("a"), RocStr::from("b")]), - RocList - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn applied_tag_function_result() { - assert_evals_to!( - indoc!( - r#" - x : List (Result Str *) - x = List.map ["a", "b"] Ok - - List.keepOks x (\y -> y) - "# - ), - RocList::from_slice(&[(RocStr::from("a")), (RocStr::from("b"))]), - RocList - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -#[ignore = "This test has incorrect refcounts: https://github.com/rtfeldman/roc/issues/2968"] -fn applied_tag_function_linked_list() { - assert_evals_to!( - indoc!( - r#" - ConsList a : [Nil, Cons a (ConsList a)] - - x : List (ConsList Str) - x = List.map2 ["a", "b"] [Nil, Cons "c" Nil] Cons - - when List.first x is - Ok (Cons "a" Nil) -> 1 - _ -> 0 - "# - ), - 1, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn applied_tag_function_pair() { - assert_evals_to!( - indoc!( - r#" - Pair a : [Pair a a] - - x : List (Pair Str) - x = List.map2 ["a", "b"] ["c", "d"] Pair - - when List.first x is - Ok (Pair "a" "c") -> 1 - _ -> 0 - "# - ), - 1, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -#[should_panic(expected = "")] // TODO: this only panics because it returns 0 instead of 1! -fn tag_must_be_its_own_type() { - assert_evals_to!( - indoc!( - r#" - z : [A, B, C] - z = Z - - z - "# - ), - 1, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn recursive_tag_union_into_flat_tag_union() { - // Comprehensive test for correctness in cli/tests/repl_eval - assert_evals_to!( - indoc!( - r#" - Item : [Shallow [L Str, R Str], Deep Item] - i : Item - i = Deep (Shallow (R "woo")) - i - "# - ), - 0, - usize, - |_| 0 - ) -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn monomorphized_tag() { - assert_evals_to!( - indoc!( - r#" - b = False - f : Bool, [True, False, Idk] -> U8 - f = \_, _ -> 18 - f b b - "# - ), - 18, - u8 - ) -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn monomorphized_applied_tag() { - assert_evals_to!( - indoc!( - r#" - app "test" provides [main] to "./platform" - - main = - a = A "abc" - f = \x -> - when x is - A y -> y - B y -> y - f a - "# - ), - RocStr::from("abc"), - RocStr - ) -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn monomorphized_tag_with_polymorphic_arg() { - assert_evals_to!( - indoc!( - r#" - app "test" provides [main] to "./platform" - - main = - a = A - wrap = Wrapped a - - useWrap1 : [Wrapped [A], Other] -> U8 - useWrap1 = - \w -> when w is - Wrapped A -> 2 - Other -> 3 - - useWrap2 : [Wrapped [A, B]] -> U8 - useWrap2 = - \w -> when w is - Wrapped A -> 5 - Wrapped B -> 7 - - useWrap1 wrap * useWrap2 wrap - "# - ), - 10, - u8 - ) -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn monomorphized_tag_with_polymorphic_arg_and_monomorphic_arg() { - assert_evals_to!( - indoc!( - r#" - app "test" provides [main] to "./platform" - - main = - mono : U8 - mono = 15 - poly = A - wrap = Wrapped poly mono - - useWrap1 : [Wrapped [A] U8, Other] -> U8 - useWrap1 = - \w -> when w is - Wrapped A n -> n - Other -> 0 - - useWrap2 : [Wrapped [A, B] U8] -> U8 - useWrap2 = - \w -> when w is - Wrapped A n -> n - Wrapped B _ -> 0 - - useWrap1 wrap * useWrap2 wrap - "# - ), - 225, - u8 - ) -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn issue_2365_monomorphize_tag_with_non_empty_ext_var() { - assert_evals_to!( - indoc!( - r#" - app "test" provides [main] to "./platform" - - Single a : [A, B, C]a - Compound a : Single [D, E, F]a - - single : {} -> Single * - single = \{} -> C - - compound : {} -> Compound * - compound = \{} -> single {} - - main = compound {} - "# - ), - 2, // C - u8 - ) -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn issue_2365_monomorphize_tag_with_non_empty_ext_var_wrapped() { - assert_evals_to!( - indoc!( - r#" - app "test" provides [main] to "./platform" - - Single a : [A, B, C]a - Compound a : Single [D, E, F]a - - single : {} -> Result Str (Single *) - single = \{} -> Err C - - compound : {} -> Result Str (Compound *) - compound = \{} -> - when single {} is - Ok s -> Ok s - Err e -> Err e - - main = compound {} - "# - ), - 2, // C - u8 - ) -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn issue_2365_monomorphize_tag_with_non_empty_ext_var_wrapped_nested() { - assert_evals_to!( - indoc!( - r#" - app "test" provides [main] to "./platform" - - Single a : [A, B, C]a - Compound a : Single [D, E, F]a - - main = - single : {} -> Result Str (Single *) - single = \{} -> Err C - - compound : {} -> Result Str (Compound *) - compound = \{} -> - when single {} is - Ok s -> Ok s - Err e -> Err e - - compound {} - "# - ), - 2, // C - u8 - ) -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn issue_2445() { - assert_evals_to!( - indoc!( - r#" - app "test" provides [main] to "./platform" - - none : [None, Update a] - none = None - - press : [None, Update U8] - press = none - - main = - when press is - None -> 15 - Update _ -> 25 - "# - ), - 15, - i64 - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn issue_2458() { - assert_evals_to!( - indoc!( - r#" - Foo a : [Blah (Bar a), Nothing {}] - Bar a : Foo a - - v : Bar {} - v = Blah (Blah (Nothing {})) - - when v is - Blah (Blah (Nothing {})) -> 15 - _ -> 25 - "# - ), - 15, - u8 - ) -} - -#[test] -#[ignore = "See https://github.com/rtfeldman/roc/issues/2466"] -#[cfg(any(feature = "gen-llvm"))] -fn issue_2458_deep_recursion_var() { - assert_evals_to!( - indoc!( - r#" - Foo a : [Blah (Result (Bar a) {})] - Bar a : Foo a - - v : Bar {} - - when v is - Blah (Ok (Blah (Err {}))) -> "1" - _ -> "2" - "# - ), - 15, - u8 - ) -} - -#[test] -#[cfg(any(feature = "gen-llvm"))] -fn issue_1162() { - assert_evals_to!( - indoc!( - r#" - app "test" provides [main] to "./platform" - - RBTree k : [Node k (RBTree k) (RBTree k), Empty] - - balance : a, RBTree a -> RBTree a - balance = \key, left -> - when left is - Node _ _ lRight -> - Node key lRight Empty - - _ -> - Empty - - - tree : RBTree {} - tree = - balance {} Empty - - main : U8 - main = - when tree is - Empty -> 15 - _ -> 25 - "# - ), - 15, - u8 - ) -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn polymorphic_tag() { - assert_evals_to!( - indoc!( - r#" - x : [Y U8]* - x = Y 3 - x - "# - ), - 3, // Y is a newtype, it gets unwrapped - u8 - ) -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn issue_2725_alias_polymorphic_lambda() { - assert_evals_to!( - indoc!( - r#" - wrap = \value -> Tag value - wrapIt = wrap - wrapIt 42 - "# - ), - 42, // Tag is a newtype, it gets unwrapped - i64 - ) -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn opaque_assign_to_symbol() { - assert_evals_to!( - indoc!( - r#" - app "test" provides [out] to "./platform" - - Variable := U8 - - fromUtf8 : U8 -> Result Variable [InvalidVariableUtf8] - fromUtf8 = \char -> - Ok (@Variable char) - - out = - when fromUtf8 98 is - Ok (@Variable n) -> n - _ -> 1 - "# - ), - 98, - u8 - ) -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn issue_2777_default_branch_codegen() { - assert_evals_to!( - indoc!( - r#" - f1 = \color -> - when color is - Red -> "red" - Yellow -> "yellow" - _ -> "unknown" - - r1 = Red |> f1 |> Str.concat (f1 Orange) - - f2 = \color -> - when color is - Red -> "red" - Yellow -> "yellow" - Green -> "green" - _ -> "unknown" - - r2 = Red |> f2 |> Str.concat (f2 Orange) - - f3 = \color -> - when color is - Red -> "red" - Yellow -> "yellow" - Green -> "green" - _ -> "unknown" - - r3 = Orange |> f3 |> Str.concat (f3 Red) - - f4 = \color -> - when color is - Red -> "red" - Yellow | Gold -> "yellow" - _ -> "unknown" - - r4 = Red |> f4 |> Str.concat (f4 Orange) - - [r1, r2, r3, r4] - "# - ), - RocList::from_slice(&[ - RocStr::from("redunknown"), - RocStr::from("redunknown"), - RocStr::from("unknownred"), - RocStr::from("redunknown"), - ]), - RocList - ) -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -#[should_panic(expected = "Erroneous")] -fn issue_2900_unreachable_pattern() { - assert_evals_to!( - indoc!( - r#" - foo : [Foo, Bar, Baz, Blah] -> Str - foo = \arg -> - when arg is - Foo -> "foo" - AnUnreachableTag -> "blah" - _ -> "other" - - foo Foo - "# - ), - RocStr::from("foo"), - RocStr, - |x| x, - true // ignore type errors - ) -} diff --git a/compiler/test_gen/src/helpers/dev.rs b/compiler/test_gen/src/helpers/dev.rs deleted file mode 100644 index 59f65b93e4..0000000000 --- a/compiler/test_gen/src/helpers/dev.rs +++ /dev/null @@ -1,267 +0,0 @@ -use libloading::Library; -use roc_build::link::{link, LinkType}; -use roc_builtins::bitcode; -use roc_collections::all::MutMap; -use roc_load::Threading; -use roc_region::all::LineInfo; -use tempfile::tempdir; - -#[allow(unused_imports)] -use roc_mono::ir::pretty_print_ir_symbols; - -#[allow(dead_code)] -fn promote_expr_to_module(src: &str) -> String { - let mut buffer = String::from("app \"test\" provides [main] to \"./platform\"\n\nmain =\n"); - - for line in src.lines() { - // indent the body! - buffer.push_str(" "); - buffer.push_str(line); - buffer.push('\n'); - } - - buffer -} - -#[allow(dead_code)] -pub fn helper( - arena: &bumpalo::Bump, - src: &str, - _leak: bool, - lazy_literals: bool, -) -> (String, Vec, Library) { - use std::path::{Path, PathBuf}; - - let dir = tempdir().unwrap(); - let filename = PathBuf::from("Test.roc"); - let src_dir = Path::new("fake/test/path"); - let app_o_file = dir.path().join("app.o"); - - let module_src; - let temp; - if src.starts_with("app") { - // this is already a module - module_src = src; - } else { - // this is an expression, promote it to a module - temp = promote_expr_to_module(src); - module_src = &temp; - } - - let loaded = roc_load::load_and_monomorphize_from_str( - arena, - filename, - module_src, - src_dir, - Default::default(), - roc_target::TargetInfo::default_x86_64(), - roc_reporting::report::RenderTarget::ColorTerminal, - Threading::Single, - ); - - let mut loaded = loaded.expect("failed to load module"); - - use roc_load::MonomorphizedModule; - let MonomorphizedModule { - module_id, - procedures, - mut interns, - exposed_to_host, - .. - } = loaded; - - // You can comment and uncomment this block out to get more useful information - // while you're working on the dev backend! - { - // println!("=========== Procedures =========="); - // if pretty_print_ir_symbols() { - // println!(""); - // for proc in procedures.values() { - // println!("{}", proc.to_pretty(200)); - // } - // } else { - // println!("{:?}", procedures.values()); - // } - // println!("=================================\n"); - - // println!("=========== Interns =========="); - // println!("{:?}", interns); - // println!("=================================\n"); - - // println!("=========== Exposed =========="); - // println!("{:?}", exposed_to_host); - // println!("=================================\n"); - } - - debug_assert_eq!(exposed_to_host.values.len(), 1); - let main_fn_symbol = loaded.entry_point.symbol; - let main_fn_layout = loaded.entry_point.layout; - - let mut layout_ids = roc_mono::layout::LayoutIds::default(); - let main_fn_name_base = layout_ids - .get_toplevel(main_fn_symbol, &main_fn_layout) - .to_symbol_string(main_fn_symbol, &interns); - - let main_fn_name = format!("roc_{}_exposed", main_fn_name_base); - - let mut lines = Vec::new(); - // errors whose reporting we delay (so we can see that code gen generates runtime errors) - let mut delayed_errors = Vec::new(); - - for (home, (module_path, src)) in loaded.sources { - use roc_reporting::report::{can_problem, type_problem, RocDocAllocator, DEFAULT_PALETTE}; - - let can_problems = loaded.can_problems.remove(&home).unwrap_or_default(); - let type_problems = loaded.type_problems.remove(&home).unwrap_or_default(); - - let error_count = can_problems.len() + type_problems.len(); - - if error_count == 0 { - continue; - } - - let line_info = LineInfo::new(&src); - let src_lines: Vec<&str> = src.split('\n').collect(); - let palette = DEFAULT_PALETTE; - - // Report parsing and canonicalization problems - let alloc = RocDocAllocator::new(&src_lines, home, &interns); - - use roc_problem::can::Problem::*; - for problem in can_problems.into_iter() { - // Ignore "unused" problems - match problem { - UnusedDef(_, _) | UnusedArgument(_, _, _) | UnusedImport(_, _) => { - delayed_errors.push(problem); - continue; - } - _ => { - let report = can_problem(&alloc, &line_info, module_path.clone(), problem); - let mut buf = String::new(); - - report.render_color_terminal(&mut buf, &alloc, &palette); - - lines.push(buf); - } - } - } - - for problem in type_problems { - if let Some(report) = type_problem(&alloc, &line_info, module_path.clone(), problem) { - let mut buf = String::new(); - - report.render_color_terminal(&mut buf, &alloc, &palette); - - lines.push(buf); - } - } - } - - if !lines.is_empty() { - println!("{}", lines.join("\n")); - assert_eq!(0, 1, "Mistakes were made"); - } - - let env = roc_gen_dev::Env { - arena, - module_id, - exposed_to_host: exposed_to_host.values.keys().copied().collect(), - lazy_literals, - generate_allocators: true, // Needed for testing, since we don't have a platform - }; - - let target = target_lexicon::Triple::host(); - let module_object = roc_gen_dev::build_module(&env, &mut interns, &target, procedures); - - let module_out = module_object - .write() - .expect("failed to build output object"); - std::fs::write(&app_o_file, module_out).expect("failed to write object to file"); - - // std::fs::copy(&app_o_file, "/tmp/app.o").unwrap(); - - let (mut child, dylib_path) = link( - &target, - app_o_file.clone(), - // Long term we probably want a smarter way to link in zig builtins. - // With the current method all methods are kept and it adds about 100k to all outputs. - &[ - app_o_file.to_str().unwrap(), - bitcode::BUILTINS_HOST_OBJ_PATH, - ], - LinkType::Dylib, - ) - .expect("failed to link dynamic library"); - - child.wait().unwrap(); - - // Load the dylib - let path = dylib_path.as_path().to_str().unwrap(); - - // std::fs::copy(&path, "/tmp/libapp.so").unwrap(); - - let lib = unsafe { Library::new(path) }.expect("failed to load shared library"); - - (main_fn_name, delayed_errors, lib) -} - -#[allow(unused_macros)] -macro_rules! assert_evals_to { - ($src:expr, $expected:expr, $ty:ty) => {{ - assert_evals_to!($src, $expected, $ty, (|val| val)); - }}; - ($src:expr, $expected:expr, $ty:ty, $transform:expr) => { - // Same as above, except with an additional transformation argument. - { - assert_evals_to!($src, $expected, $ty, $transform, true); - } - }; - ($src:expr, $expected:expr, $ty:ty, $transform:expr, $leak:expr) => { - // Run both with and without lazy literal optimization. - { - assert_evals_to!($src, $expected, $ty, $transform, $leak, false); - } - { - assert_evals_to!($src, $expected, $ty, $transform, $leak, true); - } - }; - ($src:expr, $expected:expr, $ty:ty, $transform:expr, $leak:expr, $lazy_literals:expr) => { - use bumpalo::Bump; - use roc_gen_dev::run_jit_function_raw; - - let arena = Bump::new(); - let (main_fn_name, errors, lib) = - $crate::helpers::dev::helper(&arena, $src, $leak, $lazy_literals); - - let transform = |success| { - let expected = $expected; - let given = $transform(success); - assert_eq!(&given, &expected); - }; - run_jit_function_raw!(lib, main_fn_name, $ty, transform, errors) - }; -} - -#[allow(unused_macros)] -macro_rules! assert_expect_failed { - ($src:expr, $expected:expr, $ty:ty, $failures:expr) => {{ - use bumpalo::Bump; - use roc_gen_dev::run_jit_function_raw; - let stdlib = roc_builtins::std::standard_stdlib(); - - let arena = Bump::new(); - let (main_fn_name, errors, lib) = - $crate::helpers::dev::helper(&arena, $src, stdlib, true, true); - - let transform = |success| { - let expected = $expected; - assert_eq!(&success, &expected); - }; - run_jit_function_raw!(lib, main_fn_name, $ty, transform, errors); - }}; -} - -#[allow(unused_imports)] -pub(crate) use assert_evals_to; -#[allow(unused_imports)] -pub(crate) use assert_expect_failed; diff --git a/compiler/test_gen/src/helpers/from_wasmer_memory.rs b/compiler/test_gen/src/helpers/from_wasmer_memory.rs deleted file mode 100644 index 489ab2d541..0000000000 --- a/compiler/test_gen/src/helpers/from_wasmer_memory.rs +++ /dev/null @@ -1,169 +0,0 @@ -use roc_gen_wasm::wasm32_sized::Wasm32Sized; -use roc_std::{RocDec, RocList, RocOrder, RocStr}; -use std::convert::TryInto; - -pub trait FromWasmerMemory: Wasm32Sized { - fn decode(memory: &wasmer::Memory, offset: u32) -> Self; -} - -macro_rules! from_wasm_memory_primitive_decode { - ($type_name:ident) => { - fn decode(memory: &wasmer::Memory, offset: u32) -> Self { - use core::mem::MaybeUninit; - - let mut output: MaybeUninit = MaybeUninit::uninit(); - let width = std::mem::size_of::(); - - let ptr = output.as_mut_ptr(); - let raw_ptr = ptr as *mut u8; - let slice = unsafe { std::slice::from_raw_parts_mut(raw_ptr, width) }; - - let memory_bytes: &[u8] = unsafe { memory.data_unchecked() }; - let index = offset as usize; - let wasm_slice = &memory_bytes[index..][..width]; - - slice.copy_from_slice(wasm_slice); - - unsafe { output.assume_init() } - } - }; -} - -macro_rules! from_wasm_memory_primitive { - ($($type_name:ident ,)+) => { - $( - impl FromWasmerMemory for $type_name { - from_wasm_memory_primitive_decode!($type_name); - } - )* - } -} - -from_wasm_memory_primitive!( - u8, i8, u16, i16, u32, i32, u64, i64, u128, i128, f32, f64, bool, RocDec, RocOrder, -); - -impl FromWasmerMemory for () { - fn decode(_: &wasmer::Memory, _: u32) -> Self {} -} - -impl FromWasmerMemory for RocStr { - fn decode(memory: &wasmer::Memory, addr: u32) -> Self { - let memory_bytes = unsafe { memory.data_unchecked() }; - let index = addr as usize; - - let mut str_bytes = [0; 12]; - str_bytes.copy_from_slice(&memory_bytes[index..][..12]); - - let str_words: &[u32; 3] = unsafe { std::mem::transmute(&str_bytes) }; - - let big_elem_ptr = str_words[0] as usize; - let big_length = str_words[1] as usize; - - let last_byte = str_bytes[11]; - let is_small_str = last_byte >= 0x80; - - let slice = if is_small_str { - let small_length = (last_byte & 0x7f) as usize; - &str_bytes[0..small_length] - } else { - &memory_bytes[big_elem_ptr..][..big_length] - }; - - unsafe { RocStr::from_slice(slice) } - } -} - -impl FromWasmerMemory for RocList { - fn decode(memory: &wasmer::Memory, offset: u32) -> Self { - let bytes = ::decode(memory, offset); - - let length = (bytes >> 32) as u32; - let elements = bytes as u32; - - let mut items = Vec::with_capacity(length as usize); - - for i in 0..length { - let item = ::decode( - memory, - elements + i * ::SIZE_OF_WASM as u32, - ); - items.push(item); - } - - RocList::from_slice(&items) - } -} - -impl FromWasmerMemory for &'_ T { - fn decode(memory: &wasmer::Memory, offset: u32) -> Self { - let elements = ::decode(memory, offset); - - let actual = ::decode(memory, elements); - - let b = Box::new(actual); - - std::boxed::Box::::leak(b) - } -} - -impl FromWasmerMemory for [T; N] { - fn decode(memory: &wasmer::Memory, offset: u32) -> Self { - let memory_bytes: &[u8] = unsafe { memory.data_unchecked() }; - let index = offset as usize; - - debug_assert!(memory_bytes.len() >= index + (N * ::SIZE_OF_WASM)); - - let slice_bytes: &[u8] = &memory_bytes[index..][..N]; - let slice: &[T] = unsafe { std::mem::transmute(slice_bytes) }; - let array: &[T; N] = slice.try_into().expect("incorrect length"); - - array.clone() - } -} - -impl FromWasmerMemory for usize { - fn decode(memory: &wasmer::Memory, offset: u32) -> Self { - ::decode(memory, offset) as usize - } -} - -impl FromWasmerMemory for (T, U) { - fn decode(memory: &wasmer::Memory, offset: u32) -> Self { - debug_assert!( - T::ALIGN_OF_WASM >= U::ALIGN_OF_WASM, - "this function does not handle alignment" - ); - - let t = ::decode(memory, offset); - - let u = ::decode(memory, offset + T::ACTUAL_WIDTH as u32); - - (t, u) - } -} - -impl FromWasmerMemory for (T, U, V) { - fn decode(memory: &wasmer::Memory, offset: u32) -> Self { - debug_assert!( - T::ALIGN_OF_WASM >= U::ALIGN_OF_WASM, - "this function does not handle alignment" - ); - - debug_assert!( - U::ALIGN_OF_WASM >= V::ALIGN_OF_WASM, - "this function does not handle alignment" - ); - - let t = ::decode(memory, offset); - - let u = ::decode(memory, offset + T::ACTUAL_WIDTH as u32); - - let v = ::decode( - memory, - offset + T::ACTUAL_WIDTH as u32 + U::ACTUAL_WIDTH as u32, - ); - - (t, u, v) - } -} diff --git a/compiler/test_gen/src/helpers/llvm.rs b/compiler/test_gen/src/helpers/llvm.rs deleted file mode 100644 index 81c437ccec..0000000000 --- a/compiler/test_gen/src/helpers/llvm.rs +++ /dev/null @@ -1,673 +0,0 @@ -use crate::helpers::from_wasmer_memory::FromWasmerMemory; -use inkwell::module::Module; -use libloading::Library; -use roc_build::link::module_to_dylib; -use roc_build::program::FunctionIterator; -use roc_collections::all::MutSet; -use roc_gen_llvm::llvm::externs::add_default_roc_externs; -use roc_load::Threading; -use roc_mono::ir::OptLevel; -use roc_region::all::LineInfo; -use roc_reporting::report::RenderTarget; -use target_lexicon::Triple; - -fn promote_expr_to_module(src: &str) -> String { - let mut buffer = String::from("app \"test\" provides [main] to \"./platform\"\n\nmain =\n"); - - for line in src.lines() { - // indent the body! - buffer.push_str(" "); - buffer.push_str(line); - buffer.push('\n'); - } - - buffer -} - -#[allow(clippy::too_many_arguments)] -fn create_llvm_module<'a>( - arena: &'a bumpalo::Bump, - src: &str, - is_gen_test: bool, - ignore_problems: bool, - context: &'a inkwell::context::Context, - target: &Triple, - opt_level: OptLevel, -) -> (&'static str, String, &'a Module<'a>) { - use std::path::{Path, PathBuf}; - - let target_info = roc_target::TargetInfo::from(target); - - let filename = PathBuf::from("Test.roc"); - let src_dir = Path::new("fake/test/path"); - - let module_src; - let temp; - if src.starts_with("app") { - // this is already a module - module_src = src; - } else { - // this is an expression, promote it to a module - temp = promote_expr_to_module(src); - module_src = &temp; - } - - let loaded = roc_load::load_and_monomorphize_from_str( - arena, - filename, - module_src, - src_dir, - Default::default(), - target_info, - RenderTarget::ColorTerminal, - Threading::Single, - ); - - let mut loaded = match loaded { - Ok(x) => x, - Err(roc_load::LoadingProblem::FormattedReport(report)) => { - println!("{}", report); - panic!(); - } - Err(e) => panic!("{:?}", e), - }; - - use roc_load::MonomorphizedModule; - let MonomorphizedModule { - procedures, - entry_point, - interns, - .. - } = loaded; - - let mut lines = Vec::new(); - // errors whose reporting we delay (so we can see that code gen generates runtime errors) - let mut delayed_errors = Vec::new(); - - for (home, (module_path, src)) in loaded.sources { - use roc_reporting::report::{can_problem, type_problem, RocDocAllocator, DEFAULT_PALETTE}; - - let can_problems = loaded.can_problems.remove(&home).unwrap_or_default(); - let type_problems = loaded.type_problems.remove(&home).unwrap_or_default(); - - let error_count = can_problems.len() + type_problems.len(); - - if error_count == 0 { - continue; - } - - let line_info = LineInfo::new(&src); - let src_lines: Vec<&str> = src.split('\n').collect(); - let palette = DEFAULT_PALETTE; - - // Report parsing and canonicalization problems - let alloc = RocDocAllocator::new(&src_lines, home, &interns); - - use roc_problem::can::Problem::*; - for problem in can_problems.into_iter() { - match problem { - // Ignore "unused" problems - UnusedDef(_, _) - | UnusedArgument(_, _, _) - | UnusedImport(_, _) - | RuntimeError(_) - | UnsupportedPattern(_, _) - | ExposedButNotDefined(_) => { - let report = can_problem(&alloc, &line_info, module_path.clone(), problem); - let mut buf = String::new(); - - report.render_color_terminal(&mut buf, &alloc, &palette); - - delayed_errors.push(buf.clone()); - lines.push(buf); - } - // We should be able to compile even when abilities are used as types - AbilityUsedAsType(..) => {} - _ => { - let report = can_problem(&alloc, &line_info, module_path.clone(), problem); - let mut buf = String::new(); - - report.render_color_terminal(&mut buf, &alloc, &palette); - - lines.push(buf); - } - } - } - - for problem in type_problems { - if let Some(report) = type_problem(&alloc, &line_info, module_path.clone(), problem) { - let mut buf = String::new(); - - report.render_color_terminal(&mut buf, &alloc, &palette); - - lines.push(buf); - } - } - } - - if !lines.is_empty() { - println!("{}", lines.join("\n")); - - // only crash at this point if there were no delayed_errors - if delayed_errors.is_empty() && !ignore_problems { - assert_eq!(0, 1, "Mistakes were made"); - } - } - - let builder = context.create_builder(); - let module = roc_gen_llvm::llvm::build::module_from_builtins(target, context, "app"); - - let module = arena.alloc(module); - let (module_pass, function_pass) = - roc_gen_llvm::llvm::build::construct_optimization_passes(module, opt_level); - - let (dibuilder, compile_unit) = roc_gen_llvm::llvm::build::Env::new_debug_info(module); - - // mark our zig-defined builtins as internal - use inkwell::attributes::{Attribute, AttributeLoc}; - use inkwell::module::Linkage; - - let kind_id = Attribute::get_named_enum_kind_id("alwaysinline"); - debug_assert!(kind_id > 0); - let attr = context.create_enum_attribute(kind_id, 1); - - for function in FunctionIterator::from_module(module) { - let name = function.get_name().to_str().unwrap(); - if name.starts_with("roc_builtins") { - if name.starts_with("roc_builtins.expect") { - function.set_linkage(Linkage::External); - } else { - function.set_linkage(Linkage::Internal); - } - } - - if name.starts_with("roc_builtins.dict") { - function.add_attribute(AttributeLoc::Function, attr); - } - - if name.starts_with("roc_builtins.list") { - function.add_attribute(AttributeLoc::Function, attr); - } - } - - // Compile and add all the Procs before adding main - let env = roc_gen_llvm::llvm::build::Env { - arena, - builder: &builder, - dibuilder: &dibuilder, - compile_unit: &compile_unit, - context, - interns, - module, - target_info, - is_gen_test, - // important! we don't want any procedures to get the C calling convention - exposed_to_host: MutSet::default(), - }; - - // strip Zig debug stuff - module.strip_debug_info(); - - // Add roc_alloc, roc_realloc, and roc_dealloc, since the repl has no - // platform to provide them. - add_default_roc_externs(&env); - - let (main_fn_name, main_fn) = roc_gen_llvm::llvm::build::build_procedures_return_main( - &env, - opt_level, - procedures, - entry_point, - ); - - env.dibuilder.finalize(); - - // strip all debug info: we don't use it at the moment and causes weird validation issues - module.strip_debug_info(); - - // Uncomment this to see the module's un-optimized LLVM instruction output: - // env.module.print_to_stderr(); - - if main_fn.verify(true) { - function_pass.run_on(&main_fn); - } else { - panic!("Main function {} failed LLVM verification in NON-OPTIMIZED build. Uncomment things nearby to see more details.", main_fn_name); - } - - module_pass.run_on(env.module); - - // Verify the module - if let Err(errors) = env.module.verify() { - panic!("Errors defining module:\n\n{}", errors.to_string()); - } - - // Uncomment this to see the module's optimized LLVM instruction output: - // env.module.print_to_stderr(); - - (main_fn_name, delayed_errors.join("\n"), env.module) -} - -#[allow(dead_code)] -#[inline(never)] -pub fn helper<'a>( - arena: &'a bumpalo::Bump, - src: &str, - is_gen_test: bool, - ignore_problems: bool, - context: &'a inkwell::context::Context, -) -> (&'static str, String, Library) { - let target = target_lexicon::Triple::host(); - - let opt_level = if cfg!(debug_assertions) { - OptLevel::Normal - } else { - OptLevel::Optimize - }; - - let (main_fn_name, delayed_errors, module) = create_llvm_module( - arena, - src, - is_gen_test, - ignore_problems, - context, - &target, - opt_level, - ); - - let lib = - module_to_dylib(module, &target, opt_level).expect("Error loading compiled dylib for test"); - - (main_fn_name, delayed_errors, lib) -} - -fn wasm32_target_tripple() -> Triple { - use target_lexicon::{Architecture, BinaryFormat}; - - let mut triple = Triple::unknown(); - - triple.architecture = Architecture::Wasm32; - triple.binary_format = BinaryFormat::Wasm; - - triple -} - -#[allow(dead_code)] -pub fn helper_wasm<'a>( - arena: &'a bumpalo::Bump, - src: &str, - _is_gen_test: bool, - ignore_problems: bool, - context: &'a inkwell::context::Context, -) -> wasmer::Instance { - let target = wasm32_target_tripple(); - - let opt_level = if cfg!(debug_assertions) { - OptLevel::Normal - } else { - OptLevel::Optimize - }; - - let is_gen_test = false; - let (_main_fn_name, _delayed_errors, llvm_module) = create_llvm_module( - arena, - src, - is_gen_test, - ignore_problems, - context, - &target, - opt_level, - ); - - use inkwell::targets::{InitializationConfig, Target, TargetTriple}; - - let dir = tempfile::tempdir().unwrap(); - let dir_path = dir.path(); - // let zig_global_cache_path = std::path::PathBuf::from("/home/folkertdev/roc/wasm/mess"); - - let test_a_path = dir_path.join("test.a"); - let test_wasm_path = dir_path.join("libmain.wasm"); - - Target::initialize_webassembly(&InitializationConfig::default()); - - let triple = TargetTriple::create("wasm32-unknown-unknown-wasm"); - - llvm_module.set_triple(&triple); - llvm_module.set_source_file_name("Test.roc"); - - let target_machine = Target::from_name("wasm32") - .unwrap() - .create_target_machine( - &triple, - "", - "", // TODO: this probably should be TargetMachine::get_host_cpu_features() to enable all features. - inkwell::OptimizationLevel::None, - inkwell::targets::RelocMode::Default, - inkwell::targets::CodeModel::Default, - ) - .unwrap(); - - let file_type = inkwell::targets::FileType::Object; - - target_machine - .write_to_file(llvm_module, file_type, &test_a_path) - .unwrap(); - - use std::process::Command; - - Command::new(&crate::helpers::zig_executable()) - .current_dir(dir_path) - .args(&[ - "wasm-ld", - "/home/folkertdev/roc/wasm/libmain.a", - "/home/folkertdev/roc/wasm/libc.a", - test_a_path.to_str().unwrap(), - "-o", - test_wasm_path.to_str().unwrap(), - "--export-dynamic", - "--allow-undefined", - "--no-entry", - ]) - .status() - .unwrap(); - - // now, do wasmer stuff - - use wasmer::{Function, Instance, Module, Store}; - - let store = Store::default(); - let module = Module::from_file(&store, &test_wasm_path).unwrap(); - - // First, we create the `WasiEnv` - use wasmer_wasi::WasiState; - let mut wasi_env = WasiState::new("hello") - // .args(&["world"]) - // .env("KEY", "Value") - .finalize() - .unwrap(); - - // Then, we get the import object related to our WASI - // and attach it to the Wasm instance. - let mut import_object = wasi_env - .import_object(&module) - .unwrap_or_else(|_| wasmer::imports!()); - - { - let mut exts = wasmer::Exports::new(); - - let main_function = Function::new_native(&store, fake_wasm_main_function); - let ext = wasmer::Extern::Function(main_function); - exts.insert("main", ext); - - let main_function = Function::new_native(&store, wasm_roc_panic); - let ext = wasmer::Extern::Function(main_function); - exts.insert("roc_panic", ext); - - import_object.register("env", exts); - } - - Instance::new(&module, &import_object).unwrap() -} - -#[allow(dead_code)] -fn wasm_roc_panic(address: u32, tag_id: u32) { - match tag_id { - 0 => { - let mut string = ""; - - MEMORY.with(|f| { - let memory = f.borrow().unwrap(); - - let memory_bytes: &[u8] = unsafe { memory.data_unchecked() }; - let index = address as usize; - let slice = &memory_bytes[index..]; - let c_ptr: *const u8 = slice.as_ptr(); - - use std::ffi::CStr; - use std::os::raw::c_char; - let slice = unsafe { CStr::from_ptr(c_ptr as *const c_char) }; - string = slice.to_str().unwrap(); - }); - - panic!("Roc failed with message: {:?}", string) - } - _ => todo!(), - } -} - -use std::cell::RefCell; - -thread_local! { - pub static MEMORY: RefCell> = RefCell::new(None); -} - -#[allow(dead_code)] -fn fake_wasm_main_function(_: u32, _: u32) -> u32 { - panic!("wasm entered the main function; this should never happen!") -} - -#[allow(dead_code)] -pub fn assert_wasm_evals_to_help(src: &str, ignore_problems: bool) -> Result -where - T: FromWasmerMemory, -{ - let arena = bumpalo::Bump::new(); - let context = inkwell::context::Context::create(); - - let is_gen_test = true; - let instance = - crate::helpers::llvm::helper_wasm(&arena, src, is_gen_test, ignore_problems, &context); - - let memory = instance.exports.get_memory("memory").unwrap(); - - crate::helpers::llvm::MEMORY.with(|f| { - *f.borrow_mut() = Some(unsafe { std::mem::transmute(memory) }); - }); - - let test_wrapper = instance.exports.get_function("test_wrapper").unwrap(); - - match test_wrapper.call(&[]) { - Err(e) => Err(format!("call to `test_wrapper`: {:?}", e)), - Ok(result) => { - let address = result[0].unwrap_i32(); - - let output = ::decode( - memory, - // skip the RocCallResult tag id - address as u32 + 8, - ); - - Ok(output) - } - } -} - -#[allow(unused_macros)] -macro_rules! assert_wasm_evals_to { - ($src:expr, $expected:expr, $ty:ty, $transform:expr, $ignore_problems:expr) => { - match $crate::helpers::llvm::assert_wasm_evals_to_help::<$ty>($src, $ignore_problems) { - Err(msg) => panic!("Wasm test failed: {:?}", msg), - Ok(actual) => { - #[allow(clippy::bool_assert_comparison)] - assert_eq!($transform(actual), $expected, "Wasm test failed") - } - } - }; - - ($src:expr, $expected:expr, $ty:ty) => { - $crate::helpers::llvm::assert_wasm_evals_to!( - $src, - $expected, - $ty, - $crate::helpers::llvm::identity, - false - ); - }; - - ($src:expr, $expected:expr, $ty:ty, $transform:expr) => { - $crate::helpers::llvm::assert_wasm_evals_to!($src, $expected, $ty, $transform, false); - }; -} - -#[allow(unused_macros)] -macro_rules! assert_llvm_evals_to { - ($src:expr, $expected:expr, $ty:ty, $transform:expr, $ignore_problems:expr) => { - use bumpalo::Bump; - use inkwell::context::Context; - use roc_gen_llvm::run_jit_function; - - let arena = Bump::new(); - let context = Context::create(); - - let is_gen_test = true; - let (main_fn_name, errors, lib) = - $crate::helpers::llvm::helper(&arena, $src, is_gen_test, $ignore_problems, &context); - - let transform = |success| { - let expected = $expected; - #[allow(clippy::redundant_closure_call)] - let given = $transform(success); - assert_eq!(&given, &expected, "LLVM test failed"); - }; - run_jit_function!(lib, main_fn_name, $ty, transform, errors) - }; - - ($src:expr, $expected:expr, $ty:ty) => { - $crate::helpers::llvm::assert_llvm_evals_to!( - $src, - $expected, - $ty, - $crate::helpers::llvm::identity, - false - ); - }; - - ($src:expr, $expected:expr, $ty:ty, $transform:expr) => { - $crate::helpers::llvm::assert_llvm_evals_to!($src, $expected, $ty, $transform, false); - }; -} - -#[allow(unused_macros)] -macro_rules! assert_evals_to { - ($src:expr, $expected:expr, $ty:ty) => {{ - assert_evals_to!($src, $expected, $ty, $crate::helpers::llvm::identity, false); - }}; - ($src:expr, $expected:expr, $ty:ty, $transform:expr) => {{ - // same as above, except with an additional transformation argument. - assert_evals_to!($src, $expected, $ty, $transform, false); - }}; - ($src:expr, $expected:expr, $ty:ty, $transform:expr, $ignore_problems: expr) => {{ - // same as above, except with ignore_problems. - #[cfg(feature = "wasm-cli-run")] - $crate::helpers::llvm::assert_wasm_evals_to!( - $src, - $expected, - $ty, - $transform, - $ignore_problems - ); - - $crate::helpers::llvm::assert_llvm_evals_to!( - $src, - $expected, - $ty, - $transform, - $ignore_problems - ); - }}; -} - -#[allow(unused_macros)] -macro_rules! assert_expect_failed { - ($src:expr, $expected:expr, $ty:ty) => { - use bumpalo::Bump; - use inkwell::context::Context; - use roc_gen_llvm::run_jit_function; - - let arena = Bump::new(); - let context = Context::create(); - - let is_gen_test = true; - let (main_fn_name, errors, lib) = - $crate::helpers::llvm::helper(&arena, $src, is_gen_test, false, &context); - - let transform = |success| { - let expected = $expected; - assert_eq!(&success, &expected, "LLVM test failed"); - }; - - run_jit_function!(lib, main_fn_name, $ty, transform, errors) - }; - - ($src:expr, $expected:expr, $ty:ty) => { - $crate::helpers::llvm::assert_llvm_evals_to!( - $src, - $expected, - $ty, - $crate::helpers::llvm::identity, - false - ); - }; - - ($src:expr, $expected:expr, $ty:ty, $transform:expr) => { - $crate::helpers::llvm::assert_llvm_evals_to!($src, $expected, $ty, $transform, false); - }; -} - -macro_rules! expect_runtime_error_panic { - ($src:expr) => {{ - #[cfg(feature = "wasm-cli-run")] - $crate::helpers::llvm::assert_wasm_evals_to!( - $src, - false, // fake value/type for eval - bool, - $crate::helpers::llvm::identity, - true // ignore problems - ); - - $crate::helpers::llvm::assert_llvm_evals_to!( - $src, - false, // fake value/type for eval - bool, - $crate::helpers::llvm::identity, - true // ignore problems - ); - }}; -} - -#[allow(dead_code)] -pub fn identity(value: T) -> T { - value -} - -#[allow(unused_macros)] -macro_rules! assert_non_opt_evals_to { - ($src:expr, $expected:expr, $ty:ty) => {{ - $crate::helpers::llvm::assert_llvm_evals_to!( - $src, - $expected, - $ty, - $crate::helpers::llvm::identity - ); - }}; - ($src:expr, $expected:expr, $ty:ty, $transform:expr) => { - // Same as above, except with an additional transformation argument. - { - $crate::helpers::llvm::assert_llvm_evals_to!($src, $expected, $ty, $transform); - } - }; - ($src:expr, $expected:expr, $ty:ty, $transform:expr) => {{ - $crate::helpers::llvm::assert_llvm_evals_to!($src, $expected, $ty, $transform); - }}; -} - -#[allow(unused_imports)] -pub(crate) use assert_evals_to; -#[allow(unused_imports)] -pub(crate) use assert_expect_failed; -#[allow(unused_imports)] -pub(crate) use assert_llvm_evals_to; -#[allow(unused_imports)] -pub(crate) use assert_non_opt_evals_to; -#[allow(unused_imports)] -pub(crate) use assert_wasm_evals_to; -#[allow(unused_imports)] -pub(crate) use expect_runtime_error_panic; diff --git a/compiler/test_gen/src/helpers/mod.rs b/compiler/test_gen/src/helpers/mod.rs deleted file mode 100644 index 7285adf7df..0000000000 --- a/compiler/test_gen/src/helpers/mod.rs +++ /dev/null @@ -1,65 +0,0 @@ -extern crate bumpalo; - -#[cfg(feature = "gen-dev")] -pub mod dev; -pub mod from_wasmer_memory; -#[cfg(feature = "gen-llvm")] -pub mod llvm; -#[cfg(feature = "gen-wasm")] -pub mod wasm; - -#[allow(dead_code)] -pub fn zig_executable() -> String { - match std::env::var("ROC_ZIG") { - Ok(path) => path, - Err(_) => "zig".into(), - } -} - -/// Used in the with_larger_debug_stack() function, for tests that otherwise -/// run out of stack space in debug builds (but don't in --release builds) -#[allow(dead_code)] -const EXPANDED_STACK_SIZE: usize = 8 * 1024 * 1024; - -/// Without this, some tests pass in `cargo test --release` but fail without -/// the --release flag because they run out of stack space. This increases -/// stack size for debug builds only, while leaving the stack space at the default -/// amount for release builds. -#[allow(dead_code)] -#[cfg(debug_assertions)] -pub fn with_larger_debug_stack(run_test: F) -where - F: FnOnce(), - F: Send, - F: 'static, -{ - std::thread::Builder::new() - .stack_size(EXPANDED_STACK_SIZE) - .spawn(run_test) - .expect("Error while spawning expanded dev stack size thread") - .join() - .expect("Error while joining expanded dev stack size thread") -} - -/// In --release builds, don't increase the stack size. Run the test normally. -/// This way, we find out if any of our tests are blowing the stack even after -/// optimizations in release builds. -#[allow(dead_code)] -#[cfg(not(debug_assertions))] -#[inline(always)] -pub fn with_larger_debug_stack(run_test: F) -where - F: FnOnce(), - F: Send, - F: 'static, -{ - run_test() -} - -#[allow(dead_code)] -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum RefCount { - Live(u32), - Deallocated, - Constant, -} diff --git a/compiler/test_gen/src/helpers/wasm.rs b/compiler/test_gen/src/helpers/wasm.rs deleted file mode 100644 index 110924ccfc..0000000000 --- a/compiler/test_gen/src/helpers/wasm.rs +++ /dev/null @@ -1,426 +0,0 @@ -use super::RefCount; -use crate::helpers::from_wasmer_memory::FromWasmerMemory; -use roc_collections::all::MutSet; -use roc_gen_wasm::wasm32_result::Wasm32Result; -use roc_gen_wasm::wasm_module::{Export, ExportType}; -use roc_gen_wasm::{DEBUG_LOG_SETTINGS, MEMORY_NAME}; -use roc_load::Threading; -use std::collections::hash_map::DefaultHasher; -use std::hash::{Hash, Hasher}; -use std::marker::PhantomData; -use std::path::{Path, PathBuf}; -use wasmer::{Memory, WasmPtr}; - -// Should manually match build.rs -const PLATFORM_FILENAME: &str = "wasm_test_platform"; -const OUT_DIR_VAR: &str = "TEST_GEN_OUT"; - -const TEST_WRAPPER_NAME: &str = "test_wrapper"; -const INIT_REFCOUNT_NAME: &str = "init_refcount_test"; -const PANIC_MSG_NAME: &str = "panic_msg"; - -fn promote_expr_to_module(src: &str) -> String { - let mut buffer = String::from("app \"test\" provides [main] to \"./platform\"\n\nmain =\n"); - - for line in src.lines() { - // indent the body! - buffer.push_str(" "); - buffer.push_str(line); - buffer.push('\n'); - } - - buffer -} - -#[allow(dead_code)] -pub fn compile_and_load<'a, T: Wasm32Result>( - arena: &'a bumpalo::Bump, - src: &str, - test_wrapper_type_info: PhantomData, -) -> wasmer::Instance { - let platform_path = get_preprocessed_host_path(); - let platform_bytes = std::fs::read(&platform_path).unwrap(); - println!("Loading test host {}", platform_path.display()); - - let compiled_bytes = - compile_roc_to_wasm_bytes(arena, &platform_bytes, src, test_wrapper_type_info); - - if DEBUG_LOG_SETTINGS.keep_test_binary { - let build_dir_hash = src_hash(src); - save_wasm_file(&compiled_bytes, build_dir_hash) - }; - - load_bytes_into_runtime(compiled_bytes) -} - -fn get_preprocessed_host_path() -> PathBuf { - let out_dir = std::env::var(OUT_DIR_VAR).unwrap(); - Path::new(&out_dir) - .join([PLATFORM_FILENAME, "o"].join(".")) - .to_path_buf() -} - -fn src_hash(src: &str) -> u64 { - let mut hash_state = DefaultHasher::new(); - src.hash(&mut hash_state); - hash_state.finish() -} - -fn compile_roc_to_wasm_bytes<'a, T: Wasm32Result>( - arena: &'a bumpalo::Bump, - host_bytes: &[u8], - src: &str, - _test_wrapper_type_info: PhantomData, -) -> Vec { - let filename = PathBuf::from("Test.roc"); - let src_dir = Path::new("fake/test/path"); - - let module_src; - let temp; - if src.starts_with("app") { - // this is already a module - module_src = src; - } else { - // this is an expression, promote it to a module - temp = promote_expr_to_module(src); - module_src = &temp; - } - - let loaded = roc_load::load_and_monomorphize_from_str( - arena, - filename, - module_src, - src_dir, - Default::default(), - roc_target::TargetInfo::default_wasm32(), - roc_reporting::report::RenderTarget::ColorTerminal, - Threading::Single, - ); - - let loaded = loaded.expect("failed to load module"); - - use roc_load::MonomorphizedModule; - let MonomorphizedModule { - module_id, - procedures, - mut interns, - exposed_to_host, - .. - } = loaded; - - debug_assert_eq!(exposed_to_host.values.len(), 1); - - let exposed_to_host = exposed_to_host - .values - .keys() - .copied() - .collect::>(); - - let env = roc_gen_wasm::Env { - arena, - module_id, - exposed_to_host, - }; - - let host_module = roc_gen_wasm::parse_host(env.arena, host_bytes).unwrap_or_else(|e| { - panic!( - "I ran into a problem with the host object file, {} at offset 0x{:x}:\n{}", - get_preprocessed_host_path().display(), - e.offset, - e.message - ) - }); - - let (mut module, called_preload_fns, main_fn_index) = - roc_gen_wasm::build_app_module(&env, &mut interns, host_module, procedures); - - T::insert_wrapper(arena, &mut module, TEST_WRAPPER_NAME, main_fn_index); - - // Export the initialiser function for refcount tests - let init_refcount_idx = module - .names - .function_names - .iter() - .filter(|(_, name)| *name == INIT_REFCOUNT_NAME) - .map(|(i, _)| *i) - .next() - .unwrap(); - module.export.append(Export { - name: INIT_REFCOUNT_NAME, - ty: ExportType::Func, - index: init_refcount_idx, - }); - - module.remove_dead_preloads(env.arena, called_preload_fns); - - let mut app_module_bytes = std::vec::Vec::with_capacity(module.size()); - module.serialize(&mut app_module_bytes); - - app_module_bytes -} - -fn save_wasm_file(app_module_bytes: &[u8], build_dir_hash: u64) { - let debug_dir_str = format!("/tmp/roc/gen_wasm/{:016x}", build_dir_hash); - let debug_dir_path = Path::new(&debug_dir_str); - let final_wasm_file = debug_dir_path.join("final.wasm"); - - std::fs::create_dir_all(debug_dir_path).unwrap(); - std::fs::write(&final_wasm_file, app_module_bytes).unwrap(); - - println!( - "Debug command:\n\twasm-objdump -dx {}", - final_wasm_file.to_str().unwrap() - ); -} - -fn load_bytes_into_runtime(bytes: Vec) -> wasmer::Instance { - use wasmer::{Module, Store}; - use wasmer_wasi::WasiState; - - let store = Store::default(); - let wasmer_module = Module::new(&store, &bytes).unwrap(); - - // First, we create the `WasiEnv` - let mut wasi_env = WasiState::new("hello").finalize().unwrap(); - - // Then, we get the import object related to our WASI - // and attach it to the Wasm instance. - let import_object = wasi_env - .import_object(&wasmer_module) - .unwrap_or_else(|_| wasmer::imports!()); - - wasmer::Instance::new(&wasmer_module, &import_object).unwrap() -} - -#[allow(dead_code)] -pub fn assert_evals_to_help(src: &str, phantom: PhantomData) -> Result -where - T: FromWasmerMemory + Wasm32Result, -{ - let arena = bumpalo::Bump::new(); - - let instance = crate::helpers::wasm::compile_and_load(&arena, src, phantom); - - let memory = instance.exports.get_memory(MEMORY_NAME).unwrap(); - - let test_wrapper = instance.exports.get_function(TEST_WRAPPER_NAME).unwrap(); - - match test_wrapper.call(&[]) { - Err(e) => { - if let Some(msg) = get_roc_panic_msg(&instance, memory) { - Err(msg) - } else { - Err(e.to_string()) - } - } - Ok(result) => { - let address = result[0].unwrap_i32(); - - if false { - println!("test_wrapper returned 0x{:x}", address); - println!("Stack:"); - crate::helpers::wasm::debug_memory_hex(memory, address, std::mem::size_of::()); - } - if false { - println!("Heap:"); - // Manually provide address and size based on printf in wasm_test_platform.c - crate::helpers::wasm::debug_memory_hex(memory, 0x11440, 24); - } - let output = ::decode(memory, address as u32); - - Ok(output) - } - } -} - -/// Our test roc_panic stores a pointer to its message in a global variable so we can find it. -fn get_roc_panic_msg(instance: &wasmer::Instance, memory: &Memory) -> Option { - let memory_bytes = unsafe { memory.data_unchecked() }; - - // We need to dereference twice! - // The Wasm Global only points at the memory location of the C global value - let panic_msg_global = instance.exports.get_global(PANIC_MSG_NAME).unwrap(); - let global_addr = panic_msg_global.get().unwrap_i32() as usize; - let global_ptr = memory_bytes[global_addr..].as_ptr() as *const u32; - - // Dereference again to find the bytes of the message string - let msg_addr = unsafe { *global_ptr }; - if msg_addr == 0 { - return None; - } - let msg_index = msg_addr as usize; - let msg_len = memory_bytes[msg_index..] - .iter() - .position(|c| *c == 0) - .unwrap(); - let msg_bytes = memory_bytes[msg_index..][..msg_len].to_vec(); - let msg = unsafe { String::from_utf8_unchecked(msg_bytes) }; - Some(msg) -} - -#[allow(dead_code)] -pub fn assert_wasm_refcounts_help( - src: &str, - phantom: PhantomData, - num_refcounts: usize, -) -> Result, String> -where - T: FromWasmerMemory + Wasm32Result, -{ - let arena = bumpalo::Bump::new(); - - let instance = crate::helpers::wasm::compile_and_load(&arena, src, phantom); - - let memory = instance.exports.get_memory(MEMORY_NAME).unwrap(); - - let expected_len = num_refcounts as i32; - let init_refcount_test = instance.exports.get_function(INIT_REFCOUNT_NAME).unwrap(); - let init_result = init_refcount_test.call(&[wasmer::Value::I32(expected_len)]); - let refcount_vector_addr = match init_result { - Err(e) => return Err(format!("{:?}", e)), - Ok(result) => result[0].unwrap_i32(), - }; - - // Run the test - let test_wrapper = instance.exports.get_function(TEST_WRAPPER_NAME).unwrap(); - match test_wrapper.call(&[]) { - Err(e) => return Err(format!("{:?}", e)), - Ok(_) => {} - } - - // Check we got the right number of refcounts - let refcount_vector_len: WasmPtr = WasmPtr::new(refcount_vector_addr as u32); - let actual_len = refcount_vector_len.deref(memory).unwrap().get(); - if actual_len != expected_len { - panic!("Expected {} refcounts but got {}", expected_len, actual_len); - } - - // Read the actual refcount values - let refcount_ptr_array: WasmPtr, wasmer::Array> = - WasmPtr::new(4 + refcount_vector_addr as u32); - let refcount_ptrs = refcount_ptr_array - .deref(memory, 0, num_refcounts as u32) - .unwrap(); - - let mut refcounts = Vec::with_capacity(num_refcounts); - for i in 0..num_refcounts { - let rc_ptr = refcount_ptrs[i].get(); - let rc = if rc_ptr.offset() == 0 { - RefCount::Deallocated - } else { - let rc_encoded: i32 = rc_ptr.deref(memory).unwrap().get(); - if rc_encoded == 0 { - RefCount::Constant - } else { - let rc = rc_encoded - i32::MIN + 1; - RefCount::Live(rc as u32) - } - }; - refcounts.push(rc); - } - Ok(refcounts) -} - -/// Print out hex bytes of the test result, and a few words on either side -/// Can be handy for debugging misalignment issues etc. -pub fn debug_memory_hex(memory: &Memory, address: i32, size: usize) { - let memory_words: &[u32] = unsafe { - let memory_bytes = memory.data_unchecked(); - std::mem::transmute(memory_bytes) - }; - - let extra_words = 2; - let result_start = (address as usize) / 4; - let result_end = result_start + ((size + 3) / 4); - let start = result_start - extra_words; - let end = result_end + extra_words; - - for index in start..end { - let result_marker = if index >= result_start && index < result_end { - "|" - } else { - " " - }; - println!( - "{:x} {} {:08x}", - index * 4, - result_marker, - memory_words[index] - ); - } - println!(); -} - -#[allow(unused_macros)] -macro_rules! assert_evals_to { - ($src:expr, $expected:expr, $ty:ty) => { - $crate::helpers::wasm::assert_evals_to!( - $src, - $expected, - $ty, - $crate::helpers::wasm::identity, - false - ) - }; - - ($src:expr, $expected:expr, $ty:ty, $transform:expr) => { - $crate::helpers::wasm::assert_evals_to!($src, $expected, $ty, $transform, false); - }; - - ($src:expr, $expected:expr, $ty:ty, $transform:expr, $ignore_problems: expr) => {{ - let phantom = std::marker::PhantomData; - let _ = $ignore_problems; // Always ignore "problems"! One backend (LLVM) is enough to cover them. - match $crate::helpers::wasm::assert_evals_to_help::<$ty>($src, phantom) { - Err(msg) => panic!("{}", msg), - Ok(actual) => { - assert_eq!($transform(actual), $expected) - } - } - }}; -} - -#[allow(unused_macros)] -macro_rules! expect_runtime_error_panic { - ($src:expr) => {{ - $crate::helpers::wasm::assert_evals_to!( - $src, - false, // fake value/type for eval - bool, - $crate::helpers::wasm::identity, - true // ignore problems - ); - }}; -} - -#[allow(dead_code)] -pub fn identity(value: T) -> T { - value -} - -#[allow(unused_macros)] -macro_rules! assert_refcounts { - // We need the result type to generate the test_wrapper, even though we ignore the value! - // We can't just call `main` with no args, because some tests return structs, via pointer arg! - // Also we need to know how much stack space to reserve for the struct. - ($src: expr, $ty: ty, $expected_refcounts: expr) => {{ - let phantom = std::marker::PhantomData; - let num_refcounts = $expected_refcounts.len(); - let result = - $crate::helpers::wasm::assert_wasm_refcounts_help::<$ty>($src, phantom, num_refcounts); - match result { - Err(msg) => panic!("{:?}", msg), - Ok(actual_refcounts) => { - assert_eq!(&actual_refcounts, $expected_refcounts) - } - } - }}; -} - -#[allow(unused_imports)] -pub(crate) use assert_evals_to; - -#[allow(unused_imports)] -pub(crate) use expect_runtime_error_panic; - -#[allow(unused_imports)] -pub(crate) use assert_refcounts; diff --git a/compiler/test_gen/src/tests.rs b/compiler/test_gen/src/tests.rs deleted file mode 100644 index b5ab6f3633..0000000000 --- a/compiler/test_gen/src/tests.rs +++ /dev/null @@ -1,75 +0,0 @@ -#![warn(clippy::dbg_macro)] -// See github.com/rtfeldman/roc/issues/800 for discussion of the large_enum_variant check. -#![allow(clippy::large_enum_variant)] -// we actually want to compare against the literal float bits -#![allow(clippy::float_cmp)] - -pub mod gen_abilities; -pub mod gen_compare; -pub mod gen_dict; -pub mod gen_list; -pub mod gen_num; -pub mod gen_primitives; -pub mod gen_records; -pub mod gen_refcount; -pub mod gen_result; -pub mod gen_set; -pub mod gen_str; -pub mod gen_tags; -mod helpers; -pub mod wasm_str; - -use core::ffi::c_void; - -/// # Safety -/// The Roc application needs this. -#[no_mangle] -pub unsafe fn roc_alloc(size: usize, _alignment: u32) -> *mut c_void { - libc::malloc(size) -} - -/// # Safety -/// The Roc application needs this. -#[no_mangle] -pub unsafe fn roc_memcpy(dest: *mut c_void, src: *const c_void, bytes: usize) -> *mut c_void { - libc::memcpy(dest, src, bytes) -} - -/// # Safety -/// The Roc application needs this. -#[no_mangle] -pub unsafe 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 -/// The Roc application needs this. -#[no_mangle] -pub unsafe fn roc_dealloc(c_ptr: *mut c_void, _alignment: u32) { - libc::free(c_ptr) -} - -/// # Safety -/// The Roc application needs this. -#[no_mangle] -pub unsafe fn roc_panic(c_ptr: *mut c_void, tag_id: u32) { - use roc_gen_llvm::llvm::build::PanicTagId; - - use std::ffi::CStr; - use std::os::raw::c_char; - - match PanicTagId::try_from(tag_id) { - Ok(PanicTagId::NullTerminatedString) => { - let slice = CStr::from_ptr(c_ptr as *const c_char); - let string = slice.to_str().unwrap(); - eprintln!("Roc hit a panic: {}", string); - std::process::exit(1); - } - Err(_) => unreachable!(), - } -} diff --git a/compiler/test_gen/src/wasm_str.rs b/compiler/test_gen/src/wasm_str.rs deleted file mode 100644 index 18466ec2a4..0000000000 --- a/compiler/test_gen/src/wasm_str.rs +++ /dev/null @@ -1,1348 +0,0 @@ -// Wasm pointers are only 32bit. This effects RocStr. -// These are versions of the str tests assuming 32bit pointers. -#![cfg(feature = "gen-wasm")] - -// TODO: We need to make these tests work with the llvm wasm backend. - -// #[cfg(feature = "gen-llvm")] -// use crate::helpers::llvm::assert_wasm_evals_to as assert_evals_to; - -#[cfg(feature = "gen-wasm")] -use crate::helpers::wasm::assert_evals_to; - -#[allow(unused_imports)] -use indoc::indoc; -use roc_std::{RocList, RocStr}; - -// #[test] -// fn str_split_empty_delimiter() { -// assert_evals_to!( -// indoc!( -// r#" -// List.len (Str.split "hello" "") -// "# -// ), -// 1, -// i64 -// ); - -// assert_evals_to!( -// indoc!( -// r#" -// when List.first (Str.split "JJJ" "") is -// Ok str -> -// Str.countGraphemes str - -// _ -> -// -1 - -// "# -// ), -// 3, -// i64 -// ); -// } - -// #[test] -// fn str_split_bigger_delimiter_small_str() { -// assert_evals_to!( -// indoc!( -// r#" -// List.len (Str.split "hello" "JJJJ there") -// "# -// ), -// 1, -// i64 -// ); - -// assert_evals_to!( -// indoc!( -// r#" -// when List.first (Str.split "JJJ" "JJJJ there") is -// Ok str -> -// Str.countGraphemes str - -// _ -> -// -1 - -// "# -// ), -// 3, -// i64 -// ); -// } - -// #[test] -// fn str_split_str_concat_repeated() { -// assert_evals_to!( -// indoc!( -// r#" -// when List.first (Str.split "JJJJJ" "JJJJ there") is -// Ok str -> -// str -// |> Str.concat str -// |> Str.concat str -// |> Str.concat str -// |> Str.concat str - -// _ -> -// "Not Str!" - -// "# -// ), -// RocStr::from_slice(b"JJJJJJJJJJJJJJJJJJJJJJJJJ"), -// RocStr -// ); -// } - -// #[test] -// fn str_split_small_str_bigger_delimiter() { -// assert_evals_to!( -// indoc!( -// r#" -// when -// List.first -// (Str.split "JJJ" "0123456789abcdefghi") -// is -// Ok str -> str -// _ -> "" -// "# -// ), -// RocStr::from_slice(b"JJJ"), -// RocStr -// ); -// } - -// #[test] -// fn str_split_big_str_small_delimiter() { -// assert_evals_to!( -// indoc!( -// r#" -// Str.split "01234567789abcdefghi?01234567789abcdefghi" "?" -// "# -// ), -// RocList::from_slice(&[ -// RocStr::from_slice(b"01234567789abcdefghi"), -// RocStr::from_slice(b"01234567789abcdefghi") -// ]), -// RocList -// ); - -// assert_evals_to!( -// indoc!( -// r#" -// Str.split "01234567789abcdefghi 3ch 01234567789abcdefghi" "3ch" -// "# -// ), -// RocList::from_slice(&[ -// RocStr::from_slice(b"01234567789abcdefghi "), -// RocStr::from_slice(b" 01234567789abcdefghi") -// ]), -// RocList -// ); -// } - -// #[test] -// fn str_split_small_str_small_delimiter() { -// assert_evals_to!( -// indoc!( -// r#" -// Str.split "J!J!J" "!" -// "# -// ), -// RocList::from_slice(&[ -// RocStr::from_slice(b"J"), -// RocStr::from_slice(b"J"), -// RocStr::from_slice(b"J") -// ]), -// RocList -// ); -// } - -// #[test] -// fn str_split_bigger_delimiter_big_strs() { -// assert_evals_to!( -// indoc!( -// r#" -// Str.split -// "string to split is shorter" -// "than the delimiter which happens to be very very long" -// "# -// ), -// RocList::from_slice(&[RocStr::from_slice(b"string to split is shorter")]), -// RocList -// ); -// } - -// #[test] -// fn str_split_empty_strs() { -// assert_evals_to!( -// indoc!( -// r#" -// Str.split "" "" -// "# -// ), -// RocList::from_slice(&[RocStr::from_slice(b"")]), -// RocList -// ); -// } - -// #[test] -// fn str_split_minimal_example() { -// assert_evals_to!( -// indoc!( -// r#" -// Str.split "a," "," -// "# -// ), -// RocList::from_slice(&[RocStr::from_slice(b"a"), RocStr::from_slice(b"")]), -// RocList -// ) -// } - -// #[test] -// fn str_split_small_str_big_delimiter() { -// assert_evals_to!( -// indoc!( -// r#" -// Str.split -// "1---- ---- ---- ---- ----2---- ---- ---- ---- ----" -// "---- ---- ---- ---- ----" -// |> List.len -// "# -// ), -// 3, -// i64 -// ); - -// assert_evals_to!( -// indoc!( -// r#" -// Str.split -// "1---- ---- ---- ---- ----2---- ---- ---- ---- ----" -// "---- ---- ---- ---- ----" -// "# -// ), -// RocList::from_slice(&[ -// RocStr::from_slice(b"1"), -// RocStr::from_slice(b"2"), -// RocStr::from_slice(b"") -// ]), -// RocList -// ); -// } - -// #[test] -// fn str_split_small_str_20_char_delimiter() { -// assert_evals_to!( -// indoc!( -// r#" -// Str.split -// "3|-- -- -- -- -- -- |4|-- -- -- -- -- -- |" -// "|-- -- -- -- -- -- |" -// "# -// ), -// RocList::from_slice(&[ -// RocStr::from_slice(b"3"), -// RocStr::from_slice(b"4"), -// RocStr::from_slice(b"") -// ]), -// RocList -// ); -// } - -// #[test] -// fn str_concat_big_to_big() { -// assert_evals_to!( -// indoc!( -// r#" -// Str.concat -// "First string that is fairly long. Longer strings make for different errors. " -// "Second string that is also fairly long. Two long strings test things that might not appear with short strings." -// "# -// ), -// RocStr::from_slice(b"First string that is fairly long. Longer strings make for different errors. Second string that is also fairly long. Two long strings test things that might not appear with short strings."), -// RocStr -// ); -// } - -#[test] -#[cfg(any(feature = "gen-wasm"))] -fn small_str_literal() { - assert_evals_to!( - "\"01234567890\"", - [0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x30, 0x8b], - [u8; 12] - ); -} - -#[test] -#[cfg(any(feature = "gen-wasm"))] -fn small_str_zeroed_literal() { - // Verifies that we zero out unused bytes in the string. - // This is important so that string equality tests don't randomly - // fail due to unused memory being there! - // Note: Ensure the memory is non-zero beforehand, or it's not a real test! - // (It's trickier than it sounds!) - assert_evals_to!( - indoc!( - r#" - app "test" provides [main] to "./platform" - - createStr = \isForRealThisTime -> - if isForRealThisTime then - "J" - else - "xxxxxxx" - - functionWithReusedSpace = \isForRealThisTime -> - # Different string value on each call, at the same memory location - # (Can't inline createStr without refcounting, which isn't implemented) - reusedSpace = createStr isForRealThisTime - - # Unoptimised 'if' ensures that we don't just allocate in the caller's frame - if True then - reusedSpace - else - reusedSpace - - main = - garbage = functionWithReusedSpace False - functionWithReusedSpace True - "# - ), - [ - 0x4a, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0b1000_0001 - ], - [u8; 12] - ); -} - -#[test] -fn long_str_literal() { - assert_evals_to!( - "\"0123456789 123456789 123456789\"", - RocStr::from("0123456789 123456789 123456789"), - RocStr - ); -} - -#[test] -fn small_str_concat_empty_first_arg() { - assert_evals_to!( - r#"Str.concat "" "01234567890""#, - [0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x30, 0x8b], - [u8; 12] - ); -} - -#[test] -fn small_str_concat_empty_second_arg() { - assert_evals_to!( - r#"Str.concat "01234567890" """#, - [0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x30, 0x8b], - [u8; 12] - ); -} - -#[test] -fn small_str_concat_small_to_big() { - assert_evals_to!( - r#"Str.concat "abc" " this is longer than 7 chars""#, - RocStr::from("abc this is longer than 7 chars"), - RocStr - ); -} - -#[test] -fn small_str_concat_small_to_small_staying_small() { - assert_evals_to!( - r#"Str.concat "0" "1234567890""#, - [0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x30, 0x8b], - [u8; 12] - ); -} - -#[test] -fn small_str_concat_small_to_small_overflow_to_big() { - assert_evals_to!( - r#"Str.concat "abcdefg" "hijklmn""#, - RocStr::from("abcdefghijklmn"), - RocStr - ); -} - -#[test] -fn str_concat_empty() { - assert_evals_to!(r#"Str.concat "" """#, RocStr::default(), RocStr); -} - -#[test] -fn small_str_is_empty() { - assert_evals_to!(r#"Str.isEmpty "abc""#, false, bool); -} - -#[test] -fn big_str_is_empty() { - assert_evals_to!( - r#"Str.isEmpty "this is more than 15 chars long""#, - false, - bool - ); -} - -#[test] -fn empty_str_is_empty() { - assert_evals_to!(r#"Str.isEmpty """#, true, bool); -} - -#[test] -fn str_starts_with() { - assert_evals_to!(r#"Str.startsWith "hello world" "hell""#, true, bool); - assert_evals_to!(r#"Str.startsWith "hello world" """#, true, bool); - assert_evals_to!(r#"Str.startsWith "nope" "hello world""#, false, bool); - assert_evals_to!(r#"Str.startsWith "hell" "hello world""#, false, bool); - assert_evals_to!(r#"Str.startsWith "" "hello world""#, false, bool); -} - -#[test] -fn str_starts_with_code_point() { - assert_evals_to!( - &format!(r#"Str.startsWithCodePt "foobar" {}"#, 'f' as u32), - true, - bool - ); - assert_evals_to!( - &format!(r#"Str.startsWithCodePt "zoobar" {}"#, 'f' as u32), - false, - bool - ); -} - -#[test] -fn str_ends_with() { - assert_evals_to!(r#"Str.endsWith "hello world" "world""#, true, bool); - assert_evals_to!(r#"Str.endsWith "nope" "hello world""#, false, bool); - assert_evals_to!(r#"Str.endsWith "" "hello world""#, false, bool); -} - -#[test] -fn str_count_graphemes_small_str() { - assert_evals_to!(r#"Str.countGraphemes "å🤔""#, 2, usize); -} - -#[test] -fn str_count_graphemes_three_js() { - assert_evals_to!(r#"Str.countGraphemes "JJJ""#, 3, usize); -} - -#[test] -fn str_count_graphemes_big_str() { - assert_evals_to!( - r#"Str.countGraphemes "6🤔å🤔e¥🤔çppkd🙃1jdal🦯asdfa∆ltråø˚waia8918.,🏅jjc""#, - 45, - usize - ); -} - -#[test] -fn str_starts_with_same_big_str() { - assert_evals_to!( - r#"Str.startsWith "123456789123456789" "123456789123456789""#, - true, - bool - ); -} - -#[test] -fn str_starts_with_different_big_str() { - assert_evals_to!( - r#"Str.startsWith "12345678912345678910" "123456789123456789""#, - true, - bool - ); -} - -#[test] -fn str_starts_with_same_small_str() { - assert_evals_to!(r#"Str.startsWith "1234" "1234""#, true, bool); -} - -#[test] -fn str_starts_with_different_small_str() { - assert_evals_to!(r#"Str.startsWith "1234" "12""#, true, bool); -} -#[test] -fn str_starts_with_false_small_str() { - assert_evals_to!(r#"Str.startsWith "1234" "23""#, false, bool); -} - -// #[test] -// fn str_from_utf8_pass_single_ascii() { -// assert_evals_to!( -// indoc!( -// r#" -// when Str.fromUtf8 [97] is -// Ok val -> val -// Err _ -> "" -// "# -// ), -// roc_std::RocStr::from_slice("a".as_bytes()), -// roc_std::RocStr -// ); -// } - -// #[test] -// fn str_from_utf8_pass_many_ascii() { -// assert_evals_to!( -// indoc!( -// r#" -// when Str.fromUtf8 [97, 98, 99, 0x7E] is -// Ok val -> val -// Err _ -> "" -// "# -// ), -// roc_std::RocStr::from_slice("abc~".as_bytes()), -// roc_std::RocStr -// ); -// } - -// #[test] -// fn str_from_utf8_pass_single_unicode() { -// assert_evals_to!( -// indoc!( -// r#" -// when Str.fromUtf8 [0xE2, 0x88, 0x86] is -// Ok val -> val -// Err _ -> "" -// "# -// ), -// roc_std::RocStr::from_slice("∆".as_bytes()), -// roc_std::RocStr -// ); -// } - -// #[test] -// fn str_from_utf8_pass_many_unicode() { -// assert_evals_to!( -// indoc!( -// r#" -// when Str.fromUtf8 [0xE2, 0x88, 0x86, 0xC5, 0x93, 0xC2, 0xAC] is -// Ok val -> val -// Err _ -> "" -// "# -// ), -// roc_std::RocStr::from_slice("∆œ¬".as_bytes()), -// roc_std::RocStr -// ); -// } - -// #[test] -// fn str_from_utf8_pass_single_grapheme() { -// assert_evals_to!( -// indoc!( -// r#" -// when Str.fromUtf8 [0xF0, 0x9F, 0x92, 0x96] is -// Ok val -> val -// Err _ -> "" -// "# -// ), -// roc_std::RocStr::from_slice("💖".as_bytes()), -// roc_std::RocStr -// ); -// } - -// #[test] -// fn str_from_utf8_pass_many_grapheme() { -// assert_evals_to!( -// indoc!( -// r#" -// when Str.fromUtf8 [0xF0, 0x9F, 0x92, 0x96, 0xF0, 0x9F, 0xA4, 0xA0, 0xF0, 0x9F, 0x9A, 0x80] is -// Ok val -> val -// Err _ -> "" -// "# -// ), -// roc_std::RocStr::from_slice("💖🤠🚀".as_bytes()), -// roc_std::RocStr -// ); -// } - -// #[test] -// fn str_from_utf8_pass_all() { -// assert_evals_to!( -// indoc!( -// r#" -// when Str.fromUtf8 [0xF0, 0x9F, 0x92, 0x96, 98, 0xE2, 0x88, 0x86] is -// Ok val -> val -// Err _ -> "" -// "# -// ), -// roc_std::RocStr::from_slice("💖b∆".as_bytes()), -// roc_std::RocStr -// ); -// } - -// #[test] -// fn str_from_utf8_fail_invalid_start_byte() { -// assert_evals_to!( -// indoc!( -// r#" -// when Str.fromUtf8 [97, 98, 0x80, 99] is -// Err (BadUtf8 InvalidStartByte byteIndex) -> -// if byteIndex == 2 then -// "a" -// else -// "b" -// _ -> "" -// "# -// ), -// roc_std::RocStr::from_slice("a".as_bytes()), -// roc_std::RocStr -// ); -// } - -// #[test] -// fn str_from_utf8_fail_unexpected_end_of_sequence() { -// assert_evals_to!( -// indoc!( -// r#" -// when Str.fromUtf8 [97, 98, 99, 0xC2] is -// Err (BadUtf8 UnexpectedEndOfSequence byteIndex) -> -// if byteIndex == 3 then -// "a" -// else -// "b" -// _ -> "" -// "# -// ), -// roc_std::RocStr::from_slice("a".as_bytes()), -// roc_std::RocStr -// ); -// } - -// #[test] -// fn str_from_utf8_fail_expected_continuation() { -// assert_evals_to!( -// indoc!( -// r#" -// when Str.fromUtf8 [97, 98, 99, 0xC2, 0x00] is -// Err (BadUtf8 ExpectedContinuation byteIndex) -> -// if byteIndex == 3 then -// "a" -// else -// "b" -// _ -> "" -// "# -// ), -// roc_std::RocStr::from_slice("a".as_bytes()), -// roc_std::RocStr -// ); -// } - -// #[test] -// fn str_from_utf8_fail_overlong_encoding() { -// assert_evals_to!( -// indoc!( -// r#" -// when Str.fromUtf8 [97, 0xF0, 0x80, 0x80, 0x80] is -// Err (BadUtf8 OverlongEncoding byteIndex) -> -// if byteIndex == 1 then -// "a" -// else -// "b" -// _ -> "" -// "# -// ), -// roc_std::RocStr::from_slice("a".as_bytes()), -// roc_std::RocStr -// ); -// } - -// #[test] -// fn str_from_utf8_fail_codepoint_too_large() { -// assert_evals_to!( -// indoc!( -// r#" -// when Str.fromUtf8 [97, 0xF4, 0x90, 0x80, 0x80] is -// Err (BadUtf8 CodepointTooLarge byteIndex) -> -// if byteIndex == 1 then -// "a" -// else -// "b" -// _ -> "" -// "# -// ), -// roc_std::RocStr::from_slice("a".as_bytes()), -// roc_std::RocStr -// ); -// } - -// #[test] -// fn str_from_utf8_fail_surrogate_half() { -// assert_evals_to!( -// indoc!( -// r#" -// when Str.fromUtf8 [97, 98, 0xED, 0xA0, 0x80] is -// Err (BadUtf8 EncodesSurrogateHalf byteIndex) -> -// if byteIndex == 2 then -// "a" -// else -// "b" -// _ -> "" -// "# -// ), -// roc_std::RocStr::from_slice("a".as_bytes()), -// roc_std::RocStr -// ); -// } - -#[test] -fn str_equality() { - assert_evals_to!(r#""a" == "a""#, true, bool); - assert_evals_to!( - r#""loremipsumdolarsitamet" == "loremipsumdolarsitamet""#, - true, - bool - ); - assert_evals_to!(r#""a" != "b""#, true, bool); - assert_evals_to!(r#""a" == "b""#, false, bool); -} - -// #[test] -// fn nested_recursive_literal() { -// assert_evals_to!( -// indoc!( -// r#" -// Expr : [Add Expr Expr, Val I64, Var I64] - -// expr : Expr -// expr = Add (Add (Val 3) (Val 1)) (Add (Val 1) (Var 1)) - -// printExpr : Expr -> Str -// printExpr = \e -> -// when e is -// Add a b -> -// "Add (" -// |> Str.concat (printExpr a) -// |> Str.concat ") (" -// |> Str.concat (printExpr b) -// |> Str.concat ")" -// Val v -> "Val " |> Str.concat (Num.toStr v) -// Var v -> "Var " |> Str.concat (Num.toStr v) - -// printExpr expr -// "# -// ), -// RocStr::from_slice(b"Add (Add (Val 3) (Val 1)) (Add (Val 1) (Var 1))"), -// RocStr -// ); -// } - -#[test] -fn str_join_comma_small() { - assert_evals_to!( - r#"Str.joinWith ["1", "2"] ", " "#, - RocStr::from("1, 2"), - RocStr - ); -} - -#[test] -fn str_join_comma_big() { - assert_evals_to!( - r#"Str.joinWith ["10000000", "2000000", "30000000"] ", " "#, - RocStr::from("10000000, 2000000, 30000000"), - RocStr - ); -} - -#[test] -fn str_join_comma_single() { - assert_evals_to!(r#"Str.joinWith ["1"] ", " "#, RocStr::from("1"), RocStr); -} - -#[test] -fn str_to_utf8() { - assert_evals_to!( - r#"Str.toUtf8 "hello""#, - RocList::from_slice(&[104, 101, 108, 108, 111]), - RocList - ); - assert_evals_to!( - r#"Str.toUtf8 "this is a long string""#, - RocList::from_slice(&[ - 116, 104, 105, 115, 32, 105, 115, 32, 97, 32, 108, 111, 110, 103, 32, 115, 116, 114, - 105, 110, 103 - ]), - RocList - ); -} - -// #[test] -// fn str_from_utf8_range() { -// assert_evals_to!( -// indoc!( -// r#" -// bytes = Str.toUtf8 "hello" -// when Str.fromUtf8Range bytes { count: 5, start: 0 } is -// Ok utf8String -> utf8String -// _ -> "" -// "# -// ), -// RocStr::from("hello"), -// RocStr -// ); -// } - -// #[test] -// fn str_from_utf8_range_slice() { -// assert_evals_to!( -// indoc!( -// r#" -// bytes = Str.toUtf8 "hello" -// when Str.fromUtf8Range bytes { count: 4, start: 1 } is -// Ok utf8String -> utf8String -// _ -> "" -// "# -// ), -// RocStr::from("ello"), -// RocStr -// ); -// } - -// #[test] -// fn str_from_utf8_range_slice_not_end() { -// assert_evals_to!( -// indoc!( -// r#" -// bytes = Str.toUtf8 "hello" -// when Str.fromUtf8Range bytes { count: 3, start: 1 } is -// Ok utf8String -> utf8String -// _ -> "" -// "# -// ), -// RocStr::from("ell"), -// RocStr -// ); -// } - -// #[test] -// fn str_from_utf8_range_order_does_not_matter() { -// assert_evals_to!( -// indoc!( -// r#" -// bytes = Str.toUtf8 "hello" -// when Str.fromUtf8Range bytes { start: 1, count: 3 } is -// Ok utf8String -> utf8String -// _ -> "" -// "# -// ), -// RocStr::from("ell"), -// RocStr -// ); -// } - -// #[test] -// fn str_from_utf8_range_out_of_bounds_start_value() { -// assert_evals_to!( -// indoc!( -// r#" -// bytes = Str.toUtf8 "hello" -// when Str.fromUtf8Range bytes { start: 7, count: 3 } is -// Ok _ -> "" -// Err (BadUtf8 _ _) -> "" -// Err OutOfBounds -> "out of bounds" -// "# -// ), -// RocStr::from("out of bounds"), -// RocStr -// ); -// } - -// #[test] -// fn str_from_utf8_range_count_too_high() { -// assert_evals_to!( -// indoc!( -// r#" -// bytes = Str.toUtf8 "hello" -// when Str.fromUtf8Range bytes { start: 0, count: 6 } is -// Ok _ -> "" -// Err (BadUtf8 _ _) -> "" -// Err OutOfBounds -> "out of bounds" -// "# -// ), -// RocStr::from("out of bounds"), -// RocStr -// ); -// } - -// #[test] -// fn str_from_utf8_range_count_too_high_for_start() { -// assert_evals_to!( -// indoc!( -// r#" -// bytes = Str.toUtf8 "hello" -// when Str.fromUtf8Range bytes { start: 4, count: 3 } is -// Ok _ -> "" -// Err (BadUtf8 _ _) -> "" -// Err OutOfBounds -> "out of bounds" -// "# -// ), -// RocStr::from("out of bounds"), -// RocStr -// ); -// } - -#[test] -fn str_repeat_small() { - assert_evals_to!( - indoc!(r#"Str.repeat "Roc" 2"#), - RocStr::from("RocRoc"), - RocStr - ); -} - -#[test] -fn str_repeat_big() { - assert_evals_to!( - indoc!(r#"Str.repeat "more than 16 characters" 2"#), - RocStr::from("more than 16 charactersmore than 16 characters"), - RocStr - ); -} - -#[test] -fn str_repeat_empty_string() { - assert_evals_to!(indoc!(r#"Str.repeat "" 3"#), RocStr::from(""), RocStr); -} - -#[test] -fn str_repeat_zero_times() { - assert_evals_to!(indoc!(r#"Str.repeat "Roc" 0"#), RocStr::from(""), RocStr); -} - -#[test] -fn str_trim_empty_string() { - assert_evals_to!(indoc!(r#"Str.trim """#), RocStr::from(""), RocStr); -} - -#[test] -fn str_trim_small_blank_string() { - assert_evals_to!(indoc!(r#"Str.trim " ""#), RocStr::from(""), RocStr); -} - -#[test] -fn str_trim_small_to_small() { - assert_evals_to!( - indoc!(r#"Str.trim " hello ""#), - RocStr::from("hello"), - RocStr - ); -} - -#[test] -fn str_trim_large_to_large_unique() { - assert_evals_to!( - indoc!(r#"Str.trim (Str.concat " " "hello world from a large string ")"#), - RocStr::from("hello world from a large string"), - RocStr - ); -} - -#[test] -fn str_trim_large_to_small_unique() { - assert_evals_to!( - indoc!(r#"Str.trim (Str.concat " " "hello ")"#), - RocStr::from("hello"), - RocStr - ); -} - -#[test] -fn str_trim_large_to_large_shared() { - assert_evals_to!( - indoc!( - r#" - original : Str - original = " hello world world " - - { trimmed: Str.trim original, original: original } - "# - ), - ( - RocStr::from(" hello world world "), - RocStr::from("hello world world"), - ), - (RocStr, RocStr) - ); -} - -#[test] -fn str_trim_large_to_small_shared() { - assert_evals_to!( - indoc!( - r#" - original : Str - original = " hello " - - { trimmed: Str.trim original, original: original } - "# - ), - (RocStr::from(" hello "), RocStr::from("hello"),), - (RocStr, RocStr) - ); -} - -#[test] -fn str_trim_small_to_small_shared() { - assert_evals_to!( - indoc!( - r#" - original : Str - original = " hello " - - { trimmed: Str.trim original, original: original } - "# - ), - (RocStr::from(" hello "), RocStr::from("hello"),), - (RocStr, RocStr) - ); -} - -#[test] -fn str_trim_left_small_blank_string() { - assert_evals_to!(indoc!(r#"Str.trimLeft " ""#), RocStr::from(""), RocStr); -} - -#[test] -fn str_trim_left_small_to_small() { - assert_evals_to!( - indoc!(r#"Str.trimLeft " hello ""#), - RocStr::from("hello "), - RocStr - ); -} - -#[test] -fn str_trim_left_large_to_large_unique() { - assert_evals_to!( - indoc!(r#"Str.trimLeft (Str.concat " " "hello world from a large string ")"#), - RocStr::from("hello world from a large string "), - RocStr - ); -} - -#[test] -fn str_trim_left_large_to_small_unique() { - assert_evals_to!( - indoc!(r#"Str.trimLeft (Str.concat " " "hello ")"#), - RocStr::from("hello "), - RocStr - ); -} - -#[test] -fn str_trim_right_small_blank_string() { - assert_evals_to!(indoc!(r#"Str.trimRight " ""#), RocStr::from(""), RocStr); -} - -#[test] -fn str_trim_right_small_to_small() { - assert_evals_to!( - indoc!(r#"Str.trimRight " hello ""#), - RocStr::from(" hello"), - RocStr - ); -} - -#[test] -fn str_trim_right_large_to_large_unique() { - assert_evals_to!( - indoc!(r#"Str.trimRight (Str.concat " hello world from a large string" " ")"#), - RocStr::from(" hello world from a large string"), - RocStr - ); -} - -#[test] -fn str_trim_right_large_to_small_unique() { - assert_evals_to!( - indoc!(r#"Str.trimRight (Str.concat " hello" " ")"#), - RocStr::from(" hello"), - RocStr - ); -} - -#[test] -fn str_trim_right_large_to_large_shared() { - assert_evals_to!( - indoc!( - r#" - original : Str - original = " hello world world " - - { trimmed: Str.trimRight original, original: original } - "# - ), - ( - RocStr::from(" hello world world "), - RocStr::from(" hello world world"), - ), - (RocStr, RocStr) - ); -} - -#[test] -fn str_trim_right_large_to_small_shared() { - assert_evals_to!( - indoc!( - r#" - original : Str - original = " hello " - - { trimmed: Str.trimRight original, original: original } - "# - ), - (RocStr::from(" hello "), RocStr::from(" hello"),), - (RocStr, RocStr) - ); -} - -#[test] -fn str_trim_right_small_to_small_shared() { - assert_evals_to!( - indoc!( - r#" - original : Str - original = " hello " - - { trimmed: Str.trimRight original, original: original } - "# - ), - (RocStr::from(" hello "), RocStr::from(" hello"),), - (RocStr, RocStr) - ); -} - -#[test] -fn str_to_nat() { - assert_evals_to!( - indoc!( - r#" - when Str.toNat "1" is - Ok n -> n - Err _ -> 0 - "# - ), - 1, - usize - ); -} - -#[test] -fn str_to_i128() { - assert_evals_to!( - indoc!( - r#" - when Str.toI128 "1" is - Ok n -> n - Err _ -> 0 - "# - ), - 1, - i128 - ); -} - -#[test] -fn str_to_u128() { - assert_evals_to!( - indoc!( - r#" - when Str.toU128 "1" is - Ok n -> n - Err _ -> 0 - "# - ), - 1, - u128 - ); -} - -#[test] -fn str_to_i64() { - assert_evals_to!( - indoc!( - r#" - when Str.toI64 "1" is - Ok n -> n - Err _ -> 0 - "# - ), - 1, - i64 - ); -} - -#[test] -fn str_to_u64() { - assert_evals_to!( - indoc!( - r#" - when Str.toU64 "1" is - Ok n -> n - Err _ -> 0 - "# - ), - 1, - u64 - ); -} - -#[test] -fn str_to_i32() { - assert_evals_to!( - indoc!( - r#" - when Str.toI32 "1" is - Ok n -> n - Err _ -> 0 - "# - ), - 1, - i32 - ); -} - -#[test] -fn str_to_u32() { - assert_evals_to!( - indoc!( - r#" - when Str.toU32 "1" is - Ok n -> n - Err _ -> 0 - "# - ), - 1, - u32 - ); -} - -#[test] -fn str_to_i16() { - assert_evals_to!( - indoc!( - r#" - when Str.toI16 "1" is - Ok n -> n - Err _ -> 0 - "# - ), - 1, - i16 - ); -} - -#[test] -fn str_to_u16() { - assert_evals_to!( - indoc!( - r#" - when Str.toU16 "1" is - Ok n -> n - Err _ -> 0 - "# - ), - 1, - u16 - ); -} - -#[test] -fn str_to_i8() { - assert_evals_to!( - indoc!( - r#" - when Str.toI8 "1" is - Ok n -> n - Err _ -> 0 - "# - ), - 1, - i8 - ); -} - -#[test] -fn str_to_u8() { - assert_evals_to!( - indoc!( - r#" - when Str.toU8 "1" is - Ok n -> n - Err _ -> 0 - "# - ), - 1, - u8 - ); -} - -#[test] -fn str_to_f64() { - assert_evals_to!( - indoc!( - r#" - when Str.toF64 "1.0" is - Ok n -> n - Err _ -> 0 - "# - ), - 1.0, - f64 - ); -} - -#[test] -fn str_to_f32() { - assert_evals_to!( - indoc!( - r#" - when Str.toF32 "1.0" is - Ok n -> n - Err _ -> 0 - "# - ), - 1.0, - f32 - ); -} - -#[test] -fn str_to_dec() { - use roc_std::RocDec; - - assert_evals_to!( - indoc!( - r#" - when Str.toDec "1.0" is - Ok n -> n - Err _ -> 0 - "# - ), - RocDec::from_str("1.0").unwrap(), - RocDec - ); -} diff --git a/compiler/test_mono/Cargo.toml b/compiler/test_mono/Cargo.toml deleted file mode 100644 index eb87b5efa8..0000000000 --- a/compiler/test_mono/Cargo.toml +++ /dev/null @@ -1,23 +0,0 @@ -[package] -name = "test_mono" -version = "0.1.0" -authors = ["The Roc Contributors"] -license = "UPL-1.0" -edition = "2021" - -[[test]] -name = "test_mono" -path = "src/tests.rs" - -[dev-dependencies] -roc_collections = { path = "../collections" } -roc_module = { path = "../module" } -roc_builtins = { path = "../builtins" } -roc_load = { path = "../load" } -roc_can = { path = "../can" } -roc_mono = { path = "../mono" } -roc_target = { path = "../roc_target" } -roc_reporting = { path = "../../reporting" } -test_mono_macros = { path = "../test_mono_macros" } -bumpalo = { version = "3.8.0", features = ["collections"] } -indoc = "1.0.3" diff --git a/compiler/test_mono/generated/alias_variable.txt b/compiler/test_mono/generated/alias_variable.txt deleted file mode 100644 index ccb6249257..0000000000 --- a/compiler/test_mono/generated/alias_variable.txt +++ /dev/null @@ -1,3 +0,0 @@ -procedure Test.0 (): - let Test.3 : I64 = 3i64; - ret Test.3; diff --git a/compiler/test_mono/generated/alias_variable_and_return_it.txt b/compiler/test_mono/generated/alias_variable_and_return_it.txt deleted file mode 100644 index 63fd9ba01e..0000000000 --- a/compiler/test_mono/generated/alias_variable_and_return_it.txt +++ /dev/null @@ -1,3 +0,0 @@ -procedure Test.0 (): - let Test.2 : I64 = 5i64; - ret Test.2; diff --git a/compiler/test_mono/generated/aliased_polymorphic_closure.txt b/compiler/test_mono/generated/aliased_polymorphic_closure.txt deleted file mode 100644 index cd870ec963..0000000000 --- a/compiler/test_mono/generated/aliased_polymorphic_closure.txt +++ /dev/null @@ -1,17 +0,0 @@ -procedure Test.2 (Test.6, #Attr.12): - let Test.1 : U8 = StructAtIndex 0 #Attr.12; - let Test.11 : {U8} = Struct {Test.1}; - ret Test.11; - -procedure Test.4 (Test.5, #Attr.12): - let Test.1 : U8 = StructAtIndex 0 #Attr.12; - ret Test.1; - -procedure Test.0 (): - let Test.1 : U8 = 1i64; - let Test.8 : {} = Struct {}; - let Test.10 : {} = Struct {}; - let Test.14 : {U8} = Struct {Test.1}; - let Test.9 : {U8} = CallByName Test.2 Test.10 Test.14; - let Test.7 : U8 = CallByName Test.4 Test.8 Test.9; - ret Test.7; diff --git a/compiler/test_mono/generated/closure_in_list.txt b/compiler/test_mono/generated/closure_in_list.txt deleted file mode 100644 index 4687f8cc52..0000000000 --- a/compiler/test_mono/generated/closure_in_list.txt +++ /dev/null @@ -1,21 +0,0 @@ -procedure List.6 (#Attr.2): - let List.139 : U64 = lowlevel ListLen #Attr.2; - ret List.139; - -procedure Test.1 (Test.5): - let Test.2 : I64 = 41i64; - let Test.11 : {I64} = Struct {Test.2}; - let Test.10 : List {I64} = Array [Test.11]; - ret Test.10; - -procedure Test.3 (Test.9, #Attr.12): - let Test.2 : I64 = StructAtIndex 0 #Attr.12; - let Test.2 : I64 = 41i64; - ret Test.2; - -procedure Test.0 (): - let Test.8 : {} = Struct {}; - let Test.7 : List {I64} = CallByName Test.1 Test.8; - let Test.6 : U64 = CallByName List.6 Test.7; - dec Test.7; - ret Test.6; diff --git a/compiler/test_mono/generated/dict.txt b/compiler/test_mono/generated/dict.txt deleted file mode 100644 index 31ae33d719..0000000000 --- a/compiler/test_mono/generated/dict.txt +++ /dev/null @@ -1,13 +0,0 @@ -procedure Dict.1 (): - let Dict.28 : Dict [] [] = lowlevel DictEmpty ; - ret Dict.28; - -procedure Dict.7 (#Attr.2): - let Dict.27 : U64 = lowlevel DictSize #Attr.2; - dec #Attr.2; - ret Dict.27; - -procedure Test.0 (): - let Test.2 : Dict [] [] = CallByName Dict.1; - let Test.1 : U64 = CallByName Dict.7 Test.2; - ret Test.1; diff --git a/compiler/test_mono/generated/empty_list_of_function_type.txt b/compiler/test_mono/generated/empty_list_of_function_type.txt deleted file mode 100644 index ae00ee7d57..0000000000 --- a/compiler/test_mono/generated/empty_list_of_function_type.txt +++ /dev/null @@ -1,42 +0,0 @@ -procedure List.2 (#Attr.2, #Attr.3): - let List.144 : U64 = lowlevel ListLen #Attr.2; - let List.141 : Int1 = lowlevel NumLt #Attr.3 List.144; - if List.141 then - let List.143 : {} = lowlevel ListGetUnsafe #Attr.2 #Attr.3; - let List.142 : [C {}, C {}] = Ok List.143; - ret List.142; - else - let List.140 : {} = Struct {}; - let List.139 : [C {}, C {}] = Err List.140; - ret List.139; - -procedure Test.2 (Test.6): - let Test.18 : Str = "bar"; - ret Test.18; - -procedure Test.0 (): - joinpoint Test.16 Test.3: - let Test.14 : U64 = 0i64; - let Test.7 : [C {}, C {}] = CallByName List.2 Test.3 Test.14; - dec Test.3; - let Test.11 : U8 = 1i64; - let Test.12 : U8 = GetTagId Test.7; - let Test.13 : Int1 = lowlevel Eq Test.11 Test.12; - if Test.13 then - let Test.5 : {} = UnionAtIndex (Id 1) (Index 0) Test.7; - let Test.9 : Str = "foo"; - let Test.8 : Str = CallByName Test.2 Test.9; - dec Test.9; - ret Test.8; - else - let Test.10 : Str = "bad!"; - ret Test.10; - in - let Test.19 : Int1 = false; - if Test.19 then - let Test.1 : List {} = Array []; - jump Test.16 Test.1; - else - let Test.17 : {} = Struct {}; - let Test.15 : List {} = Array [Test.17]; - jump Test.16 Test.15; diff --git a/compiler/test_mono/generated/factorial.txt b/compiler/test_mono/generated/factorial.txt deleted file mode 100644 index a0ad7c91ac..0000000000 --- a/compiler/test_mono/generated/factorial.txt +++ /dev/null @@ -1,27 +0,0 @@ -procedure Num.20 (#Attr.2, #Attr.3): - let Num.274 : I64 = lowlevel NumSub #Attr.2 #Attr.3; - ret Num.274; - -procedure Num.21 (#Attr.2, #Attr.3): - let Num.273 : I64 = lowlevel NumMul #Attr.2 #Attr.3; - ret Num.273; - -procedure Test.1 (Test.15, Test.16): - joinpoint Test.7 Test.2 Test.3: - let Test.13 : I64 = 0i64; - let Test.14 : Int1 = lowlevel Eq Test.13 Test.2; - if Test.14 then - ret Test.3; - else - let Test.12 : I64 = 1i64; - let Test.10 : I64 = CallByName Num.20 Test.2 Test.12; - let Test.11 : I64 = CallByName Num.21 Test.2 Test.3; - jump Test.7 Test.10 Test.11; - in - jump Test.7 Test.15 Test.16; - -procedure Test.0 (): - let Test.5 : I64 = 10i64; - let Test.6 : I64 = 1i64; - let Test.4 : I64 = CallByName Test.1 Test.5 Test.6; - ret Test.4; diff --git a/compiler/test_mono/generated/guard_pattern_true.txt b/compiler/test_mono/generated/guard_pattern_true.txt deleted file mode 100644 index 47bfc64e1f..0000000000 --- a/compiler/test_mono/generated/guard_pattern_true.txt +++ /dev/null @@ -1,25 +0,0 @@ -procedure Test.1 (Test.3): - let Test.6 : I64 = 2i64; - joinpoint Test.11: - let Test.10 : I64 = 0i64; - ret Test.10; - in - let Test.13 : I64 = 2i64; - let Test.14 : Int1 = lowlevel Eq Test.13 Test.6; - if Test.14 then - joinpoint Test.8 Test.12: - if Test.12 then - let Test.7 : I64 = 42i64; - ret Test.7; - else - jump Test.11; - in - let Test.9 : Int1 = false; - jump Test.8 Test.9; - else - jump Test.11; - -procedure Test.0 (): - let Test.5 : {} = Struct {}; - let Test.4 : I64 = CallByName Test.1 Test.5; - ret Test.4; diff --git a/compiler/test_mono/generated/has_none.txt b/compiler/test_mono/generated/has_none.txt deleted file mode 100644 index 05a4158eab..0000000000 --- a/compiler/test_mono/generated/has_none.txt +++ /dev/null @@ -1,30 +0,0 @@ -procedure Test.4 (Test.30): - joinpoint Test.14 Test.5: - let Test.24 : Int1 = 1i64; - let Test.25 : Int1 = GetTagId Test.5; - let Test.26 : Int1 = lowlevel Eq Test.24 Test.25; - if Test.26 then - let Test.15 : Int1 = false; - ret Test.15; - else - let Test.20 : [C I64, C ] = UnionAtIndex (Id 0) (Index 0) Test.5; - let Test.21 : U8 = 1i64; - let Test.22 : U8 = GetTagId Test.20; - let Test.23 : Int1 = lowlevel Eq Test.21 Test.22; - if Test.23 then - let Test.16 : Int1 = true; - ret Test.16; - else - let Test.8 : [, C [C I64, C ] *self] = UnionAtIndex (Id 0) (Index 1) Test.5; - jump Test.14 Test.8; - in - jump Test.14 Test.30; - -procedure Test.0 (): - let Test.29 : I64 = 3i64; - let Test.27 : [C I64, C ] = Just Test.29; - let Test.28 : [, C [C I64, C ] *self] = Nil ; - let Test.13 : [, C [C I64, C ] *self] = Cons Test.27 Test.28; - let Test.12 : Int1 = CallByName Test.4 Test.13; - dec Test.13; - ret Test.12; diff --git a/compiler/test_mono/generated/if_guard_bind_variable_false.txt b/compiler/test_mono/generated/if_guard_bind_variable_false.txt deleted file mode 100644 index cf492ae71d..0000000000 --- a/compiler/test_mono/generated/if_guard_bind_variable_false.txt +++ /dev/null @@ -1,22 +0,0 @@ -procedure Bool.7 (#Attr.2, #Attr.3): - let Bool.14 : Int1 = lowlevel Eq #Attr.2 #Attr.3; - ret Bool.14; - -procedure Test.1 (Test.3): - let Test.6 : I64 = 10i64; - joinpoint Test.8 Test.12: - if Test.12 then - let Test.7 : I64 = 0i64; - ret Test.7; - else - let Test.11 : I64 = 42i64; - ret Test.11; - in - let Test.10 : I64 = 5i64; - let Test.9 : Int1 = CallByName Bool.7 Test.6 Test.10; - jump Test.8 Test.9; - -procedure Test.0 (): - let Test.5 : {} = Struct {}; - let Test.4 : I64 = CallByName Test.1 Test.5; - ret Test.4; diff --git a/compiler/test_mono/generated/if_multi_branch.txt b/compiler/test_mono/generated/if_multi_branch.txt deleted file mode 100644 index 6178c6a0db..0000000000 --- a/compiler/test_mono/generated/if_multi_branch.txt +++ /dev/null @@ -1,13 +0,0 @@ -procedure Test.0 (): - let Test.6 : Int1 = true; - if Test.6 then - let Test.7 : I64 = 1i64; - ret Test.7; - else - let Test.4 : Int1 = false; - if Test.4 then - let Test.5 : I64 = 2i64; - ret Test.5; - else - let Test.3 : I64 = 3i64; - ret Test.3; diff --git a/compiler/test_mono/generated/ir_int_add.txt b/compiler/test_mono/generated/ir_int_add.txt deleted file mode 100644 index 5343090dd9..0000000000 --- a/compiler/test_mono/generated/ir_int_add.txt +++ /dev/null @@ -1,19 +0,0 @@ -procedure List.6 (#Attr.2): - let List.139 : U64 = lowlevel ListLen #Attr.2; - ret List.139; - -procedure Num.19 (#Attr.2, #Attr.3): - let Num.275 : U64 = lowlevel NumAdd #Attr.2 #Attr.3; - ret Num.275; - -procedure Test.0 (): - let Test.8 : U64 = 5i64; - let Test.9 : U64 = 4i64; - let Test.6 : U64 = CallByName Num.19 Test.8 Test.9; - let Test.7 : U64 = 3i64; - let Test.3 : U64 = CallByName Num.19 Test.6 Test.7; - let Test.5 : List I64 = Array [1i64, 2i64]; - let Test.4 : U64 = CallByName List.6 Test.5; - dec Test.5; - let Test.2 : U64 = CallByName Num.19 Test.3 Test.4; - ret Test.2; diff --git a/compiler/test_mono/generated/ir_plus.txt b/compiler/test_mono/generated/ir_plus.txt deleted file mode 100644 index 8a3eff1485..0000000000 --- a/compiler/test_mono/generated/ir_plus.txt +++ /dev/null @@ -1,9 +0,0 @@ -procedure Num.19 (#Attr.2, #Attr.3): - let Num.273 : I64 = lowlevel NumAdd #Attr.2 #Attr.3; - ret Num.273; - -procedure Test.0 (): - let Test.2 : I64 = 1i64; - let Test.3 : I64 = 2i64; - let Test.1 : I64 = CallByName Num.19 Test.2 Test.3; - ret Test.1; diff --git a/compiler/test_mono/generated/ir_round.txt b/compiler/test_mono/generated/ir_round.txt deleted file mode 100644 index 19f78559ec..0000000000 --- a/compiler/test_mono/generated/ir_round.txt +++ /dev/null @@ -1,8 +0,0 @@ -procedure Num.45 (#Attr.2): - let Num.273 : I64 = lowlevel NumRound #Attr.2; - ret Num.273; - -procedure Test.0 (): - let Test.2 : Float64 = 3.6f64; - let Test.1 : I64 = CallByName Num.45 Test.2; - ret Test.1; diff --git a/compiler/test_mono/generated/ir_two_defs.txt b/compiler/test_mono/generated/ir_two_defs.txt deleted file mode 100644 index d99d01140c..0000000000 --- a/compiler/test_mono/generated/ir_two_defs.txt +++ /dev/null @@ -1,9 +0,0 @@ -procedure Num.19 (#Attr.2, #Attr.3): - let Num.273 : I64 = lowlevel NumAdd #Attr.2 #Attr.3; - ret Num.273; - -procedure Test.0 (): - let Test.4 : I64 = 3i64; - let Test.5 : I64 = 4i64; - let Test.3 : I64 = CallByName Num.19 Test.4 Test.5; - ret Test.3; diff --git a/compiler/test_mono/generated/ir_when_idiv.txt b/compiler/test_mono/generated/ir_when_idiv.txt deleted file mode 100644 index a4c3915acf..0000000000 --- a/compiler/test_mono/generated/ir_when_idiv.txt +++ /dev/null @@ -1,25 +0,0 @@ -procedure Num.40 (#Attr.2, #Attr.3): - let Num.278 : I64 = 0i64; - let Num.275 : Int1 = lowlevel NotEq #Attr.3 Num.278; - if Num.275 then - let Num.277 : I64 = lowlevel NumDivUnchecked #Attr.2 #Attr.3; - let Num.276 : [C {}, C I64] = Ok Num.277; - ret Num.276; - else - let Num.274 : {} = Struct {}; - let Num.273 : [C {}, C I64] = Err Num.274; - ret Num.273; - -procedure Test.0 (): - let Test.8 : I64 = 1000i64; - let Test.9 : I64 = 10i64; - let Test.2 : [C {}, C I64] = CallByName Num.40 Test.8 Test.9; - let Test.5 : U8 = 1i64; - let Test.6 : U8 = GetTagId Test.2; - let Test.7 : Int1 = lowlevel Eq Test.5 Test.6; - if Test.7 then - let Test.1 : I64 = UnionAtIndex (Id 1) (Index 0) Test.2; - ret Test.1; - else - let Test.4 : I64 = -1i64; - ret Test.4; diff --git a/compiler/test_mono/generated/ir_when_record.txt b/compiler/test_mono/generated/ir_when_record.txt deleted file mode 100644 index bbac521770..0000000000 --- a/compiler/test_mono/generated/ir_when_record.txt +++ /dev/null @@ -1,6 +0,0 @@ -procedure Test.0 (): - let Test.4 : I64 = 1i64; - let Test.5 : Float64 = 3.14f64; - let Test.2 : {I64, Float64} = Struct {Test.4, Test.5}; - let Test.1 : I64 = StructAtIndex 0 Test.2; - ret Test.1; diff --git a/compiler/test_mono/generated/is_nil.txt b/compiler/test_mono/generated/is_nil.txt deleted file mode 100644 index 9b47d797ca..0000000000 --- a/compiler/test_mono/generated/is_nil.txt +++ /dev/null @@ -1,18 +0,0 @@ -procedure Test.3 (Test.4): - let Test.13 : Int1 = 1i64; - let Test.14 : Int1 = GetTagId Test.4; - let Test.15 : Int1 = lowlevel Eq Test.13 Test.14; - if Test.15 then - let Test.11 : Int1 = true; - ret Test.11; - else - let Test.12 : Int1 = false; - ret Test.12; - -procedure Test.0 (): - let Test.16 : I64 = 2i64; - let Test.17 : [, C I64 *self] = Nil ; - let Test.10 : [, C I64 *self] = Cons Test.16 Test.17; - let Test.9 : Int1 = CallByName Test.3 Test.10; - dec Test.10; - ret Test.9; diff --git a/compiler/test_mono/generated/issue_2535_polymorphic_fields_referenced_in_list.txt b/compiler/test_mono/generated/issue_2535_polymorphic_fields_referenced_in_list.txt deleted file mode 100644 index 0b14e41d86..0000000000 --- a/compiler/test_mono/generated/issue_2535_polymorphic_fields_referenced_in_list.txt +++ /dev/null @@ -1,19 +0,0 @@ -procedure Test.1 (): - let Test.11 : I64 = 2i64; - let Test.12 : U8 = 1i64; - let Test.10 : {I64, U8} = Struct {Test.11, Test.12}; - ret Test.10; - -procedure Test.1 (): - let Test.7 : I64 = 1i64; - let Test.8 : U8 = 2i64; - let Test.6 : {I64, U8} = Struct {Test.7, Test.8}; - ret Test.6; - -procedure Test.0 (): - let Test.9 : {I64, U8} = CallByName Test.1; - let Test.3 : U8 = StructAtIndex 1 Test.9; - let Test.5 : {I64, U8} = CallByName Test.1; - let Test.4 : U8 = StructAtIndex 1 Test.5; - let Test.2 : List U8 = Array [Test.3, Test.4]; - ret Test.2; diff --git a/compiler/test_mono/generated/issue_2583_specialize_errors_behind_unified_branches.txt b/compiler/test_mono/generated/issue_2583_specialize_errors_behind_unified_branches.txt deleted file mode 100644 index ac2eaef4f3..0000000000 --- a/compiler/test_mono/generated/issue_2583_specialize_errors_behind_unified_branches.txt +++ /dev/null @@ -1,40 +0,0 @@ -procedure List.9 (#Attr.2): - let List.145 : U64 = 0i64; - let List.146 : U64 = lowlevel ListLen #Attr.2; - let List.141 : Int1 = lowlevel NotEq List.145 List.146; - if List.141 then - let List.144 : U64 = 0i64; - let List.143 : I64 = lowlevel ListGetUnsafe #Attr.2 List.144; - let List.142 : [C Int1, C I64] = Ok List.143; - ret List.142; - else - let List.140 : Int1 = true; - let List.139 : [C Int1, C I64] = Err List.140; - ret List.139; - -procedure Str.27 (#Attr.2): - let #Attr.3 : {I64, U8} = lowlevel StrToNum #Attr.2; - let Str.70 : U8 = StructAtIndex 1 #Attr.3; - let Str.71 : U8 = 0i64; - let Str.67 : Int1 = lowlevel NumGt Str.70 Str.71; - if Str.67 then - let Str.69 : Int1 = false; - let Str.68 : [C Int1, C I64] = Err Str.69; - ret Str.68; - else - let Str.66 : I64 = StructAtIndex 0 #Attr.3; - let Str.65 : [C Int1, C I64] = Ok Str.66; - ret Str.65; - -procedure Test.0 (): - let Test.4 : Int1 = true; - if Test.4 then - let Test.6 : List I64 = Array []; - let Test.5 : [C Int1, C I64] = CallByName List.9 Test.6; - dec Test.6; - ret Test.5; - else - let Test.3 : Str = ""; - let Test.2 : [C Int1, C I64] = CallByName Str.27 Test.3; - dec Test.3; - ret Test.2; diff --git a/compiler/test_mono/generated/issue_2725_alias_polymorphic_lambda.txt b/compiler/test_mono/generated/issue_2725_alias_polymorphic_lambda.txt deleted file mode 100644 index 1eba5a003d..0000000000 --- a/compiler/test_mono/generated/issue_2725_alias_polymorphic_lambda.txt +++ /dev/null @@ -1,7 +0,0 @@ -procedure Test.2 (Test.3): - ret Test.3; - -procedure Test.0 (): - let Test.6 : I64 = 42i64; - let Test.5 : I64 = CallByName Test.2 Test.6; - ret Test.5; diff --git a/compiler/test_mono/generated/issue_2810.txt b/compiler/test_mono/generated/issue_2810.txt deleted file mode 100644 index 31f6b7125e..0000000000 --- a/compiler/test_mono/generated/issue_2810.txt +++ /dev/null @@ -1,6 +0,0 @@ -procedure Test.0 (): - let Test.19 : [C [C [C *self, C ]], C ] = SystemTool ; - let Test.17 : [C [C *self, C ]] = Job Test.19; - let Test.16 : [C [C [C *self, C ]], C ] = FromJob Test.17; - let Test.7 : [C [C *self, C ]] = Job Test.16; - ret Test.7; diff --git a/compiler/test_mono/generated/let_with_record_pattern.txt b/compiler/test_mono/generated/let_with_record_pattern.txt deleted file mode 100644 index 58abbb1866..0000000000 --- a/compiler/test_mono/generated/let_with_record_pattern.txt +++ /dev/null @@ -1,6 +0,0 @@ -procedure Test.0 (): - let Test.4 : I64 = 2i64; - let Test.5 : Float64 = 3.14f64; - let Test.3 : {I64, Float64} = Struct {Test.4, Test.5}; - let Test.1 : I64 = StructAtIndex 0 Test.3; - ret Test.1; diff --git a/compiler/test_mono/generated/let_with_record_pattern_list.txt b/compiler/test_mono/generated/let_with_record_pattern_list.txt deleted file mode 100644 index 9475864c6c..0000000000 --- a/compiler/test_mono/generated/let_with_record_pattern_list.txt +++ /dev/null @@ -1,8 +0,0 @@ -procedure Test.0 (): - let Test.4 : List I64 = Array [1i64, 3i64, 4i64]; - let Test.5 : Float64 = 3.14f64; - let Test.3 : {List I64, Float64} = Struct {Test.4, Test.5}; - let Test.1 : List I64 = StructAtIndex 0 Test.3; - inc Test.1; - dec Test.3; - ret Test.1; diff --git a/compiler/test_mono/generated/let_x_in_x.txt b/compiler/test_mono/generated/let_x_in_x.txt deleted file mode 100644 index 72af22119a..0000000000 --- a/compiler/test_mono/generated/let_x_in_x.txt +++ /dev/null @@ -1,3 +0,0 @@ -procedure Test.0 (): - let Test.2 : I64 = 1337i64; - ret Test.2; diff --git a/compiler/test_mono/generated/let_x_in_x_indirect.txt b/compiler/test_mono/generated/let_x_in_x_indirect.txt deleted file mode 100644 index 6215e7f0af..0000000000 --- a/compiler/test_mono/generated/let_x_in_x_indirect.txt +++ /dev/null @@ -1,6 +0,0 @@ -procedure Test.0 (): - let Test.2 : I64 = 1337i64; - let Test.3 : I64 = 17i64; - let Test.7 : {I64, I64} = Struct {Test.2, Test.3}; - let Test.6 : I64 = StructAtIndex 0 Test.7; - ret Test.6; diff --git a/compiler/test_mono/generated/linked_list_length_twice.txt b/compiler/test_mono/generated/linked_list_length_twice.txt deleted file mode 100644 index dbed7f9abb..0000000000 --- a/compiler/test_mono/generated/linked_list_length_twice.txt +++ /dev/null @@ -1,25 +0,0 @@ -procedure Num.19 (#Attr.2, #Attr.3): - let Num.274 : I64 = lowlevel NumAdd #Attr.2 #Attr.3; - ret Num.274; - -procedure Test.4 (Test.6): - let Test.16 : Int1 = 1i64; - let Test.17 : Int1 = GetTagId Test.6; - let Test.18 : Int1 = lowlevel Eq Test.16 Test.17; - if Test.18 then - let Test.12 : I64 = 0i64; - ret Test.12; - else - let Test.7 : [, C I64 *self] = UnionAtIndex (Id 0) (Index 1) Test.6; - let Test.14 : I64 = 1i64; - let Test.15 : I64 = CallByName Test.4 Test.7; - let Test.13 : I64 = CallByName Num.19 Test.14 Test.15; - ret Test.13; - -procedure Test.0 (): - let Test.3 : [, C I64 *self] = Nil ; - let Test.9 : I64 = CallByName Test.4 Test.3; - let Test.10 : I64 = CallByName Test.4 Test.3; - dec Test.3; - let Test.8 : I64 = CallByName Num.19 Test.9 Test.10; - ret Test.8; diff --git a/compiler/test_mono/generated/list_append.txt b/compiler/test_mono/generated/list_append.txt deleted file mode 100644 index 93bb4798f8..0000000000 --- a/compiler/test_mono/generated/list_append.txt +++ /dev/null @@ -1,9 +0,0 @@ -procedure List.4 (#Attr.2, #Attr.3): - let List.139 : List I64 = lowlevel ListAppend #Attr.2 #Attr.3; - ret List.139; - -procedure Test.0 (): - let Test.2 : List I64 = Array [1i64]; - let Test.3 : I64 = 2i64; - let Test.1 : List I64 = CallByName List.4 Test.2 Test.3; - ret Test.1; diff --git a/compiler/test_mono/generated/list_append_closure.txt b/compiler/test_mono/generated/list_append_closure.txt deleted file mode 100644 index 80d4aae9c5..0000000000 --- a/compiler/test_mono/generated/list_append_closure.txt +++ /dev/null @@ -1,13 +0,0 @@ -procedure List.4 (#Attr.2, #Attr.3): - let List.139 : List I64 = lowlevel ListAppend #Attr.2 #Attr.3; - ret List.139; - -procedure Test.1 (Test.2): - let Test.6 : I64 = 42i64; - let Test.5 : List I64 = CallByName List.4 Test.2 Test.6; - ret Test.5; - -procedure Test.0 (): - let Test.4 : List I64 = Array [1i64, 2i64]; - let Test.3 : List I64 = CallByName Test.1 Test.4; - ret Test.3; diff --git a/compiler/test_mono/generated/list_cannot_update_inplace.txt b/compiler/test_mono/generated/list_cannot_update_inplace.txt deleted file mode 100644 index 273525877f..0000000000 --- a/compiler/test_mono/generated/list_cannot_update_inplace.txt +++ /dev/null @@ -1,45 +0,0 @@ -procedure List.3 (List.63, List.64, List.65): - let List.142 : {List I64, I64} = CallByName List.57 List.63 List.64 List.65; - let List.141 : List I64 = StructAtIndex 0 List.142; - inc List.141; - dec List.142; - ret List.141; - -procedure List.57 (#Attr.2, #Attr.3, #Attr.4): - let List.147 : U64 = lowlevel ListLen #Attr.2; - let List.145 : Int1 = lowlevel NumLt #Attr.3 List.147; - if List.145 then - let List.146 : {List I64, I64} = lowlevel ListReplaceUnsafe #Attr.2 #Attr.3 #Attr.4; - ret List.146; - else - let List.144 : {List I64, I64} = Struct {#Attr.2, #Attr.4}; - ret List.144; - -procedure List.6 (#Attr.2): - let List.140 : U64 = lowlevel ListLen #Attr.2; - ret List.140; - -procedure Num.19 (#Attr.2, #Attr.3): - let Num.273 : U64 = lowlevel NumAdd #Attr.2 #Attr.3; - ret Num.273; - -procedure Test.1 (): - let Test.8 : List I64 = Array [1i64, 2i64, 3i64]; - ret Test.8; - -procedure Test.2 (Test.3): - let Test.12 : U64 = 0i64; - let Test.13 : I64 = 0i64; - let Test.11 : List I64 = CallByName List.3 Test.3 Test.12 Test.13; - ret Test.11; - -procedure Test.0 (): - let Test.10 : List I64 = CallByName Test.1; - let Test.9 : List I64 = CallByName Test.2 Test.10; - let Test.5 : U64 = CallByName List.6 Test.9; - dec Test.9; - let Test.7 : List I64 = CallByName Test.1; - let Test.6 : U64 = CallByName List.6 Test.7; - dec Test.7; - let Test.4 : U64 = CallByName Num.19 Test.5 Test.6; - ret Test.4; diff --git a/compiler/test_mono/generated/list_get.txt b/compiler/test_mono/generated/list_get.txt deleted file mode 100644 index 7e5e29fff8..0000000000 --- a/compiler/test_mono/generated/list_get.txt +++ /dev/null @@ -1,23 +0,0 @@ -procedure List.2 (#Attr.2, #Attr.3): - let List.144 : U64 = lowlevel ListLen #Attr.2; - let List.141 : Int1 = lowlevel NumLt #Attr.3 List.144; - if List.141 then - let List.143 : I64 = lowlevel ListGetUnsafe #Attr.2 #Attr.3; - let List.142 : [C {}, C I64] = Ok List.143; - ret List.142; - else - let List.140 : {} = Struct {}; - let List.139 : [C {}, C I64] = Err List.140; - ret List.139; - -procedure Test.1 (Test.2): - let Test.6 : List I64 = Array [1i64, 2i64, 3i64]; - let Test.7 : U64 = 0i64; - let Test.5 : [C {}, C I64] = CallByName List.2 Test.6 Test.7; - dec Test.6; - ret Test.5; - -procedure Test.0 (): - let Test.4 : {} = Struct {}; - let Test.3 : [C {}, C I64] = CallByName Test.1 Test.4; - ret Test.3; diff --git a/compiler/test_mono/generated/list_len.txt b/compiler/test_mono/generated/list_len.txt deleted file mode 100644 index b3c1b27a5b..0000000000 --- a/compiler/test_mono/generated/list_len.txt +++ /dev/null @@ -1,21 +0,0 @@ -procedure List.6 (#Attr.2): - let List.139 : U64 = lowlevel ListLen #Attr.2; - ret List.139; - -procedure List.6 (#Attr.2): - let List.140 : U64 = lowlevel ListLen #Attr.2; - ret List.140; - -procedure Num.19 (#Attr.2, #Attr.3): - let Num.273 : U64 = lowlevel NumAdd #Attr.2 #Attr.3; - ret Num.273; - -procedure Test.0 (): - let Test.7 : List I64 = Array [1i64, 2i64, 3i64]; - let Test.4 : U64 = CallByName List.6 Test.7; - dec Test.7; - let Test.6 : List Float64 = Array [1f64]; - let Test.5 : U64 = CallByName List.6 Test.6; - dec Test.6; - let Test.3 : U64 = CallByName Num.19 Test.4 Test.5; - ret Test.3; diff --git a/compiler/test_mono/generated/list_map_closure_borrows.txt b/compiler/test_mono/generated/list_map_closure_borrows.txt deleted file mode 100644 index e816d7c4a1..0000000000 --- a/compiler/test_mono/generated/list_map_closure_borrows.txt +++ /dev/null @@ -1,61 +0,0 @@ -procedure List.2 (#Attr.2, #Attr.3): - let List.145 : U64 = lowlevel ListLen #Attr.2; - let List.142 : Int1 = lowlevel NumLt #Attr.3 List.145; - if List.142 then - let List.144 : Str = lowlevel ListGetUnsafe #Attr.2 #Attr.3; - let List.143 : [C {}, C Str] = Ok List.144; - ret List.143; - else - let List.141 : {} = Struct {}; - let List.140 : [C {}, C Str] = Err List.141; - ret List.140; - -procedure List.5 (#Attr.2, #Attr.3): - let List.146 : List Str = lowlevel ListMap { xs: `#Attr.#arg1` } #Attr.2 Test.3 #Attr.3; - ret List.146; - -procedure Str.16 (#Attr.2, #Attr.3): - let Str.65 : Str = lowlevel StrRepeat #Attr.2 #Attr.3; - ret Str.65; - -procedure Str.3 (#Attr.2, #Attr.3): - let Str.66 : Str = lowlevel StrConcat #Attr.2 #Attr.3; - ret Str.66; - -procedure Test.1 (): - let Test.21 : Str = "lllllllllllllllllllllooooooooooong"; - let Test.22 : Str = "g"; - let Test.20 : Str = CallByName Str.3 Test.21 Test.22; - dec Test.22; - let Test.19 : List Str = Array [Test.20]; - ret Test.19; - -procedure Test.2 (): - let Test.15 : List Str = CallByName Test.1; - let Test.16 : {} = Struct {}; - let Test.14 : List Str = CallByName List.5 Test.15 Test.16; - dec Test.15; - ret Test.14; - -procedure Test.3 (Test.4): - let Test.18 : U64 = 2i64; - let Test.17 : Str = CallByName Str.16 Test.4 Test.18; - ret Test.17; - -procedure Test.0 (): - let Test.12 : List Str = CallByName Test.2; - let Test.13 : U64 = 0i64; - let Test.6 : [C {}, C Str] = CallByName List.2 Test.12 Test.13; - dec Test.12; - let Test.9 : U8 = 1i64; - let Test.10 : U8 = GetTagId Test.6; - let Test.11 : Int1 = lowlevel Eq Test.9 Test.10; - if Test.11 then - let Test.5 : Str = UnionAtIndex (Id 1) (Index 0) Test.6; - inc Test.5; - dec Test.6; - ret Test.5; - else - dec Test.6; - let Test.8 : Str = "Hello, World!\n"; - ret Test.8; diff --git a/compiler/test_mono/generated/list_map_closure_owns.txt b/compiler/test_mono/generated/list_map_closure_owns.txt deleted file mode 100644 index 446a193f6c..0000000000 --- a/compiler/test_mono/generated/list_map_closure_owns.txt +++ /dev/null @@ -1,60 +0,0 @@ -procedure List.2 (#Attr.2, #Attr.3): - let List.145 : U64 = lowlevel ListLen #Attr.2; - let List.142 : Int1 = lowlevel NumLt #Attr.3 List.145; - if List.142 then - let List.144 : Str = lowlevel ListGetUnsafe #Attr.2 #Attr.3; - let List.143 : [C {}, C Str] = Ok List.144; - ret List.143; - else - let List.141 : {} = Struct {}; - let List.140 : [C {}, C Str] = Err List.141; - ret List.140; - -procedure List.5 (#Attr.2, #Attr.3): - inc #Attr.2; - let List.146 : List Str = lowlevel ListMap { xs: `#Attr.#arg1` } #Attr.2 Test.3 #Attr.3; - decref #Attr.2; - ret List.146; - -procedure Str.3 (#Attr.2, #Attr.3): - let Str.66 : Str = lowlevel StrConcat #Attr.2 #Attr.3; - ret Str.66; - -procedure Test.1 (): - let Test.21 : Str = "lllllllllllllllllllllooooooooooong"; - let Test.22 : Str = "g"; - let Test.20 : Str = CallByName Str.3 Test.21 Test.22; - dec Test.22; - let Test.19 : List Str = Array [Test.20]; - ret Test.19; - -procedure Test.2 (): - let Test.15 : List Str = CallByName Test.1; - let Test.16 : {} = Struct {}; - let Test.14 : List Str = CallByName List.5 Test.15 Test.16; - dec Test.15; - ret Test.14; - -procedure Test.3 (Test.4): - let Test.18 : Str = "!"; - let Test.17 : Str = CallByName Str.3 Test.4 Test.18; - dec Test.18; - ret Test.17; - -procedure Test.0 (): - let Test.12 : List Str = CallByName Test.2; - let Test.13 : U64 = 0i64; - let Test.6 : [C {}, C Str] = CallByName List.2 Test.12 Test.13; - dec Test.12; - let Test.9 : U8 = 1i64; - let Test.10 : U8 = GetTagId Test.6; - let Test.11 : Int1 = lowlevel Eq Test.9 Test.10; - if Test.11 then - let Test.5 : Str = UnionAtIndex (Id 1) (Index 0) Test.6; - inc Test.5; - dec Test.6; - ret Test.5; - else - dec Test.6; - let Test.8 : Str = "Hello, World!\n"; - ret Test.8; diff --git a/compiler/test_mono/generated/list_pass_to_function.txt b/compiler/test_mono/generated/list_pass_to_function.txt deleted file mode 100644 index 05dcf76ea7..0000000000 --- a/compiler/test_mono/generated/list_pass_to_function.txt +++ /dev/null @@ -1,27 +0,0 @@ -procedure List.3 (List.63, List.64, List.65): - let List.140 : {List I64, I64} = CallByName List.57 List.63 List.64 List.65; - let List.139 : List I64 = StructAtIndex 0 List.140; - inc List.139; - dec List.140; - ret List.139; - -procedure List.57 (#Attr.2, #Attr.3, #Attr.4): - let List.145 : U64 = lowlevel ListLen #Attr.2; - let List.143 : Int1 = lowlevel NumLt #Attr.3 List.145; - if List.143 then - let List.144 : {List I64, I64} = lowlevel ListReplaceUnsafe #Attr.2 #Attr.3 #Attr.4; - ret List.144; - else - let List.142 : {List I64, I64} = Struct {#Attr.2, #Attr.4}; - ret List.142; - -procedure Test.2 (Test.3): - let Test.6 : U64 = 0i64; - let Test.7 : I64 = 0i64; - let Test.5 : List I64 = CallByName List.3 Test.3 Test.6 Test.7; - ret Test.5; - -procedure Test.0 (): - let Test.1 : List I64 = Array [1i64, 2i64, 3i64]; - let Test.4 : List I64 = CallByName Test.2 Test.1; - ret Test.4; diff --git a/compiler/test_mono/generated/list_sort_asc.txt b/compiler/test_mono/generated/list_sort_asc.txt deleted file mode 100644 index 50253ab644..0000000000 --- a/compiler/test_mono/generated/list_sort_asc.txt +++ /dev/null @@ -1,22 +0,0 @@ -procedure List.28 (#Attr.2, #Attr.3): - let List.143 : List I64 = lowlevel ListSortWith { xs: `#Attr.#arg1` } #Attr.2 Num.46 #Attr.3; - let Bool.14 : Int1 = lowlevel ListIsUnique #Attr.2; - if Bool.14 then - ret List.143; - else - decref #Attr.2; - ret List.143; - -procedure List.54 (List.98): - let List.141 : {} = Struct {}; - let List.140 : List I64 = CallByName List.28 List.98 List.141; - ret List.140; - -procedure Num.46 (#Attr.2, #Attr.3): - let Num.273 : U8 = lowlevel NumCompare #Attr.2 #Attr.3; - ret Num.273; - -procedure Test.0 (): - let Test.2 : List I64 = Array [4i64, 3i64, 2i64, 1i64]; - let Test.1 : List I64 = CallByName List.54 Test.2; - ret Test.1; diff --git a/compiler/test_mono/generated/monomorphized_applied_tag.txt b/compiler/test_mono/generated/monomorphized_applied_tag.txt deleted file mode 100644 index 4e672c7597..0000000000 --- a/compiler/test_mono/generated/monomorphized_applied_tag.txt +++ /dev/null @@ -1,20 +0,0 @@ -procedure Test.2 (Test.4): - let Test.11 : U8 = 0i64; - let Test.12 : U8 = GetTagId Test.4; - let Test.13 : Int1 = lowlevel Eq Test.11 Test.12; - if Test.13 then - let Test.5 : Str = UnionAtIndex (Id 0) (Index 0) Test.4; - inc Test.5; - dec Test.4; - ret Test.5; - else - let Test.6 : Str = UnionAtIndex (Id 1) (Index 0) Test.4; - inc Test.6; - dec Test.4; - ret Test.6; - -procedure Test.0 (): - let Test.14 : Str = "A"; - let Test.8 : [C Str, C Str] = A Test.14; - let Test.7 : Str = CallByName Test.2 Test.8; - ret Test.7; diff --git a/compiler/test_mono/generated/monomorphized_floats.txt b/compiler/test_mono/generated/monomorphized_floats.txt deleted file mode 100644 index 6fc9243f46..0000000000 --- a/compiler/test_mono/generated/monomorphized_floats.txt +++ /dev/null @@ -1,9 +0,0 @@ -procedure Test.2 (Test.3, Test.4): - let Test.8 : U64 = 18i64; - ret Test.8; - -procedure Test.0 (): - let Test.6 : Float32 = 100f64; - let Test.7 : Float64 = 100f64; - let Test.5 : U64 = CallByName Test.2 Test.6 Test.7; - ret Test.5; diff --git a/compiler/test_mono/generated/monomorphized_ints.txt b/compiler/test_mono/generated/monomorphized_ints.txt deleted file mode 100644 index 408f1ff44b..0000000000 --- a/compiler/test_mono/generated/monomorphized_ints.txt +++ /dev/null @@ -1,9 +0,0 @@ -procedure Test.2 (Test.3, Test.4): - let Test.8 : U64 = 18i64; - ret Test.8; - -procedure Test.0 (): - let Test.6 : U8 = 100i64; - let Test.7 : U32 = 100i64; - let Test.5 : U64 = CallByName Test.2 Test.6 Test.7; - ret Test.5; diff --git a/compiler/test_mono/generated/monomorphized_ints_aliased.txt b/compiler/test_mono/generated/monomorphized_ints_aliased.txt deleted file mode 100644 index d9433bf0af..0000000000 --- a/compiler/test_mono/generated/monomorphized_ints_aliased.txt +++ /dev/null @@ -1,21 +0,0 @@ -procedure Num.19 (#Attr.2, #Attr.3): - let Num.273 : U64 = lowlevel NumAdd #Attr.2 #Attr.3; - ret Num.273; - -procedure Test.5 (Test.7, Test.8): - let Test.17 : U64 = 1i64; - ret Test.17; - -procedure Test.6 (Test.7, Test.8): - let Test.14 : U64 = 1i64; - ret Test.14; - -procedure Test.0 (): - let Test.15 : U8 = 100i64; - let Test.16 : U32 = 100i64; - let Test.10 : U64 = CallByName Test.5 Test.15 Test.16; - let Test.12 : U32 = 100i64; - let Test.13 : U8 = 100i64; - let Test.11 : U64 = CallByName Test.6 Test.12 Test.13; - let Test.9 : U64 = CallByName Num.19 Test.10 Test.11; - ret Test.9; diff --git a/compiler/test_mono/generated/monomorphized_list.txt b/compiler/test_mono/generated/monomorphized_list.txt deleted file mode 100644 index 813339d654..0000000000 --- a/compiler/test_mono/generated/monomorphized_list.txt +++ /dev/null @@ -1,11 +0,0 @@ -procedure Test.2 (Test.3, Test.4): - let Test.8 : U64 = 18i64; - ret Test.8; - -procedure Test.0 (): - let Test.6 : List U8 = Array [1i64, 2i64, 3i64]; - let Test.7 : List U16 = Array [1i64, 2i64, 3i64]; - let Test.5 : U64 = CallByName Test.2 Test.6 Test.7; - dec Test.7; - dec Test.6; - ret Test.5; diff --git a/compiler/test_mono/generated/monomorphized_tag.txt b/compiler/test_mono/generated/monomorphized_tag.txt deleted file mode 100644 index 3e2dd8f0bb..0000000000 --- a/compiler/test_mono/generated/monomorphized_tag.txt +++ /dev/null @@ -1,9 +0,0 @@ -procedure Test.2 (Test.4, Test.5): - let Test.9 : U8 = 18i64; - ret Test.9; - -procedure Test.0 (): - let Test.7 : Int1 = false; - let Test.8 : U8 = 0u8; - let Test.6 : U8 = CallByName Test.2 Test.7 Test.8; - ret Test.6; diff --git a/compiler/test_mono/generated/monomorphized_tag_with_aliased_args.txt b/compiler/test_mono/generated/monomorphized_tag_with_aliased_args.txt deleted file mode 100644 index 5126202c19..0000000000 --- a/compiler/test_mono/generated/monomorphized_tag_with_aliased_args.txt +++ /dev/null @@ -1,10 +0,0 @@ -procedure Test.4 (Test.8): - let Test.11 : U64 = 1i64; - ret Test.11; - -procedure Test.0 (): - let Test.13 : Int1 = false; - let Test.12 : Int1 = false; - let Test.10 : {Int1, Int1} = Struct {Test.12, Test.13}; - let Test.9 : U64 = CallByName Test.4 Test.10; - ret Test.9; diff --git a/compiler/test_mono/generated/nested_closure.txt b/compiler/test_mono/generated/nested_closure.txt deleted file mode 100644 index fe4e104d86..0000000000 --- a/compiler/test_mono/generated/nested_closure.txt +++ /dev/null @@ -1,16 +0,0 @@ -procedure Test.1 (Test.5): - let Test.2 : I64 = 42i64; - let Test.3 : {I64} = Struct {Test.2}; - ret Test.3; - -procedure Test.3 (Test.10, #Attr.12): - let Test.2 : I64 = StructAtIndex 0 #Attr.12; - let Test.2 : I64 = 42i64; - ret Test.2; - -procedure Test.0 (): - let Test.7 : {} = Struct {}; - let Test.9 : {} = Struct {}; - let Test.8 : {I64} = CallByName Test.1 Test.9; - let Test.6 : I64 = CallByName Test.3 Test.7 Test.8; - ret Test.6; diff --git a/compiler/test_mono/generated/opaque_assign_to_symbol.txt b/compiler/test_mono/generated/opaque_assign_to_symbol.txt deleted file mode 100644 index 23eee2ceea..0000000000 --- a/compiler/test_mono/generated/opaque_assign_to_symbol.txt +++ /dev/null @@ -1,8 +0,0 @@ -procedure Test.3 (Test.4): - let Test.8 : [C {}, C U8] = Ok Test.4; - ret Test.8; - -procedure Test.0 (): - let Test.7 : U8 = 98i64; - let Test.6 : [C {}, C U8] = CallByName Test.3 Test.7; - ret Test.6; diff --git a/compiler/test_mono/generated/optional_when.txt b/compiler/test_mono/generated/optional_when.txt deleted file mode 100644 index 7eaa7288b5..0000000000 --- a/compiler/test_mono/generated/optional_when.txt +++ /dev/null @@ -1,42 +0,0 @@ -procedure Num.21 (#Attr.2, #Attr.3): - let Num.275 : I64 = lowlevel NumMul #Attr.2 #Attr.3; - ret Num.275; - -procedure Test.1 (Test.6): - let Test.21 : Int1 = false; - let Test.22 : Int1 = lowlevel Eq Test.21 Test.6; - if Test.22 then - let Test.8 : I64 = 3i64; - ret Test.8; - else - let Test.10 : I64 = 5i64; - ret Test.10; - -procedure Test.1 (Test.6): - let Test.29 : Int1 = StructAtIndex 1 Test.6; - let Test.30 : Int1 = false; - let Test.31 : Int1 = lowlevel Eq Test.30 Test.29; - if Test.31 then - let Test.8 : I64 = StructAtIndex 0 Test.6; - ret Test.8; - else - let Test.10 : I64 = StructAtIndex 0 Test.6; - ret Test.10; - -procedure Test.0 (): - let Test.39 : I64 = 7i64; - let Test.40 : Int1 = false; - let Test.38 : {I64, Int1} = Struct {Test.39, Test.40}; - let Test.34 : I64 = CallByName Test.1 Test.38; - let Test.37 : Int1 = false; - let Test.35 : I64 = CallByName Test.1 Test.37; - let Test.24 : I64 = CallByName Num.21 Test.34 Test.35; - let Test.32 : I64 = 11i64; - let Test.33 : Int1 = true; - let Test.26 : {I64, Int1} = Struct {Test.32, Test.33}; - let Test.25 : I64 = CallByName Test.1 Test.26; - let Test.16 : I64 = CallByName Num.21 Test.24 Test.25; - let Test.23 : Int1 = true; - let Test.17 : I64 = CallByName Test.1 Test.23; - let Test.15 : I64 = CallByName Num.21 Test.16 Test.17; - ret Test.15; diff --git a/compiler/test_mono/generated/peano.txt b/compiler/test_mono/generated/peano.txt deleted file mode 100644 index 8b7370a838..0000000000 --- a/compiler/test_mono/generated/peano.txt +++ /dev/null @@ -1,6 +0,0 @@ -procedure Test.0 (): - let Test.11 : [, C *self] = Z ; - let Test.10 : [, C *self] = S Test.11; - let Test.9 : [, C *self] = S Test.10; - let Test.3 : [, C *self] = S Test.9; - ret Test.3; diff --git a/compiler/test_mono/generated/peano1.txt b/compiler/test_mono/generated/peano1.txt deleted file mode 100644 index 63db968efc..0000000000 --- a/compiler/test_mono/generated/peano1.txt +++ /dev/null @@ -1,15 +0,0 @@ -procedure Test.0 (): - let Test.15 : [, C *self] = Z ; - let Test.14 : [, C *self] = S Test.15; - let Test.13 : [, C *self] = S Test.14; - let Test.3 : [, C *self] = S Test.13; - let Test.10 : Int1 = 1i64; - let Test.11 : Int1 = GetTagId Test.3; - dec Test.3; - let Test.12 : Int1 = lowlevel Eq Test.10 Test.11; - if Test.12 then - let Test.8 : I64 = 0i64; - ret Test.8; - else - let Test.9 : I64 = 1i64; - ret Test.9; diff --git a/compiler/test_mono/generated/peano2.txt b/compiler/test_mono/generated/peano2.txt deleted file mode 100644 index 73dd42687a..0000000000 --- a/compiler/test_mono/generated/peano2.txt +++ /dev/null @@ -1,26 +0,0 @@ -procedure Test.0 (): - let Test.21 : [, C *self] = Z ; - let Test.20 : [, C *self] = S Test.21; - let Test.19 : [, C *self] = S Test.20; - let Test.3 : [, C *self] = S Test.19; - let Test.16 : Int1 = 0i64; - let Test.17 : Int1 = GetTagId Test.3; - let Test.18 : Int1 = lowlevel Eq Test.16 Test.17; - if Test.18 then - let Test.12 : [, C *self] = UnionAtIndex (Id 0) (Index 0) Test.3; - inc Test.12; - dec Test.3; - let Test.13 : Int1 = 0i64; - let Test.14 : Int1 = GetTagId Test.12; - dec Test.12; - let Test.15 : Int1 = lowlevel Eq Test.13 Test.14; - if Test.15 then - let Test.8 : I64 = 1i64; - ret Test.8; - else - let Test.9 : I64 = 0i64; - ret Test.9; - else - dec Test.3; - let Test.10 : I64 = 0i64; - ret Test.10; diff --git a/compiler/test_mono/generated/quicksort_help.txt b/compiler/test_mono/generated/quicksort_help.txt deleted file mode 100644 index 2c00aa804b..0000000000 --- a/compiler/test_mono/generated/quicksort_help.txt +++ /dev/null @@ -1,41 +0,0 @@ -procedure Num.19 (#Attr.2, #Attr.3): - let Num.273 : I64 = lowlevel NumAdd #Attr.2 #Attr.3; - ret Num.273; - -procedure Num.20 (#Attr.2, #Attr.3): - let Num.274 : I64 = lowlevel NumSub #Attr.2 #Attr.3; - ret Num.274; - -procedure Num.22 (#Attr.2, #Attr.3): - let Num.275 : Int1 = lowlevel NumLt #Attr.2 #Attr.3; - ret Num.275; - -procedure Test.1 (Test.24, Test.25, Test.26): - joinpoint Test.12 Test.2 Test.3 Test.4: - let Test.14 : Int1 = CallByName Num.22 Test.3 Test.4; - if Test.14 then - dec Test.2; - let Test.23 : List [] = Array []; - let Test.22 : I64 = 0i64; - let Test.21 : {I64, List []} = Struct {Test.22, Test.23}; - let Test.5 : I64 = StructAtIndex 0 Test.21; - let Test.6 : List [] = StructAtIndex 1 Test.21; - inc Test.6; - dec Test.21; - let Test.20 : I64 = 1i64; - let Test.19 : I64 = CallByName Num.20 Test.5 Test.20; - let Test.16 : List I64 = CallByName Test.1 Test.6 Test.3 Test.19; - let Test.18 : I64 = 1i64; - let Test.17 : I64 = CallByName Num.19 Test.5 Test.18; - jump Test.12 Test.16 Test.17 Test.4; - else - ret Test.2; - in - jump Test.12 Test.24 Test.25 Test.26; - -procedure Test.0 (): - let Test.9 : List I64 = Array []; - let Test.10 : I64 = 0i64; - let Test.11 : I64 = 0i64; - let Test.8 : List I64 = CallByName Test.1 Test.9 Test.10 Test.11; - ret Test.8; diff --git a/compiler/test_mono/generated/quicksort_swap.txt b/compiler/test_mono/generated/quicksort_swap.txt deleted file mode 100644 index e435934819..0000000000 --- a/compiler/test_mono/generated/quicksort_swap.txt +++ /dev/null @@ -1,69 +0,0 @@ -procedure List.2 (#Attr.2, #Attr.3): - let List.156 : U64 = lowlevel ListLen #Attr.2; - let List.153 : Int1 = lowlevel NumLt #Attr.3 List.156; - if List.153 then - let List.155 : I64 = lowlevel ListGetUnsafe #Attr.2 #Attr.3; - let List.154 : [C {}, C I64] = Ok List.155; - ret List.154; - else - let List.152 : {} = Struct {}; - let List.151 : [C {}, C I64] = Err List.152; - ret List.151; - -procedure List.3 (List.63, List.64, List.65): - let List.143 : {List I64, I64} = CallByName List.57 List.63 List.64 List.65; - let List.142 : List I64 = StructAtIndex 0 List.143; - inc List.142; - dec List.143; - ret List.142; - -procedure List.57 (#Attr.2, #Attr.3, #Attr.4): - let List.160 : U64 = lowlevel ListLen #Attr.2; - let List.158 : Int1 = lowlevel NumLt #Attr.3 List.160; - if List.158 then - let List.159 : {List I64, I64} = lowlevel ListReplaceUnsafe #Attr.2 #Attr.3 #Attr.4; - ret List.159; - else - let List.157 : {List I64, I64} = Struct {#Attr.2, #Attr.4}; - ret List.157; - -procedure Test.1 (Test.2): - let Test.28 : U64 = 0i64; - let Test.26 : [C {}, C I64] = CallByName List.2 Test.2 Test.28; - let Test.27 : U64 = 0i64; - let Test.25 : [C {}, C I64] = CallByName List.2 Test.2 Test.27; - let Test.8 : {[C {}, C I64], [C {}, C I64]} = Struct {Test.25, Test.26}; - joinpoint Test.22: - let Test.13 : List I64 = Array []; - ret Test.13; - in - let Test.19 : [C {}, C I64] = StructAtIndex 1 Test.8; - let Test.20 : U8 = 1i64; - let Test.21 : U8 = GetTagId Test.19; - let Test.24 : Int1 = lowlevel Eq Test.20 Test.21; - if Test.24 then - let Test.16 : [C {}, C I64] = StructAtIndex 0 Test.8; - let Test.17 : U8 = 1i64; - let Test.18 : U8 = GetTagId Test.16; - let Test.23 : Int1 = lowlevel Eq Test.17 Test.18; - if Test.23 then - let Test.15 : [C {}, C I64] = StructAtIndex 0 Test.8; - let Test.4 : I64 = UnionAtIndex (Id 1) (Index 0) Test.15; - let Test.14 : [C {}, C I64] = StructAtIndex 1 Test.8; - let Test.5 : I64 = UnionAtIndex (Id 1) (Index 0) Test.14; - let Test.12 : U64 = 0i64; - let Test.10 : List I64 = CallByName List.3 Test.2 Test.12 Test.5; - let Test.11 : U64 = 0i64; - let Test.9 : List I64 = CallByName List.3 Test.10 Test.11 Test.4; - ret Test.9; - else - dec Test.2; - jump Test.22; - else - dec Test.2; - jump Test.22; - -procedure Test.0 (): - let Test.7 : List I64 = Array [1i64, 2i64]; - let Test.6 : List I64 = CallByName Test.1 Test.7; - ret Test.6; diff --git a/compiler/test_mono/generated/record_optional_field_function_use_default.txt b/compiler/test_mono/generated/record_optional_field_function_use_default.txt deleted file mode 100644 index 6adb8079f7..0000000000 --- a/compiler/test_mono/generated/record_optional_field_function_use_default.txt +++ /dev/null @@ -1,13 +0,0 @@ -procedure Num.19 (#Attr.2, #Attr.3): - let Num.273 : I64 = lowlevel NumAdd #Attr.2 #Attr.3; - ret Num.273; - -procedure Test.1 (Test.4): - let Test.8 : I64 = 10i64; - let Test.7 : I64 = CallByName Num.19 Test.8 Test.4; - ret Test.7; - -procedure Test.0 (): - let Test.9 : I64 = 9i64; - let Test.5 : I64 = CallByName Test.1 Test.9; - ret Test.5; diff --git a/compiler/test_mono/generated/rigids.txt b/compiler/test_mono/generated/rigids.txt deleted file mode 100644 index 7aab37c374..0000000000 --- a/compiler/test_mono/generated/rigids.txt +++ /dev/null @@ -1,67 +0,0 @@ -procedure List.2 (#Attr.2, #Attr.3): - let List.156 : U64 = lowlevel ListLen #Attr.2; - let List.153 : Int1 = lowlevel NumLt #Attr.3 List.156; - if List.153 then - let List.155 : I64 = lowlevel ListGetUnsafe #Attr.2 #Attr.3; - let List.154 : [C {}, C I64] = Ok List.155; - ret List.154; - else - let List.152 : {} = Struct {}; - let List.151 : [C {}, C I64] = Err List.152; - ret List.151; - -procedure List.3 (List.63, List.64, List.65): - let List.143 : {List I64, I64} = CallByName List.57 List.63 List.64 List.65; - let List.142 : List I64 = StructAtIndex 0 List.143; - inc List.142; - dec List.143; - ret List.142; - -procedure List.57 (#Attr.2, #Attr.3, #Attr.4): - let List.160 : U64 = lowlevel ListLen #Attr.2; - let List.158 : Int1 = lowlevel NumLt #Attr.3 List.160; - if List.158 then - let List.159 : {List I64, I64} = lowlevel ListReplaceUnsafe #Attr.2 #Attr.3 #Attr.4; - ret List.159; - else - let List.157 : {List I64, I64} = Struct {#Attr.2, #Attr.4}; - ret List.157; - -procedure Test.1 (Test.2, Test.3, Test.4): - let Test.29 : [C {}, C I64] = CallByName List.2 Test.4 Test.3; - let Test.28 : [C {}, C I64] = CallByName List.2 Test.4 Test.2; - let Test.13 : {[C {}, C I64], [C {}, C I64]} = Struct {Test.28, Test.29}; - joinpoint Test.25: - let Test.16 : List I64 = Array []; - ret Test.16; - in - let Test.22 : [C {}, C I64] = StructAtIndex 1 Test.13; - let Test.23 : U8 = 1i64; - let Test.24 : U8 = GetTagId Test.22; - let Test.27 : Int1 = lowlevel Eq Test.23 Test.24; - if Test.27 then - let Test.19 : [C {}, C I64] = StructAtIndex 0 Test.13; - let Test.20 : U8 = 1i64; - let Test.21 : U8 = GetTagId Test.19; - let Test.26 : Int1 = lowlevel Eq Test.20 Test.21; - if Test.26 then - let Test.18 : [C {}, C I64] = StructAtIndex 0 Test.13; - let Test.6 : I64 = UnionAtIndex (Id 1) (Index 0) Test.18; - let Test.17 : [C {}, C I64] = StructAtIndex 1 Test.13; - let Test.7 : I64 = UnionAtIndex (Id 1) (Index 0) Test.17; - let Test.15 : List I64 = CallByName List.3 Test.4 Test.2 Test.7; - let Test.14 : List I64 = CallByName List.3 Test.15 Test.3 Test.6; - ret Test.14; - else - dec Test.4; - jump Test.25; - else - dec Test.4; - jump Test.25; - -procedure Test.0 (): - let Test.10 : U64 = 0i64; - let Test.11 : U64 = 0i64; - let Test.12 : List I64 = Array [1i64]; - let Test.9 : List I64 = CallByName Test.1 Test.10 Test.11 Test.12; - ret Test.9; diff --git a/compiler/test_mono/generated/simple_if.txt b/compiler/test_mono/generated/simple_if.txt deleted file mode 100644 index 04a1917375..0000000000 --- a/compiler/test_mono/generated/simple_if.txt +++ /dev/null @@ -1,8 +0,0 @@ -procedure Test.0 (): - let Test.3 : Int1 = true; - if Test.3 then - let Test.4 : I64 = 1i64; - ret Test.4; - else - let Test.2 : I64 = 2i64; - ret Test.2; diff --git a/compiler/test_mono/generated/somehow_drops_definitions.txt b/compiler/test_mono/generated/somehow_drops_definitions.txt deleted file mode 100644 index 1ac4ec978e..0000000000 --- a/compiler/test_mono/generated/somehow_drops_definitions.txt +++ /dev/null @@ -1,53 +0,0 @@ -procedure Num.19 (#Attr.2, #Attr.3): - let Num.274 : I64 = lowlevel NumAdd #Attr.2 #Attr.3; - ret Num.274; - -procedure Num.21 (#Attr.2, #Attr.3): - let Num.273 : I64 = lowlevel NumMul #Attr.2 #Attr.3; - ret Num.273; - -procedure Test.1 (): - let Test.26 : I64 = 1i64; - ret Test.26; - -procedure Test.2 (): - let Test.22 : I64 = 2i64; - ret Test.22; - -procedure Test.3 (Test.6): - let Test.25 : I64 = CallByName Test.1; - let Test.24 : I64 = CallByName Num.19 Test.6 Test.25; - ret Test.24; - -procedure Test.4 (Test.7): - let Test.21 : I64 = CallByName Test.2; - let Test.20 : I64 = CallByName Num.21 Test.7 Test.21; - ret Test.20; - -procedure Test.5 (Test.8, Test.9): - joinpoint Test.15 Test.14: - ret Test.14; - in - switch Test.8: - case 0: - let Test.16 : I64 = CallByName Test.3 Test.9; - jump Test.15 Test.16; - - default: - let Test.17 : I64 = CallByName Test.4 Test.9; - jump Test.15 Test.17; - - -procedure Test.0 (): - joinpoint Test.19 Test.12: - let Test.13 : I64 = 42i64; - let Test.11 : I64 = CallByName Test.5 Test.12 Test.13; - ret Test.11; - in - let Test.23 : Int1 = true; - if Test.23 then - let Test.3 : Int1 = false; - jump Test.19 Test.3; - else - let Test.4 : Int1 = true; - jump Test.19 Test.4; diff --git a/compiler/test_mono/generated/specialize_ability_call.txt b/compiler/test_mono/generated/specialize_ability_call.txt deleted file mode 100644 index ebc12cb8bf..0000000000 --- a/compiler/test_mono/generated/specialize_ability_call.txt +++ /dev/null @@ -1,7 +0,0 @@ -procedure Test.7 (Test.9): - ret Test.9; - -procedure Test.0 (): - let Test.11 : U64 = 1234i64; - let Test.10 : U64 = CallByName Test.7 Test.11; - ret Test.10; diff --git a/compiler/test_mono/generated/specialize_closures.txt b/compiler/test_mono/generated/specialize_closures.txt deleted file mode 100644 index 251e80285d..0000000000 --- a/compiler/test_mono/generated/specialize_closures.txt +++ /dev/null @@ -1,53 +0,0 @@ -procedure Num.19 (#Attr.2, #Attr.3): - let Num.274 : I64 = lowlevel NumAdd #Attr.2 #Attr.3; - ret Num.274; - -procedure Num.21 (#Attr.2, #Attr.3): - let Num.273 : I64 = lowlevel NumMul #Attr.2 #Attr.3; - ret Num.273; - -procedure Test.1 (Test.2, Test.3): - let Test.17 : U8 = GetTagId Test.2; - joinpoint Test.18 Test.16: - ret Test.16; - in - switch Test.17: - case 0: - let Test.19 : I64 = CallByName Test.7 Test.3 Test.2; - jump Test.18 Test.19; - - default: - let Test.20 : I64 = CallByName Test.8 Test.3 Test.2; - jump Test.18 Test.20; - - -procedure Test.7 (Test.10, #Attr.12): - let Test.4 : I64 = UnionAtIndex (Id 0) (Index 0) #Attr.12; - let Test.26 : I64 = CallByName Num.19 Test.10 Test.4; - ret Test.26; - -procedure Test.8 (Test.11, #Attr.12): - let Test.6 : Int1 = UnionAtIndex (Id 1) (Index 1) #Attr.12; - let Test.5 : I64 = UnionAtIndex (Id 1) (Index 0) #Attr.12; - if Test.6 then - let Test.24 : I64 = CallByName Num.21 Test.11 Test.5; - ret Test.24; - else - ret Test.11; - -procedure Test.0 (): - let Test.4 : I64 = 1i64; - let Test.5 : I64 = 2i64; - let Test.6 : Int1 = true; - joinpoint Test.22 Test.14: - let Test.15 : I64 = 42i64; - let Test.13 : I64 = CallByName Test.1 Test.14 Test.15; - ret Test.13; - in - let Test.25 : Int1 = true; - if Test.25 then - let Test.7 : [C I64, C I64 Int1] = ClosureTag(Test.7) Test.4; - jump Test.22 Test.7; - else - let Test.8 : [C I64, C I64 Int1] = ClosureTag(Test.8) Test.5 Test.6; - jump Test.22 Test.8; diff --git a/compiler/test_mono/generated/specialize_lowlevel.txt b/compiler/test_mono/generated/specialize_lowlevel.txt deleted file mode 100644 index 74bc5c3bbb..0000000000 --- a/compiler/test_mono/generated/specialize_lowlevel.txt +++ /dev/null @@ -1,44 +0,0 @@ -procedure Num.19 (#Attr.2, #Attr.3): - let Num.274 : I64 = lowlevel NumAdd #Attr.2 #Attr.3; - ret Num.274; - -procedure Num.21 (#Attr.2, #Attr.3): - let Num.273 : I64 = lowlevel NumMul #Attr.2 #Attr.3; - ret Num.273; - -procedure Test.6 (Test.8, #Attr.12): - let Test.4 : I64 = UnionAtIndex (Id 0) (Index 0) #Attr.12; - let Test.22 : I64 = CallByName Num.19 Test.8 Test.4; - ret Test.22; - -procedure Test.7 (Test.9, #Attr.12): - let Test.5 : I64 = UnionAtIndex (Id 1) (Index 0) #Attr.12; - let Test.20 : I64 = CallByName Num.21 Test.9 Test.5; - ret Test.20; - -procedure Test.0 (): - let Test.4 : I64 = 1i64; - let Test.5 : I64 = 2i64; - let Test.12 : I64 = 42i64; - joinpoint Test.19 Test.13: - let Test.14 : U8 = GetTagId Test.13; - joinpoint Test.15 Test.11: - ret Test.11; - in - switch Test.14: - case 0: - let Test.16 : I64 = CallByName Test.6 Test.12 Test.13; - jump Test.15 Test.16; - - default: - let Test.17 : I64 = CallByName Test.7 Test.12 Test.13; - jump Test.15 Test.17; - - in - let Test.21 : Int1 = true; - if Test.21 then - let Test.6 : [C I64, C I64] = ClosureTag(Test.6) Test.4; - jump Test.19 Test.6; - else - let Test.7 : [C I64, C I64] = ClosureTag(Test.7) Test.5; - jump Test.19 Test.7; diff --git a/compiler/test_mono/generated/when_on_record.txt b/compiler/test_mono/generated/when_on_record.txt deleted file mode 100644 index 71fec29389..0000000000 --- a/compiler/test_mono/generated/when_on_record.txt +++ /dev/null @@ -1,9 +0,0 @@ -procedure Num.19 (#Attr.2, #Attr.3): - let Num.273 : I64 = lowlevel NumAdd #Attr.2 #Attr.3; - ret Num.273; - -procedure Test.0 (): - let Test.5 : I64 = 2i64; - let Test.4 : I64 = 3i64; - let Test.3 : I64 = CallByName Num.19 Test.5 Test.4; - ret Test.3; diff --git a/compiler/test_mono/src/tests.rs b/compiler/test_mono/src/tests.rs deleted file mode 100644 index 11c6ad52be..0000000000 --- a/compiler/test_mono/src/tests.rs +++ /dev/null @@ -1,1443 +0,0 @@ -#![cfg(test)] -#![warn(clippy::dbg_macro)] -// See github.com/rtfeldman/roc/issues/800 for discussion of the large_enum_variant check. -#![allow(clippy::large_enum_variant)] -// we actually want to compare against the literal float bits -#![allow(clippy::float_cmp)] - -#[macro_use] -extern crate indoc; - -/// Used in the with_larger_debug_stack() function, for tests that otherwise -/// run out of stack space in debug builds (but don't in --release builds) -#[allow(dead_code)] -const EXPANDED_STACK_SIZE: usize = 8 * 1024 * 1024; - -use test_mono_macros::*; - -use roc_collections::all::MutMap; -use roc_load::Threading; -use roc_module::symbol::Symbol; -use roc_mono::ir::Proc; -use roc_mono::ir::ProcLayout; - -const TARGET_INFO: roc_target::TargetInfo = roc_target::TargetInfo::default_x86_64(); - -/// Without this, some tests pass in `cargo test --release` but fail without -/// the --release flag because they run out of stack space. This increases -/// stack size for debug builds only, while leaving the stack space at the default -/// amount for release builds. -#[allow(dead_code)] -#[cfg(debug_assertions)] -pub fn with_larger_debug_stack(run_test: F) -where - F: FnOnce(), - F: Send, - F: 'static, -{ - std::thread::Builder::new() - .stack_size(EXPANDED_STACK_SIZE) - .spawn(run_test) - .expect("Error while spawning expanded dev stack size thread") - .join() - .expect("Error while joining expanded dev stack size thread") -} - -/// In --release builds, don't increase the stack size. Run the test normally. -/// This way, we find out if any of our tests are blowing the stack even after -/// optimizations in release builds. -#[allow(dead_code)] -#[cfg(not(debug_assertions))] -#[inline(always)] -pub fn with_larger_debug_stack(run_test: F) -where - F: FnOnce(), - F: Send, - F: 'static, -{ - run_test() -} - -fn promote_expr_to_module(src: &str) -> String { - let mut buffer = String::from("app \"test\" provides [main] to \"./platform\"\n\nmain =\n"); - - for line in src.lines() { - // indent the body! - buffer.push_str(" "); - buffer.push_str(line); - buffer.push('\n'); - } - - buffer -} - -fn compiles_to_ir(test_name: &str, src: &str) { - use bumpalo::Bump; - use std::path::{Path, PathBuf}; - - let arena = &Bump::new(); - - let filename = PathBuf::from("Test.roc"); - let src_dir = Path::new("fake/test/path"); - - let module_src; - let temp; - if src.starts_with("app") { - // this is already a module - module_src = src; - } else { - // this is an expression, promote it to a module - temp = promote_expr_to_module(src); - module_src = &temp; - } - - let loaded = roc_load::load_and_monomorphize_from_str( - arena, - filename, - module_src, - src_dir, - Default::default(), - TARGET_INFO, - roc_reporting::report::RenderTarget::Generic, - Threading::Single, - ); - - let mut loaded = match loaded { - Ok(x) => x, - Err(roc_load::LoadingProblem::FormattedReport(report)) => { - println!("{}", report); - panic!(); - } - Err(e) => panic!("{:?}", e), - }; - - use roc_load::MonomorphizedModule; - let MonomorphizedModule { - module_id: home, - procedures, - exposed_to_host, - .. - } = loaded; - - let can_problems = loaded.can_problems.remove(&home).unwrap_or_default(); - let type_problems = loaded.type_problems.remove(&home).unwrap_or_default(); - - if !can_problems.is_empty() { - println!("Ignoring {} canonicalization problems", can_problems.len()); - } - - assert!(type_problems.is_empty()); - - debug_assert_eq!(exposed_to_host.values.len(), 1); - - let main_fn_symbol = exposed_to_host.values.keys().copied().next().unwrap(); - - verify_procedures(test_name, procedures, main_fn_symbol); -} - -#[cfg(debug_assertions)] -fn verify_procedures( - test_name: &str, - procedures: MutMap<(Symbol, ProcLayout<'_>), Proc<'_>>, - main_fn_symbol: Symbol, -) { - let index = procedures - .keys() - .position(|(s, _)| *s == main_fn_symbol) - .unwrap(); - - let mut procs_string = procedures - .values() - .map(|proc| proc.to_pretty(200)) - .collect::>(); - - let main_fn = procs_string.swap_remove(index); - - procs_string.sort(); - procs_string.push(main_fn); - - let result = procs_string.join("\n"); - - let path = format!("generated/{}.txt", test_name); - std::fs::create_dir_all("generated").unwrap(); - std::fs::write(&path, result).unwrap(); - - use std::process::Command; - let is_tracked = Command::new("git") - .args(&["ls-files", "--error-unmatch", &path]) - .output() - .unwrap(); - - if !is_tracked.status.success() { - panic!( - "The file {:?} is not tracked by git. Try using `git add` on it", - &path - ); - } - - let has_changes = Command::new("git") - .args(&["diff", "--color=always", &path]) - .output() - .unwrap(); - - if !has_changes.status.success() { - eprintln!("`git diff {:?}` failed", &path); - unreachable!(); - } - - if !has_changes.stdout.is_empty() { - println!("{}", std::str::from_utf8(&has_changes.stdout).unwrap()); - panic!("Output changed: resolve conflicts and `git add` the file."); - } -} - -// NOTE because the Show instance of module names is different in --release mode, -// these tests would all fail. In the future, when we do interesting optimizations, -// we'll likely want some tests for --release too. -#[cfg(not(debug_assertions))] -fn verify_procedures( - _expected: &str, - _procedures: MutMap<(Symbol, ProcLayout<'_>), Proc<'_>>, - _main_fn_symbol: Symbol, -) { - // Do nothing -} - -#[mono_test] -fn ir_int_literal() { - r#" - 5 - "# -} - -#[mono_test] -fn ir_int_add() { - r#" - x = [1,2] - 5 + 4 + 3 + List.len x - "# -} - -#[mono_test] -fn ir_assignment() { - r#" - x = 5 - - x - "# -} - -#[mono_test] -fn ir_when_maybe() { - r#" - when Just 3 is - Just n -> n - Nothing -> 0 - "# -} - -#[mono_test] -fn ir_when_these() { - r#" - when These 1 2 is - This x -> x - That y -> y - These x _ -> x - "# -} - -#[mono_test] -fn ir_when_record() { - r#" - when { x: 1, y: 3.14 } is - { x } -> x - "# -} - -#[mono_test] -fn ir_plus() { - r#" - 1 + 2 - "# -} - -#[mono_test] -fn ir_round() { - r#" - Num.round 3.6 - "# -} - -#[mono_test] -fn ir_when_idiv() { - r#" - when Num.divTruncChecked 1000 10 is - Ok val -> val - Err _ -> -1 - "# -} - -#[mono_test] -fn ir_two_defs() { - r#" - x = 3 - y = 4 - - x + y - "# -} - -#[mono_test] -fn ir_when_just() { - r#" - x : [Nothing, Just I64] - x = Just 41 - - when x is - Just v -> v + 0x1 - Nothing -> 0x1 - "# -} - -#[mono_test] -fn one_element_tag() { - r#" - x : [Pair I64] - x = Pair 2 - - x - "# -} - -#[mono_test] -fn guard_pattern_true() { - r#" - wrapper = \{} -> - when 2 is - 2 if False -> 42 - _ -> 0 - - wrapper {} - "# -} - -#[mono_test] -fn when_on_record() { - r#" - when { x: 0x2 } is - { x } -> x + 3 - "# -} - -#[mono_test] -fn when_nested_maybe() { - r#" - Maybe a : [Nothing, Just a] - - x : Maybe (Maybe I64) - x = Just (Just 41) - - when x is - Just (Just v) -> v + 0x1 - _ -> 0x1 - "# -} - -#[mono_test] -fn when_on_two_values() { - r#" - when Pair 2 3 is - Pair 4 3 -> 9 - Pair a b -> a + b - "# -} - -#[mono_test] -fn dict() { - r#" - Dict.len Dict.empty - "# -} - -#[mono_test] -fn list_append_closure() { - r#" - myFunction = \l -> List.append l 42 - - myFunction [1, 2] - "# -} - -#[mono_test] -fn list_append() { - // TODO this leaks at the moment - // ListAppend needs to decrement its arguments - r#" - List.append [1] 2 - "# -} - -#[mono_test] -fn list_len() { - r#" - x = [1,2,3] - y = [1.0] - - List.len x + List.len y - "# -} - -#[mono_test] -fn when_joinpoint() { - r#" - wrapper = \{} -> - x : [Red, White, Blue] - x = Blue - - y = - when x is - Red -> 1 - White -> 2 - Blue -> 3 - - y - - wrapper {} - "# -} - -#[mono_test] -fn simple_if() { - r#" - if True then - 1 - else - 2 - "# -} - -#[mono_test] -fn if_multi_branch() { - r#" - if True then - 1 - else if False then - 2 - else - 3 - "# -} - -#[mono_test] -fn when_on_result() { - r#" - wrapper = \{} -> - x : Result I64 I64 - x = Ok 2 - - y = - when x is - Ok 3 -> 1 - Ok _ -> 2 - Err _ -> 3 - y - - wrapper {} - "# -} - -#[mono_test] -fn let_with_record_pattern() { - r#" - { x } = { x: 0x2, y: 3.14 } - - x - "# -} - -#[mono_test] -fn let_with_record_pattern_list() { - r#" - { x } = { x: [1, 3, 4], y: 3.14 } - - x - "# -} - -#[mono_test] -fn if_guard_bind_variable_false() { - r#" - wrapper = \{} -> - when 10 is - x if x == 5 -> 0 - _ -> 42 - - wrapper {} - "# -} - -#[mono_test] -fn alias_variable() { - r#" - x = 5 - y = x - - 3 - "# -} - -#[mono_test] -fn alias_variable_and_return_it() { - r#" - x = 5 - y = x - - y - "# -} - -#[mono_test] -fn branch_store_variable() { - r#" - when 0 is - 1 -> 12 - a -> a - "# -} - -#[mono_test] -fn list_pass_to_function() { - r#" - x : List I64 - x = [1,2,3] - - id : List I64 -> List I64 - id = \y -> List.set y 0 0 - - id x - "# -} - -#[mono_test] -fn record_optional_field_let_no_use_default() { - r#" - f = \r -> - { x ? 10, y } = r - x + y - - - f { x: 4, y: 9 } - "# -} - -#[mono_test] -fn record_optional_field_let_use_default() { - r#" - f = \r -> - { x ? 10, y } = r - x + y - - - f { y: 9 } - "# -} - -#[mono_test] -fn record_optional_field_function_no_use_default() { - r#" - f = \{ x ? 10, y } -> x + y - - - f { x: 4, y: 9 } - "# -} - -#[mono_test] -fn record_optional_field_function_use_default() { - r#" - f = \{ x ? 10, y } -> x + y - - - f { y: 9 } - "# -} - -#[mono_test] -fn quicksort_help() { - // do we still need with_larger_debug_stack? - r#" - quicksortHelp : List (Num a), I64, I64 -> List (Num a) - quicksortHelp = \list, low, high -> - if low < high then - (Pair partitionIndex partitioned) = Pair 0 [] - - partitioned - |> quicksortHelp low (partitionIndex - 1) - |> quicksortHelp (partitionIndex + 1) high - else - list - - quicksortHelp [] 0 0 - "# -} - -#[mono_test] -fn quicksort_swap() { - indoc!( - r#" - app "test" provides [main] to "./platform" - - swap = \list -> - when Pair (List.get list 0) (List.get list 0) is - Pair (Ok atI) (Ok atJ) -> - list - |> List.set 0 atJ - |> List.set 0 atI - - _ -> - [] - - main = - swap [1, 2] - "# - ) -} - -// #[ignore] -// #[mono_test] -// fn quicksort_partition_help() { -// indoc!( -// r#" -// app "test" provides [main] to "./platform" - -// partitionHelp : I64, I64, List (Num a), I64, (Num a) -> [Pair I64 (List (Num a))] -// partitionHelp = \i, j, list, high, pivot -> -// if j < high then -// when List.get list j is -// Ok value -> -// if value <= pivot then -// partitionHelp (i + 1) (j + 1) (swap (i + 1) j list) high pivot -// else -// partitionHelp i (j + 1) list high pivot - -// Err _ -> -// Pair i list -// else -// Pair i list - -// main = -// partitionHelp 0 0 [] 0 0 -// "# -// ) -// } - -// #[ignore] -// #[mono_test] -// fn quicksort_full() { -// indoc!( -// r#" -// app "test" provides [main] to "./platform" - -// quicksortHelp : List (Num a), I64, I64 -> List (Num a) -// quicksortHelp = \list, low, high -> -// if low < high then -// (Pair partitionIndex partitioned) = partition low high list - -// partitioned -// |> quicksortHelp low (partitionIndex - 1) -// |> quicksortHelp (partitionIndex + 1) high -// else -// list - -// swap : I64, I64, List a -> List a -// swap = \i, j, list -> -// when Pair (List.get list i) (List.get list j) is -// Pair (Ok atI) (Ok atJ) -> -// list -// |> List.set i atJ -// |> List.set j atI - -// _ -> -// [] - -// partition : I64, I64, List (Num a) -> [Pair I64 (List (Num a))] -// partition = \low, high, initialList -> -// when List.get initialList high is -// Ok pivot -> -// when partitionHelp (low - 1) low initialList high pivot is -// Pair newI newList -> -// Pair (newI + 1) (swap (newI + 1) high newList) - -// Err _ -> -// Pair (low - 1) initialList - -// partitionHelp : I64, I64, List (Num a), I64, (Num a) -> [Pair I64 (List (Num a))] -// partitionHelp = \i, j, list, high, pivot -> -// if j < high then -// when List.get list j is -// Ok value -> -// if value <= pivot then -// partitionHelp (i + 1) (j + 1) (swap (i + 1) j list) high pivot -// else -// partitionHelp i (j + 1) list high pivot - -// Err _ -> -// Pair i list -// else -// Pair i list - -// quicksort = \originalList -> -// n = List.len originalList -// quicksortHelp originalList 0 (n - 1) - -// main = -// quicksort [1,2,3] -// "# -// ) -// } - -#[mono_test] -fn factorial() { - r#" - factorial = \n, accum -> - when n is - 0 -> - accum - - _ -> - factorial (n - 1) (n * accum) - - factorial 10 1 - "# -} - -#[mono_test] -fn is_nil() { - r#" - ConsList a : [Cons a (ConsList a), Nil] - - isNil : ConsList a -> Bool - isNil = \list -> - when list is - Nil -> True - Cons _ _ -> False - - isNil (Cons 0x2 Nil) - "# -} - -#[mono_test] -#[ignore] -fn has_none() { - r#" - Maybe a : [Just a, Nothing] - ConsList a : [Cons a (ConsList a), Nil] - - hasNone : ConsList (Maybe a) -> Bool - hasNone = \list -> - when list is - Nil -> False - Cons Nothing _ -> True - Cons (Just _) xs -> hasNone xs - - hasNone (Cons (Just 3) Nil) - "# -} - -#[mono_test] -fn mk_pair_of() { - indoc!( - r#" - app "test" provides [main] to "./platform" - - mkPairOf = \x -> Pair x x - - main = - mkPairOf [1,2,3] - "# - ) -} - -#[mono_test] -fn fst() { - indoc!( - r#" - app "test" provides [main] to "./platform" - - fst = \x, _ -> x - - main = - fst [1,2,3] [3,2,1] - "# - ) -} - -#[mono_test] -fn list_cannot_update_inplace() { - indoc!( - r#" - app "test" provides [main] to "./platform" - - x : List I64 - x = [1,2,3] - - add : List I64 -> List I64 - add = \y -> List.set y 0 0 - - main = - List.len (add x) + List.len x - "# - ) -} - -#[mono_test] -fn list_get() { - r#" - wrapper = \{} -> - List.get [1,2,3] 0 - - wrapper {} - "# -} - -#[mono_test] -fn peano() { - r#" - Peano : [S Peano, Z] - - three : Peano - three = S (S (S Z)) - - three - "# -} - -#[mono_test] -fn peano1() { - r#" - Peano : [S Peano, Z] - - three : Peano - three = S (S (S Z)) - - when three is - Z -> 0 - S _ -> 1 - "# -} - -#[mono_test] -fn peano2() { - r#" - Peano : [S Peano, Z] - - three : Peano - three = S (S (S Z)) - - when three is - S (S _) -> 1 - S (_) -> 0 - Z -> 0 - "# -} - -#[mono_test] -fn optional_when() { - r#" - f = \r -> - when r is - { x: Blue, y ? 3 } -> y - { x: Red, y ? 5 } -> y - - a = f { x: Blue, y: 7 } - b = f { x: Blue } - c = f { x: Red, y: 11 } - d = f { x: Red } - - a * b * c * d - "# -} - -#[mono_test] -fn nested_pattern_match() { - r#" - Maybe a : [Nothing, Just a] - - x : Maybe (Maybe I64) - x = Just (Just 41) - - when x is - Just (Just v) -> v + 0x1 - _ -> 0x1 - "# -} - -#[mono_test] -#[ignore] -fn linked_list_length_twice() { - r#" - LinkedList a : [Nil, Cons a (LinkedList a)] - - nil : LinkedList I64 - nil = Nil - - length : LinkedList a -> I64 - length = \list -> - when list is - Nil -> 0 - Cons _ rest -> 1 + length rest - - length nil + length nil - "# -} - -#[mono_test] -fn rigids() { - indoc!( - r#" - app "test" provides [main] to "./platform" - - swap : Nat, Nat, List a -> List a - swap = \i, j, list -> - when Pair (List.get list i) (List.get list j) is - Pair (Ok atI) (Ok atJ) -> - foo = atJ - - list - |> List.set i foo - |> List.set j atI - - _ -> - [] - - main = - swap 0 0 [0x1] - "# - ) -} - -#[mono_test] -fn let_x_in_x() { - r#" - x = 5 - - answer = - 1337 - - unused = - nested = 17 - nested - - answer - "# -} - -#[mono_test] -fn let_x_in_x_indirect() { - r#" - x = 5 - - answer = - 1337 - - unused = - nested = 17 - - i = 1 - - nested - - { answer, unused }.answer - "# -} - -#[mono_test] -fn nested_closure() { - indoc!( - r#" - app "test" provides [main] to "./platform" - - foo = \{} -> - x = 42 - f = \{} -> x - f - - main = - f = foo {} - f {} - "# - ) -} - -#[mono_test] -fn closure_in_list() { - indoc!( - r#" - app "test" provides [main] to "./platform" - - foo = \{} -> - x = 41 - - f = \{} -> x - - [f] - - main = - items = foo {} - - List.len items - "# - ) -} - -#[ignore] -#[mono_test] -fn somehow_drops_definitions() { - indoc!( - r#" - app "test" provides [main] to "./platform" - - one : I64 - one = 1 - - two : I64 - two = 2 - - increment : I64 -> I64 - increment = \x -> x + one - - double : I64 -> I64 - double = \x -> x * two - - apply : (a -> a), a -> a - apply = \f, x -> f x - - main = - apply (if True then increment else double) 42 - "# - ) -} - -#[mono_test] -fn specialize_closures() { - indoc!( - r#" - app "test" provides [main] to "./platform" - - - apply : (a -> a), a -> a - apply = \f, x -> f x - - main = - one : I64 - one = 1 - - two : I64 - two = 2 - - b : Bool - b = True - - increment : I64 -> I64 - increment = \x -> x + one - - double : I64 -> I64 - double = \x -> if b then x * two else x - - apply (if True then increment else double) 42 - "# - ) -} - -#[mono_test] -fn specialize_lowlevel() { - indoc!( - r#" - app "test" provides [main] to "./platform" - - apply : (a -> a), a -> a - apply = \f, x -> f x - - main = - one : I64 - one = 1 - - two : I64 - two = 2 - - increment : I64 -> I64 - increment = \x -> x + one - - double : I64 -> I64 - double = \x -> x * two - - (if True then increment else double) 42 - "# - ) -} - -#[mono_test] -fn empty_list_of_function_type() { - // see https://github.com/rtfeldman/roc/issues/1732 - indoc!( - r#" - app "test" provides [main] to "./platform" - - main = - myList : List (Str -> Str) - myList = [] - - myClosure : Str -> Str - myClosure = \_ -> "bar" - - choose = - if False then - myList - else - [myClosure] - - when List.get choose 0 is - Ok f -> f "foo" - Err _ -> "bad!" - "# - ) -} - -#[mono_test] -fn monomorphized_ints() { - indoc!( - r#" - app "test" provides [main] to "./platform" - - main = - x = 100 - - f : U8, U32 -> Nat - f = \_, _ -> 18 - - f x x - "# - ) -} - -#[mono_test] -fn monomorphized_floats() { - indoc!( - r#" - app "test" provides [main] to "./platform" - - main = - x = 100.0 - - f : F32, F64 -> Nat - f = \_, _ -> 18 - - f x x - "# - ) -} - -#[mono_test] -#[ignore = "TODO"] -fn monomorphized_ints_aliased() { - indoc!( - r#" - app "test" provides [main] to "./platform" - - main = - y = 100 - w1 = y - w2 = y - - f = \_, _ -> 1 - - f1 : U8, U32 -> Nat - f1 = f - - f2 : U32, U8 -> Nat - f2 = f - - f1 w1 w2 + f2 w1 w2 - "# - ) -} - -#[mono_test] -fn monomorphized_tag() { - indoc!( - r#" - app "test" provides [main] to "./platform" - - main = - b = False - f : Bool, [True, False, Idk] -> U8 - f = \_, _ -> 18 - f b b - "# - ) -} - -#[mono_test] -fn monomorphized_tag_with_aliased_args() { - indoc!( - r#" - app "test" provides [main] to "./platform" - - main = - b = False - c = False - a = A b c - f : [A Bool Bool] -> Nat - f = \_ -> 1 - f a - "# - ) -} - -#[mono_test] -fn monomorphized_list() { - indoc!( - r#" - app "test" provides [main] to "./platform" - - main = - l = [1, 2, 3] - - f : List U8, List U16 -> Nat - f = \_, _ -> 18 - - f l l - "# - ) -} - -#[mono_test] -fn monomorphized_applied_tag() { - indoc!( - r#" - app "test" provides [main] to "./platform" - - main = - a = A "A" - f = \x -> - when x is - A y -> y - B y -> y - f a - "# - ) -} - -#[mono_test] -#[ignore = "Cannot compile polymorphic closures yet"] -fn aliased_polymorphic_closure() { - indoc!( - r#" - n : U8 - n = 1 - f = \{} -> (\a -> n) - g = f {} - g {} - "# - ) -} - -#[mono_test] -fn issue_2535_polymorphic_fields_referenced_in_list() { - indoc!( - r#" - app "test" provides [nums] to "./platform" - - alpha = { a: 1, b: 2 } - - nums : List U8 - nums = - [ - alpha.a, - alpha.b, - ] - "# - ) -} - -#[mono_test] -fn issue_2725_alias_polymorphic_lambda() { - indoc!( - r#" - wrap = \value -> Tag value - wrapIt = wrap - wrapIt 42 - "# - ) -} - -#[mono_test] -fn issue_2583_specialize_errors_behind_unified_branches() { - indoc!( - r#" - if True then List.first [] else Str.toI64 "" - "# - ) -} - -#[mono_test] -fn issue_2810() { - indoc!( - r#" - Command : [Command Tool] - - Job : [Job Command] - - Tool : [SystemTool, FromJob Job] - - a : Job - a = Job (Command (FromJob (Job (Command SystemTool)))) - a - "# - ) -} - -#[mono_test] -fn issue_2811() { - indoc!( - r#" - x = Command { tool: "bash" } - Command c = x - c.tool - "# - ) -} - -#[mono_test] -fn specialize_ability_call() { - indoc!( - r#" - app "test" provides [main] to "./platform" - - Hash has - hash : a -> U64 | a has Hash - - Id := U64 - - hash : Id -> U64 - hash = \@Id n -> n - - main = hash (@Id 1234) - "# - ) -} - -#[mono_test] -fn opaque_assign_to_symbol() { - indoc!( - r#" - app "test" provides [out] to "./platform" - - Variable := U8 - - fromUtf8 : U8 -> Result Variable [InvalidVariableUtf8] - fromUtf8 = \char -> - Ok (@Variable char) - - out = fromUtf8 98 - "# - ) -} - -#[mono_test] -fn encode() { - indoc!( - r#" - app "test" provides [myU8Bytes] to "./platform" - - Encoder fmt := List U8, fmt -> List U8 | fmt has Format - - Encoding has - toEncoder : val -> Encoder fmt | val has Encoding, fmt has Format - - Format has - u8 : U8 -> Encoder fmt | fmt has Format - - - Linear := {} - - # impl Format for Linear - u8 = \n -> @Encoder (\lst, @Linear {} -> List.append lst n) - - MyU8 := U8 - - # impl Encoding for MyU8 - toEncoder = \@MyU8 n -> u8 n - - myU8Bytes = - when toEncoder (@MyU8 15) is - @Encoder doEncode -> doEncode [] (@Linear {}) - "# - ) -} - -// #[ignore] -// #[mono_test] -// fn static_str_closure() { -// indoc!( -// r#" -// app "test" provides [main] to "./platform" - -// main : Str -// main = -// x = "long string that is malloced" - -// f : {} -> Str -// f = (\_ -> x) - -// f {} -// "# -// ) -// } - -#[mono_test] -fn list_map_closure_borrows() { - indoc!( - r#" - app "test" provides [out] to "./platform" - - list = [Str.concat "lllllllllllllllllllllooooooooooong" "g"] - - example1 = List.map list \string -> Str.repeat string 2 - - out = - when List.get example1 0 is - Ok s -> s - Err _ -> "Hello, World!\n" - "# - ) -} - -#[mono_test] -fn list_map_closure_owns() { - indoc!( - r#" - app "test" provides [out] to "./platform" - - list = [Str.concat "lllllllllllllllllllllooooooooooong" "g"] - - example2 = List.map list \string -> Str.concat string "!" - - out = - when List.get example2 0 is - Ok s -> s - Err _ -> "Hello, World!\n" - "# - ) -} - -#[mono_test] -fn list_sort_asc() { - indoc!( - r#" - app "test" provides [out] to "./platform" - - out = List.sortAsc [4, 3, 2, 1] - "# - ) -} diff --git a/compiler/test_mono_macros/Cargo.lock b/compiler/test_mono_macros/Cargo.lock deleted file mode 100644 index e4c1c9929b..0000000000 --- a/compiler/test_mono_macros/Cargo.lock +++ /dev/null @@ -1,47 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 3 - -[[package]] -name = "proc-macro2" -version = "1.0.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba508cc11742c0dc5c1659771673afbab7a0efab23aa17e854cbab0837ed0b43" -dependencies = [ - "unicode-xid", -] - -[[package]] -name = "quote" -version = "1.0.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "syn" -version = "1.0.81" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2afee18b8beb5a596ecb4a2dce128c719b4ba399d34126b9e4396e3f9860966" -dependencies = [ - "proc-macro2", - "quote", - "unicode-xid", -] - -[[package]] -name = "test_mono_macros" -version = "0.1.0" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "unicode-xid" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" diff --git a/compiler/test_mono_macros/Cargo.toml b/compiler/test_mono_macros/Cargo.toml deleted file mode 100644 index c5889844c5..0000000000 --- a/compiler/test_mono_macros/Cargo.toml +++ /dev/null @@ -1,14 +0,0 @@ -[package] -name = "test_mono_macros" -version = "0.1.0" -authors = ["The Roc Contributors"] -license = "UPL-1.0" -edition = "2021" - -[lib] -proc-macro = true - -[dependencies] -syn = { version = "1.0.81", features = ["full", "extra-traits"] } -quote = "1.0.10" -proc-macro2 = "1.0.32" diff --git a/compiler/test_mono_macros/src/lib.rs b/compiler/test_mono_macros/src/lib.rs deleted file mode 100644 index 2be3ab4ceb..0000000000 --- a/compiler/test_mono_macros/src/lib.rs +++ /dev/null @@ -1,28 +0,0 @@ -extern crate proc_macro; - -use proc_macro::TokenStream; -use quote::quote; - -#[proc_macro_attribute] -pub fn mono_test(_args: TokenStream, item: TokenStream) -> TokenStream { - let task_fn = syn::parse_macro_input!(item as syn::ItemFn); - - let args = task_fn.sig.inputs.clone(); - - let name = task_fn.sig.ident.clone(); - let name_str = name.to_string(); - let body = task_fn.block.clone(); - - let visibility = &task_fn.vis; - let attributes = task_fn.attrs; - - let result = quote! { - #[test] - #(#attributes)* - #visibility fn #name(#args) { - compiles_to_ir(#name_str, #body); - - } - }; - result.into() -} diff --git a/compiler/types/Cargo.toml b/compiler/types/Cargo.toml deleted file mode 100644 index 266d118d6c..0000000000 --- a/compiler/types/Cargo.toml +++ /dev/null @@ -1,15 +0,0 @@ -[package] -name = "roc_types" -version = "0.1.0" -authors = ["The Roc Contributors"] -license = "UPL-1.0" -edition = "2021" - -[dependencies] -roc_collections = { path = "../collections" } -roc_region = { path = "../region" } -roc_module = { path = "../module" } -roc_error_macros = {path="../../error_macros"} -roc_debug_flags = {path="../debug_flags"} -bumpalo = { version = "3.8.0", features = ["collections"] } -static_assertions = "1.1.0" diff --git a/compiler/types/src/builtin_aliases.rs b/compiler/types/src/builtin_aliases.rs deleted file mode 100644 index 3002f85783..0000000000 --- a/compiler/types/src/builtin_aliases.rs +++ /dev/null @@ -1,1078 +0,0 @@ -use crate::solved_types::{BuiltinAlias, SolvedType}; -use crate::subs::VarId; -use crate::types::{AliasKind, RecordField}; -use roc_collections::all::{default_hasher, MutMap}; -use roc_module::ident::TagName; -use roc_module::symbol::Symbol; -use roc_region::all::{Loc, Region}; -use std::collections::HashMap; - -const NUM_BUILTIN_IMPORTS: usize = 8; - -/// These can be shared between definitions, they will get instantiated when converted to Type -const TVAR1: VarId = VarId::from_u32(1); - -pub fn aliases() -> MutMap { - let mut aliases = HashMap::with_capacity_and_hasher(NUM_BUILTIN_IMPORTS, default_hasher()); - - let mut add_alias = |symbol, alias| { - debug_assert!( - !aliases.contains_key(&symbol), - "Duplicate alias definition for {:?}", - symbol - ); - - // TODO instead of using Region::zero for all of these, - // instead use the Region where they were defined in their - // source .roc files! This can give nicer error messages. - aliases.insert(symbol, alias); - }; - - // Int range : Num (Integer range) - add_alias( - Symbol::NUM_INT, - BuiltinAlias { - region: Region::zero(), - vars: vec![Loc::at(Region::zero(), "range".into())], - typ: int_alias_content(flex(TVAR1)), - kind: AliasKind::Structural, - }, - ); - - // Frac range : Num (FloatingPoint range) - add_alias( - Symbol::NUM_FRAC, - BuiltinAlias { - region: Region::zero(), - vars: vec![Loc::at(Region::zero(), "range".into())], - typ: frac_alias_content(flex(TVAR1)), - kind: AliasKind::Structural, - }, - ); - - // Num range := range - add_alias( - Symbol::NUM_NUM, - BuiltinAlias { - region: Region::zero(), - vars: vec![Loc::at(Region::zero(), "range".into())], - typ: num_alias_content(flex(TVAR1)), - kind: AliasKind::Opaque, - }, - ); - - // Integer range := range - add_alias( - Symbol::NUM_INTEGER, - BuiltinAlias { - region: Region::zero(), - vars: vec![Loc::at(Region::zero(), "range".into())], - typ: integer_alias_content(flex(TVAR1)), - kind: AliasKind::Opaque, - }, - ); - - // Natural := [] - add_alias( - Symbol::NUM_NATURAL, - BuiltinAlias { - region: Region::zero(), - vars: vec![], - typ: natural_alias_content(), - kind: AliasKind::Opaque, - }, - ); - - // Nat : Int Natural - add_alias( - Symbol::NUM_NAT, - BuiltinAlias { - region: Region::zero(), - vars: Vec::new(), - typ: nat_alias_content(), - kind: AliasKind::Structural, - }, - ); - - // Signed128 := [] - add_alias( - Symbol::NUM_SIGNED128, - BuiltinAlias { - region: Region::zero(), - vars: vec![], - typ: signed128_alias_content(), - kind: AliasKind::Opaque, - }, - ); - - // I128 : Int Signed128 - add_alias( - Symbol::NUM_I128, - BuiltinAlias { - region: Region::zero(), - vars: Vec::new(), - typ: i128_alias_content(), - kind: AliasKind::Structural, - }, - ); - - // U128 : Int Unsigned128 - add_alias( - Symbol::NUM_U128, - BuiltinAlias { - region: Region::zero(), - vars: Vec::new(), - typ: u128_alias_content(), - kind: AliasKind::Structural, - }, - ); - - // Signed64 := [] - add_alias( - Symbol::NUM_SIGNED64, - BuiltinAlias { - region: Region::zero(), - vars: vec![], - typ: signed64_alias_content(), - kind: AliasKind::Opaque, - }, - ); - - // I64 : Int Signed64 - add_alias( - Symbol::NUM_I64, - BuiltinAlias { - region: Region::zero(), - vars: Vec::new(), - typ: i64_alias_content(), - kind: AliasKind::Structural, - }, - ); - - // U64 : Int Unsigned64 - add_alias( - Symbol::NUM_U64, - BuiltinAlias { - region: Region::zero(), - vars: Vec::new(), - typ: u64_alias_content(), - kind: AliasKind::Structural, - }, - ); - - // Signed32 := [] - add_alias( - Symbol::NUM_SIGNED32, - BuiltinAlias { - region: Region::zero(), - vars: vec![], - typ: signed32_alias_content(), - kind: AliasKind::Opaque, - }, - ); - - // I32 : Int Signed32 - add_alias( - Symbol::NUM_I32, - BuiltinAlias { - region: Region::zero(), - vars: Vec::new(), - typ: i32_alias_content(), - kind: AliasKind::Structural, - }, - ); - - // U32 : Int Unsigned32 - add_alias( - Symbol::NUM_U32, - BuiltinAlias { - region: Region::zero(), - vars: Vec::new(), - typ: u32_alias_content(), - kind: AliasKind::Structural, - }, - ); - - // Signed16 := [] - add_alias( - Symbol::NUM_SIGNED16, - BuiltinAlias { - region: Region::zero(), - vars: vec![], - typ: signed16_alias_content(), - kind: AliasKind::Opaque, - }, - ); - - // I16 : Int Signed16 - add_alias( - Symbol::NUM_I16, - BuiltinAlias { - region: Region::zero(), - vars: Vec::new(), - typ: i16_alias_content(), - kind: AliasKind::Structural, - }, - ); - - // U16 : Int Unsigned16 - add_alias( - Symbol::NUM_U16, - BuiltinAlias { - region: Region::zero(), - vars: Vec::new(), - typ: u16_alias_content(), - kind: AliasKind::Structural, - }, - ); - - // Signed8 := [] - add_alias( - Symbol::NUM_SIGNED8, - BuiltinAlias { - region: Region::zero(), - vars: vec![], - typ: signed8_alias_content(), - kind: AliasKind::Opaque, - }, - ); - - // I8 : Int Signed8 - add_alias( - Symbol::NUM_I8, - BuiltinAlias { - region: Region::zero(), - vars: Vec::new(), - typ: i8_alias_content(), - kind: AliasKind::Structural, - }, - ); - - // U8 : Int Unsigned8 - add_alias( - Symbol::NUM_U8, - BuiltinAlias { - region: Region::zero(), - vars: Vec::new(), - typ: u8_alias_content(), - kind: AliasKind::Structural, - }, - ); - - // Decimal := [] - add_alias( - Symbol::NUM_DECIMAL, - BuiltinAlias { - region: Region::zero(), - vars: vec![], - typ: decimal_alias_content(), - kind: AliasKind::Opaque, - }, - ); - - // Binary64 := [] - add_alias( - Symbol::NUM_BINARY64, - BuiltinAlias { - region: Region::zero(), - vars: vec![], - typ: binary64_alias_content(), - kind: AliasKind::Opaque, - }, - ); - - // Binary32 := [] - add_alias( - Symbol::NUM_BINARY32, - BuiltinAlias { - region: Region::zero(), - vars: vec![], - typ: binary32_alias_content(), - kind: AliasKind::Opaque, - }, - ); - - // FloatingPoint range := range - add_alias( - Symbol::NUM_FLOATINGPOINT, - BuiltinAlias { - region: Region::zero(), - vars: vec![Loc::at(Region::zero(), "range".into())], - typ: floatingpoint_alias_content(flex(TVAR1)), - kind: AliasKind::Opaque, - }, - ); - - // Dec : Frac Decimal - add_alias( - Symbol::NUM_DEC, - BuiltinAlias { - region: Region::zero(), - vars: Vec::new(), - typ: dec_alias_content(), - kind: AliasKind::Structural, - }, - ); - - // F64 : Frac Binary64 - add_alias( - Symbol::NUM_F64, - BuiltinAlias { - region: Region::zero(), - vars: Vec::new(), - typ: f64_alias_content(), - kind: AliasKind::Structural, - }, - ); - - // F32 : Frac Binary32 - add_alias( - Symbol::NUM_F32, - BuiltinAlias { - region: Region::zero(), - vars: Vec::new(), - typ: f32_alias_content(), - kind: AliasKind::Structural, - }, - ); - - // Bool : [True, False] - add_alias( - Symbol::BOOL_BOOL, - BuiltinAlias { - region: Region::zero(), - vars: Vec::new(), - typ: bool_alias_content(), - kind: AliasKind::Structural, - }, - ); - - // Utf8ByteProblem : [InvalidStartByte, UnexpectedEndOfSequence, ExpectedContinuation, OverlongEncoding, CodepointTooLarge, EncodesSurrogateHalf] - add_alias( - Symbol::STR_UT8_BYTE_PROBLEM, - BuiltinAlias { - region: Region::zero(), - vars: Vec::new(), - typ: str_utf8_byte_problem_alias_content(), - kind: AliasKind::Structural, - }, - ); - - // Utf8Problem : { byteIndex : Nat, problem : Utf8ByteProblem } - add_alias( - Symbol::STR_UT8_PROBLEM, - BuiltinAlias { - region: Region::zero(), - vars: Vec::new(), - typ: str_utf8_byte_problem_alias_content(), - kind: AliasKind::Structural, - }, - ); - - aliases -} - -#[inline(always)] -pub fn flex(tvar: VarId) -> SolvedType { - SolvedType::Flex(tvar) -} - -#[inline(always)] -pub fn num_type(range: SolvedType) -> SolvedType { - SolvedType::Alias( - Symbol::NUM_NUM, - vec![(range.clone())], - vec![], - Box::new(num_alias_content(range)), - AliasKind::Opaque, - ) -} - -#[inline(always)] -fn num_alias_content(range: SolvedType) -> SolvedType { - range -} - -// FLOATING POINT - -#[inline(always)] -pub fn floatingpoint_type(range: SolvedType) -> SolvedType { - SolvedType::Alias( - Symbol::NUM_FLOATINGPOINT, - vec![(range.clone())], - vec![], - Box::new(floatingpoint_alias_content(range)), - AliasKind::Opaque, - ) -} - -#[inline(always)] -fn floatingpoint_alias_content(range: SolvedType) -> SolvedType { - range -} - -// FRAC - -#[inline(always)] -pub fn frac_type(range: SolvedType) -> SolvedType { - SolvedType::Alias( - Symbol::NUM_FRAC, - vec![(range.clone())], - vec![], - Box::new(frac_alias_content(range)), - AliasKind::Structural, - ) -} - -#[inline(always)] -fn frac_alias_content(range: SolvedType) -> SolvedType { - num_type(floatingpoint_type(range)) -} - -// F64 - -#[inline(always)] -pub fn f64_type() -> SolvedType { - SolvedType::Alias( - Symbol::NUM_F64, - vec![], - vec![], - Box::new(f64_alias_content()), - AliasKind::Structural, - ) -} - -#[inline(always)] -fn f64_alias_content() -> SolvedType { - frac_alias_content(binary64_type()) -} - -// F32 - -#[inline(always)] -pub fn f32_type() -> SolvedType { - SolvedType::Alias( - Symbol::NUM_F32, - vec![], - vec![], - Box::new(f32_alias_content()), - AliasKind::Structural, - ) -} - -#[inline(always)] -fn f32_alias_content() -> SolvedType { - frac_alias_content(binary32_type()) -} - -// Nat - -#[inline(always)] -pub fn nat_type() -> SolvedType { - SolvedType::Alias( - Symbol::NUM_NAT, - vec![], - vec![], - Box::new(nat_alias_content()), - AliasKind::Structural, - ) -} - -#[inline(always)] -fn nat_alias_content() -> SolvedType { - int_alias_content(natural_type()) -} - -// I128 - -#[inline(always)] -pub fn i128_type() -> SolvedType { - SolvedType::Alias( - Symbol::NUM_I128, - vec![], - vec![], - Box::new(i128_alias_content()), - AliasKind::Structural, - ) -} - -#[inline(always)] -fn i128_alias_content() -> SolvedType { - int_alias_content(signed128_type()) -} - -// I128 - -#[inline(always)] -pub fn u128_type() -> SolvedType { - SolvedType::Alias( - Symbol::NUM_U128, - vec![], - vec![], - Box::new(u128_alias_content()), - AliasKind::Structural, - ) -} - -#[inline(always)] -fn u128_alias_content() -> SolvedType { - int_alias_content(unsigned128_type()) -} - -// U64 - -#[inline(always)] -pub fn u64_type() -> SolvedType { - SolvedType::Alias( - Symbol::NUM_U64, - vec![], - vec![], - Box::new(u64_alias_content()), - AliasKind::Structural, - ) -} - -#[inline(always)] -fn u64_alias_content() -> SolvedType { - int_alias_content(unsigned64_type()) -} - -// I64 - -#[inline(always)] -pub fn i64_type() -> SolvedType { - SolvedType::Alias( - Symbol::NUM_I64, - vec![], - vec![], - Box::new(i64_alias_content()), - AliasKind::Structural, - ) -} - -#[inline(always)] -fn i64_alias_content() -> SolvedType { - int_alias_content(signed64_type()) -} - -// U32 - -#[inline(always)] -pub fn u32_type() -> SolvedType { - SolvedType::Alias( - Symbol::NUM_U32, - vec![], - vec![], - Box::new(u32_alias_content()), - AliasKind::Structural, - ) -} - -#[inline(always)] -fn u32_alias_content() -> SolvedType { - int_alias_content(unsigned32_type()) -} - -// I32 - -#[inline(always)] -pub fn i32_type() -> SolvedType { - SolvedType::Alias( - Symbol::NUM_I32, - vec![], - vec![], - Box::new(i32_alias_content()), - AliasKind::Structural, - ) -} - -#[inline(always)] -fn i32_alias_content() -> SolvedType { - int_alias_content(signed32_type()) -} - -// U16 - -#[inline(always)] -pub fn u16_type() -> SolvedType { - SolvedType::Alias( - Symbol::NUM_U16, - vec![], - vec![], - Box::new(u16_alias_content()), - AliasKind::Structural, - ) -} - -#[inline(always)] -fn u16_alias_content() -> SolvedType { - int_alias_content(unsigned16_type()) -} - -// I16 - -#[inline(always)] -pub fn i16_type() -> SolvedType { - SolvedType::Alias( - Symbol::NUM_I16, - vec![], - vec![], - Box::new(i16_alias_content()), - AliasKind::Structural, - ) -} - -#[inline(always)] -fn i16_alias_content() -> SolvedType { - int_alias_content(signed16_type()) -} - -// U8 - -#[inline(always)] -pub fn u8_type() -> SolvedType { - SolvedType::Alias( - Symbol::NUM_U8, - vec![], - vec![], - Box::new(u8_alias_content()), - AliasKind::Structural, - ) -} - -#[inline(always)] -fn u8_alias_content() -> SolvedType { - int_alias_content(unsigned8_type()) -} - -// I8 - -#[inline(always)] -pub fn i8_type() -> SolvedType { - SolvedType::Alias( - Symbol::NUM_I8, - vec![], - vec![], - Box::new(i8_alias_content()), - AliasKind::Structural, - ) -} - -#[inline(always)] -fn i8_alias_content() -> SolvedType { - int_alias_content(signed8_type()) -} - -// INT - -#[inline(always)] -pub fn int_type(range: SolvedType) -> SolvedType { - SolvedType::Alias( - Symbol::NUM_INT, - vec![(range.clone())], - vec![], - Box::new(int_alias_content(range)), - AliasKind::Structural, - ) -} - -#[inline(always)] -fn int_alias_content(range: SolvedType) -> SolvedType { - num_type(integer_type(range)) -} - -// INTEGER - -#[inline(always)] -pub fn integer_type(range: SolvedType) -> SolvedType { - SolvedType::Alias( - Symbol::NUM_INTEGER, - vec![(range.clone())], - vec![], - Box::new(integer_alias_content(range)), - AliasKind::Opaque, - ) -} - -#[inline(always)] -fn integer_alias_content(range: SolvedType) -> SolvedType { - range -} - -#[inline(always)] -pub fn binary64_type() -> SolvedType { - SolvedType::Alias( - Symbol::NUM_BINARY64, - vec![], - vec![], - Box::new(binary64_alias_content()), - AliasKind::Structural, - ) -} - -#[inline(always)] -pub fn binary64_alias_content() -> SolvedType { - SolvedType::EmptyTagUnion -} - -#[inline(always)] -pub fn binary32_type() -> SolvedType { - SolvedType::Alias( - Symbol::NUM_BINARY32, - vec![], - vec![], - Box::new(binary32_alias_content()), - AliasKind::Structural, - ) -} - -#[inline(always)] -fn binary32_alias_content() -> SolvedType { - SolvedType::EmptyTagUnion -} - -#[inline(always)] -pub fn natural_type() -> SolvedType { - SolvedType::Alias( - Symbol::NUM_NATURAL, - vec![], - vec![], - Box::new(natural_alias_content()), - AliasKind::Structural, - ) -} - -#[inline(always)] -fn natural_alias_content() -> SolvedType { - SolvedType::EmptyTagUnion -} - -#[inline(always)] -pub fn signed128_type() -> SolvedType { - SolvedType::Alias( - Symbol::NUM_SIGNED128, - vec![], - vec![], - Box::new(signed128_alias_content()), - AliasKind::Structural, - ) -} - -#[inline(always)] -fn signed128_alias_content() -> SolvedType { - SolvedType::EmptyTagUnion -} - -#[inline(always)] -pub fn signed64_type() -> SolvedType { - SolvedType::Alias( - Symbol::NUM_SIGNED64, - vec![], - vec![], - Box::new(signed64_alias_content()), - AliasKind::Structural, - ) -} - -#[inline(always)] -fn signed64_alias_content() -> SolvedType { - SolvedType::EmptyTagUnion -} - -#[inline(always)] -pub fn signed32_type() -> SolvedType { - SolvedType::Alias( - Symbol::NUM_SIGNED32, - vec![], - vec![], - Box::new(signed32_alias_content()), - AliasKind::Structural, - ) -} - -#[inline(always)] -fn signed32_alias_content() -> SolvedType { - SolvedType::EmptyTagUnion -} - -#[inline(always)] -pub fn signed16_type() -> SolvedType { - SolvedType::Alias( - Symbol::NUM_SIGNED16, - vec![], - vec![], - Box::new(signed16_alias_content()), - AliasKind::Structural, - ) -} - -#[inline(always)] -fn signed16_alias_content() -> SolvedType { - SolvedType::EmptyTagUnion -} - -#[inline(always)] -pub fn signed8_type() -> SolvedType { - SolvedType::Alias( - Symbol::NUM_SIGNED8, - vec![], - vec![], - Box::new(signed8_alias_content()), - AliasKind::Structural, - ) -} - -#[inline(always)] -fn signed8_alias_content() -> SolvedType { - SolvedType::EmptyTagUnion -} - -#[inline(always)] -pub fn unsigned128_type() -> SolvedType { - SolvedType::Alias( - Symbol::NUM_UNSIGNED128, - vec![], - vec![], - Box::new(unsigned128_alias_content()), - AliasKind::Structural, - ) -} - -#[inline(always)] -fn unsigned128_alias_content() -> SolvedType { - SolvedType::EmptyTagUnion -} - -#[inline(always)] -pub fn unsigned64_type() -> SolvedType { - SolvedType::Alias( - Symbol::NUM_UNSIGNED64, - vec![], - vec![], - Box::new(unsigned64_alias_content()), - AliasKind::Structural, - ) -} - -#[inline(always)] -fn unsigned64_alias_content() -> SolvedType { - SolvedType::EmptyTagUnion -} - -#[inline(always)] -pub fn unsigned32_type() -> SolvedType { - SolvedType::Alias( - Symbol::NUM_UNSIGNED32, - vec![], - vec![], - Box::new(unsigned32_alias_content()), - AliasKind::Structural, - ) -} - -#[inline(always)] -fn unsigned32_alias_content() -> SolvedType { - SolvedType::EmptyTagUnion -} - -#[inline(always)] -pub fn unsigned16_type() -> SolvedType { - SolvedType::Alias( - Symbol::NUM_UNSIGNED16, - vec![], - vec![], - Box::new(unsigned16_alias_content()), - AliasKind::Structural, - ) -} - -#[inline(always)] -fn unsigned16_alias_content() -> SolvedType { - SolvedType::EmptyTagUnion -} - -#[inline(always)] -pub fn unsigned8_type() -> SolvedType { - SolvedType::Alias( - Symbol::NUM_UNSIGNED8, - vec![], - vec![], - Box::new(unsigned8_alias_content()), - AliasKind::Structural, - ) -} - -#[inline(always)] -fn unsigned8_alias_content() -> SolvedType { - SolvedType::EmptyTagUnion -} - -#[inline(always)] -fn decimal_alias_content() -> SolvedType { - SolvedType::EmptyTagUnion -} - -// Dec - -#[inline(always)] -pub fn dec_type() -> SolvedType { - SolvedType::Alias( - Symbol::NUM_DEC, - vec![], - vec![], - Box::new(dec_alias_content()), - AliasKind::Structural, - ) -} - -#[inline(always)] -fn dec_alias_content() -> SolvedType { - frac_alias_content(decimal_type()) -} - -#[inline(always)] -pub fn decimal_type() -> SolvedType { - SolvedType::Alias( - Symbol::NUM_DECIMAL, - vec![], - vec![], - Box::new(decimal_alias_content()), - AliasKind::Structural, - ) -} - -#[inline(always)] -pub fn bool_type() -> SolvedType { - SolvedType::Alias( - Symbol::BOOL_BOOL, - vec![], - vec![], - Box::new(bool_alias_content()), - AliasKind::Structural, - ) -} - -fn bool_alias_content() -> SolvedType { - SolvedType::TagUnion( - vec![ - (TagName("False".into()), vec![]), - (TagName("True".into()), vec![]), - ], - Box::new(SolvedType::EmptyTagUnion), - ) -} - -#[inline(always)] -pub fn ordering_type() -> SolvedType { - // [LT, EQ, GT] - SolvedType::TagUnion( - vec![ - (TagName("EQ".into()), vec![]), - (TagName("GT".into()), vec![]), - (TagName("LT".into()), vec![]), - ], - Box::new(SolvedType::EmptyTagUnion), - ) -} - -#[inline(always)] -pub fn result_type(a: SolvedType, e: SolvedType) -> SolvedType { - SolvedType::Alias( - Symbol::RESULT_RESULT, - vec![a.clone(), e.clone()], - vec![], - Box::new(result_alias_content(a, e)), - AliasKind::Structural, - ) -} - -#[inline(always)] -pub fn box_type(a: SolvedType) -> SolvedType { - SolvedType::Apply(Symbol::BOX_BOX_TYPE, vec![a]) -} - -#[inline(always)] -fn result_alias_content(a: SolvedType, e: SolvedType) -> SolvedType { - SolvedType::TagUnion( - vec![ - (TagName("Err".into()), vec![e]), - (TagName("Ok".into()), vec![a]), - ], - Box::new(SolvedType::EmptyTagUnion), - ) -} - -#[inline(always)] -pub fn list_type(a: SolvedType) -> SolvedType { - SolvedType::Apply(Symbol::LIST_LIST, vec![a]) -} - -#[inline(always)] -pub fn str_type() -> SolvedType { - SolvedType::Apply(Symbol::STR_STR, Vec::new()) -} - -#[inline(always)] -pub fn str_utf8_problem_type() -> SolvedType { - SolvedType::Alias( - Symbol::STR_UT8_PROBLEM, - vec![], - vec![], - Box::new(str_utf8_problem_alias_content()), - AliasKind::Structural, - ) -} - -#[inline(always)] -pub fn str_utf8_problem_alias_content() -> SolvedType { - SolvedType::Record { - fields: vec![ - ("byteIndex".into(), RecordField::Required(nat_type())), - ( - "problem".into(), - RecordField::Required(str_utf8_byte_problem_type()), - ), - ], - ext: Box::new(SolvedType::EmptyRecord), - } -} - -#[inline(always)] -pub fn str_utf8_byte_problem_type() -> SolvedType { - SolvedType::Alias( - Symbol::STR_UT8_BYTE_PROBLEM, - vec![], - vec![], - Box::new(str_utf8_byte_problem_alias_content()), - AliasKind::Structural, - ) -} - -#[inline(always)] -pub fn str_utf8_byte_problem_alias_content() -> SolvedType { - // 1. This must have the same values as the Zig struct Utf8ByteProblem in src/str.zig - // 2. This must be in alphabetical order - // - // [CodepointTooLarge, EncodesSurrogateHalf, OverlongEncoding, InvalidStartByte, UnexpectedEndOfSequence, ExpectedContinuation] - SolvedType::TagUnion( - vec![ - (TagName("CodepointTooLarge".into()), vec![]), - (TagName("EncodesSurrogateHalf".into()), vec![]), - (TagName("ExpectedContinuation".into()), vec![]), - (TagName("InvalidStartByte".into()), vec![]), - (TagName("OverlongEncoding".into()), vec![]), - (TagName("UnexpectedEndOfSequence".into()), vec![]), - ], - Box::new(SolvedType::EmptyTagUnion), - ) -} - -#[inline(always)] -pub fn set_type(a: SolvedType) -> SolvedType { - SolvedType::Apply(Symbol::SET_SET, vec![a]) -} - -#[inline(always)] -pub fn dict_type(key: SolvedType, value: SolvedType) -> SolvedType { - SolvedType::Apply(Symbol::DICT_DICT, vec![key, value]) -} diff --git a/compiler/types/src/lib.rs b/compiler/types/src/lib.rs deleted file mode 100644 index 0846e058aa..0000000000 --- a/compiler/types/src/lib.rs +++ /dev/null @@ -1,10 +0,0 @@ -#![warn(clippy::dbg_macro)] -// See github.com/rtfeldman/roc/issues/800 for discussion of the large_enum_variant check. -#![allow(clippy::large_enum_variant)] -pub mod builtin_aliases; -pub mod num; -pub mod pretty_print; -pub mod solved_types; -pub mod subs; -pub mod types; -mod unification_table; diff --git a/compiler/types/src/num.rs b/compiler/types/src/num.rs deleted file mode 100644 index 725ff00fbe..0000000000 --- a/compiler/types/src/num.rs +++ /dev/null @@ -1,327 +0,0 @@ -use crate::subs::Variable; -use roc_module::symbol::Symbol; - -/// A bound placed on a number because of its literal value. -/// e.g. `-5` cannot be unsigned, and 300 does not fit in a U8 -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum NumericRange { - IntAtLeastSigned(IntWidth), - IntAtLeastEitherSign(IntWidth), - NumAtLeastSigned(IntWidth), - NumAtLeastEitherSign(IntWidth), -} - -impl NumericRange { - pub fn contains_symbol(&self, symbol: Symbol) -> Option { - let contains = match symbol { - Symbol::NUM_I8 => self.contains_int_width(IntWidth::I8), - Symbol::NUM_U8 => self.contains_int_width(IntWidth::U8), - Symbol::NUM_I16 => self.contains_int_width(IntWidth::I16), - Symbol::NUM_U16 => self.contains_int_width(IntWidth::U16), - Symbol::NUM_I32 => self.contains_int_width(IntWidth::I32), - Symbol::NUM_U32 => self.contains_int_width(IntWidth::U32), - Symbol::NUM_I64 => self.contains_int_width(IntWidth::I64), - Symbol::NUM_NAT => self.contains_int_width(IntWidth::Nat), - Symbol::NUM_U64 => self.contains_int_width(IntWidth::U64), - Symbol::NUM_I128 => self.contains_int_width(IntWidth::I128), - Symbol::NUM_U128 => self.contains_int_width(IntWidth::U128), - - Symbol::NUM_DEC => self.contains_float_width(FloatWidth::Dec), - Symbol::NUM_F32 => self.contains_float_width(FloatWidth::F32), - Symbol::NUM_F64 => self.contains_float_width(FloatWidth::F64), - - Symbol::NUM_NUM | Symbol::NUM_INT | Symbol::NUM_FRAC => { - // these satisfy any range that they are given - true - } - - _ => { - return None; - } - }; - - Some(contains) - } - - fn contains_float_width(&self, _width: FloatWidth) -> bool { - // we don't currently check the float width - true - } - - fn contains_int_width(&self, width: IntWidth) -> bool { - use NumericRange::*; - - let (range_signedness, at_least_width) = match self { - IntAtLeastSigned(width) => (SignDemand::Signed, width), - IntAtLeastEitherSign(width) => (SignDemand::NoDemand, width), - NumAtLeastSigned(width) => (SignDemand::Signed, width), - NumAtLeastEitherSign(width) => (SignDemand::NoDemand, width), - }; - - let (actual_signedness, _) = width.signedness_and_width(); - - if let (IntSignedness::Unsigned, SignDemand::Signed) = (actual_signedness, range_signedness) - { - return false; - } - - width.signedness_and_width().1 >= at_least_width.signedness_and_width().1 - } - - pub fn variable_slice(&self) -> &'static [Variable] { - use NumericRange::*; - - match self { - IntAtLeastSigned(width) => { - let target = int_width_to_variable(*width); - let start = SIGNED_VARIABLES.iter().position(|v| *v == target).unwrap(); - let end = SIGNED_VARIABLES.len() - 3; - - &SIGNED_VARIABLES[start..end] - } - IntAtLeastEitherSign(width) => { - let target = int_width_to_variable(*width); - let start = ALL_VARIABLES.iter().position(|v| *v == target).unwrap(); - let end = ALL_VARIABLES.len() - 3; - - &ALL_VARIABLES[start..end] - } - NumAtLeastSigned(width) => { - let target = int_width_to_variable(*width); - let start = SIGNED_VARIABLES.iter().position(|v| *v == target).unwrap(); - - &SIGNED_VARIABLES[start..] - } - NumAtLeastEitherSign(width) => { - let target = int_width_to_variable(*width); - let start = ALL_VARIABLES.iter().position(|v| *v == target).unwrap(); - - &ALL_VARIABLES[start..] - } - } - } -} - -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -enum IntSignedness { - Unsigned, - Signed, -} - -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub enum IntWidth { - U8, - U16, - U32, - U64, - U128, - I8, - I16, - I32, - I64, - I128, - Nat, -} - -impl IntWidth { - /// Returns the `IntSignedness` and bit width of a variant. - fn signedness_and_width(&self) -> (IntSignedness, u32) { - use IntSignedness::*; - use IntWidth::*; - match self { - U8 => (Unsigned, 8), - U16 => (Unsigned, 16), - U32 => (Unsigned, 32), - U64 => (Unsigned, 64), - U128 => (Unsigned, 128), - I8 => (Signed, 8), - I16 => (Signed, 16), - I32 => (Signed, 32), - I64 => (Signed, 64), - I128 => (Signed, 128), - // TODO: this is platform specific! - Nat => (Unsigned, 64), - } - } - - pub fn type_str(&self) -> &'static str { - use IntWidth::*; - match self { - U8 => "U8", - U16 => "U16", - U32 => "U32", - U64 => "U64", - U128 => "U128", - I8 => "I8", - I16 => "I16", - I32 => "I32", - I64 => "I64", - I128 => "I128", - Nat => "Nat", - } - } - - pub fn max_value(&self) -> u128 { - use IntWidth::*; - match self { - U8 => u8::MAX as u128, - U16 => u16::MAX as u128, - U32 => u32::MAX as u128, - U64 => u64::MAX as u128, - U128 => u128::MAX, - I8 => i8::MAX as u128, - I16 => i16::MAX as u128, - I32 => i32::MAX as u128, - I64 => i64::MAX as u128, - I128 => i128::MAX as u128, - // TODO: this is platform specific! - Nat => u64::MAX as u128, - } - } - - pub fn min_value(&self) -> i128 { - use IntWidth::*; - match self { - U8 | U16 | U32 | U64 | U128 | Nat => 0, - I8 => i8::MIN as i128, - I16 => i16::MIN as i128, - I32 => i32::MIN as i128, - I64 => i64::MIN as i128, - I128 => i128::MIN, - } - } - - /// Checks if `self` represents superset of integers that `lower_bound` represents, on a particular - /// side of the integers relative to 0. - /// - /// If `is_negative` is true, the negative side is checked; otherwise the positive side is checked. - pub fn is_superset(&self, lower_bound: &Self, is_negative: bool) -> bool { - use IntSignedness::*; - - if is_negative { - match ( - self.signedness_and_width(), - lower_bound.signedness_and_width(), - ) { - ((Signed, us), (Signed, lower_bound)) => us >= lower_bound, - // Unsigned ints can never represent negative numbers; signed (non-zero width) - // ints always can. - ((Unsigned, _), (Signed, _)) => false, - ((Signed, _), (Unsigned, _)) => true, - // Trivially true; both can only express 0. - ((Unsigned, _), (Unsigned, _)) => true, - } - } else { - match ( - self.signedness_and_width(), - lower_bound.signedness_and_width(), - ) { - ((Signed, us), (Signed, lower_bound)) - | ((Unsigned, us), (Unsigned, lower_bound)) => us >= lower_bound, - - // Unsigned ints with the same bit width as their unsigned counterparts can always - // express 2x more integers on the positive side as unsigned ints. - ((Unsigned, us), (Signed, lower_bound)) => us >= lower_bound, - - // ...but that means signed int widths can represent less than their unsigned - // counterparts, so the below is true iff the bit width is strictly greater. E.g. - // i16 is a superset of u8, but i16 is not a superset of u16. - ((Signed, us), (Unsigned, lower_bound)) => us > lower_bound, - } - } - } -} - -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub enum FloatWidth { - Dec, - F32, - F64, -} - -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub enum SignDemand { - /// Can be signed or unsigned. - NoDemand, - /// Must be signed. - Signed, -} - -/// Describes a bound on the width of an integer. -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub enum IntBound { - /// There is no bound on the width. - None, - /// Must have an exact width. - Exact(IntWidth), - /// Must have a certain sign and a minimum width. - AtLeast { sign: SignDemand, width: IntWidth }, -} - -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub enum FloatBound { - None, - Exact(FloatWidth), -} - -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub enum NumBound { - None, - /// Must be an integer of a certain size, or any float. - AtLeastIntOrFloat { - sign: SignDemand, - width: IntWidth, - }, -} - -pub const fn int_width_to_variable(w: IntWidth) -> Variable { - match w { - IntWidth::U8 => Variable::U8, - IntWidth::U16 => Variable::U16, - IntWidth::U32 => Variable::U32, - IntWidth::U64 => Variable::U64, - IntWidth::U128 => Variable::U128, - IntWidth::I8 => Variable::I8, - IntWidth::I16 => Variable::I16, - IntWidth::I32 => Variable::I32, - IntWidth::I64 => Variable::I64, - IntWidth::I128 => Variable::I128, - IntWidth::Nat => Variable::NAT, - } -} - -pub const fn float_width_to_variable(w: FloatWidth) -> Variable { - match w { - FloatWidth::Dec => Variable::DEC, - FloatWidth::F32 => Variable::F32, - FloatWidth::F64 => Variable::F64, - } -} - -const ALL_VARIABLES: &[Variable] = &[ - Variable::I8, - Variable::U8, - Variable::I16, - Variable::U16, - Variable::I32, - Variable::U32, - Variable::I64, - Variable::NAT, // FIXME: Nat's order here depends on the platfor, - Variable::U64, - Variable::I128, - Variable::U128, - Variable::F32, - Variable::F64, - Variable::DEC, -]; - -const SIGNED_VARIABLES: &[Variable] = &[ - Variable::I8, - Variable::I16, - Variable::I32, - Variable::I64, - Variable::I128, - Variable::F32, - Variable::F64, - Variable::DEC, -]; diff --git a/compiler/types/src/pretty_print.rs b/compiler/types/src/pretty_print.rs deleted file mode 100644 index 172069340d..0000000000 --- a/compiler/types/src/pretty_print.rs +++ /dev/null @@ -1,1242 +0,0 @@ -use crate::subs::{ - self, AliasVariables, Content, FlatType, GetSubsSlice, Label, Subs, SubsIndex, UnionLabels, - UnionTags, UnsortedUnionLabels, Variable, -}; -use crate::types::{name_type_var, RecordField, Uls}; -use roc_collections::all::MutMap; -use roc_error_macros::internal_error; -use roc_module::ident::{Lowercase, TagName}; -use roc_module::symbol::{Interns, ModuleId, Symbol}; - -pub static WILDCARD: &str = "*"; -static EMPTY_RECORD: &str = "{}"; -static EMPTY_TAG_UNION: &str = "[]"; - -/// Rerquirements for parentheses. -/// -/// If we're inside a function (that is, this is either an argument or a return -/// value), we may need to use parens. Examples: -/// -/// a -> (* -> a) -/// (* -> a) -> a -/// -/// Separately, if we're inside a type parameter, we may need to use parens: -/// -/// List I64 -/// List (List I64) -/// -/// Otherwise, parens are unnecessary. -#[derive(Clone, Copy, Debug, PartialEq)] -pub enum Parens { - InFn, - InTypeParam, - Unnecessary, -} - -macro_rules! write_parens { - ($insert_parens:expr, $buf:expr, $body:expr) => {{ - if $insert_parens { - $buf.push('('); - } - - $body - - if $insert_parens { - $buf.push(')'); - } - } - }; -} - -pub struct DebugPrint { - pub print_lambda_sets: bool, - pub print_only_under_alias: bool, -} - -impl DebugPrint { - pub const NOTHING: DebugPrint = DebugPrint { - print_lambda_sets: false, - print_only_under_alias: false, - }; -} - -struct Env<'a> { - home: ModuleId, - interns: &'a Interns, - debug: DebugPrint, -} - -/// How many times a root variable appeared in Subs. -/// -/// We only care about whether it was a single time or multiple times, -/// because single appearances get a wildcard (*) and multiple times -/// get a generated letter ("a" etc). -#[derive(Debug)] -enum Appearances { - Single, - Multiple, -} - -/// Generate names for all type variables, replacing FlexVar(None) with -/// FlexVar(Some(name)) where appropriate. Example: for the identity -/// function, generate a name of "a" for both its argument and return -/// type variables. -/// -/// We also want to count how many times a root appears, because we should -/// only generate a name for it if it appears more than once. -fn find_names_needed( - variable: Variable, - subs: &mut Subs, - roots: &mut Vec, - root_appearances: &mut MutMap, - names_taken: &mut MutMap, -) { - use crate::subs::Content::*; - use crate::subs::FlatType::*; - - while let Err((recursive, _chain)) = subs.occurs(variable) { - let rec_var = subs.fresh_unnamed_flex_var(); - let content = subs.get_content_without_compacting(recursive); - - match content { - Content::Structure(FlatType::TagUnion(tags, ext_var)) => { - let ext_var = *ext_var; - - let mut new_tags = MutMap::default(); - - for (name_index, slice_index) in tags.iter_all() { - let slice = subs[slice_index]; - - let mut new_vars = Vec::new(); - for var_index in slice { - let var = subs[var_index]; - new_vars.push(if var == recursive { rec_var } else { var }); - } - - new_tags.insert(subs[name_index].clone(), new_vars); - } - - let mut x: Vec<_> = new_tags.into_iter().collect(); - x.sort(); - - let union_tags = UnionTags::insert_into_subs(subs, x); - - let flat_type = FlatType::RecursiveTagUnion(rec_var, union_tags, ext_var); - subs.set_content(recursive, Content::Structure(flat_type)); - } - _ => panic!( - "unfixable recursive type in roc_types::pretty_print {:?} {:?} {:?}", - recursive, variable, content - ), - } - } - - match &subs.get_content_without_compacting(variable).clone() { - RecursionVar { opt_name: None, .. } | FlexVar(None) => { - let root = subs.get_root_key_without_compacting(variable); - - // If this var is *not* its own root, then the - // root var necessarily appears in multiple places. - // We need a name for it! - match root_appearances.get(&root) { - Some(Appearances::Single) => { - root_appearances.insert(root, Appearances::Multiple); - } - Some(Appearances::Multiple) => { - // It's already multiple, so do nothing! - } - None => { - roots.push(root); - root_appearances.insert(root, Appearances::Single); - } - } - } - FlexAbleVar(None, _) => { - let root = subs.get_root_key_without_compacting(variable); - if !root_appearances.contains_key(&root) { - roots.push(root); - } - // Able vars are always printed at least twice (in the signature, and in the "has" - // clause set). - root_appearances.insert(root, Appearances::Multiple); - } - RecursionVar { - opt_name: Some(name_index), - .. - } - | FlexVar(Some(name_index)) - | FlexAbleVar(Some(name_index), _) - | RigidVar(name_index) - | RigidAbleVar(name_index, _) => { - let root = subs.get_root_key_without_compacting(variable); - - // User-defined names are already taken. - // We must not accidentally generate names that collide with them! - let name = subs.field_names[name_index.index as usize].clone(); - match names_taken.get(&name) { - Some(var) if *var == root => {} - Some(_) => { - if !root_appearances.contains_key(&root) { - roots.push(root); - } - // We want a name, but the default name is already taken by another root. - root_appearances.insert(root, Appearances::Multiple); - } - None => { - names_taken.insert(name, root); - } - } - } - Structure(Apply(_, args)) => { - for index in args.into_iter() { - let var = subs[index]; - find_names_needed(var, subs, roots, root_appearances, names_taken); - } - } - Structure(Func(arg_vars, _closure_var, ret_var)) => { - for index in arg_vars.into_iter() { - let var = subs[index]; - find_names_needed(var, subs, roots, root_appearances, names_taken); - } - - find_names_needed(*ret_var, subs, roots, root_appearances, names_taken); - } - Structure(Record(sorted_fields, ext_var)) => { - for index in sorted_fields.iter_variables() { - let var = subs[index]; - find_names_needed(var, subs, roots, root_appearances, names_taken); - } - - find_names_needed(*ext_var, subs, roots, root_appearances, names_taken); - } - Structure(TagUnion(tags, ext_var)) => { - for slice_index in tags.variables() { - let slice = subs[slice_index]; - for var_index in slice { - let var = subs[var_index]; - find_names_needed(var, subs, roots, root_appearances, names_taken); - } - } - - find_names_needed(*ext_var, subs, roots, root_appearances, names_taken); - } - Structure(FunctionOrTagUnion(_, _, ext_var)) => { - find_names_needed(*ext_var, subs, roots, root_appearances, names_taken); - } - Structure(RecursiveTagUnion(rec_var, tags, ext_var)) => { - for slice_index in tags.variables() { - let slice = subs[slice_index]; - for var_index in slice { - let var = subs[var_index]; - find_names_needed(var, subs, roots, root_appearances, names_taken); - } - } - - find_names_needed(*ext_var, subs, roots, root_appearances, names_taken); - find_names_needed(*rec_var, subs, roots, root_appearances, names_taken); - } - Alias(_symbol, args, _actual, _kind) => { - // only find names for named parameters! - for var_index in args.into_iter().take(args.len()) { - let var = subs[var_index]; - find_names_needed(var, subs, roots, root_appearances, names_taken); - } - // TODO should we also look in the actual variable? - // find_names_needed(_actual, subs, roots, root_appearances, names_taken); - } - LambdaSet(subs::LambdaSet { - solved, - recursion_var, - unspecialized, - }) => { - for slice_index in solved.variables() { - let slice = subs[slice_index]; - for var_index in slice { - let var = subs[var_index]; - find_names_needed(var, subs, roots, root_appearances, names_taken); - } - } - - for uls_index in unspecialized.into_iter() { - let Uls(var, _, _) = subs[uls_index]; - find_names_needed(var, subs, roots, root_appearances, names_taken); - } - - if let Some(rec_var) = recursion_var.into_variable() { - find_names_needed(rec_var, subs, roots, root_appearances, names_taken); - } - } - &RangedNumber(typ, _) => { - find_names_needed(typ, subs, roots, root_appearances, names_taken); - } - Error | Structure(Erroneous(_)) | Structure(EmptyRecord) | Structure(EmptyTagUnion) => { - // Errors and empty records don't need names. - } - } -} - -struct NamedResult { - recursion_structs_to_expand: Vec, -} - -fn name_all_type_vars(variable: Variable, subs: &mut Subs) -> NamedResult { - let mut roots = Vec::new(); - let mut letters_used = 0; - let mut appearances = MutMap::default(); - let mut taken = MutMap::default(); - - // Populate names_needed - find_names_needed(variable, subs, &mut roots, &mut appearances, &mut taken); - - let mut recursion_structs_to_expand = vec![]; - - for root in roots { - // show the type variable number instead of `*`. useful for debugging - // set_root_name(root, (format!("<{:?}>", root).into()), subs); - match appearances.get(&root) { - Some(Appearances::Multiple) => { - letters_used = name_root(letters_used, root, subs, &mut taken); - } - Some(Appearances::Single) => { - if let Content::RecursionVar { structure, .. } = - subs.get_content_without_compacting(root) - { - recursion_structs_to_expand.push(*structure); - letters_used = name_root(letters_used, root, subs, &mut taken); - } - } - _ => {} - } - } - - NamedResult { - recursion_structs_to_expand, - } -} - -fn name_root( - letters_used: u32, - root: Variable, - subs: &mut Subs, - taken: &mut MutMap, -) -> u32 { - let (generated_name, new_letters_used) = - name_type_var(letters_used, &mut taken.keys(), |var, str| { - var.as_str() == str - }); - - taken.insert(generated_name.clone(), root); - - set_root_name(root, generated_name, subs); - - new_letters_used -} - -fn set_root_name(root: Variable, name: Lowercase, subs: &mut Subs) { - use crate::subs::Content::*; - - let old_content = subs.get_content_without_compacting(root); - - match old_content { - FlexVar(_) => { - let name_index = SubsIndex::push_new(&mut subs.field_names, name); - let content = FlexVar(Some(name_index)); - subs.set_content(root, content); - } - &FlexAbleVar(_, ability) => { - let name_index = SubsIndex::push_new(&mut subs.field_names, name); - let content = FlexAbleVar(Some(name_index), ability); - subs.set_content(root, content); - } - RecursionVar { - opt_name: None, - structure, - } => { - let structure = *structure; - let name_index = SubsIndex::push_new(&mut subs.field_names, name); - let content = RecursionVar { - structure, - opt_name: Some(name_index), - }; - subs.set_content(root, content); - } - RecursionVar { - opt_name: Some(_existing), - .. - } => { - panic!("TODO FIXME - make sure the generated name does not clash with any bound vars! In other words, if the user decided to name a type variable 'a', make sure we don't generate 'a' to name a different one!"); - } - - _ => (), - } -} - -#[derive(Default)] -struct Context<'a> { - able_variables: Vec<(&'a str, Symbol)>, - recursion_structs_to_expand: Vec, -} - -fn content_to_string( - content: &Content, - subs: &Subs, - home: ModuleId, - interns: &Interns, - named_result: NamedResult, - debug_print: DebugPrint, -) -> String { - let mut buf = String::new(); - let env = Env { - home, - interns, - debug: debug_print, - }; - let mut ctx = Context { - able_variables: vec![], - recursion_structs_to_expand: named_result.recursion_structs_to_expand, - }; - - write_content(&env, &mut ctx, content, subs, &mut buf, Parens::Unnecessary); - - ctx.able_variables.sort(); - ctx.able_variables.dedup(); - for (i, (var, ability)) in ctx.able_variables.into_iter().enumerate() { - buf.push_str(if i == 0 { " | " } else { ", " }); - buf.push_str(var); - buf.push_str(" has "); - write_symbol(&env, ability, &mut buf); - } - - buf -} - -pub fn name_and_print_var( - var: Variable, - subs: &mut Subs, - home: ModuleId, - interns: &Interns, - debug_print: DebugPrint, -) -> String { - let named_result = name_all_type_vars(var, subs); - let content = subs.get_content_without_compacting(var); - content_to_string(content, subs, home, interns, named_result, debug_print) -} - -pub fn get_single_arg<'a>(subs: &'a Subs, args: &'a AliasVariables) -> &'a Content { - debug_assert_eq!(args.len(), 1); - - let arg_var_index = args - .into_iter() - .next() - .expect("Num was not applied to a type argument!"); - let arg_var = subs[arg_var_index]; - subs.get_content_without_compacting(arg_var) -} - -fn write_content<'a>( - env: &Env, - ctx: &mut Context<'a>, - content: &Content, - subs: &'a Subs, - buf: &mut String, - parens: Parens, -) { - use crate::subs::Content::*; - - match content { - FlexVar(Some(name_index)) => { - let name = &subs.field_names[name_index.index as usize]; - buf.push_str(name.as_str()) - } - FlexVar(None) => buf.push_str(WILDCARD), - RigidVar(name_index) => { - let name = &subs.field_names[name_index.index as usize]; - buf.push_str(name.as_str()) - } - FlexAbleVar(opt_name_index, ability) => { - let name = opt_name_index - .map(|name_index| subs.field_names[name_index.index as usize].as_str()) - .unwrap_or(WILDCARD); - ctx.able_variables.push((name, *ability)); - buf.push_str(name); - } - RigidAbleVar(name_index, ability) => { - let name = subs.field_names[name_index.index as usize].as_str(); - ctx.able_variables.push((name, *ability)); - buf.push_str(name); - } - RecursionVar { - opt_name, - structure, - } => match opt_name { - Some(name_index) => { - if let Some(idx) = ctx - .recursion_structs_to_expand - .iter() - .position(|v| v == structure) - { - ctx.recursion_structs_to_expand.swap_remove(idx); - - write_content( - env, - ctx, - subs.get_content_without_compacting(*structure), - subs, - buf, - parens, - ); - } else { - let name = &subs.field_names[name_index.index as usize]; - buf.push_str(name.as_str()) - } - } - None => { - unreachable!("This should always be filled in!") - } - }, - Structure(flat_type) => write_flat_type(env, ctx, flat_type, subs, buf, parens), - Alias(symbol, args, actual, _kind) => { - let write_parens = parens == Parens::InTypeParam && !args.is_empty(); - - match *symbol { - Symbol::NUM_NUM => { - let content = get_single_arg(subs, args); - match *content { - Alias(nested, args, _actual, _kind) => match nested { - Symbol::NUM_INTEGER => { - write_integer( - env, - ctx, - get_single_arg(subs, &args), - subs, - buf, - parens, - false, - ); - } - Symbol::NUM_FLOATINGPOINT => write_float( - env, - ctx, - get_single_arg(subs, &args), - subs, - buf, - parens, - write_parens, - ), - - _ => write_parens!(write_parens, buf, { - buf.push_str("Num "); - write_content(env, ctx, content, subs, buf, parens); - }), - }, - - _ => write_parens!(write_parens, buf, { - buf.push_str("Num "); - write_content(env, ctx, content, subs, buf, parens); - }), - } - } - - Symbol::NUM_INT => { - let content = get_single_arg(subs, args); - - write_integer(env, ctx, content, subs, buf, parens, write_parens) - } - - Symbol::NUM_FRAC => write_float( - env, - ctx, - get_single_arg(subs, args), - subs, - buf, - parens, - write_parens, - ), - - _ if env.debug.print_only_under_alias => write_parens!(write_parens, buf, { - let content = subs.get_content_without_compacting(*actual); - write_content(env, ctx, content, subs, buf, parens) - }), - - _ => write_parens!(write_parens, buf, { - write_symbol(env, *symbol, buf); - - for var_index in args.named_type_arguments() { - let var = subs[var_index]; - buf.push(' '); - write_content( - env, - ctx, - subs.get_content_without_compacting(var), - subs, - buf, - Parens::InTypeParam, - ); - } - - roc_debug_flags::dbg_do!(roc_debug_flags::ROC_PRETTY_PRINT_ALIAS_CONTENTS, { - buf.push_str("[[ but really "); - let content = subs.get_content_without_compacting(*actual); - write_content(env, ctx, content, subs, buf, parens); - buf.push_str("]]"); - }); - }), - } - } - LambdaSet(subs::LambdaSet { - solved, - recursion_var, - unspecialized, - }) => { - debug_assert!(env.debug.print_lambda_sets); - - buf.push_str("[["); - - let print_symbol = |symbol: &Symbol| { - format!( - "{}({})", - symbol.as_str(env.interns), - symbol.ident_id().index(), - ) - }; - - write_sorted_tags2( - env, - ctx, - subs, - buf, - solved.unsorted_lambdas(subs), - print_symbol, - ); - - buf.push(']'); - - if let Some(rec_var) = recursion_var.into_variable() { - buf.push_str(" as "); - write_content( - env, - ctx, - subs.get_content_without_compacting(rec_var), - subs, - buf, - parens, - ) - } - - for Uls(var, member, region) in subs.get_subs_slice(*unspecialized) { - buf.push_str(" + "); - write_content( - env, - ctx, - subs.get_content_without_compacting(*var), - subs, - buf, - Parens::Unnecessary, - ); - buf.push(':'); - buf.push_str(&print_symbol(member)); - buf.push(':'); - buf.push_str(®ion.to_string()); - } - - buf.push(']'); - } - RangedNumber(typ, _range_vars) => write_content( - env, - ctx, - subs.get_content_without_compacting(*typ), - subs, - buf, - parens, - ), - Error => buf.push_str(""), - } -} - -fn write_float<'a>( - env: &Env, - ctx: &mut Context<'a>, - content: &Content, - subs: &'a Subs, - buf: &mut String, - parens: Parens, - write_parens: bool, -) { - use crate::subs::Content::*; - match content { - Alias(Symbol::NUM_BINARY32, _, _, _) => buf.push_str("F32"), - Alias(Symbol::NUM_BINARY64, _, _, _) => buf.push_str("F64"), - Alias(Symbol::NUM_DECIMAL, _, _, _) => buf.push_str("Dec"), - _ => write_parens!(write_parens, buf, { - buf.push_str("Float "); - write_content(env, ctx, content, subs, buf, parens); - }), - } -} - -fn write_integer<'a>( - env: &Env, - ctx: &mut Context<'a>, - content: &Content, - subs: &'a Subs, - buf: &mut String, - parens: Parens, - write_parens: bool, -) { - use crate::subs::Content::*; - - macro_rules! derive_num_writes { - ($($lit:expr, $tag:path)*) => { - write_parens!( - write_parens, - buf, - match content { - $( - &Alias($tag, _, _, _) => { - buf.push_str($lit) - }, - )* - actual => { - buf.push_str("Int "); - write_content(env, ctx, actual, subs, buf, parens); - } - } - ) - } - } - - derive_num_writes! { - "U8", Symbol::NUM_UNSIGNED8 - "U16", Symbol::NUM_UNSIGNED16 - "U32", Symbol::NUM_UNSIGNED32 - "U64", Symbol::NUM_UNSIGNED64 - "U128", Symbol::NUM_UNSIGNED128 - "I8", Symbol::NUM_SIGNED8 - "I16", Symbol::NUM_SIGNED16 - "I32", Symbol::NUM_SIGNED32 - "I64", Symbol::NUM_SIGNED64 - "I128", Symbol::NUM_SIGNED128 - "Nat", Symbol::NUM_NATURAL - } -} - -enum ExtContent<'a> { - Empty, - Content(Variable, &'a Content), -} - -impl<'a> ExtContent<'a> { - fn from_var(subs: &'a Subs, ext: Variable) -> Self { - let content = subs.get_content_without_compacting(ext); - match content { - Content::Structure(FlatType::EmptyTagUnion) => ExtContent::Empty, - Content::Structure(FlatType::EmptyRecord) => ExtContent::Empty, - - Content::FlexVar(_) | Content::RigidVar(_) => ExtContent::Content(ext, content), - - other => unreachable!("something weird ended up in an ext var: {:?}", other), - } - } -} - -fn write_ext_content<'a>( - env: &Env, - ctx: &mut Context<'a>, - subs: &'a Subs, - buf: &mut String, - ext_content: ExtContent<'a>, - parens: Parens, -) { - if let ExtContent::Content(_, content) = ext_content { - // This is an open record or tag union, so print the variable - // right after the '}' or ']' - // - // e.g. the "*" at the end of `{ x: I64 }*` - // or the "r" at the end of `{ x: I64 }r` - write_content(env, ctx, content, subs, buf, parens) - } -} - -fn write_sorted_tags2<'a, L>( - env: &Env, - ctx: &mut Context<'a>, - subs: &'a Subs, - buf: &mut String, - tags: UnsortedUnionLabels, - label_to_string: impl Fn(&L) -> String, -) where - L: Label + Ord, -{ - let mut sorted_fields = tags.tags; - - sorted_fields.sort_by(|(a, _), (b, _)| a.cmp(b)); - - let mut any_written_yet = false; - - for (label, vars) in sorted_fields { - if any_written_yet { - buf.push_str(", "); - } else { - any_written_yet = true; - } - - buf.push_str(&label_to_string(label)); - - for var in vars { - buf.push(' '); - write_content( - env, - ctx, - subs.get_content_without_compacting(*var), - subs, - buf, - Parens::InTypeParam, - ); - } - } -} - -fn write_sorted_tags<'a>( - env: &Env, - ctx: &mut Context<'a>, - subs: &'a Subs, - buf: &mut String, - tags: &MutMap>, - ext_var: Variable, -) -> ExtContent<'a> { - // Sort the fields so they always end up in the same order. - let mut sorted_fields = Vec::with_capacity(tags.len()); - - for (label, vars) in tags { - sorted_fields.push((label, vars)); - } - - // If the `ext` contains tags, merge them into the list of tags. - // this can occur when inferring mutually recursive tags - let mut from_ext = Default::default(); - let _ext_content = chase_ext_tag_union(subs, ext_var, &mut from_ext); - - for (tag_name, arguments) in from_ext.iter() { - sorted_fields.push((tag_name, arguments)); - } - - sorted_fields.sort_by(|(a, _), (b, _)| a.as_ident_str().cmp(&b.as_ident_str())); - - let mut any_written_yet = false; - - for (label, vars) in sorted_fields { - if any_written_yet { - buf.push_str(", "); - } else { - any_written_yet = true; - } - - buf.push_str(label.as_ident_str().as_str()); - - for var in vars { - buf.push(' '); - write_content( - env, - ctx, - subs.get_content_without_compacting(*var), - subs, - buf, - Parens::InTypeParam, - ); - } - } - - ExtContent::from_var(subs, ext_var) -} - -fn write_flat_type<'a>( - env: &Env, - ctx: &mut Context<'a>, - flat_type: &FlatType, - subs: &'a Subs, - buf: &mut String, - parens: Parens, -) { - use crate::subs::FlatType::*; - - match flat_type { - Apply(symbol, args) => write_apply( - env, - ctx, - *symbol, - subs.get_subs_slice(*args), - subs, - buf, - parens, - ), - EmptyRecord => buf.push_str(EMPTY_RECORD), - EmptyTagUnion => buf.push_str(EMPTY_TAG_UNION), - Func(args, closure, ret) => write_fn( - env, - ctx, - subs.get_subs_slice(*args), - *closure, - *ret, - subs, - buf, - parens, - ), - Record(fields, ext_var) => { - use crate::types::{gather_fields, RecordStructure}; - - // If the `ext` has concrete fields (e.g. { foo : I64}{ bar : Bool }), merge them - let RecordStructure { - fields: sorted_fields, - ext, - } = gather_fields(subs, *fields, *ext_var) - .expect("Something ended up weird in this record type"); - let ext_var = ext; - - if fields.is_empty() { - buf.push_str(EMPTY_RECORD) - } else { - buf.push_str("{ "); - - let mut any_written_yet = false; - - for (label, record_field) in sorted_fields { - use RecordField::*; - - let var = *record_field.as_inner(); - - if any_written_yet { - buf.push_str(", "); - } else { - any_written_yet = true; - } - buf.push_str(label.as_str()); - - match record_field { - Optional(_) => buf.push_str(" ? "), - Required(_) => buf.push_str(" : "), - Demanded(_) => buf.push_str(" : "), - }; - - write_content( - env, - ctx, - subs.get_content_without_compacting(var), - subs, - buf, - Parens::Unnecessary, - ); - } - - buf.push_str(" }"); - } - - match subs.get_content_without_compacting(ext_var) { - Content::Structure(EmptyRecord) => { - // This is a closed record. We're done! - } - content => { - // This is an open record, so print the variable - // right after the '}' - // - // e.g. the "*" at the end of `{ x: I64 }*` - // or the "r" at the end of `{ x: I64 }r` - write_content(env, ctx, content, subs, buf, parens) - } - } - } - TagUnion(tags, ext_var) => { - buf.push('['); - - // Sort the fields so they always end up in the same order. - let (tags, new_ext_var) = tags.unsorted_tags_and_ext(subs, *ext_var); - write_sorted_tags2(env, ctx, subs, buf, tags, |tag| tag.0.as_str().to_string()); - - buf.push(']'); - - write_ext_content( - env, - ctx, - subs, - buf, - ExtContent::from_var(subs, new_ext_var), - parens, - ) - } - - FunctionOrTagUnion(tag_name, _, ext_var) => { - buf.push('['); - - let mut tags: MutMap = MutMap::default(); - tags.insert(subs[*tag_name].clone(), vec![]); - let ext_content = write_sorted_tags(env, ctx, subs, buf, &tags, *ext_var); - - buf.push(']'); - - write_ext_content(env, ctx, subs, buf, ext_content, parens) - } - - RecursiveTagUnion(rec_var, tags, ext_var) => { - buf.push('['); - - let (tags, new_ext_var) = tags.unsorted_tags_and_ext(subs, *ext_var); - write_sorted_tags2(env, ctx, subs, buf, tags, |tag| tag.0.as_str().to_string()); - - buf.push(']'); - - write_ext_content( - env, - ctx, - subs, - buf, - ExtContent::from_var(subs, new_ext_var), - parens, - ); - - buf.push_str(" as "); - write_content( - env, - ctx, - subs.get_content_without_compacting(*rec_var), - subs, - buf, - parens, - ) - } - Erroneous(problem) => { - buf.push_str(&format!("", problem)); - } - } -} - -fn push_union<'a, L: Label>( - subs: &'a Subs, - tags: &UnionLabels, - fields: &mut Vec<(L, Vec)>, -) { - fields.reserve(tags.len()); - for (name_index, slice_index) in tags.iter_all() { - let subs_slice = subs[slice_index]; - let slice = subs.get_subs_slice(subs_slice); - let tag_name = L::index_subs(subs, name_index).clone(); - - fields.push((tag_name, slice.to_vec())); - } -} - -pub fn chase_ext_tag_union<'a>( - subs: &'a Subs, - var: Variable, - fields: &mut Vec<(TagName, Vec)>, -) -> Result<(), (Variable, &'a Content)> { - use FlatType::*; - match subs.get_content_without_compacting(var) { - Content::Structure(EmptyTagUnion) => Ok(()), - Content::Structure(TagUnion(tags, ext_var)) => { - push_union(subs, tags, fields); - chase_ext_tag_union(subs, *ext_var, fields) - } - - Content::Structure(RecursiveTagUnion(_, tags, ext_var)) => { - push_union(subs, tags, fields); - chase_ext_tag_union(subs, *ext_var, fields) - } - Content::Structure(FunctionOrTagUnion(tag_name, _, ext_var)) => { - fields.push((subs[*tag_name].clone(), vec![])); - - chase_ext_tag_union(subs, *ext_var, fields) - } - - Content::Alias(_, _, var, _) => chase_ext_tag_union(subs, *var, fields), - - content => Err((var, content)), - } -} - -pub enum ResolvedLambdaSet { - Set(Vec<(Symbol, Vec)>), - /// TODO: figure out if this can happen in a correct program, or is the result of a bug in our - /// compiler. See https://github.com/rtfeldman/roc/issues/3163. - Unbound, -} - -pub fn resolve_lambda_set(subs: &Subs, mut var: Variable) -> ResolvedLambdaSet { - let mut set = vec![]; - loop { - match subs.get_content_without_compacting(var) { - Content::LambdaSet(subs::LambdaSet { - solved, - recursion_var: _, - unspecialized, - }) => { - debug_assert!( - unspecialized.is_empty(), - "unspecialized lambda sets left over during resolution" - ); - push_union(subs, solved, &mut set); - return ResolvedLambdaSet::Set(set); - } - Content::RecursionVar { structure, .. } => { - var = *structure; - } - Content::FlexVar(_) => return ResolvedLambdaSet::Unbound, - - c => internal_error!("called with a non-lambda set {:?}", c), - } - } -} - -fn write_apply<'a>( - env: &Env, - ctx: &mut Context<'a>, - symbol: Symbol, - args: &[Variable], - subs: &'a Subs, - buf: &mut String, - parens: Parens, -) { - let write_parens = parens == Parens::InTypeParam && !args.is_empty(); - - // Hardcoded type aliases - match symbol { - Symbol::STR_STR => { - buf.push_str("Str"); - } - Symbol::NUM_NUM => { - let arg = args - .iter() - .next() - .unwrap_or_else(|| panic!("Num did not have any type parameters somehow.")); - let arg_content = subs.get_content_without_compacting(*arg); - let mut arg_param = String::new(); - - let mut default_case = |subs, content| { - if write_parens { - buf.push('('); - } - - write_content(env, ctx, content, subs, &mut arg_param, Parens::InTypeParam); - buf.push_str("Num "); - buf.push_str(&arg_param); - - if write_parens { - buf.push(')'); - } - }; - - match &arg_content { - Content::Structure(FlatType::Apply(symbol, nested_args)) => match *symbol { - Symbol::NUM_INTEGER if nested_args.len() == 1 => { - buf.push_str("I64"); - } - Symbol::NUM_FLOATINGPOINT if nested_args.len() == 1 => { - buf.push_str("F64"); - } - _ => default_case(subs, arg_content), - }, - _ => default_case(subs, arg_content), - } - } - _ => { - if write_parens { - buf.push('('); - } - - write_symbol(env, symbol, buf); - - for arg in args { - buf.push(' '); - write_content( - env, - ctx, - subs.get_content_without_compacting(*arg), - subs, - buf, - Parens::InTypeParam, - ); - } - - if write_parens { - buf.push(')'); - } - } - } -} - -#[allow(clippy::too_many_arguments)] -fn write_fn<'a>( - env: &Env, - ctx: &mut Context<'a>, - args: &[Variable], - closure: Variable, - ret: Variable, - subs: &'a Subs, - buf: &mut String, - parens: Parens, -) { - let mut needs_comma = false; - let use_parens = parens != Parens::Unnecessary; - - if use_parens { - buf.push('('); - } - - for arg in args { - if needs_comma { - buf.push_str(", "); - } else { - needs_comma = true; - } - - write_content( - env, - ctx, - subs.get_content_without_compacting(*arg), - subs, - buf, - Parens::InFn, - ); - } - - if !env.debug.print_lambda_sets { - buf.push_str(" -> "); - } else { - buf.push_str(" -"); - write_content( - env, - ctx, - subs.get_content_without_compacting(closure), - subs, - buf, - parens, - ); - buf.push_str("-> "); - } - - write_content( - env, - ctx, - subs.get_content_without_compacting(ret), - subs, - buf, - Parens::InFn, - ); - - if use_parens { - buf.push(')'); - } -} - -fn write_symbol(env: &Env, symbol: Symbol, buf: &mut String) { - let interns = &env.interns; - let ident_str = symbol.as_str(interns); - let module_id = symbol.module_id(); - - // Don't qualify the symbol if it's in our home module, - // or if it's a builtin (since all their types are always in scope) - if module_id != env.home && !module_id.is_builtin() { - buf.push_str(module_id.to_ident_str(interns).as_str()); - buf.push('.'); - } - - buf.push_str(ident_str); -} diff --git a/compiler/types/src/solved_types.rs b/compiler/types/src/solved_types.rs deleted file mode 100644 index 30e8234a3c..0000000000 --- a/compiler/types/src/solved_types.rs +++ /dev/null @@ -1,305 +0,0 @@ -use crate::subs::{VarId, VarStore, Variable}; -use crate::types::{AliasKind, OptAbleType, Problem, RecordField, Type, TypeExtension}; -use roc_collections::all::{ImMap, SendMap}; -use roc_module::ident::{Lowercase, TagName}; -use roc_module::symbol::Symbol; -use roc_region::all::{Loc, Region}; - -/// A marker that a given Subs has been solved. -/// The only way to obtain a Solved is by running the solver on it. -#[derive(Clone, Debug)] -pub struct Solved(pub T); - -impl Solved { - pub fn inner(&self) -> &'_ T { - &self.0 - } - - pub fn inner_mut(&mut self) -> &'_ mut T { - &mut self.0 - } - - pub fn into_inner(self) -> T { - self.0 - } -} - -#[derive(Debug, Clone)] -pub struct SolvedLambdaSet(pub SolvedType); - -/// This is a fully solved type, with no Variables remaining in it. -#[derive(Debug, Clone)] -pub enum SolvedType { - /// A function. The types of its arguments, then the type of its return value. - Func(Vec, Box, Box), - /// Applying a type to some arguments (e.g. Map.Map String Int) - Apply(Symbol, Vec), - /// A bound type variable, e.g. `a` in `(a -> a)` - Rigid(Lowercase), - Flex(VarId), - Wildcard, - /// Inline type alias, e.g. `as List a` in `[Cons a (List a), Nil] as List a` - Record { - fields: Vec<(Lowercase, RecordField)>, - /// The row type variable in an open record, e.g. the `r` in `{ name: Str }r`. - /// This is None if it's a closed record annotation like `{ name: Str }`. - ext: Box, - }, - EmptyRecord, - TagUnion(Vec<(TagName, Vec)>, Box), - LambdaTag(Symbol, Vec), - FunctionOrTagUnion(TagName, Symbol, Box), - RecursiveTagUnion(VarId, Vec<(TagName, Vec)>, Box), - EmptyTagUnion, - /// A type from an Invalid module - Erroneous(Problem), - - Alias( - Symbol, - Vec, - Vec, - Box, - AliasKind, - ), - - HostExposedAlias { - name: Symbol, - arguments: Vec, - lambda_set_variables: Vec, - actual_var: VarId, - actual: Box, - }, - - /// A type error - Error, -} - -#[derive(Clone, Debug)] -pub struct BuiltinAlias { - pub region: Region, - pub vars: Vec>, - pub typ: SolvedType, - pub kind: AliasKind, -} - -#[derive(Debug, Clone, Default)] -pub struct FreeVars { - pub named_vars: ImMap, - pub unnamed_vars: ImMap, - pub wildcards: Vec, -} - -pub fn to_type( - solved_type: &SolvedType, - free_vars: &mut FreeVars, - var_store: &mut VarStore, -) -> Type { - use SolvedType::*; - - match solved_type { - Func(args, closure, ret) => { - let mut new_args = Vec::with_capacity(args.len()); - - for arg in args { - new_args.push(to_type(arg, free_vars, var_store)); - } - - let new_ret = to_type(ret, free_vars, var_store); - let new_closure = to_type(closure, free_vars, var_store); - - Type::Function(new_args, Box::new(new_closure), Box::new(new_ret)) - } - Apply(symbol, args) => { - let mut new_args = Vec::with_capacity(args.len()); - - for arg in args { - new_args.push(to_type(arg, free_vars, var_store)); - } - - Type::Apply(*symbol, new_args, Region::zero()) - } - Rigid(lowercase) => { - if let Some(var) = free_vars.named_vars.get(lowercase) { - Type::Variable(*var) - } else { - let var = var_store.fresh(); - free_vars.named_vars.insert(lowercase.clone(), var); - Type::Variable(var) - } - } - Flex(var_id) => Type::Variable(var_id_to_flex_var(*var_id, free_vars, var_store)), - Wildcard => { - let var = var_store.fresh(); - free_vars.wildcards.push(var); - Type::Variable(var) - } - Record { fields, ext } => { - use RecordField::*; - - let mut new_fields = SendMap::default(); - - for (label, field) in fields { - let field_val = match field { - Required(typ) => Required(to_type(typ, free_vars, var_store)), - Optional(typ) => Optional(to_type(typ, free_vars, var_store)), - Demanded(typ) => Demanded(to_type(typ, free_vars, var_store)), - }; - - new_fields.insert(label.clone(), field_val); - } - - let ext = match ext.as_ref() { - SolvedType::EmptyRecord => TypeExtension::Closed, - other => TypeExtension::Open(Box::new(to_type(other, free_vars, var_store))), - }; - - Type::Record(new_fields, ext) - } - EmptyRecord => Type::EmptyRec, - EmptyTagUnion => Type::EmptyTagUnion, - TagUnion(tags, ext) => { - let mut new_tags = Vec::with_capacity(tags.len()); - - for (tag_name, args) in tags { - let mut new_args = Vec::with_capacity(args.len()); - - for arg in args.iter() { - new_args.push(to_type(arg, free_vars, var_store)); - } - - new_tags.push((tag_name.clone(), new_args)); - } - - let ext = match ext.as_ref() { - SolvedType::EmptyTagUnion => TypeExtension::Closed, - other => TypeExtension::Open(Box::new(to_type(other, free_vars, var_store))), - }; - - Type::TagUnion(new_tags, ext) - } - LambdaTag(name, args) => { - let mut new_args = Vec::with_capacity(args.len()); - - for arg in args.iter() { - new_args.push(to_type(arg, free_vars, var_store)); - } - - Type::ClosureTag { - name: *name, - captures: new_args, - } - } - FunctionOrTagUnion(tag_name, symbol, ext) => { - let ext = match ext.as_ref() { - SolvedType::EmptyTagUnion => TypeExtension::Closed, - other => TypeExtension::Open(Box::new(to_type(other, free_vars, var_store))), - }; - - Type::FunctionOrTagUnion(tag_name.clone(), *symbol, ext) - } - RecursiveTagUnion(rec_var_id, tags, ext) => { - let mut new_tags = Vec::with_capacity(tags.len()); - - for (tag_name, args) in tags { - let mut new_args = Vec::with_capacity(args.len()); - - for arg in args.iter() { - new_args.push(to_type(arg, free_vars, var_store)); - } - - new_tags.push((tag_name.clone(), new_args)); - } - - let ext = match ext.as_ref() { - SolvedType::EmptyTagUnion => TypeExtension::Closed, - other => TypeExtension::Open(Box::new(to_type(other, free_vars, var_store))), - }; - - let rec_var = free_vars - .unnamed_vars - .get(rec_var_id) - .expect("rec var not in unnamed vars"); - - Type::RecursiveTagUnion(*rec_var, new_tags, ext) - } - Alias(symbol, solved_type_variables, solved_lambda_sets, solved_actual, kind) => { - let mut type_variables = Vec::with_capacity(solved_type_variables.len()); - - for solved_arg in solved_type_variables { - type_variables.push(OptAbleType { - typ: to_type(solved_arg, free_vars, var_store), - // TODO: is this always correct? - opt_ability: None, - }); - } - - let mut lambda_set_variables = Vec::with_capacity(solved_lambda_sets.len()); - for solved_set in solved_lambda_sets { - lambda_set_variables.push(crate::types::LambdaSet(to_type( - &solved_set.0, - free_vars, - var_store, - ))) - } - - let actual = to_type(solved_actual, free_vars, var_store); - - Type::Alias { - symbol: *symbol, - type_arguments: type_variables, - lambda_set_variables, - actual: Box::new(actual), - kind: *kind, - } - } - HostExposedAlias { - name, - arguments: solved_type_variables, - lambda_set_variables: solved_lambda_sets, - actual_var, - actual: solved_actual, - } => { - let mut type_variables = Vec::with_capacity(solved_type_variables.len()); - - for solved_arg in solved_type_variables { - type_variables.push(to_type(solved_arg, free_vars, var_store)); - } - - let mut lambda_set_variables = Vec::with_capacity(solved_lambda_sets.len()); - for solved_set in solved_lambda_sets { - lambda_set_variables.push(crate::types::LambdaSet(to_type( - &solved_set.0, - free_vars, - var_store, - ))) - } - - let actual = to_type(solved_actual, free_vars, var_store); - - Type::HostExposedAlias { - name: *name, - type_arguments: type_variables, - lambda_set_variables, - actual_var: var_id_to_flex_var(*actual_var, free_vars, var_store), - actual: Box::new(actual), - } - } - Error => Type::Erroneous(Problem::SolvedTypeError), - Erroneous(problem) => Type::Erroneous(problem.clone()), - } -} - -fn var_id_to_flex_var( - var_id: VarId, - free_vars: &mut FreeVars, - var_store: &mut VarStore, -) -> Variable { - if let Some(var) = free_vars.unnamed_vars.get(&var_id) { - *var - } else { - let var = var_store.fresh(); - free_vars.unnamed_vars.insert(var_id, var); - - var - } -} diff --git a/compiler/types/src/subs.rs b/compiler/types/src/subs.rs deleted file mode 100644 index c7bb17f8bf..0000000000 --- a/compiler/types/src/subs.rs +++ /dev/null @@ -1,4999 +0,0 @@ -#![deny(unsafe_op_in_unsafe_fn)] -use crate::types::{ - name_type_var, AliasKind, ErrorType, Problem, RecordField, RecordFieldsError, TypeExt, Uls, -}; -use roc_collections::all::{ImMap, ImSet, MutSet, SendMap}; -use roc_collections::{VecMap, VecSet}; -use roc_error_macros::internal_error; -use roc_module::ident::{Lowercase, TagName, Uppercase}; -use roc_module::symbol::Symbol; -use std::fmt; -use std::iter::{once, Iterator, Map}; - -use crate::unification_table::{Snapshot, UnificationTable}; - -// if your changes cause this number to go down, great! -// please change it to the lower number. -// if it went up, maybe check that the change is really required -roc_error_macros::assert_sizeof_all!(Descriptor, 5 * 8); -roc_error_macros::assert_sizeof_all!(Content, 3 * 8 + 4); -roc_error_macros::assert_sizeof_all!(FlatType, 3 * 8); -roc_error_macros::assert_sizeof_all!(UnionTags, 12); -roc_error_macros::assert_sizeof_all!(RecordFields, 2 * 8); - -roc_error_macros::assert_sizeof_aarch64!(Problem, 6 * 8); -roc_error_macros::assert_sizeof_wasm!(Problem, 32); -roc_error_macros::assert_sizeof_default!(Problem, 6 * 8); - -#[derive(Clone, Copy, Hash, PartialEq, Eq)] -pub struct Mark(i32); - -impl Mark { - pub const NONE: Mark = Mark(2); - pub const OCCURS: Mark = Mark(1); - pub const GET_VAR_NAMES: Mark = Mark(0); - - #[inline(always)] - pub fn next(self) -> Mark { - Mark(self.0 + 1) - } -} - -impl fmt::Debug for Mark { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - if self == &Mark::NONE { - write!(f, "none") - } else if self == &Mark::OCCURS { - write!(f, "occurs") - } else if self == &Mark::GET_VAR_NAMES { - write!(f, "get_var_names") - } else { - write!(f, "Mark({})", self.0) - } - } -} - -#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] -pub enum ErrorTypeContext { - None, - ExpandRanges, -} - -struct ErrorTypeState { - taken: MutSet, - letters_used: u32, - problems: Vec, - context: ErrorTypeContext, - recursive_tag_unions_seen: Vec, -} - -#[derive(Clone, Copy, Debug)] -struct SubsHeader { - utable: u64, - variables: u64, - tag_names: u64, - closure_names: u64, - field_names: u64, - record_fields: u64, - variable_slices: u64, - unspecialized_lambda_sets: u64, - exposed_vars_by_symbol: u64, -} - -impl SubsHeader { - fn from_subs(subs: &Subs, exposed_vars_by_symbol: usize) -> Self { - // TODO what do we do with problems? they should - // be reported and then removed from Subs I think - debug_assert!(subs.problems.is_empty()); - - Self { - utable: subs.utable.len() as u64, - variables: subs.variables.len() as u64, - tag_names: subs.tag_names.len() as u64, - closure_names: subs.closure_names.len() as u64, - field_names: subs.field_names.len() as u64, - record_fields: subs.record_fields.len() as u64, - variable_slices: subs.variable_slices.len() as u64, - unspecialized_lambda_sets: subs.unspecialized_lambda_sets.len() as u64, - exposed_vars_by_symbol: exposed_vars_by_symbol as u64, - } - } - - fn to_array(self) -> [u8; std::mem::size_of::()] { - unsafe { std::mem::transmute(self) } - } - - fn from_array(array: [u8; std::mem::size_of::()]) -> Self { - unsafe { std::mem::transmute(array) } - } -} - -unsafe fn slice_as_bytes(slice: &[T]) -> &[u8] { - let ptr = slice.as_ptr(); - let byte_length = std::mem::size_of::() * slice.len(); - - unsafe { std::slice::from_raw_parts(ptr as *const u8, byte_length) } -} - -fn round_to_multiple_of(value: usize, base: usize) -> usize { - (value + (base - 1)) / base * base -} - -struct SerializedTagName(SubsSlice); - -impl Subs { - pub fn serialize( - &self, - exposed_vars_by_symbol: &[(Symbol, Variable)], - writer: &mut impl std::io::Write, - ) -> std::io::Result { - let mut written = 0; - - let header = SubsHeader::from_subs(self, exposed_vars_by_symbol.len()).to_array(); - written += header.len(); - writer.write_all(&header)?; - - written = self.utable.serialize(writer, written)?; - - written = Self::serialize_slice(&self.variables, writer, written)?; - written = Self::serialize_tag_names(&self.tag_names, writer, written)?; - written = Self::serialize_slice(&self.closure_names, writer, written)?; - written = Self::serialize_field_names(&self.field_names, writer, written)?; - written = Self::serialize_slice(&self.record_fields, writer, written)?; - written = Self::serialize_slice(&self.variable_slices, writer, written)?; - written = Self::serialize_slice(&self.unspecialized_lambda_sets, writer, written)?; - written = Self::serialize_slice(exposed_vars_by_symbol, writer, written)?; - - Ok(written) - } - - /// Lowercase can be heap-allocated - fn serialize_field_names( - lowercases: &[Lowercase], - writer: &mut impl std::io::Write, - written: usize, - ) -> std::io::Result { - let mut buf: Vec = Vec::new(); - let mut slices: Vec> = Vec::new(); - - for field_name in lowercases { - let slice = - SubsSlice::extend_new(&mut buf, field_name.as_str().as_bytes().iter().copied()); - slices.push(slice); - } - - let written = Self::serialize_slice(&slices, writer, written)?; - - Self::serialize_slice(&buf, writer, written) - } - - /// Global tag names can be heap-allocated - fn serialize_tag_names( - tag_names: &[TagName], - writer: &mut impl std::io::Write, - written: usize, - ) -> std::io::Result { - let mut buf: Vec = Vec::new(); - let mut slices: Vec = Vec::new(); - - for TagName(uppercase) in tag_names { - let slice = - SubsSlice::extend_new(&mut buf, uppercase.as_str().as_bytes().iter().copied()); - let serialized = SerializedTagName(slice); - slices.push(serialized); - } - - let written = Self::serialize_slice(&slices, writer, written)?; - - Self::serialize_slice(&buf, writer, written) - } - - pub(crate) fn serialize_slice( - slice: &[T], - writer: &mut impl std::io::Write, - written: usize, - ) -> std::io::Result { - let alignment = std::mem::align_of::(); - let padding_bytes = round_to_multiple_of(written, alignment) - written; - - for _ in 0..padding_bytes { - writer.write_all(&[0])?; - } - - let bytes_slice = unsafe { slice_as_bytes(slice) }; - writer.write_all(bytes_slice)?; - - Ok(written + padding_bytes + bytes_slice.len()) - } - - pub fn deserialize(bytes: &[u8]) -> (Self, &[(Symbol, Variable)]) { - let mut offset = 0; - let header_slice = &bytes[..std::mem::size_of::()]; - offset += header_slice.len(); - let header = SubsHeader::from_array(header_slice.try_into().unwrap()); - - let (utable, offset) = UnificationTable::deserialize(bytes, header.utable as usize, offset); - - let (variables, offset) = Self::deserialize_slice(bytes, header.variables as usize, offset); - let (tag_names, offset) = - Self::deserialize_tag_names(bytes, header.tag_names as usize, offset); - let (closure_names, offset) = - Self::deserialize_slice(bytes, header.closure_names as usize, offset); - let (field_names, offset) = - Self::deserialize_field_names(bytes, header.field_names as usize, offset); - let (record_fields, offset) = - Self::deserialize_slice(bytes, header.record_fields as usize, offset); - let (variable_slices, offset) = - Self::deserialize_slice(bytes, header.variable_slices as usize, offset); - let (unspecialized_lambda_sets, offset) = - Self::deserialize_slice(bytes, header.unspecialized_lambda_sets as usize, offset); - let (exposed_vars_by_symbol, _) = - Self::deserialize_slice(bytes, header.exposed_vars_by_symbol as usize, offset); - - ( - Self { - utable, - variables: variables.to_vec(), - tag_names: tag_names.to_vec(), - closure_names: closure_names.to_vec(), - field_names, - record_fields: record_fields.to_vec(), - variable_slices: variable_slices.to_vec(), - unspecialized_lambda_sets: unspecialized_lambda_sets.to_vec(), - tag_name_cache: Default::default(), - problems: Default::default(), - uls_of_var: Default::default(), - }, - exposed_vars_by_symbol, - ) - } - - fn deserialize_field_names( - bytes: &[u8], - length: usize, - offset: usize, - ) -> (Vec, usize) { - let (slices, mut offset) = Self::deserialize_slice::>(bytes, length, offset); - - let string_slice = &bytes[offset..]; - - let mut lowercases = Vec::with_capacity(length); - for subs_slice in slices { - let bytes = &string_slice[subs_slice.indices()]; - offset += bytes.len(); - let string = unsafe { std::str::from_utf8_unchecked(bytes) }; - - lowercases.push(string.into()); - } - - (lowercases, offset) - } - - fn deserialize_tag_names(bytes: &[u8], length: usize, offset: usize) -> (Vec, usize) { - let (slices, mut offset) = - Self::deserialize_slice::(bytes, length, offset); - - let string_slice = &bytes[offset..]; - - let mut tag_names = Vec::with_capacity(length); - for SerializedTagName(subs_slice) in slices { - let bytes = &string_slice[subs_slice.indices()]; - offset += bytes.len(); - let string = unsafe { std::str::from_utf8_unchecked(bytes) }; - - let tag_name = TagName(string.into()); - - tag_names.push(tag_name); - } - - (tag_names, offset) - } - - pub(crate) fn deserialize_slice( - bytes: &[u8], - length: usize, - mut offset: usize, - ) -> (&[T], usize) { - let alignment = std::mem::align_of::(); - let size = std::mem::size_of::(); - - offset = round_to_multiple_of(offset, alignment); - - let byte_length = length * size; - let byte_slice = &bytes[offset..][..byte_length]; - - let slice = unsafe { std::slice::from_raw_parts(byte_slice.as_ptr() as *const T, length) }; - - (slice, offset + byte_length) - } -} - -/// Mapping of variables to [Content::LambdaSet]s containing unspecialized lambda sets depending on -/// that variable. -#[derive(Clone, Default, Debug)] -pub struct UlsOfVar(VecMap>); - -impl UlsOfVar { - pub fn add(&mut self, var: Variable, dependent_lambda_set: Variable) -> bool { - // NOTE: this adds the var directly without following unification links. - // [Subs::remove_dependent_unspecialized_lambda_sets] follows unifications when removing. - let set = self.0.get_or_insert(var, Default::default); - set.insert(dependent_lambda_set) - } - - pub fn extend( - &mut self, - var: Variable, - dependent_lambda_sets: impl IntoIterator, - ) { - // NOTE: this adds the var directly without following unification links. - // [Subs::remove_dependent_unspecialized_lambda_sets] follows unifications when removing. - let set = self.0.get_or_insert(var, Default::default); - set.extend(dependent_lambda_sets); - } - - pub fn union(&mut self, other: Self) { - for (key, lset) in other.drain() { - self.extend(key, lset); - } - } - - /// NOTE: this does not follow unification links. - pub fn drain(self) -> impl Iterator)> { - self.0 - .into_iter() - .map(|(v, set): (Variable, VecSet)| (v, set.into_iter())) - } - - pub fn len(&self) -> usize { - self.0.len() - } - - pub fn is_empty(&self) -> bool { - self.0.is_empty() - } -} - -#[derive(Clone)] -pub struct Subs { - utable: UnificationTable, - pub variables: Vec, - pub tag_names: Vec, - pub closure_names: Vec, - pub field_names: Vec, - pub record_fields: Vec>, - pub variable_slices: Vec, - pub unspecialized_lambda_sets: Vec, - pub tag_name_cache: TagNameCache, - pub problems: Vec, - pub uls_of_var: UlsOfVar, -} - -#[derive(Debug, Clone, Default)] -pub struct TagNameCache { - tag_names: Vec, - tag_names_slices: Vec>, -} - -impl TagNameCache { - pub fn get_mut(&mut self, tag_name: &TagName) -> Option<&mut SubsSlice> { - match self.tag_names.iter().position(|u| u == tag_name) { - Some(index) => Some(&mut self.tag_names_slices[index]), - None => None, - } - } - - pub fn push(&mut self, tag_name: &TagName, slice: SubsSlice) { - self.tag_names.push(tag_name.clone()); - self.tag_names_slices.push(slice); - } -} - -impl Default for Subs { - fn default() -> Self { - Subs::new() - } -} - -/// A slice into the Vec of subs -/// -/// The starting position is a u32 which should be plenty -/// We limit slices to u16::MAX = 65535 elements -pub struct SubsSlice { - pub start: u32, - pub length: u16, - _marker: std::marker::PhantomData, -} - -/// An index into the Vec of subs -pub struct SubsIndex { - pub index: u32, - _marker: std::marker::PhantomData, -} - -// make `subs[some_index]` work. The types/trait resolution make sure we get the -// element from the right vector - -impl std::ops::Index> for Subs { - type Output = Variable; - - fn index(&self, index: SubsIndex) -> &Self::Output { - &self.variables[index.index as usize] - } -} - -impl std::ops::IndexMut> for Subs { - fn index_mut(&mut self, index: SubsIndex) -> &mut Self::Output { - &mut self.variables[index.index as usize] - } -} - -impl std::ops::Index> for Subs { - type Output = Lowercase; - - fn index(&self, index: SubsIndex) -> &Self::Output { - &self.field_names[index.index as usize] - } -} - -impl std::ops::Index> for Subs { - type Output = TagName; - - fn index(&self, index: SubsIndex) -> &Self::Output { - &self.tag_names[index.index as usize] - } -} - -impl std::ops::IndexMut> for Subs { - fn index_mut(&mut self, index: SubsIndex) -> &mut Self::Output { - &mut self.tag_names[index.index as usize] - } -} - -impl std::ops::Index> for Subs { - type Output = Symbol; - - fn index(&self, index: SubsIndex) -> &Self::Output { - &self.closure_names[index.index as usize] - } -} - -impl std::ops::IndexMut> for Subs { - fn index_mut(&mut self, index: SubsIndex) -> &mut Self::Output { - &mut self.closure_names[index.index as usize] - } -} - -impl std::ops::Index> for Subs { - type Output = Uls; - - fn index(&self, index: SubsIndex) -> &Self::Output { - &self.unspecialized_lambda_sets[index.index as usize] - } -} - -impl std::ops::IndexMut> for Subs { - fn index_mut(&mut self, index: SubsIndex) -> &mut Self::Output { - &mut self.unspecialized_lambda_sets[index.index as usize] - } -} - -impl std::ops::IndexMut> for Subs { - fn index_mut(&mut self, index: SubsIndex) -> &mut Self::Output { - &mut self.field_names[index.index as usize] - } -} - -impl std::ops::Index>> for Subs { - type Output = RecordField<()>; - - fn index(&self, index: SubsIndex>) -> &Self::Output { - &self.record_fields[index.index as usize] - } -} - -impl std::ops::IndexMut>> for Subs { - fn index_mut(&mut self, index: SubsIndex>) -> &mut Self::Output { - &mut self.record_fields[index.index as usize] - } -} - -impl std::ops::Index> for Subs { - type Output = VariableSubsSlice; - - fn index(&self, index: SubsIndex) -> &Self::Output { - &self.variable_slices[index.index as usize] - } -} - -impl std::ops::IndexMut> for Subs { - fn index_mut(&mut self, index: SubsIndex) -> &mut Self::Output { - &mut self.variable_slices[index.index as usize] - } -} - -// custom debug - -impl std::fmt::Debug for SubsIndex { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "SubsIndex<{}>({})", - std::any::type_name::(), - self.index - ) - } -} - -impl std::fmt::Debug for SubsSlice { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "SubsSlice {{ start: {}, length: {} }}", - self.start, self.length - ) - } -} - -// derive of copy and clone does not play well with PhantomData - -impl Copy for SubsIndex {} - -impl Clone for SubsIndex { - fn clone(&self) -> Self { - Self { - index: self.index, - _marker: self._marker, - } - } -} - -impl Copy for SubsSlice {} - -impl Clone for SubsSlice { - fn clone(&self) -> Self { - Self { - start: self.start, - length: self.length, - _marker: self._marker, - } - } -} - -impl Default for SubsSlice { - fn default() -> Self { - Self { - start: Default::default(), - length: Default::default(), - _marker: Default::default(), - } - } -} - -impl SubsSlice { - pub fn get_slice<'a>(&self, slice: &'a [T]) -> &'a [T] { - &slice[self.indices()] - } - - pub fn get_slice_mut<'a>(&self, slice: &'a mut [T]) -> &'a mut [T] { - &mut slice[self.indices()] - } - - #[inline(always)] - pub fn indices(&self) -> std::ops::Range { - self.start as usize..(self.start as usize + self.length as usize) - } - - pub const fn len(&self) -> usize { - self.length as usize - } - - pub const fn is_empty(&self) -> bool { - self.len() == 0 - } - - pub const fn new(start: u32, length: u16) -> Self { - Self { - start, - length, - _marker: std::marker::PhantomData, - } - } - - pub fn extend_new(vec: &mut Vec, it: impl IntoIterator) -> Self { - let start = vec.len(); - - vec.extend(it); - - let end = vec.len(); - - Self::new(start as u32, (end - start) as u16) - } -} - -impl SubsSlice { - pub fn reserve_variable_slices(subs: &mut Subs, length: usize) -> Self { - let start = subs.variable_slices.len() as u32; - - subs.variable_slices.reserve(length); - - let value = VariableSubsSlice::default(); - for _ in 0..length { - subs.variable_slices.push(value); - } - - Self::new(start, length as u16) - } -} - -impl SubsSlice { - pub fn reserve_tag_names(subs: &mut Subs, length: usize) -> Self { - let start = subs.tag_names.len() as u32; - - subs.tag_names - .extend(std::iter::repeat(TagName(Uppercase::default())).take(length)); - - Self::new(start, length as u16) - } -} - -impl SubsSlice { - pub fn reserve_uls_slice(subs: &mut Subs, length: usize) -> Self { - let start = subs.unspecialized_lambda_sets.len() as u32; - - subs.unspecialized_lambda_sets - .extend(std::iter::repeat(Uls(Variable::NULL, Symbol::UNDERSCORE, 0)).take(length)); - - Self::new(start, length as u16) - } -} - -impl SubsIndex { - pub const fn new(start: u32) -> Self { - Self { - index: start, - _marker: std::marker::PhantomData, - } - } - - pub fn push_new(vector: &mut Vec, value: T) -> Self { - let index = Self::new(vector.len() as _); - - vector.push(value); - - index - } - - pub const fn as_slice(self) -> SubsSlice { - SubsSlice::new(self.index, 1) - } -} - -impl IntoIterator for SubsSlice { - type Item = SubsIndex; - - #[allow(clippy::type_complexity)] - type IntoIter = Map, fn(u32) -> Self::Item>; - - fn into_iter(self) -> Self::IntoIter { - (self.start..(self.start + self.length as u32)).map(u32_to_index) - } -} - -fn u32_to_index(i: u32) -> SubsIndex { - SubsIndex { - index: i, - _marker: std::marker::PhantomData, - } -} - -pub trait GetSubsSlice { - fn get_subs_slice(&self, subs_slice: SubsSlice) -> &[T]; -} - -impl GetSubsSlice for Subs { - fn get_subs_slice(&self, subs_slice: SubsSlice) -> &[Variable] { - subs_slice.get_slice(&self.variables) - } -} - -impl GetSubsSlice> for Subs { - fn get_subs_slice(&self, subs_slice: SubsSlice>) -> &[RecordField<()>] { - subs_slice.get_slice(&self.record_fields) - } -} - -impl GetSubsSlice for Subs { - fn get_subs_slice(&self, subs_slice: SubsSlice) -> &[Lowercase] { - subs_slice.get_slice(&self.field_names) - } -} - -impl GetSubsSlice for Subs { - fn get_subs_slice(&self, subs_slice: SubsSlice) -> &[TagName] { - subs_slice.get_slice(&self.tag_names) - } -} - -impl GetSubsSlice for Subs { - fn get_subs_slice(&self, subs_slice: SubsSlice) -> &[Symbol] { - subs_slice.get_slice(&self.closure_names) - } -} - -impl GetSubsSlice for Subs { - fn get_subs_slice(&self, subs_slice: SubsSlice) -> &[Uls] { - subs_slice.get_slice(&self.unspecialized_lambda_sets) - } -} - -impl fmt::Debug for Subs { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - writeln!(f)?; - for i in 0..self.len() { - let var = Variable(i as u32); - let desc = self.get_without_compacting(var); - - let root = self.get_root_key_without_compacting(var); - - if var == root { - write!(f, "{} => ", i)?; - - subs_fmt_desc(&desc, self, f)?; - } else { - write!(f, "{} => <{:?}>", i, root)?; - } - - writeln!(f)?; - } - - Ok(()) - } -} - -fn subs_fmt_desc(this: &Descriptor, subs: &Subs, f: &mut fmt::Formatter) -> fmt::Result { - subs_fmt_content(&this.content, subs, f)?; - - write!(f, " r: {:?}", &this.rank)?; - write!(f, " m: {:?}", &this.mark)?; - write!(f, " c: {:?}", &this.copy) -} - -pub struct SubsFmtContent<'a>(pub &'a Content, pub &'a Subs); - -impl<'a> fmt::Debug for SubsFmtContent<'a> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - subs_fmt_content(self.0, self.1, f) - } -} - -fn subs_fmt_content(this: &Content, subs: &Subs, f: &mut fmt::Formatter) -> fmt::Result { - match this { - Content::FlexVar(name) => write!(f, "Flex({:?})", name), - Content::FlexAbleVar(name, symbol) => write!(f, "FlexAble({:?}, {:?})", name, symbol), - Content::RigidVar(name) => write!(f, "Rigid({:?})", name), - Content::RigidAbleVar(name, symbol) => write!(f, "RigidAble({:?}, {:?})", name, symbol), - Content::RecursionVar { - structure, - opt_name, - } => write!(f, "Recursion({:?}, {:?})", structure, opt_name), - Content::Structure(flat_type) => subs_fmt_flat_type(flat_type, subs, f), - Content::Alias(name, arguments, actual, kind) => { - let slice = subs.get_subs_slice(arguments.all_variables()); - let wrap = match kind { - AliasKind::Structural => "Alias", - AliasKind::Opaque => "Opaque", - }; - - write!( - f, - "{}({:?}, {:?}, <{:?}>{:?})", - wrap, - name, - slice, - actual, - SubsFmtContent(subs.get_content_without_compacting(*actual), subs) - ) - } - Content::LambdaSet(LambdaSet { - solved, - recursion_var, - unspecialized, - }) => { - write!(f, "LambdaSet([")?; - - for (name, slice) in solved.iter_from_subs(subs) { - write!(f, "{:?} ", name)?; - for var in slice { - write!( - f, - "<{:?}>{:?} ", - var, - SubsFmtContent(subs.get_content_without_compacting(*var), subs) - )?; - } - write!(f, ", ")?; - } - - write!(f, "]")?; - if let Some(rec_var) = recursion_var.into_variable() { - write!(f, " as <{:?}>", rec_var)?; - } - for uls in subs.get_subs_slice(*unspecialized) { - write!(f, " + {:?}", uls)?; - } - write!(f, ")") - } - Content::RangedNumber(typ, range) => { - write!(f, "RangedNumber({:?}, {:?})", typ, range) - } - Content::Error => write!(f, "Error"), - } -} - -pub struct SubsFmtFlatType<'a>(pub &'a FlatType, pub &'a Subs); - -impl<'a> fmt::Debug for SubsFmtFlatType<'a> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - subs_fmt_flat_type(self.0, self.1, f) - } -} - -fn subs_fmt_flat_type(this: &FlatType, subs: &Subs, f: &mut fmt::Formatter) -> fmt::Result { - match this { - FlatType::Apply(name, arguments) => { - let slice = subs.get_subs_slice(*arguments); - - write!(f, "Apply({:?}, {:?})", name, slice) - } - FlatType::Func(arguments, lambda_set, result) => { - let slice = subs.get_subs_slice(*arguments); - write!(f, "Func([")?; - for var in slice { - let content = subs.get_content_without_compacting(*var); - write!(f, "<{:?}>{:?},", *var, SubsFmtContent(content, subs))?; - } - let result_content = subs.get_content_without_compacting(*result); - let lambda_content = subs.get_content_without_compacting(*lambda_set); - write!( - f, - "], <{:?}={:?}>{:?}, <{:?}>{:?})", - lambda_set, - subs.get_root_key_without_compacting(*lambda_set), - SubsFmtContent(lambda_content, subs), - *result, - SubsFmtContent(result_content, subs) - ) - } - FlatType::Record(fields, ext) => { - write!(f, "{{ ")?; - - let (it, new_ext) = fields.sorted_iterator_and_ext(subs, *ext); - for (name, content) in it { - let separator = match content { - RecordField::Optional(_) => '?', - RecordField::Required(_) => ':', - RecordField::Demanded(_) => ':', - }; - write!(f, "{:?} {} {:?}, ", name, separator, content)?; - } - - write!(f, "}}<{:?}>", new_ext) - } - FlatType::TagUnion(tags, ext) => { - write!(f, "[")?; - - let (it, new_ext) = tags.sorted_iterator_and_ext(subs, *ext); - for (name, slice) in it { - write!(f, "{:?} ", name)?; - for var in slice { - write!( - f, - "<{:?}>{:?} ", - var, - SubsFmtContent(subs.get_content_without_compacting(*var), subs) - )?; - } - write!(f, ", ")?; - } - - write!(f, "]<{:?}>", new_ext) - } - FlatType::FunctionOrTagUnion(tagname_index, symbol, ext) => { - let tagname: &TagName = &subs[*tagname_index]; - - write!( - f, - "FunctionOrTagUnion({:?}, {:?}, {:?})", - tagname, symbol, ext - ) - } - FlatType::RecursiveTagUnion(rec, tags, ext) => { - write!(f, "[")?; - - let (it, new_ext) = tags.sorted_iterator_and_ext(subs, *ext); - for (name, slice) in it { - write!(f, "{:?} {:?}, ", name, slice)?; - } - - write!(f, "]<{:?}> as <{:?}>", new_ext, rec) - } - FlatType::Erroneous(e) => write!(f, "Erroneous({:?})", e), - FlatType::EmptyRecord => write!(f, "EmptyRecord"), - FlatType::EmptyTagUnion => write!(f, "EmptyTagUnion"), - } -} - -#[cfg(debug_assertions)] -pub struct DebugUtable<'a>(pub &'a Subs); - -#[cfg(debug_assertions)] -impl std::fmt::Debug for DebugUtable<'_> { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str("UnificationTable {\n")?; - for v in 0..self.0.utable.len() { - f.write_fmt(format_args!(" {} => ", v))?; - let var = unsafe { Variable::from_index(v as u32) }; - let root = self.0.utable.root_key_without_compacting(var); - if root == var { - let desc = self.0.utable.get_descriptor(root); - let fmt_content = crate::subs::SubsFmtContent(&desc.content, self.0); - f.write_fmt(format_args!("{:?} at {}\n", fmt_content, desc.rank))?; - } else { - f.write_fmt(format_args!("{}\n", root.index()))?; - } - } - f.write_str("}") - } -} - -#[derive(Debug)] -pub struct VarStore { - next: u32, -} - -impl Default for VarStore { - fn default() -> Self { - VarStore::new(Variable::FIRST_USER_SPACE_VAR) - } -} - -impl VarStore { - #[inline(always)] - pub fn new(next_var: Variable) -> Self { - debug_assert!(next_var.0 >= Variable::FIRST_USER_SPACE_VAR.0); - - VarStore { next: next_var.0 } - } - - pub fn new_from_subs(subs: &Subs) -> Self { - let next_var = (subs.utable.len()) as u32; - debug_assert!(next_var >= Variable::FIRST_USER_SPACE_VAR.0); - - VarStore { next: next_var } - } - - pub fn peek(&mut self) -> u32 { - self.next - } - - pub fn fresh(&mut self) -> Variable { - // Increment the counter and return the value it had before it was incremented. - let answer = self.next; - - self.next += 1; - - Variable(answer) - } -} - -#[derive(Copy, Clone, PartialEq, Eq, Hash)] -pub struct OptVariable(u32); - -impl OptVariable { - pub const NONE: OptVariable = OptVariable(Variable::NULL.0); - - #[inline(always)] - pub const fn is_none(self) -> bool { - self.0 == Self::NONE.0 - } - - #[inline(always)] - pub const fn is_some(self) -> bool { - self.0 != Self::NONE.0 - } - - #[inline(always)] - pub const fn into_variable(self) -> Option { - if self.is_none() { - None - } else { - Some(Variable(self.0)) - } - } - - pub fn unwrap_or_else(self, or_else: F) -> Variable - where - F: FnOnce() -> Variable, - { - if self.is_none() { - or_else() - } else { - Variable(self.0) - } - } - - pub fn map(self, f: F) -> OptVariable - where - F: FnOnce(Variable) -> Variable, - { - self.into_variable() - .map(f) - .map(OptVariable::from) - .unwrap_or(OptVariable::NONE) - } -} - -impl fmt::Debug for OptVariable { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - (*self).into_variable().fmt(f) - } -} - -impl From for Option { - fn from(opt_var: OptVariable) -> Self { - opt_var.into_variable() - } -} - -/// Marks whether a when expression is exhaustive using a variable. -#[derive(Clone, Copy, Debug)] -pub struct ExhaustiveMark(Variable); - -impl ExhaustiveMark { - pub fn new(var_store: &mut VarStore) -> Self { - Self(var_store.fresh()) - } - - // NOTE: only ever use this if you *know* a pattern match is surely exhaustive! - // Otherwise you will get unpleasant unification errors. - pub fn known_exhaustive() -> Self { - Self(Variable::EMPTY_TAG_UNION) - } - - pub fn variable_for_introduction(&self) -> Variable { - debug_assert!( - self.0 != Variable::EMPTY_TAG_UNION, - "Attempting to introduce known mark" - ); - self.0 - } - - pub fn set_non_exhaustive(&self, subs: &mut Subs) { - subs.set_content(self.0, Content::Error); - } - - pub fn is_non_exhaustive(&self, subs: &Subs) -> bool { - matches!(subs.get_content_without_compacting(self.0), Content::Error) - } -} - -/// Marks whether a when branch is redundant using a variable. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct RedundantMark(Variable); - -impl RedundantMark { - pub fn new(var_store: &mut VarStore) -> Self { - Self(var_store.fresh()) - } - - // NOTE: only ever use this if you *know* a pattern match is surely exhaustive! - // Otherwise you will get unpleasant unification errors. - pub fn known_non_redundant() -> Self { - Self(Variable::EMPTY_TAG_UNION) - } - - pub fn variable_for_introduction(&self) -> Variable { - debug_assert!( - self.0 != Variable::EMPTY_TAG_UNION, - "Attempting to introduce known mark" - ); - self.0 - } - - pub fn set_redundant(&self, subs: &mut Subs) { - subs.set_content(self.0, Content::Error); - } - - pub fn is_redundant(&self, subs: &Subs) -> bool { - matches!(subs.get_content_without_compacting(self.0), Content::Error) - } -} - -pub fn new_marks(var_store: &mut VarStore) -> (RedundantMark, ExhaustiveMark) { - ( - RedundantMark::new(var_store), - ExhaustiveMark::new(var_store), - ) -} - -/// Marks whether a recursive let-cycle was determined to be illegal during solving. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct IllegalCycleMark(OptVariable); - -impl IllegalCycleMark { - pub fn new(var_store: &mut VarStore) -> Self { - Self(OptVariable(var_store.fresh().index())) - } - - /// used for recursive blocks with just one function; invalid recursion in such blocks is - /// always a type error, so we don't need to generate a custom error message in such cases - pub const fn empty() -> Self { - Self(OptVariable::NONE) - } - - pub fn set_illegal(&self, subs: &mut Subs) { - if let Some(var) = self.0.into_variable() { - subs.set_content(var, Content::Error); - } - } - - pub fn is_illegal(&self, subs: &Subs) -> bool { - if let Some(var) = self.0.into_variable() { - matches!(subs.get_content_without_compacting(var), Content::Error) - } else { - false - } - } -} - -#[derive(Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] -pub struct Variable(u32); - -macro_rules! define_const_var { - ($($(:pub)? $name:ident),* $(,)?) => { - #[allow(non_camel_case_types, clippy::upper_case_acronyms)] - enum ConstVariables { - $( $name, )* - FINAL_CONST_VAR - } - - impl Variable { - $( pub const $name: Variable = Variable(ConstVariables::$name as u32); )* - - pub const NUM_RESERVED_VARS: usize = ConstVariables::FINAL_CONST_VAR as usize; - } - - }; -} - -define_const_var! { - // Reserved for indicating the absence of a variable. - // This lets us avoid using Option for the Descriptor's - // copy field, which is a relevant space savings because we make - // a *ton* of Descriptors. - // - // Also relevant: because this has the value 0, Descriptors can 0-initialize - // to it in bulk - which is relevant, because Descriptors get initialized in bulk. - NULL, - - :pub EMPTY_RECORD, - :pub EMPTY_TAG_UNION, - - BOOL_ENUM, - :pub BOOL, - - ORDER_ENUM, - :pub ORDER, - - // Signed8 := [] - :pub SIGNED8, - :pub SIGNED16, - :pub SIGNED32, - :pub SIGNED64, - :pub SIGNED128, - - :pub UNSIGNED8, - :pub UNSIGNED16, - :pub UNSIGNED32, - :pub UNSIGNED64, - :pub UNSIGNED128, - - :pub NATURAL, - - // Integer Signed8 := Signed8 - INTEGER_SIGNED8, - INTEGER_SIGNED16, - INTEGER_SIGNED32, - INTEGER_SIGNED64, - INTEGER_SIGNED128, - - INTEGER_UNSIGNED8, - INTEGER_UNSIGNED16, - INTEGER_UNSIGNED32, - INTEGER_UNSIGNED64, - INTEGER_UNSIGNED128, - - INTEGER_NATURAL, - - // Num (Integer Signed8) := Integer Signed8 - NUM_INTEGER_SIGNED8, - NUM_INTEGER_SIGNED16, - NUM_INTEGER_SIGNED32, - NUM_INTEGER_SIGNED64, - NUM_INTEGER_SIGNED128, - - NUM_INTEGER_UNSIGNED8, - NUM_INTEGER_UNSIGNED16, - NUM_INTEGER_UNSIGNED32, - NUM_INTEGER_UNSIGNED64, - NUM_INTEGER_UNSIGNED128, - - NUM_INTEGER_NATURAL, - - // I8 : Num (Integer Signed8) - :pub I8, - :pub I16, - :pub I32, - :pub I64, - :pub I128, - - :pub U8, - :pub U16, - :pub U32, - :pub U64, - :pub U128, - - :pub NAT, - - // Binary32 : [] - BINARY32, - BINARY64, - DECIMAL, - - // Float Binary32 := Binary32 - FLOAT_BINARY32, - FLOAT_BINARY64, - FLOAT_DECIMAL, - - // Num (Float Binary32) := Float Binary32 - NUM_FLOAT_BINARY32, - NUM_FLOAT_BINARY64, - NUM_FLOAT_DECIMAL, - - :pub F32, - :pub F64, - - :pub DEC, -} - -impl Variable { - const FIRST_USER_SPACE_VAR: Variable = Variable(Self::NUM_RESERVED_VARS as u32); - - /// # Safety - /// - /// It is not guaranteed that the variable is in bounds. - pub unsafe fn from_index(v: u32) -> Self { - Variable(v) - } - - #[inline(always)] - pub const fn index(&self) -> u32 { - self.0 - } - - pub const fn get_reserved(symbol: Symbol) -> Option { - // Must be careful here: the variables must in fact be in Subs - match symbol { - Symbol::NUM_I128 => Some(Variable::I128), - Symbol::NUM_I64 => Some(Variable::I64), - Symbol::NUM_I32 => Some(Variable::I32), - Symbol::NUM_I16 => Some(Variable::I16), - Symbol::NUM_I8 => Some(Variable::I8), - - Symbol::NUM_U128 => Some(Variable::U128), - Symbol::NUM_U64 => Some(Variable::U64), - Symbol::NUM_U32 => Some(Variable::U32), - Symbol::NUM_U16 => Some(Variable::U16), - Symbol::NUM_U8 => Some(Variable::U8), - - Symbol::NUM_NAT => Some(Variable::NAT), - - Symbol::BOOL_BOOL => Some(Variable::BOOL), - - Symbol::NUM_F64 => Some(Variable::F64), - Symbol::NUM_F32 => Some(Variable::F32), - - Symbol::NUM_DEC => Some(Variable::DEC), - - _ => None, - } - } -} - -impl From for OptVariable { - fn from(var: Variable) -> Self { - OptVariable(var.0) - } -} - -impl fmt::Debug for Variable { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - self.0.fmt(f) - } -} - -/// Used in SolvedType -#[derive(Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] -pub struct VarId(u32); - -impl VarId { - pub fn from_var(var: Variable, subs: &Subs) -> Self { - let var = subs.get_root_key_without_compacting(var); - let Variable(n) = var; - - VarId(n) - } - - pub const fn from_u32(n: u32) -> Self { - VarId(n) - } -} - -impl fmt::Debug for VarId { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - self.0.fmt(f) - } -} - -#[allow(clippy::too_many_arguments)] -fn integer_type( - subs: &mut Subs, - - num_signed64: Symbol, - num_i64: Symbol, - - signed64: Variable, - - integer_signed64: Variable, - - num_integer_signed64: Variable, - - var_i64: Variable, -) { - // define the type Signed64 := [] - { - subs.set_content(signed64, { - Content::Alias( - num_signed64, - AliasVariables::default(), - Variable::EMPTY_TAG_UNION, - AliasKind::Opaque, - ) - }); - } - - // define the type `Num.Integer Num.Signed64 := Num.Signed64` - { - let vars = AliasVariables::insert_into_subs(subs, [signed64], []); - subs.set_content(integer_signed64, { - Content::Alias(Symbol::NUM_INTEGER, vars, signed64, AliasKind::Opaque) - }); - } - - // define the type `Num.Num (Num.Integer Num.Signed64) := Num.Integer Num.Signed64` - { - let vars = AliasVariables::insert_into_subs(subs, [integer_signed64], []); - subs.set_content(num_integer_signed64, { - Content::Alias(Symbol::NUM_NUM, vars, integer_signed64, AliasKind::Opaque) - }); - } - - // define the type `Num.I64 : Num.Num (Num.Integer Num.Signed64)` - { - subs.set_content(var_i64, { - Content::Alias( - num_i64, - AliasVariables::default(), - num_integer_signed64, - AliasKind::Structural, - ) - }); - } -} - -fn define_integer_types(subs: &mut Subs) { - integer_type( - subs, - Symbol::NUM_SIGNED128, - Symbol::NUM_I128, - Variable::SIGNED128, - Variable::INTEGER_SIGNED128, - Variable::NUM_INTEGER_SIGNED128, - Variable::I128, - ); - - integer_type( - subs, - Symbol::NUM_SIGNED64, - Symbol::NUM_I64, - Variable::SIGNED64, - Variable::INTEGER_SIGNED64, - Variable::NUM_INTEGER_SIGNED64, - Variable::I64, - ); - - integer_type( - subs, - Symbol::NUM_SIGNED32, - Symbol::NUM_I32, - Variable::SIGNED32, - Variable::INTEGER_SIGNED32, - Variable::NUM_INTEGER_SIGNED32, - Variable::I32, - ); - - integer_type( - subs, - Symbol::NUM_SIGNED16, - Symbol::NUM_I16, - Variable::SIGNED16, - Variable::INTEGER_SIGNED16, - Variable::NUM_INTEGER_SIGNED16, - Variable::I16, - ); - - integer_type( - subs, - Symbol::NUM_SIGNED8, - Symbol::NUM_I8, - Variable::SIGNED8, - Variable::INTEGER_SIGNED8, - Variable::NUM_INTEGER_SIGNED8, - Variable::I8, - ); - - integer_type( - subs, - Symbol::NUM_UNSIGNED128, - Symbol::NUM_U128, - Variable::UNSIGNED128, - Variable::INTEGER_UNSIGNED128, - Variable::NUM_INTEGER_UNSIGNED128, - Variable::U128, - ); - - integer_type( - subs, - Symbol::NUM_UNSIGNED64, - Symbol::NUM_U64, - Variable::UNSIGNED64, - Variable::INTEGER_UNSIGNED64, - Variable::NUM_INTEGER_UNSIGNED64, - Variable::U64, - ); - - integer_type( - subs, - Symbol::NUM_UNSIGNED32, - Symbol::NUM_U32, - Variable::UNSIGNED32, - Variable::INTEGER_UNSIGNED32, - Variable::NUM_INTEGER_UNSIGNED32, - Variable::U32, - ); - - integer_type( - subs, - Symbol::NUM_UNSIGNED16, - Symbol::NUM_U16, - Variable::UNSIGNED16, - Variable::INTEGER_UNSIGNED16, - Variable::NUM_INTEGER_UNSIGNED16, - Variable::U16, - ); - - integer_type( - subs, - Symbol::NUM_UNSIGNED8, - Symbol::NUM_U8, - Variable::UNSIGNED8, - Variable::INTEGER_UNSIGNED8, - Variable::NUM_INTEGER_UNSIGNED8, - Variable::U8, - ); - - integer_type( - subs, - Symbol::NUM_NATURAL, - Symbol::NUM_NAT, - Variable::NATURAL, - Variable::INTEGER_NATURAL, - Variable::NUM_INTEGER_NATURAL, - Variable::NAT, - ); -} - -#[allow(clippy::too_many_arguments)] -fn float_type( - subs: &mut Subs, - - num_binary64: Symbol, - num_f64: Symbol, - - binary64: Variable, - - float_binary64: Variable, - - num_float_binary64: Variable, - - var_f64: Variable, -) { - // define the type Binary64 := [] - { - subs.set_content(binary64, { - Content::Alias( - num_binary64, - AliasVariables::default(), - Variable::EMPTY_TAG_UNION, - AliasKind::Structural, - ) - }); - } - - // define the type `Num.Float Num.Binary64 := Num.Binary64` - { - let vars = AliasVariables::insert_into_subs(subs, [binary64], []); - subs.set_content(float_binary64, { - Content::Alias(Symbol::NUM_FLOATINGPOINT, vars, binary64, AliasKind::Opaque) - }); - } - - // define the type `Num.Num (Num.Float Num.Binary64) := Num.Float Num.Binary64` - { - let vars = AliasVariables::insert_into_subs(subs, [float_binary64], []); - subs.set_content(num_float_binary64, { - Content::Alias(Symbol::NUM_NUM, vars, float_binary64, AliasKind::Opaque) - }); - } - - // define the type `F64: Num.Num (Num.Float Num.Binary64)` - { - subs.set_content(var_f64, { - Content::Alias( - num_f64, - AliasVariables::default(), - num_float_binary64, - AliasKind::Structural, - ) - }); - } -} - -fn define_float_types(subs: &mut Subs) { - float_type( - subs, - Symbol::NUM_BINARY32, - Symbol::NUM_F32, - Variable::BINARY32, - Variable::FLOAT_BINARY32, - Variable::NUM_FLOAT_BINARY32, - Variable::F32, - ); - - float_type( - subs, - Symbol::NUM_BINARY64, - Symbol::NUM_F64, - Variable::BINARY64, - Variable::FLOAT_BINARY64, - Variable::NUM_FLOAT_BINARY64, - Variable::F64, - ); - - float_type( - subs, - Symbol::NUM_DECIMAL, - Symbol::NUM_DEC, - Variable::DECIMAL, - Variable::FLOAT_DECIMAL, - Variable::NUM_FLOAT_DECIMAL, - Variable::DEC, - ); -} - -impl Subs { - pub const RESULT_TAG_NAMES: SubsSlice = SubsSlice::new(0, 2); - pub const TAG_NAME_ERR: SubsIndex = SubsIndex::new(0); - pub const TAG_NAME_OK: SubsIndex = SubsIndex::new(1); - pub const TAG_NAME_INVALID_NUM_STR: SubsIndex = SubsIndex::new(2); - pub const TAG_NAME_BAD_UTF_8: SubsIndex = SubsIndex::new(3); - pub const TAG_NAME_OUT_OF_BOUNDS: SubsIndex = SubsIndex::new(4); - - pub fn new() -> Self { - Self::with_capacity(0) - } - - pub fn with_capacity(capacity: usize) -> Self { - let capacity = capacity.max(Variable::NUM_RESERVED_VARS); - - let mut tag_names = Vec::with_capacity(32); - - tag_names.push(TagName("Err".into())); - tag_names.push(TagName("Ok".into())); - - tag_names.push(TagName("InvalidNumStr".into())); - tag_names.push(TagName("BadUtf8".into())); - tag_names.push(TagName("OutOfBounds".into())); - - let mut subs = Subs { - utable: UnificationTable::default(), - variables: Vec::new(), - tag_names, - closure_names: Vec::new(), - field_names: Vec::new(), - record_fields: Vec::new(), - // store an empty slice at the first position - // used for "TagOrFunction" - variable_slices: vec![VariableSubsSlice::default()], - unspecialized_lambda_sets: Vec::new(), - tag_name_cache: Default::default(), - problems: Vec::new(), - uls_of_var: Default::default(), - }; - - subs.utable.reserve(capacity); - - define_integer_types(&mut subs); - define_float_types(&mut subs); - - subs.set_content( - Variable::EMPTY_RECORD, - Content::Structure(FlatType::EmptyRecord), - ); - subs.set_content( - Variable::EMPTY_TAG_UNION, - Content::Structure(FlatType::EmptyTagUnion), - ); - - let bool_union_tags = UnionTags::insert_into_subs( - &mut subs, - [(TagName("False".into()), []), (TagName("True".into()), [])], - ); - - subs.set_content(Variable::BOOL_ENUM, { - Content::Structure(FlatType::TagUnion( - bool_union_tags, - Variable::EMPTY_TAG_UNION, - )) - }); - - subs.set_content(Variable::BOOL, { - Content::Alias( - Symbol::BOOL_BOOL, - AliasVariables::default(), - Variable::BOOL_ENUM, - AliasKind::Structural, - ) - }); - - subs - } - - pub fn new_from_varstore(var_store: VarStore) -> Self { - let entries = var_store.next; - - Self::with_capacity(entries as usize) - } - - pub fn extend_by(&mut self, entries: usize) { - self.utable.reserve(entries); - } - - #[inline(always)] - pub fn fresh(&mut self, value: Descriptor) -> Variable { - // self.utable.new_key(value) - - self.utable - .push(value.content, value.rank, value.mark, value.copy) - } - - #[inline(always)] - pub fn fresh_unnamed_flex_var(&mut self) -> Variable { - self.fresh(Descriptor::from(unnamed_flex_var())) - } - - pub fn rigid_var(&mut self, var: Variable, name: Lowercase) { - let name_index = SubsIndex::push_new(&mut self.field_names, name); - let content = Content::RigidVar(name_index); - let desc = Descriptor::from(content); - - self.set(var, desc); - } - - pub fn rigid_able_var(&mut self, var: Variable, name: Lowercase, ability: Symbol) { - let name_index = SubsIndex::push_new(&mut self.field_names, name); - let content = Content::RigidAbleVar(name_index, ability); - let desc = Descriptor::from(content); - - self.set(var, desc); - } - - /// Unions two keys without the possibility of failure. - pub fn union(&mut self, left: Variable, right: Variable, desc: Descriptor) { - let l_root = self.utable.inlined_get_root_key(left); - let r_root = self.utable.inlined_get_root_key(right); - - // NOTE this swapping is intentional! most of our unifying commands are based on the elm - // source, but unify_roots is from `ena`, not the elm source. Turns out that they have - // different ideas of how the merge should go (l into r or the reverse), and this matters! - self.utable.unify_roots(r_root, l_root, desc) - } - - pub fn get(&mut self, key: Variable) -> Descriptor { - self.utable.get_descriptor(key) - } - - pub fn get_rank(&self, key: Variable) -> Rank { - self.utable.get_rank(key) - } - - pub fn get_copy(&self, key: Variable) -> OptVariable { - self.utable.get_copy(key) - } - - pub fn get_mark(&self, key: Variable) -> Mark { - self.utable.get_mark(key) - } - - pub fn get_rank_mark(&self, key: Variable) -> (Rank, Mark) { - (self.utable.get_rank(key), self.utable.get_mark(key)) - } - - pub fn get_mark_unchecked(&self, key: Variable) -> Mark { - self.utable.get_mark_unchecked(key) - } - - pub fn get_content_unchecked(&self, key: Variable) -> &Content { - self.utable.get_content_unchecked(key) - } - - pub fn get_rank_unchecked(&self, key: Variable) -> Rank { - self.utable.get_rank_unchecked(key) - } - - pub fn get_copy_unchecked(&self, key: Variable) -> OptVariable { - self.utable.get_copy_unchecked(key) - } - - #[inline(always)] - pub fn get_without_compacting(&self, key: Variable) -> Descriptor { - self.utable.get_descriptor(key) - } - - pub fn get_content_without_compacting(&self, key: Variable) -> &Content { - self.utable.get_content(key) - } - - #[inline(always)] - pub fn get_root_key(&mut self, key: Variable) -> Variable { - self.utable.inlined_get_root_key(key) - } - - #[inline(always)] - pub fn get_root_key_without_compacting(&self, key: Variable) -> Variable { - self.utable.root_key_without_compacting(key) - } - - #[inline(always)] - pub fn set(&mut self, key: Variable, r_value: Descriptor) { - let l_key = self.utable.inlined_get_root_key(key); - - // self.utable.update_value(l_key, |node| node.value = r_value); - self.utable.set_descriptor(l_key, r_value) - } - - pub fn set_rank(&mut self, key: Variable, rank: Rank) { - self.utable.set_rank(key, rank) - } - - pub fn set_mark(&mut self, key: Variable, mark: Mark) { - self.utable.set_mark(key, mark) - } - - pub fn set_rank_unchecked(&mut self, key: Variable, rank: Rank) { - self.utable.set_rank_unchecked(key, rank) - } - - pub fn set_mark_unchecked(&mut self, key: Variable, mark: Mark) { - self.utable.set_mark_unchecked(key, mark) - } - - pub fn set_copy_unchecked(&mut self, key: Variable, copy: OptVariable) { - self.utable.set_copy_unchecked(key, copy) - } - - pub fn set_copy(&mut self, key: Variable, copy: OptVariable) { - self.utable.set_copy(key, copy) - } - - pub fn set_rank_mark(&mut self, key: Variable, rank: Rank, mark: Mark) { - self.utable.set_rank(key, rank); - self.utable.set_mark(key, mark); - } - - pub fn set_content(&mut self, key: Variable, content: Content) { - self.utable.set_content(key, content); - } - - pub fn set_content_unchecked(&mut self, key: Variable, content: Content) { - self.utable.set_content_unchecked(key, content); - } - - pub fn modify(&mut self, key: Variable, mapper: F) -> T - where - F: FnOnce(&mut Descriptor) -> T, - { - self.utable.modify(key, mapper) - } - - #[inline(always)] - pub fn get_rank_set_mark(&mut self, key: Variable, mark: Mark) -> Rank { - self.utable.get_rank_set_mark(key, mark) - } - - pub fn equivalent(&mut self, left: Variable, right: Variable) -> bool { - self.utable.unioned(left, right) - } - - pub fn redundant(&self, var: Variable) -> bool { - self.utable.is_redirect(var) - } - - /// Determines if there is any variable in [var] that occurs recursively. - /// - /// The [Err] variant returns the occurring variable and the chain of variables that led - /// to a recursive occurrence, in order of proximity. For example, if the type "r" has a - /// reference chain r -> t1 -> t2 -> r, [occurs] will return `Err(r, [t2, t1, r])`. - /// - /// This ignores [Content::RecursionVar]s that occur recursively, because those are - /// already priced in and expected to occur. Use [Subs::occurs_including_recursion_vars] if you - /// need to check for recursion var occurrence. - pub fn occurs(&self, var: Variable) -> Result<(), (Variable, Vec)> { - occurs(self, &[], var, false) - } - - /// Like [Subs::occurs], but also errors when recursion vars occur. - pub fn occurs_including_recursion_vars( - &self, - var: Variable, - ) -> Result<(), (Variable, Vec)> { - occurs(self, &[], var, true) - } - - pub fn mark_tag_union_recursive( - &mut self, - recursive: Variable, - tags: UnionTags, - ext_var: Variable, - ) { - let (rec_var, new_tags) = self.mark_union_recursive_help(recursive, tags); - - let new_ext_var = self.explicit_substitute(recursive, rec_var, ext_var); - let flat_type = FlatType::RecursiveTagUnion(rec_var, new_tags, new_ext_var); - - self.set_content(recursive, Content::Structure(flat_type)); - } - - pub fn mark_lambda_set_recursive( - &mut self, - recursive: Variable, - solved_lambdas: UnionLambdas, - unspecialized_lambdas: SubsSlice, - ) { - let (rec_var, new_tags) = self.mark_union_recursive_help(recursive, solved_lambdas); - - let new_lambda_set = Content::LambdaSet(LambdaSet { - solved: new_tags, - recursion_var: OptVariable::from(rec_var), - unspecialized: unspecialized_lambdas, - }); - - self.set_content(recursive, new_lambda_set); - } - - fn mark_union_recursive_help( - &mut self, - recursive: Variable, - tags: UnionLabels, - ) -> (Variable, UnionLabels) { - let description = self.get(recursive); - - let rec_var = self.fresh_unnamed_flex_var(); - self.set_rank(rec_var, description.rank); - self.set_content( - rec_var, - Content::RecursionVar { - opt_name: None, - structure: recursive, - }, - ); - - let new_variable_slices = SubsSlice::reserve_variable_slices(self, tags.len()); - - let it = new_variable_slices.indices().zip(tags.iter_all()); - for (variable_slice_index, (_, slice_index)) in it { - let slice = self[slice_index]; - - let new_variables = VariableSubsSlice::reserve_into_subs(self, slice.len()); - for (target_index, var_index) in new_variables.indices().zip(slice) { - let var = self[var_index]; - self.variables[target_index] = self.explicit_substitute(recursive, rec_var, var); - } - self.variable_slices[variable_slice_index] = new_variables; - } - - let tag_names: SubsSlice = tags.labels(); - let new_tags = UnionLabels::from_slices(tag_names, new_variable_slices); - - (rec_var, new_tags) - } - - pub fn explicit_substitute( - &mut self, - from: Variable, - to: Variable, - in_var: Variable, - ) -> Variable { - let x = self.get_root_key(from); - let y = self.get_root_key(to); - let z = self.get_root_key(in_var); - let mut seen = ImSet::default(); - explicit_substitute(self, x, y, z, &mut seen) - } - - pub fn var_to_error_type(&mut self, var: Variable) -> (ErrorType, Vec) { - self.var_to_error_type_contextual(var, ErrorTypeContext::None) - } - - pub fn var_to_error_type_contextual( - &mut self, - var: Variable, - context: ErrorTypeContext, - ) -> (ErrorType, Vec) { - let names = get_var_names(self, var, ImMap::default()); - let mut taken = MutSet::default(); - - for (name, _) in names { - taken.insert(name); - } - - let mut state = ErrorTypeState { - taken, - letters_used: 0, - problems: Vec::new(), - context, - recursive_tag_unions_seen: Vec::new(), - }; - - (var_to_err_type(self, &mut state, var), state.problems) - } - - pub fn len(&self) -> usize { - self.utable.len() - } - - pub fn is_empty(&self) -> bool { - self.utable.is_empty() - } - - pub fn contains(&self, var: Variable) -> bool { - (var.index() as usize) < self.len() - } - - pub fn snapshot(&mut self) -> Snapshot { - self.utable.snapshot() - } - - pub fn rollback_to(&mut self, snapshot: Snapshot) { - self.utable.rollback_to(snapshot) - } - - pub fn commit_snapshot(&mut self, _snapshot: Snapshot) { - // self.utable.commit(snapshot) - } - - pub fn vars_since_snapshot(&mut self, snapshot: &Snapshot) -> core::ops::Range { - self.utable.vars_since_snapshot(snapshot) - } - - pub fn get_lambda_set(&self, lambda_set: Variable) -> LambdaSet { - match self.get_content_without_compacting(lambda_set) { - Content::LambdaSet(lambda_set) => *lambda_set, - _ => internal_error!("not a lambda set"), - } - } - - pub fn remove_dependent_unspecialized_lambda_sets( - &mut self, - var: Variable, - ) -> impl Iterator + '_ { - let utable = &self.utable; - let root_var = utable.root_key_without_compacting(var); - - self.uls_of_var - .0 - .drain_filter(move |cand_var, _| { - utable.root_key_without_compacting(*cand_var) == root_var - }) - .flat_map(|(_, lambda_set_vars)| lambda_set_vars.into_iter()) - } -} - -#[inline(always)] -const fn unnamed_flex_var() -> Content { - Content::FlexVar(None) -} - -#[derive(Copy, Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct Rank(u32); - -impl Rank { - pub const NONE: Rank = Rank(0); - - pub fn is_none(&self) -> bool { - *self == Self::NONE - } - - pub const fn toplevel() -> Self { - Rank(1) - } - - /// the rank at which we introduce imports. - /// - /// Type checking starts at rank 1 aka toplevel. When there are rigid/flex variables introduced by a - /// constraint, then these must be generalized relative to toplevel, and hence are introduced at - /// rank 2. - /// - /// We always use: even if there are no rigids imported, introducing at rank 2 is correct - /// (if slightly inefficient) because there are no rigids anyway so generalization is trivial - pub const fn import() -> Self { - Rank(2) - } - - pub fn next(self) -> Self { - Rank(self.0 + 1) - } - - pub fn into_usize(self) -> usize { - self.0 as usize - } -} - -impl fmt::Display for Rank { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - self.0.fmt(f) - } -} - -impl fmt::Debug for Rank { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - self.0.fmt(f) - } -} - -impl From for usize { - fn from(rank: Rank) -> Self { - rank.0 as usize - } -} - -impl From for Rank { - fn from(index: usize) -> Self { - Rank(index as u32) - } -} - -#[derive(Clone, Copy)] -pub struct Descriptor { - pub content: Content, - pub rank: Rank, - pub mark: Mark, - pub copy: OptVariable, -} - -impl fmt::Debug for Descriptor { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!( - f, - "{:?}, r: {:?}, m: {:?} c: {:?}", - self.content, - self.rank, - self.mark, - self.copy.into_variable() - ) - } -} - -impl Default for Descriptor { - fn default() -> Self { - unnamed_flex_var().into() - } -} - -impl From for Descriptor { - fn from(content: Content) -> Descriptor { - Descriptor { - content, - rank: Rank::NONE, - mark: Mark::NONE, - copy: OptVariable::NONE, - } - } -} - -roc_error_macros::assert_sizeof_all!(Content, 3 * 8 + 4); -roc_error_macros::assert_sizeof_all!((Symbol, AliasVariables, Variable), 2 * 8 + 4); -roc_error_macros::assert_sizeof_all!(AliasVariables, 8); -roc_error_macros::assert_sizeof_all!(FlatType, 3 * 8); - -roc_error_macros::assert_sizeof_aarch64!((Variable, Option), 4 * 8); -roc_error_macros::assert_sizeof_wasm!((Variable, Option), 4 * 4); -roc_error_macros::assert_sizeof_default!((Variable, Option), 4 * 8); - -roc_error_macros::assert_copyable!(Content); -roc_error_macros::assert_copyable!(Descriptor); - -#[derive(Clone, Copy, Debug)] -pub enum Content { - /// A type variable which the user did not name in an annotation, - /// - /// When we auto-generate a type var name, e.g. the "a" in (a -> a), we - /// change the Option in here from None to Some. - FlexVar(Option>), - /// name given in a user-written annotation - RigidVar(SubsIndex), - /// Like a [Self::FlexVar], but is also bound to an ability. - /// This can only happen when unified with a [Self::RigidAbleVar]. - FlexAbleVar(Option>, Symbol), - /// Like a [Self::RigidVar], but is also bound to an ability. - /// For example, "a has Hash". - RigidAbleVar(SubsIndex, Symbol), - /// name given to a recursion variable - RecursionVar { - structure: Variable, - opt_name: Option>, - }, - LambdaSet(LambdaSet), - Structure(FlatType), - Alias(Symbol, AliasVariables, Variable, AliasKind), - RangedNumber(Variable, crate::num::NumericRange), - Error, -} - -/// Stores the lambdas an arrow might pass through; for example -/// -/// f : {} -> {} -/// g : {} -> {} -/// if b then f else g -/// -/// has the type {} -[f, g]-> {} where [f, g] is the solved lambda set. -#[derive(Clone, Copy, Debug)] -pub struct LambdaSet { - /// The resolved lambda symbols we know. - pub solved: UnionLambdas, - /// Lambda sets may be recursive. For example, consider the annotated program - /// - /// ```text - /// XEffect : A -> B - /// - /// after : ({} -> XEffect) -> XEffect - /// after = - /// \cont -> - /// f = \A -[`f (typeof cont)]-> when cont {} is A -> B - /// f - /// - /// nestForever : {} -> XEffect - /// nestForever = \{} -[`nestForever]-> after nestForever - /// ^^^^^^^^^^^ {} -[`nestForever]-> A -[`f ({} -[`nestForever]-> A -[`f ...]-> B)]-> B - /// ``` - /// - /// where [`nestForever] and [`f ...] refer to the lambda sets of their respective arrows. `f` - /// captures `cont`. The usage of `after` in `nestForever` means that `nestForever` has type - /// ``nestForever : {} -[`nestForever]-> A -[`f (typeof cont)]-> B``. But also, `after` is called - /// with ``nestForever`, which means in this case `typeof cont = typeof nestForever``. So we see - /// that ``nestForever : {} -[`nestForever]-> A -[`f (typeof nestForever)]-> B``, and the lambda - /// set ``[`f (typeof nestForever)]`` is recursive. - /// - /// However, we don't know if a lambda set is recursive or not until type inference. - pub recursion_var: OptVariable, - /// Lambdas we won't know until an ability specialization is resolved. - pub unspecialized: SubsSlice, -} - -#[derive(Clone, Copy, Debug, Default)] -pub struct AliasVariables { - pub variables_start: u32, - pub all_variables_len: u16, - - /// an alias has type variables and lambda set variables - pub type_variables_len: u16, -} - -impl AliasVariables { - pub const fn all_variables(&self) -> VariableSubsSlice { - SubsSlice::new(self.variables_start, self.all_variables_len) - } - - pub const fn type_variables(&self) -> VariableSubsSlice { - SubsSlice::new(self.variables_start, self.type_variables_len) - } - - pub const fn lambda_set_variables(&self) -> VariableSubsSlice { - SubsSlice::new( - self.variables_start + self.type_variables_len as u32, - self.all_variables_len - self.type_variables_len, - ) - } - - pub const fn len(&self) -> usize { - self.type_variables_len as usize - } - - pub const fn is_empty(&self) -> bool { - self.type_variables_len == 0 - } - - pub fn replace_variables( - &mut self, - subs: &mut Subs, - variables: impl IntoIterator, - ) { - let variables_start = subs.variables.len() as u32; - subs.variables.extend(variables); - let variables_len = (subs.variables.len() - variables_start as usize) as u16; - - debug_assert_eq!(variables_len, self.all_variables_len); - - self.variables_start = variables_start; - } - - pub fn named_type_arguments(&self) -> impl Iterator> { - self.all_variables() - .into_iter() - .take(self.type_variables_len as usize) - } - - pub fn unnamed_type_arguments(&self) -> impl Iterator> { - self.all_variables() - .into_iter() - .skip(self.type_variables_len as usize) - } - - pub fn insert_into_subs( - subs: &mut Subs, - type_arguments: I1, - unnamed_arguments: I2, - ) -> Self - where - I1: IntoIterator, - I2: IntoIterator, - { - let variables_start = subs.variables.len() as u32; - - subs.variables.extend(type_arguments); - - let type_variables_len = (subs.variables.len() as u32 - variables_start) as u16; - - subs.variables.extend(unnamed_arguments); - - let all_variables_len = (subs.variables.len() as u32 - variables_start) as u16; - - if type_variables_len == 3 { - panic!(); - } - - Self { - variables_start, - type_variables_len, - all_variables_len, - } - } -} - -impl IntoIterator for AliasVariables { - type Item = ::Item; - - type IntoIter = ::IntoIter; - - fn into_iter(self) -> Self::IntoIter { - self.all_variables().into_iter() - } -} - -impl Content { - #[inline(always)] - pub fn is_number(&self) -> bool { - matches!( - &self, - Content::Structure(FlatType::Apply(Symbol::NUM_NUM, _)) - ) - } -} - -#[derive(Clone, Copy, Debug)] -pub enum FlatType { - Apply(Symbol, VariableSubsSlice), - Func(VariableSubsSlice, Variable, Variable), - Record(RecordFields, Variable), - TagUnion(UnionTags, Variable), - FunctionOrTagUnion(SubsIndex, Symbol, Variable), - RecursiveTagUnion(Variable, UnionTags, Variable), - Erroneous(SubsIndex), - EmptyRecord, - EmptyTagUnion, -} - -impl FlatType { - pub fn get_singleton_tag_union<'a>(&'a self, subs: &'a Subs) -> Option<&'a TagName> { - match self { - Self::TagUnion(tags, ext) => { - let tags = tags.unsorted_tags_and_ext(subs, *ext).0.tags; - if tags.len() != 1 { - return None; - } - let (tag_name, vars) = tags[0]; - if !vars.is_empty() { - return None; - } - Some(tag_name) - } - _ => None, - } - } -} - -#[derive(PartialEq, Eq, Debug, Clone, Copy)] -pub enum Builtin { - Str, - Int, - Float, - EmptyRecord, -} - -pub type VariableSubsSlice = SubsSlice; - -impl VariableSubsSlice { - /// Reserve space for `length` variables in the subs.variables array - /// - /// This is useful when we know how many variables e.g. a loop will produce, - /// but the loop itself also produces new variables. We often want to work - /// with slices, and the loop itself would break up our contiguous slice of variables - /// - /// This function often helps prevent an intermediate array. See also `indices` above - /// to conveniently get a slice or iterator over the indices - pub fn reserve_into_subs(subs: &mut Subs, length: usize) -> Self { - let start = subs.variables.len() as u32; - - subs.variables - .extend(std::iter::repeat(Variable::NULL).take(length)); - - Self::new(start, length as u16) - } - - pub fn insert_into_subs(subs: &mut Subs, input: I) -> Self - where - I: IntoIterator, - { - let start = subs.variables.len() as u32; - - subs.variables.extend(input.into_iter()); - - let length = (subs.variables.len() as u32 - start) as u16; - - Self::new(start, length) - } -} - -pub trait Label: Sized + Clone { - fn index_subs(subs: &Subs, idx: SubsIndex) -> &Self; - fn get_subs_slice(subs: &Subs, slice: SubsSlice) -> &[Self]; - fn push_new(subs: &mut Subs, name: Self) -> SubsIndex; - fn extend_new(subs: &mut Subs, slice: impl IntoIterator) -> SubsSlice; - /// Reserves [size_hint] in the appropriate slice, and returns the current next start of the - /// slice. - fn reserve(subs: &mut Subs, size_hint: usize) -> u32; -} - -pub type UnionTags = UnionLabels; -pub type UnionLambdas = UnionLabels; - -impl Label for TagName { - fn index_subs(subs: &Subs, idx: SubsIndex) -> &Self { - &subs[idx] - } - fn get_subs_slice(subs: &Subs, slice: SubsSlice) -> &[Self] { - subs.get_subs_slice(slice) - } - fn push_new(subs: &mut Subs, name: Self) -> SubsIndex { - SubsIndex::push_new(&mut subs.tag_names, name) - } - fn extend_new(subs: &mut Subs, slice: impl IntoIterator) -> SubsSlice { - SubsSlice::extend_new(&mut subs.tag_names, slice) - } - fn reserve(subs: &mut Subs, size_hint: usize) -> u32 { - let tag_names_start = subs.tag_names.len() as u32; - subs.tag_names.reserve(size_hint); - tag_names_start - } -} -impl Label for Symbol { - fn index_subs(subs: &Subs, idx: SubsIndex) -> &Self { - &subs[idx] - } - fn get_subs_slice(subs: &Subs, slice: SubsSlice) -> &[Self] { - subs.get_subs_slice(slice) - } - fn push_new(subs: &mut Subs, name: Self) -> SubsIndex { - SubsIndex::push_new(&mut subs.closure_names, name) - } - fn extend_new(subs: &mut Subs, slice: impl IntoIterator) -> SubsSlice { - SubsSlice::extend_new(&mut subs.closure_names, slice) - } - fn reserve(subs: &mut Subs, size_hint: usize) -> u32 { - let closure_names_start = subs.closure_names.len() as u32; - subs.closure_names.reserve(size_hint); - closure_names_start - } -} - -#[derive(Clone, Debug)] -pub struct UnionLabels { - length: u16, - labels_start: u32, - variables_start: u32, - _marker: std::marker::PhantomData, -} - -impl Default for UnionLabels { - fn default() -> Self { - Self { - length: Default::default(), - labels_start: Default::default(), - variables_start: Default::default(), - _marker: Default::default(), - } - } -} - -impl Copy for UnionLabels {} - -impl UnionLabels -where - L: Label, -{ - pub fn is_newtype_wrapper(&self, subs: &Subs) -> bool { - if self.length != 1 { - return false; - } - - let slice = subs.variable_slices[self.variables_start as usize]; - slice.length == 1 - } - - pub fn from_tag_name_index(index: SubsIndex) -> Self { - Self::from_slices( - SubsSlice::new(index.index, 1), - SubsSlice::new(0, 1), // the first variablesubsslice is the empty slice - ) - } - - pub fn from_slices(labels: SubsSlice, variables: SubsSlice) -> Self { - debug_assert_eq!( - labels.len(), - variables.len(), - "tag name len != variables len: {:?} {:?}", - labels, - variables, - ); - - Self { - length: labels.len() as u16, - labels_start: labels.start, - variables_start: variables.start, - _marker: Default::default(), - } - } - - pub fn labels(&self) -> SubsSlice { - SubsSlice::new(self.labels_start, self.length) - } - - pub fn variables(&self) -> SubsSlice { - SubsSlice::new(self.variables_start, self.length) - } - - pub fn len(&self) -> usize { - self.length as usize - } - - pub fn is_empty(&self) -> bool { - self.len() == 0 - } - - pub fn insert_into_subs(subs: &mut Subs, input: I) -> Self - where - I: IntoIterator, - I2: IntoIterator, - { - let variables_start = subs.variable_slices.len() as u32; - - let it = input.into_iter(); - let size_hint = it.size_hint().0; - - let labels_start = L::reserve(subs, size_hint); - subs.variable_slices.reserve(size_hint); - - let mut length = 0; - for (k, v) in it { - let variables = VariableSubsSlice::insert_into_subs(subs, v.into_iter()); - - L::push_new(subs, k); - subs.variable_slices.push(variables); - - length += 1; - } - - Self::from_slices( - SubsSlice::new(labels_start, length), - SubsSlice::new(variables_start, length), - ) - } - - pub fn tag_without_arguments(subs: &mut Subs, tag_name: L) -> Self { - let idx = L::push_new(subs, tag_name); - - Self { - length: 1, - labels_start: idx.index, - variables_start: 0, - _marker: Default::default(), - } - } - - pub fn insert_slices_into_subs(subs: &mut Subs, input: I) -> Self - where - I: IntoIterator, - { - let variables_start = subs.variable_slices.len() as u32; - - let it = input.into_iter(); - let size_hint = it.size_hint().0; - - let labels_start = L::reserve(subs, size_hint); - subs.variable_slices.reserve(size_hint); - - let mut length = 0; - for (k, variables) in it { - L::push_new(subs, k); - subs.variable_slices.push(variables); - - length += 1; - } - - Self { - length, - labels_start, - variables_start, - _marker: Default::default(), - } - } - - pub fn iter_all( - &self, - ) -> impl Iterator, SubsIndex)> + ExactSizeIterator - { - self.labels().into_iter().zip(self.variables().into_iter()) - } - - /// Iterator over (Tag, &[Variable]) pairs obtained by - /// looking up slices in the given Subs - pub fn iter_from_subs<'a>( - &'a self, - subs: &'a Subs, - ) -> impl Iterator + ExactSizeIterator { - self.iter_all().map(move |(name_index, payload_index)| { - ( - L::index_subs(subs, name_index), - subs.get_subs_slice(subs[payload_index]), - ) - }) - } -} - -impl UnionLabels -where - L: Label + Ord, -{ - pub fn is_sorted_no_duplicates(&self, subs: &Subs) -> bool { - let mut iter = self.iter_from_subs(subs).peekable(); - while let Some((before, _)) = iter.next() { - if let Some((after, _)) = iter.peek() { - if before >= after { - return false; - } - } - } - true - } -} - -impl UnionTags { - #[inline(always)] - pub fn unsorted_iterator<'a>( - &'a self, - subs: &'a Subs, - ext: Variable, - ) -> impl Iterator + 'a { - let (it, _) = crate::types::gather_tags_unsorted_iter(subs, *self, ext); - - let f = move |(label, slice): (_, SubsSlice)| (label, subs.get_subs_slice(slice)); - - it.map(f) - } - - #[inline(always)] - pub fn unsorted_tags_and_ext<'a>( - &'a self, - subs: &'a Subs, - ext: Variable, - ) -> (UnsortedUnionLabels<'a, TagName>, Variable) { - let (it, ext) = crate::types::gather_tags_unsorted_iter(subs, *self, ext); - let f = move |(label, slice): (_, SubsSlice)| (label, subs.get_subs_slice(slice)); - let it = it.map(f); - - (UnsortedUnionLabels { tags: it.collect() }, ext) - } - - #[inline(always)] - pub fn sorted_iterator_and_ext<'a>( - &'_ self, - subs: &'a Subs, - ext: Variable, - ) -> (SortedTagsIterator<'a>, Variable) { - if is_empty_tag_union(subs, ext) { - ( - Box::new(self.iter_all().map(move |(i1, i2)| { - let tag_name: &TagName = &subs[i1]; - let subs_slice = subs[i2]; - - let slice = subs.get_subs_slice(subs_slice); - - (tag_name.clone(), slice) - })), - ext, - ) - } else { - let union_structure = crate::types::gather_tags(subs, *self, ext); - - ( - Box::new(union_structure.fields.into_iter()), - union_structure.ext, - ) - } - } - - #[inline(always)] - pub fn sorted_slices_iterator_and_ext<'a>( - &'_ self, - subs: &'a Subs, - ext: Variable, - ) -> (SortedTagsSlicesIterator<'a>, Variable) { - if is_empty_tag_union(subs, ext) { - ( - Box::new(self.iter_all().map(move |(i1, i2)| { - let tag_name: &TagName = &subs[i1]; - let subs_slice = subs[i2]; - - (tag_name.clone(), subs_slice) - })), - ext, - ) - } else { - let (fields, ext) = crate::types::gather_tags_slices(subs, *self, ext); - - (Box::new(fields.into_iter()), ext) - } - } -} - -impl UnionLambdas { - // In practice UnionLambdas are always sorted to begin with because they start off with exactly - // one element and are merged in sorted order during solving. TODO: go through and cleanup - // sorted/unsorted discrepancies here and in [UnionTags]. - pub fn unsorted_lambdas<'a>(&'a self, subs: &'a Subs) -> UnsortedUnionLabels<'a, Symbol> { - let it = self - .iter_all() - .map(|(s, vars)| (&subs[s], subs.get_subs_slice(subs[vars]))); - - UnsortedUnionLabels { tags: it.collect() } - } -} - -#[derive(Debug)] -pub struct UnsortedUnionLabels<'a, L: Label> { - pub tags: Vec<(&'a L, &'a [Variable])>, -} - -impl<'a, L: Label> UnsortedUnionLabels<'a, L> { - pub fn is_newtype_wrapper(&self, _subs: &Subs) -> bool { - if self.tags.len() != 1 { - return false; - } - self.tags[0].1.len() == 1 - } - - pub fn get_newtype(&self, _subs: &Subs) -> (&L, Variable) { - let (tag_name, vars) = self.tags[0]; - (tag_name, vars[0]) - } -} - -pub type SortedTagsIterator<'a> = Box + 'a>; -pub type SortedTagsSlicesIterator<'a> = Box + 'a>; - -pub fn is_empty_tag_union(subs: &Subs, mut var: Variable) -> bool { - use crate::subs::Content::*; - use crate::subs::FlatType::*; - - loop { - match subs.get_content_without_compacting(var) { - FlexVar(_) => return true, - Structure(EmptyTagUnion) => return true, - Structure(TagUnion(sub_fields, sub_ext)) => { - if !sub_fields.is_empty() { - return false; - } - - var = *sub_ext; - } - Structure(RecursiveTagUnion(_, sub_fields, sub_ext)) => { - if !sub_fields.is_empty() { - return false; - } - - var = *sub_ext; - } - - Alias(_, _, actual_var, _) => { - // TODO according to elm/compiler: "TODO may be dropping useful alias info here" - var = *actual_var; - } - - _other => { - return false; - } - } - } -} - -#[derive(Clone, Copy, Debug)] -pub struct RecordFields { - pub length: u16, - pub field_names_start: u32, - pub variables_start: u32, - pub field_types_start: u32, -} - -fn first(x: &(K, V), y: &(K, V)) -> std::cmp::Ordering { - x.0.cmp(&y.0) -} - -pub type SortedIterator<'a> = Box)> + 'a>; - -impl RecordFields { - pub const fn len(&self) -> usize { - self.length as usize - } - - pub fn is_empty(&self) -> bool { - self.len() == 0 - } - - pub fn empty() -> Self { - Self { - length: 0, - field_names_start: 0, - variables_start: 0, - field_types_start: 0, - } - } - - pub const fn variables(&self) -> SubsSlice { - SubsSlice::new(self.variables_start, self.length) - } - - pub const fn field_names(&self) -> SubsSlice { - SubsSlice::new(self.field_names_start, self.length) - } - - pub const fn record_fields(&self) -> SubsSlice> { - SubsSlice::new(self.field_types_start, self.length) - } - - pub fn iter_variables(&self) -> impl Iterator> { - let slice = SubsSlice::new(self.variables_start, self.length); - slice.into_iter() - } - - pub fn has_only_optional_fields(&self, subs: &Subs) -> bool { - let slice: SubsSlice> = SubsSlice::new(self.field_types_start, self.length); - - subs.get_subs_slice(slice) - .iter() - .all(|field| matches!(field, RecordField::Optional(_))) - } - - pub fn compare( - x: &(Lowercase, RecordField), - y: &(Lowercase, RecordField), - ) -> std::cmp::Ordering { - first(x, y) - } - - pub fn insert_into_subs(subs: &mut Subs, input: I) -> Self - where - I: IntoIterator)>, - { - let field_names_start = subs.field_names.len() as u32; - let variables_start = subs.variables.len() as u32; - let field_types_start = subs.record_fields.len() as u32; - - let it = input.into_iter(); - let size_hint = it.size_hint().0; - - subs.variables.reserve(size_hint); - subs.field_names.reserve(size_hint); - subs.record_fields.reserve(size_hint); - - let mut length = 0; - for (k, v) in it { - let var = *v.as_inner(); - let record_field = v.map(|_| ()); - - subs.field_names.push(k); - subs.variables.push(var); - subs.record_fields.push(record_field); - - length += 1; - } - - RecordFields { - length, - field_names_start, - variables_start, - field_types_start, - } - } - - #[inline(always)] - pub fn unsorted_iterator<'a>( - &'a self, - subs: &'a Subs, - ext: Variable, - ) -> Result)> + 'a, RecordFieldsError> - { - let (it, _) = crate::types::gather_fields_unsorted_iter(subs, *self, ext)?; - - Ok(it) - } - - #[inline(always)] - pub fn unsorted_iterator_and_ext<'a>( - &'a self, - subs: &'a Subs, - ext: Variable, - ) -> ( - impl Iterator)> + 'a, - Variable, - ) { - let (it, ext) = crate::types::gather_fields_unsorted_iter(subs, *self, ext) - .expect("Something weird ended up in a record type"); - - (it, ext) - } - - /// Get a sorted iterator over the fields of this record type - /// - /// Implementation: When the record has an `ext` variable that is the empty record, then - /// we read the (assumed sorted) fields directly from Subs. Otherwise we have to chase the - /// ext var, then sort the fields. - /// - /// Hopefully the inline will get rid of the Box in practice - #[inline(always)] - pub fn sorted_iterator<'a>(&'_ self, subs: &'a Subs, ext: Variable) -> SortedIterator<'a> { - self.sorted_iterator_and_ext(subs, ext).0 - } - - #[inline(always)] - pub fn sorted_iterator_and_ext<'a>( - &'_ self, - subs: &'a Subs, - ext: Variable, - ) -> (SortedIterator<'a>, Variable) { - if is_empty_record(subs, ext) { - ( - Box::new(self.iter_all().map(move |(i1, i2, i3)| { - let field_name: Lowercase = subs[i1].clone(); - let variable = subs[i2]; - let record_field: RecordField = subs[i3].map(|_| variable); - - (field_name, record_field) - })), - ext, - ) - } else { - let record_structure = crate::types::gather_fields(subs, *self, ext) - .expect("Something ended up weird in this record type"); - - ( - Box::new(record_structure.fields.into_iter()), - record_structure.ext, - ) - } - } - - pub fn iter_all( - &self, - ) -> impl Iterator< - Item = ( - SubsIndex, - SubsIndex, - SubsIndex>, - ), - > { - let helper = |start| start..(start + self.length as u32); - - let range1 = helper(self.field_names_start); - let range2 = helper(self.variables_start); - let range3 = helper(self.field_types_start); - - let it = range1 - .into_iter() - .zip(range2.into_iter()) - .zip(range3.into_iter()); - - it.map(|((i1, i2), i3)| (SubsIndex::new(i1), SubsIndex::new(i2), SubsIndex::new(i3))) - } -} - -fn is_empty_record(subs: &Subs, mut var: Variable) -> bool { - use crate::subs::Content::*; - use crate::subs::FlatType::*; - - loop { - match subs.get_content_without_compacting(var) { - Structure(EmptyRecord) => return true, - Structure(Record(sub_fields, sub_ext)) => { - if !sub_fields.is_empty() { - return false; - } - - var = *sub_ext; - } - - Alias(_, _, actual_var, _) => { - // TODO according to elm/compiler: "TODO may be dropping useful alias info here" - var = *actual_var; - } - - _ => return false, - } - } -} - -fn occurs( - subs: &Subs, - seen: &[Variable], - input_var: Variable, - include_recursion_var: bool, -) -> Result<(), (Variable, Vec)> { - use self::Content::*; - use self::FlatType::*; - - let root_var = subs.get_root_key_without_compacting(input_var); - - if seen.contains(&root_var) { - Err((root_var, vec![])) - } else { - match subs.get_content_without_compacting(root_var) { - FlexVar(_) - | RigidVar(_) - | FlexAbleVar(_, _) - | RigidAbleVar(_, _) - | RecursionVar { .. } - | Error => Ok(()), - - Structure(flat_type) => { - let mut new_seen = seen.to_owned(); - - new_seen.push(root_var); - - match flat_type { - Apply(_, args) => short_circuit( - subs, - root_var, - &new_seen, - subs.get_subs_slice(*args).iter(), - include_recursion_var, - ), - Func(arg_vars, closure_var, ret_var) => { - let it = once(ret_var) - .chain(once(closure_var)) - .chain(subs.get_subs_slice(*arg_vars).iter()); - short_circuit(subs, root_var, &new_seen, it, include_recursion_var) - } - Record(vars_by_field, ext_var) => { - let slice = - SubsSlice::new(vars_by_field.variables_start, vars_by_field.length); - let it = once(ext_var).chain(subs.get_subs_slice(slice).iter()); - short_circuit(subs, root_var, &new_seen, it, include_recursion_var) - } - TagUnion(tags, ext_var) => { - occurs_union(subs, root_var, &new_seen, include_recursion_var, tags)?; - - short_circuit_help( - subs, - root_var, - &new_seen, - *ext_var, - include_recursion_var, - ) - } - FunctionOrTagUnion(_, _, ext_var) => { - let it = once(ext_var); - short_circuit(subs, root_var, &new_seen, it, include_recursion_var) - } - RecursiveTagUnion(rec_var, tags, ext_var) => { - if include_recursion_var { - new_seen.push(subs.get_root_key_without_compacting(*rec_var)); - } - occurs_union(subs, root_var, &new_seen, include_recursion_var, tags)?; - - short_circuit_help( - subs, - root_var, - &new_seen, - *ext_var, - include_recursion_var, - ) - } - EmptyRecord | EmptyTagUnion | Erroneous(_) => Ok(()), - } - } - Alias(_, args, _, _) => { - let mut new_seen = seen.to_owned(); - new_seen.push(root_var); - - for var_index in args.into_iter() { - let var = subs[var_index]; - short_circuit_help(subs, root_var, &new_seen, var, include_recursion_var)?; - } - - Ok(()) - } - LambdaSet(self::LambdaSet { - solved, - recursion_var, - unspecialized: _, - }) => { - let mut new_seen = seen.to_owned(); - new_seen.push(root_var); - - if include_recursion_var { - if let Some(v) = recursion_var.into_variable() { - new_seen.push(subs.get_root_key_without_compacting(v)); - } - } - - // unspecialized lambda vars excluded because they are not explicitly part of the - // type (they only matter after being resolved). - - occurs_union(subs, root_var, &new_seen, include_recursion_var, solved) - } - RangedNumber(typ, _range_vars) => { - let mut new_seen = seen.to_owned(); - new_seen.push(root_var); - - short_circuit_help(subs, root_var, &new_seen, *typ, include_recursion_var)?; - // _range_vars excluded because they are not explicitly part of the type. - - Ok(()) - } - } - } -} - -#[inline(always)] -fn occurs_union( - subs: &Subs, - root_var: Variable, - seen: &[Variable], - include_recursion_var: bool, - tags: &UnionLabels, -) -> Result<(), (Variable, Vec)> { - for slice_index in tags.variables() { - let slice = subs[slice_index]; - for var_index in slice { - let var = subs[var_index]; - short_circuit_help(subs, root_var, seen, var, include_recursion_var)?; - } - } - Ok(()) -} - -#[inline(always)] -fn short_circuit<'a, T>( - subs: &Subs, - root_key: Variable, - seen: &[Variable], - iter: T, - include_recursion_var: bool, -) -> Result<(), (Variable, Vec)> -where - T: Iterator, -{ - for var in iter { - short_circuit_help(subs, root_key, seen, *var, include_recursion_var)?; - } - - Ok(()) -} - -#[inline(always)] -fn short_circuit_help( - subs: &Subs, - root_key: Variable, - seen: &[Variable], - var: Variable, - include_recursion_var: bool, -) -> Result<(), (Variable, Vec)> { - if let Err((v, mut vec)) = occurs(subs, seen, var, include_recursion_var) { - vec.push(root_key); - return Err((v, vec)); - } - - Ok(()) -} - -fn explicit_substitute( - subs: &mut Subs, - from: Variable, - to: Variable, - in_var: Variable, - seen: &mut ImSet, -) -> Variable { - use self::Content::*; - use self::FlatType::*; - let in_root = subs.get_root_key(in_var); - if subs.get_root_key(from) == in_root { - to - } else if seen.contains(&in_root) { - in_var - } else { - seen.insert(in_root); - - match subs.get(in_var).content { - FlexVar(_) - | RigidVar(_) - | FlexAbleVar(_, _) - | RigidAbleVar(_, _) - | RecursionVar { .. } - | Error => in_var, - - Structure(flat_type) => { - match flat_type { - Apply(symbol, args) => { - for var_index in args.into_iter() { - let var = subs[var_index]; - let answer = explicit_substitute(subs, from, to, var, seen); - subs[var_index] = answer; - } - - subs.set_content(in_var, Structure(Apply(symbol, args))); - } - Func(arg_vars, closure_var, ret_var) => { - for var_index in arg_vars.into_iter() { - let var = subs[var_index]; - let answer = explicit_substitute(subs, from, to, var, seen); - subs[var_index] = answer; - } - - let new_ret_var = explicit_substitute(subs, from, to, ret_var, seen); - let new_closure_var = - explicit_substitute(subs, from, to, closure_var, seen); - - subs.set_content( - in_var, - Structure(Func(arg_vars, new_closure_var, new_ret_var)), - ); - } - TagUnion(tags, ext_var) => { - let new_ext_var = explicit_substitute(subs, from, to, ext_var, seen); - - let union_tags = explicit_substitute_union(subs, from, to, tags, seen); - - subs.set_content(in_var, Structure(TagUnion(union_tags, new_ext_var))); - } - FunctionOrTagUnion(tag_name, symbol, ext_var) => { - let new_ext_var = explicit_substitute(subs, from, to, ext_var, seen); - subs.set_content( - in_var, - Structure(FunctionOrTagUnion(tag_name, symbol, new_ext_var)), - ); - } - RecursiveTagUnion(rec_var, tags, ext_var) => { - // NOTE rec_var is not substituted, verify that this is correct! - let new_ext_var = explicit_substitute(subs, from, to, ext_var, seen); - - let union_tags = explicit_substitute_union(subs, from, to, tags, seen); - - subs.set_content( - in_var, - Structure(RecursiveTagUnion(rec_var, union_tags, new_ext_var)), - ); - } - Record(vars_by_field, ext_var) => { - let new_ext_var = explicit_substitute(subs, from, to, ext_var, seen); - - for index in vars_by_field.iter_variables() { - let var = subs[index]; - let new_var = explicit_substitute(subs, from, to, var, seen); - subs[index] = new_var; - } - - subs.set_content(in_var, Structure(Record(vars_by_field, new_ext_var))); - } - - EmptyRecord | EmptyTagUnion | Erroneous(_) => {} - } - - in_var - } - Alias(symbol, args, actual, kind) => { - for index in args.into_iter() { - let var = subs[index]; - let new_var = explicit_substitute(subs, from, to, var, seen); - subs[index] = new_var; - } - - let new_actual = explicit_substitute(subs, from, to, actual, seen); - - subs.set_content(in_var, Alias(symbol, args, new_actual, kind)); - - in_var - } - LambdaSet(self::LambdaSet { - solved, - recursion_var, - unspecialized, - }) => { - // NOTE recursion_var is not substituted, verify that this is correct! - let new_solved = explicit_substitute_union(subs, from, to, solved, seen); - - for Uls(v, _, _) in subs.get_subs_slice(unspecialized) { - debug_assert!(*v != from, "unspecialized lambda set vars should never occur in a position where they need to be explicitly substituted."); - } - - subs.set_content( - in_var, - LambdaSet(self::LambdaSet { - solved: new_solved, - recursion_var, - unspecialized, - }), - ); - - in_var - } - RangedNumber(typ, range) => { - let new_typ = explicit_substitute(subs, from, to, typ, seen); - - subs.set_content(in_var, RangedNumber(new_typ, range)); - - in_var - } - } - } -} - -#[inline(always)] -fn explicit_substitute_union( - subs: &mut Subs, - from: Variable, - to: Variable, - tags: UnionLabels, - seen: &mut ImSet, -) -> UnionLabels { - let mut new_slices = Vec::new(); - for slice_index in tags.variables() { - let slice = subs[slice_index]; - - let mut new_variables = Vec::new(); - for var_index in slice { - let var = subs[var_index]; - let new_var = explicit_substitute(subs, from, to, var, seen); - new_variables.push(new_var); - } - - let start = subs.variables.len() as u32; - let length = new_variables.len() as u16; - - subs.variables.extend(new_variables); - - new_slices.push(VariableSubsSlice::new(start, length)); - } - - let start = subs.variable_slices.len() as u32; - let length = new_slices.len(); - - subs.variable_slices.extend(new_slices); - - let mut union_tags = tags; - debug_assert_eq!(length, union_tags.len()); - union_tags.variables_start = start; - union_tags -} - -fn get_var_names( - subs: &mut Subs, - var: Variable, - taken_names: ImMap, -) -> ImMap { - use self::Content::*; - let desc = subs.get(var); - - if desc.mark == Mark::GET_VAR_NAMES { - taken_names - } else { - subs.set_mark(var, Mark::GET_VAR_NAMES); - - match desc.content { - Error | FlexVar(None) | FlexAbleVar(None, _) => taken_names, - - FlexVar(Some(name_index)) | FlexAbleVar(Some(name_index), _) => add_name( - subs, - 0, - name_index, - var, - |name| FlexVar(Some(name)), - taken_names, - ), - - RecursionVar { - opt_name, - structure, - } => match opt_name { - Some(name_index) => add_name( - subs, - 0, - name_index, - var, - |name| RecursionVar { - opt_name: Some(name), - structure, - }, - taken_names, - ), - None => taken_names, - }, - - RigidVar(name_index) | RigidAbleVar(name_index, _) => { - add_name(subs, 0, name_index, var, RigidVar, taken_names) - } - - Alias(_, args, _, _) => args.into_iter().fold(taken_names, |answer, arg_var| { - get_var_names(subs, subs[arg_var], answer) - }), - - LambdaSet(self::LambdaSet { - solved, - recursion_var, - unspecialized, - }) => { - let taken_names = get_var_names_union(subs, solved, taken_names); - let mut taken_names = match recursion_var.into_variable() { - Some(v) => get_var_names(subs, v, taken_names), - None => taken_names, - }; - for uls_index in unspecialized { - let Uls(v, _, _) = subs[uls_index]; - taken_names = get_var_names(subs, v, taken_names); - } - taken_names - } - - RangedNumber(typ, _) => get_var_names(subs, typ, taken_names), - - Structure(flat_type) => match flat_type { - FlatType::Apply(_, args) => { - args.into_iter().fold(taken_names, |answer, arg_var| { - get_var_names(subs, subs[arg_var], answer) - }) - } - - FlatType::Func(arg_vars, closure_var, ret_var) => { - let taken_names = get_var_names(subs, ret_var, taken_names); - let taken_names = get_var_names(subs, closure_var, taken_names); - - let mut accum = taken_names; - - for var_index in arg_vars.into_iter() { - let arg_var = subs[var_index]; - - accum = get_var_names(subs, arg_var, accum) - } - - accum - } - - FlatType::EmptyRecord | FlatType::EmptyTagUnion | FlatType::Erroneous(_) => { - taken_names - } - - FlatType::Record(vars_by_field, ext_var) => { - let mut accum = get_var_names(subs, ext_var, taken_names); - - for var_index in vars_by_field.iter_variables() { - let arg_var = subs[var_index]; - - accum = get_var_names(subs, arg_var, accum) - } - - accum - } - FlatType::TagUnion(tags, ext_var) => { - let taken_names = get_var_names(subs, ext_var, taken_names); - get_var_names_union(subs, tags, taken_names) - } - - FlatType::FunctionOrTagUnion(_, _, ext_var) => { - get_var_names(subs, ext_var, taken_names) - } - - FlatType::RecursiveTagUnion(rec_var, tags, ext_var) => { - let taken_names = get_var_names(subs, ext_var, taken_names); - let taken_names = get_var_names(subs, rec_var, taken_names); - get_var_names_union(subs, tags, taken_names) - } - }, - } - } -} - -#[inline(always)] -fn get_var_names_union( - subs: &mut Subs, - tags: UnionLabels, - mut taken_names: ImMap, -) -> ImMap { - for slice_index in tags.variables() { - let slice = subs[slice_index]; - for var_index in slice { - let var = subs[var_index]; - taken_names = get_var_names(subs, var, taken_names) - } - } - taken_names -} - -fn add_name( - subs: &mut Subs, - index: usize, - given_name_index: SubsIndex, - var: Variable, - content_from_name: F, - taken_names: ImMap, -) -> ImMap -where - F: FnOnce(SubsIndex) -> Content, -{ - let given_name = subs.field_names[given_name_index.index as usize].clone(); - - let indexed_name = if index == 0 { - given_name.clone() - } else { - // TODO is this the proper use of index here, or should we be - // doing something else like turning it into an ASCII letter? - Lowercase::from(format!("{}{}", given_name, index)) - }; - - match taken_names.get(&indexed_name) { - None => { - if indexed_name != given_name { - let indexed_name_index = - SubsIndex::push_new(&mut subs.field_names, indexed_name.clone()); - subs.set_content(var, content_from_name(indexed_name_index)); - } - - let mut answer = taken_names.clone(); - - answer.insert(indexed_name, var); - - taken_names - } - Some(&other_var) => { - if subs.equivalent(var, other_var) { - taken_names - } else { - add_name( - subs, - index + 1, - given_name_index, - var, - content_from_name, - taken_names, - ) - } - } - } -} - -fn var_to_err_type(subs: &mut Subs, state: &mut ErrorTypeState, var: Variable) -> ErrorType { - let desc = subs.get(var); - - if desc.mark == Mark::OCCURS { - ErrorType::Infinite - } else { - subs.set_mark(var, Mark::OCCURS); - - let err_type = content_to_err_type(subs, state, var, desc.content); - - subs.set_mark(var, desc.mark); - - err_type - } -} - -fn content_to_err_type( - subs: &mut Subs, - state: &mut ErrorTypeState, - var: Variable, - content: Content, -) -> ErrorType { - use self::Content::*; - - match content { - Structure(flat_type) => flat_type_to_err_type(subs, state, flat_type), - - FlexVar(opt_name) => { - let name = match opt_name { - Some(name_index) => subs.field_names[name_index.index as usize].clone(), - None => { - // set the name so when this variable occurs elsewhere in the type it gets the same name - let name = get_fresh_var_name(state); - let name_index = SubsIndex::push_new(&mut subs.field_names, name.clone()); - - subs.set_content(var, FlexVar(Some(name_index))); - - name - } - }; - - ErrorType::FlexVar(name) - } - - RigidVar(name_index) => { - let name = subs.field_names[name_index.index as usize].clone(); - ErrorType::RigidVar(name) - } - - FlexAbleVar(opt_name, ability) => { - let name = match opt_name { - Some(name_index) => subs.field_names[name_index.index as usize].clone(), - None => { - // set the name so when this variable occurs elsewhere in the type it gets the same name - let name = get_fresh_var_name(state); - let name_index = SubsIndex::push_new(&mut subs.field_names, name.clone()); - - subs.set_content(var, FlexVar(Some(name_index))); - - name - } - }; - - ErrorType::FlexAbleVar(name, ability) - } - - RigidAbleVar(name_index, ability) => { - let name = subs.field_names[name_index.index as usize].clone(); - ErrorType::RigidAbleVar(name, ability) - } - - RecursionVar { - opt_name, - structure, - } => { - let name = match opt_name { - Some(name_index) => subs.field_names[name_index.index as usize].clone(), - None => { - let name = get_fresh_var_name(state); - let name_index = SubsIndex::push_new(&mut subs.field_names, name.clone()); - - subs.set_content(var, FlexVar(Some(name_index))); - - name - } - }; - - if state.recursive_tag_unions_seen.contains(&var) { - ErrorType::FlexVar(name) - } else { - var_to_err_type(subs, state, structure) - } - } - - Alias(symbol, args, aliased_to, kind) => { - let err_type = var_to_err_type(subs, state, aliased_to); - - let mut err_args = Vec::with_capacity(args.len()); - - for var_index in args.into_iter() { - let var = subs[var_index]; - - let arg = var_to_err_type(subs, state, var); - - err_args.push(arg); - } - - ErrorType::Alias(symbol, err_args, Box::new(err_type), kind) - } - - LambdaSet(self::LambdaSet { .. }) => { - // Don't print lambda sets since we don't expect them to be exposed to the user - ErrorType::Error - } - - RangedNumber(typ, range) => { - let err_type = var_to_err_type(subs, state, typ); - - if state.context == ErrorTypeContext::ExpandRanges { - let mut types = Vec::new(); - for var in range.variable_slice() { - types.push(var_to_err_type(subs, state, *var)); - } - ErrorType::Range(Box::new(err_type), types) - } else { - err_type - } - } - - Error => ErrorType::Error, - } -} - -fn flat_type_to_err_type( - subs: &mut Subs, - state: &mut ErrorTypeState, - flat_type: FlatType, -) -> ErrorType { - use self::FlatType::*; - - match flat_type { - Apply(symbol, args) => { - let arg_types = args - .into_iter() - .map(|index| { - let arg_var = subs[index]; - var_to_err_type(subs, state, arg_var) - }) - .collect(); - - ErrorType::Type(symbol, arg_types) - } - - Func(arg_vars, closure_var, ret_var) => { - let args = arg_vars - .into_iter() - .map(|index| { - let arg_var = subs[index]; - var_to_err_type(subs, state, arg_var) - }) - .collect(); - - let ret = var_to_err_type(subs, state, ret_var); - let closure = var_to_err_type(subs, state, closure_var); - - ErrorType::Function(args, Box::new(closure), Box::new(ret)) - } - - EmptyRecord => ErrorType::Record(SendMap::default(), TypeExt::Closed), - EmptyTagUnion => ErrorType::TagUnion(SendMap::default(), TypeExt::Closed), - - Record(vars_by_field, ext_var) => { - let mut err_fields = SendMap::default(); - - for (i1, i2, i3) in vars_by_field.iter_all() { - let label = subs[i1].clone(); - let var = subs[i2]; - let record_field = subs[i3]; - - let error_type = var_to_err_type(subs, state, var); - - use RecordField::*; - let err_record_field = match record_field { - Optional(_) => Optional(error_type), - Required(_) => Required(error_type), - Demanded(_) => Demanded(error_type), - }; - - err_fields.insert(label, err_record_field); - } - - match var_to_err_type(subs, state, ext_var).unwrap_structural_alias() { - ErrorType::Record(sub_fields, sub_ext) => { - ErrorType::Record(sub_fields.union(err_fields), sub_ext) - } - - ErrorType::FlexVar(var) => { - ErrorType::Record(err_fields, TypeExt::FlexOpen(var)) - } - - ErrorType::RigidVar(var) => { - ErrorType::Record(err_fields, TypeExt::RigidOpen(var)) - } - - other => - panic!("Tried to convert a record extension to an error, but the record extension had the ErrorType of {:?}", other) - } - } - - TagUnion(tags, ext_var) => { - let err_tags = union_tags_to_err_tags(subs, state, tags); - - match var_to_err_type(subs, state, ext_var).unwrap_structural_alias() { - ErrorType::TagUnion(sub_tags, sub_ext) => { - ErrorType::TagUnion(sub_tags.union(err_tags), sub_ext) - } - ErrorType::RecursiveTagUnion(_, sub_tags, sub_ext) => { - ErrorType::TagUnion(sub_tags.union(err_tags), sub_ext) - } - - ErrorType::FlexVar(var) => { - ErrorType::TagUnion(err_tags, TypeExt::FlexOpen(var)) - } - - ErrorType::RigidVar(var) => { - ErrorType::TagUnion(err_tags, TypeExt::RigidOpen(var)) - } - - other => - panic!("Tried to convert a tag union extension to an error, but the tag union extension had the ErrorType of {:?}", other) - } - } - - FunctionOrTagUnion(tag_name, _, ext_var) => { - let tag_name = subs[tag_name].clone(); - - let mut err_tags = SendMap::default(); - - err_tags.insert(tag_name, vec![]); - - match var_to_err_type(subs, state, ext_var).unwrap_structural_alias() { - ErrorType::TagUnion(sub_tags, sub_ext) => { - ErrorType::TagUnion(sub_tags.union(err_tags), sub_ext) - } - ErrorType::RecursiveTagUnion(_, sub_tags, sub_ext) => { - ErrorType::TagUnion(sub_tags.union(err_tags), sub_ext) - } - - ErrorType::FlexVar(var) => { - ErrorType::TagUnion(err_tags, TypeExt::FlexOpen(var)) - } - - ErrorType::RigidVar(var) => { - ErrorType::TagUnion(err_tags, TypeExt::RigidOpen(var)) - } - - other => - panic!("Tried to convert a tag union extension to an error, but the tag union extension had the ErrorType of {:?}", other) - } - } - - RecursiveTagUnion(rec_var, tags, ext_var) => { - state.recursive_tag_unions_seen.push(rec_var); - - let err_tags = union_tags_to_err_tags(subs, state, tags); - - let rec_error_type = Box::new(var_to_err_type(subs, state, rec_var)); - - match var_to_err_type(subs, state, ext_var).unwrap_structural_alias() { - ErrorType::RecursiveTagUnion(rec_var, sub_tags, sub_ext) => { - debug_assert!(rec_var == rec_error_type); - ErrorType::RecursiveTagUnion(rec_error_type, sub_tags.union(err_tags), sub_ext) - } - - ErrorType::TagUnion(sub_tags, sub_ext) => { - ErrorType::RecursiveTagUnion(rec_error_type, sub_tags.union(err_tags), sub_ext) - } - - ErrorType::FlexVar(var) => { - ErrorType::RecursiveTagUnion(rec_error_type, err_tags, TypeExt::FlexOpen(var)) - } - - ErrorType::RigidVar(var) => { - ErrorType::RecursiveTagUnion(rec_error_type, err_tags, TypeExt::RigidOpen(var)) - } - - other => - panic!("Tried to convert a recursive tag union extension to an error, but the tag union extension had the ErrorType of {:?}", other) - } - } - - Erroneous(problem_index) => { - let problem = subs.problems[problem_index.index as usize].clone(); - state.problems.push(problem); - - ErrorType::Error - } - } -} - -#[inline(always)] -fn union_tags_to_err_tags( - subs: &mut Subs, - state: &mut ErrorTypeState, - tags: UnionTags, -) -> SendMap> { - let mut err_tags = SendMap::default(); - - for (name_index, slice_index) in tags.iter_all() { - let mut err_vars = Vec::with_capacity(tags.len()); - - let slice = subs[slice_index]; - for var_index in slice { - let var = subs[var_index]; - err_vars.push(var_to_err_type(subs, state, var)); - } - - let tag = subs[name_index].clone(); - err_tags.insert(tag, err_vars); - } - - err_tags -} - -fn get_fresh_var_name(state: &mut ErrorTypeState) -> Lowercase { - let (name, new_index) = - name_type_var(state.letters_used, &mut state.taken.iter(), |var, str| { - var.as_str() == str - }); - - state.letters_used = new_index; - - state.taken.insert(name.clone()); - - name -} - -#[derive(Clone, Debug)] -pub struct StorageSubs { - subs: Subs, -} - -#[derive(Copy, Clone, Debug)] -struct StorageSubsOffsets { - utable: u32, - variables: u32, - tag_names: u32, - closure_names: u32, - field_names: u32, - record_fields: u32, - variable_slices: u32, - unspecialized_lambda_sets: u32, - problems: u32, -} - -impl StorageSubs { - pub fn new(subs: Subs) -> Self { - Self { subs } - } - - pub fn fresh_unnamed_flex_var(&mut self) -> Variable { - self.subs.fresh_unnamed_flex_var() - } - - pub fn as_inner_mut(&mut self) -> &mut Subs { - &mut self.subs - } - - pub fn extend_with_variable(&mut self, source: &mut Subs, variable: Variable) -> Variable { - storage_copy_var_to(source, &mut self.subs, variable) - } - - pub fn import_variable_from(&mut self, source: &mut Subs, variable: Variable) -> CopiedImport { - copy_import_to(source, &mut self.subs, variable, Rank::import()) - } - - pub fn export_variable_to(&mut self, target: &mut Subs, variable: Variable) -> CopiedImport { - copy_import_to(&mut self.subs, target, variable, Rank::import()) - } - - pub fn merge_into(self, target: &mut Subs) -> impl Fn(Variable) -> Variable { - let self_offsets = StorageSubsOffsets { - utable: self.subs.utable.len() as u32, - variables: self.subs.variables.len() as u32, - tag_names: self.subs.tag_names.len() as u32, - closure_names: self.subs.closure_names.len() as u32, - field_names: self.subs.field_names.len() as u32, - record_fields: self.subs.record_fields.len() as u32, - variable_slices: self.subs.variable_slices.len() as u32, - unspecialized_lambda_sets: self.subs.unspecialized_lambda_sets.len() as u32, - problems: self.subs.problems.len() as u32, - }; - - let offsets = StorageSubsOffsets { - utable: (target.utable.len() - Variable::NUM_RESERVED_VARS) as u32, - variables: target.variables.len() as u32, - tag_names: target.tag_names.len() as u32, - closure_names: target.closure_names.len() as u32, - field_names: target.field_names.len() as u32, - record_fields: target.record_fields.len() as u32, - variable_slices: target.variable_slices.len() as u32, - unspecialized_lambda_sets: target.unspecialized_lambda_sets.len() as u32, - problems: target.problems.len() as u32, - }; - - // The first Variable::NUM_RESERVED_VARS are the same in every subs, - // so we can skip copying them! - let range = Variable::NUM_RESERVED_VARS..self.subs.utable.len(); - - // fill new slots with empty values - target.extend_by(range.len()); - - for i in range { - let variable = Variable(i as u32); - let descriptor = self.subs.utable.get_descriptor(variable); - debug_assert!(descriptor.copy.is_none()); - - let new_content = Self::offset_content(&offsets, &descriptor.content); - - let new_descriptor = Descriptor { - rank: descriptor.rank, - mark: descriptor.mark, - copy: OptVariable::NONE, - content: new_content, - }; - - let new_variable = Self::offset_variable(&offsets, variable); - target.set(new_variable, new_descriptor); - } - - target.variables.extend( - self.subs - .variables - .iter() - .map(|v| Self::offset_variable(&offsets, *v)), - ); - - target.variable_slices.extend( - self.subs - .variable_slices - .into_iter() - .map(|v| Self::offset_variable_slice(&offsets, v)), - ); - - target.tag_names.extend(self.subs.tag_names); - target.closure_names.extend(self.subs.closure_names); - target.field_names.extend(self.subs.field_names); - target.record_fields.extend(self.subs.record_fields); - target - .unspecialized_lambda_sets - .extend(self.subs.unspecialized_lambda_sets); - target.problems.extend(self.subs.problems); - - debug_assert_eq!( - target.utable.len(), - (self_offsets.utable + offsets.utable) as usize - ); - - debug_assert_eq!( - target.tag_names.len(), - (self_offsets.tag_names + offsets.tag_names) as usize - ); - - debug_assert_eq!( - target.closure_names.len(), - (self_offsets.closure_names + offsets.closure_names) as usize - ); - - move |v| { - let offsets = offsets; - Self::offset_variable(&offsets, v) - } - } - - fn offset_flat_type(offsets: &StorageSubsOffsets, flat_type: &FlatType) -> FlatType { - match flat_type { - FlatType::Apply(symbol, arguments) => { - FlatType::Apply(*symbol, Self::offset_variable_slice(offsets, *arguments)) - } - FlatType::Func(arguments, lambda_set, result) => FlatType::Func( - Self::offset_variable_slice(offsets, *arguments), - Self::offset_variable(offsets, *lambda_set), - Self::offset_variable(offsets, *result), - ), - FlatType::Record(record_fields, ext) => FlatType::Record( - Self::offset_record_fields(offsets, *record_fields), - Self::offset_variable(offsets, *ext), - ), - FlatType::TagUnion(union_tags, ext) => FlatType::TagUnion( - Self::offset_tag_union(offsets, *union_tags), - Self::offset_variable(offsets, *ext), - ), - FlatType::FunctionOrTagUnion(tag_name, symbol, ext) => FlatType::FunctionOrTagUnion( - Self::offset_tag_name_index(offsets, *tag_name), - *symbol, - Self::offset_variable(offsets, *ext), - ), - FlatType::RecursiveTagUnion(rec, union_tags, ext) => FlatType::RecursiveTagUnion( - Self::offset_variable(offsets, *rec), - Self::offset_tag_union(offsets, *union_tags), - Self::offset_variable(offsets, *ext), - ), - FlatType::Erroneous(problem) => { - FlatType::Erroneous(Self::offset_problem(offsets, *problem)) - } - FlatType::EmptyRecord => FlatType::EmptyRecord, - FlatType::EmptyTagUnion => FlatType::EmptyTagUnion, - } - } - - fn offset_content(offsets: &StorageSubsOffsets, content: &Content) -> Content { - use Content::*; - - match content { - FlexVar(opt_name) => FlexVar(*opt_name), - RigidVar(name) => RigidVar(*name), - FlexAbleVar(opt_name, ability) => FlexAbleVar(*opt_name, *ability), - RigidAbleVar(name, ability) => RigidAbleVar(*name, *ability), - RecursionVar { - structure, - opt_name, - } => RecursionVar { - structure: Self::offset_variable(offsets, *structure), - opt_name: *opt_name, - }, - Structure(flat_type) => Structure(Self::offset_flat_type(offsets, flat_type)), - Alias(symbol, alias_variables, actual, kind) => Alias( - *symbol, - Self::offset_alias_variables(offsets, *alias_variables), - Self::offset_variable(offsets, *actual), - *kind, - ), - LambdaSet(self::LambdaSet { - solved, - recursion_var, - unspecialized, - }) => LambdaSet(self::LambdaSet { - solved: Self::offset_lambda_set(offsets, *solved), - recursion_var: recursion_var.map(|v| Self::offset_variable(offsets, v)), - unspecialized: Self::offset_uls_slice(offsets, *unspecialized), - }), - RangedNumber(typ, range) => RangedNumber(Self::offset_variable(offsets, *typ), *range), - Error => Content::Error, - } - } - - fn offset_alias_variables( - offsets: &StorageSubsOffsets, - mut alias_variables: AliasVariables, - ) -> AliasVariables { - alias_variables.variables_start += offsets.variables; - - alias_variables - } - - fn offset_tag_union(offsets: &StorageSubsOffsets, mut union_tags: UnionTags) -> UnionTags { - union_tags.labels_start += offsets.tag_names; - union_tags.variables_start += offsets.variable_slices; - - union_tags - } - - fn offset_lambda_set( - offsets: &StorageSubsOffsets, - mut union_lambdas: UnionLambdas, - ) -> UnionLambdas { - union_lambdas.labels_start += offsets.closure_names; - union_lambdas.variables_start += offsets.variable_slices; - - union_lambdas - } - - fn offset_record_fields( - offsets: &StorageSubsOffsets, - mut record_fields: RecordFields, - ) -> RecordFields { - record_fields.field_names_start += offsets.field_names; - record_fields.variables_start += offsets.variables; - record_fields.field_types_start += offsets.record_fields; - - record_fields - } - - fn offset_tag_name_index( - offsets: &StorageSubsOffsets, - mut tag_name: SubsIndex, - ) -> SubsIndex { - tag_name.index += offsets.tag_names; - - tag_name - } - - fn offset_variable(offsets: &StorageSubsOffsets, variable: Variable) -> Variable { - if variable.index() < Variable::FIRST_USER_SPACE_VAR.index() { - variable - } else { - let new_index = variable.0 + offsets.utable; - Variable(new_index) - } - } - - fn offset_variable_slice( - offsets: &StorageSubsOffsets, - mut slice: VariableSubsSlice, - ) -> VariableSubsSlice { - slice.start += offsets.variables; - - slice - } - - fn offset_uls_slice(offsets: &StorageSubsOffsets, mut slice: SubsSlice) -> SubsSlice { - slice.start += offsets.unspecialized_lambda_sets; - - slice - } - - fn offset_problem( - offsets: &StorageSubsOffsets, - mut problem_index: SubsIndex, - ) -> SubsIndex { - problem_index.index += offsets.problems; - - problem_index - } -} - -use std::cell::RefCell; -std::thread_local! { - /// Scratchpad arena so we don't need to allocate a new one all the time - static SCRATCHPAD: RefCell> = RefCell::new(Some(bumpalo::Bump::with_capacity(4 * 1024))); -} - -fn take_scratchpad() -> bumpalo::Bump { - SCRATCHPAD.with(|f| f.take().unwrap()) -} - -fn put_scratchpad(scratchpad: bumpalo::Bump) { - SCRATCHPAD.with(|f| { - f.replace(Some(scratchpad)); - }); -} - -pub fn storage_copy_var_to( - source: &mut Subs, // mut to set the copy - target: &mut Subs, - var: Variable, -) -> Variable { - let rank = Rank::toplevel(); - - let mut arena = take_scratchpad(); - - let copy = { - let visited = bumpalo::collections::Vec::with_capacity_in(256, &arena); - - let mut env = StorageCopyVarToEnv { - visited, - source, - target, - max_rank: rank, - }; - - let copy = storage_copy_var_to_help(&mut env, var); - - // we have tracked all visited variables, and can now traverse them - // in one go (without looking at the UnificationTable) and clear the copy field - for var in env.visited { - env.source.modify(var, |descriptor| { - if descriptor.copy.is_some() { - descriptor.rank = Rank::NONE; - descriptor.mark = Mark::NONE; - descriptor.copy = OptVariable::NONE; - } - }); - } - - copy - }; - - arena.reset(); - put_scratchpad(arena); - - copy -} - -struct StorageCopyVarToEnv<'a> { - visited: bumpalo::collections::Vec<'a, Variable>, - source: &'a mut Subs, - target: &'a mut Subs, - max_rank: Rank, -} - -#[inline(always)] -fn storage_copy_union( - env: &mut StorageCopyVarToEnv<'_>, - tags: UnionLabels, -) -> UnionLabels { - let new_variable_slices = SubsSlice::reserve_variable_slices(env.target, tags.len()); - - let it = (new_variable_slices.indices()).zip(tags.variables()); - for (target_index, index) in it { - let slice = env.source[index]; - - let new_variables = SubsSlice::reserve_into_subs(env.target, slice.len()); - let it = (new_variables.indices()).zip(slice); - for (target_index, var_index) in it { - let var = env.source[var_index]; - let copy_var = storage_copy_var_to_help(env, var); - env.target.variables[target_index] = copy_var; - } - - env.target.variable_slices[target_index] = new_variables; - } - - let new_tag_names = { - let tag_names = tags.labels(); - let slice = L::get_subs_slice(env.source, tag_names); - - L::extend_new(env.target, slice.iter().cloned()) - }; - - UnionLabels::from_slices(new_tag_names, new_variable_slices) -} - -fn storage_copy_var_to_help(env: &mut StorageCopyVarToEnv<'_>, var: Variable) -> Variable { - use Content::*; - use FlatType::*; - - let desc = env.source.get_without_compacting(var); - - if let Some(copy) = desc.copy.into_variable() { - debug_assert!(env.target.contains(copy)); - return copy; - } else if desc.rank != Rank::NONE { - // DO NOTHING, Fall through - // - // The original deep_copy_var can do - // return var; - // - // but we cannot, because this `var` is in the source, not the target, and we - // should only return variables in the target. so, we have to create a new - // variable in the target. - } - - env.visited.push(var); - - let max_rank = env.max_rank; - - let make_descriptor = |content| Descriptor { - content, - rank: max_rank, - mark: Mark::NONE, - copy: OptVariable::NONE, - }; - - let copy = env.target.fresh_unnamed_flex_var(); - - // Link the original variable to the new variable. This lets us - // avoid making multiple copies of the variable we are instantiating. - // - // Need to do this before recursively copying to avoid looping. - env.source.modify(var, |descriptor| { - descriptor.mark = Mark::NONE; - descriptor.copy = copy.into(); - }); - - // Now we recursively copy the content of the variable. - // We have already marked the variable as copied, so we - // will not repeat this work or crawl this variable again. - match desc.content { - Structure(flat_type) => { - let new_flat_type = match flat_type { - Apply(symbol, arguments) => { - let new_arguments = SubsSlice::reserve_into_subs(env.target, arguments.len()); - - for (target_index, var_index) in (new_arguments.indices()).zip(arguments) { - let var = env.source[var_index]; - let copy_var = storage_copy_var_to_help(env, var); - env.target.variables[target_index] = copy_var; - } - - Apply(symbol, new_arguments) - } - - Func(arguments, closure_var, ret_var) => { - let new_ret_var = storage_copy_var_to_help(env, ret_var); - - let new_closure_var = storage_copy_var_to_help(env, closure_var); - - let new_arguments = SubsSlice::reserve_into_subs(env.target, arguments.len()); - - for (target_index, var_index) in (new_arguments.indices()).zip(arguments) { - let var = env.source[var_index]; - let copy_var = storage_copy_var_to_help(env, var); - env.target.variables[target_index] = copy_var; - } - - Func(new_arguments, new_closure_var, new_ret_var) - } - - same @ EmptyRecord | same @ EmptyTagUnion | same @ Erroneous(_) => same, - - Record(fields, ext_var) => { - let record_fields = { - let new_variables = - VariableSubsSlice::reserve_into_subs(env.target, fields.len()); - - let it = (new_variables.indices()).zip(fields.iter_variables()); - for (target_index, var_index) in it { - let var = env.source[var_index]; - let copy_var = storage_copy_var_to_help(env, var); - env.target.variables[target_index] = copy_var; - } - - let field_names_start = env.target.field_names.len() as u32; - let field_types_start = env.target.record_fields.len() as u32; - - let field_names = &env.source.field_names[fields.field_names().indices()]; - env.target.field_names.extend(field_names.iter().cloned()); - - let record_fields = - &env.source.record_fields[fields.record_fields().indices()]; - env.target - .record_fields - .extend(record_fields.iter().copied()); - - RecordFields { - length: fields.len() as _, - field_names_start, - variables_start: new_variables.start, - field_types_start, - } - }; - - Record(record_fields, storage_copy_var_to_help(env, ext_var)) - } - - TagUnion(tags, ext_var) => { - let new_ext = storage_copy_var_to_help(env, ext_var); - let union_tags = storage_copy_union(env, tags); - - TagUnion(union_tags, new_ext) - } - - FunctionOrTagUnion(tag_name, symbol, ext_var) => { - let new_tag_name = SubsIndex::new(env.target.tag_names.len() as u32); - - env.target.tag_names.push(env.source[tag_name].clone()); - - FunctionOrTagUnion(new_tag_name, symbol, storage_copy_var_to_help(env, ext_var)) - } - - RecursiveTagUnion(rec_var, tags, ext_var) => { - let union_tags = storage_copy_union(env, tags); - - let new_ext = storage_copy_var_to_help(env, ext_var); - let new_rec_var = storage_copy_var_to_help(env, rec_var); - - RecursiveTagUnion(new_rec_var, union_tags, new_ext) - } - }; - - env.target - .set(copy, make_descriptor(Structure(new_flat_type))); - - copy - } - - FlexVar(Some(name_index)) => { - let name = env.source.field_names[name_index.index as usize].clone(); - let new_name_index = SubsIndex::push_new(&mut env.target.field_names, name); - - let content = FlexVar(Some(new_name_index)); - env.target.set_content(copy, content); - - copy - } - - FlexVar(None) | Error => copy, - - RecursionVar { - opt_name, - structure, - } => { - let new_structure = storage_copy_var_to_help(env, structure); - - debug_assert!((new_structure.index() as usize) < env.target.len()); - - env.target.set( - copy, - make_descriptor(RecursionVar { - opt_name, - structure: new_structure, - }), - ); - - copy - } - - RigidVar(name_index) => { - let name = env.source.field_names[name_index.index as usize].clone(); - let new_name_index = SubsIndex::push_new(&mut env.target.field_names, name); - env.target - .set(copy, make_descriptor(FlexVar(Some(new_name_index)))); - - copy - } - - FlexAbleVar(opt_name_index, ability) => { - let new_name_index = opt_name_index.map(|name_index| { - let name = env.source.field_names[name_index.index as usize].clone(); - SubsIndex::push_new(&mut env.target.field_names, name) - }); - - let content = FlexAbleVar(new_name_index, ability); - env.target.set_content(copy, content); - - copy - } - - RigidAbleVar(name_index, ability) => { - let name = env.source.field_names[name_index.index as usize].clone(); - let new_name_index = SubsIndex::push_new(&mut env.target.field_names, name); - env.target.set( - copy, - make_descriptor(FlexAbleVar(Some(new_name_index), ability)), - ); - - copy - } - - Alias(symbol, arguments, real_type_var, kind) => { - let new_variables = - SubsSlice::reserve_into_subs(env.target, arguments.all_variables_len as _); - for (target_index, var_index) in - (new_variables.indices()).zip(arguments.all_variables()) - { - let var = env.source[var_index]; - let copy_var = storage_copy_var_to_help(env, var); - env.target.variables[target_index] = copy_var; - } - - let new_arguments = AliasVariables { - variables_start: new_variables.start, - ..arguments - }; - - let new_real_type_var = storage_copy_var_to_help(env, real_type_var); - let new_content = Alias(symbol, new_arguments, new_real_type_var, kind); - - env.target.set(copy, make_descriptor(new_content)); - - copy - } - - LambdaSet(self::LambdaSet { - solved, - recursion_var, - unspecialized, - }) => { - let new_solved = storage_copy_union(env, solved); - let new_rec_var = recursion_var.map(|v| storage_copy_var_to_help(env, v)); - - // NB: we are only copying into storage here, not instantiating like in solve::deep_copy_var. - // So no bookkeeping should be done for the new unspecialized lambda sets. - let new_unspecialized = SubsSlice::reserve_uls_slice(env.target, unspecialized.len()); - for (target_index, source_index) in - (new_unspecialized.into_iter()).zip(unspecialized.into_iter()) - { - let Uls(var, sym, region) = env.source[source_index]; - let new_var = storage_copy_var_to_help(env, var); - env.target[target_index] = Uls(new_var, sym, region); - } - - let new_content = LambdaSet(self::LambdaSet { - solved: new_solved, - recursion_var: new_rec_var, - unspecialized: new_unspecialized, - }); - env.target.set(copy, make_descriptor(new_content)); - copy - } - - RangedNumber(typ, range) => { - let new_typ = storage_copy_var_to_help(env, typ); - - let new_content = RangedNumber(new_typ, range); - - env.target.set(copy, make_descriptor(new_content)); - copy - } - } -} - -/// Bookkeeping to correctly move these types into the target subs -/// -/// We track the rigid/flex variables because they need to be part of a `Let` -/// constraint, introducing these variables at the right rank -/// -/// We also track `registered` variables. An import should be equivalent to -/// a call to `type_to_var` (solve.rs). The `copy_import_to` function puts -/// the right `Contents` into the target `Subs` at the right locations, -/// but `type_to_var` furthermore adds the variables used to store those `Content`s -/// to `Pools` at the right rank. Here we remember the variables used to store `Content`s -/// so that we can later add them to `Pools` -#[derive(Debug)] -pub struct CopiedImport { - pub variable: Variable, - pub flex: Vec, - pub rigid: Vec, - pub flex_able: Vec, - pub rigid_able: Vec, - pub translations: Vec<(Variable, Variable)>, - pub registered: Vec, -} - -struct CopyImportEnv<'a> { - visited: bumpalo::collections::Vec<'a, Variable>, - source: &'a mut Subs, - target: &'a mut Subs, - flex: Vec, - rigid: Vec, - flex_able: Vec, - rigid_able: Vec, - translations: Vec<(Variable, Variable)>, - registered: Vec, -} - -pub fn copy_import_to( - source: &mut Subs, // mut to set the copy - target: &mut Subs, - var: Variable, - rank: Rank, -) -> CopiedImport { - let mut arena = take_scratchpad(); - - let copied_import = { - let visited = bumpalo::collections::Vec::with_capacity_in(256, &arena); - - let mut env = CopyImportEnv { - visited, - source, - target, - flex: Vec::new(), - rigid: Vec::new(), - flex_able: Vec::new(), - rigid_able: Vec::new(), - translations: Vec::new(), - registered: Vec::new(), - }; - - let copy = copy_import_to_help(&mut env, rank, var); - - let CopyImportEnv { - visited, - source, - flex, - rigid, - flex_able, - rigid_able, - translations, - registered, - target: _, - } = env; - - // we have tracked all visited variables, and can now traverse them - // in one go (without looking at the UnificationTable) and clear the copy field - - for var in visited { - source.modify(var, |descriptor| { - if descriptor.copy.is_some() { - descriptor.rank = Rank::NONE; - descriptor.mark = Mark::NONE; - descriptor.copy = OptVariable::NONE; - } - }); - } - - CopiedImport { - variable: copy, - flex, - rigid, - flex_able, - rigid_able, - translations, - registered, - } - }; - - arena.reset(); - put_scratchpad(arena); - - copied_import -} - -/// is this content registered (in the current pool) by type_to_variable? -/// TypeToVar skips registering for flex and rigid variables, and -/// also for the empty records and tag unions (they used the Variable::EMPTY_RECORD/...) -/// standard variables -fn is_registered(content: &Content) -> bool { - match content { - Content::FlexVar(_) - | Content::RigidVar(_) - | Content::FlexAbleVar(..) - | Content::RigidAbleVar(..) => false, - Content::Structure(FlatType::EmptyRecord | FlatType::EmptyTagUnion) => false, - - Content::Structure(_) - | Content::RecursionVar { .. } - | Content::Alias(_, _, _, _) - | Content::RangedNumber(_, _) - | Content::Error - | Content::LambdaSet(_) => true, - } -} - -#[inline(always)] -fn copy_union( - env: &mut CopyImportEnv<'_>, - max_rank: Rank, - tags: UnionLabels, -) -> UnionLabels { - let new_variable_slices = SubsSlice::reserve_variable_slices(env.target, tags.len()); - - let it = (new_variable_slices.indices()).zip(tags.variables()); - for (target_index, index) in it { - let slice = env.source[index]; - - let new_variables = SubsSlice::reserve_into_subs(env.target, slice.len()); - let it = (new_variables.indices()).zip(slice); - for (target_index, var_index) in it { - let var = env.source[var_index]; - let copy_var = copy_import_to_help(env, max_rank, var); - env.target.variables[target_index] = copy_var; - } - - env.target.variable_slices[target_index] = new_variables; - } - - let new_tag_names = { - let tag_names = tags.labels(); - let slice = L::get_subs_slice(env.source, tag_names); - - L::extend_new(env.target, slice.iter().cloned()) - }; - - UnionLabels::from_slices(new_tag_names, new_variable_slices) -} - -fn copy_import_to_help(env: &mut CopyImportEnv<'_>, max_rank: Rank, var: Variable) -> Variable { - use Content::*; - use FlatType::*; - - let desc = env.source.get_without_compacting(var); - - if let Some(copy) = desc.copy.into_variable() { - debug_assert!(env.target.contains(copy)); - return copy; - } else if desc.rank != Rank::NONE { - // DO NOTHING, Fall through - // - // The original copy_import can do - // return var; - // - // but we cannot, because this `var` is in the source, not the target, and we - // should only return variables in the target. so, we have to create a new - // variable in the target. - } - - env.visited.push(var); - - let make_descriptor = |content| Descriptor { - content, - rank: max_rank, - mark: Mark::NONE, - copy: OptVariable::NONE, - }; - - // let copy = env.target.fresh_unnamed_flex_var(); - let copy = env.target.fresh(make_descriptor(unnamed_flex_var())); - - // is this content registered (in the current pool) by type_to_variable? - if is_registered(&desc.content) { - env.registered.push(copy); - } - - // Link the original variable to the new variable. This lets us - // avoid making multiple copies of the variable we are instantiating. - // - // Need to do this before recursively copying to avoid looping. - env.source.modify(var, |descriptor| { - descriptor.mark = Mark::NONE; - descriptor.copy = copy.into(); - }); - - // Now we recursively copy the content of the variable. - // We have already marked the variable as copied, so we - // will not repeat this work or crawl this variable again. - match desc.content { - Structure(Erroneous(_)) => { - // Make this into a flex var so that we don't have to copy problems across module - // boundaries - the error will be reported locally. - env.target.set(copy, make_descriptor(FlexVar(None))); - - copy - } - Structure(flat_type) => { - let new_flat_type = match flat_type { - Apply(symbol, arguments) => { - let new_arguments = SubsSlice::reserve_into_subs(env.target, arguments.len()); - - for (target_index, var_index) in (new_arguments.indices()).zip(arguments) { - let var = env.source[var_index]; - let copy_var = copy_import_to_help(env, max_rank, var); - env.target.variables[target_index] = copy_var; - } - - Apply(symbol, new_arguments) - } - - Func(arguments, closure_var, ret_var) => { - let new_ret_var = copy_import_to_help(env, max_rank, ret_var); - - let new_closure_var = copy_import_to_help(env, max_rank, closure_var); - - let new_arguments = SubsSlice::reserve_into_subs(env.target, arguments.len()); - - for (target_index, var_index) in (new_arguments.indices()).zip(arguments) { - let var = env.source[var_index]; - let copy_var = copy_import_to_help(env, max_rank, var); - env.target.variables[target_index] = copy_var; - } - - Func(new_arguments, new_closure_var, new_ret_var) - } - - Erroneous(_) => internal_error!("I thought this was handled above"), - - same @ EmptyRecord | same @ EmptyTagUnion => same, - - Record(fields, ext_var) => { - let record_fields = { - let new_variables = - VariableSubsSlice::reserve_into_subs(env.target, fields.len()); - - let it = (new_variables.indices()).zip(fields.iter_variables()); - for (target_index, var_index) in it { - let var = env.source[var_index]; - let copy_var = copy_import_to_help(env, max_rank, var); - env.target.variables[target_index] = copy_var; - } - - let field_names_start = env.target.field_names.len() as u32; - let field_types_start = env.target.record_fields.len() as u32; - - let field_names = &env.source.field_names[fields.field_names().indices()]; - env.target.field_names.extend(field_names.iter().cloned()); - - let record_fields = - &env.source.record_fields[fields.record_fields().indices()]; - env.target - .record_fields - .extend(record_fields.iter().copied()); - - RecordFields { - length: fields.len() as _, - field_names_start, - variables_start: new_variables.start, - field_types_start, - } - }; - - Record(record_fields, copy_import_to_help(env, max_rank, ext_var)) - } - - TagUnion(tags, ext_var) => { - let new_ext = copy_import_to_help(env, max_rank, ext_var); - - let union_tags = copy_union(env, max_rank, tags); - - TagUnion(union_tags, new_ext) - } - - FunctionOrTagUnion(tag_name, symbol, ext_var) => { - let new_tag_name = SubsIndex::new(env.target.tag_names.len() as u32); - - env.target.tag_names.push(env.source[tag_name].clone()); - - FunctionOrTagUnion( - new_tag_name, - symbol, - copy_import_to_help(env, max_rank, ext_var), - ) - } - - RecursiveTagUnion(rec_var, tags, ext_var) => { - let union_tags = copy_union(env, max_rank, tags); - - let new_ext = copy_import_to_help(env, max_rank, ext_var); - let new_rec_var = copy_import_to_help(env, max_rank, rec_var); - - RecursiveTagUnion(new_rec_var, union_tags, new_ext) - } - }; - - env.target - .set(copy, make_descriptor(Structure(new_flat_type))); - - copy - } - - FlexVar(opt_name_index) => { - if let Some(name_index) = opt_name_index { - let name = env.source.field_names[name_index.index as usize].clone(); - let new_name_index = SubsIndex::push_new(&mut env.target.field_names, name); - - let content = FlexVar(Some(new_name_index)); - env.target.set_content(copy, content); - } - - env.flex.push(copy); - - copy - } - - FlexAbleVar(opt_name_index, ability) => { - if let Some(name_index) = opt_name_index { - let name = env.source.field_names[name_index.index as usize].clone(); - let new_name_index = SubsIndex::push_new(&mut env.target.field_names, name); - - let content = FlexAbleVar(Some(new_name_index), ability); - env.target.set_content(copy, content); - } - - env.flex_able.push(copy); - - copy - } - - Error => { - // Open question: should this return Error, or a Flex var? - - env.target.set(copy, make_descriptor(Error)); - - copy - } - - RigidVar(name_index) => { - let name = env.source.field_names[name_index.index as usize].clone(); - let new_name_index = SubsIndex::push_new(&mut env.target.field_names, name); - - env.target - .set(copy, make_descriptor(RigidVar(new_name_index))); - - env.rigid.push(copy); - - env.translations.push((var, copy)); - - copy - } - - RigidAbleVar(name_index, ability) => { - let name = env.source.field_names[name_index.index as usize].clone(); - let new_name_index = SubsIndex::push_new(&mut env.target.field_names, name); - - env.target - .set(copy, make_descriptor(RigidAbleVar(new_name_index, ability))); - - env.rigid_able.push(copy); - - env.translations.push((var, copy)); - - copy - } - - RecursionVar { - opt_name, - structure, - } => { - let new_structure = copy_import_to_help(env, max_rank, structure); - - debug_assert!((new_structure.index() as usize) < env.target.len()); - - env.target.set( - copy, - make_descriptor(RecursionVar { - opt_name, - structure: new_structure, - }), - ); - - copy - } - - Alias(symbol, arguments, real_type_var, kind) => { - let new_variables = - SubsSlice::reserve_into_subs(env.target, arguments.all_variables_len as _); - for (target_index, var_index) in - (new_variables.indices()).zip(arguments.all_variables()) - { - let var = env.source[var_index]; - let copy_var = copy_import_to_help(env, max_rank, var); - env.target.variables[target_index] = copy_var; - } - - let new_arguments = AliasVariables { - variables_start: new_variables.start, - ..arguments - }; - - let new_real_type_var = copy_import_to_help(env, max_rank, real_type_var); - let new_content = Alias(symbol, new_arguments, new_real_type_var, kind); - - env.target.set(copy, make_descriptor(new_content)); - - copy - } - - LambdaSet(self::LambdaSet { - solved, - recursion_var, - unspecialized, - }) => { - let new_solved = copy_union(env, max_rank, solved); - let new_rec_var = - recursion_var.map(|rec_var| copy_import_to_help(env, max_rank, rec_var)); - - // NB: we are only copying across subs here, not instantiating like in deep_copy_var. - // So no bookkeeping should be done for the new unspecialized lambda sets. - let new_unspecialized = SubsSlice::reserve_uls_slice(env.target, unspecialized.len()); - for (target_index, source_index) in - (new_unspecialized.into_iter()).zip(unspecialized.into_iter()) - { - let Uls(var, sym, region) = env.source[source_index]; - let new_var = copy_import_to_help(env, max_rank, var); - env.target[target_index] = Uls(new_var, sym, region); - } - - let new_content = LambdaSet(self::LambdaSet { - solved: new_solved, - recursion_var: new_rec_var, - unspecialized: new_unspecialized, - }); - - env.target.set(copy, make_descriptor(new_content)); - - copy - } - - RangedNumber(typ, range) => { - let new_typ = copy_import_to_help(env, max_rank, typ); - - let new_content = RangedNumber(new_typ, range); - - env.target.set(copy, make_descriptor(new_content)); - copy - } - } -} diff --git a/compiler/types/src/types.rs b/compiler/types/src/types.rs deleted file mode 100644 index eac1f69870..0000000000 --- a/compiler/types/src/types.rs +++ /dev/null @@ -1,2906 +0,0 @@ -use crate::num::NumericRange; -use crate::pretty_print::Parens; -use crate::subs::{ - GetSubsSlice, RecordFields, Subs, UnionTags, VarStore, Variable, VariableSubsSlice, -}; -use roc_collections::all::{HumanIndex, ImMap, ImSet, MutMap, MutSet, SendMap}; -use roc_error_macros::internal_error; -use roc_module::called_via::CalledVia; -use roc_module::ident::{ForeignSymbol, Ident, Lowercase, TagName}; -use roc_module::low_level::LowLevel; -use roc_module::symbol::{Interns, ModuleId, Symbol}; -use roc_region::all::{Loc, Region}; -use std::fmt; - -pub const TYPE_NUM: &str = "Num"; -pub const TYPE_INTEGER: &str = "Integer"; -pub const TYPE_FLOATINGPOINT: &str = "FloatingPoint"; - -const GREEK_LETTERS: &[char] = &[ - 'α', 'ν', 'β', 'ξ', 'γ', 'ο', 'δ', 'π', 'ε', 'ρ', 'ζ', 'σ', 'η', 'τ', 'θ', 'υ', 'ι', 'φ', 'κ', - 'χ', 'λ', 'ψ', 'μ', 'ω', 'ς', -]; - -/// -/// Intuitively -/// -/// - Demanded: only introduced by pattern matches, e.g. { x } -> -/// Cannot unify with an Optional field, but can unify with a Required field -/// - Required: introduced by record literals and type annotations. -/// Can unify with Optional and Demanded -/// - Optional: introduced by pattern matches and annotations. -/// Can unify with Required, but not with Demanded -#[derive(PartialEq, Eq, Clone, Hash)] -pub enum RecordField { - Optional(T), - Required(T), - Demanded(T), -} - -impl Copy for RecordField {} - -impl fmt::Debug for RecordField { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - use RecordField::*; - - match self { - Optional(typ) => write!(f, "Optional({:?})", typ), - Required(typ) => write!(f, "Required({:?})", typ), - Demanded(typ) => write!(f, "Demanded({:?})", typ), - } - } -} - -impl RecordField { - pub fn into_inner(self) -> T { - use RecordField::*; - - match self { - Optional(t) => t, - Required(t) => t, - Demanded(t) => t, - } - } - - pub fn as_inner(&self) -> &T { - use RecordField::*; - - match self { - Optional(t) => t, - Required(t) => t, - Demanded(t) => t, - } - } - - pub fn as_inner_mut(&mut self) -> &mut T { - use RecordField::*; - - match self { - Optional(t) => t, - Required(t) => t, - Demanded(t) => t, - } - } - - pub fn map(&self, mut f: F) -> RecordField - where - F: FnMut(&T) -> U, - { - use RecordField::*; - match self { - Optional(t) => Optional(f(t)), - Required(t) => Required(f(t)), - Demanded(t) => Demanded(f(t)), - } - } -} - -impl RecordField { - pub fn substitute(&mut self, substitutions: &ImMap) { - use RecordField::*; - - match self { - Optional(typ) => typ.substitute(substitutions), - Required(typ) => typ.substitute(substitutions), - Demanded(typ) => typ.substitute(substitutions), - } - } - - pub fn substitute_alias( - &mut self, - rep_symbol: Symbol, - rep_args: &[Type], - actual: &Type, - ) -> Result<(), Region> { - use RecordField::*; - - match self { - Optional(typ) => typ.substitute_alias(rep_symbol, rep_args, actual), - Required(typ) => typ.substitute_alias(rep_symbol, rep_args, actual), - Demanded(typ) => typ.substitute_alias(rep_symbol, rep_args, actual), - } - } - - pub fn instantiate_aliases<'a, F>( - &mut self, - region: Region, - aliases: &'a F, - var_store: &mut VarStore, - introduced: &mut ImSet, - ) where - F: Fn(Symbol) -> Option<&'a Alias>, - { - use RecordField::*; - - match self { - Optional(typ) => typ.instantiate_aliases(region, aliases, var_store, introduced), - Required(typ) => typ.instantiate_aliases(region, aliases, var_store, introduced), - Demanded(typ) => typ.instantiate_aliases(region, aliases, var_store, introduced), - } - } - - pub fn contains_symbol(&self, rep_symbol: Symbol) -> bool { - use RecordField::*; - - match self { - Optional(typ) => typ.contains_symbol(rep_symbol), - Required(typ) => typ.contains_symbol(rep_symbol), - Demanded(typ) => typ.contains_symbol(rep_symbol), - } - } - pub fn contains_variable(&self, rep_variable: Variable) -> bool { - use RecordField::*; - - match self { - Optional(typ) => typ.contains_variable(rep_variable), - Required(typ) => typ.contains_variable(rep_variable), - Demanded(typ) => typ.contains_variable(rep_variable), - } - } -} - -#[derive(Debug, PartialEq, Eq, Clone)] -pub struct LambdaSet(pub Type); - -impl LambdaSet { - pub fn as_inner(&self) -> &Type { - &self.0 - } - - fn as_inner_mut(&mut self) -> &mut Type { - &mut self.0 - } - - fn instantiate_aliases<'a, F>( - &mut self, - region: Region, - aliases: &'a F, - var_store: &mut VarStore, - introduced: &mut ImSet, - ) where - F: Fn(Symbol) -> Option<&'a Alias>, - { - self.0 - .instantiate_aliases(region, aliases, var_store, introduced) - } -} - -#[derive(PartialEq, Eq, Clone)] -pub struct AliasCommon { - pub symbol: Symbol, - pub type_arguments: Vec, - pub lambda_set_variables: Vec, -} - -#[derive(Clone, Copy, Debug)] -pub struct OptAbleVar { - pub var: Variable, - pub opt_ability: Option, -} - -impl OptAbleVar { - pub fn unbound(var: Variable) -> Self { - Self { - var, - opt_ability: None, - } - } -} - -#[derive(PartialEq, Eq, Debug)] -pub struct OptAbleType { - pub typ: Type, - pub opt_ability: Option, -} - -impl OptAbleType { - pub fn unbound(typ: Type) -> Self { - Self { - typ, - opt_ability: None, - } - } -} - -#[derive(PartialEq, Eq)] -pub enum Type { - EmptyRec, - EmptyTagUnion, - /// A function. The types of its arguments, size of its closure, then the type of its return value. - Function(Vec, Box, Box), - Record(SendMap>, TypeExtension), - TagUnion(Vec<(TagName, Vec)>, TypeExtension), - FunctionOrTagUnion(TagName, Symbol, TypeExtension), - /// A function name that is used in our defunctionalization algorithm. For example in - /// g = \a -> - /// f = \{} -> a - /// f - /// the closure under "f" has name "f" and captures "a". - ClosureTag { - name: Symbol, - captures: Vec, - }, - UnspecializedLambdaSet(Uls), - DelayedAlias(AliasCommon), - Alias { - symbol: Symbol, - type_arguments: Vec, - lambda_set_variables: Vec, - actual: Box, - kind: AliasKind, - }, - HostExposedAlias { - name: Symbol, - type_arguments: Vec, - lambda_set_variables: Vec, - actual_var: Variable, - actual: Box, - }, - RecursiveTagUnion(Variable, Vec<(TagName, Vec)>, TypeExtension), - /// Applying a type to some arguments (e.g. Dict.Dict String Int) - Apply(Symbol, Vec, Region), - Variable(Variable), - RangedNumber(Box, NumericRange), - /// A type error, which will code gen to a runtime error - Erroneous(Problem), -} - -/// A lambda set under an arrow in a ability member signature. For example, in -/// Default has default : {} -> a | a has Default -/// the unspecialized lambda set for the arrow "{} -> a" would be `a:default:1`. -/// -/// Lambda sets in member signatures are never known until those members are specialized at a -/// usage site. Unspecialized lambda sets aid us in recovering those lambda sets; when we -/// instantiate `a` with a proper type `T`, we'll know to resolve the lambda set by extracting -/// it at region "1" from the specialization of "default" for `T`. -#[derive(PartialEq, Eq, Clone, Copy, PartialOrd, Ord)] -pub struct Uls(pub Variable, pub Symbol, pub u8); - -impl std::fmt::Debug for Uls { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "Uls({:?}:{:?}:{:?})", self.0, self.1, self.2) - } -} - -static mut TYPE_CLONE_COUNT: std::sync::atomic::AtomicUsize = - std::sync::atomic::AtomicUsize::new(0); - -pub fn get_type_clone_count() -> usize { - if cfg!(debug_assertions) { - unsafe { TYPE_CLONE_COUNT.load(std::sync::atomic::Ordering::SeqCst) } - } else { - 0 - } -} - -impl Clone for Type { - fn clone(&self) -> Self { - #[cfg(debug_assertions)] - unsafe { - TYPE_CLONE_COUNT.fetch_add(1, std::sync::atomic::Ordering::SeqCst) - }; - - match self { - Self::EmptyRec => Self::EmptyRec, - Self::EmptyTagUnion => Self::EmptyTagUnion, - Self::Function(arg0, arg1, arg2) => { - Self::Function(arg0.clone(), arg1.clone(), arg2.clone()) - } - Self::Record(arg0, arg1) => Self::Record(arg0.clone(), arg1.clone()), - Self::TagUnion(arg0, arg1) => Self::TagUnion(arg0.clone(), arg1.clone()), - Self::FunctionOrTagUnion(arg0, arg1, arg2) => { - Self::FunctionOrTagUnion(arg0.clone(), *arg1, arg2.clone()) - } - Self::ClosureTag { name, captures } => Self::ClosureTag { - name: *name, - captures: captures.clone(), - }, - Self::UnspecializedLambdaSet(uls) => Self::UnspecializedLambdaSet(*uls), - Self::DelayedAlias(arg0) => Self::DelayedAlias(arg0.clone()), - Self::Alias { - symbol, - type_arguments, - lambda_set_variables, - actual, - kind, - } => Self::Alias { - symbol: *symbol, - type_arguments: type_arguments.clone(), - lambda_set_variables: lambda_set_variables.clone(), - actual: actual.clone(), - kind: *kind, - }, - Self::HostExposedAlias { - name, - type_arguments, - lambda_set_variables, - actual_var, - actual, - } => Self::HostExposedAlias { - name: *name, - type_arguments: type_arguments.clone(), - lambda_set_variables: lambda_set_variables.clone(), - actual_var: *actual_var, - actual: actual.clone(), - }, - Self::RecursiveTagUnion(arg0, arg1, arg2) => { - Self::RecursiveTagUnion(*arg0, arg1.clone(), arg2.clone()) - } - Self::Apply(arg0, arg1, arg2) => Self::Apply(*arg0, arg1.clone(), *arg2), - Self::Variable(arg0) => Self::Variable(*arg0), - Self::RangedNumber(arg0, arg1) => Self::RangedNumber(arg0.clone(), *arg1), - Self::Erroneous(arg0) => Self::Erroneous(arg0.clone()), - } - } -} - -impl Clone for OptAbleType { - fn clone(&self) -> Self { - // This passes through `Type`, so defer to that to bump the clone counter. - Self { - typ: self.typ.clone(), - opt_ability: self.opt_ability, - } - } -} - -#[derive(PartialEq, Eq, Clone)] -pub enum TypeExtension { - Open(Box), - Closed, -} - -impl TypeExtension { - #[inline(always)] - pub fn from_type(typ: Type) -> Self { - match typ { - Type::EmptyTagUnion | Type::EmptyRec => Self::Closed, - _ => Self::Open(Box::new(typ)), - } - } - - #[inline(always)] - pub fn is_closed(&self) -> bool { - match self { - TypeExtension::Open(_) => false, - TypeExtension::Closed => true, - } - } - - #[inline(always)] - fn iter_mut(&mut self) -> impl Iterator { - match self { - TypeExtension::Open(ext) => Some(ext.as_mut()).into_iter(), - TypeExtension::Closed => None.into_iter(), - } - } -} - -impl<'a> IntoIterator for &'a TypeExtension { - type Item = &'a Type; - - type IntoIter = std::option::IntoIter; - - fn into_iter(self) -> Self::IntoIter { - match self { - TypeExtension::Open(ext) => Some(ext.as_ref()).into_iter(), - TypeExtension::Closed => None.into_iter(), - } - } -} - -fn write_tags<'a>( - f: &mut fmt::Formatter, - tags: impl ExactSizeIterator)>, -) -> fmt::Result { - write!(f, "[")?; - - let mut it = tags.peekable(); - while let Some((label, arguments)) = it.next() { - write!(f, "{:?}", label)?; - - for argument in arguments { - write!(f, " {:?}", argument)?; - } - - if it.peek().is_some() { - write!(f, ", ")?; - } - } - - write!(f, "]") -} - -impl fmt::Debug for Type { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - Type::EmptyRec => write!(f, "{{}}"), - Type::EmptyTagUnion => write!(f, "[]"), - Type::Function(args, closure, ret) => { - write!(f, "Fn(")?; - - for (index, arg) in args.iter().enumerate() { - if index > 0 { - write!(f, ", ")?; - } - - write!(f, "{:?}", arg)?; - } - - write!(f, " |{:?}|", closure)?; - write!(f, " -> ")?; - - ret.fmt(f)?; - - write!(f, ")") - } - Type::Variable(var) => write!(f, "<{:?}>", var), - - Type::Apply(symbol, args, _) => { - write!(f, "({:?}", symbol)?; - - for arg in args { - write!(f, " {:?}", arg)?; - } - - write!(f, ")") - } - Type::Erroneous(problem) => { - write!(f, "Erroneous(")?; - - problem.fmt(f)?; - - write!(f, ")") - } - Type::DelayedAlias(AliasCommon { - symbol, - type_arguments, - lambda_set_variables, - }) => { - write!(f, "(DelayedAlias {:?}", symbol)?; - - for arg in type_arguments { - write!(f, " {:?}", arg)?; - } - - for (lambda_set, greek_letter) in - lambda_set_variables.iter().zip(GREEK_LETTERS.iter()) - { - write!(f, " {}@{:?}", greek_letter, lambda_set.0)?; - } - - write!(f, ")")?; - - Ok(()) - } - - Type::Alias { - symbol, - type_arguments, - lambda_set_variables, - actual: _actual, - .. - } => { - write!(f, "(Alias {:?}", symbol)?; - - for arg in type_arguments { - write!(f, " {:?}", &arg.typ)?; - if let Some(ab) = arg.opt_ability { - write!(f, ":{:?}", ab)?; - } - } - - for (lambda_set, greek_letter) in - lambda_set_variables.iter().zip(GREEK_LETTERS.iter()) - { - write!(f, " {}@{:?}", greek_letter, lambda_set.0)?; - } - - // Sometimes it's useful to see the expansion of the alias - write!(f, "[ but actually {:?} ]", _actual)?; - - write!(f, ")")?; - - Ok(()) - } - Type::HostExposedAlias { - name, - type_arguments: arguments, - .. - } => { - write!(f, "HostExposedAlias {:?}", name)?; - - for arg in arguments { - write!(f, " {:?}", arg)?; - } - - // Sometimes it's useful to see the expansion of the alias - // write!(f, "[ but actually {:?} ]", _actual)?; - - Ok(()) - } - Type::Record(fields, ext) => { - write!(f, "{{")?; - - if !fields.is_empty() { - write!(f, " ")?; - } - - let mut any_written_yet = false; - - for (label, field_type) in fields { - match field_type { - RecordField::Optional(_) => write!(f, "{:?} ? {:?}", label, field_type)?, - RecordField::Required(_) => write!(f, "{:?} : {:?}", label, field_type)?, - RecordField::Demanded(_) => write!(f, "{:?} : {:?}", label, field_type)?, - } - - if any_written_yet { - write!(f, ", ")?; - } else { - any_written_yet = true; - } - } - - if !fields.is_empty() { - write!(f, " ")?; - } - - write!(f, "}}")?; - - match ext { - TypeExtension::Closed => { - // This is a closed record. We're done! - Ok(()) - } - TypeExtension::Open(other) => { - // This is an open record, so print the variable - // right after the '}' - // - // e.g. the "*" at the end of `{ x: Int }*` - // or the "r" at the end of `{ x: Int }r` - other.fmt(f) - } - } - } - Type::TagUnion(tags, ext) => { - write_tags(f, tags.iter())?; - - match ext { - TypeExtension::Closed => { - // This is a closed variant. We're done! - Ok(()) - } - TypeExtension::Open(other) => { - // This is an open tag union, so print the variable - // right after the ']' - // - // e.g. the "*" at the end of `[Foo]*` - // or the "r" at the end of `[DivByZero]r` - other.fmt(f) - } - } - } - Type::FunctionOrTagUnion(tag_name, _, ext) => { - write!(f, "[")?; - write!(f, "{:?}", tag_name)?; - write!(f, "]")?; - - match ext { - TypeExtension::Closed => { - // This is a closed variant. We're done! - Ok(()) - } - TypeExtension::Open(other) => { - // This is an open tag union, so print the variable - // right after the ']' - // - // e.g. the "*" at the end of `[Foo]*` - // or the "r" at the end of `[DivByZero]r` - other.fmt(f) - } - } - } - Type::ClosureTag { name, captures } => { - write!(f, "ClosureTag(")?; - - write!(f, "{:?}, ", name)?; - for capture in captures { - write!(f, "{:?}, ", capture)?; - } - - write!(f, ")") - } - Type::RecursiveTagUnion(rec, tags, ext) => { - write_tags(f, tags.iter())?; - - match ext { - TypeExtension::Closed => { - // This is a closed variant. We're done! - Ok(()) - } - TypeExtension::Open(other) => { - // This is an open tag union, so print the variable - // right after the ']' - // - // e.g. the "*" at the end of `[Foo]*` - // or the "r" at the end of `[DivByZero]r` - other.fmt(f) - } - }?; - - write!(f, " as <{:?}>", rec) - } - Type::RangedNumber(typ, range_vars) => { - write!(f, "Ranged({:?}, {:?})", typ, range_vars) - } - Type::UnspecializedLambdaSet(uls) => { - write!(f, "{:?}", uls) - } - } - } -} - -impl Type { - pub fn arity(&self) -> usize { - if let Type::Function(args, _, _) = self { - args.len() - } else { - 0 - } - } - pub fn is_recursive(&self) -> bool { - matches!(self, Type::RecursiveTagUnion(_, _, _)) - } - - pub fn is_empty_tag_union(&self) -> bool { - matches!(self, Type::EmptyTagUnion) - } - - pub fn is_empty_record(&self) -> bool { - matches!(self, Type::EmptyRec) - } - - pub fn variables(&self) -> ImSet { - let mut result = ImSet::default(); - variables_help(self, &mut result); - - result - } - - pub fn variables_detail(&self) -> VariableDetail { - let mut result = Default::default(); - variables_help_detailed(self, &mut result); - - result - } - - pub fn substitute(&mut self, substitutions: &ImMap) { - use Type::*; - - let mut stack = vec![self]; - - while let Some(typ) = stack.pop() { - match typ { - Variable(v) => { - if let Some(replacement) = substitutions.get(v) { - *typ = replacement.clone(); - } - } - Function(args, closure, ret) => { - stack.extend(args); - stack.push(closure); - stack.push(ret); - } - ClosureTag { name: _, captures } => stack.extend(captures), - TagUnion(tags, ext) => { - for (_, args) in tags { - stack.extend(args.iter_mut()); - } - - if let TypeExtension::Open(ext) = ext { - stack.push(ext); - } - } - FunctionOrTagUnion(_, _, ext) => { - if let TypeExtension::Open(ext) = ext { - stack.push(ext); - } - } - RecursiveTagUnion(_, tags, ext) => { - for (_, args) in tags { - stack.extend(args.iter_mut()); - } - - if let TypeExtension::Open(ext) = ext { - stack.push(ext); - } - } - Record(fields, ext) => { - for (_, x) in fields.iter_mut() { - stack.push(x.as_inner_mut()); - } - - if let TypeExtension::Open(ext) = ext { - stack.push(ext); - } - } - Type::DelayedAlias(AliasCommon { - type_arguments, - lambda_set_variables, - .. - }) => { - for value in type_arguments.iter_mut() { - stack.push(value); - } - - for lambda_set in lambda_set_variables.iter_mut() { - stack.push(lambda_set.as_inner_mut()); - } - } - Alias { - type_arguments, - lambda_set_variables, - actual, - .. - } => { - for value in type_arguments.iter_mut() { - stack.push(&mut value.typ); - } - - for lambda_set in lambda_set_variables.iter_mut() { - stack.push(lambda_set.as_inner_mut()); - } - - stack.push(actual); - } - HostExposedAlias { - type_arguments, - lambda_set_variables, - actual: actual_type, - .. - } => { - for value in type_arguments.iter_mut() { - stack.push(value); - } - - for lambda_set in lambda_set_variables.iter_mut() { - stack.push(lambda_set.as_inner_mut()); - } - - stack.push(actual_type); - } - Apply(_, args, _) => { - stack.extend(args); - } - RangedNumber(typ, _) => { - stack.push(typ); - } - UnspecializedLambdaSet(Uls(v, _, _)) => { - debug_assert!( - substitutions.get(v).is_none(), - "unspecialized lambda sets should never be substituted before solving" - ); - } - - EmptyRec | EmptyTagUnion | Erroneous(_) => {} - } - } - } - - pub fn substitute_variables(&mut self, substitutions: &MutMap) { - use Type::*; - - let mut stack = vec![self]; - - while let Some(typ) = stack.pop() { - match typ { - Variable(v) => { - if let Some(replacement) = substitutions.get(v) { - *v = *replacement; - } - } - Function(args, closure, ret) => { - stack.extend(args); - stack.push(closure); - stack.push(ret); - } - ClosureTag { name: _, captures } => { - stack.extend(captures); - } - TagUnion(tags, ext) => { - for (_, args) in tags { - stack.extend(args.iter_mut()); - } - - if let TypeExtension::Open(ext) = ext { - stack.push(ext); - } - } - FunctionOrTagUnion(_, _, ext) => { - if let TypeExtension::Open(ext) = ext { - stack.push(ext); - } - } - RecursiveTagUnion(rec_var, tags, ext) => { - if let Some(replacement) = substitutions.get(rec_var) { - *rec_var = *replacement; - } - - for (_, args) in tags { - stack.extend(args.iter_mut()); - } - - if let TypeExtension::Open(ext) = ext { - stack.push(ext); - } - } - Record(fields, ext) => { - for (_, x) in fields.iter_mut() { - stack.push(x.as_inner_mut()); - } - if let TypeExtension::Open(ext) = ext { - stack.push(ext); - } - } - Type::DelayedAlias(AliasCommon { - type_arguments, - lambda_set_variables, - .. - }) => { - for value in type_arguments.iter_mut() { - stack.push(value); - } - - for lambda_set in lambda_set_variables.iter_mut() { - stack.push(lambda_set.as_inner_mut()); - } - } - Alias { - type_arguments, - lambda_set_variables, - actual, - .. - } => { - for value in type_arguments.iter_mut() { - stack.push(&mut value.typ); - } - for lambda_set in lambda_set_variables.iter_mut() { - stack.push(lambda_set.as_inner_mut()); - } - - stack.push(actual); - } - HostExposedAlias { - type_arguments, - lambda_set_variables, - actual: actual_type, - .. - } => { - for value in type_arguments.iter_mut() { - stack.push(value); - } - - for lambda_set in lambda_set_variables.iter_mut() { - stack.push(lambda_set.as_inner_mut()); - } - - stack.push(actual_type); - } - Apply(_, args, _) => { - stack.extend(args); - } - RangedNumber(typ, _) => { - stack.push(typ); - } - UnspecializedLambdaSet(Uls(v, _, _)) => { - debug_assert!( - substitutions.get(v).is_none(), - "unspecialized lambda sets should never be substituted before solving" - ); - } - - EmptyRec | EmptyTagUnion | Erroneous(_) => {} - } - } - } - - /// Swap Apply(rep_symbol, rep_args) with `actual`. Returns `Err` if there is an - /// `Apply(rep_symbol, _)`, but the args don't match. - pub fn substitute_alias( - &mut self, - rep_symbol: Symbol, - rep_args: &[Type], - actual: &Type, - ) -> Result<(), Region> { - use Type::*; - - match self { - Function(args, closure, ret) => { - for arg in args { - arg.substitute_alias(rep_symbol, rep_args, actual)?; - } - closure.substitute_alias(rep_symbol, rep_args, actual)?; - ret.substitute_alias(rep_symbol, rep_args, actual) - } - FunctionOrTagUnion(_, _, ext) => match ext { - TypeExtension::Open(ext) => ext.substitute_alias(rep_symbol, rep_args, actual), - TypeExtension::Closed => Ok(()), - }, - RecursiveTagUnion(_, tags, ext) | TagUnion(tags, ext) => { - for (_, args) in tags { - for x in args { - x.substitute_alias(rep_symbol, rep_args, actual)?; - } - } - - match ext { - TypeExtension::Open(ext) => ext.substitute_alias(rep_symbol, rep_args, actual), - TypeExtension::Closed => Ok(()), - } - } - Record(fields, ext) => { - for (_, x) in fields.iter_mut() { - x.substitute_alias(rep_symbol, rep_args, actual)?; - } - - match ext { - TypeExtension::Open(ext) => ext.substitute_alias(rep_symbol, rep_args, actual), - TypeExtension::Closed => Ok(()), - } - } - DelayedAlias(AliasCommon { - type_arguments, - lambda_set_variables: _no_aliases_in_lambda_sets, - .. - }) => { - for ta in type_arguments { - ta.substitute_alias(rep_symbol, rep_args, actual)?; - } - - Ok(()) - } - Alias { - type_arguments, - actual: alias_actual, - .. - } => { - for ta in type_arguments { - ta.typ.substitute_alias(rep_symbol, rep_args, actual)?; - } - alias_actual.substitute_alias(rep_symbol, rep_args, actual) - } - HostExposedAlias { - actual: actual_type, - .. - } => actual_type.substitute_alias(rep_symbol, rep_args, actual), - Apply(symbol, args, region) if *symbol == rep_symbol => { - if args.len() == rep_args.len() - && args.iter().zip(rep_args.iter()).all(|(t1, t2)| t1 == t2) - { - *self = actual.clone(); - - if let Apply(_, args, _) = self { - for arg in args { - arg.substitute_alias(rep_symbol, rep_args, actual)?; - } - } - return Ok(()); - } - Err(*region) - } - Apply(_, args, _) => { - for arg in args { - arg.substitute_alias(rep_symbol, rep_args, actual)?; - } - Ok(()) - } - RangedNumber(typ, _) => typ.substitute_alias(rep_symbol, rep_args, actual), - UnspecializedLambdaSet(..) => Ok(()), - EmptyRec | EmptyTagUnion | ClosureTag { .. } | Erroneous(_) | Variable(_) => Ok(()), - } - } - - fn contains_symbol_ext(ext: &TypeExtension, rep_symbol: Symbol) -> bool { - match ext { - TypeExtension::Open(ext) => ext.contains_symbol(rep_symbol), - TypeExtension::Closed => false, - } - } - - pub fn contains_symbol(&self, rep_symbol: Symbol) -> bool { - use Type::*; - - match self { - Function(args, closure, ret) => { - ret.contains_symbol(rep_symbol) - || closure.contains_symbol(rep_symbol) - || args.iter().any(|arg| arg.contains_symbol(rep_symbol)) - } - FunctionOrTagUnion(_, _, ext) => Self::contains_symbol_ext(ext, rep_symbol), - RecursiveTagUnion(_, tags, ext) | TagUnion(tags, ext) => { - Self::contains_symbol_ext(ext, rep_symbol) - || tags - .iter() - .flat_map(|v| v.1.iter()) - .any(|arg| arg.contains_symbol(rep_symbol)) - } - - Record(fields, ext) => { - Self::contains_symbol_ext(ext, rep_symbol) - || fields.values().any(|arg| arg.contains_symbol(rep_symbol)) - } - DelayedAlias(AliasCommon { - symbol, - type_arguments, - lambda_set_variables, - .. - }) => { - symbol == &rep_symbol - || type_arguments.iter().any(|v| v.contains_symbol(rep_symbol)) - || lambda_set_variables - .iter() - .any(|v| v.0.contains_symbol(rep_symbol)) - } - Alias { - symbol: alias_symbol, - actual: actual_type, - .. - } => alias_symbol == &rep_symbol || actual_type.contains_symbol(rep_symbol), - HostExposedAlias { name, actual, .. } => { - name == &rep_symbol || actual.contains_symbol(rep_symbol) - } - Apply(symbol, _, _) if *symbol == rep_symbol => true, - Apply(_, args, _) => args.iter().any(|arg| arg.contains_symbol(rep_symbol)), - RangedNumber(typ, _) => typ.contains_symbol(rep_symbol), - UnspecializedLambdaSet(Uls(_, sym, _)) => *sym == rep_symbol, - EmptyRec | EmptyTagUnion | ClosureTag { .. } | Erroneous(_) | Variable(_) => false, - } - } - - fn contains_variable_ext(ext: &TypeExtension, rep_variable: Variable) -> bool { - match ext { - TypeExtension::Open(ext) => ext.contains_variable(rep_variable), - TypeExtension::Closed => false, - } - } - - pub fn contains_variable(&self, rep_variable: Variable) -> bool { - use Type::*; - - match self { - Variable(v) => *v == rep_variable, - Function(args, closure, ret) => { - ret.contains_variable(rep_variable) - || closure.contains_variable(rep_variable) - || args.iter().any(|arg| arg.contains_variable(rep_variable)) - } - FunctionOrTagUnion(_, _, ext) => Self::contains_variable_ext(ext, rep_variable), - ClosureTag { name: _, captures } => { - captures.iter().any(|t| t.contains_variable(rep_variable)) - } - UnspecializedLambdaSet(Uls(v, _, _)) => *v == rep_variable, - RecursiveTagUnion(_, tags, ext) | TagUnion(tags, ext) => { - Self::contains_variable_ext(ext, rep_variable) - || tags - .iter() - .flat_map(|v| v.1.iter()) - .any(|arg| arg.contains_variable(rep_variable)) - } - - Record(fields, ext) => { - Self::contains_variable_ext(ext, rep_variable) - || fields - .values() - .any(|arg| arg.contains_variable(rep_variable)) - } - DelayedAlias(AliasCommon { .. }) => { - todo!() - } - Alias { - actual: actual_type, - .. - } => actual_type.contains_variable(rep_variable), - HostExposedAlias { actual, .. } => actual.contains_variable(rep_variable), - Apply(_, args, _) => args.iter().any(|arg| arg.contains_variable(rep_variable)), - RangedNumber(typ, _) => typ.contains_variable(rep_variable), - EmptyRec | EmptyTagUnion | Erroneous(_) => false, - } - } - - pub fn symbols(&self) -> Vec { - symbols_help(self) - } - - /// a shallow dealias, continue until the first constructor is not an alias. - pub fn shallow_dealias(&self) -> &Self { - let mut result = self; - while let Type::Alias { actual, .. } = result { - result = actual; - } - result - } - - pub fn shallow_structural_dealias(&self) -> &Self { - let mut result = self; - while let Type::Alias { - actual, - kind: AliasKind::Structural, - .. - } = result - { - result = actual; - } - result - } - - pub fn instantiate_aliases<'a, F>( - &mut self, - region: Region, - aliases: &'a F, - var_store: &mut VarStore, - new_lambda_set_variables: &mut ImSet, - ) where - F: Fn(Symbol) -> Option<&'a Alias>, - { - use Type::*; - - match self { - Function(args, closure, ret) => { - for arg in args { - arg.instantiate_aliases(region, aliases, var_store, new_lambda_set_variables); - } - closure.instantiate_aliases(region, aliases, var_store, new_lambda_set_variables); - ret.instantiate_aliases(region, aliases, var_store, new_lambda_set_variables); - } - FunctionOrTagUnion(_, _, ext) => { - if let TypeExtension::Open(ext) = ext { - ext.instantiate_aliases(region, aliases, var_store, new_lambda_set_variables); - } - } - RecursiveTagUnion(_, tags, ext) | TagUnion(tags, ext) => { - for (_, args) in tags { - for x in args { - x.instantiate_aliases(region, aliases, var_store, new_lambda_set_variables); - } - } - - if let TypeExtension::Open(ext) = ext { - ext.instantiate_aliases(region, aliases, var_store, new_lambda_set_variables); - } - } - Record(fields, ext) => { - for (_, x) in fields.iter_mut() { - x.instantiate_aliases(region, aliases, var_store, new_lambda_set_variables); - } - - if let TypeExtension::Open(ext) = ext { - ext.instantiate_aliases(region, aliases, var_store, new_lambda_set_variables); - } - } - DelayedAlias(AliasCommon { - type_arguments, - lambda_set_variables, - symbol: _, - }) => { - debug_assert!(lambda_set_variables - .iter() - .all(|lambda_set| matches!(lambda_set.0, Type::Variable(..)))); - type_arguments.iter_mut().for_each(|t| { - t.instantiate_aliases(region, aliases, var_store, new_lambda_set_variables) - }); - } - HostExposedAlias { - type_arguments: type_args, - lambda_set_variables, - actual: actual_type, - .. - } => { - for arg in type_args { - arg.instantiate_aliases(region, aliases, var_store, new_lambda_set_variables); - } - - for arg in lambda_set_variables { - arg.instantiate_aliases(region, aliases, var_store, new_lambda_set_variables); - } - - actual_type.instantiate_aliases( - region, - aliases, - var_store, - new_lambda_set_variables, - ); - } - Alias { - type_arguments: type_args, - lambda_set_variables, - actual: actual_type, - .. - } => { - for arg in type_args { - arg.typ.instantiate_aliases( - region, - aliases, - var_store, - new_lambda_set_variables, - ); - } - - for arg in lambda_set_variables { - arg.instantiate_aliases(region, aliases, var_store, new_lambda_set_variables); - } - - actual_type.instantiate_aliases( - region, - aliases, - var_store, - new_lambda_set_variables, - ); - } - Apply(symbol, args, _) => { - if let Some(alias) = aliases(*symbol) { - // TODO switch to this, but we still need to check for recursion with the - // `else` branch - if false { - let mut type_var_to_arg = Vec::new(); - - for (_, arg_ann) in alias.type_variables.iter().zip(args) { - type_var_to_arg.push(arg_ann.clone()); - } - - let mut lambda_set_variables = - Vec::with_capacity(alias.lambda_set_variables.len()); - - for _ in 0..alias.lambda_set_variables.len() { - let lvar = var_store.fresh(); - - new_lambda_set_variables.insert(lvar); - - lambda_set_variables.push(LambdaSet(Type::Variable(lvar))); - } - - let alias = Type::DelayedAlias(AliasCommon { - symbol: *symbol, - type_arguments: type_var_to_arg, - lambda_set_variables, - }); - - *self = alias; - } else { - if args.len() != alias.type_variables.len() { - *self = Type::Erroneous(Problem::BadTypeArguments { - symbol: *symbol, - region, - type_got: args.len() as u8, - alias_needs: alias.type_variables.len() as u8, - }); - return; - } - - let mut actual = alias.typ.clone(); - - let mut named_args = Vec::with_capacity(args.len()); - let mut substitution = ImMap::default(); - - // TODO substitute further in args - for ( - Loc { - value: - AliasVar { - var: placeholder, - opt_bound_ability, - .. - }, - .. - }, - filler, - ) in alias.type_variables.iter().zip(args.iter()) - { - let mut filler = filler.clone(); - filler.instantiate_aliases( - region, - aliases, - var_store, - new_lambda_set_variables, - ); - named_args.push(OptAbleType { - typ: filler.clone(), - opt_ability: *opt_bound_ability, - }); - substitution.insert(*placeholder, filler); - } - - // make sure hidden variables are freshly instantiated - let mut lambda_set_variables = - Vec::with_capacity(alias.lambda_set_variables.len()); - for typ in alias.lambda_set_variables.iter() { - if let Type::Variable(var) = typ.0 { - let fresh = var_store.fresh(); - new_lambda_set_variables.insert(fresh); - substitution.insert(var, Type::Variable(fresh)); - lambda_set_variables.push(LambdaSet(Type::Variable(fresh))); - } else { - unreachable!("at this point there should be only vars in there"); - } - } - - actual.instantiate_aliases( - region, - aliases, - var_store, - new_lambda_set_variables, - ); - - actual.substitute(&substitution); - - // instantiate recursion variable! - if let Type::RecursiveTagUnion(rec_var, mut tags, mut ext) = actual { - let new_rec_var = var_store.fresh(); - substitution.clear(); - substitution.insert(rec_var, Type::Variable(new_rec_var)); - - for typ in tags.iter_mut().flat_map(|v| v.1.iter_mut()) { - typ.substitute(&substitution); - } - - if let TypeExtension::Open(ext) = &mut ext { - ext.substitute(&substitution); - } - - actual = Type::RecursiveTagUnion(new_rec_var, tags, ext); - } - let alias = Type::Alias { - symbol: *symbol, - type_arguments: named_args, - lambda_set_variables, - actual: Box::new(actual), - kind: alias.kind, - }; - - *self = alias; - } - } else { - // one of the special-cased Apply types. - for x in args { - x.instantiate_aliases(region, aliases, var_store, new_lambda_set_variables); - } - } - } - RangedNumber(typ, _) => { - typ.instantiate_aliases(region, aliases, var_store, new_lambda_set_variables); - } - UnspecializedLambdaSet(..) => {} - EmptyRec | EmptyTagUnion | ClosureTag { .. } | Erroneous(_) | Variable(_) => {} - } - } - - pub fn instantiate_lambda_sets_as_unspecialized( - &mut self, - able_var: Variable, - ability_member: Symbol, - ) { - instantiate_lambda_sets_as_unspecialized(self, able_var, ability_member) - } - - pub fn is_tag_union_like(&self) -> bool { - matches!( - self, - Type::TagUnion(..) - | Type::RecursiveTagUnion(..) - | Type::FunctionOrTagUnion(..) - | Type::EmptyTagUnion - ) - } - - /// We say a type is "narrow" if no type composing it is a proper sum; that is, no type - /// composing it is a tag union with more than one variant. - /// - /// The types checked here must have all of their non-builtin `Apply`s instantiated, as a - /// non-instantiated `Apply` would be ambiguous. - /// - /// The following are narrow: - /// - /// ```roc - /// U8 - /// [A I8] - /// [A [B [C U8]]] - /// [A (R a)] as R a - /// ``` - /// - /// The following are not: - /// - /// ```roc - /// [A I8, B U8 ] - /// [A [B [Result U8 {}]]] (Result U8 {} is actually [Ok U8, Err {}]) - /// [A { lst: List (R a) }] as R a (List a is morally [Cons (List a), Nil] as List a) - /// ``` - pub fn is_narrow(&self) -> bool { - match self.shallow_dealias() { - Type::TagUnion(tags, ext) | Type::RecursiveTagUnion(_, tags, ext) => { - matches!(ext, TypeExtension::Closed) - && tags.len() == 1 - && tags[0].1.len() == 1 - && tags[0].1[0].is_narrow() - } - Type::Record(fields, ext) => match ext { - TypeExtension::Open(ext) => { - fields.values().all(|field| field.as_inner().is_narrow()) && ext.is_narrow() - } - TypeExtension::Closed => fields.values().all(|field| field.as_inner().is_narrow()), - }, - Type::Function(args, clos, ret) => { - args.iter().all(|a| a.is_narrow()) && clos.is_narrow() && ret.is_narrow() - } - // Lists and sets are morally two-tagged unions, as they can be empty - Type::Apply(Symbol::LIST_LIST | Symbol::SET_SET, _, _) => false, - Type::Apply(..) => internal_error!("cannot chase an Apply!"), - Type::Alias { .. } => internal_error!("should be dealiased"), - // Non-composite types are trivially narrow - _ => true, - } - } - - pub fn expect_variable(&self, reason: &'static str) -> Variable { - match self { - Type::Variable(v) => *v, - _ => internal_error!("{}", reason), - } - } -} - -fn symbols_help(initial: &Type) -> Vec { - use Type::*; - - let mut output = vec![]; - let mut stack = vec![initial]; - - while let Some(tipe) = stack.pop() { - match tipe { - Function(args, closure, ret) => { - stack.push(ret); - stack.push(closure); - stack.extend(args); - } - FunctionOrTagUnion(_, _, ext) => { - stack.extend(ext); - } - RecursiveTagUnion(_, tags, ext) | TagUnion(tags, ext) => { - stack.extend(ext); - stack.extend(tags.iter().flat_map(|v| v.1.iter())); - } - - Record(fields, ext) => { - stack.extend(ext); - stack.extend(fields.values().map(|field| field.as_inner())); - } - DelayedAlias(AliasCommon { - symbol, - type_arguments, - .. - }) => { - output.push(*symbol); - stack.extend(type_arguments); - } - Alias { - symbol: alias_symbol, - actual: actual_type, - .. - } => { - // because the type parameters are inlined in the actual type, we don't need to look - // at the type parameters here - output.push(*alias_symbol); - stack.push(actual_type); - } - HostExposedAlias { name, actual, .. } => { - // because the type parameters are inlined in the actual type, we don't need to look - // at the type parameters here - output.push(*name); - stack.push(actual); - } - Apply(symbol, args, _) => { - output.push(*symbol); - stack.extend(args); - } - Erroneous(Problem::CyclicAlias(alias, _, _)) => { - output.push(*alias); - } - RangedNumber(typ, _) => { - stack.push(typ); - } - UnspecializedLambdaSet(Uls(_, _sym, _)) => { - // ignore the member symbol because unspecialized lambda sets are internal-only - } - EmptyRec | EmptyTagUnion | ClosureTag { .. } | Erroneous(_) | Variable(_) => {} - } - } - - output.sort(); - output.dedup(); - - output -} - -fn variables_help(tipe: &Type, accum: &mut ImSet) { - use Type::*; - - match tipe { - EmptyRec | EmptyTagUnion | Erroneous(_) => (), - - Variable(v) => { - accum.insert(*v); - } - - Function(args, closure, ret) => { - for arg in args { - variables_help(arg, accum); - } - variables_help(closure, accum); - variables_help(ret, accum); - } - Record(fields, ext) => { - use RecordField::*; - - for (_, field) in fields { - match field { - Optional(x) => variables_help(x, accum), - Required(x) => variables_help(x, accum), - Demanded(x) => variables_help(x, accum), - }; - } - - if let TypeExtension::Open(ext) = ext { - variables_help(ext, accum); - } - } - ClosureTag { name: _, captures } => { - for t in captures { - variables_help(t, accum); - } - } - UnspecializedLambdaSet(Uls(v, _, _)) => { - accum.insert(*v); - } - TagUnion(tags, ext) => { - for (_, args) in tags { - for x in args { - variables_help(x, accum); - } - } - - if let TypeExtension::Open(ext) = ext { - variables_help(ext, accum); - } - } - FunctionOrTagUnion(_, _, ext) => { - if let TypeExtension::Open(ext) = ext { - variables_help(ext, accum); - } - } - RecursiveTagUnion(rec, tags, ext) => { - for (_, args) in tags { - for x in args { - variables_help(x, accum); - } - } - - if let TypeExtension::Open(ext) = ext { - variables_help(ext, accum); - } - - // just check that this is actually a recursive type - debug_assert!(accum.contains(rec)); - - // this rec var doesn't need to be in flex_vars or rigid_vars - accum.remove(rec); - } - DelayedAlias(AliasCommon { - type_arguments, - lambda_set_variables, - .. - }) => { - for arg in type_arguments { - variables_help(arg, accum); - } - - for lambda_set in lambda_set_variables { - variables_help(&lambda_set.0, accum); - } - } - Alias { - type_arguments, - actual, - .. - } => { - for arg in type_arguments { - variables_help(&arg.typ, accum); - } - variables_help(actual, accum); - } - HostExposedAlias { - type_arguments: arguments, - actual, - .. - } => { - for arg in arguments { - variables_help(arg, accum); - } - variables_help(actual, accum); - } - RangedNumber(typ, _) => { - variables_help(typ, accum); - } - Apply(_, args, _) => { - for x in args { - variables_help(x, accum); - } - } - } -} - -#[derive(Default)] -pub struct VariableDetail { - pub type_variables: MutSet, - pub lambda_set_variables: Vec, - pub recursion_variables: MutSet, -} - -impl VariableDetail { - pub fn is_empty(&self) -> bool { - self.type_variables.is_empty() - && self.lambda_set_variables.is_empty() - && self.recursion_variables.is_empty() - } -} - -fn variables_help_detailed(tipe: &Type, accum: &mut VariableDetail) { - use Type::*; - - match tipe { - EmptyRec | EmptyTagUnion | Erroneous(_) => (), - - Variable(v) => { - accum.type_variables.insert(*v); - } - - Function(args, closure, ret) => { - for arg in args { - variables_help_detailed(arg, accum); - } - if let Type::Variable(v) = **closure { - accum.lambda_set_variables.push(v); - } else { - variables_help_detailed(closure, accum); - } - - variables_help_detailed(ret, accum); - } - Record(fields, ext) => { - use RecordField::*; - - for (_, field) in fields { - match field { - Optional(x) => variables_help_detailed(x, accum), - Required(x) => variables_help_detailed(x, accum), - Demanded(x) => variables_help_detailed(x, accum), - }; - } - - if let TypeExtension::Open(ext) = ext { - variables_help_detailed(ext, accum); - } - } - ClosureTag { name: _, captures } => { - for t in captures { - variables_help_detailed(t, accum); - } - } - TagUnion(tags, ext) => { - for (_, args) in tags { - for x in args { - variables_help_detailed(x, accum); - } - } - - if let TypeExtension::Open(ext) = ext { - variables_help_detailed(ext, accum); - } - } - FunctionOrTagUnion(_, _, ext) => { - if let TypeExtension::Open(ext) = ext { - variables_help_detailed(ext, accum); - } - } - UnspecializedLambdaSet(Uls(var, _, _)) => { - accum.type_variables.insert(*var); - } - RecursiveTagUnion(rec, tags, ext) => { - for (_, args) in tags { - for x in args { - variables_help_detailed(x, accum); - } - } - - if let TypeExtension::Open(ext) = ext { - variables_help_detailed(ext, accum); - } - - // just check that this is actually a recursive type - // debug_assert!(accum.type_variables.contains(rec)); - - // this rec var doesn't need to be in flex_vars or rigid_vars - accum.type_variables.remove(rec); - - accum.recursion_variables.insert(*rec); - } - DelayedAlias(AliasCommon { - type_arguments, - lambda_set_variables, - .. - }) => { - for arg in type_arguments { - variables_help_detailed(arg, accum); - } - - for lambda_set in lambda_set_variables { - if let Type::Variable(v) = lambda_set.0 { - accum.lambda_set_variables.push(v); - } else { - variables_help_detailed(&lambda_set.0, accum); - } - } - } - Alias { - type_arguments, - actual, - .. - } => { - for arg in type_arguments { - variables_help_detailed(&arg.typ, accum); - } - variables_help_detailed(actual, accum); - } - HostExposedAlias { - type_arguments: arguments, - actual, - .. - } => { - for arg in arguments { - variables_help_detailed(arg, accum); - } - variables_help_detailed(actual, accum); - } - RangedNumber(typ, _) => { - variables_help_detailed(typ, accum); - } - Apply(_, args, _) => { - for x in args { - variables_help_detailed(x, accum); - } - } - } -} - -#[derive(Debug)] -pub struct RecordStructure { - /// Invariant: these should be sorted! - pub fields: Vec<(Lowercase, RecordField)>, - pub ext: Variable, -} - -#[derive(Debug)] -pub struct TagUnionStructure<'a> { - /// Invariant: these should be sorted! - pub fields: Vec<(TagName, &'a [Variable])>, - pub ext: Variable, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum PReason { - TypedArg { - opt_name: Option, - index: HumanIndex, - }, - WhenMatch { - index: HumanIndex, - sub_pattern: HumanIndex, - }, - TagArg { - tag_name: TagName, - index: HumanIndex, - }, - PatternGuard, - OptionalField, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum AnnotationSource { - TypedIfBranch { - index: HumanIndex, - num_branches: usize, - region: Region, - }, - TypedWhenBranch { - index: HumanIndex, - region: Region, - }, - TypedBody { - region: Region, - }, - RequiredSymbol { - region: Region, - }, -} - -impl AnnotationSource { - pub fn region(&self) -> Region { - match self { - &Self::TypedIfBranch { region, .. } - | &Self::TypedWhenBranch { region, .. } - | &Self::TypedBody { region, .. } => region, - &Self::RequiredSymbol { region, .. } => region, - } - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum Reason { - FnArg { - name: Option, - arg_index: HumanIndex, - }, - TypedArg { - name: Option, - arg_index: HumanIndex, - }, - FnCall { - name: Option, - arity: u8, - }, - LowLevelOpArg { - op: LowLevel, - arg_index: HumanIndex, - }, - ForeignCallArg { - foreign_symbol: ForeignSymbol, - arg_index: HumanIndex, - }, - FloatLiteral, - IntLiteral, - NumLiteral, - StrInterpolation, - WhenBranches, - WhenBranch { - index: HumanIndex, - }, - WhenGuard, - ExpectCondition, - IfCondition, - IfBranch { - index: HumanIndex, - total_branches: usize, - }, - ElemInList { - index: HumanIndex, - }, - RecordUpdateValue(Lowercase), - RecordUpdateKeys(Symbol, SendMap), - RecordDefaultField(Lowercase), - NumericLiteralSuffix, - InvalidAbilityMemberSpecialization { - member_name: Symbol, - def_region: Region, - unimplemented_abilities: DoesNotImplementAbility, - }, - GeneralizedAbilityMemberSpecialization { - member_name: Symbol, - def_region: Region, - }, -} - -#[derive(PartialEq, Debug, Clone)] -pub enum Category { - Lookup(Symbol), - CallResult(Option, CalledVia), - LowLevelOpResult(LowLevel), - ForeignCall, - TagApply { - tag_name: TagName, - args_count: usize, - }, - OpaqueWrap(Symbol), - OpaqueArg, - Lambda, - Uniqueness, - ClosureSize, - StrInterpolation, - - // storing variables in the ast - Storage(&'static str, u32), - - // control flow - If, - When, - - // types - Float, - Int, - Num, - List, - Str, - Character, - - // records - Record, - Accessor(Lowercase), - Access(Lowercase), - DefaultValue(Lowercase), // for setting optional fields - - AbilityMemberSpecialization(Symbol), - - Expect, - Unknown, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum PatternCategory { - Record, - EmptyRecord, - PatternGuard, - PatternDefault, - Set, - Map, - Ctor(TagName), - Opaque(Symbol), - Str, - Num, - Int, - Float, - Character, -} - -#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)] -pub enum AliasKind { - /// A structural alias is something like - /// List a : [Nil, Cons a (List a)] - /// It is typed structurally, so that a `List U8` is always equal to a `[Nil]_`, for example. - Structural, - /// An opaque alias corresponds to an opaque type from the language syntax, like - /// Age := U32 - /// It is type nominally, so that `Age` is never equal to `U8` - the only way to unwrap the - /// structural type inside `Age` is to unwrap the opaque, so `Age` = `@Age U8`. - Opaque, -} - -#[derive(Clone, Debug, PartialEq)] -pub struct AliasVar { - pub name: Lowercase, - pub var: Variable, - /// `Some` if this variable is bound to an ability; `None` otherwise. - pub opt_bound_ability: Option, -} - -impl AliasVar { - pub fn unbound(name: Lowercase, var: Variable) -> AliasVar { - Self { - name, - var, - opt_bound_ability: None, - } - } -} - -impl From<&AliasVar> for OptAbleVar { - fn from(av: &AliasVar) -> OptAbleVar { - OptAbleVar { - var: av.var, - opt_ability: av.opt_bound_ability, - } - } -} - -#[derive(Clone, Debug, PartialEq)] -pub struct Alias { - pub region: Region, - pub type_variables: Vec>, - - /// lambda set variables, e.g. the one annotating the arrow in - /// a |c|-> b - pub lambda_set_variables: Vec, - - pub recursion_variables: MutSet, - - pub typ: Type, - - pub kind: AliasKind, -} - -impl Alias { - pub fn header_region(&self) -> Region { - Region::across_all( - [self.region] - .iter() - .chain(self.type_variables.iter().map(|tv| &tv.region)), - ) - } -} - -#[derive(PartialEq, Eq, Debug, Clone, Hash)] -pub enum Problem { - CanonicalizationProblem, - CircularType(Symbol, Box, Region), - CyclicAlias(Symbol, Region, Vec), - UnrecognizedIdent(Ident), - Shadowed(Region, Loc), - BadTypeArguments { - symbol: Symbol, - region: Region, - type_got: u8, - alias_needs: u8, - }, - InvalidModule, - SolvedTypeError, - HasClauseIsNotAbility(Region), -} - -#[derive(PartialEq, Eq, Debug, Clone)] -pub enum Mismatch { - TypeMismatch, - IfConditionNotBool, - InconsistentIfElse, - InconsistentWhenBranches, - CanonicalizationProblem, - TypeNotInRange, - DoesNotImplementAbiity(Variable, Symbol), -} - -pub type DoesNotImplementAbility = Vec<(ErrorType, Symbol)>; - -#[derive(PartialEq, Eq, Clone, Hash)] -pub enum ErrorType { - Infinite, - Type(Symbol, Vec), - FlexVar(Lowercase), - RigidVar(Lowercase), - FlexAbleVar(Lowercase, Symbol), - RigidAbleVar(Lowercase, Symbol), - Record(SendMap>, TypeExt), - TagUnion(SendMap>, TypeExt), - RecursiveTagUnion(Box, SendMap>, TypeExt), - Function(Vec, Box, Box), - Alias(Symbol, Vec, Box, AliasKind), - Range(Box, Vec), - Error, -} - -impl std::fmt::Debug for ErrorType { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - // TODO remove clone - write!(f, "{:?}", write_debug_error_type(self.clone())) - } -} - -impl ErrorType { - pub fn unwrap_structural_alias(self) -> ErrorType { - match self { - ErrorType::Alias(_, _, real, AliasKind::Structural) => real.unwrap_structural_alias(), - real => real, - } - } - - /// Adds all named type variables used in the type to a set. - pub fn add_names(&self, taken: &mut MutSet) { - use ErrorType::*; - match self { - Infinite => {} - Type(_, ts) => ts.iter().for_each(|t| t.add_names(taken)), - FlexVar(v) | RigidVar(v) | FlexAbleVar(v, _) | RigidAbleVar(v, _) => { - taken.insert(v.clone()); - } - Record(fields, ext) => { - fields - .iter() - .for_each(|(_, t)| t.as_inner().add_names(taken)); - ext.add_names(taken); - } - TagUnion(tags, ext) => { - tags.iter() - .for_each(|(_, ts)| ts.iter().for_each(|t| t.add_names(taken))); - ext.add_names(taken); - } - RecursiveTagUnion(t, tags, ext) => { - t.add_names(taken); - tags.iter() - .for_each(|(_, ts)| ts.iter().for_each(|t| t.add_names(taken))); - ext.add_names(taken); - } - Function(args, capt, ret) => { - args.iter().for_each(|t| t.add_names(taken)); - capt.add_names(taken); - ret.add_names(taken); - } - Alias(_, ts, t, _) => { - ts.iter().for_each(|t| { - t.add_names(taken); - }); - t.add_names(taken); - } - Range(typ, ts) => { - typ.add_names(taken); - ts.iter().for_each(|t| { - t.add_names(taken); - }); - } - Error => {} - } - } -} - -pub fn write_error_type(home: ModuleId, interns: &Interns, error_type: ErrorType) -> String { - let mut buf = String::new(); - write_error_type_help(home, interns, error_type, &mut buf, Parens::Unnecessary); - - buf -} - -fn write_error_type_help( - home: ModuleId, - interns: &Interns, - error_type: ErrorType, - buf: &mut String, - parens: Parens, -) { - use ErrorType::*; - - match error_type { - Infinite => buf.push('∞'), - Error => buf.push('?'), - FlexVar(name) => buf.push_str(name.as_str()), - RigidVar(name) => buf.push_str(name.as_str()), - Type(symbol, arguments) => { - let write_parens = parens == Parens::InTypeParam && !arguments.is_empty(); - - if write_parens { - buf.push('('); - } - buf.push_str(symbol.as_str(interns)); - - for arg in arguments { - buf.push(' '); - - write_error_type_help(home, interns, arg, buf, Parens::InTypeParam); - } - - if write_parens { - buf.push(')'); - } - } - Alias(Symbol::NUM_NUM, mut arguments, _actual, _) => { - debug_assert!(arguments.len() == 1); - - let argument = arguments.remove(0); - - match argument { - Type(Symbol::NUM_INTEGER, _) => { - buf.push_str("Int"); - } - Type(Symbol::NUM_FLOATINGPOINT, _) => { - buf.push_str("F64"); - } - other => { - let write_parens = parens == Parens::InTypeParam; - - if write_parens { - buf.push('('); - } - buf.push_str("Num "); - write_error_type_help(home, interns, other, buf, Parens::InTypeParam); - - if write_parens { - buf.push(')'); - } - } - } - } - Function(arguments, _closure, result) => { - let write_parens = parens != Parens::Unnecessary; - - if write_parens { - buf.push(')'); - } - - let mut it = arguments.into_iter().peekable(); - - while let Some(arg) = it.next() { - write_error_type_help(home, interns, arg, buf, Parens::InFn); - if it.peek().is_some() { - buf.push_str(", "); - } - } - - buf.push_str(" -> "); - - write_error_type_help(home, interns, *result, buf, Parens::InFn); - - if write_parens { - buf.push(')'); - } - } - Record(fields, ext) => { - buf.push('{'); - - for (label, field) in fields { - use RecordField::*; - - buf.push_str(label.as_str()); - - let content = match field { - Optional(content) => { - buf.push_str(" ? "); - content - } - Required(content) => { - buf.push_str(" : "); - content - } - Demanded(content) => { - buf.push_str(" : "); - content - } - }; - - write_error_type_help(home, interns, content, buf, Parens::Unnecessary); - } - - buf.push('}'); - write_type_ext(ext, buf); - } - - other => todo!("cannot format {:?} yet", other), - } -} - -pub fn write_debug_error_type(error_type: ErrorType) -> String { - let mut buf = String::new(); - write_debug_error_type_help(error_type, &mut buf, Parens::Unnecessary); - - buf -} - -fn write_debug_error_type_help(error_type: ErrorType, buf: &mut String, parens: Parens) { - use ErrorType::*; - - match error_type { - Infinite => buf.push('∞'), - Error => buf.push('?'), - FlexVar(name) | RigidVar(name) => buf.push_str(name.as_str()), - FlexAbleVar(name, symbol) | RigidAbleVar(name, symbol) => { - let write_parens = parens == Parens::InTypeParam; - if write_parens { - buf.push('('); - } - buf.push_str(name.as_str()); - buf.push_str(&format!(" has {:?}", symbol)); - if write_parens { - buf.push(')'); - } - } - Type(symbol, arguments) => { - let write_parens = parens == Parens::InTypeParam && !arguments.is_empty(); - - if write_parens { - buf.push('('); - } - buf.push_str(&format!("{:?}", symbol)); - - for arg in arguments { - buf.push(' '); - - write_debug_error_type_help(arg, buf, Parens::InTypeParam); - } - - if write_parens { - buf.push(')'); - } - } - Alias(Symbol::NUM_NUM, mut arguments, _actual, _) => { - debug_assert!(arguments.len() == 1); - - let argument = arguments.remove(0); - - match argument { - Type(Symbol::NUM_INTEGER, _) => { - buf.push_str("Int"); - } - Type(Symbol::NUM_FLOATINGPOINT, _) => { - buf.push_str("F64"); - } - other => { - let write_parens = parens == Parens::InTypeParam; - - if write_parens { - buf.push('('); - } - buf.push_str("Num "); - write_debug_error_type_help(other, buf, Parens::InTypeParam); - - if write_parens { - buf.push(')'); - } - } - } - } - Alias(symbol, arguments, _actual, _) => { - let write_parens = parens == Parens::InTypeParam && !arguments.is_empty(); - - if write_parens { - buf.push('('); - } - buf.push_str(&format!("{:?}", symbol)); - - for arg in arguments { - buf.push(' '); - - write_debug_error_type_help(arg, buf, Parens::InTypeParam); - } - - // useful for debugging - let write_out_alias = true; - if write_out_alias { - buf.push_str("[[ but really "); - write_debug_error_type_help(*_actual, buf, Parens::Unnecessary); - buf.push_str("]]"); - } - - if write_parens { - buf.push(')'); - } - } - Function(arguments, _closure, result) => { - let write_parens = parens != Parens::Unnecessary; - - if write_parens { - buf.push('('); - } - - let mut it = arguments.into_iter().peekable(); - - while let Some(arg) = it.next() { - write_debug_error_type_help(arg, buf, Parens::InFn); - if it.peek().is_some() { - buf.push_str(", "); - } - } - - buf.push_str(" -> "); - - write_debug_error_type_help(*result, buf, Parens::InFn); - - if write_parens { - buf.push(')'); - } - } - Record(fields, ext) => { - buf.push('{'); - - for (label, field) in fields { - use RecordField::*; - - buf.push_str(label.as_str()); - - let content = match field { - Optional(content) => { - buf.push_str(" ? "); - content - } - Required(content) => { - buf.push_str(" : "); - content - } - Demanded(content) => { - buf.push_str(" : "); - content - } - }; - - write_debug_error_type_help(content, buf, Parens::Unnecessary); - } - - buf.push('}'); - write_type_ext(ext, buf); - } - TagUnion(tags, ext) => { - buf.push('['); - - let mut it = tags.into_iter().peekable(); - - while let Some((tag, args)) = it.next() { - buf.push_str(&format!("{:?}", tag)); - for arg in args { - buf.push(' '); - write_debug_error_type_help(arg, buf, Parens::InTypeParam); - } - - if it.peek().is_some() { - buf.push_str(", "); - } - } - - buf.push(']'); - write_type_ext(ext, buf); - } - RecursiveTagUnion(rec, tags, ext) => { - buf.push('['); - - let mut it = tags.into_iter().peekable(); - while let Some((tag, args)) = it.next() { - buf.push_str(&format!("{:?}", tag)); - for arg in args { - buf.push(' '); - write_debug_error_type_help(arg, buf, Parens::Unnecessary); - } - - if it.peek().is_some() { - buf.push_str(", "); - } - } - - buf.push(']'); - write_type_ext(ext, buf); - - buf.push_str(" as "); - - write_debug_error_type_help(*rec, buf, Parens::Unnecessary); - } - Range(typ, types) => { - write_debug_error_type_help(*typ, buf, parens); - buf.push('<'); - - let mut it = types.into_iter().peekable(); - while let Some(typ) = it.next() { - write_debug_error_type_help(typ, buf, Parens::Unnecessary); - - if it.peek().is_some() { - buf.push_str(", "); - } - } - - buf.push('>'); - } - } -} - -#[derive(PartialEq, Eq, Debug, Clone, Hash)] -pub enum TypeExt { - Closed, - FlexOpen(Lowercase), - RigidOpen(Lowercase), -} - -impl TypeExt { - pub fn add_names(&self, taken: &mut MutSet) { - use TypeExt::*; - match self { - Closed => {} - FlexOpen(n) | RigidOpen(n) => { - taken.insert(n.clone()); - } - } - } -} - -fn write_type_ext(ext: TypeExt, buf: &mut String) { - use TypeExt::*; - match ext { - Closed => {} - FlexOpen(lowercase) | RigidOpen(lowercase) => { - buf.push_str(lowercase.as_str()); - } - } -} - -static THE_LETTER_A: u32 = 'a' as u32; - -pub fn name_type_var bool>( - letters_used: u32, - taken: &mut impl Iterator, - mut predicate: F, -) -> (Lowercase, u32) { - // TODO we should arena-allocate this String, - // so all the strings in the entire pass only require ~1 allocation. - let mut buf = String::with_capacity((letters_used as usize) / 26 + 1); - - let is_taken = { - let mut remaining = letters_used as i32; - - while remaining >= 0 { - buf.push(std::char::from_u32(THE_LETTER_A + ((remaining as u32) % 26)).unwrap()); - remaining -= 26; - } - - let generated_name: &str = buf.as_str(); - - taken.any(|item| predicate(&item, generated_name)) - }; - - if is_taken { - // If the generated name is already taken, try again. - name_type_var(letters_used + 1, taken, predicate) - } else { - (buf.into(), letters_used + 1) - } -} - -#[derive(Debug, Copy, Clone)] -pub struct RecordFieldsError; - -pub fn gather_fields_unsorted_iter( - subs: &Subs, - other_fields: RecordFields, - mut var: Variable, -) -> Result< - ( - impl Iterator)> + '_, - Variable, - ), - RecordFieldsError, -> { - use crate::subs::Content::*; - use crate::subs::FlatType::*; - - let mut stack = vec![other_fields]; - - loop { - match subs.get_content_without_compacting(var) { - Structure(Record(sub_fields, sub_ext)) => { - stack.push(*sub_fields); - - if var == Variable::EMPTY_RECORD { - break; - } else { - var = *sub_ext; - } - } - - Alias(_, _, actual_var, _) => { - // TODO according to elm/compiler: "TODO may be dropping useful alias info here" - var = *actual_var; - } - - Structure(EmptyRecord) => break, - FlexVar(_) => break, - - // TODO investigate apparently this one pops up in the reporting tests! - RigidVar(_) => break, - - _ => return Err(RecordFieldsError), - } - } - - let it = stack - .into_iter() - .flat_map(|fields| fields.iter_all()) - .map(move |(i1, i2, i3)| { - let field_name: &Lowercase = &subs[i1]; - let variable = subs[i2]; - let record_field: RecordField = subs[i3].map(|_| variable); - - (field_name, record_field) - }); - - Ok((it, var)) -} - -pub fn gather_fields( - subs: &Subs, - other_fields: RecordFields, - var: Variable, -) -> Result { - let (it, ext) = gather_fields_unsorted_iter(subs, other_fields, var)?; - - let mut result: Vec<_> = it - .map(|(ref_label, field)| (ref_label.clone(), field)) - .collect(); - - result.sort_by(|(a, _), (b, _)| a.cmp(b)); - - Ok(RecordStructure { - fields: result, - ext, - }) -} - -pub fn gather_tags_unsorted_iter( - subs: &Subs, - other_fields: UnionTags, - mut var: Variable, -) -> ( - impl Iterator + '_, - Variable, -) { - use crate::subs::Content::*; - use crate::subs::FlatType::*; - - let mut stack = vec![other_fields]; - - loop { - match subs.get_content_without_compacting(var) { - Structure(TagUnion(sub_fields, sub_ext)) => { - stack.push(*sub_fields); - - var = *sub_ext; - } - - Structure(FunctionOrTagUnion(_tag_name_index, _, _sub_ext)) => { - todo!("this variant does not use SOA yet, and therefore this case is unreachable right now") - // let sub_fields: UnionTags = (*tag_name_index).into(); - // stack.push(sub_fields); - // - // var = *sub_ext; - } - - Structure(RecursiveTagUnion(_, _sub_fields, _sub_ext)) => { - todo!("this variant does not use SOA yet, and therefore this case is unreachable right now") - // stack.push(*sub_fields); - // - // var = *sub_ext; - } - - Alias(_, _, actual_var, _) => { - // TODO according to elm/compiler: "TODO may be dropping useful alias info here" - var = *actual_var; - } - - Structure(EmptyTagUnion) => break, - FlexVar(_) => break, - - // TODO investigate this likely can happen when there is a type error - RigidVar(_) => break, - - other => unreachable!( - "something weird ended up in a tag union type: {:?} at {:?}", - other, var - ), - } - } - - let it = stack - .into_iter() - .flat_map(|union_tags| union_tags.iter_all()) - .map(move |(i1, i2)| { - let tag_name: &TagName = &subs[i1]; - let subs_slice = subs[i2]; - - (tag_name, subs_slice) - }); - - (it, var) -} - -pub fn gather_tags_slices( - subs: &Subs, - other_fields: UnionTags, - var: Variable, -) -> (Vec<(TagName, VariableSubsSlice)>, Variable) { - let (it, ext) = gather_tags_unsorted_iter(subs, other_fields, var); - - let mut result: Vec<_> = it - .map(|(ref_label, field): (_, VariableSubsSlice)| (ref_label.clone(), field)) - .collect(); - - result.sort_by(|(a, _), (b, _)| a.cmp(b)); - - (result, ext) -} - -pub fn gather_tags(subs: &Subs, other_fields: UnionTags, var: Variable) -> TagUnionStructure { - let (it, ext) = gather_tags_unsorted_iter(subs, other_fields, var); - - let mut result: Vec<_> = it - .map(|(ref_label, field): (_, VariableSubsSlice)| { - (ref_label.clone(), subs.get_subs_slice(field)) - }) - .collect(); - - result.sort_by(|(a, _), (b, _)| a.cmp(b)); - - TagUnionStructure { - fields: result, - ext, - } -} - -fn instantiate_lambda_sets_as_unspecialized( - typ: &mut Type, - able_var: Variable, - ability_member: Symbol, -) { - // We want to pop and assign lambda sets pre-order for readability, so types - // should be pushed onto the stack in post-order - let mut stack = vec![typ]; - let mut region = 0; - - let mut new_uls = || { - region += 1; - Type::UnspecializedLambdaSet(Uls(able_var, ability_member, region)) - }; - - while let Some(typ) = stack.pop() { - match typ { - Type::EmptyRec => {} - Type::EmptyTagUnion => {} - Type::Function(args, lambda_set, ret) => { - debug_assert!( - matches!(**lambda_set, Type::Variable(..)), - "lambda set already bound" - ); - - **lambda_set = new_uls(); - stack.push(ret); - stack.extend(args.iter_mut().rev()); - } - Type::Record(fields, ext) => { - stack.extend(ext.iter_mut()); - for (_, x) in fields.iter_mut() { - stack.push(x.as_inner_mut()); - } - } - Type::TagUnion(tags, ext) | Type::RecursiveTagUnion(_, tags, ext) => { - stack.extend(ext.iter_mut()); - for (_, ts) in tags { - for t in ts.iter_mut().rev() { - stack.push(t); - } - } - } - Type::FunctionOrTagUnion(_, _, ext) => { - stack.extend(ext.iter_mut()); - } - Type::ClosureTag { name: _, captures } => { - stack.extend(captures.iter_mut().rev()); - } - Type::UnspecializedLambdaSet(..) => { - internal_error!("attempting to re-instantiate ULS") - } - Type::DelayedAlias(AliasCommon { - symbol: _, - type_arguments, - lambda_set_variables, - }) => { - for lambda_set in lambda_set_variables.iter_mut() { - debug_assert!(matches!(lambda_set.0, Type::Variable(_))); - lambda_set.0 = new_uls(); - } - stack.extend(type_arguments.iter_mut().rev()); - } - Type::Alias { - symbol: _, - type_arguments, - lambda_set_variables, - actual, - kind: _, - } => { - for lambda_set in lambda_set_variables.iter_mut() { - debug_assert!(matches!(lambda_set.0, Type::Variable(_))); - lambda_set.0 = new_uls(); - } - stack.push(actual); - stack.extend(type_arguments.iter_mut().rev().map(|t| &mut t.typ)); - } - Type::HostExposedAlias { - name: _, - type_arguments, - lambda_set_variables, - actual_var: _, - actual, - } => { - for lambda_set in lambda_set_variables.iter_mut() { - debug_assert!(matches!(lambda_set.0, Type::Variable(_))); - lambda_set.0 = new_uls(); - } - stack.push(actual); - stack.extend(type_arguments.iter_mut().rev()); - } - Type::Apply(_sym, args, _region) => { - stack.extend(args.iter_mut().rev()); - } - Type::Variable(_) => {} - Type::RangedNumber(t, _) => { - stack.push(t); - } - Type::Erroneous(_) => {} - } - } -} - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn instantiate_lambda_sets_as_unspecialized() { - let mut var_store = VarStore::default(); - let l1 = Box::new(Type::Variable(var_store.fresh())); - let l2 = Box::new(Type::Variable(var_store.fresh())); - let l3 = Box::new(Type::Variable(var_store.fresh())); - let mut typ = Type::Function( - vec![Type::Function(vec![], l2, Box::new(Type::EmptyRec))], - l1, - Box::new(Type::TagUnion( - vec![( - TagName("A".into()), - vec![Type::Function(vec![], l3, Box::new(Type::EmptyRec))], - )], - TypeExtension::Closed, - )), - ); - - let able_var = var_store.fresh(); - let member = Symbol::UNDERSCORE; - typ.instantiate_lambda_sets_as_unspecialized(able_var, member); - - macro_rules! check_uls { - ($typ:expr, $region:literal) => {{ - match $typ { - Type::UnspecializedLambdaSet(Uls(var1, member1, $region)) => { - assert!(var1 == able_var && member1 == member) - } - _ => panic!(), - } - }}; - } - - match typ { - Type::Function(args, l1, ret) => { - check_uls!(*l1, 1); - - match args.as_slice() { - [Type::Function(args, l2, ret)] => { - check_uls!(**l2, 2); - assert!(args.is_empty()); - assert!(matches!(**ret, Type::EmptyRec)); - } - _ => panic!(), - } - - match *ret { - Type::TagUnion(tags, TypeExtension::Closed) => match tags.as_slice() { - [(name, args)] => { - assert_eq!(name.0.as_str(), "A"); - match args.as_slice() { - [Type::Function(args, l3, ret)] => { - check_uls!(**l3, 3); - assert!(args.is_empty()); - assert!(matches!(**ret, Type::EmptyRec)); - } - _ => panic!(), - } - } - _ => panic!(), - }, - _ => panic!(), - } - } - _ => panic!(), - } - } -} diff --git a/compiler/types/src/unification_table.rs b/compiler/types/src/unification_table.rs deleted file mode 100644 index 9f2f95a223..0000000000 --- a/compiler/types/src/unification_table.rs +++ /dev/null @@ -1,344 +0,0 @@ -use crate::subs::{Content, Descriptor, Mark, OptVariable, Rank, Variable, VariableSubsSlice}; - -#[derive(Clone, Default)] -pub struct UnificationTable { - contents: Vec, - ranks: Vec, - marks: Vec, - copies: Vec, - redirects: Vec, -} - -pub struct Snapshot(UnificationTable); - -impl UnificationTable { - #[allow(unused)] - pub fn with_capacity(cap: usize) -> Self { - Self { - contents: Vec::with_capacity(cap), // vec![Content::Error; cap], - ranks: Vec::with_capacity(cap), // vec![Rank::NONE; cap], - marks: Vec::with_capacity(cap), // vec![Mark::NONE; cap], - copies: Vec::with_capacity(cap), // vec![OptVariable::NONE; cap], - redirects: Vec::with_capacity(cap), // vec![OptVariable::NONE; cap], - } - } - - pub fn len(&self) -> usize { - self.contents.len() - } - - pub fn is_empty(&self) -> bool { - self.contents.is_empty() - } - - pub fn reserve(&mut self, extra_length: usize) -> VariableSubsSlice { - use std::iter::repeat; - - let start = self.contents.len(); - - self.contents - .extend(repeat(Content::FlexVar(None)).take(extra_length)); - self.ranks.extend(repeat(Rank::NONE).take(extra_length)); - self.marks.extend(repeat(Mark::NONE).take(extra_length)); - self.copies - .extend(repeat(OptVariable::NONE).take(extra_length)); - self.redirects - .extend(repeat(OptVariable::NONE).take(extra_length)); - - VariableSubsSlice::new(start as _, extra_length as _) - } - - pub fn push( - &mut self, - content: Content, - rank: Rank, - mark: Mark, - copy: OptVariable, - ) -> Variable { - let variable = unsafe { Variable::from_index(self.len() as _) }; - - self.contents.push(content); - self.ranks.push(rank); - self.marks.push(mark); - self.copies.push(copy); - self.redirects.push(OptVariable::NONE); - - variable - } - - #[allow(unused)] - pub fn set( - &mut self, - key: Variable, - content: Content, - rank: Rank, - mark: Mark, - copy: OptVariable, - ) { - let index = self.root_key(key).index() as usize; - - self.contents[index] = content; - self.ranks[index] = rank; - self.marks[index] = mark; - self.copies[index] = copy; - } - - pub fn modify(&mut self, key: Variable, mapper: F) -> T - where - F: FnOnce(&mut Descriptor) -> T, - { - let index = self.root_key(key).index() as usize; - - let mut desc = Descriptor { - content: self.contents[index], - rank: self.ranks[index], - mark: self.marks[index], - copy: self.copies[index], - }; - - let result = mapper(&mut desc); - - self.contents[index] = desc.content; - self.ranks[index] = desc.rank; - self.marks[index] = desc.mark; - self.copies[index] = desc.copy; - - result - } - - // GET UNCHECKED - - #[inline(always)] - pub fn get_rank_unchecked(&self, key: Variable) -> Rank { - self.ranks[key.index() as usize] - } - - #[inline(always)] - pub fn get_mark_unchecked(&self, key: Variable) -> Mark { - self.marks[key.index() as usize] - } - - #[allow(unused)] - #[inline(always)] - pub fn get_copy_unchecked(&self, key: Variable) -> OptVariable { - self.copies[key.index() as usize] - } - - #[inline(always)] - pub fn get_content_unchecked(&self, key: Variable) -> &Content { - &self.contents[key.index() as usize] - } - - // GET CHECKED - - #[inline(always)] - pub fn get_rank(&self, key: Variable) -> Rank { - self.ranks[self.root_key_without_compacting(key).index() as usize] - } - - #[inline(always)] - pub fn get_mark(&self, key: Variable) -> Mark { - self.marks[self.root_key_without_compacting(key).index() as usize] - } - - #[inline(always)] - pub fn get_copy(&self, key: Variable) -> OptVariable { - let index = self.root_key_without_compacting(key).index() as usize; - self.copies[index] - } - - #[inline(always)] - pub fn get_content(&self, key: Variable) -> &Content { - &self.contents[self.root_key_without_compacting(key).index() as usize] - } - - // SET UNCHECKED - - #[inline(always)] - pub fn set_rank_unchecked(&mut self, key: Variable, value: Rank) { - self.ranks[key.index() as usize] = value; - } - - #[inline(always)] - pub fn set_mark_unchecked(&mut self, key: Variable, value: Mark) { - self.marks[key.index() as usize] = value; - } - - #[allow(unused)] - #[inline(always)] - pub fn set_copy_unchecked(&mut self, key: Variable, value: OptVariable) { - self.copies[key.index() as usize] = value; - } - - #[allow(unused)] - #[inline(always)] - pub fn set_content_unchecked(&mut self, key: Variable, value: Content) { - self.contents[key.index() as usize] = value; - } - - // SET CHECKED - - #[inline(always)] - pub fn set_rank(&mut self, key: Variable, value: Rank) { - let index = self.root_key(key).index() as usize; - self.ranks[index] = value; - } - - #[inline(always)] - pub fn set_mark(&mut self, key: Variable, value: Mark) { - let index = self.root_key(key).index() as usize; - self.marks[index] = value; - } - - #[inline(always)] - pub fn set_copy(&mut self, key: Variable, value: OptVariable) { - let index = self.root_key(key).index() as usize; - self.copies[index] = value; - } - - #[inline(always)] - pub fn set_content(&mut self, key: Variable, value: Content) { - let index = self.root_key(key).index() as usize; - self.contents[index] = value; - } - - // ROOT KEY - - #[inline(always)] - pub fn root_key(&mut self, mut key: Variable) -> Variable { - let index = key.index() as usize; - - while let Some(redirect) = self.redirects[key.index() as usize].into_variable() { - key = redirect; - } - - if index != key.index() as usize { - self.redirects[index] = OptVariable::from(key); - } - - key - } - - #[inline(always)] - pub fn root_key_without_compacting(&self, mut key: Variable) -> Variable { - while let Some(redirect) = self.redirects[key.index() as usize].into_variable() { - key = redirect; - } - - key - } - - pub fn snapshot(&self) -> Snapshot { - Snapshot(self.clone()) - } - - pub fn rollback_to(&mut self, snapshot: Snapshot) { - *self = snapshot.0; - } - - pub fn vars_since_snapshot(&self, snapshot: &Snapshot) -> std::ops::Range { - unsafe { - let start = Variable::from_index(snapshot.0.len() as u32); - let end = Variable::from_index(self.len() as u32); - - start..end - } - } - - pub fn is_redirect(&self, key: Variable) -> bool { - self.redirects[key.index() as usize].is_some() - } - - pub fn unioned(&mut self, a: Variable, b: Variable) -> bool { - self.root_key(a) == self.root_key(b) - } - - // custom very specific helpers - #[inline(always)] - pub fn get_rank_set_mark(&mut self, key: Variable, mark: Mark) -> Rank { - let index = self.root_key(key).index() as usize; - - self.marks[index] = mark; - - self.ranks[index] - } - - // TODO remove - #[inline(always)] - pub fn inlined_get_root_key(&mut self, key: Variable) -> Variable { - self.root_key(key) - } - - /// NOTE: assumes variables are root - pub fn unify_roots(&mut self, to: Variable, from: Variable, desc: Descriptor) { - let from_index = from.index() as usize; - let to_index = to.index() as usize; - - // redirect from -> to - if from_index != to_index { - self.redirects[from_index] = OptVariable::from(to); - } - - // update to's Descriptor - self.contents[to_index] = desc.content; - self.ranks[to_index] = desc.rank; - self.marks[to_index] = desc.mark; - self.copies[to_index] = desc.copy; - } - - pub fn get_descriptor(&self, key: Variable) -> Descriptor { - let index = self.root_key_without_compacting(key).index() as usize; - - Descriptor { - content: self.contents[index], - rank: self.ranks[index], - mark: self.marks[index], - copy: self.copies[index], - } - } - - pub fn set_descriptor(&mut self, key: Variable, desc: Descriptor) { - let index = self.root_key(key).index() as usize; - - self.contents[index] = desc.content; - self.ranks[index] = desc.rank; - self.marks[index] = desc.mark; - self.copies[index] = desc.copy; - } - - pub(crate) fn serialize( - &self, - writer: &mut impl std::io::Write, - mut written: usize, - ) -> std::io::Result { - use crate::subs::Subs; - - written = Subs::serialize_slice(&self.contents, writer, written)?; - written = Subs::serialize_slice(&self.ranks, writer, written)?; - written = Subs::serialize_slice(&self.marks, writer, written)?; - written = Subs::serialize_slice(&self.copies, writer, written)?; - written = Subs::serialize_slice(&self.redirects, writer, written)?; - - Ok(written) - } - - pub(crate) fn deserialize(bytes: &[u8], length: usize, offset: usize) -> (Self, usize) { - use crate::subs::Subs; - - let (contents, offset) = Subs::deserialize_slice::(bytes, length, offset); - let (ranks, offset) = Subs::deserialize_slice::(bytes, length, offset); - let (marks, offset) = Subs::deserialize_slice::(bytes, length, offset); - let (copies, offset) = Subs::deserialize_slice::(bytes, length, offset); - let (redirects, offset) = Subs::deserialize_slice::(bytes, length, offset); - - let this = Self { - contents: contents.to_vec(), - ranks: ranks.to_vec(), - marks: marks.to_vec(), - copies: copies.to_vec(), - redirects: redirects.to_vec(), - }; - - (this, offset) - } -} diff --git a/compiler/unify/Cargo.toml b/compiler/unify/Cargo.toml deleted file mode 100644 index 3cb709c120..0000000000 --- a/compiler/unify/Cargo.toml +++ /dev/null @@ -1,24 +0,0 @@ -[package] -authors = ["The Roc Contributors"] -edition = "2021" -license = "UPL-1.0" -name = "roc_unify" -version = "0.1.0" - -[dependencies] -bitflags = "1.3.2" - -[dependencies.roc_collections] -path = "../collections" - -[dependencies.roc_error_macros] -path = "../../error_macros" - -[dependencies.roc_module] -path = "../module" - -[dependencies.roc_types] -path = "../types" - -[dependencies.roc_debug_flags] -path = "../debug_flags" diff --git a/compiler/unify/src/lib.rs b/compiler/unify/src/lib.rs deleted file mode 100644 index 06addd9f5b..0000000000 --- a/compiler/unify/src/lib.rs +++ /dev/null @@ -1,5 +0,0 @@ -#![warn(clippy::dbg_macro)] -// See github.com/rtfeldman/roc/issues/800 for discussion of the large_enum_variant check. -#![allow(clippy::large_enum_variant)] - -pub mod unify; diff --git a/compiler/unify/src/unify.rs b/compiler/unify/src/unify.rs deleted file mode 100644 index 65b5571bb5..0000000000 --- a/compiler/unify/src/unify.rs +++ /dev/null @@ -1,2361 +0,0 @@ -use bitflags::bitflags; -use roc_debug_flags::dbg_do; -#[cfg(debug_assertions)] -use roc_debug_flags::{ROC_PRINT_MISMATCHES, ROC_PRINT_UNIFICATIONS}; -use roc_error_macros::internal_error; -use roc_module::ident::{Lowercase, TagName}; -use roc_module::symbol::Symbol; -use roc_types::num::NumericRange; -use roc_types::subs::Content::{self, *}; -use roc_types::subs::{ - AliasVariables, Descriptor, ErrorTypeContext, FlatType, GetSubsSlice, LambdaSet, Mark, - OptVariable, RecordFields, Subs, SubsIndex, SubsSlice, UlsOfVar, UnionLabels, UnionLambdas, - UnionTags, Variable, VariableSubsSlice, -}; -use roc_types::types::{AliasKind, DoesNotImplementAbility, ErrorType, Mismatch, RecordField, Uls}; - -macro_rules! mismatch { - () => {{ - dbg_do!(ROC_PRINT_MISMATCHES, { - eprintln!( - "Mismatch in {} Line {} Column {}", - file!(), - line!(), - column!() - ); - }); - - Outcome { - mismatches: vec![Mismatch::TypeMismatch], - ..Outcome::default() - } - }}; - ($msg:expr) => {{ - dbg_do!(ROC_PRINT_MISMATCHES, { - eprintln!( - "Mismatch in {} Line {} Column {}", - file!(), - line!(), - column!() - ); - eprintln!($msg); - eprintln!(""); - }); - - Outcome { - mismatches: vec![Mismatch::TypeMismatch], - ..Outcome::default() - } - }}; - ($msg:expr,) => {{ - mismatch!($msg) - }}; - ($msg:expr, $($arg:tt)*) => {{ - dbg_do!(ROC_PRINT_MISMATCHES, { - eprintln!( - "Mismatch in {} Line {} Column {}", - file!(), - line!(), - column!() - ); - eprintln!($msg, $($arg)*); - eprintln!(""); - }); - - Outcome { - mismatches: vec![Mismatch::TypeMismatch], - ..Outcome::default() - } - }}; - (%not_able, $var:expr, $ability:expr, $msg:expr, $($arg:tt)*) => {{ - dbg_do!(ROC_PRINT_MISMATCHES, { - eprintln!( - "Mismatch in {} Line {} Column {}", - file!(), - line!(), - column!() - ); - eprintln!($msg, $($arg)*); - eprintln!(""); - }); - - Outcome { - mismatches: vec![Mismatch::TypeMismatch, Mismatch::DoesNotImplementAbiity($var, $ability)], - ..Outcome::default() - } - }} -} - -type Pool = Vec; - -bitflags! { - pub struct Mode : u8 { - /// Instructs the unifier to solve two types for equality. - /// - /// For example, { n : Str }a ~ { n: Str, m : Str } will solve "a" to "{ m : Str }". - const EQ = 1 << 0; - /// Instructs the unifier to treat the right-hand-side of a constraint as - /// present in the left-hand-side, rather than strictly equal. - /// - /// For example, t1 += [A Str] says we should "add" the tag "A Str" to the type of "t1". - const PRESENT = 1 << 1; - } -} - -impl Mode { - fn is_eq(&self) -> bool { - debug_assert!(!self.contains(Mode::EQ | Mode::PRESENT)); - self.contains(Mode::EQ) - } - - fn is_present(&self) -> bool { - debug_assert!(!self.contains(Mode::EQ | Mode::PRESENT)); - self.contains(Mode::PRESENT) - } - - fn as_eq(self) -> Self { - (self - Mode::PRESENT) | Mode::EQ - } - - #[cfg(debug_assertions)] - fn pretty_print(&self) -> &str { - if self.contains(Mode::EQ) { - "~" - } else if self.contains(Mode::PRESENT) { - "+=" - } else { - unreachable!("Bad mode!") - } - } -} - -#[derive(Debug)] -pub struct Context { - first: Variable, - first_desc: Descriptor, - second: Variable, - second_desc: Descriptor, - mode: Mode, -} - -#[derive(Debug)] -pub enum Unified { - Success { - vars: Pool, - must_implement_ability: MustImplementConstraints, - lambda_sets_to_specialize: UlsOfVar, - }, - Failure(Pool, ErrorType, ErrorType, DoesNotImplementAbility), - BadType(Pool, roc_types::types::Problem), -} - -impl Unified { - pub fn expect_success( - self, - err_msg: &'static str, - ) -> (Pool, MustImplementConstraints, UlsOfVar) { - match self { - Unified::Success { - vars, - must_implement_ability, - lambda_sets_to_specialize, - } => (vars, must_implement_ability, lambda_sets_to_specialize), - _ => internal_error!("{}", err_msg), - } - } -} - -/// Type obligated to implement an ability. -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] -pub enum Obligated { - /// Opaque types can either define custom implementations for an ability, or ask the compiler - /// to generate an implementation of a builtin ability for them. In any case they have unique - /// obligation rules for abilities. - Opaque(Symbol), - /// A structural type for which the compiler can at most generate an adhoc implementation of - /// a builtin ability. - Adhoc(Variable), -} - -/// Specifies that `type` must implement the ability `ability`. -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] -pub struct MustImplementAbility { - pub typ: Obligated, - pub ability: Symbol, -} - -#[derive(Debug, Clone, PartialEq, Eq, Default)] -pub struct MustImplementConstraints(Vec); - -impl MustImplementConstraints { - pub fn push(&mut self, must_implement: MustImplementAbility) { - self.0.push(must_implement) - } - - pub fn extend(&mut self, other: Self) { - self.0.extend(other.0) - } - - pub fn is_empty(&self) -> bool { - self.0.is_empty() - } - - pub fn get_unique(mut self) -> Vec { - self.0.sort(); - self.0.dedup(); - self.0 - } - - pub fn iter_for_ability(&self, ability: Symbol) -> impl Iterator { - self.0.iter().filter(move |mia| mia.ability == ability) - } -} - -#[derive(Debug, Default)] -pub struct Outcome { - mismatches: Vec, - /// We defer these checks until the end of a solving phase. - /// NOTE: this vector is almost always empty! - must_implement_ability: MustImplementConstraints, - /// We defer resolution of these lambda sets to the caller of [unify]. - /// See also [merge_flex_able_with_concrete]. - lambda_sets_to_specialize: UlsOfVar, -} - -impl Outcome { - fn union(&mut self, other: Self) { - self.mismatches.extend(other.mismatches); - self.must_implement_ability - .extend(other.must_implement_ability); - self.lambda_sets_to_specialize - .union(other.lambda_sets_to_specialize); - } -} - -#[inline(always)] -pub fn unify(subs: &mut Subs, var1: Variable, var2: Variable, mode: Mode) -> Unified { - let mut vars = Vec::new(); - let Outcome { - mismatches, - must_implement_ability, - lambda_sets_to_specialize, - } = unify_pool(subs, &mut vars, var1, var2, mode); - - if mismatches.is_empty() { - Unified::Success { - vars, - must_implement_ability, - lambda_sets_to_specialize, - } - } else { - let error_context = if mismatches.contains(&Mismatch::TypeNotInRange) { - ErrorTypeContext::ExpandRanges - } else { - ErrorTypeContext::None - }; - - let (type1, mut problems) = subs.var_to_error_type_contextual(var1, error_context); - let (type2, problems2) = subs.var_to_error_type_contextual(var2, error_context); - - problems.extend(problems2); - - subs.union(var1, var2, Content::Error.into()); - - if !problems.is_empty() { - Unified::BadType(vars, problems.remove(0)) - } else { - let do_not_implement_ability = mismatches - .into_iter() - .filter_map(|mismatch| match mismatch { - Mismatch::DoesNotImplementAbiity(var, ab) => { - let (err_type, _new_problems) = - subs.var_to_error_type_contextual(var, error_context); - Some((err_type, ab)) - } - _ => None, - }) - .collect(); - - Unified::Failure(vars, type1, type2, do_not_implement_ability) - } - } -} - -#[inline(always)] -pub fn unify_pool( - subs: &mut Subs, - pool: &mut Pool, - var1: Variable, - var2: Variable, - mode: Mode, -) -> Outcome { - if subs.equivalent(var1, var2) { - Outcome::default() - } else { - let ctx = Context { - first: var1, - first_desc: subs.get(var1), - second: var2, - second_desc: subs.get(var2), - mode, - }; - - unify_context(subs, pool, ctx) - } -} - -/// Set `ROC_PRINT_UNIFICATIONS` in debug runs to print unifications as they start and complete as -/// a tree to stderr. -/// NOTE: Only run this on individual tests! Run on multiple threads, this would clobber each others' output. -#[cfg(debug_assertions)] -fn debug_print_unified_types(subs: &mut Subs, ctx: &Context, opt_outcome: Option<&Outcome>) { - use roc_types::subs::SubsFmtContent; - - static mut UNIFICATION_DEPTH: usize = 0; - - dbg_do!(ROC_PRINT_UNIFICATIONS, { - let prefix = match opt_outcome { - None => "❔", - Some(outcome) if outcome.mismatches.is_empty() => "✅", - Some(_) => "❌", - }; - - let depth = unsafe { UNIFICATION_DEPTH }; - let indent = 2; - let (use_depth, new_depth) = if opt_outcome.is_none() { - (depth, depth + indent) - } else { - (depth - indent, depth - indent) - }; - - // NOTE: names are generated here (when creating an error type) and that modifies names - // generated by pretty_print.rs. So many test will fail with changes in variable names when - // this block runs. - // let (type1, _problems1) = subs.var_to_error_type(ctx.first); - // let (type2, _problems2) = subs.var_to_error_type(ctx.second); - // println!("\n --------------- \n"); - // dbg!(ctx.first, type1); - // println!("\n --- \n"); - // dbg!(ctx.second, type2); - // println!("\n --------------- \n"); - let content_1 = subs.get(ctx.first).content; - let content_2 = subs.get(ctx.second).content; - let mode = ctx.mode.pretty_print(); - eprintln!( - "{}{}({:?}-{:?}): {:?} {:?} {} {:?} {:?}", - " ".repeat(use_depth), - prefix, - subs.get_root_key_without_compacting(ctx.first), - subs.get_root_key_without_compacting(ctx.second), - ctx.first, - SubsFmtContent(&content_1, subs), - mode, - ctx.second, - SubsFmtContent(&content_2, subs), - ); - - unsafe { UNIFICATION_DEPTH = new_depth }; - }) -} - -fn unify_context(subs: &mut Subs, pool: &mut Pool, ctx: Context) -> Outcome { - #[cfg(debug_assertions)] - debug_print_unified_types(subs, &ctx, None); - - // This #[allow] is needed in release builds, where `result` is no longer used. - #[allow(clippy::let_and_return)] - let result = match &ctx.first_desc.content { - FlexVar(opt_name) => unify_flex(subs, &ctx, opt_name, &ctx.second_desc.content), - FlexAbleVar(opt_name, ability) => { - unify_flex_able(subs, &ctx, opt_name, *ability, &ctx.second_desc.content) - } - RecursionVar { - opt_name, - structure, - } => unify_recursion( - subs, - pool, - &ctx, - opt_name, - *structure, - &ctx.second_desc.content, - ), - RigidVar(name) => unify_rigid(subs, &ctx, name, &ctx.second_desc.content), - RigidAbleVar(name, ability) => { - unify_rigid_able(subs, &ctx, name, *ability, &ctx.second_desc.content) - } - Structure(flat_type) => { - unify_structure(subs, pool, &ctx, flat_type, &ctx.second_desc.content) - } - Alias(symbol, args, real_var, AliasKind::Structural) => { - unify_alias(subs, pool, &ctx, *symbol, *args, *real_var) - } - Alias(symbol, args, real_var, AliasKind::Opaque) => { - unify_opaque(subs, pool, &ctx, *symbol, *args, *real_var) - } - LambdaSet(lset) => unify_lambda_set(subs, pool, &ctx, *lset, &ctx.second_desc.content), - &RangedNumber(typ, range_vars) => unify_ranged_number(subs, pool, &ctx, typ, range_vars), - Error => { - // Error propagates. Whatever we're comparing it to doesn't matter! - merge(subs, &ctx, Error) - } - }; - - #[cfg(debug_assertions)] - debug_print_unified_types(subs, &ctx, Some(&result)); - - result -} - -#[inline(always)] -fn unify_ranged_number( - subs: &mut Subs, - pool: &mut Pool, - ctx: &Context, - real_var: Variable, - range_vars: NumericRange, -) -> Outcome { - let other_content = &ctx.second_desc.content; - - let outcome = match other_content { - FlexVar(_) => { - // Ranged number wins - merge(subs, ctx, RangedNumber(real_var, range_vars)) - } - RecursionVar { .. } - | RigidVar(..) - | Alias(..) - | Structure(..) - | RigidAbleVar(..) - | FlexAbleVar(..) => unify_pool(subs, pool, real_var, ctx.second, ctx.mode), - &RangedNumber(other_real_var, other_range_vars) => { - let outcome = unify_pool(subs, pool, real_var, other_real_var, ctx.mode); - if outcome.mismatches.is_empty() { - check_valid_range(subs, ctx.first, other_range_vars) - } else { - outcome - } - // TODO: We should probably check that "range_vars" and "other_range_vars" intersect - } - LambdaSet(..) => mismatch!(), - Error => merge(subs, ctx, Error), - }; - - if !outcome.mismatches.is_empty() { - return outcome; - } - - check_valid_range(subs, ctx.second, range_vars) -} - -fn check_valid_range(subs: &mut Subs, var: Variable, range: NumericRange) -> Outcome { - let content = subs.get_content_without_compacting(var); - - match content { - &Content::Alias(symbol, _, actual, _) => { - match range.contains_symbol(symbol) { - None => { - // symbol not recognized; go into the alias - return check_valid_range(subs, actual, range); - } - Some(false) => { - let outcome = Outcome { - mismatches: vec![Mismatch::TypeNotInRange], - must_implement_ability: Default::default(), - lambda_sets_to_specialize: Default::default(), - }; - - return outcome; - } - Some(true) => { /* fall through */ } - } - } - - Content::RangedNumber(_, _) => { - // these ranges always intersect, we need more information before we can say more - } - - _ => { - // anything else is definitely a type error, and will be reported elsewhere - } - } - - Outcome::default() -} - -#[inline(always)] -#[allow(clippy::too_many_arguments)] -fn unify_two_aliases( - subs: &mut Subs, - pool: &mut Pool, - ctx: &Context, - // _symbol has an underscore because it's unused in --release builds - _symbol: Symbol, - args: AliasVariables, - real_var: Variable, - other_args: AliasVariables, - other_real_var: Variable, - other_content: &Content, -) -> Outcome { - if args.len() == other_args.len() { - let mut outcome = Outcome::default(); - let it = args - .all_variables() - .into_iter() - .zip(other_args.all_variables().into_iter()); - - let length_before = subs.len(); - - for (l, r) in it { - let l_var = subs[l]; - let r_var = subs[r]; - outcome.union(unify_pool(subs, pool, l_var, r_var, ctx.mode)); - } - - if outcome.mismatches.is_empty() { - outcome.union(merge(subs, ctx, *other_content)); - } - - let length_after = subs.len(); - - let args_unification_had_changes = length_after != length_before; - - if !args.is_empty() && args_unification_had_changes && outcome.mismatches.is_empty() { - // We need to unify the real vars because unification of type variables - // may have made them larger, which then needs to be reflected in the `real_var`. - outcome.union(unify_pool(subs, pool, real_var, other_real_var, ctx.mode)); - } - - outcome - } else { - dbg!(args.len(), other_args.len()); - mismatch!("{:?}", _symbol) - } -} - -// Unifies a structural alias -#[inline(always)] -fn unify_alias( - subs: &mut Subs, - pool: &mut Pool, - ctx: &Context, - symbol: Symbol, - args: AliasVariables, - real_var: Variable, -) -> Outcome { - let other_content = &ctx.second_desc.content; - - let kind = AliasKind::Structural; - - match other_content { - FlexVar(_) => { - // Alias wins - merge(subs, ctx, Alias(symbol, args, real_var, kind)) - } - RecursionVar { structure, .. } => unify_pool(subs, pool, real_var, *structure, ctx.mode), - RigidVar(_) | RigidAbleVar(..) | FlexAbleVar(..) => { - unify_pool(subs, pool, real_var, ctx.second, ctx.mode) - } - Alias(_, _, _, AliasKind::Opaque) => unify_pool(subs, pool, real_var, ctx.second, ctx.mode), - Alias(other_symbol, other_args, other_real_var, AliasKind::Structural) => { - if symbol == *other_symbol { - unify_two_aliases( - subs, - pool, - ctx, - symbol, - args, - real_var, - *other_args, - *other_real_var, - other_content, - ) - } else { - unify_pool(subs, pool, real_var, *other_real_var, ctx.mode) - } - } - Structure(_) => unify_pool(subs, pool, real_var, ctx.second, ctx.mode), - RangedNumber(other_real_var, other_range_vars) => { - let outcome = unify_pool(subs, pool, real_var, *other_real_var, ctx.mode); - if outcome.mismatches.is_empty() { - check_valid_range(subs, real_var, *other_range_vars) - } else { - outcome - } - } - LambdaSet(..) => mismatch!("cannot unify alias {:?} with lambda set {:?}: lambda sets should never be directly behind an alias!", ctx.first, other_content), - Error => merge(subs, ctx, Error), - } -} - -#[inline(always)] -fn unify_opaque( - subs: &mut Subs, - pool: &mut Pool, - ctx: &Context, - symbol: Symbol, - args: AliasVariables, - real_var: Variable, -) -> Outcome { - let other_content = &ctx.second_desc.content; - - let kind = AliasKind::Opaque; - - match other_content { - FlexVar(_) => { - // Alias wins - merge(subs, ctx, Alias(symbol, args, real_var, kind)) - } - FlexAbleVar(_, ability) if args.is_empty() => { - // Opaque type wins - merge_flex_able_with_concrete( - subs, - ctx, - ctx.second, - *ability, - Alias(symbol, args, real_var, kind), - Obligated::Opaque(symbol), - ) - } - Alias(_, _, other_real_var, AliasKind::Structural) => { - unify_pool(subs, pool, ctx.first, *other_real_var, ctx.mode) - } - Alias(other_symbol, other_args, other_real_var, AliasKind::Opaque) => { - // Opaques types are only equal if the opaque symbols are equal! - if symbol == *other_symbol { - unify_two_aliases( - subs, - pool, - ctx, - symbol, - args, - real_var, - *other_args, - *other_real_var, - other_content, - ) - } else { - mismatch!("{:?}", symbol) - } - } - RangedNumber(other_real_var, other_range_vars) => { - // This opaque might be a number, check if it unifies with the target ranged number var. - let outcome = unify_pool(subs, pool, ctx.first, *other_real_var, ctx.mode); - if outcome.mismatches.is_empty() { - check_valid_range(subs, ctx.first, *other_range_vars) - } else { - outcome - } - } - // _other has an underscore because it's unused in --release builds - _other => { - // The type on the left is an opaque, but the one on the right is not! - mismatch!("Cannot unify opaque {:?} with {:?}", symbol, _other) - } - } -} - -#[inline(always)] -fn unify_structure( - subs: &mut Subs, - pool: &mut Pool, - ctx: &Context, - flat_type: &FlatType, - other: &Content, -) -> Outcome { - match other { - FlexVar(_) => { - // If the other is flex, Structure wins! - let outcome = merge(subs, ctx, Structure(*flat_type)); - - // And if we see a flex variable on the right hand side of a presence - // constraint, we know we need to open up the structure we're trying to unify with. - match (ctx.mode.is_present(), flat_type) { - (true, FlatType::TagUnion(tags, _ext)) => { - let new_ext = subs.fresh_unnamed_flex_var(); - let mut new_desc = ctx.first_desc; - new_desc.content = Structure(FlatType::TagUnion(*tags, new_ext)); - subs.set(ctx.first, new_desc); - } - (true, FlatType::FunctionOrTagUnion(tn, sym, _ext)) => { - let new_ext = subs.fresh_unnamed_flex_var(); - let mut new_desc = ctx.first_desc; - new_desc.content = Structure(FlatType::FunctionOrTagUnion(*tn, *sym, new_ext)); - subs.set(ctx.first, new_desc); - } - _ => {} - } - outcome - } - FlexAbleVar(_, ability) => { - // Structure wins - merge_flex_able_with_concrete( - subs, - ctx, - ctx.second, - *ability, - Structure(*flat_type), - Obligated::Adhoc(ctx.first), - ) - } - // _name has an underscore because it's unused in --release builds - RigidVar(_name) => { - // Type mismatch! Rigid can only unify with flex. - mismatch!( - "trying to unify {:?} with rigid var {:?}", - &flat_type, - _name - ) - } - RigidAbleVar(_, _ability) => { - mismatch!( - %not_able, ctx.first, *_ability, - "trying to unify {:?} with RigidAble {:?}", - &flat_type, - &other - ) - } - RecursionVar { structure, .. } => match flat_type { - FlatType::TagUnion(_, _) => { - // unify the structure with this unrecursive tag union - let mut outcome = unify_pool(subs, pool, ctx.first, *structure, ctx.mode); - - if outcome.mismatches.is_empty() { - outcome.union(fix_tag_union_recursion_variable( - subs, ctx, ctx.first, other, - )); - } - - outcome - } - FlatType::RecursiveTagUnion(rec, _, _) => { - debug_assert!(is_recursion_var(subs, *rec)); - // unify the structure with this recursive tag union - unify_pool(subs, pool, ctx.first, *structure, ctx.mode) - } - FlatType::FunctionOrTagUnion(_, _, _) => { - // unify the structure with this unrecursive tag union - let mut outcome = unify_pool(subs, pool, ctx.first, *structure, ctx.mode); - - if outcome.mismatches.is_empty() { - outcome.union(fix_tag_union_recursion_variable( - subs, ctx, ctx.first, other, - )); - } - - outcome - } - // Only tag unions can be recursive; everything else is an error. - _ => mismatch!( - "trying to unify {:?} with recursive type var {:?}", - &flat_type, - structure - ), - }, - - Structure(ref other_flat_type) => { - // Unify the two flat types - unify_flat_type(subs, pool, ctx, flat_type, other_flat_type) - } - // _sym has an underscore because it's unused in --release builds - Alias(_sym, _, real_var, kind) => match kind { - AliasKind::Structural => { - // NB: not treating this as a presence constraint seems pivotal! I - // can't quite figure out why, but it doesn't seem to impact other types. - unify_pool(subs, pool, ctx.first, *real_var, ctx.mode.as_eq()) - } - AliasKind::Opaque => { - mismatch!( - "Cannot unify structure {:?} with opaque {:?}", - &flat_type, - _sym - ) - } - }, - LambdaSet(..) => { - mismatch!( - "Cannot unify structure \n{:?} \nwith lambda set\n {:?}", - roc_types::subs::SubsFmtContent(&Content::Structure(*flat_type), subs), - roc_types::subs::SubsFmtContent(other, subs), - // &flat_type, - // other - ) - } - RangedNumber(other_real_var, other_range_vars) => { - let outcome = unify_pool(subs, pool, ctx.first, *other_real_var, ctx.mode); - if outcome.mismatches.is_empty() { - check_valid_range(subs, ctx.first, *other_range_vars) - } else { - outcome - } - } - Error => merge(subs, ctx, Error), - } -} - -#[inline(always)] -fn unify_lambda_set( - subs: &mut Subs, - pool: &mut Pool, - ctx: &Context, - lambda_set: LambdaSet, - other: &Content, -) -> Outcome { - match other { - FlexVar(_) => merge(subs, ctx, Content::LambdaSet(lambda_set)), - Content::LambdaSet(other_lambda_set) => { - unify_lambda_set_help(subs, pool, ctx, lambda_set, *other_lambda_set) - } - RecursionVar { structure, .. } => { - // suppose that the recursion var is a lambda set - unify_pool(subs, pool, ctx.first, *structure, ctx.mode) - } - RigidVar(..) | RigidAbleVar(..) => mismatch!("Lambda sets never unify with rigid"), - FlexAbleVar(..) => mismatch!("Lambda sets should never have abilities attached to them"), - Structure(..) => mismatch!("Lambda set cannot unify with non-lambda set structure"), - RangedNumber(..) => mismatch!("Lambda sets are never numbers"), - Alias(..) => mismatch!("Lambda set can never be directly under an alias!"), - Error => merge(subs, ctx, Error), - } -} - -fn unify_lambda_set_help( - subs: &mut Subs, - pool: &mut Pool, - ctx: &Context, - lset1: self::LambdaSet, - lset2: self::LambdaSet, -) -> Outcome { - // LambdaSets unify like TagUnions, but can grow unbounded regardless of the extension - // variable. - - let LambdaSet { - solved: solved1, - recursion_var: rec1, - unspecialized: uls1, - } = lset1; - let LambdaSet { - solved: solved2, - recursion_var: rec2, - unspecialized: uls2, - } = lset2; - - debug_assert!( - (rec1.into_variable().into_iter()) - .chain(rec2.into_variable().into_iter()) - .all(|v| is_recursion_var(subs, v)), - "Recursion var is present, but it doesn't have a recursive content!" - ); - - let Separate { - only_in_1, - only_in_2, - in_both, - } = separate_union_lambdas(subs, solved1, solved2); - - let num_shared = in_both.len(); - - let mut joined_lambdas = vec![]; - for (tag_name, (vars1, vars2)) in in_both { - let mut matching_vars = vec![]; - - if vars1.len() != vars2.len() { - continue; // this is a type mismatch; not adding the tag will trigger it below. - } - - let num_vars = vars1.len(); - for (var1, var2) in (vars1.into_iter()).zip(vars2.into_iter()) { - let (var1, var2) = (subs[var1], subs[var2]); - - // Lambda sets are effectively tags under another name, and their usage can also result - // in the arguments of a lambda name being recursive. It very well may happen that - // during unification, a lambda set previously marked as not recursive becomes - // recursive. See the docs of [LambdaSet] for one example, or https://github.com/rtfeldman/roc/pull/2307. - // - // Like with tag unions, if it has, we'll always pass through this branch. So, take - // this opportunity to promote the lambda set to recursive if need be. - maybe_mark_union_recursive(subs, var1); - maybe_mark_union_recursive(subs, var2); - - let outcome = unify_pool(subs, pool, var1, var2, ctx.mode); - - if outcome.mismatches.is_empty() { - matching_vars.push(var1); - } - } - - if matching_vars.len() == num_vars { - joined_lambdas.push((tag_name, matching_vars)); - } - } - - if joined_lambdas.len() == num_shared { - let all_lambdas = joined_lambdas; - let all_lambdas = merge_sorted( - all_lambdas, - only_in_1.into_iter().map(|(name, subs_slice)| { - let vec = subs.get_subs_slice(subs_slice).to_vec(); - (name, vec) - }), - ); - let all_lambdas = merge_sorted( - all_lambdas, - only_in_2.into_iter().map(|(name, subs_slice)| { - let vec = subs.get_subs_slice(subs_slice).to_vec(); - (name, vec) - }), - ); - - let recursion_var = match (rec1.into_variable(), rec2.into_variable()) { - // Prefer left when it's available. - (Some(rec), _) | (_, Some(rec)) => OptVariable::from(rec), - (None, None) => OptVariable::NONE, - }; - - // Combine the unspecialized lambda sets as needed. Note that we don't need to update the - // bookkeeping of variable -> lambda set to be resolved, because if we had v1 -> lset1, and - // now lset1 ~ lset2, then afterward either lset1 still resolves to itself or re-points to - // lset2. In either case the merged unspecialized lambda sets will be there. - let merged_unspecialized = match (uls1.is_empty(), uls2.is_empty()) { - (true, true) => SubsSlice::default(), - (false, true) => uls1, - (true, false) => uls2, - (false, false) => { - let mut all_uls = (subs.get_subs_slice(uls1).iter()) - .chain(subs.get_subs_slice(uls2)) - .map(|&Uls(var, sym, region)| { - // Take the root key to deduplicate - Uls(subs.get_root_key_without_compacting(var), sym, region) - }) - .collect::>(); - all_uls.sort(); - all_uls.dedup(); - - SubsSlice::extend_new(&mut subs.unspecialized_lambda_sets, all_uls) - } - }; - - let new_solved = UnionLabels::insert_into_subs(subs, all_lambdas); - let new_lambda_set = Content::LambdaSet(LambdaSet { - solved: new_solved, - recursion_var, - unspecialized: merged_unspecialized, - }); - - merge(subs, ctx, new_lambda_set) - } else { - mismatch!( - "Problem with lambda sets: there should be {:?} matching lambda, but only found {:?}", - num_shared, - &joined_lambdas - ) - } -} - -/// Ensures that a non-recursive tag union, when unified with a recursion var to become a recursive -/// tag union, properly contains a recursion variable that recurses on itself. -// -// When might this not be the case? For example, in the code -// -// Indirect : [Indirect ConsList] -// -// ConsList : [Nil, Cons Indirect] -// -// l : ConsList -// l = Cons (Indirect (Cons (Indirect Nil))) -// # ^^^^^^^^^^^^^^^~~~~~~~~~~~~~~~~~~~~~^ region-a -// # ~~~~~~~~~~~~~~~~~~~~~ region-b -// l -// -// Suppose `ConsList` has the expanded type `[Nil, Cons [Indirect ]] as `. -// After unifying the tag application annotated "region-b" with the recursion variable ``, -// the tentative total-type of the application annotated "region-a" would be -// ` = [Nil, Cons [Indirect ]] as `. That is, the type of the recursive tag union -// would be inlined at the site "v", rather than passing through the correct recursion variable -// "rec" first. -// -// This is not incorrect from a type perspective, but causes problems later on for e.g. layout -// determination, which expects recursion variables to be placed correctly. Attempting to detect -// this during layout generation does not work so well because it may be that there *are* recursive -// tag unions that should be inlined, and not pass through recursion variables. So instead, try to -// resolve these cases here. -// -// See tests labeled "issue_2810" for more examples. -fn fix_tag_union_recursion_variable( - subs: &mut Subs, - ctx: &Context, - tag_union_promoted_to_recursive: Variable, - recursion_var: &Content, -) -> Outcome { - debug_assert!(matches!( - subs.get_content_without_compacting(tag_union_promoted_to_recursive), - Structure(FlatType::RecursiveTagUnion(..)) - )); - - let has_recursing_recursive_variable = subs - .occurs_including_recursion_vars(tag_union_promoted_to_recursive) - .is_err(); - - if !has_recursing_recursive_variable { - merge(subs, ctx, *recursion_var) - } else { - Outcome::default() - } -} - -fn unify_record( - subs: &mut Subs, - pool: &mut Pool, - ctx: &Context, - fields1: RecordFields, - ext1: Variable, - fields2: RecordFields, - ext2: Variable, -) -> Outcome { - let (separate, ext1, ext2) = separate_record_fields(subs, fields1, ext1, fields2, ext2); - - let shared_fields = separate.in_both; - - if separate.only_in_1.is_empty() { - if separate.only_in_2.is_empty() { - // these variable will be the empty record, but we must still unify them - let ext_outcome = unify_pool(subs, pool, ext1, ext2, ctx.mode); - - if !ext_outcome.mismatches.is_empty() { - return ext_outcome; - } - - let mut field_outcome = - unify_shared_fields(subs, pool, ctx, shared_fields, OtherFields::None, ext1); - - field_outcome.union(ext_outcome); - - field_outcome - } else { - let only_in_2 = RecordFields::insert_into_subs(subs, separate.only_in_2); - let flat_type = FlatType::Record(only_in_2, ext2); - let sub_record = fresh(subs, pool, ctx, Structure(flat_type)); - let ext_outcome = unify_pool(subs, pool, ext1, sub_record, ctx.mode); - - if !ext_outcome.mismatches.is_empty() { - return ext_outcome; - } - - let mut field_outcome = unify_shared_fields( - subs, - pool, - ctx, - shared_fields, - OtherFields::None, - sub_record, - ); - - field_outcome.union(ext_outcome); - - field_outcome - } - } else if separate.only_in_2.is_empty() { - let only_in_1 = RecordFields::insert_into_subs(subs, separate.only_in_1); - let flat_type = FlatType::Record(only_in_1, ext1); - let sub_record = fresh(subs, pool, ctx, Structure(flat_type)); - let ext_outcome = unify_pool(subs, pool, sub_record, ext2, ctx.mode); - - if !ext_outcome.mismatches.is_empty() { - return ext_outcome; - } - - let mut field_outcome = unify_shared_fields( - subs, - pool, - ctx, - shared_fields, - OtherFields::None, - sub_record, - ); - - field_outcome.union(ext_outcome); - - field_outcome - } else { - let only_in_1 = RecordFields::insert_into_subs(subs, separate.only_in_1); - let only_in_2 = RecordFields::insert_into_subs(subs, separate.only_in_2); - - let other_fields = OtherFields::Other(only_in_1, only_in_2); - - let ext = fresh(subs, pool, ctx, Content::FlexVar(None)); - let flat_type1 = FlatType::Record(only_in_1, ext); - let flat_type2 = FlatType::Record(only_in_2, ext); - - let sub1 = fresh(subs, pool, ctx, Structure(flat_type1)); - let sub2 = fresh(subs, pool, ctx, Structure(flat_type2)); - - let rec1_outcome = unify_pool(subs, pool, ext1, sub2, ctx.mode); - if !rec1_outcome.mismatches.is_empty() { - return rec1_outcome; - } - - let rec2_outcome = unify_pool(subs, pool, sub1, ext2, ctx.mode); - if !rec2_outcome.mismatches.is_empty() { - return rec2_outcome; - } - - let mut field_outcome = - unify_shared_fields(subs, pool, ctx, shared_fields, other_fields, ext); - - field_outcome - .mismatches - .reserve(rec1_outcome.mismatches.len() + rec2_outcome.mismatches.len()); - field_outcome.union(rec1_outcome); - field_outcome.union(rec2_outcome); - - field_outcome - } -} - -enum OtherFields { - None, - Other(RecordFields, RecordFields), -} - -type SharedFields = Vec<(Lowercase, (RecordField, RecordField))>; - -fn unify_shared_fields( - subs: &mut Subs, - pool: &mut Pool, - ctx: &Context, - shared_fields: SharedFields, - other_fields: OtherFields, - ext: Variable, -) -> Outcome { - let mut matching_fields = Vec::with_capacity(shared_fields.len()); - let num_shared_fields = shared_fields.len(); - - for (name, (actual, expected)) in shared_fields { - let local_outcome = unify_pool( - subs, - pool, - actual.into_inner(), - expected.into_inner(), - ctx.mode, - ); - - if local_outcome.mismatches.is_empty() { - use RecordField::*; - - // Unification of optional fields - // - // Demanded does not unify with Optional - // Unifying Required with Demanded => Demanded - // Unifying Optional with Required => Required - // Unifying X with X => X - let actual = match (actual, expected) { - (Demanded(_), Optional(_)) | (Optional(_), Demanded(_)) => { - // this is an error, but we continue to give better error messages - continue; - } - (Demanded(val), Required(_)) - | (Required(val), Demanded(_)) - | (Demanded(val), Demanded(_)) => Demanded(val), - (Required(val), Required(_)) => Required(val), - (Required(val), Optional(_)) => Required(val), - (Optional(val), Required(_)) => Required(val), - (Optional(val), Optional(_)) => Optional(val), - }; - - matching_fields.push((name, actual)); - } - } - - if num_shared_fields == matching_fields.len() { - // pull fields in from the ext_var - - let (ext_fields, new_ext_var) = RecordFields::empty().sorted_iterator_and_ext(subs, ext); - let ext_fields: Vec<_> = ext_fields.into_iter().collect(); - - let fields: RecordFields = match other_fields { - OtherFields::None => { - if ext_fields.is_empty() { - RecordFields::insert_into_subs(subs, matching_fields) - } else { - let all_fields = merge_sorted(matching_fields, ext_fields); - RecordFields::insert_into_subs(subs, all_fields) - } - } - OtherFields::Other(other1, other2) => { - let mut all_fields = merge_sorted(matching_fields, ext_fields); - all_fields = merge_sorted( - all_fields, - other1.iter_all().map(|(i1, i2, i3)| { - let field_name: Lowercase = subs[i1].clone(); - let variable = subs[i2]; - let record_field: RecordField = subs[i3].map(|_| variable); - - (field_name, record_field) - }), - ); - - all_fields = merge_sorted( - all_fields, - other2.iter_all().map(|(i1, i2, i3)| { - let field_name: Lowercase = subs[i1].clone(); - let variable = subs[i2]; - let record_field: RecordField = subs[i3].map(|_| variable); - - (field_name, record_field) - }), - ); - - RecordFields::insert_into_subs(subs, all_fields) - } - }; - - let flat_type = FlatType::Record(fields, new_ext_var); - - merge(subs, ctx, Structure(flat_type)) - } else { - mismatch!("in unify_shared_fields") - } -} - -fn separate_record_fields( - subs: &Subs, - fields1: RecordFields, - ext1: Variable, - fields2: RecordFields, - ext2: Variable, -) -> ( - Separate>, - Variable, - Variable, -) { - let (it1, new_ext1) = fields1.sorted_iterator_and_ext(subs, ext1); - let (it2, new_ext2) = fields2.sorted_iterator_and_ext(subs, ext2); - - let it1 = it1.collect::>(); - let it2 = it2.collect::>(); - - (separate(it1, it2), new_ext1, new_ext2) -} - -#[derive(Debug)] -struct Separate { - only_in_1: Vec<(K, V)>, - only_in_2: Vec<(K, V)>, - in_both: Vec<(K, (V, V))>, -} - -fn merge_sorted(input1: I1, input2: I2) -> Vec<(K, V)> -where - K: Ord, - I1: IntoIterator, - I2: IntoIterator, -{ - use std::cmp::Ordering; - - let mut it1 = input1.into_iter().peekable(); - let mut it2 = input2.into_iter().peekable(); - - let input1_len = it1.size_hint().0; - let input2_len = it2.size_hint().0; - - let mut result = Vec::with_capacity(input1_len + input2_len); - - loop { - let which = match (it1.peek(), it2.peek()) { - (Some((l, _)), Some((r, _))) => Some(l.cmp(r)), - (Some(_), None) => Some(Ordering::Less), - (None, Some(_)) => Some(Ordering::Greater), - (None, None) => None, - }; - - match which { - Some(Ordering::Less) => { - result.push(it1.next().unwrap()); - } - Some(Ordering::Equal) => { - let (k, v) = it1.next().unwrap(); - let (_, _) = it2.next().unwrap(); - result.push((k, v)); - } - Some(Ordering::Greater) => { - result.push(it2.next().unwrap()); - } - None => break, - } - } - - result -} - -fn separate(input1: I1, input2: I2) -> Separate -where - K: Ord, - I1: IntoIterator, - I2: IntoIterator, -{ - use std::cmp::Ordering; - - let mut it1 = input1.into_iter().peekable(); - let mut it2 = input2.into_iter().peekable(); - - let input1_len = it1.size_hint().0; - let input2_len = it2.size_hint().0; - - let max_common = std::cmp::min(input1_len, input2_len); - - let mut result = Separate { - only_in_1: Vec::with_capacity(input1_len), - only_in_2: Vec::with_capacity(input2_len), - in_both: Vec::with_capacity(max_common), - }; - - loop { - let which = match (it1.peek(), it2.peek()) { - (Some((l, _)), Some((r, _))) => Some(l.cmp(r)), - (Some(_), None) => Some(Ordering::Less), - (None, Some(_)) => Some(Ordering::Greater), - (None, None) => None, - }; - - match which { - Some(Ordering::Less) => { - result.only_in_1.push(it1.next().unwrap()); - } - Some(Ordering::Equal) => { - let (k, v1) = it1.next().unwrap(); - let (_, v2) = it2.next().unwrap(); - result.in_both.push((k, (v1, v2))); - } - Some(Ordering::Greater) => { - result.only_in_2.push(it2.next().unwrap()); - } - None => break, - } - } - - result -} - -fn separate_union_tags( - subs: &Subs, - fields1: UnionTags, - ext1: Variable, - fields2: UnionTags, - ext2: Variable, -) -> (Separate, Variable, Variable) { - let (it1, new_ext1) = fields1.sorted_slices_iterator_and_ext(subs, ext1); - let (it2, new_ext2) = fields2.sorted_slices_iterator_and_ext(subs, ext2); - - (separate(it1, it2), new_ext1, new_ext2) -} - -fn separate_union_lambdas( - subs: &Subs, - fields1: UnionLambdas, - fields2: UnionLambdas, -) -> Separate { - debug_assert!(fields1.is_sorted_no_duplicates(subs)); - debug_assert!(fields2.is_sorted_no_duplicates(subs)); - let it1 = fields1.iter_all().map(|(s, vars)| (subs[s], subs[vars])); - let it2 = fields2.iter_all().map(|(s, vars)| (subs[s], subs[vars])); - - separate(it1, it2) -} - -#[derive(Debug, Copy, Clone)] -enum Rec { - None, - Left(Variable), - Right(Variable), - Both(Variable, Variable), -} - -#[allow(clippy::too_many_arguments)] -fn unify_tag_unions( - subs: &mut Subs, - pool: &mut Pool, - ctx: &Context, - tags1: UnionTags, - initial_ext1: Variable, - tags2: UnionTags, - initial_ext2: Variable, - recursion_var: Rec, -) -> Outcome { - let (separate, mut ext1, ext2) = - separate_union_tags(subs, tags1, initial_ext1, tags2, initial_ext2); - - let shared_tags = separate.in_both; - - if let (true, Content::Structure(FlatType::EmptyTagUnion)) = - (ctx.mode.is_present(), subs.get(ext1).content) - { - if !separate.only_in_2.is_empty() { - // Create a new extension variable that we'll fill in with the - // contents of the tag union from our presence contraint. - // - // If there's no new (toplevel) tags we need to register for - // presence, for example in the cases - // [A] += [A] - // [A, B] += [A] - // [A M, B] += [A N] - // then we don't need to create a fresh ext variable, since the - // tag union is definitely not growing on the top level. - // Notice that in the last case - // [A M, B] += [A N] - // the nested tag `A` **will** grow, but we don't need to modify - // the top level extension variable for that! - let new_ext = fresh(subs, pool, ctx, Content::FlexVar(None)); - let new_union = Structure(FlatType::TagUnion(tags1, new_ext)); - let mut new_desc = ctx.first_desc; - new_desc.content = new_union; - subs.set(ctx.first, new_desc); - - ext1 = new_ext; - } - } - - if separate.only_in_1.is_empty() { - if separate.only_in_2.is_empty() { - let ext_outcome = if ctx.mode.is_eq() { - unify_pool(subs, pool, ext1, ext2, ctx.mode) - } else { - // In a presence context, we don't care about ext2 being equal to ext1 - Outcome::default() - }; - - if !ext_outcome.mismatches.is_empty() { - return ext_outcome; - } - - let mut shared_tags_outcome = unify_shared_tags_new( - subs, - pool, - ctx, - shared_tags, - OtherTags2::Empty, - ext1, - recursion_var, - ); - - shared_tags_outcome.union(ext_outcome); - - shared_tags_outcome - } else { - let unique_tags2 = UnionTags::insert_slices_into_subs(subs, separate.only_in_2); - let flat_type = FlatType::TagUnion(unique_tags2, ext2); - let sub_record = fresh(subs, pool, ctx, Structure(flat_type)); - let ext_outcome = unify_pool(subs, pool, ext1, sub_record, ctx.mode); - - if !ext_outcome.mismatches.is_empty() { - return ext_outcome; - } - - let mut shared_tags_outcome = unify_shared_tags_new( - subs, - pool, - ctx, - shared_tags, - OtherTags2::Empty, - sub_record, - recursion_var, - ); - - shared_tags_outcome.union(ext_outcome); - - shared_tags_outcome - } - } else if separate.only_in_2.is_empty() { - let unique_tags1 = UnionTags::insert_slices_into_subs(subs, separate.only_in_1); - let flat_type = FlatType::TagUnion(unique_tags1, ext1); - let sub_record = fresh(subs, pool, ctx, Structure(flat_type)); - - // In a presence context, we don't care about ext2 being equal to tags1 - if ctx.mode.is_eq() { - let ext_outcome = unify_pool(subs, pool, sub_record, ext2, ctx.mode); - - if !ext_outcome.mismatches.is_empty() { - return ext_outcome; - } - } - - unify_shared_tags_new( - subs, - pool, - ctx, - shared_tags, - OtherTags2::Empty, - sub_record, - recursion_var, - ) - } else { - let other_tags = OtherTags2::Union(separate.only_in_1.clone(), separate.only_in_2.clone()); - - let unique_tags1 = UnionTags::insert_slices_into_subs(subs, separate.only_in_1); - let unique_tags2 = UnionTags::insert_slices_into_subs(subs, separate.only_in_2); - - let ext_content = if ctx.mode.is_present() { - Content::Structure(FlatType::EmptyTagUnion) - } else { - Content::FlexVar(None) - }; - let ext = fresh(subs, pool, ctx, ext_content); - let flat_type1 = FlatType::TagUnion(unique_tags1, ext); - let flat_type2 = FlatType::TagUnion(unique_tags2, ext); - - let sub1 = fresh(subs, pool, ctx, Structure(flat_type1)); - let sub2 = fresh(subs, pool, ctx, Structure(flat_type2)); - - // NOTE: for clearer error messages, we rollback unification of the ext vars when either fails - // - // This is inspired by - // - // - // f : [Red, Green] -> Bool - // f = \_ -> True - // - // f Blue - // - // In this case, we want the mismatch to be between `[Blue]a` and `[Red, Green]`, but - // without rolling back, the mismatch is between `[Blue, Red, Green]a` and `[Red, Green]`. - // TODO is this also required for the other cases? - - let snapshot = subs.snapshot(); - - let ext1_outcome = unify_pool(subs, pool, ext1, sub2, ctx.mode); - if !ext1_outcome.mismatches.is_empty() { - subs.rollback_to(snapshot); - return ext1_outcome; - } - - if ctx.mode.is_eq() { - let ext2_outcome = unify_pool(subs, pool, sub1, ext2, ctx.mode); - if !ext2_outcome.mismatches.is_empty() { - subs.rollback_to(snapshot); - return ext2_outcome; - } - } - - subs.commit_snapshot(snapshot); - - unify_shared_tags_new(subs, pool, ctx, shared_tags, other_tags, ext, recursion_var) - } -} - -#[derive(Debug)] -enum OtherTags2 { - Empty, - Union( - Vec<(TagName, VariableSubsSlice)>, - Vec<(TagName, VariableSubsSlice)>, - ), -} - -/// Promotes a non-recursive tag union or lambda set to its recursive variant, if it is found to be -/// recursive. -fn maybe_mark_union_recursive(subs: &mut Subs, union_var: Variable) { - 'outer: while let Err((_, chain)) = subs.occurs(union_var) { - // walk the chain till we find a tag union or lambda set, starting from the variable that - // occurred recursively, which is always at the end of the chain. - for &v in chain.iter().rev() { - let description = subs.get(v); - match description.content { - Content::Structure(FlatType::TagUnion(tags, ext_var)) => { - subs.mark_tag_union_recursive(v, tags, ext_var); - continue 'outer; - } - LambdaSet(self::LambdaSet { - solved, - recursion_var: OptVariable::NONE, - unspecialized, - }) => { - subs.mark_lambda_set_recursive(v, solved, unspecialized); - continue 'outer; - } - _ => { /* fall through */ } - } - } - - // Might not be any tag union if we only pass through `Apply`s. Otherwise, we have a bug! - if chain.iter().all(|&v| { - matches!( - subs.get_content_without_compacting(v), - Content::Structure(FlatType::Apply(..)) - ) - }) { - return; - } else { - internal_error!("recursive loop does not contain a tag union") - } - } -} - -fn unify_shared_tags_new( - subs: &mut Subs, - pool: &mut Pool, - ctx: &Context, - shared_tags: Vec<(TagName, (VariableSubsSlice, VariableSubsSlice))>, - other_tags: OtherTags2, - ext: Variable, - recursion_var: Rec, -) -> Outcome { - let mut matching_tags = Vec::default(); - let num_shared_tags = shared_tags.len(); - - for (name, (actual_vars, expected_vars)) in shared_tags { - let mut matching_vars = Vec::with_capacity(actual_vars.len()); - - let actual_len = actual_vars.len(); - let expected_len = expected_vars.len(); - - for (actual_index, expected_index) in actual_vars.into_iter().zip(expected_vars.into_iter()) - { - let actual = subs[actual_index]; - let expected = subs[expected_index]; - // NOTE the arguments of a tag can be recursive. For instance in the expression - // - // ConsList a : [Nil, Cons a (ConsList a)] - // - // Cons 1 (Cons "foo" Nil) - // - // We need to not just check the outer layer (inferring ConsList Int) - // but also the inner layer (finding a type error, as desired) - // - // This correction introduces the same issue as in https://github.com/elm/compiler/issues/1964 - // Polymorphic recursion is now a type error. - // - // The strategy is to expand the recursive tag union as deeply as the non-recursive one - // is. - // - // > RecursiveTagUnion(rvar, [Cons a rvar, Nil], ext) - // - // Conceptually becomes - // - // > RecursiveTagUnion(rvar, [Cons a [Cons a rvar, Nil], Nil], ext) - // - // and so on until the whole non-recursive tag union can be unified with it. - // - // One thing we have to watch out for is that a tag union we're hoping to - // match a recursive tag union with didn't itself become recursive. If it has, - // since we're expanding tag unions to equal depths as described above, - // we'll always pass through this branch. So, we promote tag unions to recursive - // ones here if it turns out they are that. - maybe_mark_union_recursive(subs, actual); - maybe_mark_union_recursive(subs, expected); - - let mut outcome = Outcome::default(); - - outcome.union(unify_pool(subs, pool, actual, expected, ctx.mode)); - - // clearly, this is very suspicious: these variables have just been unified. And yet, - // not doing this leads to stack overflows - if let Rec::Right(_) = recursion_var { - if outcome.mismatches.is_empty() { - matching_vars.push(expected); - } - } else if outcome.mismatches.is_empty() { - matching_vars.push(actual); - } - } - - // only do this check after unification so the error message has more info - if actual_len == expected_len && actual_len == matching_vars.len() { - matching_tags.push((name, matching_vars)); - } - } - - if num_shared_tags == matching_tags.len() { - // pull fields in from the ext_var - - let (ext_fields, new_ext_var) = UnionTags::default().sorted_iterator_and_ext(subs, ext); - let ext_fields: Vec<_> = ext_fields - .into_iter() - .map(|(label, variables)| (label, variables.to_vec())) - .collect(); - - let new_tags: UnionTags = match other_tags { - OtherTags2::Empty => { - if ext_fields.is_empty() { - UnionTags::insert_into_subs(subs, matching_tags) - } else { - let all_fields = merge_sorted(matching_tags, ext_fields); - UnionTags::insert_into_subs(subs, all_fields) - } - } - OtherTags2::Union(other1, other2) => { - let mut all_fields = merge_sorted(matching_tags, ext_fields); - all_fields = merge_sorted( - all_fields, - other1.into_iter().map(|(field_name, subs_slice)| { - let vec = subs.get_subs_slice(subs_slice).to_vec(); - - (field_name, vec) - }), - ); - - all_fields = merge_sorted( - all_fields, - other2.into_iter().map(|(field_name, subs_slice)| { - let vec = subs.get_subs_slice(subs_slice).to_vec(); - - (field_name, vec) - }), - ); - - UnionTags::insert_into_subs(subs, all_fields) - } - }; - - unify_shared_tags_merge_new(subs, ctx, new_tags, new_ext_var, recursion_var) - } else { - mismatch!( - "Problem with Tag Union\nThere should be {:?} matching tags, but I only got \n{:?}", - num_shared_tags, - &matching_tags - ) - } -} - -fn unify_shared_tags_merge_new( - subs: &mut Subs, - ctx: &Context, - new_tags: UnionTags, - new_ext_var: Variable, - recursion_var: Rec, -) -> Outcome { - let flat_type = match recursion_var { - Rec::None => FlatType::TagUnion(new_tags, new_ext_var), - Rec::Left(rec) | Rec::Right(rec) | Rec::Both(rec, _) => { - debug_assert!(is_recursion_var(subs, rec)); - FlatType::RecursiveTagUnion(rec, new_tags, new_ext_var) - } - }; - - merge(subs, ctx, Structure(flat_type)) -} - -#[inline(always)] -fn unify_flat_type( - subs: &mut Subs, - pool: &mut Pool, - ctx: &Context, - left: &FlatType, - right: &FlatType, -) -> Outcome { - use roc_types::subs::FlatType::*; - - match (left, right) { - (EmptyRecord, EmptyRecord) => merge(subs, ctx, Structure(*left)), - - (Record(fields, ext), EmptyRecord) if fields.has_only_optional_fields(subs) => { - unify_pool(subs, pool, *ext, ctx.second, ctx.mode) - } - - (EmptyRecord, Record(fields, ext)) if fields.has_only_optional_fields(subs) => { - unify_pool(subs, pool, ctx.first, *ext, ctx.mode) - } - - (Record(fields1, ext1), Record(fields2, ext2)) => { - unify_record(subs, pool, ctx, *fields1, *ext1, *fields2, *ext2) - } - - (EmptyTagUnion, EmptyTagUnion) => merge(subs, ctx, Structure(*left)), - - (TagUnion(tags, ext), EmptyTagUnion) if tags.is_empty() => { - unify_pool(subs, pool, *ext, ctx.second, ctx.mode) - } - - (EmptyTagUnion, TagUnion(tags, ext)) if tags.is_empty() => { - unify_pool(subs, pool, ctx.first, *ext, ctx.mode) - } - - (TagUnion(tags1, ext1), TagUnion(tags2, ext2)) => { - unify_tag_unions(subs, pool, ctx, *tags1, *ext1, *tags2, *ext2, Rec::None) - } - - (RecursiveTagUnion(recursion_var, tags1, ext1), TagUnion(tags2, ext2)) => { - debug_assert!(is_recursion_var(subs, *recursion_var)); - // this never happens in type-correct programs, but may happen if there is a type error - - let rec = Rec::Left(*recursion_var); - - unify_tag_unions(subs, pool, ctx, *tags1, *ext1, *tags2, *ext2, rec) - } - - (TagUnion(tags1, ext1), RecursiveTagUnion(recursion_var, tags2, ext2)) => { - debug_assert!(is_recursion_var(subs, *recursion_var)); - - let rec = Rec::Right(*recursion_var); - - unify_tag_unions(subs, pool, ctx, *tags1, *ext1, *tags2, *ext2, rec) - } - - (RecursiveTagUnion(rec1, tags1, ext1), RecursiveTagUnion(rec2, tags2, ext2)) => { - debug_assert!(is_recursion_var(subs, *rec1)); - debug_assert!(is_recursion_var(subs, *rec2)); - - let rec = Rec::Both(*rec1, *rec2); - let mut outcome = unify_tag_unions(subs, pool, ctx, *tags1, *ext1, *tags2, *ext2, rec); - outcome.union(unify_pool(subs, pool, *rec1, *rec2, ctx.mode)); - - outcome - } - - (Apply(l_symbol, l_args), Apply(r_symbol, r_args)) if l_symbol == r_symbol => { - let mut outcome = unify_zip_slices(subs, pool, *l_args, *r_args); - - if outcome.mismatches.is_empty() { - outcome.union(merge(subs, ctx, Structure(Apply(*r_symbol, *r_args)))); - } - - outcome - } - (Func(l_args, l_closure, l_ret), Func(r_args, r_closure, r_ret)) - if l_args.len() == r_args.len() => - { - let arg_outcome = unify_zip_slices(subs, pool, *l_args, *r_args); - let ret_outcome = unify_pool(subs, pool, *l_ret, *r_ret, ctx.mode); - let closure_outcome = unify_pool(subs, pool, *l_closure, *r_closure, ctx.mode); - - let mut outcome = ret_outcome; - - outcome.union(closure_outcome); - outcome.union(arg_outcome); - - if outcome.mismatches.is_empty() { - outcome.union(merge( - subs, - ctx, - Structure(Func(*r_args, *r_closure, *r_ret)), - )); - } - - outcome - } - (FunctionOrTagUnion(tag_name, tag_symbol, ext), Func(args, closure, ret)) => { - unify_function_or_tag_union_and_func( - subs, - pool, - ctx, - tag_name, - *tag_symbol, - *ext, - *args, - *ret, - *closure, - true, - ) - } - (Func(args, closure, ret), FunctionOrTagUnion(tag_name, tag_symbol, ext)) => { - unify_function_or_tag_union_and_func( - subs, - pool, - ctx, - tag_name, - *tag_symbol, - *ext, - *args, - *ret, - *closure, - false, - ) - } - (FunctionOrTagUnion(tag_name_1, _, ext1), FunctionOrTagUnion(tag_name_2, _, ext2)) => { - let tag_name_1_ref = &subs[*tag_name_1]; - let tag_name_2_ref = &subs[*tag_name_2]; - - if tag_name_1_ref == tag_name_2_ref { - let outcome = unify_pool(subs, pool, *ext1, *ext2, ctx.mode); - if outcome.mismatches.is_empty() { - let content = *subs.get_content_without_compacting(ctx.second); - merge(subs, ctx, content) - } else { - outcome - } - } else { - let tags1 = UnionTags::from_tag_name_index(*tag_name_1); - let tags2 = UnionTags::from_tag_name_index(*tag_name_2); - - unify_tag_unions(subs, pool, ctx, tags1, *ext1, tags2, *ext2, Rec::None) - } - } - (TagUnion(tags1, ext1), FunctionOrTagUnion(tag_name, _, ext2)) => { - let tags2 = UnionTags::from_tag_name_index(*tag_name); - - unify_tag_unions(subs, pool, ctx, *tags1, *ext1, tags2, *ext2, Rec::None) - } - (FunctionOrTagUnion(tag_name, _, ext1), TagUnion(tags2, ext2)) => { - let tags1 = UnionTags::from_tag_name_index(*tag_name); - - unify_tag_unions(subs, pool, ctx, tags1, *ext1, *tags2, *ext2, Rec::None) - } - - (RecursiveTagUnion(recursion_var, tags1, ext1), FunctionOrTagUnion(tag_name, _, ext2)) => { - // this never happens in type-correct programs, but may happen if there is a type error - debug_assert!(is_recursion_var(subs, *recursion_var)); - - let tags2 = UnionTags::from_tag_name_index(*tag_name); - let rec = Rec::Left(*recursion_var); - - unify_tag_unions(subs, pool, ctx, *tags1, *ext1, tags2, *ext2, rec) - } - - (FunctionOrTagUnion(tag_name, _, ext1), RecursiveTagUnion(recursion_var, tags2, ext2)) => { - debug_assert!(is_recursion_var(subs, *recursion_var)); - - let tags1 = UnionTags::from_tag_name_index(*tag_name); - let rec = Rec::Right(*recursion_var); - - unify_tag_unions(subs, pool, ctx, tags1, *ext1, *tags2, *ext2, rec) - } - - // these have underscores because they're unused in --release builds - (_other1, _other2) => { - // any other combination is a mismatch - mismatch!( - "Trying to unify two flat types that are incompatible: {:?} ~ {:?}", - roc_types::subs::SubsFmtFlatType(_other1, subs), - roc_types::subs::SubsFmtFlatType(_other2, subs) - ) - } - } -} - -fn unify_zip_slices( - subs: &mut Subs, - pool: &mut Pool, - left: SubsSlice, - right: SubsSlice, -) -> Outcome { - let mut outcome = Outcome::default(); - - let it = left.into_iter().zip(right.into_iter()); - - for (l_index, r_index) in it { - let l_var = subs[l_index]; - let r_var = subs[r_index]; - - outcome.union(unify_pool(subs, pool, l_var, r_var, Mode::EQ)); - } - - outcome -} - -#[inline(always)] -fn unify_rigid( - subs: &mut Subs, - ctx: &Context, - name: &SubsIndex, - other: &Content, -) -> Outcome { - match other { - FlexVar(_) => { - // If the other is flex, rigid wins! - merge(subs, ctx, RigidVar(*name)) - } - FlexAbleVar(_, other_ability) => { - // Mismatch - Rigid can unify with FlexAble only when the Rigid has an ability - // bound as well, otherwise the user failed to correctly annotate the bound. - mismatch!( - %not_able, ctx.first, *other_ability, - "Rigid {:?} with FlexAble {:?}", ctx.first, other - ) - } - - RigidVar(_) - | RigidAbleVar(..) - | RecursionVar { .. } - | Structure(_) - | Alias(..) - | RangedNumber(..) - | LambdaSet(..) => { - // Type mismatch! Rigid can only unify with flex, even if the - // rigid names are the same. - mismatch!("Rigid {:?} with {:?}", ctx.first, &other) - } - - Error => { - // Error propagates. - merge(subs, ctx, Error) - } - } -} - -#[inline(always)] -fn unify_rigid_able( - subs: &mut Subs, - ctx: &Context, - name: &SubsIndex, - ability: Symbol, - other: &Content, -) -> Outcome { - match other { - FlexVar(_) => { - // If the other is flex, rigid wins! - merge(subs, ctx, RigidVar(*name)) - } - FlexAbleVar(_, other_ability) => { - if ability == *other_ability { - // The ability bounds are the same, so rigid wins! - merge(subs, ctx, RigidAbleVar(*name, ability)) - } else { - // Mismatch for now. - // TODO check ability hierarchies. - mismatch!( - %not_able, ctx.second, ability, - "RigidAble {:?} with ability {:?} not compatible with ability {:?}", - ctx.first, - ability, - other_ability - ) - } - } - - RigidVar(_) - | RigidAbleVar(..) - | RecursionVar { .. } - | Structure(_) - | Alias(..) - | RangedNumber(..) - | LambdaSet(..) => { - // Type mismatch! Rigid can only unify with flex, even if the - // rigid names are the same. - mismatch!("Rigid {:?} with {:?}", ctx.first, &other) - } - - Error => { - // Error propagates. - merge(subs, ctx, Error) - } - } -} - -#[inline(always)] -fn unify_flex( - subs: &mut Subs, - ctx: &Context, - opt_name: &Option>, - other: &Content, -) -> Outcome { - match other { - FlexVar(other_opt_name) => { - // Prefer using right's name. - let opt_name = opt_name.or(*other_opt_name); - merge(subs, ctx, FlexVar(opt_name)) - } - - FlexAbleVar(opt_other_name, ability) => { - // Prefer using right's name. - let opt_name = (opt_other_name).or(*opt_name); - merge(subs, ctx, FlexAbleVar(opt_name, *ability)) - } - - RigidVar(_) - | RigidAbleVar(_, _) - | RecursionVar { .. } - | Structure(_) - | Alias(_, _, _, _) - | RangedNumber(..) - | LambdaSet(..) => { - // TODO special-case boolean here - // In all other cases, if left is flex, defer to right. - merge(subs, ctx, *other) - } - - Error => merge(subs, ctx, Error), - } -} - -#[inline(always)] -fn unify_flex_able( - subs: &mut Subs, - ctx: &Context, - opt_name: &Option>, - ability: Symbol, - other: &Content, -) -> Outcome { - match other { - FlexVar(opt_other_name) => { - // Prefer using right's name. - let opt_name = (opt_other_name).or(*opt_name); - merge(subs, ctx, FlexAbleVar(opt_name, ability)) - } - - FlexAbleVar(opt_other_name, other_ability) => { - // Prefer the right's name when possible. - let opt_name = (opt_other_name).or(*opt_name); - - if ability == *other_ability { - merge(subs, ctx, FlexAbleVar(opt_name, ability)) - } else { - // Ability names differ; mismatch for now. - // TODO check ability hierarchies. - mismatch!( - %not_able, ctx.second, ability, - "FlexAble {:?} with ability {:?} not compatible with ability {:?}", - ctx.first, - ability, - other_ability - ) - } - } - - RigidAbleVar(_, other_ability) => { - if ability == *other_ability { - merge(subs, ctx, *other) - } else { - mismatch!(%not_able, ctx.second, ability, "RigidAble {:?} vs {:?}", ability, other_ability) - } - } - - RigidVar(_) => mismatch!("FlexAble can never unify with non-able Rigid"), - RecursionVar { .. } => mismatch!("FlexAble with RecursionVar"), - LambdaSet(..) => mismatch!("FlexAble with LambdaSet"), - - Alias(name, args, _real_var, AliasKind::Opaque) => { - if args.is_empty() { - // Opaque type wins - merge_flex_able_with_concrete( - subs, - ctx, - ctx.first, - ability, - *other, - Obligated::Opaque(*name), - ) - } else { - mismatch!("FlexAble vs Opaque with type vars") - } - } - - Structure(_) | Alias(_, _, _, AliasKind::Structural) | RangedNumber(..) => { - // Structural type wins. - merge_flex_able_with_concrete( - subs, - ctx, - ctx.first, - ability, - *other, - Obligated::Adhoc(ctx.second), - ) - } - - Error => merge(subs, ctx, Error), - } -} - -fn merge_flex_able_with_concrete( - subs: &mut Subs, - ctx: &Context, - flex_able_var: Variable, - ability: Symbol, - concrete_content: Content, - concrete_obligation: Obligated, -) -> Outcome { - let mut outcome = merge(subs, ctx, concrete_content); - let must_implement_ability = MustImplementAbility { - typ: concrete_obligation, - ability, - }; - outcome.must_implement_ability.push(must_implement_ability); - - // Figure which, if any, lambda sets should be specialized thanks to the flex able var - // being instantiated. Now as much as I would love to do that here, we don't, because we might - // be in the middle of solving a module and not resolved all available ability implementations - // yet! Instead we chuck it up in the [Outcome] and let our caller do the resolution. - // - // If we ever organize ability implementations so that they are well-known before any other - // unification is done, they can be solved in-band here! - let uls_of_concrete = subs.remove_dependent_unspecialized_lambda_sets(flex_able_var); - outcome - .lambda_sets_to_specialize - .extend(flex_able_var, uls_of_concrete); - - outcome -} - -#[inline(always)] -fn unify_recursion( - subs: &mut Subs, - pool: &mut Pool, - ctx: &Context, - opt_name: &Option>, - structure: Variable, - other: &Content, -) -> Outcome { - match other { - RecursionVar { - opt_name: other_opt_name, - structure: _other_structure, - } => { - // NOTE: structure and other_structure may not be unified yet, but will be - // we should not do that here, it would create an infinite loop! - let name = (*opt_name).or(*other_opt_name); - merge( - subs, - ctx, - RecursionVar { - opt_name: name, - structure, - }, - ) - } - - Structure(_) => { - // unify the structure variable with this Structure - unify_pool(subs, pool, structure, ctx.second, ctx.mode) - } - RigidVar(_) => { - mismatch!("RecursionVar {:?} with rigid {:?}", ctx.first, &other) - } - - FlexAbleVar(..) | RigidAbleVar(..) => { - mismatch!("RecursionVar {:?} with able var {:?}", ctx.first, &other) - } - - FlexVar(_) => merge( - subs, - ctx, - RecursionVar { - structure, - opt_name: *opt_name, - }, - ), - - // _opaque has an underscore because it's unused in --release builds - Alias(_opaque, _, _, AliasKind::Opaque) => { - mismatch!( - "RecursionVar {:?} cannot be equal to opaque {:?}", - ctx.first, - _opaque - ) - } - - Alias(_, _, actual, _) => { - // look at the type the alias stands for - - unify_pool(subs, pool, ctx.first, *actual, ctx.mode) - } - - RangedNumber(..) => mismatch!( - "RecursionVar {:?} with ranged number {:?}", - ctx.first, - &other - ), - - LambdaSet(..) => { - // suppose that the recursion var is a lambda set - unify_pool(subs, pool, structure, ctx.second, ctx.mode) - } - - Error => merge(subs, ctx, Error), - } -} - -pub fn merge(subs: &mut Subs, ctx: &Context, content: Content) -> Outcome { - let rank = ctx.first_desc.rank.min(ctx.second_desc.rank); - let desc = Descriptor { - content, - rank, - mark: Mark::NONE, - copy: OptVariable::NONE, - }; - - subs.union(ctx.first, ctx.second, desc); - - Outcome::default() -} - -fn register(subs: &mut Subs, desc: Descriptor, pool: &mut Pool) -> Variable { - let var = subs.fresh(desc); - - pool.push(var); - - var -} - -fn fresh(subs: &mut Subs, pool: &mut Pool, ctx: &Context, content: Content) -> Variable { - register( - subs, - Descriptor { - content, - rank: ctx.first_desc.rank.min(ctx.second_desc.rank), - mark: Mark::NONE, - copy: OptVariable::NONE, - }, - pool, - ) -} - -fn is_recursion_var(subs: &Subs, var: Variable) -> bool { - matches!( - subs.get_content_without_compacting(var), - Content::RecursionVar { .. } - ) -} - -#[allow(clippy::too_many_arguments)] -fn unify_function_or_tag_union_and_func( - subs: &mut Subs, - pool: &mut Pool, - ctx: &Context, - tag_name_index: &SubsIndex, - tag_symbol: Symbol, - tag_ext: Variable, - function_arguments: VariableSubsSlice, - function_return: Variable, - function_lambda_set: Variable, - left: bool, -) -> Outcome { - let tag_name = subs[*tag_name_index].clone(); - - let union_tags = UnionTags::insert_slices_into_subs(subs, [(tag_name, function_arguments)]); - let content = Content::Structure(FlatType::TagUnion(union_tags, tag_ext)); - - let new_tag_union_var = fresh(subs, pool, ctx, content); - - let mut outcome = if left { - unify_pool(subs, pool, new_tag_union_var, function_return, ctx.mode) - } else { - unify_pool(subs, pool, function_return, new_tag_union_var, ctx.mode) - }; - - { - let union_tags = UnionLambdas::tag_without_arguments(subs, tag_symbol); - let lambda_set_content = LambdaSet(self::LambdaSet { - solved: union_tags, - recursion_var: OptVariable::NONE, - unspecialized: SubsSlice::default(), - }); - - let tag_lambda_set = register( - subs, - Descriptor { - content: lambda_set_content, - rank: ctx.first_desc.rank.min(ctx.second_desc.rank), - mark: Mark::NONE, - copy: OptVariable::NONE, - }, - pool, - ); - - let closure_outcome = if left { - unify_pool(subs, pool, tag_lambda_set, function_lambda_set, ctx.mode) - } else { - unify_pool(subs, pool, function_lambda_set, tag_lambda_set, ctx.mode) - }; - - outcome.union(closure_outcome); - } - - if outcome.mismatches.is_empty() { - let desc = if left { - subs.get(ctx.second) - } else { - subs.get(ctx.first) - }; - - subs.union(ctx.first, ctx.second, desc); - } - - outcome -} diff --git a/crates/README.md b/crates/README.md new file mode 100644 index 0000000000..7cbb4058b9 --- /dev/null +++ b/crates/README.md @@ -0,0 +1,151 @@ +# Roc Internals + +Roc has different rust crates for various binaries and libraries. Their roles are briefly described below. If you'd like to learn more, have any questions, or suspect something is out of date, please start a discussion on the [Roc Zulip](https://roc.zulipchat.com/)! + +You can use `cargo doc` to generate docs for a specific package; e.g. + +``` +cargo doc --package roc_ast --open +``` + +## `cli/` - `roc_cli` + +The `roc` binary that brings together all functionality in the Roc toolset. + +## `cli_utils/` - `cli_utils` + +Provides shared code for cli tests and benchmarks. + +## `compiler/` + +Compiles `.roc` files and combines them with their platform into an executable binary. See [compiler/README.md](./compiler/README.md) for more information. + +TODO explain what "compiler frontend" is +TODO explain what "compiler backend" is + +The compiler includes the following sub-crates; +- `roc_alias_analysis` Performs analysis and optimizations to remove unneeded [reference counts](https://en.wikipedia.org/wiki/Reference_counting) at runtime, and supports in-place mutation. +- `arena_pool` An implementation of an [arena allocator](https://mgravell.github.io/Pipelines.Sockets.Unofficial/docs/arenas.html) designed for the compiler's workloads. +- `roc_build` Responsible for coordinating building and linking of a Roc app with its host. +- `roc_builtins` provides the Roc functions and modules that are implicitly imported into every module. See [README.md](./compiler/builtins/README.md) for more information. +- `roc_can` [Canonicalize](https://en.wikipedia.org/wiki/Canonicalization) a roc [abstract syntax tree](https://en.wikipedia.org/wiki/Abstract_syntax_tree), [resolving symbols](https://stackoverflow.com/a/1175493/4200103), [re-ordering definitions](https://www.oreilly.com/library/view/c-high-performance/9781787120952/546b5677-9157-4333-bc90-16db696436ac.xhtml), and preparing a module for [type inference](https://en.wikipedia.org/wiki/Type_inference). +- `roc_collections` Domain-specific collections created for the needs of the compiler. +- `roc_constrain` Responsible for building the set of constraints that are used during [type inference](https://en.wikipedia.org/wiki/Type_inference) of a program, and for gathering context needed for pleasant error messages when a type error occurs. +- `roc_debug_flags` Environment variables that can be toggled to aid debugging of the compiler itself. +- `roc_derive` provides auto-derivers for builtin abilities like `Hash` and `Decode`. +- `roc_exhaustive` provides [exhaustiveness](https://dev.to/babak/exhaustive-type-checking-with-typescript-4l3f) checking for Roc. +- `roc_fmt` The roc code formatter. +- `roc_gen_dev` provides the compiler backend to generate Roc binaries fast, for a nice developer experience. See [README.md](./compiler/gen_dev/README.md) for more information. +- `roc_gen_llvm` provides the LLVM backend to generate Roc binaries. Used to generate a binary with the fastest possible execution speed. +- `roc_gen_wasm` provides the WASM backend to generate Roc binaries. See [README.md](./compiler/gen_wasm/README.md) for more information. +- `roc_ident` Implements data structures used for efficiently representing small strings, like identifiers. +- `roc_intern` provides generic interners for concurrent and single-thread use cases. +- `roc_late_solve` provides type unification and solving primitives from the perspective of the compiler backend. +- `roc_load` Used to load a .roc file and coordinate the compiler pipeline, including parsing, type checking, and [code generation](https://en.wikipedia.org/wiki/Code_generation_(compiler)). +- `roc_load_internal` The internal implementation of roc_load, separate from roc_load to support caching. +- `roc_module` Implements data structures used for efficiently representing unique modules and identifiers in Roc programs. +- `roc_mono` Roc's main intermediate representation (IR), which is responsible for [monomorphization](https://en.wikipedia.org/wiki/Monomorphization), defunctionalization, inserting [ref-count](https://en.wikipedia.org/wiki/Reference_counting) instructions, and transforming a Roc program into a form that is easy to consume by a backend. +- `roc_parse` Implements the Roc parser, which transforms a textual representation of a Roc program to an [abstract syntax tree](https://en.wikipedia.org/wiki/Abstract_syntax_tree). +- `roc_problem` provides types to describe problems that can occur when compiling `.roc` code. +- `roc_region` Data structures for storing source-code-location information, used heavily for contextual error messages. +- `roc_target` provides types and helpers for compiler targets such as `default_x86_64`. +- `roc_serialize` provides helpers for serializing and deserializing to/from bytes. +- `roc_solve` The entry point of Roc's [type inference](https://en.wikipedia.org/wiki/Type_inference) system. Implements type inference and specialization of abilities. +- `roc_solve_problem` provides types to describe problems that can occur during solving. +- `test_derive` Tests Roc's auto-derivers. +- `test_gen` contains all of Roc's [code generation](https://en.wikipedia.org/wiki/Code_generation_(compiler)) tests. See [README.md](./compiler/test_gen/README.md) for more information. +- `test_mono` Tests Roc's generation of the mono intermediate representation. +- `test_mono_macros` Macros for use in `test_mono`. +- `roc_types` Various representations and utilities for dealing with types in the Roc compiler. +- `roc_unify` Implements Roc's unification algorithm, the heartstone of Roc's [type inference](https://en.wikipedia.org/wiki/Type_inference). + +## `docs/` - `roc_docs` + +Generates html documentation from Roc files. +Used for [roc-lang.org/builtins/Num](https://www.roc-lang.org/builtins/Num). + +## `docs_cli/` - `roc_docs_cli` library and `roc-docs` binary + +Provides a binary that is only used for static build servers. + +## `error_macros/` - `roc_error_macros` + +Provides macros for consistent reporting of errors in Roc's rust code. + +## `glue/` - `roc_glue` + +The `roc_glue` crate generates code needed for platform hosts to communicate with Roc apps. This tool is not necessary for writing a platform in another language, however, it's a great convenience! Currently supports Rust platforms, and the plan is to support any language via a plugin model. + +## `highlight/` - `roc_highlight` + +Provides syntax highlighting for the static site gen platform which is used by the tutorial. + +## `linker/` - `roc_linker` + +Surgical linker that links platforms to Roc applications. We created our own linker for performance, since regular linkers add complexity that is not needed for linking Roc apps. Because we want `roc` to manage the build system and final linking of the executable, it is significantly less practical to use a regular linker. See [README.md](./linker/README.md) for more information. + +## `repl_cli/` - `roc_repl_cli` + +Command Line Interface(CLI) functionality for the Read-Evaluate-Print-Loop (REPL). + +## `repl_eval/` - `roc_repl_eval` + +Provides the functionality for the REPL to evaluate Roc expressions. + +## `repl_state/` - `roc_repl_state` + +Implements the state machine the to handle user input for the REPL (CLI and web) +If the user enters an expression, like `x * 2`, check it evaluate it. +If the user enters a declaration, like `x = 123`, check it and remember it, but don't evaluate. + +## `repl_expect/` - `roc_repl_expect` + +Supports evaluating `expect` and printing contextual information when they fail. + +## `repl_test/` - `repl_test` + +Tests the roc REPL. + +## `repl_wasm/` - `roc_repl_wasm` + +Provides a build of the REPL for the Roc website using WebAssembly. See [README.md](./repl_wasm/README.md) for more information. + +## `reporting/` - `roc_reporting` + +Responsible for generating warning and error messages. + +## `roc_std/` - `roc_std` + +Provides Rust representations of Roc data structures. + +## `test_utils/` - `roc_test_utils` + +Provides testing utility functions for use throughout the Rust code base. + +## `tracing/` - `roc_tracing` + +Provides tracing utility functions for various executable entry points. + +## `utils/` - `roc_utils` + +Provides utility functions used all over the code base. + +## `vendor/` + +These are files that were originally obtained somewhere else (e.g. crates.io) but which we needed to fork for some Roc-specific reason. See [README.md](./vendor/README.md) for more information. + +## `wasi-libc-sys/` - `wasi_libc_sys` + +Provides a Rust wrapper for the WebAssembly test platform built on libc and is primarily used for testing purposes. + +# Building a Roc Application + +Below is a simplified diagram to illustrate how a Roc application and host are combined to build an executable file. + +![Building a Roc Application using Rust](./building_a_roc_application.svg) + +# Roc Compiler Stages + +Below is a simplified diagram to illustrate the different stages of the Roc Compiler. + +![Roc Compiler Stages](./roc_compiler_stages.svg) diff --git a/crates/building_a_roc_application.svg b/crates/building_a_roc_application.svg new file mode 100644 index 0000000000..50bff9e784 --- /dev/null +++ b/crates/building_a_roc_application.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml new file mode 100644 index 0000000000..219c7a48b1 --- /dev/null +++ b/crates/cli/Cargo.toml @@ -0,0 +1,97 @@ +[package] +name = "roc_cli" +description = "The Roc binary that brings together all functionality in the Roc toolset." +default-run = "roc" + +authors.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true +version.workspace = true + +[[bin]] +bench = false +name = "roc" +path = "src/main.rs" +test = false + +[features] +default = ["target-aarch64", "target-x86_64", "target-wasm32"] + +i386-cli-run = ["target-x86"] +wasm32-cli-run = ["target-wasm32", "run-wasm32"] + +run-wasm32 = ["roc_wasm_interp"] + +# Compiling for a different target than the current machine can cause linker errors. +target-aarch64 = ["roc_build/target-aarch64", "roc_repl_cli/target-aarch64"] +target-arm = ["roc_build/target-arm", "roc_repl_cli/target-arm"] +target-wasm32 = ["roc_build/target-wasm32"] +target-x86 = ["roc_build/target-x86", "roc_repl_cli/target-x86"] +target-x86_64 = ["roc_build/target-x86_64", "roc_repl_cli/target-x86_64"] + +target-all = ["target-aarch64", "target-arm", "target-x86", "target-x86_64", "target-wasm32"] + +sanitizers = ["roc_build/sanitizers"] + + +[dependencies] +roc_build = { path = "../compiler/build" } +roc_builtins = { path = "../compiler/builtins" } +roc_can = { path = "../compiler/can" } +roc_collections = { path = "../compiler/collections" } +roc_docs = { path = "../docs" } +roc_error_macros = { path = "../error_macros" } +roc_fmt = { path = "../compiler/fmt" } +roc_gen_llvm = { path = "../compiler/gen_llvm" } +roc_gen_dev = { path = "../compiler/gen_dev" } +roc_glue = { path = "../glue" } +roc_linker = { path = "../linker" } +roc_load = { path = "../compiler/load" } +roc_module = { path = "../compiler/module" } +roc_mono = { path = "../compiler/mono" } +roc_packaging = { path = "../packaging" } +roc_parse = { path = "../compiler/parse" } +roc_region = { path = "../compiler/region" } +roc_repl_cli = { path = "../repl_cli", optional = true } +roc_reporting = { path = "../reporting" } +roc_target = { path = "../compiler/roc_target" } +roc_tracing = { path = "../tracing" } +roc_wasm_interp = { path = "../wasm_interp", optional = true } + +ven_pretty = { path = "../vendor/pretty" } + +bumpalo.workspace = true +clap.workspace = true +const_format.workspace = true +distance.workspace = true +errno.workspace = true +indoc.workspace = true +inkwell.workspace = true +libc.workspace = true +libloading.workspace = true +mimalloc.workspace = true +signal-hook.workspace = true +strum.workspace = true +target-lexicon.workspace = true +tempfile.workspace = true + +# for now, uses unix/libc functions that windows does not support +[target.'cfg(not(windows))'.dependencies] +roc_repl_expect = { path = "../repl_expect" } + + +[dev-dependencies] +cli_utils = { path = "../cli_utils" } +roc_test_utils = { path = "../test_utils" } +roc_command_utils = { path = "../utils/command" } + +criterion.workspace = true +indoc.workspace = true +parking_lot.workspace = true +pretty_assertions.workspace = true +serial_test.workspace = true + +[[bench]] +name = "time_bench" +harness = false diff --git a/crates/cli/benches/README.md b/crates/cli/benches/README.md new file mode 100644 index 0000000000..cea3d2f103 --- /dev/null +++ b/crates/cli/benches/README.md @@ -0,0 +1,20 @@ + +# Running the benchmarks + +Install cargo criterion: + +```sh +cargo install cargo-criterion +``` + +To prevent stack overflow on the `CFold` benchmark: + +```sh +ulimit -s unlimited +``` + +In the `cli` folder execute: + +```sh +cargo criterion +``` diff --git a/cli/benches/time_bench.rs b/crates/cli/benches/time_bench.rs similarity index 100% rename from cli/benches/time_bench.rs rename to crates/cli/benches/time_bench.rs diff --git a/crates/cli/build.rs b/crates/cli/build.rs new file mode 100644 index 0000000000..5d171b71cf --- /dev/null +++ b/crates/cli/build.rs @@ -0,0 +1,6 @@ +fn main() { + // workaround for issue https://github.com/NixOS/nixpkgs/issues/166205 . This println can be removed when this issue is fixed. Upgrading to LLVM 14 could also fix this issue. + // also see https://github.com/NixOS/nixpkgs/pull/181485 + #[cfg(all(target_os = "macos", target_arch = "aarch64"))] + println!("cargo:rustc-link-lib=c++abi") +} diff --git a/crates/cli/src/format.rs b/crates/cli/src/format.rs new file mode 100644 index 0000000000..69a6aee56f --- /dev/null +++ b/crates/cli/src/format.rs @@ -0,0 +1,242 @@ +use std::ffi::OsStr; +use std::io::Write; +use std::path::{Path, PathBuf}; + +use bumpalo::Bump; +use roc_error_macros::{internal_error, user_error}; +use roc_fmt::def::fmt_defs; +use roc_fmt::module::fmt_module; +use roc_fmt::spaces::RemoveSpaces; +use roc_fmt::{Ast, Buf}; +use roc_parse::{ + module::{self, module_defs}, + parser::{Parser, SyntaxError}, + state::State, +}; + +#[derive(Copy, Clone, Debug)] +pub enum FormatMode { + WriteToFile, + WriteToStdout, + CheckOnly, +} + +fn flatten_directories(files: std::vec::Vec) -> std::vec::Vec { + let mut to_flatten = files; + let mut files = vec![]; + + while let Some(path) = to_flatten.pop() { + if path.is_dir() { + match path.read_dir() { + Ok(directory) => { + for item in directory { + match item { + Ok(file) => { + let file_path = file.path(); + if file_path.is_dir() { + to_flatten.push(file_path); + } else if is_roc_file(&file_path) { + files.push(file_path); + } + } + + Err(error) => internal_error!( + "There was an error while trying to read a file from a directory: {:?}", + error + ), + } + } + } + + Err(error) => internal_error!( + "There was an error while trying to read the contents of a directory: {:?}", + error + ), + } + } else if is_roc_file(&path) { + files.push(path); + } + } + + files +} + +fn is_roc_file(path: &Path) -> bool { + matches!(path.extension().and_then(OsStr::to_str), Some("roc")) +} + +pub fn format_files(files: std::vec::Vec, mode: FormatMode) -> Result<(), String> { + let arena = Bump::new(); + + for file in flatten_directories(files) { + let src = std::fs::read_to_string(&file).unwrap(); + + match format_src(&arena, &src) { + Ok(buf) => { + match mode { + FormatMode::CheckOnly => { + // If we notice that this file needs to be formatted, return early + if buf.as_str() != src { + return Err("One or more files need to be reformatted.".to_string()); + } + } + FormatMode::WriteToFile => { + // If all the checks above passed, actually write out the new file. + std::fs::write(&file, buf.as_str()).unwrap(); + } + FormatMode::WriteToStdout => { + std::io::stdout().lock().write_all(buf.as_bytes()).unwrap() + } + } + } + Err(err) => match err { + FormatProblem::ParsingFailed { + formatted_src, + parse_err, + } => { + let fail_file = file.with_extension("roc-format-failed"); + + std::fs::write(&fail_file, formatted_src.as_str()).unwrap(); + + internal_error!( + "Formatting bug; formatted code isn't valid\n\n\ + I wrote the incorrect result to this file for debugging purposes:\n{}\n\n\ + Parse error was: {:?}\n\n", + fail_file.display(), + parse_err + ); + } + FormatProblem::ReformattingChangedAst { + formatted_src, + ast_before, + ast_after, + } => { + let mut fail_file = file.clone(); + fail_file.set_extension("roc-format-failed"); + std::fs::write(&fail_file, formatted_src.as_str()).unwrap(); + + let mut before_file = file.clone(); + before_file.set_extension("roc-format-failed-ast-before"); + std::fs::write(&before_file, ast_before).unwrap(); + + let mut after_file = file.clone(); + after_file.set_extension("roc-format-failed-ast-after"); + std::fs::write(&after_file, ast_after).unwrap(); + + internal_error!( + "Formatting bug; formatting didn't reparse as the same tree\n\n\ + I wrote the incorrect result to this file for debugging purposes:\n{}\n\n\ + I wrote the tree before and after formatting to these files for debugging purposes:\n{}\n{}\n\n", + fail_file.display(), + before_file.display(), + after_file.display() + ); + } + FormatProblem::ReformattingUnstable { + formatted_src, + reformatted_src, + } => { + let mut unstable_1_file = file.clone(); + unstable_1_file.set_extension("roc-format-unstable-1"); + std::fs::write(&unstable_1_file, formatted_src).unwrap(); + + let mut unstable_2_file = file.clone(); + unstable_2_file.set_extension("roc-format-unstable-2"); + std::fs::write(&unstable_2_file, reformatted_src).unwrap(); + + internal_error!( + "Formatting bug; formatting is not stable. Reformatting the formatted file changed it again.\n\n\ + I wrote the result of formatting to this file for debugging purposes:\n{}\n\n\ + I wrote the result of double-formatting here:\n{}\n\n", + unstable_1_file.display(), + unstable_2_file.display() + ); + } + }, + } + } + + Ok(()) +} + +#[derive(Debug)] +pub enum FormatProblem { + ParsingFailed { + formatted_src: String, + parse_err: String, + }, + ReformattingChangedAst { + formatted_src: String, + ast_before: String, + ast_after: String, + }, + ReformattingUnstable { + formatted_src: String, + reformatted_src: String, + }, +} + +pub fn format_src(arena: &Bump, src: &str) -> Result { + let ast = arena.alloc(parse_all(arena, src).unwrap_or_else(|e| { + user_error!("Unexpected parse failure when parsing this formatting:\n\n{:?}\n\nParse error was:\n\n{:?}\n\n", src, e) + })); + let mut buf = Buf::new_in(arena); + fmt_all(&mut buf, ast); + + let reparsed_ast = match arena.alloc(parse_all(arena, buf.as_str())) { + Ok(ast) => ast, + Err(e) => { + return Err(FormatProblem::ParsingFailed { + formatted_src: buf.as_str().to_string(), + parse_err: format!("{:?}", e), + }); + } + }; + + let ast_normalized = ast.remove_spaces(arena); + let reparsed_ast_normalized = reparsed_ast.remove_spaces(arena); + + // HACK! + // We compare the debug format strings of the ASTs, because I'm finding in practice that _somewhere_ deep inside the ast, + // the PartialEq implementation is returning `false` even when the Debug-formatted impl is exactly the same. + // I don't have the patience to debug this right now, so let's leave it for another day... + // TODO: fix PartialEq impl on ast types + if format!("{ast_normalized:?}") != format!("{reparsed_ast_normalized:?}") { + return Err(FormatProblem::ReformattingChangedAst { + formatted_src: buf.as_str().to_string(), + ast_before: format!("{ast_normalized:#?}\n"), + ast_after: format!("{reparsed_ast_normalized:#?}\n"), + }); + } + + // Now verify that the resultant formatting is _stable_ - i.e. that it doesn't change again if re-formatted + let mut reformatted_buf = Buf::new_in(arena); + + fmt_all(&mut reformatted_buf, reparsed_ast); + + if buf.as_str() != reformatted_buf.as_str() { + return Err(FormatProblem::ReformattingUnstable { + formatted_src: buf.as_str().to_string(), + reformatted_src: reformatted_buf.as_str().to_string(), + }); + } + + Ok(buf.as_str().to_string()) +} + +fn parse_all<'a>(arena: &'a Bump, src: &'a str) -> Result, SyntaxError<'a>> { + let (module, state) = module::parse_header(arena, State::new(src.as_bytes())) + .map_err(|e| SyntaxError::Header(e.problem))?; + + let (_, defs, _) = module_defs().parse(arena, state, 0).map_err(|(_, e)| e)?; + + Ok(Ast { module, defs }) +} + +fn fmt_all<'a>(buf: &mut Buf<'a>, ast: &'a Ast) { + fmt_module(buf, &ast.module); + + fmt_defs(buf, &ast.defs, 0); + + buf.fmt_end_of_file(); +} diff --git a/crates/cli/src/lib.rs b/crates/cli/src/lib.rs new file mode 100644 index 0000000000..f5ffeac79c --- /dev/null +++ b/crates/cli/src/lib.rs @@ -0,0 +1,1295 @@ +//! Provides the core CLI functionality for the Roc binary. + +#[macro_use] +extern crate const_format; + +use bumpalo::Bump; +use clap::{ + builder::PossibleValuesParser, parser::ValueSource, value_parser, Arg, ArgAction, ArgMatches, + Command, +}; +use roc_build::link::{LinkType, LinkingStrategy}; +use roc_build::program::{ + handle_error_module, handle_loading_problem, standard_load_config, BuildFileError, + BuildOrdering, BuiltFile, CodeGenBackend, CodeGenOptions, DEFAULT_ROC_FILENAME, +}; +use roc_error_macros::{internal_error, user_error}; +use roc_gen_dev::AssemblyBackendMode; +use roc_gen_llvm::llvm::build::LlvmBackendMode; +use roc_load::{ExpectMetadata, Threading}; +use roc_mono::ir::OptLevel; +use roc_packaging::cache::RocCacheDir; +use roc_packaging::tarball::Compression; +use roc_target::Target; +use std::env; +use std::ffi::{CString, OsStr, OsString}; +use std::io; +use std::mem::ManuallyDrop; +use std::os::raw::{c_char, c_int}; +use std::path::{Path, PathBuf}; +use std::process; +use std::time::Instant; +use strum::IntoEnumIterator; +use target_lexicon::{Architecture, Triple}; +#[cfg(not(target_os = "linux"))] +use tempfile::TempDir; + +mod format; +pub use format::{format_files, format_src, FormatMode}; + +pub const CMD_BUILD: &str = "build"; +pub const CMD_RUN: &str = "run"; +pub const CMD_DEV: &str = "dev"; +pub const CMD_REPL: &str = "repl"; +pub const CMD_DOCS: &str = "docs"; +pub const CMD_CHECK: &str = "check"; +pub const CMD_VERSION: &str = "version"; +pub const CMD_FORMAT: &str = "format"; +pub const CMD_TEST: &str = "test"; +pub const CMD_GLUE: &str = "glue"; +pub const CMD_GEN_STUB_LIB: &str = "gen-stub-lib"; +pub const CMD_PREPROCESS_HOST: &str = "preprocess-host"; + +pub const FLAG_DEBUG: &str = "debug"; +pub const FLAG_BUNDLE: &str = "bundle"; +pub const FLAG_DEV: &str = "dev"; +pub const FLAG_OPTIMIZE: &str = "optimize"; +pub const FLAG_MAX_THREADS: &str = "max-threads"; +pub const FLAG_OPT_SIZE: &str = "opt-size"; +pub const FLAG_LIB: &str = "lib"; +pub const FLAG_NO_LINK: &str = "no-link"; +pub const FLAG_TARGET: &str = "target"; +pub const FLAG_TIME: &str = "time"; +pub const FLAG_LINKER: &str = "linker"; +pub const FLAG_PREBUILT: &str = "prebuilt-platform"; +pub const FLAG_CHECK: &str = "check"; +pub const FLAG_STDIN: &str = "stdin"; +pub const FLAG_STDOUT: &str = "stdout"; +pub const FLAG_WASM_STACK_SIZE_KB: &str = "wasm-stack-size-kb"; +pub const FLAG_OUTPUT: &str = "output"; +pub const ROC_FILE: &str = "ROC_FILE"; +pub const ROC_DIR: &str = "ROC_DIR"; +pub const GLUE_DIR: &str = "GLUE_DIR"; +pub const GLUE_SPEC: &str = "GLUE_SPEC"; +pub const DIRECTORY_OR_FILES: &str = "DIRECTORY_OR_FILES"; +pub const ARGS_FOR_APP: &str = "ARGS_FOR_APP"; + +const VERSION: &str = include_str!("../../../version.txt"); +const DEFAULT_GENERATED_DOCS_DIR: &str = "generated-docs"; + +pub fn build_app() -> Command { + let flag_optimize = Arg::new(FLAG_OPTIMIZE) + .long(FLAG_OPTIMIZE) + .help("Optimize the compiled program to run faster\n(Optimization takes time to complete.)") + .action(ArgAction::SetTrue) + .required(false); + + let flag_max_threads = Arg::new(FLAG_MAX_THREADS) + .long(FLAG_MAX_THREADS) + .help("Limit the number of threads (and hence cores) used during compilation") + .value_parser(value_parser!(usize)) + .required(false); + + let flag_opt_size = Arg::new(FLAG_OPT_SIZE) + .long(FLAG_OPT_SIZE) + .help("Optimize the compiled program to have a small binary size\n(Optimization takes time to complete.)") + .action(ArgAction::SetTrue) + .required(false); + + let flag_dev = Arg::new(FLAG_DEV) + .long(FLAG_DEV) + .help("Make compilation finish as soon as possible, at the expense of runtime performance") + .action(ArgAction::SetTrue) + .required(false); + + let flag_debug = Arg::new(FLAG_DEBUG) + .long(FLAG_DEBUG) + .help("Store LLVM debug information in the generated program") + .action(ArgAction::SetTrue) + .required(false); + + let flag_time = Arg::new(FLAG_TIME) + .long(FLAG_TIME) + .help("Print detailed compilation time information") + .action(ArgAction::SetTrue) + .required(false); + + let flag_linker = Arg::new(FLAG_LINKER) + .long(FLAG_LINKER) + .help("Set which linker to use\n(The surgical linker is enabled by default only when building for wasm32 or x86_64 Linux, because those are the only targets it currently supports. Otherwise the legacy linker is used by default.)") + .value_parser(["surgical", "legacy"]) + .required(false); + + let flag_prebuilt = Arg::new(FLAG_PREBUILT) + .long(FLAG_PREBUILT) + .help("Assume the platform has been prebuilt and skip rebuilding the platform\n(This is enabled implicitly when using `roc build` with a --target other than `--target `, unless the target is wasm.)") + .action(ArgAction::SetTrue) + .required(false); + + let flag_wasm_stack_size_kb = Arg::new(FLAG_WASM_STACK_SIZE_KB) + .long(FLAG_WASM_STACK_SIZE_KB) + .help("Stack size in kilobytes for wasm32 target\n(This only applies when --dev also provided.)") + .value_parser(value_parser!(u32)) + .required(false); + + let roc_file_to_run = Arg::new(ROC_FILE) + .help("The .roc file of an app to run") + .value_parser(value_parser!(PathBuf)) + .required(false) + .default_value(DEFAULT_ROC_FILENAME); + + let args_for_app = Arg::new(ARGS_FOR_APP) + .help("Arguments to pass into the app being run\ne.g. `roc run -- arg1 arg2`") + .value_parser(value_parser!(OsString)) + .num_args(0..) + .allow_hyphen_values(true); + + let build_target_values_parser = + PossibleValuesParser::new(Target::iter().map(Into::<&'static str>::into)); + + Command::new("roc") + .version(concatcp!(VERSION, "\n")) + .about("Run the given .roc file, if there are no compilation errors.\nYou can use one of the SUBCOMMANDS below to do something else!") + .args_conflicts_with_subcommands(true) + .subcommand(Command::new(CMD_BUILD) + .about("Build a binary from the given .roc file, but don't run it") + .arg(Arg::new(FLAG_OUTPUT) + .long(FLAG_OUTPUT) + .help("The full path to the output binary, including filename. To specify directory only, specify a path that ends in a directory separator (e.g. a slash).") + .value_parser(value_parser!(OsString)) + .required(false) + ) + .arg(flag_optimize.clone()) + .arg(flag_max_threads.clone()) + .arg(flag_opt_size.clone()) + .arg(flag_dev.clone()) + .arg(flag_debug.clone()) + .arg(flag_time.clone()) + .arg(flag_linker.clone()) + .arg(flag_prebuilt.clone()) + .arg(flag_wasm_stack_size_kb) + .arg( + Arg::new(FLAG_TARGET) + .long(FLAG_TARGET) + .help("Choose a different target") + .default_value(Into::<&'static str>::into(Target::default())) + .value_parser(build_target_values_parser.clone()) + .required(false), + ) + .arg( + Arg::new(FLAG_LIB) + .long(FLAG_LIB) + .help("Build a C library instead of an executable") + .action(ArgAction::SetTrue) + .required(false), + ) + .arg( + Arg::new(FLAG_BUNDLE) + .long(FLAG_BUNDLE) + .help("Create an archive of a package (for example, a .tar, .tar.gz, or .tar.br file), so others can add it as a HTTPS dependency.") + .conflicts_with(FLAG_TARGET) + .value_parser([".tar", ".tar.gz", ".tar.br"]) + .required(false), + ) + .arg( + Arg::new(FLAG_NO_LINK) + .long(FLAG_NO_LINK) + .help("Do not link\n(Instead, just output the `.o` file.)") + .action(ArgAction::SetTrue) + .required(false), + ) + .arg( + Arg::new(ROC_FILE) + .help("The .roc file to build") + .value_parser(value_parser!(PathBuf)) + .required(false) + .default_value(DEFAULT_ROC_FILENAME), + ) + ) + .subcommand(Command::new(CMD_TEST) + .about("Run all top-level `expect`s in a main module and any modules it imports") + .arg(flag_optimize.clone()) + .arg(flag_max_threads.clone()) + .arg(flag_opt_size.clone()) + .arg(flag_dev.clone()) + .arg(flag_debug.clone()) + .arg(flag_time.clone()) + .arg(flag_linker.clone()) + .arg(flag_prebuilt.clone()) + .arg( + Arg::new(ROC_FILE) + .help("The .roc file for the main module") + .value_parser(value_parser!(PathBuf)) + .required(false) + .default_value(DEFAULT_ROC_FILENAME) + ) + .arg(args_for_app.clone().last(true)) + ) + .subcommand(Command::new(CMD_REPL) + .about("Launch the interactive Read Eval Print Loop (REPL)") + ) + .subcommand(Command::new(CMD_RUN) + .about("Run a .roc file even if it has build errors") + .arg(flag_optimize.clone()) + .arg(flag_max_threads.clone()) + .arg(flag_opt_size.clone()) + .arg(flag_dev.clone()) + .arg(flag_debug.clone()) + .arg(flag_time.clone()) + .arg(flag_linker.clone()) + .arg(flag_prebuilt.clone()) + .arg(roc_file_to_run.clone()) + .arg(args_for_app.clone().last(true)) + ) + .subcommand(Command::new(CMD_DEV) + .about("`check` a .roc file, and then run it if there were no errors") + .arg(flag_optimize.clone()) + .arg(flag_max_threads.clone()) + .arg(flag_opt_size.clone()) + .arg(flag_dev.clone()) + .arg(flag_debug.clone()) + .arg(flag_time.clone()) + .arg(flag_linker.clone()) + .arg(flag_prebuilt.clone()) + .arg(roc_file_to_run.clone()) + .arg(args_for_app.clone().last(true)) + ) + .subcommand(Command::new(CMD_FORMAT) + .about("Format a .roc file using standard Roc formatting") + .arg( + Arg::new(DIRECTORY_OR_FILES) + .index(1) + .num_args(0..) + .required(false) + .value_parser(value_parser!(OsString))) + .arg( + Arg::new(FLAG_CHECK) + .long(FLAG_CHECK) + .help("Checks that specified files are formatted\n(If formatting is needed, return a non-zero exit code.)") + .action(ArgAction::SetTrue) + .required(false), + ) + .arg( + Arg::new(FLAG_STDIN) + .long(FLAG_STDIN) + .help("Read file to format from stdin") + .action(ArgAction::SetTrue) + .required(false), + ) + .arg( + Arg::new(FLAG_STDOUT) + .long(FLAG_STDOUT) + .help("Print formatted file to stdout") + .action(ArgAction::SetTrue) + .required(false), + ) + ) + .subcommand(Command::new(CMD_VERSION) + .about(concatcp!("Print the Roc compiler’s version, which is currently ", VERSION))) + .subcommand(Command::new(CMD_CHECK) + .about("Check the code for problems, but don’t build or run it") + .arg(flag_time.clone()) + .arg(flag_max_threads.clone()) + .arg( + Arg::new(ROC_FILE) + .help("The .roc file of an app to check") + .value_parser(value_parser!(PathBuf)) + .required(false) + .default_value(DEFAULT_ROC_FILENAME), + ) + ) + .subcommand( + Command::new(CMD_DOCS) + .about("Generate documentation for a Roc package") + .arg(Arg::new(FLAG_OUTPUT) + .long(FLAG_OUTPUT) + .help("Output directory for the generated documentation files.") + .value_parser(value_parser!(OsString)) + .required(false) + .default_value(DEFAULT_GENERATED_DOCS_DIR), + ) + .arg(Arg::new(ROC_FILE) + .help("The package's main .roc file") + .value_parser(value_parser!(PathBuf)) + .required(false) + .default_value(DEFAULT_ROC_FILENAME), + ) + ) + .subcommand(Command::new(CMD_GLUE) + .about("Generate glue code between a platform's Roc API and its host language") + .arg(&flag_dev) + .arg( + Arg::new(GLUE_SPEC) + .help("The specification for how to translate Roc types into output files.") + .value_parser(value_parser!(PathBuf)) + .required(true) + ) + .arg( + Arg::new(GLUE_DIR) + .help("The directory for the generated glue code.\nNote: The implementation can write to any file in this directory.") + .value_parser(value_parser!(PathBuf)) + .required(true) + ) + .arg( + Arg::new(ROC_FILE) + .help("The .roc file whose exposed types should be translated.") + .value_parser(value_parser!(PathBuf)) + .required(false) + .default_value(DEFAULT_ROC_FILENAME) + ) + ) + .subcommand(Command::new(CMD_GEN_STUB_LIB) + .about("Generate a stubbed shared library that can be used for linking a platform binary.\nThe stubbed library has prototypes, but no function bodies.\n\nNote: This command will be removed in favor of just using `roc build` once all platforms support the surgical linker") + .arg( + Arg::new(ROC_FILE) + .help("The .roc file for an app using the platform") + .value_parser(value_parser!(PathBuf)) + .required(true) + ) + .arg( + Arg::new(FLAG_TARGET) + .long(FLAG_TARGET) + .help("Choose a different target") + .default_value(Into::<&'static str>::into(Target::default())) + .value_parser(build_target_values_parser.clone()) + .required(false), + ) + ) + .subcommand(Command::new(CMD_PREPROCESS_HOST) + .about("Runs the surgical linker preprocessor to generate `.rh` and `.rm` files.") + .arg( + Arg::new(ROC_FILE) + .help("The .roc file for an app using the platform") + .value_parser(value_parser!(PathBuf)) + .required(true) + ) + .arg( + Arg::new(FLAG_TARGET) + .long(FLAG_TARGET) + .help("Choose a different target") + .default_value(Into::<&'static str>::into(Target::default())) + .value_parser(build_target_values_parser) + .required(false), + ) + ) + .arg(flag_optimize) + .arg(flag_max_threads) + .arg(flag_opt_size) + .arg(flag_dev) + .arg(flag_debug) + .arg(flag_time) + .arg(flag_linker) + .arg(flag_prebuilt) + .arg(roc_file_to_run) + .arg(args_for_app.trailing_var_arg(true)) +} + +#[derive(Debug, PartialEq, Eq)] +pub enum BuildConfig { + BuildOnly, + BuildAndRun, + BuildAndRunIfNoErrors, +} + +fn opt_level_from_flags(matches: &ArgMatches) -> OptLevel { + match ( + matches.get_flag(FLAG_OPTIMIZE), + matches.get_flag(FLAG_OPT_SIZE), + matches.get_flag(FLAG_DEV), + ) { + (true, false, false) => OptLevel::Optimize, + (false, true, false) => OptLevel::Size, + (false, false, true) => OptLevel::Development, + (false, false, false) => OptLevel::Normal, + _ => user_error!("build can be only one of `--dev`, `--optimize`, or `--opt-size`"), + } +} + +#[cfg(windows)] +pub fn test(_matches: &ArgMatches, _triple: Triple) -> io::Result { + todo!("running tests does not work on windows right now") +} + +#[cfg(not(windows))] +pub fn test(matches: &ArgMatches, triple: Triple) -> io::Result { + use roc_build::program::report_problems_monomorphized; + use roc_load::{ExecutionMode, FunctionKind, LoadConfig, LoadMonomorphizedError}; + use roc_packaging::cache; + use roc_target::TargetInfo; + + let start_time = Instant::now(); + let arena = Bump::new(); + let opt_level = opt_level_from_flags(matches); + + let threading = match matches.get_one::(FLAG_MAX_THREADS) { + None => Threading::AllAvailable, + Some(0) => user_error!("cannot build with at most 0 threads"), + Some(1) => Threading::Single, + Some(n) => Threading::AtMost(*n), + }; + + let path = matches.get_one::(ROC_FILE).unwrap(); + + // Spawn the root task + if !path.exists() { + let current_dir = env::current_dir().unwrap(); + let expected_file_path = current_dir.join(path); + + let current_dir_string = current_dir.display(); + let expected_file_path_string = expected_file_path.display(); + + // TODO these should use roc_reporting to display nicer error messages. + match matches.value_source(ROC_FILE) { + Some(ValueSource::DefaultValue) => { + eprintln!( + "\nThe current directory ({current_dir_string}) does not contain a {DEFAULT_ROC_FILENAME} file to use as a default.\n\nYou can run `roc help` for more information on how to provide a .roc file.\n" + ) + } + _ => eprintln!("\nThis file was not found: {expected_file_path_string}\n\nYou can run `roc help` for more information on how to provide a .roc file.\n"), + } + + process::exit(1); + } + + let arena = &arena; + let target = &triple; + let opt_level = opt_level; + let target_info = TargetInfo::from(target); + // TODO may need to determine this dynamically based on dev builds. + let function_kind = FunctionKind::LambdaSet; + + // Step 1: compile the app and generate the .o file + let load_config = LoadConfig { + target_info, + function_kind, + // TODO: expose this from CLI? + render: roc_reporting::report::RenderTarget::ColorTerminal, + palette: roc_reporting::report::DEFAULT_PALETTE, + threading, + exec_mode: ExecutionMode::Test, + }; + let load_result = roc_load::load_and_monomorphize( + arena, + path.to_path_buf(), + RocCacheDir::Persistent(cache::roc_cache_dir().as_path()), + load_config, + ); + + let mut loaded = match load_result { + Ok(loaded) => loaded, + Err(LoadMonomorphizedError::LoadingProblem(problem)) => { + return handle_loading_problem(problem); + } + Err(LoadMonomorphizedError::ErrorModule(module)) => { + return handle_error_module(module, start_time.elapsed(), path.as_os_str(), false); + } + }; + let problems = report_problems_monomorphized(&mut loaded); + + let mut expectations = std::mem::take(&mut loaded.expectations); + + let interns = loaded.interns.clone(); + + let (lib, expects, layout_interner) = roc_repl_expect::run::expect_mono_module_to_dylib( + arena, + target.clone(), + loaded, + opt_level, + LlvmBackendMode::CliTest, + ) + .unwrap(); + + // Print warnings before running tests. + { + debug_assert_eq!( + problems.errors, 0, + "if there were errors, we would have already exited." + ); + if problems.warnings > 0 { + problems.print_to_stdout(start_time.elapsed()); + println!(".\n\nRunning tests…\n\n\x1B[36m{}\x1B[39m", "─".repeat(80)); + } + } + + // Run the tests. + let arena = &bumpalo::Bump::new(); + let interns = arena.alloc(interns); + + let mut writer = std::io::stdout(); + + let (failed, passed) = roc_repl_expect::run::run_toplevel_expects( + &mut writer, + roc_reporting::report::RenderTarget::ColorTerminal, + arena, + interns, + &layout_interner.into_global(), + &lib, + &mut expectations, + expects, + ) + .unwrap(); + + let total_time = start_time.elapsed(); + + if failed == 0 && passed == 0 { + // TODO print this in a more nicely formatted way! + println!("No expectations were found."); + + // If no tests ran, treat that as an error. This is perhaps + // briefly annoying at the very beginning of a project when + // you actually have zero tests, but it can save you from + // having a change to your CI script accidentally stop + // running tests altogether! + Ok(2) + } else { + let failed_color = if failed == 0 { + 32 // green + } else { + 31 // red + }; + + println!( + "\n\x1B[{failed_color}m{failed}\x1B[39m failed and \x1B[32m{passed}\x1B[39m passed in {} ms.\n", + total_time.as_millis(), + ); + + Ok((failed > 0) as i32) + } +} + +/// Find the element of `options` with the smallest edit distance to +/// `reference`. Returns a tuple containing the element and the distance, or +/// `None` if the `options` `Vec` is empty. +fn nearest_match<'a>(reference: &str, options: &'a [String]) -> Option<(&'a String, usize)> { + options + .iter() + .map(|s| (s, distance::damerau_levenshtein(reference, s))) + .min_by(|(_, a), (_, b)| a.cmp(b)) +} + +pub fn build( + matches: &ArgMatches, + subcommands: &[String], + config: BuildConfig, + triple: Triple, + out_path: Option<&Path>, + roc_cache_dir: RocCacheDir<'_>, + link_type: LinkType, +) -> io::Result { + use roc_build::program::build_file; + use BuildConfig::*; + + let path = matches.get_one::(ROC_FILE).unwrap(); + { + // Spawn the root task + if !path.exists() { + let current_dir = env::current_dir().unwrap(); + let expected_file_path = current_dir.join(path); + + let current_dir_string = current_dir.display(); + let expected_file_path_string = expected_file_path.display(); + + // TODO these should use roc_reporting to display nicer error messages. + match matches.value_source(ROC_FILE) { + Some(ValueSource::DefaultValue) => { + eprintln!( + "\nThe current directory ({current_dir_string}) does not contain a {DEFAULT_ROC_FILENAME} file to use as a default.\n\nYou can run `roc help` for more information on how to provide a .roc file.\n" + ) + } + _ => { + let mut error_lines = Vec::new(); + error_lines.push(format!( + "This file was not found: {expected_file_path_string}" + )); + // Add some additional hints if run as `roc [FILENAME]`. + if matches.subcommand().is_none() { + match path.to_str() { + Some(possible_typo) if !possible_typo.ends_with(".roc") => { + if let Some((nearest_command, _)) = + nearest_match(possible_typo, subcommands) + { + error_lines.push(format!( + "Did you mean to use the {nearest_command} subcommand?" + )); + } + } + _ => (), + } + } + error_lines.push("You can run `roc help` to see the list of available subcommands and for more information on how to provide a .roc file.".to_string()); + + eprintln!("\n{}\n", error_lines.join("\n\n")); + } + } + + process::exit(1); + } + + if config == BuildConfig::BuildOnly && matches.contains_id(FLAG_BUNDLE) { + let start_time = Instant::now(); + + let compression = + Compression::try_from(matches.get_one::(FLAG_BUNDLE).unwrap().as_str()) + .unwrap(); + + // Print a note of advice. This is mainly here because brotli takes so long but produces + // such smaller output files; the idea is to encourage people to wait for brotli, + // so that downloads go faster. The compression only happens once, but the network + // transfer and decompression will happen many more times! + match compression { + Compression::Brotli => { + println!("Compressing with Brotli at maximum quality level…\n\n(Note: Brotli compression can take awhile! Using --{FLAG_BUNDLE} .tar.gz takes less time, but usually produces a significantly larger output file. Brotli is generally worth the up-front wait if this is a file people will be downloading!)\n"); + } + Compression::Gzip => { + println!("Compressing with gzip at minimum quality…\n\n(Note: Gzip usually runs faster than Brotli but typically produces significantly larger output files. Consider using --{FLAG_BUNDLE} .tar.br if this is a file people will be downloading!)\n"); + } + Compression::Uncompressed => { + println!("Building .tar archive without compression…\n\n(Note: Compression takes more time to run but typically produces much smaller output files. Consider using --{FLAG_BUNDLE} .tar.br if this is a file people will be downloading!)\n"); + } + } + + // Rather than building an executable or library, we're building + // a tarball so this code can be distributed via a HTTPS + let filename = roc_packaging::tarball::build(path, compression)?; + let total_time_ms = start_time.elapsed().as_millis(); + let total_time = if total_time_ms > 1000 { + format!("{}s {}ms", total_time_ms / 1000, total_time_ms % 1000) + } else { + format!("{total_time_ms} ms") + }; + let created_path = path.with_file_name(&filename); + + println!( + "\nBundled \x1B[33m{}\x1B[39m and its dependent files into the following archive in {total_time}:\n\n\t\x1B[33m{}\x1B[39m\n\nTo distribute this archive as a package, upload this to some URL and then add it as a dependency with:\n\n\t\x1B[32m\"https://your-url-goes-here/{filename}\"\x1B[39m\n", + path.to_string_lossy(), + created_path.to_string_lossy() + ); + + return Ok(0); + } + } + + // the process will end after this function, + // so we don't want to spend time freeing these values + let arena = ManuallyDrop::new(Bump::new()); + + let opt_level = if let BuildConfig::BuildAndRunIfNoErrors = config { + OptLevel::Development + } else { + opt_level_from_flags(matches) + }; + + // Note: This allows using `--dev` with `--optimize`. + // This means frontend optimizations and dev backend. + let code_gen_backend = if matches.get_flag(FLAG_DEV) { + if matches!(triple.architecture, Architecture::Wasm32) { + CodeGenBackend::Wasm + } else { + CodeGenBackend::Assembly(AssemblyBackendMode::Binary) + } + } else { + let backend_mode = match opt_level { + OptLevel::Development => LlvmBackendMode::BinaryDev, + OptLevel::Normal | OptLevel::Size | OptLevel::Optimize => LlvmBackendMode::Binary, + }; + + CodeGenBackend::Llvm(backend_mode) + }; + + let emit_debug_info = matches.get_flag(FLAG_DEBUG); + let emit_timings = matches.get_flag(FLAG_TIME); + + let threading = match matches.get_one::(FLAG_MAX_THREADS) { + None => Threading::AllAvailable, + Some(0) => user_error!("cannot build with at most 0 threads"), + Some(1) => Threading::Single, + Some(n) => Threading::AtMost(*n), + }; + + let wasm_dev_backend = matches!(code_gen_backend, CodeGenBackend::Wasm); + + let linking_strategy = if wasm_dev_backend { + LinkingStrategy::Additive + } else if !roc_linker::supported(link_type, &triple) + || matches.get_one::(FLAG_LINKER).map(|s| s.as_str()) == Some("legacy") + { + LinkingStrategy::Legacy + } else { + LinkingStrategy::Surgical + }; + + let prebuilt = { + let cross_compile = triple != Triple::host(); + let targeting_wasm = matches!(triple.architecture, Architecture::Wasm32); + + matches.get_flag(FLAG_PREBUILT) || + // When compiling for a different target, assume a prebuilt platform. + // Otherwise compilation would most likely fail because many toolchains + // assume you're compiling for the current machine. We make an exception + // for Wasm, because cross-compiling is the norm in that case. + (cross_compile && !targeting_wasm) + }; + + let wasm_dev_stack_bytes: Option = matches + .try_get_one::(FLAG_WASM_STACK_SIZE_KB) + .ok() + .flatten() + .map(|x| x * 1024); + + let build_ordering = match config { + BuildAndRunIfNoErrors => BuildOrdering::BuildIfChecks, + _ => BuildOrdering::AlwaysBuild, + }; + + let code_gen_options = CodeGenOptions { + backend: code_gen_backend, + opt_level, + emit_debug_info, + }; + + let load_config = standard_load_config(&triple, build_ordering, threading); + + let res_binary_path = build_file( + &arena, + &triple, + path.to_owned(), + code_gen_options, + emit_timings, + link_type, + linking_strategy, + prebuilt, + wasm_dev_stack_bytes, + roc_cache_dir, + load_config, + out_path, + ); + + match res_binary_path { + Ok(BuiltFile { + binary_path, + problems, + total_time, + expect_metadata, + }) => { + match config { + BuildOnly => { + // If possible, report the generated executable name relative to the current dir. + let generated_filename = binary_path + .strip_prefix(env::current_dir().unwrap()) + .unwrap_or(&binary_path) + .to_str() + .unwrap(); + + // No need to waste time freeing this memory, + // since the process is about to exit anyway. + // std::mem::forget(arena); + + problems.print_to_stdout(total_time); + println!(" while successfully building:\n\n {generated_filename}"); + + // Return a nonzero exit code if there were problems + Ok(problems.exit_code()) + } + BuildAndRun => { + if problems.fatally_errored { + problems.print_to_stdout(total_time); + println!( + ".\n\nCannot run program due to fatal error…\n\n\x1B[36m{}\x1B[39m", + "─".repeat(80) + ); + + // Return a nonzero exit code due to fatal problem + return Ok(problems.exit_code()); + } + if problems.errors > 0 || problems.warnings > 0 { + problems.print_to_stdout(total_time); + println!( + ".\n\nRunning program anyway…\n\n\x1B[36m{}\x1B[39m", + "─".repeat(80) + ); + } + + let args = matches + .get_many::(ARGS_FOR_APP) + .unwrap_or_default() + .map(|s| s.as_os_str()); + + // don't waste time deallocating; the process ends anyway + // ManuallyDrop will leak the bytes because we don't drop manually + let bytes = &ManuallyDrop::new(std::fs::read(&binary_path).unwrap()); + + roc_run(&arena, opt_level, triple, args, bytes, expect_metadata) + } + BuildAndRunIfNoErrors => { + if problems.fatally_errored { + problems.print_to_stdout(total_time); + println!( + ".\n\nCannot run program due to fatal error…\n\n\x1B[36m{}\x1B[39m", + "─".repeat(80) + ); + + // Return a nonzero exit code due to fatal problem + return Ok(problems.exit_code()); + } + debug_assert_eq!( + problems.errors, 0, + "if there are non-fatal errors, they should have been returned as an error variant" + ); + + if problems.warnings > 0 { + problems.print_to_stdout(total_time); + println!( + ".\n\nRunning program…\n\n\x1B[36m{}\x1B[39m", + "─".repeat(80) + ); + } + + let args = matches + .get_many::(ARGS_FOR_APP) + .unwrap_or_default() + .map(|s| s.as_os_str()); + + // don't waste time deallocating; the process ends anyway + // ManuallyDrop will leak the bytes because we don't drop manually + let bytes = &ManuallyDrop::new(std::fs::read(&binary_path).unwrap()); + + roc_run(&arena, opt_level, triple, args, bytes, expect_metadata) + } + } + } + Err(BuildFileError::ErrorModule { module, total_time }) => { + handle_error_module(module, total_time, path.as_os_str(), true) + } + Err(BuildFileError::LoadingProblem(problem)) => handle_loading_problem(problem), + } +} + +fn roc_run<'a, I: IntoIterator>( + arena: &Bump, + opt_level: OptLevel, + triple: Triple, + args: I, + binary_bytes: &[u8], + expect_metadata: ExpectMetadata, +) -> io::Result { + match triple.architecture { + Architecture::Wasm32 => { + let executable = roc_run_executable_file_path(binary_bytes)?; + let path = executable.as_path(); + // If possible, report the generated executable name relative to the current dir. + let generated_filename = path + .strip_prefix(env::current_dir().unwrap()) + .unwrap_or(path); + + #[cfg(target_family = "unix")] + { + use std::os::unix::ffi::OsStrExt; + + run_wasm( + generated_filename, + args.into_iter().map(|os_str| os_str.as_bytes()), + ); + } + + #[cfg(not(target_family = "unix"))] + { + run_wasm( + generated_filename, + args.into_iter().map(|os_str| { + os_str.to_str().expect( + "Roc does not currently support passing non-UTF8 arguments to Wasm.", + ) + }), + ); + } + + Ok(0) + } + _ => roc_run_native(arena, opt_level, args, binary_bytes, expect_metadata), + } +} + +#[cfg(target_family = "unix")] +fn os_str_as_utf8_bytes(os_str: &OsStr) -> &[u8] { + use std::os::unix::ffi::OsStrExt; + os_str.as_bytes() +} + +#[cfg(not(target_family = "unix"))] +fn os_str_as_utf8_bytes(os_str: &OsStr) -> &[u8] { + os_str.to_str().unwrap().as_bytes() +} + +fn make_argv_envp<'a, I: IntoIterator, S: AsRef>( + arena: &'a Bump, + executable: &ExecutableFile, + args: I, +) -> ( + bumpalo::collections::Vec<'a, CString>, + bumpalo::collections::Vec<'a, CString>, +) { + use bumpalo::collections::CollectIn; + + let path = executable.as_path(); + let path_cstring = CString::new(os_str_as_utf8_bytes(path.as_os_str())).unwrap(); + + // argv is an array of pointers to strings passed to the new program + // as its command-line arguments. By convention, the first of these + // strings (i.e., argv[0]) should contain the filename associated + // with the file being executed. The argv array must be terminated + // by a NULL pointer. (Thus, in the new program, argv[argc] will be NULL.) + let it = args + .into_iter() + .map(|x| CString::new(os_str_as_utf8_bytes(x.as_ref())).unwrap()); + + let argv_cstrings: bumpalo::collections::Vec = + std::iter::once(path_cstring).chain(it).collect_in(arena); + + // envp is an array of pointers to strings, conventionally of the + // form key=value, which are passed as the environment of the new + // program. The envp array must be terminated by a NULL pointer. + let mut buffer = Vec::with_capacity(100); + let envp_cstrings: bumpalo::collections::Vec = std::env::vars_os() + .map(|(k, v)| { + buffer.clear(); + + use std::io::Write; + buffer.write_all(os_str_as_utf8_bytes(&k)).unwrap(); + buffer.write_all(b"=").unwrap(); + buffer.write_all(os_str_as_utf8_bytes(&v)).unwrap(); + + CString::new(buffer.as_slice()).unwrap() + }) + .collect_in(arena); + + (argv_cstrings, envp_cstrings) +} + +/// Run on the native OS (not on wasm) +#[cfg(target_family = "unix")] +fn roc_run_native, S: AsRef>( + arena: &Bump, + opt_level: OptLevel, + args: I, + binary_bytes: &[u8], + expect_metadata: ExpectMetadata, +) -> std::io::Result { + use bumpalo::collections::CollectIn; + + unsafe { + let executable = roc_run_executable_file_path(binary_bytes)?; + let (argv_cstrings, envp_cstrings) = make_argv_envp(arena, &executable, args); + + let argv: bumpalo::collections::Vec<*const c_char> = argv_cstrings + .iter() + .map(|s| s.as_ptr()) + .chain([std::ptr::null()]) + .collect_in(arena); + + let envp: bumpalo::collections::Vec<*const c_char> = envp_cstrings + .iter() + .map(|s| s.as_ptr()) + .chain([std::ptr::null()]) + .collect_in(arena); + + match opt_level { + OptLevel::Development => roc_dev_native(arena, executable, argv, envp, expect_metadata), + OptLevel::Normal | OptLevel::Size | OptLevel::Optimize => { + roc_run_native_fast(executable, &argv, &envp); + } + } + } + + Ok(1) +} + +unsafe fn roc_run_native_fast( + executable: ExecutableFile, + argv: &[*const c_char], + envp: &[*const c_char], +) { + if executable.execve(argv, envp) != 0 { + internal_error!( + "libc::{}({:?}, ..., ...) failed: {:?}", + ExecutableFile::SYSCALL, + executable.as_path(), + errno::errno() + ); + } +} + +#[derive(Debug)] +enum ExecutableFile { + #[cfg(target_os = "linux")] + MemFd(libc::c_int, PathBuf), + #[cfg(not(target_os = "linux"))] + OnDisk(TempDir, PathBuf), +} + +impl ExecutableFile { + #[cfg(target_os = "linux")] + const SYSCALL: &'static str = "fexecve"; + + #[cfg(not(target_os = "linux"))] + const SYSCALL: &'static str = "execve"; + + fn as_path(&self) -> &Path { + match self { + #[cfg(target_os = "linux")] + ExecutableFile::MemFd(_, path_buf) => path_buf.as_ref(), + #[cfg(not(target_os = "linux"))] + ExecutableFile::OnDisk(_, path_buf) => path_buf.as_ref(), + } + } + + unsafe fn execve(&self, argv: &[*const c_char], envp: &[*const c_char]) -> c_int { + match self { + #[cfg(target_os = "linux")] + ExecutableFile::MemFd(fd, _path) => libc::fexecve(*fd, argv.as_ptr(), envp.as_ptr()), + + #[cfg(all(target_family = "unix", not(target_os = "linux")))] + ExecutableFile::OnDisk(_, path) => { + use std::os::unix::ffi::OsStrExt; + + let path_cstring = CString::new(path.as_os_str().as_bytes()).unwrap(); + libc::execve(path_cstring.as_ptr().cast(), argv.as_ptr(), envp.as_ptr()) + } + + #[cfg(target_family = "windows")] + ExecutableFile::OnDisk(_, path) => { + let path_cstring = CString::new(path.to_str().unwrap()).unwrap(); + + libc::execve(path_cstring.as_ptr().cast(), argv.as_ptr(), envp.as_ptr()) + } + } + } +} + +// with Expect +#[cfg(target_family = "unix")] +fn roc_dev_native( + arena: &Bump, + executable: ExecutableFile, + argv: bumpalo::collections::Vec<*const c_char>, + envp: bumpalo::collections::Vec<*const c_char>, + expect_metadata: ExpectMetadata, +) -> ! { + use std::sync::{atomic::AtomicBool, Arc}; + + use roc_repl_expect::run::{ChildProcessMsg, ExpectMemory}; + + let ExpectMetadata { + mut expectations, + interns, + layout_interner, + } = expect_metadata; + + // let shm_name = + let shm_name = format!("/roc_expect_buffer_{}", std::process::id()); + let mut memory = ExpectMemory::create_or_reuse_mmap(&shm_name); + + let layout_interner = layout_interner.into_global(); + + let mut writer = std::io::stdout(); + + match unsafe { libc::fork() } { + 0 => unsafe { + // we are the child + + executable.execve(&argv, &envp); + + // Display a human-friendly error message + println!("Error {:?}", std::io::Error::last_os_error()); + + std::process::exit(1); + }, + -1 => { + // something failed + + // Display a human-friendly error message + println!("Error {:?}", std::io::Error::last_os_error()); + + std::process::exit(1) + } + pid @ 1.. => { + let sigchld = Arc::new(AtomicBool::new(false)); + signal_hook::flag::register(signal_hook::consts::SIGCHLD, Arc::clone(&sigchld)) + .unwrap(); + + let exit_code = loop { + match memory.wait_for_child(sigchld.clone()) { + ChildProcessMsg::Terminate => { + let mut status = 0; + let options = 0; + unsafe { libc::waitpid(pid, &mut status, options) }; + + break status; + } + ChildProcessMsg::Expect => { + roc_repl_expect::run::render_expects_in_memory( + &mut writer, + arena, + &mut expectations, + &interns, + &layout_interner, + &memory, + ) + .unwrap(); + + memory.reset(); + } + } + }; + + std::process::exit(exit_code) + } + _ => unreachable!(), + } +} + +#[cfg(target_os = "linux")] +fn roc_run_executable_file_path(binary_bytes: &[u8]) -> std::io::Result { + // on linux, we use the `memfd_create` function to create an in-memory anonymous file. + let flags = 0; + let anonymous_file_name = "roc_file_descriptor\0"; + let fd = unsafe { libc::memfd_create(anonymous_file_name.as_ptr().cast(), flags) }; + + if fd == 0 { + internal_error!( + "libc::memfd_create({:?}, {}) failed: file descriptor is 0", + anonymous_file_name, + flags + ); + } + + let path = PathBuf::from(format!("/proc/self/fd/{fd}")); + + std::fs::write(&path, binary_bytes)?; + + Ok(ExecutableFile::MemFd(fd, path)) +} + +#[cfg(all(target_family = "unix", not(target_os = "linux")))] +fn roc_run_executable_file_path(binary_bytes: &[u8]) -> std::io::Result { + use std::fs::OpenOptions; + use std::io::Write; + use std::os::unix::fs::OpenOptionsExt; + + let temp_dir = tempfile::tempdir()?; + + // We have not found a way to use a virtual file on non-Linux OSes. + // Hence we fall back to just writing the file to the file system, and using that file. + let app_path_buf = temp_dir.path().join("roc_app_binary"); + let mut file = OpenOptions::new() + .create(true) + .write(true) + .mode(0o777) // create the file as executable + .open(&app_path_buf)?; + + file.write_all(binary_bytes)?; + + // We store the TempDir in this variant alongside the path to the executable, + // so that the TempDir doesn't get dropped until after we're done with the path. + // If we didn't do that, then the tempdir would potentially get deleted by the + // TempDir's Drop impl before the file had been executed. + Ok(ExecutableFile::OnDisk(temp_dir, app_path_buf)) +} + +#[cfg(target_family = "windows")] +fn roc_run_executable_file_path(binary_bytes: &[u8]) -> std::io::Result { + use std::fs::OpenOptions; + use std::io::Write; + + let temp_dir = tempfile::tempdir()?; + + // We have not found a way to use a virtual file on non-Linux OSes. + // Hence we fall back to just writing the file to the file system, and using that file. + let app_path_buf = temp_dir.path().join("roc_app_binary.exe"); + let mut file = OpenOptions::new() + .create(true) + .write(true) + //.mode(0o777) // create the file as executable + .open(&app_path_buf)?; + + file.write_all(binary_bytes)?; + + // We store the TempDir in this variant alongside the path to the executable, + // so that the TempDir doesn't get dropped until after we're done with the path. + // If we didn't do that, then the tempdir would potentially get deleted by the + // TempDir's Drop impl before the file had been executed. + Ok(ExecutableFile::OnDisk(temp_dir, app_path_buf)) +} + +/// Run on the native OS (not on wasm) +#[cfg(not(target_family = "unix"))] +fn roc_run_native, S: AsRef>( + arena: &Bump, // This should be passed an owned value, not a reference, so we can usefully mem::forget it! + opt_level: OptLevel, + args: I, + binary_bytes: &[u8], + _expect_metadata: ExpectMetadata, +) -> io::Result { + use bumpalo::collections::CollectIn; + + unsafe { + let executable = roc_run_executable_file_path(binary_bytes)?; + + // TODO forward the arguments + let (argv_cstrings, envp_cstrings) = make_argv_envp(&arena, &executable, args); + + let argv: bumpalo::collections::Vec<*const c_char> = argv_cstrings + .iter() + .map(|s| s.as_ptr()) + .chain([std::ptr::null()]) + .collect_in(arena); + + let envp: bumpalo::collections::Vec<*const c_char> = envp_cstrings + .iter() + .map(|s| s.as_ptr()) + .chain([std::ptr::null()]) + .collect_in(arena); + + match opt_level { + OptLevel::Development => { + // roc_run_native_debug(executable, &argv, &envp, expectations, interns) + internal_error!("running `expect`s does not currently work on windows") + } + OptLevel::Normal | OptLevel::Size | OptLevel::Optimize => { + roc_run_native_fast(executable, &argv, &envp); + } + } + } + + Ok(1) +} + +#[cfg(feature = "run-wasm32")] +fn run_wasm, S: AsRef<[u8]>>(wasm_path: &std::path::Path, args: I) { + use bumpalo::collections::Vec; + use roc_wasm_interp::{DefaultImportDispatcher, Instance}; + + let bytes = std::fs::read(wasm_path).unwrap(); + let arena = Bump::new(); + + let mut argv = Vec::<&[u8]>::new_in(&arena); + for arg in args { + let mut arg_copy = Vec::::new_in(&arena); + arg_copy.extend_from_slice(arg.as_ref()); + argv.push(arg_copy.into_bump_slice()); + } + let import_dispatcher = DefaultImportDispatcher::new(&argv); + + let mut instance = Instance::from_bytes(&arena, &bytes, import_dispatcher, false).unwrap(); + + instance + .call_export("_start", []) + .unwrap() + .unwrap() + .expect_i32() + .unwrap(); +} + +#[cfg(not(feature = "run-wasm32"))] +fn run_wasm, S: AsRef<[u8]>>(_wasm_path: &std::path::Path, _args: I) { + println!("Running wasm files is not supported on this target."); +} diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs new file mode 100644 index 0000000000..9d6fc8af81 --- /dev/null +++ b/crates/cli/src/main.rs @@ -0,0 +1,417 @@ +//! The `roc` binary that brings together all functionality in the Roc toolset. +use bumpalo::Bump; +use roc_build::link::LinkType; +use roc_build::program::{check_file, CodeGenBackend}; +use roc_cli::{ + build_app, format_files, format_src, test, BuildConfig, FormatMode, CMD_BUILD, CMD_CHECK, + CMD_DEV, CMD_DOCS, CMD_FORMAT, CMD_GEN_STUB_LIB, CMD_GLUE, CMD_PREPROCESS_HOST, CMD_REPL, + CMD_RUN, CMD_TEST, CMD_VERSION, DIRECTORY_OR_FILES, FLAG_CHECK, FLAG_DEV, FLAG_LIB, + FLAG_NO_LINK, FLAG_OUTPUT, FLAG_STDIN, FLAG_STDOUT, FLAG_TARGET, FLAG_TIME, GLUE_DIR, + GLUE_SPEC, ROC_FILE, +}; +use roc_docs::generate_docs_html; +use roc_error_macros::user_error; +use roc_gen_dev::AssemblyBackendMode; +use roc_gen_llvm::llvm::build::LlvmBackendMode; +use roc_load::{FunctionKind, LoadingProblem, Threading}; +use roc_packaging::cache::{self, RocCacheDir}; +use roc_target::{get_target_triple_str, Target}; +use std::fs::{self, FileType}; +use std::io::{self, Read, Write}; +use std::path::{Path, PathBuf}; +use std::str::FromStr; +use target_lexicon::Triple; + +#[macro_use] +extern crate const_format; + +#[global_allocator] +static ALLOC: mimalloc::MiMalloc = mimalloc::MiMalloc; + +use std::ffi::{OsStr, OsString}; + +use roc_cli::build; + +fn main() -> io::Result<()> { + let _tracing_guards = roc_tracing::setup_tracing!(); + + let app = build_app(); + let subcommands: Vec = app + .get_subcommands() + .map(|c| c.get_name().to_owned()) + .collect(); + let matches = app.get_matches(); + + let exit_code = match matches.subcommand() { + None => { + if matches.contains_id(ROC_FILE) { + build( + &matches, + &subcommands, + BuildConfig::BuildAndRunIfNoErrors, + Triple::host(), + None, + RocCacheDir::Persistent(cache::roc_cache_dir().as_path()), + LinkType::Executable, + ) + } else { + Ok(1) + } + } + Some((CMD_RUN, matches)) => { + if matches.contains_id(ROC_FILE) { + build( + matches, + &subcommands, + BuildConfig::BuildAndRun, + Triple::host(), + None, + RocCacheDir::Persistent(cache::roc_cache_dir().as_path()), + LinkType::Executable, + ) + } else { + eprintln!("What .roc file do you want to run? Specify it at the end of the `roc run` command."); + + Ok(1) + } + } + Some((CMD_TEST, matches)) => { + if matches.contains_id(ROC_FILE) { + test(matches, Triple::host()) + } else { + eprintln!("What .roc file do you want to test? Specify it at the end of the `roc test` command."); + + Ok(1) + } + } + Some((CMD_DEV, matches)) => { + if matches.contains_id(ROC_FILE) { + build( + matches, + &subcommands, + BuildConfig::BuildAndRunIfNoErrors, + Triple::host(), + None, + RocCacheDir::Persistent(cache::roc_cache_dir().as_path()), + LinkType::Executable, + ) + } else { + eprintln!("What .roc file do you want to build? Specify it at the end of the `roc run` command."); + + Ok(1) + } + } + Some((CMD_GLUE, matches)) => { + let input_path = matches.get_one::(ROC_FILE).unwrap(); + let output_path = matches.get_one::(GLUE_DIR).unwrap(); + let spec_path = matches.get_one::(GLUE_SPEC).unwrap(); + + // have the backend supply `roc_alloc` and friends + let backend = match matches.get_flag(FLAG_DEV) { + true => CodeGenBackend::Assembly(AssemblyBackendMode::Test), + false => CodeGenBackend::Llvm(LlvmBackendMode::BinaryGlue), + }; + + if !output_path.exists() || output_path.is_dir() { + roc_glue::generate(input_path, output_path, spec_path, backend) + } else { + eprintln!("`roc glue` must be given a directory to output into, because the glue might generate multiple files."); + + Ok(1) + } + } + Some((CMD_GEN_STUB_LIB, matches)) => { + let input_path = matches.get_one::(ROC_FILE).unwrap(); + let target = matches + .get_one::(FLAG_TARGET) + .and_then(|s| Target::from_str(s).ok()) + .unwrap_or_default(); + let function_kind = FunctionKind::LambdaSet; + roc_linker::generate_stub_lib( + input_path, + RocCacheDir::Persistent(cache::roc_cache_dir().as_path()), + &target.to_triple(), + function_kind, + ); + Ok(0) + } + Some((CMD_PREPROCESS_HOST, matches)) => { + let input_path = matches.get_one::(ROC_FILE).unwrap(); + let target = matches + .get_one::(FLAG_TARGET) + .and_then(|s| Target::from_str(s).ok()) + .unwrap_or_default(); + + let triple = target.to_triple(); + let function_kind = FunctionKind::LambdaSet; + let (platform_path, stub_lib, stub_dll_symbols) = roc_linker::generate_stub_lib( + &input_path, + RocCacheDir::Persistent(cache::roc_cache_dir().as_path()), + &triple, + function_kind, + ); + + // TODO: pipeline the executable location through here. + // Currently it is essentally hardcoded as platform_path/dynhost. + roc_linker::preprocess_host( + &triple, + &platform_path.with_file_name("main.roc"), + // The target triple string must be derived from the triple to convert from the generic + // `system` target to the exact specific target. + &platform_path + .with_file_name(format!("{}.rh", get_target_triple_str(&triple).unwrap())), + &stub_lib, + &stub_dll_symbols, + ); + Ok(0) + } + Some((CMD_BUILD, matches)) => { + let target = matches + .get_one::(FLAG_TARGET) + .and_then(|s| Target::from_str(s).ok()) + .unwrap_or_default(); + let link_type = match (matches.get_flag(FLAG_LIB), matches.get_flag(FLAG_NO_LINK)) { + (true, false) => LinkType::Dylib, + (true, true) => user_error!("build can only be one of `--lib` or `--no-link`"), + (false, true) => LinkType::None, + (false, false) => LinkType::Executable, + }; + let out_path = matches + .get_one::(FLAG_OUTPUT) + .map(OsString::as_ref); + + Ok(build( + matches, + &subcommands, + BuildConfig::BuildOnly, + target.to_triple(), + out_path, + RocCacheDir::Persistent(cache::roc_cache_dir().as_path()), + link_type, + )?) + } + Some((CMD_CHECK, matches)) => { + let arena = Bump::new(); + + let emit_timings = matches.get_flag(FLAG_TIME); + let roc_file_path = matches.get_one::(ROC_FILE).unwrap(); + let threading = match matches.get_one::(roc_cli::FLAG_MAX_THREADS) { + None => Threading::AllAvailable, + Some(0) => user_error!("cannot build with at most 0 threads"), + Some(1) => Threading::Single, + Some(n) => Threading::AtMost(*n), + }; + + match check_file( + &arena, + roc_file_path.to_owned(), + emit_timings, + RocCacheDir::Persistent(cache::roc_cache_dir().as_path()), + threading, + ) { + Ok((problems, total_time)) => { + println!( + "\x1B[{}m{}\x1B[39m {} and \x1B[{}m{}\x1B[39m {} found in {} ms.", + if problems.errors == 0 { + 32 // green + } else { + 33 // yellow + }, + problems.errors, + if problems.errors == 1 { + "error" + } else { + "errors" + }, + if problems.warnings == 0 { + 32 // green + } else { + 33 // yellow + }, + problems.warnings, + if problems.warnings == 1 { + "warning" + } else { + "warnings" + }, + total_time.as_millis(), + ); + + Ok(problems.exit_code()) + } + + Err(LoadingProblem::FormattedReport(report)) => { + print!("{report}"); + + Ok(1) + } + Err(other) => { + panic!("build_file failed with error:\n{other:?}"); + } + } + } + Some((CMD_REPL, _)) => Ok(roc_repl_cli::main()), + Some((CMD_DOCS, matches)) => { + let root_path = matches.get_one::(ROC_FILE).unwrap(); + let out_dir = matches.get_one::(FLAG_OUTPUT).unwrap(); + + generate_docs_html(root_path.to_owned(), out_dir.as_ref()); + + Ok(0) + } + Some((CMD_FORMAT, matches)) => { + let from_stdin = matches.get_flag(FLAG_STDIN); + let to_stdout = matches.get_flag(FLAG_STDOUT); + let format_mode = if to_stdout { + FormatMode::WriteToStdout + } else { + match matches.get_flag(FLAG_CHECK) { + true => FormatMode::CheckOnly, + false => FormatMode::WriteToFile, + } + }; + + if from_stdin && matches!(format_mode, FormatMode::WriteToFile) { + eprintln!("When using the --stdin flag, either the --check or the --stdout flag must also be specified. (Otherwise, it's unclear what filename to write to!)"); + std::process::exit(1); + } + + let roc_files = { + let mut roc_files = Vec::new(); + + let mut values: Vec = Vec::new(); + + match matches.get_many::(DIRECTORY_OR_FILES) { + Some(os_values) => { + for os_string in os_values { + values.push(os_string.to_owned()); + } + } + None => { + let mut os_string_values: Vec = Vec::new(); + + read_all_roc_files( + &std::env::current_dir()?.as_os_str().to_os_string(), + &mut os_string_values, + )?; + + for os_string in os_string_values { + values.push(os_string); + } + } + } + + // Populate roc_files + for os_str in values { + let metadata = fs::metadata(os_str.clone())?; + roc_files_recursive(os_str.as_os_str(), metadata.file_type(), &mut roc_files)?; + } + + roc_files + }; + + let format_exit_code = if from_stdin { + let mut buf = Vec::new(); + let arena = Bump::new(); + + io::stdin().read_to_end(&mut buf)?; + + let src = std::str::from_utf8(&buf).unwrap_or_else(|err| { + eprintln!("Stdin contained invalid UTF-8 bytes: {err:?}"); + std::process::exit(1); + }); + + match format_src(&arena, src) { + Ok(formatted_src) => { + match format_mode { + FormatMode::CheckOnly => { + if src == formatted_src { + eprintln!("One or more files need to be reformatted."); + 1 + } else { + 0 + } + } + FormatMode::WriteToStdout => { + std::io::stdout() + .lock() + .write_all(formatted_src.as_bytes()) + .unwrap(); + + 0 + } + FormatMode::WriteToFile => { + // We would have errored out already if you specified --stdin + // without either --stdout or --check specified as well. + unreachable!() + } + } + } + Err(problem) => { + eprintln!("`roc format` failed: {problem:?}"); + 1 + } + } + } else { + match format_files(roc_files, format_mode) { + Ok(()) => 0, + Err(message) => { + eprintln!("{message}"); + 1 + } + } + }; + + Ok(format_exit_code) + } + Some((CMD_VERSION, _)) => { + print!( + "{}", + concatcp!("roc ", include_str!("../../../version.txt")) + ); + + Ok(0) + } + _ => unreachable!(), + }?; + + std::process::exit(exit_code); +} + +fn read_all_roc_files( + dir: &OsString, + roc_file_paths: &mut Vec, +) -> Result<(), std::io::Error> { + let entries = fs::read_dir(dir)?; + + for entry in entries { + let path = entry?.path(); + + if path.is_dir() { + read_all_roc_files(&path.into_os_string(), roc_file_paths)?; + } else if path.extension().and_then(OsStr::to_str) == Some("roc") { + let file_path = path.into_os_string(); + roc_file_paths.push(file_path); + } + } + + Ok(()) +} + +fn roc_files_recursive>( + path: P, + file_type: FileType, + roc_files: &mut Vec, +) -> io::Result<()> { + if file_type.is_dir() { + for entry_res in fs::read_dir(path)? { + let entry = entry_res?; + + roc_files_recursive(entry.path(), entry.file_type()?, roc_files)?; + } + } else { + roc_files.push(path.as_ref().to_path_buf()); + } + + Ok(()) +} diff --git a/crates/cli/tests/cli_run.rs b/crates/cli/tests/cli_run.rs new file mode 100644 index 0000000000..8bddb5465f --- /dev/null +++ b/crates/cli/tests/cli_run.rs @@ -0,0 +1,1497 @@ +#[macro_use] +extern crate pretty_assertions; + +extern crate bumpalo; +extern crate indoc; +extern crate roc_collections; +extern crate roc_load; +extern crate roc_module; + +#[cfg(test)] +mod cli_run { + use cli_utils::helpers::{ + extract_valgrind_errors, file_path_from_root, fixture_file, fixtures_dir, has_error, + known_bad_file, run_cmd, run_roc, run_with_valgrind, strip_colors, Out, ValgrindError, + ValgrindErrorXWhat, + }; + use const_format::concatcp; + use indoc::indoc; + use roc_cli::{CMD_BUILD, CMD_CHECK, CMD_DEV, CMD_FORMAT, CMD_RUN, CMD_TEST}; + use roc_test_utils::assert_multiline_str_eq; + use serial_test::serial; + use std::iter; + use std::path::Path; + + #[cfg(all(unix, not(target_os = "macos")))] + const ALLOW_VALGRIND: bool = true; + + // Disallow valgrind on macOS by default, because it reports a ton + // of false positives. For local development on macOS, feel free to + // change this to true! + #[cfg(target_os = "macos")] + const ALLOW_VALGRIND: bool = false; + + #[cfg(windows)] + const ALLOW_VALGRIND: bool = false; + + // use valgrind (if supported on the current platform) + #[derive(Debug, Clone, Copy)] + enum UseValgrind { + Yes, + No, + } + + #[derive(Debug, Clone, Copy)] + enum TestCliCommands { + Many, + Run, + Test, + Dev, + } + + const OPTIMIZE_FLAG: &str = concatcp!("--", roc_cli::FLAG_OPTIMIZE); + const LINKER_FLAG: &str = concatcp!("--", roc_cli::FLAG_LINKER); + const CHECK_FLAG: &str = concatcp!("--", roc_cli::FLAG_CHECK); + const PREBUILT_PLATFORM: &str = concatcp!("--", roc_cli::FLAG_PREBUILT); + #[allow(dead_code)] + const TARGET_FLAG: &str = concatcp!("--", roc_cli::FLAG_TARGET); + + #[derive(Debug)] + enum CliMode { + Roc, // buildAndRunIfNoErrors + RocBuild, // buildOnly + RocRun, // buildAndRun + RocTest, + RocDev, + } + + #[cfg(all(target_os = "linux", target_arch = "x86_64"))] + const TEST_LEGACY_LINKER: bool = true; + + // Surgical linker currently only supports linux x86_64, + // so we're always testing the legacy linker on other targets. + #[cfg(not(all(target_os = "linux", target_arch = "x86_64")))] + const TEST_LEGACY_LINKER: bool = false; + + #[derive(Debug, PartialEq, Eq)] + enum Arg<'a> { + ExamplePath(&'a str), + PlainText(&'a str), + } + + fn check_compile_error(file: &Path, flags: &[&str], expected: &str) { + let compile_out = run_roc( + [CMD_CHECK, file.to_str().unwrap()].iter().chain(flags), + &[], + &[], + ); + let err = compile_out.stdout.trim(); + let err = strip_colors(err); + + // e.g. "1 error and 0 warnings found in 123 ms." + let (before_first_digit, _) = err.split_at(err.rfind("found in ").unwrap()); + let err = format!("{before_first_digit}found in ms."); + + // make paths consistent + let err = err.replace('\\', "/"); + + // consistency with typewriters, very important + let err = err.replace('\r', ""); + + assert_multiline_str_eq!(err.as_str(), expected); + } + + fn check_format_check_as_expected(file: &Path, expects_success_exit_code: bool) { + let out = run_roc([CMD_FORMAT, file.to_str().unwrap(), CHECK_FLAG], &[], &[]); + + assert_eq!(out.status.success(), expects_success_exit_code); + } + + fn run_roc_on_failure_is_panic<'a, I: IntoIterator>( + file: &'a Path, + args: I, + stdin: &[&str], + roc_app_args: &[String], + env: &[(&str, &str)], + ) -> Out { + let compile_out = run_roc_on(file, args, stdin, roc_app_args, env); + + assert!( + compile_out.status.success(), + "\n___________\nRoc command failed with status {:?}:\n\n {} {}\n___________\n", + compile_out.status, + compile_out.stdout, + compile_out.stderr, + ); + + compile_out + } + + fn run_roc_on<'a, I: IntoIterator>( + file: &'a Path, + args: I, + stdin: &[&str], + roc_app_args: &[String], + env: &[(&str, &str)], + ) -> Out { + let compile_out = run_roc( + // converting these all to String avoids lifetime issues + args.into_iter() + .map(|arg| arg.to_string()) + .chain([file.to_str().unwrap().to_string(), "--".to_string()]) + .chain(roc_app_args.iter().cloned()), + stdin, + env, + ); + + if has_error(&compile_out.stderr) { + panic!("\n___________\nThe roc command:\n\n {:?}\n\nhad unexpected stderr:\n\n {}\n___________\n", compile_out.cmd_str, compile_out.stderr); + } + + compile_out + } + + #[allow(clippy::too_many_arguments)] + fn check_output_with_stdin( + file: &Path, + stdin: &[&str], + flags: &[&str], + roc_app_args: &[String], + extra_env: &[(&str, &str)], + expected_ending: &str, + use_valgrind: UseValgrind, + test_cli_commands: TestCliCommands, + ) { + // valgrind does not yet support avx512 instructions, see #1963. + // we can't enable this only when testing with valgrind because of host re-use between tests + #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] + if is_x86_feature_detected!("avx512f") { + std::env::set_var("NO_AVX512", "1"); + } + + // TODO: expects don't currently work on windows + let cli_commands = if cfg!(windows) { + match test_cli_commands { + TestCliCommands::Many => vec![CliMode::RocBuild, CliMode::RocRun], + TestCliCommands::Run => vec![CliMode::RocRun], + TestCliCommands::Test => vec![], + TestCliCommands::Dev => vec![], + } + } else { + match test_cli_commands { + TestCliCommands::Many => vec![CliMode::RocBuild, CliMode::RocRun, CliMode::Roc], + TestCliCommands::Run => vec![CliMode::Roc], + TestCliCommands::Test => vec![CliMode::RocTest], + TestCliCommands::Dev => vec![CliMode::RocDev], + } + }; + + for cli_mode in cli_commands.iter() { + let flags = { + let mut vec = flags.to_vec(); + + // max-threads segfaults on windows right now + if !cfg!(windows) { + vec.push("--max-threads=1"); + } + + vec.into_iter() + }; + + let out = match cli_mode { + CliMode::RocBuild => { + run_roc_on_failure_is_panic( + file, + iter::once(CMD_BUILD).chain(flags.clone()), + &[], + &[], + &[], + ); + + let file_ext = if cfg!(windows) { "exe " } else { "" }; + + if matches!(use_valgrind, UseValgrind::Yes) && ALLOW_VALGRIND { + let mut valgrind_args = + vec![file.with_extension(file_ext).to_str().unwrap().to_string()]; + valgrind_args.extend(roc_app_args.iter().cloned()); + let (valgrind_out, raw_xml) = + run_with_valgrind(stdin.iter().copied(), &valgrind_args); + if valgrind_out.status.success() { + let memory_errors = extract_valgrind_errors(&raw_xml).unwrap_or_else(|err| { + panic!("failed to parse the `valgrind` xml output:\n\n Error was:\n\n {:?}\n\n valgrind xml was:\n\n \"{}\"\n\n valgrind stdout was:\n\n \"{}\"\n\n valgrind stderr was:\n\n \"{}\"", err, raw_xml, valgrind_out.stdout, valgrind_out.stderr); + }); + + if !memory_errors.is_empty() { + for error in memory_errors { + let ValgrindError { + kind, + what: _, + xwhat, + } = error; + println!("Valgrind Error: {kind}\n"); + + if let Some(ValgrindErrorXWhat { + text, + leakedbytes: _, + leakedblocks: _, + }) = xwhat + { + println!(" {text}"); + } + } + panic!("Valgrind reported memory errors"); + } + } else { + let exit_code = match valgrind_out.status.code() { + Some(code) => format!("exit code {code}"), + None => "no exit code".to_string(), + }; + + panic!("`valgrind` exited with {}. valgrind stdout was: \"{}\"\n\nvalgrind stderr was: \"{}\"", exit_code, valgrind_out.stdout, valgrind_out.stderr); + } + + valgrind_out + } else { + run_cmd( + file.with_extension(file_ext).to_str().unwrap(), + stdin.iter().copied(), + roc_app_args, + extra_env.iter().copied(), + ) + } + } + CliMode::Roc => { + run_roc_on_failure_is_panic(file, flags.clone(), stdin, roc_app_args, extra_env) + } + CliMode::RocRun => run_roc_on_failure_is_panic( + file, + iter::once(CMD_RUN).chain(flags.clone()), + stdin, + roc_app_args, + extra_env, + ), + CliMode::RocTest => { + // here failure is what we expect + + run_roc_on( + file, + iter::once(CMD_TEST).chain(flags.clone()), + stdin, + roc_app_args, + extra_env, + ) + } + CliMode::RocDev => { + // here failure is what we expect + + run_roc_on( + file, + iter::once(CMD_DEV).chain(flags.clone()), + stdin, + roc_app_args, + extra_env, + ) + } + }; + + let mut actual = strip_colors(&out.stdout); + + // e.g. "1 failed and 0 passed in 123 ms." + if let Some(split) = actual.rfind("passed in ") { + let (before_first_digit, _) = actual.split_at(split); + actual = format!("{before_first_digit}passed in ms."); + } + + let self_path = file.display().to_string(); + actual = actual.replace(&self_path, ""); + + if !actual.ends_with(expected_ending) { + panic!( + "> expected output to end with:\n{}\n> but instead got:\n{}\n> stderr was:\n{}", + expected_ending, actual, out.stderr + ); + } + + if !out.status.success() && !matches!(cli_mode, CliMode::RocTest) { + // We don't need stdout, Cargo prints it for us. + panic!( + "Example program exited with status {:?}\nstderr was:\n{:#?}", + out.status, out.stderr + ); + } + } + } + + // when you want to run `roc test` to execute `expect`s, perhaps on a library rather than an application. + fn test_roc_expect(dir_name: &str, roc_filename: &str) { + let path = file_path_from_root(dir_name, roc_filename); + let out = run_roc([CMD_TEST, path.to_str().unwrap()], &[], &[]); + assert!(out.status.success()); + } + + // when you don't need args, stdin or extra_env + fn test_roc_app_slim( + dir_name: &str, + roc_filename: &str, + expected_ending: &str, + use_valgrind: UseValgrind, + ) { + test_roc_app( + dir_name, + roc_filename, + &[], + &[], + &[], + expected_ending, + use_valgrind, + TestCliCommands::Run, + ) + } + + #[allow(clippy::too_many_arguments)] + fn test_roc_app( + dir_name: &str, + roc_filename: &str, + stdin: &[&str], + args: &[Arg], + extra_env: &[(&str, &str)], + expected_ending: &str, + use_valgrind: UseValgrind, + test_cli_commands: TestCliCommands, + ) { + let file_name = file_path_from_root(dir_name, roc_filename); + let mut roc_app_args: Vec = Vec::new(); + + for arg in args { + match arg { + Arg::ExamplePath(file) => { + roc_app_args.push( + file_path_from_root(dir_name, file) + .to_str() + .unwrap() + .to_string(), + ); + } + Arg::PlainText(arg) => { + roc_app_args.push(arg.to_string()); + } + } + } + + // workaround for surgical linker issue, see PR #3990 + let mut custom_flags: Vec<&str> = Vec::new(); + + if dir_name.to_ascii_lowercase().contains("webassembly") + || roc_filename.to_ascii_lowercase().contains("webassembly") + { + // this is a web assembly example, but we don't test with JS at the moment + eprintln!( + "WARNING: skipping testing example {roc_filename} because it only works in a browser!" + ); + return; + } + + // Only run Swift examples on macOS + if dir_name.to_ascii_lowercase().contains("swift") + || roc_filename.to_ascii_lowercase().contains("swift") + { + if cfg!(target_os = "macos") { + run_roc_on(&file_name, [CMD_BUILD, OPTIMIZE_FLAG], &[], &[], &[]); + return; + } else { + eprintln!( + "WARNING: skipping testing example {roc_filename} because it only works on MacOS." + ); + return; + } + } + + if dir_name.starts_with("examples/gui") + || roc_filename.ends_with("gui.roc") + || dir_name.ends_with("-interop") + { + // Since these require things the build system often doesn't have + // (e.g. GUIs open a window, interop needs a language installed) + // we do `roc build` on them but don't run them. + run_roc_on(&file_name, [CMD_BUILD, OPTIMIZE_FLAG], &[], &[], &[]); + return; + } else if roc_filename == "args.roc" { + custom_flags = vec![LINKER_FLAG, "legacy"]; + } + + // Check with and without optimizations + check_output_with_stdin( + &file_name, + stdin, + &custom_flags, + &roc_app_args, + extra_env, + expected_ending, + use_valgrind, + test_cli_commands, + ); + + custom_flags.push(OPTIMIZE_FLAG); + // This is mostly because the false interpreter is still very slow - + // 25s for the cli tests is just not acceptable during development! + #[cfg(not(debug_assertions))] + check_output_with_stdin( + &file_name, + stdin, + &custom_flags, + &roc_app_args, + extra_env, + expected_ending, + use_valgrind, + test_cli_commands, + ); + + // Also check with the legacy linker. + + if TEST_LEGACY_LINKER { + check_output_with_stdin( + &file_name, + stdin, + &[LINKER_FLAG, "legacy"], + &roc_app_args, + extra_env, + expected_ending, + use_valgrind, + test_cli_commands, + ); + } + } + + #[test] + #[serial(zig_platform_parser_package_basic_cli_url)] + #[cfg_attr(windows, ignore)] + fn hello_world() { + test_roc_app_slim( + "examples", + "helloWorld.roc", + "Hello, World!\n", + UseValgrind::Yes, + ) + } + + #[cfg(windows)] + const LINE_ENDING: &str = "\r\n"; + #[cfg(not(windows))] + const LINE_ENDING: &str = "\n"; + + #[test] + #[cfg_attr(windows, ignore)] + // uses C platform + fn platform_switching_main() { + test_roc_app_slim( + "examples/platform-switching", + "main.roc", + &("Which platform am I running on now?".to_string() + LINE_ENDING), + UseValgrind::Yes, + ) + } + + // We exclude the C platforming switching example + // because the main platform switching example runs the c platform. + // If we don't, a race condition leads to test flakiness. + + #[test] + #[cfg_attr(windows, ignore)] + fn platform_switching_rust() { + test_roc_app_slim( + "examples/platform-switching", + "rocLovesRust.roc", + "Roc <3 Rust!\n", + UseValgrind::Yes, + ) + } + + // zig_platform_parser_package_basic_cli_url use to be split up but then things could get stuck + #[test] + #[serial(zig_platform_parser_package_basic_cli_url)] + #[cfg_attr(windows, ignore)] + fn platform_switching_zig() { + test_roc_app_slim( + "examples/platform-switching", + "rocLovesZig.roc", + "Roc <3 Zig!\n", + UseValgrind::Yes, + ) + } + + #[test] + fn platform_switching_wasm() { + test_roc_app_slim( + "examples/platform-switching", + "rocLovesWebAssembly.roc", + "Roc <3 Web Assembly!\n", + UseValgrind::Yes, + ) + } + + #[test] + fn platform_switching_swift() { + test_roc_app_slim( + "examples/platform-switching", + "rocLovesSwift.roc", + "Roc <3 Swift!\n", + UseValgrind::Yes, + ) + } + + #[test] + fn expects_dev_and_test() { + // these are in the same test function so we don't have to worry about race conditions + // on the building of the platform + + test_roc_app( + "crates/cli_testing_examples/expects", + "expects.roc", + &[], + &[], + &[], + indoc!( + r#" + This expectation failed: + + 19│ expect words == [] + ^^^^^^^^^^^ + + When it failed, these variables had these values: + + words : List Str + words = ["this", "will", "for", "sure", "be", "a", "large", "string", "so", "when", "we", "split", "it", "it", "will", "use", "seamless", "slices", "which", "affect", "printing"] + + [#UserApp] 42 + [#UserApp] "Fjoer en ferdjer frieten oan dyn geve lea" + [#UserApp] "abc" + [#UserApp] 10 + [#UserApp] (A (B C)) + Program finished! + "# + ), + UseValgrind::Yes, + TestCliCommands::Dev, + ); + + test_roc_app( + "crates/cli_testing_examples/expects", + "expects.roc", + &[], + &[], + &[], + indoc!( + r#" + This expectation failed: + + 6│> expect + 7│> a = 1 + 8│> b = 2 + 9│> + 10│> a == b + + When it failed, these variables had these values: + + a : Num * + a = 1 + + b : Num * + b = 2 + + + + 1 failed and 0 passed in ms."# + ), + UseValgrind::Yes, + TestCliCommands::Test, + ); + } + + #[test] + #[cfg_attr( + windows, + ignore = "this platform is broken, and `roc run --lib` is missing on windows" + )] + fn ruby_interop() { + test_roc_app_slim( + "examples/ruby-interop", + "libhello.roc", + "", + UseValgrind::Yes, + ) + } + + #[test] + #[cfg_attr( + windows, + ignore = "Flaky failure: Roc command failed with status ExitStatus(ExitStatus(3221225477))" + )] + fn fibonacci() { + test_roc_app_slim( + "crates/cli_testing_examples/algorithms", + "fibonacci.roc", + "", + UseValgrind::Yes, + ) + } + + #[test] + fn hello_gui() { + test_roc_app_slim("examples/gui", "hello-guiBROKEN.roc", "", UseValgrind::No) + } + + #[cfg_attr(windows, ignore)] // flaky error; issue #5024 + #[serial(breakout)] + #[test] + fn breakout() { + test_roc_app_slim( + "examples/gui/breakout", + "breakoutBROKEN.roc", + "", + UseValgrind::No, + ) + } + + #[test] + #[serial(breakout)] + fn breakout_hello_gui() { + test_roc_app_slim( + "examples/gui/breakout", + "hello-guiBROKEN.roc", + "", + UseValgrind::No, + ) + } + + #[test] + #[cfg_attr(windows, ignore)] + fn quicksort() { + test_roc_app_slim( + "crates/cli_testing_examples/algorithms", + "quicksort.roc", + "[0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2]\n", + UseValgrind::Yes, + ) + } + + #[test] + #[ignore = "currently broken in basic-cli platform"] + #[cfg_attr(windows, ignore = "missing __udivdi3 and some other symbols")] + #[serial(cli_platform)] + fn cli_args() { + test_roc_app( + "examples/cli", + "argsBROKEN.roc", + &[], + &[ + Arg::PlainText("log"), + Arg::PlainText("-b"), + Arg::PlainText("3"), + Arg::PlainText("--num"), + Arg::PlainText("81"), + ], + &[], + "4\n", + UseValgrind::No, + TestCliCommands::Run, + ) + } + + // TODO: remove in favor of cli_args once mono bugs are resolved in investigation + #[test] + #[cfg_attr(windows, ignore = "missing __udivdi3 and some other symbols")] + #[serial(cli_platform)] + fn cli_args_check() { + let path = file_path_from_root("examples/cli", "argsBROKEN.roc"); + let out = run_roc([CMD_CHECK, path.to_str().unwrap()], &[], &[]); + assert!(out.status.success()); + } + + // TODO: write a new test once mono bugs are resolved in investigation + #[test] + #[cfg(not(debug_assertions))] // https://github.com/roc-lang/roc/issues/4806 + fn check_virtual_dom_server() { + let path = file_path_from_root("examples/virtual-dom-wip", "example-server.roc"); + let out = run_roc([CMD_CHECK, path.to_str().unwrap()], &[], &[]); + assert!(out.status.success()); + } + + // TODO: write a new test once mono bugs are resolved in investigation + #[test] + #[cfg(not(debug_assertions))] // https://github.com/roc-lang/roc/issues/4806 + fn check_virtual_dom_client() { + let path = file_path_from_root("examples/virtual-dom-wip", "example-client.roc"); + let out = run_roc([CMD_CHECK, path.to_str().unwrap()], &[], &[]); + assert!(out.status.success()); + } + + #[test] + #[cfg_attr(windows, ignore)] + #[serial(cli_platform)] + fn cli_countdown_check() { + let path = file_path_from_root("examples/cli", "countdown.roc"); + let out = run_roc([CMD_CHECK, path.to_str().unwrap()], &[], &[]); + assert!(out.status.success()); + } + + #[test] + #[cfg_attr(windows, ignore)] + #[serial(cli_platform)] + fn cli_echo_check() { + let path = file_path_from_root("examples/cli", "echo.roc"); + let out = run_roc([CMD_CHECK, path.to_str().unwrap()], &[], &[]); + assert!(out.status.success()); + } + + #[test] + #[cfg_attr(windows, ignore)] + #[serial(cli_platform)] + fn cli_file_check() { + let path = file_path_from_root("examples/cli", "fileBROKEN.roc"); + let out = run_roc([CMD_CHECK, path.to_str().unwrap()], &[], &[]); + assert!(out.status.success()); + } + + #[test] + #[cfg_attr(windows, ignore)] + #[serial(cli_platform)] + fn cli_form_check() { + let path = file_path_from_root("examples/cli", "form.roc"); + let out = run_roc([CMD_CHECK, path.to_str().unwrap()], &[], &[]); + assert!(out.status.success()); + } + + #[test] + #[cfg_attr(windows, ignore)] + #[serial(cli_platform)] + fn cli_http_get_check() { + let path = file_path_from_root("examples/cli", "http-get.roc"); + let out = run_roc([CMD_CHECK, path.to_str().unwrap()], &[], &[]); + assert!(out.status.success()); + } + + #[test] + #[cfg_attr(windows, ignore)] + fn interactive_effects() { + test_roc_app( + "examples/cli", + "effects.roc", + &["hi there!"], + &[], + &[], + "hi there!\nIt is known\n", + UseValgrind::Yes, + TestCliCommands::Run, + ) + } + + #[test] + #[cfg_attr(windows, ignore)] + // tea = The Elm Architecture + fn terminal_ui_tea() { + test_roc_app( + "examples/cli", + "tui.roc", + &["foo\n"], // NOTE: adding more lines leads to memory leaks + &[], + &[], + "Hello Worldfoo!\n", + UseValgrind::Yes, + TestCliCommands::Run, + ) + } + + #[test] + #[cfg_attr(any(target_os = "windows", target_os = "linux"), ignore = "Segfault")] + fn false_interpreter() { + test_roc_app( + "examples/cli/false-interpreter", + "False.roc", + &[], + &[Arg::ExamplePath("examples/sqrt.false")], + &[], + "1414", + UseValgrind::Yes, + TestCliCommands::Many, + ) + } + + #[test] + fn swift_ui() { + test_roc_app_slim("examples/swiftui", "main.roc", "", UseValgrind::No) + } + + #[test] + #[cfg_attr(windows, ignore)] + fn static_site_gen() { + test_roc_app( + "examples/static-site-gen", + "static-site.roc", + &[], + &[Arg::ExamplePath("input"), Arg::ExamplePath("output")], + &[], + "Processed 4 files with 3 successes and 0 errors\n", + UseValgrind::No, + TestCliCommands::Run, + ) + } + + #[test] + #[serial(cli_platform)] + #[cfg_attr(windows, ignore)] + fn with_env_vars() { + test_roc_app( + "examples/cli", + "env.roc", + &[], + &[], + &[ + ("EDITOR", "roc-editor"), + ("SHLVL", "3"), + ("LETTERS", "a,c,e,j"), + ], + "Your favorite editor is roc-editor!\n\ + Your current shell level is 3!\n\ + Your favorite letters are: a c e j\n", + UseValgrind::No, + TestCliCommands::Run, + ) + } + + #[test] + #[serial(cli_platform)] + #[cfg_attr(windows, ignore)] + fn ingested_file() { + test_roc_app( + "examples/cli", + "ingested-file.roc", + &[], + &[], + &[], + indoc!( + r#" + This roc file can print it's own source code. The source is: + + app "ingested-file" + packages { pf: "https://github.com/roc-lang/basic-cli/releases/download/0.7.0/bkGby8jb0tmZYsy2hg1E_B2QrCgcSTxdUlHtETwm5m4.tar.br" } + imports [ + pf.Stdout, + "ingested-file.roc" as ownCode : Str, + ] + provides [main] to pf + + main = + Stdout.line "\nThis roc file can print it's own source code. The source is:\n\n\(ownCode)" + + "# + ), + UseValgrind::No, + TestCliCommands::Run, + ) + } + + #[test] + #[serial(cli_platform)] + #[cfg_attr(windows, ignore)] + fn ingested_file_bytes() { + test_roc_app( + "examples/cli", + "ingested-file-bytes.roc", + &[], + &[], + &[], + "30461\n", + UseValgrind::No, + TestCliCommands::Run, + ) + } + + #[test] + #[serial(zig_platform_parser_package_basic_cli_url)] + #[cfg_attr(windows, ignore)] + fn parse_movies_csv() { + test_roc_app_slim( + "examples/parser/examples", + "parse-movies-csv.roc", + "Parse success!\n", + UseValgrind::No, + ) + } + + #[test] + #[serial(zig_platform_parser_package_basic_cli_url)] + #[cfg_attr(windows, ignore)] + fn parse_letter_counts() { + test_roc_app_slim( + "examples/parser/examples", + "letter-counts.roc", + "I counted 7 letter A's!\n", + UseValgrind::No, + ) + } + + #[test] + #[cfg_attr(windows, ignore)] + fn parse_http() { + test_roc_expect("examples/parser/package", "ParserHttp.roc") + } + + #[test] + #[cfg_attr(windows, ignore)] + fn inspect_logging() { + test_roc_app_slim( + "examples", + "inspect-logging.roc", + r#"{friends: [{2}, {2}, {0, 1}], people: [{age: 27, favoriteColor: Blue, firstName: "John", hasBeard: Bool.true, lastName: "Smith"}, {age: 47, favoriteColor: Green, firstName: "Debby", hasBeard: Bool.false, lastName: "Johnson"}, {age: 33, favoriteColor: (RGB (255, 255, 0)), firstName: "Jane", hasBeard: Bool.false, lastName: "Doe"}]} +"#, + UseValgrind::Yes, + ) + } + + #[test] + fn inspect_gui() { + test_roc_app_slim("examples", "inspect-gui.roc", "", UseValgrind::No) + } + + // TODO not sure if this cfg should still be here: #[cfg(not(debug_assertions))] + // this is for testing the benchmarks, to perform proper benchmarks see crates/cli/benches/README.md + mod test_benchmarks { + use super::{TestCliCommands, UseValgrind}; + use cli_utils::helpers::cli_testing_dir; + + use super::{check_output_with_stdin, OPTIMIZE_FLAG, PREBUILT_PLATFORM}; + + use std::{path::Path, sync::Once}; + + static BENCHMARKS_BUILD_PLATFORM: Once = Once::new(); + + fn test_benchmark( + roc_filename: &str, + stdin: &[&str], + expected_ending: &str, + use_valgrind: UseValgrind, + ) { + let file_name = cli_testing_dir("benchmarks").join(roc_filename); + + // TODO fix QuicksortApp and then remove this! + match roc_filename { + "QuicksortApp.roc" => { + eprintln!( + "WARNING: skipping testing benchmark {roc_filename} because the test is broken right now!" + ); + return; + } + "TestAStar.roc" => { + if cfg!(feature = "wasm32-cli-run") { + eprintln!( + "WARNING: skipping testing benchmark {roc_filename} because it currently does not work on wasm32 due to dictionaries." + ); + return; + } + } + _ => {} + } + + #[cfg(all(not(feature = "wasm32-cli-run"), not(feature = "i386-cli-run")))] + check_output_regular(&file_name, stdin, expected_ending, use_valgrind); + + #[cfg(feature = "wasm32-cli-run")] + check_output_wasm(&file_name, stdin, expected_ending); + + #[cfg(feature = "i386-cli-run")] + check_output_i386(&file_name, stdin, expected_ending, use_valgrind); + } + + #[cfg(all(not(feature = "wasm32-cli-run"), not(feature = "i386-cli-run")))] + fn check_output_regular( + file_name: &Path, + stdin: &[&str], + expected_ending: &str, + use_valgrind: UseValgrind, + ) { + let mut ran_without_optimizations = false; + + BENCHMARKS_BUILD_PLATFORM.call_once(|| { + // Check with and without optimizations + check_output_with_stdin( + file_name, + stdin, + &[], + &[], + &[], + expected_ending, + use_valgrind, + TestCliCommands::Run, + ); + + ran_without_optimizations = true; + }); + + // now we can pass the `PREBUILT_PLATFORM` flag, because the + // `call_once` will have built the platform + + if !ran_without_optimizations { + // Check with and without optimizations + check_output_with_stdin( + file_name, + stdin, + &[PREBUILT_PLATFORM], + &[], + &[], + expected_ending, + use_valgrind, + TestCliCommands::Run, + ); + } + + check_output_with_stdin( + file_name, + stdin, + &[PREBUILT_PLATFORM, OPTIMIZE_FLAG], + &[], + &[], + expected_ending, + use_valgrind, + TestCliCommands::Run, + ); + } + + #[cfg(feature = "wasm32-cli-run")] + fn check_output_wasm(file_name: &Path, stdin: &[&str], expected_ending: &str) { + // Check with and without optimizations + check_wasm_output_with_stdin(file_name, stdin, &[], expected_ending); + + check_wasm_output_with_stdin(file_name, stdin, &[OPTIMIZE_FLAG], expected_ending); + } + + #[cfg(feature = "wasm32-cli-run")] + fn check_wasm_output_with_stdin( + file: &Path, + stdin: &[&str], + flags: &[&str], + expected_ending: &str, + ) { + use super::{concatcp, run_roc, CMD_BUILD, TARGET_FLAG}; + + let mut flags = flags.to_vec(); + flags.push(concatcp!(TARGET_FLAG, "=wasm32")); + + let compile_out = run_roc( + [CMD_BUILD, file.to_str().unwrap()] + .iter() + .chain(flags.as_slice()), + &[], + &[], + ); + + assert!( + compile_out.status.success(), + "bad status stderr:\n{}\nstdout:\n{}", + compile_out.stderr, + compile_out.stdout + ); + + let stdout = crate::run_wasm(&file.with_extension("wasm"), stdin); + + if !stdout.ends_with(expected_ending) { + panic!( + "expected output to end with {:?} but instead got {:#?}", + expected_ending, stdout + ); + } + } + + #[cfg(feature = "i386-cli-run")] + fn check_output_i386( + file_name: &Path, + stdin: &[&str], + expected_ending: &str, + use_valgrind: UseValgrind, + ) { + use super::{concatcp, CMD_BUILD, TARGET_FLAG}; + + check_output_with_stdin( + &file_name, + stdin, + &[concatcp!(TARGET_FLAG, "=x86_32")], + &[], + &[], + expected_ending, + use_valgrind, + TestCliCommands::Run, + ); + + check_output_with_stdin( + &file_name, + stdin, + &[concatcp!(TARGET_FLAG, "=x86_32"), OPTIMIZE_FLAG], + &[], + &[], + expected_ending, + use_valgrind, + TestCliCommands::Run, + ); + } + + #[test] + #[cfg_attr(windows, ignore)] + fn nqueens() { + test_benchmark("NQueens.roc", &["6"], "4\n", UseValgrind::Yes) + } + + #[test] + #[cfg_attr(windows, ignore)] + fn cfold() { + test_benchmark("CFold.roc", &["3"], "11 & 11\n", UseValgrind::Yes) + } + + #[test] + #[cfg_attr(windows, ignore)] + fn deriv() { + test_benchmark( + "Deriv.roc", + &["2"], + "1 count: 6\n2 count: 22\n", + UseValgrind::Yes, + ) + } + + #[test] + #[cfg_attr(windows, ignore)] + fn rbtree_ck() { + test_benchmark("RBTreeCk.roc", &["100"], "10\n", UseValgrind::Yes) + } + + #[test] + #[cfg_attr(windows, ignore)] + fn rbtree_insert() { + test_benchmark( + "RBTreeInsert.roc", + &[], + "Node Black 0 {} Empty Empty\n", + UseValgrind::Yes, + ) + } + + /* + // rbtree_del does not work + #[test] + fn rbtree_del() { + test_benchmark( + "RBTreeDel.roc", + &["420"], + &[], + "30\n", + true + ) + }*/ + + #[test] + #[cfg_attr(windows, ignore)] + fn astar() { + test_benchmark("TestAStar.roc", &[], "True\n", UseValgrind::No) + } + + #[test] + #[cfg_attr(windows, ignore)] + fn base64() { + test_benchmark( + "TestBase64.roc", + &[], + "encoded: SGVsbG8gV29ybGQ=\ndecoded: Hello World\n", + UseValgrind::Yes, + ) + } + + #[test] + #[cfg_attr(windows, ignore)] + fn closure() { + test_benchmark("Closure.roc", &[], "", UseValgrind::No) + } + + #[test] + #[cfg_attr(windows, ignore)] + fn issue2279() { + test_benchmark("Issue2279.roc", &[], "Hello, world!\n", UseValgrind::Yes) + } + + #[test] + fn quicksort_app() { + test_benchmark( + "QuicksortApp.roc", + &[], + "todo put the correct quicksort answer here", + UseValgrind::Yes, + ) + } + } + + #[test] + #[serial(multi_dep_str)] + #[cfg_attr(windows, ignore)] + fn run_multi_dep_str_unoptimized() { + check_output_with_stdin( + &fixture_file("multi-dep-str", "Main.roc"), + &[], + &[], + &[], + &[], + "I am Dep2.str2\n", + UseValgrind::Yes, + TestCliCommands::Run, + ); + } + + #[test] + #[serial(multi_dep_str)] + #[cfg_attr(windows, ignore)] + fn run_multi_dep_str_optimized() { + check_output_with_stdin( + &fixture_file("multi-dep-str", "Main.roc"), + &[], + &[OPTIMIZE_FLAG], + &[], + &[], + "I am Dep2.str2\n", + UseValgrind::Yes, + TestCliCommands::Run, + ); + } + + #[test] + #[serial(multi_dep_thunk)] + #[cfg_attr(windows, ignore)] + fn run_multi_dep_thunk_unoptimized() { + check_output_with_stdin( + &fixture_file("multi-dep-thunk", "Main.roc"), + &[], + &[], + &[], + &[], + "I am Dep2.value2\n", + UseValgrind::Yes, + TestCliCommands::Run, + ); + } + + #[test] + #[serial(multi_dep_thunk)] + #[cfg_attr( + windows, + ignore = "Flaky failure: Roc command failed with status ExitStatus(ExitStatus(3221225477))" + )] + fn run_multi_dep_thunk_optimized() { + check_output_with_stdin( + &fixture_file("multi-dep-thunk", "Main.roc"), + &[], + &[OPTIMIZE_FLAG], + &[], + &[], + "I am Dep2.value2\n", + UseValgrind::Yes, + TestCliCommands::Run, + ); + } + + #[test] + #[serial(multi_dep_thunk)] + #[cfg_attr(windows, ignore)] + fn run_packages_unoptimized() { + check_output_with_stdin( + &fixture_file("packages", "app.roc"), + &[], + &[], + &[], + &[], + "Hello, World! This text came from a package! This text came from a CSV package!\n", + UseValgrind::Yes, + TestCliCommands::Run, + ); + } + + #[test] + #[serial(multi_dep_thunk)] + #[cfg_attr(windows, ignore)] + fn run_packages_optimized() { + check_output_with_stdin( + &fixture_file("packages", "app.roc"), + &[], + &[OPTIMIZE_FLAG], + &[], + &[], + "Hello, World! This text came from a package! This text came from a CSV package!\n", + UseValgrind::Yes, + TestCliCommands::Run, + ); + } + + #[test] + fn known_type_error() { + check_compile_error( + &known_bad_file("TypeError.roc"), + &[], + indoc!( + r#" + ── TYPE MISMATCH ─────────────────────────────── tests/known_bad/TypeError.roc ─ + + Something is off with the body of the main definition: + + 6│ main : Str -> Task {} [] + 7│ main = /_ -> + 8│ "this is a string, not a Task {} [] function like the platform expects." + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + The body is a string of type: + + Str + + But the type annotation on main says it should be: + + Effect.Effect (Result {} []) + + Tip: Type comparisons between an opaque type are only ever equal if + both types are the same opaque type. Did you mean to create an opaque + type by wrapping it? If I have an opaque type Age := U32 I can create + an instance of this opaque type by doing @Age 23. + + ──────────────────────────────────────────────────────────────────────────────── + + 1 error and 0 warnings found in ms."# + ), + ); + } + + #[test] + fn exposed_not_defined() { + check_compile_error( + &known_bad_file("ExposedNotDefined.roc"), + &[], + indoc!( + r#" + ── MISSING DEFINITION ────────────────── tests/known_bad/ExposedNotDefined.roc ─ + + bar is listed as exposed, but it isn't defined in this module. + + You can fix this by adding a definition for bar, or by removing it + from exposes. + + ──────────────────────────────────────────────────────────────────────────────── + + 1 error and 0 warnings found in ms."# + ), + ); + } + + #[test] + fn unused_import() { + check_compile_error( + &known_bad_file("UnusedImport.roc"), + &[], + indoc!( + r#" + ── UNUSED IMPORT ──────────────────────────── tests/known_bad/UnusedImport.roc ─ + + Nothing from Symbol is used in this module. + + 3│ imports [Symbol.{ Ident }] + ^^^^^^^^^^^^^^^^ + + Since Symbol isn't used, you don't need to import it. + + ──────────────────────────────────────────────────────────────────────────────── + + 0 errors and 1 warning found in ms."# + ), + ); + } + + #[test] + fn unknown_generates_with() { + check_compile_error( + &known_bad_file("UnknownGeneratesWith.roc"), + &[], + indoc!( + r#" + ── UNKNOWN GENERATES FUNCTION ─────── tests/known_bad/UnknownGeneratesWith.roc ─ + + I don't know how to generate the foobar function. + + 4│ generates Effect with [after, map, always, foobar] + ^^^^^^ + + Only specific functions like `after` and `map` can be generated.Learn + more about hosted modules at TODO. + + ──────────────────────────────────────────────────────────────────────────────── + + 1 error and 0 warnings found in ms."# + ), + ); + } + + #[test] + fn format_check_good() { + check_format_check_as_expected(&fixture_file("format", "Formatted.roc"), true); + } + + #[test] + fn format_check_reformatting_needed() { + check_format_check_as_expected(&fixture_file("format", "NotFormatted.roc"), false); + } + + #[test] + fn format_check_folders() { + // This fails, because "NotFormatted.roc" is present in this folder + check_format_check_as_expected(&fixtures_dir("format"), false); + + // This doesn't fail, since only "Formatted.roc" and non-roc files are present in this folder + check_format_check_as_expected(&fixtures_dir("format/formatted_directory"), true); + } +} + +#[cfg(feature = "wasm32-cli-run")] +fn run_wasm(wasm_path: &std::path::Path, stdin: &[&str]) -> String { + use bumpalo::Bump; + use roc_wasm_interp::{DefaultImportDispatcher, Instance, Value, WasiFile}; + + let wasm_bytes = std::fs::read(wasm_path).unwrap(); + let arena = Bump::new(); + + let mut instance = { + let mut fake_stdin = vec![]; + let fake_stdout = vec![]; + let fake_stderr = vec![]; + for s in stdin { + fake_stdin.extend_from_slice(s.as_bytes()) + } + + let mut dispatcher = DefaultImportDispatcher::default(); + dispatcher.wasi.files = vec![ + WasiFile::ReadOnly(fake_stdin), + WasiFile::WriteOnly(fake_stdout), + WasiFile::WriteOnly(fake_stderr), + ]; + + Instance::from_bytes(&arena, &wasm_bytes, dispatcher, false).unwrap() + }; + + let result = instance.call_export("_start", []); + + match result { + Ok(Some(Value::I32(0))) => match &instance.import_dispatcher.wasi.files[1] { + WasiFile::WriteOnly(fake_stdout) => String::from_utf8(fake_stdout.clone()) + .unwrap_or_else(|_| "Wasm test printed invalid UTF-8".into()), + _ => unreachable!(), + }, + Ok(Some(Value::I32(exit_code))) => { + format!("WASI app exit code {}", exit_code) + } + Ok(Some(val)) => { + format!("WASI _start returned an unexpected number type {:?}", val) + } + Ok(None) => "WASI _start returned no value".into(), + Err(e) => { + format!("WASI error {}", e) + } + } +} diff --git a/crates/cli/tests/fixtures/.gitignore b/crates/cli/tests/fixtures/.gitignore new file mode 100644 index 0000000000..7c3b9c35b8 --- /dev/null +++ b/crates/cli/tests/fixtures/.gitignore @@ -0,0 +1,11 @@ +app +*.o +*.dSYM +dynhost +libapp.so +metadata +preprocessedhost +packages-test + +multi-dep-str/multi-dep-str +multi-dep-thunk/multi-dep-thunk \ No newline at end of file diff --git a/crates/cli/tests/fixtures/format/Formatted.roc b/crates/cli/tests/fixtures/format/Formatted.roc new file mode 100644 index 0000000000..9804d6ac46 --- /dev/null +++ b/crates/cli/tests/fixtures/format/Formatted.roc @@ -0,0 +1,6 @@ +app "formatted" + packages { pf: "platform/main.roc" } imports [] + provides [main] to pf + +main : Str +main = Dep1.value1 {} diff --git a/cli/tests/fixtures/format/NotARocFile.txt b/crates/cli/tests/fixtures/format/NotARocFile.txt similarity index 100% rename from cli/tests/fixtures/format/NotARocFile.txt rename to crates/cli/tests/fixtures/format/NotARocFile.txt diff --git a/crates/cli/tests/fixtures/format/NotFormatted.roc b/crates/cli/tests/fixtures/format/NotFormatted.roc new file mode 100644 index 0000000000..2ae89d0830 --- /dev/null +++ b/crates/cli/tests/fixtures/format/NotFormatted.roc @@ -0,0 +1,6 @@ +app "formatted" + packages { pf: "platform/main.roc" } + provides [main] to pf + +main : Str +main = Dep1.value1 {} diff --git a/crates/cli/tests/fixtures/format/formatted_directory/Formatted.roc b/crates/cli/tests/fixtures/format/formatted_directory/Formatted.roc new file mode 100644 index 0000000000..9804d6ac46 --- /dev/null +++ b/crates/cli/tests/fixtures/format/formatted_directory/Formatted.roc @@ -0,0 +1,6 @@ +app "formatted" + packages { pf: "platform/main.roc" } imports [] + provides [main] to pf + +main : Str +main = Dep1.value1 {} diff --git a/cli/tests/fixtures/format/formatted_directory/NestedNotARocFile.txt b/crates/cli/tests/fixtures/format/formatted_directory/NestedNotARocFile.txt similarity index 100% rename from cli/tests/fixtures/format/formatted_directory/NestedNotARocFile.txt rename to crates/cli/tests/fixtures/format/formatted_directory/NestedNotARocFile.txt diff --git a/cli/tests/fixtures/format/formatted_directory/NotARocFile b/crates/cli/tests/fixtures/format/formatted_directory/NotARocFile similarity index 100% rename from cli/tests/fixtures/format/formatted_directory/NotARocFile rename to crates/cli/tests/fixtures/format/formatted_directory/NotARocFile diff --git a/cli/tests/fixtures/format/formatted_directory/ignored/NotARocFile.txt b/crates/cli/tests/fixtures/format/formatted_directory/ignored/NotARocFile.txt similarity index 100% rename from cli/tests/fixtures/format/formatted_directory/ignored/NotARocFile.txt rename to crates/cli/tests/fixtures/format/formatted_directory/ignored/NotARocFile.txt diff --git a/crates/cli/tests/fixtures/multi-dep-str/.gitignore b/crates/cli/tests/fixtures/multi-dep-str/.gitignore new file mode 100644 index 0000000000..c098216e78 --- /dev/null +++ b/crates/cli/tests/fixtures/multi-dep-str/.gitignore @@ -0,0 +1 @@ +Main diff --git a/cli/tests/fixtures/multi-dep-str/Dep1.roc b/crates/cli/tests/fixtures/multi-dep-str/Dep1.roc similarity index 100% rename from cli/tests/fixtures/multi-dep-str/Dep1.roc rename to crates/cli/tests/fixtures/multi-dep-str/Dep1.roc diff --git a/cli/tests/fixtures/multi-dep-str/Dep2.roc b/crates/cli/tests/fixtures/multi-dep-str/Dep2.roc similarity index 100% rename from cli/tests/fixtures/multi-dep-str/Dep2.roc rename to crates/cli/tests/fixtures/multi-dep-str/Dep2.roc diff --git a/crates/cli/tests/fixtures/multi-dep-str/Main.roc b/crates/cli/tests/fixtures/multi-dep-str/Main.roc new file mode 100644 index 0000000000..83af6b7ebe --- /dev/null +++ b/crates/cli/tests/fixtures/multi-dep-str/Main.roc @@ -0,0 +1,7 @@ +app "multi-dep-str" + packages { pf: "platform/main.roc" } + imports [Dep1] + provides [main] to pf + +main : Str +main = Dep1.str1 diff --git a/crates/cli/tests/fixtures/multi-dep-str/platform/host.zig b/crates/cli/tests/fixtures/multi-dep-str/platform/host.zig new file mode 100644 index 0000000000..6f91e049f8 --- /dev/null +++ b/crates/cli/tests/fixtures/multi-dep-str/platform/host.zig @@ -0,0 +1,119 @@ +const std = @import("std"); +const builtin = @import("builtin"); +const str = @import("glue").str; +const RocStr = str.RocStr; +const testing = std.testing; +const expectEqual = testing.expectEqual; +const expect = testing.expect; + +const mem = std.mem; +const Allocator = mem.Allocator; + +extern fn roc__mainForHost_1_exposed_generic(*RocStr) void; + +const Align = 2 * @alignOf(usize); +extern fn malloc(size: usize) callconv(.C) ?*align(Align) anyopaque; +extern fn realloc(c_ptr: [*]align(Align) u8, size: usize) callconv(.C) ?*anyopaque; +extern fn free(c_ptr: [*]align(Align) u8) callconv(.C) void; +extern fn memcpy(dst: [*]u8, src: [*]u8, size: usize) callconv(.C) void; +extern fn memset(dst: [*]u8, value: i32, size: usize) callconv(.C) void; + +export fn roc_alloc(size: usize, alignment: u32) callconv(.C) ?*anyopaque { + _ = alignment; + return malloc(size); +} + +export fn roc_realloc(c_ptr: *anyopaque, new_size: usize, old_size: usize, alignment: u32) callconv(.C) ?*anyopaque { + _ = old_size; + _ = alignment; + return realloc(@as([*]align(Align) u8, @alignCast(@ptrCast(c_ptr))), new_size); +} + +export fn roc_dealloc(c_ptr: *anyopaque, alignment: u32) callconv(.C) void { + _ = alignment; + free(@as([*]align(Align) u8, @alignCast(@ptrCast(c_ptr)))); +} + +export fn roc_memset(dst: [*]u8, value: i32, size: usize) callconv(.C) void { + return memset(dst, value, size); +} + +export fn roc_panic(msg: *RocStr, tag_id: u32) callconv(.C) void { + const stderr = std.io.getStdErr().writer(); + switch (tag_id) { + 0 => { + stderr.print("Roc standard library crashed with message\n\n {s}\n\nShutting down\n", .{msg.asSlice()}) catch unreachable; + }, + 1 => { + stderr.print("Application crashed with message\n\n {s}\n\nShutting down\n", .{msg.asSlice()}) catch unreachable; + }, + else => unreachable, + } + std.process.exit(1); +} + +export fn roc_dbg(loc: *RocStr, msg: *RocStr) callconv(.C) void { + const stderr = std.io.getStdErr().writer(); + stderr.print("[{s}] {s}\n", .{ loc.asSlice(), msg.asSlice() }) catch unreachable; +} + +extern fn kill(pid: c_int, sig: c_int) c_int; +extern fn shm_open(name: *const i8, oflag: c_int, mode: c_uint) c_int; +extern fn mmap(addr: ?*anyopaque, length: c_uint, prot: c_int, flags: c_int, fd: c_int, offset: c_uint) *anyopaque; +extern fn getppid() c_int; + +fn roc_getppid() callconv(.C) c_int { + return getppid(); +} + +fn roc_getppid_windows_stub() callconv(.C) c_int { + return 0; +} + +fn roc_shm_open(name: *const i8, oflag: c_int, mode: c_uint) callconv(.C) c_int { + return shm_open(name, oflag, mode); +} +fn roc_mmap(addr: ?*anyopaque, length: c_uint, prot: c_int, flags: c_int, fd: c_int, offset: c_uint) callconv(.C) *anyopaque { + return mmap(addr, length, prot, flags, fd, offset); +} + +comptime { + if (builtin.os.tag == .macos or builtin.os.tag == .linux) { + @export(roc_getppid, .{ .name = "roc_getppid", .linkage = .Strong }); + @export(roc_mmap, .{ .name = "roc_mmap", .linkage = .Strong }); + @export(roc_shm_open, .{ .name = "roc_shm_open", .linkage = .Strong }); + } + + if (builtin.os.tag == .windows) { + @export(roc_getppid_windows_stub, .{ .name = "roc_getppid", .linkage = .Strong }); + } +} + +const Unit = extern struct {}; + +pub export fn main() i32 { + const stdout = std.io.getStdOut().writer(); + const stderr = std.io.getStdErr().writer(); + + var timer = std.time.Timer.start() catch unreachable; + + // actually call roc to populate the callresult + var callresult = RocStr.empty(); + roc__mainForHost_1_exposed_generic(&callresult); + + const nanos = timer.read(); + const seconds = (@as(f64, @floatFromInt(nanos)) / 1_000_000_000.0); + + // stdout the result + stdout.print("{s}\n", .{callresult.asSlice()}) catch unreachable; + + callresult.decref(); + + stderr.print("runtime: {d:.3}ms\n", .{seconds * 1000}) catch unreachable; + + return 0; +} + +fn to_seconds(tms: std.os.timespec) f64 { + return @as(f64, @floatFromInt(tms.tv_sec)) + (@as(f64, @floatFromInt(tms.tv_nsec)) / 1_000_000_000.0); +} diff --git a/cli/tests/fixtures/multi-dep-str/platform/Package-Config.roc b/crates/cli/tests/fixtures/multi-dep-str/platform/main.roc similarity index 100% rename from cli/tests/fixtures/multi-dep-str/platform/Package-Config.roc rename to crates/cli/tests/fixtures/multi-dep-str/platform/main.roc diff --git a/crates/cli/tests/fixtures/multi-dep-thunk/.gitignore b/crates/cli/tests/fixtures/multi-dep-thunk/.gitignore new file mode 100644 index 0000000000..c098216e78 --- /dev/null +++ b/crates/cli/tests/fixtures/multi-dep-thunk/.gitignore @@ -0,0 +1 @@ +Main diff --git a/cli/tests/fixtures/multi-dep-thunk/Dep1.roc b/crates/cli/tests/fixtures/multi-dep-thunk/Dep1.roc similarity index 100% rename from cli/tests/fixtures/multi-dep-thunk/Dep1.roc rename to crates/cli/tests/fixtures/multi-dep-thunk/Dep1.roc diff --git a/cli/tests/fixtures/multi-dep-thunk/Dep2.roc b/crates/cli/tests/fixtures/multi-dep-thunk/Dep2.roc similarity index 100% rename from cli/tests/fixtures/multi-dep-thunk/Dep2.roc rename to crates/cli/tests/fixtures/multi-dep-thunk/Dep2.roc diff --git a/crates/cli/tests/fixtures/multi-dep-thunk/Main.roc b/crates/cli/tests/fixtures/multi-dep-thunk/Main.roc new file mode 100644 index 0000000000..13902d40a5 --- /dev/null +++ b/crates/cli/tests/fixtures/multi-dep-thunk/Main.roc @@ -0,0 +1,7 @@ +app "multi-dep-thunk" + packages { pf: "platform/main.roc" } + imports [Dep1] + provides [main] to pf + +main : Str +main = Dep1.value1 {} diff --git a/crates/cli/tests/fixtures/multi-dep-thunk/platform/host.zig b/crates/cli/tests/fixtures/multi-dep-thunk/platform/host.zig new file mode 100644 index 0000000000..a4a0ff9012 --- /dev/null +++ b/crates/cli/tests/fixtures/multi-dep-thunk/platform/host.zig @@ -0,0 +1,119 @@ +const std = @import("std"); +const builtin = @import("builtin"); +const str = @import("glue").str; +const RocStr = str.RocStr; +const testing = std.testing; +const expectEqual = testing.expectEqual; +const expect = testing.expect; + +const mem = std.mem; +const Allocator = mem.Allocator; + +extern fn roc__mainForHost_1_exposed_generic(*RocStr) void; + +const Align = 2 * @alignOf(usize); +extern fn malloc(size: usize) callconv(.C) ?*anyopaque; +extern fn realloc(c_ptr: [*]align(@alignOf(u128)) u8, size: usize) callconv(.C) ?*anyopaque; +extern fn free(c_ptr: [*]align(@alignOf(u128)) u8) callconv(.C) void; +extern fn memcpy(dst: [*]u8, src: [*]u8, size: usize) callconv(.C) void; +extern fn memset(dst: [*]u8, value: i32, size: usize) callconv(.C) void; + +export fn roc_alloc(size: usize, alignment: u32) callconv(.C) ?*anyopaque { + _ = alignment; + return malloc(size); +} + +export fn roc_realloc(c_ptr: *anyopaque, new_size: usize, old_size: usize, alignment: u32) callconv(.C) ?*anyopaque { + _ = old_size; + _ = alignment; + return realloc(@as([*]align(Align) u8, @alignCast(@ptrCast(c_ptr))), new_size); +} + +export fn roc_dealloc(c_ptr: *anyopaque, alignment: u32) callconv(.C) void { + _ = alignment; + free(@as([*]align(Align) u8, @alignCast(@ptrCast(c_ptr)))); +} + +export fn roc_memset(dst: [*]u8, value: i32, size: usize) callconv(.C) void { + return memset(dst, value, size); +} + +export fn roc_panic(msg: *RocStr, tag_id: u32) callconv(.C) void { + const stderr = std.io.getStdErr().writer(); + switch (tag_id) { + 0 => { + stderr.print("Roc standard library crashed with message\n\n {s}\n\nShutting down\n", .{msg.asSlice()}) catch unreachable; + }, + 1 => { + stderr.print("Application crashed with message\n\n {s}\n\nShutting down\n", .{msg.asSlice()}) catch unreachable; + }, + else => unreachable, + } + std.process.exit(1); +} + +export fn roc_dbg(loc: *RocStr, msg: *RocStr) callconv(.C) void { + const stderr = std.io.getStdErr().writer(); + stderr.print("[{s}] {s}\n", .{ loc.asSlice(), msg.asSlice() }) catch unreachable; +} + +extern fn kill(pid: c_int, sig: c_int) c_int; +extern fn shm_open(name: *const i8, oflag: c_int, mode: c_uint) c_int; +extern fn mmap(addr: ?*anyopaque, length: c_uint, prot: c_int, flags: c_int, fd: c_int, offset: c_uint) *anyopaque; +extern fn getppid() c_int; + +fn roc_getppid() callconv(.C) c_int { + return getppid(); +} + +fn roc_getppid_windows_stub() callconv(.C) c_int { + return 0; +} + +fn roc_shm_open(name: *const i8, oflag: c_int, mode: c_uint) callconv(.C) c_int { + return shm_open(name, oflag, mode); +} +fn roc_mmap(addr: ?*anyopaque, length: c_uint, prot: c_int, flags: c_int, fd: c_int, offset: c_uint) callconv(.C) *anyopaque { + return mmap(addr, length, prot, flags, fd, offset); +} + +comptime { + if (builtin.os.tag == .macos or builtin.os.tag == .linux) { + @export(roc_getppid, .{ .name = "roc_getppid", .linkage = .Strong }); + @export(roc_mmap, .{ .name = "roc_mmap", .linkage = .Strong }); + @export(roc_shm_open, .{ .name = "roc_shm_open", .linkage = .Strong }); + } + + if (builtin.os.tag == .windows) { + @export(roc_getppid_windows_stub, .{ .name = "roc_getppid", .linkage = .Strong }); + } +} + +const Unit = extern struct {}; + +pub export fn main() i32 { + const stdout = std.io.getStdOut().writer(); + const stderr = std.io.getStdErr().writer(); + + var timer = std.time.Timer.start() catch unreachable; + + // actually call roc to populate the callresult + var callresult = RocStr.empty(); + roc__mainForHost_1_exposed_generic(&callresult); + + const nanos = timer.read(); + const seconds = (@as(f64, @floatFromInt(nanos)) / 1_000_000_000.0); + + // stdout the result + stdout.print("{s}\n", .{callresult.asSlice()}) catch unreachable; + + callresult.decref(); + + stderr.print("runtime: {d:.3}ms\n", .{seconds * 1000}) catch unreachable; + + return 0; +} + +fn to_seconds(tms: std.os.timespec) f64 { + return @as(f64, @floatFromInt(tms.tv_sec)) + (@as(f64, @floatFromInt(tms.tv_nsec)) / 1_000_000_000.0); +} diff --git a/cli/tests/fixtures/multi-dep-thunk/platform/Package-Config.roc b/crates/cli/tests/fixtures/multi-dep-thunk/platform/main.roc similarity index 100% rename from cli/tests/fixtures/multi-dep-thunk/platform/Package-Config.roc rename to crates/cli/tests/fixtures/multi-dep-thunk/platform/main.roc diff --git a/crates/cli/tests/fixtures/packages/app.roc b/crates/cli/tests/fixtures/packages/app.roc new file mode 100644 index 0000000000..a37bee685b --- /dev/null +++ b/crates/cli/tests/fixtures/packages/app.roc @@ -0,0 +1,6 @@ +app "packages-test" + packages { pf: "platform/main.roc", json: "json/main.roc", csv: "csv/main.roc" } + imports [json.JsonParser, csv.Csv] + provides [main] to pf + +main = "Hello, World! \(JsonParser.example) \(Csv.example)" diff --git a/crates/cli/tests/fixtures/packages/csv/Csv.roc b/crates/cli/tests/fixtures/packages/csv/Csv.roc new file mode 100644 index 0000000000..bc71f776d4 --- /dev/null +++ b/crates/cli/tests/fixtures/packages/csv/Csv.roc @@ -0,0 +1,6 @@ +interface Csv + exposes [example] + imports [] + +example : Str +example = "This text came from a CSV package!" \ No newline at end of file diff --git a/crates/cli/tests/fixtures/packages/csv/main.roc b/crates/cli/tests/fixtures/packages/csv/main.roc new file mode 100644 index 0000000000..12d60ce654 --- /dev/null +++ b/crates/cli/tests/fixtures/packages/csv/main.roc @@ -0,0 +1,3 @@ +package "csv" + exposes [Csv] + packages {} \ No newline at end of file diff --git a/crates/cli/tests/fixtures/packages/json/JsonParser.roc b/crates/cli/tests/fixtures/packages/json/JsonParser.roc new file mode 100644 index 0000000000..2ebd6b1a62 --- /dev/null +++ b/crates/cli/tests/fixtures/packages/json/JsonParser.roc @@ -0,0 +1,6 @@ +interface JsonParser + exposes [example] + imports [] + +example : Str +example = "This text came from a package!" \ No newline at end of file diff --git a/crates/cli/tests/fixtures/packages/json/main.roc b/crates/cli/tests/fixtures/packages/json/main.roc new file mode 100644 index 0000000000..5379d207d4 --- /dev/null +++ b/crates/cli/tests/fixtures/packages/json/main.roc @@ -0,0 +1,3 @@ +package "json" + exposes [JsonParser] + packages {} \ No newline at end of file diff --git a/crates/cli/tests/fixtures/packages/platform/host.zig b/crates/cli/tests/fixtures/packages/platform/host.zig new file mode 100644 index 0000000000..6f91e049f8 --- /dev/null +++ b/crates/cli/tests/fixtures/packages/platform/host.zig @@ -0,0 +1,119 @@ +const std = @import("std"); +const builtin = @import("builtin"); +const str = @import("glue").str; +const RocStr = str.RocStr; +const testing = std.testing; +const expectEqual = testing.expectEqual; +const expect = testing.expect; + +const mem = std.mem; +const Allocator = mem.Allocator; + +extern fn roc__mainForHost_1_exposed_generic(*RocStr) void; + +const Align = 2 * @alignOf(usize); +extern fn malloc(size: usize) callconv(.C) ?*align(Align) anyopaque; +extern fn realloc(c_ptr: [*]align(Align) u8, size: usize) callconv(.C) ?*anyopaque; +extern fn free(c_ptr: [*]align(Align) u8) callconv(.C) void; +extern fn memcpy(dst: [*]u8, src: [*]u8, size: usize) callconv(.C) void; +extern fn memset(dst: [*]u8, value: i32, size: usize) callconv(.C) void; + +export fn roc_alloc(size: usize, alignment: u32) callconv(.C) ?*anyopaque { + _ = alignment; + return malloc(size); +} + +export fn roc_realloc(c_ptr: *anyopaque, new_size: usize, old_size: usize, alignment: u32) callconv(.C) ?*anyopaque { + _ = old_size; + _ = alignment; + return realloc(@as([*]align(Align) u8, @alignCast(@ptrCast(c_ptr))), new_size); +} + +export fn roc_dealloc(c_ptr: *anyopaque, alignment: u32) callconv(.C) void { + _ = alignment; + free(@as([*]align(Align) u8, @alignCast(@ptrCast(c_ptr)))); +} + +export fn roc_memset(dst: [*]u8, value: i32, size: usize) callconv(.C) void { + return memset(dst, value, size); +} + +export fn roc_panic(msg: *RocStr, tag_id: u32) callconv(.C) void { + const stderr = std.io.getStdErr().writer(); + switch (tag_id) { + 0 => { + stderr.print("Roc standard library crashed with message\n\n {s}\n\nShutting down\n", .{msg.asSlice()}) catch unreachable; + }, + 1 => { + stderr.print("Application crashed with message\n\n {s}\n\nShutting down\n", .{msg.asSlice()}) catch unreachable; + }, + else => unreachable, + } + std.process.exit(1); +} + +export fn roc_dbg(loc: *RocStr, msg: *RocStr) callconv(.C) void { + const stderr = std.io.getStdErr().writer(); + stderr.print("[{s}] {s}\n", .{ loc.asSlice(), msg.asSlice() }) catch unreachable; +} + +extern fn kill(pid: c_int, sig: c_int) c_int; +extern fn shm_open(name: *const i8, oflag: c_int, mode: c_uint) c_int; +extern fn mmap(addr: ?*anyopaque, length: c_uint, prot: c_int, flags: c_int, fd: c_int, offset: c_uint) *anyopaque; +extern fn getppid() c_int; + +fn roc_getppid() callconv(.C) c_int { + return getppid(); +} + +fn roc_getppid_windows_stub() callconv(.C) c_int { + return 0; +} + +fn roc_shm_open(name: *const i8, oflag: c_int, mode: c_uint) callconv(.C) c_int { + return shm_open(name, oflag, mode); +} +fn roc_mmap(addr: ?*anyopaque, length: c_uint, prot: c_int, flags: c_int, fd: c_int, offset: c_uint) callconv(.C) *anyopaque { + return mmap(addr, length, prot, flags, fd, offset); +} + +comptime { + if (builtin.os.tag == .macos or builtin.os.tag == .linux) { + @export(roc_getppid, .{ .name = "roc_getppid", .linkage = .Strong }); + @export(roc_mmap, .{ .name = "roc_mmap", .linkage = .Strong }); + @export(roc_shm_open, .{ .name = "roc_shm_open", .linkage = .Strong }); + } + + if (builtin.os.tag == .windows) { + @export(roc_getppid_windows_stub, .{ .name = "roc_getppid", .linkage = .Strong }); + } +} + +const Unit = extern struct {}; + +pub export fn main() i32 { + const stdout = std.io.getStdOut().writer(); + const stderr = std.io.getStdErr().writer(); + + var timer = std.time.Timer.start() catch unreachable; + + // actually call roc to populate the callresult + var callresult = RocStr.empty(); + roc__mainForHost_1_exposed_generic(&callresult); + + const nanos = timer.read(); + const seconds = (@as(f64, @floatFromInt(nanos)) / 1_000_000_000.0); + + // stdout the result + stdout.print("{s}\n", .{callresult.asSlice()}) catch unreachable; + + callresult.decref(); + + stderr.print("runtime: {d:.3}ms\n", .{seconds * 1000}) catch unreachable; + + return 0; +} + +fn to_seconds(tms: std.os.timespec) f64 { + return @as(f64, @floatFromInt(tms.tv_sec)) + (@as(f64, @floatFromInt(tms.tv_nsec)) / 1_000_000_000.0); +} diff --git a/crates/cli/tests/fixtures/packages/platform/main.roc b/crates/cli/tests/fixtures/packages/platform/main.roc new file mode 100644 index 0000000000..edc3368f93 --- /dev/null +++ b/crates/cli/tests/fixtures/packages/platform/main.roc @@ -0,0 +1,9 @@ +platform "multi-module" + requires {}{ main : Str } + exposes [] + packages {} + imports [] + provides [mainForHost] + +mainForHost : Str +mainForHost = main diff --git a/crates/cli/tests/known_bad/ExposedNotDefined.roc b/crates/cli/tests/known_bad/ExposedNotDefined.roc new file mode 100644 index 0000000000..b3335684b1 --- /dev/null +++ b/crates/cli/tests/known_bad/ExposedNotDefined.roc @@ -0,0 +1,3 @@ +interface ExposedNotDefined + exposes [bar] + imports [] diff --git a/cli/tests/known_bad/Symbol.roc b/crates/cli/tests/known_bad/Symbol.roc similarity index 100% rename from cli/tests/known_bad/Symbol.roc rename to crates/cli/tests/known_bad/Symbol.roc diff --git a/crates/cli/tests/known_bad/TypeError.roc b/crates/cli/tests/known_bad/TypeError.roc new file mode 100644 index 0000000000..070aca541a --- /dev/null +++ b/crates/cli/tests/known_bad/TypeError.roc @@ -0,0 +1,8 @@ +app "type-error" + packages { pf: "../../../../examples/cli/false-interpreter/platform/main.roc" } + imports [pf.Task.{ Task }] + provides [main] to pf + +main : Str -> Task {} [] +main = \_ -> + "this is a string, not a Task {} [] function like the platform expects." \ No newline at end of file diff --git a/cli/tests/known_bad/UnknownGeneratesWith.roc b/crates/cli/tests/known_bad/UnknownGeneratesWith.roc similarity index 100% rename from cli/tests/known_bad/UnknownGeneratesWith.roc rename to crates/cli/tests/known_bad/UnknownGeneratesWith.roc diff --git a/cli/tests/known_bad/UnusedImport.roc b/crates/cli/tests/known_bad/UnusedImport.roc similarity index 100% rename from cli/tests/known_bad/UnusedImport.roc rename to crates/cli/tests/known_bad/UnusedImport.roc diff --git a/crates/cli_testing_examples/.gitignore b/crates/cli_testing_examples/.gitignore new file mode 100644 index 0000000000..f394d04f8b --- /dev/null +++ b/crates/cli_testing_examples/.gitignore @@ -0,0 +1,12 @@ +*.dSYM +libhost.a +libapp.so +dynhost +preprocessedhost +metadata + +expects/expects +benchmarks/rbtree-ck +benchmarks/rbtree-insert +benchmarks/test-astar +benchmarks/test-base64 diff --git a/examples/algorithms/.gitignore b/crates/cli_testing_examples/algorithms/.gitignore similarity index 100% rename from examples/algorithms/.gitignore rename to crates/cli_testing_examples/algorithms/.gitignore diff --git a/examples/algorithms/README.md b/crates/cli_testing_examples/algorithms/README.md similarity index 100% rename from examples/algorithms/README.md rename to crates/cli_testing_examples/algorithms/README.md diff --git a/crates/cli_testing_examples/algorithms/fibonacci-platform/host.zig b/crates/cli_testing_examples/algorithms/fibonacci-platform/host.zig new file mode 100644 index 0000000000..8abd265eee --- /dev/null +++ b/crates/cli_testing_examples/algorithms/fibonacci-platform/host.zig @@ -0,0 +1,130 @@ +const std = @import("std"); +const builtin = @import("builtin"); +const str = @import("glue").str; +const RocStr = str.RocStr; +const testing = std.testing; +const expectEqual = testing.expectEqual; +const expect = testing.expect; +const maxInt = std.math.maxInt; + +const mem = std.mem; +const Allocator = mem.Allocator; + +// NOTE the LLVM backend expects this signature +// extern fn roc__mainForHost_1_exposed(i64, *i64) void; +extern fn roc__mainForHost_1_exposed(i64) i64; + +const Align = 2 * @alignOf(usize); +extern fn malloc(size: usize) callconv(.C) ?*align(Align) anyopaque; +extern fn realloc(c_ptr: [*]align(Align) u8, size: usize) callconv(.C) ?*anyopaque; +extern fn free(c_ptr: [*]align(Align) u8) callconv(.C) void; +extern fn memcpy(dst: [*]u8, src: [*]u8, size: usize) callconv(.C) void; +extern fn memset(dst: [*]u8, value: i32, size: usize) callconv(.C) void; + +const DEBUG: bool = false; + +export fn roc_alloc(size: usize, alignment: u32) callconv(.C) ?*anyopaque { + if (DEBUG) { + var ptr = malloc(size); + const stdout = std.io.getStdOut().writer(); + stdout.print("alloc: {d} (alignment {d}, size {d})\n", .{ ptr, alignment, size }) catch unreachable; + return ptr; + } else { + return malloc(size); + } +} + +export fn roc_realloc(c_ptr: *anyopaque, new_size: usize, old_size: usize, alignment: u32) callconv(.C) ?*anyopaque { + if (DEBUG) { + const stdout = std.io.getStdOut().writer(); + stdout.print("realloc: {d} (alignment {d}, old_size {d})\n", .{ c_ptr, alignment, old_size }) catch unreachable; + } + + return realloc(@as([*]align(Align) u8, @alignCast(@ptrCast(c_ptr))), new_size); +} + +export fn roc_dealloc(c_ptr: *anyopaque, alignment: u32) callconv(.C) void { + if (DEBUG) { + const stdout = std.io.getStdOut().writer(); + stdout.print("dealloc: {d} (alignment {d})\n", .{ c_ptr, alignment }) catch unreachable; + } + + free(@as([*]align(Align) u8, @alignCast(@ptrCast(c_ptr)))); +} + +export fn roc_panic(msg: *RocStr, tag_id: u32) callconv(.C) void { + const stderr = std.io.getStdErr().writer(); + switch (tag_id) { + 0 => { + stderr.print("Roc standard library crashed with message\n\n {s}\n\nShutting down\n", .{msg.asSlice()}) catch unreachable; + }, + 1 => { + stderr.print("Application crashed with message\n\n {s}\n\nShutting down\n", .{msg.asSlice()}) catch unreachable; + }, + else => unreachable, + } + std.process.exit(1); +} + +export fn roc_dbg(loc: *RocStr, msg: *RocStr) callconv(.C) void { + const stderr = std.io.getStdErr().writer(); + stderr.print("[{s}] {s}\n", .{ loc.asSlice(), msg.asSlice() }) catch unreachable; +} + +export fn roc_memset(dst: [*]u8, value: i32, size: usize) callconv(.C) void { + return memset(dst, value, size); +} + +extern fn kill(pid: c_int, sig: c_int) c_int; +extern fn shm_open(name: *const i8, oflag: c_int, mode: c_uint) c_int; +extern fn mmap(addr: ?*anyopaque, length: c_uint, prot: c_int, flags: c_int, fd: c_int, offset: c_uint) *anyopaque; +extern fn getppid() c_int; + +fn roc_getppid() callconv(.C) c_int { + return getppid(); +} + +fn roc_getppid_windows_stub() callconv(.C) c_int { + return 0; +} + +fn roc_shm_open(name: *const i8, oflag: c_int, mode: c_uint) callconv(.C) c_int { + return shm_open(name, oflag, mode); +} +fn roc_mmap(addr: ?*anyopaque, length: c_uint, prot: c_int, flags: c_int, fd: c_int, offset: c_uint) callconv(.C) *anyopaque { + return mmap(addr, length, prot, flags, fd, offset); +} + +comptime { + if (builtin.os.tag == .macos or builtin.os.tag == .linux) { + @export(roc_getppid, .{ .name = "roc_getppid", .linkage = .Strong }); + @export(roc_mmap, .{ .name = "roc_mmap", .linkage = .Strong }); + @export(roc_shm_open, .{ .name = "roc_shm_open", .linkage = .Strong }); + } + + if (builtin.os.tag == .windows) { + @export(roc_getppid_windows_stub, .{ .name = "roc_getppid", .linkage = .Strong }); + } +} + +pub export fn main() u8 { + const stdout = std.io.getStdOut().writer(); + + var timer = std.time.Timer.start() catch unreachable; + + const result = roc__mainForHost_1_exposed(10); + + const nanos = timer.read(); + const seconds = (@as(f64, @floatFromInt(nanos)) / 1_000_000_000.0); + + stdout.print("{d}\n", .{result}) catch unreachable; + + const stderr = std.io.getStdErr().writer(); + stderr.print("runtime: {d:.3}ms\n", .{seconds * 1000}) catch unreachable; + + return 0; +} + +fn to_seconds(tms: std.os.timespec) f64 { + return @as(f64, @floatFromInt(tms.tv_sec)) + (@as(f64, @floatFromInt(tms.tv_nsec)) / 1_000_000_000.0); +} diff --git a/examples/algorithms/fibonacci-platform/Package-Config.roc b/crates/cli_testing_examples/algorithms/fibonacci-platform/main.roc similarity index 100% rename from examples/algorithms/fibonacci-platform/Package-Config.roc rename to crates/cli_testing_examples/algorithms/fibonacci-platform/main.roc diff --git a/examples/algorithms/fibonacci.roc b/crates/cli_testing_examples/algorithms/fibonacci.roc similarity index 80% rename from examples/algorithms/fibonacci.roc rename to crates/cli_testing_examples/algorithms/fibonacci.roc index e18ab03b36..906475c98d 100644 --- a/examples/algorithms/fibonacci.roc +++ b/crates/cli_testing_examples/algorithms/fibonacci.roc @@ -1,5 +1,5 @@ app "fibonacci" - packages { pf: "fibonacci-platform" } + packages { pf: "fibonacci-platform/main.roc" } imports [] provides [main] to pf diff --git a/crates/cli_testing_examples/algorithms/quicksort-platform/host.zig b/crates/cli_testing_examples/algorithms/quicksort-platform/host.zig new file mode 100644 index 0000000000..138e276e21 --- /dev/null +++ b/crates/cli_testing_examples/algorithms/quicksort-platform/host.zig @@ -0,0 +1,160 @@ +const std = @import("std"); +const builtin = @import("builtin"); +const str = @import("glue").str; +const RocStr = str.RocStr; +const testing = std.testing; +const expectEqual = testing.expectEqual; +const expect = testing.expect; + +const mem = std.mem; +const Allocator = mem.Allocator; + +extern fn roc__mainForHost_1_exposed(input: RocList) callconv(.C) RocList; + +const Align = 2 * @alignOf(usize); +extern fn malloc(size: usize) callconv(.C) ?*align(Align) anyopaque; +extern fn realloc(c_ptr: [*]align(Align) u8, size: usize) callconv(.C) ?*anyopaque; +extern fn free(c_ptr: [*]align(Align) u8) callconv(.C) void; +extern fn memcpy(dst: [*]u8, src: [*]u8, size: usize) callconv(.C) void; +extern fn memset(dst: [*]u8, value: i32, size: usize) callconv(.C) void; + +const DEBUG: bool = false; + +export fn roc_alloc(size: usize, alignment: u32) callconv(.C) ?*anyopaque { + if (DEBUG) { + var ptr = malloc(size); + const stdout = std.io.getStdOut().writer(); + stdout.print("alloc: {d} (alignment {d}, size {d})\n", .{ ptr, alignment, size }) catch unreachable; + return ptr; + } else { + return malloc(size); + } +} + +export fn roc_realloc(c_ptr: *anyopaque, new_size: usize, old_size: usize, alignment: u32) callconv(.C) ?*anyopaque { + if (DEBUG) { + const stdout = std.io.getStdOut().writer(); + stdout.print("realloc: {d} (alignment {d}, old_size {d})\n", .{ c_ptr, alignment, old_size }) catch unreachable; + } + + return realloc(@as([*]align(Align) u8, @alignCast(@ptrCast(c_ptr))), new_size); +} + +export fn roc_dealloc(c_ptr: *anyopaque, alignment: u32) callconv(.C) void { + if (DEBUG) { + const stdout = std.io.getStdOut().writer(); + stdout.print("dealloc: {d} (alignment {d})\n", .{ c_ptr, alignment }) catch unreachable; + } + + free(@as([*]align(Align) u8, @alignCast(@ptrCast(c_ptr)))); +} + +export fn roc_panic(msg: *RocStr, tag_id: u32) callconv(.C) void { + const stderr = std.io.getStdErr().writer(); + switch (tag_id) { + 0 => { + stderr.print("Roc standard library crashed with message\n\n {s}\n\nShutting down\n", .{msg.asSlice()}) catch unreachable; + }, + 1 => { + stderr.print("Application crashed with message\n\n {s}\n\nShutting down\n", .{msg.asSlice()}) catch unreachable; + }, + else => unreachable, + } + std.process.exit(1); +} + +export fn roc_dbg(loc: *RocStr, msg: *RocStr) callconv(.C) void { + const stderr = std.io.getStdErr().writer(); + stderr.print("[{s}] {s}\n", .{ loc.asSlice(), msg.asSlice() }) catch unreachable; +} + +export fn roc_memset(dst: [*]u8, value: i32, size: usize) callconv(.C) void { + return memset(dst, value, size); +} + +extern fn kill(pid: c_int, sig: c_int) c_int; +extern fn shm_open(name: *const i8, oflag: c_int, mode: c_uint) c_int; +extern fn mmap(addr: ?*anyopaque, length: c_uint, prot: c_int, flags: c_int, fd: c_int, offset: c_uint) *anyopaque; +extern fn getppid() c_int; + +fn roc_getppid() callconv(.C) c_int { + return getppid(); +} + +fn roc_getppid_windows_stub() callconv(.C) c_int { + return 0; +} + +fn roc_shm_open(name: *const i8, oflag: c_int, mode: c_uint) callconv(.C) c_int { + return shm_open(name, oflag, mode); +} +fn roc_mmap(addr: ?*anyopaque, length: c_uint, prot: c_int, flags: c_int, fd: c_int, offset: c_uint) callconv(.C) *anyopaque { + return mmap(addr, length, prot, flags, fd, offset); +} + +comptime { + if (builtin.os.tag == .macos or builtin.os.tag == .linux) { + @export(roc_getppid, .{ .name = "roc_getppid", .linkage = .Strong }); + @export(roc_mmap, .{ .name = "roc_mmap", .linkage = .Strong }); + @export(roc_shm_open, .{ .name = "roc_shm_open", .linkage = .Strong }); + } + + if (builtin.os.tag == .windows) { + @export(roc_getppid_windows_stub, .{ .name = "roc_getppid", .linkage = .Strong }); + } +} + +// warning! the array is currently stack-allocated so don't make this too big +const NUM_NUMS = 100; + +const RocList = extern struct { elements: [*]i64, length: usize, capacity: usize }; + +const Unit = extern struct {}; + +pub export fn main() u8 { + const stdout = std.io.getStdOut().writer(); + + var raw_numbers: [NUM_NUMS + 1]i64 = undefined; + + // set refcount to one + raw_numbers[0] = -9223372036854775808; + + var numbers = raw_numbers[1..]; + + for (numbers, 0..) |_, i| { + numbers[i] = @mod(@as(i64, @intCast(i)), 12); + } + + var roc_list = RocList{ .elements = numbers, .length = NUM_NUMS, .capacity = NUM_NUMS }; + + var timer = std.time.Timer.start() catch unreachable; + + // actually call roc to populate the callresult + const callresult: RocList = roc__mainForHost_1_exposed(roc_list); + + // stdout the result + const length = @min(20, callresult.length); + var result = callresult.elements[0..length]; + + const nanos = timer.read(); + const seconds = (@as(f64, @floatFromInt(nanos)) / 1_000_000_000.0); + + for (result, 0..) |x, i| { + if (i == 0) { + stdout.print("[{}, ", .{x}) catch unreachable; + } else if (i == length - 1) { + stdout.print("{}]\n", .{x}) catch unreachable; + } else { + stdout.print("{}, ", .{x}) catch unreachable; + } + } + + const stderr = std.io.getStdErr().writer(); + stderr.print("runtime: {d:.3}ms\n", .{seconds * 1000}) catch unreachable; + + return 0; +} + +fn to_seconds(tms: std.os.timespec) f64 { + return @as(f64, @floatFromInt(tms.tv_sec)) + (@as(f64, @floatFromInt(tms.tv_nsec)) / 1_000_000_000.0); +} diff --git a/examples/algorithms/quicksort-platform/Package-Config.roc b/crates/cli_testing_examples/algorithms/quicksort-platform/main.roc similarity index 100% rename from examples/algorithms/quicksort-platform/Package-Config.roc rename to crates/cli_testing_examples/algorithms/quicksort-platform/main.roc diff --git a/examples/algorithms/quicksort.roc b/crates/cli_testing_examples/algorithms/quicksort.roc similarity index 86% rename from examples/algorithms/quicksort.roc rename to crates/cli_testing_examples/algorithms/quicksort.roc index c12e391f3c..d3b2f9a832 100644 --- a/examples/algorithms/quicksort.roc +++ b/crates/cli_testing_examples/algorithms/quicksort.roc @@ -1,5 +1,5 @@ app "quicksort" - packages { pf: "quicksort-platform" } + packages { pf: "quicksort-platform/main.roc" } imports [] provides [quicksort] to pf @@ -14,8 +14,8 @@ quicksortHelp = \list, low, high -> when partition low high list is Pair partitionIndex partitioned -> partitioned - |> quicksortHelp low (partitionIndex - 1) - |> quicksortHelp (partitionIndex + 1) high + |> quicksortHelp low (partitionIndex - 1) + |> quicksortHelp (partitionIndex + 1) high else list @@ -26,6 +26,7 @@ partition = \low, high, initialList -> when partitionHelp low low initialList high pivot is Pair newI newList -> Pair newI (swap newI high newList) + Err _ -> Pair low initialList @@ -38,6 +39,7 @@ partitionHelp = \i, j, list, high, pivot -> partitionHelp (i + 1) (j + 1) (swap i j list) high pivot else partitionHelp i (j + 1) list high pivot + Err _ -> Pair i list else @@ -48,8 +50,9 @@ swap = \i, j, list -> when Pair (List.get list i) (List.get list j) is Pair (Ok atI) (Ok atJ) -> list - |> List.set i atJ - |> List.set j atI + |> List.set i atJ + |> List.set j atI + _ -> # to prevent a decrement on list # turns out this is very important for optimizations diff --git a/crates/cli_testing_examples/benchmarks/.gitignore b/crates/cli_testing_examples/benchmarks/.gitignore new file mode 100644 index 0000000000..f2823be630 --- /dev/null +++ b/crates/cli_testing_examples/benchmarks/.gitignore @@ -0,0 +1,12 @@ +cfold +closure +deriv +issue2279 +nqueens +quicksortapp +RBTreeCk +RBTreeDel +RBTreeInsert +TestAStar +TestBase64 +*.wasm diff --git a/crates/cli_testing_examples/benchmarks/AStar.roc b/crates/cli_testing_examples/benchmarks/AStar.roc new file mode 100644 index 0000000000..14f4a42dbc --- /dev/null +++ b/crates/cli_testing_examples/benchmarks/AStar.roc @@ -0,0 +1,124 @@ +interface AStar + exposes [findPath, Model, initialModel, cheapestOpen, reconstructPath] + imports [Quicksort] + +findPath = \costFn, moveFn, start, end -> + astar costFn moveFn end (initialModel start) + +Model position : { + evaluated : Set position, + openSet : Set position, + costs : Dict position F64, + cameFrom : Dict position position, +} where position implements Hash & Eq + +initialModel : position -> Model position where position implements Hash & Eq +initialModel = \start -> { + evaluated: Set.empty {}, + openSet: Set.single start, + costs: Dict.single start 0, + cameFrom: Dict.empty {}, +} + +cheapestOpen : (position -> F64), Model position -> Result position {} where position implements Hash & Eq +cheapestOpen = \costFn, model -> + model.openSet + |> Set.toList + |> List.keepOks + (\position -> + when Dict.get model.costs position is + Err _ -> Err {} + Ok cost -> Ok { cost: cost + costFn position, position } + ) + |> Quicksort.sortBy .cost + |> List.first + |> Result.map .position + |> Result.mapErr (\_ -> {}) + +reconstructPath : Dict position position, position -> List position where position implements Hash & Eq +reconstructPath = \cameFrom, goal -> + when Dict.get cameFrom goal is + Err _ -> [] + Ok next -> List.append (reconstructPath cameFrom next) goal + +updateCost : position, position, Model position -> Model position where position implements Hash & Eq +updateCost = \current, neighbor, model -> + newCameFrom = + Dict.insert model.cameFrom neighbor current + + newCosts = + Dict.insert model.costs neighbor distanceTo + + distanceTo = + reconstructPath newCameFrom neighbor + |> List.len + |> Num.toFrac + + newModel = + { model & + costs: newCosts, + cameFrom: newCameFrom, + } + + when Dict.get model.costs neighbor is + Err _ -> + newModel + + Ok previousDistance -> + if distanceTo < previousDistance then + newModel + else + model + +astar : (position, position -> F64), (position -> Set position), position, Model position -> Result (List position) {} where position implements Hash & Eq +astar = \costFn, moveFn, goal, model -> + when cheapestOpen (\source -> costFn source goal) model is + Err {} -> Err {} + Ok current -> + if current == goal then + Ok (reconstructPath model.cameFrom goal) + else + modelPopped = + { model & + openSet: Set.remove model.openSet current, + evaluated: Set.insert model.evaluated current, + } + + neighbors = + moveFn current + + newNeighbors = + Set.difference neighbors modelPopped.evaluated + + modelWithNeighbors : Model position + modelWithNeighbors = + { modelPopped & + openSet: Set.union modelPopped.openSet newNeighbors, + } + + walker : Model position, position -> Model position + walker = \amodel, n -> updateCost current n amodel + + modelWithCosts = + Set.walk newNeighbors modelWithNeighbors walker + + astar costFn moveFn goal modelWithCosts + +# takeStep = \moveFn, _goal, model, current -> +# modelPopped = +# { model & +# openSet: Set.remove model.openSet current, +# evaluated: Set.insert model.evaluated current, +# } +# +# neighbors = moveFn current +# +# newNeighbors = Set.difference neighbors modelPopped.evaluated +# +# modelWithNeighbors = { modelPopped & openSet: Set.union modelPopped.openSet newNeighbors } +# +# # a lot goes wrong here +# modelWithCosts = +# Set.walk newNeighbors modelWithNeighbors (\n, m -> updateCost current n m) +# +# modelWithCosts diff --git a/examples/benchmarks/Base64.roc b/crates/cli_testing_examples/benchmarks/Base64.roc similarity index 79% rename from examples/benchmarks/Base64.roc rename to crates/cli_testing_examples/benchmarks/Base64.roc index e5c389bbff..792af7ca31 100644 --- a/examples/benchmarks/Base64.roc +++ b/crates/cli_testing_examples/benchmarks/Base64.roc @@ -1,32 +1,35 @@ interface Base64 exposes [fromBytes, fromStr, toBytes, toStr] imports [Base64.Decode, Base64.Encode] # base 64 encoding from a sequence of bytes -fromBytes : List U8 -> Result Str [InvalidInput]* +fromBytes : List U8 -> Result Str [InvalidInput] fromBytes = \bytes -> when Base64.Decode.fromBytes bytes is Ok v -> Ok v + Err _ -> Err InvalidInput # base 64 encoding from a string -fromStr : Str -> Result Str [InvalidInput]* +fromStr : Str -> Result Str [InvalidInput] fromStr = \str -> fromBytes (Str.toUtf8 str) # base64-encode bytes to the original -toBytes : Str -> Result (List U8) [InvalidInput]* +toBytes : Str -> Result (List U8) [InvalidInput] toBytes = \str -> Ok (Base64.Encode.toBytes str) -toStr : Str -> Result Str [InvalidInput]* +toStr : Str -> Result Str [InvalidInput] toStr = \str -> when toBytes str is Ok bytes -> when Str.fromUtf8 bytes is Ok v -> Ok v + Err _ -> Err InvalidInput + Err _ -> Err InvalidInput diff --git a/crates/cli_testing_examples/benchmarks/Base64/Decode.roc b/crates/cli_testing_examples/benchmarks/Base64/Decode.roc new file mode 100644 index 0000000000..35680025d9 --- /dev/null +++ b/crates/cli_testing_examples/benchmarks/Base64/Decode.roc @@ -0,0 +1,120 @@ +interface Base64.Decode exposes [fromBytes] imports [Bytes.Decode.{ ByteDecoder, DecodeProblem }] + +fromBytes : List U8 -> Result Str DecodeProblem +fromBytes = \bytes -> + Bytes.Decode.decode bytes (decodeBase64 (List.len bytes)) + +decodeBase64 : Nat -> ByteDecoder Str +decodeBase64 = \width -> Bytes.Decode.loop loopHelp { remaining: width, string: "" } + +loopHelp : { remaining : Nat, string : Str } -> ByteDecoder (Bytes.Decode.Step { remaining : Nat, string : Str } Str) +loopHelp = \{ remaining, string } -> + if remaining >= 3 then + x, y, z <- Bytes.Decode.map3 Bytes.Decode.u8 Bytes.Decode.u8 Bytes.Decode.u8 + + a : U32 + a = Num.intCast x + b : U32 + b = Num.intCast y + c : U32 + c = Num.intCast z + combined = Num.bitwiseOr (Num.bitwiseOr (Num.shiftLeftBy a 16) (Num.shiftLeftBy b 8)) c + + Loop { + remaining: remaining - 3, + string: Str.concat string (bitsToChars combined 0), + } + else if remaining == 0 then + Bytes.Decode.succeed (Done string) + else if remaining == 2 then + x, y <- Bytes.Decode.map2 Bytes.Decode.u8 Bytes.Decode.u8 + + a : U32 + a = Num.intCast x + b : U32 + b = Num.intCast y + combined = Num.bitwiseOr (Num.shiftLeftBy a 16) (Num.shiftLeftBy b 8) + + Done (Str.concat string (bitsToChars combined 1)) + else + # remaining = 1 + x <- Bytes.Decode.map Bytes.Decode.u8 + + a : U32 + a = Num.intCast x + + Done (Str.concat string (bitsToChars (Num.shiftLeftBy a 16) 2)) + +bitsToChars : U32, Int * -> Str +bitsToChars = \bits, missing -> + when Str.fromUtf8 (bitsToCharsHelp bits missing) is + Ok str -> str + Err _ -> "" + +# Mask that can be used to get the lowest 6 bits of a binary number +lowest6BitsMask : Int * +lowest6BitsMask = 63 + +bitsToCharsHelp : U32, Int * -> List U8 +bitsToCharsHelp = \bits, missing -> + # The input is 24 bits, which we have to partition into 4 6-bit segments. We achieve this by + # shifting to the right by (a multiple of) 6 to remove unwanted bits on the right, then `Num.bitwiseAnd` + # with `0b111111` (which is 2^6 - 1 or 63) (so, 6 1s) to remove unwanted bits on the left. + # any 6-bit number is a valid base64 digit, so this is actually safe + p = + Num.shiftRightZfBy bits 18 + |> Num.intCast + |> unsafeToChar + + q = + Num.bitwiseAnd (Num.shiftRightZfBy bits 12) lowest6BitsMask + |> Num.intCast + |> unsafeToChar + + r = + Num.bitwiseAnd (Num.shiftRightZfBy bits 6) lowest6BitsMask + |> Num.intCast + |> unsafeToChar + + s = + Num.bitwiseAnd bits lowest6BitsMask + |> Num.intCast + |> unsafeToChar + + equals : U8 + equals = 61 + + when missing is + 0 -> [p, q, r, s] + 1 -> [p, q, r, equals] + 2 -> [p, q, equals, equals] + _ -> + # unreachable + [] + +# Base64 index to character/digit +unsafeToChar : U8 -> U8 +unsafeToChar = \n -> + if n <= 25 then + # uppercase characters + 65 + n + else if n <= 51 then + # lowercase characters + 97 + (n - 26) + else if n <= 61 then + # digit characters + 48 + (n - 52) + else + # special cases + when n is + 62 -> + # '+' + 43 + + 63 -> + # '/' + 47 + + _ -> + # anything else is invalid '\u{0000}' + 0 diff --git a/crates/cli_testing_examples/benchmarks/Base64/Encode.roc b/crates/cli_testing_examples/benchmarks/Base64/Encode.roc new file mode 100644 index 0000000000..74a6ef9009 --- /dev/null +++ b/crates/cli_testing_examples/benchmarks/Base64/Encode.roc @@ -0,0 +1,176 @@ +interface Base64.Encode + exposes [toBytes] + imports [Bytes.Encode.{ ByteEncoder }] + +InvalidChar : U8 + +# State : [None, One U8, Two U8, Three U8] +toBytes : Str -> List U8 +toBytes = \str -> + str + |> Str.toUtf8 + |> encodeChunks + |> Bytes.Encode.sequence + |> Bytes.Encode.encode + +encodeChunks : List U8 -> List ByteEncoder +encodeChunks = \bytes -> + List.walk bytes { output: [], accum: None } folder + |> encodeResidual + +coerce : Nat, a -> a +coerce = \_, x -> x + +# folder : { output : List ByteEncoder, accum : State }, U8 -> { output : List ByteEncoder, accum : State } +folder = \{ output, accum }, char -> + when accum is + Unreachable n -> coerce n { output, accum: Unreachable n } + None -> { output, accum: One char } + One a -> { output, accum: Two a char } + Two a b -> { output, accum: Three a b char } + Three a b c -> + when encodeCharacters a b c char is + Ok encoder -> + { + output: List.append output encoder, + accum: None, + } + + Err _ -> + { output, accum: None } + +# SGVs bG8g V29y bGQ= +# encodeResidual : { output : List ByteEncoder, accum : State } -> List ByteEncoder +encodeResidual = \{ output, accum } -> + when accum is + Unreachable _ -> output + None -> output + One _ -> output + Two a b -> + when encodeCharacters a b equals equals is + Ok encoder -> List.append output encoder + Err _ -> output + + Three a b c -> + when encodeCharacters a b c equals is + Ok encoder -> List.append output encoder + Err _ -> output + +equals : U8 +equals = 61 + +# Convert 4 characters to 24 bits (as an ByteEncoder) +encodeCharacters : U8, U8, U8, U8 -> Result ByteEncoder InvalidChar +encodeCharacters = \a, b, c, d -> + if !(isValidChar a) then + Err a + else if !(isValidChar b) then + Err b + else + # `=` is the padding character, and must be special-cased + # only the `c` and `d` char are allowed to be padding + n1 = unsafeConvertChar a + n2 = unsafeConvertChar b + + x : U32 + x = Num.intCast n1 + + y : U32 + y = Num.intCast n2 + + if d == equals then + if c == equals then + n = Num.bitwiseOr (Num.shiftLeftBy x 18) (Num.shiftLeftBy y 12) + + # masking higher bits is not needed, Encode.unsignedInt8 ignores higher bits + b1 : U8 + b1 = Num.intCast (Num.shiftRightBy n 16) + + Ok (Bytes.Encode.u8 b1) + else if !(isValidChar c) then + Err c + else + n3 = unsafeConvertChar c + + z : U32 + z = Num.intCast n3 + + n = Num.bitwiseOr (Num.bitwiseOr (Num.shiftLeftBy x 18) (Num.shiftLeftBy y 12)) (Num.shiftLeftBy z 6) + + combined : U16 + combined = Num.intCast (Num.shiftRightBy n 8) + + Ok (Bytes.Encode.u16 BE combined) + else if !(isValidChar d) then + Err d + else + n3 = unsafeConvertChar c + n4 = unsafeConvertChar d + + z : U32 + z = Num.intCast n3 + + w : U32 + w = Num.intCast n4 + + n = + Num.bitwiseOr + (Num.bitwiseOr (Num.shiftLeftBy x 18) (Num.shiftLeftBy y 12)) + (Num.bitwiseOr (Num.shiftLeftBy z 6) w) + + b3 : U8 + b3 = Num.intCast n + + combined : U16 + combined = Num.intCast (Num.shiftRightBy n 8) + + Ok (Bytes.Encode.sequence [Bytes.Encode.u16 BE combined, Bytes.Encode.u8 b3]) + +# is the character a base64 digit? +# The base16 digits are: A-Z, a-z, 0-1, '+' and '/' +isValidChar : U8 -> Bool +isValidChar = \c -> + if isAlphaNum c then + Bool.true + else + when c is + 43 -> + # '+' + Bool.true + + 47 -> + # '/' + Bool.true + + _ -> + Bool.false + +isAlphaNum : U8 -> Bool +isAlphaNum = \key -> + (key >= 48 && key <= 57) || (key >= 64 && key <= 90) || (key >= 97 && key <= 122) + +# Convert a base64 character/digit to its index +# See also [Wikipedia](https://en.wikipedia.org/wiki/Base64#Base64_table) +unsafeConvertChar : U8 -> U8 +unsafeConvertChar = \key -> + if key >= 65 && key <= 90 then + # A-Z + key - 65 + else if key >= 97 && key <= 122 then + # a-z + (key - 97) + 26 + else if key >= 48 && key <= 57 then + # 0-9 + (key - 48) + 26 + 26 + else + when key is + 43 -> + # '+' + 62 + + 47 -> + # '/' + 63 + + _ -> + 0 diff --git a/crates/cli_testing_examples/benchmarks/Bytes/Decode.roc b/crates/cli_testing_examples/benchmarks/Bytes/Decode.roc new file mode 100644 index 0000000000..e52395ca3a --- /dev/null +++ b/crates/cli_testing_examples/benchmarks/Bytes/Decode.roc @@ -0,0 +1,111 @@ +interface Bytes.Decode exposes [ByteDecoder, decode, map, map2, u8, loop, Step, succeed, DecodeProblem, after, map3] imports [] + +State : { bytes : List U8, cursor : Nat } + +DecodeProblem : [OutOfBytes] + +ByteDecoder a := State -> [Good State a, Bad DecodeProblem] + +decode : List U8, ByteDecoder a -> Result a DecodeProblem +decode = \bytes, @ByteDecoder decoder -> + when decoder { bytes, cursor: 0 } is + Good _ value -> + Ok value + + Bad e -> + Err e + +succeed : a -> ByteDecoder a +succeed = \value -> @ByteDecoder \state -> Good state value + +map : ByteDecoder a, (a -> b) -> ByteDecoder b +map = \@ByteDecoder decoder, transform -> + @ByteDecoder + \state -> + when decoder state is + Good state1 value -> + Good state1 (transform value) + + Bad e -> + Bad e + +map2 : ByteDecoder a, ByteDecoder b, (a, b -> c) -> ByteDecoder c +map2 = \@ByteDecoder decoder1, @ByteDecoder decoder2, transform -> + @ByteDecoder + \state1 -> + when decoder1 state1 is + Good state2 a -> + when decoder2 state2 is + Good state3 b -> + Good state3 (transform a b) + + Bad e -> + Bad e + + Bad e -> + Bad e + +map3 : ByteDecoder a, ByteDecoder b, ByteDecoder c, (a, b, c -> d) -> ByteDecoder d +map3 = \@ByteDecoder decoder1, @ByteDecoder decoder2, @ByteDecoder decoder3, transform -> + @ByteDecoder + \state1 -> + when decoder1 state1 is + Good state2 a -> + when decoder2 state2 is + Good state3 b -> + when decoder3 state3 is + Good state4 c -> + Good state4 (transform a b c) + + Bad e -> + Bad e + + Bad e -> + Bad e + + Bad e -> + Bad e + +after : ByteDecoder a, (a -> ByteDecoder b) -> ByteDecoder b +after = \@ByteDecoder decoder, transform -> + @ByteDecoder + \state -> + when decoder state is + Good state1 value -> + (@ByteDecoder decoder1) = transform value + + decoder1 state1 + + Bad e -> + Bad e + +u8 : ByteDecoder U8 +u8 = @ByteDecoder + \state -> + when List.get state.bytes state.cursor is + Ok b -> + Good { state & cursor: state.cursor + 1 } b + + Err _ -> + Bad OutOfBytes + +Step state b : [Loop state, Done b] + +loop : (state -> ByteDecoder (Step state a)), state -> ByteDecoder a +loop = \stepper, initial -> + @ByteDecoder + \state -> + loopHelp stepper initial state + +loopHelp = \stepper, accum, state -> + (@ByteDecoder stepper1) = stepper accum + + when stepper1 state is + Good newState (Done value) -> + Good newState value + + Good newState (Loop newAccum) -> + loopHelp stepper newAccum newState + + Bad e -> + Bad e diff --git a/crates/cli_testing_examples/benchmarks/Bytes/Encode.roc b/crates/cli_testing_examples/benchmarks/Bytes/Encode.roc new file mode 100644 index 0000000000..9743721164 --- /dev/null +++ b/crates/cli_testing_examples/benchmarks/Bytes/Encode.roc @@ -0,0 +1,134 @@ +interface Bytes.Encode exposes [ByteEncoder, sequence, u8, u16, bytes, empty, encode] imports [] + +Endianness : [BE, LE] + +ByteEncoder : [Signed8 I8, Unsigned8 U8, Signed16 Endianness I16, Unsigned16 Endianness U16, Sequence Nat (List ByteEncoder), Bytes (List U8)] + +u8 : U8 -> ByteEncoder +u8 = \value -> Unsigned8 value + +empty : ByteEncoder +empty = + foo : List ByteEncoder + foo = [] + + Sequence 0 foo + +u16 : Endianness, U16 -> ByteEncoder +u16 = \endianness, value -> Unsigned16 endianness value + +bytes : List U8 -> ByteEncoder +bytes = \bs -> Bytes bs + +sequence : List ByteEncoder -> ByteEncoder +sequence = \encoders -> + Sequence (getWidths encoders 0) encoders + +getWidth : ByteEncoder -> Nat +getWidth = \encoder -> + when encoder is + Signed8 _ -> 1 + Unsigned8 _ -> 1 + Signed16 _ _ -> 2 + Unsigned16 _ _ -> 2 + # Signed32 _ -> 4 + # Unsigned32 _ -> 4 + # Signed64 _ -> 8 + # Unsigned64 _ -> 8 + # Signed128 _ -> 16 + # Unsigned128 _ -> 16 + Sequence w _ -> w + Bytes bs -> List.len bs + +getWidths : List ByteEncoder, Nat -> Nat +getWidths = \encoders, initial -> + List.walk encoders initial \accum, encoder -> accum + getWidth encoder + +encode : ByteEncoder -> List U8 +encode = \encoder -> + output = List.repeat 0 (getWidth encoder) + + encodeHelp encoder 0 output + |> .output + +encodeHelp : ByteEncoder, Nat, List U8 -> { output : List U8, offset : Nat } +encodeHelp = \encoder, offset, output -> + when encoder is + Unsigned8 value -> + { + output: List.set output offset value, + offset: offset + 1, + } + + Signed8 value -> + cast : U8 + cast = Num.intCast value + + { + output: List.set output offset cast, + offset: offset + 1, + } + + Unsigned16 endianness value -> + a : U8 + a = Num.intCast (Num.shiftRightBy value 8) + + b : U8 + b = Num.intCast value + + newOutput = + when endianness is + BE -> + output + |> List.set (offset + 0) a + |> List.set (offset + 1) b + + LE -> + output + |> List.set (offset + 0) b + |> List.set (offset + 1) a + + { + output: newOutput, + offset: offset + 2, + } + + Signed16 endianness value -> + a : U8 + a = Num.intCast (Num.shiftRightBy value 8) + + b : U8 + b = Num.intCast value + + newOutput = + when endianness is + BE -> + output + |> List.set (offset + 0) a + |> List.set (offset + 1) b + + LE -> + output + |> List.set (offset + 0) b + |> List.set (offset + 1) a + + { + output: newOutput, + offset: offset + 1, + } + + Bytes bs -> + List.walk + bs + { output, offset } + \accum, byte -> { + offset: accum.offset + 1, + output: List.set accum.output offset byte, + } + + Sequence _ encoders -> + List.walk + encoders + { output, offset } + \accum, single -> + encodeHelp single accum.offset accum.output diff --git a/crates/cli_testing_examples/benchmarks/CFold.roc b/crates/cli_testing_examples/benchmarks/CFold.roc new file mode 100644 index 0000000000..e4b1e2423d --- /dev/null +++ b/crates/cli_testing_examples/benchmarks/CFold.roc @@ -0,0 +1,126 @@ +app "cfold" + packages { pf: "platform/main.roc" } + imports [pf.Task] + provides [main] to pf + +# adapted from https://github.com/koka-lang/koka/blob/master/test/bench/haskell/cfold.hs +main : Task.Task {} [] +main = + inputResult <- Task.attempt Task.getInt + + when inputResult is + Ok n -> + e = mkExpr n 1 # original koka n = 20 (set `ulimit -s unlimited` to avoid stack overflow for n = 20) + unoptimized = eval e + optimized = eval (constFolding (reassoc e)) + + unoptimized + |> Num.toStr + |> Str.concat " & " + |> Str.concat (Num.toStr optimized) + |> Task.putLine + + Err GetIntError -> + Task.putLine "Error: Failed to get Integer from stdin." + +Expr : [ + Add Expr Expr, + Mul Expr Expr, + Val I64, + Var I64, +] + +mkExpr : I64, I64 -> Expr +mkExpr = \n, v -> + when n is + 0 -> + if v == 0 then Var 1 else Val v + + _ -> + Add (mkExpr (n - 1) (v + 1)) (mkExpr (n - 1) (max (v - 1) 0)) + +max : I64, I64 -> I64 +max = \a, b -> if a > b then a else b + +appendAdd : Expr, Expr -> Expr +appendAdd = \e1, e2 -> + when e1 is + Add a1 a2 -> + Add a1 (appendAdd a2 e2) + + _ -> + Add e1 e2 + +appendMul : Expr, Expr -> Expr +appendMul = \e1, e2 -> + when e1 is + Mul a1 a2 -> + Mul a1 (appendMul a2 e2) + + _ -> + Mul e1 e2 + +eval : Expr -> I64 +eval = \e -> + when e is + Var _ -> + 0 + + Val v -> + v + + Add l r -> + eval l + eval r + + Mul l r -> + eval l * eval r + +reassoc : Expr -> Expr +reassoc = \e -> + when e is + Add e1 e2 -> + x1 = reassoc e1 + x2 = reassoc e2 + + appendAdd x1 x2 + + Mul e1 e2 -> + x1 = reassoc e1 + x2 = reassoc e2 + + appendMul x1 x2 + + _ -> + e + +constFolding : Expr -> Expr +constFolding = \e -> + when e is + Add e1 e2 -> + x1 = constFolding e1 + x2 = constFolding e2 + + when x1 is + Val a -> + when x2 is + Val b -> Val (a + b) + Add (Val b) x | Add x (Val b) -> Add (Val (a + b)) x + _ -> Add x1 x2 + + _ -> Add x1 x2 + + Mul e1 e2 -> + x1 = constFolding e1 + x2 = constFolding e2 + + when x1 is + Val a -> + when x2 is + Val b -> Val (a * b) + Mul (Val b) x | Mul x (Val b) -> Mul (Val (a * b)) x + _ -> Mul x1 x2 + + _ -> Mul x1 x2 + + _ -> + e diff --git a/examples/benchmarks/Closure.roc b/crates/cli_testing_examples/benchmarks/Closure.roc similarity index 90% rename from examples/benchmarks/Closure.roc rename to crates/cli_testing_examples/benchmarks/Closure.roc index 653f9519e5..920a62aa1d 100644 --- a/examples/benchmarks/Closure.roc +++ b/crates/cli_testing_examples/benchmarks/Closure.roc @@ -1,9 +1,9 @@ app "closure" - packages { pf: "platform" } + packages { pf: "platform/main.roc" } imports [pf.Task] provides [main] to pf -# see https://github.com/rtfeldman/roc/issues/985 +# see https://github.com/roc-lang/roc/issues/985 main : Task.Task {} [] main = closure1 {} # |> Task.after (\_ -> closure2 {}) @@ -13,7 +13,7 @@ main = closure1 {} closure1 : {} -> Task.Task {} [] closure1 = \_ -> Task.succeed (foo toUnitBorrowed "a long string such that it's malloced") - |> Task.map (\_ -> {}) + |> Task.map \_ -> {} toUnitBorrowed = \x -> Str.countGraphemes x diff --git a/crates/cli_testing_examples/benchmarks/Deriv.roc b/crates/cli_testing_examples/benchmarks/Deriv.roc new file mode 100644 index 0000000000..7e4d22a6c2 --- /dev/null +++ b/crates/cli_testing_examples/benchmarks/Deriv.roc @@ -0,0 +1,167 @@ +app "deriv" + packages { pf: "platform/main.roc" } + imports [pf.Task] + provides [main] to pf + +# based on: https://github.com/koka-lang/koka/blob/master/test/bench/haskell/deriv.hs +IO a : Task.Task a [] + +main : Task.Task {} [] +main = + inputResult <- Task.attempt Task.getInt + + when inputResult is + Ok n -> + x : Expr + x = Var "x" + + f : Expr + f = pow x x + + nest deriv n f # original koka n = 10 + |> Task.map \_ -> {} + + Err GetIntError -> + Task.putLine "Error: Failed to get Integer from stdin." + +nestHelp : I64, (I64, Expr -> IO Expr), I64, Expr -> IO Expr +nestHelp = \s, f, m, x -> when m is + 0 -> Task.succeed x + _ -> + w <- Task.after (f (s - m) x) + nestHelp s f (m - 1) w + +nest : (I64, Expr -> IO Expr), I64, Expr -> IO Expr +nest = \f, n, e -> nestHelp n f n e + +Expr : [Val I64, Var Str, Add Expr Expr, Mul Expr Expr, Pow Expr Expr, Ln Expr] + +divmod : I64, I64 -> Result { div : I64, mod : I64 } [DivByZero] +divmod = \l, r -> + when Pair (Num.divTruncChecked l r) (Num.remChecked l r) is + Pair (Ok div) (Ok mod) -> Ok { div, mod } + _ -> Err DivByZero + +pown : I64, I64 -> I64 +pown = \a, n -> + when n is + 0 -> 1 + 1 -> a + _ -> + when divmod n 2 is + Ok { div, mod } -> + b = pown a div + + b * b * (if mod == 0 then 1 else a) + + Err DivByZero -> + -1 + +add : Expr, Expr -> Expr +add = \a, b -> + when Pair a b is + Pair (Val n) (Val m) -> + Val (n + m) + + Pair (Val 0) f -> + f + + Pair f (Val 0) -> + f + + Pair f (Val n) -> + add (Val n) f + + Pair (Val n) (Add (Val m) f) -> + add (Val (n + m)) f + + Pair f (Add (Val n) g) -> + add (Val n) (add f g) + + Pair (Add f g) h -> + add f (add g h) + + Pair f g -> + Add f g + +mul : Expr, Expr -> Expr +mul = \a, b -> + when Pair a b is + Pair (Val n) (Val m) -> + Val (n * m) + + Pair (Val 0) _ -> + Val 0 + + Pair _ (Val 0) -> + Val 0 + + Pair (Val 1) f -> + f + + Pair f (Val 1) -> + f + + Pair f (Val n) -> + mul (Val n) f + + Pair (Val n) (Mul (Val m) f) -> + mul (Val (n * m)) f + + Pair f (Mul (Val n) g) -> + mul (Val n) (mul f g) + + Pair (Mul f g) h -> + mul f (mul g h) + + Pair f g -> + Mul f g + +pow : Expr, Expr -> Expr +pow = \a, b -> + when Pair a b is + Pair (Val m) (Val n) -> Val (pown m n) + Pair _ (Val 0) -> Val 1 + Pair f (Val 1) -> f + Pair (Val 0) _ -> Val 0 + Pair f g -> Pow f g + +ln : Expr -> Expr +ln = \f -> + when f is + Val 1 -> Val 0 + _ -> Ln f + +d : Str, Expr -> Expr +d = \x, expr -> + when expr is + Val _ -> Val 0 + Var y -> if x == y then Val 1 else Val 0 + Add f g -> add (d x f) (d x g) + Mul f g -> add (mul f (d x g)) (mul g (d x f)) + Pow f g -> + mul (pow f g) (add (mul (mul g (d x f)) (pow f (Val (-1)))) (mul (ln f) (d x g))) + + Ln f -> + mul (d x f) (pow f (Val (-1))) + +count : Expr -> I64 +count = \expr -> + when expr is + Val _ -> 1 + Var _ -> 1 + Add f g -> count f + count g + Mul f g -> count f + count g + Pow f g -> count f + count g + Ln f -> count f + +deriv : I64, Expr -> IO Expr +deriv = \i, f -> + fprime = d "x" f + line = + Num.toStr (i + 1) + |> Str.concat " count: " + |> Str.concat (Num.toStr (count fprime)) + + Task.putLine line + |> Task.after \_ -> Task.succeed fprime diff --git a/examples/benchmarks/Issue2279.roc b/crates/cli_testing_examples/benchmarks/Issue2279.roc similarity index 75% rename from examples/benchmarks/Issue2279.roc rename to crates/cli_testing_examples/benchmarks/Issue2279.roc index 2927f677e5..c6d8be7523 100644 --- a/examples/benchmarks/Issue2279.roc +++ b/crates/cli_testing_examples/benchmarks/Issue2279.roc @@ -1,11 +1,11 @@ app "issue2279" - packages { pf: "platform" } + packages { pf: "platform/main.roc" } imports [Issue2279Help, pf.Task] provides [main] to pf main = text = - if True then + if Bool.true then Issue2279Help.text else Issue2279Help.asText 42 diff --git a/examples/benchmarks/Issue2279Help.roc b/crates/cli_testing_examples/benchmarks/Issue2279Help.roc similarity index 100% rename from examples/benchmarks/Issue2279Help.roc rename to crates/cli_testing_examples/benchmarks/Issue2279Help.roc diff --git a/crates/cli_testing_examples/benchmarks/NQueens.roc b/crates/cli_testing_examples/benchmarks/NQueens.roc new file mode 100644 index 0000000000..15593e3721 --- /dev/null +++ b/crates/cli_testing_examples/benchmarks/NQueens.roc @@ -0,0 +1,62 @@ +app "nqueens" + packages { pf: "platform/main.roc" } + imports [pf.Task] + provides [main] to pf + +main : Task.Task {} [] +main = + inputResult <- Task.attempt Task.getInt + + when inputResult is + Ok n -> + queens n # original koka 13 + |> Num.toStr + |> Task.putLine + + Err GetIntError -> + Task.putLine "Error: Failed to get Integer from stdin." + +ConsList a : [Nil, Cons a (ConsList a)] + +queens = \n -> length (findSolutions n n) + +findSolutions = \n, k -> + if k <= 0 then + # should we use U64 as input type here instead? + Cons Nil Nil + else + extend n Nil (findSolutions n (k - 1)) + +extend = \n, acc, solutions -> + when solutions is + Nil -> acc + Cons soln rest -> extend n (appendSafe n soln acc) rest + +appendSafe : I64, ConsList I64, ConsList (ConsList I64) -> ConsList (ConsList I64) +appendSafe = \k, soln, solns -> + if k <= 0 then + solns + else if safe k 1 soln then + appendSafe (k - 1) soln (Cons (Cons k soln) solns) + else + appendSafe (k - 1) soln solns + +safe : I64, I64, ConsList I64 -> Bool +safe = \queen, diagonal, xs -> + when xs is + Nil -> Bool.true + Cons q t -> + if queen != q && queen != q + diagonal && queen != q - diagonal then + safe queen (diagonal + 1) t + else + Bool.false + +length : ConsList a -> I64 +length = \xs -> + lengthHelp xs 0 + +lengthHelp : ConsList a, I64 -> I64 +lengthHelp = \foobar, acc -> + when foobar is + Cons _ lrest -> lengthHelp lrest (1 + acc) + Nil -> acc diff --git a/crates/cli_testing_examples/benchmarks/Quicksort.roc b/crates/cli_testing_examples/benchmarks/Quicksort.roc new file mode 100644 index 0000000000..0e934709c4 --- /dev/null +++ b/crates/cli_testing_examples/benchmarks/Quicksort.roc @@ -0,0 +1,75 @@ +interface Quicksort exposes [sortBy, sortWith, show] imports [] + +show : List I64 -> Str +show = \list -> + if List.isEmpty list then + "[]" + else + content = + list + |> List.map Num.toStr + |> Str.joinWith ", " + + "[\(content)]" + +sortBy : List a, (a -> Num *) -> List a +sortBy = \list, toComparable -> + sortWith list (\x, y -> Num.compare (toComparable x) (toComparable y)) + +Order a : a, a -> [LT, GT, EQ] + +sortWith : List a, (a, a -> [LT, GT, EQ]) -> List a +sortWith = \list, order -> + n = List.len list + + quicksortHelp list order 0 (n - 1) + +quicksortHelp : List a, Order a, Nat, Nat -> List a +quicksortHelp = \list, order, low, high -> + if low < high then + when partition low high list order is + Pair partitionIndex partitioned -> + partitioned + |> quicksortHelp order low (Num.subSaturated partitionIndex 1) + |> quicksortHelp order (partitionIndex + 1) high + else + list + +partition : Nat, Nat, List a, Order a -> [Pair Nat (List a)] +partition = \low, high, initialList, order -> + when List.get initialList high is + Ok pivot -> + when partitionHelp low low initialList order high pivot is + Pair newI newList -> + Pair newI (swap newI high newList) + + Err _ -> + Pair low initialList + +partitionHelp : Nat, Nat, List c, Order c, Nat, c -> [Pair Nat (List c)] +partitionHelp = \i, j, list, order, high, pivot -> + if j < high then + when List.get list j is + Ok value -> + when order value pivot is + LT | EQ -> + partitionHelp (i + 1) (j + 1) (swap i j list) order high pivot + + GT -> + partitionHelp i (j + 1) list order high pivot + + Err _ -> + Pair i list + else + Pair i list + +swap : Nat, Nat, List a -> List a +swap = \i, j, list -> + when Pair (List.get list i) (List.get list j) is + Pair (Ok atI) (Ok atJ) -> + list + |> List.set i atJ + |> List.set j atI + + _ -> + [] diff --git a/examples/benchmarks/QuicksortApp.roc b/crates/cli_testing_examples/benchmarks/QuicksortApp.roc similarity index 99% rename from examples/benchmarks/QuicksortApp.roc rename to crates/cli_testing_examples/benchmarks/QuicksortApp.roc index 930a9f35b4..67766bc982 100644 --- a/examples/benchmarks/QuicksortApp.roc +++ b/crates/cli_testing_examples/benchmarks/QuicksortApp.roc @@ -1,13 +1,14 @@ app "quicksortapp" - packages { pf: "platform" } + packages { pf: "platform/main.roc" } imports [pf.Task, Quicksort] provides [main] to pf main : Task.Task {} [] main = - Task.after - Task.getInt - \n -> + inputResult <- Task.attempt Task.getInt + + when inputResult is + Ok n -> unsortedList = if n == 0 then # small unsorted list of 20 elements (0-9) @@ -17,8 +18,11 @@ main = [4281, 4149, 4579, 3763, 4892, 3305, 740, 1003, 3748, 4353, 1027, 2205, 4096, 4047, 1883, 3757, 3813, 2757, 2241, 1417, 2054, 1819, 680, 3645, 1979, 3897, 2180, 4072, 3688, 3440, 1107, 3511, 133, 4586, 3432, 3279, 2743, 1489, 4058, 2880, 141, 3039, 3270, 1518, 3879, 3241, 577, 981, 233, 2238, 1571, 1056, 721, 2856, 2309, 1274, 969, 1132, 1492, 2659, 106, 1145, 2328, 3854, 998, 2594, 1359, 1172, 3952, 4137, 3539, 3558, 3947, 1705, 1726, 1292, 1669, 2636, 661, 1079, 4190, 4398, 3954, 2016, 4704, 2748, 2273, 261, 321, 1278, 917, 713, 1241, 1827, 1402, 2956, 4481, 3126, 250, 3932, 727, 2689, 2566, 4584, 1718, 1634, 3977, 3045, 3360, 1072, 982, 2277, 4175, 707, 1192, 3124, 2408, 2734, 4173, 4491, 3060, 1095, 845, 4405, 4617, 2762, 1431, 2720, 4510, 1516, 1770, 1010, 499, 2346, 4191, 2684, 3979, 1794, 2796, 1973, 407, 2764, 1975, 3629, 3945, 4635, 2157, 497, 1720, 583, 520, 2036, 3638, 561, 2168, 2301, 4432, 4086, 1465, 1560, 90, 4092, 2882, 3496, 3609, 4961, 507, 106, 242, 1752, 2154, 4410, 4066, 2333, 1570, 621, 1622, 1915, 517, 4424, 2901, 4704, 394, 1066, 4001, 4645, 624, 3833, 165, 2312, 3795, 3707, 557, 4567, 1897, 4894, 4840, 2805, 655, 1514, 2376, 3504, 1029, 4207, 3054, 4802, 4253, 2339, 2278, 3180, 2426, 861, 1582, 1373, 131, 4390, 4168, 4749, 1948, 2489, 586, 614, 2833, 840, 4136, 2443, 4112, 1850, 2207, 4087, 162, 1030, 1983, 2464, 2840, 4644, 2613, 1305, 4340, 3742, 1422, 1703, 1184, 2389, 997, 413, 3787, 199, 1727, 780, 1055, 879, 3400, 4592, 2140, 4564, 3466, 4630, 2127, 197, 4719, 4045, 2818, 2817, 2207, 507, 3047, 3619, 2327, 4196, 1659, 1603, 2104, 1649, 3893, 1615, 2905, 2216, 224, 2140, 4018, 2590, 3647, 3456, 2173, 1573, 4315, 2389, 2787, 137, 4287, 792, 4260, 1363, 3609, 3610, 2550, 2788, 1020, 4347, 2513, 4633, 3633, 3875, 4670, 503, 1698, 4651, 4725, 3394, 1085, 1275, 407, 4394, 132, 2102, 1239, 2186, 588, 1212, 4641, 4371, 4448, 1910, 3735, 4489, 2793, 2422, 2177, 3639, 2284, 3887, 2759, 4667, 2735, 976, 2055, 2791, 2593, 1406, 2439, 1010, 1160, 14, 3388, 3788, 3982, 3907, 3423, 996, 2863, 2370, 2846, 2297, 18, 1961, 1495, 290, 2390, 4248, 3001, 2365, 513, 2494, 1963, 3833, 3284, 375, 4760, 2711, 1436, 195, 1891, 3935, 4384, 1943, 3347, 126, 1582, 3977, 1398, 2483, 528, 230, 1896, 1421, 1843, 4487, 2893, 4240, 3157, 4135, 2887, 113, 1307, 1960, 593, 2435, 4358, 102, 4780, 537, 1394, 1901, 4554, 2099, 2599, 4467, 4145, 192, 1759, 4141, 4102, 3631, 3425, 467, 3017, 1210, 1108, 1243, 3980, 2772, 1525, 2382, 2795, 4731, 2688, 2303, 1511, 3260, 4863, 4792, 3306, 432, 3068, 1586, 3358, 1327, 3136, 3715, 1515, 4144, 4902, 3557, 3891, 1116, 172, 4655, 2079, 1759, 366, 1223, 2066, 155, 1343, 1106, 1016, 3055, 810, 1430, 4618, 1031, 4488, 2571, 240, 4196, 1896, 2887, 4930, 3694, 2395, 1156, 2889, 4749, 4109, 112, 1578, 450, 1422, 1505, 2266, 3756, 3597, 2655, 3748, 3371, 1659, 1867, 139, 879, 469, 1601, 2566, 3471, 1905, 4403, 2714, 1657, 3867, 563, 1820, 4992, 3084, 4559, 2727, 493, 1045, 4884, 1304, 4596, 4843, 978, 3010, 2282, 3267, 1995, 3942, 4235, 2676, 528, 417, 1860, 4944, 2352, 2603, 496, 2395, 4391, 1183, 4271, 3431, 3219, 3728, 873, 4956, 4460, 3638, 1935, 511, 1820, 1753, 2536, 131, 2253, 736, 1438, 2762, 4677, 4836, 8, 4524, 4711, 1575, 334, 3860, 4697, 4237, 3095, 4017, 3574, 2925, 2398, 4938, 2612, 408, 2086, 1938, 1349, 3405, 2827, 4158, 2071, 858, 377, 1043, 776, 4126, 3981, 81, 4471, 916, 52, 427, 8, 4922, 310, 43, 4770, 800, 3928, 1616, 848, 2818, 1300, 4847, 2744, 2528, 3825, 3582, 4781, 2911, 4309, 1188, 2451, 447, 2057, 966, 1159, 2683, 2542, 4010, 367, 252, 1199, 3765, 3974, 1794, 3464, 55, 501, 3389, 745, 763, 3937, 1077, 624, 2896, 3609, 4555, 3830, 3731, 1240, 1844, 4091, 3081, 587, 424, 3000, 1682, 2332, 3235, 3995, 3520, 3567, 2748, 501, 4661, 594, 385, 3271, 3980, 401, 2904, 1184, 1240, 3933, 825, 4068, 1561, 4387, 2776, 2581, 3117, 2219, 3224, 980, 2160, 1941, 2588, 3254, 4410, 261, 414, 802, 1536, 4245, 2935, 1059, 4098, 2750, 4707, 224, 1867, 740, 593, 2448, 408, 2244, 3719, 4149, 1952, 1509, 893, 558, 2409, 409, 4287, 4817, 3790, 2356, 3100, 4204, 416, 2778, 3876, 894, 1115, 4071, 630, 386, 1346, 41, 4039, 2816, 3001, 500, 1557, 15, 775, 42, 3161, 1449, 2916, 2333, 4947, 3883, 4744, 2792, 2434, 1929, 3599, 831, 4511, 813, 927, 4273, 4285, 496, 4068, 2450, 3893, 4982, 173, 667, 3347, 370, 2424, 3855, 538, 2692, 3084, 456, 107, 4433, 4088, 1181, 1755, 4761, 4932, 3014, 221, 1925, 1649, 2944, 1646, 3994, 1389, 4617, 4609, 3952, 2870, 3535, 4859, 4733, 3696, 3549, 1038, 716, 3474, 2122, 4629, 3495, 992, 4594, 4766, 2389, 4901, 4041, 2453, 894, 2077, 874, 188, 2405, 543, 991, 3601, 3121, 329, 402, 2948, 4382, 3203, 3387, 2098, 3473, 3986, 516, 3243, 2341, 490, 4828, 861, 4527, 2197, 3777, 4860, 1031, 1546, 751, 4670, 826, 4832, 2813, 3820, 4927, 3628, 2082, 4043, 3990, 2929, 1545, 4235, 3792, 4680, 131, 4097, 2694, 652, 4785, 4419, 3044, 3127, 470, 3, 1198, 3754, 2969, 503, 3957, 582, 4109, 1982, 4487, 4330, 1452, 2202, 3601, 3797, 4331, 3630, 471, 1655, 1435, 3624, 394, 4306, 207, 1488, 126, 367, 4870, 3969, 3448, 4265, 3475, 1058, 2437, 267, 591, 4352, 4363, 1096, 3807, 2547, 4328, 2811, 4251, 3241, 3518, 1377, 4983, 1353, 1900, 676, 1969, 1213, 190, 1700, 4020, 2219, 4822, 4659, 1324, 4961, 4675, 2317, 467, 835, 179, 1129, 1093, 852, 1360, 3585, 1999, 742, 3284, 3485, 3383, 4864, 4474, 3600, 653, 1333, 767, 4075, 2345, 4888, 1263, 4482, 2638, 4430, 1173, 4543, 2068, 1466, 4302, 3583, 345, 2633, 2515, 2630, 3717, 693, 1906, 2944, 510, 4609, 4064, 3643, 4010, 657, 3435, 2258, 630, 3037, 3199, 3904, 1139, 1446, 1776, 492, 2942, 188, 2935, 1247, 2000, 4413, 1036, 3485, 4691, 606, 1678, 541, 4380, 2673, 3241, 4403, 1896, 1202, 3323, 2433, 2982, 647, 1564, 3543, 1916, 483, 4040, 2974, 4234, 4025, 3208, 2109, 1055, 2989, 4210, 1944, 3504, 465, 1687, 1569, 2783, 97, 65, 4948, 3311, 1885, 2228, 3961, 4695, 4268, 4063, 166, 3998, 2070, 3547, 2148, 2995, 2451, 2833, 4243, 438, 951, 23, 2913, 1301, 2593, 4557, 2278, 4460, 1142, 3740, 2562, 3369, 2601, 3222, 2230, 2475, 4720, 4568, 572, 2737, 4157, 685, 1934, 3377, 4864, 1586, 3916, 2828, 1462, 2539, 4147, 3028, 4734, 3198, 4312, 234, 3578, 1409, 3776, 291, 1168, 1437, 1454, 3965, 4735, 2310, 3865, 4361, 4374, 3684, 1920, 3844, 3982, 1316, 3922, 2116, 641, 4220, 684, 4937, 313, 2456, 65, 4674, 4960, 2432, 4547, 4090, 624, 210, 3766, 3452, 2226, 1189, 605, 4665, 1528, 1553, 1532, 1669, 4051, 2136, 2392, 4053, 2251, 1601, 992, 3924, 2205, 444, 3260, 417, 922, 2362, 3285, 3414, 2350, 4612, 806, 2803, 1990, 2229, 4273, 3311, 379, 2451, 3479, 1900, 4704, 744, 3895, 1795, 817, 3163, 1770, 1677, 4136, 3643, 2368, 2223, 923, 1526, 3917, 2906, 4849, 4457, 4614, 482, 1340, 4931, 1243, 595, 1297, 720, 3060, 2013, 3678, 1163, 2373, 2067, 4725, 2306, 1205, 4180, 4280, 4069, 600, 648, 3515, 762, 778, 1094, 2792, 2495, 1410, 1023, 1627, 3494, 3172, 2991, 2502, 2014, 4077, 3158, 706, 3970, 1436, 4509, 3981, 1207, 1725, 1552, 738, 3873, 4389, 4788, 448, 784, 729, 355, 3491, 308, 328, 4536, 3921, 871, 330, 3655, 4562, 2544, 1274, 3874, 3156, 3979, 1462, 3874, 4016, 4828, 1497, 2818, 4819, 4440, 4833, 2974, 4997, 232, 4843, 4631, 3602, 3109, 1249, 1565, 4556, 3899, 4216, 791, 2908, 2877, 3838, 3102, 3187, 1579, 472, 1916, 3707, 4749, 2701, 2184, 3172, 3743, 4503, 4928, 79, 4743, 1840, 4946, 2584, 1103, 4328, 1819, 4892, 1850, 749, 1717, 3878, 3507, 957, 1469, 57, 3449, 3451, 2988, 4790, 440, 2495, 770, 3417, 4243, 4273, 4189, 757, 2809, 2711, 444, 23, 3318, 3323, 2848, 316, 2036, 44, 2443, 1718, 2042, 1755, 1361, 1049, 2577, 2771, 4908, 3446, 2370, 2802, 3208, 1352, 1932, 2292, 439, 546, 2160, 4736, 4236, 625, 302, 2662, 1115, 3377, 1892, 4705, 1164, 1657, 4675, 2581, 3397, 2128, 744, 4944, 3110, 1673, 672, 4960, 3888, 1621, 3368, 4293, 3066, 3140, 1191, 1965, 2444, 4717, 218, 1385, 1668, 1207, 2764, 588, 2350, 964, 3098, 4045, 764, 3454, 4673, 1115, 3129, 3415, 4340, 1040, 2705, 924, 2776, 1, 3048, 2226, 2977, 3184, 222, 258, 4304, 453, 2176, 4623, 4166, 1949, 3002, 3917, 2304, 4147, 473, 4184, 441, 4788, 4072, 4982, 1434, 1227, 3484, 2237, 2781, 4271, 4578, 1609, 3531, 482, 2884, 1737, 1193, 4598, 2875, 2117, 1684, 2827, 4542, 3435, 908, 4076, 145, 4829, 1008, 445, 3505, 3711, 3246, 3289, 1011, 3018, 2962, 3631, 3019, 120, 4109, 2033, 2615, 4078, 3339, 1183, 3553, 969, 2093, 1306, 1102, 4674, 4567, 3871, 208, 4076, 2969, 2475, 2923, 960, 1618, 1666, 4600, 4468, 290, 4774, 4164, 2037, 2039, 187, 248, 4995, 1048, 1934, 4592, 2120, 3030, 4183, 4832, 1816, 1653, 839, 1832, 16, 2201, 2838, 323, 3270, 2133, 1722, 3816, 3469, 2197, 4336, 3734, 2088, 4666, 2299, 4975, 2704, 3493, 3574, 1152, 853, 1294, 95, 3057, 3010, 1821, 2855, 909, 4879, 620, 4944, 3780, 2516, 4279, 4588, 1714, 2439, 3161, 320, 1382, 1444, 4825, 4228, 3487, 2604, 2232, 1539, 201, 3867, 2869, 411, 46, 3228, 247, 3911, 3511, 351, 2769, 1322, 4826, 2203, 890, 3788, 2824, 3837, 1896, 1967, 3239, 2078, 2617, 3591, 4943, 2532, 2276, 3184, 3456, 312, 2519, 1270, 3996, 2548, 908, 333, 1845, 1167, 369, 1957, 4307, 1501, 1971, 3247, 4857, 3054, 807, 3738, 3072, 4964, 4901, 3514, 2858, 3055, 4569, 3854, 1774, 1876, 770, 2121, 2326, 1937, 2661, 163, 201, 2200, 4013, 3648, 1735, 2438, 3907, 3636, 4299, 3736, 21, 4253, 2514, 186, 466, 2205, 4728, 1035, 1754, 2873, 3581, 3441, 3539, 2999, 4225, 1547, 1689, 2009, 1691, 3751, 3759, 3438, 4774, 867, 1777, 2616, 7, 756, 3411, 1973, 2544, 1132, 2400, 1380, 2520, 2538, 423, 702, 3146, 2029, 3476, 4848, 1003, 3617, 1455, 666, 134, 1296, 813, 1842, 723, 2874, 160, 4041, 1240, 1907, 1634, 725, 1881, 3700, 1238, 1639, 3915, 4499, 2692, 940, 3218, 1931, 3801, 1719, 764, 4816, 1876, 4402, 3599, 890, 198, 940, 1534, 599, 3052, 1232, 910, 3055, 2397, 2772, 1996, 250, 3285, 3573, 2523, 3802, 828, 4554, 4330, 1176, 1222, 4915, 1798, 1580, 4738, 4551, 3322, 1683, 4755, 2995, 4512, 1820, 3685, 224, 4170, 1802, 2824, 2628, 4285, 4236, 4198, 3336, 2651, 10, 2809, 4060, 3718, 831, 2947, 3734, 928, 3624, 849, 2567, 2310, 4474, 4721, 2904, 1491, 4365, 4447, 2052, 4744, 3973, 2650, 3480, 876, 2463, 1084, 3942, 691, 1073, 4757, 229, 1497, 2229, 1669, 674, 256, 2206, 3311, 2800, 592, 2838, 1042, 2123, 602, 434, 552, 2776, 4179, 2976, 4592, 686, 2805, 4840, 484, 4134, 789, 2790, 73, 3113, 4158, 1956, 1377, 1199, 4901, 4755, 1026, 2821, 2649, 3301, 2651, 1398, 330, 1482, 4844, 687, 1116, 4131, 659, 3150, 4282, 475, 2162, 2084, 4439, 3145, 145, 4693, 2926, 3200, 4720, 4341, 4980, 4850, 3961, 3565, 4604, 466, 4441, 832, 2084, 4875, 739, 2790, 523, 3983, 4160, 630, 4038, 1891, 2324, 667, 420, 1361, 2710, 1205, 412, 2410, 2888, 4873, 2372, 612, 1923, 2857, 2725, 4823, 1042, 1721, 2140, 4529, 2801, 3637, 1995, 1503, 4956, 1766, 3910, 1474, 644, 1410, 4705, 3892, 617, 532, 3475, 3111, 646, 4344, 4830, 571, 3380, 862, 4291, 4558, 2115, 861, 1151, 3521, 4244, 2037, 2272, 39, 1371, 1355, 4179, 3072, 442, 4189, 3669, 93, 3968, 3759, 1213, 4340, 1154, 2941, 1192, 3821, 1289, 2307, 559, 424, 3911, 3161, 1996, 4525, 3811, 2902, 3669, 1219, 770, 2820, 1780, 1950, 4995, 345, 3772, 307, 1887, 1452, 3359, 835, 4830, 3846, 2849, 2132, 4070, 1971, 1797, 2890, 3468, 1600, 3872, 937, 4282, 1998, 3550, 908, 4867, 2377, 3462, 2349, 4434, 3216, 1461, 2681, 1775, 4752, 1439, 3203, 4235, 4147, 4750, 4041, 2051, 10, 2780, 2054, 3566, 4111, 2698, 3520, 3816, 2589, 4700, 2338, 1973, 2444, 1465, 1741, 712, 3457, 4272, 4895, 1409, 4963, 2163, 3369, 4570, 1976, 4879, 1459, 2809, 477, 4946, 3800, 3455, 4720, 434, 2919, 4824, 3688, 3555, 2618, 3615, 385, 4330, 4140, 1552, 737, 4288, 4000, 4086, 186, 1673, 2449, 3272, 3262, 1682, 14, 3119, 1206, 2461, 1625, 4644, 2637, 224, 2300, 1708, 2596, 3650, 1312, 775, 4863, 1556, 3868, 1180, 1061, 836, 4295, 2343, 2063, 2186, 3632, 674, 169, 2082, 3316, 2149, 2812, 1032, 2852, 114, 2735, 4646, 58, 744, 3299, 3623, 3607, 3923, 2972, 3414, 3583, 765, 3515, 2965, 295, 4348, 4470, 647, 4716, 2449, 3788, 643, 4188, 3504, 2477, 3341, 1219, 3362, 3267, 3859, 4867, 356, 576, 914, 4799, 2016, 464, 326, 1346, 170, 3880, 4708, 3797, 3992, 2334, 1675, 2084, 481, 421, 1967, 3402, 2183, 766, 2457, 1001, 2993, 3087, 2149, 1252, 3249, 108, 1506, 4488, 2467, 4145, 4475, 3194, 3128, 829, 2059, 3855, 3016, 4874, 1767, 4926, 4170, 119, 3115, 3378, 1256, 1436, 1124, 2819, 592, 522, 3965, 1341, 543, 4912, 2172, 2042, 4834, 4408, 4710, 1863, 4139, 1023, 3996, 2430, 3018, 1574, 2133, 2783, 3716, 1290, 4735, 2425, 4100, 4572, 596, 3189, 2472, 2581, 1409, 2491, 394, 1688, 4597, 2323, 3615, 218, 391, 3287, 2251, 2159, 448, 1178, 925, 228, 3149, 129, 4113, 4031, 2203, 3489, 2631, 268, 2556, 1934, 4581, 712, 4455, 2137, 2474, 3369, 2065, 4924, 3954, 3654, 4730, 3018, 2201, 3975, 1824, 1174, 3195, 1826, 3162, 3273, 1324, 2747, 3767, 619, 714, 3690, 4744, 4198, 597, 604, 1481, 2615, 658, 701, 3166, 4601, 2613, 2864, 3198, 2340, 593, 697, 2345, 4135, 503, 4291, 1559, 3834, 2900, 601, 3722, 613, 313, 896, 1389, 210, 896, 1019, 1174, 1272, 4386, 1672, 2158, 58, 3045, 1933, 2193, 3534, 1660, 2259, 3120, 4678, 1699, 857, 2200, 4399, 2460, 4972, 2731, 166, 3209, 1124, 3143, 249, 2763, 3986, 4186, 1018, 875, 2707, 294, 4042, 1571, 2964, 1407, 780, 4172, 1436, 4576, 543, 2045, 4535, 774, 1744, 4726, 2401, 2392, 1832, 4104, 2271, 4690, 477, 1582, 433, 4457, 2891, 3310, 1586, 4998, 228, 4771, 2426, 3411, 3969, 1677, 610, 4306, 1486, 2107, 341, 2435, 2791, 2670, 4437, 543, 3487, 3252, 3308, 653, 2615, 1414, 247, 2325, 4668, 166, 4087, 4913, 395, 1459, 3842, 4085, 4738, 4150, 4796, 3732, 409, 109, 2460, 3631, 3196, 4989, 4543, 115, 2910, 1277, 3398, 2277, 2748, 3873, 4529, 4035, 2397, 1047, 290, 4040, 2444, 4099, 4129, 344, 3112, 2617, 2039, 1399, 2786, 3335, 258, 1057, 2648, 2521, 4177, 2132, 4976, 2691, 4035, 3683, 1648, 4866, 336, 4792, 2064, 681, 4395, 3937, 1383, 179, 3190, 4743, 3518, 190, 3276, 3536, 4118, 998, 2950, 315, 3981, 1849, 655, 4864, 2239, 667, 1854, 505, 672, 2905, 3005, 2902, 848, 2674, 1363, 2172, 1997, 164, 183, 943, 619, 496, 86, 4457, 1680, 15, 13, 2785, 4164, 2774, 2497, 1415, 1930, 1784, 4144, 2684, 1093, 1712, 959, 3937, 4338, 1185, 4047, 3026, 788, 3554, 856, 2842, 4365, 1365, 3869, 601, 4791, 872, 4103, 2249, 3718, 4285, 2845, 108, 673, 833, 4721, 863, 793, 3078, 4059, 2608, 1448, 4657, 4414, 1090, 1232, 4575, 4572, 4087, 2080, 4579, 301, 4170, 3781, 186, 3661, 3120, 3710, 1090, 4137, 2690, 1002, 1378, 3566, 4420, 1529, 4317, 147, 2660, 4156, 3361, 2691, 4853, 2907, 1066, 1353, 3779, 1257, 3063, 3499, 1749, 719, 3958, 3278, 1047, 646, 2964, 405, 483, 2180, 4509, 2785, 1833, 4437, 3314, 4822, 1122, 1222, 2021, 1598, 4674, 1441, 288, 1657, 2195, 3179, 4479, 3943, 144, 2018, 2633, 4544, 2660, 3875, 3266, 4555, 2934, 3211, 3275, 3769, 1122, 1680, 2140, 2433, 979, 92, 1207, 4971, 583, 964, 3770, 1427, 4636, 1320, 3383, 639, 3218, 438, 1905, 4190, 3919, 1235, 381, 4334, 4223, 4435, 901, 1383, 333, 3596, 2853, 2455, 812, 4043, 2766, 717, 213, 416, 4165, 1130, 1416, 2119, 929, 519, 8, 4904, 4318, 732, 3043, 1728, 3011, 786, 4442, 1085, 2812, 873, 2545, 0, 4085, 4022, 3708, 633, 414, 2440, 1506, 1065, 4450, 4968, 3964, 2380, 3439, 469, 1505, 1001, 1138, 2042, 4548, 826, 298, 2688, 1860, 4108, 3749, 2870, 4513, 1655, 4236, 3919, 4107, 2159, 2809, 4569, 753, 2943, 4429, 4401, 4903, 1727, 28, 4438, 3322, 3042, 4108, 2398, 1697, 2448, 1057, 3503, 1125, 3337, 477, 2051, 986, 4733, 4114, 3420, 48, 1215, 2541, 4511, 1560, 4966, 1588, 2444, 1061, 4625, 4125, 4792, 1184, 4061, 1296, 4415, 3554, 2496, 1860, 267, 916, 3203, 1612, 2416, 4188, 665, 3186, 305, 1590, 4456, 3020, 3224, 3352, 1270, 1526, 3658, 3837, 3119, 2982, 2003, 3052, 1934, 3280, 4316, 1643, 3127, 4071, 493, 3411, 3367, 718, 712, 527, 1844, 4150, 4344, 3559, 4891, 3679, 3341, 1934, 4526, 4279, 2921, 1827, 3431, 2588, 4870, 3826, 2388, 1028, 361, 2999, 2922, 2094, 4244, 139, 4082, 2190, 2652, 1028, 4033, 4299, 280, 178, 4238, 3511, 1381, 1374, 3551, 3766, 4112, 4130, 3104, 2021, 542, 599, 1814, 4610, 2997, 331, 3988, 4890, 2068, 4253, 3929, 2127, 1849, 2185, 4164, 2904, 3439, 4543, 4256, 4934, 4869, 4192, 1043, 799, 4027, 4173, 3357, 4348, 2056, 4101, 2143, 3038, 2384, 1281, 4937, 200, 2489, 1413, 4841, 3930, 3444, 4835, 833, 1938, 4381, 766, 4436, 3167, 2287, 3828, 4270, 2539, 2365, 2508, 3965, 4631, 3099, 2806, 4160, 1234, 1811, 427, 820, 1093, 2433, 4020, 2786, 535, 566, 556, 4616, 4227, 893, 277, 3345, 2043, 3202, 1756, 2757, 2485, 2876, 719, 365, 789, 1865, 1740, 58, 684, 2535, 3405, 3233, 787, 4600, 2421, 1935, 3757, 1462, 4891, 3377, 3338, 4793, 931, 1931, 1105, 1413, 3060, 1602, 531, 1095, 375, 372, 1809, 1276, 2307, 3231, 1493, 344, 3842, 2380, 2042, 3500, 4944, 1290, 1009, 3114, 2857, 937, 4159, 3801, 4189, 4252, 978, 428, 2146, 1386, 182, 558, 2147, 4472, 3930, 3932, 4225, 2803, 4052, 2102, 3013, 267, 901, 1181, 4125, 2851, 4699, 28, 3887, 553, 3347, 2091, 2564, 1176, 3894, 1551, 3435, 1722, 4968, 4875, 3005, 4179, 2207, 2281, 3621, 1336, 2068, 84, 3681, 3086, 4740, 3802, 2458, 4982, 1095, 4132, 4280, 3031, 4305, 1142, 512, 4683, 1554, 3895, 1477, 498, 1242, 4849, 3286, 1396, 3081, 1204, 975, 2235, 2340, 2545, 746, 75, 763, 2986, 1127, 403, 560, 83, 2602, 3947, 485, 4218, 3570, 311, 420, 3835, 2016, 2726, 2372, 1879, 1714, 1202, 3171, 2427, 1227, 3891, 2887, 3736, 2324, 1641, 4637, 4550, 1356, 817, 1207, 789, 4859, 4249, 1438, 4504, 3388, 2365, 3958, 3082, 283, 94, 4211, 2433, 2134, 2256, 1406, 1137, 150, 2519, 2215, 1393, 3664, 4962, 4491, 4873, 282, 4187, 2245, 1296, 3150, 1359, 4711, 4, 4986, 4376, 1933, 549, 3639, 3690, 2302, 4220, 2204, 21, 4578, 1827, 2654, 833, 465, 1340, 303, 4747, 3926, 4667, 69, 4510, 2911, 4950, 3194, 4501, 3167, 923, 2779, 4962, 316, 2158, 1576, 528, 2863, 835, 453, 4153, 3682, 4604, 279, 3484, 797, 1075, 4679, 461, 3688, 3821, 1589, 3247, 589, 1433, 1053, 4798, 2262, 1252, 745, 3621, 814, 997, 1658, 585, 2823, 2677, 3168, 1806, 3065, 3228, 1779, 4536, 4985, 659, 1323, 2465, 4608, 963, 845, 4132, 4142, 2232, 3475, 650, 4563, 1772, 2816, 4082, 4732, 60, 4223, 4211, 4238, 2573, 2569, 737, 178, 3722, 2675, 2632, 4457, 1870, 2275, 1915, 865, 4310, 3789, 1225, 4035, 1217, 4180, 3858, 2130, 1957, 2226, 452, 1307, 917, 713, 4780, 1161, 4473, 393, 829, 1855, 4188, 3483, 4258, 1210, 4033, 3714, 3888, 3897, 4081, 2573, 4360, 960, 4116, 2805, 2854, 4939, 426, 4636, 2845, 4617, 4297, 4341, 1481, 1134, 2437, 2880, 1104, 694, 3780, 4247, 4179, 4870, 2589, 4813, 4651, 1298, 3268, 1018, 657, 2514, 3414, 845, 603, 2546, 1548, 1817, 3770, 844, 2303, 4894, 1581, 4758, 290, 3948, 3456, 2670, 3584, 4368, 706, 1762, 561, 1730, 4119, 4831, 1684, 4001, 1232, 794, 3548, 2316, 765, 1309, 1671, 2616, 1978, 1261, 2111, 570, 23, 210, 2020, 2259, 1078, 1930, 160, 1885, 3150, 511, 144, 4710, 3274, 4083, 4744, 2621, 615, 2967, 1974, 4789, 1044, 672, 283, 3747, 2104, 163, 2959, 1853, 1649, 4748, 1999, 3346, 1339, 2609, 4163, 4669, 4514, 4991, 4621, 1751, 2345, 2265, 3182, 628, 2759, 3863, 2615, 1767, 2935, 1311, 800, 3241, 3998, 3740, 1481, 66, 2898, 4816, 1176, 1462, 2096, 1065, 302, 59, 63, 4819, 2476, 2387, 2607, 3751, 3089, 2947, 4332, 2265, 3890, 2041, 67, 90, 3051, 3009, 4818, 3734, 3008, 1228, 2036, 2741, 3020, 481, 2609, 1641, 4509, 3847, 3822, 1501, 2265, 3800, 2127, 3822, 270, 3206, 927, 90, 3112, 3427, 2096, 1855, 3829, 4611, 1058, 870, 3113, 1028, 798, 4890, 2958, 4104, 2574, 4894, 2454, 4271, 4728, 1253, 1230, 359, 1095, 1297, 2153, 1004, 383, 2781, 2733, 2319, 3984, 3581, 337, 1103, 1911, 1253, 4525, 527, 3394, 1785, 2506, 1539, 1449, 3729, 2402, 1064, 3705, 4892, 4521, 2836, 3505, 3806, 4918, 459, 1495, 3599, 2139, 3809, 1373, 3871, 1919, 3702, 3097, 1211, 1887, 3487, 1286, 3467, 3142, 2636, 1319, 4275, 3138, 756, 4254, 1752, 1608, 4401, 829, 57, 4105, 4566, 135, 4106, 846, 4560, 4673, 4659, 52, 3188, 4244, 4053, 815, 2640, 112, 2280, 3000, 1838, 3639, 2581, 4828, 218, 374, 4038, 1241, 3499, 2158, 3540, 1951, 4771, 131, 3189, 1341, 4741, 4690, 3817, 1552, 2086, 2351, 1451, 1861, 1639, 836, 881, 1385, 14, 1481, 3476, 4453, 3713, 4419, 1995, 4432, 3933, 2167, 734, 1848, 1040, 225, 1488, 1707, 2, 1347, 1341, 2519, 87, 88, 3706, 1880, 2256, 33, 2289, 2942, 3924, 1321, 21, 3769, 3912, 3953, 3425, 4693, 163, 2861, 4181, 1918, 3838, 4871, 2532, 2235, 226, 2219, 4503, 4273, 3782, 4358, 1951, 4361, 2410, 4951, 4377, 2912, 2063, 260, 4937, 1140, 603, 1292, 1330, 3068, 4365, 363, 424, 2708, 291, 3102, 3857, 4466, 3747, 4476, 1544, 293, 1013, 387, 1142, 3991, 2793, 4335, 2122, 289, 3397, 1722, 961, 2172, 4921, 2191, 2301, 3566, 4118, 1498, 1035, 2410, 3879, 2613, 675, 348, 4728, 1150, 1723, 4122, 236, 3486, 3306, 3026, 1049, 2766, 582, 1034, 1514, 840, 2317, 4649, 3800, 4575, 2363, 2059, 2226, 2550, 109, 3517, 2474, 3835, 4790, 4665, 3450, 3113, 2911, 1274, 2086, 4997, 4145, 3349, 1284, 3109, 4474, 2385, 3716, 3594, 3228, 786, 1578, 4239, 2035, 2381, 2447, 4433, 4161, 749, 455, 1320, 343, 1227, 3263, 3308, 1739, 910, 2148, 3094, 4190, 3752, 2966, 3170, 100, 2063, 4427, 3092, 3553, 2156, 4876, 2690, 4148, 4794, 2999, 921, 4758, 3498, 119, 4431, 1373, 2311, 3320, 3801, 2102, 888, 1800, 4792, 4403, 794, 664, 1703, 4190, 1673, 4827, 3331, 4359, 3843, 2091, 4935, 1691, 4164, 3736, 1067, 1412, 2192, 2441, 993, 2410, 3892, 4322, 1768, 2554, 1517, 198, 585, 18, 2312, 3513, 4572, 253, 3077, 4655, 2776, 1869, 719, 4484, 318, 2119, 3315, 1825, 4639, 1419, 3410, 4547, 3713, 2801, 4241, 1079, 2440, 4985, 4035, 285, 1024, 488, 3046, 2216, 1782, 4521, 204, 71, 4435, 4202, 214, 3166, 1885, 3467, 2997, 433, 3705, 230, 2486, 2652, 3802, 2552, 801, 4835, 4055, 1252, 3653, 3586, 1079, 2474, 1473, 3170, 2868, 872, 3832, 4600, 1873, 2454, 1024, 2988, 4640, 3016, 2003, 3122, 434, 3207, 4173, 3078, 4822, 1787, 3109, 4302, 4286, 1542, 4455, 3286, 2360, 3340, 1605, 3196, 2927, 63, 165, 1659, 252, 828, 3844, 3134, 2907, 3043, 4389, 3915, 2876, 1581, 1384, 2097, 3746, 4788, 4533, 1998, 3945, 1152, 449, 4479, 2896, 2604, 330, 3460, 4743, 2939, 3668, 4762, 4924, 3843, 3198, 138, 2740, 4389, 4432, 1226, 4685, 1914, 2818, 3514, 2613, 1389, 4971, 523, 190, 3365, 1581, 2881, 1496, 4997, 530, 243, 4823, 2188, 2995, 3679, 615, 1558, 3394, 3649, 102, 2823, 849, 4905, 4605, 3137, 2944, 3796, 4154, 1945, 1734, 2830, 2496, 2410, 741, 150, 3602, 1605, 3330, 4904, 3214, 4181, 2082, 3408, 3305, 4255, 2608, 2121, 1180, 3495, 42, 2012, 3516, 1143, 4904, 3505, 4714, 3037, 255, 3984, 1279, 2083, 3934, 4372, 2172, 4196, 1207, 339, 2467, 2205, 4089, 3789, 3887, 3402, 3881, 2546, 2132, 4635, 3569, 3082, 1323, 4763, 824, 628, 4253, 1791, 3744, 3302, 3074, 3350, 1994, 3086, 260, 1098, 2633, 799, 4808, 4419, 334, 3884, 2767, 3681, 4939, 465, 1791, 4214, 148, 1674, 3905, 4072, 4337, 717, 726, 3989, 2816, 625, 4911, 1463, 2830, 4210, 4909, 2550, 2155, 1581, 993, 3728, 4693, 370, 4989, 223, 4136, 1160, 4339, 2693, 3075, 1071, 4358, 2647, 3148, 4649, 4513, 1265, 3939, 3646, 4659, 2926, 1088, 4221, 2628, 744, 4574, 4347, 4239, 2228, 2234, 2897, 1140, 3273, 2629, 616, 1422, 1445, 2548, 104, 2711, 2339, 3861, 826, 2622, 3041, 1195, 1602, 4566, 4780, 1304, 2718, 3972, 32, 4140, 3108, 341, 4285, 1365, 2246, 821, 3118, 3870, 1290, 1407, 3457, 1086, 2415, 2634, 4370, 3196, 181, 4192, 6, 1960, 4244, 1957, 595, 1684, 3267, 4396, 1526, 1579, 2437, 1509, 400, 2432, 1074, 2092, 2441, 229, 1606, 1822, 2384, 1510, 4921, 3583, 3862, 4081, 1113, 2367, 105, 4072, 1064, 2072, 2797, 3705, 3351, 1444, 4913, 4757, 4559, 757, 4167, 198, 3736, 4580, 4124, 1111, 4575, 4135, 1857, 3280, 1078, 4269, 423, 3107, 3773, 905, 3074, 2804, 2551, 2572, 3859, 2437, 1328, 1338, 3276, 2349, 4462, 1007, 1819, 3847, 4720, 3077, 2085, 38, 68, 4117, 1411, 2495, 98, 3819, 2619, 1544, 2908, 2811, 1240, 2075, 4228, 518, 806, 3722, 4286, 3039, 3989, 4695, 3899, 4683, 4528, 594, 4761, 752, 800, 4070, 63, 3577, 4086, 1886, 1367, 3256, 99, 1024, 762, 4021, 581, 1744, 2024, 4842, 3200, 3255, 2777, 829, 2524, 2587, 2354, 1506, 224, 1677, 88, 3306, 4582, 470, 2997, 2632, 3862, 2743, 2692, 4571, 2414, 1815, 654, 4387, 2367, 4666, 1640, 3251, 2550, 442, 1440, 1138, 3562, 2363, 3573, 131, 2508, 2276, 991, 1340, 686, 3266, 1538, 491, 2199, 2729, 2843, 249, 4163, 3714, 377, 4539, 4381, 1678, 1871, 1856, 1086, 2347, 1811, 4552, 2802, 3865, 4272, 4960, 2091, 2482, 3890, 1233, 2742, 134, 3301, 2754, 4374, 4176, 2805, 218, 495, 3130, 2889, 1972, 2668, 2436, 839, 2317, 941, 1230, 1309, 4118, 1921, 1556, 3664, 1844, 596, 1791, 3706, 1949, 1658, 4501, 2779, 4255, 2787, 3714, 3309, 3478, 860, 1894, 2324, 660, 3633, 2079, 659, 186, 909, 1000, 317, 392, 3779, 2710, 1609, 4514, 1410, 3321, 1232, 218, 1649, 1314, 324, 2661, 396, 736, 4007, 2544, 3026, 3382, 3923, 4223, 4553, 3045, 945, 1459, 4189, 1608, 1122, 574, 920, 2039, 4058, 2464, 4074, 4093, 310, 1682, 2503, 2960, 566, 4729, 1480, 3270, 357, 58, 3374, 3036, 3333, 821, 3462, 3443, 1415, 3967, 3793, 745, 3359, 894, 4308, 2100, 4701, 2670, 3667, 2736, 2835, 1660, 2755, 368, 2968, 1872, 4315, 2504, 1681, 3688, 878, 415, 3089, 1860, 2191, 4922, 818, 525, 1943, 4214, 1308, 1401, 4893, 2459, 1176, 2365, 2987, 980, 4515, 2718, 4421, 1216, 2459, 1992, 4982, 3582, 1969, 1372, 4283, 4824, 4142, 820, 2895, 2601, 3261, 4060, 2493, 3268, 1184, 1618, 766, 3678, 135, 3016, 3763, 438, 2127, 2270, 1254, 4129, 84, 3175, 298, 2473, 2766, 1040, 244, 242, 2339, 1984, 3900, 4950, 261, 4625, 2174, 4011, 418, 4636, 391, 4405, 2464, 3734, 1385, 2628, 3979, 1463, 19, 1316, 4524, 453, 3443, 93, 1798, 1555, 2133, 2591, 1094, 3512, 331, 1497, 3906, 4080, 3643, 2296, 4069, 3662, 3288, 2495, 1404, 2890, 4265, 1739, 740, 3281, 1477, 2981, 4261, 3177, 447, 2231, 2435, 2379, 42, 1903, 3718, 513, 1444, 779, 708, 3737, 2397, 1841, 4847, 2250, 4312, 4115, 4319, 4457, 1244, 2044, 4594, 1958, 258, 2414, 1308, 31, 4846, 4539, 324, 1167, 4073, 1362, 3209, 4703, 1255, 575, 354, 1951, 3452, 831, 3948, 3464, 4311, 4653, 1987, 4624, 953, 993, 949, 4178, 1296, 3863, 2705, 2066, 2702, 313, 4355, 351, 1407, 2772, 1991, 2640, 968, 3672, 4233, 187, 2687, 73, 406, 513, 1384, 309, 4239, 4542, 2524, 3196, 1950, 4627, 3253, 641, 2394, 842, 2808, 4831, 4647, 381, 3671, 667, 424, 571, 619, 4815, 3068, 2690, 3984, 2479, 1268, 2128, 3331, 3825, 4595, 807, 376, 1757, 1131, 883, 3861, 4361, 4950, 4270, 1602, 3162, 1793, 2187, 2150, 1776, 4769, 2580, 3677, 2980, 1320, 3573, 4496, 4839, 4202, 2313, 2041, 1227, 3071, 4110, 532, 3861, 388, 3687, 2102, 2869, 3600, 1447, 1272, 4372, 3851, 1850, 4928, 3808, 4074, 3003, 1224, 541, 3323, 3370, 4420, 1760, 161, 3133, 4654, 3154, 1179, 2089, 74, 1399, 3331, 3450, 3683, 3254, 1017, 1386, 4010, 81, 4215, 4881, 3132, 4283, 2980, 109, 3784, 2042, 2240, 2868, 124, 3021, 8, 4578, 863, 265, 3412, 3501, 928, 836, 175, 3229, 1366, 1720, 4179, 3854, 2632, 2663, 2900, 3858, 2694, 4116, 2485, 1481, 1779, 2539, 943, 807, 1496, 3670, 4705, 1700, 592, 1933, 180, 4716, 3236, 1332, 1359, 858, 2155, 4587, 4327, 3897, 4718, 3906, 4900, 3178, 2603, 2137, 1478, 4453, 4142, 3359, 2497, 4972, 1854, 1929, 1969, 1325, 3503, 176, 3398, 1740, 2567, 147, 391, 3762, 2557, 4957, 4638, 251, 2897, 3165, 507, 2062, 2904, 4156, 3717, 3223, 3143, 4701, 1789, 3205, 2688, 973, 3495, 2638, 3003, 4371, 4457, 4481, 4546, 505, 1432, 1171, 4641, 1859, 2234, 3384, 1719, 2932, 3087, 4153, 1596, 224, 1568, 4192, 3708, 4949, 2739, 149, 106, 3739, 3182, 3682, 51, 2409, 3891, 1890, 1948, 3705, 784, 354, 456, 1989, 2885, 4830, 2674, 4783, 2717, 335, 3495, 3425, 4217, 444, 790, 1912, 4043, 645, 2713, 4688, 4774, 887, 126, 4560, 4218, 1059, 1674, 4238, 2718, 162, 1814, 892, 4297, 4253, 4500, 996, 1227, 4801, 2862, 1723, 3398, 3443, 54, 1655, 2016, 3079, 3412, 2399, 479, 4144, 36, 3119, 2269, 3960, 1801, 2726, 766, 3105, 3795, 540, 423, 2656, 3552, 1708, 722, 4275, 347, 1232, 1901, 4341, 1360, 1045, 4670, 1906, 4109, 3952, 4078, 865, 3438, 2860, 1607, 186, 514, 3656, 2520, 1680, 4414, 4614, 4117, 1804, 1858, 3613, 4863, 3363, 3576, 1367, 897, 1931, 949, 4701, 4601, 764, 3517, 2338, 1573, 3554, 4393, 4552, 2438, 4181, 551, 2115, 890, 362, 3771, 2195, 1757, 4247, 4787, 557, 4465, 4134, 2704, 2656, 4228, 3758, 1960, 3087, 1970, 4052, 2723, 3321, 2185, 1069, 3802, 3492, 3652, 2566, 2710, 3449, 2227, 308, 1014, 889, 1740, 3667, 1020, 4190, 1359, 658, 3422, 4067, 2280, 659, 4816, 314, 268, 1878, 3742, 1651, 4025, 4844, 1491, 3329, 3340, 2742, 3028, 1760, 4123, 2537, 1878, 3331, 449, 3430, 4031, 1012, 1691, 3324, 2859, 3554, 3745, 1434, 1345, 3053, 1983, 1202, 2791, 4137, 95, 991, 165, 1751, 3568, 2485, 475, 3831, 3175, 4882, 3159, 629, 1965, 1199, 1217, 2538, 916, 1150, 1663, 1094, 690, 3561, 4980, 1555, 997, 563, 4943, 4839, 3615, 4441, 3288, 1634, 4320, 4618, 2952, 1346, 40, 1219, 2474, 2853, 4762, 591, 972, 2563, 3383, 1522, 1051, 3833, 3491, 844, 2622, 4282, 2878, 4411, 3556, 3263, 3429, 1426, 2763, 4631, 4687, 630, 752, 1823, 1016, 4114, 3155, 3524, 1857, 735, 538, 226, 3958, 488, 3794, 730, 1467, 4327, 4494, 200, 622, 3035, 4786, 2592, 2724, 4860, 797, 500, 3508, 1012, 1764, 166, 4178, 3422, 1995, 4290, 3630, 3825, 3454, 3225, 4548, 3445, 2567, 4834, 1215, 3884, 368, 4876, 4356, 2063, 4032, 1, 496, 3074, 4871, 4703, 3560, 954, 932, 82, 3628, 475, 3915, 2700, 2268, 1458, 4327, 3365, 2557, 1170, 3284, 541, 4590, 3276, 326, 1375, 1816, 4114, 3069, 3223, 4634, 476, 1470, 1538, 1626, 1624, 468, 2987, 3251, 3093, 562, 2870, 2010, 2097, 3147, 4738, 3792, 358, 1994, 845, 2554, 425, 3420, 4979, 3131, 1802, 919, 1532, 770, 4811, 4025, 1869, 663, 3909, 2544, 4643, 1998, 64, 4990, 3615, 4085, 2125, 929, 1949, 2028, 4337, 1932, 2690, 666, 1799, 584, 4602, 1444, 4007, 1063, 2459, 2105, 1475, 2220, 4257, 3128, 1276, 990, 659, 385, 1143, 1444, 2553, 673, 3783, 812, 4135, 4291, 3535, 3788, 2736, 3405, 249, 1081, 519, 2699, 4818, 2472, 4746, 1991, 3982, 4715, 2762, 3868, 1793, 3363, 1995, 1746, 3892, 1378, 1755, 3272, 2598, 1958, 295, 3169, 3193, 3302, 1191, 2548, 421, 4620, 3079, 3804, 106, 1722, 1460, 404, 476, 4923, 66, 4327, 1201, 2284, 1167, 4200, 4949, 1302, 3315, 992, 4312, 4835, 4916, 3600, 549, 2133, 55, 4078, 3062, 3633, 58, 839, 1100, 3388, 3694, 3007, 1129, 4679, 3745, 4746, 164, 199, 1528, 1098, 117, 3426, 3895, 3880, 1581, 773, 2260, 2081, 4563, 255, 3112, 1793, 561, 2161, 4728, 4928, 846, 1077, 4671, 4348, 413, 1459, 1781, 3051, 3057, 847, 331, 1660, 123, 2945, 1965, 980, 4853, 294, 1271, 3068, 398, 3272, 1251, 3595, 916, 2532, 2991, 667, 2537, 3977, 2978, 4634, 1645, 3381, 1397, 3087, 3116, 1105, 3427, 4799, 142, 3490, 4602, 349, 1257, 488, 3091, 2258, 223, 1505, 1881, 3444, 3403, 3327, 3586, 1202, 1847, 3116, 1297, 2285, 4287, 185, 756, 4939, 4802, 3685, 4646, 1123, 3666, 177, 886, 1148, 4627, 4200, 4617, 1645, 3818, 4556, 991, 1434, 2577, 3177, 3926, 3110, 2353, 4258, 4261, 3154, 1348, 1450, 1876, 1634, 362, 3839, 3622, 1818, 2566, 4680, 4471, 164, 1339, 3770, 4233, 2296, 1857, 4293, 4710, 1771, 1600, 974, 2342, 1280, 1839, 3068, 1679, 3845, 2653, 3048, 1621, 3282, 1169, 4805, 2175, 3851, 127, 1207, 4414, 1793, 3907, 751, 194, 263, 3127, 3731, 72, 9, 1444, 2884, 3639, 4444, 3021, 4549, 899, 3216, 3344, 2609, 2306, 2164, 3121, 3213, 2375, 2128, 1299, 271, 4975, 2541, 3553, 671, 384, 510, 954, 4247, 2670, 2771, 104, 2552, 1029, 4799, 4048, 559, 4702, 956, 4229, 91, 2149, 3302, 2684, 1920, 523, 1831, 1877, 4523, 2958, 2739, 2160, 1828, 2677, 1586, 2180, 3892, 1595, 4260, 4008, 4264, 3943, 260, 1322, 502, 2729, 4002, 1481, 119, 4338, 3146, 109, 1414, 3901, 369, 1938, 2660, 1172, 1938, 2091, 3088, 4504, 2311, 3943, 2109, 4803, 3092, 2853, 411, 2672, 2485, 1833, 757, 554, 4912, 1642, 948, 1975, 2065, 1620, 4827, 4028, 1823, 3776, 2694, 2293, 395, 1612, 4950, 4952, 4982, 2592, 1489, 3656, 3068, 27, 4957, 2073, 1346, 4753, 2286, 3224, 2146, 370, 292, 1182, 1551, 402, 4870, 1063, 1387, 2063, 4629, 1840, 196, 4332, 2496, 1830, 540, 147, 219, 2457, 923, 755, 102, 4733, 3289, 587, 810, 2088, 3113, 493, 934, 1008, 3836, 2827, 4764, 3095, 1329, 2356, 3619, 784, 135, 2980, 1004, 4722, 4568, 4471, 4187, 418, 492, 4838, 3731, 2442, 3410, 2295, 2516, 2269, 3263, 4575, 3195, 3540, 1482, 1021, 3777, 795, 2002, 889, 742, 3831, 1438, 1073, 3934, 4554, 4865, 4144, 3396, 151, 2695, 4436, 268, 4554, 71, 1438, 1712, 4603, 2831, 4261, 4472, 3234, 1275, 2251, 650, 3407, 640, 3140, 1497, 3230, 4831, 1356, 2183, 4958, 930, 2185, 2578, 935, 3865, 4846, 772, 3225, 3672, 2593, 4589, 1976, 4259, 2439, 2921, 1338, 918, 560, 39, 2312, 2105, 972, 4567, 2384, 3560, 2990, 3194, 1741, 70, 700, 181, 2999, 4095, 1642, 2307, 4035, 3351, 4676, 2809, 734, 1191, 366, 1547, 3419, 167, 1214, 846, 1906, 385, 2202, 1586, 3467, 4389, 3776, 913, 4760, 1132, 3334, 3423, 4948, 2638, 1082, 2689, 2262, 4684, 2967, 292, 2070, 1059, 3508, 1061, 2189, 4530, 852, 26, 230, 2667, 1263, 3651, 1058, 65, 1693, 1243, 2844, 164, 2989, 47, 3180, 2770, 3490, 4822, 3884, 306, 4574, 407, 4669, 1784, 1739, 4751, 2073, 2740, 1275, 309, 2310, 2928, 4132, 2214, 1662, 3128, 3402, 1406, 2927, 2638, 1320, 1043, 4526, 3883, 4345, 4257, 4263, 1941, 2991, 178, 2520, 4227, 4068, 4446, 3634, 4106, 709, 1094, 3058, 2650, 3687, 1007, 4837, 3630, 4999, 4642, 2827, 514, 4300, 4267, 4884, 1737, 3614, 4249, 4112, 3486, 3399, 1395, 434, 934, 4978, 834, 337, 4459, 4410, 4627, 4741, 2955, 525, 4883, 745, 2826, 1815, 4838, 3190, 576, 3952, 4733, 1932, 1552, 1315, 4412, 1262, 720, 3781, 333, 2992, 4261, 2539, 3548, 3285, 4621, 3589, 1792, 1567, 4942, 2038, 3497, 817, 4322, 1091, 669, 478, 2614, 3234, 3338, 4085, 4434, 4249, 2693, 4579, 2833, 3803, 2236, 2225, 2300, 2049, 4447, 3511, 164, 4849, 4528, 1869, 3741, 650, 4041, 1411, 2382, 2527, 980, 4243, 2574, 806, 3111, 596, 2960, 1062, 4292, 3821, 844, 2694, 2771, 2098, 2435, 953, 2770, 2318, 2151, 3816, 3643, 1536, 2839, 1855, 1914, 946, 2291, 1254, 996, 4041, 3904, 1800, 3345, 4649, 1061, 4659, 3117, 820, 702, 4122, 3727, 3869, 644, 2258, 2887, 4471, 4674, 930, 1719, 3349, 3833, 932, 2914, 3198, 1554, 3698, 698, 4175, 4580, 3245, 3055, 506, 672, 1129, 4028, 4448, 259, 1064, 251, 322, 1724, 3707, 1008, 1877, 3575, 4494, 1063, 324, 2663, 3856, 905, 2144, 273, 3906, 2790, 35, 3765, 653, 818, 1217, 2378, 478, 4928, 2288, 3180, 4372, 1209, 4738, 4479, 489, 2036, 1747, 1776, 775, 2309, 4608, 3041, 1572, 2923, 1624, 1904, 1878, 1153, 657, 2232, 1928, 2125, 4822, 3504, 2042, 3558, 1349, 2498, 3717, 4525, 878, 4357, 1777, 1400, 3111, 2179, 4243, 2495, 1070, 1284, 2498, 812, 875, 120, 1573, 4038, 4583, 3193, 990, 2522, 3127, 2089, 2588, 1721, 4945, 1615, 3699, 2088, 1677, 1336, 3719, 3240, 2663, 4439, 316, 3843, 211, 3767, 912, 1474, 1076, 3243, 1738, 2191, 2317, 1416, 3401, 3514, 1486, 3791, 3712, 1107, 553, 1749, 2090, 2271, 3948, 2175, 4712, 1112, 3250, 941, 174, 2299, 4849, 1354, 4217, 861, 1801, 3084, 2189, 2196, 3448, 291, 4205, 988, 20, 1797, 4161, 1446, 2347, 1631, 1869, 2225, 3698, 881, 4104, 3866, 1177, 3083, 3243, 3160, 2665, 301, 863, 3440, 2124, 3151, 831, 1182, 1310, 1925, 2635, 2144, 3511, 1623, 3511, 786, 547, 1236, 473, 3844, 4262, 3349, 4740, 914, 4747, 100, 3617, 2622, 2432, 3499, 878, 3988, 1013, 1274, 1138, 4549, 4706, 471, 1226, 3226, 1382, 2749, 2729, 4438, 4536, 2133, 2659, 1375, 4493, 3596, 3082, 389, 1844, 4697, 4435, 4498, 4205, 4008, 3247, 4822, 1925, 3081, 4331, 1949, 521, 165, 666, 1981, 1980, 2183, 4274, 2398, 4060, 4838, 4394, 1179, 1679, 892, 2016, 2755, 4796, 110, 262, 1784, 4096, 4436, 2345, 3308, 3379, 3340, 3521, 2237, 2716, 4266, 4849, 3304, 4198, 4169, 2162, 1477, 4679, 3473, 4447, 2162, 3269, 112, 3120, 2655, 1283, 1330, 2021, 956, 2805, 1420, 3571, 4038, 4978, 1300, 1382, 1498, 3894, 4421, 3644, 4219, 4360, 4149, 3703, 2979, 1164, 1062, 3182, 2467, 1171, 3386, 4093, 413, 1018, 594, 431, 2187, 1125, 3272, 2160, 2524, 852, 4586, 235, 1869, 3164, 1247, 2657, 3220, 987, 3450, 4669, 2302, 2424, 151, 2865, 4729, 467, 4848, 4615, 358, 3704, 96, 2700, 3132, 3339, 3814, 878, 3951, 438, 4946, 304, 510, 1716, 2395, 1228, 205, 771, 159, 1731, 3135, 337, 4796, 837, 2151, 4102, 2255, 1565, 1569, 3787, 1576, 3981, 2628, 977, 4656, 4462, 4611, 2206, 1161, 2176, 1960, 2826, 4590, 4274, 1810, 2464, 401, 4024, 4718, 3173, 594, 998, 173, 3970, 2133, 3136, 676, 2902, 4838, 2490, 2527, 2959, 2199, 1220, 3090, 4171, 4283, 356, 4457, 3367, 4602, 1701, 742, 3512, 1025, 1595, 3822, 2846, 2748, 3979, 55, 3928, 2625, 1847, 878, 1265, 3248, 2972, 1382, 2627, 501, 3660, 4987, 1422, 2924, 1489, 2617, 332, 2473, 1332, 2125, 1856, 162, 3498, 3358, 901, 4243, 1572, 1127, 3609, 101, 388, 562, 2954, 4718, 1740, 1752, 2855, 233, 4226, 703, 2157, 2189, 2702, 1913, 1796, 3212, 2868, 3, 4135, 6, 3131, 4076, 3000, 3267, 4033, 4283, 4413, 1612, 1598, 814, 4767, 3588, 4134, 812, 75, 274, 2758, 4981, 2129, 166, 316, 3207, 4044, 990, 761, 2557, 4785, 2666, 3211, 1450, 2491, 3354, 2307, 4251, 1666, 1891, 2128, 2908, 2424, 932, 3336, 2358, 2619, 4375, 1566, 3974, 1252, 208, 1113, 1375, 775, 2988, 3462, 1933, 1084, 195, 3280, 3403, 1887, 1201, 3470, 340, 408, 4421, 1349, 1462, 214, 4664, 3537, 999, 3935, 2784, 4605, 4685, 4997, 2768, 3118, 4448, 149, 2098, 4658, 502, 848, 1500, 839, 1724, 1693, 726, 4083, 2417, 729, 4957, 4462, 1526, 1563, 1821, 2752, 2644, 2651, 2221, 3398, 4854, 3489, 3867, 1874, 841, 4196, 2898, 406, 686, 3933, 309, 2411, 4309, 2112, 992, 2599, 4529, 162, 4964, 1844, 4043, 3360, 3152, 955, 1017, 3123, 3215, 2094, 3990, 3928, 1791, 4937, 1441, 1575, 3987, 1097, 1609, 2744, 1788, 1043, 2857, 4790, 4576, 3446, 1875, 3908, 632, 4073, 2373, 121, 1119, 3010, 2231, 3096, 107, 1532, 2660, 4567, 1968, 360, 1513, 1163, 1824, 3306, 647, 4026, 2527, 1229, 3744, 2654, 4739, 1102, 2273, 2030, 678, 807, 1883, 1832, 990, 3932, 3028, 4979, 4394, 1268, 1495, 3185, 2222, 4631, 2697, 3274, 830, 793, 3555, 1413, 1492, 3851, 1866, 160, 1876, 643, 751, 3935, 1541, 4925, 2153, 2199, 1537, 3004, 1673, 67, 4282, 1271, 2518, 2838, 4185, 1997, 3521, 1158, 4580, 992, 1333, 1875, 1095, 3117, 2949, 3977, 3454, 2374, 647, 4472, 2984, 4135, 975, 2823, 1647, 1940, 4738, 4994, 1122, 1555, 4691, 2245, 1866, 559, 2654, 2372, 4669, 4605, 2805, 1619, 710, 493, 1276, 4894, 2097, 1095, 2317, 4049, 880, 3946, 4718, 4297, 4675, 772, 1739, 4487, 887, 4400, 4850, 2525, 3973, 4477, 4386, 1976, 1224, 61, 4361, 2142, 1072, 1509, 3363, 4686, 4980, 1119, 2198, 4649, 2176, 2632, 1511, 2741, 963, 4106, 277, 1392, 4883, 1832, 3582, 1666, 4077, 923, 4194, 3347, 2715, 1040, 1536, 4398, 3671, 1171, 3112, 4614, 4127, 3931, 2154, 1013, 2118, 2905, 3381, 2708, 1824, 4663, 2046, 4050, 4469, 2854, 3189, 2219, 311, 1342, 1016, 3268, 3199, 4290, 2054, 4721, 3731, 1863, 3915, 167, 2858, 1282, 2194, 207, 4876, 2158, 2550, 4346, 1561, 484, 4693, 990, 292, 108, 3642, 4805, 3187, 1735, 2153, 1635, 3389, 1996, 2972, 1346, 1840, 1078, 4579, 2380, 3432, 3520, 3753, 337, 2241, 1855, 3131, 1664, 4101, 2494, 226, 4762, 4693, 3234, 4902, 4568, 1028, 4699, 3843, 1625, 2466, 4104, 3594, 3337, 3385, 1291, 4438, 4423, 1992, 1791, 2821, 3753, 655, 3957, 1968, 526, 3353, 2597, 319, 1526, 4431, 4005, 4483, 3931, 2261, 1482, 4166, 3766, 4781, 3837, 2427, 3191, 3570, 4371, 835, 1140, 3825, 4037, 1887, 2322, 4062, 2241, 2146, 4876, 3243, 2612, 2238, 868, 2068, 491, 318, 1766, 2344, 4593, 4782, 1979, 741, 1806, 2487, 1559, 786, 2490, 2426, 1283, 1273, 4533, 425, 1835, 2637, 3558, 3779, 4042, 3441, 1145, 3464, 3016, 3842, 2694, 2186, 3463, 1338, 2665, 4181, 4470, 3116, 237, 2591, 1410, 2789, 2569, 1932, 3113, 1056, 4001, 4245, 3045, 4149, 3832, 1265, 1778, 3439, 2459, 3689, 4343, 3544, 1697, 1851, 2458, 602, 4854, 4615, 393, 2983, 2317, 4002, 4779, 4811, 1011, 3785, 4316, 3773, 1206, 3143, 369, 1279, 125, 1226, 2734, 3250, 1361, 1861, 4843, 165, 2071, 481, 4000, 2951, 386, 2630, 2756, 4787, 1611, 834, 1403, 1885, 4023, 4858, 1855, 3558, 3518, 954, 1082, 4203, 4471, 4000, 1307, 4117, 4659, 133, 3108, 170, 1828, 309, 1306, 4511, 802, 1989, 2885, 963, 2067, 117, 1722, 42, 991, 3567, 92, 3713, 3642, 1615, 1382, 1671, 3405, 1214, 4327, 711, 4431, 4022, 2412, 2662, 3072, 205, 4801, 1627, 471, 3998, 1206, 4986, 2488, 2592, 796, 1106, 2416, 1381, 3482, 4934, 4815, 2591, 4230, 4864, 2147, 1276, 638, 908, 4099, 3389, 3227, 3225, 2523, 4155, 2561, 1119, 3121, 1107, 3292, 4595, 3722, 4094, 2940, 3275, 2145, 2387, 2970, 4838, 1168, 2872, 1692, 3758, 2106, 177, 4222, 4839, 2646, 3693, 82, 4746, 4165, 4527, 4888, 1796, 1303, 4119, 3320, 4121, 201, 2531, 1083, 473, 703, 1460, 3572, 257, 712, 3842, 240, 1675, 582, 3830, 4888, 1474, 4197, 3479, 4115, 832, 211, 1625, 1575, 2064, 3373, 3614, 39, 939, 2082, 1612, 3535, 4061, 1989, 1259, 352, 4924, 1773, 1664, 2298, 4226, 1870, 677, 2944, 199, 617, 73, 1510, 2363, 3986, 1349, 1098, 4238, 1225, 4968, 1385, 3293, 4877, 857, 3858, 2685, 1190, 2704, 2483, 2762, 1741, 1757, 271, 113, 3828, 1752, 2585, 4261, 4636, 1846, 1451, 2239, 588, 3487, 3247, 4210, 2888, 2568, 3132, 2108, 2508, 4085, 4607, 4250, 4782, 508, 4165, 2377, 3984, 4536, 4926, 4243, 1564, 3399, 3245, 1595, 3302, 553, 2894, 1627, 4951, 2918, 1539, 3392, 459, 1744, 2789, 4006, 802, 2676, 3453, 3567, 1894, 194, 547, 2089, 668, 2960, 4436, 4010, 4914, 3710, 545, 4904, 1751, 4096, 4690, 1681, 2229, 1388, 3669, 4937, 3877, 170, 3122, 1497, 4621, 332, 569, 1715, 731, 3518, 4744, 1193, 1675, 1450, 1518, 188, 1583, 3528, 2902, 2026, 4299, 3239, 1907, 4297, 4358, 2379, 942, 4936, 875, 1513, 4606, 4594, 93, 634, 3094, 2415, 3922, 4782, 4773, 3527, 3401, 4575, 162, 4145, 1205, 1575, 858, 4197, 1253, 2795, 3166, 1173, 2042, 1683, 4203, 1182, 1609, 2657, 831, 2754, 2555, 2318, 35, 4454, 4421, 2513, 2723, 42, 3766, 1407, 3735, 1982, 4364, 1406, 399, 4302, 3794, 3583, 1908, 2162, 4748, 1266, 3419, 1798, 295, 1689, 2889, 3434, 1833, 80, 66, 4148, 4218, 1948, 4487, 1659, 2052, 1944, 2412, 370, 4522, 35, 3828, 908, 4486, 3774, 3744, 3881, 1243, 888, 3988, 3764, 2684, 4748, 4339, 4136, 3617, 168, 4414, 1843, 1174, 2252, 605, 1362, 2529, 2093, 2104, 4318, 1346, 729, 4894, 2509, 3770, 4261, 2537, 1867, 2972, 173, 4284, 2787, 3314, 2938, 823, 1428, 3463, 3945, 4054, 451, 3530, 238, 4863, 1582, 3684, 3695, 1506, 3919, 1105, 2821, 4287, 2495, 1547, 4449, 4616, 3082, 1503, 1573, 4914, 418, 2076, 4059, 2331, 3495, 2047, 4484, 1759, 2065, 339, 3559, 2465, 1131, 4314, 1535, 3902, 4616, 4482, 1663, 1647, 4679, 3806, 874, 835, 2890, 4538, 2554, 3027, 364, 1163, 1520, 498, 39, 2191, 1748, 1444, 2270, 1817, 3534, 412, 1087, 3491, 4339, 2273, 3717, 2996, 345, 1417, 735, 3731, 527, 3741, 3739, 2976, 1511, 4757, 3544, 2792, 4910, 1308, 3028, 3101, 3400, 2336, 4293, 3461, 3243, 986, 2340, 2095, 4531, 2157, 1980, 1578, 3018, 1856, 2886, 3884, 3685, 2584, 3556, 4529, 4872, 183, 2467, 12, 2462, 4658, 906, 1944, 4557, 1184, 2759, 2521, 734, 498, 3038, 957, 3763, 540, 2129, 1039, 1534, 717, 1894, 2119, 1155, 4317, 2448, 2516, 4032, 4393, 3331, 3212, 3713, 2840, 4612, 3776, 4309, 4626, 4082, 1501, 3168, 3213, 205, 1476, 4030, 2432, 2507, 4294, 3473, 3289, 3747, 532, 2950, 3881, 4856, 2458, 2646, 3253, 2718, 1973, 4660, 997, 2223, 1591, 597, 3421, 3584, 2946, 3202, 3192, 3133, 2665, 4747, 2637, 400, 635, 920, 3463, 2909, 1739, 1634, 655, 2284, 4281, 1899, 644, 2738, 1264, 1240, 2217, 2145, 1312, 2114, 4407, 3144, 266, 1935, 3758, 2389, 4865, 3650, 3217, 3837, 1074, 2131, 194, 413, 4626, 4091, 445, 1918, 4161, 1990, 2158, 2689, 4431, 3923, 2806, 4595, 3285, 404, 2971, 1440, 3587, 2620, 2623, 1631, 694, 3604, 3311, 2657, 3605, 4308, 118, 1284, 3820, 89, 3984, 3935, 2376, 633, 1113, 3293, 4858, 3798, 2931, 3859, 693, 4920, 4922, 2075, 1852, 4430, 1567, 3905, 4834, 11, 1951, 3168, 1720, 2367, 131, 1560, 2566, 1022, 2145, 3048, 3305, 1857, 323, 1503, 2677, 2065, 2045, 3638, 4019, 1054, 2359, 1416, 2848, 1579, 4857, 1591, 3278, 4818, 323, 1188, 3150, 804, 2056, 778, 1257, 3886, 2213, 4728, 4908, 2362, 4918, 4525, 2759, 4495, 4218, 458, 2174, 1110, 3421, 2766, 4787, 2677, 3059, 4663, 1867, 4965, 4146, 4981, 3292, 281, 847, 4020, 3009, 1770, 3787, 4981, 4269, 3511, 3871, 3720, 4726, 3534, 3008, 1360, 1516, 305, 2292, 1121, 2822, 4712, 3612, 4700, 2425, 3494, 3841, 2117, 3894, 2728, 4274, 3508, 2361, 8, 286, 2528, 4989, 1434, 388, 1427, 259, 319, 2580, 4506, 2501, 2426, 2527, 2780, 2707, 3999, 608, 920, 247, 1756, 1062, 1227, 2812, 72, 2224, 4719, 151, 488, 4136, 1235, 1688, 4479, 3779, 2252, 3751, 2145, 679, 3486, 4584, 2476, 2337, 4906, 3525, 4648, 2630, 4370, 1363, 4070, 2833, 455, 936, 2677, 3350, 56, 1655, 1113, 956, 1679, 2170, 961, 555, 1332, 3495, 3136, 429, 3776, 3972, 1432, 1612, 1925, 4161, 4817, 999, 2842, 3170, 1979, 4662, 2473, 2190, 3551, 892, 4894, 1989, 3243, 841, 2604, 3506, 3874, 3169, 3269, 907, 3965, 3975, 1568, 149, 1983, 512, 1343, 265, 3290, 1276, 2191, 4768, 3049, 1269, 907, 4981, 1179, 2282, 3472, 3199, 4993, 34, 935, 4432, 866, 2624, 3280, 2972, 2373, 2465, 2729, 3030, 4870, 346, 4683, 4684, 2862, 214, 1867, 2367, 3841, 3805, 2590, 1394, 4983, 3653, 3210, 2787, 4258, 226, 2966, 4553, 2810, 2463, 1655, 135, 4135, 496, 4851, 2147, 1371, 656, 4500, 3348, 4658, 1189, 430, 2047, 4550, 946, 4233, 1789, 1023, 1137, 2788, 686, 1425, 4380, 3652, 616, 4357, 1950, 2032, 3237, 3181, 600, 1742, 3142, 1922, 1174, 4722, 995, 708, 2599, 2411, 1271, 2319, 2183, 2934, 2169, 3938, 4112, 1702, 3579, 2048, 2244, 2433, 2083, 1356, 3535, 3887, 4565, 986, 1128, 1769, 885, 3688, 47, 764, 3549, 2944, 419, 3862, 4456, 780, 3155, 820, 113, 2931, 161, 1053, 4023, 839, 4964, 3644, 2282, 2809, 1500, 3956, 39, 1012, 39, 1115, 3989, 4061, 2230, 88, 142, 558, 2636, 1947, 4496, 1353, 4774, 4410, 1308, 4198, 2513, 1307, 319, 2903, 2263, 1228, 2420, 4303, 2409, 4173, 38, 3673, 4514, 1388, 4751, 3979, 2259, 2762, 1436, 1261, 3772, 1977, 3808, 1138, 2524, 29, 163, 4859, 4811, 4014, 811, 3539, 4707, 4343, 4823, 2346, 808, 4856, 4826, 3150, 3367, 2758, 4405, 3445, 4537, 1178, 1399, 653, 850, 879, 4301, 493, 270, 4962, 2027, 3759, 2352, 1685, 1440, 3800, 4155, 2576, 3444, 3497, 4751, 3798, 4768, 3533, 2852, 1074, 4492, 1106, 3830, 2164, 2818, 2355, 3372, 3948, 2124, 4496, 1158, 1356, 2984, 4503, 4760, 378, 3519, 3352, 2034, 3751, 907, 2375, 371, 2888, 4148, 3848, 4634, 2428, 3861, 51, 3850, 1565, 4764, 4032, 3755, 4494, 437, 2320, 1433, 483, 1814, 40, 4420, 1624, 4983, 840, 3421, 2121, 3348, 2138, 3257, 346, 1151, 1293, 4574, 1528, 4791, 4425, 2534, 3838, 1920, 988, 4391, 4614, 2179, 1064, 258, 1641, 295, 1840, 648, 3297, 3246, 365, 2637, 1973, 2299, 2275, 450, 732, 1978, 1917, 4963, 4983, 1142, 4622, 3520, 3775, 709, 1198, 4471, 1623, 4149, 4097, 1951, 4374, 353, 919, 1560, 1151, 2303, 4775, 4631, 3859, 2405, 1743, 1358, 62, 4245, 3164, 855, 1539, 2789, 3891, 1415, 167, 359, 4220, 967, 4400, 4319, 616, 4810, 498, 905, 2228, 1440, 4435, 3932, 445, 2083, 1446, 3374, 1545, 1172, 208, 3211, 3324, 2807, 1148, 2776, 3348, 4362, 4829, 3358, 1170, 3575, 3143, 1021, 425, 3608, 1696, 348, 3289, 4177, 4265, 3649, 1681, 4944, 15, 2954, 1336, 3720, 1235, 4285, 2919, 2079, 1270, 896, 2266, 4140, 1282, 2752, 1746, 3303, 650, 4929, 4309, 4299, 4038, 3849, 3540, 2521, 1894, 1461, 289, 2547, 2387, 3306, 4451, 2927, 1629, 2122, 3308, 1885, 2058, 4899, 2216, 2282, 4456, 1571, 4408, 1936, 1714, 4794, 1090, 3569, 3628, 3718, 2630, 2194, 3432, 2989, 1653, 4948, 1833, 1485, 3063, 2749, 3572, 2932, 3693, 791, 4106, 1547, 1899, 2078, 4004, 997, 1127, 4039, 3612, 1250, 1603, 554, 3259, 2916, 1666, 886, 1501, 2, 4618, 3790, 4967, 226, 4069, 572, 153, 4984, 1645, 2133, 2527, 4056, 820, 3228, 1453, 4967, 1852, 2491, 473, 4737, 3235, 1736, 544, 4440, 4817, 229, 3678, 2664, 2923, 1745, 2594, 4986, 1316, 3746, 1220, 256, 2820, 4255, 2541, 2957, 3494, 3348, 1562, 3588, 3407, 4961, 1942, 2330, 390, 4414, 1050, 1870, 4987, 4089, 4044, 4936, 668, 867, 163, 1388, 224, 1804, 1634, 4886, 2195, 354, 4088, 638, 1201, 3339, 591, 336, 1712, 3150, 373, 2497, 2301, 2790, 3429, 581, 279, 1252, 3013, 892, 4297, 350, 1007, 718, 754, 83, 4910, 2797, 2695, 4482, 1354, 2908, 3269, 4320, 1434, 4843, 2871, 1004, 2937, 716, 4889, 3723, 2711, 2036, 3178, 4883, 1232, 2831, 2713, 1683, 3679, 4171, 163, 4179, 248, 2311, 3073, 4816, 920, 4321, 428, 772, 2173, 3775, 2058, 4258, 1990, 4001, 113, 2382, 275, 4487, 250, 915, 1956, 2633, 1355, 1335, 2000, 4616, 3293, 1868, 2313, 2819, 1472, 3889, 2068, 2934, 3493, 2496, 426, 4566, 1270, 2525, 4575, 79, 1237, 1345, 3934, 3878, 767, 1758, 92, 2779, 1477, 1946, 4517, 2611, 3170, 3664, 2063, 1621, 2547, 4544, 1785, 1067, 2806, 358, 3330, 1879, 1104, 4455, 2749, 2586, 2044, 1468, 1197, 1462, 1804, 4810, 2392, 4072, 4525, 1714, 2456, 2999, 2637, 3402, 2677, 1472, 28, 4215, 901, 4847, 3361, 957, 422, 2909, 3156, 4340, 4808, 1330, 814, 1865, 4992, 71, 1156, 809, 748, 4408, 2089, 3036, 1519, 4454, 927, 3219, 1040, 3893, 1885, 3149, 3264, 4296, 3112, 679, 3044, 3377, 3751, 937, 2884, 2021, 2260, 2404, 2261, 1956, 767, 688, 29, 541, 4799, 1140, 2154, 3141, 3011, 1629, 3420, 3252, 2898, 3964, 295, 813, 2125, 4404, 2848, 4179, 2038, 3387, 2224, 4298, 3329, 2071, 374, 397, 4497, 2453, 4182, 845, 2364, 3961, 3064, 571, 3397, 0, 1519, 1111, 2265, 4056, 4935, 515, 4177, 3122, 621, 1926, 4031, 3481, 3541, 3503, 3745, 3352, 819, 1914, 1735, 2560, 4055, 2457, 4481, 1594, 187, 4112, 3442, 1982, 3553, 2463, 651, 1372, 429, 957, 1888, 3971, 2717, 4187, 1789, 1502, 482, 4344, 3468, 560, 2461, 1089, 545, 2995, 2131, 2926, 2763, 3386, 1313, 3739, 3192, 2447, 2509, 4256, 1826, 749, 66, 3634, 2750, 1486, 552, 2545, 1190, 1186, 4306, 3405, 557, 2027, 334, 4257, 2336, 2131, 1485, 2871, 1998, 2954, 2520, 3123, 1071, 409, 1393, 3007, 2556, 2943, 2859, 1576, 3214, 986, 3061, 1034, 1402, 2827, 2470, 433, 4428, 3208, 2523, 4429, 1608, 2169, 210, 1259, 4269, 1252, 2234, 2302, 453, 2372, 712, 3341, 4843, 4430, 4678, 460, 1200, 1205, 652, 2789, 2561, 2351, 4917, 4411, 2449, 1840, 3768, 1313, 2248, 3771, 2169, 147, 3447, 307, 2202, 779, 4048, 2711, 4845, 1521, 22, 2198, 4842, 3352, 396, 502, 2650, 249, 4859, 229, 1790, 2019, 1465, 3294, 881, 1856, 4847, 3438, 2794, 2262, 3305, 2930, 2377, 4462, 3267, 2471, 1559, 2817, 4006, 544, 2133, 3678, 4468, 2613, 125, 1688, 802, 2138, 4216, 824, 2087, 2294, 4597, 4898, 3428, 4975, 1611, 230, 1969, 1029, 2469, 180, 2087, 1986, 1006, 3634, 1792, 2875, 3237, 4997, 3243, 3228, 1026, 1986, 2391, 851, 2148, 3528, 3380, 430, 3931, 2980, 538, 4861, 2089, 2918, 1275, 521, 3005, 247, 596, 742, 4939, 4792, 1910, 2692, 4939, 3993, 4600, 1359, 3049, 258, 4340, 134, 4356, 1855, 941, 1991, 816, 2048, 2310, 3591, 565, 3768, 843, 4882, 145, 4554, 3390, 730, 4974, 770, 2214, 4250, 1991, 3719, 2010, 3257, 37, 2859, 207, 2098, 2732, 1526, 1955, 4494, 50, 2596, 4023, 4362, 1432, 4662, 908, 4015, 1186, 2205, 850, 4232, 4398, 4301, 4131, 2079, 70, 2394, 36, 4099, 4862, 416, 333, 2110, 4521, 4375, 1575, 4040, 11, 1921, 1870, 2515, 954, 2861, 1949, 4871, 2673, 1955, 60, 1286, 4746, 652, 1039, 1280, 1953, 4870, 4213, 3476, 3236, 2736, 4708, 3641, 4065, 4166, 4631, 2351, 3225, 1176, 1381, 761, 3130, 829, 172, 1383, 2965, 4141, 2499, 2996, 497, 919, 2427, 2863, 3881, 3415, 78, 3621, 2088, 1158, 3266, 1063, 373, 4707, 3063, 3330, 4471, 3490, 4771, 2405, 4767, 4238, 2347, 1562, 1652, 1398, 585, 4340, 2743, 2106, 4104, 4312, 3647, 4381, 3313, 3412, 3094, 4438, 2922, 1021, 460, 2087, 474, 1785, 1524, 2137, 2609, 3273, 4233, 3665, 3929, 1543, 3541, 1571, 4174, 519, 3437, 567, 365, 914, 2454, 2770, 2351, 3449, 3862, 4216, 4239, 4176, 859, 3093, 2349, 1880, 4325, 571, 2705, 69, 3423, 964, 495, 670, 1636, 683, 2069, 3722, 3663, 3406, 2820, 3060, 2089, 4488, 3537, 3797, 545, 1630, 867, 1431, 1738, 1417, 2298, 1045, 2784, 3567, 4523, 3292, 3447, 1405, 3079, 4700, 4765, 3183, 1554, 3964, 3207, 2531, 4635, 1826, 3022, 2615, 1587, 3588, 580, 4300, 2073, 1130, 441, 4808, 3306, 1271, 1984, 3431, 3340, 4609, 3312, 1152, 126, 4432, 929, 4753, 4415, 1069, 4690, 151, 1570, 4687, 3921, 1088, 4495, 2772, 4136, 364, 1785, 993, 53, 3313, 2254, 3461, 1672, 2404, 750, 2329, 2080, 1246, 4389, 496, 4979, 4857, 1775, 1395, 4240, 1461, 548, 4143, 4838, 3895, 905, 3160, 4999, 3143, 3866, 4893, 1162, 3741, 4494, 3379, 3486, 4976, 186, 2319, 498, 1484, 507, 1115, 457, 522, 2798, 2302, 1840, 2680, 645, 3681, 4372, 763, 1329, 4656, 1053, 3541, 1970, 3015, 1769, 1583, 4294, 2256, 954, 4986, 170, 2396, 1159, 2097, 2879, 691, 3792, 2679, 2337, 4008, 4024, 2040, 674, 1576, 2195, 1869, 1305, 2217, 1920, 138, 678, 389, 2212, 3152, 2915, 910, 2080, 3932, 3131, 3463, 4182, 2464, 4623, 1512, 3670, 1576, 629, 2441, 3551, 2706, 1384, 3257, 3610, 450, 2672, 4573, 491, 1551, 2888, 4370, 1851, 3631, 4041, 2654, 547, 2370, 3045, 1859, 3539, 1678, 447, 3445, 1439, 2830, 52, 1538, 1087, 2482, 3930, 2730, 3721, 568, 2780, 1866, 4338, 3893, 3481, 3785, 2259, 3194, 3049, 757, 1410, 1853, 4921, 724, 933, 1741, 3725, 1014, 4558, 89, 2767, 1976, 7, 1106, 2946, 2611, 2427, 1417, 2633, 672, 3009, 3052, 246, 2770, 1555, 2970, 1219, 4624, 4866, 4050, 1156, 714, 1624, 3336, 106, 1880, 3602, 4814, 2410, 443, 2940, 3424, 1866, 800, 1709, 2111, 8, 4567, 2563, 4921, 1971, 1908, 2382, 1711, 156, 3642, 1647, 731, 1162, 670, 3015, 3797, 2287, 2824, 1810, 4874, 3193, 2147, 4534, 986, 3036, 3361, 718, 3988, 2868, 2546, 824, 3978, 461, 2254, 4122, 1912, 975, 2792, 4521, 2417, 2535, 2887, 3957, 4788, 2786, 1153, 4063, 4730, 4928, 710, 2962, 1655, 4930, 3521, 4446, 502, 2153, 3243, 1790, 1428, 612, 3122, 2221, 2197, 3706, 4323, 683, 3727, 1981, 2015, 1909, 2309, 1989, 2804, 4681, 4623, 4195, 793, 554, 2391, 3740, 4788, 1780, 1323, 4580, 3128, 4572, 3814, 3690, 1731, 3313, 4011, 1808, 3393, 4002, 4597, 4360, 3521, 3948, 3414, 2037, 2810, 2827, 414, 1693, 4452, 607, 1941, 1545, 4395, 2801, 361, 4890, 3384, 416, 1513, 4639, 4473, 408, 2792, 973, 2955, 4419, 2791, 3878, 2366, 3294, 4735, 2267, 2037, 3963, 4018, 3158, 2251, 3131, 3504, 2564, 4003, 1679, 4630, 4559, 4176, 4996, 1132, 4537, 3583, 1253, 3959, 2245, 1240, 2784, 1615, 3763, 1503, 2245, 3850, 2035, 4988, 4438, 331, 1098, 2410, 1825, 628, 2288, 4928, 3246, 2323, 2317, 208, 2919, 1601, 535, 4812, 2513, 237, 3210, 1499, 3687, 3860, 1544, 4041, 1117, 2719, 2566, 2396, 3423, 2634, 2852, 1705, 2397, 47, 3799, 4277, 4163, 2372, 1916, 4412, 1156, 1246, 3699, 274, 4598, 1915, 3560, 2542, 4802, 2711, 288, 2135, 2222, 4492, 1912, 418, 1525, 3298, 4993, 1517, 3873, 1551, 1017, 877, 1033, 4471, 1051, 2454, 2534, 3151, 2857, 3097, 1103, 2999, 301, 1686, 1630, 4862, 1205, 4509, 716, 1849, 4338, 2240, 3436, 4917, 3512, 140, 929, 1837, 895, 2651, 1741, 899, 1251, 2871, 2901, 311, 135, 3715, 2427, 3976, 3413, 4922, 681, 1902, 1165, 2705, 3843, 257, 2506, 3428, 2198, 3593, 3198, 3165, 4914, 1244, 4812, 3753, 4839, 1799, 3321, 2504, 4618, 1570, 3208, 4683, 913, 2327, 4528, 4977, 1198, 1293, 882, 3453, 2564, 868, 2377, 2605, 427, 4532, 690, 420, 351, 762, 1550, 1420, 1888, 781, 1387, 86, 4336, 3461, 2029, 646, 2054, 3051, 624, 2102, 3223, 2826, 4280, 4159, 3647, 1045, 4363, 249, 4335, 984, 3938, 1417, 4787, 322, 3957, 3248, 1716, 1511, 4461, 2471, 29, 2761, 2856, 4090, 1525, 4004, 4057, 3538, 532, 1660, 4643, 4727, 1019, 4004, 687, 2561, 508, 3710, 4317, 4252, 4302, 3978, 2162, 964, 4709, 138, 329, 3695, 4640, 4031, 979, 3000, 3530, 1749, 4177, 2045, 4409, 3337, 1626, 2157, 4432, 3492, 3724, 3454, 223, 3050, 763, 561, 3706, 16, 2696, 1942, 923, 2839, 3625, 4465, 826, 2600, 2185, 874, 705, 397, 1547, 1931, 2375, 3224, 3358, 2990, 281, 645, 3657, 2011, 4842, 613, 2969, 1522, 595, 3562, 1649, 4093, 36, 2965, 941, 982, 818, 1946, 4686, 193, 3532, 2196, 1307, 3647, 3659, 2587, 3753, 1905, 3026, 4681, 1064, 1173, 4899, 54, 1089, 4753, 3587, 2032, 4250, 2825, 2782, 4400, 1069, 1266, 2841, 1165, 1574, 4457, 1335, 4009, 3184, 4577, 4475, 1612, 2796, 4026, 3230, 4443, 4228, 4210, 4307, 2594, 3293, 392, 3372, 3962, 2974, 1685, 1525, 3057, 682, 3347, 227, 1317, 650, 662, 2210, 2519, 2920, 1537, 1757, 3865, 1708, 2828, 2206, 4637, 1738, 3913, 2077, 4022, 4002, 2020, 1313, 1330, 3297, 190, 3917, 2569, 1790, 87, 541, 4180, 1084, 3309, 19, 3481, 3867, 4834, 3051, 3926, 1749, 2750, 2389, 1886, 1503, 3984, 3310, 2409, 370, 3936, 2, 4733, 4643, 614, 2558, 2561, 3213, 3240, 19, 3879, 605, 2754, 4734, 1117, 438, 2304, 3501, 1161, 1420, 2764, 1040, 1072, 572, 1442, 4810, 1816, 2683, 4483, 4045, 3914, 2172, 1204, 703, 4033, 1542, 712, 1358, 3385, 2334, 4282, 1084, 3512, 4293, 4376, 4244, 2877, 1599, 4911, 4581, 619, 3648, 2722, 2128, 4923, 865, 2676, 2936, 4262, 2347, 3463, 921, 4151, 2028, 2242, 697, 4879, 2686, 4313, 1591, 1740, 3793, 1932, 1351, 2407, 1705, 3041, 2220, 4904, 422, 4289, 2284, 4027, 1866, 1504, 2850, 718, 799, 186, 322, 1135, 1050, 3627, 2676, 3726, 1378, 4792, 4121, 325, 2088, 270, 4065, 1142, 3611, 4756, 2756, 1919, 601, 1944, 4218, 4731, 2085, 2518, 3104, 4032, 4426, 1614, 615, 274, 4382, 4577, 3982, 3239, 2463, 1345, 348, 1800, 43, 4798, 1503, 2533, 3155, 4915, 131, 4853, 2765, 2811, 1853, 2868, 3054, 2418, 4778, 736, 4262, 812, 2555, 4076, 3617, 4734, 532, 2110, 3532, 2943, 1395, 4100, 76, 4297, 4664, 29, 3270, 4632, 4192, 4640, 3706, 1014, 21, 4050, 3054, 1842, 1054, 570, 4259, 429, 1728, 4847, 596, 1370, 2800, 1058, 3259, 4215, 4230, 63, 2031, 3646, 3146, 1795, 4796, 218, 1452, 4021, 117, 2418, 4389, 1142, 4998, 750, 3101, 2362, 227, 753, 721, 4065, 2148, 3628, 2712, 2374, 4797, 1474, 3976, 3615, 513, 3783, 4926, 1847, 4635, 4495, 2176, 2734, 3619, 4239, 3065, 4135, 1444, 4518, 4709, 4039, 1972, 25, 704, 2535, 3024, 660, 142, 3563, 1197, 4732, 2689, 2861, 1851, 3809, 3385, 4099, 2285, 4892, 2210, 4698, 3616, 1456, 1443, 670, 3010, 1816, 1825, 2089, 1157, 413, 4957, 2539, 4394, 476, 1168, 2746, 763, 1483, 3370, 1478, 1974, 1178, 248, 562, 75, 2981, 178, 1382, 4542, 4427, 2014, 942, 408, 2177, 2005, 4939, 1048, 3729, 4481, 1159, 1380, 4377, 2239, 3447, 2724, 4739, 4201, 2932, 852, 4816, 965, 2719, 2505, 4029, 1572, 257, 4552, 4493, 3920, 2576, 2998, 4811, 1216, 3499, 106, 900, 3830, 4893, 2819, 1273, 4666, 991, 2994, 3400, 649, 4820, 167, 4248, 2546, 4791, 162, 3339, 4028, 321, 2922, 3603, 1778, 3395, 650, 1540, 3721, 4113, 3079, 2478, 291, 3551, 190, 113, 30, 4734, 467, 946, 3566, 2543, 879, 2636, 4392, 2695, 27, 1726, 4444, 3083, 4901, 1105, 1402, 322, 2147, 1886, 1775, 172, 873, 13, 4580, 2017, 3561, 3050, 1334, 4608, 4311, 1994, 236, 4235, 59, 4156, 3831, 933, 3495, 256, 4756, 3021, 757, 290, 970, 2614, 3228, 3925, 3884, 3925, 4312, 816, 2075] sort unsortedList - |> Quicksort.show - |> Task.putLine + |> Quicksort.show + |> Task.putLine + + Err GetIntError -> + Task.putLine "Error: Failed to get Integer from stdin." sort : List I64 -> List I64 sort = \list -> diff --git a/examples/benchmarks/RBTreeCk.roc b/crates/cli_testing_examples/benchmarks/RBTreeCk.roc similarity index 75% rename from examples/benchmarks/RBTreeCk.roc rename to crates/cli_testing_examples/benchmarks/RBTreeCk.roc index ccdd3a976c..5c685b195e 100644 --- a/examples/benchmarks/RBTreeCk.roc +++ b/crates/cli_testing_examples/benchmarks/RBTreeCk.roc @@ -1,5 +1,5 @@ app "rbtree-ck" - packages { pf: "platform" } + packages { pf: "platform/main.roc" } imports [pf.Task] provides [main] to pf @@ -18,8 +18,7 @@ makeMap = \freq, n -> makeMapHelp : I64, I64, Map, ConsList Map -> ConsList Map makeMapHelp = \freq, n, m, acc -> when n is - 0 -> - Cons m acc + 0 -> Cons m acc _ -> powerOf10 = n % 10 == 0 @@ -36,16 +35,15 @@ makeMapHelp = \freq, n, m, acc -> fold : (a, b, omega -> omega), Tree a b, omega -> omega fold = \f, tree, b -> when tree is - Leaf -> - b - Node _ l k v r -> - fold f r (f k v (fold f l b)) + Leaf -> b + Node _ l k v r -> fold f r (f k v (fold f l b)) main : Task.Task {} [] main = - Task.after - Task.getInt - \n -> + inputResult <- Task.attempt Task.getInt + + when inputResult is + Ok n -> # original koka n = 4_200_000 ms : ConsList Map ms = makeMap 5 n @@ -55,37 +53,36 @@ main = val = fold (\_, v, r -> if v then r + 1 else r) head 0 val - |> Num.toStr - |> Task.putLine + |> Num.toStr + |> Task.putLine + Nil -> Task.putLine "fail" + Err GetIntError -> + Task.putLine "Error: Failed to get Integer from stdin." + insert : Tree (Num k) v, Num k, v -> Tree (Num k) v insert = \t, k, v -> if isRed t then setBlack (ins t k v) else ins t k v setBlack : Tree a b -> Tree a b setBlack = \tree -> when tree is - Node _ l k v r -> - Node Black l k v r - _ -> - tree + Node _ l k v r -> Node Black l k v r + _ -> tree isRed : Tree a b -> Bool isRed = \tree -> when tree is - Node Red _ _ _ _ -> - True - _ -> - False + Node Red _ _ _ _ -> Bool.true + _ -> Bool.false lt = \x, y -> x < y ins : Tree (Num k) v, Num k, v -> Tree (Num k) v ins = \tree, kx, vx -> when tree is - Leaf -> - Node Red Leaf kx vx Leaf + Leaf -> Node Red Leaf kx vx Leaf Node Red a ky vy b -> if lt kx ky then Node Red (ins a kx vx) ky vy b @@ -93,42 +90,52 @@ ins = \tree, kx, vx -> Node Red a ky vy (ins b kx vx) else Node Red a ky vy (ins b kx vx) + Node Black a ky vy b -> if lt kx ky then - (if isRed a then balance1 (Node Black Leaf ky vy b) (ins a kx vx) else Node Black (ins a kx vx) ky vy b) + if isRed a then + balance1 (Node Black Leaf ky vy b) (ins a kx vx) + else + Node Black (ins a kx vx) ky vy b else if lt ky kx then - (if isRed b then balance2 (Node Black a ky vy Leaf) (ins b kx vx) else Node Black a ky vy (ins b kx vx)) + if isRed b then + balance2 (Node Black a ky vy Leaf) (ins b kx vx) + else + Node Black a ky vy (ins b kx vx) else Node Black a kx vx b balance1 : Tree a b, Tree a b -> Tree a b balance1 = \tree1, tree2 -> when tree1 is - Leaf -> - Leaf + Leaf -> Leaf Node _ _ kv vv t -> when tree2 is Node _ (Node Red l kx vx r1) ky vy r2 -> Node Red (Node Black l kx vx r1) ky vy (Node Black r2 kv vv t) + Node _ l1 ky vy (Node Red l2 kx vx r) -> Node Red (Node Black l1 ky vy l2) kx vx (Node Black r kv vv t) + Node _ l ky vy r -> Node Black (Node Red l ky vy r) kv vv t - Leaf -> - Leaf + + Leaf -> Leaf balance2 : Tree a b, Tree a b -> Tree a b balance2 = \tree1, tree2 -> when tree1 is - Leaf -> - Leaf + Leaf -> Leaf Node _ t kv vv _ -> when tree2 is Node _ (Node Red l kx1 vx1 r1) ky vy r2 -> Node Red (Node Black t kv vv l) kx1 vx1 (Node Black r1 ky vy r2) + Node _ l1 ky vy (Node Red l2 kx2 vx2 r2) -> Node Red (Node Black t kv vv l1) ky vy (Node Black l2 kx2 vx2 r2) + Node _ l ky vy r -> Node Black t kv vv (Node Red l ky vy r) + Leaf -> Leaf diff --git a/crates/cli_testing_examples/benchmarks/RBTreeDel.roc b/crates/cli_testing_examples/benchmarks/RBTreeDel.roc new file mode 100644 index 0000000000..7f5e940b73 --- /dev/null +++ b/crates/cli_testing_examples/benchmarks/RBTreeDel.roc @@ -0,0 +1,251 @@ +app "rbtree-del" + packages { pf: "platform/main.roc" } + imports [pf.Task] + provides [main] to pf + +Color : [Red, Black] + +Tree a b : [Leaf, Node Color (Tree a b) a b (Tree a b)] + +Map : Tree I64 Bool + +ConsList a : [Nil, Cons a (ConsList a)] + +main : Task.Task {} [] +main = + inputResult <- Task.attempt Task.getInt + + when inputResult is + Ok n -> + m = makeMap n # koka original n = 4_200_000 + val = fold (\_, v, r -> if v then r + 1 else r) m 0 + + val + |> Num.toStr + |> Task.putLine + + Err GetIntError -> + Task.putLine "Error: Failed to get Integer from stdin." + +boom : Str -> a +boom = \_ -> boom "" + +makeMap : I64 -> Map +makeMap = \n -> + makeMapHelp n n Leaf + +makeMapHelp : I64, I64, Map -> Map +makeMapHelp = \total, n, m -> + when n is + 0 -> m + _ -> + n1 = n - 1 + + powerOf10 = + n |> Num.isMultipleOf 10 + + t1 = insert m n powerOf10 + + isFrequency = + n |> Num.isMultipleOf 4 + + key = n1 + ((total - n1) // 5) + t2 = if isFrequency then delete t1 key else t1 + + makeMapHelp total n1 t2 + +fold : (a, b, omega -> omega), Tree a b, omega -> omega +fold = \f, tree, b -> + when tree is + Leaf -> b + Node _ l k v r -> fold f r (f k v (fold f l b)) + +depth : Tree * * -> I64 +depth = \tree -> + when tree is + Leaf -> 1 + Node _ l _ _ r -> 1 + depth l + depth r + +insert : Map, I64, Bool -> Map +insert = \t, k, v -> if isRed t then setBlack (ins t k v) else ins t k v + +setBlack : Tree a b -> Tree a b +setBlack = \tree -> + when tree is + Node _ l k v r -> Node Black l k v r + _ -> tree + +isRed : Tree a b -> Bool +isRed = \tree -> + when tree is + Node Red _ _ _ _ -> Bool.true + _ -> Bool.false + +ins : Tree I64 Bool, I64, Bool -> Tree I64 Bool +ins = \tree, kx, vx -> + when tree is + Leaf -> + Node Red Leaf kx vx Leaf + + Node Red a ky vy b -> + when Num.compare kx ky is + LT -> Node Red (ins a kx vx) ky vy b + GT -> Node Red a ky vy (ins b kx vx) + EQ -> Node Red a ky vy (ins b kx vx) + + Node Black a ky vy b -> + when Num.compare kx ky is + LT -> + when isRed a is + Bool.true -> balanceLeft (ins a kx vx) ky vy b + Bool.false -> Node Black (ins a kx vx) ky vy b + + GT -> + when isRed b is + Bool.true -> balanceRight a ky vy (ins b kx vx) + Bool.false -> Node Black a ky vy (ins b kx vx) + + EQ -> + Node Black a kx vx b + +balanceLeft : Tree a b, a, b, Tree a b -> Tree a b +balanceLeft = \l, k, v, r -> + when l is + Leaf -> + Leaf + + Node _ (Node Red lx kx vx rx) ky vy ry -> + Node Red (Node Black lx kx vx rx) ky vy (Node Black ry k v r) + + Node _ ly ky vy (Node Red lx kx vx rx) -> + Node Red (Node Black ly ky vy lx) kx vx (Node Black rx k v r) + + Node _ lx kx vx rx -> + Node Black (Node Red lx kx vx rx) k v r + +balanceRight : Tree a b, a, b, Tree a b -> Tree a b +balanceRight = \l, k, v, r -> + when r is + Leaf -> + Leaf + + Node _ (Node Red lx kx vx rx) ky vy ry -> + Node Red (Node Black l k v lx) kx vx (Node Black rx ky vy ry) + + Node _ lx kx vx (Node Red ly ky vy ry) -> + Node Red (Node Black l k v lx) kx vx (Node Black ly ky vy ry) + + Node _ lx kx vx rx -> + Node Black l k v (Node Red lx kx vx rx) + +isBlack : Color -> Bool +isBlack = \c -> + when c is + Black -> Bool.true + Red -> Bool.false + +Del a b : [Del (Tree a b) Bool] + +setRed : Map -> Map +setRed = \t -> + when t is + Node _ l k v r -> + Node Red l k v r + + _ -> + t + +makeBlack : Map -> Del I64 Bool +makeBlack = \t -> + when t is + Node Red l k v r -> + Del (Node Black l k v r) Bool.false + + _ -> + Del t Bool.true + +rebalanceLeft = \c, l, k, v, r -> + when l is + Node Black _ _ _ _ -> + Del (balanceLeft (setRed l) k v r) (isBlack c) + + Node Red lx kx vx rx -> + Del (Node Black lx kx vx (balanceLeft (setRed rx) k v r)) Bool.false + + _ -> + boom "unreachable" + +rebalanceRight = \c, l, k, v, r -> + when r is + Node Black _ _ _ _ -> + Del (balanceRight l k v (setRed r)) (isBlack c) + + Node Red lx kx vx rx -> + Del (Node Black (balanceRight l k v (setRed lx)) kx vx rx) Bool.false + + _ -> + boom "unreachable" + +delMin = \t -> + when t is + Node Black Leaf k v r -> + when r is + Leaf -> + Delmin (Del Leaf Bool.true) k v + + _ -> + Delmin (Del (setBlack r) Bool.false) k v + + Node Red Leaf k v r -> + Delmin (Del r Bool.false) k v + + Node c l k v r -> + when delMin l is + Delmin (Del lx Bool.true) kx vx -> + Delmin (rebalanceRight c lx k v r) kx vx + + Delmin (Del lx Bool.false) kx vx -> + Delmin (Del (Node c lx k v r) Bool.false) kx vx + + Leaf -> + Delmin (Del t Bool.false) 0 Bool.false + +delete : Tree I64 Bool, I64 -> Tree I64 Bool +delete = \t, k -> + when del t k is + Del tx _ -> + setBlack tx + +del : Tree I64 Bool, I64 -> Del I64 Bool +del = \t, k -> + when t is + Leaf -> + Del Leaf Bool.false + + Node cx lx kx vx rx -> + if (k < kx) then + when del lx k is + Del ly Bool.true -> + rebalanceRight cx ly kx vx rx + + Del ly Bool.false -> + Del (Node cx ly kx vx rx) Bool.false + else if (k > kx) then + when del rx k is + Del ry Bool.true -> + rebalanceLeft cx lx kx vx ry + + Del ry Bool.false -> + Del (Node cx lx kx vx ry) Bool.false + else + when rx is + Leaf -> + if isBlack cx then makeBlack lx else Del lx Bool.false + + Node _ _ _ _ _ -> + when delMin rx is + Delmin (Del ry Bool.true) ky vy -> + rebalanceLeft cx lx ky vy ry + + Delmin (Del ry Bool.false) ky vy -> + Del (Node cx lx ky vy ry) Bool.false diff --git a/examples/benchmarks/RBTreeInsert.roc b/crates/cli_testing_examples/benchmarks/RBTreeInsert.roc similarity index 83% rename from examples/benchmarks/RBTreeInsert.roc rename to crates/cli_testing_examples/benchmarks/RBTreeInsert.roc index 529fe18bb0..73e7b18209 100644 --- a/examples/benchmarks/RBTreeInsert.roc +++ b/crates/cli_testing_examples/benchmarks/RBTreeInsert.roc @@ -1,5 +1,5 @@ app "rbtree-insert" - packages { pf: "platform" } + packages { pf: "platform/main.roc" } imports [pf.Task] provides [main] to pf @@ -9,8 +9,8 @@ main = tree = insert 0 {} Empty tree - |> show - |> Task.putLine + |> show + |> Task.putLine show : RedBlackTree I64 {} -> Str show = \tree -> showRBTree tree Num.toStr (\{} -> "{}") @@ -18,8 +18,7 @@ show = \tree -> showRBTree tree Num.toStr (\{} -> "{}") showRBTree : RedBlackTree k v, (k -> Str), (v -> Str) -> Str showRBTree = \tree, showKey, showValue -> when tree is - Empty -> - "Empty" + Empty -> "Empty" Node color key value left right -> sColor = showColor color sKey = showKey key @@ -34,6 +33,7 @@ nodeInParens = \tree, showKey, showValue -> when tree is Empty -> showRBTree tree showKey showValue + Node _ _ _ _ _ -> inner = showRBTree tree showKey showValue @@ -42,10 +42,8 @@ nodeInParens = \tree, showKey, showValue -> showColor : NodeColor -> Str showColor = \color -> when color is - Red -> - "Red" - Black -> - "Black" + Red -> "Red" + Black -> "Black" NodeColor : [Red, Black] @@ -56,10 +54,8 @@ Key k : Num k insert : Key k, v, RedBlackTree (Key k) v -> RedBlackTree (Key k) v insert = \key, value, dict -> when insertHelp key value dict is - Node Red k v l r -> - Node Black k v l r - x -> - x + Node Red k v l r -> Node Black k v l r + x -> x insertHelp : Key k, v, RedBlackTree (Key k) v -> RedBlackTree (Key k) v insertHelp = \key, value, dict -> @@ -68,14 +64,12 @@ insertHelp = \key, value, dict -> # New nodes are always red. If it violates the rules, it will be fixed # when balancing. Node Red key value Empty Empty + Node nColor nKey nValue nLeft nRight -> when Num.compare key nKey is - LT -> - balance nColor nKey nValue (insertHelp key value nLeft) nRight - EQ -> - Node nColor nKey value nLeft nRight - GT -> - balance nColor nKey nValue nLeft (insertHelp key value nRight) + LT -> balance nColor nKey nValue (insertHelp key value nLeft) nRight + EQ -> Node nColor nKey value nLeft nRight + GT -> balance nColor nKey nValue nLeft (insertHelp key value nRight) balance : NodeColor, k, v, RedBlackTree k v, RedBlackTree k v -> RedBlackTree k v balance = \color, key, value, left, right -> @@ -89,8 +83,10 @@ balance = \color, key, value, left, right -> value (Node Black lK lV lLeft lRight) (Node Black rK rV rLeft rRight) + _ -> Node color rK rV (Node Red key value left rLeft) rRight + _ -> when left is Node Red lK lV (Node Red llK llV llLeft llRight) lRight -> @@ -100,5 +96,6 @@ balance = \color, key, value, left, right -> lV (Node Black llK llV llLeft llRight) (Node Black key value lRight right) + _ -> Node color key value left right diff --git a/crates/cli_testing_examples/benchmarks/TestAStar.roc b/crates/cli_testing_examples/benchmarks/TestAStar.roc new file mode 100644 index 0000000000..cd4eaf846d --- /dev/null +++ b/crates/cli_testing_examples/benchmarks/TestAStar.roc @@ -0,0 +1,46 @@ +app "test-astar" + packages { pf: "platform/main.roc" } + imports [pf.Task, AStar] + provides [main] to pf + +main : Task.Task {} [] +main = + Task.putLine (showBool test1) + +# Task.after Task.getInt \n -> +# when n is +# 1 -> +# Task.putLine (showBool test1) +# +# _ -> +# ns = Num.toStr n +# Task.putLine "No test \(ns)" +showBool : Bool -> Str +showBool = \b -> + if + b + then + "True" + else + "False" + +test1 : Bool +test1 = + example1 == [2, 4] + +example1 : List I64 +example1 = + step : I64 -> Set I64 + step = \n -> + when n is + 1 -> Set.fromList [2, 3] + 2 -> Set.fromList [4] + 3 -> Set.fromList [4] + _ -> Set.fromList [] + + cost : I64, I64 -> F64 + cost = \_, _ -> 1 + + when AStar.findPath cost step 1 4 is + Ok path -> path + Err _ -> [] diff --git a/crates/cli_testing_examples/benchmarks/TestBase64.roc b/crates/cli_testing_examples/benchmarks/TestBase64.roc new file mode 100644 index 0000000000..9916451731 --- /dev/null +++ b/crates/cli_testing_examples/benchmarks/TestBase64.roc @@ -0,0 +1,18 @@ +app "test-base64" + packages { pf: "platform/main.roc" } + imports [pf.Task, Base64] + provides [main] to pf + +IO a : Task.Task a [] + +main : IO {} +main = + when Base64.fromBytes (Str.toUtf8 "Hello World") is + Err _ -> Task.putLine "sadness" + Ok encoded -> + Task.after + (Task.putLine (Str.concat "encoded: " encoded)) + \_ -> + when Base64.toStr encoded is + Ok decoded -> Task.putLine (Str.concat "decoded: " decoded) + Err _ -> Task.putLine "sadness" diff --git a/examples/benchmarks/platform/Effect.roc b/crates/cli_testing_examples/benchmarks/platform/Effect.roc similarity index 100% rename from examples/benchmarks/platform/Effect.roc rename to crates/cli_testing_examples/benchmarks/platform/Effect.roc diff --git a/crates/cli_testing_examples/benchmarks/platform/Task.roc b/crates/cli_testing_examples/benchmarks/platform/Task.roc new file mode 100644 index 0000000000..16da2d1eb7 --- /dev/null +++ b/crates/cli_testing_examples/benchmarks/platform/Task.roc @@ -0,0 +1,88 @@ +interface Task + exposes [Task, succeed, fail, after, map, putLine, putInt, getInt, forever, loop, attempt] + imports [pf.Effect] + +Task ok err : Effect.Effect (Result ok err) + +forever : Task val err -> Task * err +forever = \task -> + looper = \{} -> + task + |> Effect.map + \res -> + when res is + Ok _ -> Step {} + Err e -> Done (Err e) + + Effect.loop {} looper + +loop : state, (state -> Task [Step state, Done done] err) -> Task done err +loop = \state, step -> + looper = \current -> + step current + |> Effect.map + \res -> + when res is + Ok (Step newState) -> Step newState + Ok (Done result) -> Done (Ok result) + Err e -> Done (Err e) + + Effect.loop state looper + +succeed : val -> Task val * +succeed = \val -> + Effect.always (Ok val) + +fail : err -> Task * err +fail = \val -> + Effect.always (Err val) + +after : Task a err, (a -> Task b err) -> Task b err +after = \effect, transform -> + Effect.after + effect + \result -> + when result is + Ok a -> transform a + Err err -> Task.fail err + +attempt : Task a b, (Result a b -> Task c d) -> Task c d +attempt = \task, transform -> + Effect.after + task + \result -> + when result is + Ok ok -> transform (Ok ok) + Err err -> transform (Err err) + +map : Task a err, (a -> b) -> Task b err +map = \effect, transform -> + Effect.map + effect + \result -> + when result is + Ok a -> Ok (transform a) + Err err -> Err err + +putLine : Str -> Task {} * +putLine = \line -> Effect.map (Effect.putLine line) (\_ -> Ok {}) + +putInt : I64 -> Task {} * +putInt = \line -> Effect.map (Effect.putInt line) (\_ -> Ok {}) + +getInt : Task I64 [GetIntError] +getInt = + Effect.after + Effect.getInt + \{ isError, value } -> + if + isError + then + # TODO + # when errorCode is + # # A -> Task.fail InvalidCharacter + # # B -> Task.fail IOError + # _ -> + Task.fail GetIntError + else + Task.succeed value diff --git a/crates/cli_testing_examples/benchmarks/platform/host.zig b/crates/cli_testing_examples/benchmarks/platform/host.zig new file mode 100644 index 0000000000..3362518568 --- /dev/null +++ b/crates/cli_testing_examples/benchmarks/platform/host.zig @@ -0,0 +1,243 @@ +const std = @import("std"); +const builtin = @import("builtin"); +const str = @import("glue").str; +const RocStr = str.RocStr; +const testing = std.testing; +const expectEqual = testing.expectEqual; +const expect = testing.expect; +const maxInt = std.math.maxInt; + +const mem = std.mem; +const Allocator = mem.Allocator; + +extern fn roc__mainForHost_1_exposed_generic([*]u8) void; +extern fn roc__mainForHost_1_exposed_size() i64; +extern fn roc__mainForHost_0_caller(*const u8, [*]u8, [*]u8) void; +extern fn roc__mainForHost_0_size() i64; +extern fn roc__mainForHost_0_result_size() i64; + +const Align = 2 * @alignOf(usize); +extern fn malloc(size: usize) callconv(.C) ?*align(Align) anyopaque; +extern fn realloc(c_ptr: [*]align(Align) u8, size: usize) callconv(.C) ?*anyopaque; +extern fn free(c_ptr: [*]align(Align) u8) callconv(.C) void; +extern fn memcpy(dst: [*]u8, src: [*]u8, size: usize) callconv(.C) void; +extern fn memset(dst: [*]u8, value: i32, size: usize) callconv(.C) void; + +const DEBUG: bool = false; + +export fn roc_alloc(size: usize, alignment: u32) callconv(.C) ?*anyopaque { + if (DEBUG) { + var ptr = malloc(size); + const stdout = std.io.getStdOut().writer(); + stdout.print("alloc: {d} (alignment {d}, size {d})\n", .{ ptr, alignment, size }) catch unreachable; + return ptr; + } else { + return malloc(size); + } +} + +export fn roc_realloc(c_ptr: *anyopaque, new_size: usize, old_size: usize, alignment: u32) callconv(.C) ?*anyopaque { + if (DEBUG) { + const stdout = std.io.getStdOut().writer(); + stdout.print("realloc: {d} (alignment {d}, old_size {d})\n", .{ c_ptr, alignment, old_size }) catch unreachable; + } + + return realloc(@as([*]align(Align) u8, @alignCast(@ptrCast(c_ptr))), new_size); +} + +export fn roc_dealloc(c_ptr: *anyopaque, alignment: u32) callconv(.C) void { + if (DEBUG) { + const stdout = std.io.getStdOut().writer(); + stdout.print("dealloc: {d} (alignment {d})\n", .{ c_ptr, alignment }) catch unreachable; + } + + free(@as([*]align(Align) u8, @alignCast(@ptrCast(c_ptr)))); +} + +export fn roc_panic(msg: *RocStr, tag_id: u32) callconv(.C) void { + const stderr = std.io.getStdErr().writer(); + switch (tag_id) { + 0 => { + stderr.print("Roc standard library crashed with message\n\n {s}\n\nShutting down\n", .{msg.asSlice()}) catch unreachable; + }, + 1 => { + stderr.print("Application crashed with message\n\n {s}\n\nShutting down\n", .{msg.asSlice()}) catch unreachable; + }, + else => unreachable, + } + std.process.exit(1); +} + +export fn roc_dbg(loc: *RocStr, msg: *RocStr) callconv(.C) void { + const stderr = std.io.getStdErr().writer(); + stderr.print("[{s}] {s}\n", .{ loc.asSlice(), msg.asSlice() }) catch unreachable; +} + +export fn roc_memset(dst: [*]u8, value: i32, size: usize) callconv(.C) void { + return memset(dst, value, size); +} + +extern fn kill(pid: c_int, sig: c_int) c_int; +extern fn shm_open(name: *const i8, oflag: c_int, mode: c_uint) c_int; +extern fn mmap(addr: ?*anyopaque, length: c_uint, prot: c_int, flags: c_int, fd: c_int, offset: c_uint) *anyopaque; +extern fn getppid() c_int; + +fn roc_getppid() callconv(.C) c_int { + return getppid(); +} + +fn roc_getppid_windows_stub() callconv(.C) c_int { + return 0; +} + +fn roc_shm_open(name: *const i8, oflag: c_int, mode: c_uint) callconv(.C) c_int { + return shm_open(name, oflag, mode); +} +fn roc_mmap(addr: ?*anyopaque, length: c_uint, prot: c_int, flags: c_int, fd: c_int, offset: c_uint) callconv(.C) *anyopaque { + return mmap(addr, length, prot, flags, fd, offset); +} + +comptime { + if (builtin.os.tag == .macos or builtin.os.tag == .linux) { + @export(roc_getppid, .{ .name = "roc_getppid", .linkage = .Strong }); + @export(roc_mmap, .{ .name = "roc_mmap", .linkage = .Strong }); + @export(roc_shm_open, .{ .name = "roc_shm_open", .linkage = .Strong }); + } + + if (builtin.os.tag == .windows) { + @export(roc_getppid_windows_stub, .{ .name = "roc_getppid", .linkage = .Strong }); + } +} + +const Unit = extern struct {}; + +pub fn main() !u8 { + const stderr = std.io.getStdErr().writer(); + + // The size might be zero; if so, make it at least 8 so that we don't have a nullptr + const size = @max(@as(usize, @intCast(roc__mainForHost_1_exposed_size())), 8); + const raw_output = roc_alloc(@as(usize, @intCast(size)), @alignOf(u64)).?; + var output = @as([*]u8, @ptrCast(raw_output)); + + defer { + roc_dealloc(raw_output, @alignOf(u64)); + } + + var timer = std.time.Timer.start() catch unreachable; + + roc__mainForHost_1_exposed_generic(output); + + const closure_data_pointer = @as([*]u8, @ptrCast(output)); + + call_the_closure(closure_data_pointer); + + const nanos = timer.read(); + const seconds = (@as(f64, @floatFromInt(nanos)) / 1_000_000_000.0); + + stderr.print("runtime: {d:.3}ms\n", .{seconds * 1000}) catch unreachable; + + return 0; +} + +fn to_seconds(tms: std.os.timespec) f64 { + return @as(f64, @floatFromInt(tms.tv_sec)) + (@as(f64, @floatFromInt(tms.tv_nsec)) / 1_000_000_000.0); +} + +fn call_the_closure(closure_data_pointer: [*]u8) void { + const allocator = std.heap.page_allocator; + + // The size might be zero; if so, make it at least 8 so that we don't have a nullptr + const size = @max(roc__mainForHost_0_result_size(), 8); + const raw_output = allocator.alignedAlloc(u8, @alignOf(u64), @as(usize, @intCast(size))) catch unreachable; + var output = @as([*]u8, @ptrCast(raw_output)); + + defer { + allocator.free(raw_output); + } + + const flags: u8 = 0; + + roc__mainForHost_0_caller(&flags, closure_data_pointer, output); + + // The closure returns result, nothing interesting to do with it + return; +} + +pub export fn roc_fx_putInt(int: i64) i64 { + const stdout = std.io.getStdOut().writer(); + + stdout.print("{d}", .{int}) catch unreachable; + + stdout.print("\n", .{}) catch unreachable; + + return 0; +} + +export fn roc_fx_putLine(rocPath: *str.RocStr) callconv(.C) void { + const stdout = std.io.getStdOut().writer(); + + for (rocPath.asSlice()) |char| { + stdout.print("{c}", .{char}) catch unreachable; + } + + stdout.print("\n", .{}) catch unreachable; +} + +const GetInt = extern struct { + value: i64, + is_error: bool, +}; + +comptime { + if (@sizeOf(usize) == 8) { + @export(roc_fx_getInt_64bit, .{ .name = "roc_fx_getInt" }); + } else { + @export(roc_fx_getInt_32bit, .{ .name = "roc_fx_getInt" }); + } +} + +fn roc_fx_getInt_64bit() callconv(.C) GetInt { + if (roc_fx_getInt_help()) |value| { + const get_int = GetInt{ .is_error = false, .value = value }; + return get_int; + } else |err| switch (err) { + error.InvalidCharacter => { + return GetInt{ .is_error = true, .value = 0 }; + }, + else => { + return GetInt{ .is_error = true, .value = 0 }; + }, + } + + return 0; +} + +fn roc_fx_getInt_32bit(output: *GetInt) callconv(.C) void { + if (roc_fx_getInt_help()) |value| { + const get_int = GetInt{ .is_error = false, .value = value }; + output.* = get_int; + } else |err| switch (err) { + error.InvalidCharacter => { + output.* = GetInt{ .is_error = true, .value = 0 }; + }, + else => { + output.* = GetInt{ .is_error = true, .value = 0 }; + }, + } + + return; +} + +fn roc_fx_getInt_help() !i64 { + const stdout = std.io.getStdOut().writer(); + stdout.print("Please enter an integer\n", .{}) catch unreachable; + + const stdin = std.io.getStdIn().reader(); + var buf: [40]u8 = undefined; + + // make sure to strip `\r` on windows + const raw_line: []u8 = (try stdin.readUntilDelimiterOrEof(&buf, '\n')) orelse ""; + const line = std.mem.trimRight(u8, raw_line, &std.ascii.whitespace); + + return std.fmt.parseInt(i64, line, 10); +} diff --git a/crates/cli_testing_examples/benchmarks/platform/main.roc b/crates/cli_testing_examples/benchmarks/platform/main.roc new file mode 100644 index 0000000000..e4802e2503 --- /dev/null +++ b/crates/cli_testing_examples/benchmarks/platform/main.roc @@ -0,0 +1,9 @@ +platform "benchmarks" + requires {} { main : Task {} [] } + exposes [] + packages {} + imports [Task.{ Task }] + provides [mainForHost] + +mainForHost : Task {} [] +mainForHost = main diff --git a/crates/cli_testing_examples/expects/expects.roc b/crates/cli_testing_examples/expects/expects.roc new file mode 100644 index 0000000000..c25b73bc68 --- /dev/null +++ b/crates/cli_testing_examples/expects/expects.roc @@ -0,0 +1,28 @@ +app "expects-test" + packages { pf: "zig-platform/main.roc" } + imports [] + provides [main] to pf + +expect + a = 1 + b = 2 + + a == b + +polyDbg = \x -> + dbg x + x + +main = + str = "this will for sure be a large string so when we split it it will use seamless slices which affect printing" + words = Str.split str " " + expect words == [] + + x = 42 + dbg x + dbg "Fjoer en ferdjer frieten oan dyn geve lea" + + r = {x : polyDbg "abc", y: polyDbg 10u8, z : polyDbg (A (B C))} + + when r is + _ -> "Program finished!\n" diff --git a/crates/cli_testing_examples/expects/zig-platform/host.zig b/crates/cli_testing_examples/expects/zig-platform/host.zig new file mode 100644 index 0000000000..a830db2cb7 --- /dev/null +++ b/crates/cli_testing_examples/expects/zig-platform/host.zig @@ -0,0 +1,129 @@ +const std = @import("std"); +const builtin = @import("builtin"); +const str = @import("glue").str; +const RocStr = str.RocStr; +const testing = std.testing; +const expectEqual = testing.expectEqual; +const expect = testing.expect; + +const Align = 2 * @alignOf(usize); +extern fn malloc(size: usize) callconv(.C) ?*align(Align) anyopaque; +extern fn realloc(c_ptr: [*]align(Align) u8, size: usize) callconv(.C) ?*anyopaque; +extern fn free(c_ptr: [*]align(Align) u8) callconv(.C) void; +extern fn memcpy(dst: [*]u8, src: [*]u8, size: usize) callconv(.C) void; +extern fn memset(dst: [*]u8, value: i32, size: usize) callconv(.C) void; + +const DEBUG: bool = false; + +export fn roc_alloc(size: usize, alignment: u32) callconv(.C) ?*anyopaque { + if (DEBUG) { + var ptr = malloc(size); + const stdout = std.io.getStdOut().writer(); + stdout.print("alloc: {d} (alignment {d}, size {d})\n", .{ ptr, alignment, size }) catch unreachable; + return ptr; + } else { + return malloc(size); + } +} + +export fn roc_realloc(c_ptr: *anyopaque, new_size: usize, old_size: usize, alignment: u32) callconv(.C) ?*anyopaque { + if (DEBUG) { + const stdout = std.io.getStdOut().writer(); + stdout.print("realloc: {d} (alignment {d}, old_size {d})\n", .{ c_ptr, alignment, old_size }) catch unreachable; + } + + return realloc(@as([*]align(Align) u8, @alignCast(@ptrCast(c_ptr))), new_size); +} + +export fn roc_dealloc(c_ptr: *anyopaque, alignment: u32) callconv(.C) void { + if (DEBUG) { + const stdout = std.io.getStdOut().writer(); + stdout.print("dealloc: {d} (alignment {d})\n", .{ c_ptr, alignment }) catch unreachable; + } + + free(@as([*]align(Align) u8, @alignCast(@ptrCast(c_ptr)))); +} + +export fn roc_panic(msg: *RocStr, tag_id: u32) callconv(.C) void { + _ = tag_id; + + const stderr = std.io.getStdErr().writer(); + stderr.print("Application crashed with message\n\n {s}\n\nShutting down\n", .{msg.asSlice()}) catch unreachable; + std.process.exit(1); +} + +export fn roc_dbg(loc: *RocStr, msg: *RocStr) callconv(.C) void { + // This platform uses stdout for testing purposes instead of the normal stderr. + const stdout = std.io.getStdOut().writer(); + stdout.print("[{s}] {s}\n", .{ loc.asSlice(), msg.asSlice() }) catch unreachable; +} + +export fn roc_memset(dst: [*]u8, value: i32, size: usize) callconv(.C) void { + return memset(dst, value, size); +} + +extern fn kill(pid: c_int, sig: c_int) c_int; +extern fn shm_open(name: *const i8, oflag: c_int, mode: c_uint) c_int; +extern fn mmap(addr: ?*anyopaque, length: c_uint, prot: c_int, flags: c_int, fd: c_int, offset: c_uint) *anyopaque; +extern fn getppid() c_int; + +fn roc_getppid() callconv(.C) c_int { + return getppid(); +} + +fn roc_getppid_windows_stub() callconv(.C) c_int { + return 0; +} + +fn roc_send_signal(pid: c_int, sig: c_int) callconv(.C) c_int { + return kill(pid, sig); +} +fn roc_shm_open(name: *const i8, oflag: c_int, mode: c_uint) callconv(.C) c_int { + return shm_open(name, oflag, mode); +} +fn roc_mmap(addr: ?*anyopaque, length: c_uint, prot: c_int, flags: c_int, fd: c_int, offset: c_uint) callconv(.C) *anyopaque { + return mmap(addr, length, prot, flags, fd, offset); +} + +comptime { + if (builtin.os.tag == .macos or builtin.os.tag == .linux) { + @export(roc_getppid, .{ .name = "roc_getppid", .linkage = .Strong }); + @export(roc_mmap, .{ .name = "roc_mmap", .linkage = .Strong }); + @export(roc_send_signal, .{ .name = "roc_send_signal", .linkage = .Strong }); + @export(roc_shm_open, .{ .name = "roc_shm_open", .linkage = .Strong }); + } + + if (builtin.os.tag == .windows) { + @export(roc_getppid_windows_stub, .{ .name = "roc_getppid", .linkage = .Strong }); + } +} + +const mem = std.mem; +const Allocator = mem.Allocator; + +extern fn roc__mainForHost_1_exposed_generic(*RocStr) void; + +const Unit = extern struct {}; + +pub fn main() u8 { + const stdout = std.io.getStdOut().writer(); + const stderr = std.io.getStdErr().writer(); + + var timer = std.time.Timer.start() catch unreachable; + + // actually call roc to populate the callresult + var callresult = RocStr.empty(); + roc__mainForHost_1_exposed_generic(&callresult); + + const nanos = timer.read(); + const seconds = (@as(f64, @floatFromInt(nanos)) / 1_000_000_000.0); + + // stdout the result + stdout.print("{s}", .{callresult.asSlice()}) catch unreachable; + + callresult.decref(); + + stderr.print("runtime: {d:.3}ms\n", .{seconds * 1000}) catch unreachable; + + return 0; +} diff --git a/crates/cli_testing_examples/expects/zig-platform/main.roc b/crates/cli_testing_examples/expects/zig-platform/main.roc new file mode 100644 index 0000000000..a52fe9a480 --- /dev/null +++ b/crates/cli_testing_examples/expects/zig-platform/main.roc @@ -0,0 +1,9 @@ +platform "echo-in-zig" + requires {} { main : Str } + exposes [] + packages {} + imports [] + provides [mainForHost] + +mainForHost : Str +mainForHost = main diff --git a/crates/cli_utils/Cargo.toml b/crates/cli_utils/Cargo.toml new file mode 100644 index 0000000000..2530866df5 --- /dev/null +++ b/crates/cli_utils/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "cli_utils" +description = "Provides shared code for cli tests and benchmarks." + +authors.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true +version.workspace = true + +[dependencies] +roc_collections = { path = "../compiler/collections" } +roc_load = { path = "../compiler/load" } +roc_module = { path = "../compiler/module" } +roc_reporting = { path = "../reporting" } +roc_command_utils = { path = "../utils/command" } + +bumpalo.workspace = true +criterion.workspace = true +serde-xml-rs.workspace = true +serde.workspace = true +tempfile.workspace = true + +[target.'cfg(unix)'.dependencies] +rlimit.workspace = true diff --git a/cli_utils/src/bench_utils.rs b/crates/cli_utils/src/bench_utils.rs similarity index 98% rename from cli_utils/src/bench_utils.rs rename to crates/cli_utils/src/bench_utils.rs index 8596ee68d2..ddac5232bf 100644 --- a/cli_utils/src/bench_utils.rs +++ b/crates/cli_utils/src/bench_utils.rs @@ -1,12 +1,10 @@ -use crate::helpers::{example_file, run_cmd, run_roc}; -use const_format::concatcp; +use crate::helpers::{file_path_from_root, run_cmd, run_roc}; use criterion::{black_box, measurement::Measurement, BenchmarkGroup}; -use roc_cli::CMD_BUILD; use std::{path::Path, thread}; const CFOLD_STACK_SIZE: usize = 8192 * 100000; -const OPTIMIZE_FLAG: &str = concatcp!("--", roc_cli::FLAG_OPTIMIZE); +const OPTIMIZE_FLAG: &str = "--optimize"; fn exec_bench_w_input( file: &Path, @@ -16,18 +14,18 @@ fn exec_bench_w_input( bench_group_opt: Option<&mut BenchmarkGroup>, ) { let compile_out = run_roc( - [CMD_BUILD, OPTIMIZE_FLAG, file.to_str().unwrap()], + ["build", OPTIMIZE_FLAG, file.to_str().unwrap()], &[stdin_str], + &[], ); - if !compile_out.stderr.is_empty() { + if !compile_out.stderr.is_empty() && compile_out.stderr != "🔨 Rebuilding platform...\n" { panic!("{}", compile_out.stderr); } assert!( compile_out.status.success(), - "build ended with bad status {:?}", - compile_out + "build ended with bad status {compile_out:?}" ); check_cmd_output(file, stdin_str, executable_filename, expected_ending); @@ -50,19 +48,16 @@ fn check_cmd_output( let out = if cmd_str.contains("cfold") { let child = thread::Builder::new() .stack_size(CFOLD_STACK_SIZE) - .spawn(move || run_cmd(&cmd_str, [stdin_str], &[])) + .spawn(move || run_cmd(&cmd_str, [stdin_str], &[], [])) .unwrap(); child.join().unwrap() } else { - run_cmd(&cmd_str, [stdin_str], &[]) + run_cmd(&cmd_str, [stdin_str], &[], []) }; if !&out.stdout.ends_with(expected_ending) { - panic!( - "expected output to end with {:?} but instead got {:#?}", - expected_ending, out - ); + panic!("expected output to end with {expected_ending:?} but instead got {out:#?}"); } assert!(out.status.success()); } @@ -97,21 +92,22 @@ fn bench_cmd( } if let Some(bench_group) = bench_group_opt { - bench_group.bench_function(&format!("Benchmarking {:?}", executable_filename), |b| { - b.iter(|| run_cmd(black_box(&cmd_str), black_box([stdin_str]), &[])) + bench_group.bench_function(&format!("Benchmarking {executable_filename:?}"), |b| { + b.iter(|| run_cmd(black_box(&cmd_str), black_box([stdin_str]), &[], [])) }); } else { run_cmd( black_box(file.with_file_name(executable_filename).to_str().unwrap()), black_box([stdin_str]), &[], + [], ); } } pub fn bench_nqueens(bench_group_opt: Option<&mut BenchmarkGroup>) { exec_bench_w_input( - &example_file("benchmarks", "NQueens.roc"), + &file_path_from_root("crates/cli_testing_examples/benchmarks", "NQueens.roc"), "11", "nqueens", "2680\n", //2680-14200 @@ -121,7 +117,7 @@ pub fn bench_nqueens(bench_group_opt: Option<&mut BenchmarkGroup pub fn bench_cfold(bench_group_opt: Option<&mut BenchmarkGroup>) { exec_bench_w_input( - &example_file("benchmarks", "CFold.roc"), + &file_path_from_root("crates/cli_testing_examples/benchmarks", "CFold.roc"), "17", "cfold", "396354 & 396354\n", @@ -131,7 +127,7 @@ pub fn bench_cfold(bench_group_opt: Option<&mut BenchmarkGroup(bench_group_opt: Option<&mut BenchmarkGroup>) { exec_bench_w_input( - &example_file("benchmarks", "Deriv.roc"), + &file_path_from_root("crates/cli_testing_examples/benchmarks", "Deriv.roc"), "8", "deriv", "1 count: 6\n2 count: 22\n3 count: 90\n4 count: 420\n5 count: 2202\n6 count: 12886\n7 count: 83648\n8 count: 598592\n", @@ -141,7 +137,7 @@ pub fn bench_deriv(bench_group_opt: Option<&mut BenchmarkGroup(bench_group_opt: Option<&mut BenchmarkGroup>) { exec_bench_w_input( - &example_file("benchmarks", "RBTreeCk.roc"), + &file_path_from_root("crates/cli_testing_examples/benchmarks", "RBTreeCk.roc"), "80000", "rbtree-ck", "8000\n", @@ -152,7 +148,7 @@ pub fn bench_rbtree_ck(bench_group_opt: Option<&mut BenchmarkGro #[allow(dead_code)] pub fn bench_rbtree_delete(bench_group_opt: Option<&mut BenchmarkGroup>) { exec_bench_w_input( - &example_file("benchmarks", "RBTreeDel.roc"), + &file_path_from_root("crates/cli_testing_examples/benchmarks", "RBTreeDel.roc"), "100000", "rbtree-del", "7000\n", @@ -162,7 +158,7 @@ pub fn bench_rbtree_delete(bench_group_opt: Option<&mut Benchmar pub fn bench_quicksort(bench_group_opt: Option<&mut BenchmarkGroup>) { exec_bench_w_input( - &example_file("benchmarks", "QuicksortApp.roc"), + &file_path_from_root("crates/cli_testing_examples/benchmarks", "QuicksortApp.roc"), "1", // 1 for sorting large list, 0 for a small list "quicksortapp", "[0, 0, 1, 1, 2, 2, 2, 3, 3, 4, 6, 6, 7, 7, 8, 8, 8, 8, 8, 8, 9, 10, 10, 11, 11, 12, 13, 13, 14, 14, 14, 15, 15, 15, 16, 16, 18, 18, 19, 19, 19, 20, 21, 21, 21, 21, 22, 23, 23, 23, 25, 26, 27, 27, 28, 28, 28, 29, 29, 29, 29, 30, 31, 32, 33, 34, 35, 35, 35, 36, 36, 36, 37, 38, 38, 39, 39, 39, 39, 39, 39, 40, 40, 41, 42, 42, 42, 42, 42, 43, 43, 44, 46, 47, 47, 47, 48, 50, 51, 51, 52, 52, 52, 53, 54, 54, 55, 55, 55, 56, 57, 57, 58, 58, 58, 58, 58, 59, 59, 60, 60, 61, 62, 63, 63, 63, 63, 64, 65, 65, 65, 66, 66, 66, 66, 67, 67, 68, 69, 69, 70, 70, 71, 71, 71, 72, 72, 73, 73, 73, 74, 75, 75, 75, 76, 78, 79, 79, 80, 81, 81, 82, 82, 83, 83, 84, 84, 86, 86, 87, 87, 88, 88, 88, 89, 89, 90, 90, 90, 91, 92, 92, 92, 93, 93, 93, 94, 95, 95, 96, 97, 98, 99, 100, 100, 101, 102, 102, 102, 104, 104, 105, 106, 106, 106, 106, 106, 106, 107, 107, 108, 108, 108, 109, 109, 109, 109, 110, 112, 112, 112, 113, 113, 113, 113, 113, 114, 115, 117, 117, 117, 118, 119, 119, 119, 120, 120, 121, 123, 124, 125, 125, 126, 126, 126, 126, 127, 129, 131, 131, 131, 131, 131, 131, 131, 132, 133, 133, 134, 134, 134, 135, 135, 135, 135, 135, 137, 138, 138, 138, 139, 139, 140, 141, 142, 142, 142, 144, 144, 145, 145, 145, 147, 147, 147, 147, 148, 149, 149, 149, 150, 150, 151, 151, 151, 151, 153, 155, 156, 159, 160, 160, 160, 161, 161, 162, 162, 162, 162, 162, 162, 163, 163, 163, 163, 163, 163, 164, 164, 164, 164, 164, 165, 165, 165, 165, 165, 166, 166, 166, 166, 166, 167, 167, 167, 167, 168, 169, 170, 170, 170, 170, 172, 172, 172, 173, 173, 173, 174, 175, 176, 177, 177, 178, 178, 178, 178, 179, 179, 180, 180, 181, 181, 182, 183, 183, 185, 186, 186, 186, 186, 186, 186, 186, 187, 187, 187, 188, 188, 188, 190, 190, 190, 190, 190, 192, 193, 194, 194, 194, 195, 195, 196, 197, 198, 198, 198, 199, 199, 199, 200, 200, 201, 201, 201, 204, 205, 205, 205, 207, 207, 207, 208, 208, 208, 208, 210, 210, 210, 210, 211, 211, 213, 214, 214, 214, 218, 218, 218, 218, 218, 218, 219, 221, 222, 223, 223, 223, 224, 224, 224, 224, 224, 224, 224, 225, 226, 226, 226, 226, 226, 227, 227, 228, 228, 229, 229, 229, 229, 230, 230, 230, 230, 232, 233, 233, 234, 235, 236, 236, 237, 237, 238, 240, 240, 242, 242, 243, 244, 246, 247, 247, 247, 247, 248, 248, 248, 249, 249, 249, 249, 249, 250, 250, 250, 251, 251, 252, 252, 253, 255, 255, 256, 256, 256, 257, 257, 257, 258, 258, 258, 258, 258, 259, 259, 260, 260, 260, 261, 261, 261, 262, 263, 265, 265, 266, 267, 267, 267, 268, 268, 268, 270, 270, 270, 271, 271, 273, 274, 274, 274, 275, 277, 277, 279, 279, 280, 281, 281, 282, 283, 283, 285, 286, 288, 288, 289, 289, 290, 290, 290, 290, 290, 291, 291, 291, 291, 292, 292, 292, 293, 294, 294, 295, 295, 295, 295, 295, 298, 298, 301, 301, 301, 302, 302, 303, 304, 305, 305, 306, 307, 307, 308, 308, 309, 309, 309, 309, 310, 310, 311, 311, 311, 312, 313, 313, 313, 314, 315, 316, 316, 316, 316, 317, 318, 318, 319, 319, 319, 320, 321, 321, 322, 322, 322, 322, 323, 323, 323, 324, 324, 324, 325, 326, 326, 328, 329, 329, 330, 330, 330, 331, 331, 331, 331, 332, 332, 333, 333, 333, 333, 334, 334, 334, 335, 336, 336, 337, 337, 337, 337, 339, 339, 340, 341, 341, 343, 344, 344, 345, 345, 345, 346, 346, 347, 348, 348, 348, 349, 350, 351, 351, 351, 352, 353, 354, 354, 354, 355, 356, 356, 357, 358, 358, 358, 359, 359, 360, 361, 361, 362, 362, 363, 364, 364, 365, 365, 365, 366, 366, 367, 367, 368, 368, 369, 369, 369, 370, 370, 370, 370, 370, 371, 372, 373, 373, 374, 374, 375, 375, 376, 377, 377, 378, 379, 381, 381, 383, 384, 385, 385, 385, 385, 386, 386, 387, 388, 388, 388, 389, 389, 390, 391, 391, 391, 392, 392, 393, 393, 394, 394, 394, 395, 395, 396, 396, 397, 397, 398, 399, 400, 400, 401, 401, 402, 402, 403, 404, 404, 405, 406, 406, 407, 407, 407, 408, 408, 408, 408, 408, 409, 409, 409, 411, 411, 412, 412, 413, 413, 413, 413, 413, 414, 414, 414, 415, 416, 416, 416, 416, 417, 417, 418, 418, 418, 418, 419, 420, 420, 420, 421, 421, 422, 422, 423, 423, 423, 424, 424, 424, 424, 425, 425, 425, 426, 426, 427, 427, 427, 428, 428, 429, 429, 429, 430, 430, 431, 432, 433, 433, 433, 434, 434, 434, 434, 437, 438, 438, 438, 438, 438, 439, 440, 441, 441, 442, 442, 443, 444, 444, 444, 445, 445, 445, 447, 447, 447, 448, 448, 449, 449, 450, 450, 450, 451, 452, 453, 453, 453, 453, 455, 455, 456, 456, 457, 458, 459, 459, 460, 460, 461, 461, 464, 465, 465, 465, 466, 466, 467, 467, 467, 467, 468, 469, 469, 470, 470, 471, 471, 471, 472, 473, 473, 473, 473, 474, 475, 475, 475, 476, 476, 476, 477, 477, 477, 478, 478, 479, 481, 481, 481, 482, 482, 482, 483, 483, 483, 484, 484, 485, 488, 488, 488, 488, 489, 490, 491, 491, 491, 492, 492, 493, 493, 493, 493, 493, 495, 495, 496, 496, 496, 496, 496, 496, 497, 497, 498, 498, 498, 498, 498, 499, 500, 500, 501, 501, 501, 502, 502, 502, 502, 503, 503, 503, 505, 505, 506, 507, 507, 507, 507, 508, 508, 510, 510, 510, 511, 511, 512, 512, 513, 513, 513, 513, 514, 514, 515, 516, 517, 518, 519, 519, 519, 520, 521, 521, 522, 522, 523, 523, 523, 525, 525, 526, 527, 527, 527, 528, 528, 528, 530, 531, 532, 532, 532, 532, 532, 535, 535, 537, 538, 538, 538, 540, 540, 540, 541, 541, 541, 541, 541, 542, 543, 543, 543, 543, 544, 544, 545, 545, 545, 546, 547, 547, 547, 548, 549, 549, 551, 552, 552, 553, 553, 553, 554, 554, 554, 555, 556, 557, 557, 557, 558, 558, 558, 559, 559, 559, 560, 560, 560, 561, 561, 561, 561, 562, 562, 562, 563, 563, 565, 566, 566, 567, 568, 569, 570, 570, 571, 571, 571, 571, 572, 572, 572, 574, 575, 576, 576, 577, 580, 581, 581, 582, 582, 582, 583, 583, 584, 585, 585, 585, 586, 587, 587, 588, 588, 588, 589, 591, 591, 591, 592, 592, 592, 593, 593, 593, 594, 594, 594, 594, 595, 595, 595, 596, 596, 596, 596, 596, 597, 597, 599, 599, 600, 600, 601, 601, 601, 602, 602, 603, 603, 604, 605, 605, 605, 606, 607, 608, 610, 612, 612, 613, 613, 614, 614, 615, 615, 615, 616, 616, 616, 617, 617, 619, 619, 619, 619, 620, 621, 621, 622, 624, 624, 624, 624, 625, 625, 628, 628, 628, 629, 629, 630, 630, 630, 630, 632, 633, 633, 634, 635, 638, 638, 639, 640, 641, 641, 643, 643, 644, 644, 644, 645, 645, 645, 646, 646, 646, 647, 647, 647, 647, 648, 648, 649, 650, 650, 650, 650, 650, 650, 651, 652, 652, 652, 653, 653, 653, 653, 654, 655, 655, 655, 655, 656, 657, 657, 657, 658, 658, 659, 659, 659, 659, 659, 660, 660, 661, 662, 663, 664, 665, 666, 666, 666, 667, 667, 667, 667, 667, 668, 668, 669, 670, 670, 670, 671, 672, 672, 672, 672, 672, 673, 673, 674, 674, 674, 675, 676, 676, 677, 678, 678, 679, 679, 680, 681, 681, 682, 683, 683, 684, 684, 685, 686, 686, 686, 686, 687, 687, 688, 690, 690, 691, 691, 693, 693, 694, 694, 697, 697, 698, 700, 701, 702, 702, 703, 703, 703, 704, 705, 706, 706, 707, 708, 708, 709, 709, 710, 710, 711, 712, 712, 712, 712, 712, 712, 713, 713, 714, 714, 716, 716, 716, 717, 717, 717, 718, 718, 718, 718, 719, 719, 719, 720, 720, 721, 721, 722, 723, 724, 725, 726, 726, 727, 729, 729, 729, 730, 730, 731, 731, 732, 732, 734, 734, 734, 735, 735, 736, 736, 736, 737, 737, 738, 739, 740, 740, 740, 741, 741, 742, 742, 742, 742, 744, 744, 744, 744, 745, 745, 745, 745, 746, 748, 749, 749, 749, 750, 750, 751, 751, 751, 752, 752, 753, 753, 754, 755, 756, 756, 756, 757, 757, 757, 757, 757, 761, 761, 762, 762, 762, 763, 763, 763, 763, 763, 764, 764, 764, 764, 765, 765, 766, 766, 766, 766, 767, 767, 767, 770, 770, 770, 770, 770, 771, 772, 772, 772, 773, 774, 775, 775, 775, 775, 776, 778, 778, 779, 779, 780, 780, 780, 781, 784, 784, 784, 786, 786, 786, 786, 787, 788, 789, 789, 789, 790, 791, 791, 792, 793, 793, 793, 794, 794, 795, 796, 797, 797, 798, 799, 799, 799, 800, 800, 800, 800, 801, 802, 802, 802, 802, 804, 806, 806, 806, 807, 807, 807, 807, 808, 809, 810, 810, 811, 812, 812, 812, 812, 812, 813, 813, 813, 814, 814, 814, 815, 816, 816, 817, 817, 817, 818, 818, 818, 819, 820, 820, 820, 820, 820, 821, 821, 823, 824, 824, 824, 825, 826, 826, 826, 826, 828, 828, 829, 829, 829, 829, 829, 830, 831, 831, 831, 831, 831, 832, 832, 833, 833, 833, 834, 834, 835, 835, 835, 835, 835, 836, 836, 836, 837, 839, 839, 839, 839, 839, 840, 840, 840, 841, 841, 842, 843, 844, 844, 844, 845, 845, 845, 845, 845, 846, 846, 846, 847, 847, 848, 848, 848, 849, 849, 850, 850, 851, 852, 852, 852, 852, 853, 855, 856, 857, 857, 858, 858, 858, 859, 860, 861, 861, 861, 861, 862, 863, 863, 863, 865, 865, 865, 866, 867, 867, 867, 868, 868, 870, 871, 872, 872, 873, 873, 873, 874, 874, 874, 875, 875, 875, 876, 877, 878, 878, 878, 878, 878, 879, 879, 879, 879, 880, 881, 881, 881, 882, 883, 885, 886, 886, 887, 887, 888, 888, 889, 889, 890, 890, 890, 892, 892, 892, 892, 893, 893, 894, 894, 894, 895, 896, 896, 896, 897, 899, 899, 900, 901, 901, 901, 901, 905, 905, 905, 905, 906, 907, 907, 907, 908, 908, 908, 908, 908, 908, 909, 909, 910, 910, 910, 912, 913, 913, 914, 914, 914, 915, 916, 916, 916, 916, 917, 917, 918, 919, 919, 919, 920, 920, 920, 920, 921, 921, 922, 923, 923, 923, 923, 923, 924, 925, 927, 927, 927, 928, 928, 929, 929, 929, 929, 930, 930, 931, 932, 932, 932, 933, 933, 934, 934, 935, 935, 936, 937, 937, 937, 939, 940, 940, 941, 941, 941, 941, 942, 942, 943, 943, 945, 946, 946, 946, 948, 949, 949, 951, 953, 953, 954, 954, 954, 954, 954, 955, 956, 956, 956, 957, 957, 957, 957, 959, 960, 960, 961, 961, 963, 963, 963, 964, 964, 964, 964, 965, 966, 967, 968, 969, 969, 970, 972, 972, 973, 973, 974, 975, 975, 975, 976, 977, 978, 978, 979, 979, 980, 980, 980, 980, 981, 982, 982, 984, 986, 986, 986, 986, 986, 987, 988, 988, 990, 990, 990, 990, 990, 991, 991, 991, 991, 991, 991, 992, 992, 992, 992, 992, 993, 993, 993, 993, 995, 996, 996, 996, 997, 997, 997, 997, 997, 998, 998, 998, 999, 999, 1000, 1001, 1001, 1002, 1003, 1003, 1004, 1004, 1004, 1006, 1007, 1007, 1007, 1008, 1008, 1008, 1009, 1010, 1010, 1011, 1011, 1012, 1012, 1012, 1013, 1013, 1013, 1014, 1014, 1014, 1016, 1016, 1016, 1017, 1017, 1017, 1018, 1018, 1018, 1019, 1019, 1020, 1020, 1021, 1021, 1021, 1022, 1023, 1023, 1023, 1024, 1024, 1024, 1025, 1026, 1026, 1027, 1028, 1028, 1028, 1028, 1029, 1029, 1029, 1030, 1031, 1031, 1032, 1033, 1034, 1034, 1035, 1035, 1036, 1038, 1039, 1039, 1040, 1040, 1040, 1040, 1040, 1040, 1042, 1042, 1043, 1043, 1043, 1043, 1044, 1045, 1045, 1045, 1045, 1047, 1047, 1048, 1048, 1049, 1049, 1050, 1050, 1051, 1051, 1053, 1053, 1053, 1054, 1054, 1055, 1055, 1056, 1056, 1057, 1057, 1058, 1058, 1058, 1058, 1059, 1059, 1059, 1061, 1061, 1061, 1061, 1062, 1062, 1062, 1063, 1063, 1063, 1063, 1064, 1064, 1064, 1064, 1064, 1065, 1065, 1066, 1066, 1067, 1067, 1069, 1069, 1069, 1070, 1071, 1071, 1072, 1072, 1072, 1073, 1073, 1074, 1074, 1074, 1075, 1076, 1077, 1077, 1078, 1078, 1078, 1079, 1079, 1079, 1081, 1082, 1082, 1083, 1084, 1084, 1084, 1084, 1085, 1085, 1086, 1086, 1087, 1087, 1088, 1088, 1089, 1089, 1090, 1090, 1090, 1091, 1093, 1093, 1093, 1094, 1094, 1094, 1094, 1095, 1095, 1095, 1095, 1095, 1095, 1096, 1097, 1098, 1098, 1098, 1098, 1100, 1102, 1102, 1103, 1103, 1103, 1104, 1104, 1105, 1105, 1105, 1105, 1106, 1106, 1106, 1106, 1107, 1107, 1107, 1108, 1110, 1111, 1111, 1112, 1113, 1113, 1113, 1113, 1115, 1115, 1115, 1115, 1115, 1116, 1116, 1117, 1117, 1119, 1119, 1119, 1121, 1122, 1122, 1122, 1122, 1123, 1124, 1124, 1125, 1125, 1127, 1127, 1127, 1128, 1129, 1129, 1129, 1130, 1130, 1131, 1131, 1132, 1132, 1132, 1132, 1134, 1135, 1137, 1137, 1138, 1138, 1138, 1138, 1139, 1140, 1140, 1140, 1140, 1142, 1142, 1142, 1142, 1142, 1142, 1143, 1143, 1145, 1145, 1148, 1148, 1150, 1150, 1151, 1151, 1151, 1152, 1152, 1152, 1153, 1153, 1154, 1155, 1156, 1156, 1156, 1156, 1157, 1158, 1158, 1158, 1159, 1159, 1159, 1160, 1160, 1161, 1161, 1161, 1162, 1162, 1163, 1163, 1163, 1164, 1164, 1165, 1165, 1167, 1167, 1167, 1168, 1168, 1168, 1169, 1170, 1170, 1171, 1171, 1171, 1172, 1172, 1172, 1173, 1173, 1173, 1174, 1174, 1174, 1174, 1176, 1176, 1176, 1176, 1176, 1177, 1178, 1178, 1178, 1179, 1179, 1179, 1180, 1180, 1181, 1181, 1182, 1182, 1182, 1183, 1183, 1184, 1184, 1184, 1184, 1184, 1185, 1186, 1186, 1188, 1188, 1189, 1189, 1190, 1190, 1191, 1191, 1191, 1192, 1192, 1193, 1193, 1195, 1197, 1197, 1198, 1198, 1198, 1199, 1199, 1199, 1200, 1201, 1201, 1201, 1202, 1202, 1202, 1202, 1204, 1204, 1205, 1205, 1205, 1205, 1205, 1206, 1206, 1206, 1207, 1207, 1207, 1207, 1207, 1207, 1209, 1210, 1210, 1211, 1212, 1213, 1213, 1214, 1214, 1215, 1215, 1216, 1216, 1217, 1217, 1217, 1219, 1219, 1219, 1219, 1220, 1220, 1222, 1222, 1223, 1224, 1224, 1225, 1225, 1226, 1226, 1226, 1227, 1227, 1227, 1227, 1227, 1227, 1228, 1228, 1228, 1229, 1230, 1230, 1232, 1232, 1232, 1232, 1232, 1232, 1233, 1234, 1235, 1235, 1235, 1236, 1237, 1238, 1239, 1240, 1240, 1240, 1240, 1240, 1240, 1241, 1241, 1242, 1243, 1243, 1243, 1243, 1244, 1244, 1246, 1246, 1247, 1247, 1249, 1250, 1251, 1251, 1252, 1252, 1252, 1252, 1252, 1252, 1253, 1253, 1253, 1253, 1254, 1254, 1255, 1256, 1257, 1257, 1257, 1259, 1259, 1261, 1261, 1262, 1263, 1263, 1264, 1265, 1265, 1265, 1266, 1266, 1268, 1268, 1269, 1270, 1270, 1270, 1270, 1271, 1271, 1271, 1271, 1272, 1272, 1273, 1273, 1274, 1274, 1274, 1274, 1275, 1275, 1275, 1275, 1276, 1276, 1276, 1276, 1276, 1277, 1278, 1279, 1279, 1280, 1280, 1281, 1282, 1282, 1283, 1283, 1284, 1284, 1284, 1286, 1286, 1289, 1290, 1290, 1290, 1291, 1292, 1292, 1293, 1293, 1294, 1296, 1296, 1296, 1296, 1297, 1297, 1297, 1298, 1299, 1300, 1300, 1301, 1302, 1303, 1304, 1304, 1305, 1305, 1306, 1306, 1307, 1307, 1307, 1307, 1307, 1308, 1308, 1308, 1308, 1309, 1309, 1310, 1311, 1312, 1312, 1313, 1313, 1313, 1314, 1315, 1316, 1316, 1316, 1317, 1319, 1320, 1320, 1320, 1320, 1321, 1322, 1322, 1323, 1323, 1323, 1324, 1324, 1325, 1327, 1328, 1329, 1329, 1330, 1330, 1330, 1330, 1332, 1332, 1332, 1333, 1333, 1334, 1335, 1335, 1336, 1336, 1336, 1338, 1338, 1338, 1339, 1339, 1340, 1340, 1340, 1341, 1341, 1341, 1342, 1343, 1343, 1345, 1345, 1345, 1346, 1346, 1346, 1346, 1346, 1346, 1347, 1348, 1349, 1349, 1349, 1349, 1351, 1352, 1353, 1353, 1353, 1354, 1354, 1355, 1355, 1356, 1356, 1356, 1356, 1358, 1358, 1359, 1359, 1359, 1359, 1359, 1360, 1360, 1360, 1361, 1361, 1361, 1362, 1362, 1363, 1363, 1363, 1365, 1365, 1366, 1367, 1367, 1370, 1371, 1371, 1372, 1372, 1373, 1373, 1373, 1374, 1375, 1375, 1375, 1377, 1377, 1378, 1378, 1378, 1380, 1380, 1381, 1381, 1381, 1382, 1382, 1382, 1382, 1382, 1382, 1383, 1383, 1383, 1384, 1384, 1384, 1385, 1385, 1385, 1385, 1386, 1386, 1387, 1387, 1388, 1388, 1388, 1389, 1389, 1389, 1392, 1393, 1393, 1394, 1394, 1395, 1395, 1395, 1396, 1397, 1398, 1398, 1398, 1399, 1399, 1399, 1400, 1401, 1402, 1402, 1402, 1403, 1404, 1405, 1406, 1406, 1406, 1406, 1407, 1407, 1407, 1407, 1409, 1409, 1409, 1410, 1410, 1410, 1410, 1410, 1411, 1411, 1412, 1413, 1413, 1413, 1414, 1414, 1415, 1415, 1415, 1416, 1416, 1416, 1417, 1417, 1417, 1417, 1417, 1419, 1420, 1420, 1420, 1421, 1422, 1422, 1422, 1422, 1425, 1426, 1427, 1427, 1428, 1428, 1430, 1431, 1431, 1432, 1432, 1432, 1433, 1433, 1434, 1434, 1434, 1434, 1434, 1435, 1436, 1436, 1436, 1436, 1436, 1437, 1438, 1438, 1438, 1438, 1439, 1439, 1440, 1440, 1440, 1440, 1441, 1441, 1442, 1443, 1444, 1444, 1444, 1444, 1444, 1444, 1444, 1444, 1445, 1446, 1446, 1446, 1447, 1448, 1449, 1449, 1450, 1450, 1450, 1451, 1451, 1452, 1452, 1452, 1453, 1454, 1455, 1456, 1458, 1459, 1459, 1459, 1459, 1460, 1460, 1461, 1461, 1461, 1462, 1462, 1462, 1462, 1462, 1462, 1463, 1463, 1465, 1465, 1465, 1466, 1467, 1468, 1469, 1470, 1472, 1472, 1473, 1474, 1474, 1474, 1474, 1475, 1476, 1477, 1477, 1477, 1477, 1478, 1478, 1480, 1481, 1481, 1481, 1481, 1481, 1481, 1482, 1482, 1482, 1483, 1484, 1485, 1485, 1486, 1486, 1486, 1488, 1488, 1489, 1489, 1489, 1491, 1491, 1492, 1492, 1493, 1495, 1495, 1495, 1496, 1496, 1497, 1497, 1497, 1497, 1497, 1498, 1498, 1499, 1500, 1500, 1501, 1501, 1501, 1501, 1502, 1503, 1503, 1503, 1503, 1503, 1503, 1504, 1505, 1505, 1505, 1506, 1506, 1506, 1506, 1509, 1509, 1509, 1510, 1510, 1511, 1511, 1511, 1511, 1512, 1513, 1513, 1513, 1514, 1514, 1515, 1516, 1516, 1517, 1517, 1518, 1518, 1519, 1519, 1520, 1521, 1522, 1522, 1524, 1525, 1525, 1525, 1525, 1526, 1526, 1526, 1526, 1526, 1526, 1528, 1528, 1528, 1529, 1532, 1532, 1532, 1534, 1534, 1535, 1536, 1536, 1536, 1537, 1537, 1538, 1538, 1538, 1539, 1539, 1539, 1539, 1540, 1541, 1542, 1542, 1543, 1544, 1544, 1544, 1545, 1545, 1545, 1546, 1547, 1547, 1547, 1547, 1547, 1548, 1550, 1551, 1551, 1551, 1551, 1552, 1552, 1552, 1552, 1553, 1554, 1554, 1554, 1555, 1555, 1555, 1555, 1556, 1556, 1557, 1558, 1559, 1559, 1559, 1560, 1560, 1560, 1560, 1561, 1561, 1562, 1562, 1563, 1564, 1564, 1565, 1565, 1565, 1566, 1567, 1567, 1568, 1568, 1569, 1569, 1570, 1570, 1570, 1571, 1571, 1571, 1571, 1572, 1572, 1572, 1573, 1573, 1573, 1573, 1574, 1574, 1575, 1575, 1575, 1575, 1575, 1576, 1576, 1576, 1576, 1576, 1578, 1578, 1578, 1579, 1579, 1579, 1580, 1581, 1581, 1581, 1581, 1581, 1582, 1582, 1582, 1582, 1583, 1583, 1586, 1586, 1586, 1586, 1586, 1587, 1588, 1589, 1590, 1591, 1591, 1591, 1594, 1595, 1595, 1595, 1596, 1598, 1598, 1599, 1600, 1600, 1601, 1601, 1601, 1602, 1602, 1602, 1603, 1603, 1605, 1605, 1606, 1607, 1608, 1608, 1608, 1609, 1609, 1609, 1609, 1611, 1611, 1612, 1612, 1612, 1612, 1612, 1612, 1614, 1615, 1615, 1615, 1615, 1616, 1618, 1618, 1619, 1620, 1621, 1621, 1621, 1622, 1623, 1623, 1624, 1624, 1624, 1624, 1625, 1625, 1625, 1626, 1626, 1627, 1627, 1627, 1629, 1629, 1630, 1630, 1631, 1631, 1634, 1634, 1634, 1634, 1634, 1634, 1635, 1636, 1639, 1639, 1640, 1641, 1641, 1641, 1642, 1642, 1643, 1645, 1645, 1645, 1646, 1647, 1647, 1647, 1648, 1649, 1649, 1649, 1649, 1649, 1651, 1652, 1653, 1653, 1655, 1655, 1655, 1655, 1655, 1655, 1657, 1657, 1657, 1658, 1658, 1659, 1659, 1659, 1659, 1660, 1660, 1660, 1660, 1662, 1663, 1663, 1664, 1664, 1666, 1666, 1666, 1666, 1668, 1669, 1669, 1669, 1671, 1671, 1672, 1672, 1673, 1673, 1673, 1673, 1674, 1674, 1675, 1675, 1675, 1677, 1677, 1677, 1677, 1678, 1678, 1678, 1679, 1679, 1679, 1679, 1680, 1680, 1680, 1681, 1681, 1681, 1682, 1682, 1682, 1683, 1683, 1683, 1684, 1684, 1684, 1685, 1685, 1686, 1687, 1688, 1688, 1688, 1689, 1689, 1691, 1691, 1691, 1692, 1693, 1693, 1693, 1696, 1697, 1697, 1698, 1699, 1700, 1700, 1701, 1702, 1703, 1703, 1705, 1705, 1705, 1707, 1708, 1708, 1708, 1709, 1711, 1712, 1712, 1712, 1714, 1714, 1714, 1714, 1715, 1716, 1716, 1717, 1718, 1718, 1719, 1719, 1719, 1720, 1720, 1720, 1721, 1721, 1722, 1722, 1722, 1722, 1722, 1723, 1723, 1724, 1724, 1725, 1726, 1726, 1727, 1727, 1728, 1728, 1730, 1731, 1731, 1734, 1735, 1735, 1735, 1736, 1737, 1737, 1738, 1738, 1738, 1739, 1739, 1739, 1739, 1739, 1740, 1740, 1740, 1740, 1740, 1741, 1741, 1741, 1741, 1741, 1742, 1743, 1744, 1744, 1744, 1745, 1746, 1746, 1747, 1748, 1749, 1749, 1749, 1749, 1751, 1751, 1751, 1752, 1752, 1752, 1752, 1753, 1754, 1755, 1755, 1755, 1756, 1756, 1757, 1757, 1757, 1757, 1758, 1759, 1759, 1759, 1760, 1760, 1762, 1764, 1766, 1766, 1767, 1767, 1768, 1769, 1769, 1770, 1770, 1770, 1771, 1772, 1773, 1774, 1775, 1775, 1775, 1776, 1776, 1776, 1777, 1777, 1778, 1778, 1779, 1779, 1780, 1780, 1781, 1782, 1784, 1784, 1784, 1785, 1785, 1785, 1785, 1787, 1788, 1789, 1789, 1789, 1790, 1790, 1790, 1791, 1791, 1791, 1791, 1791, 1792, 1792, 1793, 1793, 1793, 1793, 1794, 1794, 1795, 1795, 1796, 1796, 1797, 1797, 1798, 1798, 1798, 1799, 1799, 1800, 1800, 1800, 1801, 1801, 1802, 1802, 1804, 1804, 1804, 1806, 1806, 1808, 1809, 1810, 1810, 1811, 1811, 1814, 1814, 1814, 1815, 1815, 1816, 1816, 1816, 1816, 1817, 1817, 1818, 1819, 1819, 1819, 1820, 1820, 1820, 1821, 1821, 1822, 1823, 1823, 1824, 1824, 1824, 1825, 1825, 1825, 1826, 1826, 1826, 1827, 1827, 1827, 1828, 1828, 1830, 1831, 1832, 1832, 1832, 1832, 1833, 1833, 1833, 1833, 1835, 1837, 1838, 1839, 1840, 1840, 1840, 1840, 1840, 1840, 1841, 1842, 1842, 1843, 1843, 1844, 1844, 1844, 1844, 1844, 1845, 1846, 1847, 1847, 1847, 1848, 1849, 1849, 1849, 1850, 1850, 1850, 1851, 1851, 1851, 1852, 1852, 1853, 1853, 1853, 1854, 1854, 1855, 1855, 1855, 1855, 1855, 1855, 1856, 1856, 1856, 1856, 1857, 1857, 1857, 1857, 1858, 1859, 1859, 1860, 1860, 1860, 1860, 1861, 1861, 1863, 1863, 1865, 1865, 1866, 1866, 1866, 1866, 1866, 1867, 1867, 1867, 1867, 1867, 1868, 1869, 1869, 1869, 1869, 1869, 1869, 1870, 1870, 1870, 1870, 1871, 1872, 1873, 1874, 1875, 1875, 1876, 1876, 1876, 1876, 1877, 1877, 1878, 1878, 1878, 1879, 1879, 1880, 1880, 1880, 1881, 1881, 1883, 1883, 1885, 1885, 1885, 1885, 1885, 1885, 1886, 1886, 1886, 1887, 1887, 1887, 1887, 1888, 1888, 1890, 1891, 1891, 1891, 1892, 1894, 1894, 1894, 1894, 1896, 1896, 1896, 1896, 1897, 1899, 1899, 1900, 1900, 1901, 1901, 1902, 1903, 1904, 1905, 1905, 1905, 1906, 1906, 1906, 1907, 1907, 1908, 1908, 1909, 1910, 1910, 1911, 1912, 1912, 1912, 1913, 1914, 1914, 1914, 1915, 1915, 1915, 1916, 1916, 1916, 1917, 1918, 1918, 1919, 1919, 1920, 1920, 1920, 1920, 1921, 1921, 1922, 1923, 1925, 1925, 1925, 1925, 1926, 1928, 1929, 1929, 1930, 1930, 1931, 1931, 1931, 1931, 1932, 1932, 1932, 1932, 1932, 1933, 1933, 1933, 1933, 1934, 1934, 1934, 1934, 1934, 1935, 1935, 1935, 1936, 1937, 1938, 1938, 1938, 1938, 1940, 1941, 1941, 1941, 1942, 1942, 1943, 1943, 1944, 1944, 1944, 1944, 1945, 1946, 1946, 1947, 1948, 1948, 1948, 1949, 1949, 1949, 1949, 1949, 1950, 1950, 1950, 1951, 1951, 1951, 1951, 1951, 1952, 1953, 1955, 1955, 1956, 1956, 1956, 1957, 1957, 1957, 1958, 1958, 1960, 1960, 1960, 1960, 1961, 1963, 1965, 1965, 1965, 1967, 1967, 1968, 1968, 1969, 1969, 1969, 1969, 1970, 1970, 1971, 1971, 1971, 1972, 1972, 1973, 1973, 1973, 1973, 1973, 1974, 1974, 1975, 1975, 1976, 1976, 1976, 1976, 1977, 1978, 1978, 1979, 1979, 1979, 1980, 1980, 1981, 1981, 1982, 1982, 1982, 1983, 1983, 1983, 1984, 1984, 1986, 1986, 1987, 1989, 1989, 1989, 1989, 1989, 1990, 1990, 1990, 1991, 1991, 1991, 1991, 1992, 1992, 1994, 1994, 1994, 1995, 1995, 1995, 1995, 1995, 1996, 1996, 1996, 1997, 1997, 1998, 1998, 1998, 1998, 1999, 1999, 2000, 2000, 2002, 2003, 2003, 2005, 2009, 2010, 2010, 2011, 2012, 2013, 2014, 2014, 2015, 2016, 2016, 2016, 2016, 2016, 2017, 2018, 2019, 2020, 2020, 2021, 2021, 2021, 2021, 2024, 2026, 2027, 2027, 2028, 2028, 2029, 2029, 2030, 2031, 2032, 2032, 2033, 2034, 2035, 2035, 2036, 2036, 2036, 2036, 2036, 2037, 2037, 2037, 2037, 2038, 2038, 2039, 2039, 2039, 2040, 2041, 2041, 2042, 2042, 2042, 2042, 2042, 2042, 2042, 2043, 2044, 2044, 2045, 2045, 2045, 2046, 2047, 2047, 2048, 2048, 2049, 2051, 2051, 2052, 2052, 2054, 2054, 2054, 2054, 2055, 2056, 2056, 2057, 2058, 2058, 2059, 2059, 2062, 2063, 2063, 2063, 2063, 2063, 2063, 2064, 2064, 2065, 2065, 2065, 2065, 2066, 2066, 2067, 2067, 2068, 2068, 2068, 2068, 2068, 2069, 2070, 2070, 2071, 2071, 2071, 2072, 2073, 2073, 2073, 2075, 2075, 2075, 2076, 2077, 2077, 2078, 2078, 2079, 2079, 2079, 2079, 2080, 2080, 2080, 2081, 2082, 2082, 2082, 2082, 2083, 2083, 2083, 2084, 2084, 2084, 2085, 2085, 2086, 2086, 2086, 2087, 2087, 2087, 2088, 2088, 2088, 2088, 2088, 2089, 2089, 2089, 2089, 2089, 2089, 2089, 2090, 2091, 2091, 2091, 2091, 2092, 2093, 2093, 2094, 2094, 2095, 2096, 2096, 2097, 2097, 2097, 2097, 2098, 2098, 2098, 2098, 2099, 2100, 2102, 2102, 2102, 2102, 2102, 2104, 2104, 2104, 2105, 2105, 2106, 2106, 2107, 2108, 2109, 2109, 2110, 2110, 2111, 2111, 2112, 2114, 2115, 2115, 2116, 2117, 2117, 2118, 2119, 2119, 2119, 2120, 2121, 2121, 2121, 2122, 2122, 2122, 2123, 2124, 2124, 2125, 2125, 2125, 2125, 2127, 2127, 2127, 2127, 2128, 2128, 2128, 2128, 2128, 2129, 2129, 2130, 2131, 2131, 2131, 2132, 2132, 2132, 2133, 2133, 2133, 2133, 2133, 2133, 2133, 2133, 2134, 2135, 2136, 2137, 2137, 2137, 2138, 2138, 2139, 2140, 2140, 2140, 2140, 2142, 2143, 2144, 2144, 2145, 2145, 2145, 2145, 2146, 2146, 2146, 2147, 2147, 2147, 2147, 2147, 2148, 2148, 2148, 2148, 2149, 2149, 2149, 2150, 2151, 2151, 2153, 2153, 2153, 2153, 2154, 2154, 2154, 2155, 2155, 2156, 2157, 2157, 2157, 2157, 2158, 2158, 2158, 2158, 2158, 2159, 2159, 2160, 2160, 2160, 2160, 2161, 2162, 2162, 2162, 2162, 2162, 2163, 2164, 2164, 2167, 2168, 2169, 2169, 2169, 2170, 2172, 2172, 2172, 2172, 2172, 2173, 2173, 2174, 2174, 2175, 2175, 2176, 2176, 2176, 2176, 2177, 2177, 2179, 2179, 2180, 2180, 2180, 2183, 2183, 2183, 2183, 2184, 2185, 2185, 2185, 2185, 2186, 2186, 2186, 2187, 2187, 2188, 2189, 2189, 2189, 2190, 2190, 2191, 2191, 2191, 2191, 2191, 2192, 2193, 2194, 2194, 2195, 2195, 2195, 2195, 2196, 2196, 2197, 2197, 2197, 2198, 2198, 2198, 2199, 2199, 2199, 2200, 2200, 2201, 2201, 2202, 2202, 2202, 2203, 2203, 2204, 2205, 2205, 2205, 2205, 2205, 2206, 2206, 2206, 2207, 2207, 2207, 2210, 2210, 2212, 2213, 2214, 2214, 2215, 2216, 2216, 2216, 2217, 2217, 2219, 2219, 2219, 2219, 2220, 2220, 2221, 2221, 2222, 2222, 2223, 2223, 2224, 2224, 2225, 2225, 2226, 2226, 2226, 2226, 2227, 2228, 2228, 2228, 2229, 2229, 2229, 2230, 2230, 2231, 2231, 2232, 2232, 2232, 2234, 2234, 2234, 2235, 2235, 2236, 2237, 2237, 2238, 2238, 2239, 2239, 2239, 2240, 2240, 2241, 2241, 2241, 2242, 2244, 2244, 2245, 2245, 2245, 2245, 2246, 2248, 2249, 2250, 2251, 2251, 2251, 2251, 2252, 2252, 2253, 2254, 2254, 2255, 2256, 2256, 2256, 2258, 2258, 2258, 2259, 2259, 2259, 2259, 2260, 2260, 2261, 2261, 2262, 2262, 2262, 2263, 2265, 2265, 2265, 2265, 2266, 2266, 2267, 2268, 2269, 2269, 2270, 2270, 2271, 2271, 2272, 2273, 2273, 2273, 2275, 2275, 2276, 2276, 2277, 2277, 2278, 2278, 2280, 2280, 2281, 2282, 2282, 2282, 2282, 2284, 2284, 2284, 2284, 2285, 2285, 2286, 2287, 2287, 2288, 2288, 2289, 2291, 2292, 2292, 2293, 2294, 2295, 2296, 2296, 2297, 2298, 2298, 2299, 2299, 2299, 2300, 2300, 2301, 2301, 2301, 2302, 2302, 2302, 2302, 2303, 2303, 2303, 2304, 2304, 2306, 2306, 2307, 2307, 2307, 2307, 2309, 2309, 2309, 2310, 2310, 2310, 2310, 2311, 2311, 2311, 2312, 2312, 2312, 2313, 2313, 2316, 2317, 2317, 2317, 2317, 2317, 2317, 2317, 2318, 2318, 2319, 2319, 2319, 2320, 2322, 2323, 2323, 2324, 2324, 2324, 2325, 2326, 2327, 2327, 2328, 2329, 2330, 2331, 2332, 2333, 2333, 2334, 2334, 2336, 2336, 2337, 2337, 2338, 2338, 2339, 2339, 2339, 2340, 2340, 2340, 2341, 2342, 2343, 2344, 2345, 2345, 2345, 2345, 2346, 2346, 2347, 2347, 2347, 2347, 2349, 2349, 2349, 2350, 2350, 2351, 2351, 2351, 2351, 2352, 2352, 2353, 2354, 2355, 2356, 2356, 2358, 2359, 2360, 2361, 2362, 2362, 2362, 2363, 2363, 2363, 2364, 2365, 2365, 2365, 2365, 2366, 2367, 2367, 2367, 2367, 2368, 2370, 2370, 2370, 2372, 2372, 2372, 2372, 2372, 2373, 2373, 2373, 2374, 2374, 2375, 2375, 2375, 2376, 2376, 2377, 2377, 2377, 2377, 2378, 2379, 2379, 2380, 2380, 2380, 2381, 2382, 2382, 2382, 2382, 2384, 2384, 2384, 2385, 2387, 2387, 2387, 2388, 2389, 2389, 2389, 2389, 2389, 2390, 2391, 2391, 2392, 2392, 2392, 2394, 2394, 2395, 2395, 2395, 2396, 2396, 2397, 2397, 2397, 2397, 2398, 2398, 2398, 2399, 2400, 2401, 2402, 2404, 2404, 2405, 2405, 2405, 2407, 2408, 2409, 2409, 2409, 2409, 2410, 2410, 2410, 2410, 2410, 2410, 2410, 2411, 2411, 2412, 2412, 2414, 2414, 2415, 2415, 2416, 2416, 2417, 2417, 2418, 2418, 2420, 2421, 2422, 2424, 2424, 2424, 2425, 2425, 2426, 2426, 2426, 2426, 2427, 2427, 2427, 2427, 2427, 2428, 2430, 2432, 2432, 2432, 2432, 2433, 2433, 2433, 2433, 2433, 2434, 2435, 2435, 2435, 2435, 2436, 2437, 2437, 2437, 2437, 2438, 2438, 2439, 2439, 2439, 2440, 2440, 2441, 2441, 2441, 2442, 2443, 2443, 2444, 2444, 2444, 2444, 2447, 2447, 2448, 2448, 2448, 2449, 2449, 2449, 2450, 2451, 2451, 2451, 2453, 2453, 2454, 2454, 2454, 2454, 2455, 2456, 2456, 2457, 2457, 2457, 2458, 2458, 2458, 2459, 2459, 2459, 2459, 2460, 2460, 2461, 2461, 2462, 2463, 2463, 2463, 2463, 2464, 2464, 2464, 2464, 2464, 2465, 2465, 2465, 2466, 2467, 2467, 2467, 2467, 2469, 2470, 2471, 2471, 2472, 2472, 2473, 2473, 2473, 2474, 2474, 2474, 2474, 2475, 2475, 2476, 2476, 2477, 2478, 2479, 2482, 2482, 2483, 2483, 2485, 2485, 2485, 2485, 2486, 2487, 2488, 2489, 2489, 2490, 2490, 2491, 2491, 2491, 2493, 2494, 2494, 2495, 2495, 2495, 2495, 2495, 2495, 2496, 2496, 2496, 2496, 2497, 2497, 2497, 2498, 2498, 2499, 2501, 2502, 2503, 2504, 2504, 2505, 2506, 2506, 2507, 2508, 2508, 2508, 2509, 2509, 2513, 2513, 2513, 2513, 2514, 2514, 2515, 2515, 2516, 2516, 2516, 2518, 2518, 2519, 2519, 2519, 2519, 2520, 2520, 2520, 2520, 2521, 2521, 2521, 2522, 2523, 2523, 2523, 2524, 2524, 2524, 2524, 2525, 2525, 2527, 2527, 2527, 2527, 2527, 2528, 2528, 2529, 2531, 2531, 2532, 2532, 2532, 2533, 2534, 2534, 2535, 2535, 2535, 2536, 2537, 2537, 2537, 2538, 2538, 2539, 2539, 2539, 2539, 2539, 2541, 2541, 2541, 2542, 2542, 2543, 2544, 2544, 2544, 2544, 2545, 2545, 2545, 2546, 2546, 2546, 2546, 2547, 2547, 2547, 2548, 2548, 2548, 2550, 2550, 2550, 2550, 2550, 2551, 2552, 2552, 2553, 2554, 2554, 2554, 2555, 2555, 2556, 2556, 2557, 2557, 2557, 2558, 2560, 2561, 2561, 2561, 2561, 2562, 2563, 2563, 2564, 2564, 2564, 2566, 2566, 2566, 2566, 2566, 2566, 2567, 2567, 2567, 2568, 2569, 2569, 2569, 2571, 2572, 2573, 2573, 2574, 2574, 2576, 2576, 2577, 2577, 2578, 2580, 2580, 2581, 2581, 2581, 2581, 2584, 2584, 2585, 2586, 2587, 2587, 2588, 2588, 2588, 2589, 2589, 2590, 2590, 2591, 2591, 2591, 2592, 2592, 2592, 2593, 2593, 2593, 2594, 2594, 2594, 2596, 2596, 2597, 2598, 2599, 2599, 2599, 2600, 2601, 2601, 2602, 2603, 2603, 2604, 2604, 2604, 2605, 2607, 2608, 2608, 2609, 2609, 2609, 2609, 2611, 2611, 2612, 2612, 2613, 2613, 2613, 2613, 2613, 2614, 2614, 2615, 2615, 2615, 2615, 2615, 2616, 2616, 2617, 2617, 2617, 2618, 2619, 2619, 2620, 2621, 2622, 2622, 2622, 2623, 2624, 2625, 2627, 2628, 2628, 2628, 2628, 2629, 2630, 2630, 2630, 2630, 2631, 2632, 2632, 2632, 2632, 2633, 2633, 2633, 2633, 2633, 2634, 2634, 2635, 2636, 2636, 2636, 2636, 2637, 2637, 2637, 2637, 2637, 2638, 2638, 2638, 2638, 2640, 2640, 2644, 2646, 2646, 2647, 2648, 2649, 2650, 2650, 2650, 2651, 2651, 2651, 2651, 2652, 2652, 2653, 2654, 2654, 2654, 2654, 2655, 2655, 2656, 2656, 2657, 2657, 2657, 2659, 2659, 2660, 2660, 2660, 2660, 2661, 2661, 2662, 2662, 2663, 2663, 2663, 2664, 2665, 2665, 2665, 2666, 2667, 2668, 2670, 2670, 2670, 2670, 2672, 2672, 2673, 2673, 2674, 2674, 2675, 2676, 2676, 2676, 2676, 2677, 2677, 2677, 2677, 2677, 2677, 2679, 2680, 2681, 2683, 2683, 2684, 2684, 2684, 2684, 2685, 2686, 2687, 2688, 2688, 2688, 2689, 2689, 2689, 2689, 2690, 2690, 2690, 2690, 2691, 2691, 2692, 2692, 2692, 2692, 2693, 2693, 2694, 2694, 2694, 2694, 2694, 2695, 2695, 2695, 2696, 2697, 2698, 2699, 2700, 2700, 2701, 2702, 2702, 2704, 2704, 2704, 2705, 2705, 2705, 2705, 2706, 2707, 2707, 2708, 2708, 2710, 2710, 2710, 2711, 2711, 2711, 2711, 2711, 2711, 2712, 2713, 2713, 2714, 2715, 2716, 2717, 2717, 2718, 2718, 2718, 2718, 2719, 2719, 2720, 2722, 2723, 2723, 2724, 2724, 2725, 2726, 2726, 2727, 2728, 2729, 2729, 2729, 2729, 2730, 2731, 2732, 2733, 2734, 2734, 2734, 2735, 2735, 2736, 2736, 2736, 2737, 2738, 2739, 2739, 2740, 2740, 2741, 2741, 2742, 2742, 2743, 2743, 2743, 2744, 2744, 2746, 2747, 2748, 2748, 2748, 2748, 2749, 2749, 2749, 2750, 2750, 2750, 2752, 2752, 2754, 2754, 2754, 2755, 2755, 2756, 2756, 2757, 2757, 2758, 2758, 2759, 2759, 2759, 2759, 2761, 2762, 2762, 2762, 2762, 2762, 2763, 2763, 2763, 2764, 2764, 2764, 2765, 2766, 2766, 2766, 2766, 2767, 2767, 2768, 2769, 2770, 2770, 2770, 2770, 2771, 2771, 2771, 2772, 2772, 2772, 2772, 2774, 2776, 2776, 2776, 2776, 2776, 2777, 2778, 2779, 2779, 2779, 2780, 2780, 2780, 2781, 2781, 2782, 2783, 2783, 2784, 2784, 2784, 2785, 2785, 2786, 2786, 2786, 2787, 2787, 2787, 2787, 2788, 2788, 2789, 2789, 2789, 2789, 2790, 2790, 2790, 2790, 2791, 2791, 2791, 2791, 2792, 2792, 2792, 2792, 2792, 2793, 2793, 2794, 2795, 2795, 2796, 2796, 2797, 2797, 2798, 2800, 2800, 2801, 2801, 2801, 2802, 2802, 2803, 2803, 2804, 2804, 2805, 2805, 2805, 2805, 2805, 2805, 2806, 2806, 2806, 2807, 2808, 2809, 2809, 2809, 2809, 2809, 2809, 2810, 2810, 2811, 2811, 2811, 2812, 2812, 2812, 2813, 2816, 2816, 2816, 2817, 2817, 2818, 2818, 2818, 2818, 2818, 2819, 2819, 2819, 2820, 2820, 2820, 2821, 2821, 2821, 2822, 2823, 2823, 2823, 2824, 2824, 2824, 2825, 2826, 2826, 2826, 2827, 2827, 2827, 2827, 2827, 2827, 2828, 2828, 2830, 2830, 2830, 2831, 2831, 2833, 2833, 2833, 2833, 2835, 2836, 2838, 2838, 2838, 2839, 2839, 2840, 2840, 2841, 2842, 2842, 2843, 2844, 2845, 2845, 2846, 2846, 2848, 2848, 2848, 2849, 2850, 2851, 2852, 2852, 2852, 2853, 2853, 2853, 2854, 2854, 2855, 2855, 2856, 2856, 2857, 2857, 2857, 2857, 2858, 2858, 2859, 2859, 2859, 2860, 2861, 2861, 2861, 2862, 2862, 2863, 2863, 2863, 2864, 2865, 2868, 2868, 2868, 2868, 2868, 2869, 2869, 2870, 2870, 2870, 2871, 2871, 2871, 2872, 2873, 2874, 2875, 2875, 2876, 2876, 2877, 2877, 2878, 2879, 2880, 2880, 2881, 2882, 2884, 2884, 2884, 2885, 2885, 2886, 2887, 2887, 2887, 2887, 2887, 2888, 2888, 2888, 2888, 2889, 2889, 2889, 2890, 2890, 2890, 2891, 2893, 2894, 2895, 2896, 2896, 2897, 2897, 2898, 2898, 2898, 2900, 2900, 2901, 2901, 2902, 2902, 2902, 2902, 2903, 2904, 2904, 2904, 2904, 2905, 2905, 2905, 2906, 2907, 2907, 2908, 2908, 2908, 2908, 2909, 2909, 2910, 2911, 2911, 2911, 2912, 2913, 2914, 2915, 2916, 2916, 2918, 2918, 2919, 2919, 2919, 2920, 2921, 2921, 2922, 2922, 2922, 2923, 2923, 2923, 2924, 2925, 2926, 2926, 2926, 2927, 2927, 2927, 2928, 2929, 2930, 2931, 2931, 2932, 2932, 2932, 2934, 2934, 2934, 2935, 2935, 2935, 2936, 2937, 2938, 2939, 2940, 2940, 2941, 2942, 2942, 2943, 2943, 2943, 2944, 2944, 2944, 2944, 2944, 2945, 2946, 2946, 2947, 2947, 2948, 2949, 2950, 2950, 2951, 2952, 2954, 2954, 2954, 2955, 2955, 2956, 2957, 2958, 2958, 2959, 2959, 2960, 2960, 2960, 2962, 2962, 2964, 2964, 2965, 2965, 2965, 2966, 2966, 2967, 2967, 2968, 2969, 2969, 2969, 2970, 2970, 2971, 2972, 2972, 2972, 2972, 2972, 2974, 2974, 2974, 2976, 2976, 2977, 2978, 2979, 2980, 2980, 2980, 2980, 2981, 2981, 2982, 2982, 2983, 2984, 2984, 2986, 2987, 2987, 2988, 2988, 2988, 2989, 2989, 2989, 2990, 2990, 2991, 2991, 2991, 2992, 2993, 2994, 2995, 2995, 2995, 2995, 2996, 2996, 2997, 2997, 2997, 2998, 2999, 2999, 2999, 2999, 2999, 2999, 3000, 3000, 3000, 3000, 3001, 3001, 3002, 3003, 3003, 3004, 3005, 3005, 3005, 3007, 3007, 3008, 3008, 3009, 3009, 3009, 3010, 3010, 3010, 3010, 3011, 3011, 3013, 3013, 3014, 3015, 3015, 3016, 3016, 3016, 3016, 3017, 3018, 3018, 3018, 3018, 3019, 3020, 3020, 3021, 3021, 3021, 3022, 3024, 3026, 3026, 3026, 3026, 3027, 3028, 3028, 3028, 3028, 3030, 3030, 3031, 3035, 3036, 3036, 3036, 3037, 3037, 3038, 3038, 3039, 3039, 3041, 3041, 3041, 3042, 3043, 3043, 3044, 3044, 3045, 3045, 3045, 3045, 3045, 3046, 3047, 3048, 3048, 3048, 3049, 3049, 3049, 3050, 3050, 3051, 3051, 3051, 3051, 3052, 3052, 3052, 3053, 3054, 3054, 3054, 3054, 3055, 3055, 3055, 3055, 3057, 3057, 3057, 3058, 3059, 3060, 3060, 3060, 3060, 3061, 3062, 3063, 3063, 3063, 3064, 3065, 3065, 3066, 3068, 3068, 3068, 3068, 3068, 3068, 3069, 3071, 3072, 3072, 3072, 3073, 3074, 3074, 3074, 3075, 3077, 3077, 3078, 3078, 3079, 3079, 3079, 3079, 3081, 3081, 3081, 3082, 3082, 3082, 3082, 3083, 3083, 3084, 3084, 3084, 3086, 3086, 3087, 3087, 3087, 3087, 3088, 3089, 3089, 3090, 3091, 3092, 3092, 3093, 3093, 3094, 3094, 3094, 3095, 3095, 3096, 3097, 3097, 3098, 3099, 3100, 3101, 3101, 3102, 3102, 3104, 3104, 3105, 3107, 3108, 3108, 3109, 3109, 3109, 3110, 3110, 3111, 3111, 3111, 3112, 3112, 3112, 3112, 3112, 3113, 3113, 3113, 3113, 3113, 3114, 3115, 3116, 3116, 3116, 3117, 3117, 3117, 3118, 3118, 3119, 3119, 3119, 3120, 3120, 3120, 3121, 3121, 3121, 3122, 3122, 3122, 3122, 3123, 3123, 3124, 3126, 3127, 3127, 3127, 3127, 3128, 3128, 3128, 3128, 3129, 3130, 3130, 3131, 3131, 3131, 3131, 3131, 3132, 3132, 3132, 3133, 3133, 3134, 3135, 3136, 3136, 3136, 3137, 3138, 3140, 3140, 3141, 3142, 3142, 3143, 3143, 3143, 3143, 3143, 3144, 3145, 3146, 3146, 3146, 3147, 3148, 3149, 3149, 3150, 3150, 3150, 3150, 3150, 3150, 3151, 3151, 3152, 3152, 3154, 3154, 3155, 3155, 3155, 3156, 3156, 3157, 3158, 3158, 3159, 3160, 3160, 3161, 3161, 3161, 3162, 3162, 3163, 3164, 3164, 3165, 3165, 3166, 3166, 3166, 3167, 3167, 3168, 3168, 3168, 3169, 3169, 3170, 3170, 3170, 3170, 3171, 3172, 3172, 3173, 3175, 3175, 3177, 3177, 3178, 3178, 3179, 3180, 3180, 3180, 3181, 3182, 3182, 3182, 3183, 3184, 3184, 3184, 3185, 3186, 3187, 3187, 3188, 3189, 3189, 3189, 3190, 3190, 3191, 3192, 3192, 3193, 3193, 3193, 3194, 3194, 3194, 3194, 3195, 3195, 3196, 3196, 3196, 3196, 3198, 3198, 3198, 3198, 3198, 3199, 3199, 3199, 3200, 3200, 3202, 3202, 3203, 3203, 3203, 3205, 3206, 3207, 3207, 3207, 3208, 3208, 3208, 3208, 3209, 3209, 3210, 3210, 3211, 3211, 3211, 3212, 3212, 3213, 3213, 3213, 3214, 3214, 3215, 3216, 3216, 3217, 3218, 3218, 3219, 3219, 3220, 3222, 3223, 3223, 3223, 3224, 3224, 3224, 3224, 3225, 3225, 3225, 3225, 3226, 3227, 3228, 3228, 3228, 3228, 3228, 3228, 3229, 3230, 3230, 3231, 3233, 3234, 3234, 3234, 3235, 3235, 3236, 3236, 3237, 3237, 3239, 3239, 3239, 3240, 3240, 3241, 3241, 3241, 3241, 3243, 3243, 3243, 3243, 3243, 3243, 3243, 3243, 3245, 3245, 3246, 3246, 3246, 3247, 3247, 3247, 3247, 3248, 3248, 3249, 3250, 3250, 3251, 3251, 3252, 3252, 3253, 3253, 3254, 3254, 3255, 3256, 3257, 3257, 3257, 3259, 3259, 3260, 3260, 3261, 3262, 3263, 3263, 3263, 3264, 3266, 3266, 3266, 3267, 3267, 3267, 3267, 3267, 3268, 3268, 3268, 3269, 3269, 3269, 3270, 3270, 3270, 3270, 3271, 3272, 3272, 3272, 3272, 3273, 3273, 3273, 3274, 3274, 3275, 3275, 3276, 3276, 3276, 3278, 3278, 3279, 3280, 3280, 3280, 3280, 3281, 3282, 3284, 3284, 3284, 3285, 3285, 3285, 3285, 3286, 3286, 3287, 3288, 3288, 3289, 3289, 3289, 3289, 3290, 3292, 3292, 3292, 3293, 3293, 3293, 3293, 3294, 3294, 3297, 3297, 3298, 3299, 3301, 3301, 3302, 3302, 3302, 3302, 3303, 3304, 3305, 3305, 3305, 3305, 3306, 3306, 3306, 3306, 3306, 3306, 3308, 3308, 3308, 3308, 3309, 3309, 3310, 3310, 3311, 3311, 3311, 3311, 3312, 3313, 3313, 3313, 3314, 3314, 3315, 3315, 3316, 3318, 3320, 3320, 3321, 3321, 3321, 3322, 3322, 3323, 3323, 3323, 3324, 3324, 3327, 3329, 3329, 3330, 3330, 3330, 3331, 3331, 3331, 3331, 3331, 3333, 3334, 3335, 3336, 3336, 3336, 3337, 3337, 3337, 3338, 3338, 3339, 3339, 3339, 3339, 3340, 3340, 3340, 3340, 3341, 3341, 3341, 3344, 3345, 3345, 3346, 3347, 3347, 3347, 3347, 3347, 3348, 3348, 3348, 3348, 3349, 3349, 3349, 3350, 3350, 3351, 3351, 3352, 3352, 3352, 3352, 3353, 3354, 3357, 3358, 3358, 3358, 3358, 3359, 3359, 3359, 3360, 3360, 3361, 3361, 3361, 3362, 3363, 3363, 3363, 3365, 3365, 3367, 3367, 3367, 3368, 3369, 3369, 3369, 3370, 3370, 3371, 3372, 3372, 3373, 3374, 3374, 3377, 3377, 3377, 3377, 3378, 3379, 3379, 3380, 3380, 3381, 3381, 3382, 3383, 3383, 3383, 3384, 3384, 3385, 3385, 3385, 3386, 3386, 3387, 3387, 3388, 3388, 3388, 3389, 3389, 3389, 3390, 3392, 3393, 3394, 3394, 3394, 3395, 3396, 3397, 3397, 3397, 3398, 3398, 3398, 3398, 3399, 3399, 3400, 3400, 3400, 3401, 3401, 3402, 3402, 3402, 3402, 3403, 3403, 3405, 3405, 3405, 3405, 3405, 3406, 3407, 3407, 3408, 3410, 3410, 3411, 3411, 3411, 3412, 3412, 3412, 3413, 3414, 3414, 3414, 3414, 3415, 3415, 3417, 3419, 3419, 3420, 3420, 3420, 3421, 3421, 3421, 3422, 3422, 3423, 3423, 3423, 3423, 3424, 3425, 3425, 3425, 3426, 3427, 3427, 3428, 3428, 3429, 3429, 3430, 3431, 3431, 3431, 3432, 3432, 3432, 3434, 3435, 3435, 3435, 3436, 3437, 3438, 3438, 3438, 3439, 3439, 3439, 3440, 3440, 3441, 3441, 3442, 3443, 3443, 3443, 3444, 3444, 3444, 3445, 3445, 3445, 3446, 3446, 3447, 3447, 3447, 3448, 3448, 3449, 3449, 3449, 3450, 3450, 3450, 3451, 3452, 3452, 3453, 3453, 3454, 3454, 3454, 3454, 3455, 3456, 3456, 3456, 3457, 3457, 3460, 3461, 3461, 3461, 3462, 3462, 3462, 3463, 3463, 3463, 3463, 3463, 3464, 3464, 3464, 3466, 3467, 3467, 3467, 3468, 3468, 3469, 3470, 3471, 3472, 3473, 3473, 3473, 3474, 3475, 3475, 3475, 3476, 3476, 3476, 3478, 3479, 3479, 3480, 3481, 3481, 3481, 3482, 3483, 3484, 3484, 3485, 3485, 3486, 3486, 3486, 3486, 3487, 3487, 3487, 3487, 3489, 3489, 3490, 3490, 3490, 3491, 3491, 3491, 3492, 3492, 3493, 3493, 3494, 3494, 3494, 3495, 3495, 3495, 3495, 3495, 3495, 3495, 3496, 3497, 3497, 3498, 3498, 3499, 3499, 3499, 3499, 3500, 3501, 3501, 3503, 3503, 3503, 3504, 3504, 3504, 3504, 3504, 3505, 3505, 3505, 3506, 3507, 3508, 3508, 3508, 3511, 3511, 3511, 3511, 3511, 3511, 3511, 3512, 3512, 3512, 3512, 3513, 3514, 3514, 3514, 3515, 3515, 3516, 3517, 3517, 3518, 3518, 3518, 3518, 3519, 3520, 3520, 3520, 3520, 3521, 3521, 3521, 3521, 3521, 3524, 3525, 3527, 3528, 3528, 3530, 3530, 3531, 3532, 3532, 3533, 3534, 3534, 3534, 3535, 3535, 3535, 3535, 3536, 3537, 3537, 3538, 3539, 3539, 3539, 3539, 3540, 3540, 3540, 3541, 3541, 3541, 3543, 3544, 3544, 3547, 3548, 3548, 3549, 3549, 3550, 3551, 3551, 3551, 3551, 3552, 3553, 3553, 3553, 3553, 3554, 3554, 3554, 3554, 3555, 3555, 3556, 3556, 3557, 3558, 3558, 3558, 3558, 3559, 3559, 3560, 3560, 3560, 3561, 3561, 3562, 3562, 3563, 3565, 3566, 3566, 3566, 3566, 3567, 3567, 3567, 3567, 3568, 3569, 3569, 3570, 3570, 3571, 3572, 3572, 3573, 3573, 3573, 3574, 3574, 3575, 3575, 3576, 3577, 3578, 3579, 3581, 3581, 3582, 3582, 3582, 3583, 3583, 3583, 3583, 3583, 3584, 3584, 3585, 3586, 3586, 3587, 3587, 3588, 3588, 3588, 3589, 3591, 3591, 3593, 3594, 3594, 3595, 3596, 3596, 3597, 3599, 3599, 3599, 3600, 3600, 3600, 3601, 3601, 3602, 3602, 3602, 3603, 3604, 3605, 3607, 3608, 3609, 3609, 3609, 3609, 3610, 3610, 3611, 3612, 3612, 3613, 3614, 3614, 3615, 3615, 3615, 3615, 3615, 3616, 3617, 3617, 3617, 3617, 3619, 3619, 3619, 3621, 3621, 3621, 3622, 3623, 3624, 3624, 3625, 3627, 3628, 3628, 3628, 3628, 3629, 3630, 3630, 3630, 3631, 3631, 3631, 3631, 3632, 3633, 3633, 3633, 3634, 3634, 3634, 3636, 3637, 3638, 3638, 3638, 3639, 3639, 3639, 3639, 3641, 3642, 3642, 3642, 3643, 3643, 3643, 3643, 3644, 3644, 3645, 3646, 3646, 3647, 3647, 3647, 3647, 3648, 3648, 3649, 3649, 3650, 3650, 3651, 3652, 3652, 3653, 3653, 3654, 3655, 3656, 3656, 3657, 3658, 3659, 3660, 3661, 3662, 3663, 3664, 3664, 3664, 3665, 3666, 3667, 3667, 3668, 3669, 3669, 3669, 3670, 3670, 3671, 3671, 3672, 3672, 3673, 3677, 3678, 3678, 3678, 3678, 3679, 3679, 3679, 3681, 3681, 3681, 3682, 3682, 3683, 3683, 3684, 3684, 3685, 3685, 3685, 3687, 3687, 3687, 3688, 3688, 3688, 3688, 3688, 3689, 3690, 3690, 3690, 3693, 3693, 3694, 3694, 3695, 3695, 3696, 3698, 3698, 3699, 3699, 3700, 3702, 3703, 3704, 3705, 3705, 3705, 3705, 3706, 3706, 3706, 3706, 3706, 3707, 3707, 3707, 3708, 3708, 3710, 3710, 3710, 3711, 3712, 3713, 3713, 3713, 3713, 3714, 3714, 3714, 3715, 3715, 3716, 3716, 3717, 3717, 3717, 3717, 3718, 3718, 3718, 3718, 3719, 3719, 3719, 3720, 3720, 3721, 3721, 3722, 3722, 3722, 3722, 3722, 3723, 3724, 3725, 3726, 3727, 3727, 3728, 3728, 3729, 3729, 3731, 3731, 3731, 3731, 3731, 3732, 3734, 3734, 3734, 3734, 3735, 3735, 3736, 3736, 3736, 3736, 3737, 3738, 3739, 3739, 3739, 3740, 3740, 3740, 3741, 3741, 3741, 3742, 3742, 3743, 3744, 3744, 3744, 3745, 3745, 3745, 3746, 3746, 3747, 3747, 3747, 3748, 3748, 3749, 3751, 3751, 3751, 3751, 3751, 3752, 3753, 3753, 3753, 3753, 3754, 3755, 3756, 3757, 3757, 3758, 3758, 3758, 3759, 3759, 3759, 3762, 3763, 3763, 3763, 3763, 3764, 3765, 3765, 3766, 3766, 3766, 3766, 3767, 3767, 3768, 3768, 3769, 3769, 3770, 3770, 3770, 3770, 3771, 3771, 3772, 3772, 3773, 3773, 3774, 3775, 3775, 3776, 3776, 3776, 3776, 3776, 3777, 3777, 3779, 3779, 3779, 3779, 3780, 3780, 3781, 3781, 3782, 3783, 3783, 3784, 3785, 3785, 3787, 3787, 3787, 3788, 3788, 3788, 3788, 3789, 3789, 3790, 3790, 3791, 3792, 3792, 3792, 3793, 3793, 3794, 3794, 3795, 3795, 3796, 3797, 3797, 3797, 3797, 3798, 3798, 3799, 3800, 3800, 3800, 3800, 3801, 3801, 3801, 3802, 3802, 3802, 3802, 3803, 3804, 3805, 3806, 3806, 3807, 3808, 3808, 3809, 3809, 3811, 3813, 3814, 3814, 3816, 3816, 3816, 3817, 3818, 3819, 3820, 3820, 3821, 3821, 3821, 3822, 3822, 3822, 3825, 3825, 3825, 3825, 3826, 3828, 3828, 3828, 3829, 3830, 3830, 3830, 3830, 3831, 3831, 3831, 3832, 3832, 3833, 3833, 3833, 3833, 3834, 3835, 3835, 3836, 3837, 3837, 3837, 3837, 3838, 3838, 3838, 3839, 3841, 3841, 3842, 3842, 3842, 3842, 3843, 3843, 3843, 3843, 3843, 3844, 3844, 3844, 3845, 3846, 3847, 3847, 3848, 3849, 3850, 3850, 3851, 3851, 3851, 3854, 3854, 3854, 3855, 3855, 3856, 3857, 3858, 3858, 3858, 3859, 3859, 3859, 3859, 3860, 3860, 3861, 3861, 3861, 3861, 3862, 3862, 3862, 3862, 3863, 3863, 3865, 3865, 3865, 3865, 3866, 3866, 3867, 3867, 3867, 3867, 3868, 3868, 3869, 3869, 3870, 3871, 3871, 3871, 3872, 3873, 3873, 3873, 3874, 3874, 3874, 3875, 3875, 3876, 3877, 3878, 3878, 3878, 3879, 3879, 3879, 3880, 3880, 3881, 3881, 3881, 3881, 3883, 3883, 3884, 3884, 3884, 3884, 3884, 3886, 3887, 3887, 3887, 3887, 3888, 3888, 3889, 3890, 3890, 3891, 3891, 3891, 3891, 3892, 3892, 3892, 3892, 3893, 3893, 3893, 3893, 3894, 3894, 3894, 3895, 3895, 3895, 3895, 3897, 3897, 3897, 3899, 3899, 3900, 3901, 3902, 3904, 3904, 3905, 3905, 3906, 3906, 3906, 3907, 3907, 3907, 3908, 3909, 3910, 3911, 3911, 3912, 3913, 3914, 3915, 3915, 3915, 3915, 3916, 3917, 3917, 3917, 3919, 3919, 3919, 3920, 3921, 3921, 3922, 3922, 3923, 3923, 3923, 3924, 3924, 3925, 3925, 3926, 3926, 3926, 3928, 3928, 3928, 3929, 3929, 3930, 3930, 3930, 3931, 3931, 3931, 3932, 3932, 3932, 3932, 3932, 3933, 3933, 3933, 3934, 3934, 3934, 3935, 3935, 3935, 3935, 3936, 3937, 3937, 3937, 3938, 3938, 3939, 3942, 3942, 3943, 3943, 3943, 3945, 3945, 3945, 3946, 3947, 3947, 3948, 3948, 3948, 3948, 3948, 3951, 3952, 3952, 3952, 3952, 3953, 3954, 3954, 3956, 3957, 3957, 3957, 3957, 3958, 3958, 3958, 3959, 3960, 3961, 3961, 3961, 3962, 3963, 3964, 3964, 3964, 3965, 3965, 3965, 3965, 3967, 3968, 3969, 3969, 3970, 3970, 3971, 3972, 3972, 3973, 3973, 3974, 3974, 3975, 3975, 3976, 3976, 3977, 3977, 3977, 3977, 3978, 3978, 3979, 3979, 3979, 3979, 3979, 3980, 3980, 3981, 3981, 3981, 3981, 3982, 3982, 3982, 3982, 3983, 3984, 3984, 3984, 3984, 3984, 3984, 3986, 3986, 3986, 3987, 3988, 3988, 3988, 3988, 3989, 3989, 3989, 3990, 3990, 3991, 3992, 3993, 3994, 3995, 3996, 3996, 3998, 3998, 3998, 3999, 4000, 4000, 4000, 4001, 4001, 4001, 4001, 4002, 4002, 4002, 4002, 4003, 4004, 4004, 4004, 4005, 4006, 4006, 4007, 4007, 4008, 4008, 4008, 4009, 4010, 4010, 4010, 4010, 4011, 4011, 4013, 4014, 4015, 4016, 4017, 4018, 4018, 4019, 4020, 4020, 4020, 4021, 4021, 4022, 4022, 4022, 4023, 4023, 4023, 4024, 4024, 4025, 4025, 4025, 4026, 4026, 4027, 4027, 4028, 4028, 4028, 4029, 4030, 4031, 4031, 4031, 4031, 4032, 4032, 4032, 4032, 4033, 4033, 4033, 4033, 4035, 4035, 4035, 4035, 4035, 4037, 4038, 4038, 4038, 4038, 4038, 4039, 4039, 4039, 4040, 4040, 4040, 4041, 4041, 4041, 4041, 4041, 4041, 4041, 4042, 4042, 4043, 4043, 4043, 4043, 4044, 4044, 4045, 4045, 4045, 4047, 4047, 4048, 4048, 4049, 4050, 4050, 4050, 4051, 4052, 4052, 4053, 4053, 4054, 4055, 4055, 4056, 4056, 4057, 4058, 4058, 4059, 4059, 4060, 4060, 4060, 4061, 4061, 4061, 4062, 4063, 4063, 4064, 4065, 4065, 4065, 4066, 4067, 4068, 4068, 4068, 4069, 4069, 4069, 4070, 4070, 4070, 4071, 4071, 4072, 4072, 4072, 4072, 4072, 4073, 4073, 4074, 4074, 4075, 4076, 4076, 4076, 4076, 4077, 4077, 4078, 4078, 4078, 4080, 4081, 4081, 4082, 4082, 4082, 4083, 4083, 4085, 4085, 4085, 4085, 4085, 4086, 4086, 4086, 4087, 4087, 4087, 4088, 4088, 4089, 4089, 4090, 4090, 4091, 4091, 4092, 4093, 4093, 4093, 4094, 4095, 4096, 4096, 4096, 4097, 4097, 4098, 4099, 4099, 4099, 4099, 4100, 4100, 4101, 4101, 4102, 4102, 4103, 4104, 4104, 4104, 4104, 4104, 4105, 4106, 4106, 4106, 4106, 4107, 4108, 4108, 4109, 4109, 4109, 4109, 4110, 4111, 4112, 4112, 4112, 4112, 4112, 4113, 4113, 4114, 4114, 4114, 4115, 4115, 4116, 4116, 4117, 4117, 4117, 4118, 4118, 4118, 4119, 4119, 4121, 4121, 4122, 4122, 4122, 4123, 4124, 4125, 4125, 4126, 4127, 4129, 4129, 4130, 4131, 4131, 4132, 4132, 4132, 4134, 4134, 4134, 4135, 4135, 4135, 4135, 4135, 4135, 4135, 4135, 4136, 4136, 4136, 4136, 4136, 4136, 4137, 4137, 4137, 4139, 4140, 4140, 4140, 4141, 4141, 4142, 4142, 4142, 4143, 4144, 4144, 4144, 4144, 4145, 4145, 4145, 4145, 4146, 4147, 4147, 4147, 4148, 4148, 4148, 4149, 4149, 4149, 4149, 4149, 4150, 4150, 4151, 4153, 4153, 4154, 4155, 4155, 4156, 4156, 4156, 4157, 4158, 4158, 4159, 4159, 4160, 4160, 4161, 4161, 4161, 4161, 4163, 4163, 4163, 4164, 4164, 4164, 4164, 4165, 4165, 4165, 4166, 4166, 4166, 4167, 4168, 4169, 4170, 4170, 4170, 4171, 4171, 4172, 4173, 4173, 4173, 4173, 4174, 4175, 4175, 4176, 4176, 4176, 4177, 4177, 4177, 4177, 4178, 4178, 4179, 4179, 4179, 4179, 4179, 4179, 4179, 4180, 4180, 4180, 4181, 4181, 4181, 4181, 4182, 4182, 4183, 4184, 4185, 4186, 4187, 4187, 4187, 4188, 4188, 4188, 4189, 4189, 4189, 4189, 4190, 4190, 4190, 4190, 4190, 4191, 4192, 4192, 4192, 4192, 4194, 4195, 4196, 4196, 4196, 4196, 4197, 4197, 4198, 4198, 4198, 4198, 4200, 4200, 4201, 4202, 4202, 4203, 4203, 4204, 4205, 4205, 4207, 4210, 4210, 4210, 4210, 4211, 4211, 4213, 4214, 4214, 4215, 4215, 4215, 4216, 4216, 4216, 4217, 4217, 4218, 4218, 4218, 4218, 4218, 4219, 4220, 4220, 4220, 4221, 4222, 4223, 4223, 4223, 4225, 4225, 4226, 4226, 4227, 4227, 4228, 4228, 4228, 4228, 4229, 4230, 4230, 4232, 4233, 4233, 4233, 4233, 4234, 4235, 4235, 4235, 4235, 4236, 4236, 4236, 4237, 4238, 4238, 4238, 4238, 4238, 4239, 4239, 4239, 4239, 4239, 4240, 4240, 4241, 4243, 4243, 4243, 4243, 4243, 4243, 4244, 4244, 4244, 4244, 4244, 4245, 4245, 4245, 4247, 4247, 4247, 4248, 4248, 4249, 4249, 4249, 4250, 4250, 4250, 4251, 4251, 4252, 4252, 4253, 4253, 4253, 4253, 4253, 4254, 4255, 4255, 4255, 4256, 4256, 4257, 4257, 4257, 4258, 4258, 4258, 4258, 4259, 4259, 4260, 4260, 4261, 4261, 4261, 4261, 4261, 4261, 4262, 4262, 4262, 4263, 4264, 4265, 4265, 4265, 4266, 4267, 4268, 4269, 4269, 4269, 4270, 4270, 4271, 4271, 4271, 4272, 4272, 4273, 4273, 4273, 4273, 4274, 4274, 4274, 4275, 4275, 4277, 4279, 4279, 4280, 4280, 4280, 4281, 4281, 4282, 4282, 4282, 4282, 4282, 4283, 4283, 4283, 4283, 4284, 4285, 4285, 4285, 4285, 4285, 4286, 4286, 4287, 4287, 4287, 4287, 4288, 4289, 4290, 4290, 4291, 4291, 4291, 4292, 4293, 4293, 4293, 4293, 4294, 4294, 4295, 4296, 4297, 4297, 4297, 4297, 4297, 4297, 4298, 4299, 4299, 4299, 4299, 4300, 4300, 4301, 4301, 4302, 4302, 4302, 4302, 4303, 4304, 4305, 4306, 4306, 4306, 4307, 4307, 4308, 4308, 4309, 4309, 4309, 4309, 4310, 4311, 4311, 4312, 4312, 4312, 4312, 4312, 4313, 4314, 4315, 4315, 4316, 4316, 4317, 4317, 4317, 4318, 4318, 4319, 4319, 4320, 4320, 4321, 4322, 4322, 4323, 4325, 4327, 4327, 4327, 4327, 4327, 4328, 4328, 4330, 4330, 4330, 4331, 4331, 4332, 4332, 4334, 4335, 4335, 4336, 4336, 4337, 4337, 4338, 4338, 4338, 4338, 4339, 4339, 4339, 4340, 4340, 4340, 4340, 4340, 4340, 4341, 4341, 4341, 4343, 4343, 4344, 4344, 4344, 4345, 4346, 4347, 4347, 4348, 4348, 4348, 4352, 4353, 4355, 4356, 4356, 4357, 4357, 4358, 4358, 4358, 4358, 4359, 4360, 4360, 4360, 4361, 4361, 4361, 4361, 4362, 4362, 4363, 4363, 4364, 4365, 4365, 4365, 4368, 4370, 4370, 4370, 4371, 4371, 4371, 4372, 4372, 4372, 4372, 4374, 4374, 4374, 4375, 4375, 4376, 4376, 4377, 4377, 4380, 4380, 4381, 4381, 4381, 4382, 4382, 4384, 4386, 4386, 4387, 4387, 4389, 4389, 4389, 4389, 4389, 4389, 4390, 4391, 4391, 4392, 4393, 4393, 4394, 4394, 4394, 4394, 4395, 4395, 4396, 4398, 4398, 4398, 4399, 4400, 4400, 4400, 4401, 4401, 4402, 4403, 4403, 4403, 4404, 4405, 4405, 4405, 4407, 4408, 4408, 4408, 4409, 4410, 4410, 4410, 4410, 4411, 4411, 4412, 4412, 4413, 4413, 4414, 4414, 4414, 4414, 4414, 4415, 4415, 4419, 4419, 4419, 4419, 4420, 4420, 4420, 4421, 4421, 4421, 4421, 4423, 4424, 4425, 4426, 4427, 4427, 4428, 4429, 4429, 4430, 4430, 4430, 4431, 4431, 4431, 4431, 4432, 4432, 4432, 4432, 4432, 4432, 4433, 4433, 4434, 4434, 4435, 4435, 4435, 4435, 4436, 4436, 4436, 4436, 4437, 4437, 4438, 4438, 4438, 4438, 4438, 4439, 4439, 4440, 4440, 4441, 4441, 4442, 4443, 4444, 4444, 4446, 4446, 4447, 4447, 4447, 4448, 4448, 4448, 4449, 4450, 4451, 4452, 4453, 4453, 4454, 4454, 4455, 4455, 4455, 4456, 4456, 4456, 4457, 4457, 4457, 4457, 4457, 4457, 4457, 4457, 4459, 4460, 4460, 4461, 4462, 4462, 4462, 4462, 4465, 4465, 4466, 4467, 4468, 4468, 4469, 4470, 4470, 4471, 4471, 4471, 4471, 4471, 4471, 4471, 4471, 4472, 4472, 4472, 4473, 4473, 4474, 4474, 4474, 4475, 4475, 4476, 4477, 4479, 4479, 4479, 4479, 4481, 4481, 4481, 4481, 4482, 4482, 4482, 4483, 4483, 4484, 4484, 4486, 4487, 4487, 4487, 4487, 4487, 4488, 4488, 4488, 4489, 4491, 4491, 4492, 4492, 4493, 4493, 4494, 4494, 4494, 4494, 4494, 4495, 4495, 4495, 4496, 4496, 4496, 4497, 4498, 4499, 4500, 4500, 4501, 4501, 4503, 4503, 4503, 4504, 4504, 4506, 4509, 4509, 4509, 4509, 4510, 4510, 4511, 4511, 4511, 4512, 4513, 4513, 4514, 4514, 4514, 4515, 4517, 4518, 4521, 4521, 4521, 4521, 4522, 4523, 4523, 4524, 4524, 4525, 4525, 4525, 4525, 4525, 4526, 4526, 4527, 4527, 4528, 4528, 4528, 4529, 4529, 4529, 4529, 4530, 4531, 4532, 4533, 4533, 4534, 4535, 4536, 4536, 4536, 4536, 4537, 4537, 4538, 4539, 4539, 4542, 4542, 4542, 4543, 4543, 4543, 4544, 4544, 4546, 4547, 4547, 4548, 4548, 4549, 4549, 4550, 4550, 4551, 4552, 4552, 4552, 4553, 4553, 4554, 4554, 4554, 4554, 4554, 4555, 4555, 4556, 4556, 4557, 4557, 4558, 4558, 4559, 4559, 4559, 4560, 4560, 4562, 4563, 4563, 4564, 4565, 4566, 4566, 4566, 4567, 4567, 4567, 4567, 4567, 4568, 4568, 4568, 4569, 4569, 4570, 4571, 4572, 4572, 4572, 4572, 4573, 4574, 4574, 4574, 4575, 4575, 4575, 4575, 4575, 4575, 4576, 4576, 4577, 4577, 4578, 4578, 4578, 4579, 4579, 4579, 4579, 4580, 4580, 4580, 4580, 4580, 4581, 4581, 4582, 4583, 4584, 4584, 4586, 4586, 4587, 4588, 4589, 4590, 4590, 4592, 4592, 4592, 4593, 4594, 4594, 4594, 4595, 4595, 4595, 4596, 4597, 4597, 4597, 4598, 4598, 4600, 4600, 4600, 4600, 4601, 4601, 4602, 4602, 4602, 4603, 4604, 4604, 4605, 4605, 4605, 4606, 4607, 4608, 4608, 4608, 4609, 4609, 4609, 4610, 4611, 4611, 4612, 4612, 4614, 4614, 4614, 4614, 4615, 4615, 4616, 4616, 4616, 4616, 4617, 4617, 4617, 4617, 4618, 4618, 4618, 4618, 4620, 4621, 4621, 4621, 4622, 4623, 4623, 4623, 4624, 4624, 4625, 4625, 4626, 4626, 4627, 4627, 4627, 4629, 4629, 4630, 4630, 4631, 4631, 4631, 4631, 4631, 4631, 4632, 4633, 4634, 4634, 4634, 4635, 4635, 4635, 4635, 4636, 4636, 4636, 4636, 4637, 4637, 4638, 4639, 4639, 4640, 4640, 4640, 4641, 4641, 4642, 4643, 4643, 4643, 4644, 4644, 4645, 4646, 4646, 4647, 4648, 4649, 4649, 4649, 4649, 4651, 4651, 4653, 4654, 4655, 4655, 4656, 4656, 4657, 4658, 4658, 4658, 4659, 4659, 4659, 4659, 4659, 4660, 4661, 4662, 4662, 4663, 4663, 4664, 4664, 4665, 4665, 4666, 4666, 4666, 4667, 4667, 4668, 4669, 4669, 4669, 4669, 4670, 4670, 4670, 4671, 4673, 4673, 4674, 4674, 4674, 4674, 4675, 4675, 4675, 4676, 4677, 4678, 4678, 4679, 4679, 4679, 4679, 4680, 4680, 4681, 4681, 4683, 4683, 4683, 4683, 4684, 4684, 4685, 4685, 4686, 4686, 4687, 4687, 4688, 4690, 4690, 4690, 4690, 4691, 4691, 4693, 4693, 4693, 4693, 4693, 4695, 4695, 4697, 4697, 4698, 4699, 4699, 4700, 4700, 4700, 4701, 4701, 4701, 4702, 4703, 4703, 4704, 4704, 4704, 4705, 4705, 4705, 4706, 4707, 4707, 4707, 4708, 4708, 4709, 4709, 4710, 4710, 4710, 4711, 4711, 4712, 4712, 4714, 4715, 4716, 4716, 4717, 4718, 4718, 4718, 4718, 4719, 4719, 4720, 4720, 4720, 4720, 4721, 4721, 4721, 4722, 4722, 4725, 4725, 4726, 4726, 4727, 4728, 4728, 4728, 4728, 4728, 4729, 4729, 4730, 4730, 4731, 4731, 4732, 4732, 4733, 4733, 4733, 4733, 4733, 4734, 4734, 4734, 4734, 4735, 4735, 4735, 4736, 4737, 4738, 4738, 4738, 4738, 4738, 4739, 4739, 4740, 4740, 4741, 4741, 4743, 4743, 4743, 4744, 4744, 4744, 4744, 4744, 4746, 4746, 4746, 4746, 4747, 4747, 4747, 4748, 4748, 4748, 4749, 4749, 4749, 4750, 4751, 4751, 4751, 4752, 4753, 4753, 4753, 4755, 4755, 4756, 4756, 4757, 4757, 4757, 4758, 4758, 4760, 4760, 4760, 4761, 4761, 4762, 4762, 4762, 4763, 4764, 4764, 4765, 4766, 4767, 4767, 4768, 4768, 4769, 4770, 4771, 4771, 4771, 4773, 4774, 4774, 4774, 4774, 4775, 4778, 4779, 4780, 4780, 4780, 4781, 4781, 4782, 4782, 4782, 4783, 4785, 4785, 4786, 4787, 4787, 4787, 4787, 4788, 4788, 4788, 4788, 4788, 4789, 4790, 4790, 4790, 4791, 4791, 4791, 4792, 4792, 4792, 4792, 4792, 4792, 4793, 4794, 4794, 4796, 4796, 4796, 4796, 4797, 4798, 4798, 4799, 4799, 4799, 4799, 4801, 4801, 4802, 4802, 4802, 4803, 4805, 4805, 4808, 4808, 4808, 4810, 4810, 4810, 4811, 4811, 4811, 4811, 4812, 4812, 4813, 4814, 4815, 4815, 4816, 4816, 4816, 4816, 4816, 4817, 4817, 4817, 4818, 4818, 4818, 4819, 4819, 4820, 4822, 4822, 4822, 4822, 4822, 4822, 4823, 4823, 4823, 4824, 4824, 4825, 4826, 4826, 4827, 4827, 4828, 4828, 4828, 4829, 4829, 4830, 4830, 4830, 4831, 4831, 4831, 4832, 4832, 4833, 4834, 4834, 4834, 4834, 4835, 4835, 4835, 4836, 4837, 4838, 4838, 4838, 4838, 4838, 4838, 4839, 4839, 4839, 4839, 4840, 4840, 4841, 4842, 4842, 4842, 4843, 4843, 4843, 4843, 4843, 4844, 4844, 4845, 4846, 4846, 4847, 4847, 4847, 4847, 4847, 4848, 4848, 4849, 4849, 4849, 4849, 4849, 4850, 4850, 4851, 4853, 4853, 4853, 4854, 4854, 4856, 4856, 4857, 4857, 4857, 4858, 4858, 4859, 4859, 4859, 4859, 4860, 4860, 4861, 4862, 4862, 4863, 4863, 4863, 4863, 4864, 4864, 4864, 4864, 4865, 4865, 4866, 4866, 4867, 4867, 4869, 4870, 4870, 4870, 4870, 4870, 4870, 4871, 4871, 4871, 4872, 4873, 4873, 4874, 4874, 4875, 4875, 4876, 4876, 4876, 4876, 4877, 4879, 4879, 4879, 4881, 4882, 4882, 4883, 4883, 4883, 4884, 4884, 4886, 4888, 4888, 4888, 4889, 4890, 4890, 4890, 4891, 4891, 4892, 4892, 4892, 4892, 4893, 4893, 4893, 4894, 4894, 4894, 4894, 4894, 4894, 4895, 4898, 4899, 4899, 4900, 4901, 4901, 4901, 4901, 4902, 4902, 4903, 4904, 4904, 4904, 4904, 4904, 4905, 4906, 4908, 4908, 4909, 4910, 4910, 4911, 4911, 4912, 4912, 4913, 4913, 4914, 4914, 4914, 4915, 4915, 4916, 4917, 4917, 4918, 4918, 4920, 4921, 4921, 4921, 4921, 4922, 4922, 4922, 4922, 4923, 4923, 4924, 4924, 4924, 4925, 4926, 4926, 4926, 4927, 4928, 4928, 4928, 4928, 4928, 4928, 4929, 4930, 4930, 4931, 4932, 4934, 4934, 4935, 4935, 4936, 4936, 4937, 4937, 4937, 4937, 4937, 4938, 4939, 4939, 4939, 4939, 4939, 4939, 4942, 4943, 4943, 4944, 4944, 4944, 4944, 4944, 4945, 4946, 4946, 4946, 4947, 4948, 4948, 4948, 4949, 4949, 4950, 4950, 4950, 4950, 4951, 4951, 4952, 4956, 4956, 4957, 4957, 4957, 4957, 4958, 4960, 4960, 4960, 4961, 4961, 4961, 4962, 4962, 4962, 4963, 4963, 4964, 4964, 4964, 4965, 4966, 4967, 4967, 4968, 4968, 4968, 4971, 4971, 4972, 4972, 4974, 4975, 4975, 4975, 4976, 4976, 4977, 4978, 4978, 4979, 4979, 4979, 4980, 4980, 4980, 4981, 4981, 4981, 4981, 4982, 4982, 4982, 4982, 4982, 4983, 4983, 4983, 4983, 4984, 4985, 4985, 4986, 4986, 4986, 4986, 4987, 4987, 4988, 4989, 4989, 4989, 4990, 4991, 4992, 4992, 4993, 4993, 4994, 4995, 4995, 4996, 4997, 4997, 4997, 4997, 4997, 4998, 4998, 4999, 4999]\n", diff --git a/crates/cli_utils/src/helpers.rs b/crates/cli_utils/src/helpers.rs new file mode 100644 index 0000000000..1239e0bcbd --- /dev/null +++ b/crates/cli_utils/src/helpers.rs @@ -0,0 +1,484 @@ +extern crate bumpalo; +extern crate roc_collections; +extern crate roc_load; +extern crate roc_module; +extern crate tempfile; + +use roc_command_utils::{cargo, pretty_command_string, root_dir}; +use serde::Deserialize; +use serde_xml_rs::from_str; +use std::env; +use std::ffi::{OsStr, OsString}; +use std::io::Read; +use std::io::Write; +use std::path::PathBuf; +use std::process::{Command, ExitStatus, Stdio}; +use std::sync::Mutex; +use tempfile::NamedTempFile; + +#[derive(Debug)] +pub struct Out { + pub cmd_str: OsString, // command with all its arguments, for easy debugging + pub stdout: String, + pub stderr: String, + pub status: ExitStatus, +} + +pub fn run_roc(args: I, stdin_vals: &[&str], extra_env: &[(&str, &str)]) -> Out +where + I: IntoIterator, + S: AsRef, +{ + run_roc_with_stdin_and_env(args, stdin_vals, extra_env) +} + +// Since glue is always compiling the same plugin, it can not be run in parallel. +// That would lead to a race condition in writing the output shared library. +// Thus, all calls to glue in a test are made sequential. +// TODO: In the future, look into compiling the shared library once and then caching it. +static GLUE_LOCK: Mutex<()> = Mutex::new(()); + +pub fn run_glue(args: I) -> Out +where + I: IntoIterator, + S: AsRef, +{ + let _guard = GLUE_LOCK.lock().unwrap(); + + run_roc_with_stdin(args, &[]) +} + +pub fn has_error(stderr: &str) -> bool { + let stderr_stripped = stderr + .replacen("🔨 Rebuilding platform...\n", "", 1) + // for some reason, llvm prints out this warning when targeting windows + .replacen( + "warning: ignoring debug info with an invalid version (0) in app\r\n", + "", + 1, + ); + + let is_reporting_runtime = + stderr_stripped.starts_with("runtime: ") && stderr_stripped.ends_with("ms\n"); + + let is_clean = stderr_stripped.is_empty() || + is_reporting_runtime || + // macOS ld reports this warning, but if we remove -undefined dynamic_lookup, + // linking stops working properly. + stderr_stripped.trim() == "ld: warning: -undefined dynamic_lookup may not work with chained fixups"; + + !is_clean +} + +pub fn path_to_roc_binary() -> PathBuf { + path_to_binary(if cfg!(windows) { "roc.exe" } else { "roc" }) +} + +pub fn path_to_binary(binary_name: &str) -> PathBuf { + // Adapted from https://github.com/volta-cli/volta/blob/cefdf7436a15af3ce3a38b8fe53bb0cfdb37d3dd/tests/acceptance/support/sandbox.rs#L680 + // by the Volta Contributors - license information can be found in + // the LEGAL_DETAILS file in the root directory of this distribution. + // + // Thank you, Volta contributors! + let mut path = env::var_os("CARGO_BIN_PATH") + .map(PathBuf::from) + .or_else(|| { + env::current_exe().ok().map(|mut path| { + path.pop(); + if path.ends_with("deps") { + path.pop(); + } + path + }) + }) + .unwrap_or_else(|| panic!("CARGO_BIN_PATH wasn't set, and couldn't be inferred from context. Can't run CLI tests.")); + + path.push(binary_name); + + path +} + +pub fn strip_colors(str: &str) -> String { + use roc_reporting::report::ANSI_STYLE_CODES; + + str.replace(ANSI_STYLE_CODES.red, "") + .replace(ANSI_STYLE_CODES.green, "") + .replace(ANSI_STYLE_CODES.yellow, "") + .replace(ANSI_STYLE_CODES.blue, "") + .replace(ANSI_STYLE_CODES.magenta, "") + .replace(ANSI_STYLE_CODES.cyan, "") + .replace(ANSI_STYLE_CODES.white, "") + .replace(ANSI_STYLE_CODES.bold, "") + .replace(ANSI_STYLE_CODES.underline, "") + .replace(ANSI_STYLE_CODES.reset, "") + .replace(ANSI_STYLE_CODES.color_reset, "") +} + +pub fn run_roc_with_stdin(args: I, stdin_vals: &[&str]) -> Out +where + I: IntoIterator, + S: AsRef, +{ + run_roc_with_stdin_and_env(args, stdin_vals, &[]) +} + +pub fn run_roc_with_stdin_and_env( + args: I, + stdin_vals: &[&str], + extra_env: &[(&str, &str)], +) -> Out +where + I: IntoIterator, + S: AsRef, +{ + let roc_path = build_roc_bin_cached(); + let mut roc_cmd = Command::new(roc_path); + + for arg in args { + roc_cmd.arg(arg); + } + + for (k, v) in extra_env { + roc_cmd.env(k, v); + } + + let roc_cmd_str = pretty_command_string(&roc_cmd); + + let mut roc_cmd_child = roc_cmd + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .unwrap_or_else(|err| { + panic!("Failed to execute command\n\n {roc_cmd_str:?}\n\nwith error:\n\n {err}",) + }); + + { + let stdin = roc_cmd_child.stdin.as_mut().expect("Failed to open stdin"); + + for stdin_str in stdin_vals.iter() { + stdin + .write_all(stdin_str.as_bytes()) + .unwrap_or_else(|err| { + panic!( + "Failed to write to stdin for command\n\n {roc_cmd_str:?}\n\nwith error:\n\n {err}", + ) + }); + } + } + + let roc_cmd_output = roc_cmd_child.wait_with_output().unwrap_or_else(|err| { + panic!("Failed to get output for command\n\n {roc_cmd_str:?}\n\nwith error:\n\n {err}",) + }); + + Out { + cmd_str: roc_cmd_str, + stdout: String::from_utf8(roc_cmd_output.stdout).unwrap(), + stderr: String::from_utf8(roc_cmd_output.stderr).unwrap(), + status: roc_cmd_output.status, + } +} + +// If we don't already have a /target/release/roc, build it! +pub fn build_roc_bin_cached() -> PathBuf { + let roc_binary_path = path_to_roc_binary(); + + if !roc_binary_path.exists() { + build_roc_bin(&[]); + } + + roc_binary_path +} + +pub fn build_roc_bin(extra_args: &[&str]) -> PathBuf { + let roc_binary_path = path_to_roc_binary(); + + // Remove the /target/release/roc part + let root_project_dir = roc_binary_path + .parent() + .unwrap() + .parent() + .unwrap() + .parent() + .unwrap(); + + // cargo build --bin roc + // (with --release iff the test is being built with --release) + let mut args = if cfg!(debug_assertions) { + vec!["build", "--bin", "roc"] + } else { + vec!["build", "--release", "--bin", "roc"] + }; + + args.extend(extra_args); + + let mut cargo_cmd = cargo(); + + cargo_cmd.current_dir(root_project_dir).args(&args); + + let cargo_cmd_str = format!("{cargo_cmd:?}"); + + let cargo_output = cargo_cmd.output().unwrap(); + + if !cargo_output.status.success() { + panic!( + "The following cargo command failed:\n\n {}\n\n stdout was:\n\n {}\n\n stderr was:\n\n {}\n", + cargo_cmd_str, + String::from_utf8(cargo_output.stdout).unwrap(), + String::from_utf8(cargo_output.stderr).unwrap() + ); + } + + roc_binary_path +} + +pub fn run_cmd<'a, I: IntoIterator, E: IntoIterator>( + cmd_name: &str, + stdin_vals: I, + args: &[String], + env: E, +) -> Out { + let mut cmd = Command::new(cmd_name); + + for arg in args { + cmd.arg(arg); + } + + for (env, val) in env.into_iter() { + cmd.env(env, val); + } + + let cmd_str = pretty_command_string(&cmd); + + let mut child = cmd + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .unwrap_or_else(|_| panic!("failed to execute cmd `{cmd_name}` in CLI test")); + + { + let stdin = child.stdin.as_mut().expect("Failed to open stdin"); + + for stdin_str in stdin_vals { + stdin + .write_all(stdin_str.as_bytes()) + .expect("Failed to write to stdin"); + } + } + + let output = child + .wait_with_output() + .unwrap_or_else(|_| panic!("failed to execute cmd `{cmd_name}` in CLI test")); + + Out { + cmd_str, + stdout: String::from_utf8(output.stdout).unwrap(), + stderr: String::from_utf8(output.stderr).unwrap(), + status: output.status, + } +} + +pub fn run_with_valgrind<'a, I: IntoIterator>( + stdin_vals: I, + args: &[String], +) -> (Out, String) { + //TODO: figure out if there is a better way to get the valgrind executable. + let mut cmd = Command::new("valgrind"); + let named_tempfile = + NamedTempFile::new().expect("Unable to create tempfile for valgrind results"); + + cmd.arg("--tool=memcheck"); + cmd.arg("--xml=yes"); + cmd.arg(format!("--xml-file={}", named_tempfile.path().display())); + + // If you are having valgrind issues on MacOS, you may need to suppress some + // of the errors. Read more here: https://github.com/roc-lang/roc/issues/746 + if let Some(suppressions_file_os_str) = env::var_os("VALGRIND_SUPPRESSIONS") { + match suppressions_file_os_str.to_str() { + None => { + panic!("Could not determine suppression file location from OsStr"); + } + Some(suppressions_file) => { + let mut buf = String::new(); + + buf.push_str("--suppressions="); + buf.push_str(suppressions_file); + + cmd.arg(buf); + } + } + } + + for arg in args { + cmd.arg(arg); + } + + let cmd_str = pretty_command_string(&cmd); + + cmd.stdin(Stdio::piped()); + cmd.stdout(Stdio::piped()); + cmd.stderr(Stdio::piped()); + + let mut child = cmd + .spawn() + .expect("failed to execute compiled `valgrind` binary in CLI test"); + + { + let stdin = child.stdin.as_mut().expect("Failed to open stdin"); + + for stdin_str in stdin_vals { + stdin + .write_all(stdin_str.as_bytes()) + .expect("Failed to write to stdin"); + } + } + + let output = child + .wait_with_output() + .expect("failed to execute compiled `valgrind` binary in CLI test"); + + let mut file = named_tempfile.into_file(); + let mut raw_xml = String::new(); + + file.read_to_string(&mut raw_xml).unwrap(); + + ( + Out { + cmd_str, + stdout: String::from_utf8(output.stdout).unwrap(), + stderr: String::from_utf8(output.stderr).unwrap(), + status: output.status, + }, + raw_xml, + ) +} + +#[derive(Debug, Deserialize)] +struct ValgrindOutput { + #[serde(rename = "$value")] + pub fields: Vec, +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "lowercase")] +enum ValgrindField { + ProtocolVersion(isize), + ProtocolTool(String), + Preamble(ValgrindDummyStruct), + Pid(isize), + PPid(isize), + Tool(String), + Args(ValgrindDummyStruct), + Error(ValgrindError), + Status(ValgrindDummyStruct), + Stack(ValgrindDummyStruct), + #[serde(rename = "fatal_signal")] + FatalSignal(ValgrindDummyStruct), + ErrorCounts(ValgrindDummyStruct), + SuppCounts(ValgrindDummyStruct), +} + +#[derive(Debug, Deserialize)] +struct ValgrindDummyStruct {} + +#[derive(Debug, Deserialize, Clone)] +pub struct ValgrindError { + pub kind: String, + #[serde(default)] + pub what: Option, + #[serde(default)] + pub xwhat: Option, +} + +#[derive(Debug, Deserialize, Clone)] +pub struct ValgrindErrorXWhat { + pub text: String, + #[serde(default)] + pub leakedbytes: Option, + #[serde(default)] + pub leakedblocks: Option, +} + +#[allow(dead_code)] +pub fn extract_valgrind_errors(xml: &str) -> Result, serde_xml_rs::Error> { + let parsed_xml: ValgrindOutput = from_str(xml)?; + let answer = parsed_xml + .fields + .iter() + .filter_map(|field| match field { + ValgrindField::Error(err) => Some(err.clone()), + _ => None, + }) + .collect(); + + Ok(answer) +} + +// start the dir with crates/cli_testing_examples +#[allow(dead_code)] +pub fn cli_testing_dir(dir_name: &str) -> PathBuf { + let mut path = root_dir(); + + // Descend into examples/{dir_name} + path.push("crates"); + path.push("cli_testing_examples"); + path.extend(dir_name.split('/')); // Make slashes cross-target + + path +} + +#[allow(dead_code)] +pub fn dir_path_from_root(dir_name: &str) -> PathBuf { + let mut path = root_dir(); + + path.extend(dir_name.split('/')); // Make slashes cross-target + + path +} + +#[allow(dead_code)] +pub fn file_path_from_root(dir_name: &str, file_name: &str) -> PathBuf { + let mut path = dir_path_from_root(dir_name); + + path.push(file_name); + + path +} + +#[allow(dead_code)] +pub fn fixtures_dir(dir_name: &str) -> PathBuf { + let mut path = root_dir(); + + // Descend into crates/cli/tests/fixtures/{dir_name} + path.push("crates"); + path.push("cli"); + path.push("tests"); + path.push("fixtures"); + path.extend(dir_name.split('/')); // Make slashes cross-target + + path +} + +#[allow(dead_code)] +pub fn fixture_file(dir_name: &str, file_name: &str) -> PathBuf { + let mut path = fixtures_dir(dir_name); + + path.push(file_name); + + path +} + +#[allow(dead_code)] +pub fn known_bad_file(file_name: &str) -> PathBuf { + let mut path = root_dir(); + + // Descend into cli/tests/known_bad/{file_name} + path.push("crates"); + path.push("cli"); + path.push("tests"); + path.push("known_bad"); + path.push(file_name); + + path +} diff --git a/crates/cli_utils/src/lib.rs b/crates/cli_utils/src/lib.rs new file mode 100644 index 0000000000..e8f1d37a9e --- /dev/null +++ b/crates/cli_utils/src/lib.rs @@ -0,0 +1,3 @@ +//! Provides shared code for cli tests and benchmarks. +pub mod bench_utils; +pub mod helpers; diff --git a/crates/compiler/DESIGN.md b/crates/compiler/DESIGN.md new file mode 100644 index 0000000000..be77a57225 --- /dev/null +++ b/crates/compiler/DESIGN.md @@ -0,0 +1,194 @@ +# Compiler Design + +The current Roc compiler is designed as a pipelining compiler parallelizable +across Roc modules. + +Roc's compilation pipeline consists of a few major components, which form the +table of contents for this document. + + + + +- [Parsing](#parsing) +- [Canonicalization](#canonicalization) + - [Symbol Resolution](#symbol-resolution) + - [Type-alias normalization](#type-alias-normalization) + - [Closure naming](#closure-naming) +- [Constraint Generation](#constraint-generation) + - [(Mutually-)recursive definitions](#mutually-recursive-definitions) +- [Type Solving](#type-solving) + - [Unification](#unification) + - [Type Inference](#type-inference) + - [Recursive Types](#recursive-types) + - [Lambda Sets](#lambda-sets) + - [Ability Collection](#ability-collection) + - [Ability Specialization](#ability-specialization) + - [Ability Derivation](#ability-derivation) + - [Exhaustiveness Checking](#exhaustiveness-checking) + - [Debugging](#debugging) +- [IR Generation](#ir-generation) + - [Memory Layouts](#memory-layouts) + - [Compiling Calls](#compiling-calls) + - [Decision Trees](#decision-trees) + - [Tail-call Optimization](#tail-call-optimization) + - [Reference-count insertion](#reference-count-insertion) + - [Reusing Memory Allocations](#reusing-memory-allocations) + - [Debugging](#debugging-1) +- [LLVM Code Generator](#llvm-code-generator) + - [Morphic Analysis](#morphic-analysis) + - [C ABI](#c-abi) + - [Test Harness](#test-harness) + - [Debugging](#debugging-2) +- [WASM Code Generator](#wasm-code-generator) + - [WASM Interpreter](#wasm-interpreter) + - [Debugging](#debugging-3) +- [Dev Code Generator](#dev-code-generator) + - [Debugging](#debugging-4) +- [Builtins](#builtins) +- [Compiler Driver](#compiler-driver) + - [Caching types](#caching-types) +- [Repl](#repl) +- [`test` and `dbg`](#test-and-dbg) +- [Formatter](#formatter) +- [Glue](#glue) +- [Active areas of research / help wanted](#active-areas-of-research--help-wanted) + + + +## Parsing + +Roc's parsers are designed as [combinators](https://en.wikipedia.org/wiki/Parser_combinator). +A list of Roc's parse AST and combinators can be found in [the root parse +file](./parse/src/parser.rs). + +Combinators enable parsing to compose as functions would - for example, the +`one_of` combinator supports attempting multiple parsing strategies, and +succeeding on the first one; the `and_then` combinator chains two parsers +together, failing if either parser in the sequence fails. + +Since Roc is an indentation-sensitive language, parsing must be cognizant and +deligent about handling indentation and de-indentation levels. Most parsing +functions take a `min_indent` parameter that specifies the minimum indentation +of the scope an expression should be parsed in. Generally, failing to reach +`min_indent` indicates that an expression has ended (but perhaps too early). + +## Canonicalization + +After parsing a Roc program into an AST, the AST is transformed into a [canonical +form](./can/src/expr.rs) AST. This may seem a bit redundant - why build another +tree, when we already have the AST? Canonicalization performs a few analyses +to catch user errors, and sets up the state necessary to solve the types in a +program. Among other things, canonicalization + +- Uniquely identifies names (think variable and function names). Along the way, + canonicalization builds a graph of all variables' references, and catches + unused definitions, undefined definitions, and shadowed definitions. +- Resolves type signatures, including aliases, into a form suitable for type + solving. +- Determines the order definitions are used in, if they are defined + out-of-order. +- Eliminates syntax sugar (for example, renaming `+` to the function call `add` + and converting backpassing to function calls). +- Collects declared abilities, and ability implementations defined for opaque + types. Derived abilities for opaque types are elaborated during + canonicalization. + +### Symbol Resolution + +Identifiers, like variable names, are resolved to [Symbol](./module/src/symbol.rs)s. + +Currently, a symbol is a 64-bit value with +- the bottom 32 bits defining the [ModuleId](./module/src/ident.rs) the symbol + is defined in +- the top 32 bits defining the [IdentId](./module/src/ident.rs) of the symbol + in the module + +A symbol is unique per identifier name and the scope +that the identifier has been declared in. Symbols are how the rest of the +compiler refers to value definitions - since the unique scope and identifier +name is disambiguated when symbols are created, referencing symbols requires no +further name resolution. + +As symbols are constructed, canonicalization also keeps track of all references +to a given symbol. This simplifies catching unused definitions, undefined +definitions, and shadowing, to an index into an array. + +### Type-alias normalization + +### Closure naming + +## Constraint Generation + +### (Mutually-)recursive definitions + +## Type Solving + +### Unification + +### Type Inference + +### Recursive Types + +### Lambda Sets + +### Ability Collection + +### Ability Specialization + +### Ability Derivation + +### Exhaustiveness Checking + +### Debugging + +## IR Generation + +### Memory Layouts + +### Compiling Calls + +### Decision Trees + +### Tail-call Optimization + +### Reference-count insertion + +### Reusing Memory Allocations + +### Debugging + +## LLVM Code Generator + +### Morphic Analysis + +### C ABI + +### Test Harness + +### Debugging + +## WASM Code Generator + +### WASM Interpreter + +### Debugging + +## Dev Code Generator + +### Debugging + +## Builtins + +## Compiler Driver + +### Caching types + +## Repl + +## `test` and `dbg` + +## Formatter + +## Glue + +## Active areas of research / help wanted diff --git a/crates/compiler/README.md b/crates/compiler/README.md new file mode 100644 index 0000000000..28f983c3f1 --- /dev/null +++ b/crates/compiler/README.md @@ -0,0 +1,63 @@ +# The Roc Compiler + +For an overview of the design and architecture of the compiler, see +[DESIGN.md](./DESIGN.md). If you want to dive into the +implementation or get some tips on debugging the compiler, see below + +## Getting started with the code + +The compiler contains a lot of code! If you're new to the project it can be hard to know where to start. It's useful to have some sort of "main entry point", or at least a "good place to start" for each of the main phases. + +After you get into the details, you'll discover that some parts of the compiler have more than one entry point. And things can be interwoven together in subtle and complex ways, for reasons to do with performance, edge case handling, etc. But if this is "day one" for you, and you're just trying to get familiar with things, this should be "good enough". + +The compiler is invoked from the CLI via `build_file` in cli/src/build.rs + +| Phase | Entry point / main functions | +| ------------------------------------- | ------------------------------------------------ | +| Compiler entry point | load/src/file.rs: load, load_and_monomorphize | +| Parse header | parse/src/module.rs: parse_header | +| Parse definitions | parse/src/module.rs: module_defs | +| Canonicalize | can/src/def.rs: canonicalize_defs | +| Type check | solve/src/module.rs: run_solve | +| Gather types to specialize | mono/src/ir.rs: PartialProc::from_named_function | +| Solve specialized types | mono/src/ir.rs: from_can, with_hole | +| Insert reference counting | mono/src/ir.rs: Proc::insert_refcount_operations | +| Code gen (optimized but slow) | gen_llvm/src/llvm/build.rs: build_procedures | +| Code gen (unoptimized but fast, CPU) | gen_dev/src/object_builder.rs: build_module | +| Code gen (unoptimized but fast, Wasm) | gen_wasm/src/lib.rs: build_module | + +For a more detailed understanding of the compilation phases, see the `Phase`, `BuildTask`, and `Msg` enums in `load/src/file.rs`. + +## Debugging the compiler + +Please see the [debug flags](./debug_flags/src/lib.rs) for information on how to +ask the compiler to emit debug information during various stages of compilation. + +There are some goals for more sophisticated debugging tools: + +- A nicer unification debugger, see . + Any interest in helping out here is greatly appreciated. + +### General Tips + +#### Miscompilations + +If you observe a miscomplication, you may first want to check the generated mono +IR for your code - maybe there was a problem during specialization or layout +generation. One way to do this is to add a test to `test_mono/src/tests.rs` +and run the tests with `cargo test -p test_mono`; this will write the mono +IR to a file. + +#### Typechecking errors + +First, try to minimize your reproduction into a test that fits in +[`solve_expr`](./solve/tests/solve_expr.rs). + +Once you've done this, check out the `ROC_PRINT_UNIFICATIONS` debug flag. It +will show you where type unification went right and wrong. This is usually +enough to figure out a fix for the bug. + +If that doesn't work and you know your error has something to do with ranks, +you may want to instrument `deep_copy_var_help` in [solve](./solve/src/solve.rs). + +If that doesn't work, chatting on Zulip is always a good strategy. diff --git a/crates/compiler/alias_analysis/Cargo.toml b/crates/compiler/alias_analysis/Cargo.toml new file mode 100644 index 0000000000..3d6fd4987b --- /dev/null +++ b/crates/compiler/alias_analysis/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "roc_alias_analysis" + +authors.workspace = true +edition.workspace = true +license.workspace = true +version.workspace = true + +[dependencies] +morphic_lib = { path = "../../vendor/morphic_lib" } +roc_collections = { path = "../collections" } +roc_debug_flags = { path = "../debug_flags" } +roc_error_macros = { path = "../../error_macros" } +roc_module = { path = "../module" } +roc_mono = { path = "../mono" } + +bumpalo.workspace = true diff --git a/crates/compiler/alias_analysis/src/lib.rs b/crates/compiler/alias_analysis/src/lib.rs new file mode 100644 index 0000000000..452a90567a --- /dev/null +++ b/crates/compiler/alias_analysis/src/lib.rs @@ -0,0 +1,1844 @@ +// https://github.com/morphic-lang/morphic_lib/issues/19 +#![allow(clippy::result_large_err)] + +use bumpalo::Bump; +use morphic_lib::TypeContext; +use morphic_lib::{ + BlockExpr, BlockId, CalleeSpecVar, ConstDefBuilder, ConstName, EntryPointName, ExprContext, + FuncDef, FuncDefBuilder, FuncName, ModDefBuilder, ModName, ProgramBuilder, Result, + TypeDefBuilder, TypeId, TypeName, UpdateModeVar, ValueId, +}; +use roc_collections::all::{MutMap, MutSet}; +use roc_error_macros::internal_error; +use roc_module::low_level::LowLevel; +use roc_module::symbol::Symbol; + +use roc_mono::ir::{ + Call, CallType, EntryPoint, ErasedField, Expr, HigherOrderLowLevel, HostExposedLambdaSet, + ListLiteralElement, Literal, ModifyRc, OptLevel, Proc, ProcLayout, SingleEntryPoint, Stmt, +}; +use roc_mono::layout::{ + Builtin, InLayout, Layout, LayoutInterner, LayoutRepr, Niche, RawFunctionLayout, + STLayoutInterner, UnionLayout, +}; + +// just using one module for now +pub const MOD_APP: ModName = ModName(b"UserApp"); + +pub const STATIC_STR_NAME: ConstName = ConstName(&Symbol::STR_ALIAS_ANALYSIS_STATIC.to_ne_bytes()); +pub const STATIC_LIST_NAME: ConstName = ConstName(b"THIS IS A STATIC LIST"); + +const ENTRY_POINT_NAME: &[u8] = b"mainForHost"; + +pub fn func_name_bytes(proc: &Proc) -> [u8; SIZE] { + let bytes = func_name_bytes_help( + proc.name.name(), + proc.args.iter().map(|x| x.0), + proc.name.niche(), + proc.ret_layout, + ); + bytes +} + +#[inline(always)] +fn debug() -> bool { + use roc_debug_flags::dbg_do; + + #[cfg(debug_assertions)] + use roc_debug_flags::ROC_DEBUG_ALIAS_ANALYSIS; + + dbg_do!(ROC_DEBUG_ALIAS_ANALYSIS, { + return true; + }); + false +} + +const SIZE: usize = 16; + +#[derive(Debug, Clone, Copy, Hash)] +struct TagUnionId(u64); + +fn recursive_tag_union_name_bytes(union_layout: &UnionLayout) -> TagUnionId { + use std::collections::hash_map::DefaultHasher; + use std::hash::Hash; + use std::hash::Hasher; + + let mut hasher = DefaultHasher::new(); + union_layout.hash(&mut hasher); + + TagUnionId(hasher.finish()) +} + +impl TagUnionId { + const fn as_bytes(&self) -> [u8; 8] { + self.0.to_ne_bytes() + } +} + +pub fn func_name_bytes_help<'a, I>( + symbol: Symbol, + argument_layouts: I, + niche: Niche<'a>, + return_layout: InLayout<'a>, +) -> [u8; SIZE] +where + I: IntoIterator>, +{ + let mut name_bytes = [0u8; SIZE]; + + use std::collections::hash_map::DefaultHasher; + use std::hash::Hash; + use std::hash::Hasher; + + let layout_hash = { + let mut hasher = DefaultHasher::new(); + + for layout in argument_layouts { + layout.hash(&mut hasher); + } + + niche.hash(&mut hasher); + + return_layout.hash(&mut hasher); + + hasher.finish() + }; + + let sbytes = symbol.to_ne_bytes(); + let lbytes = layout_hash.to_ne_bytes(); + + let it = sbytes + .iter() + .chain(lbytes.iter()) + .zip(name_bytes.iter_mut()); + + for (source, target) in it { + *target = *source; + } + + if debug() { + for (i, c) in (format!("{symbol:?}")).chars().take(25).enumerate() { + name_bytes[25 + i] = c as u8; + } + } + + name_bytes +} + +fn bytes_as_ascii(bytes: &[u8]) -> String { + use std::fmt::Write; + + let mut buf = String::new(); + + for byte in bytes { + write!(buf, "{byte:02X}").unwrap(); + } + + buf +} + +pub fn spec_program<'a, 'r, I1, I2>( + arena: &'a Bump, + interner: &'r STLayoutInterner<'a>, + opt_level: OptLevel, + entry_point: roc_mono::ir::EntryPoint<'a>, + procs: I1, + hels: I2, +) -> Result +where + I1: Iterator>, + I2: Iterator>, +{ + let main_module = { + let mut m = ModDefBuilder::new(); + + // a const that models all static strings + let static_str_def = { + let mut cbuilder = ConstDefBuilder::new(); + let block = cbuilder.add_block(); + let cell = cbuilder.add_new_heap_cell(block)?; + let value_id = cbuilder.add_make_tuple(block, &[cell])?; + let root = BlockExpr(block, value_id); + let str_type_id = str_type(&mut cbuilder)?; + + cbuilder.build(str_type_id, root)? + }; + m.add_const(STATIC_STR_NAME, static_str_def)?; + + // a const that models all static lists + let static_list_def = { + let mut cbuilder = ConstDefBuilder::new(); + let block = cbuilder.add_block(); + let cell = cbuilder.add_new_heap_cell(block)?; + + let unit_type = cbuilder.add_tuple_type(&[])?; + let bag = cbuilder.add_empty_bag(block, unit_type)?; + let value_id = cbuilder.add_make_tuple(block, &[cell, bag])?; + let root = BlockExpr(block, value_id); + let list_type_id = static_list_type(&mut cbuilder)?; + + cbuilder.build(list_type_id, root)? + }; + m.add_const(STATIC_LIST_NAME, static_list_def)?; + + let mut type_definitions = MutSet::default(); + let mut host_exposed_functions = Vec::new(); + let mut erased_functions = Vec::new(); + + for hels in hels { + match hels.raw_function_layout { + RawFunctionLayout::Function(_, _, _) => { + let it = hels.proc_layout.arguments.iter().copied(); + let bytes = + func_name_bytes_help(hels.symbol, it, Niche::NONE, hels.proc_layout.result); + + host_exposed_functions.push((bytes, hels.proc_layout.arguments)); + } + RawFunctionLayout::ErasedFunction(..) => { + let it = hels.proc_layout.arguments.iter().copied(); + let bytes = + func_name_bytes_help(hels.symbol, it, Niche::NONE, hels.proc_layout.result); + + host_exposed_functions.push((bytes, hels.proc_layout.arguments)); + } + RawFunctionLayout::ZeroArgumentThunk(_) => { + let bytes = + func_name_bytes_help(hels.symbol, [], Niche::NONE, hels.proc_layout.result); + + host_exposed_functions.push((bytes, hels.proc_layout.arguments)); + } + } + } + + // all other functions + for proc in procs { + let bytes = func_name_bytes(proc); + let func_name = FuncName(&bytes); + + if debug() { + eprintln!( + "{:?}: {:?} with {:?} args", + proc.name, + bytes_as_ascii(&bytes), + (proc.args, proc.ret_layout), + ); + } + + let (spec, type_names) = proc_spec(arena, interner, proc)?; + + if proc.is_erased { + let args = &*arena.alloc_slice_fill_iter(proc.args.iter().map(|(lay, _)| *lay)); + erased_functions.push((bytes, args)); + } + + type_definitions.extend(type_names); + + m.add_func(func_name, spec)?; + } + + match entry_point { + EntryPoint::Single(SingleEntryPoint { + symbol: entry_point_symbol, + layout: entry_point_layout, + }) => { + // the entry point wrapper + let roc_main_bytes = func_name_bytes_help( + entry_point_symbol, + entry_point_layout.arguments.iter().copied(), + Niche::NONE, + entry_point_layout.result, + ); + let roc_main = FuncName(&roc_main_bytes); + + let mut env = Env::new(); + + let entry_point_function = build_entry_point( + &mut env, + interner, + entry_point_layout, + Some(roc_main), + &host_exposed_functions, + &erased_functions, + )?; + + type_definitions.extend(env.type_names); + + let entry_point_name = FuncName(ENTRY_POINT_NAME); + m.add_func(entry_point_name, entry_point_function)?; + } + EntryPoint::Expects { symbols } => { + // construct a big pattern match picking one of the expects at random + let layout: ProcLayout<'a> = ProcLayout { + arguments: &[], + result: Layout::UNIT, + niche: Niche::NONE, + }; + + let host_exposed: Vec<_> = symbols + .iter() + .map(|symbol| { + ( + func_name_bytes_help(*symbol, [], Niche::NONE, layout.result), + [].as_slice(), + ) + }) + .collect(); + + let mut env = Env::new(); + let entry_point_function = build_entry_point( + &mut env, + interner, + layout, + None, + &host_exposed, + &erased_functions, + )?; + + type_definitions.extend(env.type_names); + + let entry_point_name = FuncName(ENTRY_POINT_NAME); + m.add_func(entry_point_name, entry_point_function)?; + } + } + + for union_layout in type_definitions { + let type_name_bytes = recursive_tag_union_name_bytes(&union_layout).as_bytes(); + let type_name = TypeName(&type_name_bytes); + + let mut builder = TypeDefBuilder::new(); + + let mut env = Env::new(); + let variant_types = + recursive_variant_types(&mut env, &mut builder, interner, &union_layout)?; + + // FIXME: dropping additional env.type_names here! + + let root_type = if let UnionLayout::NonNullableUnwrapped(_) = union_layout { + debug_assert_eq!(variant_types.len(), 1); + variant_types[0] + } else { + let cell_type = builder.add_heap_cell_type(); + let data_type = builder.add_union_type(&variant_types)?; + + builder.add_tuple_type(&[cell_type, data_type])? + }; + + let type_def = builder.build(root_type)?; + + m.add_named_type(type_name, type_def)?; + } + + m.build()? + }; + + let program = { + let mut p = ProgramBuilder::new(); + p.add_mod(MOD_APP, main_module)?; + + p.add_entry_point( + EntryPointName(ENTRY_POINT_NAME), + MOD_APP, + FuncName(ENTRY_POINT_NAME), + )?; + + p.build()? + }; + + if debug() { + eprintln!("{}", program.to_source_string()); + } + + match opt_level { + OptLevel::Development | OptLevel::Normal => morphic_lib::solve_trivial(program), + OptLevel::Optimize | OptLevel::Size => morphic_lib::solve(program), + } +} + +/// if you want an "escape hatch" which allows you construct "best-case scenario" values +/// of an arbitrary type in much the same way that 'unknown_with' allows you to construct +/// "worst-case scenario" values of an arbitrary type, you can use the following terrible hack: +/// use 'add_make_union' to construct an instance of variant 0 of a union type 'union {(), your_type}', +/// and then use 'add_unwrap_union' to extract variant 1 from the value you just constructed. +/// In the current implementation (but not necessarily in future versions), +/// I can promise this will effectively give you a value of type 'your_type' +/// all of whose heap cells are considered unique and mutable. +fn terrible_hack(builder: &mut FuncDefBuilder, block: BlockId, type_id: TypeId) -> Result { + let variant_types = vec![builder.add_tuple_type(&[])?, type_id]; + let unit = builder.add_make_tuple(block, &[])?; + let value = builder.add_make_union(block, &variant_types, 0, unit)?; + + builder.add_unwrap_union(block, value, 1) +} + +fn build_entry_point<'a>( + env: &mut Env<'a>, + interner: &STLayoutInterner<'a>, + layout: roc_mono::ir::ProcLayout<'a>, + entry_point_function: Option, + host_exposed_functions: &[([u8; SIZE], &'a [InLayout<'a>])], + erased_functions: &[([u8; SIZE], &'a [InLayout<'a>])], +) -> Result { + let mut builder = FuncDefBuilder::new(); + let outer_block = builder.add_block(); + + let mut cases = Vec::new(); + + if let Some(entry_point_function) = entry_point_function { + let block = builder.add_block(); + + // to the modelling language, the arguments appear out of thin air + let argument_type = build_tuple_type(env, &mut builder, interner, layout.arguments)?; + + // does not make any assumptions about the input + // let argument = builder.add_unknown_with(block, &[], argument_type)?; + + // assumes the input can be updated in-place + let argument = terrible_hack(&mut builder, block, argument_type)?; + + let name_bytes = [0; 16]; + let spec_var = CalleeSpecVar(&name_bytes); + let result = builder.add_call(block, spec_var, MOD_APP, entry_point_function, argument)?; + + // to the modelling language, the result disappears into the void + let unit_type = builder.add_tuple_type(&[])?; + let unit_value = builder.add_unknown_with(block, &[result], unit_type)?; + + cases.push(BlockExpr(block, unit_value)); + } + + // add fake calls to host-exposed functions so they are specialized + for (name_bytes, layouts) in host_exposed_functions.iter().chain(erased_functions) { + let host_exposed_func_name = FuncName(name_bytes); + + if Some(host_exposed_func_name) == entry_point_function { + continue; + } + + let block = builder.add_block(); + + let struct_layout = LayoutRepr::struct_(layouts); + let type_id = layout_spec(env, &mut builder, interner, struct_layout)?; + + let argument = builder.add_unknown_with(block, &[], type_id)?; + + let spec_var = CalleeSpecVar(name_bytes); + let result = + builder.add_call(block, spec_var, MOD_APP, host_exposed_func_name, argument)?; + + let unit_type = builder.add_tuple_type(&[])?; + let unit_value = builder.add_unknown_with(block, &[result], unit_type)?; + + cases.push(BlockExpr(block, unit_value)); + } + + let unit_type = builder.add_tuple_type(&[])?; + let unit_value = if cases.is_empty() { + builder.add_make_tuple(outer_block, &[])? + } else { + builder.add_choice(outer_block, &cases)? + }; + + let root = BlockExpr(outer_block, unit_value); + let spec = builder.build(unit_type, unit_type, root)?; + + Ok(spec) +} + +fn proc_spec<'a>( + arena: &'a Bump, + interner: &STLayoutInterner<'a>, + proc: &Proc<'a>, +) -> Result<(FuncDef, MutSet>)> { + let mut builder = FuncDefBuilder::new(); + let mut env = Env::new(); + + let block = builder.add_block(); + + // introduce the arguments + let mut argument_layouts = bumpalo::collections::Vec::with_capacity_in(proc.args.len(), arena); + for (i, (layout, symbol)) in proc.args.iter().enumerate() { + let value_id = builder.add_get_tuple_field(block, builder.get_argument(), i as u32)?; + env.symbols.insert(*symbol, value_id); + + argument_layouts.push(*layout); + } + + let value_id = stmt_spec( + &mut builder, + interner, + &mut env, + block, + proc.ret_layout, + &proc.body, + )?; + + let root = BlockExpr(block, value_id); + let args_struct_layout = LayoutRepr::struct_(argument_layouts.into_bump_slice()); + let arg_type_id = layout_spec(&mut env, &mut builder, interner, args_struct_layout)?; + let ret_type_id = layout_spec( + &mut env, + &mut builder, + interner, + interner.get_repr(proc.ret_layout), + )?; + + let spec = builder.build(arg_type_id, ret_type_id, root)?; + + Ok((spec, env.type_names)) +} + +struct Env<'a> { + symbols: MutMap, + join_points: MutMap, + type_names: MutSet>, +} + +impl<'a> Env<'a> { + fn new() -> Self { + Self { + symbols: Default::default(), + join_points: Default::default(), + type_names: Default::default(), + } + } +} + +fn apply_refcount_operation( + builder: &mut FuncDefBuilder, + env: &mut Env<'_>, + block: BlockId, + modify_rc: &ModifyRc, +) -> Result<()> { + match modify_rc { + ModifyRc::Inc(symbol, _) => { + let argument = env.symbols[symbol]; + + // a recursive touch is never worse for optimizations than a normal touch + // and a bit more permissive in its type + builder.add_recursive_touch(block, argument)?; + } + + ModifyRc::Dec(symbol) => { + let argument = env.symbols[symbol]; + builder.add_recursive_touch(block, argument)?; + } + ModifyRc::DecRef(symbol) => { + // this is almost certainly suboptimal, but not incorrect + let argument = env.symbols[symbol]; + builder.add_recursive_touch(block, argument)?; + } + ModifyRc::Free(symbol) => { + // this is almost certainly suboptimal, but not incorrect + let argument = env.symbols[symbol]; + builder.add_recursive_touch(block, argument)?; + } + } + + Ok(()) +} + +fn stmt_spec<'a>( + builder: &mut FuncDefBuilder, + interner: &STLayoutInterner<'a>, + env: &mut Env<'a>, + block: BlockId, + layout: InLayout<'a>, + stmt: &Stmt<'a>, +) -> Result { + use Stmt::*; + + match stmt { + Let(symbol, expr, expr_layout, mut continuation) => { + let value_id = expr_spec(builder, interner, env, block, *expr_layout, expr)?; + env.symbols.insert(*symbol, value_id); + + let mut queue = vec![symbol]; + + loop { + match continuation { + Let(symbol, expr, expr_layout, c) => { + let value_id = + expr_spec(builder, interner, env, block, *expr_layout, expr)?; + env.symbols.insert(*symbol, value_id); + + queue.push(symbol); + continuation = c; + } + Refcounting(modify_rc, c) => { + // in practice it is common to see a chain of `Let`s interspersed with + // Inc/Dec. For e.g. the False interpreter, this caused stack overflows. + // so we handle RC operations here to limit recursion depth + apply_refcount_operation(builder, env, block, modify_rc)?; + + continuation = c; + } + _ => break, + } + } + + let result = stmt_spec(builder, interner, env, block, layout, continuation)?; + + for symbol in queue { + env.symbols.remove(symbol); + } + + Ok(result) + } + Switch { + cond_symbol: _, + cond_layout: _, + branches, + default_branch, + ret_layout: _lies, + } => { + let mut cases = Vec::with_capacity(branches.len() + 1); + + let it = branches + .iter() + .map(|(_, _, body)| body) + .chain(std::iter::once(default_branch.1)); + + for branch in it { + let block = builder.add_block(); + let value_id = stmt_spec(builder, interner, env, block, layout, branch)?; + cases.push(BlockExpr(block, value_id)); + } + + builder.add_choice(block, &cases) + } + Dbg { remainder, .. } => stmt_spec(builder, interner, env, block, layout, remainder), + Expect { remainder, .. } => stmt_spec(builder, interner, env, block, layout, remainder), + ExpectFx { remainder, .. } => stmt_spec(builder, interner, env, block, layout, remainder), + Ret(symbol) => Ok(env.symbols[symbol]), + Refcounting(modify_rc, continuation) => { + apply_refcount_operation(builder, env, block, modify_rc)?; + + stmt_spec(builder, interner, env, block, layout, continuation) + } + Join { + id, + parameters, + body, + remainder, + } => { + let mut type_ids = Vec::new(); + + for p in parameters.iter() { + type_ids.push(layout_spec( + env, + builder, + interner, + interner.get_repr(p.layout), + )?); + } + + let ret_type_id = layout_spec(env, builder, interner, interner.get_repr(layout))?; + + let jp_arg_type_id = builder.add_tuple_type(&type_ids)?; + + let (jpid, jp_argument) = + builder.declare_continuation(block, jp_arg_type_id, ret_type_id)?; + + // NOTE join point arguments can shadow variables from the outer scope + // the ordering of steps here is important + + // add this ID so both body and remainder can reference it + env.join_points.insert(*id, jpid); + + // first, with the current variable bindings, process the remainder + let cont_block = builder.add_block(); + let cont_value_id = stmt_spec(builder, interner, env, cont_block, layout, remainder)?; + + // only then introduce variables bound by the jump point, and process its body + let join_body_sub_block = { + let jp_body_block = builder.add_block(); + + // unpack the argument + for (i, p) in parameters.iter().enumerate() { + let value_id = + builder.add_get_tuple_field(jp_body_block, jp_argument, i as u32)?; + + env.symbols.insert(p.symbol, value_id); + } + + let jp_body_value_id = + stmt_spec(builder, interner, env, jp_body_block, layout, body)?; + + BlockExpr(jp_body_block, jp_body_value_id) + }; + + env.join_points.remove(id); + builder.define_continuation(jpid, join_body_sub_block)?; + + builder.add_sub_block(block, BlockExpr(cont_block, cont_value_id)) + } + Jump(id, symbols) => { + let ret_type_id = layout_spec(env, builder, interner, interner.get_repr(layout))?; + let argument = build_tuple_value(builder, env, block, symbols)?; + + let jpid = env.join_points[id]; + builder.add_jump(block, jpid, argument, ret_type_id) + } + Crash(msg, _) => { + // Model this as a foreign call rather than TERMINATE because + // we want ownership of the message. + let result_type = layout_spec(env, builder, interner, interner.get_repr(layout))?; + + builder.add_unknown_with(block, &[env.symbols[msg]], result_type) + } + } +} + +fn build_tuple_value( + builder: &mut FuncDefBuilder, + env: &Env, + block: BlockId, + symbols: &[Symbol], +) -> Result { + let mut value_ids = Vec::new(); + + for field in symbols.iter() { + let value_id = match env.symbols.get(field) { + None => panic!( + "Symbol {:?} is not defined in environment {:?}", + field, &env.symbols + ), + Some(x) => *x, + }; + value_ids.push(value_id); + } + + builder.add_make_tuple(block, &value_ids) +} + +fn build_recursive_tuple_type<'a>( + env: &mut Env<'a>, + builder: &mut impl TypeContext, + interner: &STLayoutInterner<'a>, + layouts: &[InLayout<'a>], +) -> Result { + let mut field_types = Vec::new(); + + for field in layouts.iter() { + let type_id = layout_spec_help(env, builder, interner, interner.get_repr(*field))?; + field_types.push(type_id); + } + + builder.add_tuple_type(&field_types) +} + +fn build_tuple_type<'a>( + env: &mut Env<'a>, + builder: &mut impl TypeContext, + interner: &STLayoutInterner<'a>, + layouts: &[InLayout<'a>], +) -> Result { + let mut field_types = Vec::new(); + + for field in layouts.iter() { + field_types.push(layout_spec( + env, + builder, + interner, + interner.get_repr(*field), + )?); + } + + builder.add_tuple_type(&field_types) +} + +fn add_loop( + builder: &mut FuncDefBuilder, + block: BlockId, + state_type: TypeId, + init_state: ValueId, + make_body: impl for<'a> FnOnce(&'a mut FuncDefBuilder, BlockId, ValueId) -> Result, +) -> Result { + let sub_block = builder.add_block(); + let (loop_cont, loop_arg) = builder.declare_continuation(sub_block, state_type, state_type)?; + let body = builder.add_block(); + let ret_branch = builder.add_block(); + let loop_branch = builder.add_block(); + let new_state = make_body(builder, loop_branch, loop_arg)?; + let unreachable = builder.add_jump(loop_branch, loop_cont, new_state, state_type)?; + let result = builder.add_choice( + body, + &[ + BlockExpr(ret_branch, loop_arg), + BlockExpr(loop_branch, unreachable), + ], + )?; + builder.define_continuation(loop_cont, BlockExpr(body, result))?; + let unreachable = builder.add_jump(sub_block, loop_cont, init_state, state_type)?; + builder.add_sub_block(block, BlockExpr(sub_block, unreachable)) +} + +fn call_spec<'a>( + builder: &mut FuncDefBuilder, + interner: &STLayoutInterner<'a>, + env: &mut Env<'a>, + block: BlockId, + layout: InLayout<'a>, + call: &Call<'a>, +) -> Result { + use CallType::*; + + match &call.call_type { + ByName { + name, + ret_layout, + arg_layouts, + specialization_id, + } => { + let array = specialization_id.to_bytes(); + let spec_var = CalleeSpecVar(&array); + + let arg_value_id = build_tuple_value(builder, env, block, call.arguments)?; + let args_it = arg_layouts.iter().copied(); + let captures_niche = name.niche(); + let bytes = func_name_bytes_help(name.name(), args_it, captures_niche, *ret_layout); + let name = FuncName(&bytes); + let module = MOD_APP; + builder.add_call(block, spec_var, module, name, arg_value_id) + } + ByPointer { + pointer, + ret_layout, + arg_layouts: _, + } => { + let result_type = layout_spec(env, builder, interner, interner.get_repr(*ret_layout))?; + let fnptr = env.symbols[pointer]; + let arg_value_id = build_tuple_value(builder, env, block, call.arguments)?; + builder.add_unknown_with(block, &[fnptr, arg_value_id], result_type) + } + Foreign { + foreign_symbol: _, + ret_layout, + } => { + let arguments: Vec<_> = call + .arguments + .iter() + .map(|symbol| env.symbols[symbol]) + .collect(); + + let result_type = layout_spec(env, builder, interner, interner.get_repr(*ret_layout))?; + + builder.add_unknown_with(block, &arguments, result_type) + } + LowLevel { op, update_mode } => lowlevel_spec( + builder, + interner, + env, + block, + layout, + op, + *update_mode, + call.arguments, + ), + HigherOrder(HigherOrderLowLevel { + closure_env_layout, + update_mode, + op, + passed_function, + .. + }) => { + use roc_mono::low_level::HigherOrder::*; + + let array = passed_function.specialization_id.to_bytes(); + let spec_var = CalleeSpecVar(&array); + + let mode = update_mode.to_bytes(); + let update_mode_var = UpdateModeVar(&mode); + + let args_it = passed_function.argument_layouts.iter().copied(); + let captures_niche = passed_function.name.niche(); + let bytes = func_name_bytes_help( + passed_function.name.name(), + args_it, + captures_niche, + passed_function.return_layout, + ); + let name = FuncName(&bytes); + let module = MOD_APP; + + let closure_env = env.symbols[&passed_function.captured_environment]; + + let return_layout = &passed_function.return_layout; + let argument_layouts = passed_function.argument_layouts; + + macro_rules! call_function { + ($builder: expr, $block:expr, [$($arg:expr),+ $(,)?]) => {{ + let argument = if closure_env_layout.is_none() { + $builder.add_make_tuple($block, &[$($arg),+])? + } else { + $builder.add_make_tuple($block, &[$($arg),+, closure_env])? + }; + + $builder.add_call($block, spec_var, module, name, argument)? + }}; + } + + match op { + ListMap { xs } => { + let list = env.symbols[xs]; + + let loop_body = |builder: &mut FuncDefBuilder, block, state| { + let input_bag = builder.add_get_tuple_field(block, list, LIST_BAG_INDEX)?; + + let element = builder.add_bag_get(block, input_bag)?; + + let new_element = call_function!(builder, block, [element]); + + list_append(builder, block, update_mode_var, state, new_element) + }; + + let output_element_type = + layout_spec(env, builder, interner, interner.get_repr(*return_layout))?; + + let state_layout = LayoutRepr::Builtin(Builtin::List(*return_layout)); + let state_type = layout_spec(env, builder, interner, state_layout)?; + + let init_state = new_list(builder, block, output_element_type)?; + + add_loop(builder, block, state_type, init_state, loop_body) + } + + ListSortWith { xs } => { + let list = env.symbols[xs]; + + let loop_body = |builder: &mut FuncDefBuilder, block, state| { + let bag = builder.add_get_tuple_field(block, state, LIST_BAG_INDEX)?; + let cell = builder.add_get_tuple_field(block, state, LIST_CELL_INDEX)?; + + let element_1 = builder.add_bag_get(block, bag)?; + let element_2 = builder.add_bag_get(block, bag)?; + + let _ = call_function!(builder, block, [element_1, element_2]); + + builder.add_update(block, update_mode_var, cell)?; + + with_new_heap_cell(builder, block, bag) + }; + + let arg0_layout = argument_layouts[0]; + + let state_layout = LayoutRepr::Builtin(Builtin::List(arg0_layout)); + let state_type = layout_spec(env, builder, interner, state_layout)?; + let init_state = list; + + add_loop(builder, block, state_type, init_state, loop_body) + } + + ListMap2 { xs, ys } => { + let list1 = env.symbols[xs]; + let list2 = env.symbols[ys]; + + let loop_body = |builder: &mut FuncDefBuilder, block, state| { + let input_bag_1 = + builder.add_get_tuple_field(block, list1, LIST_BAG_INDEX)?; + let input_bag_2 = + builder.add_get_tuple_field(block, list2, LIST_BAG_INDEX)?; + + let element_1 = builder.add_bag_get(block, input_bag_1)?; + let element_2 = builder.add_bag_get(block, input_bag_2)?; + + let new_element = call_function!(builder, block, [element_1, element_2]); + + list_append(builder, block, update_mode_var, state, new_element) + }; + + let output_element_type = + layout_spec(env, builder, interner, interner.get_repr(*return_layout))?; + + let state_layout = LayoutRepr::Builtin(Builtin::List(*return_layout)); + let state_type = layout_spec(env, builder, interner, state_layout)?; + + let init_state = new_list(builder, block, output_element_type)?; + + add_loop(builder, block, state_type, init_state, loop_body) + } + + ListMap3 { xs, ys, zs } => { + let list1 = env.symbols[xs]; + let list2 = env.symbols[ys]; + let list3 = env.symbols[zs]; + + let loop_body = |builder: &mut FuncDefBuilder, block, state| { + let input_bag_1 = + builder.add_get_tuple_field(block, list1, LIST_BAG_INDEX)?; + let input_bag_2 = + builder.add_get_tuple_field(block, list2, LIST_BAG_INDEX)?; + let input_bag_3 = + builder.add_get_tuple_field(block, list3, LIST_BAG_INDEX)?; + + let element_1 = builder.add_bag_get(block, input_bag_1)?; + let element_2 = builder.add_bag_get(block, input_bag_2)?; + let element_3 = builder.add_bag_get(block, input_bag_3)?; + + let new_element = + call_function!(builder, block, [element_1, element_2, element_3]); + + list_append(builder, block, update_mode_var, state, new_element) + }; + + let output_element_type = + layout_spec(env, builder, interner, interner.get_repr(*return_layout))?; + + let state_layout = LayoutRepr::Builtin(Builtin::List(*return_layout)); + let state_type = layout_spec(env, builder, interner, state_layout)?; + + let init_state = new_list(builder, block, output_element_type)?; + + add_loop(builder, block, state_type, init_state, loop_body) + } + ListMap4 { xs, ys, zs, ws } => { + let list1 = env.symbols[xs]; + let list2 = env.symbols[ys]; + let list3 = env.symbols[zs]; + let list4 = env.symbols[ws]; + + let loop_body = |builder: &mut FuncDefBuilder, block, state| { + let input_bag_1 = + builder.add_get_tuple_field(block, list1, LIST_BAG_INDEX)?; + let input_bag_2 = + builder.add_get_tuple_field(block, list2, LIST_BAG_INDEX)?; + let input_bag_3 = + builder.add_get_tuple_field(block, list3, LIST_BAG_INDEX)?; + let input_bag_4 = + builder.add_get_tuple_field(block, list4, LIST_BAG_INDEX)?; + + let element_1 = builder.add_bag_get(block, input_bag_1)?; + let element_2 = builder.add_bag_get(block, input_bag_2)?; + let element_3 = builder.add_bag_get(block, input_bag_3)?; + let element_4 = builder.add_bag_get(block, input_bag_4)?; + + let new_element = call_function!( + builder, + block, + [element_1, element_2, element_3, element_4] + ); + + list_append(builder, block, update_mode_var, state, new_element) + }; + + let output_element_type = + layout_spec(env, builder, interner, interner.get_repr(*return_layout))?; + + let state_layout = LayoutRepr::Builtin(Builtin::List(*return_layout)); + let state_type = layout_spec(env, builder, interner, state_layout)?; + + let init_state = new_list(builder, block, output_element_type)?; + + add_loop(builder, block, state_type, init_state, loop_body) + } + } + } + } +} + +fn list_append( + builder: &mut FuncDefBuilder, + block: BlockId, + update_mode_var: UpdateModeVar, + list: ValueId, + to_insert: ValueId, +) -> Result { + let bag = builder.add_get_tuple_field(block, list, LIST_BAG_INDEX)?; + let cell = builder.add_get_tuple_field(block, list, LIST_CELL_INDEX)?; + + let _unit = builder.add_update(block, update_mode_var, cell)?; + + let new_bag = builder.add_bag_insert(block, bag, to_insert)?; + + with_new_heap_cell(builder, block, new_bag) +} + +fn list_clone( + builder: &mut FuncDefBuilder, + block: BlockId, + update_mode_var: UpdateModeVar, + list: ValueId, +) -> Result { + let bag = builder.add_get_tuple_field(block, list, LIST_BAG_INDEX)?; + let cell = builder.add_get_tuple_field(block, list, LIST_CELL_INDEX)?; + + let _unit = builder.add_update(block, update_mode_var, cell)?; + + with_new_heap_cell(builder, block, bag) +} + +#[allow(clippy::too_many_arguments)] +fn lowlevel_spec<'a>( + builder: &mut FuncDefBuilder, + interner: &STLayoutInterner<'a>, + env: &mut Env<'a>, + block: BlockId, + layout: InLayout<'a>, + op: &LowLevel, + update_mode: roc_mono::ir::UpdateModeId, + arguments: &[Symbol], +) -> Result { + use LowLevel::*; + + let type_id = layout_spec(env, builder, interner, interner.get_repr(layout))?; + let mode = update_mode.to_bytes(); + let update_mode_var = UpdateModeVar(&mode); + + match op { + NumAdd | NumSub => { + // NOTE these numeric operations panic (e.g. on overflow) + + let pass_block = { + let block = builder.add_block(); + let value = new_num(builder, block)?; + BlockExpr(block, value) + }; + + let fail_block = { + let block = builder.add_block(); + let value = builder.add_terminate(block, type_id)?; + BlockExpr(block, value) + }; + + let sub_block = { + let block = builder.add_block(); + let choice = builder.add_choice(block, &[pass_block, fail_block])?; + + BlockExpr(block, choice) + }; + + builder.add_sub_block(block, sub_block) + } + NumToFrac => { + // just dream up a unit value + builder.add_make_tuple(block, &[]) + } + Eq | NotEq => { + // just dream up a unit value + builder.add_make_tuple(block, &[]) + } + NumLte | NumLt | NumGt | NumGte | NumCompare => { + // just dream up a unit value + builder.add_make_tuple(block, &[]) + } + ListLen => { + // TODO should this touch the heap cell? + // just dream up a unit value + builder.add_make_tuple(block, &[]) + } + ListGetUnsafe => { + // NOTE the ListGet lowlevel op is only evaluated if the index is in-bounds + let list = env.symbols[&arguments[0]]; + + let bag = builder.add_get_tuple_field(block, list, LIST_BAG_INDEX)?; + let cell = builder.add_get_tuple_field(block, list, LIST_CELL_INDEX)?; + + let _unit = builder.add_touch(block, cell)?; + + builder.add_bag_get(block, bag) + } + ListReplaceUnsafe => { + let list = env.symbols[&arguments[0]]; + let to_insert = env.symbols[&arguments[2]]; + + let bag = builder.add_get_tuple_field(block, list, LIST_BAG_INDEX)?; + let cell = builder.add_get_tuple_field(block, list, LIST_CELL_INDEX)?; + + let _unit1 = builder.add_touch(block, cell)?; + let _unit2 = builder.add_update(block, update_mode_var, cell)?; + + builder.add_bag_insert(block, bag, to_insert)?; + + let old_value = builder.add_bag_get(block, bag)?; + let new_list = with_new_heap_cell(builder, block, bag)?; + + // depending on the types, the list or value will come first in the struct + let fields = match interner.get_repr(layout) { + LayoutRepr::Struct(field_layouts) => field_layouts, + _ => unreachable!(), + }; + + match (interner.get_repr(fields[0]), interner.get_repr(fields[1])) { + (LayoutRepr::Builtin(Builtin::List(_)), LayoutRepr::Builtin(Builtin::List(_))) => { + // field name is the tie breaker, list is first in + // { list : List a, value : a } + builder.add_make_tuple(block, &[new_list, old_value]) + } + (LayoutRepr::Builtin(Builtin::List(_)), _) => { + builder.add_make_tuple(block, &[new_list, old_value]) + } + (_, LayoutRepr::Builtin(Builtin::List(_))) => { + builder.add_make_tuple(block, &[old_value, new_list]) + } + _ => unreachable!(), + } + } + ListSwap => { + let list = env.symbols[&arguments[0]]; + + let bag = builder.add_get_tuple_field(block, list, LIST_BAG_INDEX)?; + let cell = builder.add_get_tuple_field(block, list, LIST_CELL_INDEX)?; + + let _unit = builder.add_update(block, update_mode_var, cell)?; + + with_new_heap_cell(builder, block, bag) + } + ListWithCapacity => { + // essentially an empty list, capacity is not relevant for morphic + + match interner.get_repr(layout) { + LayoutRepr::Builtin(Builtin::List(element_layout)) => { + let type_id = + layout_spec(env, builder, interner, interner.get_repr(element_layout))?; + new_list(builder, block, type_id) + } + _ => unreachable!("empty array does not have a list layout"), + } + } + ListReserve => { + let list = env.symbols[&arguments[0]]; + + list_clone(builder, block, update_mode_var, list) + } + ListReleaseExcessCapacity => { + let list = env.symbols[&arguments[0]]; + + list_clone(builder, block, update_mode_var, list) + } + ListAppendUnsafe => { + let list = env.symbols[&arguments[0]]; + let to_insert = env.symbols[&arguments[1]]; + + list_append(builder, block, update_mode_var, list, to_insert) + } + StrToUtf8 => { + let string = env.symbols[&arguments[0]]; + + let u8_type = builder.add_tuple_type(&[])?; + let bag = builder.add_empty_bag(block, u8_type)?; + let cell = builder.add_get_tuple_field(block, string, LIST_CELL_INDEX)?; + + builder.add_make_tuple(block, &[cell, bag]) + } + StrFromUtf8Range => { + let list = env.symbols[&arguments[0]]; + + let cell = builder.add_get_tuple_field(block, list, LIST_CELL_INDEX)?; + let string = builder.add_make_tuple(block, &[cell])?; + + let byte_index = builder.add_make_tuple(block, &[])?; + let is_ok = builder.add_make_tuple(block, &[])?; + let problem_code = builder.add_make_tuple(block, &[])?; + + builder.add_make_tuple(block, &[byte_index, string, is_ok, problem_code]) + } + _other => { + // println!("missing {:?}", _other); + // TODO overly pessimstic + let arguments: Vec<_> = arguments.iter().map(|symbol| env.symbols[symbol]).collect(); + + let result_type = layout_spec(env, builder, interner, interner.get_repr(layout))?; + + builder.add_unknown_with(block, &arguments, result_type) + } + } +} + +fn recursive_tag_variant<'a>( + env: &mut Env<'a>, + builder: &mut impl TypeContext, + interner: &STLayoutInterner<'a>, + fields: &[InLayout<'a>], +) -> Result { + build_recursive_tuple_type(env, builder, interner, fields) +} + +fn recursive_variant_types<'a>( + env: &mut Env<'a>, + builder: &mut impl TypeContext, + interner: &STLayoutInterner<'a>, + union_layout: &UnionLayout<'a>, +) -> Result> { + use UnionLayout::*; + + let mut result; + + match union_layout { + NonRecursive(_) => { + unreachable!() + } + Recursive(tags) => { + result = Vec::with_capacity(tags.len()); + + for tag in tags.iter() { + result.push(recursive_tag_variant(env, builder, interner, tag)?); + } + } + NonNullableUnwrapped(fields) => { + result = vec![recursive_tag_variant(env, builder, interner, fields)?]; + } + NullableWrapped { + nullable_id, + other_tags: tags, + } => { + result = Vec::with_capacity(tags.len() + 1); + + let cutoff = *nullable_id as usize; + + for tag in tags[..cutoff].iter() { + result.push(recursive_tag_variant(env, builder, interner, tag)?); + } + + result.push(recursive_tag_variant(env, builder, interner, &[])?); + + for tag in tags[cutoff..].iter() { + result.push(recursive_tag_variant(env, builder, interner, tag)?); + } + } + NullableUnwrapped { + nullable_id, + other_fields: fields, + } => { + let unit = recursive_tag_variant(env, builder, interner, &[])?; + let other_type = recursive_tag_variant(env, builder, interner, fields)?; + + if *nullable_id { + // nullable_id == 1 + result = vec![other_type, unit]; + } else { + result = vec![unit, other_type]; + } + } + } + + Ok(result) +} + +#[allow(dead_code)] +fn worst_case_type(context: &mut impl TypeContext) -> Result { + let cell = context.add_heap_cell_type(); + context.add_bag_type(cell) +} + +fn expr_spec<'a>( + builder: &mut FuncDefBuilder, + interner: &STLayoutInterner<'a>, + env: &mut Env<'a>, + block: BlockId, + layout: InLayout<'a>, + expr: &Expr<'a>, +) -> Result { + use Expr::*; + + match expr { + Literal(literal) => literal_spec(builder, block, literal), + NullPointer => { + let pointer_type = layout_spec(env, builder, interner, interner.get_repr(layout))?; + + builder.add_unknown_with(block, &[], pointer_type) + } + Call(call) => call_spec(builder, interner, env, block, layout, call), + Tag { + tag_layout, + tag_id, + arguments, + reuse: _, + } => { + let data_id = build_tuple_value(builder, env, block, arguments)?; + + let value_id = match tag_layout { + UnionLayout::NonRecursive(tags) => { + let variant_types = non_recursive_variant_types(env, builder, interner, tags)?; + let value_id = build_tuple_value(builder, env, block, arguments)?; + return builder.add_make_union(block, &variant_types, *tag_id as u32, value_id); + } + UnionLayout::NonNullableUnwrapped(_) => { + let value_id = data_id; + + let type_name_bytes = recursive_tag_union_name_bytes(tag_layout).as_bytes(); + let type_name = TypeName(&type_name_bytes); + + env.type_names.insert(*tag_layout); + + return builder.add_make_named(block, MOD_APP, type_name, value_id); + } + UnionLayout::Recursive(_) => data_id, + UnionLayout::NullableWrapped { .. } => data_id, + UnionLayout::NullableUnwrapped { .. } => data_id, + }; + + let variant_types = recursive_variant_types(env, builder, interner, tag_layout)?; + + let union_id = + builder.add_make_union(block, &variant_types, *tag_id as u32, value_id)?; + + let tag_value_id = with_new_heap_cell(builder, block, union_id)?; + + let type_name_bytes = recursive_tag_union_name_bytes(tag_layout).as_bytes(); + let type_name = TypeName(&type_name_bytes); + + env.type_names.insert(*tag_layout); + + builder.add_make_named(block, MOD_APP, type_name, tag_value_id) + } + Struct(fields) => build_tuple_value(builder, env, block, fields), + UnionAtIndex { + index, + tag_id, + structure, + union_layout, + } => match union_layout { + UnionLayout::NonRecursive(_) => { + let index = (*index) as u32; + let tag_value_id = env.symbols[structure]; + let tuple_value_id = + builder.add_unwrap_union(block, tag_value_id, *tag_id as u32)?; + + builder.add_get_tuple_field(block, tuple_value_id, index) + } + UnionLayout::Recursive(_) + | UnionLayout::NullableUnwrapped { .. } + | UnionLayout::NullableWrapped { .. } => { + let index = (*index) as u32; + let tag_value_id = env.symbols[structure]; + + let type_name_bytes = recursive_tag_union_name_bytes(union_layout).as_bytes(); + let type_name = TypeName(&type_name_bytes); + + // unwrap the named wrapper + let union_id = builder.add_unwrap_named(block, MOD_APP, type_name, tag_value_id)?; + + // now we have a tuple (cell, union { ... }); decompose + let heap_cell = builder.add_get_tuple_field(block, union_id, TAG_CELL_INDEX)?; + let union_data = builder.add_get_tuple_field(block, union_id, TAG_DATA_INDEX)?; + + // we're reading from this value, so touch the heap cell + builder.add_touch(block, heap_cell)?; + + // next, unwrap the union at the tag id that we've got + let variant_id = builder.add_unwrap_union(block, union_data, *tag_id as u32)?; + + builder.add_get_tuple_field(block, variant_id, index) + } + UnionLayout::NonNullableUnwrapped { .. } => { + let index = (*index) as u32; + debug_assert!(*tag_id == 0); + + let tag_value_id = env.symbols[structure]; + + let type_name_bytes = recursive_tag_union_name_bytes(union_layout).as_bytes(); + let type_name = TypeName(&type_name_bytes); + + // the unwrapped recursive tag variant + let variant_id = + builder.add_unwrap_named(block, MOD_APP, type_name, tag_value_id)?; + + builder.add_get_tuple_field(block, variant_id, index) + } + }, + GetElementPointer { + indices, + structure, + union_layout, + .. + } => { + debug_assert!(indices.len() >= 2); + let tag_id = indices[0] as u32; + let index = indices[1]; + let tag_value_id = env.symbols[structure]; + + let type_name_bytes = recursive_tag_union_name_bytes(union_layout).as_bytes(); + let type_name = TypeName(&type_name_bytes); + + // unwrap the named wrapper + let union_id = builder.add_unwrap_named(block, MOD_APP, type_name, tag_value_id)?; + + // now we have a tuple (cell, union { ... }); decompose + let heap_cell = builder.add_get_tuple_field(block, union_id, TAG_CELL_INDEX)?; + let union_data = builder.add_get_tuple_field(block, union_id, TAG_DATA_INDEX)?; + + // we're reading from this value, so touch the heap cell + builder.add_touch(block, heap_cell)?; + + // next, unwrap the union at the tag id that we've got + let variant_id = builder.add_unwrap_union(block, union_data, tag_id)?; + + let value = builder.add_get_tuple_field(block, variant_id, index as u32)?; + + // construct the box. Here the heap_cell of the tag is re-used, I'm hoping that that + // conveys to morphic that we're borrowing into the existing tag?! + builder.add_make_tuple(block, &[heap_cell, value]) + } + + StructAtIndex { + index, structure, .. + } => { + let value_id = env.symbols[structure]; + builder.add_get_tuple_field(block, value_id, *index as u32) + } + Array { elem_layout, elems } => { + let type_id = layout_spec(env, builder, interner, interner.get_repr(*elem_layout))?; + + let list = new_list(builder, block, type_id)?; + + let mut bag = builder.add_get_tuple_field(block, list, LIST_BAG_INDEX)?; + let mut all_constants = true; + + for element in elems.iter() { + let value_id = if let ListLiteralElement::Symbol(symbol) = element { + all_constants = false; + env.symbols[symbol] + } else { + builder.add_make_tuple(block, &[]).unwrap() + }; + + bag = builder.add_bag_insert(block, bag, value_id)?; + } + + if all_constants { + new_static_list(builder, block) + } else { + with_new_heap_cell(builder, block, bag) + } + } + + EmptyArray => match interner.get_repr(layout) { + LayoutRepr::Builtin(Builtin::List(element_layout)) => { + let type_id = + layout_spec(env, builder, interner, interner.get_repr(element_layout))?; + new_list(builder, block, type_id) + } + _ => unreachable!("empty array does not have a list layout"), + }, + Reset { + symbol, + update_mode, + } + | ResetRef { + symbol, + update_mode, + } => { + let tag_value_id = env.symbols[symbol]; + + let union_layout = match interner.get_repr(layout) { + LayoutRepr::Union(ul) => ul, + _ => unreachable!(), + }; + + let type_name_bytes = recursive_tag_union_name_bytes(&union_layout).as_bytes(); + let type_name = TypeName(&type_name_bytes); + + // unwrap the named wrapper + let union_id = builder.add_unwrap_named(block, MOD_APP, type_name, tag_value_id)?; + + let heap_cell = builder.add_get_tuple_field(block, union_id, TAG_CELL_INDEX)?; + let union_data = builder.add_get_tuple_field(block, union_id, TAG_DATA_INDEX)?; + + let mode = update_mode.to_bytes(); + let update_mode_var = UpdateModeVar(&mode); + + let _unit = builder.add_update(block, update_mode_var, heap_cell)?; + + let value = with_new_heap_cell(builder, block, union_data)?; + builder.add_make_named(block, MOD_APP, type_name, value) + } + FunctionPointer { .. } => { + let pointer_type = layout_spec(env, builder, interner, interner.get_repr(layout))?; + + builder.add_unknown_with(block, &[], pointer_type) + } + ErasedMake { callee, value } => { + let value = match value { + Some(v) => box_erasure_value_unknown(builder, block, env.symbols[v]), + // model nullptr + None => box_erasure_value_unknown_nullptr(builder, block), + }?; + + let callee = env.symbols[callee]; + + erasure_make(builder, block, value, callee) + } + ErasedLoad { symbol, field } => { + let value = env.symbols[symbol]; + let loaded_type = layout_spec(env, builder, interner, interner.get_repr(layout))?; + + erasure_load(builder, block, value, *field, loaded_type) + } + RuntimeErrorFunction(_) => { + let type_id = layout_spec(env, builder, interner, interner.get_repr(layout))?; + + builder.add_terminate(block, type_id) + } + GetTagId { .. } => { + // TODO touch heap cell in recursive cases + + builder.add_make_tuple(block, &[]) + } + Alloca { initializer, .. } => { + let initializer = &initializer.as_ref().map(|s| env.symbols[s]); + let values = match initializer { + Some(initializer) => std::slice::from_ref(initializer), + None => &[], + }; + + let type_id = layout_spec(env, builder, interner, interner.get_repr(layout))?; + builder.add_unknown_with(block, values, type_id) + } + } +} + +fn literal_spec( + builder: &mut FuncDefBuilder, + block: BlockId, + literal: &Literal, +) -> Result { + use Literal::*; + + match literal { + Str(_) => new_static_string(builder, block), + Int(_) | U128(_) | Float(_) | Decimal(_) | Bool(_) | Byte(_) => { + builder.add_make_tuple(block, &[]) + } + } +} + +fn layout_spec<'a>( + env: &mut Env<'a>, + builder: &mut impl TypeContext, + interner: &STLayoutInterner<'a>, + layout: LayoutRepr<'a>, +) -> Result { + layout_spec_help(env, builder, interner, layout) +} + +fn non_recursive_variant_types<'a>( + env: &mut Env<'a>, + builder: &mut impl TypeContext, + interner: &STLayoutInterner<'a>, + tags: &[&[InLayout<'a>]], +) -> Result> { + let mut result = Vec::with_capacity(tags.len()); + + for tag in tags.iter() { + result.push(build_tuple_type(env, builder, interner, tag)?); + } + + Ok(result) +} + +fn layout_spec_help<'a>( + env: &mut Env<'a>, + builder: &mut impl TypeContext, + interner: &STLayoutInterner<'a>, + layout: LayoutRepr<'a>, +) -> Result { + use LayoutRepr::*; + + match layout { + Builtin(builtin) => builtin_spec(env, builder, interner, &builtin), + Struct(field_layouts) => build_recursive_tuple_type(env, builder, interner, field_layouts), + LambdaSet(lambda_set) => layout_spec_help( + env, + builder, + interner, + interner.get_repr(lambda_set.runtime_representation()), + ), + Union(union_layout) => { + match union_layout { + UnionLayout::NonRecursive(&[]) => { + // must model Void as Unit, otherwise we run into problems where + // we have to construct values of the void type, + // which is of course not possible + builder.add_tuple_type(&[]) + } + UnionLayout::NonRecursive(tags) => { + let variant_types = non_recursive_variant_types(env, builder, interner, tags)?; + builder.add_union_type(&variant_types) + } + UnionLayout::Recursive(_) + | UnionLayout::NullableUnwrapped { .. } + | UnionLayout::NullableWrapped { .. } + | UnionLayout::NonNullableUnwrapped(_) => { + let type_name_bytes = recursive_tag_union_name_bytes(&union_layout).as_bytes(); + let type_name = TypeName(&type_name_bytes); + + env.type_names.insert(union_layout); + + Ok(builder.add_named_type(MOD_APP, type_name)) + } + } + } + + Ptr(inner_layout) => { + let inner_type = + layout_spec_help(env, builder, interner, interner.get_repr(inner_layout))?; + let cell_type = builder.add_heap_cell_type(); + + builder.add_tuple_type(&[cell_type, inner_type]) + } + + // TODO(recursive-layouts): update once we have recursive pointer loops + RecursivePointer(union_layout) => match interner.get_repr(union_layout) { + LayoutRepr::Union(union_layout) => { + assert!(!matches!(union_layout, UnionLayout::NonRecursive(..))); + let type_name_bytes = recursive_tag_union_name_bytes(&union_layout).as_bytes(); + let type_name = TypeName(&type_name_bytes); + + Ok(builder.add_named_type(MOD_APP, type_name)) + } + _ => internal_error!("somehow, a non-recursive layout is under a recursive pointer"), + }, + + FunctionPointer(_) => function_pointer_type(builder), + Erased(_) => erasure_type(builder), + } +} + +fn builtin_spec<'a>( + env: &mut Env<'a>, + builder: &mut impl TypeContext, + interner: &STLayoutInterner<'a>, + builtin: &Builtin<'a>, +) -> Result { + use Builtin::*; + + match builtin { + Int(_) | Bool => builder.add_tuple_type(&[]), + Decimal | Float(_) => builder.add_tuple_type(&[]), + Str => str_type(builder), + List(element_layout) => { + let element_type = + layout_spec_help(env, builder, interner, interner.get_repr(*element_layout))?; + + let cell = builder.add_heap_cell_type(); + let bag = builder.add_bag_type(element_type)?; + + builder.add_tuple_type(&[cell, bag]) + } + } +} + +fn str_type(builder: &mut TC) -> Result { + let cell_id = builder.add_heap_cell_type(); + builder.add_tuple_type(&[cell_id]) +} + +fn static_list_type(builder: &mut TC) -> Result { + let unit_type = builder.add_tuple_type(&[])?; + let cell = builder.add_heap_cell_type(); + let bag = builder.add_bag_type(unit_type)?; + + builder.add_tuple_type(&[cell, bag]) +} + +const LIST_CELL_INDEX: u32 = 0; +const LIST_BAG_INDEX: u32 = 1; + +const TAG_CELL_INDEX: u32 = 0; +const TAG_DATA_INDEX: u32 = 1; + +fn with_new_heap_cell( + builder: &mut FuncDefBuilder, + block: BlockId, + value: ValueId, +) -> Result { + let cell = builder.add_new_heap_cell(block)?; + builder.add_make_tuple(block, &[cell, value]) +} + +fn new_list(builder: &mut FuncDefBuilder, block: BlockId, element_type: TypeId) -> Result { + let bag = builder.add_empty_bag(block, element_type)?; + with_new_heap_cell(builder, block, bag) +} + +fn new_static_string(builder: &mut FuncDefBuilder, block: BlockId) -> Result { + let module = MOD_APP; + + builder.add_const_ref(block, module, STATIC_STR_NAME) +} + +fn new_static_list(builder: &mut FuncDefBuilder, block: BlockId) -> Result { + let module = MOD_APP; + + builder.add_const_ref(block, module, STATIC_LIST_NAME) +} + +fn new_num(builder: &mut FuncDefBuilder, block: BlockId) -> Result { + // we model all our numbers as unit values + builder.add_make_tuple(block, &[]) +} + +fn function_pointer_type(builder: &mut TC) -> Result { + builder.add_tuple_type(&[]) +} + +const ERASURE_CALEE_INDEX: u32 = 0; +const ERASURE_VALUE_INDEX: u32 = 1; + +/// Erasure type modeled as +/// +/// ```text +/// Tuple(callee: FnPtr, value: HeapCell) +/// ``` +fn erasure_type(builder: &mut TC) -> Result { + let value_cell_id = builder.add_heap_cell_type(); + let callee_id = function_pointer_type(builder)?; + builder.add_tuple_type(&[value_cell_id, callee_id]) +} + +fn erasure_box_value_type(builder: &mut TC) -> TypeId { + builder.add_heap_cell_type() +} + +fn box_erasure_value_unknown( + builder: &mut FuncDefBuilder, + block: BlockId, + value: ValueId, +) -> Result { + let heap_cell = erasure_box_value_type(builder); + builder.add_unknown_with(block, &[value], heap_cell) +} + +fn box_erasure_value_unknown_nullptr( + builder: &mut FuncDefBuilder, + block: BlockId, +) -> Result { + let heap_cell = erasure_box_value_type(builder); + builder.add_unknown_with(block, &[], heap_cell) +} + +/// Erasure value modeled as +/// +/// ```text +/// callee = make_tuple(&[]) +/// value = unknown(make_tuple(...captures)) +/// +/// x : Tuple(callee: FnPtr, value: HeapCell) +/// x = make_tuple(callee, value) +/// ``` +fn erasure_make( + builder: &mut FuncDefBuilder, + block: BlockId, + value: ValueId, + callee: ValueId, +) -> Result { + builder.add_make_tuple(block, &[value, callee]) +} + +fn erasure_load( + builder: &mut FuncDefBuilder, + block: BlockId, + value: ValueId, + field: ErasedField, + loaded_type: TypeId, +) -> Result { + match field { + ErasedField::Callee => builder.add_get_tuple_field(block, value, ERASURE_CALEE_INDEX), + ErasedField::Value | ErasedField::ValuePtr => { + let unknown_heap_cell_value = + builder.add_get_tuple_field(block, value, ERASURE_VALUE_INDEX)?; + // Cast the unknown cell to the wanted type + builder.add_unknown_with(block, &[unknown_heap_cell_value], loaded_type) + } + } +} diff --git a/crates/compiler/arena_pool/Cargo.toml b/crates/compiler/arena_pool/Cargo.toml new file mode 100644 index 0000000000..ab35ee4d37 --- /dev/null +++ b/crates/compiler/arena_pool/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "arena-pool" +description = "An implementation of an arena allocator designed for the compiler's workloads." + +authors.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true +version.workspace = true + +[dependencies] +roc_error_macros = { path = "../../error_macros" } diff --git a/crates/compiler/arena_pool/src/lib.rs b/crates/compiler/arena_pool/src/lib.rs new file mode 100644 index 0000000000..3f35a74c62 --- /dev/null +++ b/crates/compiler/arena_pool/src/lib.rs @@ -0,0 +1,2 @@ +//! An implementation of an [arena allocator](https://mgravell.github.io/Pipelines.Sockets.Unofficial/docs/arenas.html) designed for the compiler's workloads. +pub mod pool; diff --git a/crates/compiler/arena_pool/src/pool.rs b/crates/compiler/arena_pool/src/pool.rs new file mode 100644 index 0000000000..2dc2286723 --- /dev/null +++ b/crates/compiler/arena_pool/src/pool.rs @@ -0,0 +1,397 @@ +use roc_error_macros::internal_error; +use std::marker::PhantomPinned; +use std::ptr::{copy_nonoverlapping, NonNull}; + +pub struct ArenaRef { + ptr: NonNull, + _pin: PhantomPinned, +} + +impl ArenaRef { + pub fn get<'a, A: AsArena>(&'a self, arena: &A) -> &'a T { + arena.verify_ownership(self.ptr); + + // SAFETY: we know this pointer is safe to follow because it will only + // get deallocated once the pool where it was created gets deallocated + // (along with all of the Arenas it detached), and we just verified that + // this ArenaRef's ID matches a pool which has not yet been deallocated. + unsafe { self.ptr.as_ref() } + } + + pub fn get_mut<'a, A: AsArena>(&'a mut self, arena: &A) -> &'a mut T { + arena.verify_ownership(self.ptr); + + // SAFETY: we know this pointer is safe to follow because it will only + // get deallocated once the pool where it was created gets deallocated + // (along with all of the Arenas it detached), and we just verified that + // this ArenaRef's ID matches a pool which has not yet been deallocated. + unsafe { self.ptr.as_mut() } + } +} + +/// Like a Vec, except the capacity you give it initially is its maximum +/// capacity forever. If you ever exceed it, it'll panic! +pub struct ArenaVec { + buffer_ptr: NonNull, + len: usize, + capacity: usize, + _pin: PhantomPinned, +} + +impl ArenaVec { + pub fn new_in(arena: &mut Arena) -> Self { + // We can't start with a NonNull::dangling pointer because when we go + // to push elements into this, they'll try to verify the dangling + // pointer resides in the arena it was given, which will likely panic. + // + // Instead, we'll take a pointer inside the array but never use it + // other than for verification, because our capacity is 0. + Self::with_capacity_in(0, arena) + } + + pub fn with_capacity_in(capacity: usize, arena: &mut Arena) -> Self { + let ptr = arena.alloc_vec(capacity); + + Self { + buffer_ptr: unsafe { NonNull::new_unchecked(ptr) }, + capacity, + len: 0, + _pin: PhantomPinned, + } + } + + pub fn push(&mut self, val: T, arena: &mut Arena) { + // Verify that this is the arena where we originally got our buffer, + // and is therefore safe to read and to write to. (If we have sufficient + // capacity, we'll write to it, and otherwise we'll read from it when + // copying our buffer over to the new reserved block.) + arena.verify_ownership(self.buffer_ptr); + + if self.len <= self.capacity { + // We're all set! + // + // This empty branch is just here for branch prediction, + // since this should be the most common case in practice. + } else { + // Double our capacity and reserve a new block. + self.capacity *= 2; + + let ptr = arena.alloc_vec(self.capacity); + + // SAFETY: the existing buffer must have at least self.len elements, + // as must the new one, so copying that many between them is safe. + unsafe { + // Copy all elements from the current buffer into the new one + copy_nonoverlapping(self.buffer_ptr.as_ptr(), ptr, self.len); + } + + self.buffer_ptr = unsafe { NonNull::new_unchecked(ptr) }; + } + + // Store the element in the appropriate memory address. + let elem_ptr = unsafe { &mut *self.buffer_ptr.as_ptr().add(self.len) }; + + *elem_ptr = val; + + self.len += 1; + } + + pub fn get<'a>(&'a self, index: usize, arena: &Arena) -> Option<&'a T> { + arena.verify_ownership(self.buffer_ptr); + + if index < self.len { + // SAFETY: we know this pointer is safe to follow because we've + // done a bounds check, and because we know it will only get + // deallocated once the pool where it was created gets deallocated + // (along with all of the Arenas it detached), and we just verified that + // this ArenaRef's ID matches a pool which has not yet been deallocated. + Some(unsafe { &*self.buffer_ptr.as_ptr().add(index) }) + } else { + None + } + } + + pub fn get_mut<'a>(&'a mut self, index: usize, arena: &Arena) -> Option<&'a mut T> { + arena.verify_ownership(self.buffer_ptr); + + if index < self.len { + // SAFETY: we know this pointer is safe to follow because we've + // done a bounds check, and because we know it will only get + // deallocated once the pool where it was created gets deallocated + // (along with all of the Arenas it detached), and we just verified that + // this ArenaRef's ID matches a pool which has not yet been deallocated. + Some(unsafe { &mut *self.buffer_ptr.as_ptr().add(index) }) + } else { + None + } + } +} + +#[derive(PartialEq, Eq)] +pub struct ArenaPool { + first_chunk: Vec, + extra_chunks: Vec>, + num_leased: usize, + default_chunk_capacity: usize, +} + +impl ArenaPool { + const DEFAULT_CHUNK_SIZE: usize = 1024; + + /// Be careful! Both of these arguments are of type usize. + /// + /// The first is the number of elements that will be in each arena. + /// The second is the number of arenas. + /// + /// This returns a new Pool, and also an iterator of Arenas. These Arenas can + /// be given to different threads, where they can be used to allocate + /// ArenaRef and ArenaVec values which can then be dereferenced by the Arena + /// that created them, or by this pool once those Arenas have been + /// reabsorbed back into it. + /// + /// (A word of warning: if you try to use this pool to dereference ArenaRec + /// and ArenaVec values which were allocated by arenas that have *not* yet + /// been reabsorbed, it may work some of the time and panic other times, + /// depending on whether the arena needed to allocate extra chunks beyond + /// its initial chunk. tl;dr - doing that may panic, so don't try it!) + /// + /// Before this pool gets dropped, you must call reabsorb() on every + /// arena that has been leased - otherwise, you'll get a panic when this + /// gets dropped! The memory safety of the system depends on all arenas + /// having been reabsorbed before the pool gets deallocated, which is why + /// the pool's Drop implementation enforces it. + pub fn new(num_arenas: usize, elems_per_arena: usize) -> (ArenaPool, ArenaIter) { + Self::with_chunk_size(num_arenas, elems_per_arena, Self::DEFAULT_CHUNK_SIZE) + } + + /// Like `new`, except you can also specify the chunk size that each + /// arena will use to allocate its extra chunks if it runs out of space + /// in its main buffer. + /// + /// Things will run fastest if that main buffer never runs out, though! + pub fn with_chunk_size( + num_arenas: usize, + elems_per_arena: usize, + chunk_size: usize, + ) -> (ArenaPool, ArenaIter) { + let mut first_chunk = Vec::with_capacity(elems_per_arena * num_arenas); + let iter = ArenaIter { + ptr: first_chunk.as_mut_ptr(), + quantity_remaining: num_arenas, + first_chunk_capacity: elems_per_arena, + }; + let pool = Self { + first_chunk, + extra_chunks: Vec::new(), + num_leased: num_arenas, + default_chunk_capacity: chunk_size, + }; + + (pool, iter) + } + + /// Return an arena to the pool. (This would have been called "return" but + /// that's a reserved keyword.) + pub fn reabsorb(&mut self, arena: Arena) { + // Ensure we're reabsorbing an arena that was + // actually leased by this pool in the first place! + verify_ownership( + self.first_chunk.as_ptr(), + self.first_chunk.capacity(), + &self.extra_chunks, + arena.first_chunk_ptr, + ); + + // Add the arena's extra chunks to our own, so their memory remains live + // after the arena gets dropped. This is important, because at this + // point their pointers can still potentially be dereferenced! + self.extra_chunks.extend(arena.extra_chunks); + + self.num_leased -= 1; + } +} + +impl Drop for ArenaPool { + fn drop(&mut self) { + // When an ArenaPool gets dropped, it must not have any leased + // arenas remaining. If it does, there will be outstanding IDs which + // could be used with those non-reabsorbed Arenas to read freed memory! + // This would be a use-after-free; we panic rather than permit that. + assert_eq!(self.num_leased, 0); + } +} + +pub struct ArenaIter { + ptr: *mut T, + quantity_remaining: usize, + first_chunk_capacity: usize, +} + +// Implement `Iterator` for `Fibonacci`. +// The `Iterator` trait only requires a method to be defined for the `next` element. +impl Iterator for ArenaIter { + type Item = Arena; + + // Here, we define the sequence using `.curr` and `.next`. + // The return type is `Option`: + // * When the `Iterator` is finished, `None` is returned. + // * Otherwise, the next value is wrapped in `Some` and returned. + fn next(&mut self) -> Option> { + if self.quantity_remaining != 0 { + let first_chunk_ptr = self.ptr; + + self.ptr = unsafe { self.ptr.add(self.first_chunk_capacity) }; + self.quantity_remaining -= 1; + + Some(Arena { + first_chunk_ptr, + first_chunk_len: 0, + first_chunk_cap: self.first_chunk_capacity, + extra_chunks: Vec::new(), + }) + } else { + None + } + } +} + +#[derive(PartialEq, Eq)] +pub struct Arena { + first_chunk_ptr: *mut T, + first_chunk_len: usize, + first_chunk_cap: usize, + extra_chunks: Vec>, +} + +impl Arena { + pub fn alloc(&mut self, val: T) -> ArenaRef { + let ptr: *mut T = if self.first_chunk_len < self.first_chunk_cap { + // We have enough room in the first chunk for 1 allocation. + self.first_chunk_len += 1; + + // Return a pointer to the next available slot. + unsafe { self.first_chunk_ptr.add(self.first_chunk_len) } + } else { + // We ran out of space in the first chunk, so we turn to extra chunks. + // First, ensure that we have an extra chunk with enough space in it. + match self.extra_chunks.last() { + Some(chunk) => { + if chunk.len() >= chunk.capacity() { + // We've run out of space in our last chunk. Create a new one! + self.extra_chunks + .push(Vec::with_capacity(self.first_chunk_cap)); + } + } + None => { + // We've never had extra chunks until now. Create the first one! + self.extra_chunks + .push(Vec::with_capacity(self.first_chunk_cap)); + } + } + + let chunk = self.extra_chunks.last_mut().unwrap(); + let index = chunk.len(); + + chunk.push(val); + + // Get a pointer to a memory address within our particular chunk. + &mut chunk[index] + }; + + ArenaRef { + ptr: unsafe { NonNull::new_unchecked(ptr) }, + _pin: PhantomPinned, + } + } + + fn alloc_vec(&mut self, num_elems: usize) -> *mut T { + if self.first_chunk_len + num_elems <= self.first_chunk_cap { + // We have enough room in the first chunk for this vec. + self.first_chunk_len += num_elems; + + // Return a pointer to the next available element. + unsafe { self.first_chunk_ptr.add(self.first_chunk_len) } + } else { + let new_chunk_cap = self.first_chunk_cap.max(num_elems); + + // We ran out of space in the first chunk, so we turn to extra chunks. + // First, ensure that we have an extra chunk with enough space in it. + match self.extra_chunks.last() { + Some(chunk) => { + if chunk.len() + num_elems >= chunk.capacity() { + // We don't have enough space in our last chunk. + // Create a new one! + self.extra_chunks.push(Vec::with_capacity(new_chunk_cap)); + } + } + None => { + // We've never had extra chunks until now. Create the first one! + self.extra_chunks.push(Vec::with_capacity(new_chunk_cap)); + } + } + + let chunk = self.extra_chunks.last_mut().unwrap(); + let index = chunk.len(); + + // Get a pointer to a memory address within our particular chunk. + &mut chunk[index] + } + } +} + +pub trait AsArena { + fn verify_ownership(&self, ptr: NonNull); +} + +impl AsArena for ArenaPool { + fn verify_ownership(&self, ptr: NonNull) { + verify_ownership( + self.first_chunk.as_ptr(), + self.first_chunk.capacity(), + &self.extra_chunks, + ptr.as_ptr(), + ); + } +} + +impl AsArena for Arena { + fn verify_ownership(&self, ptr: NonNull) { + verify_ownership( + self.first_chunk_ptr, + self.first_chunk_cap, + &self.extra_chunks, + ptr.as_ptr(), + ); + } +} + +fn verify_ownership( + first_chunk_ptr: *const T, + first_chunk_cap: usize, + extra_chunks: &[Vec], + ptr: *const T, +) { + let addr = ptr as usize; + let start_addr = first_chunk_ptr as usize; + let end_addr = start_addr + first_chunk_cap; + + if start_addr <= addr && addr < end_addr { + // This is within our first chunk's address space, so it's verified! + } else { + // This wasn't within our first chunk's address space, so we need + // to see if we can find it in one of our extra_chunks. + for chunk in extra_chunks { + let start_addr = chunk.as_ptr() as usize; + let end_addr = start_addr + chunk.capacity(); + + if start_addr <= addr && addr < end_addr { + // Found it! No need to loop anymore; verification passed. + return; + } + } + + // The address wasn't within any of our chunks' bounds. + // Panic to avoid use-after-free errors! + internal_error!("Pointer ownership verification failed."); + } +} diff --git a/compiler/arena_pool/tests/test_arena_pool.rs b/crates/compiler/arena_pool/tests/test_arena_pool.rs similarity index 100% rename from compiler/arena_pool/tests/test_arena_pool.rs rename to crates/compiler/arena_pool/tests/test_arena_pool.rs diff --git a/crates/compiler/build/Cargo.toml b/crates/compiler/build/Cargo.toml new file mode 100644 index 0000000000..2dae039b57 --- /dev/null +++ b/crates/compiler/build/Cargo.toml @@ -0,0 +1,56 @@ +[package] +name = "roc_build" +description = "Responsible for coordinating building and linking of a Roc app with its host." + +authors.workspace = true +edition.workspace = true +license.workspace = true +version.workspace = true + +[dependencies] +roc_bitcode = { path = "../builtins/bitcode" } +roc_can = { path = "../can" } +roc_collections = { path = "../collections" } +roc_constrain = { path = "../constrain" } +roc_error_macros = { path = "../../error_macros" } +roc_gen_dev = { path = "../gen_dev", default-features = false } +roc_gen_llvm = { path = "../gen_llvm" } +roc_gen_wasm = { path = "../gen_wasm" } +roc_linker = { path = "../../linker" } +roc_load = { path = "../load" } +roc_module = { path = "../module" } +roc_mono = { path = "../mono" } +roc_packaging = { path = "../../packaging" } +roc_parse = { path = "../parse" } +roc_problem = { path = "../problem" } +roc_region = { path = "../region" } +roc_reporting = { path = "../../reporting" } +roc_solve_problem = { path = "../solve_problem" } +roc_std = { path = "../../roc_std" } +roc_target = { path = "../roc_target" } +roc_types = { path = "../types" } +roc_unify = { path = "../unify" } +roc_command_utils = { path = "../../utils/command" } + +wasi_libc_sys = { path = "../../wasi-libc-sys" } + +bumpalo.workspace = true +indoc.workspace = true +inkwell.workspace = true +libloading.workspace = true +target-lexicon.workspace = true +tempfile.workspace = true + +[target.'cfg(target_os = "macos")'.dependencies] +serde_json.workspace = true + +[features] +target-aarch64 = ["roc_gen_dev/target-aarch64"] +target-arm = [] +target-wasm32 = [] +target-x86 = [] +target-x86_64 = ["roc_gen_dev/target-x86_64"] + +# This is used to enable fuzzing and sanitizers. +# Example use is describe here: https://github.com/bhansconnect/roc-fuzz +sanitizers = [] diff --git a/crates/compiler/build/src/lib.rs b/crates/compiler/build/src/lib.rs new file mode 100644 index 0000000000..8cc3304900 --- /dev/null +++ b/crates/compiler/build/src/lib.rs @@ -0,0 +1,7 @@ +//! Responsible for coordinating building and linking of a Roc app with its host. +#![warn(clippy::dbg_macro)] +// See github.com/roc-lang/roc/issues/800 for discussion of the large_enum_variant check. +#![allow(clippy::large_enum_variant)] +pub mod link; +pub mod program; +pub mod target; diff --git a/crates/compiler/build/src/link.rs b/crates/compiler/build/src/link.rs new file mode 100644 index 0000000000..8e3b016f10 --- /dev/null +++ b/crates/compiler/build/src/link.rs @@ -0,0 +1,1430 @@ +use crate::target::{arch_str, target_zig_str}; +use libloading::{Error, Library}; +use roc_command_utils::{cargo, clang, rustup, zig}; +use roc_error_macros::internal_error; +use roc_mono::ir::OptLevel; +use std::collections::HashMap; +use std::ffi::OsString; +use std::fs::DirEntry; +use std::io; +use std::path::{Path, PathBuf}; +use std::process::{self, Child, Command}; +use std::{env, fs}; +use target_lexicon::{Architecture, OperatingSystem, Triple}; +use wasi_libc_sys::{WASI_COMPILER_RT_PATH, WASI_LIBC_PATH}; + +pub use roc_linker::LinkType; + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum LinkingStrategy { + /// Compile app and host object files, then use a linker like lld or wasm-ld + Legacy, + /// Compile app and host object files, then use the Roc surgical linker + Surgical, + /// Initialise the backend from a host object file, then add the app to it. No linker needed. + Additive, +} + +/// input_paths can include the host as well as the app. e.g. &["host.o", "roc_app.o"] +pub fn link( + target: &Triple, + output_path: PathBuf, + input_paths: &[&str], + link_type: LinkType, +) -> io::Result<(Child, PathBuf)> { + match target { + Triple { + architecture: Architecture::Wasm32, + .. + } => link_wasm32(target, output_path, input_paths, link_type), + Triple { + operating_system: OperatingSystem::Linux, + .. + } => link_linux(target, output_path, input_paths, link_type), + Triple { + operating_system: OperatingSystem::Darwin, + .. + } => link_macos(target, output_path, input_paths, link_type), + Triple { + operating_system: OperatingSystem::Windows, + .. + } => link_windows(target, output_path, input_paths, link_type), + _ => internal_error!("TODO gracefully handle unsupported target: {:?}", target), + } +} + +/// Same format as the precompiled host filename, except with a file extension like ".o" or ".obj" +pub fn legacy_host_file(target: &Triple, platform_main_roc: &Path) -> Option { + let os = roc_target::OperatingSystem::from(target.operating_system); + let lib_ext = os.static_library_file_ext(); + + let file_name = roc_linker::preprocessed_host_filename(target)? + .replace(roc_linker::PRECOMPILED_HOST_EXT, lib_ext); + + let lib_path = platform_main_roc.with_file_name(file_name); + if lib_path.exists() { + Some(lib_path) + } else { + let obj_ext = os.object_file_ext(); + Some(lib_path.with_extension(obj_ext)) + } +} + +// Attempts to find a file that is stored relative to the roc executable. +// Since roc is built in target/debug/roc, we may need to drop that path to find the file. +// This is used to avoid depending on the current working directory. +pub fn get_relative_path(sub_path: &Path) -> Option { + if sub_path.is_absolute() { + internal_error!( + "get_relative_path requires sub_path to be relative, instead got {:?}", + sub_path + ); + } + let exe_relative_str_path_opt = std::env::current_exe().ok(); + + if let Some(exe_relative_str_path) = exe_relative_str_path_opt { + #[cfg(windows)] + let exe_relative_str_path = roc_command_utils::strip_windows_prefix(&exe_relative_str_path); + + let mut curr_parent_opt = exe_relative_str_path.parent(); + + // We need to support paths like ./roc, ./bin/roc, ./target/debug/roc and tests like ./target/debug/deps/valgrind-63c787aa176d1277 + // This requires dropping up to 3 directories. + for _ in 0..=3 { + if let Some(curr_parent) = curr_parent_opt { + let potential_path = curr_parent.join(sub_path); + + if std::path::Path::exists(&potential_path) { + return Some(potential_path); + } else { + curr_parent_opt = curr_parent.parent(); + } + } else { + break; + } + } + } + + None +} + +fn find_zig_glue_path() -> PathBuf { + // First try using the repo path relative to the executable location. + let path = get_relative_path(Path::new("crates/compiler/builtins/bitcode/src/glue.zig")); + if let Some(path) = path { + return path; + } + // Fallback on a lib path relative to the executable location. + let path = get_relative_path(Path::new("lib/glue.zig")); + if let Some(path) = path { + return path; + } + + internal_error!("cannot find `glue.zig`. Check the source code in find_zig_glue_path() to show all the paths I tried.") +} + +fn find_wasi_libc_path() -> PathBuf { + // Environment variable defined in wasi-libc-sys/build.rs + let wasi_libc_pathbuf = PathBuf::from(WASI_LIBC_PATH); + if std::path::Path::exists(&wasi_libc_pathbuf) { + return wasi_libc_pathbuf; + } + + internal_error!("cannot find `wasi-libc.a`") +} + +#[cfg(unix)] +#[allow(clippy::too_many_arguments)] +pub fn build_zig_host_native( + env_path: &str, + env_home: &str, + emit_bin: &str, + zig_host_src: &str, + target: &str, + opt_level: OptLevel, + shared_lib_path: Option<&Path>, + builtins_host_path: &Path, +) -> Command { + let mut zig_cmd = zig(); + zig_cmd + .env_clear() + .env("PATH", env_path) + .env("HOME", env_home); + + if let Some(shared_lib_path) = shared_lib_path { + // with LLVM, the builtins are already part of the roc app, + // but with the dev backend, they are missing. To minimize work, + // we link them as part of the host executable + zig_cmd.args([ + "build-exe", + "-fPIE", + "-rdynamic", // make sure roc_alloc and friends are exposed + shared_lib_path.to_str().unwrap(), + builtins_host_path.to_str().unwrap(), + ]); + } else { + zig_cmd.args(["build-obj", "-fPIC"]); + } + + zig_cmd.args([ + zig_host_src, + &format!("-femit-bin={emit_bin}"), + "--mod", + &format!("glue::{}", find_zig_glue_path().to_str().unwrap()), + "--deps", + "glue", + // include libc + "-lc", + // cross-compile? + "-target", + target, + ]); + + // some examples need the compiler-rt in the app object file. + // but including it on windows causes weird crashes, at least + // when we use zig 0.9. It looks like zig 0.10 is going to fix + // this problem for us, so this is a temporary workaround + if !target.contains("windows") { + zig_cmd.args([ + // include the zig runtime + "-fcompiler-rt", + ]); + } + + // valgrind does not yet support avx512 instructions, see #1963. + if env::var("NO_AVX512").is_ok() { + zig_cmd.args(["-mcpu", "x86_64"]); + } + + if matches!(opt_level, OptLevel::Optimize) { + zig_cmd.args(["-O", "ReleaseSafe"]); + } else if matches!(opt_level, OptLevel::Size) { + zig_cmd.args(["-O", "ReleaseSmall", "-fno-strip"]); + } + + zig_cmd +} + +#[cfg(windows)] +#[allow(clippy::too_many_arguments)] +pub fn build_zig_host_native( + env_path: &str, + env_home: &str, + emit_bin: &str, + zig_host_src: &str, + target: &str, + opt_level: OptLevel, + shared_lib_path: Option<&Path>, + builtins_host_path: &Path, +) -> Command { + // to prevent `clang failed with stderr: zig: error: unable to make temporary file: No such file or directory` + let env_userprofile = env::var("USERPROFILE").unwrap_or_else(|_| "".to_string()); + + let mut zig_cmd = zig(); + zig_cmd + .env_clear() + .env("PATH", env_path) + .env("HOME", env_home) + .env("USERPROFILE", env_userprofile); + + if let Some(shared_lib_path) = shared_lib_path { + zig_cmd.args(&[ + "build-exe", + // "-fPIE", PIE seems to fail on windows + shared_lib_path.to_str().unwrap(), + builtins_host_path.to_str().unwrap(), + ]); + } else { + zig_cmd.args(&["build-obj"]); + } + + zig_cmd.args(&[ + zig_host_src, + &format!("-femit-bin={}", emit_bin), + "--mod", + &format!("glue::{}", find_zig_glue_path().to_str().unwrap()), + "--deps", + "glue", + // include the zig runtime + // "-fcompiler-rt", compiler-rt causes segfaults on windows; investigate why + // include libc + "-lc", + "-rdynamic", + // cross-compile? + "-target", + target, + ]); + + if matches!(opt_level, OptLevel::Optimize) { + zig_cmd.args(&["-O", "ReleaseSafe"]); + } else if matches!(opt_level, OptLevel::Size) { + zig_cmd.args(&["-O", "ReleaseSmall"]); + } + + zig_cmd +} + +pub fn build_zig_host_wasm32( + env_path: &str, + env_home: &str, + emit_bin: &str, + zig_host_src: &str, + opt_level: OptLevel, + shared_lib_path: Option<&Path>, +) -> Command { + if shared_lib_path.is_some() { + unimplemented!("Linking a shared library to wasm not yet implemented"); + } + + // NOTE currently just to get compiler warnings if the host code is invalid. + // the produced artifact is not used + let mut zig_cmd = zig(); + + zig_cmd + .env_clear() + .env("PATH", env_path) + .env("HOME", env_home) + .args([ + "build-obj", + zig_host_src, + emit_bin, + "--mod", + &format!("glue::{}", find_zig_glue_path().to_str().unwrap()), + "--deps", + "glue", + // include the zig runtime + // "-fcompiler-rt", + // include libc + "--library", + "c", + "-target", + "wasm32-wasi", + // "-femit-llvm-ir=/home/folkertdev/roc/roc/crates/cli_testing_examples/benchmarks/platform/host.ll", + "-fPIC", + "-fstrip", + ]); + + if matches!(opt_level, OptLevel::Optimize) { + zig_cmd.args(["-O", "ReleaseSafe"]); + } else if matches!(opt_level, OptLevel::Size) { + zig_cmd.args(["-O", "ReleaseSmall"]); + } + + zig_cmd +} + +#[allow(clippy::too_many_arguments)] +pub fn build_c_host_native( + target: &Triple, + env_path: &str, + env_home: &str, + env_cpath: &str, + dest: &str, + sources: &[&str], + opt_level: OptLevel, + shared_lib_path: Option<&Path>, + builtins_host_path: &Path, +) -> Command { + let mut clang_cmd = clang(); + clang_cmd + .env_clear() + .env("PATH", env_path) + .env("CPATH", env_cpath) + .env("HOME", env_home) + .args(sources) + .args(["-o", dest]); + if let Some(shared_lib_path) = shared_lib_path { + match target.operating_system { + OperatingSystem::Windows => { + // just use zig as a C compiler + + // I think we only ever have one C source file in practice + assert_eq!(sources.len(), 1); + + return build_zig_host_native( + env_path, + env_home, + dest, + sources[0], + get_target_str(target), + opt_level, + Some(shared_lib_path), + builtins_host_path, + ); + } + _ => { + clang_cmd.args([ + shared_lib_path.to_str().unwrap(), + // This line is commented out because + // @bhansconnect: With the addition of Str.graphemes, always + // linking the built-ins led to a surgical linker bug for + // optimized builds. Disabling until it is needed for dev + // builds. + // builtins_host_path, + "-fPIE", + "-pie", + "-lm", + "-lpthread", + "-ldl", + "-lrt", + "-lutil", + ]); + } + } + } else { + clang_cmd.args(["-fPIC", "-c"]); + } + if matches!(opt_level, OptLevel::Optimize) { + clang_cmd.arg("-O3"); + } else if matches!(opt_level, OptLevel::Size) { + clang_cmd.arg("-Os"); + } + + clang_cmd +} + +#[allow(clippy::too_many_arguments)] +pub fn build_swift_host_native( + env_path: &str, + env_home: &str, + dest: &str, + sources: &[&str], + opt_level: OptLevel, + shared_lib_path: Option<&Path>, + objc_header_path: Option<&str>, + arch: Architecture, +) -> Command { + if shared_lib_path.is_some() { + unimplemented!("Linking a shared library to Swift not yet implemented"); + } + + let mut command = Command::new("arch"); + command + .env_clear() + .env("PATH", env_path) + .env("HOME", env_home); + + match arch { + Architecture::Aarch64(_) => command.arg("-arm64"), + _ => command.arg(format!("-{arch}")), + }; + + command + .arg("xcrun") // xcrun helps swiftc to find the right header files + .arg("swiftc") + .args(sources) + .arg("-emit-object") + // `-module-name host` renames the .o file to "host" - otherwise you get an error like: + // error: module name "legacy_macos-arm64" is not a valid identifier; use -module-name flag to specify an alternate name + .arg("-module-name") + .arg("host") + .arg("-parse-as-library") + .args(["-o", dest]); + + if let Some(objc_header) = objc_header_path { + command.args(["-import-objc-header", objc_header]); + } + + if matches!(opt_level, OptLevel::Optimize) { + command.arg("-O"); + } else if matches!(opt_level, OptLevel::Size) { + command.arg("-Osize"); + } + + command +} + +pub fn rebuild_host( + opt_level: OptLevel, + target: &Triple, + platform_main_roc: &Path, + shared_lib_path: Option<&Path>, +) -> PathBuf { + let c_host_src = platform_main_roc.with_file_name("host.c"); + let c_host_dest = platform_main_roc.with_file_name("c_host.o"); + let zig_host_src = platform_main_roc.with_file_name("host.zig"); + let rust_host_src = platform_main_roc.with_file_name("host.rs"); + let rust_host_dest = platform_main_roc.with_file_name("rust_host.o"); + let cargo_host_src = platform_main_roc.with_file_name("Cargo.toml"); + let swift_host_src = platform_main_roc.with_file_name("host.swift"); + let swift_host_header_src = platform_main_roc.with_file_name("host.h"); + + let os = roc_target::OperatingSystem::from(target.operating_system); + let executable_extension = match os { + roc_target::OperatingSystem::Windows => "exe", + roc_target::OperatingSystem::Unix => "", + roc_target::OperatingSystem::Wasi => "", + }; + + let host_dest = if matches!(target.architecture, Architecture::Wasm32) { + if matches!(opt_level, OptLevel::Development) { + platform_main_roc.with_extension("o") + } else { + platform_main_roc.with_extension("bc") + } + } else if shared_lib_path.is_some() { + platform_main_roc + .with_file_name("dynhost") + .with_extension(executable_extension) + } else { + legacy_host_file(target, platform_main_roc).unwrap() + }; + + let env_path = env::var("PATH").unwrap_or_else(|_| "".to_string()); + let env_home = env::var("HOME").unwrap_or_else(|_| "".to_string()); + let env_cpath = env::var("CPATH").unwrap_or_else(|_| "".to_string()); + + let builtins_host_tempfile = + roc_bitcode::host_tempfile().expect("failed to write host builtins object to tempfile"); + + if zig_host_src.exists() { + // Compile host.zig + let zig_cmd = match target.architecture { + Architecture::Wasm32 => { + let emit_bin = if matches!(opt_level, OptLevel::Development) { + format!("-femit-bin={}", host_dest.to_str().unwrap()) + } else { + format!("-femit-llvm-ir={}", host_dest.to_str().unwrap()) + }; + build_zig_host_wasm32( + &env_path, + &env_home, + &emit_bin, + zig_host_src.to_str().unwrap(), + opt_level, + shared_lib_path, + ) + } + Architecture::X86_64 => build_zig_host_native( + &env_path, + &env_home, + host_dest.to_str().unwrap(), + zig_host_src.to_str().unwrap(), + get_target_str(target), + opt_level, + shared_lib_path, + builtins_host_tempfile.path(), + ), + Architecture::X86_32(_) => build_zig_host_native( + &env_path, + &env_home, + host_dest.to_str().unwrap(), + zig_host_src.to_str().unwrap(), + "i386-linux-musl", + opt_level, + shared_lib_path, + builtins_host_tempfile.path(), + ), + Architecture::Aarch64(_) => build_zig_host_native( + &env_path, + &env_home, + host_dest.to_str().unwrap(), + zig_host_src.to_str().unwrap(), + target_zig_str(target), + opt_level, + shared_lib_path, + builtins_host_tempfile.path(), + ), + _ => internal_error!("Unsupported architecture {:?}", target.architecture), + }; + + run_build_command(zig_cmd, "host.zig", 0); + } else if cargo_host_src.exists() { + // Compile and link Cargo.toml, if it exists + let cargo_dir = platform_main_roc.parent().unwrap(); + + let mut cargo_cmd = if cfg!(windows) { + // on windows, we need the nightly toolchain so we can use `-Z export-executable-symbols` + // using `+nightly` only works when running cargo through rustup + let mut cmd = rustup(); + cmd.args(["run", "nightly-2023-05-28", "cargo"]); + + cmd + } else { + cargo() + }; + + cargo_cmd.arg("build").current_dir(cargo_dir); + // Rust doesn't expose size without editing the cargo.toml. Instead just use release. + if matches!(opt_level, OptLevel::Optimize | OptLevel::Size) { + cargo_cmd.arg("--release"); + } + + let source_file = if shared_lib_path.is_some() { + let rust_flags = if cfg!(windows) { + "-Z export-executable-symbols" + } else { + "-C link-args=-rdynamic" + }; + cargo_cmd.env("RUSTFLAGS", rust_flags); + cargo_cmd.args(["--bin", "host"]); + "src/main.rs" + } else { + cargo_cmd.arg("--lib"); + "src/lib.rs" + }; + + run_build_command(cargo_cmd, source_file, 0); + + let cargo_out_dir = find_used_target_sub_folder(opt_level, cargo_dir.join("target")); + + if shared_lib_path.is_some() { + // For surgical linking, just copy the dynamically linked rust app. + let mut exe_path = cargo_out_dir.join("host"); + exe_path.set_extension(executable_extension); + if let Err(e) = std::fs::copy(&exe_path, &host_dest) { + internal_error!( + "unable to copy {} => {}: {:?}\n\nIs the file used by another invocation of roc?", + exe_path.display(), + host_dest.display(), + e, + ); + } + } else { + // Cargo hosts depend on a c wrapper for the api. Compile host.c as well. + + let clang_cmd = build_c_host_native( + target, + &env_path, + &env_home, + &env_cpath, + c_host_dest.to_str().unwrap(), + &[c_host_src.to_str().unwrap()], + opt_level, + shared_lib_path, + builtins_host_tempfile.path(), + ); + + run_build_command(clang_cmd, "host.c", 0); + + let mut ld_cmd = Command::new("ld"); + + ld_cmd.env_clear().env("PATH", &env_path).args([ + "-r", + "-L", + cargo_out_dir.to_str().unwrap(), + c_host_dest.to_str().unwrap(), + "-lhost", + "-o", + host_dest.to_str().unwrap(), + ]); + + run_build_command(ld_cmd, "c_host.o", 0); + + // Clean up c_host.o + if c_host_dest.exists() { + std::fs::remove_file(c_host_dest).unwrap(); + } + } + } else if rust_host_src.exists() { + // Compile and link host.rs, if it exists + let mut rustc_cmd = Command::new("rustc"); + rustc_cmd.args([ + rust_host_src.to_str().unwrap(), + "-o", + rust_host_dest.to_str().unwrap(), + ]); + if matches!(opt_level, OptLevel::Optimize) { + rustc_cmd.arg("-O"); + } else if matches!(opt_level, OptLevel::Size) { + rustc_cmd.args(["-C", "opt-level=s"]); + } + + run_build_command(rustc_cmd, "host.rs", 0); + + // Rust hosts depend on a c wrapper for the api. Compile host.c as well. + if shared_lib_path.is_some() { + // If compiling to executable, let c deal with linking as well. + let clang_cmd = build_c_host_native( + target, + &env_path, + &env_home, + &env_cpath, + host_dest.to_str().unwrap(), + &[ + c_host_src.to_str().unwrap(), + rust_host_dest.to_str().unwrap(), + ], + opt_level, + shared_lib_path, + builtins_host_tempfile.path(), + ); + run_build_command(clang_cmd, "host.c", 0); + } else { + let clang_cmd = build_c_host_native( + target, + &env_path, + &env_home, + &env_cpath, + c_host_dest.to_str().unwrap(), + &[c_host_src.to_str().unwrap()], + opt_level, + shared_lib_path, + builtins_host_tempfile.path(), + ); + + run_build_command(clang_cmd, "host.c", 0); + + let mut ld_cmd = Command::new("ld"); + + ld_cmd.env_clear().env("PATH", &env_path).args([ + "-r", + c_host_dest.to_str().unwrap(), + rust_host_dest.to_str().unwrap(), + "-o", + host_dest.to_str().unwrap(), + ]); + + run_build_command(ld_cmd, "rust_host.o", 0); + } + + // Clean up rust_host.o and c_host.o + if c_host_dest.exists() { + std::fs::remove_file(c_host_dest).unwrap(); + } + if rust_host_dest.exists() { + std::fs::remove_file(rust_host_dest).unwrap(); + } + } else if c_host_src.exists() { + // Compile host.c, if it exists + let clang_cmd = build_c_host_native( + target, + &env_path, + &env_home, + &env_cpath, + host_dest.to_str().unwrap(), + &[c_host_src.to_str().unwrap()], + opt_level, + shared_lib_path, + builtins_host_tempfile.path(), + ); + + run_build_command(clang_cmd, "host.c", 0); + } else if swift_host_src.exists() { + // Compile host.swift, if it exists + let swiftc_cmd = build_swift_host_native( + &env_path, + &env_home, + host_dest.to_str().unwrap(), + &[swift_host_src.to_str().unwrap()], + opt_level, + shared_lib_path, + swift_host_header_src + .exists() + .then(|| swift_host_header_src.to_str().unwrap()), + target.architecture, + ); + + run_build_command(swiftc_cmd, "host.swift", 0); + } + + // Extend the lifetime of the tempfile so it doesn't get dropped + // (and thus deleted) before the build process is done using it! + let _ = builtins_host_tempfile; + + host_dest +} + +// there can be multiple release folders, one in target and one in target/x86_64-unknown-linux-musl, +// we want the one that was most recently used +fn find_used_target_sub_folder(opt_level: OptLevel, target_folder: PathBuf) -> PathBuf { + let out_folder_name = if matches!(opt_level, OptLevel::Optimize | OptLevel::Size) { + "release" + } else { + "debug" + }; + + let matching_folders = find_in_folder_or_subfolders(&target_folder, out_folder_name); + let mut matching_folders_iter = matching_folders.iter(); + + let mut out_folder = match matching_folders_iter.next() { + Some(dir_entry) => dir_entry, + None => internal_error!("I could not find a folder named {} in {:?}. This may be because the `cargo build` for the platform went wrong.", out_folder_name, target_folder) + }; + + let mut out_folder_last_change = out_folder.metadata().unwrap().modified().unwrap(); + + for dir_entry in matching_folders_iter { + let last_modified = dir_entry.metadata().unwrap().modified().unwrap(); + + if last_modified > out_folder_last_change { + out_folder_last_change = last_modified; + out_folder = dir_entry; + } + } + + out_folder.path().canonicalize().unwrap() +} + +fn find_in_folder_or_subfolders(path: &PathBuf, folder_to_find: &str) -> Vec { + let mut matching_dirs = vec![]; + + if let Ok(entries) = fs::read_dir(path) { + for entry in entries.flatten() { + if entry.file_type().unwrap().is_dir() { + let dir_name = entry + .file_name() + .into_string() + .unwrap_or_else(|_| "".to_string()); + + if dir_name == folder_to_find { + matching_dirs.push(entry) + } else { + let matched_in_sub_dir = + find_in_folder_or_subfolders(&entry.path(), folder_to_find); + + matching_dirs.extend(matched_in_sub_dir); + } + } + } + } + + matching_dirs +} + +fn get_target_str(target: &Triple) -> &str { + if target.operating_system == OperatingSystem::Windows + && target.environment == target_lexicon::Environment::Gnu + { + "x86_64-windows-gnu" + } else { + "native" + } +} + +fn nix_paths() -> Vec { + let mut paths = vec![]; + + if let Some(nix_libgcc_s_path) = env::var_os("NIX_LIBGCC_S_PATH") { + paths.push(nix_libgcc_s_path.into_string().unwrap()) + } + + if let Some(nix_glibc_path) = nix_glibc_path_opt() { + paths.push(nix_glibc_path.into_string().unwrap()) + } + + paths +} + +fn nix_glibc_path_opt() -> Option { + env::var_os("NIX_GLIBC_PATH") +} + +fn build_path(segments: [&str; N]) -> Option { + let mut guess_path = PathBuf::new(); + for s in segments { + guess_path.push(s); + } + if guess_path.exists() { + Some(guess_path) + } else { + None + } +} + +/// Given a list of library directories and the name of a library, find the 1st match +/// +/// The provided list of library directories should be in the form of a list of +/// directories, where each directory is represented by a series of path segments, like +/// +/// ["/usr", "lib"] +/// +/// Each directory will be checked for a file with the provided filename, and the first +/// match will be returned. +/// +/// If there are no matches, [`None`] will be returned. +fn look_for_library(lib_dirs: &[PathBuf], lib_filename: &str) -> Option { + lib_dirs + .iter() + .map(|path| { + let mut path_cl = path.clone(); + path_cl.push(lib_filename); + path_cl + }) + .find(|path| path.exists()) +} + +fn strs_to_path(strs: &[&str]) -> PathBuf { + strs.iter().collect() +} + +fn link_linux( + target: &Triple, + output_path: PathBuf, + input_paths: &[&str], + link_type: LinkType, +) -> io::Result<(Child, PathBuf)> { + let architecture = format!("{}-linux-gnu", target.architecture); + + // Command::new("cp") + // .args(&[input_paths[0], "/home/folkertdev/roc/wasm/host.o"]) + // .output() + // .unwrap(); + // + // Command::new("cp") + // .args(&[input_paths[1], "/home/folkertdev/roc/wasm/app.o"]) + // .output() + // .unwrap(); + + if let Architecture::X86_32(_) = target.architecture { + return Ok(( + zig() + .args(["build-exe"]) + .args(input_paths) + .args([ + "-target", + "i386-linux-musl", + "-lc", + &format!("-femit-bin={}", output_path.to_str().unwrap()), + ]) + .spawn()?, + output_path, + )); + } + + let nix_paths_vec_string = nix_paths(); + let nix_paths_vec: Vec = nix_paths_vec_string.iter().map(PathBuf::from).collect(); + let usr_lib_arch_path = strs_to_path(&["/usr", "lib", &architecture]); + let lib_arch_path = strs_to_path(&["/lib", &architecture]); + + let mut lib_dirs: Vec = vec![]; + + // start with nix paths, this prevents version incompatibility + if !nix_paths_vec.is_empty() { + lib_dirs.extend(nix_paths_vec) + } + + lib_dirs.extend([ + usr_lib_arch_path, + lib_arch_path, + strs_to_path(&["/usr", "lib64"]), + strs_to_path(&["/usr", "lib"]), + ]); + + // Look for the libraries we'll need + let libgcc_name = "libgcc_s.so.1"; + let libgcc_path = look_for_library(&lib_dirs, libgcc_name); + + let crti_name = "crti.o"; + let crti_path = look_for_library(&lib_dirs, crti_name); + + let crtn_name = "crtn.o"; + let crtn_path = look_for_library(&lib_dirs, crtn_name); + + let scrt1_name = "Scrt1.o"; + let scrt1_path = look_for_library(&lib_dirs, scrt1_name); + + // Unwrap all the paths at once so we can inform the user of all missing libs at once + let (libgcc_path, crti_path, crtn_path, scrt1_path) = + match (libgcc_path, crti_path, crtn_path, scrt1_path) { + (Some(libgcc), Some(crti), Some(crtn), Some(scrt1)) => (libgcc, crti, crtn, scrt1), + (maybe_gcc, maybe_crti, maybe_crtn, maybe_scrt1) => { + if maybe_gcc.is_none() { + eprintln!("Couldn't find libgcc_s.so.1!"); + eprintln!("You may need to install libgcc\n"); + } + if maybe_crti.is_none() | maybe_crtn.is_none() | maybe_scrt1.is_none() { + eprintln!("Couldn't find the libc development files!"); + eprintln!("We need the files crti.o, crtn.o, and Scrt1.o"); + eprintln!(); + eprintln!("On Ubuntu/Debian execute:"); + eprintln!("\tsudo apt install libc-dev\n"); + eprintln!("On ArchLinux/Manjaro execute:"); + eprintln!("\tsudo pacman -S glibc\n"); + eprintln!("On Fedora execute:"); + eprintln!("\tsudo dnf install glibc-devel\n"); + } + + let dirs = lib_dirs + .iter() + .map(|path_buf| { + path_buf + .as_path() + .to_str() + .unwrap_or("FAILED TO CONVERT PATH TO STR") + .to_string() + }) + .collect::>() + .join("\n"); + eprintln!("We looked in the following directories:\n{dirs}"); + process::exit(1); + } + }; + + let ld_linux = match target.architecture { + Architecture::X86_64 => { + // give preference to nix_path if it's defined, this prevents bugs + if let Some(nix_glibc_path) = nix_glibc_path_opt() { + build_path([ + &nix_glibc_path.into_string().unwrap(), + "ld-linux-x86-64.so.2", + ]) + } else { + build_path(["/lib64", "ld-linux-x86-64.so.2"]) + } + } + Architecture::Aarch64(_) => build_path(["/lib", "ld-linux-aarch64.so.1"]), + _ => internal_error!( + "TODO gracefully handle unsupported linux architecture: {:?}", + target.architecture + ), + }; + let ld_linux = ld_linux.unwrap(); + let ld_linux = ld_linux.to_str().unwrap(); + + let mut soname; + let (base_args, output_path) = match link_type { + LinkType::Executable => ( + // Presumably this S stands for Static, since if we include Scrt1.o + // in the linking for dynamic builds, linking fails. + vec![scrt1_path.to_string_lossy().into_owned()], + output_path, + ), + LinkType::Dylib => { + // TODO: do we actually need the version number on this? + // Do we even need the "-soname" argument? + // + // See https://software.intel.com/content/www/us/en/develop/articles/create-a-unix-including-linux-shared-library.html + + soname = output_path.clone(); + soname.set_extension("so.1"); + + let mut output_path = output_path; + + output_path.set_extension("so.1.0"); + + ( + // TODO: find a way to avoid using a vec! here - should theoretically be + // able to do this somehow using &[] but the borrow checker isn't having it. + // Also find a way to have these be string slices instead of Strings. + vec![ + "-shared".to_string(), + "-soname".to_string(), + soname.as_path().to_str().unwrap().to_string(), + ], + output_path, + ) + } + LinkType::None => internal_error!("link_linux should not be called with link type of none"), + }; + + let env_path = env::var("PATH").unwrap_or_else(|_| "".to_string()); + + // NOTE: order of arguments to `ld` matters here! + // The `-l` flags should go after the `.o` arguments + + let mut command = Command::new("ld"); + + command + // Don't allow LD_ env vars to affect this + .env_clear() + .env("PATH", &env_path) + // Keep NIX_ env vars + .envs( + env::vars() + .filter(|(k, _)| k.starts_with("NIX_")) + .collect::>(), + ) + .args([ + "--gc-sections", + "--eh-frame-hdr", + "-A", + arch_str(target), + "-pie", + &*crti_path.to_string_lossy(), + &*crtn_path.to_string_lossy(), + ]) + .args(&base_args) + .args(["-dynamic-linker", ld_linux]) + .args(input_paths) + // ld.lld requires this argument, and does not accept --arch + // .args(&["-L/usr/lib/x86_64-linux-gnu"]) + .args([ + // Libraries - see https://github.com/roc-lang/roc/pull/554#discussion_r496365925 + // for discussion and further references + "-lc", + "-lm", + "-lpthread", + "-ldl", + "-lrt", + "-lutil", + "-lc_nonshared", + libgcc_path.to_str().unwrap(), + // Output + "-o", + output_path.as_path().to_str().unwrap(), // app (or app.so or app.dylib etc.) + ]); + + let output = command.spawn()?; + + Ok((output, output_path)) +} + +fn link_macos( + target: &Triple, + output_path: PathBuf, + input_paths: &[&str], + link_type: LinkType, +) -> io::Result<(Child, PathBuf)> { + let (link_type_args, output_path) = match link_type { + LinkType::Executable => (vec!["-execute"], output_path), + LinkType::Dylib => { + let mut output_path = output_path; + + output_path.set_extension("dylib"); + + (vec!["-dylib", "-undefined", "dynamic_lookup"], output_path) + } + LinkType::None => internal_error!("link_macos should not be called with link type of none"), + }; + + let arch = match target.architecture { + Architecture::Aarch64(_) => "arm64".to_string(), + _ => target.architecture.to_string(), + }; + + let mut ld_command = Command::new("ld"); + + ld_command + // NOTE: order of arguments to `ld` matters here! + // The `-l` flags should go after the `.o` arguments + // Don't allow LD_ env vars to affect this + .env_clear() + .args(&link_type_args) + .args([ + // NOTE: we don't do --gc-sections on macOS because the default + // macOS linker doesn't support it, but it's a performance + // optimization, so if we ever switch to a different linker, + // we'd like to re-enable it on macOS! + // "--gc-sections", + "-arch", + &arch, + // Suppress warnings, because otherwise it prints: + // + // ld: warning: -undefined dynamic_lookup may not work with chained fixups + // + // We can't disable that option without breaking either x64 mac or ARM mac + "-w", + "-macos_version_min", + &get_macos_version(), + ]) + .args(input_paths); + + let sdk_path = "/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/lib"; + if Path::new(sdk_path).exists() { + ld_command.arg(format!("-L{sdk_path}")); + ld_command.arg(format!("-L{sdk_path}/swift")); + }; + + let roc_link_flags = match env::var("ROC_LINK_FLAGS") { + Ok(flags) => { + println!("⚠️ CAUTION: The ROC_LINK_FLAGS environment variable is a temporary workaround, and will no longer do anything once surgical linking lands! If you're concerned about what this means for your use case, please ask about it on Zulip."); + + flags + } + Err(_) => "".to_string(), + }; + for roc_link_flag in roc_link_flags.split_whitespace() { + ld_command.arg(roc_link_flag); + } + + ld_command.args([ + // Libraries - see https://github.com/roc-lang/roc/pull/554#discussion_r496392274 + // for discussion and further references + "-lSystem", + "-lresolv", + "-lpthread", + // This `-F PATH` flag is needed for `-framework` flags to work + "-F", + "/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/System/Library/Frameworks/", + // These frameworks are needed for GUI examples to work + "-framework", + "AudioUnit", + "-framework", + "Cocoa", + "-framework", + "CoreAudio", + "-framework", + "CoreVideo", + "-framework", + "IOKit", + "-framework", + "Metal", + "-framework", + "QuartzCore", + // "-lrt", // TODO shouldn't we need this? + // "-lc_nonshared", // TODO shouldn't we need this? + // "-lgcc", // TODO will eventually need compiler_rt from gcc or something - see https://github.com/roc-lang/roc/pull/554#discussion_r496370840 + "-framework", + "Security", + // Output + "-o", + output_path.to_str().unwrap(), // app + ]); + + let mut ld_child = ld_command.spawn()?; + + match target.architecture { + Architecture::Aarch64(_) => { + ld_child.wait()?; + let codesign_child = Command::new("codesign") + .args(["-s", "-", output_path.to_str().unwrap()]) + .spawn()?; + + Ok((codesign_child, output_path)) + } + _ => Ok((ld_child, output_path)), + } +} + +fn get_macos_version() -> String { + let cmd_stdout = Command::new("sw_vers") + .arg("-productVersion") + .output() + .expect("Failed to execute command 'sw_vers -productVersion'") + .stdout; + + let full_version_string = String::from_utf8(cmd_stdout) + .expect("Failed to convert output of command 'sw_vers -productVersion' into a utf8 string"); + + full_version_string + .trim_end() + .split('.') + .take(3) + .collect::>() + .join(".") +} + +fn link_wasm32( + _target: &Triple, + output_path: PathBuf, + input_paths: &[&str], + _link_type: LinkType, +) -> io::Result<(Child, PathBuf)> { + let child = zig() + // .env_clear() + // .env("PATH", &env_path) + .args(["build-exe"]) + .args(input_paths) + .args([ + // include wasi libc + // TOOD: This now compiles fine with `-lc`. That said, the output file doesn't work. + // using `-lc` is broken in zig 8 (and early 9) in combination with ReleaseSmall + find_wasi_libc_path().to_str().unwrap(), + &format!("-femit-bin={}", output_path.to_str().unwrap()), + "-target", + "wasm32-wasi-musl", + "--mod", + &format!("glue::{}", find_zig_glue_path().to_str().unwrap()), + "--deps", + "glue", + "-fstrip", + "-O", + "ReleaseSmall", + // useful for debugging + // "-femit-llvm-ir=/home/folkertdev/roc/roc/crates/cli_testing_examples/benchmarks/platform/host.ll", + ]) + .spawn()?; + + Ok((child, output_path)) +} + +fn link_windows( + target: &Triple, + output_path: PathBuf, + input_paths: &[&str], + link_type: LinkType, +) -> io::Result<(Child, PathBuf)> { + match link_type { + LinkType::Dylib => { + let child = zig() + .args(["build-lib"]) + .args(input_paths) + .args([ + "-lc", + &format!("-femit-bin={}", output_path.to_str().unwrap()), + "-target", + "native", + "--mod", + &format!("glue::{}", find_zig_glue_path().to_str().unwrap()), + "--deps", + "glue", + "-O", + "Debug", + "-dynamic", + ]) + .spawn()?; + + Ok((child, output_path)) + } + LinkType::Executable => { + let child = zig() + .args(["build-exe"]) + .args(input_paths) + .args([ + "-target", + get_target_str(target), + "--subsystem", + "console", + "-lc", + &format!("-femit-bin={}", output_path.to_str().unwrap()), + ]) + .spawn()?; + + Ok((child, output_path)) + } + LinkType::None => todo!(), + } +} + +pub fn llvm_module_to_dylib( + module: &inkwell::module::Module, + target: &Triple, + opt_level: OptLevel, +) -> Result { + use crate::target::{self, convert_opt_level}; + use inkwell::targets::{FileType, RelocMode}; + + let dir = tempfile::tempdir().unwrap(); + let filename = PathBuf::from("Test.roc"); + let file_path = dir.path().join(filename); + let mut app_o_file = file_path; + + app_o_file.set_file_name("app.o"); + + // Emit the .o file using position-independent code (PIC) - needed for dylibs + let reloc = RelocMode::PIC; + let target_machine = + target::target_machine(target, convert_opt_level(opt_level), reloc).unwrap(); + + target_machine + .write_to_file(module, FileType::Object, &app_o_file) + .expect("Writing .o file failed"); + + // Link app.o into a dylib - e.g. app.so or app.dylib + let (mut child, dylib_path) = link( + &Triple::host(), + app_o_file.clone(), + &[app_o_file.to_str().unwrap()], + LinkType::Dylib, + ) + .unwrap(); + + let exit_status = child.wait().unwrap(); + + assert!( + exit_status.success(), + "\n___________\nLinking command failed with status {exit_status:?}:\n\n {child:?}\n___________\n" + ); + + // Load the dylib + let path = dylib_path.as_path().to_str().unwrap(); + + if matches!(target.architecture, Architecture::Aarch64(_)) { + // On AArch64 darwin machines, calling `ldopen` on Roc-generated libs from multiple threads + // sometimes fails with + // cannot dlopen until fork() handlers have completed + // This may be due to codesigning. In any case, spinning until we are able to dlopen seems + // to be okay. + loop { + match unsafe { Library::new(path) } { + Ok(lib) => return Ok(lib), + Err(Error::DlOpen { .. }) => continue, + Err(other) => return Err(other), + } + } + } + + unsafe { Library::new(path) } +} + +pub fn preprocess_host_wasm32(host_input_path: &Path, preprocessed_host_path: &Path) { + let host_input = host_input_path.to_str().unwrap(); + let output_file = preprocessed_host_path.to_str().unwrap(); + + /* + Notes: + zig build-obj just gives you back the first input file, doesn't combine them! + zig build-lib works but doesn't emit relocations, even with --emit-relocs (bug?) + (gen_wasm needs relocs for host-to-app calls and stack size adjustment) + zig wasm-ld is a wrapper around wasm-ld and gives us maximum flexiblity + (but seems to be an unofficial API) + */ + + let builtins_host_tempfile = roc_bitcode::host_wasm_tempfile() + .expect("failed to write host builtins object to tempfile"); + + let mut zig_cmd = zig(); + let args = &[ + "wasm-ld", + builtins_host_tempfile.path().to_str().unwrap(), + host_input, + WASI_LIBC_PATH, + WASI_COMPILER_RT_PATH, // builtins need __multi3, __udivti3, __fixdfti + "-o", + output_file, + "--export-all", + "--no-entry", + "--import-undefined", + "--relocatable", + ]; + + zig_cmd.args(args); + + // println!("\npreprocess_host_wasm32"); + // println!("zig {}\n", args.join(" ")); + + run_build_command(zig_cmd, output_file, 0); + + // Extend the lifetime of the tempfile so it doesn't get dropped + // (and thus deleted) before the Zig process is done using it! + let _ = builtins_host_tempfile; +} + +fn run_build_command(mut command: Command, file_to_build: &str, flaky_fail_counter: usize) { + let mut command_string = std::ffi::OsString::new(); + command_string.push(command.get_program()); + + for arg in command.get_args() { + command_string.push(" "); + command_string.push(arg); + } + + let cmd_str = command_string.to_str().unwrap(); + let cmd_output = command.output().unwrap(); + let max_flaky_fail_count = 10; + + if !cmd_output.status.success() { + match std::str::from_utf8(&cmd_output.stderr) { + Ok(stderr) => { + // flaky error seen on macos 12 apple silicon, related to https://github.com/ziglang/zig/issues/9711 + if stderr.contains("unable to save cached ZIR code") { + if flaky_fail_counter < max_flaky_fail_count { + run_build_command(command, file_to_build, flaky_fail_counter + 1) + } else { + internal_error!( + "Error:\n Failed to rebuild {} {} times, this is not a flaky failure:\n The executed command was:\n {}\n stderr of that command:\n {}", + file_to_build, + max_flaky_fail_count, + cmd_str, + stderr + ) + } + } else { + internal_error!( + "Error:\n Failed to rebuild {}:\n The executed command was:\n {}\n stderr of that command:\n {}", + file_to_build, + cmd_str, + stderr + ) + } + }, + Err(utf8_err) => internal_error!( + "Error:\n Failed to rebuild {}:\n The executed command was:\n {}\n stderr of that command could not be parsed as valid utf8:\n {}", + file_to_build, + cmd_str, + utf8_err + ), + } + } +} diff --git a/crates/compiler/build/src/program.rs b/crates/compiler/build/src/program.rs new file mode 100644 index 0000000000..664de63332 --- /dev/null +++ b/crates/compiler/build/src/program.rs @@ -0,0 +1,1373 @@ +use crate::link::{ + legacy_host_file, link, preprocess_host_wasm32, rebuild_host, LinkType, LinkingStrategy, +}; +use bumpalo::Bump; +use inkwell::memory_buffer::MemoryBuffer; +use roc_error_macros::internal_error; +use roc_gen_dev::AssemblyBackendMode; +use roc_gen_llvm::llvm::build::{module_from_builtins, LlvmBackendMode}; +use roc_gen_llvm::llvm::externs::add_default_roc_externs; +use roc_load::{ + EntryPoint, ExecutionMode, ExpectMetadata, FunctionKind, LoadConfig, LoadMonomorphizedError, + LoadedModule, LoadingProblem, MonomorphizedModule, Threading, +}; +use roc_mono::ir::{OptLevel, SingleEntryPoint}; +use roc_packaging::cache::RocCacheDir; +use roc_reporting::{ + cli::{report_problems, Problems}, + report::{RenderTarget, DEFAULT_PALETTE}, +}; +use roc_target::{OperatingSystem, TargetInfo}; +use std::ffi::OsStr; +use std::ops::Deref; +use std::{ + path::{Path, PathBuf}, + thread::JoinHandle, + time::{Duration, Instant}, +}; +use target_lexicon::Triple; + +#[cfg(feature = "target-wasm32")] +use roc_collections::all::MutSet; + +pub const DEFAULT_ROC_FILENAME: &str = "main.roc"; + +#[derive(Debug, Clone, Copy, Default)] +pub struct CodeGenTiming { + pub generate_final_ir: Duration, + pub code_gen_object: Duration, + pub total: Duration, +} + +pub fn report_problems_monomorphized(loaded: &mut MonomorphizedModule) -> Problems { + report_problems( + &loaded.sources, + &loaded.interns, + &mut loaded.can_problems, + &mut loaded.type_problems, + ) +} + +pub fn report_problems_typechecked(loaded: &mut LoadedModule) -> Problems { + report_problems( + &loaded.sources, + &loaded.interns, + &mut loaded.can_problems, + &mut loaded.type_problems, + ) +} + +pub enum CodeObject { + MemoryBuffer(MemoryBuffer), + Vector(Vec), +} + +impl Deref for CodeObject { + type Target = [u8]; + + fn deref(&self) -> &Self::Target { + match self { + CodeObject::MemoryBuffer(memory_buffer) => memory_buffer.as_slice(), + CodeObject::Vector(vector) => vector.as_slice(), + } + } +} + +#[derive(Debug, Clone, Copy)] +pub enum CodeGenBackend { + Assembly(AssemblyBackendMode), + Llvm(LlvmBackendMode), + Wasm, +} + +#[derive(Debug, Clone, Copy)] +pub struct CodeGenOptions { + pub backend: CodeGenBackend, + pub opt_level: OptLevel, + pub emit_debug_info: bool, +} + +type GenFromMono<'a> = (CodeObject, CodeGenTiming, ExpectMetadata<'a>); + +#[allow(clippy::too_many_arguments)] +pub fn gen_from_mono_module<'a>( + arena: &'a bumpalo::Bump, + loaded: MonomorphizedModule<'a>, + roc_file_path: &Path, + target: &target_lexicon::Triple, + code_gen_options: CodeGenOptions, + preprocessed_host_path: &Path, + wasm_dev_stack_bytes: Option, +) -> GenFromMono<'a> { + let path = roc_file_path; + let debug = code_gen_options.emit_debug_info; + let opt = code_gen_options.opt_level; + + match code_gen_options.backend { + CodeGenBackend::Wasm => gen_from_mono_module_dev( + arena, + loaded, + target, + preprocessed_host_path, + wasm_dev_stack_bytes, + AssemblyBackendMode::Binary, // dummy value, unused in practice + ), + CodeGenBackend::Assembly(backend_mode) => gen_from_mono_module_dev( + arena, + loaded, + target, + preprocessed_host_path, + wasm_dev_stack_bytes, + backend_mode, + ), + CodeGenBackend::Llvm(backend_mode) => { + gen_from_mono_module_llvm(arena, loaded, path, target, opt, backend_mode, debug) + } + } +} + +// TODO how should imported modules factor into this? What if those use builtins too? +// TODO this should probably use more helper functions +// TODO make this polymorphic in the llvm functions so it can be reused for another backend. +fn gen_from_mono_module_llvm<'a>( + arena: &'a bumpalo::Bump, + loaded: MonomorphizedModule<'a>, + roc_file_path: &Path, + target: &target_lexicon::Triple, + opt_level: OptLevel, + backend_mode: LlvmBackendMode, + emit_debug_info: bool, +) -> GenFromMono<'a> { + use crate::target::{self, convert_opt_level}; + use inkwell::attributes::{Attribute, AttributeLoc}; + use inkwell::context::Context; + use inkwell::module::Linkage; + use inkwell::targets::{FileType, RelocMode}; + + let all_code_gen_start = Instant::now(); + + // Generate the binary + let target_info = roc_target::TargetInfo::from(target); + let context = Context::create(); + let module = arena.alloc(module_from_builtins(target, &context, "app")); + + // strip Zig debug stuff + // module.strip_debug_info(); + + // mark our zig-defined builtins as internal + let app_ll_file = { + let mut temp = PathBuf::from(roc_file_path); + temp.set_extension("ll"); + + temp + }; + + let kind_id = Attribute::get_named_enum_kind_id("alwaysinline"); + debug_assert!(kind_id > 0); + let enum_attr = context.create_enum_attribute(kind_id, 0); + + for function in module.get_functions() { + let name = function.get_name().to_str().unwrap(); + + // mark our zig-defined builtins as internal + if name.starts_with("roc_builtins") { + function.set_linkage(Linkage::Internal); + } + + if name.starts_with("roc_builtins.dict") + || name.starts_with("roc_builtins.list") + || name.starts_with("roc_builtins.dec") + || name.starts_with("list.RocList") + || name.starts_with("dict.RocDict") + || name.contains("incref") + || name.contains("decref") + { + function.add_attribute(AttributeLoc::Function, enum_attr); + } + } + + let builder = context.create_builder(); + let (dibuilder, compile_unit) = roc_gen_llvm::llvm::build::Env::new_debug_info(module); + let (mpm, _fpm) = roc_gen_llvm::llvm::build::construct_optimization_passes(module, opt_level); + + // Compile and add all the Procs before adding main + let env = roc_gen_llvm::llvm::build::Env { + arena, + builder: &builder, + dibuilder: &dibuilder, + compile_unit: &compile_unit, + context: &context, + interns: loaded.interns, + module, + target_info, + mode: backend_mode, + + exposed_to_host: loaded + .exposed_to_host + .top_level_values + .keys() + .copied() + .collect(), + }; + + // does not add any externs for this mode (we have a host) but cleans up some functions around + // expects that would confuse the surgical linker + add_default_roc_externs(&env); + + let entry_point = match loaded.entry_point { + EntryPoint::Executable { + exposed_to_host, + platform_path: _, + } => { + // TODO support multiple of these! + debug_assert_eq!(exposed_to_host.len(), 1); + let (symbol, layout) = exposed_to_host[0]; + + roc_mono::ir::EntryPoint::Single(SingleEntryPoint { symbol, layout }) + } + EntryPoint::Test => roc_mono::ir::EntryPoint::Expects { symbols: &[] }, + }; + + roc_gen_llvm::llvm::build::build_procedures( + &env, + &loaded.layout_interner, + opt_level, + loaded.procedures, + loaded.host_exposed_lambda_sets, + entry_point, + Some(&app_ll_file), + &loaded.glue_layouts, + ); + + // We are now finished building the LLVM IR. + let generate_final_ir = all_code_gen_start.elapsed(); + let code_gen_object_start = Instant::now(); + + env.dibuilder.finalize(); + + // we don't use the debug info, and it causes weird errors. + module.strip_debug_info(); + + // Uncomment this to see the module's optimized LLVM instruction output: + // env.module.print_to_stderr(); + + mpm.run_on(module); + + // Verify the module + if let Err(errors) = env.module.verify() { + // write the ll code to a file, so we can modify it + env.module.print_to_file(&app_ll_file).unwrap(); + + internal_error!( + "😱 LLVM errors when defining module; I wrote the full LLVM IR to {:?}\n\n {}", + app_ll_file, + errors.to_string(), + ); + } + + // Uncomment this to see the module's optimized LLVM instruction output: + // env.module.print_to_stderr(); + + // annotate the LLVM IR output with debug info + // so errors are reported with the line number of the LLVM source + let memory_buffer = if cfg!(feature = "sanitizers") && std::env::var("ROC_SANITIZERS").is_ok() { + let dir = tempfile::tempdir().unwrap(); + let dir = dir.into_path(); + + let app_ll_file = dir.join("app.ll"); + let app_bc_file = dir.join("app.bc"); + let app_o_file = dir.join("app.o"); + + // write the ll code to a file, so we can modify it + module.print_to_file(&app_ll_file).unwrap(); + + // Apply coverage passes. + // Note, this is specifically tailored for `cargo afl` and afl++. + // It most likely will not work with other fuzzer setups without modification. + let mut passes = vec![]; + let mut extra_args = vec![]; + let mut unrecognized = vec![]; + for sanitizer in std::env::var("ROC_SANITIZERS") + .unwrap() + .split(',') + .map(|x| x.trim()) + { + match sanitizer { + "address" => passes.push("asan-module"), + "memory" => passes.push("msan-module"), + "thread" => passes.push("tsan-module"), + "cargo-fuzz" => { + passes.push("sancov-module"); + extra_args.extend_from_slice(&[ + "-sanitizer-coverage-level=3", + "-sanitizer-coverage-prune-blocks=0", + "-sanitizer-coverage-inline-8bit-counters", + "-sanitizer-coverage-pc-table", + ]); + } + "afl.rs" => { + passes.push("sancov-module"); + extra_args.extend_from_slice(&[ + "-sanitizer-coverage-level=3", + "-sanitizer-coverage-prune-blocks=0", + "-sanitizer-coverage-trace-pc-guard", + ]); + } + x => unrecognized.push(x.to_owned()), + } + } + if !unrecognized.is_empty() { + let out = unrecognized + .iter() + .map(|x| format!("{x:?}")) + .collect::>() + .join(", "); + eprintln!("Unrecognized sanitizer: {out}\nSupported options are \"address\", \"memory\", \"thread\", \"cargo-fuzz\", and \"afl.rs\"."); + eprintln!("Note: \"cargo-fuzz\" and \"afl.rs\" both enable sanitizer coverage for fuzzing. They just use different parameters to match the respective libraries.") + } + + use std::process::Command; + let mut opt = Command::new("opt"); + opt.args([ + app_ll_file.to_str().unwrap(), + "-o", + app_bc_file.to_str().unwrap(), + ]) + .args(extra_args); + if !passes.is_empty() { + opt.arg(format!("-passes={}", passes.join(","))); + } + let opt = opt.output().unwrap(); + + assert!(opt.stderr.is_empty(), "{opt:#?}"); + + // write the .o file. Note that this builds the .o for the local machine, + // and ignores the `target_machine` entirely. + // + // different systems name this executable differently, so we shotgun for + // the most common ones and then give up. + let bc_to_object = Command::new("llc") + .args([ + "-relocation-model=pic", + "-filetype=obj", + app_bc_file.to_str().unwrap(), + "-o", + app_o_file.to_str().unwrap(), + ]) + .output() + .unwrap(); + + assert!(bc_to_object.status.success(), "{bc_to_object:#?}"); + + MemoryBuffer::create_from_file(&app_o_file).expect("memory buffer creation works") + } else if emit_debug_info { + module.strip_debug_info(); + + let mut app_ll_dbg_file = PathBuf::from(roc_file_path); + app_ll_dbg_file.set_extension("dbg.ll"); + + let mut app_o_file = PathBuf::from(roc_file_path); + app_o_file.set_extension("o"); + + use std::process::Command; + + // write the ll code to a file, so we can modify it + module.print_to_file(&app_ll_file).unwrap(); + + // run the debugir https://github.com/vaivaswatha/debugir tool + match Command::new("debugir") + .args(["-instnamer", app_ll_file.to_str().unwrap()]) + .output() + { + Ok(_) => {} + Err(error) => { + use std::io::ErrorKind; + match error.kind() { + ErrorKind::NotFound => internal_error!( + r"I could not find the `debugir` tool on the PATH, install it from https://github.com/vaivaswatha/debugir" + ), + _ => internal_error!("{:?}", error), + } + } + } + + use target_lexicon::Architecture; + match target.architecture { + Architecture::X86_64 + | Architecture::X86_32(_) + | Architecture::Aarch64(_) + | Architecture::Wasm32 => { + // write the .o file. Note that this builds the .o for the local machine, + // and ignores the `target_machine` entirely. + // + // different systems name this executable differently, so we shotgun for + // the most common ones and then give up. + let ll_to_object = Command::new("llc") + .args([ + "-relocation-model=pic", + "-filetype=obj", + app_ll_dbg_file.to_str().unwrap(), + "-o", + app_o_file.to_str().unwrap(), + ]) + .output() + .unwrap(); + + assert!(ll_to_object.stderr.is_empty(), "{ll_to_object:#?}"); + } + _ => unreachable!(), + } + + MemoryBuffer::create_from_file(&app_o_file).expect("memory buffer creation works") + } else { + // Emit the .o file + use target_lexicon::Architecture; + match target.architecture { + Architecture::X86_64 | Architecture::X86_32(_) | Architecture::Aarch64(_) => { + let reloc = RelocMode::PIC; + let target_machine = + target::target_machine(target, convert_opt_level(opt_level), reloc).unwrap(); + + target_machine + .write_to_memory_buffer(env.module, FileType::Object) + .expect("Writing .o file failed") + } + Architecture::Wasm32 => { + // Useful for debugging + // module.print_to_file(app_ll_file); + module.write_bitcode_to_memory() + } + _ => internal_error!( + "TODO gracefully handle unsupported architecture: {:?}", + target.architecture + ), + } + }; + + let code_gen_object = code_gen_object_start.elapsed(); + let total = all_code_gen_start.elapsed(); + + ( + CodeObject::MemoryBuffer(memory_buffer), + CodeGenTiming { + generate_final_ir, + code_gen_object, + total, + }, + ExpectMetadata { + interns: env.interns, + layout_interner: loaded.layout_interner, + expectations: loaded.expectations, + }, + ) +} + +#[cfg(feature = "target-wasm32")] +fn gen_from_mono_module_dev<'a>( + arena: &'a bumpalo::Bump, + loaded: MonomorphizedModule<'a>, + target: &target_lexicon::Triple, + preprocessed_host_path: &Path, + wasm_dev_stack_bytes: Option, + backend_mode: AssemblyBackendMode, +) -> GenFromMono<'a> { + use target_lexicon::Architecture; + + match target.architecture { + Architecture::Wasm32 => gen_from_mono_module_dev_wasm32( + arena, + loaded, + preprocessed_host_path, + wasm_dev_stack_bytes, + ), + Architecture::X86_64 | Architecture::Aarch64(_) => { + gen_from_mono_module_dev_assembly(arena, loaded, target, backend_mode) + } + _ => todo!(), + } +} + +#[cfg(not(feature = "target-wasm32"))] +pub fn gen_from_mono_module_dev<'a>( + arena: &'a bumpalo::Bump, + loaded: MonomorphizedModule<'a>, + target: &target_lexicon::Triple, + _host_input_path: &Path, + _wasm_dev_stack_bytes: Option, + backend_mode: AssemblyBackendMode, +) -> GenFromMono<'a> { + use target_lexicon::Architecture; + + match target.architecture { + Architecture::X86_64 | Architecture::Aarch64(_) => { + gen_from_mono_module_dev_assembly(arena, loaded, target, backend_mode) + } + _ => todo!(), + } +} + +#[cfg(feature = "target-wasm32")] +fn gen_from_mono_module_dev_wasm32<'a>( + arena: &'a bumpalo::Bump, + loaded: MonomorphizedModule<'a>, + preprocessed_host_path: &Path, + wasm_dev_stack_bytes: Option, +) -> GenFromMono<'a> { + let all_code_gen_start = Instant::now(); + let MonomorphizedModule { + module_id, + procedures, + mut interns, + mut layout_interner, + .. + } = loaded; + + let exposed_to_host = loaded + .exposed_to_host + .top_level_values + .keys() + .copied() + .collect::>(); + + let env = roc_gen_wasm::Env { + arena, + module_id, + exposed_to_host, + stack_bytes: wasm_dev_stack_bytes.unwrap_or(roc_gen_wasm::Env::DEFAULT_STACK_BYTES), + }; + + let host_bytes = std::fs::read(preprocessed_host_path).unwrap_or_else(|_| { + internal_error!( + "Failed to read host object file {}! Try omitting --prebuilt-platform", + preprocessed_host_path.display() + ) + }); + + let host_module = roc_gen_wasm::parse_host(arena, &host_bytes).unwrap_or_else(|e| { + internal_error!( + "I ran into a problem with the host object file, {} at offset 0x{:x}:\n{}", + preprocessed_host_path.display(), + e.offset, + e.message + ) + }); + + let final_binary_bytes = roc_gen_wasm::build_app_binary( + &env, + &mut layout_interner, + &mut interns, + host_module, + procedures, + ); + + let generate_final_ir = all_code_gen_start.elapsed(); + let code_gen_object_start = Instant::now(); + let code_gen_object = code_gen_object_start.elapsed(); + let total = all_code_gen_start.elapsed(); + + ( + CodeObject::Vector(final_binary_bytes), + CodeGenTiming { + generate_final_ir, + code_gen_object, + total, + }, + ExpectMetadata { + interns, + layout_interner, + expectations: loaded.expectations, + }, + ) +} + +fn gen_from_mono_module_dev_assembly<'a>( + arena: &'a bumpalo::Bump, + loaded: MonomorphizedModule<'a>, + target: &target_lexicon::Triple, + backend_mode: AssemblyBackendMode, +) -> GenFromMono<'a> { + let all_code_gen_start = Instant::now(); + + let lazy_literals = true; + + let MonomorphizedModule { + module_id, + procedures, + mut interns, + exposed_to_host, + mut layout_interner, + .. + } = loaded; + + let env = roc_gen_dev::Env { + arena, + module_id, + exposed_to_host: exposed_to_host.top_level_values.keys().copied().collect(), + lazy_literals, + mode: backend_mode, + }; + + let module_object = + roc_gen_dev::build_module(&env, &mut interns, &mut layout_interner, target, procedures); + + let generate_final_ir = all_code_gen_start.elapsed(); + let code_gen_object_start = Instant::now(); + + let module_out = module_object + .write() + .expect("failed to build output object"); + + let code_gen_object = code_gen_object_start.elapsed(); + let total = all_code_gen_start.elapsed(); + + ( + CodeObject::Vector(module_out), + CodeGenTiming { + generate_final_ir, + code_gen_object, + total, + }, + ExpectMetadata { + interns, + layout_interner, + expectations: loaded.expectations, + }, + ) +} + +fn report_timing(buf: &mut String, label: &str, duration: Duration) { + use std::fmt::Write; + + writeln!( + buf, + " {:9.3} ms {}", + duration.as_secs_f64() * 1000.0, + label, + ) + .unwrap() +} + +pub struct BuiltFile<'a> { + pub binary_path: PathBuf, + pub problems: Problems, + pub total_time: Duration, + pub expect_metadata: ExpectMetadata<'a>, +} + +pub enum BuildOrdering { + /// Run up through typechecking first; continue building iff that is successful. + BuildIfChecks, + /// Always build the Roc binary, even if there are type errors. + AlwaysBuild, +} + +#[derive(Debug)] +#[allow(clippy::large_enum_variant)] +pub enum BuildFileError<'a> { + LoadingProblem(LoadingProblem<'a>), + ErrorModule { + module: LoadedModule, + total_time: Duration, + }, +} + +impl<'a> BuildFileError<'a> { + fn from_mono_error(error: LoadMonomorphizedError<'a>, compilation_start: Instant) -> Self { + match error { + LoadMonomorphizedError::LoadingProblem(problem) => { + BuildFileError::LoadingProblem(problem) + } + LoadMonomorphizedError::ErrorModule(module) => BuildFileError::ErrorModule { + module, + total_time: compilation_start.elapsed(), + }, + } + } +} + +pub fn handle_error_module( + mut module: roc_load::LoadedModule, + total_time: std::time::Duration, + filename: &OsStr, + print_run_anyway_hint: bool, +) -> std::io::Result { + debug_assert!(module.total_problems() > 0); + + let problems = report_problems_typechecked(&mut module); + + problems.print_to_stdout(total_time); + + if print_run_anyway_hint { + // If you're running "main.roc" then you can just do `roc run` + // to re-run the program. + print!(".\n\nYou can run the program anyway with \x1B[32mroc run"); + + if filename != DEFAULT_ROC_FILENAME { + print!(" {}", &filename.to_string_lossy()); + } + + println!("\x1B[39m"); + } + + Ok(problems.exit_code()) +} + +pub fn handle_loading_problem(problem: LoadingProblem) -> std::io::Result { + match problem { + LoadingProblem::FormattedReport(report) => { + print!("{report}"); + Ok(1) + } + _ => { + // TODO: tighten up the types here, we should always end up with a + // formatted report from load. + print!("Failed with error: {problem:?}"); + Ok(1) + } + } +} + +pub fn standard_load_config( + target: &Triple, + order: BuildOrdering, + threading: Threading, +) -> LoadConfig { + let target_info = TargetInfo::from(target); + + let exec_mode = match order { + BuildOrdering::BuildIfChecks => ExecutionMode::ExecutableIfCheck, + BuildOrdering::AlwaysBuild => ExecutionMode::Executable, + }; + + // UNSTABLE(lambda-erasure) + let function_kind = if cfg!(debug_assertions) { + if std::env::var("EXPERIMENTAL_ROC_ERASE").is_ok() { + FunctionKind::Erased + } else { + FunctionKind::LambdaSet + } + } else { + FunctionKind::LambdaSet + }; + + LoadConfig { + target_info, + function_kind, + render: RenderTarget::ColorTerminal, + palette: DEFAULT_PALETTE, + threading, + exec_mode, + } +} + +#[allow(clippy::too_many_arguments)] +pub fn build_file<'a>( + arena: &'a Bump, + target: &Triple, + app_module_path: PathBuf, + code_gen_options: CodeGenOptions, + emit_timings: bool, + link_type: LinkType, + linking_strategy: LinkingStrategy, + prebuilt_requested: bool, + wasm_dev_stack_bytes: Option, + roc_cache_dir: RocCacheDir<'_>, + load_config: LoadConfig, + out_path: Option<&Path>, +) -> Result, BuildFileError<'a>> { + let compilation_start = Instant::now(); + + // Step 1: compile the app and generate the .o file + let loaded = + roc_load::load_and_monomorphize(arena, app_module_path.clone(), roc_cache_dir, load_config) + .map_err(|e| BuildFileError::from_mono_error(e, compilation_start))?; + + build_loaded_file( + arena, + target, + app_module_path, + code_gen_options, + emit_timings, + link_type, + linking_strategy, + prebuilt_requested, + wasm_dev_stack_bytes, + loaded, + compilation_start, + out_path, + ) +} + +#[allow(clippy::too_many_arguments)] +fn build_loaded_file<'a>( + arena: &'a Bump, + target: &Triple, + app_module_path: PathBuf, + code_gen_options: CodeGenOptions, + emit_timings: bool, + link_type: LinkType, + mut linking_strategy: LinkingStrategy, + prebuilt_requested: bool, + wasm_dev_stack_bytes: Option, + loaded: roc_load::MonomorphizedModule<'a>, + compilation_start: Instant, + out_path: Option<&Path>, +) -> Result, BuildFileError<'a>> { + let operating_system = roc_target::OperatingSystem::from(target.operating_system); + + let platform_main_roc = match &loaded.entry_point { + EntryPoint::Executable { platform_path, .. } => platform_path.to_path_buf(), + _ => unreachable!(), + }; + + // For example, if we're loading the platform from a URL, it's automatically prebuilt + // even if the --prebuilt-platform CLI flag wasn't set. + let is_platform_prebuilt = prebuilt_requested || loaded.uses_prebuilt_platform; + + if is_platform_prebuilt && linking_strategy == LinkingStrategy::Surgical { + // Fallback to legacy linking if the preprocessed host file does not exist, but a legacy host does exist. + let preprocessed_host_path = platform_main_roc + .with_file_name(roc_linker::preprocessed_host_filename(target).unwrap()); + let legacy_host_path = legacy_host_file(target, &platform_main_roc).unwrap(); + if !preprocessed_host_path.exists() && legacy_host_path.exists() { + linking_strategy = LinkingStrategy::Legacy; + } + } + + // the preprocessed host is stored beside the platform's main.roc + let preprocessed_host_path = if linking_strategy == LinkingStrategy::Legacy { + if let roc_target::OperatingSystem::Wasi = operating_system { + // when compiling a wasm application, we implicitly assume here that the host is in zig + // and has a file called "host.zig" + platform_main_roc.with_file_name("host.zig") + } else { + legacy_host_file(target, &platform_main_roc).unwrap() + } + } else { + platform_main_roc.with_file_name(roc_linker::preprocessed_host_filename(target).unwrap()) + }; + + let mut output_exe_path = match out_path { + Some(path) => { + // true iff the path ends with a directory separator, + // e.g. '/' on UNIX, '/' or '\\' on Windows + let ends_with_sep = { + #[cfg(unix)] + { + use std::os::unix::ffi::OsStrExt; + + path.as_os_str().as_bytes().ends_with(&[b'/']) + } + + #[cfg(windows)] + { + use std::os::windows::ffi::OsStrExt; + + let last = path.as_os_str().encode_wide().last(); + + last == Some(0x002f)// UTF-16 slash + || last == Some(0x005c) // UTF-16 backslash + } + }; + + // If you specified a path that ends in in a directory separator, then + // use that directory, but use the app module's filename for the filename. + if ends_with_sep { + let filename = app_module_path.file_name().unwrap_or_default(); + + with_executable_extension(&path.join(filename), operating_system) + } else { + path.to_path_buf() + } + } + None => with_executable_extension(&app_module_path, operating_system), + }; + + // We don't need to spawn a rebuild thread when using a prebuilt host. + let rebuild_thread = if matches!(link_type, LinkType::Dylib | LinkType::None) { + None + } else if is_platform_prebuilt { + if !preprocessed_host_path.exists() { + invalid_prebuilt_platform(prebuilt_requested, preprocessed_host_path); + + std::process::exit(1); + } + + if linking_strategy == LinkingStrategy::Surgical { + // Copy preprocessed host to executable location. + // The surgical linker will modify that copy in-place. + std::fs::copy(&preprocessed_host_path, output_exe_path.as_path()).unwrap(); + } + + None + } else { + // TODO this should probably be moved before load_and_monomorphize. + // To do this we will need to preprocess files just for their exported symbols. + // Also, we should no longer need to do this once we have platforms on + // a package repository, as we can then get prebuilt platforms from there. + + let dll_stub_symbols = roc_linker::ExposedSymbols::from_exposed_to_host( + &loaded.interns, + &loaded.exposed_to_host, + ); + + let join_handle = spawn_rebuild_thread( + code_gen_options.opt_level, + linking_strategy, + platform_main_roc.clone(), + preprocessed_host_path.clone(), + output_exe_path.clone(), + target, + dll_stub_symbols, + ); + + Some(join_handle) + }; + + let buf = &mut String::with_capacity(1024); + + let mut it = loaded.timings.iter().peekable(); + while let Some((module_id, module_timing)) = it.next() { + let module_name = loaded.interns.module_name(*module_id); + + buf.push_str(" "); + + if module_name.is_empty() { + // the App module + buf.push_str("Application Module"); + } else { + buf.push_str(module_name); + } + + buf.push('\n'); + + use std::fmt::Write; + write!(buf, "{module_timing}").unwrap(); + + if it.peek().is_some() { + buf.push('\n'); + } + } + + // This only needs to be mutable for report_problems. This can't be done + // inside a nested scope without causing a borrow error! + let mut loaded = loaded; + let problems = report_problems_monomorphized(&mut loaded); + let loaded = loaded; + + enum HostRebuildTiming { + BeforeApp(u128), + ConcurrentWithApp(JoinHandle), + } + + let opt_rebuild_timing = if let Some(rebuild_thread) = rebuild_thread { + if linking_strategy == LinkingStrategy::Additive { + let rebuild_duration = rebuild_thread + .join() + .expect("Failed to (re)build platform."); + + if emit_timings && !is_platform_prebuilt { + println!("Finished rebuilding the platform in {rebuild_duration} ms\n"); + } + + Some(HostRebuildTiming::BeforeApp(rebuild_duration)) + } else { + Some(HostRebuildTiming::ConcurrentWithApp(rebuild_thread)) + } + } else { + None + }; + + let (roc_app_bytes, code_gen_timing, expect_metadata) = gen_from_mono_module( + arena, + loaded, + &app_module_path, + target, + code_gen_options, + &preprocessed_host_path, + wasm_dev_stack_bytes, + ); + + buf.push('\n'); + buf.push_str(" "); + buf.push_str("Code Generation"); + buf.push('\n'); + + report_timing( + buf, + "Generate final IR from Mono IR", + code_gen_timing.generate_final_ir, + ); + report_timing(buf, "Generate object", code_gen_timing.code_gen_object); + buf.push('\n'); + report_timing(buf, "Total", code_gen_timing.total); + + let compilation_end = compilation_start.elapsed(); + let size = roc_app_bytes.len(); + + if emit_timings { + println!( + "\n\nCompilation finished!\n\nHere's how long each module took to compile:\n\n{buf}" + ); + + println!( + "Finished compilation and code gen in {} ms\n\nProduced a app.o file of size {:?}\n", + compilation_end.as_millis(), + size, + ); + } + + if let Some(HostRebuildTiming::ConcurrentWithApp(thread)) = opt_rebuild_timing { + let rebuild_duration = thread.join().expect("Failed to (re)build platform."); + + if emit_timings && !is_platform_prebuilt { + println!("Finished rebuilding the platform in {rebuild_duration} ms\n"); + } + } + + // Step 2: link the prebuilt platform and compiled app + let link_start = Instant::now(); + + match (linking_strategy, link_type) { + (LinkingStrategy::Surgical, _) => { + roc_linker::link_preprocessed_host( + target, + &platform_main_roc, + &roc_app_bytes, + &output_exe_path, + ); + } + (LinkingStrategy::Additive, _) | (LinkingStrategy::Legacy, LinkType::None) => { + // Just copy the object file to the output folder. + output_exe_path.set_extension(operating_system.object_file_ext()); + std::fs::write(&output_exe_path, &*roc_app_bytes).unwrap(); + } + (LinkingStrategy::Legacy, _) => { + let extension = if matches!(operating_system, roc_target::OperatingSystem::Wasi) { + // Legacy linker is only by used llvm wasm backend, not dev. + // llvm wasm backend directly emits a bitcode file when targeting wasi, not a `.o` or `.wasm` file. + // If we set the extension wrong, zig will print a ton of warnings when linking. + "bc" + } else { + operating_system.object_file_ext() + }; + let app_o_file = tempfile::Builder::new() + .prefix("roc_app") + .suffix(&format!(".{extension}")) + .tempfile() + .map_err(|err| todo!("TODO Gracefully handle tempfile creation error {:?}", err))?; + let app_o_file = app_o_file.path(); + + std::fs::write(app_o_file, &*roc_app_bytes).unwrap(); + + let builtins_host_tempfile = roc_bitcode::host_tempfile() + .expect("failed to write host builtins object to tempfile"); + + let mut inputs = vec![app_o_file.to_str().unwrap()]; + + if !matches!(link_type, LinkType::Dylib | LinkType::None) { + // the host has been compiled into a .o or .obj file + inputs.push(preprocessed_host_path.as_path().to_str().unwrap()); + } + + if matches!(code_gen_options.backend, CodeGenBackend::Assembly(_)) { + inputs.push(builtins_host_tempfile.path().to_str().unwrap()); + } + + let (mut child, _) = link(target, output_exe_path.clone(), &inputs, link_type) + .map_err(|_| todo!("gracefully handle `ld` failing to spawn."))?; + + let exit_status = child + .wait() + .map_err(|_| todo!("gracefully handle error after `ld` spawned"))?; + + // Extend the lifetime of the tempfile so it doesn't get dropped + // (and thus deleted) before the child process is done using it! + let _ = builtins_host_tempfile; + + if !exit_status.success() { + todo!( + "gracefully handle `ld` (or `zig` in the case of wasm with --optimize) returning exit code {:?}", + exit_status.code() + ); + } + } + } + + let linking_time = link_start.elapsed(); + + if emit_timings { + println!("Finished linking in {} ms\n", linking_time.as_millis()); + } + + let total_time = compilation_start.elapsed(); + + Ok(BuiltFile { + binary_path: output_exe_path, + problems, + total_time, + expect_metadata, + }) +} + +fn invalid_prebuilt_platform(prebuilt_requested: bool, preprocessed_host_path: PathBuf) { + let prefix = if prebuilt_requested { + "Because I was run with --prebuilt-platform, " + } else { + "" + }; + + let preprocessed_host_path_str = preprocessed_host_path.to_string_lossy(); + let extra_err_msg = if preprocessed_host_path_str.ends_with(".rh") { + "\n\n\tNote: If the platform does have an .rh1 file but no .rh file, it's because it's been built with an older version of roc. Contact the author to release a new build of the platform using a roc release newer than March 21 2023.\n" + } else { + "" + }; + + eprintln!( + indoc::indoc!( + r#" + {}I was expecting this file to exist: + + {} + + However, it was not there!{} + + If you have the platform's source code locally, you may be able to generate it by re-running this command omitting --prebuilt-platform + "# + ), + prefix, + preprocessed_host_path.to_string_lossy(), + extra_err_msg + ); +} + +#[allow(clippy::too_many_arguments)] +fn spawn_rebuild_thread( + opt_level: OptLevel, + linking_strategy: LinkingStrategy, + platform_main_roc: PathBuf, + preprocessed_host_path: PathBuf, + output_exe_path: PathBuf, + target: &Triple, + dll_stub_symbols: Vec, +) -> std::thread::JoinHandle { + let thread_local_target = target.clone(); + std::thread::spawn(move || { + // Printing to stderr because we want stdout to contain only the output of the roc program. + // We are aware of the trade-offs. + // `cargo run` follows the same approach + eprintln!("🔨 Rebuilding platform..."); + + let rebuild_host_start = Instant::now(); + + match linking_strategy { + LinkingStrategy::Additive => { + let host_dest = rebuild_host( + opt_level, + &thread_local_target, + platform_main_roc.as_path(), + None, + ); + + preprocess_host_wasm32(host_dest.as_path(), &preprocessed_host_path); + } + LinkingStrategy::Surgical => { + build_and_preprocess_host_lowlevel( + opt_level, + &thread_local_target, + platform_main_roc.as_path(), + preprocessed_host_path.as_path(), + &dll_stub_symbols, + ); + + // Copy preprocessed host to executable location. + // The surgical linker will modify that copy in-place. + std::fs::copy(&preprocessed_host_path, output_exe_path.as_path()).unwrap(); + } + LinkingStrategy::Legacy => { + rebuild_host( + opt_level, + &thread_local_target, + platform_main_roc.as_path(), + None, + ); + } + } + + rebuild_host_start.elapsed().as_millis() + }) +} + +pub fn build_and_preprocess_host( + opt_level: OptLevel, + target: &Triple, + platform_main_roc: &Path, + preprocessed_host_path: &Path, + exposed_symbols: roc_linker::ExposedSymbols, +) { + let stub_dll_symbols = exposed_symbols.stub_dll_symbols(); + + build_and_preprocess_host_lowlevel( + opt_level, + target, + platform_main_roc, + preprocessed_host_path, + &stub_dll_symbols, + ) +} + +fn build_and_preprocess_host_lowlevel( + opt_level: OptLevel, + target: &Triple, + platform_main_roc: &Path, + preprocessed_host_path: &Path, + stub_dll_symbols: &[String], +) { + let stub_lib = + roc_linker::generate_stub_lib_from_loaded(target, platform_main_roc, stub_dll_symbols); + + debug_assert!(stub_lib.exists()); + + rebuild_host(opt_level, target, platform_main_roc, Some(&stub_lib)); + + roc_linker::preprocess_host( + target, + platform_main_roc, + preprocessed_host_path, + &stub_lib, + stub_dll_symbols, + ) +} + +#[allow(clippy::too_many_arguments)] +pub fn check_file<'a>( + arena: &'a Bump, + roc_file_path: PathBuf, + emit_timings: bool, + roc_cache_dir: RocCacheDir<'_>, + threading: Threading, +) -> Result<(Problems, Duration), LoadingProblem<'a>> { + let compilation_start = Instant::now(); + + // only used for generating errors. We don't do code generation, so hardcoding should be fine + // we need monomorphization for when exhaustiveness checking + let target_info = TargetInfo::default_x86_64(); + + // Step 1: compile the app and generate the .o file + + let load_config = LoadConfig { + target_info, + // TODO: we may not want this for just checking. + function_kind: FunctionKind::LambdaSet, + // TODO: expose this from CLI? + render: RenderTarget::ColorTerminal, + palette: DEFAULT_PALETTE, + threading, + exec_mode: ExecutionMode::Check, + }; + let mut loaded = + roc_load::load_and_typecheck(arena, roc_file_path, roc_cache_dir, load_config)?; + + let buf = &mut String::with_capacity(1024); + + let mut it = loaded.timings.iter().peekable(); + while let Some((module_id, module_timing)) = it.next() { + let module_name = loaded.interns.module_name(*module_id); + + buf.push_str(" "); + + if module_name.is_empty() { + // the App module + buf.push_str("Application Module"); + } else { + buf.push_str(module_name); + } + + buf.push('\n'); + + report_timing(buf, "Read .roc file from disk", module_timing.read_roc_file); + report_timing(buf, "Parse header", module_timing.parse_header); + report_timing(buf, "Parse body", module_timing.parse_body); + report_timing(buf, "Canonicalize", module_timing.canonicalize); + report_timing(buf, "Constrain", module_timing.constrain); + report_timing(buf, "Solve", module_timing.solve); + report_timing(buf, "Other", module_timing.other()); + buf.push('\n'); + report_timing(buf, "Total", module_timing.total()); + + if it.peek().is_some() { + buf.push('\n'); + } + } + + let compilation_end = compilation_start.elapsed(); + + if emit_timings { + println!( + "\n\nCompilation finished!\n\nHere's how long each module took to compile:\n\n{buf}" + ); + + println!("Finished checking in {} ms\n", compilation_end.as_millis(),); + } + + Ok((report_problems_typechecked(&mut loaded), compilation_end)) +} + +pub fn build_str_test<'a>( + arena: &'a Bump, + app_module_path: &Path, + app_module_source: &'a str, + assume_prebuild: bool, +) -> Result, BuildFileError<'a>> { + let triple = target_lexicon::Triple::host(); + + let code_gen_options = CodeGenOptions { + backend: CodeGenBackend::Llvm(LlvmBackendMode::Binary), + opt_level: OptLevel::Normal, + emit_debug_info: false, + }; + + let emit_timings = false; + let link_type = LinkType::Executable; + let linking_strategy = LinkingStrategy::Surgical; + let wasm_dev_stack_bytes = None; + + let roc_cache_dir = roc_packaging::cache::RocCacheDir::Disallowed; + let build_ordering = BuildOrdering::AlwaysBuild; + let threading = Threading::AtMost(2); + + let load_config = standard_load_config(&triple, build_ordering, threading); + + let compilation_start = std::time::Instant::now(); + + // Step 1: compile the app and generate the .o file + let loaded = roc_load::load_and_monomorphize_from_str( + arena, + PathBuf::from("valgrind_test.roc"), + app_module_source, + app_module_path.to_path_buf(), + roc_cache_dir, + load_config, + ) + .map_err(|e| BuildFileError::from_mono_error(e, compilation_start))?; + + build_loaded_file( + arena, + &triple, + app_module_path.to_path_buf(), + code_gen_options, + emit_timings, + link_type, + linking_strategy, + assume_prebuild, + wasm_dev_stack_bytes, + loaded, + compilation_start, + None, + ) +} + +fn with_executable_extension(path: &Path, os: OperatingSystem) -> PathBuf { + path.with_extension(os.executable_file_ext().unwrap_or_default()) +} diff --git a/compiler/build/src/target.rs b/crates/compiler/build/src/target.rs similarity index 83% rename from compiler/build/src/target.rs rename to crates/compiler/build/src/target.rs index e947c1d582..227e17895d 100644 --- a/compiler/build/src/target.rs +++ b/crates/compiler/build/src/target.rs @@ -2,6 +2,7 @@ use inkwell::{ targets::{CodeModel, InitializationConfig, RelocMode, Target, TargetMachine, TargetTriple}, OptimizationLevel, }; +use roc_error_macros::internal_error; use roc_mono::ir::OptLevel; use target_lexicon::{Architecture, Environment, OperatingSystem, Triple}; @@ -44,7 +45,7 @@ pub fn target_triple_str(target: &Triple) -> &'static str { operating_system: OperatingSystem::Windows, .. } => "x86_64-pc-windows-gnu", - _ => panic!("TODO gracefully handle unsupported target: {:?}", target), + _ => internal_error!("TODO gracefully handle unsupported target: {:?}", target), } } @@ -86,13 +87,13 @@ pub fn target_zig_str(target: &Triple) -> &'static str { architecture: Architecture::X86_64, operating_system: OperatingSystem::Darwin, .. - } => "x86_64-apple-darwin", + } => "x86_64-macos-none", Triple { architecture: Architecture::Aarch64(_), operating_system: OperatingSystem::Darwin, .. - } => "aarch64-apple-darwin", - _ => panic!("TODO gracefully handle unsupported target: {:?}", target), + } => "aarch64-macos-none", + _ => internal_error!("TODO gracefully handle unsupported target: {:?}", target), } } @@ -112,7 +113,7 @@ pub fn init_arch(target: &Triple) { Architecture::Wasm32 if cfg!(feature = "target-wasm32") => { Target::initialize_webassembly(&InitializationConfig::default()); } - _ => panic!( + _ => internal_error!( "TODO gracefully handle unsupported target architecture: {:?}", target.architecture ), @@ -132,7 +133,7 @@ pub fn arch_str(target: &Triple) -> &'static str { Architecture::Aarch64(_) if cfg!(feature = "target-aarch64") => "aarch64", Architecture::Arm(_) if cfg!(feature = "target-arm") => "arm", Architecture::Wasm32 if cfg!(feature = "target-webassembly") => "wasm32", - _ => panic!( + _ => internal_error!( "TODO gracefully handle unsupported target architecture: {:?}", target.architecture ), @@ -148,20 +149,29 @@ pub fn target_machine( init_arch(target); - let code_model = match target.architecture { - // LLVM 12 will not compile our programs without a large code model. - // The reason is not totally clear to me, but my guess is a few special-cases in - // llvm/lib/Target/AArch64/AArch64ISelLowering.cpp (instructions) - // llvm/lib/Target/AArch64/AArch64Subtarget.cpp (GoT tables) - // Revisit when upgrading to LLVM 13. - Architecture::Aarch64(..) => CodeModel::Large, + let code_model = match target { + Triple { + operating_system: OperatingSystem::Darwin, + architecture: Architecture::Aarch64(_), + .. + } => { + // We used to have a problem that LLVM 12 would not compile our programs without a large code model. + // The reason was not totally clear to us, but one guess is a few special-cases in + // llvm/lib/Target/AArch64/AArch64ISelLowering.cpp (instructions) + // llvm/lib/Target/AArch64/AArch64Subtarget.cpp (GoT tables) + // Revisit when upgrading to LLVM 13. + // + // Most recently, we seem to only see this problem on macOS ARM64; removing this + // failed macOS CI here: https://github.com/roc-lang/roc/pull/5644 + CodeModel::Large + } _ => CodeModel::Default, }; Target::from_name(arch).unwrap().create_target_machine( &TargetTriple::create(target_triple_str(target)), "generic", - "", // TODO: this probably should be TargetMachine::get_host_cpu_features() to enable all features. + "", opt, reloc, code_model, diff --git a/compiler/builtins/.gitignore b/crates/compiler/builtins/.gitignore similarity index 100% rename from compiler/builtins/.gitignore rename to crates/compiler/builtins/.gitignore diff --git a/crates/compiler/builtins/Cargo.toml b/crates/compiler/builtins/Cargo.toml new file mode 100644 index 0000000000..06409eb327 --- /dev/null +++ b/crates/compiler/builtins/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "roc_builtins" +description = "Provides the Roc functions and modules that are implicitly imported into every module." + +authors.workspace = true +edition.workspace = true +license.workspace = true +version.workspace = true + +[dependencies] +roc_collections = { path = "../collections" } +roc_module = { path = "../module" } +roc_region = { path = "../region" } +roc_target = { path = "../roc_target" } +roc_error_macros = { path = "../../error_macros" } + +tempfile.workspace = true + diff --git a/crates/compiler/builtins/README.md b/crates/compiler/builtins/README.md new file mode 100644 index 0000000000..7e53c09c57 --- /dev/null +++ b/crates/compiler/builtins/README.md @@ -0,0 +1,127 @@ +# So you want to add a builtin? + +Builtins are the functions and modules that are implicitly imported into every module. All of them compile down to llvm, but some are implemented directly as llvm and others in terms of intermediate functions. Either way, making a new builtin means touching many files. Lets make it easy for you and just list out which modules you need to visit to make a builtin. Here is what it takes: + +## A builtin written only in Roc + +Edit the appropriate `roc/*.roc` file with your new implementation. All normal rules for writing Roc code apply. Be sure to add a declaration, definition, some documentation and add it to the exposes list it in the module head. + +Next, look towards the bottom of the `compiler/module/src/symbol.rs` file. Inside the `define_builtins!` macro, there is a list for each of the builtin modules and the function or value names it contains. Add a new entry to the appropriate list for your new function. + +For each of the builtin modules, there is a file in `compiler/test_gen/src/` like `gen_num.rs`, `gen_str.rs` etc. Add new tests for the module you are changing to the appropriate file here. You can look at the existing test cases for examples and inspiration. + +You can run your new tests locally using `cargo test-gen-llvm`. You can add a filter like `cargo test-gen-llvm gen_str` (to only run tests defined in `gen_str.rs`) or `cargo test-gen-llvm gen_str::str_split` (to only run tests defined in `gen_str` whose names start with `str_split`). More details can be found in the README in the `compiler/test_gen` directory. + +## A builtin implemented directly as LLVM + +### module/src/symbol.rs + +Towards the bottom of `symbol.rs` there is a `define_builtins!` macro being used that takes many modules and function names. The first level (`List`, `Int` ..) is the module name, and the second level is the function or value name (`reverse`, `rem` ..). If you wanted to add a `Int` function called `addTwo` go to `2 Int: "Int" => {` and inside that case add to the bottom `38 INT_ADD_TWO: "addTwo"` (assuming there are 37 existing ones). + +Some of these have `#` inside their name (`first#list`, `#lt` ..). This is a trick we are doing to hide implementation details from Roc programmers. To a Roc programmer, a name with `#` in it is invalid, because `#` means everything after it is parsed to a comment. We are constructing these functions manually, so we are circumventing the parsing step and don't have such restrictions. We get to make functions and values with `#` which as a consequence are not accessible to Roc programmers. Roc programmers simply cannot reference them. + +But we can use these values and some of these are necessary for implementing builtins. For example, `List.get` returns tags, and it is not easy for us to create tags when composing LLVM. What is easier however, is: + +- ..writing `List.#getUnsafe` that has the dangerous signature of `List elem, Nat -> elem` in LLVM +- ..writing `List elem, Nat -> Result elem [OutOfBounds]*` in a type safe way that uses `getUnsafe` internally, only after it checks if the `elem` at `Nat` index exists. + +### can/src/builtins.rs + +Right at the top of this module is a function called `builtin_defs`. All this is doing is mapping the `Symbol` defined in `module/src/symbol.rs` to its implementation. Some of the builtins are quite complex, such as `list_get`. What makes `list_get` is that it returns tags, and in order to return tags it first has to defer to lower-level functions via an if statement. + +Lets look at `List.repeat : elem, Nat -> List elem`, which is more straight-forward, and points directly to its lower level implementation: + +```rust +fn list_repeat(symbol: Symbol, var_store: &mut VarStore) -> Def { + let elem_var = var_store.fresh(); + let len_var = var_store.fresh(); + let list_var = var_store.fresh(); + + let body = RunLowLevel { + op: LowLevel::ListRepeat, + args: vec![ + (elem_var, Var(Symbol::ARG_1)), + (len_var, Var(Symbol::ARG_2)), + ], + ret_var: list_var, + }; + + defn( + symbol, + vec![(elem_var, Symbol::ARG_1), (len_var, Symbol::ARG_2)], + var_store, + body, + list_var, + ) +} +``` + +In these builtin definitions you will need to allocate for and list the arguments. For `List.repeat`, the arguments are the `elem_var` and the `len_var`. So in both the `body` and `defn` we list these arguments in a vector, with the `Symbol::ARG_1` and `Symvol::ARG_2` designating which argument is which. + +Since `List.repeat` is implemented entirely as low level functions, its `body` is a `RunLowLevel`, and the `op` is `LowLevel::ListRepeat`. Lets talk about `LowLevel` in the next section. + +## Connecting the definition to the implementation + +### module/src/low_level.rs + +This `LowLevel` thing connects the builtin defined in this module to its implementation. It's referenced in `can/src/builtins.rs` and it is used in `gen/src/llvm/build.rs`. + +## Bottom level LLVM values and functions + +### gen/src/llvm/build.rs + +This is where bottom-level functions that need to be written as LLVM are created. If the function leads to a tag that's a good sign it should not be written here in `build.rs`. If it's simple fundamental stuff like `INT_ADD` then it certainly should be written here. + +## Letting the compiler know these functions exist + +### builtins/src/std.rs + +It's one thing to actually write these functions, it's _another_ thing to let the Roc compiler know they exist as part of the standard library. You have to tell the compiler "Hey, this function exists, and it has this type signature". That happens in `std.rs`. + +## Specifying how we pass args to the function + +### builtins/mono/src/borrow.rs + +After we have all of this, we need to specify if the arguments we're passing are owned, borrowed or irrelevant. Towards the bottom of this file, add a new case for your builtin and specify each arg. Be sure to read the comment, as it explains this in more detail. + +## Testing it + +### solve/tests/solve_expr.rs + +To make sure that Roc is properly inferring the type of the new builtin, add a test to this file similar to: + +```rust + #[test] +fn atan() { + infer_eq_without_problem( + indoc!( + r#" + Num.atan + "# + ), + "Frac a -> Frac a", + ); +} +``` + +But replace `Num.atan` and the type signature with the new builtin. + +### test_gen/test/*.rs + +In this directory, there are a couple files like `gen_num.rs`, `gen_str.rs`, etc. For the `Str` module builtins, put the test in `gen_str.rs`, etc. Find the one for the new builtin, and add a test like: + +```rust +#[test] +fn atan() { + assert_evals_to!("Num.atan 10", 1.4711276743037347, f64); +} +``` + +But replace `Num.atan`, the return value, and the return type with your new builtin. + +## Mistakes that are easy to make!! + +When implementing a new builtin, it is often easy to copy and paste the implementation for an existing builtin. This can take you quite far since many builtins are very similar, but it also risks forgetting to change one small part of what you copy and pasted and losing a lot of time later on when you cant figure out why things don't work. So, speaking from experience, even if you are copying an existing builtin, try and implement it manually without copying and pasting. Two recent instances of this (as of September 7th, 2020): + +- `List.keepIf` did not work for a long time because in builtins its `LowLevel` was `ListMap`. This was because I copy and pasted the `List.map` implementation in `builtins.rs +- `List.walkBackwards` had mysterious memory bugs for a little while because in `unique.rs` its return type was `list_type(flex(b))` instead of `flex(b)` since it was copy and pasted from `List.keepIf`. diff --git a/crates/compiler/builtins/bitcode/.gitignore b/crates/compiler/builtins/bitcode/.gitignore new file mode 100644 index 0000000000..7a3a8456f8 --- /dev/null +++ b/crates/compiler/builtins/bitcode/.gitignore @@ -0,0 +1,5 @@ +zig-out +zig-cache +src/zig-cache +benchmark/zig-cache +dec diff --git a/crates/compiler/builtins/bitcode/Cargo.toml b/crates/compiler/builtins/bitcode/Cargo.toml new file mode 100644 index 0000000000..dfcaf7c8eb --- /dev/null +++ b/crates/compiler/builtins/bitcode/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "roc_bitcode" +description = "Compiles the zig bitcode to `.o` for builtins" + +authors.workspace = true +edition.workspace = true +license.workspace = true +version.workspace = true + +[dependencies] +tempfile.workspace = true + +[build-dependencies] +roc_command_utils = { path = "../../../utils/command" } +roc_error_macros = { path = "../../../error_macros" } +# dunce can be removed once ziglang/zig#5109 is fixed +dunce = "1.0.3" + +[target.'cfg(target_os = "macos")'.build-dependencies] +tempfile.workspace = true diff --git a/crates/compiler/builtins/bitcode/README.md b/crates/compiler/builtins/bitcode/README.md new file mode 100644 index 0000000000..51e19330fa --- /dev/null +++ b/crates/compiler/builtins/bitcode/README.md @@ -0,0 +1,36 @@ +# Bitcode for Builtins + +## Adding a bitcode builtin + +To add a builtin: + +1. Add the function to the relevant module. For `Num` builtin use it in `src/num.zig`, for `Str` builtins use `src/str.zig`, and so on. **For anything you add, you must add tests for it!** Not only does to make the builtins more maintainable, it's the the easiest way to test these functions on Zig. To run the test, run: `zig build test` +2. Make sure the function is public with the `pub` keyword and uses the C calling convention. This is really easy, just add `pub` and `callconv(.C)` to the function declaration like so: `pub fn atan(num: f64) callconv(.C) f64 { ... }` +3. In `src/main.zig`, export the function. This is also organized by module. For example, for a `Num` function find the `Num` section and add: `comptime { exportNumFn(num.atan, "atan"); }`. The first argument is the function, the second is the name of it in LLVM. +4. In `compiler/builtins/src/bitcode.rs`, add a constant for the new function. This is how we use it in Rust. Once again, this is organized by module, so just find the relevant area and add your new function. +5. You can now use your function in Rust using `call_bitcode_fn` in `llvm/src/build.rs`! + +## How it works + +Roc's builtins are implemented in the compiler using LLVM only. +When their implementations are simple enough (e.g. addition), they +can be implemented directly in Inkwell. + +When their implementations are complex enough, it's nicer to +implement them in a higher-level language like Zig, then compile +the result to LLVM bitcode, and import that bitcode into the compiler. + +Compiling the bitcode happens automatically in a Rust build script at `compiler/builtins/build.rs`. +Then `builtins/src/bitcode/rs` statically imports the compiled bitcode for use in the compiler. + +You can find the compiled bitcode in `target/debug/build/roc_builtins-[some random characters]/out/builtins.bc`. +There will be two directories like `roc_builtins-[some random characters]`, look for the one that has an +`out` directory as a child. + +> The bitcode is a bunch of bytes that aren't particularly human-readable. +> If you want to take a look at the human-readable LLVM IR, look at +> `compiler/builtins/bitcode/builtins.ll` + +## Calling bitcode functions + +Use the `call_bitcode_fn` function defined in `llvm/src/build.rs` to call bitcode functions. diff --git a/crates/compiler/builtins/bitcode/bc/Cargo.toml b/crates/compiler/builtins/bitcode/bc/Cargo.toml new file mode 100644 index 0000000000..456dca72f4 --- /dev/null +++ b/crates/compiler/builtins/bitcode/bc/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "roc_bitcode_bc" +description = "Compiles the zig bitcode to `.bc` for llvm" + +authors.workspace = true +edition.workspace = true +license.workspace = true +version.workspace = true + +[build-dependencies] +roc_command_utils = { path = "../../../../utils/command" } +roc_error_macros = { path = "../../../../error_macros" } +# dunce can be removed once ziglang/zig#5109 is fixed +dunce = "1.0.3" + +[target.'cfg(target_os = "macos")'.build-dependencies] +tempfile.workspace = true diff --git a/crates/compiler/builtins/bitcode/bc/build.rs b/crates/compiler/builtins/bitcode/bc/build.rs new file mode 100644 index 0000000000..b79adf1066 --- /dev/null +++ b/crates/compiler/builtins/bitcode/bc/build.rs @@ -0,0 +1,156 @@ +use roc_command_utils::{pretty_command_string, zig}; +use roc_error_macros::internal_error; +use std::fs; +use std::io; +use std::path::Path; +use std::str; +use std::{env, path::PathBuf, process::Command}; + +#[cfg(target_os = "macos")] +use tempfile::tempdir; + +/// To debug the zig code with debug prints, we need to disable the wasm code gen +const DEBUG: bool = false; + +fn main() { + println!("cargo:rerun-if-changed=build.rs"); + + // "." is relative to where "build.rs" is + // dunce can be removed once ziglang/zig#5109 is fixed + let bitcode_path = dunce::canonicalize(Path::new(".")).unwrap().join(".."); + + // workaround for github.com/ziglang/zig/issues/9711 + #[cfg(target_os = "macos")] + let zig_cache_dir = tempdir().expect("Failed to create temp directory for zig cache"); + #[cfg(target_os = "macos")] + std::env::set_var("ZIG_GLOBAL_CACHE_DIR", zig_cache_dir.path().as_os_str()); + + // LLVM .bc FILES + + generate_bc_file(&bitcode_path, "ir", "builtins-host"); + + if !DEBUG { + generate_bc_file(&bitcode_path, "ir-wasm32", "builtins-wasm32"); + } + + generate_bc_file(&bitcode_path, "ir-x86", "builtins-x86"); + generate_bc_file(&bitcode_path, "ir-x86_64", "builtins-x86_64"); + generate_bc_file(&bitcode_path, "ir-aarch64", "builtins-aarch64"); + generate_bc_file( + &bitcode_path, + "ir-windows-x86_64", + "builtins-windows-x86_64", + ); + + get_zig_files(bitcode_path.as_path(), &|path| { + let path: &Path = path; + println!( + "cargo:rerun-if-changed={}", + path.to_str().expect("Failed to convert path to str") + ); + }) + .unwrap(); + + #[cfg(target_os = "macos")] + zig_cache_dir + .close() + .expect("Failed to delete temp dir zig_cache_dir."); +} + +fn generate_bc_file(bitcode_path: &Path, zig_object: &str, file_name: &str) { + let mut ll_path = bitcode_path.join("zig-out").join(file_name); + ll_path.set_extension("ll"); + let dest_ir_host = ll_path.to_str().expect("Invalid dest ir path"); + + println!("Compiling host ir to: {dest_ir_host}"); + + let mut bc_path = bitcode_path.join("zig-out").join(file_name); + bc_path.set_extension("bc"); + let dest_bc_64bit = bc_path.to_str().expect("Invalid dest bc path"); + println!("Compiling 64-bit bitcode to: {dest_bc_64bit}"); + + // workaround for github.com/ziglang/zig/issues/9711 + #[cfg(target_os = "macos")] + let _ = fs::remove_dir_all("./zig-cache"); + + let mut zig_cmd = zig(); + + zig_cmd + .current_dir(bitcode_path) + .args(["build", zig_object, "-Drelease=true"]); + + run_command(zig_cmd, 0); +} + +pub fn get_lib_dir() -> PathBuf { + // Currently we have the OUT_DIR variable which points to `/target/debug/build/roc_builtins-*/out/`. + // So we just need to add "/bitcode" to that. + let dir = PathBuf::from(env::var_os("OUT_DIR").unwrap()); + + // create dir if it does not exist + fs::create_dir_all(&dir).expect("Failed to make $OUT_DIR/ dir."); + + dir +} + +fn run_command(mut command: Command, flaky_fail_counter: usize) { + let command_str = pretty_command_string(&command); + let command_str = command_str.to_string_lossy(); + + let output_result = command.output(); + + match output_result { + Ok(output) => match output.status.success() { + true => (), + false => { + let error_str = match str::from_utf8(&output.stderr) { + Ok(stderr) => stderr.to_string(), + Err(_) => format!("Failed to run \"{command_str}\""), + }; + + // Flaky test errors that only occur sometimes on MacOS ci server. + if error_str.contains("FileNotFound") + || error_str.contains("unable to save cached ZIR code") + || error_str.contains("LLVM failed to emit asm") + { + if flaky_fail_counter == 10 { + internal_error!("{} failed 10 times in a row. The following error is unlikely to be a flaky error: {}", command_str, error_str); + } else { + run_command(command, flaky_fail_counter + 1) + } + } else if error_str + .contains("lld-link: error: failed to write the output file: Permission denied") + { + internal_error!("{} failed with:\n\n {}\n\nWorkaround:\n\n Re-run the cargo command that triggered this build.\n\n", command_str, error_str); + } else { + internal_error!("{} failed with:\n\n {}\n", command_str, error_str); + } + } + }, + Err(reason) => internal_error!("{} failed: {}", command_str, reason), + } +} + +fn get_zig_files(dir: &Path, cb: &dyn Fn(&Path)) -> io::Result<()> { + if dir.is_dir() { + for entry in fs::read_dir(dir)? { + let entry = entry?; + let path_buf = entry.path(); + if path_buf.is_dir() { + if !path_buf.ends_with("zig-cache") { + get_zig_files(&path_buf, cb).unwrap(); + } + } else { + let path = path_buf.as_path(); + + match path.extension() { + Some(osstr) if osstr == "zig" => { + cb(path); + } + _ => {} + } + } + } + } + Ok(()) +} diff --git a/crates/compiler/builtins/bitcode/bc/src/lib.rs b/crates/compiler/builtins/bitcode/bc/src/lib.rs new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/crates/compiler/builtins/bitcode/bc/src/lib.rs @@ -0,0 +1 @@ + diff --git a/crates/compiler/builtins/bitcode/benchmark.sh b/crates/compiler/builtins/bitcode/benchmark.sh new file mode 100755 index 0000000000..410188d451 --- /dev/null +++ b/crates/compiler/builtins/bitcode/benchmark.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +# https://vaneyckt.io/posts/safer_bash_scripts_with_set_euxo_pipefail/ +set -euxo pipefail + +zig build-exe benchmark/dec.zig -O ReleaseFast --main-pkg-path . +./dec diff --git a/crates/compiler/builtins/bitcode/benchmark/dec.zig b/crates/compiler/builtins/bitcode/benchmark/dec.zig new file mode 100644 index 0000000000..965d3bdd0c --- /dev/null +++ b/crates/compiler/builtins/bitcode/benchmark/dec.zig @@ -0,0 +1,232 @@ +const std = @import("std"); +const time = std.time; +const Timer = time.Timer; + +const RocStr = @import("../src/str.zig").RocStr; +const RocDec = @import("../src/dec.zig").RocDec; + +fn roc_alloc(_: usize, _: u32) callconv(.C) ?*anyopaque { + @panic("Not needed for dec benchmark"); +} +fn roc_panic(_: *anyopaque, _: u32) callconv(.C) void { + @panic("Not needed for dec benchmark"); +} +fn roc_dbg(_: *anyopaque, _: *anyopaque) callconv(.C) void { + @panic("Not needed for dec benchmark"); +} + +comptime { + @export(roc_alloc, .{ .name = "roc_alloc", .linkage = .Strong }); + @export(roc_panic, .{ .name = "roc_panic", .linkage = .Strong }); + @export(roc_dbg, .{ .name = "roc_dbg", .linkage = .Strong }); +} + +var timer: Timer = undefined; + +pub fn main() !void { + const stdout = std.io.getStdOut().writer(); + try stdout.print("Warning: Timer seems to step in units of 41ns\n\n", .{}); + timer = try Timer.start(); + + const n = 1000; + + // Add/Sub are too fast and need a higher n. + const add_sub_n = 10000; + + // This number are very close to 1 to avoid over and underflow. + const f1 = 1.00123; + const dec1 = RocDec.fromF64(f1).?; + + // `asin` and `acos` have a limited range, so they will use this value. + const f2 = 0.00130000847; + const dec2 = RocDec.fromF64(f2).?; + + try stdout.print("Dec:\n", .{}); + try stdout.print("{} additions took ", .{add_sub_n}); + const decAdd = try avg_runs(RocDec, add_sub_n, RocDec.add, dec1); + + try stdout.print("{} subtractions took ", .{add_sub_n}); + const decSub = try avg_runs(RocDec, add_sub_n, RocDec.sub, dec1); + + try stdout.print("{} multiplications took ", .{n}); + const decMul = try avg_runs(RocDec, n, RocDec.mul, dec1); + + try stdout.print("{} divisions took ", .{n}); + const decDiv = try avg_runs(RocDec, n, RocDec.div, dec1); + + try stdout.print("{} sin took ", .{n}); + const decSin = try avg_runs(RocDec, n, sinDec, dec1); + + try stdout.print("{} cos took ", .{n}); + const decCos = try avg_runs(RocDec, n, cosDec, dec1); + + try stdout.print("{} tan took ", .{n}); + const decTan = try avg_runs(RocDec, n, tanDec, dec1); + + try stdout.print("{} asin took ", .{n}); + const decAsin = try avg_runs(RocDec, n, asinDec, dec2); + + try stdout.print("{} acos took ", .{n}); + const decAcos = try avg_runs(RocDec, n, acosDec, dec2); + + try stdout.print("{} atan took ", .{n}); + const decAtan = try avg_runs(RocDec, n, atanDec, dec1); + + try stdout.print("\n\nF64:\n", .{}); + try stdout.print("{} additions took ", .{add_sub_n}); + const f64Add = try avg_runs(f64, add_sub_n, addF64, f1); + + try stdout.print("{} subtractions took ", .{add_sub_n}); + const f64Sub = try avg_runs(f64, add_sub_n, subF64, f1); + + try stdout.print("{} multiplications took ", .{n}); + const f64Mul = try avg_runs(f64, n, mulF64, f1); + + try stdout.print("{} divisions took ", .{n}); + const f64Div = try avg_runs(f64, n, divF64, f1); + + try stdout.print("{} sin took ", .{n}); + const f64Sin = try avg_runs(f64, n, sinF64, f1); + + try stdout.print("{} cos took ", .{n}); + const f64Cos = try avg_runs(f64, n, cosF64, f1); + + try stdout.print("{} tan took ", .{n}); + const f64Tan = try avg_runs(f64, n, tanF64, f1); + + try stdout.print("{} asin took ", .{n}); + const f64Asin = try avg_runs(f64, n, asinF64, f2); + + try stdout.print("{} acos took ", .{n}); + const f64Acos = try avg_runs(f64, n, acosF64, f2); + + try stdout.print("{} atan took ", .{n}); + const f64Atan = try avg_runs(f64, n, atanF64, f1); + + try stdout.print("\n\nDec/F64:\n", .{}); + try stdout.print("addition: {d:0.2}\n", .{@as(f64, @floatFromInt(decAdd)) / @as(f64, @floatFromInt(f64Add))}); + try stdout.print("subtraction: {d:0.2}\n", .{@as(f64, @floatFromInt(decSub)) / @as(f64, @floatFromInt(f64Sub))}); + try stdout.print("multiplication: {d:0.2}\n", .{@as(f64, @floatFromInt(decMul)) / @as(f64, @floatFromInt(f64Mul))}); + try stdout.print("division: {d:0.2}\n", .{@as(f64, @floatFromInt(decDiv)) / @as(f64, @floatFromInt(f64Div))}); + try stdout.print("sin: {d:0.2}\n", .{@as(f64, @floatFromInt(decSin)) / @as(f64, @floatFromInt(f64Sin))}); + try stdout.print("cos: {d:0.2}\n", .{@as(f64, @floatFromInt(decCos)) / @as(f64, @floatFromInt(f64Cos))}); + try stdout.print("tan: {d:0.2}\n", .{@as(f64, @floatFromInt(decTan)) / @as(f64, @floatFromInt(f64Tan))}); + try stdout.print("asin: {d:0.2}\n", .{@as(f64, @floatFromInt(decAsin)) / @as(f64, @floatFromInt(f64Asin))}); + try stdout.print("acos: {d:0.2}\n", .{@as(f64, @floatFromInt(decAcos)) / @as(f64, @floatFromInt(f64Acos))}); + try stdout.print("atan: {d:0.2}\n", .{@as(f64, @floatFromInt(decAtan)) / @as(f64, @floatFromInt(f64Atan))}); +} + +fn avg_runs(comptime T: type, comptime n: usize, comptime op: fn (T, T) T, v: T) !u64 { + const stdout = std.io.getStdOut().writer(); + + const warmups = 10000; + const repeats = 10000; + var runs = [_]u64{0} ** (warmups + repeats); + + var i: usize = 0; + while (i < warmups + repeats) : (i += 1) { + // Never inline run to ensure it doesn't optimize for the value of `v`. + runs[i] = callWrapper(u64, .never_inline, run, .{ T, n, op, v }); + } + + var real_runs = runs[warmups..runs.len]; + std.sort.sort(u64, real_runs, {}, comptime std.sort.asc(u64)); + + const median = real_runs[real_runs.len / 2]; + const highest = real_runs[real_runs.len - 1]; + const lowest = real_runs[0]; + + try stdout.print("{}ns (lowest: {}ns, highest: {}ns)\n", .{ median, lowest, highest }); + return median; +} + +fn run(comptime T: type, comptime n: usize, comptime op: fn (T, T) T, v: T) u64 { + var a = v; + timer.reset(); + + // Split into outer and inner loop to avoid breaking comptime. + const max_inline = 100; + comptime var outer = n / max_inline; + comptime var inner = std.math.min(n, max_inline); + var i: usize = 0; + while (i < outer) : (i += 1) { + comptime var j = 0; + inline while (j < inner) : (j += 1) { + a = callWrapper(T, .always_inline, op, .{ a, v }); + } + } + const rem = n % max_inline; + comptime var j = 0; + inline while (j < rem) : (j += 1) { + a = callWrapper(T, .always_inline, op, .{ a, v }); + } + + // Clobber `a` to avoid removal as dead code. + asm volatile ("" + : + : [a] "r,m" (&a), + : "memory" + ); + return timer.read(); +} + +// This is needed to work around a bug with using `@call` in loops. +inline fn callWrapper(comptime T: type, call_modifier: anytype, comptime func: anytype, params: anytype) T { + return @call(.{ .modifier = call_modifier }, func, params); +} + +fn addF64(x: f64, y: f64) f64 { + return x + y; +} +fn subF64(x: f64, y: f64) f64 { + return x - y; +} +fn mulF64(x: f64, y: f64) f64 { + return x * y; +} +fn divF64(x: f64, y: f64) f64 { + return x / y; +} +fn sinF64(x: f64, _: f64) f64 { + return std.math.sin(x); +} +fn cosF64(x: f64, _: f64) f64 { + return std.math.cos(x); +} +fn tanF64(x: f64, _: f64) f64 { + return std.math.tan(x); +} +fn asinF64(x: f64, _: f64) f64 { + return std.math.asin(x); +} +const pi_over_2 = std.math.pi / 2.0; +fn acosF64(x: f64, _: f64) f64 { + // acos is only stable if we subtract pi/2. + // The perf should be essentially the same because subtraction is much faster than acos. + return std.math.acos(x) - pi_over_2; +} +fn atanF64(x: f64, _: f64) f64 { + return std.math.atan(x); +} + +fn sinDec(x: RocDec, _: RocDec) RocDec { + return x.sin(); +} +fn cosDec(x: RocDec, _: RocDec) RocDec { + return x.cos(); +} +fn tanDec(x: RocDec, _: RocDec) RocDec { + return x.tan(); +} +fn asinDec(x: RocDec, _: RocDec) RocDec { + return x.asin(); +} +const pi_over_2_dec = RocDec.fromF64(pi_over_2).?; +fn acosDec(x: RocDec, _: RocDec) RocDec { + // acos is only stable if we subtract pi/2. + // The perf should be essentially the same because subtraction is much faster than acos. + return x.acos().sub(pi_over_2_dec); +} +fn atanDec(x: RocDec, _: RocDec) RocDec { + return x.atan(); +} diff --git a/crates/compiler/builtins/bitcode/build.rs b/crates/compiler/builtins/bitcode/build.rs new file mode 100644 index 0000000000..d84fbcccca --- /dev/null +++ b/crates/compiler/builtins/bitcode/build.rs @@ -0,0 +1,218 @@ +use roc_command_utils::{pretty_command_string, zig}; +use roc_error_macros::internal_error; +use std::fs; +use std::io; +use std::path::Path; +use std::str; +use std::{env, path::PathBuf, process::Command}; + +#[cfg(target_os = "macos")] +use tempfile::tempdir; + +/// To debug the zig code with debug prints, we need to disable the wasm code gen +const DEBUG: bool = false; + +fn main() { + println!("cargo:rerun-if-changed=build.rs"); + + // "." is relative to where "build.rs" is + // dunce can be removed once ziglang/zig#5109 is fixed + let bitcode_path = dunce::canonicalize(Path::new(".")).unwrap(); + + // workaround for github.com/ziglang/zig/issues/9711 + #[cfg(target_os = "macos")] + let zig_cache_dir = tempdir().expect("Failed to create temp directory for zig cache"); + #[cfg(target_os = "macos")] + std::env::set_var("ZIG_GLOBAL_CACHE_DIR", zig_cache_dir.path().as_os_str()); + + // OBJECT FILES + #[cfg(windows)] + const BUILTINS_HOST_FILE: &str = "builtins-host.obj"; + + #[cfg(not(windows))] + const BUILTINS_HOST_FILE: &str = "builtins-host.o"; + + generate_object_file(&bitcode_path, "object", BUILTINS_HOST_FILE); + + generate_object_file( + &bitcode_path, + "windows-x86_64-object", + "builtins-windows-x86_64.obj", + ); + + if !DEBUG { + generate_object_file(&bitcode_path, "wasm32-object", "builtins-wasm32.o"); + } + + copy_zig_builtins_to_target_dir(&bitcode_path); + + get_zig_files(bitcode_path.as_path(), &|path| { + let path: &Path = path; + println!( + "cargo:rerun-if-changed={}", + path.to_str().expect("Failed to convert path to str") + ); + }) + .unwrap(); + + #[cfg(target_os = "macos")] + zig_cache_dir + .close() + .expect("Failed to delete temp dir zig_cache_dir."); +} + +fn generate_object_file(bitcode_path: &Path, zig_object: &str, object_file_name: &str) { + let dest_obj_path = get_lib_dir().join(object_file_name); + let dest_obj = dest_obj_path.to_str().expect("Invalid dest object path"); + + let src_obj_path = bitcode_path.join("zig-out").join(object_file_name); + let src_obj = src_obj_path.to_str().expect("Invalid src object path"); + + println!("Compiling zig object `{zig_object}` to: {src_obj}"); + + let mut zig_cmd = zig(); + + zig_cmd + .current_dir(bitcode_path) + .args(["build", zig_object, "-Drelease=true"]); + + run_command(zig_cmd, 0); + + println!("Moving zig object `{zig_object}` to: {dest_obj}"); + + // we store this .o file in rust's `target` folder (for wasm we need to leave a copy here too) + fs::copy(src_obj, dest_obj).unwrap_or_else(|err| { + internal_error!( + "Failed to copy object file {} to {}: {:?}", + src_obj, + dest_obj, + err + ); + }); +} + +pub fn get_lib_dir() -> PathBuf { + // Currently we have the OUT_DIR variable which points to `/target/debug/build/roc_builtins-*/out/`. + // So we just need to add "/bitcode" to that. + let dir = PathBuf::from(env::var_os("OUT_DIR").unwrap()); + + // create dir if it does not exist + fs::create_dir_all(&dir).expect("Failed to make $OUT_DIR/ dir."); + + dir +} + +fn copy_zig_builtins_to_target_dir(bitcode_path: &Path) { + // To enable roc to find the zig builtins, we want them to be moved to a folder next to the roc executable. + // So if /roc is the executable. The zig files will be in /lib/*.zig + let target_profile_dir = get_lib_dir(); + + let zig_src_dir = bitcode_path.join("src"); + + cp_unless_zig_cache(&zig_src_dir, &target_profile_dir).unwrap_or_else(|err| { + internal_error!( + "Failed to copy zig bitcode files {:?} to {:?}: {:?}", + zig_src_dir, + target_profile_dir, + err + ); + }); +} + +// recursively copy all the .zig files from this directory, but do *not* recurse into zig-cache/ +fn cp_unless_zig_cache(src_dir: &Path, target_dir: &Path) -> io::Result<()> { + // Make sure the destination directory exists before we try to copy anything into it. + std::fs::create_dir_all(target_dir).unwrap_or_else(|err| { + internal_error!( + "Failed to create output library directory for zig bitcode {:?}: {:?}", + target_dir, + err + ); + }); + + for entry in fs::read_dir(src_dir)? { + let src_path = entry?.path(); + let src_filename = src_path.file_name().unwrap(); + + // Only copy individual files if they have the .zig extension + if src_path.extension().unwrap_or_default() == "zig" { + let dest = target_dir.join(src_filename); + + fs::copy(&src_path, &dest).unwrap_or_else(|err| { + internal_error!( + "Failed to copy zig bitcode file {:?} to {:?}: {:?}", + src_path, + dest, + err + ); + }); + } else if src_path.is_dir() && src_filename != "zig-cache" { + // Recursively copy all directories except zig-cache + cp_unless_zig_cache(&src_path, &target_dir.join(src_filename))?; + } + } + + Ok(()) +} + +fn run_command(mut command: Command, flaky_fail_counter: usize) { + let command_str = pretty_command_string(&command); + let command_str = command_str.to_string_lossy(); + + let output_result = command.output(); + + match output_result { + Ok(output) => match output.status.success() { + true => (), + false => { + let error_str = match str::from_utf8(&output.stderr) { + Ok(stderr) => stderr.to_string(), + Err(_) => format!("Failed to run \"{command_str}\""), + }; + + // Flaky test errors that only occur sometimes on MacOS ci server. + if error_str.contains("FileNotFound") + || error_str.contains("unable to save cached ZIR code") + || error_str.contains("LLVM failed to emit asm") + { + if flaky_fail_counter == 10 { + internal_error!("{} failed 10 times in a row. The following error is unlikely to be a flaky error: {}", command_str, error_str); + } else { + run_command(command, flaky_fail_counter + 1) + } + } else if error_str + .contains("lld-link: error: failed to write the output file: Permission denied") + { + internal_error!("{} failed with:\n\n {}\n\nWorkaround:\n\n Re-run the cargo command that triggered this build.\n\n", command_str, error_str); + } else { + internal_error!("{} failed with:\n\n {}\n", command_str, error_str); + } + } + }, + Err(reason) => internal_error!("{} failed: {}", command_str, reason), + } +} + +fn get_zig_files(dir: &Path, cb: &dyn Fn(&Path)) -> io::Result<()> { + if dir.is_dir() { + for entry in fs::read_dir(dir)? { + let entry = entry?; + let path_buf = entry.path(); + if path_buf.is_dir() { + if !path_buf.ends_with("zig-cache") { + get_zig_files(&path_buf, cb).unwrap(); + } + } else { + let path = path_buf.as_path(); + + match path.extension() { + Some(osstr) if osstr == "zig" => { + cb(path); + } + _ => {} + } + } + } + } + Ok(()) +} diff --git a/crates/compiler/builtins/bitcode/build.zig b/crates/compiler/builtins/bitcode/build.zig new file mode 100644 index 0000000000..c0bd931386 --- /dev/null +++ b/crates/compiler/builtins/bitcode/build.zig @@ -0,0 +1,157 @@ +const std = @import("std"); +const builtin = @import("builtin"); +const mem = std.mem; +const Build = std.Build; +const LazyPath = Build.LazyPath; +const CrossTarget = std.zig.CrossTarget; +const Arch = std.Target.Cpu.Arch; + +pub fn build(b: *Build) void { + // const mode = b.standardOptimizeOption(.{ .preferred_optimize_mode = .ReleaseFast }); + const mode = b.standardOptimizeOption(.{ .preferred_optimize_mode = .ReleaseFast }); + + // Options + const fallback_main_path = "./src/main.zig"; + const main_path_desc = b.fmt("Override path to main.zig. Used by \"ir\" and \"test\". Defaults to \"{s}\". ", .{fallback_main_path}); + const main_path = .{ .path = b.option([]const u8, "main-path", main_path_desc) orelse fallback_main_path }; + + // Tests + const main_tests = b.addTest(.{ .root_source_file = main_path, .link_libc = true }); + const test_step = b.step("test", "Run tests"); + test_step.dependOn(&b.addRunArtifact(main_tests).step); + + // Targets + const host_target = b.standardTargetOptions(.{ + .default_target = CrossTarget{ + .cpu_model = .baseline, + .os_tag = builtin.os.tag, + }, + }); + const linux32_target = makeLinux32Target(); + const linux_x64_target = makeLinuxX64Target(); + const linux_aarch64_target = makeLinuxAarch64Target(); + const windows64_target = makeWindows64Target(); + const wasm32_target = makeWasm32Target(); + + // LLVM IR + generateLlvmIrFile(b, mode, host_target, main_path, "ir", "builtins-host"); + generateLlvmIrFile(b, mode, linux32_target, main_path, "ir-x86", "builtins-x86"); + generateLlvmIrFile(b, mode, linux_x64_target, main_path, "ir-x86_64", "builtins-x86_64"); + generateLlvmIrFile(b, mode, linux_aarch64_target, main_path, "ir-aarch64", "builtins-aarch64"); + generateLlvmIrFile(b, mode, windows64_target, main_path, "ir-windows-x86_64", "builtins-windows-x86_64"); + generateLlvmIrFile(b, mode, wasm32_target, main_path, "ir-wasm32", "builtins-wasm32"); + + // Generate Object Files + generateObjectFile(b, mode, host_target, main_path, "object", "builtins-host"); + generateObjectFile(b, mode, windows64_target, main_path, "windows-x86_64-object", "builtins-windows-x86_64"); + generateObjectFile(b, mode, wasm32_target, main_path, "wasm32-object", "builtins-wasm32"); +} + +// TODO zig 0.9 can generate .bc directly, switch to that when it is released! +fn generateLlvmIrFile( + b: *Build, + mode: std.builtin.Mode, + target: CrossTarget, + main_path: LazyPath, + step_name: []const u8, + object_name: []const u8, +) void { + const obj = b.addObject(.{ .name = object_name, .root_source_file = main_path, .optimize = mode, .target = target, .use_llvm = true }); + obj.strip = true; + + // Generating the bin seems required to get zig to generate the llvm ir. + _ = obj.getEmittedBin(); + const ir_file = obj.getEmittedLlvmIr(); + const bc_file = obj.getEmittedLlvmBc(); + const install_ir = b.addInstallFile(ir_file, b.fmt("{s}.ll", .{object_name})); + const install_bc = b.addInstallFile(bc_file, b.fmt("{s}.bc", .{object_name})); + + const ir = b.step(step_name, "Build LLVM ir"); + ir.dependOn(&install_ir.step); + ir.dependOn(&install_bc.step); + b.getInstallStep().dependOn(ir); +} + +// Generate Object File +// TODO: figure out how to get this to emit symbols that are only scoped to linkage (global but hidden). +// @bhansconnect: I believe anything with global scope will still be preserved by the linker even if it +// is never called. I think it could theoretically be called by a dynamic lib that links to the executable +// or something similar. +fn generateObjectFile( + b: *Build, + mode: std.builtin.Mode, + target: CrossTarget, + main_path: LazyPath, + step_name: []const u8, + object_name: []const u8, +) void { + const obj = b.addObject(.{ .name = object_name, .root_source_file = main_path, .optimize = mode, .target = target, .use_llvm = true }); + obj.strip = true; + obj.link_function_sections = true; + obj.force_pic = true; + + const obj_file = obj.getEmittedBin(); + + var suffix = + if (target.os_tag == .windows) + "obj" + else + "o"; + const install = b.addInstallFile(obj_file, b.fmt("{s}.{s}", .{ object_name, suffix })); + + const obj_step = b.step(step_name, "Build object file for linking"); + obj_step.dependOn(&obj.step); + obj_step.dependOn(&install.step); + b.getInstallStep().dependOn(obj_step); +} + +fn makeLinux32Target() CrossTarget { + var target = CrossTarget.parse(.{}) catch unreachable; + + target.cpu_arch = std.Target.Cpu.Arch.x86; + target.os_tag = std.Target.Os.Tag.linux; + target.abi = std.Target.Abi.musl; + + return target; +} + +fn makeLinuxAarch64Target() CrossTarget { + var target = CrossTarget.parse(.{}) catch unreachable; + + target.cpu_arch = std.Target.Cpu.Arch.aarch64; + target.os_tag = std.Target.Os.Tag.linux; + target.abi = std.Target.Abi.musl; + + return target; +} + +fn makeLinuxX64Target() CrossTarget { + var target = CrossTarget.parse(.{}) catch unreachable; + + target.cpu_arch = std.Target.Cpu.Arch.x86_64; + target.os_tag = std.Target.Os.Tag.linux; + target.abi = std.Target.Abi.musl; + + return target; +} + +fn makeWindows64Target() CrossTarget { + var target = CrossTarget.parse(.{}) catch unreachable; + + target.cpu_arch = std.Target.Cpu.Arch.x86_64; + target.os_tag = std.Target.Os.Tag.windows; + target.abi = std.Target.Abi.gnu; + + return target; +} + +fn makeWasm32Target() CrossTarget { + var target = CrossTarget.parse(.{}) catch unreachable; + + // 32-bit wasm + target.cpu_arch = std.Target.Cpu.Arch.wasm32; + target.os_tag = std.Target.Os.Tag.freestanding; + target.abi = std.Target.Abi.none; + + return target; +} diff --git a/crates/compiler/builtins/bitcode/run-tests.sh b/crates/compiler/builtins/bitcode/run-tests.sh new file mode 100755 index 0000000000..0d0aead164 --- /dev/null +++ b/crates/compiler/builtins/bitcode/run-tests.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash + +# https://vaneyckt.io/posts/safer_bash_scripts_with_set_euxo_pipefail/ +set -euxo pipefail + +# Execute zig tests (see build.zig) +zig build test + +# check formatting of zig files +find src/*.zig -type f -print0 | xargs -n 1 -0 zig fmt --check || (echo "zig fmt --check FAILED! Check the previous lines to see which files were improperly formatted." && exit 1) diff --git a/crates/compiler/builtins/bitcode/run-wasm-tests.sh b/crates/compiler/builtins/bitcode/run-wasm-tests.sh new file mode 100755 index 0000000000..f57715186a --- /dev/null +++ b/crates/compiler/builtins/bitcode/run-wasm-tests.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +# https://vaneyckt.io/posts/safer_bash_scripts_with_set_euxo_pipefail/ +set -euxo pipefail + +# For non-native binaries, Zig test needs a "test command" it can use +cargo build --locked --release -p roc_wasm_interp +zig test -target wasm32-wasi-musl -O ReleaseFast src/main.zig --test-cmd ../../../../target/release/roc_wasm_interp --test-cmd-bin diff --git a/crates/compiler/builtins/bitcode/src/compiler_rt.zig b/crates/compiler/builtins/bitcode/src/compiler_rt.zig new file mode 100644 index 0000000000..9a22da5bd4 --- /dev/null +++ b/crates/compiler/builtins/bitcode/src/compiler_rt.zig @@ -0,0 +1,478 @@ +const std = @import("std"); +const builtin = @import("builtin"); +const math = std.math; + +// Eventually, we need to statically ingest compiler-rt and get it working with the surgical linker, then these should not be needed anymore. +// Until then, we are manually ingesting used parts of compiler-rt here. +// +// Taken from +// https://github.com/ziglang/zig/tree/4976b58ab16069f8d3267b69ed030f29685c1abe/lib/compiler_rt/ +// Thank you Zig Contributors! + +// Libcalls that involve u128 on Windows x86-64 are expected by LLVM to use the +// calling convention of @Vector(2, u64), rather than what's standard. +pub const want_windows_v2u64_abi = builtin.os.tag == .windows and builtin.cpu.arch == .x86_64 and @import("builtin").object_format != .c; + +const v2u64 = @Vector(2, u64); + +// Export it as weak incase it is already linked in by something else. +comptime { + if (!want_windows_v2u64_abi) { + @export(__muloti4, .{ .name = "__muloti4", .linkage = .Weak }); + @export(__lshrti3, .{ .name = "__lshrti3", .linkage = .Weak }); + @export(__divti3, .{ .name = "__divti3", .linkage = .Weak }); + @export(__modti3, .{ .name = "__modti3", .linkage = .Weak }); + @export(__umodti3, .{ .name = "__umodti3", .linkage = .Weak }); + @export(__udivti3, .{ .name = "__udivti3", .linkage = .Weak }); + @export(__fixdfti, .{ .name = "__fixdfti", .linkage = .Weak }); + @export(__fixsfti, .{ .name = "__fixsfti", .linkage = .Weak }); + @export(__fixunsdfti, .{ .name = "__fixunsdfti", .linkage = .Weak }); + @export(__fixunssfti, .{ .name = "__fixunssfti", .linkage = .Weak }); + } +} + +pub fn __muloti4(a: i128, b: i128, overflow: *c_int) callconv(.C) i128 { + if (2 * @bitSizeOf(i128) <= @bitSizeOf(usize)) { + return muloXi4_genericFast(i128, a, b, overflow); + } else { + return muloXi4_genericSmall(i128, a, b, overflow); + } +} + +pub fn __divti3(a: i128, b: i128) callconv(.C) i128 { + return div(a, b); +} + +fn __divti3_windows_x86_64(a: v2u64, b: v2u64) callconv(.C) v2u64 { + return @as(v2u64, @bitCast(div(@as(i128, @bitCast(a)), @as(i128, @bitCast(b))))); +} + +inline fn div(a: i128, b: i128) i128 { + const s_a = a >> (128 - 1); + const s_b = b >> (128 - 1); + + const an = (a ^ s_a) -% s_a; + const bn = (b ^ s_b) -% s_b; + + const r = udivmod(u128, @as(u128, @bitCast(an)), @as(u128, @bitCast(bn)), null); + const s = s_a ^ s_b; + return (@as(i128, @bitCast(r)) ^ s) -% s; +} + +pub fn __udivti3(a: u128, b: u128) callconv(.C) u128 { + return udivmod(u128, a, b, null); +} + +fn __udivti3_windows_x86_64(a: v2u64, b: v2u64) callconv(.C) v2u64 { + return @as(v2u64, @bitCast(udivmod(u128, @as(u128, @bitCast(a)), @as(u128, @bitCast(b)), null))); +} + +pub fn __umodti3(a: u128, b: u128) callconv(.C) u128 { + var r: u128 = undefined; + _ = udivmod(u128, a, b, &r); + return r; +} + +fn __umodti3_windows_x86_64(a: v2u64, b: v2u64) callconv(.C) v2u64 { + var r: u128 = undefined; + _ = udivmod(u128, @as(u128, @bitCast(a)), @as(u128, @bitCast(b)), &r); + return @as(v2u64, @bitCast(r)); +} + +pub fn __modti3(a: i128, b: i128) callconv(.C) i128 { + return mod(a, b); +} + +fn __modti3_windows_x86_64(a: v2u64, b: v2u64) callconv(.C) v2u64 { + return @as(v2u64, @bitCast(mod(@as(i128, @bitCast(a)), @as(i128, @bitCast(b))))); +} + +inline fn mod(a: i128, b: i128) i128 { + const s_a = a >> (128 - 1); // s = a < 0 ? -1 : 0 + const s_b = b >> (128 - 1); // s = b < 0 ? -1 : 0 + + const an = (a ^ s_a) -% s_a; // negate if s == -1 + const bn = (b ^ s_b) -% s_b; // negate if s == -1 + + var r: u128 = undefined; + _ = udivmod(u128, @as(u128, @bitCast(an)), @as(u128, @bitCast(bn)), &r); + return (@as(i128, @bitCast(r)) ^ s_a) -% s_a; // negate if s == -1 +} + +pub fn __fixdfti(a: f64) callconv(.C) i128 { + return floatToInt(i128, a); +} + +fn __fixdfti_windows_x86_64(a: f64) callconv(.C) v2u64 { + return @as(v2u64, @bitCast(floatToInt(i128, a))); +} + +pub fn __fixsfti(a: f32) callconv(.C) i128 { + return floatToInt(i128, a); +} + +fn __fixsfti_windows_x86_64(a: f32) callconv(.C) v2u64 { + return @as(v2u64, @bitCast(floatToInt(i128, a))); +} + +pub fn __fixunsdfti(a: f64) callconv(.C) u128 { + return floatToInt(u128, a); +} + +fn __fixunsdfti_windows_x86_64(a: f64) callconv(.C) v2u64 { + return @as(v2u64, @bitCast(floatToInt(u128, a))); +} + +pub fn __fixunssfti(a: f32) callconv(.C) u128 { + return floatToInt(u128, a); +} + +fn __fixunssfti_windows_x86_64(a: f32) callconv(.C) v2u64 { + return @as(v2u64, @bitCast(floatToInt(u128, a))); +} +// mulo - multiplication overflow +// * return a*%b. +// * return if a*b overflows => 1 else => 0 +// - muloXi4_genericSmall as default +// - muloXi4_genericFast for 2*bitsize <= usize + +inline fn muloXi4_genericSmall(comptime ST: type, a: ST, b: ST, overflow: *c_int) ST { + overflow.* = 0; + const min = math.minInt(ST); + var res: ST = a *% b; + // Hacker's Delight section Overflow subsection Multiplication + // case a=-2^{31}, b=-1 problem, because + // on some machines a*b = -2^{31} with overflow + // Then -2^{31}/-1 overflows and any result is possible. + // => check with a<0 and b=-2^{31} + if ((a < 0 and b == min) or (a != 0 and @divTrunc(res, a) != b)) + overflow.* = 1; + return res; +} + +inline fn muloXi4_genericFast(comptime ST: type, a: ST, b: ST, overflow: *c_int) ST { + overflow.* = 0; + const EST = switch (ST) { + i32 => i64, + i64 => i128, + i128 => i256, + else => unreachable, + }; + const min = math.minInt(ST); + const max = math.maxInt(ST); + var res: EST = @as(EST, a) * @as(EST, b); + //invariant: -2^{bitwidth(EST)} < res < 2^{bitwidth(EST)-1} + if (res < min or max < res) + overflow.* = 1; + return @as(ST, @truncate(res)); +} + +const native_endian = builtin.cpu.arch.endian(); +const low = switch (native_endian) { + .Big => 1, + .Little => 0, +}; +const high = 1 - low; + +pub fn udivmod(comptime DoubleInt: type, a: DoubleInt, b: DoubleInt, maybe_rem: ?*DoubleInt) DoubleInt { + // @setRuntimeSafety(builtin.is_test); + + const double_int_bits = @typeInfo(DoubleInt).Int.bits; + const single_int_bits = @divExact(double_int_bits, 2); + const SingleInt = std.meta.Int(.unsigned, single_int_bits); + const SignedDoubleInt = std.meta.Int(.signed, double_int_bits); + const Log2SingleInt = std.math.Log2Int(SingleInt); + + const n = @as([2]SingleInt, @bitCast(a)); + const d = @as([2]SingleInt, @bitCast(b)); + var q: [2]SingleInt = undefined; + var r: [2]SingleInt = undefined; + var sr: c_uint = undefined; + // special cases, X is unknown, K != 0 + if (n[high] == 0) { + if (d[high] == 0) { + // 0 X + // --- + // 0 X + if (maybe_rem) |rem| { + rem.* = n[low] % d[low]; + } + return n[low] / d[low]; + } + // 0 X + // --- + // K X + if (maybe_rem) |rem| { + rem.* = n[low]; + } + return 0; + } + // n[high] != 0 + if (d[low] == 0) { + if (d[high] == 0) { + // K X + // --- + // 0 0 + if (maybe_rem) |rem| { + rem.* = n[high] % d[low]; + } + return n[high] / d[low]; + } + // d[high] != 0 + if (n[low] == 0) { + // K 0 + // --- + // K 0 + if (maybe_rem) |rem| { + r[high] = n[high] % d[high]; + r[low] = 0; + rem.* = @as(DoubleInt, @bitCast(r)); + } + return n[high] / d[high]; + } + // K K + // --- + // K 0 + if ((d[high] & (d[high] - 1)) == 0) { + // d is a power of 2 + if (maybe_rem) |rem| { + r[low] = n[low]; + r[high] = n[high] & (d[high] - 1); + rem.* = @as(DoubleInt, @bitCast(r)); + } + return n[high] >> @as(Log2SingleInt, @intCast(@ctz(d[high]))); + } + // K K + // --- + // K 0 + sr = @as(c_uint, @bitCast(@as(c_int, @clz(d[high])) - @as(c_int, @clz(n[high])))); + // 0 <= sr <= single_int_bits - 2 or sr large + if (sr > single_int_bits - 2) { + if (maybe_rem) |rem| { + rem.* = a; + } + return 0; + } + sr += 1; + // 1 <= sr <= single_int_bits - 1 + // q.all = a << (double_int_bits - sr); + q[low] = 0; + q[high] = n[low] << @as(Log2SingleInt, @intCast(single_int_bits - sr)); + // r.all = a >> sr; + r[high] = n[high] >> @as(Log2SingleInt, @intCast(sr)); + r[low] = (n[high] << @as(Log2SingleInt, @intCast(single_int_bits - sr))) | (n[low] >> @as(Log2SingleInt, @intCast(sr))); + } else { + // d[low] != 0 + if (d[high] == 0) { + // K X + // --- + // 0 K + if ((d[low] & (d[low] - 1)) == 0) { + // d is a power of 2 + if (maybe_rem) |rem| { + rem.* = n[low] & (d[low] - 1); + } + if (d[low] == 1) { + return a; + } + sr = @ctz(d[low]); + q[high] = n[high] >> @as(Log2SingleInt, @intCast(sr)); + q[low] = (n[high] << @as(Log2SingleInt, @intCast(single_int_bits - sr))) | (n[low] >> @as(Log2SingleInt, @intCast(sr))); + return @as(DoubleInt, @bitCast(q)); + } + // K X + // --- + // 0 K + sr = 1 + single_int_bits + @as(c_uint, @clz(d[low])) - @as(c_uint, @clz(n[high])); + // 2 <= sr <= double_int_bits - 1 + // q.all = a << (double_int_bits - sr); + // r.all = a >> sr; + if (sr == single_int_bits) { + q[low] = 0; + q[high] = n[low]; + r[high] = 0; + r[low] = n[high]; + } else if (sr < single_int_bits) { + // 2 <= sr <= single_int_bits - 1 + q[low] = 0; + q[high] = n[low] << @as(Log2SingleInt, @intCast(single_int_bits - sr)); + r[high] = n[high] >> @as(Log2SingleInt, @intCast(sr)); + r[low] = (n[high] << @as(Log2SingleInt, @intCast(single_int_bits - sr))) | (n[low] >> @as(Log2SingleInt, @intCast(sr))); + } else { + // single_int_bits + 1 <= sr <= double_int_bits - 1 + q[low] = n[low] << @as(Log2SingleInt, @intCast(double_int_bits - sr)); + q[high] = (n[high] << @as(Log2SingleInt, @intCast(double_int_bits - sr))) | (n[low] >> @as(Log2SingleInt, @intCast(sr - single_int_bits))); + r[high] = 0; + r[low] = n[high] >> @as(Log2SingleInt, @intCast(sr - single_int_bits)); + } + } else { + // K X + // --- + // K K + sr = @as(c_uint, @bitCast(@as(c_int, @clz(d[high])) - @as(c_int, @clz(n[high])))); + // 0 <= sr <= single_int_bits - 1 or sr large + if (sr > single_int_bits - 1) { + if (maybe_rem) |rem| { + rem.* = a; + } + return 0; + } + sr += 1; + // 1 <= sr <= single_int_bits + // q.all = a << (double_int_bits - sr); + // r.all = a >> sr; + q[low] = 0; + if (sr == single_int_bits) { + q[high] = n[low]; + r[high] = 0; + r[low] = n[high]; + } else { + r[high] = n[high] >> @as(Log2SingleInt, @intCast(sr)); + r[low] = (n[high] << @as(Log2SingleInt, @intCast(single_int_bits - sr))) | (n[low] >> @as(Log2SingleInt, @intCast(sr))); + q[high] = n[low] << @as(Log2SingleInt, @intCast(single_int_bits - sr)); + } + } + } + // Not a special case + // q and r are initialized with: + // q.all = a << (double_int_bits - sr); + // r.all = a >> sr; + // 1 <= sr <= double_int_bits - 1 + var carry: u32 = 0; + var r_all: DoubleInt = undefined; + while (sr > 0) : (sr -= 1) { + // r:q = ((r:q) << 1) | carry + r[high] = (r[high] << 1) | (r[low] >> (single_int_bits - 1)); + r[low] = (r[low] << 1) | (q[high] >> (single_int_bits - 1)); + q[high] = (q[high] << 1) | (q[low] >> (single_int_bits - 1)); + q[low] = (q[low] << 1) | carry; + // carry = 0; + // if (r.all >= b) + // { + // r.all -= b; + // carry = 1; + // } + r_all = @as(DoubleInt, @bitCast(r)); + const s: SignedDoubleInt = @as(SignedDoubleInt, @bitCast(b -% r_all -% 1)) >> (double_int_bits - 1); + carry = @as(u32, @intCast(s & 1)); + r_all -= b & @as(DoubleInt, @bitCast(s)); + r = @as([2]SingleInt, @bitCast(r_all)); + } + const q_all = (@as(DoubleInt, @bitCast(q)) << 1) | carry; + if (maybe_rem) |rem| { + rem.* = r_all; + } + return q_all; +} + +pub inline fn floatToInt(comptime I: type, a: anytype) I { + const Log2Int = math.Log2Int; + const Int = @import("std").meta.Int; + const F = @TypeOf(a); + const float_bits = @typeInfo(F).Float.bits; + const int_bits = @typeInfo(I).Int.bits; + const rep_t = Int(.unsigned, float_bits); + const sig_bits = math.floatMantissaBits(F); + const exp_bits = math.floatExponentBits(F); + const fractional_bits = floatFractionalBits(F); + + // const implicit_bit = if (F != f80) (@as(rep_t, 1) << sig_bits) else 0; + const implicit_bit = @as(rep_t, 1) << sig_bits; + const max_exp = (1 << (exp_bits - 1)); + const exp_bias = max_exp - 1; + const sig_mask = (@as(rep_t, 1) << sig_bits) - 1; + + // Break a into sign, exponent, significand + const a_rep: rep_t = @as(rep_t, @bitCast(a)); + const negative = (a_rep >> (float_bits - 1)) != 0; + const exponent = @as(i32, @intCast((a_rep << 1) >> (sig_bits + 1))) - exp_bias; + const significand: rep_t = (a_rep & sig_mask) | implicit_bit; + + // If the exponent is negative, the result rounds to zero. + if (exponent < 0) return 0; + + // If the value is too large for the integer type, saturate. + switch (@typeInfo(I).Int.signedness) { + .unsigned => { + if (negative) return 0; + if (@as(c_uint, @intCast(exponent)) >= @min(int_bits, max_exp)) return math.maxInt(I); + }, + .signed => if (@as(c_uint, @intCast(exponent)) >= @min(int_bits - 1, max_exp)) { + return if (negative) math.minInt(I) else math.maxInt(I); + }, + } + + // If 0 <= exponent < sig_bits, right shift to get the result. + // Otherwise, shift left. + var result: I = undefined; + if (exponent < fractional_bits) { + result = @as(I, @intCast(significand >> @as(Log2Int(rep_t), @intCast(fractional_bits - exponent)))); + } else { + result = @as(I, @intCast(significand)) << @as(Log2Int(I), @intCast(exponent - fractional_bits)); + } + + if ((@typeInfo(I).Int.signedness == .signed) and negative) + return ~result +% 1; + return result; +} + +/// Returns the number of fractional bits in the mantissa of floating point type T. +pub inline fn floatFractionalBits(comptime T: type) comptime_int { + comptime std.debug.assert(@typeInfo(T) == .Float); + + // standard IEEE floats have an implicit 0.m or 1.m integer part + // f80 is special and has an explicitly stored bit in the MSB + // this function corresponds to `MANT_DIG - 1' from C + return switch (@typeInfo(T).Float.bits) { + 16 => 10, + 32 => 23, + 64 => 52, + 80 => 63, + 128 => 112, + else => @compileError("unknown floating point type " ++ @typeName(T)), + }; +} + +pub fn __lshrti3(a: i128, b: i32) callconv(.C) i128 { + return lshrXi3(i128, a, b); +} + +// Logical shift right: shift in 0 from left to right +// Precondition: 0 <= b < T.bit_count +inline fn lshrXi3(comptime T: type, a: T, b: i32) T { + const word_t = HalveInt(T, false); + const S = std.math.Log2Int(word_t.HalfT); + + const input = word_t{ .all = a }; + var output: word_t = undefined; + + if (b >= word_t.bits) { + output.s.high = 0; + output.s.low = input.s.high >> @as(S, @intCast(b - word_t.bits)); + } else if (b == 0) { + return a; + } else { + output.s.high = input.s.high >> @as(S, @intCast(b)); + output.s.low = input.s.high << @as(S, @intCast(word_t.bits - b)); + output.s.low |= input.s.low >> @as(S, @intCast(b)); + } + + return output.all; +} + +/// Allows to access underlying bits as two equally sized lower and higher +/// signed or unsigned integers. +fn HalveInt(comptime T: type, comptime signed_half: bool) type { + return extern union { + pub const bits = @divExact(@typeInfo(T).Int.bits, 2); + pub const HalfTU = std.meta.Int(.unsigned, bits); + pub const HalfTS = std.meta.Int(.signed, bits); + pub const HalfT = if (signed_half) HalfTS else HalfTU; + + all: T, + s: if (native_endian == .Little) + extern struct { low: HalfT, high: HalfT } + else + extern struct { high: HalfT, low: HalfT }, + }; +} diff --git a/crates/compiler/builtins/bitcode/src/dec.zig b/crates/compiler/builtins/bitcode/src/dec.zig new file mode 100644 index 0000000000..d60057843c --- /dev/null +++ b/crates/compiler/builtins/bitcode/src/dec.zig @@ -0,0 +1,1343 @@ +const std = @import("std"); +const str = @import("str.zig"); +const num_ = @import("num.zig"); +const utils = @import("utils.zig"); + +const math = std.math; +const RocStr = str.RocStr; +const WithOverflow = utils.WithOverflow; +const roc_panic = @import("panic.zig").panic_help; +const U256 = num_.U256; +const mul_u128 = num_.mul_u128; + +pub const RocDec = extern struct { + num: i128, + + pub const decimal_places: u5 = 18; + pub const whole_number_places: u5 = 21; + const max_digits: u6 = 39; + const max_str_length: u6 = max_digits + 2; // + 2 here to account for the sign & decimal dot + + pub const min: RocDec = .{ .num = math.minInt(i128) }; + pub const max: RocDec = .{ .num = math.maxInt(i128) }; + + pub const one_point_zero_i128: i128 = math.pow(i128, 10, RocDec.decimal_places); + pub const one_point_zero: RocDec = .{ .num = one_point_zero_i128 }; + + pub fn fromU64(num: u64) RocDec { + return .{ .num = num * one_point_zero_i128 }; + } + + pub fn fromF64(num: f64) ?RocDec { + var result: f64 = num * comptime @as(f64, @floatFromInt(one_point_zero_i128)); + + if (result > comptime @as(f64, @floatFromInt(math.maxInt(i128)))) { + return null; + } + + if (result < comptime @as(f64, @floatFromInt(math.minInt(i128)))) { + return null; + } + + var ret: RocDec = .{ .num = @as(i128, @intFromFloat(result)) }; + return ret; + } + + pub fn toF64(dec: RocDec) f64 { + return @as(f64, @floatFromInt(dec.num)) / comptime @as(f64, @floatFromInt(one_point_zero_i128)); + } + + // TODO: If Str.toDec eventually supports more error types, return errors here. + // For now, just return null which will give the default error. + pub fn fromStr(roc_str: RocStr) ?RocDec { + if (roc_str.isEmpty()) { + return null; + } + + const length = roc_str.len(); + + const roc_str_slice = roc_str.asSlice(); + + var is_negative: bool = roc_str_slice[0] == '-'; + var initial_index: usize = if (is_negative) 1 else 0; + + var point_index: ?usize = null; + var index: usize = initial_index; + while (index < length) { + var byte: u8 = roc_str_slice[index]; + if (byte == '.' and point_index == null) { + point_index = index; + index += 1; + continue; + } + + if (!isDigit(byte)) { + return null; + } + index += 1; + } + + var before_str_length = length; + var after_val_i128: ?i128 = null; + if (point_index) |pi| { + before_str_length = pi; + + var after_str_len = (length - 1) - pi; + if (after_str_len > decimal_places) { + // TODO: runtime exception for too many decimal places! + return null; + } + var diff_decimal_places = decimal_places - after_str_len; + + var after_str = roc_str_slice[pi + 1 .. length]; + var after_u64 = std.fmt.parseUnsigned(u64, after_str, 10) catch null; + after_val_i128 = if (after_u64) |f| @as(i128, @intCast(f)) * math.pow(i128, 10, diff_decimal_places) else null; + } + + var before_str = roc_str_slice[initial_index..before_str_length]; + var before_val_not_adjusted = std.fmt.parseUnsigned(i128, before_str, 10) catch null; + + var before_val_i128: ?i128 = null; + if (before_val_not_adjusted) |before| { + const answer = @mulWithOverflow(before, one_point_zero_i128); + const result = answer[0]; + const overflowed = answer[1]; + if (overflowed == 1) { + // TODO: runtime exception for overflow! + return null; + } + before_val_i128 = result; + } + + const dec: RocDec = blk: { + if (before_val_i128) |before| { + if (after_val_i128) |after| { + var answer = @addWithOverflow(before, after); + const result = answer[0]; + const overflowed = answer[1]; + if (overflowed == 1) { + // TODO: runtime exception for overflow! + return null; + } + break :blk .{ .num = result }; + } else { + break :blk .{ .num = before }; + } + } else if (after_val_i128) |after| { + break :blk .{ .num = after }; + } else { + return null; + } + }; + + if (is_negative) { + return dec.negate(); + } else { + return dec; + } + } + + inline fn isDigit(c: u8) bool { + return (c -% 48) <= 9; + } + + pub fn toStr(self: RocDec) RocStr { + // Special case + if (self.num == 0) { + return RocStr.init("0.0", 3); + } + + const num = self.num; + const is_negative = num < 0; + + // Format the backing i128 into an array of digit (ascii) characters (u8s) + var digit_bytes_storage: [max_digits + 1]u8 = undefined; + var num_digits = std.fmt.formatIntBuf(digit_bytes_storage[0..], num, 10, .lower, .{}); + var digit_bytes: [*]u8 = digit_bytes_storage[0..]; + + // space where we assemble all the characters that make up the final string + var str_bytes: [max_str_length]u8 = undefined; + var position: usize = 0; + + // if negative, the first character is a negating minus + if (is_negative) { + str_bytes[position] = '-'; + position += 1; + + // but also, we have one fewer digit than we have characters + num_digits -= 1; + + // and we drop the minus to make later arithmetic correct + digit_bytes += 1; + } + + // Get the slice for before the decimal point + var before_digits_offset: usize = 0; + if (num_digits > decimal_places) { + // we have more digits than fit after the decimal point, + // so we must have digits before the decimal point + before_digits_offset = num_digits - decimal_places; + + for (digit_bytes[0..before_digits_offset]) |c| { + str_bytes[position] = c; + position += 1; + } + } else { + // otherwise there are no actual digits before the decimal point + // but we format it with a '0' + str_bytes[position] = '0'; + position += 1; + } + + // we've done everything before the decimal point, so now we can put the decimal point in + str_bytes[position] = '.'; + position += 1; + + const trailing_zeros: u6 = count_trailing_zeros_base10(num); + if (trailing_zeros >= decimal_places) { + // add just a single zero if all decimal digits are zero + str_bytes[position] = '0'; + position += 1; + } else { + // Figure out if we need to prepend any zeros to the after decimal point + // For example, for the number 0.000123 we need to prepend 3 zeros after the decimal point + const after_zeros_num = if (num_digits < decimal_places) decimal_places - num_digits else 0; + + var i: usize = 0; + while (i < after_zeros_num) : (i += 1) { + str_bytes[position] = '0'; + position += 1; + } + + // otherwise append the decimal digits except the trailing zeros + for (digit_bytes[before_digits_offset .. num_digits - trailing_zeros]) |c| { + str_bytes[position] = c; + position += 1; + } + } + + return RocStr.init(&str_bytes, position); + } + + pub fn toI128(self: RocDec) i128 { + return self.num; + } + + pub fn eq(self: RocDec, other: RocDec) bool { + return self.num == other.num; + } + + pub fn neq(self: RocDec, other: RocDec) bool { + return self.num != other.num; + } + + pub fn negate(self: RocDec) ?RocDec { + var negated = math.negate(self.num) catch null; + return if (negated) |n| .{ .num = n } else null; + } + + pub fn abs(self: RocDec) !RocDec { + const absolute = try math.absInt(self.num); + return RocDec{ .num = absolute }; + } + + pub fn addWithOverflow(self: RocDec, other: RocDec) WithOverflow(RocDec) { + const answer = @addWithOverflow(self.num, other.num); + + return .{ .value = RocDec{ .num = answer[0] }, .has_overflowed = answer[1] == 1 }; + } + + pub fn add(self: RocDec, other: RocDec) RocDec { + const answer = RocDec.addWithOverflow(self, other); + + if (answer.has_overflowed) { + roc_panic("Decimal addition overflowed!", 0); + unreachable; + } else { + return answer.value; + } + } + + pub fn addSaturated(self: RocDec, other: RocDec) RocDec { + const answer = RocDec.addWithOverflow(self, other); + if (answer.has_overflowed) { + // We can unambiguously tell which way it wrapped, because we have 129 bits including the overflow bit + if (answer.value.num < 0) { + return RocDec.max; + } else { + return RocDec.min; + } + } else { + return answer.value; + } + } + + pub fn subWithOverflow(self: RocDec, other: RocDec) WithOverflow(RocDec) { + const answer = @subWithOverflow(self.num, other.num); + + return .{ .value = RocDec{ .num = answer[0] }, .has_overflowed = answer[1] == 1 }; + } + + pub fn sub(self: RocDec, other: RocDec) RocDec { + const answer = RocDec.subWithOverflow(self, other); + + if (answer.has_overflowed) { + roc_panic("Decimal subtraction overflowed!", 0); + unreachable; + } else { + return answer.value; + } + } + + pub fn subSaturated(self: RocDec, other: RocDec) RocDec { + const answer = RocDec.subWithOverflow(self, other); + if (answer.has_overflowed) { + if (answer.value.num < 0) { + return RocDec.max; + } else { + return RocDec.min; + } + } else { + return answer.value; + } + } + + pub fn mulWithOverflow(self: RocDec, other: RocDec) WithOverflow(RocDec) { + const self_i128 = self.num; + const other_i128 = other.num; + // const answer = 0; //self_i256 * other_i256; + + const is_answer_negative = (self_i128 < 0) != (other_i128 < 0); + + const self_u128 = @as(u128, @intCast(math.absInt(self_i128) catch { + if (other_i128 == 0) { + return .{ .value = RocDec{ .num = 0 }, .has_overflowed = false }; + } else if (other_i128 == RocDec.one_point_zero.num) { + return .{ .value = self, .has_overflowed = false }; + } else if (is_answer_negative) { + return .{ .value = RocDec.min, .has_overflowed = true }; + } else { + return .{ .value = RocDec.max, .has_overflowed = true }; + } + })); + + const other_u128 = @as(u128, @intCast(math.absInt(other_i128) catch { + if (self_i128 == 0) { + return .{ .value = RocDec{ .num = 0 }, .has_overflowed = false }; + } else if (self_i128 == RocDec.one_point_zero.num) { + return .{ .value = other, .has_overflowed = false }; + } else if (is_answer_negative) { + return .{ .value = RocDec.min, .has_overflowed = true }; + } else { + return .{ .value = RocDec.max, .has_overflowed = true }; + } + })); + + const unsigned_answer: i128 = mul_and_decimalize(self_u128, other_u128); + + if (is_answer_negative) { + return .{ .value = RocDec{ .num = -unsigned_answer }, .has_overflowed = false }; + } else { + return .{ .value = RocDec{ .num = unsigned_answer }, .has_overflowed = false }; + } + } + + pub fn mul(self: RocDec, other: RocDec) RocDec { + const answer = RocDec.mulWithOverflow(self, other); + + if (answer.has_overflowed) { + roc_panic("Decimal multiplication overflowed!", 0); + unreachable; + } else { + return answer.value; + } + } + + pub fn mulSaturated(self: RocDec, other: RocDec) RocDec { + const answer = RocDec.mulWithOverflow(self, other); + return answer.value; + } + + pub fn div(self: RocDec, other: RocDec) RocDec { + const numerator_i128 = self.num; + const denominator_i128 = other.num; + + // (0 / n) is always 0 + if (numerator_i128 == 0) { + return RocDec{ .num = 0 }; + } + + // (n / 0) is an error + if (denominator_i128 == 0) { + @panic("TODO runtime exception for dividing by 0!"); + } + + // If they're both negative, or if neither is negative, the final answer + // is positive or zero. If one is negative and the denominator isn't, the + // final answer is negative (or zero, in which case final sign won't matter). + // + // It's important that we do this in terms of negatives, because doing + // it in terms of positives can cause bugs when one is zero. + const is_answer_negative = (numerator_i128 < 0) != (denominator_i128 < 0); + + // Break the two i128s into two { hi: u64, lo: u64 } tuples, discarding + // the sign for now. + // + // We'll multiply all 4 combinations of these (hi1 x lo1, hi2 x lo2, + // hi1 x lo2, hi2 x lo1) and add them as appropriate, then apply the + // appropriate sign at the very end. + // + // We do checked_abs because if we had -i128::MAX before, this will overflow. + + const numerator_abs_i128 = math.absInt(numerator_i128) catch { + // Currently, if you try to do multiplication on i64::MIN, panic + // unless you're specifically multiplying by 0 or 1. + // + // Maybe we could support more cases in the future + if (denominator_i128 == one_point_zero_i128) { + return self; + } else { + @panic("TODO runtime exception for overflow when dividing!"); + } + }; + const numerator_u128 = @as(u128, @intCast(numerator_abs_i128)); + + const denominator_abs_i128 = math.absInt(denominator_i128) catch { + // Currently, if you try to do multiplication on i64::MIN, panic + // unless you're specifically multiplying by 0 or 1. + // + // Maybe we could support more cases in the future + if (numerator_i128 == one_point_zero_i128) { + return other; + } else { + @panic("TODO runtime exception for overflow when dividing!"); + } + }; + const denominator_u128 = @as(u128, @intCast(denominator_abs_i128)); + + const numerator_u256: U256 = mul_u128(numerator_u128, math.pow(u128, 10, decimal_places)); + const answer = div_u256_by_u128(numerator_u256, denominator_u128); + + var unsigned_answer: i128 = undefined; + if (answer.hi == 0 and answer.lo <= math.maxInt(i128)) { + unsigned_answer = @as(i128, @intCast(answer.lo)); + } else { + @panic("TODO runtime exception for overflow when dividing!"); + } + + return RocDec{ .num = if (is_answer_negative) -unsigned_answer else unsigned_answer }; + } + + fn mod2pi(self: RocDec) RocDec { + // This is made to be used before calling trig functions that work on the range 0 to 2*pi. + // It should be reasonable fast (much faster than calling @mod) and much more accurate as well. + // b is 2*pi as a dec. which is 6.2831853071795864769252867665590057684 + // as dec is times 10^18 so 6283185307179586476.9252867665590057684 + const b0: u64 = 6283185307179586476; + // Fraction that reprensents 64 bits of precision past what dec normally supports. + // 0.9252867665590057684 as binary to 64 places. + const b1: u64 = 0b1110110011011111100101111111000111001010111000100101011111110111; + + // This is dec/(b0+1), but as a multiplication. + // So dec * (1/(b0+1)). This is way faster. + const dec = self.num; + const tmp = @as(i128, @intCast(num_.mul_u128(math.absCast(dec), 249757942369376157886101012127821356963).hi >> (190 - 128))); + const q0 = if (dec < 0) -tmp else tmp; + + const upper = q0 * b0; + const answer = @mulWithOverflow(q0, b1); + const lower = answer[0]; + const overflowed = answer[1]; + // TODO: maybe write this out branchlessly. + // Currently is is probably cmovs, but could be just math? + const q0_sign: i128 = + if (q0 > 0) 1 else -1; + const overflowed_val: i128 = if (overflowed == 1) q0_sign << 64 else 0; + const full = upper + @as(i128, @intCast(lower >> 64)) + overflowed_val; + + var out = dec - full; + if (out < 0) { + out += b0; + } + + return RocDec{ .num = out }; + } + + pub fn log(self: RocDec) RocDec { + return fromF64(@log(self.toF64())).?; + } + + // I belive the output of the trig functions is always in range of Dec. + // If not, we probably should just make it saturate the Dec. + // I don't think this should crash or return errors. + pub fn sin(self: RocDec) RocDec { + return fromF64(math.sin(self.mod2pi().toF64())).?; + } + + pub fn cos(self: RocDec) RocDec { + return fromF64(math.cos(self.mod2pi().toF64())).?; + } + + pub fn tan(self: RocDec) RocDec { + return fromF64(math.tan(self.mod2pi().toF64())).?; + } + + pub fn asin(self: RocDec) RocDec { + return fromF64(math.asin(self.toF64())).?; + } + + pub fn acos(self: RocDec) RocDec { + return fromF64(math.acos(self.toF64())).?; + } + + pub fn atan(self: RocDec) RocDec { + return fromF64(math.atan(self.toF64())).?; + } +}; + +// A number has `k` trailling zeros if `10^k` divides into it cleanly +inline fn count_trailing_zeros_base10(input: i128) u6 { + if (input == 0) { + // this should not happen in practice + return 0; + } + + var count: u6 = 0; + var k: i128 = 1; + + while (true) { + if (@mod(input, std.math.pow(i128, 10, k)) == 0) { + count += 1; + k += 1; + } else { + break; + } + } + + return count; +} + +fn mul_and_decimalize(a: u128, b: u128) i128 { + const answer_u256 = mul_u128(a, b); + + var lhs_hi = answer_u256.hi; + var lhs_lo = answer_u256.lo; + + // Divide - or just add 1, multiply by floor(2^315/10^18), then right shift 315 times. + // floor(2^315/10^18) is 66749594872528440074844428317798503581334516323645399060845050244444366430645 + + // Add 1. + // This can't overflow because the initial numbers are only 127bit due to removing the sign bit. + var answer = @addWithOverflow(lhs_lo, 1); + lhs_lo = answer[0]; + var overflowed = answer[1]; + lhs_hi = blk: { + if (overflowed == 1) { + break :blk lhs_hi + 1; + } else { + break :blk lhs_hi + 0; + } + }; + + // This needs to do multiplication in a way that expands, + // since we throw away 315 bits we care only about the higher end, not lower. + // So like need to do high low mult with 2 U256's and then bitshift. + // I bet this has a lot of room for multiplication optimization. + const rhs_hi: u128 = 0x9392ee8e921d5d073aff322e62439fcf; + const rhs_lo: u128 = 0x32d7f344649470f90cac0c573bf9e1b5; + + const ea = mul_u128(lhs_lo, rhs_lo); + const gf = mul_u128(lhs_hi, rhs_lo); + const jh = mul_u128(lhs_lo, rhs_hi); + const lk = mul_u128(lhs_hi, rhs_hi); + + const e = ea.hi; + // const _a = ea.lo; + + const g = gf.hi; + const f = gf.lo; + + const j = jh.hi; + const h = jh.lo; + + const l = lk.hi; + const k = lk.lo; + + // b = e + f + h + answer = @addWithOverflow(e, f); + const e_plus_f = answer[0]; + overflowed = answer[1]; + var b_carry1: u128 = undefined; + if (overflowed == 1) { + b_carry1 = 1; + } else { + b_carry1 = 0; + } + + answer = @addWithOverflow(e_plus_f, h); + overflowed = answer[1]; + var b_carry2: u128 = undefined; + if (overflowed == 1) { + b_carry2 = 1; + } else { + b_carry2 = 0; + } + + // c = carry + g + j + k // it doesn't say +k but I think it should be? + answer = @addWithOverflow(g, j); + const g_plus_j = answer[0]; + overflowed = answer[1]; + var c_carry1: u128 = undefined; + if (overflowed == 1) { + c_carry1 = 1; + } else { + c_carry1 = 0; + } + + answer = @addWithOverflow(g_plus_j, k); + const g_plus_j_plus_k = answer[0]; + overflowed = answer[1]; + var c_carry2: u128 = undefined; + if (overflowed == 1) { + c_carry2 = 1; + } else { + c_carry2 = 0; + } + + answer = @addWithOverflow(g_plus_j_plus_k, b_carry1); + const c_without_bcarry2 = answer[0]; + overflowed = answer[1]; + var c_carry3: u128 = undefined; + if (overflowed == 1) { + c_carry3 = 1; + } else { + c_carry3 = 0; + } + + answer = @addWithOverflow(c_without_bcarry2, b_carry2); + const c = answer[0]; + overflowed = answer[1]; + var c_carry4: u128 = undefined; + if (overflowed == 1) { + c_carry4 = 1; + } else { + c_carry4 = 0; + } + + // d = carry + l + answer = @addWithOverflow(l, c_carry1); + overflowed = answer[1]; + answer = @addWithOverflow(answer[0], c_carry2); + overflowed = overflowed | answer[1]; + answer = @addWithOverflow(answer[0], c_carry3); + overflowed = overflowed | answer[1]; + answer = @addWithOverflow(answer[0], c_carry4); + overflowed = overflowed | answer[1]; + const d = answer[0]; + + if (overflowed == 1) { + @panic("TODO runtime exception for overflow!"); + } + + // Final 512bit value is d, c, b, a + // need to left shift 321 times + // 315 - 256 is 59. So left shift d, c 59 times. + return @as(i128, @intCast(c >> 59 | (d << (128 - 59)))); +} + +// Multiply two 128-bit ints and divide the result by 10^DECIMAL_PLACES +// +// Adapted from https://github.com/nlordell/ethnum-rs/blob/c9ed57e131bffde7bcc8274f376e5becf62ef9ac/src/intrinsics/native/divmod.rs +// Copyright (c) 2020 Nicholas Rodrigues Lordello +// Licensed under the Apache License version 2.0 +// +// When translating this to Zig, we often have to use math.shr/shl instead of >>/<< +// This is because casting to the right types for Zig can be kind of tricky. +// See https://github.com/ziglang/zig/issues/7605 +fn div_u256_by_u128(numer: U256, denom: u128) U256 { + const N_UDWORD_BITS: u8 = 128; + const N_UTWORD_BITS: u9 = 256; + + var q: U256 = undefined; + var r: U256 = undefined; + var sr: u8 = undefined; + + // special case + if (numer.hi == 0) { + // 0 X + // --- + // 0 X + return .{ + .hi = 0, + .lo = numer.lo / denom, + }; + } + + // numer.hi != 0 + if (denom == 0) { + // K X + // --- + // 0 0 + return .{ + .hi = 0, + .lo = numer.hi / denom, + }; + } else { + // K X + // --- + // 0 K + // NOTE: Modified from `if (d.low() & (d.low() - 1)) == 0`. + if (math.isPowerOfTwo(denom)) { + // if d is a power of 2 + if (denom == 1) { + return numer; + } + + sr = @ctz(denom); + + return .{ + .hi = math.shr(u128, numer.hi, sr), + .lo = math.shl(u128, numer.hi, N_UDWORD_BITS - sr) | math.shr(u128, numer.lo, sr), + }; + } + + // K X + // --- + // 0 K + var denom_leading_zeros = @clz(denom); + var numer_hi_leading_zeros = @clz(numer.hi); + sr = 1 + N_UDWORD_BITS + denom_leading_zeros - numer_hi_leading_zeros; + // 2 <= sr <= N_UTWORD_BITS - 1 + // q.all = n.all << (N_UTWORD_BITS - sr); + // r.all = n.all >> sr; + // #[allow(clippy::comparison_chain)] + if (sr == N_UDWORD_BITS) { + q = .{ + .hi = numer.lo, + .lo = 0, + }; + r = .{ + .hi = 0, + .lo = numer.hi, + }; + } else if (sr < N_UDWORD_BITS) { + // 2 <= sr <= N_UDWORD_BITS - 1 + q = .{ + .hi = math.shl(u128, numer.lo, N_UDWORD_BITS - sr), + .lo = 0, + }; + r = .{ + .hi = math.shr(u128, numer.hi, sr), + .lo = math.shl(u128, numer.hi, N_UDWORD_BITS - sr) | math.shr(u128, numer.lo, sr), + }; + } else { + // N_UDWORD_BITS + 1 <= sr <= N_UTWORD_BITS - 1 + q = .{ + .hi = math.shl(u128, numer.hi, N_UTWORD_BITS - sr) | math.shr(u128, numer.lo, sr - N_UDWORD_BITS), + .lo = math.shl(u128, numer.lo, N_UTWORD_BITS - sr), + }; + r = .{ + .hi = 0, + .lo = math.shr(u128, numer.hi, sr - N_UDWORD_BITS), + }; + } + } + + // Not a special case + // q and r are initialized with: + // q.all = n.all << (N_UTWORD_BITS - sr); + // r.all = n.all >> sr; + // 1 <= sr <= N_UTWORD_BITS - 1 + var carry: u128 = 0; + + while (sr > 0) { + // r:q = ((r:q) << 1) | carry + r.hi = (r.hi << 1) | (r.lo >> (N_UDWORD_BITS - 1)); + r.lo = (r.lo << 1) | (q.hi >> (N_UDWORD_BITS - 1)); + q.hi = (q.hi << 1) | (q.lo >> (N_UDWORD_BITS - 1)); + q.lo = (q.lo << 1) | carry; + + // carry = 0; + // if (r.all >= d.all) + // { + // r.all -= d.all; + // carry = 1; + // } + // NOTE: Modified from `(d - r - 1) >> (N_UTWORD_BITS - 1)` to be an + // **arithmetic** shift. + + var answer = @subWithOverflow(denom, r.lo); + var lo = answer[0]; + var lo_overflowed = answer[1]; + var hi = 0 -% @as(u128, @intCast(@as(u1, @bitCast(lo_overflowed)))) -% r.hi; + + answer = @subWithOverflow(lo, 1); + lo = answer[0]; + lo_overflowed = answer[1]; + hi = hi -% @as(u128, @intCast(@as(u1, @bitCast(lo_overflowed)))); + + // NOTE: this U256 was originally created by: + // + // ((hi as i128) >> 127).as_u256() + // + // As an implementation of `as_u256`, we wrap a negative value around to the maximum value of U256. + + var s_u128 = math.shr(u128, hi, 127); + var s_hi: u128 = undefined; + var s_lo: u128 = undefined; + if (s_u128 == 1) { + s_hi = math.maxInt(u128); + s_lo = math.maxInt(u128); + } else { + s_hi = 0; + s_lo = 0; + } + var s = .{ + .hi = s_hi, + .lo = s_lo, + }; + + carry = s.lo & 1; + + // var (lo, carry) = r.lo.overflowing_sub(denom & s.lo); + answer = @subWithOverflow(r.lo, (denom & s.lo)); + lo = answer[0]; + lo_overflowed = answer[1]; + hi = r.hi -% @as(u128, @intCast(@as(u1, @bitCast(lo_overflowed)))); + + r = .{ .hi = hi, .lo = lo }; + + sr -= 1; + } + + var hi = (q.hi << 1) | (q.lo >> (127)); + var lo = (q.lo << 1) | carry; + + return .{ .hi = hi, .lo = lo }; +} + +const testing = std.testing; +const expectEqual = testing.expectEqual; +const expectError = testing.expectError; +const expectEqualSlices = testing.expectEqualSlices; +const expect = testing.expect; + +test "fromU64" { + var dec = RocDec.fromU64(25); + + try expectEqual(RocDec{ .num = 25000000000000000000 }, dec); +} + +test "fromF64" { + var dec = RocDec.fromF64(25.5); + try expectEqual(RocDec{ .num = 25500000000000000000 }, dec.?); +} + +test "fromF64 overflow" { + var dec = RocDec.fromF64(1e308); + try expectEqual(dec, null); +} + +test "fromStr: empty" { + var roc_str = RocStr.init("", 0); + var dec = RocDec.fromStr(roc_str); + + try expectEqual(dec, null); +} + +test "fromStr: 0" { + var roc_str = RocStr.init("0", 1); + var dec = RocDec.fromStr(roc_str); + + try expectEqual(RocDec{ .num = 0 }, dec.?); +} + +test "fromStr: 1" { + var roc_str = RocStr.init("1", 1); + var dec = RocDec.fromStr(roc_str); + + try expectEqual(RocDec.one_point_zero, dec.?); +} + +test "fromStr: 123.45" { + var roc_str = RocStr.init("123.45", 6); + var dec = RocDec.fromStr(roc_str); + + try expectEqual(RocDec{ .num = 123450000000000000000 }, dec.?); +} + +test "fromStr: .45" { + var roc_str = RocStr.init(".45", 3); + var dec = RocDec.fromStr(roc_str); + + try expectEqual(RocDec{ .num = 450000000000000000 }, dec.?); +} + +test "fromStr: 0.45" { + var roc_str = RocStr.init("0.45", 4); + var dec = RocDec.fromStr(roc_str); + + try expectEqual(RocDec{ .num = 450000000000000000 }, dec.?); +} + +test "fromStr: 123" { + var roc_str = RocStr.init("123", 3); + var dec = RocDec.fromStr(roc_str); + + try expectEqual(RocDec{ .num = 123000000000000000000 }, dec.?); +} + +test "fromStr: -.45" { + var roc_str = RocStr.init("-.45", 4); + var dec = RocDec.fromStr(roc_str); + + try expectEqual(RocDec{ .num = -450000000000000000 }, dec.?); +} + +test "fromStr: -0.45" { + var roc_str = RocStr.init("-0.45", 5); + var dec = RocDec.fromStr(roc_str); + + try expectEqual(RocDec{ .num = -450000000000000000 }, dec.?); +} + +test "fromStr: -123" { + var roc_str = RocStr.init("-123", 4); + var dec = RocDec.fromStr(roc_str); + + try expectEqual(RocDec{ .num = -123000000000000000000 }, dec.?); +} + +test "fromStr: -123.45" { + var roc_str = RocStr.init("-123.45", 7); + var dec = RocDec.fromStr(roc_str); + + try expectEqual(RocDec{ .num = -123450000000000000000 }, dec.?); +} + +test "fromStr: abc" { + var roc_str = RocStr.init("abc", 3); + var dec = RocDec.fromStr(roc_str); + + try expectEqual(dec, null); +} + +test "fromStr: 123.abc" { + var roc_str = RocStr.init("123.abc", 7); + var dec = RocDec.fromStr(roc_str); + + try expectEqual(dec, null); +} + +test "fromStr: abc.123" { + var roc_str = RocStr.init("abc.123", 7); + var dec = RocDec.fromStr(roc_str); + + try expectEqual(dec, null); +} + +test "fromStr: .123.1" { + var roc_str = RocStr.init(".123.1", 6); + var dec = RocDec.fromStr(roc_str); + + try expectEqual(dec, null); +} + +test "toStr: 100.00" { + var dec: RocDec = .{ .num = 100000000000000000000 }; + var res_roc_str = dec.toStr(); + + const res_slice: []const u8 = "100.0"[0..]; + try expectEqualSlices(u8, res_slice, res_roc_str.asSlice()); +} + +test "toStr: 123.45" { + var dec: RocDec = .{ .num = 123450000000000000000 }; + var res_roc_str = dec.toStr(); + + const res_slice: []const u8 = "123.45"[0..]; + try expectEqualSlices(u8, res_slice, res_roc_str.asSlice()); +} + +test "toStr: -123.45" { + var dec: RocDec = .{ .num = -123450000000000000000 }; + var res_roc_str = dec.toStr(); + + const res_slice: []const u8 = "-123.45"[0..]; + try expectEqualSlices(u8, res_slice, res_roc_str.asSlice()); +} + +test "toStr: 123.0" { + var dec: RocDec = .{ .num = 123000000000000000000 }; + var res_roc_str = dec.toStr(); + + const res_slice: []const u8 = "123.0"[0..]; + try expectEqualSlices(u8, res_slice, res_roc_str.asSlice()); +} + +test "toStr: -123.0" { + var dec: RocDec = .{ .num = -123000000000000000000 }; + var res_roc_str = dec.toStr(); + + const res_slice: []const u8 = "-123.0"[0..]; + try expectEqualSlices(u8, res_slice, res_roc_str.asSlice()); +} + +test "toStr: 0.45" { + var dec: RocDec = .{ .num = 450000000000000000 }; + var res_roc_str = dec.toStr(); + + const res_slice: []const u8 = "0.45"[0..]; + try expectEqualSlices(u8, res_slice, res_roc_str.asSlice()); +} + +test "toStr: -0.45" { + var dec: RocDec = .{ .num = -450000000000000000 }; + var res_roc_str = dec.toStr(); + + const res_slice: []const u8 = "-0.45"[0..]; + try expectEqualSlices(u8, res_slice, res_roc_str.asSlice()); +} + +test "toStr: 0.00045" { + var dec: RocDec = .{ .num = 450000000000000 }; + var res_roc_str = dec.toStr(); + + const res_slice: []const u8 = "0.00045"[0..]; + try expectEqualSlices(u8, res_slice, res_roc_str.asSlice()); +} + +test "toStr: -0.00045" { + var dec: RocDec = .{ .num = -450000000000000 }; + var res_roc_str = dec.toStr(); + + const res_slice: []const u8 = "-0.00045"[0..]; + try expectEqualSlices(u8, res_slice, res_roc_str.asSlice()); +} + +test "toStr: -111.123456" { + var dec: RocDec = .{ .num = -111123456000000000000 }; + var res_roc_str = dec.toStr(); + + const res_slice: []const u8 = "-111.123456"[0..]; + try expectEqualSlices(u8, res_slice, res_roc_str.asSlice()); +} + +test "toStr: 123.1111111" { + var dec: RocDec = .{ .num = 123111111100000000000 }; + var res_roc_str = dec.toStr(); + + const res_slice: []const u8 = "123.1111111"[0..]; + try expectEqualSlices(u8, res_slice, res_roc_str.asSlice()); +} + +test "toStr: 123.1111111111111 (big str)" { + var dec: RocDec = .{ .num = 123111111111111000000 }; + var res_roc_str = dec.toStr(); + errdefer res_roc_str.decref(); + defer res_roc_str.decref(); + + const res_slice: []const u8 = "123.111111111111"[0..]; + try expectEqualSlices(u8, res_slice, res_roc_str.asSlice()); +} + +test "toStr: 123.111111111111444444 (max number of decimal places)" { + var dec: RocDec = .{ .num = 123111111111111444444 }; + var res_roc_str = dec.toStr(); + errdefer res_roc_str.decref(); + defer res_roc_str.decref(); + + const res_slice: []const u8 = "123.111111111111444444"[0..]; + try expectEqualSlices(u8, res_slice, res_roc_str.asSlice()); +} + +test "toStr: 12345678912345678912.111111111111111111 (max number of digits)" { + var dec: RocDec = .{ .num = 12345678912345678912111111111111111111 }; + var res_roc_str = dec.toStr(); + errdefer res_roc_str.decref(); + defer res_roc_str.decref(); + + const res_slice: []const u8 = "12345678912345678912.111111111111111111"[0..]; + try expectEqualSlices(u8, res_slice, res_roc_str.asSlice()); +} + +test "toStr: std.math.maxInt" { + var dec: RocDec = .{ .num = std.math.maxInt(i128) }; + var res_roc_str = dec.toStr(); + errdefer res_roc_str.decref(); + defer res_roc_str.decref(); + + const res_slice: []const u8 = "170141183460469231731.687303715884105727"[0..]; + try expectEqualSlices(u8, res_slice, res_roc_str.asSlice()); +} + +test "toStr: std.math.minInt" { + var dec: RocDec = .{ .num = std.math.minInt(i128) }; + var res_roc_str = dec.toStr(); + errdefer res_roc_str.decref(); + defer res_roc_str.decref(); + + const res_slice: []const u8 = "-170141183460469231731.687303715884105728"[0..]; + try expectEqualSlices(u8, res_slice, res_roc_str.asSlice()); +} + +test "toStr: 0" { + var dec: RocDec = .{ .num = 0 }; + var res_roc_str = dec.toStr(); + + const res_slice: []const u8 = "0.0"[0..]; + try expectEqualSlices(u8, res_slice, res_roc_str.asSlice()); +} + +test "add: 0" { + var dec: RocDec = .{ .num = 0 }; + + try expectEqual(RocDec{ .num = 0 }, dec.add(.{ .num = 0 })); +} + +test "add: 1" { + var dec: RocDec = .{ .num = 0 }; + + try expectEqual(RocDec{ .num = 1 }, dec.add(.{ .num = 1 })); +} + +test "sub: 0" { + var dec: RocDec = .{ .num = 1 }; + + try expectEqual(RocDec{ .num = 1 }, dec.sub(.{ .num = 0 })); +} + +test "sub: 1" { + var dec: RocDec = .{ .num = 1 }; + + try expectEqual(RocDec{ .num = 0 }, dec.sub(.{ .num = 1 })); +} + +test "mul: by 0" { + var dec: RocDec = .{ .num = 0 }; + + try expectEqual(RocDec{ .num = 0 }, dec.mul(.{ .num = 0 })); +} + +test "mul: by 1" { + var dec: RocDec = RocDec.fromU64(15); + + try expectEqual(RocDec.fromU64(15), dec.mul(RocDec.fromU64(1))); +} + +test "mul: by 2" { + var dec: RocDec = RocDec.fromU64(15); + + try expectEqual(RocDec.fromU64(30), dec.mul(RocDec.fromU64(2))); +} + +test "div: 0 / 2" { + var dec: RocDec = RocDec.fromU64(0); + + try expectEqual(RocDec.fromU64(0), dec.div(RocDec.fromU64(2))); +} + +test "div: 2 / 2" { + var dec: RocDec = RocDec.fromU64(2); + + try expectEqual(RocDec.fromU64(1), dec.div(RocDec.fromU64(2))); +} + +test "div: 20 / 2" { + var dec: RocDec = RocDec.fromU64(20); + + try expectEqual(RocDec.fromU64(10), dec.div(RocDec.fromU64(2))); +} + +test "div: 8 / 5" { + var dec: RocDec = RocDec.fromU64(8); + var res: RocDec = RocDec.fromStr(RocStr.init("1.6", 3)).?; + try expectEqual(res, dec.div(RocDec.fromU64(5))); +} + +test "div: 10 / 3" { + var numer: RocDec = RocDec.fromU64(10); + var denom: RocDec = RocDec.fromU64(3); + + var roc_str = RocStr.init("3.333333333333333333", 20); + errdefer roc_str.decref(); + defer roc_str.decref(); + + var res: RocDec = RocDec.fromStr(roc_str).?; + + try expectEqual(res, numer.div(denom)); +} + +test "div: 341 / 341" { + var number1: RocDec = RocDec.fromU64(341); + var number2: RocDec = RocDec.fromU64(341); + try expectEqual(RocDec.fromU64(1), number1.div(number2)); +} + +test "div: 342 / 343" { + var number1: RocDec = RocDec.fromU64(342); + var number2: RocDec = RocDec.fromU64(343); + var roc_str = RocStr.init("0.997084548104956268", 20); + try expectEqual(RocDec.fromStr(roc_str), number1.div(number2)); +} + +test "div: 680 / 340" { + var number1: RocDec = RocDec.fromU64(680); + var number2: RocDec = RocDec.fromU64(340); + try expectEqual(RocDec.fromU64(2), number1.div(number2)); +} + +test "div: 500 / 1000" { + var number1: RocDec = RocDec.fromU64(500); + var number2: RocDec = RocDec.fromU64(1000); + var roc_str = RocStr.init("0.5", 3); + try expectEqual(RocDec.fromStr(roc_str), number1.div(number2)); +} + +test "log: 1" { + try expectEqual(RocDec.fromU64(0), RocDec.log(RocDec.fromU64(1))); +} + +// exports + +pub fn fromStr(arg: RocStr) callconv(.C) num_.NumParseResult(i128) { + if (@call(.always_inline, RocDec.fromStr, .{arg})) |dec| { + return .{ .errorcode = 0, .value = dec.num }; + } else { + return .{ .errorcode = 1, .value = 0 }; + } +} + +pub fn toStr(arg: RocDec) callconv(.C) RocStr { + return @call(.always_inline, RocDec.toStr, .{arg}); +} + +pub fn fromF64C(arg: f64) callconv(.C) i128 { + if (@call(.always_inline, RocDec.fromF64, .{arg})) |dec| { + return dec.num; + } else { + @panic("TODO runtime exception failing convert f64 to RocDec"); + } +} + +pub fn fromF32C(arg_f32: f32) callconv(.C) i128 { + const arg_f64 = arg_f32; + if (@call(.always_inline, RocDec.fromF64, .{arg_f64})) |dec| { + return dec.num; + } else { + @panic("TODO runtime exception failing convert f64 to RocDec"); + } +} + +pub fn toF64(arg: RocDec) callconv(.C) f64 { + return @call(.always_inline, RocDec.toF64, .{arg}); +} + +pub fn exportFromInt(comptime T: type, comptime name: []const u8) void { + comptime var f = struct { + fn func(self: T) callconv(.C) i128 { + const this = @as(i128, @intCast(self)); + + const answer = @mulWithOverflow(this, RocDec.one_point_zero_i128); + if (answer[1] == 1) { + @panic("TODO runtime exception failing convert integer to RocDec"); + } else { + return answer[0]; + } + } + }.func; + @export(f, .{ .name = name ++ @typeName(T), .linkage = .Strong }); +} + +pub fn fromU64C(arg: u64) callconv(.C) i128 { + return @call(.always_inline, RocDec.fromU64, .{arg}).toI128(); +} + +pub fn toI128(arg: RocDec) callconv(.C) i128 { + return @call(.always_inline, RocDec.toI128, .{arg}); +} + +pub fn eqC(arg1: RocDec, arg2: RocDec) callconv(.C) bool { + return @call(.always_inline, RocDec.eq, .{ arg1, arg2 }); +} + +pub fn neqC(arg1: RocDec, arg2: RocDec) callconv(.C) bool { + return @call(.always_inline, RocDec.neq, .{ arg1, arg2 }); +} + +pub fn negateC(arg: RocDec) callconv(.C) i128 { + return if (@call(.always_inline, RocDec.negate, .{arg})) |dec| dec.num else @panic("TODO overflow for negating RocDec"); +} + +pub fn absC(arg: RocDec) callconv(.C) i128 { + const result = @call(.always_inline, RocDec.abs, .{arg}) catch @panic("TODO overflow for calling absolute value on RocDec"); + return result.num; +} + +pub fn addC(arg1: RocDec, arg2: RocDec) callconv(.C) WithOverflow(RocDec) { + return @call(.always_inline, RocDec.addWithOverflow, .{ arg1, arg2 }); +} + +pub fn subC(arg1: RocDec, arg2: RocDec) callconv(.C) WithOverflow(RocDec) { + return @call(.always_inline, RocDec.subWithOverflow, .{ arg1, arg2 }); +} + +pub fn mulC(arg1: RocDec, arg2: RocDec) callconv(.C) WithOverflow(RocDec) { + return @call(.always_inline, RocDec.mulWithOverflow, .{ arg1, arg2 }); +} + +pub fn divC(arg1: RocDec, arg2: RocDec) callconv(.C) i128 { + return @call(.always_inline, RocDec.div, .{ arg1, arg2 }).num; +} + +pub fn logC(arg: RocDec) callconv(.C) i128 { + return @call(.always_inline, RocDec.log, .{arg}).num; +} + +pub fn sinC(arg: RocDec) callconv(.C) i128 { + return @call(.always_inline, RocDec.sin, .{arg}).num; +} + +pub fn cosC(arg: RocDec) callconv(.C) i128 { + return @call(.always_inline, RocDec.cos, .{arg}).num; +} + +pub fn tanC(arg: RocDec) callconv(.C) i128 { + return @call(.always_inline, RocDec.tan, .{arg}).num; +} + +pub fn asinC(arg: RocDec) callconv(.C) i128 { + return @call(.always_inline, RocDec.asin, .{arg}).num; +} + +pub fn acosC(arg: RocDec) callconv(.C) i128 { + return @call(.always_inline, RocDec.acos, .{arg}).num; +} + +pub fn atanC(arg: RocDec) callconv(.C) i128 { + return @call(.always_inline, RocDec.atan, .{arg}).num; +} + +pub fn addOrPanicC(arg1: RocDec, arg2: RocDec) callconv(.C) RocDec { + return @call(.always_inline, RocDec.add, .{ arg1, arg2 }); +} + +pub fn addSaturatedC(arg1: RocDec, arg2: RocDec) callconv(.C) RocDec { + return @call(.always_inline, RocDec.addSaturated, .{ arg1, arg2 }); +} + +pub fn subOrPanicC(arg1: RocDec, arg2: RocDec) callconv(.C) RocDec { + return @call(.always_inline, RocDec.sub, .{ arg1, arg2 }); +} + +pub fn subSaturatedC(arg1: RocDec, arg2: RocDec) callconv(.C) RocDec { + return @call(.always_inline, RocDec.subSaturated, .{ arg1, arg2 }); +} + +pub fn mulOrPanicC(arg1: RocDec, arg2: RocDec) callconv(.C) RocDec { + return @call(.always_inline, RocDec.mul, .{ arg1, arg2 }); +} + +pub fn mulSaturatedC(arg1: RocDec, arg2: RocDec) callconv(.C) RocDec { + return @call(.always_inline, RocDec.mulSaturated, .{ arg1, arg2 }); +} diff --git a/crates/compiler/builtins/bitcode/src/expect.zig b/crates/compiler/builtins/bitcode/src/expect.zig new file mode 100644 index 0000000000..7ae9826c1d --- /dev/null +++ b/crates/compiler/builtins/bitcode/src/expect.zig @@ -0,0 +1,98 @@ +const std = @import("std"); +const builtin = @import("builtin"); + +const Atomic = std.atomic.Atomic; + +const O_RDWR: c_int = 2; +const O_CREAT: c_int = 64; + +pub const PROT_WRITE: c_int = 2; +pub const MAP_SHARED: c_int = 0x0001; + +// IMPORTANT: shared memory object names must begin with / and contain no other slashes! +var SHARED_BUFFER: []u8 = undefined; + +pub fn setSharedBuffer(ptr: [*]u8, length: usize) callconv(.C) usize { + SHARED_BUFFER = ptr[0..length]; + + // the rust side expects that a pointer is returned + return 0; +} + +pub fn expectFailedStartSharedBuffer() callconv(.C) [*]u8 { + return SHARED_BUFFER.ptr; +} + +pub fn expectFailedStartSharedFile() callconv(.C) [*]u8 { + // IMPORTANT: shared memory object names must begin with / and contain no other slashes! + var name: [100]u8 = undefined; + _ = std.fmt.bufPrint(name[0..100], "/roc_expect_buffer_{}\x00", .{roc_getppid()}) catch unreachable; + + if (builtin.os.tag == .macos or builtin.os.tag == .linux) { + const shared_fd = roc_shm_open(@as(*const i8, @ptrCast(&name)), O_RDWR | O_CREAT, 0o666); + + const length = 4096; + + const shared_ptr = roc_mmap( + null, + length, + PROT_WRITE, + MAP_SHARED, + shared_fd, + 0, + ); + + const ptr = @as([*]u8, @ptrCast(shared_ptr)); + + return ptr; + } else { + unreachable; + } +} + +extern fn roc_shm_open(name: *const i8, oflag: c_int, mode: c_uint) c_int; +extern fn roc_mmap(addr: ?*anyopaque, length: c_uint, prot: c_int, flags: c_int, fd: c_int, offset: c_uint) *anyopaque; +extern fn roc_getppid() c_int; + +pub fn readSharedBufferEnv() callconv(.C) void { + if (builtin.os.tag == .macos or builtin.os.tag == .linux) { + + // IMPORTANT: shared memory object names must begin with / and contain no other slashes! + var name: [100]u8 = undefined; + _ = std.fmt.bufPrint(name[0..100], "/roc_expect_buffer_{}\x00", .{roc_getppid()}) catch unreachable; + + const shared_fd = roc_shm_open(@as(*const i8, @ptrCast(&name)), O_RDWR | O_CREAT, 0o666); + const length = 4096; + + const shared_ptr = roc_mmap( + null, + length, + PROT_WRITE, + MAP_SHARED, + shared_fd, + 0, + ); + + const ptr = @as([*]u8, @ptrCast(shared_ptr)); + + SHARED_BUFFER = ptr[0..length]; + } +} + +pub fn notifyParent(shared_buffer: [*]u8, tag: u32) callconv(.C) void { + if (builtin.os.tag == .macos or builtin.os.tag == .linux) { + const usize_ptr = @as([*]u32, @ptrCast(@alignCast(shared_buffer))); + const atomic_ptr = @as(*Atomic(u32), @ptrCast(&usize_ptr[5])); + atomic_ptr.storeUnchecked(tag); + + // wait till the parent is done before proceeding + const Ordering = std.atomic.Ordering; + while (atomic_ptr.load(Ordering.Acquire) != 0) { + std.atomic.spinLoopHint(); + } + } +} + +pub fn notifyParentExpect(shared_buffer: [*]u8) callconv(.C) void { + notifyParent(shared_buffer, 1); +} diff --git a/crates/compiler/builtins/bitcode/src/glue.zig b/crates/compiler/builtins/bitcode/src/glue.zig new file mode 100644 index 0000000000..4343bd03ce --- /dev/null +++ b/crates/compiler/builtins/bitcode/src/glue.zig @@ -0,0 +1,7 @@ +// This is a glue package that just re-exports other libs useful for zig hosts. +// Long term, a slimmed down version of these libraries without all of the roc builtins should be create via `roc glue`. +// We also should make RocList use comptime types in order to make it nice to use in zig. + +pub const dec = @import("dec.zig"); +pub const list = @import("list.zig"); +pub const str = @import("str.zig"); diff --git a/compiler/builtins/bitcode/src/hash.zig b/crates/compiler/builtins/bitcode/src/hash.zig similarity index 97% rename from compiler/builtins/bitcode/src/hash.zig rename to crates/compiler/builtins/bitcode/src/hash.zig index 6fb42129ad..cb67609342 100644 --- a/compiler/builtins/bitcode/src/hash.zig +++ b/crates/compiler/builtins/bitcode/src/hash.zig @@ -40,7 +40,7 @@ fn read_8bytes_swapped(data: []const u8) u64 { fn mum(a: u64, b: u64) u64 { var r = std.math.mulWide(u64, a, b); r = (r >> 64) ^ r; - return @truncate(u64, r); + return @as(u64, @truncate(r)); } fn mix0(a: u64, b: u64, seed: u64) u64 { @@ -94,7 +94,7 @@ const WyhashStateless = struct { std.debug.assert(b.len < 32); const seed = self.seed; - const rem_len = @intCast(u5, b.len); + const rem_len = @as(u5, @intCast(b.len)); const rem_key = b[0..rem_len]; self.seed = switch (rem_len) { @@ -176,7 +176,7 @@ pub const Wyhash = struct { self.state.update(b[off .. off + aligned_len]); mem.copy(u8, self.buf[self.buf_len..], b[off + aligned_len ..]); - self.buf_len += @intCast(u8, b[off + aligned_len ..].len); + self.buf_len += @as(u8, @intCast(b[off + aligned_len ..].len)); } pub fn final(self: *Wyhash) u64 { @@ -232,8 +232,8 @@ test "test vectors streaming" { test "iterative non-divisible update" { var buf: [8192]u8 = undefined; - for (buf) |*e, i| { - e.* = @truncate(u8, i); + for (buf, 0..) |*e, i| { + e.* = @as(u8, @truncate(i)); } const seed = 0x128dad08f; diff --git a/compiler/builtins/bitcode/src/helpers/grapheme.zig b/crates/compiler/builtins/bitcode/src/helpers/grapheme.zig similarity index 99% rename from compiler/builtins/bitcode/src/helpers/grapheme.zig rename to crates/compiler/builtins/bitcode/src/helpers/grapheme.zig index 36cd2c4418..eceaf52f1e 100644 --- a/compiler/builtins/bitcode/src/helpers/grapheme.zig +++ b/crates/compiler/builtins/bitcode/src/helpers/grapheme.zig @@ -42,32 +42,32 @@ pub const BoundClass = enum(u8) { }; test "Bound Class" { - try expectEqual(0, @enumToInt(BoundClass.START)); + try expectEqual(0, @intFromEnum(BoundClass.START)); } // https://github.com/JuliaStrings/utf8proc/blob/master/utf8proc.c#L261 fn graphemeBreakSimple(lbc: BoundClass, tbc: BoundClass) bool { - const lbc_u8 = @enumToInt(lbc); - const tbc_u8 = @enumToInt(tbc); - return (if (lbc_u8 == @enumToInt(BoundClass.START)) // GB1 + const lbc_u8 = @intFromEnum(lbc); + const tbc_u8 = @intFromEnum(tbc); + return (if (lbc_u8 == @intFromEnum(BoundClass.START)) // GB1 true - else if (lbc_u8 == @enumToInt(BoundClass.CR) and tbc_u8 == @enumToInt(BoundClass.LF)) // GB3 + else if (lbc_u8 == @intFromEnum(BoundClass.CR) and tbc_u8 == @intFromEnum(BoundClass.LF)) // GB3 false - else if (lbc_u8 >= @enumToInt(BoundClass.CR) and lbc_u8 <= @enumToInt(BoundClass.CONTROL)) // GB4 + else if (lbc_u8 >= @intFromEnum(BoundClass.CR) and lbc_u8 <= @intFromEnum(BoundClass.CONTROL)) // GB4 true - else if (tbc_u8 >= @enumToInt(BoundClass.CR) and tbc_u8 <= @enumToInt(BoundClass.CONTROL)) // GB5 + else if (tbc_u8 >= @intFromEnum(BoundClass.CR) and tbc_u8 <= @intFromEnum(BoundClass.CONTROL)) // GB5 true - else if (lbc_u8 == @enumToInt(BoundClass.L) and (tbc_u8 == @enumToInt(BoundClass.L) or tbc_u8 == @enumToInt(BoundClass.V) or tbc_u8 == @enumToInt(BoundClass.LV) or tbc_u8 == @enumToInt(BoundClass.LVT))) // GB6 + else if (lbc_u8 == @intFromEnum(BoundClass.L) and (tbc_u8 == @intFromEnum(BoundClass.L) or tbc_u8 == @intFromEnum(BoundClass.V) or tbc_u8 == @intFromEnum(BoundClass.LV) or tbc_u8 == @intFromEnum(BoundClass.LVT))) // GB6 false - else if ((lbc_u8 == @enumToInt(BoundClass.LV) or lbc_u8 == @enumToInt(BoundClass.V)) and (tbc_u8 == @enumToInt(BoundClass.V) or tbc_u8 == @enumToInt(BoundClass.T))) // GB7 + else if ((lbc_u8 == @intFromEnum(BoundClass.LV) or lbc_u8 == @intFromEnum(BoundClass.V)) and (tbc_u8 == @intFromEnum(BoundClass.V) or tbc_u8 == @intFromEnum(BoundClass.T))) // GB7 false - else if ((lbc_u8 == @enumToInt(BoundClass.LVT) or lbc_u8 == @enumToInt(BoundClass.T)) and tbc_u8 == @enumToInt(BoundClass.T)) // GB8 + else if ((lbc_u8 == @intFromEnum(BoundClass.LVT) or lbc_u8 == @intFromEnum(BoundClass.T)) and tbc_u8 == @intFromEnum(BoundClass.T)) // GB8 false - else if (tbc_u8 == @enumToInt(BoundClass.EXTEND) or tbc_u8 == @enumToInt(BoundClass.ZWJ) or tbc_u8 == @enumToInt(BoundClass.SPACINGMARK) or lbc_u8 == @enumToInt(BoundClass.PREPEND)) // GB9a + else if (tbc_u8 == @intFromEnum(BoundClass.EXTEND) or tbc_u8 == @intFromEnum(BoundClass.ZWJ) or tbc_u8 == @intFromEnum(BoundClass.SPACINGMARK) or lbc_u8 == @intFromEnum(BoundClass.PREPEND)) // GB9a false - else if (lbc_u8 == @enumToInt(BoundClass.E_ZWG) and tbc_u8 == @enumToInt(BoundClass.EXTENDED_PICTOGRAPHIC)) // GB11 (requires additional handling below) + else if (lbc_u8 == @intFromEnum(BoundClass.E_ZWG) and tbc_u8 == @intFromEnum(BoundClass.EXTENDED_PICTOGRAPHIC)) // GB11 (requires additional handling below) false - else if (lbc_u8 == @enumToInt(BoundClass.REGIONAL_INDICATOR) and tbc_u8 == @enumToInt(BoundClass.REGIONAL_INDICATOR)) // GB12/13 (requires additional handling below) + else if (lbc_u8 == @intFromEnum(BoundClass.REGIONAL_INDICATOR) and tbc_u8 == @intFromEnum(BoundClass.REGIONAL_INDICATOR)) // GB12/13 (requires additional handling below) false else // GB999 true); diff --git a/crates/compiler/builtins/bitcode/src/lib.rs b/crates/compiler/builtins/bitcode/src/lib.rs new file mode 100644 index 0000000000..58efea062e --- /dev/null +++ b/crates/compiler/builtins/bitcode/src/lib.rs @@ -0,0 +1,65 @@ +use tempfile::NamedTempFile; + +const HOST_WASM: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/builtins-wasm32.o")); +// TODO: in the future, we should use Zig's cross-compilation to generate and store these +// for all targets, so that we can do cross-compilation! +#[cfg(unix)] +const HOST_UNIX: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/builtins-host.o")); +#[cfg(windows)] +const HOST_WINDOWS: &[u8] = + include_bytes!(concat!(env!("OUT_DIR"), "/builtins-windows-x86_64.obj")); + +pub fn host_wasm_tempfile() -> std::io::Result { + let tempfile = tempfile::Builder::new() + .prefix("host_bitcode") + .suffix(".wasm") + .rand_bytes(8) + .tempfile()?; + + std::fs::write(tempfile.path(), HOST_WASM)?; + + Ok(tempfile) +} + +#[cfg(unix)] +fn host_unix_tempfile() -> std::io::Result { + let tempfile = tempfile::Builder::new() + .prefix("host_bitcode") + .suffix(".o") + .rand_bytes(8) + .tempfile()?; + + std::fs::write(tempfile.path(), HOST_UNIX)?; + + Ok(tempfile) +} + +#[cfg(windows)] +fn host_windows_tempfile() -> std::io::Result { + let tempfile = tempfile::Builder::new() + .prefix("host_bitcode") + .suffix(".obj") + .rand_bytes(8) + .tempfile()?; + + std::fs::write(tempfile.path(), HOST_WINDOWS)?; + + Ok(tempfile) +} + +pub fn host_tempfile() -> std::io::Result { + #[cfg(unix)] + { + host_unix_tempfile() + } + + #[cfg(windows)] + { + host_windows_tempfile() + } + + #[cfg(not(any(windows, unix)))] + { + unreachable!() + } +} diff --git a/crates/compiler/builtins/bitcode/src/libc.zig b/crates/compiler/builtins/bitcode/src/libc.zig new file mode 100644 index 0000000000..d02467634a --- /dev/null +++ b/crates/compiler/builtins/bitcode/src/libc.zig @@ -0,0 +1,87 @@ +const std = @import("std"); +const builtin = @import("builtin"); +const arch = builtin.cpu.arch; +const musl = @import("libc/musl.zig"); +const folly = @import("libc/folly.zig"); +const cpuid = @import("libc/cpuid.zig"); + +comptime { + // TODO: remove this workaround. + // Our wasm llvm pipeline always links in memcpy. + // As such, our impl will conflict. + if (builtin.is_test) { + // We don't need memcpy for tests because the tests are built with -lc + } else if (arch != .wasm32) { + @export(memcpy, .{ .name = "memcpy", .linkage = .Strong }); + } +} + +const Memcpy = *const fn (noalias [*]u8, noalias [*]const u8, len: usize) callconv(.C) [*]u8; + +pub var memcpy_target: Memcpy = switch (arch) { + .x86_64 => dispatch_memcpy, + else => unreachable, +}; + +pub fn memcpy(noalias dest: [*]u8, noalias src: [*]const u8, len: usize) callconv(.C) [*]u8 { + switch (builtin.os.tag) { + .windows => { + return musl.memcpy(dest, src, len); + }, + else => switch (arch) { + // x86_64 has a special optimized memcpy that can use avx2. + .x86_64 => { + return memcpy_target(dest, src, len); + }, + else => { + return musl.memcpy(dest, src, len); + }, + }, + } +} + +const MemcpyDecision = enum { + uninitialized, + folly_prefetchw, + folly_prefetcht0, + musl, +}; + +var memcpy_decision: MemcpyDecision = .uninitialized; + +fn dispatch_memcpy(noalias dest: [*]u8, noalias src: [*]const u8, len: usize) callconv(.C) [*]u8 { + switch (arch) { + .x86_64 => { + // TODO: Switch this to overwrite the memcpy_target pointer once the surgical linker can support it. + // Then dispatch will just happen on the first call instead of every call. + // if (cpuid.supports_avx2()) { + // if (cpuid.supports_prefetchw()) { + // memcpy_target = folly.memcpy_prefetchw; + // } else { + // memcpy_target = folly.memcpy_prefetcht0; + // } + // } else { + // memcpy_target = musl.memcpy; + // } + // return memcpy_target(dest, src, len); + switch (memcpy_decision) { + .uninitialized => { + if (cpuid.supports_avx2()) { + if (cpuid.supports_prefetchw()) { + memcpy_decision = .folly_prefetchw; + } else { + memcpy_decision = .folly_prefetcht0; + } + } else { + memcpy_decision = .musl; + } + return dispatch_memcpy(dest, src, len); + }, + .folly_prefetchw => return folly.memcpy_prefetchw(dest, src, len), + .folly_prefetcht0 => return folly.memcpy_prefetcht0(dest, src, len), + .musl => return musl.memcpy(dest, src, len), + } + }, + else => unreachable, + } +} diff --git a/crates/compiler/builtins/bitcode/src/libc/assembly_util.zig b/crates/compiler/builtins/bitcode/src/libc/assembly_util.zig new file mode 100644 index 0000000000..93bbc9f98d --- /dev/null +++ b/crates/compiler/builtins/bitcode/src/libc/assembly_util.zig @@ -0,0 +1,7 @@ +const builtin = @import("builtin"); +const os = builtin.os; + +pub const function_prefix = switch (os.tag) { + .macos => "_", + else => "", +}; diff --git a/crates/compiler/builtins/bitcode/src/libc/cpuid.S b/crates/compiler/builtins/bitcode/src/libc/cpuid.S new file mode 100644 index 0000000000..1ea5eca2e4 --- /dev/null +++ b/crates/compiler/builtins/bitcode/src/libc/cpuid.S @@ -0,0 +1,53 @@ +// Check if AVX2 is supported. +// Returns 1 if AVX2 is supported, 0 otherwise. +.global {[function_prefix]s}supports_avx2; +{[function_prefix]s}supports_avx2: + // Save the EBX register. + push %rbx + + // Call the CPUID instruction with the EAX register set to 7 and ECX set to 0. + // This will get the CPUID information for the current CPU. + mov $7, %eax + mov $0, %ecx + cpuid + + // The AVX2 feature flag is located in the EBX register at bit 5. + bt $5, %ebx + jc .avx2_supported + + // AVX2 is not supported. + pop %rbx + mov $0, %eax + ret + + .avx2_supported: + pop %rbx + mov $1, %eax + ret + + // Check if prefetchw is supported. + // Returns 1 if the prefetchw instruction is supported, 0 otherwise. +.global {[function_prefix]s}supports_prefetchw; +{[function_prefix]s}supports_prefetchw: + // Save the EBX register. + push %rbx + + // Call the CPUID instruction with the EAX register set to 0x80000001 and ECX set to 0. + // This will get the CPUID information for the current CPU. + mov $0x80000001, %eax + mov $0, %ecx + cpuid + + // The prefetchw feature flag is located in the ECX register at bit 8. + bt $8, %ecx + jc .prefetchw_supported + + // AVX2 is not supported. + pop %rbx + mov $0, %eax + ret + + .prefetchw_supported: + pop %rbx + mov $1, %eax + ret diff --git a/crates/compiler/builtins/bitcode/src/libc/cpuid.zig b/crates/compiler/builtins/bitcode/src/libc/cpuid.zig new file mode 100644 index 0000000000..6b2dc63af6 --- /dev/null +++ b/crates/compiler/builtins/bitcode/src/libc/cpuid.zig @@ -0,0 +1,18 @@ +const std = @import("std"); +const builtin = @import("builtin"); +const arch = builtin.cpu.arch; +const function_prefix = @import("assembly_util.zig").function_prefix; + +// I couldn't manage to define this in a PIE friendly way with inline assembly. +// Instead, I am defining it as global assembly functions. +comptime { + switch (arch) { + .x86_64 => { + asm (std.fmt.comptimePrint(@embedFile("cpuid.S"), .{ .function_prefix = function_prefix })); + }, + else => unreachable, + } +} + +pub extern fn supports_avx2() bool; +pub extern fn supports_prefetchw() bool; diff --git a/crates/compiler/builtins/bitcode/src/libc/folly.zig b/crates/compiler/builtins/bitcode/src/libc/folly.zig new file mode 100644 index 0000000000..f8d0612b61 --- /dev/null +++ b/crates/compiler/builtins/bitcode/src/libc/folly.zig @@ -0,0 +1,2 @@ +pub const memcpy_prefetchw = @import("folly/memcpy.zig").__folly_memcpy_prefetchw; +pub const memcpy_prefetcht0 = @import("folly/memcpy.zig").__folly_memcpy_prefetcht0; diff --git a/crates/compiler/builtins/bitcode/src/libc/folly/memcpy-x86_64.S b/crates/compiler/builtins/bitcode/src/libc/folly/memcpy-x86_64.S new file mode 100644 index 0000000000..266e56b0a2 --- /dev/null +++ b/crates/compiler/builtins/bitcode/src/libc/folly/memcpy-x86_64.S @@ -0,0 +1,437 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * __folly_memcpy: An optimized memcpy implementation that uses prefetch and + * AVX2 instructions. + * + * This implementation of memcpy acts as a memmove: while overlapping copies + * are undefined in memcpy, in some implementations they're the same function and + * legacy programs rely on this behavior. + * + * This implementation uses prefetch to avoid dtlb misses. This can + * substantially reduce dtlb store misses in cases where the destination + * location is absent from L1 cache and where the copy size is small enough + * that the hardware prefetcher doesn't have a large impact. + * + * The number of branches is limited by the use of overlapping loads & stores. + * This helps with copies where the source and destination cache lines are already + * present in L1 because there are fewer instructions to execute and fewer + * branches to potentially mispredict. + * e.g. to copy the last 4 <= n <= 7 bytes: copy the first & last 4 bytes (overlapped): + * movl (%rsi), %r8d + * movl -4(%rsi,%rdx), %r9d + * movl %r8d, (%rdi) + * movl %r9d, -4(%rdi,%rdx) + * + * + * For sizes up to 256 all source data is first read into registers and then written: + * - n <= 16: overlapping movs + * - n <= 32: overlapping unaligned 16-byte SSE XMM load/stores + * - n <= 256: overlapping unaligned 32-byte AVX YMM load/stores + * + * Large copies (> 256 bytes) use unaligned loads + aligned stores. + * This is observed to always be faster than rep movsb, so the rep movsb + * instruction is not used. + * - The head & tail may be unaligned => they're always written using unaligned stores. + * + * If the copy size is humongous (> 32 KiB) and the source and destination are both + * aligned, this memcpy will use non-temporal operations (AVX2). This can have + * a substantial speedup for copies where data is absent from L1, but it + * is significantly slower if the source and destination data were already + * in L1. The use of non-temporal operations also has the effect that after + * the copy is complete, the data will be moved out of L1, even if the data was + * present before the copy started. + * + * For n > 256 and overlapping src & dst buffers (memmove): + * - use unaligned loads + aligned stores, but not non-temporal stores + * - for dst < src forward copy in 128 byte batches: + * - unaligned load the first 32 bytes & last 4 x 32 bytes + * - forward copy (unaligned load + aligned stores) 4 x 32 bytes at a time + * - unaligned store the first 32 bytes & last 4 x 32 bytes + * - for dst > src backward copy in 128 byte batches: + * - unaligned load the first 4 x 32 bytes & last 32 bytes + * - backward copy (unaligned load + aligned stores) 4 x 32 bytes at a time + * - unaligned store the first 4 x 32 bytes & last 32 bytes + * + * @author Logan Evans + */ + + + // .type {[function_prefix]s}__folly_memcpy_short_{[prefetch]s}, @function not supported by windows +{[function_prefix]s}__folly_memcpy_short_{[prefetch]s}: + .cfi_startproc + +.L_GE1_LE7_{[prefetch]s}: + cmp $1, %rdx + je .L_EQ1_{[prefetch]s} + + cmp $4, %rdx + jae .L_GE4_LE7_{[prefetch]s} + +.L_GE2_LE3_{[prefetch]s}: + movw (%rsi), %r8w + movw -2(%rsi,%rdx), %r9w + movw %r8w, (%rdi) + movw %r9w, -2(%rdi,%rdx) + ret + + .balign 2 +.L_EQ1_{[prefetch]s}: + movb (%rsi), %r8b + movb %r8b, (%rdi) + ret + + // Aligning the target of a jump to an even address has a measurable + // speedup in microbenchmarks. + .balign 2 +.L_GE4_LE7_{[prefetch]s}: + movl (%rsi), %r8d + movl -4(%rsi,%rdx), %r9d + movl %r8d, (%rdi) + movl %r9d, -4(%rdi,%rdx) + ret + + .cfi_endproc + // .size {[function_prefix]s}__folly_memcpy_short_{[prefetch]s}, .-{[function_prefix]s}__folly_memcpy_short_{[prefetch]s} not supported by windows + +// memcpy is an alternative entrypoint into the function named __folly_memcpy. +// The compiler is able to call memcpy since the name is global while +// stacktraces will show __folly_memcpy since that is the name of the function. +// This is intended to aid in debugging by making it obvious which version of +// memcpy is being used. + .balign 64 + .globl {[function_prefix]s}__folly_memcpy_{[prefetch]s} + // .type {[function_prefix]s}__folly_memcpy_{[prefetch]s}, @function not supported by windows + +{[function_prefix]s}__folly_memcpy_{[prefetch]s}: + .cfi_startproc + + mov %rdi, %rax // return: $rdi + + test %rdx, %rdx + je .L_EQ0_{[prefetch]s} + + {[prefetch]s} (%rdi) + {[prefetch]s} -1(%rdi,%rdx) + + cmp $8, %rdx + jb .L_GE1_LE7_{[prefetch]s} + +.L_GE8_{[prefetch]s}: + cmp $32, %rdx + ja .L_GE33_{[prefetch]s} + +.L_GE8_LE32_{[prefetch]s}: + cmp $16, %rdx + ja .L_GE17_LE32_{[prefetch]s} + +.L_GE8_LE16_{[prefetch]s}: + mov (%rsi), %r8 + mov -8(%rsi,%rdx), %r9 + mov %r8, (%rdi) + mov %r9, -8(%rdi,%rdx) +.L_EQ0_{[prefetch]s}: + ret + + .balign 2 +.L_GE17_LE32_{[prefetch]s}: + movdqu (%rsi), %xmm0 + movdqu -16(%rsi,%rdx), %xmm1 + movdqu %xmm0, (%rdi) + movdqu %xmm1, -16(%rdi,%rdx) + ret + + .balign 2 +.L_GE193_LE256_{[prefetch]s}: + vmovdqu %ymm3, 96(%rdi) + vmovdqu %ymm4, -128(%rdi,%rdx) + +.L_GE129_LE192_{[prefetch]s}: + vmovdqu %ymm2, 64(%rdi) + vmovdqu %ymm5, -96(%rdi,%rdx) + +.L_GE65_LE128_{[prefetch]s}: + vmovdqu %ymm1, 32(%rdi) + vmovdqu %ymm6, -64(%rdi,%rdx) + +.L_GE33_LE64_{[prefetch]s}: + vmovdqu %ymm0, (%rdi) + vmovdqu %ymm7, -32(%rdi,%rdx) + + vzeroupper + ret + + .balign 2 +.L_GE33_{[prefetch]s}: + vmovdqu (%rsi), %ymm0 + vmovdqu -32(%rsi,%rdx), %ymm7 + + cmp $64, %rdx + jbe .L_GE33_LE64_{[prefetch]s} + + {[prefetch]s} 64(%rdi) + + vmovdqu 32(%rsi), %ymm1 + vmovdqu -64(%rsi,%rdx), %ymm6 + + cmp $128, %rdx + jbe .L_GE65_LE128_{[prefetch]s} + + {[prefetch]s} 128(%rdi) + + vmovdqu 64(%rsi), %ymm2 + vmovdqu -96(%rsi,%rdx), %ymm5 + + cmp $192, %rdx + jbe .L_GE129_LE192_{[prefetch]s} + + {[prefetch]s} 192(%rdi) + + vmovdqu 96(%rsi), %ymm3 + vmovdqu -128(%rsi,%rdx), %ymm4 + + cmp $256, %rdx + jbe .L_GE193_LE256_{[prefetch]s} + +.L_GE257_{[prefetch]s}: + {[prefetch]s} 256(%rdi) + + // Check if there is an overlap. If there is an overlap then the caller + // has a bug since this is undefined behavior. However, for legacy + // reasons this behavior is expected by some callers. + // + // All copies through 256 bytes will operate as a memmove since for + // those sizes all reads are performed before any writes. + // + // This check uses the idea that there is an overlap if + // (%rdi < (%rsi + %rdx)) && (%rsi < (%rdi + %rdx)), + // or equivalently, there is no overlap if + // ((%rsi + %rdx) <= %rdi) || ((%rdi + %rdx) <= %rsi). + // + // %r9 will be used after .L_ALIGNED_DST_LOOP to calculate how many + // bytes remain to be copied. + + // (%rsi + %rdx <= %rdi) => no overlap + lea (%rsi,%rdx), %r9 + cmp %rdi, %r9 + jbe .L_NO_OVERLAP_{[prefetch]s} + + // (%rdi + %rdx <= %rsi) => no overlap + lea (%rdi,%rdx), %r8 + cmp %rsi, %r8 + // If no info is available in branch predictor's cache, Intel CPUs assume + // forward jumps are not taken. Use a forward jump as overlapping buffers + // are unlikely. + ja .L_OVERLAP_{[prefetch]s} + + .balign 2 +.L_NO_OVERLAP_{[prefetch]s}: + vmovdqu %ymm0, (%rdi) + vmovdqu %ymm1, 32(%rdi) + vmovdqu %ymm2, 64(%rdi) + vmovdqu %ymm3, 96(%rdi) + + // Align %rdi to a 32 byte boundary. + // %rcx = 128 - 31 & %rdi + mov $128, %rcx + and $31, %rdi + sub %rdi, %rcx + + lea (%rsi,%rcx), %rsi + lea (%rax,%rcx), %rdi + sub %rcx, %rdx + + // %r8 is the end condition for the loop. + lea -128(%rsi,%rdx), %r8 + + // This threshold is half of L1 cache on a Skylake machine, which means that + // potentially all of L1 will be populated by this copy once it is executed + // (dst and src are cached for temporal copies). + // NON_TEMPORAL_STORE_THRESHOLD = $32768 + // cmp NON_TEMPORAL_STORE_THRESHOLD, %rdx + cmp $32768, %rdx + jae .L_NON_TEMPORAL_LOOP_{[prefetch]s} + + .balign 2 +.L_ALIGNED_DST_LOOP_{[prefetch]s}: + {[prefetch]s} 128(%rdi) + {[prefetch]s} 192(%rdi) + + vmovdqu (%rsi), %ymm0 + vmovdqu 32(%rsi), %ymm1 + vmovdqu 64(%rsi), %ymm2 + vmovdqu 96(%rsi), %ymm3 + add $128, %rsi + + vmovdqa %ymm0, (%rdi) + vmovdqa %ymm1, 32(%rdi) + vmovdqa %ymm2, 64(%rdi) + vmovdqa %ymm3, 96(%rdi) + add $128, %rdi + + cmp %r8, %rsi + jb .L_ALIGNED_DST_LOOP_{[prefetch]s} + +.L_ALIGNED_DST_LOOP_END_{[prefetch]s}: + sub %rsi, %r9 + mov %r9, %rdx + + vmovdqu %ymm4, -128(%rdi,%rdx) + vmovdqu %ymm5, -96(%rdi,%rdx) + vmovdqu %ymm6, -64(%rdi,%rdx) + vmovdqu %ymm7, -32(%rdi,%rdx) + + vzeroupper + ret + + .balign 2 +.L_NON_TEMPORAL_LOOP_{[prefetch]s}: + testb $31, %sil + jne .L_ALIGNED_DST_LOOP_{[prefetch]s} + // This is prefetching the source data unlike ALIGNED_DST_LOOP which + // prefetches the destination data. This choice is again informed by + // benchmarks. With a non-temporal store the entirety of the cache line + // is being written so the previous data can be discarded without being + // fetched. + prefetchnta 128(%rsi) + prefetchnta 196(%rsi) + + vmovntdqa (%rsi), %ymm0 + vmovntdqa 32(%rsi), %ymm1 + vmovntdqa 64(%rsi), %ymm2 + vmovntdqa 96(%rsi), %ymm3 + add $128, %rsi + + vmovntdq %ymm0, (%rdi) + vmovntdq %ymm1, 32(%rdi) + vmovntdq %ymm2, 64(%rdi) + vmovntdq %ymm3, 96(%rdi) + add $128, %rdi + + cmp %r8, %rsi + jb .L_NON_TEMPORAL_LOOP_{[prefetch]s} + + sfence + jmp .L_ALIGNED_DST_LOOP_END_{[prefetch]s} + + +.L_OVERLAP_{[prefetch]s}: + .balign 2 + cmp %rdi, %rsi + jb .L_OVERLAP_BWD_{[prefetch]s} // %rsi < %rdi => backward-copy + je .L_RET_{[prefetch]s} // %rsi == %rdi => return, nothing to copy + + // Source & destination buffers overlap. Forward copy. + + vmovdqu (%rsi), %ymm8 + + // Align %rdi to a 32 byte boundary. + // %rcx = 32 - 31 & %rdi + mov $32, %rcx + and $31, %rdi + sub %rdi, %rcx + + lea (%rsi,%rcx), %rsi + lea (%rax,%rcx), %rdi + sub %rcx, %rdx + + // %r8 is the end condition for the loop. + lea -128(%rsi,%rdx), %r8 + + +.L_OVERLAP_FWD_ALIGNED_DST_LOOP_{[prefetch]s}: + {[prefetch]s} 128(%rdi) + {[prefetch]s} 192(%rdi) + + vmovdqu (%rsi), %ymm0 + vmovdqu 32(%rsi), %ymm1 + vmovdqu 64(%rsi), %ymm2 + vmovdqu 96(%rsi), %ymm3 + add $128, %rsi + + vmovdqa %ymm0, (%rdi) + vmovdqa %ymm1, 32(%rdi) + vmovdqa %ymm2, 64(%rdi) + vmovdqa %ymm3, 96(%rdi) + add $128, %rdi + + cmp %r8, %rsi + jb .L_OVERLAP_FWD_ALIGNED_DST_LOOP_{[prefetch]s} + + sub %rsi, %r9 + mov %r9, %rdx + + vmovdqu %ymm4, -128(%rdi,%rdx) + vmovdqu %ymm5, -96(%rdi,%rdx) + vmovdqu %ymm6, -64(%rdi,%rdx) + vmovdqu %ymm7, -32(%rdi,%rdx) + vmovdqu %ymm8, (%rax) // %rax == the original (unaligned) %rdi + + vzeroupper + +.L_RET_{[prefetch]s}: + ret + +.L_OVERLAP_BWD_{[prefetch]s}: + // Save last 32 bytes. + vmovdqu -32(%rsi, %rdx), %ymm8 + lea -32(%rdi, %rdx), %r9 + + + // %r8 is the end condition for the loop. + lea 128(%rsi), %r8 + + // Align %rdi+%rdx (destination end) to a 32 byte boundary. + // %rcx = (%rdi + %rdx - 32) & 31 + mov %r9, %rcx + and $31, %rcx + // Set %rsi & %rdi to the end of the 32 byte aligned range. + sub %rcx, %rdx + add %rdx, %rsi + add %rdx, %rdi + + +.L_OVERLAP_BWD_ALIGNED_DST_LOOP_{[prefetch]s}: + {[prefetch]s} -128(%rdi) + {[prefetch]s} -192(%rdi) + + vmovdqu -32(%rsi), %ymm4 + vmovdqu -64(%rsi), %ymm5 + vmovdqu -96(%rsi), %ymm6 + vmovdqu -128(%rsi), %ymm7 + sub $128, %rsi + + vmovdqa %ymm4, -32(%rdi) + vmovdqa %ymm5, -64(%rdi) + vmovdqa %ymm6, -96(%rdi) + vmovdqa %ymm7, -128(%rdi) + sub $128, %rdi + + cmp %r8, %rsi + ja .L_OVERLAP_BWD_ALIGNED_DST_LOOP_{[prefetch]s} + + vmovdqu %ymm0, (%rax) // %rax == the original unaligned %rdi + vmovdqu %ymm1, 32(%rax) + vmovdqu %ymm2, 64(%rax) + vmovdqu %ymm3, 96(%rax) + vmovdqu %ymm8, (%r9) + + vzeroupper + ret + + .cfi_endproc + // .size {[function_prefix]s}__folly_memcpy_{[prefetch]s}, .-{[function_prefix]s}__folly_memcpy_{[prefetch]s} not supported by windows diff --git a/crates/compiler/builtins/bitcode/src/libc/folly/memcpy.zig b/crates/compiler/builtins/bitcode/src/libc/folly/memcpy.zig new file mode 100644 index 0000000000..e7f1b068db --- /dev/null +++ b/crates/compiler/builtins/bitcode/src/libc/folly/memcpy.zig @@ -0,0 +1,18 @@ +const std = @import("std"); +const builtin = @import("builtin"); +const arch = builtin.cpu.arch; +const function_prefix = @import("../assembly_util.zig").function_prefix; + +comptime { + switch (arch) { + .x86_64 => { + inline for ([_][]const u8{ "prefetchw", "prefetcht0" }) |prefetch| { + asm (std.fmt.comptimePrint(@embedFile("memcpy-x86_64.S"), .{ .prefetch = prefetch, .function_prefix = function_prefix })); + } + }, + else => unreachable, + } +} + +pub extern fn __folly_memcpy_prefetchw(noalias dest: [*]u8, noalias src: [*]const u8, len: usize) callconv(.SysV) [*]u8; +pub extern fn __folly_memcpy_prefetcht0(noalias dest: [*]u8, noalias src: [*]const u8, len: usize) callconv(.SysV) [*]u8; diff --git a/crates/compiler/builtins/bitcode/src/libc/musl.zig b/crates/compiler/builtins/bitcode/src/libc/musl.zig new file mode 100644 index 0000000000..8bff515806 --- /dev/null +++ b/crates/compiler/builtins/bitcode/src/libc/musl.zig @@ -0,0 +1 @@ +pub const memcpy = @import("musl/memcpy.zig").memcpy; diff --git a/crates/compiler/builtins/bitcode/src/libc/musl/COPYRIGHT b/crates/compiler/builtins/bitcode/src/libc/musl/COPYRIGHT new file mode 100644 index 0000000000..c1628e9ac8 --- /dev/null +++ b/crates/compiler/builtins/bitcode/src/libc/musl/COPYRIGHT @@ -0,0 +1,193 @@ +musl as a whole is licensed under the following standard MIT license: + +---------------------------------------------------------------------- +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. +---------------------------------------------------------------------- + +Authors/contributors include: + +A. Wilcox +Ada Worcester +Alex Dowad +Alex Suykov +Alexander Monakov +Andre McCurdy +Andrew Kelley +Anthony G. Basile +Aric Belsito +Arvid Picciani +Bartosz Brachaczek +Benjamin Peterson +Bobby Bingham +Boris Brezillon +Brent Cook +Chris Spiegel +Clément Vasseur +Daniel Micay +Daniel Sabogal +Daurnimator +David Carlier +David Edelsohn +Denys Vlasenko +Dmitry Ivanov +Dmitry V. Levin +Drew DeVault +Emil Renner Berthing +Fangrui Song +Felix Fietkau +Felix Janda +Gianluca Anzolin +Hauke Mehrtens +He X +Hiltjo Posthuma +Isaac Dunham +Jaydeep Patil +Jens Gustedt +Jeremy Huntwork +Jo-Philipp Wich +Joakim Sindholt +John Spencer +Julien Ramseier +Justin Cormack +Kaarle Ritvanen +Khem Raj +Kylie McClain +Leah Neukirchen +Luca Barbato +Luka Perkov +M Farkas-Dyck (Strake) +Mahesh Bodapati +Markus Wichmann +Masanori Ogino +Michael Clark +Michael Forney +Mikhail Kremnyov +Natanael Copa +Nicholas J. Kain +orc +Pascal Cuoq +Patrick Oppenlander +Petr Hosek +Petr Skocik +Pierre Carrier +Reini Urban +Rich Felker +Richard Pennington +Ryan Fairfax +Samuel Holland +Segev Finer +Shiz +sin +Solar Designer +Stefan Kristiansson +Stefan O'Rear +Szabolcs Nagy +Timo Teräs +Trutz Behn +Valentin Ochs +Will Dietz +William Haddon +William Pitcock + +Portions of this software are derived from third-party works licensed +under terms compatible with the above MIT license: + +The TRE regular expression implementation (src/regex/reg* and +src/regex/tre*) is Copyright © 2001-2008 Ville Laurikari and licensed +under a 2-clause BSD license (license text in the source files). The +included version has been heavily modified by Rich Felker in 2012, in +the interests of size, simplicity, and namespace cleanliness. + +Much of the math library code (src/math/* and src/complex/*) is +Copyright © 1993,2004 Sun Microsystems or +Copyright © 2003-2011 David Schultz or +Copyright © 2003-2009 Steven G. Kargl or +Copyright © 2003-2009 Bruce D. Evans or +Copyright © 2008 Stephen L. Moshier or +Copyright © 2017-2018 Arm Limited +and labelled as such in comments in the individual source files. All +have been licensed under extremely permissive terms. + +The ARM memcpy code (src/string/arm/memcpy.S) is Copyright © 2008 +The Android Open Source Project and is licensed under a two-clause BSD +license. It was taken from Bionic libc, used on Android. + +The AArch64 memcpy and memset code (src/string/aarch64/*) are +Copyright © 1999-2019, Arm Limited. + +The implementation of DES for crypt (src/crypt/crypt_des.c) is +Copyright © 1994 David Burren. It is licensed under a BSD license. + +The implementation of blowfish crypt (src/crypt/crypt_blowfish.c) was +originally written by Solar Designer and placed into the public +domain. The code also comes with a fallback permissive license for use +in jurisdictions that may not recognize the public domain. + +The smoothsort implementation (src/stdlib/qsort.c) is Copyright © 2011 +Valentin Ochs and is licensed under an MIT-style license. + +The x86_64 port was written by Nicholas J. Kain and is licensed under +the standard MIT terms. + +The mips and microblaze ports were originally written by Richard +Pennington for use in the ellcc project. The original code was adapted +by Rich Felker for build system and code conventions during upstream +integration. It is licensed under the standard MIT terms. + +The mips64 port was contributed by Imagination Technologies and is +licensed under the standard MIT terms. + +The powerpc port was also originally written by Richard Pennington, +and later supplemented and integrated by John Spencer. It is licensed +under the standard MIT terms. + +All other files which have no copyright comments are original works +produced specifically for use as part of this library, written either +by Rich Felker, the main author of the library, or by one or more +contibutors listed above. Details on authorship of individual files +can be found in the git version control history of the project. The +omission of copyright and license comments in each file is in the +interest of source tree size. + +In addition, permission is hereby granted for all public header files +(include/* and arch/*/bits/*) and crt files intended to be linked into +applications (crt/*, ldso/dlstart.c, and arch/*/crt_arch.h) to omit +the copyright notice and permission notice otherwise required by the +license, and to use these files without any requirement of +attribution. These files include substantial contributions from: + +Bobby Bingham +John Spencer +Nicholas J. Kain +Rich Felker +Richard Pennington +Stefan Kristiansson +Szabolcs Nagy + +all of whom have explicitly granted such permission. + +This file previously contained text expressing a belief that most of +the files covered by the above exception were sufficiently trivial not +to be subject to copyright, resulting in confusion over whether it +negated the permissions granted in the license. In the spirit of +permissive licensing, and of not having licensing issues being an +obstacle to adoption, that text has been removed. diff --git a/crates/compiler/builtins/bitcode/src/libc/musl/README.md b/crates/compiler/builtins/bitcode/src/libc/musl/README.md new file mode 100644 index 0000000000..d2d8d90c89 --- /dev/null +++ b/crates/compiler/builtins/bitcode/src/libc/musl/README.md @@ -0,0 +1,2 @@ +This set of files all come from [musl libc](https://musl.libc.org/). +Roc just directly uses a few of them instead of depending on musl libc fully. diff --git a/crates/compiler/builtins/bitcode/src/libc/musl/memcpy-x86.S b/crates/compiler/builtins/bitcode/src/libc/musl/memcpy-x86.S new file mode 100644 index 0000000000..83a2d4261d --- /dev/null +++ b/crates/compiler/builtins/bitcode/src/libc/musl/memcpy-x86.S @@ -0,0 +1,30 @@ +.global {[function_prefix]s}musl_memcpy +// Windows does not support the type directive. +// .type {[function_prefix]s}musl_memcpy,@function +{[function_prefix]s}musl_memcpy: + push %esi + push %edi + mov 12(%esp),%edi + mov 16(%esp),%esi + mov 20(%esp),%ecx + mov %edi,%eax + cmp $4,%ecx + jc 1f + test $3,%edi + jz 1f +2: movsb + dec %ecx + test $3,%edi + jnz 2b +1: mov %ecx,%edx + shr $2,%ecx + rep + movsl + and $3,%edx + jz 1f +2: movsb + dec %edx + jnz 2b +1: pop %edi + pop %esi + ret \ No newline at end of file diff --git a/crates/compiler/builtins/bitcode/src/libc/musl/memcpy-x86_64.S b/crates/compiler/builtins/bitcode/src/libc/musl/memcpy-x86_64.S new file mode 100644 index 0000000000..21391b4553 --- /dev/null +++ b/crates/compiler/builtins/bitcode/src/libc/musl/memcpy-x86_64.S @@ -0,0 +1,23 @@ +.global {[function_prefix]s}musl_memcpy +// Windows does not support the type directive. +// .type {[function_prefix]s}musl_memcpy,@function +{[function_prefix]s}musl_memcpy: + mov %rdi,%rax + cmp $8,%rdx + jc 1f + test $7,%edi + jz 1f +2: movsb + dec %rdx + test $7,%edi + jnz 2b +1: mov %rdx,%rcx + shr $3,%rcx + rep + movsq + and $7,%edx + jz 1f +2: movsb + dec %edx + jnz 2b +1: ret diff --git a/crates/compiler/builtins/bitcode/src/libc/musl/memcpy.zig b/crates/compiler/builtins/bitcode/src/libc/musl/memcpy.zig new file mode 100644 index 0000000000..b301900f21 --- /dev/null +++ b/crates/compiler/builtins/bitcode/src/libc/musl/memcpy.zig @@ -0,0 +1,223 @@ +const std = @import("std"); +const builtin = @import("builtin"); +const arch = builtin.cpu.arch; +const function_prefix = @import("../assembly_util.zig").function_prefix; + +comptime { + switch (arch) { + .x86_64 => { + asm (std.fmt.comptimePrint(@embedFile("memcpy-x86_64.S"), .{ .function_prefix = function_prefix })); + }, + .x86 => { + asm (std.fmt.comptimePrint(@embedFile("memcpy-x86.S"), .{ .function_prefix = function_prefix })); + }, + // TODO: add assembly implementations for other platforms. + else => {}, + } +} + +pub const memcpy = + switch (builtin.os.tag) { + .windows => fallback_memcpy, + else => switch (arch) { + .x86_64, .x86 => musl_memcpy, + else => fallback_memcpy, + }, +}; + +pub extern fn musl_memcpy(noalias dest: [*]u8, noalias src: [*]const u8, len: usize) callconv(.C) [*]u8; + +// Note: this is written to only support little endian targets. +// To support big endian, `<<` and `>>` wold need to be swapped. +pub fn fallback_memcpy(noalias dest: [*]u8, noalias src: [*]const u8, len: usize) callconv(.C) [*]u8 { + var d = dest; + var s = src; + var n = len; + switch (@min(n, @intFromPtr(s) % 4)) { + 1 => { + d[0] = s[0]; + d += 1; + s += 1; + n -= 1; + }, + 2 => { + d[0] = s[0]; + d[1] = s[1]; + d += 2; + s += 2; + n -= 2; + }, + 3 => { + d[0] = s[0]; + d[1] = s[1]; + d[2] = s[2]; + d += 3; + s += 3; + n -= 3; + }, + else => {}, + } + + if (@intFromPtr(d) % 4 == 0) { + var d4 = @as([*]align(4) u8, @alignCast(d)); + var s4 = @as([*]align(4) const u8, @alignCast(s)); + while (n >= 16) : (n -= 16) { + var d_u32 = @as([*]u32, @ptrCast(d4)); + var s_u32 = @as([*]const u32, @ptrCast(s4)); + d_u32[0] = s_u32[0]; + d_u32[1] = s_u32[1]; + d_u32[2] = s_u32[2]; + d_u32[3] = s_u32[3]; + + d4 += 16; + s4 += 16; + } + if (n & 8 != 0) { + var d_u32 = @as([*]u32, @ptrCast(d4)); + var s_u32 = @as([*]const u32, @ptrCast(s4)); + d_u32[0] = s_u32[0]; + d_u32[1] = s_u32[1]; + + d4 += 8; + s4 += 8; + } + if (n & 4 != 0) { + var d_u32 = @as([*]u32, @ptrCast(d4)); + var s_u32 = @as([*]const u32, @ptrCast(s4)); + d_u32[0] = s_u32[0]; + + d4 += 4; + s4 += 4; + } + d = d4; + s = s4; + if (n & 2 != 0) { + d[0] = s[0]; + d += 1; + s += 1; + d[0] = s[0]; + d += 1; + s += 1; + } + if (n & 1 != 0) { + d[0] = s[0]; + } + return dest; + } + if (n >= 32) { + switch (@intFromPtr(d) % 4) { + 1 => { + var w = @as([*]const u32, @ptrCast(@alignCast(s)))[0]; + d[0] = s[0]; + d += 1; + s += 1; + d[0] = s[0]; + d += 1; + s += 1; + d[0] = s[0]; + d += 1; + s += 1; + n -= 3; + while (n >= 17) : (n -= 16) { + var d_u32 = @as([*]u32, @ptrCast(@alignCast(d))); + var s_u32 = @as([*]const u32, @ptrCast(@alignCast(s + 1))); + var x = s_u32[0]; + d_u32[0] = (w >> 24) | (x << 8); + w = s_u32[1]; + d_u32[1] = (x >> 24) | (w << 8); + x = s_u32[2]; + d_u32[2] = (w >> 24) | (x << 8); + w = s_u32[3]; + d_u32[3] = (x >> 24) | (w << 8); + + d += 16; + s += 16; + } + }, + 2 => { + var w = @as([*]const u32, @ptrCast(@alignCast(s)))[0]; + d[0] = s[0]; + d += 1; + s += 1; + d[0] = s[0]; + d += 1; + s += 1; + n -= 2; + while (n >= 18) : (n -= 16) { + var d_u32 = @as([*]u32, @ptrCast(@alignCast(d))); + var s_u32 = @as([*]const u32, @ptrCast(@alignCast(s + 2))); + var x = s_u32[0]; + d_u32[0] = (w >> 16) | (x << 16); + w = s_u32[1]; + d_u32[1] = (x >> 16) | (w << 16); + x = s_u32[2]; + d_u32[2] = (w >> 16) | (x << 16); + w = s_u32[3]; + d_u32[3] = (x >> 16) | (w << 16); + + d += 16; + s += 16; + } + }, + 3 => { + var w = @as([*]const u32, @ptrCast(@alignCast(s)))[0]; + d[0] = s[0]; + d += 1; + s += 1; + n -= 1; + while (n >= 19) : (n -= 16) { + var d_u32 = @as([*]u32, @ptrCast(@alignCast(d))); + var s_u32 = @as([*]const u32, @ptrCast(@alignCast(s + 3))); + var x = s_u32[0]; + d_u32[0] = (w >> 8) | (x << 24); + w = s_u32[1]; + d_u32[1] = (x >> 8) | (w << 24); + x = s_u32[2]; + d_u32[2] = (w >> 8) | (x << 24); + w = s_u32[3]; + d_u32[3] = (x >> 8) | (w << 24); + + d += 16; + s += 16; + } + }, + else => unreachable, + } + } + if (n & 16 != 0) { + comptime var i = 0; + inline while (i < 16) : (i += 1) { + d[0] = s[0]; + d += 1; + s += 1; + } + } + if (n & 8 != 0) { + comptime var i = 0; + inline while (i < 8) : (i += 1) { + d[0] = s[0]; + d += 1; + s += 1; + } + } + if (n & 4 != 0) { + comptime var i = 0; + inline while (i < 4) : (i += 1) { + d[0] = s[0]; + d += 1; + s += 1; + } + } + if (n & 2 != 0) { + d[0] = s[0]; + d += 1; + s += 1; + d[0] = s[0]; + d += 1; + s += 1; + } + if (n & 1 != 0) { + d[0] = s[0]; + } + return dest; +} diff --git a/crates/compiler/builtins/bitcode/src/list.zig b/crates/compiler/builtins/bitcode/src/list.zig new file mode 100644 index 0000000000..ad0691df60 --- /dev/null +++ b/crates/compiler/builtins/bitcode/src/list.zig @@ -0,0 +1,990 @@ +const std = @import("std"); +const utils = @import("utils.zig"); +const UpdateMode = utils.UpdateMode; +const mem = std.mem; +const math = std.math; + +const expect = std.testing.expect; + +const EqFn = *const fn (?[*]u8, ?[*]u8) callconv(.C) bool; +const CompareFn = *const fn (?[*]u8, ?[*]u8, ?[*]u8) callconv(.C) u8; +const Opaque = ?[*]u8; + +const Inc = *const fn (?[*]u8) callconv(.C) void; +const IncN = *const fn (?[*]u8, usize) callconv(.C) void; +const Dec = *const fn (?[*]u8) callconv(.C) void; +const HasTagId = *const fn (u16, ?[*]u8) callconv(.C) extern struct { matched: bool, data: ?[*]u8 }; + +const SEAMLESS_SLICE_BIT: usize = + @as(usize, @bitCast(@as(isize, std.math.minInt(isize)))); + +pub const RocList = extern struct { + bytes: ?[*]u8, + length: usize, + // This technically points to directly after the refcount. + // This is an optimization that enables use one code path for regular lists and slices for geting the refcount ptr. + capacity_or_ref_ptr: usize, + + pub inline fn len(self: RocList) usize { + return self.length; + } + + pub fn getCapacity(self: RocList) usize { + const list_capacity = self.capacity_or_ref_ptr; + const slice_capacity = self.length; + const slice_mask = self.seamlessSliceMask(); + const capacity = (list_capacity & ~slice_mask) | (slice_capacity & slice_mask); + return capacity; + } + + pub fn isSeamlessSlice(self: RocList) bool { + return @as(isize, @bitCast(self.capacity_or_ref_ptr)) < 0; + } + + // This returns all ones if the list is a seamless slice. + // Otherwise, it returns all zeros. + // This is done without branching for optimization purposes. + pub fn seamlessSliceMask(self: RocList) usize { + return @as(usize, @bitCast(@as(isize, @bitCast(self.capacity_or_ref_ptr)) >> (@bitSizeOf(isize) - 1))); + } + + pub fn isEmpty(self: RocList) bool { + return self.len() == 0; + } + + pub fn empty() RocList { + return RocList{ .bytes = null, .length = 0, .capacity_or_ref_ptr = 0 }; + } + + pub fn eql(self: RocList, other: RocList) bool { + if (self.len() != other.len()) { + return false; + } + + // Their lengths are the same, and one is empty; they're both empty! + if (self.isEmpty()) { + return true; + } + + var index: usize = 0; + const self_bytes = self.bytes orelse unreachable; + const other_bytes = other.bytes orelse unreachable; + + while (index < self.len()) { + if (self_bytes[index] != other_bytes[index]) { + return false; + } + + index += 1; + } + + return true; + } + + pub fn fromSlice(comptime T: type, slice: []const T) RocList { + if (slice.len == 0) { + return RocList.empty(); + } + + var list = allocate(@alignOf(T), slice.len, @sizeOf(T)); + + if (slice.len > 0) { + const dest = list.bytes orelse unreachable; + const src = @as([*]const u8, @ptrCast(slice.ptr)); + const num_bytes = slice.len * @sizeOf(T); + + @memcpy(dest[0..num_bytes], src[0..num_bytes]); + } + + return list; + } + + // returns a pointer to just after the refcount. + // It is just after the refcount as an optimization for other shared code paths. + // For regular list, it just returns their bytes pointer. + // For seamless slices, it returns the pointer stored in capacity_or_ref_ptr. + pub fn getRefcountPtr(self: RocList) ?[*]u8 { + const list_ref_ptr = @intFromPtr(self.bytes); + const slice_ref_ptr = self.capacity_or_ref_ptr << 1; + const slice_mask = self.seamlessSliceMask(); + const ref_ptr = (list_ref_ptr & ~slice_mask) | (slice_ref_ptr & slice_mask); + return @as(?[*]u8, @ptrFromInt(ref_ptr)); + } + + pub fn decref(self: RocList, alignment: u32) void { + // We use the raw capacity to ensure we always decrement the refcount of seamless slices. + utils.decref(self.getRefcountPtr(), self.capacity_or_ref_ptr, alignment); + } + + pub fn elements(self: RocList, comptime T: type) ?[*]T { + return @as(?[*]T, @ptrCast(@alignCast(self.bytes))); + } + + pub fn isUnique(self: RocList) bool { + return self.refcountMachine() == utils.REFCOUNT_ONE; + } + + fn refcountMachine(self: RocList) usize { + if (self.getCapacity() == 0 and !self.isSeamlessSlice()) { + // the zero-capacity is Clone, copying it will not leak memory + return utils.REFCOUNT_ONE; + } + + const ptr: [*]usize = @as([*]usize, @ptrCast(@alignCast(self.bytes))); + return (ptr - 1)[0]; + } + + fn refcountHuman(self: RocList) usize { + return self.refcountMachine() - utils.REFCOUNT_ONE + 1; + } + + pub fn makeUniqueExtra(self: RocList, alignment: u32, element_width: usize, update_mode: UpdateMode) RocList { + if (update_mode == .InPlace) { + return self; + } else { + return self.makeUnique(alignment, element_width); + } + } + + pub fn makeUnique(self: RocList, alignment: u32, element_width: usize) RocList { + if (self.isUnique()) { + return self; + } + + if (self.isEmpty()) { + // Empty is not necessarily unique on it's own. + // The list could have capacity and be shared. + self.decref(alignment); + return RocList.empty(); + } + + // unfortunately, we have to clone + var new_list = RocList.allocate(alignment, self.length, element_width); + + var old_bytes: [*]u8 = @as([*]u8, @ptrCast(self.bytes)); + var new_bytes: [*]u8 = @as([*]u8, @ptrCast(new_list.bytes)); + + const number_of_bytes = self.len() * element_width; + @memcpy(new_bytes[0..number_of_bytes], old_bytes[0..number_of_bytes]); + + // NOTE we fuse an increment of all keys/values with a decrement of the input list. + self.decref(alignment); + + return new_list; + } + + pub fn allocate( + alignment: u32, + length: usize, + element_width: usize, + ) RocList { + if (length == 0) { + return empty(); + } + + const capacity = utils.calculateCapacity(0, length, element_width); + const data_bytes = capacity * element_width; + return RocList{ + .bytes = utils.allocateWithRefcount(data_bytes, alignment), + .length = length, + .capacity_or_ref_ptr = capacity, + }; + } + + pub fn allocateExact( + alignment: u32, + length: usize, + element_width: usize, + ) RocList { + if (length == 0) { + return empty(); + } + + const data_bytes = length * element_width; + return RocList{ + .bytes = utils.allocateWithRefcount(data_bytes, alignment), + .length = length, + .capacity_or_ref_ptr = length, + }; + } + + pub fn reallocate( + self: RocList, + alignment: u32, + new_length: usize, + element_width: usize, + ) RocList { + if (self.bytes) |source_ptr| { + if (self.isUnique() and !self.isSeamlessSlice()) { + const capacity = self.capacity_or_ref_ptr; + if (capacity >= new_length) { + return RocList{ .bytes = self.bytes, .length = new_length, .capacity_or_ref_ptr = capacity }; + } else { + const new_capacity = utils.calculateCapacity(capacity, new_length, element_width); + const new_source = utils.unsafeReallocate(source_ptr, alignment, capacity, new_capacity, element_width); + return RocList{ .bytes = new_source, .length = new_length, .capacity_or_ref_ptr = new_capacity }; + } + } + return self.reallocateFresh(alignment, new_length, element_width); + } + return RocList.allocate(alignment, new_length, element_width); + } + + /// reallocate by explicitly making a new allocation and copying elements over + fn reallocateFresh( + self: RocList, + alignment: u32, + new_length: usize, + element_width: usize, + ) RocList { + const old_length = self.length; + + const result = RocList.allocate(alignment, new_length, element_width); + + // transfer the memory + if (self.bytes) |source_ptr| { + const dest_ptr = result.bytes orelse unreachable; + + @memcpy(dest_ptr[0..(old_length * element_width)], source_ptr[0..(old_length * element_width)]); + @memset(dest_ptr[(old_length * element_width)..(new_length * element_width)], 0); + } + + self.decref(alignment); + + return result; + } +}; + +const Caller0 = *const fn (?[*]u8, ?[*]u8) callconv(.C) void; +const Caller1 = *const fn (?[*]u8, ?[*]u8, ?[*]u8) callconv(.C) void; +const Caller2 = *const fn (?[*]u8, ?[*]u8, ?[*]u8, ?[*]u8) callconv(.C) void; +const Caller3 = *const fn (?[*]u8, ?[*]u8, ?[*]u8, ?[*]u8, ?[*]u8) callconv(.C) void; +const Caller4 = *const fn (?[*]u8, ?[*]u8, ?[*]u8, ?[*]u8, ?[*]u8, ?[*]u8) callconv(.C) void; + +pub fn listMap( + list: RocList, + caller: Caller1, + data: Opaque, + inc_n_data: IncN, + data_is_owned: bool, + alignment: u32, + old_element_width: usize, + new_element_width: usize, +) callconv(.C) RocList { + if (list.bytes) |source_ptr| { + const size = list.len(); + var i: usize = 0; + const output = RocList.allocate(alignment, size, new_element_width); + const target_ptr = output.bytes orelse unreachable; + + if (data_is_owned) { + inc_n_data(data, size); + } + + while (i < size) : (i += 1) { + caller(data, source_ptr + (i * old_element_width), target_ptr + (i * new_element_width)); + } + + return output; + } else { + return RocList.empty(); + } +} + +fn decrementTail(list: RocList, start_index: usize, element_width: usize, dec: Dec) void { + if (list.bytes) |source| { + var i = start_index; + while (i < list.len()) : (i += 1) { + const element = source + i * element_width; + dec(element); + } + } +} + +pub fn listMap2( + list1: RocList, + list2: RocList, + caller: Caller2, + data: Opaque, + inc_n_data: IncN, + data_is_owned: bool, + alignment: u32, + a_width: usize, + b_width: usize, + c_width: usize, + dec_a: Dec, + dec_b: Dec, +) callconv(.C) RocList { + const output_length = @min(list1.len(), list2.len()); + + // if the lists don't have equal length, we must consume the remaining elements + // In this case we consume by (recursively) decrementing the elements + decrementTail(list1, output_length, a_width, dec_a); + decrementTail(list2, output_length, b_width, dec_b); + + if (data_is_owned) { + inc_n_data(data, output_length); + } + + if (list1.bytes) |source_a| { + if (list2.bytes) |source_b| { + const output = RocList.allocate(alignment, output_length, c_width); + const target_ptr = output.bytes orelse unreachable; + + var i: usize = 0; + while (i < output_length) : (i += 1) { + const element_a = source_a + i * a_width; + const element_b = source_b + i * b_width; + const target = target_ptr + i * c_width; + caller(data, element_a, element_b, target); + } + + return output; + } else { + return RocList.empty(); + } + } else { + return RocList.empty(); + } +} + +pub fn listMap3( + list1: RocList, + list2: RocList, + list3: RocList, + caller: Caller3, + data: Opaque, + inc_n_data: IncN, + data_is_owned: bool, + alignment: u32, + a_width: usize, + b_width: usize, + c_width: usize, + d_width: usize, + dec_a: Dec, + dec_b: Dec, + dec_c: Dec, +) callconv(.C) RocList { + const smaller_length = @min(list1.len(), list2.len()); + const output_length = @min(smaller_length, list3.len()); + + decrementTail(list1, output_length, a_width, dec_a); + decrementTail(list2, output_length, b_width, dec_b); + decrementTail(list3, output_length, c_width, dec_c); + + if (data_is_owned) { + inc_n_data(data, output_length); + } + + if (list1.bytes) |source_a| { + if (list2.bytes) |source_b| { + if (list3.bytes) |source_c| { + const output = RocList.allocate(alignment, output_length, d_width); + const target_ptr = output.bytes orelse unreachable; + + var i: usize = 0; + while (i < output_length) : (i += 1) { + const element_a = source_a + i * a_width; + const element_b = source_b + i * b_width; + const element_c = source_c + i * c_width; + const target = target_ptr + i * d_width; + + caller(data, element_a, element_b, element_c, target); + } + + return output; + } else { + return RocList.empty(); + } + } else { + return RocList.empty(); + } + } else { + return RocList.empty(); + } +} + +pub fn listMap4( + list1: RocList, + list2: RocList, + list3: RocList, + list4: RocList, + caller: Caller4, + data: Opaque, + inc_n_data: IncN, + data_is_owned: bool, + alignment: u32, + a_width: usize, + b_width: usize, + c_width: usize, + d_width: usize, + e_width: usize, + dec_a: Dec, + dec_b: Dec, + dec_c: Dec, + dec_d: Dec, +) callconv(.C) RocList { + const output_length = @min(@min(list1.len(), list2.len()), @min(list3.len(), list4.len())); + + decrementTail(list1, output_length, a_width, dec_a); + decrementTail(list2, output_length, b_width, dec_b); + decrementTail(list3, output_length, c_width, dec_c); + decrementTail(list4, output_length, d_width, dec_d); + + if (data_is_owned) { + inc_n_data(data, output_length); + } + + if (list1.bytes) |source_a| { + if (list2.bytes) |source_b| { + if (list3.bytes) |source_c| { + if (list4.bytes) |source_d| { + const output = RocList.allocate(alignment, output_length, e_width); + const target_ptr = output.bytes orelse unreachable; + + var i: usize = 0; + while (i < output_length) : (i += 1) { + const element_a = source_a + i * a_width; + const element_b = source_b + i * b_width; + const element_c = source_c + i * c_width; + const element_d = source_d + i * d_width; + const target = target_ptr + i * e_width; + + caller(data, element_a, element_b, element_c, element_d, target); + } + + return output; + } else { + return RocList.empty(); + } + } else { + return RocList.empty(); + } + } else { + return RocList.empty(); + } + } else { + return RocList.empty(); + } +} + +pub fn listWithCapacity( + capacity: usize, + alignment: u32, + element_width: usize, +) callconv(.C) RocList { + return listReserve(RocList.empty(), alignment, capacity, element_width, .InPlace); +} + +pub fn listReserve( + list: RocList, + alignment: u32, + spare: usize, + element_width: usize, + update_mode: UpdateMode, +) callconv(.C) RocList { + const old_length = list.len(); + if ((update_mode == .InPlace or list.isUnique()) and list.getCapacity() >= list.len() + spare) { + return list; + } else { + var output = list.reallocate(alignment, old_length + spare, element_width); + output.length = old_length; + return output; + } +} + +pub fn listReleaseExcessCapacity( + list: RocList, + alignment: u32, + element_width: usize, + update_mode: UpdateMode, +) callconv(.C) RocList { + const old_length = list.len(); + // We use the direct list.capacity_or_ref_ptr to make sure both that there is no extra capacity and that it isn't a seamless slice. + if ((update_mode == .InPlace or list.isUnique()) and list.capacity_or_ref_ptr == old_length) { + return list; + } else if (old_length == 0) { + list.decref(alignment); + return RocList.empty(); + } else { + var output = RocList.allocateExact(alignment, old_length, element_width); + if (list.bytes) |source_ptr| { + const dest_ptr = output.bytes orelse unreachable; + + @memcpy(dest_ptr[0..(old_length * element_width)], source_ptr[0..(old_length * element_width)]); + } + list.decref(alignment); + return output; + } +} + +pub fn listAppendUnsafe( + list: RocList, + element: Opaque, + element_width: usize, +) callconv(.C) RocList { + const old_length = list.len(); + var output = list; + output.length += 1; + + if (output.bytes) |bytes| { + if (element) |source| { + const target = bytes + old_length * element_width; + @memcpy(target[0..element_width], source[0..element_width]); + } + } + + return output; +} + +fn listAppend(list: RocList, alignment: u32, element: Opaque, element_width: usize, update_mode: UpdateMode) callconv(.C) RocList { + const with_capacity = listReserve(list, alignment, 1, element_width, update_mode); + return listAppendUnsafe(with_capacity, element, element_width); +} + +pub fn listPrepend(list: RocList, alignment: u32, element: Opaque, element_width: usize) callconv(.C) RocList { + const old_length = list.len(); + // TODO: properly wire in update mode. + var with_capacity = listReserve(list, alignment, 1, element_width, .Immutable); + with_capacity.length += 1; + + // can't use one memcpy here because source and target overlap + if (with_capacity.bytes) |target| { + var i: usize = old_length; + + while (i > 0) { + i -= 1; + + // move the ith element to the (i + 1)th position + const to = target + (i + 1) * element_width; + const from = target + i * element_width; + @memcpy(to[0..element_width], from[0..element_width]); + } + + // finally copy in the new first element + if (element) |source| { + @memcpy(target[0..element_width], source[0..element_width]); + } + } + + return with_capacity; +} + +pub fn listSwap( + list: RocList, + alignment: u32, + element_width: usize, + index_1: usize, + index_2: usize, + update_mode: UpdateMode, +) callconv(.C) RocList { + const size = list.len(); + if (index_1 == index_2 or index_1 >= size or index_2 >= size) { + // Either index out of bounds so we just return + return list; + } + + const newList = blk: { + if (update_mode == .InPlace) { + break :blk list; + } else { + break :blk list.makeUnique(alignment, element_width); + } + }; + + const source_ptr = @as([*]u8, @ptrCast(newList.bytes)); + swapElements(source_ptr, element_width, index_1, index_2); + + return newList; +} + +pub fn listSublist( + list: RocList, + alignment: u32, + element_width: usize, + start: usize, + len: usize, + dec: Dec, +) callconv(.C) RocList { + const size = list.len(); + if (len == 0 or start >= size) { + // Decrement the reference counts of all elements. + if (list.bytes) |source_ptr| { + var i: usize = 0; + while (i < size) : (i += 1) { + const element = source_ptr + i * element_width; + dec(element); + } + } + if (list.isUnique()) { + var output = list; + output.length = 0; + return output; + } + list.decref(alignment); + return RocList.empty(); + } + + if (list.bytes) |source_ptr| { + const keep_len = @min(len, size - start); + const drop_start_len = start; + const drop_end_len = size - (start + keep_len); + + // Decrement the reference counts of elements before `start`. + var i: usize = 0; + while (i < drop_start_len) : (i += 1) { + const element = source_ptr + i * element_width; + dec(element); + } + + // Decrement the reference counts of elements after `start + keep_len`. + i = 0; + while (i < drop_end_len) : (i += 1) { + const element = source_ptr + (start + keep_len + i) * element_width; + dec(element); + } + + if (start == 0 and list.isUnique()) { + var output = list; + output.length = keep_len; + return output; + } else { + const list_ref_ptr = (@intFromPtr(source_ptr) >> 1) | SEAMLESS_SLICE_BIT; + const slice_ref_ptr = list.capacity_or_ref_ptr; + const slice_mask = list.seamlessSliceMask(); + const ref_ptr = (list_ref_ptr & ~slice_mask) | (slice_ref_ptr & slice_mask); + return RocList{ + .bytes = source_ptr + start * element_width, + .length = keep_len, + .capacity_or_ref_ptr = ref_ptr, + }; + } + } + + return RocList.empty(); +} + +pub fn listDropAt( + list: RocList, + alignment: u32, + element_width: usize, + drop_index: usize, + dec: Dec, +) callconv(.C) RocList { + const size = list.len(); + // If droping the first or last element, return a seamless slice. + // For simplicity, do this by calling listSublist. + // In the future, we can test if it is faster to manually inline the important parts here. + if (drop_index == 0) { + return listSublist(list, alignment, element_width, 1, size -| 1, dec); + } else if (drop_index == size -| 1) { + return listSublist(list, alignment, element_width, 0, size -| 1, dec); + } + + if (list.bytes) |source_ptr| { + if (drop_index >= size) { + return list; + } + + if (drop_index < size) { + const element = source_ptr + drop_index * element_width; + dec(element); + } + + // NOTE + // we need to return an empty list explicitly, + // because we rely on the pointer field being null if the list is empty + // which also requires duplicating the utils.decref call to spend the RC token + if (size < 2) { + list.decref(alignment); + return RocList.empty(); + } + + if (list.isUnique()) { + var i = drop_index; + while (i < size - 1) : (i += 1) { + const copy_target = source_ptr + i * element_width; + const copy_source = copy_target + element_width; + + @memcpy(copy_target[0..element_width], copy_source[0..element_width]); + } + + var new_list = list; + + new_list.length -= 1; + return new_list; + } + + const output = RocList.allocate(alignment, size - 1, element_width); + const target_ptr = output.bytes orelse unreachable; + + const head_size = drop_index * element_width; + @memcpy(target_ptr[0..head_size], source_ptr[0..head_size]); + + const tail_target = target_ptr + drop_index * element_width; + const tail_source = source_ptr + (drop_index + 1) * element_width; + const tail_size = (size - drop_index - 1) * element_width; + @memcpy(tail_target[0..tail_size], tail_source[0..tail_size]); + + list.decref(alignment); + + return output; + } else { + return RocList.empty(); + } +} + +fn partition(source_ptr: [*]u8, transform: Opaque, wrapper: CompareFn, element_width: usize, low: isize, high: isize) isize { + const pivot = source_ptr + (@as(usize, @intCast(high)) * element_width); + var i = (low - 1); // Index of smaller element and indicates the right position of pivot found so far + var j = low; + + while (j <= high - 1) : (j += 1) { + const current_elem = source_ptr + (@as(usize, @intCast(j)) * element_width); + + const ordering = wrapper(transform, current_elem, pivot); + const order = @as(utils.Ordering, @enumFromInt(ordering)); + + switch (order) { + utils.Ordering.LT => { + // the current element is smaller than the pivot; swap it + i += 1; + swapElements(source_ptr, element_width, @as(usize, @intCast(i)), @as(usize, @intCast(j))); + }, + utils.Ordering.EQ, utils.Ordering.GT => {}, + } + } + swapElements(source_ptr, element_width, @as(usize, @intCast(i + 1)), @as(usize, @intCast(high))); + return (i + 1); +} + +fn quicksort(source_ptr: [*]u8, transform: Opaque, wrapper: CompareFn, element_width: usize, low: isize, high: isize) void { + if (low < high) { + // partition index + const pi = partition(source_ptr, transform, wrapper, element_width, low, high); + + _ = quicksort(source_ptr, transform, wrapper, element_width, low, pi - 1); // before pi + _ = quicksort(source_ptr, transform, wrapper, element_width, pi + 1, high); // after pi + } +} + +pub fn listSortWith( + input: RocList, + caller: CompareFn, + data: Opaque, + inc_n_data: IncN, + data_is_owned: bool, + alignment: u32, + element_width: usize, +) callconv(.C) RocList { + var list = input.makeUnique(alignment, element_width); + + if (data_is_owned) { + inc_n_data(data, list.len()); + } + + if (list.bytes) |source_ptr| { + const low = 0; + const high: isize = @as(isize, @intCast(list.len())) - 1; + quicksort(source_ptr, data, caller, element_width, low, high); + } + + return list; +} + +// SWAP ELEMENTS + +inline fn swapHelp(width: usize, temporary: [*]u8, ptr1: [*]u8, ptr2: [*]u8) void { + @memcpy(temporary[0..width], ptr1[0..width]); + @memcpy(ptr1[0..width], ptr2[0..width]); + @memcpy(ptr2[0..width], temporary[0..width]); +} + +fn swap(width_initial: usize, p1: [*]u8, p2: [*]u8) void { + const threshold: usize = 64; + + var width = width_initial; + + var ptr1 = p1; + var ptr2 = p2; + + var buffer_actual: [threshold]u8 = undefined; + var buffer: [*]u8 = buffer_actual[0..]; + + while (true) { + if (width < threshold) { + swapHelp(width, buffer, ptr1, ptr2); + return; + } else { + swapHelp(threshold, buffer, ptr1, ptr2); + + ptr1 += threshold; + ptr2 += threshold; + + width -= threshold; + } + } +} + +fn swapElements(source_ptr: [*]u8, element_width: usize, index_1: usize, index_2: usize) void { + var element_at_i = source_ptr + (index_1 * element_width); + var element_at_j = source_ptr + (index_2 * element_width); + + return swap(element_width, element_at_i, element_at_j); +} + +pub fn listConcat(list_a: RocList, list_b: RocList, alignment: u32, element_width: usize) callconv(.C) RocList { + // NOTE we always use list_a! because it is owned, we must consume it, and it may have unused capacity + if (list_b.isEmpty()) { + if (list_a.getCapacity() == 0) { + // a could be a seamless slice, so we still need to decref. + list_a.decref(alignment); + return list_b; + } else { + // we must consume this list. Even though it has no elements, it could still have capacity + list_b.decref(alignment); + + return list_a; + } + } else if (list_a.isUnique()) { + const total_length: usize = list_a.len() + list_b.len(); + + const resized_list_a = list_a.reallocate(alignment, total_length, element_width); + + // These must exist, otherwise, the lists would have been empty. + const source_a = resized_list_a.bytes orelse unreachable; + const source_b = list_b.bytes orelse unreachable; + @memcpy(source_a[(list_a.len() * element_width)..(total_length * element_width)], source_b[0..(list_b.len() * element_width)]); + + // decrement list b. + list_b.decref(alignment); + + return resized_list_a; + } else if (list_b.isUnique()) { + const total_length: usize = list_a.len() + list_b.len(); + + const resized_list_b = list_b.reallocate(alignment, total_length, element_width); + + // These must exist, otherwise, the lists would have been empty. + const source_a = list_a.bytes orelse unreachable; + const source_b = resized_list_b.bytes orelse unreachable; + + // This is a bit special, we need to first copy the elements of list_b to the end, + // then copy the elements of list_a to the beginning. + // This first call must use mem.copy because the slices might overlap. + const byte_count_a = list_a.len() * element_width; + const byte_count_b = list_b.len() * element_width; + mem.copyBackwards(u8, source_b[byte_count_a .. byte_count_a + byte_count_b], source_b[0..byte_count_b]); + @memcpy(source_b[0..byte_count_a], source_a[0..byte_count_a]); + + // decrement list a. + list_a.decref(alignment); + + return resized_list_b; + } + const total_length: usize = list_a.len() + list_b.len(); + + const output = RocList.allocate(alignment, total_length, element_width); + + // These must exist, otherwise, the lists would have been empty. + const target = output.bytes orelse unreachable; + const source_a = list_a.bytes orelse unreachable; + const source_b = list_b.bytes orelse unreachable; + + @memcpy(target[0..(list_a.len() * element_width)], source_a[0..(list_a.len() * element_width)]); + @memcpy(target[(list_a.len() * element_width)..(total_length * element_width)], source_b[0..(list_b.len() * element_width)]); + + // decrement list a and b. + list_a.decref(alignment); + list_b.decref(alignment); + + return output; +} + +pub fn listReplaceInPlace( + list: RocList, + index: usize, + element: Opaque, + element_width: usize, + out_element: ?[*]u8, +) callconv(.C) RocList { + // INVARIANT: bounds checking happens on the roc side + // + // at the time of writing, the function is implemented roughly as + // `if inBounds then LowLevelListReplace input index item else input` + // so we don't do a bounds check here. Hence, the list is also non-empty, + // because inserting into an empty list is always out of bounds + return listReplaceInPlaceHelp(list, index, element, element_width, out_element); +} + +pub fn listReplace( + list: RocList, + alignment: u32, + index: usize, + element: Opaque, + element_width: usize, + out_element: ?[*]u8, +) callconv(.C) RocList { + // INVARIANT: bounds checking happens on the roc side + // + // at the time of writing, the function is implemented roughly as + // `if inBounds then LowLevelListReplace input index item else input` + // so we don't do a bounds check here. Hence, the list is also non-empty, + // because inserting into an empty list is always out of bounds + return listReplaceInPlaceHelp(list.makeUnique(alignment, element_width), index, element, element_width, out_element); +} + +inline fn listReplaceInPlaceHelp( + list: RocList, + index: usize, + element: Opaque, + element_width: usize, + out_element: ?[*]u8, +) RocList { + // the element we will replace + var element_at_index = (list.bytes orelse unreachable) + (index * element_width); + + // copy out the old element + @memcpy((out_element orelse unreachable)[0..element_width], element_at_index[0..element_width]); + + // copy in the new element + @memcpy(element_at_index[0..element_width], (element orelse unreachable)[0..element_width]); + + return list; +} + +pub fn listIsUnique( + list: RocList, +) callconv(.C) bool { + return list.isEmpty() or list.isUnique(); +} + +pub fn listCapacity( + list: RocList, +) callconv(.C) usize { + return list.getCapacity(); +} + +pub fn listRefcountPtr( + list: RocList, +) callconv(.C) ?[*]u8 { + return list.getRefcountPtr(); +} + +test "listConcat: non-unique with unique overlapping" { + var nonUnique = RocList.fromSlice(u8, ([_]u8{1})[0..]); + var bytes: [*]u8 = @as([*]u8, @ptrCast(nonUnique.bytes)); + const ptr_width = @sizeOf(usize); + const refcount_ptr = @as([*]isize, @ptrCast(@as([*]align(ptr_width) u8, @alignCast(bytes)) - ptr_width)); + utils.increfRcPtrC(&refcount_ptr[0], 1); + defer nonUnique.decref(@sizeOf(u8)); // listConcat will dec the other refcount + + var unique = RocList.fromSlice(u8, ([_]u8{ 2, 3, 4 })[0..]); + defer unique.decref(@sizeOf(u8)); + + var concatted = listConcat(nonUnique, unique, 1, 1); + var wanted = RocList.fromSlice(u8, ([_]u8{ 1, 2, 3, 4 })[0..]); + defer wanted.decref(@sizeOf(u8)); + + try expect(concatted.eql(wanted)); +} diff --git a/crates/compiler/builtins/bitcode/src/main.zig b/crates/compiler/builtins/bitcode/src/main.zig new file mode 100644 index 0000000000..d631d01c37 --- /dev/null +++ b/crates/compiler/builtins/bitcode/src/main.zig @@ -0,0 +1,325 @@ +const std = @import("std"); +const builtin = @import("builtin"); +const math = std.math; +const utils = @import("utils.zig"); +const expect = @import("expect.zig"); +const panic_utils = @import("panic.zig"); + +comptime { + _ = @import("compiler_rt.zig"); + _ = @import("libc.zig"); +} + +const ROC_BUILTINS = "roc_builtins"; +const NUM = "num"; +const STR = "str"; + +// Dec Module +const dec = @import("dec.zig"); + +comptime { + exportDecFn(dec.absC, "abs"); + exportDecFn(dec.acosC, "acos"); + exportDecFn(dec.addC, "add_with_overflow"); + exportDecFn(dec.addOrPanicC, "add_or_panic"); + exportDecFn(dec.addSaturatedC, "add_saturated"); + exportDecFn(dec.asinC, "asin"); + exportDecFn(dec.atanC, "atan"); + exportDecFn(dec.cosC, "cos"); + exportDecFn(dec.divC, "div"); + exportDecFn(dec.eqC, "eq"); + exportDecFn(dec.fromF32C, "from_float.f32"); + exportDecFn(dec.fromF64C, "from_float.f64"); + exportDecFn(dec.fromStr, "from_str"); + exportDecFn(dec.fromU64C, "from_u64"); + exportDecFn(dec.logC, "log"); + exportDecFn(dec.mulC, "mul_with_overflow"); + exportDecFn(dec.mulOrPanicC, "mul_or_panic"); + exportDecFn(dec.mulSaturatedC, "mul_saturated"); + exportDecFn(dec.negateC, "negate"); + exportDecFn(dec.neqC, "neq"); + exportDecFn(dec.sinC, "sin"); + exportDecFn(dec.subC, "sub_with_overflow"); + exportDecFn(dec.subOrPanicC, "sub_or_panic"); + exportDecFn(dec.subSaturatedC, "sub_saturated"); + exportDecFn(dec.tanC, "tan"); + exportDecFn(dec.toF64, "to_f64"); + exportDecFn(dec.toI128, "to_i128"); + exportDecFn(dec.toStr, "to_str"); + + inline for (INTEGERS) |T| { + dec.exportFromInt(T, ROC_BUILTINS ++ ".dec.from_int."); + } +} + +// List Module +const list = @import("list.zig"); + +comptime { + exportListFn(list.listMap, "map"); + exportListFn(list.listMap2, "map2"); + exportListFn(list.listMap3, "map3"); + exportListFn(list.listMap4, "map4"); + exportListFn(list.listAppendUnsafe, "append_unsafe"); + exportListFn(list.listReserve, "reserve"); + exportListFn(list.listPrepend, "prepend"); + exportListFn(list.listWithCapacity, "with_capacity"); + exportListFn(list.listSortWith, "sort_with"); + exportListFn(list.listConcat, "concat"); + exportListFn(list.listSublist, "sublist"); + exportListFn(list.listDropAt, "drop_at"); + exportListFn(list.listReplace, "replace"); + exportListFn(list.listReplaceInPlace, "replace_in_place"); + exportListFn(list.listSwap, "swap"); + exportListFn(list.listIsUnique, "is_unique"); + exportListFn(list.listCapacity, "capacity"); + exportListFn(list.listRefcountPtr, "refcount_ptr"); + exportListFn(list.listReleaseExcessCapacity, "release_excess_capacity"); +} + +// Num Module +const num = @import("num.zig"); + +const INTEGERS = [_]type{ i8, i16, i32, i64, i128, u8, u16, u32, u64, u128 }; +const WIDEINTS = [_]type{ i16, i32, i64, i128, i256, u16, u32, u64, u128, u256 }; +const FLOATS = [_]type{ f32, f64 }; +const NUMBERS = INTEGERS ++ FLOATS; + +comptime { + exportNumFn(num.bytesToU16C, "bytes_to_u16"); + exportNumFn(num.bytesToU32C, "bytes_to_u32"); + exportNumFn(num.bytesToU64C, "bytes_to_u64"); + exportNumFn(num.bytesToU128C, "bytes_to_u128"); + + exportNumFn(num.shiftRightZeroFillI128, "shift_right_zero_fill.i128"); + exportNumFn(num.shiftRightZeroFillU128, "shift_right_zero_fill.u128"); + + exportNumFn(num.compareI128, "compare.i128"); + exportNumFn(num.compareU128, "compare.u128"); + + exportNumFn(num.lessThanI128, "less_than.i128"); + exportNumFn(num.lessThanOrEqualI128, "less_than_or_equal.i128"); + exportNumFn(num.greaterThanI128, "greater_than.i128"); + exportNumFn(num.greaterThanOrEqualI128, "greater_than_or_equal.i128"); + + exportNumFn(num.lessThanU128, "less_than.u128"); + exportNumFn(num.lessThanOrEqualU128, "less_than_or_equal.u128"); + exportNumFn(num.greaterThanU128, "greater_than.u128"); + exportNumFn(num.greaterThanOrEqualU128, "greater_than_or_equal.u128"); + + exportNumFn(num.compareI128, "compare.i128"); + exportNumFn(num.compareU128, "compare.u128"); + + exportNumFn(num.lessThanI128, "less_than.i128"); + exportNumFn(num.lessThanOrEqualI128, "less_than_or_equal.i128"); + exportNumFn(num.greaterThanI128, "greater_than.i128"); + exportNumFn(num.greaterThanOrEqualI128, "greater_than_or_equal.i128"); + + exportNumFn(num.lessThanU128, "less_than.u128"); + exportNumFn(num.lessThanOrEqualU128, "less_than_or_equal.u128"); + exportNumFn(num.greaterThanU128, "greater_than.u128"); + exportNumFn(num.greaterThanOrEqualU128, "greater_than_or_equal.u128"); + + inline for (INTEGERS, 0..) |T, i| { + num.exportPow(T, ROC_BUILTINS ++ "." ++ NUM ++ ".pow_int."); + num.exportDivCeil(T, ROC_BUILTINS ++ "." ++ NUM ++ ".div_ceil."); + + num.exportRound(f32, T, ROC_BUILTINS ++ "." ++ NUM ++ ".round_f32."); + num.exportRound(f64, T, ROC_BUILTINS ++ "." ++ NUM ++ ".round_f64."); + num.exportFloor(f32, T, ROC_BUILTINS ++ "." ++ NUM ++ ".floor_f32."); + num.exportFloor(f64, T, ROC_BUILTINS ++ "." ++ NUM ++ ".floor_f64."); + num.exportCeiling(f32, T, ROC_BUILTINS ++ "." ++ NUM ++ ".ceiling_f32."); + num.exportCeiling(f64, T, ROC_BUILTINS ++ "." ++ NUM ++ ".ceiling_f64."); + + num.exportAddWithOverflow(T, ROC_BUILTINS ++ "." ++ NUM ++ ".add_with_overflow."); + num.exportAddOrPanic(T, ROC_BUILTINS ++ "." ++ NUM ++ ".add_or_panic."); + num.exportAddSaturatedInt(T, ROC_BUILTINS ++ "." ++ NUM ++ ".add_saturated."); + + num.exportSubWithOverflow(T, ROC_BUILTINS ++ "." ++ NUM ++ ".sub_with_overflow."); + num.exportSubOrPanic(T, ROC_BUILTINS ++ "." ++ NUM ++ ".sub_or_panic."); + num.exportSubSaturatedInt(T, ROC_BUILTINS ++ "." ++ NUM ++ ".sub_saturated."); + + num.exportMulWithOverflow(T, WIDEINTS[i], ROC_BUILTINS ++ "." ++ NUM ++ ".mul_with_overflow."); + num.exportMulOrPanic(T, WIDEINTS[i], ROC_BUILTINS ++ "." ++ NUM ++ ".mul_or_panic."); + num.exportMulSaturatedInt(T, WIDEINTS[i], ROC_BUILTINS ++ "." ++ NUM ++ ".mul_saturated."); + num.exportMulWrappedInt(T, ROC_BUILTINS ++ "." ++ NUM ++ ".mul_wrapped."); + + num.exportIsMultipleOf(T, ROC_BUILTINS ++ "." ++ NUM ++ ".is_multiple_of."); + + num.exportCountLeadingZeroBits(T, ROC_BUILTINS ++ "." ++ NUM ++ ".count_leading_zero_bits."); + num.exportCountTrailingZeroBits(T, ROC_BUILTINS ++ "." ++ NUM ++ ".count_trailing_zero_bits."); + num.exportCountOneBits(T, ROC_BUILTINS ++ "." ++ NUM ++ ".count_one_bits."); + } + + inline for (INTEGERS) |FROM| { + inline for (INTEGERS) |TO| { + // We're exporting more than we need here, but that's okay. + num.exportToIntCheckingMax(FROM, TO, ROC_BUILTINS ++ "." ++ NUM ++ ".int_to_" ++ @typeName(TO) ++ "_checking_max."); + num.exportToIntCheckingMaxAndMin(FROM, TO, ROC_BUILTINS ++ "." ++ NUM ++ ".int_to_" ++ @typeName(TO) ++ "_checking_max_and_min."); + } + } + + inline for (FLOATS) |T| { + num.exportAsin(T, ROC_BUILTINS ++ "." ++ NUM ++ ".asin."); + num.exportAcos(T, ROC_BUILTINS ++ "." ++ NUM ++ ".acos."); + num.exportAtan(T, ROC_BUILTINS ++ "." ++ NUM ++ ".atan."); + + num.exportSin(T, ROC_BUILTINS ++ "." ++ NUM ++ ".sin."); + num.exportCos(T, ROC_BUILTINS ++ "." ++ NUM ++ ".cos."); + num.exportTan(T, ROC_BUILTINS ++ "." ++ NUM ++ ".tan."); + + num.exportPow(T, ROC_BUILTINS ++ "." ++ NUM ++ ".pow."); + num.exportLog(T, ROC_BUILTINS ++ "." ++ NUM ++ ".log."); + num.exportFAbs(T, ROC_BUILTINS ++ "." ++ NUM ++ ".fabs."); + num.exportSqrt(T, ROC_BUILTINS ++ "." ++ NUM ++ ".sqrt."); + + num.exportAddWithOverflow(T, ROC_BUILTINS ++ "." ++ NUM ++ ".add_with_overflow."); + num.exportSubWithOverflow(T, ROC_BUILTINS ++ "." ++ NUM ++ ".sub_with_overflow."); + num.exportMulWithOverflow(T, T, ROC_BUILTINS ++ "." ++ NUM ++ ".mul_with_overflow."); + + num.exportIsNan(T, ROC_BUILTINS ++ "." ++ NUM ++ ".is_nan."); + num.exportIsInfinite(T, ROC_BUILTINS ++ "." ++ NUM ++ ".is_infinite."); + num.exportIsFinite(T, ROC_BUILTINS ++ "." ++ NUM ++ ".is_finite."); + } +} + +// Str Module +const str = @import("str.zig"); +comptime { + exportStrFn(str.init, "init"); + exportStrFn(str.strToScalarsC, "to_scalars"); + exportStrFn(str.strSplit, "str_split"); + exportStrFn(str.countSegments, "count_segments"); + exportStrFn(str.countGraphemeClusters, "count_grapheme_clusters"); + exportStrFn(str.countUtf8Bytes, "count_utf8_bytes"); + exportStrFn(str.isEmpty, "is_empty"); + exportStrFn(str.getCapacity, "capacity"); + exportStrFn(str.startsWith, "starts_with"); + exportStrFn(str.startsWithScalar, "starts_with_scalar"); + exportStrFn(str.endsWith, "ends_with"); + exportStrFn(str.strConcatC, "concat"); + exportStrFn(str.strJoinWithC, "joinWith"); + exportStrFn(str.strNumberOfBytes, "number_of_bytes"); + exportStrFn(str.strEqual, "equal"); + exportStrFn(str.substringUnsafe, "substring_unsafe"); + exportStrFn(str.getUnsafe, "get_unsafe"); + exportStrFn(str.reserve, "reserve"); + exportStrFn(str.getScalarUnsafe, "get_scalar_unsafe"); + exportStrFn(str.appendScalar, "append_scalar"); + exportStrFn(str.strToUtf8C, "to_utf8"); + exportStrFn(str.fromUtf8RangeC, "from_utf8_range"); + exportStrFn(str.repeat, "repeat"); + exportStrFn(str.strTrim, "trim"); + exportStrFn(str.strTrimStart, "trim_start"); + exportStrFn(str.strTrimEnd, "trim_end"); + exportStrFn(str.strCloneTo, "clone_to"); + exportStrFn(str.withCapacity, "with_capacity"); + exportStrFn(str.strGraphemes, "graphemes"); + exportStrFn(str.strRefcountPtr, "refcount_ptr"); + exportStrFn(str.strReleaseExcessCapacity, "release_excess_capacity"); + + inline for (INTEGERS) |T| { + str.exportFromInt(T, ROC_BUILTINS ++ "." ++ STR ++ ".from_int."); + num.exportParseInt(T, ROC_BUILTINS ++ "." ++ STR ++ ".to_int."); + } + + inline for (FLOATS) |T| { + str.exportFromFloat(T, ROC_BUILTINS ++ "." ++ STR ++ ".from_float."); + num.exportParseFloat(T, ROC_BUILTINS ++ "." ++ STR ++ ".to_float."); + } +} + +// Utils +comptime { + exportUtilsFn(utils.test_dbg, "test_dbg"); + exportUtilsFn(utils.test_panic, "test_panic"); + exportUtilsFn(utils.increfRcPtrC, "incref_rc_ptr"); + exportUtilsFn(utils.decrefRcPtrC, "decref_rc_ptr"); + exportUtilsFn(utils.freeRcPtrC, "free_rc_ptr"); + exportUtilsFn(utils.increfDataPtrC, "incref_data_ptr"); + exportUtilsFn(utils.decrefDataPtrC, "decref_data_ptr"); + exportUtilsFn(utils.freeDataPtrC, "free_data_ptr"); + exportUtilsFn(utils.isUnique, "is_unique"); + exportUtilsFn(utils.decrefCheckNullC, "decref_check_null"); + exportUtilsFn(utils.allocateWithRefcountC, "allocate_with_refcount"); + exportUtilsFn(utils.dictPseudoSeed, "dict_pseudo_seed"); + + @export(panic_utils.panic, .{ .name = "roc_builtins.utils." ++ "panic", .linkage = .Weak }); + + if (builtin.target.cpu.arch != .wasm32) { + exportUtilsFn(expect.expectFailedStartSharedBuffer, "expect_failed_start_shared_buffer"); + exportUtilsFn(expect.expectFailedStartSharedFile, "expect_failed_start_shared_file"); + exportUtilsFn(expect.notifyParentExpect, "notify_parent_expect"); + + // sets the buffer used for expect failures + @export(expect.setSharedBuffer, .{ .name = "set_shared_buffer", .linkage = .Weak }); + + exportUtilsFn(expect.readSharedBufferEnv, "read_env_shared_buffer"); + } + + if (builtin.target.cpu.arch == .aarch64) { + @export(__roc_force_setjmp, .{ .name = "__roc_force_setjmp", .linkage = .Weak }); + @export(__roc_force_longjmp, .{ .name = "__roc_force_longjmp", .linkage = .Weak }); + } +} + +// Utils continued - SJLJ +// For tests (in particular test_gen), roc_panic is implemented in terms of +// setjmp/longjmp. LLVM is unable to generate code for longjmp on AArch64 (https://github.com/roc-lang/roc/issues/2965), +// so instead we ask Zig to please provide implementations for us, which is does +// (seemingly via musl). +pub extern fn setjmp([*c]c_int) c_int; +pub extern fn longjmp([*c]c_int, c_int) noreturn; +pub extern fn _setjmp([*c]c_int) c_int; +pub extern fn _longjmp([*c]c_int, c_int) noreturn; +pub extern fn sigsetjmp([*c]c_int, c_int) c_int; +pub extern fn siglongjmp([*c]c_int, c_int) noreturn; +pub extern fn longjmperror() void; +// Zig won't expose the externs (and hence link correctly) unless we force them to be used. +fn __roc_force_setjmp(it: [*c]c_int) callconv(.C) c_int { + return setjmp(it); +} +fn __roc_force_longjmp(a0: [*c]c_int, a1: c_int) callconv(.C) noreturn { + longjmp(a0, a1); +} + +// Export helpers - Must be run inside a comptime +fn exportBuiltinFn(comptime func: anytype, comptime func_name: []const u8) void { + @export(func, .{ .name = "roc_builtins." ++ func_name, .linkage = .Strong }); +} +fn exportNumFn(comptime func: anytype, comptime func_name: []const u8) void { + exportBuiltinFn(func, "num." ++ func_name); +} +fn exportStrFn(comptime func: anytype, comptime func_name: []const u8) void { + exportBuiltinFn(func, "str." ++ func_name); +} +fn exportDictFn(comptime func: anytype, comptime func_name: []const u8) void { + exportBuiltinFn(func, "dict." ++ func_name); +} +fn exportListFn(comptime func: anytype, comptime func_name: []const u8) void { + exportBuiltinFn(func, "list." ++ func_name); +} +fn exportDecFn(comptime func: anytype, comptime func_name: []const u8) void { + exportBuiltinFn(func, "dec." ++ func_name); +} + +fn exportUtilsFn(comptime func: anytype, comptime func_name: []const u8) void { + exportBuiltinFn(func, "utils." ++ func_name); +} + +// Custom panic function, as builtin Zig version errors during LLVM verification +pub fn panic(message: []const u8, stacktrace: ?*std.builtin.StackTrace, _: ?usize) noreturn { + if (builtin.is_test) { + std.debug.print("{s}: {?}", .{ message, stacktrace }); + } + + unreachable; +} + +// Run all tests in imported modules +// https://github.com/ziglang/zig/blob/master/lib/std/std.zig#L94 +test { + const testing = std.testing; + + testing.refAllDecls(@This()); +} diff --git a/crates/compiler/builtins/bitcode/src/num.zig b/crates/compiler/builtins/bitcode/src/num.zig new file mode 100644 index 0000000000..c978af99e1 --- /dev/null +++ b/crates/compiler/builtins/bitcode/src/num.zig @@ -0,0 +1,660 @@ +const std = @import("std"); +const math = std.math; +const RocList = @import("list.zig").RocList; +const RocStr = @import("str.zig").RocStr; +const WithOverflow = @import("utils.zig").WithOverflow; +const Ordering = @import("utils.zig").Ordering; +const roc_panic = @import("panic.zig").panic_help; + +pub fn NumParseResult(comptime T: type) type { + // on the roc side we sort by alignment; putting the errorcode last + // always works out (no number with smaller alignment than 1) + return extern struct { + value: T, + errorcode: u8, // 0 indicates success + }; +} + +pub const U256 = struct { + hi: u128, + lo: u128, +}; + +pub fn mul_u128(a: u128, b: u128) U256 { + var hi: u128 = undefined; + var lo: u128 = undefined; + + const bits_in_dword_2: u32 = 64; + const lower_mask: u128 = math.maxInt(u128) >> bits_in_dword_2; + + lo = (a & lower_mask) * (b & lower_mask); + + var t = lo >> bits_in_dword_2; + + lo &= lower_mask; + + t += (a >> bits_in_dword_2) * (b & lower_mask); + + lo += (t & lower_mask) << bits_in_dword_2; + + hi = t >> bits_in_dword_2; + + t = lo >> bits_in_dword_2; + + lo &= lower_mask; + + t += (b >> bits_in_dword_2) * (a & lower_mask); + + lo += (t & lower_mask) << bits_in_dword_2; + + hi += t >> bits_in_dword_2; + + hi += (a >> bits_in_dword_2) * (b >> bits_in_dword_2); + + return .{ .hi = hi, .lo = lo }; +} + +pub fn exportParseInt(comptime T: type, comptime name: []const u8) void { + comptime var f = struct { + fn func(buf: RocStr) callconv(.C) NumParseResult(T) { + // a radix of 0 will make zig determine the radix from the frefix: + // * A prefix of "0b" implies radix=2, + // * A prefix of "0o" implies radix=8, + // * A prefix of "0x" implies radix=16, + // * Otherwise radix=10 is assumed. + const radix = 0; + if (std.fmt.parseInt(T, buf.asSlice(), radix)) |success| { + return .{ .errorcode = 0, .value = success }; + } else |_| { + return .{ .errorcode = 1, .value = 0 }; + } + } + }.func; + @export(f, .{ .name = name ++ @typeName(T), .linkage = .Strong }); +} + +pub fn exportParseFloat(comptime T: type, comptime name: []const u8) void { + comptime var f = struct { + fn func(buf: RocStr) callconv(.C) NumParseResult(T) { + if (std.fmt.parseFloat(T, buf.asSlice())) |success| { + return .{ .errorcode = 0, .value = success }; + } else |_| { + return .{ .errorcode = 1, .value = 0 }; + } + } + }.func; + @export(f, .{ .name = name ++ @typeName(T), .linkage = .Strong }); +} + +pub fn exportPow(comptime T: type, comptime name: []const u8) void { + comptime var f = struct { + fn func(base: T, exp: T) callconv(.C) T { + return std.math.pow(T, base, exp); + } + }.func; + @export(f, .{ .name = name ++ @typeName(T), .linkage = .Strong }); +} + +pub fn exportIsNan(comptime T: type, comptime name: []const u8) void { + comptime var f = struct { + fn func(input: T) callconv(.C) bool { + return std.math.isNan(input); + } + }.func; + @export(f, .{ .name = name ++ @typeName(T), .linkage = .Strong }); +} + +pub fn exportIsInfinite(comptime T: type, comptime name: []const u8) void { + comptime var f = struct { + fn func(input: T) callconv(.C) bool { + return std.math.isInf(input); + } + }.func; + @export(f, .{ .name = name ++ @typeName(T), .linkage = .Strong }); +} + +pub fn exportIsFinite(comptime T: type, comptime name: []const u8) void { + comptime var f = struct { + fn func(input: T) callconv(.C) bool { + return std.math.isFinite(input); + } + }.func; + @export(f, .{ .name = name ++ @typeName(T), .linkage = .Strong }); +} + +pub fn exportAsin(comptime T: type, comptime name: []const u8) void { + comptime var f = struct { + fn func(input: T) callconv(.C) T { + return std.math.asin(input); + } + }.func; + @export(f, .{ .name = name ++ @typeName(T), .linkage = .Strong }); +} + +pub fn exportAcos(comptime T: type, comptime name: []const u8) void { + comptime var f = struct { + fn func(input: T) callconv(.C) T { + return std.math.acos(input); + } + }.func; + @export(f, .{ .name = name ++ @typeName(T), .linkage = .Strong }); +} + +pub fn exportAtan(comptime T: type, comptime name: []const u8) void { + comptime var f = struct { + fn func(input: T) callconv(.C) T { + return std.math.atan(input); + } + }.func; + @export(f, .{ .name = name ++ @typeName(T), .linkage = .Strong }); +} + +pub fn exportSin(comptime T: type, comptime name: []const u8) void { + comptime var f = struct { + fn func(input: T) callconv(.C) T { + return math.sin(input); + } + }.func; + @export(f, .{ .name = name ++ @typeName(T), .linkage = .Strong }); +} + +pub fn exportCos(comptime T: type, comptime name: []const u8) void { + comptime var f = struct { + fn func(input: T) callconv(.C) T { + return math.cos(input); + } + }.func; + @export(f, .{ .name = name ++ @typeName(T), .linkage = .Strong }); +} + +pub fn exportTan(comptime T: type, comptime name: []const u8) void { + comptime var f = struct { + fn func(input: T) callconv(.C) T { + return math.tan(input); + } + }.func; + @export(f, .{ .name = name ++ @typeName(T), .linkage = .Strong }); +} + +pub fn exportLog(comptime T: type, comptime name: []const u8) void { + comptime var f = struct { + fn func(input: T) callconv(.C) T { + return @log(input); + } + }.func; + @export(f, .{ .name = name ++ @typeName(T), .linkage = .Strong }); +} + +pub fn exportFAbs(comptime T: type, comptime name: []const u8) void { + comptime var f = struct { + fn func(input: T) callconv(.C) T { + return @fabs(input); + } + }.func; + @export(f, .{ .name = name ++ @typeName(T), .linkage = .Strong }); +} + +pub fn exportSqrt(comptime T: type, comptime name: []const u8) void { + comptime var f = struct { + fn func(input: T) callconv(.C) T { + return math.sqrt(input); + } + }.func; + @export(f, .{ .name = name ++ @typeName(T), .linkage = .Strong }); +} + +pub fn exportRound(comptime F: type, comptime T: type, comptime name: []const u8) void { + comptime var f = struct { + fn func(input: F) callconv(.C) T { + return @as(T, @intFromFloat((math.round(input)))); + } + }.func; + @export(f, .{ .name = name ++ @typeName(T), .linkage = .Strong }); +} + +pub fn exportFloor(comptime F: type, comptime T: type, comptime name: []const u8) void { + comptime var f = struct { + fn func(input: F) callconv(.C) T { + return @as(T, @intFromFloat((math.floor(input)))); + } + }.func; + @export(f, .{ .name = name ++ @typeName(T), .linkage = .Strong }); +} + +pub fn exportCeiling(comptime F: type, comptime T: type, comptime name: []const u8) void { + comptime var f = struct { + fn func(input: F) callconv(.C) T { + return @as(T, @intFromFloat((math.ceil(input)))); + } + }.func; + @export(f, .{ .name = name ++ @typeName(T), .linkage = .Strong }); +} + +pub fn exportDivCeil(comptime T: type, comptime name: []const u8) void { + comptime var f = struct { + fn func(a: T, b: T) callconv(.C) T { + return math.divCeil(T, a, b) catch @panic("TODO runtime exception for dividing by 0!"); + } + }.func; + @export(f, .{ .name = name ++ @typeName(T), .linkage = .Strong }); +} + +pub fn ToIntCheckedResult(comptime T: type) type { + // On the Roc side we sort by alignment; putting the errorcode last + // always works out (no number with smaller alignment than 1). + return extern struct { + value: T, + out_of_bounds: bool, + }; +} + +pub fn exportToIntCheckingMax(comptime From: type, comptime To: type, comptime name: []const u8) void { + comptime var f = struct { + fn func(input: From) callconv(.C) ToIntCheckedResult(To) { + if (input > std.math.maxInt(To)) { + return .{ .out_of_bounds = true, .value = 0 }; + } + return .{ .out_of_bounds = false, .value = @as(To, @intCast(input)) }; + } + }.func; + @export(f, .{ .name = name ++ @typeName(From), .linkage = .Strong }); +} + +pub fn exportToIntCheckingMaxAndMin(comptime From: type, comptime To: type, comptime name: []const u8) void { + comptime var f = struct { + fn func(input: From) callconv(.C) ToIntCheckedResult(To) { + if (input > std.math.maxInt(To) or input < std.math.minInt(To)) { + return .{ .out_of_bounds = true, .value = 0 }; + } + return .{ .out_of_bounds = false, .value = @as(To, @intCast(input)) }; + } + }.func; + @export(f, .{ .name = name ++ @typeName(From), .linkage = .Strong }); +} + +pub fn bytesToU16C(arg: RocList, position: usize) callconv(.C) u16 { + return @call(.always_inline, bytesToU16, .{ arg, position }); +} + +fn bytesToU16(arg: RocList, position: usize) u16 { + const bytes = @as([*]const u8, @ptrCast(arg.bytes)); + return @as(u16, @bitCast([_]u8{ bytes[position], bytes[position + 1] })); +} + +pub fn bytesToU32C(arg: RocList, position: usize) callconv(.C) u32 { + return @call(.always_inline, bytesToU32, .{ arg, position }); +} + +fn bytesToU32(arg: RocList, position: usize) u32 { + const bytes = @as([*]const u8, @ptrCast(arg.bytes)); + return @as(u32, @bitCast([_]u8{ bytes[position], bytes[position + 1], bytes[position + 2], bytes[position + 3] })); +} + +pub fn bytesToU64C(arg: RocList, position: usize) callconv(.C) u64 { + return @call(.always_inline, bytesToU64, .{ arg, position }); +} + +fn bytesToU64(arg: RocList, position: usize) u64 { + const bytes = @as([*]const u8, @ptrCast(arg.bytes)); + return @as(u64, @bitCast([_]u8{ bytes[position], bytes[position + 1], bytes[position + 2], bytes[position + 3], bytes[position + 4], bytes[position + 5], bytes[position + 6], bytes[position + 7] })); +} + +pub fn bytesToU128C(arg: RocList, position: usize) callconv(.C) u128 { + return @call(.always_inline, bytesToU128, .{ arg, position }); +} + +fn bytesToU128(arg: RocList, position: usize) u128 { + const bytes = @as([*]const u8, @ptrCast(arg.bytes)); + return @as(u128, @bitCast([_]u8{ bytes[position], bytes[position + 1], bytes[position + 2], bytes[position + 3], bytes[position + 4], bytes[position + 5], bytes[position + 6], bytes[position + 7], bytes[position + 8], bytes[position + 9], bytes[position + 10], bytes[position + 11], bytes[position + 12], bytes[position + 13], bytes[position + 14], bytes[position + 15] })); +} + +fn isMultipleOf(comptime T: type, lhs: T, rhs: T) bool { + if (rhs == 0 or rhs == -1) { + // lhs is a multiple of rhs iff + // + // - rhs == -1 + // - both rhs and lhs are 0 + // + // the -1 case is important for overflow reasons `isize::MIN % -1` crashes in rust + return (rhs == -1) or (lhs == 0); + } else { + const rem = @mod(lhs, rhs); + return rem == 0; + } +} + +pub fn exportIsMultipleOf(comptime T: type, comptime name: []const u8) void { + comptime var f = struct { + fn func(self: T, other: T) callconv(.C) bool { + return @call(.always_inline, isMultipleOf, .{ T, self, other }); + } + }.func; + @export(f, .{ .name = name ++ @typeName(T), .linkage = .Strong }); +} + +fn addWithOverflow(comptime T: type, self: T, other: T) WithOverflow(T) { + switch (@typeInfo(T)) { + .Int => { + const answer = @addWithOverflow(self, other); + return .{ .value = answer[0], .has_overflowed = answer[1] == 1 }; + }, + else => { + const answer = self + other; + const overflowed = !std.math.isFinite(answer); + return .{ .value = answer, .has_overflowed = overflowed }; + }, + } +} + +pub fn exportAddWithOverflow(comptime T: type, comptime name: []const u8) void { + comptime var f = struct { + fn func(self: T, other: T) callconv(.C) WithOverflow(T) { + return @call(.always_inline, addWithOverflow, .{ T, self, other }); + } + }.func; + @export(f, .{ .name = name ++ @typeName(T), .linkage = .Strong }); +} + +pub fn exportAddSaturatedInt(comptime T: type, comptime name: []const u8) void { + comptime var f = struct { + fn func(self: T, other: T) callconv(.C) T { + const result = addWithOverflow(T, self, other); + if (result.has_overflowed) { + // We can unambiguously tell which way it wrapped, because we have N+1 bits including the overflow bit + if (result.value >= 0 and @typeInfo(T).Int.signedness == .signed) { + return std.math.minInt(T); + } else { + return std.math.maxInt(T); + } + } else { + return result.value; + } + } + }.func; + @export(f, .{ .name = name ++ @typeName(T), .linkage = .Strong }); +} + +pub fn exportAddOrPanic(comptime T: type, comptime name: []const u8) void { + comptime var f = struct { + fn func(self: T, other: T) callconv(.C) T { + const result = addWithOverflow(T, self, other); + if (result.has_overflowed) { + roc_panic("integer addition overflowed!", 0); + unreachable; + } else { + return result.value; + } + } + }.func; + @export(f, .{ .name = name ++ @typeName(T), .linkage = .Strong }); +} + +fn subWithOverflow(comptime T: type, self: T, other: T) WithOverflow(T) { + switch (@typeInfo(T)) { + .Int => { + const answer = @subWithOverflow(self, other); + return .{ .value = answer[0], .has_overflowed = answer[1] == 1 }; + }, + else => { + const answer = self - other; + const overflowed = !std.math.isFinite(answer); + return .{ .value = answer, .has_overflowed = overflowed }; + }, + } +} + +pub fn exportSubWithOverflow(comptime T: type, comptime name: []const u8) void { + comptime var f = struct { + fn func(self: T, other: T) callconv(.C) WithOverflow(T) { + return @call(.always_inline, subWithOverflow, .{ T, self, other }); + } + }.func; + @export(f, .{ .name = name ++ @typeName(T), .linkage = .Strong }); +} + +pub fn exportSubSaturatedInt(comptime T: type, comptime name: []const u8) void { + comptime var f = struct { + fn func(self: T, other: T) callconv(.C) T { + const result = subWithOverflow(T, self, other); + if (result.has_overflowed) { + if (@typeInfo(T).Int.signedness == .unsigned) { + return 0; + } else if (self < 0) { + return std.math.minInt(T); + } else { + return std.math.maxInt(T); + } + } else { + return result.value; + } + } + }.func; + @export(f, .{ .name = name ++ @typeName(T), .linkage = .Strong }); +} + +pub fn exportSubOrPanic(comptime T: type, comptime name: []const u8) void { + comptime var f = struct { + fn func(self: T, other: T) callconv(.C) T { + const result = subWithOverflow(T, self, other); + if (result.has_overflowed) { + roc_panic("integer subtraction overflowed!", 0); + unreachable; + } else { + return result.value; + } + } + }.func; + @export(f, .{ .name = name ++ @typeName(T), .linkage = .Strong }); +} + +fn mulWithOverflow(comptime T: type, comptime W: type, self: T, other: T) WithOverflow(T) { + switch (@typeInfo(T)) { + .Int => { + if (T == i128) { + const is_answer_negative = (self < 0) != (other < 0); + const max = std.math.maxInt(i128); + const min = std.math.minInt(i128); + + const self_u128 = @as(u128, @intCast(math.absInt(self) catch { + if (other == 0) { + return .{ .value = 0, .has_overflowed = false }; + } else if (other == 1) { + return .{ .value = self, .has_overflowed = false }; + } else if (is_answer_negative) { + return .{ .value = min, .has_overflowed = true }; + } else { + return .{ .value = max, .has_overflowed = true }; + } + })); + + const other_u128 = @as(u128, @intCast(math.absInt(other) catch { + if (self == 0) { + return .{ .value = 0, .has_overflowed = false }; + } else if (self == 1) { + return .{ .value = other, .has_overflowed = false }; + } else if (is_answer_negative) { + return .{ .value = min, .has_overflowed = true }; + } else { + return .{ .value = max, .has_overflowed = true }; + } + })); + + const answer256: U256 = mul_u128(self_u128, other_u128); + + if (is_answer_negative) { + if (answer256.hi != 0 or answer256.lo > (1 << 127)) { + return .{ .value = min, .has_overflowed = true }; + } else if (answer256.lo == (1 << 127)) { + return .{ .value = min, .has_overflowed = false }; + } else { + return .{ .value = -@as(i128, @intCast(answer256.lo)), .has_overflowed = false }; + } + } else { + if (answer256.hi != 0 or answer256.lo > @as(u128, @intCast(max))) { + return .{ .value = max, .has_overflowed = true }; + } else { + return .{ .value = @as(i128, @intCast(answer256.lo)), .has_overflowed = false }; + } + } + } else { + const self_wide: W = self; + const other_wide: W = other; + const answer: W = self_wide * other_wide; + + const max: W = std.math.maxInt(T); + const min: W = std.math.minInt(T); + + if (answer > max) { + return .{ .value = max, .has_overflowed = true }; + } else if (answer < min) { + return .{ .value = min, .has_overflowed = true }; + } else { + return .{ .value = @as(T, @intCast(answer)), .has_overflowed = false }; + } + } + }, + else => { + const answer = self * other; + const overflowed = !std.math.isFinite(answer); + return .{ .value = answer, .has_overflowed = overflowed }; + }, + } +} + +pub fn exportMulWithOverflow(comptime T: type, comptime W: type, comptime name: []const u8) void { + comptime var f = struct { + fn func(self: T, other: T) callconv(.C) WithOverflow(T) { + return @call(.always_inline, mulWithOverflow, .{ T, W, self, other }); + } + }.func; + @export(f, .{ .name = name ++ @typeName(T), .linkage = .Strong }); +} + +pub fn exportMulSaturatedInt(comptime T: type, comptime W: type, comptime name: []const u8) void { + comptime var f = struct { + fn func(self: T, other: T) callconv(.C) T { + const result = @call(.always_inline, mulWithOverflow, .{ T, W, self, other }); + return result.value; + } + }.func; + @export(f, .{ .name = name ++ @typeName(T), .linkage = .Strong }); +} + +pub fn exportMulWrappedInt(comptime T: type, comptime name: []const u8) void { + comptime var f = struct { + fn func(self: T, other: T) callconv(.C) T { + return self *% other; + } + }.func; + @export(f, .{ .name = name ++ @typeName(T), .linkage = .Strong }); +} + +pub fn shiftRightZeroFillI128(self: i128, other: u8) callconv(.C) i128 { + if (other & 0b1000_0000 > 0) { + return 0; + } else { + return self >> @as(u7, @intCast(other)); + } +} + +pub fn shiftRightZeroFillU128(self: u128, other: u8) callconv(.C) u128 { + if (other & 0b1000_0000 > 0) { + return 0; + } else { + return self >> @as(u7, @intCast(other)); + } +} + +pub fn compareI128(self: i128, other: i128) callconv(.C) Ordering { + if (self == other) { + return Ordering.EQ; + } else if (self < other) { + return Ordering.LT; + } else { + return Ordering.GT; + } +} + +pub fn compareU128(self: u128, other: u128) callconv(.C) Ordering { + if (self == other) { + return Ordering.EQ; + } else if (self < other) { + return Ordering.LT; + } else { + return Ordering.GT; + } +} + +pub fn lessThanI128(self: i128, other: i128) callconv(.C) bool { + return self < other; +} + +pub fn lessThanOrEqualI128(self: i128, other: i128) callconv(.C) bool { + return self <= other; +} + +pub fn greaterThanI128(self: i128, other: i128) callconv(.C) bool { + return self > other; +} + +pub fn greaterThanOrEqualI128(self: i128, other: i128) callconv(.C) bool { + return self >= other; +} + +pub fn lessThanU128(self: u128, other: u128) callconv(.C) bool { + return self < other; +} + +pub fn lessThanOrEqualU128(self: u128, other: u128) callconv(.C) bool { + return self <= other; +} + +pub fn greaterThanU128(self: u128, other: u128) callconv(.C) bool { + return self > other; +} + +pub fn greaterThanOrEqualU128(self: u128, other: u128) callconv(.C) bool { + return self >= other; +} + +pub fn exportMulOrPanic(comptime T: type, comptime W: type, comptime name: []const u8) void { + comptime var f = struct { + fn func(self: T, other: T) callconv(.C) T { + const result = @call(.always_inline, mulWithOverflow, .{ T, W, self, other }); + if (result.has_overflowed) { + roc_panic("integer multiplication overflowed!", 0); + unreachable; + } else { + return result.value; + } + } + }.func; + @export(f, .{ .name = name ++ @typeName(T), .linkage = .Strong }); +} + +pub fn exportCountLeadingZeroBits(comptime T: type, comptime name: []const u8) void { + comptime var f = struct { + fn func(self: T) callconv(.C) usize { + return @as(usize, @clz(self)); + } + }.func; + @export(f, .{ .name = name ++ @typeName(T), .linkage = .Strong }); +} + +pub fn exportCountTrailingZeroBits(comptime T: type, comptime name: []const u8) void { + comptime var f = struct { + fn func(self: T) callconv(.C) usize { + return @as(usize, @ctz(self)); + } + }.func; + @export(f, .{ .name = name ++ @typeName(T), .linkage = .Strong }); +} + +pub fn exportCountOneBits(comptime T: type, comptime name: []const u8) void { + comptime var f = struct { + fn func(self: T) callconv(.C) usize { + return @as(usize, @popCount(self)); + } + }.func; + @export(f, .{ .name = name ++ @typeName(T), .linkage = .Strong }); +} diff --git a/crates/compiler/builtins/bitcode/src/panic.zig b/crates/compiler/builtins/bitcode/src/panic.zig new file mode 100644 index 0000000000..76ecd69e12 --- /dev/null +++ b/crates/compiler/builtins/bitcode/src/panic.zig @@ -0,0 +1,15 @@ +const std = @import("std"); +const RocStr = @import("str.zig").RocStr; + +// Signals to the host that the program has panicked +extern fn roc_panic(msg: *const RocStr, tag_id: u32) callconv(.C) void; + +pub fn panic_help(msg: []const u8, tag_id: u32) void { + var str = RocStr.init(msg.ptr, msg.len); + roc_panic(&str, tag_id); +} + +// must export this explicitly because right now it is not used from zig code +pub fn panic(msg: *const RocStr, alignment: u32) callconv(.C) void { + return roc_panic(msg, alignment); +} diff --git a/crates/compiler/builtins/bitcode/src/str.zig b/crates/compiler/builtins/bitcode/src/str.zig new file mode 100644 index 0000000000..3c39f5afba --- /dev/null +++ b/crates/compiler/builtins/bitcode/src/str.zig @@ -0,0 +1,2903 @@ +const utils = @import("utils.zig"); +const RocList = @import("list.zig").RocList; +const grapheme = @import("helpers/grapheme.zig"); +const UpdateMode = utils.UpdateMode; +const std = @import("std"); +const mem = std.mem; +const unicode = std.unicode; +const testing = std.testing; +const expectEqual = testing.expectEqual; +const expectError = testing.expectError; +const expect = testing.expect; + +const InPlace = enum(u8) { + InPlace, + Clone, +}; + +const MASK_ISIZE: isize = std.math.minInt(isize); +const MASK: usize = @as(usize, @bitCast(MASK_ISIZE)); +const SEAMLESS_SLICE_BIT: usize = MASK; + +const SMALL_STR_MAX_LENGTH = SMALL_STRING_SIZE - 1; +const SMALL_STRING_SIZE = @sizeOf(RocStr); + +fn init_blank_small_string(comptime n: usize) [n]u8 { + var prime_list: [n]u8 = undefined; + + var i = 0; + while (i < n) : (i += 1) { + prime_list[i] = 0; + } + + return prime_list; +} + +pub const RocStr = extern struct { + str_bytes: ?[*]u8, + str_len: usize, + str_capacity: usize, + + pub const alignment = @alignOf(usize); + + pub inline fn empty() RocStr { + return RocStr{ + .str_len = 0, + .str_bytes = null, + .str_capacity = MASK, + }; + } + + // This clones the pointed-to bytes if they won't fit in a + // small string, and returns a (pointer, len) tuple which points to them. + pub fn init(bytes_ptr: [*]const u8, length: usize) RocStr { + var result = RocStr.allocate(length); + @memcpy(result.asU8ptrMut()[0..length], bytes_ptr[0..length]); + + return result; + } + + // This requires that the list is non-null. + // It also requires that start and count define a slice that does not go outside the bounds of the list. + pub fn fromSubListUnsafe(list: RocList, start: usize, count: usize, update_mode: UpdateMode) RocStr { + const start_byte = @as([*]u8, @ptrCast(list.bytes)) + start; + if (list.isSeamlessSlice()) { + return RocStr{ + .str_bytes = start_byte, + .str_len = count | SEAMLESS_SLICE_BIT, + .str_capacity = list.capacity_or_ref_ptr & (~SEAMLESS_SLICE_BIT), + }; + } else if (start == 0 and (update_mode == .InPlace or list.isUnique())) { + // Rare case, we can take over the original list. + return RocStr{ + .str_bytes = start_byte, + .str_len = count, + .str_capacity = list.capacity_or_ref_ptr, // This is guaranteed to be a proper capacity. + }; + } else { + // Create seamless slice pointing to the list. + return RocStr{ + .str_bytes = start_byte, + .str_len = count | SEAMLESS_SLICE_BIT, + .str_capacity = @intFromPtr(list.bytes) >> 1, + }; + } + } + + pub fn isSeamlessSlice(self: RocStr) bool { + return !self.isSmallStr() and @as(isize, @bitCast(self.str_len)) < 0; + } + + pub fn fromSlice(slice: []const u8) RocStr { + return RocStr.init(slice.ptr, slice.len); + } + + fn allocateBig(length: usize, capacity: usize) RocStr { + const first_element = utils.allocateWithRefcount(capacity, @sizeOf(usize)); + + return RocStr{ + .str_bytes = first_element, + .str_len = length, + .str_capacity = capacity, + }; + } + + // allocate space for a (big or small) RocStr, but put nothing in it yet. + // May have a larger capacity than the length. + pub fn allocate(length: usize) RocStr { + const element_width = 1; + const result_is_big = length >= SMALL_STRING_SIZE; + + if (result_is_big) { + const capacity = utils.calculateCapacity(0, length, element_width); + return RocStr.allocateBig(length, capacity); + } else { + var string = RocStr.empty(); + + string.asU8ptrMut()[@sizeOf(RocStr) - 1] = @as(u8, @intCast(length)) | 0b1000_0000; + + return string; + } + } + + // allocate space for a (big or small) RocStr, but put nothing in it yet. + // Will have the exact same capacity as length if it is not a small string. + pub fn allocateExact(length: usize) RocStr { + const result_is_big = length >= SMALL_STRING_SIZE; + + if (result_is_big) { + return RocStr.allocateBig(length, length); + } else { + var string = RocStr.empty(); + + string.asU8ptrMut()[@sizeOf(RocStr) - 1] = @as(u8, @intCast(length)) | 0b1000_0000; + + return string; + } + } + + // This returns all ones if the list is a seamless slice. + // Otherwise, it returns all zeros. + // This is done without branching for optimization purposes. + pub fn seamlessSliceMask(self: RocStr) usize { + return @as(usize, @bitCast(@as(isize, @bitCast(self.str_len)) >> (@bitSizeOf(isize) - 1))); + } + + // returns a pointer to just after the refcount. + // It is just after the refcount as an optimization for other shared code paths. + // For regular list, it just returns their bytes pointer. + // For seamless slices, it returns the pointer stored in capacity_or_ref_ptr. + // This does not return a valid value if the input is a small string. + pub fn getRefcountPtr(self: RocStr) ?[*]u8 { + const str_ref_ptr = @intFromPtr(self.str_bytes); + const slice_ref_ptr = self.str_capacity << 1; + const slice_mask = self.seamlessSliceMask(); + const ref_ptr = (str_ref_ptr & ~slice_mask) | (slice_ref_ptr & slice_mask); + return @as(?[*]u8, @ptrFromInt(ref_ptr)); + } + + pub fn incref(self: RocStr, n: usize) void { + if (!self.isSmallStr()) { + const ref_ptr = self.getRefcountPtr(); + if (ref_ptr != null) { + const isizes: [*]isize = @as([*]isize, @ptrCast(@alignCast(ref_ptr))); + utils.increfRcPtrC(@as(*isize, @ptrCast(isizes - 1)), @as(isize, @intCast(n))); + } + } + } + + pub fn decref(self: RocStr) void { + if (!self.isSmallStr()) { + utils.decref(self.getRefcountPtr(), self.str_capacity, RocStr.alignment); + } + } + + pub fn eq(self: RocStr, other: RocStr) bool { + // If they are byte-for-byte equal, they're definitely equal! + if (self.str_bytes == other.str_bytes and self.str_len == other.str_len and self.str_capacity == other.str_capacity) { + return true; + } + + const self_len = self.len(); + const other_len = other.len(); + + // If their lengths are different, they're definitely unequal. + if (self_len != other_len) { + return false; + } + + // Now we have to look at the string contents + const self_bytes = self.asU8ptr(); + const other_bytes = other.asU8ptr(); + // TODO: we can make an optimization like memcmp does in glibc. + // We can check the min shared alignment 1, 2, 4, or 8. + // Then do a copy at that alignment before falling back on one byte at a time. + // Currently we have to be unaligned because slices can be at any alignment. + var b: usize = 0; + while (b < self_len) : (b += 1) { + if (self_bytes[b] != other_bytes[b]) { + return false; + } + } + + return true; + } + + pub fn clone(str: RocStr) RocStr { + if (str.isSmallStr()) { + // just return the bytes + return str; + } else { + var new_str = RocStr.allocateBig(str.str_len, str.str_len); + + var old_bytes: [*]u8 = @as([*]u8, @ptrCast(str.str_bytes)); + var new_bytes: [*]u8 = @as([*]u8, @ptrCast(new_str.str_bytes)); + + @memcpy(new_bytes[0..str.str_len], old_bytes[0..str.str_len]); + + return new_str; + } + } + + pub fn reallocate( + self: RocStr, + new_length: usize, + ) RocStr { + const element_width = 1; + const old_capacity = self.getCapacity(); + + if (self.isSmallStr() or self.isSeamlessSlice() or !self.isUnique()) { + return self.reallocateFresh(new_length); + } + + if (self.str_bytes) |source_ptr| { + if (old_capacity > new_length) { + var output = self; + output.setLen(new_length); + return output; + } + const new_capacity = utils.calculateCapacity(old_capacity, new_length, element_width); + const new_source = utils.unsafeReallocate( + source_ptr, + RocStr.alignment, + old_capacity, + new_capacity, + element_width, + ); + + return RocStr{ .str_bytes = new_source, .str_len = new_length, .str_capacity = new_capacity }; + } + return self.reallocateFresh(new_length); + } + + /// reallocate by explicitly making a new allocation and copying elements over + fn reallocateFresh( + self: RocStr, + new_length: usize, + ) RocStr { + const old_length = self.len(); + + const element_width = 1; + const result_is_big = new_length >= SMALL_STRING_SIZE; + + if (result_is_big) { + const capacity = utils.calculateCapacity(0, new_length, element_width); + var result = RocStr.allocateBig(new_length, capacity); + + // transfer the memory + + const source_ptr = self.asU8ptr(); + const dest_ptr = result.asU8ptrMut(); + + std.mem.copy(u8, dest_ptr[0..old_length], source_ptr[0..old_length]); + @memset(dest_ptr[old_length..new_length], 0); + + self.decref(); + + return result; + } else { + var string = RocStr.empty(); + + // I believe taking this reference on the stack here is important for correctness. + // Doing it via a method call seemed to cause issues + const dest_ptr = @as([*]u8, @ptrCast(&string)); + dest_ptr[@sizeOf(RocStr) - 1] = @as(u8, @intCast(new_length)) | 0b1000_0000; + + const source_ptr = self.asU8ptr(); + + std.mem.copy(u8, dest_ptr[0..old_length], source_ptr[0..old_length]); + @memset(dest_ptr[old_length..new_length], 0); + + self.decref(); + + return string; + } + } + + pub fn isSmallStr(self: RocStr) bool { + return @as(isize, @bitCast(self.str_capacity)) < 0; + } + + test "isSmallStr: returns true for empty string" { + try expect(isSmallStr(RocStr.empty())); + } + + fn asArray(self: RocStr) [@sizeOf(RocStr)]u8 { + const as_ptr = @as([*]const u8, @ptrCast(&self)); + const slice = as_ptr[0..@sizeOf(RocStr)]; + + return slice.*; + } + + pub fn len(self: RocStr) usize { + if (self.isSmallStr()) { + return self.asArray()[@sizeOf(RocStr) - 1] ^ 0b1000_0000; + } else { + return self.str_len & (~SEAMLESS_SLICE_BIT); + } + } + + pub fn setLen(self: *RocStr, length: usize) void { + if (self.isSmallStr()) { + self.asU8ptrMut()[@sizeOf(RocStr) - 1] = @as(u8, @intCast(length)) | 0b1000_0000; + } else { + self.str_len = length | (SEAMLESS_SLICE_BIT & self.str_len); + } + } + + pub fn getCapacity(self: RocStr) usize { + if (self.isSmallStr()) { + return SMALL_STR_MAX_LENGTH; + } else if (self.isSeamlessSlice()) { + return self.str_len & (~SEAMLESS_SLICE_BIT); + } else { + return self.str_capacity; + } + } + + // This does a small string check, but no bounds checking whatsoever! + pub fn getUnchecked(self: RocStr, index: usize) u8 { + if (self.isSmallStr()) { + return self.asArray()[index]; + } else { + const bytes = self.str_bytes orelse unreachable; + + return bytes[index]; + } + } + + pub fn isEmpty(self: RocStr) bool { + return self.len() == 0; + } + + pub fn isUnique(self: RocStr) bool { + // small strings can be copied + if (self.isSmallStr()) { + return true; + } + + // otherwise, check if the refcount is one + return @call(.always_inline, RocStr.isRefcountOne, .{self}); + } + + fn isRefcountOne(self: RocStr) bool { + return self.refcountMachine() == utils.REFCOUNT_ONE; + } + + fn refcountMachine(self: RocStr) usize { + if ((self.getCapacity() == 0 and !self.isSeamlessSlice()) or self.isSmallStr()) { + return utils.REFCOUNT_ONE; + } + + const ptr: [*]usize = @as([*]usize, @ptrCast(@alignCast(self.str_bytes))); + return (ptr - 1)[0]; + } + + fn refcountHuman(self: RocStr) usize { + return self.refcountMachine() - utils.REFCOUNT_ONE + 1; + } + + pub fn asSlice(self: *const RocStr) []const u8 { + return self.asU8ptr()[0..self.len()]; + } + + pub fn asSliceWithCapacity(self: *const RocStr) []const u8 { + return self.asU8ptr()[0..self.getCapacity()]; + } + + pub fn asSliceWithCapacityMut(self: *RocStr) []u8 { + return self.asU8ptrMut()[0..self.getCapacity()]; + } + + pub fn asU8ptr(self: *const RocStr) [*]const u8 { + if (self.isSmallStr()) { + return @as([*]const u8, @ptrCast(self)); + } else { + return @as([*]const u8, @ptrCast(self.str_bytes)); + } + } + + pub fn asU8ptrMut(self: *RocStr) [*]u8 { + if (self.isSmallStr()) { + return @as([*]u8, @ptrCast(self)); + } else { + return @as([*]u8, @ptrCast(self.str_bytes)); + } + } + + // Given a pointer to some bytes, write the first (len) bytes of this + // RocStr's contents into it. + // + // One use for this function is writing into an `alloca` for a C string that + // only needs to live long enough to be passed as an argument to + // a C function - like the file path argument to `fopen`. + pub fn memcpy(self: RocStr, dest: [*]u8) void { + const src = self.asU8ptr(); + @memcpy(dest[0..self.len()], src[0..self.len()]); + } + + test "RocStr.eq: small, equal" { + const str1_len = 3; + var str1: [str1_len]u8 = "abc".*; + const str1_ptr: [*]u8 = &str1; + var roc_str1 = RocStr.init(str1_ptr, str1_len); + + const str2_len = 3; + var str2: [str2_len]u8 = "abc".*; + const str2_ptr: [*]u8 = &str2; + var roc_str2 = RocStr.init(str2_ptr, str2_len); + + try expect(roc_str1.eq(roc_str2)); + + roc_str1.decref(); + roc_str2.decref(); + } + + test "RocStr.eq: small, not equal, different length" { + const str1_len = 4; + var str1: [str1_len]u8 = "abcd".*; + const str1_ptr: [*]u8 = &str1; + var roc_str1 = RocStr.init(str1_ptr, str1_len); + + const str2_len = 3; + var str2: [str2_len]u8 = "abc".*; + const str2_ptr: [*]u8 = &str2; + var roc_str2 = RocStr.init(str2_ptr, str2_len); + + defer { + roc_str1.decref(); + roc_str2.decref(); + } + + try expect(!roc_str1.eq(roc_str2)); + } + + test "RocStr.eq: small, not equal, same length" { + const str1_len = 3; + var str1: [str1_len]u8 = "acb".*; + const str1_ptr: [*]u8 = &str1; + var roc_str1 = RocStr.init(str1_ptr, str1_len); + + const str2_len = 3; + var str2: [str2_len]u8 = "abc".*; + const str2_ptr: [*]u8 = &str2; + var roc_str2 = RocStr.init(str2_ptr, str2_len); + + defer { + roc_str1.decref(); + roc_str2.decref(); + } + + try expect(!roc_str1.eq(roc_str2)); + } + + test "RocStr.eq: large, equal" { + const content = "012345678901234567890123456789"; + const roc_str1 = RocStr.init(content, content.len); + const roc_str2 = RocStr.init(content, content.len); + + defer { + roc_str1.decref(); + roc_str2.decref(); + } + + try expect(roc_str1.eq(roc_str2)); + } + + test "RocStr.eq: large, different lengths, unequal" { + const content1 = "012345678901234567890123456789"; + const roc_str1 = RocStr.init(content1, content1.len); + const content2 = "012345678901234567890"; + const roc_str2 = RocStr.init(content2, content2.len); + + defer { + roc_str1.decref(); + roc_str2.decref(); + } + + try expect(!roc_str1.eq(roc_str2)); + } + + test "RocStr.eq: large, different content, unequal" { + const content1 = "012345678901234567890123456789!!"; + const roc_str1 = RocStr.init(content1, content1.len); + const content2 = "012345678901234567890123456789--"; + const roc_str2 = RocStr.init(content2, content2.len); + + defer { + roc_str1.decref(); + roc_str2.decref(); + } + + try expect(!roc_str1.eq(roc_str2)); + } + + test "RocStr.eq: large, garbage after end, equal" { + const content = "012345678901234567890123456789"; + const roc_str1 = RocStr.init(content, content.len); + const roc_str2 = RocStr.init(content, content.len); + try expect(roc_str1.str_bytes != roc_str2.str_bytes); + + // Insert garbage after the end of each string + roc_str1.str_bytes.?[30] = '!'; + roc_str1.str_bytes.?[31] = '!'; + roc_str2.str_bytes.?[30] = '-'; + roc_str2.str_bytes.?[31] = '-'; + + defer { + roc_str1.decref(); + roc_str2.decref(); + } + + try expect(roc_str1.eq(roc_str2)); + } +}; + +pub fn init(bytes_ptr: [*]const u8, length: usize) callconv(.C) RocStr { + return @call(.always_inline, RocStr.init, .{ bytes_ptr, length }); +} + +// Str.equal +pub fn strEqual(self: RocStr, other: RocStr) callconv(.C) bool { + return self.eq(other); +} + +// Str.numberOfBytes +pub fn strNumberOfBytes(string: RocStr) callconv(.C) usize { + return string.len(); +} + +// Str.toScalars +pub fn strToScalarsC(str: RocStr) callconv(.C) RocList { + return @call(.always_inline, strToScalars, .{str}); +} + +fn strToScalars(string: RocStr) callconv(.C) RocList { + const str_len = string.len(); + + if (str_len == 0) { + return RocList.empty(); + } + + var capacity = str_len; + + if (!string.isSmallStr()) { + capacity = string.getCapacity(); + } + + // For purposes of preallocation, assume the number of code points is the same + // as the number of bytes. This might be longer than necessary, but definitely + // should not require a second allocation. + var answer = RocList.allocate(@alignOf(u32), capacity, @sizeOf(u32)); + + // `orelse unreachable` is fine here, because we already did an early + // return to verify the string was nonempty. + var answer_elems = answer.elements(u32) orelse unreachable; + var src_index: usize = 0; + var answer_index: usize = 0; + + while (src_index < str_len) { + src_index += writeNextScalar(string, src_index, answer_elems, answer_index); + answer_index += 1; + } + + answer.length = answer_index; + + return answer; +} + +// Given a non-empty RocStr, and a src_index byte index into that string, +// and a destination [*]u32, and an index into that destination, +// Parses the next scalar value out of the string (at the given byte index), +// writes it into the destination, and returns the number of bytes parsed. +inline fn writeNextScalar(non_empty_string: RocStr, src_index: usize, dest: [*]u32, dest_index: usize) usize { + const utf8_byte = non_empty_string.getUnchecked(src_index); + + // How UTF-8 bytes work: + // https://docs.teradata.com/r/Teradata-Database-International-Character-Set-Support/June-2017/Client-Character-Set-Options/UTF8-Client-Character-Set-Support/UTF8-Multibyte-Sequences + if (utf8_byte <= 127) { + // It's an ASCII character. Copy it over directly. + dest[dest_index] = @as(u32, @intCast(utf8_byte)); + + return 1; + } else if (utf8_byte >> 5 == 0b0000_0110) { + // Its three high order bits are 110, so this is a two-byte sequence. + + // Example: + // utf-8: 1100 1111 1011 0001 + // code pt: 0000 0011 1111 0001 (decimal: 1009) + + // Discard the first byte's high order bits of 110. + var code_pt = @as(u32, @intCast(utf8_byte & 0b0001_1111)); + + // Discard the second byte's high order bits of 10. + code_pt <<= 6; + code_pt |= non_empty_string.getUnchecked(src_index + 1) & 0b0011_1111; + + dest[dest_index] = code_pt; + + return 2; + } else if (utf8_byte >> 4 == 0b0000_1110) { + // Its four high order bits are 1110, so this is a three-byte sequence. + + // Discard the first byte's high order bits of 1110. + var code_pt = @as(u32, @intCast(utf8_byte & 0b0000_1111)); + + // Discard the second byte's high order bits of 10. + code_pt <<= 6; + code_pt |= non_empty_string.getUnchecked(src_index + 1) & 0b0011_1111; + + // Discard the third byte's high order bits of 10 (same as second byte). + code_pt <<= 6; + code_pt |= non_empty_string.getUnchecked(src_index + 2) & 0b0011_1111; + + dest[dest_index] = code_pt; + + return 3; + } else { + // This must be a four-byte sequence, so the five high order bits should be 11110. + + // Discard the first byte's high order bits of 11110. + var code_pt = @as(u32, @intCast(utf8_byte & 0b0000_0111)); + + // Discard the second byte's high order bits of 10. + code_pt <<= 6; + code_pt |= non_empty_string.getUnchecked(src_index + 1) & 0b0011_1111; + + // Discard the third byte's high order bits of 10 (same as second byte). + code_pt <<= 6; + code_pt |= non_empty_string.getUnchecked(src_index + 2) & 0b0011_1111; + + // Discard the fourth byte's high order bits of 10 (same as second and third). + code_pt <<= 6; + code_pt |= non_empty_string.getUnchecked(src_index + 3) & 0b0011_1111; + + dest[dest_index] = code_pt; + + return 4; + } +} + +test "strToScalars: empty string" { + const str = RocStr.fromSlice(""); + defer RocStr.decref(str); + + const expected = RocList.empty(); + const actual = strToScalars(str); + defer actual.decref(@sizeOf(u32)); + + try expect(RocList.eql(actual, expected)); +} + +test "strToScalars: One ASCII char" { + const str = RocStr.fromSlice("R"); + defer RocStr.decref(str); + + const expected_array = [_]u32{82}; + const expected = RocList.fromSlice(u32, expected_array[0..expected_array.len]); + defer expected.decref(@sizeOf(u32)); + + const actual = strToScalars(str); + defer actual.decref(@sizeOf(u32)); + + try expect(RocList.eql(actual, expected)); +} + +test "strToScalars: Multiple ASCII chars" { + const str = RocStr.fromSlice("Roc!"); + defer RocStr.decref(str); + + const expected_array = [_]u32{ 82, 111, 99, 33 }; + const expected = RocList.fromSlice(u32, expected_array[0..expected_array.len]); + defer expected.decref(@sizeOf(u32)); + + const actual = strToScalars(str); + defer actual.decref(@sizeOf(u32)); + + try expect(RocList.eql(actual, expected)); +} + +test "strToScalars: One 2-byte UTF-8 character" { + const str = RocStr.fromSlice("é"); + defer RocStr.decref(str); + + const expected_array = [_]u32{233}; + const expected = RocList.fromSlice(u32, expected_array[0..expected_array.len]); + defer expected.decref(@sizeOf(u32)); + + const actual = strToScalars(str); + defer actual.decref(@sizeOf(u32)); + + try expect(RocList.eql(actual, expected)); +} + +test "strToScalars: Multiple 2-byte UTF-8 characters" { + const str = RocStr.fromSlice("Cäfés"); + defer RocStr.decref(str); + + const expected_array = [_]u32{ 67, 228, 102, 233, 115 }; + const expected = RocList.fromSlice(u32, expected_array[0..expected_array.len]); + defer expected.decref(@sizeOf(u32)); + + const actual = strToScalars(str); + defer actual.decref(@sizeOf(u32)); + + try expect(RocList.eql(actual, expected)); +} + +test "strToScalars: One 3-byte UTF-8 character" { + const str = RocStr.fromSlice("鹏"); + defer RocStr.decref(str); + + const expected_array = [_]u32{40527}; + const expected = RocList.fromSlice(u32, expected_array[0..expected_array.len]); + defer expected.decref(@sizeOf(u32)); + + const actual = strToScalars(str); + defer actual.decref(@sizeOf(u32)); + + try expect(RocList.eql(actual, expected)); +} + +test "strToScalars: Multiple 3-byte UTF-8 characters" { + const str = RocStr.fromSlice("鹏很有趣"); + defer RocStr.decref(str); + + const expected_array = [_]u32{ 40527, 24456, 26377, 36259 }; + const expected = RocList.fromSlice(u32, expected_array[0..expected_array.len]); + defer expected.decref(@sizeOf(u32)); + + const actual = strToScalars(str); + defer actual.decref(@sizeOf(u32)); + + try expect(RocList.eql(actual, expected)); +} + +test "strToScalars: One 4-byte UTF-8 character" { + // from https://design215.com/toolbox/utf8-4byte-characters.php + const str = RocStr.fromSlice("𒀀"); + defer RocStr.decref(str); + + const expected_array = [_]u32{73728}; + const expected = RocList.fromSlice(u32, expected_array[0..expected_array.len]); + defer expected.decref(@sizeOf(u32)); + + const actual = strToScalars(str); + defer actual.decref(@sizeOf(u32)); + + try expect(RocList.eql(actual, expected)); +} + +test "strToScalars: Multiple 4-byte UTF-8 characters" { + // from https://design215.com/toolbox/utf8-4byte-characters.php + const str = RocStr.fromSlice("𒀀𒀁"); + defer RocStr.decref(str); + + const expected_array = [_]u32{ 73728, 73729 }; + const expected = RocList.fromSlice(u32, expected_array[0..expected_array.len]); + defer expected.decref(@sizeOf(u32)); + + const actual = strToScalars(str); + defer actual.decref(@sizeOf(u32)); + + try expect(RocList.eql(actual, expected)); +} + +// Str.fromInt +pub fn exportFromInt(comptime T: type, comptime name: []const u8) void { + comptime var f = struct { + fn func(int: T) callconv(.C) RocStr { + return @call(.always_inline, strFromIntHelp, .{ T, int }); + } + }.func; + + @export(f, .{ .name = name ++ @typeName(T), .linkage = .Strong }); +} + +fn strFromIntHelp(comptime T: type, int: T) RocStr { + // determine maximum size for this T + const size = comptime blk: { + // the string representation of the minimum i128 value uses at most 40 characters + var buf: [40]u8 = undefined; + var resultMin = std.fmt.bufPrint(&buf, "{}", .{std.math.minInt(T)}) catch unreachable; + var resultMax = std.fmt.bufPrint(&buf, "{}", .{std.math.maxInt(T)}) catch unreachable; + var result = if (resultMin.len > resultMax.len) resultMin.len else resultMax.len; + break :blk result; + }; + + var buf: [size]u8 = undefined; + const result = std.fmt.bufPrint(&buf, "{}", .{int}) catch unreachable; + + return RocStr.init(&buf, result.len); +} + +// Str.fromFloat +pub fn exportFromFloat(comptime T: type, comptime name: []const u8) void { + comptime var f = struct { + fn func(float: T) callconv(.C) RocStr { + return @call(.always_inline, strFromFloatHelp, .{ T, float }); + } + }.func; + + @export(f, .{ .name = name ++ @typeName(T), .linkage = .Strong }); +} + +fn strFromFloatHelp(comptime T: type, float: T) RocStr { + var buf: [400]u8 = undefined; + const result = std.fmt.bufPrint(&buf, "{d}", .{float}) catch unreachable; + + return RocStr.init(&buf, result.len); +} + +// Str.split +pub fn strSplit(string: RocStr, delimiter: RocStr) callconv(.C) RocList { + const segment_count = countSegments(string, delimiter); + const list = RocList.allocate(@alignOf(RocStr), segment_count, @sizeOf(RocStr)); + + if (list.bytes) |bytes| { + const strings = @as([*]RocStr, @ptrCast(@alignCast(bytes))); + strSplitHelp(strings, string, delimiter); + } + + return list; +} + +fn initFromSmallStr(slice_bytes: [*]u8, len: usize, _: usize) RocStr { + return RocStr.init(slice_bytes, len); +} + +// The ref_ptr must already be shifted to be ready for storing in a seamless slice. +fn initFromBigStr(slice_bytes: [*]u8, len: usize, ref_ptr: usize) RocStr { + // Here we can make seamless slices instead of copying to a new small str. + return RocStr{ + .str_bytes = slice_bytes, + .str_len = len | SEAMLESS_SLICE_BIT, + .str_capacity = ref_ptr, + }; +} + +fn strSplitHelp(array: [*]RocStr, string: RocStr, delimiter: RocStr) void { + var ret_array_index: usize = 0; + var slice_start_index: usize = 0; + var str_index: usize = 0; + + const str_bytes = string.asU8ptr(); + const str_len = string.len(); + const ref_ptr = @intFromPtr(string.getRefcountPtr()) >> 1; + const init_fn = if (string.isSmallStr()) + &initFromSmallStr + else + &initFromBigStr; + + const delimiter_bytes_ptrs = delimiter.asU8ptr(); + const delimiter_len = delimiter.len(); + + if (str_len >= delimiter_len and delimiter_len > 0) { + const end_index: usize = str_len - delimiter_len + 1; + while (str_index <= end_index) { + var delimiter_index: usize = 0; + var matches_delimiter = true; + + while (delimiter_index < delimiter_len) { + var delimiterChar = delimiter_bytes_ptrs[delimiter_index]; + + if (str_index + delimiter_index >= str_len) { + matches_delimiter = false; + break; + } + + var strChar = str_bytes[str_index + delimiter_index]; + + if (delimiterChar != strChar) { + matches_delimiter = false; + break; + } + + delimiter_index += 1; + } + + if (matches_delimiter) { + const segment_len: usize = str_index - slice_start_index; + + array[ret_array_index] = init_fn(@constCast(str_bytes) + slice_start_index, segment_len, ref_ptr); + slice_start_index = str_index + delimiter_len; + ret_array_index += 1; + str_index += delimiter_len; + } else { + str_index += 1; + } + } + } + + array[ret_array_index] = init_fn(@constCast(str_bytes) + slice_start_index, str_len - slice_start_index, ref_ptr); + + if (!string.isSmallStr()) { + // Correct refcount for all of the splits made. + string.incref(ret_array_index + 1); + } +} + +test "strSplitHelp: empty delimiter" { + // Str.split "abc" "" == ["abc"] + const str_arr = "abc"; + const str = RocStr.init(str_arr, str_arr.len); + + const delimiter_arr = ""; + const delimiter = RocStr.init(delimiter_arr, delimiter_arr.len); + + var array: [1]RocStr = undefined; + const array_ptr: [*]RocStr = &array; + + strSplitHelp(array_ptr, str, delimiter); + + var expected = [1]RocStr{ + str, + }; + + defer { + for (array) |roc_str| { + roc_str.decref(); + } + + for (expected) |roc_str| { + roc_str.decref(); + } + + str.decref(); + delimiter.decref(); + } + + try expectEqual(array.len, expected.len); + try expect(array[0].eq(expected[0])); +} + +test "strSplitHelp: no delimiter" { + // Str.split "abc" "!" == ["abc"] + const str_arr = "abc"; + const str = RocStr.init(str_arr, str_arr.len); + + const delimiter_arr = "!"; + const delimiter = RocStr.init(delimiter_arr, delimiter_arr.len); + + var array: [1]RocStr = undefined; + const array_ptr: [*]RocStr = &array; + + strSplitHelp(array_ptr, str, delimiter); + + var expected = [1]RocStr{ + str, + }; + + defer { + for (array) |roc_str| { + roc_str.decref(); + } + + for (expected) |roc_str| { + roc_str.decref(); + } + + str.decref(); + delimiter.decref(); + } + + try expectEqual(array.len, expected.len); + try expect(array[0].eq(expected[0])); +} + +test "strSplitHelp: empty start" { + const str_arr = "/a"; + const str = RocStr.init(str_arr, str_arr.len); + + const delimiter_arr = "/"; + const delimiter = RocStr.init(delimiter_arr, delimiter_arr.len); + + const array_len: usize = 2; + var array: [array_len]RocStr = [_]RocStr{ + undefined, + undefined, + }; + const array_ptr: [*]RocStr = &array; + + strSplitHelp(array_ptr, str, delimiter); + + const one = RocStr.init("a", 1); + + var expected = [2]RocStr{ + RocStr.empty(), one, + }; + + defer { + for (array) |rocStr| { + rocStr.decref(); + } + + for (expected) |rocStr| { + rocStr.decref(); + } + + str.decref(); + delimiter.decref(); + } + + try expectEqual(array.len, expected.len); + try expect(array[0].eq(expected[0])); + try expect(array[1].eq(expected[1])); +} + +test "strSplitHelp: empty end" { + const str_arr = "1---- ---- ---- ---- ----2---- ---- ---- ---- ----"; + const str = RocStr.init(str_arr, str_arr.len); + + const delimiter_arr = "---- ---- ---- ---- ----"; + const delimiter = RocStr.init(delimiter_arr, delimiter_arr.len); + + const array_len: usize = 3; + var array: [array_len]RocStr = [_]RocStr{ + undefined, + undefined, + undefined, + }; + const array_ptr: [*]RocStr = &array; + + strSplitHelp(array_ptr, str, delimiter); + + const one = RocStr.init("1", 1); + const two = RocStr.init("2", 1); + + var expected = [3]RocStr{ + one, two, RocStr.empty(), + }; + + defer { + for (array) |rocStr| { + rocStr.decref(); + } + + for (expected) |rocStr| { + rocStr.decref(); + } + + str.decref(); + delimiter.decref(); + } + + try expectEqual(array.len, expected.len); + try expect(array[0].eq(expected[0])); + try expect(array[1].eq(expected[1])); + try expect(array[2].eq(expected[2])); +} + +test "strSplitHelp: string equals delimiter" { + const str_delimiter_arr = "/"; + const str_delimiter = RocStr.init(str_delimiter_arr, str_delimiter_arr.len); + + const array_len: usize = 2; + var array: [array_len]RocStr = [_]RocStr{ + undefined, + undefined, + }; + const array_ptr: [*]RocStr = &array; + + strSplitHelp(array_ptr, str_delimiter, str_delimiter); + + var expected = [2]RocStr{ RocStr.empty(), RocStr.empty() }; + + defer { + for (array) |rocStr| { + rocStr.decref(); + } + + for (expected) |rocStr| { + rocStr.decref(); + } + + str_delimiter.decref(); + } + + try expectEqual(array.len, expected.len); + try expect(array[0].eq(expected[0])); + try expect(array[1].eq(expected[1])); +} + +test "strSplitHelp: delimiter on sides" { + const str_arr = "tttghittt"; + const str = RocStr.init(str_arr, str_arr.len); + + const delimiter_arr = "ttt"; + const delimiter = RocStr.init(delimiter_arr, delimiter_arr.len); + + const array_len: usize = 3; + var array: [array_len]RocStr = [_]RocStr{ + undefined, + undefined, + undefined, + }; + const array_ptr: [*]RocStr = &array; + strSplitHelp(array_ptr, str, delimiter); + + const ghi_arr = "ghi"; + const ghi = RocStr.init(ghi_arr, ghi_arr.len); + + var expected = [3]RocStr{ + RocStr.empty(), ghi, RocStr.empty(), + }; + + defer { + for (array) |rocStr| { + rocStr.decref(); + } + + for (expected) |rocStr| { + rocStr.decref(); + } + + str.decref(); + delimiter.decref(); + } + + try expectEqual(array.len, expected.len); + try expect(array[0].eq(expected[0])); + try expect(array[1].eq(expected[1])); + try expect(array[2].eq(expected[2])); +} + +test "strSplitHelp: three pieces" { + // Str.split "a!b!c" "!" == ["a", "b", "c"] + const str_arr = "a!b!c"; + const str = RocStr.init(str_arr, str_arr.len); + + const delimiter_arr = "!"; + const delimiter = RocStr.init(delimiter_arr, delimiter_arr.len); + + const array_len: usize = 3; + var array: [array_len]RocStr = undefined; + const array_ptr: [*]RocStr = &array; + + strSplitHelp(array_ptr, str, delimiter); + + const a = RocStr.init("a", 1); + const b = RocStr.init("b", 1); + const c = RocStr.init("c", 1); + + var expected_array = [array_len]RocStr{ + a, b, c, + }; + + defer { + for (array) |roc_str| { + roc_str.decref(); + } + + for (expected_array) |roc_str| { + roc_str.decref(); + } + + str.decref(); + delimiter.decref(); + } + + try expectEqual(expected_array.len, array.len); + try expect(array[0].eq(expected_array[0])); + try expect(array[1].eq(expected_array[1])); + try expect(array[2].eq(expected_array[2])); +} + +test "strSplitHelp: overlapping delimiter 1" { + // Str.split "aaa" "aa" == ["", "a"] + const str_arr = "aaa"; + const str = RocStr.init(str_arr, str_arr.len); + + const delimiter_arr = "aa"; + const delimiter = RocStr.init(delimiter_arr, delimiter_arr.len); + + var array: [2]RocStr = undefined; + const array_ptr: [*]RocStr = &array; + + strSplitHelp(array_ptr, str, delimiter); + + var expected = [2]RocStr{ + RocStr.empty(), + RocStr.init("a", 1), + }; + + // strings are all small so we ignore freeing the memory + + try expectEqual(array.len, expected.len); + try expect(array[0].eq(expected[0])); + try expect(array[1].eq(expected[1])); +} + +test "strSplitHelp: overlapping delimiter 2" { + // Str.split "aaa" "aa" == ["", "a"] + const str_arr = "aaaa"; + const str = RocStr.init(str_arr, str_arr.len); + + const delimiter_arr = "aa"; + const delimiter = RocStr.init(delimiter_arr, delimiter_arr.len); + + var array: [3]RocStr = undefined; + const array_ptr: [*]RocStr = &array; + + strSplitHelp(array_ptr, str, delimiter); + + var expected = [3]RocStr{ + RocStr.empty(), + RocStr.empty(), + RocStr.empty(), + }; + + // strings are all small so we ignore freeing the memory + + try expectEqual(array.len, expected.len); + try expect(array[0].eq(expected[0])); + try expect(array[1].eq(expected[1])); + try expect(array[2].eq(expected[2])); +} + +// This is used for `Str.split : Str, Str -> Array Str +// It is used to count how many segments the input `_str` +// needs to be broken into, so that we can allocate a array +// of that size. It always returns at least 1. +pub fn countSegments(string: RocStr, delimiter: RocStr) callconv(.C) usize { + const str_bytes = string.asU8ptr(); + const str_len = string.len(); + + const delimiter_bytes_ptrs = delimiter.asU8ptr(); + const delimiter_len = delimiter.len(); + + var count: usize = 1; + + if (str_len >= delimiter_len and delimiter_len > 0) { + var str_index: usize = 0; + const end_cond: usize = str_len - delimiter_len + 1; + + while (str_index < end_cond) { + var delimiter_index: usize = 0; + + var matches_delimiter = true; + + while (delimiter_index < delimiter_len) { + const delimiterChar = delimiter_bytes_ptrs[delimiter_index]; + const strChar = str_bytes[str_index + delimiter_index]; + + if (delimiterChar != strChar) { + matches_delimiter = false; + break; + } + + delimiter_index += 1; + } + + if (matches_delimiter) { + count += 1; + str_index += delimiter_len; + } else { + str_index += 1; + } + } + } + + return count; +} + +test "countSegments: long delimiter" { + // Str.split "str" "delimiter" == ["str"] + // 1 segment + const str_arr = "str"; + const str = RocStr.init(str_arr, str_arr.len); + + const delimiter_arr = "delimiter"; + const delimiter = RocStr.init(delimiter_arr, delimiter_arr.len); + + defer { + str.decref(); + delimiter.decref(); + } + + const segments_count = countSegments(str, delimiter); + try expectEqual(segments_count, 1); +} + +test "countSegments: delimiter at start" { + // Str.split "hello there" "hello" == ["", " there"] + // 2 segments + const str_arr = "hello there"; + const str = RocStr.init(str_arr, str_arr.len); + + const delimiter_arr = "hello"; + const delimiter = RocStr.init(delimiter_arr, delimiter_arr.len); + + defer { + str.decref(); + delimiter.decref(); + } + + const segments_count = countSegments(str, delimiter); + + try expectEqual(segments_count, 2); +} + +test "countSegments: delimiter interspered" { + // Str.split "a!b!c" "!" == ["a", "b", "c"] + // 3 segments + const str_arr = "a!b!c"; + const str = RocStr.init(str_arr, str_arr.len); + + const delimiter_arr = "!"; + const delimiter = RocStr.init(delimiter_arr, delimiter_arr.len); + + defer { + str.decref(); + delimiter.decref(); + } + + const segments_count = countSegments(str, delimiter); + + try expectEqual(segments_count, 3); +} + +test "countSegments: string equals delimiter" { + // Str.split "/" "/" == ["", ""] + // 2 segments + const str_delimiter_arr = "/"; + const str_delimiter = RocStr.init(str_delimiter_arr, str_delimiter_arr.len); + + defer { + str_delimiter.decref(); + } + + const segments_count = countSegments(str_delimiter, str_delimiter); + + try expectEqual(segments_count, 2); +} + +test "countSegments: overlapping delimiter 1" { + // Str.split "aaa" "aa" == ["", "a"] + const segments_count = countSegments(RocStr.init("aaa", 3), RocStr.init("aa", 2)); + + try expectEqual(segments_count, 2); +} + +test "countSegments: overlapping delimiter 2" { + // Str.split "aaa" "aa" == ["", "a"] + const segments_count = countSegments(RocStr.init("aaaa", 4), RocStr.init("aa", 2)); + + try expectEqual(segments_count, 3); +} + +// Str.countGraphemeClusters +pub fn countGraphemeClusters(string: RocStr) callconv(.C) usize { + if (string.isEmpty()) { + return 0; + } + + const bytes_len = string.len(); + const bytes_ptr = string.asU8ptr(); + + var bytes = bytes_ptr[0..bytes_len]; + var iter = (unicode.Utf8View.init(bytes) catch unreachable).iterator(); + + var count: usize = 0; + var grapheme_break_state: ?grapheme.BoundClass = null; + var grapheme_break_state_ptr = &grapheme_break_state; + var opt_last_codepoint: ?u21 = null; + while (iter.nextCodepoint()) |cur_codepoint| { + if (opt_last_codepoint) |last_codepoint| { + var did_break = grapheme.isGraphemeBreak(last_codepoint, cur_codepoint, grapheme_break_state_ptr); + if (did_break) { + count += 1; + grapheme_break_state = null; + } + } + opt_last_codepoint = cur_codepoint; + } + + // If there are no breaks, but the str is not empty, then there + // must be a single grapheme + if (bytes_len != 0) { + count += 1; + } + + return count; +} + +// Str.graphemes +pub fn strGraphemes(roc_str: RocStr) callconv(.C) RocList { + var break_state: ?grapheme.BoundClass = null; + var opt_last_codepoint: ?u21 = null; + var index: usize = 0; + var last_codepoint_len: u8 = 0; + + const ref_ptr = @intFromPtr(roc_str.getRefcountPtr()) >> 1; + const init_fn = if (roc_str.isSmallStr()) + &initFromSmallStr + else + &initFromBigStr; + + var result = RocList.allocate(@alignOf(RocStr), countGraphemeClusters(roc_str), @sizeOf(RocStr)); + const graphemes = result.elements(RocStr) orelse return result; + var slice = roc_str.asSlice(); + var iter = (unicode.Utf8View.init(slice) catch unreachable).iterator(); + + while (iter.nextCodepoint()) |cur_codepoint| { + const cur_codepoint_len = unicode.utf8CodepointSequenceLength(cur_codepoint) catch unreachable; + if (opt_last_codepoint) |last_codepoint| { + var did_break = grapheme.isGraphemeBreak(last_codepoint, cur_codepoint, &break_state); + if (did_break) { + graphemes[index] = init_fn(@constCast(slice.ptr), last_codepoint_len, ref_ptr); + slice = slice[last_codepoint_len..]; + index += 1; + break_state = null; + last_codepoint_len = 0; + } + } + last_codepoint_len += cur_codepoint_len; + opt_last_codepoint = cur_codepoint; + } + // Append last grapheme + graphemes[index] = init_fn(@constCast(slice.ptr), slice.len, ref_ptr); + + if (!roc_str.isSmallStr()) { + // Correct refcount for all of the splits made. + roc_str.incref(index + 1); + } + return result; +} + +// these test both countGraphemeClusters() and strGraphemes() +fn graphemesTest(input: []const u8, expected: []const []const u8) !void { + const rocstr = RocStr.fromSlice(input); + defer rocstr.decref(); + const count = countGraphemeClusters(rocstr); + try expectEqual(expected.len, count); + + const graphemes = strGraphemes(rocstr); + defer graphemes.decref(@sizeOf(u8)); + if (input.len == 0) return; // empty string + const elems = graphemes.elements(RocStr) orelse unreachable; + for (expected, 0..) |g, i| { + try std.testing.expectEqualStrings(g, elems[i].asSlice()); + } +} + +test "graphemes: empty string" { + try graphemesTest("", &.{}); +} + +test "graphemes: ascii characters" { + try graphemesTest("abcd", &.{ "a", "b", "c", "d" }); +} + +test "graphemes: utf8 characters" { + try graphemesTest("ãxā", &.{ "ã", "x", "ā" }); +} + +test "graphemes: emojis" { + try graphemesTest("🤔🤔🤔", &.{ "🤔", "🤔", "🤔" }); +} + +test "graphemes: emojis and ut8 characters" { + try graphemesTest("🤔å🤔¥🤔ç", &.{ "🤔", "å", "🤔", "¥", "🤔", "ç" }); +} + +test "graphemes: emojis, ut8, and ascii characters" { + try graphemesTest("6🤔å🤔e¥🤔çpp", &.{ "6", "🤔", "å", "🤔", "e", "¥", "🤔", "ç", "p", "p" }); +} + +pub fn countUtf8Bytes(string: RocStr) callconv(.C) usize { + return string.len(); +} + +pub fn isEmpty(string: RocStr) callconv(.C) bool { + return string.isEmpty(); +} + +pub fn getCapacity(string: RocStr) callconv(.C) usize { + return string.getCapacity(); +} + +pub fn substringUnsafe(string: RocStr, start: usize, length: usize) callconv(.C) RocStr { + const slice = string.asSlice()[start .. start + length]; + return RocStr.fromSlice(slice); +} + +pub fn getUnsafe(string: RocStr, index: usize) callconv(.C) u8 { + return string.getUnchecked(index); +} + +test "substringUnsafe: start" { + const str = RocStr.fromSlice("abcdef"); + defer str.decref(); + + const expected = RocStr.fromSlice("abc"); + defer expected.decref(); + + const actual = substringUnsafe(str, 0, 3); + + try expect(RocStr.eq(actual, expected)); +} + +test "substringUnsafe: middle" { + const str = RocStr.fromSlice("abcdef"); + defer str.decref(); + + const expected = RocStr.fromSlice("bcd"); + defer expected.decref(); + + const actual = substringUnsafe(str, 1, 3); + + try expect(RocStr.eq(actual, expected)); +} + +test "substringUnsafe: end" { + const str = RocStr.fromSlice("a string so long it is heap-allocated"); + defer str.decref(); + + const expected = RocStr.fromSlice("heap-allocated"); + defer expected.decref(); + + const actual = substringUnsafe(str, 23, 37 - 23); + + try expect(RocStr.eq(actual, expected)); +} + +// Str.startsWith +pub fn startsWith(string: RocStr, prefix: RocStr) callconv(.C) bool { + const bytes_len = string.len(); + const bytes_ptr = string.asU8ptr(); + + const prefix_len = prefix.len(); + const prefix_ptr = prefix.asU8ptr(); + + if (prefix_len > bytes_len) { + return false; + } + + // we won't exceed bytes_len due to the previous check + var i: usize = 0; + while (i < prefix_len) { + if (bytes_ptr[i] != prefix_ptr[i]) { + return false; + } + i += 1; + } + return true; +} + +// Str.repeat +pub fn repeat(string: RocStr, count: usize) callconv(.C) RocStr { + const bytes_len = string.len(); + const bytes_ptr = string.asU8ptr(); + + var ret_string = RocStr.allocate(count * bytes_len); + var ret_string_ptr = ret_string.asU8ptrMut(); + + var i: usize = 0; + while (i < count) : (i += 1) { + @memcpy(ret_string_ptr[0..bytes_len], bytes_ptr[0..bytes_len]); + ret_string_ptr += bytes_len; + } + + return ret_string; +} + +// Str.startsWithScalar +pub fn startsWithScalar(string: RocStr, prefix: u32) callconv(.C) bool { + const str_len = string.len(); + + if (str_len == 0) { + return false; + } + + // Write this (non-empty) string's first scalar into `first_scalar` + var first_scalar: [1]u32 = undefined; + + _ = writeNextScalar(string, 0, &first_scalar, 0); + + // Return whether `first_scalar` equals `prefix` + return @as(*u32, @ptrCast(&first_scalar)).* == prefix; +} + +test "startsWithScalar: empty string" { + const whole = RocStr.empty(); + const prefix: u32 = 'x'; + try expect(!startsWithScalar(whole, prefix)); +} + +test "startsWithScalar: ascii char" { + const whole = RocStr.fromSlice("foobar"); + const prefix: u32 = 'f'; + try expect(startsWithScalar(whole, prefix)); +} + +test "startsWithScalar: emoji" { + const yes = RocStr.fromSlice("💖foobar"); + const no = RocStr.fromSlice("foobar"); + const prefix: u32 = '💖'; + + try expect(startsWithScalar(yes, prefix)); + try expect(!startsWithScalar(no, prefix)); +} + +test "startsWith: foo starts with fo" { + const foo = RocStr.fromSlice("foo"); + const fo = RocStr.fromSlice("fo"); + try expect(startsWith(foo, fo)); +} + +test "startsWith: 123456789123456789 starts with 123456789123456789" { + const str = RocStr.fromSlice("123456789123456789"); + defer str.decref(); + try expect(startsWith(str, str)); +} + +test "startsWith: 12345678912345678910 starts with 123456789123456789" { + const str = RocStr.fromSlice("12345678912345678910"); + defer str.decref(); + const prefix = RocStr.fromSlice("123456789123456789"); + defer prefix.decref(); + + try expect(startsWith(str, prefix)); +} + +// Str.endsWith +pub fn endsWith(string: RocStr, suffix: RocStr) callconv(.C) bool { + const bytes_len = string.len(); + const bytes_ptr = string.asU8ptr(); + + const suffix_len = suffix.len(); + const suffix_ptr = suffix.asU8ptr(); + + if (suffix_len > bytes_len) { + return false; + } + + const offset: usize = bytes_len - suffix_len; + var i: usize = 0; + while (i < suffix_len) { + if (bytes_ptr[i + offset] != suffix_ptr[i]) { + return false; + } + i += 1; + } + return true; +} + +test "endsWith: foo ends with oo" { + const foo = RocStr.init("foo", 3); + const oo = RocStr.init("oo", 2); + defer foo.decref(); + defer oo.decref(); + + try expect(endsWith(foo, oo)); +} + +test "endsWith: 123456789123456789 ends with 123456789123456789" { + const str = RocStr.init("123456789123456789", 18); + defer str.decref(); + try expect(endsWith(str, str)); +} + +test "endsWith: 12345678912345678910 ends with 345678912345678910" { + const str = RocStr.init("12345678912345678910", 20); + const suffix = RocStr.init("345678912345678910", 18); + defer str.decref(); + defer suffix.decref(); + + try expect(endsWith(str, suffix)); +} + +test "endsWith: hello world ends with world" { + const str = RocStr.init("hello world", 11); + const suffix = RocStr.init("world", 5); + defer str.decref(); + defer suffix.decref(); + + try expect(endsWith(str, suffix)); +} + +// Str.concat +pub fn strConcatC(arg1: RocStr, arg2: RocStr) callconv(.C) RocStr { + return @call(.always_inline, strConcat, .{ arg1, arg2 }); +} + +fn strConcat(arg1: RocStr, arg2: RocStr) RocStr { + // NOTE: we don't special-case the first argument being empty. That is because it is owned and + // may have sufficient capacity to store the rest of the list. + if (arg2.isEmpty()) { + // the first argument is owned, so we can return it without cloning + return arg1; + } else { + const combined_length = arg1.len() + arg2.len(); + + var result = arg1.reallocate(combined_length); + @memcpy(result.asU8ptrMut()[arg1.len()..combined_length], arg2.asU8ptr()[0..arg2.len()]); + + return result; + } +} + +test "RocStr.concat: small concat small" { + const str1_len = 3; + var str1: [str1_len]u8 = "foo".*; + const str1_ptr: [*]u8 = &str1; + var roc_str1 = RocStr.init(str1_ptr, str1_len); + + const str2_len = 3; + var str2: [str2_len]u8 = "abc".*; + const str2_ptr: [*]u8 = &str2; + var roc_str2 = RocStr.init(str2_ptr, str2_len); + + const str3_len = 6; + var str3: [str3_len]u8 = "fooabc".*; + const str3_ptr: [*]u8 = &str3; + var roc_str3 = RocStr.init(str3_ptr, str3_len); + + defer { + roc_str1.decref(); + roc_str2.decref(); + roc_str3.decref(); + } + + const result = strConcat(roc_str1, roc_str2); + + defer result.decref(); + + try expect(roc_str3.eq(result)); +} + +pub const RocListStr = extern struct { + list_elements: ?[*]RocStr, + list_length: usize, + list_capacity_or_ref_ptr: usize, +}; + +// Str.joinWith +pub fn strJoinWithC(list: RocList, separator: RocStr) callconv(.C) RocStr { + const roc_list_str = RocListStr{ + .list_elements = @as(?[*]RocStr, @ptrCast(@alignCast(list.bytes))), + .list_length = list.length, + .list_capacity_or_ref_ptr = list.capacity_or_ref_ptr, + }; + + return @call(.always_inline, strJoinWith, .{ roc_list_str, separator }); +} + +fn strJoinWith(list: RocListStr, separator: RocStr) RocStr { + const len = list.list_length; + + if (len == 0) { + return RocStr.empty(); + } else { + const ptr = @as([*]RocStr, @ptrCast(list.list_elements)); + const slice: []RocStr = ptr[0..len]; + + // determine the size of the result + var total_size: usize = 0; + for (slice) |substr| { + total_size += substr.len(); + } + + // include size of the separator + total_size += separator.len() * (len - 1); + + var result = RocStr.allocate(total_size); + var result_ptr = result.asU8ptrMut(); + + var offset: usize = 0; + for (slice[0 .. len - 1]) |substr| { + substr.memcpy(result_ptr + offset); + offset += substr.len(); + + separator.memcpy(result_ptr + offset); + offset += separator.len(); + } + + const substr = slice[len - 1]; + substr.memcpy(result_ptr + offset); + + return result; + } +} + +test "RocStr.joinWith: result is big" { + const sep_len = 2; + var sep: [sep_len]u8 = ", ".*; + const sep_ptr: [*]u8 = &sep; + var roc_sep = RocStr.init(sep_ptr, sep_len); + + const elem_len = 13; + var elem: [elem_len]u8 = "foobarbazspam".*; + const elem_ptr: [*]u8 = &elem; + var roc_elem = RocStr.init(elem_ptr, elem_len); + + const result_len = 43; + var xresult: [result_len]u8 = "foobarbazspam, foobarbazspam, foobarbazspam".*; + const result_ptr: [*]u8 = &xresult; + var roc_result = RocStr.init(result_ptr, result_len); + + var elements: [3]RocStr = .{ roc_elem, roc_elem, roc_elem }; + const list = RocListStr{ + .list_length = 3, + .list_capacity_or_ref_ptr = 3, + .list_elements = @as([*]RocStr, @ptrCast(&elements)), + }; + + defer { + roc_sep.decref(); + roc_elem.decref(); + roc_result.decref(); + } + + const result = strJoinWith(list, roc_sep); + + defer result.decref(); + + try expect(roc_result.eq(result)); +} + +// Str.toUtf8 +pub fn strToUtf8C(arg: RocStr) callconv(.C) RocList { + return strToBytes(arg); +} + +inline fn strToBytes(arg: RocStr) RocList { + const length = arg.len(); + if (length == 0) { + return RocList.empty(); + } else if (arg.isSmallStr()) { + const ptr = utils.allocateWithRefcount(length, RocStr.alignment); + + @memcpy(ptr[0..length], arg.asU8ptr()[0..length]); + + return RocList{ .length = length, .bytes = ptr, .capacity_or_ref_ptr = length }; + } else { + const is_seamless_slice = arg.str_len & SEAMLESS_SLICE_BIT; + return RocList{ .length = length, .bytes = arg.str_bytes, .capacity_or_ref_ptr = arg.str_capacity | is_seamless_slice }; + } +} + +const FromUtf8Result = extern struct { + byte_index: usize, + string: RocStr, + is_ok: bool, + problem_code: Utf8ByteProblem, +}; + +const CountAndStart = extern struct { + count: usize, + start: usize, +}; + +pub fn fromUtf8RangeC( + list: RocList, + start: usize, + count: usize, + update_mode: UpdateMode, +) callconv(.C) FromUtf8Result { + return fromUtf8Range(list, start, count, update_mode); +} + +pub fn fromUtf8Range(arg: RocList, start: usize, count: usize, update_mode: UpdateMode) FromUtf8Result { + if (arg.len() == 0 or count == 0) { + arg.decref(RocStr.alignment); + return FromUtf8Result{ + .is_ok = true, + .string = RocStr.empty(), + .byte_index = 0, + .problem_code = Utf8ByteProblem.InvalidStartByte, + }; + } + const bytes = @as([*]const u8, @ptrCast(arg.bytes))[start .. start + count]; + + if (isValidUnicode(bytes)) { + // Make a seamless slice of the input. + const string = RocStr.fromSubListUnsafe(arg, start, count, update_mode); + return FromUtf8Result{ + .is_ok = true, + .string = string, + .byte_index = 0, + .problem_code = Utf8ByteProblem.InvalidStartByte, + }; + } else { + const temp = errorToProblem(@as([*]u8, @ptrCast(arg.bytes)), arg.length); + + // decref the list + arg.decref(RocStr.alignment); + + return FromUtf8Result{ + .is_ok = false, + .string = RocStr.empty(), + .byte_index = temp.index, + .problem_code = temp.problem, + }; + } +} + +fn errorToProblem(bytes: [*]u8, length: usize) struct { index: usize, problem: Utf8ByteProblem } { + var index: usize = 0; + + while (index < length) { + const nextNumBytes = numberOfNextCodepointBytes(bytes, length, index) catch |err| { + switch (err) { + error.UnexpectedEof => { + return .{ .index = index, .problem = Utf8ByteProblem.UnexpectedEndOfSequence }; + }, + error.Utf8InvalidStartByte => return .{ .index = index, .problem = Utf8ByteProblem.InvalidStartByte }, + error.Utf8ExpectedContinuation => return .{ .index = index, .problem = Utf8ByteProblem.ExpectedContinuation }, + error.Utf8OverlongEncoding => return .{ .index = index, .problem = Utf8ByteProblem.OverlongEncoding }, + error.Utf8EncodesSurrogateHalf => return .{ .index = index, .problem = Utf8ByteProblem.EncodesSurrogateHalf }, + error.Utf8CodepointTooLarge => return .{ .index = index, .problem = Utf8ByteProblem.CodepointTooLarge }, + } + }; + index += nextNumBytes; + } + + unreachable; +} + +pub fn isValidUnicode(buf: []const u8) bool { + const size = @sizeOf(u64); + // TODO: we should test changing the step on other platforms. + // The general tradeoff is making extremely large strings potentially much faster + // at the cost of small strings being slightly slower. + const step = size; + var i: usize = 0; + while (i + step < buf.len) { + var bytes: u64 = undefined; + @memcpy(@as([*]u8, @ptrCast(&bytes))[0..size], buf[i..(i + size)]); + const unicode_bytes = bytes & 0x8080_8080_8080_8080; + if (unicode_bytes == 0) { + i += step; + continue; + } + + while (buf[i] < 0b1000_0000) : (i += 1) {} + + while (buf[i] >= 0b1000_0000) { + // This forces prefetching, otherwise the loop can run at about half speed. + if (i + 4 >= buf.len) break; + var small_buf: [4]u8 = undefined; + @memcpy(small_buf[0..4], buf[i..(i + 4)]); + // TODO: Should we always inline these function calls below? + if (std.unicode.utf8ByteSequenceLength(small_buf[0])) |cp_len| { + if (std.meta.isError(std.unicode.utf8Decode(small_buf[0..cp_len]))) { + return false; + } + i += cp_len; + } else |_| { + return false; + } + } + } + + if (i == buf.len) return true; + while (buf[i] < 0b1000_0000) { + i += 1; + if (i == buf.len) return true; + } + + return @call(.always_inline, unicode.utf8ValidateSlice, .{buf[i..]}); +} + +const Utf8DecodeError = error{ + UnexpectedEof, + Utf8InvalidStartByte, + Utf8ExpectedContinuation, + Utf8OverlongEncoding, + Utf8EncodesSurrogateHalf, + Utf8CodepointTooLarge, +}; + +// Essentially unicode.utf8ValidateSlice -> https://github.com/ziglang/zig/blob/0.7.x/lib/std/unicode.zig#L156 +// but only for the next codepoint from the index. Then we return the number of bytes of that codepoint. +// TODO: we only ever use the values 0-4, so can we use smaller int than `usize`? +pub fn numberOfNextCodepointBytes(ptr: [*]u8, len: usize, index: usize) Utf8DecodeError!usize { + const codepoint_len = try unicode.utf8ByteSequenceLength(ptr[index]); + const codepoint_end_index = index + codepoint_len; + if (codepoint_end_index > len) { + return error.UnexpectedEof; + } + _ = try unicode.utf8Decode(ptr[index..codepoint_end_index]); + return codepoint_end_index - index; +} + +// Return types for validateUtf8Bytes +// Values must be in alphabetical order. That is, lowest values are the first alphabetically. +pub const Utf8ByteProblem = enum(u8) { + CodepointTooLarge = 0, + EncodesSurrogateHalf = 1, + ExpectedContinuation = 2, + InvalidStartByte = 3, + OverlongEncoding = 4, + UnexpectedEndOfSequence = 5, +}; + +fn validateUtf8Bytes(bytes: [*]u8, length: usize) FromUtf8Result { + return fromUtf8Range(RocList{ .bytes = bytes, .length = length, .capacity_or_ref_ptr = length }, 0, length, .Immutable); +} + +fn validateUtf8BytesX(str: RocList) FromUtf8Result { + return fromUtf8Range(str, 0, str.len(), .Immutable); +} + +fn expectOk(result: FromUtf8Result) !void { + try expectEqual(result.is_ok, true); +} + +fn sliceHelp(bytes: [*]const u8, length: usize) RocList { + var list = RocList.allocate(RocStr.alignment, length, @sizeOf(u8)); + var list_bytes = list.bytes orelse unreachable; + @memcpy(list_bytes[0..length], bytes[0..length]); + list.length = length; + + return list; +} + +fn toErrUtf8ByteResponse(index: usize, problem: Utf8ByteProblem) FromUtf8Result { + return FromUtf8Result{ .is_ok = false, .string = RocStr.empty(), .byte_index = index, .problem_code = problem }; +} + +// NOTE on memory: the validate function consumes a RC token of the input. Since +// we freshly created it (in `sliceHelp`), it has only one RC token, and input list will be deallocated. +// +// If we tested with big strings, we'd have to deallocate the output string, but never the input list + +test "validateUtf8Bytes: ascii" { + const raw = "abc"; + const ptr: [*]const u8 = @as([*]const u8, @ptrCast(raw)); + const list = sliceHelp(ptr, raw.len); + + const str_result = validateUtf8BytesX(list); + defer str_result.string.decref(); + try expectOk(str_result); +} + +test "validateUtf8Bytes: unicode œ" { + const raw = "œ"; + const ptr: [*]const u8 = @as([*]const u8, @ptrCast(raw)); + const list = sliceHelp(ptr, raw.len); + + const str_result = validateUtf8BytesX(list); + defer str_result.string.decref(); + try expectOk(str_result); +} + +test "validateUtf8Bytes: unicode ∆" { + const raw = "∆"; + const ptr: [*]const u8 = @as([*]const u8, @ptrCast(raw)); + const list = sliceHelp(ptr, raw.len); + + const str_result = validateUtf8BytesX(list); + defer str_result.string.decref(); + try expectOk(str_result); +} + +test "validateUtf8Bytes: emoji" { + const raw = "💖"; + const ptr: [*]const u8 = @as([*]const u8, @ptrCast(raw)); + const list = sliceHelp(ptr, raw.len); + + const str_result = validateUtf8BytesX(list); + defer str_result.string.decref(); + try expectOk(str_result); +} + +test "validateUtf8Bytes: unicode ∆ in middle of array" { + const raw = "œb∆c¬"; + const ptr: [*]const u8 = @as([*]const u8, @ptrCast(raw)); + const list = sliceHelp(ptr, raw.len); + + const str_result = validateUtf8BytesX(list); + defer str_result.string.decref(); + try expectOk(str_result); +} + +fn expectErr(list: RocList, index: usize, err: Utf8DecodeError, problem: Utf8ByteProblem) !void { + const str_ptr = @as([*]u8, @ptrCast(list.bytes)); + const str_len = list.length; + + try expectError(err, numberOfNextCodepointBytes(str_ptr, str_len, index)); + try expectEqual(toErrUtf8ByteResponse(index, problem), validateUtf8Bytes(str_ptr, str_len)); +} + +test "validateUtf8Bytes: invalid start byte" { + // https://github.com/ziglang/zig/blob/0.7.x/lib/std/unicode.zig#L426 + const raw = "ab\x80c"; + const ptr: [*]const u8 = @as([*]const u8, @ptrCast(raw)); + const list = sliceHelp(ptr, raw.len); + + try expectErr(list, 2, error.Utf8InvalidStartByte, Utf8ByteProblem.InvalidStartByte); +} + +test "validateUtf8Bytes: unexpected eof for 2 byte sequence" { + // https://github.com/ziglang/zig/blob/0.7.x/lib/std/unicode.zig#L426 + const raw = "abc\xc2"; + const ptr: [*]const u8 = @as([*]const u8, @ptrCast(raw)); + const list = sliceHelp(ptr, raw.len); + + try expectErr(list, 3, error.UnexpectedEof, Utf8ByteProblem.UnexpectedEndOfSequence); +} + +test "validateUtf8Bytes: expected continuation for 2 byte sequence" { + // https://github.com/ziglang/zig/blob/0.7.x/lib/std/unicode.zig#L426 + const raw = "abc\xc2\x00"; + const ptr: [*]const u8 = @as([*]const u8, @ptrCast(raw)); + const list = sliceHelp(ptr, raw.len); + + try expectErr(list, 3, error.Utf8ExpectedContinuation, Utf8ByteProblem.ExpectedContinuation); +} + +test "validateUtf8Bytes: unexpected eof for 3 byte sequence" { + // https://github.com/ziglang/zig/blob/0.7.x/lib/std/unicode.zig#L430 + const raw = "abc\xe0\x00"; + const ptr: [*]const u8 = @as([*]const u8, @ptrCast(raw)); + const list = sliceHelp(ptr, raw.len); + + try expectErr(list, 3, error.UnexpectedEof, Utf8ByteProblem.UnexpectedEndOfSequence); +} + +test "validateUtf8Bytes: expected continuation for 3 byte sequence" { + // https://github.com/ziglang/zig/blob/0.7.x/lib/std/unicode.zig#L430 + const raw = "abc\xe0\xa0\xc0"; + const ptr: [*]const u8 = @as([*]const u8, @ptrCast(raw)); + const list = sliceHelp(ptr, raw.len); + + try expectErr(list, 3, error.Utf8ExpectedContinuation, Utf8ByteProblem.ExpectedContinuation); +} + +test "validateUtf8Bytes: unexpected eof for 4 byte sequence" { + // https://github.com/ziglang/zig/blob/0.7.x/lib/std/unicode.zig#L437 + const raw = "abc\xf0\x90\x00"; + const ptr: [*]const u8 = @as([*]const u8, @ptrCast(raw)); + const list = sliceHelp(ptr, raw.len); + + try expectErr(list, 3, error.UnexpectedEof, Utf8ByteProblem.UnexpectedEndOfSequence); +} + +test "validateUtf8Bytes: expected continuation for 4 byte sequence" { + // https://github.com/ziglang/zig/blob/0.7.x/lib/std/unicode.zig#L437 + const raw = "abc\xf0\x90\x80\x00"; + const ptr: [*]const u8 = @as([*]const u8, @ptrCast(raw)); + const list = sliceHelp(ptr, raw.len); + + try expectErr(list, 3, error.Utf8ExpectedContinuation, Utf8ByteProblem.ExpectedContinuation); +} + +test "validateUtf8Bytes: overlong" { + // https://github.com/ziglang/zig/blob/0.7.x/lib/std/unicode.zig#L451 + const raw = "abc\xf0\x80\x80\x80"; + const ptr: [*]const u8 = @as([*]const u8, @ptrCast(raw)); + const list = sliceHelp(ptr, raw.len); + + try expectErr(list, 3, error.Utf8OverlongEncoding, Utf8ByteProblem.OverlongEncoding); +} + +test "validateUtf8Bytes: codepoint out too large" { + // https://github.com/ziglang/zig/blob/0.7.x/lib/std/unicode.zig#L465 + const raw = "abc\xf4\x90\x80\x80"; + const ptr: [*]const u8 = @as([*]const u8, @ptrCast(raw)); + const list = sliceHelp(ptr, raw.len); + + try expectErr(list, 3, error.Utf8CodepointTooLarge, Utf8ByteProblem.CodepointTooLarge); +} + +test "validateUtf8Bytes: surrogate halves" { + // https://github.com/ziglang/zig/blob/0.7.x/lib/std/unicode.zig#L468 + const raw = "abc\xed\xa0\x80"; + const ptr: [*]const u8 = @as([*]const u8, @ptrCast(raw)); + const list = sliceHelp(ptr, raw.len); + + try expectErr(list, 3, error.Utf8EncodesSurrogateHalf, Utf8ByteProblem.EncodesSurrogateHalf); +} + +fn isWhitespace(codepoint: u21) bool { + // https://www.unicode.org/Public/UCD/latest/ucd/PropList.txt + return switch (codepoint) { + 0x0009...0x000D => true, // control characters + 0x0020 => true, // space + 0x0085 => true, // control character + 0x00A0 => true, // no-break space + 0x1680 => true, // ogham space + 0x2000...0x200A => true, // en quad..hair space + 0x200E...0x200F => true, // left-to-right & right-to-left marks + 0x2028 => true, // line separator + 0x2029 => true, // paragraph separator + 0x202F => true, // narrow no-break space + 0x205F => true, // medium mathematical space + 0x3000 => true, // ideographic space + + else => false, + }; +} + +test "isWhitespace" { + try expect(isWhitespace(' ')); + try expect(isWhitespace('\u{00A0}')); + try expect(!isWhitespace('x')); +} + +pub fn strTrim(input_string: RocStr) callconv(.C) RocStr { + var string = input_string; + + if (string.isEmpty()) { + string.decref(); + return RocStr.empty(); + } + + const bytes_ptr = string.asU8ptrMut(); + + const leading_bytes = countLeadingWhitespaceBytes(string); + const original_len = string.len(); + + if (original_len == leading_bytes) { + string.decref(); + return RocStr.empty(); + } + + const trailing_bytes = countTrailingWhitespaceBytes(string); + const new_len = original_len - leading_bytes - trailing_bytes; + + if (string.isSmallStr()) { + // Just create another small string of the correct bytes. + // No need to decref because it is a small string. + return RocStr.init(string.asU8ptr() + leading_bytes, new_len); + } else if (leading_bytes == 0 and string.isUnique()) { + // Big and unique with no leading bytes to remove. + // Just take ownership and shrink the length. + var new_string = string; + new_string.str_len = new_len; + + return new_string; + } else if (string.isSeamlessSlice()) { + // Already a seamless slice, just update the range. + return RocStr{ + .str_bytes = bytes_ptr + leading_bytes, + .str_len = new_len | SEAMLESS_SLICE_BIT, + .str_capacity = string.str_capacity, + }; + } else { + // Not unique or removing leading bytes, just make a slice. + return RocStr{ + .str_bytes = bytes_ptr + leading_bytes, + .str_len = new_len | SEAMLESS_SLICE_BIT, + .str_capacity = @intFromPtr(bytes_ptr) >> 1, + }; + } +} + +pub fn strTrimStart(input_string: RocStr) callconv(.C) RocStr { + var string = input_string; + + if (string.isEmpty()) { + string.decref(); + return RocStr.empty(); + } + + const bytes_ptr = string.asU8ptrMut(); + + const leading_bytes = countLeadingWhitespaceBytes(string); + const original_len = string.len(); + + if (original_len == leading_bytes) { + string.decref(); + return RocStr.empty(); + } + + const new_len = original_len - leading_bytes; + + if (string.isSmallStr()) { + // Just create another small string of the correct bytes. + // No need to decref because it is a small string. + return RocStr.init(string.asU8ptr() + leading_bytes, new_len); + } else if (leading_bytes == 0 and string.isUnique()) { + // Big and unique with no leading bytes to remove. + // Just take ownership and shrink the length. + var new_string = string; + new_string.str_len = new_len; + + return new_string; + } else if (string.isSeamlessSlice()) { + // Already a seamless slice, just update the range. + return RocStr{ + .str_bytes = bytes_ptr + leading_bytes, + .str_len = new_len | SEAMLESS_SLICE_BIT, + .str_capacity = string.str_capacity, + }; + } else { + // Not unique or removing leading bytes, just make a slice. + return RocStr{ + .str_bytes = bytes_ptr + leading_bytes, + .str_len = new_len | SEAMLESS_SLICE_BIT, + .str_capacity = @intFromPtr(bytes_ptr) >> 1, + }; + } +} + +pub fn strTrimEnd(input_string: RocStr) callconv(.C) RocStr { + var string = input_string; + + if (string.isEmpty()) { + string.decref(); + return RocStr.empty(); + } + + const bytes_ptr = string.asU8ptrMut(); + + const trailing_bytes = countTrailingWhitespaceBytes(string); + const original_len = string.len(); + + if (original_len == trailing_bytes) { + string.decref(); + return RocStr.empty(); + } + + const new_len = original_len - trailing_bytes; + + if (string.isSmallStr()) { + // Just create another small string of the correct bytes. + // No need to decref because it is a small string. + return RocStr.init(string.asU8ptr(), new_len); + } else if (string.isUnique()) { + // Big and unique with no leading bytes to remove. + // Just take ownership and shrink the length. + var new_string = string; + new_string.str_len = new_len; + + return new_string; + } else if (string.isSeamlessSlice()) { + // Already a seamless slice, just update the range. + return RocStr{ + .str_bytes = bytes_ptr, + .str_len = new_len | SEAMLESS_SLICE_BIT, + .str_capacity = string.str_capacity, + }; + } else { + // Not unique, just make a slice. + return RocStr{ + .str_bytes = bytes_ptr, + .str_len = new_len | SEAMLESS_SLICE_BIT, + .str_capacity = @intFromPtr(bytes_ptr) >> 1, + }; + } +} + +fn countLeadingWhitespaceBytes(string: RocStr) usize { + var byte_count: usize = 0; + + var bytes = string.asU8ptr()[0..string.len()]; + var iter = unicode.Utf8View.initUnchecked(bytes).iterator(); + while (iter.nextCodepoint()) |codepoint| { + if (isWhitespace(codepoint)) { + byte_count += unicode.utf8CodepointSequenceLength(codepoint) catch break; + } else { + break; + } + } + + return byte_count; +} + +fn countTrailingWhitespaceBytes(string: RocStr) usize { + var byte_count: usize = 0; + + var bytes = string.asU8ptr()[0..string.len()]; + var iter = ReverseUtf8View.initUnchecked(bytes).iterator(); + while (iter.nextCodepoint()) |codepoint| { + if (isWhitespace(codepoint)) { + byte_count += unicode.utf8CodepointSequenceLength(codepoint) catch break; + } else { + break; + } + } + + return byte_count; +} + +/// A backwards version of Utf8View from std.unicode +const ReverseUtf8View = struct { + bytes: []const u8, + + pub fn initUnchecked(s: []const u8) ReverseUtf8View { + return ReverseUtf8View{ .bytes = s }; + } + + pub fn iterator(s: ReverseUtf8View) ReverseUtf8Iterator { + return ReverseUtf8Iterator{ + .bytes = s.bytes, + .i = if (s.bytes.len > 0) s.bytes.len - 1 else null, + }; + } +}; + +/// A backwards version of Utf8Iterator from std.unicode +const ReverseUtf8Iterator = struct { + bytes: []const u8, + // NOTE null signifies complete/empty + i: ?usize, + + pub fn nextCodepointSlice(it: *ReverseUtf8Iterator) ?[]const u8 { + if (it.i) |index| { + var i = index; + + // NOTE this relies on the string being valid utf8 to not run off the end + while (!utf8BeginByte(it.bytes[i])) { + i -= 1; + } + + const cp_len = unicode.utf8ByteSequenceLength(it.bytes[i]) catch unreachable; + const slice = it.bytes[i .. i + cp_len]; + + it.i = if (i == 0) null else i - 1; + + return slice; + } else { + return null; + } + } + + pub fn nextCodepoint(it: *ReverseUtf8Iterator) ?u21 { + const slice = it.nextCodepointSlice() orelse return null; + + return switch (slice.len) { + 1 => @as(u21, slice[0]), + 2 => unicode.utf8Decode2(slice) catch unreachable, + 3 => unicode.utf8Decode3(slice) catch unreachable, + 4 => unicode.utf8Decode4(slice) catch unreachable, + else => unreachable, + }; + } +}; + +fn utf8BeginByte(byte: u8) bool { + return switch (byte) { + 0b1000_0000...0b1011_1111 => false, + else => true, + }; +} + +test "strTrim: empty" { + const trimmedEmpty = strTrim(RocStr.empty()); + try expect(trimmedEmpty.eq(RocStr.empty())); +} + +test "strTrim: null byte" { + const bytes = [_]u8{0}; + const original = RocStr.init(&bytes, 1); + + try expectEqual(@as(usize, 1), original.len()); + try expectEqual(@as(usize, SMALL_STR_MAX_LENGTH), original.getCapacity()); + + const original_with_capacity = reserve(original, 40); + defer original_with_capacity.decref(); + + try expectEqual(@as(usize, 1), original_with_capacity.len()); + try expectEqual(@as(usize, 64), original_with_capacity.getCapacity()); + + const trimmed = strTrim(original.clone()); + defer trimmed.decref(); + + try expect(original.eq(trimmed)); +} + +test "strTrim: blank" { + const original_bytes = " "; + const original = RocStr.init(original_bytes, original_bytes.len); + + const trimmed = strTrim(original); + defer trimmed.decref(); + + try expect(trimmed.eq(RocStr.empty())); +} + +test "strTrim: large to large" { + const original_bytes = " hello even more giant world "; + const original = RocStr.init(original_bytes, original_bytes.len); + + try expect(!original.isSmallStr()); + + const expected_bytes = "hello even more giant world"; + const expected = RocStr.init(expected_bytes, expected_bytes.len); + defer expected.decref(); + + try expect(!expected.isSmallStr()); + + const trimmed = strTrim(original); + defer trimmed.decref(); + + try expect(trimmed.eq(expected)); +} + +test "strTrim: large to small sized slice" { + const original_bytes = " hello "; + const original = RocStr.init(original_bytes, original_bytes.len); + + try expect(!original.isSmallStr()); + + const expected_bytes = "hello"; + const expected = RocStr.init(expected_bytes, expected_bytes.len); + defer expected.decref(); + + try expect(expected.isSmallStr()); + + try expect(original.isUnique()); + const trimmed = strTrim(original); + defer trimmed.decref(); + + try expect(trimmed.eq(expected)); + try expect(!trimmed.isSmallStr()); +} + +test "strTrim: small to small" { + const original_bytes = " hello "; + const original = RocStr.init(original_bytes, original_bytes.len); + defer original.decref(); + + try expect(original.isSmallStr()); + + const expected_bytes = "hello"; + const expected = RocStr.init(expected_bytes, expected_bytes.len); + defer expected.decref(); + + try expect(expected.isSmallStr()); + + const trimmed = strTrim(original); + + try expect(trimmed.eq(expected)); + try expect(trimmed.isSmallStr()); +} + +test "strTrimStart: empty" { + const trimmedEmpty = strTrimStart(RocStr.empty()); + try expect(trimmedEmpty.eq(RocStr.empty())); +} + +test "strTrimStart: blank" { + const original_bytes = " "; + const original = RocStr.init(original_bytes, original_bytes.len); + defer original.decref(); + + const trimmed = strTrimStart(original); + + try expect(trimmed.eq(RocStr.empty())); +} + +test "strTrimStart: large to large" { + const original_bytes = " hello even more giant world "; + const original = RocStr.init(original_bytes, original_bytes.len); + defer original.decref(); + + try expect(!original.isSmallStr()); + + const expected_bytes = "hello even more giant world "; + const expected = RocStr.init(expected_bytes, expected_bytes.len); + defer expected.decref(); + + try expect(!expected.isSmallStr()); + + const trimmed = strTrimStart(original); + + try expect(trimmed.eq(expected)); +} + +test "strTrimStart: large to small" { + // `original` will be consumed by the concat; do not free explicitly + const original_bytes = " hello "; + const original = RocStr.init(original_bytes, original_bytes.len); + + try expect(!original.isSmallStr()); + + const expected_bytes = "hello "; + const expected = RocStr.init(expected_bytes, expected_bytes.len); + defer expected.decref(); + + try expect(expected.isSmallStr()); + + const trimmed = strTrimStart(original); + defer trimmed.decref(); + + try expect(trimmed.eq(expected)); + try expect(!trimmed.isSmallStr()); +} + +test "strTrimStart: small to small" { + const original_bytes = " hello "; + const original = RocStr.init(original_bytes, original_bytes.len); + defer original.decref(); + + try expect(original.isSmallStr()); + + const expected_bytes = "hello "; + const expected = RocStr.init(expected_bytes, expected_bytes.len); + defer expected.decref(); + + try expect(expected.isSmallStr()); + + const trimmed = strTrimStart(original); + + try expect(trimmed.eq(expected)); + try expect(trimmed.isSmallStr()); +} + +test "strTrimEnd: empty" { + const trimmedEmpty = strTrimEnd(RocStr.empty()); + try expect(trimmedEmpty.eq(RocStr.empty())); +} + +test "strTrimEnd: blank" { + const original_bytes = " "; + const original = RocStr.init(original_bytes, original_bytes.len); + defer original.decref(); + + const trimmed = strTrimEnd(original); + + try expect(trimmed.eq(RocStr.empty())); +} + +test "strTrimEnd: large to large" { + const original_bytes = " hello even more giant world "; + const original = RocStr.init(original_bytes, original_bytes.len); + defer original.decref(); + + try expect(!original.isSmallStr()); + + const expected_bytes = " hello even more giant world"; + const expected = RocStr.init(expected_bytes, expected_bytes.len); + defer expected.decref(); + + try expect(!expected.isSmallStr()); + + const trimmed = strTrimEnd(original); + + try expect(trimmed.eq(expected)); +} + +test "strTrimEnd: large to small" { + // `original` will be consumed by the concat; do not free explicitly + const original_bytes = " hello "; + const original = RocStr.init(original_bytes, original_bytes.len); + + try expect(!original.isSmallStr()); + + const expected_bytes = " hello"; + const expected = RocStr.init(expected_bytes, expected_bytes.len); + defer expected.decref(); + + try expect(expected.isSmallStr()); + + const trimmed = strTrimEnd(original); + defer trimmed.decref(); + + try expect(trimmed.eq(expected)); + try expect(!trimmed.isSmallStr()); +} + +test "strTrimEnd: small to small" { + const original_bytes = " hello "; + const original = RocStr.init(original_bytes, original_bytes.len); + defer original.decref(); + + try expect(original.isSmallStr()); + + const expected_bytes = " hello"; + const expected = RocStr.init(expected_bytes, expected_bytes.len); + defer expected.decref(); + + try expect(expected.isSmallStr()); + + const trimmed = strTrimEnd(original); + + try expect(trimmed.eq(expected)); + try expect(trimmed.isSmallStr()); +} + +test "ReverseUtf8View: hello world" { + const original_bytes = "hello world"; + const expected_bytes = "dlrow olleh"; + + var i: usize = 0; + var iter = ReverseUtf8View.initUnchecked(original_bytes).iterator(); + while (iter.nextCodepoint()) |codepoint| { + try expect(expected_bytes[i] == codepoint); + i += 1; + } +} + +test "ReverseUtf8View: empty" { + const original_bytes = ""; + + var iter = ReverseUtf8View.initUnchecked(original_bytes).iterator(); + while (iter.nextCodepoint()) |_| { + try expect(false); + } +} + +test "capacity: small string" { + const data_bytes = "foobar"; + var data = RocStr.init(data_bytes, data_bytes.len); + defer data.decref(); + + try expectEqual(data.getCapacity(), SMALL_STR_MAX_LENGTH); +} + +test "capacity: big string" { + const data_bytes = "a string so large that it must be heap-allocated"; + var data = RocStr.init(data_bytes, data_bytes.len); + defer data.decref(); + + try expect(data.getCapacity() >= data_bytes.len); +} + +pub fn appendScalar(string: RocStr, scalar_u32: u32) callconv(.C) RocStr { + const scalar = @as(u21, @intCast(scalar_u32)); + const width = std.unicode.utf8CodepointSequenceLength(scalar) catch unreachable; + + var output = string.reallocate(string.len() + width); + var slice = output.asSliceWithCapacityMut(); + + _ = std.unicode.utf8Encode(scalar, slice[string.len() .. string.len() + width]) catch unreachable; + + return output; +} + +test "appendScalar: small A" { + const A: []const u8 = "A"; + + const data_bytes = "hello"; + var data = RocStr.init(data_bytes, data_bytes.len); + + const actual = appendScalar(data, A[0]); + defer actual.decref(); + + const expected_bytes = "helloA"; + const expected = RocStr.init(expected_bytes, expected_bytes.len); + defer expected.decref(); + + try expect(actual.eq(expected)); +} + +test "appendScalar: small 😀" { + const data_bytes = "hello"; + var data = RocStr.init(data_bytes, data_bytes.len); + + const actual = appendScalar(data, 0x1F600); + defer actual.decref(); + + const expected_bytes = "hello😀"; + const expected = RocStr.init(expected_bytes, expected_bytes.len); + defer expected.decref(); + + try expect(actual.eq(expected)); +} + +test "appendScalar: big A" { + const A: []const u8 = "A"; + + const data_bytes = "a string so large that it must be heap-allocated"; + var data = RocStr.init(data_bytes, data_bytes.len); + + const actual = appendScalar(data, A[0]); + defer actual.decref(); + + const expected_bytes = "a string so large that it must be heap-allocatedA"; + const expected = RocStr.init(expected_bytes, expected_bytes.len); + defer expected.decref(); + + try expect(actual.eq(expected)); +} + +test "appendScalar: big 😀" { + const data_bytes = "a string so large that it must be heap-allocated"; + var data = RocStr.init(data_bytes, data_bytes.len); + + const actual = appendScalar(data, 0x1F600); + defer actual.decref(); + + const expected_bytes = "a string so large that it must be heap-allocated😀"; + const expected = RocStr.init(expected_bytes, expected_bytes.len); + defer expected.decref(); + + try expect(actual.eq(expected)); +} + +pub fn reserve(string: RocStr, spare: usize) callconv(.C) RocStr { + const old_length = string.len(); + if (string.getCapacity() >= old_length + spare) { + return string; + } else { + var output = string.reallocate(old_length + spare); + output.setLen(old_length); + return output; + } +} + +pub fn withCapacity(capacity: usize) callconv(.C) RocStr { + var str = RocStr.allocate(capacity); + str.setLen(0); + return str; +} + +pub fn getScalarUnsafe(string: RocStr, index: usize) callconv(.C) extern struct { bytesParsed: usize, scalar: u32 } { + const slice = string.asSlice(); + const bytesParsed = @as(usize, @intCast(std.unicode.utf8ByteSequenceLength(slice[index]) catch unreachable)); + const scalar = std.unicode.utf8Decode(slice[index .. index + bytesParsed]) catch unreachable; + + return .{ .bytesParsed = bytesParsed, .scalar = @as(u32, @intCast(scalar)) }; +} + +test "getScalarUnsafe" { + const data_bytes = "A"; + var data = RocStr.init(data_bytes, data_bytes.len); + + const result = getScalarUnsafe(data, 0); + + const expected = try std.unicode.utf8Decode("A"); + + try expectEqual(result.scalar, @as(u32, @intCast(expected))); + try expectEqual(result.bytesParsed, 1); +} + +pub fn strCloneTo( + string: RocStr, + ptr: [*]u8, + offset: usize, + extra_offset: usize, +) callconv(.C) usize { + const WIDTH: usize = @sizeOf(RocStr); + if (string.isSmallStr()) { + const array: [@sizeOf(RocStr)]u8 = @as([@sizeOf(RocStr)]u8, @bitCast(string)); + + var i: usize = 0; + while (i < WIDTH) : (i += 1) { + ptr[offset + i] = array[i]; + } + + return extra_offset; + } else { + const slice = string.asSlice(); + + var relative = string; + relative.str_bytes = @as(?[*]u8, @ptrFromInt(extra_offset)); // i.e. just after the string struct + + // write the string struct + const array = relative.asArray(); + @memcpy(ptr[offset..(offset + WIDTH)], array[0..WIDTH]); + + // write the string bytes just after the struct + @memcpy(ptr[extra_offset..(extra_offset + slice.len)], slice); + + return extra_offset + slice.len; + } +} + +pub fn strRefcountPtr( + string: RocStr, +) callconv(.C) ?[*]u8 { + return string.getRefcountPtr(); +} + +pub fn strReleaseExcessCapacity( + string: RocStr, +) callconv(.C) RocStr { + const old_length = string.len(); + // We use the direct list.capacity_or_ref_ptr to make sure both that there is no extra capacity and that it isn't a seamless slice. + if (string.isSmallStr()) { + // SmallStr has no excess capacity. + return string; + } else if (string.isUnique() and !string.isSeamlessSlice() and string.getCapacity() == old_length) { + return string; + } else if (old_length == 0) { + string.decref(); + return RocStr.empty(); + } else { + var output = RocStr.allocateExact(old_length); + const source_ptr = string.asU8ptr(); + const dest_ptr = output.asU8ptrMut(); + + @memcpy(dest_ptr[0..old_length], source_ptr[0..old_length]); + string.decref(); + + return output; + } +} diff --git a/crates/compiler/builtins/bitcode/src/utils.zig b/crates/compiler/builtins/bitcode/src/utils.zig new file mode 100644 index 0000000000..6a77a13d4d --- /dev/null +++ b/crates/compiler/builtins/bitcode/src/utils.zig @@ -0,0 +1,530 @@ +const std = @import("std"); +const builtin = @import("builtin"); +const Monotonic = std.builtin.AtomicOrder.Monotonic; + +const DEBUG_INCDEC = false; +const DEBUG_TESTING_ALLOC = false; +const DEBUG_ALLOC = false; + +pub fn WithOverflow(comptime T: type) type { + return extern struct { value: T, has_overflowed: bool }; +} + +// If allocation fails, this must cxa_throw - it must not return a null pointer! +extern fn roc_alloc(size: usize, alignment: u32) callconv(.C) ?*anyopaque; + +// This should never be passed a null pointer. +// If allocation fails, this must cxa_throw - it must not return a null pointer! +extern fn roc_realloc(c_ptr: *anyopaque, new_size: usize, old_size: usize, alignment: u32) callconv(.C) ?*anyopaque; + +// This should never be passed a null pointer. +extern fn roc_dealloc(c_ptr: *anyopaque, alignment: u32) callconv(.C) void; + +extern fn roc_dbg(file_path: *anyopaque, message: *anyopaque) callconv(.C) void; + +// Since roc_dbg is never used by the builtins, we need at export a function that uses it to stop DCE. +pub fn test_dbg(file_path: *anyopaque, message: *anyopaque) callconv(.C) void { + roc_dbg(file_path, message); +} + +extern fn kill(pid: c_int, sig: c_int) c_int; +extern fn shm_open(name: *const i8, oflag: c_int, mode: c_uint) c_int; +extern fn mmap(addr: ?*anyopaque, length: c_uint, prot: c_int, flags: c_int, fd: c_int, offset: c_uint) *anyopaque; +extern fn getppid() c_int; + +fn testing_roc_getppid() callconv(.C) c_int { + return getppid(); +} + +fn roc_getppid_windows_stub() callconv(.C) c_int { + return 0; +} + +fn testing_roc_shm_open(name: *const i8, oflag: c_int, mode: c_uint) callconv(.C) c_int { + return shm_open(name, oflag, mode); +} +fn testing_roc_mmap(addr: ?*anyopaque, length: c_uint, prot: c_int, flags: c_int, fd: c_int, offset: c_uint) callconv(.C) *anyopaque { + return mmap(addr, length, prot, flags, fd, offset); +} + +fn testing_roc_dbg(file_path: *anyopaque, message: *anyopaque) callconv(.C) void { + _ = message; + _ = file_path; +} + +comptime { + // During tests, use the testing allocators to satisfy these functions. + if (builtin.is_test) { + @export(testing_roc_alloc, .{ .name = "roc_alloc", .linkage = .Strong }); + @export(testing_roc_realloc, .{ .name = "roc_realloc", .linkage = .Strong }); + @export(testing_roc_dealloc, .{ .name = "roc_dealloc", .linkage = .Strong }); + @export(testing_roc_panic, .{ .name = "roc_panic", .linkage = .Strong }); + @export(testing_roc_dbg, .{ .name = "roc_dbg", .linkage = .Strong }); + + if (builtin.os.tag == .macos or builtin.os.tag == .linux) { + @export(testing_roc_getppid, .{ .name = "roc_getppid", .linkage = .Strong }); + @export(testing_roc_mmap, .{ .name = "roc_mmap", .linkage = .Strong }); + @export(testing_roc_shm_open, .{ .name = "roc_shm_open", .linkage = .Strong }); + } + + if (builtin.os.tag == .windows) { + @export(roc_getppid_windows_stub, .{ .name = "roc_getppid", .linkage = .Strong }); + } + } +} + +fn testing_roc_alloc(size: usize, _: u32) callconv(.C) ?*anyopaque { + // We store an extra usize which is the size of the full allocation. + const full_size = size + @sizeOf(usize); + var raw_ptr = (std.testing.allocator.alloc(u8, full_size) catch unreachable).ptr; + @as([*]usize, @alignCast(@ptrCast(raw_ptr)))[0] = full_size; + raw_ptr += @sizeOf(usize); + const ptr = @as(?*anyopaque, @ptrCast(raw_ptr)); + + if (DEBUG_TESTING_ALLOC and builtin.target.cpu.arch != .wasm32) { + std.debug.print("+ alloc {*}: {} bytes\n", .{ ptr, size }); + } + + return ptr; +} + +fn testing_roc_realloc(c_ptr: *anyopaque, new_size: usize, old_size: usize, _: u32) callconv(.C) ?*anyopaque { + const raw_ptr = @as([*]u8, @ptrCast(c_ptr)) - @sizeOf(usize); + const slice = raw_ptr[0..(old_size + @sizeOf(usize))]; + + const new_full_size = new_size + @sizeOf(usize); + var new_raw_ptr = (std.testing.allocator.realloc(slice, new_full_size) catch unreachable).ptr; + @as([*]usize, @alignCast(@ptrCast(new_raw_ptr)))[0] = new_full_size; + new_raw_ptr += @sizeOf(usize); + const new_ptr = @as(?*anyopaque, @ptrCast(new_raw_ptr)); + + if (DEBUG_TESTING_ALLOC and builtin.target.cpu.arch != .wasm32) { + std.debug.print("- realloc {*}\n", .{new_ptr}); + } + + return new_ptr; +} + +fn testing_roc_dealloc(c_ptr: *anyopaque, _: u32) callconv(.C) void { + const raw_ptr = @as([*]u8, @ptrCast(c_ptr)) - @sizeOf(usize); + const full_size = @as([*]usize, @alignCast(@ptrCast(raw_ptr)))[0]; + const slice = raw_ptr[0..full_size]; + + if (DEBUG_TESTING_ALLOC and builtin.target.cpu.arch != .wasm32) { + std.debug.print("💀 dealloc {*}\n", .{slice.ptr}); + } + + std.testing.allocator.free(slice); +} + +fn testing_roc_panic(c_ptr: *anyopaque, tag_id: u32) callconv(.C) void { + _ = c_ptr; + _ = tag_id; + + @panic("Roc panicked"); +} + +pub fn alloc(size: usize, alignment: u32) ?[*]u8 { + return @as(?[*]u8, @ptrCast(roc_alloc(size, alignment))); +} + +pub fn realloc(c_ptr: [*]u8, new_size: usize, old_size: usize, alignment: u32) [*]u8 { + if (DEBUG_INCDEC and builtin.target.cpu.arch != .wasm32) { + std.debug.print("- realloc {*}\n", .{c_ptr}); + } + return @as([*]u8, @ptrCast(roc_realloc(c_ptr, new_size, old_size, alignment))); +} + +pub fn dealloc(c_ptr: [*]u8, alignment: u32) void { + return roc_dealloc(c_ptr, alignment); +} + +// indirection because otherwise zig creates an alias to the panic function which our LLVM code +// does not know how to deal with +pub fn test_panic(c_ptr: *anyopaque, crash_tag: u32) callconv(.C) void { + _ = c_ptr; + _ = crash_tag; + + // const cstr = @ptrCast([*:0]u8, c_ptr); + // + // const stderr = std.io.getStdErr().writer(); + // stderr.print("Roc panicked: {s}!\n", .{cstr}) catch unreachable; + // + // std.c.exit(1); +} + +pub const Inc = fn (?[*]u8) callconv(.C) void; +pub const IncN = fn (?[*]u8, u64) callconv(.C) void; +pub const Dec = fn (?[*]u8) callconv(.C) void; + +const REFCOUNT_MAX_ISIZE: isize = 0; +pub const REFCOUNT_ONE_ISIZE: isize = std.math.minInt(isize); +pub const REFCOUNT_ONE: usize = @as(usize, @bitCast(REFCOUNT_ONE_ISIZE)); + +pub const IntWidth = enum(u8) { + U8 = 0, + U16 = 1, + U32 = 2, + U64 = 3, + U128 = 4, + I8 = 5, + I16 = 6, + I32 = 7, + I64 = 8, + I128 = 9, +}; + +const Refcount = enum { + none, + normal, + atomic, +}; + +const RC_TYPE = Refcount.normal; + +pub fn increfRcPtrC(ptr_to_refcount: *isize, amount: isize) callconv(.C) void { + if (RC_TYPE == Refcount.none) return; + + if (DEBUG_INCDEC and builtin.target.cpu.arch != .wasm32) { + std.debug.print("| increment {*}: ", .{ptr_to_refcount}); + } + + // Ensure that the refcount is not whole program lifetime. + if (ptr_to_refcount.* != REFCOUNT_MAX_ISIZE) { + // Note: we assume that a refcount will never overflow. + // As such, we do not need to cap incrementing. + switch (RC_TYPE) { + Refcount.normal => { + if (DEBUG_INCDEC and builtin.target.cpu.arch != .wasm32) { + const old = @as(usize, @bitCast(ptr_to_refcount.*)); + const new = old + @as(usize, @intCast(amount)); + + const oldH = old - REFCOUNT_ONE + 1; + const newH = new - REFCOUNT_ONE + 1; + + std.debug.print("{} + {} = {}!\n", .{ oldH, amount, newH }); + } + + ptr_to_refcount.* += amount; + }, + Refcount.atomic => { + _ = @atomicRmw(isize, ptr_to_refcount, std.builtin.AtomicRmwOp.Add, amount, Monotonic); + }, + Refcount.none => unreachable, + } + } +} + +pub fn decrefRcPtrC( + bytes_or_null: ?[*]isize, + alignment: u32, +) callconv(.C) void { + // IMPORTANT: bytes_or_null is this case is expected to be a pointer to the refcount + // (NOT the start of the data, or the start of the allocation) + + // this is of course unsafe, but we trust what we get from the llvm side + var bytes = @as([*]isize, @ptrCast(bytes_or_null)); + + return @call(.always_inline, decref_ptr_to_refcount, .{ bytes, alignment }); +} + +pub fn decrefCheckNullC( + bytes_or_null: ?[*]u8, + alignment: u32, +) callconv(.C) void { + if (bytes_or_null) |bytes| { + const isizes: [*]isize = @as([*]isize, @ptrCast(@alignCast(bytes))); + return @call(.always_inline, decref_ptr_to_refcount, .{ isizes - 1, alignment }); + } +} + +pub fn decrefDataPtrC( + bytes_or_null: ?[*]u8, + alignment: u32, +) callconv(.C) void { + var bytes = bytes_or_null orelse return; + + const data_ptr = @intFromPtr(bytes); + const tag_mask: usize = if (@sizeOf(usize) == 8) 0b111 else 0b11; + const unmasked_ptr = data_ptr & ~tag_mask; + + const isizes: [*]isize = @as([*]isize, @ptrFromInt(unmasked_ptr)); + const rc_ptr = isizes - 1; + + return decrefRcPtrC(rc_ptr, alignment); +} + +pub fn increfDataPtrC( + bytes_or_null: ?[*]u8, + inc_amount: isize, +) callconv(.C) void { + var bytes = bytes_or_null orelse return; + + const ptr = @intFromPtr(bytes); + const tag_mask: usize = if (@sizeOf(usize) == 8) 0b111 else 0b11; + const masked_ptr = ptr & ~tag_mask; + + const isizes: *isize = @as(*isize, @ptrFromInt(masked_ptr - @sizeOf(usize))); + + return increfRcPtrC(isizes, inc_amount); +} + +pub fn freeDataPtrC( + bytes_or_null: ?[*]u8, + alignment: u32, +) callconv(.C) void { + var bytes = bytes_or_null orelse return; + + const ptr = @intFromPtr(bytes); + const tag_mask: usize = if (@sizeOf(usize) == 8) 0b111 else 0b11; + const masked_ptr = ptr & ~tag_mask; + + const isizes: [*]isize = @as([*]isize, @ptrFromInt(masked_ptr)); + + // we always store the refcount right before the data + return freeRcPtrC(isizes - 1, alignment); +} + +pub fn freeRcPtrC( + bytes_or_null: ?[*]isize, + alignment: u32, +) callconv(.C) void { + var bytes = bytes_or_null orelse return; + return free_ptr_to_refcount(bytes, alignment); +} + +pub fn decref( + bytes_or_null: ?[*]u8, + data_bytes: usize, + alignment: u32, +) void { + if (data_bytes == 0) { + return; + } + + var bytes = bytes_or_null orelse return; + + const isizes: [*]isize = @as([*]isize, @ptrCast(@alignCast(bytes))); + + decref_ptr_to_refcount(isizes - 1, alignment); +} + +inline fn free_ptr_to_refcount( + refcount_ptr: [*]isize, + alignment: u32, +) void { + if (RC_TYPE == Refcount.none) return; + const extra_bytes = @max(alignment, @sizeOf(usize)); + const allocation_ptr = @as([*]u8, @ptrCast(refcount_ptr)) - (extra_bytes - @sizeOf(usize)); + + // NOTE: we don't even check whether the refcount is "infinity" here! + dealloc(allocation_ptr, alignment); + + if (DEBUG_ALLOC and builtin.target.cpu.arch != .wasm32) { + std.debug.print("💀 freed {*}\n", .{allocation_ptr}); + } +} + +inline fn decref_ptr_to_refcount( + refcount_ptr: [*]isize, + alignment: u32, +) void { + if (RC_TYPE == Refcount.none) return; + + if (DEBUG_INCDEC and builtin.target.cpu.arch != .wasm32) { + std.debug.print("| decrement {*}: ", .{refcount_ptr}); + } + + // Ensure that the refcount is not whole program lifetime. + const refcount: isize = refcount_ptr[0]; + if (refcount != REFCOUNT_MAX_ISIZE) { + switch (RC_TYPE) { + Refcount.normal => { + const old = @as(usize, @bitCast(refcount)); + refcount_ptr[0] = refcount -% 1; + const new = @as(usize, @bitCast(refcount -% 1)); + + if (DEBUG_INCDEC and builtin.target.cpu.arch != .wasm32) { + const oldH = old - REFCOUNT_ONE + 1; + const newH = new - REFCOUNT_ONE + 1; + + std.debug.print("{} - 1 = {}!\n", .{ oldH, newH }); + } + + if (refcount == REFCOUNT_ONE_ISIZE) { + free_ptr_to_refcount(refcount_ptr, alignment); + } + }, + Refcount.atomic => { + var last = @atomicRmw(isize, &refcount_ptr[0], std.builtin.AtomicRmwOp.Sub, 1, Monotonic); + if (last == REFCOUNT_ONE_ISIZE) { + free_ptr_to_refcount(refcount_ptr, alignment); + } + }, + Refcount.none => unreachable, + } + } +} + +pub fn isUnique( + bytes_or_null: ?[*]u8, +) callconv(.C) bool { + var bytes = bytes_or_null orelse return true; + + const ptr = @intFromPtr(bytes); + const tag_mask: usize = if (@sizeOf(usize) == 8) 0b111 else 0b11; + const masked_ptr = ptr & ~tag_mask; + + const isizes: [*]isize = @as([*]isize, @ptrFromInt(masked_ptr)); + + const refcount = (isizes - 1)[0]; + + if (DEBUG_INCDEC and builtin.target.cpu.arch != .wasm32) { + std.debug.print("| is unique {*}\n", .{isizes - 1}); + } + + return refcount == REFCOUNT_ONE_ISIZE; +} + +// We follow roughly the [fbvector](https://github.com/facebook/folly/blob/main/folly/docs/FBVector.md) when it comes to growing a RocList. +// Here is [their growth strategy](https://github.com/facebook/folly/blob/3e0525988fd444201b19b76b390a5927c15cb697/folly/FBVector.h#L1128) for push_back: +// +// (1) initial size +// Instead of growing to size 1 from empty, fbvector allocates at least +// 64 bytes. You may still use reserve to reserve a lesser amount of +// memory. +// (2) 1.5x +// For medium-sized vectors, the growth strategy is 1.5x. See the docs +// for details. +// This does not apply to very small or very large fbvectors. This is a +// heuristic. +// +// In our case, we exposed allocate and reallocate, which will use a smart growth stategy. +// We also expose allocateExact and reallocateExact for case where a specific number of elements is requested. + +// calculateCapacity should only be called in cases the list will be growing. +// requested_length should always be greater than old_capacity. +pub inline fn calculateCapacity( + old_capacity: usize, + requested_length: usize, + element_width: usize, +) usize { + // TODO: there are two adjustments that would likely lead to better results for Roc. + // 1. Deal with the fact we allocate an extra u64 for refcount. + // This may lead to allocating page size + 8 bytes. + // That could mean allocating an entire page for 8 bytes of data which isn't great. + // 2. Deal with the fact that we can request more than 1 element at a time. + // fbvector assumes just appending 1 element at a time when using this algorithm. + // As such, they will generally grow in a way that should better match certain memory multiple. + // This is also the normal case for roc, but we could also grow by a much larger amount. + // We may want to round to multiples of 2 or something similar. + var new_capacity: usize = 0; + if (element_width == 0) { + return requested_length; + } else if (old_capacity == 0) { + new_capacity = 64 / element_width; + } else if (old_capacity < 4096 / element_width) { + new_capacity = old_capacity * 2; + } else if (old_capacity > 4096 * 32 / element_width) { + new_capacity = old_capacity * 2; + } else { + new_capacity = (old_capacity * 3 + 1) / 2; + } + + return @max(new_capacity, requested_length); +} + +pub fn allocateWithRefcountC( + data_bytes: usize, + element_alignment: u32, +) callconv(.C) [*]u8 { + return allocateWithRefcount(data_bytes, element_alignment); +} + +pub fn allocateWithRefcount( + data_bytes: usize, + element_alignment: u32, +) [*]u8 { + const ptr_width = @sizeOf(usize); + const alignment = @max(ptr_width, element_alignment); + const length = alignment + data_bytes; + + var new_bytes: [*]u8 = alloc(length, alignment) orelse unreachable; + + if (DEBUG_ALLOC and builtin.target.cpu.arch != .wasm32) { + std.debug.print("+ allocated {*} ({} bytes with alignment {})\n", .{ new_bytes, data_bytes, alignment }); + } + + const data_ptr = new_bytes + alignment; + const refcount_ptr = @as([*]usize, @ptrCast(@as([*]align(ptr_width) u8, @alignCast(data_ptr)) - ptr_width)); + refcount_ptr[0] = if (RC_TYPE == Refcount.none) REFCOUNT_MAX_ISIZE else REFCOUNT_ONE; + + return data_ptr; +} + +pub const CSlice = extern struct { + pointer: *anyopaque, + len: usize, +}; + +pub fn unsafeReallocate( + source_ptr: [*]u8, + alignment: u32, + old_length: usize, + new_length: usize, + element_width: usize, +) [*]u8 { + const align_width: usize = @max(alignment, @sizeOf(usize)); + + const old_width = align_width + old_length * element_width; + const new_width = align_width + new_length * element_width; + + if (old_width >= new_width) { + return source_ptr; + } + + // TODO handle out of memory + // NOTE realloc will dealloc the original allocation + const old_allocation = source_ptr - align_width; + const new_allocation = realloc(old_allocation, new_width, old_width, alignment); + + const new_source = @as([*]u8, @ptrCast(new_allocation)) + align_width; + return new_source; +} + +pub const Ordering = enum(u8) { + EQ = 0, + GT = 1, + LT = 2, +}; + +pub const UpdateMode = enum(u8) { + Immutable = 0, + InPlace = 1, +}; + +test "increfC, refcounted data" { + var mock_rc: isize = REFCOUNT_ONE_ISIZE + 17; + var ptr_to_refcount: *isize = &mock_rc; + increfRcPtrC(ptr_to_refcount, 2); + try std.testing.expectEqual(mock_rc, REFCOUNT_ONE_ISIZE + 19); +} + +test "increfC, static data" { + var mock_rc: isize = REFCOUNT_MAX_ISIZE; + var ptr_to_refcount: *isize = &mock_rc; + increfRcPtrC(ptr_to_refcount, 2); + try std.testing.expectEqual(mock_rc, REFCOUNT_MAX_ISIZE); +} + +// This returns a compilation dependent pseudo random seed for dictionaries. +// The seed is the address of this function. +// This avoids all roc Dicts using a known seed and being trivial to DOS. +// Still not as secure as true random, but a lot better. +// This value must not change between calls unless Dict is changed to store the seed on creation. +// Note: On esstentially all OSes, this will be affected by ASLR and different each run. +// In wasm, the value will be constant to the build as a whole. +// Either way, it can not be know by an attacker unless they get access to the executable. +pub fn dictPseudoSeed() callconv(.C) u64 { + return @as(u64, @intCast(@intFromPtr(&dictPseudoSeed))); +} diff --git a/crates/compiler/builtins/roc/Bool.roc b/crates/compiler/builtins/roc/Bool.roc new file mode 100644 index 0000000000..c6d331a915 --- /dev/null +++ b/crates/compiler/builtins/roc/Bool.roc @@ -0,0 +1,128 @@ +interface Bool + exposes [Bool, Eq, true, false, and, or, not, isEq, isNotEq] + imports [] + +## Defines a type that can be compared for total equality. +## +## Total equality means that all values of the type can be compared to each +## other, and two values `a`, `b` are identical if and only if `isEq a b` is +## `Bool.true`. +## +## Not all types support total equality. For example, [`F32`](../Num#F32) and [`F64`](../Num#F64) can +## be a `NaN` ([Not a Number](https://en.wikipedia.org/wiki/NaN)), and the +## [IEEE-754](https://en.wikipedia.org/wiki/IEEE_754) floating point standard +## specifies that two `NaN`s are not equal. +Eq implements + ## Returns `Bool.true` if the input values are equal. This is + ## equivalent to the logic + ## [XNOR](https://en.wikipedia.org/wiki/Logical_equality) gate. The infix + ## operator `==` can be used as shorthand for `Bool.isEq`. + ## + ## **Note** that when `isEq` is determined by the Roc compiler, values are + ## compared using structural equality. The rules for this are as follows: + ## + ## 1. Tags are equal if their name and also contents are equal. + ## 2. Records are equal if their fields are equal. + ## 3. The collections [Str], [List], [Dict], and [Set] are equal iff they + ## are the same length and their elements are equal. + ## 4. [Num] values are equal if their numbers are equal. However, if both + ## inputs are *NaN* then `isEq` returns `Bool.false`. Refer to `Num.isNaN` + ## for more detail. + ## 5. Functions cannot be compared for structural equality, therefore Roc + ## cannot derive `isEq` for types that contain functions. + isEq : a, a -> Bool where a implements Eq + +## Represents the boolean true and false using an opaque type. +## `Bool` implements the `Eq` ability. +Bool := [True, False] implements [Eq { isEq: boolIsEq }] + +boolIsEq = \@Bool b1, @Bool b2 -> structuralEq b1 b2 + +## The boolean true value. +true : Bool +true = @Bool True + +## The boolean false value. +false : Bool +false = @Bool False + +## Returns `Bool.true` when both inputs are `Bool.true`. This is equivalent to +## the logic [AND](https://en.wikipedia.org/wiki/Logical_conjunction) +## gate. The infix operator `&&` can also be used as shorthand for +## `Bool.and`. +## +## ``` +## expect (Bool.and Bool.true Bool.true) == Bool.true +## expect (Bool.true && Bool.true) == Bool.true +## expect (Bool.false && Bool.true) == Bool.false +## expect (Bool.true && Bool.false) == Bool.false +## expect (Bool.false && Bool.false) == Bool.false +## ``` +## +## ## Performance Details +## +## In Roc the `&&` and `||` work the same way as any +## other function. However, in some languages `&&` and `||` are special-cased. +## In these languages the compiler will skip evaluating the expression after the +## first operator under certain circumstances. For example an expression like +## `enablePets && likesDogs user` would compile to. +## ``` +## if enablePets then +## likesDogs user +## else +## Bool.false +## ``` +## Roc does not do this because conditionals like `if` and `when` have a +## performance cost. Calling a function can sometimes be faster across the board +## than doing an `if` to decide whether to skip calling it. +and : Bool, Bool -> Bool + +## Returns `Bool.true` when either input is a `Bool.true`. This is equivalent to +## the logic [OR](https://en.wikipedia.org/wiki/Logical_disjunction) gate. +## The infix operator `||` can also be used as shorthand for `Bool.or`. +## ``` +## expect (Bool.or Bool.false Bool.true) == Bool.true +## expect (Bool.true || Bool.true) == Bool.true +## expect (Bool.false || Bool.true) == Bool.true +## expect (Bool.true || Bool.false) == Bool.true +## expect (Bool.false || Bool.false) == Bool.false +## ``` +## +## ## Performance Details +## +## In Roc the `&&` and `||` work the same way as any +## other functions. However, in some languages `&&` and `||` are special-cased. +## Refer to the note in `Bool.and` for more detail. +or : Bool, Bool -> Bool + +## Returns `Bool.false` when given `Bool.true`, and vice versa. This is +## equivalent to the logic [NOT](https://en.wikipedia.org/wiki/Negation) +## gate. The operator `!` can also be used as shorthand for `Bool.not`. +## ``` +## expect (Bool.not Bool.false) == Bool.true +## expect (!Bool.false) == Bool.true +## ``` +not : Bool -> Bool + +## This will call the function `Bool.isEq` on the inputs, and then `Bool.not` +## on the result. The is equivalent to the logic +## [XOR](https://en.wikipedia.org/wiki/Exclusive_or) gate. The infix operator +## `!=` can also be used as shorthand for `Bool.isNotEq`. +## +## **Note** that `isNotEq` does not accept arguments whose types contain +## functions. +## ``` +## expect (Bool.isNotEq Bool.false Bool.true) == Bool.true +## expect (Bool.false != Bool.false) == Bool.false +## expect "Apples" != "Oranges" +## ``` +isNotEq : a, a -> Bool where a implements Eq +isNotEq = \a, b -> structuralNotEq a b + +# INTERNAL COMPILER USE ONLY: used to lower calls to `isEq` to structural +# equality via the `Eq` low-level for derived types. +structuralEq : a, a -> Bool + +# INTERNAL COMPILER USE ONLY: used to lower calls to `isNotEq` to structural +# inequality via the `NotEq` low-level for derived types. +structuralNotEq : a, a -> Bool diff --git a/crates/compiler/builtins/roc/Box.roc b/crates/compiler/builtins/roc/Box.roc new file mode 100644 index 0000000000..e2ca6b945a --- /dev/null +++ b/crates/compiler/builtins/roc/Box.roc @@ -0,0 +1,31 @@ +## Most users won't need Box, it is used for: +## - Holding unknown Roc types when developing [platforms](https://github.com/roc-lang/roc/wiki/Roc-concepts-explained#platform). +## - To improve performance in rare cases. +## +interface Box + exposes [box, unbox] + imports [] + +## Allocates a value on the heap. Boxing is an expensive process as it copies +## the value from the stack to the heap. This may provide a performance +## optimization for advanced use cases with large values. A platform may require +## that some values are boxed. +## ``` +## expect Box.unbox (Box.box "Stack Faster") == "Stack Faster" +## ``` +box : a -> Box a + +## Returns a boxed value. +## ``` +## expect Box.unbox (Box.box "Stack Faster") == "Stack Faster" +## ``` +unbox : Box a -> a + +# # we'd need reset/reuse for box for this to be efficient +# # that is currently not implemented +# map : Box a, (a -> b) -> Box b +# map = \boxed, transform = +# boxed +# |> Box.unbox +# |> transform +# |> Box.box diff --git a/crates/compiler/builtins/roc/Decode.roc b/crates/compiler/builtins/roc/Decode.roc new file mode 100644 index 0000000000..af2800eff3 --- /dev/null +++ b/crates/compiler/builtins/roc/Decode.roc @@ -0,0 +1,171 @@ +interface Decode + exposes [ + DecodeError, + DecodeResult, + Decoder, + Decoding, + DecoderFormatting, + decoder, + u8, + u16, + u32, + u64, + u128, + i8, + i16, + i32, + i64, + i128, + f32, + f64, + dec, + bool, + string, + list, + record, + tuple, + custom, + decodeWith, + fromBytesPartial, + fromBytes, + mapResult, + ] + imports [ + List, + Result.{ Result }, + Num.{ + U8, + U16, + U32, + U64, + U128, + I8, + I16, + I32, + I64, + I128, + Nat, + F32, + F64, + Dec, + }, + Bool.{ Bool }, + ] + +## Error types when decoding a `List U8` of utf-8 bytes using a [Decoder] +DecodeError : [TooShort] + +## Return type of a [Decoder]. +## +## This is can be useful when creating a [custom](#custom) decoder or when +## using [fromBytesPartial](#fromBytesPartial). For example writing unit tests, +## such as; +## ``` +## expect +## input = "\"hello\", " |> Str.toUtf8 +## actual = Decode.fromBytesPartial input Json.json +## expected = Ok "hello" +## +## actual.result == expected +## ``` +DecodeResult val : { result : Result val DecodeError, rest : List U8 } + +## Decodes a `List U8` of utf-8 bytes where `val` is the type of the decoded +## value, and `fmt` is a [Decoder] which implements the [DecoderFormatting] +## ability +Decoder val fmt := List U8, fmt -> DecodeResult val where fmt implements DecoderFormatting + +## Definition of the [Decoding] ability +Decoding implements + decoder : Decoder val fmt where val implements Decoding, fmt implements DecoderFormatting + +## Definition of the [DecoderFormatting] ability +DecoderFormatting implements + u8 : Decoder U8 fmt where fmt implements DecoderFormatting + u16 : Decoder U16 fmt where fmt implements DecoderFormatting + u32 : Decoder U32 fmt where fmt implements DecoderFormatting + u64 : Decoder U64 fmt where fmt implements DecoderFormatting + u128 : Decoder U128 fmt where fmt implements DecoderFormatting + i8 : Decoder I8 fmt where fmt implements DecoderFormatting + i16 : Decoder I16 fmt where fmt implements DecoderFormatting + i32 : Decoder I32 fmt where fmt implements DecoderFormatting + i64 : Decoder I64 fmt where fmt implements DecoderFormatting + i128 : Decoder I128 fmt where fmt implements DecoderFormatting + f32 : Decoder F32 fmt where fmt implements DecoderFormatting + f64 : Decoder F64 fmt where fmt implements DecoderFormatting + dec : Decoder Dec fmt where fmt implements DecoderFormatting + bool : Decoder Bool fmt where fmt implements DecoderFormatting + string : Decoder Str fmt where fmt implements DecoderFormatting + list : Decoder elem fmt -> Decoder (List elem) fmt where fmt implements DecoderFormatting + + ## `record state stepField finalizer` decodes a record field-by-field. + ## + ## `stepField` returns a decoder for the given field in the record, or + ## `Skip` if the field is not a part of the decoded record. + ## + ## `finalizer` should produce the record value from the decoded `state`. + record : state, (state, Str -> [Keep (Decoder state fmt), Skip]), (state -> Result val DecodeError) -> Decoder val fmt where fmt implements DecoderFormatting + + ## `tuple state stepElem finalizer` decodes a tuple element-by-element. + ## + ## `stepElem` returns a decoder for the nth index in the tuple, or + ## `TooLong` if the index is larger than the expected size of the tuple. The + ## index passed to `stepElem` is 0-indexed. + ## + ## `finalizer` should produce the tuple value from the decoded `state`. + tuple : state, (state, Nat -> [Next (Decoder state fmt), TooLong]), (state -> Result val DecodeError) -> Decoder val fmt where fmt implements DecoderFormatting + +## Build a custom [Decoder] function. For example the implementation of +## `decodeBool` could be defined as follows; +## +## ``` +## decodeBool = Decode.custom \bytes, @Json {} -> +## when bytes is +## ['f', 'a', 'l', 's', 'e', ..] -> { result: Ok Bool.false, rest: List.dropFirst bytes 5 } +## ['t', 'r', 'u', 'e', ..] -> { result: Ok Bool.true, rest: List.dropFirst bytes 4 } +## _ -> { result: Err TooShort, rest: bytes } +## ``` +custom : (List U8, fmt -> DecodeResult val) -> Decoder val fmt where fmt implements DecoderFormatting +custom = \decode -> @Decoder decode + +## Decode a `List U8` utf-8 bytes using a specific [Decoder] function +decodeWith : List U8, Decoder val fmt, fmt -> DecodeResult val where fmt implements DecoderFormatting +decodeWith = \bytes, @Decoder decode, fmt -> decode bytes fmt + +## Decode a `List U8` utf-8 bytes and return a [DecodeResult](#DecodeResult) +## ``` +## expect +## input = "\"hello\", " |> Str.toUtf8 +## actual = Decode.fromBytesPartial input Json.json +## expected = Ok "hello" +## +## actual.result == expected +## ``` +fromBytesPartial : List U8, fmt -> DecodeResult val where val implements Decoding, fmt implements DecoderFormatting +fromBytesPartial = \bytes, fmt -> decodeWith bytes decoder fmt + +## Decode a `List U8` utf-8 bytes and return a [Result] with no leftover bytes +## expected. If successful returns `Ok val`, however, if there are bytes +## remaining returns `Err Leftover (List U8)`. +## ``` +## expect +## input = "\"hello\", " |> Str.toUtf8 +## actual = Decode.fromBytes input Json.json +## expected = Ok "hello" +## +## actual == expected +## ``` +fromBytes : List U8, fmt -> Result val [Leftover (List U8)]DecodeError where val implements Decoding, fmt implements DecoderFormatting +fromBytes = \bytes, fmt -> + when fromBytesPartial bytes fmt is + { result, rest } -> + if List.isEmpty rest then + when result is + Ok val -> Ok val + Err TooShort -> Err TooShort + else + Err (Leftover rest) + +## Transform the `val` of a [DecodeResult] +mapResult : DecodeResult a, (a -> b) -> DecodeResult b +mapResult = \{ result, rest }, mapper -> { result: Result.map result mapper, rest } diff --git a/crates/compiler/builtins/roc/Dict.roc b/crates/compiler/builtins/roc/Dict.roc new file mode 100644 index 0000000000..172c0e501e --- /dev/null +++ b/crates/compiler/builtins/roc/Dict.roc @@ -0,0 +1,1448 @@ +interface Dict + exposes [ + Dict, + empty, + withCapacity, + single, + clear, + capacity, + len, + isEmpty, + get, + contains, + insert, + remove, + update, + walk, + walkUntil, + toList, + fromList, + keys, + values, + insertAll, + keepShared, + removeAll, + map, + joinMap, + ] + imports [ + Bool.{ Bool, Eq }, + Result.{ Result }, + List, + Str, + Num.{ Nat, U64, U8, I8 }, + Hash.{ Hasher, Hash }, + Inspect.{ Inspect, Inspector, InspectFormatter }, + ] + +## A [dictionary](https://en.wikipedia.org/wiki/Associative_array) that lets you +## associate keys with values. +## +## ## Inserting +## +## The most basic way to use a dictionary is to start with an empty one and +## then: +## 1. Call [Dict.insert] passing a key and a value, to associate that key with +## that value in the dictionary. +## 2. Later, call [Dict.get] passing the same key as before, and it will return +## the value you stored. +## +## Here's an example of a dictionary which uses a city's name as the key, and +## its population as the associated value. +## ``` +## populationByCity = +## Dict.empty {} +## |> Dict.insert "London" 8_961_989 +## |> Dict.insert "Philadelphia" 1_603_797 +## |> Dict.insert "Shanghai" 24_870_895 +## |> Dict.insert "Delhi" 16_787_941 +## |> Dict.insert "Amsterdam" 872_680 +## ``` +## ## Accessing keys or values +## +## We can use [Dict.keys] and [Dict.values] functions to get only the keys or +## only the values. +## +## You may notice that these lists have the same order as the original insertion +## order. This will be true if all you ever do is [Dict.insert] and [Dict.get] operations +## on the dictionary, but [Dict.remove] operations can change this order. +## +## ## Removing +## +## We can remove an element from the dictionary, like so: +## ``` +## populationByCity +## |> Dict.remove "Philadelphia" +## |> Dict.keys +## == +## ["London", "Amsterdam", "Shanghai", "Delhi"] +## ``` +## Notice that the order has changed. Philadelphia was not only removed from the +## list, but Amsterdam - the last entry we inserted - has been moved into the +## spot where Philadelphia was previously. This is exactly what [Dict.remove] +## does. It removes an element and moves the most recent insertion into the +## vacated spot. +## +## This move is done as a performance optimization, and it lets [remove] have +## [constant time complexity](https://en.wikipedia.org/wiki/Time_complexity#Constant_time). +## +## Dict is inspired by [IndexMap](https://docs.rs/indexmap/latest/indexmap/map/struct.IndexMap.html). +## The internal implementation of a dictionary is similar to [absl::flat_hash_map](https://abseil.io/docs/cpp/guides/container). +## It has a list of keys value pairs that is ordered based on insertion. +## It uses a list of indices into the data as the backing of a hash map. +Dict k v := { + # TODO: Add hashflooding ordered map fall back. + # TODO: Add Groups and SIMD h1 key comparison (initial tests where slower, but with proper SIMD should be fast). + # TODO: As an optimization, we can make all of these lists in one allocation + # TODO: Grow data with the rest of the hashmap. This will require creating a list of garbage data. + # TODO: Change remove to use tombstones. Store the tombstones in a bitmap. + # TODO: define Eq and Hash that are unordered. Only if value implements hash/eq? + metadata : List I8, + dataIndices : List Nat, + data : List (k, v), + size : Nat, +} where k implements Hash & Eq + implements [ + Eq { + isEq, + }, + Hash { + hash: hashDict, + }, + Inspect { + toInspector: toInspectorDict, + }, + ] + +isEq : Dict k v, Dict k v -> Bool where k implements Hash & Eq, v implements Eq +isEq = \xs, ys -> + if len xs != len ys then + Bool.false + else + walkUntil xs Bool.true \_, k, xVal -> + when get ys k is + Ok yVal if yVal == xVal -> + Continue Bool.true + + _ -> + Break Bool.false + +hashDict : hasher, Dict k v -> hasher where k implements Hash & Eq, v implements Hash, hasher implements Hasher +hashDict = \hasher, dict -> Hash.hashUnordered hasher (toList dict) List.walk + +toInspectorDict : Dict k v -> Inspector f where k implements Inspect & Hash & Eq, v implements Inspect, f implements InspectFormatter +toInspectorDict = \dict -> + fmt <- Inspect.custom + Inspect.apply (Inspect.dict dict walk Inspect.toInspector Inspect.toInspector) fmt + +## Return an empty dictionary. +## ``` +## emptyDict = Dict.empty {} +## ``` +empty : {} -> Dict * * +empty = \{} -> + @Dict { + metadata: List.repeat emptySlot 8, + dataIndices: List.repeat 0 8, + data: [], + size: 0, + } + +## Returns the max number of elements the dictionary can hold before requiring a rehash. +## ``` +## foodDict = +## Dict.empty {} +## |> Dict.insert "apple" "fruit" +## +## capacityOfDict = Dict.capacity foodDict +## ``` +capacity : Dict * * -> Nat +capacity = \@Dict { dataIndices } -> + cap = List.len dataIndices + + Num.subWrap cap (Num.shiftRightZfBy cap 3) + +## Return a dictionary with space allocated for a number of entries. This +## may provide a performance optimization if you know how many entries will be +## inserted. +withCapacity : Nat -> Dict * * +withCapacity = \_ -> + # TODO: power of 2 * 8 and actual implementation + empty {} + +## Returns a dictionary containing the key and value provided as input. +## ``` +## expect +## Dict.single "A" "B" +## |> Bool.isEq (Dict.insert (Dict.empty {}) "A" "B") +## ``` +single : k, v -> Dict k v where k implements Hash & Eq +single = \k, v -> + insert (empty {}) k v + +## Returns dictionary with the keys and values specified by the input [List]. +## ``` +## expect +## Dict.single 1 "One" +## |> Dict.insert 2 "Two" +## |> Dict.insert 3 "Three" +## |> Dict.insert 4 "Four" +## |> Bool.isEq (Dict.fromList [(1, "One"), (2, "Two"), (3, "Three"), (4, "Four")]) +## ``` +fromList : List (k, v) -> Dict k v where k implements Hash & Eq +fromList = \data -> + # TODO: make this efficient. Should just set data and then set all indicies in the hashmap. + List.walk data (empty {}) (\dict, (k, v) -> insert dict k v) + +## Returns the number of values in the dictionary. +## ``` +## expect +## Dict.empty {} +## |> Dict.insert "One" "A Song" +## |> Dict.insert "Two" "Candy Canes" +## |> Dict.insert "Three" "Boughs of Holly" +## |> Dict.len +## |> Bool.isEq 3 +## ``` +len : Dict * * -> Nat +len = \@Dict { size } -> + size + +## Check if the dictinoary is empty. +## ``` +## Dict.isEmpty (Dict.empty {} |> Dict.insert "key" 42) +## +## Dict.isEmpty (Dict.empty {}) +## ``` +isEmpty : Dict * * -> Bool +isEmpty = \@Dict { size } -> + size == 0 + +## Clears all elements from a dictionary keeping around the allocation if it isn't huge. +## ``` +## songs = +## Dict.empty {} +## |> Dict.insert "One" "A Song" +## |> Dict.insert "Two" "Candy Canes" +## |> Dict.insert "Three" "Boughs of Holly" +## +## clearSongs = Dict.clear songs +## +## expect Dict.len clearSongs == 0 +## ``` +clear : Dict k v -> Dict k v where k implements Hash & Eq +clear = \@Dict { metadata, dataIndices, data } -> + cap = List.len dataIndices + + # Only clear large allocations. + if cap > 128 * 8 then + empty {} + else + @Dict { + metadata: List.map metadata (\_ -> emptySlot), + # just leave data indicies as garbage, no need to clear. + dataIndices, + # use takeFirst to keep around the capacity. + data: List.takeFirst data 0, + size: 0, + } + +## Convert each value in the dictionary to something new, by calling a conversion +## function on each of them which receives both the key and the old value. Then return a +## new dictionary containing the same keys and the converted values. +map : Dict k a, (k, a -> b) -> Dict k b where k implements Hash & Eq, b implements Hash & Eq +map = \dict, transform -> + init = withCapacity (capacity dict) + + walk dict init \answer, k, v -> + insert answer k (transform k v) + +## Like [Dict.map], except the transformation function wraps the return value +## in a dictionary. At the end, all the dictionaries get joined together +## (using [Dict.insertAll]) into one dictionary. +## +## You may know a similar function named `concatMap` in other languages. +joinMap : Dict a b, (a, b -> Dict x y) -> Dict x y where a implements Hash & Eq, x implements Hash & Eq +joinMap = \dict, transform -> + init = withCapacity (capacity dict) # Might be a pessimization + + walk dict init \answer, k, v -> + insertAll answer (transform k v) + +## Iterate through the keys and values in the dictionary and call the provided +## function with signature `state, k, v -> state` for each value, with an +## initial `state` value provided for the first call. +## ``` +## expect +## Dict.empty {} +## |> Dict.insert "Apples" 12 +## |> Dict.insert "Orange" 24 +## |> Dict.walk 0 (\count, _, qty -> count + qty) +## |> Bool.isEq 36 +## ``` +walk : Dict k v, state, (state, k, v -> state) -> state where k implements Hash & Eq +walk = \@Dict { data }, initialState, transform -> + List.walk data initialState (\state, (k, v) -> transform state k v) + +## Same as [Dict.walk], except you can stop walking early. +## +## ## Performance Details +## +## Compared to [Dict.walk], this can potentially visit fewer elements (which can +## improve performance) at the cost of making each step take longer. +## However, the added cost to each step is extremely small, and can easily +## be outweighed if it results in skipping even a small number of elements. +## +## As such, it is typically better for performance to use this over [Dict.walk] +## if returning `Break` earlier than the last element is expected to be common. +## ``` +## people = +## Dict.empty {} +## |> Dict.insert "Alice" 17 +## |> Dict.insert "Bob" 18 +## |> Dict.insert "Charlie" 19 +## +## isAdult = \_, _, age -> +## if age >= 18 then +## Break Bool.true +## else +## Continue Bool.false +## +## someoneIsAnAdult = Dict.walkUntil people Bool.false isAdult +## +## expect someoneIsAnAdult == Bool.true +## ``` +walkUntil : Dict k v, state, (state, k, v -> [Continue state, Break state]) -> state where k implements Hash & Eq +walkUntil = \@Dict { data }, initialState, transform -> + List.walkUntil data initialState (\state, (k, v) -> transform state k v) + +## Get the value for a given key. If there is a value for the specified key it +## will return [Ok value], otherwise return [Err KeyNotFound]. +## ``` +## dictionary = +## Dict.empty {} +## |> Dict.insert 1 "Apple" +## |> Dict.insert 2 "Orange" +## +## expect Dict.get dictionary 1 == Ok "Apple" +## expect Dict.get dictionary 2000 == Err KeyNotFound +## ``` +get : Dict k v, k -> Result v [KeyNotFound] where k implements Hash & Eq +get = \@Dict { metadata, dataIndices, data }, key -> + hashKey = + createLowLevelHasher PseudoRandSeed + |> Hash.hash key + |> complete + h1Key = h1 hashKey + h2Key = h2 hashKey + probe = newProbe h1Key (div8 (List.len metadata)) + + when findIndexHelper metadata dataIndices data h2Key key probe 0 is + Ok index -> + dataIndex = listGetUnsafe dataIndices index + (_, v) = listGetUnsafe data dataIndex + + Ok v + + Err NotFound -> + Err KeyNotFound + +## Check if the dictionary has a value for a specified key. +## ``` +## expect +## Dict.empty {} +## |> Dict.insert 1234 "5678" +## |> Dict.contains 1234 +## |> Bool.isEq Bool.true +## ``` +contains : Dict k v, k -> Bool where k implements Hash & Eq +contains = \@Dict { metadata, dataIndices, data }, key -> + hashKey = + createLowLevelHasher PseudoRandSeed + |> Hash.hash key + |> complete + h1Key = h1 hashKey + h2Key = h2 hashKey + probe = newProbe h1Key (div8 (List.len metadata)) + + when findIndexHelper metadata dataIndices data h2Key key probe 0 is + Ok _ -> + Bool.true + + Err NotFound -> + Bool.false + +## Insert a value into the dictionary at a specified key. +## ``` +## expect +## Dict.empty {} +## |> Dict.insert "Apples" 12 +## |> Dict.get "Apples" +## |> Bool.isEq (Ok 12) +## ``` +insert : Dict k v, k, v -> Dict k v where k implements Hash & Eq +insert = \@Dict { metadata, dataIndices, data, size }, key, value -> + hashKey = + createLowLevelHasher PseudoRandSeed + |> Hash.hash key + |> complete + h1Key = h1 hashKey + h2Key = h2 hashKey + probe = newProbe h1Key (div8 (List.len metadata)) + + when findIndexHelper metadata dataIndices data h2Key key probe 0 is + Ok index -> + dataIndex = listGetUnsafe dataIndices index + + @Dict { + metadata, + dataIndices, + data: List.set data dataIndex (key, value), + size, + } + + Err NotFound -> + # The dictionary has grown, it might need to rehash. + rehashedDict = + maybeRehash + ( + @Dict { + metadata, + dataIndices, + data, + size: Num.addWrap size 1, + } + ) + + # Need to rescan searching for the first empty or deleted cell. + insertNotFoundHelper rehashedDict key value h1Key h2Key + +## Remove a value from the dictionary for a specified key. +## ``` +## expect +## Dict.empty {} +## |> Dict.insert "Some" "Value" +## |> Dict.remove "Some" +## |> Dict.len +## |> Bool.isEq 0 +## ``` +remove : Dict k v, k -> Dict k v where k implements Hash & Eq +remove = \@Dict { metadata, dataIndices, data, size }, key -> + # TODO: change this from swap remove to tombstone and test is performance is still good. + hashKey = + createLowLevelHasher PseudoRandSeed + |> Hash.hash key + |> complete + h1Key = h1 hashKey + h2Key = h2 hashKey + probe = newProbe h1Key (div8 (List.len metadata)) + + when findIndexHelper metadata dataIndices data h2Key key probe 0 is + Ok index -> + last = Num.subWrap (List.len data) 1 + dataIndex = listGetUnsafe dataIndices index + + if dataIndex == last then + @Dict { + metadata: List.set metadata index deletedSlot, + dataIndices, + data: List.dropLast data 1, + size: Num.subWrap size 1, + } + else + swapAndUpdateDataIndex (@Dict { metadata, dataIndices, data, size }) index last + + Err NotFound -> + @Dict { metadata, dataIndices, data, size } + +## Insert or remove a value for a specified key. This function enables a +## performance optimization for the use case of providing a default when a value +## is missing. This is more efficient than doing both a `Dict.get` and then a +## `Dict.insert` call, and supports being piped. +## ``` +## alterValue : [Present Bool, Missing] -> [Present Bool, Missing] +## alterValue = \possibleValue -> +## when possibleValue is +## Missing -> Present Bool.false +## Present value -> if value then Missing else Present Bool.true +## +## expect Dict.update (Dict.empty {}) "a" alterValue == Dict.single "a" Bool.false +## expect Dict.update (Dict.single "a" Bool.false) "a" alterValue == Dict.single "a" Bool.true +## expect Dict.update (Dict.single "a" Bool.true) "a" alterValue == Dict.empty {} +## ``` +update : Dict k v, k, ([Present v, Missing] -> [Present v, Missing]) -> Dict k v where k implements Hash & Eq +update = \dict, key, alter -> + # TODO: look into optimizing by merging substeps and reducing lookups. + possibleValue = + get dict key + |> Result.map Present + |> Result.withDefault Missing + + when alter possibleValue is + Present value -> insert dict key value + Missing -> remove dict key + +## Returns the keys and values of a dictionary as a [List]. +## This requires allocating a temporary list, prefer using [Dict.toList] or [Dict.walk] instead. +## ``` +## expect +## Dict.single 1 "One" +## |> Dict.insert 2 "Two" +## |> Dict.insert 3 "Three" +## |> Dict.insert 4 "Four" +## |> Dict.toList +## |> Bool.isEq [(1, "One"), (2, "Two"), (3, "Three"), (4, "Four")] +## ``` +toList : Dict k v -> List (k, v) where k implements Hash & Eq +toList = \@Dict { data } -> + data + +## Returns the keys of a dictionary as a [List]. +## This requires allocating a temporary [List], prefer using [Dict.toList] or [Dict.walk] instead. +## ``` +## expect +## Dict.single 1 "One" +## |> Dict.insert 2 "Two" +## |> Dict.insert 3 "Three" +## |> Dict.insert 4 "Four" +## |> Dict.keys +## |> Bool.isEq [1,2,3,4] +## ``` +keys : Dict k v -> List k where k implements Hash & Eq +keys = \@Dict { data } -> + List.map data (\(k, _) -> k) + +## Returns the values of a dictionary as a [List]. +## This requires allocating a temporary [List], prefer using [Dict.toList] or [Dict.walk] instead. +## ``` +## expect +## Dict.single 1 "One" +## |> Dict.insert 2 "Two" +## |> Dict.insert 3 "Three" +## |> Dict.insert 4 "Four" +## |> Dict.values +## |> Bool.isEq ["One","Two","Three","Four"] +## ``` +values : Dict k v -> List v where k implements Hash & Eq +values = \@Dict { data } -> + List.map data (\(_, v) -> v) + +## Combine two dictionaries by keeping the [union](https://en.wikipedia.org/wiki/Union_(set_theory)) +## of all the key-value pairs. This means that all the key-value pairs in +## both dictionaries will be combined. Note that where there are pairs +## with the same key, the value contained in the second input will be +## retained, and the value in the first input will be removed. +## ``` +## first = +## Dict.single 1 "Not Me" +## |> Dict.insert 2 "And Me" +## +## second = +## Dict.single 1 "Keep Me" +## |> Dict.insert 3 "Me Too" +## |> Dict.insert 4 "And Also Me" +## +## expected = +## Dict.single 1 "Keep Me" +## |> Dict.insert 2 "And Me" +## |> Dict.insert 3 "Me Too" +## |> Dict.insert 4 "And Also Me" +## +## expect +## Dict.insertAll first second == expected +## ``` +insertAll : Dict k v, Dict k v -> Dict k v where k implements Hash & Eq +insertAll = \xs, ys -> + walk ys xs insert + +## Combine two dictionaries by keeping the [intersection](https://en.wikipedia.org/wiki/Intersection_(set_theory)) +## of all the key-value pairs. This means that we keep only those pairs +## that are in both dictionaries. Note that where there are pairs with +## the same key, the value contained in the first input will be retained, +## and the value in the second input will be removed. +## ``` +## first = +## Dict.single 1 "Keep Me" +## |> Dict.insert 2 "And Me" +## +## second = +## Dict.single 1 "Keep Me" +## |> Dict.insert 2 "And Me" +## |> Dict.insert 3 "But Not Me" +## |> Dict.insert 4 "Or Me" +## +## expect Dict.keepShared first second == first +## ``` +keepShared : Dict k v, Dict k v -> Dict k v where k implements Hash & Eq +keepShared = \xs, ys -> + walk + xs + (empty {}) + (\state, k, v -> + if contains ys k then + insert state k v + else + state + ) + +## Remove the key-value pairs in the first input that are also in the second +## using the [set difference](https://en.wikipedia.org/wiki/Complement_(set_theory)#Relative_complement) +## of the values. This means that we will be left with only those pairs that +## are in the first dictionary and whose keys are not in the second. +## ``` +## first = +## Dict.single 1 "Keep Me" +## |> Dict.insert 2 "And Me" +## |> Dict.insert 3 "Remove Me" +## +## second = +## Dict.single 3 "Remove Me" +## |> Dict.insert 4 "I do nothing..." +## +## expected = +## Dict.single 1 "Keep Me" +## |> Dict.insert 2 "And Me" +## +## expect Dict.removeAll first second == expected +## ``` +removeAll : Dict k v, Dict k v -> Dict k v where k implements Hash & Eq +removeAll = \xs, ys -> + walk ys xs (\state, k, _ -> remove state k) + +swapAndUpdateDataIndex : Dict k v, Nat, Nat -> Dict k v where k implements Hash & Eq +swapAndUpdateDataIndex = \@Dict { metadata, dataIndices, data, size }, removedIndex, lastIndex -> + (key, _) = listGetUnsafe data lastIndex + hashKey = + createLowLevelHasher PseudoRandSeed + |> Hash.hash key + |> complete + h1Key = h1 hashKey + h2Key = h2 hashKey + probe = newProbe h1Key (div8 (List.len metadata)) + + when findIndexHelper metadata dataIndices data h2Key key probe 0 is + Ok index -> + dataIndex = listGetUnsafe dataIndices removedIndex + # Swap and remove data. + nextData = + data + |> List.swap dataIndex lastIndex + |> List.dropLast 1 + + @Dict { + # Set old metadata as deleted. + metadata: List.set metadata removedIndex deletedSlot, + # Update index of swaped element. + dataIndices: List.set dataIndices index dataIndex, + data: nextData, + size: Num.subWrap size 1, + } + + Err NotFound -> + # This should be impossible. + crash "unreachable state in dict swapAndUpdateDataIndex hit. Definitely a standard library bug." + +insertNotFoundHelper : Dict k v, k, v, U64, I8 -> Dict k v +insertNotFoundHelper = \@Dict { metadata, dataIndices, data, size }, key, value, h1Key, h2Key -> + probe = newProbe h1Key (div8 (List.len metadata)) + index = nextEmptyOrDeletedHelper metadata probe 0 + dataIndex = List.len data + nextData = List.append data (key, value) + + @Dict { + metadata: List.set metadata index h2Key, + dataIndices: List.set dataIndices index dataIndex, + data: nextData, + size, + } + +nextEmptyOrDeletedHelper : List I8, Probe, Nat -> Nat +nextEmptyOrDeletedHelper = \metadata, probe, offset -> + # For inserting, we can use deleted indices. + index = Num.addWrap (mul8 probe.slotIndex) offset + + md = listGetUnsafe metadata index + + if md < 0 then + # Empty or deleted slot, no possibility of the element. + index + else if offset == 7 then + nextEmptyOrDeletedHelper metadata (nextProbe probe) 0 + else + nextEmptyOrDeletedHelper metadata probe (Num.addWrap offset 1) + +# TODO: investigate if this needs to be split into more specific helper functions. +# There is a chance that returning specific sub-info like the value would be faster. +findIndexHelper : List I8, List Nat, List (k, v), I8, k, Probe, Nat -> Result Nat [NotFound] where k implements Hash & Eq +findIndexHelper = \metadata, dataIndices, data, h2Key, key, probe, offset -> + # For finding a value, we must search past all deleted element tombstones. + index = Num.addWrap (mul8 probe.slotIndex) offset + + md = listGetUnsafe metadata index + + if md == emptySlot then + # Empty slot, no possibility of the element. + Err NotFound + else if md == h2Key then + # Potentially matching slot, check if the key is a match. + dataIndex = listGetUnsafe dataIndices index + (k, _) = listGetUnsafe data dataIndex + + if k == key then + # We have a match, return its index. + Ok index + else if offset == 7 then + # No match, keep checking. + findIndexHelper metadata dataIndices data h2Key key (nextProbe probe) 0 + else + findIndexHelper metadata dataIndices data h2Key key probe (Num.addWrap offset 1) + else if offset == 7 then + # Used slot, check next slot. + findIndexHelper metadata dataIndices data h2Key key (nextProbe probe) 0 + else + findIndexHelper metadata dataIndices data h2Key key probe (Num.addWrap offset 1) + +# This is how we grow the container. +# If we aren't to the load factor yet, just ignore this. +# The container must have an updated size including any elements about to be inserted. +maybeRehash : Dict k v -> Dict k v where k implements Hash & Eq +maybeRehash = \@Dict { metadata, dataIndices, data, size } -> + cap = List.len dataIndices + maxLoadCap = + # This is 7/8 * capacity, which is the max load factor. + Num.subWrap cap (Num.shiftRightZfBy cap 3) + + if size > maxLoadCap then + rehash (@Dict { metadata, dataIndices, data, size }) + else + @Dict { metadata, dataIndices, data, size } + +# TODO: switch rehash to iterate data and eventually clear out tombstones as well. +rehash : Dict k v -> Dict k v where k implements Hash & Eq +rehash = \@Dict { metadata, dataIndices, data, size } -> + newLen = 2 * List.len dataIndices + newDict = + @Dict { + metadata: List.repeat emptySlot newLen, + dataIndices: List.repeat 0 newLen, + data, + size, + } + + rehashHelper newDict metadata dataIndices data 0 + +rehashHelper : Dict k v, List I8, List Nat, List (k, v), Nat -> Dict k v where k implements Hash & Eq +rehashHelper = \dict, oldMetadata, oldDataIndices, oldData, index -> + when List.get oldMetadata index is + Ok md -> + nextDict = + if md >= 0 then + # We have an actual element here + dataIndex = listGetUnsafe oldDataIndices index + (k, _) = listGetUnsafe oldData dataIndex + + insertForRehash dict k dataIndex + else + # Empty or deleted data + dict + + rehashHelper nextDict oldMetadata oldDataIndices oldData (Num.addWrap index 1) + + Err OutOfBounds -> + # Walked entire list, complete now. + dict + +insertForRehash : Dict k v, k, Nat -> Dict k v where k implements Hash & Eq +insertForRehash = \@Dict { metadata, dataIndices, data, size }, key, dataIndex -> + hashKey = + createLowLevelHasher PseudoRandSeed + |> Hash.hash key + |> complete + h1Key = h1 hashKey + h2Key = h2 hashKey + probe = newProbe h1Key (div8 (List.len metadata)) + index = nextEmptyOrDeletedHelper metadata probe 0 + + @Dict { + metadata: List.set metadata index h2Key, + dataIndices: List.set dataIndices index dataIndex, + data, + size, + } + +emptySlot : I8 +emptySlot = -128 +deletedSlot : I8 +deletedSlot = -2 + +# Capacity must be a power of 2. +# We still will use slots of 8 even though this version has no true slots. +# We just move an element at a time. +# Thus, the true index is slotIndex * 8 + offset. +Probe : { slotIndex : Nat, probeI : Nat, mask : Nat } + +newProbe : U64, Nat -> Probe +newProbe = \h1Key, slots -> + mask = Num.subSaturated slots 1 + slotIndex = Num.bitwiseAnd (Num.toNat h1Key) mask + + { slotIndex, probeI: 1, mask } + +nextProbe : Probe -> Probe +nextProbe = \{ slotIndex, probeI, mask } -> + nextSlotIndex = Num.bitwiseAnd (Num.addWrap slotIndex probeI) mask + + { slotIndex: nextSlotIndex, probeI: Num.addWrap probeI 1, mask } + +mul8 = \val -> Num.shiftLeftBy val 3 +div8 = \val -> Num.shiftRightZfBy val 3 + +h1 : U64 -> U64 +h1 = \hashKey -> + Num.shiftRightZfBy hashKey 7 + +h2 : U64 -> I8 +h2 = \hashKey -> + Num.toI8 (Num.bitwiseAnd hashKey 0b0111_1111) + +expect + val = + empty {} + |> insert "foo" "bar" + |> get "foo" + + val == Ok "bar" + +expect + dict1 = + empty {} + |> insert 1 "bar" + |> insert 2 "baz" + + dict2 = + empty {} + |> insert 2 "baz" + |> insert 1 "bar" + + dict1 == dict2 + +expect + dict1 = + empty {} + |> insert 1 "bar" + |> insert 2 "baz" + + dict2 = + empty {} + |> insert 1 "bar" + |> insert 2 "baz!" + + dict1 != dict2 + +expect + inner1 = + empty {} + |> insert 1 "bar" + |> insert 2 "baz" + + inner2 = + empty {} + |> insert 2 "baz" + |> insert 1 "bar" + + outer = + empty {} + |> insert inner1 "wrong" + |> insert inner2 "right" + + get outer inner1 == Ok "right" + +expect + inner1 = + empty {} + |> insert 1 "bar" + |> insert 2 "baz" + + inner2 = + empty {} + |> insert 2 "baz" + |> insert 1 "bar" + + outer1 = + empty {} + |> insert inner1 "val" + + outer2 = + empty {} + |> insert inner2 "val" + + outer1 == outer2 + +expect + val = + empty {} + |> insert "foo" "bar" + |> insert "foo" "baz" + |> get "foo" + + val == Ok "baz" + +expect + val = + empty {} + |> insert "foo" "bar" + |> get "bar" + + val == Err KeyNotFound + +expect + empty {} + |> insert "foo" {} + |> contains "foo" + +expect + dict = + empty {} + |> insert "foo" {} + |> insert "bar" {} + |> insert "baz" {} + + contains dict "baz" && Bool.not (contains dict "other") + +expect + dict = + fromList [(1u8, 1u8), (2, 2), (3, 3)] + |> remove 1 + |> remove 3 + + keys dict == [2] + +expect + list = + fromList [(1u8, 1u8), (2u8, 2u8), (3, 3)] + |> remove 1 + |> insert 0 0 + |> remove 3 + |> keys + + list == [0, 2] + +# Reach capacity, no rehash. +expect + val = + empty {} + |> insert "a" 0 + |> insert "b" 1 + |> insert "c" 2 + |> insert "d" 3 + |> insert "e" 4 + |> insert "f" 5 + |> insert "g" 6 + |> capacity + + val == 7 + +expect + dict = + empty {} + |> insert "a" 0 + |> insert "b" 1 + |> insert "c" 2 + |> insert "d" 3 + |> insert "e" 4 + |> insert "f" 5 + |> insert "g" 6 + + (get dict "a" == Ok 0) + && (get dict "b" == Ok 1) + && (get dict "c" == Ok 2) + && (get dict "d" == Ok 3) + && (get dict "e" == Ok 4) + && (get dict "f" == Ok 5) + && (get dict "g" == Ok 6) + +# Force rehash. +expect + val = + empty {} + |> insert "a" 0 + |> insert "b" 1 + |> insert "c" 2 + |> insert "d" 3 + |> insert "e" 4 + |> insert "f" 5 + |> insert "g" 6 + |> insert "h" 7 + |> capacity + + val == 14 + +expect + dict = + empty {} + |> insert "a" 0 + |> insert "b" 1 + |> insert "c" 2 + |> insert "d" 3 + |> insert "e" 4 + |> insert "f" 5 + |> insert "g" 6 + |> insert "h" 7 + + (get dict "a" == Ok 0) + && (get dict "b" == Ok 1) + && (get dict "c" == Ok 2) + && (get dict "d" == Ok 3) + && (get dict "e" == Ok 4) + && (get dict "f" == Ok 5) + && (get dict "g" == Ok 6) + && (get dict "h" == Ok 7) + +expect + empty {} + |> insert "Some" "Value" + |> remove "Some" + |> len + |> Bool.isEq 0 + +# Makes sure a Dict with Nat keys works +expect + empty {} + |> insert 7nat "Testing" + |> get 7 + |> Bool.isEq (Ok "Testing") + +# We have decided not to expose the standard roc hashing algorithm. +# This is to avoid external dependence and the need for versioning. +# The current implementation is a form of [Wyhash final4](https://github.com/wangyi-fudan/wyhash/blob/77e50f267fbc7b8e2d09f2d455219adb70ad4749/wyhash.h). +# It is 64bit and little endian specific currently. +# TODO: wyhash is slow for large keys, use something like cityhash if the keys are too long. +# TODO: Add a builtin to distinguish big endian systems and change loading orders. +# TODO: Switch out Wymum on systems with slow 128bit multiplication. +LowLevelHasher := { initializedSeed : U64, state : U64 } implements [ + Hasher { + addBytes, + addU8, + addU16, + addU32, + addU64, + addU128, + complete, + }, + ] + +# unsafe primitive that does not perform a bounds check +# TODO hide behind an InternalList.roc module +listGetUnsafe : List a, Nat -> a + +# Returns a application specific pseudo random seed for Dict. +# This avoids trivial DOS attacks. +pseudoSeed : {} -> U64 + +createLowLevelHasher : [PseudoRandSeed, WithSeed U64] -> LowLevelHasher +createLowLevelHasher = \seedOpt -> + seed = + when seedOpt is + PseudoRandSeed -> pseudoSeed {} + WithSeed s -> s + @LowLevelHasher { initializedSeed: initSeed seed, state: seed } + +combineState : LowLevelHasher, { a : U64, b : U64, seed : U64, length : U64 } -> LowLevelHasher +combineState = \@LowLevelHasher { initializedSeed, state }, { a, b, seed, length } -> + mum = + a + |> Num.bitwiseXor wyp1 + |> wymum (Num.bitwiseXor b seed) + nexta = + mum.lower + |> Num.bitwiseXor wyp0 + |> Num.bitwiseXor length + nextb = + mum.upper + |> Num.bitwiseXor wyp1 + hash = wymix nexta nextb + + @LowLevelHasher { initializedSeed, state: wymix state hash } + +initSeed = \seed -> + seed + |> Num.bitwiseXor wyp0 + |> wymix wyp1 + |> Num.bitwiseXor seed + +complete = \@LowLevelHasher { state } -> state + +# These implementations hash each value individually with the seed and then mix +# the resulting hash with the state. There are other options that may be faster +# like using the output of the last hash as the seed to the current hash. +# I am simply not sure the tradeoffs here. Theoretically this method is more sound. +# Either way, the performance will be similar and we can change this later. +addU8 = \@LowLevelHasher { initializedSeed, state }, u8 -> + p0 = Num.toU64 u8 + a = + Num.shiftLeftBy p0 16 + |> Num.bitwiseOr (Num.shiftLeftBy p0 8) + |> Num.bitwiseOr p0 + b = 0 + + combineState (@LowLevelHasher { initializedSeed, state }) { a, b, seed: initializedSeed, length: 1 } + +addU16 = \@LowLevelHasher { initializedSeed, state }, u16 -> + p0 = Num.bitwiseAnd u16 0xFF |> Num.toU64 + p1 = Num.shiftRightZfBy u16 8 |> Num.toU64 + a = + Num.shiftLeftBy p0 16 + |> Num.bitwiseOr (Num.shiftLeftBy p1 8) + |> Num.bitwiseOr p1 + b = 0 + + combineState (@LowLevelHasher { initializedSeed, state }) { a, b, seed: initializedSeed, length: 2 } + +addU32 = \@LowLevelHasher { initializedSeed, state }, u32 -> + p0 = Num.toU64 u32 + a = Num.shiftLeftBy p0 32 |> Num.bitwiseOr p0 + + combineState (@LowLevelHasher { initializedSeed, state }) { a, b: a, seed: initializedSeed, length: 4 } + +addU64 = \@LowLevelHasher { initializedSeed, state }, u64 -> + p0 = Num.bitwiseAnd 0xFFFF_FFFF u64 + p1 = Num.shiftRightZfBy u64 32 + a = Num.shiftLeftBy p0 32 |> Num.bitwiseOr p1 + b = Num.shiftLeftBy p1 32 |> Num.bitwiseOr p0 + + combineState (@LowLevelHasher { initializedSeed, state }) { a, b, seed: initializedSeed, length: 8 } + +addU128 = \@LowLevelHasher { initializedSeed, state }, u128 -> + lower = u128 |> Num.toU64 + upper = Num.shiftRightZfBy u128 64 |> Num.toU64 + p0 = Num.bitwiseAnd 0xFFFF_FFFF lower + p1 = Num.shiftRightZfBy lower 32 |> Num.bitwiseAnd 0xFFFF_FFFF + p2 = Num.bitwiseAnd 0xFFFF_FFFF upper + p3 = Num.shiftRightZfBy upper 32 |> Num.bitwiseAnd 0xFFFF_FFFF + a = Num.shiftLeftBy p0 32 |> Num.bitwiseOr p2 + b = Num.shiftLeftBy p3 32 |> Num.bitwiseOr p1 + + combineState (@LowLevelHasher { initializedSeed, state }) { a, b, seed: initializedSeed, length: 16 } + +addBytes : LowLevelHasher, List U8 -> LowLevelHasher +addBytes = \@LowLevelHasher { initializedSeed, state }, list -> + length = List.len list + abs = + if length <= 16 then + if length >= 4 then + x = Num.shiftRightZfBy length 3 |> Num.shiftLeftBy 2 + a = Num.bitwiseOr (wyr4 list 0 |> Num.shiftLeftBy 32) (wyr4 list x) + b = + (wyr4 list (Num.subWrap length 4) |> Num.shiftLeftBy 32) + |> Num.bitwiseOr (wyr4 list (Num.subWrap length 4 |> Num.subWrap x)) + + { a, b, seed: initializedSeed } + else if length > 0 then + { a: wyr3 list 0 length, b: 0, seed: initializedSeed } + else + { a: 0, b: 0, seed: initializedSeed } + else if length <= 48 then + hashBytesHelper16 initializedSeed list 0 length + else + hashBytesHelper48 initializedSeed initializedSeed initializedSeed list 0 length + + combineState (@LowLevelHasher { initializedSeed, state }) { a: abs.a, b: abs.b, seed: abs.seed, length: Num.toU64 length } + +hashBytesHelper48 : U64, U64, U64, List U8, Nat, Nat -> { a : U64, b : U64, seed : U64 } +hashBytesHelper48 = \seed, see1, see2, list, index, remaining -> + newSeed = wymix (Num.bitwiseXor (wyr8 list index) wyp1) (Num.bitwiseXor (wyr8 list (Num.addWrap index 8)) seed) + newSee1 = wymix (Num.bitwiseXor (wyr8 list (Num.addWrap index 16)) wyp2) (Num.bitwiseXor (wyr8 list (Num.addWrap index 24)) see1) + newSee2 = wymix (Num.bitwiseXor (wyr8 list (Num.addWrap index 32)) wyp3) (Num.bitwiseXor (wyr8 list (Num.addWrap index 40)) see2) + newRemaining = Num.subWrap remaining 48 + newIndex = Num.addWrap index 48 + + if newRemaining > 48 then + hashBytesHelper48 newSeed newSee1 newSee2 list newIndex newRemaining + else if newRemaining > 16 then + finalSeed = Num.bitwiseXor newSee2 (Num.bitwiseXor newSee1 newSeed) + + hashBytesHelper16 finalSeed list newIndex newRemaining + else + finalSeed = Num.bitwiseXor newSee2 (Num.bitwiseXor newSee1 newSeed) + + { a: wyr8 list (Num.subWrap newRemaining 16 |> Num.addWrap newIndex), b: wyr8 list (Num.subWrap newRemaining 8 |> Num.addWrap newIndex), seed: finalSeed } + +hashBytesHelper16 : U64, List U8, Nat, Nat -> { a : U64, b : U64, seed : U64 } +hashBytesHelper16 = \seed, list, index, remaining -> + newSeed = wymix (Num.bitwiseXor (wyr8 list index) wyp1) (Num.bitwiseXor (wyr8 list (Num.addWrap index 8)) seed) + newRemaining = Num.subWrap remaining 16 + newIndex = Num.addWrap index 16 + + if newRemaining <= 16 then + { a: wyr8 list (Num.subWrap newRemaining 16 |> Num.addWrap newIndex), b: wyr8 list (Num.subWrap newRemaining 8 |> Num.addWrap newIndex), seed: newSeed } + else + hashBytesHelper16 newSeed list newIndex newRemaining + +wyp0 : U64 +wyp0 = 0xa0761d6478bd642f +wyp1 : U64 +wyp1 = 0xe7037ed1a0b428db +wyp2 : U64 +wyp2 = 0x8ebc6af09c88c6e3 +wyp3 : U64 +wyp3 = 0x589965cc75374cc3 + +wymix : U64, U64 -> U64 +wymix = \a, b -> + { lower, upper } = wymum a b + + Num.bitwiseXor lower upper + +wymum : U64, U64 -> { lower : U64, upper : U64 } +wymum = \a, b -> + r = Num.toU128 a * Num.toU128 b + lower = Num.toU64 r + upper = Num.shiftRightZfBy r 64 |> Num.toU64 + + # This is the more robust form, which we may look into later + # { lower: Num.bitwiseXor a lower, upper: Num.bitwiseXor b upper } + { lower, upper } + +# Get the next 8 bytes as a U64 +wyr8 : List U8, Nat -> U64 +wyr8 = \list, index -> + # With seamless slices and Num.fromBytes, this should be possible to make faster and nicer. + # It would also deal with the fact that on big endian systems we want to invert the order here. + # Without seamless slices, we would need fromBytes to take an index. + p1 = listGetUnsafe list index |> Num.toU64 + p2 = listGetUnsafe list (Num.addWrap index 1) |> Num.toU64 + p3 = listGetUnsafe list (Num.addWrap index 2) |> Num.toU64 + p4 = listGetUnsafe list (Num.addWrap index 3) |> Num.toU64 + p5 = listGetUnsafe list (Num.addWrap index 4) |> Num.toU64 + p6 = listGetUnsafe list (Num.addWrap index 5) |> Num.toU64 + p7 = listGetUnsafe list (Num.addWrap index 6) |> Num.toU64 + p8 = listGetUnsafe list (Num.addWrap index 7) |> Num.toU64 + a = Num.bitwiseOr p1 (Num.shiftLeftBy p2 8) + b = Num.bitwiseOr (Num.shiftLeftBy p3 16) (Num.shiftLeftBy p4 24) + c = Num.bitwiseOr (Num.shiftLeftBy p5 32) (Num.shiftLeftBy p6 40) + d = Num.bitwiseOr (Num.shiftLeftBy p7 48) (Num.shiftLeftBy p8 56) + + Num.bitwiseOr (Num.bitwiseOr a b) (Num.bitwiseOr c d) + +# Get the next 4 bytes as a U64 with some shifting. +wyr4 : List U8, Nat -> U64 +wyr4 = \list, index -> + p1 = listGetUnsafe list index |> Num.toU64 + p2 = listGetUnsafe list (Num.addWrap index 1) |> Num.toU64 + p3 = listGetUnsafe list (Num.addWrap index 2) |> Num.toU64 + p4 = listGetUnsafe list (Num.addWrap index 3) |> Num.toU64 + a = Num.bitwiseOr p1 (Num.shiftLeftBy p2 8) + b = Num.bitwiseOr (Num.shiftLeftBy p3 16) (Num.shiftLeftBy p4 24) + + Num.bitwiseOr a b + +# Get the next K bytes with some shifting. +# K must be 3 or less. +wyr3 : List U8, Nat, Nat -> U64 +wyr3 = \list, index, k -> + # ((uint64_t)p[0])<<16)|(((uint64_t)p[k>>1])<<8)|p[k-1] + p1 = listGetUnsafe list index |> Num.toU64 + p2 = listGetUnsafe list (Num.shiftRightZfBy k 1 |> Num.addWrap index) |> Num.toU64 + p3 = listGetUnsafe list (Num.subWrap k 1 |> Num.addWrap index) |> Num.toU64 + a = Num.bitwiseOr (Num.shiftLeftBy p1 16) (Num.shiftLeftBy p2 8) + + Num.bitwiseOr a p3 + +testSeed = WithSeed 0x526F_6352_616E_643F + +# TODO: would be great to have table driven expects for this. +# Would also be great to have some sort of property based hasher +# where we can compare `addU*` functions to the `addBytes` function. +expect + hash = + createLowLevelHasher testSeed + |> addBytes [] + |> complete + + hash == 0xD59C59757DBBE6B3 + +expect + hash = + createLowLevelHasher testSeed + |> addBytes [0x42] + |> complete + + hash == 0x38CE03D0E61AF963 + +expect + hash = + createLowLevelHasher testSeed + |> addU8 0x42 + |> complete + + hash == 0x38CE03D0E61AF963 + +expect + hash = + createLowLevelHasher testSeed + |> addBytes [0xFF, 0xFF] + |> complete + + hash == 0xE1CB2FA0D6A64113 + +expect + hash = + createLowLevelHasher testSeed + |> addU16 0xFFFF + |> complete + + hash == 0xE1CB2FA0D6A64113 + +expect + hash = + createLowLevelHasher testSeed + |> addBytes [0x36, 0xA7] + |> complete + + hash == 0x26B8319EDAF81B15 + +expect + hash = + createLowLevelHasher testSeed + |> addU16 0xA736 + |> complete + + hash == 0x26B8319EDAF81B15 + +expect + hash = + createLowLevelHasher testSeed + |> addBytes [0x00, 0x00, 0x00, 0x00] + |> complete + + hash == 0xA187D7CA074F9EE7 + +expect + hash = + createLowLevelHasher testSeed + |> addU32 0x0000_0000 + |> complete + + hash == 0xA187D7CA074F9EE7 + +expect + hash = + createLowLevelHasher testSeed + |> addBytes [0xA9, 0x2F, 0xEE, 0x21] + |> complete + + hash == 0xA499EFE4C1454D09 + +expect + hash = + createLowLevelHasher testSeed + |> addU32 0x21EE_2FA9 + |> complete + + hash == 0xA499EFE4C1454D09 + +expect + hash = + createLowLevelHasher testSeed + |> addBytes [0x5D, 0x66, 0xB1, 0x8F, 0x68, 0x44, 0xC7, 0x03, 0xE1, 0xDD, 0x23, 0x34, 0xBB, 0x9A, 0x42, 0xA7] + |> complete + + hash == 0xDD39A206AED64C73 + +expect + hash = + createLowLevelHasher testSeed + |> addU128 0xA742_9ABB_3423_DDE1_03C7_4468_8FB1_665D + |> complete + + hash == 0xDD39A206AED64C73 + +expect + hash = + createLowLevelHasher testSeed + |> Hash.hashStrBytes "abcdefghijklmnopqrstuvwxyz" + |> complete + + hash == 0x51C59DF5B1D15F40 + +expect + hash = + createLowLevelHasher testSeed + |> Hash.hashStrBytes "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" + |> complete + + hash == 0xD8D0A129D97A4E95 + +expect + hash = + createLowLevelHasher testSeed + |> Hash.hashStrBytes "1234567890123456789012345678901234567890123456789012345678901234567890" + |> complete + + hash == 0x8188065B44FB4AAA + +expect + hash = + createLowLevelHasher testSeed + |> addBytes (List.repeat 0x77 100) + |> complete + + hash == 0x47A2A606EADF3378 + +# Note, had to specify u8 in the lists below to avoid ability type resolution error. +# Apparently it won't pick the default integer. +expect + hash = + createLowLevelHasher testSeed + |> Hash.hashUnordered [8u8, 82u8, 3u8, 8u8, 24u8] List.walk + |> complete + + hash == 0xB2E8254C08F16B20 + +expect + hash1 = + createLowLevelHasher testSeed + |> Hash.hashUnordered ([0u8, 1u8, 2u8, 3u8, 4u8]) List.walk + |> complete + + hash2 = + createLowLevelHasher testSeed + |> Hash.hashUnordered [4u8, 3u8, 2u8, 1u8, 0u8] List.walk + |> complete + + hash1 == hash2 + +expect + hash1 = + createLowLevelHasher testSeed + |> Hash.hashUnordered [0u8, 1u8, 2u8, 3u8, 4u8] List.walk + |> complete + + hash2 = + createLowLevelHasher testSeed + |> Hash.hashUnordered [4u8, 3u8, 2u8, 1u8, 0u8, 0u8] List.walk + |> complete + + hash1 != hash2 + +expect + empty {} + |> len + |> Bool.isEq 0 + +expect + empty {} + |> insert "One" "A Song" + |> insert "Two" "Candy Canes" + |> insert "Three" "Boughs of Holly" + |> clear + |> len + |> Bool.isEq 0 + +expect + Dict.empty {} + |> Dict.insert "Alice" 17 + |> Dict.insert "Bob" 18 + |> Dict.insert "Charlie" 19 + |> Dict.walkUntil Bool.false (\_, _, age -> if age >= 18 then Break Bool.true else Continue Bool.false) + |> Bool.isEq Bool.true diff --git a/crates/compiler/builtins/roc/Encode.roc b/crates/compiler/builtins/roc/Encode.roc new file mode 100644 index 0000000000..af8b0307c1 --- /dev/null +++ b/crates/compiler/builtins/roc/Encode.roc @@ -0,0 +1,86 @@ +interface Encode + exposes [ + Encoder, + Encoding, + toEncoder, + EncoderFormatting, + u8, + u16, + u32, + u64, + u128, + i8, + i16, + i32, + i64, + i128, + f32, + f64, + dec, + bool, + string, + list, + record, + tag, + tuple, + custom, + appendWith, + append, + toBytes, + ] + imports [ + Num.{ + U8, + U16, + U32, + U64, + U128, + I8, + I16, + I32, + I64, + I128, + F32, + F64, + Dec, + }, + Bool.{ Bool }, + ] + +Encoder fmt := List U8, fmt -> List U8 where fmt implements EncoderFormatting + +Encoding implements + toEncoder : val -> Encoder fmt where val implements Encoding, fmt implements EncoderFormatting + +EncoderFormatting implements + u8 : U8 -> Encoder fmt where fmt implements EncoderFormatting + u16 : U16 -> Encoder fmt where fmt implements EncoderFormatting + u32 : U32 -> Encoder fmt where fmt implements EncoderFormatting + u64 : U64 -> Encoder fmt where fmt implements EncoderFormatting + u128 : U128 -> Encoder fmt where fmt implements EncoderFormatting + i8 : I8 -> Encoder fmt where fmt implements EncoderFormatting + i16 : I16 -> Encoder fmt where fmt implements EncoderFormatting + i32 : I32 -> Encoder fmt where fmt implements EncoderFormatting + i64 : I64 -> Encoder fmt where fmt implements EncoderFormatting + i128 : I128 -> Encoder fmt where fmt implements EncoderFormatting + f32 : F32 -> Encoder fmt where fmt implements EncoderFormatting + f64 : F64 -> Encoder fmt where fmt implements EncoderFormatting + dec : Dec -> Encoder fmt where fmt implements EncoderFormatting + bool : Bool -> Encoder fmt where fmt implements EncoderFormatting + string : Str -> Encoder fmt where fmt implements EncoderFormatting + list : List elem, (elem -> Encoder fmt) -> Encoder fmt where fmt implements EncoderFormatting + record : List { key : Str, value : Encoder fmt } -> Encoder fmt where fmt implements EncoderFormatting + tuple : List (Encoder fmt) -> Encoder fmt where fmt implements EncoderFormatting + tag : Str, List (Encoder fmt) -> Encoder fmt where fmt implements EncoderFormatting + +custom : (List U8, fmt -> List U8) -> Encoder fmt where fmt implements EncoderFormatting +custom = \encoder -> @Encoder encoder + +appendWith : List U8, Encoder fmt, fmt -> List U8 where fmt implements EncoderFormatting +appendWith = \lst, @Encoder doEncoding, fmt -> doEncoding lst fmt + +append : List U8, val, fmt -> List U8 where val implements Encoding, fmt implements EncoderFormatting +append = \lst, val, fmt -> appendWith lst (toEncoder val) fmt + +toBytes : val, fmt -> List U8 where val implements Encoding, fmt implements EncoderFormatting +toBytes = \val, fmt -> appendWith [] (toEncoder val) fmt diff --git a/crates/compiler/builtins/roc/Hash.roc b/crates/compiler/builtins/roc/Hash.roc new file mode 100644 index 0000000000..6a52054091 --- /dev/null +++ b/crates/compiler/builtins/roc/Hash.roc @@ -0,0 +1,145 @@ +interface Hash + exposes [ + Hash, + Hasher, + hash, + addBytes, + addU8, + addU16, + addU32, + addU64, + addU128, + hashBool, + hashI8, + hashI16, + hashI32, + hashI64, + hashI128, + hashNat, + hashDec, + complete, + hashStrBytes, + hashList, + hashUnordered, + ] imports [ + Bool.{ Bool, isEq }, + List, + Str, + Num.{ U8, U16, U32, U64, U128, I8, I16, I32, I64, I128, Nat, Dec }, + ] + +## A value that can be hashed. +Hash implements + ## Hashes a value into a [Hasher]. + ## Note that [hash] does not produce a hash value itself; the hasher must be + ## [complete]d in order to extract the hash value. + hash : hasher, a -> hasher where a implements Hash, hasher implements Hasher + +## Describes a hashing algorithm that is fed bytes and produces an integer hash. +## +## The [Hasher] ability describes general-purpose hashers. It only allows +## emission of 64-bit unsigned integer hashes. It is not suitable for +## cryptographically-secure hashing. +Hasher implements + ## Adds a list of bytes to the hasher. + addBytes : a, List U8 -> a where a implements Hasher + + ## Adds a single U8 to the hasher. + addU8 : a, U8 -> a where a implements Hasher + + ## Adds a single U16 to the hasher. + addU16 : a, U16 -> a where a implements Hasher + + ## Adds a single U32 to the hasher. + addU32 : a, U32 -> a where a implements Hasher + + ## Adds a single U64 to the hasher. + addU64 : a, U64 -> a where a implements Hasher + + ## Adds a single U128 to the hasher. + addU128 : a, U128 -> a where a implements Hasher + + ## Completes the hasher, extracting a hash value from its + ## accumulated hash state. + complete : a -> U64 where a implements Hasher + +## Adds a string into a [Hasher] by hashing its UTF-8 bytes. +hashStrBytes = \hasher, s -> + addBytes hasher (Str.toUtf8 s) + +## Adds a list of [Hash]able elements to a [Hasher] by hashing each element. +hashList = \hasher, lst -> + List.walk lst hasher \accumHasher, elem -> + hash accumHasher elem + +## Adds a single [Bool] to a hasher. +hashBool : a, Bool -> a where a implements Hasher +hashBool = \hasher, b -> + asU8 = if b then 1 else 0 + addU8 hasher asU8 + +## Adds a single I8 to a hasher. +hashI8 : a, I8 -> a where a implements Hasher +hashI8 = \hasher, n -> addU8 hasher (Num.toU8 n) + +## Adds a single I16 to a hasher. +hashI16 : a, I16 -> a where a implements Hasher +hashI16 = \hasher, n -> addU16 hasher (Num.toU16 n) + +## Adds a single I32 to a hasher. +hashI32 : a, I32 -> a where a implements Hasher +hashI32 = \hasher, n -> addU32 hasher (Num.toU32 n) + +## Adds a single I64 to a hasher. +hashI64 : a, I64 -> a where a implements Hasher +hashI64 = \hasher, n -> addU64 hasher (Num.toU64 n) + +## Adds a single I128 to a hasher. +hashI128 : a, I128 -> a where a implements Hasher +hashI128 = \hasher, n -> addU128 hasher (Num.toU128 n) + +## Adds a single Nat to a hasher. +hashNat : a, Nat -> a where a implements Hasher +hashNat = \hasher, n -> + isPlatform32bit = + x : Nat + x = 0xffff_ffff + y = Num.addWrap x 1 + + y == 0 + + if isPlatform32bit then + addU32 hasher (Num.toU32 n) + else + addU64 hasher (Num.toU64 n) + +## LOWLEVEL get the i128 representation of a Dec. +i128OfDec : Dec -> I128 + +## Adds a single [Dec] to a hasher. +hashDec : a, Dec -> a where a implements Hasher +hashDec = \hasher, n -> hashI128 hasher (i128OfDec n) + +## Adds a container of [Hash]able elements to a [Hasher] by hashing each element. +## The container is iterated using the walk method passed in. +## The order of the elements does not affect the final hash. +hashUnordered = \hasher, container, walk -> + walk + container + 0 + (\accum, elem -> + x = + # Note, we intentionally copy the hasher in every iteration. + # Having the same base state is required for unordered hashing. + hasher + |> hash elem + |> complete + nextAccum = Num.addWrap accum x + + if nextAccum < accum then + # we don't want to lose a bit of entropy on overflow, so add it back in. + Num.addWrap nextAccum 1 + else + nextAccum + ) + |> \accum -> addU64 hasher accum diff --git a/crates/compiler/builtins/roc/Inspect.roc b/crates/compiler/builtins/roc/Inspect.roc new file mode 100644 index 0000000000..a86b83a247 --- /dev/null +++ b/crates/compiler/builtins/roc/Inspect.roc @@ -0,0 +1,349 @@ +interface Inspect + exposes [ + Inspect, + Inspector, + InspectFormatter, + ElemWalker, + KeyValWalker, + inspect, + init, + list, + set, + dict, + tag, + tuple, + record, + bool, + str, + function, + opaque, + u8, + i8, + u16, + i16, + u32, + i32, + u64, + i64, + u128, + i128, + nat, + f32, + f64, + dec, + custom, + apply, + toInspector, + DbgFormatter, + toDbgStr, + ] + imports [ + Bool.{ Bool }, + Num.{ U8, U16, U32, U64, U128, I8, I16, I32, I64, I128, F32, F64, Dec, Nat }, + List, + Str, + ] + +KeyValWalker state collection key val : collection, state, (state, key, val -> state) -> state +ElemWalker state collection elem : collection, state, (state, elem -> state) -> state + +InspectFormatter implements + init : {} -> f where f implements InspectFormatter + + tag : Str, List (Inspector f) -> Inspector f where f implements InspectFormatter + tuple : List (Inspector f) -> Inspector f where f implements InspectFormatter + record : List { key : Str, value : Inspector f } -> Inspector f where f implements InspectFormatter + bool : Bool -> Inspector f where f implements InspectFormatter + str : Str -> Inspector f where f implements InspectFormatter + + list : list, ElemWalker state list elem, (elem -> Inspector f) -> Inspector f where f implements InspectFormatter + set : set, ElemWalker state set elem, (elem -> Inspector f) -> Inspector f where f implements InspectFormatter + dict : dict, KeyValWalker state dict key value, (key -> Inspector f), (value -> Inspector f) -> Inspector f where f implements InspectFormatter + + # In text, this would render as `` + # TODO: Pass the type name to opaque so that it can be displayed. + opaque : * -> Inspector f where f implements InspectFormatter + + # In text, this would render as `` + # TODO: Maybe pass the the function name or signiture to function so that it can be displayed. + function : * -> Inspector f where f implements InspectFormatter + + u8 : U8 -> Inspector f where f implements InspectFormatter + i8 : I8 -> Inspector f where f implements InspectFormatter + u16 : U16 -> Inspector f where f implements InspectFormatter + i16 : I16 -> Inspector f where f implements InspectFormatter + u32 : U32 -> Inspector f where f implements InspectFormatter + i32 : I32 -> Inspector f where f implements InspectFormatter + u64 : U64 -> Inspector f where f implements InspectFormatter + i64 : I64 -> Inspector f where f implements InspectFormatter + u128 : U128 -> Inspector f where f implements InspectFormatter + i128 : I128 -> Inspector f where f implements InspectFormatter + nat : Nat -> Inspector f where f implements InspectFormatter + f32 : F32 -> Inspector f where f implements InspectFormatter + f64 : F64 -> Inspector f where f implements InspectFormatter + dec : Dec -> Inspector f where f implements InspectFormatter + +Inspector f := f -> f where f implements InspectFormatter + +custom : (f -> f) -> Inspector f where f implements InspectFormatter +custom = \fn -> @Inspector fn + +apply : Inspector f, f -> f where f implements InspectFormatter +apply = \@Inspector fn, fmt -> fn fmt + +Inspect implements + toInspector : val -> Inspector f where val implements Inspect, f implements InspectFormatter + +inspect : val -> f where val implements Inspect, f implements InspectFormatter +inspect = \val -> + (@Inspector valFn) = toInspector val + valFn (init {}) + +# The current default formatter for inspect. +# This just returns a simple string for debugging. +# More powerful formatters will likely be wanted in the future. +DbgFormatter := { data : Str } + implements [ + InspectFormatter { + init: dbgInit, + list: dbgList, + set: dbgSet, + dict: dbgDict, + tag: dbgTag, + tuple: dbgTuple, + record: dbgRecord, + bool: dbgBool, + str: dbgStr, + opaque: dbgOpaque, + function: dbgFunction, + u8: dbgU8, + i8: dbgI8, + u16: dbgU16, + i16: dbgI16, + u32: dbgU32, + i32: dbgI32, + u64: dbgU64, + i64: dbgI64, + u128: dbgU128, + i128: dbgI128, + nat: dbgNat, + f32: dbgF32, + f64: dbgF64, + dec: dbgDec, + }, + ] + +dbgInit : {} -> DbgFormatter +dbgInit = \{} -> @DbgFormatter { data: "" } + +dbgList : list, ElemWalker (DbgFormatter, Bool) list elem, (elem -> Inspector DbgFormatter) -> Inspector DbgFormatter +dbgList = \content, walkFn, toDbgInspector -> + f0 <- custom + dbgWrite f0 "[" + |> \f1 -> + (f2, prependSep), elem <- walkFn content (f1, Bool.false) + f3 = + if prependSep then + dbgWrite f2 ", " + else + f2 + + elem + |> toDbgInspector + |> apply f3 + |> \f4 -> (f4, Bool.true) + |> .0 + |> dbgWrite "]" + +dbgSet : set, ElemWalker (DbgFormatter, Bool) set elem, (elem -> Inspector DbgFormatter) -> Inspector DbgFormatter +dbgSet = \content, walkFn, toDbgInspector -> + f0 <- custom + dbgWrite f0 "{" + |> \f1 -> + (f2, prependSep), elem <- walkFn content (f1, Bool.false) + f3 = + if prependSep then + dbgWrite f2 ", " + else + f2 + + elem + |> toDbgInspector + |> apply f3 + |> \f4 -> (f4, Bool.true) + |> .0 + |> dbgWrite "}" + +dbgDict : dict, KeyValWalker (DbgFormatter, Bool) dict key value, (key -> Inspector DbgFormatter), (value -> Inspector DbgFormatter) -> Inspector DbgFormatter +dbgDict = \d, walkFn, keyToInspector, valueToInspector -> + f0 <- custom + dbgWrite f0 "{" + |> \f1 -> + (f2, prependSep), key, value <- walkFn d (f1, Bool.false) + f3 = + if prependSep then + dbgWrite f2 ", " + else + f2 + + apply (keyToInspector key) f3 + |> dbgWrite ": " + |> \x -> apply (valueToInspector value) x + |> \f4 -> (f4, Bool.true) + |> .0 + |> dbgWrite "}" + +dbgTag : Str, List (Inspector DbgFormatter) -> Inspector DbgFormatter +dbgTag = \name, fields -> + if List.isEmpty fields then + f0 <- custom + dbgWrite f0 name + else + f0 <- custom + dbgWrite f0 "(" + |> dbgWrite name + |> \f1 -> + f2, inspector <- List.walk fields f1 + dbgWrite f2 " " + |> \x -> apply inspector x + |> dbgWrite ")" + +dbgTuple : List (Inspector DbgFormatter) -> Inspector DbgFormatter +dbgTuple = \fields -> + f0 <- custom + dbgWrite f0 "(" + |> \f1 -> + (f2, prependSep), inspector <- List.walk fields (f1, Bool.false) + f3 = + if prependSep then + dbgWrite f2 ", " + else + f2 + + apply inspector f3 + |> \f4 -> (f4, Bool.true) + |> .0 + |> dbgWrite ")" + +dbgRecord : List { key : Str, value : Inspector DbgFormatter } -> Inspector DbgFormatter +dbgRecord = \fields -> + f0 <- custom + dbgWrite f0 "{" + |> \f1 -> + (f2, prependSep), { key, value } <- List.walk fields (f1, Bool.false) + f3 = + if prependSep then + dbgWrite f2 ", " + else + f2 + + dbgWrite f3 key + |> dbgWrite ": " + |> \x -> apply value x + |> \f4 -> (f4, Bool.true) + |> .0 + |> dbgWrite "}" + +dbgBool : Bool -> Inspector DbgFormatter +dbgBool = \b -> + if b then + f0 <- custom + dbgWrite f0 "Bool.true" + else + f0 <- custom + dbgWrite f0 "Bool.false" + +dbgStr : Str -> Inspector DbgFormatter +dbgStr = \s -> + f0 <- custom + f0 + |> dbgWrite "\"" + |> dbgWrite s # TODO: Should we be escaping strings for dbg/logging? + |> dbgWrite "\"" + +dbgOpaque : * -> Inspector DbgFormatter +dbgOpaque = \_ -> + f0 <- custom + dbgWrite f0 "" + +dbgFunction : * -> Inspector DbgFormatter +dbgFunction = \_ -> + f0 <- custom + dbgWrite f0 "" + +dbgU8 : U8 -> Inspector DbgFormatter +dbgU8 = \num -> + f0 <- custom + dbgWrite f0 (num |> Num.toStr) + +dbgI8 : I8 -> Inspector DbgFormatter +dbgI8 = \num -> + f0 <- custom + dbgWrite f0 (num |> Num.toStr) + +dbgU16 : U16 -> Inspector DbgFormatter +dbgU16 = \num -> + f0 <- custom + dbgWrite f0 (num |> Num.toStr) + +dbgI16 : I16 -> Inspector DbgFormatter +dbgI16 = \num -> + f0 <- custom + dbgWrite f0 (num |> Num.toStr) + +dbgU32 : U32 -> Inspector DbgFormatter +dbgU32 = \num -> + f0 <- custom + dbgWrite f0 (num |> Num.toStr) + +dbgI32 : I32 -> Inspector DbgFormatter +dbgI32 = \num -> + f0 <- custom + dbgWrite f0 (num |> Num.toStr) + +dbgU64 : U64 -> Inspector DbgFormatter +dbgU64 = \num -> + f0 <- custom + dbgWrite f0 (num |> Num.toStr) + +dbgI64 : I64 -> Inspector DbgFormatter +dbgI64 = \num -> + f0 <- custom + dbgWrite f0 (num |> Num.toStr) + +dbgU128 : U128 -> Inspector DbgFormatter +dbgU128 = \num -> + f0 <- custom + dbgWrite f0 (num |> Num.toStr) + +dbgI128 : I128 -> Inspector DbgFormatter +dbgI128 = \num -> + f0 <- custom + dbgWrite f0 (num |> Num.toStr) + +dbgNat : Nat -> Inspector DbgFormatter +dbgNat = \num -> + f0 <- custom + dbgWrite f0 (num |> Num.toStr) + +dbgF32 : F32 -> Inspector DbgFormatter +dbgF32 = \num -> + f0 <- custom + dbgWrite f0 (num |> Num.toStr) + +dbgF64 : F64 -> Inspector DbgFormatter +dbgF64 = \num -> + f0 <- custom + dbgWrite f0 (num |> Num.toStr) + +dbgDec : Dec -> Inspector DbgFormatter +dbgDec = \num -> + f0 <- custom + dbgWrite f0 (num |> Num.toStr) + +dbgWrite : DbgFormatter, Str -> DbgFormatter +dbgWrite = \@DbgFormatter { data }, added -> + @DbgFormatter { data: Str.concat data added } + +toDbgStr : DbgFormatter -> Str +toDbgStr = \@DbgFormatter { data } -> data diff --git a/crates/compiler/builtins/roc/List.roc b/crates/compiler/builtins/roc/List.roc new file mode 100644 index 0000000000..bcfa47366e --- /dev/null +++ b/crates/compiler/builtins/roc/List.roc @@ -0,0 +1,1307 @@ +interface List + exposes [ + isEmpty, + get, + set, + replace, + update, + append, + appendIfOk, + prepend, + prependIfOk, + map, + len, + withCapacity, + walkBackwards, + concat, + first, + single, + repeat, + reverse, + join, + keepIf, + contains, + sum, + walk, + last, + keepOks, + keepErrs, + mapWithIndex, + map2, + map3, + product, + walkWithIndex, + walkUntil, + walkFrom, + walkFromUntil, + range, + sortWith, + swap, + dropAt, + min, + max, + map4, + mapTry, + walkTry, + joinMap, + any, + takeFirst, + takeLast, + dropFirst, + dropLast, + findFirst, + findLast, + findFirstIndex, + findLastIndex, + sublist, + intersperse, + split, + splitFirst, + splitLast, + startsWith, + endsWith, + all, + dropIf, + sortAsc, + sortDesc, + reserve, + releaseExcessCapacity, + walkBackwardsUntil, + countIf, + chunksOf, + ] + imports [ + Bool.{ Bool, Eq }, + Result.{ Result }, + Num.{ Nat, Num, Int }, + ] + +## ## Types +## +## A sequential list of values. +## ``` +## [1, 2, 3] # a list of numbers +## ["a", "b", "c"] # a list of strings +## [[1.1], [], [2.2, 3.3]] # a list of lists of numbers +## ``` +## The maximum size of a [List] is limited by the amount of heap memory available +## to the current process. If there is not enough memory available, attempting to +## create the list could crash. (On Linux, where [overcommit](https://www.etalabs.net/overcommit.html) +## is normally enabled, not having enough memory could result in the list appearing +## to be created just fine, but then crashing later.) +## +## > The theoretical maximum length for a list created in Roc is half of +## > `Num.maxNat`. Attempting to create a list bigger than that +## > in Roc code will always fail, although in practice it is likely to fail +## > at much smaller lengths due to insufficient memory being available. +## +## ## Performance Details +## +## Under the hood, a list is a record containing a `len : Nat` field, a `capacity : Nat` +## field, and a pointer to a reference count and a flat array of bytes. +## +## ## Shared Lists +## +## Shared lists are [reference counted](https://en.wikipedia.org/wiki/Reference_counting). +## +## Each time a given list gets referenced, its reference count ("refcount" for short) +## gets incremented. Each time a list goes out of scope, its refcount count gets +## decremented. Once a refcount, has been decremented more times than it has been +## incremented, we know nothing is referencing it anymore, and the list's memory +## will be immediately freed. +## +## Let's look at an example. +## ``` +## ratings = [5, 4, 3] +## +## { foo: ratings, bar: ratings } +## ``` +## The first line binds the name `ratings` to the list `[5, 4, 3]`. The list +## begins with a refcount of 1, because so far only `ratings` is referencing it. +## +## The second line alters this refcount. `{ foo: ratings` references +## the `ratings` list, and so does `bar: ratings }`. This will result in its +## refcount getting incremented from 1 to 3. +## +## Let's turn this example into a function. +## ``` +## getRatings = \first -> +## ratings = [first, 4, 3] +## +## { foo: ratings, bar: ratings } +## +## getRatings 5 +## ``` +## At the end of the `getRatings` function, when the record gets returned, +## the original `ratings =` binding has gone out of scope and is no longer +## accessible. (Trying to reference `ratings` outside the scope of the +## `getRatings` function would be an error!) +## +## Since `ratings` represented a way to reference the list, and that way is no +## longer accessible, the list's refcount gets decremented when `ratings` goes +## out of scope. It will decrease from 3 back down to 2. +## +## Putting these together, when we call `getRatings 5`, what we get back is +## a record with two fields, `foo`, and `bar`, each of which refers to the same +## list, and that list has a refcount of 2. +## +## Let's change the last line to be `(getRatings 5).bar` instead of `getRatings 5`: +## ``` +## getRatings = \first -> +## ratings = [first, 4, 3] +## +## { foo: ratings, bar: ratings } +## +## (getRatings 5).bar +## ``` +## Now, when this expression returns, only the `bar` field of the record will +## be returned. This will mean that the `foo` field becomes inaccessible, causing +## the list's refcount to get decremented from 2 to 1. At this point, the list is back +## where it started: there is only 1 reference to it. +## +## Finally let's suppose the final line were changed to this: +## ``` +## List.first (getRatings 5).bar +## ``` +## This call to [List.first] means that even the list in the `bar` field has become +## inaccessible. As such, this line will cause the list's refcount to get +## decremented all the way to 0. At that point, nothing is referencing the list +## anymore, and its memory will get freed. +## +## Things are different if this is a list of lists instead of a list of numbers. +## Let's look at a simpler example using [List.first] - first with a list of numbers, +## and then with a list of lists, to see how they differ. +## +## Here's the example using a list of numbers. +## ``` +## nums = [1, 2, 3, 4, 5, 6, 7] +## +## first = List.first nums +## last = List.last nums +## +## first +## ``` +## It makes a list, calls [List.first] and [List.last] on it, and then returns `first`. +## +## Here's the equivalent code with a list of lists: +## ``` +## lists = [[1], [2, 3], [], [4, 5, 6, 7]] +## +## first = List.first lists +## last = List.last lists +## +## first +## ``` +## **TODO** explain how in the former example, when we go to free `nums` at the end, +## we can free it immediately because there are no other refcounts. However, +## in the case of `lists`, we have to iterate through the list and decrement +## the refcounts of each of its contained lists - because they, too, have +## refcounts! Importantly, because the first element had its refcount incremented +## because the function returned `first`, that element will actually end up +## *not* getting freed at the end - but all the others will be. +## +## In the `lists` example, `lists = [...]` also creates a list with an initial +## refcount of 1. Separately, it also creates several other lists - each with +## their own refcounts - to go inside that list. (The empty list at the end +## does not use heap memory, and thus has no refcount.) +## +## At the end, we once again call [List.first] on the list, but this time +## +## * Copying small lists (64 elements or fewer) is typically slightly faster than copying small persistent data structures. This is because, at small sizes, persistent data structures tend to be thin wrappers around flat arrays anyway. They don't have any copying advantage until crossing a certain minimum size threshold. +## * Even when copying is faster, other list operations may still be slightly slower with persistent data structures. For example, even if it were a persistent data structure, [List.map], [List.walk], and [List.keepIf] would all need to traverse every element in the list and build up the result from scratch. These operations are all +## * Roc's compiler optimizes many list operations into in-place mutations behind the scenes, depending on how the list is being used. For example, [List.map], [List.keepIf], and [List.set] can all be optimized to perform in-place mutations. +## * If possible, it is usually best for performance to use large lists in a way where the optimizer can turn them into in-place mutations. If this is not possible, a persistent data structure might be faster - but this is a rare enough scenario that it would not be good for the average Roc program's performance if this were the way [List] worked by default. Instead, you can look outside Roc's standard modules for an implementation of a persistent data structure - likely built using [List] under the hood! + +# separator so List.isEmpty doesn't absorb the above into its doc comment + +## Check if the list is empty. +## ``` +## List.isEmpty [1, 2, 3] +## +## List.isEmpty [] +## ``` +isEmpty : List * -> Bool +isEmpty = \list -> + List.len list == 0 + +# unsafe primitive that does not perform a bounds check +# but will cause a reference count increment on the value it got out of the list +getUnsafe : List a, Nat -> a + +## Returns an element from a list at the given index. +## +## Returns `Err OutOfBounds` if the given index exceeds the List's length +## ``` +## expect List.get [100, 200, 300] 1 == Ok 200 +## expect List.get [100, 200, 300] 5 == Err OutOfBounds +## ``` +get : List a, Nat -> Result a [OutOfBounds] +get = \list, index -> + if index < List.len list then + Ok (List.getUnsafe list index) + else + Err OutOfBounds + +# unsafe primitive that does not perform a bounds check +# but will cause a reference count increment on the value it got out of the list +replaceUnsafe : List a, Nat, a -> { list : List a, value : a } + +replace : List a, Nat, a -> { list : List a, value : a } +replace = \list, index, newValue -> + if index < List.len list then + List.replaceUnsafe list index newValue + else + { list, value: newValue } + +## Replaces the element at the given index with a replacement. +## ``` +## List.set ["a", "b", "c"] 1 "B" +## ``` +## If the given index is outside the bounds of the list, returns the original +## list unmodified. +## +## To drop the element at a given index, instead of replacing it, see [List.dropAt]. +set : List a, Nat, a -> List a +set = \list, index, value -> + (List.replace list index value).list + +## Updates the element at the given index with the given function. +## ``` +## List.update [1, 2, 3] 1 (\x -> x + 1) +## ``` +## If the given index is outside the bounds of the list, returns the original +## list unmodified. +## +## To replace the element at a given index, instead of updating based on the current value, +## see [List.set] and [List.replace] +update : List a, Nat, (a -> a) -> List a +update = \list, index, func -> + when List.get list index is + Err OutOfBounds -> list + Ok value -> + newValue = func value + (replaceUnsafe list index newValue).list + +# Update one element in bounds +expect + list : List Nat + list = [1, 2, 3] + got = update list 1 (\x -> x + 42) + want = [1, 44, 3] + got == want + +# Update out of bounds +expect + list : List Nat + list = [1, 2, 3] + got = update list 5 (\x -> x + 42) + got == list + +# Update chain +expect + list : List Nat + list = [1, 2, 3] + got = + list + |> update 0 (\x -> x + 10) + |> update 1 (\x -> x + 20) + |> update 2 (\x -> x + 30) + want = [11, 22, 33] + got == want + +## Add a single element to the end of a list. +## ``` +## List.append [1, 2, 3] 4 +## +## [0, 1, 2] +## |> List.append 3 +## ``` +append : List a, a -> List a +append = \list, element -> + list + |> List.reserve 1 + |> List.appendUnsafe element + +## If the given [Result] is `Ok`, add it to the end of a list. +## Otherwise, return the list unmodified. +## +## ``` +## List.appendIfOk [1, 2, 3] (Ok 4) +## +## [0, 1, 2] +## |> List.appendIfOk (Err 3) +## ``` +appendIfOk : List a, Result a * -> List a +appendIfOk = \list, result -> + when result is + Ok elem -> append list elem + Err _ -> list + +## Writes the element after the current last element unconditionally. +## In other words, it is assumed that +## +## - the list is owned (i.e. can be updated in-place +## - the list has at least one element of spare capacity +appendUnsafe : List a, a -> List a + +## Add a single element to the beginning of a list. +## ``` +## List.prepend [1, 2, 3] 0 +## +## [2, 3, 4] +## |> List.prepend 1 +## ``` +prepend : List a, a -> List a + +## If the given [Result] is `Ok`, add it to the beginning of a list. +## Otherwise, return the list unmodified. +## +## ``` +## List.prepend [1, 2, 3] (Ok 0) +## +## [2, 3, 4] +## |> List.prepend (Err 1) +## ``` +prependIfOk : List a, Result a * -> List a +prependIfOk = \list, result -> + when result is + Ok elem -> prepend list elem + Err _ -> list + +## Returns the length of the list - the number of elements it contains. +## +## One [List] can store up to 2,147,483,648 elements (just over 2 billion), which +## is exactly equal to the highest valid #I32 value. This means the #U32 this function +## returns can always be safely converted to an #I32 without losing any data. +len : List * -> Nat + +## Create a list with space for at least capacity elements +withCapacity : Nat -> List * + +## Enlarge the list for at least capacity additional elements +reserve : List a, Nat -> List a + +## Shrink the memory footprint of a list such that it's capacity and length are equal. +## Note: This will also convert seamless slices to regular lists. +releaseExcessCapacity : List a -> List a + +## Put two lists together. +## ``` +## List.concat [1, 2, 3] [4, 5] +## +## [0, 1, 2] +## |> List.concat [3, 4] +## ``` +concat : List a, List a -> List a + +## Returns the last element in the list, or `ListWasEmpty` if it was empty. +## ``` +## expect List.last [1, 2, 3] == Ok 3 +## expect List.last [] == Err ListWasEmpty +## ``` +last : List a -> Result a [ListWasEmpty] +last = \list -> + when List.get list (Num.subSaturated (List.len list) 1) is + Ok v -> Ok v + Err _ -> Err ListWasEmpty + +## A list with a single element in it. +## +## This is useful in pipelines, like so: +## ``` +## websites = +## Str.concat domain ".com" +## |> List.single +## ``` +single : a -> List a +single = \x -> [x] + +## Returns a list with the given length, where every element is the given value. +repeat : a, Nat -> List a +repeat = \value, count -> + repeatHelp value count (List.withCapacity count) + +repeatHelp : a, Nat, List a -> List a +repeatHelp = \value, count, accum -> + if count > 0 then + repeatHelp value (Num.subWrap count 1) (List.appendUnsafe accum value) + else + accum + +## Returns the list with its elements reversed. +## ``` +## expect List.reverse [1, 2, 3] == [3, 2, 1] +## ``` +reverse : List a -> List a +reverse = \list -> + reverseHelp list 0 (Num.subSaturated (List.len list) 1) + +reverseHelp = \list, left, right -> + if left < right then + reverseHelp (List.swap list left right) (Num.addWrap left 1) (Num.subWrap right 1) + else + list + +## Join the given lists together into one list. +## ``` +## expect List.join [[1], [2, 3], [], [4, 5]] == [1, 2, 3, 4, 5] +## expect List.join [[], []] == [] +## expect List.join [] == [] +## ``` +join : List (List a) -> List a +join = \lists -> + totalLength = + List.walk lists 0 (\state, list -> Num.addWrap state (List.len list)) + + List.walk lists (List.withCapacity totalLength) \state, list -> List.concat state list + +contains : List a, a -> Bool where a implements Eq +contains = \list, needle -> + List.any list (\x -> x == needle) + +## Build a value using each element in the list. +## +## Starting with a given `state` value, this walks through each element in the +## list from first to last, running a given `step` function on that element +## which updates the `state`. It returns the final `state` at the end. +## +## You can use it in a pipeline: +## ``` +## [2, 4, 8] +## |> List.walk 0 Num.add +## ``` +## This returns 14 because: +## * `state` starts at 0 +## * Each `step` runs `Num.add state elem`, and the return value becomes the new `state`. +## +## Here is a table of how `state` changes as [List.walk] walks over the elements +## `[2, 4, 8]` using [Num.add] as its `step` function to determine the next `state`. +## +## state | elem | Num.add state elem +## :---: | :---: | :----------------: +## 0 | | +## 0 | 2 | 2 +## 2 | 4 | 6 +## 6 | 8 | 14 +## +## The following returns -6: +## ``` +## [1, 2, 3] +## |> List.walk 0 Num.sub +## ``` +## Note that in other languages, `walk` is sometimes called `reduce`, +## `fold`, `foldLeft`, or `foldl`. +walk : List elem, state, (state, elem -> state) -> state +walk = \list, init, func -> + walkHelp list init func 0 (List.len list) + +## internal helper +walkHelp : List elem, s, (s, elem -> s), Nat, Nat -> s +walkHelp = \list, state, f, index, length -> + if index < length then + nextState = f state (List.getUnsafe list index) + + walkHelp list nextState f (Num.addWrap index 1) length + else + state + +## Like [walk], but at each step the function also receives the index of the current element. +walkWithIndex : List elem, state, (state, elem, Nat -> state) -> state +walkWithIndex = \list, init, func -> + walkWithIndexHelp list init func 0 (List.len list) + +## internal helper +walkWithIndexHelp : List elem, s, (s, elem, Nat -> s), Nat, Nat -> s +walkWithIndexHelp = \list, state, f, index, length -> + if index < length then + nextState = f state (List.getUnsafe list index) index + + walkWithIndexHelp list nextState f (Num.addWrap index 1) length + else + state + +## Note that in other languages, `walkBackwards` is sometimes called `reduceRight`, +## `fold`, `foldRight`, or `foldr`. +walkBackwards : List elem, state, (state, elem -> state) -> state +walkBackwards = \list, state, func -> + walkBackwardsHelp list state func (len list) + +## internal helper +walkBackwardsHelp : List elem, state, (state, elem -> state), Nat -> state +walkBackwardsHelp = \list, state, f, indexPlusOne -> + if indexPlusOne == 0 then + state + else + index = Num.subWrap indexPlusOne 1 + nextState = f state (getUnsafe list index) + + walkBackwardsHelp list nextState f index + +## Same as [List.walk], except you can stop walking early. +## +## ## Performance Details +## +## Compared to [List.walk], this can potentially visit fewer elements (which can +## improve performance) at the cost of making each step take longer. +## However, the added cost to each step is extremely small, and can easily +## be outweighed if it results in skipping even a small number of elements. +## +## As such, it is typically better for performance to use this over [List.walk] +## if returning `Break` earlier than the last element is expected to be common. +walkUntil : List elem, state, (state, elem -> [Continue state, Break state]) -> state +walkUntil = \list, initial, step -> + when List.iterate list initial step is + Continue new -> new + Break new -> new + +## Same as [List.walkUntil], but does it from the end of the list instead. +walkBackwardsUntil : List elem, state, (state, elem -> [Continue state, Break state]) -> state +walkBackwardsUntil = \list, initial, func -> + when List.iterateBackwards list initial func is + Continue new -> new + Break new -> new + +## Walks to the end of the list from a specified starting index +walkFrom : List elem, Nat, state, (state, elem -> state) -> state +walkFrom = \list, index, state, func -> + step : _, _ -> [Continue _, Break []] + step = \currentState, element -> Continue (func currentState element) + + when List.iterHelp list state step index (List.len list) is + Continue new -> new + +## A combination of [List.walkFrom] and [List.walkUntil] +walkFromUntil : List elem, Nat, state, (state, elem -> [Continue state, Break state]) -> state +walkFromUntil = \list, index, state, func -> + when List.iterHelp list state func index (List.len list) is + Continue new -> new + Break new -> new + +sum : List (Num a) -> Num a +sum = \list -> + List.walk list 0 Num.add + +product : List (Num a) -> Num a +product = \list -> + List.walk list 1 Num.mul + +## Run the given predicate on each element of the list, returning `Bool.true` if +## any of the elements satisfy it. +any : List a, (a -> Bool) -> Bool +any = \list, predicate -> + looper = \{}, element -> + if predicate element then + Break {} + else + Continue {} + + when List.iterate list {} looper is + Continue {} -> Bool.false + Break {} -> Bool.true + +## Run the given predicate on each element of the list, returning `Bool.true` if +## all of the elements satisfy it. +all : List a, (a -> Bool) -> Bool +all = \list, predicate -> + looper = \{}, element -> + if predicate element then + Continue {} + else + Break {} + + when List.iterate list {} looper is + Continue {} -> Bool.true + Break {} -> Bool.false + +## Run the given function on each element of a list, and return all the +## elements for which the function returned `Bool.true`. +## ``` +## List.keepIf [1, 2, 3, 4] (\num -> num > 2) +## ``` +## ## Performance Details +## +## [List.keepIf] always returns a list that takes up exactly the same amount +## of memory as the original, even if its length decreases. This is because it +## can't know in advance exactly how much space it will need, and if it guesses a +## length that's too low, it would have to re-allocate. +## +## (If you want to do an operation like this which reduces the memory footprint +## of the resulting list, you can do two passes over the list with [List.walk] - one +## to calculate the precise new size, and another to populate the new list.) +## +## If given a unique list, [List.keepIf] will mutate it in place to assemble the appropriate list. +## If that happens, this function will not allocate any new memory on the heap. +## If all elements in the list end up being kept, Roc will return the original +## list unaltered. +## +keepIf : List a, (a -> Bool) -> List a +keepIf = \list, predicate -> + length = List.len list + + keepIfHelp list predicate 0 0 length + +keepIfHelp : List a, (a -> Bool), Nat, Nat, Nat -> List a +keepIfHelp = \list, predicate, kept, index, length -> + if index < length then + if predicate (List.getUnsafe list index) then + keepIfHelp (List.swap list kept index) predicate (Num.addWrap kept 1) (Num.addWrap index 1) length + else + keepIfHelp list predicate kept (Num.addWrap index 1) length + else + List.takeFirst list kept + +## Run the given function on each element of a list, and return all the +## elements for which the function returned `Bool.false`. +## ``` +## List.dropIf [1, 2, 3, 4] (\num -> num > 2) +## ``` +## ## Performance Details +## +## `List.dropIf` has the same performance characteristics as [List.keepIf]. +## See its documentation for details on those characteristics! +dropIf : List a, (a -> Bool) -> List a +dropIf = \list, predicate -> + List.keepIf list (\e -> Bool.not (predicate e)) + +## Run the given function on each element of a list, and return the +## number of elements for which the function returned `Bool.true`. +## ``` +## expect List.countIf [1, -2, -3] Num.isNegative == 2 +## expect List.countIf [1, 2, 3] (\num -> num > 1 ) == 2 +## ``` +countIf : List a, (a -> Bool) -> Nat +countIf = \list, predicate -> + walkState = \state, elem -> + if predicate elem then + Num.addWrap state 1 + else + state + + List.walk list 0 walkState + +## This works like [List.map], except only the transformed values that are +## wrapped in `Ok` are kept. Any that are wrapped in `Err` are dropped. +## ``` +## expect List.keepOks ["1", "Two", "23", "Bird"] Str.toI32 == [1, 23] +## +## expect List.keepOks [["a", "b"], [], ["c", "d", "e"], [] ] List.first == ["a", "c"] +## +## fn = \str -> if Str.isEmpty str then Err StrWasEmpty else Ok str +## expect List.keepOks ["", "a", "bc", "", "d", "ef", ""] fn == ["a", "bc", "d", "ef"] +## ``` +keepOks : List before, (before -> Result after *) -> List after +keepOks = \list, toResult -> + walker = \accum, element -> + when toResult element is + Ok keep -> List.append accum keep + Err _drop -> accum + + List.walk list (List.withCapacity (List.len list)) walker + +## This works like [List.map], except only the transformed values that are +## wrapped in `Err` are kept. Any that are wrapped in `Ok` are dropped. +## ``` +## List.keepErrs [["a", "b"], [], [], ["c", "d", "e"]] List.last +## +## fn = \str -> if Str.isEmpty str then Err StrWasEmpty else Ok (Str.len str) +## +## List.keepErrs ["", "a", "bc", "", "d", "ef", ""] +## ``` +keepErrs : List before, (before -> Result * after) -> List after +keepErrs = \list, toResult -> + walker = \accum, element -> + when toResult element is + Ok _drop -> accum + Err keep -> List.append accum keep + + List.walk list (List.withCapacity (List.len list)) walker + +## Convert each element in the list to something new, by calling a conversion +## function on each of them. Then return a new list of the converted values. +## ``` +## expect List.map [1, 2, 3] (\num -> num + 1) == [2, 3, 4] +## +## expect List.map ["", "a", "bc"] Str.isEmpty == [Bool.true, Bool.false, Bool.false] +## ``` +map : List a, (a -> b) -> List b + +## Run a transformation function on the first element of each list, +## and use that as the first element in the returned list. +## Repeat until a list runs out of elements. +## +## Some languages have a function named `zip`, which does something similar to +## calling [List.map2] passing two lists and `Pair`: +## ``` +## zipped = List.map2 ["a", "b", "c"] [1, 2, 3] Pair +## ``` +map2 : List a, List b, (a, b -> c) -> List c + +## Run a transformation function on the first element of each list, +## and use that as the first element in the returned list. +## Repeat until a list runs out of elements. +map3 : List a, List b, List c, (a, b, c -> d) -> List d + +## Run a transformation function on the first element of each list, +## and use that as the first element in the returned list. +## Repeat until a list runs out of elements. +map4 : List a, List b, List c, List d, (a, b, c, d -> e) -> List e + +## This works like [List.map], except it also passes the index +## of the element to the conversion function. +## ``` +## expect List.mapWithIndex [10, 20, 30] (\num, index -> num + index) == [10, 21, 32] +## ``` +mapWithIndex : List a, (a, Nat -> b) -> List b +mapWithIndex = \src, func -> + length = len src + dest = withCapacity length + + mapWithIndexHelp src dest func 0 length + +# Internal helper +mapWithIndexHelp : List a, List b, (a, Nat -> b), Nat, Nat -> List b +mapWithIndexHelp = \src, dest, func, index, length -> + if index < length then + elem = getUnsafe src index + mappedElem = func elem index + newDest = List.appendUnsafe dest mappedElem + + mapWithIndexHelp src newDest func (Num.addWrap index 1) length + else + dest + +## Returns a list of all the integers between `start` and `end`. +## +## To include the `start` and `end` integers themselves, use `At` like so: +## ``` +## List.range { start: At 2, end: At 5 } # returns [2, 3, 4, 5] +## ``` +## To exclude them, use `After` and `Before`, like so: +## ``` +## List.range { start: After 2, end: Before 5 } # returns [3, 4] +## ``` +## You can have the list end at a certain length rather than a certain integer: +## ``` +## List.range { start: At 6, end: Length 4 } # returns [6, 7, 8, 9] +## ``` +## If `step` is specified, each integer increases by that much. (`step: 1` is the default.) +## ``` +## List.range { start: After 0, end: Before 9, step: 3 } # returns [3, 6] +## ``` +## List.range will also generate a reversed list if step is negative or end comes before start: +## ``` +## List.range { start: At 5, end: At 2 } # returns [5, 4, 3, 2] +## ``` +## All of these options are compatible with the others. For example, you can use `At` or `After` +## with `start` regardless of what `end` and `step` are set to. +range : _ +range = \{ start, end, step ? 0 } -> + { calcNext, stepIsPositive } = + if step == 0 then + when T start end is + T (At x) (At y) | T (At x) (Before y) | T (After x) (At y) | T (After x) (Before y) -> + if x < y then + { + calcNext: \i -> Num.addChecked i 1, + stepIsPositive: Bool.true, + } + else + { + calcNext: \i -> Num.subChecked i 1, + stepIsPositive: Bool.false, + } + + T (At _) (Length _) | T (After _) (Length _) -> + { + calcNext: \i -> Num.addChecked i 1, + stepIsPositive: Bool.true, + } + else + { + calcNext: \i -> Num.addChecked i step, + stepIsPositive: step > 0, + } + + inclusiveStart = + when start is + At x -> Ok x + After x -> calcNext x + + when end is + At at -> + isValid = + if stepIsPositive then + \i -> i <= at + else + \i -> i >= at + + # TODO: switch to List.withCapacity + rangeHelp [] inclusiveStart calcNext isValid + + Before before -> + isValid = + if stepIsPositive then + \i -> i < before + else + \i -> i > before + + # TODO: switch to List.withCapacity + rangeHelp [] inclusiveStart calcNext isValid + + Length l -> + rangeLengthHelp (List.withCapacity l) inclusiveStart l calcNext + +rangeHelp = \accum, i, calcNext, isValid -> + when i is + Ok val -> + if isValid val then + # TODO: change this to List.appendUnsafe once capacity is set correctly + rangeHelp (List.append accum val) (calcNext val) calcNext isValid + else + accum + + Err _ -> + # We went past the end of the numeric range and there is no next. + # return the generated list. + accum + +rangeLengthHelp = \accum, i, remaining, calcNext -> + if remaining == 0 then + accum + else + when i is + Ok val -> + rangeLengthHelp (List.appendUnsafe accum val) (calcNext val) (Num.subWrap remaining 1) calcNext + + Err _ -> + # We went past the end of the numeric range and there is no next. + # The list is not the correct length yet, so we must crash. + crash "List.range: failed to generate enough elements to fill the range before overflowing the numeric type" + +expect + List.range { start: At 0, end: At 4 } == [0, 1, 2, 3, 4] + +expect + List.range { start: After 0, end: At 4 } == [1, 2, 3, 4] + +expect + List.range { start: At 0, end: At 4, step: 2 } == [0, 2, 4] + +expect + List.range { start: At 0, end: Before 4 } == [0, 1, 2, 3] + +expect + List.range { start: After 0, end: Before 4 } == [1, 2, 3] + +expect + List.range { start: At 0, end: Before 4, step: 2 } == [0, 2] + +expect + List.range { start: At 4, end: Length 5 } == [4, 5, 6, 7, 8] + +expect + List.range { start: At 4, end: Length 5, step: 10 } == [4, 14, 24, 34, 44] + +expect + List.range { start: At 4, end: Length 5, step: -3 } == [4, 1, -2, -5, -8] + +expect + List.range { start: After 250u8, end: At 255 } == [251, 252, 253, 254, 255] + +expect + List.range { start: After 250u8, end: At 255, step: 10 } == [] + +expect + List.range { start: After 250u8, end: At 245, step: 10 } == [] + +expect + List.range { start: At 4, end: At 0 } == [4, 3, 2, 1, 0] + +## Sort with a custom comparison function +sortWith : List a, (a, a -> [LT, EQ, GT]) -> List a + +## Sorts a list in ascending order (lowest to highest), using a function which +## specifies a way to represent each element as a number. +## +## To sort in descending order (highest to lowest), use [List.sortDesc] instead. +sortAsc : List (Num a) -> List (Num a) +sortAsc = \list -> List.sortWith list Num.compare + +## Sorts a list in descending order (highest to lowest), using a function which +## specifies a way to represent each element as a number. +## +## To sort in ascending order (lowest to highest), use [List.sortAsc] instead. +sortDesc : List (Num a) -> List (Num a) +sortDesc = \list -> List.sortWith list (\a, b -> Num.compare b a) + +swap : List a, Nat, Nat -> List a + +## Returns the first element in the list, or `ListWasEmpty` if it was empty. +first : List a -> Result a [ListWasEmpty] +first = \list -> + when List.get list 0 is + Ok v -> Ok v + Err _ -> Err ListWasEmpty + +## Returns the given number of elements from the beginning of the list. +## ``` +## List.takeFirst [1, 2, 3, 4, 5, 6, 7, 8] 4 +## ``` +## If there are fewer elements in the list than the requested number, +## returns the entire list. +## ``` +## List.takeFirst [1, 2] 5 +## ``` +## To *remove* elements from the beginning of the list, use `List.takeLast`. +## +## To remove elements from both the beginning and end of the list, +## use `List.sublist`. +## +## To split the list into two lists, use `List.split`. +## +takeFirst : List elem, Nat -> List elem +takeFirst = \list, outputLength -> + List.sublist list { start: 0, len: outputLength } + +## Returns the given number of elements from the end of the list. +## ``` +## List.takeLast [1, 2, 3, 4, 5, 6, 7, 8] 4 +## ``` +## If there are fewer elements in the list than the requested number, +## returns the entire list. +## ``` +## List.takeLast [1, 2] 5 +## ``` +## To *remove* elements from the end of the list, use `List.takeFirst`. +## +## To remove elements from both the beginning and end of the list, +## use `List.sublist`. +## +## To split the list into two lists, use `List.split`. +## +takeLast : List elem, Nat -> List elem +takeLast = \list, outputLength -> + List.sublist list { start: Num.subSaturated (List.len list) outputLength, len: outputLength } + +## Drops n elements from the beginning of the list. +dropFirst : List elem, Nat -> List elem +dropFirst = \list, n -> + remaining = Num.subSaturated (List.len list) n + + List.takeLast list remaining + +## Drops n elements from the end of the list. +dropLast : List elem, Nat -> List elem +dropLast = \list, n -> + remaining = Num.subSaturated (List.len list) n + + List.takeFirst list remaining + +## Drops the element at the given index from the list. +## +## This has no effect if the given index is outside the bounds of the list. +## +## To replace the element at a given index, instead of dropping it, see [List.set]. +dropAt : List elem, Nat -> List elem + +min : List (Num a) -> Result (Num a) [ListWasEmpty] +min = \list -> + when List.first list is + Ok initial -> + Ok (minHelp list initial) + + Err ListWasEmpty -> + Err ListWasEmpty + +minHelp : List (Num a), Num a -> Num a +minHelp = \list, initial -> + List.walk list initial \bestSoFar, current -> + if current < bestSoFar then + current + else + bestSoFar + +max : List (Num a) -> Result (Num a) [ListWasEmpty] +max = \list -> + when List.first list is + Ok initial -> + Ok (maxHelp list initial) + + Err ListWasEmpty -> + Err ListWasEmpty + +maxHelp : List (Num a), Num a -> Num a +maxHelp = \list, initial -> + List.walk list initial \bestSoFar, current -> + if current > bestSoFar then + current + else + bestSoFar + +## Like [List.map], except the transformation function wraps the return value +## in a list. At the end, all the lists get joined together into one list. +## +## You may know a similar function named `concatMap` in other languages. +joinMap : List a, (a -> List b) -> List b +joinMap = \list, mapper -> + List.walk list [] \state, elem -> List.concat state (mapper elem) + +## Returns the first element of the list satisfying a predicate function. +## If no satisfying element is found, an `Err NotFound` is returned. +findFirst : List elem, (elem -> Bool) -> Result elem [NotFound] +findFirst = \list, pred -> + callback = \_, elem -> + if pred elem then + Break elem + else + Continue {} + + when List.iterate list {} callback is + Continue {} -> Err NotFound + Break found -> Ok found + +## Returns the last element of the list satisfying a predicate function. +## If no satisfying element is found, an `Err NotFound` is returned. +findLast : List elem, (elem -> Bool) -> Result elem [NotFound] +findLast = \list, pred -> + callback = \_, elem -> + if pred elem then + Break elem + else + Continue {} + + when List.iterateBackwards list {} callback is + Continue {} -> Err NotFound + Break found -> Ok found + +## Returns the index at which the first element in the list +## satisfying a predicate function can be found. +## If no satisfying element is found, an `Err NotFound` is returned. +findFirstIndex : List elem, (elem -> Bool) -> Result Nat [NotFound] +findFirstIndex = \list, matcher -> + foundIndex = List.iterate list 0 \index, elem -> + if matcher elem then + Break index + else + Continue (Num.addWrap index 1) + + when foundIndex is + Break index -> Ok index + Continue _ -> Err NotFound + +## Returns the last index at which the first element in the list +## satisfying a predicate function can be found. +## If no satisfying element is found, an `Err NotFound` is returned. +findLastIndex : List elem, (elem -> Bool) -> Result Nat [NotFound] +findLastIndex = \list, matches -> + foundIndex = List.iterateBackwards list (List.len list) \prevIndex, elem -> + answer = Num.subWrap prevIndex 1 + + if matches elem then + Break answer + else + Continue answer + + when foundIndex is + Break index -> Ok index + Continue _ -> Err NotFound + +## Returns a subsection of the given list, beginning at the `start` index and +## including a total of `len` elements. +## +## If `start` is outside the bounds of the given list, returns the empty list. +## ``` +## List.sublist [1, 2, 3] { start: 4, len: 0 } +## ``` +## If more elements are requested than exist in the list, returns as many as it can. +## ``` +## List.sublist [1, 2, 3, 4, 5] { start: 2, len: 10 } +## ``` +## > If you want a sublist which goes all the way to the end of the list, no +## > matter how long the list is, `List.takeLast` can do that more efficiently. +## +## Some languages have a function called **`slice`** which works similarly to this. +sublist : List elem, { start : Nat, len : Nat } -> List elem +sublist = \list, config -> + sublistLowlevel list config.start config.len + +## low-level slicing operation that does no bounds checking +sublistLowlevel : List elem, Nat, Nat -> List elem + +## Intersperses `sep` between the elements of `list` +## ``` +## List.intersperse [1, 2, 3] 9 # [1, 9, 2, 9, 3] +## ``` +intersperse : List elem, elem -> List elem +intersperse = \list, sep -> + capacity = 2 * List.len list + init = List.withCapacity capacity + newList = + List.walk list init \acc, elem -> + acc + |> List.appendUnsafe elem + |> List.appendUnsafe sep + + List.dropLast newList 1 + +## Returns `Bool.true` if the first list starts with the second list. +## +## If the second list is empty, this always returns `Bool.true`; every list +## is considered to "start with" an empty list. +## +## If the first list is empty, this only returns `Bool.true` if the second list is empty. +startsWith : List elem, List elem -> Bool where elem implements Eq +startsWith = \list, prefix -> + # TODO once we have seamless slices, verify that this wouldn't + # have better performance with a function like List.compareSublists + prefix == List.sublist list { start: 0, len: List.len prefix } + +## Returns `Bool.true` if the first list ends with the second list. +## +## If the second list is empty, this always returns `Bool.true`; every list +## is considered to "end with" an empty list. +## +## If the first list is empty, this only returns `Bool.true` if the second list is empty. +endsWith : List elem, List elem -> Bool where elem implements Eq +endsWith = \list, suffix -> + # TODO once we have seamless slices, verify that this wouldn't + # have better performance with a function like List.compareSublists + length = List.len suffix + start = Num.subSaturated (List.len list) length + + suffix == List.sublist list { start, len: length } + +## Splits the list into two lists, around the given index. +## +## The returned lists are labeled `before` and `others`. The `before` list will +## contain all the elements whose index in the original list was **less than** +## than the given index, # and the `others` list will be all the others. (This +## means if you give an index of 0, the `before` list will be empty and the +## `others` list will have the same elements as the original list.) +split : List elem, Nat -> { before : List elem, others : List elem } +split = \elements, userSplitIndex -> + length = List.len elements + splitIndex = if length > userSplitIndex then userSplitIndex else length + before = List.sublist elements { start: 0, len: splitIndex } + others = List.sublist elements { start: splitIndex, len: Num.subWrap length splitIndex } + + { before, others } + +## Returns the elements before the first occurrence of a delimiter, as well as the +## remaining elements after that occurrence. If the delimiter is not found, returns `Err`. +## ``` +## List.splitFirst [Foo, Z, Bar, Z, Baz] Z == Ok { before: [Foo], after: [Bar, Z, Baz] } +## ``` +splitFirst : List elem, elem -> Result { before : List elem, after : List elem } [NotFound] where elem implements Eq +splitFirst = \list, delimiter -> + when List.findFirstIndex list (\elem -> elem == delimiter) is + Ok index -> + before = List.sublist list { start: 0, len: index } + after = List.sublist list { start: Num.addWrap index 1, len: Num.subWrap (List.len list) index |> Num.subWrap 1 } + + Ok { before, after } + + Err NotFound -> Err NotFound + +## Returns the elements before the last occurrence of a delimiter, as well as the +## remaining elements after that occurrence. If the delimiter is not found, returns `Err`. +## ``` +## List.splitLast [Foo, Z, Bar, Z, Baz] Z == Ok { before: [Foo, Z, Bar], after: [Baz] } +## ``` +splitLast : List elem, elem -> Result { before : List elem, after : List elem } [NotFound] where elem implements Eq +splitLast = \list, delimiter -> + when List.findLastIndex list (\elem -> elem == delimiter) is + Ok index -> + before = List.sublist list { start: 0, len: index } + after = List.sublist list { start: Num.addWrap index 1, len: Num.subWrap (List.len list) index |> Num.subWrap 1 } + + Ok { before, after } + + Err NotFound -> Err NotFound + +## Splits the list into many chunks, each of which is length of the given chunk +## size. The last chunk will be shorter if the list does not evenly divide by the +## chunk size. If the provided list is empty or if the chunk size is 0 then the +## result is an empty list. +chunksOf : List a, Nat -> List (List a) +chunksOf = \list, chunkSize -> + if chunkSize == 0 || List.isEmpty list then + [] + else + chunkCapacity = Num.divCeil (List.len list) chunkSize + chunksOfHelp list chunkSize (List.withCapacity chunkCapacity) + +chunksOfHelp : List a, Nat, List (List a) -> List (List a) +chunksOfHelp = \listRest, chunkSize, chunks -> + if List.isEmpty listRest then + chunks + else + { before, others } = List.split listRest chunkSize + chunksOfHelp others chunkSize (List.append chunks before) + +## Like [List.map], except the transformation function returns a [Result]. +## If that function ever returns `Err`, [mapTry] immediately returns that `Err`. +## If it returns `Ok` for every element, [mapTry] returns `Ok` with the transformed list. +mapTry : List elem, (elem -> Result ok err) -> Result (List ok) err +mapTry = \list, toResult -> + walkTry list [] \state, elem -> + Result.map (toResult elem) \ok -> + List.append state ok + +## Same as [List.walk], except you can stop walking early by returning `Err`. +## +## ## Performance Details +## +## Compared to [List.walk], this can potentially visit fewer elements (which can +## improve performance) at the cost of making each step take longer. +## However, the added cost to each step is extremely small, and can easily +## be outweighed if it results in skipping even a small number of elements. +## +## As such, it is typically better for performance to use this over [List.walk] +## if returning `Break` earlier than the last element is expected to be common. +walkTry : List elem, state, (state, elem -> Result state err) -> Result state err +walkTry = \list, init, func -> + walkTryHelp list init func 0 (List.len list) + +## internal helper +walkTryHelp : List elem, state, (state, elem -> Result state err), Nat, Nat -> Result state err +walkTryHelp = \list, state, f, index, length -> + if index < length then + when f state (List.getUnsafe list index) is + Ok nextState -> walkTryHelp list nextState f (Num.addWrap index 1) length + Err b -> Err b + else + Ok state + +## Primitive for iterating over a List, being able to decide at every element whether to continue +iterate : List elem, s, (s, elem -> [Continue s, Break b]) -> [Continue s, Break b] +iterate = \list, init, func -> + iterHelp list init func 0 (List.len list) + +## internal helper +iterHelp : List elem, s, (s, elem -> [Continue s, Break b]), Nat, Nat -> [Continue s, Break b] +iterHelp = \list, state, f, index, length -> + if index < length then + when f state (List.getUnsafe list index) is + Continue nextState -> iterHelp list nextState f (Num.addWrap index 1) length + Break b -> Break b + else + Continue state + +## Primitive for iterating over a List from back to front, being able to decide at every +## element whether to continue +iterateBackwards : List elem, s, (s, elem -> [Continue s, Break b]) -> [Continue s, Break b] +iterateBackwards = \list, init, func -> + iterBackwardsHelp list init func (List.len list) + +## internal helper +iterBackwardsHelp : List elem, s, (s, elem -> [Continue s, Break b]), Nat -> [Continue s, Break b] +iterBackwardsHelp = \list, state, f, prevIndex -> + if prevIndex > 0 then + index = Num.subWrap prevIndex 1 + + when f state (List.getUnsafe list index) is + Continue nextState -> iterBackwardsHelp list nextState f index + Break b -> Break b + else + Continue state diff --git a/crates/compiler/builtins/roc/Num.roc b/crates/compiler/builtins/roc/Num.roc new file mode 100644 index 0000000000..7897de6126 --- /dev/null +++ b/crates/compiler/builtins/roc/Num.roc @@ -0,0 +1,1476 @@ +interface Num + exposes [ + Num, + Int, + Frac, + Integer, + FloatingPoint, + I128, + I64, + I32, + I16, + I8, + U128, + U64, + U32, + U16, + U8, + Signed128, + Signed64, + Signed32, + Signed16, + Signed8, + Unsigned128, + Unsigned64, + Unsigned32, + Unsigned16, + Unsigned8, + Nat, + Dec, + F64, + F32, + Natural, + Decimal, + Binary32, + Binary64, + e, + pi, + tau, + abs, + absDiff, + neg, + add, + sub, + mul, + min, + max, + isLt, + isLte, + isGt, + isGte, + sin, + cos, + tan, + atan, + acos, + asin, + isZero, + isEven, + isOdd, + toFrac, + isPositive, + isNegative, + isNaN, + isInfinite, + isFinite, + rem, + remChecked, + div, + divChecked, + sqrt, + sqrtChecked, + log, + logChecked, + round, + ceiling, + floor, + compare, + pow, + powInt, + countLeadingZeroBits, + countTrailingZeroBits, + countOneBits, + addWrap, + addChecked, + addSaturated, + bitwiseAnd, + bitwiseXor, + bitwiseOr, + bitwiseNot, + shiftLeftBy, + shiftRightBy, + shiftRightZfBy, + subWrap, + subChecked, + subSaturated, + mulWrap, + mulSaturated, + mulChecked, + intCast, + bytesToU16, + bytesToU32, + bytesToU64, + bytesToU128, + divCeil, + divCeilChecked, + divTrunc, + divTruncChecked, + toStr, + isMultipleOf, + minI8, + maxI8, + minU8, + maxU8, + minI16, + maxI16, + minU16, + maxU16, + minI32, + maxI32, + minU32, + maxU32, + minI64, + maxI64, + minU64, + maxU64, + minI128, + maxI128, + minU128, + maxU128, + minF32, + maxF32, + minF64, + maxF64, + toI8, + toI8Checked, + toI16, + toI16Checked, + toI32, + toI32Checked, + toI64, + toI64Checked, + toI128, + toI128Checked, + toU8, + toU8Checked, + toU16, + toU16Checked, + toU32, + toU32Checked, + toU64, + toU64Checked, + toU128, + toU128Checked, + toNat, + toNatChecked, + toF32, + toF32Checked, + toF64, + toF64Checked, + ] + imports [ + Bool.{ Bool }, + Result.{ Result }, + ] + +## Represents a number that could be either an [Int] or a [Frac]. +## +## This is useful for functions that can work on either, for example [Num.add], whose type is: +## ``` +## add : Num a, Num a -> Num a +## ``` +## The number 1.5 technically has the type `Num (Fraction *)`, so when you pass +## two of them to [Num.add], the answer you get is `3.0 : Num (Fraction *)`. +## +## Similarly, the number 0x1 (that is, the integer 1 in hexadecimal notation) +## technically has the type `Num (Integer *)`, so when you pass two of them to +## [Num.add], the answer you get is `2 : Num (Integer *)`. +## +## The type [`Frac a`](#Frac) is defined to be an alias for `Num (Fraction a)`, +## so `3.0 : Num (Fraction *)` is the same value as `3.0 : Frac *`. +## Similarly, the type [`Int a`](#Int) is defined to be an alias for +## `Num (Integer a)`, so `2 : Num (Integer *)` is the same value as +## `2 : Int *`. +## +## In this way, the [Num] type makes it possible to have `1 + 0x1` return +## `2 : Int *` and `1.5 + 1.5` return `3.0 : Frac`. +## +## ## Number Literals +## +## Number literals without decimal points (like `0`, `4` or `360`) +## have the type `Num *` at first, but usually end up taking on +## a more specific type based on how they're used. +## +## For example, in `(1 + List.len myList)`, the `1` has the type `Num *` at first, +## but because `List.len` returns a `Nat`, the `1` ends up changing from +## `Num *` to the more specific `Nat`, and the expression as a whole +## ends up having the type `Nat`. +## +## Sometimes number literals don't become more specific. For example, +## the `Num.toStr` function has the type `Num * -> Str`. This means that +## when calling `Num.toStr (5 + 6)`, the expression `(5 + 6)` +## still has the type `Num *`. When this happens, `Num *` defaults to +## being an [I64] - so this addition expression would overflow +## if either 5 or 6 were replaced with a number big enough to cause +## addition overflow on an [I64] value. +## +## If this default of [I64] is not big enough for your purposes, +## you can add an `i128` to the end of the number literal, like so: +## ``` +## Num.toStr 5_000_000_000i128 +## ``` +## This `i128` suffix specifies that you want this number literal to be +## an [I128] instead of a `Num *`. All the other numeric types have +## suffixes just like `i128`; here are some other examples: +## +## * `215u8` is a `215` value of type [U8] +## * `76.4f32` is a `76.4` value of type [F32] +## * `123.45dec` is a `123.45` value of type [Dec] +## * `12345nat` is a `12345` value of type [Nat] +## +## In practice, these are rarely needed. It's most common to write +## number literals without any suffix. +Num range := range + +## A fixed-size integer - that is, a number with no fractional component. +## +## Integers come in two flavors: signed and unsigned. Signed integers can be +## negative ("signed" refers to how they can incorporate a minus sign), +## whereas unsigned integers cannot be negative. +## +## Since integers have a fixed size, the size you choose determines both the +## range of numbers it can represent, and also how much memory it takes up. +## +## [U8] is an an example of an integer. It is an unsigned [Int] that takes up 8 bits +## (aka 1 byte) in memory. The `U` is for Unsigned and the 8 is for 8 bits. +## Because it has 8 bits to work with, it can store 256 numbers (2^8), +## and because it is unsigned, its min value is 0. This means the 256 numbers +## it can store range from 0 to 255. +## +## [I8] is a signed integer that takes up 8 bits. The `I` is for Integer, since +## integers in mathematics are signed by default. Because it has 8 bits just +## like [U8], it can store 256 numbers (still 2^8), but because it is signed, +## the range is different. Its 256 numbers range from -128 to 127. +## +## Here are some other examples: +## +## * [U16] is like [U8], except it takes up 16 bits in memory. It can store 65,536 numbers (2^16), ranging from 0 to 65,536. +## * [I16] is like [U16], except it is signed. It can still store the same 65,536 numbers (2^16), ranging from -32,768 to 32,767. +## +## This pattern continues up to [U128] and [I128]. +## +## ## Performance Details +## +## In general, using smaller numeric sizes means your program will use less memory. +## However, if a mathematical operation results in an answer that is too big +## or too small to fit in the size available for that answer (which is typically +## the same size as the inputs), then you'll get an overflow error. +## +## As such, minimizing memory usage without causing overflows involves choosing +## number sizes based on your knowledge of what numbers you expect your program +## to encounter at runtime. +## +## Minimizing memory usage does not imply maximum runtime speed! +## CPUs are typically fastest at performing integer operations on integers that +## are the same size as that CPU's native machine word size. That means a 64-bit +## CPU is typically fastest at executing instructions on [U64] and [I64] values, +## whereas a 32-bit CPU is typically fastest on [U32] and [I32] values. +## +## Putting these factors together, here are some reasonable guidelines for optimizing performance through integer size choice: +## +## * Start by deciding if this integer should allow negative numbers, and choose signed or unsigned accordingly. +## * Next, think about the range of numbers you expect this number to hold. Choose the smallest size you will never expect to overflow, no matter the inputs your program receives. (Validating inputs for size, and presenting the user with an error if they are too big, can help guard against overflow.) +## * Finally, if a particular numeric calculation is running too slowly, you can try experimenting with other number sizes. This rarely makes a meaningful difference, but some processors can operate on different number sizes at different speeds. +## +## All number literals without decimal points are compatible with [Int] values. +## +## You can optionally put underscores in your [Int] literals. +## They have no effect on the number's value, but can make large numbers easier to read. +## ``` +## 1_000_000 +## ``` +## Integers come in two flavors: *signed* and *unsigned*. +## +## * *Unsigned* integers can never be negative. The lowest value they can hold is zero. +## * *Signed* integers can be negative. +## +## Integers also come in different sizes. Choosing a size depends on your performance +## needs and the range of numbers you need to represent. At a high level, the +## general trade-offs are: +## +## * Larger integer sizes can represent a wider range of numbers. If you absolutely need to represent numbers in a certain range, make sure to pick an integer size that can hold them! +## * Smaller integer sizes take up less memory. These savings rarely matter in variables and function arguments, but the sizes of integers that you use in data structures can add up. This can also affect whether those data structures fit in [cache lines](https://en.wikipedia.org/wiki/CPU_cache#Cache_performance), which can be a performance bottleneck. +## * Certain CPUs work faster on some numeric sizes than others. If the CPU is taking too long to run numeric calculations, you may find a performance improvement by experimenting with numeric sizes that are larger than otherwise necessary. However, in practice, doing this typically degrades overall performance, so be careful to measure properly! +## +## Here are the different fixed size integer types: +## +## | Range | Type | Size | +## |--------------------------------------------------------|-------|----------| +## | ` -128` | [I8] | 1 Byte | +## | ` 127` | | | +## |--------------------------------------------------------|-------|----------| +## | ` 0` | [U8] | 1 Byte | +## | ` 255` | | | +## |--------------------------------------------------------|-------|----------| +## | ` -32_768` | [I16] | 2 Bytes | +## | ` 32_767` | | | +## |--------------------------------------------------------|-------|----------| +## | ` 0` | [U16] | 2 Bytes | +## | ` 65_535` | | | +## |--------------------------------------------------------|-------|----------| +## | ` -2_147_483_648` | [I32] | 4 Bytes | +## | ` 2_147_483_647` | | | +## |--------------------------------------------------------|-------|----------| +## | ` 0` | [U32] | 4 Bytes | +## | ` (over 4 billion) 4_294_967_295` | | | +## |--------------------------------------------------------|-------|----------| +## | ` -9_223_372_036_854_775_808` | [I64] | 8 Bytes | +## | ` 9_223_372_036_854_775_807` | | | +## |--------------------------------------------------------|-------|----------| +## | ` 0` | [U64] | 8 Bytes | +## | ` (over 18 quintillion) 18_446_744_073_709_551_615` | | | +## |--------------------------------------------------------|-------|----------| +## | `-170_141_183_460_469_231_731_687_303_715_884_105_728` | [I128]| 16 Bytes | +## | ` 170_141_183_460_469_231_731_687_303_715_884_105_727` | | | +## |--------------------------------------------------------|-------|----------| +## | ` (over 340 undecillion) 0` | [U128]| 16 Bytes | +## | ` 340_282_366_920_938_463_463_374_607_431_768_211_455` | | | +## +## Roc also has one variable-size integer type: [Nat]. The size of [Nat] is equal +## to the size of a memory address, which varies by system. For example, when +## compiling for a 64-bit system, [Nat] is the same as [U64]. When compiling for a +## 32-bit system, it's the same as [U32]. +## +## A common use for [Nat] is to store the length ("len" for short) of a +## collection like a [List]. 64-bit systems can represent longer +## lists in memory than 32-bit systems, which is why the length of a list +## is represented as a [Nat] in Roc. +## +## If any operation would result in an [Int] that is either too big +## or too small to fit in that range (e.g. calling `Num.maxI32 + 1`), +## then the operation will *overflow*. When an overflow occurs, the program will crash. +## +## As such, it's very important to design your code not to exceed these bounds! +## If you need to do math outside these bounds, consider using a larger numeric size. +Int range : Num (Integer range) + +## A fixed-size number with a fractional component. +## +## Roc fractions come in two flavors: fixed-point base-10 and floating-point base-2. +## +## * [Dec] is a 128-bit [fixed-point](https://en.wikipedia.org/wiki/Fixed-point_arithmetic) base-10 number. It's a great default choice, especially when precision is important - for example when representing currency. With [Dec], `0.1 + 0.2` returns `0.3`. [Dec] has 18 decimal places of precision and a range from `-170_141_183_460_469_231_731.687303715884105728` to `170_141_183_460_469_231_731.687303715884105727`. +## * [F64] and [F32] are [floating-point](https://en.wikipedia.org/wiki/Floating-point_arithmetic) base-2 numbers. They sacrifice precision for lower memory usage and improved performance on some operations. This makes them a good fit for representing graphical coordinates. With [F64], `0.1 + 0.2` returns `0.30000000000000004`. +## +## If you don't specify a type, Roc will default to using [Dec] because it's +## the least error-prone overall. For example, suppose you write this: +## ``` +## wasItPrecise = 0.1 + 0.2 == 0.3 +## ``` +## The value of `wasItPrecise` here will be `Bool.true`, because Roc uses [Dec] +## by default when there are no types specified. +## +## In contrast, suppose we use `f32` or `f64` for one of these numbers: +## ``` +## wasItPrecise = 0.1f64 + 0.2 == 0.3 +## ``` +## Here, `wasItPrecise` will be `Bool.false` because the entire calculation will have +## been done in a base-2 floating point calculation, which causes noticeable +## precision loss in this case. +## +## The floating-point numbers ([F32] and [F64]) also have three values which +## are not ordinary [finite numbers](https://en.wikipedia.org/wiki/Finite_number). +## They are: +## * ∞ ([infinity](https://en.wikipedia.org/wiki/Infinity)) +## * -∞ (negative infinity) +## * *NaN* ([not a number](https://en.wikipedia.org/wiki/NaN)) +## +## These values are different from ordinary numbers in that they only occur +## when a floating-point calculation encounters an error. For example: +## * Dividing a positive [F64] by `0.0` returns ∞. +## * Dividing a negative [F64] by `0.0` returns -∞. +## * Dividing a [F64] of `0.0` by `0.0` returns [*NaN*](Num.isNaN). +## +## These rules come from the [IEEE-754](https://en.wikipedia.org/wiki/IEEE_754) +## floating point standard. Because almost all modern processors are built to +## this standard, deviating from these rules has a significant performance +## cost! Since the most common reason to choose [F64] or [F32] over [Dec] is +## access to hardware-accelerated performance, Roc follows these rules exactly. +## +## There's no literal syntax for these error values, but you can check to see if +## you ended up with one of them by using #isNaN, #isFinite, and #isInfinite. +## Whenever a function in this module could return one of these values, that +## possibility is noted in the function's documentation. +## +## ## Performance Details +## +## On typical modern CPUs, performance is similar between [Dec], [F64], and [F32] +## for addition and subtraction. For example, [F32] and [F64] do addition using +## a single CPU floating-point addition instruction, which typically takes a +## few clock cycles to complete. In contrast, [Dec] does addition using a few +## CPU integer arithmetic instructions, each of which typically takes only one +## clock cycle to complete. Exact numbers will vary by CPU, but they should be +## similar overall. +## +## [Dec] is significantly slower for multiplication and division. It not only +## needs to do more arithmetic instructions than [F32] and [F64] do, but also +## those instructions typically take more clock cycles to complete. +## +## With [Num.sqrt] and trigonometry functions like [Num.cos], there is +## an even bigger performance difference. [F32] and [F64] can do these in a +## single instruction, whereas [Dec] needs entire custom procedures - which use +## loops and conditionals. If you need to do performance-critical trigonometry +## or square roots, either [F64] or [F32] is probably a better choice than the +## usual default choice of [Dec], despite the precision problems they bring. +Frac range : Num (FloatingPoint range) + +Signed128 := [] +Signed64 := [] +Signed32 := [] +Signed16 := [] +Signed8 := [] + +Unsigned128 := [] +Unsigned64 := [] +Unsigned32 := [] +Unsigned16 := [] +Unsigned8 := [] + +Natural := [] + +Integer range := range + +I128 : Num (Integer Signed128) +I64 : Num (Integer Signed64) +I32 : Num (Integer Signed32) +I16 : Num (Integer Signed16) + +## A signed 8-bit integer, ranging from -128 to 127 +I8 : Int Signed8 + +U128 : Num (Integer Unsigned128) +U64 : Num (Integer Unsigned64) +U32 : Num (Integer Unsigned32) +U16 : Num (Integer Unsigned16) +U8 : Num (Integer Unsigned8) + +## A [natural number](https://en.wikipedia.org/wiki/Natural_number) represented +## as a 64-bit unsigned integer on 64-bit systems, a 32-bit unsigned integer +## on 32-bit systems, and so on. +## +## This system-specific size makes it useful for certain data structure +## functions like [List.len], because the number of elements many data structures +## can hold is also system-specific. For example, the maximum number of elements +## a [List] can hold on a 64-bit system fits in a 64-bit unsigned integer, and +## on a 32-bit system it fits in 32-bit unsigned integer. This makes [Nat] a +## good fit for [List.len] regardless of system. +Nat : Num (Integer Natural) + +Decimal := [] +Binary64 := [] +Binary32 := [] + +FloatingPoint range := range + +## A 64-bit [IEEE 754 binary floating-point number](https://en.wikipedia.org/wiki/IEEE_754). +## +## [F64] represents decimal numbers less precisely than [Dec] does, but operations on it +## can be faster because CPUs have hardware-level support for [F64] but not [Dec]. There +## are other tradeoffs between the two, such as: +## * [Dec] has a fixed number of digits it can represent before the decimal point, and a fixed number it can represent after the decimal point. In contrast, [F64]'s decimal point can "float"—which conceptually means if you don't need many digits before the decimal point, you can get more digits of precision afterwards (and vice versa). +## * [Dec] represents its number internally in [base-10](https://en.wikipedia.org/wiki/Decimal), whereas [F64] uses [base-2](https://en.wikipedia.org/wiki/Binary_number). This can lead to imprecise answers like `0.1 + 0.2` returning `0.3` for [Dec] and `0.30000000000000004` for [F64]. This is not a bug; rather, it's a consequence of [F64]'s base-2 representation. +## * [Dec] always gives a precise answer (or an error), whereas [F64] can lose precision. For example, increasing a very large [F64] number (using addition, perhaps) can result in the whole number portion being incorrect. `1234567890123456789 + 100` correctly results in a number ending in `889` for `Dec`, but results in a number ending `800` in [F64] due to precision loss. +F64 : Num (FloatingPoint Binary64) + +## A 32-bit [IEEE 754 binary floating-point number](https://en.wikipedia.org/wiki/IEEE_754). +## +## This works just like [F64] (see its docs for a comparison with [Dec]) except it's smaller. +## That in turn means it takes up less memory, but can store smaller numbers (and becomes imprecise +## more easily than [F64] does). +F32 : Num (FloatingPoint Binary32) + +## A [decimal](https://en.wikipedia.org/wiki/Decimal) number. +## +## [Dec] is a more precise way to represent decimal numbers (like currency) than [F32] and [F64] +## are, because [Dec] is represented in memory as base-10. In contrast, [F64] and [F32] +## are [base-2](https://en.wikipedia.org/wiki/Binary_number) in memory, which can lead to decimal +## precision loss even when doing addition and subtraction. For example, when +## using [F64], `0.1 + 0.2` returns 0.30000000000000004, +## whereas when using [Dec], `0.1 + 0.2` returns 0.3. +## +## Under the hood, a [Dec] is an [I128], and operations on it perform +## [base-10 fixed-point arithmetic](https://en.wikipedia.org/wiki/Fixed-point_arithmetic) +## with 18 decimal places of precision. +## +## This means a [Dec] can represent whole numbers up to slightly over 170 +## quintillion, along with 18 decimal places. (To be precise, it can store +## numbers between `-170_141_183_460_469_231_731.687303715884105728` +## and `170_141_183_460_469_231_731.687303715884105727`.) Why 18 +## decimal places? It's the highest number of decimal places where you can +## still convert any [U64] to a [Dec] without losing information. +## +## There are some use cases where [F64] and [F32] can be better choices than [Dec] +## despite their issues with base-10 numbers. For example, in graphical applications +## they can be a better choice for representing coordinates because they take up +## less memory, certain relevant calculations run faster (see performance +## details, below), and base-10 generally isn't as big a concern when +## dealing with screen coordinates as it is when dealing with currency. +## +## Another scenario where [F64] can be a better choice than [Dec] is when representing +## extremely small numbers. The smallest positive [F64] that can be represented without precision +## loss is 2^(−1074), which is about 5 * 10^(-324). Here is that number next to the smallest +## [Dec] that can be represented: +## +## * Smallest [F64]: 0.000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005 +## * Smallest [Dec]: 0.000000000000000001 +## +## This is because [floating-point](https://en.wikipedia.org/wiki/Floating-point_arithmetic) numbers +## like [F64] can gain more digits of precision after the `.` when they aren't using as many digits +## before the `.` - and this applies the most if the digit before `.` is `0`. So whereas [Dec] always +## has 18 digits after the `.`, the number of digits after the `.` that [F32] can [F64] can represent +## without precision loss depends on what comes before it. +## +## ## Performance Details +## +## CPUs have dedicated instructions for many [F32] and [F64] operations, but none for [Dec]. +## Internally, [Dec] is represented as a 128-bit integer and uses multiple instructions to +## perform fractional operations. This gives [F32] and [F64] performance advantages +## for many operations. +## +## Here's a comparison of about how long [Dec] takes to perform a given operation compared to [F64], +## based on benchmarks on an [M1](https://en.wikipedia.org/wiki/Apple_M1) CPU: +## * [add] 0.6x +## * [sub] 0.6x +## * [mul] 15x +## * [div] 55x +## * [sin] 3.9x +## * [cos] 3.6x +## * [tan] 2.3x +## * [asin] 1.8x +## * [acos] 1.7x +## * [atan] 1.7x +## +## Keep in mind that arithmetic instructions are basically [the fastest thing a CPU does](http://norvig.com/21-days.html#answers), +## so (for example) a network request that takes 10 milliseconds to complete would go on this +## list as about 10000000x. So these performance differences might be more or less noticeable than +## the base-10 representation differences depending on the use case. +Dec : Num (FloatingPoint Decimal) + +## Euler's number (e) +e : Frac * +e = 2.71828182845904523536028747135266249775724709369995 + +## Archimedes' constant (π) +pi : Frac * +pi = 3.14159265358979323846264338327950288419716939937510 + +## Circle constant (τ) +tau : Frac * +tau = 2 * pi + +# ------- Functions +## Convert a number to a [Str]. +## +## This is the same as calling `Num.format {}` - so for more details on +## exact formatting, see `Num.format`. +## ``` +## Num.toStr 42 +## ``` +## Only [Frac] values will include a decimal point, and they will always include one. +## ``` +## Num.toStr 4.2 +## Num.toStr 4.0 +## ``` +## When this function is given a non-[finite](Num.isFinite) +## [F64] or [F32] value, the returned string will be `"NaN"`, `"∞"`, or `"-∞"`. +## +## To get strings in hexadecimal, octal, or binary format, use `Num.format`. +toStr : Num * -> Str +intCast : Int a -> Int b + +bytesToU16Lowlevel : List U8, Nat -> U16 +bytesToU32Lowlevel : List U8, Nat -> U32 +bytesToU64Lowlevel : List U8, Nat -> U64 +bytesToU128Lowlevel : List U8, Nat -> U128 + +bytesToU16 : List U8, Nat -> Result U16 [OutOfBounds] +bytesToU16 = \bytes, index -> + # we need at least 1 more byte + offset = 1 + + if Num.addSaturated index offset < List.len bytes then + Ok (bytesToU16Lowlevel bytes index) + else + Err OutOfBounds + +bytesToU32 : List U8, Nat -> Result U32 [OutOfBounds] +bytesToU32 = \bytes, index -> + # we need at least 3 more bytes + offset = 3 + + if Num.addSaturated index offset < List.len bytes then + Ok (bytesToU32Lowlevel bytes index) + else + Err OutOfBounds + +bytesToU64 : List U8, Nat -> Result U64 [OutOfBounds] +bytesToU64 = \bytes, index -> + # we need at least 7 more bytes + offset = 7 + + if Num.addSaturated index offset < List.len bytes then + Ok (bytesToU64Lowlevel bytes index) + else + Err OutOfBounds + +bytesToU128 : List U8, Nat -> Result U128 [OutOfBounds] +bytesToU128 = \bytes, index -> + # we need at least 15 more bytes + offset = 15 + + if Num.addSaturated index offset < List.len bytes then + Ok (bytesToU128Lowlevel bytes index) + else + Err OutOfBounds + +compare : Num a, Num a -> [LT, EQ, GT] + +## Returns `Bool.true` if the first number is less than the second. +## +## `a < b` is shorthand for `Num.isLt a b`. +## +## If either argument is [*NaN*](Num.isNaN), returns `Bool.false` no matter what. (*NaN* +## is [defined to be unordered](https://en.wikipedia.org/wiki/NaN#Comparison_with_NaN).) +## ``` +## 5 +## |> Num.isLt 6 +## ``` +isLt : Num a, Num a -> Bool + +## Returns `Bool.true` if the first number is greater than the second. +## +## `a > b` is shorthand for `Num.isGt a b`. +## +## If either argument is [*NaN*](Num.isNaN), returns `Bool.false` no matter what. (*NaN* +## is [defined to be unordered](https://en.wikipedia.org/wiki/NaN#Comparison_with_NaN).) +## ``` +## 6 +## |> Num.isGt 5 +## ``` +isGt : Num a, Num a -> Bool + +## Returns `Bool.true` if the first number is less than or equal to the second. +## +## `a <= b` is shorthand for `Num.isLte a b`. +## +## If either argument is [*NaN*](Num.isNaN), returns `Bool.false` no matter what. (*NaN* +## is [defined to be unordered](https://en.wikipedia.org/wiki/NaN#Comparison_with_NaN).) +isLte : Num a, Num a -> Bool + +## Returns `Bool.true` if the first number is greater than or equal to the second. +## +## `a >= b` is shorthand for `Num.isGte a b`. +## +## If either argument is [*NaN*](Num.isNaN), returns `Bool.false` no matter what. (*NaN* +## is [defined to be unordered](https://en.wikipedia.org/wiki/NaN#Comparison_with_NaN).) +isGte : Num a, Num a -> Bool + +## Returns `Bool.true` if the number is `0`, and `Bool.false` otherwise. +isZero : Num a -> Bool + +## A number is even if dividing it by 2 gives a remainder of 0. +## +## Examples of even numbers: 0, 2, 4, 6, 8, -2, -4, -6, -8 +isEven : Int a -> Bool +isEven = \x -> Num.isMultipleOf x 2 + +## A number is odd if dividing it by 2 gives a remainder of 1. +## +## Examples of odd numbers: 1, 3, 5, 7, -1, -3, -5, -7 +isOdd : Int a -> Bool +isOdd = \x -> Bool.not (Num.isMultipleOf x 2) + +## Positive numbers are greater than `0`. +isPositive : Num a -> Bool +isPositive = \x -> x > 0 + +## Negative numbers are less than `0`. +isNegative : Num a -> Bool +isNegative = \x -> x < 0 + +toFrac : Num * -> Frac * + +## Returns `Bool.true` if the [Frac] is not a number as defined by [IEEE-754](https://en.wikipedia.org/wiki/IEEE_754) +## +## ``` +## Num.isNaN (0 / 0) +## ``` +isNaN : Frac * -> Bool + +## Returns `Bool.true` if the [Frac] is positive or negative infinity as defined by [IEEE-754](https://en.wikipedia.org/wiki/IEEE_754) +## +## ``` +## Num.isInfinite (1 / 0) +## +## Num.isInfinite (-1 / 0) +## ``` +isInfinite : Frac * -> Bool + +## Returns `Bool.true` if the [Frac] is not an infinity as defined by [IEEE-754](https://en.wikipedia.org/wiki/IEEE_754) +## +## ``` +## Num.isFinite 42 +## ``` +isFinite : Frac * -> Bool + +## Returns the absolute value of the number. +## +## * For a positive number, returns the same number. +## * For a negative number, returns the same number except positive. +## * For zero, returns zero. +## ``` +## Num.abs 4 +## +## Num.abs -2.5 +## +## Num.abs 0 +## +## Num.abs 0.0 +## ``` +## This is safe to use with any [Frac], but it can cause overflow when used with certain [Int] values. +## +## For example, calling #Num.abs on the lowest value of a signed integer (such as [Num.minI64] or [Num.minI32]) will cause overflow. +## This is because, for any given size of signed integer (32-bit, 64-bit, etc.) its negated lowest value turns out to be 1 higher than +## the highest value it can represent. (For this reason, calling [Num.neg] on the lowest signed value will also cause overflow.) +## +## Calling this on an unsigned integer (like [U32] or [U64]) never does anything. +abs : Num a -> Num a + +## Returns the absolute difference between two numbers. +## +## ``` +## Num.absDiff 5 3 +## +## Num.absDiff -3 5 +## +## Num.absDiff 3.0 5.0 +## ``` +## +## If the answer to this operation can't fit in the return value (e.g. an +## [I8] answer that's higher than 127 or lower than -128), the result is an +## *overflow*. For [F64] and [F32], overflow results in an answer of either +## ∞ or -∞. For all other number types, overflow results in a panic. +absDiff : Num a, Num a -> Num a +absDiff = \a, b -> + if a > b then + a - b + else + b - a + +## Returns a negative number when given a positive one, and vice versa. +## ``` +## Num.neg 5 +## +## Num.neg -2.5 +## +## Num.neg 0 +## +## Num.neg 0.0 +## ``` +## This is safe to use with any [Frac], but it can cause overflow when used with certain [Int] values. +## +## For example, calling #Num.neg on the lowest value of a signed integer (such as [Num.minI64] or [Num.minI32]) will cause overflow. +## This is because, for any given size of signed integer (32-bit, 64-bit, etc.) its negated lowest value turns out to be 1 higher than +## the highest value it can represent. (For this reason, calling #Num.abs on the lowest signed value will also cause overflow.) +## +## Additionally, calling #Num.neg on any unsigned integer (such as any [U64] or [U32] value) other than zero will cause overflow. +## +## (It will never crash when given a [Frac], however, because of how floating point numbers represent positive and negative numbers.) +neg : Num a -> Num a + +## Adds two numbers of the same type. +## +## (To add an [Int] and a [Frac], first convert one so that they both have the same type. There are functions in this module that can convert both [Int] to [Frac] and the other way around.) +## +## `a + b` is shorthand for `Num.add a b`. +## ``` +## 5 + 7 +## +## Num.add 5 7 +## ``` +## `Num.add` can be convenient in pipelines. +## ``` +## Frac.pi +## |> Num.add 1.0 +## ``` +## If the answer to this operation can't fit in the return value (e.g. an +## [I8] answer that's higher than 127 or lower than -128), the result is an +## *overflow*. For [F64] and [F32], overflow results in an answer of either +## ∞ or -∞. For all other number types, overflow results in a panic. +add : Num a, Num a -> Num a + +## Subtracts two numbers of the same type. +## +## (To subtract an [Int] and a [Frac], first convert one so that they both have the same type. There are functions in this module that can convert both [Int] to [Frac] and the other way around.) +## +## `a - b` is shorthand for `Num.sub a b`. +## ``` +## 7 - 5 +## +## Num.sub 7 5 +## ``` +## `Num.sub` can be convenient in pipelines. +## ``` +## Frac.pi +## |> Num.sub 2.0 +## ``` +## If the answer to this operation can't fit in the return value (e.g. an +## [I8] answer that's higher than 127 or lower than -128), the result is an +## *overflow*. For [F64] and [F32], overflow results in an answer of either +## ∞ or -∞. For all other number types, overflow results in a panic. +sub : Num a, Num a -> Num a + +## Multiplies two numbers of the same type. +## +## (To multiply an [Int] and a [Frac], first convert one so that they both have the same type. There are functions in this module that can convert both [Int] to [Frac] and the other way around.) +## +## `a * b` is shorthand for `Num.mul a b`. +## ``` +## 5 * 7 +## +## Num.mul 5 7 +## ``` +## +## `Num.mul` can be convenient in pipelines. +## +## ``` +## Frac.pi +## |> Num.mul 2.0 +## ``` +## If the answer to this operation can't fit in the return value (e.g. an +## [I8] answer that's higher than 127 or lower than -128), the result is an +## *overflow*. For [F64] and [F32], overflow results in an answer of either +## ∞ or -∞. For all other number types, overflow results in a panic. +mul : Num a, Num a -> Num a + +## Obtains the smaller between two numbers of the same type. +## +## ``` +## Num.min 100 0 +## +## Num.min 3.0 -3.0 +## ``` +min : Num a, Num a -> Num a +min = \a, b -> + if a < b then + a + else + b + +## Obtains the greater between two numbers of the same type. +## +## ``` +## Num.max 100 0 +## +## Num.max 3.0 -3.0 +## ``` +max : Num a, Num a -> Num a +max = \a, b -> + if a > b then + a + else + b + +sin : Frac a -> Frac a +cos : Frac a -> Frac a +tan : Frac a -> Frac a + +asin : Frac a -> Frac a +acos : Frac a -> Frac a +atan : Frac a -> Frac a + +## Returns an approximation of the absolute value of a [Frac]'s square root. +## +## The square root of a negative number is an irrational number, and [Frac] only +## supports rational numbers. As such, you should make sure never to pass this +## function a negative number! Calling [sqrt] on a negative [Dec] will cause a panic. +## +## Calling [sqrt] on [F32] and [F64] values follows these rules: +## * Passing a negative [F64] or [F32] returns [*NaN*](Num.isNaN). +## * Passing [*NaN*](Num.isNaN) or -∞ also returns [*NaN*](Num.isNaN). +## * Passing ∞ returns ∞. +## +## > These rules come from the [IEEE-754](https://en.wikipedia.org/wiki/IEEE_754) +## > floating point standard. Because almost all modern processors are built to +## > this standard, deviating from these rules has a significant performance +## > cost! Since the most common reason to choose [F64] or [F32] over [Dec] is +## > access to hardware-accelerated performance, Roc follows these rules exactly. +## ``` +## Num.sqrt 4.0 +## +## Num.sqrt 1.5 +## +## Num.sqrt 0.0 +## +## Num.sqrt -4.0f64 +## ``` +sqrt : Frac a -> Frac a + +sqrtChecked : Frac a -> Result (Frac a) [SqrtOfNegative] +sqrtChecked = \x -> + if x < 0.0 then + Err SqrtOfNegative + else + Ok (Num.sqrt x) + +## Natural logarithm +log : Frac a -> Frac a + +logChecked : Frac a -> Result (Frac a) [LogNeedsPositive] +logChecked = \x -> + if x <= 0.0 then + Err LogNeedsPositive + else + Ok (Num.log x) + +## Divides one [Frac] by another. +## +## `a / b` is shorthand for `Num.div a b`. +## +## [Division by zero is undefined in mathematics](https://en.wikipedia.org/wiki/Division_by_zero). +## As such, you should make sure never to pass zero as the denominator to this function! +## Calling [div] on a [Dec] denominator of zero will cause a panic. +## +## Calling [div] on [F32] and [F64] values follows these rules: +## * Dividing a positive [F64] or [F32] by zero returns ∞. +## * Dividing a negative [F64] or [F32] by zero returns -∞. +## * Dividing a zero [F64] or [F32] by zero returns [*NaN*](Num.isNaN). +## +## > These rules come from the [IEEE-754](https://en.wikipedia.org/wiki/IEEE_754) +## > floating point standard. Because almost all modern processors are built to +## > this standard, deviating from these rules has a significant performance +## > cost! Since the most common reason to choose [F64] or [F32] over [Dec] is +## > access to hardware-accelerated performance, Roc follows these rules exactly. +## +## To divide an [Int] and a [Frac], first convert the [Int] to a [Frac] using +## one of the functions in this module like #toDec. +## ``` +## 5.0 / 7.0 +## +## Num.div 5 7 +## ``` +## `Num.div` can be convenient in pipelines. +## ``` +## Num.pi +## |> Num.div 2.0 +## ``` +div : Frac a, Frac a -> Frac a + +divChecked : Frac a, Frac a -> Result (Frac a) [DivByZero] +divChecked = \a, b -> + if Num.isZero b then + Err DivByZero + else + Ok (Num.div a b) + +divCeil : Int a, Int a -> Int a + +divCeilChecked : Int a, Int a -> Result (Int a) [DivByZero] +divCeilChecked = \a, b -> + if Num.isZero b then + Err DivByZero + else + Ok (Num.divCeil a b) + +## Divides two integers, truncating the result towards zero. +## +## `a // b` is shorthand for `Num.divTrunc a b`. +## +## Division by zero is undefined in mathematics. As such, you should make +## sure never to pass zero as the denominator to this function! If you do, +## it will crash. +## ``` +## 5 // 7 +## +## Num.divTrunc 5 7 +## +## 8 // -3 +## +## Num.divTrunc 8 -3 +## ``` +divTrunc : Int a, Int a -> Int a + +divTruncChecked : Int a, Int a -> Result (Int a) [DivByZero] +divTruncChecked = \a, b -> + if Num.isZero b then + Err DivByZero + else + Ok (Num.divTrunc a b) + +## Obtains the remainder (truncating modulo) from the division of two integers. +## +## `a % b` is shorthand for `Num.rem a b`. +## ``` +## 5 % 7 +## +## Num.rem 5 7 +## +## -8 % -3 +## +## Num.rem -8 -3 +## ``` +rem : Int a, Int a -> Int a + +remChecked : Int a, Int a -> Result (Int a) [DivByZero] +remChecked = \a, b -> + if Num.isZero b then + Err DivByZero + else + Ok (Num.rem a b) + +isMultipleOf : Int a, Int a -> Bool + +## Does a "bitwise and". Each bit of the output is 1 if the corresponding bit +## of x AND of y is 1, otherwise it's 0. +bitwiseAnd : Int a, Int a -> Int a + +## Does a "bitwise exclusive or". Each bit of the output is the same as the +## corresponding bit in x if that bit in y is 0, and it's the complement of +## the bit in x if that bit in y is 1. +bitwiseXor : Int a, Int a -> Int a + +## Does a "bitwise or". Each bit of the output is 0 if the corresponding bit +## of x OR of y is 0, otherwise it's 1. +bitwiseOr : Int a, Int a -> Int a + +## Returns the complement of x - the number you get by switching each 1 for a +## 0 and each 0 for a 1. This is the same as -x - 1. +bitwiseNot : Int a -> Int a +bitwiseNot = \n -> + bitwiseXor n (subWrap 0 1) + +## Bitwise left shift of a number by another +## +## The least significant bits always become 0. This means that shifting left is +## like multiplying by factors of two for unsigned integers. +## ``` +## shiftLeftBy 0b0000_0011 2 == 0b0000_1100 +## +## 0b0000_0101 |> shiftLeftBy 2 == 0b0000_1100 +## ``` +## In some languages `shiftLeftBy` is implemented as a binary operator `<<`. +shiftLeftBy : Int a, U8 -> Int a + +## Bitwise arithmetic shift of a number by another +## +## The most significant bits are copied from the current. +## ``` +## shiftRightBy 0b0000_0011 2 == 0b0000_1100 +## +## 0b0001_0100 |> shiftRightBy 2 == 0b0000_0101 +## +## 0b1001_0000 |> shiftRightBy 2 == 0b1110_0100 +## ``` +## In some languages `shiftRightBy` is implemented as a binary operator `>>>`. +shiftRightBy : Int a, U8 -> Int a + +## Bitwise logical right shift of a number by another +## +## The most significant bits always become 0. This means that shifting left is +## like dividing by factors of two for unsigned integers. +## ``` +## shiftRightBy 0b0010_1000 2 == 0b0000_1010 +## +## 0b0010_1000 |> shiftRightBy 2 == 0b0000_1010 +## +## 0b1001_0000 |> shiftRightBy 2 == 0b0010_0100 +## ``` +## In some languages `shiftRightBy` is implemented as a binary operator `>>`. +shiftRightZfBy : Int a, U8 -> Int a + +## Round off the given fraction to the nearest integer. +round : Frac * -> Int * +floor : Frac * -> Int * +ceiling : Frac * -> Int * + +## Raises a [Frac] to the power of another [Frac]. +## +## For an [Int] alternative to this function, see [Num.powInt] +pow : Frac a, Frac a -> Frac a + +## Raises an integer to the power of another, by multiplying the integer by +## itself the given number of times. +## +## This process is known as [exponentiation by squaring](https://en.wikipedia.org/wiki/Exponentiation_by_squaring). +## +## For a [Frac] alternative to this function, which supports negative exponents, +## see #Num.pow. +## +## ## Warning +## +## It is very easy for this function to produce an answer +## so large it causes an overflow. +powInt : Int a, Int a -> Int a + +## Counts the number of most-significant (leading in a big-Endian sense) zeroes in an integer. +## +## ``` +## Num.countLeadingZeroBits 0b0001_1100u8 +## +## 3 +## +## Num.countLeadingZeroBits 0b0000_0000u8 +## +## 8 +## ``` +countLeadingZeroBits : Int a -> Nat + +## Counts the number of least-significant (trailing in a big-Endian sense) zeroes in an integer. +## +## ``` +## Num.countTrailingZeroBits 0b0001_1100u8 +## +## 2 +## +## Num.countTrailingZeroBits 0b0000_0000u8 +## +## 8 +## ``` +countTrailingZeroBits : Int a -> Nat + +## Counts the number of set bits in an integer. +## +## ``` +## Num.countOneBits 0b0001_1100u8 +## +## 3 +## +## Num.countOneBits 0b0000_0000u8 +## +## 0 +## ``` +countOneBits : Int a -> Nat + +addWrap : Int range, Int range -> Int range + +## Adds two numbers, clamping on the maximum representable number rather than +## overflowing. +## +## This is the same as [Num.add] except for the saturating behavior if the +## addition is to overflow. +## For example, if `x : U8` is 200 and `y : U8` is 100, `addSaturated x y` will +## yield 255, the maximum value of a `U8`. +addSaturated : Num a, Num a -> Num a + +## Adds two numbers and checks for overflow. +## +## This is the same as [Num.add] except if the operation overflows, instead of +## panicking or returning ∞ or -∞, it will return `Err Overflow`. +addChecked : Num a, Num a -> Result (Num a) [Overflow] +addChecked = \a, b -> + result = addCheckedLowlevel a b + + if result.b then + Err Overflow + else + Ok result.a + +addCheckedLowlevel : Num a, Num a -> { b : Bool, a : Num a } + +subWrap : Int range, Int range -> Int range + +## Subtracts two numbers, clamping on the minimum representable number rather +## than overflowing. +## +## This is the same as [Num.sub] except for the saturating behavior if the +## subtraction is to overflow. +## For example, if `x : U8` is 10 and `y : U8` is 20, `subSaturated x y` will +## yield 0, the minimum value of a `U8`. +subSaturated : Num a, Num a -> Num a + +## Subtracts two numbers and checks for overflow. +## +## This is the same as [Num.sub] except if the operation overflows, instead of +## panicking or returning ∞ or -∞, it will return `Err Overflow`. +subChecked : Num a, Num a -> Result (Num a) [Overflow] +subChecked = \a, b -> + result = subCheckedLowlevel a b + + if result.b then + Err Overflow + else + Ok result.a + +subCheckedLowlevel : Num a, Num a -> { b : Bool, a : Num a } + +mulWrap : Int range, Int range -> Int range + +## Multiplies two numbers, clamping on the maximum representable number rather than +## overflowing. +## +## This is the same as [Num.mul] except for the saturating behavior if the +## addition is to overflow. +mulSaturated : Num a, Num a -> Num a + +## Multiplies two numbers and checks for overflow. +## +## This is the same as [Num.mul] except if the operation overflows, instead of +## panicking or returning ∞ or -∞, it will return `Err Overflow`. +mulChecked : Num a, Num a -> Result (Num a) [Overflow] +mulChecked = \a, b -> + result = mulCheckedLowlevel a b + + if result.b then + Err Overflow + else + Ok result.a + +mulCheckedLowlevel : Num a, Num a -> { b : Bool, a : Num a } + +## Returns the lowest number that can be stored in an [I8] without underflowing +## its available memory and crashing. +## +## For reference, this number is `-128`. +## +## Note that the positive version of this number is larger than [Num.maxI8], +## which means if you call [Num.abs] on [Num.minI8], it will overflow and crash! +minI8 : I8 +minI8 = -128i8 + +## Returns the highest number that can be stored in an [I8] without overflowing +## its available memory and crashing. +## +## For reference, this number is `127`. +## +## Note that this is smaller than the positive version of [Num.minI8], +## which means if you call [Num.abs] on [Num.minI8], it will overflow and crash! +maxI8 : I8 +maxI8 = 127i8 + +## Returns the lowest number that can be stored in a [U8] without underflowing +## its available memory and crashing. +## +## For reference, this number is zero, because [U8] is +## [unsigned](https://en.wikipedia.org/wiki/Signed_number_representations), +## and zero is the lowest unsigned number. +## Unsigned numbers cannot be negative. +minU8 : U8 +minU8 = 0u8 + +## Returns the highest number that can be stored in a [U8] without overflowing +## its available memory and crashing. +## +## For reference, this number is `255`. +maxU8 : U8 +maxU8 = 255u8 + +## Returns the lowest number that can be stored in an [I16] without underflowing +## its available memory and crashing. +## +## For reference, this number is `-32_768`. +## +## Note that the positive version of this number is larger than [Num.maxI16], +## which means if you call [Num.abs] on [Num.minI16], it will overflow and crash! +minI16 : I16 +minI16 = -32768i16 + +## Returns the highest number that can be stored in an [I16] without overflowing +## its available memory and crashing. +## +## For reference, this number is `32_767`. +## +## Note that this is smaller than the positive version of [Num.minI16], +## which means if you call [Num.abs] on [Num.minI16], it will overflow and crash! +maxI16 : I16 +maxI16 = 32767i16 + +## Returns the lowest number that can be stored in a [U16] without underflowing +## its available memory and crashing. +## +## For reference, this number is zero, because [U16] is +## [unsigned](https://en.wikipedia.org/wiki/Signed_number_representations), +## and zero is the lowest unsigned number. +## Unsigned numbers cannot be negative. +minU16 : U16 +minU16 = 0u16 + +## Returns the highest number that can be stored in a [U16] without overflowing +## its available memory and crashing. +## +## For reference, this number is `65_535`. +maxU16 : U16 +maxU16 = 65535u16 + +## Returns the lowest number that can be stored in an [I32] without underflowing +## its available memory and crashing. +## +## For reference, this number is `-2_147_483_648`. +## +## Note that the positive version of this number is larger than [Num.maxI32], +## which means if you call [Num.abs] on [Num.minI32], it will overflow and crash! +minI32 : I32 +minI32 = -2147483648 + +## Returns the highest number that can be stored in an [I32] without overflowing +## its available memory and crashing. +## +## For reference, this number is `2_147_483_647`, +## which is over 2 million. +## +## Note that this is smaller than the positive version of [Num.minI32], +## which means if you call [Num.abs] on [Num.minI32], it will overflow and crash! +maxI32 : I32 +maxI32 = 2147483647 + +## Returns the lowest number that can be stored in a [U32] without underflowing +## its available memory and crashing. +## +## For reference, this number is zero, because [U32] is +## [unsigned](https://en.wikipedia.org/wiki/Signed_number_representations), +## and zero is the lowest unsigned number. +## Unsigned numbers cannot be negative. +minU32 : U32 +minU32 = 0 + +## Returns the highest number that can be stored in a [U32] without overflowing +## its available memory and crashing. +## +## For reference, this number is `4_294_967_295`. +maxU32 : U32 +maxU32 = 4294967295 + +## Returns the lowest number that can be stored in an [I64] without underflowing +## its available memory and crashing. +## +## For reference, this number is `-9_223_372_036_854_775_808`, +## which is under 9 quintillion. +## +## Note that the positive version of this number is larger than [Num.maxI64], +## which means if you call [Num.abs] on [Num.minI64], it will overflow and crash! +minI64 : I64 +minI64 = -9223372036854775808 + +## Returns the highest number that can be stored in an [I64] without overflowing +## its available memory and crashing. +## +## For reference, this number is `9_223_372_036_854_775_807`, +## which is over 9 quintillion. +## +## Note that this is smaller than the positive version of [Num.minI64], +## which means if you call [Num.abs] on [Num.minI64], it will overflow and crash! +maxI64 : I64 +maxI64 = 9223372036854775807 + +## Returns the lowest number that can be stored in a [U64] without underflowing +## its available memory and crashing. +## +## For reference, this number is zero, because [U64] is +## [unsigned](https://en.wikipedia.org/wiki/Signed_number_representations), +## and zero is the lowest unsigned number. +## Unsigned numbers cannot be negative. +minU64 : U64 +minU64 = 0 + +## Returns the highest number that can be stored in a [U64] without overflowing +## its available memory and crashing. +## +## For reference, this number is `18_446_744_073_709_551_615`, +## which is over 18 quintillion. +maxU64 : U64 +maxU64 = 18446744073709551615 + +## Returns the lowest number that can be stored in an [I128] without underflowing +## its available memory and crashing. +## +## For reference, this number is `-170_141_183_460_469_231_731_687_303_715_884_105_728`. +## which is under 170 undecillion. +## +## Note that the positive version of this number is larger than [Num.maxI128], +## which means if you call [Num.abs] on [Num.minI128], it will overflow and crash! +minI128 : I128 +minI128 = -170141183460469231731687303715884105728 + +## Returns the highest number that can be stored in an [I128] without overflowing +## its available memory and crashing. +## +## For reference, this number is `170_141_183_460_469_231_731_687_303_715_884_105_727`, +## which is over 170 undecillion. +## +## Note that this is smaller than the positive version of [Num.minI128], +## which means if you call [Num.abs] on [Num.minI128], it will overflow and crash! +maxI128 : I128 +maxI128 = 170141183460469231731687303715884105727 + +## Returns the lowest number that can be stored in a [U128] without underflowing +## its available memory and crashing. +## +## For reference, this number is zero, because [U128] is +## [unsigned](https://en.wikipedia.org/wiki/Signed_number_representations), +## and zero is the lowest unsigned number. +## Unsigned numbers cannot be negative. +minU128 : U128 +minU128 = 0 + +## Returns the highest number that can be stored in a [U128] without overflowing +## its available memory and crashing. +## +## For reference, this number is `340_282_366_920_938_463_463_374_607_431_768_211_455`, +## which is over 340 undecillion. +maxU128 : U128 +maxU128 = 340282366920938463463374607431768211455 + +minF32 : F32 +minF32 = -3.40282347e38 + +maxF32 : F32 +maxF32 = 3.40282347e38 + +minF64 : F64 +minF64 = -1.7976931348623157e308 + +maxF64 : F64 +maxF64 = 1.7976931348623157e308 + +## Converts an [Int] to an [I8]. If the given number can't be precisely represented in an [I8], +## the returned number may be different from the given number. +toI8 : Int * -> I8 +toI16 : Int * -> I16 +toI32 : Int * -> I32 +toI64 : Int * -> I64 +toI128 : Int * -> I128 +toU8 : Int * -> U8 +toU16 : Int * -> U16 +toU32 : Int * -> U32 +toU64 : Int * -> U64 +toU128 : Int * -> U128 + +## Converts an [Int] to a [Nat]. If the given number doesn't fit in [Nat], it will be truncated. +## Since [Nat] has a different maximum number depending on the system you're building +## for, this may give a different answer on different systems. +## +## For example, on a 32-bit system, `Num.maxNat` will return the same answer as +## `Num.maxU32`. This means that calling `Num.toNat 9_000_000_000` on a 32-bit +## system will return `Num.maxU32` instead of 9 billion, because 9 billion is +## higher than `Num.maxU32` and will not fit in a [Nat] on a 32-bit system. +## +## However, calling `Num.toNat 9_000_000_000` on a 64-bit system will return +## the [Nat] value of 9_000_000_000. This is because on a 64-bit system, [Nat] can +## hold up to `Num.maxU64`, and 9_000_000_000 is lower than `Num.maxU64`. +## +## To convert a [Frac] to a [Nat], first call either `Num.round`, `Num.ceil`, or `Num.floor` +## on it, then call this on the resulting [Int]. +toNat : Int * -> Nat + +## Converts a [Num] to an [F32]. If the given number can't be precisely represented in an [F32], +## the returned number may be different from the given number. +toF32 : Num * -> F32 + +## Converts a [Num] to an [F64]. If the given number can't be precisely represented in an [F64], +## the returned number may be different from the given number. +toF64 : Num * -> F64 + +## Converts a [Int] to an [I8]. +## If the given integer can't be precisely represented in an [I8], returns +## `Err OutOfBounds`. +toI8Checked : Int * -> Result I8 [OutOfBounds] +toI16Checked : Int * -> Result I16 [OutOfBounds] +toI32Checked : Int * -> Result I32 [OutOfBounds] +toI64Checked : Int * -> Result I64 [OutOfBounds] +toI128Checked : Int * -> Result I128 [OutOfBounds] +toU8Checked : Int * -> Result U8 [OutOfBounds] +toU16Checked : Int * -> Result U16 [OutOfBounds] +toU32Checked : Int * -> Result U32 [OutOfBounds] +toU64Checked : Int * -> Result U64 [OutOfBounds] +toU128Checked : Int * -> Result U128 [OutOfBounds] +toNatChecked : Int * -> Result Nat [OutOfBounds] +toF32Checked : Num * -> Result F32 [OutOfBounds] +toF64Checked : Num * -> Result F64 [OutOfBounds] diff --git a/crates/compiler/builtins/roc/Result.roc b/crates/compiler/builtins/roc/Result.roc new file mode 100644 index 0000000000..7ff2035176 --- /dev/null +++ b/crates/compiler/builtins/roc/Result.roc @@ -0,0 +1,94 @@ +interface Result + exposes [Result, isOk, isErr, map, mapErr, try, onErr, withDefault] + imports [Bool.{ Bool }] + +## The result of an operation that could fail: either the operation went +## okay, or else there was an error of some sort. +Result ok err : [Ok ok, Err err] + +## Returns `Bool.true` if the result indicates a success, else returns `Bool.false` +## ``` +## Result.isOk (Ok 5) +## ``` +isOk : Result ok err -> Bool +isOk = \result -> + when result is + Ok _ -> Bool.true + Err _ -> Bool.false + +## Returns `Bool.true` if the result indicates a failure, else returns `Bool.false` +## ``` +## Result.isErr (Err "uh oh") +## ``` +isErr : Result ok err -> Bool +isErr = \result -> + when result is + Ok _ -> Bool.false + Err _ -> Bool.true + +## If the result is `Ok`, returns the value it holds. Otherwise, returns +## the given default value. +## ``` +## Result.withDefault (Ok 7) 42 +## Result.withDefault (Err "uh oh") 42 +## ``` +withDefault : Result ok err, ok -> ok +withDefault = \result, default -> + when result is + Ok value -> value + Err _ -> default + +## If the result is `Ok`, transforms the value it holds by running a conversion +## function on it. Then returns a new `Ok` holding the transformed value. If the +## result is `Err`, this has no effect. Use [mapErr] to transform an `Err`. +## ``` +## Result.map (Ok 12) Num.neg +## Result.map (Err "yipes!") Num.neg +## ``` +## +## Functions like `map` are common in Roc; see for example [List.map], +## `Set.map`, and `Dict.map`. +map : Result a err, (a -> b) -> Result b err +map = \result, transform -> + when result is + Ok v -> Ok (transform v) + Err e -> Err e + +## If the result is `Err`, transforms the value it holds by running a conversion +## function on it. Then returns a new `Err` holding the transformed value. If +## the result is `Ok`, this has no effect. Use [map] to transform an `Ok`. +## ``` +## Result.mapErr (Err "yipes!") Str.isEmpty +## Result.mapErr (Ok 12) Str.isEmpty +## ``` +mapErr : Result ok a, (a -> b) -> Result ok b +mapErr = \result, transform -> + when result is + Ok v -> Ok v + Err e -> Err (transform e) + +## If the result is `Ok`, transforms the entire result by running a conversion +## function on the value the `Ok` holds. Then returns that new result. If the +## result is `Err`, this has no effect. Use `onErr` to transform an `Err`. +## ``` +## Result.try (Ok -1) \num -> if num < 0 then Err "negative!" else Ok -num +## Result.try (Err "yipes!") \num -> if num < 0 then Err "negative!" else Ok -num +## ``` +try : Result a err, (a -> Result b err) -> Result b err +try = \result, transform -> + when result is + Ok v -> transform v + Err e -> Err e + +## If the result is `Err`, transforms the entire result by running a conversion +## function on the value the `Err` holds. Then returns that new result. If the +## result is `Ok`, this has no effect. Use `try` to transform an `Ok`. +## ``` +## Result.onErr (Ok 10) \errorNum -> Str.toNat errorNum +## Result.onErr (Err "42") \errorNum -> Str.toNat errorNum +## ``` +onErr : Result a err, (err -> Result a otherErr) -> Result a otherErr +onErr = \result, transform -> + when result is + Ok v -> Ok v + Err e -> transform e diff --git a/crates/compiler/builtins/roc/Set.roc b/crates/compiler/builtins/roc/Set.roc new file mode 100644 index 0000000000..b942461f31 --- /dev/null +++ b/crates/compiler/builtins/roc/Set.roc @@ -0,0 +1,445 @@ +interface Set + exposes [ + Set, + empty, + single, + walk, + walkUntil, + insert, + len, + isEmpty, + capacity, + remove, + contains, + toList, + fromList, + union, + intersection, + difference, + map, + joinMap, + ] + imports [ + List, + Bool.{ Bool, Eq }, + Dict.{ Dict }, + Num.{ Nat }, + Hash.{ Hash, Hasher }, + Inspect.{ Inspect, Inspector, InspectFormatter }, + ] + +## Provides a [set](https://en.wikipedia.org/wiki/Set_(abstract_data_type)) +## type which stores a collection of unique values, without any ordering +Set k := Dict.Dict k {} where k implements Hash & Eq + implements [ + Eq { + isEq, + }, + Hash { + hash: hashSet, + }, + Inspect { + toInspector: toInspectorSet, + }, + ] + +isEq : Set k, Set k -> Bool where k implements Hash & Eq +isEq = \xs, ys -> + if len xs != len ys then + Bool.false + else + walkUntil xs Bool.true \_, elem -> + if contains ys elem then + Continue Bool.true + else + Break Bool.false + +hashSet : hasher, Set k -> hasher where k implements Hash & Eq, hasher implements Hasher +hashSet = \hasher, @Set inner -> Hash.hash hasher inner + +toInspectorSet : Set k -> Inspector f where k implements Inspect & Hash & Eq, f implements InspectFormatter +toInspectorSet = \set -> + fmt <- Inspect.custom + Inspect.apply (Inspect.set set walk Inspect.toInspector) fmt + +## Creates a new empty `Set`. +## ``` +## emptySet = Set.empty {} +## countValues = Set.len emptySet +## +## expect countValues == 0 +## ``` +empty : {} -> Set * +empty = \{} -> @Set (Dict.empty {}) + +## Return a dictionary with space allocated for a number of entries. This +## may provide a performance optimization if you know how many entries will be +## inserted. +withCapacity : Nat -> Set * +withCapacity = \cap -> + @Set (Dict.withCapacity cap) + +## Creates a new `Set` with a single value. +## ``` +## singleItemSet = Set.single "Apple" +## countValues = Set.len singleItemSet +## +## expect countValues == 1 +## ``` +single : k -> Set k where k implements Hash & Eq +single = \key -> + Dict.single key {} |> @Set + +## Insert a value into a `Set`. +## ``` +## fewItemSet = +## Set.empty {} +## |> Set.insert "Apple" +## |> Set.insert "Pear" +## |> Set.insert "Banana" +## +## countValues = Set.len fewItemSet +## +## expect countValues == 3 +## ``` +insert : Set k, k -> Set k where k implements Hash & Eq +insert = \@Set dict, key -> + Dict.insert dict key {} |> @Set + +# Inserting a duplicate key has no effect. +expect + actual = + empty {} + |> insert "foo" + |> insert "bar" + |> insert "foo" + |> insert "baz" + + expected = + empty {} + |> insert "foo" + |> insert "bar" + |> insert "baz" + + expected == actual + +## Counts the number of values in a given `Set`. +## ``` +## fewItemSet = +## Set.empty {} +## |> Set.insert "Apple" +## |> Set.insert "Pear" +## |> Set.insert "Banana" +## +## countValues = Set.len fewItemSet +## +## expect countValues == 3 +## ``` +len : Set * -> Nat +len = \@Set dict -> + Dict.len dict + +## Returns the max number of elements the set can hold before requiring a rehash. +## ``` +## foodSet = +## Set.empty {} +## |> Set.insert "apple" +## +## capacityOfSet = Set.capacity foodSet +## ``` +capacity : Set * -> Nat +capacity = \@Set dict -> + Dict.capacity dict + +## Check if the set is empty. +## ``` +## Set.isEmpty (Set.empty {} |> Set.insert 42) +## +## Set.isEmpty (Set.empty {}) +## ``` +isEmpty : Set * -> Bool +isEmpty = \@Set dict -> + Dict.isEmpty dict + +# Inserting a duplicate key has no effect on length. +expect + actual = + empty {} + |> insert "foo" + |> insert "bar" + |> insert "foo" + |> insert "baz" + |> len + + actual == 3 + +## Removes the value from the given `Set`. +## ``` +## numbers = +## Set.empty {} +## |> Set.insert 10 +## |> Set.insert 20 +## |> Set.remove 10 +## +## has10 = Set.contains numbers 10 +## has20 = Set.contains numbers 20 +## +## expect has10 == Bool.false +## expect has20 == Bool.true +## ``` +remove : Set k, k -> Set k where k implements Hash & Eq +remove = \@Set dict, key -> + Dict.remove dict key |> @Set + +## Test if a value is in the `Set`. +## ``` +## Fruit : [Apple, Pear, Banana] +## +## fruit : Set Fruit +## fruit = +## Set.single Apple +## |> Set.insert Pear +## +## hasApple = Set.contains fruit Apple +## hasBanana = Set.contains fruit Banana +## +## expect hasApple == Bool.true +## expect hasBanana == Bool.false +## ``` +contains : Set k, k -> Bool where k implements Hash & Eq +contains = \@Set dict, key -> + Dict.contains dict key + +## Retrieve the values in a `Set` as a `List`. +## ``` +## numbers : Set U64 +## numbers = Set.fromList [1,2,3,4,5] +## +## values = [1,2,3,4,5] +## +## expect Set.toList numbers == values +## ``` +toList : Set k -> List k where k implements Hash & Eq +toList = \@Set dict -> + Dict.keys dict + +## Create a `Set` from a `List` of values. +## ``` +## values = +## Set.empty {} +## |> Set.insert Banana +## |> Set.insert Apple +## |> Set.insert Pear +## +## expect Set.fromList [Pear, Apple, Banana] == values +## ``` +fromList : List k -> Set k where k implements Hash & Eq +fromList = \list -> + initial = @Set (Dict.withCapacity (List.len list)) + + List.walk list initial insert + +## Combine two `Set` collection by keeping the +## [union](https://en.wikipedia.org/wiki/Union_(set_theory)) +## of all the values pairs. This means that all of the values in both `Set`s +## will be combined. +## ``` +## set1 = Set.single Left +## set2 = Set.single Right +## +## expect Set.union set1 set2 == Set.fromList [Left, Right] +## ``` +union : Set k, Set k -> Set k where k implements Hash & Eq +union = \@Set dict1, @Set dict2 -> + Dict.insertAll dict1 dict2 |> @Set + +## Combine two `Set`s by keeping the [intersection](https://en.wikipedia.org/wiki/Intersection_(set_theory)) +## of all the values pairs. This means that we keep only those values that are +## in both `Set`s. +## ``` +## set1 = Set.fromList [Left, Other] +## set2 = Set.fromList [Left, Right] +## +## expect Set.intersection set1 set2 == Set.single Left +## ``` +intersection : Set k, Set k -> Set k where k implements Hash & Eq +intersection = \@Set dict1, @Set dict2 -> + Dict.keepShared dict1 dict2 |> @Set + +## Remove the values in the first `Set` that are also in the second `Set` +## using the [set difference](https://en.wikipedia.org/wiki/Complement_(set_theory)#Relative_complement) +## of the values. This means that we will be left with only those values that +## are in the first and not in the second. +## ``` +## first = Set.fromList [Left, Right, Up, Down] +## second = Set.fromList [Left, Right] +## +## expect Set.difference first second == Set.fromList [Up, Down] +## ``` +difference : Set k, Set k -> Set k where k implements Hash & Eq +difference = \@Set dict1, @Set dict2 -> + Dict.removeAll dict1 dict2 |> @Set + +## Iterate through the values of a given `Set` and build a value. +## ``` +## values = Set.fromList ["March", "April", "May"] +## +## startsWithLetterM = \month -> +## when Str.toUtf8 month is +## ['M', ..] -> Bool.true +## _ -> Bool.false +## +## reduce = \state, k -> +## if startsWithLetterM k then +## state + 1 +## else +## state +## +## result = Set.walk values 0 reduce +## +## expect result == 2 +## ``` +walk : Set k, state, (state, k -> state) -> state where k implements Hash & Eq +walk = \@Set dict, state, step -> + Dict.walk dict state (\s, k, _ -> step s k) + +## Convert each value in the set to something new, by calling a conversion +## function on each of them which receives the old value. Then return a +## new set containing the converted values. +map : Set a, (a -> b) -> Set b where a implements Hash & Eq, b implements Hash & Eq +map = \set, transform -> + init = withCapacity (capacity set) + + walk set init \answer, k -> + insert answer (transform k) + +## Like [Set.map], except the transformation function wraps the return value +## in a set. At the end, all the sets get joined together +## (using [Set.union]) into one set. +## +## You may know a similar function named `concatMap` in other languages. +joinMap : Set a, (a -> Set b) -> Set b where a implements Hash & Eq, b implements Hash & Eq +joinMap = \set, transform -> + init = withCapacity (capacity set) # Might be a pessimization + + walk set init \answer, k -> + union answer (transform k) + +## Iterate through the values of a given `Set` and build a value, can stop +## iterating part way through the collection. +## ``` +## numbers = Set.fromList [1,2,3,4,5,6,42,7,8,9,10] +## +## find42 = \state, k -> +## if k == 42 then +## Break FoundTheAnswer +## else +## Continue state +## +## result = Set.walkUntil numbers NotFound find42 +## +## expect result == FoundTheAnswer +## ``` +walkUntil : Set k, state, (state, k -> [Continue state, Break state]) -> state where k implements Hash & Eq +walkUntil = \@Set dict, state, step -> + Dict.walkUntil dict state (\s, k, _ -> step s k) + +expect + first = + single "Keep Me" + |> insert "And Me" + |> insert "Remove Me" + + second = + single "Remove Me" + |> insert "I do nothing..." + + expected = + single "Keep Me" + |> insert "And Me" + + difference first second == expected + +expect + first = + single "Keep Me" + |> insert "And Me" + |> insert "Remove Me" + + second = + single "Remove Me" + |> insert "I do nothing..." + + expected = + single "Keep Me" + |> insert "And Me" + + difference first second == expected + +expect + first = + single 1 + |> insert 2 + + second = + single 1 + |> insert 3 + |> insert 4 + + expected = + single 1 + |> insert 2 + |> insert 3 + |> insert 4 + + union first second == expected + +expect + base = + single "Remove Me" + |> insert "Keep Me" + |> insert "And Me" + + expected = + single "Keep Me" + |> insert "And Me" + + remove base "Remove Me" == expected + +expect + x = + single 0 + |> insert 1 + |> insert 2 + |> insert 3 + |> insert 4 + |> insert 5 + |> insert 6 + |> insert 7 + |> insert 8 + |> insert 9 + + x == fromList (toList x) + +expect + orderOne : Set Nat + orderOne = + single 1 + |> insert 2 + + orderTwo : Set Nat + orderTwo = + single 2 + |> insert 1 + + wrapperOne : Set (Set Nat) + wrapperOne = + single orderOne + |> insert orderTwo + + wrapperTwo : Set (Set Nat) + wrapperTwo = + single orderTwo + |> insert orderOne + + wrapperOne == wrapperTwo diff --git a/crates/compiler/builtins/roc/Str.roc b/crates/compiler/builtins/roc/Str.roc new file mode 100644 index 0000000000..15ecf45f32 --- /dev/null +++ b/crates/compiler/builtins/roc/Str.roc @@ -0,0 +1,1013 @@ +## Roc strings are sequences of text values. This module includes functions for combining strings, +## as well as breaking them up into smaller units—most commonly [extended grapheme clusters](http://www.unicode.org/glossary/#extended_grapheme_cluster) +## (referred to in this module's documentation as "graphemes" rather than "characters" for clarity; +## "characters" can mean very different things in different languages). +## +## This module focuses on graphemes (as opposed to, say, Unicode code points or LATIN-1 bytes) +## because graphemes avoid common classes of bugs. Breaking strings up using code points often +## leads to bugs around things like emoji, where multiple code points combine to form to a +## single rendered glyph. Graphemes avoid these bugs by treating multi-code-point things like +## emojis as indivisible units. +## +## Because graphemes can have variable length (there's no upper limit on how many code points one +## grapheme can represent), it takes linear time to count the number of graphemes in a string, +## and also linear time to find an individual grapheme within a string by its position (or "index") +## among the string's other graphemes. The only way to get constant-time access to these is in a way +## that can result in bugs if the string contains multi-code-point things like emojis, which is why +## this module does not offer those. +## +## +## ## Working with Unicode strings in Roc +## +## Unicode can represent text values which span multiple languages, symbols, and emoji. +## Here are some valid Roc strings: +## ``` +## "Roc!" +## "鹏" +## "🕊" +## ``` +## Every Unicode string is a sequence of [extended grapheme clusters](http://www.unicode.org/glossary/#extended_grapheme_cluster). +## An extended grapheme cluster represents what a person reading a string might +## call a "character" - like "A" or "ö" or "👩‍👩‍👦‍👦". +## Because the term "character" means different things in different areas of +## programming, and "extended grapheme cluster" is a mouthful, in Roc we use the +## term "grapheme" as a shorthand for the more precise "extended grapheme cluster." +## +## You can get the number of graphemes in a string by calling `Str.countGraphemes` on it: +## ``` +## Str.countGraphemes "Roc!" +## Str.countGraphemes "折り紙" +## Str.countGraphemes "🕊" +## ``` +## > The `countGraphemes` function walks through the entire string to get its answer, +## > so if you want to check whether a string is empty, you'll get much better performance +## > by calling `Str.isEmpty myStr` instead of `Str.countGraphemes myStr == 0`. +## +## ### Escape sequences +## +## If you put a `\` in a Roc string literal, it begins an *escape sequence*. +## An escape sequence is a convenient way to insert certain strings into other strings. +## For example, suppose you write this Roc string: +## ``` +## "I took the one less traveled by,\nAnd that has made all the difference." +## ``` +## The `"\n"` in the middle will insert a line break into this string. There are +## other ways of getting a line break in there, but `"\n"` is the most common. +## +## Another way you could insert a newlines is by writing `\u(0A)` instead of `\n`. +## That would result in the same string, because the `\u` escape sequence inserts +## [Unicode code points](https://unicode.org/glossary/#code_point) directly into +## the string. The Unicode code point 10 is a newline, and 10 is `0A` in hexadecimal. +## `\u` escape sequences are always followed by a hexadecimal number inside `(` and `)` +## like this. +## +## As another example, `"R\u(6F)c"` is the same string as `"Roc"`, because +## `"\u(6F)"` corresponds to the Unicode code point for lowercase `o`. If you +## want to [spice things up a bit](https://en.wikipedia.org/wiki/Metal_umlaut), +## you can write `"R\u(F6)c"` as an alternative way to get the string `"Röc"\. +## +## Roc strings also support these escape sequences: +## +## * `\\` - an actual backslash (writing a single `\` always begins an escape sequence!) +## * `\"` - an actual quotation mark (writing a `"` without a `\` ends the string) +## * `\r` - [carriage return](https://en.wikipedia.org/wiki/Carriage_Return) +## * `\t` - [horizontal tab](https://en.wikipedia.org/wiki/Tab_key#Tab_characters) +## * `\v` - [vertical tab](https://en.wikipedia.org/wiki/Tab_key#Tab_characters) +## +## You can also use escape sequences to insert named strings into other strings, like so: +## ``` +## name = "Lee" +## city = "Roctown" +## greeting = "Hello there, \(name)! Welcome to \(city)." +## ``` +## Here, `greeting` will become the string `"Hello there, Lee! Welcome to Roctown."`. +## This is known as [string interpolation](https://en.wikipedia.org/wiki/String_interpolation), +## and you can use it as many times as you like inside a string. The name +## between the parentheses must refer to a `Str` value that is currently in +## scope, and it must be a name - it can't be an arbitrary expression like a function call. +interface Str + exposes [ + Utf8Problem, + Utf8ByteProblem, + concat, + isEmpty, + joinWith, + split, + repeat, + countGraphemes, + countUtf8Bytes, + startsWithScalar, + toUtf8, + fromUtf8, + fromUtf8Range, + startsWith, + endsWith, + trim, + trimStart, + trimEnd, + toDec, + toF64, + toF32, + toNat, + toU128, + toI128, + toU64, + toI64, + toU32, + toI32, + toU16, + toI16, + toU8, + toI8, + toScalars, + replaceEach, + replaceFirst, + replaceLast, + splitFirst, + splitLast, + walkUtf8, + walkUtf8WithIndex, + reserve, + releaseExcessCapacity, + appendScalar, + walkScalars, + walkScalarsUntil, + withCapacity, + withPrefix, + graphemes, + contains, + ] + imports [ + Bool.{ Bool, Eq }, + Result.{ Result }, + List, + Num.{ Nat, Num, U8, U16, U32, U64, U128, I8, I16, I32, I64, I128, F32, F64, Dec }, + ] + +Utf8ByteProblem : [ + InvalidStartByte, + UnexpectedEndOfSequence, + ExpectedContinuation, + OverlongEncoding, + CodepointTooLarge, + EncodesSurrogateHalf, +] + +Utf8Problem : { byteIndex : Nat, problem : Utf8ByteProblem } + +## Returns [Bool.true] if the string is empty, and [Bool.false] otherwise. +## ``` +## expect Str.isEmpty "hi!" == Bool.false +## expect Str.isEmpty "" == Bool.true +## ``` +isEmpty : Str -> Bool + +## Concatenates two strings together. +## ``` +## expect Str.concat "ab" "cd" == "abcd" +## expect Str.concat "hello" "" == "hello" +## expect Str.concat "" "" == "" +## ``` +concat : Str, Str -> Str + +## Returns a string of the specified capacity without any content. +## +## This is a performance optimization tool that's like calling [Str.reserve] on an empty string. +## It's useful when you plan to build up a string incrementally, for example by calling [Str.concat] on it: +## +## ``` +## greeting = "Hello and welcome to Roc" +## subject = "Awesome Programmer" +## +## # Evaluates to "Hello and welcome to Roc, Awesome Programmer!" +## helloWorld = +## Str.withCapacity 45 +## |> Str.concat greeting +## |> Str.concat ", " +## |> Str.concat subject +## |> Str.concat "!" +## ``` +## +## In general, if you plan to use [Str.concat] on an empty string, it will be faster to start with +## [Str.withCapacity] than with `""`. Even if you don't know the exact capacity of the string, giving [withCapacity] +## a higher value than ends up being necessary can help prevent reallocation and copying—at +## the cost of using more memory than is necessary. +## +## For more details on how the performance optimization works, see [Str.reserve]. +withCapacity : Nat -> Str + +## Increase a string's capacity by at least the given number of additional bytes. +## +## This can improve the performance of string concatenation operations like [Str.concat] by +## allocating extra capacity up front, which can prevent the need for reallocations and copies. +## Consider the following example which does not use [Str.reserve]: +## +## ``` +## greeting = "Hello and welcome to Roc" +## subject = "Awesome Programmer" +## +## # Evaluates to "Hello and welcome to Roc, Awesome Programmer!" +## helloWorld = +## greeting +## |> Str.concat ", " +## |> Str.concat subject +## |> Str.concat "!" +## ``` +## +## In this example: +## 1. We start with `greeting`, which has both a length and capacity of 24 (bytes). +## 2. `|> Str.concat ", "` will see that there isn't enough capacity to add 2 more bytes for the `", "`, so it will create a new heap allocation with enough bytes to hold both. (This probably will be more than 7 bytes, because when [Str] functions reallocate, they apply a multiplier to the exact capacity required. This makes it less likely that future realloctions will be needed. The multiplier amount is not specified, because it may change in future releases of Roc, but it will likely be around 1.5 to 2 times the exact capacity required.) Then it will copy the current bytes (`"Hello"`) into the new allocation, and finally concatenate the `", "` into the new allocation. The old allocation will then be deallocated because it's no longer referenced anywhere in the program. +## 3. `|> Str.concat subject` will again check if there is enough capacity in the string. If it doesn't find enough capacity once again, it will make a third allocation, copy the existing bytes (`"Hello, "`) into that third allocation, and then deallocate the second allocation because it's already no longer being referenced anywhere else in the program. (It may find enough capacity in this particular case, because the previous [Str.concat] allocated something like 1.5 to 2 times the necessary capacity in order to anticipate future concatenations like this...but if something longer than `"World"` were being concatenated here, it might still require further reallocation and copying.) +## 4. `|> Str.concat "!\n"` will repeat this process once more. +## +## This process can have significant performance costs due to multiple reallocation of new strings, copying between old strings and new strings, and deallocation of immediately obsolete strings. +## +## Here's a modified example which uses [Str.reserve] to eliminate the need for all that reallocation, copying, and deallocation. +## +## ``` +## helloWorld = +## greeting +## |> Str.reserve 21 +## |> Str.concat ", " +## |> Str.concat subject +## |> Str.concat "!" +## ``` +## +## In this example: +## 1. We again start with `greeting`, which has both a length and capacity of 24 bytes. +## 2. `|> Str.reserve 21` will ensure that there is enough capacity in the string for an additional 21 bytes (to make room for `", "`, `"Awesome Programmer"`, and `"!"`). Since the current capacity is only 24, it will create a new 45-byte (24 + 21) heap allocation and copy the contents of the existing allocation (`greeting`) into it. +## 3. `|> Str.concat ", "` will concatenate `, ` to the string. No reallocation, copying, or deallocation will be necessary, because the string already has a capacity of 45 btytes, and `greeting` will only use 24 of them. +## 4. `|> Str.concat subject` will concatenate `subject` (`"Awesome Programmer"`) to the string. Again, no reallocation, copying, or deallocation will be necessary. +## 5. `|> Str.concat "!\n"` will concatenate `"!\n"` to the string, still without any reallocation, copying, or deallocation. +## +## Here, [Str.reserve] prevented multiple reallocations, copies, and deallocations during the +## [Str.concat] calls. Notice that it did perform a heap allocation before any [Str.concat] calls +## were made, which means that using [Str.reserve] is not free! You should only use it if you actually +## expect to make use of the extra capacity. +## +## Ideally, you'd be able to predict exactly how many extra bytes of capacity will be needed, but this +## may not always be knowable. When you don't know exactly how many bytes to reserve, you can often get better +## performance by choosing a number of bytes that's too high, because a number that's too low could lead to reallocations. There's a limit to +## this, of course; if you always give it ten times what it turns out to need, that could prevent +## reallocations but will also waste a lot of memory! +## +## If you plan to use [Str.reserve] on an empty string, it's generally better to use [Str.withCapacity] instead. +reserve : Str, Nat -> Str + +## Combines a [List] of strings into a single string, with a separator +## string in between each. +## ``` +## expect Str.joinWith ["one", "two", "three"] ", " == "one, two, three" +## expect Str.joinWith ["1", "2", "3", "4"] "." == "1.2.3.4" +## ``` +joinWith : List Str, Str -> Str + +## Split a string around a separator. +## +## Passing `""` for the separator is not useful; +## it returns the original string wrapped in a [List]. To split a string +## into its individual [graphemes](https://stackoverflow.com/a/27331885/4200103), use `Str.graphemes` +## ``` +## expect Str.split "1,2,3" "," == ["1","2","3"] +## expect Str.split "1,2,3" "" == ["1,2,3"] +## ``` +split : Str, Str -> List Str + +## Repeats a string the given number of times. +## ``` +## expect Str.repeat "z" 3 == "zzz" +## expect Str.repeat "na" 8 == "nananananananana" +## ``` +## Returns `""` when given `""` for the string or `0` for the count. +## ``` +## expect Str.repeat "" 10 == "" +## expect Str.repeat "anything" 0 == "" +## ``` +repeat : Str, Nat -> Str + +## Counts the number of [extended grapheme clusters](http://www.unicode.org/glossary/#extended_grapheme_cluster) +## in the string. +## +## Note that the number of extended grapheme clusters can be different from the number +## of visual glyphs rendered! Consider the following examples: +## ``` +## expect Str.countGraphemes "Roc" == 3 +## expect Str.countGraphemes "👩‍👩‍👦‍👦" == 4 +## expect Str.countGraphemes "🕊" == 1 +## ``` +## Note that "👩‍👩‍👦‍👦" takes up 4 graphemes (even though visually it appears as a single +## glyph) because under the hood it's represented using an emoji modifier sequence. +## In contrast, "🕊" only takes up 1 grapheme because under the hood it's represented +## using a single Unicode code point. +countGraphemes : Str -> Nat + +## Split a string into its constituent graphemes. +## +## This function breaks a string into its individual [graphemes](https://stackoverflow.com/a/27331885/4200103), +## returning them as a list of strings. This is useful for working with text that +## contains complex characters, such as emojis. +## +## Examples: +## ``` +## expect Str.graphemes "Roc" == ["R", "o", "c"] +## expect Str.graphemes "नमस्ते" == ["न", "म", "स्", "ते"] +## expect Str.graphemes "👩‍👩‍👦‍👦" == ["👩‍", "👩‍", "👦‍", "👦"] +## ``` +## +## Note that the "👩‍👩‍👦‍👦" example consists of 4 grapheme clusters, although it visually +## appears as a single glyph. This is because it uses an emoji modifier sequence. +graphemes : Str -> List Str + +## If the string begins with a [Unicode code point](http://www.unicode.org/glossary/#code_point) +## equal to the given [U32], returns [Bool.true]. Otherwise returns [Bool.false]. +## +## If the given string is empty, or if the given [U32] is not a valid +## code point, returns [Bool.false]. +## ``` +## expect Str.startsWithScalar "鹏 means 'roc'" 40527 # "鹏" is Unicode scalar 40527 +## expect !Str.startsWithScalar "9" 9 # the Unicode scalar for "9" is 57, not 9 +## expect !Str.startsWithScalar "" 40527 +## ``` +## +## ## Performance Details +## +## This runs slightly faster than [Str.startsWith], so +## if you want to check whether a string begins with something that's representable +## in a single code point, you can use (for example) `Str.startsWithScalar '鹏'` +## instead of `Str.startsWith "鹏"`. ('鹏' evaluates to the [U32] value `40527`.) +## This will not work for graphemes which take up multiple code points, however; +## `Str.startsWithScalar '👩‍👩‍👦‍👦'` would be a compiler error because 👩‍👩‍👦‍👦 takes up +## multiple code points and cannot be represented as a single [U32]. +## You'd need to use `Str.startsWithScalar "🕊"` instead. +startsWithScalar : Str, U32 -> Bool + +## Returns a [List] of the [Unicode scalar values](https://unicode.org/glossary/#unicode_scalar_value) +## in the given string. +## +## (Roc strings contain only scalar values, not [surrogate code points](https://unicode.org/glossary/#surrogate_code_point), +## so this is equivalent to returning a list of the string's [code points](https://unicode.org/glossary/#code_point).) +## ``` +## expect Str.toScalars "Roc" == [82, 111, 99] +## expect Str.toScalars "鹏" == [40527] +## expect Str.toScalars "சி" == [2970, 3007] +## expect Str.toScalars "🐦" == [128038] +## expect Str.toScalars "👩‍👩‍👦‍👦" == [128105, 8205, 128105, 8205, 128102, 8205, 128102] +## expect Str.toScalars "I ♥ Roc" == [73, 32, 9829, 32, 82, 111, 99] +## expect Str.toScalars "" == [] +## ``` +toScalars : Str -> List U32 + +## Returns a [List] of the string's [U8] UTF-8 [code units](https://unicode.org/glossary/#code_unit). +## (To split the string into a [List] of smaller [Str] values instead of [U8] values, +## see [Str.split].) +## ``` +## expect Str.toUtf8 "Roc" == [82, 111, 99] +## expect Str.toUtf8 "鹏" == [233, 185, 143] +## expect Str.toUtf8 "சி" == [224, 174, 154, 224, 174, 191] +## expect Str.toUtf8 "🐦" == [240, 159, 144, 166] +## ``` +toUtf8 : Str -> List U8 + +## Converts a [List] of [U8] UTF-8 [code units](https://unicode.org/glossary/#code_unit) to a string. +## +## Returns `Err` if the given bytes are invalid UTF-8, and returns `Ok ""` when given `[]`. +## ``` +## expect Str.fromUtf8 [82, 111, 99] == Ok "Roc" +## expect Str.fromUtf8 [233, 185, 143] == Ok "鹏" +## expect Str.fromUtf8 [224, 174, 154, 224, 174, 191] == Ok "சி" +## expect Str.fromUtf8 [240, 159, 144, 166] == Ok "🐦" +## expect Str.fromUtf8 [] == Ok "" +## expect Str.fromUtf8 [255] |> Result.isErr +## ``` +fromUtf8 : List U8 -> Result Str [BadUtf8 Utf8ByteProblem Nat] +fromUtf8 = \bytes -> + result = fromUtf8RangeLowlevel bytes 0 (List.len bytes) + + if result.cIsOk then + Ok result.bString + else + Err (BadUtf8 result.dProblemCode result.aByteIndex) + +expect (Str.fromUtf8 [82, 111, 99]) == Ok "Roc" +expect (Str.fromUtf8 [224, 174, 154, 224, 174, 191]) == Ok "சி" +expect (Str.fromUtf8 [240, 159, 144, 166]) == Ok "🐦" +expect (Str.fromUtf8 []) == Ok "" +expect (Str.fromUtf8 [255]) |> Result.isErr + +## Encode part of a [List] of [U8] UTF-8 [code units](https://unicode.org/glossary/#code_unit) +## into a [Str] +## ``` +## expect Str.fromUtf8Range [72, 105, 80, 103] { start : 0, count : 2 } == Ok "Hi" +## ``` +fromUtf8Range : List U8, { start : Nat, count : Nat } -> Result Str [BadUtf8 Utf8ByteProblem Nat, OutOfBounds] +fromUtf8Range = \bytes, config -> + if Num.addSaturated config.start config.count <= List.len bytes then + result = fromUtf8RangeLowlevel bytes config.start config.count + + if result.cIsOk then + Ok result.bString + else + Err (BadUtf8 result.dProblemCode result.aByteIndex) + else + Err OutOfBounds + +expect (Str.fromUtf8Range [72, 105, 80, 103] { start: 0, count: 2 }) == Ok "Hi" +expect (Str.fromUtf8Range [233, 185, 143, 224, 174, 154, 224, 174, 191] { start: 3, count: 3 }) == Ok "ச" +expect (Str.fromUtf8Range [240, 159, 144, 166] { start: 0, count: 4 }) == Ok "🐦" +expect (Str.fromUtf8Range [] { start: 0, count: 0 }) == Ok "" +expect (Str.fromUtf8Range [72, 105, 80, 103] { start: 2, count: 3 }) |> Result.isErr + +FromUtf8Result : { + aByteIndex : Nat, + bString : Str, + cIsOk : Bool, + dProblemCode : Utf8ByteProblem, +} + +fromUtf8RangeLowlevel : List U8, Nat, Nat -> FromUtf8Result + +## Check if the given [Str] starts with a value. +## ``` +## expect Str.startsWith "ABC" "A" == Bool.true +## expect Str.startsWith "ABC" "X" == Bool.false +## ``` +startsWith : Str, Str -> Bool + +## Check if the given [Str] ends with a value. +## ``` +## expect Str.endsWith "ABC" "C" == Bool.true +## expect Str.endsWith "ABC" "X" == Bool.false +## ``` +endsWith : Str, Str -> Bool + +## Return the [Str] with all whitespace removed from both the beginning +## as well as the end. +## ``` +## expect Str.trim " Hello \n\n" == "Hello" +## ``` +trim : Str -> Str + +## Return the [Str] with all whitespace removed from the beginning. +## ``` +## expect Str.trimStart " Hello \n\n" == "Hello \n\n" +## ``` +trimStart : Str -> Str + +## Return the [Str] with all whitespace removed from the end. +## ``` +## expect Str.trimEnd " Hello \n\n" == " Hello" +## ``` +trimEnd : Str -> Str + +## Encode a [Str] to a [Dec]. A [Dec] value is a 128-bit decimal +## [fixed-point number](https://en.wikipedia.org/wiki/Fixed-point_arithmetic). +## ``` +## expect Str.toDec "10" == Ok 10dec +## expect Str.toDec "-0.25" == Ok -0.25dec +## expect Str.toDec "not a number" == Err InvalidNumStr +## ``` +toDec : Str -> Result Dec [InvalidNumStr] +toDec = \string -> strToNumHelp string + +## Encode a [Str] to a [F64]. A [F64] value is a 64-bit +## [floating-point number](https://en.wikipedia.org/wiki/IEEE_754) and can be +## specified with a `f64` suffix. +## ``` +## expect Str.toF64 "0.10" == Ok 0.10f64 +## expect Str.toF64 "not a number" == Err InvalidNumStr +## ``` +toF64 : Str -> Result F64 [InvalidNumStr] +toF64 = \string -> strToNumHelp string + +## Encode a [Str] to a [F32].A [F32] value is a 32-bit +## [floating-point number](https://en.wikipedia.org/wiki/IEEE_754) and can be +## specified with a `f32` suffix. +## ``` +## expect Str.toF32 "0.10" == Ok 0.10f32 +## expect Str.toF32 "not a number" == Err InvalidNumStr +## ``` +toF32 : Str -> Result F32 [InvalidNumStr] +toF32 = \string -> strToNumHelp string + +## Convert a [Str] to a [Nat]. If the given number doesn't fit in [Nat], it will be [truncated](https://www.ualberta.ca/computing-science/media-library/teaching-resources/java/truncation-rounding.html). +## [Nat] has a different maximum number depending on the system you're building +## for, so this may give a different answer on different systems. +## +## For example, on a 32-bit system, `Num.maxNat` will return the same answer as +## `Num.maxU32`. This means that calling `Str.toNat "9_000_000_000"` on a 32-bit +## system will return `Num.maxU32` instead of 9 billion, because 9 billion is +## larger than `Num.maxU32` and will not fit in a [Nat] on a 32-bit system. +## +## Calling `Str.toNat "9_000_000_000"` on a 64-bit system will return +## the [Nat] value of 9_000_000_000. This is because on a 64-bit system, [Nat] can +## hold up to `Num.maxU64`, and 9_000_000_000 is smaller than `Num.maxU64`. +## ``` +## expect Str.toNat "9_000_000_000" == Ok 9000000000 +## expect Str.toNat "not a number" == Err InvalidNumStr +## ``` +toNat : Str -> Result Nat [InvalidNumStr] +toNat = \string -> strToNumHelp string + +## Encode a [Str] to an unsigned [U128] integer. A [U128] value can hold numbers +## from `0` to `340_282_366_920_938_463_463_374_607_431_768_211_455` (over +## 340 undecillion). It can be specified with a u128 suffix. +## ``` +## expect Str.toU128 "1500" == Ok 1500u128 +## expect Str.toU128 "0.1" == Err InvalidNumStr +## expect Str.toU128 "-1" == Err InvalidNumStr +## expect Str.toU128 "not a number" == Err InvalidNumStr +## ``` +toU128 : Str -> Result U128 [InvalidNumStr] +toU128 = \string -> strToNumHelp string + +## Encode a [Str] to a signed [I128] integer. A [I128] value can hold numbers +## from `-170_141_183_460_469_231_731_687_303_715_884_105_728` to +## `170_141_183_460_469_231_731_687_303_715_884_105_727`. It can be specified +## with a i128 suffix. +## ``` +## expect Str.toI128 "1500" == Ok 1500i128 +## expect Str.toI128 "-1" == Ok -1i128 +## expect Str.toI128 "0.1" == Err InvalidNumStr +## expect Str.toI128 "not a number" == Err InvalidNumStr +## ``` +toI128 : Str -> Result I128 [InvalidNumStr] +toI128 = \string -> strToNumHelp string + +## Encode a [Str] to an unsigned [U64] integer. A [U64] value can hold numbers +## from `0` to `18_446_744_073_709_551_615` (over 18 quintillion). It +## can be specified with a u64 suffix. +## ``` +## expect Str.toU64 "1500" == Ok 1500u64 +## expect Str.toU64 "0.1" == Err InvalidNumStr +## expect Str.toU64 "-1" == Err InvalidNumStr +## expect Str.toU64 "not a number" == Err InvalidNumStr +## ``` +toU64 : Str -> Result U64 [InvalidNumStr] +toU64 = \string -> strToNumHelp string + +## Encode a [Str] to a signed [I64] integer. A [I64] value can hold numbers +## from `-9_223_372_036_854_775_808` to `9_223_372_036_854_775_807`. It can be +## specified with a i64 suffix. +## ``` +## expect Str.toI64 "1500" == Ok 1500i64 +## expect Str.toI64 "-1" == Ok -1i64 +## expect Str.toI64 "0.1" == Err InvalidNumStr +## expect Str.toI64 "not a number" == Err InvalidNumStr +## ``` +toI64 : Str -> Result I64 [InvalidNumStr] +toI64 = \string -> strToNumHelp string + +## Encode a [Str] to an unsigned [U32] integer. A [U32] value can hold numbers +## from `0` to `4_294_967_295` (over 4 billion). It can be specified with +## a u32 suffix. +## ``` +## expect Str.toU32 "1500" == Ok 1500u32 +## expect Str.toU32 "0.1" == Err InvalidNumStr +## expect Str.toU32 "-1" == Err InvalidNumStr +## expect Str.toU32 "not a number" == Err InvalidNumStr +## ``` +toU32 : Str -> Result U32 [InvalidNumStr] +toU32 = \string -> strToNumHelp string + +## Encode a [Str] to a signed [I32] integer. A [I32] value can hold numbers +## from `-2_147_483_648` to `2_147_483_647`. It can be +## specified with a i32 suffix. +## ``` +## expect Str.toI32 "1500" == Ok 1500i32 +## expect Str.toI32 "-1" == Ok -1i32 +## expect Str.toI32 "0.1" == Err InvalidNumStr +## expect Str.toI32 "not a number" == Err InvalidNumStr +## ``` +toI32 : Str -> Result I32 [InvalidNumStr] +toI32 = \string -> strToNumHelp string + +## Encode a [Str] to an unsigned [U16] integer. A [U16] value can hold numbers +## from `0` to `65_535`. It can be specified with a u16 suffix. +## ``` +## expect Str.toU16 "1500" == Ok 1500u16 +## expect Str.toU16 "0.1" == Err InvalidNumStr +## expect Str.toU16 "-1" == Err InvalidNumStr +## expect Str.toU16 "not a number" == Err InvalidNumStr +## ``` +toU16 : Str -> Result U16 [InvalidNumStr] +toU16 = \string -> strToNumHelp string + +## Encode a [Str] to a signed [I16] integer. A [I16] value can hold numbers +## from `-32_768` to `32_767`. It can be +## specified with a i16 suffix. +## ``` +## expect Str.toI16 "1500" == Ok 1500i16 +## expect Str.toI16 "-1" == Ok -1i16 +## expect Str.toI16 "0.1" == Err InvalidNumStr +## expect Str.toI16 "not a number" == Err InvalidNumStr +## ``` +toI16 : Str -> Result I16 [InvalidNumStr] +toI16 = \string -> strToNumHelp string + +## Encode a [Str] to an unsigned [U8] integer. A [U8] value can hold numbers +## from `0` to `255`. It can be specified with a u8 suffix. +## ``` +## expect Str.toU8 "250" == Ok 250u8 +## expect Str.toU8 "-0.1" == Err InvalidNumStr +## expect Str.toU8 "not a number" == Err InvalidNumStr +## expect Str.toU8 "1500" == Err InvalidNumStr +## ``` +toU8 : Str -> Result U8 [InvalidNumStr] +toU8 = \string -> strToNumHelp string + +## Encode a [Str] to a signed [I8] integer. A [I8] value can hold numbers +## from `-128` to `127`. It can be +## specified with a i8 suffix. +## ``` +## expect Str.toI8 "-15" == Ok -15i8 +## expect Str.toI8 "150.00" == Err InvalidNumStr +## expect Str.toI8 "not a number" == Err InvalidNumStr +## ``` +toI8 : Str -> Result I8 [InvalidNumStr] +toI8 = \string -> strToNumHelp string + +## Get the byte at the given index, without performing a bounds check. +getUnsafe : Str, Nat -> U8 + +## Gives the number of bytes in a [Str] value. +## ``` +## expect Str.countUtf8Bytes "Hello World" == 11 +## ``` +countUtf8Bytes : Str -> Nat + +## string slice that does not do bounds checking or utf-8 verification +substringUnsafe : Str, Nat, Nat -> Str + +## Returns the given [Str] with each occurrence of a substring replaced. +## If the substring is not found, returns the original string. +## +## ``` +## expect Str.replaceEach "foo/bar/baz" "/" "_" == "foo_bar_baz" +## expect Str.replaceEach "not here" "/" "_" == "not here" +## ``` +replaceEach : Str, Str, Str -> Str +replaceEach = \haystack, needle, flower -> + when splitFirst haystack needle is + Ok { before, after } -> + # We found at least one needle, so start the buffer off with + # `before` followed by the first replacement flower. + Str.withCapacity (Str.countUtf8Bytes haystack) + |> Str.concat before + |> Str.concat flower + |> replaceEachHelp after needle flower + + Err NotFound -> haystack + +replaceEachHelp : Str, Str, Str, Str -> Str +replaceEachHelp = \buf, haystack, needle, flower -> + when splitFirst haystack needle is + Ok { before, after } -> + buf + |> Str.concat before + |> Str.concat flower + |> replaceEachHelp after needle flower + + Err NotFound -> Str.concat buf haystack + +expect Str.replaceEach "abXdeXghi" "X" "_" == "ab_de_ghi" +expect Str.replaceEach "abcdefg" "nothing" "_" == "abcdefg" + +## Returns the given [Str] with the first occurrence of a substring replaced. +## If the substring is not found, returns the original string. +## +## ``` +## expect Str.replaceFirst "foo/bar/baz" "/" "_" == "foo_bar/baz" +## expect Str.replaceFirst "no slashes here" "/" "_" == "no slashes here" +## ``` +replaceFirst : Str, Str, Str -> Str +replaceFirst = \haystack, needle, flower -> + when splitFirst haystack needle is + Ok { before, after } -> + "\(before)\(flower)\(after)" + + Err NotFound -> haystack + +expect Str.replaceFirst "abXdeXghi" "X" "_" == "ab_deXghi" +expect Str.replaceFirst "abcdefg" "nothing" "_" == "abcdefg" + +## Returns the given [Str] with the last occurrence of a substring replaced. +## If the substring is not found, returns the original string. +## +## ``` +## expect Str.replaceLast "foo/bar/baz" "/" "_" == "foo/bar_baz" +## expect Str.replaceLast "no slashes here" "/" "_" == "no slashes here" +## ``` +replaceLast : Str, Str, Str -> Str +replaceLast = \haystack, needle, flower -> + when splitLast haystack needle is + Ok { before, after } -> + "\(before)\(flower)\(after)" + + Err NotFound -> haystack + +expect Str.replaceLast "abXdeXghi" "X" "_" == "abXde_ghi" +expect Str.replaceLast "abcdefg" "nothing" "_" == "abcdefg" + +## Returns the given [Str] before the first occurrence of a [delimiter](https://www.computerhope.com/jargon/d/delimite.htm), as well +## as the rest of the string after that occurrence. +## Returns [Err NotFound] if the delimiter is not found. +## ``` +## expect Str.splitFirst "foo/bar/baz" "/" == Ok { before: "foo", after: "bar/baz" } +## expect Str.splitFirst "no slashes here" "/" == Err NotFound +## ``` +splitFirst : Str, Str -> Result { before : Str, after : Str } [NotFound] +splitFirst = \haystack, needle -> + when firstMatch haystack needle is + Some index -> + remaining = Str.countUtf8Bytes haystack - Str.countUtf8Bytes needle - index + + before = Str.substringUnsafe haystack 0 index + after = Str.substringUnsafe haystack (Num.addWrap index (Str.countUtf8Bytes needle)) remaining + + Ok { before, after } + + None -> + Err NotFound + +# splitFirst when needle isn't in haystack +expect splitFirst "foo" "z" == Err NotFound + +# splitFirst when needle isn't in haystack, and haystack is empty +expect splitFirst "" "z" == Err NotFound + +# splitFirst when haystack ends with needle repeated +expect splitFirst "foo" "o" == Ok { before: "f", after: "o" } + +# splitFirst with multi-byte needle +expect splitFirst "hullabaloo" "ab" == Ok { before: "hull", after: "aloo" } + +# splitFirst when needle is haystack +expect splitFirst "foo" "foo" == Ok { before: "", after: "" } + +firstMatch : Str, Str -> [Some Nat, None] +firstMatch = \haystack, needle -> + haystackLength = Str.countUtf8Bytes haystack + needleLength = Str.countUtf8Bytes needle + lastPossible = Num.subSaturated haystackLength needleLength + + firstMatchHelp haystack needle 0 lastPossible + +firstMatchHelp : Str, Str, Nat, Nat -> [Some Nat, None] +firstMatchHelp = \haystack, needle, index, lastPossible -> + if index <= lastPossible then + if matchesAt haystack index needle then + Some index + else + firstMatchHelp haystack needle (Num.addWrap index 1) lastPossible + else + None + +## Returns the given [Str] before the last occurrence of a delimiter, as well as +## the rest of the string after that occurrence. +## Returns [Err NotFound] if the delimiter is not found. +## ``` +## expect Str.splitLast "foo/bar/baz" "/" == Ok { before: "foo/bar", after: "baz" } +## expect Str.splitLast "no slashes here" "/" == Err NotFound +## ``` +splitLast : Str, Str -> Result { before : Str, after : Str } [NotFound] +splitLast = \haystack, needle -> + when lastMatch haystack needle is + Some index -> + remaining = Str.countUtf8Bytes haystack - Str.countUtf8Bytes needle - index + + before = Str.substringUnsafe haystack 0 index + after = Str.substringUnsafe haystack (Num.addWrap index (Str.countUtf8Bytes needle)) remaining + + Ok { before, after } + + None -> + Err NotFound + +# splitLast when needle isn't in haystack +expect Str.splitLast "foo" "z" == Err NotFound + +# splitLast when haystack ends with needle repeated +expect Str.splitLast "foo" "o" == Ok { before: "fo", after: "" } + +# splitLast with multi-byte needle +expect Str.splitLast "hullabaloo" "ab" == Ok { before: "hull", after: "aloo" } + +# splitLast when needle is haystack +expect Str.splitLast "foo" "foo" == Ok { before: "", after: "" } + +lastMatch : Str, Str -> [Some Nat, None] +lastMatch = \haystack, needle -> + haystackLength = Str.countUtf8Bytes haystack + needleLength = Str.countUtf8Bytes needle + lastPossibleIndex = Num.subSaturated haystackLength needleLength + + lastMatchHelp haystack needle lastPossibleIndex + +lastMatchHelp : Str, Str, Nat -> [Some Nat, None] +lastMatchHelp = \haystack, needle, index -> + if matchesAt haystack index needle then + Some index + else + when Num.subChecked index 1 is + Ok nextIndex -> + lastMatchHelp haystack needle nextIndex + + Err _ -> + None + +min = \x, y -> if x < y then x else y + +matchesAt : Str, Nat, Str -> Bool +matchesAt = \haystack, haystackIndex, needle -> + haystackLength = Str.countUtf8Bytes haystack + needleLength = Str.countUtf8Bytes needle + endIndex = min (Num.addSaturated haystackIndex needleLength) haystackLength + + matchesAtHelp { + haystack, + haystackIndex, + needle, + needleIndex: 0, + needleLength, + endIndex, + } + +matchesAtHelp = \state -> + { haystack, haystackIndex, needle, needleIndex, needleLength, endIndex } = state + isAtEndOfHaystack = haystackIndex >= endIndex + + if isAtEndOfHaystack then + didWalkEntireNeedle = needleIndex == needleLength + + didWalkEntireNeedle + else + doesThisMatch = + Str.getUnsafe haystack haystackIndex + == + Str.getUnsafe needle needleIndex + doesRestMatch = + matchesAtHelp + { state & + haystackIndex: Num.addWrap haystackIndex 1, + needleIndex: Num.addWrap needleIndex 1, + } + + doesThisMatch && doesRestMatch + +## Walks over the `UTF-8` bytes of the given [Str] and calls a function to update +## state for each byte. The index for that byte in the string is provided +## to the update function. +## ``` +## f : List U8, U8, Nat -> List U8 +## f = \state, byte, _ -> List.append state byte +## expect Str.walkUtf8WithIndex "ABC" [] f == [65, 66, 67] +## ``` +walkUtf8WithIndex : Str, state, (state, U8, Nat -> state) -> state +walkUtf8WithIndex = \string, state, step -> + walkUtf8WithIndexHelp string state step 0 (Str.countUtf8Bytes string) + +walkUtf8WithIndexHelp : Str, state, (state, U8, Nat -> state), Nat, Nat -> state +walkUtf8WithIndexHelp = \string, state, step, index, length -> + if index < length then + byte = Str.getUnsafe string index + newState = step state byte index + + walkUtf8WithIndexHelp string newState step (Num.addWrap index 1) length + else + state + +## Walks over the `UTF-8` bytes of the given [Str] and calls a function to update +## state for each byte. +## +## ``` +## sumOfUtf8Bytes = +## Str.walkUtf8 "Hello, World!" 0 \total, byte -> +## total + byte +## +## expect sumOfUtf8Bytes == 105 +## ``` +walkUtf8 : Str, state, (state, U8 -> state) -> state +walkUtf8 = \str, initial, step -> + walkUtf8Help str initial step 0 (Str.countUtf8Bytes str) + +walkUtf8Help : Str, state, (state, U8 -> state), Nat, Nat -> state +walkUtf8Help = \str, state, step, index, length -> + if index < length then + byte = Str.getUnsafe str index + newState = step state byte + + walkUtf8Help str newState step (Num.addWrap index 1) length + else + state + +expect (walkUtf8 "ABC" [] List.append) == [65, 66, 67] +expect (walkUtf8 "鹏" [] List.append) == [233, 185, 143] + +## Shrink the memory footprint of a str such that its capacity and length are equal. +## Note: This will also convert seamless slices to regular lists. +releaseExcessCapacity : Str -> Str + +## is UB when the scalar is invalid +appendScalarUnsafe : Str, U32 -> Str + +## Append a [U32] scalar to the given string. If the given scalar is not a valid +## unicode value, it returns [Err InvalidScalar]. +## ``` +## expect Str.appendScalar "H" 105 == Ok "Hi" +## expect Str.appendScalar "😢" 0xabcdef == Err InvalidScalar +## ``` +appendScalar : Str, U32 -> Result Str [InvalidScalar] +appendScalar = \string, scalar -> + if isValidScalar scalar then + Ok (appendScalarUnsafe string scalar) + else + Err InvalidScalar + +isValidScalar : U32 -> Bool +isValidScalar = \scalar -> + scalar <= 0xD7FF || (scalar >= 0xE000 && scalar <= 0x10FFFF) + +getScalarUnsafe : Str, Nat -> { scalar : U32, bytesParsed : Nat } + +## Walks over the unicode [U32] values for the given [Str] and calls a function +## to update state for each. +## ``` +## f : List U32, U32 -> List U32 +## f = \state, scalar -> List.append state scalar +## expect Str.walkScalars "ABC" [] f == [65, 66, 67] +## ``` +walkScalars : Str, state, (state, U32 -> state) -> state +walkScalars = \string, init, step -> + walkScalarsHelp string init step 0 (Str.countUtf8Bytes string) + +walkScalarsHelp : Str, state, (state, U32 -> state), Nat, Nat -> state +walkScalarsHelp = \string, state, step, index, length -> + if index < length then + { scalar, bytesParsed } = getScalarUnsafe string index + newState = step state scalar + + walkScalarsHelp string newState step (Num.addWrap index bytesParsed) length + else + state + +## Walks over the unicode [U32] values for the given [Str] and calls a function +## to update state for each. +## ``` +## f : List U32, U32 -> [Break (List U32), Continue (List U32)] +## f = \state, scalar -> +## check = 66 +## if scalar == check then +## Break [check] +## else +## Continue (List.append state scalar) +## expect Str.walkScalarsUntil "ABC" [] f == [66] +## expect Str.walkScalarsUntil "AxC" [] f == [65, 120, 67] +## ``` +walkScalarsUntil : Str, state, (state, U32 -> [Break state, Continue state]) -> state +walkScalarsUntil = \string, init, step -> + walkScalarsUntilHelp string init step 0 (Str.countUtf8Bytes string) + +walkScalarsUntilHelp : Str, state, (state, U32 -> [Break state, Continue state]), Nat, Nat -> state +walkScalarsUntilHelp = \string, state, step, index, length -> + if index < length then + { scalar, bytesParsed } = getScalarUnsafe string index + + when step state scalar is + Continue newState -> + walkScalarsUntilHelp string newState step (Num.addWrap index bytesParsed) length + + Break newState -> + newState + else + state + +strToNum : Str -> { berrorcode : U8, aresult : Num * } + +strToNumHelp : Str -> Result (Num a) [InvalidNumStr] +strToNumHelp = \string -> + result : { berrorcode : U8, aresult : Num a } + result = strToNum string + + if result.berrorcode == 0 then + Ok result.aresult + else + Err InvalidNumStr + +## Adds a prefix to the given [Str]. +## ``` +## expect Str.withPrefix "Awesome" "Roc" == "RocAwesome" +## ``` +withPrefix : Str, Str -> Str +withPrefix = \str, prefix -> Str.concat prefix str + +## Determines whether or not the first Str contains the second. +## ``` +## expect Str.contains "foobarbaz" "bar" +## expect !(Str.contains "apple" "orange") +## expect Str.contains "anything" "" +## ``` +contains : Str, Str -> Bool +contains = \haystack, needle -> + when firstMatch haystack needle is + Some _index -> Bool.true + None -> Bool.false diff --git a/crates/compiler/builtins/roc/TotallyNotJson.roc b/crates/compiler/builtins/roc/TotallyNotJson.roc new file mode 100644 index 0000000000..1ed15f61dd --- /dev/null +++ b/crates/compiler/builtins/roc/TotallyNotJson.roc @@ -0,0 +1,1770 @@ +## THIS MODULE IS DEPRECATED AND CURRENTLY IN THE PROCESS OF BEING REMOVED +## FROM STD LIBRARY +interface TotallyNotJson + exposes [ + Json, + json, + jsonWithOptions, + ] + imports [ + List, + Str, + Result.{ Result }, + Encode, + Encode.{ + Encoder, + EncoderFormatting, + appendWith, + }, + Decode, + Decode.{ + DecoderFormatting, + DecodeResult, + }, + Num.{ + U8, + U16, + U32, + U64, + U128, + I8, + I16, + I32, + I64, + I128, + F32, + F64, + Nat, + Dec, + }, + Bool.{ Bool, Eq }, + Result, + ] + +## An opaque type with the `EncoderFormatting` and +## `DecoderFormatting` abilities. +Json := { fieldNameMapping : FieldNameMapping } + implements [ + EncoderFormatting { + u8: encodeU8, + u16: encodeU16, + u32: encodeU32, + u64: encodeU64, + u128: encodeU128, + i8: encodeI8, + i16: encodeI16, + i32: encodeI32, + i64: encodeI64, + i128: encodeI128, + f32: encodeF32, + f64: encodeF64, + dec: encodeDec, + bool: encodeBool, + string: encodeString, + list: encodeList, + record: encodeRecord, + tuple: encodeTuple, + tag: encodeTag, + }, + DecoderFormatting { + u8: decodeU8, + u16: decodeU16, + u32: decodeU32, + u64: decodeU64, + u128: decodeU128, + i8: decodeI8, + i16: decodeI16, + i32: decodeI32, + i64: decodeI64, + i128: decodeI128, + f32: decodeF32, + f64: decodeF64, + dec: decodeDec, + bool: decodeBool, + string: decodeString, + list: decodeList, + record: decodeRecord, + tuple: decodeTuple, + }, + ] + +## Returns a JSON `Encoder` and `Decoder` +json = @Json { fieldNameMapping: Default } + +## Returns a JSON `Encoder` and `Decoder` with configuration options +jsonWithOptions = \{ fieldNameMapping ? Default } -> + @Json { fieldNameMapping } + +## Mapping between Roc record fields and JSON object names +FieldNameMapping : [ + Default, # no transformation + SnakeCase, # snake_case + PascalCase, # PascalCase + KebabCase, # kabab-case + CamelCase, # camelCase + Custom (Str -> Str), # provide a custom formatting +] + +# TODO encode as JSON numbers as base 10 decimal digits +# e.g. the REPL `Num.toStr 12e42f64` gives +# "12000000000000000000000000000000000000000000" : Str +# which should be encoded as "12e42" : Str +numToBytes = \n -> + n |> Num.toStr |> Str.toUtf8 + +encodeU8 = \n -> + Encode.custom \bytes, @Json {} -> + List.concat bytes (numToBytes n) + +encodeU16 = \n -> + Encode.custom \bytes, @Json {} -> + List.concat bytes (numToBytes n) + +encodeU32 = \n -> + Encode.custom \bytes, @Json {} -> + List.concat bytes (numToBytes n) + +encodeU64 = \n -> + Encode.custom \bytes, @Json {} -> + List.concat bytes (numToBytes n) + +encodeU128 = \n -> + Encode.custom \bytes, @Json {} -> + List.concat bytes (numToBytes n) + +encodeI8 = \n -> + Encode.custom \bytes, @Json {} -> + List.concat bytes (numToBytes n) + +encodeI16 = \n -> + Encode.custom \bytes, @Json {} -> + List.concat bytes (numToBytes n) + +encodeI32 = \n -> + Encode.custom \bytes, @Json {} -> + List.concat bytes (numToBytes n) + +encodeI64 = \n -> + Encode.custom \bytes, @Json {} -> + List.concat bytes (numToBytes n) + +encodeI128 = \n -> + Encode.custom \bytes, @Json {} -> + List.concat bytes (numToBytes n) + +encodeF32 = \n -> + Encode.custom \bytes, @Json {} -> + List.concat bytes (numToBytes n) + +encodeF64 = \n -> + Encode.custom \bytes, @Json {} -> + List.concat bytes (numToBytes n) + +encodeDec = \n -> + Encode.custom \bytes, @Json {} -> + List.concat bytes (numToBytes n) + +encodeBool = \b -> + Encode.custom \bytes, @Json {} -> + if b then + List.concat bytes (Str.toUtf8 "true") + else + List.concat bytes (Str.toUtf8 "false") + +# Test encode boolean +expect + input = [Bool.true, Bool.false] + actual = Encode.toBytes input json + expected = Str.toUtf8 "[true,false]" + + actual == expected + +encodeString = \str -> + Encode.custom \bytes, @Json {} -> + List.concat bytes (encodeStrBytes str) + +# TODO add support for unicode escapes (including 2,3,4 byte code points) +# these should be encoded using a 12-byte sequence encoding the UTF-16 surrogate +# pair. For example a string containing only G clef character U+1D11E is +# represented as "\\uD834\\uDD1E" (note "\\" here is a single reverse solidus) +encodeStrBytes = \str -> + bytes = Str.toUtf8 str + + initialState = { bytePos: 0, status: NoEscapesFound } + + firstPassState = + List.walkUntil bytes initialState \{ bytePos, status }, b -> + when b is + 0x22 -> Break { bytePos, status: FoundEscape } # U+0022 Quotation mark + 0x5c -> Break { bytePos, status: FoundEscape } # U+005c Reverse solidus + 0x2f -> Break { bytePos, status: FoundEscape } # U+002f Solidus + 0x08 -> Break { bytePos, status: FoundEscape } # U+0008 Backspace + 0x0c -> Break { bytePos, status: FoundEscape } # U+000c Form feed + 0x0a -> Break { bytePos, status: FoundEscape } # U+000a Line feed + 0x0d -> Break { bytePos, status: FoundEscape } # U+000d Carriage return + 0x09 -> Break { bytePos, status: FoundEscape } # U+0009 Tab + _ -> Continue { bytePos: bytePos + 1, status } + + when firstPassState.status is + NoEscapesFound -> + (List.len bytes) + + 2 + |> List.withCapacity + |> List.concat ['"'] + |> List.concat bytes + |> List.concat ['"'] + + FoundEscape -> + { before: bytesBeforeEscape, others: bytesWithEscapes } = + List.split bytes firstPassState.bytePos + + # Reserve List with 120% capacity for escaped bytes to reduce + # allocations, include starting quote, and bytes up to first escape + initial = + List.len bytes + |> Num.mul 120 + |> Num.divCeil 100 + |> List.withCapacity + |> List.concat ['"'] + |> List.concat bytesBeforeEscape + + # Walk the remaining bytes and include escape '\' as required + # add closing quote + List.walk bytesWithEscapes initial \encodedBytes, byte -> + List.concat encodedBytes (escapedByteToJson byte) + |> List.concat ['"'] + +# Prepend an "\" escape byte +escapedByteToJson : U8 -> List U8 +escapedByteToJson = \b -> + when b is + 0x22 -> [0x5c, 0x22] # U+0022 Quotation mark + 0x5c -> [0x5c, 0x5c] # U+005c Reverse solidus + 0x2f -> [0x5c, 0x2f] # U+002f Solidus + 0x08 -> [0x5c, 'b'] # U+0008 Backspace + 0x0c -> [0x5c, 'f'] # U+000c Form feed + 0x0a -> [0x5c, 'n'] # U+000a Line feed + 0x0d -> [0x5c, 'r'] # U+000d Carriage return + 0x09 -> [0x5c, 'r'] # U+0009 Tab + _ -> [b] + +expect escapedByteToJson '\n' == ['\\', 'n'] +expect escapedByteToJson '\\' == ['\\', '\\'] +expect escapedByteToJson '"' == ['\\', '"'] + +# Test encode small string +expect + input = "G'day" + actual = Encode.toBytes input json + expected = Str.toUtf8 "\"G'day\"" + + actual == expected + +# Test encode large string +expect + input = "the quick brown fox jumps over the lazy dog" + actual = Encode.toBytes input json + expected = Str.toUtf8 "\"the quick brown fox jumps over the lazy dog\"" + + actual == expected + +# Test encode with escapes e.g. "\r" encodes to "\\r" +expect + input = "the quick brown fox jumps over the lazy doga\r\nbc\\\"xz" + actual = Encode.toBytes input json + expected = Str.toUtf8 "\"the quick brown fox jumps over the lazy doga\\r\\nbc\\\\\\\"xz\"" + + actual == expected + +encodeList = \lst, encodeElem -> + Encode.custom \bytes, @Json { fieldNameMapping } -> + writeList = \{ buffer, elemsLeft }, elem -> + bufferWithElem = appendWith buffer (encodeElem elem) (@Json { fieldNameMapping }) + bufferWithSuffix = + if elemsLeft > 1 then + List.append bufferWithElem (Num.toU8 ',') + else + bufferWithElem + + { buffer: bufferWithSuffix, elemsLeft: elemsLeft - 1 } + + head = List.append bytes (Num.toU8 '[') + { buffer: withList } = List.walk lst { buffer: head, elemsLeft: List.len lst } writeList + + List.append withList (Num.toU8 ']') + +# Test encode list of floats +expect + input : List F64 + input = [-1, 0.00001, 1e12, 2.0e-2, 0.0003, 43] + actual = Encode.toBytes input json + expected = Str.toUtf8 "[-1,0.00001,1000000000000,0.02,0.0003,43]" + + actual == expected + +encodeRecord = \fields -> + Encode.custom \bytes, @Json { fieldNameMapping } -> + writeRecord = \{ buffer, fieldsLeft }, { key, value } -> + + fieldName = toObjectNameUsingMap key fieldNameMapping + + bufferWithKeyValue = + List.append buffer (Num.toU8 '"') + |> List.concat (Str.toUtf8 fieldName) + |> List.append (Num.toU8 '"') + |> List.append (Num.toU8 ':') # Note we need to encode using the json config here + |> appendWith value (@Json { fieldNameMapping }) + + bufferWithSuffix = + if fieldsLeft > 1 then + List.append bufferWithKeyValue (Num.toU8 ',') + else + bufferWithKeyValue + + { buffer: bufferWithSuffix, fieldsLeft: fieldsLeft - 1 } + + bytesHead = List.append bytes (Num.toU8 '{') + { buffer: bytesWithRecord } = List.walk fields { buffer: bytesHead, fieldsLeft: List.len fields } writeRecord + + List.append bytesWithRecord (Num.toU8 '}') + +# Test encode for a record with two strings ignoring whitespace +expect + input = { fruitCount: 2, ownerName: "Farmer Joe" } + encoder = jsonWithOptions { fieldNameMapping: PascalCase } + actual = Encode.toBytes input encoder + expected = Str.toUtf8 "{\"FruitCount\":2,\"OwnerName\":\"Farmer Joe\"}" + + actual == expected + +# Test encode of record with an array of strings and a boolean field +expect + input = { fruitFlavours: ["Apples", "Bananas", "Pears"], isFresh: Bool.true } + encoder = jsonWithOptions { fieldNameMapping: KebabCase } + actual = Encode.toBytes input encoder + expected = Str.toUtf8 "{\"fruit-flavours\":[\"Apples\",\"Bananas\",\"Pears\"],\"is-fresh\":true}" + + actual == expected + +# Test encode of record with a string and number field +expect + input = { firstSegment: "ab", secondSegment: 10u8 } + encoder = jsonWithOptions { fieldNameMapping: SnakeCase } + actual = Encode.toBytes input encoder + expected = Str.toUtf8 "{\"first_segment\":\"ab\",\"second_segment\":10}" + + actual == expected + +# Test encode of record of a record +expect + input = { outer: { inner: "a" }, other: { one: "b", two: 10u8 } } + encoder = jsonWithOptions { fieldNameMapping: Custom toYellingCase } + actual = Encode.toBytes input encoder + expected = Str.toUtf8 "{\"OTHER\":{\"ONE\":\"b\",\"TWO\":10},\"OUTER\":{\"INNER\":\"a\"}}" + + actual == expected + +toYellingCase = \str -> + Str.graphemes str + |> List.map toUppercase + |> Str.joinWith "" + +encodeTuple = \elems -> + Encode.custom \bytes, @Json { fieldNameMapping } -> + writeTuple = \{ buffer, elemsLeft }, elemEncoder -> + bufferWithElem = + appendWith buffer elemEncoder (@Json { fieldNameMapping }) + + bufferWithSuffix = + if elemsLeft > 1 then + List.append bufferWithElem (Num.toU8 ',') + else + bufferWithElem + + { buffer: bufferWithSuffix, elemsLeft: elemsLeft - 1 } + + bytesHead = List.append bytes (Num.toU8 '[') + { buffer: bytesWithRecord } = List.walk elems { buffer: bytesHead, elemsLeft: List.len elems } writeTuple + + List.append bytesWithRecord (Num.toU8 ']') + +# Test encode of tuple +expect + input = ("The Answer is", 42) + actual = Encode.toBytes input json + expected = Str.toUtf8 "[\"The Answer is\",42]" + + actual == expected + +encodeTag = \name, payload -> + Encode.custom \bytes, @Json { fieldNameMapping } -> + # Idea: encode `A v1 v2` as `{"A": [v1, v2]}` + writePayload = \{ buffer, itemsLeft }, encoder -> + bufferWithValue = appendWith buffer encoder (@Json { fieldNameMapping }) + bufferWithSuffix = + if itemsLeft > 1 then + List.append bufferWithValue (Num.toU8 ',') + else + bufferWithValue + + { buffer: bufferWithSuffix, itemsLeft: itemsLeft - 1 } + + bytesHead = + List.append bytes (Num.toU8 '{') + |> List.append (Num.toU8 '"') + |> List.concat (Str.toUtf8 name) + |> List.append (Num.toU8 '"') + |> List.append (Num.toU8 ':') + |> List.append (Num.toU8 '[') + + { buffer: bytesWithPayload } = List.walk payload { buffer: bytesHead, itemsLeft: List.len payload } writePayload + + List.append bytesWithPayload (Num.toU8 ']') + |> List.append (Num.toU8 '}') + +# Test encode of tag +expect + input = TheAnswer "is" 42 + encoder = jsonWithOptions { fieldNameMapping: KebabCase } + actual = Encode.toBytes input encoder + expected = Str.toUtf8 "{\"TheAnswer\":[\"is\",42]}" + + actual == expected + +decodeU8 = Decode.custom \bytes, @Json {} -> + { taken, rest } = takeJsonNumber bytes + + result = + taken + |> Str.fromUtf8 + |> Result.try Str.toU8 + |> Result.mapErr \_ -> TooShort + + { result, rest } + +# Test decode of U8 +expect + actual = Str.toUtf8 "255" |> Decode.fromBytes json + actual == Ok 255u8 + +decodeU16 = Decode.custom \bytes, @Json {} -> + { taken, rest } = takeJsonNumber bytes + + result = + taken + |> Str.fromUtf8 + |> Result.try Str.toU16 + |> Result.mapErr \_ -> TooShort + + { result, rest } + +# Test decode of U16 +expect + actual = Str.toUtf8 "65535" |> Decode.fromBytes json + actual == Ok 65_535u16 + +decodeU32 = Decode.custom \bytes, @Json {} -> + { taken, rest } = takeJsonNumber bytes + + result = + taken + |> Str.fromUtf8 + |> Result.try Str.toU32 + |> Result.mapErr \_ -> TooShort + + { result, rest } + +# Test decode of U32 +expect + actual = Str.toUtf8 "4000000000" |> Decode.fromBytes json + actual == Ok 4_000_000_000u32 + +decodeU64 = Decode.custom \bytes, @Json {} -> + { taken, rest } = takeJsonNumber bytes + + result = + taken + |> Str.fromUtf8 + |> Result.try Str.toU64 + |> Result.mapErr \_ -> TooShort + + { result, rest } + +# Test decode of U64 +expect + actual = Str.toUtf8 "18446744073709551614" |> Decode.fromBytes json + actual == Ok 18_446_744_073_709_551_614u64 + +decodeU128 = Decode.custom \bytes, @Json {} -> + { taken, rest } = takeJsonNumber bytes + + result = + taken + |> Str.fromUtf8 + |> Result.try Str.toU128 + |> Result.mapErr \_ -> TooShort + + { result, rest } + +# Test decode of U128 +expect + actual = Str.toUtf8 "1234567" |> Decode.fromBytesPartial json + actual.result == Ok 1234567u128 + +# TODO should we support decoding bigints, note that valid json is only a +# double precision float-64 +# expect +# actual = Str.toUtf8 "340282366920938463463374607431768211455" |> Decode.fromBytesPartial json +# actual.result == Ok 340_282_366_920_938_463_463_374_607_431_768_211_455u128 + +decodeI8 = Decode.custom \bytes, @Json {} -> + { taken, rest } = takeJsonNumber bytes + + result = + taken + |> Str.fromUtf8 + |> Result.try Str.toI8 + |> Result.mapErr \_ -> TooShort + + { result, rest } + +# Test decode of I8 +expect + actual = Str.toUtf8 "-125" |> Decode.fromBytesPartial json + actual.result == Ok -125i8 + +decodeI16 = Decode.custom \bytes, @Json {} -> + { taken, rest } = takeJsonNumber bytes + + result = + taken + |> Str.fromUtf8 + |> Result.try Str.toI16 + |> Result.mapErr \_ -> TooShort + + { result, rest } + +# Test decode of I16 +expect + actual = Str.toUtf8 "-32768" |> Decode.fromBytesPartial json + actual.result == Ok -32_768i16 + +decodeI32 = Decode.custom \bytes, @Json {} -> + { taken, rest } = takeJsonNumber bytes + + result = + taken + |> Str.fromUtf8 + |> Result.try Str.toI32 + |> Result.mapErr \_ -> TooShort + + { result, rest } + +# Test decode of I32 +expect + actual = Str.toUtf8 "-2147483648" |> Decode.fromBytesPartial json + actual.result == Ok -2_147_483_648i32 + +decodeI64 = Decode.custom \bytes, @Json {} -> + { taken, rest } = takeJsonNumber bytes + + result = + taken + |> Str.fromUtf8 + |> Result.try Str.toI64 + |> Result.mapErr \_ -> TooShort + + { result, rest } + +# Test decode of I64 +expect + actual = Str.toUtf8 "-9223372036854775808" |> Decode.fromBytesPartial json + actual.result == Ok -9_223_372_036_854_775_808i64 + +decodeI128 = Decode.custom \bytes, @Json {} -> + { taken, rest } = takeJsonNumber bytes + + result = + taken + |> Str.fromUtf8 + |> Result.try Str.toI128 + |> Result.mapErr \_ -> TooShort + + { result, rest } + +# Test decode of I128 +# expect +# actual = Str.toUtf8 "-170141183460469231731687303715884105728" |> Decode.fromBytesPartial json +# actual.result == Ok -170_141_183_460_469_231_731_687_303_715_884_105_728i128 + +decodeF32 = Decode.custom \bytes, @Json {} -> + { taken, rest } = takeJsonNumber bytes + + result = + taken + |> Str.fromUtf8 + |> Result.try Str.toF32 + |> Result.mapErr \_ -> TooShort + + { result, rest } + +# Test decode of F32 +expect + actual : DecodeResult F32 + actual = Str.toUtf8 "12.34e-5" |> Decode.fromBytesPartial json + numStr = actual.result |> Result.map Num.toStr + + Result.withDefault numStr "" == "0.00012339999375399202" + +decodeF64 = Decode.custom \bytes, @Json {} -> + { taken, rest } = takeJsonNumber bytes + + result = + taken + |> Str.fromUtf8 + |> Result.try Str.toF64 + |> Result.mapErr \_ -> TooShort + + { result, rest } + +# Test decode of F64 +expect + actual : DecodeResult F64 + actual = Str.toUtf8 "12.34e-5" |> Decode.fromBytesPartial json + numStr = actual.result |> Result.map Num.toStr + + Result.withDefault numStr "" == "0.0001234" + +decodeDec = Decode.custom \bytes, @Json {} -> + { taken, rest } = takeJsonNumber bytes + + result = + taken + |> Str.fromUtf8 + |> Result.try Str.toDec + |> Result.mapErr \_ -> TooShort + + { result, rest } + +# Test decode of Dec +expect + actual : DecodeResult Dec + actual = Str.toUtf8 "12.0034" |> Decode.fromBytesPartial json + + actual.result == Ok 12.0034dec + +decodeBool = Decode.custom \bytes, @Json {} -> + when bytes is + ['f', 'a', 'l', 's', 'e', ..] -> { result: Ok Bool.false, rest: List.dropFirst bytes 5 } + ['t', 'r', 'u', 'e', ..] -> { result: Ok Bool.true, rest: List.dropFirst bytes 4 } + _ -> { result: Err TooShort, rest: bytes } + +# Test decode of Bool +expect + actual = "true\n" |> Str.toUtf8 |> Decode.fromBytesPartial json + expected = Ok Bool.true + actual.result == expected + +# Test decode of Bool +expect + actual = "false ]\n" |> Str.toUtf8 |> Decode.fromBytesPartial json + expected = Ok Bool.false + actual.result == expected + +decodeTuple = \initialState, stepElem, finalizer -> Decode.custom \initialBytes, @Json {} -> + # NB: the stepper function must be passed explicitly until #2894 is resolved. + decodeElems = \stepper, state, index, bytes -> + { val: newState, rest: beforeCommaOrBreak } <- tryDecode + ( + when stepper state index is + TooLong -> + { rest: beforeCommaOrBreak } <- bytes |> anything |> tryDecode + { result: Ok state, rest: beforeCommaOrBreak } + + Next decoder -> + Decode.decodeWith bytes decoder json + ) + + { result: commaResult, rest: nextBytes } = comma beforeCommaOrBreak + + when commaResult is + Ok {} -> decodeElems stepElem newState (index + 1) nextBytes + Err _ -> { result: Ok newState, rest: nextBytes } + + { rest: afterBracketBytes } <- initialBytes |> openBracket |> tryDecode + + { val: endStateResult, rest: beforeClosingBracketBytes } <- decodeElems stepElem initialState 0 afterBracketBytes |> tryDecode + + { rest: afterTupleBytes } <- beforeClosingBracketBytes |> closingBracket |> tryDecode + + when finalizer endStateResult is + Ok val -> { result: Ok val, rest: afterTupleBytes } + Err e -> { result: Err e, rest: afterTupleBytes } + +# Test decode of tuple +expect + input = Str.toUtf8 "[\"The Answer is\",42]" + actual = Decode.fromBytesPartial input json + + actual.result == Ok ("The Answer is", 42) + +parseExactChar : List U8, U8 -> DecodeResult {} +parseExactChar = \bytes, char -> + when List.get bytes 0 is + Ok c -> + if + c == char + then + { result: Ok {}, rest: (List.split bytes 1).others } + else + { result: Err TooShort, rest: bytes } + + Err _ -> { result: Err TooShort, rest: bytes } + +openBracket : List U8 -> DecodeResult {} +openBracket = \bytes -> parseExactChar bytes '[' + +closingBracket : List U8 -> DecodeResult {} +closingBracket = \bytes -> parseExactChar bytes ']' + +anything : List U8 -> DecodeResult {} +anything = \bytes -> { result: Err TooShort, rest: bytes } + +comma : List U8 -> DecodeResult {} +comma = \bytes -> parseExactChar bytes ',' + +tryDecode : DecodeResult a, ({ val : a, rest : List U8 } -> DecodeResult b) -> DecodeResult b +tryDecode = \{ result, rest }, mapper -> + when result is + Ok val -> mapper { val, rest } + Err e -> { result: Err e, rest } + +# JSON NUMBER PRIMITIVE -------------------------------------------------------- + +# Takes the bytes for a valid Json number primitive into a RocStr +# +# Note that this does not handle leading whitespace, any whitespace must be +# handled in json list or record decoding. +# +# |> List.dropIf \b -> b == '+' +# TODO ^^ not needed if roc supports "1e+2", this supports +# "+" which is permitted in Json numbers +# +# |> List.map \b -> if b == 'E' then 'e' else b +# TODO ^^ not needed if roc supports "1E2", this supports +# "E" which is permitted in Json numbers +takeJsonNumber : List U8 -> { taken : List U8, rest : List U8 } +takeJsonNumber = \bytes -> + when List.walkUntil bytes Start numberHelp is + Finish n | Zero n | Integer n | FractionB n | ExponentC n -> + taken = + bytes + |> List.sublist { start: 0, len: n } + |> List.dropIf \b -> b == '+' + |> List.map \b -> if b == 'E' then 'e' else b + + { taken, rest: List.dropFirst bytes n } + + _ -> + { taken: [], rest: bytes } + +numberHelp : NumberState, U8 -> [Continue NumberState, Break NumberState] +numberHelp = \state, byte -> + when (state, byte) is + (Start, b) if b == '0' -> Continue (Zero 1) + (Start, b) if b == '-' -> Continue (Minus 1) + (Start, b) if isDigit1to9 b -> Continue (Integer 1) + (Minus n, b) if b == '0' -> Continue (Zero (n + 1)) + (Minus n, b) if isDigit1to9 b -> Continue (Integer (n + 1)) + (Zero n, b) if b == '.' -> Continue (FractionA (n + 1)) + (Zero n, b) if isValidEnd b -> Break (Finish n) + (Integer n, b) if isDigit0to9 b && n <= maxBytes -> Continue (Integer (n + 1)) + (Integer n, b) if b == '.' && n < maxBytes -> Continue (FractionA (n + 1)) + (Integer n, b) if isValidEnd b && n <= maxBytes -> Break (Finish n) + (FractionA n, b) if isDigit0to9 b && n <= maxBytes -> Continue (FractionB (n + 1)) + (FractionB n, b) if isDigit0to9 b && n <= maxBytes -> Continue (FractionB (n + 1)) + (FractionB n, b) if b == 'e' || b == 'E' && n <= maxBytes -> Continue (ExponentA (n + 1)) + (FractionB n, b) if isValidEnd b && n <= maxBytes -> Break (Finish n) + (ExponentA n, b) if b == '-' || b == '+' && n <= maxBytes -> Continue (ExponentB (n + 1)) + (ExponentA n, b) if isDigit0to9 b && n <= maxBytes -> Continue (ExponentC (n + 1)) + (ExponentB n, b) if isDigit0to9 b && n <= maxBytes -> Continue (ExponentC (n + 1)) + (ExponentC n, b) if isDigit0to9 b && n <= maxBytes -> Continue (ExponentC (n + 1)) + (ExponentC n, b) if isValidEnd b && n <= maxBytes -> Break (Finish n) + _ -> Break Invalid + +NumberState : [ + Start, + Minus Nat, + Zero Nat, + Integer Nat, + FractionA Nat, + FractionB Nat, + ExponentA Nat, + ExponentB Nat, + ExponentC Nat, + Invalid, + Finish Nat, +] + +# TODO confirm if we would like to be able to decode +# "340282366920938463463374607431768211455" which is MAX U128 and 39 bytes +maxBytes : Nat +maxBytes = 21 # Max bytes in a double precision float + +isDigit0to9 : U8 -> Bool +isDigit0to9 = \b -> b >= '0' && b <= '9' + +isDigit1to9 : U8 -> Bool +isDigit1to9 = \b -> b >= '1' && b <= '9' + +isValidEnd : U8 -> Bool +isValidEnd = \b -> + when b is + ']' | ',' | ' ' | '\n' | '\r' | '\t' | '}' -> Bool.true + _ -> Bool.false + +expect + actual = "0.0" |> Str.toUtf8 |> Decode.fromBytes json + expected = Ok 0.0dec + actual == expected + +expect + actual = "0" |> Str.toUtf8 |> Decode.fromBytes json + expected = Ok 0u8 + actual == expected + +expect + actual = "1 " |> Str.toUtf8 |> Decode.fromBytesPartial json + expected = { result: Ok 1dec, rest: [' '] } + actual == expected + +expect + actual = "2]" |> Str.toUtf8 |> Decode.fromBytesPartial json + expected = { result: Ok 2u64, rest: [']'] } + actual == expected + +expect + actual = "30,\n" |> Str.toUtf8 |> Decode.fromBytesPartial json + expected = { result: Ok 30i64, rest: [',', '\n'] } + actual == expected + +expect + actual : DecodeResult U16 + actual = "+1" |> Str.toUtf8 |> Decode.fromBytesPartial json + expected = { result: Err TooShort, rest: ['+', '1'] } + actual == expected + +expect + actual : DecodeResult U16 + actual = ".0" |> Str.toUtf8 |> Decode.fromBytesPartial json + expected = { result: Err TooShort, rest: ['.', '0'] } + actual == expected + +expect + actual : DecodeResult U64 + actual = "-.1" |> Str.toUtf8 |> Decode.fromBytesPartial json + actual.result == Err TooShort + +expect + actual : DecodeResult Dec + actual = "72" |> Str.toUtf8 |> Decode.fromBytesPartial json + expected = Ok 72dec + actual.result == expected + +expect + actual : DecodeResult Dec + actual = "-0" |> Str.toUtf8 |> Decode.fromBytesPartial json + expected = Ok 0dec + actual.result == expected + +expect + actual : DecodeResult Dec + actual = "-7" |> Str.toUtf8 |> Decode.fromBytesPartial json + expected = Ok -7dec + actual.result == expected + +expect + actual : DecodeResult Dec + actual = "-0\n" |> Str.toUtf8 |> Decode.fromBytesPartial json + expected = { result: Ok 0dec, rest: ['\n'] } + actual == expected + +expect + actual : DecodeResult Dec + actual = "123456789000 \n" |> Str.toUtf8 |> Decode.fromBytesPartial json + expected = { result: Ok 123456789000dec, rest: [' ', '\n'] } + actual == expected + +expect + actual : DecodeResult Dec + actual = "-12.03" |> Str.toUtf8 |> Decode.fromBytesPartial json + expected = Ok -12.03 + actual.result == expected + +expect + actual : DecodeResult U64 + actual = "-12." |> Str.toUtf8 |> Decode.fromBytesPartial json + expected = Err TooShort + actual.result == expected + +expect + actual : DecodeResult U64 + actual = "01.1" |> Str.toUtf8 |> Decode.fromBytesPartial json + expected = Err TooShort + actual.result == expected + +expect + actual : DecodeResult U64 + actual = ".0" |> Str.toUtf8 |> Decode.fromBytesPartial json + expected = Err TooShort + actual.result == expected + +expect + actual : DecodeResult U64 + actual = "1.e1" |> Str.toUtf8 |> Decode.fromBytesPartial json + expected = Err TooShort + actual.result == expected + +expect + actual : DecodeResult U64 + actual = "-1.2E" |> Str.toUtf8 |> Decode.fromBytesPartial json + expected = Err TooShort + actual.result == expected + +expect + actual : DecodeResult U64 + actual = "0.1e+" |> Str.toUtf8 |> Decode.fromBytesPartial json + expected = Err TooShort + actual.result == expected + +expect + actual : DecodeResult U64 + actual = "-03" |> Str.toUtf8 |> Decode.fromBytesPartial json + expected = Err TooShort + actual.result == expected + +# JSON STRING PRIMITIVE -------------------------------------------------------- + +# Decode a Json string primitive into a RocStr +# +# Note that decodeStr does not handle leading whitespace, any whitespace must be +# handled in json list or record decodin. +decodeString = Decode.custom \bytes, @Json {} -> + when bytes is + ['n', 'u', 'l', 'l', ..] -> + { result: Ok "null", rest: List.dropFirst bytes 4 } + + _ -> + { taken: strBytes, rest } = takeJsonString bytes + + if List.isEmpty strBytes then + { result: Err TooShort, rest: bytes } + else + # Remove starting and ending quotation marks, replace unicode + # escpapes with Roc equivalent, and try to parse RocStr from + # bytes + result = + strBytes + |> List.sublist { + start: 1, + len: Num.subSaturated (List.len strBytes) 2, + } + |> \bytesWithoutQuotationMarks -> + replaceEscapedChars { inBytes: bytesWithoutQuotationMarks, outBytes: [] } + |> .outBytes + |> Str.fromUtf8 + + when result is + Ok str -> + { result: Ok str, rest } + + Err _ -> + { result: Err TooShort, rest: bytes } + +takeJsonString : List U8 -> { taken : List U8, rest : List U8 } +takeJsonString = \bytes -> + when List.walkUntil bytes Start stringHelp is + Finish n -> + { + taken: List.sublist bytes { start: 0, len: n }, + rest: List.dropFirst bytes n, + } + + _ -> + { taken: [], rest: bytes } + +stringHelp : StringState, U8 -> [Continue StringState, Break StringState] +stringHelp = \state, byte -> + when (state, byte) is + (Start, b) if b == '"' -> Continue (Chars 1) + (Chars n, b) if b == '"' -> Break (Finish (n + 1)) + (Chars n, b) if b == '\\' -> Continue (Escaped (n + 1)) + (Chars n, _) -> Continue (Chars (n + 1)) + (Escaped n, b) if isEscapedChar b -> Continue (Chars (n + 1)) + (Escaped n, b) if b == 'u' -> Continue (UnicodeA (n + 1)) + (UnicodeA n, b) if isHex b -> Continue (UnicodeB (n + 1)) + (UnicodeB n, b) if isHex b -> Continue (UnicodeC (n + 1)) + (UnicodeC n, b) if isHex b -> Continue (UnicodeD (n + 1)) + (UnicodeD n, b) if isHex b -> Continue (Chars (n + 1)) + _ -> Break (InvalidNumber) + +StringState : [ + Start, + Chars Nat, + Escaped Nat, + UnicodeA Nat, + UnicodeB Nat, + UnicodeC Nat, + UnicodeD Nat, + Finish Nat, + InvalidNumber, +] + +isEscapedChar : U8 -> Bool +isEscapedChar = \b -> + when b is + '"' | '\\' | '/' | 'b' | 'f' | 'n' | 'r' | 't' -> Bool.true + _ -> Bool.false + +escapedCharFromJson : U8 -> U8 +escapedCharFromJson = \b -> + when b is + '"' -> 0x22 # U+0022 Quotation mark + '\\' -> 0x5c # U+005c Reverse solidus + '/' -> 0x2f # U+002f Solidus + 'b' -> 0x08 # U+0008 Backspace + 'f' -> 0x0c # U+000c Form feed + 'n' -> 0x0a # U+000a Line feed + 'r' -> 0x0d # U+000d Carriage return + 't' -> 0x09 # U+0009 Tab + _ -> b + +expect escapedCharFromJson 'n' == '\n' + +isHex : U8 -> Bool +isHex = \b -> + (b >= '0' && b <= '9') + || (b >= 'a' && b <= 'f') + || (b >= 'A' && b <= 'F') + +expect isHex '0' && isHex 'f' && isHex 'F' && isHex 'A' && isHex '9' +expect !(isHex 'g' && isHex 'x' && isHex 'u' && isHex '\\' && isHex '-') + +jsonHexToDecimal : U8 -> U8 +jsonHexToDecimal = \b -> + if b >= '0' && b <= '9' then + b - '0' + else if b >= 'a' && b <= 'f' then + b - 'a' + 10 + else if b >= 'A' && b <= 'F' then + b - 'A' + 10 + else + crash "got an invalid hex char" + +expect jsonHexToDecimal '0' == 0 +expect jsonHexToDecimal '9' == 9 +expect jsonHexToDecimal 'a' == 10 +expect jsonHexToDecimal 'A' == 10 +expect jsonHexToDecimal 'f' == 15 +expect jsonHexToDecimal 'F' == 15 + +decimalHexToByte : U8, U8 -> U8 +decimalHexToByte = \upper, lower -> + Num.bitwiseOr (Num.shiftLeftBy upper 4) lower + +expect + actual = decimalHexToByte 3 7 + expected = '7' + actual == expected + +expect + actual = decimalHexToByte 7 4 + expected = 't' + actual == expected + +hexToUtf8 : U8, U8, U8, U8 -> List U8 +hexToUtf8 = \a, b, c, d -> + i = jsonHexToDecimal a + j = jsonHexToDecimal b + k = jsonHexToDecimal c + l = jsonHexToDecimal d + + if i == 0 && j == 0 then + [decimalHexToByte k l] + else + [decimalHexToByte i j, decimalHexToByte k l] + +# Test for \u0074 == U+74 == 't' in Basic Multilingual Plane +expect + actual = hexToUtf8 '0' '0' '7' '4' + expected = ['t'] + actual == expected + +# Test for \u0068 == U+68 == 'h' in Basic Multilingual Plane +expect + actual = hexToUtf8 '0' '0' '6' '8' + expected = ['h'] + actual == expected + +# Test for \u2c64 == U+2C64 == 'Ɽ' in Latin Extended-C +expect + actual = hexToUtf8 '2' 'C' '6' '4' + expected = [44, 100] + actual == expected + +unicodeReplacement = hexToUtf8 'f' 'f' 'd' 'd' + +replaceEscapedChars : { inBytes : List U8, outBytes : List U8 } -> { inBytes : List U8, outBytes : List U8 } +replaceEscapedChars = \{ inBytes, outBytes } -> + + firstByte = List.get inBytes 0 + secondByte = List.get inBytes 1 + inBytesWithoutFirstTwo = List.dropFirst inBytes 2 + inBytesWithoutFirstSix = List.dropFirst inBytes 6 + + when Pair firstByte secondByte is + Pair (Ok a) (Ok b) if a == '\\' && b == 'u' -> + # Extended json unicode escape + when inBytesWithoutFirstTwo is + [c, d, e, f, ..] -> + utf8Bytes = hexToUtf8 c d e f + + replaceEscapedChars { + inBytes: inBytesWithoutFirstSix, + outBytes: List.concat outBytes utf8Bytes, + } + + _ -> + # Invalid Unicode Escape + replaceEscapedChars { + inBytes: inBytesWithoutFirstTwo, + outBytes: List.concat outBytes unicodeReplacement, + } + + Pair (Ok a) (Ok b) if a == '\\' && isEscapedChar b -> + # Shorthand json unicode escape + replaceEscapedChars { + inBytes: inBytesWithoutFirstTwo, + outBytes: List.append outBytes (escapedCharFromJson b), + } + + Pair (Ok a) _ -> + # Process next character + replaceEscapedChars { + inBytes: List.dropFirst inBytes 1, + outBytes: List.append outBytes a, + } + + _ -> + { inBytes, outBytes } + +# Test replacement of both extended and shorthand unicode escapes +expect + inBytes = Str.toUtf8 "\\\\\\u0074\\u0068\\u0065\\t\\u0071\\u0075\\u0069\\u0063\\u006b\\n" + actual = replaceEscapedChars { inBytes, outBytes: [] } + expected = { inBytes: [], outBytes: ['\\', 't', 'h', 'e', '\t', 'q', 'u', 'i', 'c', 'k', '\n'] } + + actual == expected + +# Test decode simple string +expect + input = "\"hello\", " |> Str.toUtf8 + actual = Decode.fromBytesPartial input json + expected = Ok "hello" + + actual.result == expected + +# Test decode string with extended and shorthand json escapes +expect + input = "\"h\\\"\\u0065llo\\n\"]\n" |> Str.toUtf8 + actual = Decode.fromBytesPartial input json + expected = Ok "h\"ello\n" + + actual.result == expected + +# Test json string decoding with escapes +expect + input = Str.toUtf8 "\"a\r\nbc\\txz\"\t\n, " + actual = Decode.fromBytesPartial input json + expected = Ok "a\r\nbc\txz" + + actual.result == expected + +# Test decode of a null +expect + input = Str.toUtf8 "null" + actual = Decode.fromBytesPartial input json + expected = Ok "null" + + actual.result == expected + +# JSON ARRAYS ------------------------------------------------------------------ + +decodeList = \elemDecoder -> Decode.custom \bytes, @Json {} -> + + decodeElems = arrayElemDecoder elemDecoder + + result = + when List.walkUntil bytes (BeforeOpeningBracket 0) arrayOpeningHelp is + AfterOpeningBracket n -> Ok (List.dropFirst bytes n) + _ -> Err ExpectedOpeningBracket + + when result is + Ok elemBytes -> decodeElems elemBytes [] + Err ExpectedOpeningBracket -> + crash "expected opening bracket" + +arrayElemDecoder = \elemDecoder -> + + decodeElems = \bytes, accum -> + + # Done't need a comma before the first element + state = + if List.isEmpty accum then + BeforeNextElement 0 + else + BeforeNextElemOrClosingBracket 0 + + when List.walkUntil bytes state arrayClosingHelp is + AfterClosingBracket n -> + # Eat remaining whitespace + rest = List.dropFirst bytes n + + # Return List of decoded elements + { result: Ok accum, rest } + + BeforeNextElement n -> + # Eat any whitespace before element + elemBytes = List.dropFirst bytes n + + # Decode current element + { result, rest } = Decode.decodeWith elemBytes elemDecoder json + + when result is + Ok elem -> + # Accumulate decoded value and walk to next element + # or the end of the list + decodeElems rest (List.append accum elem) + + Err _ -> + # Unable to decode next element + { result: Err TooShort, rest } + + BeforeNextElemOrClosingBracket _ -> + if List.isEmpty accum then + # Handle empty lists + { result: Ok [], rest: bytes } + else + # Expected comma or closing bracket after last element + { result: Err TooShort, rest: bytes } + + decodeElems + +arrayOpeningHelp : ArrayOpeningState, U8 -> [Continue ArrayOpeningState, Break ArrayOpeningState] +arrayOpeningHelp = \state, byte -> + when (state, byte) is + (BeforeOpeningBracket n, b) if isWhitespace b -> Continue (BeforeOpeningBracket (n + 1)) + (BeforeOpeningBracket n, b) if b == '[' -> Continue (AfterOpeningBracket (n + 1)) + (AfterOpeningBracket n, b) if isWhitespace b -> Continue (AfterOpeningBracket (n + 1)) + _ -> Break state + +arrayClosingHelp : ArrayClosingState, U8 -> [Continue ArrayClosingState, Break ArrayClosingState] +arrayClosingHelp = \state, byte -> + when (state, byte) is + (BeforeNextElemOrClosingBracket n, b) if isWhitespace b -> Continue (BeforeNextElemOrClosingBracket (n + 1)) + (BeforeNextElemOrClosingBracket n, b) if b == ',' -> Continue (BeforeNextElement (n + 1)) + (BeforeNextElemOrClosingBracket n, b) if b == ']' -> Continue (AfterClosingBracket (n + 1)) + (BeforeNextElement n, b) if isWhitespace b -> Continue (BeforeNextElement (n + 1)) + (BeforeNextElement n, b) if b == ']' -> Continue (AfterClosingBracket (n + 1)) + (AfterClosingBracket n, b) if isWhitespace b -> Continue (AfterClosingBracket (n + 1)) + _ -> Break state + +isWhitespace = \b -> + when b is + ' ' | '\n' | '\r' | '\t' -> Bool.true + _ -> Bool.false + +expect + input = ['1', 'a', ' ', '\n', 0x0d, 0x09] + actual = List.map input isWhitespace + expected = [Bool.false, Bool.false, Bool.true, Bool.true, Bool.true, Bool.true] + + actual == expected + +ArrayOpeningState : [ + BeforeOpeningBracket Nat, + AfterOpeningBracket Nat, +] + +ArrayClosingState : [ + BeforeNextElemOrClosingBracket Nat, + BeforeNextElement Nat, + AfterClosingBracket Nat, +] + +# Test decoding an empty array +expect + input = Str.toUtf8 "[ ]" + + actual : DecodeResult (List U8) + actual = Decode.fromBytesPartial input json + + actual.result == Ok [] + +# Test decode array of json numbers with whitespace +expect + input = Str.toUtf8 "\n[\t 1 , 2 , 3]" + + actual : DecodeResult (List U64) + actual = Decode.fromBytesPartial input json + + expected = Ok [1, 2, 3] + + actual.result == expected + +# Test decode array of json strings ignoring whitespace +expect + input = Str.toUtf8 "\n\t [\n \"one\"\r , \"two\" , \n\"3\"\t]" + + actual : DecodeResult (List Str) + actual = Decode.fromBytesPartial input json + expected = Ok ["one", "two", "3"] + + actual.result == expected + +# JSON OBJECTS ----------------------------------------------------------------- + +decodeRecord = \initialState, stepField, finalizer -> Decode.custom \bytes, @Json { fieldNameMapping } -> + + # Recursively build up record from object field:value pairs + decodeFields = \recordState, bytesBeforeField -> + + # Decode the json string field name + { result: objectNameResult, rest: bytesAfterField } = + Decode.decodeWith bytesBeforeField decodeString json + + # Count the bytes until the field value + countBytesBeforeValue = + when List.walkUntil bytesAfterField (BeforeColon 0) objectHelp is + AfterColon n -> n + _ -> 0 + + valueBytes = List.dropFirst bytesAfterField countBytesBeforeValue + + when objectNameResult is + Err TooShort -> + # Invalid object, unable to decode field name or find colon ':' + # after field and before the value + { result: Err TooShort, rest: bytes } + + Ok objectName -> + # Decode the json value + { val: updatedRecord, rest: bytesAfterValue } <- + ( + fieldName = + fromObjectNameUsingMap objectName fieldNameMapping + + # Retrieve value decoder for the current field + when stepField recordState fieldName is + Skip -> + # TODO This doesn't seem right, shouldn't we eat + # the remaining json object value bytes if we are skipping this + # field? + { result: Ok recordState, rest: valueBytes } + + Keep valueDecoder -> + # Decode the value using the decoder from the recordState + # Note we need to pass json config options recursively here + Decode.decodeWith valueBytes valueDecoder (@Json { fieldNameMapping }) + ) + |> tryDecode + + # Check if another field or '}' for end of object + when List.walkUntil bytesAfterValue (AfterObjectValue 0) objectHelp is + ObjectFieldNameStart n -> + rest = List.dropFirst bytesAfterValue n + + # Decode the next field and value + decodeFields updatedRecord rest + + AfterClosingBrace n -> + rest = List.dropFirst bytesAfterValue n + + # Build final record from decoded fields and values + when finalizer updatedRecord is + Ok val -> { result: Ok val, rest } + Err e -> { result: Err e, rest } + + _ -> + # Invalid object + { result: Err TooShort, rest: bytesAfterValue } + + countBytesBeforeFirstField = + when List.walkUntil bytes (BeforeOpeningBrace 0) objectHelp is + ObjectFieldNameStart n -> n + _ -> 0 + + if countBytesBeforeFirstField == 0 then + # Invalid object, expected opening brace '{' followed by a field + { result: Err TooShort, rest: bytes } + else + bytesBeforeFirstField = List.dropFirst bytes countBytesBeforeFirstField + + # Begin decoding field:value pairs + decodeFields initialState bytesBeforeFirstField + +objectHelp : ObjectState, U8 -> [Break ObjectState, Continue ObjectState] +objectHelp = \state, byte -> + when (state, byte) is + (BeforeOpeningBrace n, b) if isWhitespace b -> Continue (BeforeOpeningBrace (n + 1)) + (BeforeOpeningBrace n, b) if b == '{' -> Continue (AfterOpeningBrace (n + 1)) + (AfterOpeningBrace n, b) if isWhitespace b -> Continue (AfterOpeningBrace (n + 1)) + (AfterOpeningBrace n, b) if b == '"' -> Break (ObjectFieldNameStart n) + (BeforeColon n, b) if isWhitespace b -> Continue (BeforeColon (n + 1)) + (BeforeColon n, b) if b == ':' -> Continue (AfterColon (n + 1)) + (AfterColon n, b) if isWhitespace b -> Continue (AfterColon (n + 1)) + (AfterColon n, _) -> Break (AfterColon n) + (AfterObjectValue n, b) if isWhitespace b -> Continue (AfterObjectValue (n + 1)) + (AfterObjectValue n, b) if b == ',' -> Continue (AfterComma (n + 1)) + (AfterObjectValue n, b) if b == '}' -> Continue (AfterClosingBrace (n + 1)) + (AfterComma n, b) if isWhitespace b -> Continue (AfterComma (n + 1)) + (AfterComma n, b) if b == '"' -> Break (ObjectFieldNameStart n) + (AfterClosingBrace n, b) if isWhitespace b -> Continue (AfterClosingBrace (n + 1)) + (AfterClosingBrace n, _) -> Break (AfterClosingBrace n) + _ -> Break InvalidObject + +ObjectState : [ + BeforeOpeningBrace Nat, + AfterOpeningBrace Nat, + ObjectFieldNameStart Nat, + BeforeColon Nat, + AfterColon Nat, + AfterObjectValue Nat, + AfterComma Nat, + AfterClosingBrace Nat, + InvalidObject, +] + +# Test decode of record with two strings ignoring whitespace +expect + input = Str.toUtf8 " {\n\"FruitCount\"\t:2\n, \"OwnerName\": \"Farmer Joe\" } " + decoder = jsonWithOptions { fieldNameMapping: PascalCase } + actual = Decode.fromBytesPartial input decoder + expected = Ok { fruitCount: 2, ownerName: "Farmer Joe" } + + actual.result == expected + +# Test decode of record with an array of strings and a boolean field +expect + input = Str.toUtf8 "{\"fruit-flavours\": [\"Apples\",\"Bananas\",\"Pears\"], \"is-fresh\": true }" + decoder = jsonWithOptions { fieldNameMapping: KebabCase } + actual = Decode.fromBytesPartial input decoder + expected = Ok { fruitFlavours: ["Apples", "Bananas", "Pears"], isFresh: Bool.true } + + actual.result == expected + +# Test decode of record with a string and number field +expect + input = Str.toUtf8 "{\"first_segment\":\"ab\",\"second_segment\":10}" + decoder = jsonWithOptions { fieldNameMapping: SnakeCase } + actual = Decode.fromBytesPartial input decoder + expected = Ok { firstSegment: "ab", secondSegment: 10u8 } + + actual.result == expected + +# Test decode of record of a record +expect + input = Str.toUtf8 "{\"OUTER\":{\"INNER\":\"a\"},\"OTHER\":{\"ONE\":\"b\",\"TWO\":10}}" + decoder = jsonWithOptions { fieldNameMapping: Custom fromYellingCase } + actual = Decode.fromBytesPartial input decoder + expected = Ok { outer: { inner: "a" }, other: { one: "b", two: 10u8 } } + + actual.result == expected + +fromYellingCase = \str -> + Str.graphemes str + |> List.map toLowercase + |> Str.joinWith "" + +expect fromYellingCase "YELLING" == "yelling" + +# Complex example from IETF RFC 8259 (2017) +complexExampleJson = Str.toUtf8 "{\"Image\":{\"Animated\":false,\"Height\":600,\"Ids\":[116,943,234,38793],\"Thumbnail\":{\"Height\":125,\"Url\":\"http:\\/\\/www.example.com\\/image\\/481989943\",\"Width\":100},\"Title\":\"View from 15th Floor\",\"Width\":800}}" +complexExampleRecord = { + image: { + width: 800, + height: 600, + title: "View from 15th Floor", + thumbnail: { + url: "http://www.example.com/image/481989943", + height: 125, + width: 100, + }, + animated: Bool.false, + ids: [116, 943, 234, 38793], + }, +} + +# Test decode of Complex Example +expect + input = complexExampleJson + decoder = jsonWithOptions { fieldNameMapping: PascalCase } + actual = Decode.fromBytes input decoder + expected = Ok complexExampleRecord + + actual == expected + +# Test encode of Complex Example +expect + input = complexExampleRecord + encoder = jsonWithOptions { fieldNameMapping: PascalCase } + actual = Encode.toBytes input encoder + expected = complexExampleJson + + actual == expected + +fromObjectNameUsingMap : Str, FieldNameMapping -> Str +fromObjectNameUsingMap = \objectName, fieldNameMapping -> + when fieldNameMapping is + Default -> objectName + SnakeCase -> fromSnakeCase objectName + PascalCase -> fromPascalCase objectName + KebabCase -> fromKebabCase objectName + CamelCase -> fromCamelCase objectName + Custom transformation -> transformation objectName + +toObjectNameUsingMap : Str, FieldNameMapping -> Str +toObjectNameUsingMap = \fieldName, fieldNameMapping -> + when fieldNameMapping is + Default -> fieldName + SnakeCase -> toSnakeCase fieldName + PascalCase -> toPascalCase fieldName + KebabCase -> toKebabCase fieldName + CamelCase -> toCamelCase fieldName + Custom transformation -> transformation fieldName + +# Convert a `snake_case` JSON Object name to a Roc Field name +fromSnakeCase = \str -> + snakeToCamel str + +# Convert a `PascalCase` JSON Object name to a Roc Field name +fromPascalCase = \str -> + pascalToCamel str + +# Convert a `kabab-case` JSON Object name to a Roc Field name +fromKebabCase = \str -> + kebabToCamel str + +# Convert a `camelCase` JSON Object name to a Roc Field name +fromCamelCase = \str -> + # Nothing to change as Roc field names are camelCase by default + str + +# Convert a `camelCase` Roc Field name to a `snake_case` JSON Object name +toSnakeCase = \str -> + camelToSnake str + +# Convert a `camelCase` Roc Field name to a `PascalCase` JSON Object name +toPascalCase = \str -> + camelToPascal str + +# Convert a `camelCase` Roc Field name to a `kabab-case` JSON Object name +toKebabCase = \str -> + camelToKebeb str + +# Convert a `camelCase` Roc Field name to a `camelCase` JSON Object name +toCamelCase = \str -> + # Nothing to change as Roc field names are camelCase by default + str + +snakeToCamel : Str -> Str +snakeToCamel = \str -> + segments = Str.split str "_" + when segments is + [first, ..] -> + segments + |> List.dropFirst 1 + |> List.map uppercaseFirst + |> List.prepend first + |> Str.joinWith "" + + _ -> str + +expect snakeToCamel "snake_case_string" == "snakeCaseString" + +pascalToCamel : Str -> Str +pascalToCamel = \str -> + segments = Str.graphemes str + when segments is + [a, ..] -> + first = toLowercase a + rest = List.dropFirst segments 1 + + Str.joinWith (List.prepend rest first) "" + + _ -> str + +expect pascalToCamel "PascalCaseString" == "pascalCaseString" + +kebabToCamel : Str -> Str +kebabToCamel = \str -> + segments = Str.split str "-" + when segments is + [first, ..] -> + segments + |> List.dropFirst 1 + |> List.map uppercaseFirst + |> List.prepend first + |> Str.joinWith "" + + _ -> str + +expect kebabToCamel "kebab-case-string" == "kebabCaseString" + +camelToPascal : Str -> Str +camelToPascal = \str -> + segments = Str.graphemes str + when segments is + [a, ..] -> + first = toUppercase a + rest = List.dropFirst segments 1 + + Str.joinWith (List.prepend rest first) "" + + _ -> str + +expect camelToPascal "someCaseString" == "SomeCaseString" + +camelToKebeb : Str -> Str +camelToKebeb = \str -> + rest = Str.graphemes str + taken = List.withCapacity (List.len rest) + + camelToKebabHelp { taken, rest } + |> .taken + |> Str.joinWith "" + +camelToKebabHelp : { taken : List Str, rest : List Str } -> { taken : List Str, rest : List Str } +camelToKebabHelp = \{ taken, rest } -> + when rest is + [] -> { taken, rest } + [a, ..] if isUpperCase a -> + camelToKebabHelp { + taken: List.concat taken ["-", toLowercase a], + rest: List.dropFirst rest 1, + } + + [a, ..] -> + camelToKebabHelp { + taken: List.append taken a, + rest: List.dropFirst rest 1, + } + +expect camelToKebeb "someCaseString" == "some-case-string" + +camelToSnake : Str -> Str +camelToSnake = \str -> + rest = Str.graphemes str + taken = List.withCapacity (List.len rest) + + camelToSnakeHelp { taken, rest } + |> .taken + |> Str.joinWith "" + +camelToSnakeHelp : { taken : List Str, rest : List Str } -> { taken : List Str, rest : List Str } +camelToSnakeHelp = \{ taken, rest } -> + when rest is + [] -> { taken, rest } + [a, ..] if isUpperCase a -> + camelToSnakeHelp { + taken: List.concat taken ["_", toLowercase a], + rest: List.dropFirst rest 1, + } + + [a, ..] -> + camelToSnakeHelp { + taken: List.append taken a, + rest: List.dropFirst rest 1, + } + +expect camelToSnake "someCaseString" == "some_case_string" + +uppercaseFirst : Str -> Str +uppercaseFirst = \str -> + segments = Str.graphemes str + when segments is + [a, ..] -> + first = toUppercase a + rest = List.dropFirst segments 1 + + Str.joinWith (List.prepend rest first) "" + + _ -> str + +toUppercase : Str -> Str +toUppercase = \str -> + when str is + "a" -> "A" + "b" -> "B" + "c" -> "C" + "d" -> "D" + "e" -> "E" + "f" -> "F" + "g" -> "G" + "h" -> "H" + "i" -> "I" + "j" -> "J" + "k" -> "K" + "l" -> "L" + "m" -> "M" + "n" -> "N" + "o" -> "O" + "p" -> "P" + "q" -> "Q" + "r" -> "R" + "s" -> "S" + "t" -> "T" + "u" -> "U" + "v" -> "V" + "w" -> "W" + "x" -> "X" + "y" -> "Y" + "z" -> "Z" + _ -> str + +toLowercase : Str -> Str +toLowercase = \str -> + when str is + "A" -> "a" + "B" -> "b" + "C" -> "c" + "D" -> "d" + "E" -> "e" + "F" -> "f" + "G" -> "g" + "H" -> "h" + "I" -> "i" + "J" -> "j" + "K" -> "k" + "L" -> "l" + "M" -> "m" + "N" -> "n" + "O" -> "o" + "P" -> "p" + "Q" -> "q" + "R" -> "r" + "S" -> "s" + "T" -> "t" + "U" -> "u" + "V" -> "v" + "W" -> "w" + "X" -> "x" + "Y" -> "y" + "Z" -> "z" + _ -> str + +isUpperCase : Str -> Bool +isUpperCase = \str -> + when str is + "A" | "B" | "C" | "D" | "E" | "F" | "G" | "H" | "I" | "J" | "K" | "L" | "M" | "N" | "O" | "P" | "Q" | "R" | "S" | "T" | "U" | "V" | "W" | "X" | "Y" | "Z" -> Bool.true + _ -> Bool.false diff --git a/crates/compiler/builtins/roc/main.roc b/crates/compiler/builtins/roc/main.roc new file mode 100644 index 0000000000..8e73148b6b --- /dev/null +++ b/crates/compiler/builtins/roc/main.roc @@ -0,0 +1,3 @@ +package "builtins" + exposes [Str, Num, Bool, Result, List, Dict, Set, Decode, Encode, Hash, Box, TotallyNotJson] + packages {} diff --git a/crates/compiler/builtins/src/bitcode.rs b/crates/compiler/builtins/src/bitcode.rs new file mode 100644 index 0000000000..39eae1f97b --- /dev/null +++ b/crates/compiler/builtins/src/bitcode.rs @@ -0,0 +1,492 @@ +use roc_module::symbol::Symbol; +use roc_target::TargetInfo; +use std::ops::Index; + +#[derive(Debug, Default, Copy, Clone)] +pub struct IntrinsicName { + pub options: [&'static str; 14], +} + +impl IntrinsicName { + pub const fn default() -> Self { + Self { options: [""; 14] } + } +} + +#[repr(u8)] +pub enum DecWidth { + Dec, +} + +#[repr(u8)] +#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug, PartialOrd, Ord)] +pub enum FloatWidth { + F32, + F64, +} + +impl FloatWidth { + pub const fn stack_size(&self) -> u32 { + use FloatWidth::*; + + // NOTE: this must never use mem::size_of, because that returns the size + // for the target of *the compiler itself* (e.g. this Rust code), not what + // the compiler is targeting (e.g. what the Roc code will be compiled to). + match self { + F32 => 4, + F64 => 8, + } + } + + pub const fn alignment_bytes(&self, target_info: TargetInfo) -> u32 { + use roc_target::Architecture::*; + use FloatWidth::*; + + // NOTE: this must never use mem::align_of, because that returns the alignment + // for the target of *the compiler itself* (e.g. this Rust code), not what + // the compiler is targeting (e.g. what the Roc code will be compiled to). + match self { + F32 => 4, + F64 => match target_info.architecture { + X86_64 | Aarch64 | Wasm32 => 8, + X86_32 | Aarch32 => 4, + }, + } + } + + pub const fn try_from_symbol(symbol: Symbol) -> Option { + match symbol { + Symbol::NUM_F64 | Symbol::NUM_BINARY64 => Some(FloatWidth::F64), + Symbol::NUM_F32 | Symbol::NUM_BINARY32 => Some(FloatWidth::F32), + _ => None, + } + } + + pub const fn type_name(&self) -> &'static str { + match self { + Self::F32 => "f32", + Self::F64 => "f64", + } + } +} + +#[repr(u8)] +#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug, PartialOrd, Ord)] +pub enum IntWidth { + U8 = 0, + U16 = 1, + U32 = 2, + U64 = 3, + U128 = 4, + I8 = 5, + I16 = 6, + I32 = 7, + I64 = 8, + I128 = 9, +} + +impl IntWidth { + pub const fn is_signed(&self) -> bool { + use IntWidth::*; + + matches!(self, I8 | I16 | I32 | I64 | I128) + } + + pub const fn stack_size(&self) -> u32 { + use IntWidth::*; + + // NOTE: this must never use mem::size_of, because that returns the size + // for the target of *the compiler itself* (e.g. this Rust code), not what + // the compiler is targeting (e.g. what the Roc code will be compiled to). + match self { + U8 | I8 => 1, + U16 | I16 => 2, + U32 | I32 => 4, + U64 | I64 => 8, + U128 | I128 => 16, + } + } + + pub const fn alignment_bytes(&self, target_info: TargetInfo) -> u32 { + use roc_target::Architecture; + use IntWidth::*; + + // NOTE: this must never use mem::align_of, because that returns the alignment + // for the target of *the compiler itself* (e.g. this Rust code), not what + // the compiler is targeting (e.g. what the Roc code will be compiled to). + match self { + U8 | I8 => 1, + U16 | I16 => 2, + U32 | I32 => 4, + U64 | I64 => match target_info.architecture { + Architecture::X86_64 + | Architecture::Aarch64 + | Architecture::Aarch32 + | Architecture::Wasm32 => 8, + Architecture::X86_32 => 4, + }, + U128 | I128 => { + // the C ABI defines 128-bit integers to always be 16B aligned, + // according to https://reviews.llvm.org/D28990#655487 + // + // however, rust does not always think that this is true + match target_info.architecture { + Architecture::X86_64 => 16, + Architecture::Aarch64 | Architecture::Aarch32 | Architecture::Wasm32 => 16, + Architecture::X86_32 => 8, + } + } + } + } + + pub const fn try_from_symbol(symbol: Symbol) -> Option { + match symbol { + Symbol::NUM_I128 | Symbol::NUM_SIGNED128 => Some(IntWidth::I128), + Symbol::NUM_I64 | Symbol::NUM_SIGNED64 => Some(IntWidth::I64), + Symbol::NUM_I32 | Symbol::NUM_SIGNED32 => Some(IntWidth::I32), + Symbol::NUM_I16 | Symbol::NUM_SIGNED16 => Some(IntWidth::I16), + Symbol::NUM_I8 | Symbol::NUM_SIGNED8 => Some(IntWidth::I8), + Symbol::NUM_U128 | Symbol::NUM_UNSIGNED128 => Some(IntWidth::U128), + Symbol::NUM_U64 | Symbol::NUM_UNSIGNED64 => Some(IntWidth::U64), + Symbol::NUM_U32 | Symbol::NUM_UNSIGNED32 => Some(IntWidth::U32), + Symbol::NUM_U16 | Symbol::NUM_UNSIGNED16 => Some(IntWidth::U16), + Symbol::NUM_U8 | Symbol::NUM_UNSIGNED8 => Some(IntWidth::U8), + _ => None, + } + } + + pub const fn type_name(&self) -> &'static str { + match self { + Self::I8 => "i8", + Self::I16 => "i16", + Self::I32 => "i32", + Self::I64 => "i64", + Self::I128 => "i128", + Self::U8 => "u8", + Self::U16 => "u16", + Self::U32 => "u32", + Self::U64 => "u64", + Self::U128 => "u128", + } + } +} + +impl Index for IntrinsicName { + type Output = str; + + fn index(&self, _: DecWidth) -> &Self::Output { + self.options[0] + } +} + +impl Index for IntrinsicName { + type Output = str; + + fn index(&self, index: FloatWidth) -> &Self::Output { + match index { + FloatWidth::F32 => self.options[1], + FloatWidth::F64 => self.options[2], + } + } +} + +impl Index for IntrinsicName { + type Output = str; + + fn index(&self, index: IntWidth) -> &Self::Output { + match index { + IntWidth::U8 => self.options[4], + IntWidth::U16 => self.options[5], + IntWidth::U32 => self.options[6], + IntWidth::U64 => self.options[7], + IntWidth::U128 => self.options[8], + IntWidth::I8 => self.options[9], + IntWidth::I16 => self.options[10], + IntWidth::I32 => self.options[11], + IntWidth::I64 => self.options[12], + IntWidth::I128 => self.options[13], + } + } +} + +#[macro_export] +macro_rules! float_intrinsic { + ($name:literal) => {{ + let mut output = IntrinsicName::default(); + + output.options[1] = concat!($name, ".f32"); + output.options[2] = concat!($name, ".f64"); + + output + }}; +} + +#[macro_export] +macro_rules! llvm_int_intrinsic { + ($signed_name:literal, $unsigned_name:literal) => {{ + let mut output = IntrinsicName::default(); + + // The indeces align with the `Index` impl for `IntrinsicName`. + // LLVM uses the same types for both signed and unsigned integers. + output.options[4] = concat!($unsigned_name, ".i8"); + output.options[5] = concat!($unsigned_name, ".i16"); + output.options[6] = concat!($unsigned_name, ".i32"); + output.options[7] = concat!($unsigned_name, ".i64"); + output.options[8] = concat!($unsigned_name, ".i128"); + + output.options[9] = concat!($signed_name, ".i8"); + output.options[10] = concat!($signed_name, ".i16"); + output.options[11] = concat!($signed_name, ".i32"); + output.options[12] = concat!($signed_name, ".i64"); + output.options[13] = concat!($signed_name, ".i128"); + + output + }}; + + ($name:literal) => { + int_intrinsic!($name, $name) + }; +} + +#[macro_export] +macro_rules! int_intrinsic { + ($name:expr) => {{ + let mut output = IntrinsicName::default(); + + // The indices align with the `Index` impl for `IntrinsicName`. + output.options[4] = concat!($name, ".u8"); + output.options[5] = concat!($name, ".u16"); + output.options[6] = concat!($name, ".u32"); + output.options[7] = concat!($name, ".u64"); + output.options[8] = concat!($name, ".u128"); + + output.options[9] = concat!($name, ".i8"); + output.options[10] = concat!($name, ".i16"); + output.options[11] = concat!($name, ".i32"); + output.options[12] = concat!($name, ".i64"); + output.options[13] = concat!($name, ".i128"); + + output + }}; +} + +pub const NUM_SIN: IntrinsicName = float_intrinsic!("roc_builtins.num.sin"); +pub const NUM_COS: IntrinsicName = float_intrinsic!("roc_builtins.num.cos"); +pub const NUM_TAN: IntrinsicName = float_intrinsic!("roc_builtins.num.tan"); +pub const NUM_ASIN: IntrinsicName = float_intrinsic!("roc_builtins.num.asin"); +pub const NUM_ACOS: IntrinsicName = float_intrinsic!("roc_builtins.num.acos"); +pub const NUM_ATAN: IntrinsicName = float_intrinsic!("roc_builtins.num.atan"); +pub const NUM_IS_NAN: IntrinsicName = float_intrinsic!("roc_builtins.num.is_nan"); +pub const NUM_IS_INFINITE: IntrinsicName = float_intrinsic!("roc_builtins.num.is_infinite"); +pub const NUM_IS_FINITE: IntrinsicName = float_intrinsic!("roc_builtins.num.is_finite"); +pub const NUM_LOG: IntrinsicName = float_intrinsic!("roc_builtins.num.log"); +pub const NUM_POW: IntrinsicName = float_intrinsic!("roc_builtins.num.pow"); +pub const NUM_FABS: IntrinsicName = float_intrinsic!("roc_builtins.num.fabs"); +pub const NUM_SQRT: IntrinsicName = float_intrinsic!("roc_builtins.num.sqrt"); + +pub const NUM_POW_INT: IntrinsicName = int_intrinsic!("roc_builtins.num.pow_int"); +pub const NUM_DIV_CEIL: IntrinsicName = int_intrinsic!("roc_builtins.num.div_ceil"); +pub const NUM_CEILING_F32: IntrinsicName = int_intrinsic!("roc_builtins.num.ceiling_f32"); +pub const NUM_CEILING_F64: IntrinsicName = int_intrinsic!("roc_builtins.num.ceiling_f64"); +pub const NUM_FLOOR_F32: IntrinsicName = int_intrinsic!("roc_builtins.num.floor_f32"); +pub const NUM_FLOOR_F64: IntrinsicName = int_intrinsic!("roc_builtins.num.floor_f64"); +pub const NUM_ROUND_F32: IntrinsicName = int_intrinsic!("roc_builtins.num.round_f32"); +pub const NUM_ROUND_F64: IntrinsicName = int_intrinsic!("roc_builtins.num.round_f64"); + +pub const NUM_ADD_OR_PANIC_INT: IntrinsicName = int_intrinsic!("roc_builtins.num.add_or_panic"); +pub const NUM_ADD_SATURATED_INT: IntrinsicName = int_intrinsic!("roc_builtins.num.add_saturated"); +pub const NUM_ADD_CHECKED_INT: IntrinsicName = int_intrinsic!("roc_builtins.num.add_with_overflow"); +pub const NUM_ADD_CHECKED_FLOAT: IntrinsicName = + float_intrinsic!("roc_builtins.num.add_with_overflow"); + +pub const NUM_SUB_OR_PANIC_INT: IntrinsicName = int_intrinsic!("roc_builtins.num.sub_or_panic"); +pub const NUM_SUB_SATURATED_INT: IntrinsicName = int_intrinsic!("roc_builtins.num.sub_saturated"); +pub const NUM_SUB_CHECKED_INT: IntrinsicName = int_intrinsic!("roc_builtins.num.sub_with_overflow"); +pub const NUM_SUB_CHECKED_FLOAT: IntrinsicName = + float_intrinsic!("roc_builtins.num.sub_with_overflow"); + +pub const NUM_MUL_OR_PANIC_INT: IntrinsicName = int_intrinsic!("roc_builtins.num.mul_or_panic"); +pub const NUM_MUL_SATURATED_INT: IntrinsicName = int_intrinsic!("roc_builtins.num.mul_saturated"); +pub const NUM_MUL_WRAP_INT: IntrinsicName = int_intrinsic!("roc_builtins.num.mul_wrapped"); +pub const NUM_MUL_CHECKED_INT: IntrinsicName = int_intrinsic!("roc_builtins.num.mul_with_overflow"); +pub const NUM_MUL_CHECKED_FLOAT: IntrinsicName = + float_intrinsic!("roc_builtins.num.mul_with_overflow"); + +pub const NUM_IS_MULTIPLE_OF: IntrinsicName = int_intrinsic!("roc_builtins.num.is_multiple_of"); + +pub const NUM_SHIFT_RIGHT_ZERO_FILL: IntrinsicName = + int_intrinsic!("roc_builtins.num.shift_right_zero_fill"); + +pub const NUM_COMPARE: IntrinsicName = int_intrinsic!("roc_builtins.num.compare"); +pub const NUM_LESS_THAN: IntrinsicName = int_intrinsic!("roc_builtins.num.less_than"); +pub const NUM_LESS_THAN_OR_EQUAL: IntrinsicName = + int_intrinsic!("roc_builtins.num.less_than_or_equal"); +pub const NUM_GREATER_THAN: IntrinsicName = int_intrinsic!("roc_builtins.num.greater_than"); +pub const NUM_GREATER_THAN_OR_EQUAL: IntrinsicName = + int_intrinsic!("roc_builtins.num.greater_than_or_equal"); + +pub const NUM_COUNT_LEADING_ZERO_BITS: IntrinsicName = + int_intrinsic!("roc_builtins.num.count_leading_zero_bits"); +pub const NUM_COUNT_TRAILING_ZERO_BITS: IntrinsicName = + int_intrinsic!("roc_builtins.num.count_trailing_zero_bits"); +pub const NUM_COUNT_ONE_BITS: IntrinsicName = int_intrinsic!("roc_builtins.num.count_one_bits"); + +pub const NUM_BYTES_TO_U16: &str = "roc_builtins.num.bytes_to_u16"; +pub const NUM_BYTES_TO_U32: &str = "roc_builtins.num.bytes_to_u32"; +pub const NUM_BYTES_TO_U64: &str = "roc_builtins.num.bytes_to_u64"; +pub const NUM_BYTES_TO_U128: &str = "roc_builtins.num.bytes_to_u128"; + +pub const STR_INIT: &str = "roc_builtins.str.init"; +pub const STR_COUNT_SEGMENTS: &str = "roc_builtins.str.count_segments"; +pub const STR_CONCAT: &str = "roc_builtins.str.concat"; +pub const STR_JOIN_WITH: &str = "roc_builtins.str.joinWith"; +pub const STR_SPLIT: &str = "roc_builtins.str.str_split"; +pub const STR_TO_SCALARS: &str = "roc_builtins.str.to_scalars"; +pub const STR_COUNT_GRAPEHEME_CLUSTERS: &str = "roc_builtins.str.count_grapheme_clusters"; +pub const STR_COUNT_UTF8_BYTES: &str = "roc_builtins.str.count_utf8_bytes"; +pub const STR_IS_EMPTY: &str = "roc_builtins.str.is_empty"; +pub const STR_CAPACITY: &str = "roc_builtins.str.capacity"; +pub const STR_STARTS_WITH: &str = "roc_builtins.str.starts_with"; +pub const STR_STARTS_WITH_SCALAR: &str = "roc_builtins.str.starts_with_scalar"; +pub const STR_ENDS_WITH: &str = "roc_builtins.str.ends_with"; +pub const STR_NUMBER_OF_BYTES: &str = "roc_builtins.str.number_of_bytes"; +pub const STR_FROM_INT: IntrinsicName = int_intrinsic!("roc_builtins.str.from_int"); +pub const STR_FROM_FLOAT: IntrinsicName = float_intrinsic!("roc_builtins.str.from_float"); +pub const STR_TO_INT: IntrinsicName = int_intrinsic!("roc_builtins.str.to_int"); +pub const STR_TO_FLOAT: IntrinsicName = float_intrinsic!("roc_builtins.str.to_float"); +pub const STR_TO_DECIMAL: &str = "roc_builtins.str.to_decimal"; +pub const STR_EQUAL: &str = "roc_builtins.str.equal"; +pub const STR_SUBSTRING_UNSAFE: &str = "roc_builtins.str.substring_unsafe"; +pub const STR_TO_UTF8: &str = "roc_builtins.str.to_utf8"; +pub const STR_FROM_UTF8_RANGE: &str = "roc_builtins.str.from_utf8_range"; +pub const STR_REPEAT: &str = "roc_builtins.str.repeat"; +pub const STR_TRIM: &str = "roc_builtins.str.trim"; +pub const STR_TRIM_START: &str = "roc_builtins.str.trim_start"; +pub const STR_TRIM_END: &str = "roc_builtins.str.trim_end"; +pub const STR_GET_UNSAFE: &str = "roc_builtins.str.get_unsafe"; +pub const STR_RESERVE: &str = "roc_builtins.str.reserve"; +pub const STR_APPEND_SCALAR: &str = "roc_builtins.str.append_scalar"; +pub const STR_GET_SCALAR_UNSAFE: &str = "roc_builtins.str.get_scalar_unsafe"; +pub const STR_CLONE_TO: &str = "roc_builtins.str.clone_to"; +pub const STR_WITH_CAPACITY: &str = "roc_builtins.str.with_capacity"; +pub const STR_GRAPHEMES: &str = "roc_builtins.str.graphemes"; +pub const STR_REFCOUNT_PTR: &str = "roc_builtins.str.refcount_ptr"; +pub const STR_RELEASE_EXCESS_CAPACITY: &str = "roc_builtins.str.release_excess_capacity"; + +pub const LIST_MAP: &str = "roc_builtins.list.map"; +pub const LIST_MAP2: &str = "roc_builtins.list.map2"; +pub const LIST_MAP3: &str = "roc_builtins.list.map3"; +pub const LIST_MAP4: &str = "roc_builtins.list.map4"; +pub const LIST_SUBLIST: &str = "roc_builtins.list.sublist"; +pub const LIST_DROP_AT: &str = "roc_builtins.list.drop_at"; +pub const LIST_SWAP: &str = "roc_builtins.list.swap"; +pub const LIST_WITH_CAPACITY: &str = "roc_builtins.list.with_capacity"; +pub const LIST_SORT_WITH: &str = "roc_builtins.list.sort_with"; +pub const LIST_CONCAT: &str = "roc_builtins.list.concat"; +pub const LIST_REPLACE: &str = "roc_builtins.list.replace"; +pub const LIST_REPLACE_IN_PLACE: &str = "roc_builtins.list.replace_in_place"; +pub const LIST_IS_UNIQUE: &str = "roc_builtins.list.is_unique"; +pub const LIST_PREPEND: &str = "roc_builtins.list.prepend"; +pub const LIST_APPEND_UNSAFE: &str = "roc_builtins.list.append_unsafe"; +pub const LIST_RESERVE: &str = "roc_builtins.list.reserve"; +pub const LIST_CAPACITY: &str = "roc_builtins.list.capacity"; +pub const LIST_REFCOUNT_PTR: &str = "roc_builtins.list.refcount_ptr"; +pub const LIST_RELEASE_EXCESS_CAPACITY: &str = "roc_builtins.list.release_excess_capacity"; + +pub const DEC_ABS: &str = "roc_builtins.dec.abs"; +pub const DEC_ACOS: &str = "roc_builtins.dec.acos"; +pub const DEC_ADD_OR_PANIC: &str = "roc_builtins.dec.add_or_panic"; +pub const DEC_ADD_SATURATED: &str = "roc_builtins.dec.add_saturated"; +pub const DEC_ADD_WITH_OVERFLOW: &str = "roc_builtins.dec.add_with_overflow"; +pub const DEC_ASIN: &str = "roc_builtins.dec.asin"; +pub const DEC_ATAN: &str = "roc_builtins.dec.atan"; +pub const DEC_COS: &str = "roc_builtins.dec.cos"; +pub const DEC_DIV: &str = "roc_builtins.dec.div"; +pub const DEC_EQ: &str = "roc_builtins.dec.eq"; +pub const DEC_FROM_F64: &str = "roc_builtins.dec.from_f64"; +pub const DEC_FROM_FLOAT: IntrinsicName = float_intrinsic!("roc_builtins.dec.from_float"); +pub const DEC_FROM_INT: IntrinsicName = int_intrinsic!("roc_builtins.dec.from_int"); +pub const DEC_FROM_STR: &str = "roc_builtins.dec.from_str"; +pub const DEC_FROM_U64: &str = "roc_builtins.dec.from_u64"; +pub const DEC_LOG: &str = "roc_builtins.dec.log"; +pub const DEC_MUL_OR_PANIC: &str = "roc_builtins.dec.mul_or_panic"; +pub const DEC_MUL_SATURATED: &str = "roc_builtins.dec.mul_saturated"; +pub const DEC_MUL_WITH_OVERFLOW: &str = "roc_builtins.dec.mul_with_overflow"; +pub const DEC_NEGATE: &str = "roc_builtins.dec.negate"; +pub const DEC_NEQ: &str = "roc_builtins.dec.neq"; +pub const DEC_SIN: &str = "roc_builtins.dec.sin"; +pub const DEC_SUB_OR_PANIC: &str = "roc_builtins.dec.sub_or_panic"; +pub const DEC_SUB_SATURATED: &str = "roc_builtins.dec.sub_saturated"; +pub const DEC_SUB_WITH_OVERFLOW: &str = "roc_builtins.dec.sub_with_overflow"; +pub const DEC_TAN: &str = "roc_builtins.dec.tan"; +pub const DEC_TO_I128: &str = "roc_builtins.dec.to_i128"; +pub const DEC_TO_STR: &str = "roc_builtins.dec.to_str"; + +pub const UTILS_TEST_PANIC: &str = "roc_builtins.utils.test_panic"; +pub const UTILS_ALLOCATE_WITH_REFCOUNT: &str = "roc_builtins.utils.allocate_with_refcount"; +pub const UTILS_INCREF_RC_PTR: &str = "roc_builtins.utils.incref_rc_ptr"; +pub const UTILS_DECREF_RC_PTR: &str = "roc_builtins.utils.decref_rc_ptr"; +pub const UTILS_FREE_RC_PTR: &str = "roc_builtins.utils.free_rc_ptr"; +pub const UTILS_INCREF_DATA_PTR: &str = "roc_builtins.utils.incref_data_ptr"; +pub const UTILS_DECREF_DATA_PTR: &str = "roc_builtins.utils.decref_data_ptr"; +pub const UTILS_FREE_DATA_PTR: &str = "roc_builtins.utils.free_data_ptr"; +pub const UTILS_IS_UNIQUE: &str = "roc_builtins.utils.is_unique"; +pub const UTILS_DECREF_CHECK_NULL: &str = "roc_builtins.utils.decref_check_null"; +pub const UTILS_DICT_PSEUDO_SEED: &str = "roc_builtins.utils.dict_pseudo_seed"; + +pub const UTILS_EXPECT_FAILED_START_SHARED_BUFFER: &str = + "roc_builtins.utils.expect_failed_start_shared_buffer"; +pub const UTILS_EXPECT_FAILED_START_SHARED_FILE: &str = + "roc_builtins.utils.expect_failed_start_shared_file"; +pub const UTILS_EXPECT_READ_ENV_SHARED_BUFFER: &str = "roc_builtins.utils.read_env_shared_buffer"; +pub const NOTIFY_PARENT_EXPECT: &str = "roc_builtins.utils.notify_parent_expect"; + +pub const UTILS_LONGJMP: &str = "longjmp"; +pub const UTILS_SETJMP: &str = "setjmp"; + +#[derive(Debug, Default)] +pub struct IntToIntrinsicName { + pub options: [IntrinsicName; 10], +} + +impl IntToIntrinsicName { + pub const fn default() -> Self { + Self { + options: [IntrinsicName::default(); 10], + } + } +} + +impl Index for IntToIntrinsicName { + type Output = IntrinsicName; + + fn index(&self, index: IntWidth) -> &Self::Output { + &self.options[index as usize] + } +} + +#[macro_export] +macro_rules! int_to_int_intrinsic { + ($name_prefix:literal, $name_suffix:literal) => {{ + let mut output = IntToIntrinsicName::default(); + + output.options[0] = int_intrinsic!(concat!($name_prefix, "u8", $name_suffix)); + output.options[1] = int_intrinsic!(concat!($name_prefix, "u16", $name_suffix)); + output.options[2] = int_intrinsic!(concat!($name_prefix, "u32", $name_suffix)); + output.options[3] = int_intrinsic!(concat!($name_prefix, "u64", $name_suffix)); + output.options[4] = int_intrinsic!(concat!($name_prefix, "u128", $name_suffix)); + + output.options[5] = int_intrinsic!(concat!($name_prefix, "i8", $name_suffix)); + output.options[6] = int_intrinsic!(concat!($name_prefix, "i16", $name_suffix)); + output.options[7] = int_intrinsic!(concat!($name_prefix, "i32", $name_suffix)); + output.options[8] = int_intrinsic!(concat!($name_prefix, "i64", $name_suffix)); + output.options[9] = int_intrinsic!(concat!($name_prefix, "i128", $name_suffix)); + + output + }}; +} + +pub const NUM_INT_TO_INT_CHECKING_MAX: IntToIntrinsicName = + int_to_int_intrinsic!("roc_builtins.num.int_to_", "_checking_max"); +pub const NUM_INT_TO_INT_CHECKING_MAX_AND_MIN: IntToIntrinsicName = + int_to_int_intrinsic!("roc_builtins.num.int_to_", "_checking_max_and_min"); diff --git a/crates/compiler/builtins/src/lib.rs b/crates/compiler/builtins/src/lib.rs new file mode 100644 index 0000000000..431551ee7f --- /dev/null +++ b/crates/compiler/builtins/src/lib.rs @@ -0,0 +1,6 @@ +//! Provides the Roc functions and modules that are implicitly imported into every module. +#![warn(clippy::dbg_macro)] +// See github.com/roc-lang/roc/issues/800 for discussion of the large_enum_variant check. +#![allow(clippy::large_enum_variant)] +pub mod bitcode; +pub mod roc; diff --git a/crates/compiler/builtins/src/roc.rs b/crates/compiler/builtins/src/roc.rs new file mode 100644 index 0000000000..158c698de2 --- /dev/null +++ b/crates/compiler/builtins/src/roc.rs @@ -0,0 +1,39 @@ +use roc_error_macros::internal_error; +use roc_module::symbol::ModuleId; + +#[inline(always)] +pub fn module_source(module_id: ModuleId) -> &'static str { + match module_id { + ModuleId::RESULT => RESULT, + ModuleId::NUM => NUM, + ModuleId::STR => STR, + ModuleId::LIST => LIST, + ModuleId::DICT => DICT, + ModuleId::SET => SET, + ModuleId::BOX => BOX, + ModuleId::BOOL => BOOL, + ModuleId::ENCODE => ENCODE, + ModuleId::DECODE => DECODE, + ModuleId::HASH => HASH, + ModuleId::INSPECT => INSPECT, + ModuleId::JSON => JSON, + _ => internal_error!( + "ModuleId {:?} is not part of the standard library", + module_id + ), + } +} + +const RESULT: &str = include_str!("../roc/Result.roc"); +const NUM: &str = include_str!("../roc/Num.roc"); +const STR: &str = include_str!("../roc/Str.roc"); +const LIST: &str = include_str!("../roc/List.roc"); +const DICT: &str = include_str!("../roc/Dict.roc"); +const SET: &str = include_str!("../roc/Set.roc"); +const BOX: &str = include_str!("../roc/Box.roc"); +const BOOL: &str = include_str!("../roc/Bool.roc"); +const ENCODE: &str = include_str!("../roc/Encode.roc"); +const DECODE: &str = include_str!("../roc/Decode.roc"); +const HASH: &str = include_str!("../roc/Hash.roc"); +const INSPECT: &str = include_str!("../roc/Inspect.roc"); +const JSON: &str = include_str!("../roc/TotallyNotJson.roc"); diff --git a/crates/compiler/can/Cargo.toml b/crates/compiler/can/Cargo.toml new file mode 100644 index 0000000000..986c5fd732 --- /dev/null +++ b/crates/compiler/can/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "roc_can" +description = "Canonicalize a roc abstract syntax tree, resolving symbols, re-ordering definitions, and preparing a module for type inference." + +authors.workspace = true +edition.workspace = true +license.workspace = true +version.workspace = true + +[dependencies] +roc_collections = { path = "../collections" } +roc_error_macros = { path = "../../error_macros" } +roc_exhaustive = { path = "../exhaustive" } +roc_module = { path = "../module" } +roc_parse = { path = "../parse" } +roc_problem = { path = "../problem" } +roc_region = { path = "../region" } +roc_serialize = { path = "../serialize" } +roc_types = { path = "../types" } + +ven_pretty = { path = "../../vendor/pretty" } + +bitvec.workspace = true +bumpalo.workspace = true +static_assertions.workspace = true + +[dev-dependencies] +indoc.workspace = true +pretty_assertions.workspace = true diff --git a/crates/compiler/can/src/abilities.rs b/crates/compiler/can/src/abilities.rs new file mode 100644 index 0000000000..2d0ee1badb --- /dev/null +++ b/crates/compiler/can/src/abilities.rs @@ -0,0 +1,1373 @@ +use std::num::NonZeroU32; + +use roc_collections::{all::MutMap, VecMap, VecSet}; +use roc_error_macros::internal_error; +use roc_module::symbol::{ModuleId, Symbol}; +use roc_region::all::Region; +use roc_types::{ + subs::Variable, + types::{MemberImpl, Type}, +}; + +/// During type solving and monomorphization, a module must know how its imported ability +/// implementations are resolved - are they derived, or have a concrete implementation? +/// +/// Unfortunately we cannot keep this information opaque, as it's important for properly +/// restoring specialization lambda sets. As such, we need to export implementation information, +/// which is the job of this structure. +pub type ResolvedImplementations = VecMap; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct MemberVariables { + pub able_vars: Vec, + /// This includes - named rigid vars, lambda sets, wildcards. See + /// [`IntroducedVariables::collect_rigid`](crate::annotation::IntroducedVariables::collect_rigid). + pub rigid_vars: Vec, + pub flex_vars: Vec, +} + +/// The member and its signature is defined locally, in the module the store is created for. +/// We need to instantiate and introduce this during solving. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ResolvedMemberType(Variable); + +/// Member type information that needs to be resolved from imports. +#[derive(Debug, Clone)] +pub enum PendingMemberType { + /// The member and its signature is defined locally, in the module the store is created for. + /// We need to instantiate and introduce this during solving. + Local { + signature_var: Variable, + signature: Type, + variables: MemberVariables, + }, + /// The member was defined in another module, so we'll import its variable when it's time to + /// solve. At that point we'll resolve `var` here. + Imported, +} + +pub trait ResolvePhase: std::fmt::Debug + Clone + Copy { + type MemberType: std::fmt::Debug + Clone; +} + +#[derive(Default, Debug, Clone, Copy)] +pub struct Pending; +impl ResolvePhase for Pending { + type MemberType = PendingMemberType; +} + +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] +pub struct Resolved; +impl ResolvePhase for Resolved { + type MemberType = ResolvedMemberType; +} + +/// Stores information about an ability member definition, including the parent ability, the +/// defining type, and what type variables need to be instantiated with instances of the ability. +// TODO: SoA and put me in an arena +#[derive(Debug, Clone, PartialEq)] +pub struct AbilityMemberData { + pub parent_ability: Symbol, + pub region: Region, + pub typ: Phase::MemberType, +} + +impl AbilityMemberData { + pub fn signature_var(&self) -> Variable { + self.typ.0 + } +} + +/// Solved lambda sets for an ability member specialization. For example, if we have +/// +/// Default implements default : {} -[[] + a:default:1]-> a where a implements Default +/// +/// A := {} +/// default = \{} -[[closA]]-> @A {} +/// +/// and this [MemberSpecialization] is for `A`, then there is a mapping of +/// `1` to the variable representing `[[closA]]`. +pub type SpecializationLambdaSets = VecMap; + +/// A particular specialization of an ability member. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct MemberSpecializationInfo { + _phase: std::marker::PhantomData, + pub symbol: Symbol, + pub specialization_lambda_sets: SpecializationLambdaSets, +} + +impl MemberSpecializationInfo { + pub fn new(symbol: Symbol, specialization_lambda_sets: SpecializationLambdaSets) -> Self { + Self { + _phase: Default::default(), + symbol, + specialization_lambda_sets, + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[repr(transparent)] +pub struct SpecializationId(NonZeroU32); + +static_assertions::assert_eq_size!(SpecializationId, Option); + +pub enum SpecializationLambdaSetError {} + +/// A key into a particular implementation of an ability member for an opaque type. +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] +pub struct ImplKey { + pub opaque: Symbol, + pub ability_member: Symbol, +} + +/// Fully-resolved implementation of an ability member for an opaque type. +/// This is only fully known after type solving of the owning module. +#[derive(Clone, Debug)] +pub enum ResolvedImpl { + Impl(MemberSpecializationInfo), + Error, +} + +/// Stores information about what abilities exist in a scope, what it means to implement an +/// ability, and what types implement them. +// TODO(abilities): this should probably go on the Scope, I don't put it there for now because we +// are only dealing with intra-module abilities for now. +// TODO(abilities): many of these should be `VecMap`s. Do some benchmarking. +#[derive(Debug, Clone)] +pub struct IAbilitiesStore { + /// Maps an ability to the members defining it. + members_of_ability: MutMap>, + /// Map of symbols that specialize an ability member to the root ability symbol name, + /// and the type the specialization claims to implement the ability for. + /// + /// For example, in the program + /// + /// Hash implements hash : a -> U64 where a implements Hash + /// + /// Id := {} implements [Hash {hash: myHash}] + /// myHash = \@Id n -> n + /// + /// We keep the mapping myHash->(hash, Id) + specialization_to_root: MutMap, + + /// Information about all members composing abilities. + ability_members: MutMap>, + + /// Maps a tuple (member, type) specifying that `type` implements an ability + /// member `member`, to how that implementation is defined. + declared_implementations: MutMap, + + /// Information about specialized ability member implementations for a type. + specializations: MutMap>, + + next_specialization_id: NonZeroU32, + + /// Resolved specializations for a symbol. These might be ephemeral (known due to type solving), + /// or resolved on-the-fly during mono. + resolved_specializations: MutMap, +} + +impl Default for IAbilitiesStore { + fn default() -> Self { + Self { + members_of_ability: Default::default(), + specialization_to_root: Default::default(), + ability_members: Default::default(), + declared_implementations: Default::default(), + specializations: Default::default(), + next_specialization_id: + // Safety: 1 != 0 + unsafe { NonZeroU32::new_unchecked(1) }, + resolved_specializations: Default::default(), + } + } +} + +pub type AbilitiesStore = IAbilitiesStore; +pub type PendingAbilitiesStore = IAbilitiesStore; + +impl IAbilitiesStore { + /// Records the definition of an ability, including its members. + pub fn register_ability(&mut self, ability: Symbol, members: I) + where + I: IntoIterator)>, + I::IntoIter: ExactSizeIterator, + { + let members = members.into_iter(); + let mut members_vec = Vec::with_capacity(members.len()); + for (member, member_data) in members { + members_vec.push(member); + let old_member = self.ability_members.insert(member, member_data); + debug_assert!(old_member.is_none(), "Replacing existing member definition"); + } + let old_ability = self.members_of_ability.insert(ability, members_vec); + debug_assert!( + old_ability.is_none(), + "Replacing existing ability definition" + ); + } + + /// Checks if `name` is a root ability member symbol name. + /// Note that this will return `false` for specializations of an ability member, which have + /// different symbols from the root. + pub fn is_ability_member_name(&self, name: Symbol) -> bool { + self.ability_members.contains_key(&name) + } + + pub fn is_ability(&self, ability: Symbol) -> bool { + self.members_of_ability.contains_key(&ability) + } + + /// Iterator over all abilities and their members that this store knows about. + pub fn iter_abilities(&self) -> impl Iterator { + self.members_of_ability + .iter() + .map(|(k, v)| (*k, v.as_slice())) + } + + /// Returns information about all known ability members and their root symbols. + pub fn root_ability_members(&self) -> &MutMap> { + &self.ability_members + } + + #[inline(always)] + fn register_one_declared_impl(&mut self, impl_key: ImplKey, member_impl: MemberImpl) { + if let MemberImpl::Impl(specialization_symbol) = member_impl { + self.specialization_to_root + .insert(specialization_symbol, impl_key); + } + self.declared_implementations.insert(impl_key, member_impl); + } + + /// Records the implementations of an ability an opaque type declares to have. + /// + /// Calling this function does not validate that the implementations are correctly specializing + /// in their definition, nor does it store type information about the implementations. + /// + /// It is expected that during type solving, the owner of the abilities store marks the claimed + /// implementation as either a proper or erroring implementation using + /// [`Self::mark_implementation`]. + pub fn register_declared_implementations( + &mut self, + implementing_type: Symbol, + // (ability member, implementation) + implementations: impl IntoIterator, + ) { + for (member, member_impl) in implementations.into_iter() { + let impl_key = ImplKey { + opaque: implementing_type, + ability_member: member, + }; + self.register_one_declared_impl(impl_key, member_impl); + } + } + + /// Returns whether a symbol is declared to specialize an ability member. + pub fn is_specialization_name(&self, symbol: Symbol) -> bool { + self.specialization_to_root.contains_key(&symbol) + } + + pub fn members_of_ability(&self, ability: Symbol) -> Option<&[Symbol]> { + self.members_of_ability.get(&ability).map(|v| v.as_ref()) + } + + pub fn fresh_specialization_id(&mut self) -> SpecializationId { + debug_assert!(self.next_specialization_id.get() != std::u32::MAX); + + let id = SpecializationId(self.next_specialization_id); + // Safety: we already checked this won't overflow, and we started > 0. + self.next_specialization_id = + unsafe { NonZeroU32::new_unchecked(self.next_specialization_id.get() + 1) }; + id + } + + /// Finds the implementation key for a symbol specializing the ability member, if it specializes any. + /// For example, suppose `hashId : Id -> U64` specializes `hash : a -> U64 where a implements Hash`. + /// Calling this with `hashId` would retrieve (hash, hashId). + pub fn impl_key(&self, specializing_symbol: Symbol) -> Option<&ImplKey> { + self.specialization_to_root.get(&specializing_symbol) + } + + /// Answers the question, "does an opaque type claim to implement a particular ability?" + /// + /// Whether the given opaque typ faithfully implements or derives all members of the given ability + /// without errors is not validated. + /// + /// When the given ability is not known to the current store, this call will return `false`. + pub fn has_declared_implementation(&self, opaque: Symbol, ability: Symbol) -> bool { + // Idea: choose an ability member and check whether there is a declared implementation for it. + // During canonicalization, we would have added either all members as declared + // implementations, or none if the opaque doesn't implement the ability. + match self.members_of_ability(ability) { + Some(members) => self.declared_implementations.contains_key(&ImplKey { + opaque, + ability_member: members[0], + }), + None => false, + } + } + + /// Creates a store from [`self`] that closes over the abilities/members given by the + /// imported `symbols`, and their specializations (if any). + pub fn closure_from_imported(&self, symbols: &VecSet) -> PendingAbilitiesStore { + let Self { + members_of_ability, + ability_members, + declared_implementations, + specializations, + + // Covered by `declared_implementations` + specialization_to_root: _, + + // Taking closure for a new module, so specialization IDs can be fresh + next_specialization_id: _, + resolved_specializations: _, + } = self; + + let mut new = PendingAbilitiesStore::default(); + + // 1. Figure out the abilities we need to introduce. + let mut abilities_to_introduce = VecSet::with_capacity(2); + symbols.iter().for_each(|symbol| { + if let Some(member_data) = ability_members.get(symbol) { + // If the symbol is member of an ability, we need to capture the entire ability. + abilities_to_introduce.insert(member_data.parent_ability); + } + if members_of_ability.contains_key(symbol) { + abilities_to_introduce.insert(*symbol); + } + }); + + // 2. Add each ability, and any specializations of its members we know about. + for ability in abilities_to_introduce.into_iter() { + let members = members_of_ability.get(&ability).unwrap(); + let mut imported_member_data = Vec::with_capacity(members.len()); + for member in members { + let AbilityMemberData { + parent_ability, + region, + typ: _, + } = ability_members.get(member).unwrap().clone(); + // All external members need to be marked as imported. We'll figure out their real + // type variables when it comes time to solve the module we're currently importing + // into. + let imported_data = AbilityMemberData { + parent_ability, + region, + typ: PendingMemberType::Imported, + }; + + imported_member_data.push((*member, imported_data)); + } + + new.register_ability(ability, imported_member_data); + + // Add any specializations of the ability's members we know about. + declared_implementations + .iter() + .filter(|(impl_key, _)| members.contains(&impl_key.ability_member)) + .for_each(|(&impl_key, member_impl)| { + new.register_one_declared_impl(impl_key, *member_impl); + + if let MemberImpl::Impl(spec_symbol) = member_impl { + if let Some(specialization_info) = specializations.get(spec_symbol) { + new.import_specialization(specialization_info); + } + } + }); + } + + new + } +} + +#[derive(Debug)] +pub enum MarkError { + NoDeclaredImpl, + ImplIsNotCustom, +} + +impl IAbilitiesStore { + /// Finds the symbol name and ability member definition for a symbol specializing the ability + /// member, if it specializes any. + /// For example, suppose `hashId : Id -> U64` specializes `hash : a -> U64 where a implements Hash`. + /// Calling this with `hashId` would retrieve the ability member data for `hash`, and what type + /// `hashId` is specializing for. + pub fn impl_key_and_def( + &self, + specializing_symbol: Symbol, + ) -> Option<(ImplKey, &AbilityMemberData)> { + let impl_key = self.impl_key(specializing_symbol)?; + debug_assert!(self.ability_members.contains_key(&impl_key.ability_member)); + let root_data = self + .ability_members + .get(&impl_key.ability_member) + .expect("impl keys can only exist for known ability members"); + Some((*impl_key, root_data)) + } + + /// Finds the ability member definition for a member name. + pub fn member_def(&self, member: Symbol) -> Option<&AbilityMemberData> { + self.ability_members.get(&member) + } + + /// Returns an iterator over pairs ((ability member, type), implementation) specifying that + /// the given type implements an ability member. + pub fn iter_declared_implementations( + &self, + ) -> impl Iterator + '_ { + self.declared_implementations.iter().map(|(k, v)| (*k, v)) + } + + /// Retrieves the declared implementation of `member` for `typ`, if it exists. + pub fn get_implementation(&self, impl_key: ImplKey) -> Option<&MemberImpl> { + self.declared_implementations.get(&impl_key) + } + + /// Marks a declared implementation as either properly specializing, or as erroring. + pub fn mark_implementation( + &mut self, + impl_key: ImplKey, + mark: Result, ()>, + ) -> Result<(), MarkError> { + match self.declared_implementations.get_mut(&impl_key) { + Some(member_impl) => match *member_impl { + MemberImpl::Impl(specialization_symbol) => { + debug_assert!(!self.specializations.contains_key(&specialization_symbol)); + + match mark { + Ok(specialization_info) => { + self.specializations + .insert(specialization_symbol, specialization_info); + } + Err(()) => { + // Mark the member implementation as erroring, so we know to generate a + // runtime error function as appropriate. + *member_impl = MemberImpl::Error; + } + } + + Ok(()) + } + MemberImpl::Error => Err(MarkError::ImplIsNotCustom), + }, + None => Err(MarkError::NoDeclaredImpl), + } + } + + pub fn specialization_info( + &self, + specialization_symbol: Symbol, + ) -> Option<&MemberSpecializationInfo> { + self.specializations.get(&specialization_symbol) + } + + pub fn insert_resolved(&mut self, id: SpecializationId, specialization: Symbol) { + let old_specialization = self.resolved_specializations.insert(id, specialization); + + debug_assert!( + old_specialization.is_none(), + "Existing resolution: {old_specialization:?}" + ); + } + + pub fn get_resolved(&self, id: SpecializationId) -> Option { + self.resolved_specializations.get(&id).copied() + } + + pub fn serialize(&self, writer: &mut impl std::io::Write) -> std::io::Result { + serialize::serialize(self, writer) + } + + pub fn deserialize(bytes: &[u8]) -> (Self, usize) { + serialize::deserialize(bytes) + } +} + +pub use serialize::deserialize_solved_implementations; +pub use serialize::serialize_solved_implementations; + +impl IAbilitiesStore { + pub fn import_implementation(&mut self, impl_key: ImplKey, resolved_impl: &ResolvedImpl) { + let member_impl = match resolved_impl { + ResolvedImpl::Impl(specialization) => { + self.import_specialization(specialization); + MemberImpl::Impl(specialization.symbol) + } + ResolvedImpl::Error => MemberImpl::Error, + }; + + let old_declared_impl = self.declared_implementations.insert(impl_key, member_impl); + debug_assert!( + old_declared_impl.is_none() || + // Can happen between we import declared implementations during canonicalization, but + // implementation information only after solving + old_declared_impl.unwrap() == member_impl, + "Replacing existing declared impl: {:?}", + (impl_key, old_declared_impl) + ); + } + + fn import_specialization( + &mut self, + specialization: &MemberSpecializationInfo, + ) { + let MemberSpecializationInfo { + _phase, + symbol, + specialization_lambda_sets, + } = specialization; + + let old_spec = self.specializations.insert( + *symbol, + MemberSpecializationInfo { + _phase: Default::default(), + symbol: *symbol, + specialization_lambda_sets: specialization_lambda_sets.clone(), + }, + ); + debug_assert!(old_spec.is_none(), "Replacing existing specialization"); + } + + pub fn union(&mut self, other: Self) { + let Self { + members_of_ability: other_members_of_ability, + ability_members: mut other_ability_members, + specialization_to_root, + declared_implementations, + next_specialization_id, + resolved_specializations, + specializations, + } = other; + + for (ability, members) in other_members_of_ability.into_iter() { + if let Some(my_members) = self.members_of_ability(ability) { + debug_assert!( + my_members == members, + "Two abilities have different definitions, definitely a bug" + ); + } + let member_data = members + .into_iter() + .map(|member| (member, other_ability_members.remove(&member).unwrap())); + self.register_ability(ability, member_data); + } + + for (specialization, member) in specialization_to_root.into_iter() { + let old_root = self.specialization_to_root.insert(specialization, member); + debug_assert!(old_root.is_none() || old_root.unwrap() == member); + } + + for (impl_key, impl_) in declared_implementations.into_iter() { + let old_impl = self.declared_implementations.insert(impl_key, impl_); + debug_assert!(old_impl.is_none() || old_impl.unwrap() == impl_); + } + + for (symbol, specialization_info) in specializations.into_iter() { + let old_specialization = self + .specializations + .insert(symbol, specialization_info.clone()); + debug_assert!( + old_specialization.is_none() + || old_specialization.unwrap().symbol == specialization_info.symbol + ); + } + + debug_assert_eq!(next_specialization_id.get(), 1); + debug_assert_eq!(self.next_specialization_id.get(), 1); + debug_assert!(resolved_specializations.is_empty()); + debug_assert!(self.resolved_specializations.is_empty()); + } + + pub fn resolve_for_module( + self, + my_module: ModuleId, + my_module_ctx: &mut Ctx, + mut variable_of_symbol: VarOfSymbol, + mut import_lambda_set_var_from_module: ImportVar, + ) -> AbilitiesStore + where + VarOfSymbol: FnMut(&mut Ctx, Symbol) -> Variable, + ImportVar: FnMut(&mut Ctx, ModuleId, Variable) -> Variable, + { + let Self { + members_of_ability, + ability_members, + specialization_to_root, + declared_implementations, + next_specialization_id, + resolved_specializations, + specializations, + } = self; + + let ability_members = ability_members + .into_iter() + .map(|(member_symbol, member_data)| { + let AbilityMemberData { + parent_ability, + region, + typ, + } = member_data; + + let typ = match typ { + PendingMemberType::Local { + signature_var, + signature: _, + variables: _, + } => ResolvedMemberType(signature_var), + PendingMemberType::Imported => { + ResolvedMemberType(variable_of_symbol(my_module_ctx, member_symbol)) + } + }; + + let member_data = AbilityMemberData { + parent_ability, + region, + typ, + }; + + (member_symbol, member_data) + }) + .collect(); + + let specializations = specializations + .into_iter() + .map( + |(symbol, specialization)| { + let MemberSpecializationInfo { + _phase, + symbol: _, + specialization_lambda_sets, + } = specialization; + let symbol_module = symbol.module_id(); + + // NOTE: this totally assumes we're dealing with subs that belong to an + // individual module, things would be badly broken otherwise + let member_specialization = if symbol_module == my_module { + internal_error!("Ability store may only be pending before module solving, \ + so there shouldn't be any known module specializations at this point, but we found one for {:?}", symbol); + } else { + let specialization_lambda_sets = specialization_lambda_sets + .into_iter() + .map(|(region, variable)| { + ( + region, + import_lambda_set_var_from_module( + my_module_ctx, + symbol_module, + variable, + ), + ) + }) + .collect(); + + MemberSpecializationInfo::new(symbol, specialization_lambda_sets) + }; + (symbol, member_specialization) + } + ) + .collect(); + + AbilitiesStore { + members_of_ability, + ability_members, + specialization_to_root, + declared_implementations, + next_specialization_id, + resolved_specializations, + specializations, + } + } +} + +mod serialize { + use roc_collections::{MutMap, VecMap}; + use roc_module::symbol::Symbol; + use roc_region::all::Region; + use roc_serialize::bytes; + use roc_types::{ + subs::{SubsSlice, Variable}, + types::MemberImpl, + }; + + use super::{ + AbilitiesStore, AbilityMemberData, ImplKey, MemberSpecializationInfo, Resolved, + ResolvedImpl, ResolvedImplementations, ResolvedMemberType, SpecializationId, + }; + + use std::io::{self, Write}; + + #[repr(C)] + #[derive(Clone, Copy, Debug)] + struct Header { + members_of_ability: u64, + specialization_to_root: u64, + ability_members: u64, + declared_implementations: u64, + specializations: u64, + next_specialization_id: u64, + resolved_specializations: u64, + } + + impl Header { + fn from_store(store: &AbilitiesStore) -> Self { + let AbilitiesStore { + members_of_ability, + specialization_to_root, + ability_members, + declared_implementations, + specializations, + next_specialization_id, + resolved_specializations, + } = store; + + Self { + members_of_ability: members_of_ability.len() as _, + specialization_to_root: specialization_to_root.len() as _, + ability_members: ability_members.len() as _, + declared_implementations: declared_implementations.len() as _, + specializations: specializations.len() as _, + next_specialization_id: next_specialization_id.get() as _, + resolved_specializations: resolved_specializations.len() as _, + } + } + + fn to_array(self) -> [u8; std::mem::size_of::()] { + // Safety: With repr(c) all fields are in order and properly aligned without padding. + unsafe { std::mem::transmute(self) } + } + + fn from_array(array: [u8; std::mem::size_of::()]) -> Self { + // Safety: With repr(c) all fields are in order and properly aligned without padding. + unsafe { std::mem::transmute(array) } + } + } + + pub(super) fn serialize(store: &AbilitiesStore, writer: &mut impl Write) -> io::Result { + let header = Header::from_store(store).to_array(); + let written = header.len(); + writer.write_all(&header)?; + + let AbilitiesStore { + members_of_ability, + specialization_to_root, + ability_members, + declared_implementations, + specializations, + next_specialization_id: _, // written in the header + resolved_specializations, + } = store; + + let written = serialize_members_of_ability(members_of_ability, writer, written)?; + let written = serialize_specializations_to_root(specialization_to_root, writer, written)?; + let written = serialize_ability_members(ability_members, writer, written)?; + let written = + serialize_declared_implementations(declared_implementations, writer, written)?; + let written = serialize_specializations(specializations, writer, written)?; + let written = + serialize_resolved_specializations(resolved_specializations, writer, written)?; + + Ok(written) + } + + pub(super) fn deserialize(bytes: &[u8]) -> (AbilitiesStore, usize) { + let mut offset = 0; + + let header_slice = &bytes[..std::mem::size_of::
()]; + offset += header_slice.len(); + let header = Header::from_array(header_slice.try_into().unwrap()); + + let (members_of_ability, offset) = + deserialize_members_of_ability(bytes, header.members_of_ability as _, offset); + let (specialization_to_root, offset) = + deserialize_specialization_to_root(bytes, header.specialization_to_root as _, offset); + let (ability_members, offset) = + deserialize_ability_members(bytes, header.ability_members as _, offset); + let (declared_implementations, offset) = deserialize_declared_implementations( + bytes, + header.declared_implementations as _, + offset, + ); + let (specializations, offset) = + deserialize_specializations(bytes, header.specializations as _, offset); + let (resolved_specializations, offset) = deserialize_resolved_specializations( + bytes, + header.resolved_specializations as _, + offset, + ); + + ( + AbilitiesStore { + members_of_ability, + specialization_to_root, + ability_members, + declared_implementations, + specializations, + next_specialization_id: (header.next_specialization_id as u32).try_into().unwrap(), + resolved_specializations, + }, + offset, + ) + } + + fn serialize_members_of_ability( + members_of_ability: &MutMap>, + writer: &mut impl Write, + written: usize, + ) -> io::Result { + bytes::serialize_map( + members_of_ability, + bytes::serialize_slice, + bytes::serialize_slice_of_slices, + writer, + written, + ) + } + + fn deserialize_members_of_ability( + bytes: &[u8], + length: usize, + offset: usize, + ) -> (MutMap>, usize) { + bytes::deserialize_map( + bytes, + bytes::deserialize_vec, + bytes::deserialize_slice_of_slices, + length, + offset, + ) + } + + #[derive(Clone, Copy)] + #[repr(C)] + struct SerImplKey(Symbol, Symbol); + impl From<&ImplKey> for SerImplKey { + fn from(k: &ImplKey) -> Self { + Self(k.opaque, k.ability_member) + } + } + impl From<&SerImplKey> for ImplKey { + fn from(k: &SerImplKey) -> Self { + Self { + opaque: k.0, + ability_member: k.1, + } + } + } + + fn serialize_specializations_to_root( + specialization_to_root: &MutMap, + writer: &mut impl Write, + written: usize, + ) -> io::Result { + bytes::serialize_map( + specialization_to_root, + bytes::serialize_slice, + |keys, writer, written| { + bytes::serialize_slice( + &keys.iter().map(SerImplKey::from).collect::>(), + writer, + written, + ) + }, + writer, + written, + ) + } + + fn deserialize_specialization_to_root( + bytes: &[u8], + length: usize, + offset: usize, + ) -> (MutMap, usize) { + bytes::deserialize_map( + bytes, + bytes::deserialize_vec, + |bytes, length, offset| { + let (slice, offset) = bytes::deserialize_slice::(bytes, length, offset); + (slice.iter().map(ImplKey::from).collect(), offset) + }, + length, + offset, + ) + } + + #[derive(Clone, Copy)] + #[repr(C)] + struct SerMemberData(Symbol, Region, Variable); + impl From<&AbilityMemberData> for SerMemberData { + fn from(k: &AbilityMemberData) -> Self { + Self(k.parent_ability, k.region, k.typ.0) + } + } + impl From<&SerMemberData> for AbilityMemberData { + fn from(k: &SerMemberData) -> Self { + Self { + parent_ability: k.0, + region: k.1, + typ: ResolvedMemberType(k.2), + } + } + } + + fn serialize_ability_members( + ability_members: &MutMap>, + writer: &mut impl Write, + written: usize, + ) -> io::Result { + bytes::serialize_map( + ability_members, + bytes::serialize_slice, + |keys, writer, written| { + bytes::serialize_slice( + &keys.iter().map(SerMemberData::from).collect::>(), + writer, + written, + ) + }, + writer, + written, + ) + } + + fn deserialize_ability_members( + bytes: &[u8], + length: usize, + offset: usize, + ) -> (MutMap>, usize) { + bytes::deserialize_map( + bytes, + bytes::deserialize_vec, + |bytes, length, offset| { + let (slice, offset) = + bytes::deserialize_slice::(bytes, length, offset); + (slice.iter().map(AbilityMemberData::from).collect(), offset) + }, + length, + offset, + ) + } + + #[derive(Clone, Copy)] + #[repr(C)] + enum SerMemberImpl { + Impl(Symbol), + Error, + } + impl From<&MemberImpl> for SerMemberImpl { + fn from(k: &MemberImpl) -> Self { + match k { + MemberImpl::Impl(s) => Self::Impl(*s), + MemberImpl::Error => Self::Error, + } + } + } + impl From<&SerMemberImpl> for MemberImpl { + fn from(k: &SerMemberImpl) -> Self { + match k { + SerMemberImpl::Impl(s) => Self::Impl(*s), + SerMemberImpl::Error => Self::Error, + } + } + } + + fn serialize_declared_implementations( + declared_implementations: &MutMap, + writer: &mut impl Write, + written: usize, + ) -> io::Result { + bytes::serialize_map( + declared_implementations, + bytes::serialize_slice, + |keys, writer, written| { + bytes::serialize_slice( + &keys.iter().map(SerMemberImpl::from).collect::>(), + writer, + written, + ) + }, + writer, + written, + ) + } + + fn deserialize_declared_implementations( + bytes: &[u8], + length: usize, + offset: usize, + ) -> (MutMap, usize) { + bytes::deserialize_map( + bytes, + bytes::deserialize_vec, + |bytes, length, offset| { + let (slice, offset) = + bytes::deserialize_slice::(bytes, length, offset); + (slice.iter().map(MemberImpl::from).collect(), offset) + }, + length, + offset, + ) + } + + #[derive(Clone, Copy)] + #[repr(C)] + struct SerMemberSpecInfo(Symbol, SubsSlice, SubsSlice); + + fn serialize_specializations( + specializations: &MutMap>, + writer: &mut impl Write, + written: usize, + ) -> io::Result { + bytes::serialize_map( + specializations, + bytes::serialize_slice, + |spec_info, writer, written| { + let mut spec_lambda_sets_regions: Vec = Vec::new(); + let mut spec_lambda_sets_vars: Vec = Vec::new(); + let mut ser_member_spec_infos: Vec = Vec::new(); + + for MemberSpecializationInfo { + _phase: _, + symbol, + specialization_lambda_sets, + } in spec_info + { + let regions = SubsSlice::extend_new( + &mut spec_lambda_sets_regions, + specialization_lambda_sets.keys().copied(), + ); + let vars = SubsSlice::extend_new( + &mut spec_lambda_sets_vars, + specialization_lambda_sets.values().copied(), + ); + ser_member_spec_infos.push(SerMemberSpecInfo(*symbol, regions, vars)); + } + + let written = bytes::serialize_slice(&ser_member_spec_infos, writer, written)?; + let written = bytes::serialize_slice(&spec_lambda_sets_regions, writer, written)?; + let written = bytes::serialize_slice(&spec_lambda_sets_vars, writer, written)?; + + Ok(written) + }, + writer, + written, + ) + } + + fn deserialize_specializations( + bytes: &[u8], + length: usize, + offset: usize, + ) -> (MutMap>, usize) { + bytes::deserialize_map( + bytes, + bytes::deserialize_vec, + |bytes, length, offset| { + let (serialized_slices, offset) = + bytes::deserialize_slice::(bytes, length, offset); + + let (regions_slice, offset) = { + let total_items = serialized_slices.iter().map(|s| s.1.len()).sum(); + bytes::deserialize_slice::(bytes, total_items, offset) + }; + + let (vars_slice, offset) = { + let total_items = serialized_slices.iter().map(|s| s.2.len()).sum(); + bytes::deserialize_slice::(bytes, total_items, offset) + }; + + let mut spec_infos: Vec> = + Vec::with_capacity(length); + for SerMemberSpecInfo(symbol, regions, vars) in serialized_slices { + let regions = regions_slice[regions.indices()].to_vec(); + let lset_vars = vars_slice[vars.indices()].to_vec(); + let spec_info = MemberSpecializationInfo::new(*symbol, unsafe { + VecMap::zip(regions, lset_vars) + }); + spec_infos.push(spec_info) + } + + (spec_infos, offset) + }, + length, + offset, + ) + } + + fn serialize_resolved_specializations( + resolved_specializations: &MutMap, + writer: &mut impl Write, + written: usize, + ) -> io::Result { + bytes::serialize_map( + resolved_specializations, + bytes::serialize_slice, + bytes::serialize_slice, + writer, + written, + ) + } + + fn deserialize_resolved_specializations( + bytes: &[u8], + length: usize, + offset: usize, + ) -> (MutMap, usize) { + bytes::deserialize_map( + bytes, + bytes::deserialize_vec, + bytes::deserialize_vec, + length, + offset, + ) + } + + #[derive(Copy, Clone)] + #[repr(C)] + enum SerResolvedImpl { + Impl(SerMemberSpecInfo), + Error, + } + impl SerResolvedImpl { + fn num_regions(&self) -> usize { + match self { + SerResolvedImpl::Impl(spec) => spec.1.len(), + SerResolvedImpl::Error => 0, + } + } + } + + pub fn serialize_solved_implementations( + solved_impls: &ResolvedImplementations, + writer: &mut impl std::io::Write, + ) -> std::io::Result { + let len = solved_impls.len() as u64; + + let written = bytes::serialize_slice(&[len], writer, 0)?; + + bytes::serialize_vec_map( + solved_impls, + |keys, writer, written| { + bytes::serialize_slice( + &keys.iter().map(SerImplKey::from).collect::>(), + writer, + written, + ) + }, + |resolved_impls, writer, written| { + let mut spec_lambda_sets_regions: Vec = Vec::new(); + let mut spec_lambda_sets_vars: Vec = Vec::new(); + let mut ser_resolved_impls: Vec = Vec::new(); + + for resolved_impl in resolved_impls { + let ser_resolved_impl = match resolved_impl { + ResolvedImpl::Impl(MemberSpecializationInfo { + _phase: _, + symbol, + specialization_lambda_sets, + }) => { + let regions = SubsSlice::extend_new( + &mut spec_lambda_sets_regions, + specialization_lambda_sets.keys().copied(), + ); + let vars = SubsSlice::extend_new( + &mut spec_lambda_sets_vars, + specialization_lambda_sets.values().copied(), + ); + SerResolvedImpl::Impl(SerMemberSpecInfo(*symbol, regions, vars)) + } + ResolvedImpl::Error => SerResolvedImpl::Error, + }; + + ser_resolved_impls.push(ser_resolved_impl); + } + + let written = bytes::serialize_slice(&ser_resolved_impls, writer, written)?; + let written = bytes::serialize_slice(&spec_lambda_sets_regions, writer, written)?; + let written = bytes::serialize_slice(&spec_lambda_sets_vars, writer, written)?; + + Ok(written) + }, + writer, + written, + ) + } + + pub fn deserialize_solved_implementations(bytes: &[u8]) -> (ResolvedImplementations, usize) { + let (len_slice, offset) = bytes::deserialize_slice::(bytes, 1, 0); + let length = len_slice[0]; + + bytes::deserialize_vec_map( + bytes, + |bytes, length, offset| { + let (slice, offset) = bytes::deserialize_slice::(bytes, length, offset); + (slice.iter().map(ImplKey::from).collect(), offset) + }, + |bytes, length, offset| { + let (serialized_slices, offset) = + bytes::deserialize_slice::(bytes, length, offset); + + let total_num_regions = serialized_slices.iter().map(|s| s.num_regions()).sum(); + + let (regions_slice, offset) = + bytes::deserialize_slice::(bytes, total_num_regions, offset); + + let (vars_slice, offset) = + { bytes::deserialize_slice::(bytes, total_num_regions, offset) }; + + let mut all_resolved: Vec = Vec::with_capacity(length); + for ser_resolved in serialized_slices { + let resolved = match ser_resolved { + SerResolvedImpl::Impl(SerMemberSpecInfo(symbol, regions, vars)) => { + let regions = regions_slice[regions.indices()].to_vec(); + let lset_vars = vars_slice[vars.indices()].to_vec(); + let spec_info = MemberSpecializationInfo::new(*symbol, unsafe { + VecMap::zip(regions, lset_vars) + }); + ResolvedImpl::Impl(spec_info) + } + SerResolvedImpl::Error => ResolvedImpl::Error, + }; + + all_resolved.push(resolved); + } + + (all_resolved, offset) + }, + length as _, + offset, + ) + } +} + +#[cfg(test)] +mod test { + use roc_collections::VecMap; + use roc_module::symbol::Symbol; + use roc_region::all::Region; + use roc_types::{subs::Variable, types::MemberImpl}; + + use super::{ + AbilitiesStore, AbilityMemberData, ImplKey, MemberSpecializationInfo, ResolvedMemberType, + }; + + #[test] + fn serde_abilities_store() { + let store = { + let mut store = AbilitiesStore::default(); + store.register_ability( + Symbol::ARG_1, + [ + ( + Symbol::ARG_2, + AbilityMemberData { + parent_ability: Symbol::ARG_1, + region: Region::zero(), + typ: ResolvedMemberType(Variable::BOOL), + }, + ), + ( + Symbol::ARG_3, + AbilityMemberData { + parent_ability: Symbol::ARG_1, + region: Region::zero(), + typ: ResolvedMemberType(Variable::BOOL), + }, + ), + ], + ); + store.register_ability( + Symbol::ARG_4, + [( + Symbol::ARG_5, + AbilityMemberData { + parent_ability: Symbol::ARG_4, + region: Region::zero(), + typ: ResolvedMemberType(Variable::BOOL), + }, + )], + ); + + store.register_declared_implementations( + Symbol::ATTR_ATTR, + [ + (Symbol::ARG_2, MemberImpl::Impl(Symbol::ATTR_INVALID)), + (Symbol::ARG_3, MemberImpl::Impl(Symbol::ARG_CLOSURE)), + ], + ); + + store.register_declared_implementations( + Symbol::ATTR_ATTR, + [(Symbol::ARG_5, MemberImpl::Error)], + ); + + store + .mark_implementation( + ImplKey { + opaque: Symbol::ATTR_ATTR, + ability_member: Symbol::ARG_2, + }, + Ok(MemberSpecializationInfo::new(Symbol::UNDERSCORE, { + let mut map = VecMap::default(); + map.insert(1, Variable::BOOL); + map.insert(2, Variable::U8); + map + })), + ) + .unwrap(); + + store + .mark_implementation( + ImplKey { + opaque: Symbol::ATTR_ATTR, + ability_member: Symbol::ARG_3, + }, + Ok(MemberSpecializationInfo::new(Symbol::UNDERSCORE, { + let mut map = VecMap::default(); + map.insert(1, Variable::BOOL); + map.insert(2, Variable::U8); + map.insert(3, Variable::U32); + map.insert(4, Variable::U64); + map + })), + ) + .unwrap(); + + let spec_id1 = store.fresh_specialization_id(); + let spec_id2 = store.fresh_specialization_id(); + + store.insert_resolved(spec_id1, Symbol::ARG_2); + store.insert_resolved(spec_id2, Symbol::ARG_3); + + store + }; + + let mut bytes = Vec::new(); + let written = store.serialize(&mut bytes).unwrap(); + assert_eq!(bytes.len(), written); + + let AbilitiesStore { + members_of_ability, + specialization_to_root, + ability_members, + declared_implementations, + specializations, + next_specialization_id, + resolved_specializations, + } = store; + + let (de_store, offset) = AbilitiesStore::deserialize(&bytes); + + assert_eq!(bytes.len(), offset); + + assert_eq!(members_of_ability, de_store.members_of_ability); + assert_eq!(specialization_to_root, de_store.specialization_to_root); + assert_eq!(ability_members, de_store.ability_members); + assert_eq!(declared_implementations, de_store.declared_implementations); + assert_eq!(specializations, de_store.specializations); + assert_eq!(next_specialization_id, de_store.next_specialization_id); + assert_eq!(resolved_specializations, de_store.resolved_specializations); + } +} diff --git a/crates/compiler/can/src/annotation.rs b/crates/compiler/can/src/annotation.rs new file mode 100644 index 0000000000..e3525f3e23 --- /dev/null +++ b/crates/compiler/can/src/annotation.rs @@ -0,0 +1,1553 @@ +use crate::env::Env; +use crate::procedure::References; +use crate::scope::{PendingAbilitiesInScope, Scope}; +use roc_collections::{ImMap, MutSet, SendMap, VecMap, VecSet}; +use roc_module::ident::{Ident, Lowercase, TagName}; +use roc_module::symbol::Symbol; +use roc_parse::ast::{AssignedField, ExtractSpaces, Pattern, Tag, TypeAnnotation, TypeHeader}; +use roc_problem::can::ShadowKind; +use roc_region::all::{Loc, Region}; +use roc_types::subs::{VarStore, Variable}; +use roc_types::types::{ + name_type_var, AbilitySet, Alias, AliasCommon, AliasKind, AliasVar, ExtImplicitOpenness, + LambdaSet, OptAbleType, OptAbleVar, RecordField, Type, TypeExtension, +}; + +#[derive(Clone, Debug)] +pub struct Annotation { + pub typ: Type, + pub introduced_variables: IntroducedVariables, + pub references: VecSet, + pub aliases: VecMap, +} + +impl Annotation { + pub fn add_to( + &self, + aliases: &mut VecMap, + references: &mut References, + introduced_variables: &mut IntroducedVariables, + ) { + for symbol in self.references.iter() { + references.insert_type_lookup(*symbol); + } + + introduced_variables.union(&self.introduced_variables); + + for (name, alias) in self.aliases.iter() { + if !aliases.contains_key(name) { + aliases.insert(*name, alias.clone()); + } + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub enum NamedOrAbleVariable<'a> { + Named(&'a NamedVariable), + Able(&'a AbleVariable), +} + +impl<'a> NamedOrAbleVariable<'a> { + pub fn first_seen(&self) -> Region { + match self { + NamedOrAbleVariable::Named(nv) => nv.first_seen, + NamedOrAbleVariable::Able(av) => av.first_seen, + } + } + + pub fn name(&self) -> &Lowercase { + match self { + NamedOrAbleVariable::Named(nv) => &nv.name, + NamedOrAbleVariable::Able(av) => &av.name, + } + } + + pub fn variable(&self) -> Variable { + match self { + NamedOrAbleVariable::Named(nv) => nv.variable, + NamedOrAbleVariable::Able(av) => av.variable, + } + } +} + +pub enum OwnedNamedOrAble { + Named(NamedVariable), + Able(AbleVariable), +} + +impl OwnedNamedOrAble { + pub fn first_seen(&self) -> Region { + match self { + OwnedNamedOrAble::Named(nv) => nv.first_seen, + OwnedNamedOrAble::Able(av) => av.first_seen, + } + } + + pub fn ref_name(&self) -> &Lowercase { + match self { + OwnedNamedOrAble::Named(nv) => &nv.name, + OwnedNamedOrAble::Able(av) => &av.name, + } + } + + pub fn name(self) -> Lowercase { + match self { + OwnedNamedOrAble::Named(nv) => nv.name, + OwnedNamedOrAble::Able(av) => av.name, + } + } + + pub fn variable(&self) -> Variable { + match self { + OwnedNamedOrAble::Named(nv) => nv.variable, + OwnedNamedOrAble::Able(av) => av.variable, + } + } + + pub fn opt_abilities(&self) -> Option<&AbilitySet> { + match self { + OwnedNamedOrAble::Named(_) => None, + OwnedNamedOrAble::Able(av) => Some(&av.abilities), + } + } +} + +/// A named type variable, not bound to an ability. +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub struct NamedVariable { + pub variable: Variable, + pub name: Lowercase, + // NB: there may be multiple occurrences of a variable + pub first_seen: Region, +} + +/// A type variable bound to an ability, like "a implements Hash". +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub struct AbleVariable { + pub variable: Variable, + pub name: Lowercase, + pub abilities: AbilitySet, + // NB: there may be multiple occurrences of a variable + pub first_seen: Region, +} + +#[derive(Clone, Debug, Default)] +pub struct IntroducedVariables { + pub wildcards: Vec>, + pub lambda_sets: Vec, + /// Explicit inference variables, i.e. `_` + pub inferred: Vec>, + /// Named type variables + pub named: VecSet, + /// Named type variables bound to an ability + pub able: VecSet, + /// Extension variables which should be inferred in output position. + pub infer_ext_in_output: Vec, + pub host_exposed_aliases: VecMap, +} + +impl IntroducedVariables { + #[inline(always)] + fn debug_assert_not_already_present(&self, var: Variable) { + debug_assert!((self.wildcards.iter().map(|v| &v.value)) + .chain(self.lambda_sets.iter()) + .chain(self.inferred.iter().map(|v| &v.value)) + .chain(self.named.iter().map(|nv| &nv.variable)) + .chain(self.able.iter().map(|av| &av.variable)) + .chain(self.infer_ext_in_output.iter()) + .chain(self.host_exposed_aliases.values()) + .all(|&v| v != var)); + } + + pub fn insert_named(&mut self, name: Lowercase, var: Loc) { + self.debug_assert_not_already_present(var.value); + + let named_variable = NamedVariable { + name, + variable: var.value, + first_seen: var.region, + }; + + self.named.insert(named_variable); + } + + pub fn insert_able(&mut self, name: Lowercase, var: Loc, abilities: AbilitySet) { + self.debug_assert_not_already_present(var.value); + + let able_variable = AbleVariable { + name, + abilities, + variable: var.value, + first_seen: var.region, + }; + + self.able.insert(able_variable); + } + + pub fn insert_wildcard(&mut self, var: Loc) { + self.debug_assert_not_already_present(var.value); + self.wildcards.push(var); + } + + pub fn insert_inferred(&mut self, var: Loc) { + self.debug_assert_not_already_present(var.value); + self.inferred.push(var); + } + + pub fn insert_infer_ext_in_output(&mut self, var: Variable) { + self.debug_assert_not_already_present(var); + self.infer_ext_in_output.push(var); + } + + pub fn insert_lambda_set(&mut self, var: Variable) { + self.debug_assert_not_already_present(var); + self.lambda_sets.push(var); + } + + pub fn insert_host_exposed_alias(&mut self, symbol: Symbol, var: Variable) { + self.debug_assert_not_already_present(var); + self.host_exposed_aliases.insert(symbol, var); + } + + pub fn union(&mut self, other: &Self) { + self.wildcards.extend(other.wildcards.iter().copied()); + self.lambda_sets.extend(other.lambda_sets.iter().copied()); + self.inferred.extend(other.inferred.iter().copied()); + self.host_exposed_aliases + .extend(other.host_exposed_aliases.iter().map(|(k, v)| (*k, *v))); + + self.named.extend(other.named.iter().cloned()); + self.able.extend(other.able.iter().cloned()); + self.infer_ext_in_output + .extend(other.infer_ext_in_output.iter().cloned()); + } + + pub fn union_owned(&mut self, other: Self) { + self.wildcards.extend(other.wildcards); + self.lambda_sets.extend(other.lambda_sets); + self.inferred.extend(other.inferred); + self.host_exposed_aliases.extend(other.host_exposed_aliases); + + self.named.extend(other.named); + self.able.extend(other.able); + self.infer_ext_in_output.extend(other.infer_ext_in_output); + } + + pub fn var_by_name(&self, name: &Lowercase) -> Option { + (self.named.iter().map(|nv| (&nv.name, nv.variable))) + .chain(self.able.iter().map(|av| (&av.name, av.variable))) + .find(|(cand, _)| cand == &name) + .map(|(_, var)| var) + } + + pub fn iter_named(&self) -> impl Iterator { + (self.named.iter().map(NamedOrAbleVariable::Named)) + .chain(self.able.iter().map(NamedOrAbleVariable::Able)) + } + + pub fn named_var_by_name(&self, name: &Lowercase) -> Option { + self.iter_named().find(|v| v.name() == name) + } + + pub fn collect_able(&self) -> Vec { + self.able.iter().map(|av| av.variable).collect() + } + + pub fn collect_rigid(&self) -> Vec { + (self.named.iter().map(|nv| nv.variable)) + .chain(self.wildcards.iter().map(|wc| wc.value)) + // For our purposes, lambda set vars are treated like rigids + .chain(self.lambda_sets.iter().copied()) + .collect() + } + + pub fn collect_flex(&self) -> Vec { + self.inferred.iter().map(|iv| iv.value).collect() + } +} + +fn malformed(env: &mut Env, region: Region, name: &str) { + use roc_problem::can::RuntimeError::*; + + let problem = MalformedTypeName((*name).into(), region); + env.problem(roc_problem::can::Problem::RuntimeError(problem)); +} + +pub(crate) enum AnnotationFor { + Value, + Alias, + Opaque, +} + +/// Canonicalizes a top-level type annotation. +pub(crate) fn canonicalize_annotation( + env: &mut Env, + scope: &mut Scope, + annotation: &TypeAnnotation, + region: Region, + var_store: &mut VarStore, + pending_abilities_in_scope: &PendingAbilitiesInScope, + annotation_for: AnnotationFor, +) -> Annotation { + let mut introduced_variables = IntroducedVariables::default(); + let mut references = VecSet::default(); + let mut aliases = VecMap::default(); + + let (annotation, region) = match annotation { + TypeAnnotation::Where(annotation, clauses) => { + // Add each "implements" clause. The association of a variable to an ability will be saved on + // `introduced_variables`, which we'll process later. + for clause in clauses.iter() { + let opt_err = canonicalize_has_clause( + env, + scope, + var_store, + &mut introduced_variables, + clause, + pending_abilities_in_scope, + &mut references, + ); + if let Err(err_type) = opt_err { + return Annotation { + typ: err_type, + introduced_variables, + references, + aliases, + }; + } + } + (&annotation.value, annotation.region) + } + annot => (annot, region), + }; + + let pol = match annotation_for { + // Values always have positive polarity. + AnnotationFor::Value => CanPolarity::Pos, + AnnotationFor::Alias => CanPolarity::InAlias, + AnnotationFor::Opaque => CanPolarity::InOpaque, + }; + + let typ = can_annotation_help( + env, + pol, + annotation, + region, + scope, + var_store, + &mut introduced_variables, + &mut aliases, + &mut references, + ); + + Annotation { + typ, + introduced_variables, + references, + aliases, + } +} + +#[derive(Clone, Copy, PartialEq, Eq)] +enum CanPolarity { + /// In an alias; polarity should be disregarded for now. + InAlias, + /// In an opaque type; polarity should be disregarded for now. + InOpaque, + Neg, + Pos, +} + +impl CanPolarity { + fn set_neg(self) -> Self { + match self { + CanPolarity::InAlias | CanPolarity::InOpaque => self, + CanPolarity::Neg | CanPolarity::Pos => CanPolarity::Neg, + } + } + + fn set_pos(self) -> Self { + match self { + CanPolarity::InAlias | CanPolarity::InOpaque => self, + CanPolarity::Neg | CanPolarity::Pos => CanPolarity::Pos, + } + } +} + +pub(crate) fn make_apply_symbol( + env: &mut Env, + region: Region, + scope: &mut Scope, + module_name: &str, + ident: &str, +) -> Result { + if module_name.is_empty() { + // Since module_name was empty, this is an unqualified type. + // Look it up in scope! + + match scope.lookup_str(ident, region) { + Ok(symbol) => Ok(symbol), + Err(problem) => { + env.problem(roc_problem::can::Problem::RuntimeError(problem)); + + Err(Type::Error) + } + } + } else { + match env.qualified_lookup(scope, module_name, ident, region) { + Ok(symbol) => Ok(symbol), + Err(problem) => { + // Either the module wasn't imported, or + // it was imported but it doesn't expose this ident. + env.problem(roc_problem::can::Problem::RuntimeError(problem)); + + // A failed import should have already been reported through + // roc_can::env::Env::qualified_lookup's checks + Err(Type::Error) + } + } + } +} + +/// Retrieves all symbols in an annotations that reference a type definition, that is either an +/// alias or an opaque type. +/// +/// For example, in `[A Age U8, B Str {}]`, there are three type definition references - `Age`, +/// `U8`, and `Str`. +pub fn find_type_def_symbols( + scope: &mut Scope, + initial_annotation: &roc_parse::ast::TypeAnnotation, +) -> Vec { + use roc_parse::ast::TypeAnnotation::*; + + let mut result = Vec::new(); + + let mut stack = vec![initial_annotation]; + + while let Some(annotation) = stack.pop() { + match annotation { + Apply(_module_name, ident, arguments) => { + let ident: Ident = (*ident).into(); + let symbol = scope.scopeless_symbol(&ident, Region::zero()); + + result.push(symbol); + + for t in arguments.iter() { + stack.push(&t.value); + } + } + Function(arguments, result) => { + for t in arguments.iter() { + stack.push(&t.value); + } + + stack.push(&result.value); + } + BoundVariable(_) => {} + As(actual, _, _) => { + stack.push(&actual.value); + } + Tuple { elems, ext } => { + stack.extend(elems.iter().map(|t| &t.value)); + stack.extend(ext.iter().map(|t| &t.value)); + } + Record { fields, ext } => { + let mut inner_stack = Vec::with_capacity(fields.items.len()); + + for field in fields.items.iter() { + inner_stack.push(&field.value) + } + + while let Some(assigned_field) = inner_stack.pop() { + match assigned_field { + AssignedField::RequiredValue(_, _, t) + | AssignedField::OptionalValue(_, _, t) => { + stack.push(&t.value); + } + AssignedField::LabelOnly(_) => {} + AssignedField::SpaceBefore(inner, _) + | AssignedField::SpaceAfter(inner, _) => inner_stack.push(inner), + AssignedField::Malformed(_) => {} + } + } + + for t in ext.iter() { + stack.push(&t.value); + } + } + TagUnion { ext, tags } => { + let mut inner_stack = Vec::with_capacity(tags.items.len()); + + for tag in tags.items.iter() { + inner_stack.push(&tag.value) + } + + while let Some(tag) = inner_stack.pop() { + match tag { + Tag::Apply { args, .. } => { + for t in args.iter() { + stack.push(&t.value); + } + } + Tag::SpaceBefore(inner, _) | Tag::SpaceAfter(inner, _) => { + inner_stack.push(inner) + } + Tag::Malformed(_) => {} + } + } + + for t in ext.iter() { + stack.push(&t.value); + } + } + SpaceBefore(inner, _) | SpaceAfter(inner, _) => { + stack.push(inner); + } + Where(annotation, clauses) => { + stack.push(&annotation.value); + + for has_clause in clauses.iter() { + for ab in has_clause.value.abilities { + stack.push(&ab.value); + } + } + } + Inferred | Wildcard | Malformed(_) => {} + } + } + + result +} + +fn find_fresh_var_name(introduced_variables: &IntroducedVariables) -> Lowercase { + name_type_var("", 0, &mut introduced_variables.iter_named(), |var, str| { + var.name().as_str() == str + }) + .0 +} + +#[allow(clippy::too_many_arguments)] +fn can_annotation_help( + env: &mut Env, + pol: CanPolarity, + annotation: &roc_parse::ast::TypeAnnotation, + region: Region, + scope: &mut Scope, + var_store: &mut VarStore, + introduced_variables: &mut IntroducedVariables, + local_aliases: &mut VecMap, + references: &mut VecSet, +) -> Type { + use roc_parse::ast::TypeAnnotation::*; + + match annotation { + Function(argument_types, return_type) => { + let mut args = Vec::new(); + + for arg in *argument_types { + let arg_ann = can_annotation_help( + env, + pol.set_neg(), + &arg.value, + arg.region, + scope, + var_store, + introduced_variables, + local_aliases, + references, + ); + + args.push(arg_ann); + } + + let ret = can_annotation_help( + env, + pol.set_pos(), + &return_type.value, + return_type.region, + scope, + var_store, + introduced_variables, + local_aliases, + references, + ); + + let lambda_set = var_store.fresh(); + introduced_variables.insert_lambda_set(lambda_set); + let closure = Type::Variable(lambda_set); + + Type::Function(args, Box::new(closure), Box::new(ret)) + } + Apply(module_name, ident, type_arguments) => { + let symbol = match make_apply_symbol(env, region, scope, module_name, ident) { + Err(problem) => return problem, + Ok(symbol) => symbol, + }; + + let mut args = Vec::new(); + + references.insert(symbol); + + if scope.abilities_store.is_ability(symbol) { + let fresh_ty_var = find_fresh_var_name(introduced_variables); + + env.problem(roc_problem::can::Problem::AbilityUsedAsType( + fresh_ty_var.clone(), + symbol, + region, + )); + + // Generate an variable bound to the ability so we can keep compiling. + let var = var_store.fresh(); + introduced_variables.insert_able( + fresh_ty_var, + Loc::at(region, var), + AbilitySet::singleton(symbol), + ); + return Type::Variable(var); + } + + for arg in *type_arguments { + let arg_ann = can_annotation_help( + env, + pol, + &arg.value, + arg.region, + scope, + var_store, + introduced_variables, + local_aliases, + references, + ); + + args.push(Loc::at(arg.region, arg_ann)); + } + + match scope.lookup_alias(symbol) { + Some(alias) => { + // use a known alias + + if alias.type_variables.len() != args.len() { + env.problem(roc_problem::can::Problem::BadTypeArguments { + symbol, + region, + alias_needs: alias.type_variables.len() as u8, + type_got: args.len() as u8, + alias_kind: alias.kind, + }); + return Type::Error; + } + + let mut type_var_to_arg = Vec::new(); + + for (alias_arg, arg_ann) in alias.type_variables.iter().zip(args) { + type_var_to_arg.push(Loc::at( + arg_ann.region, + OptAbleType { + typ: arg_ann.value, + opt_abilities: alias_arg.value.opt_bound_abilities.clone(), + }, + )); + } + + let mut lambda_set_variables = + Vec::with_capacity(alias.lambda_set_variables.len()); + + for _ in 0..alias.lambda_set_variables.len() { + let lvar = var_store.fresh(); + + introduced_variables.insert_lambda_set(lvar); + + lambda_set_variables.push(LambdaSet(Type::Variable(lvar))); + } + + let mut infer_ext_in_output_types = + Vec::with_capacity(alias.infer_ext_in_output_variables.len()); + for _ in 0..alias.infer_ext_in_output_variables.len() { + // Unfortunately the polarity might still be undetermined at this point, + // since this might be a delayed alias inside an alias. In these cases + // generate fresh variables to hold the extension-variables-to-be-inferred, + // which will be instantiated when the alias is used at a concrete site. + // Otherwise, instantiate the variables with how they should behave based + // on the polarity + let typ = match pol { + CanPolarity::InAlias | CanPolarity::Pos => { + let var = var_store.fresh(); + introduced_variables.insert_infer_ext_in_output(var); + Type::Variable(var) + } + // TODO: determine for opaques + CanPolarity::InOpaque => Type::EmptyTagUnion, + CanPolarity::Neg => Type::EmptyTagUnion, + }; + infer_ext_in_output_types.push(typ); + } + + Type::DelayedAlias(AliasCommon { + symbol, + type_arguments: type_var_to_arg, + lambda_set_variables, + infer_ext_in_output_types, + }) + } + None => Type::Apply(symbol, args, region), + } + } + BoundVariable(v) => { + let name = Lowercase::from(*v); + + match introduced_variables.var_by_name(&name) { + Some(var) => Type::Variable(var), + None => { + let var = var_store.fresh(); + + introduced_variables.insert_named(name, Loc::at(region, var)); + + Type::Variable(var) + } + } + } + As( + loc_inner, + _spaces, + alias_header @ TypeHeader { + name, + vars: loc_vars, + }, + ) => { + let symbol = match scope.introduce(name.value.into(), region) { + Ok(symbol) => symbol, + + Err((shadowed_symbol, shadow, _new_symbol)) => { + env.problem(roc_problem::can::Problem::Shadowing { + original_region: shadowed_symbol.region, + shadow, + kind: ShadowKind::Variable, + }); + + return Type::Error; + } + }; + + let inner_type = can_annotation_help( + env, + CanPolarity::InOpaque, + &loc_inner.value, + region, + scope, + var_store, + introduced_variables, + local_aliases, + references, + ); + let mut vars = Vec::with_capacity(loc_vars.len()); + let mut lowercase_vars: Vec> = Vec::with_capacity(loc_vars.len()); + + references.insert(symbol); + + for loc_var in *loc_vars { + let var = match loc_var.value { + Pattern::Identifier(name) if name.chars().next().unwrap().is_lowercase() => { + name + } + _ => unreachable!("I thought this was validated during parsing"), + }; + let var_name = Lowercase::from(var); + + // TODO(abilities): check that there are no abilities bound here. + if let Some(var) = introduced_variables.var_by_name(&var_name) { + vars.push(Type::Variable(var)); + lowercase_vars.push(Loc::at( + loc_var.region, + AliasVar { + name: var_name, + var, + opt_bound_abilities: None, + }, + )); + } else { + let var = var_store.fresh(); + + introduced_variables + .insert_named(var_name.clone(), Loc::at(loc_var.region, var)); + vars.push(Type::Variable(var)); + + lowercase_vars.push(Loc::at( + loc_var.region, + AliasVar { + name: var_name, + var, + opt_bound_abilities: None, + }, + )); + } + } + + let alias_args = vars.clone(); + + let alias_actual = if let Type::TagUnion(tags, ext) = inner_type { + let rec_var = var_store.fresh(); + + let mut new_tags = Vec::with_capacity(tags.len()); + let mut is_nested_datatype = false; + for (tag_name, args) in tags { + let mut new_args = Vec::with_capacity(args.len()); + for arg in args { + let mut new_arg = arg.clone(); + let substitution_result = + new_arg.substitute_alias(symbol, &alias_args, &Type::Variable(rec_var)); + + if let Err(differing_recursion_region) = substitution_result { + env.problems + .push(roc_problem::can::Problem::NestedDatatype { + alias: symbol, + def_region: alias_header.region(), + differing_recursion_region, + }); + is_nested_datatype = true; + } + + // Either way, add the argument; not doing so would only result in more + // confusing error messages later on. + new_args.push(new_arg); + } + new_tags.push((tag_name.clone(), new_args)); + } + if is_nested_datatype { + // We don't have a way to represent nested data types; hence, we don't actually + // use the recursion var in them, and should avoid marking them as such. + Type::TagUnion(new_tags, ext) + } else { + Type::RecursiveTagUnion(rec_var, new_tags, ext) + } + } else { + inner_type + }; + + let mut hidden_variables = MutSet::default(); + hidden_variables.extend(alias_actual.variables()); + + for loc_var in lowercase_vars.iter() { + hidden_variables.remove(&loc_var.value.var); + } + + // TODO: handle implicit ext variables in `as` aliases + let infer_ext_in_output = vec![]; + + scope.add_alias( + symbol, + region, + lowercase_vars, + infer_ext_in_output, + alias_actual, + AliasKind::Structural, // aliases in "as" are never opaque + ); + + let alias = scope.lookup_alias(symbol).unwrap(); + local_aliases.insert(symbol, alias.clone()); + + Type::Alias { + symbol, + type_arguments: vars.into_iter().map(OptAbleType::unbound).collect(), + lambda_set_variables: alias.lambda_set_variables.clone(), + infer_ext_in_output_types: alias + .infer_ext_in_output_variables + .iter() + .map(|v| Type::Variable(*v)) + .collect(), + actual: Box::new(alias.typ.clone()), + kind: alias.kind, + } + } + + Tuple { elems, ext } => { + let (ext_type, is_implicit_openness) = can_extension_type( + env, + pol, + scope, + var_store, + introduced_variables, + local_aliases, + references, + ext, + roc_problem::can::ExtensionTypeKind::Record, + ); + + debug_assert!( + matches!(is_implicit_openness, ExtImplicitOpenness::No), + "tuples should never be implicitly inferred open" + ); + + debug_assert!(!elems.is_empty()); // We don't allow empty tuples + + let elem_types = can_assigned_tuple_elems( + env, + pol, + &elems.items, + scope, + var_store, + introduced_variables, + local_aliases, + references, + ); + + Type::Tuple( + elem_types, + TypeExtension::from_type(ext_type, is_implicit_openness), + ) + } + Record { fields, ext } => { + let (ext_type, is_implicit_openness) = can_extension_type( + env, + pol, + scope, + var_store, + introduced_variables, + local_aliases, + references, + ext, + roc_problem::can::ExtensionTypeKind::Record, + ); + + debug_assert!( + matches!(is_implicit_openness, ExtImplicitOpenness::No), + "records should never be implicitly inferred open" + ); + + if fields.is_empty() { + match ext { + Some(_) => { + // just `a` does not mean the same as `{}a`, so even + // if there are no fields, still make this a `Record`, + // not an EmptyRec + Type::Record( + Default::default(), + TypeExtension::from_type(ext_type, is_implicit_openness), + ) + } + + None => Type::EmptyRec, + } + } else { + let field_types = can_assigned_fields( + env, + pol, + &fields.items, + region, + scope, + var_store, + introduced_variables, + local_aliases, + references, + ); + + Type::Record( + field_types, + TypeExtension::from_type(ext_type, is_implicit_openness), + ) + } + } + TagUnion { tags, ext, .. } => { + let (ext_type, is_implicit_openness) = can_extension_type( + env, + pol, + scope, + var_store, + introduced_variables, + local_aliases, + references, + ext, + roc_problem::can::ExtensionTypeKind::TagUnion, + ); + + if tags.is_empty() { + match ext { + Some(_) => { + // just `a` does not mean the same as `[]`, so even + // if there are no fields, still make this a `TagUnion`, + // not an EmptyTagUnion + Type::TagUnion( + Default::default(), + TypeExtension::from_type(ext_type, is_implicit_openness), + ) + } + + None => Type::EmptyTagUnion, + } + } else { + let mut tag_types = can_tags( + env, + pol, + tags.items, + region, + scope, + var_store, + introduced_variables, + local_aliases, + references, + ); + + // sort here; we later instantiate type aliases, so this type might get duplicated + // many times. Then, when inserting into the subs, the tags are sorted. + // in theory we save a lot of time by sorting once here + insertion_sort_by(&mut tag_types, |a, b| a.0.cmp(&b.0)); + + Type::TagUnion( + tag_types, + TypeExtension::from_type(ext_type, is_implicit_openness), + ) + } + } + SpaceBefore(nested, _) | SpaceAfter(nested, _) => can_annotation_help( + env, + pol, + nested, + region, + scope, + var_store, + introduced_variables, + local_aliases, + references, + ), + Wildcard => { + let var = var_store.fresh(); + + introduced_variables.insert_wildcard(Loc::at(region, var)); + + Type::Variable(var) + } + Inferred => { + // Inference variables aren't bound to a rigid or a wildcard, so all we have to do is + // make a fresh unconstrained variable, and let the type solver fill it in for us 🤠 + let var = var_store.fresh(); + + introduced_variables.insert_inferred(Loc::at(region, var)); + + Type::Variable(var) + } + Where(_annotation, clauses) => { + debug_assert!(!clauses.is_empty()); + + // Implements clauses are allowed only on the top level of a signature, which we handle elsewhere. + env.problem(roc_problem::can::Problem::IllegalImplementsClause { + region: Region::across_all(clauses.iter().map(|clause| &clause.region)), + }); + + Type::Error + } + Malformed(string) => { + malformed(env, region, string); + + let var = var_store.fresh(); + + introduced_variables.insert_wildcard(Loc::at(region, var)); + + Type::Variable(var) + } + } +} + +fn canonicalize_has_clause( + env: &mut Env, + scope: &mut Scope, + var_store: &mut VarStore, + introduced_variables: &mut IntroducedVariables, + clause: &Loc>, + pending_abilities_in_scope: &PendingAbilitiesInScope, + references: &mut VecSet, +) -> Result<(), Type> { + let Loc { + region, + value: roc_parse::ast::ImplementsClause { var, abilities }, + } = clause; + let region = *region; + + let var_name = var.extract_spaces().item; + debug_assert!( + var_name.starts_with(char::is_lowercase), + "Vars should have been parsed as lowercase" + ); + let var_name = Lowercase::from(var_name); + + let mut can_abilities = AbilitySet::with_capacity(abilities.len()); + for &Loc { + region, + value: ability, + } in *abilities + { + let ability = match ability { + TypeAnnotation::Apply(module_name, ident, _type_arguments) => { + let symbol = make_apply_symbol(env, region, scope, module_name, ident)?; + + // Ability defined locally, whose members we are constructing right now... + if !pending_abilities_in_scope.contains_key(&symbol) + // or an ability that was imported from elsewhere + && !scope.abilities_store.is_ability(symbol) + { + env.problem(roc_problem::can::Problem::ImplementsClauseIsNotAbility { region }); + return Err(Type::Error); + } + symbol + } + _ => { + env.problem(roc_problem::can::Problem::ImplementsClauseIsNotAbility { region }); + return Err(Type::Error); + } + }; + + references.insert(ability); + let already_seen = can_abilities.insert(ability); + + if already_seen { + env.problem(roc_problem::can::Problem::DuplicateImplementsAbility { ability, region }); + } + } + + if let Some(shadowing) = introduced_variables.named_var_by_name(&var_name) { + let var_name_ident = var_name.to_string().into(); + let shadow = Loc::at(region, var_name_ident); + env.problem(roc_problem::can::Problem::Shadowing { + original_region: shadowing.first_seen(), + shadow, + kind: ShadowKind::Variable, + }); + return Err(Type::Error); + } + + let var = var_store.fresh(); + + introduced_variables.insert_able(var_name, Loc::at(region, var), can_abilities); + + Ok(()) +} + +#[allow(clippy::too_many_arguments)] +fn can_extension_type( + env: &mut Env, + pol: CanPolarity, + scope: &mut Scope, + var_store: &mut VarStore, + introduced_variables: &mut IntroducedVariables, + local_aliases: &mut VecMap, + references: &mut VecSet, + opt_ext: &Option<&Loc>, + ext_problem_kind: roc_problem::can::ExtensionTypeKind, +) -> (Type, ExtImplicitOpenness) { + fn valid_record_ext_type(typ: &Type) -> bool { + // Include erroneous types so that we don't overreport errors. + matches!( + typ, + Type::EmptyRec | Type::Record(..) | Type::Variable(..) | Type::Error + ) + } + fn valid_tag_ext_type(typ: &Type) -> bool { + matches!( + typ, + Type::EmptyTagUnion | Type::TagUnion(..) | Type::Variable(..) | Type::Error + ) + } + + use roc_problem::can::ExtensionTypeKind; + + let valid_extension_type: fn(&Type) -> bool = match ext_problem_kind { + ExtensionTypeKind::Record => valid_record_ext_type, + ExtensionTypeKind::TagUnion => valid_tag_ext_type, + }; + + match opt_ext { + Some(loc_ann) => { + let ext_type = can_annotation_help( + env, + pol, + &loc_ann.value, + loc_ann.region, + scope, + var_store, + introduced_variables, + local_aliases, + references, + ); + if valid_extension_type(shallow_dealias_with_scope(scope, &ext_type)) { + if matches!(loc_ann.extract_spaces().item, TypeAnnotation::Wildcard) + && matches!(ext_problem_kind, ExtensionTypeKind::TagUnion) + && pol == CanPolarity::Pos + { + // Wildcards are redundant in positive positions, since they will always be + // inferred as necessary there! + env.problem(roc_problem::can::Problem::UnnecessaryOutputWildcard { + region: loc_ann.region, + }) + } + + (ext_type, ExtImplicitOpenness::No) + } else { + // Report an error but mark the extension variable to be inferred + // so that we're as permissive as possible. + // + // THEORY: invalid extension types can appear in this position. Otherwise + // they would be caught as errors during unification. + env.problem(roc_problem::can::Problem::InvalidExtensionType { + region: loc_ann.region, + kind: ext_problem_kind, + }); + + let var = var_store.fresh(); + + introduced_variables.insert_inferred(Loc::at_zero(var)); + + ( + Type::Variable(var), + // Since this is an error anyway, just be permissive + ExtImplicitOpenness::No, + ) + } + } + None => match ext_problem_kind { + ExtensionTypeKind::Record => (Type::EmptyRec, ExtImplicitOpenness::No), + ExtensionTypeKind::TagUnion => { + // In negative positions a missing extension variable forces a closed tag union; + // otherwise, open-in-output-position means we give the tag an inference variable. + match pol { + CanPolarity::Neg | CanPolarity::InOpaque => { + (Type::EmptyTagUnion, ExtImplicitOpenness::No) + } + CanPolarity::Pos | CanPolarity::InAlias => { + let var = var_store.fresh(); + introduced_variables.insert_infer_ext_in_output(var); + + (Type::Variable(var), ExtImplicitOpenness::Yes) + } + } + } + }, + } +} + +/// a shallow dealias, continue until the first constructor is not an alias. +fn shallow_dealias_with_scope<'a>(scope: &'a mut Scope, typ: &'a Type) -> &'a Type { + let mut result = typ; + loop { + match result { + Type::Alias { actual, .. } => { + // another loop + result = actual; + } + Type::DelayedAlias(AliasCommon { symbol, .. }) => match scope.lookup_alias(*symbol) { + None => unreachable!(), + Some(alias) => { + result = &alias.typ; + } + }, + + _ => break, + } + } + + result +} + +pub fn freshen_opaque_def( + var_store: &mut VarStore, + opaque: &Alias, +) -> (Vec, Vec, Type) { + debug_assert!(opaque.kind == AliasKind::Opaque); + + let fresh_variables: Vec = opaque + .type_variables + .iter() + .map(|alias_var| OptAbleVar { + var: var_store.fresh(), + opt_abilities: alias_var.value.opt_bound_abilities.clone(), + }) + .collect(); + + // NB: We don't introduce the fresh variables here, we introduce them during constraint gen. + // NB: If there are bugs, check whether this is a problem! + let mut introduced_variables = IntroducedVariables::default(); + + let mut substitutions = ImMap::default(); + + // Freshen all type variables used in the opaque. + for (loc_var, fresh_var) in opaque.type_variables.iter().zip(fresh_variables.iter()) { + let old_var = loc_var.value.var; + substitutions.insert(old_var, Type::Variable(fresh_var.var)); + // NB: fresh var not introduced in this pass; see above. + } + + // Freshen all nested recursion variables. + for &rec_var in opaque.recursion_variables.iter() { + let new = var_store.fresh(); + substitutions.insert(rec_var, Type::Variable(new)); + } + + // Freshen all nested lambda sets. + let mut new_lambda_set_variables = Vec::with_capacity(opaque.lambda_set_variables.len()); + for typ in opaque.lambda_set_variables.iter() { + if let Type::Variable(var) = typ.0 { + let fresh = var_store.fresh(); + substitutions.insert(var, Type::Variable(fresh)); + introduced_variables.insert_lambda_set(fresh); + new_lambda_set_variables.push(LambdaSet(Type::Variable(fresh))); + } else { + unreachable!("at this point there should be only vars in there"); + } + } + + // Fresh the real type with our new variables. + let actual_type = { + let mut typ = opaque.typ.clone(); + typ.substitute(&substitutions); + typ + }; + + (fresh_variables, new_lambda_set_variables, actual_type) +} + +fn insertion_sort_by(arr: &mut [T], mut compare: F) +where + F: FnMut(&T, &T) -> std::cmp::Ordering, +{ + for i in 1..arr.len() { + let val = &arr[i]; + let mut j = i; + let pos = arr[..i] + .binary_search_by(|x| compare(x, val)) + .unwrap_or_else(|pos| pos); + // Swap all elements until specific position. + while j > pos { + arr.swap(j - 1, j); + j -= 1; + } + } +} + +// TODO trim down these arguments! +#[allow(clippy::too_many_arguments)] +fn can_assigned_fields<'a>( + env: &mut Env, + pol: CanPolarity, + fields: &&[Loc>>], + region: Region, + scope: &mut Scope, + var_store: &mut VarStore, + introduced_variables: &mut IntroducedVariables, + local_aliases: &mut VecMap, + references: &mut VecSet, +) -> SendMap> { + use roc_parse::ast::AssignedField::*; + use roc_types::types::RecordField::*; + + // SendMap doesn't have a `with_capacity` + let mut field_types = SendMap::default(); + + // field names we've seen so far in this record + let mut seen = std::collections::HashMap::with_capacity(fields.len()); + + 'outer: for loc_field in fields.iter() { + let mut field = &loc_field.value; + + // use this inner loop to unwrap the SpaceAfter/SpaceBefore + // when we find the name of this field, break out of the loop + // with that value, so we can check whether the field name is + // a duplicate + let new_name = 'inner: loop { + match field { + RequiredValue(field_name, _, annotation) => { + let field_type = can_annotation_help( + env, + pol, + &annotation.value, + annotation.region, + scope, + var_store, + introduced_variables, + local_aliases, + references, + ); + + let label = Lowercase::from(field_name.value); + field_types.insert(label.clone(), RigidRequired(field_type)); + + break 'inner label; + } + OptionalValue(field_name, _, annotation) => { + let field_type = can_annotation_help( + env, + pol, + &annotation.value, + annotation.region, + scope, + var_store, + introduced_variables, + local_aliases, + references, + ); + + let label = Lowercase::from(field_name.value); + field_types.insert(label.clone(), RigidOptional(field_type)); + + break 'inner label; + } + LabelOnly(loc_field_name) => { + // Interpret { a, b } as { a : a, b : b } + let field_name = Lowercase::from(loc_field_name.value); + let field_type = { + if let Some(var) = introduced_variables.var_by_name(&field_name) { + Type::Variable(var) + } else { + let field_var = var_store.fresh(); + introduced_variables.insert_named( + field_name.clone(), + Loc::at(loc_field_name.region, field_var), + ); + Type::Variable(field_var) + } + }; + + field_types.insert(field_name.clone(), RigidRequired(field_type)); + + break 'inner field_name; + } + SpaceBefore(nested, _) | SpaceAfter(nested, _) => { + // check the nested field instead + field = nested; + continue 'inner; + } + Malformed(string) => { + malformed(env, region, string); + + // completely skip this element, advance to the next tag + continue 'outer; + } + } + }; + + // ensure that the new name is not already in this record: + // note that the right-most tag wins when there are two with the same name + if let Some(replaced_region) = seen.insert(new_name.clone(), loc_field.region) { + env.problem(roc_problem::can::Problem::DuplicateRecordFieldType { + field_name: new_name, + record_region: region, + field_region: loc_field.region, + replaced_region, + }); + } + } + + field_types +} + +// TODO trim down these arguments! +#[allow(clippy::too_many_arguments)] +fn can_assigned_tuple_elems( + env: &mut Env, + pol: CanPolarity, + elems: &&[Loc], + scope: &mut Scope, + var_store: &mut VarStore, + introduced_variables: &mut IntroducedVariables, + local_aliases: &mut VecMap, + references: &mut VecSet, +) -> VecMap { + let mut elem_types = VecMap::with_capacity(elems.len()); + + for (index, loc_elem) in elems.iter().enumerate() { + let elem_type = can_annotation_help( + env, + pol, + &loc_elem.value, + loc_elem.region, + scope, + var_store, + introduced_variables, + local_aliases, + references, + ); + + elem_types.insert(index, elem_type); + } + + elem_types +} + +// TODO trim down these arguments! +#[allow(clippy::too_many_arguments)] +fn can_tags<'a>( + env: &mut Env, + pol: CanPolarity, + tags: &'a [Loc>], + region: Region, + scope: &mut Scope, + var_store: &mut VarStore, + introduced_variables: &mut IntroducedVariables, + local_aliases: &mut VecMap, + references: &mut VecSet, +) -> Vec<(TagName, Vec)> { + let mut tag_types = Vec::with_capacity(tags.len()); + + // tag names we've seen so far in this tag union + let mut seen = std::collections::HashMap::with_capacity(tags.len()); + + 'outer: for loc_tag in tags.iter() { + let mut tag = &loc_tag.value; + + // use this inner loop to unwrap the SpaceAfter/SpaceBefore + // when we find the name of this tag, break out of the loop + // with that value, so we can check whether the tag name is + // a duplicate + let new_name = 'inner: loop { + match tag { + Tag::Apply { name, args } => { + let name = name.value.into(); + let mut arg_types = Vec::with_capacity(args.len()); + + for arg in args.iter() { + let ann = can_annotation_help( + env, + pol, + &arg.value, + arg.region, + scope, + var_store, + introduced_variables, + local_aliases, + references, + ); + + arg_types.push(ann); + } + + let tag_name = TagName(name); + tag_types.push((tag_name.clone(), arg_types)); + + break 'inner tag_name; + } + Tag::SpaceBefore(nested, _) | Tag::SpaceAfter(nested, _) => { + // check the nested tag instead + tag = nested; + continue 'inner; + } + Tag::Malformed(string) => { + malformed(env, region, string); + + // completely skip this element, advance to the next tag + continue 'outer; + } + } + }; + + // ensure that the new name is not already in this tag union: + // note that the right-most tag wins when there are two with the same name + if let Some(replaced_region) = seen.insert(new_name.clone(), loc_tag.region) { + env.problem(roc_problem::can::Problem::DuplicateTag { + tag_name: new_name, + tag_region: loc_tag.region, + tag_union_region: region, + replaced_region, + }); + } + } + + tag_types +} diff --git a/crates/compiler/can/src/builtins.rs b/crates/compiler/can/src/builtins.rs new file mode 100644 index 0000000000..168faff98d --- /dev/null +++ b/crates/compiler/can/src/builtins.rs @@ -0,0 +1,597 @@ +use crate::def::Def; +use crate::expr::{AnnotatedMark, ClosureData, Expr::*}; +use crate::expr::{Expr, Recursive}; + +use crate::pattern::Pattern; +use roc_collections::all::SendMap; +use roc_module::ident::TagName; +use roc_module::low_level::LowLevel; +use roc_module::symbol::Symbol; +use roc_region::all::{Loc, Region}; +use roc_types::subs::{VarStore, Variable}; + +/// We use a rust macro to ensure that every LowLevel gets handled +macro_rules! map_symbol_to_lowlevel_and_arity { + ($($lowlevel:ident; $symbol:ident; $number_of_args:literal),* $(,)?) => { + fn def_for_symbol(symbol: Symbol, var_store: &mut VarStore) -> Option { + // expands to a big (but non-exhaustive) match on symbols and maps them to a def + // usually this means wrapping a lowlevel in a `Def` with the right number of + // arguments (see the big enumeration below). In this match we have a bunch of cases + // where that default strategy does not work. + match symbol { + $( + Symbol::$symbol => Some((lowlevel_n($number_of_args))(Symbol::$symbol, LowLevel::$lowlevel, var_store)), + )* + + Symbol::NUM_TO_I8 => Some(lowlevel_1(Symbol::NUM_TO_I8, LowLevel::NumIntCast, var_store)), + Symbol::NUM_TO_I16 => Some(lowlevel_1(Symbol::NUM_TO_I16, LowLevel::NumIntCast, var_store)), + Symbol::NUM_TO_I32 => Some(lowlevel_1(Symbol::NUM_TO_I32, LowLevel::NumIntCast, var_store)), + Symbol::NUM_TO_I64 => Some(lowlevel_1(Symbol::NUM_TO_I64, LowLevel::NumIntCast, var_store)), + Symbol::NUM_TO_I128 => Some(lowlevel_1(Symbol::NUM_TO_I128, LowLevel::NumIntCast, var_store)), + Symbol::NUM_TO_U8 => Some(lowlevel_1(Symbol::NUM_TO_U8, LowLevel::NumIntCast, var_store)), + Symbol::NUM_TO_U16 => Some(lowlevel_1(Symbol::NUM_TO_U16, LowLevel::NumIntCast, var_store)), + Symbol::NUM_TO_U32 => Some(lowlevel_1(Symbol::NUM_TO_U32, LowLevel::NumIntCast, var_store)), + Symbol::NUM_TO_U64 => Some(lowlevel_1(Symbol::NUM_TO_U64, LowLevel::NumIntCast, var_store)), + Symbol::NUM_TO_U128 => Some(lowlevel_1(Symbol::NUM_TO_U128, LowLevel::NumIntCast, var_store)), + Symbol::NUM_TO_NAT => Some(lowlevel_1(Symbol::NUM_TO_NAT, LowLevel::NumIntCast, var_store)), + + Symbol::NUM_INT_CAST => Some(lowlevel_1(Symbol::NUM_INT_CAST, LowLevel::NumIntCast, var_store)), + + Symbol::NUM_TO_F32 => Some(lowlevel_1(Symbol::NUM_TO_F32, LowLevel::NumToFloatCast, var_store)), + Symbol::NUM_TO_F64 => Some(lowlevel_1(Symbol::NUM_TO_F64, LowLevel::NumToFloatCast, var_store)), + + Symbol::NUM_TO_I8_CHECKED => Some(to_num_checked(Symbol::NUM_TO_I8_CHECKED, var_store, LowLevel::NumToIntChecked)), + Symbol::NUM_TO_I16_CHECKED => Some(to_num_checked(Symbol::NUM_TO_I16_CHECKED, var_store, LowLevel::NumToIntChecked)), + Symbol::NUM_TO_I32_CHECKED => Some(to_num_checked(Symbol::NUM_TO_I32_CHECKED, var_store, LowLevel::NumToIntChecked)), + Symbol::NUM_TO_I64_CHECKED => Some(to_num_checked(Symbol::NUM_TO_I64_CHECKED, var_store, LowLevel::NumToIntChecked)), + Symbol::NUM_TO_I128_CHECKED => Some(to_num_checked(Symbol::NUM_TO_I128_CHECKED, var_store, LowLevel::NumToIntChecked)), + Symbol::NUM_TO_U8_CHECKED => Some(to_num_checked(Symbol::NUM_TO_U8_CHECKED, var_store, LowLevel::NumToIntChecked)), + Symbol::NUM_TO_U16_CHECKED => Some(to_num_checked(Symbol::NUM_TO_U16_CHECKED, var_store, LowLevel::NumToIntChecked)), + Symbol::NUM_TO_U32_CHECKED => Some(to_num_checked(Symbol::NUM_TO_U32_CHECKED, var_store, LowLevel::NumToIntChecked)), + Symbol::NUM_TO_U64_CHECKED => Some(to_num_checked(Symbol::NUM_TO_U64_CHECKED, var_store, LowLevel::NumToIntChecked)), + Symbol::NUM_TO_U128_CHECKED => Some(to_num_checked(Symbol::NUM_TO_U128_CHECKED, var_store, LowLevel::NumToIntChecked)), + Symbol::NUM_TO_NAT_CHECKED => Some(to_num_checked(Symbol::NUM_TO_NAT_CHECKED, var_store, LowLevel::NumToIntChecked)), + + Symbol::NUM_TO_F32_CHECKED => Some(to_num_checked(Symbol::NUM_TO_F32_CHECKED, var_store, LowLevel::NumToFloatChecked)), + Symbol::NUM_TO_F64_CHECKED => Some(to_num_checked(Symbol::NUM_TO_F64_CHECKED, var_store, LowLevel::NumToFloatChecked)), + + Symbol::NUM_IS_ZERO => Some(to_num_is_zero(Symbol::NUM_IS_ZERO, var_store)), + + _ => None, + } + } + + fn _enforce_exhaustiveness(lowlevel: LowLevel) -> Symbol { + // when adding a new lowlevel, this match will stop being exhaustive, and give a + // compiler error. Most likely, you are adding a new lowlevel that maps directly to a + // symbol. For instance, you want to have `List.foo` to stand for the `ListFoo` + // lowlevel. In that case, see below in the invocation of `map_symbol_to_lowlevel_and_arity!` + // + // Below, we explicitly handle some exceptions to the pattern where a lowlevel maps + // directly to a symbol. If you are unsure if your lowlevel is an exception, assume + // that it isn't and just see if that works. + #[allow(unreachable_patterns)] // multiple symbols can map to one low-level + match lowlevel { + $( + LowLevel::$lowlevel => Symbol::$symbol, + )* + + // these are implemented explicitly in for_symbol because they are polymorphic + LowLevel::NumIntCast => unreachable!(), + LowLevel::NumToFloatCast => unreachable!(), + LowLevel::NumToIntChecked => unreachable!(), + LowLevel::NumToFloatChecked => unreachable!(), + + // these are used internally and not tied to a symbol + LowLevel::Hash => unimplemented!(), + LowLevel::PtrCast => unimplemented!(), + LowLevel::PtrStore => unimplemented!(), + LowLevel::PtrLoad => unimplemented!(), + LowLevel::PtrClearTagId => unimplemented!(), + LowLevel::RefCountIncRcPtr => unimplemented!(), + LowLevel::RefCountDecRcPtr=> unimplemented!(), + LowLevel::RefCountIncDataPtr => unimplemented!(), + LowLevel::RefCountDecDataPtr=> unimplemented!(), + LowLevel::RefCountIsUnique => unimplemented!(), + + LowLevel::SetJmp => unimplemented!(), + LowLevel::LongJmp => unimplemented!(), + LowLevel::SetLongJmpBuffer => unimplemented!(), + + // these are not implemented, not sure why + LowLevel::StrFromInt => unimplemented!(), + LowLevel::StrFromFloat => unimplemented!(), + LowLevel::NumIsFinite => unimplemented!(), + } + } + }; +} + +// here is where we actually specify the mapping for the fast majority of cases that follow the +// pattern of a symbol mapping directly to a lowlevel. In other words, most lowlevels (left) are generated +// by only one specific symbol (center). We also specify the arity (number of arguments) of the lowlevel (right) +map_symbol_to_lowlevel_and_arity! { + StrConcat; STR_CONCAT; 2, + StrJoinWith; STR_JOIN_WITH; 2, + StrIsEmpty; STR_IS_EMPTY; 1, + StrStartsWith; STR_STARTS_WITH; 2, + StrStartsWithScalar; STR_STARTS_WITH_SCALAR; 2, + StrEndsWith; STR_ENDS_WITH; 2, + StrSplit; STR_SPLIT; 2, + StrCountGraphemes; STR_COUNT_GRAPHEMES; 1, + StrCountUtf8Bytes; STR_COUNT_UTF8_BYTES; 1, + StrFromUtf8Range; STR_FROM_UTF8_RANGE_LOWLEVEL; 3, + StrToUtf8; STR_TO_UTF8; 1, + StrRepeat; STR_REPEAT; 2, + StrTrim; STR_TRIM; 1, + StrTrimStart; STR_TRIM_START; 1, + StrTrimEnd; STR_TRIM_END; 1, + StrToScalars; STR_TO_SCALARS; 1, + StrGetUnsafe; STR_GET_UNSAFE; 2, + StrSubstringUnsafe; STR_SUBSTRING_UNSAFE; 3, + StrReserve; STR_RESERVE; 2, + StrAppendScalar; STR_APPEND_SCALAR_UNSAFE; 2, + StrGetScalarUnsafe; STR_GET_SCALAR_UNSAFE; 2, + StrToNum; STR_TO_NUM; 1, + StrGetCapacity; STR_CAPACITY; 1, + StrWithCapacity; STR_WITH_CAPACITY; 1, + StrGraphemes; STR_GRAPHEMES; 1, + StrReleaseExcessCapacity; STR_RELEASE_EXCESS_CAPACITY; 1, + + ListLen; LIST_LEN; 1, + ListWithCapacity; LIST_WITH_CAPACITY; 1, + ListReserve; LIST_RESERVE; 2, + ListIsUnique; LIST_IS_UNIQUE; 1, + ListAppendUnsafe; LIST_APPEND_UNSAFE; 2, + ListPrepend; LIST_PREPEND; 2, + ListGetUnsafe; LIST_GET_UNSAFE; 2, + ListReplaceUnsafe; LIST_REPLACE_UNSAFE; 3, + ListConcat; LIST_CONCAT; 2, + ListMap; LIST_MAP; 2, + ListMap2; LIST_MAP2; 3, + ListMap3; LIST_MAP3; 4, + ListMap4; LIST_MAP4; 5, + ListSortWith; LIST_SORT_WITH; 2, + ListSublist; LIST_SUBLIST_LOWLEVEL; 3, + ListDropAt; LIST_DROP_AT; 2, + ListSwap; LIST_SWAP; 3, + ListGetCapacity; LIST_CAPACITY; 1, + ListReleaseExcessCapacity; LIST_RELEASE_EXCESS_CAPACITY; 1, + + ListGetUnsafe; DICT_LIST_GET_UNSAFE; 2, + + NumAdd; NUM_ADD; 2, + NumAddWrap; NUM_ADD_WRAP; 2, + NumAddChecked; NUM_ADD_CHECKED_LOWLEVEL; 2, + NumAddSaturated; NUM_ADD_SATURATED; 2, + NumSub; NUM_SUB; 2, + NumSubWrap; NUM_SUB_WRAP; 2, + NumSubChecked; NUM_SUB_CHECKED_LOWLEVEL; 2, + NumSubSaturated; NUM_SUB_SATURATED; 2, + NumMul; NUM_MUL; 2, + NumMulWrap; NUM_MUL_WRAP; 2, + NumMulSaturated; NUM_MUL_SATURATED; 2, + NumMulChecked; NUM_MUL_CHECKED_LOWLEVEL; 2, + NumGt; NUM_GT; 2, + NumGte; NUM_GTE; 2, + NumLt; NUM_LT; 2, + NumLte; NUM_LTE; 2, + NumCompare; NUM_COMPARE; 2, + NumDivFrac; NUM_DIV_FRAC; 2, + NumDivTruncUnchecked; NUM_DIV_TRUNC; 2, + NumDivCeilUnchecked; NUM_DIV_CEIL; 2, + NumRemUnchecked; NUM_REM; 2, + NumIsMultipleOf; NUM_IS_MULTIPLE_OF; 2, + NumAbs; NUM_ABS; 1, + NumNeg; NUM_NEG; 1, + NumSin; NUM_SIN; 1, + NumCos; NUM_COS; 1, + NumTan; NUM_TAN; 1, + NumSqrtUnchecked; NUM_SQRT; 1, + NumLogUnchecked; NUM_LOG; 1, + NumRound; NUM_ROUND; 1, + NumToFrac; NUM_TO_FRAC; 1, + NumIsNan; NUM_IS_NAN; 1, + NumIsInfinite; NUM_IS_INFINITE; 1, + NumIsFinite; NUM_IS_FINITE; 1, + NumPow; NUM_POW; 2, + NumCeiling; NUM_CEILING; 1, + NumPowInt; NUM_POW_INT; 2, + NumFloor; NUM_FLOOR; 1, + NumAtan; NUM_ATAN; 1, + NumAcos; NUM_ACOS; 1, + NumAsin; NUM_ASIN; 1, + NumBytesToU16; NUM_BYTES_TO_U16_LOWLEVEL; 2, + NumBytesToU32; NUM_BYTES_TO_U32_LOWLEVEL; 2, + NumBytesToU64; NUM_BYTES_TO_U64_LOWLEVEL; 2, + NumBytesToU128; NUM_BYTES_TO_U128_LOWLEVEL; 2, + NumBitwiseAnd; NUM_BITWISE_AND; 2, + NumBitwiseXor; NUM_BITWISE_XOR; 2, + NumBitwiseOr; NUM_BITWISE_OR; 2, + NumShiftLeftBy; NUM_SHIFT_LEFT; 2, + NumShiftRightBy; NUM_SHIFT_RIGHT; 2, + NumShiftRightZfBy; NUM_SHIFT_RIGHT_ZERO_FILL; 2, + NumToStr; NUM_TO_STR; 1, + NumCountLeadingZeroBits; NUM_COUNT_LEADING_ZERO_BITS; 1, + NumCountTrailingZeroBits; NUM_COUNT_TRAILING_ZERO_BITS; 1, + NumCountOneBits; NUM_COUNT_ONE_BITS; 1, + I128OfDec; I128_OF_DEC; 1, + + Eq; BOOL_STRUCTURAL_EQ; 2, + NotEq; BOOL_STRUCTURAL_NOT_EQ; 2, + And; BOOL_AND; 2, + Or; BOOL_OR; 2, + Not; BOOL_NOT; 1, + BoxExpr; BOX_BOX_FUNCTION; 1, + UnboxExpr; BOX_UNBOX; 1, + Unreachable; LIST_UNREACHABLE; 1, + DictPseudoSeed; DICT_PSEUDO_SEED; 1, +} + +/// Some builtins cannot be constructed in code gen alone, and need to be defined +/// as separate Roc defs. For example, List.get has this type: +/// +/// List.get : List elem, Nat -> Result elem [OutOfBounds]* +/// +/// Because this returns an open tag union for its Err type, it's not possible +/// for code gen to return a hardcoded value for OutOfBounds. For example, +/// if this Result unifies to [Foo, OutOfBounds] then OutOfBOunds will +/// get assigned the number 1 (because Foo got 0 alphabetically), whereas +/// if it unifies to [OutOfBounds, Qux] then OutOfBounds will get the number 0. +/// +/// Getting these numbers right requires having List.get participate in the +/// normal type-checking and monomorphization processes. As such, this function +/// returns a normal def for List.get, which performs a bounds check and then +/// delegates to the compiler-internal List.getUnsafe function to do the actual +/// lookup (if the bounds check passed). That internal function is hardcoded in code gen, +/// which works fine because it doesn't involve any open tag unions. + +/// Implementation for a builtin +pub fn builtin_defs_map(symbol: Symbol, var_store: &mut VarStore) -> Option { + debug_assert!(symbol.is_builtin()); + + def_for_symbol(symbol, var_store) +} + +fn lowlevel_n(n: usize) -> fn(Symbol, LowLevel, &mut VarStore) -> Def { + match n { + 0 => unimplemented!(), + 1 => lowlevel_1, + 2 => lowlevel_2, + 3 => lowlevel_3, + 4 => lowlevel_4, + 5 => lowlevel_5, + _ => unimplemented!(), + } +} + +fn lowlevel_1(symbol: Symbol, op: LowLevel, var_store: &mut VarStore) -> Def { + let arg1_var = var_store.fresh(); + let ret_var = var_store.fresh(); + + let body = RunLowLevel { + op, + args: vec![(arg1_var, Var(Symbol::ARG_1, arg1_var))], + ret_var, + }; + + defn( + symbol, + vec![(arg1_var, Symbol::ARG_1)], + var_store, + body, + ret_var, + ) +} + +fn lowlevel_2(symbol: Symbol, op: LowLevel, var_store: &mut VarStore) -> Def { + let arg1_var = var_store.fresh(); + let arg2_var = var_store.fresh(); + let ret_var = var_store.fresh(); + + let body = RunLowLevel { + op, + args: vec![ + (arg1_var, Var(Symbol::ARG_1, arg1_var)), + (arg2_var, Var(Symbol::ARG_2, arg2_var)), + ], + ret_var, + }; + + defn( + symbol, + vec![(arg1_var, Symbol::ARG_1), (arg2_var, Symbol::ARG_2)], + var_store, + body, + ret_var, + ) +} + +fn lowlevel_3(symbol: Symbol, op: LowLevel, var_store: &mut VarStore) -> Def { + let arg1_var = var_store.fresh(); + let arg2_var = var_store.fresh(); + let arg3_var = var_store.fresh(); + let ret_var = var_store.fresh(); + + let body = RunLowLevel { + op, + args: vec![ + (arg1_var, Var(Symbol::ARG_1, arg1_var)), + (arg2_var, Var(Symbol::ARG_2, arg2_var)), + (arg3_var, Var(Symbol::ARG_3, arg3_var)), + ], + ret_var, + }; + + defn( + symbol, + vec![ + (arg1_var, Symbol::ARG_1), + (arg2_var, Symbol::ARG_2), + (arg3_var, Symbol::ARG_3), + ], + var_store, + body, + ret_var, + ) +} + +fn lowlevel_4(symbol: Symbol, op: LowLevel, var_store: &mut VarStore) -> Def { + let arg1_var = var_store.fresh(); + let arg2_var = var_store.fresh(); + let arg3_var = var_store.fresh(); + let arg4_var = var_store.fresh(); + let ret_var = var_store.fresh(); + + let body = RunLowLevel { + op, + args: vec![ + (arg1_var, Var(Symbol::ARG_1, arg1_var)), + (arg2_var, Var(Symbol::ARG_2, arg2_var)), + (arg3_var, Var(Symbol::ARG_3, arg3_var)), + (arg4_var, Var(Symbol::ARG_4, arg4_var)), + ], + ret_var, + }; + + defn( + symbol, + vec![ + (arg1_var, Symbol::ARG_1), + (arg2_var, Symbol::ARG_2), + (arg3_var, Symbol::ARG_3), + (arg4_var, Symbol::ARG_4), + ], + var_store, + body, + ret_var, + ) +} + +fn lowlevel_5(symbol: Symbol, op: LowLevel, var_store: &mut VarStore) -> Def { + let arg1_var = var_store.fresh(); + let arg2_var = var_store.fresh(); + let arg3_var = var_store.fresh(); + let arg4_var = var_store.fresh(); + let arg5_var = var_store.fresh(); + let ret_var = var_store.fresh(); + + let body = RunLowLevel { + op, + args: vec![ + (arg1_var, Var(Symbol::ARG_1, arg1_var)), + (arg2_var, Var(Symbol::ARG_2, arg2_var)), + (arg3_var, Var(Symbol::ARG_3, arg3_var)), + (arg4_var, Var(Symbol::ARG_4, arg4_var)), + (arg5_var, Var(Symbol::ARG_5, arg5_var)), + ], + ret_var, + }; + + defn( + symbol, + vec![ + (arg1_var, Symbol::ARG_1), + (arg2_var, Symbol::ARG_2), + (arg3_var, Symbol::ARG_3), + (arg4_var, Symbol::ARG_4), + (arg5_var, Symbol::ARG_5), + ], + var_store, + body, + ret_var, + ) +} + +#[inline(always)] +fn defn( + fn_name: Symbol, + args: Vec<(Variable, Symbol)>, + var_store: &mut VarStore, + body: Expr, + ret_var: Variable, +) -> Def { + let expr = defn_help(fn_name, args, var_store, body, ret_var); + + Def { + loc_pattern: Loc { + region: Region::zero(), + value: Pattern::Identifier(fn_name), + }, + loc_expr: Loc { + region: Region::zero(), + value: expr, + }, + expr_var: var_store.fresh(), + pattern_vars: SendMap::default(), + annotation: None, + } +} + +#[inline(always)] +fn defn_help( + fn_name: Symbol, + args: Vec<(Variable, Symbol)>, + var_store: &mut VarStore, + body: Expr, + ret_var: Variable, +) -> Expr { + use crate::pattern::Pattern::*; + + let closure_args = args + .into_iter() + .map(|(var, symbol)| { + ( + var, + AnnotatedMark::new(var_store), + no_region(Identifier(symbol)), + ) + }) + .collect(); + + Closure(ClosureData { + function_type: var_store.fresh(), + closure_type: var_store.fresh(), + return_type: ret_var, + name: fn_name, + captured_symbols: Vec::new(), + recursive: Recursive::NotRecursive, + arguments: closure_args, + loc_body: Box::new(no_region(body)), + }) +} + +#[inline(always)] +fn no_region(value: T) -> Loc { + Loc { + region: Region::zero(), + value, + } +} + +#[inline(always)] +fn tag(name: &'static str, args: Vec, var_store: &mut VarStore) -> Expr { + Expr::Tag { + tag_union_var: var_store.fresh(), + ext_var: var_store.fresh(), + name: TagName(name.into()), + arguments: args + .into_iter() + .map(|expr| (var_store.fresh(), no_region(expr))) + .collect::)>>(), + } +} + +fn to_num_checked(symbol: Symbol, var_store: &mut VarStore, lowlevel: LowLevel) -> Def { + let bool_var = var_store.fresh(); + let num_var_1 = var_store.fresh(); + let num_var_2 = var_store.fresh(); + let ret_var = var_store.fresh(); + let record_var = var_store.fresh(); + + // let arg_2 = RunLowLevel NumToXXXChecked arg_1 + // if arg_2.b then + // Err OutOfBounds + // else + // Ok arg_2.a + // + // "a" and "b" because the lowlevel return value looks like { converted_val: XXX, out_of_bounds: bool }, + // and codegen will sort by alignment, so "a" will be the first key, etc. + + let cont = If { + branch_var: ret_var, + cond_var: bool_var, + branches: vec![( + // if-condition + no_region( + // arg_2.b + RecordAccess { + record_var, + ext_var: var_store.fresh(), + field: "b".into(), + field_var: var_store.fresh(), + loc_expr: Box::new(no_region(Var(Symbol::ARG_2, var_store.fresh()))), + }, + ), + // out of bounds! + no_region(tag( + "Err", + vec![tag("OutOfBounds", Vec::new(), var_store)], + var_store, + )), + )], + final_else: Box::new( + // all is well + no_region( + // Ok arg_2.a + tag( + "Ok", + vec![ + // arg_2.a + RecordAccess { + record_var, + ext_var: var_store.fresh(), + field: "a".into(), + field_var: num_var_2, + loc_expr: Box::new(no_region(Var(Symbol::ARG_2, var_store.fresh()))), + }, + ], + var_store, + ), + ), + ), + }; + + // arg_2 = RunLowLevel NumToXXXChecked arg_1 + let def = crate::def::Def { + loc_pattern: no_region(Pattern::Identifier(Symbol::ARG_2)), + loc_expr: no_region(RunLowLevel { + op: lowlevel, + args: vec![(num_var_1, Var(Symbol::ARG_1, var_store.fresh()))], + ret_var: record_var, + }), + expr_var: record_var, + pattern_vars: SendMap::default(), + annotation: None, + }; + + let body = LetNonRec(Box::new(def), Box::new(no_region(cont))); + + defn( + symbol, + vec![(num_var_1, Symbol::ARG_1)], + var_store, + body, + ret_var, + ) +} + +fn to_num_is_zero(symbol: Symbol, var_store: &mut VarStore) -> Def { + let bool_var = var_store.fresh(); + let num_var = var_store.fresh(); + + let body = Expr::RunLowLevel { + op: LowLevel::Eq, + args: vec![ + (num_var, Var(Symbol::ARG_1, num_var)), + ( + num_var, + Num( + var_store.fresh(), + "0".to_string().into_boxed_str(), + crate::expr::IntValue::I128(0i128.to_ne_bytes()), + roc_types::num::NumBound::None, + ), + ), + ], + ret_var: bool_var, + }; + + defn( + symbol, + vec![(num_var, Symbol::ARG_1)], + var_store, + body, + bool_var, + ) +} diff --git a/compiler/can/src/constraint.rs b/crates/compiler/can/src/constraint.rs similarity index 78% rename from compiler/can/src/constraint.rs rename to crates/compiler/can/src/constraint.rs index 38c16b93f4..0f594d31a8 100644 --- a/compiler/can/src/constraint.rs +++ b/crates/compiler/can/src/constraint.rs @@ -1,3 +1,7 @@ +use std::cell::Cell; +use std::path::PathBuf; +use std::sync::Arc; + use crate::abilities::SpecializationId; use crate::exhaustive::{ExhaustiveContext, SketchedRows}; use crate::expected::{Expected, PExpected}; @@ -6,19 +10,18 @@ use roc_module::ident::TagName; use roc_module::symbol::{ModuleId, Symbol}; use roc_region::all::{Loc, Region}; use roc_types::subs::{ExhaustiveMark, IllegalCycleMark, Variable}; -use roc_types::types::{Category, PatternCategory, Type}; +use roc_types::types::{Category, PatternCategory, TypeTag, Types}; -#[derive(Debug)] pub struct Constraints { pub constraints: Vec, - pub types: Vec, + pub type_slices: Vec, pub variables: Vec, pub loc_symbols: Vec<(Symbol, Region)>, pub let_constraints: Vec, pub categories: Vec, pub pattern_categories: Vec, - pub expectations: Vec>, - pub pattern_expectations: Vec>, + pub expectations: Vec>, + pub pattern_expectations: Vec>, pub includes_tags: Vec, pub strings: Vec<&'static str>, pub sketched_rows: Vec, @@ -27,16 +30,43 @@ pub struct Constraints { pub cycles: Vec, } +impl std::fmt::Debug for Constraints { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Constraints") + .field("constraints", &self.constraints) + .field("types", &"") + .field("type_slices", &self.type_slices) + .field("variables", &self.variables) + .field("loc_symbols", &self.loc_symbols) + .field("let_constraints", &self.let_constraints) + .field("categories", &self.categories) + .field("pattern_categories", &self.pattern_categories) + .field("expectations", &"") + .field("pattern_expectations", &"") + .field("includes_tags", &self.includes_tags) + .field("strings", &self.strings) + .field("sketched_rows", &self.sketched_rows) + .field("eq", &self.eq) + .field("pattern_eq", &self.pattern_eq) + .field("cycles", &self.cycles) + .finish() + } +} + impl Default for Constraints { fn default() -> Self { Self::new() } } +pub type ExpectedTypeIndex = Index>; +pub type PExpectedTypeIndex = Index>; +pub type TypeOrVar = EitherIndex; + impl Constraints { pub fn new() -> Self { let constraints = Vec::new(); - let mut types = Vec::new(); + let type_slices = Vec::with_capacity(16); let variables = Vec::new(); let loc_symbols = Vec::new(); let let_constraints = Vec::new(); @@ -51,12 +81,6 @@ impl Constraints { let pattern_eq = Vec::new(); let cycles = Vec::new(); - types.extend([ - Type::EmptyRec, - Type::EmptyTagUnion, - Type::Apply(Symbol::STR_STR, vec![], Region::zero()), - ]); - categories.extend([ Category::Record, Category::ForeignCall, @@ -66,7 +90,7 @@ impl Constraints { Category::StrInterpolation, Category::If, Category::When, - Category::Float, + Category::Frac, Category::Int, Category::Num, Category::List, @@ -90,7 +114,7 @@ impl Constraints { Self { constraints, - types, + type_slices, variables, loc_symbols, let_constraints, @@ -107,9 +131,9 @@ impl Constraints { } } - pub const EMPTY_RECORD: Index = Index::new(0); - pub const EMPTY_TAG_UNION: Index = Index::new(1); - pub const STR: Index = Index::new(2); + pub const EMPTY_RECORD: Index>> = Index::new(0); + pub const EMPTY_TAG_UNION: Index>> = Index::new(1); + pub const STR: Index>> = Index::new(2); pub const CATEGORY_RECORD: Index = Index::new(0); pub const CATEGORY_FOREIGNCALL: Index = Index::new(1); @@ -139,18 +163,11 @@ impl Constraints { pub const PCATEGORY_CHARACTER: Index = Index::new(10); #[inline(always)] - pub fn push_type(&mut self, typ: Type) -> EitherIndex { - match typ { - Type::EmptyRec => EitherIndex::from_left(Self::EMPTY_RECORD), - Type::EmptyTagUnion => EitherIndex::from_left(Self::EMPTY_TAG_UNION), - Type::Apply(Symbol::STR_STR, args, _) if args.is_empty() => { - EitherIndex::from_left(Self::STR) - } - Type::Variable(var) => Self::push_type_variable(var), - other => { - let index: Index = Index::push_new(&mut self.types, other); - EitherIndex::from_left(index) - } + pub fn push_type(&mut self, types: &Types, typ: Index) -> TypeOrVar { + if let TypeTag::Variable(var) = types[typ] { + Self::push_type_variable(var) + } else { + EitherIndex::from_left(typ) } } @@ -159,10 +176,9 @@ impl Constraints { let mut buf = String::new(); - writeln!(buf, "Constraints statistics for module {:?}:", module_id)?; + writeln!(buf, "Constraints statistics for module {module_id:?}:")?; writeln!(buf, " constraints length: {}:", self.constraints.len())?; - writeln!(buf, " types length: {}:", self.types.len())?; writeln!( buf, " let_constraints length: {}:", @@ -175,7 +191,12 @@ impl Constraints { } #[inline(always)] - const fn push_type_variable(var: Variable) -> EitherIndex { + pub const fn push_variable(&self, var: Variable) -> TypeOrVar { + Self::push_type_variable(var) + } + + #[inline(always)] + const fn push_type_variable(var: Variable) -> TypeOrVar { // that's right, we use the variable's integer value as the index // that way, we don't need to push anything onto a vector let index: Index = Index::new(var.index()); @@ -183,11 +204,14 @@ impl Constraints { EitherIndex::from_right(index) } - #[inline(always)] - pub fn push_expected_type(&mut self, expected: Expected) -> Index> { + pub fn push_expected_type(&mut self, expected: Expected) -> ExpectedTypeIndex { Index::push_new(&mut self.expectations, expected) } + pub fn push_pat_expected_type(&mut self, expected: PExpected) -> PExpectedTypeIndex { + Index::push_new(&mut self.pattern_expectations, expected) + } + #[inline(always)] pub fn push_category(&mut self, category: Category) -> Index { match category { @@ -199,7 +223,7 @@ impl Constraints { Category::StrInterpolation => Self::CATEGORY_STRINTERPOLATION, Category::If => Self::CATEGORY_IF, Category::When => Self::CATEGORY_WHEN, - Category::Float => Self::CATEGORY_FLOAT, + Category::Frac => Self::CATEGORY_FLOAT, Category::Int => Self::CATEGORY_INT, Category::Num => Self::CATEGORY_NUM, Category::List => Self::CATEGORY_LIST, @@ -227,47 +251,39 @@ impl Constraints { } } - #[inline(always)] pub fn equal_types( &mut self, - typ: Type, - expected: Expected, + type_index: TypeOrVar, + expected_index: ExpectedTypeIndex, category: Category, region: Region, ) -> Constraint { - let type_index = self.push_type(typ); - let expected_index = Index::push_new(&mut self.expectations, expected); let category_index = Self::push_category(self, category); Constraint::Eq(Eq(type_index, expected_index, category_index, region)) } - #[inline(always)] pub fn equal_types_var( &mut self, var: Variable, - expected: Expected, + expected_index: ExpectedTypeIndex, category: Category, region: Region, ) -> Constraint { let type_index = Self::push_type_variable(var); - let expected_index = Index::push_new(&mut self.expectations, expected); let category_index = Self::push_category(self, category); Constraint::Eq(Eq(type_index, expected_index, category_index, region)) } - #[inline(always)] pub fn equal_types_with_storage( &mut self, - typ: Type, - expected: Expected, + type_index: TypeOrVar, + expected_index: ExpectedTypeIndex, category: Category, region: Region, storage_var: Variable, ) -> Constraint { - let type_index = self.push_type(typ); - let expected_index = Index::push_new(&mut self.expectations, expected); let category_index = Self::push_category(self, category); let equal = Constraint::Eq(Eq(type_index, expected_index, category_index, region)); @@ -287,13 +303,11 @@ impl Constraints { pub fn equal_pattern_types( &mut self, - typ: Type, - expected: PExpected, + type_index: TypeOrVar, + expected_index: PExpectedTypeIndex, category: PatternCategory, region: Region, ) -> Constraint { - let type_index = self.push_type(typ); - let expected_index = Index::push_new(&mut self.pattern_expectations, expected); let category_index = Self::push_pattern_category(self, category); Constraint::Pattern(type_index, expected_index, category_index, region) @@ -301,43 +315,34 @@ impl Constraints { pub fn pattern_presence( &mut self, - typ: Type, - expected: PExpected, + type_index: TypeOrVar, + expected_index: PExpectedTypeIndex, category: PatternCategory, region: Region, ) -> Constraint { - let type_index = self.push_type(typ); - let expected_index = Index::push_new(&mut self.pattern_expectations, expected); let category_index = Index::push_new(&mut self.pattern_categories, category); Constraint::PatternPresence(type_index, expected_index, category_index, region) } - pub fn is_open_type(&mut self, typ: Type) -> Constraint { - let type_index = self.push_type(typ); - + pub fn is_open_type(&mut self, type_index: TypeOrVar) -> Constraint { Constraint::IsOpenType(type_index) } - pub fn includes_tag( + pub fn includes_tag( &mut self, - typ: Type, + type_index: TypeOrVar, tag_name: TagName, - types: I, + payloads: Slice, category: PatternCategory, region: Region, - ) -> Constraint - where - I: IntoIterator, - { - let type_index = Index::push_new(&mut self.types, typ); + ) -> Constraint { let category_index = Index::push_new(&mut self.pattern_categories, category); - let types_slice = Slice::extend_new(&mut self.types, types); let includes_tag = IncludesTag { type_index, tag_name, - types: types_slice, + types: payloads, pattern_category: category_index, region, }; @@ -347,7 +352,7 @@ impl Constraints { Constraint::IncludesTag(includes_tag_index) } - fn variable_slice(&mut self, it: I) -> Slice + pub fn variable_slice(&mut self, it: I) -> Slice where I: IntoIterator, { @@ -360,24 +365,24 @@ impl Constraints { fn def_types_slice(&mut self, it: I) -> DefTypes where - I: IntoIterator)>, + I: IntoIterator)>, I::IntoIter: ExactSizeIterator, { let it = it.into_iter(); - let types_start = self.types.len(); + let types_start = self.type_slices.len(); let loc_symbols_start = self.loc_symbols.len(); // because we have an ExactSizeIterator, we can reserve space here let length = it.len(); - self.types.reserve(length); + self.type_slices.reserve(length); self.loc_symbols.reserve(length); for (symbol, loc_type) in it { let Loc { region, value } = loc_type; - self.types.push(value); + self.type_slices.push(value); self.loc_symbols.push((symbol, region)); } @@ -387,6 +392,8 @@ impl Constraints { } } + /// A constraint that some type variables exist and should be introduced into the environment + /// at a given rank. #[inline(always)] pub fn exists(&mut self, flex_vars: I, defs_constraint: Constraint) -> Constraint where @@ -402,6 +409,10 @@ impl Constraints { flex_vars: self.variable_slice(flex_vars), def_types: DefTypes::default(), defs_and_ret_constraint, + + // We're just marking that the variables exist, not that they should be generalized + // (in fact there is no return constraint, so nothing to generalize at this level) + generalizable: Generalizable(false), }; let let_index = Index::new(self.let_constraints.len() as _); @@ -428,6 +439,7 @@ impl Constraints { flex_vars: self.variable_slice(flex_vars), def_types: DefTypes::default(), defs_and_ret_constraint, + generalizable: Generalizable(false), }; let let_index = Index::new(self.let_constraints.len() as _); @@ -444,11 +456,12 @@ impl Constraints { def_types: I3, defs_constraint: Constraint, ret_constraint: Constraint, + generalizable: Generalizable, ) -> Constraint where I1: IntoIterator, I2: IntoIterator, - I3: IntoIterator)>, + I3: IntoIterator)>, I3::IntoIter: ExactSizeIterator, { // defs and ret constraint are stored consequtively, so we only need to store one index @@ -457,15 +470,16 @@ impl Constraints { self.constraints.push(defs_constraint); self.constraints.push(ret_constraint); - let let_contraint = LetConstraint { + let let_constraint = LetConstraint { rigid_vars: self.variable_slice(rigid_vars), flex_vars: self.variable_slice(flex_vars), def_types: self.def_types_slice(def_types), defs_and_ret_constraint, + generalizable, }; let let_index = Index::new(self.let_constraints.len() as _); - self.let_constraints.push(let_contraint); + self.let_constraints.push(let_constraint); Constraint::Let(let_index, Slice::default()) } @@ -485,17 +499,19 @@ impl Constraints { /// then need to find their way to the pool, and a convenient approach turned out to be to /// tag them onto the `Let` that we used to add the imported values. #[inline(always)] - pub fn let_import_constraint( + pub fn let_import_constraint( &mut self, rigid_vars: I1, - def_types: I2, + flex_vars: I2, + def_types: I3, module_constraint: Constraint, pool_variables: &[Variable], ) -> Constraint where I1: IntoIterator, - I2: IntoIterator)>, - I2::IntoIter: ExactSizeIterator, + I2: IntoIterator, + I3: IntoIterator)>, + I3::IntoIter: ExactSizeIterator, { // defs and ret constraint are stored consequtively, so we only need to store one index let defs_and_ret_constraint = Index::new(self.constraints.len() as _); @@ -505,9 +521,12 @@ impl Constraints { let let_contraint = LetConstraint { rigid_vars: self.variable_slice(rigid_vars), - flex_vars: Slice::default(), + flex_vars: self.variable_slice(flex_vars), def_types: self.def_types_slice(def_types), defs_and_ret_constraint, + // If the module these variables were solved in solved them as generalized, + // they should be generalized here too. + generalizable: Generalizable(true), }; let let_index = Index::new(self.let_constraints.len() as _); @@ -546,14 +565,10 @@ impl Constraints { pub fn lookup( &mut self, symbol: Symbol, - expected: Expected, + expected_index: ExpectedTypeIndex, region: Region, ) -> Constraint { - Constraint::Lookup( - symbol, - Index::push_new(&mut self.expectations, expected), - region, - ) + Constraint::Lookup(symbol, expected_index, region) } pub fn contains_save_the_environment(&self, constraint: &Constraint) -> bool { @@ -586,26 +601,14 @@ impl Constraints { | Constraint::PatternPresence(_, _, _, _) | Constraint::Exhaustive { .. } | Constraint::Resolve(..) + | Constraint::IngestedFile(..) | Constraint::CheckCycle(..) => false, } } pub fn store( &mut self, - typ: Type, - variable: Variable, - filename: &'static str, - line_number: u32, - ) -> Constraint { - let type_index = self.push_type(typ); - let string_index = Index::push_new(&mut self.strings, filename); - - Constraint::Store(type_index, variable, string_index, line_number) - } - - pub fn store_index( - &mut self, - type_index: EitherIndex, + type_index: TypeOrVar, variable: Variable, filename: &'static str, line_number: u32, @@ -620,8 +623,8 @@ impl Constraints { real_var: Variable, real_region: Region, category_and_expectation: Result< - (Category, Expected), - (PatternCategory, PExpected), + (Category, ExpectedTypeIndex), + (PatternCategory, PExpectedTypeIndex), >, sketched_rows: SketchedRows, context: ExhaustiveContext, @@ -633,14 +636,12 @@ impl Constraints { let equality = match category_and_expectation { Ok((category, expected)) => { let category = Index::push_new(&mut self.categories, category); - let expected = Index::push_new(&mut self.expectations, expected); let equality = Eq(real_var, expected, category, real_region); let equality = Index::push_new(&mut self.eq, equality); Ok(equality) } Err((category, expected)) => { let category = Index::push_new(&mut self.pattern_categories, category); - let expected = Index::push_new(&mut self.pattern_expectations, expected); let equality = PatternEq(real_var, expected, category, real_region); let equality = Index::push_new(&mut self.pattern_eq, equality); Err(equality) @@ -675,23 +676,48 @@ impl Constraints { Constraint::CheckCycle(cycle_index, cycle_mark) } + + pub fn ingested_file( + &mut self, + type_index: TypeOrVar, + file_path: Box, + bytes: Arc>, + ) -> Constraint { + Constraint::IngestedFile(type_index, file_path, bytes) + } } roc_error_macros::assert_sizeof_default!(Constraint, 3 * 8); roc_error_macros::assert_sizeof_aarch64!(Constraint, 3 * 8); +impl std::ops::Index for Constraints { + type Output = Expected; + + fn index(&self, index: ExpectedTypeIndex) -> &Self::Output { + &self.expectations[index.index()] + } +} + +impl std::ops::Index for Constraints { + type Output = PExpected; + + fn index(&self, index: PExpectedTypeIndex) -> &Self::Output { + &self.pattern_expectations[index.index()] + } +} + #[derive(Clone, Copy, Debug)] pub struct Eq( - pub EitherIndex, - pub Index>, + pub TypeOrVar, + pub ExpectedTypeIndex, pub Index, pub Region, ); #[derive(Clone, Copy, Debug)] pub struct PatternEq( - pub EitherIndex, - pub Index>, + pub TypeOrVar, + pub PExpectedTypeIndex, pub Index, pub Region, ); @@ -713,7 +739,6 @@ pub struct PatternEq( pub struct OpportunisticResolve { /// The specialized type of this lookup, to try to resolve. pub specialization_variable: Variable, - pub specialization_expectation: Index>, /// The ability member to try to resolve. pub member: Symbol, @@ -721,19 +746,14 @@ pub struct OpportunisticResolve { pub specialization_id: SpecializationId, } -#[derive(Clone, Copy)] +#[derive(Clone)] pub enum Constraint { Eq(Eq), - Store( - EitherIndex, - Variable, - Index<&'static str>, - u32, - ), - Lookup(Symbol, Index>, Region), + Store(TypeOrVar, Variable, Index<&'static str>, u32), + Lookup(Symbol, ExpectedTypeIndex, Region), Pattern( - EitherIndex, - Index>, + TypeOrVar, + PExpectedTypeIndex, Index, Region, ), @@ -748,11 +768,11 @@ pub enum Constraint { Let(Index, Slice), And(Slice), /// Presence constraints - IsOpenType(EitherIndex), // Theory; always applied to a variable? if yes the use that + IsOpenType(TypeOrVar), // Theory; always applied to a variable? if yes the use that IncludesTag(Index), PatternPresence( - EitherIndex, - Index>, + TypeOrVar, + PExpectedTypeIndex, Index, Region, ), @@ -765,27 +785,39 @@ pub enum Constraint { /// Attempt to resolve a specialization. Resolve(OpportunisticResolve), CheckCycle(Index, IllegalCycleMark), + + IngestedFile(TypeOrVar, Box, Arc>), } #[derive(Debug, Clone, Copy, Default)] pub struct DefTypes { - pub types: Slice, + pub types: Slice, pub loc_symbols: Slice<(Symbol, Region)>, } +#[derive(Debug, Clone, Copy)] +pub struct Generalizable(pub bool); + #[derive(Debug, Clone)] pub struct LetConstraint { pub rigid_vars: Slice, pub flex_vars: Slice, pub def_types: DefTypes, pub defs_and_ret_constraint: Index<(Constraint, Constraint)>, + + /// Whether the defs introduces in the let-binding can be generalized. + /// Only + /// - syntactic functions + /// - syntactic numbers + /// may be eligible for generalization, though there are other restrictions too. + pub generalizable: Generalizable, } #[derive(Debug, Clone)] pub struct IncludesTag { - pub type_index: Index, + pub type_index: TypeOrVar, pub tag_name: TagName, - pub types: Slice, + pub types: Slice, pub pattern_category: Index, pub region: Region, } @@ -801,16 +833,16 @@ impl std::fmt::Debug for Constraint { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::Eq(Eq(arg0, arg1, arg2, arg3)) => { - write!(f, "Eq({:?}, {:?}, {:?}, {:?})", arg0, arg1, arg2, arg3) + write!(f, "Eq({arg0:?}, {arg1:?}, {arg2:?}, {arg3:?})") } Self::Store(arg0, arg1, arg2, arg3) => { - write!(f, "Store({:?}, {:?}, {:?}, {:?})", arg0, arg1, arg2, arg3) + write!(f, "Store({arg0:?}, {arg1:?}, {arg2:?}, {arg3:?})") } Self::Lookup(arg0, arg1, arg2) => { - write!(f, "Lookup({:?}, {:?}, {:?})", arg0, arg1, arg2) + write!(f, "Lookup({arg0:?}, {arg1:?}, {arg2:?})") } Self::Pattern(arg0, arg1, arg2, arg3) => { - write!(f, "Pattern({:?}, {:?}, {:?}, {:?})", arg0, arg1, arg2, arg3) + write!(f, "Pattern({arg0:?}, {arg1:?}, {arg2:?}, {arg3:?})") } Self::True => write!(f, "True"), Self::SaveTheEnvironment => write!(f, "SaveTheEnvironment"), @@ -819,24 +851,19 @@ impl std::fmt::Debug for Constraint { Self::IsOpenType(arg0) => f.debug_tuple("IsOpenType").field(arg0).finish(), Self::IncludesTag(arg0) => f.debug_tuple("IncludesTag").field(arg0).finish(), Self::PatternPresence(arg0, arg1, arg2, arg3) => { - write!( - f, - "PatternPresence({:?}, {:?}, {:?}, {:?})", - arg0, arg1, arg2, arg3 - ) + write!(f, "PatternPresence({arg0:?}, {arg1:?}, {arg2:?}, {arg3:?})") } Self::Exhaustive(arg0, arg1, arg2, arg3) => { - write!( - f, - "Exhaustive({:?}, {:?}, {:?}, {:?})", - arg0, arg1, arg2, arg3 - ) + write!(f, "Exhaustive({arg0:?}, {arg1:?}, {arg2:?}, {arg3:?})") } Self::Resolve(arg0) => { - write!(f, "Resolve({:?})", arg0) + write!(f, "Resolve({arg0:?})") } Self::CheckCycle(arg0, arg1) => { - write!(f, "CheckCycle({:?}, {:?})", arg0, arg1) + write!(f, "CheckCycle({arg0:?}, {arg1:?})") + } + Self::IngestedFile(arg0, arg1, arg2) => { + write!(f, "IngestedFile({arg0:?}, {arg1:?}, {arg2:?})") } } } diff --git a/crates/compiler/can/src/copy.rs b/crates/compiler/can/src/copy.rs new file mode 100644 index 0000000000..73f535f710 --- /dev/null +++ b/crates/compiler/can/src/copy.rs @@ -0,0 +1,1494 @@ +use crate::{ + def::Def, + expr::{ + ClosureData, Expr, Field, OpaqueWrapFunctionData, StructAccessorData, WhenBranchPattern, + }, + pattern::{DestructType, ListPatterns, Pattern, RecordDestruct, TupleDestruct}, +}; +use roc_module::{ + ident::{Lowercase, TagName}, + symbol::Symbol, +}; +use roc_types::{ + subs::{ + self, AliasVariables, Descriptor, GetSubsSlice, OptVariable, RecordFields, Subs, SubsIndex, + SubsSlice, TupleElems, UnionLambdas, UnionTags, Variable, VariableSubsSlice, + }, + types::{RecordField, Uls}, +}; + +trait CopyEnv { + #[inline(always)] + fn clear_source_copy(&mut self, var: Variable) { + self.mut_source().modify(var, |descriptor| { + if descriptor.copy.into_variable().is_some() { + descriptor.copy = OptVariable::NONE; + } else { + debug_assert!(false, "{var:?} marked as copied but it wasn't"); + } + }) + } + + #[inline(always)] + fn source_root_var(&self, var: Variable) -> Variable { + self.source().get_root_key_without_compacting(var) + } + + #[inline(always)] + fn source_desc(&self, var: Variable) -> Descriptor { + self.source().get_without_compacting(var) + } + + #[inline(always)] + fn set_source_copy(&mut self, var: Variable, copy: OptVariable) { + self.mut_source().set_copy(var, copy) + } + + #[inline(always)] + fn get_copy(&self, var: Variable) -> OptVariable { + self.source().get_copy(var) + } + + #[inline(always)] + fn target_fresh(&mut self, descriptor: Descriptor) -> Variable { + self.target().fresh(descriptor) + } + + fn mut_source(&mut self) -> &mut Subs; + + fn source(&self) -> &Subs; + + fn target(&mut self) -> &mut Subs; + + fn clone_name(&mut self, name: SubsIndex) -> SubsIndex; + + fn clone_field_names(&mut self, field_names: SubsSlice) -> SubsSlice; + + fn clone_tuple_elem_indices( + &mut self, + tuple_elem_indices: SubsSlice, + ) -> SubsSlice; + + fn clone_tag_names(&mut self, tag_names: SubsSlice) -> SubsSlice; + + fn clone_lambda_names(&mut self, lambda_names: SubsSlice) -> SubsSlice; + + fn clone_record_fields( + &mut self, + record_fields: SubsSlice>, + ) -> SubsSlice>; +} + +impl CopyEnv for Subs { + #[inline(always)] + fn mut_source(&mut self) -> &mut Subs { + self + } + + #[inline(always)] + fn source(&self) -> &Subs { + self + } + + #[inline(always)] + fn target(&mut self) -> &mut Subs { + self + } + + #[inline(always)] + fn clone_name(&mut self, name: SubsIndex) -> SubsIndex { + name + } + + #[inline(always)] + fn clone_field_names(&mut self, field_names: SubsSlice) -> SubsSlice { + field_names + } + + #[inline(always)] + fn clone_tuple_elem_indices( + &mut self, + tuple_elem_indices: SubsSlice, + ) -> SubsSlice { + tuple_elem_indices + } + + #[inline(always)] + fn clone_tag_names(&mut self, tag_names: SubsSlice) -> SubsSlice { + tag_names + } + + #[inline(always)] + fn clone_lambda_names(&mut self, lambda_names: SubsSlice) -> SubsSlice { + lambda_names + } + + #[inline(always)] + fn clone_record_fields( + &mut self, + record_fields: SubsSlice>, + ) -> SubsSlice> { + record_fields + } +} + +struct AcrossSubs<'a> { + source: &'a mut Subs, + target: &'a mut Subs, +} + +impl<'a> CopyEnv for AcrossSubs<'a> { + #[inline(always)] + fn mut_source(&mut self) -> &mut Subs { + self.source + } + + #[inline(always)] + fn source(&self) -> &Subs { + self.source + } + + #[inline(always)] + fn target(&mut self) -> &mut Subs { + self.target + } + + #[inline(always)] + fn clone_name(&mut self, name: SubsIndex) -> SubsIndex { + SubsIndex::push_new(&mut self.target.field_names, self.source[name].clone()) + } + + #[inline(always)] + fn clone_field_names(&mut self, field_names: SubsSlice) -> SubsSlice { + SubsSlice::extend_new( + &mut self.target.field_names, + self.source.get_subs_slice(field_names).iter().cloned(), + ) + } + + #[inline(always)] + fn clone_tuple_elem_indices( + &mut self, + tuple_elem_indices: SubsSlice, + ) -> SubsSlice { + SubsSlice::extend_new( + &mut self.target.tuple_elem_indices, + self.source + .get_subs_slice(tuple_elem_indices) + .iter() + .cloned(), + ) + } + + #[inline(always)] + fn clone_tag_names(&mut self, tag_names: SubsSlice) -> SubsSlice { + SubsSlice::extend_new( + &mut self.target.tag_names, + self.source.get_subs_slice(tag_names).iter().cloned(), + ) + } + + #[inline(always)] + fn clone_lambda_names(&mut self, lambda_names: SubsSlice) -> SubsSlice { + SubsSlice::extend_new( + &mut self.target.symbol_names, + self.source.get_subs_slice(lambda_names).iter().cloned(), + ) + } + + #[inline(always)] + fn clone_record_fields( + &mut self, + record_fields: SubsSlice>, + ) -> SubsSlice> { + SubsSlice::extend_new( + &mut self.target.record_fields, + self.source.get_subs_slice(record_fields).iter().copied(), + ) + } +} + +pub fn deep_copy_type_vars_into_expr( + subs: &mut Subs, + var: Variable, + expr: &Expr, +) -> Option<(Variable, Expr)> { + deep_copy_expr_top(subs, var, expr) +} + +#[allow(unused)] // TODO to be removed when this is used for the derivers +pub fn deep_copy_expr_across_subs( + source: &mut Subs, + target: &mut Subs, + var: Variable, + expr: &Expr, +) -> (Variable, Expr) { + let mut across_subs = AcrossSubs { source, target }; + deep_copy_expr_top(&mut across_subs, var, expr).unwrap() +} + +/// Deep copies all type variables in [`expr`]. +/// Returns [`None`] if the expression does not need to be copied. +fn deep_copy_expr_top( + env: &mut C, + var: Variable, + expr: &Expr, +) -> Option<(Variable, Expr)> { + // Always deal with the root, so that aliases propagate correctly. + let expr_var = env.source_root_var(var); + + let mut copied = Vec::with_capacity(16); + + let copy_expr_var = deep_copy_type_vars(env, &mut copied, expr_var); + + if copied.is_empty() { + return None; + } + + let copied_expr = deep_copy_expr_help(env, &mut copied, expr); + + // we have tracked all visited variables, and can now traverse them + // in one go (without looking at the UnificationTable) and clear the copy field + for var in copied { + env.clear_source_copy(var); + } + + Some((copy_expr_var, copied_expr)) +} + +fn deep_copy_expr_help(env: &mut C, copied: &mut Vec, expr: &Expr) -> Expr { + use Expr::*; + + macro_rules! sub { + ($var:expr) => {{ + deep_copy_type_vars(env, copied, $var) + }}; + } + + macro_rules! go_help { + ($expr:expr) => {{ + deep_copy_expr_help(env, copied, $expr) + }}; + } + + match expr { + Num(var, str, val, bound) => Num(sub!(*var), str.clone(), *val, *bound), + Int(v1, v2, str, val, bound) => Int(sub!(*v1), sub!(*v2), str.clone(), *val, *bound), + Float(v1, v2, str, val, bound) => Float(sub!(*v1), sub!(*v2), str.clone(), *val, *bound), + Str(str) => Str(str.clone()), + SingleQuote(v1, v2, char, bound) => SingleQuote(sub!(*v1), sub!(*v2), *char, *bound), + IngestedFile(file_path, bytes, var) => { + IngestedFile(file_path.clone(), bytes.clone(), sub!(*var)) + } + List { + elem_var, + loc_elems, + } => List { + elem_var: sub!(*elem_var), + loc_elems: loc_elems.iter().map(|le| le.map(|e| go_help!(e))).collect(), + }, + Var(sym, var) => Var(*sym, sub!(*var)), + &AbilityMember(sym, specialization, specialization_var) => { + AbilityMember(sym, specialization, sub!(specialization_var)) + } + When { + loc_cond, + cond_var, + expr_var, + region, + branches, + branches_cond_var, + exhaustive, + } => When { + loc_cond: Box::new(loc_cond.map(|e| go_help!(e))), + cond_var: sub!(*cond_var), + expr_var: sub!(*expr_var), + region: *region, + branches: branches + .iter() + .map( + |crate::expr::WhenBranch { + patterns, + value, + guard, + redundant, + }| crate::expr::WhenBranch { + patterns: patterns + .iter() + .map( + |WhenBranchPattern { + pattern, + degenerate, + }| WhenBranchPattern { + pattern: pattern + .map(|p| deep_copy_pattern_help(env, copied, p)), + degenerate: *degenerate, + }, + ) + .collect(), + value: value.map(|e| go_help!(e)), + guard: guard.as_ref().map(|le| le.map(|e| go_help!(e))), + redundant: *redundant, + }, + ) + .collect(), + branches_cond_var: sub!(*branches_cond_var), + exhaustive: *exhaustive, + }, + If { + cond_var, + branch_var, + branches, + final_else, + } => If { + cond_var: sub!(*cond_var), + branch_var: sub!(*branch_var), + branches: branches + .iter() + .map(|(c, e)| (c.map(|e| go_help!(e)), e.map(|e| go_help!(e)))) + .collect(), + final_else: Box::new(final_else.map(|e| go_help!(e))), + }, + + LetRec(defs, body, cycle_mark) => LetRec( + defs.iter() + .map( + |Def { + loc_pattern, + loc_expr, + expr_var, + pattern_vars, + annotation, + }| Def { + loc_pattern: loc_pattern.map(|p| deep_copy_pattern_help(env, copied, p)), + loc_expr: loc_expr.map(|e| go_help!(e)), + expr_var: sub!(*expr_var), + pattern_vars: pattern_vars.iter().map(|(s, v)| (*s, sub!(*v))).collect(), + // Annotation should only be used in constraining, don't clone before + // constraining :) + annotation: annotation.clone(), + }, + ) + .collect(), + Box::new(body.map(|e| go_help!(e))), + *cycle_mark, + ), + LetNonRec(def, body) => { + let Def { + loc_pattern, + loc_expr, + expr_var, + pattern_vars, + annotation, + } = &**def; + let def = Def { + loc_pattern: loc_pattern.map(|p| deep_copy_pattern_help(env, copied, p)), + loc_expr: loc_expr.map(|e| go_help!(e)), + expr_var: sub!(*expr_var), + pattern_vars: pattern_vars.iter().map(|(s, v)| (*s, sub!(*v))).collect(), + // Annotation should only be used in constraining, don't clone before + // constraining :) + annotation: annotation.clone(), + }; + LetNonRec(Box::new(def), Box::new(body.map(|e| go_help!(e)))) + } + + Call(f, args, called_via) => { + let (fn_var, fn_expr, clos_var, ret_var) = &**f; + Call( + Box::new(( + sub!(*fn_var), + fn_expr.map(|e| go_help!(e)), + sub!(*clos_var), + sub!(*ret_var), + )), + args.iter() + .map(|(var, expr)| (sub!(*var), expr.map(|e| go_help!(e)))) + .collect(), + *called_via, + ) + } + Crash { msg, ret_var } => Crash { + msg: Box::new(msg.map(|m| go_help!(m))), + ret_var: sub!(*ret_var), + }, + RunLowLevel { op, args, ret_var } => RunLowLevel { + op: *op, + args: args + .iter() + .map(|(var, expr)| (sub!(*var), go_help!(expr))) + .collect(), + ret_var: sub!(*ret_var), + }, + ForeignCall { + foreign_symbol, + args, + ret_var, + } => ForeignCall { + foreign_symbol: foreign_symbol.clone(), + args: args + .iter() + .map(|(var, expr)| (sub!(*var), go_help!(expr))) + .collect(), + ret_var: sub!(*ret_var), + }, + + Closure(ClosureData { + function_type, + closure_type, + return_type, + name, + captured_symbols, + recursive, + arguments, + loc_body, + }) => Closure(ClosureData { + function_type: sub!(*function_type), + closure_type: sub!(*closure_type), + return_type: sub!(*return_type), + name: *name, + captured_symbols: captured_symbols + .iter() + .map(|(s, v)| (*s, sub!(*v))) + .collect(), + recursive: *recursive, + arguments: arguments + .iter() + .map(|(v, mark, pat)| { + ( + sub!(*v), + *mark, + pat.map(|p| deep_copy_pattern_help(env, copied, p)), + ) + }) + .collect(), + loc_body: Box::new(loc_body.map(|e| go_help!(e))), + }), + + Record { record_var, fields } => Record { + record_var: sub!(*record_var), + fields: fields + .iter() + .map( + |( + k, + Field { + var, + region, + loc_expr, + }, + )| { + ( + k.clone(), + Field { + var: sub!(*var), + region: *region, + loc_expr: Box::new(loc_expr.map(|e| go_help!(e))), + }, + ) + }, + ) + .collect(), + }, + + EmptyRecord => EmptyRecord, + + Tuple { tuple_var, elems } => Tuple { + tuple_var: sub!(*tuple_var), + elems: elems + .iter() + .map(|(var, loc_expr)| (sub!(*var), Box::new(loc_expr.map(|e| go_help!(e))))) + .collect(), + }, + + RecordAccess { + record_var, + ext_var, + field_var, + loc_expr, + field, + } => RecordAccess { + record_var: sub!(*record_var), + ext_var: sub!(*ext_var), + field_var: sub!(*field_var), + loc_expr: Box::new(loc_expr.map(|e| go_help!(e))), + field: field.clone(), + }, + + RecordAccessor(StructAccessorData { + name, + function_var, + record_var, + closure_var, + ext_var, + field_var, + field, + }) => RecordAccessor(StructAccessorData { + name: *name, + function_var: sub!(*function_var), + record_var: sub!(*record_var), + closure_var: sub!(*closure_var), + ext_var: sub!(*ext_var), + field_var: sub!(*field_var), + field: field.clone(), + }), + + TupleAccess { + tuple_var, + ext_var, + elem_var, + loc_expr, + index, + } => TupleAccess { + tuple_var: sub!(*tuple_var), + ext_var: sub!(*ext_var), + elem_var: sub!(*elem_var), + loc_expr: Box::new(loc_expr.map(|e| go_help!(e))), + index: *index, + }, + + RecordUpdate { + record_var, + ext_var, + symbol, + updates, + } => RecordUpdate { + record_var: sub!(*record_var), + ext_var: sub!(*ext_var), + symbol: *symbol, + updates: updates + .iter() + .map( + |( + k, + Field { + var, + region, + loc_expr, + }, + )| { + ( + k.clone(), + Field { + var: sub!(*var), + region: *region, + loc_expr: Box::new(loc_expr.map(|e| go_help!(e))), + }, + ) + }, + ) + .collect(), + }, + + Tag { + tag_union_var: variant_var, + ext_var, + name, + arguments, + } => Tag { + tag_union_var: sub!(*variant_var), + ext_var: sub!(*ext_var), + name: name.clone(), + arguments: arguments + .iter() + .map(|(v, e)| (sub!(*v), e.map(|e| go_help!(e)))) + .collect(), + }, + + ZeroArgumentTag { + closure_name, + variant_var, + ext_var, + name, + } => ZeroArgumentTag { + closure_name: *closure_name, + variant_var: sub!(*variant_var), + ext_var: sub!(*ext_var), + name: name.clone(), + }, + + OpaqueRef { + opaque_var, + name, + argument, + specialized_def_type, + type_arguments, + lambda_set_variables, + } => OpaqueRef { + opaque_var: sub!(*opaque_var), + name: *name, + argument: Box::new((sub!(argument.0), argument.1.map(|e| go_help!(e)))), + // These shouldn't matter for opaques during mono, because they are only used for reporting + // and pretty-printing to the user. During mono we decay immediately into the argument. + // NB: if there are bugs, check if not substituting here is the problem! + specialized_def_type: specialized_def_type.clone(), + type_arguments: type_arguments.clone(), + lambda_set_variables: lambda_set_variables.clone(), + }, + + OpaqueWrapFunction(OpaqueWrapFunctionData { + opaque_name, + opaque_var, + specialized_def_type, + type_arguments, + lambda_set_variables, + function_name, + function_var, + argument_var, + closure_var, + }) => OpaqueWrapFunction(OpaqueWrapFunctionData { + opaque_name: *opaque_name, + opaque_var: sub!(*opaque_var), + function_name: *function_name, + function_var: sub!(*function_var), + argument_var: sub!(*argument_var), + closure_var: sub!(*closure_var), + // The following three are only used for constraining + specialized_def_type: specialized_def_type.clone(), + type_arguments: type_arguments.clone(), + lambda_set_variables: lambda_set_variables.clone(), + }), + + Expect { + loc_condition, + loc_continuation, + lookups_in_cond, + } => Expect { + loc_condition: Box::new(loc_condition.map(|e| go_help!(e))), + loc_continuation: Box::new(loc_continuation.map(|e| go_help!(e))), + lookups_in_cond: lookups_in_cond.to_vec(), + }, + + ExpectFx { + loc_condition, + loc_continuation, + lookups_in_cond, + } => ExpectFx { + loc_condition: Box::new(loc_condition.map(|e| go_help!(e))), + loc_continuation: Box::new(loc_continuation.map(|e| go_help!(e))), + lookups_in_cond: lookups_in_cond.to_vec(), + }, + + Dbg { + loc_message, + loc_continuation, + variable, + symbol, + } => Dbg { + loc_message: Box::new(loc_message.map(|e| go_help!(e))), + loc_continuation: Box::new(loc_continuation.map(|e| go_help!(e))), + variable: sub!(*variable), + symbol: *symbol, + }, + + TypedHole(v) => TypedHole(sub!(*v)), + + RuntimeError(err) => RuntimeError(err.clone()), + } +} + +fn deep_copy_pattern_help( + env: &mut C, + copied: &mut Vec, + pat: &Pattern, +) -> Pattern { + use Pattern::*; + + macro_rules! sub { + ($var:expr) => {{ + deep_copy_type_vars(env, copied, $var) + }}; + } + + macro_rules! go_help { + ($pat:expr) => {{ + deep_copy_pattern_help(env, copied, $pat) + }}; + } + + match pat { + Identifier(s) => Identifier(*s), + As(subpattern, s) => As(Box::new(subpattern.map(|p| go_help!(p))), *s), + AppliedTag { + whole_var, + ext_var, + tag_name, + arguments, + } => AppliedTag { + whole_var: sub!(*whole_var), + ext_var: sub!(*ext_var), + tag_name: tag_name.clone(), + arguments: arguments + .iter() + .map(|(var, lp)| (sub!(*var), lp.map(|p| go_help!(p)))) + .collect(), + }, + UnwrappedOpaque { + whole_var, + opaque, + argument, + specialized_def_type, + type_arguments, + lambda_set_variables, + } => { + let (arg_var, arg_pat) = &**argument; + UnwrappedOpaque { + whole_var: sub!(*whole_var), + opaque: *opaque, + argument: Box::new((sub!(*arg_var), arg_pat.map(|p| go_help!(p)))), + // These are only used for constraining, they shouldn't matter where pattern + // cloning is useful (in monomorphization or during deriving) + specialized_def_type: specialized_def_type.clone(), + type_arguments: type_arguments.clone(), + lambda_set_variables: lambda_set_variables.clone(), + } + } + RecordDestructure { + whole_var, + ext_var, + destructs, + } => RecordDestructure { + whole_var: sub!(*whole_var), + ext_var: sub!(*ext_var), + destructs: destructs + .iter() + .map(|lrd| { + lrd.map( + |RecordDestruct { + var, + label, + symbol, + typ, + }| RecordDestruct { + var: sub!(*var), + label: label.clone(), + symbol: *symbol, + typ: match typ { + DestructType::Required => DestructType::Required, + DestructType::Optional(var, expr) => DestructType::Optional( + sub!(*var), + expr.map(|e| deep_copy_expr_help(env, copied, e)), + ), + DestructType::Guard(var, pat) => { + DestructType::Guard(sub!(*var), pat.map(|p| go_help!(p))) + } + }, + }, + ) + }) + .collect(), + }, + TupleDestructure { + whole_var, + ext_var, + destructs, + } => TupleDestructure { + whole_var: sub!(*whole_var), + ext_var: sub!(*ext_var), + destructs: destructs + .iter() + .map(|lrd| { + lrd.map( + |TupleDestruct { + destruct_index: index, + var, + typ: (tyvar, pat), + }: &crate::pattern::TupleDestruct| TupleDestruct { + destruct_index: *index, + var: sub!(*var), + typ: (sub!(*tyvar), pat.map(|p| go_help!(p))), + }, + ) + }) + .collect(), + }, + List { + list_var, + elem_var, + patterns: ListPatterns { patterns, opt_rest }, + } => List { + list_var: sub!(*list_var), + elem_var: sub!(*elem_var), + patterns: ListPatterns { + patterns: patterns.iter().map(|lp| lp.map(|p| go_help!(p))).collect(), + opt_rest: *opt_rest, + }, + }, + NumLiteral(var, s, n, bound) => NumLiteral(sub!(*var), s.clone(), *n, *bound), + IntLiteral(v1, v2, s, n, bound) => IntLiteral(sub!(*v1), sub!(*v2), s.clone(), *n, *bound), + FloatLiteral(v1, v2, s, n, bound) => { + FloatLiteral(sub!(*v1), sub!(*v2), s.clone(), *n, *bound) + } + StrLiteral(s) => StrLiteral(s.clone()), + SingleQuote(v1, v2, c, bound) => SingleQuote(sub!(*v1), sub!(*v2), *c, *bound), + Underscore => Underscore, + AbilityMemberSpecialization { ident, specializes } => AbilityMemberSpecialization { + ident: *ident, + specializes: *specializes, + }, + Shadowed(region, ident, symbol) => Shadowed(*region, ident.clone(), *symbol), + OpaqueNotInScope(ident) => OpaqueNotInScope(ident.clone()), + UnsupportedPattern(region) => UnsupportedPattern(*region), + MalformedPattern(problem, region) => MalformedPattern(*problem, *region), + } +} + +/// Deep copies the type variables in [`var`], returning a map of original -> new type variable for +/// all type variables copied. +#[inline] +fn deep_copy_type_vars( + env: &mut C, + copied: &mut Vec, + var: Variable, +) -> Variable { + // Always deal with the root, so that unified variables are treated the same. + let var = env.source_root_var(var); + + let cloned_var = help(env, copied, var); + + return cloned_var; + + #[must_use] + #[inline] + fn help(env: &mut C, visited: &mut Vec, var: Variable) -> Variable { + use roc_types::subs::Content::*; + use roc_types::subs::FlatType::*; + + // Always deal with the root, so that unified variables are treated the same. + let var = env.source_root_var(var); + + let desc = env.source_desc(var); + + // Unlike `deep_copy_var` in solve, here we are cloning *all* flex and rigid vars. + // So we only want to short-circuit if we've already done the cloning work for a particular + // var. + if let Some(copy) = desc.copy.into_variable() { + return copy; + } + + let content = desc.content; + + let copy_descriptor = Descriptor { + content: Error, // we'll update this below + rank: desc.rank, + mark: desc.mark, + copy: OptVariable::NONE, + }; + + let copy = env.target_fresh(copy_descriptor); + env.set_source_copy(var, copy.into()); + + visited.push(var); + + macro_rules! descend_slice { + ($slice:expr) => { + for var_index in $slice { + let var = env.source()[var_index]; + let _ = help(env, visited, var); + } + }; + } + + macro_rules! descend_var { + ($var:expr) => {{ + help(env, visited, $var) + }}; + } + + macro_rules! clone_var_slice { + ($slice:expr) => {{ + let new_arguments = + VariableSubsSlice::reserve_into_subs(env.target(), $slice.len()); + for (target_index, var_index) in (new_arguments.indices()).zip($slice) { + let var = env.source()[var_index]; + let copy_var = env.get_copy(var).into_variable().unwrap_or(var); + + env.target().variables[target_index] = copy_var; + } + new_arguments + }}; + } + + macro_rules! perform_clone { + ($do_clone:expr) => {{ + // It may the case that while deep-copying nested variables of this type, we + // ended up copying the type itself (notably if it was self-referencing, in a + // recursive type). In that case, short-circuit with the known copy. + // if let Some(copy) = subs.get_ref(var).copy.into_variable() { + // return copy; + // } + // Perform the clone. + $do_clone + }}; + } + + // Now we recursively copy the content of the variable. + // We have already marked the variable as copied, so we + // will not repeat this work or crawl this variable again. + let new_content = match content { + // The vars for which we want to do something interesting. + FlexVar(opt_name) => FlexVar(opt_name.map(|n| env.clone_name(n))), + FlexAbleVar(opt_name, abilities) => FlexAbleVar( + opt_name.map(|n| env.clone_name(n)), + env.clone_lambda_names(abilities), + ), + RigidVar(name) => RigidVar(env.clone_name(name)), + RigidAbleVar(name, abilities) => { + RigidAbleVar(env.clone_name(name), env.clone_lambda_names(abilities)) + } + + // Everything else is a mechanical descent. + Structure(flat_type) => match flat_type { + EmptyRecord | EmptyTuple | EmptyTagUnion => Structure(flat_type), + Apply(symbol, arguments) => { + descend_slice!(arguments); + + perform_clone!({ + let new_arguments = clone_var_slice!(arguments); + Structure(Apply(symbol, new_arguments)) + }) + } + Func(arguments, closure_var, ret_var) => { + descend_slice!(arguments); + + let new_closure_var = descend_var!(closure_var); + let new_ret_var = descend_var!(ret_var); + + perform_clone!({ + let new_arguments = clone_var_slice!(arguments); + Structure(Func(new_arguments, new_closure_var, new_ret_var)) + }) + } + Record(fields, ext_var) => { + let new_ext_var = descend_var!(ext_var); + + descend_slice!(fields.variables()); + + perform_clone!({ + let new_variables = clone_var_slice!(fields.variables()); + let new_field_names = env.clone_field_names(fields.field_names()); + let new_record_fields = env.clone_record_fields(fields.record_fields()); + + let new_fields = { + RecordFields { + length: fields.length, + field_names_start: new_field_names.start, + variables_start: new_variables.start, + field_types_start: new_record_fields.start, + } + }; + + Structure(Record(new_fields, new_ext_var)) + }) + } + Tuple(elems, ext_var) => { + let new_ext_var = descend_var!(ext_var); + + descend_slice!(elems.variables()); + + perform_clone!({ + let new_variables = clone_var_slice!(elems.variables()); + let new_elem_indices = env.clone_tuple_elem_indices(elems.elem_indices()); + + let new_elems = { + TupleElems { + length: elems.length, + variables_start: new_variables.start, + elem_index_start: new_elem_indices.start, + } + }; + + Structure(Tuple(new_elems, new_ext_var)) + }) + } + TagUnion(tags, ext_var) => { + let new_ext_var = ext_var.map(|v| descend_var!(v)); + + for variables_slice_index in tags.variables() { + let variables_slice = env.source()[variables_slice_index]; + descend_slice!(variables_slice); + } + + perform_clone!({ + let new_variable_slices = + SubsSlice::reserve_variable_slices(env.target(), tags.len()); + let it = (new_variable_slices.indices()).zip(tags.variables()); + for (target_index, index) in it { + let slice = env.source()[index]; + let new_variables = clone_var_slice!(slice); + env.target().variable_slices[target_index] = new_variables; + } + let new_tag_names = env.clone_tag_names(tags.labels()); + + let new_union_tags = + UnionTags::from_slices(new_tag_names, new_variable_slices); + + Structure(TagUnion(new_union_tags, new_ext_var)) + }) + } + RecursiveTagUnion(rec_var, tags, ext_var) => { + let new_ext_var = ext_var.map(|v| descend_var!(v)); + let new_rec_var = descend_var!(rec_var); + + for variables_slice_index in tags.variables() { + let variables_slice = env.source()[variables_slice_index]; + descend_slice!(variables_slice); + } + + perform_clone!({ + let new_variable_slices = + SubsSlice::reserve_variable_slices(env.target(), tags.len()); + let it = (new_variable_slices.indices()).zip(tags.variables()); + for (target_index, index) in it { + let slice = env.source()[index]; + let new_variables = clone_var_slice!(slice); + + env.target().variable_slices[target_index] = new_variables; + } + let new_tag_names = env.clone_tag_names(tags.labels()); + + let new_union_tags = + UnionTags::from_slices(new_tag_names, new_variable_slices); + + Structure(RecursiveTagUnion(new_rec_var, new_union_tags, new_ext_var)) + }) + } + FunctionOrTagUnion(tag_names, symbols, ext_var) => { + let new_ext_var = ext_var.map(|v| descend_var!(v)); + let new_tag_names = env.clone_tag_names(tag_names); + let new_symbols = env.clone_lambda_names(symbols); + perform_clone!(Structure(FunctionOrTagUnion( + new_tag_names, + new_symbols, + new_ext_var + ))) + } + }, + + RecursionVar { + opt_name, + structure, + } => { + let new_structure = descend_var!(structure); + let new_opt_name = opt_name.map(|n| env.clone_name(n)); + + perform_clone!({ + RecursionVar { + opt_name: new_opt_name, + structure: new_structure, + } + }) + } + + Alias(symbol, arguments, real_type_var, kind) => { + let new_real_type_var = descend_var!(real_type_var); + descend_slice!(arguments.all_variables()); + + perform_clone!({ + let new_variables = clone_var_slice!(arguments.all_variables()); + let new_arguments = AliasVariables { + variables_start: new_variables.start, + ..arguments + }; + + Alias(symbol, new_arguments, new_real_type_var, kind) + }) + } + + LambdaSet(subs::LambdaSet { + solved, + recursion_var, + unspecialized, + ambient_function, + }) => { + let new_rec_var = recursion_var.map(|var| descend_var!(var)); + for variables_slice_index in solved.variables() { + let variables_slice = env.source()[variables_slice_index]; + descend_slice!(variables_slice); + } + for uls_index in unspecialized { + let Uls(var, _, _) = env.source()[uls_index]; + descend_var!(var); + } + let new_ambient_function = descend_var!(ambient_function); + + perform_clone!({ + let new_variable_slices = + SubsSlice::reserve_variable_slices(env.target(), solved.len()); + let it = (new_variable_slices.indices()).zip(solved.variables()); + for (target_index, index) in it { + let slice = env.source()[index]; + let new_variables = clone_var_slice!(slice); + + env.target().variable_slices[target_index] = new_variables; + } + + let new_solved_labels = env.clone_lambda_names(solved.labels()); + + let new_solved = + UnionLambdas::from_slices(new_solved_labels, new_variable_slices); + + let new_unspecialized = + SubsSlice::reserve_uls_slice(env.target(), unspecialized.len()); + for (target_index, uls_index) in + (new_unspecialized.into_iter()).zip(unspecialized.into_iter()) + { + let Uls(var, sym, region) = env.source()[uls_index]; + let copy_var = env.get_copy(var).into_variable().unwrap_or(var); + + env.target()[target_index] = Uls(copy_var, sym, region); + env.target().uls_of_var.add(copy_var, copy); + } + + LambdaSet(subs::LambdaSet { + solved: new_solved, + recursion_var: new_rec_var, + unspecialized: new_unspecialized, + ambient_function: new_ambient_function, + }) + }) + } + ErasedLambda => ErasedLambda, + + RangedNumber(range) => { + perform_clone!(RangedNumber(range)) + } + Error => Error, + }; + + env.target().set_content(copy, new_content); + + copy + } +} + +#[cfg(test)] +mod test { + use crate::{ + copy::{deep_copy_type_vars_into_expr, AcrossSubs}, + expr::Expr, + }; + + use super::{deep_copy_expr_across_subs, deep_copy_type_vars}; + use roc_error_macros::internal_error; + use roc_module::{ident::TagName, symbol::Symbol}; + use roc_region::all::Loc; + use roc_types::{ + subs::{ + self, Content, Content::*, Descriptor, FlatType, GetSubsSlice, Mark, OptVariable, Rank, + Subs, SubsIndex, SubsSlice, Variable, + }, + types::Uls, + }; + + #[cfg(test)] + fn new_var(subs: &mut Subs, content: Content) -> Variable { + subs.fresh(Descriptor { + content, + rank: Rank::toplevel(), + mark: Mark::NONE, + copy: OptVariable::NONE, + }) + } + + #[test] + fn copy_flex_var() { + let mut subs = Subs::new(); + + let field_name = SubsIndex::push_new(&mut subs.field_names, "a".into()); + let var = new_var(&mut subs, FlexVar(Some(field_name))); + + let mut copied = vec![]; + + let copy = deep_copy_type_vars(&mut subs, &mut copied, var); + + assert_ne!(var, copy); + + match subs.get_content_without_compacting(copy) { + FlexVar(Some(name)) => { + assert_eq!(subs[*name].as_str(), "a"); + } + it => unreachable!("{:?}", it), + } + } + + #[test] + fn copy_rigid_var() { + let mut subs = Subs::new(); + + let field_name = SubsIndex::push_new(&mut subs.field_names, "a".into()); + let var = new_var(&mut subs, RigidVar(field_name)); + + let mut copied = vec![]; + + let copy = deep_copy_type_vars(&mut subs, &mut copied, var); + + assert_ne!(var, copy); + + match subs.get_content_without_compacting(var) { + RigidVar(name) => { + assert_eq!(subs[*name].as_str(), "a"); + } + it => unreachable!("{:?}", it), + } + } + + #[test] + fn copy_flex_able_var() { + let mut subs = Subs::new(); + + let field_name = SubsIndex::push_new(&mut subs.field_names, "a".into()); + let abilities = SubsSlice::extend_new(&mut subs.symbol_names, [Symbol::UNDERSCORE]); + let var = new_var(&mut subs, FlexAbleVar(Some(field_name), abilities)); + + let mut copied = vec![]; + + let copy = deep_copy_type_vars(&mut subs, &mut copied, var); + + assert_ne!(var, copy); + + match subs.get_content_without_compacting(var) { + FlexAbleVar(Some(name), abilities) => { + assert_eq!(subs[*name].as_str(), "a"); + assert_eq!(subs.get_subs_slice(*abilities), [Symbol::UNDERSCORE]); + } + it => unreachable!("{:?}", it), + } + } + + #[test] + fn copy_rigid_able_var() { + let mut subs = Subs::new(); + + let field_name = SubsIndex::push_new(&mut subs.field_names, "a".into()); + let abilities = SubsSlice::extend_new(&mut subs.symbol_names, [Symbol::UNDERSCORE]); + let var = new_var(&mut subs, RigidAbleVar(field_name, abilities)); + + let mut copied = vec![]; + + let copy = deep_copy_type_vars(&mut subs, &mut copied, var); + + assert_ne!(var, copy); + match subs.get_content_without_compacting(var) { + RigidAbleVar(name, abilities) => { + assert_eq!(subs[*name].as_str(), "a"); + assert_eq!(subs.get_subs_slice(*abilities), [Symbol::UNDERSCORE]); + } + it => internal_error!("{:?}", it), + } + } + + #[test] + fn copy_deep_expr() { + let mut subs = Subs::new(); + + let a = SubsIndex::push_new(&mut subs.field_names, "a".into()); + let b = SubsIndex::push_new(&mut subs.field_names, "b".into()); + let var1 = new_var(&mut subs, FlexVar(Some(a))); + let var2 = new_var(&mut subs, FlexVar(Some(b))); + + let expr = Expr::Tag { + tag_union_var: var1, + ext_var: Variable::EMPTY_TAG_UNION, + name: TagName("F".into()), + arguments: vec![( + var2, + Loc::at_zero(Expr::Tag { + tag_union_var: var2, + ext_var: Variable::EMPTY_TAG_UNION, + name: TagName("G".into()), + arguments: vec![], + }), + )], + }; + + let (var, expr) = deep_copy_type_vars_into_expr(&mut subs, var1, &expr).unwrap(); + + assert!(subs.get_copy(var1).is_none()); + assert!(subs.get_copy(var2).is_none()); + + match expr { + Expr::Tag { + tag_union_var: variant_var, + ext_var, + name, + mut arguments, + } => { + assert_ne!(var1, variant_var); + assert_ne!(var2, variant_var); + + match subs.get_content_without_compacting(variant_var) { + FlexVar(Some(name)) => { + assert_eq!(subs[*name].as_str(), "a"); + } + it => panic!("{it:?}"), + } + assert_eq!(var, variant_var); + assert!(matches!( + subs.get_content_without_compacting(ext_var), + Content::Structure(FlatType::EmptyTagUnion) + )); + assert_eq!(name.0.as_str(), "F"); + + assert_eq!(arguments.len(), 1); + let (v2, arg) = arguments.pop().unwrap(); + assert_ne!(var1, v2); + assert_ne!(var2, v2); + match subs.get_content_without_compacting(v2) { + FlexVar(Some(name)) => { + assert_eq!(subs[*name].as_str(), "b"); + } + it => panic!("{it:?}"), + } + + match arg.value { + Expr::Tag { + tag_union_var: variant_var, + ext_var, + name, + arguments, + } => { + assert_eq!(variant_var, v2); + assert!(matches!( + subs.get_content_without_compacting(ext_var), + Content::Structure(FlatType::EmptyTagUnion) + )); + assert_eq!(name.0.as_str(), "G"); + assert_eq!(arguments.len(), 0); + } + e => panic!("{e:?}"), + } + } + e => panic!("{e:?}"), + } + } + + #[test] + fn copy_deep_expr_across_subs() { + let mut source = Subs::new(); + let mut target = Subs::new(); + + let a = SubsIndex::push_new(&mut source.field_names, "a".into()); + let b = SubsIndex::push_new(&mut source.field_names, "b".into()); + let var1 = new_var(&mut source, FlexVar(Some(a))); + let var2 = new_var(&mut source, FlexVar(Some(b))); + + let expr = Expr::Tag { + tag_union_var: var1, + ext_var: Variable::EMPTY_TAG_UNION, + name: TagName("F".into()), + arguments: vec![( + var2, + Loc::at_zero(Expr::Tag { + tag_union_var: var2, + ext_var: Variable::EMPTY_TAG_UNION, + name: TagName("G".into()), + arguments: vec![], + }), + )], + }; + + let (var, expr) = deep_copy_expr_across_subs(&mut source, &mut target, var1, &expr); + + assert!(source.get_copy(var1).is_none()); + assert!(source.get_copy(var2).is_none()); + + match expr { + Expr::Tag { + tag_union_var: variant_var, + ext_var, + name, + mut arguments, + } => { + match target.get_content_without_compacting(variant_var) { + FlexVar(Some(name)) => { + assert_eq!(target[*name].as_str(), "a"); + } + it => panic!("{it:?}"), + } + assert_eq!(var, variant_var); + assert!(matches!( + target.get_content_without_compacting(ext_var), + Content::Structure(FlatType::EmptyTagUnion) + )); + assert_eq!(name.0.as_str(), "F"); + + assert_eq!(arguments.len(), 1); + let (v2, arg) = arguments.pop().unwrap(); + match target.get_content_without_compacting(v2) { + FlexVar(Some(name)) => { + assert_eq!(target[*name].as_str(), "b"); + } + it => panic!("{it:?}"), + } + + match arg.value { + Expr::Tag { + tag_union_var: variant_var, + ext_var, + name, + arguments, + } => { + assert_eq!(variant_var, v2); + assert!(matches!( + target.get_content_without_compacting(ext_var), + Content::Structure(FlatType::EmptyTagUnion) + )); + assert_eq!(name.0.as_str(), "G"); + assert_eq!(arguments.len(), 0); + } + e => panic!("{e:?}"), + } + } + e => panic!("{e:?}"), + } + } + + #[test] + fn copy_across_subs_preserve_uls_of_var() { + let mut source = Subs::new(); + let mut target = Subs::new(); + + let a = new_var(&mut source, FlexVar(None)); + let uls = SubsSlice::extend_new( + &mut source.unspecialized_lambda_sets, + vec![Uls(a, Symbol::UNDERSCORE, 3)], + ); + let lambda_set_var = new_var( + &mut source, + LambdaSet(subs::LambdaSet { + solved: Default::default(), + recursion_var: OptVariable::NONE, + unspecialized: uls, + ambient_function: Variable::NULL, + }), + ); + + source.uls_of_var.add(a, lambda_set_var); + + assert!(target.uls_of_var.is_empty()); + + let mut env = AcrossSubs { + source: &mut source, + target: &mut target, + }; + let mut copied = vec![]; + let copy_uls = deep_copy_type_vars(&mut env, &mut copied, lambda_set_var); + let copy_a = deep_copy_type_vars(&mut env, &mut copied, a); + + assert!(matches!( + target.get_content_without_compacting(copy_a), + Content::FlexVar(None) + )); + assert!(matches!( + target.get_content_without_compacting(copy_uls), + Content::LambdaSet(..) + )); + + let uls_of_var: Vec<_> = target + .remove_dependent_unspecialized_lambda_sets(copy_a) + .collect(); + assert_eq!(uls_of_var.len(), 1); + assert_eq!(uls_of_var[0], copy_uls); + } +} diff --git a/crates/compiler/can/src/debug.rs b/crates/compiler/can/src/debug.rs new file mode 100644 index 0000000000..99b1e88420 --- /dev/null +++ b/crates/compiler/can/src/debug.rs @@ -0,0 +1,6 @@ +mod pretty_print; + +pub use pretty_print::pretty_print_declarations; +pub use pretty_print::pretty_print_def; +pub use pretty_print::pretty_write_declarations; +pub use pretty_print::Ctx as PPCtx; diff --git a/crates/compiler/can/src/debug/pretty_print.rs b/crates/compiler/can/src/debug/pretty_print.rs new file mode 100644 index 0000000000..b1fee35636 --- /dev/null +++ b/crates/compiler/can/src/debug/pretty_print.rs @@ -0,0 +1,571 @@ +//! Pretty-prints the canonical AST back to check our work - do things look reasonable? + +use crate::def::Def; +use crate::expr::Expr::{self, *}; +use crate::expr::{ + ClosureData, DeclarationTag, Declarations, FunctionDef, OpaqueWrapFunctionData, WhenBranch, +}; +use crate::pattern::{Pattern, RecordDestruct, TupleDestruct}; + +use roc_module::symbol::{Interns, ModuleId, Symbol}; + +use ven_pretty::{text, Arena, DocAllocator, DocBuilder}; + +pub struct Ctx<'a> { + pub home: ModuleId, + pub interns: &'a Interns, + pub print_lambda_names: bool, +} + +pub fn pretty_print_declarations(c: &Ctx, declarations: &Declarations) -> String { + let f = Arena::new(); + print_declarations_help(c, &f, declarations) + .1 + .pretty(80) + .to_string() +} + +pub fn pretty_write_declarations( + writer: &mut impl std::io::Write, + c: &Ctx, + declarations: &Declarations, +) -> std::io::Result<()> { + let f = Arena::new(); + print_declarations_help(c, &f, declarations) + .1 + .render(80, writer) +} + +pub fn pretty_print_def(c: &Ctx, d: &Def) -> String { + let f = Arena::new(); + def(c, &f, d).append(f.hardline()).1.pretty(80).to_string() +} + +fn print_declarations_help<'a>( + c: &Ctx, + f: &'a Arena<'a>, + declarations: &'a Declarations, +) -> DocBuilder<'a, Arena<'a>> { + let mut defs = Vec::with_capacity(declarations.len()); + for (index, tag) in declarations.iter_bottom_up() { + let symbol = declarations.symbols[index].value; + let body = &declarations.expressions[index]; + + let def = match tag { + DeclarationTag::Value => def_symbol_help(c, f, symbol, &body.value), + DeclarationTag::Function(f_index) + | DeclarationTag::Recursive(f_index) + | DeclarationTag::TailRecursive(f_index) => { + let function_def = &declarations.function_bodies[f_index.index()].value; + toplevel_function(c, f, symbol, function_def, &body.value) + } + DeclarationTag::Expectation => todo!(), + DeclarationTag::ExpectationFx => todo!(), + DeclarationTag::Destructure(_) => todo!(), + DeclarationTag::MutualRecursion { .. } => { + // the defs will be printed next + continue; + } + }; + + defs.push(def); + } + + f.intersperse(defs, f.hardline().append(f.hardline())) +} + +macro_rules! maybe_paren { + ($paren_if_above:expr, $my_prec:expr, $doc:expr) => { + maybe_paren!($paren_if_above, $my_prec, || true, $doc) + }; + ($paren_if_above:expr, $my_prec:expr, $extra_cond:expr, $doc:expr) => { + if $my_prec > $paren_if_above && $extra_cond() { + $doc.parens().group() + } else { + $doc + } + }; +} + +fn def<'a>(c: &Ctx, f: &'a Arena<'a>, d: &'a Def) -> DocBuilder<'a, Arena<'a>> { + let Def { + loc_pattern, + loc_expr, + expr_var: _, + pattern_vars: _, + annotation: _, + } = d; + + def_help(c, f, &loc_pattern.value, &loc_expr.value) +} + +fn def_symbol_help<'a>( + c: &Ctx, + f: &'a Arena<'a>, + sym: Symbol, + body: &'a Expr, +) -> DocBuilder<'a, Arena<'a>> { + pp_sym(c, f, sym) + .append(f.text(" =")) + .append(f.line()) + .append(expr(c, EPrec::Free, f, body)) + .nest(2) + .group() +} + +fn def_help<'a>( + c: &Ctx, + f: &'a Arena<'a>, + pat: &'a Pattern, + body: &'a Expr, +) -> DocBuilder<'a, Arena<'a>> { + pattern(c, PPrec::Free, f, pat) + .append(f.text(" =")) + .append(f.line()) + .append(expr(c, EPrec::Free, f, body)) + .nest(2) + .group() +} + +fn toplevel_function<'a>( + c: &Ctx, + f: &'a Arena<'a>, + sym: Symbol, + function_def: &'a FunctionDef, + body: &'a Expr, +) -> DocBuilder<'a, Arena<'a>> { + let FunctionDef { arguments, .. } = function_def; + + let args = arguments + .iter() + .map(|arg| pattern(c, PPrec::Free, f, &arg.2.value)); + + pp_sym(c, f, sym) + .append(f.text(" =")) + .append(f.line()) + .append(f.text("\\")) + .append(f.intersperse(args, f.text(", "))) + .append(f.text(" ->")) + .group() + .append(f.line()) + .append(expr(c, EPrec::Free, f, body).group()) + .nest(2) + .group() +} + +#[derive(PartialEq, PartialOrd)] +enum EPrec { + Free, + AppArg, +} + +fn expr<'a>(c: &Ctx, p: EPrec, f: &'a Arena<'a>, e: &'a Expr) -> DocBuilder<'a, Arena<'a>> { + use EPrec::*; + match e { + Num(_, n, _, _) | Int(_, _, n, _, _) | Float(_, _, n, _, _) => f.text(&**n), + Str(s) => text!(f, r#""{}""#, s), + SingleQuote(_, _, c, _) => text!(f, "'{}'", c), + IngestedFile(file_path, bytes, _) => { + text!(f, "", file_path, bytes.len()) + } + List { + elem_var: _, + loc_elems, + } => f + .reflow("[") + .append( + f.concat(loc_elems.iter().map(|le| { + f.hardline() + .append(expr(c, Free, f, &le.value)) + .append(f.text(",")) + })) + .group() + .nest(2), + ) + .append(f.line_()) + .append("]") + .group() + .flat_alt( + f.reflow("[") + .append(f.intersperse( + loc_elems.iter().map(|le| expr(c, Free, f, &le.value)), + f.reflow(", "), + )) + .append(f.line_()) + .append("]") + .group(), + ), + Var(sym, _) | AbilityMember(sym, _, _) => pp_sym(c, f, *sym), + When { + loc_cond, branches, .. + } => maybe_paren!( + Free, + p, + f.reflow("when ") + .append(expr(c, Free, f, &loc_cond.value)) + .append(f.text(" is")) + .append( + f.concat( + branches + .iter() + .map(|b| f.hardline().append(branch(c, f, b))) + ) + .group(), + ) + .nest(2) + .group() + ), + If { + branches, + final_else, + .. + } => f + .concat(branches.iter().enumerate().map(|(i, (cond, body))| { + let head = if i == 0 { "if " } else { "else if " }; + (f.reflow(head) + .append(expr(c, Free, f, &cond.value)) + .group() + .nest(2)) + .append(f.line()) + .append( + f.reflow("then") + .append(f.softline().append(expr(c, Free, f, &body.value))) + .group() + .nest(2), + ) + .append(f.line()) + })) + .append( + f.reflow("else ") + .append(expr(c, Free, f, &final_else.value)) + .group() + .nest(2), + ) + .group(), + LetRec(_, _, _) => todo!(), + LetNonRec(loc_def, body) => def(c, f, loc_def) + .append(f.hardline()) + .append(expr(c, Free, f, &body.value)) + .group(), + Call(fun, args, _) => { + let (_, fun, _, _) = &**fun; + maybe_paren!( + Free, + p, + expr(c, AppArg, f, &fun.value) + .append( + f.concat(args.iter().map(|le| f.line().append(expr( + c, + AppArg, + f, + &le.1.value + )))) + .group() + ) + .group() + .nest(2) + ) + } + RunLowLevel { args, .. } => { + let op = "LowLevel"; + + maybe_paren!( + Free, + p, + f.reflow(op) + .append( + f.concat( + args.iter() + .map(|le| f.line().append(expr(c, AppArg, f, &le.1))) + ) + .group() + ) + .group() + .nest(2) + ) + } + ForeignCall { .. } => todo!(), + Closure(ClosureData { + arguments, + loc_body, + name, + .. + }) => f + .text("\\") + .append( + f.intersperse( + arguments + .iter() + .map(|(_, _, arg)| pattern(c, PPrec::Free, f, &arg.value)), + f.text(", "), + ), + ) + .append(if c.print_lambda_names { + f.text(" -[") + .append(pp_sym(c, f, *name)) + .append(f.text("]->")) + } else { + f.text(" ->") + }) + .append(f.line()) + .append(expr(c, Free, f, &loc_body.value)) + .nest(2) + .group(), + Record { fields, .. } => f + .reflow("{") + .append( + f.intersperse( + fields.iter().map(|(name, field)| { + let field = f + .text(name.as_str()) + .append(f.reflow(": ")) + .append(expr(c, Free, f, &field.loc_expr.value)) + .nest(2) + .group(); + f.line().append(field) + }), + f.reflow(","), + ) + .nest(2) + .group(), + ) + .append(f.line()) + .append(f.text("}")) + .group(), + Tuple { elems, .. } => f + .reflow("(") + .append( + f.intersperse( + elems.iter().map(|(_var, elem)| { + f.line() + .append(expr(c, Free, f, &elem.value)) + .nest(2) + .group() + }), + f.reflow(","), + ) + .nest(2) + .group(), + ) + .append(f.line()) + .append(f.text(")")) + .group(), + EmptyRecord => f.text("{}"), + RecordAccess { + loc_expr, field, .. + } => expr(c, AppArg, f, &loc_expr.value) + .append(text!(f, ".{}", field.as_str())) + .group(), + TupleAccess { + loc_expr, index, .. + } => expr(c, AppArg, f, &loc_expr.value) + .append(text!(f, ".{index}")) + .group(), + OpaqueWrapFunction(OpaqueWrapFunctionData { opaque_name, .. }) => { + text!(f, "@{}", opaque_name.as_str(c.interns)) + } + RecordAccessor(_) => todo!(), + RecordUpdate { + symbol, updates, .. + } => f + .reflow("{") + .append(f.line()) + .append(f.text(symbol.as_str(c.interns).to_string())) + .append(f.reflow(" &")) + .append( + f.intersperse( + updates.iter().map(|(name, field)| { + let field = f + .text(name.as_str()) + .append(f.reflow(": ")) + .append(expr(c, Free, f, &field.loc_expr.value)) + .nest(2) + .group(); + f.line().append(field) + }), + f.reflow(","), + ) + .nest(2) + .group(), + ) + .append(f.line()) + .append(f.text("}")) + .group(), + Tag { + name, arguments, .. + } => maybe_paren!( + Free, + p, + || !arguments.is_empty(), + f.text(name.0.as_str()) + .append(if arguments.is_empty() { + f.nil() + } else { + f.space() + }) + .append( + f.intersperse( + arguments + .iter() + .map(|(_, le)| expr(c, AppArg, f, &le.value)), + f.space(), + ) + ) + .group() + ), + Crash { .. } => todo!(), + ZeroArgumentTag { .. } => todo!(), + OpaqueRef { name, argument, .. } => maybe_paren!( + Free, + p, + || true, + pp_sym(c, f, *name) + .append(f.space()) + .append(expr(c, AppArg, f, &argument.1.value)) + .group() + ), + Dbg { .. } => todo!(), + Expect { .. } => todo!(), + ExpectFx { .. } => todo!(), + TypedHole(_) => todo!(), + RuntimeError(_) => todo!(), + } +} + +fn pp_sym<'a>(c: &Ctx, f: &'a Arena<'a>, sym: Symbol) -> DocBuilder<'a, Arena<'a>> { + if sym.module_id() == c.home { + f.text(sym.as_str(c.interns).to_owned()) + } else { + text!( + f, + "{}.{}", + sym.module_string(c.interns), + sym.as_str(c.interns), + ) + } +} + +fn branch<'a>(c: &Ctx, f: &'a Arena<'a>, b: &'a WhenBranch) -> DocBuilder<'a, Arena<'a>> { + let WhenBranch { + patterns, + value, + guard, + redundant: _, + } = b; + + f.intersperse( + patterns + .iter() + .map(|lp| pattern(c, PPrec::Free, f, &lp.pattern.value)), + f.text(" | "), + ) + .append(match guard { + Some(e) => f.text("if ").append(expr(c, EPrec::Free, f, &e.value)), + None => f.nil(), + }) + .append(f.text(" ->")) + .append(f.line()) + .append(expr(c, EPrec::Free, f, &value.value)) + .nest(2) + .group() +} + +#[derive(PartialEq, PartialOrd)] +enum PPrec { + Free, + AppArg, +} + +fn pattern<'a>( + c: &Ctx, + prec: PPrec, + f: &'a Arena<'a>, + p: &'a Pattern, +) -> DocBuilder<'a, Arena<'a>> { + use PPrec::*; + use Pattern::*; + match p { + Identifier(sym) + | AbilityMemberSpecialization { + specializes: sym, .. + } => pp_sym(c, f, *sym), + As(subpattern, symbol) => pattern(c, prec, f, &subpattern.value) + .append(f.text(" as ")) + .append(pp_sym(c, f, *symbol)), + AppliedTag { + tag_name, + arguments, + .. + } => maybe_paren!( + Free, + prec, + f.text(tag_name.0.as_str()) + .append(if arguments.is_empty() { + f.nil() + } else { + f.space() + }) + .append( + f.intersperse( + arguments + .iter() + .map(|(_, lp)| pattern(c, AppArg, f, &lp.value)), + f.space(), + ) + ) + .group() + ), + UnwrappedOpaque { + opaque, argument, .. + } => text!(f, "@{} ", opaque.module_string(c.interns)) + .append(pattern(c, Free, f, &argument.1.value)) + .group(), + RecordDestructure { destructs, .. } => f + .text("{") + .append( + f.intersperse( + destructs.iter().map(|l| &l.value).map( + |RecordDestruct { label, typ, .. }| match typ { + crate::pattern::DestructType::Required => f.text(label.as_str()), + crate::pattern::DestructType::Optional(_, e) => f + .text(label.as_str()) + .append(f.text(" ? ")) + .append(expr(c, EPrec::Free, f, &e.value)), + crate::pattern::DestructType::Guard(_, p) => f + .text(label.as_str()) + .append(f.text(": ")) + .append(pattern(c, Free, f, &p.value)), + }, + ), + f.text(", "), + ), + ) + .append(f.text("}")) + .group(), + TupleDestructure { destructs, .. } => f + .text("(") + .append( + f.intersperse( + destructs + .iter() + .map(|l| &l.value) + .map(|TupleDestruct { typ: (_, p), .. }| pattern(c, Free, f, &p.value)), + f.text(", "), + ), + ) + .append(f.text(")")) + .group(), + List { .. } => todo!(), + NumLiteral(_, n, _, _) | IntLiteral(_, _, n, _, _) | FloatLiteral(_, _, n, _, _) => { + f.text(&**n) + } + StrLiteral(s) => text!(f, r#""{}""#, s), + SingleQuote(_, _, c, _) => text!(f, "'{}'", c), + Underscore => f.text("_"), + + Shadowed(_, _, _) => todo!(), + OpaqueNotInScope(_) => todo!(), + UnsupportedPattern(_) => todo!(), + MalformedPattern(_, _) => todo!(), + } +} diff --git a/crates/compiler/can/src/def.rs b/crates/compiler/can/src/def.rs new file mode 100644 index 0000000000..804665e3a9 --- /dev/null +++ b/crates/compiler/can/src/def.rs @@ -0,0 +1,3298 @@ +use crate::abilities::AbilityMemberData; +use crate::abilities::ImplKey; +use crate::abilities::MemberVariables; +use crate::abilities::PendingMemberType; +use crate::annotation::canonicalize_annotation; +use crate::annotation::find_type_def_symbols; +use crate::annotation::make_apply_symbol; +use crate::annotation::AnnotationFor; +use crate::annotation::IntroducedVariables; +use crate::annotation::OwnedNamedOrAble; +use crate::derive; +use crate::env::Env; +use crate::expr::get_lookup_symbols; +use crate::expr::AnnotatedMark; +use crate::expr::ClosureData; +use crate::expr::Declarations; +use crate::expr::Expr::{self, *}; +use crate::expr::StructAccessorData; +use crate::expr::{canonicalize_expr, Output, Recursive}; +use crate::pattern::{canonicalize_def_header_pattern, BindingsFromPattern, Pattern}; +use crate::procedure::References; +use crate::scope::create_alias; +use crate::scope::{PendingAbilitiesInScope, Scope}; +use roc_collections::ReferenceMatrix; +use roc_collections::VecMap; +use roc_collections::VecSet; +use roc_collections::{ImSet, MutMap, SendMap}; +use roc_error_macros::internal_error; +use roc_module::ident::Ident; +use roc_module::ident::Lowercase; +use roc_module::symbol::IdentId; +use roc_module::symbol::ModuleId; +use roc_module::symbol::Symbol; +use roc_parse::ast; +use roc_parse::ast::AssignedField; +use roc_parse::ast::Defs; +use roc_parse::ast::ExtractSpaces; +use roc_parse::ast::TypeHeader; +use roc_parse::ident::Accessor; +use roc_parse::pattern::PatternType; +use roc_problem::can::ShadowKind; +use roc_problem::can::{CycleEntry, Problem, RuntimeError}; +use roc_region::all::{Loc, Region}; +use roc_types::subs::IllegalCycleMark; +use roc_types::subs::{VarStore, Variable}; +use roc_types::types::AliasCommon; +use roc_types::types::AliasKind; +use roc_types::types::AliasVar; +use roc_types::types::IndexOrField; +use roc_types::types::LambdaSet; +use roc_types::types::MemberImpl; +use roc_types::types::OptAbleType; +use roc_types::types::{Alias, Type}; +use std::fmt::Debug; + +#[derive(Clone, Debug)] +pub struct Def { + pub loc_pattern: Loc, + pub loc_expr: Loc, + pub expr_var: Variable, + pub pattern_vars: SendMap, + pub annotation: Option, +} + +impl Def { + pub fn region(&self) -> Region { + let head_region = match &self.annotation { + Some(ann) => { + if ann.region.start() < self.loc_pattern.region.start() { + ann.region + } else { + // Happens with annotation-only bodies like foo : T, since `T` is after the + // pattern. + self.loc_pattern.region + } + } + None => self.loc_pattern.region, + }; + Region::span_across(&head_region, &self.loc_expr.region) + } +} + +#[derive(Clone, Debug)] +pub struct Annotation { + pub signature: Type, + pub introduced_variables: IntroducedVariables, + pub aliases: VecMap, + pub region: Region, +} + +impl Annotation { + fn freshen(mut self, var_store: &mut VarStore) -> Self { + let mut substitutions = MutMap::default(); + + for v in self.introduced_variables.lambda_sets.iter_mut() { + let new = var_store.fresh(); + substitutions.insert(*v, new); + + *v = new; + } + + self.signature.substitute_variables(&substitutions); + + self + } +} + +#[derive(Debug)] +pub(crate) struct CanDefs { + defs: Vec>, + dbgs: ExpectsOrDbgs, + expects: ExpectsOrDbgs, + expects_fx: ExpectsOrDbgs, + def_ordering: DefOrdering, + aliases: VecMap, +} + +#[derive(Clone, Debug)] +pub struct ExpectsOrDbgs { + pub conditions: Vec, + pub regions: Vec, + pub preceding_comment: Vec, +} + +impl ExpectsOrDbgs { + fn with_capacity(capacity: usize) -> Self { + Self { + conditions: Vec::with_capacity(capacity), + regions: Vec::with_capacity(capacity), + preceding_comment: Vec::with_capacity(capacity), + } + } + + fn push(&mut self, loc_can_condition: Loc, preceding_comment: Region) { + self.conditions.push(loc_can_condition.value); + self.regions.push(loc_can_condition.region); + self.preceding_comment.push(preceding_comment); + } +} + +/// A Def that has had patterns and type annnotations canonicalized, +/// but no Expr canonicalization has happened yet. Also, it has had spaces +/// and nesting resolved, and knows whether annotations are standalone or not. +#[derive(Debug, Clone)] +enum PendingValueDef<'a> { + /// A standalone annotation with no body + AnnotationOnly( + &'a Loc>, + Loc, + &'a Loc>, + ), + /// A body with no type annotation + Body(Loc, &'a Loc>), + /// A body with a type annotation + TypedBody( + &'a Loc>, + Loc, + &'a Loc>, + &'a Loc>, + ), +} + +impl PendingValueDef<'_> { + fn loc_pattern(&self) -> &Loc { + match self { + PendingValueDef::AnnotationOnly(_, loc_pattern, _) => loc_pattern, + PendingValueDef::Body(loc_pattern, _) => loc_pattern, + PendingValueDef::TypedBody(_, loc_pattern, _, _) => loc_pattern, + } + } +} + +#[derive(Debug, Clone)] +struct PendingAbilityMember<'a> { + name: Loc, + typ: Loc>, +} + +#[derive(Debug, Clone)] +enum PendingTypeDef<'a> { + /// A structural type alias, e.g. `Ints : List Int` + Alias { + name: Loc, + vars: Vec>, + ann: &'a Loc>, + }, + + /// An opaque type alias, e.g. `Age := U32`. + Opaque { + name_str: &'a str, + name: Loc, + vars: Vec>, + ann: &'a Loc>, + derived: Option<&'a Loc>>, + }, + + Ability { + name: Loc, + members: Vec>, + }, + + /// An invalid alias, that is ignored in the rest of the pipeline + /// e.g. a definition like `MyAlias 1 : Int` + /// with an incorrect pattern + InvalidAlias { + #[allow(dead_code)] + kind: AliasKind, + symbol: Symbol, + region: Region, + }, + + /// An alias with a name that shadows another symbol + ShadowedAlias, + + /// An invalid ability, that is ignored in the rest of the pipeline. + /// E.g. a shadowed ability, or with a bad definition. + InvalidAbility { + symbol: Symbol, + region: Region, + }, + + AbilityNotOnToplevel, + AbilityShadows, +} + +impl PendingTypeDef<'_> { + fn introduction(&self) -> Option<(Symbol, Region)> { + match self { + PendingTypeDef::Alias { name, ann, .. } => { + let region = Region::span_across(&name.region, &ann.region); + + Some((name.value, region)) + } + PendingTypeDef::Opaque { + name_str: _, + name, + vars: _, + ann, + derived, + } => { + let end = derived.map(|d| d.region).unwrap_or(ann.region); + let region = Region::span_across(&name.region, &end); + + Some((name.value, region)) + } + PendingTypeDef::Ability { name, .. } => Some((name.value, name.region)), + PendingTypeDef::InvalidAlias { symbol, region, .. } => Some((*symbol, *region)), + PendingTypeDef::ShadowedAlias { .. } => None, + PendingTypeDef::InvalidAbility { symbol, region } => Some((*symbol, *region)), + PendingTypeDef::AbilityNotOnToplevel => None, + PendingTypeDef::AbilityShadows => None, + } + } +} + +// See github.com/roc-lang/roc/issues/800 for discussion of the large_enum_variant check. +#[derive(Clone, Debug)] +#[allow(clippy::large_enum_variant)] +pub enum Declaration { + Declare(Def), + DeclareRec(Vec, IllegalCycleMark), + Builtin(Def), + Expects(ExpectsOrDbgs), + ExpectsFx(ExpectsOrDbgs), + /// If we know a cycle is illegal during canonicalization. + /// Otherwise we will try to detect this during solving; see [`IllegalCycleMark`]. + InvalidCycle(Vec), +} + +impl Declaration { + pub fn def_count(&self) -> usize { + use Declaration::*; + match self { + Declare(_) => 1, + DeclareRec(defs, _) => defs.len(), + InvalidCycle { .. } => 0, + Builtin(_) => 0, + Expects(_) => 0, + ExpectsFx(_) => 0, + } + } + + pub fn region(&self) -> Region { + match self { + Declaration::Declare(def) => def.region(), + Declaration::DeclareRec(defs, _) => Region::span_across( + &defs.first().unwrap().region(), + &defs.last().unwrap().region(), + ), + Declaration::Builtin(def) => def.region(), + Declaration::InvalidCycle(cycles) => Region::span_across( + &cycles.first().unwrap().expr_region, + &cycles.last().unwrap().expr_region, + ), + Declaration::Expects(expects) | Declaration::ExpectsFx(expects) => Region::span_across( + expects.regions.first().unwrap(), + expects.regions.last().unwrap(), + ), + } + } +} + +/// Returns a topologically sorted sequence of alias/opaque names +fn sort_type_defs_before_introduction( + referenced_symbols: VecMap>, +) -> Vec { + let capacity = referenced_symbols.len(); + let mut matrix = ReferenceMatrix::new(capacity); + + let (symbols, referenced) = referenced_symbols.unzip(); + + for (index, references) in referenced.iter().enumerate() { + for referenced in references { + match symbols.iter().position(|k| k == referenced) { + None => { /* not defined in this scope */ } + Some(ref_index) => matrix.set_row_col(index, ref_index, true), + } + } + } + + // find the strongly connected components and their relations + matrix + .strongly_connected_components_all() + .groups() + .flat_map(|(group, _)| group.iter_ones()) + .map(|index| symbols[index]) + .collect() +} + +#[inline(always)] +#[allow(clippy::too_many_arguments)] +fn canonicalize_alias<'a>( + env: &mut Env<'a>, + output: &mut Output, + var_store: &mut VarStore, + scope: &mut Scope, + pending_abilities_in_scope: &PendingAbilitiesInScope, + + name: Loc, + ann: &'a Loc>, + vars: &[Loc], + kind: AliasKind, +) -> Result { + let symbol = name.value; + let annotation_for = match kind { + AliasKind::Structural => AnnotationFor::Alias, + AliasKind::Opaque => AnnotationFor::Opaque, + }; + let can_ann = canonicalize_annotation( + env, + scope, + &ann.value, + ann.region, + var_store, + pending_abilities_in_scope, + annotation_for, + ); + + // Record all the annotation's references in output.references.lookups + for symbol in can_ann.references { + output.references.insert_type_lookup(symbol); + } + + let mut can_vars: Vec> = Vec::with_capacity(vars.len()); + let mut is_phantom = false; + + let IntroducedVariables { + named, + able, + wildcards, + inferred, + infer_ext_in_output, + .. + } = can_ann.introduced_variables; + + let mut named: Vec<_> = (named.into_iter().map(OwnedNamedOrAble::Named)) + .chain(able.into_iter().map(OwnedNamedOrAble::Able)) + .collect(); + for loc_lowercase in vars.iter() { + let opt_index = named + .iter() + .position(|nv| nv.ref_name() == &loc_lowercase.value); + match opt_index { + Some(index) => { + // This is a valid lowercase rigid var for the type def. + let named_variable = named.swap_remove(index); + let var = named_variable.variable(); + let opt_bound_abilities = named_variable.opt_abilities().map(ToOwned::to_owned); + let name = named_variable.name(); + + can_vars.push(Loc { + value: AliasVar { + name, + var, + opt_bound_abilities, + }, + region: loc_lowercase.region, + }); + } + None => match kind { + AliasKind::Structural => { + is_phantom = true; + + env.problems.push(Problem::PhantomTypeArgument { + typ: symbol, + variable_region: loc_lowercase.region, + variable_name: loc_lowercase.value.clone(), + alias_kind: AliasKind::Structural, + }); + } + AliasKind::Opaque => { + // Opaques can have phantom types. + can_vars.push(Loc { + value: AliasVar { + name: loc_lowercase.value.clone(), + var: var_store.fresh(), + opt_bound_abilities: None, + }, + region: loc_lowercase.region, + }); + } + }, + } + } + + if is_phantom { + // Bail out + return Err(()); + } + + let num_unbound = named.len() + wildcards.len() + inferred.len(); + if num_unbound > 0 { + let one_occurrence = named + .iter() + .map(|nv| Loc::at(nv.first_seen(), nv.variable())) + .chain(wildcards) + .chain(inferred) + .next() + .unwrap() + .region; + + env.problems.push(Problem::UnboundTypeVariable { + typ: symbol, + num_unbound, + one_occurrence, + kind, + }); + + // Bail out + return Err(()); + } + + Ok(create_alias( + symbol, + name.region, + can_vars.clone(), + infer_ext_in_output, + can_ann.typ, + kind, + )) +} + +/// Canonicalizes a claimed ability implementation like `{ eq }` or `{ eq: myEq }`. +/// Returns a mapping of the ability member to the implementation symbol. +/// If there was an error, a problem will be recorded and nothing is returned. +fn canonicalize_claimed_ability_impl<'a>( + env: &mut Env<'a>, + scope: &mut Scope, + ability: Symbol, + loc_impl: &Loc>>, +) -> Result<(Symbol, Symbol), ()> { + let ability_home = ability.module_id(); + + match loc_impl.extract_spaces().item { + AssignedField::LabelOnly(label) => { + let label_str = label.value; + let region = label.region; + + let member_symbol = + match env.qualified_lookup_with_module_id(scope, ability_home, label_str, region) { + Ok(symbol) => symbol, + Err(_) => { + env.problem(Problem::NotAnAbilityMember { + ability, + name: label_str.to_owned(), + region, + }); + + return Err(()); + } + }; + + // There are two options for how the implementation symbol is defined. + // + // OPTION-1: The implementation identifier is the only identifier of that name in the + // scope. For example, + // + // interface F imports [] exposes [] + // + // Hello := {} implements [Encoding.{ toEncoder }] + // + // toEncoder = \@Hello {} -> ... + // + // In this case, we just do a simple lookup in the scope to find our + // `toEncoder` impl. + // + // OPTION-2: The implementation identifier is a unique shadow of the ability member, + // which has also been explicitly imported. For example, + // + // interface F imports [Encoding.{ toEncoder }] exposes [] + // + // Hello := {} implements [Encoding.{ toEncoder }] + // + // toEncoder = \@Hello {} -> ... + // + // In this case, we allow the `F` module's `toEncoder` def to shadow + // `toEncoder` only to define this specialization's `toEncoder`. + // + // To handle both cases, try checking for a shadow first, then check for a direct + // reference. We want to check for a direct reference second so that if there is a + // shadow, we won't accidentally grab the imported symbol. + let opt_impl_symbol = (scope.lookup_ability_member_shadow(member_symbol)) + .or_else(|| scope.lookup_str(label_str, region).ok()); + + match opt_impl_symbol { + // It's possible that even if we find a symbol it is still only the member + // definition symbol, for example when the ability is defined in the same + // module as an implementer: + // + // Eq implements eq : a, a -> U64 where a implements Eq + // + // A := U8 implements [Eq {eq}] + // + // So, do a final check that the implementation symbol is not resolved directly + // to the member. + Some(impl_symbol) if impl_symbol != member_symbol => { + Ok((member_symbol, impl_symbol)) + } + _ => { + env.problem(Problem::ImplementationNotFound { + member: member_symbol, + region: label.region, + }); + Err(()) + } + } + } + AssignedField::RequiredValue(label, _spaces, value) => { + let impl_ident = match value.value { + ast::Expr::Var { module_name, ident } => { + if module_name.is_empty() { + ident + } else { + env.problem(Problem::QualifiedAbilityImpl { + region: value.region, + }); + return Err(()); + } + } + _ => { + env.problem(Problem::AbilityImplNotIdent { + region: value.region, + }); + return Err(()); + } + }; + let impl_region = value.region; + + let member_symbol = match env.qualified_lookup_with_module_id( + scope, + ability_home, + label.value, + label.region, + ) { + Ok(symbol) => symbol, + Err(_) => { + env.problem(Problem::NotAnAbilityMember { + ability, + name: label.value.to_owned(), + region: label.region, + }); + return Err(()); + } + }; + + let impl_symbol = match scope.lookup(&impl_ident.into(), impl_region) { + Ok(symbol) => symbol, + Err(err) => { + env.problem(Problem::RuntimeError(err)); + return Err(()); + } + }; + + Ok((member_symbol, impl_symbol)) + } + AssignedField::OptionalValue(_, _, _) => { + env.problem(Problem::OptionalAbilityImpl { + ability, + region: loc_impl.region, + }); + Err(()) + } + AssignedField::Malformed(_) => { + // An error will already have been reported + Err(()) + } + AssignedField::SpaceBefore(_, _) | AssignedField::SpaceAfter(_, _) => { + internal_error!("unreachable") + } + } +} + +struct SeparatedMembers { + not_required: Vec, + not_implemented: Vec, +} + +/// Partitions ability members in a `has [ Ability {...members} ]` clause into the members the +/// opaque type claims to implement but are not part of the ability, and the ones it does not +/// implement. +fn separate_implemented_and_required_members( + implemented: VecSet, + required: VecSet, +) -> SeparatedMembers { + use std::cmp::Ordering; + + let mut implemented = implemented.into_vec(); + let mut required = required.into_vec(); + + implemented.sort(); + required.sort(); + + let mut implemented = implemented.into_iter().peekable(); + let mut required = required.into_iter().peekable(); + + let mut not_required = vec![]; + let mut not_implemented = vec![]; + + loop { + // Equal => both required and implemented + // Less => implemented but not required + // Greater => required but not implemented + + let ord = match (implemented.peek(), required.peek()) { + (Some(implemented), Some(required)) => Some(implemented.cmp(required)), + (Some(_), None) => Some(Ordering::Less), + (None, Some(_)) => Some(Ordering::Greater), + (None, None) => None, + }; + + match ord { + Some(Ordering::Less) => { + not_required.push(implemented.next().unwrap()); + } + Some(Ordering::Greater) => { + not_implemented.push(required.next().unwrap()); + } + Some(Ordering::Equal) => { + _ = implemented.next().unwrap(); + _ = required.next().unwrap(); + } + None => break, + } + } + + SeparatedMembers { + not_required, + not_implemented, + } +} + +type DerivedDef<'a> = Loc>; + +struct CanonicalizedOpaque<'a> { + opaque_def: Alias, + derived_defs: Vec>, +} + +#[inline(always)] +#[allow(clippy::too_many_arguments)] +fn canonicalize_opaque<'a>( + env: &mut Env<'a>, + output: &mut Output, + var_store: &mut VarStore, + scope: &mut Scope, + pending_abilities_in_scope: &PendingAbilitiesInScope, + + name: Loc, + name_str: &'a str, + ann: &'a Loc>, + vars: &[Loc], + has_abilities: Option<&'a Loc>>, +) -> Result, ()> { + let alias = canonicalize_alias( + env, + output, + var_store, + scope, + pending_abilities_in_scope, + name, + ann, + vars, + AliasKind::Opaque, + )?; + + let mut derived_defs = Vec::new(); + if let Some(has_abilities) = has_abilities { + let has_abilities = has_abilities.value.collection(); + + let mut derived_abilities = vec![]; + + for has_ability in has_abilities.items { + let region = has_ability.region; + let (ability, opt_impls) = match has_ability.value.extract_spaces().item { + ast::ImplementsAbility::ImplementsAbility { ability, impls } => (ability, impls), + _ => internal_error!("spaces not extracted"), + }; + + let ability_region = ability.region; + + // Op := {} has [Eq] + let (ability, members) = match ability.value { + ast::TypeAnnotation::Apply(module_name, ident, []) => { + match make_apply_symbol(env, region, scope, module_name, ident) { + Ok(ability) => { + let opt_members = scope + .abilities_store + .members_of_ability(ability) + .map(|members| members.iter().copied().collect()) + .or_else(|| pending_abilities_in_scope.get(&ability).cloned()); + + if let Some(members) = opt_members { + // This is an ability we already imported into the scope, + // or which is also undergoing canonicalization at the moment. + (ability, members) + } else { + env.problem(Problem::NotAnAbility(ability_region)); + continue; + } + } + Err(_) => { + // This is bad apply; an error will have been reported for it + // already. + continue; + } + } + } + _ => { + // Register the problem but keep going. + env.problem(Problem::NotAnAbility(ability_region)); + continue; + } + }; + + if let Some(impls) = opt_impls { + let mut impl_map: VecMap> = VecMap::default(); + + // First up canonicalize all the claimed implementations, building a map of ability + // member -> implementation. + for loc_impl in impls.extract_spaces().item.items { + let (member, impl_symbol) = + match canonicalize_claimed_ability_impl(env, scope, ability, loc_impl) { + Ok((member, impl_symbol)) => (member, impl_symbol), + Err(()) => continue, + }; + + // Did the user claim this implementation for a specialization of a different + // type? e.g. + // + // A implements [Hash {hash: myHash}] + // B implements [Hash {hash: myHash}] + // + // If so, that's an error and we drop the impl for this opaque type. + let member_impl = match scope.abilities_store.impl_key(impl_symbol) { + Some(ImplKey { + opaque, + ability_member, + }) => { + env.problem(Problem::OverloadedSpecialization { + overload: loc_impl.region, + original_opaque: *opaque, + ability_member: *ability_member, + }); + MemberImpl::Error + } + None => MemberImpl::Impl(impl_symbol), + }; + + // Did the user already claim an implementation for the ability member for this + // type previously? (e.g. Hash {hash: hash1, hash: hash2}) + let opt_old_impl_symbol = + impl_map.insert(member, Loc::at(loc_impl.region, member_impl)); + + if let Some(old_impl_symbol) = opt_old_impl_symbol { + env.problem(Problem::DuplicateImpl { + original: old_impl_symbol.region, + duplicate: loc_impl.region, + }); + } + } + + // Check that the members this opaque claims to implement corresponds 1-to-1 with + // the members the ability offers. + let SeparatedMembers { + not_required, + not_implemented, + } = separate_implemented_and_required_members( + impl_map.iter().map(|(member, _)| *member).collect(), + members, + ); + + if !not_required.is_empty() { + // Implementing something that's not required is a recoverable error, we don't + // need to skip association of the implemented abilities. Just remove the + // unneeded members. + for sym in not_required.iter() { + impl_map.remove(sym); + } + + env.problem(Problem::ImplementsNonRequired { + region, + ability, + not_required, + }); + } + + if !not_implemented.is_empty() { + // We'll generate runtime errors for the members that are needed but + // unspecified. + for sym in not_implemented.iter() { + impl_map.insert(*sym, Loc::at_zero(MemberImpl::Error)); + } + + env.problem(Problem::DoesNotImplementAbility { + region, + ability, + not_implemented, + }); + } + + let impls = impl_map + .into_iter() + .map(|(member, def)| (member, def.value)); + + scope + .abilities_store + .register_declared_implementations(name.value, impls); + } else if let Some((_, members)) = ability.derivable_ability() { + let num_members = members.len(); + + derived_defs.reserve(num_members); + + let mut impls = Vec::with_capacity(num_members); + for &member in members.iter() { + let (derived_impl, impl_pat, impl_body) = + derive::synthesize_member_impl(env, scope, name_str, member); + + let derived_def = Loc::at( + derive::DERIVED_REGION, + PendingValue::Def(PendingValueDef::Body(impl_pat, impl_body)), + ); + + impls.push((member, MemberImpl::Impl(derived_impl))); + derived_defs.push(derived_def); + } + + scope + .abilities_store + .register_declared_implementations(name.value, impls); + + derived_abilities.push(Loc::at(ability_region, ability)); + } else { + // There was no record specified of functions to use for + // members, but also this isn't a builtin ability, so we don't + // know how to auto-derive it. + env.problem(Problem::IllegalDerivedAbility(region)); + } + } + + if !derived_abilities.is_empty() { + // Fresh instance of this opaque to be checked for derivability during solving. + let fresh_inst = Type::DelayedAlias(AliasCommon { + symbol: name.value, + type_arguments: alias + .type_variables + .iter() + .map(|alias_var| { + Loc::at( + alias_var.region, + OptAbleType { + typ: Type::Variable(var_store.fresh()), + opt_abilities: alias_var.value.opt_bound_abilities.clone(), + }, + ) + }) + .collect(), + lambda_set_variables: alias + .lambda_set_variables + .iter() + .map(|_| LambdaSet(Type::Variable(var_store.fresh()))) + .collect(), + infer_ext_in_output_types: alias + .infer_ext_in_output_variables + .iter() + .map(|_| Type::Variable(var_store.fresh())) + .collect(), + }); + + let old = output + .pending_derives + .insert(name.value, (fresh_inst, derived_abilities)); + + debug_assert!(old.is_none()); + } + } + + Ok(CanonicalizedOpaque { + opaque_def: alias, + derived_defs, + }) +} + +#[inline(always)] +pub(crate) fn canonicalize_defs<'a>( + env: &mut Env<'a>, + mut output: Output, + var_store: &mut VarStore, + scope: &mut Scope, + loc_defs: &'a mut roc_parse::ast::Defs<'a>, + pattern_type: PatternType, +) -> (CanDefs, Output, MutMap) { + // Canonicalizing defs while detecting shadowing involves a multi-step process: + // + // 1. Go through each of the patterns. + // 2. For each identifier pattern, get the scope.symbol() for the ident. (That symbol will use the home module for its module.) + // 3. If that symbol is already in scope, then we're about to shadow it. Error! + // 4. Otherwise, add it to the scope immediately, so we can detect shadowing within the same + // pattern (e.g. (Foo a a) = ...) + // 5. Add this canonicalized pattern and its corresponding ast::Expr to pending_exprs. + // 5. Once every pattern has been processed and added to scope, go back and canonicalize the exprs from + // pending_exprs, this time building up a canonical def for each one. + // + // This way, whenever any expr is doing lookups, it knows everything that's in scope - + // even defs that appear after it in the source. + // + // This naturally handles recursion too, because a given expr which refers + // to itself won't be processed until after its def has been added to scope. + + let mut pending_type_defs = Vec::with_capacity(loc_defs.type_defs.len()); + let mut pending_value_defs = Vec::with_capacity(loc_defs.value_defs.len()); + let mut pending_abilities_in_scope = PendingAbilitiesInScope::default(); + + // Convert the type defs into pending defs first, then all the value defs. + // Follow this order because we need all value symbols to fully canonicalize type defs (in case + // there are opaques that implement an ability using a value symbol). But, value symbols might + // shadow symbols defined in a local ability def. + + for (_, either_index) in loc_defs.tags.iter().enumerate() { + if let Ok(type_index) = either_index.split() { + let type_def = &loc_defs.type_defs[type_index.index()]; + let pending_type_def = to_pending_type_def(env, type_def, scope, pattern_type); + if let PendingTypeDef::Ability { name, members } = &pending_type_def { + pending_abilities_in_scope.insert( + name.value, + members.iter().map(|mem| mem.name.value).collect(), + ); + } + pending_type_defs.push(pending_type_def); + } + } + + for (index, either_index) in loc_defs.tags.iter().enumerate() { + if let Err(value_index) = either_index.split() { + let value_def = &loc_defs.value_defs[value_index.index()]; + let region = loc_defs.regions[index]; + + let pending = to_pending_value_def( + env, + var_store, + value_def, + scope, + &pending_abilities_in_scope, + &mut output, + pattern_type, + ); + + pending_value_defs.push(Loc::at(region, pending)); + } + } + + if cfg!(debug_assertions) { + scope.register_debug_idents(); + } + + let CanonicalizedTypeDefs { + aliases, + symbols_introduced, + derived_defs, + } = canonicalize_type_defs( + env, + &mut output, + var_store, + scope, + &pending_abilities_in_scope, + pending_type_defs, + ); + + // Add the derived ASTs, so that we create proper canonicalized defs for them. + // They can go at the end, and derived defs should never reference anything other than builtin + // ability members. + pending_value_defs.extend(derived_defs); + + // Now that we have the scope completely assembled, and shadowing resolved, + // we're ready to canonicalize any body exprs. + canonicalize_value_defs( + env, + output, + var_store, + scope, + pending_value_defs, + pattern_type, + aliases, + symbols_introduced, + ) +} + +#[allow(clippy::too_many_arguments)] +fn canonicalize_value_defs<'a>( + env: &mut Env<'a>, + mut output: Output, + var_store: &mut VarStore, + scope: &mut Scope, + value_defs: Vec>>, + pattern_type: PatternType, + mut aliases: VecMap, + mut symbols_introduced: MutMap, +) -> (CanDefs, Output, MutMap) { + // Canonicalize all the patterns, record shadowing problems, and store + // the ast::Expr values in pending_exprs for further canonicalization + // once we've finished assembling the entire scope. + let mut pending_value_defs = Vec::with_capacity(value_defs.len()); + let mut pending_dbgs = Vec::with_capacity(value_defs.len()); + let mut pending_expects = Vec::with_capacity(value_defs.len()); + let mut pending_expect_fx = Vec::with_capacity(value_defs.len()); + + for loc_pending_def in value_defs { + match loc_pending_def.value { + PendingValue::Def(pending_def) => { + // Record the ast::Expr for later. We'll do another pass through these + // once we have the entire scope assembled. If we were to canonicalize + // the exprs right now, they wouldn't have symbols in scope from defs + // that get would have gotten added later in the defs list! + pending_value_defs.push(pending_def); + } + PendingValue::SignatureDefMismatch => { /* skip */ } + PendingValue::Dbg(pending_dbg) => { + pending_dbgs.push(pending_dbg); + } + PendingValue::Expect(pending_expect) => { + pending_expects.push(pending_expect); + } + PendingValue::ExpectFx(pending_expect) => { + pending_expect_fx.push(pending_expect); + } + } + } + + let mut symbol_to_index: Vec<(IdentId, u32)> = Vec::with_capacity(pending_value_defs.len()); + + for (def_index, pending_def) in pending_value_defs.iter().enumerate() { + let mut new_bindings = BindingsFromPattern::new(pending_def.loc_pattern()).peekable(); + + if new_bindings.peek().is_none() { + env.problem(Problem::NoIdentifiersIntroduced( + pending_def.loc_pattern().region, + )); + } + + for (s, r) in new_bindings { + // store the top-level defs, used to ensure that closures won't capture them + if let PatternType::TopLevelDef = pattern_type { + env.top_level_symbols.insert(s); + } + + symbols_introduced.insert(s, r); + + debug_assert_eq!(env.home, s.module_id()); + debug_assert!( + !symbol_to_index.iter().any(|(id, _)| *id == s.ident_id()), + "{s:?}" + ); + + symbol_to_index.push((s.ident_id(), def_index as u32)); + } + } + + let capacity = pending_value_defs.len(); + let mut defs = Vec::with_capacity(capacity); + let mut def_ordering = DefOrdering::from_symbol_to_id(env.home, symbol_to_index, capacity); + + for (def_id, pending_def) in pending_value_defs.into_iter().enumerate() { + let temp_output = canonicalize_pending_value_def( + env, + pending_def, + output, + scope, + var_store, + pattern_type, + &mut aliases, + ); + + output = temp_output.output; + + defs.push(Some(temp_output.def)); + + def_ordering.insert_symbol_references(def_id as u32, &temp_output.references) + } + + let mut dbgs = ExpectsOrDbgs::with_capacity(pending_dbgs.len()); + let mut expects = ExpectsOrDbgs::with_capacity(pending_expects.len()); + let mut expects_fx = ExpectsOrDbgs::with_capacity(pending_expects.len()); + + for pending in pending_dbgs { + let (loc_can_condition, can_output) = canonicalize_expr( + env, + var_store, + scope, + pending.condition.region, + &pending.condition.value, + ); + + dbgs.push(loc_can_condition, pending.preceding_comment); + + output.union(can_output); + } + + for pending in pending_expects { + let (loc_can_condition, can_output) = canonicalize_expr( + env, + var_store, + scope, + pending.condition.region, + &pending.condition.value, + ); + + expects.push(loc_can_condition, pending.preceding_comment); + + output.union(can_output); + } + + for pending in pending_expect_fx { + let (loc_can_condition, can_output) = canonicalize_expr( + env, + var_store, + scope, + pending.condition.region, + &pending.condition.value, + ); + + expects_fx.push(loc_can_condition, pending.preceding_comment); + + output.union(can_output); + } + + let can_defs = CanDefs { + defs, + dbgs, + expects, + expects_fx, + def_ordering, + aliases, + }; + + (can_defs, output, symbols_introduced) +} + +struct CanonicalizedTypeDefs<'a> { + aliases: VecMap, + symbols_introduced: MutMap, + derived_defs: Vec>, +} + +fn canonicalize_type_defs<'a>( + env: &mut Env<'a>, + output: &mut Output, + var_store: &mut VarStore, + scope: &mut Scope, + pending_abilities_in_scope: &PendingAbilitiesInScope, + pending_type_defs: Vec>, +) -> CanonicalizedTypeDefs<'a> { + enum TypeDef<'a> { + Alias( + Loc, + Vec>, + &'a Loc>, + ), + Opaque( + &'a str, + Loc, + Vec>, + &'a Loc>, + Option<&'a Loc>>, + ), + Ability(Loc, Vec>), + } + + let mut type_defs = MutMap::default(); + let mut referenced_type_symbols = VecMap::default(); + + // Determine which idents we introduced in the course of this process. + let mut symbols_introduced = MutMap::default(); + + for pending_def in pending_type_defs.into_iter() { + if let Some((symbol, region)) = pending_def.introduction() { + symbols_introduced.insert(symbol, region); + } + + match pending_def { + PendingTypeDef::Alias { name, vars, ann } => { + let referenced_symbols = find_type_def_symbols(scope, &ann.value); + + referenced_type_symbols.insert(name.value, referenced_symbols); + + type_defs.insert(name.value, TypeDef::Alias(name, vars, ann)); + } + PendingTypeDef::Opaque { + name_str, + name, + vars, + ann, + derived, + } => { + let referenced_symbols = find_type_def_symbols(scope, &ann.value); + + referenced_type_symbols.insert(name.value, referenced_symbols); + // Don't need to insert references for derived types, because these can only contain + // builtin abilities, and hence do not affect the type def sorting. We'll insert + // references of usages when canonicalizing the derives. + + type_defs.insert( + name.value, + TypeDef::Opaque(name_str, name, vars, ann, derived), + ); + } + PendingTypeDef::Ability { name, members } => { + let mut referenced_symbols = Vec::with_capacity(2); + + for member in members.iter() { + // Add the referenced type symbols of each member function. We need to make + // sure those are processed first before we resolve the whole ability + // definition. + referenced_symbols.extend(find_type_def_symbols(scope, &member.typ.value)); + } + + referenced_type_symbols.insert(name.value, referenced_symbols); + type_defs.insert(name.value, TypeDef::Ability(name, members)); + } + PendingTypeDef::InvalidAlias { .. } + | PendingTypeDef::InvalidAbility { .. } + | PendingTypeDef::AbilityShadows + | PendingTypeDef::ShadowedAlias { .. } + | PendingTypeDef::AbilityNotOnToplevel => { /* ignore */ } + } + } + + let sorted = sort_type_defs_before_introduction(referenced_type_symbols); + let mut aliases = VecMap::default(); + let mut abilities = MutMap::default(); + let mut all_derived_defs = Vec::new(); + + for type_name in sorted { + match type_defs.remove(&type_name).unwrap() { + TypeDef::Alias(name, vars, ann) => { + let alias = canonicalize_alias( + env, + output, + var_store, + scope, + pending_abilities_in_scope, + name, + ann, + &vars, + AliasKind::Structural, + ); + + if let Ok(alias) = alias { + aliases.insert(name.value, alias); + } + } + + TypeDef::Opaque(name_str, name, vars, ann, derived) => { + let alias_and_derives = canonicalize_opaque( + env, + output, + var_store, + scope, + pending_abilities_in_scope, + name, + name_str, + ann, + &vars, + derived, + ); + + if let Ok(CanonicalizedOpaque { + opaque_def, + derived_defs, + }) = alias_and_derives + { + aliases.insert(name.value, opaque_def); + all_derived_defs.extend(derived_defs); + } + } + + TypeDef::Ability(name, members) => { + // For now we enforce that aliases cannot reference abilities, so let's wait to + // resolve ability definitions until aliases are resolved and in scope below. + abilities.insert(name.value, members); + } + } + } + + // Now that we know the alias dependency graph, we can try to insert recursion variables + // where aliases are recursive tag unions, or detect illegal recursions. + let aliases = correct_mutual_recursive_type_alias(env, aliases, var_store); + + for (symbol, alias) in aliases.iter() { + scope.add_alias( + *symbol, + alias.region, + alias.type_variables.clone(), + alias.infer_ext_in_output_variables.clone(), + alias.typ.clone(), + alias.kind, + ); + } + + // Resolve all pending abilities, to add them to scope. + resolve_abilities( + env, + output, + var_store, + scope, + abilities, + pending_abilities_in_scope, + ); + + CanonicalizedTypeDefs { + aliases, + symbols_introduced, + derived_defs: all_derived_defs, + } +} + +/// Resolve all pending abilities, to add them to scope. +#[allow(clippy::too_many_arguments)] +fn resolve_abilities( + env: &mut Env, + output: &mut Output, + var_store: &mut VarStore, + scope: &mut Scope, + abilities: MutMap>, + pending_abilities_in_scope: &PendingAbilitiesInScope, +) { + for (ability, members) in abilities { + let mut can_members = Vec::with_capacity(members.len()); + + for PendingAbilityMember { + name: + Loc { + value: member_sym, + region: member_name_region, + }, + typ, + } in members + { + let member_annot = canonicalize_annotation( + env, + scope, + &typ.value, + typ.region, + var_store, + pending_abilities_in_scope, + AnnotationFor::Value, + ); + + // Record all the annotation's references in output.references.lookups + for symbol in member_annot.references { + output.references.insert_type_lookup(symbol); + } + + // What variables in the annotation are bound to the parent ability, and what variables + // are bound to some other ability? + let (variables_bound_to_ability, _variables_bound_to_other_abilities): ( + Vec<_>, + Vec<_>, + ) = member_annot + .introduced_variables + .able + .iter() + .partition(|av| av.abilities.contains(&ability)); + + let var_bound_to_ability = match variables_bound_to_ability.as_slice() { + [one] => one.variable, + [] => { + // There are no variables bound to the parent ability - then this member doesn't + // need to be a part of the ability. + env.problem(Problem::AbilityMemberMissingImplementsClause { + member: member_sym, + ability, + region: member_name_region, + }); + // Pretend the member isn't a part of the ability + continue; + } + [..] => { + // There is more than one variable bound to the member signature, so something like + // Eq implements eq : a, b -> Bool where a implements Eq, b implements Eq + // We have no way of telling what type implements a particular instance of Eq in + // this case (a or b?), so disallow it. + let span_has_clauses = Region::across_all( + variables_bound_to_ability.iter().map(|v| &v.first_seen), + ); + let bound_var_names = variables_bound_to_ability + .iter() + .map(|v| v.name.clone()) + .collect(); + env.problem(Problem::AbilityMemberMultipleBoundVars { + member: member_sym, + ability, + span_implements_clauses: span_has_clauses, + bound_var_names, + }); + // Pretend the member isn't a part of the ability + continue; + } + }; + + // The introduced variables are good; add them to the output. + output + .introduced_variables + .union(&member_annot.introduced_variables); + + let iv = member_annot.introduced_variables; + + let variables = MemberVariables { + able_vars: iv.collect_able(), + rigid_vars: iv.collect_rigid(), + flex_vars: iv.collect_flex(), + }; + + let signature = { + let mut signature = member_annot.typ; + signature + .instantiate_lambda_sets_as_unspecialized(var_bound_to_ability, member_sym); + signature + }; + + can_members.push(( + member_sym, + AbilityMemberData { + parent_ability: ability, + region: member_name_region, + typ: PendingMemberType::Local { + variables, + signature, + signature_var: var_store.fresh(), + }, + }, + )); + } + + // Store what symbols a type must define implementations for to have this ability. + scope.abilities_store.register_ability(ability, can_members); + } +} + +#[derive(Debug)] +struct DefOrdering { + home: ModuleId, + symbol_to_id: Vec<(IdentId, u32)>, + + // an length x length matrix indicating who references who + references: ReferenceMatrix, + + // references without looking into closure bodies. + // Used to spot definitely-wrong recursion + direct_references: ReferenceMatrix, +} + +impl DefOrdering { + fn from_symbol_to_id( + home: ModuleId, + symbol_to_id: Vec<(IdentId, u32)>, + capacity: usize, + ) -> Self { + // NOTE: because of `Pair a b = someDef` patterns, we can have more symbols than defs + // but because `_ = someDef` we can also have more defs than symbols + + Self { + home, + symbol_to_id, + references: ReferenceMatrix::new(capacity), + direct_references: ReferenceMatrix::new(capacity), + } + } + + fn insert_symbol_references(&mut self, def_id: u32, def_references: &DefReferences) { + match def_references { + DefReferences::Value(references) => { + let it = references.value_lookups().chain(references.calls()); + + for referenced in it { + if let Some(ref_id) = self.get_id(*referenced) { + self.references + .set_row_col(def_id as usize, ref_id as usize, true); + + self.direct_references + .set_row_col(def_id as usize, ref_id as usize, true); + } + } + } + DefReferences::Function(references) => { + let it = references.value_lookups().chain(references.calls()); + + for referenced in it { + if let Some(ref_id) = self.get_id(*referenced) { + self.references + .set_row_col(def_id as usize, ref_id as usize, true); + } + } + } + DefReferences::AnnotationWithoutBody => { + // annotatations without bodies don't reference any other definitions + } + } + } + + fn get_id(&self, symbol: Symbol) -> Option { + if symbol.module_id() != self.home { + return None; + } + + let target = symbol.ident_id(); + + for (ident_id, def_id) in self.symbol_to_id.iter() { + if target == *ident_id { + return Some(*def_id); + } + } + + None + } + + fn get_symbol(&self, id: usize) -> Option { + for (ident_id, def_id) in self.symbol_to_id.iter() { + if id as u32 == *def_id { + return Some(Symbol::new(self.home, *ident_id)); + } + } + + None + } +} + +#[inline(always)] +pub(crate) fn sort_can_defs_new( + env: &mut Env<'_>, + scope: &mut Scope, + var_store: &mut VarStore, + defs: CanDefs, + mut output: Output, + exposed_symbols: &VecSet, +) -> (Declarations, Output) { + let CanDefs { + defs, + dbgs: _, + expects, + expects_fx, + def_ordering, + aliases, + } = defs; + + // TODO: inefficient, but I want to make this what CanDefs contains in the future + let mut defs: Vec<_> = defs.into_iter().map(|x| x.unwrap()).collect(); + + // symbols are put in declarations in dependency order, from "main" up, so + // + // x = 3 + // y = x + 1 + // + // will get ordering [ y, x ] + let mut declarations = Declarations::with_capacity(defs.len()); + + // because of the ordering of declarations, expects should come first because they are + // independent, but can rely on all other top-level symbols in the module + let it = expects + .conditions + .into_iter() + .zip(expects.regions) + .zip(expects.preceding_comment); + + for ((condition, region), preceding_comment) in it { + // an `expect` does not have a user-defined name, but we'll need a name to call the expectation + let name = scope.gen_unique_symbol(); + + declarations.push_expect(preceding_comment, name, Loc::at(region, condition)); + } + + let it = expects_fx + .conditions + .into_iter() + .zip(expects_fx.regions) + .zip(expects_fx.preceding_comment); + + for ((condition, region), preceding_comment) in it { + // an `expect` does not have a user-defined name, but we'll need a name to call the expectation + let name = scope.gen_unique_symbol(); + + declarations.push_expect_fx(preceding_comment, name, Loc::at(region, condition)); + } + + for (symbol, alias) in aliases.into_iter() { + output.aliases.insert(symbol, alias); + } + + // We first perform SCC based on any reference, both variable usage and calls + // considering both value definitions and function bodies. This will spot any + // recursive relations between any 2 definitions. + let sccs = def_ordering.references.strongly_connected_components_all(); + + sccs.reorder(&mut defs); + + for (group, is_initial) in sccs.groups().rev() { + match group.count_ones() { + 1 => { + // a group with a single Def, nice and simple + let def = defs.pop().unwrap(); + let index = group.first_one().unwrap(); + + if def_ordering.references.get_row_col(index, index) { + // push the "header" for this group of recursive definitions + let cycle_mark = IllegalCycleMark::new(var_store); + declarations.push_recursive_group(1, cycle_mark); + + // then push the definition + let (symbol, specializes) = match def.loc_pattern.value { + Pattern::Identifier(symbol) => (symbol, None), + + Pattern::AbilityMemberSpecialization { ident, specializes } => { + (ident, Some(specializes)) + } + + _ => { + internal_error!("destructures cannot participate in a recursive group; it's always a type error") + } + }; + + let host_annotation = if exposed_symbols.contains(&symbol) { + def.annotation + .clone() + .map(|a| (var_store.fresh(), a.freshen(var_store))) + } else { + None + }; + + if is_initial && !exposed_symbols.contains(&symbol) { + env.problem(Problem::DefsOnlyUsedInRecursion(1, def.region())); + } + + match def.loc_expr.value { + Closure(closure_data) => { + declarations.push_recursive_def( + Loc::at(def.loc_pattern.region, symbol), + Loc::at(def.loc_expr.region, closure_data), + def.expr_var, + def.annotation, + host_annotation, + specializes, + ); + } + _ => { + declarations.push_value_def( + Loc::at(def.loc_pattern.region, symbol), + def.loc_expr, + def.expr_var, + def.annotation, + host_annotation, + specializes, + ); + } + } + } else { + match def.loc_pattern.value { + Pattern::Identifier(symbol) => { + let host_annotation = if exposed_symbols.contains(&symbol) { + def.annotation + .clone() + .map(|a| (var_store.fresh(), a.freshen(var_store))) + } else { + None + }; + + match def.loc_expr.value { + Closure(closure_data) => { + declarations.push_function_def( + Loc::at(def.loc_pattern.region, symbol), + Loc::at(def.loc_expr.region, closure_data), + def.expr_var, + def.annotation, + host_annotation, + None, + ); + } + _ => { + declarations.push_value_def( + Loc::at(def.loc_pattern.region, symbol), + def.loc_expr, + def.expr_var, + def.annotation, + host_annotation, + None, + ); + } + } + } + Pattern::AbilityMemberSpecialization { + ident: symbol, + specializes, + } => { + let host_annotation = if exposed_symbols.contains(&symbol) { + def.annotation + .clone() + .map(|a| (var_store.fresh(), a.freshen(var_store))) + } else { + None + }; + + match def.loc_expr.value { + Closure(closure_data) => { + declarations.push_function_def( + Loc::at(def.loc_pattern.region, symbol), + Loc::at(def.loc_expr.region, closure_data), + def.expr_var, + def.annotation, + host_annotation, + Some(specializes), + ); + } + _ => { + declarations.push_value_def( + Loc::at(def.loc_pattern.region, symbol), + def.loc_expr, + def.expr_var, + def.annotation, + host_annotation, + Some(specializes), + ); + } + } + } + _ => { + declarations.push_destructure_def( + def.loc_pattern, + def.loc_expr, + def.expr_var, + def.annotation, + def.pattern_vars.into_iter().collect(), + ); + } + } + } + } + group_length => { + let group_defs = defs.split_off(defs.len() - group_length); + + // push the "header" for this group of recursive definitions + let cycle_mark = IllegalCycleMark::new(var_store); + declarations.push_recursive_group(group_length as u16, cycle_mark); + + let mut group_is_initial = is_initial; + let mut whole_region = None; + + // then push the definitions of this group + for def in group_defs { + let (symbol, specializes) = match def.loc_pattern.value { + Pattern::Identifier(symbol) => (symbol, None), + + Pattern::AbilityMemberSpecialization { ident, specializes } => { + (ident, Some(specializes)) + } + + _ => { + internal_error!("destructures cannot participate in a recursive group; it's always a type error") + } + }; + + group_is_initial = group_is_initial && !exposed_symbols.contains(&symbol); + whole_region = match whole_region { + None => Some(def.region()), + Some(r) => Some(Region::span_across(&r, &def.region())), + }; + + let host_annotation = if exposed_symbols.contains(&symbol) { + def.annotation + .clone() + .map(|a| (var_store.fresh(), a.freshen(var_store))) + } else { + None + }; + + match def.loc_expr.value { + Closure(closure_data) => { + declarations.push_recursive_def( + Loc::at(def.loc_pattern.region, symbol), + Loc::at(def.loc_expr.region, closure_data), + def.expr_var, + def.annotation, + host_annotation, + specializes, + ); + } + _ => { + declarations.push_value_def( + Loc::at(def.loc_pattern.region, symbol), + def.loc_expr, + def.expr_var, + def.annotation, + host_annotation, + specializes, + ); + } + } + } + + if group_is_initial { + env.problem(Problem::DefsOnlyUsedInRecursion( + group_length, + whole_region.unwrap(), + )); + } + } + } + } + + (declarations, output) +} + +#[inline(always)] +pub(crate) fn sort_can_defs( + env: &mut Env<'_>, + var_store: &mut VarStore, + defs: CanDefs, + mut output: Output, +) -> (Vec, Output) { + let CanDefs { + mut defs, + dbgs, + expects, + expects_fx, + def_ordering, + aliases, + } = defs; + + for (symbol, alias) in aliases.into_iter() { + output.aliases.insert(symbol, alias); + } + + macro_rules! take_def { + ($index:expr) => { + match defs[$index].take() { + Some(def) => def, + None => { + // NOTE: a `_ = someDef` can mean we don't have a symbol here + let symbol = def_ordering.get_symbol($index); + + roc_error_macros::internal_error!("def not available {:?}", symbol) + } + } + }; + } + + // We first perform SCC based on any reference, both variable usage and calls + // considering both value definitions and function bodies. This will spot any + // recursive relations between any 2 definitions. + let sccs = def_ordering.references.strongly_connected_components_all(); + + let mut declarations = Vec::with_capacity(defs.len()); + + for (group, is_initial) in sccs.groups() { + if group.count_ones() == 1 { + // a group with a single Def, nice and simple + let index = group.iter_ones().next().unwrap(); + + let def = take_def!(index); + let is_specialization = matches!( + def.loc_pattern.value, + Pattern::AbilityMemberSpecialization { .. } + ); + + let declaration = if def_ordering.references.get_row_col(index, index) { + debug_assert!(!is_specialization, "Self-recursive specializations can only be determined during solving - but it was determined for {def:?} now, that's a bug!"); + + if is_initial + && !def + .pattern_vars + .keys() + .any(|sym| output.references.has_value_lookup(*sym)) + { + // This defs is only used in recursion with itself. + env.problem(Problem::DefsOnlyUsedInRecursion(1, def.region())); + } + + // this function calls itself, and must be typechecked as a recursive def + Declaration::DeclareRec(vec![mark_def_recursive(def)], IllegalCycleMark::empty()) + } else { + Declaration::Declare(def) + }; + + declarations.push(declaration); + } else { + // There is something recursive going on between the Defs of this group. + // Now we use the direct_references to see if it is clearly invalid recursion, e.g. + // + // x = y + // y = x + // + // We allow indirect recursion (behind a lambda), e.g. + // + // boom = \{} -> boom {} + // + // In general we cannot spot faulty recursion (halting problem), so this is our + // purely-syntactic heuristic. We'll have a second attempt once we know the types in + // the cycle. + let direct_sccs = def_ordering + .direct_references + .strongly_connected_components_subset(group); + + debug_assert!( + !group.iter_ones().any(|index| matches!(defs[index].as_ref().unwrap().loc_pattern.value, Pattern::AbilityMemberSpecialization{..})), + "A specialization is involved in a recursive cycle - this should not be knowable until solving"); + + let declaration = if direct_sccs.groups().count() == 1 { + // all defs are part of the same direct cycle, that is invalid! + let mut entries = Vec::with_capacity(group.count_ones()); + + for index in group.iter_ones() { + let def = take_def!(index); + let symbol = def_ordering.get_symbol(index).unwrap(); + + entries.push(make_cycle_entry(symbol, &def)) + } + + let problem = Problem::RuntimeError(RuntimeError::CircularDef(entries.clone())); + env.problem(problem); + + Declaration::InvalidCycle(entries) + } else { + let rec_defs: Vec = group + .iter_ones() + .map(|index| mark_def_recursive(take_def!(index))) + .collect(); + + if is_initial + && !rec_defs.iter().any(|def| { + def.pattern_vars + .keys() + .any(|sym| output.references.has_value_lookup(*sym)) + }) + { + // These defs are only used in mutual recursion with themselves. + let region = Region::span_across( + &rec_defs.first().unwrap().region(), + &rec_defs.last().unwrap().region(), + ); + env.problem(Problem::DefsOnlyUsedInRecursion(rec_defs.len(), region)); + } + + Declaration::DeclareRec(rec_defs, IllegalCycleMark::new(var_store)) + }; + + declarations.push(declaration); + } + } + + if !dbgs.conditions.is_empty() { + declarations.push(Declaration::Expects(dbgs)); + } + + if !expects.conditions.is_empty() { + declarations.push(Declaration::Expects(expects)); + } + + if !expects_fx.conditions.is_empty() { + declarations.push(Declaration::ExpectsFx(expects_fx)); + } + + (declarations, output) +} + +fn mark_def_recursive(mut def: Def) -> Def { + if let Closure(ClosureData { + recursive: recursive @ Recursive::NotRecursive, + .. + }) = &mut def.loc_expr.value + { + *recursive = Recursive::Recursive + } + + def +} + +fn make_cycle_entry(symbol: Symbol, def: &Def) -> CycleEntry { + CycleEntry { + symbol, + symbol_region: def.loc_pattern.region, + expr_region: def.loc_expr.region, + } +} + +fn pattern_to_vars_by_symbol( + vars_by_symbol: &mut SendMap, + pattern: &Pattern, + expr_var: Variable, +) { + use Pattern::*; + match pattern { + Identifier(symbol) | Shadowed(_, _, symbol) => { + vars_by_symbol.insert(*symbol, expr_var); + } + + As(subpattern, symbol) => { + vars_by_symbol.insert(*symbol, expr_var); + + pattern_to_vars_by_symbol(vars_by_symbol, &subpattern.value, expr_var); + } + + AbilityMemberSpecialization { + ident, + specializes: _, + } => { + vars_by_symbol.insert(*ident, expr_var); + } + + AppliedTag { arguments, .. } => { + for (var, nested) in arguments { + pattern_to_vars_by_symbol(vars_by_symbol, &nested.value, *var); + } + } + + UnwrappedOpaque { + argument, opaque, .. + } => { + let (var, nested) = &**argument; + pattern_to_vars_by_symbol(vars_by_symbol, &nested.value, *var); + vars_by_symbol.insert(*opaque, expr_var); + } + + TupleDestructure { destructs, .. } => { + for destruct in destructs { + pattern_to_vars_by_symbol( + vars_by_symbol, + &destruct.value.typ.1.value, + destruct.value.typ.0, + ); + } + } + + RecordDestructure { destructs, .. } => { + for destruct in destructs { + vars_by_symbol.insert(destruct.value.symbol, destruct.value.var); + } + } + + List { + patterns, elem_var, .. + } => { + for pat in patterns.patterns.iter() { + pattern_to_vars_by_symbol(vars_by_symbol, &pat.value, *elem_var); + } + } + + NumLiteral(..) + | IntLiteral(..) + | FloatLiteral(..) + | StrLiteral(_) + | SingleQuote(..) + | Underscore + | MalformedPattern(_, _) + | UnsupportedPattern(_) + | OpaqueNotInScope(..) => {} + } +} + +fn single_can_def( + loc_can_pattern: Loc, + loc_can_expr: Loc, + expr_var: Variable, + opt_loc_annotation: Option>, + pattern_vars: SendMap, +) -> Def { + let def_annotation = opt_loc_annotation.map(|loc_annotation| Annotation { + signature: loc_annotation.value.typ, + introduced_variables: loc_annotation.value.introduced_variables, + aliases: loc_annotation.value.aliases, + region: loc_annotation.region, + }); + + Def { + expr_var, + loc_pattern: loc_can_pattern, + loc_expr: Loc { + region: loc_can_expr.region, + value: loc_can_expr.value, + }, + pattern_vars, + annotation: def_annotation, + } +} + +// Functions' references don't count in defs. +// See 3d5a2560057d7f25813112dfa5309956c0f9e6a9 and its +// parent commit for the bug this fixed! +enum DefReferences { + /// A value may not reference itself + Value(References), + + /// If the def is a function, different rules apply (it can call itself) + Function(References), + + /// An annotation without a body references no other defs + AnnotationWithoutBody, +} + +struct DefOutput { + output: Output, + def: Def, + references: DefReferences, +} + +// TODO trim down these arguments! +#[allow(clippy::too_many_arguments)] +#[allow(clippy::cognitive_complexity)] +fn canonicalize_pending_value_def<'a>( + env: &mut Env<'a>, + pending_def: PendingValueDef<'a>, + mut output: Output, + scope: &mut Scope, + var_store: &mut VarStore, + pattern_type: PatternType, + aliases: &mut VecMap, +) -> DefOutput { + use PendingValueDef::*; + + // All abilities should be resolved by the time we're canonicalizing value defs. + let pending_abilities_in_scope = &Default::default(); + + let output = match pending_def { + AnnotationOnly(_, loc_can_pattern, loc_ann) => { + // Make types for the body expr, even if we won't end up having a body. + let expr_var = var_store.fresh(); + let mut vars_by_symbol = SendMap::default(); + + // annotation sans body cannot introduce new rigids that are visible in other annotations + // but the rigids can show up in type error messages, so still register them + let type_annotation = canonicalize_annotation( + env, + scope, + &loc_ann.value, + loc_ann.region, + var_store, + pending_abilities_in_scope, + AnnotationFor::Value, + ); + + // Record all the annotation's references in output.references.lookups + type_annotation.add_to( + aliases, + &mut output.references, + &mut output.introduced_variables, + ); + + pattern_to_vars_by_symbol(&mut vars_by_symbol, &loc_can_pattern.value, expr_var); + + let arity = type_annotation.typ.arity(); + + let problem = match &loc_can_pattern.value { + Pattern::Identifier(symbol) => RuntimeError::NoImplementationNamed { + def_symbol: *symbol, + }, + Pattern::Shadowed(region, loc_ident, _new_symbol) => RuntimeError::Shadowing { + original_region: *region, + shadow: loc_ident.clone(), + kind: ShadowKind::Variable, + }, + _ => RuntimeError::NoImplementation, + }; + + // Fabricate a body for this annotation, that will error at runtime + let value = Expr::RuntimeError(problem); + let is_closure = arity > 0; + let loc_can_expr = if !is_closure { + Loc { + value, + region: loc_ann.region, + } + } else { + let symbol = match &loc_can_pattern.value { + Pattern::Identifier(symbol) => *symbol, + _ => scope.gen_unique_symbol(), + }; + + // generate a fake pattern for each argument. this makes signatures + // that are functions only crash when they are applied. + let mut underscores = Vec::with_capacity(arity); + + for _ in 0..arity { + let underscore: Loc = Loc { + value: Pattern::Underscore, + region: Region::zero(), + }; + + underscores.push(( + var_store.fresh(), + AnnotatedMark::known_exhaustive(), + underscore, + )); + } + + let body_expr = Loc { + value, + region: loc_ann.region, + }; + + Loc { + value: Closure(ClosureData { + function_type: var_store.fresh(), + closure_type: var_store.fresh(), + return_type: var_store.fresh(), + name: symbol, + captured_symbols: Vec::new(), + recursive: Recursive::NotRecursive, + arguments: underscores, + loc_body: Box::new(body_expr), + }), + region: loc_ann.region, + } + }; + + let def = single_can_def( + loc_can_pattern, + loc_can_expr, + expr_var, + Some(Loc::at(loc_ann.region, type_annotation)), + vars_by_symbol.clone(), + ); + + DefOutput { + output, + references: DefReferences::AnnotationWithoutBody, + def, + } + } + + TypedBody(_loc_pattern, loc_can_pattern, loc_ann, loc_expr) => { + let type_annotation = canonicalize_annotation( + env, + scope, + &loc_ann.value, + loc_ann.region, + var_store, + pending_abilities_in_scope, + AnnotationFor::Value, + ); + + // Record all the annotation's references in output.references.lookups + type_annotation.add_to( + aliases, + &mut output.references, + &mut output.introduced_variables, + ); + + canonicalize_pending_body( + env, + output, + scope, + var_store, + loc_can_pattern, + loc_expr, + Some(Loc::at(loc_ann.region, type_annotation)), + ) + } + Body(loc_can_pattern, loc_expr) => { + // + canonicalize_pending_body( + env, + output, + scope, + var_store, + loc_can_pattern, + loc_expr, + None, + ) + } + }; + + // Disallow ability specializations that aren't on the toplevel (note: we might loosen this + // restriction later on). + if pattern_type != PatternType::TopLevelDef { + if let Loc { + value: Pattern::AbilityMemberSpecialization { specializes, .. }, + region, + } = output.def.loc_pattern + { + env.problem(Problem::NestedSpecialization(specializes, region)); + } + } + + output +} + +// TODO trim down these arguments! +#[allow(clippy::too_many_arguments)] +#[allow(clippy::cognitive_complexity)] +fn canonicalize_pending_body<'a>( + env: &mut Env<'a>, + mut output: Output, + scope: &mut Scope, + var_store: &mut VarStore, + + loc_can_pattern: Loc, + loc_expr: &'a Loc, + + opt_loc_annotation: Option>, +) -> DefOutput { + let mut loc_value = &loc_expr.value; + + while let ast::Expr::ParensAround(value) = loc_value { + loc_value = value; + } + + // We treat closure definitions `foo = \a, b -> ...` differently from other body expressions, + // because they need more bookkeeping (for tail calls, closure captures, etc.) + // + // Only defs of the form `foo = ...` can be closure declarations or self tail calls. + let (loc_can_expr, def_references) = { + match (&loc_can_pattern.value, &loc_value) { + ( + Pattern::Identifier(defined_symbol) + | Pattern::AbilityMemberSpecialization { + ident: defined_symbol, + .. + }, + ast::Expr::Closure(arguments, body), + ) => { + // bookkeeping for tail-call detection. + let outer_tailcallable = env.tailcallable_symbol; + env.tailcallable_symbol = Some(*defined_symbol); + + let (mut closure_data, can_output) = crate::expr::canonicalize_closure( + env, + var_store, + scope, + arguments, + body, + Some(*defined_symbol), + ); + + // reset the tailcallable_symbol + env.tailcallable_symbol = outer_tailcallable; + + // The closure is self tail recursive iff it tail calls itself (by defined name). + let is_recursive = match can_output.tail_call { + Some(tail_symbol) if tail_symbol == *defined_symbol => Recursive::TailRecursive, + _ => Recursive::NotRecursive, + }; + + closure_data.recursive = is_recursive; + closure_data.name = *defined_symbol; + + let loc_can_expr = Loc::at(loc_expr.region, Expr::Closure(closure_data)); + + let def_references = DefReferences::Function(can_output.references.clone()); + output.union(can_output); + + (loc_can_expr, def_references) + } + + // Turn f = .foo into f = \rcd -[f]-> rcd.foo + ( + Pattern::Identifier(defined_symbol) + | Pattern::AbilityMemberSpecialization { + ident: defined_symbol, + .. + }, + ast::Expr::AccessorFunction(field), + ) => { + let field = match field { + Accessor::RecordField(field) => IndexOrField::Field((*field).into()), + Accessor::TupleIndex(index) => IndexOrField::Index(index.parse().unwrap()), + }; + let (loc_can_expr, can_output) = ( + Loc::at( + loc_expr.region, + RecordAccessor(StructAccessorData { + name: *defined_symbol, + function_var: var_store.fresh(), + record_var: var_store.fresh(), + ext_var: var_store.fresh(), + closure_var: var_store.fresh(), + field_var: var_store.fresh(), + field, + }), + ), + Output::default(), + ); + let def_references = DefReferences::Value(can_output.references.clone()); + output.union(can_output); + + (loc_can_expr, def_references) + } + + _ => { + let (loc_can_expr, can_output) = + canonicalize_expr(env, var_store, scope, loc_expr.region, &loc_expr.value); + + let def_references = DefReferences::Value(can_output.references.clone()); + output.union(can_output); + + (loc_can_expr, def_references) + } + } + }; + + let expr_var = var_store.fresh(); + let mut vars_by_symbol = SendMap::default(); + + pattern_to_vars_by_symbol(&mut vars_by_symbol, &loc_can_pattern.value, expr_var); + + let def = single_can_def( + loc_can_pattern, + loc_can_expr, + expr_var, + opt_loc_annotation, + vars_by_symbol, + ); + + DefOutput { + output, + references: def_references, + def, + } +} + +#[inline(always)] +pub fn can_defs_with_return<'a>( + env: &mut Env<'a>, + var_store: &mut VarStore, + scope: &mut Scope, + loc_defs: &'a mut Defs<'a>, + loc_ret: &'a Loc>, +) -> (Expr, Output) { + let (unsorted, defs_output, symbols_introduced) = canonicalize_defs( + env, + Output::default(), + var_store, + scope, + loc_defs, + PatternType::DefExpr, + ); + + // The def as a whole is a tail call iff its return expression is a tail call. + // Use its output as a starting point because its tail_call already has the right answer! + let (ret_expr, mut output) = + canonicalize_expr(env, var_store, scope, loc_ret.region, &loc_ret.value); + + output + .introduced_variables + .union(&defs_output.introduced_variables); + + // Sort the defs with the output of the return expression - we'll use this to catch unused defs + // due only to recursion. + let (declarations, mut output) = sort_can_defs(env, var_store, unsorted, output); + + output.references.union_mut(&defs_output.references); + + // Now that we've collected all the references, check to see if any of the new idents + // we defined went unused by the return expression or any other def. + for (symbol, region) in symbols_introduced { + if !output.references.has_type_or_value_lookup(symbol) + && !scope.abilities_store.is_specialization_name(symbol) + { + env.problem(Problem::UnusedDef(symbol, region)); + } + } + + let mut loc_expr: Loc = ret_expr; + + for declaration in declarations.into_iter().rev() { + loc_expr = decl_to_let(declaration, loc_expr); + } + + (loc_expr.value, output) +} + +fn decl_to_let(decl: Declaration, loc_ret: Loc) -> Loc { + match decl { + Declaration::Declare(def) => { + let region = Region::span_across(&def.loc_pattern.region, &loc_ret.region); + let expr = Expr::LetNonRec(Box::new(def), Box::new(loc_ret)); + Loc::at(region, expr) + } + Declaration::DeclareRec(defs, cycle_mark) => { + let region = Region::span_across(&defs[0].loc_pattern.region, &loc_ret.region); + let expr = Expr::LetRec(defs, Box::new(loc_ret), cycle_mark); + Loc::at(region, expr) + } + Declaration::InvalidCycle(entries) => { + Loc::at_zero(Expr::RuntimeError(RuntimeError::CircularDef(entries))) + } + Declaration::Builtin(_) => { + // Builtins should only be added to top-level decls, not to let-exprs! + unreachable!() + } + Declaration::Expects(expects) => { + let mut loc_ret = loc_ret; + + let conditions = expects.conditions.into_iter().rev(); + let condition_regions = expects.regions.into_iter().rev(); + let expect_regions = expects.preceding_comment.into_iter().rev(); + + let it = expect_regions.zip(condition_regions).zip(conditions); + + for ((expect_region, condition_region), condition) in it { + let region = Region::span_across(&expect_region, &loc_ret.region); + let lookups_in_cond = get_lookup_symbols(&condition); + + let expr = Expr::Expect { + loc_condition: Box::new(Loc::at(condition_region, condition)), + loc_continuation: Box::new(loc_ret), + lookups_in_cond, + }; + + loc_ret = Loc::at(region, expr); + } + + loc_ret + } + Declaration::ExpectsFx(expects) => { + // Expects should only be added to top-level decls, not to let-exprs! + unreachable!("{:?}", &expects) + } + } +} + +fn to_pending_alias_or_opaque<'a>( + env: &mut Env<'a>, + scope: &mut Scope, + name: &'a Loc<&'a str>, + vars: &'a [Loc>], + ann: &'a Loc>, + opt_derived: Option<&'a Loc>>, + kind: AliasKind, +) -> PendingTypeDef<'a> { + let region = Region::span_across(&name.region, &ann.region); + + match scope.introduce_without_shadow_symbol(&Ident::from(name.value), region) { + Ok(symbol) => { + let mut can_rigids: Vec> = Vec::with_capacity(vars.len()); + + for loc_var in vars.iter() { + match loc_var.value { + ast::Pattern::Identifier(name) + if name.chars().next().unwrap().is_lowercase() => + { + let lowercase = Lowercase::from(name); + can_rigids.push(Loc { + value: lowercase, + region: loc_var.region, + }); + } + _ => { + // any other pattern in this position is a syntax error. + let problem = Problem::InvalidAliasRigid { + alias_name: symbol, + region: loc_var.region, + }; + env.problems.push(problem); + + return PendingTypeDef::InvalidAlias { + kind, + symbol, + region, + }; + } + } + } + + let name_str = name.value; + let name = Loc { + region: name.region, + value: symbol, + }; + + match kind { + AliasKind::Structural => PendingTypeDef::Alias { + name, + vars: can_rigids, + ann, + }, + AliasKind::Opaque => PendingTypeDef::Opaque { + name_str, + name, + vars: can_rigids, + ann, + derived: opt_derived, + }, + } + } + + Err((original_sym, original_region, loc_shadowed_symbol)) => { + let shadow_kind = match kind { + AliasKind::Structural => ShadowKind::Alias(original_sym), + AliasKind::Opaque => ShadowKind::Opaque(original_sym), + }; + + env.problem(Problem::Shadowing { + original_region, + shadow: loc_shadowed_symbol, + kind: shadow_kind, + }); + + PendingTypeDef::ShadowedAlias + } + } +} + +fn to_pending_type_def<'a>( + env: &mut Env<'a>, + def: &'a ast::TypeDef<'a>, + scope: &mut Scope, + pattern_type: PatternType, +) -> PendingTypeDef<'a> { + use ast::TypeDef::*; + + match def { + Alias { + header: TypeHeader { name, vars }, + ann, + } => to_pending_alias_or_opaque(env, scope, name, vars, ann, None, AliasKind::Structural), + Opaque { + header: TypeHeader { name, vars }, + typ: ann, + derived, + } => to_pending_alias_or_opaque( + env, + scope, + name, + vars, + ann, + derived.as_ref(), + AliasKind::Opaque, + ), + + Ability { + header, members, .. + } if pattern_type != PatternType::TopLevelDef => { + let header_region = header.region(); + let region = Region::span_across( + &header_region, + &members.last().map(|m| m.region()).unwrap_or(header_region), + ); + env.problem(Problem::AbilityNotOnToplevel { region }); + + PendingTypeDef::AbilityNotOnToplevel + } + + Ability { + header: TypeHeader { name, vars }, + members, + loc_implements: _, + } => { + let name = match scope + .introduce_without_shadow_symbol(&Ident::from(name.value), name.region) + { + Ok(symbol) => Loc::at(name.region, symbol), + Err((original_symbol, original_region, shadowed_symbol)) => { + env.problem(Problem::Shadowing { + original_region, + shadow: shadowed_symbol, + kind: ShadowKind::Ability(original_symbol), + }); + return PendingTypeDef::AbilityShadows; + } + }; + + if !vars.is_empty() { + // Disallow ability type arguments, at least for now. + let variables_region = Region::across_all(vars.iter().map(|v| &v.region)); + + env.problem(Problem::AbilityHasTypeVariables { + name: name.value, + variables_region, + }); + return PendingTypeDef::InvalidAbility { + symbol: name.value, + region: name.region, + }; + } + + let mut named_members = Vec::with_capacity(members.len()); + + for member in *members { + let name_region = member.name.region; + let member_name = member.name.extract_spaces().item; + + let member_sym = match scope.introduce(member_name.into(), name_region) { + Ok(sym) => sym, + Err((shadowed_symbol, shadow, _new_symbol)) => { + env.problem(roc_problem::can::Problem::Shadowing { + original_region: shadowed_symbol.region, + shadow, + kind: ShadowKind::Variable, + }); + // Pretend the member isn't a part of the ability + continue; + } + }; + + named_members.push(PendingAbilityMember { + name: Loc::at(name_region, member_sym), + typ: member.typ, + }); + + if pattern_type == PatternType::TopLevelDef { + env.top_level_symbols.insert(member_sym); + } + } + + PendingTypeDef::Ability { + name, + members: named_members, + } + } + } +} + +enum PendingValue<'a> { + Def(PendingValueDef<'a>), + Dbg(PendingExpectOrDbg<'a>), + Expect(PendingExpectOrDbg<'a>), + ExpectFx(PendingExpectOrDbg<'a>), + SignatureDefMismatch, +} + +struct PendingExpectOrDbg<'a> { + condition: &'a Loc>, + preceding_comment: Region, +} + +fn to_pending_value_def<'a>( + env: &mut Env<'a>, + var_store: &mut VarStore, + def: &'a ast::ValueDef<'a>, + scope: &mut Scope, + pending_abilities_in_scope: &PendingAbilitiesInScope, + output: &mut Output, + pattern_type: PatternType, +) -> PendingValue<'a> { + use ast::ValueDef::*; + + match def { + Annotation(loc_pattern, loc_ann) => { + // This takes care of checking for shadowing and adding idents to scope. + let loc_can_pattern = canonicalize_def_header_pattern( + env, + var_store, + scope, + pending_abilities_in_scope, + output, + pattern_type, + &loc_pattern.value, + loc_pattern.region, + ); + + PendingValue::Def(PendingValueDef::AnnotationOnly( + loc_pattern, + loc_can_pattern, + loc_ann, + )) + } + Body(loc_pattern, loc_expr) => { + // This takes care of checking for shadowing and adding idents to scope. + let loc_can_pattern = canonicalize_def_header_pattern( + env, + var_store, + scope, + pending_abilities_in_scope, + output, + pattern_type, + &loc_pattern.value, + loc_pattern.region, + ); + + PendingValue::Def(PendingValueDef::Body(loc_can_pattern, loc_expr)) + } + + AnnotatedBody { + ann_pattern, + ann_type, + comment: _, + body_pattern, + body_expr, + } => { + if ann_pattern.value.equivalent(&body_pattern.value) { + // NOTE: Pick the body pattern, picking the annotation one is + // incorrect in the presence of optional record fields! + // + // { x, y } : { x : Int, y ? Bool }* + // { x, y ? False } = rec + // + // This takes care of checking for shadowing and adding idents to scope. + let loc_can_pattern = canonicalize_def_header_pattern( + env, + var_store, + scope, + pending_abilities_in_scope, + output, + pattern_type, + &body_pattern.value, + body_pattern.region, + ); + + PendingValue::Def(PendingValueDef::TypedBody( + body_pattern, + loc_can_pattern, + ann_type, + body_expr, + )) + } else { + // the pattern of the annotation does not match the pattern of the body direc + env.problems.push(Problem::SignatureDefMismatch { + annotation_pattern: ann_pattern.region, + def_pattern: body_pattern.region, + }); + + // TODO: Should we instead build some PendingValueDef::InvalidAnnotatedBody ? This would + // remove the `Option` on this function (and be probably more reliable for further + // problem/error reporting) + PendingValue::SignatureDefMismatch + } + } + + Dbg { + condition, + preceding_comment, + } => PendingValue::Dbg(PendingExpectOrDbg { + condition, + preceding_comment: *preceding_comment, + }), + + Expect { + condition, + preceding_comment, + } => PendingValue::Expect(PendingExpectOrDbg { + condition, + preceding_comment: *preceding_comment, + }), + + ExpectFx { + condition, + preceding_comment, + } => PendingValue::ExpectFx(PendingExpectOrDbg { + condition, + preceding_comment: *preceding_comment, + }), + } +} + +/// Make aliases recursive +fn correct_mutual_recursive_type_alias( + env: &mut Env, + original_aliases: VecMap, + var_store: &mut VarStore, +) -> VecMap { + let capacity = original_aliases.len(); + let mut matrix = ReferenceMatrix::new(capacity); + + let (symbols_introduced, mut aliases) = original_aliases.unzip(); + + for (index, alias) in aliases.iter().enumerate() { + for referenced in alias.typ.symbols() { + match symbols_introduced.iter().position(|k| referenced == *k) { + None => { /* ignore */ } + Some(ref_id) => matrix.set_row_col(index, ref_id, true), + } + } + } + + let mut solved_aliases = bitvec::vec::BitVec::::repeat(false, capacity); + + let sccs = matrix.strongly_connected_components_all(); + + // scratchpad to store aliases that are modified in the current iteration. + // Only used when there is are more than one alias in a group. See below why + // this is needed. + let scratchpad_capacity = sccs + .groups() + .map(|(r, _)| r.count_ones()) + .max() + .unwrap_or_default(); + let mut scratchpad = Vec::with_capacity(scratchpad_capacity); + + for (cycle, _is_initial) in sccs.groups() { + debug_assert!(cycle.count_ones() > 0); + + // We need to instantiate the alias with any symbols in the currrent module it + // depends on. + // + // the `strongly_connected_components` returns SCCs in a topologically sorted order: + // SCC_0 has those aliases that don't rely on any other, SCC_1 has only those that rely on SCC_1, etc. + // + // Hence, we only need to worry about symbols in the current SCC or any prior one. + // It cannot be using any of the others, and we've already instantiated aliases coming from other modules. + let mut to_instantiate = solved_aliases | cycle; + + // Make sure we report only one error for the cycle, not an error for every + // alias in the cycle. + let mut can_still_report_error = true; + + for index in cycle.iter_ones() { + // Don't try to instantiate the alias itself in its own definition. + to_instantiate.set(index, false); + + // Within a recursive group, we must instantiate all aliases like how they came to the + // loop. e.g. given + // + // A : [ConsA B, NilA] + // B : [ConsB A, NilB] + // + // Our goal is + // + // A : [ConsA [ConsB A, NilB], NilA] + // B : [ConsB [ConsA B, NilA], NilB] + // + // But if we would first instantiate B into A, then use the updated A to instantiate B, + // we get + // + // A : [ConsA [ConsB A, NilB], NilA] + // B : [ConsB [ConsA [ConsB A, NilB], NilA], NilB] + // + // Which is incorrect. We do need the instantiated version however. + // e.g. if in a next group we have: + // + // C : A + // + // Then we must use the instantiated version + // + // C : [ConsA [ConsB A, NilB], NilA] + // + // So, we cannot replace the original version of A with its instantiated version + // while we process A's group. We have to store the instantiated version until the + // current group is done, then move it to the `aliases` array. That is what the scratchpad is for. + let alias = if cycle.count_ones() == 1 { + // an optimization: we can modify the alias in the `aliases` list directly + // because it is the only alias in the group. + &mut aliases[index] + } else { + scratchpad.push((index, aliases[index].clone())); + + &mut scratchpad.last_mut().unwrap().1 + }; + + // Now, `alias` is possibly a mutable borrow from the `aliases` vector. But we also want + // to immutably borrow other elements from that vector to instantiate them into `alias`. + // The borrow checker disallows that. + // + // So we get creative: we swap out the element we want to modify with a dummy. We can + // then freely modify the type we moved out, and the `to_instantiate` mask + // makes sure that our dummy is not used. + + let alias_region = alias.region; + let mut alias_type = Type::EmptyRec; + + std::mem::swap(&mut alias_type, &mut alias.typ); + + let can_instantiate_symbol = |s| match symbols_introduced.iter().position(|i| *i == s) { + Some(s_index) if to_instantiate[s_index] => aliases.get(s_index), + _ => None, + }; + + let mut new_lambda_sets = ImSet::default(); + let mut new_recursion_variables = ImSet::default(); + let mut new_infer_ext_vars = ImSet::default(); + alias_type.instantiate_aliases( + alias_region, + &can_instantiate_symbol, + var_store, + &mut new_lambda_sets, + &mut new_recursion_variables, + &mut new_infer_ext_vars, + ); + + let alias = if cycle.count_ones() > 1 { + &mut scratchpad.last_mut().unwrap().1 + } else { + &mut aliases[index] + }; + + // swap the type back + std::mem::swap(&mut alias_type, &mut alias.typ); + + // We can instantiate this alias in future iterations + to_instantiate.set(index, true); + + // add any lambda sets that the instantiation created to the current alias + alias.lambda_set_variables.extend( + new_lambda_sets + .iter() + .map(|var| LambdaSet(Type::Variable(*var))), + ); + + // add any new recursion variables + alias.recursion_variables.extend(new_recursion_variables); + + // add any new infer-in-output extension variables that the instantiation created to the current alias + alias + .infer_ext_in_output_variables + .extend(new_infer_ext_vars); + + // Now mark the alias recursive, if it needs to be. + let rec = symbols_introduced[index]; + let is_self_recursive = cycle.count_ones() == 1 && matrix.get_row_col(index, index); + let is_mutually_recursive = cycle.count_ones() > 1; + + if is_self_recursive || is_mutually_recursive { + let _made_recursive = make_tag_union_of_alias_recursive( + env, + rec, + alias, + vec![], + var_store, + &mut can_still_report_error, + ); + } + } + + // the current group has instantiated. Now we can move the updated aliases to the `aliases` vector + for (index, alias) in scratchpad.drain(..) { + aliases[index] = alias; + } + + // The cycle we just instantiated and marked recursive may still be an illegal cycle, if + // all the types in the cycle are narrow newtypes. We can't figure this out until now, + // because we need all the types to be deeply instantiated. + let all_are_narrow = cycle.iter_ones().all(|index| { + let typ = &aliases[index].typ; + matches!(typ, Type::RecursiveTagUnion(..)) && typ.is_narrow() + }); + + if all_are_narrow { + // This cycle is illegal! + let mut indices = cycle.iter_ones(); + let first_index = indices.next().unwrap(); + + let rest: Vec = indices.map(|i| symbols_introduced[i]).collect(); + + let alias_name = symbols_introduced[first_index]; + let alias = aliases.get_mut(first_index).unwrap(); + + mark_cyclic_alias( + env, + &mut alias.typ, + alias_name, + alias.kind, + alias.region, + rest, + can_still_report_error, + ) + } + + // We've instantiated all we could, so all instantiatable aliases are solved now + solved_aliases = to_instantiate; + } + + // Safety: both vectors are equal length and there are no duplicates + unsafe { VecMap::zip(symbols_introduced, aliases) } +} + +fn make_tag_union_of_alias_recursive( + env: &mut Env, + alias_name: Symbol, + alias: &mut Alias, + others: Vec, + var_store: &mut VarStore, + can_report_cyclic_error: &mut bool, +) -> Result<(), ()> { + let alias_args = alias + .type_variables + .iter() + .map(|l| Type::Variable(l.value.var)); + + let alias_opt_able_vars = alias.type_variables.iter().map(|l| OptAbleType { + typ: Type::Variable(l.value.var), + opt_abilities: l.value.opt_bound_abilities.clone(), + }); + + let lambda_set_vars = alias.lambda_set_variables.iter(); + let infer_ext_in_output_variables = alias + .infer_ext_in_output_variables + .iter() + .map(|v| Type::Variable(*v)); + + let made_recursive = make_tag_union_recursive_help( + env, + Loc::at(alias.header_region(), alias_name), + alias_args, + alias_opt_able_vars, + lambda_set_vars, + infer_ext_in_output_variables, + alias.kind, + alias.region, + others, + &mut alias.typ, + var_store, + can_report_cyclic_error, + ); + + match made_recursive { + MakeTagUnionRecursive::Cyclic => Ok(()), + MakeTagUnionRecursive::MadeRecursive { recursion_variable } => { + alias.recursion_variables.clear(); + alias.recursion_variables.insert(recursion_variable); + + Ok(()) + } + MakeTagUnionRecursive::InvalidRecursion => Err(()), + } +} + +enum MakeTagUnionRecursive { + Cyclic, + MadeRecursive { recursion_variable: Variable }, + InvalidRecursion, +} + +/// Attempt to make a tag union recursive at the position of `recursive_alias`; for example, +/// +/// ```roc +/// [Cons a (ConsList a), Nil] as ConsList a +/// ``` +/// +/// can be made recursive at the position "ConsList a" with a fresh recursive variable, say r1: +/// +/// ```roc +/// [Cons a r1, Nil] as r1 +/// ``` +/// +/// Returns `Err` if the tag union is recursive, but there is no structure-preserving recursion +/// variable for it. This can happen when the type is a nested datatype, for example in either of +/// +/// ```roc +/// Nested a : [Chain a (Nested (List a)), Term] +/// DuoList a b : [Cons a (DuoList b a), Nil] +/// ``` +/// +/// When `Err` is returned, a problem will be added to `env`. +#[allow(clippy::too_many_arguments)] +fn make_tag_union_recursive_help<'a, 'b>( + env: &mut Env<'a>, + recursive_alias: Loc, + alias_args: impl Iterator, + alias_opt_able_vars: impl Iterator, + lambda_set_variables: impl Iterator, + infer_ext_in_output_variables: impl Iterator, + alias_kind: AliasKind, + region: Region, + others: Vec, + typ: &'b mut Type, + var_store: &mut VarStore, + can_report_cyclic_error: &mut bool, +) -> MakeTagUnionRecursive { + use MakeTagUnionRecursive::*; + + let symbol = recursive_alias.value; + let alias_region = recursive_alias.region; + + match typ { + Type::TagUnion(tags, ext) => { + let recursion_variable = var_store.fresh(); + let type_arguments: Vec<_> = alias_args.collect(); + + let mut pending_typ = + Type::RecursiveTagUnion(recursion_variable, tags.to_vec(), ext.clone()); + + let substitution = match alias_kind { + // Inline recursion var directly wherever the alias is used. + AliasKind::Structural => Type::Variable(recursion_variable), + // Wrap the recursion var in the opaque wherever it's used to avoid leaking the + // inner type out as structural. + AliasKind::Opaque => Type::Alias { + symbol, + type_arguments: alias_opt_able_vars.collect(), + lambda_set_variables: lambda_set_variables.cloned().collect(), + infer_ext_in_output_types: infer_ext_in_output_variables.collect(), + actual: Box::new(Type::Variable(recursion_variable)), + kind: AliasKind::Opaque, + }, + }; + + let substitution_result = + pending_typ.substitute_alias(symbol, &type_arguments, &substitution); + match substitution_result { + Ok(()) => { + // We can substitute the alias presence for the variable exactly. + *typ = pending_typ; + + MadeRecursive { recursion_variable } + } + Err(differing_recursion_region) => { + env.problems.push(Problem::NestedDatatype { + alias: symbol, + def_region: alias_region, + differing_recursion_region, + }); + + InvalidRecursion + } + } + } + Type::RecursiveTagUnion(recursion_variable, _, _) => MadeRecursive { + recursion_variable: *recursion_variable, + }, + Type::Alias { + actual, + type_arguments, + lambda_set_variables, + infer_ext_in_output_types, + kind, + .. + } => { + // NB: We need to collect the type arguments to shut off rustc's closure type + // instantiator. Otherwise we get unfortunate errors like + // reached the recursion limit while instantiating `make_tag_union_recursive_help::<...n/src/def.rs:1879:65: 1879:77]>>` + #[allow(clippy::needless_collect)] + let alias_args: Vec = type_arguments.iter().map(|ta| ta.typ.clone()).collect(); + let recursive_alias = Loc::at_zero(symbol); + + // try to make `actual` recursive + make_tag_union_recursive_help( + env, + recursive_alias, + alias_args.into_iter(), + type_arguments.iter().cloned(), + lambda_set_variables.iter(), + infer_ext_in_output_types.iter().cloned(), + *kind, + region, + others, + actual, + var_store, + can_report_cyclic_error, + ) + } + _ => { + // take care to report a cyclic alias only once (not once for each alias in the cycle) + mark_cyclic_alias( + env, + typ, + symbol, + alias_kind, + region, + others, + *can_report_cyclic_error, + ); + *can_report_cyclic_error = false; + + Cyclic + } + } +} + +fn mark_cyclic_alias( + env: &mut Env, + typ: &mut Type, + symbol: Symbol, + alias_kind: AliasKind, + region: Region, + others: Vec, + report: bool, +) { + *typ = Type::Error; + + if report { + let problem = Problem::CyclicAlias(symbol, region, others, alias_kind); + env.problems.push(problem); + } +} diff --git a/crates/compiler/can/src/derive.rs b/crates/compiler/can/src/derive.rs new file mode 100644 index 0000000000..fd51376919 --- /dev/null +++ b/crates/compiler/can/src/derive.rs @@ -0,0 +1,295 @@ +//! Derives parse trees for ability member impls of Opaques. +//! These are derived at canonicalization time rather than type-checking time, +//! as structural types are, due to the following reasons: +//! - Derived impls for opaques are not generalizable, and hence cannot be owned by the Derived +//! module, because they may require immediate specialization unknown to the Derived module. +//! - Derived impls for opaques are typically very small, effectively deferring the +//! implementation to the value they wrap. + +use roc_error_macros::internal_error; +use roc_module::{called_via::CalledVia, symbol::Symbol}; +use roc_parse::ast; +use roc_region::all::{Loc, Region}; + +use crate::{env::Env, pattern::Pattern, scope::Scope}; + +fn to_encoder<'a>(env: &mut Env<'a>, at_opaque: &'a str) -> ast::Expr<'a> { + let alloc_pat = |it| env.arena.alloc(Loc::at(DERIVED_REGION, it)); + let alloc_expr = |it| env.arena.alloc(Loc::at(DERIVED_REGION, it)); + + let payload = "#payload"; + + // \@Opaq payload + let opaque_ref = alloc_pat(ast::Pattern::OpaqueRef(at_opaque)); + let opaque_apply_pattern = ast::Pattern::Apply( + opaque_ref, + &*env + .arena + .alloc([Loc::at(DERIVED_REGION, ast::Pattern::Identifier(payload))]), + ); + + // Encode.toEncoder payload + let call_member = alloc_expr(ast::Expr::Apply( + alloc_expr(ast::Expr::Var { + module_name: "Encode", + ident: "toEncoder", + }), + &*env.arena.alloc([&*alloc_expr(ast::Expr::Var { + module_name: "", + ident: payload, + })]), + roc_module::called_via::CalledVia::Space, + )); + + // \@Opaq payload -> Encode.toEncoder payload + ast::Expr::Closure( + env.arena + .alloc([Loc::at(DERIVED_REGION, opaque_apply_pattern)]), + call_member, + ) +} + +fn decoder<'a>(env: &mut Env<'a>, at_opaque: &'a str) -> ast::Expr<'a> { + let alloc_expr = |it| env.arena.alloc(Loc::at(DERIVED_REGION, it)); + + let call_custom = { + let bytes = "#bytes"; + let fmt = "#fmt"; + + // Decode.decodeWith bytes Decode.decoder fmt + let call_decode_with = ast::Expr::Apply( + alloc_expr(ast::Expr::Var { + module_name: "Decode", + ident: "decodeWith", + }), + env.arena.alloc([ + &*alloc_expr(ast::Expr::Var { + module_name: "", + ident: bytes, + }), + alloc_expr(ast::Expr::Var { + module_name: "Decode", + ident: "decoder", + }), + alloc_expr(ast::Expr::Var { + module_name: "", + ident: fmt, + }), + ]), + CalledVia::Space, + ); + + // Decode.mapResult (Decode.decodeWith bytes Decode.decoder fmt) @Opaq + let call_map_result = ast::Expr::Apply( + alloc_expr(ast::Expr::Var { + module_name: "Decode", + ident: "mapResult", + }), + env.arena.alloc([ + &*alloc_expr(call_decode_with), + alloc_expr(ast::Expr::OpaqueRef(at_opaque)), + ]), + CalledVia::Space, + ); + + // \bytes, fmt -> + // Decode.mapResult (Decode.decodeWith bytes Decode.decoder fmt) @Opaq + let custom_closure = ast::Expr::Closure( + env.arena.alloc([ + Loc::at(DERIVED_REGION, ast::Pattern::Identifier(bytes)), + Loc::at(DERIVED_REGION, ast::Pattern::Identifier(fmt)), + ]), + alloc_expr(call_map_result), + ); + + // Decode.custom \bytes, fmt -> ... + ast::Expr::Apply( + alloc_expr(ast::Expr::Var { + module_name: "Decode", + ident: "custom", + }), + env.arena.alloc([&*alloc_expr(custom_closure)]), + CalledVia::Space, + ) + }; + + call_custom +} + +fn hash<'a>(env: &mut Env<'a>, at_opaque: &'a str) -> ast::Expr<'a> { + let alloc_pat = |it| env.arena.alloc(Loc::at(DERIVED_REGION, it)); + let alloc_expr = |it| env.arena.alloc(Loc::at(DERIVED_REGION, it)); + let hasher = "#hasher"; + + let payload = "#payload"; + + // \@Opaq payload + let opaque_ref = alloc_pat(ast::Pattern::OpaqueRef(at_opaque)); + let opaque_apply_pattern = ast::Pattern::Apply( + opaque_ref, + &*env + .arena + .alloc([Loc::at(DERIVED_REGION, ast::Pattern::Identifier(payload))]), + ); + + // Hash.hash hasher payload + let call_member = alloc_expr(ast::Expr::Apply( + alloc_expr(ast::Expr::Var { + module_name: "Hash", + ident: "hash", + }), + &*env.arena.alloc([ + &*alloc_expr(ast::Expr::Var { + module_name: "", + ident: hasher, + }), + &*alloc_expr(ast::Expr::Var { + module_name: "", + ident: payload, + }), + ]), + roc_module::called_via::CalledVia::Space, + )); + + // \hasher, @Opaq payload -> Hash.hash hasher payload + ast::Expr::Closure( + env.arena.alloc([ + Loc::at(DERIVED_REGION, ast::Pattern::Identifier(hasher)), + Loc::at(DERIVED_REGION, opaque_apply_pattern), + ]), + call_member, + ) +} + +fn is_eq<'a>(env: &mut Env<'a>, at_opaque: &'a str) -> ast::Expr<'a> { + let alloc_pat = |it| env.arena.alloc(Loc::at(DERIVED_REGION, it)); + let alloc_expr = |it| env.arena.alloc(Loc::at(DERIVED_REGION, it)); + + let payload1 = "#payload1"; + let payload2 = "#payload2"; + + let opaque_ref = alloc_pat(ast::Pattern::OpaqueRef(at_opaque)); + // \@Opaq payload1 + let opaque1 = ast::Pattern::Apply( + opaque_ref, + &*env + .arena + .alloc([Loc::at(DERIVED_REGION, ast::Pattern::Identifier(payload1))]), + ); + // \@Opaq payload2 + let opaque2 = ast::Pattern::Apply( + opaque_ref, + &*env + .arena + .alloc([Loc::at(DERIVED_REGION, ast::Pattern::Identifier(payload2))]), + ); + + // Bool.isEq payload1 payload2 + let call_member = alloc_expr(ast::Expr::Apply( + alloc_expr(ast::Expr::Var { + module_name: "Bool", + ident: "isEq", + }), + &*env.arena.alloc([ + &*alloc_expr(ast::Expr::Var { + module_name: "", + ident: payload1, + }), + &*alloc_expr(ast::Expr::Var { + module_name: "", + ident: payload2, + }), + ]), + roc_module::called_via::CalledVia::Space, + )); + + // \@Opaq payload1, @Opaq payload2 -> Bool.isEq payload1 payload2 + ast::Expr::Closure( + env.arena.alloc([ + Loc::at(DERIVED_REGION, opaque1), + Loc::at(DERIVED_REGION, opaque2), + ]), + call_member, + ) +} + +fn to_inspector<'a>(env: &mut Env<'a>, at_opaque: &'a str) -> ast::Expr<'a> { + let alloc_pat = |it| env.arena.alloc(Loc::at(DERIVED_REGION, it)); + let alloc_expr = |it| env.arena.alloc(Loc::at(DERIVED_REGION, it)); + + let payload = "#payload"; + + // \@Opaq payload + let opaque_ref = alloc_pat(ast::Pattern::OpaqueRef(at_opaque)); + let opaque_apply_pattern = ast::Pattern::Apply( + opaque_ref, + &*env + .arena + .alloc([Loc::at(DERIVED_REGION, ast::Pattern::Identifier(payload))]), + ); + + // Inspect.toInspector payload + let call_member = alloc_expr(ast::Expr::Apply( + alloc_expr(ast::Expr::Var { + module_name: "Inspect", + ident: "toInspector", + }), + &*env.arena.alloc([&*alloc_expr(ast::Expr::Var { + module_name: "", + ident: payload, + })]), + roc_module::called_via::CalledVia::Space, + )); + + // TODO: change the derived implementation to be something that includes the opaque symbol in + // the derivation, e.g. something like + // + // \@Opaq payload -> + // Inspect.opaqueWrapper "toString symbol" payload + + // \@Opaq payload -> Inspect.toInspector payload + ast::Expr::Closure( + env.arena + .alloc([Loc::at(DERIVED_REGION, opaque_apply_pattern)]), + call_member, + ) +} + +pub const DERIVED_REGION: Region = Region::zero(); + +pub(crate) fn synthesize_member_impl<'a>( + env: &mut Env<'a>, + scope: &mut Scope, + opaque_name: &'a str, + ability_member: Symbol, +) -> (Symbol, Loc, &'a Loc>) { + // @Opaq + let at_opaque = env.arena.alloc_str(&format!("@{opaque_name}")); + + let (impl_name, def_body): (String, ast::Expr<'a>) = match ability_member { + Symbol::ENCODE_TO_ENCODER => ( + format!("#{opaque_name}_toEncoder"), + to_encoder(env, at_opaque), + ), + Symbol::DECODE_DECODER => (format!("#{opaque_name}_decoder"), decoder(env, at_opaque)), + Symbol::HASH_HASH => (format!("#{opaque_name}_hash"), hash(env, at_opaque)), + Symbol::BOOL_IS_EQ => (format!("#{opaque_name}_isEq"), is_eq(env, at_opaque)), + Symbol::INSPECT_TO_INSPECTOR => ( + format!("#{opaque_name}_toInspector"), + to_inspector(env, at_opaque), + ), + other => internal_error!("{:?} is not a derivable ability member!", other), + }; + + let impl_symbol = scope + .introduce_str(&impl_name, DERIVED_REGION) + .expect("this name is not unique"); + + let def_pattern = Pattern::Identifier(impl_symbol); + + ( + impl_symbol, + Loc::at(DERIVED_REGION, def_pattern), + env.arena.alloc(Loc::at(DERIVED_REGION, def_body)), + ) +} diff --git a/compiler/can/src/effect_module.rs b/crates/compiler/can/src/effect_module.rs similarity index 84% rename from compiler/can/src/effect_module.rs rename to crates/compiler/can/src/effect_module.rs index eee68f6e35..91fdb5dd72 100644 --- a/compiler/can/src/effect_module.rs +++ b/crates/compiler/can/src/effect_module.rs @@ -1,14 +1,14 @@ use crate::annotation::IntroducedVariables; -use crate::def::{Declaration, Def}; -use crate::expr::{AnnotatedMark, ClosureData, Expr, Recursive}; +use crate::def::Def; +use crate::expr::{AnnotatedMark, ClosureData, Declarations, Expr, Recursive, WhenBranchPattern}; use crate::pattern::Pattern; use crate::scope::Scope; -use roc_collections::{SendMap, VecSet}; +use roc_collections::{SendMap, VecMap, VecSet}; use roc_module::called_via::CalledVia; use roc_module::ident::TagName; use roc_module::symbol::Symbol; use roc_region::all::{Loc, Region}; -use roc_types::subs::{ExhaustiveMark, IllegalCycleMark, RedundantMark, VarStore, Variable}; +use roc_types::subs::{ExhaustiveMark, RedundantMark, VarStore, Variable}; use roc_types::types::{AliasKind, LambdaSet, OptAbleType, OptAbleVar, Type, TypeExtension}; #[derive(Debug, Default, Clone, Copy)] @@ -38,7 +38,7 @@ pub(crate) fn build_effect_builtins( effect_symbol: Symbol, var_store: &mut VarStore, exposed_symbols: &mut VecSet, - declarations: &mut Vec, + declarations: &mut Declarations, generated_functions: HostedGeneratedFunctions, ) { macro_rules! helper { @@ -54,37 +54,31 @@ pub(crate) fn build_effect_builtins( if generated_functions.after { let def = helper!(build_effect_after); - declarations.push(Declaration::Declare(def)); + declarations.push_def(def); } // Effect.map : Effect a, (a -> b) -> Effect b if generated_functions.map { let def = helper!(build_effect_map); - declarations.push(Declaration::Declare(def)); + declarations.push_def(def); } // Effect.always : a -> Effect a if generated_functions.always { let def = helper!(build_effect_always); - declarations.push(Declaration::Declare(def)); + declarations.push_def(def); } // Effect.forever : Effect a -> Effect b if generated_functions.forever { let def = helper!(build_effect_forever); - declarations.push(Declaration::DeclareRec( - vec![def], - IllegalCycleMark::empty(), - )); + declarations.push_def(def); } // Effect.loop : a, (a -> Effect [Step a, Done b]) -> Effect b if generated_functions.loop_ { let def = helper!(build_effect_loop); - declarations.push(Declaration::DeclareRec( - vec![def], - IllegalCycleMark::empty(), - )); + declarations.push_def(def); } // Useful when working on functions in this module. By default symbols that we named do now @@ -130,14 +124,15 @@ fn build_effect_always( Loc::at_zero(empty_record_pattern(var_store)), )]; - let body = Expr::Var(value_symbol); + let value_var = var_store.fresh(); + let body = Expr::Var(value_symbol, value_var); Expr::Closure(ClosureData { function_type: var_store.fresh(), closure_type: var_store.fresh(), return_type: var_store.fresh(), name: inner_closure_symbol, - captured_symbols: vec![(value_symbol, var_store.fresh())], + captured_symbols: vec![(value_symbol, value_var)], recursive: Recursive::NotRecursive, arguments, loc_body: Box::new(Loc::at_zero(body)), @@ -207,7 +202,7 @@ fn build_effect_always( let def_annotation = crate::def::Annotation { signature, introduced_variables, - aliases: SendMap::default(), + aliases: VecMap::default(), region: Region::zero(), }; @@ -237,20 +232,22 @@ fn build_effect_map( .introduce("effect_map_thunk".into(), Region::zero()) .unwrap() }; + let thunk_var = var_store.fresh(); let mapper_symbol = { scope .introduce("effect_map_mapper".into(), Region::zero()) .unwrap() }; + let mapper_var = var_store.fresh(); let map_symbol = { scope.introduce("map".into(), Region::zero()).unwrap() }; // `thunk {}` let force_thunk_call = { let boxed = ( - var_store.fresh(), - Loc::at_zero(Expr::Var(thunk_symbol)), + thunk_var, + Loc::at_zero(Expr::Var(thunk_symbol, thunk_var)), var_store.fresh(), var_store.fresh(), ); @@ -262,8 +259,8 @@ fn build_effect_map( // `toEffect (thunk {})` let mapper_call = { let boxed = ( - var_store.fresh(), - Loc::at_zero(Expr::Var(mapper_symbol)), + mapper_var, + Loc::at_zero(Expr::Var(mapper_symbol, mapper_var)), var_store.fresh(), var_store.fresh(), ); @@ -399,7 +396,7 @@ fn build_effect_map( let def_annotation = crate::def::Annotation { signature, introduced_variables, - aliases: SendMap::default(), + aliases: VecMap::default(), region: Region::zero(), }; @@ -417,53 +414,115 @@ fn build_effect_map( (map_symbol, def) } +fn force_thunk(expr: Expr, thunk_var: Variable, var_store: &mut VarStore) -> Expr { + let boxed = ( + thunk_var, + Loc::at_zero(expr), + var_store.fresh(), + var_store.fresh(), + ); + + let arguments = vec![(var_store.fresh(), Loc::at_zero(Expr::EmptyRecord))]; + Expr::Call(Box::new(boxed), arguments, CalledVia::Space) +} + fn build_effect_after( scope: &mut Scope, - effect_symbol: Symbol, + effect_opaque_symbol: Symbol, var_store: &mut VarStore, ) -> (Symbol, Def) { - // Effect.after = \@Effect effect, toEffect -> toEffect (effect {}) + // Effect.after = \@Effect effect, toEffect -> + // @Effect \{} -> + // when toEffect (effect {}) is + // @Effect thunk -> thunk {} - let thunk_symbol = { - scope - .introduce("effect_after_thunk".into(), Region::zero()) - .unwrap() - }; + let thunk_symbol = new_symbol!(scope, "effect_after_thunk"); - let to_effect_symbol = { - scope - .introduce("effect_after_toEffect".into(), Region::zero()) - .unwrap() - }; + let effect_symbol = new_symbol!(scope, "effect_after_effect"); + let to_effect_symbol = new_symbol!(scope, "effect_after_toEffect"); + let after_symbol = new_symbol!(scope, "after"); + let outer_closure_symbol = new_symbol!(scope, "effect_after_inner"); - let after_symbol = { scope.introduce("after".into(), Region::zero()).unwrap() }; + // `effect {}` + let force_effect_var = var_store.fresh(); + let force_effect_call = force_thunk( + Expr::Var(effect_symbol, force_effect_var), + force_effect_var, + var_store, + ); - // `thunk {}` - let force_thunk_call = { - let boxed = ( - var_store.fresh(), - Loc::at_zero(Expr::Var(thunk_symbol)), - var_store.fresh(), - var_store.fresh(), - ); - - let arguments = vec![(var_store.fresh(), Loc::at_zero(Expr::EmptyRecord))]; - Expr::Call(Box::new(boxed), arguments, CalledVia::Space) - }; - - // `toEffect (thunk {})` + // `toEffect (effect {})` + let to_effect_var = var_store.fresh(); let to_effect_call = { let boxed = ( - var_store.fresh(), - Loc::at_zero(Expr::Var(to_effect_symbol)), + to_effect_var, + Loc::at_zero(Expr::Var(to_effect_symbol, to_effect_var)), var_store.fresh(), var_store.fresh(), ); - let arguments = vec![(var_store.fresh(), Loc::at_zero(force_thunk_call))]; + let arguments = vec![(var_store.fresh(), Loc::at_zero(force_effect_call))]; Expr::Call(Box::new(boxed), arguments, CalledVia::Space) }; + // let @Effect thunk = toEffect (effect {}) in thunk {} + let let_effect_thunk = { + // `thunk {}` + let force_inner_thunk_var = var_store.fresh(); + let force_inner_thunk_call = force_thunk( + Expr::Var(thunk_symbol, force_inner_thunk_var), + force_inner_thunk_var, + var_store, + ); + + let (specialized_def_type, type_arguments, lambda_set_variables) = + build_fresh_opaque_variables(var_store); + + let pattern = Pattern::UnwrappedOpaque { + whole_var: var_store.fresh(), + opaque: effect_opaque_symbol, + argument: Box::new(( + var_store.fresh(), + Loc::at_zero(Pattern::Identifier(thunk_symbol)), + )), + specialized_def_type, + type_arguments, + lambda_set_variables, + }; + let pattern = WhenBranchPattern { + pattern: Loc::at_zero(pattern), + degenerate: false, + }; + + let branches = vec![crate::expr::WhenBranch { + guard: None, + value: Loc::at_zero(force_inner_thunk_call), + patterns: vec![pattern], + redundant: RedundantMark::new(var_store), + }]; + + Expr::When { + cond_var: var_store.fresh(), + branches_cond_var: var_store.fresh(), + expr_var: var_store.fresh(), + region: Region::zero(), + loc_cond: Box::new(Loc::at_zero(to_effect_call)), + branches, + exhaustive: ExhaustiveMark::new(var_store), + } + }; + + // @Effect \{} -> when toEffect (effect {}) is @Effect thunk -> thunk {} + let outer_effect = wrap_in_effect_thunk( + let_effect_thunk, + effect_opaque_symbol, + outer_closure_symbol, + vec![effect_symbol, to_effect_symbol], + var_store, + ); + + scope.register_debug_idents(); + let (specialized_def_type, type_arguments, lambda_set_variables) = build_fresh_opaque_variables(var_store); @@ -472,11 +531,11 @@ fn build_effect_after( var_store.fresh(), AnnotatedMark::new(var_store), Loc::at_zero(Pattern::UnwrappedOpaque { - opaque: effect_symbol, + opaque: effect_opaque_symbol, whole_var: var_store.fresh(), argument: Box::new(( var_store.fresh(), - Loc::at_zero(Pattern::Identifier(thunk_symbol)), + Loc::at_zero(Pattern::Identifier(effect_symbol)), )), specialized_def_type, type_arguments, @@ -499,7 +558,7 @@ fn build_effect_after( captured_symbols: Vec::new(), recursive: Recursive::NotRecursive, arguments, - loc_body: Box::new(Loc::at_zero(to_effect_call)), + loc_body: Box::new(Loc::at_zero(outer_effect)), }); let mut introduced_variables = IntroducedVariables::default(); @@ -512,15 +571,24 @@ fn build_effect_after( introduced_variables.insert_named("b".into(), Loc::at_zero(var_b)); let effect_a = build_effect_opaque( - effect_symbol, + effect_opaque_symbol, var_a, Type::Variable(var_a), var_store, &mut introduced_variables, ); - let effect_b = build_effect_opaque( - effect_symbol, + let effect_b1 = build_effect_opaque( + effect_opaque_symbol, + var_b, + Type::Variable(var_b), + var_store, + &mut introduced_variables, + ); + + // we need a second b2 to give it a unique lambda set variable + let effect_b2 = build_effect_opaque( + effect_opaque_symbol, var_b, Type::Variable(var_b), var_store, @@ -528,26 +596,26 @@ fn build_effect_after( ); let closure_var = var_store.fresh(); - introduced_variables.insert_wildcard(Loc::at_zero(closure_var)); + introduced_variables.insert_lambda_set(closure_var); let a_to_effect_b = Type::Function( vec![Type::Variable(var_a)], Box::new(Type::Variable(closure_var)), - Box::new(effect_b.clone()), + Box::new(effect_b1), ); let closure_var = var_store.fresh(); - introduced_variables.insert_wildcard(Loc::at_zero(closure_var)); + introduced_variables.insert_lambda_set(closure_var); Type::Function( vec![effect_a, a_to_effect_b], Box::new(Type::Variable(closure_var)), - Box::new(effect_b), + Box::new(effect_b2), ) }; let def_annotation = crate::def::Annotation { signature, introduced_variables, - aliases: SendMap::default(), + aliases: VecMap::default(), region: Region::zero(), }; @@ -648,9 +716,10 @@ fn force_effect( let ret_var = var_store.fresh(); let force_thunk_call = { + let thunk_var = var_store.fresh(); let boxed = ( - var_store.fresh(), - Loc::at_zero(Expr::Var(thunk_symbol)), + thunk_var, + Loc::at_zero(Expr::Var(thunk_symbol, thunk_var)), var_store.fresh(), ret_var, ); @@ -779,7 +848,7 @@ fn build_effect_forever( let def_annotation = crate::def::Annotation { signature, introduced_variables, - aliases: SendMap::default(), + aliases: VecMap::default(), region: Region::zero(), }; @@ -830,6 +899,7 @@ fn build_effect_forever_inner_body( effect: Symbol, var_store: &mut VarStore, ) -> Expr { + let thunk1_var = var_store.fresh(); let thunk1_symbol = { scope.introduce("thunk1".into(), Region::zero()).unwrap() }; let thunk2_symbol = { scope.introduce("thunk2".into(), Region::zero()).unwrap() }; @@ -855,7 +925,7 @@ fn build_effect_forever_inner_body( Def { loc_pattern: Loc::at_zero(pattern), - loc_expr: Loc::at_zero(Expr::Var(effect)), + loc_expr: Loc::at_zero(Expr::Var(effect, var_store.fresh())), expr_var: var_store.fresh(), pattern_vars, annotation: None, @@ -866,8 +936,8 @@ fn build_effect_forever_inner_body( let force_thunk_call = { let ret_var = var_store.fresh(); let boxed = ( - var_store.fresh(), - Loc::at_zero(Expr::Var(thunk1_symbol)), + thunk1_var, + Loc::at_zero(Expr::Var(thunk1_symbol, thunk1_var)), var_store.fresh(), ret_var, ); @@ -891,12 +961,13 @@ fn build_effect_forever_inner_body( let forever_effect = { let boxed = ( var_store.fresh(), - Loc::at_zero(Expr::Var(forever_symbol)), + Loc::at_zero(Expr::Var(forever_symbol, var_store.fresh())), var_store.fresh(), var_store.fresh(), ); - let arguments = vec![(var_store.fresh(), Loc::at_zero(Expr::Var(effect)))]; + let effect_var = var_store.fresh(); + let arguments = vec![(effect_var, Loc::at_zero(Expr::Var(effect, effect_var)))]; Expr::Call(Box::new(boxed), arguments, CalledVia::Space) }; @@ -1009,6 +1080,7 @@ fn build_effect_loop( lambda_set_variables: vec![roc_types::types::LambdaSet(Type::Variable( closure_var, ))], + infer_ext_in_output_types: vec![], actual: Box::new(actual), kind: AliasKind::Opaque, } @@ -1036,7 +1108,7 @@ fn build_effect_loop( let def_annotation = crate::def::Annotation { signature, introduced_variables, - aliases: SendMap::default(), + aliases: VecMap::default(), region: Region::zero(), }; @@ -1144,14 +1216,16 @@ fn build_effect_loop_inner_body( // `step state` let rhs = { + let step_var = var_store.fresh(); let boxed = ( - var_store.fresh(), - Loc::at_zero(Expr::Var(step_symbol)), + step_var, + Loc::at_zero(Expr::Var(step_symbol, step_var)), var_store.fresh(), var_store.fresh(), ); - let arguments = vec![(var_store.fresh(), Loc::at_zero(Expr::Var(state_symbol)))]; + let state_var = var_store.fresh(); + let arguments = vec![(state_var, Loc::at_zero(Expr::Var(state_symbol, state_var)))]; Expr::Call(Box::new(boxed), arguments, CalledVia::Space) }; @@ -1166,10 +1240,11 @@ fn build_effect_loop_inner_body( // thunk1 {} let force_thunk_call = { + let thunk1_var = var_store.fresh(); let ret_var = var_store.fresh(); let boxed = ( - var_store.fresh(), - Loc::at_zero(Expr::Var(thunk1_symbol)), + thunk1_var, + Loc::at_zero(Expr::Var(thunk1_symbol, thunk1_var)), var_store.fresh(), ret_var, ); @@ -1182,16 +1257,22 @@ fn build_effect_loop_inner_body( // recursive call `loop newState step` let loop_new_state_step = { + let loop_var = var_store.fresh(); let boxed = ( - var_store.fresh(), - Loc::at_zero(Expr::Var(loop_symbol)), + loop_var, + Loc::at_zero(Expr::Var(loop_symbol, loop_var)), var_store.fresh(), var_store.fresh(), ); + let new_state_var = var_store.fresh(); + let step_var = var_store.fresh(); let arguments = vec![ - (var_store.fresh(), Loc::at_zero(Expr::Var(new_state_symbol))), - (var_store.fresh(), Loc::at_zero(Expr::Var(step_symbol))), + ( + new_state_var, + Loc::at_zero(Expr::Var(new_state_symbol, new_state_var)), + ), + (step_var, Loc::at_zero(Expr::Var(step_symbol, step_var))), ]; Expr::Call(Box::new(boxed), arguments, CalledVia::Space) }; @@ -1206,9 +1287,13 @@ fn build_effect_loop_inner_body( let step_tag_name = TagName("Step".into()); let step_pattern = applied_tag_pattern(step_tag_name, &[new_state_symbol], var_store); + let step_pattern = WhenBranchPattern { + pattern: Loc::at_zero(step_pattern), + degenerate: false, + }; crate::expr::WhenBranch { - patterns: vec![Loc::at_zero(step_pattern)], + patterns: vec![step_pattern], value: Loc::at_zero(force_thunk2), guard: None, redundant: RedundantMark::new(var_store), @@ -1218,10 +1303,14 @@ fn build_effect_loop_inner_body( let done_branch = { let done_tag_name = TagName("Done".into()); let done_pattern = applied_tag_pattern(done_tag_name, &[done_symbol], var_store); + let done_pattern = WhenBranchPattern { + pattern: Loc::at_zero(done_pattern), + degenerate: false, + }; crate::expr::WhenBranch { - patterns: vec![Loc::at_zero(done_pattern)], - value: Loc::at_zero(Expr::Var(done_symbol)), + patterns: vec![done_pattern], + value: Loc::at_zero(Expr::Var(done_symbol, var_store.fresh())), guard: None, redundant: RedundantMark::new(var_store), } @@ -1273,7 +1362,7 @@ pub fn build_host_exposed_def( match typ.shallow_structural_dealias() { Type::Function(args, _, _) => { for i in 0..args.len() { - let name = format!("closure_arg_{}_{}", ident, i); + let name = format!("closure_arg_{ident}_{i}"); let arg_symbol = { let ident = name.clone().into(); @@ -1289,10 +1378,10 @@ pub fn build_host_exposed_def( )); captured_symbols.push((arg_symbol, arg_var)); - linked_symbol_arguments.push((arg_var, Expr::Var(arg_symbol))); + linked_symbol_arguments.push((arg_var, Expr::Var(arg_symbol, arg_var))); } - let foreign_symbol_name = format!("roc_fx_{}", ident); + let foreign_symbol_name = format!("roc_fx_{ident}"); let low_level_call = Expr::ForeignCall { foreign_symbol: foreign_symbol_name.into(), args: linked_symbol_arguments, @@ -1300,7 +1389,7 @@ pub fn build_host_exposed_def( }; let effect_closure_symbol = { - let name = format!("effect_closure_{}", ident); + let name = format!("effect_closure_{ident}"); let ident = name.into(); scope.introduce(ident, Region::zero()).unwrap() @@ -1346,7 +1435,7 @@ pub fn build_host_exposed_def( _ => { // not a function - let foreign_symbol_name = format!("roc_fx_{}", ident); + let foreign_symbol_name = format!("roc_fx_{ident}"); let low_level_call = Expr::ForeignCall { foreign_symbol: foreign_symbol_name.into(), args: linked_symbol_arguments, @@ -1354,7 +1443,7 @@ pub fn build_host_exposed_def( }; let effect_closure_symbol = { - let name = format!("effect_closure_{}", ident); + let name = format!("effect_closure_{ident}"); let ident = name.into(); scope.introduce(ident, Region::zero()).unwrap() @@ -1430,7 +1519,8 @@ fn build_effect_opaque( introduced_variables: &mut IntroducedVariables, ) -> Type { let closure_var = var_store.fresh(); - introduced_variables.insert_wildcard(Loc::at_zero(closure_var)); + // introduced_variables.insert_wildcard(Loc::at_zero(closure_var)); + introduced_variables.insert_lambda_set(closure_var); let actual = Type::Function( vec![Type::EmptyRec], @@ -1442,6 +1532,7 @@ fn build_effect_opaque( symbol: effect_symbol, type_arguments: vec![OptAbleType::unbound(Type::Variable(a_var))], lambda_set_variables: vec![roc_types::types::LambdaSet(Type::Variable(closure_var))], + infer_ext_in_output_types: vec![], actual: Box::new(actual), kind: AliasKind::Opaque, } @@ -1463,7 +1554,7 @@ fn build_fresh_opaque_variables( ); let type_arguments = vec![OptAbleVar { var: a_var, - opt_ability: None, + opt_abilities: None, }]; let lambda_set_variables = vec![roc_types::types::LambdaSet(Type::Variable(closure_var))]; diff --git a/crates/compiler/can/src/env.rs b/crates/compiler/can/src/env.rs new file mode 100644 index 0000000000..38c26ec464 --- /dev/null +++ b/crates/compiler/can/src/env.rs @@ -0,0 +1,196 @@ +use crate::procedure::References; +use crate::scope::Scope; +use bumpalo::Bump; +use roc_collections::{MutMap, VecSet}; +use roc_module::ident::{Ident, Lowercase, ModuleName}; +use roc_module::symbol::{IdentIdsByModule, ModuleId, ModuleIds, Symbol}; +use roc_problem::can::{Problem, RuntimeError}; +use roc_region::all::{Loc, Region}; + +/// The canonicalization environment for a particular module. +pub struct Env<'a> { + /// The module's path. Opaques and unqualified references to identifiers + /// are assumed to be relative to this path. + pub home: ModuleId, + + pub dep_idents: &'a IdentIdsByModule, + + pub module_ids: &'a ModuleIds, + + /// Problems we've encountered along the way, which will be reported to the user at the end. + pub problems: Vec, + + /// Closures + pub closures: MutMap, + + /// current tail-callable symbol + pub tailcallable_symbol: Option, + + /// Symbols of values/functions which were referenced by qualified lookups. + pub qualified_value_lookups: VecSet, + + /// Symbols of types which were referenced by qualified lookups. + pub qualified_type_lookups: VecSet, + + pub top_level_symbols: VecSet, + + pub arena: &'a Bump, +} + +impl<'a> Env<'a> { + pub fn new( + arena: &'a Bump, + home: ModuleId, + dep_idents: &'a IdentIdsByModule, + module_ids: &'a ModuleIds, + ) -> Env<'a> { + Env { + arena, + home, + dep_idents, + module_ids, + problems: Vec::new(), + closures: MutMap::default(), + qualified_value_lookups: VecSet::default(), + qualified_type_lookups: VecSet::default(), + tailcallable_symbol: None, + top_level_symbols: VecSet::default(), + } + } + + pub fn qualified_lookup( + &mut self, + scope: &Scope, + module_name_str: &str, + ident: &str, + region: Region, + ) -> Result { + debug_assert!( + !module_name_str.is_empty(), + "Called env.qualified_lookup with an unqualified ident: {ident:?}" + ); + + let module_name = ModuleName::from(module_name_str); + + match self.module_ids.get_id(&module_name) { + Some(module_id) => self.qualified_lookup_help(scope, module_id, ident, region), + None => Err(RuntimeError::ModuleNotImported { + module_name, + imported_modules: self + .module_ids + .available_modules() + .map(|string| string.as_ref().into()) + .collect(), + region, + module_exists: false, + }), + } + } + + pub fn qualified_lookup_with_module_id( + &mut self, + scope: &Scope, + module_id: ModuleId, + ident: &str, + region: Region, + ) -> Result { + self.qualified_lookup_help(scope, module_id, ident, region) + } + + /// Returns Err if the symbol resolved, but it was not exposed by the given module + fn qualified_lookup_help( + &mut self, + scope: &Scope, + module_id: ModuleId, + ident: &str, + region: Region, + ) -> Result { + let is_type_name = ident.starts_with(|c: char| c.is_uppercase()); + + // You can do qualified lookups on your own module, e.g. + // if I'm in the Foo module, I can do a `Foo.bar` lookup. + if module_id == self.home { + match scope.locals.ident_ids.get_id(ident) { + Some(ident_id) => { + let symbol = Symbol::new(module_id, ident_id); + + if is_type_name { + self.qualified_type_lookups.insert(symbol); + } else { + self.qualified_value_lookups.insert(symbol); + } + + Ok(symbol) + } + None => { + let error = RuntimeError::LookupNotInScope { + loc_name: Loc { + value: Ident::from(ident), + region, + }, + suggestion_options: scope + .locals + .ident_ids + .ident_strs() + .map(|(_, string)| string.into()) + .collect(), + underscored_suggestion_region: None, + }; + Err(error) + } + } + } else { + match self.dep_idents.get(&module_id) { + Some(exposed_ids) => match exposed_ids.get_id(ident) { + Some(ident_id) => { + let symbol = Symbol::new(module_id, ident_id); + + if is_type_name { + self.qualified_type_lookups.insert(symbol); + } else { + self.qualified_value_lookups.insert(symbol); + } + + Ok(symbol) + } + None => { + let exposed_values = exposed_ids + .ident_strs() + .filter(|(_, ident)| ident.starts_with(|c: char| c.is_lowercase())) + .map(|(_, ident)| Lowercase::from(ident)) + .collect(); + Err(RuntimeError::ValueNotExposed { + module_name: self + .module_ids + .get_name(module_id) + .expect("Module ID known, but not in the module IDs somehow") + .clone(), + ident: Ident::from(ident), + region, + exposed_values, + }) + } + }, + None => Err(RuntimeError::ModuleNotImported { + module_name: self + .module_ids + .get_name(module_id) + .expect("Module ID known, but not in the module IDs somehow") + .clone(), + imported_modules: self + .dep_idents + .keys() + .filter_map(|module_id| self.module_ids.get_name(*module_id)) + .map(|module_name| module_name.as_ref().into()) + .collect(), + region, + module_exists: true, + }), + } + } + } + + pub fn problem(&mut self, problem: Problem) { + self.problems.push(problem) + } +} diff --git a/crates/compiler/can/src/exhaustive.rs b/crates/compiler/can/src/exhaustive.rs new file mode 100644 index 0000000000..3d1c0e841b --- /dev/null +++ b/crates/compiler/can/src/exhaustive.rs @@ -0,0 +1,748 @@ +use crate::expr::{self, IntValue, WhenBranch}; +use crate::pattern::DestructType; +use roc_collections::all::HumanIndex; +use roc_collections::VecMap; +use roc_error_macros::internal_error; +use roc_exhaustive::{ + is_useful, Ctor, CtorName, Error, Guard, ListArity, Literal, Pattern, RenderAs, TagId, Union, +}; +use roc_module::ident::{Lowercase, TagIdIntType, TagName}; +use roc_module::symbol::Symbol; +use roc_region::all::{Loc, Region}; +use roc_types::subs::{ + Content, FlatType, GetSubsSlice, RedundantMark, SortedTagsIterator, Subs, SubsFmtContent, + Variable, +}; +use roc_types::types::{gather_tags_unsorted_iter, AliasKind}; + +pub use roc_exhaustive::Context as ExhaustiveContext; + +pub const GUARD_CTOR: &str = "#Guard"; +pub const NONEXHAUSIVE_CTOR: &str = "#Open"; + +pub struct ExhaustiveSummary { + pub errors: Vec, + pub exhaustive: bool, + pub redundancies: Vec, +} + +#[derive(Debug)] +pub struct TypeError; + +/// Exhaustiveness-checks [sketched rows][SketchedRows] against an expected type. +/// +/// Returns an error if the sketch has a type error, in which case exhautiveness checking will not +/// have been performed. +pub fn check( + subs: &Subs, + real_var: Variable, + sketched_rows: SketchedRows, + context: ExhaustiveContext, +) -> Result { + let overall_region = sketched_rows.overall_region; + let mut all_errors = Vec::with_capacity(1); + + let NonRedundantSummary { + non_redundant_rows, + errors, + redundancies, + } = sketched_rows.reify_to_non_redundant(subs, real_var)?; + all_errors.extend(errors); + + let exhaustive = match roc_exhaustive::check(overall_region, context, non_redundant_rows) { + Ok(()) => true, + Err(errors) => { + all_errors.extend(errors); + false + } + }; + + Ok(ExhaustiveSummary { + errors: all_errors, + exhaustive, + redundancies, + }) +} + +#[derive(Clone, Debug, PartialEq, Eq)] +enum SketchedPattern { + Anything, + Literal(Literal), + /// A constructor whose expected union is not yet known. + /// We'll know the whole union when reifying the sketched pattern against an expected case type. + Ctor(TagName, Vec), + KnownCtor(Union, TagId, Vec), + List(ListArity, Vec), +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +enum IndexCtor<'a> { + /// Index an opaque type. There should be one argument. + Opaque, + /// Index a record type. The arguments are the types of the record fields. + Record(&'a [Lowercase]), + /// Index a tuple type. + Tuple, + /// Index a guard constructor. The arguments are a faux guard pattern, and then the real + /// pattern being guarded. E.g. `A B if g` becomes Guard { [True, (A B)] }. + Guard, + /// Index a tag union with the given tag constructor. + Tag(&'a TagName), + /// Index a list type. The argument is the element type. + List, +} + +impl<'a> IndexCtor<'a> { + fn of_union(un: &'a Union, tag_id: TagId) -> Self { + let Union { + alternatives, + render_as, + } = un; + + match render_as { + RenderAs::Tag => { + let tag_name = alternatives + .iter() + .find(|ctor| ctor.tag_id == tag_id) + .map(|Ctor { name, .. }| match name { + CtorName::Tag(tag) => tag, + CtorName::Opaque(_) => { + internal_error!("tag union should never have opaque alternative") + } + }) + .expect("indexable tag ID must be known to alternatives"); + Self::Tag(tag_name) + } + RenderAs::Opaque => Self::Opaque, + RenderAs::Record(fields) => Self::Record(fields), + RenderAs::Tuple => Self::Tuple, + RenderAs::Guard => Self::Guard, + } + } +} + +/// Index a variable as a certain constructor, to get the expected argument types of that constructor. +fn index_var( + subs: &Subs, + mut var: Variable, + ctor: IndexCtor, + render_as: &RenderAs, +) -> Result, TypeError> { + if matches!(ctor, IndexCtor::Guard) { + // `A B if g` becomes Guard { [True, (A B)] }, so the arguments are a bool, and the type + // of the pattern. + return Ok(vec![Variable::BOOL, var]); + } + loop { + match subs.get_content_without_compacting(var) { + Content::FlexVar(_) + | Content::RigidVar(_) + | Content::FlexAbleVar(_, _) + | Content::RigidAbleVar(_, _) + | Content::LambdaSet(_) + | Content::ErasedLambda + | Content::RangedNumber(..) => return Err(TypeError), + Content::Error => return Err(TypeError), + Content::RecursionVar { + structure, + opt_name: _, + } => { + var = *structure; + } + Content::Structure(structure) => match structure { + FlatType::Func(_, _, _) => return Err(TypeError), + FlatType::Apply(Symbol::LIST_LIST, args) => { + match (subs.get_subs_slice(*args), ctor) { + ([elem_var], IndexCtor::List) => { + return Ok(vec![*elem_var]); + } + _ => internal_error!("list types can only be indexed by list patterns"), + } + } + FlatType::Apply(..) => internal_error!("not an indexable constructor"), + FlatType::Record(fields, ext) => { + let fields_order = match render_as { + RenderAs::Record(fields) => fields, + _ => internal_error!( + "record constructors must always be rendered as records" + ), + }; + let iter = fields + .unsorted_iterator(subs, *ext) + .expect("should not have errors if performing exhautiveness checking"); + + let map: VecMap<_, _> = iter + .map(|(name, field)| (name, *field.as_inner())) + .collect(); + + let field_types = fields_order + .iter() + .map(|field| { + *map.get(&field) + .expect("field must be present during exhautiveness checking") + }) + .collect(); + + return Ok(field_types); + } + FlatType::Tuple(elems, ext) => { + let elem_types = elems + .sorted_iterator(subs, *ext) + .map(|(_, elem)| elem) + .collect(); + + return Ok(elem_types); + } + FlatType::TagUnion(tags, ext) | FlatType::RecursiveTagUnion(_, tags, ext) => { + let tag_ctor = match ctor { + IndexCtor::Tag(name) => name, + _ => { + internal_error!("constructor in a tag union must be tag") + } + }; + let mut iter = tags.unsorted_iterator(subs, *ext); + let opt_vars = iter.find_map(|(tag, vars)| { + if tag == tag_ctor { + Some(vars.to_vec()) + } else { + None + } + }); + let vars = opt_vars.expect("constructor must be known in the indexable type if we are exhautiveness checking"); + return Ok(vars); + } + FlatType::FunctionOrTagUnion(tags, _, _) => { + let tag_ctor = match ctor { + IndexCtor::Tag(name) => name, + _ => { + internal_error!("constructor in a tag union must be tag") + } + }; + + let tags = subs.get_subs_slice(*tags); + debug_assert!(tags.contains(tag_ctor), "constructor must be known in the indexable type if we are exhautiveness checking"); + + return Ok(vec![]); + } + FlatType::EmptyRecord => { + debug_assert!(matches!(ctor, IndexCtor::Record(..))); + // If there are optional record fields we don't unify them, but we need to + // cover them. Since optional fields correspond to "any" patterns, we can pass + // through arbitrary types. + let num_fields = match render_as { + RenderAs::Record(fields) => fields.len(), + _ => internal_error!( + "record constructors must always be rendered as records" + ), + }; + return Ok(std::iter::repeat(Variable::NULL).take(num_fields).collect()); + } + FlatType::EmptyTuple => { + return Ok(std::iter::repeat(Variable::NULL).take(0).collect()); + } + FlatType::EmptyTagUnion => { + internal_error!("empty tag unions are not indexable") + } + }, + Content::Alias(_, _, var, AliasKind::Opaque) => { + debug_assert!(matches!(ctor, IndexCtor::Opaque)); + return Ok(vec![*var]); + } + Content::Alias(_, _, inner, AliasKind::Structural) => { + var = *inner; + } + } + } +} + +impl SketchedPattern { + fn reify(self, subs: &Subs, real_var: Variable) -> Result { + match self { + Self::Anything => Ok(Pattern::Anything), + Self::Literal(lit) => Ok(Pattern::Literal(lit)), + Self::KnownCtor(union, tag_id, patterns) => { + let index_ctor = IndexCtor::of_union(&union, tag_id); + let arg_vars = index_var(subs, real_var, index_ctor, &union.render_as)?; + + debug_assert!(arg_vars.len() == patterns.len()); + let args = (patterns.into_iter()) + .zip(arg_vars) + .map(|(pat, var)| pat.reify(subs, var)) + .collect::, _>>()?; + + Ok(Pattern::Ctor(union, tag_id, args)) + } + Self::Ctor(tag_name, patterns) => { + let arg_vars = + index_var(subs, real_var, IndexCtor::Tag(&tag_name), &RenderAs::Tag)?; + let (union, tag_id) = convert_tag(subs, real_var, &tag_name); + + debug_assert!(arg_vars.len() == patterns.len()); + let args = (patterns.into_iter()) + .zip(arg_vars) + .map(|(pat, var)| pat.reify(subs, var)) + .collect::, _>>()?; + + Ok(Pattern::Ctor(union, tag_id, args)) + } + Self::List(arity, patterns) => { + let elem_var = index_var(subs, real_var, IndexCtor::List, &RenderAs::Tag)?[0]; + + let patterns = patterns + .into_iter() + .map(|pat| pat.reify(subs, elem_var)) + .collect::, _>>()?; + + Ok(Pattern::List(arity, patterns)) + } + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +struct SketchedRow { + patterns: Vec, + region: Region, + guard: Guard, + redundant_mark: RedundantMark, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct SketchedRows { + rows: Vec, + overall_region: Region, +} + +impl SketchedRows { + fn reify_to_non_redundant( + self, + subs: &Subs, + real_var: Variable, + ) -> Result { + to_nonredundant_rows(subs, real_var, self) + } +} + +fn sketch_pattern(pattern: &crate::pattern::Pattern) -> SketchedPattern { + use crate::pattern::Pattern::*; + use SketchedPattern as SP; + + match pattern { + As(subpattern, _) => sketch_pattern(&subpattern.value), + &NumLiteral(_, _, IntValue::I128(n), _) | &IntLiteral(_, _, _, IntValue::I128(n), _) => { + SP::Literal(Literal::Int(n)) + } + &NumLiteral(_, _, IntValue::U128(n), _) | &IntLiteral(_, _, _, IntValue::U128(n), _) => { + SP::Literal(Literal::U128(n)) + } + &FloatLiteral(_, _, _, f, _) => SP::Literal(Literal::Float(f64::to_bits(f))), + StrLiteral(v) => SP::Literal(Literal::Str(v.clone())), + &SingleQuote(_, _, c, _) => SP::Literal(Literal::Byte(c as u8)), + RecordDestructure { destructs, .. } => { + let tag_id = TagId(0); + let mut patterns = std::vec::Vec::with_capacity(destructs.len()); + let mut field_names = std::vec::Vec::with_capacity(destructs.len()); + + for Loc { + value: destruct, + region: _, + } in destructs + { + field_names.push(destruct.label.clone()); + + match &destruct.typ { + DestructType::Required | DestructType::Optional(..) => { + patterns.push(SP::Anything) + } + DestructType::Guard(_, guard) => patterns.push(sketch_pattern(&guard.value)), + } + } + + let union = Union { + render_as: RenderAs::Record(field_names), + alternatives: vec![Ctor { + name: CtorName::Tag(TagName("#Record".into())), + tag_id, + arity: destructs.len(), + }], + }; + + SP::KnownCtor(union, tag_id, patterns) + } + + TupleDestructure { destructs, .. } => { + let tag_id = TagId(0); + let mut patterns = std::vec::Vec::with_capacity(destructs.len()); + + for Loc { + value: destruct, + region: _, + } in destructs + { + patterns.push(sketch_pattern(&destruct.typ.1.value)); + } + + let union = Union { + render_as: RenderAs::Tuple, + alternatives: vec![Ctor { + name: CtorName::Tag(TagName("#Record".into())), + tag_id, + arity: destructs.len(), + }], + }; + + SP::KnownCtor(union, tag_id, patterns) + } + + List { + patterns, + list_var: _, + elem_var: _, + } => { + let arity = patterns.arity(); + + let sketched_elem_patterns = patterns + .patterns + .iter() + .map(|p| sketch_pattern(&p.value)) + .collect(); + + SP::List(arity, sketched_elem_patterns) + } + + AppliedTag { + tag_name, + arguments, + .. + } => { + let simplified_args: std::vec::Vec<_> = arguments + .iter() + .map(|(_, arg)| sketch_pattern(&arg.value)) + .collect(); + + SP::Ctor(tag_name.clone(), simplified_args) + } + + UnwrappedOpaque { + opaque, argument, .. + } => { + let (_, argument) = &(**argument); + + let tag_id = TagId(0); + + let union = Union { + render_as: RenderAs::Opaque, + alternatives: vec![Ctor { + name: CtorName::Opaque(*opaque), + tag_id, + arity: 1, + }], + }; + + SP::KnownCtor(union, tag_id, vec![sketch_pattern(&argument.value)]) + } + + // Treat this like a literal so we mark it as non-exhaustive + MalformedPattern(..) => SP::Literal(Literal::Byte(1)), + + Underscore + | Identifier(_) + | AbilityMemberSpecialization { .. } + | Shadowed(..) + | OpaqueNotInScope(..) + | UnsupportedPattern(..) => SP::Anything, + } +} + +pub fn sketch_when_branches(region: Region, patterns: &[expr::WhenBranch]) -> SketchedRows { + let mut rows: Vec = Vec::with_capacity(patterns.len()); + + // If any of the branches has a guard, e.g. + // + // when x is + // y if y < 10 -> "foo" + // _ -> "bar" + // + // then we treat it as a pattern match on the pattern and a boolean, wrapped in the #Guard + // constructor. We can use this special constructor name to generate better error messages. + // This transformation of the pattern match only works because we only report exhaustiveness + // errors: the Pattern created in this file is not used for code gen. + // + // when x is + // #Guard y True -> "foo" + // #Guard _ _ -> "bar" + let any_has_guard = patterns.iter().any(|branch| branch.guard.is_some()); + + use SketchedPattern as SP; + for WhenBranch { + patterns, + guard, + value: _, + redundant, + } in patterns + { + let guard = if guard.is_some() { + Guard::HasGuard + } else { + Guard::NoGuard + }; + + for loc_pat in patterns { + // Decompose each pattern in the branch into its own row. + + let patterns = if any_has_guard { + let guard_pattern = match guard { + Guard::HasGuard => SP::Literal(Literal::Bit(true)), + Guard::NoGuard => SP::Anything, + }; + + let tag_id = TagId(0); + + let union = Union { + render_as: RenderAs::Guard, + alternatives: vec![Ctor { + tag_id, + name: CtorName::Tag(TagName(GUARD_CTOR.into())), + arity: 2, + }], + }; + + vec![SP::KnownCtor( + union, + tag_id, + // NB: ordering the guard pattern first seems to be better at catching + // non-exhaustive constructors in the second argument; see the paper to see if + // there is a way to improve this in general. + vec![guard_pattern, sketch_pattern(&loc_pat.pattern.value)], + )] + } else { + // Simple case + vec![sketch_pattern(&loc_pat.pattern.value)] + }; + + let row = SketchedRow { + patterns, + region: loc_pat.pattern.region, + guard, + redundant_mark: *redundant, + }; + rows.push(row); + } + } + + SketchedRows { + rows, + overall_region: region, + } +} + +pub fn sketch_pattern_to_rows(region: Region, pattern: &crate::pattern::Pattern) -> SketchedRows { + let row = SketchedRow { + patterns: vec![sketch_pattern(pattern)], + region, + // A single row cannot be redundant! + redundant_mark: RedundantMark::known_non_redundant(), + guard: Guard::NoGuard, + }; + SketchedRows { + rows: vec![row], + overall_region: region, + } +} + +/// REDUNDANT PATTERNS + +struct NonRedundantSummary { + non_redundant_rows: Vec>, + redundancies: Vec, + errors: Vec, +} + +/// INVARIANT: Produces a list of rows where (forall row. length row == 1) +fn to_nonredundant_rows( + subs: &Subs, + real_var: Variable, + rows: SketchedRows, +) -> Result { + let SketchedRows { + rows, + overall_region, + } = rows; + let mut checked_rows = Vec::with_capacity(rows.len()); + + let mut redundancies = vec![]; + let mut errors = vec![]; + + for ( + row_number, + SketchedRow { + patterns, + guard, + region, + redundant_mark, + }, + ) in rows.into_iter().enumerate() + { + let next_row: Vec = patterns + .into_iter() + .map(|pattern| pattern.reify(subs, real_var)) + .collect::>()?; + + let redundant_err = if !is_inhabited_row(&next_row) { + Some(Error::Unmatchable { + overall_region, + branch_region: region, + index: HumanIndex::zero_based(row_number), + }) + } else if !(matches!(guard, Guard::HasGuard) + || is_useful(checked_rows.clone(), next_row.clone())) + { + Some(Error::Redundant { + overall_region, + branch_region: region, + index: HumanIndex::zero_based(row_number), + }) + } else { + None + }; + + match redundant_err { + None => { + checked_rows.push(next_row); + } + Some(err) => { + redundancies.push(redundant_mark); + errors.push(err); + } + } + } + + Ok(NonRedundantSummary { + non_redundant_rows: checked_rows, + redundancies, + errors, + }) +} + +fn is_inhabited_row(patterns: &[Pattern]) -> bool { + patterns.iter().any(is_inhabited_pattern) +} + +fn is_inhabited_pattern(pat: &Pattern) -> bool { + let mut stack = vec![pat]; + while let Some(pat) = stack.pop() { + match pat { + Pattern::Anything => {} + Pattern::Literal(_) => {} + Pattern::Ctor(union, id, pats) => { + if !union.alternatives.iter().any(|alt| alt.tag_id == *id) { + // The tag ID was dropped from the union, which means that this tag ID is one + // that is not material to the union, and so is uninhabited! + return false; + } + stack.extend(pats); + } + Pattern::List(_, pats) => { + // List is uninhabited if any element is uninhabited. + stack.extend(pats); + } + } + } + true +} + +fn convert_tag(subs: &Subs, whole_var: Variable, this_tag: &TagName) -> (Union, TagId) { + let content = subs.get_content_without_compacting(whole_var); + + use {Content::*, FlatType::*}; + + let (sorted_tags, ext) = match dealias_tag(subs, content) { + Structure(TagUnion(tags, ext) | RecursiveTagUnion(_, tags, ext)) => { + let (sorted_tags, ext) = tags.sorted_iterator_and_ext(subs, *ext); + (sorted_tags, ext) + } + Structure(FunctionOrTagUnion(tags, _, ext)) => { + let (ext_tags, ext) = gather_tags_unsorted_iter(subs, Default::default(), *ext) + .unwrap_or_else(|_| { + internal_error!("Content is not a tag union: {:?}", subs.dbg(whole_var)) + }); + let mut all_tags: Vec<(TagName, &[Variable])> = Vec::with_capacity(tags.len()); + for tag in subs.get_subs_slice(*tags) { + all_tags.push((tag.clone(), &[])); + } + for (tag, vars) in ext_tags { + debug_assert!(vars.is_empty()); + all_tags.push((tag.clone(), &[])); + } + (Box::new(all_tags.into_iter()) as SortedTagsIterator, ext) + } + _ => internal_error!( + "Content is not a tag union: {:?}", + SubsFmtContent(content, subs) + ), + }; + + let mut num_tags = sorted_tags.len(); + + // DEVIATION: model openness by attaching a #Open constructor, that can never + // be matched unless there's an `Anything` pattern. + let opt_openness_tag = match subs.get_content_without_compacting(ext.var()) { + FlexVar(_) | RigidVar(_) => { + let openness_tag = TagName(NONEXHAUSIVE_CTOR.into()); + num_tags += 1; + Some((openness_tag, &[] as _)) + } + Structure(EmptyTagUnion) => None, + // Anything else is erroneous and we ignore + _ => None, + }; + + // High tag ID if we're out-of-bounds. + let mut my_tag_id = TagId(num_tags as TagIdIntType); + + let mut alternatives = Vec::with_capacity(num_tags); + let alternatives_iter = sorted_tags.into_iter().chain(opt_openness_tag); + + let mut index = 0; + for (tag, args) in alternatives_iter { + let is_inhabited = args.iter().all(|v| subs.is_inhabited(*v)); + if !is_inhabited { + // This constructor is not material; we don't need to match over it! + continue; + } + + let tag_id = TagId(index as TagIdIntType); + index += 1; + + if this_tag == &tag { + my_tag_id = tag_id; + } + alternatives.push(Ctor { + name: CtorName::Tag(tag), + tag_id, + arity: args.len(), + }); + } + + let union = Union { + alternatives, + render_as: RenderAs::Tag, + }; + + (union, my_tag_id) +} + +pub fn dealias_tag<'a>(subs: &'a Subs, content: &'a Content) -> &'a Content { + use Content::*; + let mut result = content; + loop { + match result { + Alias(_, _, real_var, AliasKind::Structural) + | RecursionVar { + structure: real_var, + .. + } => result = subs.get_content_without_compacting(*real_var), + _ => return result, + } + } +} diff --git a/compiler/can/src/expected.rs b/crates/compiler/can/src/expected.rs similarity index 81% rename from compiler/can/src/expected.rs rename to crates/compiler/can/src/expected.rs index df156e00fc..d1fae08746 100644 --- a/compiler/can/src/expected.rs +++ b/crates/compiler/can/src/expected.rs @@ -38,6 +38,16 @@ impl PExpected { } } + #[inline(always)] + pub fn map(self, f: impl FnOnce(T) -> U) -> PExpected { + match self { + PExpected::NoExpectation(val) => PExpected::NoExpectation(f(val)), + PExpected::ForReason(reason, val, region) => { + PExpected::ForReason(reason, f(val), region) + } + } + } + pub fn replace(self, new: U) -> PExpected { match self { PExpected::NoExpectation(_val) => PExpected::NoExpectation(new), @@ -89,6 +99,17 @@ impl Expected { } } + #[inline(always)] + pub fn map(self, f: impl FnOnce(T) -> U) -> Expected { + match self { + Expected::NoExpectation(val) => Expected::NoExpectation(f(val)), + Expected::ForReason(reason, val, region) => Expected::ForReason(reason, f(val), region), + Expected::FromAnnotation(pattern, size, source, val) => { + Expected::FromAnnotation(pattern, size, source, f(val)) + } + } + } + pub fn replace(self, new: U) -> Expected { match self { Expected::NoExpectation(_val) => Expected::NoExpectation(new), diff --git a/crates/compiler/can/src/expr.rs b/crates/compiler/can/src/expr.rs new file mode 100644 index 0000000000..474a721bdb --- /dev/null +++ b/crates/compiler/can/src/expr.rs @@ -0,0 +1,3354 @@ +use crate::abilities::SpecializationId; +use crate::annotation::{freshen_opaque_def, IntroducedVariables}; +use crate::builtins::builtin_defs_map; +use crate::def::{can_defs_with_return, Annotation, Def}; +use crate::env::Env; +use crate::num::{ + finish_parsing_base, finish_parsing_float, finish_parsing_num, float_expr_from_result, + int_expr_from_result, num_expr_from_result, FloatBound, IntBound, NumBound, +}; +use crate::pattern::{canonicalize_pattern, BindingsFromPattern, Pattern, PermitShadows}; +use crate::procedure::References; +use crate::scope::Scope; +use crate::traverse::{walk_expr, Visitor}; +use roc_collections::soa::Index; +use roc_collections::{SendMap, VecMap, VecSet}; +use roc_error_macros::internal_error; +use roc_module::called_via::CalledVia; +use roc_module::ident::{ForeignSymbol, Lowercase, TagName}; +use roc_module::low_level::LowLevel; +use roc_module::symbol::Symbol; +use roc_parse::ast::{self, Defs, PrecedenceConflict, StrLiteral}; +use roc_parse::ident::Accessor; +use roc_parse::pattern::PatternType::*; +use roc_problem::can::{PrecedenceProblem, Problem, RuntimeError}; +use roc_region::all::{Loc, Region}; +use roc_types::num::SingleQuoteBound; +use roc_types::subs::{ExhaustiveMark, IllegalCycleMark, RedundantMark, VarStore, Variable}; +use roc_types::types::{Alias, Category, IndexOrField, LambdaSet, OptAbleVar, Type}; +use std::fmt::{Debug, Display}; +use std::fs::File; +use std::io::Read; +use std::path::PathBuf; +use std::sync::Arc; +use std::{char, u32}; + +/// Derives that an opaque type has claimed, to checked and recorded after solving. +pub type PendingDerives = VecMap>)>; + +#[derive(Clone, Default, Debug)] +pub struct Output { + pub references: References, + pub tail_call: Option, + pub introduced_variables: IntroducedVariables, + pub aliases: VecMap, + pub non_closures: VecSet, + pub pending_derives: PendingDerives, +} + +impl Output { + pub fn union(&mut self, other: Self) { + self.references.union_mut(&other.references); + + if let (None, Some(later)) = (self.tail_call, other.tail_call) { + self.tail_call = Some(later); + } + + self.introduced_variables + .union_owned(other.introduced_variables); + self.aliases.extend(other.aliases); + self.non_closures.extend(other.non_closures); + + { + let expected_derives_size = self.pending_derives.len() + other.pending_derives.len(); + self.pending_derives.extend(other.pending_derives); + debug_assert!( + expected_derives_size == self.pending_derives.len(), + "Derives overwritten from nested scope - something is very wrong" + ); + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq, Copy)] +pub enum IntValue { + I128([u8; 16]), + U128([u8; 16]), +} + +impl Display for IntValue { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + IntValue::I128(n) => Display::fmt(&i128::from_ne_bytes(*n), f), + IntValue::U128(n) => Display::fmt(&u128::from_ne_bytes(*n), f), + } + } +} + +#[derive(Clone, Debug)] +pub enum Expr { + // Literals + + // Num stores the `a` variable in `Num a`. Not the same as the variable + // stored in Int and Float below, which is strictly for better error messages + Num(Variable, Box, IntValue, NumBound), + + // Int and Float store a variable to generate better error messages + Int(Variable, Variable, Box, IntValue, IntBound), + Float(Variable, Variable, Box, f64, FloatBound), + Str(Box), + // Number variable, precision variable, value, bound + SingleQuote(Variable, Variable, char, SingleQuoteBound), + List { + elem_var: Variable, + loc_elems: Vec>, + }, + + // An ingested files, it's bytes, and the type variable. + IngestedFile(Box, Arc>, Variable), + + // Lookups + Var(Symbol, Variable), + AbilityMember( + /// Actual member name + Symbol, + /// Specialization to use, and its variable. + /// The specialization id may be [`None`] if construction of an ability member usage can + /// prove the usage is polymorphic. + Option, + Variable, + ), + + // Branching + When { + /// The actual condition of the when expression. + loc_cond: Box>, + cond_var: Variable, + /// Result type produced by the branches. + expr_var: Variable, + region: Region, + /// The branches of the when, and the type of the condition that they expect to be matched + /// against. + branches: Vec, + branches_cond_var: Variable, + /// Whether the branches are exhaustive. + exhaustive: ExhaustiveMark, + }, + If { + cond_var: Variable, + branch_var: Variable, + branches: Vec<(Loc, Loc)>, + final_else: Box>, + }, + + // Let + LetRec(Vec, Box>, IllegalCycleMark), + LetNonRec(Box, Box>), + + /// This is *only* for calling functions, not for tag application. + /// The Tag variant contains any applied values inside it. + Call( + Box<(Variable, Loc, Variable, Variable)>, + Vec<(Variable, Loc)>, + CalledVia, + ), + RunLowLevel { + op: LowLevel, + args: Vec<(Variable, Expr)>, + ret_var: Variable, + }, + ForeignCall { + foreign_symbol: ForeignSymbol, + args: Vec<(Variable, Expr)>, + ret_var: Variable, + }, + + Closure(ClosureData), + + // Product Types + Record { + record_var: Variable, + fields: SendMap, + }, + + /// Empty record constant + EmptyRecord, + + Tuple { + tuple_var: Variable, + elems: Vec<(Variable, Box>)>, + }, + + /// The "crash" keyword + Crash { + msg: Box>, + ret_var: Variable, + }, + + /// Look up exactly one field on a record, e.g. (expr).foo. + RecordAccess { + record_var: Variable, + ext_var: Variable, + field_var: Variable, + loc_expr: Box>, + field: Lowercase, + }, + + /// tuple or field accessor as a function, e.g. (.foo) expr or (.1) expr + RecordAccessor(StructAccessorData), + + TupleAccess { + tuple_var: Variable, + ext_var: Variable, + elem_var: Variable, + loc_expr: Box>, + index: usize, + }, + + RecordUpdate { + record_var: Variable, + ext_var: Variable, + symbol: Symbol, + updates: SendMap, + }, + + // Sum Types + Tag { + tag_union_var: Variable, + ext_var: Variable, + name: TagName, + arguments: Vec<(Variable, Loc)>, + }, + + ZeroArgumentTag { + closure_name: Symbol, + variant_var: Variable, + ext_var: Variable, + name: TagName, + }, + + /// A wrapping of an opaque type, like `@Age 21` + OpaqueRef { + opaque_var: Variable, + name: Symbol, + argument: Box<(Variable, Loc)>, + + // The following help us link this opaque reference to the type specified by its + // definition, which we then use during constraint generation. For example + // suppose we have + // + // Id n := [Id U64 n] + // @Id "sasha" + // + // Then `opaque` is "Id", `argument` is "sasha", but this is not enough for us to + // infer the type of the expression as "Id Str" - we need to link the specialized type of + // the variable "n". + // That's what `specialized_def_type` and `type_arguments` are for; they are specialized + // for the expression from the opaque definition. `type_arguments` is something like + // [(n, fresh1)], and `specialized_def_type` becomes "[Id U64 fresh1]". + specialized_def_type: Box, + type_arguments: Vec, + lambda_set_variables: Vec, + }, + + // Opaque as a function, e.g. @Id as a shorthand for \x -> @Id x + OpaqueWrapFunction(OpaqueWrapFunctionData), + + // Test + Expect { + loc_condition: Box>, + loc_continuation: Box>, + lookups_in_cond: Vec, + }, + + // not parsed, but is generated when lowering toplevel effectful expects + ExpectFx { + loc_condition: Box>, + loc_continuation: Box>, + lookups_in_cond: Vec, + }, + + Dbg { + loc_message: Box>, + loc_continuation: Box>, + variable: Variable, + symbol: Symbol, + }, + + /// Rendered as empty box in editor + TypedHole(Variable), + + /// Compiles, but will crash if reached + RuntimeError(RuntimeError), +} + +#[derive(Clone, Copy, Debug)] +pub struct ExpectLookup { + pub symbol: Symbol, + pub var: Variable, + pub ability_info: Option, +} + +#[derive(Clone, Copy, Debug)] +pub struct DbgLookup { + pub symbol: Symbol, + pub var: Variable, + pub region: Region, + pub ability_info: Option, +} + +impl Expr { + pub fn category(&self) -> Category { + match self { + Self::Num(..) => Category::Num, + Self::Int(..) => Category::Int, + Self::Float(..) => Category::Frac, + Self::Str(..) => Category::Str, + Self::IngestedFile(file_path, _, _) => Category::IngestedFile(file_path.clone()), + Self::SingleQuote(..) => Category::Character, + Self::List { .. } => Category::List, + &Self::Var(sym, _) => Category::Lookup(sym), + &Self::AbilityMember(sym, _, _) => Category::Lookup(sym), + Self::When { .. } => Category::When, + Self::If { .. } => Category::If, + Self::LetRec(_, expr, _) => expr.value.category(), + Self::LetNonRec(_, expr) => expr.value.category(), + &Self::Call(_, _, called_via) => Category::CallResult(None, called_via), + &Self::RunLowLevel { op, .. } => Category::LowLevelOpResult(op), + Self::ForeignCall { .. } => Category::ForeignCall, + Self::Closure(..) => Category::Lambda, + Self::Tuple { .. } => Category::Tuple, + Self::Record { .. } => Category::Record, + Self::EmptyRecord => Category::Record, + Self::RecordAccess { field, .. } => Category::RecordAccess(field.clone()), + Self::RecordAccessor(data) => Category::Accessor(data.field.clone()), + Self::TupleAccess { index, .. } => Category::TupleAccess(*index), + Self::RecordUpdate { .. } => Category::Record, + Self::Tag { + name, arguments, .. + } => Category::TagApply { + tag_name: name.clone(), + args_count: arguments.len(), + }, + Self::ZeroArgumentTag { name, .. } => Category::TagApply { + tag_name: name.clone(), + args_count: 0, + }, + &Self::OpaqueRef { name, .. } => Category::OpaqueWrap(name), + &Self::OpaqueWrapFunction(OpaqueWrapFunctionData { opaque_name, .. }) => { + Category::OpaqueWrap(opaque_name) + } + Self::Expect { .. } => Category::Expect, + Self::ExpectFx { .. } => Category::Expect, + Self::Crash { .. } => Category::Crash, + + Self::Dbg { .. } => Category::Expect, + + // these nodes place no constraints on the expression's type + Self::TypedHole(_) | Self::RuntimeError(..) => Category::Unknown, + } + } +} + +/// Stores exhaustiveness-checking metadata for a closure argument that may +/// have an annotated type. +#[derive(Clone, Copy, Debug)] +pub struct AnnotatedMark { + pub annotation_var: Variable, + pub exhaustive: ExhaustiveMark, +} + +impl AnnotatedMark { + pub fn new(var_store: &mut VarStore) -> Self { + Self { + annotation_var: var_store.fresh(), + exhaustive: ExhaustiveMark::new(var_store), + } + } + + // NOTE: only ever use this if you *know* a pattern match is surely exhaustive! + // Otherwise you will get unpleasant unification errors. + pub fn known_exhaustive() -> Self { + Self { + annotation_var: Variable::EMPTY_TAG_UNION, + exhaustive: ExhaustiveMark::known_exhaustive(), + } + } +} + +#[derive(Clone, Debug)] +pub struct ClosureData { + pub function_type: Variable, + pub closure_type: Variable, + pub return_type: Variable, + pub name: Symbol, + pub captured_symbols: Vec<(Symbol, Variable)>, + pub recursive: Recursive, + pub arguments: Vec<(Variable, AnnotatedMark, Loc)>, + pub loc_body: Box>, +} + +/// A record or tuple accessor like `.foo` or `.0`, which is equivalent to `\r -> r.foo` +/// Struct accessors are desugared to closures; they need to have a name +/// so the closure can have a correct lambda set. +/// +/// We distinguish them from closures so we can have better error messages +/// during constraint generation. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct StructAccessorData { + pub name: Symbol, + pub function_var: Variable, + pub record_var: Variable, + pub closure_var: Variable, + pub ext_var: Variable, + pub field_var: Variable, + + /// Note that the `field` field is an `IndexOrField` in order to represent both + /// record and tuple accessors. This is different from `TupleAccess` and + /// `RecordAccess` (and RecordFields/TupleElems), which share less of their implementation. + pub field: IndexOrField, +} + +impl StructAccessorData { + pub fn to_closure_data(self, record_symbol: Symbol) -> ClosureData { + let StructAccessorData { + name, + function_var, + record_var, + closure_var, + ext_var, + field_var, + field, + } = self; + + // IDEA: convert accessor from + // + // .foo + // + // into + // + // (\r -> r.foo) + let body = match field { + IndexOrField::Index(index) => Expr::TupleAccess { + tuple_var: record_var, + ext_var, + elem_var: field_var, + loc_expr: Box::new(Loc::at_zero(Expr::Var(record_symbol, record_var))), + index, + }, + IndexOrField::Field(field) => Expr::RecordAccess { + record_var, + ext_var, + field_var, + loc_expr: Box::new(Loc::at_zero(Expr::Var(record_symbol, record_var))), + field, + }, + }; + + let loc_body = Loc::at_zero(body); + + let arguments = vec![( + record_var, + AnnotatedMark::known_exhaustive(), + Loc::at_zero(Pattern::Identifier(record_symbol)), + )]; + + ClosureData { + function_type: function_var, + closure_type: closure_var, + return_type: field_var, + name, + captured_symbols: vec![], + recursive: Recursive::NotRecursive, + arguments, + loc_body: Box::new(loc_body), + } + } +} + +/// An opaque wrapper like `@Foo`, which is equivalent to `\p -> @Foo p` +/// These are desugared to closures, but we distinguish them so we can have +/// better error messages during constraint generation. +#[derive(Clone, Debug)] +pub struct OpaqueWrapFunctionData { + pub opaque_name: Symbol, + pub opaque_var: Variable, + // The following fields help link the concrete opaque type; see + // `Expr::OpaqueRef` for more info on how they're used. + pub specialized_def_type: Type, + pub type_arguments: Vec, + pub lambda_set_variables: Vec, + + pub function_name: Symbol, + pub function_var: Variable, + pub argument_var: Variable, + pub closure_var: Variable, +} + +impl OpaqueWrapFunctionData { + pub fn to_closure_data(self, argument_symbol: Symbol) -> ClosureData { + let OpaqueWrapFunctionData { + opaque_name, + opaque_var, + specialized_def_type, + type_arguments, + lambda_set_variables, + function_name, + function_var, + argument_var, + closure_var, + } = self; + + // IDEA: convert + // + // @Foo + // + // into + // + // (\p -> @Foo p) + let body = Expr::OpaqueRef { + opaque_var, + name: opaque_name, + argument: Box::new(( + argument_var, + Loc::at_zero(Expr::Var(argument_symbol, argument_var)), + )), + specialized_def_type: Box::new(specialized_def_type), + type_arguments, + lambda_set_variables, + }; + + let loc_body = Loc::at_zero(body); + + let arguments = vec![( + argument_var, + AnnotatedMark::known_exhaustive(), + Loc::at_zero(Pattern::Identifier(argument_symbol)), + )]; + + ClosureData { + function_type: function_var, + closure_type: closure_var, + return_type: opaque_var, + name: function_name, + captured_symbols: vec![], + recursive: Recursive::NotRecursive, + arguments, + loc_body: Box::new(loc_body), + } + } +} + +#[derive(Clone, Debug)] +pub struct Field { + pub var: Variable, + // The region of the full `foo: f bar`, rather than just `f bar` + pub region: Region, + pub loc_expr: Box>, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Recursive { + NotRecursive = 0, + Recursive = 1, + TailRecursive = 2, +} + +impl Recursive { + pub fn is_recursive(&self) -> bool { + match self { + Recursive::NotRecursive => false, + Recursive::Recursive | Recursive::TailRecursive => true, + } + } +} + +#[derive(Clone, Debug)] +pub struct WhenBranchPattern { + pub pattern: Loc, + /// Degenerate branch patterns are those that don't fully bind symbols that the branch body + /// needs. For example, in `A x | B y -> x`, the `B y` pattern is degenerate. + /// Degenerate patterns emit a runtime error if reached in a program. + pub degenerate: bool, +} + +#[derive(Clone, Debug)] +pub struct WhenBranch { + pub patterns: Vec, + pub value: Loc, + pub guard: Option>, + /// Whether this branch is redundant in the `when` it appears in + pub redundant: RedundantMark, +} + +impl WhenBranch { + pub fn pattern_region(&self) -> Region { + Region::span_across( + &self + .patterns + .first() + .expect("when branch has no pattern?") + .pattern + .region, + &self + .patterns + .last() + .expect("when branch has no pattern?") + .pattern + .region, + ) + } +} + +impl WhenBranch { + pub fn region(&self) -> Region { + Region::across_all( + self.patterns + .iter() + .map(|p| &p.pattern.region) + .chain([self.value.region].iter()), + ) + } +} + +pub fn canonicalize_expr<'a>( + env: &mut Env<'a>, + var_store: &mut VarStore, + scope: &mut Scope, + region: Region, + expr: &'a ast::Expr<'a>, +) -> (Loc, Output) { + use Expr::*; + + let (expr, output) = match expr { + &ast::Expr::Num(str) => { + let answer = num_expr_from_result(var_store, finish_parsing_num(str), region, env); + + (answer, Output::default()) + } + &ast::Expr::Float(str) => { + let answer = float_expr_from_result(var_store, finish_parsing_float(str), region, env); + + (answer, Output::default()) + } + ast::Expr::Record(fields) => { + if fields.is_empty() { + (EmptyRecord, Output::default()) + } else { + match canonicalize_fields(env, var_store, scope, region, fields.items) { + Ok((can_fields, output)) => ( + Record { + record_var: var_store.fresh(), + fields: can_fields, + }, + output, + ), + Err(CanonicalizeRecordProblem::InvalidOptionalValue { + field_name, + field_region, + record_region, + }) => ( + Expr::RuntimeError(roc_problem::can::RuntimeError::InvalidOptionalValue { + field_name, + field_region, + record_region, + }), + Output::default(), + ), + } + } + } + + ast::Expr::RecordUpdate { + fields, + update: loc_update, + } => { + let (can_update, update_out) = + canonicalize_expr(env, var_store, scope, loc_update.region, &loc_update.value); + if let Var(symbol, _) = &can_update.value { + match canonicalize_fields(env, var_store, scope, region, fields.items) { + Ok((can_fields, mut output)) => { + output.references.union_mut(&update_out.references); + + let answer = RecordUpdate { + record_var: var_store.fresh(), + ext_var: var_store.fresh(), + symbol: *symbol, + updates: can_fields, + }; + + (answer, output) + } + Err(CanonicalizeRecordProblem::InvalidOptionalValue { + field_name, + field_region, + record_region, + }) => ( + Expr::RuntimeError(roc_problem::can::RuntimeError::InvalidOptionalValue { + field_name, + field_region, + record_region, + }), + Output::default(), + ), + } + } else { + // only (optionally qualified) variables can be updated, not arbitrary expressions + + let error = roc_problem::can::RuntimeError::InvalidRecordUpdate { + region: can_update.region, + }; + + let answer = Expr::RuntimeError(error.clone()); + + env.problems.push(Problem::RuntimeError(error)); + + (answer, Output::default()) + } + } + + ast::Expr::Tuple(fields) => { + let mut can_elems = Vec::with_capacity(fields.len()); + let mut references = References::new(); + + for loc_elem in fields.iter() { + let (can_expr, elem_out) = + canonicalize_expr(env, var_store, scope, loc_elem.region, &loc_elem.value); + + references.union_mut(&elem_out.references); + + can_elems.push((var_store.fresh(), Box::new(can_expr))); + } + + let output = Output { + references, + tail_call: None, + ..Default::default() + }; + + ( + Tuple { + tuple_var: var_store.fresh(), + elems: can_elems, + }, + output, + ) + } + + ast::Expr::Str(literal) => flatten_str_literal(env, var_store, scope, literal), + + ast::Expr::IngestedFile(file_path, _) => match File::open(file_path) { + Ok(mut file) => { + let mut bytes = vec![]; + match file.read_to_end(&mut bytes) { + Ok(_) => ( + Expr::IngestedFile( + file_path.to_path_buf().into(), + Arc::new(bytes), + var_store.fresh(), + ), + Output::default(), + ), + Err(e) => { + env.problems.push(Problem::FileProblem { + filename: file_path.to_path_buf(), + error: e.kind(), + }); + + // This will not manifest as a real runtime error and is just returned to have a value here. + // The pushed FileProblem will be fatal to compilation. + ( + Expr::RuntimeError(roc_problem::can::RuntimeError::NoImplementation), + Output::default(), + ) + } + } + } + Err(e) => { + env.problems.push(Problem::FileProblem { + filename: file_path.to_path_buf(), + error: e.kind(), + }); + + // This will not manifest as a real runtime error and is just returned to have a value here. + // The pushed FileProblem will be fatal to compilation. + ( + Expr::RuntimeError(roc_problem::can::RuntimeError::NoImplementation), + Output::default(), + ) + } + }, + + ast::Expr::SingleQuote(string) => { + let mut it = string.chars().peekable(); + if let Some(char) = it.next() { + if it.peek().is_none() { + ( + Expr::SingleQuote( + var_store.fresh(), + var_store.fresh(), + char, + SingleQuoteBound::from_char(char), + ), + Output::default(), + ) + } else { + // multiple chars is found + let error = roc_problem::can::RuntimeError::MultipleCharsInSingleQuote(region); + let answer = Expr::RuntimeError(error); + + (answer, Output::default()) + } + } else { + // no characters found + let error = roc_problem::can::RuntimeError::EmptySingleQuote(region); + let answer = Expr::RuntimeError(error); + + (answer, Output::default()) + } + } + + ast::Expr::List(loc_elems) => { + if loc_elems.is_empty() { + ( + List { + elem_var: var_store.fresh(), + loc_elems: Vec::new(), + }, + Output::default(), + ) + } else { + let mut can_elems = Vec::with_capacity(loc_elems.len()); + let mut references = References::new(); + + for loc_elem in loc_elems.iter() { + let (can_expr, elem_out) = + canonicalize_expr(env, var_store, scope, loc_elem.region, &loc_elem.value); + + references.union_mut(&elem_out.references); + + can_elems.push(can_expr); + } + + let output = Output { + references, + tail_call: None, + ..Default::default() + }; + + ( + List { + elem_var: var_store.fresh(), + loc_elems: can_elems, + }, + output, + ) + } + } + ast::Expr::Apply(loc_fn, loc_args, application_style) => { + // The expression that evaluates to the function being called, e.g. `foo` in + // (foo) bar baz + let fn_region = loc_fn.region; + + // The function's return type + let mut args = Vec::new(); + let mut output = Output::default(); + + for loc_arg in loc_args.iter() { + let (arg_expr, arg_out) = + canonicalize_expr(env, var_store, scope, loc_arg.region, &loc_arg.value); + + args.push((var_store.fresh(), arg_expr)); + output.references.union_mut(&arg_out.references); + } + + if let ast::Expr::OpaqueRef(name) = loc_fn.value { + // We treat opaques specially, since an opaque can wrap exactly one argument. + + debug_assert!(!args.is_empty()); + + if args.len() > 1 { + let problem = + roc_problem::can::RuntimeError::OpaqueAppliedToMultipleArgs(region); + env.problem(Problem::RuntimeError(problem.clone())); + (RuntimeError(problem), output) + } else { + match scope.lookup_opaque_ref(name, loc_fn.region) { + Err(runtime_error) => { + env.problem(Problem::RuntimeError(runtime_error.clone())); + (RuntimeError(runtime_error), output) + } + Ok((name, opaque_def)) => { + let argument = Box::new(args.pop().unwrap()); + output.references.insert_type_lookup(name); + + let (type_arguments, lambda_set_variables, specialized_def_type) = + freshen_opaque_def(var_store, opaque_def); + + let opaque_ref = OpaqueRef { + opaque_var: var_store.fresh(), + name, + argument, + specialized_def_type: Box::new(specialized_def_type), + type_arguments, + lambda_set_variables, + }; + + (opaque_ref, output) + } + } + } + } else if let ast::Expr::Crash = loc_fn.value { + // We treat crash specially, since crashing must be applied with one argument. + + debug_assert!(!args.is_empty()); + + let mut args = Vec::new(); + let mut output = Output::default(); + + for loc_arg in loc_args.iter() { + let (arg_expr, arg_out) = + canonicalize_expr(env, var_store, scope, loc_arg.region, &loc_arg.value); + + args.push(arg_expr); + output.references.union_mut(&arg_out.references); + } + + let crash = if args.len() > 1 { + let args_region = Region::span_across( + &loc_args.first().unwrap().region, + &loc_args.last().unwrap().region, + ); + env.problem(Problem::OverAppliedCrash { + region: args_region, + }); + // Still crash, just with our own message, and drop the references. + Crash { + msg: Box::new(Loc::at( + region, + Expr::Str(String::from("hit a crash!").into_boxed_str()), + )), + ret_var: var_store.fresh(), + } + } else { + let msg = args.pop().unwrap(); + Crash { + msg: Box::new(msg), + ret_var: var_store.fresh(), + } + }; + + (crash, output) + } else { + // Canonicalize the function expression and its arguments + let (fn_expr, fn_expr_output) = + canonicalize_expr(env, var_store, scope, fn_region, &loc_fn.value); + + output.union(fn_expr_output); + + // Default: We're not tail-calling a symbol (by name), we're tail-calling a function value. + output.tail_call = None; + + let expr = match fn_expr.value { + Var(symbol, _) => { + output.references.insert_call(symbol); + + // we're tail-calling a symbol by name, check if it's the tail-callable symbol + output.tail_call = match &env.tailcallable_symbol { + Some(tc_sym) if *tc_sym == symbol => Some(symbol), + Some(_) | None => None, + }; + + Call( + Box::new(( + var_store.fresh(), + fn_expr, + var_store.fresh(), + var_store.fresh(), + )), + args, + *application_style, + ) + } + RuntimeError(_) => { + // We can't call a runtime error; bail out by propagating it! + return (fn_expr, output); + } + Tag { + tag_union_var: variant_var, + ext_var, + name, + .. + } => Tag { + tag_union_var: variant_var, + ext_var, + name, + arguments: args, + }, + ZeroArgumentTag { + variant_var, + ext_var, + name, + .. + } => Tag { + tag_union_var: variant_var, + ext_var, + name, + arguments: args, + }, + _ => { + // This could be something like ((if True then fn1 else fn2) arg1 arg2). + Call( + Box::new(( + var_store.fresh(), + fn_expr, + var_store.fresh(), + var_store.fresh(), + )), + args, + *application_style, + ) + } + }; + + (expr, output) + } + } + ast::Expr::Var { module_name, ident } => { + canonicalize_var_lookup(env, var_store, scope, module_name, ident, region) + } + ast::Expr::Underscore(name) => { + // we parse underscores, but they are not valid expression syntax + + let problem = roc_problem::can::RuntimeError::MalformedIdentifier( + (*name).into(), + if name.is_empty() { + roc_parse::ident::BadIdent::UnderscoreAlone(region.start()) + } else { + roc_parse::ident::BadIdent::UnderscoreAtStart { + position: region.start(), + // Check if there's an ignored identifier with this name in scope (for better error messages) + declaration_region: scope.lookup_ignored_local(name), + } + }, + region, + ); + + env.problem(Problem::RuntimeError(problem.clone())); + + (RuntimeError(problem), Output::default()) + } + ast::Expr::Crash => { + // Naked crashes aren't allowed; we'll admit this with our own message, but yield an + // error. + env.problem(Problem::UnappliedCrash { region }); + + ( + Crash { + msg: Box::new(Loc::at( + region, + Expr::Str(String::from("hit a crash!").into_boxed_str()), + )), + ret_var: var_store.fresh(), + }, + Output::default(), + ) + } + ast::Expr::Defs(loc_defs, loc_ret) => { + // The body expression gets a new scope for canonicalization, + scope.inner_scope(|inner_scope| { + let defs: Defs = (*loc_defs).clone(); + can_defs_with_return(env, var_store, inner_scope, env.arena.alloc(defs), loc_ret) + }) + } + ast::Expr::RecordBuilder(_) => { + internal_error!("RecordBuilder should have been desugared by now") + } + ast::Expr::Backpassing(_, _, _) => { + internal_error!("Backpassing should have been desugared by now") + } + ast::Expr::Closure(loc_arg_patterns, loc_body_expr) => { + let (closure_data, output) = + canonicalize_closure(env, var_store, scope, loc_arg_patterns, loc_body_expr, None); + + (Closure(closure_data), output) + } + ast::Expr::When(loc_cond, branches) => { + // Infer the condition expression's type. + let cond_var = var_store.fresh(); + let (can_cond, mut output) = + canonicalize_expr(env, var_store, scope, loc_cond.region, &loc_cond.value); + + // the condition can never be a tail-call + output.tail_call = None; + + let mut can_branches = Vec::with_capacity(branches.len()); + + for branch in branches.iter() { + let (can_when_branch, branch_references) = scope.inner_scope(|inner_scope| { + canonicalize_when_branch( + env, + var_store, + inner_scope, + region, + branch, + &mut output, + ) + }); + + output.references.union_mut(&branch_references); + + can_branches.push(can_when_branch); + } + + // A "when" with no branches is a runtime error, but it will mess things up + // if code gen mistakenly thinks this is a tail call just because its condition + // happened to be one. (The condition gave us our initial output value.) + if branches.is_empty() { + output.tail_call = None; + } + + // Incorporate all three expressions into a combined Output value. + let expr = When { + expr_var: var_store.fresh(), + cond_var, + region, + loc_cond: Box::new(can_cond), + branches: can_branches, + branches_cond_var: var_store.fresh(), + exhaustive: ExhaustiveMark::new(var_store), + }; + + (expr, output) + } + ast::Expr::RecordAccess(record_expr, field) => { + let (loc_expr, output) = canonicalize_expr(env, var_store, scope, region, record_expr); + + ( + RecordAccess { + record_var: var_store.fresh(), + field_var: var_store.fresh(), + ext_var: var_store.fresh(), + loc_expr: Box::new(loc_expr), + field: Lowercase::from(*field), + }, + output, + ) + } + ast::Expr::AccessorFunction(field) => ( + RecordAccessor(StructAccessorData { + name: scope.gen_unique_symbol(), + function_var: var_store.fresh(), + record_var: var_store.fresh(), + ext_var: var_store.fresh(), + closure_var: var_store.fresh(), + field_var: var_store.fresh(), + field: match field { + Accessor::RecordField(field) => IndexOrField::Field((*field).into()), + Accessor::TupleIndex(index) => IndexOrField::Index(index.parse().unwrap()), + }, + }), + Output::default(), + ), + ast::Expr::TupleAccess(tuple_expr, field) => { + let (loc_expr, output) = canonicalize_expr(env, var_store, scope, region, tuple_expr); + + ( + TupleAccess { + tuple_var: var_store.fresh(), + ext_var: var_store.fresh(), + elem_var: var_store.fresh(), + loc_expr: Box::new(loc_expr), + index: field.parse().unwrap(), + }, + output, + ) + } + ast::Expr::Tag(tag) => { + let variant_var = var_store.fresh(); + let ext_var = var_store.fresh(); + + let symbol = scope.gen_unique_symbol(); + + ( + ZeroArgumentTag { + name: TagName((*tag).into()), + variant_var, + closure_name: symbol, + ext_var, + }, + Output::default(), + ) + } + ast::Expr::OpaqueRef(name) => { + // If we're here, the opaque reference is definitely not wrapping an argument - wrapped + // arguments are handled in the Apply branch. + // Treat this as a function \payload -> @Opaque payload + match scope.lookup_opaque_ref(name, region) { + Err(runtime_error) => { + env.problem(Problem::RuntimeError(runtime_error.clone())); + (RuntimeError(runtime_error), Output::default()) + } + Ok((name, opaque_def)) => { + let mut output = Output::default(); + output.references.insert_type_lookup(name); + + let (type_arguments, lambda_set_variables, specialized_def_type) = + freshen_opaque_def(var_store, opaque_def); + + let fn_symbol = scope.gen_unique_symbol(); + + ( + OpaqueWrapFunction(OpaqueWrapFunctionData { + opaque_name: name, + opaque_var: var_store.fresh(), + specialized_def_type, + type_arguments, + lambda_set_variables, + + function_name: fn_symbol, + function_var: var_store.fresh(), + argument_var: var_store.fresh(), + closure_var: var_store.fresh(), + }), + output, + ) + } + } + } + ast::Expr::Expect(condition, continuation) => { + let mut output = Output::default(); + + let (loc_condition, output1) = + canonicalize_expr(env, var_store, scope, condition.region, &condition.value); + + // Get all the lookups that were referenced in the condition, + // so we can print their values later. + let lookups_in_cond = get_lookup_symbols(&loc_condition.value); + + let (loc_continuation, output2) = canonicalize_expr( + env, + var_store, + scope, + continuation.region, + &continuation.value, + ); + + output.union(output1); + output.union(output2); + + ( + Expect { + loc_condition: Box::new(loc_condition), + loc_continuation: Box::new(loc_continuation), + lookups_in_cond, + }, + output, + ) + } + ast::Expr::Dbg(_, _) => { + internal_error!("Dbg should have been desugared by now") + } + ast::Expr::LowLevelDbg(message, continuation) => { + let mut output = Output::default(); + + let (loc_message, output1) = + canonicalize_expr(env, var_store, scope, message.region, &message.value); + + let (loc_continuation, output2) = canonicalize_expr( + env, + var_store, + scope, + continuation.region, + &continuation.value, + ); + + output.union(output1); + output.union(output2); + + // the symbol is used to bind the message `x = message`, and identify this `dbg`. + // That would cause issues if we dbg a variable, like `dbg y`, because in the IR we + // cannot alias variables. Hence, we make the dbg use that same variable `y` + let symbol = match &loc_message.value { + Expr::Var(symbol, _) => *symbol, + _ => scope.gen_unique_symbol(), + }; + + ( + Dbg { + loc_message: Box::new(loc_message), + loc_continuation: Box::new(loc_continuation), + variable: var_store.fresh(), + symbol, + }, + output, + ) + } + ast::Expr::If(if_thens, final_else_branch) => { + let mut branches = Vec::with_capacity(if_thens.len()); + let mut output = Output::default(); + + for (condition, then_branch) in if_thens.iter() { + let (loc_cond, cond_output) = + canonicalize_expr(env, var_store, scope, condition.region, &condition.value); + + let (loc_then, then_output) = canonicalize_expr( + env, + var_store, + scope, + then_branch.region, + &then_branch.value, + ); + + branches.push((loc_cond, loc_then)); + + output.references.union_mut(&cond_output.references); + output.references.union_mut(&then_output.references); + } + + let (loc_else, else_output) = canonicalize_expr( + env, + var_store, + scope, + final_else_branch.region, + &final_else_branch.value, + ); + + output.references.union_mut(&else_output.references); + + ( + If { + cond_var: var_store.fresh(), + branch_var: var_store.fresh(), + branches, + final_else: Box::new(loc_else), + }, + output, + ) + } + + ast::Expr::PrecedenceConflict(ast::PrecedenceConflict { + whole_region, + binop1_position, + binop2_position, + binop1, + binop2, + expr: _, + }) => { + use roc_problem::can::RuntimeError::*; + + let region1 = Region::new( + *binop1_position, + binop1_position.bump_column(binop1.width() as u32), + ); + let loc_binop1 = Loc::at(region1, *binop1); + + let region2 = Region::new( + *binop2_position, + binop2_position.bump_column(binop2.width() as u32), + ); + let loc_binop2 = Loc::at(region2, *binop2); + + let problem = + PrecedenceProblem::BothNonAssociative(*whole_region, loc_binop1, loc_binop2); + + env.problem(Problem::PrecedenceProblem(problem.clone())); + + ( + RuntimeError(InvalidPrecedence(problem, region)), + Output::default(), + ) + } + ast::Expr::MalformedClosure => { + use roc_problem::can::RuntimeError::*; + (RuntimeError(MalformedClosure(region)), Output::default()) + } + ast::Expr::MalformedIdent(name, bad_ident) => { + use roc_problem::can::RuntimeError::*; + + let problem = MalformedIdentifier((*name).into(), *bad_ident, region); + env.problem(Problem::RuntimeError(problem.clone())); + + (RuntimeError(problem), Output::default()) + } + ast::Expr::MultipleRecordBuilders(sub_expr) => { + use roc_problem::can::RuntimeError::*; + + let problem = MultipleRecordBuilders(sub_expr.region); + env.problem(Problem::RuntimeError(problem.clone())); + + (RuntimeError(problem), Output::default()) + } + ast::Expr::UnappliedRecordBuilder(sub_expr) => { + use roc_problem::can::RuntimeError::*; + + let problem = UnappliedRecordBuilder(sub_expr.region); + env.problem(Problem::RuntimeError(problem.clone())); + + (RuntimeError(problem), Output::default()) + } + &ast::Expr::NonBase10Int { + string, + base, + is_negative, + } => { + // the minus sign is added before parsing, to get correct overflow/underflow behavior + let answer = match finish_parsing_base(string, base, is_negative) { + Ok((int, bound)) => { + // Done in this kinda round about way with intermediate variables + // to keep borrowed values around and make this compile + let int_string = int.to_string(); + let int_str = int_string.as_str(); + int_expr_from_result(var_store, Ok((int_str, int, bound)), region, base, env) + } + Err(e) => int_expr_from_result(var_store, Err(e), region, base, env), + }; + + (answer, Output::default()) + } + &ast::Expr::ParensAround(sub_expr) => { + let (loc_expr, output) = canonicalize_expr(env, var_store, scope, region, sub_expr); + + (loc_expr.value, output) + } + // Below this point, we shouln't see any of these nodes anymore because + // operator desugaring should have removed them! + bad_expr @ ast::Expr::SpaceBefore(_, _) => { + internal_error!( + "A SpaceBefore did not get removed during operator desugaring somehow: {:#?}", + bad_expr + ); + } + bad_expr @ ast::Expr::SpaceAfter(_, _) => { + internal_error!( + "A SpaceAfter did not get removed during operator desugaring somehow: {:#?}", + bad_expr + ); + } + bad_expr @ ast::Expr::BinOps { .. } => { + internal_error!( + "A binary operator chain did not get desugared somehow: {:#?}", + bad_expr + ); + } + bad_expr @ ast::Expr::UnaryOp(_, _) => { + internal_error!( + "A unary operator did not get desugared somehow: {:#?}", + bad_expr + ); + } + }; + + // At the end, diff used_idents and defined_idents to see which were unused. + // Add warnings for those! + + // In a later phase, unused top level declarations won't get monomorphized or code-genned. + // We aren't going to bother with DCE at the level of local defs. It's going to be + // a rounding error anyway (especially given that they'll be surfaced as warnings), LLVM will + // DCE them in optimized builds, and it's not worth the bookkeeping for dev builds. + ( + Loc { + region, + value: expr, + }, + output, + ) +} + +pub fn canonicalize_closure<'a>( + env: &mut Env<'a>, + var_store: &mut VarStore, + scope: &mut Scope, + loc_arg_patterns: &'a [Loc>], + loc_body_expr: &'a Loc>, + opt_def_name: Option, +) -> (ClosureData, Output) { + scope.inner_scope(|inner_scope| { + canonicalize_closure_body( + env, + var_store, + inner_scope, + loc_arg_patterns, + loc_body_expr, + opt_def_name, + ) + }) +} + +fn canonicalize_closure_body<'a>( + env: &mut Env<'a>, + var_store: &mut VarStore, + scope: &mut Scope, + loc_arg_patterns: &'a [Loc>], + loc_body_expr: &'a Loc>, + opt_def_name: Option, +) -> (ClosureData, Output) { + // The globally unique symbol that will refer to this closure once it gets converted + // into a top-level procedure for code gen. + let (symbol, is_anonymous) = match opt_def_name { + Some(name) => (name, false), + None => (scope.gen_unique_symbol(), true), + }; + + let mut can_args = Vec::with_capacity(loc_arg_patterns.len()); + let mut output = Output::default(); + + for loc_pattern in loc_arg_patterns.iter() { + let can_argument_pattern = canonicalize_pattern( + env, + var_store, + scope, + &mut output, + FunctionArg, + &loc_pattern.value, + loc_pattern.region, + PermitShadows(false), + ); + + can_args.push(( + var_store.fresh(), + AnnotatedMark::new(var_store), + can_argument_pattern, + )); + } + + let bound_by_argument_patterns: Vec<_> = + BindingsFromPattern::new_many(can_args.iter().map(|x| &x.2)).collect(); + + let (loc_body_expr, new_output) = canonicalize_expr( + env, + var_store, + scope, + loc_body_expr.region, + &loc_body_expr.value, + ); + + let mut captured_symbols: Vec<_> = new_output + .references + .value_lookups() + .copied() + // filter out the closure's name itself + .filter(|s| *s != symbol) + // symbols bound either in this pattern or deeper down are not captured! + .filter(|s| !new_output.references.bound_symbols().any(|x| x == s)) + .filter(|s| bound_by_argument_patterns.iter().all(|(k, _)| s != k)) + // filter out top-level symbols those will be globally available, and don't need to be captured + .filter(|s| !env.top_level_symbols.contains(s)) + // filter out imported symbols those will be globally available, and don't need to be captured + .filter(|s| s.module_id() == env.home) + // filter out functions that don't close over anything + .filter(|s| !new_output.non_closures.contains(s)) + .filter(|s| !output.non_closures.contains(s)) + .map(|s| (s, var_store.fresh())) + .collect(); + + output.union(new_output); + + // Now that we've collected all the references, check to see if any of the args we defined + // went unreferenced. If any did, report them as unused arguments. + for (sub_symbol, region) in bound_by_argument_patterns { + if !output.references.has_value_lookup(sub_symbol) { + // The body never referenced this argument we declared. It's an unused argument! + env.problem(Problem::UnusedArgument( + symbol, + is_anonymous, + sub_symbol, + region, + )); + } else { + // We shouldn't ultimately count arguments as referenced locals. Otherwise, + // we end up with weird conclusions like the expression (\x -> x + 1) + // references the (nonexistent) local variable x! + output.references.remove_value_lookup(&sub_symbol); + } + } + + // store the references of this function in the Env. This information is used + // when we canonicalize a surrounding def (if it exists) + env.closures.insert(symbol, output.references.clone()); + + // sort symbols, so we know the order in which they're stored in the closure record + captured_symbols.sort(); + + // store that this function doesn't capture anything. It will be promoted to a + // top-level function, and does not need to be captured by other surrounding functions. + if captured_symbols.is_empty() { + output.non_closures.insert(symbol); + } + + let closure_data = ClosureData { + function_type: var_store.fresh(), + closure_type: var_store.fresh(), + return_type: var_store.fresh(), + name: symbol, + captured_symbols, + recursive: Recursive::NotRecursive, + arguments: can_args, + loc_body: Box::new(loc_body_expr), + }; + + (closure_data, output) +} + +enum MultiPatternVariables { + OnePattern, + MultiPattern { + bound_occurrences: VecMap, + }, +} + +impl MultiPatternVariables { + #[inline(always)] + fn new(num_patterns: usize) -> Self { + if num_patterns > 1 { + Self::MultiPattern { + bound_occurrences: VecMap::with_capacity(2), + } + } else { + Self::OnePattern + } + } + + #[inline(always)] + fn add_pattern(&mut self, pattern: &Loc) { + match self { + MultiPatternVariables::OnePattern => {} + MultiPatternVariables::MultiPattern { bound_occurrences } => { + for (sym, region) in BindingsFromPattern::new(pattern) { + if !bound_occurrences.contains_key(&sym) { + bound_occurrences.insert(sym, (region, 0)); + } + bound_occurrences.get_mut(&sym).unwrap().1 += 1; + } + } + } + } + + #[inline(always)] + fn get_unbound(self) -> impl Iterator { + let bound_occurrences = match self { + MultiPatternVariables::OnePattern => Default::default(), + MultiPatternVariables::MultiPattern { bound_occurrences } => bound_occurrences, + }; + + bound_occurrences + .into_iter() + .filter_map(|(sym, (region, occurs))| { + if occurs == 1 { + Some((sym, region)) + } else { + None + } + }) + } +} + +#[inline(always)] +fn canonicalize_when_branch<'a>( + env: &mut Env<'a>, + var_store: &mut VarStore, + scope: &mut Scope, + _region: Region, + branch: &'a ast::WhenBranch<'a>, + output: &mut Output, +) -> (WhenBranch, References) { + let mut patterns = Vec::with_capacity(branch.patterns.len()); + let mut multi_pattern_variables = MultiPatternVariables::new(branch.patterns.len()); + + for (i, loc_pattern) in branch.patterns.iter().enumerate() { + let permit_shadows = PermitShadows(i > 0); // patterns can shadow symbols defined in the first pattern. + + let can_pattern = canonicalize_pattern( + env, + var_store, + scope, + output, + WhenBranch, + &loc_pattern.value, + loc_pattern.region, + permit_shadows, + ); + + multi_pattern_variables.add_pattern(&can_pattern); + patterns.push(WhenBranchPattern { + pattern: can_pattern, + degenerate: false, + }); + } + + let mut some_symbols_not_bound_in_all_patterns = false; + for (unbound_symbol, region) in multi_pattern_variables.get_unbound() { + env.problem(Problem::NotBoundInAllPatterns { + unbound_symbol, + region, + }); + + some_symbols_not_bound_in_all_patterns = true; + } + + let (value, mut branch_output) = canonicalize_expr( + env, + var_store, + scope, + branch.value.region, + &branch.value.value, + ); + + let guard = match &branch.guard { + None => None, + Some(loc_expr) => { + let (can_guard, guard_branch_output) = + canonicalize_expr(env, var_store, scope, loc_expr.region, &loc_expr.value); + + branch_output.union(guard_branch_output); + Some(can_guard) + } + }; + + let references = branch_output.references.clone(); + output.union(branch_output); + + // Now that we've collected all the references for this branch, check to see if + // any of the new idents it defined were unused. If any were, report it. + let mut pattern_bound_symbols_body_needs = VecSet::default(); + for (symbol, region) in BindingsFromPattern::new_many(patterns.iter().map(|pat| &pat.pattern)) { + if output.references.has_value_lookup(symbol) { + pattern_bound_symbols_body_needs.insert(symbol); + } else { + env.problem(Problem::UnusedBranchDef(symbol, region)); + } + } + + if some_symbols_not_bound_in_all_patterns && !pattern_bound_symbols_body_needs.is_empty() { + // There might be branches that don't bind all the symbols needed by the body; mark those + // branches degenerate. + for pattern in patterns.iter_mut() { + let bound_by_pattern: VecSet<_> = BindingsFromPattern::new(&pattern.pattern) + .map(|(sym, _)| sym) + .collect(); + + let binds_all_needed = pattern_bound_symbols_body_needs + .iter() + .all(|sym| bound_by_pattern.contains(sym)); + + if !binds_all_needed { + pattern.degenerate = true; + } + } + } + + ( + WhenBranch { + patterns, + value, + guard, + redundant: RedundantMark::new(var_store), + }, + references, + ) +} + +enum CanonicalizeRecordProblem { + InvalidOptionalValue { + field_name: Lowercase, + field_region: Region, + record_region: Region, + }, +} +fn canonicalize_fields<'a>( + env: &mut Env<'a>, + var_store: &mut VarStore, + scope: &mut Scope, + region: Region, + fields: &'a [Loc>>], +) -> Result<(SendMap, Output), CanonicalizeRecordProblem> { + let mut can_fields = SendMap::default(); + let mut output = Output::default(); + + for loc_field in fields.iter() { + match canonicalize_field(env, var_store, scope, &loc_field.value) { + Ok((label, field_expr, field_out, field_var)) => { + let field = Field { + var: field_var, + region: loc_field.region, + loc_expr: Box::new(field_expr), + }; + + let replaced = can_fields.insert(label.clone(), field); + + if let Some(old) = replaced { + env.problems.push(Problem::DuplicateRecordFieldValue { + field_name: label, + field_region: loc_field.region, + record_region: region, + replaced_region: old.region, + }); + } + + output.references.union_mut(&field_out.references); + } + Err(CanonicalizeFieldProblem::InvalidOptionalValue { + field_name, + field_region, + }) => { + env.problems.push(Problem::InvalidOptionalValue { + field_name: field_name.clone(), + field_region, + record_region: region, + }); + return Err(CanonicalizeRecordProblem::InvalidOptionalValue { + field_name, + field_region, + record_region: region, + }); + } + } + } + + Ok((can_fields, output)) +} + +enum CanonicalizeFieldProblem { + InvalidOptionalValue { + field_name: Lowercase, + field_region: Region, + }, +} +fn canonicalize_field<'a>( + env: &mut Env<'a>, + var_store: &mut VarStore, + scope: &mut Scope, + field: &'a ast::AssignedField<'a, ast::Expr<'a>>, +) -> Result<(Lowercase, Loc, Output, Variable), CanonicalizeFieldProblem> { + use roc_parse::ast::AssignedField::*; + + match field { + // Both a label and a value, e.g. `{ name: "blah" }` + RequiredValue(label, _, loc_expr) => { + let field_var = var_store.fresh(); + let (loc_can_expr, output) = + canonicalize_expr(env, var_store, scope, loc_expr.region, &loc_expr.value); + + Ok(( + Lowercase::from(label.value), + loc_can_expr, + output, + field_var, + )) + } + + OptionalValue(label, _, loc_expr) => Err(CanonicalizeFieldProblem::InvalidOptionalValue { + field_name: Lowercase::from(label.value), + field_region: Region::span_across(&label.region, &loc_expr.region), + }), + + // A label with no value, e.g. `{ name }` (this is sugar for { name: name }) + LabelOnly(_) => { + internal_error!("Somehow a LabelOnly record field was not desugared!"); + } + + SpaceBefore(sub_field, _) | SpaceAfter(sub_field, _) => { + canonicalize_field(env, var_store, scope, sub_field) + } + + Malformed(_string) => { + internal_error!("TODO canonicalize malformed record field"); + } + } +} + +fn canonicalize_var_lookup( + env: &mut Env<'_>, + var_store: &mut VarStore, + scope: &mut Scope, + module_name: &str, + ident: &str, + region: Region, +) -> (Expr, Output) { + use Expr::*; + + let mut output = Output::default(); + let can_expr = if module_name.is_empty() { + // Since module_name was empty, this is an unqualified var. + // Look it up in scope! + match scope.lookup_str(ident, region) { + Ok(symbol) => { + output.references.insert_value_lookup(symbol); + + if scope.abilities_store.is_ability_member_name(symbol) { + AbilityMember( + symbol, + Some(scope.abilities_store.fresh_specialization_id()), + var_store.fresh(), + ) + } else { + Var(symbol, var_store.fresh()) + } + } + Err(problem) => { + env.problem(Problem::RuntimeError(problem.clone())); + + RuntimeError(problem) + } + } + } else { + // Since module_name was nonempty, this is a qualified var. + // Look it up in the env! + match env.qualified_lookup(scope, module_name, ident, region) { + Ok(symbol) => { + output.references.insert_value_lookup(symbol); + + if scope.abilities_store.is_ability_member_name(symbol) { + AbilityMember( + symbol, + Some(scope.abilities_store.fresh_specialization_id()), + var_store.fresh(), + ) + } else { + Var(symbol, var_store.fresh()) + } + } + Err(problem) => { + // Either the module wasn't imported, or + // it was imported but it doesn't expose this ident. + env.problem(Problem::RuntimeError(problem.clone())); + + RuntimeError(problem) + } + } + }; + + // If it's valid, this ident should be in scope already. + + (can_expr, output) +} + +/// Currently uses the heuristic of "only inline if it's a builtin" +pub fn inline_calls(var_store: &mut VarStore, expr: Expr) -> Expr { + use Expr::*; + + match expr { + // Num stores the `a` variable in `Num a`. Not the same as the variable + // stored in Int and Float below, which is strictly for better error messages + other @ Num(..) + | other @ Int(..) + | other @ Float(..) + | other @ Str { .. } + | other @ IngestedFile(..) + | other @ SingleQuote(..) + | other @ RuntimeError(_) + | other @ EmptyRecord + | other @ RecordAccessor { .. } + | other @ RecordUpdate { .. } + | other @ Var(..) + | other @ AbilityMember(..) + | other @ RunLowLevel { .. } + | other @ TypedHole { .. } + | other @ ForeignCall { .. } + | other @ OpaqueWrapFunction(_) + | other @ Crash { .. } => other, + + List { + elem_var, + loc_elems, + } => { + let mut new_elems = Vec::with_capacity(loc_elems.len()); + + for loc_elem in loc_elems { + let value = inline_calls(var_store, loc_elem.value); + + new_elems.push(Loc { + value, + region: loc_elem.region, + }); + } + + List { + elem_var, + loc_elems: new_elems, + } + } + // Branching + When { + cond_var, + expr_var, + region, + loc_cond, + branches, + branches_cond_var, + exhaustive, + } => { + let loc_cond = Box::new(Loc { + region: loc_cond.region, + value: inline_calls(var_store, loc_cond.value), + }); + + let mut new_branches = Vec::with_capacity(branches.len()); + + for branch in branches { + let value = Loc { + value: inline_calls(var_store, branch.value.value), + region: branch.value.region, + }; + let guard = match branch.guard { + Some(loc_expr) => Some(Loc { + region: loc_expr.region, + value: inline_calls(var_store, loc_expr.value), + }), + None => None, + }; + let new_branch = WhenBranch { + patterns: branch.patterns, + value, + guard, + redundant: RedundantMark::new(var_store), + }; + + new_branches.push(new_branch); + } + + When { + cond_var, + expr_var, + region, + loc_cond, + branches: new_branches, + branches_cond_var, + exhaustive, + } + } + If { + cond_var, + branch_var, + branches, + final_else, + } => { + let mut new_branches = Vec::with_capacity(branches.len()); + + for (loc_cond, loc_expr) in branches { + let loc_cond = Loc { + value: inline_calls(var_store, loc_cond.value), + region: loc_cond.region, + }; + + let loc_expr = Loc { + value: inline_calls(var_store, loc_expr.value), + region: loc_expr.region, + }; + + new_branches.push((loc_cond, loc_expr)); + } + + let final_else = Box::new(Loc { + region: final_else.region, + value: inline_calls(var_store, final_else.value), + }); + + If { + cond_var, + branch_var, + branches: new_branches, + final_else, + } + } + + Expect { + loc_condition, + loc_continuation, + lookups_in_cond, + } => { + let loc_condition = Loc { + region: loc_condition.region, + value: inline_calls(var_store, loc_condition.value), + }; + + let loc_continuation = Loc { + region: loc_continuation.region, + value: inline_calls(var_store, loc_continuation.value), + }; + + Expect { + loc_condition: Box::new(loc_condition), + loc_continuation: Box::new(loc_continuation), + lookups_in_cond, + } + } + + ExpectFx { + loc_condition, + loc_continuation, + lookups_in_cond, + } => { + let loc_condition = Loc { + region: loc_condition.region, + value: inline_calls(var_store, loc_condition.value), + }; + + let loc_continuation = Loc { + region: loc_continuation.region, + value: inline_calls(var_store, loc_continuation.value), + }; + + ExpectFx { + loc_condition: Box::new(loc_condition), + loc_continuation: Box::new(loc_continuation), + lookups_in_cond, + } + } + + Dbg { + loc_message, + loc_continuation, + variable, + symbol, + } => { + let loc_message = Loc { + region: loc_message.region, + value: inline_calls(var_store, loc_message.value), + }; + + let loc_continuation = Loc { + region: loc_continuation.region, + value: inline_calls(var_store, loc_continuation.value), + }; + + Dbg { + loc_message: Box::new(loc_message), + loc_continuation: Box::new(loc_continuation), + variable, + symbol, + } + } + + LetRec(defs, loc_expr, mark) => { + let mut new_defs = Vec::with_capacity(defs.len()); + + for def in defs { + new_defs.push(Def { + loc_pattern: def.loc_pattern, + loc_expr: Loc { + region: def.loc_expr.region, + value: inline_calls(var_store, def.loc_expr.value), + }, + expr_var: def.expr_var, + pattern_vars: def.pattern_vars, + annotation: def.annotation, + }); + } + + let loc_expr = Loc { + region: loc_expr.region, + value: inline_calls(var_store, loc_expr.value), + }; + + LetRec(new_defs, Box::new(loc_expr), mark) + } + + LetNonRec(def, loc_expr) => { + let def = Def { + loc_pattern: def.loc_pattern, + loc_expr: Loc { + region: def.loc_expr.region, + value: inline_calls(var_store, def.loc_expr.value), + }, + expr_var: def.expr_var, + pattern_vars: def.pattern_vars, + annotation: def.annotation, + }; + + let loc_expr = Loc { + region: loc_expr.region, + value: inline_calls(var_store, loc_expr.value), + }; + + LetNonRec(Box::new(def), Box::new(loc_expr)) + } + + Closure(ClosureData { + function_type, + closure_type, + return_type, + recursive, + name, + captured_symbols, + arguments, + loc_body, + }) => { + let loc_expr = *loc_body; + let loc_expr = Loc { + value: inline_calls(var_store, loc_expr.value), + region: loc_expr.region, + }; + + Closure(ClosureData { + function_type, + closure_type, + return_type, + recursive, + name, + captured_symbols, + arguments, + loc_body: Box::new(loc_expr), + }) + } + + Record { record_var, fields } => { + todo!( + "Inlining for Record with record_var {:?} and fields {:?}", + record_var, + fields + ); + } + + RecordAccess { + record_var, + ext_var, + field_var, + loc_expr, + field, + } => { + todo!("Inlining for RecordAccess with record_var {:?}, ext_var {:?}, field_var {:?}, loc_expr {:?}, field {:?}", record_var, ext_var, field_var, loc_expr, field); + } + + Tuple { tuple_var, elems } => { + todo!( + "Inlining for Tuple with tuple_var {:?} and elems {:?}", + tuple_var, + elems + ); + } + + TupleAccess { + tuple_var, + ext_var, + elem_var, + loc_expr, + index, + } => { + todo!("Inlining for TupleAccess with tuple_var {:?}, ext_var {:?}, elem_var {:?}, loc_expr {:?}, index {:?}", tuple_var, ext_var, elem_var, loc_expr, index); + } + + Tag { + tag_union_var: variant_var, + ext_var, + name, + arguments, + } => { + todo!( + "Inlining for Tag with variant_var {:?}, ext_var {:?}, name {:?}, arguments {:?}", + variant_var, + ext_var, + name, + arguments + ); + } + + OpaqueRef { + opaque_var, + name, + argument, + specialized_def_type, + type_arguments, + lambda_set_variables, + } => { + let (var, loc_expr) = *argument; + let argument = Box::new(( + var, + loc_expr.map_owned(|expr| inline_calls(var_store, expr)), + )); + + OpaqueRef { + opaque_var, + name, + argument, + specialized_def_type, + type_arguments, + lambda_set_variables, + } + } + + ZeroArgumentTag { + closure_name, + variant_var, + ext_var, + name, + } => { + todo!( + "Inlining for ZeroArgumentTag with closure_name {:?}, variant_var {:?}, ext_var {:?}, name {:?}", + closure_name, + variant_var, + ext_var, + name, + ); + } + + Call(boxed_tuple, args, called_via) => { + let (fn_var, loc_expr, closure_var, expr_var) = *boxed_tuple; + + match loc_expr.value { + Var(symbol, _) if symbol.is_builtin() => { + match builtin_defs_map(symbol, var_store) { + Some(Def { + loc_expr: + Loc { + value: + Closure(ClosureData { + recursive, + arguments: params, + loc_body: boxed_body, + .. + }), + .. + }, + .. + }) => { + debug_assert_eq!(recursive, Recursive::NotRecursive); + + // Since this is a canonicalized Expr, we should have + // already detected any arity mismatches and replaced this + // with a RuntimeError if there was a mismatch. + debug_assert_eq!(params.len(), args.len()); + + // Start with the function's body as the answer. + let mut loc_answer = *boxed_body; + + // Wrap the body in one LetNonRec for each argument, + // such that at the end we have all the arguments in + // scope with the values the caller provided. + for ( + (_param_var, _exhaustive_mark, loc_pattern), + (expr_var, loc_expr), + ) in params.iter().cloned().zip(args.into_iter()).rev() + { + // TODO get the correct vars into here. + // Not sure if param_var should be involved. + let pattern_vars = SendMap::default(); + + let def = Def { + loc_pattern, + loc_expr, + expr_var, + pattern_vars, + annotation: None, + }; + + loc_answer = Loc { + region: Region::zero(), + value: LetNonRec(Box::new(def), Box::new(loc_answer)), + }; + } + + loc_answer.value + } + Some(_) => { + internal_error!("Tried to inline a non-function"); + } + None => { + internal_error!( + "Tried to inline a builtin that wasn't registered: {:?}", + symbol + ); + } + } + } + _ => { + // For now, we only inline calls to builtins. Leave this alone! + Call( + Box::new((fn_var, loc_expr, closure_var, expr_var)), + args, + called_via, + ) + } + } + } + } +} + +fn flatten_str_literal<'a>( + env: &mut Env<'a>, + var_store: &mut VarStore, + scope: &mut Scope, + literal: &StrLiteral<'a>, +) -> (Expr, Output) { + use ast::StrLiteral::*; + + match literal { + PlainLine(str_slice) => (Expr::Str((*str_slice).into()), Output::default()), + Line(segments) => flatten_str_lines(env, var_store, scope, &[segments]), + Block(lines) => flatten_str_lines(env, var_store, scope, lines), + } +} + +/// Comments, newlines, and nested interpolation are disallowed inside interpolation +pub fn is_valid_interpolation(expr: &ast::Expr<'_>) -> bool { + match expr { + // These definitely contain neither comments nor newlines, so they are valid + ast::Expr::Var { .. } + | ast::Expr::SingleQuote(_) + | ast::Expr::Str(StrLiteral::PlainLine(_)) + | ast::Expr::Float(_) + | ast::Expr::Num(_) + | ast::Expr::NonBase10Int { .. } + | ast::Expr::AccessorFunction(_) + | ast::Expr::Crash + | ast::Expr::Underscore(_) + | ast::Expr::MalformedIdent(_, _) + | ast::Expr::Tag(_) + | ast::Expr::OpaqueRef(_) + | ast::Expr::MalformedClosure => true, + // Newlines are disallowed inside interpolation, and these all require newlines + ast::Expr::Dbg(_, _) + | ast::Expr::LowLevelDbg(_, _) + | ast::Expr::Defs(_, _) + | ast::Expr::Expect(_, _) + | ast::Expr::When(_, _) + | ast::Expr::Backpassing(_, _, _) + | ast::Expr::IngestedFile(_, _) + | ast::Expr::SpaceBefore(_, _) + | ast::Expr::Str(StrLiteral::Block(_)) + | ast::Expr::SpaceAfter(_, _) => false, + // These can contain subexpressions, so we need to recursively check those + ast::Expr::Str(StrLiteral::Line(segments)) => { + segments.iter().all(|segment| match segment { + ast::StrSegment::EscapedChar(_) + | ast::StrSegment::Unicode(_) + | ast::StrSegment::Plaintext(_) => true, + // Disallow nested interpolation. Alternatively, we could allow it but require + // a comment above it apologizing to the next person who has to read the code. + ast::StrSegment::Interpolated(_) => false, + }) + } + ast::Expr::Record(fields) => fields.iter().all(|loc_field| match loc_field.value { + ast::AssignedField::RequiredValue(_label, loc_comments, loc_val) + | ast::AssignedField::OptionalValue(_label, loc_comments, loc_val) => { + loc_comments.is_empty() && is_valid_interpolation(&loc_val.value) + } + ast::AssignedField::Malformed(_) | ast::AssignedField::LabelOnly(_) => true, + ast::AssignedField::SpaceBefore(_, _) | ast::AssignedField::SpaceAfter(_, _) => false, + }), + ast::Expr::Tuple(fields) => fields + .iter() + .all(|loc_field| is_valid_interpolation(&loc_field.value)), + ast::Expr::MultipleRecordBuilders(loc_expr) + | ast::Expr::UnappliedRecordBuilder(loc_expr) + | ast::Expr::PrecedenceConflict(PrecedenceConflict { expr: loc_expr, .. }) + | ast::Expr::UnaryOp(loc_expr, _) + | ast::Expr::Closure(_, loc_expr) => is_valid_interpolation(&loc_expr.value), + ast::Expr::TupleAccess(sub_expr, _) + | ast::Expr::ParensAround(sub_expr) + | ast::Expr::RecordAccess(sub_expr, _) => is_valid_interpolation(sub_expr), + ast::Expr::Apply(loc_expr, args, _called_via) => { + is_valid_interpolation(&loc_expr.value) + && args + .iter() + .all(|loc_arg| is_valid_interpolation(&loc_arg.value)) + } + ast::Expr::BinOps(loc_exprs, loc_expr) => { + is_valid_interpolation(&loc_expr.value) + && loc_exprs + .iter() + .all(|(loc_expr, _binop)| is_valid_interpolation(&loc_expr.value)) + } + ast::Expr::If(branches, final_branch) => { + is_valid_interpolation(&final_branch.value) + && branches.iter().all(|(loc_before, loc_after)| { + is_valid_interpolation(&loc_before.value) + && is_valid_interpolation(&loc_after.value) + }) + } + ast::Expr::List(elems) => elems + .iter() + .all(|loc_expr| is_valid_interpolation(&loc_expr.value)), + ast::Expr::RecordUpdate { update, fields } => { + is_valid_interpolation(&update.value) + && fields.iter().all(|loc_field| match loc_field.value { + ast::AssignedField::RequiredValue(_label, loc_comments, loc_val) + | ast::AssignedField::OptionalValue(_label, loc_comments, loc_val) => { + loc_comments.is_empty() && is_valid_interpolation(&loc_val.value) + } + ast::AssignedField::Malformed(_) | ast::AssignedField::LabelOnly(_) => true, + ast::AssignedField::SpaceBefore(_, _) + | ast::AssignedField::SpaceAfter(_, _) => false, + }) + } + ast::Expr::RecordBuilder(fields) => fields.iter().all(|loc_field| match loc_field.value { + ast::RecordBuilderField::Value(_label, comments, loc_expr) => { + comments.is_empty() && is_valid_interpolation(&loc_expr.value) + } + ast::RecordBuilderField::ApplyValue( + _label, + comments_before, + comments_after, + loc_expr, + ) => { + comments_before.is_empty() + && comments_after.is_empty() + && is_valid_interpolation(&loc_expr.value) + } + ast::RecordBuilderField::Malformed(_) | ast::RecordBuilderField::LabelOnly(_) => true, + ast::RecordBuilderField::SpaceBefore(_, _) + | ast::RecordBuilderField::SpaceAfter(_, _) => false, + }), + } +} + +enum StrSegment { + Interpolation(Loc), + Plaintext(Box), +} + +fn flatten_str_lines<'a>( + env: &mut Env<'a>, + var_store: &mut VarStore, + scope: &mut Scope, + lines: &[&[ast::StrSegment<'a>]], +) -> (Expr, Output) { + use ast::StrSegment::*; + + let mut buf = String::new(); + let mut segments = Vec::new(); + let mut output = Output::default(); + + for line in lines { + for segment in line.iter() { + match segment { + Plaintext(string) => { + buf.push_str(string); + } + Unicode(loc_hex_digits) => match u32::from_str_radix(loc_hex_digits.value, 16) { + Ok(code_pt) => match char::from_u32(code_pt) { + Some(ch) => { + buf.push(ch); + } + None => { + env.problem(Problem::InvalidUnicodeCodePt(loc_hex_digits.region)); + + return ( + Expr::RuntimeError(RuntimeError::InvalidUnicodeCodePt( + loc_hex_digits.region, + )), + output, + ); + } + }, + Err(_) => { + env.problem(Problem::InvalidHexadecimal(loc_hex_digits.region)); + + return ( + Expr::RuntimeError(RuntimeError::InvalidHexadecimal( + loc_hex_digits.region, + )), + output, + ); + } + }, + Interpolated(loc_expr) => { + if is_valid_interpolation(loc_expr.value) { + // Interpolations desugar to Str.concat calls + output.references.insert_call(Symbol::STR_CONCAT); + + if !buf.is_empty() { + segments.push(StrSegment::Plaintext(buf.into())); + + buf = String::new(); + } + + let (loc_expr, new_output) = canonicalize_expr( + env, + var_store, + scope, + loc_expr.region, + loc_expr.value, + ); + + output.union(new_output); + + segments.push(StrSegment::Interpolation(loc_expr)); + } else { + env.problem(Problem::InvalidInterpolation(loc_expr.region)); + + return ( + Expr::RuntimeError(RuntimeError::InvalidInterpolation(loc_expr.region)), + output, + ); + } + } + EscapedChar(escaped) => buf.push(escaped.unescape()), + } + } + } + + if !buf.is_empty() { + segments.push(StrSegment::Plaintext(buf.into())); + } + + (desugar_str_segments(var_store, segments), output) +} + +/// Resolve string interpolations by desugaring a sequence of StrSegments +/// into nested calls to Str.concat +fn desugar_str_segments(var_store: &mut VarStore, segments: Vec) -> Expr { + use StrSegment::*; + + let mut iter = segments.into_iter().rev(); + let mut loc_expr = match iter.next() { + Some(Plaintext(string)) => Loc::at(Region::zero(), Expr::Str(string)), + Some(Interpolation(loc_expr)) => loc_expr, + None => { + // No segments? Empty string! + + Loc::at(Region::zero(), Expr::Str("".into())) + } + }; + + for seg in iter { + let loc_new_expr = match seg { + Plaintext(string) => Loc::at(Region::zero(), Expr::Str(string)), + Interpolation(loc_interpolated_expr) => loc_interpolated_expr, + }; + + let fn_expr = Loc::at( + Region::zero(), + Expr::Var(Symbol::STR_CONCAT, var_store.fresh()), + ); + let expr = Expr::Call( + Box::new(( + var_store.fresh(), + fn_expr, + var_store.fresh(), + var_store.fresh(), + )), + vec![ + (var_store.fresh(), loc_new_expr), + (var_store.fresh(), loc_expr), + ], + CalledVia::StringInterpolation, + ); + + loc_expr = Loc::at(Region::zero(), expr); + } + + loc_expr.value +} + +#[derive(Clone, Debug)] +pub struct Declarations { + pub declarations: Vec, + + /// same lengths as declarations; has a dummy value if not applicable + pub variables: Vec, + pub symbols: Vec>, + pub annotations: Vec>, + + // used for ability member specializatons. + pub specializes: VecMap, + + pub host_exposed_annotations: VecMap, + + pub function_bodies: Vec>, + pub expressions: Vec>, + pub destructs: Vec, +} + +impl Default for Declarations { + fn default() -> Self { + Self::new() + } +} + +impl Declarations { + pub fn new() -> Self { + Self::with_capacity(0) + } + pub fn with_capacity(capacity: usize) -> Self { + Self { + declarations: Vec::with_capacity(capacity), + variables: Vec::with_capacity(capacity), + symbols: Vec::with_capacity(capacity), + annotations: Vec::with_capacity(capacity), + host_exposed_annotations: VecMap::new(), + function_bodies: Vec::with_capacity(capacity), + expressions: Vec::with_capacity(capacity), + specializes: VecMap::default(), // number of specializations is probably low + destructs: Vec::new(), // number of destructs is probably low + } + } + + /// To store a recursive group in the vectors without nesting, we first push a "header" + /// here, then push the definitions that are part of that recursive group + pub fn push_recursive_group(&mut self, length: u16, cycle_mark: IllegalCycleMark) -> usize { + let index = self.declarations.len(); + + let tag = DeclarationTag::MutualRecursion { length, cycle_mark }; + self.declarations.push(tag); + + // dummy values + self.variables.push(Variable::NULL); + self.symbols.push(Loc::at_zero(Symbol::ATTR_ATTR)); + self.annotations.push(None); + self.expressions.push(Loc::at_zero(Expr::EmptyRecord)); + + index + } + + pub fn push_recursive_def( + &mut self, + symbol: Loc, + loc_closure_data: Loc, + expr_var: Variable, + annotation: Option, + host_annotation: Option<(Variable, Annotation)>, + specializes: Option, + ) -> usize { + let index = self.declarations.len(); + + let function_def = FunctionDef { + closure_type: loc_closure_data.value.closure_type, + return_type: loc_closure_data.value.return_type, + captured_symbols: loc_closure_data.value.captured_symbols, + arguments: loc_closure_data.value.arguments, + }; + + let loc_function_def = Loc::at(loc_closure_data.region, function_def); + + let function_def_index = Index::push_new(&mut self.function_bodies, loc_function_def); + + let tag = match loc_closure_data.value.recursive { + Recursive::NotRecursive | Recursive::Recursive => { + DeclarationTag::Recursive(function_def_index) + } + Recursive::TailRecursive => DeclarationTag::TailRecursive(function_def_index), + }; + + if let Some(annotation) = host_annotation { + self.host_exposed_annotations + .insert(self.declarations.len(), annotation); + } + + self.declarations.push(tag); + self.variables.push(expr_var); + self.symbols.push(symbol); + self.annotations.push(annotation); + + self.expressions.push(*loc_closure_data.value.loc_body); + + if let Some(specializes) = specializes { + self.specializes.insert(index, specializes); + } + + index + } + + pub fn push_function_def( + &mut self, + symbol: Loc, + loc_closure_data: Loc, + expr_var: Variable, + annotation: Option, + host_annotation: Option<(Variable, Annotation)>, + specializes: Option, + ) -> usize { + let index = self.declarations.len(); + + let function_def = FunctionDef { + closure_type: loc_closure_data.value.closure_type, + return_type: loc_closure_data.value.return_type, + captured_symbols: loc_closure_data.value.captured_symbols, + arguments: loc_closure_data.value.arguments, + }; + + let loc_function_def = Loc::at(loc_closure_data.region, function_def); + + let function_def_index = Index::push_new(&mut self.function_bodies, loc_function_def); + + if let Some(annotation) = host_annotation { + self.host_exposed_annotations + .insert(self.declarations.len(), annotation); + } + + self.declarations + .push(DeclarationTag::Function(function_def_index)); + self.variables.push(expr_var); + self.symbols.push(symbol); + self.annotations.push(annotation); + + self.expressions.push(*loc_closure_data.value.loc_body); + + if let Some(specializes) = specializes { + self.specializes.insert(index, specializes); + } + + index + } + + pub fn push_expect( + &mut self, + preceding_comment: Region, + name: Symbol, + loc_expr: Loc, + ) -> usize { + let index = self.declarations.len(); + + self.declarations.push(DeclarationTag::Expectation); + self.variables.push(Variable::BOOL); + self.symbols.push(Loc::at(preceding_comment, name)); + self.annotations.push(None); + + self.expressions.push(loc_expr); + + index + } + + pub fn push_expect_fx( + &mut self, + preceding_comment: Region, + name: Symbol, + loc_expr: Loc, + ) -> usize { + let index = self.declarations.len(); + + self.declarations.push(DeclarationTag::ExpectationFx); + self.variables.push(Variable::BOOL); + self.symbols.push(Loc::at(preceding_comment, name)); + self.annotations.push(None); + + self.expressions.push(loc_expr); + + index + } + + pub fn push_value_def( + &mut self, + symbol: Loc, + loc_expr: Loc, + expr_var: Variable, + annotation: Option, + host_annotation: Option<(Variable, Annotation)>, + specializes: Option, + ) -> usize { + let index = self.declarations.len(); + + if let Some(annotation) = host_annotation { + self.host_exposed_annotations + .insert(self.declarations.len(), annotation); + } + + self.declarations.push(DeclarationTag::Value); + self.variables.push(expr_var); + self.symbols.push(symbol); + self.annotations.push(annotation); + + self.expressions.push(loc_expr); + + if let Some(specializes) = specializes { + self.specializes.insert(index, specializes); + } + + index + } + + /// Any def with a weird pattern + pub fn push_destructure_def( + &mut self, + loc_pattern: Loc, + loc_expr: Loc, + expr_var: Variable, + annotation: Option, + pattern_vars: VecMap, + ) -> usize { + let index = self.declarations.len(); + + let destruct_def = DestructureDef { + loc_pattern, + pattern_vars, + }; + + let destructure_def_index = Index::push_new(&mut self.destructs, destruct_def); + + self.declarations + .push(DeclarationTag::Destructure(destructure_def_index)); + self.variables.push(expr_var); + self.symbols.push(Loc::at_zero(Symbol::ATTR_ATTR)); + self.annotations.push(annotation); + + self.expressions.push(loc_expr); + + index + } + + pub fn push_def(&mut self, def: Def) { + match def.loc_pattern.value { + Pattern::Identifier(symbol) => match def.loc_expr.value { + Expr::Closure(closure_data) => match closure_data.recursive { + Recursive::NotRecursive => { + self.push_function_def( + Loc::at(def.loc_pattern.region, symbol), + Loc::at(def.loc_expr.region, closure_data), + def.expr_var, + def.annotation, + None, + None, + ); + } + + Recursive::Recursive | Recursive::TailRecursive => { + self.push_recursive_def( + Loc::at(def.loc_pattern.region, symbol), + Loc::at(def.loc_expr.region, closure_data), + def.expr_var, + def.annotation, + None, + None, + ); + } + }, + _ => { + self.push_value_def( + Loc::at(def.loc_pattern.region, symbol), + def.loc_expr, + def.expr_var, + def.annotation, + None, + None, + ); + } + }, + _ => todo!(), + } + } + + pub fn update_builtin_def(&mut self, index: usize, def: Def) { + match def.loc_pattern.value { + Pattern::Identifier(s) => assert_eq!(s, self.symbols[index].value), + p => internal_error!("a builtin definition has a non-identifier pattern: {:?}", p), + } + + match def.loc_expr.value { + Expr::Closure(closure_data) => { + let function_def = FunctionDef { + closure_type: closure_data.closure_type, + return_type: closure_data.return_type, + captured_symbols: closure_data.captured_symbols, + arguments: closure_data.arguments, + }; + + let loc_function_def = Loc::at(def.loc_expr.region, function_def); + + let function_def_index = + Index::push_new(&mut self.function_bodies, loc_function_def); + + self.declarations[index] = DeclarationTag::Function(function_def_index); + self.expressions[index] = *closure_data.loc_body; + self.variables[index] = def.expr_var; + } + _ => { + self.declarations[index] = DeclarationTag::Value; + self.expressions[index] = def.loc_expr; + self.variables[index] = def.expr_var; + } + } + } + + pub fn len(&self) -> usize { + self.declarations.len() + } + + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + pub fn iter_top_down(&self) -> impl Iterator + '_ { + self.declarations.iter().scan(0, |state, e| { + let length_so_far = *state; + + *state += e.len(); + + Some((length_so_far, *e)) + }) + } + + pub fn iter_bottom_up(&self) -> impl Iterator + '_ { + self.declarations + .iter() + .rev() + .scan(self.declarations.len() - 1, |state, e| { + let length_so_far = *state; + + *state = length_so_far.saturating_sub(e.len()); + + Some((length_so_far, *e)) + }) + } + + pub fn expects(&self) -> ExpectCollector { + let mut collector = ExpectCollector { + expects: VecMap::default(), + dbgs: VecMap::default(), + }; + + let var = Variable::EMPTY_RECORD; + + for index in 0..self.len() { + use crate::expr::DeclarationTag::*; + + match self.declarations[index] { + Value | Function(_) | Recursive(_) | TailRecursive(_) | Destructure(_) => { + // def pattern has no default expressions, so skip + let loc_expr = &self.expressions[index]; + + collector.visit_expr(&loc_expr.value, loc_expr.region, var); + } + MutualRecursion { .. } => { + // the self of this group will be treaded individually by later iterations + } + Expectation => { + let loc_expr = + toplevel_expect_to_inline_expect_pure(self.expressions[index].clone()); + + collector.visit_expr(&loc_expr.value, loc_expr.region, var); + } + ExpectationFx => { + let loc_expr = + toplevel_expect_to_inline_expect_fx(self.expressions[index].clone()); + + collector.visit_expr(&loc_expr.value, loc_expr.region, var); + } + } + } + + collector + } +} + +roc_error_macros::assert_sizeof_default!(DeclarationTag, 8); + +#[derive(Clone, Copy, Debug)] +pub enum DeclarationTag { + Value, + Expectation, + ExpectationFx, + Function(Index>), + Recursive(Index>), + TailRecursive(Index>), + Destructure(Index), + MutualRecursion { + length: u16, + cycle_mark: IllegalCycleMark, + }, +} + +impl DeclarationTag { + fn len(self) -> usize { + use DeclarationTag::*; + + match self { + Function(_) | Recursive(_) | TailRecursive(_) => 1, + Value => 1, + Expectation | ExpectationFx => 1, + Destructure(_) => 1, + MutualRecursion { length, .. } => length as usize + 1, + } + } +} + +#[derive(Clone, Debug)] +pub struct FunctionDef { + pub closure_type: Variable, + pub return_type: Variable, + pub captured_symbols: Vec<(Symbol, Variable)>, + pub arguments: Vec<(Variable, AnnotatedMark, Loc)>, +} + +#[derive(Clone, Debug)] +pub struct DestructureDef { + pub loc_pattern: Loc, + pub pattern_vars: VecMap, +} + +pub(crate) fn get_lookup_symbols(expr: &Expr) -> Vec { + let mut stack: Vec<&Expr> = vec![expr]; + let mut lookups: Vec = Vec::new(); + + while let Some(expr) = stack.pop() { + match expr { + Expr::Var(symbol, var) + | Expr::RecordUpdate { + symbol, + record_var: var, + .. + } => { + // Don't introduce duplicates, or make unused variables + if !lookups.iter().any(|l| l.symbol == *symbol) { + lookups.push(ExpectLookup { + symbol: *symbol, + var: *var, + ability_info: None, + }); + } + } + Expr::AbilityMember(symbol, spec_id, var) => { + if !lookups.iter().any(|l| l.symbol == *symbol) { + lookups.push(ExpectLookup { + symbol: *symbol, + var: *var, + ability_info: *spec_id, + }); + } + } + Expr::List { loc_elems, .. } => { + stack.extend(loc_elems.iter().map(|loc_elem| &loc_elem.value)); + } + Expr::When { + loc_cond, branches, .. + } => { + stack.push(&loc_cond.value); + + stack.reserve(branches.len()); + + for branch in branches { + stack.push(&branch.value.value); + + if let Some(guard) = &branch.guard { + stack.push(&guard.value); + } + } + } + Expr::If { + branches, + final_else, + .. + } => { + stack.reserve(1 + branches.len() * 2); + + for (loc_cond, loc_body) in branches { + stack.push(&loc_cond.value); + stack.push(&loc_body.value); + } + + stack.push(&final_else.value); + } + Expr::LetRec(defs, expr, _illegal_cycle_mark) => { + for def in defs { + stack.push(&def.loc_expr.value); + } + stack.push(&expr.value); + } + Expr::LetNonRec(def, expr) => { + stack.push(&def.loc_expr.value); + stack.push(&expr.value); + } + Expr::Call(boxed_expr, args, _called_via) => { + stack.reserve(1 + args.len()); + + match &boxed_expr.1.value { + Expr::Var(_, _) => { + // do nothing + } + function_expr => { + // add the expr being called + stack.push(function_expr); + } + } + + for (_var, loc_arg) in args { + stack.push(&loc_arg.value); + } + } + Expr::Tag { arguments, .. } => { + stack.extend(arguments.iter().map(|(_var, loc_expr)| &loc_expr.value)); + } + Expr::RunLowLevel { args, .. } | Expr::ForeignCall { args, .. } => { + stack.extend(args.iter().map(|(_var, arg)| arg)); + } + Expr::OpaqueRef { argument, .. } => { + stack.push(&argument.1.value); + } + Expr::RecordAccess { loc_expr, .. } + | Expr::TupleAccess { loc_expr, .. } + | Expr::Closure(ClosureData { + loc_body: loc_expr, .. + }) => { + stack.push(&loc_expr.value); + } + Expr::Record { fields, .. } => { + stack.extend(fields.iter().map(|(_, field)| &field.loc_expr.value)); + } + Expr::Tuple { elems, .. } => { + stack.extend(elems.iter().map(|(_, elem)| &elem.value)); + } + Expr::Expect { + loc_continuation, .. + } + | Expr::ExpectFx { + loc_continuation, .. + } + | Expr::Dbg { + loc_continuation, .. + } => { + stack.push(&loc_continuation.value); + + // Intentionally ignore the lookups in the nested `expect` condition itself, + // because they couldn't possibly influence the outcome of this `expect`! + } + Expr::Crash { msg, .. } => stack.push(&msg.value), + Expr::Num(_, _, _, _) + | Expr::Float(_, _, _, _, _) + | Expr::Int(_, _, _, _, _) + | Expr::Str(_) + | Expr::IngestedFile(..) + | Expr::ZeroArgumentTag { .. } + | Expr::RecordAccessor(_) + | Expr::SingleQuote(..) + | Expr::EmptyRecord + | Expr::TypedHole(_) + | Expr::RuntimeError(_) + | Expr::OpaqueWrapFunction(_) => {} + } + } + + lookups +} + +/// Here we transform +/// +/// ```ignore +/// expect +/// a = 1 +/// b = 2 +/// +/// a == b +/// ``` +/// +/// into +/// +/// ```ignore +/// a = 1 +/// b = 2 +/// +/// expect a == b +/// +/// {} +/// ``` +/// +/// This is supposed to happen just before monomorphization: +/// all type errors and such are generated from the user source, +/// but this transformation means that we don't need special codegen for toplevel expects +pub fn toplevel_expect_to_inline_expect_pure(loc_expr: Loc) -> Loc { + toplevel_expect_to_inline_expect_help(loc_expr, false) +} + +pub fn toplevel_expect_to_inline_expect_fx(loc_expr: Loc) -> Loc { + toplevel_expect_to_inline_expect_help(loc_expr, true) +} + +fn toplevel_expect_to_inline_expect_help(mut loc_expr: Loc, has_effects: bool) -> Loc { + enum StoredDef { + NonRecursive(Region, Box), + Recursive(Region, Vec, IllegalCycleMark), + } + + let mut stack = vec![]; + let mut lookups_in_cond = vec![]; + + loop { + match loc_expr.value { + Expr::LetNonRec(boxed_def, remainder) => { + lookups_in_cond.extend(boxed_def.pattern_vars.iter().map(|(a, b)| ExpectLookup { + symbol: *a, + var: *b, + ability_info: None, + })); + + stack.push(StoredDef::NonRecursive(loc_expr.region, boxed_def)); + loc_expr = *remainder; + } + Expr::LetRec(defs, remainder, mark) => { + for def in &defs { + lookups_in_cond.extend(def.pattern_vars.iter().map(|(a, b)| ExpectLookup { + symbol: *a, + var: *b, + ability_info: None, + })); + } + + stack.push(StoredDef::Recursive(loc_expr.region, defs, mark)); + loc_expr = *remainder; + } + _ => break, + } + } + + let expect_region = loc_expr.region; + let expect = if has_effects { + Expr::ExpectFx { + loc_condition: Box::new(loc_expr), + loc_continuation: Box::new(Loc::at_zero(Expr::EmptyRecord)), + lookups_in_cond, + } + } else { + Expr::Expect { + loc_condition: Box::new(loc_expr), + loc_continuation: Box::new(Loc::at_zero(Expr::EmptyRecord)), + lookups_in_cond, + } + }; + + let mut loc_expr = Loc::at(expect_region, expect); + + for stored in stack.into_iter().rev() { + match stored { + StoredDef::NonRecursive(region, boxed_def) => { + loc_expr = Loc::at(region, Expr::LetNonRec(boxed_def, Box::new(loc_expr))); + } + StoredDef::Recursive(region, defs, illegal_cycle_mark) => { + let let_rec = Expr::LetRec(defs, Box::new(loc_expr), illegal_cycle_mark); + loc_expr = Loc::at(region, let_rec); + } + } + } + + loc_expr +} + +pub struct ExpectCollector { + pub expects: VecMap>, + pub dbgs: VecMap, +} + +impl crate::traverse::Visitor for ExpectCollector { + fn visit_expr(&mut self, expr: &Expr, _region: Region, var: Variable) { + match expr { + Expr::Expect { + lookups_in_cond, + loc_condition, + .. + } + | Expr::ExpectFx { + lookups_in_cond, + loc_condition, + .. + } => { + self.expects + .insert(loc_condition.region, lookups_in_cond.to_vec()); + } + Expr::Dbg { + loc_message, + variable, + symbol, + .. + } => { + let lookup = DbgLookup { + symbol: *symbol, + var: *variable, + region: loc_message.region, + ability_info: None, + }; + + self.dbgs.insert(*symbol, lookup); + } + _ => (), + } + + walk_expr(self, expr, var) + } +} diff --git a/crates/compiler/can/src/lib.rs b/crates/compiler/can/src/lib.rs new file mode 100644 index 0000000000..22d16ef3ef --- /dev/null +++ b/crates/compiler/can/src/lib.rs @@ -0,0 +1,32 @@ +//! [Canonicalize](https://en.wikipedia.org/wiki/Canonicalization) a roc +//! [abstract syntax tree](https://en.wikipedia.org/wiki/Abstract_syntax_tree), +//! [resolving symbols](https://stackoverflow.com/a/1175493/4200103), +//! [re-ordering definitions](https://www.oreilly.com/library/view/c-high-performance/9781787120952/546b5677-9157-4333-bc90-16db696436ac.xhtml), +//! and preparing a module for [type inference](https://en.wikipedia.org/wiki/Type_inference). +#![warn(clippy::dbg_macro)] +// See github.com/roc-lang/roc/issues/800 for discussion of the large_enum_variant check. +#![allow(clippy::large_enum_variant)] +pub mod abilities; +pub mod annotation; +pub mod builtins; +pub mod constraint; +pub mod copy; +pub mod def; +mod derive; +pub mod effect_module; +pub mod env; +pub mod exhaustive; +pub mod expected; +pub mod expr; +pub mod module; +pub mod num; +pub mod operator; +pub mod pattern; +pub mod procedure; +pub mod scope; +pub mod string; +pub mod traverse; + +pub use derive::DERIVED_REGION; + +pub mod debug; diff --git a/crates/compiler/can/src/module.rs b/crates/compiler/can/src/module.rs new file mode 100644 index 0000000000..729d53c4aa --- /dev/null +++ b/crates/compiler/can/src/module.rs @@ -0,0 +1,1294 @@ +use crate::abilities::{AbilitiesStore, ImplKey, PendingAbilitiesStore, ResolvedImpl}; +use crate::annotation::{canonicalize_annotation, AnnotationFor}; +use crate::def::{canonicalize_defs, Def}; +use crate::effect_module::HostedGeneratedFunctions; +use crate::env::Env; +use crate::expr::{ + ClosureData, DbgLookup, Declarations, ExpectLookup, Expr, Output, PendingDerives, +}; +use crate::pattern::{BindingsFromPattern, Pattern}; +use crate::scope::Scope; +use bumpalo::Bump; +use roc_collections::{MutMap, SendMap, VecMap, VecSet}; +use roc_error_macros::internal_error; +use roc_module::ident::Ident; +use roc_module::ident::Lowercase; +use roc_module::symbol::{IdentIds, IdentIdsByModule, ModuleId, ModuleIds, Symbol}; +use roc_parse::ast::{Defs, TypeAnnotation}; +use roc_parse::header::HeaderType; +use roc_parse::pattern::PatternType; +use roc_problem::can::{Problem, RuntimeError}; +use roc_region::all::{Loc, Region}; +use roc_types::subs::{ExposedTypesStorageSubs, Subs, VarStore, Variable}; +use roc_types::types::{AbilitySet, Alias, AliasKind, AliasVar, Type}; + +/// The types of all exposed values/functions of a collection of modules +#[derive(Clone, Debug, Default)] +pub struct ExposedByModule { + exposed: MutMap, +} + +impl ExposedByModule { + pub fn insert(&mut self, module_id: ModuleId, exposed: ExposedModuleTypes) { + self.exposed.insert(module_id, exposed); + } + + pub fn get(&self, module_id: &ModuleId) -> Option<&ExposedModuleTypes> { + self.exposed.get(module_id) + } + + /// Convenient when you need mutable access to the StorageSubs in the ExposedModuleTypes + pub fn get_mut(&mut self, module_id: &ModuleId) -> Option<&mut ExposedModuleTypes> { + self.exposed.get_mut(module_id) + } + + /// Create a clone of `self` that has just a subset of the modules + /// + /// Useful when we know what modules a particular module imports, and want just + /// the exposed types for those exposed modules. + pub fn retain_modules<'a>(&self, it: impl Iterator) -> Self { + let mut output = Self::default(); + + for module_id in it { + match self.exposed.get(module_id) { + None => { + internal_error!("Module {:?} did not register its exposed values", module_id) + } + Some(exposed_types) => { + output.exposed.insert(*module_id, exposed_types.clone()); + } + } + } + + output + } + + pub fn iter_all(&self) -> impl Iterator { + self.exposed.iter() + } + + /// # Safety + /// + /// May only be called when the exposed types of a modules are no longer needed, or may be + /// transitioned into another context. + pub unsafe fn remove(&mut self, module_id: &ModuleId) -> Option { + self.exposed.remove(module_id) + } +} + +#[derive(Clone, Debug, Default)] +pub struct ExposedForModule { + pub exposed_by_module: ExposedByModule, + pub imported_values: Vec, +} + +impl ExposedForModule { + pub fn new<'a>( + it: impl Iterator, + exposed_by_module: ExposedByModule, + ) -> Self { + let mut imported_values = Vec::new(); + + for symbol in it { + let module = exposed_by_module.exposed.get(&symbol.module_id()); + if let Some(ExposedModuleTypes { .. }) = module { + imported_values.push(*symbol); + } else { + continue; + } + } + + Self { + imported_values, + exposed_by_module, + } + } +} + +/// During type solving and monomorphization, a module must know how its imported ability +/// implementations are resolved - are they derived, or have a concrete implementation? +/// +/// Unfortunately we cannot keep this information opaque, as it's important for properly +/// restoring specialization lambda sets. As such, we need to export implementation information, +/// which is the job of this structure. +pub type ResolvedImplementations = VecMap; + +/// The types of all exposed values/functions of a module. This includes ability member +/// specializations. +#[derive(Clone, Debug)] +pub struct ExposedModuleTypes { + pub exposed_types_storage_subs: ExposedTypesStorageSubs, + pub resolved_implementations: ResolvedImplementations, +} + +#[derive(Debug)] +pub struct Module { + pub module_id: ModuleId, + pub exposed_imports: MutMap, + pub exposed_symbols: VecSet, + pub referenced_values: VecSet, + pub referenced_types: VecSet, + /// all aliases. `bool` indicates whether it is exposed + pub aliases: MutMap, + pub rigid_variables: RigidVariables, + pub abilities_store: PendingAbilitiesStore, + pub loc_expects: VecMap>, + pub loc_dbgs: VecMap, +} + +#[derive(Debug, Default)] +pub struct RigidVariables { + pub named: MutMap, + pub able: MutMap, + pub wildcards: VecSet, +} + +#[derive(Debug)] +pub struct ModuleOutput { + pub aliases: MutMap, + pub rigid_variables: RigidVariables, + pub declarations: Declarations, + pub exposed_imports: MutMap, + pub exposed_symbols: VecSet, + pub problems: Vec, + pub referenced_values: VecSet, + pub referenced_types: VecSet, + pub symbols_from_requires: Vec<(Loc, Loc)>, + pub pending_derives: PendingDerives, + pub scope: Scope, + pub loc_expects: VecMap>, + pub loc_dbgs: VecMap, +} + +fn validate_generate_with<'a>( + generate_with: &'a [Loc>], +) -> (HostedGeneratedFunctions, Vec>) { + let mut functions = HostedGeneratedFunctions::default(); + let mut unknown = Vec::new(); + + for generated in generate_with { + match generated.value.as_str() { + "after" => functions.after = true, + "map" => functions.map = true, + "always" => functions.always = true, + "loop" => functions.loop_ = true, + "forever" => functions.forever = true, + other => { + // we don't know how to generate this function + let ident = Ident::from(other); + unknown.push(Loc::at(generated.region, ident)); + } + } + } + + (functions, unknown) +} + +#[derive(Debug)] +enum GeneratedInfo { + Hosted { + effect_symbol: Symbol, + generated_functions: HostedGeneratedFunctions, + }, + Builtin, + NotSpecial, +} + +impl GeneratedInfo { + fn from_header_type( + env: &mut Env, + scope: &mut Scope, + var_store: &mut VarStore, + header_type: &HeaderType, + ) -> Self { + match header_type { + HeaderType::Hosted { + generates, + generates_with, + name: _, + exposes: _, + } => { + let name: &str = generates.into(); + let (generated_functions, unknown_generated) = + validate_generate_with(generates_with); + + for unknown in unknown_generated { + env.problem(Problem::UnknownGeneratesWith(unknown)); + } + + let effect_symbol = scope.introduce(name.into(), Region::zero()).unwrap(); + + { + let a_var = var_store.fresh(); + + let actual = + crate::effect_module::build_effect_actual(Type::Variable(a_var), var_store); + + scope.add_alias( + effect_symbol, + Region::zero(), + vec![Loc::at_zero(AliasVar::unbound("a".into(), a_var))], + vec![], + actual, + AliasKind::Opaque, + ); + } + + GeneratedInfo::Hosted { + effect_symbol, + generated_functions, + } + } + HeaderType::Builtin { + generates_with, + name: _, + exposes: _, + } => { + debug_assert!(generates_with.is_empty()); + GeneratedInfo::Builtin + } + _ => GeneratedInfo::NotSpecial, + } + } +} + +fn has_no_implementation(expr: &Expr) -> bool { + match expr { + Expr::RuntimeError(RuntimeError::NoImplementationNamed { .. }) => true, + Expr::Closure(closure_data) + if matches!( + closure_data.loc_body.value, + Expr::RuntimeError(RuntimeError::NoImplementationNamed { .. }) + ) => + { + true + } + + _ => false, + } +} + +// TODO trim these down +#[allow(clippy::too_many_arguments)] +pub fn canonicalize_module_defs<'a>( + arena: &'a Bump, + loc_defs: &'a mut Defs<'a>, + header_type: &roc_parse::header::HeaderType, + home: ModuleId, + module_ids: &'a ModuleIds, + exposed_ident_ids: IdentIds, + dep_idents: &'a IdentIdsByModule, + aliases: MutMap, + imported_abilities_state: PendingAbilitiesStore, + exposed_imports: MutMap, + exposed_symbols: VecSet, + symbols_from_requires: &[(Loc, Loc>)], + var_store: &mut VarStore, +) -> ModuleOutput { + let mut can_exposed_imports = MutMap::default(); + let mut scope = Scope::new(home, exposed_ident_ids, imported_abilities_state); + let mut env = Env::new(arena, home, dep_idents, module_ids); + + for (name, alias) in aliases.into_iter() { + scope.add_alias( + name, + alias.region, + alias.type_variables, + alias.infer_ext_in_output_variables, + alias.typ, + alias.kind, + ); + } + + let generated_info = + GeneratedInfo::from_header_type(&mut env, &mut scope, var_store, header_type); + + // Desugar operators (convert them to Apply calls, taking into account + // operator precedence and associativity rules), before doing other canonicalization. + // + // If we did this *during* canonicalization, then each time we + // visited a BinOp node we'd recursively try to apply this to each of its nested + // operators, and then again on *their* nested operators, ultimately applying the + // rules multiple times unnecessarily. + crate::operator::desugar_defs(arena, loc_defs); + + let mut rigid_variables = RigidVariables::default(); + + // Exposed values are treated like defs that appear before any others, e.g. + // + // imports [Foo.{ bar, baz }] + // + // ...is basically the same as if we'd added these extra defs at the start of the module: + // + // bar = Foo.bar + // baz = Foo.baz + // + // Here we essentially add those "defs" to "the beginning of the module" + // by canonicalizing them right before we canonicalize the actual ast::Def nodes. + for (ident, (symbol, region)) in exposed_imports { + let first_char = ident.as_inline_str().as_str().chars().next().unwrap(); + + if first_char.is_lowercase() { + match scope.import(ident, symbol, region) { + Ok(()) => { + // Add an entry to exposed_imports using the current module's name + // as the key; e.g. if this is the Foo module and we have + // exposes [Bar.{ baz }] then insert Foo.baz as the key, so when + // anything references `baz` in this Foo module, it will resolve to Bar.baz. + can_exposed_imports.insert(symbol, region); + } + Err((_shadowed_symbol, _region)) => { + internal_error!("TODO gracefully handle shadowing in imports.") + } + } + } else { + // This is a type alias or ability + + // the symbol should already be added to the scope when this module is canonicalized + debug_assert!( + scope.contains_alias(symbol) || scope.abilities_store.is_ability(symbol), + "The {symbol:?} is not a type alias or ability known in {home:?}" + ); + + // but now we know this symbol by a different identifier, so we still need to add it to + // the scope + match scope.import(ident, symbol, region) { + Ok(()) => { + // here we do nothing special + } + Err((shadowed_symbol, _region)) => { + internal_error!( + "TODO gracefully handle shadowing in imports, {:?} is shadowed.", + shadowed_symbol + ) + } + } + } + } + + let (defs, output, symbols_introduced) = canonicalize_defs( + &mut env, + Output::default(), + var_store, + &mut scope, + loc_defs, + PatternType::TopLevelDef, + ); + + let pending_derives = output.pending_derives; + + // See if any of the new idents we defined went unused. + // If any were unused and also not exposed, report it. + // + // We'll catch symbols that are only referenced due to (mutual) recursion later, + // when sorting the defs. + for (symbol, region) in symbols_introduced { + if !output.references.has_type_or_value_lookup(symbol) + && !exposed_symbols.contains(&symbol) + && !scope.abilities_store.is_specialization_name(symbol) + && !symbol.is_exposed_for_builtin_derivers() + { + env.problem(Problem::UnusedDef(symbol, region)); + } + } + + for named in output.introduced_variables.named { + rigid_variables.named.insert(named.variable, named.name); + } + + for able in output.introduced_variables.able { + rigid_variables + .able + .insert(able.variable, (able.name, able.abilities)); + } + + for var in output.introduced_variables.wildcards { + rigid_variables.wildcards.insert(var.value); + } + + let mut referenced_values = VecSet::default(); + let mut referenced_types = VecSet::default(); + + // Gather up all the symbols that were referenced across all the defs' lookups. + referenced_values.extend(output.references.value_lookups().copied()); + referenced_types.extend(output.references.type_lookups().copied()); + + // Gather up all the symbols that were referenced across all the defs' calls. + referenced_values.extend(output.references.calls().copied()); + + // Gather up all the symbols that were referenced from other modules. + referenced_values.extend(env.qualified_value_lookups.iter().copied()); + referenced_types.extend(env.qualified_type_lookups.iter().copied()); + + // NOTE previously we inserted builtin defs into the list of defs here + // this is now done later, in file.rs. + + // assume all exposed symbols are not actually defined in the module + // then as we walk the module and encounter the definitions, remove + // symbols from this set + let mut exposed_but_not_defined = exposed_symbols.clone(); + + let new_output = Output { + aliases: output.aliases, + ..Default::default() + }; + + let (mut declarations, mut output) = crate::def::sort_can_defs_new( + &mut env, + &mut scope, + var_store, + defs, + new_output, + &exposed_symbols, + ); + + debug_assert!( + output.pending_derives.is_empty(), + "I thought pending derives are only found during def introduction" + ); + + let symbols_from_requires = symbols_from_requires + .iter() + .map(|(symbol, loc_ann)| { + // We've already canonicalized the module, so there are no pending abilities. + let pending_abilities_in_scope = &Default::default(); + + let ann = canonicalize_annotation( + &mut env, + &mut scope, + &loc_ann.value, + loc_ann.region, + var_store, + pending_abilities_in_scope, + AnnotationFor::Value, + ); + + ann.add_to( + &mut output.aliases, + &mut output.references, + &mut output.introduced_variables, + ); + + ( + *symbol, + Loc { + value: ann.typ, + region: loc_ann.region, + }, + ) + }) + .collect(); + + if let GeneratedInfo::Hosted { + effect_symbol, + generated_functions, + } = generated_info + { + let mut exposed_symbols = VecSet::default(); + + // NOTE this currently builds all functions, not just the ones that the user requested + crate::effect_module::build_effect_builtins( + &mut scope, + effect_symbol, + var_store, + &mut exposed_symbols, + &mut declarations, + generated_functions, + ); + } + + for index in 0..declarations.len() { + use crate::expr::DeclarationTag::*; + + let tag = declarations.declarations[index]; + + match tag { + Value => { + let symbol = &declarations.symbols[index].value; + + // Remove this from exposed_symbols, + // so that at the end of the process, + // we can see if there were any + // exposed symbols which did not have + // corresponding defs. + exposed_but_not_defined.remove(symbol); + + // Temporary hack: we don't know exactly what symbols are hosted symbols, + // and which are meant to be normal definitions without a body. So for now + // we just assume they are hosted functions (meant to be provided by the platform) + if has_no_implementation(&declarations.expressions[index].value) { + match generated_info { + GeneratedInfo::Builtin => { + match crate::builtins::builtin_defs_map(*symbol, var_store) { + None => { + internal_error!("A builtin module contains a signature without implementation for {:?}", symbol) + } + Some(replacement_def) => { + declarations.update_builtin_def(index, replacement_def); + } + } + } + GeneratedInfo::Hosted { effect_symbol, .. } => { + let ident_id = symbol.ident_id(); + let ident = scope + .locals + .ident_ids + .get_name(ident_id) + .unwrap() + .to_string(); + + let def_annotation = declarations.annotations[index].clone().unwrap(); + + let annotation = crate::annotation::Annotation { + typ: def_annotation.signature, + introduced_variables: def_annotation.introduced_variables, + references: Default::default(), + aliases: Default::default(), + }; + + let hosted_def = crate::effect_module::build_host_exposed_def( + &mut scope, + *symbol, + &ident, + effect_symbol, + var_store, + annotation, + ); + + declarations.update_builtin_def(index, hosted_def); + } + _ => (), + } + } + } + Function(_) | Recursive(_) | TailRecursive(_) => { + let symbol = &declarations.symbols[index].value; + + // Remove this from exposed_symbols, + // so that at the end of the process, + // we can see if there were any + // exposed symbols which did not have + // corresponding defs. + exposed_but_not_defined.remove(symbol); + + // Temporary hack: we don't know exactly what symbols are hosted symbols, + // and which are meant to be normal definitions without a body. So for now + // we just assume they are hosted functions (meant to be provided by the platform) + if has_no_implementation(&declarations.expressions[index].value) { + match generated_info { + GeneratedInfo::Builtin => { + match crate::builtins::builtin_defs_map(*symbol, var_store) { + None => { + internal_error!("A builtin module contains a signature without implementation for {:?}", symbol) + } + Some(replacement_def) => { + declarations.update_builtin_def(index, replacement_def); + } + } + } + GeneratedInfo::Hosted { effect_symbol, .. } => { + let ident_id = symbol.ident_id(); + let ident = scope + .locals + .ident_ids + .get_name(ident_id) + .unwrap() + .to_string(); + + let def_annotation = declarations.annotations[index].clone().unwrap(); + + let annotation = crate::annotation::Annotation { + typ: def_annotation.signature, + introduced_variables: def_annotation.introduced_variables, + references: Default::default(), + aliases: Default::default(), + }; + + let hosted_def = crate::effect_module::build_host_exposed_def( + &mut scope, + *symbol, + &ident, + effect_symbol, + var_store, + annotation, + ); + + declarations.update_builtin_def(index, hosted_def); + } + _ => (), + } + } + } + Destructure(d_index) => { + let destruct_def = &declarations.destructs[d_index.index()]; + + for (symbol, _) in BindingsFromPattern::new(&destruct_def.loc_pattern) { + exposed_but_not_defined.remove(&symbol); + } + } + MutualRecursion { .. } => { + // the declarations of this group will be treaded individually by later iterations + } + Expectation => { /* ignore */ } + ExpectationFx => { /* ignore */ } + } + } + + let mut aliases = MutMap::default(); + + if let GeneratedInfo::Hosted { effect_symbol, .. } = generated_info { + // Remove this from exposed_symbols, + // so that at the end of the process, + // we can see if there were any + // exposed symbols which did not have + // corresponding defs. + exposed_but_not_defined.remove(&effect_symbol); + + let hosted_alias = scope.lookup_alias(effect_symbol).unwrap().clone(); + aliases.insert(effect_symbol, hosted_alias); + } + + for (symbol, alias) in output.aliases { + // Remove this from exposed_symbols, + // so that at the end of the process, + // we can see if there were any + // exposed symbols which did not have + // corresponding defs. + exposed_but_not_defined.remove(&symbol); + + aliases.insert(symbol, alias); + } + + for (ability, members) in scope + .abilities_store + .iter_abilities() + .filter(|(ab, _)| ab.module_id() == home) + { + exposed_but_not_defined.remove(&ability); + members.iter().for_each(|member| { + debug_assert!(member.module_id() == home); + exposed_but_not_defined.remove(member); + }); + } + + // By this point, all exposed symbols should have been removed from + // exposed_symbols and added to exposed_vars_by_symbol. If any were + // not, that means they were declared as exposed but there was + // no actual declaration with that name! + for symbol in exposed_but_not_defined { + env.problem(Problem::ExposedButNotDefined(symbol)); + + // In case this exposed value is referenced by other modules, + // create a decl for it whose implementation is a runtime error. + let mut pattern_vars = SendMap::default(); + pattern_vars.insert(symbol, var_store.fresh()); + + let runtime_error = RuntimeError::ExposedButNotDefined(symbol); + let def = Def { + loc_pattern: Loc::at(Region::zero(), Pattern::Identifier(symbol)), + loc_expr: Loc::at(Region::zero(), Expr::RuntimeError(runtime_error)), + expr_var: var_store.fresh(), + pattern_vars, + annotation: None, + }; + + declarations.push_def(def); + } + + // Incorporate any remaining output.lookups entries into references. + referenced_values.extend(output.references.value_lookups().copied()); + referenced_types.extend(output.references.type_lookups().copied()); + + // Incorporate any remaining output.calls entries into references. + referenced_values.extend(output.references.calls().copied()); + + // Gather up all the symbols that were referenced from other modules. + referenced_values.extend(env.qualified_value_lookups.iter().copied()); + referenced_types.extend(env.qualified_type_lookups.iter().copied()); + + let mut fix_closures_no_capture_symbols = VecSet::default(); + let mut fix_closures_closure_captures = VecMap::default(); + for index in 0..declarations.len() { + use crate::expr::DeclarationTag::*; + + // For each declaration, we need to fixup the closures inside its def. + // Reuse the fixup buffer allocations from the previous iteration. + fix_closures_no_capture_symbols.clear(); + fix_closures_closure_captures.clear(); + + match declarations.declarations[index] { + Value => { + // def pattern has no default expressions, so skip + let loc_expr = &mut declarations.expressions[index]; + + fix_values_captured_in_closure_expr( + &mut loc_expr.value, + &mut fix_closures_no_capture_symbols, + &mut fix_closures_closure_captures, + ); + } + Function(f_index) | Recursive(f_index) | TailRecursive(f_index) => { + let name = declarations.symbols[index].value; + let function_def = &mut declarations.function_bodies[f_index.index()].value; + let loc_expr = &mut declarations.expressions[index]; + + function_def.captured_symbols.retain(|(s, _)| *s != name); + + let mut no_capture_symbols = VecSet::default(); + if function_def.captured_symbols.is_empty() { + no_capture_symbols.insert(name); + } + + // patterns can contain default expressions, so must go over them too! + for (_, _, loc_pat) in function_def.arguments.iter_mut() { + fix_values_captured_in_closure_pattern( + &mut loc_pat.value, + &mut fix_closures_no_capture_symbols, + &mut fix_closures_closure_captures, + ); + } + + fix_values_captured_in_closure_expr( + &mut loc_expr.value, + &mut fix_closures_no_capture_symbols, + &mut fix_closures_closure_captures, + ); + } + Destructure(d_index) => { + let destruct_def = &mut declarations.destructs[d_index.index()]; + let loc_pat = &mut destruct_def.loc_pattern; + let loc_expr = &mut declarations.expressions[index]; + + fix_values_captured_in_closure_pattern( + &mut loc_pat.value, + &mut fix_closures_no_capture_symbols, + &mut fix_closures_closure_captures, + ); + fix_values_captured_in_closure_expr( + &mut loc_expr.value, + &mut fix_closures_no_capture_symbols, + &mut fix_closures_closure_captures, + ); + } + MutualRecursion { .. } => { + // the declarations of this group will be treaded individually by later iterations + } + Expectation => { + let loc_expr = &mut declarations.expressions[index]; + fix_values_captured_in_closure_expr( + &mut loc_expr.value, + &mut fix_closures_no_capture_symbols, + &mut fix_closures_closure_captures, + ); + } + ExpectationFx => { + let loc_expr = &mut declarations.expressions[index]; + fix_values_captured_in_closure_expr( + &mut loc_expr.value, + &mut fix_closures_no_capture_symbols, + &mut fix_closures_closure_captures, + ); + } + } + } + + let collected = declarations.expects(); + + ModuleOutput { + scope, + aliases, + rigid_variables, + declarations, + referenced_values, + referenced_types, + exposed_imports: can_exposed_imports, + problems: env.problems, + symbols_from_requires, + pending_derives, + loc_expects: collected.expects, + loc_dbgs: collected.dbgs, + exposed_symbols, + } +} + +fn fix_values_captured_in_closure_def( + def: &mut crate::def::Def, + no_capture_symbols: &mut VecSet, + closure_captures: &mut VecMap>, +) { + // patterns can contain default expressions, so much go over them too! + fix_values_captured_in_closure_pattern( + &mut def.loc_pattern.value, + no_capture_symbols, + closure_captures, + ); + + fix_values_captured_in_closure_expr( + &mut def.loc_expr.value, + no_capture_symbols, + closure_captures, + ); +} + +fn fix_values_captured_in_closure_defs( + defs: &mut [crate::def::Def], + no_capture_symbols: &mut VecSet, + closure_captures: &mut VecMap>, +) { + // Mutually recursive functions should both capture the union of all their capture sets + // + // Really unfortunate we make a lot of clones here, can this be done more efficiently? + let mut total_capture_set = VecMap::default(); + for def in defs.iter_mut() { + if let Expr::Closure(ClosureData { + captured_symbols, .. + }) = &def.loc_expr.value + { + total_capture_set.extend(captured_symbols.iter().copied()); + } + } + for def in defs.iter() { + for symbol in + crate::traverse::symbols_introduced_from_pattern(&def.loc_pattern).map(|ls| ls.value) + { + total_capture_set.remove(&symbol); + } + } + + let mut total_capture_set: Vec<_> = total_capture_set.into_iter().collect(); + total_capture_set.sort_by_key(|(sym, _)| *sym); + for def in defs.iter_mut() { + if let Expr::Closure(ClosureData { + captured_symbols, .. + }) = &mut def.loc_expr.value + { + *captured_symbols = total_capture_set.clone(); + } + } + + for def in defs.iter_mut() { + fix_values_captured_in_closure_def(def, no_capture_symbols, closure_captures); + } +} + +fn fix_values_captured_in_closure_pattern( + pattern: &mut crate::pattern::Pattern, + no_capture_symbols: &mut VecSet, + closure_captures: &mut VecMap>, +) { + use crate::pattern::Pattern::*; + + match pattern { + AppliedTag { + arguments: loc_args, + .. + } => { + for (_, loc_arg) in loc_args.iter_mut() { + fix_values_captured_in_closure_pattern( + &mut loc_arg.value, + no_capture_symbols, + closure_captures, + ); + } + } + UnwrappedOpaque { argument, .. } => { + let (_, loc_arg) = &mut **argument; + fix_values_captured_in_closure_pattern( + &mut loc_arg.value, + no_capture_symbols, + closure_captures, + ); + } + RecordDestructure { destructs, .. } => { + for loc_destruct in destructs.iter_mut() { + use crate::pattern::DestructType::*; + match &mut loc_destruct.value.typ { + Required => {} + Optional(_, loc_expr) => fix_values_captured_in_closure_expr( + &mut loc_expr.value, + no_capture_symbols, + closure_captures, + ), + Guard(_, loc_pattern) => fix_values_captured_in_closure_pattern( + &mut loc_pattern.value, + no_capture_symbols, + closure_captures, + ), + } + } + } + TupleDestructure { destructs, .. } => { + for loc_destruct in destructs.iter_mut() { + fix_values_captured_in_closure_pattern( + &mut loc_destruct.value.typ.1.value, + no_capture_symbols, + closure_captures, + ) + } + } + List { patterns, .. } => { + for loc_pat in patterns.patterns.iter_mut() { + fix_values_captured_in_closure_pattern( + &mut loc_pat.value, + no_capture_symbols, + closure_captures, + ); + } + } + As(subpattern, _) => { + fix_values_captured_in_closure_pattern( + &mut subpattern.value, + no_capture_symbols, + closure_captures, + ); + } + + Identifier(_) + | NumLiteral(..) + | IntLiteral(..) + | FloatLiteral(..) + | StrLiteral(_) + | SingleQuote(..) + | Underscore + | Shadowed(..) + | MalformedPattern(_, _) + | UnsupportedPattern(_) + | OpaqueNotInScope(..) + | AbilityMemberSpecialization { .. } => (), + } +} + +fn fix_values_captured_in_closure_expr( + expr: &mut crate::expr::Expr, + no_capture_symbols: &mut VecSet, + closure_captures: &mut VecMap>, +) { + use crate::expr::Expr::*; + + match expr { + LetNonRec(def, loc_expr) => { + // LetNonRec(Box, Box>, Variable, Aliases), + fix_values_captured_in_closure_def(def, no_capture_symbols, closure_captures); + fix_values_captured_in_closure_expr( + &mut loc_expr.value, + no_capture_symbols, + closure_captures, + ); + } + LetRec(defs, loc_expr, _) => { + // LetRec(Vec, Box>, Variable, Aliases), + fix_values_captured_in_closure_defs(defs, no_capture_symbols, closure_captures); + fix_values_captured_in_closure_expr( + &mut loc_expr.value, + no_capture_symbols, + closure_captures, + ); + } + + Expect { + loc_condition, + loc_continuation, + .. + } + | ExpectFx { + loc_condition, + loc_continuation, + .. + } + | Dbg { + loc_message: loc_condition, + loc_continuation, + .. + } => { + fix_values_captured_in_closure_expr( + &mut loc_condition.value, + no_capture_symbols, + closure_captures, + ); + fix_values_captured_in_closure_expr( + &mut loc_continuation.value, + no_capture_symbols, + closure_captures, + ); + } + + Crash { msg, ret_var: _ } => { + fix_values_captured_in_closure_expr( + &mut msg.value, + no_capture_symbols, + closure_captures, + ); + } + + Closure(ClosureData { + captured_symbols, + name, + arguments, + loc_body, + .. + }) => { + captured_symbols.retain(|(s, _)| !no_capture_symbols.contains(s)); + captured_symbols.retain(|(s, _)| s != name); + + let original_captures_len = captured_symbols.len(); + let mut i = 0; + let mut added_captures = false; + while i < original_captures_len { + // If we've captured a capturing closure, replace the captured closure symbol with + // the symbols of its captures. That way, we can construct the closure with the + // captures it needs inside our body. + // + // E.g. + // x = "" + // inner = \{} -> x + // outer = \{} -> inner {} + // + // initially `outer` captures [inner], but this is then replaced with just [x]. + let (captured_symbol, _) = captured_symbols[i]; + if let Some(captures) = closure_captures.get(&captured_symbol) { + debug_assert!(!captures.is_empty()); + captured_symbols.extend(captures); + captured_symbols.swap_remove(i); + // Jump two, because the next element is now one of the newly-added captures, + // which we don't need to check. + i += 2; + + added_captures = true; + } else { + i += 1; + } + } + if added_captures { + // Re-sort, since we've added new captures. + captured_symbols.sort_by_key(|(sym, _)| *sym); + captured_symbols.dedup_by_key(|(sym, _)| *sym); + } + + if captured_symbols.is_empty() { + no_capture_symbols.insert(*name); + } else { + closure_captures.insert(*name, captured_symbols.to_vec()); + } + + // patterns can contain default expressions, so much go over them too! + for (_, _, loc_pat) in arguments.iter_mut() { + fix_values_captured_in_closure_pattern( + &mut loc_pat.value, + no_capture_symbols, + closure_captures, + ); + } + + fix_values_captured_in_closure_expr( + &mut loc_body.value, + no_capture_symbols, + closure_captures, + ); + } + + Num(..) + | Int(..) + | Float(..) + | Str(_) + | SingleQuote(..) + | IngestedFile(..) + | Var(..) + | AbilityMember(..) + | EmptyRecord + | TypedHole { .. } + | RuntimeError(_) + | ZeroArgumentTag { .. } + | RecordAccessor { .. } => {} + + List { loc_elems, .. } => { + for elem in loc_elems.iter_mut() { + fix_values_captured_in_closure_expr( + &mut elem.value, + no_capture_symbols, + closure_captures, + ); + } + } + + When { + loc_cond, branches, .. + } => { + fix_values_captured_in_closure_expr( + &mut loc_cond.value, + no_capture_symbols, + closure_captures, + ); + + for branch in branches.iter_mut() { + fix_values_captured_in_closure_expr( + &mut branch.value.value, + no_capture_symbols, + closure_captures, + ); + + // patterns can contain default expressions, so much go over them too! + for loc_pat in branch.patterns.iter_mut() { + fix_values_captured_in_closure_pattern( + &mut loc_pat.pattern.value, + no_capture_symbols, + closure_captures, + ); + } + + if let Some(guard) = &mut branch.guard { + fix_values_captured_in_closure_expr( + &mut guard.value, + no_capture_symbols, + closure_captures, + ); + } + } + } + + If { + branches, + final_else, + .. + } => { + for (loc_cond, loc_then) in branches.iter_mut() { + fix_values_captured_in_closure_expr( + &mut loc_cond.value, + no_capture_symbols, + closure_captures, + ); + fix_values_captured_in_closure_expr( + &mut loc_then.value, + no_capture_symbols, + closure_captures, + ); + } + + fix_values_captured_in_closure_expr( + &mut final_else.value, + no_capture_symbols, + closure_captures, + ); + } + + Call(function, arguments, _) => { + fix_values_captured_in_closure_expr( + &mut function.1.value, + no_capture_symbols, + closure_captures, + ); + + for (_, loc_arg) in arguments.iter_mut() { + fix_values_captured_in_closure_expr( + &mut loc_arg.value, + no_capture_symbols, + closure_captures, + ); + } + } + RunLowLevel { args, .. } | ForeignCall { args, .. } => { + for (_, arg) in args.iter_mut() { + fix_values_captured_in_closure_expr(arg, no_capture_symbols, closure_captures); + } + } + + Record { fields, .. } + | RecordUpdate { + updates: fields, .. + } => { + for (_, field) in fields.iter_mut() { + fix_values_captured_in_closure_expr( + &mut field.loc_expr.value, + no_capture_symbols, + closure_captures, + ); + } + } + + Tuple { elems, .. } => { + for (_var, expr) in elems.iter_mut() { + fix_values_captured_in_closure_expr( + &mut expr.value, + no_capture_symbols, + closure_captures, + ); + } + } + + RecordAccess { loc_expr, .. } | TupleAccess { loc_expr, .. } => { + fix_values_captured_in_closure_expr( + &mut loc_expr.value, + no_capture_symbols, + closure_captures, + ); + } + + Tag { arguments, .. } => { + for (_, loc_arg) in arguments.iter_mut() { + fix_values_captured_in_closure_expr( + &mut loc_arg.value, + no_capture_symbols, + closure_captures, + ); + } + } + OpaqueRef { argument, .. } => { + let (_, loc_arg) = &mut **argument; + fix_values_captured_in_closure_expr( + &mut loc_arg.value, + no_capture_symbols, + closure_captures, + ); + } + OpaqueWrapFunction(_) => {} + } +} + +/// Type state for a single module. +#[derive(Debug)] +pub struct TypeState { + pub subs: Subs, + pub exposed_vars_by_symbol: Vec<(Symbol, Variable)>, + pub abilities: AbilitiesStore, + pub solved_implementations: ResolvedImplementations, +} + +impl TypeState { + pub fn serialize(&self, writer: &mut impl std::io::Write) -> std::io::Result { + let Self { + subs, + exposed_vars_by_symbol, + abilities, + solved_implementations, + } = self; + + let written_subs = subs.serialize(exposed_vars_by_symbol, writer)?; + let written_ab = abilities.serialize(writer)?; + let written_solved_impls = + crate::abilities::serialize_solved_implementations(solved_implementations, writer)?; + + Ok(written_subs + written_ab + written_solved_impls) + } + + pub fn deserialize(bytes: &[u8]) -> (Self, usize) { + let ((subs, exposed_vars_by_symbol), len_subs) = Subs::deserialize(bytes); + let bytes = &bytes[len_subs..]; + + let (abilities, len_abilities) = AbilitiesStore::deserialize(bytes); + let bytes = &bytes[len_abilities..]; + + let (solved_implementations, len_solved_impls) = + crate::abilities::deserialize_solved_implementations(bytes); + + let total_offset = len_subs + len_abilities + len_solved_impls; + + ( + Self { + subs, + exposed_vars_by_symbol: exposed_vars_by_symbol.to_vec(), + abilities, + solved_implementations, + }, + total_offset, + ) + } +} diff --git a/crates/compiler/can/src/num.rs b/crates/compiler/can/src/num.rs new file mode 100644 index 0000000000..6efa4d3805 --- /dev/null +++ b/crates/compiler/can/src/num.rs @@ -0,0 +1,362 @@ +use crate::env::Env; +use crate::expr::{Expr, IntValue}; +use roc_parse::ast::Base; +use roc_problem::can::Problem; +use roc_problem::can::RuntimeError::*; +use roc_problem::can::{FloatErrorKind, IntErrorKind}; +use roc_region::all::Region; +pub use roc_types::num::{FloatBound, FloatWidth, IntBound, IntLitWidth, NumBound, SignDemand}; +use roc_types::subs::VarStore; + +use std::str; + +#[inline(always)] +pub fn num_expr_from_result( + var_store: &mut VarStore, + result: Result<(&str, ParsedNumResult), (&str, IntErrorKind)>, + region: Region, + env: &mut Env, +) -> Expr { + match result { + Ok((str, ParsedNumResult::UnknownNum(num, bound))) => { + Expr::Num(var_store.fresh(), (*str).into(), num, bound) + } + Ok((str, ParsedNumResult::Int(num, bound))) => Expr::Int( + var_store.fresh(), + var_store.fresh(), + (*str).into(), + num, + bound, + ), + Ok((str, ParsedNumResult::Float(num, bound))) => Expr::Float( + var_store.fresh(), + var_store.fresh(), + (*str).into(), + num, + bound, + ), + Err((raw, error)) => { + // (Num *) compiles to Int if it doesn't + // get specialized to something else first, + // so use int's overflow bounds here. + let runtime_error = InvalidInt(error, Base::Decimal, region, raw.into()); + + env.problem(Problem::RuntimeError(runtime_error.clone())); + + Expr::RuntimeError(runtime_error) + } + } +} + +#[inline(always)] +pub fn int_expr_from_result( + var_store: &mut VarStore, + result: Result<(&str, IntValue, IntBound), (&str, IntErrorKind)>, + region: Region, + base: Base, + env: &mut Env, +) -> Expr { + // Int stores a variable to generate better error messages + match result { + Ok((str, int, bound)) => Expr::Int( + var_store.fresh(), + var_store.fresh(), + (*str).into(), + int, + bound, + ), + Err((raw, error)) => { + let runtime_error = InvalidInt(error, base, region, raw.into()); + + env.problem(Problem::RuntimeError(runtime_error.clone())); + + Expr::RuntimeError(runtime_error) + } + } +} + +#[inline(always)] +pub fn float_expr_from_result( + var_store: &mut VarStore, + result: Result<(&str, f64, FloatBound), (&str, FloatErrorKind)>, + region: Region, + env: &mut Env, +) -> Expr { + // Float stores a variable to generate better error messages + match result { + Ok((str, float, bound)) => Expr::Float( + var_store.fresh(), + var_store.fresh(), + (*str).into(), + float, + bound, + ), + Err((raw, error)) => { + let runtime_error = InvalidFloat(error, region, raw.into()); + + env.problem(Problem::RuntimeError(runtime_error.clone())); + + Expr::RuntimeError(runtime_error) + } + } +} + +pub enum ParsedNumResult { + Int(IntValue, IntBound), + Float(f64, FloatBound), + UnknownNum(IntValue, NumBound), +} + +#[inline(always)] +pub fn finish_parsing_num(raw: &str) -> Result<(&str, ParsedNumResult), (&str, IntErrorKind)> { + // Ignore underscores. + let radix = 10; + let (_, raw_without_suffix) = parse_literal_suffix(raw); + match from_str_radix(raw.replace('_', "").as_str(), radix) { + Ok(result) => Ok((raw_without_suffix, result)), + Err(e) => Err((raw, e)), + } +} + +#[inline(always)] +pub fn finish_parsing_base( + raw: &str, + base: Base, + is_negative: bool, +) -> Result<(IntValue, IntBound), (&str, IntErrorKind)> { + let radix = match base { + Base::Hex => 16, + Base::Decimal => 10, + Base::Octal => 8, + Base::Binary => 2, + }; + + // Ignore underscores, insert - when negative to get correct underflow/overflow behavior + (if is_negative { + from_str_radix(format!("-{}", raw.replace('_', "")).as_str(), radix) + } else { + from_str_radix(raw.replace('_', "").as_str(), radix) + }) + .and_then(|parsed| match parsed { + ParsedNumResult::Float(..) => Err(IntErrorKind::FloatSuffix), + ParsedNumResult::Int(val, bound) => Ok((val, bound)), + ParsedNumResult::UnknownNum(val, NumBound::None) => Ok((val, IntBound::None)), + ParsedNumResult::UnknownNum(val, NumBound::AtLeastIntOrFloat { sign, width }) => { + Ok((val, IntBound::AtLeast { sign, width })) + } + }) + .map_err(|e| (raw, e)) +} + +#[inline(always)] +pub fn finish_parsing_float(raw: &str) -> Result<(&str, f64, FloatBound), (&str, FloatErrorKind)> { + let (opt_bound, raw_without_suffix) = parse_literal_suffix(raw); + + let bound = match opt_bound { + None => FloatBound::None, + Some(ParsedWidth::Float(fw)) => FloatBound::Exact(fw), + Some(ParsedWidth::Int(_)) => return Err((raw, FloatErrorKind::IntSuffix)), + }; + + // Ignore underscores. + match raw_without_suffix.replace('_', "").parse::() { + Ok(float) if float.is_finite() => Ok((raw_without_suffix, float, bound)), + Ok(float) => { + if float.is_sign_positive() { + Err((raw, FloatErrorKind::PositiveInfinity)) + } else { + Err((raw, FloatErrorKind::NegativeInfinity)) + } + } + Err(_) => Err((raw, FloatErrorKind::Error)), + } +} + +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +enum ParsedWidth { + Int(IntLitWidth), + Float(FloatWidth), +} + +fn parse_literal_suffix(num_str: &str) -> (Option, &str) { + macro_rules! parse_num_suffix { + ($($suffix:expr, $width:expr)*) => {$( + if num_str.ends_with($suffix) { + return (Some($width), num_str.get(0..num_str.len() - $suffix.len()).unwrap()); + } + )*} + } + + parse_num_suffix! { + "u8", ParsedWidth::Int(IntLitWidth::U8) + "u16", ParsedWidth::Int(IntLitWidth::U16) + "u32", ParsedWidth::Int(IntLitWidth::U32) + "u64", ParsedWidth::Int(IntLitWidth::U64) + "u128", ParsedWidth::Int(IntLitWidth::U128) + "i8", ParsedWidth::Int(IntLitWidth::I8) + "i16", ParsedWidth::Int(IntLitWidth::I16) + "i32", ParsedWidth::Int(IntLitWidth::I32) + "i64", ParsedWidth::Int(IntLitWidth::I64) + "i128", ParsedWidth::Int(IntLitWidth::I128) + "nat", ParsedWidth::Int(IntLitWidth::Nat) + "dec", ParsedWidth::Float(FloatWidth::Dec) + "f32", ParsedWidth::Float(FloatWidth::F32) + "f64", ParsedWidth::Float(FloatWidth::F64) + } + + (None, num_str) +} + +/// Integer parsing code taken from the rust libcore, +/// pulled in so we can give custom error messages +/// +/// The Rust Project is dual-licensed under either Apache 2.0 or MIT, +/// at the user's choice. License information can be found in +/// the LEGAL_DETAILS file in the root directory of this distribution. +/// +/// Thanks to the Rust project and its contributors! +fn from_str_radix(src: &str, radix: u32) -> Result { + use self::IntErrorKind::*; + + assert!( + (2..=36).contains(&radix), + "from_str_radix_int: must lie in the range `[2, 36]` - found {radix}" + ); + + let (opt_exact_bound, src) = parse_literal_suffix(src); + + use std::num::IntErrorKind as StdIEK; + let result = match i128::from_str_radix(src, radix) { + Ok(result) => IntValue::I128(result.to_ne_bytes()), + Err(pie) => match pie.kind() { + StdIEK::Empty => return Err(IntErrorKind::Empty), + StdIEK::InvalidDigit => return Err(IntErrorKind::InvalidDigit), + StdIEK::NegOverflow => return Err(IntErrorKind::Underflow), + StdIEK::PosOverflow => { + // try a u128 + match u128::from_str_radix(src, radix) { + Ok(result) => IntValue::U128(result.to_ne_bytes()), + Err(pie) => match pie.kind() { + StdIEK::InvalidDigit => return Err(IntErrorKind::InvalidDigit), + StdIEK::PosOverflow => return Err(IntErrorKind::Overflow), + StdIEK::Empty | StdIEK::Zero | StdIEK::NegOverflow => unreachable!(), + _ => unreachable!("I thought all possibilities were exhausted, but std::num added a new one") + }, + } + } + StdIEK::Zero => unreachable!("Parsed a i128"), + _ => unreachable!( + "I thought all possibilities were exhausted, but std::num added a new one" + ), + }, + }; + + let (lower_bound, is_negative) = match result { + IntValue::I128(bytes) => { + let num = i128::from_ne_bytes(bytes); + + (lower_bound_of_int_literal(num), num < 0) + } + IntValue::U128(_) => (IntLitWidth::U128, false), + }; + + match opt_exact_bound { + None => { + // There's no exact bound, but we do have a lower bound. + let sign_demand = if is_negative { + SignDemand::Signed + } else { + SignDemand::NoDemand + }; + Ok(ParsedNumResult::UnknownNum( + result, + NumBound::AtLeastIntOrFloat { + sign: sign_demand, + width: lower_bound, + }, + )) + } + Some(ParsedWidth::Float(fw)) => { + // For now, assume floats can represent all integers + // TODO: this is somewhat incorrect, revisit + Ok(ParsedNumResult::Float( + match result { + IntValue::I128(n) => i128::from_ne_bytes(n) as f64, + IntValue::U128(n) => i128::from_ne_bytes(n) as f64, + }, + FloatBound::Exact(fw), + )) + } + Some(ParsedWidth::Int(exact_width)) => { + // We need to check if the exact bound >= lower bound. + if exact_width.is_superset(&lower_bound, is_negative) { + // Great! Use the exact bound. + Ok(ParsedNumResult::Int(result, IntBound::Exact(exact_width))) + } else { + // This is something like 200i8; the lower bound is u8, which holds strictly more + // ints on the positive side than i8 does. Report an error depending on which side + // of the integers we checked. + let err = if is_negative { + UnderflowsSuffix { + suffix_type: exact_width.type_str(), + min_value: exact_width.min_value(), + } + } else { + OverflowsSuffix { + suffix_type: exact_width.type_str(), + max_value: exact_width.max_value(), + } + }; + Err(err) + } + } + } +} + +fn lower_bound_of_int_literal(result: i128) -> IntLitWidth { + use IntLitWidth::*; + if result >= 0 { + // Positive + let result = result as u128; + if result > U64.max_value() { + I128 + } else if result > I64.max_value() { + U64 + } else if result > F64.max_value() { + I64 + } else if result > U32.max_value() { + F64 + } else if result > I32.max_value() { + U32 + } else if result > F32.max_value() { + I32 + } else if result > U16.max_value() { + F32 + } else if result > I16.max_value() { + U16 + } else if result > U8.max_value() { + I16 + } else if result > I8.max_value() { + U8 + } else { + I8 + } + } else { + // Negative + if result < I64.min_value() { + I128 + } else if result < F64.min_value() { + I64 + } else if result < I32.min_value() { + F64 + } else if result < F32.min_value() { + I32 + } else if result < I16.min_value() { + F32 + } else if result < I8.min_value() { + I16 + } else { + I8 + } + } +} diff --git a/crates/compiler/can/src/operator.rs b/crates/compiler/can/src/operator.rs new file mode 100644 index 0000000000..e6bfcd897b --- /dev/null +++ b/crates/compiler/can/src/operator.rs @@ -0,0 +1,930 @@ +#![allow(clippy::manual_map)] + +use bumpalo::collections::Vec; +use bumpalo::Bump; +use roc_error_macros::internal_error; +use roc_module::called_via::BinOp::Pizza; +use roc_module::called_via::{BinOp, CalledVia}; +use roc_module::ident::ModuleName; +use roc_parse::ast::Expr::{self, *}; +use roc_parse::ast::{ + AssignedField, Collection, Pattern, RecordBuilderField, StrLiteral, StrSegment, ValueDef, + WhenBranch, +}; +use roc_region::all::{Loc, Region}; + +// BinOp precedence logic adapted from Gluon by Markus Westerlind +// https://github.com/gluon-lang/gluon - license information can be found in +// the LEGAL_DETAILS file in the root directory of this distribution. +// +// Thank you, Markus! + +fn new_op_call_expr<'a>( + arena: &'a Bump, + left: &'a Loc>, + loc_op: Loc, + right: &'a Loc>, +) -> Loc> { + let region = Region::span_across(&left.region, &right.region); + + let value = match loc_op.value { + Pizza => { + // Rewrite the Pizza operator into an Apply + + match &right.value { + Apply(function, arguments, _called_via) => { + let mut args = Vec::with_capacity_in(1 + arguments.len(), arena); + + args.push(left); + args.extend(arguments.iter()); + + let args = args.into_bump_slice(); + + Apply(function, args, CalledVia::BinOp(Pizza)) + } + _ => { + // e.g. `1 |> (if b then (\a -> a) else (\c -> c))` + Apply(right, arena.alloc([left]), CalledVia::BinOp(Pizza)) + } + } + } + binop => { + // This is a normal binary operator like (+), so desugar it + // into the appropriate function call. + let (module_name, ident) = binop_to_function(binop); + + let args = arena.alloc([left, right]); + + let loc_expr = arena.alloc(Loc { + value: Expr::Var { module_name, ident }, + region: loc_op.region, + }); + + Apply(loc_expr, args, CalledVia::BinOp(binop)) + } + }; + + Loc { region, value } +} + +fn desugar_value_def<'a>(arena: &'a Bump, def: &'a ValueDef<'a>) -> ValueDef<'a> { + use ValueDef::*; + + match def { + Body(loc_pattern, loc_expr) => Body( + desugar_loc_pattern(arena, loc_pattern), + desugar_expr(arena, loc_expr), + ), + ann @ Annotation(_, _) => *ann, + AnnotatedBody { + ann_pattern, + ann_type, + comment, + body_pattern, + body_expr, + } => AnnotatedBody { + ann_pattern, + ann_type, + comment: *comment, + body_pattern, + body_expr: desugar_expr(arena, body_expr), + }, + Dbg { + condition, + preceding_comment, + } => { + let desugared_condition = &*arena.alloc(desugar_expr(arena, condition)); + Dbg { + condition: desugared_condition, + preceding_comment: *preceding_comment, + } + } + Expect { + condition, + preceding_comment, + } => { + let desugared_condition = &*arena.alloc(desugar_expr(arena, condition)); + Expect { + condition: desugared_condition, + preceding_comment: *preceding_comment, + } + } + ExpectFx { + condition, + preceding_comment, + } => { + let desugared_condition = &*arena.alloc(desugar_expr(arena, condition)); + ExpectFx { + condition: desugared_condition, + preceding_comment: *preceding_comment, + } + } + } +} + +pub fn desugar_defs<'a>(arena: &'a Bump, defs: &mut roc_parse::ast::Defs<'a>) { + for value_def in defs.value_defs.iter_mut() { + *value_def = desugar_value_def(arena, arena.alloc(*value_def)); + } +} + +/// Reorder the expression tree based on operator precedence and associativity rules, +/// then replace the BinOp nodes with Apply nodes. Also drop SpaceBefore and SpaceAfter nodes. +pub fn desugar_expr<'a>(arena: &'a Bump, loc_expr: &'a Loc>) -> &'a Loc> { + match &loc_expr.value { + Float(..) + | Num(..) + | NonBase10Int { .. } + | SingleQuote(_) + | AccessorFunction(_) + | Var { .. } + | Underscore { .. } + | MalformedIdent(_, _) + | MalformedClosure + | PrecedenceConflict { .. } + | MultipleRecordBuilders { .. } + | UnappliedRecordBuilder { .. } + | Tag(_) + | OpaqueRef(_) + | IngestedFile(_, _) + | Crash => loc_expr, + + Str(str_literal) => match str_literal { + StrLiteral::PlainLine(_) => loc_expr, + StrLiteral::Line(segments) => { + let region = loc_expr.region; + let value = Str(StrLiteral::Line(desugar_str_segments(arena, segments))); + + arena.alloc(Loc { region, value }) + } + StrLiteral::Block(lines) => { + let region = loc_expr.region; + let new_lines = Vec::from_iter_in( + lines + .iter() + .map(|segments| desugar_str_segments(arena, segments)), + arena, + ); + let value = Str(StrLiteral::Block(new_lines.into_bump_slice())); + + arena.alloc(Loc { region, value }) + } + }, + + TupleAccess(sub_expr, paths) => { + let region = loc_expr.region; + let loc_sub_expr = Loc { + region, + value: **sub_expr, + }; + let value = TupleAccess(&desugar_expr(arena, arena.alloc(loc_sub_expr)).value, paths); + + arena.alloc(Loc { region, value }) + } + RecordAccess(sub_expr, paths) => { + let region = loc_expr.region; + let loc_sub_expr = Loc { + region, + value: **sub_expr, + }; + let value = RecordAccess(&desugar_expr(arena, arena.alloc(loc_sub_expr)).value, paths); + + arena.alloc(Loc { region, value }) + } + List(items) => { + let mut new_items = Vec::with_capacity_in(items.len(), arena); + + for item in items.iter() { + new_items.push(desugar_expr(arena, item)); + } + let new_items = new_items.into_bump_slice(); + let value: Expr<'a> = List(items.replace_items(new_items)); + + arena.alloc(Loc { + region: loc_expr.region, + value, + }) + } + Record(fields) => arena.alloc(Loc { + region: loc_expr.region, + value: Record(fields.map_items(arena, |field| { + let value = desugar_field(arena, &field.value); + Loc { + value, + region: field.region, + } + })), + }), + Tuple(fields) => arena.alloc(Loc { + region: loc_expr.region, + value: Tuple(fields.map_items(arena, |field| desugar_expr(arena, field))), + }), + RecordUpdate { fields, update } => { + // NOTE the `update` field is always a `Var { .. }`, we only desugar it to get rid of + // any spaces before/after + let new_update = desugar_expr(arena, update); + + let new_fields = fields.map_items(arena, |field| { + let value = desugar_field(arena, &field.value); + Loc { + value, + region: field.region, + } + }); + + arena.alloc(Loc { + region: loc_expr.region, + value: RecordUpdate { + update: new_update, + fields: new_fields, + }, + }) + } + Closure(loc_patterns, loc_ret) => arena.alloc(Loc { + region: loc_expr.region, + value: Closure( + desugar_loc_patterns(arena, loc_patterns), + desugar_expr(arena, loc_ret), + ), + }), + Backpassing(loc_patterns, loc_body, loc_ret) => { + // loc_patterns <- loc_body + // + // loc_ret + + // first desugar the body, because it may contain |> + let desugared_body = desugar_expr(arena, loc_body); + + let desugared_ret = desugar_expr(arena, loc_ret); + let desugared_loc_patterns = desugar_loc_patterns(arena, loc_patterns); + let closure = Expr::Closure(desugared_loc_patterns, desugared_ret); + let loc_closure = Loc::at(loc_expr.region, closure); + + match &desugared_body.value { + Expr::Apply(function, arguments, called_via) => { + let mut new_arguments: Vec<'a, &'a Loc>> = + Vec::with_capacity_in(arguments.len() + 1, arena); + new_arguments.extend(arguments.iter()); + new_arguments.push(arena.alloc(loc_closure)); + + let call = Expr::Apply(function, new_arguments.into_bump_slice(), *called_via); + let loc_call = Loc::at(loc_expr.region, call); + + arena.alloc(loc_call) + } + _ => { + // e.g. `x <- (if b then (\a -> a) else (\c -> c))` + let call = Expr::Apply( + desugared_body, + arena.alloc([&*arena.alloc(loc_closure)]), + CalledVia::Space, + ); + let loc_call = Loc::at(loc_expr.region, call); + + arena.alloc(loc_call) + } + } + } + RecordBuilder(_) => arena.alloc(Loc { + value: UnappliedRecordBuilder(loc_expr), + region: loc_expr.region, + }), + BinOps(lefts, right) => desugar_bin_ops(arena, loc_expr.region, lefts, right), + Defs(defs, loc_ret) => { + let mut defs = (*defs).clone(); + desugar_defs(arena, &mut defs); + + let loc_ret = desugar_expr(arena, loc_ret); + + arena.alloc(Loc::at(loc_expr.region, Defs(arena.alloc(defs), loc_ret))) + } + Apply(loc_fn, loc_args, called_via) => { + let mut desugared_args = Vec::with_capacity_in(loc_args.len(), arena); + let mut builder_apply_exprs = None; + + for loc_arg in loc_args.iter() { + let mut current = loc_arg.value; + let arg = loop { + match current { + RecordBuilder(fields) => { + if builder_apply_exprs.is_some() { + return arena.alloc(Loc { + value: MultipleRecordBuilders(loc_expr), + region: loc_expr.region, + }); + } + + let builder_arg = record_builder_arg(arena, loc_arg.region, fields); + builder_apply_exprs = Some(builder_arg.apply_exprs); + + break builder_arg.closure; + } + SpaceBefore(expr, _) | SpaceAfter(expr, _) => { + current = *expr; + } + _ => break loc_arg, + } + }; + + desugared_args.push(desugar_expr(arena, arg)); + } + + let desugared_args = desugared_args.into_bump_slice(); + + let mut apply: &Loc = arena.alloc(Loc { + value: Apply(desugar_expr(arena, loc_fn), desugared_args, *called_via), + region: loc_expr.region, + }); + + match builder_apply_exprs { + None => {} + + Some(apply_exprs) => { + for expr in apply_exprs { + let desugared_expr = desugar_expr(arena, expr); + + let args = std::slice::from_ref(arena.alloc(apply)); + + apply = arena.alloc(Loc { + value: Apply(desugared_expr, args, CalledVia::RecordBuilder), + region: loc_expr.region, + }); + } + } + } + + apply + } + When(loc_cond_expr, branches) => { + let loc_desugared_cond = &*arena.alloc(desugar_expr(arena, loc_cond_expr)); + let mut desugared_branches = Vec::with_capacity_in(branches.len(), arena); + + for branch in branches.iter() { + let desugared_expr = desugar_expr(arena, &branch.value); + let desugared_patterns = desugar_loc_patterns(arena, branch.patterns); + + let desugared_guard = if let Some(guard) = &branch.guard { + Some(*desugar_expr(arena, guard)) + } else { + None + }; + + desugared_branches.push(&*arena.alloc(WhenBranch { + patterns: desugared_patterns, + value: *desugared_expr, + guard: desugared_guard, + })); + } + + let desugared_branches = desugared_branches.into_bump_slice(); + + arena.alloc(Loc { + value: When(loc_desugared_cond, desugared_branches), + region: loc_expr.region, + }) + } + UnaryOp(loc_arg, loc_op) => { + use roc_module::called_via::UnaryOp::*; + + let region = loc_op.region; + let op = loc_op.value; + // TODO desugar this in canonicalization instead, so we can work + // in terms of integers exclusively and not need to create strings + // which canonicalization then needs to look up, check if they're exposed, etc + let value = match op { + Negate => Var { + module_name: ModuleName::NUM, + ident: "neg", + }, + Not => Var { + module_name: ModuleName::BOOL, + ident: "not", + }, + }; + let loc_fn_var = arena.alloc(Loc { region, value }); + let desugared_args = arena.alloc([desugar_expr(arena, loc_arg)]); + + arena.alloc(Loc { + value: Apply(loc_fn_var, desugared_args, CalledVia::UnaryOp(op)), + region: loc_expr.region, + }) + } + SpaceBefore(expr, _) | SpaceAfter(expr, _) => { + // Since we've already begun canonicalization, spaces and parens + // are no longer needed and should be dropped. + desugar_expr( + arena, + arena.alloc(Loc { + value: **expr, + region: loc_expr.region, + }), + ) + } + ParensAround(expr) => { + let desugared = desugar_expr( + arena, + arena.alloc(Loc { + value: **expr, + region: loc_expr.region, + }), + ); + + arena.alloc(Loc { + value: ParensAround(&desugared.value), + region: loc_expr.region, + }) + } + If(if_thens, final_else_branch) => { + // If does not get desugared into `when` so we can give more targeted error messages during type checking. + let desugared_final_else = &*arena.alloc(desugar_expr(arena, final_else_branch)); + + let mut desugared_if_thens = Vec::with_capacity_in(if_thens.len(), arena); + + for (condition, then_branch) in if_thens.iter() { + desugared_if_thens.push(( + *desugar_expr(arena, condition), + *desugar_expr(arena, then_branch), + )); + } + + arena.alloc(Loc { + value: If(desugared_if_thens.into_bump_slice(), desugared_final_else), + region: loc_expr.region, + }) + } + Expect(condition, continuation) => { + let desugared_condition = &*arena.alloc(desugar_expr(arena, condition)); + let desugared_continuation = &*arena.alloc(desugar_expr(arena, continuation)); + arena.alloc(Loc { + value: Expect(desugared_condition, desugared_continuation), + region: loc_expr.region, + }) + } + Dbg(condition, continuation) => { + // Desugars a `dbg x` statement into + // `roc_dbg (Inspect.toDbgStr (Inspect.inspect x))` + let desugared_continuation = &*arena.alloc(desugar_expr(arena, continuation)); + + let region = condition.region; + // TODO desugar this in canonicalization instead, so we can work + // in terms of integers exclusively and not need to create strings + // which canonicalization then needs to look up, check if they're exposed, etc + let inspect = Var { + module_name: ModuleName::INSPECT, + ident: "inspect", + }; + let loc_inspect_fn_var = arena.alloc(Loc { + value: inspect, + region, + }); + let desugared_inspect_args = arena.alloc([desugar_expr(arena, condition)]); + + let inspector = arena.alloc(Loc { + value: Apply(loc_inspect_fn_var, desugared_inspect_args, CalledVia::Space), + region, + }); + + let to_dbg_str = Var { + module_name: ModuleName::INSPECT, + ident: "toDbgStr", + }; + let loc_to_dbg_str_fn_var = arena.alloc(Loc { + value: to_dbg_str, + region, + }); + let to_dbg_str_args = arena.alloc([&*inspector]); + let dbg_str = arena.alloc(Loc { + value: Apply(loc_to_dbg_str_fn_var, to_dbg_str_args, CalledVia::Space), + region, + }); + arena.alloc(Loc { + value: LowLevelDbg(dbg_str, desugared_continuation), + region: loc_expr.region, + }) + } + LowLevelDbg(_, _) => unreachable!("Only exists after desugaring"), + } +} + +fn desugar_str_segments<'a>( + arena: &'a Bump, + segments: &'a [StrSegment<'a>], +) -> &'a [StrSegment<'a>] { + Vec::from_iter_in( + segments.iter().map(|segment| match segment { + StrSegment::Plaintext(_) | StrSegment::Unicode(_) | StrSegment::EscapedChar(_) => { + *segment + } + StrSegment::Interpolated(loc_expr) => { + let loc_desugared = desugar_expr( + arena, + arena.alloc(Loc { + region: loc_expr.region, + value: *loc_expr.value, + }), + ); + StrSegment::Interpolated(Loc { + region: loc_desugared.region, + value: arena.alloc(loc_desugared.value), + }) + } + }), + arena, + ) + .into_bump_slice() +} + +fn desugar_field<'a>( + arena: &'a Bump, + field: &'a AssignedField<'a, Expr<'a>>, +) -> AssignedField<'a, Expr<'a>> { + use roc_parse::ast::AssignedField::*; + + match field { + RequiredValue(loc_str, spaces, loc_expr) => RequiredValue( + Loc { + value: loc_str.value, + region: loc_str.region, + }, + spaces, + desugar_expr(arena, loc_expr), + ), + OptionalValue(loc_str, spaces, loc_expr) => OptionalValue( + Loc { + value: loc_str.value, + region: loc_str.region, + }, + spaces, + desugar_expr(arena, loc_expr), + ), + LabelOnly(loc_str) => { + // Desugar { x } into { x: x } + let loc_expr = Loc { + value: Var { + module_name: "", + ident: loc_str.value, + }, + region: loc_str.region, + }; + + RequiredValue( + Loc { + value: loc_str.value, + region: loc_str.region, + }, + &[], + desugar_expr(arena, arena.alloc(loc_expr)), + ) + } + SpaceBefore(field, _spaces) => desugar_field(arena, field), + SpaceAfter(field, _spaces) => desugar_field(arena, field), + + Malformed(string) => Malformed(string), + } +} + +fn desugar_loc_patterns<'a>( + arena: &'a Bump, + loc_patterns: &'a [Loc>], +) -> &'a [Loc>] { + Vec::from_iter_in( + loc_patterns.iter().map(|loc_pattern| Loc { + region: loc_pattern.region, + value: desugar_pattern(arena, loc_pattern.value), + }), + arena, + ) + .into_bump_slice() +} + +fn desugar_loc_pattern<'a>( + arena: &'a Bump, + loc_pattern: &'a Loc>, +) -> &'a Loc> { + arena.alloc(Loc { + region: loc_pattern.region, + value: desugar_pattern(arena, loc_pattern.value), + }) +} + +fn desugar_pattern<'a>(arena: &'a Bump, pattern: Pattern<'a>) -> Pattern<'a> { + use roc_parse::ast::Pattern::*; + + match pattern { + Identifier(_) + | Tag(_) + | OpaqueRef(_) + | NumLiteral(_) + | NonBase10Literal { .. } + | FloatLiteral(_) + | StrLiteral(_) + | Underscore(_) + | SingleQuote(_) + | ListRest(_) + | Malformed(_) + | MalformedIdent(_, _) + | QualifiedIdentifier { .. } => pattern, + + Apply(tag, arg_patterns) => { + // Skip desugaring the tag, it should either be a Tag or OpaqueRef + let desugared_arg_patterns = Vec::from_iter_in( + arg_patterns.iter().map(|arg_pattern| Loc { + region: arg_pattern.region, + value: desugar_pattern(arena, arg_pattern.value), + }), + arena, + ) + .into_bump_slice(); + + Apply(tag, desugared_arg_patterns) + } + RecordDestructure(field_patterns) => { + RecordDestructure(field_patterns.map_items(arena, |field_pattern| Loc { + region: field_pattern.region, + value: desugar_pattern(arena, field_pattern.value), + })) + } + RequiredField(name, field_pattern) => { + RequiredField(name, desugar_loc_pattern(arena, field_pattern)) + } + OptionalField(name, expr) => OptionalField(name, desugar_expr(arena, expr)), + Tuple(patterns) => Tuple(patterns.map_items(arena, |elem_pattern| Loc { + region: elem_pattern.region, + value: desugar_pattern(arena, elem_pattern.value), + })), + List(patterns) => List(patterns.map_items(arena, |elem_pattern| Loc { + region: elem_pattern.region, + value: desugar_pattern(arena, elem_pattern.value), + })), + As(sub_pattern, symbol) => As(desugar_loc_pattern(arena, sub_pattern), symbol), + SpaceBefore(sub_pattern, _spaces) => desugar_pattern(arena, *sub_pattern), + SpaceAfter(sub_pattern, _spaces) => desugar_pattern(arena, *sub_pattern), + } +} + +struct RecordBuilderArg<'a> { + closure: &'a Loc>, + apply_exprs: Vec<'a, &'a Loc>>, +} + +fn record_builder_arg<'a>( + arena: &'a Bump, + region: Region, + fields: Collection<'a, Loc>>, +) -> RecordBuilderArg<'a> { + let mut record_fields = Vec::with_capacity_in(fields.len(), arena); + let mut apply_exprs = Vec::with_capacity_in(fields.len(), arena); + let mut apply_field_names = Vec::with_capacity_in(fields.len(), arena); + + // Build the record that the closure will return and gather apply expressions + + for field in fields.iter() { + let mut current = field.value; + + let new_field = loop { + match current { + RecordBuilderField::Value(label, spaces, expr) => { + break AssignedField::RequiredValue(label, spaces, expr) + } + RecordBuilderField::ApplyValue(label, _, _, expr) => { + apply_field_names.push(label); + apply_exprs.push(expr); + + let var = arena.alloc(Loc { + region: label.region, + value: Expr::Var { + module_name: "", + ident: arena.alloc("#".to_owned() + label.value), + }, + }); + + break AssignedField::RequiredValue(label, &[], var); + } + RecordBuilderField::LabelOnly(label) => break AssignedField::LabelOnly(label), + RecordBuilderField::SpaceBefore(sub_field, _) => { + current = *sub_field; + } + RecordBuilderField::SpaceAfter(sub_field, _) => { + current = *sub_field; + } + RecordBuilderField::Malformed(malformed) => { + break AssignedField::Malformed(malformed) + } + } + }; + + record_fields.push(Loc { + value: new_field, + region: field.region, + }); + } + + let record_fields = fields.replace_items(record_fields.into_bump_slice()); + + let mut body = arena.alloc(Loc { + value: Record(record_fields), + region, + }); + + // Construct the builder's closure + // + // { x: #x, y: #y, z: 3 } + // \#y -> { x: #x, y: #y, z: 3 } + // \#x -> \#y -> { x: #x, y: #y, z: 3 } + + for label in apply_field_names.iter().rev() { + let name = arena.alloc("#".to_owned() + label.value); + let ident = roc_parse::ast::Pattern::Identifier(name); + + let arg_pattern = arena.alloc(Loc { + value: ident, + region: label.region, + }); + + body = arena.alloc(Loc { + value: Closure(std::slice::from_ref(arg_pattern), body), + region, + }); + } + + RecordBuilderArg { + closure: body, + apply_exprs, + } +} + +// TODO move this desugaring to canonicalization, so we can use Symbols instead of strings +#[inline(always)] +fn binop_to_function(binop: BinOp) -> (&'static str, &'static str) { + use self::BinOp::*; + + match binop { + Caret => (ModuleName::NUM, "pow"), + Star => (ModuleName::NUM, "mul"), + Slash => (ModuleName::NUM, "div"), + DoubleSlash => (ModuleName::NUM, "divTrunc"), + Percent => (ModuleName::NUM, "rem"), + Plus => (ModuleName::NUM, "add"), + Minus => (ModuleName::NUM, "sub"), + Equals => (ModuleName::BOOL, "isEq"), + NotEquals => (ModuleName::BOOL, "isNotEq"), + LessThan => (ModuleName::NUM, "isLt"), + GreaterThan => (ModuleName::NUM, "isGt"), + LessThanOrEq => (ModuleName::NUM, "isLte"), + GreaterThanOrEq => (ModuleName::NUM, "isGte"), + And => (ModuleName::BOOL, "and"), + Or => (ModuleName::BOOL, "or"), + Pizza => unreachable!("Cannot desugar the |> operator"), + Assignment => unreachable!("Cannot desugar the = operator"), + IsAliasType => unreachable!("Cannot desugar the : operator"), + IsOpaqueType => unreachable!("Cannot desugar the := operator"), + Backpassing => unreachable!("Cannot desugar the <- operator"), + } +} + +fn desugar_bin_ops<'a>( + arena: &'a Bump, + whole_region: Region, + lefts: &'a [(Loc>, Loc)], + right: &'a Loc>, +) -> &'a Loc> { + let mut arg_stack: Vec<&'a Loc> = Vec::with_capacity_in(lefts.len() + 1, arena); + let mut op_stack: Vec> = Vec::with_capacity_in(lefts.len(), arena); + + for (loc_expr, loc_op) in lefts { + arg_stack.push(desugar_expr(arena, loc_expr)); + match run_binop_step(arena, whole_region, &mut arg_stack, &mut op_stack, *loc_op) { + Err(problem) => return problem, + Ok(()) => continue, + } + } + + let mut expr = desugar_expr(arena, right); + + for (left, loc_op) in arg_stack.into_iter().zip(op_stack.into_iter()).rev() { + expr = arena.alloc(new_op_call_expr(arena, left, loc_op, expr)); + } + + expr +} + +enum Step<'a> { + Error(&'a Loc>), + Push(Loc), + Skip, +} + +fn run_binop_step<'a>( + arena: &'a Bump, + whole_region: Region, + arg_stack: &mut Vec<&'a Loc>>, + op_stack: &mut Vec>, + next_op: Loc, +) -> Result<(), &'a Loc>> { + use Step::*; + + match binop_step(arena, whole_region, arg_stack, op_stack, next_op) { + Error(problem) => Err(problem), + Push(loc_op) => run_binop_step(arena, whole_region, arg_stack, op_stack, loc_op), + Skip => Ok(()), + } +} + +fn binop_step<'a>( + arena: &'a Bump, + whole_region: Region, + arg_stack: &mut Vec<&'a Loc>>, + op_stack: &mut Vec>, + next_op: Loc, +) -> Step<'a> { + use roc_module::called_via::Associativity::*; + use std::cmp::Ordering; + + match op_stack.pop() { + Some(stack_op) => { + match next_op.value.cmp(&stack_op.value) { + Ordering::Less => { + // Inline + let right = arg_stack.pop().unwrap(); + let left = arg_stack.pop().unwrap(); + + arg_stack.push(arena.alloc(new_op_call_expr(arena, left, stack_op, right))); + + Step::Push(next_op) + } + + Ordering::Greater => { + // Swap + op_stack.push(stack_op); + op_stack.push(next_op); + + Step::Skip + } + + Ordering::Equal => { + match ( + next_op.value.associativity(), + stack_op.value.associativity(), + ) { + (LeftAssociative, LeftAssociative) => { + // Inline + let right = arg_stack.pop().unwrap(); + let left = arg_stack.pop().unwrap(); + + arg_stack + .push(arena.alloc(new_op_call_expr(arena, left, stack_op, right))); + + Step::Push(next_op) + } + + (RightAssociative, RightAssociative) => { + // Swap + op_stack.push(stack_op); + op_stack.push(next_op); + + Step::Skip + } + + (NonAssociative, NonAssociative) => { + // Both operators were non-associative, e.g. (True == False == False). + // We should tell the author to disambiguate by grouping them with parens. + let bad_op = next_op; + let right = arg_stack.pop().unwrap(); + let left = arg_stack.pop().unwrap(); + let broken_expr = + arena.alloc(new_op_call_expr(arena, left, stack_op, right)); + let region = broken_expr.region; + let data = roc_parse::ast::PrecedenceConflict { + whole_region, + binop1_position: stack_op.region.start(), + binop1: stack_op.value, + binop2_position: bad_op.region.start(), + binop2: bad_op.value, + expr: arena.alloc(broken_expr), + }; + let value = Expr::PrecedenceConflict(arena.alloc(data)); + + Step::Error(arena.alloc(Loc { region, value })) + } + + _ => { + // The operators had the same precedence but different associativity. + // + // In many languages, this case can happen due to (for example) <| and |> having the same + // precedence but different associativity. Languages which support custom operators with + // (e.g. Haskell) can potentially have arbitrarily many of these cases. + // + // By design, Roc neither allows custom operators nor has any built-in operators with + // the same precedence and different associativity, so this should never happen! + internal_error!("BinOps had the same associativity, but different precedence. This should never happen!"); + } + } + } + } + } + None => { + op_stack.push(next_op); + Step::Skip + } + } +} diff --git a/crates/compiler/can/src/pattern.rs b/crates/compiler/can/src/pattern.rs new file mode 100644 index 0000000000..e0cf6de1ff --- /dev/null +++ b/crates/compiler/can/src/pattern.rs @@ -0,0 +1,1083 @@ +use crate::annotation::freshen_opaque_def; +use crate::env::Env; +use crate::expr::{canonicalize_expr, Expr, IntValue, Output}; +use crate::num::{ + finish_parsing_base, finish_parsing_float, finish_parsing_num, FloatBound, IntBound, NumBound, + ParsedNumResult, +}; +use crate::scope::{PendingAbilitiesInScope, Scope}; +use roc_exhaustive::ListArity; +use roc_module::ident::{Ident, Lowercase, TagName}; +use roc_module::symbol::Symbol; +use roc_parse::ast::{self, StrLiteral, StrSegment}; +use roc_parse::pattern::PatternType; +use roc_problem::can::{MalformedPatternProblem, Problem, RuntimeError, ShadowKind}; +use roc_region::all::{Loc, Region}; +use roc_types::num::SingleQuoteBound; +use roc_types::subs::{VarStore, Variable}; +use roc_types::types::{LambdaSet, OptAbleVar, PatternCategory, Type}; + +/// A pattern, including possible problems (e.g. shadowing) so that +/// codegen can generate a runtime error if this pattern is reached. +#[derive(Clone, Debug)] +pub enum Pattern { + Identifier(Symbol), + As(Box>, Symbol), + AppliedTag { + whole_var: Variable, + ext_var: Variable, + tag_name: TagName, + arguments: Vec<(Variable, Loc)>, + }, + UnwrappedOpaque { + whole_var: Variable, + opaque: Symbol, + argument: Box<(Variable, Loc)>, + + // The following help us link this opaque reference to the type specified by its + // definition, which we then use during constraint generation. For example + // suppose we have + // + // Id n := [Id U64 n] + // strToBool : Str -> Bool + // + // f = \@Id who -> strToBool who + // + // Then `opaque` is "Id", `argument` is "who", but this is not enough for us to + // infer the type of the expression as "Id Str" - we need to link the specialized type of + // the variable "n". + // That's what `specialized_def_type` and `type_arguments` are for; they are specialized + // for the expression from the opaque definition. `type_arguments` is something like + // [(n, fresh1)], and `specialized_def_type` becomes "[Id U64 fresh1]". + specialized_def_type: Box, + type_arguments: Vec, + lambda_set_variables: Vec, + }, + RecordDestructure { + whole_var: Variable, + ext_var: Variable, + destructs: Vec>, + }, + TupleDestructure { + whole_var: Variable, + ext_var: Variable, + destructs: Vec>, + }, + List { + list_var: Variable, + elem_var: Variable, + patterns: ListPatterns, + }, + NumLiteral(Variable, Box, IntValue, NumBound), + IntLiteral(Variable, Variable, Box, IntValue, IntBound), + FloatLiteral(Variable, Variable, Box, f64, FloatBound), + StrLiteral(Box), + SingleQuote(Variable, Variable, char, SingleQuoteBound), + Underscore, + + /// An identifier that marks a specialization of an ability member. + /// For example, given an ability member definition `hash : a -> U64 where a implements Hash`, + /// there may be the specialization `hash : Bool -> U64`. In this case we generate a + /// new symbol for the specialized "hash" identifier. + AbilityMemberSpecialization { + /// The symbol for this specialization. + ident: Symbol, + /// The ability name being specialized. + specializes: Symbol, + }, + + // Runtime Exceptions + Shadowed(Region, Loc, Symbol), + OpaqueNotInScope(Loc), + // Example: (5 = 1 + 2) is an unsupported pattern in an assignment; Int patterns aren't allowed in assignments! + UnsupportedPattern(Region), + // parse error patterns + MalformedPattern(MalformedPatternProblem, Region), +} + +impl Pattern { + pub fn opt_var(&self) -> Option { + use Pattern::*; + match self { + Identifier(_) => None, + As(pattern, _) => pattern.value.opt_var(), + + AppliedTag { whole_var, .. } => Some(*whole_var), + UnwrappedOpaque { whole_var, .. } => Some(*whole_var), + RecordDestructure { whole_var, .. } => Some(*whole_var), + TupleDestructure { whole_var, .. } => Some(*whole_var), + List { + list_var: whole_var, + .. + } => Some(*whole_var), + NumLiteral(var, ..) => Some(*var), + IntLiteral(var, ..) => Some(*var), + FloatLiteral(var, ..) => Some(*var), + StrLiteral(_) => None, + SingleQuote(..) => None, + Underscore => None, + + AbilityMemberSpecialization { .. } => None, + + Shadowed(..) | OpaqueNotInScope(..) | UnsupportedPattern(..) | MalformedPattern(..) => { + None + } + } + } + + /// Is this pattern sure to cover all instances of a type T, assuming it typechecks against T? + pub fn surely_exhaustive(&self) -> bool { + use Pattern::*; + match self { + Identifier(..) + | Underscore + | Shadowed(..) + | OpaqueNotInScope(..) + | UnsupportedPattern(..) + | MalformedPattern(..) + | AbilityMemberSpecialization { .. } => true, + + RecordDestructure { destructs, .. } => { + // If all destructs are surely exhaustive, then this is surely exhaustive. + destructs.iter().all(|d| match &d.value.typ { + DestructType::Required | DestructType::Optional(_, _) => true, + DestructType::Guard(_, pat) => pat.value.surely_exhaustive(), + }) + } + TupleDestructure { destructs, .. } => { + // If all destructs are surely exhaustive, then this is surely exhaustive. + destructs + .iter() + .all(|d| d.value.typ.1.value.surely_exhaustive()) + } + + As(pattern, _identifier) => pattern.value.surely_exhaustive(), + List { patterns, .. } => patterns.surely_exhaustive(), + AppliedTag { .. } + | NumLiteral(..) + | IntLiteral(..) + | FloatLiteral(..) + | StrLiteral(..) + | SingleQuote(..) => false, + UnwrappedOpaque { argument, .. } => { + // Opaques can only match against one constructor (the opaque symbol), so this is + // surely exhaustive against T if the inner pattern is surely exhaustive against + // its type U. + argument.1.value.surely_exhaustive() + } + } + } + + pub fn category(&self) -> PatternCategory { + use Pattern::*; + use PatternCategory as C; + + match self { + Identifier(_) => C::PatternDefault, + As(pattern, _) => pattern.value.category(), + + AppliedTag { tag_name, .. } => C::Ctor(tag_name.clone()), + UnwrappedOpaque { opaque, .. } => C::Opaque(*opaque), + RecordDestructure { destructs, .. } if destructs.is_empty() => C::EmptyRecord, + RecordDestructure { .. } => C::Record, + TupleDestructure { .. } => C::Tuple, + List { .. } => C::List, + NumLiteral(..) => C::Num, + IntLiteral(..) => C::Int, + FloatLiteral(..) => C::Float, + StrLiteral(_) => C::Str, + SingleQuote(..) => C::Character, + Underscore => C::PatternDefault, + + AbilityMemberSpecialization { .. } => C::PatternDefault, + + Shadowed(..) | OpaqueNotInScope(..) | UnsupportedPattern(..) | MalformedPattern(..) => { + C::PatternDefault + } + } + } +} + +#[derive(Clone, Debug)] +pub struct ListPatterns { + pub patterns: Vec>, + /// Where a rest pattern splits patterns before and after it, if it does at all. + /// If present, patterns at index >= the rest index appear after the rest pattern. + /// For example: + /// [ .., A, B ] -> patterns = [A, B], rest = 0 + /// [ A, .., B ] -> patterns = [A, B], rest = 1 + /// [ A, B, .. ] -> patterns = [A, B], rest = 2 + pub opt_rest: Option<(usize, Option)>, +} + +impl ListPatterns { + /// Is this list pattern the trivially-exhaustive pattern `[..]`? + fn surely_exhaustive(&self) -> bool { + self.patterns.is_empty() && matches!(self.opt_rest, Some((0, _))) + } + + pub fn arity(&self) -> ListArity { + match self.opt_rest { + Some((i, _)) => { + let before = i; + let after = self.patterns.len() - before; + ListArity::Slice(before, after) + } + None => ListArity::Exact(self.patterns.len()), + } + } +} + +#[derive(Clone, Debug)] +pub struct RecordDestruct { + pub var: Variable, + pub label: Lowercase, + pub symbol: Symbol, + pub typ: DestructType, +} + +#[derive(Clone, Debug)] +pub struct TupleDestruct { + pub var: Variable, + pub destruct_index: usize, + pub typ: (Variable, Loc), +} + +#[derive(Clone, Debug)] +pub enum DestructType { + Required, + Optional(Variable, Loc), + Guard(Variable, Loc), +} + +#[allow(clippy::too_many_arguments)] +pub fn canonicalize_def_header_pattern<'a>( + env: &mut Env<'a>, + var_store: &mut VarStore, + scope: &mut Scope, + pending_abilities_in_scope: &PendingAbilitiesInScope, + output: &mut Output, + pattern_type: PatternType, + pattern: &ast::Pattern<'a>, + region: Region, +) -> Loc { + use roc_parse::ast::Pattern::*; + + match pattern { + // Identifiers that shadow ability members may appear (and may only appear) at the header of a def. + Identifier(name) => { + match scope.introduce_or_shadow_ability_member( + pending_abilities_in_scope, + (*name).into(), + region, + ) { + Ok((symbol, shadowing_ability_member)) => { + let can_pattern = match shadowing_ability_member { + // A fresh identifier. + None => { + output.references.insert_bound(symbol); + Pattern::Identifier(symbol) + } + // Likely a specialization of an ability. + Some(ability_member_name) => { + output.references.insert_bound(symbol); + Pattern::AbilityMemberSpecialization { + ident: symbol, + specializes: ability_member_name, + } + } + }; + Loc::at(region, can_pattern) + } + Err((original_region, shadow, new_symbol)) => { + env.problem(Problem::RuntimeError(RuntimeError::Shadowing { + original_region, + shadow: shadow.clone(), + kind: ShadowKind::Variable, + })); + output.references.insert_bound(new_symbol); + + let can_pattern = Pattern::Shadowed(original_region, shadow, new_symbol); + Loc::at(region, can_pattern) + } + } + } + _ => canonicalize_pattern( + env, + var_store, + scope, + output, + pattern_type, + pattern, + region, + PermitShadows(false), + ), + } +} + +/// Allow binding of symbols that appear shadowed. +/// +/// For example, in the branch `A x | B x -> ...`, both pattern bind `x`; that's not a shadow! +#[derive(PartialEq, Eq, Clone, Copy)] +pub struct PermitShadows(pub bool); + +fn canonicalize_pattern_symbol( + env: &mut Env, + scope: &mut Scope, + output: &mut Output, + region: Region, + permit_shadows: PermitShadows, + name: &str, +) -> Result { + match scope.introduce_str(name, region) { + Ok(symbol) => { + output.references.insert_bound(symbol); + + Ok(symbol) + } + Err((shadowed_symbol, shadow, new_symbol)) => { + if permit_shadows.0 { + output.references.insert_bound(shadowed_symbol.value); + + Ok(shadowed_symbol.value) + } else { + env.problem(Problem::RuntimeError(RuntimeError::Shadowing { + original_region: shadowed_symbol.region, + shadow: shadow.clone(), + kind: ShadowKind::Variable, + })); + output.references.insert_bound(new_symbol); + + Err(Pattern::Shadowed( + shadowed_symbol.region, + shadow, + new_symbol, + )) + } + } + } +} + +#[allow(clippy::too_many_arguments)] +pub fn canonicalize_pattern<'a>( + env: &mut Env<'a>, + var_store: &mut VarStore, + scope: &mut Scope, + output: &mut Output, + pattern_type: PatternType, + pattern: &ast::Pattern<'a>, + region: Region, + permit_shadows: PermitShadows, +) -> Loc { + use roc_parse::ast::Pattern::*; + use PatternType::*; + + let can_pattern = match pattern { + Identifier(name) => { + match canonicalize_pattern_symbol(env, scope, output, region, permit_shadows, name) { + Ok(symbol) => Pattern::Identifier(symbol), + Err(pattern) => pattern, + } + } + Underscore(name) => { + // An underscored identifier can't be used, but we'll still add it to the scope + // for better error messages if someone tries to use it. + scope.introduce_ignored_local(name, region); + Pattern::Underscore + } + Tag(name) => { + // Canonicalize the tag's name. + Pattern::AppliedTag { + whole_var: var_store.fresh(), + ext_var: var_store.fresh(), + tag_name: TagName((*name).into()), + arguments: vec![], + } + } + OpaqueRef(name) => { + // If this opaque ref had an argument, we would be in the "Apply" branch. + let loc_name = Loc::at(region, (*name).into()); + env.problem(Problem::RuntimeError(RuntimeError::OpaqueNotApplied( + loc_name, + ))); + Pattern::UnsupportedPattern(region) + } + Apply(tag, patterns) => { + let mut can_patterns = Vec::with_capacity(patterns.len()); + for loc_pattern in *patterns { + let can_pattern = canonicalize_pattern( + env, + var_store, + scope, + output, + pattern_type, + &loc_pattern.value, + loc_pattern.region, + permit_shadows, + ); + + can_patterns.push((var_store.fresh(), can_pattern)); + } + + match tag.value { + Tag(name) => { + let tag_name = TagName(name.into()); + Pattern::AppliedTag { + whole_var: var_store.fresh(), + ext_var: var_store.fresh(), + tag_name, + arguments: can_patterns, + } + } + + OpaqueRef(name) => match scope.lookup_opaque_ref(name, tag.region) { + Ok((opaque, opaque_def)) => { + debug_assert!(!can_patterns.is_empty()); + + if can_patterns.len() > 1 { + env.problem(Problem::RuntimeError( + RuntimeError::OpaqueAppliedToMultipleArgs(region), + )); + + Pattern::UnsupportedPattern(region) + } else { + let argument = Box::new(can_patterns.pop().unwrap()); + + let (type_arguments, lambda_set_variables, specialized_def_type) = + freshen_opaque_def(var_store, opaque_def); + + output.references.insert_type_lookup(opaque); + + Pattern::UnwrappedOpaque { + whole_var: var_store.fresh(), + opaque, + argument, + specialized_def_type: Box::new(specialized_def_type), + type_arguments, + lambda_set_variables, + } + } + } + Err(runtime_error) => { + env.problem(Problem::RuntimeError(runtime_error)); + + Pattern::OpaqueNotInScope(Loc::at(tag.region, name.into())) + } + }, + _ => unreachable!("Other patterns cannot be applied"), + } + } + + &FloatLiteral(str) => match pattern_type { + WhenBranch => match finish_parsing_float(str) { + Err(_error) => { + let problem = MalformedPatternProblem::MalformedFloat; + malformed_pattern(env, problem, region) + } + Ok((str_without_suffix, float, bound)) => Pattern::FloatLiteral( + var_store.fresh(), + var_store.fresh(), + str_without_suffix.into(), + float, + bound, + ), + }, + ptype => unsupported_pattern(env, ptype, region), + }, + + &NumLiteral(str) => match pattern_type { + WhenBranch => match finish_parsing_num(str) { + Err(_error) => { + let problem = MalformedPatternProblem::MalformedInt; + malformed_pattern(env, problem, region) + } + Ok((parsed, ParsedNumResult::UnknownNum(int, bound))) => { + Pattern::NumLiteral(var_store.fresh(), (parsed).into(), int, bound) + } + Ok((parsed, ParsedNumResult::Int(int, bound))) => Pattern::IntLiteral( + var_store.fresh(), + var_store.fresh(), + (parsed).into(), + int, + bound, + ), + Ok((parsed, ParsedNumResult::Float(float, bound))) => Pattern::FloatLiteral( + var_store.fresh(), + var_store.fresh(), + (parsed).into(), + float, + bound, + ), + }, + ptype => unsupported_pattern(env, ptype, region), + }, + + &NonBase10Literal { + string, + base, + is_negative, + } => match pattern_type { + WhenBranch => match finish_parsing_base(string, base, is_negative) { + Err(_error) => { + let problem = MalformedPatternProblem::MalformedBase(base); + malformed_pattern(env, problem, region) + } + Ok((IntValue::U128(_), _)) if is_negative => { + // Can't negate a u128; that doesn't fit in any integer literal type we support. + let problem = MalformedPatternProblem::MalformedInt; + malformed_pattern(env, problem, region) + } + Ok((int, bound)) => { + use std::ops::Neg; + + let sign_str = if is_negative { "-" } else { "" }; + let int_str = format!("{sign_str}{int}").into_boxed_str(); + let i = match int { + // Safety: this is fine because I128::MAX = |I128::MIN| - 1 + IntValue::I128(n) if is_negative => { + IntValue::I128(i128::from_ne_bytes(n).neg().to_ne_bytes()) + } + IntValue::I128(n) => IntValue::I128(n), + IntValue::U128(_) => unreachable!(), + }; + Pattern::IntLiteral(var_store.fresh(), var_store.fresh(), int_str, i, bound) + } + }, + ptype => unsupported_pattern(env, ptype, region), + }, + + StrLiteral(literal) => match pattern_type { + WhenBranch => flatten_str_literal(literal), + ptype => unsupported_pattern(env, ptype, region), + }, + + SingleQuote(string) => { + let mut it = string.chars().peekable(); + if let Some(char) = it.next() { + if it.peek().is_none() { + Pattern::SingleQuote( + var_store.fresh(), + var_store.fresh(), + char, + SingleQuoteBound::from_char(char), + ) + } else { + // multiple chars is found + let problem = MalformedPatternProblem::MultipleCharsInSingleQuote; + malformed_pattern(env, problem, region) + } + } else { + // no characters found + let problem = MalformedPatternProblem::EmptySingleQuote; + malformed_pattern(env, problem, region) + } + } + + SpaceBefore(sub_pattern, _) | SpaceAfter(sub_pattern, _) => { + return canonicalize_pattern( + env, + var_store, + scope, + output, + pattern_type, + sub_pattern, + region, + permit_shadows, + ) + } + + Tuple(patterns) => { + let ext_var = var_store.fresh(); + let whole_var = var_store.fresh(); + let mut destructs = Vec::with_capacity(patterns.len()); + + for (i, loc_pattern) in patterns.iter().enumerate() { + let can_guard = canonicalize_pattern( + env, + var_store, + scope, + output, + pattern_type, + &loc_pattern.value, + loc_pattern.region, + permit_shadows, + ); + + destructs.push(Loc { + region: loc_pattern.region, + value: TupleDestruct { + destruct_index: i, + var: var_store.fresh(), + typ: (var_store.fresh(), can_guard), + }, + }); + } + + Pattern::TupleDestructure { + whole_var, + ext_var, + destructs, + } + } + + RecordDestructure(patterns) => { + let ext_var = var_store.fresh(); + let whole_var = var_store.fresh(); + let mut destructs = Vec::with_capacity(patterns.len()); + let mut opt_erroneous = None; + + for loc_pattern in patterns.iter() { + match loc_pattern.value { + Identifier(label) => { + match scope.introduce(label.into(), region) { + Ok(symbol) => { + output.references.insert_bound(symbol); + + destructs.push(Loc { + region: loc_pattern.region, + value: RecordDestruct { + var: var_store.fresh(), + label: Lowercase::from(label), + symbol, + typ: DestructType::Required, + }, + }); + } + Err((shadowed_symbol, shadow, new_symbol)) => { + env.problem(Problem::RuntimeError(RuntimeError::Shadowing { + original_region: shadowed_symbol.region, + shadow: shadow.clone(), + kind: ShadowKind::Variable, + })); + + // No matter what the other patterns + // are, we're definitely shadowed and will + // get a runtime exception as soon as we + // encounter the first bad pattern. + opt_erroneous = Some(Pattern::Shadowed( + shadowed_symbol.region, + shadow, + new_symbol, + )); + } + }; + } + + RequiredField(label, loc_guard) => { + // a guard does not introduce the label into scope! + let symbol = + scope.scopeless_symbol(&Ident::from(label), loc_pattern.region); + let can_guard = canonicalize_pattern( + env, + var_store, + scope, + output, + pattern_type, + &loc_guard.value, + loc_guard.region, + permit_shadows, + ); + + destructs.push(Loc { + region: loc_pattern.region, + value: RecordDestruct { + var: var_store.fresh(), + label: Lowercase::from(label), + symbol, + typ: DestructType::Guard(var_store.fresh(), can_guard), + }, + }); + } + OptionalField(label, loc_default) => { + // an optional DOES introduce the label into scope! + match scope.introduce(label.into(), region) { + Ok(symbol) => { + let (can_default, expr_output) = canonicalize_expr( + env, + var_store, + scope, + loc_default.region, + &loc_default.value, + ); + + // an optional field binds the symbol! + output.references.insert_bound(symbol); + + output.union(expr_output); + + destructs.push(Loc { + region: loc_pattern.region, + value: RecordDestruct { + var: var_store.fresh(), + label: Lowercase::from(label), + symbol, + typ: DestructType::Optional(var_store.fresh(), can_default), + }, + }); + } + Err((shadowed_symbol, shadow, new_symbol)) => { + env.problem(Problem::RuntimeError(RuntimeError::Shadowing { + original_region: shadowed_symbol.region, + shadow: shadow.clone(), + kind: ShadowKind::Variable, + })); + + // No matter what the other patterns + // are, we're definitely shadowed and will + // get a runtime exception as soon as we + // encounter the first bad pattern. + opt_erroneous = Some(Pattern::Shadowed( + shadowed_symbol.region, + shadow, + new_symbol, + )); + } + }; + } + _ => unreachable!("Any other pattern should have given a parse error"), + } + } + + // If we encountered an erroneous pattern (e.g. one with shadowing), + // use the resulting RuntimeError. Otherwise, return a successful record destructure. + opt_erroneous.unwrap_or(Pattern::RecordDestructure { + whole_var, + ext_var, + destructs, + }) + } + + RequiredField(_name, _loc_pattern) => { + unreachable!("should have been handled in RecordDestructure"); + } + OptionalField(_name, _loc_pattern) => { + unreachable!("should have been handled in RecordDestructure"); + } + + List(patterns) => { + // We want to admit the following cases: + // + // [] + // [..] + // [.., P_1,* P_n] + // [P_1,* P_n, ..] + // [P_1,* P_m, .., P_n,* P_q] + // [P_1,* P_n] + // + // So, a list-rest pattern can appear anywhere in a list pattern, but can appear at + // most once. + let elem_var = var_store.fresh(); + let list_var = var_store.fresh(); + + let mut rest_index = None; + let mut rest_name = None; + let mut can_pats = Vec::with_capacity(patterns.len()); + let mut opt_erroneous = None; + + for (i, loc_pattern) in patterns.iter().enumerate() { + match &loc_pattern.value { + ListRest(opt_pattern_as) => match rest_index { + None => { + rest_index = Some(i); + + if let Some((_, pattern_as)) = opt_pattern_as { + match canonicalize_pattern_symbol( + env, + scope, + output, + region, + permit_shadows, + pattern_as.identifier.value, + ) { + Ok(symbol) => { + rest_name = Some(symbol); + } + Err(pattern) => { + opt_erroneous = Some(pattern); + } + } + } + } + Some(_) => { + env.problem(Problem::MultipleListRestPattern { + region: loc_pattern.region, + }); + + opt_erroneous = Some(Pattern::MalformedPattern( + MalformedPatternProblem::DuplicateListRestPattern, + loc_pattern.region, + )); + } + }, + pattern => { + let pat = canonicalize_pattern( + env, + var_store, + scope, + output, + pattern_type, + pattern, + loc_pattern.region, + permit_shadows, + ); + can_pats.push(pat); + } + } + } + + // If we encountered an erroneous pattern (e.g. one with shadowing), + // use the resulting RuntimeError. Otherwise, return a successful record destructure. + opt_erroneous.unwrap_or(Pattern::List { + list_var, + elem_var, + patterns: ListPatterns { + patterns: can_pats, + opt_rest: rest_index.map(|i| (i, rest_name)), + }, + }) + } + ListRest(_opt_pattern_as) => { + // Parsing should make sure these only appear in list patterns, where we will generate + // better contextual errors. + let problem = MalformedPatternProblem::Unknown; + malformed_pattern(env, problem, region) + } + + As(loc_pattern, pattern_as) => { + let can_subpattern = canonicalize_pattern( + env, + var_store, + scope, + output, + pattern_type, + &loc_pattern.value, + loc_pattern.region, + permit_shadows, + ); + + match canonicalize_pattern_symbol( + env, + scope, + output, + region, + permit_shadows, + pattern_as.identifier.value, + ) { + Ok(symbol) => Pattern::As(Box::new(can_subpattern), symbol), + Err(pattern) => pattern, + } + } + + Malformed(_str) => { + let problem = MalformedPatternProblem::Unknown; + malformed_pattern(env, problem, region) + } + + MalformedIdent(_str, problem) => { + let problem = MalformedPatternProblem::BadIdent(*problem); + malformed_pattern(env, problem, region) + } + + QualifiedIdentifier { .. } => { + let problem = MalformedPatternProblem::QualifiedIdentifier; + malformed_pattern(env, problem, region) + } + }; + + Loc { + region, + value: can_pattern, + } +} + +/// When we detect an unsupported pattern type (e.g. 5 = 1 + 2 is unsupported because you can't +/// assign to Int patterns), report it to Env and return an UnsupportedPattern runtime error pattern. +fn unsupported_pattern(env: &mut Env, pattern_type: PatternType, region: Region) -> Pattern { + use roc_problem::can::BadPattern; + env.problem(Problem::UnsupportedPattern( + BadPattern::Unsupported(pattern_type), + region, + )); + + Pattern::UnsupportedPattern(region) +} + +/// When we detect a malformed pattern like `3.X` or `0b5`, +/// report it to Env and return an UnsupportedPattern runtime error pattern. +fn malformed_pattern(env: &mut Env, problem: MalformedPatternProblem, region: Region) -> Pattern { + env.problem(Problem::RuntimeError(RuntimeError::MalformedPattern( + problem, region, + ))); + + Pattern::MalformedPattern(problem, region) +} + +/// An iterator over the bindings made by a pattern. +/// +/// We attempt to make no allocations when we can. +pub enum BindingsFromPattern<'a> { + Empty, + One(&'a Loc), + Many(Vec>), +} + +pub enum BindingsFromPatternWork<'a> { + Pattern(&'a Loc), + RecordDestruct(&'a Loc), + TupleDestruct(&'a Loc), +} + +impl<'a> BindingsFromPattern<'a> { + pub fn new(initial: &'a Loc) -> Self { + Self::One(initial) + } + + pub fn new_many(mut it: I) -> Self + where + I: Iterator>, + { + if let (1, Some(1)) = it.size_hint() { + Self::new(it.next().unwrap()) + } else { + Self::Many(it.map(BindingsFromPatternWork::Pattern).collect()) + } + } + + fn next_many(stack: &mut Vec>) -> Option<(Symbol, Region)> { + use Pattern::*; + + while let Some(work) = stack.pop() { + match work { + BindingsFromPatternWork::Pattern(loc_pattern) => { + use BindingsFromPatternWork::*; + + match &loc_pattern.value { + Identifier(symbol) + | AbilityMemberSpecialization { + ident: symbol, + specializes: _, + } => { + return Some((*symbol, loc_pattern.region)); + } + As(pattern, symbol) => { + stack.push(Pattern(pattern)); + return Some((*symbol, loc_pattern.region)); + } + AppliedTag { + arguments: loc_args, + .. + } => { + let it = loc_args.iter().rev().map(|(_, p)| Pattern(p)); + stack.extend(it); + } + UnwrappedOpaque { argument, .. } => { + let (_, loc_arg) = &**argument; + stack.push(Pattern(loc_arg)); + } + TupleDestructure { destructs, .. } => { + let it = destructs.iter().rev().map(TupleDestruct); + stack.extend(it); + } + RecordDestructure { destructs, .. } => { + let it = destructs.iter().rev().map(RecordDestruct); + stack.extend(it); + } + NumLiteral(..) + | IntLiteral(..) + | FloatLiteral(..) + | StrLiteral(_) + | SingleQuote(..) + | Underscore + | Shadowed(_, _, _) + | MalformedPattern(_, _) + | UnsupportedPattern(_) + | OpaqueNotInScope(..) => (), + List { patterns, .. } => { + stack.extend(patterns.patterns.iter().rev().map(Pattern)); + } + } + } + BindingsFromPatternWork::RecordDestruct(loc_destruct) => { + match &loc_destruct.value.typ { + DestructType::Required | DestructType::Optional(_, _) => { + return Some((loc_destruct.value.symbol, loc_destruct.region)); + } + DestructType::Guard(_, inner) => { + // a guard does not introduce the symbol + stack.push(BindingsFromPatternWork::Pattern(inner)) + } + } + } + BindingsFromPatternWork::TupleDestruct(loc_destruct) => { + let inner = &loc_destruct.value.typ.1; + stack.push(BindingsFromPatternWork::Pattern(inner)) + } + } + } + + None + } +} + +impl<'a> Iterator for BindingsFromPattern<'a> { + type Item = (Symbol, Region); + + fn next(&mut self) -> Option { + use Pattern::*; + + match self { + BindingsFromPattern::Empty => None, + BindingsFromPattern::One(loc_pattern) => match &loc_pattern.value { + Identifier(symbol) + | AbilityMemberSpecialization { + ident: symbol, + specializes: _, + } => { + let region = loc_pattern.region; + *self = Self::Empty; + Some((*symbol, region)) + } + _ => { + *self = Self::Many(vec![BindingsFromPatternWork::Pattern(loc_pattern)]); + self.next() + } + }, + BindingsFromPattern::Many(stack) => Self::next_many(stack), + } + } +} + +fn flatten_str_literal(literal: &StrLiteral<'_>) -> Pattern { + use ast::StrLiteral::*; + + match literal { + PlainLine(str_slice) => Pattern::StrLiteral((*str_slice).into()), + Line(segments) => flatten_str_lines(&[segments]), + Block(lines) => flatten_str_lines(lines), + } +} + +fn flatten_str_lines(lines: &[&[StrSegment<'_>]]) -> Pattern { + use StrSegment::*; + + let mut buf = String::new(); + + for line in lines { + for segment in line.iter() { + match segment { + Plaintext(string) => { + buf.push_str(string); + } + Unicode(loc_digits) => { + todo!("parse unicode digits {:?}", loc_digits); + } + Interpolated(loc_expr) => { + return Pattern::UnsupportedPattern(loc_expr.region); + } + EscapedChar(escaped) => buf.push(escaped.unescape()), + } + } + } + + Pattern::StrLiteral(buf.into()) +} diff --git a/compiler/can/src/procedure.rs b/crates/compiler/can/src/procedure.rs similarity index 100% rename from compiler/can/src/procedure.rs rename to crates/compiler/can/src/procedure.rs diff --git a/crates/compiler/can/src/scope.rs b/crates/compiler/can/src/scope.rs new file mode 100644 index 0000000000..b427f869a5 --- /dev/null +++ b/crates/compiler/can/src/scope.rs @@ -0,0 +1,859 @@ +use roc_collections::{VecMap, VecSet}; +use roc_error_macros::internal_error; +use roc_module::ident::Ident; +use roc_module::symbol::{IdentId, IdentIds, ModuleId, Symbol}; +use roc_problem::can::RuntimeError; +use roc_region::all::{Loc, Region}; +use roc_types::subs::Variable; +use roc_types::types::{Alias, AliasKind, AliasVar, Type}; + +use crate::abilities::PendingAbilitiesStore; + +use bitvec::vec::BitVec; + +// ability -> member names +pub(crate) type PendingAbilitiesInScope = VecMap>; + +#[derive(Clone, Debug)] +pub struct Scope { + /// The type aliases currently in scope + pub aliases: VecMap, + + /// The abilities currently in scope, and their implementors. + pub abilities_store: PendingAbilitiesStore, + + /// The current module being processed. This will be used to turn + /// unqualified idents into Symbols. + home: ModuleId, + + /// The first `exposed_ident_count` identifiers are exposed + exposed_ident_count: usize, + + /// Identifiers that are imported (and introduced in the header) + imports: Vec<(Ident, Symbol, Region)>, + + /// Shadows of an ability member, for example a local specialization of `eq` for the ability + /// member `Eq implements eq : a, a -> Bool where a implements Eq` gets a shadow symbol it can use for its + /// implementation. + /// + /// Only one shadow of an ability member is permitted per scope. + shadows: VecMap>, + + /// Identifiers that are in scope, and defined in the current module + pub locals: ScopedIdentIds, + + /// Ignored variables (variables that start with an underscore). + /// We won't intern them because they're only used during canonicalization for error reporting. + ignored_locals: VecMap, +} + +impl Scope { + pub fn new( + home: ModuleId, + initial_ident_ids: IdentIds, + starting_abilities_store: PendingAbilitiesStore, + ) -> Scope { + let default_imports = + // Add all `Apply` types. + (Symbol::apply_types_in_scope().into_iter()) + // Add all tag names we might want to suggest as hints in error messages. + .chain(Symbol::symbols_in_scope_for_hints()); + + let default_imports = default_imports.map(|(a, (b, c))| (a, b, c)).collect(); + + Scope { + home, + exposed_ident_count: initial_ident_ids.len(), + locals: ScopedIdentIds::from_ident_ids(home, initial_ident_ids), + aliases: VecMap::default(), + abilities_store: starting_abilities_store, + shadows: VecMap::default(), + imports: default_imports, + ignored_locals: VecMap::default(), + } + } + + pub fn lookup(&self, ident: &Ident, region: Region) -> Result { + self.lookup_str(ident.as_str(), region) + } + + pub fn lookup_ability_member_shadow(&self, member: Symbol) -> Option { + self.shadows.get(&member).map(|loc_shadow| loc_shadow.value) + } + + pub fn add_docs_imports(&mut self) { + self.imports + .push(("Dict".into(), Symbol::DICT_DICT, Region::zero())); + self.imports + .push(("Set".into(), Symbol::SET_SET, Region::zero())); + } + + pub fn lookup_str(&self, ident: &str, region: Region) -> Result { + use ContainsIdent::*; + + match self.scope_contains_ident(ident) { + InScope(symbol, _) => Ok(symbol), + NotInScope(_) | NotPresent => { + // identifier not found + + let error = RuntimeError::LookupNotInScope { + loc_name: Loc { + region, + value: Ident::from(ident), + }, + suggestion_options: self.idents_in_scope().map(|v| v.as_ref().into()).collect(), + // Check if the user just forgot to remove an underscore from an ignored identifier + underscored_suggestion_region: self.lookup_ignored_local(ident), + }; + + Err(error) + } + } + } + + fn idents_in_scope(&self) -> impl Iterator + '_ { + let it1 = self.locals.idents_in_scope(); + let it2 = self.imports.iter().map(|t| t.0.clone()); + + it2.chain(it1) + } + + /// Check if there is an opaque type alias referenced by `opaque_ref` referenced in the + /// current scope. E.g. `@Age` must reference an opaque `Age` declared in this module, not any + /// other! + pub fn lookup_opaque_ref( + &self, + opaque_ref: &str, + lookup_region: Region, + ) -> Result<(Symbol, &Alias), RuntimeError> { + debug_assert!(opaque_ref.starts_with('@')); + let opaque_str = &opaque_ref[1..]; + let opaque = opaque_str.into(); + + match self.locals.has_in_scope(&opaque) { + Some((symbol, _)) => match self.lookup_opaque_alias(symbol) { + Ok(alias) => Ok((symbol, alias)), + Err(opt_alias_def_region) => { + Err(self.opaque_not_defined_error(opaque, lookup_region, opt_alias_def_region)) + } + }, + None => { + // opaque types can only be wrapped/unwrapped in the scope they are defined in (and below) + let error = if let Some((_, decl_region)) = self.has_imported(opaque_str) { + // specific error for when the opaque is imported, which definitely does not work + RuntimeError::OpaqueOutsideScope { + opaque, + referenced_region: lookup_region, + imported_region: decl_region, + } + } else { + self.opaque_not_defined_error(opaque, lookup_region, None) + }; + + Err(error) + } + } + } + + fn lookup_opaque_alias(&self, symbol: Symbol) -> Result<&Alias, Option> { + match self.aliases.get(&symbol) { + None => Err(None), + + Some(alias) => match alias.kind { + AliasKind::Opaque => Ok(alias), + AliasKind::Structural => Err(Some(alias.header_region())), + }, + } + } + + fn is_opaque(&self, ident_id: IdentId, string: &str) -> Option> { + if string.is_empty() { + return None; + } + + let symbol = Symbol::new(self.home, ident_id); + + if let Some(AliasKind::Opaque) = self.aliases.get(&symbol).map(|alias| alias.kind) { + Some(string.into()) + } else { + None + } + } + + fn opaque_not_defined_error( + &self, + opaque: Ident, + lookup_region: Region, + opt_defined_alias: Option, + ) -> RuntimeError { + // for opaques, we only look at the locals because opaques can only be matched + // on in the module that defines them. + let opaques_in_scope = self + .locals + .ident_ids + .ident_strs() + .filter_map(|(ident_id, string)| self.is_opaque(ident_id, string)) + .collect(); + + RuntimeError::OpaqueNotDefined { + usage: Loc::at(lookup_region, opaque), + opaques_in_scope, + opt_defined_alias, + } + } + + fn has_imported(&self, ident: &str) -> Option<(Symbol, Region)> { + for (import, shadow, original_region) in self.imports.iter() { + if ident == import.as_str() { + return Some((*shadow, *original_region)); + } + } + + None + } + + /// Is an identifier in scope, either in the locals or imports + fn scope_contains_ident(&self, ident: &str) -> ContainsIdent { + // exposed imports are likely to be small + match self.has_imported(ident) { + Some((symbol, region)) => ContainsIdent::InScope(symbol, region), + None => self.locals.contains_ident(ident), + } + } + + fn introduce_help(&mut self, ident: &str, region: Region) -> Result { + match self.scope_contains_ident(ident) { + ContainsIdent::InScope(original_symbol, original_region) => { + // the ident is already in scope; up to the caller how to handle that + // (usually it's shadowing, but it is valid to shadow ability members) + Err((original_symbol, original_region)) + } + ContainsIdent::NotPresent => { + // We know nothing about this ident yet; introduce it to the scope + let ident_id = self.locals.introduce_into_scope(ident, region); + Ok(Symbol::new(self.home, ident_id)) + } + ContainsIdent::NotInScope(existing) => { + // The ident is not in scope, but its name is already in the string interner + if existing.index() < self.exposed_ident_count { + // if the identifier is exposed, use the IdentId we already have for it + // other modules depend on the symbol having that IdentId + let symbol = Symbol::new(self.home, existing); + + self.locals.in_scope.set(existing.index(), true); + self.locals.regions[existing.index()] = region; + + Ok(symbol) + } else { + // create a new IdentId that under the hood uses the same string bytes as an existing one + let ident_id = self.locals.introduce_into_scope_duplicate(existing, region); + + Ok(Symbol::new(self.home, ident_id)) + } + } + } + } + + /// Introduce a new ident to scope. + /// + /// Returns Err if this would shadow an existing ident, including the + /// Symbol and Region of the ident we already had in scope under that name. + /// + /// If this ident shadows an existing one, a new ident is allocated for the shadow. This is + /// done so that all identifiers have unique symbols, which is important in particular when + /// we generate code for value identifiers. + /// If this behavior is undesirable, use [`Self::introduce_without_shadow_symbol`]. + pub fn introduce( + &mut self, + ident: Ident, + region: Region, + ) -> Result, Loc, Symbol)> { + self.introduce_str(ident.as_str(), region) + } + + pub fn introduce_str( + &mut self, + ident: &str, + region: Region, + ) -> Result, Loc, Symbol)> { + match self.introduce_help(ident, region) { + Ok(symbol) => Ok(symbol), + Err((shadowed_symbol, original_region)) => { + let shadow = Loc { + value: Ident::from(ident), + region, + }; + let symbol = self.locals.scopeless_symbol(ident, region); + + Err((Loc::at(original_region, shadowed_symbol), shadow, symbol)) + } + } + } + + /// Like [Self::introduce], but does not introduce a new symbol for the shadowing symbol. + pub fn introduce_without_shadow_symbol( + &mut self, + ident: &Ident, + region: Region, + ) -> Result)> { + match self.introduce_help(ident.as_str(), region) { + Err((symbol, original_region)) => { + let shadow = Loc { + value: ident.clone(), + region, + }; + Err((symbol, original_region, shadow)) + } + Ok(symbol) => Ok(symbol), + } + } + + /// Like [Self::introduce], but handles the case of when an ident matches an ability member + /// name. In such cases a new symbol is created for the ident (since it's expected to be a + /// specialization of the ability member), but the ident is not added to the ident->symbol map. + /// + /// If the ident does not match an ability name, the behavior of this function is exactly that + /// of `introduce`. + #[allow(clippy::type_complexity)] + pub fn introduce_or_shadow_ability_member( + &mut self, + pending_abilities_in_scope: &PendingAbilitiesInScope, + ident: Ident, + region: Region, + ) -> Result<(Symbol, Option), (Region, Loc, Symbol)> { + let ident = &ident; + + match self.introduce_help(ident.as_str(), region) { + Err((original_symbol, original_region)) => { + let shadow_symbol = self.scopeless_symbol(ident, region); + + if self.abilities_store.is_ability_member_name(original_symbol) + || pending_abilities_in_scope + .iter() + .any(|(_, members)| members.iter().any(|m| *m == original_symbol)) + { + match self.shadows.get(&original_symbol) { + Some(loc_original_shadow) => { + // Duplicate shadow of an ability members; that's illegal. + let shadow = Loc { + value: ident.clone(), + region, + }; + Err((loc_original_shadow.region, shadow, shadow_symbol)) + } + None => { + self.shadows + .insert(original_symbol, Loc::at(region, shadow_symbol)); + + Ok((shadow_symbol, Some(original_symbol))) + } + } + } else { + // This is an illegal shadow. + let shadow = Loc { + value: ident.clone(), + region, + }; + + Err((original_region, shadow, shadow_symbol)) + } + } + Ok(symbol) => Ok((symbol, None)), + } + } + + pub fn get_member_shadow(&self, ability_member: Symbol) -> Option<&Loc> { + self.shadows.get(&ability_member) + } + + /// Create a new symbol, but don't add it to the scope (yet) + /// + /// Used for record guards like { x: Just _ } where the `x` is not added to the scope, + /// but also in other places where we need to create a symbol and we don't have the right + /// scope information yet. An identifier can be introduced later, and will use the same IdentId + pub fn scopeless_symbol(&mut self, ident: &Ident, region: Region) -> Symbol { + self.locals.scopeless_symbol(ident.as_str(), region) + } + + /// Import a Symbol from another module into this module's top-level scope. + /// + /// Returns Err if this would shadow an existing ident, including the + /// Symbol and Region of the ident we already had in scope under that name. + pub fn import( + &mut self, + ident: Ident, + symbol: Symbol, + region: Region, + ) -> Result<(), (Symbol, Region)> { + if let Some((s, r)) = self.has_imported(ident.as_str()) { + return Err((s, r)); + } + + self.imports.push((ident, symbol, region)); + + Ok(()) + } + + pub fn add_alias( + &mut self, + name: Symbol, + region: Region, + vars: Vec>, + infer_ext_in_output_variables: Vec, + typ: Type, + kind: AliasKind, + ) { + let alias = create_alias(name, region, vars, infer_ext_in_output_variables, typ, kind); + self.aliases.insert(name, alias); + } + + pub fn lookup_alias(&self, symbol: Symbol) -> Option<&Alias> { + self.aliases.get(&symbol) + } + + pub fn contains_alias(&mut self, name: Symbol) -> bool { + self.aliases.contains_key(&name) + } + + pub fn inner_scope(&mut self, f: F) -> T + where + F: FnOnce(&mut Scope) -> T, + { + // store enough information to roll back to the original outer scope + // + // - abilities_store: ability definitions not allowed in inner scopes + // - locals: everything introduced in the inner scope is marked as not in scope in the rollback + // - aliases: stored in a VecMap, we just discard anything added in an inner scope + // - exposed_ident_count: unchanged + // - home: unchanged + let aliases_count = self.aliases.len(); + let ignored_locals_count = self.ignored_locals.len(); + let locals_snapshot = self.locals.in_scope.len(); + + let result = f(self); + + self.aliases.truncate(aliases_count); + self.ignored_locals.truncate(ignored_locals_count); + + // anything added in the inner scope is no longer in scope now + for i in locals_snapshot..self.locals.in_scope.len() { + self.locals.in_scope.set(i, false); + } + + result + } + + pub fn register_debug_idents(&self) { + self.home.register_debug_idents(&self.locals.ident_ids) + } + + /// Generates a unique, new symbol like "$1" or "$5", + /// using the home module as the module_id. + /// + /// This is used, for example, during canonicalization of an Expr::Closure + /// to generate a unique symbol to refer to that closure. + pub fn gen_unique_symbol(&mut self) -> Symbol { + Symbol::new(self.home, self.locals.gen_unique()) + } + + /// Introduce a new ignored variable (variable starting with an underscore). + /// The underscore itself should not be included in `ident`. + pub fn introduce_ignored_local(&mut self, ident: &str, region: Region) { + self.ignored_locals.insert(ident.to_owned(), region); + } + + /// Lookup an ignored variable (variable starting with an underscore). + /// The underscore itself should not be included in `ident`. + /// Returns the source code region of the ignored variable if it's found. + pub fn lookup_ignored_local(&self, ident: &str) -> Option { + self.ignored_locals.get(&ident.to_owned()).copied() + } +} + +pub fn create_alias( + name: Symbol, + region: Region, + vars: Vec>, + infer_ext_in_output_variables: Vec, + typ: Type, + kind: AliasKind, +) -> Alias { + let roc_types::types::VariableDetail { + type_variables, + lambda_set_variables, + recursion_variables, + } = typ.variables_detail(); + + debug_assert!({ + let mut hidden = type_variables; + + for var in (vars.iter().map(|lv| lv.value.var)) + .chain(recursion_variables.iter().copied()) + .chain(infer_ext_in_output_variables.iter().copied()) + { + hidden.remove(&var); + } + + if !hidden.is_empty() { + internal_error!( + "Found unbound type variables {:?} \n in type alias {:?} {:?} {:?} : {:?}", + hidden, + name, + &vars, + &infer_ext_in_output_variables, + &typ + ) + } + + true + }); + + let lambda_set_variables: Vec<_> = lambda_set_variables + .into_iter() + .map(|v| roc_types::types::LambdaSet(Type::Variable(v))) + .collect(); + + Alias { + region, + type_variables: vars, + lambda_set_variables, + infer_ext_in_output_variables, + recursion_variables, + typ, + kind, + } +} + +#[derive(Debug)] +enum ContainsIdent { + InScope(Symbol, Region), + NotInScope(IdentId), + NotPresent, +} + +#[derive(Clone, Debug)] +pub struct ScopedIdentIds { + pub ident_ids: IdentIds, + in_scope: BitVec, + regions: Vec, + home: ModuleId, +} + +impl ScopedIdentIds { + fn from_ident_ids(home: ModuleId, ident_ids: IdentIds) -> Self { + let capacity = ident_ids.len(); + + Self { + in_scope: BitVec::repeat(false, capacity), + ident_ids, + regions: vec![Region::zero(); capacity], + home, + } + } + + fn has_in_scope(&self, ident: &Ident) -> Option<(Symbol, Region)> { + match self.contains_ident(ident.as_str()) { + ContainsIdent::InScope(symbol, region) => Some((symbol, region)), + ContainsIdent::NotInScope(_) | ContainsIdent::NotPresent => None, + } + } + + fn contains_ident(&self, ident: &str) -> ContainsIdent { + use ContainsIdent::*; + + let mut result = NotPresent; + + for ident_id in self.ident_ids.get_id_many(ident) { + let index = ident_id.index(); + if self.in_scope[index] { + return InScope(Symbol::new(self.home, ident_id), self.regions[index]); + } else { + result = NotInScope(ident_id) + } + } + + result + } + + fn idents_in_scope(&self) -> impl Iterator + '_ { + self.ident_ids + .ident_strs() + .zip(self.in_scope.iter()) + .filter_map(|((_, string), keep)| { + if *keep { + Some(Ident::from(string)) + } else { + None + } + }) + } + + fn introduce_into_scope(&mut self, ident_name: &str, region: Region) -> IdentId { + let id = self.ident_ids.add_str(ident_name); + + debug_assert_eq!(id.index(), self.in_scope.len()); + debug_assert_eq!(id.index(), self.regions.len()); + + self.in_scope.push(true); + self.regions.push(region); + + id + } + + fn introduce_into_scope_duplicate(&mut self, existing: IdentId, region: Region) -> IdentId { + let id = self.ident_ids.duplicate_ident(existing); + + debug_assert_eq!(id.index(), self.in_scope.len()); + debug_assert_eq!(id.index(), self.regions.len()); + + self.in_scope.push(true); + self.regions.push(region); + + id + } + + /// Adds an IdentId, but does not introduce it to the scope + fn scopeless_symbol(&mut self, ident_name: &str, region: Region) -> Symbol { + let id = self.ident_ids.add_str(ident_name); + + debug_assert_eq!(id.index(), self.in_scope.len()); + debug_assert_eq!(id.index(), self.regions.len()); + + self.in_scope.push(false); + self.regions.push(region); + + Symbol::new(self.home, id) + } + + fn gen_unique(&mut self) -> IdentId { + let id = self.ident_ids.gen_unique(); + + debug_assert_eq!(id.index(), self.in_scope.len()); + debug_assert_eq!(id.index(), self.regions.len()); + + self.in_scope.push(false); + self.regions.push(Region::zero()); + + id + } +} + +#[cfg(test)] +mod test { + use super::*; + use roc_module::symbol::ModuleIds; + use roc_region::all::Position; + + use pretty_assertions::{assert_eq, assert_ne}; + + #[test] + fn scope_contains_introduced() { + let _register_module_debug_names = ModuleIds::default(); + let mut scope = Scope::new( + ModuleId::ATTR, + IdentIds::default(), + PendingAbilitiesStore::default(), + ); + + let region = Region::zero(); + let ident = Ident::from("mezolit"); + + assert!(scope.lookup(&ident, region).is_err()); + + assert!(scope.introduce(ident.clone(), region).is_ok()); + + assert!(scope.lookup(&ident, region).is_ok()); + } + + #[test] + fn second_introduce_shadows() { + let _register_module_debug_names = ModuleIds::default(); + let mut scope = Scope::new( + ModuleId::ATTR, + IdentIds::default(), + PendingAbilitiesStore::default(), + ); + + let region1 = Region::from_pos(Position { offset: 10 }); + let region2 = Region::from_pos(Position { offset: 20 }); + let ident = Ident::from("mezolit"); + + assert!(scope.lookup(&ident, Region::zero()).is_err()); + + let first = scope.introduce(ident.clone(), region1).unwrap(); + let (original, _ident, shadow_symbol) = + scope.introduce(ident.clone(), region2).unwrap_err(); + + scope.register_debug_idents(); + + assert_ne!(first, shadow_symbol); + assert_eq!(original.region, region1); + + let lookup = scope.lookup(&ident, Region::zero()).unwrap(); + + assert_eq!(first, lookup); + } + + #[test] + fn inner_scope_does_not_influence_outer() { + let _register_module_debug_names = ModuleIds::default(); + let mut scope = Scope::new( + ModuleId::ATTR, + IdentIds::default(), + PendingAbilitiesStore::default(), + ); + + let region = Region::zero(); + let ident = Ident::from("uránia"); + + assert!(scope.lookup(&ident, region).is_err()); + + scope.inner_scope(|inner| { + assert!(inner.introduce(ident.clone(), region).is_ok()); + }); + + assert!(scope.lookup(&ident, region).is_err()); + } + + #[test] + fn default_idents_in_scope() { + let _register_module_debug_names = ModuleIds::default(); + let scope = Scope::new( + ModuleId::ATTR, + IdentIds::default(), + PendingAbilitiesStore::default(), + ); + + let idents: Vec<_> = scope.idents_in_scope().collect(); + + assert_eq!( + &idents, + &[ + Ident::from("Str"), + Ident::from("List"), + Ident::from("Box"), + Ident::from("Ok"), + Ident::from("Err"), + ] + ); + } + + #[test] + fn idents_with_inner_scope() { + let _register_module_debug_names = ModuleIds::default(); + let mut scope = Scope::new( + ModuleId::ATTR, + IdentIds::default(), + PendingAbilitiesStore::default(), + ); + + let idents: Vec<_> = scope.idents_in_scope().collect(); + + assert_eq!( + &idents, + &[ + Ident::from("Str"), + Ident::from("List"), + Ident::from("Box"), + Ident::from("Ok"), + Ident::from("Err"), + ] + ); + + let builtin_count = idents.len(); + + let region = Region::zero(); + + let ident1 = Ident::from("uránia"); + let ident2 = Ident::from("malmok"); + let ident3 = Ident::from("Járnak"); + + scope.introduce(ident1.clone(), region).unwrap(); + scope.introduce(ident2.clone(), region).unwrap(); + scope.introduce(ident3.clone(), region).unwrap(); + + let idents: Vec<_> = scope.idents_in_scope().collect(); + + assert_eq!( + &idents[builtin_count..], + &[ident1.clone(), ident2.clone(), ident3.clone(),] + ); + + scope.inner_scope(|inner| { + let ident4 = Ident::from("Ångström"); + let ident5 = Ident::from("Sirály"); + + inner.introduce(ident4.clone(), region).unwrap(); + inner.introduce(ident5.clone(), region).unwrap(); + + let idents: Vec<_> = inner.idents_in_scope().collect(); + + assert_eq!( + &idents[builtin_count..], + &[ + ident1.clone(), + ident2.clone(), + ident3.clone(), + ident4, + ident5 + ] + ); + }); + + let idents: Vec<_> = scope.idents_in_scope().collect(); + + assert_eq!(&idents[builtin_count..], &[ident1, ident2, ident3,]); + } + + #[test] + fn import_is_in_scope() { + let _register_module_debug_names = ModuleIds::default(); + let mut scope = Scope::new( + ModuleId::ATTR, + IdentIds::default(), + PendingAbilitiesStore::default(), + ); + + let ident = Ident::from("product"); + let symbol = Symbol::LIST_PRODUCT; + let region = Region::zero(); + + assert!(scope.lookup(&ident, region).is_err()); + + assert!(scope.import(ident.clone(), symbol, region).is_ok()); + + assert!(scope.lookup(&ident, region).is_ok()); + + assert!(scope.idents_in_scope().any(|x| x == ident)); + } + + #[test] + fn shadow_of_import() { + let _register_module_debug_names = ModuleIds::default(); + let mut scope = Scope::new( + ModuleId::ATTR, + IdentIds::default(), + PendingAbilitiesStore::default(), + ); + + let ident = Ident::from("product"); + let symbol = Symbol::LIST_PRODUCT; + + let region1 = Region::from_pos(Position { offset: 10 }); + let region2 = Region::from_pos(Position { offset: 20 }); + + scope.import(ident.clone(), symbol, region1).unwrap(); + + let (original, _ident, shadow_symbol) = + scope.introduce(ident.clone(), region2).unwrap_err(); + + scope.register_debug_idents(); + + assert_ne!(symbol, shadow_symbol); + assert_eq!(original.region, region1); + + let lookup = scope.lookup(&ident, Region::zero()).unwrap(); + + assert_eq!(symbol, lookup); + } +} diff --git a/compiler/can/src/string.rs b/crates/compiler/can/src/string.rs similarity index 99% rename from compiler/can/src/string.rs rename to crates/compiler/can/src/string.rs index bb69adf4ff..5d2c344223 100644 --- a/compiler/can/src/string.rs +++ b/crates/compiler/can/src/string.rs @@ -1,6 +1,7 @@ // use bumpalo::collections::string::String; // use bumpalo::collections::vec::Vec; use bumpalo::Bump; +use roc_error_macros::internal_error; use roc_parse::ast::Expr; // use roc_parse::ast::{Attempting, Expr}; // use roc_parse::ident; @@ -12,7 +13,7 @@ use roc_region::all::Region; // use std::iter::Peekable; pub fn canonical_string_literal<'a>(_arena: &Bump, _raw: &'a str, _region: Region) -> Expr<'a> { - panic!("TODO restore canonicalization"); + internal_error!("TODO restore canonicalization"); } // let mut problems = std::vec::Vec::new(); diff --git a/crates/compiler/can/src/traverse.rs b/crates/compiler/can/src/traverse.rs new file mode 100644 index 0000000000..eb92dc14da --- /dev/null +++ b/crates/compiler/can/src/traverse.rs @@ -0,0 +1,963 @@ +//! Traversals over the can ast. + +use roc_module::{ident::Lowercase, symbol::Symbol}; +use roc_region::all::{Loc, Position, Region}; +use roc_types::{subs::Variable, types::MemberImpl}; + +use crate::{ + abilities::AbilitiesStore, + def::{Annotation, Def}, + expr::{ + self, AnnotatedMark, ClosureData, Declarations, Expr, Field, OpaqueWrapFunctionData, + StructAccessorData, + }, + pattern::{DestructType, Pattern, RecordDestruct, TupleDestruct}, +}; + +pub enum DeclarationInfo<'a> { + Value { + loc_symbol: Loc, + loc_expr: &'a Loc, + expr_var: Variable, + pattern: Pattern, + annotation: Option<&'a Annotation>, + }, + Expectation { + loc_condition: &'a Loc, + }, + Function { + loc_symbol: Loc, + loc_body: &'a Loc, + expr_var: Variable, + pattern: Pattern, + function: &'a Loc, + }, + Destructure { + loc_pattern: &'a Loc, + opt_pattern_var: Option, + loc_expr: &'a Loc, + expr_var: Variable, + annotation: Option<&'a Annotation>, + }, +} + +impl<'a> DeclarationInfo<'a> { + pub fn region(&self) -> Region { + use DeclarationInfo::*; + match self { + Value { + loc_symbol, + loc_expr, + .. + } => Region::span_across(&loc_symbol.region, &loc_expr.region), + Expectation { loc_condition } => loc_condition.region, + Function { + loc_symbol, + function, + .. + } => Region::span_across(&loc_symbol.region, &function.region), + Destructure { + loc_pattern, + loc_expr, + .. + } => Region::span_across(&loc_pattern.region, &loc_expr.region), + } + } + + fn var(&self) -> Variable { + match self { + DeclarationInfo::Value { expr_var, .. } => *expr_var, + DeclarationInfo::Expectation { .. } => Variable::BOOL, + DeclarationInfo::Function { expr_var, .. } => *expr_var, + DeclarationInfo::Destructure { expr_var, .. } => *expr_var, + } + } +} + +pub fn walk_decls(visitor: &mut V, decls: &Declarations) { + use crate::expr::DeclarationTag::*; + + for (index, tag) in decls.declarations.iter().enumerate() { + let info = match tag { + Value => { + let loc_expr = &decls.expressions[index]; + + let loc_symbol = decls.symbols[index]; + let expr_var = decls.variables[index]; + + let pattern = match decls.specializes.get(&index).copied() { + Some(specializes) => Pattern::AbilityMemberSpecialization { + ident: loc_symbol.value, + specializes, + }, + None => Pattern::Identifier(loc_symbol.value), + }; + + DeclarationInfo::Value { + loc_symbol, + loc_expr, + expr_var, + pattern, + annotation: decls.annotations[index].as_ref(), + } + } + Expectation | ExpectationFx => { + let loc_condition = &decls.expressions[index]; + + DeclarationInfo::Expectation { loc_condition } + } + Function(function_index) + | Recursive(function_index) + | TailRecursive(function_index) => { + let loc_body = &decls.expressions[index]; + + let loc_symbol = decls.symbols[index]; + let expr_var = decls.variables[index]; + + let pattern = match decls.specializes.get(&index).copied() { + Some(specializes) => Pattern::AbilityMemberSpecialization { + ident: loc_symbol.value, + specializes, + }, + None => Pattern::Identifier(loc_symbol.value), + }; + + let function_def = &decls.function_bodies[function_index.index()]; + + DeclarationInfo::Function { + loc_symbol, + loc_body, + expr_var, + pattern, + function: function_def, + } + } + Destructure(destructure_index) => { + let destructure = &decls.destructs[destructure_index.index()]; + let loc_pattern = &destructure.loc_pattern; + + let loc_expr = &decls.expressions[index]; + let expr_var = decls.variables[index]; + + let opt_pattern_var = match loc_pattern.value { + Pattern::Identifier(..) | Pattern::AbilityMemberSpecialization { .. } => { + Some(expr_var) + } + _ => loc_pattern.value.opt_var(), + }; + + DeclarationInfo::Destructure { + loc_pattern, + opt_pattern_var, + loc_expr, + expr_var, + annotation: decls.annotations[index].as_ref(), + } + } + MutualRecursion { .. } => { + // The actual declarations involved in the mutual recursion will come next. + continue; + } + }; + + visitor.visit_decl(info); + } +} + +fn walk_decl(visitor: &mut V, decl: DeclarationInfo<'_>) { + use DeclarationInfo::*; + + match decl { + Value { + loc_symbol, + loc_expr, + expr_var, + pattern, + annotation, + } => { + visitor.visit_pattern(&pattern, loc_symbol.region, Some(expr_var)); + + visitor.visit_expr(&loc_expr.value, loc_expr.region, expr_var); + if let Some(annot) = annotation { + visitor.visit_annotation(annot); + } + } + Expectation { loc_condition } => { + visitor.visit_expr(&loc_condition.value, loc_condition.region, Variable::BOOL); + } + Function { + loc_symbol, + loc_body, + expr_var, + pattern, + function, + } => { + visitor.visit_pattern(&pattern, loc_symbol.region, Some(expr_var)); + + walk_closure_help( + visitor, + &function.value.arguments, + loc_body, + function.value.return_type, + ) + } + Destructure { + loc_pattern, + opt_pattern_var, + loc_expr, + expr_var, + annotation, + } => { + visitor.visit_pattern(&loc_pattern.value, loc_pattern.region, opt_pattern_var); + visitor.visit_expr(&loc_expr.value, loc_expr.region, expr_var); + + if let Some(annot) = annotation { + visitor.visit_annotation(annot); + } + } + }; +} + +pub fn walk_def(visitor: &mut V, def: &Def) { + let Def { + loc_pattern, + loc_expr, + annotation, + expr_var, + .. + } = def; + + let opt_var = match loc_pattern.value { + Pattern::Identifier(..) | Pattern::AbilityMemberSpecialization { .. } => Some(*expr_var), + _ => loc_pattern.value.opt_var(), + }; + + visitor.visit_pattern(&loc_pattern.value, loc_pattern.region, opt_var); + visitor.visit_expr(&loc_expr.value, loc_expr.region, *expr_var); + if let Some(annot) = &annotation { + visitor.visit_annotation(annot); + } +} + +pub fn walk_expr(visitor: &mut V, expr: &Expr, var: Variable) { + match expr { + Expr::Closure(closure_data) => walk_closure(visitor, closure_data), + Expr::When { + cond_var, + expr_var, + loc_cond, + branches, + region: _, + branches_cond_var: _, + exhaustive: _, + } => { + walk_when(visitor, *cond_var, *expr_var, loc_cond, branches); + } + Expr::Num(..) => { /* terminal */ } + Expr::Int(..) => { /* terminal */ } + Expr::Float(..) => { /* terminal */ } + Expr::Str(..) => { /* terminal */ } + Expr::IngestedFile(..) => { /* terminal */ } + Expr::SingleQuote(..) => { /* terminal */ } + Expr::List { + elem_var, + loc_elems, + } => { + walk_list(visitor, *elem_var, loc_elems); + } + Expr::Var(..) => { /* terminal */ } + Expr::AbilityMember(..) => { /* terminal */ } + Expr::If { + cond_var, + branches, + branch_var, + final_else, + } => walk_if(visitor, *cond_var, branches, *branch_var, final_else), + Expr::LetRec(defs, body, _cycle_mark) => { + defs.iter().for_each(|def| visitor.visit_def(def)); + visitor.visit_expr(&body.value, body.region, var); + } + Expr::LetNonRec(def, body) => { + visitor.visit_def(def); + visitor.visit_expr(&body.value, body.region, var); + } + Expr::Call(f, args, _called_via) => { + let (fn_var, loc_fn, _closure_var, _ret_var) = &**f; + walk_call(visitor, *fn_var, loc_fn, args); + } + Expr::Crash { msg, .. } => { + visitor.visit_expr(&msg.value, msg.region, Variable::STR); + } + Expr::RunLowLevel { + op: _, + args, + ret_var: _, + } => { + args.iter() + .for_each(|(v, e)| visitor.visit_expr(e, Region::zero(), *v)); + } + Expr::ForeignCall { + foreign_symbol: _, + args, + ret_var: _, + } => { + args.iter() + .for_each(|(v, e)| visitor.visit_expr(e, Region::zero(), *v)); + } + Expr::Record { + record_var: _, + fields, + } => { + walk_record_fields(visitor, fields.iter()); + } + Expr::Tuple { + tuple_var: _, + elems, + } => elems + .iter() + .for_each(|(var, elem)| visitor.visit_expr(&elem.value, elem.region, *var)), + Expr::EmptyRecord => { /* terminal */ } + Expr::RecordAccess { + field_var, + loc_expr, + field: _, + record_var: _, + ext_var: _, + } => visitor.visit_expr(&loc_expr.value, loc_expr.region, *field_var), + Expr::RecordAccessor(StructAccessorData { .. }) => { /* terminal */ } + Expr::TupleAccess { + elem_var, + loc_expr, + index: _, + tuple_var: _, + ext_var: _, + } => visitor.visit_expr(&loc_expr.value, loc_expr.region, *elem_var), + Expr::OpaqueWrapFunction(OpaqueWrapFunctionData { .. }) => { /* terminal */ } + Expr::RecordUpdate { + record_var: _, + ext_var: _, + symbol: _, + updates, + } => { + walk_record_fields(visitor, updates.iter()); + } + Expr::Tag { + tag_union_var: _, + ext_var: _, + name: _, + arguments, + } => arguments + .iter() + .for_each(|(v, le)| visitor.visit_expr(&le.value, le.region, *v)), + Expr::ZeroArgumentTag { .. } => { /* terminal */ } + Expr::OpaqueRef { + opaque_var: _, + name: _, + argument, + specialized_def_type: _, + type_arguments: _, + lambda_set_variables: _, + } => { + let (var, le) = &**argument; + visitor.visit_expr(&le.value, le.region, *var); + } + Expr::Expect { + loc_condition, + loc_continuation, + lookups_in_cond: _, + } => { + visitor.visit_expr(&loc_condition.value, loc_condition.region, Variable::BOOL); + visitor.visit_expr( + &loc_continuation.value, + loc_continuation.region, + Variable::NULL, + ); + } + Expr::ExpectFx { + loc_condition, + loc_continuation, + lookups_in_cond: _, + } => { + visitor.visit_expr(&loc_condition.value, loc_condition.region, Variable::BOOL); + visitor.visit_expr( + &loc_continuation.value, + loc_continuation.region, + Variable::NULL, + ); + } + Expr::Dbg { + variable, + loc_message, + loc_continuation, + symbol: _, + } => { + visitor.visit_expr(&loc_message.value, loc_message.region, *variable); + visitor.visit_expr( + &loc_continuation.value, + loc_continuation.region, + Variable::NULL, + ); + } + Expr::TypedHole(_) => { /* terminal */ } + Expr::RuntimeError(..) => { /* terminal */ } + } +} + +#[inline(always)] +pub fn walk_closure(visitor: &mut V, clos: &ClosureData) { + let ClosureData { + arguments, + loc_body, + return_type, + .. + } = clos; + + walk_closure_help(visitor, arguments, loc_body, *return_type) +} + +fn walk_closure_help( + visitor: &mut V, + arguments: &[(Variable, AnnotatedMark, Loc)], + loc_body: &Loc, + return_type: Variable, +) { + arguments.iter().for_each(|(var, _exhaustive_mark, arg)| { + visitor.visit_pattern(&arg.value, arg.region, Some(*var)) + }); + + visitor.visit_expr(&loc_body.value, loc_body.region, return_type); +} + +#[inline(always)] +pub fn walk_when( + visitor: &mut V, + cond_var: Variable, + expr_var: Variable, + loc_cond: &Loc, + branches: &[expr::WhenBranch], +) { + visitor.visit_expr(&loc_cond.value, loc_cond.region, cond_var); + + branches + .iter() + .for_each(|branch| walk_when_branch(visitor, branch, expr_var)); +} + +#[inline(always)] +pub fn walk_when_branch( + visitor: &mut V, + branch: &expr::WhenBranch, + expr_var: Variable, +) { + let expr::WhenBranch { + patterns, + value, + guard, + redundant: _, + } = branch; + + patterns.iter().for_each(|pat| { + visitor.visit_pattern( + &pat.pattern.value, + pat.pattern.region, + pat.pattern.value.opt_var(), + ) + }); + visitor.visit_expr(&value.value, value.region, expr_var); + if let Some(guard) = guard { + visitor.visit_expr(&guard.value, guard.region, Variable::BOOL); + } +} + +#[inline(always)] +pub fn walk_list(visitor: &mut V, elem_var: Variable, loc_elems: &[Loc]) { + loc_elems + .iter() + .for_each(|le| visitor.visit_expr(&le.value, le.region, elem_var)); +} + +#[inline(always)] +pub fn walk_if( + visitor: &mut V, + cond_var: Variable, + branches: &[(Loc, Loc)], + branch_var: Variable, + final_else: &Loc, +) { + branches.iter().for_each(|(cond, body)| { + visitor.visit_expr(&cond.value, cond.region, cond_var); + visitor.visit_expr(&body.value, body.region, branch_var); + }); + visitor.visit_expr(&final_else.value, final_else.region, branch_var); +} + +#[inline(always)] +pub fn walk_call( + visitor: &mut V, + fn_var: Variable, + fn_expr: &Loc, + args: &[(Variable, Loc)], +) { + visitor.visit_expr(&fn_expr.value, fn_expr.region, fn_var); + args.iter() + .for_each(|(v, le)| visitor.visit_expr(&le.value, le.region, *v)); +} + +#[inline(always)] +pub fn walk_record_fields<'a, V: Visitor>( + visitor: &mut V, + fields: impl Iterator, +) { + fields.for_each( + |( + _name, + Field { + var, + loc_expr, + region: _, + }, + )| { visitor.visit_expr(&loc_expr.value, loc_expr.region, *var) }, + ) +} + +pub trait Visitor: Sized { + /// Most default implementations will call [Visitor::should_visit] to decide whether they + /// should descend into a node. Return `false` to skip visiting. + fn should_visit(&mut self, _region: Region) -> bool { + true + } + + fn visit_decls(&mut self, decls: &Declarations) { + walk_decls(self, decls); + } + + fn visit_decl(&mut self, decl: DeclarationInfo<'_>) { + if self.should_visit(decl.region()) { + walk_decl(self, decl); + } + } + + fn visit_def(&mut self, def: &Def) { + if self.should_visit(def.region()) { + walk_def(self, def); + } + } + + fn visit_annotation(&mut self, _pat: &Annotation) { + // ignore by default + } + + fn visit_expr(&mut self, expr: &Expr, region: Region, var: Variable) { + if self.should_visit(region) { + walk_expr(self, expr, var); + } + } + + fn visit_pattern(&mut self, pattern: &Pattern, region: Region, _opt_var: Option) { + if self.should_visit(region) { + walk_pattern(self, pattern); + } + } + + fn visit_record_destruct(&mut self, destruct: &RecordDestruct, region: Region) { + if self.should_visit(region) { + walk_record_destruct(self, destruct); + } + } + + fn visit_tuple_destruct(&mut self, destruct: &TupleDestruct, region: Region) { + if self.should_visit(region) { + self.visit_pattern( + &destruct.typ.1.value, + destruct.typ.1.region, + Some(destruct.typ.0), + ) + } + } +} + +pub fn walk_pattern(visitor: &mut V, pattern: &Pattern) { + use Pattern::*; + + match pattern { + Identifier(..) => { /* terminal */ } + As(subpattern, _symbol) => { + visitor.visit_pattern(&subpattern.value, subpattern.region, None) + } + AppliedTag { arguments, .. } => arguments + .iter() + .for_each(|(v, lp)| visitor.visit_pattern(&lp.value, lp.region, Some(*v))), + UnwrappedOpaque { argument, .. } => { + let (v, lp) = &**argument; + visitor.visit_pattern(&lp.value, lp.region, Some(*v)); + } + RecordDestructure { destructs, .. } => destructs + .iter() + .for_each(|d| visitor.visit_record_destruct(&d.value, d.region)), + TupleDestructure { destructs, .. } => destructs + .iter() + .for_each(|d| visitor.visit_tuple_destruct(&d.value, d.region)), + List { + patterns, elem_var, .. + } => patterns + .patterns + .iter() + .for_each(|p| visitor.visit_pattern(&p.value, p.region, Some(*elem_var))), + NumLiteral(..) => { /* terminal */ } + IntLiteral(..) => { /* terminal */ } + FloatLiteral(..) => { /* terminal */ } + StrLiteral(..) => { /* terminal */ } + SingleQuote(..) => { /* terminal */ } + Underscore => { /* terminal */ } + AbilityMemberSpecialization { .. } => { /* terminal */ } + Shadowed(..) => { /* terminal */ } + OpaqueNotInScope(..) => { /* terminal */ } + UnsupportedPattern(..) => { /* terminal */ } + MalformedPattern(..) => { /* terminal */ } + } +} + +pub fn walk_record_destruct(visitor: &mut V, destruct: &RecordDestruct) { + use DestructType::*; + match &destruct.typ { + Required => { /* terminal */ } + Optional(var, expr) => visitor.visit_expr(&expr.value, expr.region, *var), + Guard(var, pat) => visitor.visit_pattern(&pat.value, pat.region, Some(*var)), + } +} + +struct TypeAtVisitor { + region: Region, + typ: Option, +} + +impl Visitor for TypeAtVisitor { + fn should_visit(&mut self, region: Region) -> bool { + region.contains(&self.region) + } + + fn visit_expr(&mut self, expr: &Expr, region: Region, var: Variable) { + if region == self.region { + debug_assert!(self.typ.is_none()); + self.typ = Some(var); + return; + } + + walk_expr(self, expr, var); + } + + fn visit_pattern(&mut self, pat: &Pattern, region: Region, opt_var: Option) { + if region == self.region { + debug_assert!(self.typ.is_none()); + self.typ = opt_var; + return; + } + + walk_pattern(self, pat) + } +} + +struct TypeAtPositionVisitor { + position: Position, + region_typ: Option<(Region, Variable)>, +} + +impl Visitor for TypeAtPositionVisitor { + fn should_visit(&mut self, region: Region) -> bool { + region.contains_pos(self.position) + } + + fn visit_expr(&mut self, expr: &Expr, region: Region, var: Variable) { + if region.contains_pos(self.position) { + self.region_typ = Some((region, var)); + + walk_expr(self, expr, var); + } + } + + fn visit_pattern(&mut self, pat: &Pattern, region: Region, opt_var: Option) { + if region.contains_pos(self.position) { + if let Some(var) = opt_var { + self.region_typ = Some((region, var)); + } + + walk_pattern(self, pat); + } + } +} + +/// Attempts to find the type of an expression at `region`, if it exists. +pub fn find_type_at(region: Region, decls: &Declarations) -> Option { + let mut visitor = TypeAtVisitor { region, typ: None }; + visitor.visit_decls(decls); + visitor.typ +} + +#[derive(Debug)] +pub enum FoundSymbol { + /// Specialization(T, foo1) is the specialization of foo for T. + Specialization(Symbol, Symbol), + /// AbilityMember(Foo, foo) is the ability member foo of Foo. + AbilityMember(Symbol, Symbol), + /// Raw symbol, not specialized to anything. + Symbol(Symbol), +} + +impl FoundSymbol { + pub fn implementation_symbol(&self) -> Symbol { + match self { + FoundSymbol::Specialization(_, sym) + | FoundSymbol::AbilityMember(_, sym) + | FoundSymbol::Symbol(sym) => *sym, + } + } +} + +/// Given an ability Foo implements foo : ..., returns (T, foo1) if the symbol at the given region is a +/// Like [find_type_at], but descends into the narrowest node containing [position]. +pub fn find_closest_type_at( + position: Position, + decls: &Declarations, +) -> Option<(Region, Variable)> { + let mut visitor = TypeAtPositionVisitor { + position, + region_typ: None, + }; + visitor.visit_decls(decls); + visitor.region_typ +} + +/// Given an ability Foo has foo : ..., returns (T, foo1) if the symbol at the given region is a +/// symbol foo1 that specializes foo for T. Otherwise if the symbol is foo but the specialization +/// is unknown, (Foo, foo) is returned. Otherwise [None] is returned. +pub fn find_closest_symbol_at( + position: Position, + decls: &Declarations, + abilities_store: &AbilitiesStore, +) -> Option { + find_symbol_at_impl(Region::from_pos(position), decls, abilities_store, true) +} + +/// Given an ability Foo has foo : ..., returns (T, foo1) if the symbol at the given region is a +/// symbol foo1 that specializes foo for T. Otherwise if the symbol is foo but the specialization +/// is unknown, (Foo, foo) is returned. Otherwise [None] is returned. +pub fn find_symbol_at( + region: Region, + decls: &Declarations, + abilities_store: &AbilitiesStore, +) -> Option { + find_symbol_at_impl(region, decls, abilities_store, false) +} + +pub fn find_symbol_at_impl( + region: Region, + decls: &Declarations, + abilities_store: &AbilitiesStore, + allow_subregion: bool, +) -> Option { + let mut visitor = Finder { + region, + found: None, + abilities_store, + allow_subregion, + }; + visitor.visit_decls(decls); + return visitor.found; + + struct Finder<'a> { + region: Region, + abilities_store: &'a AbilitiesStore, + found: Option, + allow_subregion: bool, + } + + impl<'a> Finder<'a> { + fn is_at_wanted_region(&self, region: Region) -> bool { + if self.allow_subregion { + region.contains(&self.region) + } else { + region == self.region + } + } + } + + impl Visitor for Finder<'_> { + fn should_visit(&mut self, region: Region) -> bool { + region.contains(&self.region) + } + + fn visit_pattern(&mut self, pattern: &Pattern, region: Region, _opt_var: Option) { + if self.is_at_wanted_region(region) { + match pattern { + Pattern::AbilityMemberSpecialization { + ident: spec_symbol, + specializes: _, + } => { + debug_assert!(self.found.is_none()); + let spec_type = + find_specialization_type_of_symbol(*spec_symbol, self.abilities_store) + .unwrap(); + self.found = Some(FoundSymbol::Specialization(spec_type, *spec_symbol)) + } + Pattern::Identifier(symbol) => self.found = Some(FoundSymbol::Symbol(*symbol)), + _ => {} + } + } + + walk_pattern(self, pattern); + } + + fn visit_expr(&mut self, expr: &Expr, region: Region, var: Variable) { + if self.is_at_wanted_region(region) { + match expr { + &Expr::AbilityMember(member_symbol, specialization_id, _var) => { + debug_assert!(self.found.is_none()); + self.found = match specialization_id + .and_then(|id| self.abilities_store.get_resolved(id)) + { + Some(spec_symbol) => { + let spec_type = find_specialization_type_of_symbol( + spec_symbol, + self.abilities_store, + ) + .unwrap(); + Some(FoundSymbol::Specialization(spec_type, spec_symbol)) + } + None => { + let parent_ability = self + .abilities_store + .member_def(member_symbol) + .unwrap() + .parent_ability; + Some(FoundSymbol::AbilityMember(parent_ability, member_symbol)) + } + }; + return; + } + Expr::Var(symbol, _var) => self.found = Some(FoundSymbol::Symbol(*symbol)), + _ => {} + } + } + + walk_expr(self, expr, var); + } + } + + fn find_specialization_type_of_symbol( + symbol: Symbol, + abilities_store: &AbilitiesStore, + ) -> Option { + abilities_store + .iter_declared_implementations() + .find(|(_, member_impl)| matches!(member_impl, MemberImpl::Impl(sym) if *sym == symbol)) + .map(|(impl_key, _)| impl_key.opaque) + } +} + +pub fn symbols_introduced_from_pattern( + pattern: &Loc, +) -> impl Iterator> { + let mut visitor = Collector { + symbols: Vec::new(), + }; + visitor.visit_pattern(&pattern.value, pattern.region, None); + return visitor.symbols.into_iter(); + + struct Collector { + symbols: Vec>, + } + impl Visitor for Collector { + fn visit_pattern(&mut self, pattern: &Pattern, region: Region, _opt_var: Option) { + use Pattern::*; + match pattern { + Identifier(symbol) + | Shadowed(_, _, symbol) + | AbilityMemberSpecialization { ident: symbol, .. } => { + self.symbols.push(Loc::at(region, *symbol)); + } + _ => walk_pattern(self, pattern), + } + } + + fn visit_record_destruct(&mut self, destruct: &RecordDestruct, region: Region) { + // when a record field has a pattern guard, only symbols in the guard are introduced + if let DestructType::Guard(_, subpattern) = &destruct.typ { + self.visit_pattern(&subpattern.value, subpattern.region, None); + } else { + self.symbols.push(Loc::at(region, destruct.symbol)); + } + } + } +} + +pub enum FoundDeclaration<'a> { + Decl(DeclarationInfo<'a>), + Def(&'a Def), +} + +impl<'a> FoundDeclaration<'a> { + pub fn region(&self) -> Region { + match self { + FoundDeclaration::Decl(decl) => decl.region(), + FoundDeclaration::Def(def) => def.region(), + } + } + + pub fn var(&self) -> Variable { + match self { + FoundDeclaration::Decl(decl) => decl.var(), + FoundDeclaration::Def(def) => def.expr_var, + } + } +} + +/// Finds the declaration of `symbol`. +pub fn find_declaration(symbol: Symbol, decls: &'_ Declarations) -> Option> { + let mut visitor = Finder { + symbol, + found: None, + }; + visitor.visit_decls(decls); + return visitor.found; + + struct Finder<'a> { + symbol: Symbol, + found: Option>, + } + + impl Visitor for Finder<'_> { + fn should_visit(&mut self, _region: Region) -> bool { + true + } + + fn visit_decl(&mut self, decl: DeclarationInfo<'_>) { + match decl { + DeclarationInfo::Value { loc_symbol, .. } + | DeclarationInfo::Function { loc_symbol, .. } + if loc_symbol.value == self.symbol => + { + self.found = Some(FoundDeclaration::Decl(unsafe { std::mem::transmute(decl) })); + } + DeclarationInfo::Destructure { .. } => { + // TODO destructures + walk_decl(self, decl); + } + _ => { + walk_decl(self, decl); + } + } + } + + fn visit_def(&mut self, def: &Def) { + if matches!(def.loc_pattern.value, Pattern::Identifier(s) if s == self.symbol) { + debug_assert!(self.found.is_none()); + // Safety: the def can't escape the passed in `decls`, and the visitor does not + // synthesize defs. + self.found = Some(FoundDeclaration::Def(unsafe { std::mem::transmute(def) })); + return; + } + + walk_def(self, def) + } + } +} diff --git a/crates/compiler/can/tests/helpers/mod.rs b/crates/compiler/can/tests/helpers/mod.rs new file mode 100644 index 0000000000..d2f95267c6 --- /dev/null +++ b/crates/compiler/can/tests/helpers/mod.rs @@ -0,0 +1,112 @@ +extern crate bumpalo; + +use self::bumpalo::Bump; +use roc_can::env::Env; +use roc_can::expr::Output; +use roc_can::expr::{canonicalize_expr, Expr}; +use roc_can::operator; +use roc_can::scope::Scope; +use roc_collections::all::MutMap; +use roc_module::symbol::{IdentIds, Interns, ModuleId, ModuleIds, Symbol}; +use roc_problem::can::Problem; +use roc_region::all::{Loc, Region}; +use roc_types::subs::{VarStore, Variable}; +use roc_types::types::{AliasVar, Type}; +use std::hash::Hash; + +pub fn test_home() -> ModuleId { + ModuleIds::default().get_or_insert(&"Test".into()) +} + +#[allow(dead_code)] +pub fn can_expr(expr_str: &str) -> CanExprOut { + can_expr_with(&Bump::new(), test_home(), expr_str) +} + +pub struct CanExprOut { + pub loc_expr: Loc, + pub output: Output, + pub problems: Vec, + pub home: ModuleId, + pub interns: Interns, + pub var_store: VarStore, + pub var: Variable, +} + +#[allow(dead_code)] +pub fn can_expr_with(arena: &Bump, home: ModuleId, expr_str: &str) -> CanExprOut { + let loc_expr = roc_parse::test_helpers::parse_loc_with(arena, expr_str).unwrap_or_else(|e| { + panic!( + "can_expr_with() got a parse error when attempting to canonicalize:\n\n{expr_str:?} {e:?}" + ) + }); + + let mut var_store = VarStore::default(); + let var = var_store.fresh(); + let module_ids = ModuleIds::default(); + + // Desugar operators (convert them to Apply calls, taking into account + // operator precedence and associativity rules), before doing other canonicalization. + // + // If we did this *during* canonicalization, then each time we + // visited a BinOp node we'd recursively try to apply this to each of its nested + // operators, and then again on *their* nested operators, ultimately applying the + // rules multiple times unnecessarily. + let loc_expr = operator::desugar_expr(arena, &loc_expr); + + let mut scope = Scope::new(home, IdentIds::default(), Default::default()); + scope.add_alias( + Symbol::NUM_INT, + Region::zero(), + vec![Loc::at_zero(AliasVar::unbound( + "a".into(), + Variable::EMPTY_RECORD, + ))], + vec![], + Type::EmptyRec, + roc_types::types::AliasKind::Structural, + ); + + let dep_idents = IdentIds::exposed_builtins(0); + let mut env = Env::new(arena, home, &dep_idents, &module_ids); + let (loc_expr, output) = canonicalize_expr( + &mut env, + &mut var_store, + &mut scope, + Region::zero(), + &loc_expr.value, + ); + + let mut all_ident_ids = IdentIds::exposed_builtins(1); + all_ident_ids.insert(home, scope.locals.ident_ids); + + let interns = Interns { + module_ids: env.module_ids.clone(), + all_ident_ids, + }; + + CanExprOut { + loc_expr, + output, + problems: env.problems, + home: env.home, + var_store, + interns, + var, + } +} + +#[allow(dead_code)] +pub fn mut_map_from_pairs(pairs: I) -> MutMap +where + I: IntoIterator, + K: Hash + Eq, +{ + let mut answer = MutMap::default(); + + for (key, value) in pairs { + answer.insert(key, value); + } + + answer +} diff --git a/compiler/can/tests/test_can.rs b/crates/compiler/can/tests/test_can.rs similarity index 87% rename from compiler/can/tests/test_can.rs rename to crates/compiler/can/tests/test_can.rs index 5815842dab..2afe3eff29 100644 --- a/compiler/can/tests/test_can.rs +++ b/crates/compiler/can/tests/test_can.rs @@ -14,8 +14,10 @@ mod helpers; mod test_can { use crate::helpers::{can_expr_with, test_home, CanExprOut}; use bumpalo::Bump; + use core::panic; use roc_can::expr::Expr::{self, *}; use roc_can::expr::{ClosureData, IntValue, Recursive}; + use roc_module::symbol::Symbol; use roc_problem::can::{CycleEntry, FloatErrorKind, IntErrorKind, Problem, RuntimeError}; use roc_region::all::{Position, Region}; use std::{f64, i64}; @@ -330,7 +332,7 @@ mod test_can { matches!( problem, Problem::SignatureDefMismatch { .. } - | Problem::RuntimeError(RuntimeError::LookupNotInScope(_, _)) + | Problem::RuntimeError(RuntimeError::LookupNotInScope { .. }) ) })); } @@ -358,7 +360,7 @@ mod test_can { matches!( problem, Problem::SignatureDefMismatch { .. } - | Problem::RuntimeError(RuntimeError::LookupNotInScope(_, _)) + | Problem::RuntimeError(RuntimeError::LookupNotInScope { .. }) ) })); } @@ -377,8 +379,8 @@ mod test_can { let arena = Bump::new(); let CanExprOut { problems, .. } = can_expr_with(&arena, test_home(), src); - assert_eq!(problems.len(), 1); - assert!(problems.iter().all(|problem| matches!( + assert_eq!(problems.len(), 2); + assert!(problems.iter().any(|problem| matches!( problem, Problem::RuntimeError(RuntimeError::Shadowing { .. }) ))); @@ -398,8 +400,8 @@ mod test_can { let arena = Bump::new(); let CanExprOut { problems, .. } = can_expr_with(&arena, test_home(), src); - assert_eq!(problems.len(), 1); - assert!(problems.iter().all(|problem| matches!( + assert_eq!(problems.len(), 2); + assert!(problems.iter().any(|problem| matches!( problem, Problem::RuntimeError(RuntimeError::Shadowing { .. }) ))); @@ -419,9 +421,9 @@ mod test_can { let arena = Bump::new(); let CanExprOut { problems, .. } = can_expr_with(&arena, test_home(), src); - assert_eq!(problems.len(), 1); - println!("{:#?}", problems); - assert!(problems.iter().all(|problem| matches!( + assert_eq!(problems.len(), 2); + println!("{problems:#?}"); + assert!(problems.iter().any(|problem| matches!( problem, Problem::RuntimeError(RuntimeError::Shadowing { .. }) ))); @@ -655,6 +657,212 @@ mod test_can { )); } + // RECORD BUILDERS + #[test] + fn record_builder_desugar() { + let src = indoc!( + r#" + succeed = \_ -> crash "succeed" + apply = \_ -> crash "get" + + d = 3 + + succeed { + a: 1, + b: <- apply "b", + c: <- apply "c", + d + } + "# + ); + let arena = Bump::new(); + let out = can_expr_with(&arena, test_home(), src); + + assert_eq!(out.problems.len(), 0); + + // Assert that we desugar to: + // + // (apply "c") ((apply "b") (succeed \b -> \c -> { a: 1, b, c, d })) + + // (apply "c") .. + let (apply_c, c_to_b) = simplify_curried_call(&out.loc_expr.value); + assert_apply_call(apply_c, "c", &out.interns); + + // (apply "b") .. + let (apply_b, b_to_succeed) = simplify_curried_call(c_to_b); + assert_apply_call(apply_b, "b", &out.interns); + + // (succeed ..) + let (succeed, b_closure) = simplify_curried_call(b_to_succeed); + + match succeed { + Var(sym, _) => assert_eq!(sym.as_str(&out.interns), "succeed"), + _ => panic!("Not calling succeed: {:?}", succeed), + } + + // \b -> .. + let (b_sym, c_closure) = simplify_builder_closure(b_closure); + + // \c -> .. + let (c_sym, c_body) = simplify_builder_closure(c_closure); + + // { a: 1, b, c, d } + match c_body { + Record { fields, .. } => { + match get_field_expr(fields, "a") { + Num(_, num_str, _, _) => { + assert_eq!(num_str.to_string(), "1"); + } + expr => panic!("a is not a Num: {:?}", expr), + } + + assert_eq!(get_field_var_sym(fields, "b"), b_sym); + assert_eq!(get_field_var_sym(fields, "c"), c_sym); + assert_eq!(get_field_var_sym(fields, "d").as_str(&out.interns), "d"); + } + _ => panic!("Closure body wasn't a Record: {:?}", c_body), + } + } + + fn simplify_curried_call(expr: &Expr) -> (&Expr, &Expr) { + match expr { + LetNonRec(_, loc_expr) => simplify_curried_call(&loc_expr.value), + Call(fun, args, _) => (&fun.1.value, &args[0].1.value), + _ => panic!("Final Expr is not a Call: {:?}", expr), + } + } + + fn assert_apply_call(expr: &Expr, expected: &str, interns: &roc_module::symbol::Interns) { + match simplify_curried_call(expr) { + (Var(sym, _), Str(val)) => { + assert_eq!(sym.as_str(interns), "apply"); + assert_eq!(val.to_string(), expected); + } + call => panic!("Not a valid (get {}) call: {:?}", expected, call), + }; + } + + fn simplify_builder_closure(expr: &Expr) -> (Symbol, &Expr) { + use roc_can::pattern::Pattern::*; + + match expr { + Closure(closure) => match &closure.arguments[0].2.value { + Identifier(sym) => (*sym, &closure.loc_body.value), + pattern => panic!("Not an identifier pattern: {:?}", pattern), + }, + _ => panic!("Not a closure: {:?}", expr), + } + } + + fn get_field_expr<'a>( + fields: &'a roc_collections::SendMap, + name: &'a str, + ) -> &'a Expr { + let ident = roc_module::ident::Lowercase::from(name); + + &fields.get(&ident).unwrap().loc_expr.value + } + + fn get_field_var_sym( + fields: &roc_collections::SendMap, + name: &str, + ) -> roc_module::symbol::Symbol { + match get_field_expr(fields, name) { + Var(sym, _) => *sym, + expr => panic!("Not a var: {:?}", expr), + } + } + + #[test] + fn record_builder_field_names_do_not_shadow() { + let src = indoc!( + r#" + succeed = \_ -> crash "succeed" + parse = \_ -> crash "parse" + + number = "42" + + succeed { + number: <- parse number, + raw: number, + } + "# + ); + let arena = Bump::new(); + let out = can_expr_with(&arena, test_home(), src); + + assert_eq!(out.problems.len(), 0); + + let (_, number_to_succeed) = simplify_curried_call(&out.loc_expr.value); + let (_, number_closure) = simplify_curried_call(number_to_succeed); + let (apply_number_sym, record) = simplify_builder_closure(number_closure); + + match record { + Record { fields, .. } => { + assert_eq!(get_field_var_sym(fields, "number"), apply_number_sym); + + match get_field_expr(fields, "raw") { + Var(number_sym, _) => { + assert_ne!(number_sym.ident_id(), apply_number_sym.ident_id()); + assert_eq!(number_sym.as_str(&out.interns), "number") + } + expr => panic!("a is not a Num: {:?}", expr), + } + } + _ => panic!("Closure body wasn't a Record: {:?}", record), + } + } + + #[test] + fn multiple_record_builders_error() { + let src = indoc!( + r#" + succeed + { a: <- apply "a" } + { b: <- apply "b" } + "# + ); + let arena = Bump::new(); + let CanExprOut { + problems, loc_expr, .. + } = can_expr_with(&arena, test_home(), src); + + assert_eq!(problems.len(), 1); + assert!(problems.iter().all(|problem| matches!( + problem, + Problem::RuntimeError(roc_problem::can::RuntimeError::MultipleRecordBuilders { .. }) + ))); + + assert!(matches!( + loc_expr.value, + Expr::RuntimeError(roc_problem::can::RuntimeError::MultipleRecordBuilders { .. }) + )); + } + + #[test] + fn hanging_record_builder() { + let src = indoc!( + r#" + { a: <- apply "a" } + "# + ); + let arena = Bump::new(); + let CanExprOut { + problems, loc_expr, .. + } = can_expr_with(&arena, test_home(), src); + + assert_eq!(problems.len(), 1); + assert!(problems.iter().all(|problem| matches!( + problem, + Problem::RuntimeError(roc_problem::can::RuntimeError::UnappliedRecordBuilder { .. }) + ))); + + assert!(matches!( + loc_expr.value, + Expr::RuntimeError(roc_problem::can::RuntimeError::UnappliedRecordBuilder { .. }) + )); + } + // TAIL CALLS fn get_closure(expr: &Expr, i: usize) -> roc_can::expr::Recursive { match expr { @@ -917,37 +1125,6 @@ mod test_can { assert_eq!(is_circular_def, false); } - #[test] - fn invalid_self_recursion() { - let src = indoc!( - r#" - x = x - - x - "# - ); - - let home = test_home(); - let arena = Bump::new(); - let CanExprOut { - loc_expr, - problems, - interns, - .. - } = can_expr_with(&arena, home, src); - - let is_circular_def = matches!(loc_expr.value, RuntimeError(RuntimeError::CircularDef(_))); - - let problem = Problem::RuntimeError(RuntimeError::CircularDef(vec![CycleEntry { - symbol: interns.symbol(home, "x".into()), - symbol_region: Region::new(Position::new(0), Position::new(1)), - expr_region: Region::new(Position::new(4), Position::new(5)), - }])); - - assert_eq!(is_circular_def, true); - assert_eq!(problems, vec![problem]); - } - #[test] fn invalid_mutual_recursion() { let src = indoc!( @@ -1000,7 +1177,7 @@ mod test_can { fn dict() { let src = indoc!( r#" - x = Dict.empty + x = Dict.empty {} Dict.len x "# @@ -1023,7 +1200,7 @@ mod test_can { # There was a bug where annotating a def meant that its # references no longer got reported. # - # https://github.com/rtfeldman/roc/issues/298 + # https://github.com/roc-lang/roc/issues/298 x : List Booly x = [y] diff --git a/crates/compiler/checkmate/Cargo.toml b/crates/compiler/checkmate/Cargo.toml new file mode 100644 index 0000000000..3fd0d6f39e --- /dev/null +++ b/crates/compiler/checkmate/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "roc_checkmate" +description = "A framework for debugging the solver." + +authors.workspace = true +edition.workspace = true +license.workspace = true +version.workspace = true + +[dependencies] +roc_checkmate_schema = { path = "../checkmate_schema" } +roc_module = { path = "../module" } +roc_solve_schema = { path = "../solve_schema" } +roc_types = { path = "../types" } +chrono.workspace = true + +[build-dependencies] +roc_checkmate_schema = { path = "../checkmate_schema" } +serde_json.workspace = true diff --git a/crates/compiler/checkmate/README.md b/crates/compiler/checkmate/README.md new file mode 100644 index 0000000000..9a21d69bf5 --- /dev/null +++ b/crates/compiler/checkmate/README.md @@ -0,0 +1,5 @@ +# `checkmate` + +A tool to debug the solver (checker + inference + specialization engine). + +See [the document](https://rwx.notion.site/Type-debugging-tools-de42260060784cacbaf08ea4d61e0eb9?pvs=4). diff --git a/crates/compiler/checkmate/build.rs b/crates/compiler/checkmate/build.rs new file mode 100644 index 0000000000..60e290b152 --- /dev/null +++ b/crates/compiler/checkmate/build.rs @@ -0,0 +1,13 @@ +use std::fs; + +use roc_checkmate_schema::AllEvents; + +fn main() { + println!("cargo:rerun-if-changed=../checkmate_schema"); + let schema = AllEvents::schema(); + fs::write( + "schema.json", + serde_json::to_string_pretty(&schema).unwrap(), + ) + .unwrap(); +} diff --git a/crates/compiler/checkmate/schema.json b/crates/compiler/checkmate/schema.json new file mode 100644 index 0000000000..27ccb95eff --- /dev/null +++ b/crates/compiler/checkmate/schema.json @@ -0,0 +1,907 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "AllEvents", + "type": "array", + "items": { + "$ref": "#/definitions/Event" + }, + "definitions": { + "AliasKind": { + "oneOf": [ + { + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "Structural" + ] + } + } + }, + { + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "Opaque" + ] + } + } + } + ] + }, + "AliasTypeVariables": { + "type": "object", + "required": [ + "infer_ext_in_output_position_variables", + "lambda_set_variables", + "type_variables" + ], + "properties": { + "infer_ext_in_output_position_variables": { + "type": "array", + "items": { + "$ref": "#/definitions/Variable" + } + }, + "lambda_set_variables": { + "type": "array", + "items": { + "$ref": "#/definitions/Variable" + } + }, + "type_variables": { + "type": "array", + "items": { + "$ref": "#/definitions/Variable" + } + } + } + }, + "ClosureType": { + "type": "object", + "required": [ + "environment", + "function" + ], + "properties": { + "environment": { + "type": "array", + "items": { + "$ref": "#/definitions/Variable" + } + }, + "function": { + "$ref": "#/definitions/Symbol" + } + } + }, + "Content": { + "oneOf": [ + { + "type": "object", + "required": [ + "type" + ], + "properties": { + "name": { + "type": [ + "string", + "null" + ] + }, + "type": { + "type": "string", + "enum": [ + "Flex" + ] + } + } + }, + { + "type": "object", + "required": [ + "name", + "type" + ], + "properties": { + "name": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "Rigid" + ] + } + } + }, + { + "type": "object", + "required": [ + "abilities", + "type" + ], + "properties": { + "abilities": { + "type": "array", + "items": { + "$ref": "#/definitions/Symbol" + } + }, + "name": { + "type": [ + "string", + "null" + ] + }, + "type": { + "type": "string", + "enum": [ + "FlexAble" + ] + } + } + }, + { + "type": "object", + "required": [ + "abilities", + "name", + "type" + ], + "properties": { + "abilities": { + "type": "array", + "items": { + "$ref": "#/definitions/Symbol" + } + }, + "name": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "RigidAble" + ] + } + } + }, + { + "type": "object", + "required": [ + "structure", + "type" + ], + "properties": { + "name": { + "type": [ + "string", + "null" + ] + }, + "structure": { + "$ref": "#/definitions/Variable" + }, + "type": { + "type": "string", + "enum": [ + "Recursive" + ] + } + } + }, + { + "type": "object", + "required": [ + "ambient_function", + "solved", + "type", + "unspecialized" + ], + "properties": { + "ambient_function": { + "$ref": "#/definitions/Variable" + }, + "recursion_var": { + "anyOf": [ + { + "$ref": "#/definitions/Variable" + }, + { + "type": "null" + } + ] + }, + "solved": { + "type": "array", + "items": { + "$ref": "#/definitions/ClosureType" + } + }, + "type": { + "type": "string", + "enum": [ + "LambdaSet" + ] + }, + "unspecialized": { + "type": "array", + "items": { + "$ref": "#/definitions/UnspecializedClosureType" + } + } + } + }, + { + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "ErasedLambda" + ] + } + } + }, + { + "type": "object", + "required": [ + "kind", + "name", + "real_variable", + "type", + "variables" + ], + "properties": { + "kind": { + "$ref": "#/definitions/AliasKind" + }, + "name": { + "$ref": "#/definitions/Symbol" + }, + "real_variable": { + "$ref": "#/definitions/Variable" + }, + "type": { + "type": "string", + "enum": [ + "Alias" + ] + }, + "variables": { + "$ref": "#/definitions/AliasTypeVariables" + } + } + }, + { + "type": "object", + "required": [ + "symbol", + "type", + "variables" + ], + "properties": { + "symbol": { + "$ref": "#/definitions/Symbol" + }, + "type": { + "type": "string", + "enum": [ + "Apply" + ] + }, + "variables": { + "type": "array", + "items": { + "$ref": "#/definitions/Variable" + } + } + } + }, + { + "type": "object", + "required": [ + "arguments", + "lambda_type", + "ret", + "type" + ], + "properties": { + "arguments": { + "type": "array", + "items": { + "$ref": "#/definitions/Variable" + } + }, + "lambda_type": { + "$ref": "#/definitions/Variable" + }, + "ret": { + "$ref": "#/definitions/Variable" + }, + "type": { + "type": "string", + "enum": [ + "Function" + ] + } + } + }, + { + "type": "object", + "required": [ + "extension", + "fields", + "type" + ], + "properties": { + "extension": { + "$ref": "#/definitions/Variable" + }, + "fields": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/RecordField" + } + }, + "type": { + "type": "string", + "enum": [ + "Record" + ] + } + } + }, + { + "type": "object", + "required": [ + "elements", + "extension", + "type" + ], + "properties": { + "elements": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/Variable" + } + }, + "extension": { + "$ref": "#/definitions/Variable" + }, + "type": { + "type": "string", + "enum": [ + "Tuple" + ] + } + } + }, + { + "type": "object", + "required": [ + "extension", + "tags", + "type" + ], + "properties": { + "extension": { + "$ref": "#/definitions/TagUnionExtension" + }, + "tags": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "$ref": "#/definitions/Variable" + } + } + }, + "type": { + "type": "string", + "enum": [ + "TagUnion" + ] + } + } + }, + { + "type": "object", + "required": [ + "extension", + "functions", + "tags", + "type" + ], + "properties": { + "extension": { + "$ref": "#/definitions/TagUnionExtension" + }, + "functions": { + "type": "array", + "items": { + "$ref": "#/definitions/Symbol" + } + }, + "tags": { + "type": "array", + "items": { + "type": "string" + } + }, + "type": { + "type": "string", + "enum": [ + "FunctionOrTagUnion" + ] + } + } + }, + { + "type": "object", + "required": [ + "extension", + "recursion_var", + "tags", + "type" + ], + "properties": { + "extension": { + "$ref": "#/definitions/TagUnionExtension" + }, + "recursion_var": { + "$ref": "#/definitions/Variable" + }, + "tags": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "$ref": "#/definitions/Variable" + } + } + }, + "type": { + "type": "string", + "enum": [ + "RecursiveTagUnion" + ] + } + } + }, + { + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "EmptyRecord" + ] + } + } + }, + { + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "EmptyTuple" + ] + } + } + }, + { + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "EmptyTagUnion" + ] + } + } + }, + { + "type": "object", + "required": [ + "range", + "type" + ], + "properties": { + "range": { + "$ref": "#/definitions/NumericRange" + }, + "type": { + "type": "string", + "enum": [ + "RangedNumber" + ] + } + } + }, + { + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "Error" + ] + } + } + } + ] + }, + "Event": { + "oneOf": [ + { + "type": "object", + "required": [ + "left", + "mode", + "right", + "subevents", + "type" + ], + "properties": { + "left": { + "$ref": "#/definitions/Variable" + }, + "mode": { + "$ref": "#/definitions/UnificationMode" + }, + "right": { + "$ref": "#/definitions/Variable" + }, + "subevents": { + "type": "array", + "items": { + "$ref": "#/definitions/Event" + } + }, + "success": { + "type": [ + "boolean", + "null" + ] + }, + "type": { + "type": "string", + "enum": [ + "Unification" + ] + } + } + }, + { + "type": "object", + "required": [ + "from", + "to", + "type" + ], + "properties": { + "from": { + "$ref": "#/definitions/Variable" + }, + "to": { + "$ref": "#/definitions/Variable" + }, + "type": { + "type": "string", + "enum": [ + "VariableUnified" + ] + } + } + }, + { + "type": "object", + "required": [ + "type", + "variable" + ], + "properties": { + "content": { + "anyOf": [ + { + "$ref": "#/definitions/Content" + }, + { + "type": "null" + } + ] + }, + "rank": { + "anyOf": [ + { + "$ref": "#/definitions/Rank" + }, + { + "type": "null" + } + ] + }, + "type": { + "type": "string", + "enum": [ + "VariableSetDescriptor" + ] + }, + "variable": { + "$ref": "#/definitions/Variable" + } + } + } + ] + }, + "NumericRange": { + "type": "object", + "required": [ + "kind", + "min_width", + "signed" + ], + "properties": { + "kind": { + "$ref": "#/definitions/NumericRangeKind" + }, + "min_width": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "signed": { + "type": "boolean" + } + } + }, + "NumericRangeKind": { + "oneOf": [ + { + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "Int" + ] + } + } + }, + { + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "AnyNum" + ] + } + } + } + ] + }, + "Rank": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "RecordField": { + "type": "object", + "required": [ + "field_type", + "kind" + ], + "properties": { + "field_type": { + "$ref": "#/definitions/Variable" + }, + "kind": { + "$ref": "#/definitions/RecordFieldKind" + } + } + }, + "RecordFieldKind": { + "oneOf": [ + { + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "Demanded" + ] + } + } + }, + { + "type": "object", + "required": [ + "rigid", + "type" + ], + "properties": { + "rigid": { + "type": "boolean" + }, + "type": { + "type": "string", + "enum": [ + "Required" + ] + } + } + }, + { + "type": "object", + "required": [ + "rigid", + "type" + ], + "properties": { + "rigid": { + "type": "boolean" + }, + "type": { + "type": "string", + "enum": [ + "Optional" + ] + } + } + } + ] + }, + "Symbol": { + "type": "string" + }, + "TagUnionExtension": { + "oneOf": [ + { + "type": "object", + "required": [ + "type", + "variable" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "Openness" + ] + }, + "variable": { + "$ref": "#/definitions/Variable" + } + } + }, + { + "type": "object", + "required": [ + "type", + "variable" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "Any" + ] + }, + "variable": { + "$ref": "#/definitions/Variable" + } + } + } + ] + }, + "UnificationMode": { + "oneOf": [ + { + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "Eq" + ] + } + } + }, + { + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "Present" + ] + } + } + }, + { + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "LambdaSetSpecialization" + ] + } + } + } + ] + }, + "UnspecializedClosureType": { + "type": "object", + "required": [ + "ability_member", + "lambda_set_region", + "specialization" + ], + "properties": { + "ability_member": { + "$ref": "#/definitions/Symbol" + }, + "lambda_set_region": { + "type": "integer", + "format": "uint8", + "minimum": 0.0 + }, + "specialization": { + "$ref": "#/definitions/Variable" + } + } + }, + "Variable": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + } +} \ No newline at end of file diff --git a/crates/compiler/checkmate/src/collector.rs b/crates/compiler/checkmate/src/collector.rs new file mode 100644 index 0000000000..e727769749 --- /dev/null +++ b/crates/compiler/checkmate/src/collector.rs @@ -0,0 +1,175 @@ +use std::error::Error; + +use roc_checkmate_schema::{AllEvents, Event}; +use roc_types::subs as s; + +use crate::convert::AsSchema; + +#[derive(Debug)] +pub struct Collector { + events: AllEvents, + current_event_path: Vec, +} + +impl Default for Collector { + fn default() -> Self { + Self::new() + } +} + +impl Collector { + pub fn new() -> Self { + Self { + events: AllEvents(Vec::new()), + current_event_path: Vec::new(), + } + } + + pub fn unify(&mut self, subs: &s::Subs, from: s::Variable, to: s::Variable) { + let to = to.as_schema(subs); + let from = from.as_schema(subs); + self.add_event(Event::VariableUnified { to, from }); + } + + pub fn set_content(&mut self, subs: &s::Subs, var: s::Variable, content: s::Content) { + let variable = var.as_schema(subs); + let content = content.as_schema(subs); + self.add_event(Event::VariableSetDescriptor { + variable, + content: Some(content), + rank: None, + }); + } + + pub fn set_rank(&mut self, subs: &s::Subs, var: s::Variable, rank: s::Rank) { + let variable = var.as_schema(subs); + let rank = rank.as_schema(subs); + self.add_event(Event::VariableSetDescriptor { + variable, + rank: Some(rank), + content: None, + }); + } + + pub fn set_descriptor(&mut self, subs: &s::Subs, var: s::Variable, descriptor: s::Descriptor) { + let variable = var.as_schema(subs); + let rank = descriptor.rank.as_schema(subs); + let content = descriptor.content.as_schema(subs); + self.add_event(Event::VariableSetDescriptor { + variable, + rank: Some(rank), + content: Some(content), + }); + } + + pub fn start_unification( + &mut self, + subs: &s::Subs, + left: s::Variable, + right: s::Variable, + mode: roc_solve_schema::UnificationMode, + ) { + let left = left.as_schema(subs); + let right = right.as_schema(subs); + let mode = mode.as_schema(subs); + let subevents = Vec::new(); + self.add_event(Event::Unification { + left, + right, + mode, + subevents, + success: None, + }); + } + + pub fn end_unification( + &mut self, + subs: &s::Subs, + left: s::Variable, + right: s::Variable, + success: bool, + ) { + let current_event = self.get_path_event(); + match current_event { + EventW::Sub(Event::Unification { + left: l, + right: r, + success: s, + .. + }) => { + assert_eq!(left.as_schema(subs), *l); + assert_eq!(right.as_schema(subs), *r); + assert!(s.is_none()); + *s = Some(success); + } + _ => panic!("end_unification called when not in a unification"), + } + self.current_event_path.pop(); + } + + pub fn write(&self, writer: impl std::io::Write) -> Result<(), Box> { + self.events.write(writer)?; + Ok(()) + } + + fn add_event(&mut self, event: impl Into) { + let mut event = event.into(); + let is_appendable = EventW::Sub(&mut event).appendable(); + let event = event; + + let path_event = self.get_path_event(); + let new_event_index = path_event.append(event); + if is_appendable { + self.current_event_path.push(new_event_index); + } + } + + fn get_path_event(&mut self) -> EventW { + let mut event = EventW::Top(&mut self.events); + for i in &self.current_event_path { + event = event.index(*i); + } + event + } +} + +enum EventW<'a> { + Top(&'a mut AllEvents), + Sub(&'a mut Event), +} + +impl<'a> EventW<'a> { + fn append(self, event: Event) -> usize { + let list = self.subevents_mut().unwrap(); + let index = list.len(); + list.push(event); + index + } + + fn appendable(self) -> bool { + self.subevents().is_some() + } + + fn index(self, index: usize) -> EventW<'a> { + Self::Sub(&mut self.subevents_mut().unwrap()[index]) + } +} + +impl<'a> EventW<'a> { + fn subevents(self) -> Option<&'a Vec> { + use EventW::*; + match self { + Top(events) => Some(&events.0), + Sub(Event::Unification { subevents, .. }) => Some(subevents), + Sub(Event::VariableUnified { .. } | Event::VariableSetDescriptor { .. }) => None, + } + } + fn subevents_mut(self) -> Option<&'a mut Vec> { + use EventW::*; + match self { + Top(events) => Some(&mut events.0), + Sub(Event::Unification { subevents, .. }) => Some(subevents), + Sub(Event::VariableUnified { .. } | Event::VariableSetDescriptor { .. }) => None, + } + } +} diff --git a/crates/compiler/checkmate/src/convert.rs b/crates/compiler/checkmate/src/convert.rs new file mode 100644 index 0000000000..12ff472d5a --- /dev/null +++ b/crates/compiler/checkmate/src/convert.rs @@ -0,0 +1,323 @@ +use std::collections::HashMap; + +use roc_module::{ident, symbol}; +use roc_types::{ + num, + subs::{self, GetSubsSlice, Subs, SubsIndex, SubsSlice, UnionLabels}, + types, +}; + +use roc_checkmate_schema::{ + AliasKind, AliasTypeVariables, ClosureType, Content, NumericRange, NumericRangeKind, Rank, + RecordField, RecordFieldKind, Symbol, TagUnionExtension, UnificationMode, + UnspecializedClosureType, Variable, +}; + +pub trait AsSchema { + fn as_schema(&self, subs: &Subs) -> T; +} + +impl AsSchema> for Option +where + T: AsSchema, + T: Copy, +{ + fn as_schema(&self, subs: &Subs) -> Option { + self.map(|i| i.as_schema(subs)) + } +} + +impl AsSchema> for &[T] +where + T: AsSchema, +{ + fn as_schema(&self, subs: &Subs) -> Vec { + self.iter().map(|i| i.as_schema(subs)).collect() + } +} + +impl AsSchema for SubsIndex +where + Subs: std::ops::Index, Output = T>, + T: AsSchema, +{ + fn as_schema(&self, subs: &Subs) -> U { + subs[*self].as_schema(subs) + } +} + +impl AsSchema> for SubsSlice +where + Subs: GetSubsSlice, + T: AsSchema, +{ + fn as_schema(&self, subs: &Subs) -> Vec { + subs.get_subs_slice(*self) + .iter() + .map(|i| i.as_schema(subs)) + .collect() + } +} + +impl AsSchema for subs::Content { + fn as_schema(&self, subs: &Subs) -> Content { + use {subs::Content as A, Content as B}; + match self { + A::FlexVar(name) => B::Flex(name.as_schema(subs)), + A::RigidVar(name) => B::Rigid(name.as_schema(subs)), + A::FlexAbleVar(name, abilities) => { + B::FlexAble(name.as_schema(subs), abilities.as_schema(subs)) + } + A::RigidAbleVar(name, abilities) => { + B::RigidAble(name.as_schema(subs), abilities.as_schema(subs)) + } + A::RecursionVar { + structure, + opt_name, + } => B::Recursive(opt_name.as_schema(subs), structure.as_schema(subs)), + A::LambdaSet(lambda_set) => lambda_set.as_schema(subs), + A::ErasedLambda => B::ErasedLambda(), + A::Structure(flat_type) => flat_type.as_schema(subs), + A::Alias(name, type_vars, real_var, kind) => B::Alias( + name.as_schema(subs), + type_vars.as_schema(subs), + real_var.as_schema(subs), + kind.as_schema(subs), + ), + A::RangedNumber(range) => B::RangedNumber(range.as_schema(subs)), + A::Error => B::Error(), + } + } +} + +impl AsSchema for subs::FlatType { + fn as_schema(&self, subs: &Subs) -> Content { + match self { + subs::FlatType::Apply(symbol, variables) => { + Content::Apply(symbol.as_schema(subs), variables.as_schema(subs)) + } + subs::FlatType::Func(arguments, closure, ret) => Content::Function( + arguments.as_schema(subs), + closure.as_schema(subs), + ret.as_schema(subs), + ), + subs::FlatType::Record(fields, ext) => { + Content::Record(fields.as_schema(subs), ext.as_schema(subs)) + } + subs::FlatType::Tuple(elems, ext) => { + Content::Tuple(elems.as_schema(subs), ext.as_schema(subs)) + } + subs::FlatType::TagUnion(tags, ext) => { + Content::TagUnion(tags.as_schema(subs), ext.as_schema(subs)) + } + subs::FlatType::FunctionOrTagUnion(tags, functions, ext) => { + Content::FunctionOrTagUnion( + functions.as_schema(subs), + tags.as_schema(subs), + ext.as_schema(subs), + ) + } + subs::FlatType::RecursiveTagUnion(rec_var, tags, ext) => Content::RecursiveTagUnion( + rec_var.as_schema(subs), + tags.as_schema(subs), + ext.as_schema(subs), + ), + subs::FlatType::EmptyRecord => Content::EmptyRecord(), + subs::FlatType::EmptyTuple => Content::EmptyTuple(), + subs::FlatType::EmptyTagUnion => Content::EmptyTagUnion(), + } + } +} + +impl AsSchema for subs::LambdaSet { + fn as_schema(&self, subs: &Subs) -> Content { + let subs::LambdaSet { + solved, + unspecialized, + recursion_var, + ambient_function, + } = self; + + Content::LambdaSet( + solved.as_schema(subs), + unspecialized.as_schema(subs), + recursion_var.as_schema(subs), + ambient_function.as_schema(subs), + ) + } +} + +impl AsSchema for ident::Lowercase { + fn as_schema(&self, _subs: &Subs) -> String { + self.to_string() + } +} + +impl AsSchema for symbol::Symbol { + fn as_schema(&self, _subs: &Subs) -> Symbol { + Symbol(format!("{:#?}", self)) + } +} + +impl AsSchema for subs::Variable { + fn as_schema(&self, _subs: &Subs) -> Variable { + Variable(self.index()) + } +} + +impl AsSchema> for subs::OptVariable { + fn as_schema(&self, _subs: &Subs) -> Option { + self.into_variable().map(|i| i.as_schema(_subs)) + } +} + +impl AsSchema> for UnionLabels { + fn as_schema(&self, subs: &Subs) -> Vec { + self.iter_from_subs(subs) + .map(|(function, environment)| ClosureType { + function: function.as_schema(subs), + environment: environment.as_schema(subs), + }) + .collect() + } +} + +impl AsSchema for types::Uls { + fn as_schema(&self, subs: &Subs) -> UnspecializedClosureType { + let types::Uls(specialization, ability_member, lambda_set_region) = self; + + UnspecializedClosureType { + specialization: specialization.as_schema(subs), + ability_member: ability_member.as_schema(subs), + lambda_set_region: *lambda_set_region, + } + } +} + +impl AsSchema for subs::AliasVariables { + fn as_schema(&self, subs: &Subs) -> AliasTypeVariables { + let type_variables = self.type_variables().as_schema(subs); + let lambda_set_variables = self.lambda_set_variables().as_schema(subs); + let infer_ext_in_output_position_variables = + self.infer_ext_in_output_variables().as_schema(subs); + + AliasTypeVariables { + type_variables, + lambda_set_variables, + infer_ext_in_output_position_variables, + } + } +} + +impl AsSchema for types::AliasKind { + fn as_schema(&self, _subs: &Subs) -> AliasKind { + match self { + types::AliasKind::Structural => AliasKind::Structural, + types::AliasKind::Opaque => AliasKind::Opaque, + } + } +} + +impl AsSchema> for subs::RecordFields { + fn as_schema(&self, subs: &Subs) -> HashMap { + let mut map = HashMap::new(); + for (name, var, field) in self.iter_all() { + let name = name.as_schema(subs); + let field_type = var.as_schema(subs); + let kind = field.as_schema(subs); + map.insert(name, RecordField { field_type, kind }); + } + map + } +} + +impl AsSchema for types::RecordField<()> { + fn as_schema(&self, _subs: &Subs) -> RecordFieldKind { + match self { + types::RecordField::Demanded(_) => RecordFieldKind::Demanded, + types::RecordField::Required(_) => RecordFieldKind::Required { rigid: false }, + types::RecordField::Optional(_) => RecordFieldKind::Optional { rigid: false }, + types::RecordField::RigidRequired(_) => RecordFieldKind::Required { rigid: true }, + types::RecordField::RigidOptional(_) => RecordFieldKind::Optional { rigid: true }, + } + } +} + +impl AsSchema> for subs::TupleElems { + fn as_schema(&self, subs: &Subs) -> HashMap { + let mut map = HashMap::new(); + for (index, var) in self.iter_all() { + let name = subs[index] as _; + let var = var.as_schema(subs); + map.insert(name, var); + } + map + } +} + +impl AsSchema>> for subs::UnionTags { + fn as_schema(&self, subs: &Subs) -> HashMap> { + let mut map = HashMap::new(); + for (tag, payloads) in self.iter_from_subs(subs) { + map.insert(tag.as_schema(subs), payloads.as_schema(subs)); + } + map + } +} + +impl AsSchema for subs::TagExt { + fn as_schema(&self, subs: &Subs) -> TagUnionExtension { + match self { + subs::TagExt::Openness(var) => TagUnionExtension::Openness(var.as_schema(subs)), + subs::TagExt::Any(var) => TagUnionExtension::Any(var.as_schema(subs)), + } + } +} + +impl AsSchema for num::NumericRange { + fn as_schema(&self, _subs: &Subs) -> NumericRange { + let kind = + match self { + num::NumericRange::IntAtLeastSigned(_) + | num::NumericRange::IntAtLeastEitherSign(_) => NumericRangeKind::Int, + num::NumericRange::NumAtLeastSigned(_) + | num::NumericRange::NumAtLeastEitherSign(_) => NumericRangeKind::AnyNum, + }; + + let min_width = self.min_width(); + let (signedness, width) = min_width.signedness_and_width(); + let signed = signedness.is_signed(); + + NumericRange { + kind, + signed, + min_width: width, + } + } +} + +impl AsSchema for ident::TagName { + fn as_schema(&self, _subs: &Subs) -> String { + self.0.to_string() + } +} + +impl AsSchema for subs::Rank { + fn as_schema(&self, _subs: &Subs) -> Rank { + Rank(self.into_usize() as _) + } +} + +impl AsSchema for roc_solve_schema::UnificationMode { + fn as_schema(&self, _subs: &Subs) -> UnificationMode { + if self.is_eq() { + UnificationMode::Eq + } else if self.is_present() { + UnificationMode::Present + } else if self.is_lambda_set_specialization() { + UnificationMode::LambdaSetSpecialization + } else { + unreachable!() + } + } +} diff --git a/crates/compiler/checkmate/src/lib.rs b/crates/compiler/checkmate/src/lib.rs new file mode 100644 index 0000000000..5ea6ccc68f --- /dev/null +++ b/crates/compiler/checkmate/src/lib.rs @@ -0,0 +1,62 @@ +mod collector; +mod convert; + +pub use collector::Collector; + +pub fn is_checkmate_enabled() -> bool { + #[cfg(debug_assertions)] + { + let flag = std::env::var("ROC_CHECKMATE"); + flag.as_deref() == Ok("1") + } + #[cfg(not(debug_assertions))] + { + false + } +} + +#[macro_export] +macro_rules! debug_checkmate { + ($opt_collector:expr, $cm:ident => $expr:expr) => { + #[cfg(debug_assertions)] + { + if let Some($cm) = $opt_collector.as_mut() { + $expr + } + } + }; +} + +#[macro_export] +macro_rules! dump_checkmate { + ($opt_collector:expr) => { + #[cfg(debug_assertions)] + { + if let Some(cm) = $opt_collector.as_ref() { + $crate::dump_checkmate(cm); + } + } + }; +} + +pub fn dump_checkmate(collector: &Collector) { + let timestamp = chrono::Local::now().format("%Y%m%d_%H-%M-%S"); + let filename = format!("checkmate_{timestamp}.json"); + let fi = std::fs::File::create(&filename).unwrap(); + collector.write(fi).unwrap(); + eprintln!("Wrote checkmate output to {filename}"); +} + +#[macro_export] +macro_rules! with_checkmate { + ({ on => $on:expr, off => $off:expr, }) => {{ + #[cfg(debug_assertions)] + { + $on + } + #[cfg(not(debug_assertions))] + { + $off + } + }}; +} diff --git a/crates/compiler/checkmate/www/.gitignore b/crates/compiler/checkmate/www/.gitignore new file mode 100644 index 0000000000..d43e34580d --- /dev/null +++ b/crates/compiler/checkmate/www/.gitignore @@ -0,0 +1,17 @@ +/node_modules +/.pnp +.pnp.js + +/coverage + +/build + +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/crates/compiler/checkmate/www/.vim/coc-settings.json b/crates/compiler/checkmate/www/.vim/coc-settings.json new file mode 100644 index 0000000000..843796a4dc --- /dev/null +++ b/crates/compiler/checkmate/www/.vim/coc-settings.json @@ -0,0 +1,4 @@ +{ + "tsserver.useLocalTsdk": true, + "tsserver.tsdk": "${workspaceFolder}/node_modules/typescript/lib" +} \ No newline at end of file diff --git a/crates/compiler/checkmate/www/package-lock.json b/crates/compiler/checkmate/www/package-lock.json new file mode 100644 index 0000000000..c2d10140c8 --- /dev/null +++ b/crates/compiler/checkmate/www/package-lock.json @@ -0,0 +1,19129 @@ +{ + "name": "checkmate", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "checkmate", + "version": "0.1.0", + "dependencies": { + "elkjs": "^0.8.2", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-router-dom": "^6.14.2", + "react-router-hash-link": "^2.4.3", + "react-tooltip": "^5.19.0", + "reactflow": "^11.7.4" + }, + "devDependencies": { + "@babel/plugin-proposal-private-property-in-object": "^7.21.11", + "@types/jest": "^27.5.2", + "@types/node": "^16.18.38", + "@types/react": "^18.2.15", + "@types/react-dom": "^18.2.7", + "@types/react-router-hash-link": "^2.4.6", + "clsx": "^2.0.0", + "json-schema-to-typescript": "^13.0.2", + "react-scripts": "5.0.1", + "tailwindcss": "^3.3.3", + "tiny-typed-emitter": "^2.1.0", + "typescript": "^4.9.5" + } + }, + "node_modules/@aashutoshrathi/word-wrap": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", + "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", + "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.5.tgz", + "integrity": "sha512-Xmwn266vad+6DAqEB2A6V/CcZVp62BbwVmcOJc2RPuwih1kw02TjQvWVWlcKGbBPd+8/0V5DEkOcizRGYsspYQ==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.22.9", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.22.9.tgz", + "integrity": "sha512-5UamI7xkUcJ3i9qVDS+KFDEK8/7oJ55/sJMB1Ge7IEapr7KfdfV/HErR+koZwOfd+SgtFKOKRhRakdg++DcJpQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.22.9", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.22.9.tgz", + "integrity": "sha512-G2EgeufBcYw27U4hhoIwFcgc1XU7TlXJ3mv04oOv1WCuo900U/anZSPzEqNjwdjgffkk2Gs0AN0dW1CKVLcG7w==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.22.5", + "@babel/generator": "^7.22.9", + "@babel/helper-compilation-targets": "^7.22.9", + "@babel/helper-module-transforms": "^7.22.9", + "@babel/helpers": "^7.22.6", + "@babel/parser": "^7.22.7", + "@babel/template": "^7.22.5", + "@babel/traverse": "^7.22.8", + "@babel/types": "^7.22.5", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.2", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/eslint-parser": { + "version": "7.22.9", + "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.22.9.tgz", + "integrity": "sha512-xdMkt39/nviO/4vpVdrEYPwXCsYIXSSAr6mC7WQsNIlGnuxKyKE7GZjalcnbSWiC4OXGNNN3UQPeHfjSC6sTDA==", + "dev": true, + "dependencies": { + "@nicolo-ribaudo/eslint-scope-5-internals": "5.1.1-v1", + "eslint-visitor-keys": "^2.1.0", + "semver": "^6.3.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || >=14.0.0" + }, + "peerDependencies": { + "@babel/core": ">=7.11.0", + "eslint": "^7.5.0 || ^8.0.0" + } + }, + "node_modules/@babel/eslint-parser/node_modules/eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/@babel/eslint-parser/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.22.9", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.22.9.tgz", + "integrity": "sha512-KtLMbmicyuK2Ak/FTCJVbDnkN1SlT8/kceFTiuDiiRUUSMnHMidxSCdG4ndkTOHHpoomWe/4xkvHkEOncwjYIw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz", + "integrity": "sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-builder-binary-assignment-operator-visitor": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.22.5.tgz", + "integrity": "sha512-m1EP3lVOPptR+2DwD125gziZNcmoNSHGmJROKoy87loWUQyJaVXDgpmruWqDARZSmtYQ+Dl25okU8+qhVzuykw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.22.9", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.9.tgz", + "integrity": "sha512-7qYrNM6HjpnPHJbopxmb8hSPoZ0gsX8IvUS32JGVoy+pU9e5N0nLr1VjJoR6kA4d9dmGLxNYOjeB8sUDal2WMw==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.22.9", + "@babel/helper-validator-option": "^7.22.5", + "browserslist": "^4.21.9", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.22.9", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.22.9.tgz", + "integrity": "sha512-Pwyi89uO4YrGKxL/eNJ8lfEH55DnRloGPOseaA8NFNL6jAUnn+KccaISiFazCj5IolPPDjGSdzQzXVzODVRqUQ==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-function-name": "^7.22.5", + "@babel/helper-member-expression-to-functions": "^7.22.5", + "@babel/helper-optimise-call-expression": "^7.22.5", + "@babel/helper-replace-supers": "^7.22.9", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin": { + "version": "7.22.9", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.22.9.tgz", + "integrity": "sha512-+svjVa/tFwsNSG4NEy1h85+HQ5imbT92Q5/bgtS7P0GTQlP8WuFdqsiABmQouhiFGyV66oGxZFpeYHza1rNsKw==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "regexpu-core": "^5.3.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-define-polyfill-provider": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.4.1.tgz", + "integrity": "sha512-kX4oXixDxG197yhX+J3Wp+NpL2wuCFjWQAr6yX2jtCnflK9ulMI51ULFGIrWiX1jGfvAxdHp+XQCcP2bZGPs9A==", + "dev": true, + "dependencies": { + "@babel/helper-compilation-targets": "^7.22.6", + "@babel/helper-plugin-utils": "^7.22.5", + "debug": "^4.1.1", + "lodash.debounce": "^4.0.8", + "resolve": "^1.14.2" + }, + "peerDependencies": { + "@babel/core": "^7.4.0-0" + } + }, + "node_modules/@babel/helper-environment-visitor": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.5.tgz", + "integrity": "sha512-XGmhECfVA/5sAt+H+xpSg0mfrHq6FzNr9Oxh7PSEBBRUb/mL7Kz3NICXb194rCqAEdxkhPT1a88teizAFyvk8Q==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-function-name": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.22.5.tgz", + "integrity": "sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ==", + "dev": true, + "dependencies": { + "@babel/template": "^7.22.5", + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-hoist-variables": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.22.5.tgz", + "integrity": "sha512-aBiH1NKMG0H2cGZqspNvsaBe6wNGjbJjuLy29aU+eDZjSbbN53BaxlpB02xm9v34pLTZ1nIQPFYn2qMZoa5BQQ==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.5.tgz", + "integrity": "sha512-8Dl6+HD/cKifutF5qGd/8ZJi84QeAKh+CEe1sBzz8UayBBGg1dAIJrdHOcOM5b2MpzWL2yuotJTtGjETq0qjXg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.22.9", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.22.9.tgz", + "integrity": "sha512-t+WA2Xn5K+rTeGtC8jCsdAH52bjggG5TKRuRrAGNM/mjIbO4GxvlLMFOEz9wXY5I2XQ60PMFsAG2WIcG82dQMQ==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-module-imports": "^7.22.5", + "@babel/helper-simple-access": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/helper-validator-identifier": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.22.5.tgz", + "integrity": "sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz", + "integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-remap-async-to-generator": { + "version": "7.22.9", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.22.9.tgz", + "integrity": "sha512-8WWC4oR4Px+tr+Fp0X3RHDVfINGpF3ad1HIbrc8A77epiR6eMMc6jsgozkzT2uDiOOdoS9cLIQ+XD2XvI2WSmQ==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-wrap-function": "^7.22.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.22.9", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.22.9.tgz", + "integrity": "sha512-LJIKvvpgPOPUThdYqcX6IXRuIcTkcAub0IaDRGCZH0p5GPUp7PhRU9QVgFcDDd51BaPkk77ZjqFwh6DZTAEmGg==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-member-expression-to-functions": "^7.22.5", + "@babel/helper-optimise-call-expression": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", + "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.22.5.tgz", + "integrity": "sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", + "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz", + "integrity": "sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.5.tgz", + "integrity": "sha512-R3oB6xlIVKUnxNUxbmgq7pKjxpru24zlimpE8WK47fACIlM0II/Hm1RS8IaOI7NgCr6LNS+jl5l75m20npAziw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-wrap-function": { + "version": "7.22.9", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.22.9.tgz", + "integrity": "sha512-sZ+QzfauuUEfxSEjKFmi3qDSHgLsTPK/pEpoD/qonZKOtTPTLbf59oabPQ4rKekt9lFcj/hTZaOhWwFYrgjk+Q==", + "dev": true, + "dependencies": { + "@babel/helper-function-name": "^7.22.5", + "@babel/template": "^7.22.5", + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.22.6.tgz", + "integrity": "sha512-YjDs6y/fVOYFV8hAf1rxd1QvR9wJe1pDBZ2AREKq/SDayfPzgk0PBnVuTCE5X1acEpMMNOVUqoe+OwiZGJ+OaA==", + "dev": true, + "dependencies": { + "@babel/template": "^7.22.5", + "@babel/traverse": "^7.22.6", + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.5.tgz", + "integrity": "sha512-BSKlD1hgnedS5XRnGOljZawtag7H1yPfQp0tdNJCHoH6AZ+Pcm9VvkrK59/Yy593Ypg0zMxH2BxD1VPYUQ7UIw==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.22.5", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.22.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.7.tgz", + "integrity": "sha512-7NF8pOkHP5o2vpmGgNGcfAeCvOYhGLyA3Z4eBQkT1RJlWu47n63bCs93QfJ2hIAFCil7L5P2IWhs1oToVgrL0Q==", + "dev": true, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.22.5.tgz", + "integrity": "sha512-NP1M5Rf+u2Gw9qfSO4ihjcTGW5zXTi36ITLd4/EoAcEhIZ0yjMqmftDNl3QC19CX7olhrjpyU454g/2W7X0jvQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.22.5.tgz", + "integrity": "sha512-31Bb65aZaUwqCbWMnZPduIZxCBngHFlzyN6Dq6KAJjtx+lx6ohKHubc61OomYi7XwVD4Ol0XCVz4h+pYFR048g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", + "@babel/plugin-transform-optional-chaining": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.13.0" + } + }, + "node_modules/@babel/plugin-proposal-class-properties": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.18.6.tgz", + "integrity": "sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-decorators": { + "version": "7.22.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.22.7.tgz", + "integrity": "sha512-omXqPF7Onq4Bb7wHxXjM3jSMSJvUUbvDvmmds7KI5n9Cq6Ln5I05I1W2nRlRof1rGdiUxJrxwe285WF96XlBXQ==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.22.6", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-replace-supers": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/plugin-syntax-decorators": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-nullish-coalescing-operator": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.18.6.tgz", + "integrity": "sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-numeric-separator": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.18.6.tgz", + "integrity": "sha512-ozlZFogPqoLm8WBr5Z8UckIoE4YQ5KESVcNudyXOR8uqIkliTEgJ3RoketfG6pmzLdeZF0H/wjE9/cCEitBl7Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-numeric-separator": "^7.10.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-optional-chaining": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.21.0.tgz", + "integrity": "sha512-p4zeefM72gpmEe2fkUr/OnOXpWEf8nAgk7ZYVqqfFiyIG7oFfVZcCrU64hWn5xp4tQ9LkV4bTIa5rD0KANpKNA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0", + "@babel/plugin-syntax-optional-chaining": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-private-methods": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.18.6.tgz", + "integrity": "sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-private-property-in-object": { + "version": "7.21.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.11.tgz", + "integrity": "sha512-0QZ8qP/3RLDVBwBFoWAwCtgcDZJVwA5LUJRZU8x2YFfKNuFq161wK3cuGrALu5yiPu+vzwTAg/sMWVNeWeNyaw==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.18.6", + "@babel/helper-create-class-features-plugin": "^7.21.0", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-unicode-property-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.18.6.tgz", + "integrity": "sha512-2BShG/d5yoZyXZfVePH91urL5wTG6ASZU9M4o03lKK8u8UW1y08OMttBSOADTcJrnPMpvDXRG3G8fyLh4ovs8w==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-decorators": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.22.5.tgz", + "integrity": "sha512-avpUOBS7IU6al8MmF1XpAyj9QYeLPuSDJI5D4pVMSMdL7xQokKqJPYQC67RCT0aCTashUXPiGwMJ0DEXXCEmMA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-dynamic-import": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", + "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-export-namespace-from": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", + "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-flow": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.22.5.tgz", + "integrity": "sha512-9RdCl0i+q0QExayk2nOS7853w08yLucnnPML6EN9S8fgMPVtdLDCdx/cOQ/i44Lb9UeQX9A35yaqBBOMMZxPxQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-assertions": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.22.5.tgz", + "integrity": "sha512-rdV97N7KqsRzeNGoWUOK6yUsWarLjE5Su/Snk9IYPU9CwkWHs4t+rTGOvffTR8XGkJMTAdLfO0xVnXm8wugIJg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.22.5.tgz", + "integrity": "sha512-KwvoWDeNKPETmozyFE0P2rOLqh39EoQHNjqizrI5B8Vt0ZNS7M56s7dAiAqbYfiAYOuIzIh96z3iR2ktgu3tEg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.22.5.tgz", + "integrity": "sha512-gvyP4hZrgrs/wWMaocvxZ44Hw0b3W8Pe+cMxc8V1ULQ07oh8VNbIRaoD1LRZVTvD+0nieDKjfgKg89sD7rrKrg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.22.5.tgz", + "integrity": "sha512-1mS2o03i7t1c6VzH6fdQ3OA8tcEIxwG18zIPRp+UY1Ihv6W+XZzBCVxExF9upussPXJ0xE9XRHwMoNs1ep/nRQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-unicode-sets-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", + "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.22.5.tgz", + "integrity": "sha512-26lTNXoVRdAnsaDXPpvCNUq+OVWEVC6bx7Vvz9rC53F2bagUWW4u4ii2+h8Fejfh7RYqPxn+libeFBBck9muEw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-generator-functions": { + "version": "7.22.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.22.7.tgz", + "integrity": "sha512-7HmE7pk/Fmke45TODvxvkxRMV9RazV+ZZzhOL9AG8G29TLrr3jkjwF7uJfxZ30EoXpO+LJkq4oA8NjO2DTnEDg==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-remap-async-to-generator": "^7.22.5", + "@babel/plugin-syntax-async-generators": "^7.8.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-to-generator": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.22.5.tgz", + "integrity": "sha512-b1A8D8ZzE/VhNDoV1MSJTnpKkCG5bJo+19R4o4oy03zM7ws8yEMK755j61Dc3EyvdysbqH5BOOTquJ7ZX9C6vQ==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-remap-async-to-generator": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoped-functions": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.22.5.tgz", + "integrity": "sha512-tdXZ2UdknEKQWKJP1KMNmuF5Lx3MymtMN/pvA+p/VEkhK8jVcQ1fzSy8KM9qRYhAf2/lV33hoMPKI/xaI9sADA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoping": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.22.5.tgz", + "integrity": "sha512-EcACl1i5fSQ6bt+YGuU/XGCeZKStLmyVGytWkpyhCLeQVA0eu6Wtiw92V+I1T/hnezUv7j74dA/Ro69gWcU+hg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-properties": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.22.5.tgz", + "integrity": "sha512-nDkQ0NfkOhPTq8YCLiWNxp1+f9fCobEjCb0n8WdbNUBc4IB5V7P1QnX9IjpSoquKrXF5SKojHleVNs2vGeHCHQ==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-static-block": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.22.5.tgz", + "integrity": "sha512-SPToJ5eYZLxlnp1UzdARpOGeC2GbHvr9d/UV0EukuVx8atktg194oe+C5BqQ8jRTkgLRVOPYeXRSBg1IlMoVRA==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-class-static-block": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0" + } + }, + "node_modules/@babel/plugin-transform-classes": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.22.6.tgz", + "integrity": "sha512-58EgM6nuPNG6Py4Z3zSuu0xWu2VfodiMi72Jt5Kj2FECmaYk1RrTXA45z6KBFsu9tRgwQDwIiY4FXTt+YsSFAQ==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-compilation-targets": "^7.22.6", + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-function-name": "^7.22.5", + "@babel/helper-optimise-call-expression": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-replace-supers": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-computed-properties": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.22.5.tgz", + "integrity": "sha512-4GHWBgRf0krxPX+AaPtgBAlTgTeZmqDynokHOX7aqqAB4tHs3U2Y02zH6ETFdLZGcg9UQSD1WCmkVrE9ErHeOg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/template": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-destructuring": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.22.5.tgz", + "integrity": "sha512-GfqcFuGW8vnEqTUBM7UtPd5A4q797LTvvwKxXTgRsFjoqaJiEg9deBG6kWeQYkVEL569NpnmpC0Pkr/8BLKGnQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dotall-regex": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.22.5.tgz", + "integrity": "sha512-5/Yk9QxCQCl+sOIB1WelKnVRxTJDSAIxtJLL2/pqL14ZVlbH0fUQUZa/T5/UnQtBNgghR7mfB8ERBKyKPCi7Vw==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-keys": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.22.5.tgz", + "integrity": "sha512-dEnYD+9BBgld5VBXHnF/DbYGp3fqGMsyxKbtD1mDyIA7AkTSpKXFhCVuj/oQVOoALfBs77DudA0BE4d5mcpmqw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dynamic-import": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.22.5.tgz", + "integrity": "sha512-0MC3ppTB1AMxd8fXjSrbPa7LT9hrImt+/fcj+Pg5YMD7UQyWp/02+JWpdnCymmsXwIx5Z+sYn1bwCn4ZJNvhqQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-dynamic-import": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-exponentiation-operator": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.22.5.tgz", + "integrity": "sha512-vIpJFNM/FjZ4rh1myqIya9jXwrwwgFRHPjT3DkUA9ZLHuzox8jiXkOLvwm1H+PQIP3CqfC++WPKeuDi0Sjdj1g==", + "dev": true, + "dependencies": { + "@babel/helper-builder-binary-assignment-operator-visitor": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-export-namespace-from": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.22.5.tgz", + "integrity": "sha512-X4hhm7FRnPgd4nDA4b/5V280xCx6oL7Oob5+9qVS5C13Zq4bh1qq7LU0GgRU6b5dBWBvhGaXYVB4AcN6+ol6vg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-flow-strip-types": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.22.5.tgz", + "integrity": "sha512-tujNbZdxdG0/54g/oua8ISToaXTFBf8EnSb5PgQSciIXWOWKX3S4+JR7ZE9ol8FZwf9kxitzkGQ+QWeov/mCiA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-flow": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-for-of": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.22.5.tgz", + "integrity": "sha512-3kxQjX1dU9uudwSshyLeEipvrLjBCVthCgeTp6CzE/9JYrlAIaeekVxRpCWsDDfYTfRZRoCeZatCQvwo+wvK8A==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-function-name": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.22.5.tgz", + "integrity": "sha512-UIzQNMS0p0HHiQm3oelztj+ECwFnj+ZRV4KnguvlsD2of1whUeM6o7wGNj6oLwcDoAXQ8gEqfgC24D+VdIcevg==", + "dev": true, + "dependencies": { + "@babel/helper-compilation-targets": "^7.22.5", + "@babel/helper-function-name": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-json-strings": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.22.5.tgz", + "integrity": "sha512-DuCRB7fu8MyTLbEQd1ew3R85nx/88yMoqo2uPSjevMj3yoN7CDM8jkgrY0wmVxfJZyJ/B9fE1iq7EQppWQmR5A==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-json-strings": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-literals": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.22.5.tgz", + "integrity": "sha512-fTLj4D79M+mepcw3dgFBTIDYpbcB9Sm0bpm4ppXPaO+U+PKFFyV9MGRvS0gvGw62sd10kT5lRMKXAADb9pWy8g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-logical-assignment-operators": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.22.5.tgz", + "integrity": "sha512-MQQOUW1KL8X0cDWfbwYP+TbVbZm16QmQXJQ+vndPtH/BoO0lOKpVoEDMI7+PskYxH+IiE0tS8xZye0qr1lGzSA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-member-expression-literals": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.22.5.tgz", + "integrity": "sha512-RZEdkNtzzYCFl9SE9ATaUMTj2hqMb4StarOJLrZRbqqU4HSBE7UlBw9WBWQiDzrJZJdUWiMTVDI6Gv/8DPvfew==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-amd": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.22.5.tgz", + "integrity": "sha512-R+PTfLTcYEmb1+kK7FNkhQ1gP4KgjpSO6HfH9+f8/yfp2Nt3ggBjiVpRwmwTlfqZLafYKJACy36yDXlEmI9HjQ==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.22.5.tgz", + "integrity": "sha512-B4pzOXj+ONRmuaQTg05b3y/4DuFz3WcCNAXPLb2Q0GT0TrGKGxNKV4jwsXts+StaM0LQczZbOpj8o1DLPDJIiA==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-simple-access": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-systemjs": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.22.5.tgz", + "integrity": "sha512-emtEpoaTMsOs6Tzz+nbmcePl6AKVtS1yC4YNAeMun9U8YCsgadPNxnOPQ8GhHFB2qdx+LZu9LgoC0Lthuu05DQ==", + "dev": true, + "dependencies": { + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-module-transforms": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-umd": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.22.5.tgz", + "integrity": "sha512-+S6kzefN/E1vkSsKx8kmQuqeQsvCKCd1fraCM7zXm4SFoggI099Tr4G8U81+5gtMdUeMQ4ipdQffbKLX0/7dBQ==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.22.5.tgz", + "integrity": "sha512-YgLLKmS3aUBhHaxp5hi1WJTgOUb/NCuDHzGT9z9WTt3YG+CPRhJs6nprbStx6DnWM4dh6gt7SU3sZodbZ08adQ==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-new-target": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.22.5.tgz", + "integrity": "sha512-AsF7K0Fx/cNKVyk3a+DW0JLo+Ua598/NxMRvxDnkpCIGFh43+h/v2xyhRUYf6oD8gE4QtL83C7zZVghMjHd+iw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.22.5.tgz", + "integrity": "sha512-6CF8g6z1dNYZ/VXok5uYkkBBICHZPiGEl7oDnAx2Mt1hlHVHOSIKWJaXHjQJA5VB43KZnXZDIexMchY4y2PGdA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-numeric-separator": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.22.5.tgz", + "integrity": "sha512-NbslED1/6M+sXiwwtcAB/nieypGw02Ejf4KtDeMkCEpP6gWFMX1wI9WKYua+4oBneCCEmulOkRpwywypVZzs/g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-numeric-separator": "^7.10.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-rest-spread": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.22.5.tgz", + "integrity": "sha512-Kk3lyDmEslH9DnvCDA1s1kkd3YWQITiBOHngOtDL9Pt6BZjzqb6hiOlb8VfjiiQJ2unmegBqZu0rx5RxJb5vmQ==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.22.5", + "@babel/helper-compilation-targets": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-transform-parameters": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-super": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.22.5.tgz", + "integrity": "sha512-klXqyaT9trSjIUrcsYIfETAzmOEZL3cBYqOYLJxBHfMFFggmXOv+NYSX/Jbs9mzMVESw/WycLFPRx8ba/b2Ipw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-replace-supers": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-catch-binding": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.22.5.tgz", + "integrity": "sha512-pH8orJahy+hzZje5b8e2QIlBWQvGpelS76C63Z+jhZKsmzfNaPQ+LaW6dcJ9bxTpo1mtXbgHwy765Ro3jftmUg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-chaining": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.22.6.tgz", + "integrity": "sha512-Vd5HiWml0mDVtcLHIoEU5sw6HOUW/Zk0acLs/SAeuLzkGNOPc9DB4nkUajemhCmTIz3eiaKREZn2hQQqF79YTg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", + "@babel/plugin-syntax-optional-chaining": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-parameters": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.22.5.tgz", + "integrity": "sha512-AVkFUBurORBREOmHRKo06FjHYgjrabpdqRSwq6+C7R5iTCZOsM4QbcB27St0a4U6fffyAOqh3s/qEfybAhfivg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-methods": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.22.5.tgz", + "integrity": "sha512-PPjh4gyrQnGe97JTalgRGMuU4icsZFnWkzicB/fUtzlKUqvsWBKEpPPfr5a2JiyirZkHxnAqkQMO5Z5B2kK3fA==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-property-in-object": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.22.5.tgz", + "integrity": "sha512-/9xnaTTJcVoBtSSmrVyhtSvO3kbqS2ODoh2juEU72c3aYonNF0OMGiaz2gjukyKM2wBBYJP38S4JiE0Wfb5VMQ==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-create-class-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-property-literals": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.22.5.tgz", + "integrity": "sha512-TiOArgddK3mK/x1Qwf5hay2pxI6wCZnvQqrFSqbtg1GLl2JcNMitVH/YnqjP+M31pLUeTfzY1HAXFDnUBV30rQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-constant-elements": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-constant-elements/-/plugin-transform-react-constant-elements-7.22.5.tgz", + "integrity": "sha512-BF5SXoO+nX3h5OhlN78XbbDrBOffv+AxPP2ENaJOVqjWCgBDeOY3WcaUcddutGSfoap+5NEQ/q/4I3WZIvgkXA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-display-name": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.22.5.tgz", + "integrity": "sha512-PVk3WPYudRF5z4GKMEYUrLjPl38fJSKNaEOkFuoprioowGuWN6w2RKznuFNSlJx7pzzXXStPUnNSOEO0jL5EVw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.22.5.tgz", + "integrity": "sha512-rog5gZaVbUip5iWDMTYbVM15XQq+RkUKhET/IHR6oizR+JEoN6CAfTTuHcK4vwUyzca30qqHqEpzBOnaRMWYMA==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-module-imports": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-jsx": "^7.22.5", + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-development": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.22.5.tgz", + "integrity": "sha512-bDhuzwWMuInwCYeDeMzyi7TaBgRQei6DqxhbyniL7/VG4RSS7HtSL2QbY4eESy1KJqlWt8g3xeEBGPuo+XqC8A==", + "dev": true, + "dependencies": { + "@babel/plugin-transform-react-jsx": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-pure-annotations": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.22.5.tgz", + "integrity": "sha512-gP4k85wx09q+brArVinTXhWiyzLl9UpmGva0+mWyKxk6JZequ05x3eUcIUE+FyttPKJFRRVtAvQaJ6YF9h1ZpA==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regenerator": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.22.5.tgz", + "integrity": "sha512-rR7KePOE7gfEtNTh9Qw+iO3Q/e4DEsoQ+hdvM6QUDH7JRJ5qxq5AA52ZzBWbI5i9lfNuvySgOGP8ZN7LAmaiPw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "regenerator-transform": "^0.15.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-reserved-words": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.22.5.tgz", + "integrity": "sha512-DTtGKFRQUDm8svigJzZHzb/2xatPc6TzNvAIJ5GqOKDsGFYgAskjRulbR/vGsPKq3OPqtexnz327qYpP57RFyA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-runtime": { + "version": "7.22.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.22.9.tgz", + "integrity": "sha512-9KjBH61AGJetCPYp/IEyLEp47SyybZb0nDRpBvmtEkm+rUIwxdlKpyNHI1TmsGkeuLclJdleQHRZ8XLBnnh8CQ==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "babel-plugin-polyfill-corejs2": "^0.4.4", + "babel-plugin-polyfill-corejs3": "^0.8.2", + "babel-plugin-polyfill-regenerator": "^0.5.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-runtime/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/plugin-transform-shorthand-properties": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.22.5.tgz", + "integrity": "sha512-vM4fq9IXHscXVKzDv5itkO1X52SmdFBFcMIBZ2FRn2nqVYqw6dBexUgMvAjHW+KXpPPViD/Yo3GrDEBaRC0QYA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-spread": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.22.5.tgz", + "integrity": "sha512-5ZzDQIGyvN4w8+dMmpohL6MBo+l2G7tfC/O2Dg7/hjpgeWvUx8FzfeOKxGog9IimPa4YekaQ9PlDqTLOljkcxg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-sticky-regex": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.22.5.tgz", + "integrity": "sha512-zf7LuNpHG0iEeiyCNwX4j3gDg1jgt1k3ZdXBKbZSoA3BbGQGvMiSvfbZRR3Dr3aeJe3ooWFZxOOG3IRStYp2Bw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-template-literals": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.22.5.tgz", + "integrity": "sha512-5ciOehRNf+EyUeewo8NkbQiUs4d6ZxiHo6BcBcnFlgiJfu16q0bQUw9Jvo0b0gBKFG1SMhDSjeKXSYuJLeFSMA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typeof-symbol": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.22.5.tgz", + "integrity": "sha512-bYkI5lMzL4kPii4HHEEChkD0rkc+nvnlR6+o/qdqR6zrm0Sv/nodmyLhlq2DO0YKLUNd2VePmPRjJXSBh9OIdA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typescript": { + "version": "7.22.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.22.9.tgz", + "integrity": "sha512-BnVR1CpKiuD0iobHPaM1iLvcwPYN2uVFAqoLVSpEDKWuOikoCv5HbKLxclhKYUXlWkX86DoZGtqI4XhbOsyrMg==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-create-class-features-plugin": "^7.22.9", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-typescript": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-escapes": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.22.5.tgz", + "integrity": "sha512-biEmVg1IYB/raUO5wT1tgfacCef15Fbzhkx493D3urBI++6hpJ+RFG4SrWMn0NEZLfvilqKf3QDrRVZHo08FYg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-property-regex": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.22.5.tgz", + "integrity": "sha512-HCCIb+CbJIAE6sXn5CjFQXMwkCClcOfPCzTlilJ8cUatfzwHlWQkbtV0zD338u9dZskwvuOYTuuaMaA8J5EI5A==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-regex": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.22.5.tgz", + "integrity": "sha512-028laaOKptN5vHJf9/Arr/HiJekMd41hOEZYvNsrsXqJ7YPYuX2bQxh31fkZzGmq3YqHRJzYFFAVYvKfMPKqyg==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-sets-regex": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.22.5.tgz", + "integrity": "sha512-lhMfi4FC15j13eKrh3DnYHjpGj6UKQHtNKTbtc1igvAhRy4+kLhV07OpLcsN0VgDEw/MjAvJO4BdMJsHwMhzCg==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/preset-env": { + "version": "7.22.9", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.22.9.tgz", + "integrity": "sha512-wNi5H/Emkhll/bqPjsjQorSykrlfY5OWakd6AulLvMEytpKasMVUpVy8RL4qBIBs5Ac6/5i0/Rv0b/Fg6Eag/g==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.22.9", + "@babel/helper-compilation-targets": "^7.22.9", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-validator-option": "^7.22.5", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.22.5", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.22.5", + "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3", + "@babel/plugin-syntax-import-assertions": "^7.22.5", + "@babel/plugin-syntax-import-attributes": "^7.22.5", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5", + "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", + "@babel/plugin-transform-arrow-functions": "^7.22.5", + "@babel/plugin-transform-async-generator-functions": "^7.22.7", + "@babel/plugin-transform-async-to-generator": "^7.22.5", + "@babel/plugin-transform-block-scoped-functions": "^7.22.5", + "@babel/plugin-transform-block-scoping": "^7.22.5", + "@babel/plugin-transform-class-properties": "^7.22.5", + "@babel/plugin-transform-class-static-block": "^7.22.5", + "@babel/plugin-transform-classes": "^7.22.6", + "@babel/plugin-transform-computed-properties": "^7.22.5", + "@babel/plugin-transform-destructuring": "^7.22.5", + "@babel/plugin-transform-dotall-regex": "^7.22.5", + "@babel/plugin-transform-duplicate-keys": "^7.22.5", + "@babel/plugin-transform-dynamic-import": "^7.22.5", + "@babel/plugin-transform-exponentiation-operator": "^7.22.5", + "@babel/plugin-transform-export-namespace-from": "^7.22.5", + "@babel/plugin-transform-for-of": "^7.22.5", + "@babel/plugin-transform-function-name": "^7.22.5", + "@babel/plugin-transform-json-strings": "^7.22.5", + "@babel/plugin-transform-literals": "^7.22.5", + "@babel/plugin-transform-logical-assignment-operators": "^7.22.5", + "@babel/plugin-transform-member-expression-literals": "^7.22.5", + "@babel/plugin-transform-modules-amd": "^7.22.5", + "@babel/plugin-transform-modules-commonjs": "^7.22.5", + "@babel/plugin-transform-modules-systemjs": "^7.22.5", + "@babel/plugin-transform-modules-umd": "^7.22.5", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.22.5", + "@babel/plugin-transform-new-target": "^7.22.5", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.22.5", + "@babel/plugin-transform-numeric-separator": "^7.22.5", + "@babel/plugin-transform-object-rest-spread": "^7.22.5", + "@babel/plugin-transform-object-super": "^7.22.5", + "@babel/plugin-transform-optional-catch-binding": "^7.22.5", + "@babel/plugin-transform-optional-chaining": "^7.22.6", + "@babel/plugin-transform-parameters": "^7.22.5", + "@babel/plugin-transform-private-methods": "^7.22.5", + "@babel/plugin-transform-private-property-in-object": "^7.22.5", + "@babel/plugin-transform-property-literals": "^7.22.5", + "@babel/plugin-transform-regenerator": "^7.22.5", + "@babel/plugin-transform-reserved-words": "^7.22.5", + "@babel/plugin-transform-shorthand-properties": "^7.22.5", + "@babel/plugin-transform-spread": "^7.22.5", + "@babel/plugin-transform-sticky-regex": "^7.22.5", + "@babel/plugin-transform-template-literals": "^7.22.5", + "@babel/plugin-transform-typeof-symbol": "^7.22.5", + "@babel/plugin-transform-unicode-escapes": "^7.22.5", + "@babel/plugin-transform-unicode-property-regex": "^7.22.5", + "@babel/plugin-transform-unicode-regex": "^7.22.5", + "@babel/plugin-transform-unicode-sets-regex": "^7.22.5", + "@babel/preset-modules": "^0.1.5", + "@babel/types": "^7.22.5", + "babel-plugin-polyfill-corejs2": "^0.4.4", + "babel-plugin-polyfill-corejs3": "^0.8.2", + "babel-plugin-polyfill-regenerator": "^0.5.1", + "core-js-compat": "^3.31.0", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-env/node_modules/@babel/plugin-proposal-private-property-in-object": { + "version": "7.21.0-placeholder-for-preset-env.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", + "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", + "dev": true, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-env/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/preset-modules": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.5.tgz", + "integrity": "sha512-A57th6YRG7oR3cq/yt/Y84MvGgE0eJG2F1JLhKuyG+jFxEgrd/HAMJatiFtmOiZurz+0DkrvbheCLaV5f2JfjA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-proposal-unicode-property-regex": "^7.4.4", + "@babel/plugin-transform-dotall-regex": "^7.4.4", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-react": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.22.5.tgz", + "integrity": "sha512-M+Is3WikOpEJHgR385HbuCITPTaPRaNkibTEa9oiofmJvIsrceb4yp9RL9Kb+TE8LznmeyZqpP+Lopwcx59xPQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-validator-option": "^7.22.5", + "@babel/plugin-transform-react-display-name": "^7.22.5", + "@babel/plugin-transform-react-jsx": "^7.22.5", + "@babel/plugin-transform-react-jsx-development": "^7.22.5", + "@babel/plugin-transform-react-pure-annotations": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-typescript": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.22.5.tgz", + "integrity": "sha512-YbPaal9LxztSGhmndR46FmAbkJ/1fAsw293tSU+I5E5h+cnJ3d4GTwyUgGYmOXJYdGA+uNePle4qbaRzj2NISQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-validator-option": "^7.22.5", + "@babel/plugin-syntax-jsx": "^7.22.5", + "@babel/plugin-transform-modules-commonjs": "^7.22.5", + "@babel/plugin-transform-typescript": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/regjsgen": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@babel/regjsgen/-/regjsgen-0.8.0.tgz", + "integrity": "sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==", + "dev": true + }, + "node_modules/@babel/runtime": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.6.tgz", + "integrity": "sha512-wDb5pWm4WDdF6LFUde3Jl8WzPA+3ZbxYqkC6xAXuD3irdEHN1k0NfTRrJD8ZD378SJ61miMLCqIOXYhd8x+AJQ==", + "dev": true, + "dependencies": { + "regenerator-runtime": "^0.13.11" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.5.tgz", + "integrity": "sha512-X7yV7eiwAxdj9k94NEylvbVHLiVG1nvzCV2EAowhxLTwODV1jl9UzZ48leOC0sH7OnuHrIkllaBgneUykIcZaw==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.22.5", + "@babel/parser": "^7.22.5", + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.22.8", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.22.8.tgz", + "integrity": "sha512-y6LPR+wpM2I3qJrsheCTwhIinzkETbplIgPBbwvqPKc+uljeA5gP+3nP8irdYt1mjQaDnlIcG+dw8OjAco4GXw==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.22.5", + "@babel/generator": "^7.22.7", + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-function-name": "^7.22.5", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.22.7", + "@babel/types": "^7.22.5", + "debug": "^4.1.0", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.5.tgz", + "integrity": "sha512-zo3MIHGOkPOfoRXitsgHLjEXmlDaD/5KU1Uzuc9GNiZPhSqVxVRtxuPaSBZDsYZ9qV88AjtMtWW7ww98loJ9KA==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.5", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcherny/json-schema-ref-parser": { + "version": "10.0.5-fork", + "resolved": "https://registry.npmjs.org/@bcherny/json-schema-ref-parser/-/json-schema-ref-parser-10.0.5-fork.tgz", + "integrity": "sha512-E/jKbPoca1tfUPj3iSbitDZTGnq6FUFjkH6L8U2oDwSuwK1WhnnVtCG7oFOTg/DDnyoXbQYUiUiGOibHqaGVnw==", + "dev": true, + "dependencies": { + "@jsdevtools/ono": "^7.1.3", + "@types/json-schema": "^7.0.6", + "call-me-maybe": "^1.0.1", + "js-yaml": "^4.1.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/philsturgeon" + } + }, + "node_modules/@bcherny/json-schema-ref-parser/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/@bcherny/json-schema-ref-parser/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true + }, + "node_modules/@csstools/normalize.css": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/@csstools/normalize.css/-/normalize.css-12.0.0.tgz", + "integrity": "sha512-M0qqxAcwCsIVfpFQSlGN5XjXWu8l5JDZN+fPt1LeW5SZexQTgnaEvgXAY+CeygRw0EeppWHi12JxESWiWrB0Sg==", + "dev": true + }, + "node_modules/@csstools/postcss-cascade-layers": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-cascade-layers/-/postcss-cascade-layers-1.1.1.tgz", + "integrity": "sha512-+KdYrpKC5TgomQr2DlZF4lDEpHcoxnj5IGddYYfBWJAKfj1JtuHUIqMa+E1pJJ+z3kvDViWMqyqPlG4Ja7amQA==", + "dev": true, + "dependencies": { + "@csstools/selector-specificity": "^2.0.2", + "postcss-selector-parser": "^6.0.10" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/@csstools/postcss-color-function": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-color-function/-/postcss-color-function-1.1.1.tgz", + "integrity": "sha512-Bc0f62WmHdtRDjf5f3e2STwRAl89N2CLb+9iAwzrv4L2hncrbDwnQD9PCq0gtAt7pOI2leIV08HIBUd4jxD8cw==", + "dev": true, + "dependencies": { + "@csstools/postcss-progressive-custom-properties": "^1.1.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/@csstools/postcss-font-format-keywords": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-font-format-keywords/-/postcss-font-format-keywords-1.0.1.tgz", + "integrity": "sha512-ZgrlzuUAjXIOc2JueK0X5sZDjCtgimVp/O5CEqTcs5ShWBa6smhWYbS0x5cVc/+rycTDbjjzoP0KTDnUneZGOg==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/@csstools/postcss-hwb-function": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@csstools/postcss-hwb-function/-/postcss-hwb-function-1.0.2.tgz", + "integrity": "sha512-YHdEru4o3Rsbjmu6vHy4UKOXZD+Rn2zmkAmLRfPet6+Jz4Ojw8cbWxe1n42VaXQhD3CQUXXTooIy8OkVbUcL+w==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/@csstools/postcss-ic-unit": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-ic-unit/-/postcss-ic-unit-1.0.1.tgz", + "integrity": "sha512-Ot1rcwRAaRHNKC9tAqoqNZhjdYBzKk1POgWfhN4uCOE47ebGcLRqXjKkApVDpjifL6u2/55ekkpnFcp+s/OZUw==", + "dev": true, + "dependencies": { + "@csstools/postcss-progressive-custom-properties": "^1.1.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/@csstools/postcss-is-pseudo-class": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@csstools/postcss-is-pseudo-class/-/postcss-is-pseudo-class-2.0.7.tgz", + "integrity": "sha512-7JPeVVZHd+jxYdULl87lvjgvWldYu+Bc62s9vD/ED6/QTGjy0jy0US/f6BG53sVMTBJ1lzKZFpYmofBN9eaRiA==", + "dev": true, + "dependencies": { + "@csstools/selector-specificity": "^2.0.0", + "postcss-selector-parser": "^6.0.10" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/@csstools/postcss-nested-calc": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-nested-calc/-/postcss-nested-calc-1.0.0.tgz", + "integrity": "sha512-JCsQsw1wjYwv1bJmgjKSoZNvf7R6+wuHDAbi5f/7MbFhl2d/+v+TvBTU4BJH3G1X1H87dHl0mh6TfYogbT/dJQ==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/@csstools/postcss-normalize-display-values": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-normalize-display-values/-/postcss-normalize-display-values-1.0.1.tgz", + "integrity": "sha512-jcOanIbv55OFKQ3sYeFD/T0Ti7AMXc9nM1hZWu8m/2722gOTxFg7xYu4RDLJLeZmPUVQlGzo4jhzvTUq3x4ZUw==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/@csstools/postcss-oklab-function": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-oklab-function/-/postcss-oklab-function-1.1.1.tgz", + "integrity": "sha512-nJpJgsdA3dA9y5pgyb/UfEzE7W5Ka7u0CX0/HIMVBNWzWemdcTH3XwANECU6anWv/ao4vVNLTMxhiPNZsTK6iA==", + "dev": true, + "dependencies": { + "@csstools/postcss-progressive-custom-properties": "^1.1.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/@csstools/postcss-progressive-custom-properties": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-progressive-custom-properties/-/postcss-progressive-custom-properties-1.3.0.tgz", + "integrity": "sha512-ASA9W1aIy5ygskZYuWams4BzafD12ULvSypmaLJT2jvQ8G0M3I8PRQhC0h7mG0Z3LI05+agZjqSR9+K9yaQQjA==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.3" + } + }, + "node_modules/@csstools/postcss-stepped-value-functions": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-stepped-value-functions/-/postcss-stepped-value-functions-1.0.1.tgz", + "integrity": "sha512-dz0LNoo3ijpTOQqEJLY8nyaapl6umbmDcgj4AD0lgVQ572b2eqA1iGZYTTWhrcrHztWDDRAX2DGYyw2VBjvCvQ==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/@csstools/postcss-text-decoration-shorthand": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-text-decoration-shorthand/-/postcss-text-decoration-shorthand-1.0.0.tgz", + "integrity": "sha512-c1XwKJ2eMIWrzQenN0XbcfzckOLLJiczqy+YvfGmzoVXd7pT9FfObiSEfzs84bpE/VqfpEuAZ9tCRbZkZxxbdw==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/@csstools/postcss-trigonometric-functions": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@csstools/postcss-trigonometric-functions/-/postcss-trigonometric-functions-1.0.2.tgz", + "integrity": "sha512-woKaLO///4bb+zZC2s80l+7cm07M7268MsyG3M0ActXXEFi6SuhvriQYcb58iiKGbjwwIU7n45iRLEHypB47Og==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/@csstools/postcss-unset-value": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@csstools/postcss-unset-value/-/postcss-unset-value-1.0.2.tgz", + "integrity": "sha512-c8J4roPBILnelAsdLr4XOAR/GsTm0GJi4XpcfvoWk3U6KiTCqiFYc63KhRMQQX35jYMp4Ao8Ij9+IZRgMfJp1g==", + "dev": true, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/@csstools/selector-specificity": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-2.2.0.tgz", + "integrity": "sha512-+OJ9konv95ClSTOJCmMZqpd5+YGsB2S+x6w3E1oaM8UuR5j8nTNHYSz8c9BEPGDOCMQYIEEGlVPj/VY64iTbGw==", + "dev": true, + "engines": { + "node": "^14 || ^16 || >=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss-selector-parser": "^6.0.10" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.5.1.tgz", + "integrity": "sha512-Z5ba73P98O1KUYCCJTUeVpja9RcGoMdncZ6T49FCUl2lN38JtCJ+3WgIDBv0AuY4WChU5PmtJmOCTlN6FZTFKQ==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.0.tgz", + "integrity": "sha512-Lj7DECXqIVCqnqjjHMPna4vn6GJcMgul/wuS0je9OZ9gsL0zzDpKPVtcG1HaDVc+9y+qgXneTeUMbCqXJNpH1A==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "13.20.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz", + "integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/eslintrc/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@eslint/eslintrc/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "8.44.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.44.0.tgz", + "integrity": "sha512-Ag+9YM4ocKQx9AarydN0KY2j0ErMHNIocPDrVo8zAE44xLTjEtz81OdR68/cydGtk6m6jDb5Za3r2useMzYmSw==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.4.1.tgz", + "integrity": "sha512-jk3WqquEJRlcyu7997NtR5PibI+y5bi+LS3hPmguVClypenMsCY3CBa3LAQnozRCtCrYWSEtAdiskpamuJRFOQ==", + "dependencies": { + "@floating-ui/utils": "^0.1.1" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.5.1.tgz", + "integrity": "sha512-KwvVcPSXg6mQygvA1TjbN/gh///36kKtllIF8SUm0qpFj8+rvYrpvlYdL1JoA71SHpDqgSSdGOSoQ0Mp3uY5aw==", + "dependencies": { + "@floating-ui/core": "^1.4.1", + "@floating-ui/utils": "^0.1.1" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.1.1.tgz", + "integrity": "sha512-m0G6wlnhm/AX0H12IOWtK8gASEMffnX08RtKkCgTdHb9JpHKGloI7icFfLg9ZmQeavcvR0PKmzxClyuFPSjKWw==" + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.10.tgz", + "integrity": "sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ==", + "dev": true, + "dependencies": { + "@humanwhocodes/object-schema": "^1.2.1", + "debug": "^4.1.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", + "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "dev": true + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-27.5.1.tgz", + "integrity": "sha512-kZ/tNpS3NXn0mlXXXPNuDZnb4c0oZ20r4K5eemM2k30ZC3G0T02nXUvyhf5YdbXWHPEJLc9qGLxEZ216MdL+Zg==", + "dev": true, + "dependencies": { + "@jest/types": "^27.5.1", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^27.5.1", + "jest-util": "^27.5.1", + "slash": "^3.0.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/@jest/console/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@jest/console/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@jest/console/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@jest/console/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/@jest/console/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/core": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-27.5.1.tgz", + "integrity": "sha512-AK6/UTrvQD0Cd24NSqmIA6rKsu0tKIxfiCducZvqxYdmMisOYAsdItspT+fQDQYARPf8XgjAFZi0ogW2agH5nQ==", + "dev": true, + "dependencies": { + "@jest/console": "^27.5.1", + "@jest/reporters": "^27.5.1", + "@jest/test-result": "^27.5.1", + "@jest/transform": "^27.5.1", + "@jest/types": "^27.5.1", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.8.1", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^27.5.1", + "jest-config": "^27.5.1", + "jest-haste-map": "^27.5.1", + "jest-message-util": "^27.5.1", + "jest-regex-util": "^27.5.1", + "jest-resolve": "^27.5.1", + "jest-resolve-dependencies": "^27.5.1", + "jest-runner": "^27.5.1", + "jest-runtime": "^27.5.1", + "jest-snapshot": "^27.5.1", + "jest-util": "^27.5.1", + "jest-validate": "^27.5.1", + "jest-watcher": "^27.5.1", + "micromatch": "^4.0.4", + "rimraf": "^3.0.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/core/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@jest/core/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@jest/core/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@jest/core/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/@jest/core/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/core/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/environment": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-27.5.1.tgz", + "integrity": "sha512-/WQjhPJe3/ghaol/4Bq480JKXV/Rfw8nQdN7f41fM8VDHLcxKXou6QyXAh3EFr9/bVG3x74z1NWDkP87EiY8gA==", + "dev": true, + "dependencies": { + "@jest/fake-timers": "^27.5.1", + "@jest/types": "^27.5.1", + "@types/node": "*", + "jest-mock": "^27.5.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-27.5.1.tgz", + "integrity": "sha512-/aPowoolwa07k7/oM3aASneNeBGCmGQsc3ugN4u6s4C/+s5M64MFo/+djTdiwcbQlRfFElGuDXWzaWj6QgKObQ==", + "dev": true, + "dependencies": { + "@jest/types": "^27.5.1", + "@sinonjs/fake-timers": "^8.0.1", + "@types/node": "*", + "jest-message-util": "^27.5.1", + "jest-mock": "^27.5.1", + "jest-util": "^27.5.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-27.5.1.tgz", + "integrity": "sha512-ZEJNB41OBQQgGzgyInAv0UUfDDj3upmHydjieSxFvTRuZElrx7tXg/uVQ5hYVEwiXs3+aMsAeEc9X7xiSKCm4Q==", + "dev": true, + "dependencies": { + "@jest/environment": "^27.5.1", + "@jest/types": "^27.5.1", + "expect": "^27.5.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-27.5.1.tgz", + "integrity": "sha512-cPXh9hWIlVJMQkVk84aIvXuBB4uQQmFqZiacloFuGiP3ah1sbCxCosidXFDfqG8+6fO1oR2dTJTlsOy4VFmUfw==", + "dev": true, + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^27.5.1", + "@jest/test-result": "^27.5.1", + "@jest/transform": "^27.5.1", + "@jest/types": "^27.5.1", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.2", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^5.1.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-haste-map": "^27.5.1", + "jest-resolve": "^27.5.1", + "jest-util": "^27.5.1", + "jest-worker": "^27.5.1", + "slash": "^3.0.0", + "source-map": "^0.6.0", + "string-length": "^4.0.1", + "terminal-link": "^2.0.0", + "v8-to-istanbul": "^8.1.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/reporters/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@jest/reporters/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@jest/reporters/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@jest/reporters/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/@jest/reporters/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/reporters/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@jest/reporters/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/schemas": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-28.1.3.tgz", + "integrity": "sha512-/l/VWsdt/aBXgjshLWOFyFt3IVdYypu5y2Wn2rOO1un6nkqIn8SLXzgIMYXFyYsRWDyF5EthmKJMIdJvk08grg==", + "dev": true, + "dependencies": { + "@sinclair/typebox": "^0.24.1" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-27.5.1.tgz", + "integrity": "sha512-y9NIHUYF3PJRlHk98NdC/N1gl88BL08aQQgu4k4ZopQkCw9t9cV8mtl3TV8b/YCB8XaVTFrmUTAJvjsntDireg==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9", + "source-map": "^0.6.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/@jest/source-map/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@jest/test-result": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-27.5.1.tgz", + "integrity": "sha512-EW35l2RYFUcUQxFJz5Cv5MTOxlJIQs4I7gxzi2zVU7PJhOwfYq1MdC5nhSmYjX1gmMmLPvB3sIaC+BkcHRBfag==", + "dev": true, + "dependencies": { + "@jest/console": "^27.5.1", + "@jest/types": "^27.5.1", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-27.5.1.tgz", + "integrity": "sha512-LCheJF7WB2+9JuCS7VB/EmGIdQuhtqjRNI9A43idHv3E4KltCTsPsLxvdaubFHSYwY/fNjMWjl6vNRhDiN7vpQ==", + "dev": true, + "dependencies": { + "@jest/test-result": "^27.5.1", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^27.5.1", + "jest-runtime": "^27.5.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-27.5.1.tgz", + "integrity": "sha512-ipON6WtYgl/1329g5AIJVbUuEh0wZVbdpGwC99Jw4LwuoBNS95MVphU6zOeD9pDkon+LLbFL7lOQRapbB8SCHw==", + "dev": true, + "dependencies": { + "@babel/core": "^7.1.0", + "@jest/types": "^27.5.1", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^1.4.0", + "fast-json-stable-stringify": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^27.5.1", + "jest-regex-util": "^27.5.1", + "jest-util": "^27.5.1", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "source-map": "^0.6.1", + "write-file-atomic": "^3.0.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/@jest/transform/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@jest/transform/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@jest/transform/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@jest/transform/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/@jest/transform/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/transform/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@jest/transform/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/types": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.5.1.tgz", + "integrity": "sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^16.0.0", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/@jest/types/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@jest/types/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@jest/types/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@jest/types/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/@jest/types/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/types/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", + "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.5.tgz", + "integrity": "sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.18", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz", + "integrity": "sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "3.1.0", + "@jridgewell/sourcemap-codec": "1.4.14" + } + }, + "node_modules/@jridgewell/trace-mapping/node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.14", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", + "dev": true + }, + "node_modules/@jsdevtools/ono": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", + "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==", + "dev": true + }, + "node_modules/@leichtgewicht/ip-codec": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz", + "integrity": "sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==", + "dev": true + }, + "node_modules/@nicolo-ribaudo/eslint-scope-5-internals": { + "version": "5.1.1-v1", + "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz", + "integrity": "sha512-54/JRvkLIzzDWshCWfuhadfrfZVPiElY8Fcgmg1HroEly/EDSszzhBAsarCux+D/kOslTRquNzuyGSmUSTTHGg==", + "dev": true, + "dependencies": { + "eslint-scope": "5.1.1" + } + }, + "node_modules/@nicolo-ribaudo/eslint-scope-5-internals/node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@nicolo-ribaudo/eslint-scope-5-internals/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/@nicolo-ribaudo/semver-v6": { + "version": "6.3.3", + "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/semver-v6/-/semver-v6-6.3.3.tgz", + "integrity": "sha512-3Yc1fUTs69MG/uZbJlLSI3JISMn2UV2rg+1D/vROUqZyh3l6iYHCs7GMp+M40ZD7yOdDbYjJcU1oTJhrc+dGKg==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@pmmmwh/react-refresh-webpack-plugin": { + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.10.tgz", + "integrity": "sha512-j0Ya0hCFZPd4x40qLzbhGsh9TMtdb+CJQiso+WxLOPNasohq9cc5SNUcwsZaRH6++Xh91Xkm/xHCkuIiIu0LUA==", + "dev": true, + "dependencies": { + "ansi-html-community": "^0.0.8", + "common-path-prefix": "^3.0.0", + "core-js-pure": "^3.23.3", + "error-stack-parser": "^2.0.6", + "find-up": "^5.0.0", + "html-entities": "^2.1.0", + "loader-utils": "^2.0.4", + "schema-utils": "^3.0.0", + "source-map": "^0.7.3" + }, + "engines": { + "node": ">= 10.13" + }, + "peerDependencies": { + "@types/webpack": "4.x || 5.x", + "react-refresh": ">=0.10.0 <1.0.0", + "sockjs-client": "^1.4.0", + "type-fest": ">=0.17.0 <4.0.0", + "webpack": ">=4.43.0 <6.0.0", + "webpack-dev-server": "3.x || 4.x", + "webpack-hot-middleware": "2.x", + "webpack-plugin-serve": "0.x || 1.x" + }, + "peerDependenciesMeta": { + "@types/webpack": { + "optional": true + }, + "sockjs-client": { + "optional": true + }, + "type-fest": { + "optional": true + }, + "webpack-dev-server": { + "optional": true + }, + "webpack-hot-middleware": { + "optional": true + }, + "webpack-plugin-serve": { + "optional": true + } + } + }, + "node_modules/@reactflow/background": { + "version": "11.2.4", + "resolved": "https://registry.npmjs.org/@reactflow/background/-/background-11.2.4.tgz", + "integrity": "sha512-SYQbCRCU0GuxT/40Tm7ZK+l5wByGnNJSLtZhbL9C/Hl7JhsJXV3UGXr0vrlhVZUBEtkWA7XhZM/5S9XEA5XSFA==", + "dependencies": { + "@reactflow/core": "11.7.4", + "classcat": "^5.0.3", + "zustand": "^4.3.1" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@reactflow/controls": { + "version": "11.1.15", + "resolved": "https://registry.npmjs.org/@reactflow/controls/-/controls-11.1.15.tgz", + "integrity": "sha512-//33XfBYu8vQ6brfmlZwKrDoh+8hh93xO2d88XiqfIbrPEEb32SYjsb9mS9VuHKNlSIW+eB27fBA1Gt00mEj5w==", + "dependencies": { + "@reactflow/core": "11.7.4", + "classcat": "^5.0.3", + "zustand": "^4.3.1" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@reactflow/core": { + "version": "11.7.4", + "resolved": "https://registry.npmjs.org/@reactflow/core/-/core-11.7.4.tgz", + "integrity": "sha512-nt0T8ERp8TE7YCDQViaoEY9lb0StDPrWHVx3zBjhStFYET3wc88t8QRasZdf99xRTmyNtI3U3M40M5EBLNUpMw==", + "dependencies": { + "@types/d3": "^7.4.0", + "@types/d3-drag": "^3.0.1", + "@types/d3-selection": "^3.0.3", + "@types/d3-zoom": "^3.0.1", + "classcat": "^5.0.3", + "d3-drag": "^3.0.0", + "d3-selection": "^3.0.0", + "d3-zoom": "^3.0.0", + "zustand": "^4.3.1" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@reactflow/minimap": { + "version": "11.5.4", + "resolved": "https://registry.npmjs.org/@reactflow/minimap/-/minimap-11.5.4.tgz", + "integrity": "sha512-1tDBj2zX2gxu2oHU6qvH5RGNrOWRfRxu8369KhDotuuBN5yJrGXJzWIKikwhzjsNsQJYOB+B0cS44yWAfwSwzw==", + "dependencies": { + "@reactflow/core": "11.7.4", + "@types/d3-selection": "^3.0.3", + "@types/d3-zoom": "^3.0.1", + "classcat": "^5.0.3", + "d3-selection": "^3.0.0", + "d3-zoom": "^3.0.0", + "zustand": "^4.3.1" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@reactflow/node-resizer": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@reactflow/node-resizer/-/node-resizer-2.1.1.tgz", + "integrity": "sha512-5Q+IBmZfpp/bYsw3+KRVJB1nUbj6W3XAp5ycx4uNWH+K98vbssymyQsW0vvKkIhxEPg6tkiMzO4UWRWvwBwt1g==", + "dependencies": { + "@reactflow/core": "^11.6.0", + "classcat": "^5.0.4", + "d3-drag": "^3.0.0", + "d3-selection": "^3.0.0", + "zustand": "^4.3.1" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@reactflow/node-toolbar": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@reactflow/node-toolbar/-/node-toolbar-1.2.3.tgz", + "integrity": "sha512-uFQy9xpog92s0G1wsPLniwV9nyH4i/MmL7QoMsWdnKaOi7XMhd8SJcCzUdHC3imR21HltsuQITff/XQ51ApMbg==", + "dependencies": { + "@reactflow/core": "11.7.4", + "classcat": "^5.0.3", + "zustand": "^4.3.1" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@remix-run/router": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.7.2.tgz", + "integrity": "sha512-7Lcn7IqGMV+vizMPoEl5F0XDshcdDYtMI6uJLQdQz5CfZAwy3vvGKYSUk789qndt5dEC4HfSjviSYlSoHGL2+A==", + "engines": { + "node": ">=14" + } + }, + "node_modules/@rollup/plugin-babel": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz", + "integrity": "sha512-WFfdLWU/xVWKeRQnKmIAQULUI7Il0gZnBIH/ZFO069wYIfPu+8zrfp/KMW0atmELoRDq8FbiP3VCss9MhCut7Q==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.10.4", + "@rollup/pluginutils": "^3.1.0" + }, + "engines": { + "node": ">= 10.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0", + "@types/babel__core": "^7.1.9", + "rollup": "^1.20.0||^2.0.0" + }, + "peerDependenciesMeta": { + "@types/babel__core": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-node-resolve": { + "version": "11.2.1", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-11.2.1.tgz", + "integrity": "sha512-yc2n43jcqVyGE2sqV5/YCmocy9ArjVAP/BeXyTtADTBBX6V0e5UMqwO8CdQ0kzjb6zu5P1qMzsScCMRvE9OlVg==", + "dev": true, + "dependencies": { + "@rollup/pluginutils": "^3.1.0", + "@types/resolve": "1.17.1", + "builtin-modules": "^3.1.0", + "deepmerge": "^4.2.2", + "is-module": "^1.0.0", + "resolve": "^1.19.0" + }, + "engines": { + "node": ">= 10.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0" + } + }, + "node_modules/@rollup/plugin-replace": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-2.4.2.tgz", + "integrity": "sha512-IGcu+cydlUMZ5En85jxHH4qj2hta/11BHq95iHEyb2sbgiN0eCdzvUcHw5gt9pBL5lTi4JDYJ1acCoMGpTvEZg==", + "dev": true, + "dependencies": { + "@rollup/pluginutils": "^3.1.0", + "magic-string": "^0.25.7" + }, + "peerDependencies": { + "rollup": "^1.20.0 || ^2.0.0" + } + }, + "node_modules/@rollup/pluginutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.1.0.tgz", + "integrity": "sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==", + "dev": true, + "dependencies": { + "@types/estree": "0.0.39", + "estree-walker": "^1.0.1", + "picomatch": "^2.2.2" + }, + "engines": { + "node": ">= 8.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0" + } + }, + "node_modules/@rollup/pluginutils/node_modules/@types/estree": { + "version": "0.0.39", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", + "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==", + "dev": true + }, + "node_modules/@rushstack/eslint-patch": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.3.2.tgz", + "integrity": "sha512-V+MvGwaHH03hYhY+k6Ef/xKd6RYlc4q8WBx+2ANmipHJcKuktNcI/NgEsJgdSUF6Lw32njT6OnrRsKYCdgHjYw==", + "dev": true + }, + "node_modules/@sinclair/typebox": { + "version": "0.24.51", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.24.51.tgz", + "integrity": "sha512-1P1OROm/rdubP5aFDSZQILU0vrLCJ4fvHt6EoqHEM+2D/G5MK3bIaymUKLit8Js9gbns5UyJnkP/TZROLw4tUA==", + "dev": true + }, + "node_modules/@sinonjs/commons": { + "version": "1.8.6", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.6.tgz", + "integrity": "sha512-Ky+XkAkqPZSm3NLBeUng77EBQl3cmeJhITaGHdYH8kjVB+aun3S4XBRti2zt17mtt0mIUDiNxYeoJm6drVvBJQ==", + "dev": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-8.1.0.tgz", + "integrity": "sha512-OAPJUAtgeINhh/TAlUID4QTs53Njm7xzddaVlEs/SXwgtiD1tW22zAB/W1wdqfrpmikgaWQ9Fw6Ws+hsiRm5Vg==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^1.7.0" + } + }, + "node_modules/@surma/rollup-plugin-off-main-thread": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz", + "integrity": "sha512-lR8q/9W7hZpMWweNiAKU7NQerBnzQQLvi8qnTDU/fxItPhtZVMbPV3lbCwjhIlNBe9Bbr5V+KHshvWmVSG9cxQ==", + "dev": true, + "dependencies": { + "ejs": "^3.1.6", + "json5": "^2.2.0", + "magic-string": "^0.25.0", + "string.prototype.matchall": "^4.0.6" + } + }, + "node_modules/@svgr/babel-plugin-add-jsx-attribute": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-5.4.0.tgz", + "integrity": "sha512-ZFf2gs/8/6B8PnSofI0inYXr2SDNTDScPXhN7k5EqD4aZ3gi6u+rbmZHVB8IM3wDyx8ntKACZbtXSm7oZGRqVg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@svgr/babel-plugin-remove-jsx-attribute": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-attribute/-/babel-plugin-remove-jsx-attribute-5.4.0.tgz", + "integrity": "sha512-yaS4o2PgUtwLFGTKbsiAy6D0o3ugcUhWK0Z45umJ66EPWunAz9fuFw2gJuje6wqQvQWOTJvIahUwndOXb7QCPg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@svgr/babel-plugin-remove-jsx-empty-expression": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-empty-expression/-/babel-plugin-remove-jsx-empty-expression-5.0.1.tgz", + "integrity": "sha512-LA72+88A11ND/yFIMzyuLRSMJ+tRKeYKeQ+mR3DcAZ5I4h5CPWN9AHyUzJbWSYp/u2u0xhmgOe0+E41+GjEueA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@svgr/babel-plugin-replace-jsx-attribute-value": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-replace-jsx-attribute-value/-/babel-plugin-replace-jsx-attribute-value-5.0.1.tgz", + "integrity": "sha512-PoiE6ZD2Eiy5mK+fjHqwGOS+IXX0wq/YDtNyIgOrc6ejFnxN4b13pRpiIPbtPwHEc+NT2KCjteAcq33/F1Y9KQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@svgr/babel-plugin-svg-dynamic-title": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-dynamic-title/-/babel-plugin-svg-dynamic-title-5.4.0.tgz", + "integrity": "sha512-zSOZH8PdZOpuG1ZVx/cLVePB2ibo3WPpqo7gFIjLV9a0QsuQAzJiwwqmuEdTaW2pegyBE17Uu15mOgOcgabQZg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@svgr/babel-plugin-svg-em-dimensions": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-em-dimensions/-/babel-plugin-svg-em-dimensions-5.4.0.tgz", + "integrity": "sha512-cPzDbDA5oT/sPXDCUYoVXEmm3VIoAWAPT6mSPTJNbQaBNUuEKVKyGH93oDY4e42PYHRW67N5alJx/eEol20abw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@svgr/babel-plugin-transform-react-native-svg": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-react-native-svg/-/babel-plugin-transform-react-native-svg-5.4.0.tgz", + "integrity": "sha512-3eYP/SaopZ41GHwXma7Rmxcv9uRslRDTY1estspeB1w1ueZWd/tPlMfEOoccYpEMZU3jD4OU7YitnXcF5hLW2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@svgr/babel-plugin-transform-svg-component": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-5.5.0.tgz", + "integrity": "sha512-q4jSH1UUvbrsOtlo/tKcgSeiCHRSBdXoIoqX1pgcKK/aU3JD27wmMKwGtpB8qRYUYoyXvfGxUVKchLuR5pB3rQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@svgr/babel-preset": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-preset/-/babel-preset-5.5.0.tgz", + "integrity": "sha512-4FiXBjvQ+z2j7yASeGPEi8VD/5rrGQk4Xrq3EdJmoZgz/tpqChpo5hgXDvmEauwtvOc52q8ghhZK4Oy7qph4ig==", + "dev": true, + "dependencies": { + "@svgr/babel-plugin-add-jsx-attribute": "^5.4.0", + "@svgr/babel-plugin-remove-jsx-attribute": "^5.4.0", + "@svgr/babel-plugin-remove-jsx-empty-expression": "^5.0.1", + "@svgr/babel-plugin-replace-jsx-attribute-value": "^5.0.1", + "@svgr/babel-plugin-svg-dynamic-title": "^5.4.0", + "@svgr/babel-plugin-svg-em-dimensions": "^5.4.0", + "@svgr/babel-plugin-transform-react-native-svg": "^5.4.0", + "@svgr/babel-plugin-transform-svg-component": "^5.5.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@svgr/core": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@svgr/core/-/core-5.5.0.tgz", + "integrity": "sha512-q52VOcsJPvV3jO1wkPtzTuKlvX7Y3xIcWRpCMtBF3MrteZJtBfQw/+u0B1BHy5ColpQc1/YVTrPEtSYIMNZlrQ==", + "dev": true, + "dependencies": { + "@svgr/plugin-jsx": "^5.5.0", + "camelcase": "^6.2.0", + "cosmiconfig": "^7.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@svgr/hast-util-to-babel-ast": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@svgr/hast-util-to-babel-ast/-/hast-util-to-babel-ast-5.5.0.tgz", + "integrity": "sha512-cAaR/CAiZRB8GP32N+1jocovUtvlj0+e65TB50/6Lcime+EA49m/8l+P2ko+XPJ4dw3xaPS3jOL4F2X4KWxoeQ==", + "dev": true, + "dependencies": { + "@babel/types": "^7.12.6" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@svgr/plugin-jsx": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@svgr/plugin-jsx/-/plugin-jsx-5.5.0.tgz", + "integrity": "sha512-V/wVh33j12hGh05IDg8GpIUXbjAPnTdPTKuP4VNLggnwaHMPNQNae2pRnyTAILWCQdz5GyMqtO488g7CKM8CBA==", + "dev": true, + "dependencies": { + "@babel/core": "^7.12.3", + "@svgr/babel-preset": "^5.5.0", + "@svgr/hast-util-to-babel-ast": "^5.5.0", + "svg-parser": "^2.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@svgr/plugin-svgo": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@svgr/plugin-svgo/-/plugin-svgo-5.5.0.tgz", + "integrity": "sha512-r5swKk46GuQl4RrVejVwpeeJaydoxkdwkM1mBKOgJLBUJPGaLci6ylg/IjhrRsREKDkr4kbMWdgOtbXEh0fyLQ==", + "dev": true, + "dependencies": { + "cosmiconfig": "^7.0.0", + "deepmerge": "^4.2.2", + "svgo": "^1.2.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@svgr/webpack": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@svgr/webpack/-/webpack-5.5.0.tgz", + "integrity": "sha512-DOBOK255wfQxguUta2INKkzPj6AIS6iafZYiYmHn6W3pHlycSRRlvWKCfLDG10fXfLWqE3DJHgRUOyJYmARa7g==", + "dev": true, + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/plugin-transform-react-constant-elements": "^7.12.1", + "@babel/preset-env": "^7.12.1", + "@babel/preset-react": "^7.12.5", + "@svgr/core": "^5.5.0", + "@svgr/plugin-jsx": "^5.5.0", + "@svgr/plugin-svgo": "^5.5.0", + "loader-utils": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@tootallnate/once": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", + "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@trysound/sax": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", + "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==", + "dev": true, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.1", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.1.tgz", + "integrity": "sha512-aACu/U/omhdk15O4Nfb+fHgH/z3QsfQzpnvRZhYhThms83ZnAOZz7zZAWO7mn2yyNQaA4xTO8GLK3uqFU4bYYw==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.6.4", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.4.tgz", + "integrity": "sha512-tFkciB9j2K755yrTALxD44McOrk+gfpIpvC3sxHjRawj6PfnQxrse4Clq5y/Rq+G3mrBurMax/lG8Qn2t9mSsg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.1.tgz", + "integrity": "sha512-azBFKemX6kMg5Io+/rdGT0dkGreboUVR0Cdm3fz9QJWpaQGJRQXl7C+6hOTCZcMll7KFyEQpgbYI2lHdsS4U7g==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.1", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.1.tgz", + "integrity": "sha512-MitHFXnhtgwsGZWtT68URpOvLN4EREih1u3QtQiN4VdAxWKRVvGCSvw/Qth0M0Qq3pJpnGOu5JaM/ydK7OGbqg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.20.7" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.2", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", + "integrity": "sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==", + "dev": true, + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/bonjour": { + "version": "3.5.10", + "resolved": "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.10.tgz", + "integrity": "sha512-p7ienRMiS41Nu2/igbJxxLDWrSZ0WxM8UQgCeO9KhoVF7cOVFkrKsiDr1EsJIla8vV3oEEjGcz11jc5yimhzZw==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.35", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", + "integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/connect-history-api-fallback": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.0.tgz", + "integrity": "sha512-4x5FkPpLipqwthjPsF7ZRbOv3uoLUFkTA9G9v583qi4pACvq0uTELrB8OLUzPWUI4IJIyvM85vzkV1nyiI2Lig==", + "dev": true, + "dependencies": { + "@types/express-serve-static-core": "*", + "@types/node": "*" + } + }, + "node_modules/@types/d3": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.0.tgz", + "integrity": "sha512-jIfNVK0ZlxcuRDKtRS/SypEyOQ6UHaFQBKv032X45VvxSJ6Yi5G9behy9h6tNTHTDGh5Vq+KbmBjUWLgY4meCA==", + "dependencies": { + "@types/d3-array": "*", + "@types/d3-axis": "*", + "@types/d3-brush": "*", + "@types/d3-chord": "*", + "@types/d3-color": "*", + "@types/d3-contour": "*", + "@types/d3-delaunay": "*", + "@types/d3-dispatch": "*", + "@types/d3-drag": "*", + "@types/d3-dsv": "*", + "@types/d3-ease": "*", + "@types/d3-fetch": "*", + "@types/d3-force": "*", + "@types/d3-format": "*", + "@types/d3-geo": "*", + "@types/d3-hierarchy": "*", + "@types/d3-interpolate": "*", + "@types/d3-path": "*", + "@types/d3-polygon": "*", + "@types/d3-quadtree": "*", + "@types/d3-random": "*", + "@types/d3-scale": "*", + "@types/d3-scale-chromatic": "*", + "@types/d3-selection": "*", + "@types/d3-shape": "*", + "@types/d3-time": "*", + "@types/d3-time-format": "*", + "@types/d3-timer": "*", + "@types/d3-transition": "*", + "@types/d3-zoom": "*" + } + }, + "node_modules/@types/d3-array": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.0.5.tgz", + "integrity": "sha512-Qk7fpJ6qFp+26VeQ47WY0mkwXaiq8+76RJcncDEfMc2ocRzXLO67bLFRNI4OX1aGBoPzsM5Y2T+/m1pldOgD+A==" + }, + "node_modules/@types/d3-axis": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.2.tgz", + "integrity": "sha512-uGC7DBh0TZrU/LY43Fd8Qr+2ja1FKmH07q2FoZFHo1eYl8aj87GhfVoY1saJVJiq24rp1+wpI6BvQJMKgQm8oA==", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-brush": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.2.tgz", + "integrity": "sha512-2TEm8KzUG3N7z0TrSKPmbxByBx54M+S9lHoP2J55QuLU0VSQ9mE96EJSAOVNEqd1bbynMjeTS9VHmz8/bSw8rA==", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-chord": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.2.tgz", + "integrity": "sha512-abT/iLHD3sGZwqMTX1TYCMEulr+wBd0SzyOQnjYNLp7sngdOHYtNkMRI5v3w5thoN+BWtlHVDx2Osvq6fxhZWw==" + }, + "node_modules/@types/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-HKuicPHJuvPgCD+np6Se9MQvS6OCbJmOjGvylzMJRlDwUXjKTTXs6Pwgk79O09Vj/ho3u1ofXnhFOaEWWPrlwA==" + }, + "node_modules/@types/d3-contour": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.2.tgz", + "integrity": "sha512-k6/bGDoAGJZnZWaKzeB+9glgXCYGvh6YlluxzBREiVo8f/X2vpTEdgPy9DN7Z2i42PZOZ4JDhVdlTSTSkLDPlQ==", + "dependencies": { + "@types/d3-array": "*", + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-delaunay": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.1.tgz", + "integrity": "sha512-tLxQ2sfT0p6sxdG75c6f/ekqxjyYR0+LwPrsO1mbC9YDBzPJhs2HbJJRrn8Ez1DBoHRo2yx7YEATI+8V1nGMnQ==" + }, + "node_modules/@types/d3-dispatch": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.2.tgz", + "integrity": "sha512-rxN6sHUXEZYCKV05MEh4z4WpPSqIw+aP7n9ZN6WYAAvZoEAghEK1WeVZMZcHRBwyaKflU43PCUAJNjFxCzPDjg==" + }, + "node_modules/@types/d3-drag": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.2.tgz", + "integrity": "sha512-qmODKEDvyKWVHcWWCOVcuVcOwikLVsyc4q4EBJMREsoQnR2Qoc2cZQUyFUPgO9q4S3qdSqJKBsuefv+h0Qy+tw==", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-dsv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.1.tgz", + "integrity": "sha512-76pBHCMTvPLt44wFOieouXcGXWOF0AJCceUvaFkxSZEu4VDUdv93JfpMa6VGNFs01FHfuP4a5Ou68eRG1KBfTw==" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.0.tgz", + "integrity": "sha512-aMo4eaAOijJjA6uU+GIeW018dvy9+oH5Y2VPPzjjfxevvGQ/oRDs+tfYC9b50Q4BygRR8yE2QCLsrT0WtAVseA==" + }, + "node_modules/@types/d3-fetch": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.2.tgz", + "integrity": "sha512-gllwYWozWfbep16N9fByNBDTkJW/SyhH6SGRlXloR7WdtAaBui4plTP+gbUgiEot7vGw/ZZop1yDZlgXXSuzjA==", + "dependencies": { + "@types/d3-dsv": "*" + } + }, + "node_modules/@types/d3-force": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.4.tgz", + "integrity": "sha512-q7xbVLrWcXvSBBEoadowIUJ7sRpS1yvgMWnzHJggFy5cUZBq2HZL5k/pBSm0GdYWS1vs5/EDwMjSKF55PDY4Aw==" + }, + "node_modules/@types/d3-format": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.1.tgz", + "integrity": "sha512-5KY70ifCCzorkLuIkDe0Z9YTf9RR2CjBX1iaJG+rgM/cPP+sO+q9YdQ9WdhQcgPj1EQiJ2/0+yUkkziTG6Lubg==" + }, + "node_modules/@types/d3-geo": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.0.3.tgz", + "integrity": "sha512-bK9uZJS3vuDCNeeXQ4z3u0E7OeJZXjUgzFdSOtNtMCJCLvDtWDwfpRVWlyt3y8EvRzI0ccOu9xlMVirawolSCw==", + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-hierarchy": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", + "integrity": "sha512-9hjRTVoZjRFR6xo8igAJyNXQyPX6Aq++Nhb5ebrUF414dv4jr2MitM2fWiOY475wa3Za7TOS2Gh9fmqEhLTt0A==" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-jx5leotSeac3jr0RePOH1KdR9rISG91QIE4Q2PYTu4OymLTZfA3SrnURSLzKH48HmXVUru50b8nje4E79oQSQw==", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.0.0.tgz", + "integrity": "sha512-0g/A+mZXgFkQxN3HniRDbXMN79K3CdTpLsevj+PXiTcb2hVyvkZUBg37StmgCQkaD84cUJ4uaDAWq7UJOQy2Tg==" + }, + "node_modules/@types/d3-polygon": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.0.tgz", + "integrity": "sha512-D49z4DyzTKXM0sGKVqiTDTYr+DHg/uxsiWDAkNrwXYuiZVd9o9wXZIo+YsHkifOiyBkmSWlEngHCQme54/hnHw==" + }, + "node_modules/@types/d3-quadtree": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.2.tgz", + "integrity": "sha512-QNcK8Jguvc8lU+4OfeNx+qnVy7c0VrDJ+CCVFS9srBo2GL9Y18CnIxBdTF3v38flrGy5s1YggcoAiu6s4fLQIw==" + }, + "node_modules/@types/d3-random": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.1.tgz", + "integrity": "sha512-IIE6YTekGczpLYo/HehAy3JGF1ty7+usI97LqraNa8IiDur+L44d0VOjAvFQWJVdZOJHukUJw+ZdZBlgeUsHOQ==" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.3.tgz", + "integrity": "sha512-PATBiMCpvHJSMtZAMEhc2WyL+hnzarKzI6wAHYjhsonjWJYGq5BXTzQjv4l8m2jO183/4wZ90rKvSeT7o72xNQ==", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-scale-chromatic": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.0.0.tgz", + "integrity": "sha512-dsoJGEIShosKVRBZB0Vo3C8nqSDqVGujJU6tPznsBJxNJNwMF8utmS83nvCBKQYPpjCzaaHcrf66iTRpZosLPw==" + }, + "node_modules/@types/d3-selection": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.5.tgz", + "integrity": "sha512-xCB0z3Hi8eFIqyja3vW8iV01+OHGYR2di/+e+AiOcXIOrY82lcvWW8Ke1DYE/EUVMsBl4Db9RppSBS3X1U6J0w==" + }, + "node_modules/@types/d3-shape": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.1.tgz", + "integrity": "sha512-6Uh86YFF7LGg4PQkuO2oG6EMBRLuW9cbavUW46zkIO5kuS2PfTqo2o9SkgtQzguBHbLgNnU90UNsITpsX1My+A==", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.0.tgz", + "integrity": "sha512-sZLCdHvBUcNby1cB6Fd3ZBrABbjz3v1Vm90nysCQ6Vt7vd6e/h9Lt7SiJUoEX0l4Dzc7P5llKyhqSi1ycSf1Hg==" + }, + "node_modules/@types/d3-time-format": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.0.tgz", + "integrity": "sha512-yjfBUe6DJBsDin2BMIulhSHmr5qNR5Pxs17+oW4DoVPyVIXZ+m6bs7j1UVKP08Emv6jRmYrYqxYzO63mQxy1rw==" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.0.tgz", + "integrity": "sha512-HNB/9GHqu7Fo8AQiugyJbv6ZxYz58wef0esl4Mv828w1ZKpAshw/uFWVDUcIB9KKFeFKoxS3cHY07FFgtTRZ1g==" + }, + "node_modules/@types/d3-transition": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.3.tgz", + "integrity": "sha512-/S90Od8Id1wgQNvIA8iFv9jRhCiZcGhPd2qX0bKF/PS+y0W5CrXKgIiELd2CvG1mlQrWK/qlYh3VxicqG1ZvgA==", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-zoom": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.3.tgz", + "integrity": "sha512-OWk1yYIIWcZ07+igN6BeoG6rqhnJ/pYe+R1qWFM2DtW49zsoSjgb9G5xB0ZXA8hh2jAzey1XuRmMSoXdKw8MDA==", + "dependencies": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" + } + }, + "node_modules/@types/eslint": { + "version": "8.44.0", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.44.0.tgz", + "integrity": "sha512-gsF+c/0XOguWgaOgvFs+xnnRqt9GwgTvIks36WpE6ueeI4KCEHHd8K/CKHqhOqrJKsYH8m27kRzQEvWXAwXUTw==", + "dev": true, + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.4", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.4.tgz", + "integrity": "sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA==", + "dev": true, + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.1.tgz", + "integrity": "sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==", + "dev": true + }, + "node_modules/@types/express": { + "version": "4.17.17", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.17.tgz", + "integrity": "sha512-Q4FmmuLGBG58btUnfS1c1r/NQdlp3DMfGDGig8WhfpA2YRUtEkxAjkZb0yvplJGYdF1fsQ81iMDcH24sSCNC/Q==", + "dev": true, + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.17.35", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.35.tgz", + "integrity": "sha512-wALWQwrgiB2AWTT91CB62b6Yt0sNHpznUXeZEcnPU3DRdlDIz74x8Qg1UUYKSVFi+va5vKOLYRBI1bRKiLLKIg==", + "dev": true, + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/geojson": { + "version": "7946.0.10", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.10.tgz", + "integrity": "sha512-Nmh0K3iWQJzniTuPRcJn5hxXkfB1T1pgB89SBig5PlJQU5yocazeu4jATJlaA0GYFKWMqDdvYemoSnF2pXgLVA==" + }, + "node_modules/@types/glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==", + "dev": true, + "dependencies": { + "@types/minimatch": "*", + "@types/node": "*" + } + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.6.tgz", + "integrity": "sha512-Sig0SNORX9fdW+bQuTEovKj3uHcUL6LQKbCrrqb1X7J6/ReAbhCXRAhc+SMejhLELFj2QcyuxmUooZ4bt5ReSw==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/history": { + "version": "4.7.11", + "resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.11.tgz", + "integrity": "sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA==", + "dev": true + }, + "node_modules/@types/html-minifier-terser": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", + "integrity": "sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg==", + "dev": true + }, + "node_modules/@types/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-/K3ds8TRAfBvi5vfjuz8y6+GiAYBZ0x4tXv1Av6CWBWn0IlADc+ZX9pMq7oU0fNQPnBwIZl3rmeLp6SBApbxSQ==", + "dev": true + }, + "node_modules/@types/http-proxy": { + "version": "1.17.11", + "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.11.tgz", + "integrity": "sha512-HC8G7c1WmaF2ekqpnFq626xd3Zz0uvaqFmBJNRZCGEZCXkvSdJoNFn/8Ygbd9fKNQj8UzLdCETaI0UWPAjK7IA==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz", + "integrity": "sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==", + "dev": true + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", + "integrity": "sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz", + "integrity": "sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "27.5.2", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-27.5.2.tgz", + "integrity": "sha512-mpT8LJJ4CMeeahobofYWIjFo0xonRS/HfxnVEPMPFSQdGUt1uHCnoPT7Zhb+sjDU2wz0oKV0OLUR0WzrHNgfeA==", + "dev": true, + "dependencies": { + "jest-matcher-utils": "^27.0.0", + "pretty-format": "^27.0.0" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.12", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.12.tgz", + "integrity": "sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==", + "dev": true + }, + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true + }, + "node_modules/@types/lodash": { + "version": "4.14.195", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.195.tgz", + "integrity": "sha512-Hwx9EUgdwf2GLarOjQp5ZH8ZmblzcbTBC2wtQWNKARBSxM9ezRIAUpeDTgoQRAFB0+8CNWXVA9+MaSOzOF3nPg==", + "dev": true + }, + "node_modules/@types/mime": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", + "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==", + "dev": true + }, + "node_modules/@types/minimatch": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz", + "integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==", + "dev": true + }, + "node_modules/@types/node": { + "version": "16.18.38", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.38.tgz", + "integrity": "sha512-6sfo1qTulpVbkxECP+AVrHV9OoJqhzCsfTNp5NIG+enM4HyM3HvZCO798WShIXBN0+QtDIcutJCjsVYnQP5rIQ==", + "dev": true + }, + "node_modules/@types/parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==", + "dev": true + }, + "node_modules/@types/prettier": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.3.tgz", + "integrity": "sha512-+68kP9yzs4LMp7VNh8gdzMSPZFL44MLGqiHWvttYJe+6qnuVr4Ek9wSBQoveqY/r+LwjCcU29kNVkidwim+kYA==", + "dev": true + }, + "node_modules/@types/prop-types": { + "version": "15.7.5", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", + "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==", + "dev": true + }, + "node_modules/@types/q": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.5.tgz", + "integrity": "sha512-L28j2FcJfSZOnL1WBjDYp2vUHCeIFlyYI/53EwD/rKUBQ7MtUUfbQWiyKJGpcnv4/WgrhWsFKrcPstcAt/J0tQ==", + "dev": true + }, + "node_modules/@types/qs": { + "version": "6.9.7", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", + "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==", + "dev": true + }, + "node_modules/@types/range-parser": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz", + "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==", + "dev": true + }, + "node_modules/@types/react": { + "version": "18.2.15", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.15.tgz", + "integrity": "sha512-oEjE7TQt1fFTFSbf8kkNuc798ahTUzn3Le67/PWjE8MAfYAD/qB7O8hSTcromLFqHCt9bcdOg5GXMokzTjJ5SA==", + "dev": true, + "dependencies": { + "@types/prop-types": "*", + "@types/scheduler": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.2.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.7.tgz", + "integrity": "sha512-GRaAEriuT4zp9N4p1i8BDBYmEyfo+xQ3yHjJU4eiK5NDa1RmUZG+unZABUTK4/Ox/M+GaHwb6Ow8rUITrtjszA==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/react-router": { + "version": "5.1.20", + "resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.20.tgz", + "integrity": "sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q==", + "dev": true, + "dependencies": { + "@types/history": "^4.7.11", + "@types/react": "*" + } + }, + "node_modules/@types/react-router-dom": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.3.3.tgz", + "integrity": "sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw==", + "dev": true, + "dependencies": { + "@types/history": "^4.7.11", + "@types/react": "*", + "@types/react-router": "*" + } + }, + "node_modules/@types/react-router-hash-link": { + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/@types/react-router-hash-link/-/react-router-hash-link-2.4.6.tgz", + "integrity": "sha512-JOV4Q1N60tJJUPisS/u1jiXn8c4jX7ThQf8XavzZYIWOIv0RP17nbyI9YgEZh1r3APXpP9ZkU1ytrlv+1+8jcw==", + "dev": true, + "dependencies": { + "@types/history": "^4.7.11", + "@types/react": "*", + "@types/react-router-dom": "^5.3.0" + } + }, + "node_modules/@types/resolve": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz", + "integrity": "sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", + "dev": true + }, + "node_modules/@types/scheduler": { + "version": "0.16.3", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz", + "integrity": "sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==", + "dev": true + }, + "node_modules/@types/semver": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.0.tgz", + "integrity": "sha512-G8hZ6XJiHnuhQKR7ZmysCeJWE08o8T0AXtk5darsCaTVsYZhhgUrq53jizaR2FvsoeCwJhlmwTjkXBY5Pn/ZHw==", + "dev": true + }, + "node_modules/@types/send": { + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.1.tgz", + "integrity": "sha512-Cwo8LE/0rnvX7kIIa3QHCkcuF21c05Ayb0ZfxPiv0W8VRiZiNW/WuRupHKpqqGVGf7SUA44QSOUKaEd9lIrd/Q==", + "dev": true, + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-index": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.1.tgz", + "integrity": "sha512-d/Hs3nWDxNL2xAczmOVZNj92YZCS6RGxfBPjKzuu/XirCgXdpKEb88dYNbrYGint6IVWLNP+yonwVAuRC0T2Dg==", + "dev": true, + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.2.tgz", + "integrity": "sha512-J2LqtvFYCzaj8pVYKw8klQXrLLk7TBZmQ4ShlcdkELFKGwGMfevMLneMMRkMgZxotOD9wg497LpC7O8PcvAmfw==", + "dev": true, + "dependencies": { + "@types/http-errors": "*", + "@types/mime": "*", + "@types/node": "*" + } + }, + "node_modules/@types/sockjs": { + "version": "0.3.33", + "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.33.tgz", + "integrity": "sha512-f0KEEe05NvUnat+boPTZ0dgaLZ4SfSouXUgv5noUiefG2ajgKjmETo9ZJyuqsl7dfl2aHlLJUiki6B4ZYldiiw==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz", + "integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==", + "dev": true + }, + "node_modules/@types/trusted-types": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.3.tgz", + "integrity": "sha512-NfQ4gyz38SL8sDNrSixxU2Os1a5xcdFxipAFxYEuLUlvU2uDwS4NUpsImcf1//SlWItCVMMLiylsxbmNMToV/g==", + "dev": true + }, + "node_modules/@types/ws": { + "version": "8.5.5", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.5.tgz", + "integrity": "sha512-lwhs8hktwxSjf9UaZ9tG5M03PGogvFaH8gUgLNbN9HKIg0dvv6q+gkSuJ8HN4/VbyxkuLzCjlN7GquQ0gUJfIg==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/yargs": { + "version": "16.0.5", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.5.tgz", + "integrity": "sha512-AxO/ADJOBFJScHbWhq2xAhlWP24rY4aCEG/NFaMvbT3X2MgRsLjhjQwsn0Zi5zn0LG9jUhCCZMeX9Dkuw6k+vQ==", + "dev": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.0", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.0.tgz", + "integrity": "sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==", + "dev": true + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz", + "integrity": "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==", + "dev": true, + "dependencies": { + "@eslint-community/regexpp": "^4.4.0", + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/type-utils": "5.62.0", + "@typescript-eslint/utils": "5.62.0", + "debug": "^4.3.4", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "natural-compare-lite": "^1.4.0", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^5.0.0", + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/experimental-utils": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-5.62.0.tgz", + "integrity": "sha512-RTXpeB3eMkpoclG3ZHft6vG/Z30azNHuqY6wKPBHlVMZFuEvrtlEDe8gMqDb+SO+9hjC/pLekeSCryf9vMZlCw==", + "dev": true, + "dependencies": { + "@typescript-eslint/utils": "5.62.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz", + "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/typescript-estree": "5.62.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz", + "integrity": "sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/visitor-keys": "5.62.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz", + "integrity": "sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==", + "dev": true, + "dependencies": { + "@typescript-eslint/typescript-estree": "5.62.0", + "@typescript-eslint/utils": "5.62.0", + "debug": "^4.3.4", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz", + "integrity": "sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz", + "integrity": "sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/visitor-keys": "5.62.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.62.0.tgz", + "integrity": "sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@types/json-schema": "^7.0.9", + "@types/semver": "^7.3.12", + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/typescript-estree": "5.62.0", + "eslint-scope": "^5.1.1", + "semver": "^7.3.7" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz", + "integrity": "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.6.tgz", + "integrity": "sha512-IN1xI7PwOvLPgjcf180gC1bqn3q/QaOCwYUahIOhbYUu8KA/3tw2RT/T0Gidi1l7Hhj5D/INhJxiICObqpMu4Q==", + "dev": true, + "dependencies": { + "@webassemblyjs/helper-numbers": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz", + "integrity": "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz", + "integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.6.tgz", + "integrity": "sha512-z3nFzdcp1mb8nEOFFk8DrYLpHvhKC3grJD2ardfKOzmbmJvEf/tPIqCY+sNcwZIY8ZD7IkB2l7/pqhUhqm7hLA==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz", + "integrity": "sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==", + "dev": true, + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.11.6", + "@webassemblyjs/helper-api-error": "1.11.6", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz", + "integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.6.tgz", + "integrity": "sha512-LPpZbSOwTpEC2cgn4hTydySy1Ke+XEu+ETXuoyvuyezHO3Kjdu90KK95Sh9xTbmjrCsUwvWwCOQQNta37VrS9g==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/wasm-gen": "1.11.6" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz", + "integrity": "sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==", + "dev": true, + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.6.tgz", + "integrity": "sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==", + "dev": true, + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.6.tgz", + "integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==", + "dev": true + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.6.tgz", + "integrity": "sha512-Ybn2I6fnfIGuCR+Faaz7YcvtBKxvoLV3Lebn1tM4o/IAJzmi9AWYIPWpyBfU8cC+JxAO57bk4+zdsTjJR+VTOw==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/helper-wasm-section": "1.11.6", + "@webassemblyjs/wasm-gen": "1.11.6", + "@webassemblyjs/wasm-opt": "1.11.6", + "@webassemblyjs/wasm-parser": "1.11.6", + "@webassemblyjs/wast-printer": "1.11.6" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.6.tgz", + "integrity": "sha512-3XOqkZP/y6B4F0PBAXvI1/bky7GryoogUtfwExeP/v7Nzwo1QLcq5oQmpKlftZLbT+ERUOAZVQjuNVak6UXjPA==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/ieee754": "1.11.6", + "@webassemblyjs/leb128": "1.11.6", + "@webassemblyjs/utf8": "1.11.6" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.6.tgz", + "integrity": "sha512-cOrKuLRE7PCe6AsOVl7WasYf3wbSo4CeOk6PkrjS7g57MFfVUF9u6ysQBBODX0LdgSvQqRiGz3CXvIDKcPNy4g==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/wasm-gen": "1.11.6", + "@webassemblyjs/wasm-parser": "1.11.6" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.6.tgz", + "integrity": "sha512-6ZwPeGzMJM3Dqp3hCsLgESxBGtT/OeCvCZ4TA1JUPYgmhAx38tTPR9JaKy0S5H3evQpO/h2uWs2j6Yc/fjkpTQ==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-api-error": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/ieee754": "1.11.6", + "@webassemblyjs/leb128": "1.11.6", + "@webassemblyjs/utf8": "1.11.6" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.6.tgz", + "integrity": "sha512-JM7AhRcE+yW2GWYaKeHL5vt4xqee5N2WcezptmgyhNS+ScggqcT1OtXykhAb13Sn5Yas0j2uv9tHgrjwvzAP4A==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true + }, + "node_modules/abab": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", + "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", + "dev": true + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dev": true, + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", + "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-globals": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-6.0.0.tgz", + "integrity": "sha512-ZQl7LOWaF5ePqqcX4hLuv/bLXYQNfNWw2c0/yX/TsPRKamzHcTGQnlCjHT3TsmkOUVEPS3crCxiPfdzE/Trlhg==", + "dev": true, + "dependencies": { + "acorn": "^7.1.1", + "acorn-walk": "^7.1.1" + } + }, + "node_modules/acorn-globals/node_modules/acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-import-assertions": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz", + "integrity": "sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==", + "dev": true, + "peerDependencies": { + "acorn": "^8" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/acorn-walk": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz", + "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/address": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/address/-/address-1.2.2.tgz", + "integrity": "sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA==", + "dev": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/adjust-sourcemap-loader": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/adjust-sourcemap-loader/-/adjust-sourcemap-loader-4.0.0.tgz", + "integrity": "sha512-OXwN5b9pCUXNQHJpwwD2qP40byEmSgzj8B4ydSN0uMNYWiFmJ6x6KwUllMmfk8Rwu/HJDFR7U8ubsWBoN0Xp0A==", + "dev": true, + "dependencies": { + "loader-utils": "^2.0.0", + "regex-parser": "^2.2.11" + }, + "engines": { + "node": ">=8.9" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-formats/node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-html-community": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ansi-html-community/-/ansi-html-community-0.0.8.tgz", + "integrity": "sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==", + "dev": true, + "engines": [ + "node >= 0.8.0" + ], + "bin": { + "ansi-html": "bin/ansi-html" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz", + "integrity": "sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "is-array-buffer": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-flatten": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.2.tgz", + "integrity": "sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==", + "dev": true + }, + "node_modules/array-includes": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.6.tgz", + "integrity": "sha512-sgTbLvL6cNnw24FnbaDyjmvddQ2ML8arZsgaJhoABMoplz/4QRhtrYS+alr1BUM1Bwp6dhx8vVCBSLG+StwOFw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4", + "get-intrinsic": "^1.1.3", + "is-string": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.1.tgz", + "integrity": "sha512-roTU0KWIOmJ4DRLmwKd19Otg0/mT3qPNt0Qb3GWW8iObuZXxrjB/pzn0R3hqpRSWg4HCwqx+0vwOnWnvlOyeIA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4", + "es-shim-unscopables": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.1.tgz", + "integrity": "sha512-8UGn9O1FDVvMNB0UlLv4voxRMze7+FpHyF5mSMRjWHUMlpoDViniy05870VlxhfgTnLbpuwTzvD76MTtWxB/mQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4", + "es-shim-unscopables": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.reduce": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/array.prototype.reduce/-/array.prototype.reduce-1.0.5.tgz", + "integrity": "sha512-kDdugMl7id9COE8R7MHF5jWk7Dqt/fs4Pv+JXoICnYwqpjjjbUurz6w5fT5IG6brLdJhv6/VoHB0H7oyIBXd+Q==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4", + "es-array-method-boxes-properly": "^1.0.0", + "is-string": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.tosorted": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.1.tgz", + "integrity": "sha512-pZYPXPRl2PqWcsUs6LOMn+1f1532nEoPTYowBtqLwAW+W8vSVhkIGnmOX1t/UQjD6YGI0vcD2B1U7ZFGQH9jnQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4", + "es-shim-unscopables": "^1.0.0", + "get-intrinsic": "^1.1.3" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.1.tgz", + "integrity": "sha512-09x0ZWFEjj4WD8PDbykUwo3t9arLn8NIzmmYEJFpYekOAQjpkGSyrQhNoRTcwwcFRu+ycWF78QZ63oWTqSjBcw==", + "dev": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.0", + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "get-intrinsic": "^1.2.1", + "is-array-buffer": "^3.0.2", + "is-shared-array-buffer": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "dev": true + }, + "node_modules/ast-types-flow": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.7.tgz", + "integrity": "sha512-eBvWn1lvIApYMhzQMsu9ciLfkBY499mFZlNqG+/9WR7PVlroQw0vG30cOQQbaKz3sCEc44TAOu2ykzqXSNnwag==", + "dev": true + }, + "node_modules/async": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz", + "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==", + "dev": true + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true + }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/autoprefixer": { + "version": "10.4.14", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.14.tgz", + "integrity": "sha512-FQzyfOsTlwVzjHxKEqRIAdJx9niO6VCBCoEwax/VLSoQF29ggECcPuBqUMZ+u8jCZOPSy8b8/8KnuFbp0SaFZQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + } + ], + "dependencies": { + "browserslist": "^4.21.5", + "caniuse-lite": "^1.0.30001464", + "fraction.js": "^4.2.0", + "normalize-range": "^0.1.2", + "picocolors": "^1.0.0", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", + "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/axe-core": { + "version": "4.7.2", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.7.2.tgz", + "integrity": "sha512-zIURGIS1E1Q4pcrMjp+nnEh+16G56eG/MUllJH8yEvw7asDo7Ac9uhC9KIH5jzpITueEZolfYglnCGIuSBz39g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/axobject-query": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.2.1.tgz", + "integrity": "sha512-jsyHu61e6N4Vbz/v18DHwWYKK0bSWLqn47eeDSKPB7m8tqMHF9YJ+mhIk2lVteyZrY8tnSj/jHOv4YiTCuCJgg==", + "dev": true, + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/babel-jest": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-27.5.1.tgz", + "integrity": "sha512-cdQ5dXjGRd0IBRATiQ4mZGlGlRE8kJpjPOixdNRdT+m3UcNqmYWN6rK6nvtXYfY3D76cb8s/O1Ss8ea24PIwcg==", + "dev": true, + "dependencies": { + "@jest/transform": "^27.5.1", + "@jest/types": "^27.5.1", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^27.5.1", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-jest/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/babel-jest/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/babel-jest/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/babel-jest/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/babel-jest/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-jest/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-loader": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.3.0.tgz", + "integrity": "sha512-H8SvsMF+m9t15HNLMipppzkC+Y2Yq+v3SonZyU70RBL/h1gxPkH08Ot8pEE9Z4Kd+czyWJClmFS8qzIP9OZ04Q==", + "dev": true, + "dependencies": { + "find-cache-dir": "^3.3.1", + "loader-utils": "^2.0.0", + "make-dir": "^3.1.0", + "schema-utils": "^2.6.5" + }, + "engines": { + "node": ">= 8.9" + }, + "peerDependencies": { + "@babel/core": "^7.0.0", + "webpack": ">=2" + } + }, + "node_modules/babel-loader/node_modules/schema-utils": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.1.tgz", + "integrity": "sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.5", + "ajv": "^6.12.4", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 8.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-27.5.1.tgz", + "integrity": "sha512-50wCwD5EMNW4aRpOwtqzyZHIewTYNxLA4nhB+09d8BIssfNfzBRhkBIHiaPv1Si226TQSvp8gxAJm2iY2qs2hQ==", + "dev": true, + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.0.0", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/babel-plugin-macros": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", + "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.12.5", + "cosmiconfig": "^7.0.0", + "resolve": "^1.19.0" + }, + "engines": { + "node": ">=10", + "npm": ">=6" + } + }, + "node_modules/babel-plugin-named-asset-import": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/babel-plugin-named-asset-import/-/babel-plugin-named-asset-import-0.3.8.tgz", + "integrity": "sha512-WXiAc++qo7XcJ1ZnTYGtLxmBCVbddAml3CEXgWaBzNzLNoxtQ8AiGEFDMOhot9XjTCQbvP5E77Fj9Gk924f00Q==", + "dev": true, + "peerDependencies": { + "@babel/core": "^7.1.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs2": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.4.tgz", + "integrity": "sha512-9WeK9snM1BfxB38goUEv2FLnA6ja07UMfazFHzCXUb3NyDZAwfXvQiURQ6guTTMeHcOsdknULm1PDhs4uWtKyA==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.22.6", + "@babel/helper-define-polyfill-provider": "^0.4.1", + "@nicolo-ribaudo/semver-v6": "^6.3.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/babel-plugin-polyfill-corejs3": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.8.2.tgz", + "integrity": "sha512-Cid+Jv1BrY9ReW9lIfNlNpsI53N+FN7gE+f73zLAUbr9C52W4gKLWSByx47pfDJsEysojKArqOtOKZSVIIUTuQ==", + "dev": true, + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.4.1", + "core-js-compat": "^3.31.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/babel-plugin-polyfill-regenerator": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.5.1.tgz", + "integrity": "sha512-L8OyySuI6OSQ5hFy9O+7zFjyr4WhAfRjLIOkhQGYl+emwJkd/S4XXT1JpfrgR1jrQ1NcGiOh+yAdGlF8pnC3Jw==", + "dev": true, + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.4.1" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/babel-plugin-transform-react-remove-prop-types": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-react-remove-prop-types/-/babel-plugin-transform-react-remove-prop-types-0.4.24.tgz", + "integrity": "sha512-eqj0hVcJUR57/Ug2zE1Yswsw4LhuqqHhD+8v120T1cl3kjg76QwtyBrdIk4WVwK+lAhBJVYCd/v+4nc4y+8JsA==", + "dev": true + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz", + "integrity": "sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==", + "dev": true, + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.8.3", + "@babel/plugin-syntax-import-meta": "^7.8.3", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.8.3", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.8.3", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-top-level-await": "^7.8.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/babel-preset-jest": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-27.5.1.tgz", + "integrity": "sha512-Nptf2FzlPCWYuJg41HBqXVT8ym6bXOevuCTbhxlUpjwtysGaIWFvDEjp4y+G7fl13FgOdjs7P/DmErqH7da0Ag==", + "dev": true, + "dependencies": { + "babel-plugin-jest-hoist": "^27.5.1", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/babel-preset-react-app": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/babel-preset-react-app/-/babel-preset-react-app-10.0.1.tgz", + "integrity": "sha512-b0D9IZ1WhhCWkrTXyFuIIgqGzSkRIH5D5AmB0bXbzYAB1OBAwHcUeyWW2LorutLWF5btNo/N7r/cIdmvvKJlYg==", + "dev": true, + "dependencies": { + "@babel/core": "^7.16.0", + "@babel/plugin-proposal-class-properties": "^7.16.0", + "@babel/plugin-proposal-decorators": "^7.16.4", + "@babel/plugin-proposal-nullish-coalescing-operator": "^7.16.0", + "@babel/plugin-proposal-numeric-separator": "^7.16.0", + "@babel/plugin-proposal-optional-chaining": "^7.16.0", + "@babel/plugin-proposal-private-methods": "^7.16.0", + "@babel/plugin-transform-flow-strip-types": "^7.16.0", + "@babel/plugin-transform-react-display-name": "^7.16.0", + "@babel/plugin-transform-runtime": "^7.16.4", + "@babel/preset-env": "^7.16.4", + "@babel/preset-react": "^7.16.0", + "@babel/preset-typescript": "^7.16.0", + "@babel/runtime": "^7.16.3", + "babel-plugin-macros": "^3.1.0", + "babel-plugin-transform-react-remove-prop-types": "^0.4.24" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/batch": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", + "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==", + "dev": true + }, + "node_modules/bfj": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/bfj/-/bfj-7.0.2.tgz", + "integrity": "sha512-+e/UqUzwmzJamNF50tBV6tZPTORow7gQ96iFow+8b562OdMpEK0BcJEq2OSPEDmAbSMBQ7PKZ87ubFkgxpYWgw==", + "dev": true, + "dependencies": { + "bluebird": "^3.5.5", + "check-types": "^11.1.1", + "hoopy": "^0.1.4", + "tryer": "^1.0.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "dev": true + }, + "node_modules/body-parser": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", + "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", + "dev": true, + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.11.0", + "raw-body": "2.5.1", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/bonjour-service": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.1.1.tgz", + "integrity": "sha512-Z/5lQRMOG9k7W+FkeGTNjh7htqn/2LMnfOvBZ8pynNZCM9MwkQkI3zeI4oz09uWdcgmgHugVvBqxGg4VQJ5PCg==", + "dev": true, + "dependencies": { + "array-flatten": "^2.1.2", + "dns-equal": "^1.0.0", + "fast-deep-equal": "^3.1.3", + "multicast-dns": "^7.2.5" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browser-process-hrtime": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz", + "integrity": "sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==", + "dev": true + }, + "node_modules/browserslist": { + "version": "4.21.9", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.9.tgz", + "integrity": "sha512-M0MFoZzbUrRU4KNfCrDLnvyE7gub+peetoTid3TBIqtunaDJyXlwhakT+/VkvSXcfIzFfK/nkCs4nmyTmxdNSg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001503", + "electron-to-chromium": "^1.4.431", + "node-releases": "^2.0.12", + "update-browserslist-db": "^1.0.11" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true + }, + "node_modules/builtin-modules": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", + "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==", + "dev": true, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-me-maybe": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz", + "integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==", + "dev": true + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/camel-case": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz", + "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==", + "dev": true, + "dependencies": { + "pascal-case": "^3.1.2", + "tslib": "^2.0.3" + } + }, + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-api": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz", + "integrity": "sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==", + "dev": true, + "dependencies": { + "browserslist": "^4.0.0", + "caniuse-lite": "^1.0.0", + "lodash.memoize": "^4.1.2", + "lodash.uniq": "^4.5.0" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001516", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001516.tgz", + "integrity": "sha512-Wmec9pCBY8CWbmI4HsjBeQLqDTqV91nFVR83DnZpYyRnPI1wePDsTg0bGLPC5VU/3OIZV1fmxEea1b+tFKe86g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/case-sensitive-paths-webpack-plugin": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/case-sensitive-paths-webpack-plugin/-/case-sensitive-paths-webpack-plugin-2.4.0.tgz", + "integrity": "sha512-roIFONhcxog0JSSWbvVAh3OocukmSgpqOH6YpMkCvav/ySIV3JKg4Dc8vYtQjYi/UxpNE36r/9v+VqTQqgkYmw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/check-types": { + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/check-types/-/check-types-11.2.2.tgz", + "integrity": "sha512-HBiYvXvn9Z70Z88XKjz3AEKd4HJhBXsa3j7xFnITAzoS8+q6eIGi8qDB8FKPBAjtuxjI/zFpwuiCb8oDtKOYrA==", + "dev": true + }, + "node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/chrome-trace-event": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", + "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==", + "dev": true, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/ci-info": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.8.0.tgz", + "integrity": "sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.3.tgz", + "integrity": "sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==", + "dev": true + }, + "node_modules/classcat": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.4.tgz", + "integrity": "sha512-sbpkOw6z413p+HDGcBENe498WM9woqWHiJxCq7nvmxe9WmrUmqfAcxpIwAiMtM5Q3AhYkzXcNQHqsWq0mND51g==" + }, + "node_modules/classnames": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.2.tgz", + "integrity": "sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw==" + }, + "node_modules/clean-css": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.2.tgz", + "integrity": "sha512-JVJbM+f3d3Q704rF4bqQ5UUyTtuJ0JRKNbTKVEeujCCBoMdkEi+V+e8oktO9qGQNSvHrFTM6JZRXrUvGR1czww==", + "dev": true, + "dependencies": { + "source-map": "~0.6.0" + }, + "engines": { + "node": ">= 10.0" + } + }, + "node_modules/clean-css/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/cli-color": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/cli-color/-/cli-color-2.0.3.tgz", + "integrity": "sha512-OkoZnxyC4ERN3zLzZaY9Emb7f/MhBOIpePv0Ycok0fJYT+Ouo00UBEIwsVsr0yoow++n5YWlSUgST9GKhNHiRQ==", + "dev": true, + "dependencies": { + "d": "^1.0.1", + "es5-ext": "^0.10.61", + "es6-iterator": "^2.0.3", + "memoizee": "^0.4.15", + "timers-ext": "^0.1.7" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/clsx": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.0.0.tgz", + "integrity": "sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/coa": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/coa/-/coa-2.0.2.tgz", + "integrity": "sha512-q5/jG+YQnSy4nRTV4F7lPepBJZ8qBNJJDBuJdoejDyLXgmL7IEo+Le2JDZudFTFt7mrCqIRaSjws4ygRCTCAXA==", + "dev": true, + "dependencies": { + "@types/q": "^1.5.1", + "chalk": "^2.4.1", + "q": "^1.1.2" + }, + "engines": { + "node": ">= 4.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", + "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", + "dev": true + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/colord": { + "version": "2.9.3", + "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz", + "integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==", + "dev": true + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "dev": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/common-path-prefix": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/common-path-prefix/-/common-path-prefix-3.0.0.tgz", + "integrity": "sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==", + "dev": true + }, + "node_modules/common-tags": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz", + "integrity": "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==", + "dev": true, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "dev": true + }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "dev": true, + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", + "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", + "dev": true, + "dependencies": { + "accepts": "~1.3.5", + "bytes": "3.0.0", + "compressible": "~2.0.16", + "debug": "2.6.9", + "on-headers": "~1.0.2", + "safe-buffer": "5.1.2", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/compression/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/compression/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/compression/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/confusing-browser-globals": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz", + "integrity": "sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA==", + "dev": true + }, + "node_modules/connect-history-api-fallback": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz", + "integrity": "sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==", + "dev": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dev": true, + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true + }, + "node_modules/cookie": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", + "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "dev": true + }, + "node_modules/core-js": { + "version": "3.31.1", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.31.1.tgz", + "integrity": "sha512-2sKLtfq1eFST7l7v62zaqXacPc7uG8ZAya8ogijLhTtaKNcpzpB4TMoTw2Si+8GYKRwFPMMtUT0263QFWFfqyQ==", + "dev": true, + "hasInstallScript": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/core-js-compat": { + "version": "3.31.1", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.31.1.tgz", + "integrity": "sha512-wIDWd2s5/5aJSdpOJHfSibxNODxoGoWOBHt8JSPB41NOE94M7kuTPZCYLOlTtuoXTsBPKobpJ6T+y0SSy5L9SA==", + "dev": true, + "dependencies": { + "browserslist": "^4.21.9" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/core-js-pure": { + "version": "3.31.1", + "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.31.1.tgz", + "integrity": "sha512-w+C62kvWti0EPs4KPMCMVv9DriHSXfQOCQ94bGGBiEW5rrbtt/Rz8n5Krhfw9cpFyzXBjf3DB3QnPdEzGDY4Fw==", + "dev": true, + "hasInstallScript": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true + }, + "node_modules/cosmiconfig": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "dev": true, + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/crypto-random-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", + "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/css-blank-pseudo": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/css-blank-pseudo/-/css-blank-pseudo-3.0.3.tgz", + "integrity": "sha512-VS90XWtsHGqoM0t4KpH053c4ehxZ2E6HtGI7x68YFV0pTo/QmkV/YFA+NnlvK8guxZVNWGQhVNJGC39Q8XF4OQ==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.9" + }, + "bin": { + "css-blank-pseudo": "dist/cli.cjs" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/css-declaration-sorter": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-6.4.1.tgz", + "integrity": "sha512-rtdthzxKuyq6IzqX6jEcIzQF/YqccluefyCYheovBOLhFT/drQA9zj/UbRAa9J7C0o6EG6u3E6g+vKkay7/k3g==", + "dev": true, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.0.9" + } + }, + "node_modules/css-has-pseudo": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/css-has-pseudo/-/css-has-pseudo-3.0.4.tgz", + "integrity": "sha512-Vse0xpR1K9MNlp2j5w1pgWIJtm1a8qS0JwS9goFYcImjlHEmywP9VUF05aGBXzGpDJF86QXk4L0ypBmwPhGArw==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.9" + }, + "bin": { + "css-has-pseudo": "dist/cli.cjs" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/css-loader": { + "version": "6.8.1", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.8.1.tgz", + "integrity": "sha512-xDAXtEVGlD0gJ07iclwWVkLoZOpEvAWaSyf6W18S2pOC//K8+qUDIx8IIT3D+HjnmkJPQeesOPv5aiUaJsCM2g==", + "dev": true, + "dependencies": { + "icss-utils": "^5.1.0", + "postcss": "^8.4.21", + "postcss-modules-extract-imports": "^3.0.0", + "postcss-modules-local-by-default": "^4.0.3", + "postcss-modules-scope": "^3.0.0", + "postcss-modules-values": "^4.0.0", + "postcss-value-parser": "^4.2.0", + "semver": "^7.3.8" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, + "node_modules/css-minimizer-webpack-plugin": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/css-minimizer-webpack-plugin/-/css-minimizer-webpack-plugin-3.4.1.tgz", + "integrity": "sha512-1u6D71zeIfgngN2XNRJefc/hY7Ybsxd74Jm4qngIXyUEk7fss3VUzuHxLAq/R8NAba4QU9OUSaMZlbpRc7bM4Q==", + "dev": true, + "dependencies": { + "cssnano": "^5.0.6", + "jest-worker": "^27.0.2", + "postcss": "^8.3.5", + "schema-utils": "^4.0.0", + "serialize-javascript": "^6.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "@parcel/css": { + "optional": true + }, + "clean-css": { + "optional": true + }, + "csso": { + "optional": true + }, + "esbuild": { + "optional": true + } + } + }, + "node_modules/css-minimizer-webpack-plugin/node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/css-minimizer-webpack-plugin/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/css-minimizer-webpack-plugin/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "node_modules/css-minimizer-webpack-plugin/node_modules/schema-utils": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz", + "integrity": "sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/css-minimizer-webpack-plugin/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/css-prefers-color-scheme": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/css-prefers-color-scheme/-/css-prefers-color-scheme-6.0.3.tgz", + "integrity": "sha512-4BqMbZksRkJQx2zAjrokiGMd07RqOa2IxIrrN10lyBe9xhn9DEvjUK79J6jkeiv9D9hQFXKb6g1jwU62jziJZA==", + "dev": true, + "bin": { + "css-prefers-color-scheme": "dist/cli.cjs" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/css-select": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz", + "integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==", + "dev": true, + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.0.1", + "domhandler": "^4.3.1", + "domutils": "^2.8.0", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-select-base-adapter": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz", + "integrity": "sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w==", + "dev": true + }, + "node_modules/css-tree": { + "version": "1.0.0-alpha.37", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.37.tgz", + "integrity": "sha512-DMxWJg0rnz7UgxKT0Q1HU/L9BeJI0M6ksor0OgqOnF+aRCDWg/N2641HmVyU9KVIu0OVVWOb2IpC9A+BJRnejg==", + "dev": true, + "dependencies": { + "mdn-data": "2.0.4", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/css-tree/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "dev": true, + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cssdb": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/cssdb/-/cssdb-7.6.0.tgz", + "integrity": "sha512-Nna7rph8V0jC6+JBY4Vk4ndErUmfJfV6NJCaZdurL0omggabiy+QB2HCQtu5c/ACLZ0I7REv7A4QyPIoYzZx0w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + } + ] + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cssnano": { + "version": "5.1.15", + "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-5.1.15.tgz", + "integrity": "sha512-j+BKgDcLDQA+eDifLx0EO4XSA56b7uut3BQFH+wbSaSTuGLuiyTa/wbRYthUXX8LC9mLg+WWKe8h+qJuwTAbHw==", + "dev": true, + "dependencies": { + "cssnano-preset-default": "^5.2.14", + "lilconfig": "^2.0.3", + "yaml": "^1.10.2" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/cssnano" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/cssnano-preset-default": { + "version": "5.2.14", + "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-5.2.14.tgz", + "integrity": "sha512-t0SFesj/ZV2OTylqQVOrFgEh5uanxbO6ZAdeCrNsUQ6fVuXwYTxJPNAGvGTxHbD68ldIJNec7PyYZDBrfDQ+6A==", + "dev": true, + "dependencies": { + "css-declaration-sorter": "^6.3.1", + "cssnano-utils": "^3.1.0", + "postcss-calc": "^8.2.3", + "postcss-colormin": "^5.3.1", + "postcss-convert-values": "^5.1.3", + "postcss-discard-comments": "^5.1.2", + "postcss-discard-duplicates": "^5.1.0", + "postcss-discard-empty": "^5.1.1", + "postcss-discard-overridden": "^5.1.0", + "postcss-merge-longhand": "^5.1.7", + "postcss-merge-rules": "^5.1.4", + "postcss-minify-font-values": "^5.1.0", + "postcss-minify-gradients": "^5.1.1", + "postcss-minify-params": "^5.1.4", + "postcss-minify-selectors": "^5.2.1", + "postcss-normalize-charset": "^5.1.0", + "postcss-normalize-display-values": "^5.1.0", + "postcss-normalize-positions": "^5.1.1", + "postcss-normalize-repeat-style": "^5.1.1", + "postcss-normalize-string": "^5.1.0", + "postcss-normalize-timing-functions": "^5.1.0", + "postcss-normalize-unicode": "^5.1.1", + "postcss-normalize-url": "^5.1.0", + "postcss-normalize-whitespace": "^5.1.1", + "postcss-ordered-values": "^5.1.3", + "postcss-reduce-initial": "^5.1.2", + "postcss-reduce-transforms": "^5.1.0", + "postcss-svgo": "^5.1.0", + "postcss-unique-selectors": "^5.1.1" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/cssnano-utils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-3.1.0.tgz", + "integrity": "sha512-JQNR19/YZhz4psLX/rQ9M83e3z2Wf/HdJbryzte4a3NSuafyp9w/I4U+hx5C2S9g41qlstH7DEWnZaaj83OuEA==", + "dev": true, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/csso": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/csso/-/csso-4.2.0.tgz", + "integrity": "sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA==", + "dev": true, + "dependencies": { + "css-tree": "^1.1.2" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/csso/node_modules/css-tree": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz", + "integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==", + "dev": true, + "dependencies": { + "mdn-data": "2.0.14", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/csso/node_modules/mdn-data": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", + "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==", + "dev": true + }, + "node_modules/csso/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/cssom": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz", + "integrity": "sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw==", + "dev": true + }, + "node_modules/cssstyle": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", + "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", + "dev": true, + "dependencies": { + "cssom": "~0.3.6" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cssstyle/node_modules/cssom": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", + "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", + "dev": true + }, + "node_modules/csstype": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", + "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==", + "dev": true + }, + "node_modules/d": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/d/-/d-1.0.1.tgz", + "integrity": "sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==", + "dev": true, + "dependencies": { + "es5-ext": "^0.10.50", + "type": "^1.0.1" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/damerau-levenshtein": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", + "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", + "dev": true + }, + "node_modules/data-urls": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-2.0.0.tgz", + "integrity": "sha512-X5eWTSXO/BJmpdIKCRuKUgSCgAN0OwliVK3yPKbwIWU1Tdw5BRajxlzMidvh+gwko9AfQ9zIj52pzF91Q3YAvQ==", + "dev": true, + "dependencies": { + "abab": "^2.0.3", + "whatwg-mimetype": "^2.3.0", + "whatwg-url": "^8.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz", + "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==", + "dev": true + }, + "node_modules/dedent": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", + "integrity": "sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==", + "dev": true + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/default-gateway": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-6.0.3.tgz", + "integrity": "sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg==", + "dev": true, + "dependencies": { + "execa": "^5.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/define-properties": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.0.tgz", + "integrity": "sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==", + "dev": true, + "dependencies": { + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "dev": true, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "dev": true + }, + "node_modules/detect-port-alt": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/detect-port-alt/-/detect-port-alt-1.1.6.tgz", + "integrity": "sha512-5tQykt+LqfJFBEYaDITx7S7cR7mJ/zQmLXZ2qt5w04ainYZw6tBf9dBunMjVeVOdYVRUzUOE4HkY5J7+uttb5Q==", + "dev": true, + "dependencies": { + "address": "^1.0.1", + "debug": "^2.6.0" + }, + "bin": { + "detect": "bin/detect-port", + "detect-port": "bin/detect-port" + }, + "engines": { + "node": ">= 4.2.1" + } + }, + "node_modules/detect-port-alt/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/detect-port-alt/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true + }, + "node_modules/diff-sequences": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-27.5.1.tgz", + "integrity": "sha512-k1gCAXAsNgLwEL+Y8Wvl+M6oEFj5bgazfZULpS5CneoPPXRaCCW7dm+q21Ky2VEE5X+VeRDBVg1Pcvvsr4TtNQ==", + "dev": true, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true + }, + "node_modules/dns-equal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz", + "integrity": "sha512-z+paD6YUQsk+AbGCEM4PrOXSss5gd66QfcVBFTKR/HpFL9jCqikS94HYwKww6fQyO7IxrIIyUu+g0Ka9tUS2Cg==", + "dev": true + }, + "node_modules/dns-packet": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.0.tgz", + "integrity": "sha512-rza3UH1LwdHh9qyPXp8lkwpjSNk/AMD3dPytUoRoqnypDUhY0xvbdmVhWOfxO68frEfV9BU8V12Ez7ZsHGZpCQ==", + "dev": true, + "dependencies": { + "@leichtgewicht/ip-codec": "^2.0.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dom-converter": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz", + "integrity": "sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==", + "dev": true, + "dependencies": { + "utila": "~0.4" + } + }, + "node_modules/dom-serializer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", + "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", + "dev": true, + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ] + }, + "node_modules/domexception": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/domexception/-/domexception-2.0.1.tgz", + "integrity": "sha512-yxJ2mFy/sibVQlu5qHjOkf9J3K6zgmCxgJ94u2EdvDOV09H+32LtRswEcUsmUWN72pVLOEnTSRaIVVzVQgS0dg==", + "dev": true, + "dependencies": { + "webidl-conversions": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/domexception/node_modules/webidl-conversions": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-5.0.0.tgz", + "integrity": "sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "dev": true, + "dependencies": { + "domelementtype": "^2.2.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", + "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "dev": true, + "dependencies": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dot-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", + "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", + "dev": true, + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/dotenv": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz", + "integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/dotenv-expand": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-5.1.0.tgz", + "integrity": "sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==", + "dev": true + }, + "node_modules/duplexer": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", + "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", + "dev": true + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true + }, + "node_modules/ejs": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.9.tgz", + "integrity": "sha512-rC+QVNMJWv+MtPgkt0y+0rVEIdbtxVADApW9JXrUVlzHetgcyczP/E7DJmWJ4fJCZF2cPcBk0laWO9ZHMG3DmQ==", + "dev": true, + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.4.461", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.461.tgz", + "integrity": "sha512-1JkvV2sgEGTDXjdsaQCeSwYYuhLRphRpc+g6EHTFELJXEiznLt3/0pZ9JuAOQ5p2rI3YxKTbivtvajirIfhrEQ==", + "dev": true + }, + "node_modules/elkjs": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/elkjs/-/elkjs-0.8.2.tgz", + "integrity": "sha512-L6uRgvZTH+4OF5NE/MBbzQx/WYpru1xCBE9respNj6qznEewGUIfhzmm7horWWxbNO2M0WckQypGctR8lH79xQ==" + }, + "node_modules/emittery": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.8.1.tgz", + "integrity": "sha512-uDfvUjVrfGJJhymx/kz6prltenw1u7WrCg1oa94zYY8xxVpLLUu045LAT0dhDZdXG58/EpPL/5kA180fQ/qudg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "node_modules/emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.15.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz", + "integrity": "sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "dev": true, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/error-stack-parser": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz", + "integrity": "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==", + "dev": true, + "dependencies": { + "stackframe": "^1.3.4" + } + }, + "node_modules/es-abstract": { + "version": "1.22.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.1.tgz", + "integrity": "sha512-ioRRcXMO6OFyRpyzV3kE1IIBd4WG5/kltnzdxSCqoP8CMGs/Li+M1uF5o7lOkZVFjDs+NLesthnF66Pg/0q0Lw==", + "dev": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.0", + "arraybuffer.prototype.slice": "^1.0.1", + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "es-set-tostringtag": "^2.0.1", + "es-to-primitive": "^1.2.1", + "function.prototype.name": "^1.1.5", + "get-intrinsic": "^1.2.1", + "get-symbol-description": "^1.0.0", + "globalthis": "^1.0.3", + "gopd": "^1.0.1", + "has": "^1.0.3", + "has-property-descriptors": "^1.0.0", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.5", + "is-array-buffer": "^3.0.2", + "is-callable": "^1.2.7", + "is-negative-zero": "^2.0.2", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "is-string": "^1.0.7", + "is-typed-array": "^1.1.10", + "is-weakref": "^1.0.2", + "object-inspect": "^1.12.3", + "object-keys": "^1.1.1", + "object.assign": "^4.1.4", + "regexp.prototype.flags": "^1.5.0", + "safe-array-concat": "^1.0.0", + "safe-regex-test": "^1.0.0", + "string.prototype.trim": "^1.2.7", + "string.prototype.trimend": "^1.0.6", + "string.prototype.trimstart": "^1.0.6", + "typed-array-buffer": "^1.0.0", + "typed-array-byte-length": "^1.0.0", + "typed-array-byte-offset": "^1.0.0", + "typed-array-length": "^1.0.4", + "unbox-primitive": "^1.0.2", + "which-typed-array": "^1.1.10" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-array-method-boxes-properly": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz", + "integrity": "sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA==", + "dev": true + }, + "node_modules/es-module-lexer": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.3.0.tgz", + "integrity": "sha512-vZK7T0N2CBmBOixhmjdqx2gWVbFZ4DXZ/NyRMZVlJXPa7CyFS+/a4QQsDGDQy9ZfEzxFuNEsMLeQJnKP2p5/JA==", + "dev": true + }, + "node_modules/es-set-tostringtag": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.1.tgz", + "integrity": "sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.1.3", + "has": "^1.0.3", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz", + "integrity": "sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w==", + "dev": true, + "dependencies": { + "has": "^1.0.3" + } + }, + "node_modules/es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "dependencies": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es5-ext": { + "version": "0.10.62", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.62.tgz", + "integrity": "sha512-BHLqn0klhEpnOKSrzn/Xsz2UIW8j+cGmo9JLzr8BiUapV8hPL9+FliFqjwr9ngW7jWdnxv6eO+/LqyhJVqgrjA==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "es6-iterator": "^2.0.3", + "es6-symbol": "^3.1.3", + "next-tick": "^1.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/es6-iterator": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", + "integrity": "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==", + "dev": true, + "dependencies": { + "d": "1", + "es5-ext": "^0.10.35", + "es6-symbol": "^3.1.1" + } + }, + "node_modules/es6-symbol": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.3.tgz", + "integrity": "sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==", + "dev": true, + "dependencies": { + "d": "^1.0.1", + "ext": "^1.1.2" + } + }, + "node_modules/es6-weak-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.3.tgz", + "integrity": "sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA==", + "dev": true, + "dependencies": { + "d": "1", + "es5-ext": "^0.10.46", + "es6-iterator": "^2.0.3", + "es6-symbol": "^3.1.1" + } + }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "dev": true + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "dev": true, + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/escodegen/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint": { + "version": "8.45.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.45.0.tgz", + "integrity": "sha512-pd8KSxiQpdYRfYa9Wufvdoct3ZPQQuVuU5O6scNgMuOMYuxvH0IGaYK0wUFjo4UYYQQCUndlXiMbnxopwvvTiw==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.4.0", + "@eslint/eslintrc": "^2.1.0", + "@eslint/js": "8.44.0", + "@humanwhocodes/config-array": "^0.11.10", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "ajv": "^6.10.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.0", + "eslint-visitor-keys": "^3.4.1", + "espree": "^9.6.0", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-react-app": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/eslint-config-react-app/-/eslint-config-react-app-7.0.1.tgz", + "integrity": "sha512-K6rNzvkIeHaTd8m/QEh1Zko0KI7BACWkkneSs6s9cKZC/J27X3eZR6Upt1jkmZ/4FK+XUOPPxMEN7+lbUXfSlA==", + "dev": true, + "dependencies": { + "@babel/core": "^7.16.0", + "@babel/eslint-parser": "^7.16.3", + "@rushstack/eslint-patch": "^1.1.0", + "@typescript-eslint/eslint-plugin": "^5.5.0", + "@typescript-eslint/parser": "^5.5.0", + "babel-preset-react-app": "^10.0.1", + "confusing-browser-globals": "^1.0.11", + "eslint-plugin-flowtype": "^8.0.3", + "eslint-plugin-import": "^2.25.3", + "eslint-plugin-jest": "^25.3.0", + "eslint-plugin-jsx-a11y": "^6.5.1", + "eslint-plugin-react": "^7.27.1", + "eslint-plugin-react-hooks": "^4.3.0", + "eslint-plugin-testing-library": "^5.0.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "eslint": "^8.0.0" + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.7", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.7.tgz", + "integrity": "sha512-gozW2blMLJCeFpBwugLTGyvVjNoeo1knonXAcatC6bjPBZitotxdWf7Gimr25N4c0AAOo4eOUfaG82IJPDpqCA==", + "dev": true, + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.11.0", + "resolve": "^1.22.1" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-module-utils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.8.0.tgz", + "integrity": "sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==", + "dev": true, + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-flowtype": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-flowtype/-/eslint-plugin-flowtype-8.0.3.tgz", + "integrity": "sha512-dX8l6qUL6O+fYPtpNRideCFSpmWOUVx5QcaGLVqe/vlDiBSe4vYljDWDETwnyFzpl7By/WVIu6rcrniCgH9BqQ==", + "dev": true, + "dependencies": { + "lodash": "^4.17.21", + "string-natural-compare": "^3.0.1" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "@babel/plugin-syntax-flow": "^7.14.5", + "@babel/plugin-transform-react-jsx": "^7.14.9", + "eslint": "^8.1.0" + } + }, + "node_modules/eslint-plugin-import": { + "version": "2.27.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.27.5.tgz", + "integrity": "sha512-LmEt3GVofgiGuiE+ORpnvP+kAm3h6MLZJ4Q5HCyHADofsb4VzXFsRiWj3c0OFiV+3DWFh0qg3v9gcPlfc3zRow==", + "dev": true, + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "array.prototype.flatmap": "^1.3.1", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.7", + "eslint-module-utils": "^2.7.4", + "has": "^1.0.3", + "is-core-module": "^2.11.0", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.values": "^1.1.6", + "resolve": "^1.22.1", + "semver": "^6.3.0", + "tsconfig-paths": "^3.14.1" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-import/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-plugin-jest": { + "version": "25.7.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-25.7.0.tgz", + "integrity": "sha512-PWLUEXeeF7C9QGKqvdSbzLOiLTx+bno7/HC9eefePfEb257QFHg7ye3dh80AZVkaa/RQsBB1Q/ORQvg2X7F0NQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/experimental-utils": "^5.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + }, + "peerDependencies": { + "@typescript-eslint/eslint-plugin": "^4.0.0 || ^5.0.0", + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@typescript-eslint/eslint-plugin": { + "optional": true + }, + "jest": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-jsx-a11y": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.7.1.tgz", + "integrity": "sha512-63Bog4iIethyo8smBklORknVjB0T2dwB8Mr/hIC+fBS0uyHdYYpzM/Ed+YC8VxTjlXHEWFOdmgwcDn1U2L9VCA==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.20.7", + "aria-query": "^5.1.3", + "array-includes": "^3.1.6", + "array.prototype.flatmap": "^1.3.1", + "ast-types-flow": "^0.0.7", + "axe-core": "^4.6.2", + "axobject-query": "^3.1.1", + "damerau-levenshtein": "^1.0.8", + "emoji-regex": "^9.2.2", + "has": "^1.0.3", + "jsx-ast-utils": "^3.3.3", + "language-tags": "=1.0.5", + "minimatch": "^3.1.2", + "object.entries": "^1.1.6", + "object.fromentries": "^2.0.6", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=4.0" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8" + } + }, + "node_modules/eslint-plugin-jsx-a11y/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-plugin-react": { + "version": "7.32.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.32.2.tgz", + "integrity": "sha512-t2fBMa+XzonrrNkyVirzKlvn5RXzzPwRHtMvLAtVZrt8oxgnTQaYbU6SXTOO1mwQgp1y5+toMSKInnzGr0Knqg==", + "dev": true, + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flatmap": "^1.3.1", + "array.prototype.tosorted": "^1.1.1", + "doctrine": "^2.1.0", + "estraverse": "^5.3.0", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.6", + "object.fromentries": "^2.0.6", + "object.hasown": "^1.1.2", + "object.values": "^1.1.6", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.4", + "semver": "^6.3.0", + "string.prototype.matchall": "^4.0.8" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8" + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz", + "integrity": "sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==", + "dev": true, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/eslint-plugin-react/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-react/node_modules/resolve": { + "version": "2.0.0-next.4", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.4.tgz", + "integrity": "sha512-iMDbmAWtfU+MHpxt/I5iWI7cY6YVEZUQ3MBgPQ++XD1PELuJHIl82xBmObyP2KyQmkNB2dsqF7seoQQiAn5yDQ==", + "dev": true, + "dependencies": { + "is-core-module": "^2.9.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/eslint-plugin-react/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-plugin-testing-library": { + "version": "5.11.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-testing-library/-/eslint-plugin-testing-library-5.11.0.tgz", + "integrity": "sha512-ELY7Gefo+61OfXKlQeXNIDVVLPcvKTeiQOoMZG9TeuWa7Ln4dUNRv8JdRWBQI9Mbb427XGlVB1aa1QPZxBJM8Q==", + "dev": true, + "dependencies": { + "@typescript-eslint/utils": "^5.58.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0", + "npm": ">=6" + }, + "peerDependencies": { + "eslint": "^7.5.0 || ^8.0.0" + } + }, + "node_modules/eslint-scope": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.1.tgz", + "integrity": "sha512-CvefSOsDdaYYvxChovdrPo/ZGt8d5lrJWleAc1diXRKhHGiTYEI26cvo8Kle/wGnsizoCJjK73FMg1/IkIwiNA==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.1.tgz", + "integrity": "sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-webpack-plugin": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/eslint-webpack-plugin/-/eslint-webpack-plugin-3.2.0.tgz", + "integrity": "sha512-avrKcGncpPbPSUHX6B3stNGzkKFto3eL+DKM4+VyMrVnhPc3vRczVlCq3uhuFOdRvDHTVXuzwk1ZKUrqDQHQ9w==", + "dev": true, + "dependencies": { + "@types/eslint": "^7.29.0 || ^8.4.1", + "jest-worker": "^28.0.2", + "micromatch": "^4.0.5", + "normalize-path": "^3.0.0", + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0", + "webpack": "^5.0.0" + } + }, + "node_modules/eslint-webpack-plugin/node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/eslint-webpack-plugin/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/eslint-webpack-plugin/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint-webpack-plugin/node_modules/jest-worker": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-28.1.3.tgz", + "integrity": "sha512-CqRA220YV/6jCo8VWvAt1KKx6eek1VIHMPeLEbpcfSfkEeWyBNppynM/o6q+Wmw+sOhos2ml34wZbSX3G13//g==", + "dev": true, + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/eslint-webpack-plugin/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "node_modules/eslint-webpack-plugin/node_modules/schema-utils": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz", + "integrity": "sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/eslint-webpack-plugin/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/eslint/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/eslint/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/eslint/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/eslint/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/eslint/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/eslint/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/globals": { + "version": "13.20.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz", + "integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/eslint/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz", + "integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==", + "dev": true + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/event-emitter": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", + "integrity": "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==", + "dev": true, + "dependencies": { + "d": "1", + "es5-ext": "~0.10.14" + } + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "dev": true + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/expect/-/expect-27.5.1.tgz", + "integrity": "sha512-E1q5hSUG2AmYQwQJ041nvgpkODHQvB+RKlB4IYdru6uJsyFTRyZAP463M+1lINorwbqAmUggi6+WwkD8lCS/Dw==", + "dev": true, + "dependencies": { + "@jest/types": "^27.5.1", + "jest-get-type": "^27.5.1", + "jest-matcher-utils": "^27.5.1", + "jest-message-util": "^27.5.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/express": { + "version": "4.18.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", + "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", + "dev": true, + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.1", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.5.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.2.0", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.11.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.18.0", + "serve-static": "1.15.0", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/express/node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "dev": true + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/ext": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz", + "integrity": "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==", + "dev": true, + "dependencies": { + "type": "^2.7.2" + } + }, + "node_modules/ext/node_modules/type": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/type/-/type-2.7.2.tgz", + "integrity": "sha512-dzlvlNlt6AXU7EBSfpAscydQ7gXB+pPGsPnfJnZpiNJBDj7IaJzQlBZYGdEi4R9HmPdBv2XmWJ6YUtoTa7lmCw==", + "dev": true + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-glob": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.0.tgz", + "integrity": "sha512-ChDuvbOypPuNjO8yIDf36x7BlZX1smcUMTTcyoIjycexOxd6DFsKsg21qVBzEmr3G7fUKIRy2/psii+CIUt7FA==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "node_modules/fastq": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", + "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "dev": true, + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/file-loader": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-6.2.0.tgz", + "integrity": "sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw==", + "dev": true, + "dependencies": { + "loader-utils": "^2.0.0", + "schema-utils": "^3.0.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" + } + }, + "node_modules/filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "dev": true, + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/filesize": { + "version": "8.0.7", + "resolved": "https://registry.npmjs.org/filesize/-/filesize-8.0.7.tgz", + "integrity": "sha512-pjmC+bkIF8XI7fWaH8KxHcZL3DPybs1roSKP4rKDvy20tAWwIObE4+JIseG2byfGKhud5ZnM4YSGKBz7Sh0ndQ==", + "dev": true, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "dev": true, + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/find-cache-dir": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", + "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", + "dev": true, + "dependencies": { + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/avajs/find-cache-dir?sponsor=1" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", + "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", + "dev": true, + "dependencies": { + "flatted": "^3.1.0", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz", + "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", + "dev": true + }, + "node_modules/follow-redirects": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", + "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/for-each": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", + "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "dev": true, + "dependencies": { + "is-callable": "^1.1.3" + } + }, + "node_modules/fork-ts-checker-webpack-plugin": { + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-6.5.3.tgz", + "integrity": "sha512-SbH/l9ikmMWycd5puHJKTkZJKddF4iRLyW3DeZ08HTI7NGyLS38MXd/KGgeWumQO7YNQbW2u/NtPT2YowbPaGQ==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.8.3", + "@types/json-schema": "^7.0.5", + "chalk": "^4.1.0", + "chokidar": "^3.4.2", + "cosmiconfig": "^6.0.0", + "deepmerge": "^4.2.2", + "fs-extra": "^9.0.0", + "glob": "^7.1.6", + "memfs": "^3.1.2", + "minimatch": "^3.0.4", + "schema-utils": "2.7.0", + "semver": "^7.3.2", + "tapable": "^1.0.0" + }, + "engines": { + "node": ">=10", + "yarn": ">=1.0.0" + }, + "peerDependencies": { + "eslint": ">= 6", + "typescript": ">= 2.7", + "vue-template-compiler": "*", + "webpack": ">= 4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + }, + "vue-template-compiler": { + "optional": true + } + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/cosmiconfig": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-6.0.0.tgz", + "integrity": "sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg==", + "dev": true, + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.1.0", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.7.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/schema-utils": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.0.tgz", + "integrity": "sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.4", + "ajv": "^6.12.2", + "ajv-keywords": "^3.4.1" + }, + "engines": { + "node": ">= 8.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/tapable": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz", + "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/form-data": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", + "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", + "dev": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fraction.js": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz", + "integrity": "sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==", + "dev": true, + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://www.patreon.com/infusion" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/fs-monkey": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.4.tgz", + "integrity": "sha512-INM/fWAxMICjttnD0DX1rBvinKskj5G1w+oy/pnm9u/tSlnBrzFonJMcalKJ30P8RRsPzKcCG7Q8l0jx5Fh9YQ==", + "dev": true + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "node_modules/function.prototype.name": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.5.tgz", + "integrity": "sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.0", + "functions-have-names": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", + "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-own-enumerable-property-symbols": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz", + "integrity": "sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==", + "dev": true + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-stdin": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-8.0.0.tgz", + "integrity": "sha512-sY22aA6xchAzprjyqmSEQv4UbAAzRN0L2dQB0NlN5acTTK9Don6nhoc3eAbUnpZiCANAMfd/+40kVdKfFygohg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-symbol-description": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", + "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob-promise": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/glob-promise/-/glob-promise-4.2.2.tgz", + "integrity": "sha512-xcUzJ8NWN5bktoTIX7eOclO1Npxd/dyVqUJxlLIDasT4C7KZyqlPIwkdJ0Ypiy3p2ZKahTjK4M9uC3sNSfNMzw==", + "dev": true, + "dependencies": { + "@types/glob": "^7.1.3" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/ahmadnassri" + }, + "peerDependencies": { + "glob": "^7.1.6" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true + }, + "node_modules/global-modules": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-2.0.0.tgz", + "integrity": "sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==", + "dev": true, + "dependencies": { + "global-prefix": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/global-prefix": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-3.0.0.tgz", + "integrity": "sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==", + "dev": true, + "dependencies": { + "ini": "^1.3.5", + "kind-of": "^6.0.2", + "which": "^1.3.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/global-prefix/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/globalthis": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz", + "integrity": "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==", + "dev": true, + "dependencies": { + "define-properties": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, + "node_modules/gzip-size": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz", + "integrity": "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==", + "dev": true, + "dependencies": { + "duplexer": "^0.1.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/handle-thing": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", + "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==", + "dev": true + }, + "node_modules/harmony-reflect": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/harmony-reflect/-/harmony-reflect-1.6.2.tgz", + "integrity": "sha512-HIp/n38R9kQjDEziXyDTuW3vvoxxyxjxFzXLrBr18uB47GnSt+G9D29fqrpM5ZkspMcPICud3XsBJQ4Y2URg8g==", + "dev": true + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-bigints": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", + "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", + "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", + "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "bin": { + "he": "bin/he" + } + }, + "node_modules/hoopy": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/hoopy/-/hoopy-0.1.4.tgz", + "integrity": "sha512-HRcs+2mr52W0K+x8RzcLzuPPmVIKMSv97RGHy0Ea9y/mpcaK+xTrjICA04KAHi4GRzxliNqNJEFYWHghy3rSfQ==", + "dev": true, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/hpack.js": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", + "integrity": "sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==", + "dev": true, + "dependencies": { + "inherits": "^2.0.1", + "obuf": "^1.0.0", + "readable-stream": "^2.0.1", + "wbuf": "^1.1.0" + } + }, + "node_modules/hpack.js/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true + }, + "node_modules/hpack.js/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/hpack.js/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "node_modules/hpack.js/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz", + "integrity": "sha512-D5JbOMBIR/TVZkubHT+OyT2705QvogUW4IBn6nHd756OwieSF9aDYFj4dv6HHEVGYbHaLETa3WggZYWWMyy3ZQ==", + "dev": true, + "dependencies": { + "whatwg-encoding": "^1.0.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/html-entities": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.4.0.tgz", + "integrity": "sha512-igBTJcNNNhvZFRtm8uA6xMY6xYleeDwn3PeBCkDz7tHttv4F2hsDI2aPgNERWzvRcNYHNT3ymRaQzllmXj4YsQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/mdevils" + }, + { + "type": "patreon", + "url": "https://patreon.com/mdevils" + } + ] + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, + "node_modules/html-minifier-terser": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", + "integrity": "sha512-YXxSlJBZTP7RS3tWnQw74ooKa6L9b9i9QYXY21eUEvhZ3u9XLfv6OnFsQq6RxkhHygsaUMvYsZRV5rU/OVNZxw==", + "dev": true, + "dependencies": { + "camel-case": "^4.1.2", + "clean-css": "^5.2.2", + "commander": "^8.3.0", + "he": "^1.2.0", + "param-case": "^3.0.4", + "relateurl": "^0.2.7", + "terser": "^5.10.0" + }, + "bin": { + "html-minifier-terser": "cli.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/html-webpack-plugin": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.5.3.tgz", + "integrity": "sha512-6YrDKTuqaP/TquFH7h4srYWsZx+x6k6+FbsTm0ziCwGHDP78Unr1r9F/H4+sGmMbX08GQcJ+K64x55b+7VM/jg==", + "dev": true, + "dependencies": { + "@types/html-minifier-terser": "^6.0.0", + "html-minifier-terser": "^6.0.2", + "lodash": "^4.17.21", + "pretty-error": "^4.0.0", + "tapable": "^2.0.0" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/html-webpack-plugin" + }, + "peerDependencies": { + "webpack": "^5.20.0" + } + }, + "node_modules/htmlparser2": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz", + "integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.0.0", + "domutils": "^2.5.2", + "entities": "^2.0.0" + } + }, + "node_modules/http-deceiver": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", + "integrity": "sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw==", + "dev": true + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dev": true, + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-parser-js": { + "version": "0.5.8", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.8.tgz", + "integrity": "sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q==", + "dev": true + }, + "node_modules/http-proxy": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "dev": true, + "dependencies": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/http-proxy-agent": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", + "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", + "dev": true, + "dependencies": { + "@tootallnate/once": "1", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/http-proxy-middleware": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz", + "integrity": "sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw==", + "dev": true, + "dependencies": { + "@types/http-proxy": "^1.17.8", + "http-proxy": "^1.18.1", + "is-glob": "^4.0.1", + "is-plain-obj": "^3.0.0", + "micromatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "@types/express": "^4.17.13" + }, + "peerDependenciesMeta": { + "@types/express": { + "optional": true + } + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/icss-utils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", + "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", + "dev": true, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/idb": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz", + "integrity": "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==", + "dev": true + }, + "node_modules/identity-obj-proxy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/identity-obj-proxy/-/identity-obj-proxy-3.0.0.tgz", + "integrity": "sha512-00n6YnVHKrinT9t0d9+5yZC6UBNJANpYEQvL2LlX6Ab9lnmxzIRcEmTPuyGScvl1+jKuCICX1Z0Ab1pPKKdikA==", + "dev": true, + "dependencies": { + "harmony-reflect": "^1.4.6" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/ignore": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", + "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/immer": { + "version": "9.0.21", + "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz", + "integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==", + "devOptional": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-fresh/node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/import-local": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", + "integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==", + "dev": true, + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true + }, + "node_modules/internal-slot": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.5.tgz", + "integrity": "sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.0", + "has": "^1.0.3", + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ipaddr.js": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.1.0.tgz", + "integrity": "sha512-LlbxQ7xKzfBusov6UMi4MFpEg0m+mAm9xyNGEduwXMEDuf4WfzB/RZwMVYEd7IKGvh4IUkEXYxtAVu9T3OelJQ==", + "dev": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", + "integrity": "sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.0", + "is-typed-array": "^1.1.10" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true + }, + "node_modules/is-bigint": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", + "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "dev": true, + "dependencies": { + "has-bigints": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-boolean-object": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.12.1.tgz", + "integrity": "sha512-Q4ZuBAe2FUsKtyQJoQHlvP8OvBERxO3jEmy1I7hcRXcJBGGHFh/aJBswbXuS9sgrDH2QUO8ilkwNPHvHMd8clg==", + "dev": true, + "dependencies": { + "has": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true, + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", + "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==", + "dev": true + }, + "node_modules/is-negative-zero": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", + "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", + "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", + "integrity": "sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-plain-obj": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", + "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true + }, + "node_modules/is-promise": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", + "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==", + "dev": true + }, + "node_modules/is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-regexp": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz", + "integrity": "sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-root": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-root/-/is-root-2.1.0.tgz", + "integrity": "sha512-AGOriNp96vNBd3HtU+RzFEc75FfR5ymiYv8E553I71SCeXBiMsVDUtdio1OEFvrPyLIQ9tVR5RxXIFe5PUFjMg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", + "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-string": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", + "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", + "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.10.tgz", + "integrity": "sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A==", + "dev": true, + "dependencies": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", + "dev": true + }, + "node_modules/is-weakref": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", + "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz", + "integrity": "sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", + "integrity": "sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==", + "dev": true, + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^3.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/istanbul-reports": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.5.tgz", + "integrity": "sha512-nUsEMa9pBt/NOHqbcbeJEgqIlY/K7rVWUX6Lql2orY5e9roQOthbR3vtY4zzf2orPELg80fnxxk9zUyPlgwD1w==", + "dev": true, + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jake": { + "version": "10.8.7", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.8.7.tgz", + "integrity": "sha512-ZDi3aP+fG/LchyBzUM804VjddnwfSfsdeYkwt8NcbKRvo4rFkjhs456iLFn3k2ZUWvNe4i48WACDbza8fhq2+w==", + "dev": true, + "dependencies": { + "async": "^3.2.3", + "chalk": "^4.0.2", + "filelist": "^1.0.4", + "minimatch": "^3.1.2" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jake/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jake/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jake/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jake/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/jake/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jake/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest/-/jest-27.5.1.tgz", + "integrity": "sha512-Yn0mADZB89zTtjkPJEXwrac3LHudkQMR+Paqa8uxJHCBr9agxztUifWCyiYrjhMPBoUVBjyny0I7XH6ozDr7QQ==", + "dev": true, + "dependencies": { + "@jest/core": "^27.5.1", + "import-local": "^3.0.2", + "jest-cli": "^27.5.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-27.5.1.tgz", + "integrity": "sha512-buBLMiByfWGCoMsLLzGUUSpAmIAGnbR2KJoMN10ziLhOLvP4e0SlypHnAel8iqQXTrcbmfEY9sSqae5sgUsTvw==", + "dev": true, + "dependencies": { + "@jest/types": "^27.5.1", + "execa": "^5.0.0", + "throat": "^6.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-circus": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-27.5.1.tgz", + "integrity": "sha512-D95R7x5UtlMA5iBYsOHFFbMD/GVA4R/Kdq15f7xYWUfWHBto9NYRsOvnSauTgdF+ogCpJ4tyKOXhUifxS65gdw==", + "dev": true, + "dependencies": { + "@jest/environment": "^27.5.1", + "@jest/test-result": "^27.5.1", + "@jest/types": "^27.5.1", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^0.7.0", + "expect": "^27.5.1", + "is-generator-fn": "^2.0.0", + "jest-each": "^27.5.1", + "jest-matcher-utils": "^27.5.1", + "jest-message-util": "^27.5.1", + "jest-runtime": "^27.5.1", + "jest-snapshot": "^27.5.1", + "jest-util": "^27.5.1", + "pretty-format": "^27.5.1", + "slash": "^3.0.0", + "stack-utils": "^2.0.3", + "throat": "^6.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-circus/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-circus/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-circus/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-circus/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/jest-circus/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-circus/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-cli": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-27.5.1.tgz", + "integrity": "sha512-Hc6HOOwYq4/74/c62dEE3r5elx8wjYqxY0r0G/nFrLDPMFRu6RA/u8qINOIkvhxG7mMQ5EJsOGfRpI8L6eFUVw==", + "dev": true, + "dependencies": { + "@jest/core": "^27.5.1", + "@jest/test-result": "^27.5.1", + "@jest/types": "^27.5.1", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "import-local": "^3.0.2", + "jest-config": "^27.5.1", + "jest-util": "^27.5.1", + "jest-validate": "^27.5.1", + "prompts": "^2.0.1", + "yargs": "^16.2.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-cli/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-cli/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-cli/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-cli/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/jest-cli/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-cli/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-config": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-27.5.1.tgz", + "integrity": "sha512-5sAsjm6tGdsVbW9ahcChPAFCk4IlkQUknH5AvKjuLTSlcO/wCZKyFdn7Rg0EkC+OGgWODEy2hDpWB1PgzH0JNA==", + "dev": true, + "dependencies": { + "@babel/core": "^7.8.0", + "@jest/test-sequencer": "^27.5.1", + "@jest/types": "^27.5.1", + "babel-jest": "^27.5.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.1", + "graceful-fs": "^4.2.9", + "jest-circus": "^27.5.1", + "jest-environment-jsdom": "^27.5.1", + "jest-environment-node": "^27.5.1", + "jest-get-type": "^27.5.1", + "jest-jasmine2": "^27.5.1", + "jest-regex-util": "^27.5.1", + "jest-resolve": "^27.5.1", + "jest-runner": "^27.5.1", + "jest-util": "^27.5.1", + "jest-validate": "^27.5.1", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^27.5.1", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + }, + "peerDependencies": { + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-config/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-config/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-config/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-config/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/jest-config/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-config/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-diff": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-27.5.1.tgz", + "integrity": "sha512-m0NvkX55LDt9T4mctTEgnZk3fmEg3NRYutvMPWM/0iPnkFj2wIeF45O1718cMSOFO1vINkqmxqD8vE37uTEbqw==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^27.5.1", + "jest-get-type": "^27.5.1", + "pretty-format": "^27.5.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-diff/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-diff/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-diff/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-diff/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/jest-diff/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-diff/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-docblock": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-27.5.1.tgz", + "integrity": "sha512-rl7hlABeTsRYxKiUfpHrQrG4e2obOiTQWfMEH3PxPjOtdsfLQO4ReWSZaQ7DETm4xu07rl4q/h4zcKXyU0/OzQ==", + "dev": true, + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-each": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-27.5.1.tgz", + "integrity": "sha512-1Ff6p+FbhT/bXQnEouYy00bkNSY7OUpfIcmdl8vZ31A1UUaurOLPA8a8BbJOF2RDUElwJhmeaV7LnagI+5UwNQ==", + "dev": true, + "dependencies": { + "@jest/types": "^27.5.1", + "chalk": "^4.0.0", + "jest-get-type": "^27.5.1", + "jest-util": "^27.5.1", + "pretty-format": "^27.5.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-each/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-each/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-each/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-each/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/jest-each/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-each/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-environment-jsdom": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-27.5.1.tgz", + "integrity": "sha512-TFBvkTC1Hnnnrka/fUb56atfDtJ9VMZ94JkjTbggl1PEpwrYtUBKMezB3inLmWqQsXYLcMwNoDQwoBTAvFfsfw==", + "dev": true, + "dependencies": { + "@jest/environment": "^27.5.1", + "@jest/fake-timers": "^27.5.1", + "@jest/types": "^27.5.1", + "@types/node": "*", + "jest-mock": "^27.5.1", + "jest-util": "^27.5.1", + "jsdom": "^16.6.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-27.5.1.tgz", + "integrity": "sha512-Jt4ZUnxdOsTGwSRAfKEnE6BcwsSPNOijjwifq5sDFSA2kesnXTvNqKHYgM0hDq3549Uf/KzdXNYn4wMZJPlFLw==", + "dev": true, + "dependencies": { + "@jest/environment": "^27.5.1", + "@jest/fake-timers": "^27.5.1", + "@jest/types": "^27.5.1", + "@types/node": "*", + "jest-mock": "^27.5.1", + "jest-util": "^27.5.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-27.5.1.tgz", + "integrity": "sha512-2KY95ksYSaK7DMBWQn6dQz3kqAf3BB64y2udeG+hv4KfSOb9qwcYQstTJc1KCbsix+wLZWZYN8t7nwX3GOBLRw==", + "dev": true, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-27.5.1.tgz", + "integrity": "sha512-7GgkZ4Fw4NFbMSDSpZwXeBiIbx+t/46nJ2QitkOjvwPYyZmqttu2TDSimMHP1EkPOi4xUZAN1doE5Vd25H4Jng==", + "dev": true, + "dependencies": { + "@jest/types": "^27.5.1", + "@types/graceful-fs": "^4.1.2", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^27.5.1", + "jest-serializer": "^27.5.1", + "jest-util": "^27.5.1", + "jest-worker": "^27.5.1", + "micromatch": "^4.0.4", + "walker": "^1.0.7" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-jasmine2": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-jasmine2/-/jest-jasmine2-27.5.1.tgz", + "integrity": "sha512-jtq7VVyG8SqAorDpApwiJJImd0V2wv1xzdheGHRGyuT7gZm6gG47QEskOlzsN1PG/6WNaCo5pmwMHDf3AkG2pQ==", + "dev": true, + "dependencies": { + "@jest/environment": "^27.5.1", + "@jest/source-map": "^27.5.1", + "@jest/test-result": "^27.5.1", + "@jest/types": "^27.5.1", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "expect": "^27.5.1", + "is-generator-fn": "^2.0.0", + "jest-each": "^27.5.1", + "jest-matcher-utils": "^27.5.1", + "jest-message-util": "^27.5.1", + "jest-runtime": "^27.5.1", + "jest-snapshot": "^27.5.1", + "jest-util": "^27.5.1", + "pretty-format": "^27.5.1", + "throat": "^6.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-jasmine2/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-jasmine2/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-jasmine2/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-jasmine2/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/jest-jasmine2/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-jasmine2/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-leak-detector": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-27.5.1.tgz", + "integrity": "sha512-POXfWAMvfU6WMUXftV4HolnJfnPOGEu10fscNCA76KBpRRhcMN2c8d3iT2pxQS3HLbA+5X4sOUPzYO2NUyIlHQ==", + "dev": true, + "dependencies": { + "jest-get-type": "^27.5.1", + "pretty-format": "^27.5.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-27.5.1.tgz", + "integrity": "sha512-z2uTx/T6LBaCoNWNFWwChLBKYxTMcGBRjAt+2SbP929/Fflb9aa5LGma654Rz8z9HLxsrUaYzxE9T/EFIL/PAw==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^27.5.1", + "jest-get-type": "^27.5.1", + "pretty-format": "^27.5.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-matcher-utils/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-matcher-utils/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-matcher-utils/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-matcher-utils/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/jest-matcher-utils/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-matcher-utils/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-message-util": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-27.5.1.tgz", + "integrity": "sha512-rMyFe1+jnyAAf+NHwTclDz0eAaLkVDdKVHHBFWsBWHnnh5YeJMNWWsv7AbFYXfK3oTqvL7VTWkhNLu1jX24D+g==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^27.5.1", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^27.5.1", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-message-util/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-message-util/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-message-util/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-message-util/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/jest-message-util/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-message-util/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-mock": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-27.5.1.tgz", + "integrity": "sha512-K4jKbY1d4ENhbrG2zuPWaQBvDly+iZ2yAW+T1fATN78hc0sInwn7wZB8XtlNnvHug5RMwV897Xm4LqmPM4e2Og==", + "dev": true, + "dependencies": { + "@jest/types": "^27.5.1", + "@types/node": "*" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-27.5.1.tgz", + "integrity": "sha512-4bfKq2zie+x16okqDXjXn9ql2B0dScQu+vcwe4TvFVhkVyuWLqpZrZtXxLLWoXYgn0E87I6r6GRYHF7wFZBUvg==", + "dev": true, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-27.5.1.tgz", + "integrity": "sha512-FFDy8/9E6CV83IMbDpcjOhumAQPDyETnU2KZ1O98DwTnz8AOBsW/Xv3GySr1mOZdItLR+zDZ7I/UdTFbgSOVCw==", + "dev": true, + "dependencies": { + "@jest/types": "^27.5.1", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^27.5.1", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^27.5.1", + "jest-validate": "^27.5.1", + "resolve": "^1.20.0", + "resolve.exports": "^1.1.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-27.5.1.tgz", + "integrity": "sha512-QQOOdY4PE39iawDn5rzbIePNigfe5B9Z91GDD1ae/xNDlu9kaat8QQ5EKnNmVWPV54hUdxCVwwj6YMgR2O7IOg==", + "dev": true, + "dependencies": { + "@jest/types": "^27.5.1", + "jest-regex-util": "^27.5.1", + "jest-snapshot": "^27.5.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-resolve/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-resolve/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-resolve/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-resolve/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/jest-resolve/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-resolve/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-runner": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-27.5.1.tgz", + "integrity": "sha512-g4NPsM4mFCOwFKXO4p/H/kWGdJp9V8kURY2lX8Me2drgXqG7rrZAx5kv+5H7wtt/cdFIjhqYx1HrlqWHaOvDaQ==", + "dev": true, + "dependencies": { + "@jest/console": "^27.5.1", + "@jest/environment": "^27.5.1", + "@jest/test-result": "^27.5.1", + "@jest/transform": "^27.5.1", + "@jest/types": "^27.5.1", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.8.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^27.5.1", + "jest-environment-jsdom": "^27.5.1", + "jest-environment-node": "^27.5.1", + "jest-haste-map": "^27.5.1", + "jest-leak-detector": "^27.5.1", + "jest-message-util": "^27.5.1", + "jest-resolve": "^27.5.1", + "jest-runtime": "^27.5.1", + "jest-util": "^27.5.1", + "jest-worker": "^27.5.1", + "source-map-support": "^0.5.6", + "throat": "^6.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-runner/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-runner/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-runner/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-runner/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/jest-runner/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-runner/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-runtime": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-27.5.1.tgz", + "integrity": "sha512-o7gxw3Gf+H2IGt8fv0RiyE1+r83FJBRruoA+FXrlHw6xEyBsU8ugA6IPfTdVyA0w8HClpbK+DGJxH59UrNMx8A==", + "dev": true, + "dependencies": { + "@jest/environment": "^27.5.1", + "@jest/fake-timers": "^27.5.1", + "@jest/globals": "^27.5.1", + "@jest/source-map": "^27.5.1", + "@jest/test-result": "^27.5.1", + "@jest/transform": "^27.5.1", + "@jest/types": "^27.5.1", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "execa": "^5.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^27.5.1", + "jest-message-util": "^27.5.1", + "jest-mock": "^27.5.1", + "jest-regex-util": "^27.5.1", + "jest-resolve": "^27.5.1", + "jest-snapshot": "^27.5.1", + "jest-util": "^27.5.1", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-runtime/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-runtime/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-runtime/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-runtime/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/jest-runtime/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-runtime/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-serializer": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-serializer/-/jest-serializer-27.5.1.tgz", + "integrity": "sha512-jZCyo6iIxO1aqUxpuBlwTDMkzOAJS4a3eYz3YzgxxVQFwLeSA7Jfq5cbqCY+JLvTDrWirgusI/0KwxKMgrdf7w==", + "dev": true, + "dependencies": { + "@types/node": "*", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-snapshot": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-27.5.1.tgz", + "integrity": "sha512-yYykXI5a0I31xX67mgeLw1DZ0bJB+gpq5IpSuCAoyDi0+BhgU/RIrL+RTzDmkNTchvDFWKP8lp+w/42Z3us5sA==", + "dev": true, + "dependencies": { + "@babel/core": "^7.7.2", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/traverse": "^7.7.2", + "@babel/types": "^7.0.0", + "@jest/transform": "^27.5.1", + "@jest/types": "^27.5.1", + "@types/babel__traverse": "^7.0.4", + "@types/prettier": "^2.1.5", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^27.5.1", + "graceful-fs": "^4.2.9", + "jest-diff": "^27.5.1", + "jest-get-type": "^27.5.1", + "jest-haste-map": "^27.5.1", + "jest-matcher-utils": "^27.5.1", + "jest-message-util": "^27.5.1", + "jest-util": "^27.5.1", + "natural-compare": "^1.4.0", + "pretty-format": "^27.5.1", + "semver": "^7.3.2" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-snapshot/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-snapshot/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/jest-snapshot/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-snapshot/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-util": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-27.5.1.tgz", + "integrity": "sha512-Kv2o/8jNvX1MQ0KGtw480E/w4fBCDOnH6+6DmeKi6LZUIlKA5kwY0YNdlzaWTiVgxqAqik11QyxDOKk543aKXw==", + "dev": true, + "dependencies": { + "@jest/types": "^27.5.1", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-util/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-util/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-util/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-util/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/jest-util/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-util/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-validate": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-27.5.1.tgz", + "integrity": "sha512-thkNli0LYTmOI1tDB3FI1S1RTp/Bqyd9pTarJwL87OIBFuqEb5Apv5EaApEudYg4g86e3CT6kM0RowkhtEnCBQ==", + "dev": true, + "dependencies": { + "@jest/types": "^27.5.1", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^27.5.1", + "leven": "^3.1.0", + "pretty-format": "^27.5.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-validate/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-validate/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-validate/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-validate/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/jest-validate/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-validate/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-watch-typeahead": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jest-watch-typeahead/-/jest-watch-typeahead-1.1.0.tgz", + "integrity": "sha512-Va5nLSJTN7YFtC2jd+7wsoe1pNe5K4ShLux/E5iHEwlB9AxaxmggY7to9KUqKojhaJw3aXqt5WAb4jGPOolpEw==", + "dev": true, + "dependencies": { + "ansi-escapes": "^4.3.1", + "chalk": "^4.0.0", + "jest-regex-util": "^28.0.0", + "jest-watcher": "^28.0.0", + "slash": "^4.0.0", + "string-length": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "jest": "^27.0.0 || ^28.0.0" + } + }, + "node_modules/jest-watch-typeahead/node_modules/@jest/console": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-28.1.3.tgz", + "integrity": "sha512-QPAkP5EwKdK/bxIr6C1I4Vs0rm2nHiANzj/Z5X2JQkrZo6IqvC4ldZ9K95tF0HdidhA8Bo6egxSzUFPYKcEXLw==", + "dev": true, + "dependencies": { + "@jest/types": "^28.1.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^28.1.3", + "jest-util": "^28.1.3", + "slash": "^3.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/jest-watch-typeahead/node_modules/@jest/console/node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-watch-typeahead/node_modules/@jest/test-result": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-28.1.3.tgz", + "integrity": "sha512-kZAkxnSE+FqE8YjW8gNuoVkkC9I7S1qmenl8sGcDOLropASP+BkcGKwhXoyqQuGOGeYY0y/ixjrd/iERpEXHNg==", + "dev": true, + "dependencies": { + "@jest/console": "^28.1.3", + "@jest/types": "^28.1.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/jest-watch-typeahead/node_modules/@jest/types": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-28.1.3.tgz", + "integrity": "sha512-RyjiyMUZrKz/c+zlMFO1pm70DcIlST8AeWTkoUdZevew44wcNZQHsEVOiCVtgVnlFFD82FPaXycys58cf2muVQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^28.1.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/jest-watch-typeahead/node_modules/@types/yargs": { + "version": "17.0.24", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.24.tgz", + "integrity": "sha512-6i0aC7jV6QzQB8ne1joVZ0eSFIstHsCrobmOtghM11yGlH0j43FKL2UhWdELkyps0zuf7qVTUVCCR+tgSlyLLw==", + "dev": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/jest-watch-typeahead/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-watch-typeahead/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-watch-typeahead/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-watch-typeahead/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/jest-watch-typeahead/node_modules/emittery": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.10.2.tgz", + "integrity": "sha512-aITqOwnLanpHLNXZJENbOgjUBeHocD+xsSJmNrjovKBW5HbSpW3d1pEls7GFQPUWXiwG9+0P4GtHfEqC/4M0Iw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/jest-watch-typeahead/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-watch-typeahead/node_modules/jest-message-util": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-28.1.3.tgz", + "integrity": "sha512-PFdn9Iewbt575zKPf1286Ht9EPoJmYT7P0kY+RibeYZ2XtOr53pDLEFoTWXbd1h4JiGiWpTBC84fc8xMXQMb7g==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^28.1.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^28.1.3", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/jest-watch-typeahead/node_modules/jest-message-util/node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-watch-typeahead/node_modules/jest-regex-util": { + "version": "28.0.2", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-28.0.2.tgz", + "integrity": "sha512-4s0IgyNIy0y9FK+cjoVYoxamT7Zeo7MhzqRGx7YDYmaQn1wucY9rotiGkBzzcMXTtjrCAP/f7f+E0F7+fxPNdw==", + "dev": true, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/jest-watch-typeahead/node_modules/jest-util": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-28.1.3.tgz", + "integrity": "sha512-XdqfpHwpcSRko/C35uLYFM2emRAltIIKZiJ9eAmhjsj0CqZMa0p1ib0R5fWIqGhn1a103DebTbpqIaP1qCQ6tQ==", + "dev": true, + "dependencies": { + "@jest/types": "^28.1.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/jest-watch-typeahead/node_modules/jest-watcher": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-28.1.3.tgz", + "integrity": "sha512-t4qcqj9hze+jviFPUN3YAtAEeFnr/azITXQEMARf5cMwKY2SMBRnCQTXLixTl20OR6mLh9KLMrgVJgJISym+1g==", + "dev": true, + "dependencies": { + "@jest/test-result": "^28.1.3", + "@jest/types": "^28.1.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.10.2", + "jest-util": "^28.1.3", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/jest-watch-typeahead/node_modules/jest-watcher/node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-watch-typeahead/node_modules/jest-watcher/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-watch-typeahead/node_modules/pretty-format": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-28.1.3.tgz", + "integrity": "sha512-8gFb/To0OmxHR9+ZTb14Df2vNxdGCX8g1xWGUTqUw5TiZvcQf5sHKObd5UcPyLLyowNwDAMTF3XWOG1B6mxl1Q==", + "dev": true, + "dependencies": { + "@jest/schemas": "^28.1.3", + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/jest-watch-typeahead/node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-watch-typeahead/node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true + }, + "node_modules/jest-watch-typeahead/node_modules/slash": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", + "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watch-typeahead/node_modules/string-length": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-5.0.1.tgz", + "integrity": "sha512-9Ep08KAMUn0OadnVaBuRdE2l615CQ508kr0XMadjClfYpdCyvrbFp6Taebo8yyxokQ4viUd/xPPUA4FGgUa0ow==", + "dev": true, + "dependencies": { + "char-regex": "^2.0.0", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watch-typeahead/node_modules/string-length/node_modules/char-regex": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-2.0.1.tgz", + "integrity": "sha512-oSvEeo6ZUD7NepqAat3RqoucZ5SeqLJgOvVIwkafu6IP3V0pO38s/ypdVUmDDK6qIIHNlYHJAKX9E7R7HoKElw==", + "dev": true, + "engines": { + "node": ">=12.20" + } + }, + "node_modules/jest-watch-typeahead/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/jest-watch-typeahead/node_modules/strip-ansi/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/jest-watch-typeahead/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-watcher": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-27.5.1.tgz", + "integrity": "sha512-z676SuD6Z8o8qbmEGhoEUFOM1+jfEiL3DXHK/xgEiG2EyNYfFG60jluWcupY6dATjfEsKQuibReS1djInQnoVw==", + "dev": true, + "dependencies": { + "@jest/test-result": "^27.5.1", + "@jest/types": "^27.5.1", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "jest-util": "^27.5.1", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-watcher/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-watcher/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-watcher/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-watcher/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/jest-watcher/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-watcher/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/jest-worker/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/jiti": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.19.1.tgz", + "integrity": "sha512-oVhqoRDaBXf7sjkll95LHVS6Myyyb1zaunVwk4Z0+WPSW4gjS0pl01zYKHScTuyEhQsFxV5L4DR5r+YqSyqyyg==", + "dev": true, + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsdom": { + "version": "16.7.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-16.7.0.tgz", + "integrity": "sha512-u9Smc2G1USStM+s/x1ru5Sxrl6mPYCbByG1U/hUmqaVsm4tbNyS7CicOSRyuGQYZhTu0h84qkZZQ/I+dzizSVw==", + "dev": true, + "dependencies": { + "abab": "^2.0.5", + "acorn": "^8.2.4", + "acorn-globals": "^6.0.0", + "cssom": "^0.4.4", + "cssstyle": "^2.3.0", + "data-urls": "^2.0.0", + "decimal.js": "^10.2.1", + "domexception": "^2.0.1", + "escodegen": "^2.0.0", + "form-data": "^3.0.0", + "html-encoding-sniffer": "^2.0.1", + "http-proxy-agent": "^4.0.1", + "https-proxy-agent": "^5.0.0", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.0", + "parse5": "6.0.1", + "saxes": "^5.0.1", + "symbol-tree": "^3.2.4", + "tough-cookie": "^4.0.0", + "w3c-hr-time": "^1.0.2", + "w3c-xmlserializer": "^2.0.0", + "webidl-conversions": "^6.1.0", + "whatwg-encoding": "^1.0.5", + "whatwg-mimetype": "^2.3.0", + "whatwg-url": "^8.5.0", + "ws": "^7.4.6", + "xml-name-validator": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "canvas": "^2.5.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "node_modules/json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", + "dev": true + }, + "node_modules/json-schema-to-typescript": { + "version": "13.0.2", + "resolved": "https://registry.npmjs.org/json-schema-to-typescript/-/json-schema-to-typescript-13.0.2.tgz", + "integrity": "sha512-TCaEVW4aI2FmMQe7f98mvr3/oiVmXEC1xZjkTZ9L/BSoTXFlC7p64mD5AD2d8XWycNBQZUnHwXL5iVXt1HWwNQ==", + "dev": true, + "dependencies": { + "@bcherny/json-schema-ref-parser": "10.0.5-fork", + "@types/json-schema": "^7.0.11", + "@types/lodash": "^4.14.182", + "@types/prettier": "^2.6.1", + "cli-color": "^2.0.2", + "get-stdin": "^8.0.0", + "glob": "^7.1.6", + "glob-promise": "^4.2.2", + "is-glob": "^4.0.3", + "lodash": "^4.17.21", + "minimist": "^1.2.6", + "mkdirp": "^1.0.4", + "mz": "^2.7.0", + "prettier": "^2.6.2" + }, + "bin": { + "json2ts": "dist/src/cli.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/json-schema-to-typescript/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonpointer": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.1.tgz", + "integrity": "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/jsx-ast-utils": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.4.tgz", + "integrity": "sha512-fX2TVdCViod6HwKEtSWGHs57oFhVfCMwieb9PuRDgjDPh5XeqJiHFFFJCHxU5cnTc3Bu/GRL+kPiFmw8XWOfKw==", + "dev": true, + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/klona": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.6.tgz", + "integrity": "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/language-subtag-registry": { + "version": "0.3.22", + "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.22.tgz", + "integrity": "sha512-tN0MCzyWnoz/4nHS6uxdlFWoUZT7ABptwKPQ52Ea7URk6vll88bWBVhodtnlfEuCcKWNGoc+uGbw1cwa9IKh/w==", + "dev": true + }, + "node_modules/language-tags": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.5.tgz", + "integrity": "sha512-qJhlO9cGXi6hBGKoxEG/sKZDAHD5Hnu9Hs4WbOY3pCWXDhw0N8x1NenNzm2EnNLkLkk7J2SdxAkDSbb6ftT+UQ==", + "dev": true, + "dependencies": { + "language-subtag-registry": "~0.3.2" + } + }, + "node_modules/launch-editor": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.6.0.tgz", + "integrity": "sha512-JpDCcQnyAAzZZaZ7vEiSqL690w7dAEyLao+KC96zBplnYbJS7TYNjvM3M7y3dGz+v7aIsJk3hllWuc0kWAjyRQ==", + "dev": true, + "dependencies": { + "picocolors": "^1.0.0", + "shell-quote": "^1.7.3" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lilconfig": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", + "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, + "node_modules/loader-runner": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", + "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", + "dev": true, + "engines": { + "node": ">=6.11.5" + } + }, + "node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "dev": true, + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "dev": true + }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "node_modules/lodash.sortby": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", + "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==", + "dev": true + }, + "node_modules/lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==", + "dev": true + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lower-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", + "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", + "dev": true, + "dependencies": { + "tslib": "^2.0.3" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lru-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/lru-queue/-/lru-queue-0.1.0.tgz", + "integrity": "sha512-BpdYkt9EvGl8OfWHDQPISVpcl5xZthb+XPsbELj5AQXxIC8IriDZIQYjBJPEm5rS420sjZ0TLEzRcq5KdBhYrQ==", + "dev": true, + "dependencies": { + "es5-ext": "~0.10.2" + } + }, + "node_modules/magic-string": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz", + "integrity": "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==", + "dev": true, + "dependencies": { + "sourcemap-codec": "^1.4.8" + } + }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/mdn-data": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.4.tgz", + "integrity": "sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA==", + "dev": true + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/memfs": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.5.3.tgz", + "integrity": "sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==", + "dev": true, + "dependencies": { + "fs-monkey": "^1.0.4" + }, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/memoizee": { + "version": "0.4.15", + "resolved": "https://registry.npmjs.org/memoizee/-/memoizee-0.4.15.tgz", + "integrity": "sha512-UBWmJpLZd5STPm7PMUlOw/TSy972M+z8gcyQ5veOnSDRREz/0bmpyTfKt3/51DhEBqCZQn1udM/5flcSPYhkdQ==", + "dev": true, + "dependencies": { + "d": "^1.0.1", + "es5-ext": "^0.10.53", + "es6-weak-map": "^2.0.3", + "event-emitter": "^0.3.5", + "is-promise": "^2.2.2", + "lru-queue": "^0.1.0", + "next-tick": "^1.1.0", + "timers-ext": "^0.1.7" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==", + "dev": true + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/mini-css-extract-plugin": { + "version": "2.7.6", + "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.7.6.tgz", + "integrity": "sha512-Qk7HcgaPkGG6eD77mLvZS1nmxlao3j+9PkrT9Uc7HAE1id3F41+DdBRYRYkbyfNRGzm8/YWtzhw7nVPmwhqTQw==", + "dev": true, + "dependencies": { + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, + "node_modules/mini-css-extract-plugin/node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/mini-css-extract-plugin/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/mini-css-extract-plugin/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "node_modules/mini-css-extract-plugin/node_modules/schema-utils": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz", + "integrity": "sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "dev": true + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/multicast-dns": { + "version": "7.2.5", + "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz", + "integrity": "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==", + "dev": true, + "dependencies": { + "dns-packet": "^5.2.2", + "thunky": "^1.0.2" + }, + "bin": { + "multicast-dns": "cli.js" + } + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", + "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node_modules/natural-compare-lite": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz", + "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==", + "dev": true + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true + }, + "node_modules/next-tick": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", + "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==", + "dev": true + }, + "node_modules/no-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", + "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", + "dev": true, + "dependencies": { + "lower-case": "^2.0.2", + "tslib": "^2.0.3" + } + }, + "node_modules/node-forge": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", + "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", + "dev": true, + "engines": { + "node": ">= 6.13.0" + } + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true + }, + "node_modules/node-releases": { + "version": "2.0.13", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz", + "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==", + "dev": true + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-url": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/nwsapi": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.7.tgz", + "integrity": "sha512-ub5E4+FBPKwAZx0UwIQOjYWGHTEq5sPqHQNRN8Z9e4A7u3Tj1weLJsL59yH9vmvqEtBHaOmT6cYQKIZOxp35FQ==", + "dev": true + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/object-inspect": { + "version": "1.12.3", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", + "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", + "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "has-symbols": "^1.0.3", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.entries": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.6.tgz", + "integrity": "sha512-leTPzo4Zvg3pmbQ3rDK69Rl8GQvIqMWubrkxONG9/ojtFE2rD9fjMKfSI5BxW3osRH1m6VdzmqK8oAY9aT4x5w==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.6.tgz", + "integrity": "sha512-VciD13dswC4j1Xt5394WR4MzmAQmlgN72phd/riNp9vtD7tp4QQWJ0R4wvclXcafgcYK8veHRed2W6XeGBvcfg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.getownpropertydescriptors": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.6.tgz", + "integrity": "sha512-lq+61g26E/BgHv0ZTFgRvi7NMEPuAxLkFU7rukXjc/AlwH4Am5xXVnIXy3un1bg/JPbXHrixRkK1itUzzPiIjQ==", + "dev": true, + "dependencies": { + "array.prototype.reduce": "^1.0.5", + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.21.2", + "safe-array-concat": "^1.0.0" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.hasown": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/object.hasown/-/object.hasown-1.1.2.tgz", + "integrity": "sha512-B5UIT3J1W+WuWIU55h0mjlwaqxiE5vYENJXIXZ4VFe05pNYrkKuK0U/6aFcb0pKywYJh7IhfoqUfKVmrJJHZHw==", + "dev": true, + "dependencies": { + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.values": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.6.tgz", + "integrity": "sha512-FVVTkD1vENCsAcwNs9k6jea2uHC/X0+JcjG8YA60FN5CMaJmG95wT9jek/xX9nornqGRrBkKtzuAu2wuHpKqvw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/obuf": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", + "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", + "dev": true + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "dev": true, + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", + "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", + "dev": true, + "dependencies": { + "@aashutoshrathi/word-wrap": "^1.2.3", + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-retry": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", + "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", + "dev": true, + "dependencies": { + "@types/retry": "0.12.0", + "retry": "^0.13.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/param-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", + "integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==", + "dev": true, + "dependencies": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse5": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", + "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", + "dev": true + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/pascal-case": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", + "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", + "dev": true, + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==", + "dev": true + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", + "dev": true + }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", + "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-up": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/pkg-up/-/pkg-up-3.1.0.tgz", + "integrity": "sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==", + "dev": true, + "dependencies": { + "find-up": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-up/node_modules/find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "dependencies": { + "locate-path": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/pkg-up/node_modules/locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "dependencies": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/pkg-up/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-up/node_modules/p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "dependencies": { + "p-limit": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/pkg-up/node_modules/path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss": { + "version": "8.4.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.26.tgz", + "integrity": "sha512-jrXHFF8iTloAenySjM/ob3gSj7pCu0Ji49hnjqzsgSRa50hkWCKD0HQ+gMNJkW38jBI68MpAAg7ZWwHwX8NMMw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-attribute-case-insensitive": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/postcss-attribute-case-insensitive/-/postcss-attribute-case-insensitive-5.0.2.tgz", + "integrity": "sha512-XIidXV8fDr0kKt28vqki84fRK8VW8eTuIa4PChv2MqKuT6C9UjmSKzen6KaWhWEoYvwxFCa7n/tC1SZ3tyq4SQ==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.10" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/postcss-browser-comments": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-browser-comments/-/postcss-browser-comments-4.0.0.tgz", + "integrity": "sha512-X9X9/WN3KIvY9+hNERUqX9gncsgBA25XaeR+jshHz2j8+sYyHktHw1JdKuMjeLpGktXidqDhA7b/qm1mrBDmgg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "peerDependencies": { + "browserslist": ">=4", + "postcss": ">=8" + } + }, + "node_modules/postcss-calc": { + "version": "8.2.4", + "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-8.2.4.tgz", + "integrity": "sha512-SmWMSJmB8MRnnULldx0lQIyhSNvuDl9HfrZkaqqE/WHAhToYsAvDq+yAsA/kIyINDszOp3Rh0GFoNuH5Ypsm3Q==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.9", + "postcss-value-parser": "^4.2.0" + }, + "peerDependencies": { + "postcss": "^8.2.2" + } + }, + "node_modules/postcss-clamp": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-clamp/-/postcss-clamp-4.1.0.tgz", + "integrity": "sha512-ry4b1Llo/9zz+PKC+030KUnPITTJAHeOwjfAyyB60eT0AorGLdzp52s31OsPRHRf8NchkgFoG2y6fCfn1IV1Ow==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=7.6.0" + }, + "peerDependencies": { + "postcss": "^8.4.6" + } + }, + "node_modules/postcss-color-functional-notation": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/postcss-color-functional-notation/-/postcss-color-functional-notation-4.2.4.tgz", + "integrity": "sha512-2yrTAUZUab9s6CpxkxC4rVgFEVaR6/2Pipvi6qcgvnYiVqZcbDHEoBDhrXzyb7Efh2CCfHQNtcqWcIruDTIUeg==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/postcss-color-hex-alpha": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/postcss-color-hex-alpha/-/postcss-color-hex-alpha-8.0.4.tgz", + "integrity": "sha512-nLo2DCRC9eE4w2JmuKgVA3fGL3d01kGq752pVALF68qpGLmx2Qrk91QTKkdUqqp45T1K1XV8IhQpcu1hoAQflQ==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-color-rebeccapurple": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-color-rebeccapurple/-/postcss-color-rebeccapurple-7.1.1.tgz", + "integrity": "sha512-pGxkuVEInwLHgkNxUc4sdg4g3py7zUeCQ9sMfwyHAT+Ezk8a4OaaVZ8lIY5+oNqA/BXXgLyXv0+5wHP68R79hg==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/postcss-colormin": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-5.3.1.tgz", + "integrity": "sha512-UsWQG0AqTFQmpBegeLLc1+c3jIqBNB0zlDGRWR+dQ3pRKJL1oeMzyqmH3o2PIfn9MBdNrVPWhDbT769LxCTLJQ==", + "dev": true, + "dependencies": { + "browserslist": "^4.21.4", + "caniuse-api": "^3.0.0", + "colord": "^2.9.1", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-convert-values": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-5.1.3.tgz", + "integrity": "sha512-82pC1xkJZtcJEfiLw6UXnXVXScgtBrjlO5CBmuDQc+dlb88ZYheFsjTn40+zBVi3DkfF7iezO0nJUPLcJK3pvA==", + "dev": true, + "dependencies": { + "browserslist": "^4.21.4", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-custom-media": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/postcss-custom-media/-/postcss-custom-media-8.0.2.tgz", + "integrity": "sha512-7yi25vDAoHAkbhAzX9dHx2yc6ntS4jQvejrNcC+csQJAXjj15e7VcWfMgLqBNAbOvqi5uIa9huOVwdHbf+sKqg==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.3" + } + }, + "node_modules/postcss-custom-properties": { + "version": "12.1.11", + "resolved": "https://registry.npmjs.org/postcss-custom-properties/-/postcss-custom-properties-12.1.11.tgz", + "integrity": "sha512-0IDJYhgU8xDv1KY6+VgUwuQkVtmYzRwu+dMjnmdMafXYv86SWqfxkc7qdDvWS38vsjaEtv8e0vGOUQrAiMBLpQ==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/postcss-custom-selectors": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/postcss-custom-selectors/-/postcss-custom-selectors-6.0.3.tgz", + "integrity": "sha512-fgVkmyiWDwmD3JbpCmB45SvvlCD6z9CG6Ie6Iere22W5aHea6oWa7EM2bpnv2Fj3I94L3VbtvX9KqwSi5aFzSg==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.4" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.3" + } + }, + "node_modules/postcss-dir-pseudo-class": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/postcss-dir-pseudo-class/-/postcss-dir-pseudo-class-6.0.5.tgz", + "integrity": "sha512-eqn4m70P031PF7ZQIvSgy9RSJ5uI2171O/OO/zcRNYpJbvaeKFUlar1aJ7rmgiQtbm0FSPsRewjpdS0Oew7MPA==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.10" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/postcss-discard-comments": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-5.1.2.tgz", + "integrity": "sha512-+L8208OVbHVF2UQf1iDmRcbdjJkuBF6IS29yBDSiWUIzpYaAhtNl6JYnYm12FnkeCwQqF5LeklOu6rAqgfBZqQ==", + "dev": true, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-discard-duplicates": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-5.1.0.tgz", + "integrity": "sha512-zmX3IoSI2aoenxHV6C7plngHWWhUOV3sP1T8y2ifzxzbtnuhk1EdPwm0S1bIUNaJ2eNbWeGLEwzw8huPD67aQw==", + "dev": true, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-discard-empty": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-5.1.1.tgz", + "integrity": "sha512-zPz4WljiSuLWsI0ir4Mcnr4qQQ5e1Ukc3i7UfE2XcrwKK2LIPIqE5jxMRxO6GbI3cv//ztXDsXwEWT3BHOGh3A==", + "dev": true, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-discard-overridden": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-5.1.0.tgz", + "integrity": "sha512-21nOL7RqWR1kasIVdKs8HNqQJhFxLsyRfAnUDm4Fe4t4mCWL9OJiHvlHPjcd8zc5Myu89b/7wZDnOSjFgeWRtw==", + "dev": true, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-double-position-gradients": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/postcss-double-position-gradients/-/postcss-double-position-gradients-3.1.2.tgz", + "integrity": "sha512-GX+FuE/uBR6eskOK+4vkXgT6pDkexLokPaz/AbJna9s5Kzp/yl488pKPjhy0obB475ovfT1Wv8ho7U/cHNaRgQ==", + "dev": true, + "dependencies": { + "@csstools/postcss-progressive-custom-properties": "^1.1.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/postcss-env-function": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/postcss-env-function/-/postcss-env-function-4.0.6.tgz", + "integrity": "sha512-kpA6FsLra+NqcFnL81TnsU+Z7orGtDTxcOhl6pwXeEq1yFPpRMkCDpHhrz8CFQDr/Wfm0jLiNQ1OsGGPjlqPwA==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-flexbugs-fixes": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/postcss-flexbugs-fixes/-/postcss-flexbugs-fixes-5.0.2.tgz", + "integrity": "sha512-18f9voByak7bTktR2QgDveglpn9DTbBWPUzSOe9g0N4WR/2eSt6Vrcbf0hmspvMI6YWGywz6B9f7jzpFNJJgnQ==", + "dev": true, + "peerDependencies": { + "postcss": "^8.1.4" + } + }, + "node_modules/postcss-focus-visible": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/postcss-focus-visible/-/postcss-focus-visible-6.0.4.tgz", + "integrity": "sha512-QcKuUU/dgNsstIK6HELFRT5Y3lbrMLEOwG+A4s5cA+fx3A3y/JTq3X9LaOj3OC3ALH0XqyrgQIgey/MIZ8Wczw==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.9" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-focus-within": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/postcss-focus-within/-/postcss-focus-within-5.0.4.tgz", + "integrity": "sha512-vvjDN++C0mu8jz4af5d52CB184ogg/sSxAFS+oUJQq2SuCe7T5U2iIsVJtsCp2d6R4j0jr5+q3rPkBVZkXD9fQ==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.9" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-font-variant": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/postcss-font-variant/-/postcss-font-variant-5.0.0.tgz", + "integrity": "sha512-1fmkBaCALD72CK2a9i468mA/+tr9/1cBxRRMXOUaZqO43oWPR5imcyPjXwuv7PXbCid4ndlP5zWhidQVVa3hmA==", + "dev": true, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-gap-properties": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/postcss-gap-properties/-/postcss-gap-properties-3.0.5.tgz", + "integrity": "sha512-IuE6gKSdoUNcvkGIqdtjtcMtZIFyXZhmFd5RUlg97iVEvp1BZKV5ngsAjCjrVy+14uhGBQl9tzmi1Qwq4kqVOg==", + "dev": true, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/postcss-image-set-function": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/postcss-image-set-function/-/postcss-image-set-function-4.0.7.tgz", + "integrity": "sha512-9T2r9rsvYzm5ndsBE8WgtrMlIT7VbtTfE7b3BQnudUqnBcBo7L758oc+o+pdj/dUV0l5wjwSdjeOH2DZtfv8qw==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-initial": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-initial/-/postcss-initial-4.0.1.tgz", + "integrity": "sha512-0ueD7rPqX8Pn1xJIjay0AZeIuDoF+V+VvMt/uOnn+4ezUKhZM/NokDeP6DwMNyIoYByuN/94IQnt5FEkaN59xQ==", + "dev": true, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", + "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", + "dev": true, + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-lab-function": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/postcss-lab-function/-/postcss-lab-function-4.2.1.tgz", + "integrity": "sha512-xuXll4isR03CrQsmxyz92LJB2xX9n+pZJ5jE9JgcnmsCammLyKdlzrBin+25dy6wIjfhJpKBAN80gsTlCgRk2w==", + "dev": true, + "dependencies": { + "@csstools/postcss-progressive-custom-properties": "^1.1.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/postcss-load-config": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.1.tgz", + "integrity": "sha512-vEJIc8RdiBRu3oRAI0ymerOn+7rPuMvRXslTvZUKZonDHFIczxztIyJ1urxM1x9JXEikvpWWTUUqal5j/8QgvA==", + "dev": true, + "dependencies": { + "lilconfig": "^2.0.5", + "yaml": "^2.1.1" + }, + "engines": { + "node": ">= 14" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/postcss-load-config/node_modules/yaml": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.1.tgz", + "integrity": "sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ==", + "dev": true, + "engines": { + "node": ">= 14" + } + }, + "node_modules/postcss-loader": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-6.2.1.tgz", + "integrity": "sha512-WbbYpmAaKcux/P66bZ40bpWsBucjx/TTgVVzRZ9yUO8yQfVBlameJ0ZGVaPfH64hNSBh63a+ICP5nqOpBA0w+Q==", + "dev": true, + "dependencies": { + "cosmiconfig": "^7.0.0", + "klona": "^2.0.5", + "semver": "^7.3.5" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "postcss": "^7.0.0 || ^8.0.1", + "webpack": "^5.0.0" + } + }, + "node_modules/postcss-logical": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/postcss-logical/-/postcss-logical-5.0.4.tgz", + "integrity": "sha512-RHXxplCeLh9VjinvMrZONq7im4wjWGlRJAqmAVLXyZaXwfDWP73/oq4NdIp+OZwhQUMj0zjqDfM5Fj7qby+B4g==", + "dev": true, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-media-minmax": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/postcss-media-minmax/-/postcss-media-minmax-5.0.0.tgz", + "integrity": "sha512-yDUvFf9QdFZTuCUg0g0uNSHVlJ5X1lSzDZjPSFaiCWvjgsvu8vEVxtahPrLMinIDEEGnx6cBe6iqdx5YWz08wQ==", + "dev": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-merge-longhand": { + "version": "5.1.7", + "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-5.1.7.tgz", + "integrity": "sha512-YCI9gZB+PLNskrK0BB3/2OzPnGhPkBEwmwhfYk1ilBHYVAZB7/tkTHFBAnCrvBBOmeYyMYw3DMjT55SyxMBzjQ==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0", + "stylehacks": "^5.1.1" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-merge-rules": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-5.1.4.tgz", + "integrity": "sha512-0R2IuYpgU93y9lhVbO/OylTtKMVcHb67zjWIfCiKR9rWL3GUk1677LAqD/BcHizukdZEjT8Ru3oHRoAYoJy44g==", + "dev": true, + "dependencies": { + "browserslist": "^4.21.4", + "caniuse-api": "^3.0.0", + "cssnano-utils": "^3.1.0", + "postcss-selector-parser": "^6.0.5" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-minify-font-values": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-5.1.0.tgz", + "integrity": "sha512-el3mYTgx13ZAPPirSVsHqFzl+BBBDrXvbySvPGFnQcTI4iNslrPaFq4muTkLZmKlGk4gyFAYUBMH30+HurREyA==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-minify-gradients": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-5.1.1.tgz", + "integrity": "sha512-VGvXMTpCEo4qHTNSa9A0a3D+dxGFZCYwR6Jokk+/3oB6flu2/PnPXAh2x7x52EkY5xlIHLm+Le8tJxe/7TNhzw==", + "dev": true, + "dependencies": { + "colord": "^2.9.1", + "cssnano-utils": "^3.1.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-minify-params": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-5.1.4.tgz", + "integrity": "sha512-+mePA3MgdmVmv6g+30rn57USjOGSAyuxUmkfiWpzalZ8aiBkdPYjXWtHuwJGm1v5Ojy0Z0LaSYhHaLJQB0P8Jw==", + "dev": true, + "dependencies": { + "browserslist": "^4.21.4", + "cssnano-utils": "^3.1.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-minify-selectors": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-5.2.1.tgz", + "integrity": "sha512-nPJu7OjZJTsVUmPdm2TcaiohIwxP+v8ha9NehQ2ye9szv4orirRU3SDdtUmKH+10nzn0bAyOXZ0UEr7OpvLehg==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.5" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-modules-extract-imports": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz", + "integrity": "sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==", + "dev": true, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-local-by-default": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.3.tgz", + "integrity": "sha512-2/u2zraspoACtrbFRnTijMiQtb4GW4BvatjaG/bCjYQo8kLTdevCUlwuBHx2sCnSyrI3x3qj4ZK1j5LQBgzmwA==", + "dev": true, + "dependencies": { + "icss-utils": "^5.0.0", + "postcss-selector-parser": "^6.0.2", + "postcss-value-parser": "^4.1.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-scope": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.0.0.tgz", + "integrity": "sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.4" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-values": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", + "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", + "dev": true, + "dependencies": { + "icss-utils": "^5.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-nested": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.1.tgz", + "integrity": "sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.11" + }, + "engines": { + "node": ">=12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-nesting": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/postcss-nesting/-/postcss-nesting-10.2.0.tgz", + "integrity": "sha512-EwMkYchxiDiKUhlJGzWsD9b2zvq/r2SSubcRrgP+jujMXFzqvANLt16lJANC+5uZ6hjI7lpRmI6O8JIl+8l1KA==", + "dev": true, + "dependencies": { + "@csstools/selector-specificity": "^2.0.0", + "postcss-selector-parser": "^6.0.10" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/postcss-normalize": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize/-/postcss-normalize-10.0.1.tgz", + "integrity": "sha512-+5w18/rDev5mqERcG3W5GZNMJa1eoYYNGo8gB7tEwaos0ajk3ZXAI4mHGcNT47NE+ZnZD1pEpUOFLvltIwmeJA==", + "dev": true, + "dependencies": { + "@csstools/normalize.css": "*", + "postcss-browser-comments": "^4", + "sanitize.css": "*" + }, + "engines": { + "node": ">= 12" + }, + "peerDependencies": { + "browserslist": ">= 4", + "postcss": ">= 8" + } + }, + "node_modules/postcss-normalize-charset": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-5.1.0.tgz", + "integrity": "sha512-mSgUJ+pd/ldRGVx26p2wz9dNZ7ji6Pn8VWBajMXFf8jk7vUoSrZ2lt/wZR7DtlZYKesmZI680qjr2CeFF2fbUg==", + "dev": true, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-normalize-display-values": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-5.1.0.tgz", + "integrity": "sha512-WP4KIM4o2dazQXWmFaqMmcvsKmhdINFblgSeRgn8BJ6vxaMyaJkwAzpPpuvSIoG/rmX3M+IrRZEz2H0glrQNEA==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-normalize-positions": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-5.1.1.tgz", + "integrity": "sha512-6UpCb0G4eofTCQLFVuI3EVNZzBNPiIKcA1AKVka+31fTVySphr3VUgAIULBhxZkKgwLImhzMR2Bw1ORK+37INg==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-normalize-repeat-style": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-5.1.1.tgz", + "integrity": "sha512-mFpLspGWkQtBcWIRFLmewo8aC3ImN2i/J3v8YCFUwDnPu3Xz4rLohDO26lGjwNsQxB3YF0KKRwspGzE2JEuS0g==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-normalize-string": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-5.1.0.tgz", + "integrity": "sha512-oYiIJOf4T9T1N4i+abeIc7Vgm/xPCGih4bZz5Nm0/ARVJ7K6xrDlLwvwqOydvyL3RHNf8qZk6vo3aatiw/go3w==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-normalize-timing-functions": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-5.1.0.tgz", + "integrity": "sha512-DOEkzJ4SAXv5xkHl0Wa9cZLF3WCBhF3o1SKVxKQAa+0pYKlueTpCgvkFAHfk+Y64ezX9+nITGrDZeVGgITJXjg==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-normalize-unicode": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-5.1.1.tgz", + "integrity": "sha512-qnCL5jzkNUmKVhZoENp1mJiGNPcsJCs1aaRmURmeJGES23Z/ajaln+EPTD+rBeNkSryI+2WTdW+lwcVdOikrpA==", + "dev": true, + "dependencies": { + "browserslist": "^4.21.4", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-normalize-url": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-5.1.0.tgz", + "integrity": "sha512-5upGeDO+PVthOxSmds43ZeMeZfKH+/DKgGRD7TElkkyS46JXAUhMzIKiCa7BabPeIy3AQcTkXwVVN7DbqsiCew==", + "dev": true, + "dependencies": { + "normalize-url": "^6.0.1", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-normalize-whitespace": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-5.1.1.tgz", + "integrity": "sha512-83ZJ4t3NUDETIHTa3uEg6asWjSBYL5EdkVB0sDncx9ERzOKBVJIUeDO9RyA9Zwtig8El1d79HBp0JEi8wvGQnA==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-opacity-percentage": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/postcss-opacity-percentage/-/postcss-opacity-percentage-1.1.3.tgz", + "integrity": "sha512-An6Ba4pHBiDtyVpSLymUUERMo2cU7s+Obz6BTrS+gxkbnSBNKSuD0AVUc+CpBMrpVPKKfoVz0WQCX+Tnst0i4A==", + "dev": true, + "funding": [ + { + "type": "kofi", + "url": "https://ko-fi.com/mrcgrtz" + }, + { + "type": "liberapay", + "url": "https://liberapay.com/mrcgrtz" + } + ], + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/postcss-ordered-values": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-5.1.3.tgz", + "integrity": "sha512-9UO79VUhPwEkzbb3RNpqqghc6lcYej1aveQteWY+4POIwlqkYE21HKWaLDF6lWNuqCobEAyTovVhtI32Rbv2RQ==", + "dev": true, + "dependencies": { + "cssnano-utils": "^3.1.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-overflow-shorthand": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/postcss-overflow-shorthand/-/postcss-overflow-shorthand-3.0.4.tgz", + "integrity": "sha512-otYl/ylHK8Y9bcBnPLo3foYFLL6a6Ak+3EQBPOTR7luMYCOsiVTUk1iLvNf6tVPNGXcoL9Hoz37kpfriRIFb4A==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/postcss-page-break": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/postcss-page-break/-/postcss-page-break-3.0.4.tgz", + "integrity": "sha512-1JGu8oCjVXLa9q9rFTo4MbeeA5FMe00/9C7lN4va606Rdb+HkxXtXsmEDrIraQ11fGz/WvKWa8gMuCKkrXpTsQ==", + "dev": true, + "peerDependencies": { + "postcss": "^8" + } + }, + "node_modules/postcss-place": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/postcss-place/-/postcss-place-7.0.5.tgz", + "integrity": "sha512-wR8igaZROA6Z4pv0d+bvVrvGY4GVHihBCBQieXFY3kuSuMyOmEnnfFzHl/tQuqHZkfkIVBEbDvYcFfHmpSet9g==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/postcss-preset-env": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/postcss-preset-env/-/postcss-preset-env-7.8.3.tgz", + "integrity": "sha512-T1LgRm5uEVFSEF83vHZJV2z19lHg4yJuZ6gXZZkqVsqv63nlr6zabMH3l4Pc01FQCyfWVrh2GaUeCVy9Po+Aag==", + "dev": true, + "dependencies": { + "@csstools/postcss-cascade-layers": "^1.1.1", + "@csstools/postcss-color-function": "^1.1.1", + "@csstools/postcss-font-format-keywords": "^1.0.1", + "@csstools/postcss-hwb-function": "^1.0.2", + "@csstools/postcss-ic-unit": "^1.0.1", + "@csstools/postcss-is-pseudo-class": "^2.0.7", + "@csstools/postcss-nested-calc": "^1.0.0", + "@csstools/postcss-normalize-display-values": "^1.0.1", + "@csstools/postcss-oklab-function": "^1.1.1", + "@csstools/postcss-progressive-custom-properties": "^1.3.0", + "@csstools/postcss-stepped-value-functions": "^1.0.1", + "@csstools/postcss-text-decoration-shorthand": "^1.0.0", + "@csstools/postcss-trigonometric-functions": "^1.0.2", + "@csstools/postcss-unset-value": "^1.0.2", + "autoprefixer": "^10.4.13", + "browserslist": "^4.21.4", + "css-blank-pseudo": "^3.0.3", + "css-has-pseudo": "^3.0.4", + "css-prefers-color-scheme": "^6.0.3", + "cssdb": "^7.1.0", + "postcss-attribute-case-insensitive": "^5.0.2", + "postcss-clamp": "^4.1.0", + "postcss-color-functional-notation": "^4.2.4", + "postcss-color-hex-alpha": "^8.0.4", + "postcss-color-rebeccapurple": "^7.1.1", + "postcss-custom-media": "^8.0.2", + "postcss-custom-properties": "^12.1.10", + "postcss-custom-selectors": "^6.0.3", + "postcss-dir-pseudo-class": "^6.0.5", + "postcss-double-position-gradients": "^3.1.2", + "postcss-env-function": "^4.0.6", + "postcss-focus-visible": "^6.0.4", + "postcss-focus-within": "^5.0.4", + "postcss-font-variant": "^5.0.0", + "postcss-gap-properties": "^3.0.5", + "postcss-image-set-function": "^4.0.7", + "postcss-initial": "^4.0.1", + "postcss-lab-function": "^4.2.1", + "postcss-logical": "^5.0.4", + "postcss-media-minmax": "^5.0.0", + "postcss-nesting": "^10.2.0", + "postcss-opacity-percentage": "^1.1.2", + "postcss-overflow-shorthand": "^3.0.4", + "postcss-page-break": "^3.0.4", + "postcss-place": "^7.0.5", + "postcss-pseudo-class-any-link": "^7.1.6", + "postcss-replace-overflow-wrap": "^4.0.0", + "postcss-selector-not": "^6.0.1", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/postcss-pseudo-class-any-link": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/postcss-pseudo-class-any-link/-/postcss-pseudo-class-any-link-7.1.6.tgz", + "integrity": "sha512-9sCtZkO6f/5ML9WcTLcIyV1yz9D1rf0tWc+ulKcvV30s0iZKS/ONyETvoWsr6vnrmW+X+KmuK3gV/w5EWnT37w==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.10" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/postcss-reduce-initial": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-5.1.2.tgz", + "integrity": "sha512-dE/y2XRaqAi6OvjzD22pjTUQ8eOfc6m/natGHgKFBK9DxFmIm69YmaRVQrGgFlEfc1HePIurY0TmDeROK05rIg==", + "dev": true, + "dependencies": { + "browserslist": "^4.21.4", + "caniuse-api": "^3.0.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-reduce-transforms": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-5.1.0.tgz", + "integrity": "sha512-2fbdbmgir5AvpW9RLtdONx1QoYG2/EtqpNQbFASDlixBbAYuTcJ0dECwlqNqH7VbaUnEnh8SrxOe2sRIn24XyQ==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-replace-overflow-wrap": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-replace-overflow-wrap/-/postcss-replace-overflow-wrap-4.0.0.tgz", + "integrity": "sha512-KmF7SBPphT4gPPcKZc7aDkweHiKEEO8cla/GjcBK+ckKxiZslIu3C4GCRW3DNfL0o7yW7kMQu9xlZ1kXRXLXtw==", + "dev": true, + "peerDependencies": { + "postcss": "^8.0.3" + } + }, + "node_modules/postcss-selector-not": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-selector-not/-/postcss-selector-not-6.0.1.tgz", + "integrity": "sha512-1i9affjAe9xu/y9uqWH+tD4r6/hDaXJruk8xn2x1vzxC2U3J3LKO3zJW4CyxlNhA56pADJ/djpEwpH1RClI2rQ==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.10" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.0.13", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.13.tgz", + "integrity": "sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==", + "dev": true, + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-svgo": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-5.1.0.tgz", + "integrity": "sha512-D75KsH1zm5ZrHyxPakAxJWtkyXew5qwS70v56exwvw542d9CRtTo78K0WeFxZB4G7JXKKMbEZtZayTGdIky/eA==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0", + "svgo": "^2.7.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-svgo/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "dev": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/postcss-svgo/node_modules/css-tree": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz", + "integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==", + "dev": true, + "dependencies": { + "mdn-data": "2.0.14", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/postcss-svgo/node_modules/mdn-data": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", + "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==", + "dev": true + }, + "node_modules/postcss-svgo/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postcss-svgo/node_modules/svgo": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-2.8.0.tgz", + "integrity": "sha512-+N/Q9kV1+F+UeWYoSiULYo4xYSDQlTgb+ayMobAXPwMnLvop7oxKMo9OzIrX5x3eS4L4f2UHhc9axXwY8DpChg==", + "dev": true, + "dependencies": { + "@trysound/sax": "0.2.0", + "commander": "^7.2.0", + "css-select": "^4.1.3", + "css-tree": "^1.1.3", + "csso": "^4.2.0", + "picocolors": "^1.0.0", + "stable": "^0.1.8" + }, + "bin": { + "svgo": "bin/svgo" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/postcss-unique-selectors": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-5.1.1.tgz", + "integrity": "sha512-5JiODlELrz8L2HwxfPnhOWZYWDxVHWL83ufOv84NrcgipI7TaeRsatAhK4Tr2/ZiYldpK/wBvw5BD3qfaK96GA==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.5" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", + "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", + "dev": true, + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/pretty-bytes": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", + "integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==", + "dev": true, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pretty-error": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-4.0.0.tgz", + "integrity": "sha512-AoJ5YMAcXKYxKhuJGdcvse+Voc6v1RgnsR3nWcYU7q4t6z0Q6T86sv5Zq8VIRbOWWFpvdGE83LtdSMNd+6Y0xw==", + "dev": true, + "dependencies": { + "lodash": "^4.17.20", + "renderkid": "^3.0.0" + } + }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true + }, + "node_modules/promise": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/promise/-/promise-8.3.0.tgz", + "integrity": "sha512-rZPNPKTOYVNEEKFaq1HqTgOwZD+4/YHS5ukLzQCypkj+OkYx7iv0mA91lJlpPPZ8vMau3IIGj5Qlwrx+8iiSmg==", + "dev": true, + "dependencies": { + "asap": "~2.0.6" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dev": true, + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-addr/node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/psl": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", + "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==", + "dev": true + }, + "node_modules/punycode": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", + "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/q": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", + "integrity": "sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==", + "dev": true, + "engines": { + "node": ">=0.6.0", + "teleport": ">=0.2.0" + } + }, + "node_modules/qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "dev": true, + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "dev": true + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/raf": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz", + "integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==", + "dev": true, + "dependencies": { + "performance-now": "^2.1.0" + } + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", + "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "dev": true, + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/raw-body/node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/raw-body/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", + "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-app-polyfill": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/react-app-polyfill/-/react-app-polyfill-3.0.0.tgz", + "integrity": "sha512-sZ41cxiU5llIB003yxxQBYrARBqe0repqPTTYBTmMqTz9szeBbE37BehCE891NZsmdZqqP+xWKdT3eo3vOzN8w==", + "dev": true, + "dependencies": { + "core-js": "^3.19.2", + "object-assign": "^4.1.1", + "promise": "^8.1.0", + "raf": "^3.4.1", + "regenerator-runtime": "^0.13.9", + "whatwg-fetch": "^3.6.2" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/react-dev-utils": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.1.tgz", + "integrity": "sha512-84Ivxmr17KjUupyqzFode6xKhjwuEJDROWKJy/BthkL7Wn6NJ8h4WE6k/exAv6ImS+0oZLRRW5j/aINMHyeGeQ==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.16.0", + "address": "^1.1.2", + "browserslist": "^4.18.1", + "chalk": "^4.1.2", + "cross-spawn": "^7.0.3", + "detect-port-alt": "^1.1.6", + "escape-string-regexp": "^4.0.0", + "filesize": "^8.0.6", + "find-up": "^5.0.0", + "fork-ts-checker-webpack-plugin": "^6.5.0", + "global-modules": "^2.0.0", + "globby": "^11.0.4", + "gzip-size": "^6.0.0", + "immer": "^9.0.7", + "is-root": "^2.1.0", + "loader-utils": "^3.2.0", + "open": "^8.4.0", + "pkg-up": "^3.1.0", + "prompts": "^2.4.2", + "react-error-overlay": "^6.0.11", + "recursive-readdir": "^2.2.2", + "shell-quote": "^1.7.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/react-dev-utils/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/react-dev-utils/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/react-dev-utils/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/react-dev-utils/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/react-dev-utils/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/react-dev-utils/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/react-dev-utils/node_modules/loader-utils": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.2.1.tgz", + "integrity": "sha512-ZvFw1KWS3GVyYBYb7qkmRM/WwL2TQQBxgCK62rlvm4WpVQ23Nb4tYjApUlfjrEGvOs7KHEsmyUn75OHZrJMWPw==", + "dev": true, + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/react-dev-utils/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/react-dom": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", + "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.0" + }, + "peerDependencies": { + "react": "^18.2.0" + } + }, + "node_modules/react-error-overlay": { + "version": "6.0.11", + "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.11.tgz", + "integrity": "sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==", + "dev": true + }, + "node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true + }, + "node_modules/react-refresh": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz", + "integrity": "sha512-F27qZr8uUqwhWZboondsPx8tnC3Ct3SxZA3V5WyEvujRyyNv0VYPhoBg1gZ8/MV5tubQp76Trw8lTv9hzRBa+A==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "6.14.2", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.14.2.tgz", + "integrity": "sha512-09Zss2dE2z+T1D03IheqAFtK4UzQyX8nFPWx6jkwdYzGLXd5ie06A6ezS2fO6zJfEb/SpG6UocN2O1hfD+2urQ==", + "dependencies": { + "@remix-run/router": "1.7.2" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.14.2", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.14.2.tgz", + "integrity": "sha512-5pWX0jdKR48XFZBuJqHosX3AAHjRAzygouMTyimnBPOLdY3WjzUSKhus2FVMihUFWzeLebDgr4r8UeQFAct7Bg==", + "dependencies": { + "@remix-run/router": "1.7.2", + "react-router": "6.14.2" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/react-router-hash-link": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/react-router-hash-link/-/react-router-hash-link-2.4.3.tgz", + "integrity": "sha512-NU7GWc265m92xh/aYD79Vr1W+zAIXDWp3L2YZOYP4rCqPnJ6LI6vh3+rKgkidtYijozHclaEQTAHaAaMWPVI4A==", + "dependencies": { + "prop-types": "^15.7.2" + }, + "peerDependencies": { + "react": ">=15", + "react-router-dom": ">=4" + } + }, + "node_modules/react-scripts": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.1.tgz", + "integrity": "sha512-8VAmEm/ZAwQzJ+GOMLbBsTdDKOpuZh7RPs0UymvBR2vRk4iZWCskjbFnxqjrzoIvlNNRZ3QJFx6/qDSi6zSnaQ==", + "dev": true, + "dependencies": { + "@babel/core": "^7.16.0", + "@pmmmwh/react-refresh-webpack-plugin": "^0.5.3", + "@svgr/webpack": "^5.5.0", + "babel-jest": "^27.4.2", + "babel-loader": "^8.2.3", + "babel-plugin-named-asset-import": "^0.3.8", + "babel-preset-react-app": "^10.0.1", + "bfj": "^7.0.2", + "browserslist": "^4.18.1", + "camelcase": "^6.2.1", + "case-sensitive-paths-webpack-plugin": "^2.4.0", + "css-loader": "^6.5.1", + "css-minimizer-webpack-plugin": "^3.2.0", + "dotenv": "^10.0.0", + "dotenv-expand": "^5.1.0", + "eslint": "^8.3.0", + "eslint-config-react-app": "^7.0.1", + "eslint-webpack-plugin": "^3.1.1", + "file-loader": "^6.2.0", + "fs-extra": "^10.0.0", + "html-webpack-plugin": "^5.5.0", + "identity-obj-proxy": "^3.0.0", + "jest": "^27.4.3", + "jest-resolve": "^27.4.2", + "jest-watch-typeahead": "^1.0.0", + "mini-css-extract-plugin": "^2.4.5", + "postcss": "^8.4.4", + "postcss-flexbugs-fixes": "^5.0.2", + "postcss-loader": "^6.2.1", + "postcss-normalize": "^10.0.1", + "postcss-preset-env": "^7.0.1", + "prompts": "^2.4.2", + "react-app-polyfill": "^3.0.0", + "react-dev-utils": "^12.0.1", + "react-refresh": "^0.11.0", + "resolve": "^1.20.0", + "resolve-url-loader": "^4.0.0", + "sass-loader": "^12.3.0", + "semver": "^7.3.5", + "source-map-loader": "^3.0.0", + "style-loader": "^3.3.1", + "tailwindcss": "^3.0.2", + "terser-webpack-plugin": "^5.2.5", + "webpack": "^5.64.4", + "webpack-dev-server": "^4.6.0", + "webpack-manifest-plugin": "^4.0.2", + "workbox-webpack-plugin": "^6.4.1" + }, + "bin": { + "react-scripts": "bin/react-scripts.js" + }, + "engines": { + "node": ">=14.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + }, + "peerDependencies": { + "react": ">= 16", + "typescript": "^3.2.1 || ^4" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/react-tooltip": { + "version": "5.19.0", + "resolved": "https://registry.npmjs.org/react-tooltip/-/react-tooltip-5.19.0.tgz", + "integrity": "sha512-NSUk77GMpxYKHFKJVNHL++QQXRuH2QW1qDrXPtJnp2s/MJvUnU73N5TTADwDyrO2+xGlr0xHhjvQphkF60cMEA==", + "dependencies": { + "@floating-ui/dom": "^1.0.0", + "classnames": "^2.3.0" + }, + "peerDependencies": { + "react": ">=16.14.0", + "react-dom": ">=16.14.0" + } + }, + "node_modules/reactflow": { + "version": "11.7.4", + "resolved": "https://registry.npmjs.org/reactflow/-/reactflow-11.7.4.tgz", + "integrity": "sha512-QI6+oc1Ft6oFeLSdHlp+SmgymbI5Tm49wj5JyE84O4A54yN/ImfYaBhLit9Cmfzxn9Tz6tDqmGMGbk4bdtB8/w==", + "dependencies": { + "@reactflow/background": "11.2.4", + "@reactflow/controls": "11.1.15", + "@reactflow/core": "11.7.4", + "@reactflow/minimap": "11.5.4", + "@reactflow/node-resizer": "2.1.1", + "@reactflow/node-toolbar": "1.2.3" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/recursive-readdir": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.3.tgz", + "integrity": "sha512-8HrF5ZsXk5FAH9dgsx3BlUer73nIhuj+9OrQwEbLTPOBzGkL1lsFCR01am+v+0m2Cmbs1nP12hLDl5FA7EszKA==", + "dev": true, + "dependencies": { + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/regenerate": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", + "dev": true + }, + "node_modules/regenerate-unicode-properties": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.0.tgz", + "integrity": "sha512-d1VudCLoIGitcU/hEg2QqvyGZQmdC0Lf8BqdOMXGFSvJP4bNV1+XqbPQeHHLD51Jh4QJJ225dlIFvY4Ly6MXmQ==", + "dev": true, + "dependencies": { + "regenerate": "^1.4.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "dev": true + }, + "node_modules/regenerator-transform": { + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.1.tgz", + "integrity": "sha512-knzmNAcuyxV+gQCufkYcvOqX/qIIfHLv0u5x79kRxuGojfYVky1f15TzZEu2Avte8QGepvUNTnLskf8E6X6Vyg==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.8.4" + } + }, + "node_modules/regex-parser": { + "version": "2.2.11", + "resolved": "https://registry.npmjs.org/regex-parser/-/regex-parser-2.2.11.tgz", + "integrity": "sha512-jbD/FT0+9MBU2XAZluI7w2OBs1RBi6p9M83nkoZayQXXU9e8Robt69FcZc7wU4eJD/YFTjn1JdCk3rbMJajz8Q==", + "dev": true + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.0.tgz", + "integrity": "sha512-0SutC3pNudRKgquxGoRGIz946MZVHqbNfPjBdxeOhBrdgDKlRoXmYLQN9xRbrR09ZXWeGAdPuif7egofn6v5LA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "functions-have-names": "^1.2.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexpu-core": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.3.2.tgz", + "integrity": "sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ==", + "dev": true, + "dependencies": { + "@babel/regjsgen": "^0.8.0", + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^10.1.0", + "regjsparser": "^0.9.1", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regjsparser": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.9.1.tgz", + "integrity": "sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==", + "dev": true, + "dependencies": { + "jsesc": "~0.5.0" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, + "node_modules/regjsparser/node_modules/jsesc": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "integrity": "sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + } + }, + "node_modules/relateurl": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", + "integrity": "sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/renderkid": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-3.0.0.tgz", + "integrity": "sha512-q/7VIQA8lmM1hF+jn+sFSPWGlMkSAeNYcPLmDQx2zzuiDfaLrOmumR8iaUKlenFgh0XRPIUeSPlH3A+AW3Z5pg==", + "dev": true, + "dependencies": { + "css-select": "^4.1.3", + "dom-converter": "^0.2.0", + "htmlparser2": "^6.1.0", + "lodash": "^4.17.21", + "strip-ansi": "^6.0.1" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true + }, + "node_modules/resolve": { + "version": "1.22.2", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz", + "integrity": "sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==", + "dev": true, + "dependencies": { + "is-core-module": "^2.11.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-url-loader": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-url-loader/-/resolve-url-loader-4.0.0.tgz", + "integrity": "sha512-05VEMczVREcbtT7Bz+C+96eUO5HDNvdthIiMB34t7FcF8ehcu4wC0sSgPUubs3XW2Q3CNLJk/BJrCU9wVRymiA==", + "dev": true, + "dependencies": { + "adjust-sourcemap-loader": "^4.0.0", + "convert-source-map": "^1.7.0", + "loader-utils": "^2.0.0", + "postcss": "^7.0.35", + "source-map": "0.6.1" + }, + "engines": { + "node": ">=8.9" + }, + "peerDependencies": { + "rework": "1.0.1", + "rework-visit": "1.0.0" + }, + "peerDependenciesMeta": { + "rework": { + "optional": true + }, + "rework-visit": { + "optional": true + } + } + }, + "node_modules/resolve-url-loader/node_modules/picocolors": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", + "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", + "dev": true + }, + "node_modules/resolve-url-loader/node_modules/postcss": { + "version": "7.0.39", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", + "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", + "dev": true, + "dependencies": { + "picocolors": "^0.2.1", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + } + }, + "node_modules/resolve-url-loader/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve.exports": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-1.1.1.tgz", + "integrity": "sha512-/NtpHNDN7jWhAaQ9BvBUYZ6YTXsRBgfqWFWP7BZBaoMJO/I3G5OFzvTuWNlZC3aPjins1F+TNrLKsGbH4rfsRQ==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rollup": { + "version": "2.79.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.1.tgz", + "integrity": "sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw==", + "dev": true, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=10.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/rollup-plugin-terser": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/rollup-plugin-terser/-/rollup-plugin-terser-7.0.2.tgz", + "integrity": "sha512-w3iIaU4OxcF52UUXiZNsNeuXIMDvFrr+ZXK6bFZ0Q60qyVfq4uLptoS4bbq3paG3x216eQllFZX7zt6TIImguQ==", + "deprecated": "This package has been deprecated and is no longer maintained. Please use @rollup/plugin-terser", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "jest-worker": "^26.2.1", + "serialize-javascript": "^4.0.0", + "terser": "^5.0.0" + }, + "peerDependencies": { + "rollup": "^2.0.0" + } + }, + "node_modules/rollup-plugin-terser/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/rollup-plugin-terser/node_modules/jest-worker": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-26.6.2.tgz", + "integrity": "sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==", + "dev": true, + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^7.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/rollup-plugin-terser/node_modules/serialize-javascript": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz", + "integrity": "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==", + "dev": true, + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/rollup-plugin-terser/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-array-concat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.0.0.tgz", + "integrity": "sha512-9dVEFruWIsnie89yym+xWTAYASdpw3CJV7Li/6zBewGf9z2i1j31rP6jnY0pHEO4QZh6N0K11bFjWmdR8UGdPQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.0", + "has-symbols": "^1.0.3", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safe-regex-test": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz", + "integrity": "sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.3", + "is-regex": "^1.1.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, + "node_modules/sanitize.css": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/sanitize.css/-/sanitize.css-13.0.0.tgz", + "integrity": "sha512-ZRwKbh/eQ6w9vmTjkuG0Ioi3HBwPFce0O+v//ve+aOq1oeCy7jMV2qzzAlpsNuqpqCBjjriM1lbtZbF/Q8jVyA==", + "dev": true + }, + "node_modules/sass-loader": { + "version": "12.6.0", + "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-12.6.0.tgz", + "integrity": "sha512-oLTaH0YCtX4cfnJZxKSLAyglED0naiYfNG1iXfU5w1LNZ+ukoA5DtyDIN5zmKVZwYNJP4KRc5Y3hkWga+7tYfA==", + "dev": true, + "dependencies": { + "klona": "^2.0.4", + "neo-async": "^2.6.2" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "fibers": ">= 3.1.0", + "node-sass": "^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0", + "sass": "^1.3.0", + "sass-embedded": "*", + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "fibers": { + "optional": true + }, + "node-sass": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + } + } + }, + "node_modules/sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", + "dev": true + }, + "node_modules/saxes": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz", + "integrity": "sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==", + "dev": true, + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/scheduler": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", + "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/select-hose": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", + "integrity": "sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==", + "dev": true + }, + "node_modules/selfsigned": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.1.1.tgz", + "integrity": "sha512-GSL3aowiF7wa/WtSFwnUrludWFoNhftq8bUkH9pkzjpN2XSPOAYEgg6e0sS9s0rZwgJzJiQRPU18A6clnoW5wQ==", + "dev": true, + "dependencies": { + "node-forge": "^1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "dev": true, + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/serialize-javascript": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.1.tgz", + "integrity": "sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==", + "dev": true, + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/serve-index": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", + "integrity": "sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw==", + "dev": true, + "dependencies": { + "accepts": "~1.3.4", + "batch": "0.6.1", + "debug": "2.6.9", + "escape-html": "~1.0.3", + "http-errors": "~1.6.2", + "mime-types": "~2.1.17", + "parseurl": "~1.3.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/serve-index/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/serve-index/node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-index/node_modules/http-errors": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==", + "dev": true, + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-index/node_modules/inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", + "dev": true + }, + "node_modules/serve-index/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/serve-index/node_modules/setprototypeof": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", + "dev": true + }, + "node_modules/serve-index/node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "dev": true, + "dependencies": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.18.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/shell-quote": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.1.tgz", + "integrity": "sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/sockjs": { + "version": "0.3.24", + "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz", + "integrity": "sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ==", + "dev": true, + "dependencies": { + "faye-websocket": "^0.11.3", + "uuid": "^8.3.2", + "websocket-driver": "^0.7.4" + } + }, + "node_modules/source-list-map": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", + "integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==", + "dev": true + }, + "node_modules/source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-loader": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/source-map-loader/-/source-map-loader-3.0.2.tgz", + "integrity": "sha512-BokxPoLjyl3iOrgkWaakaxqnelAJSS+0V+De0kKIq6lyWrXuiPgYTGp6z3iHmqljKAaLXwZa+ctD8GccRJeVvg==", + "dev": true, + "dependencies": { + "abab": "^2.0.5", + "iconv-lite": "^0.6.3", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sourcemap-codec": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", + "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", + "deprecated": "Please use @jridgewell/sourcemap-codec instead", + "dev": true + }, + "node_modules/spdy": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz", + "integrity": "sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==", + "dev": true, + "dependencies": { + "debug": "^4.1.0", + "handle-thing": "^2.0.0", + "http-deceiver": "^1.2.7", + "select-hose": "^2.0.0", + "spdy-transport": "^3.0.0" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/spdy-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz", + "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==", + "dev": true, + "dependencies": { + "debug": "^4.1.0", + "detect-node": "^2.0.4", + "hpack.js": "^2.1.6", + "obuf": "^1.1.2", + "readable-stream": "^3.0.6", + "wbuf": "^1.7.3" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true + }, + "node_modules/stable": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz", + "integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==", + "deprecated": "Modern JS already guarantees Array#sort() is a stable sort, so this library is deprecated. See the compatibility table on MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#browser_compatibility", + "dev": true + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/stackframe": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz", + "integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==", + "dev": true + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-natural-compare": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/string-natural-compare/-/string-natural-compare-3.0.1.tgz", + "integrity": "sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw==", + "dev": true + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/string.prototype.matchall": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.8.tgz", + "integrity": "sha512-6zOCOcJ+RJAQshcTvXPHoxoQGONa3e/Lqx90wUA+wEzX78sg5Bo+1tQo4N0pohS0erG9qtCqJDjNCQBjeWVxyg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4", + "get-intrinsic": "^1.1.3", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.3", + "regexp.prototype.flags": "^1.4.3", + "side-channel": "^1.0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.7.tgz", + "integrity": "sha512-p6TmeT1T3411M8Cgg9wBTMRtY2q9+PNy9EV1i2lIXUN/btt763oIfxwN3RR8VU6wHX8j/1CFy0L+YuThm6bgOg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.6.tgz", + "integrity": "sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.6.tgz", + "integrity": "sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/stringify-object": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/stringify-object/-/stringify-object-3.3.0.tgz", + "integrity": "sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==", + "dev": true, + "dependencies": { + "get-own-enumerable-property-symbols": "^3.0.0", + "is-obj": "^1.0.1", + "is-regexp": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-comments/-/strip-comments-2.0.1.tgz", + "integrity": "sha512-ZprKx+bBLXv067WTCALv8SSz5l2+XhpYCsVtSqlMnkAXMWDq+/ekVbl1ghqP9rUHTzv6sm/DwCOiYutU/yp1fw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/style-loader": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.3.tgz", + "integrity": "sha512-53BiGLXAcll9maCYtZi2RCQZKa8NQQai5C4horqKyRmHj9H7QmcUyucrH+4KW/gBQbXM2AsB0axoEcFZPlfPcw==", + "dev": true, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, + "node_modules/stylehacks": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-5.1.1.tgz", + "integrity": "sha512-sBpcd5Hx7G6seo7b1LkpttvTz7ikD0LlH5RmdcBNb6fFR0Fl7LQwHDFr300q4cwUqi+IYrFGmsIHieMBfnN/Bw==", + "dev": true, + "dependencies": { + "browserslist": "^4.21.4", + "postcss-selector-parser": "^6.0.4" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/sucrase": { + "version": "3.33.0", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.33.0.tgz", + "integrity": "sha512-ARGC7vbufOHfpvyGcZZXFaXCMZ9A4fffOGC5ucOW7+WHDGlAe8LJdf3Jts1sWhDeiI1RSWrKy5Hodl+JWGdW2A==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "glob": "7.1.6", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/sucrase/node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/sucrase/node_modules/glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/supports-hyperlinks": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.3.0.tgz", + "integrity": "sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0", + "supports-color": "^7.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-hyperlinks/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-hyperlinks/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/svg-parser": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/svg-parser/-/svg-parser-2.0.4.tgz", + "integrity": "sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==", + "dev": true + }, + "node_modules/svgo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-1.3.2.tgz", + "integrity": "sha512-yhy/sQYxR5BkC98CY7o31VGsg014AKLEPxdfhora76l36hD9Rdy5NZA/Ocn6yayNPgSamYdtX2rFJdcv07AYVw==", + "deprecated": "This SVGO version is no longer supported. Upgrade to v2.x.x.", + "dev": true, + "dependencies": { + "chalk": "^2.4.1", + "coa": "^2.0.2", + "css-select": "^2.0.0", + "css-select-base-adapter": "^0.1.1", + "css-tree": "1.0.0-alpha.37", + "csso": "^4.0.2", + "js-yaml": "^3.13.1", + "mkdirp": "~0.5.1", + "object.values": "^1.1.0", + "sax": "~1.2.4", + "stable": "^0.1.8", + "unquote": "~1.1.1", + "util.promisify": "~1.0.0" + }, + "bin": { + "svgo": "bin/svgo" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/svgo/node_modules/css-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-2.1.0.tgz", + "integrity": "sha512-Dqk7LQKpwLoH3VovzZnkzegqNSuAziQyNZUcrdDM401iY+R5NkGBXGmtO05/yaXQziALuPogeG0b7UAgjnTJTQ==", + "dev": true, + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^3.2.1", + "domutils": "^1.7.0", + "nth-check": "^1.0.2" + } + }, + "node_modules/svgo/node_modules/css-what": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-3.4.2.tgz", + "integrity": "sha512-ACUm3L0/jiZTqfzRM3Hi9Q8eZqd6IK37mMWPLz9PJxkLWllYeRf+EHUSHYEtFop2Eqytaq1FizFVh7XfBnXCDQ==", + "dev": true, + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/svgo/node_modules/dom-serializer": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.2.tgz", + "integrity": "sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==", + "dev": true, + "dependencies": { + "domelementtype": "^2.0.1", + "entities": "^2.0.0" + } + }, + "node_modules/svgo/node_modules/domutils": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz", + "integrity": "sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==", + "dev": true, + "dependencies": { + "dom-serializer": "0", + "domelementtype": "1" + } + }, + "node_modules/svgo/node_modules/domutils/node_modules/domelementtype": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", + "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==", + "dev": true + }, + "node_modules/svgo/node_modules/nth-check": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz", + "integrity": "sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==", + "dev": true, + "dependencies": { + "boolbase": "~1.0.0" + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true + }, + "node_modules/tailwindcss": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.3.tgz", + "integrity": "sha512-A0KgSkef7eE4Mf+nKJ83i75TMyq8HqY3qmFIJSWy8bNt0v1lG7jUcpGpoTFxAwYcWOphcTBLPPJg+bDfhDf52w==", + "dev": true, + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.5.3", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.2.12", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.18.2", + "lilconfig": "^2.1.0", + "micromatch": "^4.0.5", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.0.0", + "postcss": "^8.4.23", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.1", + "postcss-nested": "^6.0.1", + "postcss-selector-parser": "^6.0.11", + "resolve": "^1.22.2", + "sucrase": "^3.32.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/temp-dir": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-2.0.0.tgz", + "integrity": "sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/tempy": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tempy/-/tempy-0.6.0.tgz", + "integrity": "sha512-G13vtMYPT/J8A4X2SjdtBTphZlrp1gKv6hZiOjw14RCWg6GbHuQBGtjlx75xLbYV/wEc0D7G5K4rxKP/cXk8Bw==", + "dev": true, + "dependencies": { + "is-stream": "^2.0.0", + "temp-dir": "^2.0.0", + "type-fest": "^0.16.0", + "unique-string": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/tempy/node_modules/type-fest": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.16.0.tgz", + "integrity": "sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/terminal-link": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-2.1.1.tgz", + "integrity": "sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ==", + "dev": true, + "dependencies": { + "ansi-escapes": "^4.2.1", + "supports-hyperlinks": "^2.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/terser": { + "version": "5.19.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.19.0.tgz", + "integrity": "sha512-JpcpGOQLOXm2jsomozdMDpd5f8ZHh1rR48OFgWUH3QsyZcfPgv2qDCYbcDEAYNd4OZRj2bWYKpwdll/udZCk/Q==", + "dev": true, + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.8.2", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.3.9", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.9.tgz", + "integrity": "sha512-ZuXsqE07EcggTWQjXUj+Aot/OMcD0bMKGgF63f7UxYcu5/AJF53aIpK1YoP5xR9l6s/Hy2b+t1AM0bLNPRuhwA==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.17", + "jest-worker": "^27.4.5", + "schema-utils": "^3.1.1", + "serialize-javascript": "^6.0.1", + "terser": "^5.16.8" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/throat": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/throat/-/throat-6.0.2.tgz", + "integrity": "sha512-WKexMoJj3vEuK0yFEapj8y64V0A6xcuPuK9Gt1d0R+dzCSJc0lHqQytAbSB4cDAK0dWh4T0E2ETkoLE2WZ41OQ==", + "dev": true + }, + "node_modules/thunky": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", + "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", + "dev": true + }, + "node_modules/timers-ext": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/timers-ext/-/timers-ext-0.1.7.tgz", + "integrity": "sha512-b85NUNzTSdodShTIbky6ZF02e8STtVVfD+fu4aXXShEELpozH+bCpJLYMPZbsABN2wDH7fJpqIoXxJpzbf0NqQ==", + "dev": true, + "dependencies": { + "es5-ext": "~0.10.46", + "next-tick": "1" + } + }, + "node_modules/tiny-typed-emitter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tiny-typed-emitter/-/tiny-typed-emitter-2.1.0.tgz", + "integrity": "sha512-qVtvMxeXbVej0cQWKqVSSAHmKZEHAvxdF8HEUBFWts8h+xEo5m/lEiPakuyZ3BnCBjOD8i24kzNOiOLLgsSxhA==", + "dev": true + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tough-cookie": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz", + "integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==", + "dev": true, + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tough-cookie/node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/tr46": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-2.1.0.tgz", + "integrity": "sha512-15Ih7phfcdP5YxqiB+iDtLoaTz4Nd35+IiAv0kQ5FNKHzXgdWqPoTIqEDDJmXceQt4JZk6lVPT8lnDlPpGDppw==", + "dev": true, + "dependencies": { + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tryer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tryer/-/tryer-1.0.1.tgz", + "integrity": "sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA==", + "dev": true + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true + }, + "node_modules/tsconfig-paths": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz", + "integrity": "sha512-o/9iXgCYc5L/JxCHPe3Hvh8Q/2xm5Z+p18PESBU6Ff33695QnCHBEjcytY2q19ua7Mbl/DavtBOLq+oG0RCL+g==", + "dev": true, + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "node_modules/tsconfig-paths/node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/tsconfig-paths/node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/tslib": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.0.tgz", + "integrity": "sha512-7At1WUettjcSRHXCyYtTselblcHl9PJFFVKiCAy/bY97+BPZXSQ2wbq0P9s8tK2G7dFQfNnlJnPAiArVBVBsfA==", + "dev": true + }, + "node_modules/tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dev": true, + "dependencies": { + "tslib": "^1.8.1" + }, + "engines": { + "node": ">= 6" + }, + "peerDependencies": { + "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" + } + }, + "node_modules/tsutils/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "node_modules/type": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/type/-/type-1.2.0.tgz", + "integrity": "sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==", + "dev": true + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dev": true, + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.0.tgz", + "integrity": "sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.1", + "is-typed-array": "^1.1.10" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.0.tgz", + "integrity": "sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "has-proto": "^1.0.1", + "is-typed-array": "^1.1.10" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.0.tgz", + "integrity": "sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg==", + "dev": true, + "dependencies": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "has-proto": "^1.0.1", + "is-typed-array": "^1.1.10" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.4.tgz", + "integrity": "sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "is-typed-array": "^1.1.9" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typedarray-to-buffer": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "dev": true, + "dependencies": { + "is-typedarray": "^1.0.0" + } + }, + "node_modules/typescript": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/unbox-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", + "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-bigints": "^1.0.2", + "has-symbols": "^1.0.3", + "which-boxed-primitive": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/unicode-canonical-property-names-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", + "integrity": "sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", + "dev": true, + "dependencies": { + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-value-ecmascript": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz", + "integrity": "sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-property-aliases-ecmascript": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", + "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/unique-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", + "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==", + "dev": true, + "dependencies": { + "crypto-random-string": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "dev": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/unquote": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/unquote/-/unquote-1.1.1.tgz", + "integrity": "sha512-vRCqFv6UhXpWxZPyGDh/F3ZpNv8/qo7w6iufLpQg9aKnQ71qM4B5KiI7Mia9COcjEhrO9LueHpMYjYzsWH3OIg==", + "dev": true + }, + "node_modules/upath": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz", + "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==", + "dev": true, + "engines": { + "node": ">=4", + "yarn": "*" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz", + "integrity": "sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dev": true, + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "node_modules/use-sync-external-store": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", + "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true + }, + "node_modules/util.promisify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/util.promisify/-/util.promisify-1.0.1.tgz", + "integrity": "sha512-g9JpC/3He3bm38zsLupWryXHoEcS22YHthuPQSJdMy6KNrzIRzWqcsHzD/WUnqe45whVou4VIsPew37DoXWNrA==", + "dev": true, + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.2", + "has-symbols": "^1.0.1", + "object.getownpropertydescriptors": "^2.1.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/utila": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/utila/-/utila-0.4.0.tgz", + "integrity": "sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA==", + "dev": true + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "dev": true, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/v8-to-istanbul": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-8.1.1.tgz", + "integrity": "sha512-FGtKtv3xIpR6BYhvgH8MI/y78oT7d8Au3ww4QIxymrCtZEh5b8gCw2siywE+puhEmuWKDtmfrvF5UlB298ut3w==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^1.6.0", + "source-map": "^0.7.3" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/w3c-hr-time": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz", + "integrity": "sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==", + "deprecated": "Use your platform's native performance.now() and performance.timeOrigin.", + "dev": true, + "dependencies": { + "browser-process-hrtime": "^1.0.0" + } + }, + "node_modules/w3c-xmlserializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-2.0.0.tgz", + "integrity": "sha512-4tzD0mF8iSiMiNs30BiLO3EpfGLZUT2MSX/G+o7ZywDzliWQ3OPtTZ0PTC3B3ca1UAf4cJMHB+2Bf56EriJuRA==", + "dev": true, + "dependencies": { + "xml-name-validator": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/watchpack": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", + "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==", + "dev": true, + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/wbuf": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz", + "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==", + "dev": true, + "dependencies": { + "minimalistic-assert": "^1.0.0" + } + }, + "node_modules/webidl-conversions": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz", + "integrity": "sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w==", + "dev": true, + "engines": { + "node": ">=10.4" + } + }, + "node_modules/webpack": { + "version": "5.88.1", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.88.1.tgz", + "integrity": "sha512-FROX3TxQnC/ox4N+3xQoWZzvGXSuscxR32rbzjpXgEzWudJFEJBpdlkkob2ylrv5yzzufD1zph1OoFsLtm6stQ==", + "dev": true, + "dependencies": { + "@types/eslint-scope": "^3.7.3", + "@types/estree": "^1.0.0", + "@webassemblyjs/ast": "^1.11.5", + "@webassemblyjs/wasm-edit": "^1.11.5", + "@webassemblyjs/wasm-parser": "^1.11.5", + "acorn": "^8.7.1", + "acorn-import-assertions": "^1.9.0", + "browserslist": "^4.14.5", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.15.0", + "es-module-lexer": "^1.2.1", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.9", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^3.2.0", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.3.7", + "watchpack": "^2.4.0", + "webpack-sources": "^3.2.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-dev-middleware": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.3.3.tgz", + "integrity": "sha512-hj5CYrY0bZLB+eTO+x/j67Pkrquiy7kWepMHmUMoPsmcUaeEnQJqFzHJOyxgWlq746/wUuA64p9ta34Kyb01pA==", + "dev": true, + "dependencies": { + "colorette": "^2.0.10", + "memfs": "^3.4.3", + "mime-types": "^2.1.31", + "range-parser": "^1.2.1", + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" + } + }, + "node_modules/webpack-dev-middleware/node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/webpack-dev-middleware/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/webpack-dev-middleware/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "node_modules/webpack-dev-middleware/node_modules/schema-utils": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz", + "integrity": "sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/webpack-dev-server": { + "version": "4.15.1", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.15.1.tgz", + "integrity": "sha512-5hbAst3h3C3L8w6W4P96L5vaV0PxSmJhxZvWKYIdgxOQm8pNZ5dEOmmSLBVpP85ReeyRt6AS1QJNyo/oFFPeVA==", + "dev": true, + "dependencies": { + "@types/bonjour": "^3.5.9", + "@types/connect-history-api-fallback": "^1.3.5", + "@types/express": "^4.17.13", + "@types/serve-index": "^1.9.1", + "@types/serve-static": "^1.13.10", + "@types/sockjs": "^0.3.33", + "@types/ws": "^8.5.5", + "ansi-html-community": "^0.0.8", + "bonjour-service": "^1.0.11", + "chokidar": "^3.5.3", + "colorette": "^2.0.10", + "compression": "^1.7.4", + "connect-history-api-fallback": "^2.0.0", + "default-gateway": "^6.0.3", + "express": "^4.17.3", + "graceful-fs": "^4.2.6", + "html-entities": "^2.3.2", + "http-proxy-middleware": "^2.0.3", + "ipaddr.js": "^2.0.1", + "launch-editor": "^2.6.0", + "open": "^8.0.9", + "p-retry": "^4.5.0", + "rimraf": "^3.0.2", + "schema-utils": "^4.0.0", + "selfsigned": "^2.1.1", + "serve-index": "^1.9.1", + "sockjs": "^0.3.24", + "spdy": "^4.0.2", + "webpack-dev-middleware": "^5.3.1", + "ws": "^8.13.0" + }, + "bin": { + "webpack-dev-server": "bin/webpack-dev-server.js" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.37.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + }, + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-dev-server/node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/webpack-dev-server/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/webpack-dev-server/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "node_modules/webpack-dev-server/node_modules/schema-utils": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz", + "integrity": "sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/webpack-dev-server/node_modules/ws": { + "version": "8.13.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", + "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", + "dev": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/webpack-manifest-plugin": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/webpack-manifest-plugin/-/webpack-manifest-plugin-4.1.1.tgz", + "integrity": "sha512-YXUAwxtfKIJIKkhg03MKuiFAD72PlrqCiwdwO4VEXdRO5V0ORCNwaOwAZawPZalCbmH9kBDmXnNeQOw+BIEiow==", + "dev": true, + "dependencies": { + "tapable": "^2.0.0", + "webpack-sources": "^2.2.0" + }, + "engines": { + "node": ">=12.22.0" + }, + "peerDependencies": { + "webpack": "^4.44.2 || ^5.47.0" + } + }, + "node_modules/webpack-manifest-plugin/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/webpack-manifest-plugin/node_modules/webpack-sources": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-2.3.1.tgz", + "integrity": "sha512-y9EI9AO42JjEcrTJFOYmVywVZdKVUfOvDUPsJea5GIr1JOEGFVqwlY2K098fFoIjOkDzHn2AjRvM8dsBZu+gCA==", + "dev": true, + "dependencies": { + "source-list-map": "^2.0.1", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack-sources": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", + "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "dev": true, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack/node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/webpack/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/websocket-driver": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "dev": true, + "dependencies": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/whatwg-encoding": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz", + "integrity": "sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==", + "dev": true, + "dependencies": { + "iconv-lite": "0.4.24" + } + }, + "node_modules/whatwg-encoding/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/whatwg-fetch": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.2.tgz", + "integrity": "sha512-bJlen0FcuU/0EMLrdbJ7zOnW6ITZLrZMIarMUVmdKtsGvZna8vxKYaexICWPfZ8qwf9fzNq+UEIZrnSaApt6RA==", + "dev": true + }, + "node_modules/whatwg-mimetype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz", + "integrity": "sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==", + "dev": true + }, + "node_modules/whatwg-url": { + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-8.7.0.tgz", + "integrity": "sha512-gAojqb/m9Q8a5IV96E3fHJM70AzCkgt4uXYX2O7EmuyOnLrViCQlsEBmF9UQIu3/aeAIp2U17rtbpZWNntQqdg==", + "dev": true, + "dependencies": { + "lodash": "^4.7.0", + "tr46": "^2.1.0", + "webidl-conversions": "^6.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "dev": true, + "dependencies": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.10.tgz", + "integrity": "sha512-uxoA5vLUfRPdjCuJ1h5LlYdmTLbYfums398v3WLkM+i/Wltl2/XyZpQWKbN++ck5L64SR/grOHqtXCUKmlZPNA==", + "dev": true, + "dependencies": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-tostringtag": "^1.0.0", + "is-typed-array": "^1.1.10" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/workbox-background-sync": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/workbox-background-sync/-/workbox-background-sync-6.6.0.tgz", + "integrity": "sha512-jkf4ZdgOJxC9u2vztxLuPT/UjlH7m/nWRQ/MgGL0v8BJHoZdVGJd18Kck+a0e55wGXdqyHO+4IQTk0685g4MUw==", + "dev": true, + "dependencies": { + "idb": "^7.0.1", + "workbox-core": "6.6.0" + } + }, + "node_modules/workbox-broadcast-update": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/workbox-broadcast-update/-/workbox-broadcast-update-6.6.0.tgz", + "integrity": "sha512-nm+v6QmrIFaB/yokJmQ/93qIJ7n72NICxIwQwe5xsZiV2aI93MGGyEyzOzDPVz5THEr5rC3FJSsO3346cId64Q==", + "dev": true, + "dependencies": { + "workbox-core": "6.6.0" + } + }, + "node_modules/workbox-build": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/workbox-build/-/workbox-build-6.6.0.tgz", + "integrity": "sha512-Tjf+gBwOTuGyZwMz2Nk/B13Fuyeo0Q84W++bebbVsfr9iLkDSo6j6PST8tET9HYA58mlRXwlMGpyWO8ETJiXdQ==", + "dev": true, + "dependencies": { + "@apideck/better-ajv-errors": "^0.3.1", + "@babel/core": "^7.11.1", + "@babel/preset-env": "^7.11.0", + "@babel/runtime": "^7.11.2", + "@rollup/plugin-babel": "^5.2.0", + "@rollup/plugin-node-resolve": "^11.2.1", + "@rollup/plugin-replace": "^2.4.1", + "@surma/rollup-plugin-off-main-thread": "^2.2.3", + "ajv": "^8.6.0", + "common-tags": "^1.8.0", + "fast-json-stable-stringify": "^2.1.0", + "fs-extra": "^9.0.1", + "glob": "^7.1.6", + "lodash": "^4.17.20", + "pretty-bytes": "^5.3.0", + "rollup": "^2.43.1", + "rollup-plugin-terser": "^7.0.0", + "source-map": "^0.8.0-beta.0", + "stringify-object": "^3.3.0", + "strip-comments": "^2.0.1", + "tempy": "^0.6.0", + "upath": "^1.2.0", + "workbox-background-sync": "6.6.0", + "workbox-broadcast-update": "6.6.0", + "workbox-cacheable-response": "6.6.0", + "workbox-core": "6.6.0", + "workbox-expiration": "6.6.0", + "workbox-google-analytics": "6.6.0", + "workbox-navigation-preload": "6.6.0", + "workbox-precaching": "6.6.0", + "workbox-range-requests": "6.6.0", + "workbox-recipes": "6.6.0", + "workbox-routing": "6.6.0", + "workbox-strategies": "6.6.0", + "workbox-streams": "6.6.0", + "workbox-sw": "6.6.0", + "workbox-window": "6.6.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/workbox-build/node_modules/@apideck/better-ajv-errors": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@apideck/better-ajv-errors/-/better-ajv-errors-0.3.6.tgz", + "integrity": "sha512-P+ZygBLZtkp0qqOAJJVX4oX/sFo5JR3eBWwwuqHHhK0GIgQOKWrAfiAaWX0aArHkRWHMuggFEgAZNxVPwPZYaA==", + "dev": true, + "dependencies": { + "json-schema": "^0.4.0", + "jsonpointer": "^5.0.0", + "leven": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "ajv": ">=8" + } + }, + "node_modules/workbox-build/node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/workbox-build/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/workbox-build/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "node_modules/workbox-build/node_modules/source-map": { + "version": "0.8.0-beta.0", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz", + "integrity": "sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==", + "dev": true, + "dependencies": { + "whatwg-url": "^7.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/workbox-build/node_modules/tr46": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", + "integrity": "sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/workbox-build/node_modules/webidl-conversions": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", + "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", + "dev": true + }, + "node_modules/workbox-build/node_modules/whatwg-url": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz", + "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==", + "dev": true, + "dependencies": { + "lodash.sortby": "^4.7.0", + "tr46": "^1.0.1", + "webidl-conversions": "^4.0.2" + } + }, + "node_modules/workbox-cacheable-response": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/workbox-cacheable-response/-/workbox-cacheable-response-6.6.0.tgz", + "integrity": "sha512-JfhJUSQDwsF1Xv3EV1vWzSsCOZn4mQ38bWEBR3LdvOxSPgB65gAM6cS2CX8rkkKHRgiLrN7Wxoyu+TuH67kHrw==", + "deprecated": "workbox-background-sync@6.6.0", + "dev": true, + "dependencies": { + "workbox-core": "6.6.0" + } + }, + "node_modules/workbox-core": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/workbox-core/-/workbox-core-6.6.0.tgz", + "integrity": "sha512-GDtFRF7Yg3DD859PMbPAYPeJyg5gJYXuBQAC+wyrWuuXgpfoOrIQIvFRZnQ7+czTIQjIr1DhLEGFzZanAT/3bQ==", + "dev": true + }, + "node_modules/workbox-expiration": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/workbox-expiration/-/workbox-expiration-6.6.0.tgz", + "integrity": "sha512-baplYXcDHbe8vAo7GYvyAmlS4f6998Jff513L4XvlzAOxcl8F620O91guoJ5EOf5qeXG4cGdNZHkkVAPouFCpw==", + "dev": true, + "dependencies": { + "idb": "^7.0.1", + "workbox-core": "6.6.0" + } + }, + "node_modules/workbox-google-analytics": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/workbox-google-analytics/-/workbox-google-analytics-6.6.0.tgz", + "integrity": "sha512-p4DJa6OldXWd6M9zRl0H6vB9lkrmqYFkRQ2xEiNdBFp9U0LhsGO7hsBscVEyH9H2/3eZZt8c97NB2FD9U2NJ+Q==", + "dev": true, + "dependencies": { + "workbox-background-sync": "6.6.0", + "workbox-core": "6.6.0", + "workbox-routing": "6.6.0", + "workbox-strategies": "6.6.0" + } + }, + "node_modules/workbox-navigation-preload": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/workbox-navigation-preload/-/workbox-navigation-preload-6.6.0.tgz", + "integrity": "sha512-utNEWG+uOfXdaZmvhshrh7KzhDu/1iMHyQOV6Aqup8Mm78D286ugu5k9MFD9SzBT5TcwgwSORVvInaXWbvKz9Q==", + "dev": true, + "dependencies": { + "workbox-core": "6.6.0" + } + }, + "node_modules/workbox-precaching": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/workbox-precaching/-/workbox-precaching-6.6.0.tgz", + "integrity": "sha512-eYu/7MqtRZN1IDttl/UQcSZFkHP7dnvr/X3Vn6Iw6OsPMruQHiVjjomDFCNtd8k2RdjLs0xiz9nq+t3YVBcWPw==", + "dev": true, + "dependencies": { + "workbox-core": "6.6.0", + "workbox-routing": "6.6.0", + "workbox-strategies": "6.6.0" + } + }, + "node_modules/workbox-range-requests": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/workbox-range-requests/-/workbox-range-requests-6.6.0.tgz", + "integrity": "sha512-V3aICz5fLGq5DpSYEU8LxeXvsT//mRWzKrfBOIxzIdQnV/Wj7R+LyJVTczi4CQ4NwKhAaBVaSujI1cEjXW+hTw==", + "dev": true, + "dependencies": { + "workbox-core": "6.6.0" + } + }, + "node_modules/workbox-recipes": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/workbox-recipes/-/workbox-recipes-6.6.0.tgz", + "integrity": "sha512-TFi3kTgYw73t5tg73yPVqQC8QQjxJSeqjXRO4ouE/CeypmP2O/xqmB/ZFBBQazLTPxILUQ0b8aeh0IuxVn9a6A==", + "dev": true, + "dependencies": { + "workbox-cacheable-response": "6.6.0", + "workbox-core": "6.6.0", + "workbox-expiration": "6.6.0", + "workbox-precaching": "6.6.0", + "workbox-routing": "6.6.0", + "workbox-strategies": "6.6.0" + } + }, + "node_modules/workbox-routing": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/workbox-routing/-/workbox-routing-6.6.0.tgz", + "integrity": "sha512-x8gdN7VDBiLC03izAZRfU+WKUXJnbqt6PG9Uh0XuPRzJPpZGLKce/FkOX95dWHRpOHWLEq8RXzjW0O+POSkKvw==", + "dev": true, + "dependencies": { + "workbox-core": "6.6.0" + } + }, + "node_modules/workbox-strategies": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/workbox-strategies/-/workbox-strategies-6.6.0.tgz", + "integrity": "sha512-eC07XGuINAKUWDnZeIPdRdVja4JQtTuc35TZ8SwMb1ztjp7Ddq2CJ4yqLvWzFWGlYI7CG/YGqaETntTxBGdKgQ==", + "dev": true, + "dependencies": { + "workbox-core": "6.6.0" + } + }, + "node_modules/workbox-streams": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/workbox-streams/-/workbox-streams-6.6.0.tgz", + "integrity": "sha512-rfMJLVvwuED09CnH1RnIep7L9+mj4ufkTyDPVaXPKlhi9+0czCu+SJggWCIFbPpJaAZmp2iyVGLqS3RUmY3fxg==", + "dev": true, + "dependencies": { + "workbox-core": "6.6.0", + "workbox-routing": "6.6.0" + } + }, + "node_modules/workbox-sw": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/workbox-sw/-/workbox-sw-6.6.0.tgz", + "integrity": "sha512-R2IkwDokbtHUE4Kus8pKO5+VkPHD2oqTgl+XJwh4zbF1HyjAbgNmK/FneZHVU7p03XUt9ICfuGDYISWG9qV/CQ==", + "dev": true + }, + "node_modules/workbox-webpack-plugin": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/workbox-webpack-plugin/-/workbox-webpack-plugin-6.6.0.tgz", + "integrity": "sha512-xNZIZHalboZU66Wa7x1YkjIqEy1gTR+zPM+kjrYJzqN7iurYZBctBLISyScjhkJKYuRrZUP0iqViZTh8rS0+3A==", + "dev": true, + "dependencies": { + "fast-json-stable-stringify": "^2.1.0", + "pretty-bytes": "^5.4.1", + "upath": "^1.2.0", + "webpack-sources": "^1.4.3", + "workbox-build": "6.6.0" + }, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "webpack": "^4.4.0 || ^5.9.0" + } + }, + "node_modules/workbox-webpack-plugin/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/workbox-webpack-plugin/node_modules/webpack-sources": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz", + "integrity": "sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==", + "dev": true, + "dependencies": { + "source-list-map": "^2.0.0", + "source-map": "~0.6.1" + } + }, + "node_modules/workbox-window": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/workbox-window/-/workbox-window-6.6.0.tgz", + "integrity": "sha512-L4N9+vka17d16geaJXXRjENLFldvkWy7JyGxElRD0JvBxvFEd8LOhr+uXCcar/NzAmIBRv9EZ+M+Qr4mOoBITw==", + "dev": true, + "dependencies": { + "@types/trusted-types": "^2.0.2", + "workbox-core": "6.6.0" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/wrap-ansi/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "node_modules/write-file-atomic": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", + "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", + "dev": true, + "dependencies": { + "imurmurhash": "^0.1.4", + "is-typedarray": "^1.0.0", + "signal-exit": "^3.0.2", + "typedarray-to-buffer": "^3.1.5" + } + }, + "node_modules/ws": { + "version": "7.5.9", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", + "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", + "dev": true, + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-name-validator": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz", + "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==", + "dev": true + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, + "node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zustand": { + "version": "4.3.9", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.3.9.tgz", + "integrity": "sha512-Tat5r8jOMG1Vcsj8uldMyqYKC5IZvQif8zetmLHs9WoZlntTHmIoNM8TpLRY31ExncuUvUOXehd0kvahkuHjDw==", + "dependencies": { + "use-sync-external-store": "1.2.0" + }, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "immer": ">=9.0", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "immer": { + "optional": true + }, + "react": { + "optional": true + } + } + } + } +} diff --git a/crates/compiler/checkmate/www/package.json b/crates/compiler/checkmate/www/package.json new file mode 100644 index 0000000000..52587642f8 --- /dev/null +++ b/crates/compiler/checkmate/www/package.json @@ -0,0 +1,54 @@ +{ + "name": "checkmate", + "version": "0.1.0", + "private": true, + "dependencies": { + "elkjs": "^0.8.2", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-router-dom": "^6.14.2", + "react-router-hash-link": "^2.4.3", + "react-tooltip": "^5.19.0", + "reactflow": "^11.7.4" + }, + "scripts": { + "start": "react-scripts start", + "build": "npm run build:codegen && react-scripts build", + "build:codegen": "node ./scripts/gen_schema_dts.js", + "check": "tsc", + "lint": "eslint src/ scripts/", + "eject": "react-scripts eject" + }, + "eslintConfig": { + "extends": [ + "react-app", + "react-app/jest" + ] + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + }, + "devDependencies": { + "@babel/plugin-proposal-private-property-in-object": "^7.21.11", + "@types/jest": "^27.5.2", + "@types/node": "^16.18.38", + "@types/react": "^18.2.15", + "@types/react-dom": "^18.2.7", + "@types/react-router-hash-link": "^2.4.6", + "clsx": "^2.0.0", + "json-schema-to-typescript": "^13.0.2", + "react-scripts": "5.0.1", + "tailwindcss": "^3.3.3", + "tiny-typed-emitter": "^2.1.0", + "typescript": "^4.9.5" + } +} diff --git a/crates/compiler/checkmate/www/public/index.html b/crates/compiler/checkmate/www/public/index.html new file mode 100644 index 0000000000..e2bb147964 --- /dev/null +++ b/crates/compiler/checkmate/www/public/index.html @@ -0,0 +1,14 @@ + + + + + + + + Checkmate + + + +
+ + diff --git a/crates/compiler/checkmate/www/scripts/gen_schema_dts.js b/crates/compiler/checkmate/www/scripts/gen_schema_dts.js new file mode 100644 index 0000000000..9bed482f08 --- /dev/null +++ b/crates/compiler/checkmate/www/scripts/gen_schema_dts.js @@ -0,0 +1,24 @@ +const {compileFromFile} = require("json-schema-to-typescript"); +const fs = require("node:fs/promises"); +const path = require("node:path"); + +const SCHEMA_PATH = path.resolve( + __dirname, + "..", + "..", + "schema.json" +); + +const DTS_PATH = path.resolve( + __dirname, + "..", + "src", + "schema.d.ts" +); + +async function main() { + const result = await compileFromFile(SCHEMA_PATH); + await fs.writeFile(DTS_PATH, result); +} + +main().catch(console.error); diff --git a/crates/compiler/checkmate/www/src/App.tsx b/crates/compiler/checkmate/www/src/App.tsx new file mode 100644 index 0000000000..d01c913086 --- /dev/null +++ b/crates/compiler/checkmate/www/src/App.tsx @@ -0,0 +1,37 @@ +import React from "react"; +import FileInput, { LoadedEvents } from "./components/FileInput"; +import Ui from "./components/Ui"; +import { BrowserRouter } from "react-router-dom"; + +export default function App() { + const [events, setEvents] = React.useState(null); + + return ( + +
+
+ +
+
+ +
+
+
+ ); +} + +interface EventsWrapperProps { + events: LoadedEvents | null; +} + +function EventsWrapper({ events }: EventsWrapperProps): JSX.Element { + if (events === null) { + return
; + } + switch (events.kind) { + case "ok": + return ; + case "err": + return
{events.error}
; + } +} diff --git a/crates/compiler/checkmate/www/src/checkmate.json b/crates/compiler/checkmate/www/src/checkmate.json new file mode 100644 index 0000000000..fc7d47d91a --- /dev/null +++ b/crates/compiler/checkmate/www/src/checkmate.json @@ -0,0 +1,545 @@ +[ + { + "type": "Unification", + "left": 1540, + "right": 71, + "mode": { "type": "Eq" }, + "success": true, + "subevents": [ + { + "type": "VariableSetDescriptor", + "variable": 71, + "rank": 1, + "content": { + "type": "Function", + "arguments": [1543, 1544], + "lambda_type": 1542, + "ret": 1541 + } + }, + { "type": "VariableUnified", "from": 1540, "to": 71 }, + { "type": "VariableUnified", "from": 71, "to": 71 } + ] + }, + { + "type": "Unification", + "left": 71, + "right": 1546, + "mode": { "type": "Eq" }, + "success": true, + "subevents": [ + { + "type": "Unification", + "left": 1543, + "right": 67, + "mode": { "type": "Eq" }, + "success": true, + "subevents": [ + { + "type": "VariableSetDescriptor", + "variable": 67, + "rank": 1, + "content": { + "type": "Alias", + "name": "`Num.Num`", + "variables": { + "type_variables": [1545], + "lambda_set_variables": [], + "infer_ext_in_output_position_variables": [] + }, + "real_variable": 1545, + "kind": { "type": "Opaque" } + } + }, + { "type": "VariableUnified", "from": 1543, "to": 67 }, + { "type": "VariableUnified", "from": 67, "to": 67 } + ] + }, + { + "type": "Unification", + "left": 1544, + "right": 69, + "mode": { "type": "Eq" }, + "success": true, + "subevents": [ + { + "type": "VariableSetDescriptor", + "variable": 69, + "rank": 1, + "content": { + "type": "Alias", + "name": "`Num.Num`", + "variables": { + "type_variables": [1545], + "lambda_set_variables": [], + "infer_ext_in_output_position_variables": [] + }, + "real_variable": 1545, + "kind": { "type": "Opaque" } + } + }, + { "type": "VariableUnified", "from": 1544, "to": 69 }, + { "type": "VariableUnified", "from": 69, "to": 69 } + ] + }, + { + "type": "Unification", + "left": 1541, + "right": 73, + "mode": { "type": "Eq" }, + "success": true, + "subevents": [ + { + "type": "VariableSetDescriptor", + "variable": 73, + "rank": 1, + "content": { + "type": "Alias", + "name": "`Num.Num`", + "variables": { + "type_variables": [1545], + "lambda_set_variables": [], + "infer_ext_in_output_position_variables": [] + }, + "real_variable": 1545, + "kind": { "type": "Opaque" } + } + }, + { "type": "VariableUnified", "from": 1541, "to": 73 }, + { "type": "VariableUnified", "from": 73, "to": 73 } + ] + }, + { + "type": "Unification", + "left": 1542, + "right": 72, + "mode": { "type": "Eq" }, + "success": true, + "subevents": [ + { + "type": "VariableSetDescriptor", + "variable": 72, + "rank": 1, + "content": { + "type": "LambdaSet", + "solved": [{ "function": "`Num.add`", "environment": [] }], + "unspecialized": [], + "recursion_var": null, + "ambient_function": 1540 + } + }, + { "type": "VariableUnified", "from": 1542, "to": 72 }, + { "type": "VariableUnified", "from": 72, "to": 72 } + ] + }, + { + "type": "VariableSetDescriptor", + "variable": 1546, + "rank": 1, + "content": { + "type": "Function", + "arguments": [67, 69], + "lambda_type": 1542, + "ret": 73 + } + }, + { "type": "VariableUnified", "from": 71, "to": 1546 }, + { "type": "VariableUnified", "from": 1546, "to": 1546 } + ] + }, + { + "type": "Unification", + "left": 66, + "right": 1547, + "mode": { "type": "Eq" }, + "success": true, + "subevents": [ + { + "type": "VariableSetDescriptor", + "variable": 1547, + "rank": 1, + "content": { + "type": "RangedNumber", + "range": { + "kind": { "type": "AnyNum" }, + "signed": true, + "min_width": 8 + } + } + }, + { "type": "VariableUnified", "from": 66, "to": 1547 }, + { "type": "VariableUnified", "from": 1547, "to": 1547 } + ] + }, + { + "type": "Unification", + "left": 1548, + "right": 67, + "mode": { "type": "Eq" }, + "success": true, + "subevents": [ + { + "type": "Unification", + "left": 66, + "right": 1545, + "mode": { "type": "Eq" }, + "success": true, + "subevents": [ + { + "type": "VariableSetDescriptor", + "variable": 1545, + "rank": 1, + "content": { + "type": "RangedNumber", + "range": { + "kind": { "type": "AnyNum" }, + "signed": true, + "min_width": 8 + } + } + }, + { "type": "VariableUnified", "from": 1547, "to": 1545 }, + { "type": "VariableUnified", "from": 1545, "to": 1545 } + ] + }, + { + "type": "VariableSetDescriptor", + "variable": 67, + "rank": 1, + "content": { + "type": "Alias", + "name": "`Num.Num`", + "variables": { + "type_variables": [66], + "lambda_set_variables": [], + "infer_ext_in_output_position_variables": [] + }, + "real_variable": 66, + "kind": { "type": "Opaque" } + } + }, + { "type": "VariableUnified", "from": 1548, "to": 67 }, + { "type": "VariableUnified", "from": 67, "to": 67 } + ] + }, + { + "type": "Unification", + "left": 68, + "right": 1549, + "mode": { "type": "Eq" }, + "success": true, + "subevents": [ + { + "type": "VariableSetDescriptor", + "variable": 1549, + "rank": 1, + "content": { + "type": "RangedNumber", + "range": { + "kind": { "type": "AnyNum" }, + "signed": true, + "min_width": 8 + } + } + }, + { "type": "VariableUnified", "from": 68, "to": 1549 }, + { "type": "VariableUnified", "from": 1549, "to": 1549 } + ] + }, + { + "type": "Unification", + "left": 1550, + "right": 69, + "mode": { "type": "Eq" }, + "success": true, + "subevents": [ + { + "type": "Unification", + "left": 68, + "right": 1545, + "mode": { "type": "Eq" }, + "success": true, + "subevents": [ + { + "type": "VariableSetDescriptor", + "variable": 1545, + "rank": 1, + "content": { + "type": "RangedNumber", + "range": { + "kind": { "type": "AnyNum" }, + "signed": true, + "min_width": 8 + } + } + }, + { "type": "VariableUnified", "from": 1549, "to": 1545 }, + { "type": "VariableUnified", "from": 1545, "to": 1545 } + ] + }, + { + "type": "VariableSetDescriptor", + "variable": 69, + "rank": 1, + "content": { + "type": "Alias", + "name": "`Num.Num`", + "variables": { + "type_variables": [68], + "lambda_set_variables": [], + "infer_ext_in_output_position_variables": [] + }, + "real_variable": 68, + "kind": { "type": "Opaque" } + } + }, + { "type": "VariableUnified", "from": 1550, "to": 69 }, + { "type": "VariableUnified", "from": 69, "to": 69 } + ] + }, + { + "type": "Unification", + "left": 73, + "right": 1551, + "mode": { "type": "Eq" }, + "success": true, + "subevents": [ + { + "type": "Unification", + "left": 73, + "right": 1552, + "mode": { "type": "Eq" }, + "success": true, + "subevents": [ + { + "type": "Unification", + "left": 1545, + "right": 1553, + "mode": { "type": "Eq" }, + "success": true, + "subevents": [ + { + "type": "Unification", + "left": 1556, + "right": 1554, + "mode": { "type": "Eq" }, + "success": true, + "subevents": [ + { + "type": "VariableSetDescriptor", + "variable": 1554, + "rank": 1, + "content": { + "type": "Alias", + "name": "`Num.Unsigned8`", + "variables": { + "type_variables": [], + "lambda_set_variables": [], + "infer_ext_in_output_position_variables": [] + }, + "real_variable": 1555, + "kind": { "type": "Opaque" } + } + }, + { "type": "VariableUnified", "from": 1556, "to": 1554 }, + { "type": "VariableUnified", "from": 1554, "to": 1554 } + ] + } + ] + }, + { + "type": "Unification", + "left": 1545, + "right": 1553, + "mode": { "type": "Eq" }, + "success": true, + "subevents": [ + { + "type": "VariableSetDescriptor", + "variable": 1553, + "rank": 1, + "content": { + "type": "Alias", + "name": "`Num.Integer`", + "variables": { + "type_variables": [1556], + "lambda_set_variables": [], + "infer_ext_in_output_position_variables": [] + }, + "real_variable": 1556, + "kind": { "type": "Opaque" } + } + }, + { "type": "VariableUnified", "from": 1545, "to": 1553 }, + { "type": "VariableUnified", "from": 1553, "to": 1553 } + ] + }, + { + "type": "VariableSetDescriptor", + "variable": 1552, + "rank": 1, + "content": { + "type": "Alias", + "name": "`Num.Num`", + "variables": { + "type_variables": [1545], + "lambda_set_variables": [], + "infer_ext_in_output_position_variables": [] + }, + "real_variable": 1545, + "kind": { "type": "Opaque" } + } + }, + { "type": "VariableUnified", "from": 73, "to": 1552 }, + { "type": "VariableUnified", "from": 1552, "to": 1552 } + ] + } + ] + }, + { + "type": "Unification", + "left": 1551, + "right": 75, + "mode": { "type": "Eq" }, + "success": true, + "subevents": [ + { + "type": "VariableSetDescriptor", + "variable": 75, + "rank": 1, + "content": { + "type": "Alias", + "name": "`Num.U8`", + "variables": { + "type_variables": [], + "lambda_set_variables": [], + "infer_ext_in_output_position_variables": [] + }, + "real_variable": 1552, + "kind": { "type": "Structural" } + } + }, + { "type": "VariableUnified", "from": 1551, "to": 75 }, + { "type": "VariableUnified", "from": 75, "to": 75 } + ] + }, + { + "type": "Unification", + "left": 80, + "right": 75, + "mode": { "type": "Eq" }, + "success": true, + "subevents": [ + { + "type": "VariableSetDescriptor", + "variable": 75, + "rank": 1, + "content": { + "type": "Alias", + "name": "`Num.U8`", + "variables": { + "type_variables": [], + "lambda_set_variables": [], + "infer_ext_in_output_position_variables": [] + }, + "real_variable": 1552, + "kind": { "type": "Structural" } + } + }, + { "type": "VariableUnified", "from": 80, "to": 75 }, + { "type": "VariableUnified", "from": 75, "to": 75 } + ] + }, + { + "type": "Unification", + "left": 1557, + "right": 79, + "mode": { "type": "Eq" }, + "success": true, + "subevents": [ + { + "type": "VariableSetDescriptor", + "variable": 79, + "rank": 1, + "content": { + "type": "Alias", + "name": "`Bool.Bool`", + "variables": { + "type_variables": [], + "lambda_set_variables": [], + "infer_ext_in_output_position_variables": [] + }, + "real_variable": 1558, + "kind": { "type": "Opaque" } + } + }, + { "type": "VariableUnified", "from": 1557, "to": 79 }, + { "type": "VariableUnified", "from": 79, "to": 79 } + ] + }, + { + "type": "Unification", + "left": 79, + "right": 5, + "mode": { "type": "Eq" }, + "success": true, + "subevents": [ + { + "type": "Unification", + "left": 1558, + "right": 4, + "mode": { "type": "Eq" }, + "success": true, + "subevents": [ + { + "type": "Unification", + "left": 84, + "right": 3, + "mode": { "type": "Eq" }, + "success": true, + "subevents": [ + { + "type": "VariableSetDescriptor", + "variable": 3, + "rank": 0, + "content": { "type": "EmptyTagUnion" } + }, + { "type": "VariableUnified", "from": 84, "to": 3 }, + { "type": "VariableUnified", "from": 3, "to": 3 } + ] + }, + { + "type": "VariableSetDescriptor", + "variable": 4, + "rank": 0, + "content": { + "type": "TagUnion", + "tags": { "False": [], "True": [] }, + "extension": { "type": "Any", "variable": 84 } + } + }, + { "type": "VariableUnified", "from": 1558, "to": 4 }, + { "type": "VariableUnified", "from": 4, "to": 4 } + ] + }, + { + "type": "VariableSetDescriptor", + "variable": 5, + "rank": 0, + "content": { + "type": "Alias", + "name": "`Bool.Bool`", + "variables": { + "type_variables": [], + "lambda_set_variables": [], + "infer_ext_in_output_position_variables": [] + }, + "real_variable": 1558, + "kind": { "type": "Opaque" } + } + }, + { "type": "VariableUnified", "from": 79, "to": 5 }, + { "type": "VariableUnified", "from": 5, "to": 5 } + ] + } +] diff --git a/crates/compiler/checkmate/www/src/components/Common/EpochCell.tsx b/crates/compiler/checkmate/www/src/components/Common/EpochCell.tsx new file mode 100644 index 0000000000..e095e9143f --- /dev/null +++ b/crates/compiler/checkmate/www/src/components/Common/EpochCell.tsx @@ -0,0 +1,28 @@ +import clsx from "clsx"; + +interface EpochCellProps { + className?: string; + children?: React.ReactNode; + focus?: boolean; +} + +export const EPOCH_STYLES = + "text-slate-900 font-mono bg-slate-200 p-1 py-0 rounded-sm text-sm transition ease-in-out duration-700 mr-2"; + +export default function EpochCell({ + className, + children, + focus, +}: EpochCellProps) { + return ( + + {children} + + ); +} diff --git a/crates/compiler/checkmate/www/src/components/Common/UnknownVariable.tsx b/crates/compiler/checkmate/www/src/components/Common/UnknownVariable.tsx new file mode 100644 index 0000000000..f6dff4183f --- /dev/null +++ b/crates/compiler/checkmate/www/src/components/Common/UnknownVariable.tsx @@ -0,0 +1,18 @@ +import clsx from "clsx"; +import { Variable } from "../../schema"; +import { VariableName } from "./VariableName"; + +export interface UnknownVariableProps { + variable: Variable; +} + +export function UnknownVariable({ + variable, +}: UnknownVariableProps): JSX.Element { + return ( +
+ + ??? +
+ ); +} diff --git a/crates/compiler/checkmate/www/src/components/Common/Variable.tsx b/crates/compiler/checkmate/www/src/components/Common/Variable.tsx new file mode 100644 index 0000000000..239b0573cc --- /dev/null +++ b/crates/compiler/checkmate/www/src/components/Common/Variable.tsx @@ -0,0 +1,73 @@ +import { ComponentProps } from "react"; +import clsx from "clsx"; +import { QuerySubs, TypeDescriptor } from "../../engine/subs"; +import { Variable } from "../../schema"; +import DrawHeadConstructor from "../Content/HeadConstructor"; +import { contentStyles } from "./../Content"; +import { VariableName } from "./VariableName"; + +interface VariableElProps { + variable: Variable; + subs: QuerySubs; + onClick?: (variable: Variable) => void; + nested?: boolean; + raw?: boolean; +} + +export function VariableElPretty(props: VariableElProps): JSX.Element { + const { variable, subs } = props; + const desc = subs.get_root(variable); + const content = ( + ( + + )} + drawVariableRaw={(variable) => ( + + )} + /> + ); + return ( + + {content} + + ); +} + +function VariableElRaw(props: VariableElProps): JSX.Element { + const desc = props.subs.get_root(props.variable); + return ; +} + +function Helper({ + children, + variable, + desc, + onClick, + nested, + raw, +}: VariableElProps & + Pick, "children"> & { + desc: TypeDescriptor | undefined; + }): JSX.Element { + const { bg } = contentStyles(desc); + return ( + + {(!nested || raw) && ( + + )} + {children ? {children} : <>} + + ); +} diff --git a/crates/compiler/checkmate/www/src/components/Common/VariableLink.tsx b/crates/compiler/checkmate/www/src/components/Common/VariableLink.tsx new file mode 100644 index 0000000000..dc504b7d17 --- /dev/null +++ b/crates/compiler/checkmate/www/src/components/Common/VariableLink.tsx @@ -0,0 +1,34 @@ +import clsx from "clsx"; +import { QuerySubs } from "../../engine/subs"; +import { Variable } from "../../schema"; +import { VariableName } from "./VariableName"; + +export interface VariableLinkProps { + variable: Variable; + subs: QuerySubs; + onClick?: (variable: Variable) => void; +} + +export function VariableLink({ + variable, + subs, + onClick, +}: VariableLinkProps): JSX.Element { + const root = subs.get_root_key(variable); + + if (variable === root) { + throw new Error("VariableLink: variable is root"); + } + + return ( +
+ + + +
+ ); +} diff --git a/crates/compiler/checkmate/www/src/components/Common/VariableName.tsx b/crates/compiler/checkmate/www/src/components/Common/VariableName.tsx new file mode 100644 index 0000000000..3e2032788b --- /dev/null +++ b/crates/compiler/checkmate/www/src/components/Common/VariableName.tsx @@ -0,0 +1,30 @@ +import clsx from "clsx"; +import { Variable } from "../../schema"; + +export interface VariableNameProps { + variable: Variable; + onClick?: (variable: Variable) => void; + className?: string; +} + +export function VariableName({ + variable, + onClick, + className, +}: VariableNameProps): JSX.Element { + return ( + { + e.stopPropagation(); + onClick?.(variable); + }} + > + {variable} + + ); +} diff --git a/crates/compiler/checkmate/www/src/components/Content/HeadConstructor.tsx b/crates/compiler/checkmate/www/src/components/Content/HeadConstructor.tsx new file mode 100644 index 0000000000..3d3f1af0be --- /dev/null +++ b/crates/compiler/checkmate/www/src/components/Content/HeadConstructor.tsx @@ -0,0 +1,344 @@ +import { TypeDescriptor } from "../../engine/subs"; +import { + ClosureType, + RecordFieldKind, + UnspecializedClosureType, + Variable, +} from "../../schema"; + +type DrawVariable = (variable: Variable) => JSX.Element; + +export interface DrawHeadConstructorProps { + desc: TypeDescriptor | undefined; + drawVariablePretty: DrawVariable; + drawVariableRaw: DrawVariable; +} + +export default function DrawHeadConstructor({ + desc, + drawVariablePretty, + drawVariableRaw, +}: DrawHeadConstructorProps): JSX.Element { + if (!desc) { + return <>???; + } + const content = desc.content; + switch (content.type) { + case "Flex": + case "Rigid": { + const { name } = content; + return name ? <>{name} : <>_; + } + case "FlexAble": + case "RigidAble": { + const { name, abilities } = content; + const nameEl = name ? <>{name} : <>_; + return ( + <> + {nameEl} has {abilities.join(", ")} + + ); + } + case "Recursive": { + const { name, structure } = content; + const structureEl = drawVariableRaw(structure); + const nameEl = name ? <>{name} to : <>; + return ( + <> + <{nameEl} + {structureEl}> + + ); + } + case "LambdaSet": { + const { ambient_function, solved, unspecialized, recursion_var } = + content; + const ambientFunctionEl = drawVariableRaw(ambient_function); + const solvedEl = ( + + ); + const unspecializedEl = ( + + ); + const recursionVarEl = recursion_var ? ( + <> as <{drawVariableRaw(recursion_var)}> + ) : ( + <> + ); + return ( + <> + [{solvedEl} + {unspecializedEl}]{recursionVarEl} ^{ambientFunctionEl} + + ); + } + case "ErasedLambda": { + return <>?; + } + case "Alias": { + const { kind, name, variables } = content; + const prefix = kind.type === "Opaque" ? "@" : ""; + const variablesEl = ( + + ); + return ( + + {prefix} + {sym(name)} + {variablesEl} + + ); + } + case "Apply": { + const { symbol, variables } = content; + const variablesEl = ( + + ); + return ( + <> + {sym(symbol)} + {variablesEl} + + ); + } + case "Function": { + const { arguments: args, lambda_type, ret } = content; + const argsEl = args.map((arg, i) => ( + + {i !== 0 ? ", " : ""} + {drawVariablePretty(arg)} + + )); + const lambdaTypeEl = drawVariablePretty(lambda_type); + const retEl = drawVariablePretty(ret); + return ( + <> + {argsEl} + {" -"} + {lambdaTypeEl} + {"-> "} + {retEl} + + ); + } + case "Record": { + const { fields, extension } = content; + const fieldsEl = Object.entries(fields).map(([key, value], i) => { + const { field_type, kind } = value; + return ( + + {i !== 0 ? ", " : ""} + {key} {}{" "} + {drawVariablePretty(field_type)} + + ); + }); + return ( + <> + {"{"} + {fieldsEl} + {"}"} + {drawVariablePretty(extension)} + + ); + } + case "Tuple": { + const { elements, extension } = content; + const elemsEl = Object.entries(elements).map(([key, value], i) => { + return ( + + {i !== 0 ? ", " : ""} + {key}: {drawVariablePretty(value)} + + ); + }); + return ( + <> + ({elemsEl}){drawVariablePretty(extension)} + + ); + } + case "TagUnion": { + const { tags, extension } = content; + return ( + <> + + {drawVariablePretty(extension.variable)} + + ); + } + case "RecursiveTagUnion": { + const { tags, extension, recursion_var } = content; + return ( + <> + ( + {drawVariablePretty(extension.variable)} as < + {drawVariableRaw(recursion_var)}>) + + ); + } + case "FunctionOrTagUnion": { + const { functions, tags, extension } = content; + const functionsEl = functions.map((f, i) => ( + + {i !== 0 ? ", " : ""} + {sym(f)} + + )); + const tagsEl = tags.map((t, i) => ( + + {i !== 0 ? ", " : ""} + {t} + + )); + return ( + <> + [{functionsEl} | {tagsEl}]{drawVariablePretty(extension.variable)} + + ); + } + case "RangedNumber": { + const { + range: { kind, min_width, signed }, + } = content; + switch (kind.type) { + case "AnyNum": + return <>ℚ{min_width}+; + case "Int": + return signed ? <>ℤ{min_width}+ : <>ℕ{min_width}+; + } + break; + } + case "EmptyRecord": { + return <>{"{}"}; + } + case "EmptyTuple": { + return <>(); + } + case "EmptyTagUnion": { + return <>[]; + } + case "Error": { + return <>⊥; + } + } +} + +function DrawVarArgumentsList({ + variables, + drawVariableRaw, +}: { + variables: Variable[]; + drawVariableRaw: DrawVariable; +}): JSX.Element { + return variables.length !== 0 ? ( + <> + {" "} + {variables.map((v, i) => ( + {drawVariableRaw(v)} + ))} + + ) : ( + <> + ); +} + +function DrawSolved({ + solved, + drawVariableRaw, +}: { + solved: ClosureType[]; + drawVariableRaw: DrawVariable; +}): JSX.Element { + const tags = solved.map(({ environment, function: fn }, i) => ( + + {i !== 0 ? ", " : ""} + + + )); + return <>[{tags}]; +} + +function DrawTags({ + tags, + drawVariableRaw, +}: { + tags: Record; + drawVariableRaw: DrawVariable; +}): JSX.Element { + const tagsEl = Object.entries(tags).map(([tag, vars], i) => ( + + {i !== 0 ? ", " : ""} + + + )); + return <>[{tagsEl}]; +} + +function DrawUnspecialized({ + unspecialized, + drawVariableRaw, +}: { + unspecialized: UnspecializedClosureType[]; + drawVariableRaw: DrawVariable; +}): JSX.Element { + const unspecs = unspecialized.map( + ({ ability_member, lambda_set_region, specialization }, i) => ( + + {" + "} + {drawVariableRaw(specialization)}:{sym(ability_member)}: + {lambda_set_region} + + ) + ); + return <>{unspecs}; +} + +function DrawTag({ + tag, + variables, + drawVariableRaw, +}: { + tag: string; + variables: Variable[]; + drawVariableRaw: DrawVariable; +}): JSX.Element { + return ( + <> + {tag} + + + ); +} + +function DrawFieldKind({ kind }: { kind: RecordFieldKind }): JSX.Element { + switch (kind.type) { + case "Required": + case "Demanded": + return <>:; + case "Optional": + return <>?; + } +} + +function sym(symbol: string): string { + if (symbol.startsWith("`")) symbol = symbol.slice(1); + if (symbol.endsWith("`")) symbol = symbol.slice(0, -1); + return symbol.split(".").at(-1)!; +} diff --git a/crates/compiler/checkmate/www/src/components/Content/index.ts b/crates/compiler/checkmate/www/src/components/Content/index.ts new file mode 100644 index 0000000000..cb4893b478 --- /dev/null +++ b/crates/compiler/checkmate/www/src/components/Content/index.ts @@ -0,0 +1,68 @@ +import { TypeDescriptor } from "../../engine/subs"; +import { assertExhaustive } from "../../utils/exhaustive"; + +export interface ContentStyles { + name: string; + bg: string; +} + +export function contentStyles(desc: TypeDescriptor | undefined): ContentStyles { + if (!desc) { + return { name: "???", bg: "bg-red-500" }; + } + + const content = desc.content; + switch (content.type) { + case "Flex": + return { name: "Flex", bg: "bg-blue-300" }; + case "FlexAble": + return { name: "FlexAble", bg: "bg-blue-400" }; + case "Rigid": + return { name: "Rigid", bg: "bg-indigo-300" }; + case "RigidAble": + return { name: "RigidAble", bg: "bg-indigo-400" }; + case "Recursive": + return { name: "Rec", bg: "bg-blue-grey-500" }; + case "LambdaSet": + return { name: "LambdaSet", bg: "bg-green-500" }; + case "ErasedLambda": + return { name: "ErasedLambda", bg: "bg-green-700" }; + case "Alias": { + switch (content.kind.type) { + case "Structural": + return { name: "Alias", bg: "bg-yellow-300" }; + case "Opaque": + return { name: "Opaque", bg: "bg-amber-400" }; + default: + assertExhaustive(content.kind); + } + break; + } + case "Apply": + return { name: "Apply", bg: "bg-orange-500" }; + case "Function": + return { name: "Func", bg: "bg-teal-400" }; + case "Record": + return { name: "Record", bg: "bg-purple-400" }; + case "Tuple": + return { name: "Tuple", bg: "bg-deep-purple-400" }; + case "TagUnion": + return { name: "Tags", bg: "bg-cyan-200" }; + case "FunctionOrTagUnion": + return { name: "Func|Tags", bg: "bg-cyan-300" }; + case "RecursiveTagUnion": + return { name: "RecTags", bg: "bg-cyan-400" }; + case "RangedNumber": + return { name: "ℕ", bg: "bg-lime-400" }; + case "EmptyRecord": + return { name: "{}", bg: "bg-purple-400" }; + case "EmptyTuple": + return { name: "()", bg: "bg-deep-purple-400" }; + case "EmptyTagUnion": + return { name: "[]", bg: "bg-cyan-200" }; + case "Error": + return { name: "Error", bg: "bg-red-400" }; + } +} + +export const LinkStyles: ContentStyles = { name: "Link", bg: "bg-slate-500" }; diff --git a/crates/compiler/checkmate/www/src/components/EventItem/Variable.tsx b/crates/compiler/checkmate/www/src/components/EventItem/Variable.tsx new file mode 100644 index 0000000000..a1458ea655 --- /dev/null +++ b/crates/compiler/checkmate/www/src/components/EventItem/Variable.tsx @@ -0,0 +1,27 @@ +import { EventEpoch } from "../../engine/engine"; +import { Variable } from "../../schema"; +import { VariableElPretty } from "../Common/Variable"; +import { CommonProps } from "./types"; + +interface VariableProps extends CommonProps { + epoch: EventEpoch; + variable: Variable; +} + +export function VariableEl({ + engine, + epoch, + variable, + graphEe, +}: VariableProps): JSX.Element { + engine.stepTo(epoch); + return ( + { + graphEe.emit("focusVariable", variable); + }} + > + ); +} diff --git a/crates/compiler/checkmate/www/src/components/EventItem/types.ts b/crates/compiler/checkmate/www/src/components/EventItem/types.ts new file mode 100644 index 0000000000..4bc66f67a5 --- /dev/null +++ b/crates/compiler/checkmate/www/src/components/EventItem/types.ts @@ -0,0 +1,9 @@ +import { TypedEmitter } from "tiny-typed-emitter"; +import type { Engine, EventEpoch } from "../../engine/engine"; +import { GraphMessage } from "../../utils/events"; + +export interface CommonProps { + selectedEpochs: EventEpoch[]; + engine: Engine; + graphEe: TypedEmitter; +} diff --git a/crates/compiler/checkmate/www/src/components/EventList/depthGroup.ts b/crates/compiler/checkmate/www/src/components/EventList/depthGroup.ts new file mode 100644 index 0000000000..17e2bc9d92 --- /dev/null +++ b/crates/compiler/checkmate/www/src/components/EventList/depthGroup.ts @@ -0,0 +1,76 @@ +export interface GroupInfo { + group: string; + groupHover: string; +} + +export function depthToGroupInfo(depth: number): GroupInfo { + switch (depth) { + case 0: + return { + group: `group/event-0`, + groupHover: `group-hover/event-0:opacity-100`, + }; + case 1: + return { + group: `group/event-1`, + groupHover: `group-hover/event-1:opacity-100`, + }; + case 2: + return { + group: `group/event-2`, + groupHover: `group-hover/event-2:opacity-100`, + }; + case 3: + return { + group: `group/event-3`, + groupHover: `group-hover/event-3:opacity-100`, + }; + case 4: + return { + group: `group/event-4`, + groupHover: `group-hover/event-4:opacity-100`, + }; + case 5: + return { + group: `group/event-5`, + groupHover: `group-hover/event-5:opacity-100`, + }; + case 6: + return { + group: `group/event-6`, + groupHover: `group-hover/event-6:opacity-100`, + }; + case 7: + return { + group: `group/event-7`, + groupHover: `group-hover/event-7:opacity-100`, + }; + case 8: + return { + group: `group/event-8`, + groupHover: `group-hover/event-8:opacity-100`, + }; + case 9: + return { + group: `group/event-9`, + groupHover: `group-hover/event-9:opacity-100`, + }; + case 10: + return { + group: `group/event-10`, + groupHover: `group-hover/event-10:opacity-100`, + }; + case 11: + return { + group: `group/event-11`, + groupHover: `group-hover/event-11:opacity-100`, + }; + case 12: + return { + group: `group/event-12`, + groupHover: `group-hover/event-12:opacity-100`, + }; + default: + throw new Error(`Too deep: ${depth}`); + } +} diff --git a/crates/compiler/checkmate/www/src/components/EventList/index.tsx b/crates/compiler/checkmate/www/src/components/EventList/index.tsx new file mode 100644 index 0000000000..aa538405e8 --- /dev/null +++ b/crates/compiler/checkmate/www/src/components/EventList/index.tsx @@ -0,0 +1,423 @@ +import clsx from "clsx"; +import React, { useCallback, useMemo, useState } from "react"; +import { TypedEmitter } from "tiny-typed-emitter"; +import { EventEpoch } from "../../engine/engine"; +import { lastSubEvent } from "../../engine/event_util"; +import { useFocusOutlineEvent } from "../../hooks/useFocusOutlineEvent"; +import { UnificationMode, Event } from "../../schema"; +import { + EventListMessage, + GlobalMessage, + GraphMessage, + LoadEpochView, +} from "../../utils/events"; +import { Refine } from "../../utils/refine"; +import EpochCell from "../Common/EpochCell"; +import { CommonProps } from "../EventItem/types"; +import { VariableEl } from "../EventItem/Variable"; +import { depthToGroupInfo } from "./depthGroup"; + +interface EventListProps extends CommonProps { + events: Event[]; + eventListEe: TypedEmitter; + globalEe: TypedEmitter; + depth: number; +} + +const MT = "my-2.5"; +const LOWER_OPACITY = "opacity-40"; + +export default function EventList(props: EventListProps): JSX.Element { + const { events, depth } = props; + return ( +
    + {events.map((event, i) => ( +
  • + +
  • + ))} +
+ ); +} + +interface OneEventProps extends CommonProps { + event: Event; + eventListEe: TypedEmitter; + globalEe: TypedEmitter; + depth: number; +} + +function OneEvent(props: OneEventProps): JSX.Element { + const { event } = props; + switch (event.type) { + case "Unification": + return ; + case "VariableUnified": + return <>; + case "VariableSetDescriptor": + return <>; + } +} + +const DROPDOWN_CLOSED = "▶"; +const DROPDOWN_OPEN = "▼"; + +const UN_UNKNOWN = "💭"; +const UN_SUCCESS = "✅"; +const UN_FAILURE = "❌"; + +function epochInRange( + epoch: EventEpoch, + [start, end]: [EventEpoch, EventEpoch] +): boolean { + return epoch >= start && epoch <= end; +} + +interface UnificationProps extends CommonProps { + event: Refine; + eventListEe: TypedEmitter; + globalEe: TypedEmitter; + depth: number; +} + +const COL_1_P = "pl-1.5"; + +const COL_1_OUTLINE_STYLES = clsx(COL_1_P, "outline-event-col-1"); +const COL_3_OUTLINE_STYLES = clsx("outline-event-col-3"); + +const COL_1_ROUNDED = "rounded-l-md"; +const COL_3_ROUNDED = "rounded-r-md"; + +const UN_EXPANDED_OUTLINE_STYLES = clsx("ring-inset ring-2 ring-blue-500"); + +const TRANSITION_SHADOW = "transition-shadow ease-in-out duration-500"; +const TRANSITION_OPACITY = "transition-opacity ease-in-out duration-150"; + +const GROUP_STLYES = "relative overflow-hidden"; + +// Space for the hover cells at the end of line. +const EOL_SPACE = "pr-12"; + +function UnificationEvent(props: UnificationProps): JSX.Element { + const { + engine, + event, + selectedEpochs, + graphEe, + eventListEe, + depth, + globalEe, + } = props; + const { mode, subevents, success } = event; + + const beforeUnificationEpoch = engine.getEventIndex(event); + const afterUnificationEpoch = engine.getEventIndex(lastSubEvent(event)); + + const containedEpoch = selectedEpochs.find((epoch) => + epochInRange(epoch, [beforeUnificationEpoch, afterUnificationEpoch]) + ); + + const leftVar = useMemo( + () => (epoch: EventEpoch) => + , + [event.left, props] + ); + const rightVar = useMemo( + () => (epoch: EventEpoch) => + , + [event.right, props] + ); + + const [isOpen, setIsOpen] = useState(false); + const isOutlined = useFocusOutlineEvent<"focusEpoch", EventEpoch | undefined>( + { + ee: eventListEe, + value: containedEpoch, + event: "focusEpoch", + } + ); + + const modeIcon = useMemo(() => , [mode]); + + const resultIcon = success ? UN_SUCCESS : UN_FAILURE; + const resultHeadline = ; + + const epochCell = useMemo(() => { + if (containedEpoch === undefined) return null; + return ; + }, [containedEpoch, graphEe]); + + const getBeforeUnificationHeadline = useCallback( + ({ + epoch, + collapsedMode, + }: { + epoch: EventEpoch; + collapsedMode?: boolean; + }) => { + const topHeadline = ( + + ); + + const optEpochCell = + collapsedMode && containedEpoch !== undefined && epochCell; + + return ( + + ); + }, + [isOpen, resultIcon, containedEpoch, epochCell, leftVar, modeIcon, rightVar] + ); + + const { group, groupHover } = depthToGroupInfo(depth); + + if (!isOpen) { + const headLine = getBeforeUnificationHeadline({ + epoch: afterUnificationEpoch, + collapsedMode: true, + }); + return ( +
+
+ {headLine} +
+ +
+ ); + } else { + const beforeIsCurrentEpoch = beforeUnificationEpoch === containedEpoch; + const afterIsCurrentEpoch = afterUnificationEpoch === containedEpoch; + + const epochCellBefore = beforeIsCurrentEpoch && epochCell; + const epochCellAfter = afterIsCurrentEpoch && epochCell; + + const outlineEpochCellAfter = afterIsCurrentEpoch && isOutlined; + const outlineEpochCellBefore = beforeIsCurrentEpoch && isOutlined; + + const headlineBefore = getBeforeUnificationHeadline({ + epoch: beforeUnificationEpoch, + }); + + const dropdownTransparent = ( + {DROPDOWN_OPEN} + ); + + const headlineAfter = ( +
+ {dropdownTransparent} + {resultHeadline} {leftVar(afterUnificationEpoch)} {modeIcon}{" "} + {rightVar(afterUnificationEpoch)} +
+ ); + + return ( +
+
+ {/* Row 1: unification start */} +
+ {epochCellBefore} +
+
+ {headlineBefore} +
+ + {/* Row 2: inner traces */} +
+
+ +
+ + {/* Row 3: unification end */} +
+ {epochCellAfter} +
+
+ {headlineAfter} +
+ + {/* Col 2: dropdown line */} +
+
+ + + +
+ ); + } +} + +function Headline({ icon }: { icon: string }): JSX.Element { + return ( +
+
{icon}
+
+ ); +} + +function UnificationModeIcon({ mode }: { mode: UnificationMode }): JSX.Element { + switch (mode.type) { + case "Eq": + return <>~; + case "Present": + return <>+=; + case "LambdaSetSpecialization": + return <>|~|; + } +} + +interface EventListEpochCellProps { + epoch: EventEpoch; + graphEe: TypedEmitter; +} + +function EventListEpochCell({ + epoch, + graphEe, +}: EventListEpochCellProps): JSX.Element { + return ( + { + e.stopPropagation(); + graphEe.emit("focusEpoch", epoch); + }} + > + {epoch} + + ); +} + +interface LoadEpochGraphLauncherProps { + groupHover: string; + epoch: EventEpoch; + globalEe: TypedEmitter; + className?: string; +} + +function LoadEpochGraphLauncher({ + groupHover, + epoch, + className, + globalEe, +}: LoadEpochGraphLauncherProps): JSX.Element { + return ( +
+ + { + globalEe.emit("loadEpoch", epoch, LoadEpochView.Top); + }} + > + ↑ + + { + globalEe.emit("loadEpoch", epoch, LoadEpochView.Bot); + }} + > + ↓ + + +
+ ); +} diff --git a/crates/compiler/checkmate/www/src/components/FileInput.tsx b/crates/compiler/checkmate/www/src/components/FileInput.tsx new file mode 100644 index 0000000000..40e9efbd23 --- /dev/null +++ b/crates/compiler/checkmate/www/src/components/FileInput.tsx @@ -0,0 +1,48 @@ +import { AllEvents } from "../schema"; + +export type EventsOk = { + kind: "ok"; + events: AllEvents; +}; + +export type EventsErr = { + kind: "err"; + error: string; +}; + +export type LoadedEvents = EventsOk | EventsErr; + +interface FileInputProps { + setResult(result: LoadedEvents): void; +} + +export default function FileInput({ setResult }: FileInputProps) { + async function setFile(e: React.ChangeEvent) { + e.preventDefault(); + const files = e.target.files; + if (!files) { + setResult({ kind: "err", error: "Please choose a checkmate file." }); + return; + } + const file = files[0]; + const buf = await file.text(); + try { + const events: AllEvents = JSON.parse(buf); + setResult({ kind: "ok", events }); + } catch (e) { + setResult({ kind: "err", error: "Invalid checkmate file." }); + return; + } + } + + return ( + setFile(e)} + className="block w-full border border-gray-200 shadow-sm rounded-md text-sm + file:bg-roc-purple-bg file:border-0 file:mr-4 file:py-2 file:px-4 cursor-pointer" + > + ); +} diff --git a/crates/compiler/checkmate/www/src/components/Graph/VariableNode.tsx b/crates/compiler/checkmate/www/src/components/Graph/VariableNode.tsx new file mode 100644 index 0000000000..06b57e8e0f --- /dev/null +++ b/crates/compiler/checkmate/www/src/components/Graph/VariableNode.tsx @@ -0,0 +1,286 @@ +import clsx from "clsx"; +import { Handle, Position } from "reactflow"; +import { Variable } from "../../schema"; +import { assertExhaustive } from "../../utils/exhaustive"; +import { contentStyles, LinkStyles } from "../Content"; +import { VariableElPretty } from "../Common/Variable"; +import { SubsSnapshot, TypeDescriptor } from "../../engine/subs"; +import { TypedEmitter } from "tiny-typed-emitter"; +import { VariableLink } from "../Common/VariableLink"; +import { VariableMessage } from "../../utils/events"; +import { useFocusOutlineEvent } from "../../hooks/useFocusOutlineEvent"; +import { UnknownVariable } from "../Common/UnknownVariable"; + +type AddSubVariableLink = ({ + from, + variable, +}: { + from: Variable; + variable: Variable; +}) => void; + +export interface VariableNodeProps { + data: { + subs: SubsSnapshot; + rawVariable: Variable; + addSubVariableLink: AddSubVariableLink; + isOutlined: boolean; + ee: TypedEmitter; + }; + targetPosition?: Position; + sourcePosition?: Position; +} + +export default function VariableNode({ + data, + targetPosition, + sourcePosition, +}: VariableNodeProps): JSX.Element { + const { + subs, + rawVariable, + addSubVariableLink, + isOutlined: isOutlinedProp, + ee: eeProp, + } = data; + + const isOutlined = useFocusOutlineEvent({ + ee: eeProp, + value: rawVariable, + event: "focus", + defaultIsOutlined: isOutlinedProp, + }); + + const varType = subs.get(rawVariable); + + let renderContent: JSX.Element; + let bgStyles: string; + const isContent = varType?.type === "descriptor"; + switch (varType?.type) { + case undefined: { + bgStyles = "bg-red-500"; + renderContent = ; + + break; + } + case "link": { + bgStyles = LinkStyles.bg; + + renderContent = ( + + addSubVariableLink({ + from: rawVariable, + variable: subs.get_root_key(rawVariable), + }) + } + /> + ); + + break; + } + case "descriptor": { + const variable = rawVariable; + const desc: TypeDescriptor = varType; + + const styles = contentStyles(desc); + bgStyles = styles.bg; + const basis: BasisProps = { + subs, + origin: variable, + addSubVariableLink, + }; + + const content = Object.entries( + VariableNodeContent(variable, desc, basis) + ).filter((el): el is [string, JSX.Element] => !!el[1]); + + let expandedContent = <>; + if (content.length > 0) { + expandedContent = ( +
    + {content.map(([key, value], i) => ( +
  • + {key}: {value} +
  • + ))} +
+ ); + } + + renderContent = ( + <> +
+ +
+ {expandedContent} + + ); + + break; + } + default: { + assertExhaustive(varType); + } + } + + return ( +
+ + {renderContent} + +
+ ); +} + +function VariableNodeContent( + variable: Variable, + desc: TypeDescriptor | undefined, + basis: BasisProps +): Record { + if (!desc) return {}; + const { content } = desc; + + switch (content.type) { + case "Flex": + case "Rigid": { + const { name } = content; + return { name: name ? <>{name} : null }; + } + case "FlexAble": + case "RigidAble": { + const { name, abilities } = content; + return { + name: <>{name}, + abilities: <>[{abilities.join(", ")}], + }; + } + case "Recursive": { + const { name, structure } = content; + return { + name: <>{name}, + structure: , + }; + } + case "LambdaSet": { + const { ambient_function, solved, unspecialized, recursion_var } = + content; + return { + "^": , + as: recursion_var ? ( + + ) : null, + }; + } + case "ErasedLambda": { + return {}; + } + case "Alias": { + const { name, real_variable, variables } = content; + return { + name: <>{name}, + }; + } + case "Apply": { + const { name, variables } = content; + return { + name: <>{name}, + }; + } + case "Function": { + const { arguments: args, lambda_type, ret } = content; + return { + args: ( + <> + {args.map((arg, i) => ( + + ))} + + ), + "||": , + ret: , + }; + } + case "FunctionOrTagUnion": { + const { tags, functions, extension } = content; + return { + tags: <>[{tags.join(", ")}], + fns: <>[{functions.join(", ")}], + }; + } + case "TagUnion": { + const { tags, extension } = content; + return {}; + } + case "RecursiveTagUnion": { + const { recursion_var, extension, tags } = content; + return { + as: , + }; + } + case "Record": { + const { fields, extension } = content; + return {}; + } + case "Tuple": { + const { elements, extension } = content; + return {}; + } + case "RangedNumber": { + const { range } = content; + return {}; + } + case "EmptyRecord": + case "EmptyTuple": + case "EmptyTagUnion": + case "Error": { + return {}; + } + default: { + return assertExhaustive(content); + } + } +} + +interface BasisProps { + subs: SubsSnapshot; + origin: Variable; + addSubVariableLink: AddSubVariableLink; +} + +function SubVariable({ + subs, + origin, + variable, + addSubVariableLink, +}: { + variable: Variable; +} & BasisProps): JSX.Element { + return ( + addSubVariableLink({ from: origin, variable })} + /> + ); +} diff --git a/crates/compiler/checkmate/www/src/components/Graph/VariablesGraph.tsx b/crates/compiler/checkmate/www/src/components/Graph/VariablesGraph.tsx new file mode 100644 index 0000000000..6b646c8d4c --- /dev/null +++ b/crates/compiler/checkmate/www/src/components/Graph/VariablesGraph.tsx @@ -0,0 +1,567 @@ +import ELK, { + type ElkNode, + type LayoutOptions, +} from "elkjs/lib/elk.bundled.js"; +import ReactFlow, { + Node, + Edge, + Background, + BackgroundVariant, + useReactFlow, + ReactFlowProvider, + NodeChange, + applyNodeChanges, + EdgeChange, + applyEdgeChanges, + Panel, + NodeTypes, + useStore, + ReactFlowState, + Position, + MarkerType, + EdgeMarkerType, +} from "reactflow"; +import { useCallback, useEffect, useRef, useState } from "react"; +import { Variable } from "../../schema"; + +import "reactflow/dist/style.css"; +import clsx from "clsx"; +import VariableNode, { VariableNodeProps } from "./VariableNode"; +import { SubsSnapshot } from "../../engine/subs"; +import { TypedEmitter } from "tiny-typed-emitter"; +import EpochCell from "../Common/EpochCell"; +import { HashLink } from "react-router-hash-link"; +import { + EventListMessage, + GraphMessage, + VariableMessage, +} from "../../utils/events"; +import { useFocusOutlineEvent } from "../../hooks/useFocusOutlineEvent"; + +export interface VariablesGraphProps { + subs: SubsSnapshot; + graphEe: TypedEmitter; + eventListEe: TypedEmitter; +} + +function horizontalityToPositions(isHorizontal: boolean): { + targetPosition: Position; + sourcePosition: Position; +} { + return { + targetPosition: isHorizontal ? Position.Left : Position.Top, + sourcePosition: isHorizontal ? Position.Right : Position.Bottom, + }; +} + +interface LayoutedElements { + nodes: Node[]; + edges: Edge[]; +} + +const LAYOUT_CONFIG_DOWN = { + keypress: "j", + emoji: "⬇️", + elkLayoutOptions: { + "elk.algorithm": "layered", + "elk.direction": "DOWN", + }, + isHorizontal: false, +} as const; +const LAYOUT_CONFIG_RIGHT = { + keypress: "l", + emoji: "➡️", + elkLayoutOptions: { + "elk.algorithm": "layered", + "elk.direction": "RIGHT", + }, + isHorizontal: true, +} as const; +const LAYOUT_CONFIG_RADIAL = { + keypress: "r", + emoji: "🌐", + elkLayoutOptions: { + "elk.algorithm": "radial", + }, + isHorizontal: false, +} as const; +const LAYOUT_CONFIG_FORCE = { + keypress: "f", + emoji: "🧲", + elkLayoutOptions: { + "elk.algorithm": "force", + }, + isHorizontal: false, +} as const; + +type LayoutConfiguration = + | typeof LAYOUT_CONFIG_DOWN + | typeof LAYOUT_CONFIG_RIGHT + | typeof LAYOUT_CONFIG_RADIAL + | typeof LAYOUT_CONFIG_FORCE; + +const LAYOUT_CONFIGURATIONS: LayoutConfiguration[] = [ + LAYOUT_CONFIG_DOWN, + LAYOUT_CONFIG_RIGHT, + LAYOUT_CONFIG_RADIAL, + LAYOUT_CONFIG_FORCE, +]; + +type ComputeElkLayoutOptions = Pick< + LayoutConfiguration, + "elkLayoutOptions" | "isHorizontal" +>; + +interface ComputeLayoutedElementsProps + extends LayoutedElements, + ComputeElkLayoutOptions {} + +// Elk has a *huge* amount of options to configure. To see everything you can +// tweak check out: +// +// - https://www.eclipse.org/elk/reference/algorithms.html +// - https://www.eclipse.org/elk/reference/options.html +const baseElkOptions: LayoutOptions = { + "elk.layered.spacing.nodeNodeBetweenLayers": "100", + "elk.spacing.nodeNode": "80", +}; + +async function computeLayoutedElements({ + nodes, + edges, + elkLayoutOptions, + isHorizontal, +}: ComputeLayoutedElementsProps): Promise { + if (nodes.length === 0) { + return Promise.resolve({ + nodes: [], + edges: [], + }); + } + + const elk = new ELK(); + const graph: ElkNode = { + id: "root", + layoutOptions: { + ...baseElkOptions, + ...elkLayoutOptions, + }, + //@ts-ignore + children: nodes.map((node) => ({ + ...node, + // Adjust the target and source handle positions based on the layout + // direction. + targetPosition: isHorizontal ? "left" : "top", + sourcePosition: isHorizontal ? "right" : "bottom", + + // Hardcode a width and height for elk to use when layouting. + //width: 150, + //height: 50, + })), + //@ts-ignore + edges: edges.map((edge) => ({ + ...edge, + })), + }; + + const layoutedGraph = await elk.layout(graph); + + if (!layoutedGraph.children || !layoutedGraph.edges) { + throw new Error("Elk did not return a valid graph"); + } + + return { + //@ts-ignore + nodes: layoutedGraph.children.map((node) => ({ + ...node, + // React Flow expects a position property on the node instead of `x` + // and `y` fields. + position: { x: node.x, y: node.y }, + })), + //@ts-ignore + edges: layoutedGraph.edges, + }; +} + +const NODE_TYPES: NodeTypes = { + variable: VariableNode, +}; + +type VariableNodeData = VariableNodeProps["data"]; +type RFVariableNode = Node; + +function newVariable( + id: string, + data: VariableNodeData, + isHorizontal: boolean +): RFVariableNode { + return { + id, + position: { x: 0, y: 0 }, + type: "variable", + data, + ...horizontalityToPositions(isHorizontal), + }; +} + +function canAddVariable(variableName: string, existingNodes: Node[]): boolean { + return !existingNodes.some((n) => n.id === variableName); +} + +function canAddEdge(edgeName: string, existingEdges: Edge[]): boolean { + return !existingEdges.some((e) => e.id === edgeName); +} + +function addNode(node: Node): NodeChange { + return { + type: "add", + item: node, + }; +} + +function addEdge(edge: Edge): EdgeChange { + return { + type: "add", + item: edge, + }; +} + +// Auto-layout logic due in part to the `feldera/dbsp` project, licensed under +// the MIT license. +// +// The source code for the original project can be found at +// https://github.com/feldera/dbsp/blob/585a1926a6d3a0f8176dc80db5e906ec7d095400/web-ui/src/streaming/builder/hooks/useAutoLayout.ts#L215 +// and its license at +// https://github.com/feldera/dbsp/blob/585a1926a6d3a0f8176dc80db5e906ec7d095400/LICENSE +const nodeCountSelector = (state: ReactFlowState) => + state.nodeInternals.size + state.edges.length; +const nodesSetInViewSelector = (state: ReactFlowState) => + Array.from(state.nodeInternals.values()).every( + (node) => node.width && node.height + ); + +type RedoLayoutFn = () => Promise; +function useRedoLayout(options: ComputeElkLayoutOptions): RedoLayoutFn { + const nodeCount = useStore(nodeCountSelector); + const nodesInitialized = useStore(nodesSetInViewSelector); + const { getNodes, setNodes, getEdges } = useReactFlow(); + const instance = useReactFlow(); + + return useCallback(async () => { + if (!nodeCount || !nodesInitialized) { + return; + } + const { nodes } = await computeLayoutedElements({ + nodes: getNodes(), + edges: getEdges(), + ...options, + }); + setNodes(nodes); + window.requestAnimationFrame(() => { + instance.fitView(); + }); + }, [ + nodeCount, + nodesInitialized, + getNodes, + getEdges, + options, + setNodes, + instance, + ]); +} + +// Does positioning of the nodes in the graph. +function useAutoLayout(options: ComputeElkLayoutOptions) { + const redoLayout = useRedoLayout(options); + + useEffect(() => { + // This wrapping is of course redundant, but exercised for the purpose of + // explicitness. + async function inner() { + await redoLayout(); + } + inner(); + }, [redoLayout]); +} + +function useKeydown({ + layoutConfig, + setLayoutConfig, + graphEe, +}: { + layoutConfig: LayoutConfiguration; + setLayoutConfig: React.Dispatch>; + graphEe: TypedEmitter; +}) { + const redoLayout = useRedoLayout(layoutConfig); + + const keyDownHandler = useCallback( + async (key: string) => { + switch (key) { + case "c": { + await redoLayout(); + return; + } + default: { + break; + } + } + + for (const config of LAYOUT_CONFIGURATIONS) { + if (key === config.keypress) { + setLayoutConfig(config); + return; + } + } + }, + [redoLayout, setLayoutConfig] + ); + graphEe.on("keydown", async (key) => await keyDownHandler(key)); +} + +function Graph({ + subs, + graphEe, + eventListEe, +}: VariablesGraphProps): JSX.Element { + const instance = useReactFlow(); + + // We need to reset the graph when the subs snapshot changes. I'm not sure + // why this isn't done by the existing state manager. + useEffect(() => { + instance.setNodes([]); + instance.setEdges([]); + }, [instance, subs.epoch]); + + const varEe = useRef(new TypedEmitter()); + // Allow an unbounded number of listeners since we attach a listener for each + // variable. + varEe.current.setMaxListeners(Infinity); + + const isOutlined = useFocusOutlineEvent({ + ee: graphEe, + value: subs.epoch, + event: "focusEpoch", + }); + + const [layoutConfig, setLayoutConfig] = + useState(LAYOUT_CONFIG_DOWN); + + const [elements, setElements] = useState({ + nodes: [], + edges: [], + }); + + const [variablesNeedingFocus, setVariablesNeedingFocus] = useState< + Set + >(new Set()); + + useEffect(() => { + if (variablesNeedingFocus.size === 0) { + return; + } + for (const variable of variablesNeedingFocus) { + varEe.current.emit("focus", variable); + } + setVariablesNeedingFocus(new Set()); + }, [variablesNeedingFocus]); + + useAutoLayout(layoutConfig); + useKeydown({ + layoutConfig, + setLayoutConfig, + graphEe, + }); + + const onNodesChange = useCallback((changes: NodeChange[]) => { + setElements(({ nodes, edges }) => { + return { + nodes: applyNodeChanges(changes, nodes), + edges, + }; + }); + }, []); + + const onEdgesChange = useCallback((changes: EdgeChange[]) => { + setElements(({ nodes, edges }) => { + return { + nodes, + edges: applyEdgeChanges(changes, edges), + }; + }); + }, []); + + interface AddNewVariableParams { + from?: Variable; + variable: Variable; + } + + const addNewVariable = useCallback( + ({ from, variable }: AddNewVariableParams) => { + const variablesToFocus = new Set(); + + setElements(({ nodes, edges }) => { + let fromVariable: Variable | undefined = from; + let toVariable: Variable | undefined = variable; + + const nodeChanges: NodeChange[] = []; + const edgeChanges: EdgeChange[] = []; + + while (toVariable !== undefined) { + const toVariableName = toVariable.toString(); + if (canAddVariable(toVariableName, nodes)) { + const newVariableNode = newVariable( + toVariable.toString(), + { + subs, + rawVariable: toVariable, + addSubVariableLink: addNewVariable, + isOutlined: true, + ee: varEe.current, + }, + layoutConfig.isHorizontal + ); + + nodeChanges.push(addNode(newVariableNode)); + } + + if (fromVariable !== undefined) { + const edgeName = `${fromVariable}->${toVariable}`; + if (canAddEdge(edgeName, edges)) { + let markerEnd: EdgeMarkerType | undefined; + if (subs.get_root_key(fromVariable) === toVariable) { + markerEnd = { + type: MarkerType.ArrowClosed, + width: 20, + height: 20, + }; + } + + const newEdge = addEdge({ + id: `${fromVariable}->${toVariable}`, + source: fromVariable.toString(), + target: toVariableName, + markerEnd, + }); + + edgeChanges.push(newEdge); + } + } + + variablesToFocus.add(toVariable); + + fromVariable = toVariable; + const rootToVariable = subs.get_root_key(toVariable); + if (toVariable !== rootToVariable) { + toVariable = rootToVariable; + } else { + toVariable = undefined; + } + } + + const newNodes = applyNodeChanges(nodeChanges, nodes); + const newEdges = applyEdgeChanges(edgeChanges, edges); + + return { nodes: newNodes, edges: newEdges }; + }); + + setVariablesNeedingFocus(variablesToFocus); + }, + [layoutConfig.isHorizontal, subs] + ); + + const addNewVariableNode = useCallback( + (variable: Variable) => { + addNewVariable({ variable }); + }, + [addNewVariable] + ); + + graphEe.on("focusVariable", addNewVariableNode); + + return ( + onNodesChange(e)} + onEdgesChange={(e) => onEdgesChange(e)} + fitView + nodesDraggable + nodesConnectable={false} + nodeTypes={NODE_TYPES} + proOptions={{ + // https://reactflow.dev/docs/guides/remove-attribution/ + hideAttribution: true, + }} + className={clsx( + "ring-inset rounded-md transition ease-in-out duration-700", + isOutlined && "ring-2 ring-blue-500" + )} + > + + { + eventListEe.emit("focusEpoch", subs.epoch); + }} + > + Epoch {subs.epoch} + + + + + + + + + ); +} + +interface LayoutPanelProps { + layoutConfig: LayoutConfiguration; + setLayoutConfig: React.Dispatch>; +} + +function LayoutPanel({ + layoutConfig, + setLayoutConfig, +}: LayoutPanelProps): JSX.Element { + const commonStyle = "rounded cursor-pointer text-2xl select-none"; + + return ( + <> + {LAYOUT_CONFIGURATIONS.map((config, i) => ( + { + setLayoutConfig(config); + }} + > + {config.emoji} + + ))} + + ); +} + +export default function VariablesGraph({ + subs, + graphEe, + eventListEe, +}: VariablesGraphProps) { + return ( + + + + ); +} diff --git a/crates/compiler/checkmate/www/src/components/Ui.tsx b/crates/compiler/checkmate/www/src/components/Ui.tsx new file mode 100644 index 0000000000..aaf7ef6649 --- /dev/null +++ b/crates/compiler/checkmate/www/src/components/Ui.tsx @@ -0,0 +1,111 @@ +import React, { useEffect, useState } from "react"; +import { AllEvents } from "../schema"; +import { Engine, EventEpoch } from "../engine/engine"; +import EventList from "./EventList/index"; +import VariablesGraph from "./Graph/VariablesGraph"; +import { TypedEmitter } from "tiny-typed-emitter"; +import { + EventListMessage, + GlobalMessage, + GraphMessage, + LoadEpochView, +} from "../utils/events"; +import { assertExhaustive } from "../utils/exhaustive"; +import { SubsSnapshot } from "../engine/subs"; + +interface UiProps { + events: AllEvents; +} + +export default function Ui({ events }: UiProps): JSX.Element { + const engine = React.useRef(new Engine(events)); + + const graphEe = React.useRef(new TypedEmitter()); + const eventListEe = React.useRef(new TypedEmitter()); + const globalEe = React.useRef(new TypedEmitter()); + + const [subsTop, setSubsTop] = useState(undefined); + const [subsBot, setSubsBot] = useState(undefined); + + useEffect(() => { + globalEe.current.on("loadEpoch", (epoch, view) => { + switch (view) { + case LoadEpochView.Top: { + setSubsTop(engine.current.stepToSnapshot(epoch)); + break; + } + case LoadEpochView.Bot: { + setSubsBot(engine.current.stepToSnapshot(epoch)); + break; + } + default: + assertExhaustive(view); + } + }); + }, []); + + const selectedEpochs = [subsTop?.epoch, subsBot?.epoch] + .filter((x): x is EventEpoch => x !== undefined) + .sort(); + + return ( +
{ + graphEe.current.emit("keydown", e.key); + }} + > +
+ +
+
+ {selectedEpochs.length === 0 && ( + + + Select an event to view. + + + )} + {subsTop !== undefined && ( + + )} + {/* */} + {subsBot !== undefined && ( + + )} +
+
+ ); +} + +interface VariablesGraphViewProps { + subs: SubsSnapshot; + graphEe: TypedEmitter; + eventListEe: TypedEmitter; +} + +function VariablesGraphView({ + subs, + graphEe, + eventListEe, +}: VariablesGraphViewProps): JSX.Element { + return ( + + ); +} diff --git a/crates/compiler/checkmate/www/src/engine/engine.tsx b/crates/compiler/checkmate/www/src/engine/engine.tsx new file mode 100644 index 0000000000..d1c4d32ed0 --- /dev/null +++ b/crates/compiler/checkmate/www/src/engine/engine.tsx @@ -0,0 +1,162 @@ +import { Event, Variable } from "../schema"; +import { assertExhaustive } from "../utils/exhaustive"; +import { + ChangeEvent, + makeDeleteVariable, + makeRevertVariable, + RollbackChange, + Subs, + SubsSnapshot, +} from "./subs"; + +export type EventEpoch = number & { __eventIndex: never }; + +function* flattenEvents(events: Event[]): Generator { + for (const event of events) { + yield event; + switch (event.type) { + case "Unification": { + yield* flattenEvents(event.subevents); + break; + } + case "VariableUnified": + case "VariableSetDescriptor": + break; + default: + assertExhaustive(event); + } + } +} + +function getFlatEvents(events: Event[]): { + flatEvents: Event[]; + map: Map; +} { + const map = new Map(); + const flatEvents = Array.from(flattenEvents(events)); + let i = 0; + for (const event of flatEvents) { + map.set(event, i as EventEpoch); + i++; + } + return { flatEvents, map }; +} + +export class Engine { + #eventIndexMap: Map; + #events: Event[]; + #subs: Subs = Subs.new(); + #reverseEvents: Map = new Map(); + + #nextIndexForward: EventEpoch = 0 as EventEpoch; + + constructor(events: Event[]) { + const { flatEvents, map } = getFlatEvents(events); + this.#eventIndexMap = map; + this.#events = flatEvents; + } + + getEventIndex(event: Event): EventEpoch { + const index = this.#eventIndexMap.get(event); + if (index === undefined) { + throw new Error("Event not found"); + } + return index; + } + + get step(): EventEpoch { + return this.#nextIndexForward; + } + + stepTo(eventIndex: EventEpoch): void { + while (this.#nextIndexForward <= eventIndex) { + this.stepForward(this.#nextIndexForward); + ++this.#nextIndexForward; + } + while (this.#nextIndexForward > eventIndex + 1) { + --this.#nextIndexForward; + this.stepBackward(this.#nextIndexForward); + } + + if (this.#nextIndexForward !== eventIndex + 1) { + throw new Error("Invalid event index"); + } + } + + stepToSnapshot(eventIndex: EventEpoch): SubsSnapshot { + this.stepTo(eventIndex); + return this.subsSnapshot(); + } + + get subs(): Readonly { + return this.#subs; + } + + subsSnapshot(): SubsSnapshot { + return this.#subs.snapshot({ + epoch: (this.#nextIndexForward - 1) as EventEpoch, + }); + } + + lastEventIndex(): EventEpoch { + return (this.#events.length - 1) as EventEpoch; + } + + private stepForward(eventIndex: EventEpoch): void { + const event = this.#events[eventIndex]; + if (!isApplicable(event)) { + return; + } + + if (!this.#reverseEvents.has(eventIndex)) { + const variable = applicableVariable(event); + const current = this.#subs.get(variable); + let revert: RollbackChange; + if (!current) { + revert = makeDeleteVariable({ variable }); + } else { + revert = makeRevertVariable({ variable, to: current }); + } + this.#reverseEvents.set(eventIndex, revert); + } + + this.#subs.apply(event); + } + + private stepBackward(eventIndex: EventEpoch): void { + const event = this.#events[eventIndex]; + if (!isApplicable(event)) { + return; + } + + const revert = this.#reverseEvents.get(eventIndex); + if (!revert) { + throw new Error("No revert found"); + } + + this.#subs.apply(revert); + } +} + +function isApplicable(event: Event): event is ChangeEvent { + switch (event.type) { + case "VariableUnified": + case "VariableSetDescriptor": + return true; + case "Unification": + return false; + default: + assertExhaustive(event); + } +} + +function applicableVariable(event: ChangeEvent): Variable { + switch (event.type) { + case "VariableUnified": + return event.from; + case "VariableSetDescriptor": + return event.variable; + default: + assertExhaustive(event); + } +} diff --git a/crates/compiler/checkmate/www/src/engine/event_util.tsx b/crates/compiler/checkmate/www/src/engine/event_util.tsx new file mode 100644 index 0000000000..d04f78a9bc --- /dev/null +++ b/crates/compiler/checkmate/www/src/engine/event_util.tsx @@ -0,0 +1,19 @@ +import { Event } from "../schema"; + +export function lastSubEvent(event: Event): Event { + switch (event.type) { + case "Unification": { + const subevents = event.subevents; + if (subevents.length === 0) { + return event; + } + return lastSubEvent(event.subevents[event.subevents.length - 1]); + } + case "VariableUnified": { + return event; + } + case "VariableSetDescriptor": { + return event; + } + } +} diff --git a/crates/compiler/checkmate/www/src/engine/subs.tsx b/crates/compiler/checkmate/www/src/engine/subs.tsx new file mode 100644 index 0000000000..58039f792b --- /dev/null +++ b/crates/compiler/checkmate/www/src/engine/subs.tsx @@ -0,0 +1,179 @@ +import { Content, Rank, Variable, Event } from "../schema"; +import { assertExhaustive } from "../utils/exhaustive"; +import { Refine } from "../utils/refine"; +import { EventEpoch } from "./engine"; + +export type TypeLink = { + type: "link"; + to: Variable; +}; + +function link({ to }: Omit): TypeLink { + return { type: "link", to }; +} + +export type TypeDescriptor = { + type: "descriptor"; + rank: Rank; + content: Content; +}; + +function descriptor({ + rank, + content, +}: Omit): TypeDescriptor { + return { type: "descriptor", rank, content }; +} + +export type VarType = TypeLink | TypeDescriptor; + +export type RevertVariableChange = { + type: "revertTo"; + variable: Variable; + to: VarType; +}; + +export type DeleteVariableChange = { + type: "delete"; + variable: Variable; +}; + +export type RollbackChange = RevertVariableChange | DeleteVariableChange; + +export function makeRevertVariable({ + variable, + to, +}: Omit): RevertVariableChange { + return { type: "revertTo", variable, to: { ...to } }; +} + +export function makeDeleteVariable({ + variable, +}: Omit): DeleteVariableChange { + return { type: "delete", variable }; +} + +export type ChangeEvent = + | Refine + | Refine; + +export type Change = ChangeEvent | RollbackChange; + +export class Subs implements QuerySubs { + #map: Map; + + private constructor(map: Map) { + this.#map = map; + } + + static new(): Subs { + return new Subs(new Map()); + } + + get(variable: Variable): VarType | undefined { + return this.#map.get(variable); + } + + get_root(variable: Variable): TypeDescriptor | undefined { + const type = this.get(variable); + if (type === undefined) { + return undefined; + } + switch (type.type) { + case "descriptor": + return type; + case "link": + return this.get_root(type.to); + default: + assertExhaustive(type); + } + } + + get_root_key(variable: Variable): Variable { + const type = this.get(variable); + if (type === undefined) { + return variable; + } + switch (type.type) { + case "descriptor": + return variable; + case "link": + return this.get_root_key(type.to); + default: + assertExhaustive(type); + } + } + + snapshot({ epoch }: { epoch: EventEpoch }): SubsSnapshot { + const snapshotMap = new Map(); + for (const [key, value] of this.#map) { + snapshotMap.set(key, { ...value }); + } + const snapshot = new Subs(snapshotMap); + return { + epoch, + get(variable: Variable): VarType | undefined { + return snapshot.get(variable); + }, + get_root(variable: Variable): TypeDescriptor | undefined { + return snapshot.get_root(variable); + }, + get_root_key(variable: Variable): Variable { + return snapshot.get_root_key(variable); + }, + __snapshot__: SnapshotSymbol, + }; + } + + apply(change: Change): void { + switch (change.type) { + case "VariableUnified": { + const { from, to } = change; + if (from !== to) { + this.#map.set(from, link({ to })); + } + break; + } + case "VariableSetDescriptor": { + const { variable, rank, content } = change; + const existing = this.get_root(variable); + if (existing !== undefined) { + const nu = descriptor({ ...existing }); + if (rank) nu.rank = rank; + if (content) nu.content = content; + this.#map.set(variable, nu); + } else { + if (typeof rank !== "number") throw new Error("rank is required"); + if (!content) throw new Error("content is required"); + this.#map.set(variable, descriptor({ rank, content })); + } + break; + } + case "revertTo": { + const { variable, to } = change; + this.#map.set(variable, { ...to }); + break; + } + case "delete": { + const { variable } = change; + this.#map.delete(variable); + break; + } + default: + assertExhaustive(change); + } + } +} + +const SnapshotSymbol = Symbol("Snapshot"); + +export interface QuerySubs { + get(variable: Variable): VarType | undefined; + get_root(variable: Variable): TypeDescriptor | undefined; + get_root_key(variable: Variable): Variable; +} + +export interface SubsSnapshot extends QuerySubs { + readonly epoch: EventEpoch; + __snapshot__: typeof SnapshotSymbol; +} diff --git a/crates/compiler/checkmate/www/src/hooks/useFocusOutlineEvent.ts b/crates/compiler/checkmate/www/src/hooks/useFocusOutlineEvent.ts new file mode 100644 index 0000000000..7b09b9d441 --- /dev/null +++ b/crates/compiler/checkmate/www/src/hooks/useFocusOutlineEvent.ts @@ -0,0 +1,42 @@ +import { useEffect, useState } from "react"; +import type { TypedEmitter } from "tiny-typed-emitter"; + +type Events = { + [K in Name]: (value: T) => void; +}; + +interface UseFocusOutlineEventProps { + value: T; + ee: TypedEmitter>; + event: Name; + defaultIsOutlined?: boolean; +} + +export function useFocusOutlineEvent({ + value, + ee, + event, + defaultIsOutlined = false, +}: UseFocusOutlineEventProps) { + const [isOutlined, setIsOutlined] = useState(defaultIsOutlined); + + useEffect(() => { + ee.on(event, (focusValue: T) => { + if (focusValue !== value) return; + setIsOutlined(true); + }); + }, [ee, event, value]); + + useEffect(() => { + if (!isOutlined) return; + const timer = setTimeout(() => { + setIsOutlined(false); + }, 500); + + return () => { + clearTimeout(timer); + }; + }, [isOutlined]); + + return isOutlined; +} diff --git a/crates/compiler/checkmate/www/src/index.css b/crates/compiler/checkmate/www/src/index.css new file mode 100644 index 0000000000..1e72a197d7 --- /dev/null +++ b/crates/compiler/checkmate/www/src/index.css @@ -0,0 +1,17 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +.outline-event-col-1 { + box-shadow: inset 2px 0px 0px #3B82F6, /* Left */ + inset -2px 0px 0px transparent, /* Right */ + inset 0px 2px 0px #3B82F6, /* Bottom */ + inset 0px -2px 0px #3B82F6; /* Top */ +} + +.outline-event-col-3 { + box-shadow: inset 2px 0px 0px transparent, /* Left */ + inset -2px 0px 0px #3B82F6, /* Right */ + inset 0px 2px 0px #3B82F6, /* Bottom */ + inset 0px -2px 0px #3B82F6; /* Top */ +} diff --git a/crates/compiler/checkmate/www/src/index.tsx b/crates/compiler/checkmate/www/src/index.tsx new file mode 100644 index 0000000000..cfd7784900 --- /dev/null +++ b/crates/compiler/checkmate/www/src/index.tsx @@ -0,0 +1,13 @@ +import React from "react"; +import ReactDOM from "react-dom/client"; +import "./index.css"; +import App from "./App"; + +const root = ReactDOM.createRoot( + document.getElementById("root") as HTMLElement +); +root.render( + + + +); diff --git a/crates/compiler/checkmate/www/src/schema.d.ts b/crates/compiler/checkmate/www/src/schema.d.ts new file mode 100644 index 0000000000..c956d6021d --- /dev/null +++ b/crates/compiler/checkmate/www/src/schema.d.ts @@ -0,0 +1,243 @@ +/* eslint-disable */ +/** + * This file was automatically generated by json-schema-to-typescript. + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, + * and run json-schema-to-typescript to regenerate this file. + */ + +export type Event = + | { + left: Variable; + mode: UnificationMode; + right: Variable; + subevents: Event[]; + success?: boolean | null; + type: "Unification"; + [k: string]: unknown; + } + | { + from: Variable; + to: Variable; + type: "VariableUnified"; + [k: string]: unknown; + } + | { + content?: Content | null; + rank?: Rank | null; + type: "VariableSetDescriptor"; + variable: Variable; + [k: string]: unknown; + }; +export type Variable = number; +export type UnificationMode = + | { + type: "Eq"; + [k: string]: unknown; + } + | { + type: "Present"; + [k: string]: unknown; + } + | { + type: "LambdaSetSpecialization"; + [k: string]: unknown; + }; +export type Content = + | { + name?: string | null; + type: "Flex"; + [k: string]: unknown; + } + | { + name: string; + type: "Rigid"; + [k: string]: unknown; + } + | { + abilities: Symbol[]; + name?: string | null; + type: "FlexAble"; + [k: string]: unknown; + } + | { + abilities: Symbol[]; + name: string; + type: "RigidAble"; + [k: string]: unknown; + } + | { + name?: string | null; + structure: Variable; + type: "Recursive"; + [k: string]: unknown; + } + | { + ambient_function: Variable; + recursion_var?: Variable | null; + solved: ClosureType[]; + type: "LambdaSet"; + unspecialized: UnspecializedClosureType[]; + [k: string]: unknown; + } + | { + type: "ErasedLambda"; + [k: string]: unknown; + } + | { + kind: AliasKind; + name: Symbol; + real_variable: Variable; + type: "Alias"; + variables: AliasTypeVariables; + [k: string]: unknown; + } + | { + symbol: Symbol; + type: "Apply"; + variables: Variable[]; + [k: string]: unknown; + } + | { + arguments: Variable[]; + lambda_type: Variable; + ret: Variable; + type: "Function"; + [k: string]: unknown; + } + | { + extension: Variable; + fields: { + [k: string]: RecordField; + }; + type: "Record"; + [k: string]: unknown; + } + | { + elements: { + [k: string]: Variable; + }; + extension: Variable; + type: "Tuple"; + [k: string]: unknown; + } + | { + extension: TagUnionExtension; + tags: { + [k: string]: Variable[]; + }; + type: "TagUnion"; + [k: string]: unknown; + } + | { + extension: TagUnionExtension; + functions: Symbol[]; + tags: string[]; + type: "FunctionOrTagUnion"; + [k: string]: unknown; + } + | { + extension: TagUnionExtension; + recursion_var: Variable; + tags: { + [k: string]: Variable[]; + }; + type: "RecursiveTagUnion"; + [k: string]: unknown; + } + | { + type: "EmptyRecord"; + [k: string]: unknown; + } + | { + type: "EmptyTuple"; + [k: string]: unknown; + } + | { + type: "EmptyTagUnion"; + [k: string]: unknown; + } + | { + range: NumericRange; + type: "RangedNumber"; + [k: string]: unknown; + } + | { + type: "Error"; + [k: string]: unknown; + }; +export type Symbol = string; +export type AliasKind = + | { + type: "Structural"; + [k: string]: unknown; + } + | { + type: "Opaque"; + [k: string]: unknown; + }; +export type RecordFieldKind = + | { + type: "Demanded"; + [k: string]: unknown; + } + | { + rigid: boolean; + type: "Required"; + [k: string]: unknown; + } + | { + rigid: boolean; + type: "Optional"; + [k: string]: unknown; + }; +export type TagUnionExtension = + | { + type: "Openness"; + variable: Variable; + [k: string]: unknown; + } + | { + type: "Any"; + variable: Variable; + [k: string]: unknown; + }; +export type NumericRangeKind = + | { + type: "Int"; + [k: string]: unknown; + } + | { + type: "AnyNum"; + [k: string]: unknown; + }; +export type Rank = number; +export type AllEvents = Event[]; + +export interface ClosureType { + environment: Variable[]; + function: Symbol; + [k: string]: unknown; +} +export interface UnspecializedClosureType { + ability_member: Symbol; + lambda_set_region: number; + specialization: Variable; + [k: string]: unknown; +} +export interface AliasTypeVariables { + infer_ext_in_output_position_variables: Variable[]; + lambda_set_variables: Variable[]; + type_variables: Variable[]; + [k: string]: unknown; +} +export interface RecordField { + field_type: Variable; + kind: RecordFieldKind; + [k: string]: unknown; +} +export interface NumericRange { + kind: NumericRangeKind; + min_width: number; + signed: boolean; + [k: string]: unknown; +} diff --git a/crates/compiler/checkmate/www/src/utils/events.ts b/crates/compiler/checkmate/www/src/utils/events.ts new file mode 100644 index 0000000000..4f01ceaabc --- /dev/null +++ b/crates/compiler/checkmate/www/src/utils/events.ts @@ -0,0 +1,25 @@ +import { EventEpoch } from "../engine/engine"; +import { Variable } from "../schema"; + +export interface VariableMessage { + focus: (variable: Variable) => void; +} + +export interface GraphMessage { + focusEpoch: (epoch: EventEpoch) => void; + focusVariable: (variable: Variable) => void; + keydown: (key: string) => void; +} + +export interface EventListMessage { + focusEpoch: (epoch: EventEpoch) => void; +} + +export enum LoadEpochView { + Top, + Bot, +} + +export interface GlobalMessage { + loadEpoch: (epoch: EventEpoch, view: LoadEpochView) => void; +} diff --git a/crates/compiler/checkmate/www/src/utils/exhaustive.ts b/crates/compiler/checkmate/www/src/utils/exhaustive.ts new file mode 100644 index 0000000000..476d9b0993 --- /dev/null +++ b/crates/compiler/checkmate/www/src/utils/exhaustive.ts @@ -0,0 +1,3 @@ +export function assertExhaustive(_: never): never { + throw new Error("Exhaustive switch"); +} diff --git a/crates/compiler/checkmate/www/src/utils/refine.ts b/crates/compiler/checkmate/www/src/utils/refine.ts new file mode 100644 index 0000000000..7be57f0856 --- /dev/null +++ b/crates/compiler/checkmate/www/src/utils/refine.ts @@ -0,0 +1,3 @@ +export type Refine = T & { + type: Type; +}; diff --git a/crates/compiler/checkmate/www/tailwind.config.js b/crates/compiler/checkmate/www/tailwind.config.js new file mode 100644 index 0000000000..c4e6ef7100 --- /dev/null +++ b/crates/compiler/checkmate/www/tailwind.config.js @@ -0,0 +1,17 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + mode: 'jit', + content: [ + "./src/**/*.{js,jsx,ts,tsx}", + ], + theme: { + extend: { + colors: { + 'roc-purple': '#7c38f5', + 'roc-purple-bg': '#ece2fd', + }, + }, + }, + plugins: [], +} + diff --git a/crates/compiler/checkmate/www/tsconfig.json b/crates/compiler/checkmate/www/tsconfig.json new file mode 100644 index 0000000000..2d519b5ad9 --- /dev/null +++ b/crates/compiler/checkmate/www/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "es2015", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noFallthroughCasesInSwitch": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx" + }, + "include": ["src"] +} diff --git a/crates/compiler/checkmate_schema/Cargo.toml b/crates/compiler/checkmate_schema/Cargo.toml new file mode 100644 index 0000000000..2af777c60d --- /dev/null +++ b/crates/compiler/checkmate_schema/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "roc_checkmate_schema" +description = "Schema for checkmate." + +authors.workspace = true +edition.workspace = true +license.workspace = true +version.workspace = true + +[dependencies] +serde.workspace = true +serde_json.workspace = true +schemars.workspace = true diff --git a/crates/compiler/checkmate_schema/src/lib.rs b/crates/compiler/checkmate_schema/src/lib.rs new file mode 100644 index 0000000000..fd89b6ea1d --- /dev/null +++ b/crates/compiler/checkmate_schema/src/lib.rs @@ -0,0 +1,225 @@ +use std::collections::HashMap; + +use schemars::{schema::RootSchema, schema_for, JsonSchema}; +use serde::Serialize; + +#[derive(Serialize, JsonSchema, Debug)] +#[serde(tag = "type")] +pub enum Constraint {} + +#[derive(Serialize, JsonSchema, Debug, PartialEq)] +pub struct Variable(pub u32); + +macro_rules! impl_content { + ($($name:ident { $($arg:ident: $ty:ty,)* },)*) => { + #[derive(Serialize, JsonSchema, Debug)] + #[serde(tag = "type")] + pub enum Content { + $( + $name { + $($arg: $ty),* + }, + )* + } + + impl Content { + $( + #[allow(non_snake_case)] + pub fn $name($($arg: $ty),*) -> Self { + Self::$name { $($arg),* } + } + )* + } + }; +} + +impl_content! { + Flex { + name: Option, + }, + Rigid { + name: String, + }, + FlexAble { + name: Option, + abilities: Vec, + }, + RigidAble { + name: String, + abilities: Vec, + }, + Recursive { + name: Option, + structure: Variable, + }, + LambdaSet { + solved: Vec, + unspecialized: Vec, + recursion_var: Option, + ambient_function: Variable, + }, + ErasedLambda {}, + Alias { + name: Symbol, + variables: AliasTypeVariables, + real_variable: Variable, + kind: AliasKind, + }, + Apply { + symbol: Symbol, + variables: Vec, + }, + Function { + arguments: Vec, + lambda_type: Variable, + ret: Variable, + }, + Record { + fields: HashMap, + extension: Variable, + }, + Tuple { + elements: HashMap, + extension: Variable, + }, + TagUnion { + tags: HashMap>, + extension: TagUnionExtension, + }, + FunctionOrTagUnion { + functions: Vec, + tags: Vec, + extension: TagUnionExtension, + }, + RecursiveTagUnion { + recursion_var: Variable, + tags: HashMap>, + extension: TagUnionExtension, + }, + EmptyRecord {}, + EmptyTuple {}, + EmptyTagUnion {}, + RangedNumber { + range: NumericRange, + }, + Error {}, +} + +#[derive(Serialize, JsonSchema, Debug)] +pub struct ClosureType { + pub function: Symbol, + pub environment: Vec, +} + +#[derive(Serialize, JsonSchema, Debug)] +pub struct UnspecializedClosureType { + pub specialization: Variable, + pub ability_member: Symbol, + pub lambda_set_region: u8, +} + +#[derive(Serialize, JsonSchema, Debug)] +#[serde(tag = "type")] +pub enum AliasKind { + Structural, + Opaque, +} + +#[derive(Serialize, JsonSchema, Debug)] +pub struct AliasTypeVariables { + pub type_variables: Vec, + pub lambda_set_variables: Vec, + pub infer_ext_in_output_position_variables: Vec, +} + +#[derive(Serialize, JsonSchema, Debug)] +pub struct RecordField { + pub kind: RecordFieldKind, + pub field_type: Variable, +} + +#[derive(Serialize, JsonSchema, Debug)] +#[serde(tag = "type")] +pub enum RecordFieldKind { + Demanded, + Required { rigid: bool }, + Optional { rigid: bool }, +} + +#[derive(Serialize, JsonSchema, Debug)] +#[serde(tag = "type", content = "variable")] +pub enum TagUnionExtension { + Openness(Variable), + Any(Variable), +} + +#[derive(Serialize, JsonSchema, Debug)] +pub struct NumericRange { + pub kind: NumericRangeKind, + pub signed: bool, + pub min_width: u32, +} + +#[derive(Serialize, JsonSchema, Debug)] +#[serde(tag = "type")] +pub enum NumericRangeKind { + Int, + AnyNum, +} + +#[derive(Serialize, JsonSchema, Debug)] +pub struct Rank(pub u32); + +#[derive(Serialize, JsonSchema, Debug)] +pub struct Descriptor { + pub content: Content, + pub rank: Rank, +} + +#[derive(Serialize, JsonSchema, Debug)] +pub struct Symbol( + // TODO: should this be module ID + symbol? + pub String, +); + +#[derive(Serialize, JsonSchema, Debug)] +#[serde(tag = "type")] +pub enum UnificationMode { + Eq, + Present, + LambdaSetSpecialization, +} + +#[derive(Serialize, JsonSchema, Debug)] +#[serde(tag = "type")] +pub enum Event { + Unification { + left: Variable, + right: Variable, + mode: UnificationMode, + success: Option, + subevents: Vec, + }, + VariableUnified { + from: Variable, + to: Variable, + }, + VariableSetDescriptor { + variable: Variable, + rank: Option, + content: Option, + }, +} + +#[derive(Serialize, JsonSchema, Debug)] +pub struct AllEvents(pub Vec); + +impl AllEvents { + pub fn schema() -> RootSchema { + schema_for!(AllEvents) + } + + pub fn write(&self, writer: impl std::io::Write) -> Result<(), serde_json::Error> { + serde_json::to_writer(writer, self) + } +} diff --git a/crates/compiler/collections/Cargo.toml b/crates/compiler/collections/Cargo.toml new file mode 100644 index 0000000000..4c9817169f --- /dev/null +++ b/crates/compiler/collections/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "roc_collections" +description = "Domain-specific collections created for the needs of the compiler." + +authors.workspace = true +edition.workspace = true +license.workspace = true +version.workspace = true + +[dependencies] +bitvec.workspace = true +bumpalo.workspace = true +fnv.workspace = true +hashbrown.workspace = true +im-rc.workspace = true +im.workspace = true +wyhash.workspace = true +smallvec.workspace = true diff --git a/crates/compiler/collections/src/all.rs b/crates/compiler/collections/src/all.rs new file mode 100644 index 0000000000..d42ec32f6c --- /dev/null +++ b/crates/compiler/collections/src/all.rs @@ -0,0 +1,224 @@ +use bumpalo::collections::String; +use bumpalo::Bump; +use std::hash::{BuildHasherDefault, Hash}; + +pub use wyhash::WyHash; + +#[inline(always)] +pub fn default_hasher() -> BuildHasherDefault { + BuildHasherDefault::default() +} + +pub type BuildHasher = BuildHasherDefault; + +// Versions of HashMap and HashSet from both std and im_rc +// which use the FNV hasher instead of the default SipHash hasher. +// FNV is faster but less secure; that's fine, since this compiler +// doesn't need cryptographically secure hashes, and also is not a +// server concerned about hash flooding attacks! +pub type MutMap = std::collections::HashMap; + +pub type MutSet = std::collections::HashSet; + +pub type ImMap = im_rc::hashmap::HashMap; + +pub type ImSet = im_rc::hashset::HashSet; + +pub type ImEntry<'a, K, V, S> = im_rc::hashmap::Entry<'a, K, V, S>; + +pub type SendMap = im::hashmap::HashMap; + +pub type SendSet = im::hashset::HashSet; + +// pub type BumpMap<'a, K, V> = hashbrown::HashMap>; +// pub type BumpSet<'a, K> = hashbrown::HashSet>; + +pub type BumpMap = hashbrown::HashMap; +pub type BumpSet = hashbrown::HashSet; + +pub type FnvMap = fnv::FnvHashMap; + +pub trait BumpMapDefault<'a> { + fn new_in(arena: &'a bumpalo::Bump) -> Self; + + fn with_capacity_in(capacity: usize, arena: &'a bumpalo::Bump) -> Self; +} + +impl<'a, K, V> BumpMapDefault<'a> for BumpMap { + fn new_in(_arena: &'a bumpalo::Bump) -> Self { + // hashbrown::HashMap::with_hasher_in(default_hasher(), hashbrown::BumpWrapper(arena)) + hashbrown::HashMap::with_hasher(default_hasher()) + } + + fn with_capacity_in(capacity: usize, _arena: &'a bumpalo::Bump) -> Self { + // hashbrown::HashMap::with_capacity_and_hasher_in( + // capacity, + // default_hasher(), + // hashbrown::BumpWrapper(arena), + // ) + hashbrown::HashMap::with_capacity_and_hasher(capacity, default_hasher()) + } +} + +impl<'a, K> BumpMapDefault<'a> for BumpSet { + fn new_in(_arena: &'a bumpalo::Bump) -> Self { + // hashbrown::HashSet::with_hasher_in(default_hasher(), hashbrown::BumpWrapper(arena)) + hashbrown::HashSet::with_hasher(default_hasher()) + } + + fn with_capacity_in(capacity: usize, _arena: &'a bumpalo::Bump) -> Self { + // hashbrown::HashSet::with_capacity_and_hasher_in( + // capacity, + // default_hasher(), + // hashbrown::BumpWrapper(arena), + // ) + hashbrown::HashSet::with_capacity_and_hasher(capacity, default_hasher()) + } +} + +pub fn arena_join<'a, I>(arena: &'a Bump, strings: &mut I, join_str: &str) -> String<'a> +where + I: Iterator, +{ + let mut buf = String::new_in(arena); + + if let Some(first) = strings.next() { + buf.push_str(first); + + for string in strings { + buf.reserve(join_str.len() + string.len()); + + buf.push_str(join_str); + buf.push_str(string); + } + } + + buf +} + +pub fn insert_all(map: &mut SendMap, elems: I) +where + K: Clone + Eq + Hash, + V: Clone, + I: Iterator, +{ + for (k, v) in elems { + map.insert(k, v); + } +} + +/// Like im's relative_complement, but for MutMap and with references for arguments. +pub fn relative_complement(map: &MutMap, other: &MutMap) -> MutMap +where + K: Clone + Eq + Hash, + V: Clone, +{ + let mut answer = MutMap::default(); + + for (key, value) in map { + // Drop any key that exists in the other map, + // by declining to insert it into the answer. + if !other.contains_key(key) { + answer.insert(key.clone(), value.clone()); + } + } + + answer +} + +/// Like intersection_with, except for MutMap and specialized to return +/// a tuple. Also, only clones the values that will be actually returned, +/// rather than cloning everything. +pub fn get_shared(map1: &MutMap, map2: &MutMap) -> MutMap +where + K: Clone + Eq + Hash, + V: Clone, +{ + let mut answer = MutMap::default(); + + for (key, right_value) in map2 { + match std::collections::HashMap::get(map1, key) { + None => (), + Some(left_value) => { + answer.insert(key.clone(), (left_value.clone(), right_value.clone())); + } + } + } + + answer +} + +/// Like im's union, but for MutMap. +pub fn union(mut map: MutMap, other: &MutMap) -> MutMap +where + K: Clone + Eq + Hash, + V: Clone, +{ + for (key, value) in other.iter() { + // If the key exists in both maps, keep the value in the owned one. + if !map.contains_key(key) { + map.insert(key.clone(), value.clone()); + } + } + + map +} + +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub struct HumanIndex(usize); + +impl HumanIndex { + pub const FIRST: Self = HumanIndex(0); + + pub fn zero_based(i: usize) -> Self { + HumanIndex(i) + } + + pub fn to_zero_based(self) -> usize { + self.0 + } + + pub fn one_based(i: usize) -> Self { + HumanIndex(i - 1) + } + + pub fn ordinal(self) -> std::string::String { + int_to_ordinal(self.0 + 1) + } +} + +fn int_to_ordinal(number: usize) -> std::string::String { + // NOTE: one-based + let remainder10 = number % 10; + let remainder100 = number % 100; + + let ending = match remainder100 { + 11..=13 => "th", + _ => match remainder10 { + 1 => "st", + 2 => "nd", + 3 => "rd", + _ => "th", + }, + }; + + format!("{number}{ending}") +} + +#[macro_export] +macro_rules! mut_map { + (@single $($x:tt)*) => (()); + (@count $($rest:expr),*) => (<[()]>::len(&[$(mut_map!(@single $rest)),*])); + + ($($key:expr => $value:expr,)+) => { mut_map!($($key => $value),+) }; + ($($key:expr => $value:expr),*) => { + { + let _cap = mut_map!(@count $($key),*); + let mut _map = ::std::collections::HashMap::with_capacity_and_hasher(_cap, $crate::all::default_hasher()); + $( + let _ = _map.insert($key, $value); + )* + _map + } + }; +} diff --git a/crates/compiler/collections/src/lib.rs b/crates/compiler/collections/src/lib.rs new file mode 100644 index 0000000000..09af7b0b44 --- /dev/null +++ b/crates/compiler/collections/src/lib.rs @@ -0,0 +1,19 @@ +//! Domain-specific collections created for the needs of the compiler. +#![warn(clippy::dbg_macro)] +// See github.com/roc-lang/roc/issues/800 for discussion of the large_enum_variant check. +#![allow(clippy::large_enum_variant)] + +pub mod all; +mod reference_matrix; +mod small_string_interner; +mod small_vec; +pub mod soa; +mod vec_map; +mod vec_set; + +pub use all::{default_hasher, BumpMap, ImEntry, ImMap, ImSet, MutMap, MutSet, SendMap}; +pub use reference_matrix::{ReferenceMatrix, Sccs, TopologicalSort}; +pub use small_string_interner::SmallStringInterner; +pub use small_vec::SmallVec; +pub use vec_map::VecMap; +pub use vec_set::VecSet; diff --git a/compiler/collections/src/reference_matrix.rs b/crates/compiler/collections/src/reference_matrix.rs similarity index 87% rename from compiler/collections/src/reference_matrix.rs rename to crates/compiler/collections/src/reference_matrix.rs index b74e361bc9..751eb8d7f9 100644 --- a/compiler/collections/src/reference_matrix.rs +++ b/crates/compiler/collections/src/reference_matrix.rs @@ -181,7 +181,7 @@ struct Params { p: Vec, s: Vec, scc: Sccs, - scca: BitVec, + scca: Vec, } impl Params { @@ -200,8 +200,10 @@ impl Params { scc: Sccs { matrix: ReferenceMatrix::new(length), components: 0, + not_initial: BitVec::repeat(false, length), }, - scca: BitVec::repeat(false, length), + // use u32::MAX as the sentinel empty value + scca: vec![u32::MAX; length], } } } @@ -215,7 +217,7 @@ fn recurse_onto(length: usize, bitvec: &BitVec, v: usize, params: &mut Params) { params.p.push(v as u32); for w in bitvec[v * length..][..length].iter_ones() { - if !params.scca[w] { + if params.scca[w] == u32::MAX { match params.preorders[w] { Preorder::Filled(pw) => loop { let index = *params.p.last().unwrap(); @@ -235,6 +237,8 @@ fn recurse_onto(length: usize, bitvec: &BitVec, v: usize, params: &mut Params) { Preorder::Empty => recurse_onto(length, bitvec, w, params), Preorder::Removed => {} } + } else { + params.scc.not_initial.set(params.scca[w] as _, true); } } @@ -246,13 +250,17 @@ fn recurse_onto(length: usize, bitvec: &BitVec, v: usize, params: &mut Params) { .scc .matrix .set_row_col(params.scc.components, node as usize, true); - params.scca.set(node as usize, true); + params.scca[node as usize] = params.scc.components as _; params.preorders[node as usize] = Preorder::Removed; if node as usize == v { break; } } + if !params.s.is_empty() { + params.scc.not_initial.set(params.scc.components, true); + } + params.scc.components += 1; } } @@ -261,6 +269,7 @@ fn recurse_onto(length: usize, bitvec: &BitVec, v: usize, params: &mut Params) { pub struct Sccs { components: usize, matrix: ReferenceMatrix, + not_initial: BitVec, } impl Sccs { @@ -271,7 +280,7 @@ impl Sccs { /// /// It is guaranteed that a group is non-empty, and that flattening the groups gives a valid /// topological ordering. - pub fn groups(&self) -> std::iter::Take> { + pub fn groups(&self) -> impl DoubleEndedIterator { // work around a panic when requesting a chunk size of 0 let length = if self.matrix.length == 0 { // the `.take(self.components)` ensures the resulting iterator will be empty @@ -286,5 +295,26 @@ impl Sccs { .bitvec .chunks_exact(length) .take(self.components) + .enumerate() + .map(|(c, slice)| (slice, !self.not_initial[c])) + } + + /// Reorder the input slice based on the SCCs. This produces a topological sort + pub fn reorder(&self, slice: &mut [T]) { + debug_assert_eq!(self.matrix.length, slice.len()); + + let mut indices: Vec<_> = self.groups().flat_map(|(s, _)| s.iter_ones()).collect(); + + for i in 0..slice.len() { + let mut index = indices[i]; + while index < i { + index = indices[index]; + } + + if i != index { + indices[i] = index; + slice.swap(i, index); + } + } } } diff --git a/compiler/collections/src/small_string_interner.rs b/crates/compiler/collections/src/small_string_interner.rs similarity index 90% rename from compiler/collections/src/small_string_interner.rs rename to crates/compiler/collections/src/small_string_interner.rs index 5a9a59e6a8..f9f009266c 100644 --- a/compiler/collections/src/small_string_interner.rs +++ b/crates/compiler/collections/src/small_string_interner.rs @@ -29,7 +29,7 @@ impl Length { } else if self.0 > 0 { Kind::Interned(self.0 as usize) } else { - Kind::Generated(self.0.abs() as usize) + Kind::Generated(self.0.unsigned_abs() as usize) } } @@ -48,9 +48,9 @@ enum Kind { impl Debug for Kind { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - Self::Generated(arg0) => write!(f, "Generated({})", arg0), + Self::Generated(arg0) => write!(f, "Generated({arg0})"), Self::Empty => write!(f, "Empty"), - Self::Interned(arg0) => write!(f, "Interned({})", arg0), + Self::Interned(arg0) => write!(f, "Interned({arg0})"), } } } @@ -69,6 +69,14 @@ impl std::fmt::Debug for SmallStringInterner { } impl SmallStringInterner { + pub const fn new() -> Self { + Self { + buffer: Vec::new(), + lengths: Vec::new(), + offsets: Vec::new(), + } + } + pub fn with_capacity(capacity: usize) -> Self { Self { // guess: the average symbol length is 5 @@ -147,7 +155,7 @@ impl SmallStringInterner { let index = self.lengths.len(); let offset = self.buffer.len(); - write!(self.buffer, "{}", index).unwrap(); + write!(self.buffer, "{index}").unwrap(); // this is a generated name, so store it as a negative length let length = Length(-((self.buffer.len() - offset) as i16)); @@ -251,14 +259,27 @@ impl SmallStringInterner { #[allow(dead_code)] fn find_i16_slice(slice: &[i16], key: i16) -> Option { - #[cfg(target_arch = "x86_64")] + // run with RUSTFLAGS="-C target-cpu=native" to enable + #[cfg(all( + target_arch = "x86_64", + target_feature = "avx", + target_feature = "avx2" + ))] return find_i16_slice_x86_64(slice, key); - #[cfg(not(target_arch = "x86_64"))] + #[cfg(not(all( + target_arch = "x86_64", + target_feature = "avx", + target_feature = "avx2" + )))] return find_i16_slice_fallback(slice, key); } -#[cfg(target_arch = "x86_64")] +#[cfg(all( + target_arch = "x86_64", + target_feature = "avx", + target_feature = "avx2" +))] fn find_i16_slice_x86_64(slice: &[i16], key: i16) -> Option { use std::arch::x86_64::*; @@ -332,7 +353,11 @@ mod test { assert!(interner.find_and_update("c", "cd").is_some()); } - #[cfg(target_arch = "x86_64")] + #[cfg(all( + target_arch = "x86_64", + target_feature = "avx", + target_feature = "avx2" + ))] #[test] fn find_test_1() { use super::find_i16_slice; diff --git a/crates/compiler/collections/src/small_vec.rs b/crates/compiler/collections/src/small_vec.rs new file mode 100644 index 0000000000..fc4cc0fa07 --- /dev/null +++ b/crates/compiler/collections/src/small_vec.rs @@ -0,0 +1,61 @@ +use std::fmt::Debug; + +use smallvec::SmallVec as Vec; + +#[derive(Default)] +pub struct SmallVec(Vec<[T; N]>); + +impl SmallVec { + pub const fn new() -> Self { + Self(Vec::new_const()) + } + + pub fn push(&mut self, value: T) { + self.0.push(value) + } + + pub fn pop(&mut self) -> Option { + self.0.pop() + } +} + +impl std::ops::Deref for SmallVec { + type Target = [T]; + + fn deref(&self) -> &Self::Target { + self.0.deref() + } +} + +impl Debug for SmallVec +where + T: Debug, +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} + +impl Clone for SmallVec +where + T: Clone, +{ + fn clone(&self) -> Self { + Self(self.0.clone()) + } +} + +impl IntoIterator for SmallVec { + type Item = T; + type IntoIter = as IntoIterator>::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.0.into_iter() + } +} + +impl FromIterator for SmallVec { + fn from_iter>(iter: I) -> Self { + Self(Vec::from_iter(iter)) + } +} diff --git a/compiler/collections/src/soa.rs b/crates/compiler/collections/src/soa.rs similarity index 83% rename from compiler/collections/src/soa.rs rename to crates/compiler/collections/src/soa.rs index bb118fff90..35ebdd11db 100644 --- a/compiler/collections/src/soa.rs +++ b/crates/compiler/collections/src/soa.rs @@ -1,17 +1,27 @@ use std::usize; -#[derive(PartialEq, Eq)] pub struct Index { index: u32, _marker: std::marker::PhantomData, } +impl Eq for Index {} + +impl PartialEq for Index { + fn eq(&self, other: &Self) -> bool { + self.index == other.index + } +} + +impl std::hash::Hash for Index { + fn hash(&self, state: &mut H) { + self.index.hash(state); + } +} + impl Clone for Index { fn clone(&self) -> Self { - Self { - index: self.index, - _marker: self._marker, - } + *self } } @@ -31,7 +41,7 @@ impl Index { } } - pub const fn index(&self) -> usize { + pub const fn index(self) -> usize { self.index as usize } @@ -42,22 +52,22 @@ impl Index { index } + + pub const fn as_slice(self) -> Slice { + Slice::new(self.index, 1) + } } #[derive(PartialEq, Eq)] pub struct Slice { - start: u32, length: u16, + start: u32, _marker: std::marker::PhantomData, } impl Clone for Slice { fn clone(&self) -> Self { - Self { - start: self.start, - length: self.length, - _marker: self._marker, - } + *self } } @@ -71,15 +81,15 @@ impl std::fmt::Debug for Slice { impl Default for Slice { fn default() -> Self { - Self { - start: Default::default(), - length: Default::default(), - _marker: Default::default(), - } + Self::empty() } } impl Slice { + pub const fn empty() -> Self { + Self::new(0, 0) + } + pub const fn new(start: u32, length: u16) -> Self { Self { start, @@ -120,6 +130,10 @@ impl Slice { pub fn into_iter(&self) -> impl Iterator> { self.indices().map(|i| Index::new(i as _)) } + + pub const fn at(&self, i: usize) -> Index { + Index::new(self.start + i as u32) + } } #[derive(PartialEq, Eq)] @@ -130,10 +144,7 @@ pub struct EitherIndex { impl Clone for EitherIndex { fn clone(&self) -> Self { - Self { - index: self.index, - _marker: self._marker, - } + *self } } diff --git a/compiler/collections/src/vec_map.rs b/crates/compiler/collections/src/vec_map.rs similarity index 84% rename from compiler/collections/src/vec_map.rs rename to crates/compiler/collections/src/vec_map.rs index c2f6b81d65..69ebd1b592 100644 --- a/compiler/collections/src/vec_map.rs +++ b/crates/compiler/collections/src/vec_map.rs @@ -1,5 +1,3 @@ -use std::iter::FromIterator; - #[derive(Debug, Clone)] pub struct VecMap { keys: Vec, @@ -8,14 +6,18 @@ pub struct VecMap { impl Default for VecMap { fn default() -> Self { + Self::new() + } +} + +impl VecMap { + pub const fn new() -> Self { Self { keys: Vec::new(), values: Vec::new(), } } -} -impl VecMap { pub fn len(&self) -> usize { debug_assert_eq!(self.keys.len(), self.values.len()); self.keys.len() @@ -27,6 +29,14 @@ impl VecMap { (k, v) } + + pub fn unzip(self) -> (Vec, Vec) { + (self.keys, self.values) + } + + pub fn unzip_slices(&self) -> (&[K], &[V]) { + (&self.keys, &self.values) + } } impl VecMap { @@ -99,6 +109,10 @@ impl VecMap { self.keys.iter().zip(self.values.iter()) } + pub fn iter_mut(&mut self) -> impl ExactSizeIterator { + self.keys.iter().zip(self.values.iter_mut()) + } + pub fn keys(&self) -> impl ExactSizeIterator { self.keys.iter() } @@ -112,10 +126,6 @@ impl VecMap { self.values.truncate(len); } - pub fn unzip(self) -> (Vec, Vec) { - (self.keys, self.values) - } - /// # Safety /// /// keys and values must have the same length, and there must not @@ -134,6 +144,12 @@ impl VecMap { cur_idx: 0, } } + + /// Removes all key/value pairs from the map, without affecting its allocated capacity. + pub fn clear(&mut self) { + self.keys.clear(); + self.values.clear(); + } } impl Extend<(K, V)> for VecMap { @@ -198,15 +214,12 @@ impl ExactSizeIterator for IntoIter { } } -impl FromIterator<(K, V)> for VecMap { +impl std::iter::FromIterator<(K, V)> for VecMap { fn from_iter>(iter: T) -> Self { - let iter = iter.into_iter(); - let size_hint = iter.size_hint(); - let mut map = VecMap::with_capacity(size_hint.1.unwrap_or(size_hint.0)); - for (k, v) in iter { - map.insert(k, v); - } - map + let mut this = Self::default(); + this.extend(iter); + + this } } @@ -241,6 +254,38 @@ where } } +impl PartialEq for VecMap +where + K: PartialEq, + V: PartialEq, +{ + fn eq(&self, other: &Self) -> bool { + if self.len() != other.len() { + return false; + } + + for (k, v) in self.iter() { + match other.get(k) { + Some(v1) => { + if v != v1 { + return false; + } + } + None => return false, + } + } + + true + } +} + +impl Eq for VecMap +where + K: Eq, + V: Eq, +{ +} + #[cfg(test)] mod test_drain_filter { use crate::VecMap; diff --git a/crates/compiler/collections/src/vec_set.rs b/crates/compiler/collections/src/vec_set.rs new file mode 100644 index 0000000000..2a19bf2b9c --- /dev/null +++ b/crates/compiler/collections/src/vec_set.rs @@ -0,0 +1,161 @@ +use std::{borrow::Borrow, iter::FromIterator}; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct VecSet { + elements: Vec, +} + +impl Default for VecSet { + fn default() -> Self { + Self { + elements: Vec::new(), + } + } +} + +impl VecSet { + pub fn into_vec(self) -> Vec { + self.elements + } +} + +impl VecSet { + pub fn with_capacity(capacity: usize) -> Self { + Self { + elements: Vec::with_capacity(capacity), + } + } + + pub fn len(&self) -> usize { + self.elements.len() + } + + pub fn is_empty(&self) -> bool { + self.elements.is_empty() + } + + pub fn singleton(value: T) -> Self { + Self { + elements: vec![value], + } + } + + pub fn swap_remove(&mut self, index: usize) -> T { + self.elements.swap_remove(index) + } + + pub fn insert(&mut self, value: T) -> bool { + if self.elements.contains(&value) { + true + } else { + self.elements.push(value); + + false + } + } + + /// Returns true iff any of the given elements previoously existed in the set. + pub fn insert_all>(&mut self, values: I) -> bool { + let mut any_existed = false; + + for value in values { + any_existed = any_existed || self.insert(value); + } + + any_existed + } + + pub fn contains(&self, value: &T) -> bool { + self.elements.contains(value) + } + + /// Performs a swap_remove if the element was present in the set, + /// then returns whether the value was present in the set. + pub fn remove(&mut self, value: &T) -> bool { + match self.elements.iter().position(|x| x == value) { + None => false, + Some(index) => { + self.elements.swap_remove(index); + + true + } + } + } + + pub fn iter(&self) -> impl Iterator { + self.elements.iter() + } + + pub fn iter_mut(&mut self) -> impl Iterator { + self.elements.iter_mut() + } + + /// Removes all elements from the set, without affecting its allocated capacity. + pub fn clear(&mut self) { + self.elements.clear() + } + + /// Retains only the elements specified by the predicate. + pub fn retain(&mut self, f: F) + where + F: FnMut(&T) -> bool, + { + self.elements.retain(f) + } +} + +impl Extend
for VecSet { + fn extend>(&mut self, iter: T) { + let it = iter.into_iter(); + let hint = it.size_hint(); + + match hint { + (0, Some(0)) => { + // done, do nothing + } + (1, Some(1)) | (2, Some(2)) => { + for value in it { + self.insert(value); + } + } + _ => { + self.elements.extend(it); + + self.elements.sort(); + self.elements.dedup(); + } + } + } +} + +impl FromIterator for VecSet { + fn from_iter>(iter: T) -> Self { + let mut set = VecSet::default(); + set.extend(iter); + set + } +} + +impl IntoIterator for VecSet { + type Item = T; + + type IntoIter = std::vec::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.elements.into_iter() + } +} + +impl Borrow<[T]> for VecSet { + fn borrow(&self) -> &[T] { + &self.elements + } +} + +impl From> for VecSet { + fn from(elements: Vec) -> Self { + // Not totally safe, but good enough for our purposes - also, duplicates in the VecSet are + // fine, just inefficient. + Self { elements } + } +} diff --git a/crates/compiler/constrain/Cargo.toml b/crates/compiler/constrain/Cargo.toml new file mode 100644 index 0000000000..01d9414867 --- /dev/null +++ b/crates/compiler/constrain/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "roc_constrain" +description = "Responsible for building the set of constraints that are used during type inference of a program, and for gathering context needed for pleasant error messages when a type error occurs." + +authors.workspace = true +edition.workspace = true +license.workspace = true +version.workspace = true + +[dependencies] +roc_can = { path = "../can" } +roc_collections = { path = "../collections" } +roc_error_macros = { path = "../../error_macros" } +roc_module = { path = "../module" } +roc_parse = { path = "../parse" } +roc_problem = { path = "../problem" } +roc_region = { path = "../region" } +roc_types = { path = "../types" } + +arrayvec.workspace = true diff --git a/crates/compiler/constrain/src/builtins.rs b/crates/compiler/constrain/src/builtins.rs new file mode 100644 index 0000000000..569e137d3d --- /dev/null +++ b/crates/compiler/constrain/src/builtins.rs @@ -0,0 +1,401 @@ +#![allow(clippy::too_many_arguments)] + +use arrayvec::ArrayVec; +use roc_can::constraint::{Constraint, Constraints, ExpectedTypeIndex}; +use roc_can::expected::Expected::{self, *}; +use roc_can::num::{FloatBound, FloatWidth, IntBound, IntLitWidth, NumBound, SignDemand}; +use roc_module::symbol::Symbol; +use roc_region::all::{Loc, Region}; +use roc_types::num::{NumericRange, SingleQuoteBound}; +use roc_types::subs::Variable; +use roc_types::types::Type::{self, *}; +use roc_types::types::{AliasKind, Category, Types}; +use roc_types::types::{OptAbleType, Reason}; + +#[inline(always)] +pub(crate) fn add_numeric_bound_constr( + types: &mut Types, + constraints: &mut Constraints, + num_constraints: &mut impl Extend, + num_var: Variable, + precision_var: Variable, + bound: impl TypedNumericBound, + region: Region, + category: Category, +) -> Type { + let range = bound.numeric_bound(); + + use roc_types::num::{float_width_to_variable, int_lit_width_to_variable}; + + match range { + NumericBound::None => { + // no additional constraints, just a Num * + num_num(Variable(num_var)) + } + NumericBound::FloatExact(width) => { + let actual_type = constraints.push_variable(float_width_to_variable(width)); + let expected = Expected::ForReason(Reason::NumericLiteralSuffix, actual_type, region); + let type_index = constraints.push_variable(num_var); + let expected_index = constraints.push_expected_type(expected); + let because_suffix = + constraints.equal_types(type_index, expected_index, category, region); + + num_constraints.extend([because_suffix]); + + Variable(num_var) + } + NumericBound::IntExact(width) => { + let actual_type = constraints.push_variable(int_lit_width_to_variable(width)); + let expected = Expected::ForReason(Reason::NumericLiteralSuffix, actual_type, region); + let type_index = constraints.push_variable(num_var); + let expected_index = constraints.push_expected_type(expected); + let because_suffix = + constraints.equal_types(type_index, expected_index, category, region); + + num_constraints.extend([because_suffix]); + + Variable(num_var) + } + NumericBound::Range(range) => { + let precision_type = constraints.push_variable(precision_var); + let expected = { + let typ = types.from_old_type(&RangedNumber(range)); + Expected::NoExpectation(constraints.push_type(types, typ)) + }; + let expected_index = constraints.push_expected_type(expected); + let constr = constraints.equal_types(precision_type, expected_index, category, region); + + num_constraints.extend([constr]); + + num_num(Variable(num_var)) + } + } +} + +#[inline(always)] +pub(crate) fn int_literal( + types: &mut Types, + constraints: &mut Constraints, + num_var: Variable, + precision_var: Variable, + expected: ExpectedTypeIndex, + region: Region, + bound: IntBound, +) -> Constraint { + let reason = Reason::IntLiteral; + + // Always add the bound first; this improves the resolved type quality in case it's an alias like "U8". + let mut constrs = ArrayVec::<_, 3>::new(); + let num_type = add_numeric_bound_constr( + types, + constraints, + &mut constrs, + num_var, + precision_var, + bound, + region, + Category::Num, + ); + + let num_type_index = { + let typ = types.from_old_type(&num_type); + constraints.push_type(types, typ) + }; + let int_precision_type = { + let typ = types.from_old_type(&num_int(Type::Variable(precision_var))); + constraints.push_type(types, typ) + }; + + let expect_precision_var = + constraints.push_expected_type(ForReason(reason, int_precision_type, region)); + + constrs.extend([ + constraints.equal_types(num_type_index, expect_precision_var, Category::Int, region), + constraints.equal_types(num_type_index, expected, Category::Int, region), + ]); + + // TODO the precision_var is not part of the exists here; for float it is. Which is correct? + let and_constraint = constraints.and_constraint(constrs); + constraints.exists([num_var], and_constraint) +} + +pub(crate) fn single_quote_literal( + types: &mut Types, + constraints: &mut Constraints, + num_var: Variable, + precision_var: Variable, + expected: ExpectedTypeIndex, + region: Region, + bound: SingleQuoteBound, +) -> Constraint { + let reason = Reason::IntLiteral; + + // Always add the bound first; this improves the resolved type quality in case it's an alias like "U8". + let mut constrs = ArrayVec::<_, 3>::new(); + let num_type = add_numeric_bound_constr( + types, + constraints, + &mut constrs, + num_var, + precision_var, + bound, + region, + Category::Character, + ); + + let num_type_index = { + let typ = types.from_old_type(&num_type); + constraints.push_type(types, typ) + }; + let int_precision_type = { + let typ = types.from_old_type(&num_int(Type::Variable(precision_var))); + constraints.push_type(types, typ) + }; + + let expect_precision_var = + constraints.push_expected_type(ForReason(reason, int_precision_type, region)); + + constrs.extend([ + constraints.equal_types( + num_type_index, + expect_precision_var, + Category::Character, + region, + ), + constraints.equal_types(num_type_index, expected, Category::Character, region), + ]); + + let and_constraint = constraints.and_constraint(constrs); + constraints.exists([num_var], and_constraint) +} + +#[inline(always)] +pub(crate) fn float_literal( + types: &mut Types, + constraints: &mut Constraints, + num_var: Variable, + precision_var: Variable, + expected: ExpectedTypeIndex, + region: Region, + bound: FloatBound, +) -> Constraint { + let reason = Reason::FloatLiteral; + + let mut constrs = ArrayVec::<_, 3>::new(); + let num_type = add_numeric_bound_constr( + types, + constraints, + &mut constrs, + num_var, + precision_var, + bound, + region, + Category::Frac, + ); + + let num_type_index = { + let typ = types.from_old_type(&num_type); + constraints.push_type(types, typ) + }; + let float_precision_type = { + let typ = types.from_old_type(&num_float(Type::Variable(precision_var))); + constraints.push_type(types, typ) + }; + + let expect_precision_var = + constraints.push_expected_type(ForReason(reason, float_precision_type, region)); + + constrs.extend([ + constraints.equal_types(num_type_index, expect_precision_var, Category::Frac, region), + constraints.equal_types(num_type_index, expected, Category::Frac, region), + ]); + + let and_constraint = constraints.and_constraint(constrs); + constraints.exists([num_var, precision_var], and_constraint) +} + +#[inline(always)] +pub(crate) fn num_literal( + types: &mut Types, + constraints: &mut Constraints, + num_var: Variable, + expected: ExpectedTypeIndex, + region: Region, + bound: NumBound, +) -> Constraint { + let mut constrs = ArrayVec::<_, 2>::new(); + let num_type = add_numeric_bound_constr( + types, + constraints, + &mut constrs, + num_var, + num_var, + bound, + region, + Category::Num, + ); + + let type_index = { + let typ = types.from_old_type(&num_type); + constraints.push_type(types, typ) + }; + constrs.extend([constraints.equal_types(type_index, expected, Category::Num, region)]); + + let and_constraint = constraints.and_constraint(constrs); + constraints.exists([num_var], and_constraint) +} + +// Try not to be too clever about inlining, at least in debug builds. +// Inlining these tiny leaf functions can lead to death by a thousand cuts, +// where we end up with huge stack frames in non-tail-recursive functions. +#[cfg_attr(not(debug_assertions), inline(always))] +pub(crate) fn builtin_type(symbol: Symbol, args: Vec) -> Type { + Type::Apply( + symbol, + args.into_iter().map(Loc::at_zero).collect(), + Region::zero(), + ) +} + +#[cfg_attr(not(debug_assertions), inline(always))] +pub(crate) fn empty_list_type(var: Variable) -> Type { + list_type(Type::Variable(var)) +} + +#[cfg_attr(not(debug_assertions), inline(always))] +pub(crate) fn list_type(typ: Type) -> Type { + builtin_type(Symbol::LIST_LIST, vec![typ]) +} + +#[cfg_attr(not(debug_assertions), inline(always))] +fn builtin_num_alias( + symbol: Symbol, + type_arguments: Vec, + actual: Box, + kind: AliasKind, +) -> Type { + Type::Alias { + symbol, + type_arguments, + actual, + lambda_set_variables: vec![], + infer_ext_in_output_types: vec![], + kind, + } +} + +#[cfg_attr(not(debug_assertions), inline(always))] +pub(crate) fn num_float(range: Type) -> Type { + builtin_num_alias( + Symbol::NUM_FRAC, + vec![OptAbleType::unbound(range.clone())], + Box::new(num_num(num_floatingpoint(range))), + AliasKind::Structural, + ) +} + +#[cfg_attr(not(debug_assertions), inline(always))] +pub(crate) fn num_floatingpoint(range: Type) -> Type { + builtin_num_alias( + Symbol::NUM_FLOATINGPOINT, + vec![OptAbleType::unbound(range.clone())], + Box::new(range), + AliasKind::Opaque, + ) +} + +#[cfg_attr(not(debug_assertions), inline(always))] +pub(crate) fn num_int(range: Type) -> Type { + builtin_num_alias( + Symbol::NUM_INT, + vec![OptAbleType::unbound(range.clone())], + Box::new(num_num(num_integer(range))), + AliasKind::Structural, + ) +} + +#[cfg_attr(not(debug_assertions), inline(always))] +pub(crate) fn num_integer(range: Type) -> Type { + builtin_num_alias( + Symbol::NUM_INTEGER, + vec![OptAbleType::unbound(range.clone())], + Box::new(range), + AliasKind::Opaque, + ) +} + +#[cfg_attr(not(debug_assertions), inline(always))] +pub(crate) fn num_num(typ: Type) -> Type { + builtin_num_alias( + Symbol::NUM_NUM, + vec![OptAbleType::unbound(typ.clone())], + Box::new(typ), + AliasKind::Opaque, + ) +} + +pub trait TypedNumericBound { + fn numeric_bound(&self) -> NumericBound; +} + +impl TypedNumericBound for IntBound { + fn numeric_bound(&self) -> NumericBound { + match self { + IntBound::None => NumericBound::None, + IntBound::Exact(w) => NumericBound::IntExact(*w), + IntBound::AtLeast { + sign: SignDemand::NoDemand, + width, + } => NumericBound::Range(NumericRange::IntAtLeastEitherSign(*width)), + IntBound::AtLeast { + sign: SignDemand::Signed, + width, + } => NumericBound::Range(NumericRange::IntAtLeastSigned(*width)), + } + } +} + +impl TypedNumericBound for FloatBound { + fn numeric_bound(&self) -> NumericBound { + match self { + FloatBound::None => NumericBound::None, + FloatBound::Exact(w) => NumericBound::FloatExact(*w), + } + } +} + +impl TypedNumericBound for NumBound { + fn numeric_bound(&self) -> NumericBound { + match self { + NumBound::None => NumericBound::None, + &NumBound::AtLeastIntOrFloat { + sign: SignDemand::NoDemand, + width, + } => NumericBound::Range(NumericRange::NumAtLeastEitherSign(width)), + &NumBound::AtLeastIntOrFloat { + sign: SignDemand::Signed, + width, + } => NumericBound::Range(NumericRange::NumAtLeastSigned(width)), + } + } +} + +impl TypedNumericBound for SingleQuoteBound { + fn numeric_bound(&self) -> NumericBound { + match self { + &SingleQuoteBound::AtLeast { width } => { + NumericBound::Range(NumericRange::IntAtLeastEitherSign(width)) + } + } + } +} + +/// A bound placed on a number because of its literal value. +/// e.g. `-5` cannot be unsigned, and 300 does not fit in a U8 +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum NumericBound { + None, + FloatExact(FloatWidth), + IntExact(IntLitWidth), + Range(NumericRange), +} diff --git a/crates/compiler/constrain/src/expr.rs b/crates/compiler/constrain/src/expr.rs new file mode 100644 index 0000000000..0779f8cfcd --- /dev/null +++ b/crates/compiler/constrain/src/expr.rs @@ -0,0 +1,4522 @@ +#![allow(clippy::too_many_arguments)] + +use std::ops::Range; + +use crate::builtins::{ + empty_list_type, float_literal, int_literal, list_type, num_literal, single_quote_literal, +}; +use crate::pattern::{constrain_pattern, PatternState}; +use roc_can::annotation::IntroducedVariables; +use roc_can::constraint::{ + Constraint, Constraints, ExpectedTypeIndex, Generalizable, OpportunisticResolve, TypeOrVar, +}; +use roc_can::def::Def; +use roc_can::exhaustive::{sketch_pattern_to_rows, sketch_when_branches, ExhaustiveContext}; +use roc_can::expected::Expected::{self, *}; +use roc_can::expected::PExpected; +use roc_can::expr::Expr::{self, *}; +use roc_can::expr::{ + AnnotatedMark, ClosureData, DeclarationTag, Declarations, DestructureDef, ExpectLookup, Field, + FunctionDef, OpaqueWrapFunctionData, StructAccessorData, WhenBranch, +}; +use roc_can::pattern::Pattern; +use roc_can::traverse::symbols_introduced_from_pattern; +use roc_collections::all::{HumanIndex, MutMap, SendMap}; +use roc_collections::soa::{Index, Slice}; +use roc_collections::VecMap; +use roc_module::ident::Lowercase; +use roc_module::symbol::{ModuleId, Symbol}; +use roc_region::all::{Loc, Region}; +use roc_types::subs::{IllegalCycleMark, Variable}; +use roc_types::types::Type::{self, *}; +use roc_types::types::{ + AliasKind, AnnotationSource, Category, IndexOrField, OptAbleType, PReason, Reason, RecordField, + TypeExtension, TypeTag, Types, +}; + +/// This is for constraining Defs +#[derive(Default, Debug)] +pub struct Info { + pub vars: Vec, + pub constraints: Vec, + pub def_types: VecMap>, +} + +impl Info { + pub fn with_capacity(capacity: usize) -> Self { + Info { + vars: Vec::with_capacity(capacity), + constraints: Vec::with_capacity(capacity), + def_types: VecMap::default(), + } + } +} + +pub struct Env { + /// for example `a` in the annotation `identity : a -> a`, we add it to this + /// map so that expressions within that annotation can share these vars. + pub rigids: MutMap, + pub resolutions_to_make: Vec, + pub home: ModuleId, +} + +fn constrain_untyped_args( + types: &mut Types, + constraints: &mut Constraints, + env: &mut Env, + arguments: &[(Variable, AnnotatedMark, Loc)], + closure_type: Type, + return_type: Type, +) -> (Vec, PatternState, Type) { + let mut vars = Vec::with_capacity(arguments.len()); + let mut pattern_types = Vec::with_capacity(arguments.len()); + + let mut pattern_state = PatternState::default(); + + for (pattern_var, annotated_mark, loc_pattern) in arguments { + // Untyped args don't need exhaustiveness checking because they are the source of truth! + let _ = annotated_mark; + + let pattern_type = Variable(*pattern_var); + let pattern_type_index = constraints.push_variable(*pattern_var); + let pattern_expected = + constraints.push_pat_expected_type(PExpected::NoExpectation(pattern_type_index)); + + pattern_types.push(pattern_type); + + constrain_pattern( + types, + constraints, + env, + &loc_pattern.value, + loc_pattern.region, + pattern_expected, + &mut pattern_state, + ); + + vars.push(*pattern_var); + } + + let function_type = + Type::Function(pattern_types, Box::new(closure_type), Box::new(return_type)); + + (vars, pattern_state, function_type) +} + +fn constrain_untyped_closure( + types: &mut Types, + constraints: &mut Constraints, + env: &mut Env, + region: Region, + expected: ExpectedTypeIndex, + + fn_var: Variable, + closure_var: Variable, + ret_var: Variable, + arguments: &[(Variable, AnnotatedMark, Loc)], + loc_body_expr: &Loc, + captured_symbols: &[(Symbol, Variable)], + name: Symbol, +) -> Constraint { + let closure_type = Type::Variable(closure_var); + let return_type = Type::Variable(ret_var); + let return_type_index = constraints.push_variable(ret_var); + let (mut vars, pattern_state, function_type) = constrain_untyped_args( + types, + constraints, + env, + arguments, + closure_type, + return_type, + ); + + vars.push(ret_var); + vars.push(closure_var); + vars.push(fn_var); + + let body_type = constraints.push_expected_type(NoExpectation(return_type_index)); + let ret_constraint = constrain_expr( + types, + constraints, + env, + loc_body_expr.region, + &loc_body_expr.value, + body_type, + ); + + // make sure the captured symbols are sorted! + debug_assert_eq!(captured_symbols.to_vec(), { + let mut copy = captured_symbols.to_vec(); + copy.sort(); + copy + }); + + let closure_constraint = constrain_closure_size( + types, + constraints, + name, + region, + fn_var, + captured_symbols, + closure_var, + &mut vars, + ); + + let pattern_state_constraints = constraints.and_constraint(pattern_state.constraints); + + let function_type = { + let typ = types.from_old_type(&function_type); + constraints.push_type(types, typ) + }; + + let cons = [ + constraints.let_constraint( + [], + pattern_state.vars, + pattern_state.headers, + pattern_state_constraints, + ret_constraint, + Generalizable(true), + ), + constraints.equal_types_with_storage( + function_type, + expected, + Category::Lambda, + region, + fn_var, + ), + closure_constraint, + ]; + + constraints.exists_many(vars, cons) +} + +pub fn constrain_expr( + types: &mut Types, + constraints: &mut Constraints, + env: &mut Env, + region: Region, + expr: &Expr, + expected: ExpectedTypeIndex, +) -> Constraint { + match expr { + &Int(var, precision, _, _, bound) => { + int_literal(types, constraints, var, precision, expected, region, bound) + } + &Num(var, _, _, bound) => num_literal(types, constraints, var, expected, region, bound), + &Float(var, precision, _, _, bound) => { + float_literal(types, constraints, var, precision, expected, region, bound) + } + EmptyRecord => constrain_empty_record(types, constraints, region, expected), + Expr::Record { record_var, fields } => { + if fields.is_empty() { + constrain_empty_record(types, constraints, region, expected) + } else { + let mut field_types = SendMap::default(); + let mut field_vars = Vec::with_capacity(fields.len()); + + // Constraints need capacity for each field + // + 1 for the record itself + 1 for record var + let mut rec_constraints = Vec::with_capacity(2 + fields.len()); + + for (label, field) in fields { + let field_var = field.var; + let loc_field_expr = &field.loc_expr; + let (field_type, field_con) = + constrain_field(types, constraints, env, field_var, loc_field_expr); + + field_vars.push(field_var); + field_types.insert(label.clone(), RecordField::Required(field_type)); + + rec_constraints.push(field_con); + } + + let record_type = { + let typ = + types.from_old_type(&Type::Record(field_types, TypeExtension::Closed)); + constraints.push_type(types, typ) + }; + + let record_con = constraints.equal_types_with_storage( + record_type, + expected, + Category::Record, + region, + *record_var, + ); + + rec_constraints.push(record_con); + field_vars.push(*record_var); + + let and_constraint = constraints.and_constraint(rec_constraints); + constraints.exists(field_vars, and_constraint) + } + } + Expr::Tuple { tuple_var, elems } => { + let mut elem_types = VecMap::with_capacity(elems.len()); + let mut elem_vars = Vec::with_capacity(elems.len()); + + // Constraints need capacity for each elem + // + 1 for the tuple itself + 1 for tuple var + let mut tuple_constraints = Vec::with_capacity(2 + elems.len()); + + for (i, (elem_var, loc_expr)) in elems.iter().enumerate() { + let elem_type = constraints.push_variable(*elem_var); + let elem_expected = constraints.push_expected_type(NoExpectation(elem_type)); + let elem_con = constrain_expr( + types, + constraints, + env, + loc_expr.region, + &loc_expr.value, + elem_expected, + ); + + elem_vars.push(*elem_var); + elem_types.insert(i, Variable(*elem_var)); + + tuple_constraints.push(elem_con); + } + + let tuple_type = { + let typ = types.from_old_type(&Type::Tuple(elem_types, TypeExtension::Closed)); + constraints.push_type(types, typ) + }; + + let tuple_con = constraints.equal_types_with_storage( + tuple_type, + expected, + Category::Tuple, + region, + *tuple_var, + ); + + tuple_constraints.push(tuple_con); + elem_vars.push(*tuple_var); + + let and_constraint = constraints.and_constraint(tuple_constraints); + constraints.exists(elem_vars, and_constraint) + } + RecordUpdate { + record_var, + ext_var, + symbol, + updates, + } => { + let mut fields: SendMap> = SendMap::default(); + let mut vars = Vec::with_capacity(updates.len() + 2); + let mut cons = Vec::with_capacity(updates.len() + 1); + for (field_name, Field { var, loc_expr, .. }) in updates.clone() { + let (var, tipe, con) = constrain_field_update( + types, + constraints, + env, + var, + loc_expr.region, + field_name.clone(), + &loc_expr, + ); + fields.insert(field_name, RecordField::Required(tipe)); + vars.push(var); + cons.push(con); + } + + let fields_type = { + let typ = types.from_old_type(&Type::Record( + fields, + TypeExtension::from_non_annotation_type(Type::Variable(*ext_var)), + )); + constraints.push_type(types, typ) + }; + let record_type = { + let typ = types.from_old_type(&Type::Variable(*record_var)); + constraints.push_type(types, typ) + }; + + // NOTE from elm compiler: fields_type is separate so that Error propagates better + let fields_type_expected = constraints.push_expected_type(NoExpectation(fields_type)); + let fields_con = constraints.equal_types_var( + *record_var, + fields_type_expected, + Category::Record, + region, + ); + let expected_record = expected; + let record_con = + constraints.equal_types_var(*record_var, expected_record, Category::Record, region); + + vars.push(*record_var); + vars.push(*ext_var); + + let record_being_updated_expectation = constraints.push_expected_type(ForReason( + Reason::RecordUpdateKeys( + *symbol, + updates + .iter() + .map(|(key, field)| (key.clone(), field.region)) + .collect(), + ), + record_type, + region, + )); + + let con = constraints.lookup(*symbol, record_being_updated_expectation, region); + + // ensure constraints are solved in this order, gives better errors + cons.insert(0, fields_con); + cons.insert(1, con); + cons.insert(2, record_con); + + let and_constraint = constraints.and_constraint(cons); + constraints.exists(vars, and_constraint) + } + Str(_) => { + let str_index = constraints.push_type(types, Types::STR); + let expected_index = expected; + constraints.equal_types(str_index, expected_index, Category::Str, region) + } + IngestedFile(file_path, bytes, var) => { + let index = constraints.push_variable(*var); + let eq_con = constraints.equal_types( + index, + expected, + Category::IngestedFile(file_path.clone()), + region, + ); + let ingested_con = constraints.ingested_file(index, file_path.clone(), bytes.clone()); + + // First resolve the type variable with the eq_con then try to ingest a file into the correct type. + let and_constraint = constraints.and_constraint(vec![eq_con, ingested_con]); + constraints.exists([*var], and_constraint) + } + SingleQuote(num_var, precision_var, _, bound) => single_quote_literal( + types, + constraints, + *num_var, + *precision_var, + expected, + region, + *bound, + ), + List { + elem_var, + loc_elems, + } => { + if loc_elems.is_empty() { + let elem_type_index = { + let typ = types.from_old_type(&empty_list_type(*elem_var)); + constraints.push_type(types, typ) + }; + let eq = constraints.equal_types(elem_type_index, expected, Category::List, region); + constraints.exists(vec![*elem_var], eq) + } else { + let list_elem_type = Type::Variable(*elem_var); + let list_elem_type_index = constraints.push_variable(*elem_var); + let mut list_constraints = Vec::with_capacity(1 + loc_elems.len()); + + for (index, loc_elem) in loc_elems.iter().enumerate() { + let elem_expected = constraints.push_expected_type(ForReason( + Reason::ElemInList { + index: HumanIndex::zero_based(index), + }, + list_elem_type_index, + loc_elem.region, + )); + let constraint = constrain_expr( + types, + constraints, + env, + loc_elem.region, + &loc_elem.value, + elem_expected, + ); + + list_constraints.push(constraint); + } + + let elem_type_index = { + let typ = types.from_old_type(&list_type(list_elem_type)); + constraints.push_type(types, typ) + }; + list_constraints.push(constraints.equal_types( + elem_type_index, + expected, + Category::List, + region, + )); + + let and_constraint = constraints.and_constraint(list_constraints); + constraints.exists([*elem_var], and_constraint) + } + } + Call(boxed, loc_args, called_via) => { + let (fn_var, loc_fn, closure_var, ret_var) = &**boxed; + // The expression that evaluates to the function being called, e.g. `foo` in + // (foo) bar baz + let opt_symbol = if let Var(symbol, _) | AbilityMember(symbol, _, _) = loc_fn.value { + Some(symbol) + } else { + None + }; + + let fn_type_index = constraints.push_variable(*fn_var); + let fn_region = loc_fn.region; + let fn_expected = constraints.push_expected_type(NoExpectation(fn_type_index)); + + let fn_reason = Reason::FnCall { + name: opt_symbol, + arity: loc_args.len() as u8, + called_via: *called_via, + }; + + let fn_con = constrain_expr( + types, + constraints, + env, + loc_fn.region, + &loc_fn.value, + fn_expected, + ); + + // The function's return type + let ret_type = Variable(*ret_var); + + // type of values captured in the closure + let closure_type = Variable(*closure_var); + + // This will be used in the occurs check + let mut vars = Vec::with_capacity(2 + loc_args.len()); + + vars.push(*fn_var); + vars.push(*ret_var); + vars.push(*closure_var); + + let mut arg_types = Vec::with_capacity(loc_args.len()); + let mut arg_cons = Vec::with_capacity(loc_args.len()); + + for (index, (arg_var, loc_arg)) in loc_args.iter().enumerate() { + let region = loc_arg.region; + let arg_type = Variable(*arg_var); + let arg_type_index = constraints.push_variable(*arg_var); + + let reason = Reason::FnArg { + name: opt_symbol, + arg_index: HumanIndex::zero_based(index), + called_via: *called_via, + }; + let expected_arg = + constraints.push_expected_type(ForReason(reason, arg_type_index, region)); + let arg_con = constrain_expr( + types, + constraints, + env, + loc_arg.region, + &loc_arg.value, + expected_arg, + ); + + vars.push(*arg_var); + arg_types.push(arg_type); + arg_cons.push(arg_con); + } + + let expected_fn_index = { + let arguments = types.from_old_type_slice(arg_types.iter()); + let lambda_set = types.from_old_type(&closure_type); + let ret = types.from_old_type(&ret_type); + let typ = types.function(arguments, lambda_set, ret); + constraints.push_type(types, typ) + }; + let expected_fn_type = + constraints.push_expected_type(ForReason(fn_reason, expected_fn_index, region)); + + let expected_final_type = expected; + + let category = Category::CallResult(opt_symbol, *called_via); + + let and_cons = [ + fn_con, + constraints.equal_types_var(*fn_var, expected_fn_type, category.clone(), fn_region), + constraints.and_constraint(arg_cons), + constraints.equal_types_var(*ret_var, expected_final_type, category, region), + ]; + + let and_constraint = constraints.and_constraint(and_cons); + constraints.exists(vars, and_constraint) + } + Expr::Crash { msg, ret_var } => { + let str_index = constraints.push_type(types, Types::STR); + let expected_msg = constraints.push_expected_type(Expected::ForReason( + Reason::CrashArg, + str_index, + msg.region, + )); + + let msg_is_str = constrain_expr( + types, + constraints, + env, + msg.region, + &msg.value, + expected_msg, + ); + let magic = constraints.equal_types_var(*ret_var, expected, Category::Crash, region); + + let and = constraints.and_constraint([msg_is_str, magic]); + + constraints.exists([*ret_var], and) + } + Var(symbol, variable) => { + // Save the expectation in the variable, then lookup the symbol's type in the environment + let expected_type = *constraints[expected].get_type_ref(); + let store_expected = constraints.store(expected_type, *variable, file!(), line!()); + + let lookup_constr = constraints.lookup(*symbol, expected, region); + + constraints.and_constraint([store_expected, lookup_constr]) + } + &AbilityMember(symbol, specialization_id, specialization_var) => { + // Save the expectation in the `specialization_var` so we know what to specialize, then + // lookup the member in the environment. + let expected_type = *constraints[expected].get_type_ref(); + let store_expected = + constraints.store(expected_type, specialization_var, file!(), line!()); + + let stored_index = constraints.push_variable(specialization_var); + let stored_specialization_var = + constraints.push_expected_type(Expected::NoExpectation(stored_index)); + + let lookup_constr = constraints.lookup(symbol, stored_specialization_var, region); + + // Make sure we attempt to resolve the specialization, if we can. + if let Some(specialization_id) = specialization_id { + env.resolutions_to_make.push(OpportunisticResolve { + specialization_variable: specialization_var, + member: symbol, + specialization_id, + }); + } + + constraints.and_constraint([store_expected, lookup_constr]) + } + Closure(ClosureData { + function_type: fn_var, + closure_type: closure_var, + return_type: ret_var, + arguments, + loc_body: boxed, + captured_symbols, + name, + .. + }) => { + // shared code with function defs without an annotation + constrain_untyped_closure( + types, + constraints, + env, + region, + expected, + *fn_var, + *closure_var, + *ret_var, + arguments, + boxed, + captured_symbols, + *name, + ) + } + + Expect { + loc_condition, + loc_continuation, + lookups_in_cond, + } => { + let expected_bool = { + let bool_type = constraints.push_variable(Variable::BOOL); + constraints.push_expected_type(Expected::ForReason( + Reason::ExpectCondition, + bool_type, + loc_condition.region, + )) + }; + + let cond_con = constrain_expr( + types, + constraints, + env, + loc_condition.region, + &loc_condition.value, + expected_bool, + ); + + let continuation_con = constrain_expr( + types, + constraints, + env, + loc_continuation.region, + &loc_continuation.value, + expected, + ); + + // + 2 for cond_con and continuation_con + let mut all_constraints = Vec::with_capacity(lookups_in_cond.len() + 2); + + all_constraints.push(cond_con); + all_constraints.push(continuation_con); + + let mut vars = Vec::with_capacity(lookups_in_cond.len()); + + for ExpectLookup { + symbol, + var, + ability_info: _, + } in lookups_in_cond.iter() + { + vars.push(*var); + + let var_index = constraints.push_variable(*var); + let store_into = constraints.push_expected_type(NoExpectation(var_index)); + + all_constraints.push(constraints.lookup(*symbol, store_into, Region::zero())); + } + + constraints.exists_many(vars, all_constraints) + } + + ExpectFx { + loc_condition, + loc_continuation, + lookups_in_cond, + } => { + let expected_bool = { + let bool_type = constraints.push_variable(Variable::BOOL); + constraints.push_expected_type(Expected::ForReason( + Reason::ExpectCondition, + bool_type, + loc_condition.region, + )) + }; + + let cond_con = constrain_expr( + types, + constraints, + env, + loc_condition.region, + &loc_condition.value, + expected_bool, + ); + + let continuation_con = constrain_expr( + types, + constraints, + env, + loc_continuation.region, + &loc_continuation.value, + expected, + ); + + // + 2 for cond_con and continuation_con + let mut all_constraints = Vec::with_capacity(lookups_in_cond.len() + 2); + + all_constraints.push(cond_con); + all_constraints.push(continuation_con); + + let mut vars = Vec::with_capacity(lookups_in_cond.len()); + + for ExpectLookup { + symbol, + var, + ability_info: _, + } in lookups_in_cond.iter() + { + vars.push(*var); + + let var_index = constraints.push_variable(*var); + let store_into = constraints.push_expected_type(NoExpectation(var_index)); + + all_constraints.push(constraints.lookup(*symbol, store_into, Region::zero())); + } + + constraints.exists_many(vars, all_constraints) + } + + Dbg { + loc_message, + loc_continuation, + variable, + symbol: _, + } => { + let dbg_type = constraints.push_variable(*variable); + let expected_dbg = constraints.push_expected_type(Expected::NoExpectation(dbg_type)); + + let message_con = constrain_expr( + types, + constraints, + env, + loc_message.region, + &loc_message.value, + expected_dbg, + ); + + let continuation_con = constrain_expr( + types, + constraints, + env, + loc_continuation.region, + &loc_continuation.value, + expected, + ); + + constraints.exists_many([*variable], [message_con, continuation_con]) + } + + If { + cond_var, + branch_var, + branches, + final_else, + } => { + let expect_bool = |constraints: &mut Constraints, region| { + let bool_type = constraints.push_variable(Variable::BOOL); + constraints.push_expected_type(Expected::ForReason( + Reason::IfCondition, + bool_type, + region, + )) + }; + let mut branch_cons = Vec::with_capacity(2 * branches.len() + 3); + + // TODO why does this cond var exist? is it for error messages? + let first_cond_region = branches[0].0.region; + let expected_bool = expect_bool(constraints, first_cond_region); + let cond_var_is_bool_con = constraints.equal_types_var( + *cond_var, + expected_bool, + Category::If, + first_cond_region, + ); + + branch_cons.push(cond_var_is_bool_con); + + let expected = constraints[expected].clone(); + match expected { + FromAnnotation(name, arity, ann_source, tipe) => { + let num_branches = branches.len() + 1; + for (index, (loc_cond, loc_body)) in branches.iter().enumerate() { + let expected_bool = expect_bool(constraints, loc_cond.region); + let cond_con = constrain_expr( + types, + constraints, + env, + loc_cond.region, + &loc_cond.value, + expected_bool, + ); + + let expected_then = constraints.push_expected_type(FromAnnotation( + name.clone(), + arity, + AnnotationSource::TypedIfBranch { + index: HumanIndex::zero_based(index), + num_branches, + region: ann_source.region(), + }, + tipe, + )); + + let then_con = constrain_expr( + types, + constraints, + env, + loc_body.region, + &loc_body.value, + expected_then, + ); + + branch_cons.push(cond_con); + branch_cons.push(then_con); + } + + let expected_else = constraints.push_expected_type(FromAnnotation( + name, + arity, + AnnotationSource::TypedIfBranch { + index: HumanIndex::zero_based(branches.len()), + num_branches, + region: ann_source.region(), + }, + tipe, + )); + let else_con = constrain_expr( + types, + constraints, + env, + final_else.region, + &final_else.value, + expected_else, + ); + + let expected_result_type = constraints.push_expected_type(NoExpectation(tipe)); + + let ast_con = constraints.equal_types_var( + *branch_var, + expected_result_type, + Category::Storage(std::file!(), std::line!()), + region, + ); + + branch_cons.push(ast_con); + branch_cons.push(else_con); + + constraints.exists_many([*cond_var, *branch_var], branch_cons) + } + _ => { + let branch_var_index = constraints.push_variable(*branch_var); + + for (index, (loc_cond, loc_body)) in branches.iter().enumerate() { + let expected_bool = expect_bool(constraints, loc_cond.region); + let cond_con = constrain_expr( + types, + constraints, + env, + loc_cond.region, + &loc_cond.value, + expected_bool, + ); + + let expected_then = constraints.push_expected_type(ForReason( + Reason::IfBranch { + index: HumanIndex::zero_based(index), + total_branches: branches.len(), + }, + branch_var_index, + loc_body.region, + )); + let then_con = constrain_expr( + types, + constraints, + env, + loc_body.region, + &loc_body.value, + expected_then, + ); + + branch_cons.push(cond_con); + branch_cons.push(then_con); + } + let expected_else = constraints.push_expected_type(ForReason( + Reason::IfBranch { + index: HumanIndex::zero_based(branches.len()), + total_branches: branches.len() + 1, + }, + branch_var_index, + final_else.region, + )); + let else_con = constrain_expr( + types, + constraints, + env, + final_else.region, + &final_else.value, + expected_else, + ); + + let expected = constraints.push_expected_type(expected); + + branch_cons.push(constraints.equal_types_var( + *branch_var, + expected, + Category::Storage(std::file!(), std::line!()), + region, + )); + branch_cons.push(else_con); + + constraints.exists_many([*cond_var, *branch_var], branch_cons) + } + } + } + When { + cond_var: real_cond_var, + expr_var, + loc_cond, + branches, + branches_cond_var, + exhaustive, + .. + } => { + let branches_cond_var = *branches_cond_var; + let branches_cond_index = constraints.push_variable(branches_cond_var); + + let body_var = *expr_var; + let body_type_index = constraints.push_variable(body_var); + + let branches_region = { + debug_assert!(!branches.is_empty()); + Region::span_across(&loc_cond.region, &branches.last().unwrap().value.region) + }; + + let branch_expr_reason = + |expected: &Expected, index, branch_region| match expected { + FromAnnotation(name, arity, ann_source, _typ) => { + // NOTE deviation from elm. + // + // in elm, `_typ` is used, but because we have this `expr_var` too + // and need to constrain it, this is what works and gives better error messages + FromAnnotation( + name.clone(), + *arity, + AnnotationSource::TypedWhenBranch { + index, + region: ann_source.region(), + }, + body_type_index, + ) + } + + _ => ForReason(Reason::WhenBranch { index }, body_type_index, branch_region), + }; + + // Our goal is to constrain and introduce variables in all pattern when branch patterns before + // looking at their bodies. + // + // pat1 -> body1 + // *^^^ +~~~~ + // pat2 -> body2 + // *^^^ +~~~~ + // + // * solve first + // + solve second + // + // For a single pattern/body pair, we must introduce variables and symbols defined in the + // pattern before solving the body, since those definitions are effectively let-bound. + // + // But also, we'd like to solve all branch pattern constraints in one swoop before looking at + // the bodies, because the patterns may have presence constraints that expect to be built up + // together. + // + // For this reason, we distinguish the two - and introduce variables in the branch patterns + // as part of the pattern constraint, solving all of those at once, and then solving the body + // constraints. + let mut pattern_vars = Vec::with_capacity(branches.len()); + let mut pattern_headers = SendMap::default(); + let mut pattern_cons = Vec::with_capacity(branches.len() + 2); + let mut delayed_is_open_constraints = Vec::with_capacity(2); + let mut body_cons = Vec::with_capacity(branches.len()); + + for (index, when_branch) in branches.iter().enumerate() { + let expected_pattern = |sub_pattern, sub_region| { + PExpected::ForReason( + PReason::WhenMatch { + index: HumanIndex::zero_based(index), + sub_pattern, + }, + branches_cond_index, + sub_region, + ) + }; + + let ConstrainedBranch { + vars: new_pattern_vars, + headers: new_pattern_headers, + pattern_constraints, + is_open_constrains, + body_constraints, + } = constrain_when_branch_help( + types, + constraints, + env, + region, + when_branch, + expected_pattern, + branch_expr_reason( + &constraints[expected], + HumanIndex::zero_based(index), + when_branch.value.region, + ), + ); + + pattern_vars.extend(new_pattern_vars); + + if cfg!(debug_assertions) { + let intersection: Vec<_> = pattern_headers + .keys() + .filter(|k| new_pattern_headers.contains_key(k)) + .collect(); + + debug_assert!( + intersection.is_empty(), + "Two patterns introduce the same symbols - that's a bug!\n{intersection:?}" + ); + } + + pattern_headers.extend(new_pattern_headers); + pattern_cons.push(pattern_constraints); + delayed_is_open_constraints.extend(is_open_constrains); + + body_cons.push(body_constraints); + } + + // Deviation: elm adds another layer of And nesting + // + // Record the original conditional expression's constraint. + // Each branch's pattern must have the same type + // as the condition expression did. + // + // The return type of each branch must equal the return type of + // the entire when-expression. + + // Layer on the "is-open" constraints at the very end, after we know what the branch + // types are supposed to look like without open-ness. + let is_open_constr = constraints.and_constraint(delayed_is_open_constraints); + pattern_cons.push(is_open_constr); + + // After solving the condition variable with what's expected from the branch patterns, + // check it against the condition expression. + // + // First, solve the condition type. + let real_cond_var = *real_cond_var; + let real_cond_type = constraints.push_variable(real_cond_var); + let expected_real_cond = + constraints.push_expected_type(Expected::NoExpectation(real_cond_type)); + let cond_constraint = constrain_expr( + types, + constraints, + env, + loc_cond.region, + &loc_cond.value, + expected_real_cond, + ); + pattern_cons.push(cond_constraint); + + // Now check the condition against the type expected by the branches. + let sketched_rows = sketch_when_branches(branches_region, branches); + let expected_by_branches = constraints.push_expected_type(Expected::ForReason( + Reason::WhenBranches, + branches_cond_index, + branches_region, + )); + let cond_matches_branches_constraint = constraints.exhaustive( + real_cond_var, + loc_cond.region, + Ok((loc_cond.value.category(), expected_by_branches)), + sketched_rows, + ExhaustiveContext::BadCase, + *exhaustive, + ); + pattern_cons.push(cond_matches_branches_constraint); + + // Solve all the pattern constraints together, introducing variables in the pattern as + // need be before solving the bodies. + let pattern_constraints = constraints.and_constraint(pattern_cons); + let body_constraints = constraints.and_constraint(body_cons); + let when_body_con = constraints.let_constraint( + [], + pattern_vars, + pattern_headers, + pattern_constraints, + body_constraints, + // Never generalize identifiers introduced in branch-patterns + Generalizable(false), + ); + + let result_con = + constraints.equal_types_var(body_var, expected, Category::When, region); + + let total_cons = [when_body_con, result_con]; + let branch_constraints = constraints.and_constraint(total_cons); + + constraints.exists( + [branches_cond_var, real_cond_var, *expr_var], + branch_constraints, + ) + } + RecordAccess { + record_var, + ext_var, + field_var, + loc_expr, + field, + } => { + let ext_var = *ext_var; + let ext_type = Type::Variable(ext_var); + let field_var = *field_var; + let field_type = Type::Variable(field_var); + + let mut rec_field_types = SendMap::default(); + + let label = field.clone(); + rec_field_types.insert(label, RecordField::Demanded(field_type)); + + let record_type = { + let typ = types.from_old_type(&Type::Record( + rec_field_types, + TypeExtension::from_non_annotation_type(ext_type), + )); + constraints.push_type(types, typ) + }; + let record_expected = constraints.push_expected_type(NoExpectation(record_type)); + + let category = Category::RecordAccess(field.clone()); + + let record_con = + constraints.equal_types_var(*record_var, record_expected, category.clone(), region); + + let expected_record = constraints.push_expected_type(NoExpectation(record_type)); + let constraint = constrain_expr( + types, + constraints, + env, + region, + &loc_expr.value, + expected_record, + ); + + let eq = constraints.equal_types_var(field_var, expected, category, region); + constraints.exists_many( + [*record_var, field_var, ext_var], + [constraint, eq, record_con], + ) + } + RecordAccessor(StructAccessorData { + name: closure_name, + function_var, + field, + record_var, + closure_var, + ext_var, + field_var, + }) => { + let ext_var = *ext_var; + let ext_type = Variable(ext_var); + let field_var = *field_var; + let field_type = Variable(field_var); + + let record_type = match field { + IndexOrField::Field(field) => { + let mut field_types = SendMap::default(); + let label = field.clone(); + field_types.insert(label, RecordField::Demanded(field_type.clone())); + Type::Record( + field_types, + TypeExtension::from_non_annotation_type(ext_type), + ) + } + IndexOrField::Index(index) => { + let mut field_types = VecMap::with_capacity(1); + field_types.insert(*index, field_type.clone()); + Type::Tuple( + field_types, + TypeExtension::from_non_annotation_type(ext_type), + ) + } + }; + + let record_type_index = { + let typ = types.from_old_type(&record_type); + constraints.push_type(types, typ) + }; + + let category = Category::Accessor(field.clone()); + + let record_expected = constraints.push_expected_type(NoExpectation(record_type_index)); + let record_con = + constraints.equal_types_var(*record_var, record_expected, category.clone(), region); + + let expected_lambda_set = { + let lambda_set_ty = { + let typ = types.from_old_type(&Type::ClosureTag { + name: *closure_name, + captures: vec![], + ambient_function: *function_var, + }); + constraints.push_type(types, typ) + }; + constraints.push_expected_type(NoExpectation(lambda_set_ty)) + }; + + let closure_type = Type::Variable(*closure_var); + + let function_type_index = { + let typ = types.from_old_type(&Type::Function( + vec![record_type], + Box::new(closure_type), + Box::new(field_type), + )); + constraints.push_type(types, typ) + }; + + let cons = [ + constraints.equal_types_var( + *closure_var, + expected_lambda_set, + category.clone(), + region, + ), + constraints.equal_types(function_type_index, expected, category.clone(), region), + { + let store_fn_var_index = constraints.push_variable(*function_var); + let store_fn_var_expected = + constraints.push_expected_type(NoExpectation(store_fn_var_index)); + constraints.equal_types( + function_type_index, + store_fn_var_expected, + category, + region, + ) + }, + record_con, + ]; + + constraints.exists_many( + [*record_var, *function_var, *closure_var, field_var, ext_var], + cons, + ) + } + TupleAccess { + tuple_var, + ext_var, + elem_var, + loc_expr, + index, + } => { + let mut tup_elem_types = VecMap::with_capacity(1); + + let ext_var = *ext_var; + let ext_type = Variable(ext_var); + + let elem_var = *elem_var; + let elem_type = Type::Variable(elem_var); + + tup_elem_types.insert(*index, elem_type); + + let tuple_type = { + let typ = types.from_old_type(&Type::Tuple( + tup_elem_types, + TypeExtension::from_non_annotation_type(ext_type), + )); + constraints.push_type(types, typ) + }; + let tuple_expected = constraints.push_expected_type(NoExpectation(tuple_type)); + + let category = Category::TupleAccess(*index); + + let tuple_con = + constraints.equal_types_var(*tuple_var, tuple_expected, category.clone(), region); + + let expected_tuple = constraints.push_expected_type(NoExpectation(tuple_type)); + let constraint = constrain_expr( + types, + constraints, + env, + region, + &loc_expr.value, + expected_tuple, + ); + + let eq = constraints.equal_types_var(elem_var, expected, category, region); + constraints.exists_many([*tuple_var, elem_var, ext_var], [constraint, eq, tuple_con]) + } + LetRec(defs, loc_ret, cycle_mark) => { + let body_con = constrain_expr( + types, + constraints, + env, + loc_ret.region, + &loc_ret.value, + expected, + ); + + constrain_recursive_defs(types, constraints, env, defs, body_con, *cycle_mark) + } + LetNonRec(def, loc_ret) => { + let mut stack = Vec::with_capacity(1); + + let mut loc_ret = loc_ret; + + stack.push(def); + + while let LetNonRec(def, new_loc_ret) = &loc_ret.value { + stack.push(def); + loc_ret = new_loc_ret; + } + + let mut body_con = constrain_expr( + types, + constraints, + env, + loc_ret.region, + &loc_ret.value, + expected, + ); + + while let Some(def) = stack.pop() { + body_con = constrain_def(types, constraints, env, def, body_con) + } + + body_con + } + Tag { + tag_union_var: variant_var, + ext_var, + name, + arguments, + } => { + // +2 because we push all the arguments, plus variant_var and ext_var + let num_vars = arguments.len() + 2; + let mut vars = Vec::with_capacity(num_vars); + let mut payload_types = Vec::with_capacity(arguments.len()); + let mut arg_cons = Vec::with_capacity(arguments.len()); + + for (var, loc_expr) in arguments { + let var_index = constraints.push_variable(*var); + let expected_arg = constraints.push_expected_type(NoExpectation(var_index)); + let arg_con = constrain_expr( + types, + constraints, + env, + loc_expr.region, + &loc_expr.value, + expected_arg, + ); + + arg_cons.push(arg_con); + vars.push(*var); + payload_types.push(Type::Variable(*var)); + } + + let tag_union_type = { + let typ = types.from_old_type(&Type::TagUnion( + vec![(name.clone(), payload_types)], + TypeExtension::from_non_annotation_type(Type::Variable(*ext_var)), + )); + constraints.push_type(types, typ) + }; + + let union_con = constraints.equal_types_with_storage( + tag_union_type, + expected, + Category::TagApply { + tag_name: name.clone(), + args_count: arguments.len(), + }, + region, + *variant_var, + ); + + vars.push(*variant_var); + vars.push(*ext_var); + arg_cons.push(union_con); + + constraints.exists_many(vars, arg_cons) + } + ZeroArgumentTag { + variant_var, + ext_var, + name, + closure_name, + } => { + let function_or_tag_union = { + let typ = types.from_old_type(&Type::FunctionOrTagUnion( + name.clone(), + *closure_name, + TypeExtension::from_non_annotation_type(Type::Variable(*ext_var)), + )); + constraints.push_type(types, typ) + }; + let union_con = constraints.equal_types_with_storage( + function_or_tag_union, + expected, + Category::TagApply { + tag_name: name.clone(), + args_count: 0, + }, + region, + *variant_var, + ); + + constraints.exists_many([*variant_var, *ext_var], [union_con]) + } + OpaqueRef { + opaque_var, + name, + argument, + specialized_def_type, + type_arguments, + lambda_set_variables, + } => { + let (arg_var, arg_loc_expr) = &**argument; + let arg_type = Type::Variable(*arg_var); + let arg_type_index = constraints.push_variable(*arg_var); + + let opaque_type = { + let typ = types.from_old_type(&Type::Alias { + symbol: *name, + type_arguments: type_arguments + .iter() + .map(|v| OptAbleType { + typ: Type::Variable(v.var), + opt_abilities: v.opt_abilities.clone(), + }) + .collect(), + lambda_set_variables: lambda_set_variables.clone(), + infer_ext_in_output_types: vec![], + actual: Box::new(arg_type), + kind: AliasKind::Opaque, + }); + constraints.push_type(types, typ) + }; + + // Constrain the argument + let expected_arg = + constraints.push_expected_type(Expected::NoExpectation(arg_type_index)); + let arg_con = constrain_expr( + types, + constraints, + env, + arg_loc_expr.region, + &arg_loc_expr.value, + expected_arg, + ); + + // Link the entire wrapped opaque type (with the now-constrained argument) to the + // expected type + let opaque_con = constraints.equal_types_with_storage( + opaque_type, + expected, + Category::OpaqueWrap(*name), + region, + *opaque_var, + ); + + // Link the entire wrapped opaque type (with the now-constrained argument) to the type + // variables of the opaque type + // TODO: better expectation here + let link_type_variables_con = { + let specialized_type_index = { + let typ = types.from_old_type(specialized_def_type); + constraints.push_type(types, typ) + }; + let expected_index = + constraints.push_expected_type(Expected::NoExpectation(specialized_type_index)); + constraints.equal_types( + arg_type_index, + expected_index, + Category::OpaqueArg, + arg_loc_expr.region, + ) + }; + + let mut vars = vec![*arg_var, *opaque_var]; + // Also add the fresh variables we created for the type argument and lambda sets + vars.extend(type_arguments.iter().map(|v| v.var)); + vars.extend(lambda_set_variables.iter().map(|v| { + v.0.expect_variable("all lambda sets should be fresh variables here") + })); + + constraints.exists_many(vars, [arg_con, opaque_con, link_type_variables_con]) + } + OpaqueWrapFunction(OpaqueWrapFunctionData { + opaque_name, + opaque_var, + specialized_def_type, + type_arguments, + lambda_set_variables, + function_name, + function_var, + argument_var, + closure_var, + }) => { + let argument_type = Type::Variable(*argument_var); + + let opaque_type = { + let typ = types.from_old_type(&Type::Alias { + symbol: *opaque_name, + type_arguments: type_arguments + .iter() + .map(|v| OptAbleType { + typ: Type::Variable(v.var), + opt_abilities: v.opt_abilities.clone(), + }) + .collect(), + lambda_set_variables: lambda_set_variables.clone(), + infer_ext_in_output_types: vec![], + actual: Box::new(argument_type.clone()), + kind: AliasKind::Opaque, + }); + constraints.push_type(types, typ) + }; + + let expected_opaque_type = constraints.push_expected_type(NoExpectation(opaque_type)); + + // Tie the opaque type to the opaque_var + let opaque_con = constraints.equal_types_var( + *opaque_var, + expected_opaque_type, + Category::OpaqueWrap(*opaque_name), + region, + ); + + // Tie the type of the value wrapped by the opaque to the opaque's type variables. + let link_type_variables_con = { + let arg_type_index = { + let typ = types.from_old_type(&argument_type); + constraints.push_type(types, typ) + }; + let specialized_type_index = { + let typ = types.from_old_type(specialized_def_type); + constraints.push_type(types, typ) + }; + let expected_specialized = + constraints.push_expected_type(Expected::NoExpectation(specialized_type_index)); + constraints.equal_types( + arg_type_index, + expected_specialized, + Category::OpaqueArg, + region, + ) + }; + + let lambda_set = { + let lambda_set_index = { + let typ = types.from_old_type(&Type::ClosureTag { + name: *function_name, + captures: vec![], + ambient_function: *function_var, + }); + constraints.push_type(types, typ) + }; + constraints.push_expected_type(NoExpectation(lambda_set_index)) + }; + + let closure_type = Type::Variable(*closure_var); + + let opaque_type = Type::Variable(*opaque_var); + + let expected_function_type = { + let fn_type = { + let typ = types.from_old_type(&Type::Function( + vec![argument_type], + Box::new(closure_type), + Box::new(opaque_type), + )); + constraints.push_type(types, typ) + }; + constraints.push_expected_type(NoExpectation(fn_type)) + }; + + let cons = [ + opaque_con, + link_type_variables_con, + constraints.equal_types_var( + *closure_var, + lambda_set, + Category::OpaqueWrap(*opaque_name), + region, + ), + constraints.equal_types_var( + *function_var, + expected_function_type, + Category::OpaqueWrap(*opaque_name), + region, + ), + constraints.equal_types_var( + *function_var, + expected, + Category::OpaqueWrap(*opaque_name), + region, + ), + ]; + + let mut vars = vec![*argument_var, *opaque_var]; + + // Also add the fresh variables we created for the type argument and lambda sets + vars.extend(type_arguments.iter().map(|v| v.var)); + vars.extend(lambda_set_variables.iter().map(|v| { + v.0.expect_variable("all lambda sets should be fresh variables here") + })); + + vars.extend([*function_var, *closure_var]); + + constraints.exists_many(vars, cons) + } + + RunLowLevel { args, ret_var, op } => { + // This is a modified version of what we do for function calls. + + // This will be used in the occurs check + let mut vars = Vec::with_capacity(1 + args.len()); + + vars.push(*ret_var); + + let mut arg_types = Vec::with_capacity(args.len()); + let mut arg_cons = Vec::with_capacity(args.len()); + + let mut add_arg = |constraints: &mut Constraints, index, arg_type: TypeOrVar, arg| { + let reason = Reason::LowLevelOpArg { + op: *op, + arg_index: HumanIndex::zero_based(index), + }; + let expected_arg = + constraints.push_expected_type(ForReason(reason, arg_type, Region::zero())); + let arg_con = + constrain_expr(types, constraints, env, Region::zero(), arg, expected_arg); + + arg_types.push(arg_type); + arg_cons.push(arg_con); + }; + + for (index, (arg_var, arg)) in args.iter().enumerate() { + vars.push(*arg_var); + let arg_var_index = constraints.push_variable(*arg_var); + + add_arg(constraints, index, arg_var_index, arg); + } + + let category = Category::LowLevelOpResult(*op); + + // Deviation: elm uses an additional And here + let eq = constraints.equal_types_var(*ret_var, expected, category, region); + arg_cons.push(eq); + constraints.exists_many(vars, arg_cons) + } + ForeignCall { + args, + ret_var, + foreign_symbol, + } => { + // This is a modified version of what we do for function calls. + + // This will be used in the occurs check + let mut vars = Vec::with_capacity(1 + args.len()); + + vars.push(*ret_var); + + let mut arg_types = Vec::with_capacity(args.len()); + let mut arg_cons = Vec::with_capacity(args.len()); + + let mut add_arg = |constraints: &mut Constraints, index, arg_type: TypeOrVar, arg| { + let reason = Reason::ForeignCallArg { + foreign_symbol: foreign_symbol.clone(), + arg_index: HumanIndex::zero_based(index), + }; + let expected_arg = + constraints.push_expected_type(ForReason(reason, arg_type, Region::zero())); + let arg_con = + constrain_expr(types, constraints, env, Region::zero(), arg, expected_arg); + + arg_types.push(arg_type); + arg_cons.push(arg_con); + }; + + for (index, (arg_var, arg)) in args.iter().enumerate() { + vars.push(*arg_var); + let arg_var_index = constraints.push_variable(*arg_var); + + add_arg(constraints, index, arg_var_index, arg); + } + + let category = Category::ForeignCall; + + // Deviation: elm uses an additional And here + let eq = constraints.equal_types_var(*ret_var, expected, category, region); + arg_cons.push(eq); + constraints.exists_many(vars, arg_cons) + } + TypedHole(var) => { + // store the expected type for this position + constraints.equal_types_var( + *var, + expected, + Category::Storage(std::file!(), std::line!()), + region, + ) + } + RuntimeError(_) => { + // Runtime Errors are always going to crash, so they don't introduce any new + // constraints. + // Instead, trivially equate the expected type to itself. This will never yield + // unification errors but it will catch errors in type translation, including ability + // obligations. + let trivial_type = *constraints[expected].get_type_ref(); + constraints.equal_types(trivial_type, expected, Category::Unknown, region) + } + } +} + +fn constrain_function_def( + types: &mut Types, + constraints: &mut Constraints, + env: &mut Env, + declarations: &Declarations, + index: usize, + function_def_index: Index>, + body_con: Constraint, +) -> Constraint { + let loc_expr = &declarations.expressions[index]; + let loc_symbol = declarations.symbols[index]; + let expr_var = declarations.variables[index]; + let expr_var_index = constraints.push_variable(expr_var); + let opt_annotation = &declarations.annotations[index]; + + let loc_function_def = &declarations.function_bodies[function_def_index.index()]; + let function_def = &loc_function_def.value; + + match opt_annotation { + Some(annotation) => { + let loc_pattern = Loc::at(loc_symbol.region, Pattern::Identifier(loc_symbol.value)); + let loc_body_expr = loc_expr; + + let arity = annotation.signature.arity(); + let rigids = &env.rigids; + let mut ftv = rigids.clone(); + + let InstantiateRigids { + signature, + new_rigid_variables, + new_infer_variables, + } = instantiate_rigids_simple( + types, + &annotation.signature, + &annotation.introduced_variables, + &mut ftv, + ); + + let signature_index = constraints.push_type(types, signature); + + let (arg_types, _signature_closure_type, ret_type) = match types[signature] { + TypeTag::Function(signature_closure_type, ret_type) => ( + types.get_type_arguments(signature), + signature_closure_type, + ret_type, + ), + _ => { + // aliases, or just something weird + + let def_pattern_state = { + let mut def_pattern_state = PatternState::default(); + + def_pattern_state.headers.insert( + loc_symbol.value, + Loc { + region: loc_function_def.region, + // todo can we use Type::Variable(expr_var) here? + value: signature_index, + }, + ); + + // TODO see if we can get away with not adding this constraint at all + def_pattern_state.vars.push(expr_var); + let annotation_expected = FromAnnotation( + loc_pattern.clone(), + arity, + AnnotationSource::TypedBody { + region: annotation.region, + }, + signature_index, + ); + + { + let expected_index = + constraints.push_expected_type(annotation_expected); + def_pattern_state.constraints.push(constraints.equal_types( + expr_var_index, + expected_index, + Category::Storage(std::file!(), std::line!()), + Region::span_across(&annotation.region, &loc_body_expr.region), + )); + } + + def_pattern_state + }; + + let annotation_expected = constraints.push_expected_type(FromAnnotation( + loc_pattern, + arity, + AnnotationSource::TypedBody { + region: annotation.region, + }, + signature_index, + )); + + let ret_constraint = constrain_untyped_closure( + types, + constraints, + env, + loc_function_def.region, + annotation_expected, + expr_var, + function_def.closure_type, + function_def.return_type, + &function_def.arguments, + loc_body_expr, + &function_def.captured_symbols, + loc_symbol.value, + ); + + let ret_constraint = + attach_resolution_constraints(constraints, env, ret_constraint); + + let cons = [ + ret_constraint, + // Store type into AST vars. We use Store so errors aren't reported twice + constraints.store(signature_index, expr_var, std::file!(), std::line!()), + ]; + let expr_con = constraints.and_constraint(cons); + + return constrain_function_def_make_constraint( + constraints, + new_rigid_variables, + new_infer_variables, + expr_con, + body_con, + def_pattern_state, + ); + } + }; + + let env = &mut Env { + home: env.home, + rigids: ftv, + resolutions_to_make: vec![], + }; + + let region = loc_function_def.region; + + let mut argument_pattern_state = PatternState { + headers: VecMap::default(), + vars: Vec::with_capacity(function_def.arguments.len()), + constraints: Vec::with_capacity(1), + delayed_is_open_constraints: vec![], + }; + let mut vars = Vec::with_capacity(argument_pattern_state.vars.capacity() + 1); + let closure_var = function_def.closure_type; + + let ret_type_index = constraints.push_type(types, ret_type); + + vars.push(function_def.return_type); + vars.push(function_def.closure_type); + + let mut def_pattern_state = PatternState::default(); + + def_pattern_state.headers.insert( + loc_symbol.value, + Loc { + region: loc_function_def.region, + // NOTE: we MUST use `expr_var` here so that the correct type variable is + // associated with the function. We prefer this to the annotation type, because the + // annotation type may be instantiated into a fresh type variable that is + // disassociated fromt the rest of the program. + // Below, we'll check that the function actually matches the annotation. + value: expr_var_index, + }, + ); + + // TODO see if we can get away with not adding this constraint at all + def_pattern_state.vars.push(expr_var); + let annotation_expected = { + constraints.push_expected_type(FromAnnotation( + loc_pattern.clone(), + arity, + AnnotationSource::TypedBody { + region: annotation.region, + }, + signature_index, + )) + }; + + { + let expr_type_index = constraints.push_variable(expr_var); + def_pattern_state.constraints.push(constraints.equal_types( + expr_type_index, + annotation_expected, + Category::Storage(std::file!(), std::line!()), + Region::span_across(&annotation.region, &loc_body_expr.region), + )); + } + + constrain_typed_function_arguments_simple( + types, + constraints, + env, + loc_symbol.value, + &mut def_pattern_state, + &mut argument_pattern_state, + &function_def.arguments, + arg_types, + ); + + let closure_constraint = constrain_closure_size( + types, + constraints, + loc_symbol.value, + region, + expr_var, + &function_def.captured_symbols, + closure_var, + &mut vars, + ); + + let return_type_annotation_expected = constraints.push_expected_type(FromAnnotation( + loc_pattern, + arity, + AnnotationSource::TypedBody { + region: annotation.region, + }, + ret_type_index, + )); + + let solved_fn_type = { + // TODO(types-soa) optimize for Variable + let pattern_types = types.from_old_type_slice( + function_def.arguments.iter().map(|a| Type::Variable(a.0)), + ); + let lambda_set = types.from_old_type(&Type::Variable(function_def.closure_type)); + let ret_var = types.from_old_type(&Type::Variable(function_def.return_type)); + + let fn_type = types.function(pattern_types, lambda_set, ret_var); + constraints.push_type(types, fn_type) + }; + + let ret_constraint = { + let con = constrain_expr( + types, + constraints, + env, + loc_body_expr.region, + &loc_body_expr.value, + return_type_annotation_expected, + ); + attach_resolution_constraints(constraints, env, con) + }; + + vars.push(expr_var); + + let defs_constraint = constraints.and_constraint(argument_pattern_state.constraints); + + let cons = [ + constraints.let_constraint( + [], + argument_pattern_state.vars, + argument_pattern_state.headers, + defs_constraint, + ret_constraint, + // This is a syntactic function, it can be generalized + Generalizable(true), + ), + // Store the inferred ret var into the function type now, so that + // when we check that the solved function type matches the annotation, we can + // display the fully inferred return variable. + constraints.store( + ret_type_index, + function_def.return_type, + std::file!(), + std::line!(), + ), + // Now, check the solved function type matches the annotation. + constraints.equal_types( + solved_fn_type, + annotation_expected, + Category::Lambda, + region, + ), + // Finally put the solved closure type into the dedicated def expr variable. + constraints.store(signature_index, expr_var, std::file!(), std::line!()), + closure_constraint, + ]; + + let expr_con = constraints.exists_many(vars, cons); + + constrain_function_def_make_constraint( + constraints, + new_rigid_variables, + new_infer_variables, + expr_con, + body_con, + def_pattern_state, + ) + } + None => { + let expr_type = constraints.push_variable(expr_var); + + let expected_expr = constraints.push_expected_type(NoExpectation(expr_type)); + let expr_con = constrain_untyped_closure( + types, + constraints, + env, + loc_function_def.region, + expected_expr, + expr_var, + function_def.closure_type, + function_def.return_type, + &function_def.arguments, + loc_expr, + &function_def.captured_symbols, + loc_symbol.value, + ); + + let expr_con = attach_resolution_constraints(constraints, env, expr_con); + + constrain_value_def_make_constraint( + constraints, + vec![], + vec![], + expr_con, + body_con, + loc_symbol, + expr_var, + expr_type, + Generalizable(true), // this is a syntactic function + ) + } + } +} + +fn constrain_destructure_def( + types: &mut Types, + constraints: &mut Constraints, + env: &mut Env, + declarations: &Declarations, + index: usize, + destructure_def_index: Index, + body_con: Constraint, +) -> Constraint { + let loc_expr = &declarations.expressions[index]; + let expr_var = declarations.variables[index]; + let expr_var_index = constraints.push_variable(expr_var); + let opt_annotation = &declarations.annotations[index]; + + let destructure_def = &declarations.destructs[destructure_def_index.index()]; + let loc_pattern = &destructure_def.loc_pattern; + + let mut def_pattern_state = + constrain_def_pattern(types, constraints, env, loc_pattern, expr_var_index); + + def_pattern_state.vars.push(expr_var); + + match opt_annotation { + Some(annotation) => { + let arity = 1; + let rigids = &env.rigids; + let mut ftv = rigids.clone(); + + let InstantiateRigids { + signature, + new_rigid_variables, + new_infer_variables, + } = instantiate_rigids( + types, + constraints, + &annotation.signature, + &annotation.introduced_variables, + loc_pattern, + &mut ftv, + &mut def_pattern_state.headers, + IsRecursiveDef::No, + ); + + let env = &mut Env { + home: env.home, + rigids: ftv, + resolutions_to_make: vec![], + }; + + let signature_index = constraints.push_type(types, signature); + + let annotation_expected = constraints.push_expected_type(FromAnnotation( + loc_pattern.clone(), + arity, + AnnotationSource::TypedBody { + region: annotation.region, + }, + signature_index, + )); + + // This will fill in inference variables in the `signature` as well, so that we can + // then take the signature as the source-of-truth without having to worry about + // incompleteness. + let ret_constraint = constrain_expr( + types, + constraints, + env, + loc_expr.region, + &loc_expr.value, + annotation_expected, + ); + + let cons = [ + ret_constraint, + // Store type into AST vars. We use Store so errors aren't reported twice + constraints.store(signature_index, expr_var, std::file!(), std::line!()), + ]; + let expr_con = constraints.and_constraint(cons); + + constrain_function_def_make_constraint( + constraints, + new_rigid_variables, + new_infer_variables, + expr_con, + body_con, + def_pattern_state, + ) + } + None => { + let expr_type = constraints.push_variable(expr_var); + + let expected_type = constraints.push_expected_type(NoExpectation(expr_type)); + let expr_con = constrain_expr( + types, + constraints, + env, + loc_expr.region, + &loc_expr.value, + expected_type, + ); + + constrain_function_def_make_constraint( + constraints, + vec![], + vec![], + expr_con, + body_con, + def_pattern_state, + ) + } + } +} + +fn constrain_value_def( + types: &mut Types, + constraints: &mut Constraints, + env: &mut Env, + declarations: &Declarations, + index: usize, + body_con: Constraint, +) -> Constraint { + let loc_expr = &declarations.expressions[index]; + let loc_symbol = declarations.symbols[index]; + let expr_var = declarations.variables[index]; + let opt_annotation = &declarations.annotations[index]; + + let generalizable = Generalizable(is_generalizable_expr(&loc_expr.value)); + + match opt_annotation { + Some(annotation) => { + let arity = 1; + let rigids = &env.rigids; + let mut ftv = rigids.clone(); + + let InstantiateRigids { + signature, + new_rigid_variables, + new_infer_variables, + } = instantiate_rigids_simple( + types, + &annotation.signature, + &annotation.introduced_variables, + &mut ftv, + ); + + let env = &mut Env { + home: env.home, + rigids: ftv, + resolutions_to_make: vec![], + }; + + let loc_pattern = Loc::at(loc_symbol.region, Pattern::Identifier(loc_symbol.value)); + + let signature_index = constraints.push_type(types, signature); + + let annotation_expected = constraints.push_expected_type(FromAnnotation( + loc_pattern, + arity, + AnnotationSource::TypedBody { + region: annotation.region, + }, + signature_index, + )); + + // This will fill in inference variables in the `signature` as well, so that we can + // then take the signature as the source-of-truth without having to worry about + // incompleteness. + let ret_constraint = constrain_expr( + types, + constraints, + env, + loc_expr.region, + &loc_expr.value, + annotation_expected, + ); + let ret_constraint = attach_resolution_constraints(constraints, env, ret_constraint); + + let cons = [ + ret_constraint, + // Store type into AST vars. We use Store so errors aren't reported twice + constraints.store(signature_index, expr_var, std::file!(), std::line!()), + ]; + let expr_con = constraints.and_constraint(cons); + + constrain_value_def_make_constraint( + constraints, + new_rigid_variables, + new_infer_variables, + expr_con, + body_con, + loc_symbol, + expr_var, + signature_index, + generalizable, + ) + } + None => { + let expr_type = constraints.push_variable(expr_var); + + let expected_type = constraints.push_expected_type(NoExpectation(expr_type)); + let expr_con = constrain_expr( + types, + constraints, + env, + loc_expr.region, + &loc_expr.value, + expected_type, + ); + + let expr_con = attach_resolution_constraints(constraints, env, expr_con); + + constrain_value_def_make_constraint( + constraints, + vec![], + vec![], + expr_con, + body_con, + loc_symbol, + expr_var, + expr_type, + generalizable, + ) + } + } +} + +struct ConstrainedBranch { + vars: Vec, + headers: VecMap>, + pattern_constraints: Constraint, + is_open_constrains: Vec, + body_constraints: Constraint, +} + +/// Constrain a when branch, returning (variables in pattern, symbols introduced in pattern, pattern constraint, body constraint). +/// We want to constraint all pattern constraints in a "when" before body constraints. +#[inline(always)] +fn constrain_when_branch_help( + types: &mut Types, + constraints: &mut Constraints, + env: &mut Env, + region: Region, + when_branch: &WhenBranch, + pattern_expected: impl Fn(HumanIndex, Region) -> PExpected, + expr_expected: Expected, +) -> ConstrainedBranch { + let expr_expected = constraints.push_expected_type(expr_expected); + let ret_constraint = constrain_expr( + types, + constraints, + env, + region, + &when_branch.value.value, + expr_expected, + ); + + let mut state = PatternState { + headers: VecMap::default(), + vars: Vec::with_capacity(2), + constraints: Vec::with_capacity(2), + delayed_is_open_constraints: Vec::new(), + }; + + for (i, loc_pattern) in when_branch.patterns.iter().enumerate() { + let pattern_expected = constraints.push_pat_expected_type(pattern_expected( + HumanIndex::zero_based(i), + loc_pattern.pattern.region, + )); + + let mut partial_state = PatternState::default(); + constrain_pattern( + types, + constraints, + env, + &loc_pattern.pattern.value, + loc_pattern.pattern.region, + pattern_expected, + &mut partial_state, + ); + + state.vars.extend(partial_state.vars); + state.constraints.extend(partial_state.constraints); + state + .delayed_is_open_constraints + .extend(partial_state.delayed_is_open_constraints); + + if i == 0 { + state.headers.extend(partial_state.headers); + } else { + // Make sure the bound variables in the patterns on the same branch agree in their types. + for (sym, all_branches_bound_typ) in state.headers.iter() { + if let Some(this_bound_typ) = partial_state.headers.get(sym) { + let whole_typ = all_branches_bound_typ.value; + let this_typ = constraints + .push_expected_type(Expected::NoExpectation(this_bound_typ.value)); + + state.constraints.push(constraints.equal_types( + whole_typ, + this_typ, + Category::When, + this_bound_typ.region, + )); + } + + // If the pattern doesn't bind all symbols introduced in the branch we'll have + // reported a canonicalization error, but still might reach here; that's okay. + } + + // Add any variables this pattern binds that the other patterns don't bind. + // This will already have been reported as an error, but we still might be able to + // solve their types. + for (sym, ty) in partial_state.headers { + if !state.headers.contains_key(&sym) { + state.headers.insert(sym, ty); + } + } + } + } + + let (pattern_constraints, delayed_is_open_constraints, body_constraints) = + if let Some(loc_guard) = &when_branch.guard { + let bool_index = constraints.push_variable(Variable::BOOL); + let expected_guard = constraints.push_expected_type(Expected::ForReason( + Reason::WhenGuard, + bool_index, + loc_guard.region, + )); + + let guard_constraint = constrain_expr( + types, + constraints, + env, + region, + &loc_guard.value, + expected_guard, + ); + + // must introduce the headers from the pattern before constraining the guard + let delayed_is_open_constraints = state.delayed_is_open_constraints; + let state_constraints = constraints.and_constraint(state.constraints); + let inner = constraints.let_constraint( + [], + [], + [], + guard_constraint, + ret_constraint, + // Never generalize identifiers introduced in branch guards + Generalizable(false), + ); + + (state_constraints, delayed_is_open_constraints, inner) + } else { + let delayed_is_open_constraints = state.delayed_is_open_constraints; + let state_constraints = constraints.and_constraint(state.constraints); + ( + state_constraints, + delayed_is_open_constraints, + ret_constraint, + ) + }; + + ConstrainedBranch { + vars: state.vars, + headers: state.headers, + pattern_constraints, + is_open_constrains: delayed_is_open_constraints, + body_constraints, + } +} + +fn constrain_field( + types: &mut Types, + constraints: &mut Constraints, + env: &mut Env, + field_var: Variable, + loc_expr: &Loc, +) -> (Type, Constraint) { + let field_type = constraints.push_variable(field_var); + let field_expected = constraints.push_expected_type(NoExpectation(field_type)); + let constraint = constrain_expr( + types, + constraints, + env, + loc_expr.region, + &loc_expr.value, + field_expected, + ); + + (Variable(field_var), constraint) +} + +#[inline(always)] +fn constrain_empty_record( + types: &mut Types, + constraints: &mut Constraints, + region: Region, + expected: ExpectedTypeIndex, +) -> Constraint { + let record_type_index = constraints.push_type(types, Types::EMPTY_RECORD); + constraints.equal_types(record_type_index, expected, Category::Record, region) +} + +fn add_host_annotation( + types: &mut Types, + constraints: &mut Constraints, + host_exposed_annotation: Option<&(Variable, roc_can::def::Annotation)>, + constraint: Constraint, +) -> Constraint { + if let Some((var, ann)) = host_exposed_annotation { + let host_annotation = { + let type_index = types.from_old_type(&ann.signature); + constraints.push_type(types, type_index) + }; + + let store_constr = constraints.store(host_annotation, *var, file!(), line!()); + + constraints.and_constraint([store_constr, constraint]) + } else { + constraint + } +} + +/// Constrain top-level module declarations +#[inline(always)] +pub fn constrain_decls( + types: &mut Types, + constraints: &mut Constraints, + home: ModuleId, + declarations: &Declarations, +) -> Constraint { + let mut constraint = Constraint::SaveTheEnvironment; + + let mut env = Env { + home, + rigids: MutMap::default(), + resolutions_to_make: vec![], + }; + + debug_assert_eq!(declarations.declarations.len(), declarations.symbols.len()); + + let mut index = 0; + while index < declarations.len() { + // Clear the rigids from the previous iteration. + // rigids are not shared between top-level definitions + env.rigids.clear(); + + use roc_can::expr::DeclarationTag::*; + let tag = declarations.declarations[index]; + + match tag { + Value => { + constraint = constrain_value_def( + types, + constraints, + &mut env, + declarations, + index, + constraint, + ); + + constraint = add_host_annotation( + types, + constraints, + declarations.host_exposed_annotations.get(&index), + constraint, + ); + } + Function(function_def_index) => { + constraint = constrain_function_def( + types, + constraints, + &mut env, + declarations, + index, + function_def_index, + constraint, + ); + + constraint = add_host_annotation( + types, + constraints, + declarations.host_exposed_annotations.get(&index), + constraint, + ); + } + Recursive(_) | TailRecursive(_) => { + constraint = add_host_annotation( + types, + constraints, + declarations.host_exposed_annotations.get(&index), + constraint, + ); + + // for the type it does not matter that a recursive call is a tail call + constraint = constrain_recursive_declarations( + types, + constraints, + &mut env, + declarations, + index..index + 1, + constraint, + IllegalCycleMark::empty(), + ); + } + Destructure(destructure_def_index) => { + constraint = constrain_destructure_def( + types, + constraints, + &mut env, + declarations, + index, + destructure_def_index, + constraint, + ); + } + MutualRecursion { length, cycle_mark } => { + // the next `length` defs belong to this group + let length = length as usize; + + constraint = constrain_recursive_declarations( + types, + constraints, + &mut env, + declarations, + index + 1..index + 1 + length, + constraint, + cycle_mark, + ); + + index += length; + } + Expectation => { + let loc_expr = &declarations.expressions[index]; + + let bool_type = constraints.push_variable(Variable::BOOL); + let expected = constraints.push_expected_type(Expected::ForReason( + Reason::ExpectCondition, + bool_type, + loc_expr.region, + )); + + let expect_constraint = constrain_expr( + types, + constraints, + &mut env, + loc_expr.region, + &loc_expr.value, + expected, + ); + + constraint = constraints.let_constraint( + [], + [], + [], + expect_constraint, + constraint, + Generalizable(false), + ) + } + ExpectationFx => { + let loc_expr = &declarations.expressions[index]; + + let bool_type = constraints.push_variable(Variable::BOOL); + let expected = constraints.push_expected_type(Expected::ForReason( + Reason::ExpectCondition, + bool_type, + loc_expr.region, + )); + + let expect_constraint = constrain_expr( + types, + constraints, + &mut env, + loc_expr.region, + &loc_expr.value, + expected, + ); + + constraint = constraints.let_constraint( + [], + [], + [], + expect_constraint, + constraint, + Generalizable(false), + ) + } + } + + index += 1; + } + + // this assert make the "root" of the constraint wasn't dropped + debug_assert!(constraints.contains_save_the_environment(&constraint)); + + constraint +} + +pub(crate) fn constrain_def_pattern( + types: &mut Types, + constraints: &mut Constraints, + env: &mut Env, + loc_pattern: &Loc, + expr_type: TypeOrVar, +) -> PatternState { + let pattern_expected = constraints.push_pat_expected_type(PExpected::NoExpectation(expr_type)); + + let mut state = PatternState { + headers: VecMap::default(), + vars: Vec::with_capacity(1), + constraints: Vec::with_capacity(1), + delayed_is_open_constraints: vec![], + }; + + constrain_pattern( + types, + constraints, + env, + &loc_pattern.value, + loc_pattern.region, + pattern_expected, + &mut state, + ); + + state +} + +/// Generate constraints for a definition with a type signature +fn constrain_typed_def( + types: &mut Types, + constraints: &mut Constraints, + env: &mut Env, + def: &Def, + body_con: Constraint, + annotation: &roc_can::def::Annotation, +) -> Constraint { + let expr_var = def.expr_var; + let expr_type_index = constraints.push_variable(expr_var); + + let mut def_pattern_state = + constrain_def_pattern(types, constraints, env, &def.loc_pattern, expr_type_index); + + def_pattern_state.vars.push(expr_var); + + let arity = annotation.signature.arity(); + let rigids = &env.rigids; + let mut ftv = rigids.clone(); + + let InstantiateRigids { + signature, + new_rigid_variables, + new_infer_variables, + } = instantiate_rigids( + types, + constraints, + &annotation.signature, + &annotation.introduced_variables, + &def.loc_pattern, + &mut ftv, + &mut def_pattern_state.headers, + IsRecursiveDef::No, + ); + + let env = &mut Env { + home: env.home, + resolutions_to_make: vec![], + rigids: ftv, + }; + + let signature_index = constraints.push_type(types, signature); + + let annotation_expected = constraints.push_expected_type(FromAnnotation( + def.loc_pattern.clone(), + arity, + AnnotationSource::TypedBody { + region: annotation.region, + }, + signature_index, + )); + + def_pattern_state.constraints.push(constraints.equal_types( + expr_type_index, + annotation_expected, + Category::Storage(std::file!(), std::line!()), + Region::span_across(&annotation.region, &def.loc_expr.region), + )); + + // when a def is annotated, and its body is a closure, treat this + // as a named function (in elm terms) for error messages. + // + // This means we get errors like "the first argument of `f` is weird" + // instead of the more generic "something is wrong with the body of `f`" + match (&def.loc_expr.value, types[signature]) { + ( + Closure(ClosureData { + function_type: fn_var, + closure_type: closure_var, + return_type: ret_var, + captured_symbols, + arguments, + loc_body, + name, + .. + }), + TypeTag::Function(_signature_closure_type, ret_type), + ) => { + let arg_types = types.get_type_arguments(signature); + + // NOTE if we ever have problems with the closure, the ignored `_closure_type` + // is probably a good place to start the investigation! + + let region = def.loc_expr.region; + + let loc_body_expr = &**loc_body; + let mut argument_pattern_state = PatternState { + headers: VecMap::default(), + vars: Vec::with_capacity(arguments.len()), + constraints: Vec::with_capacity(1), + delayed_is_open_constraints: vec![], + }; + let mut vars = Vec::with_capacity(argument_pattern_state.vars.capacity() + 1); + let ret_var = *ret_var; + let closure_var = *closure_var; + let ret_type_index = constraints.push_type(types, ret_type); + + vars.push(ret_var); + vars.push(closure_var); + + constrain_typed_function_arguments( + types, + constraints, + env, + def, + &mut def_pattern_state, + &mut argument_pattern_state, + arguments, + arg_types, + ); + + let closure_constraint = constrain_closure_size( + types, + constraints, + *name, + region, + *fn_var, + captured_symbols, + closure_var, + &mut vars, + ); + + let solved_fn_type = { + // TODO(types-soa) optimize for Variable + let arg_types = + types.from_old_type_slice(arguments.iter().map(|a| Type::Variable(a.0))); + let lambda_set = types.from_old_type(&Type::Variable(closure_var)); + let ret_var = types.from_old_type(&Type::Variable(ret_var)); + + let fn_type = types.function(arg_types, lambda_set, ret_var); + constraints.push_type(types, fn_type) + }; + + let body_type = constraints.push_expected_type(FromAnnotation( + def.loc_pattern.clone(), + arguments.len(), + AnnotationSource::TypedBody { + region: annotation.region, + }, + ret_type_index, + )); + + let ret_constraint = constrain_expr( + types, + constraints, + env, + loc_body_expr.region, + &loc_body_expr.value, + body_type, + ); + let ret_constraint = attach_resolution_constraints(constraints, env, ret_constraint); + + vars.push(*fn_var); + let defs_constraint = constraints.and_constraint(argument_pattern_state.constraints); + + let cons = [ + constraints.let_constraint( + [], + argument_pattern_state.vars, + argument_pattern_state.headers, + defs_constraint, + ret_constraint, + // This is a syntactic function, it can be generalized + Generalizable(true), + ), + // Store the inferred ret var into the function type now, so that + // when we check that the solved function type matches the annotation, we can + // display the fully inferred return variable. + constraints.store(ret_type_index, ret_var, std::file!(), std::line!()), + // Now, check the solved function type matches the annotation. + constraints.equal_types( + solved_fn_type, + annotation_expected, + Category::Lambda, + region, + ), + // Finally put the solved closure type into the dedicated def expr variables. + constraints.store(signature_index, *fn_var, std::file!(), std::line!()), + constraints.store(signature_index, expr_var, std::file!(), std::line!()), + closure_constraint, + ]; + + let expr_con = constraints.exists_many(vars, cons); + + constrain_def_make_constraint( + constraints, + new_rigid_variables.into_iter(), + new_infer_variables.into_iter(), + expr_con, + body_con, + def_pattern_state, + Generalizable(true), + ) + } + + _ => { + let annotation_expected = constraints.push_expected_type(FromAnnotation( + def.loc_pattern.clone(), + arity, + AnnotationSource::TypedBody { + region: annotation.region, + }, + expr_type_index, + )); + + let ret_constraint = constrain_expr( + types, + constraints, + env, + def.loc_expr.region, + &def.loc_expr.value, + annotation_expected, + ); + let expr_con = attach_resolution_constraints(constraints, env, ret_constraint); + + let generalizable = Generalizable(is_generalizable_expr(&def.loc_expr.value)); + + constrain_def_make_constraint( + constraints, + new_rigid_variables.into_iter(), + new_infer_variables.into_iter(), + expr_con, + body_con, + def_pattern_state, + generalizable, + ) + } + } +} + +fn constrain_typed_function_arguments( + types: &mut Types, + constraints: &mut Constraints, + env: &mut Env, + def: &Def, + def_pattern_state: &mut PatternState, + argument_pattern_state: &mut PatternState, + arguments: &[(Variable, AnnotatedMark, Loc)], + arg_types: Slice, +) { + // ensure type matches the one in the annotation + let opt_label = if let Pattern::Identifier(label) = def.loc_pattern.value { + Some(label) + } else { + None + }; + + let it = arguments.iter().zip(arg_types.into_iter()).enumerate(); + for (index, ((pattern_var, annotated_mark, loc_pattern), ann)) in it { + let pattern_var_index = constraints.push_variable(*pattern_var); + let ann_index = constraints.push_type(types, ann); + + if loc_pattern.value.surely_exhaustive() { + // OPT: we don't need to perform any type-level exhaustiveness checking. + // Check instead only that the pattern unifies with the annotation type. + let pattern_expected = constraints.push_pat_expected_type(PExpected::ForReason( + PReason::TypedArg { + index: HumanIndex::zero_based(index), + opt_name: opt_label, + }, + ann_index, + loc_pattern.region, + )); + + constrain_pattern( + types, + constraints, + env, + &loc_pattern.value, + loc_pattern.region, + pattern_expected, + argument_pattern_state, + ); + + { + // NOTE: because we perform an equality with part of the signature + // this constraint must be to the def_pattern_state's constraints + def_pattern_state.vars.push(*pattern_var); + + let ann_expected = + constraints.push_expected_type(Expected::NoExpectation(ann_index)); + let pattern_con = constraints.equal_types_var( + *pattern_var, + ann_expected, + Category::Storage(std::file!(), std::line!()), + loc_pattern.region, + ); + + def_pattern_state.constraints.push(pattern_con); + } + } else { + // We need to check the types, and run exhaustiveness checking. + let &AnnotatedMark { + annotation_var, + exhaustive, + } = annotated_mark; + + def_pattern_state.vars.push(*pattern_var); + def_pattern_state.vars.push(annotation_var); + + { + // First, solve the type that the pattern is expecting to match in this + // position. + let pattern_expected = + constraints.push_pat_expected_type(PExpected::NoExpectation(pattern_var_index)); + constrain_pattern( + types, + constraints, + env, + &loc_pattern.value, + loc_pattern.region, + pattern_expected, + argument_pattern_state, + ); + } + + { + // Store the actual type in a variable. + let ann_expected = + constraints.push_expected_type(Expected::NoExpectation(ann_index)); + argument_pattern_state + .constraints + .push(constraints.equal_types_var( + annotation_var, + ann_expected, + Category::Storage(file!(), line!()), + Region::zero(), + )); + } + + { + // let pattern_expected = PExpected::ForReason( + // PReason::TypedArg { + // index: HumanIndex::zero_based(index), + // opt_name: opt_label, + // }, + // ann.clone(), + // loc_pattern.region, + // ); + + // Exhaustiveness-check the type in the pattern against what the + // annotation wants. + let sketched_rows = sketch_pattern_to_rows(loc_pattern.region, &loc_pattern.value); + let category = loc_pattern.value.category(); + let expected = constraints.push_pat_expected_type(PExpected::ForReason( + PReason::TypedArg { + index: HumanIndex::zero_based(index), + opt_name: opt_label, + }, + pattern_var_index, + loc_pattern.region, + )); + let exhaustive_constraint = constraints.exhaustive( + annotation_var, + loc_pattern.region, + Err((category, expected)), + sketched_rows, + ExhaustiveContext::BadArg, + exhaustive, + ); + argument_pattern_state + .constraints + .push(exhaustive_constraint) + } + } + } + + // There may be argument idents left over that don't line up with the function arity. + // Add their patterns' symbols in so that they are present in the env, even though this will + // wind up a type error. + if arguments.len() > arg_types.len() { + for (pattern_var, _annotated_mark, loc_pattern) in &arguments[arg_types.len()..] { + let pattern_var_index = constraints.push_variable(*pattern_var); + + def_pattern_state.vars.push(*pattern_var); + + let pattern_expected = + constraints.push_pat_expected_type(PExpected::NoExpectation(pattern_var_index)); + constrain_pattern( + types, + constraints, + env, + &loc_pattern.value, + loc_pattern.region, + pattern_expected, + argument_pattern_state, + ); + } + } +} + +fn constrain_typed_function_arguments_simple( + types: &mut Types, + constraints: &mut Constraints, + env: &mut Env, + symbol: Symbol, + def_pattern_state: &mut PatternState, + argument_pattern_state: &mut PatternState, + arguments: &[(Variable, AnnotatedMark, Loc)], + arg_types: Slice, +) { + let it = arguments.iter().zip(arg_types.into_iter()).enumerate(); + for (index, ((pattern_var, annotated_mark, loc_pattern), ann)) in it { + let pattern_var_index = constraints.push_variable(*pattern_var); + let ann_index = constraints.push_type(types, ann); + + if loc_pattern.value.surely_exhaustive() { + // OPT: we don't need to perform any type-level exhaustiveness checking. + // Check instead only that the pattern unifies with the annotation type. + let pattern_expected = constraints.push_pat_expected_type(PExpected::ForReason( + PReason::TypedArg { + index: HumanIndex::zero_based(index), + opt_name: Some(symbol), + }, + ann_index, + loc_pattern.region, + )); + + constrain_pattern( + types, + constraints, + env, + &loc_pattern.value, + loc_pattern.region, + pattern_expected, + argument_pattern_state, + ); + + { + // NOTE: because we perform an equality with part of the signature + // this constraint must be to the def_pattern_state's constraints + def_pattern_state.vars.push(*pattern_var); + + let ann_expected = + constraints.push_expected_type(Expected::NoExpectation(ann_index)); + let pattern_con = constraints.equal_types_var( + *pattern_var, + ann_expected, + Category::Storage(std::file!(), std::line!()), + loc_pattern.region, + ); + + def_pattern_state.constraints.push(pattern_con); + } + } else { + // We need to check the types, and run exhaustiveness checking. + let &AnnotatedMark { + annotation_var, + exhaustive, + } = annotated_mark; + + def_pattern_state.vars.push(*pattern_var); + def_pattern_state.vars.push(annotation_var); + + { + // First, solve the type that the pattern is expecting to match in this + // position. + let pattern_expected = + constraints.push_pat_expected_type(PExpected::NoExpectation(pattern_var_index)); + constrain_pattern( + types, + constraints, + env, + &loc_pattern.value, + loc_pattern.region, + pattern_expected, + argument_pattern_state, + ); + } + + { + // Store the actual type in a variable. + let expected_annotation = + constraints.push_expected_type(Expected::NoExpectation(ann_index)); + argument_pattern_state + .constraints + .push(constraints.equal_types_var( + annotation_var, + expected_annotation, + Category::Storage(file!(), line!()), + Region::zero(), + )); + } + + { + // Exhaustiveness-check the type in the pattern against what the + // annotation wants. + let sketched_rows = sketch_pattern_to_rows(loc_pattern.region, &loc_pattern.value); + let category = loc_pattern.value.category(); + let expected = constraints.push_pat_expected_type(PExpected::ForReason( + PReason::TypedArg { + index: HumanIndex::zero_based(index), + opt_name: Some(symbol), + }, + pattern_var_index, + loc_pattern.region, + )); + let exhaustive_constraint = constraints.exhaustive( + annotation_var, + loc_pattern.region, + Err((category, expected)), + sketched_rows, + ExhaustiveContext::BadArg, + exhaustive, + ); + argument_pattern_state + .constraints + .push(exhaustive_constraint) + } + } + } + + // There may be argument idents left over that don't line up with the function arity. + // Add their patterns' symbols in so that they are present in the env, even though this will + // wind up a type error. + if arguments.len() > arg_types.len() { + for (pattern_var, _annotated_mark, loc_pattern) in &arguments[arg_types.len()..] { + let pattern_var_index = constraints.push_variable(*pattern_var); + + def_pattern_state.vars.push(*pattern_var); + + let pattern_expected = + constraints.push_pat_expected_type(PExpected::NoExpectation(pattern_var_index)); + constrain_pattern( + types, + constraints, + env, + &loc_pattern.value, + loc_pattern.region, + pattern_expected, + argument_pattern_state, + ); + } + } +} + +#[inline(always)] +fn attach_resolution_constraints( + constraints: &mut Constraints, + env: &mut Env, + constraint: Constraint, +) -> Constraint { + let resolution_constrs = + constraints.and_constraint(env.resolutions_to_make.drain(..).map(Constraint::Resolve)); + constraints.and_constraint([constraint, resolution_constrs]) +} + +fn constrain_def( + types: &mut Types, + constraints: &mut Constraints, + env: &mut Env, + def: &Def, + body_con: Constraint, +) -> Constraint { + match &def.annotation { + Some(annotation) => constrain_typed_def(types, constraints, env, def, body_con, annotation), + None => { + let expr_var = def.expr_var; + let expr_type_index = constraints.push_variable(expr_var); + + let mut def_pattern_state = + constrain_def_pattern(types, constraints, env, &def.loc_pattern, expr_type_index); + + def_pattern_state.vars.push(expr_var); + // no annotation, so no extra work with rigids + + let expected = constraints.push_expected_type(NoExpectation(expr_type_index)); + let expr_con = constrain_expr( + types, + constraints, + env, + def.loc_expr.region, + &def.loc_expr.value, + expected, + ); + let expr_con = attach_resolution_constraints(constraints, env, expr_con); + + let generalizable = Generalizable(is_generalizable_expr(&def.loc_expr.value)); + + constrain_def_make_constraint( + constraints, + std::iter::empty(), + std::iter::empty(), + expr_con, + body_con, + def_pattern_state, + generalizable, + ) + } + } +} + +/// Create a let-constraint for a non-recursive def. +/// Recursive defs should always use `constrain_recursive_defs`. +pub(crate) fn constrain_def_make_constraint( + constraints: &mut Constraints, + annotation_rigid_variables: impl Iterator, + annotation_infer_variables: impl Iterator, + def_expr_con: Constraint, + after_def_con: Constraint, + def_pattern_state: PatternState, + generalizable: Generalizable, +) -> Constraint { + let all_flex_variables = (def_pattern_state.vars.into_iter()).chain(annotation_infer_variables); + + let pattern_constraints = constraints.and_constraint(def_pattern_state.constraints); + let def_pattern_and_body_con = constraints.and_constraint([pattern_constraints, def_expr_con]); + + constraints.let_constraint( + annotation_rigid_variables, + all_flex_variables, + def_pattern_state.headers, + def_pattern_and_body_con, + after_def_con, + generalizable, + ) +} + +fn constrain_value_def_make_constraint( + constraints: &mut Constraints, + new_rigid_variables: Vec, + new_infer_variables: Vec, + expr_con: Constraint, + body_con: Constraint, + symbol: Loc, + expr_var: Variable, + expr_type: TypeOrVar, + generalizable: Generalizable, +) -> Constraint { + let def_con = constraints.let_constraint( + [], + new_infer_variables, + [], // empty, because our functions have no arguments! + Constraint::True, + expr_con, + generalizable, + ); + + let headers = [(symbol.value, Loc::at(symbol.region, expr_type))]; + + constraints.let_constraint( + new_rigid_variables, + [expr_var], + headers, + def_con, + body_con, + generalizable, + ) +} + +fn constrain_function_def_make_constraint( + constraints: &mut Constraints, + new_rigid_variables: Vec, + new_infer_variables: Vec, + expr_con: Constraint, + body_con: Constraint, + def_pattern_state: PatternState, +) -> Constraint { + let and_constraint = constraints.and_constraint(def_pattern_state.constraints); + + let def_con = constraints.let_constraint( + [], + new_infer_variables, + [], // empty, because our functions have no arguments! + and_constraint, + expr_con, + Generalizable(true), + ); + + constraints.let_constraint( + new_rigid_variables, + def_pattern_state.vars, + def_pattern_state.headers, + def_con, + body_con, + Generalizable(true), + ) +} + +fn constrain_closure_size( + types: &mut Types, + constraints: &mut Constraints, + name: Symbol, + region: Region, + ambient_function: Variable, + captured_symbols: &[(Symbol, Variable)], + closure_var: Variable, + variables: &mut Vec, +) -> Constraint { + debug_assert!(variables.iter().any(|s| *s == closure_var)); + + let mut captured_types = Vec::with_capacity(captured_symbols.len()); + let mut captured_symbols_constraints = Vec::with_capacity(captured_symbols.len()); + + for (symbol, var) in captured_symbols { + // make sure the variable is registered + variables.push(*var); + + // this symbol is captured, so it must be part of the closure type + captured_types.push(Type::Variable(*var)); + + // make the variable equal to the looked-up type of symbol + let store_var_index = constraints.push_variable(*var); + let store_into = constraints.push_expected_type(Expected::NoExpectation(store_var_index)); + captured_symbols_constraints.push(constraints.lookup(*symbol, store_into, Region::zero())); + } + + let finalizer = { + // pick a more efficient representation if we don't actually capture anything + let closure_type = { + let typ = types.from_old_type(&Type::ClosureTag { + name, + captures: captured_types, + ambient_function, + }); + constraints.push_type(types, typ) + }; + let clos_type = constraints.push_expected_type(NoExpectation(closure_type)); + constraints.equal_types_var(closure_var, clos_type, Category::ClosureSize, region) + }; + + captured_symbols_constraints.push(finalizer); + + constraints.and_constraint(captured_symbols_constraints) +} + +pub struct InstantiateRigids { + pub signature: Index, + pub new_rigid_variables: Vec, + pub new_infer_variables: Vec, +} + +#[derive(PartialEq, Eq)] +enum IsRecursiveDef { + Yes, + No, +} + +fn instantiate_rigids( + types: &mut Types, + constraints: &mut Constraints, + annotation: &Type, + introduced_vars: &IntroducedVariables, + loc_pattern: &Loc, + ftv: &mut MutMap, // rigids defined before the current annotation + headers: &mut VecMap>, + is_recursive_def: IsRecursiveDef, +) -> InstantiateRigids { + let mut new_rigid_variables = vec![]; + let mut new_infer_variables = vec![]; + + let mut generate_fresh_ann = |types: &mut Types| { + let mut annotation = annotation.clone(); + + let mut rigid_substitution: MutMap = MutMap::default(); + for named in introduced_vars.iter_named() { + use std::collections::hash_map::Entry::*; + + match ftv.entry(named.name().clone()) { + Occupied(occupied) => { + let existing_rigid = occupied.get(); + rigid_substitution.insert(named.variable(), *existing_rigid); + } + Vacant(vacant) => { + // It's possible to use this rigid in nested defs + vacant.insert(named.variable()); + new_rigid_variables.push(named.variable()); + } + } + } + + // wildcards are always freshly introduced in this annotation + new_rigid_variables.extend(introduced_vars.wildcards.iter().map(|v| v.value)); + + // lambda set vars are always freshly introduced in this annotation + new_rigid_variables.extend(introduced_vars.lambda_sets.iter().copied()); + + // ext-infer vars are always freshly introduced in this annotation + new_rigid_variables.extend(introduced_vars.infer_ext_in_output.iter().copied()); + + new_infer_variables.extend(introduced_vars.inferred.iter().map(|v| v.value)); + + // Instantiate rigid variables + if !rigid_substitution.is_empty() { + annotation.substitute_variables(&rigid_substitution); + } + + types.from_old_type(&annotation) + }; + + let signature = generate_fresh_ann(types); + { + // If this is a recursive def, we must also generate a fresh annotation to be used as the + // type annotation that will be used in the first def headers introduced during the solving + // of the recursive definition. + // + // That is, this annotation serves as step (1) of XREF(rec-def-strategy). We don't want to + // link to the final annotation, since it may be incomplete (or incorrect, see step (1)). + // So, we generate a fresh annotation here, and return a separate fresh annotation below; + // the latter annotation is the one used to construct the finalized type. + let annotation_index = if is_recursive_def == IsRecursiveDef::Yes { + generate_fresh_ann(types) + } else { + signature + }; + + let loc_annotation_ref = Loc::at(loc_pattern.region, annotation_index); + if let Pattern::Identifier(symbol) = loc_pattern.value { + let annotation_index = constraints.push_type(types, annotation_index); + headers.insert(symbol, Loc::at(loc_pattern.region, annotation_index)); + } else if let Some(new_headers) = crate::pattern::headers_from_annotation( + types, + constraints, + &loc_pattern.value, + &loc_annotation_ref, + ) { + headers.extend(new_headers) + } + } + + InstantiateRigids { + signature, + new_rigid_variables, + new_infer_variables, + } +} + +fn instantiate_rigids_simple( + types: &mut Types, + annotation: &Type, + introduced_vars: &IntroducedVariables, + ftv: &mut MutMap, // rigids defined before the current annotation +) -> InstantiateRigids { + let mut annotation = annotation.clone(); + let mut new_rigid_variables: Vec = Vec::new(); + + let mut rigid_substitution: MutMap = MutMap::default(); + for named in introduced_vars.iter_named() { + use std::collections::hash_map::Entry::*; + + match ftv.entry(named.name().clone()) { + Occupied(occupied) => { + let existing_rigid = occupied.get(); + rigid_substitution.insert(named.variable(), *existing_rigid); + } + Vacant(vacant) => { + // It's possible to use this rigid in nested defs + vacant.insert(named.variable()); + new_rigid_variables.push(named.variable()); + } + } + } + + // wildcards are always freshly introduced in this annotation + new_rigid_variables.extend(introduced_vars.wildcards.iter().map(|v| v.value)); + + // lambda set vars are always freshly introduced in this annotation + new_rigid_variables.extend(introduced_vars.lambda_sets.iter().copied()); + + // ext-infer vars are always freshly introduced in this annotation + new_rigid_variables.extend(introduced_vars.infer_ext_in_output.iter().copied()); + + let new_infer_variables: Vec = + introduced_vars.inferred.iter().map(|v| v.value).collect(); + + // Instantiate rigid variables + if !rigid_substitution.is_empty() { + annotation.substitute_variables(&rigid_substitution); + } + + InstantiateRigids { + signature: types.from_old_type(&annotation), + new_rigid_variables, + new_infer_variables, + } +} + +fn constrain_recursive_declarations( + types: &mut Types, + constraints: &mut Constraints, + env: &mut Env, + declarations: &Declarations, + range: Range, + body_con: Constraint, + cycle_mark: IllegalCycleMark, +) -> Constraint { + rec_defs_help_simple( + types, + constraints, + env, + declarations, + range, + body_con, + cycle_mark, + ) +} + +fn constraint_recursive_function( + types: &mut Types, + constraints: &mut Constraints, + env: &mut Env, + declarations: &Declarations, + index: usize, + function_def_index: Index>, + rigid_info: &mut Info, + flex_info: &mut Info, +) { + let loc_expr = &declarations.expressions[index]; + let loc_symbol = declarations.symbols[index]; + let expr_var = declarations.variables[index]; + let opt_annotation = &declarations.annotations[index]; + + let loc_function_def = &declarations.function_bodies[function_def_index.index()]; + let function_def = &loc_function_def.value; + + match opt_annotation { + None => { + let expr_type_index = constraints.push_variable(expr_var); + + let expected_expr = constraints.push_expected_type(NoExpectation(expr_type_index)); + let expr_con = constrain_untyped_closure( + types, + constraints, + env, + loc_function_def.region, + expected_expr, + expr_var, + function_def.closure_type, + function_def.return_type, + &function_def.arguments, + loc_expr, + &function_def.captured_symbols, + loc_symbol.value, + ); + + let expr_con = attach_resolution_constraints(constraints, env, expr_con); + let def_con = expr_con; + + flex_info.vars.push(expr_var); + flex_info.constraints.push(def_con); + flex_info.def_types.insert( + loc_symbol.value, + Loc::at(loc_symbol.region, expr_type_index), + ); + } + + Some(annotation) => { + let arity = annotation.signature.arity(); + let rigids = &env.rigids; + let mut ftv = rigids.clone(); + + let InstantiateRigids { + signature, + new_rigid_variables, + new_infer_variables, + } = instantiate_rigids_simple( + types, + &annotation.signature, + &annotation.introduced_variables, + &mut ftv, + ); + + let loc_pattern = Loc::at(loc_symbol.region, Pattern::Identifier(loc_symbol.value)); + + let signature_index = constraints.push_type(types, signature); + + let annotation_expected = constraints.push_expected_type(FromAnnotation( + loc_pattern, + arity, + AnnotationSource::TypedBody { + region: annotation.region, + }, + signature_index, + )); + + let (arg_types, _signature_closure_type, ret_type) = match types[signature] { + TypeTag::Function(signature_closure_type, ret_type) => ( + types.get_type_arguments(signature), + signature_closure_type, + ret_type, + ), + _ => todo!("TODO {:?}", (loc_symbol, &signature)), + }; + + let region = loc_function_def.region; + + let loc_body_expr = loc_expr; + let mut argument_pattern_state = PatternState { + headers: VecMap::default(), + vars: Vec::with_capacity(function_def.arguments.len()), + constraints: Vec::with_capacity(1), + delayed_is_open_constraints: vec![], + }; + let mut vars = Vec::with_capacity(argument_pattern_state.vars.capacity() + 1); + let ret_var = function_def.return_type; + let closure_var = function_def.closure_type; + let ret_type_index = constraints.push_type(types, ret_type); + + vars.push(ret_var); + vars.push(closure_var); + + let mut def_pattern_state = PatternState::default(); + + def_pattern_state.headers.insert( + loc_symbol.value, + Loc { + region, + // TODO coalesce with other `signature_index`. + // This doesn't yet work; needs investigation as to why. + // My guess is that when types SoA lands, this might just resolve itself, since + // types will be composed from variables to begin with. + value: { + let typ = + types.clone_with_variable_substitutions(signature, &Default::default()); + constraints.push_type(types, typ) + }, + }, + ); + + constrain_typed_function_arguments_simple( + types, + constraints, + env, + loc_symbol.value, + &mut def_pattern_state, + &mut argument_pattern_state, + &function_def.arguments, + arg_types, + ); + + let pattern_types = types + .from_old_type_slice(function_def.arguments.iter().map(|a| Type::Variable(a.0))); + + let closure_constraint = constrain_closure_size( + types, + constraints, + loc_symbol.value, + region, + expr_var, + &function_def.captured_symbols, + closure_var, + &mut vars, + ); + + let fn_type = { + // TODO(types-soa) optimize for Variable + let lambda_set = types.from_old_type(&Type::Variable(closure_var)); + let typ = types.function(pattern_types, lambda_set, ret_type); + constraints.push_type(types, typ) + }; + + let expr_con = { + let expected = constraints.push_expected_type(NoExpectation(ret_type_index)); + constrain_expr( + types, + constraints, + env, + loc_body_expr.region, + &loc_body_expr.value, + expected, + ) + }; + let expr_con = attach_resolution_constraints(constraints, env, expr_con); + + vars.push(expr_var); + + let state_constraints = constraints.and_constraint(argument_pattern_state.constraints); + let cons = [ + constraints.let_constraint( + [], + argument_pattern_state.vars, + argument_pattern_state.headers, + state_constraints, + expr_con, + // Syntactic function can be generalized + Generalizable(true), + ), + constraints.equal_types(fn_type, annotation_expected, Category::Lambda, region), + // "fn_var is equal to the closure's type" - fn_var is used in code gen + // Store type into AST vars. We use Store so errors aren't reported twice + constraints.store(signature_index, expr_var, std::file!(), std::line!()), + constraints.store(ret_type_index, ret_var, std::file!(), std::line!()), + closure_constraint, + ]; + + let and_constraint = constraints.and_constraint(cons); + let def_con = constraints.exists(vars, and_constraint); + + rigid_info.vars.extend(&new_rigid_variables); + flex_info.vars.extend(&new_infer_variables); + + rigid_info.constraints.push({ + // Solve the body of the recursive function, making sure it lines up with the + // signature. + // + // This happens when we're checking that the def of a recursive function actually + // aligns with what the (mutually-)recursive signature says, so finish + // generalization of the function. + let rigids = new_rigid_variables; + let flex = def_pattern_state + .vars + .into_iter() + .chain(new_infer_variables); + + constraints.let_constraint( + rigids, + flex, + // Although we will have already introduced the headers of the def in the + // outermost scope when we introduced the rigid variables, we now re-introduce + // them to force an occurs check and appropriate fixing, since we might end up + // inferring recursive types at inference variable points. E.g. + // + // f : _ -> _ + // f = \F c -> F (List.map f c) + // + // TODO: I (Ayaz) believe we can considerably simplify all this. + def_pattern_state.headers.clone(), + def_con, + Constraint::True, + Generalizable(true), + ) + }); + rigid_info.def_types.extend(def_pattern_state.headers); + } + } +} + +pub fn rec_defs_help_simple( + types: &mut Types, + constraints: &mut Constraints, + env: &mut Env, + declarations: &Declarations, + range: Range, + body_con: Constraint, + cycle_mark: IllegalCycleMark, +) -> Constraint { + let length = range.end - range.start; + + // We partition recursive defs into three buckets: + // rigid: those with fully-elaborated type annotations (no inference vars), e.g. a -> b + // hybrid: those with type annotations containing an inference variable, e.g. _ -> b + // flex: those without a type annotation + let mut rigid_info = Info::with_capacity(length); + let mut hybrid_and_flex_info = Info::with_capacity(length); + + let mut loc_symbols = Vec::with_capacity(length); + let mut expr_regions = Vec::with_capacity(length); + + // Rec defs are generalizable only if everything in the cycle is generalizable. + let generalizable = { + let generalizable = range.clone().all(|i| match declarations.declarations[i] { + DeclarationTag::Value => { + let loc_expr = &declarations.expressions[i]; + is_generalizable_expr(&loc_expr.value) + } + _ => true, // this must be a function + }); + Generalizable(generalizable) + }; + + for index in range { + // Clear the rigids from the previous iteration. + // rigids are not shared between top-level definitions + env.rigids.clear(); + + let loc_symbol = declarations.symbols[index]; + loc_symbols.push((loc_symbol.value, loc_symbol.region)); + + match declarations.declarations[index] { + DeclarationTag::Value => { + let expr_var = declarations.variables[index]; + let expr_var_index = constraints.push_variable(expr_var); + let opt_annotation = &declarations.annotations[index]; + + let loc_expr = &declarations.expressions[index]; + expr_regions.push(loc_expr.region); + + match opt_annotation { + None => { + let expected = + constraints.push_expected_type(NoExpectation(expr_var_index)); + let expr_con = constrain_expr( + types, + constraints, + env, + loc_expr.region, + &loc_expr.value, + expected, + ); + let expr_con = attach_resolution_constraints(constraints, env, expr_con); + + let def_con = expr_con; + + hybrid_and_flex_info.vars.push(expr_var); + hybrid_and_flex_info.constraints.push(def_con); + hybrid_and_flex_info + .def_types + .insert(loc_symbol.value, Loc::at(loc_symbol.region, expr_var_index)); + } + Some(annotation) => { + let arity = annotation.signature.arity(); + let rigids = &env.rigids; + let mut ftv = rigids.clone(); + + let InstantiateRigids { + signature, + new_rigid_variables, + new_infer_variables, + } = instantiate_rigids_simple( + types, + &annotation.signature, + &annotation.introduced_variables, + &mut ftv, + ); + + let loc_pattern = + Loc::at(loc_symbol.region, Pattern::Identifier(loc_symbol.value)); + + let is_hybrid = !new_infer_variables.is_empty(); + + hybrid_and_flex_info.vars.extend(new_infer_variables); + + let signature_index = constraints.push_type(types, signature); + + let annotation_expected = FromAnnotation( + loc_pattern.clone(), + arity, + AnnotationSource::TypedBody { + region: annotation.region, + }, + signature_index, + ); + + let expected = constraints.push_expected_type(annotation_expected); + + let ret_constraint = constrain_expr( + types, + constraints, + env, + loc_expr.region, + &loc_expr.value, + expected, + ); + let ret_constraint = + attach_resolution_constraints(constraints, env, ret_constraint); + + let cons = [ + ret_constraint, + // Store type into AST vars. We use Store so errors aren't reported twice + constraints.store( + signature_index, + expr_var, + std::file!(), + std::line!(), + ), + ]; + let def_con = constraints.and_constraint(cons); + + let loc_type = Loc::at(loc_symbol.region, signature_index); + if is_hybrid { + hybrid_and_flex_info.vars.extend(&new_rigid_variables); + hybrid_and_flex_info.constraints.push(def_con); + hybrid_and_flex_info + .def_types + .insert(loc_symbol.value, loc_type); + } else { + rigid_info.vars.extend(&new_rigid_variables); + + rigid_info.constraints.push(constraints.let_constraint( + new_rigid_variables, + [expr_var], + [], // no headers introduced (at this level) + def_con, + Constraint::True, + generalizable, + )); + rigid_info.def_types.insert(loc_symbol.value, loc_type); + } + } + } + } + DeclarationTag::Recursive(f_index) | DeclarationTag::TailRecursive(f_index) => { + expr_regions.push(declarations.function_bodies[f_index.index()].region); + + constraint_recursive_function( + types, + constraints, + env, + declarations, + index, + f_index, + &mut rigid_info, + &mut hybrid_and_flex_info, + ); + } + _ => unreachable!(), + } + } + + // NB(rec-def-strategy) Strategy for recursive defs: + // + // 1. Let-generalize all rigid annotations. These are the source of truth we'll solve + // everything else with. If there are circular type errors here, they will be caught + // during the let-generalization. + // + // 2. Introduce all symbols of the flex + hybrid defs, but don't generalize them yet. + // Now, solve those defs' bodies. This way, when checking something like + // f = \x -> f [x] + // we introduce `f: b -> c`, then constrain the call `f [x]`, + // forcing `b -> c ~ List b -> c` and correctly picking up a recursion error. + // Had we generalized `b -> c`, the call `f [x]` would have been generalized, and this + // error would not be found. + // + // - This works just as well for mutually recursive defs. + // - For hybrid defs, we also ensure solved types agree with what the + // elaborated parts of their type annotations demand. + // + // 3. Now properly let-generalize the flex + hybrid defs, since we now know their types and + // that they don't have circular type errors. + // + // 4. Solve the bodies of the typed body defs, and check that they agree the types of the type + // annotation. + // + // 5. Solve the rest of the program that happens after this recursive def block. + + // 2. Solve untyped defs without generalization of their symbols. + let untyped_body_constraints = constraints.and_constraint(hybrid_and_flex_info.constraints); + let untyped_def_symbols_constr = constraints.let_constraint( + [], + [], + hybrid_and_flex_info.def_types.clone(), + Constraint::True, + untyped_body_constraints, + generalizable, + ); + + // an extra constraint that propagates information to the solver to check for invalid recursion + // and generate a good error message there. + let cycle_constraint = constraints.check_cycle(loc_symbols, expr_regions, cycle_mark); + + // 4 + 5. Solve the typed body defs, and the rest of the program. + let typed_body_constraints = constraints.and_constraint(rigid_info.constraints); + let typed_body_and_final_constr = + constraints.and_constraint([typed_body_constraints, cycle_constraint, body_con]); + + // 3. Properly generalize untyped defs after solving them. + let inner = constraints.let_constraint( + [], + hybrid_and_flex_info.vars, + hybrid_and_flex_info.def_types, + untyped_def_symbols_constr, + typed_body_and_final_constr, + generalizable, + ); + + // 1. Let-generalize annotations we know. + constraints.let_constraint( + rigid_info.vars, + [], + rigid_info.def_types, + Constraint::True, + inner, + generalizable, + ) +} + +/// A let-bound expression is generalizable if it is +/// - a syntactic function under an opaque wrapper +/// - a number literal under an opaque wrapper +fn is_generalizable_expr(mut expr: &Expr) -> bool { + loop { + match expr { + Num(..) | Int(..) | Float(..) => return true, + Closure(_) => return true, + RecordAccessor(_) => { + // RecordAccessor functions `.field` are equivalent to closures `\r -> r.field`, no need to weaken them. + return true; + } + OpaqueWrapFunction(_) => { + // Opaque wrapper functions `@Q` are equivalent to closures `\x -> @Q x`, no need to weaken them. + return true; + } + RuntimeError(roc_problem::can::RuntimeError::NoImplementation) + | RuntimeError(roc_problem::can::RuntimeError::NoImplementationNamed { .. }) => { + // Allow generalization of signatures with no implementation + return true; + } + OpaqueRef { argument, .. } => expr = &argument.1.value, + Str(_) + | IngestedFile(..) + | List { .. } + | SingleQuote(_, _, _, _) + | When { .. } + | If { .. } + | LetRec(_, _, _) + | LetNonRec(_, _) + | Call(_, _, _) + | RunLowLevel { .. } + | ForeignCall { .. } + | EmptyRecord + | Expr::Record { .. } + | Expr::Tuple { .. } + | Crash { .. } + | RecordAccess { .. } + | TupleAccess { .. } + | RecordUpdate { .. } + | Expect { .. } + | ExpectFx { .. } + | Dbg { .. } + | TypedHole(_) + | RuntimeError(..) + | ZeroArgumentTag { .. } + | Tag { .. } + | AbilityMember(..) + | Var(..) => return false, + } + } +} + +fn constrain_recursive_defs( + types: &mut Types, + constraints: &mut Constraints, + env: &mut Env, + defs: &[Def], + body_con: Constraint, + cycle_mark: IllegalCycleMark, +) -> Constraint { + rec_defs_help(types, constraints, env, defs, body_con, cycle_mark) +} + +fn rec_defs_help( + types: &mut Types, + constraints: &mut Constraints, + env: &mut Env, + defs: &[Def], + body_con: Constraint, + cycle_mark: IllegalCycleMark, +) -> Constraint { + // We partition recursive defs into three buckets: + // rigid: those with fully-elaborated type annotations (no inference vars), e.g. a -> b + // hybrid: those with type annotations containing an inference variable, e.g. _ -> b + // flex: those without a type annotation + let mut rigid_info = Info::with_capacity(defs.len()); + let mut hybrid_and_flex_info = Info::with_capacity(defs.len()); + + // Rec defs are generalizable only if everything in the cycle is generalizable. + let generalizable = { + let generalizable = defs + .iter() + .all(|d| is_generalizable_expr(&d.loc_expr.value)); + Generalizable(generalizable) + }; + + for def in defs { + let expr_var = def.expr_var; + let expr_type_index = constraints.push_variable(expr_var); + + let mut def_pattern_state = + constrain_def_pattern(types, constraints, env, &def.loc_pattern, expr_type_index); + + def_pattern_state.vars.push(expr_var); + + match &def.annotation { + None => { + let expected = constraints.push_expected_type(NoExpectation(expr_type_index)); + let expr_con = constrain_expr( + types, + constraints, + env, + def.loc_expr.region, + &def.loc_expr.value, + expected, + ); + let expr_con = attach_resolution_constraints(constraints, env, expr_con); + + let def_con = expr_con; + + hybrid_and_flex_info.vars.extend(def_pattern_state.vars); + hybrid_and_flex_info.constraints.push(def_con); + hybrid_and_flex_info + .def_types + .extend(def_pattern_state.headers); + } + + Some(annotation) => { + let arity = annotation.signature.arity(); + let mut ftv = env.rigids.clone(); + + let InstantiateRigids { + signature, + new_rigid_variables, + new_infer_variables, + } = instantiate_rigids( + types, + constraints, + &annotation.signature, + &annotation.introduced_variables, + &def.loc_pattern, + &mut ftv, + &mut def_pattern_state.headers, + IsRecursiveDef::Yes, + ); + + let is_hybrid = !new_infer_variables.is_empty(); + + hybrid_and_flex_info.vars.extend(&new_infer_variables); + + let signature_index = constraints.push_type(types, signature); + + let annotation_expected = FromAnnotation( + def.loc_pattern.clone(), + arity, + AnnotationSource::TypedBody { + region: annotation.region, + }, + signature_index, + ); + + // when a def is annotated, and it's body is a closure, treat this + // as a named function (in elm terms) for error messages. + // + // This means we get errors like "the first argument of `f` is weird" + // instead of the more generic "something is wrong with the body of `f`" + match (&def.loc_expr.value, types[signature]) { + ( + Closure(ClosureData { + function_type: fn_var, + closure_type: closure_var, + return_type: ret_var, + captured_symbols, + arguments, + loc_body, + name, + .. + }), + TypeTag::Function(_closure_type, ret_type), + ) => { + // NOTE if we ever have trouble with closure type unification, the ignored + // `_closure_type` here is a good place to start investigating + + let arg_types = types.get_type_arguments(signature); + + let expected = annotation_expected; + let region = def.loc_expr.region; + + let loc_body_expr = &**loc_body; + let mut argument_pattern_state = PatternState { + headers: VecMap::default(), + vars: Vec::with_capacity(arguments.len()), + constraints: Vec::with_capacity(1), + delayed_is_open_constraints: vec![], + }; + let mut vars = + Vec::with_capacity(argument_pattern_state.vars.capacity() + 1); + let ret_var = *ret_var; + let closure_var = *closure_var; + let ret_type_index = constraints.push_type(types, ret_type); + + vars.push(ret_var); + vars.push(closure_var); + + constrain_typed_function_arguments( + types, + constraints, + env, + def, + &mut def_pattern_state, + &mut argument_pattern_state, + arguments, + arg_types, + ); + let pattern_types = types + .from_old_type_slice(arguments.iter().map(|a| Type::Variable(a.0))); + + let closure_constraint = constrain_closure_size( + types, + constraints, + *name, + region, + *fn_var, + captured_symbols, + closure_var, + &mut vars, + ); + + let fn_type_index = { + // TODO(types-soa) optimize for variable + let lambda_set = types.from_old_type(&Type::Variable(closure_var)); + let typ = types.function(pattern_types, lambda_set, ret_type); + constraints.push_type(types, typ) + }; + let expr_con = { + let body_type = + constraints.push_expected_type(NoExpectation(ret_type_index)); + + constrain_expr( + types, + constraints, + env, + loc_body_expr.region, + &loc_body_expr.value, + body_type, + ) + }; + let expr_con = attach_resolution_constraints(constraints, env, expr_con); + + vars.push(*fn_var); + + let state_constraints = + constraints.and_constraint(argument_pattern_state.constraints); + let expected_index = constraints.push_expected_type(expected); + let cons = [ + constraints.let_constraint( + [], + argument_pattern_state.vars, + argument_pattern_state.headers, + state_constraints, + expr_con, + generalizable, + ), + constraints.equal_types( + fn_type_index, + expected_index, + Category::Lambda, + region, + ), + // "fn_var is equal to the closure's type" - fn_var is used in code gen + // Store type into AST vars. We use Store so errors aren't reported twice + constraints.store(signature_index, *fn_var, std::file!(), std::line!()), + constraints.store( + signature_index, + expr_var, + std::file!(), + std::line!(), + ), + constraints.store(ret_type_index, ret_var, std::file!(), std::line!()), + closure_constraint, + ]; + + let and_constraint = constraints.and_constraint(cons); + let def_con = constraints.exists(vars, and_constraint); + + if is_hybrid { + // TODO this is not quite right, types that are purely rigid should not + // be stored as hybrid! + // However it might not be possible to fix this before types SoA lands. + hybrid_and_flex_info.vars.extend(&new_rigid_variables); + hybrid_and_flex_info.constraints.push(def_con); + hybrid_and_flex_info + .def_types + .extend(def_pattern_state.headers); + } else { + rigid_info.vars.extend(&new_rigid_variables); + + let rigids = new_rigid_variables; + let flex = def_pattern_state + .vars + .into_iter() + .chain(new_infer_variables); + + rigid_info.constraints.push(constraints.let_constraint( + rigids, + flex, + [], // no headers introduced (at this level) + def_con, + Constraint::True, + generalizable, + )); + rigid_info.def_types.extend(def_pattern_state.headers); + } + } + _ => { + let expected = constraints.push_expected_type(annotation_expected); + + let ret_constraint = constrain_expr( + types, + constraints, + env, + def.loc_expr.region, + &def.loc_expr.value, + expected, + ); + let ret_constraint = + attach_resolution_constraints(constraints, env, ret_constraint); + + let cons = [ + ret_constraint, + // Store type into AST vars. We use Store so errors aren't reported twice + constraints.store( + signature_index, + expr_var, + std::file!(), + std::line!(), + ), + ]; + let def_con = constraints.and_constraint(cons); + + if is_hybrid { + hybrid_and_flex_info.vars.extend(&new_rigid_variables); + hybrid_and_flex_info.constraints.push(def_con); + hybrid_and_flex_info + .def_types + .extend(def_pattern_state.headers); + } else { + rigid_info.vars.extend(&new_rigid_variables); + + rigid_info.constraints.push(constraints.let_constraint( + new_rigid_variables, + def_pattern_state.vars, + [], // no headers introduced (at this level) + def_con, + Constraint::True, + generalizable, + )); + rigid_info.def_types.extend(def_pattern_state.headers); + } + } + } + } + } + } + + // NB(rec-def-strategy) Strategy for recursive defs: + // + // 1. Let-generalize all rigid annotations. These are the source of truth we'll solve + // everything else with. If there are circular type errors here, they will be caught + // during the let-generalization. + // + // 2. Introduce all symbols of the flex + hybrid defs, but don't generalize them yet. + // Now, solve those defs' bodies. This way, when checking something like + // f = \x -> f [x] + // we introduce `f: b -> c`, then constrain the call `f [x]`, + // forcing `b -> c ~ List b -> c` and correctly picking up a recursion error. + // Had we generalized `b -> c`, the call `f [x]` would have been generalized, and this + // error would not be found. + // + // - This works just as well for mutually recursive defs. + // - For hybrid defs, we also ensure solved types agree with what the + // elaborated parts of their type annotations demand. + // + // 3. Now properly let-generalize the flex + hybrid defs, since we now know their types and + // that they don't have circular type errors. + // + // 4. Solve the bodies of the typed body defs, and check that they agree the types of the type + // annotation. + // + // 5. Solve the rest of the program that happens after this recursive def block. + + // 2. Solve untyped defs without generalization of their symbols. + let untyped_body_constraints = constraints.and_constraint(hybrid_and_flex_info.constraints); + let untyped_def_symbols_constr = constraints.let_constraint( + [], + [], + hybrid_and_flex_info.def_types.clone(), + Constraint::True, + untyped_body_constraints, + generalizable, + ); + + // an extra constraint that propagates information to the solver to check for invalid recursion + // and generate a good error message there. + let (loc_symbols, expr_regions): (Vec<_>, Vec<_>) = defs + .iter() + .flat_map(|def| { + symbols_introduced_from_pattern(&def.loc_pattern) + .map(move |loc_symbol| ((loc_symbol.value, loc_symbol.region), def.loc_expr.region)) + }) + .unzip(); + + let cycle_constraint = constraints.check_cycle(loc_symbols, expr_regions, cycle_mark); + + let typed_body_constraints = constraints.and_constraint(rigid_info.constraints); + let typed_body_and_final_constr = + constraints.and_constraint([typed_body_constraints, cycle_constraint, body_con]); + + // 3. Properly generalize untyped defs after solving them. + let inner = constraints.let_constraint( + [], + hybrid_and_flex_info.vars, + hybrid_and_flex_info.def_types, + untyped_def_symbols_constr, + // 4 + 5. Solve the typed body defs, and the rest of the program. + typed_body_and_final_constr, + generalizable, + ); + + // 1. Let-generalize annotations we know. + constraints.let_constraint( + rigid_info.vars, + [], + rigid_info.def_types, + Constraint::True, + inner, + generalizable, + ) +} + +#[inline(always)] +fn constrain_field_update( + types: &mut Types, + constraints: &mut Constraints, + env: &mut Env, + var: Variable, + region: Region, + field: Lowercase, + loc_expr: &Loc, +) -> (Variable, Type, Constraint) { + let field_type = constraints.push_variable(var); + let reason = Reason::RecordUpdateValue(field); + let expected = constraints.push_expected_type(ForReason(reason, field_type, region)); + let con = constrain_expr( + types, + constraints, + env, + loc_expr.region, + &loc_expr.value, + expected, + ); + + (var, Variable(var), con) +} diff --git a/crates/compiler/constrain/src/lib.rs b/crates/compiler/constrain/src/lib.rs new file mode 100644 index 0000000000..09d8cfc319 --- /dev/null +++ b/crates/compiler/constrain/src/lib.rs @@ -0,0 +1,11 @@ +//! Responsible for building the set of constraints that are used during +//! [type inference](https://en.wikipedia.org/wiki/Type_inference) of a program, +//! and for gathering context needed for pleasant error messages when a type +//! error occurs. +#![warn(clippy::dbg_macro)] +// See github.com/roc-lang/roc/issues/800 for discussion of the large_enum_variant check. +#![allow(clippy::large_enum_variant)] +pub mod builtins; +pub mod expr; +pub mod module; +pub mod pattern; diff --git a/crates/compiler/constrain/src/module.rs b/crates/compiler/constrain/src/module.rs new file mode 100644 index 0000000000..545aa9d5a9 --- /dev/null +++ b/crates/compiler/constrain/src/module.rs @@ -0,0 +1,168 @@ +use crate::expr::{constrain_def_make_constraint, constrain_def_pattern, Env}; +use roc_can::abilities::{PendingAbilitiesStore, PendingMemberType}; +use roc_can::constraint::{Constraint, Constraints, Generalizable}; +use roc_can::expected::Expected; +use roc_can::expr::Declarations; +use roc_can::pattern::Pattern; +use roc_module::symbol::{ModuleId, Symbol}; +use roc_region::all::{Loc, Region}; +use roc_types::types::{AnnotationSource, Category, Type, Types}; + +pub fn constrain_module( + types: &mut Types, + constraints: &mut Constraints, + symbols_from_requires: Vec<(Loc, Loc)>, + abilities_store: &PendingAbilitiesStore, + declarations: &Declarations, + home: ModuleId, +) -> Constraint { + let constraint = crate::expr::constrain_decls(types, constraints, home, declarations); + let constraint = constrain_symbols_from_requires( + types, + constraints, + symbols_from_requires, + home, + constraint, + ); + let constraint = + frontload_ability_constraints(types, constraints, abilities_store, home, constraint); + + // The module constraint should always save the environment at the end. + debug_assert!(constraints.contains_save_the_environment(&constraint)); + + constraint +} + +fn constrain_symbols_from_requires( + types: &mut Types, + constraints: &mut Constraints, + symbols_from_requires: Vec<(Loc, Loc)>, + home: ModuleId, + constraint: Constraint, +) -> Constraint { + symbols_from_requires + .into_iter() + .fold(constraint, |constraint, (loc_symbol, loc_type)| { + if loc_symbol.value.module_id() == home { + // 1. Required symbols can only be specified in package modules + // 2. Required symbols come from app modules + // But, if we are running e.g. `roc check` on a package module, there is no app + // module, and we will have instead put the required symbols in the package module + // namespace. If this is the case, we want to introduce the symbols as if they had + // the types they are annotated with. + let rigids = Default::default(); + let mut env = Env { + home, + rigids, + resolutions_to_make: vec![], + }; + let pattern = Loc::at_zero(roc_can::pattern::Pattern::Identifier(loc_symbol.value)); + + let type_index = { + let typ = types.from_old_type(&loc_type.value); + constraints.push_type(types, typ) + }; + let def_pattern_state = + constrain_def_pattern(types, constraints, &mut env, &pattern, type_index); + + debug_assert!(env.resolutions_to_make.is_empty()); + + constrain_def_make_constraint( + constraints, + // No new rigids or flex vars because they are represented in the type + // annotation. + std::iter::empty(), + std::iter::empty(), + Constraint::True, + constraint, + def_pattern_state, + Generalizable(true), + ) + } else { + // Otherwise, this symbol comes from an app module - we want to check that the type + // provided by the app is in fact what the package module requires. + let arity = loc_type.value.arity(); + let typ = loc_type.value; + let type_index = { + let typ = types.from_old_type(&typ); + constraints.push_type(types, typ) + }; + let expected = constraints.push_expected_type(Expected::FromAnnotation( + loc_symbol.map(|&s| Pattern::Identifier(s)), + arity, + AnnotationSource::RequiredSymbol { + region: loc_type.region, + }, + type_index, + )); + let provided_eq_requires_constr = + constraints.lookup(loc_symbol.value, expected, loc_type.region); + constraints.and_constraint([provided_eq_requires_constr, constraint]) + } + }) +} + +pub fn frontload_ability_constraints( + types: &mut Types, + constraints: &mut Constraints, + abilities_store: &PendingAbilitiesStore, + home: ModuleId, + mut constraint: Constraint, +) -> Constraint { + for (member_name, member_data) in abilities_store.root_ability_members().iter() { + if let PendingMemberType::Local { + signature_var, + variables: vars, + signature, + } = &member_data.typ + { + let signature_var = *signature_var; + let rigids = Default::default(); + let mut env = Env { + home, + rigids, + resolutions_to_make: vec![], + }; + let pattern = Loc::at_zero(roc_can::pattern::Pattern::Identifier(*member_name)); + + let signature_index = { + let typ = types.from_old_type(&signature.clone()); + constraints.push_type(types, typ) + }; + + let mut def_pattern_state = + constrain_def_pattern(types, constraints, &mut env, &pattern, signature_index); + + debug_assert!(env.resolutions_to_make.is_empty()); + + def_pattern_state.vars.push(signature_var); + + let rigid_variables = vars.rigid_vars.iter().chain(vars.able_vars.iter()).copied(); + let infer_variables = vars.flex_vars.iter().copied(); + + let signature_expectation = + constraints.push_expected_type(Expected::NoExpectation(signature_index)); + + def_pattern_state + .constraints + .push(constraints.equal_types_var( + signature_var, + signature_expectation, + Category::Storage(file!(), line!()), + Region::zero(), + )); + + constraint = constrain_def_make_constraint( + constraints, + rigid_variables, + infer_variables, + Constraint::True, + constraint, + def_pattern_state, + // Ability signatures can always be generalized since they're prototypical + Generalizable(true), + ); + } + } + constraint +} diff --git a/crates/compiler/constrain/src/pattern.rs b/crates/compiler/constrain/src/pattern.rs new file mode 100644 index 0000000000..3b3ce10a3d --- /dev/null +++ b/crates/compiler/constrain/src/pattern.rs @@ -0,0 +1,961 @@ +use crate::builtins; +use crate::expr::{constrain_expr, Env}; +use roc_can::constraint::{Constraint, Constraints, PExpectedTypeIndex, TypeOrVar}; +use roc_can::expected::{Expected, PExpected}; +use roc_can::pattern::Pattern::{self, *}; +use roc_can::pattern::{DestructType, ListPatterns, RecordDestruct, TupleDestruct}; +use roc_collections::all::{HumanIndex, SendMap}; +use roc_collections::soa::Index; +use roc_collections::VecMap; +use roc_module::ident::Lowercase; +use roc_module::symbol::Symbol; +use roc_region::all::{Loc, Region}; +use roc_types::subs::Variable; +use roc_types::types::{ + AliasKind, AliasShared, Category, OptAbleType, PReason, PatternCategory, Reason, RecordField, + Type, TypeExtension, TypeTag, Types, +}; + +#[derive(Default, Debug)] +pub struct PatternState { + pub headers: VecMap>, + pub vars: Vec, + pub constraints: Vec, + pub delayed_is_open_constraints: Vec, +} + +/// If there is a type annotation, the pattern state headers can be optimized by putting the +/// annotation in the headers. Normally +/// +/// x = 4 +/// +/// Would add `x => <42>` to the headers (i.e., symbol points to a type variable). If the +/// definition has an annotation, we instead now add `x => Int`. +pub fn headers_from_annotation( + types: &Types, + constraints: &mut Constraints, + pattern: &Pattern, + annotation: &Loc>, +) -> Option>> { + let mut headers = VecMap::default(); + // Check that the annotation structurally agrees with the pattern, preventing e.g. `{ x, y } : Int` + // in such incorrect cases we don't put the full annotation in headers, just a variable, and let + // inference generate a proper error. + let is_structurally_valid = + headers_from_annotation_help(types, constraints, pattern, annotation, &mut headers); + + if is_structurally_valid { + Some(headers) + } else { + None + } +} + +fn headers_from_annotation_help( + types: &Types, + constraints: &mut Constraints, + pattern: &Pattern, + annotation: &Loc>, + headers: &mut VecMap>, +) -> bool { + let typ = annotation.value; + match pattern { + Identifier(symbol) + | Shadowed(_, _, symbol) + | AbilityMemberSpecialization { + ident: symbol, + specializes: _, + } => { + let annotation_index = constraints.push_type(types, typ); + let typ = Loc::at(annotation.region, annotation_index); + headers.insert(*symbol, typ); + true + } + + As(subpattern, symbol) => { + let annotation_index = constraints.push_type(types, typ); + let typ = Loc::at(annotation.region, annotation_index); + headers.insert(*symbol, typ); + + headers_from_annotation_help(types, constraints, &subpattern.value, annotation, headers) + } + + Underscore + | MalformedPattern(_, _) + | UnsupportedPattern(_) + | OpaqueNotInScope(..) + | NumLiteral(..) + | IntLiteral(..) + | FloatLiteral(..) + | SingleQuote(..) + | StrLiteral(_) => true, + + RecordDestructure { destructs, .. } => { + let dealiased = types.shallow_dealias(annotation.value); + match types[dealiased] { + TypeTag::Record(fields) => { + let (field_names, _, field_types) = types.record_fields_slices(fields); + let field_names = &types[field_names]; + + for loc_destruct in destructs { + let destruct = &loc_destruct.value; + + // NOTE: We ignore both Guard and optionality when + // determining the type of the assigned def (which is what + // gets added to the header here). + // + // For example, no matter whether it's `{ x } = rec` or + // `{ x ? 0 } = rec` or `{ x: 5 } -> ...` in all cases + // the type of `x` within the binding itself is the same. + if let Some(i) = field_names + .iter() + .position(|field| field == &destruct.label) + { + let field_type_index = { + let typ = field_types.at(i); + constraints.push_type(types, typ) + }; + headers.insert( + destruct.symbol, + Loc::at(annotation.region, field_type_index), + ); + } else { + return false; + } + } + true + } + TypeTag::EmptyRecord => destructs.is_empty(), + _ => false, + } + } + + TupleDestructure { destructs: _, .. } => { + todo!(); + } + + List { patterns, .. } => { + if let Some((_, Some(rest))) = patterns.opt_rest { + let annotation_index = { + let typ = annotation.value; + constraints.push_type(types, typ) + }; + let typ = Loc::at(annotation.region, annotation_index); + headers.insert(rest, typ); + + false + } else { + // There are no interesting headers to introduce for list patterns, since the only + // exhaustive list pattern is + // \[..] -> + // which does not introduce any symbols. + false + } + } + + AppliedTag { + tag_name, + arguments, + .. + } => { + let dealiased = types.shallow_dealias(annotation.value); + match types[dealiased] { + TypeTag::TagUnion(tags, _) => { + let (tags, payloads) = types.union_tag_slices(tags); + let tags = &types[tags]; + + if let Some(i) = tags.iter().position(|name| name == tag_name) { + let arg_types_slice = types[payloads.at(i)]; + + if !arguments.len() == arg_types_slice.len() { + return false; + } + + arguments.iter().zip(arg_types_slice.into_iter()).all( + |(arg_pattern, arg_type)| { + headers_from_annotation_help( + types, + constraints, + &arg_pattern.1.value, + &Loc::at(annotation.region, arg_type), + headers, + ) + }, + ) + } else { + false + } + } + _ => false, + } + } + + UnwrappedOpaque { + whole_var: _, + opaque, + argument, + specialized_def_type: _, + type_arguments: pat_type_arguments, + lambda_set_variables: pat_lambda_set_variables, + } => { + let typ = annotation.value; + + match types[typ] { + TypeTag::OpaqueAlias { shared, actual } => { + let AliasShared { + symbol, + lambda_set_variables, + .. + } = types[shared]; + let type_arguments = types.get_type_arguments(typ); + + if symbol == *opaque + && type_arguments.len() == pat_type_arguments.len() + && lambda_set_variables.len() == pat_lambda_set_variables.len() + { + let annotation_index = constraints.push_type(types, typ); + let typ = Loc::at(annotation.region, annotation_index); + headers.insert(*opaque, typ); + + let (_, argument_pat) = &**argument; + headers_from_annotation_help( + types, + constraints, + &argument_pat.value, + &Loc::at(annotation.region, actual), + headers, + ) + } else { + false + } + } + _ => false, + } + } + } +} + +/// This accepts PatternState (rather than returning it) so that the caller can +/// initialize the Vecs in PatternState using with_capacity +/// based on its knowledge of their lengths. +pub fn constrain_pattern( + types: &mut Types, + constraints: &mut Constraints, + env: &mut Env, + pattern: &Pattern, + region: Region, + expected: PExpectedTypeIndex, + state: &mut PatternState, +) { + match pattern { + Underscore => { + // This is an underscore in a position where we destruct a variable, + // like a when expression: + // when x is + // A -> "" + // _ -> "" + // so, we know that "x" (in this case, a tag union) must be open. + let expected_type = *constraints[expected].get_type_ref(); + if could_be_a_tag_union(types, expected_type) { + state + .delayed_is_open_constraints + .push(constraints.is_open_type(expected_type)); + } + } + UnsupportedPattern(_) | MalformedPattern(_, _) | OpaqueNotInScope(..) => { + // Erroneous patterns don't add any constraints. + } + + Identifier(symbol) | Shadowed(_, _, symbol) => { + let type_index = *constraints[expected].get_type_ref(); + + if could_be_a_tag_union(types, type_index) { + state + .delayed_is_open_constraints + .push(constraints.is_open_type(type_index)); + } + + state.headers.insert( + *symbol, + Loc { + region, + value: type_index, + }, + ); + } + + As(subpattern, symbol) => { + // NOTE: we don't use `could_be_a_tag_union` here. The `PATTERN as name` should + // just use the type of the PATTERN, and not influence what type is inferred for PATTERN + + state.headers.insert( + *symbol, + Loc { + region, + value: *constraints[expected].get_type_ref(), + }, + ); + + constrain_pattern( + types, + constraints, + env, + &subpattern.value, + subpattern.region, + expected, + state, + ) + } + + AbilityMemberSpecialization { + ident: symbol, + specializes: _, + } => { + let expected = &constraints[expected]; + let type_index = *expected.get_type_ref(); + + if could_be_a_tag_union(types, type_index) { + state.constraints.push(constraints.is_open_type(type_index)); + } + + state.headers.insert( + *symbol, + Loc { + region, + value: type_index, + }, + ); + } + + &NumLiteral(precision_var, _, _, bound) => { + state.vars.push(precision_var); + + let num_type = builtins::add_numeric_bound_constr( + types, + constraints, + &mut state.constraints, + precision_var, + precision_var, + bound, + region, + Category::Num, + ); + let num_type = { + let typ = types.from_old_type(&num_type); + constraints.push_type(types, typ) + }; + + state.constraints.push(constraints.equal_pattern_types( + num_type, + expected, + PatternCategory::Num, + region, + )); + } + + &IntLiteral(num_precision_var, precision_var, _, _, bound) => { + // First constraint on the free num var; this improves the resolved type quality in + // case the bound is an alias. + let num_type = builtins::add_numeric_bound_constr( + types, + constraints, + &mut state.constraints, + num_precision_var, + num_precision_var, + bound, + region, + Category::Int, + ); + let num_type = { + let typ = types.from_old_type(&num_type); + constraints.push_type(types, typ) + }; + + // Link the free num var with the int var and our expectation. + let int_type = { + let typ = types.from_old_type(&builtins::num_int(Type::Variable(precision_var))); + constraints.push_type(types, typ) + }; + + state.constraints.push({ + let expected_index = + constraints.push_expected_type(Expected::NoExpectation(int_type)); + constraints.equal_types(num_type, expected_index, Category::Int, region) + }); + + // Also constrain the pattern against the num var, again to reuse aliases if they're present. + state.constraints.push(constraints.equal_pattern_types( + num_type, + expected, + PatternCategory::Int, + region, + )); + } + + &FloatLiteral(num_precision_var, precision_var, _, _, bound) => { + // First constraint on the free num var; this improves the resolved type quality in + // case the bound is an alias. + let num_type = builtins::add_numeric_bound_constr( + types, + constraints, + &mut state.constraints, + num_precision_var, + num_precision_var, + bound, + region, + Category::Frac, + ); + let num_type_index = { + let typ = types.from_old_type(&num_type); + constraints.push_type(types, typ) + }; // NOTE: check me if something breaks! + + // Link the free num var with the float var and our expectation. + let float_type = { + let typ = types.from_old_type(&builtins::num_float(Type::Variable(precision_var))); + constraints.push_type(types, typ) + }; + + state.constraints.push({ + let expected_index = + constraints.push_expected_type(Expected::NoExpectation(float_type)); + constraints.equal_types(num_type_index, expected_index, Category::Frac, region) + }); + + // Also constrain the pattern against the num var, again to reuse aliases if they're present. + state.constraints.push(constraints.equal_pattern_types( + num_type_index, + expected, + PatternCategory::Float, + region, + )); + } + + StrLiteral(_) => { + let str_type = constraints.push_type(types, Types::STR); + state.constraints.push(constraints.equal_pattern_types( + str_type, + expected, + PatternCategory::Str, + region, + )); + } + + &SingleQuote(num_var, precision_var, _, bound) => { + // First constraint on the free num var; this improves the resolved type quality in + // case the bound is an alias. + let num_type = builtins::add_numeric_bound_constr( + types, + constraints, + &mut state.constraints, + num_var, + num_var, + bound, + region, + Category::Int, + ); + + let num_type_index = { + let typ = types.from_old_type(&num_type); + constraints.push_type(types, typ) + }; + + // Link the free num var with the int var and our expectation. + let int_type = { + let typ = types.from_old_type(&builtins::num_int(Type::Variable(precision_var))); + constraints.push_type(types, typ) + }; + + state.constraints.push({ + let expected_index = + constraints.push_expected_type(Expected::NoExpectation(int_type)); + constraints.equal_types( + num_type_index, // TODO check me if something breaks! + expected_index, + Category::Int, + region, + ) + }); + + // Also constrain the pattern against the num var, again to reuse aliases if they're present. + state.constraints.push(constraints.equal_pattern_types( + num_type_index, + expected, + PatternCategory::Character, + region, + )); + } + + TupleDestructure { + whole_var, + ext_var, + destructs, + } => { + state.vars.push(*whole_var); + state.vars.push(*ext_var); + let ext_type = Type::Variable(*ext_var); + + let mut elem_types: VecMap = VecMap::default(); + + for Loc { + value: + TupleDestruct { + destruct_index: index, + var, + typ, + }, + .. + } in destructs.iter() + { + let pat_type = Type::Variable(*var); + let pat_type_index = constraints.push_variable(*var); + let expected = + constraints.push_pat_expected_type(PExpected::NoExpectation(pat_type_index)); + + let (guard_var, loc_pattern) = typ; + let elem_type = { + let guard_type = constraints.push_variable(*guard_var); + let expected_pat = constraints.push_pat_expected_type(PExpected::ForReason( + PReason::PatternGuard, + pat_type_index, + loc_pattern.region, + )); + + state.constraints.push(constraints.pattern_presence( + guard_type, + expected_pat, + PatternCategory::PatternGuard, + region, + )); + state.vars.push(*guard_var); + + constrain_pattern( + types, + constraints, + env, + &loc_pattern.value, + loc_pattern.region, + expected, + state, + ); + + pat_type + }; + + elem_types.insert(*index, elem_type); + + state.vars.push(*var); + } + + let tuple_type = { + let typ = types.from_old_type(&Type::Tuple( + elem_types, + TypeExtension::from_non_annotation_type(ext_type), + )); + constraints.push_type(types, typ) + }; + + let whole_var_index = constraints.push_variable(*whole_var); + let expected_record = + constraints.push_expected_type(Expected::NoExpectation(tuple_type)); + let whole_con = constraints.equal_types( + whole_var_index, + expected_record, + Category::Storage(std::file!(), std::line!()), + region, + ); + + let record_con = constraints.pattern_presence( + whole_var_index, + expected, + PatternCategory::Record, + region, + ); + + state.constraints.push(whole_con); + state.constraints.push(record_con); + } + + RecordDestructure { + whole_var, + ext_var, + destructs, + } => { + state.vars.push(*whole_var); + state.vars.push(*ext_var); + let ext_type = Type::Variable(*ext_var); + + let mut field_types: SendMap> = SendMap::default(); + + for Loc { + value: + RecordDestruct { + var, + label, + symbol, + typ, + }, + .. + } in destructs + { + let pat_type = Type::Variable(*var); + let pat_type_index = constraints.push_variable(*var); + let expected = + constraints.push_pat_expected_type(PExpected::NoExpectation(pat_type_index)); + + if !state.headers.contains_key(symbol) { + state + .headers + .insert(*symbol, Loc::at(region, pat_type_index)); + } + + let field_type = match typ { + DestructType::Guard(guard_var, loc_guard) => { + let guard_type = constraints.push_variable(*guard_var); + let expected_pat = + constraints.push_pat_expected_type(PExpected::ForReason( + PReason::PatternGuard, + pat_type_index, + loc_guard.region, + )); + + state.constraints.push(constraints.pattern_presence( + guard_type, + expected_pat, + PatternCategory::PatternGuard, + region, + )); + state.vars.push(*guard_var); + + constrain_pattern( + types, + constraints, + env, + &loc_guard.value, + loc_guard.region, + expected, + state, + ); + + RecordField::Demanded(pat_type) + } + DestructType::Optional(expr_var, loc_expr) => { + let expr_type = constraints.push_variable(*expr_var); + let expected_pat = + constraints.push_pat_expected_type(PExpected::ForReason( + PReason::OptionalField, + pat_type_index, + loc_expr.region, + )); + + state.constraints.push(constraints.pattern_presence( + expr_type, + expected_pat, + PatternCategory::PatternDefault, + region, + )); + + state.vars.push(*expr_var); + + let expr_expected = constraints.push_expected_type(Expected::ForReason( + Reason::RecordDefaultField(label.clone()), + pat_type_index, + loc_expr.region, + )); + + let expr_con = constrain_expr( + types, + constraints, + env, + loc_expr.region, + &loc_expr.value, + expr_expected, + ); + state.constraints.push(expr_con); + + RecordField::Optional(pat_type) + } + DestructType::Required => { + // Named destructures like + // {foo} -> ... + // are equivalent to wildcards on the type of `foo`, so if `foo` is a tag + // union, we must add a constraint to ensure that this destructure opens it + // up. + if could_be_a_tag_union(types, pat_type_index) { + state + .delayed_is_open_constraints + .push(constraints.is_open_type(pat_type_index)); + } + + // No extra constraints necessary. + RecordField::Demanded(pat_type) + } + }; + + field_types.insert(label.clone(), field_type); + + state.vars.push(*var); + } + + let record_type = { + let typ = types.from_old_type(&Type::Record( + field_types, + TypeExtension::from_non_annotation_type(ext_type), + )); + constraints.push_type(types, typ) + }; + + let whole_var_index = constraints.push_variable(*whole_var); + let expected_record = + constraints.push_expected_type(Expected::NoExpectation(record_type)); + let whole_con = constraints.equal_types( + whole_var_index, + expected_record, + Category::Storage(std::file!(), std::line!()), + region, + ); + + let record_con = constraints.pattern_presence( + whole_var_index, + expected, + PatternCategory::Record, + region, + ); + + state.constraints.push(whole_con); + state.constraints.push(record_con); + } + + List { + list_var, + elem_var, + patterns: ListPatterns { patterns, opt_rest }, + } => { + let elem_var_index = constraints.push_variable(*elem_var); + + if let Some((_, Some(rest))) = opt_rest { + state.headers.insert( + *rest, + Loc { + region, + value: *constraints[expected].get_type_ref(), + }, + ); + } + + for loc_pat in patterns.iter() { + let expected = constraints.push_pat_expected_type(PExpected::ForReason( + PReason::ListElem, + elem_var_index, + loc_pat.region, + )); + + constrain_pattern( + types, + constraints, + env, + &loc_pat.value, + loc_pat.region, + expected, + state, + ); + } + + let list_var_index = constraints.push_variable(*list_var); + let solved_list = { + let typ = types.from_old_type(&Type::Apply( + Symbol::LIST_LIST, + vec![Loc::at(region, Type::Variable(*elem_var))], + region, + )); + constraints.push_type(types, typ) + }; + let store_solved_list = constraints.store(solved_list, *list_var, file!(), line!()); + + let expected_constraint = constraints.pattern_presence( + list_var_index, + expected, + PatternCategory::List, + region, + ); + + state.vars.push(*list_var); + state.vars.push(*elem_var); + state.constraints.push(store_solved_list); + state.constraints.push(expected_constraint); + } + + AppliedTag { + whole_var, + ext_var, + tag_name, + arguments, + } => { + let argument_types = constraints.variable_slice(arguments.iter().map(|(var, _)| *var)); + + for (index, (pattern_var, loc_pattern)) in arguments.iter().enumerate() { + state.vars.push(*pattern_var); + + let pattern_type = constraints.push_variable(*pattern_var); + + let expected = constraints.push_pat_expected_type(PExpected::ForReason( + PReason::TagArg { + tag_name: tag_name.clone(), + index: HumanIndex::zero_based(index), + }, + pattern_type, + region, + )); + constrain_pattern( + types, + constraints, + env, + &loc_pattern.value, + loc_pattern.region, + expected, + state, + ); + } + + let pat_category = PatternCategory::Ctor(tag_name.clone()); + let expected_type = *constraints[expected].get_type_ref(); + + let whole_con = constraints.includes_tag( + expected_type, + tag_name.clone(), + argument_types, + pat_category.clone(), + region, + ); + + let whole_type = constraints.push_variable(*whole_var); + + let tag_con = constraints.pattern_presence(whole_type, expected, pat_category, region); + + state.vars.push(*whole_var); + state.vars.push(*ext_var); + state.constraints.push(whole_con); + state.constraints.push(tag_con); + } + + UnwrappedOpaque { + whole_var, + opaque, + argument, + specialized_def_type, + type_arguments, + lambda_set_variables, + } => { + // Suppose we are constraining the pattern \@Id who, where Id n := [Id U64 n] + let (arg_pattern_var, loc_arg_pattern) = &**argument; + let arg_pattern_type_index = constraints.push_variable(*arg_pattern_var); + + let opaque_type = { + let typ = types.from_old_type(&Type::Alias { + symbol: *opaque, + type_arguments: type_arguments + .iter() + .map(|v| OptAbleType { + typ: Type::Variable(v.var), + opt_abilities: v.opt_abilities.clone(), + }) + .collect(), + lambda_set_variables: lambda_set_variables.clone(), + infer_ext_in_output_types: vec![], + actual: Box::new(Type::Variable(*arg_pattern_var)), + kind: AliasKind::Opaque, + }); + constraints.push_type(types, typ) + }; + + // First, add a constraint for the argument "who" + let arg_pattern_expected = constraints + .push_pat_expected_type(PExpected::NoExpectation(arg_pattern_type_index)); + constrain_pattern( + types, + constraints, + env, + &loc_arg_pattern.value, + loc_arg_pattern.region, + arg_pattern_expected, + state, + ); + + // Next, link `whole_var` to the opaque type of "@Id who" + let whole_var_index = constraints.push_variable(*whole_var); + let expected_opaque = + constraints.push_expected_type(Expected::NoExpectation(opaque_type)); + let whole_con = constraints.equal_types( + whole_var_index, + expected_opaque, + Category::Storage(std::file!(), std::line!()), + region, + ); + + // Link the entire wrapped opaque type (with the now-constrained argument) to the type + // variables of the opaque type. + // + // For example, suppose we have `O k := [A k, B k]`, and the pattern `@O (A s) -> s == ""`. + // Previous constraints will have solved `typeof s ~ Str`, and we have the + // `specialized_def_type` being `[A k1, B k1]`, specializing `k` as `k1` for this opaque + // usage. + // We now want to link `typeof s ~ k1`, so to capture this relationship, we link + // the type of `A s` (the arg type) to `[A k1, B k1]` (the specialized opaque type). + // + // This must **always** be a presence constraint, that is enforcing + // `[A k1, B k1] += typeof (A s)`, because we are in a destructure position and not + // all constructors are covered in this branch! + let arg_pattern_type = constraints.push_variable(*arg_pattern_var); + let specialized_type_index = { + let typ = types.from_old_type(specialized_def_type); + constraints.push_type(types, typ) + }; + let specialized_type_expected = constraints + .push_pat_expected_type(PExpected::NoExpectation(specialized_type_index)); + + let link_type_variables_con = constraints.pattern_presence( + arg_pattern_type, + specialized_type_expected, + PatternCategory::Opaque(*opaque), + loc_arg_pattern.region, + ); + + // Next, link `whole_var` (the type of "@Id who") to the expected type + let whole_type = constraints.push_variable(*whole_var); + let opaque_pattern_con = constraints.pattern_presence( + whole_type, + expected, + PatternCategory::Opaque(*opaque), + region, + ); + + state + .vars + .extend_from_slice(&[*arg_pattern_var, *whole_var]); + // Also add the fresh variables we created for the type argument and lambda sets + state.vars.extend(type_arguments.iter().map(|v| v.var)); + state.vars.extend(lambda_set_variables.iter().map(|v| { + v.0.expect_variable("all lambda sets should be fresh variables here") + })); + + state.constraints.extend_from_slice(&[ + whole_con, + link_type_variables_con, + opaque_pattern_con, + ]); + } + } +} + +fn could_be_a_tag_union(types: &Types, typ: TypeOrVar) -> bool { + match typ.split() { + Ok(typ_index) => !matches!( + types[typ_index], + TypeTag::Apply { .. } | TypeTag::Function(..) | TypeTag::Record(..) + ), + Err(_) => { + // Variables are opaque at this point, assume yes + true + } + } +} diff --git a/crates/compiler/debug_flags/Cargo.toml b/crates/compiler/debug_flags/Cargo.toml new file mode 100644 index 0000000000..b3b9b4bc15 --- /dev/null +++ b/crates/compiler/debug_flags/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "roc_debug_flags" +description = "Environment variables that can be toggled to aid debugging of the compiler itself." + +authors.workspace = true +edition.workspace = true +license.workspace = true +version.workspace = true + +[dependencies] diff --git a/crates/compiler/debug_flags/src/lib.rs b/crates/compiler/debug_flags/src/lib.rs new file mode 100644 index 0000000000..ede77bd317 --- /dev/null +++ b/crates/compiler/debug_flags/src/lib.rs @@ -0,0 +1,176 @@ +//! Flags for debugging the Roc compiler. +//! +//! Lists environment variable flags that can be enabled for verbose debugging features in debug +//! builds of the compiler. +//! +//! For example, I might define the following alias to run cargo with all unifications and +//! expanded type aliases printed: +//! +//! ```bash +//! alias cargo="\ +//! ROC_PRINT_UNIFICATIONS=1 \ +//! ROC_PRETTY_PRINT_ALIAS_CONTENTS=1 \ +//! cargo" +//! ``` +//! +//! More generally, I have the following: +//! +//! ```bash +//! alias cargo="\ +//! ROC_PRETTY_PRINT_ALIAS_CONTENTS=0 \ +//! ROC_PRINT_UNIFICATIONS=0 \ +//! ROC_PRINT_MISMATCHES=0 \ +//! ROC_PRINT_IR_AFTER_SPECIALIZATION=0 \ +//! ROC_PRINT_IR_AFTER_RESET_REUSE=0 \ +//! ROC_PRINT_IR_AFTER_REFCOUNT=0 \ +//! ROC_PRETTY_PRINT_IR_SYMBOLS=0 \ +//! # ...other flags +//! cargo" +//! ``` +//! +//! Now you can turn debug flags on and off as you like. +//! +//! These flags are also set in .cargo/config found at the repository root. You can modify them +//! there to avoid maintaining a separate script. + +#[macro_export] +macro_rules! dbg_set { + ($flag:path) => {{ + #[cfg(not(debug_assertions))] + { + false + } + #[cfg(debug_assertions)] + { + let flag = std::env::var($flag); + flag.is_ok() && flag.as_deref() != Ok("0") + } + }}; +} + +#[macro_export] +macro_rules! dbg_do { + ($flag:path, $expr:expr) => { + #[cfg(debug_assertions)] + { + if $crate::dbg_set!($flag) { + $expr + } + } + }; +} + +macro_rules! flags { + ($($(#[doc = $doc:expr])+ $flag:ident)*) => {$( + $(#[doc = $doc])+ + pub static $flag: &str = stringify!($flag); + )*}; +} + +flags! { + // ===Types=== + + /// Expands the contents of aliases during pretty-printing of types. + ROC_PRETTY_PRINT_ALIAS_CONTENTS + + // ===Solve=== + + /// Prints type unifications, before and after they happen. + /// Only use this in single-threaded mode! + ROC_PRINT_UNIFICATIONS + + /// Prints types whose ability impls failed to be derived. + ROC_PRINT_UNDERIVABLE + + /// Prints traces of unspecialized lambda set compaction + ROC_TRACE_COMPACTION + + /// Like ROC_PRINT_UNIFICATIONS, in the context of typechecking derived implementations. + /// Only use this in single-threaded mode! + ROC_PRINT_UNIFICATIONS_DERIVED + + /// Prints all type mismatches hit during type unification. + ROC_PRINT_MISMATCHES + + /// Prints all type variables entered for fixpoint-fixing. + ROC_PRINT_FIXPOINT_FIXING + + /// Verifies that after let-generalization of a def, any rigid variables in the type annotation + /// of the def are indeed generalized. + /// + /// Note that rigids need not always be generalized in a def. For example, they may be + /// constrained by a type from a lower rank, as `b` is in the following def: + /// + /// F a : { foo : a } + /// foo = \arg -> + /// x : F b + /// x = arg + /// x.foo + /// + /// Instead, this flag is useful for checking that in general, introduction is correct, when + /// chainging how defs are constrained. + ROC_VERIFY_RIGID_LET_GENERALIZED + + /// Verifies that an `occurs` check indeed only contains non-recursive types that need to be + /// fixed-up with one new recursion variable. + /// + /// This flag is disabled by default because an occurs check may pass through another recursive + /// structure for which a recursive pointer has already been allocated. However, during debugging, + /// you may be interested in checking that the occurs check finds only one variable to fix. + ROC_VERIFY_OCCURS_ONE_RECURSION + + // ===Mono=== + + /// Type-checks the mono IR after specialization. + ROC_CHECK_MONO_IR + + /// Writes a pretty-printed mono IR to stderr after function specialization. + ROC_PRINT_IR_AFTER_SPECIALIZATION + + /// Writes a pretty-printed mono IR to stderr after insertion of reset/reuse + /// instructions. + ROC_PRINT_IR_AFTER_RESET_REUSE + + /// Writes a pretty-printed mono IR to stderr after insertion of refcount + /// instructions. + ROC_PRINT_IR_AFTER_REFCOUNT + + /// Writes a pretty-printed mono IR to stderr after the tail recursion (modulo cons) + /// has been applied. + ROC_PRINT_IR_AFTER_TRMC + + /// Writes a pretty-printed mono IR to stderr after performing dropspecialization. + /// Which inlines drop functions to remove pairs of alloc/dealloc instructions of its children. + ROC_PRINT_IR_AFTER_DROP_SPECIALIZATION + + /// Prints debug information during the alias analysis pass. + ROC_DEBUG_ALIAS_ANALYSIS + + /// Print to stderr when a runtime error function is generated. + ROC_PRINT_RUNTIME_ERROR_GEN + + /// Generate a layout error when an unbound type variable is found, rather than generating the + /// void layout. + ROC_NO_UNBOUND_LAYOUT + + // ===LLVM Gen=== + + /// Prints LLVM function verification output. + ROC_PRINT_LLVM_FN_VERIFICATION + + // ===WASM Gen=== + + /// Writes a `final.wasm` file to /tmp + ROC_WRITE_FINAL_WASM + + /// Prints Wasm interpreter debug log in test_gen + ROC_LOG_WASM_INTERP + + // ===Load=== + + /// Print load phases as they complete. + ROC_PRINT_LOAD_LOG + + /// Don't build and use the subs cache (speeds up compilation of load and previous crates) + ROC_SKIP_SUBS_CACHE +} diff --git a/crates/compiler/derive/Cargo.toml b/crates/compiler/derive/Cargo.toml new file mode 100644 index 0000000000..adbdba631f --- /dev/null +++ b/crates/compiler/derive/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "roc_derive" +description = "Provides auto-derivers for builtin abilities like `Hash` and `Decode`." + +authors.workspace = true +edition.workspace = true +license.workspace = true +version.workspace = true + +[dependencies] +roc_can = { path = "../can" } +roc_checkmate = { path = "../checkmate" } +roc_collections = { path = "../collections" } +roc_derive_key = { path = "../derive_key" } +roc_error_macros = { path = "../../error_macros" } +roc_module = { path = "../module" } +roc_region = { path = "../region" } +roc_solve_schema = { path = "../solve_schema" } +roc_types = { path = "../types" } +roc_unify = { path = "../unify" } + +bumpalo.workspace = true + +[features] +debug-derived-symbols = ["roc_module/debug-symbols"] +default = [] +# Enables open extension variables for constructed records and tag unions. +# This is not necessary for code generation, but may be necessary if you are +# constraining and solving generated derived bodies. +open-extension-vars = [] diff --git a/crates/compiler/derive/src/decoding.rs b/crates/compiler/derive/src/decoding.rs new file mode 100644 index 0000000000..9f0b29c807 --- /dev/null +++ b/crates/compiler/derive/src/decoding.rs @@ -0,0 +1,209 @@ +//! Derivers for the `Decoding` ability. + +use roc_can::expr::{AnnotatedMark, ClosureData, Expr, Recursive}; +use roc_can::pattern::Pattern; + +use roc_derive_key::decoding::FlatDecodableKey; +use roc_module::called_via::CalledVia; +use roc_module::symbol::Symbol; +use roc_region::all::Loc; +use roc_types::subs::{ + Content, FlatType, LambdaSet, OptVariable, SubsSlice, UnionLambdas, Variable, +}; + +use crate::util::Env; +use crate::{synth_var, DerivedBody}; + +mod list; +mod record; +mod tuple; + +pub(crate) fn derive_decoder( + env: &mut Env<'_>, + key: FlatDecodableKey, + def_symbol: Symbol, +) -> DerivedBody { + let (body, body_type) = match key { + FlatDecodableKey::List() => list::decoder(env, def_symbol), + FlatDecodableKey::Record(fields) => record::decoder(env, def_symbol, fields), + FlatDecodableKey::Tuple(arity) => tuple::decoder(env, def_symbol, arity), + }; + + let specialization_lambda_sets = + env.get_specialization_lambda_sets(body_type, Symbol::DECODE_DECODER); + + DerivedBody { + body, + body_type, + specialization_lambda_sets, + } +} + +// Wraps `myDecoder` in `Decode.custom \bytes, fmt -> Decode.decodeWith bytes myDecoder fmt`. +// +// Needed to work around the Higher-Region Restriction. See https://github.com/roc-lang/roc/issues/3724. +fn wrap_in_decode_custom_decode_with( + env: &mut Env, + bytes: Symbol, + fmt: (Symbol, Variable), + sorted_inner_decoder_captures: Vec<(Symbol, Variable)>, + inner_decoder: (Expr, Variable), +) -> (Expr, Variable) { + use Expr::*; + + debug_assert!({ + let mut sorted = sorted_inner_decoder_captures.clone(); + sorted.sort_by_key(|(sym, _)| *sym); + sorted == sorted_inner_decoder_captures + }); + + let (bytes_sym, bytes_var) = (bytes, Variable::LIST_U8); + let (fmt_sym, fmt_var) = fmt; + let (inner_decoder, inner_decoder_var) = inner_decoder; + + // Decode.decodeWith bytes inner_decoder fmt : DecodeResult val + let (decode_with_call, decode_with_result_var) = { + // Decode.decodeWith : List U8, Decoder val fmt, fmt -> DecodeResult val where fmt implements DecoderFormatting + let decode_with_type = env.import_builtin_symbol_var(Symbol::DECODE_DECODE_WITH); + + // Decode.decodeWith : bytes, inner_decoder, fmt -> DecoderResult (List val) + let this_decode_with_var_slice = + SubsSlice::insert_into_subs(env.subs, [bytes_var, inner_decoder_var, fmt_var]); + let this_decode_with_clos_var = env.subs.fresh_unnamed_flex_var(); + let this_decode_with_ret_var = env.subs.fresh_unnamed_flex_var(); + let this_decode_with_fn_var = synth_var( + env.subs, + Content::Structure(FlatType::Func( + this_decode_with_var_slice, + this_decode_with_clos_var, + this_decode_with_ret_var, + )), + ); + + // List U8, Decoder val fmt, fmt -> DecodeResult val where fmt implements DecoderFormatting + // ~ bytes, Decoder (List elem) fmt, fmt -> DecoderResult (List val) + env.unify(decode_with_type, this_decode_with_fn_var); + + let decode_with_var = Var(Symbol::DECODE_DECODE_WITH, this_decode_with_fn_var); + let decode_with_fn = Box::new(( + this_decode_with_fn_var, + Loc::at_zero(decode_with_var), + this_decode_with_clos_var, + this_decode_with_ret_var, + )); + let decode_with_call = Call( + decode_with_fn, + vec![ + // bytes inner_decoder fmt + (bytes_var, Loc::at_zero(Var(bytes_sym, bytes_var))), + (inner_decoder_var, Loc::at_zero(inner_decoder)), + (fmt_var, Loc::at_zero(Var(fmt_sym, fmt_var))), + ], + CalledVia::Space, + ); + + (decode_with_call, this_decode_with_ret_var) + }; + + // \bytes, fmt -> Decode.decodeWith bytes myDecoder fmt + let (custom_lambda, custom_var) = { + let fn_name = env.new_symbol("custom"); + + // Create fn_var for ambient capture; we fix it up below. + let fn_var = synth_var(env.subs, Content::Error); + + // -[[fn_name]]-> + let fn_name_labels = UnionLambdas::insert_into_subs( + env.subs, + [( + fn_name, + sorted_inner_decoder_captures.iter().map(|(_, var)| *var), + )], + ); + let fn_clos_var = synth_var( + env.subs, + Content::LambdaSet(LambdaSet { + solved: fn_name_labels, + recursion_var: OptVariable::NONE, + unspecialized: SubsSlice::default(), + ambient_function: fn_var, + }), + ); + + // bytes, fmt -[[fn_name]]-> DecoderResult (List elem) + let args_slice = SubsSlice::insert_into_subs(env.subs, [bytes_var, fmt_var]); + env.subs.set_content( + fn_var, + Content::Structure(FlatType::Func( + args_slice, + fn_clos_var, + decode_with_result_var, + )), + ); + + // \bytes, fmt -[[fn_name]]-> Decode.decodeWith bytes inner_decoder fmt + let clos = Closure(ClosureData { + function_type: fn_var, + closure_type: fn_clos_var, + return_type: decode_with_result_var, + name: fn_name, + captured_symbols: sorted_inner_decoder_captures, + recursive: Recursive::NotRecursive, + arguments: vec![ + ( + bytes_var, + AnnotatedMark::known_exhaustive(), + Loc::at_zero(Pattern::Identifier(bytes_sym)), + ), + ( + fmt_var, + AnnotatedMark::known_exhaustive(), + Loc::at_zero(Pattern::Identifier(fmt_sym)), + ), + ], + loc_body: Box::new(Loc::at_zero(decode_with_call)), + }); + + (clos, fn_var) + }; + + // Decode.custom \bytes, fmt -> Decode.decodeWith bytes inner_decoder fmt + let (decode_custom_call, decoder_var) = { + // (List U8, fmt -> DecodeResult val) -> Decoder val fmt where fmt implements DecoderFormatting + let decode_custom_type = env.import_builtin_symbol_var(Symbol::DECODE_CUSTOM); + + // (List U8, fmt -> DecodeResult (List elem)) -> Decoder (List elem) fmt + let this_decode_custom_args = SubsSlice::insert_into_subs(env.subs, [custom_var]); + let this_decode_custom_clos_var = env.subs.fresh_unnamed_flex_var(); + let this_decode_custom_ret_var = env.subs.fresh_unnamed_flex_var(); + let this_decode_custom_fn_var = synth_var( + env.subs, + Content::Structure(FlatType::Func( + this_decode_custom_args, + this_decode_custom_clos_var, + this_decode_custom_ret_var, + )), + ); + + // (List U8, fmt -> DecodeResult val) -> Decoder val fmt where fmt implements DecoderFormatting + // ~ (List U8, fmt -> DecodeResult (List elem)) -> Decoder (List elem) fmt + env.unify(decode_custom_type, this_decode_custom_fn_var); + + let decode_custom_var = Var(Symbol::DECODE_CUSTOM, this_decode_custom_fn_var); + let decode_custom_fn = Box::new(( + this_decode_custom_fn_var, + Loc::at_zero(decode_custom_var), + this_decode_custom_clos_var, + this_decode_custom_ret_var, + )); + let decode_custom_call = Call( + decode_custom_fn, + vec![(custom_var, Loc::at_zero(custom_lambda))], + CalledVia::Space, + ); + + (decode_custom_call, this_decode_custom_ret_var) + }; + + (decode_custom_call, decoder_var) +} diff --git a/crates/compiler/derive/src/decoding/list.rs b/crates/compiler/derive/src/decoding/list.rs new file mode 100644 index 0000000000..ed2cd66de8 --- /dev/null +++ b/crates/compiler/derive/src/decoding/list.rs @@ -0,0 +1,104 @@ +use roc_can::expr::Expr; + +use roc_error_macros::internal_error; +use roc_module::called_via::CalledVia; + +use roc_module::symbol::Symbol; +use roc_region::all::Loc; +use roc_types::subs::{Content, FlatType, GetSubsSlice, SubsSlice, Variable}; +use roc_types::types::AliasKind; + +use crate::decoding::wrap_in_decode_custom_decode_with; +use crate::synth_var; +use crate::util::Env; + +pub(crate) fn decoder(env: &mut Env<'_>, _def_symbol: Symbol) -> (Expr, Variable) { + // Build + // + // def_symbol : Decoder (List elem) fmt where elem implements Decoding, fmt implements DecoderFormatting + // def_symbol = Decode.custom \bytes, fmt -> Decode.decodeWith bytes (Decode.list Decode.decoder) fmt + // + // NB: reduction to `Decode.list Decode.decoder` is not possible to the HRR. + + use Expr::*; + + // Decode.list Decode.decoder : Decoder (List elem) fmt + let (decode_list_call, this_decode_list_ret_var) = { + // List elem + let elem_var = env.subs.fresh_unnamed_flex_var(); + + // Decode.decoder : Decoder elem fmt where elem implements Decoding, fmt implements EncoderFormatting + let (elem_decoder, elem_decoder_var) = { + // build `Decode.decoder : Decoder elem fmt` type + // Decoder val fmt where val implements Decoding, fmt implements EncoderFormatting + let elem_decoder_var = env.import_builtin_symbol_var(Symbol::DECODE_DECODER); + + // set val ~ elem + let val_var = match env.subs.get_content_without_compacting(elem_decoder_var) { + Content::Alias(Symbol::DECODE_DECODER_OPAQUE, vars, _, AliasKind::Opaque) + if vars.type_variables_len == 2 => + { + env.subs.get_subs_slice(vars.type_variables())[0] + } + _ => internal_error!("Decode.decode not an opaque type"), + }; + + env.unify(val_var, elem_var); + + ( + AbilityMember(Symbol::DECODE_DECODER, None, elem_decoder_var), + elem_decoder_var, + ) + }; + + // Build `Decode.list Decode.decoder` type + // Decoder val fmt -[uls]-> Decoder (List val) fmt where fmt implements DecoderFormatting + let decode_list_fn_var = env.import_builtin_symbol_var(Symbol::DECODE_LIST); + + // Decoder elem fmt -a-> b + let elem_decoder_var_slice = SubsSlice::insert_into_subs(env.subs, [elem_decoder_var]); + let this_decode_list_clos_var = env.subs.fresh_unnamed_flex_var(); + let this_decode_list_ret_var = env.subs.fresh_unnamed_flex_var(); + let this_decode_list_fn_var = synth_var( + env.subs, + Content::Structure(FlatType::Func( + elem_decoder_var_slice, + this_decode_list_clos_var, + this_decode_list_ret_var, + )), + ); + + // Decoder val fmt -[uls]-> Decoder (List val) fmt where fmt implements DecoderFormatting + // ~ Decoder elem fmt -a -> b + env.unify(decode_list_fn_var, this_decode_list_fn_var); + + let decode_list_member = AbilityMember(Symbol::DECODE_LIST, None, this_decode_list_fn_var); + let decode_list_fn = Box::new(( + decode_list_fn_var, + Loc::at_zero(decode_list_member), + this_decode_list_clos_var, + this_decode_list_ret_var, + )); + + let decode_list_call = Call( + decode_list_fn, + vec![(elem_decoder_var, Loc::at_zero(elem_decoder))], + CalledVia::Space, + ); + + (decode_list_call, this_decode_list_ret_var) + }; + + let bytes_sym = env.new_symbol("bytes"); + let fmt_sym = env.new_symbol("fmt"); + let fmt_var = env.subs.fresh_unnamed_flex_var(); + let captures = vec![]; + + wrap_in_decode_custom_decode_with( + env, + bytes_sym, + (fmt_sym, fmt_var), + captures, + (decode_list_call, this_decode_list_ret_var), + ) +} diff --git a/crates/compiler/derive/src/decoding/record.rs b/crates/compiler/derive/src/decoding/record.rs new file mode 100644 index 0000000000..adc0196002 --- /dev/null +++ b/crates/compiler/derive/src/decoding/record.rs @@ -0,0 +1,990 @@ +use roc_can::expr::{ + AnnotatedMark, ClosureData, Expr, Field, Recursive, WhenBranch, WhenBranchPattern, +}; +use roc_can::pattern::Pattern; +use roc_collections::SendMap; +use roc_module::called_via::CalledVia; +use roc_module::ident::Lowercase; +use roc_module::symbol::Symbol; +use roc_region::all::{Loc, Region}; +use roc_types::subs::{ + Content, ExhaustiveMark, FlatType, LambdaSet, OptVariable, RecordFields, RedundantMark, + SubsSlice, TagExt, UnionLambdas, UnionTags, Variable, +}; +use roc_types::types::RecordField; + +use crate::synth_var; +use crate::util::{Env, ExtensionKind}; + +use super::wrap_in_decode_custom_decode_with; + +/// Implements decoding of a record. For example, for +/// +/// ```text +/// {first: a, second: b} +/// ``` +/// +/// we'd like to generate an impl like +/// +/// ```roc +/// decoder : Decoder {first: a, second: b} fmt where a implements Decoding, b implements Decoding, fmt implements DecoderFormatting +/// decoder = +/// initialState : {f0: Result a [NoField], f1: Result b [NoField]} +/// initialState = {f0: Err NoField, f1: Err NoField} +/// +/// stepField = \state, field -> +/// when field is +/// "first" -> +/// Keep (Decode.custom \bytes, fmt -> +/// when Decode.decodeWith bytes Decode.decoder fmt is +/// {result, rest} -> +/// {result: Result.map result \val -> {state & f0: Ok val}, rest}) +/// "second" -> +/// Keep (Decode.custom \bytes, fmt -> +/// when Decode.decodeWith bytes Decode.decoder fmt is +/// {result, rest} -> +/// {result: Result.map result \val -> {state & f1: Ok val}, rest}) +/// _ -> Skip +/// +/// finalizer = \{f0, f1} -> +/// when f0 is +/// Ok first -> +/// when f1 is +/// Ok second -> Ok {first, second} +/// Err NoField -> Err TooShort +/// Err NoField -> Err TooShort +/// +/// Decode.custom \bytes, fmt -> Decode.decodeWith bytes (Decode.record initialState stepField finalizer) fmt +/// ``` +pub(crate) fn decoder( + env: &mut Env, + _def_symbol: Symbol, + fields: Vec, +) -> (Expr, Variable) { + // The decoded type of each field in the record, e.g. {first: a, second: b}. + let mut field_vars = Vec::with_capacity(fields.len()); + // The type of each field in the decoding state, e.g. {first: Result a [NoField], second: Result b [NoField]} + let mut result_field_vars = Vec::with_capacity(fields.len()); + + // initialState = ... + let (initial_state_var, initial_state) = + initial_state(env, &fields, &mut field_vars, &mut result_field_vars); + + // finalizer = ... + let (finalizer, finalizer_var, decode_err_var) = finalizer( + env, + initial_state_var, + &fields, + &field_vars, + &result_field_vars, + ); + + // stepField = ... + let (step_field, step_var) = step_field( + env, + fields, + &field_vars, + &result_field_vars, + initial_state_var, + decode_err_var, + ); + + // Build up the type of `Decode.record` we expect + let record_decoder_var = env.subs.fresh_unnamed_flex_var(); + let decode_record_lambda_set = env.subs.fresh_unnamed_flex_var(); + let decode_record_var = env.import_builtin_symbol_var(Symbol::DECODE_RECORD); + let this_decode_record_var = { + let flat_type = FlatType::Func( + SubsSlice::insert_into_subs(env.subs, [initial_state_var, step_var, finalizer_var]), + decode_record_lambda_set, + record_decoder_var, + ); + + synth_var(env.subs, Content::Structure(flat_type)) + }; + + env.unify(decode_record_var, this_decode_record_var); + + // Decode.record initialState stepField finalizer + let call_decode_record = Expr::Call( + Box::new(( + this_decode_record_var, + Loc::at_zero(Expr::AbilityMember( + Symbol::DECODE_RECORD, + None, + this_decode_record_var, + )), + decode_record_lambda_set, + record_decoder_var, + )), + vec![ + (initial_state_var, Loc::at_zero(initial_state)), + (step_var, Loc::at_zero(step_field)), + (finalizer_var, Loc::at_zero(finalizer)), + ], + CalledVia::Space, + ); + + let (call_decode_custom, decode_custom_ret_var) = { + let bytes_sym = env.new_symbol("bytes"); + let fmt_sym = env.new_symbol("fmt"); + let fmt_var = env.subs.fresh_unnamed_flex_var(); + + let (decode_custom, decode_custom_var) = wrap_in_decode_custom_decode_with( + env, + bytes_sym, + (fmt_sym, fmt_var), + vec![], + (call_decode_record, record_decoder_var), + ); + + (decode_custom, decode_custom_var) + }; + + (call_decode_custom, decode_custom_ret_var) +} + +// Example: +// stepField = \state, field -> +// when field is +// "first" -> +// Keep (Decode.custom \bytes, fmt -> +// # Uses a single-branch `when` because `let` is more expensive to monomorphize +// # due to checks for polymorphic expressions, and `rec` would be polymorphic. +// when Decode.decodeWith bytes Decode.decoder fmt is +// rec -> +// { +// rest: rec.rest, +// result: when rec.result is +// Ok val -> Ok {state & first: Ok val}, +// Err err -> Err err +// }) +// +// "second" -> +// Keep (Decode.custom \bytes, fmt -> +// when Decode.decodeWith bytes Decode.decoder fmt is +// rec -> +// { +// rest: rec.rest, +// result: when rec.result is +// Ok val -> Ok {state & second: Ok val}, +// Err err -> Err err +// }) +// +// _ -> Skip +fn step_field( + env: &mut Env, + fields: Vec, + field_vars: &[Variable], + result_field_vars: &[Variable], + state_record_var: Variable, + decode_err_var: Variable, +) -> (Expr, Variable) { + let state_arg_symbol = env.new_symbol("stateRecord"); + let field_arg_symbol = env.new_symbol("field"); + + // +1 because of the default branch. + let mut branches = Vec::with_capacity(fields.len() + 1); + let keep_payload_var = env.subs.fresh_unnamed_flex_var(); + let keep_or_skip_var = { + let keep_payload_subs_slice = SubsSlice::insert_into_subs(env.subs, [keep_payload_var]); + let flat_type = FlatType::TagUnion( + UnionTags::insert_slices_into_subs( + env.subs, + [ + ("Keep".into(), keep_payload_subs_slice), + ("Skip".into(), Default::default()), + ], + ), + TagExt::Any(Variable::EMPTY_TAG_UNION), + ); + + synth_var(env.subs, Content::Structure(flat_type)) + }; + + for ((field_name, &field_var), &result_field_var) in fields + .into_iter() + .zip(field_vars.iter()) + .zip(result_field_vars.iter()) + { + // Example: + // "first" -> + // Keep (Decode.custom \bytes, fmt -> + // # Uses a single-branch `when` because `let` is more expensive to monomorphize + // # due to checks for polymorphic expressions, and `rec` would be polymorphic. + // when Decode.decodeWith bytes Decode.decoder fmt is + // rec -> + // { + // rest: rec.rest, + // result: when rec.result is + // Ok val -> Ok {state & first: Ok val}, + // Err err -> Err err + // } + // ) + + let this_custom_callback_var; + let custom_callback_ret_var; + let custom_callback = { + // \bytes, fmt -> + // when Decode.decodeWith bytes Decode.decoder fmt is + // rec -> + // { + // rest: rec.rest, + // result: when rec.result is + // Ok val -> Ok {state & first: Ok val}, + // Err err -> Err err + // } + let bytes_arg_symbol = env.new_symbol("bytes"); + let fmt_arg_symbol = env.new_symbol("fmt"); + let bytes_arg_var = env.subs.fresh_unnamed_flex_var(); + let fmt_arg_var = env.subs.fresh_unnamed_flex_var(); + + // rec.result : [Ok field_var, Err DecodeError] + let rec_dot_result = { + let tag_union = FlatType::TagUnion( + UnionTags::for_result(env.subs, field_var, decode_err_var), + TagExt::Any(Variable::EMPTY_TAG_UNION), + ); + + synth_var(env.subs, Content::Structure(tag_union)) + }; + + // rec : { rest: List U8, result: (typeof rec.result) } + let rec_var = { + let fields = RecordFields::insert_into_subs( + env.subs, + [ + ("rest".into(), RecordField::Required(Variable::LIST_U8)), + ("result".into(), RecordField::Required(rec_dot_result)), + ], + ); + let record = FlatType::Record(fields, Variable::EMPTY_RECORD); + + synth_var(env.subs, Content::Structure(record)) + }; + + // `Decode.decoder` for the field's value + let decoder_var = env.import_builtin_symbol_var(Symbol::DECODE_DECODER); + let decode_with_var = env.import_builtin_symbol_var(Symbol::DECODE_DECODE_WITH); + let lambda_set_var = env.subs.fresh_unnamed_flex_var(); + let this_decode_with_var = { + let subs_slice = SubsSlice::insert_into_subs( + env.subs, + [bytes_arg_var, decoder_var, fmt_arg_var], + ); + let this_decode_with_var = synth_var( + env.subs, + Content::Structure(FlatType::Func(subs_slice, lambda_set_var, rec_var)), + ); + + env.unify(decode_with_var, this_decode_with_var); + + this_decode_with_var + }; + + // The result of decoding this field's value - either the updated state, or a decoding error. + let when_expr_var = { + let flat_type = FlatType::TagUnion( + UnionTags::for_result(env.subs, state_record_var, decode_err_var), + TagExt::Any(Variable::EMPTY_TAG_UNION), + ); + + synth_var(env.subs, Content::Structure(flat_type)) + }; + + // What our decoder passed to `Decode.custom` returns - the result of decoding the + // field's value, and the remaining bytes. + custom_callback_ret_var = { + let rest_field = RecordField::Required(Variable::LIST_U8); + let result_field = RecordField::Required(when_expr_var); + let flat_type = FlatType::Record( + RecordFields::insert_into_subs( + env.subs, + [("rest".into(), rest_field), ("result".into(), result_field)], + ), + Variable::EMPTY_RECORD, + ); + + synth_var(env.subs, Content::Structure(flat_type)) + }; + + let custom_callback_body = { + let rec_symbol = env.new_symbol("rec"); + + // # Uses a single-branch `when` because `let` is more expensive to monomorphize + // # due to checks for polymorphic expressions, and `rec` would be polymorphic. + // when Decode.decodeWith bytes Decode.decoder fmt is + // rec -> + // { + // rest: rec.rest, + // result: when rec.result is + // Ok val -> Ok {state & first: Ok val}, + // Err err -> Err err + // } + let branch_body = { + let result_val = { + // result: when rec.result is + // Ok val -> Ok {state & first: Ok val}, + // Err err -> Err err + let ok_val_symbol = env.new_symbol("val"); + let err_val_symbol = env.new_symbol("err"); + let ok_branch_expr = { + // Ok {state & first: Ok val}, + let mut updates = SendMap::default(); + + updates.insert( + field_name.clone(), + Field { + var: result_field_var, + region: Region::zero(), + loc_expr: Box::new(Loc::at_zero(Expr::Tag { + tag_union_var: result_field_var, + ext_var: env.new_ext_var(ExtensionKind::TagUnion), + name: "Ok".into(), + arguments: vec![( + field_var, + Loc::at_zero(Expr::Var(ok_val_symbol, field_var)), + )], + })), + }, + ); + + let updated_record = Expr::RecordUpdate { + record_var: state_record_var, + ext_var: env.new_ext_var(ExtensionKind::Record), + symbol: state_arg_symbol, + updates, + }; + + Expr::Tag { + tag_union_var: when_expr_var, + ext_var: env.new_ext_var(ExtensionKind::TagUnion), + name: "Ok".into(), + arguments: vec![(state_record_var, Loc::at_zero(updated_record))], + } + }; + + let branches = vec![ + // Ok val -> Ok {state & first: Ok val}, + WhenBranch { + patterns: vec![WhenBranchPattern { + pattern: Loc::at_zero(Pattern::AppliedTag { + whole_var: rec_dot_result, + ext_var: Variable::EMPTY_TAG_UNION, + tag_name: "Ok".into(), + arguments: vec![( + field_var, + Loc::at_zero(Pattern::Identifier(ok_val_symbol)), + )], + }), + degenerate: false, + }], + value: Loc::at_zero(ok_branch_expr), + guard: None, + redundant: RedundantMark::known_non_redundant(), + }, + // Err err -> Err err + WhenBranch { + patterns: vec![WhenBranchPattern { + pattern: Loc::at_zero(Pattern::AppliedTag { + whole_var: rec_dot_result, + ext_var: Variable::EMPTY_TAG_UNION, + tag_name: "Err".into(), + arguments: vec![( + decode_err_var, + Loc::at_zero(Pattern::Identifier(err_val_symbol)), + )], + }), + degenerate: false, + }], + value: Loc::at_zero(Expr::Tag { + tag_union_var: when_expr_var, + ext_var: env.new_ext_var(ExtensionKind::TagUnion), + name: "Err".into(), + arguments: vec![( + decode_err_var, + Loc::at_zero(Expr::Var(err_val_symbol, decode_err_var)), + )], + }), + guard: None, + redundant: RedundantMark::known_non_redundant(), + }, + ]; + + // when rec.result is + // Ok val -> Ok {state & first: Ok val}, + // Err err -> Err err + Expr::When { + loc_cond: Box::new(Loc::at_zero(Expr::RecordAccess { + record_var: rec_var, + ext_var: env.new_ext_var(ExtensionKind::Record), + field_var: rec_dot_result, + loc_expr: Box::new(Loc::at_zero(Expr::Var(rec_symbol, rec_var))), + field: "result".into(), + })), + cond_var: rec_dot_result, + expr_var: when_expr_var, + region: Region::zero(), + branches, + branches_cond_var: rec_dot_result, + exhaustive: ExhaustiveMark::known_exhaustive(), + } + }; + + // { + // rest: rec.rest, + // result: when rec.result is + // Ok val -> Ok {state & first: Ok val}, + // Err err -> Err err + // } + let mut fields_map = SendMap::default(); + + fields_map.insert( + "rest".into(), + Field { + var: Variable::LIST_U8, + region: Region::zero(), + loc_expr: Box::new(Loc::at_zero(Expr::RecordAccess { + record_var: rec_var, + ext_var: env.new_ext_var(ExtensionKind::Record), + field_var: Variable::LIST_U8, + loc_expr: Box::new(Loc::at_zero(Expr::Var(rec_symbol, rec_var))), + field: "rest".into(), + })), + }, + ); + + // result: when rec.result is + // Ok val -> Ok {state & first: Ok val}, + // Err err -> Err err + fields_map.insert( + "result".into(), + Field { + var: when_expr_var, + region: Region::zero(), + loc_expr: Box::new(Loc::at_zero(result_val)), + }, + ); + + Expr::Record { + record_var: custom_callback_ret_var, + fields: fields_map, + } + }; + + let branch = WhenBranch { + patterns: vec![WhenBranchPattern { + pattern: Loc::at_zero(Pattern::Identifier(rec_symbol)), + degenerate: false, + }], + value: Loc::at_zero(branch_body), + guard: None, + redundant: RedundantMark::known_non_redundant(), + }; + + let condition_expr = Expr::Call( + Box::new(( + this_decode_with_var, + Loc::at_zero(Expr::Var(Symbol::DECODE_DECODE_WITH, this_decode_with_var)), + lambda_set_var, + rec_var, + )), + vec![ + ( + Variable::LIST_U8, + Loc::at_zero(Expr::Var(bytes_arg_symbol, Variable::LIST_U8)), + ), + ( + decoder_var, + Loc::at_zero(Expr::AbilityMember( + Symbol::DECODE_DECODER, + None, + decoder_var, + )), + ), + ( + fmt_arg_var, + Loc::at_zero(Expr::Var(fmt_arg_symbol, fmt_arg_var)), + ), + ], + CalledVia::Space, + ); + + // when Decode.decodeWith bytes Decode.decoder fmt is + Expr::When { + loc_cond: Box::new(Loc::at_zero(condition_expr)), + cond_var: rec_var, + expr_var: custom_callback_ret_var, + region: Region::zero(), + branches: vec![branch], + branches_cond_var: rec_var, + exhaustive: ExhaustiveMark::known_exhaustive(), + } + }; + + let custom_closure_symbol = env.new_symbol("customCallback"); + this_custom_callback_var = env.subs.fresh_unnamed_flex_var(); + let custom_callback_lambda_set_var = { + let content = Content::LambdaSet(LambdaSet { + solved: UnionLambdas::insert_into_subs( + env.subs, + [(custom_closure_symbol, [state_record_var])], + ), + recursion_var: OptVariable::NONE, + unspecialized: Default::default(), + ambient_function: this_custom_callback_var, + }); + let custom_callback_lambda_set_var = synth_var(env.subs, content); + let subs_slice = + SubsSlice::insert_into_subs(env.subs, [bytes_arg_var, fmt_arg_var]); + + env.subs.set_content( + this_custom_callback_var, + Content::Structure(FlatType::Func( + subs_slice, + custom_callback_lambda_set_var, + custom_callback_ret_var, + )), + ); + + custom_callback_lambda_set_var + }; + + // \bytes, fmt -> … + Expr::Closure(ClosureData { + function_type: this_custom_callback_var, + closure_type: custom_callback_lambda_set_var, + return_type: custom_callback_ret_var, + name: custom_closure_symbol, + captured_symbols: vec![(state_arg_symbol, state_record_var)], + recursive: Recursive::NotRecursive, + arguments: vec![ + ( + bytes_arg_var, + AnnotatedMark::known_exhaustive(), + Loc::at_zero(Pattern::Identifier(bytes_arg_symbol)), + ), + ( + fmt_arg_var, + AnnotatedMark::known_exhaustive(), + Loc::at_zero(Pattern::Identifier(fmt_arg_symbol)), + ), + ], + loc_body: Box::new(Loc::at_zero(custom_callback_body)), + }) + }; + + let decode_custom_ret_var = env.subs.fresh_unnamed_flex_var(); + let decode_custom = { + let decode_custom_var = env.import_builtin_symbol_var(Symbol::DECODE_CUSTOM); + let decode_custom_closure_var = env.subs.fresh_unnamed_flex_var(); + let this_decode_custom_var = { + let subs_slice = SubsSlice::insert_into_subs(env.subs, [this_custom_callback_var]); + let flat_type = + FlatType::Func(subs_slice, decode_custom_closure_var, decode_custom_ret_var); + + synth_var(env.subs, Content::Structure(flat_type)) + }; + + env.unify(decode_custom_var, this_decode_custom_var); + + // Decode.custom \bytes, fmt -> … + Expr::Call( + Box::new(( + this_decode_custom_var, + Loc::at_zero(Expr::Var(Symbol::DECODE_CUSTOM, this_decode_custom_var)), + decode_custom_closure_var, + decode_custom_ret_var, + )), + vec![(this_custom_callback_var, Loc::at_zero(custom_callback))], + CalledVia::Space, + ) + }; + + env.unify(keep_payload_var, decode_custom_ret_var); + + let keep = { + // Keep (Decode.custom \bytes, fmt -> + // when Decode.decodeWith bytes Decode.decoder fmt is + // rec -> + // { + // rest: rec.rest, + // result: when rec.result is + // Ok val -> Ok {state & first: Ok val}, + // Err err -> Err err + // } + // ) + Expr::Tag { + tag_union_var: keep_or_skip_var, + ext_var: env.new_ext_var(ExtensionKind::TagUnion), + name: "Keep".into(), + arguments: vec![(decode_custom_ret_var, Loc::at_zero(decode_custom))], + } + }; + + let branch = { + // "first" -> + // Keep (Decode.custom \bytes, fmt -> + // when Decode.decodeWith bytes Decode.decoder fmt is + // rec -> + // { + // rest: rec.rest, + // result: when rec.result is + // Ok val -> Ok {state & first: Ok val}, + // Err err -> Err err + // } + // ) + WhenBranch { + patterns: vec![WhenBranchPattern { + pattern: Loc::at_zero(Pattern::StrLiteral(field_name.into())), + degenerate: false, + }], + value: Loc::at_zero(keep), + guard: None, + redundant: RedundantMark::known_non_redundant(), + } + }; + + branches.push(branch); + } + + // Example: `_ -> Skip` + let default_branch = WhenBranch { + patterns: vec![WhenBranchPattern { + pattern: Loc::at_zero(Pattern::Underscore), + degenerate: false, + }], + value: Loc::at_zero(Expr::Tag { + tag_union_var: keep_or_skip_var, + ext_var: env.new_ext_var(ExtensionKind::TagUnion), + name: "Skip".into(), + arguments: Vec::new(), + }), + guard: None, + redundant: RedundantMark::known_non_redundant(), + }; + + branches.push(default_branch); + + // when field is + let body = Expr::When { + loc_cond: Box::new(Loc::at_zero(Expr::Var(field_arg_symbol, Variable::STR))), + cond_var: Variable::STR, + expr_var: keep_or_skip_var, + region: Region::zero(), + branches, + branches_cond_var: Variable::STR, + exhaustive: ExhaustiveMark::known_exhaustive(), + }; + + let step_field_closure = env.new_symbol("stepField"); + let function_type = env.subs.fresh_unnamed_flex_var(); + let closure_type = { + let lambda_set = LambdaSet { + solved: UnionLambdas::tag_without_arguments(env.subs, step_field_closure), + recursion_var: OptVariable::NONE, + unspecialized: Default::default(), + ambient_function: function_type, + }; + + synth_var(env.subs, Content::LambdaSet(lambda_set)) + }; + + { + let args_slice = SubsSlice::insert_into_subs(env.subs, [state_record_var, Variable::STR]); + + env.subs.set_content( + function_type, + Content::Structure(FlatType::Func(args_slice, closure_type, keep_or_skip_var)), + ) + }; + + let expr = Expr::Closure(ClosureData { + function_type, + closure_type, + return_type: keep_or_skip_var, + name: step_field_closure, + captured_symbols: Vec::new(), + recursive: Recursive::NotRecursive, + arguments: vec![ + ( + state_record_var, + AnnotatedMark::known_exhaustive(), + Loc::at_zero(Pattern::Identifier(state_arg_symbol)), + ), + ( + Variable::STR, + AnnotatedMark::known_exhaustive(), + Loc::at_zero(Pattern::Identifier(field_arg_symbol)), + ), + ], + loc_body: Box::new(Loc::at_zero(body)), + }); + + (expr, function_type) +} + +// Example: +// finalizer = \rec -> +// when rec.first is +// Ok first -> +// when rec.second is +// Ok second -> Ok {first, second} +// Err NoField -> Err TooShort +// Err NoField -> Err TooShort +fn finalizer( + env: &mut Env, + state_record_var: Variable, + fields: &[Lowercase], + field_vars: &[Variable], + result_field_vars: &[Variable], +) -> (Expr, Variable, Variable) { + let state_arg_symbol = env.new_symbol("stateRecord"); + let mut fields_map = SendMap::default(); + let mut pattern_symbols = Vec::with_capacity(fields.len()); + let decode_err_var = { + let flat_type = FlatType::TagUnion( + UnionTags::tag_without_arguments(env.subs, "TooShort".into()), + TagExt::Any(Variable::EMPTY_TAG_UNION), + ); + + synth_var(env.subs, Content::Structure(flat_type)) + }; + + for (field_name, &field_var) in fields.iter().zip(field_vars.iter()) { + let symbol = env.new_symbol(field_name.as_str()); + + pattern_symbols.push(symbol); + + let field_expr = Expr::Var(symbol, field_var); + let field = Field { + var: field_var, + region: Region::zero(), + loc_expr: Box::new(Loc::at_zero(field_expr)), + }; + + fields_map.insert(field_name.clone(), field); + } + + // The bottom of the happy path - return the decoded record {first: a, second: b} wrapped with + // "Ok". + let return_type_var; + let mut body = { + let subs = &mut env.subs; + let record_field_iter = fields + .iter() + .zip(field_vars.iter()) + .map(|(field_name, &field_var)| (field_name.clone(), RecordField::Required(field_var))); + let flat_type = FlatType::Record( + RecordFields::insert_into_subs(subs, record_field_iter), + Variable::EMPTY_RECORD, + ); + let done_record_var = synth_var(subs, Content::Structure(flat_type)); + let done_record = Expr::Record { + record_var: done_record_var, + fields: fields_map, + }; + + return_type_var = { + let flat_type = FlatType::TagUnion( + UnionTags::for_result(subs, done_record_var, decode_err_var), + TagExt::Any(Variable::EMPTY_TAG_UNION), + ); + + synth_var(subs, Content::Structure(flat_type)) + }; + + Expr::Tag { + tag_union_var: return_type_var, + ext_var: env.new_ext_var(ExtensionKind::TagUnion), + name: "Ok".into(), + arguments: vec![(done_record_var, Loc::at_zero(done_record))], + } + }; + + // Unwrap each result in the decoded state + // + // when rec.first is + // Ok first -> ...happy path... + // Err NoField -> Err TooShort + for (((symbol, field_name), &field_var), &result_field_var) in pattern_symbols + .iter() + .rev() + .zip(fields.iter().rev()) + .zip(field_vars.iter().rev()) + .zip(result_field_vars.iter().rev()) + { + // when rec.first is + let cond_expr = Expr::RecordAccess { + record_var: state_record_var, + ext_var: env.new_ext_var(ExtensionKind::Record), + field_var: result_field_var, + loc_expr: Box::new(Loc::at_zero(Expr::Var(state_arg_symbol, state_record_var))), + field: field_name.clone(), + }; + + // Example: `Ok x -> expr` + let ok_branch = WhenBranch { + patterns: vec![WhenBranchPattern { + pattern: Loc::at_zero(Pattern::AppliedTag { + whole_var: result_field_var, + ext_var: Variable::EMPTY_TAG_UNION, + tag_name: "Ok".into(), + arguments: vec![(field_var, Loc::at_zero(Pattern::Identifier(*symbol)))], + }), + degenerate: false, + }], + value: Loc::at_zero(body), + guard: None, + redundant: RedundantMark::known_non_redundant(), + }; + + // Example: `_ -> Err TooShort` + let err_branch = WhenBranch { + patterns: vec![WhenBranchPattern { + pattern: Loc::at_zero(Pattern::Underscore), + degenerate: false, + }], + value: Loc::at_zero(Expr::Tag { + tag_union_var: return_type_var, + ext_var: env.new_ext_var(ExtensionKind::TagUnion), + name: "Err".into(), + arguments: vec![( + decode_err_var, + Loc::at_zero(Expr::Tag { + tag_union_var: decode_err_var, + ext_var: Variable::EMPTY_TAG_UNION, + name: "TooShort".into(), + arguments: Vec::new(), + }), + )], + }), + guard: None, + redundant: RedundantMark::known_non_redundant(), + }; + + body = Expr::When { + loc_cond: Box::new(Loc::at_zero(cond_expr)), + cond_var: result_field_var, + expr_var: return_type_var, + region: Region::zero(), + branches: vec![ok_branch, err_branch], + branches_cond_var: result_field_var, + exhaustive: ExhaustiveMark::known_exhaustive(), + }; + } + + let function_var = synth_var(env.subs, Content::Error); // We'll fix this up in subs later. + let function_symbol = env.new_symbol("finalizer"); + let lambda_set = LambdaSet { + solved: UnionLambdas::tag_without_arguments(env.subs, function_symbol), + recursion_var: OptVariable::NONE, + unspecialized: Default::default(), + ambient_function: function_var, + }; + let closure_type = synth_var(env.subs, Content::LambdaSet(lambda_set)); + let flat_type = FlatType::Func( + SubsSlice::insert_into_subs(env.subs, [state_record_var]), + closure_type, + return_type_var, + ); + + // Fix up function_var so it's not Content::Error anymore + env.subs + .set_content(function_var, Content::Structure(flat_type)); + + let finalizer = Expr::Closure(ClosureData { + function_type: function_var, + closure_type, + return_type: return_type_var, + name: function_symbol, + captured_symbols: Vec::new(), + recursive: Recursive::NotRecursive, + arguments: vec![( + state_record_var, + AnnotatedMark::known_exhaustive(), + Loc::at_zero(Pattern::Identifier(state_arg_symbol)), + )], + loc_body: Box::new(Loc::at_zero(body)), + }); + + (finalizer, function_var, decode_err_var) +} + +// Example: +// initialState : {first: Result a [NoField], second: Result b [NoField]} +// initialState = {first: Err NoField, second: Err NoField} +fn initial_state( + env: &mut Env<'_>, + field_names: &[Lowercase], + field_vars: &mut Vec, + result_field_vars: &mut Vec, +) -> (Variable, Expr) { + let mut initial_state_fields = SendMap::default(); + + for field_name in field_names { + let subs = &mut env.subs; + let field_var = subs.fresh_unnamed_flex_var(); + + field_vars.push(field_var); + + let no_field_label = "NoField"; + let union_tags = UnionTags::tag_without_arguments(subs, no_field_label.into()); + let no_field_var = synth_var( + subs, + Content::Structure(FlatType::TagUnion( + union_tags, + TagExt::Any(Variable::EMPTY_TAG_UNION), + )), + ); + let no_field = Expr::Tag { + tag_union_var: no_field_var, + ext_var: Variable::EMPTY_TAG_UNION, + name: no_field_label.into(), + arguments: Vec::new(), + }; + let err_label = "Err"; + let union_tags = UnionTags::for_result(subs, field_var, no_field_var); + let result_var = synth_var( + subs, + Content::Structure(FlatType::TagUnion( + union_tags, + TagExt::Any(Variable::EMPTY_TAG_UNION), + )), + ); + let field_expr = Expr::Tag { + tag_union_var: result_var, + ext_var: env.new_ext_var(ExtensionKind::TagUnion), + name: err_label.into(), + arguments: vec![(no_field_var, Loc::at_zero(no_field))], + }; + result_field_vars.push(result_var); + let field = Field { + var: result_var, + region: Region::zero(), + loc_expr: Box::new(Loc::at_zero(field_expr)), + }; + + initial_state_fields.insert(field_name.clone(), field); + } + + let subs = &mut env.subs; + let record_field_iter = field_names + .iter() + .zip(result_field_vars.iter()) + .map(|(field_name, &var)| (field_name.clone(), RecordField::Required(var))); + let flat_type = FlatType::Record( + RecordFields::insert_into_subs(subs, record_field_iter), + Variable::EMPTY_RECORD, + ); + + let state_record_var = synth_var(subs, Content::Structure(flat_type)); + + ( + state_record_var, + Expr::Record { + record_var: state_record_var, + fields: initial_state_fields, + }, + ) +} diff --git a/crates/compiler/derive/src/decoding/tuple.rs b/crates/compiler/derive/src/decoding/tuple.rs new file mode 100644 index 0000000000..a327bdfded --- /dev/null +++ b/crates/compiler/derive/src/decoding/tuple.rs @@ -0,0 +1,994 @@ +use roc_can::expr::{ + AnnotatedMark, ClosureData, Expr, Field, IntValue, Recursive, WhenBranch, WhenBranchPattern, +}; +use roc_can::num::{IntBound, IntLitWidth}; +use roc_can::pattern::Pattern; +use roc_collections::SendMap; +use roc_module::called_via::CalledVia; +use roc_module::ident::Lowercase; +use roc_module::symbol::Symbol; +use roc_region::all::{Loc, Region}; +use roc_types::subs::{ + Content, ExhaustiveMark, FlatType, LambdaSet, OptVariable, RecordFields, RedundantMark, + SubsSlice, TagExt, TupleElems, UnionLambdas, UnionTags, Variable, +}; +use roc_types::types::RecordField; + +use crate::synth_var; +use crate::util::{Env, ExtensionKind}; + +use super::wrap_in_decode_custom_decode_with; + +/// Implements decoding of a tuple. For example, for +/// +/// ```text +/// (a, b) +/// ``` +/// +/// we'd like to generate an impl like +/// +/// ```roc +/// decoder : Decoder (a, b) fmt where a implements Decoding, b implements Decoding, fmt implements DecoderFormatting +/// decoder = +/// initialState : {e0: Result a [NoElem], e1: Result b [NoElem]} +/// initialState = {e0: Err NoElem, e1: Err NoElem} +/// +/// stepElem = \state, index -> +/// when index is +/// 0 -> +/// Next (Decode.custom \bytes, fmt -> +/// when Decode.decodeWith bytes Decode.decoder fmt is +/// {result, rest} -> +/// {result: Result.map result \val -> {state & e0: Ok val}, rest}) +/// 1 -> +/// Next (Decode.custom \bytes, fmt -> +/// when Decode.decodeWith bytes Decode.decoder fmt is +/// {result, rest} -> +/// {result: Result.map result \val -> {state & e1: Ok val}, rest}) +/// _ -> TooLong +/// +/// finalizer = \st -> +/// when st.e0 is +/// Ok e0 -> +/// when st.e1 is +/// Ok e1 -> Ok (e0, e1) +/// Err NoElem -> Err TooShort +/// Err NoElem -> Err TooShort +/// +/// Decode.custom \bytes, fmt -> Decode.decodeWith bytes (Decode.tuple initialState stepElem finalizer) fmt +/// ``` +pub(crate) fn decoder(env: &mut Env, _def_symbol: Symbol, arity: u32) -> (Expr, Variable) { + // The decoded type of each index in the tuple, e.g. (a, b). + let mut index_vars = Vec::with_capacity(arity as _); + // The type of each index in the decoding state, e.g. {e0: Result a [NoElem], e1: Result b [NoElem]} + let mut state_fields = Vec::with_capacity(arity as _); + let mut state_field_vars = Vec::with_capacity(arity as _); + + // initialState = ... + let (state_var, initial_state) = initial_state( + env, + arity, + &mut index_vars, + &mut state_fields, + &mut state_field_vars, + ); + + // finalizer = ... + let (finalizer, finalizer_var, decode_err_var) = finalizer( + env, + &index_vars, + state_var, + &state_fields, + &state_field_vars, + ); + + // stepElem = ... + let (step_elem, step_var) = step_elem( + env, + &index_vars, + state_var, + &state_fields, + &state_field_vars, + decode_err_var, + ); + + // Build up the type of `Decode.tuple` we expect + let tuple_decoder_var = env.subs.fresh_unnamed_flex_var(); + let decode_record_lambda_set = env.subs.fresh_unnamed_flex_var(); + let decode_record_var = env.import_builtin_symbol_var(Symbol::DECODE_TUPLE); + let this_decode_record_var = { + let flat_type = FlatType::Func( + SubsSlice::insert_into_subs(env.subs, [state_var, step_var, finalizer_var]), + decode_record_lambda_set, + tuple_decoder_var, + ); + + synth_var(env.subs, Content::Structure(flat_type)) + }; + + env.unify(decode_record_var, this_decode_record_var); + + // Decode.tuple initialState stepElem finalizer + let call_decode_record = Expr::Call( + Box::new(( + this_decode_record_var, + Loc::at_zero(Expr::AbilityMember( + Symbol::DECODE_TUPLE, + None, + this_decode_record_var, + )), + decode_record_lambda_set, + tuple_decoder_var, + )), + vec![ + (state_var, Loc::at_zero(initial_state)), + (step_var, Loc::at_zero(step_elem)), + (finalizer_var, Loc::at_zero(finalizer)), + ], + CalledVia::Space, + ); + + let (call_decode_custom, decode_custom_ret_var) = { + let bytes_sym = env.new_symbol("bytes"); + let fmt_sym = env.new_symbol("fmt"); + let fmt_var = env.subs.fresh_unnamed_flex_var(); + + let (decode_custom, decode_custom_var) = wrap_in_decode_custom_decode_with( + env, + bytes_sym, + (fmt_sym, fmt_var), + vec![], + (call_decode_record, tuple_decoder_var), + ); + + (decode_custom, decode_custom_var) + }; + + (call_decode_custom, decode_custom_ret_var) +} + +// Example: +// stepElem = \state, index -> +// when index is +// 0 -> +// Next (Decode.custom \bytes, fmt -> +// # Uses a single-branch `when` because `let` is more expensive to monomorphize +// # due to checks for polymorphic expressions, and `rec` would be polymorphic. +// when Decode.decodeWith bytes Decode.decoder fmt is +// rec -> +// { +// rest: rec.rest, +// result: when rec.result is +// Ok val -> Ok {state & e0: Ok val}, +// Err err -> Err err +// }) +// +// "e1" -> +// Next (Decode.custom \bytes, fmt -> +// when Decode.decodeWith bytes Decode.decoder fmt is +// rec -> +// { +// rest: rec.rest, +// result: when rec.result is +// Ok val -> Ok {state & e1: Ok val}, +// Err err -> Err err +// }) +// +// _ -> TooLong +fn step_elem( + env: &mut Env, + index_vars: &[Variable], + state_record_var: Variable, + state_fields: &[Lowercase], + state_field_vars: &[Variable], + decode_err_var: Variable, +) -> (Expr, Variable) { + let state_arg_symbol = env.new_symbol("stateRecord"); + let index_arg_symbol = env.new_symbol("index"); + + // +1 because of the default branch. + let mut branches = Vec::with_capacity(index_vars.len() + 1); + let keep_payload_var = env.subs.fresh_unnamed_flex_var(); + let keep_or_skip_var = { + let keep_payload_subs_slice = SubsSlice::insert_into_subs(env.subs, [keep_payload_var]); + let flat_type = FlatType::TagUnion( + UnionTags::insert_slices_into_subs( + env.subs, + [ + ("Next".into(), keep_payload_subs_slice), + ("TooLong".into(), Default::default()), + ], + ), + TagExt::Any(Variable::EMPTY_TAG_UNION), + ); + + synth_var(env.subs, Content::Structure(flat_type)) + }; + + for (((index, state_field), &index_var), &result_index_var) in state_fields + .iter() + .enumerate() + .zip(index_vars) + .zip(state_field_vars) + { + // Example: + // 0 -> + // Next (Decode.custom \bytes, fmt -> + // when Decode.decodeWith bytes Decode.decoder fmt is + // rec -> + // { + // rest: rec.rest, + // result: when rec.result is + // Ok val -> Ok {state & e0: Ok val}, + // Err err -> Err err + // } + // ) + + let this_custom_callback_var; + let custom_callback_ret_var; + let custom_callback = { + // \bytes, fmt -> + // when Decode.decodeWith bytes Decode.decoder fmt is + // rec -> + // { + // rest: rec.rest, + // result: when rec.result is + // Ok val -> Ok {state & e0: Ok val}, + // Err err -> Err err + // } + let bytes_arg_symbol = env.new_symbol("bytes"); + let fmt_arg_symbol = env.new_symbol("fmt"); + let bytes_arg_var = env.subs.fresh_unnamed_flex_var(); + let fmt_arg_var = env.subs.fresh_unnamed_flex_var(); + + // rec.result : [Ok index_var, Err DecodeError] + let rec_dot_result = { + let tag_union = FlatType::TagUnion( + UnionTags::for_result(env.subs, index_var, decode_err_var), + TagExt::Any(Variable::EMPTY_TAG_UNION), + ); + + synth_var(env.subs, Content::Structure(tag_union)) + }; + + // rec : { rest: List U8, result: (typeof rec.result) } + let rec_var = { + let indexs = RecordFields::insert_into_subs( + env.subs, + [ + ("rest".into(), RecordField::Required(Variable::LIST_U8)), + ("result".into(), RecordField::Required(rec_dot_result)), + ], + ); + let record = FlatType::Record(indexs, Variable::EMPTY_RECORD); + + synth_var(env.subs, Content::Structure(record)) + }; + + // `Decode.decoder` for the index's value + let decoder_var = env.import_builtin_symbol_var(Symbol::DECODE_DECODER); + let decode_with_var = env.import_builtin_symbol_var(Symbol::DECODE_DECODE_WITH); + let lambda_set_var = env.subs.fresh_unnamed_flex_var(); + let this_decode_with_var = { + let subs_slice = SubsSlice::insert_into_subs( + env.subs, + [bytes_arg_var, decoder_var, fmt_arg_var], + ); + let this_decode_with_var = synth_var( + env.subs, + Content::Structure(FlatType::Func(subs_slice, lambda_set_var, rec_var)), + ); + + env.unify(decode_with_var, this_decode_with_var); + + this_decode_with_var + }; + + // The result of decoding this index's value - either the updated state, or a decoding error. + let when_expr_var = { + let flat_type = FlatType::TagUnion( + UnionTags::for_result(env.subs, state_record_var, decode_err_var), + TagExt::Any(Variable::EMPTY_TAG_UNION), + ); + + synth_var(env.subs, Content::Structure(flat_type)) + }; + + // What our decoder passed to `Decode.custom` returns - the result of decoding the + // index's value, and the remaining bytes. + custom_callback_ret_var = { + let rest_index = RecordField::Required(Variable::LIST_U8); + let result_index = RecordField::Required(when_expr_var); + let flat_type = FlatType::Record( + RecordFields::insert_into_subs( + env.subs, + [("rest".into(), rest_index), ("result".into(), result_index)], + ), + Variable::EMPTY_RECORD, + ); + + synth_var(env.subs, Content::Structure(flat_type)) + }; + + let custom_callback_body = { + let rec_symbol = env.new_symbol("rec"); + + // # Uses a single-branch `when` because `let` is more expensive to monomorphize + // # due to checks for polymorphic expressions, and `rec` would be polymorphic. + // when Decode.decodeWith bytes Decode.decoder fmt is + // rec -> + // { + // rest: rec.rest, + // result: when rec.result is + // Ok val -> Ok {state & e0: Ok val}, + // Err err -> Err err + // } + let branch_body = { + let result_val = { + // result: when rec.result is + // Ok val -> Ok {state & e0: Ok val}, + // Err err -> Err err + let ok_val_symbol = env.new_symbol("val"); + let err_val_symbol = env.new_symbol("err"); + let ok_branch_expr = { + // Ok {state & e0: Ok val}, + let mut updates = SendMap::default(); + + updates.insert( + state_field.clone(), + Field { + var: result_index_var, + region: Region::zero(), + loc_expr: Box::new(Loc::at_zero(Expr::Tag { + tag_union_var: result_index_var, + ext_var: env.new_ext_var(ExtensionKind::TagUnion), + name: "Ok".into(), + arguments: vec![( + index_var, + Loc::at_zero(Expr::Var(ok_val_symbol, index_var)), + )], + })), + }, + ); + + let updated_record = Expr::RecordUpdate { + record_var: state_record_var, + ext_var: env.new_ext_var(ExtensionKind::Record), + symbol: state_arg_symbol, + updates, + }; + + Expr::Tag { + tag_union_var: when_expr_var, + ext_var: env.new_ext_var(ExtensionKind::TagUnion), + name: "Ok".into(), + arguments: vec![(state_record_var, Loc::at_zero(updated_record))], + } + }; + + let branches = vec![ + // Ok val -> Ok {state & e0: Ok val}, + WhenBranch { + patterns: vec![WhenBranchPattern { + pattern: Loc::at_zero(Pattern::AppliedTag { + whole_var: rec_dot_result, + ext_var: Variable::EMPTY_TAG_UNION, + tag_name: "Ok".into(), + arguments: vec![( + index_var, + Loc::at_zero(Pattern::Identifier(ok_val_symbol)), + )], + }), + degenerate: false, + }], + value: Loc::at_zero(ok_branch_expr), + guard: None, + redundant: RedundantMark::known_non_redundant(), + }, + // Err err -> Err err + WhenBranch { + patterns: vec![WhenBranchPattern { + pattern: Loc::at_zero(Pattern::AppliedTag { + whole_var: rec_dot_result, + ext_var: Variable::EMPTY_TAG_UNION, + tag_name: "Err".into(), + arguments: vec![( + decode_err_var, + Loc::at_zero(Pattern::Identifier(err_val_symbol)), + )], + }), + degenerate: false, + }], + value: Loc::at_zero(Expr::Tag { + tag_union_var: when_expr_var, + ext_var: env.new_ext_var(ExtensionKind::TagUnion), + name: "Err".into(), + arguments: vec![( + decode_err_var, + Loc::at_zero(Expr::Var(err_val_symbol, decode_err_var)), + )], + }), + guard: None, + redundant: RedundantMark::known_non_redundant(), + }, + ]; + + // when rec.result is + // Ok val -> Ok {state & e0: Ok val}, + // Err err -> Err err + Expr::When { + loc_cond: Box::new(Loc::at_zero(Expr::RecordAccess { + record_var: rec_var, + ext_var: env.new_ext_var(ExtensionKind::Record), + field_var: rec_dot_result, + loc_expr: Box::new(Loc::at_zero(Expr::Var(rec_symbol, rec_var))), + field: "result".into(), + })), + cond_var: rec_dot_result, + expr_var: when_expr_var, + region: Region::zero(), + branches, + branches_cond_var: rec_dot_result, + exhaustive: ExhaustiveMark::known_exhaustive(), + } + }; + + // { + // rest: rec.rest, + // result: when rec.result is + // Ok val -> Ok {state & e0: Ok val}, + // Err err -> Err err + // } + let mut fields_map = SendMap::default(); + + fields_map.insert( + "rest".into(), + Field { + var: Variable::LIST_U8, + region: Region::zero(), + loc_expr: Box::new(Loc::at_zero(Expr::RecordAccess { + record_var: rec_var, + ext_var: env.new_ext_var(ExtensionKind::Record), + field_var: Variable::LIST_U8, + loc_expr: Box::new(Loc::at_zero(Expr::Var(rec_symbol, rec_var))), + field: "rest".into(), + })), + }, + ); + + // result: when rec.result is + // Ok val -> Ok {state & e0: Ok val}, + // Err err -> Err err + fields_map.insert( + "result".into(), + Field { + var: when_expr_var, + region: Region::zero(), + loc_expr: Box::new(Loc::at_zero(result_val)), + }, + ); + + Expr::Record { + record_var: custom_callback_ret_var, + fields: fields_map, + } + }; + + let branch = WhenBranch { + patterns: vec![WhenBranchPattern { + pattern: Loc::at_zero(Pattern::Identifier(rec_symbol)), + degenerate: false, + }], + value: Loc::at_zero(branch_body), + guard: None, + redundant: RedundantMark::known_non_redundant(), + }; + + let condition_expr = Expr::Call( + Box::new(( + this_decode_with_var, + Loc::at_zero(Expr::Var(Symbol::DECODE_DECODE_WITH, this_decode_with_var)), + lambda_set_var, + rec_var, + )), + vec![ + ( + Variable::LIST_U8, + Loc::at_zero(Expr::Var(bytes_arg_symbol, Variable::LIST_U8)), + ), + ( + decoder_var, + Loc::at_zero(Expr::AbilityMember( + Symbol::DECODE_DECODER, + None, + decoder_var, + )), + ), + ( + fmt_arg_var, + Loc::at_zero(Expr::Var(fmt_arg_symbol, fmt_arg_var)), + ), + ], + CalledVia::Space, + ); + + // when Decode.decodeWith bytes Decode.decoder fmt is + Expr::When { + loc_cond: Box::new(Loc::at_zero(condition_expr)), + cond_var: rec_var, + expr_var: custom_callback_ret_var, + region: Region::zero(), + branches: vec![branch], + branches_cond_var: rec_var, + exhaustive: ExhaustiveMark::known_exhaustive(), + } + }; + + let custom_closure_symbol = env.new_symbol("customCallback"); + this_custom_callback_var = env.subs.fresh_unnamed_flex_var(); + let custom_callback_lambda_set_var = { + let content = Content::LambdaSet(LambdaSet { + solved: UnionLambdas::insert_into_subs( + env.subs, + [(custom_closure_symbol, [state_record_var])], + ), + recursion_var: OptVariable::NONE, + unspecialized: Default::default(), + ambient_function: this_custom_callback_var, + }); + let custom_callback_lambda_set_var = synth_var(env.subs, content); + let subs_slice = + SubsSlice::insert_into_subs(env.subs, [bytes_arg_var, fmt_arg_var]); + + env.subs.set_content( + this_custom_callback_var, + Content::Structure(FlatType::Func( + subs_slice, + custom_callback_lambda_set_var, + custom_callback_ret_var, + )), + ); + + custom_callback_lambda_set_var + }; + + // \bytes, fmt -> … + Expr::Closure(ClosureData { + function_type: this_custom_callback_var, + closure_type: custom_callback_lambda_set_var, + return_type: custom_callback_ret_var, + name: custom_closure_symbol, + captured_symbols: vec![(state_arg_symbol, state_record_var)], + recursive: Recursive::NotRecursive, + arguments: vec![ + ( + bytes_arg_var, + AnnotatedMark::known_exhaustive(), + Loc::at_zero(Pattern::Identifier(bytes_arg_symbol)), + ), + ( + fmt_arg_var, + AnnotatedMark::known_exhaustive(), + Loc::at_zero(Pattern::Identifier(fmt_arg_symbol)), + ), + ], + loc_body: Box::new(Loc::at_zero(custom_callback_body)), + }) + }; + + let decode_custom_ret_var = env.subs.fresh_unnamed_flex_var(); + let decode_custom = { + let decode_custom_var = env.import_builtin_symbol_var(Symbol::DECODE_CUSTOM); + let decode_custom_closure_var = env.subs.fresh_unnamed_flex_var(); + let this_decode_custom_var = { + let subs_slice = SubsSlice::insert_into_subs(env.subs, [this_custom_callback_var]); + let flat_type = + FlatType::Func(subs_slice, decode_custom_closure_var, decode_custom_ret_var); + + synth_var(env.subs, Content::Structure(flat_type)) + }; + + env.unify(decode_custom_var, this_decode_custom_var); + + // Decode.custom \bytes, fmt -> … + Expr::Call( + Box::new(( + this_decode_custom_var, + Loc::at_zero(Expr::Var(Symbol::DECODE_CUSTOM, this_decode_custom_var)), + decode_custom_closure_var, + decode_custom_ret_var, + )), + vec![(this_custom_callback_var, Loc::at_zero(custom_callback))], + CalledVia::Space, + ) + }; + + env.unify(keep_payload_var, decode_custom_ret_var); + + let keep = { + // Next (Decode.custom \bytes, fmt -> + // when Decode.decodeWith bytes Decode.decoder fmt is + // rec -> + // { + // rest: rec.rest, + // result: when rec.result is + // Ok val -> Ok {state & e0: Ok val}, + // Err err -> Err err + // } + // ) + Expr::Tag { + tag_union_var: keep_or_skip_var, + ext_var: env.new_ext_var(ExtensionKind::TagUnion), + name: "Next".into(), + arguments: vec![(decode_custom_ret_var, Loc::at_zero(decode_custom))], + } + }; + + let branch = { + // 0 -> + // Next (Decode.custom \bytes, fmt -> + // when Decode.decodeWith bytes Decode.decoder fmt is + // rec -> + // { + // rest: rec.rest, + // result: when rec.result is + // Ok val -> Ok {state & e0: Ok val}, + // Err err -> Err err + // } + // ) + WhenBranch { + patterns: vec![WhenBranchPattern { + pattern: Loc::at_zero(Pattern::IntLiteral( + Variable::NAT, + Variable::NATURAL, + index.to_string().into_boxed_str(), + IntValue::I128((index as i128).to_ne_bytes()), + IntBound::Exact(IntLitWidth::Nat), + )), + degenerate: false, + }], + value: Loc::at_zero(keep), + guard: None, + redundant: RedundantMark::known_non_redundant(), + } + }; + + branches.push(branch); + } + + // Example: `_ -> TooLong` + let default_branch = WhenBranch { + patterns: vec![WhenBranchPattern { + pattern: Loc::at_zero(Pattern::Underscore), + degenerate: false, + }], + value: Loc::at_zero(Expr::Tag { + tag_union_var: keep_or_skip_var, + ext_var: env.new_ext_var(ExtensionKind::TagUnion), + name: "TooLong".into(), + arguments: Vec::new(), + }), + guard: None, + redundant: RedundantMark::known_non_redundant(), + }; + + branches.push(default_branch); + + // when index is + let body = Expr::When { + loc_cond: Box::new(Loc::at_zero(Expr::Var(index_arg_symbol, Variable::NAT))), + cond_var: Variable::NAT, + expr_var: keep_or_skip_var, + region: Region::zero(), + branches, + branches_cond_var: Variable::NAT, + exhaustive: ExhaustiveMark::known_exhaustive(), + }; + + let step_elem_closure = env.new_symbol("stepElem"); + let function_type = env.subs.fresh_unnamed_flex_var(); + let closure_type = { + let lambda_set = LambdaSet { + solved: UnionLambdas::tag_without_arguments(env.subs, step_elem_closure), + recursion_var: OptVariable::NONE, + unspecialized: Default::default(), + ambient_function: function_type, + }; + + synth_var(env.subs, Content::LambdaSet(lambda_set)) + }; + + { + let args_slice = SubsSlice::insert_into_subs(env.subs, [state_record_var, Variable::NAT]); + + env.subs.set_content( + function_type, + Content::Structure(FlatType::Func(args_slice, closure_type, keep_or_skip_var)), + ) + }; + + let expr = Expr::Closure(ClosureData { + function_type, + closure_type, + return_type: keep_or_skip_var, + name: step_elem_closure, + captured_symbols: Vec::new(), + recursive: Recursive::NotRecursive, + arguments: vec![ + ( + state_record_var, + AnnotatedMark::known_exhaustive(), + Loc::at_zero(Pattern::Identifier(state_arg_symbol)), + ), + ( + Variable::NAT, + AnnotatedMark::known_exhaustive(), + Loc::at_zero(Pattern::Identifier(index_arg_symbol)), + ), + ], + loc_body: Box::new(Loc::at_zero(body)), + }); + + (expr, function_type) +} + +// Example: +// finalizer = \rec -> +// when rec.e0 is +// Ok e0 -> +// when rec.e1 is +// Ok e1 -> Ok (e0, e1) +// Err NoElem -> Err TooShort +// Err NoElem -> Err TooShort +fn finalizer( + env: &mut Env, + index_vars: &[Variable], + state_record_var: Variable, + state_fields: &[Lowercase], + state_field_vars: &[Variable], +) -> (Expr, Variable, Variable) { + let state_arg_symbol = env.new_symbol("stateRecord"); + let mut tuple_elems = Vec::with_capacity(index_vars.len()); + let mut pattern_symbols = Vec::with_capacity(index_vars.len()); + let decode_err_var = { + let flat_type = FlatType::TagUnion( + UnionTags::tag_without_arguments(env.subs, "TooShort".into()), + TagExt::Any(Variable::EMPTY_TAG_UNION), + ); + + synth_var(env.subs, Content::Structure(flat_type)) + }; + + for (i, &index_var) in index_vars.iter().enumerate() { + let symbol = env.new_symbol(i); + + pattern_symbols.push(symbol); + + let index_expr = Expr::Var(symbol, index_var); + + tuple_elems.push((index_var, Box::new(Loc::at_zero(index_expr)))); + } + + // The bottom of the happy path - return the decoded tuple (a, b) wrapped with + // "Ok". + let return_type_var; + let mut body = { + let subs = &mut env.subs; + let tuple_indices_iter = index_vars.iter().copied().enumerate(); + let flat_type = FlatType::Tuple( + TupleElems::insert_into_subs(subs, tuple_indices_iter), + Variable::EMPTY_TUPLE, + ); + let done_tuple_var = synth_var(subs, Content::Structure(flat_type)); + let done_record = Expr::Tuple { + tuple_var: done_tuple_var, + elems: tuple_elems, + }; + + return_type_var = { + let flat_type = FlatType::TagUnion( + UnionTags::for_result(subs, done_tuple_var, decode_err_var), + TagExt::Any(Variable::EMPTY_TAG_UNION), + ); + + synth_var(subs, Content::Structure(flat_type)) + }; + + Expr::Tag { + tag_union_var: return_type_var, + ext_var: env.new_ext_var(ExtensionKind::TagUnion), + name: "Ok".into(), + arguments: vec![(done_tuple_var, Loc::at_zero(done_record))], + } + }; + + // Unwrap each result in the decoded state + // + // when rec.e0 is + // Ok e0 -> ...happy path... + // Err NoElem -> Err TooShort + for (((symbol, field), &index_var), &result_index_var) in pattern_symbols + .iter() + .zip(state_fields) + .zip(index_vars) + .zip(state_field_vars) + .rev() + { + // when rec.e0 is + let cond_expr = Expr::RecordAccess { + record_var: state_record_var, + ext_var: env.new_ext_var(ExtensionKind::Record), + field_var: result_index_var, + loc_expr: Box::new(Loc::at_zero(Expr::Var(state_arg_symbol, state_record_var))), + field: field.clone(), + }; + + // Example: `Ok x -> expr` + let ok_branch = WhenBranch { + patterns: vec![WhenBranchPattern { + pattern: Loc::at_zero(Pattern::AppliedTag { + whole_var: result_index_var, + ext_var: Variable::EMPTY_TAG_UNION, + tag_name: "Ok".into(), + arguments: vec![(index_var, Loc::at_zero(Pattern::Identifier(*symbol)))], + }), + degenerate: false, + }], + value: Loc::at_zero(body), + guard: None, + redundant: RedundantMark::known_non_redundant(), + }; + + // Example: `_ -> Err TooShort` + let err_branch = WhenBranch { + patterns: vec![WhenBranchPattern { + pattern: Loc::at_zero(Pattern::Underscore), + degenerate: false, + }], + value: Loc::at_zero(Expr::Tag { + tag_union_var: return_type_var, + ext_var: env.new_ext_var(ExtensionKind::TagUnion), + name: "Err".into(), + arguments: vec![( + decode_err_var, + Loc::at_zero(Expr::Tag { + tag_union_var: decode_err_var, + ext_var: Variable::EMPTY_TAG_UNION, + name: "TooShort".into(), + arguments: Vec::new(), + }), + )], + }), + guard: None, + redundant: RedundantMark::known_non_redundant(), + }; + + body = Expr::When { + loc_cond: Box::new(Loc::at_zero(cond_expr)), + cond_var: result_index_var, + expr_var: return_type_var, + region: Region::zero(), + branches: vec![ok_branch, err_branch], + branches_cond_var: result_index_var, + exhaustive: ExhaustiveMark::known_exhaustive(), + }; + } + + let function_var = synth_var(env.subs, Content::Error); // We'll fix this up in subs later. + let function_symbol = env.new_symbol("finalizer"); + let lambda_set = LambdaSet { + solved: UnionLambdas::tag_without_arguments(env.subs, function_symbol), + recursion_var: OptVariable::NONE, + unspecialized: Default::default(), + ambient_function: function_var, + }; + let closure_type = synth_var(env.subs, Content::LambdaSet(lambda_set)); + let flat_type = FlatType::Func( + SubsSlice::insert_into_subs(env.subs, [state_record_var]), + closure_type, + return_type_var, + ); + + // Fix up function_var so it's not Content::Error anymore + env.subs + .set_content(function_var, Content::Structure(flat_type)); + + let finalizer = Expr::Closure(ClosureData { + function_type: function_var, + closure_type, + return_type: return_type_var, + name: function_symbol, + captured_symbols: Vec::new(), + recursive: Recursive::NotRecursive, + arguments: vec![( + state_record_var, + AnnotatedMark::known_exhaustive(), + Loc::at_zero(Pattern::Identifier(state_arg_symbol)), + )], + loc_body: Box::new(Loc::at_zero(body)), + }); + + (finalizer, function_var, decode_err_var) +} + +// Example: +// initialState : {e0: Result a [NoElem], e1: Result b [NoElem]} +// initialState = {e0: Err NoElem, e1: Err NoElem} +fn initial_state( + env: &mut Env<'_>, + arity: u32, + index_vars: &mut Vec, + state_fields: &mut Vec, + state_field_vars: &mut Vec, +) -> (Variable, Expr) { + let mut initial_state_fields = SendMap::default(); + + for i in 0..arity { + let subs = &mut env.subs; + let index_var = subs.fresh_unnamed_flex_var(); + + index_vars.push(index_var); + + let state_field = Lowercase::from(format!("e{i}")); + state_fields.push(state_field.clone()); + + let no_index_label = "NoElem"; + let union_tags = UnionTags::tag_without_arguments(subs, no_index_label.into()); + let no_index_var = synth_var( + subs, + Content::Structure(FlatType::TagUnion( + union_tags, + TagExt::Any(Variable::EMPTY_TAG_UNION), + )), + ); + let no_index = Expr::Tag { + tag_union_var: no_index_var, + ext_var: Variable::EMPTY_TAG_UNION, + name: no_index_label.into(), + arguments: Vec::new(), + }; + let err_label = "Err"; + let union_tags = UnionTags::for_result(subs, index_var, no_index_var); + let result_var = synth_var( + subs, + Content::Structure(FlatType::TagUnion( + union_tags, + TagExt::Any(Variable::EMPTY_TAG_UNION), + )), + ); + let index_expr = Expr::Tag { + tag_union_var: result_var, + ext_var: env.new_ext_var(ExtensionKind::TagUnion), + name: err_label.into(), + arguments: vec![(no_index_var, Loc::at_zero(no_index))], + }; + state_field_vars.push(result_var); + let index = Field { + var: result_var, + region: Region::zero(), + loc_expr: Box::new(Loc::at_zero(index_expr)), + }; + + initial_state_fields.insert(state_field, index); + } + + let subs = &mut env.subs; + let record_index_iter = state_fields + .iter() + .zip(state_field_vars.iter()) + .map(|(index_name, &var)| (index_name.clone(), RecordField::Required(var))); + let flat_type = FlatType::Record( + RecordFields::insert_into_subs(subs, record_index_iter), + Variable::EMPTY_RECORD, + ); + + let state_record_var = synth_var(subs, Content::Structure(flat_type)); + + ( + state_record_var, + Expr::Record { + record_var: state_record_var, + fields: initial_state_fields, + }, + ) +} diff --git a/crates/compiler/derive/src/encoding.rs b/crates/compiler/derive/src/encoding.rs new file mode 100644 index 0000000000..ccade76936 --- /dev/null +++ b/crates/compiler/derive/src/encoding.rs @@ -0,0 +1,1089 @@ +//! Derivers for the `Encoding` ability. + +use std::iter::once; + +use roc_can::expr::{ + AnnotatedMark, ClosureData, Expr, Field, Recursive, WhenBranch, WhenBranchPattern, +}; +use roc_can::pattern::Pattern; +use roc_collections::SendMap; +use roc_derive_key::encoding::FlatEncodableKey; +use roc_module::called_via::CalledVia; +use roc_module::ident::Lowercase; +use roc_module::symbol::Symbol; +use roc_region::all::{Loc, Region}; +use roc_types::subs::{ + Content, ExhaustiveMark, FlatType, GetSubsSlice, LambdaSet, OptVariable, RecordFields, + RedundantMark, SubsSlice, TagExt, TupleElems, UnionLambdas, UnionTags, Variable, + VariableSubsSlice, +}; +use roc_types::types::RecordField; + +use crate::util::Env; +use crate::{synth_var, DerivedBody}; + +pub(crate) fn derive_to_encoder( + env: &mut Env<'_>, + key: FlatEncodableKey, + def_symbol: Symbol, +) -> DerivedBody { + let (body, body_type) = match key { + FlatEncodableKey::List() => to_encoder_list(env, def_symbol), + FlatEncodableKey::Set() => todo!(), + FlatEncodableKey::Dict() => todo!(), + FlatEncodableKey::Record(fields) => { + // Generalized record var so we can reuse this impl between many records: + // if fields = { a, b }, this is { a: t1, b: t2 } for fresh t1, t2. + let flex_fields = fields + .into_iter() + .map(|name| { + ( + name, + RecordField::Required(env.subs.fresh_unnamed_flex_var()), + ) + }) + .collect::>(); + let fields = RecordFields::insert_into_subs(env.subs, flex_fields); + let record_var = synth_var( + env.subs, + Content::Structure(FlatType::Record(fields, Variable::EMPTY_RECORD)), + ); + + to_encoder_record(env, record_var, fields, def_symbol) + } + FlatEncodableKey::Tuple(arity) => { + // Generalized tuple var so we can reuse this impl between many tuples: + // if arity = n, this is (t1, ..., tn) for fresh t1, ..., tn. + let flex_elems = (0..arity) + .map(|idx| (idx as usize, env.subs.fresh_unnamed_flex_var())) + .collect::>(); + let elems = TupleElems::insert_into_subs(env.subs, flex_elems); + let tuple_var = synth_var( + env.subs, + Content::Structure(FlatType::Tuple(elems, Variable::EMPTY_TUPLE)), + ); + + to_encoder_tuple(env, tuple_var, elems, def_symbol) + } + FlatEncodableKey::TagUnion(tags) => { + // Generalized tag union var so we can reuse this impl between many unions: + // if tags = [ A arity=2, B arity=1 ], this is [ A t1 t2, B t3 ] for fresh t1, t2, t3 + let flex_tag_labels = tags + .into_iter() + .map(|(label, arity)| { + let variables_slice = + VariableSubsSlice::reserve_into_subs(env.subs, arity.into()); + for var_index in variables_slice { + env.subs[var_index] = env.subs.fresh_unnamed_flex_var(); + } + (label, variables_slice) + }) + .collect::>(); + let union_tags = UnionTags::insert_slices_into_subs(env.subs, flex_tag_labels); + let tag_union_var = synth_var( + env.subs, + Content::Structure(FlatType::TagUnion( + union_tags, + TagExt::Any(Variable::EMPTY_TAG_UNION), + )), + ); + + to_encoder_tag_union(env, tag_union_var, union_tags, def_symbol) + } + }; + + let specialization_lambda_sets = + env.get_specialization_lambda_sets(body_type, Symbol::ENCODE_TO_ENCODER); + + DerivedBody { + body, + body_type, + specialization_lambda_sets, + } +} + +fn to_encoder_list(env: &mut Env<'_>, fn_name: Symbol) -> (Expr, Variable) { + // Build \lst -> Encode.list lst (\elem -> Encode.toEncoder elem) + // + // TODO eta reduce this baby ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + use Expr::*; + + let lst_sym = env.new_symbol("lst"); + let elem_sym = env.new_symbol("elem"); + + // List elem + let elem_var = env.subs.fresh_unnamed_flex_var(); + let elem_var_slice = SubsSlice::insert_into_subs(env.subs, [elem_var]); + let list_var = synth_var( + env.subs, + Content::Structure(FlatType::Apply(Symbol::LIST_LIST, elem_var_slice)), + ); + + // build `toEncoder elem` type + // val -[uls]-> Encoder fmt where fmt implements EncoderFormatting + let to_encoder_fn_var = env.import_builtin_symbol_var(Symbol::ENCODE_TO_ENCODER); + + // elem -[clos]-> t1 + let to_encoder_clos_var = env.subs.fresh_unnamed_flex_var(); // clos + let elem_encoder_var = env.subs.fresh_unnamed_flex_var(); // t1 + let elem_to_encoder_fn_var = synth_var( + env.subs, + Content::Structure(FlatType::Func( + elem_var_slice, + to_encoder_clos_var, + elem_encoder_var, + )), + ); + + // val -[uls]-> Encoder fmt where fmt implements EncoderFormatting + // ~ elem -[clos]-> t1 + env.unify(to_encoder_fn_var, elem_to_encoder_fn_var); + + // toEncoder : (typeof rcd.a) -[clos]-> Encoder fmt where fmt implements EncoderFormatting + let to_encoder_var = AbilityMember(Symbol::ENCODE_TO_ENCODER, None, elem_to_encoder_fn_var); + let to_encoder_fn = Box::new(( + to_encoder_fn_var, + Loc::at_zero(to_encoder_var), + to_encoder_clos_var, + elem_encoder_var, + )); + + // toEncoder elem + let to_encoder_call = Call( + to_encoder_fn, + vec![(elem_var, Loc::at_zero(Var(elem_sym, elem_var)))], + CalledVia::Space, + ); + + // elem -[to_elem_encoder]-> toEncoder elem + let to_elem_encoder_sym = env.new_symbol("to_elem_encoder"); + + // Create fn_var for ambient capture; we fix it up below. + let to_elem_encoder_fn_var = synth_var(env.subs, Content::Error); + + // -[to_elem_encoder]-> + let to_elem_encoder_labels = + UnionLambdas::insert_into_subs(env.subs, once((to_elem_encoder_sym, vec![]))); + let to_elem_encoder_lset = synth_var( + env.subs, + Content::LambdaSet(LambdaSet { + solved: to_elem_encoder_labels, + recursion_var: OptVariable::NONE, + unspecialized: SubsSlice::default(), + ambient_function: to_elem_encoder_fn_var, + }), + ); + // elem -[to_elem_encoder]-> toEncoder elem + env.subs.set_content( + to_elem_encoder_fn_var, + Content::Structure(FlatType::Func( + elem_var_slice, + to_elem_encoder_lset, + elem_encoder_var, + )), + ); + + // \elem -> toEncoder elem + let to_elem_encoder = Closure(ClosureData { + function_type: to_elem_encoder_fn_var, + closure_type: to_elem_encoder_lset, + return_type: elem_encoder_var, + name: to_elem_encoder_sym, + captured_symbols: vec![], + recursive: Recursive::NotRecursive, + arguments: vec![( + elem_var, + AnnotatedMark::known_exhaustive(), + Loc::at_zero(Pattern::Identifier(elem_sym)), + )], + loc_body: Box::new(Loc::at_zero(to_encoder_call)), + }); + + // build `Encode.list lst (\elem -> Encode.toEncoder elem)` type + // List e, (e -> Encoder fmt) -[uls]-> Encoder fmt where fmt implements EncoderFormatting + let encode_list_fn_var = env.import_builtin_symbol_var(Symbol::ENCODE_LIST); + + // List elem, to_elem_encoder_fn_var -[clos]-> t1 + let this_encode_list_args_slice = + VariableSubsSlice::insert_into_subs(env.subs, [list_var, to_elem_encoder_fn_var]); + let this_encode_list_clos_var = env.subs.fresh_unnamed_flex_var(); // clos + let this_list_encoder_var = env.subs.fresh_unnamed_flex_var(); // t1 + let this_encode_list_fn_var = synth_var( + env.subs, + Content::Structure(FlatType::Func( + this_encode_list_args_slice, + this_encode_list_clos_var, + this_list_encoder_var, + )), + ); + + // List e, (e -> Encoder fmt) -[uls]-> Encoder fmt where fmt implements EncoderFormatting + // ~ List elem, to_elem_encoder_fn_var -[clos]-> t1 + env.unify(encode_list_fn_var, this_encode_list_fn_var); + + // Encode.list : List elem, to_elem_encoder_fn_var -[clos]-> Encoder fmt where fmt implements EncoderFormatting + let encode_list = AbilityMember(Symbol::ENCODE_LIST, None, this_encode_list_fn_var); + let encode_list_fn = Box::new(( + this_encode_list_fn_var, + Loc::at_zero(encode_list), + this_encode_list_clos_var, + this_list_encoder_var, + )); + + // Encode.list lst to_elem_encoder + let encode_list_call = Call( + encode_list_fn, + vec![ + (list_var, Loc::at_zero(Var(lst_sym, list_var))), + (to_elem_encoder_fn_var, Loc::at_zero(to_elem_encoder)), + ], + CalledVia::Space, + ); + + // Encode.custom \bytes, fmt -> Encode.appendWith bytes (Encode.list ..) fmt + let (body, this_encoder_var) = wrap_in_encode_custom( + env, + encode_list_call, + this_list_encoder_var, + lst_sym, + list_var, + ); + + // \lst -> Encode.list lst (\elem -> Encode.toEncoder elem) + // Create fn_var for ambient capture; we fix it up below. + let fn_var = synth_var(env.subs, Content::Error); + + // -[fn_name]-> + let fn_name_labels = UnionLambdas::insert_into_subs(env.subs, once((fn_name, vec![]))); + let fn_clos_var = synth_var( + env.subs, + Content::LambdaSet(LambdaSet { + solved: fn_name_labels, + recursion_var: OptVariable::NONE, + unspecialized: SubsSlice::default(), + ambient_function: fn_var, + }), + ); + // List elem -[fn_name]-> Encoder fmt + let list_var_slice = SubsSlice::insert_into_subs(env.subs, once(list_var)); + env.subs.set_content( + fn_var, + Content::Structure(FlatType::Func( + list_var_slice, + fn_clos_var, + this_encoder_var, + )), + ); + + // \lst -[fn_name]-> Encode.list lst (\elem -> Encode.toEncoder elem) + let clos = Closure(ClosureData { + function_type: fn_var, + closure_type: fn_clos_var, + return_type: this_encoder_var, + name: fn_name, + captured_symbols: vec![], + recursive: Recursive::NotRecursive, + arguments: vec![( + list_var, + AnnotatedMark::known_exhaustive(), + Loc::at_zero(Pattern::Identifier(lst_sym)), + )], + loc_body: Box::new(Loc::at_zero(body)), + }); + + (clos, fn_var) +} + +fn to_encoder_record( + env: &mut Env<'_>, + record_var: Variable, + fields: RecordFields, + fn_name: Symbol, +) -> (Expr, Variable) { + // Suppose rcd = { a: t1, b: t2 }. Build + // + // \rcd -> Encode.record [ + // { key: "a", value: Encode.toEncoder rcd.a }, + // { key: "b", value: Encode.toEncoder rcd.b }, + // ] + + let rcd_sym = env.new_symbol("rcd"); + let whole_rcd_var = env.subs.fresh_unnamed_flex_var(); // type of the { key, value } records in the list + + use Expr::*; + + let fields_list = fields + .iter_all() + .map(|(field_name_index, field_var_index, _)| { + let field_name = env.subs[field_name_index].clone(); + let field_var = env.subs[field_var_index]; + let field_var_slice = VariableSubsSlice::new(field_var_index.index, 1); + + // key: "a" + let key_field = Field { + var: Variable::STR, + region: Region::zero(), + loc_expr: Box::new(Loc::at_zero(Str(field_name.as_str().into()))), + }; + + // rcd.a + let field_access = RecordAccess { + record_var, + ext_var: env.subs.fresh_unnamed_flex_var(), + field_var, + loc_expr: Box::new(Loc::at_zero(Var( + rcd_sym, + env.subs.fresh_unnamed_flex_var(), + ))), + field: field_name, + }; + + // build `toEncoder rcd.a` type + // val -[uls]-> Encoder fmt where fmt implements EncoderFormatting + let to_encoder_fn_var = env.import_builtin_symbol_var(Symbol::ENCODE_TO_ENCODER); + + // (typeof rcd.a) -[clos]-> t1 + let to_encoder_clos_var = env.subs.fresh_unnamed_flex_var(); // clos + let encoder_var = env.subs.fresh_unnamed_flex_var(); // t1 + let this_to_encoder_fn_var = synth_var( + env.subs, + Content::Structure(FlatType::Func( + field_var_slice, + to_encoder_clos_var, + encoder_var, + )), + ); + + // val -[uls]-> Encoder fmt where fmt implements EncoderFormatting + // ~ (typeof rcd.a) -[clos]-> t1 + env.unify(to_encoder_fn_var, this_to_encoder_fn_var); + + // toEncoder : (typeof rcd.a) -[clos]-> Encoder fmt where fmt implements EncoderFormatting + let to_encoder_var = AbilityMember(Symbol::ENCODE_TO_ENCODER, None, to_encoder_fn_var); + let to_encoder_fn = Box::new(( + to_encoder_fn_var, + Loc::at_zero(to_encoder_var), + to_encoder_clos_var, + encoder_var, + )); + + // toEncoder rcd.a + let to_encoder_call = Call( + to_encoder_fn, + vec![(field_var, Loc::at_zero(field_access))], + CalledVia::Space, + ); + + // value: toEncoder rcd.a + let value_field = Field { + var: encoder_var, + region: Region::zero(), + loc_expr: Box::new(Loc::at_zero(to_encoder_call)), + }; + + // { key: "a", value: toEncoder rcd.a } + let mut kv = SendMap::default(); + kv.insert("key".into(), key_field); + kv.insert("value".into(), value_field); + + let this_record_fields = RecordFields::insert_into_subs( + env.subs, + (once(("key".into(), RecordField::Required(Variable::STR)))) + .chain(once(("value".into(), RecordField::Required(encoder_var)))), + ); + let this_record_var = synth_var( + env.subs, + Content::Structure(FlatType::Record(this_record_fields, Variable::EMPTY_RECORD)), + ); + // NOTE: must be done to unify the lambda sets under `encoder_var` + env.unify(this_record_var, whole_rcd_var); + + Loc::at_zero(Record { + record_var: whole_rcd_var, + fields: kv, + }) + }) + .collect::>(); + + // typeof [ { key: .., value: .. }, { key: .., value: .. } ] + let fields_rcd_var_slice = VariableSubsSlice::insert_into_subs(env.subs, once(whole_rcd_var)); + let fields_list_var = synth_var( + env.subs, + Content::Structure(FlatType::Apply(Symbol::LIST_LIST, fields_rcd_var_slice)), + ); + + // [ { key: .., value: ..}, .. ] + let fields_list = List { + elem_var: whole_rcd_var, + loc_elems: fields_list, + }; + + // build `Encode.record [ { key: .., value: ..}, .. ]` type + // List { key : Str, value : Encoder fmt } -[uls]-> Encoder fmt where fmt implements EncoderFormatting + let encode_record_fn_var = env.import_builtin_symbol_var(Symbol::ENCODE_RECORD); + + // fields_list_var -[clos]-> t1 + let fields_list_var_slice = + VariableSubsSlice::insert_into_subs(env.subs, once(fields_list_var)); + let encode_record_clos_var = env.subs.fresh_unnamed_flex_var(); // clos + let encoder_var = env.subs.fresh_unnamed_flex_var(); // t1 + let this_encode_record_fn_var = synth_var( + env.subs, + Content::Structure(FlatType::Func( + fields_list_var_slice, + encode_record_clos_var, + encoder_var, + )), + ); + + // List { key : Str, value : Encoder fmt } -[uls]-> Encoder fmt where fmt implements EncoderFormatting + // ~ fields_list_var -[clos]-> t1 + env.unify(encode_record_fn_var, this_encode_record_fn_var); + + // Encode.record : fields_list_var -[clos]-> Encoder fmt where fmt implements EncoderFormatting + let encode_record_var = AbilityMember(Symbol::ENCODE_RECORD, None, encode_record_fn_var); + let encode_record_fn = Box::new(( + encode_record_fn_var, + Loc::at_zero(encode_record_var), + encode_record_clos_var, + encoder_var, + )); + + // Encode.record [ { key: .., value: .. }, .. ] + let encode_record_call = Call( + encode_record_fn, + vec![(fields_list_var, Loc::at_zero(fields_list))], + CalledVia::Space, + ); + + // Encode.custom \bytes, fmt -> Encode.appendWith bytes (Encode.record ..) fmt + let (body, this_encoder_var) = + wrap_in_encode_custom(env, encode_record_call, encoder_var, rcd_sym, record_var); + + // Create fn_var for ambient capture; we fix it up below. + let fn_var = synth_var(env.subs, Content::Error); + + // -[fn_name]-> + let fn_name_labels = UnionLambdas::insert_into_subs(env.subs, once((fn_name, vec![]))); + let fn_clos_var = synth_var( + env.subs, + Content::LambdaSet(LambdaSet { + solved: fn_name_labels, + recursion_var: OptVariable::NONE, + unspecialized: SubsSlice::default(), + ambient_function: fn_var, + }), + ); + // typeof rcd -[fn_name]-> (typeof Encode.record [ .. ] = Encoder fmt) + let record_var_slice = SubsSlice::insert_into_subs(env.subs, once(record_var)); + env.subs.set_content( + fn_var, + Content::Structure(FlatType::Func( + record_var_slice, + fn_clos_var, + this_encoder_var, + )), + ); + + // \rcd -[fn_name]-> Encode.record [ { key: .., value: .. }, .. ] + let clos = Closure(ClosureData { + function_type: fn_var, + closure_type: fn_clos_var, + return_type: this_encoder_var, + name: fn_name, + captured_symbols: vec![], + recursive: Recursive::NotRecursive, + arguments: vec![( + record_var, + AnnotatedMark::known_exhaustive(), + Loc::at_zero(Pattern::Identifier(rcd_sym)), + )], + loc_body: Box::new(Loc::at_zero(body)), + }); + + (clos, fn_var) +} + +fn to_encoder_tuple( + env: &mut Env<'_>, + tuple_var: Variable, + elems: TupleElems, + fn_name: Symbol, +) -> (Expr, Variable) { + // Suppose tup = (t1, t2). Build + // + // \tup -> Encode.tuple [ + // Encode.toEncoder tup.0, + // Encode.toEncoder tup.1, + // ] + + let tup_sym = env.new_symbol("tup"); + let whole_encoder_in_list_var = env.subs.fresh_unnamed_flex_var(); // type of the encoder in the list + + use Expr::*; + + let elem_encoders_list = elems + .iter_all() + .map(|(elem_index, elem_var_index)| { + let index = env.subs[elem_index]; + let elem_var = env.subs[elem_var_index]; + let elem_var_slice = VariableSubsSlice::new(elem_var_index.index, 1); + + // tup.0 + let tuple_access = TupleAccess { + tuple_var, + ext_var: env.subs.fresh_unnamed_flex_var(), + elem_var, + loc_expr: Box::new(Loc::at_zero(Var( + tup_sym, + env.subs.fresh_unnamed_flex_var(), + ))), + index, + }; + + // build `toEncoder tup.0` type + // val -[uls]-> Encoder fmt where fmt implements EncoderFormatting + let to_encoder_fn_var = env.import_builtin_symbol_var(Symbol::ENCODE_TO_ENCODER); + + // (typeof tup.0) -[clos]-> t1 + let to_encoder_clos_var = env.subs.fresh_unnamed_flex_var(); // clos + let encoder_var = env.subs.fresh_unnamed_flex_var(); // t1 + let this_to_encoder_fn_var = synth_var( + env.subs, + Content::Structure(FlatType::Func( + elem_var_slice, + to_encoder_clos_var, + encoder_var, + )), + ); + + // val -[uls]-> Encoder fmt where fmt implements EncoderFormatting + // ~ (typeof tup.0) -[clos]-> t1 + env.unify(to_encoder_fn_var, this_to_encoder_fn_var); + + // toEncoder : (typeof tup.0) -[clos]-> Encoder fmt where fmt implements EncoderFormatting + let to_encoder_var = AbilityMember(Symbol::ENCODE_TO_ENCODER, None, to_encoder_fn_var); + let to_encoder_fn = Box::new(( + to_encoder_fn_var, + Loc::at_zero(to_encoder_var), + to_encoder_clos_var, + encoder_var, + )); + + // toEncoder tup.0 + let to_encoder_call = Call( + to_encoder_fn, + vec![(elem_var, Loc::at_zero(tuple_access))], + CalledVia::Space, + ); + + // NOTE: must be done to unify the lambda sets under `encoder_var` + env.unify(encoder_var, whole_encoder_in_list_var); + + Loc::at_zero(to_encoder_call) + }) + .collect::>(); + + // typeof [ toEncoder tup.0, toEncoder tup.1 ] + let whole_encoder_in_list_var_slice = + VariableSubsSlice::insert_into_subs(env.subs, once(whole_encoder_in_list_var)); + let elem_encoders_list_var = synth_var( + env.subs, + Content::Structure(FlatType::Apply( + Symbol::LIST_LIST, + whole_encoder_in_list_var_slice, + )), + ); + + // [ toEncoder tup.0, toEncoder tup.1 ] + let elem_encoders_list = List { + elem_var: whole_encoder_in_list_var, + loc_elems: elem_encoders_list, + }; + + // build `Encode.tuple [ toEncoder tup.0, toEncoder tup.1 ]` type + // List (Encoder fmt) -[uls]-> Encoder fmt where fmt implements EncoderFormatting + let encode_tuple_fn_var = env.import_builtin_symbol_var(Symbol::ENCODE_TUPLE); + + // elem_encoders_list_var -[clos]-> t1 + let elem_encoders_list_var_slice = + VariableSubsSlice::insert_into_subs(env.subs, once(elem_encoders_list_var)); + let encode_tuple_clos_var = env.subs.fresh_unnamed_flex_var(); // clos + let encoder_var = env.subs.fresh_unnamed_flex_var(); // t1 + let this_encode_tuple_fn_var = synth_var( + env.subs, + Content::Structure(FlatType::Func( + elem_encoders_list_var_slice, + encode_tuple_clos_var, + encoder_var, + )), + ); + + // List (Encoder fmt) -[uls]-> Encoder fmt where fmt implements EncoderFormatting + // ~ elem_encoders_list_var -[clos]-> t1 + env.unify(encode_tuple_fn_var, this_encode_tuple_fn_var); + + // Encode.tuple : elem_encoders_list_var -[clos]-> Encoder fmt where fmt implements EncoderFormatting + let encode_tuple_var = AbilityMember(Symbol::ENCODE_TUPLE, None, encode_tuple_fn_var); + let encode_tuple_fn = Box::new(( + encode_tuple_fn_var, + Loc::at_zero(encode_tuple_var), + encode_tuple_clos_var, + encoder_var, + )); + + // Encode.tuple [ { key: .., value: .. }, .. ] + let encode_tuple_call = Call( + encode_tuple_fn, + vec![(elem_encoders_list_var, Loc::at_zero(elem_encoders_list))], + CalledVia::Space, + ); + + // Encode.custom \bytes, fmt -> Encode.appendWith bytes (Encode.tuple_var ..) fmt + let (body, this_encoder_var) = + wrap_in_encode_custom(env, encode_tuple_call, encoder_var, tup_sym, tuple_var); + + // Create fn_var for ambient capture; we fix it up below. + let fn_var = synth_var(env.subs, Content::Error); + + // -[fn_name]-> + let fn_name_labels = UnionLambdas::insert_into_subs(env.subs, once((fn_name, vec![]))); + let fn_clos_var = synth_var( + env.subs, + Content::LambdaSet(LambdaSet { + solved: fn_name_labels, + recursion_var: OptVariable::NONE, + unspecialized: SubsSlice::default(), + ambient_function: fn_var, + }), + ); + // typeof tup -[fn_name]-> (typeof Encode.tuple [ .. ] = Encoder fmt) + let tuple_var_slice = SubsSlice::insert_into_subs(env.subs, once(tuple_var)); + env.subs.set_content( + fn_var, + Content::Structure(FlatType::Func( + tuple_var_slice, + fn_clos_var, + this_encoder_var, + )), + ); + + // \tup -[fn_name]-> Encode.tuple [ { key: .., value: .. }, .. ] + let clos = Closure(ClosureData { + function_type: fn_var, + closure_type: fn_clos_var, + return_type: this_encoder_var, + name: fn_name, + captured_symbols: vec![], + recursive: Recursive::NotRecursive, + arguments: vec![( + tuple_var, + AnnotatedMark::known_exhaustive(), + Loc::at_zero(Pattern::Identifier(tup_sym)), + )], + loc_body: Box::new(Loc::at_zero(body)), + }); + + (clos, fn_var) +} + +fn to_encoder_tag_union( + env: &mut Env<'_>, + tag_union_var: Variable, + tags: UnionTags, + fn_name: Symbol, +) -> (Expr, Variable) { + // Suppose tag = [ A t1 t2, B t3 ]. Build + // + // \tag -> when tag is + // A v1 v2 -> Encode.tag "A" [ Encode.toEncoder v1, Encode.toEncoder v2 ] + // B v3 -> Encode.tag "B" [ Encode.toEncoder v3 ] + + let tag_sym = env.new_symbol("tag"); + let whole_tag_encoders_var = env.subs.fresh_unnamed_flex_var(); // type of the Encode.tag ... calls in the branch bodies + + use Expr::*; + + let branches = tags + .iter_all() + .map(|(tag_name_index, tag_vars_slice_index)| { + // A + let tag_name = &env.subs[tag_name_index].clone(); + let vars_slice = env.subs[tag_vars_slice_index]; + // t1 t2 + let payload_vars = env.subs.get_subs_slice(vars_slice).to_vec(); + // v1 v2 + let payload_syms: Vec<_> = std::iter::repeat_with(|| env.unique_symbol()) + .take(payload_vars.len()) + .collect(); + + // `A v1 v2` pattern + let pattern = Pattern::AppliedTag { + whole_var: tag_union_var, + tag_name: tag_name.clone(), + ext_var: Variable::EMPTY_TAG_UNION, + // (t1, v1) (t2, v2) + arguments: (payload_vars.iter()) + .zip(payload_syms.iter()) + .map(|(var, sym)| (*var, Loc::at_zero(Pattern::Identifier(*sym)))) + .collect(), + }; + let branch_pattern = WhenBranchPattern { + pattern: Loc::at_zero(pattern), + degenerate: false, + }; + + // whole type of the elements in [ Encode.toEncoder v1, Encode.toEncoder v2 ] + let whole_payload_encoders_var = env.subs.fresh_unnamed_flex_var(); + // [ Encode.toEncoder v1, Encode.toEncoder v2 ] + let payload_to_encoders = (payload_syms.iter()) + .zip(payload_vars.iter()) + .map(|(&sym, &sym_var)| { + // build `toEncoder v1` type + // expected: val -[uls]-> Encoder fmt where fmt implements EncoderFormatting + let to_encoder_fn_var = + env.import_builtin_symbol_var(Symbol::ENCODE_TO_ENCODER); + + // wanted: t1 -[clos]-> t' + let var_slice_of_sym_var = + VariableSubsSlice::insert_into_subs(env.subs, [sym_var]); // [ t1 ] + let to_encoder_clos_var = env.subs.fresh_unnamed_flex_var(); // clos + let encoder_var = env.subs.fresh_unnamed_flex_var(); // t' + let this_to_encoder_fn_var = synth_var( + env.subs, + Content::Structure(FlatType::Func( + var_slice_of_sym_var, + to_encoder_clos_var, + encoder_var, + )), + ); + + // val -[uls]-> Encoder fmt where fmt implements EncoderFormatting + // ~ t1 -[clos]-> t' + env.unify(to_encoder_fn_var, this_to_encoder_fn_var); + + // toEncoder : t1 -[clos]-> Encoder fmt where fmt implements EncoderFormatting + let to_encoder_var = + AbilityMember(Symbol::ENCODE_TO_ENCODER, None, this_to_encoder_fn_var); + let to_encoder_fn = Box::new(( + this_to_encoder_fn_var, + Loc::at_zero(to_encoder_var), + to_encoder_clos_var, + encoder_var, + )); + + // toEncoder rcd.a + let to_encoder_call = Call( + to_encoder_fn, + vec![(sym_var, Loc::at_zero(Var(sym, sym_var)))], + CalledVia::Space, + ); + + // NOTE: must be done to unify the lambda sets under `encoder_var` + env.unify(encoder_var, whole_payload_encoders_var); + + Loc::at_zero(to_encoder_call) + }) + .collect(); + + // typeof [ Encode.toEncoder v1, Encode.toEncoder v2 ] + let whole_encoders_var_slice = + VariableSubsSlice::insert_into_subs(env.subs, [whole_payload_encoders_var]); + let payload_encoders_list_var = synth_var( + env.subs, + Content::Structure(FlatType::Apply(Symbol::LIST_LIST, whole_encoders_var_slice)), + ); + + // [ Encode.toEncoder v1, Encode.toEncoder v2 ] + let payload_encoders_list = List { + elem_var: whole_payload_encoders_var, + loc_elems: payload_to_encoders, + }; + + // build `Encode.tag "A" [ ... ]` type + // expected: Str, List (Encoder fmt) -[uls]-> Encoder fmt where fmt implements EncoderFormatting + let encode_tag_fn_var = env.import_builtin_symbol_var(Symbol::ENCODE_TAG); + + // wanted: Str, List whole_encoders_var -[clos]-> t' + let this_encode_tag_args_var_slice = VariableSubsSlice::insert_into_subs( + env.subs, + [Variable::STR, payload_encoders_list_var], + ); + let this_encode_tag_clos_var = env.subs.fresh_unnamed_flex_var(); // -[clos]-> + let this_encoder_var = env.subs.fresh_unnamed_flex_var(); // t' + let this_encode_tag_fn_var = synth_var( + env.subs, + Content::Structure(FlatType::Func( + this_encode_tag_args_var_slice, + this_encode_tag_clos_var, + this_encoder_var, + )), + ); + + // Str, List (Encoder fmt) -[uls]-> Encoder fmt where fmt implements EncoderFormatting + // ~ Str, List whole_encoders_var -[clos]-> t' + env.unify(encode_tag_fn_var, this_encode_tag_fn_var); + + // Encode.tag : Str, List whole_encoders_var -[clos]-> Encoder fmt where fmt implements EncoderFormatting + let encode_tag_var = AbilityMember(Symbol::ENCODE_TAG, None, this_encode_tag_fn_var); + let encode_tag_fn = Box::new(( + this_encode_tag_fn_var, + Loc::at_zero(encode_tag_var), + this_encode_tag_clos_var, + this_encoder_var, + )); + + // Encode.tag "A" [ Encode.toEncoder v1, Encode.toEncoder v2 ] + let encode_tag_call = Call( + encode_tag_fn, + vec![ + // (Str, "A") + (Variable::STR, Loc::at_zero(Str(tag_name.0.as_str().into()))), + // (List (Encoder fmt), [ Encode.toEncoder v1, Encode.toEncoder v2 ]) + ( + payload_encoders_list_var, + Loc::at_zero(payload_encoders_list), + ), + ], + CalledVia::Space, + ); + + // NOTE: must be done to unify the lambda sets under `encoder_var` + // Encode.tag "A" [ Encode.toEncoder v1, Encode.toEncoder v2 ] ~ whole_encoders + env.unify(this_encoder_var, whole_tag_encoders_var); + + WhenBranch { + patterns: vec![branch_pattern], + value: Loc::at_zero(encode_tag_call), + guard: None, + redundant: RedundantMark::known_non_redundant(), + } + }) + .collect::>(); + + // when tag is + // A v1 v2 -> Encode.tag "A" [ Encode.toEncoder v1, Encode.toEncoder v2 ] + // B v3 -> Encode.tag "B" [ Encode.toEncoder v3 ] + let when_branches = When { + loc_cond: Box::new(Loc::at_zero(Var(tag_sym, tag_union_var))), + cond_var: tag_union_var, + expr_var: whole_tag_encoders_var, + region: Region::zero(), + branches, + branches_cond_var: tag_union_var, + exhaustive: ExhaustiveMark::known_exhaustive(), + }; + + // Encode.custom \bytes, fmt -> Encode.appendWith bytes (when ..) fmt + let (body, this_encoder_var) = wrap_in_encode_custom( + env, + when_branches, + whole_tag_encoders_var, + tag_sym, + tag_union_var, + ); + + // Create fn_var for ambient capture; we fix it up below. + let fn_var = synth_var(env.subs, Content::Error); + + // -[fn_name]-> + let fn_name_labels = UnionLambdas::insert_into_subs(env.subs, once((fn_name, vec![]))); + let fn_clos_var = synth_var( + env.subs, + Content::LambdaSet(LambdaSet { + solved: fn_name_labels, + recursion_var: OptVariable::NONE, + unspecialized: SubsSlice::default(), + ambient_function: fn_var, + }), + ); + // tag_union_var -[fn_name]-> whole_tag_encoders_var + let tag_union_var_slice = SubsSlice::insert_into_subs(env.subs, once(tag_union_var)); + env.subs.set_content( + fn_var, + Content::Structure(FlatType::Func( + tag_union_var_slice, + fn_clos_var, + this_encoder_var, + )), + ); + + // \tag -> + // Encode.custom \bytes, fmt -> Encode.appendWith bytes ( + // when tag is + // A v1 v2 -> Encode.tag "A" [ Encode.toEncoder v1, Encode.toEncoder v2 ] + // B v3 -> Encode.tag "B" [ Encode.toEncoder v3 ]) + // fmt + let clos = Closure(ClosureData { + function_type: fn_var, + closure_type: fn_clos_var, + return_type: this_encoder_var, + name: fn_name, + captured_symbols: vec![], + recursive: Recursive::NotRecursive, + arguments: vec![( + tag_union_var, + AnnotatedMark::known_exhaustive(), + Loc::at_zero(Pattern::Identifier(tag_sym)), + )], + loc_body: Box::new(Loc::at_zero(body)), + }); + + (clos, fn_var) +} + +/// Lift `encoder` to `Encode.custom \bytes, fmt -> Encode.appendWith bytes encoder fmt` +/// +/// TODO: currently it appears that just `encoder` is not isomorphic to the lift, on the +/// monomorphization level, even though we would think it is. In particular, unspecialized lambda +/// sets fail to resolve when we use the non-lifted version. +/// More investigation is needed to figure out why. +fn wrap_in_encode_custom( + env: &mut Env, + encoder: Expr, + encoder_var: Variable, + captured_symbol: Symbol, + captured_var: Variable, +) -> (Expr, Variable) { + use Expr::*; + + let fn_name = env.new_symbol("custom"); + + // bytes: List U8 + let bytes_sym = env.new_symbol("bytes"); + let bytes_var = Variable::LIST_U8; + + // fmt: fmt where fmt implements EncoderFormatting + let fmt_sym = env.new_symbol("fmt"); + let fmt_var = env.subs.fresh_unnamed_flex_var(); + + // build `Encode.appendWith bytes encoder fmt` type + // expected: Encode.appendWith : List U8, Encoder fmt, fmt -[appendWith]-> List U8 where fmt implements EncoderFormatting + let append_with_fn_var = env.import_builtin_symbol_var(Symbol::ENCODE_APPEND_WITH); + + // wanted: Encode.appendWith : List U8, encoder_var, fmt -[clos]-> List U8 where fmt implements EncoderFormatting + let this_append_with_args_var_slice = + VariableSubsSlice::insert_into_subs(env.subs, [Variable::LIST_U8, encoder_var, fmt_var]); + let this_append_with_clos_var = env.subs.fresh_unnamed_flex_var(); // -[clos]-> + let this_append_with_fn_var = synth_var( + env.subs, + Content::Structure(FlatType::Func( + this_append_with_args_var_slice, + this_append_with_clos_var, + Variable::LIST_U8, + )), + ); + + // List U8, Encoder fmt, fmt -[appendWith]-> List U8 where fmt implements EncoderFormatting + // ~ List U8, encoder_var, fmt -[clos]-> List U8 where fmt implements EncoderFormatting + env.unify(append_with_fn_var, this_append_with_fn_var); + + // Encode.appendWith : List U8, encoder_var, fmt -[appendWith]-> List U8 where fmt implements EncoderFormatting + let append_with_fn = Box::new(( + this_append_with_fn_var, + Loc::at_zero(Var(Symbol::ENCODE_APPEND_WITH, this_append_with_fn_var)), + this_append_with_clos_var, + Variable::LIST_U8, + )); + + // Encode.appendWith bytes encoder fmt + let append_with_call = Call( + append_with_fn, + vec![ + // (bytes_var, bytes) + (bytes_var, Loc::at_zero(Var(bytes_sym, bytes_var))), + // (encoder_var, encoder) + (encoder_var, Loc::at_zero(encoder)), + // (fmt, fmt_var) + (fmt_var, Loc::at_zero(Var(fmt_sym, fmt_var))), + ], + CalledVia::Space, + ); + + // Create fn_var for ambient capture; we fix it up below. + let fn_var = synth_var(env.subs, Content::Error); + + // -[[FN_name captured_var]]-> + let fn_name_labels = + UnionLambdas::insert_into_subs(env.subs, once((fn_name, vec![captured_var]))); + let fn_clos_var = synth_var( + env.subs, + Content::LambdaSet(LambdaSet { + solved: fn_name_labels, + recursion_var: OptVariable::NONE, + unspecialized: SubsSlice::default(), + ambient_function: fn_var, + }), + ); + + // bytes, fmt -[[FN_name captured_var]]-> Encode.appendWith bytes encoder fmt + let args_slice = SubsSlice::insert_into_subs(env.subs, vec![bytes_var, fmt_var]); + env.subs.set_content( + fn_var, + Content::Structure(FlatType::Func(args_slice, fn_clos_var, Variable::LIST_U8)), + ); + + // \bytes, fmt -[[fn_name captured_var]]-> Encode.appendWith bytes encoder fmt + let clos = Closure(ClosureData { + function_type: fn_var, + closure_type: fn_clos_var, + return_type: Variable::LIST_U8, + name: fn_name, + captured_symbols: vec![(captured_symbol, captured_var)], + recursive: Recursive::NotRecursive, + arguments: vec![ + ( + bytes_var, + AnnotatedMark::known_exhaustive(), + Loc::at_zero(Pattern::Identifier(bytes_sym)), + ), + ( + fmt_var, + AnnotatedMark::known_exhaustive(), + Loc::at_zero(Pattern::Identifier(fmt_sym)), + ), + ], + loc_body: Box::new(Loc::at_zero(append_with_call)), + }); + + // Build + // Encode.custom \bytes, fmt -> Encode.appendWith bytes encoder fmt + // + // expected: Encode.custom : (List U8, fmt -> List U8) -> Encoder fmt where fmt implements EncoderFormatting + let custom_fn_var = env.import_builtin_symbol_var(Symbol::ENCODE_CUSTOM); + + // wanted: Encode.custom : fn_var -[clos]-> t' + let this_custom_args_var_slice = VariableSubsSlice::insert_into_subs(env.subs, [fn_var]); + let this_custom_clos_var = env.subs.fresh_unnamed_flex_var(); // -[clos]-> + let this_custom_encoder_var = env.subs.fresh_unnamed_flex_var(); // t' + let this_custom_fn_var = synth_var( + env.subs, + Content::Structure(FlatType::Func( + this_custom_args_var_slice, + this_custom_clos_var, + this_custom_encoder_var, + )), + ); + + // (List U8, fmt -> List U8) -[..]-> Encoder fmt where fmt implements EncoderFormatting + // ~ fn_var -[clos]-> t' + env.unify(custom_fn_var, this_custom_fn_var); + + // Encode.custom : (List U8, fmt -> List U8) -> Encoder fmt where fmt implements EncoderFormatting + let custom_fn = Box::new(( + this_custom_fn_var, + Loc::at_zero(Var(Symbol::ENCODE_CUSTOM, this_custom_fn_var)), + this_custom_clos_var, // -[clos]-> + this_custom_encoder_var, // t' ~ Encoder fmt + )); + + // Encode.custom \bytes, fmt -> Encode.appendWith bytes encoder fmt + let custom_call = Call( + custom_fn, + vec![(fn_var, Loc::at_zero(clos))], + CalledVia::Space, + ); + + (custom_call, this_custom_encoder_var) +} diff --git a/crates/compiler/derive/src/hash.rs b/crates/compiler/derive/src/hash.rs new file mode 100644 index 0000000000..f890f787b1 --- /dev/null +++ b/crates/compiler/derive/src/hash.rs @@ -0,0 +1,565 @@ +//! Derivers for the `Hash` ability. + +use std::iter::once; + +use roc_can::{ + expr::{AnnotatedMark, ClosureData, Expr, IntValue, Recursive, WhenBranch, WhenBranchPattern}, + num::{IntBound, IntLitWidth}, + pattern::Pattern, +}; +use roc_derive_key::hash::FlatHashKey; +use roc_error_macros::internal_error; +use roc_module::{ + called_via::CalledVia, + ident::{Lowercase, TagName}, + symbol::Symbol, +}; +use roc_region::all::{Loc, Region}; +use roc_types::{ + num::int_lit_width_to_variable, + subs::{ + Content, ExhaustiveMark, FlatType, GetSubsSlice, LambdaSet, OptVariable, RecordFields, + RedundantMark, Subs, SubsIndex, SubsSlice, TagExt, TupleElems, UnionLambdas, UnionTags, + Variable, VariableSubsSlice, + }, + types::RecordField, +}; + +use crate::{synth_var, util::Env, DerivedBody}; + +pub(crate) fn derive_hash(env: &mut Env<'_>, key: FlatHashKey, def_symbol: Symbol) -> DerivedBody { + let (body_type, body) = match key { + FlatHashKey::Record(fields) => hash_record(env, def_symbol, fields), + FlatHashKey::Tuple(arity) => hash_tuple(env, def_symbol, arity), + FlatHashKey::TagUnion(tags) => { + if tags.len() == 1 { + hash_newtype_tag_union(env, def_symbol, tags.into_iter().next().unwrap()) + } else { + hash_tag_union(env, def_symbol, tags) + } + } + }; + + let specialization_lambda_sets = + env.get_specialization_lambda_sets(body_type, Symbol::HASH_HASH); + + DerivedBody { + body, + body_type, + specialization_lambda_sets, + } +} + +fn hash_record(env: &mut Env<'_>, fn_name: Symbol, fields: Vec) -> (Variable, Expr) { + // Suppose rcd = { f1, ..., fn }. + // Build a generalized type t_rcd = { f1: t1, ..., fn: tn }, with fresh t1, ..., tn, + // so that we can re-use the derived impl for many records of the same fields. + let (record_var, record_fields) = { + let flex_fields = fields + .into_iter() + .map(|name| { + ( + name, + RecordField::Required(env.subs.fresh_unnamed_flex_var()), + ) + }) + .collect::>(); + let fields = RecordFields::insert_into_subs(env.subs, flex_fields); + let record_var = synth_var( + env.subs, + Content::Structure(FlatType::Record(fields, Variable::EMPTY_RECORD)), + ); + + (record_var, fields) + }; + + // Now, a hasher for this record is + // + // hash_rcd : hasher, { f1: t1, ..., fn: tn } -> hasher where hasher implements Hasher + // hash_rcd = \hasher, rcd -> + // Hash.hash ( + // Hash.hash + // ... + // (Hash.hash hasher rcd.f1) + // ... + // rcd.f_n1) + // rcd.fn + // + // So, just a build a fold travelling up the fields. + let rcd_sym = env.new_symbol("rcd"); + + let hasher_sym = env.new_symbol("hasher"); + let hasher_var = synth_var(env.subs, Content::FlexAbleVar(None, Subs::AB_HASHER)); + + let (body_var, body) = record_fields.iter_all().fold( + (hasher_var, Expr::Var(hasher_sym, hasher_var)), + |total_hasher, (field_name, field_var, _)| { + let field_name = env.subs[field_name].clone(); + let field_var = env.subs[field_var]; + + let field_access = Expr::RecordAccess { + record_var, + field_var, + ext_var: env.subs.fresh_unnamed_flex_var(), + loc_expr: Box::new(Loc::at_zero(Expr::Var( + rcd_sym, + env.subs.fresh_unnamed_flex_var(), + ))), + field: field_name, + }; + + call_hash_hash(env, total_hasher, (field_var, field_access)) + }, + ); + + // Finally, build the closure + // \hasher, rcd -> body + build_outer_derived_closure( + env, + fn_name, + (hasher_var, hasher_sym), + (record_var, Pattern::Identifier(rcd_sym)), + (body_var, body), + ) +} + +fn hash_tuple(env: &mut Env<'_>, fn_name: Symbol, arity: u32) -> (Variable, Expr) { + // Suppose tup = (v1, ..., vn). + // Build a generalized type t_tup = (t1, ..., tn), with fresh t1, ..., tn, + // so that we can re-use the derived impl for many tuples of the same arity. + let (tuple_var, tuple_elems) = { + // TODO: avoid an allocation here by pre-allocating the indices and variables `TupleElems` + // will be instantiated with. + let flex_elems: Vec<_> = (0..arity) + .map(|i| (i as usize, env.subs.fresh_unnamed_flex_var())) + .collect(); + let elems = TupleElems::insert_into_subs(env.subs, flex_elems); + let tuple_var = synth_var( + env.subs, + Content::Structure(FlatType::Tuple(elems, Variable::EMPTY_TUPLE)), + ); + + (tuple_var, elems) + }; + + // Now, a hasher for this tuple is + // + // hash_tup : hasher, (t1, ..., tn) -> hasher where hasher implements Hasher + // hash_tup = \hasher, tup -> + // Hash.hash ( + // Hash.hash + // ... + // (Hash.hash hasher tup.0) + // ... + // tup.n1) + // tup.n + // + // So, just a build a fold travelling up the elements. + let tup_sym = env.new_symbol("tup"); + + let hasher_sym = env.new_symbol("hasher"); + let hasher_var = synth_var(env.subs, Content::FlexAbleVar(None, Subs::AB_HASHER)); + + let (body_var, body) = tuple_elems.iter_all().fold( + (hasher_var, Expr::Var(hasher_sym, hasher_var)), + |total_hasher, (elem_idx, elem_var)| { + let index = env.subs[elem_idx]; + let elem_var = env.subs[elem_var]; + + let elem_access = Expr::TupleAccess { + tuple_var, + elem_var, + ext_var: env.subs.fresh_unnamed_flex_var(), + loc_expr: Box::new(Loc::at_zero(Expr::Var( + tup_sym, + env.subs.fresh_unnamed_flex_var(), + ))), + index, + }; + + call_hash_hash(env, total_hasher, (elem_var, elem_access)) + }, + ); + + // Finally, build the closure + // \hasher, rcd -> body + build_outer_derived_closure( + env, + fn_name, + (hasher_var, hasher_sym), + (tuple_var, Pattern::Identifier(tup_sym)), + (body_var, body), + ) +} + +/// Build a `hash` implementation for a non-singleton tag union. +fn hash_tag_union( + env: &mut Env<'_>, + fn_name: Symbol, + tags: Vec<(TagName, u16)>, +) -> (Variable, Expr) { + // Suppose tags = [ A p11 .. p1n, ..., Q pq1 .. pqm ] + // Build a generalized type t_tags = [ A t11 .. t1n, ..., Q tq1 .. tqm ], + // with fresh t1, ..., tqm, so that we can re-use the derived impl for many + // unions of the same tags and payloads. + let (union_var, union_tags) = { + let flex_tag_labels = tags + .into_iter() + .map(|(label, arity)| { + let variables_slice = VariableSubsSlice::reserve_into_subs(env.subs, arity.into()); + for var_index in variables_slice { + env.subs[var_index] = env.subs.fresh_unnamed_flex_var(); + } + (label, variables_slice) + }) + .collect::>(); + let union_tags = UnionTags::insert_slices_into_subs(env.subs, flex_tag_labels); + let tag_union_var = synth_var( + env.subs, + Content::Structure(FlatType::TagUnion( + union_tags, + TagExt::Any(Variable::EMPTY_TAG_UNION), + )), + ); + + (tag_union_var, union_tags) + }; + + // Now, a hasher for this tag union is + // + // hash_union : hasher, [ A t11 .. t1n, ..., Q tq1 .. tqm ] -> hasher where hasher implements Hasher + // hash_union = \hasher, union -> + // when union is + // A x11 .. x1n -> Hash.hash (... (Hash.hash (Hash.uN hasher 0) x11) ...) x1n + // ... + // Q xq1 .. xqm -> Hash.hash (... (Hash.hash (Hash.uN hasher (q - 1)) xq1) ...) xqm + // + // where `Hash.uN` is the appropriate hasher for the discriminant value - typically a `u8`, but + // if there are more than `u8::MAX` tags, we use `u16`, and so on. + let union_sym = env.new_symbol("union"); + + let hasher_sym = env.new_symbol("hasher"); + let hasher_var = synth_var(env.subs, Content::FlexAbleVar(None, Subs::AB_HASHER)); + + let (discr_width, discr_precision_var, hash_discr_member) = if union_tags.len() > u64::MAX as _ + { + // Should never happen, `usize` isn't more than 64 bits on most machines, but who knows? + // Maybe someday soon someone will try to compile a huge Roc program on a 128-bit one. + internal_error!("new record unlocked: you fit more than 18 billion, billion tags in a Roc program, and the compiler didn't fall over! But it will now. 🤯") + } else if union_tags.len() > u32::MAX as _ { + (IntLitWidth::U64, Variable::UNSIGNED64, Symbol::HASH_ADD_U64) + } else if union_tags.len() > u16::MAX as _ { + (IntLitWidth::U32, Variable::UNSIGNED32, Symbol::HASH_ADD_U32) + } else if union_tags.len() > u8::MAX as _ { + (IntLitWidth::U16, Variable::UNSIGNED16, Symbol::HASH_ADD_U16) + } else { + (IntLitWidth::U8, Variable::UNSIGNED8, Symbol::HASH_ADD_U8) + }; + let discr_num_var = int_lit_width_to_variable(discr_width); + + // Build the branches of the body + let whole_hasher_var = env.subs.fresh_unnamed_flex_var(); + let branches = union_tags + .iter_all() + .enumerate() + .map(|(discr_n, (tag, payloads))| { + // A + let tag_name = env.subs[tag].clone(); + // t11 .. t1n + let payload_vars = env.subs.get_subs_slice(env.subs[payloads]).to_vec(); + // x11 .. x1n + let payload_syms: Vec<_> = std::iter::repeat_with(|| env.unique_symbol()) + .take(payload_vars.len()) + .collect(); + + // `A x1 .. x1n` pattern + let pattern = Pattern::AppliedTag { + whole_var: union_var, + tag_name, + ext_var: Variable::EMPTY_TAG_UNION, + // (t1, v1) (t2, v2) + arguments: (payload_vars.iter()) + .zip(payload_syms.iter()) + .map(|(var, sym)| (*var, Loc::at_zero(Pattern::Identifier(*sym)))) + .collect(), + }; + let branch_pattern = WhenBranchPattern { + pattern: Loc::at_zero(pattern), + degenerate: false, + }; + + // discrHasher = (Hash.uN hasher n) + let (discr_hasher_var, disc_hasher_expr) = call_hash_ability_member( + env, + hash_discr_member, + (hasher_var, Expr::Var(hasher_sym, hasher_var)), + ( + discr_num_var, + Expr::Int( + discr_num_var, + discr_precision_var, + format!("{discr_n}").into_boxed_str(), + IntValue::I128((discr_n as i128).to_ne_bytes()), + IntBound::Exact(discr_width), + ), + ), + ); + + // Fold up `Hash.hash (... (Hash.hash discrHasher x11) ...) x1n` + let (body_var, body_expr) = (payload_vars.into_iter()).zip(payload_syms).fold( + (discr_hasher_var, disc_hasher_expr), + |total_hasher, (payload_var, payload_sym)| { + call_hash_hash( + env, + total_hasher, + (payload_var, Expr::Var(payload_sym, payload_var)), + ) + }, + ); + + env.unify(whole_hasher_var, body_var); + + WhenBranch { + patterns: vec![branch_pattern], + value: Loc::at_zero(body_expr), + guard: None, + redundant: RedundantMark::known_non_redundant(), + } + }) + .collect(); + + // when union is + // ... + let when_var = whole_hasher_var; + let when_expr = Expr::When { + loc_cond: Box::new(Loc::at_zero(Expr::Var(union_sym, union_var))), + cond_var: union_var, + expr_var: when_var, + region: Region::zero(), + branches, + branches_cond_var: union_var, + exhaustive: ExhaustiveMark::known_exhaustive(), + }; + + // Finally, build the closure + // \hasher, rcd -> body + build_outer_derived_closure( + env, + fn_name, + (hasher_var, hasher_sym), + (union_var, Pattern::Identifier(union_sym)), + (when_var, when_expr), + ) +} + +/// Build a `hash` implementation for a newtype (singleton) tag union. +/// If a tag union is a newtype, we do not need to hash its discriminant. +fn hash_newtype_tag_union( + env: &mut Env<'_>, + fn_name: Symbol, + tag: (TagName, u16), +) -> (Variable, Expr) { + // Suppose tags = [ A p1 .. pn ] + // Build a generalized type t_tags = [ A t1 .. tn ], + // with fresh t1, ..., tn, so that we can re-use the derived impl for many + // unions of the same tag and payload arity. + let (union_var, tag_name, payload_variables) = { + let (label, arity) = tag; + + let variables_slice = VariableSubsSlice::reserve_into_subs(env.subs, arity.into()); + for var_index in variables_slice { + env.subs[var_index] = env.subs.fresh_unnamed_flex_var(); + } + + let variables_slices_slice = + SubsSlice::extend_new(&mut env.subs.variable_slices, [variables_slice]); + let tag_name_index = SubsIndex::push_new(&mut env.subs.tag_names, label.clone()); + + let union_tags = UnionTags::from_slices(tag_name_index.as_slice(), variables_slices_slice); + let tag_union_var = synth_var( + env.subs, + Content::Structure(FlatType::TagUnion( + union_tags, + TagExt::Any(Variable::EMPTY_TAG_UNION), + )), + ); + + ( + tag_union_var, + label, + env.subs.get_subs_slice(variables_slice).to_vec(), + ) + }; + + // Now, a hasher for this tag union is + // + // hash_union : hasher, [ A t1 .. tn ] -> hasher where hasher implements Hasher + // hash_union = \hasher, A x1 .. xn -> + // Hash.hash (... (Hash.hash discrHasher x1) ...) xn + let hasher_sym = env.new_symbol("hasher"); + let hasher_var = synth_var(env.subs, Content::FlexAbleVar(None, Subs::AB_HASHER)); + + // A + let tag_name = tag_name; + // t1 .. tn + let payload_vars = payload_variables; + // x11 .. x1n + let payload_syms: Vec<_> = std::iter::repeat_with(|| env.unique_symbol()) + .take(payload_vars.len()) + .collect(); + + // `A x1 .. x1n` pattern + let pattern = Pattern::AppliedTag { + whole_var: union_var, + tag_name, + ext_var: Variable::EMPTY_TAG_UNION, + // (t1, v1) (t2, v2) + arguments: (payload_vars.iter()) + .zip(payload_syms.iter()) + .map(|(var, sym)| (*var, Loc::at_zero(Pattern::Identifier(*sym)))) + .collect(), + }; + + // Fold up `Hash.hash (... (Hash.hash discrHasher x11) ...) x1n` + let (body_var, body_expr) = (payload_vars.into_iter()).zip(payload_syms).fold( + (hasher_var, Expr::Var(hasher_sym, hasher_var)), + |total_hasher, (payload_var, payload_sym)| { + call_hash_hash( + env, + total_hasher, + (payload_var, Expr::Var(payload_sym, payload_var)), + ) + }, + ); + + // Finally, build the closure + // \hasher, rcd -> body + build_outer_derived_closure( + env, + fn_name, + (hasher_var, hasher_sym), + (union_var, pattern), + (body_var, body_expr), + ) +} + +fn call_hash_hash( + env: &mut Env<'_>, + hasher: (Variable, Expr), + val: (Variable, Expr), +) -> (Variable, Expr) { + call_hash_ability_member(env, Symbol::HASH_HASH, hasher, val) +} + +fn call_hash_ability_member( + env: &mut Env<'_>, + member: Symbol, + hasher: (Variable, Expr), + val: (Variable, Expr), +) -> (Variable, Expr) { + let (in_hasher_var, in_hasher_expr) = hasher; + let (in_val_var, in_val_expr) = val; + + // build `member ...` function type. `member` here is `Hash.hash` or `Hash.addU16`. + // + // hasher, val -[uls]-> hasher where hasher implements Hasher, val implements Hash + let exposed_hash_fn_var = env.import_builtin_symbol_var(member); + + // (typeof body), (typeof field) -[clos]-> hasher_result + let this_arguments_slice = + VariableSubsSlice::insert_into_subs(env.subs, [in_hasher_var, in_val_var]); + let this_hash_clos_var = env.subs.fresh_unnamed_flex_var(); + let this_out_hasher_var = env.subs.fresh_unnamed_flex_var(); + let this_hash_fn_var = synth_var( + env.subs, + Content::Structure(FlatType::Func( + this_arguments_slice, + this_hash_clos_var, + this_out_hasher_var, + )), + ); + + // hasher, val -[uls]-> hasher where hasher implements Hasher, val implements Hash + // ~ (typeof body), (typeof field) -[clos]-> hasher_result + env.unify(exposed_hash_fn_var, this_hash_fn_var); + + // Hash.hash : hasher, (typeof field) -[clos]-> hasher where hasher implements Hasher, (typeof field) implements Hash + let hash_fn_head = Expr::AbilityMember(member, None, this_hash_fn_var); + let hash_fn_data = Box::new(( + this_hash_fn_var, + Loc::at_zero(hash_fn_head), + this_hash_clos_var, + this_out_hasher_var, + )); + + let hash_arguments = vec![ + (in_hasher_var, Loc::at_zero(in_hasher_expr)), + (in_val_var, Loc::at_zero(in_val_expr)), + ]; + let call_hash = Expr::Call(hash_fn_data, hash_arguments, CalledVia::Space); + + (this_out_hasher_var, call_hash) +} + +fn build_outer_derived_closure( + env: &mut Env<'_>, + fn_name: Symbol, + hasher: (Variable, Symbol), + val: (Variable, Pattern), + body: (Variable, Expr), +) -> (Variable, Expr) { + let (hasher_var, hasher_sym) = hasher; + let (val_var, val_pattern) = val; + let (body_var, body_expr) = body; + + let (fn_var, fn_clos_var) = { + // Create fn_var for ambient capture; we fix it up below. + let fn_var = synth_var(env.subs, Content::Error); + + // -[fn_name]-> + let fn_captures = vec![]; + let fn_name_labels = UnionLambdas::insert_into_subs(env.subs, once((fn_name, fn_captures))); + let fn_clos_var = synth_var( + env.subs, + Content::LambdaSet(LambdaSet { + solved: fn_name_labels, + recursion_var: OptVariable::NONE, + unspecialized: SubsSlice::default(), + ambient_function: fn_var, + }), + ); + + // hasher, rcd_var -[fn_name]-> (hasher = body_var) + let args_slice = SubsSlice::insert_into_subs(env.subs, [hasher_var, val_var]); + env.subs.set_content( + fn_var, + Content::Structure(FlatType::Func(args_slice, fn_clos_var, body_var)), + ); + + (fn_var, fn_clos_var) + }; + + let clos_expr = Expr::Closure(ClosureData { + function_type: fn_var, + closure_type: fn_clos_var, + return_type: body_var, + name: fn_name, + captured_symbols: vec![], + recursive: Recursive::NotRecursive, + arguments: vec![ + ( + hasher_var, + AnnotatedMark::known_exhaustive(), + Loc::at_zero(Pattern::Identifier(hasher_sym)), + ), + ( + val_var, + AnnotatedMark::known_exhaustive(), + Loc::at_zero(val_pattern), + ), + ], + loc_body: Box::new(Loc::at_zero(body_expr)), + }); + + (fn_var, clos_expr) +} diff --git a/crates/compiler/derive/src/inspect.rs b/crates/compiler/derive/src/inspect.rs new file mode 100644 index 0000000000..75536efea7 --- /dev/null +++ b/crates/compiler/derive/src/inspect.rs @@ -0,0 +1,1089 @@ +//! Derivers for the `Inspect` ability. + +use std::iter::once; + +use roc_can::expr::{ + AnnotatedMark, ClosureData, Expr, Field, Recursive, WhenBranch, WhenBranchPattern, +}; +use roc_can::pattern::Pattern; +use roc_collections::SendMap; +use roc_derive_key::inspect::FlatInspectableKey; +use roc_module::called_via::CalledVia; +use roc_module::ident::Lowercase; +use roc_module::symbol::Symbol; +use roc_region::all::{Loc, Region}; +use roc_types::subs::{ + Content, ExhaustiveMark, FlatType, GetSubsSlice, LambdaSet, OptVariable, RecordFields, + RedundantMark, SubsSlice, TagExt, TupleElems, UnionLambdas, UnionTags, Variable, + VariableSubsSlice, +}; +use roc_types::types::RecordField; + +use crate::util::Env; +use crate::{synth_var, DerivedBody}; + +pub(crate) fn derive_to_inspector( + env: &mut Env<'_>, + key: FlatInspectableKey, + def_symbol: Symbol, +) -> DerivedBody { + let (body, body_type) = match key { + FlatInspectableKey::List() => to_inspector_list(env, def_symbol), + FlatInspectableKey::Set() => unreachable!(), + FlatInspectableKey::Dict() => unreachable!(), + FlatInspectableKey::Record(fields) => { + // Generalized record var so we can reuse this impl between many records: + // if fields = { a, b }, this is { a: t1, b: t2 } for fresh t1, t2. + let flex_fields = fields + .into_iter() + .map(|name| { + ( + name, + RecordField::Required(env.subs.fresh_unnamed_flex_var()), + ) + }) + .collect::>(); + let fields = RecordFields::insert_into_subs(env.subs, flex_fields); + let record_var = synth_var( + env.subs, + Content::Structure(FlatType::Record(fields, Variable::EMPTY_RECORD)), + ); + + to_inspector_record(env, record_var, fields, def_symbol) + } + FlatInspectableKey::Tuple(arity) => { + // Generalized tuple var so we can reuse this impl between many tuples: + // if arity = n, this is (t1, ..., tn) for fresh t1, ..., tn. + let flex_elems = (0..arity) + .map(|idx| (idx as usize, env.subs.fresh_unnamed_flex_var())) + .collect::>(); + let elems = TupleElems::insert_into_subs(env.subs, flex_elems); + let tuple_var = synth_var( + env.subs, + Content::Structure(FlatType::Tuple(elems, Variable::EMPTY_TUPLE)), + ); + + to_inspector_tuple(env, tuple_var, elems, def_symbol) + } + FlatInspectableKey::TagUnion(tags) => { + // Generalized tag union var so we can reuse this impl between many unions: + // if tags = [ A arity=2, B arity=1 ], this is [ A t1 t2, B t3 ] for fresh t1, t2, t3 + let flex_tag_labels = tags + .into_iter() + .map(|(label, arity)| { + let variables_slice = + VariableSubsSlice::reserve_into_subs(env.subs, arity.into()); + for var_index in variables_slice { + env.subs[var_index] = env.subs.fresh_unnamed_flex_var(); + } + (label, variables_slice) + }) + .collect::>(); + let union_tags = UnionTags::insert_slices_into_subs(env.subs, flex_tag_labels); + let tag_union_var = synth_var( + env.subs, + Content::Structure(FlatType::TagUnion( + union_tags, + TagExt::Any(Variable::EMPTY_TAG_UNION), + )), + ); + + to_inspector_tag_union(env, tag_union_var, union_tags, def_symbol) + } + FlatInspectableKey::Function(_arity) => { + // Desired output: \x, y, z -> ... ===> "" + + todo!(); + } + FlatInspectableKey::Opaque => todo!(), + FlatInspectableKey::Error => todo!(), + }; + + let specialization_lambda_sets = + env.get_specialization_lambda_sets(body_type, Symbol::INSPECT_TO_INSPECTOR); + + DerivedBody { + body, + body_type, + specialization_lambda_sets, + } +} + +fn to_inspector_list(env: &mut Env<'_>, fn_name: Symbol) -> (Expr, Variable) { + // Build \lst -> list, List.walk, (\elem -> Inspect.toInspector elem) + + use Expr::*; + + let lst_sym = env.new_symbol("lst"); + let elem_sym = env.new_symbol("elem"); + + // List elem + let elem_var = env.subs.fresh_unnamed_flex_var(); + let elem_var_slice = SubsSlice::insert_into_subs(env.subs, [elem_var]); + let list_var = synth_var( + env.subs, + Content::Structure(FlatType::Apply(Symbol::LIST_LIST, elem_var_slice)), + ); + + // build `toInspector elem` type + // val -[uls]-> Inspector fmt where fmt implements InspectorFormatter + let to_inspector_fn_var = env.import_builtin_symbol_var(Symbol::INSPECT_TO_INSPECTOR); + + // elem -[clos]-> t1 + let to_inspector_clos_var = env.subs.fresh_unnamed_flex_var(); // clos + let elem_inspector_var = env.subs.fresh_unnamed_flex_var(); // t1 + let elem_to_inspector_fn_var = synth_var( + env.subs, + Content::Structure(FlatType::Func( + elem_var_slice, + to_inspector_clos_var, + elem_inspector_var, + )), + ); + + // val -[uls]-> Inspector fmt where fmt implements InspectorFormatter + // ~ elem -[clos]-> t1 + env.unify(to_inspector_fn_var, elem_to_inspector_fn_var); + + // toInspector : (typeof rcd.a) -[clos]-> Inspector fmt where fmt implements InspectorFormatter + let to_inspector_var = + AbilityMember(Symbol::INSPECT_TO_INSPECTOR, None, elem_to_inspector_fn_var); + let to_inspector_fn = Box::new(( + to_inspector_fn_var, + Loc::at_zero(to_inspector_var), + to_inspector_clos_var, + elem_inspector_var, + )); + + // toInspector elem + let to_inspector_call = Call( + to_inspector_fn, + vec![(elem_var, Loc::at_zero(Var(elem_sym, elem_var)))], + CalledVia::Space, + ); + + // elem -[to_elem_inspector]-> toInspector elem + let to_elem_inspector_sym = env.new_symbol("to_elem_inspector"); + + // Create fn_var for ambient capture; we fix it up below. + let to_elem_inspector_fn_var = synth_var(env.subs, Content::Error); + + // -[to_elem_inspector]-> + let to_elem_inspector_labels = + UnionLambdas::insert_into_subs(env.subs, once((to_elem_inspector_sym, vec![]))); + let to_elem_inspector_lset = synth_var( + env.subs, + Content::LambdaSet(LambdaSet { + solved: to_elem_inspector_labels, + recursion_var: OptVariable::NONE, + unspecialized: SubsSlice::default(), + ambient_function: to_elem_inspector_fn_var, + }), + ); + // elem -[to_elem_inspector]-> toInspector elem + env.subs.set_content( + to_elem_inspector_fn_var, + Content::Structure(FlatType::Func( + elem_var_slice, + to_elem_inspector_lset, + elem_inspector_var, + )), + ); + + // \elem -> toInspector elem + let to_elem_inspector = Closure(ClosureData { + function_type: to_elem_inspector_fn_var, + closure_type: to_elem_inspector_lset, + return_type: elem_inspector_var, + name: to_elem_inspector_sym, + captured_symbols: vec![], + recursive: Recursive::NotRecursive, + arguments: vec![( + elem_var, + AnnotatedMark::known_exhaustive(), + Loc::at_zero(Pattern::Identifier(elem_sym)), + )], + loc_body: Box::new(Loc::at_zero(to_inspector_call)), + }); + + // build `Inspect.list lst (\elem -> Inspect.toInspector elem)` type + // List e, (e -> Inspector fmt) -[uls]-> Inspector fmt where fmt implements InspectorFormatter + let inspect_list_fn_var = env.import_builtin_symbol_var(Symbol::INSPECT_LIST); + + // List elem, List.walk, to_elem_inspector_fn_var -[clos]-> t1 + let list_walk_fn_var = env.import_builtin_symbol_var(Symbol::LIST_WALK); + let this_inspect_list_args_slice = VariableSubsSlice::insert_into_subs( + env.subs, + [list_var, list_walk_fn_var, to_elem_inspector_fn_var], + ); + let this_inspect_list_clos_var = env.subs.fresh_unnamed_flex_var(); // clos + let this_list_inspector_var = env.subs.fresh_unnamed_flex_var(); // t1 + let this_inspect_list_fn_var = synth_var( + env.subs, + Content::Structure(FlatType::Func( + this_inspect_list_args_slice, + this_inspect_list_clos_var, + this_list_inspector_var, + )), + ); + + // List e, (e -> Inspector fmt) -[uls]-> Inspector fmt where fmt implements InspectorFormatter + // ~ List elem, to_elem_inspector_fn_var -[clos]-> t1 + env.unify(inspect_list_fn_var, this_inspect_list_fn_var); + + // Inspect.list : List elem, to_elem_inspector_fn_var -[clos]-> Inspector fmt where fmt implements InspectorFormatter + let inspect_list = AbilityMember(Symbol::INSPECT_LIST, None, this_inspect_list_fn_var); + let inspect_list_fn = Box::new(( + this_inspect_list_fn_var, + Loc::at_zero(inspect_list), + this_inspect_list_clos_var, + this_list_inspector_var, + )); + + // Inspect.list lst to_elem_inspector + let inspect_list_call = Call( + inspect_list_fn, + vec![ + (list_var, Loc::at_zero(Var(lst_sym, list_var))), + ( + list_walk_fn_var, + Loc::at_zero(Var(Symbol::LIST_WALK, list_walk_fn_var)), + ), + (to_elem_inspector_fn_var, Loc::at_zero(to_elem_inspector)), + ], + CalledVia::Space, + ); + + // Inspect.custom \fmt -> Inspect.apply (Inspect.list ..) fmt + let (body, this_inspector_var) = wrap_in_inspect_custom( + env, + inspect_list_call, + this_list_inspector_var, + lst_sym, + list_var, + ); + + // \lst -> Inspect.list lst (\elem -> Inspect.toInspector elem) + // Create fn_var for ambient capture; we fix it up below. + let fn_var = synth_var(env.subs, Content::Error); + + // -[fn_name]-> + let fn_name_labels = UnionLambdas::insert_into_subs(env.subs, once((fn_name, vec![]))); + let fn_clos_var = synth_var( + env.subs, + Content::LambdaSet(LambdaSet { + solved: fn_name_labels, + recursion_var: OptVariable::NONE, + unspecialized: SubsSlice::default(), + ambient_function: fn_var, + }), + ); + // List elem -[fn_name]-> Inspector fmt + let list_var_slice = SubsSlice::insert_into_subs(env.subs, once(list_var)); + env.subs.set_content( + fn_var, + Content::Structure(FlatType::Func( + list_var_slice, + fn_clos_var, + this_inspector_var, + )), + ); + + // \lst -[fn_name]-> Inspect.list lst (\elem -> Inspect.toInspector elem) + let clos = Closure(ClosureData { + function_type: fn_var, + closure_type: fn_clos_var, + return_type: this_inspector_var, + name: fn_name, + captured_symbols: vec![], + recursive: Recursive::NotRecursive, + arguments: vec![( + list_var, + AnnotatedMark::known_exhaustive(), + Loc::at_zero(Pattern::Identifier(lst_sym)), + )], + loc_body: Box::new(Loc::at_zero(body)), + }); + + (clos, fn_var) +} + +fn to_inspector_record( + env: &mut Env<'_>, + record_var: Variable, + fields: RecordFields, + fn_name: Symbol, +) -> (Expr, Variable) { + // Suppose rcd = { a: t1, b: t2 }. Build + // + // \rcd -> Inspect.record [ + // { key: "a", value: Inspect.toInspector rcd.a }, + // { key: "b", value: Inspect.toInspector rcd.b }, + // ] + + let rcd_sym = env.new_symbol("rcd"); + let whole_rcd_var = env.subs.fresh_unnamed_flex_var(); // type of the { key, value } records in the list + + use Expr::*; + + let fields_list = fields + .iter_all() + .map(|(field_name_index, field_var_index, _)| { + let field_name = env.subs[field_name_index].clone(); + let field_var = env.subs[field_var_index]; + let field_var_slice = VariableSubsSlice::new(field_var_index.index, 1); + + // key: "a" + let key_field = Field { + var: Variable::STR, + region: Region::zero(), + loc_expr: Box::new(Loc::at_zero(Str(field_name.as_str().into()))), + }; + + // rcd.a + let field_access = RecordAccess { + record_var, + ext_var: env.subs.fresh_unnamed_flex_var(), + field_var, + loc_expr: Box::new(Loc::at_zero(Var( + rcd_sym, + env.subs.fresh_unnamed_flex_var(), + ))), + field: field_name, + }; + + // build `toInspector rcd.a` type + // val -[uls]-> Inspector fmt where fmt implements InspectorFormatter + let to_inspector_fn_var = env.import_builtin_symbol_var(Symbol::INSPECT_TO_INSPECTOR); + + // (typeof rcd.a) -[clos]-> t1 + let to_inspector_clos_var = env.subs.fresh_unnamed_flex_var(); // clos + let inspector_var = env.subs.fresh_unnamed_flex_var(); // t1 + let this_to_inspector_fn_var = synth_var( + env.subs, + Content::Structure(FlatType::Func( + field_var_slice, + to_inspector_clos_var, + inspector_var, + )), + ); + + // val -[uls]-> Inspector fmt where fmt implements InspectorFormatter + // ~ (typeof rcd.a) -[clos]-> t1 + env.unify(to_inspector_fn_var, this_to_inspector_fn_var); + + // toInspector : (typeof rcd.a) -[clos]-> Inspector fmt where fmt implements InspectorFormatter + let to_inspector_var = + AbilityMember(Symbol::INSPECT_TO_INSPECTOR, None, to_inspector_fn_var); + let to_inspector_fn = Box::new(( + to_inspector_fn_var, + Loc::at_zero(to_inspector_var), + to_inspector_clos_var, + inspector_var, + )); + + // toInspector rcd.a + let to_inspector_call = Call( + to_inspector_fn, + vec![(field_var, Loc::at_zero(field_access))], + CalledVia::Space, + ); + + // value: toInspector rcd.a + let value_field = Field { + var: inspector_var, + region: Region::zero(), + loc_expr: Box::new(Loc::at_zero(to_inspector_call)), + }; + + // { key: "a", value: toInspector rcd.a } + let mut kv = SendMap::default(); + kv.insert("key".into(), key_field); + kv.insert("value".into(), value_field); + + let this_record_fields = RecordFields::insert_into_subs( + env.subs, + (once(("key".into(), RecordField::Required(Variable::STR)))) + .chain(once(("value".into(), RecordField::Required(inspector_var)))), + ); + let this_record_var = synth_var( + env.subs, + Content::Structure(FlatType::Record(this_record_fields, Variable::EMPTY_RECORD)), + ); + // NOTE: must be done to unify the lambda sets under `inspector_var` + env.unify(this_record_var, whole_rcd_var); + + Loc::at_zero(Record { + record_var: whole_rcd_var, + fields: kv, + }) + }) + .collect::>(); + + // typeof [ { key: .., value: .. }, { key: .., value: .. } ] + let fields_rcd_var_slice = VariableSubsSlice::insert_into_subs(env.subs, once(whole_rcd_var)); + let fields_list_var = synth_var( + env.subs, + Content::Structure(FlatType::Apply(Symbol::LIST_LIST, fields_rcd_var_slice)), + ); + + // [ { key: .., value: ..}, .. ] + let fields_list = List { + elem_var: whole_rcd_var, + loc_elems: fields_list, + }; + + // build `Inspect.record [ { key: .., value: ..}, .. ]` type + // List { key : Str, value : Inspector fmt } -[uls]-> Inspector fmt where fmt implements InspectorFormatter + let inspect_record_fn_var = env.import_builtin_symbol_var(Symbol::INSPECT_RECORD); + + // fields_list_var -[clos]-> t1 + let fields_list_var_slice = + VariableSubsSlice::insert_into_subs(env.subs, once(fields_list_var)); + let inspect_record_clos_var = env.subs.fresh_unnamed_flex_var(); // clos + let inspector_var = env.subs.fresh_unnamed_flex_var(); // t1 + let this_inspect_record_fn_var = synth_var( + env.subs, + Content::Structure(FlatType::Func( + fields_list_var_slice, + inspect_record_clos_var, + inspector_var, + )), + ); + + // List { key : Str, value : Inspector fmt } -[uls]-> Inspector fmt where fmt implements InspectorFormatter + // ~ fields_list_var -[clos]-> t1 + env.unify(inspect_record_fn_var, this_inspect_record_fn_var); + + // Inspect.record : fields_list_var -[clos]-> Inspector fmt where fmt implements InspectorFormatter + let inspect_record_var = AbilityMember(Symbol::INSPECT_RECORD, None, inspect_record_fn_var); + let inspect_record_fn = Box::new(( + inspect_record_fn_var, + Loc::at_zero(inspect_record_var), + inspect_record_clos_var, + inspector_var, + )); + + // Inspect.record [ { key: .., value: .. }, .. ] + let inspect_record_call = Call( + inspect_record_fn, + vec![(fields_list_var, Loc::at_zero(fields_list))], + CalledVia::Space, + ); + + // Inspect.custom \fmt -> Inspect.apply (Inspect.record ..) fmt + let (body, this_inspector_var) = + wrap_in_inspect_custom(env, inspect_record_call, inspector_var, rcd_sym, record_var); + + // Create fn_var for ambient capture; we fix it up below. + let fn_var = synth_var(env.subs, Content::Error); + + // -[fn_name]-> + let fn_name_labels = UnionLambdas::insert_into_subs(env.subs, once((fn_name, vec![]))); + let fn_clos_var = synth_var( + env.subs, + Content::LambdaSet(LambdaSet { + solved: fn_name_labels, + recursion_var: OptVariable::NONE, + unspecialized: SubsSlice::default(), + ambient_function: fn_var, + }), + ); + // typeof rcd -[fn_name]-> (typeof Inspect.record [ .. ] = Inspector fmt) + let record_var_slice = SubsSlice::insert_into_subs(env.subs, once(record_var)); + env.subs.set_content( + fn_var, + Content::Structure(FlatType::Func( + record_var_slice, + fn_clos_var, + this_inspector_var, + )), + ); + + // \rcd -[fn_name]-> Inspect.record [ { key: .., value: .. }, .. ] + let clos = Closure(ClosureData { + function_type: fn_var, + closure_type: fn_clos_var, + return_type: this_inspector_var, + name: fn_name, + captured_symbols: vec![], + recursive: Recursive::NotRecursive, + arguments: vec![( + record_var, + AnnotatedMark::known_exhaustive(), + Loc::at_zero(Pattern::Identifier(rcd_sym)), + )], + loc_body: Box::new(Loc::at_zero(body)), + }); + + (clos, fn_var) +} + +fn to_inspector_tuple( + env: &mut Env<'_>, + tuple_var: Variable, + elems: TupleElems, + fn_name: Symbol, +) -> (Expr, Variable) { + // Suppose tup = (t1, t2). Build + // + // \tup -> Inspect.tuple [ + // Inspect.toInspector tup.0, + // Inspect.toInspector tup.1, + // ] + + let tup_sym = env.new_symbol("tup"); + let whole_inspector_in_list_var = env.subs.fresh_unnamed_flex_var(); // type of the inspector in the list + + use Expr::*; + + let elem_inspectors_list = elems + .iter_all() + .map(|(elem_index, elem_var_index)| { + let index = env.subs[elem_index]; + let elem_var = env.subs[elem_var_index]; + let elem_var_slice = VariableSubsSlice::new(elem_var_index.index, 1); + + // tup.0 + let tuple_access = TupleAccess { + tuple_var, + ext_var: env.subs.fresh_unnamed_flex_var(), + elem_var, + loc_expr: Box::new(Loc::at_zero(Var( + tup_sym, + env.subs.fresh_unnamed_flex_var(), + ))), + index, + }; + + // build `toInspector tup.0` type + // val -[uls]-> Inspector fmt where fmt implements InspectorFormatter + let to_inspector_fn_var = env.import_builtin_symbol_var(Symbol::INSPECT_TO_INSPECTOR); + + // (typeof tup.0) -[clos]-> t1 + let to_inspector_clos_var = env.subs.fresh_unnamed_flex_var(); // clos + let inspector_var = env.subs.fresh_unnamed_flex_var(); // t1 + let this_to_inspector_fn_var = synth_var( + env.subs, + Content::Structure(FlatType::Func( + elem_var_slice, + to_inspector_clos_var, + inspector_var, + )), + ); + + // val -[uls]-> Inspector fmt where fmt implements InspectorFormatter + // ~ (typeof tup.0) -[clos]-> t1 + env.unify(to_inspector_fn_var, this_to_inspector_fn_var); + + // toInspector : (typeof tup.0) -[clos]-> Inspector fmt where fmt implements InspectorFormatter + let to_inspector_var = + AbilityMember(Symbol::INSPECT_TO_INSPECTOR, None, to_inspector_fn_var); + let to_inspector_fn = Box::new(( + to_inspector_fn_var, + Loc::at_zero(to_inspector_var), + to_inspector_clos_var, + inspector_var, + )); + + // toInspector tup.0 + let to_inspector_call = Call( + to_inspector_fn, + vec![(elem_var, Loc::at_zero(tuple_access))], + CalledVia::Space, + ); + + // NOTE: must be done to unify the lambda sets under `inspector_var` + env.unify(inspector_var, whole_inspector_in_list_var); + + Loc::at_zero(to_inspector_call) + }) + .collect::>(); + + // typeof [ toInspector tup.0, toInspector tup.1 ] + let whole_inspector_in_list_var_slice = + VariableSubsSlice::insert_into_subs(env.subs, once(whole_inspector_in_list_var)); + let elem_inspectors_list_var = synth_var( + env.subs, + Content::Structure(FlatType::Apply( + Symbol::LIST_LIST, + whole_inspector_in_list_var_slice, + )), + ); + + // [ toInspector tup.0, toInspector tup.1 ] + let elem_inspectors_list = List { + elem_var: whole_inspector_in_list_var, + loc_elems: elem_inspectors_list, + }; + + // build `Inspect.tuple [ toInspector tup.0, toInspector tup.1 ]` type + // List (Inspector fmt) -[uls]-> Inspector fmt where fmt implements InspectorFormatter + let inspect_tuple_fn_var = env.import_builtin_symbol_var(Symbol::INSPECT_TUPLE); + + // elem_inspectors_list_var -[clos]-> t1 + let elem_inspectors_list_var_slice = + VariableSubsSlice::insert_into_subs(env.subs, once(elem_inspectors_list_var)); + let inspect_tuple_clos_var = env.subs.fresh_unnamed_flex_var(); // clos + let inspector_var = env.subs.fresh_unnamed_flex_var(); // t1 + let this_inspect_tuple_fn_var = synth_var( + env.subs, + Content::Structure(FlatType::Func( + elem_inspectors_list_var_slice, + inspect_tuple_clos_var, + inspector_var, + )), + ); + + // List (Inspector fmt) -[uls]-> Inspector fmt where fmt implements InspectorFormatter + // ~ elem_inspectors_list_var -[clos]-> t1 + env.unify(inspect_tuple_fn_var, this_inspect_tuple_fn_var); + + // Inspect.tuple : elem_inspectors_list_var -[clos]-> Inspector fmt where fmt implements InspectorFormatter + let inspect_tuple_var = AbilityMember(Symbol::INSPECT_TUPLE, None, inspect_tuple_fn_var); + let inspect_tuple_fn = Box::new(( + inspect_tuple_fn_var, + Loc::at_zero(inspect_tuple_var), + inspect_tuple_clos_var, + inspector_var, + )); + + // Inspect.tuple [ { key: .., value: .. }, .. ] + let inspect_tuple_call = Call( + inspect_tuple_fn, + vec![(elem_inspectors_list_var, Loc::at_zero(elem_inspectors_list))], + CalledVia::Space, + ); + + // Inspect.custom \fmt -> Inspect.apply (Inspect.tuple_var ..) fmt + let (body, this_inspector_var) = + wrap_in_inspect_custom(env, inspect_tuple_call, inspector_var, tup_sym, tuple_var); + + // Create fn_var for ambient capture; we fix it up below. + let fn_var = synth_var(env.subs, Content::Error); + + // -[fn_name]-> + let fn_name_labels = UnionLambdas::insert_into_subs(env.subs, once((fn_name, vec![]))); + let fn_clos_var = synth_var( + env.subs, + Content::LambdaSet(LambdaSet { + solved: fn_name_labels, + recursion_var: OptVariable::NONE, + unspecialized: SubsSlice::default(), + ambient_function: fn_var, + }), + ); + // typeof tup -[fn_name]-> (typeof Inspect.tuple [ .. ] = Inspector fmt) + let tuple_var_slice = SubsSlice::insert_into_subs(env.subs, once(tuple_var)); + env.subs.set_content( + fn_var, + Content::Structure(FlatType::Func( + tuple_var_slice, + fn_clos_var, + this_inspector_var, + )), + ); + + // \tup -[fn_name]-> Inspect.tuple [ { key: .., value: .. }, .. ] + let clos = Closure(ClosureData { + function_type: fn_var, + closure_type: fn_clos_var, + return_type: this_inspector_var, + name: fn_name, + captured_symbols: vec![], + recursive: Recursive::NotRecursive, + arguments: vec![( + tuple_var, + AnnotatedMark::known_exhaustive(), + Loc::at_zero(Pattern::Identifier(tup_sym)), + )], + loc_body: Box::new(Loc::at_zero(body)), + }); + + (clos, fn_var) +} + +fn to_inspector_tag_union( + env: &mut Env<'_>, + tag_union_var: Variable, + tags: UnionTags, + fn_name: Symbol, +) -> (Expr, Variable) { + // Suppose tag = [ A t1 t2, B t3 ]. Build + // + // \tag -> when tag is + // A v1 v2 -> Inspect.tag "A" [ Inspect.toInspector v1, Inspect.toInspector v2 ] + // B v3 -> Inspect.tag "B" [ Inspect.toInspector v3 ] + + let tag_sym = env.new_symbol("tag"); + let whole_tag_inspectors_var = env.subs.fresh_unnamed_flex_var(); // type of the Inspect.tag ... calls in the branch bodies + + use Expr::*; + + let branches = tags + .iter_all() + .map(|(tag_name_index, tag_vars_slice_index)| { + // A + let tag_name = &env.subs[tag_name_index].clone(); + let vars_slice = env.subs[tag_vars_slice_index]; + // t1 t2 + let payload_vars = env.subs.get_subs_slice(vars_slice).to_vec(); + // v1 v2 + let payload_syms: Vec<_> = std::iter::repeat_with(|| env.unique_symbol()) + .take(payload_vars.len()) + .collect(); + + // `A v1 v2` pattern + let pattern = Pattern::AppliedTag { + whole_var: tag_union_var, + tag_name: tag_name.clone(), + ext_var: Variable::EMPTY_TAG_UNION, + // (t1, v1) (t2, v2) + arguments: (payload_vars.iter()) + .zip(payload_syms.iter()) + .map(|(var, sym)| (*var, Loc::at_zero(Pattern::Identifier(*sym)))) + .collect(), + }; + let branch_pattern = WhenBranchPattern { + pattern: Loc::at_zero(pattern), + degenerate: false, + }; + + // whole type of the elements in [ Inspect.toInspector v1, Inspect.toInspector v2 ] + let whole_payload_inspectors_var = env.subs.fresh_unnamed_flex_var(); + // [ Inspect.toInspector v1, Inspect.toInspector v2 ] + let payload_to_inspectors = (payload_syms.iter()) + .zip(payload_vars.iter()) + .map(|(&sym, &sym_var)| { + // build `toInspector v1` type + // expected: val -[uls]-> Inspector fmt where fmt implements InspectorFormatter + let to_inspector_fn_var = + env.import_builtin_symbol_var(Symbol::INSPECT_TO_INSPECTOR); + + // wanted: t1 -[clos]-> t' + let var_slice_of_sym_var = + VariableSubsSlice::insert_into_subs(env.subs, [sym_var]); // [ t1 ] + let to_inspector_clos_var = env.subs.fresh_unnamed_flex_var(); // clos + let inspector_var = env.subs.fresh_unnamed_flex_var(); // t' + let this_to_inspector_fn_var = synth_var( + env.subs, + Content::Structure(FlatType::Func( + var_slice_of_sym_var, + to_inspector_clos_var, + inspector_var, + )), + ); + + // val -[uls]-> Inspector fmt where fmt implements InspectorFormatter + // ~ t1 -[clos]-> t' + env.unify(to_inspector_fn_var, this_to_inspector_fn_var); + + // toInspector : t1 -[clos]-> Inspector fmt where fmt implements InspectorFormatter + let to_inspector_var = + AbilityMember(Symbol::INSPECT_TO_INSPECTOR, None, this_to_inspector_fn_var); + let to_inspector_fn = Box::new(( + this_to_inspector_fn_var, + Loc::at_zero(to_inspector_var), + to_inspector_clos_var, + inspector_var, + )); + + // toInspector rcd.a + let to_inspector_call = Call( + to_inspector_fn, + vec![(sym_var, Loc::at_zero(Var(sym, sym_var)))], + CalledVia::Space, + ); + + // NOTE: must be done to unify the lambda sets under `inspector_var` + env.unify(inspector_var, whole_payload_inspectors_var); + + Loc::at_zero(to_inspector_call) + }) + .collect(); + + // typeof [ Inspect.toInspector v1, Inspect.toInspector v2 ] + let whole_inspectors_var_slice = + VariableSubsSlice::insert_into_subs(env.subs, [whole_payload_inspectors_var]); + let payload_inspectors_list_var = synth_var( + env.subs, + Content::Structure(FlatType::Apply( + Symbol::LIST_LIST, + whole_inspectors_var_slice, + )), + ); + + // [ Inspect.toInspector v1, Inspect.toInspector v2 ] + let payload_inspectors_list = List { + elem_var: whole_payload_inspectors_var, + loc_elems: payload_to_inspectors, + }; + + // build `Inspect.tag "A" [ ... ]` type + // expected: Str, List (Inspector fmt) -[uls]-> Inspector fmt where fmt implements InspectorFormatter + let inspect_tag_fn_var = env.import_builtin_symbol_var(Symbol::INSPECT_TAG); + + // wanted: Str, List whole_inspectors_var -[clos]-> t' + let this_inspect_tag_args_var_slice = VariableSubsSlice::insert_into_subs( + env.subs, + [Variable::STR, payload_inspectors_list_var], + ); + let this_inspect_tag_clos_var = env.subs.fresh_unnamed_flex_var(); // -[clos]-> + let this_inspector_var = env.subs.fresh_unnamed_flex_var(); // t' + let this_inspect_tag_fn_var = synth_var( + env.subs, + Content::Structure(FlatType::Func( + this_inspect_tag_args_var_slice, + this_inspect_tag_clos_var, + this_inspector_var, + )), + ); + + // Str, List (Inspector fmt) -[uls]-> Inspector fmt where fmt implements InspectorFormatter + // ~ Str, List whole_inspectors_var -[clos]-> t' + env.unify(inspect_tag_fn_var, this_inspect_tag_fn_var); + + // Inspect.tag : Str, List whole_inspectors_var -[clos]-> Inspector fmt where fmt implements InspectorFormatter + let inspect_tag_var = AbilityMember(Symbol::INSPECT_TAG, None, this_inspect_tag_fn_var); + let inspect_tag_fn = Box::new(( + this_inspect_tag_fn_var, + Loc::at_zero(inspect_tag_var), + this_inspect_tag_clos_var, + this_inspector_var, + )); + + // Inspect.tag "A" [ Inspect.toInspector v1, Inspect.toInspector v2 ] + let inspect_tag_call = Call( + inspect_tag_fn, + vec![ + // (Str, "A") + (Variable::STR, Loc::at_zero(Str(tag_name.0.as_str().into()))), + // (List (Inspector fmt), [ Inspect.toInspector v1, Inspect.toInspector v2 ]) + ( + payload_inspectors_list_var, + Loc::at_zero(payload_inspectors_list), + ), + ], + CalledVia::Space, + ); + + // NOTE: must be done to unify the lambda sets under `inspector_var` + // Inspect.tag "A" [ Inspect.toInspector v1, Inspect.toInspector v2 ] ~ whole_inspectors + env.unify(this_inspector_var, whole_tag_inspectors_var); + + WhenBranch { + patterns: vec![branch_pattern], + value: Loc::at_zero(inspect_tag_call), + guard: None, + redundant: RedundantMark::known_non_redundant(), + } + }) + .collect::>(); + + // when tag is + // A v1 v2 -> Inspect.tag "A" [ Inspect.toInspector v1, Inspect.toInspector v2 ] + // B v3 -> Inspect.tag "B" [ Inspect.toInspector v3 ] + let when_branches = When { + loc_cond: Box::new(Loc::at_zero(Var(tag_sym, tag_union_var))), + cond_var: tag_union_var, + expr_var: whole_tag_inspectors_var, + region: Region::zero(), + branches, + branches_cond_var: tag_union_var, + exhaustive: ExhaustiveMark::known_exhaustive(), + }; + + // Inspect.custom \fmt -> Inspect.apply (when ..) fmt + let (body, this_inspector_var) = wrap_in_inspect_custom( + env, + when_branches, + whole_tag_inspectors_var, + tag_sym, + tag_union_var, + ); + + // Create fn_var for ambient capture; we fix it up below. + let fn_var = synth_var(env.subs, Content::Error); + + // -[fn_name]-> + let fn_name_labels = UnionLambdas::insert_into_subs(env.subs, once((fn_name, vec![]))); + let fn_clos_var = synth_var( + env.subs, + Content::LambdaSet(LambdaSet { + solved: fn_name_labels, + recursion_var: OptVariable::NONE, + unspecialized: SubsSlice::default(), + ambient_function: fn_var, + }), + ); + // tag_union_var -[fn_name]-> whole_tag_inspectors_var + let tag_union_var_slice = SubsSlice::insert_into_subs(env.subs, once(tag_union_var)); + env.subs.set_content( + fn_var, + Content::Structure(FlatType::Func( + tag_union_var_slice, + fn_clos_var, + this_inspector_var, + )), + ); + + // \tag -> + // Inspect.custom \fmt -> Inspect.apply ( + // when tag is + // A v1 v2 -> Inspect.tag "A" [ Inspect.toInspector v1, Inspect.toInspector v2 ] + // B v3 -> Inspect.tag "B" [ Inspect.toInspector v3 ]) + // fmt + let clos = Closure(ClosureData { + function_type: fn_var, + closure_type: fn_clos_var, + return_type: this_inspector_var, + name: fn_name, + captured_symbols: vec![], + recursive: Recursive::NotRecursive, + arguments: vec![( + tag_union_var, + AnnotatedMark::known_exhaustive(), + Loc::at_zero(Pattern::Identifier(tag_sym)), + )], + loc_body: Box::new(Loc::at_zero(body)), + }); + + (clos, fn_var) +} + +/// Lift `inspector` to `Inspect.custom \fmt -> Inspect.apply inspector fmt` +fn wrap_in_inspect_custom( + env: &mut Env, + inspector: Expr, + inspector_var: Variable, + captured_symbol: Symbol, + captured_var: Variable, +) -> (Expr, Variable) { + use Expr::*; + + let fn_name = env.new_symbol("custom"); + + // fmt: fmt where fmt implements InspectorFormatter + let fmt_sym = env.new_symbol("fmt"); + let fmt_var = env.subs.fresh_unnamed_flex_var(); + + // build `Inspect.apply inspector fmt` type + // expected: Inspect.apply : Inspector fmt, fmt -[apply]-> fmt where fmt implements InspectorFormatter + let apply_fn_var = env.import_builtin_symbol_var(Symbol::INSPECT_APPLY); + + // wanted: Inspect.apply : inspector_var, fmt -[clos]-> fmt where fmt implements InspectorFormatter + let this_apply_args_var_slice = + VariableSubsSlice::insert_into_subs(env.subs, [inspector_var, fmt_var]); + let this_apply_clos_var = env.subs.fresh_unnamed_flex_var(); // -[clos]-> + let this_apply_fn_var = synth_var( + env.subs, + Content::Structure(FlatType::Func( + this_apply_args_var_slice, + this_apply_clos_var, + fmt_var, + )), + ); + + // Inspector fmt, fmt -[apply]-> ft where fmt implements InspectorFormatter + // ~ inspector_var, fmt -[clos]-> fmt where fmt implements InspectorFormatter + env.unify(apply_fn_var, this_apply_fn_var); + + // Inspect.apply : inspector_var, fmt -[apply]-> fmt where fmt implements InspectorFormatter + let apply_fn = Box::new(( + this_apply_fn_var, + Loc::at_zero(Var(Symbol::INSPECT_APPLY, this_apply_fn_var)), + this_apply_clos_var, + fmt_var, + )); + + // Inspect.apply inspector fmt + let apply_call = Call( + apply_fn, + vec![ + // (inspector_var, inspector) + (inspector_var, Loc::at_zero(inspector)), + // (fmt, fmt_var) + (fmt_var, Loc::at_zero(Var(fmt_sym, fmt_var))), + ], + CalledVia::Space, + ); + + // Create fn_var for ambient capture; we fix it up below. + let fn_var = synth_var(env.subs, Content::Error); + + // -[[FN_name captured_var]]-> + let fn_name_labels = + UnionLambdas::insert_into_subs(env.subs, once((fn_name, vec![captured_var]))); + let fn_clos_var = synth_var( + env.subs, + Content::LambdaSet(LambdaSet { + solved: fn_name_labels, + recursion_var: OptVariable::NONE, + unspecialized: SubsSlice::default(), + ambient_function: fn_var, + }), + ); + + // fmt -[[FN_name captured_var]]-> Inspect.apply inspector fmt + let args_slice = SubsSlice::insert_into_subs(env.subs, vec![fmt_var]); + env.subs.set_content( + fn_var, + Content::Structure(FlatType::Func(args_slice, fn_clos_var, fmt_var)), + ); + + // \fmt -[[fn_name captured_var]]-> Inspect.apply inspector fmt + let clos = Closure(ClosureData { + function_type: fn_var, + closure_type: fn_clos_var, + return_type: fmt_var, + name: fn_name, + captured_symbols: vec![(captured_symbol, captured_var)], + recursive: Recursive::NotRecursive, + arguments: vec![( + fmt_var, + AnnotatedMark::known_exhaustive(), + Loc::at_zero(Pattern::Identifier(fmt_sym)), + )], + loc_body: Box::new(Loc::at_zero(apply_call)), + }); + + // Build + // Inspect.custom \fmt -> Inspect.apply inspector fmt + // + // expected: Inspect.custom : (fmt -> fmt) -> Inspector fmt where fmt implements InspectorFormatter + let custom_fn_var = env.import_builtin_symbol_var(Symbol::INSPECT_CUSTOM); + + // wanted: Inspect.custom : fn_var -[clos]-> t' + let this_custom_args_var_slice = VariableSubsSlice::insert_into_subs(env.subs, [fn_var]); + let this_custom_clos_var = env.subs.fresh_unnamed_flex_var(); // -[clos]-> + let this_custom_inspector_var = env.subs.fresh_unnamed_flex_var(); // t' + let this_custom_fn_var = synth_var( + env.subs, + Content::Structure(FlatType::Func( + this_custom_args_var_slice, + this_custom_clos_var, + this_custom_inspector_var, + )), + ); + + // (fmt -> fmt) -[..]-> Inspector fmt where fmt implements InspectorFormatter + // ~ fn_var -[clos]-> t' + env.unify(custom_fn_var, this_custom_fn_var); + + // Inspect.custom : (fmt -> fmt) -> Inspector fmt where fmt implements InspectorFormatter + let custom_fn = Box::new(( + this_custom_fn_var, + Loc::at_zero(Var(Symbol::INSPECT_CUSTOM, this_custom_fn_var)), + this_custom_clos_var, // -[clos]-> + this_custom_inspector_var, // t' ~ Inspector fmt + )); + + // Inspect.custom \fmt -> Inspect.apply inspector fmt + let custom_call = Call( + custom_fn, + vec![(fn_var, Loc::at_zero(clos))], + CalledVia::Space, + ); + + (custom_call, this_custom_inspector_var) +} diff --git a/crates/compiler/derive/src/lib.rs b/crates/compiler/derive/src/lib.rs new file mode 100644 index 0000000000..fdb4679d6e --- /dev/null +++ b/crates/compiler/derive/src/lib.rs @@ -0,0 +1,228 @@ +//! Auto-derivers of builtin ability methods. + +use std::iter::once; +use std::sync::{Arc, Mutex}; + +use roc_can::abilities::SpecializationLambdaSets; +use roc_can::expr::Expr; +use roc_can::pattern::Pattern; +use roc_can::{def::Def, module::ExposedByModule}; +use roc_collections::{MutMap, VecMap}; +use roc_derive_key::DeriveKey; +use roc_module::symbol::{IdentIds, ModuleId, Symbol}; +use roc_region::all::Loc; +use roc_types::subs::{ + copy_import_to, Content, Descriptor, Mark, OptVariable, Rank, Subs, Variable, +}; +use util::Env; + +mod decoding; +mod encoding; +mod hash; +mod inspect; +mod util; + +pub(crate) const DERIVED_SYNTH: ModuleId = ModuleId::DERIVED_SYNTH; + +pub fn synth_var(subs: &mut Subs, content: Content) -> Variable { + let descriptor = Descriptor { + content, + // NOTE: this is incorrect, but that is irrelevant - derivers may only be called during + // monomorphization (or later), at which point we do not care about variable + // generalization. Hence ranks should not matter. + rank: Rank::toplevel(), + mark: Mark::NONE, + copy: OptVariable::NONE, + }; + subs.fresh(descriptor) +} + +/// Map of [`DeriveKey`]s to their derived symbols. +/// +/// This represents the [`Derived_synth`][Symbol::DERIVED_SYNTH] module. +#[derive(Debug, Default)] +pub struct DerivedModule { + map: MutMap, + subs: Subs, + derived_ident_ids: IdentIds, +} + +pub(crate) struct DerivedBody { + pub body: Expr, + pub body_type: Variable, + /// mapping of lambda set region -> the specialization lambda set for this derived body + pub specialization_lambda_sets: SpecializationLambdaSets, +} + +fn build_derived_body( + derived_subs: &mut Subs, + derived_ident_ids: &mut IdentIds, + exposed_by_module: &ExposedByModule, + derived_symbol: Symbol, + derive_key: DeriveKey, +) -> (Def, SpecializationLambdaSets) { + let mut env = Env { + subs: derived_subs, + exposed_types: exposed_by_module, + derived_ident_ids, + }; + + let DerivedBody { + body, + body_type, + specialization_lambda_sets, + } = match derive_key { + DeriveKey::ToEncoder(to_encoder_key) => { + encoding::derive_to_encoder(&mut env, to_encoder_key, derived_symbol) + } + DeriveKey::Decoder(decoder_key) => { + decoding::derive_decoder(&mut env, decoder_key, derived_symbol) + } + DeriveKey::Hash(hash_key) => hash::derive_hash(&mut env, hash_key, derived_symbol), + DeriveKey::ToInspector(to_inspector_key) => { + inspect::derive_to_inspector(&mut env, to_inspector_key, derived_symbol) + } + }; + + let def = Def { + loc_pattern: Loc::at_zero(Pattern::Identifier(derived_symbol)), + loc_expr: Loc::at_zero(body), + expr_var: body_type, + pattern_vars: once((derived_symbol, body_type)).collect(), + annotation: None, + }; + + (def, specialization_lambda_sets) +} + +impl DerivedModule { + pub fn get_or_insert( + &mut self, + // TODO: we only need "exposed by builtin modules that expose builtin abilities" + exposed_by_module: &ExposedByModule, + key: DeriveKey, + ) -> &(Symbol, Def, SpecializationLambdaSets) { + if let Some(entry) = self.map.get(&key) { + // rustc won't let us return an immutable reference *and* continue using + // `self.map` immutably below, but this is safe, because we are not returning + // an immutable reference to the entry. + return unsafe { std::mem::transmute(entry) }; + } + + let ident_id = if cfg!(debug_assertions) || cfg!(feature = "debug-derived-symbols") { + let debug_name = key.debug_name(); + let ident_id = self.derived_ident_ids.get_or_insert(&debug_name); + + // This is expensive, but yields much better symbols when debugging. + // TODO: hide behind debug_flags? + DERIVED_SYNTH.register_debug_idents(&self.derived_ident_ids); + + ident_id + } else { + self.derived_ident_ids.gen_unique() + }; + + let derived_symbol = Symbol::new(DERIVED_SYNTH, ident_id); + let (derived_def, specialization_lsets) = build_derived_body( + &mut self.subs, + &mut self.derived_ident_ids, + exposed_by_module, + derived_symbol, + key.clone(), + ); + + let triple = (derived_symbol, derived_def, specialization_lsets); + self.map.entry(key).or_insert(triple) + } + + pub fn is_derived_def(&self, def_symbol: Symbol) -> bool { + self.map + .iter() + .any(|(_, (symbol, _, _))| *symbol == def_symbol) + } + + pub fn iter_all( + &self, + ) -> impl Iterator { + self.map.iter() + } + + /// Generate a unique symbol. This should only be used when generating code inside the Derived + /// module; other modules should use [`Self::get_or_insert`] to generate a symbol for a derived + /// ability member usage. + pub fn gen_unique(&mut self) -> Symbol { + let ident_id = self.derived_ident_ids.gen_unique(); + Symbol::new(DERIVED_SYNTH, ident_id) + } + + // TODO: just pass and copy the ambient function directly, don't pass the lambda set var. + pub fn copy_lambda_set_ambient_function_to_subs( + &self, + lambda_set_var: Variable, + target: &mut Subs, + _target_rank: Rank, + ) -> Variable { + let ambient_function_var = self.subs.get_lambda_set(lambda_set_var).ambient_function; + + let copied_import = copy_import_to( + &self.subs, + target, + // bookkeep unspecialized lambda sets of var - I think we want this here + true, + ambient_function_var, + // TODO: I think this is okay because the only use of `copy_lambda_set_var_to_subs` + // (at least right now) is for lambda set compaction, which will automatically unify + // and lower ranks, and never generalize. + // + // However this is a bad coupling and maybe not a good assumption, we should revisit + // this when possible. + Rank::import(), + // target_rank, + ); + + copied_import.variable + } + + /// Gets the derived defs that should be loaded into the derived gen module, skipping over the + /// defs that have already been loaded. + pub fn iter_load_for_gen_module( + &mut self, + gen_subs: &mut Subs, + should_load_def: impl Fn(Symbol) -> bool, + ) -> VecMap { + self.map + .values() + .filter_map(|(symbol, def, _)| { + if should_load_def(*symbol) { + let (new_expr_var, new_expr) = roc_can::copy::deep_copy_expr_across_subs( + &mut self.subs, + gen_subs, + def.expr_var, + &def.loc_expr.value, + ); + Some((*symbol, (new_expr, new_expr_var))) + } else { + None + } + }) + .collect() + } + + /// # Safety + /// + /// Prefer using a fresh Derived module with [`Derived::default`]. Use this only in testing. + pub unsafe fn from_components(subs: Subs, ident_ids: IdentIds) -> Self { + Self { + map: Default::default(), + subs, + derived_ident_ids: ident_ids, + } + } + + pub fn decompose(self) -> (Subs, IdentIds) { + (self.subs, self.derived_ident_ids) + } +} + +/// Thread-sharable [`DerivedModule`]. +pub type SharedDerivedModule = Arc>; diff --git a/crates/compiler/derive/src/util.rs b/crates/compiler/derive/src/util.rs new file mode 100644 index 0000000000..14e3d9518e --- /dev/null +++ b/crates/compiler/derive/src/util.rs @@ -0,0 +1,204 @@ +use roc_can::{abilities::SpecializationLambdaSets, module::ExposedByModule}; +use roc_checkmate::with_checkmate; +use roc_error_macros::internal_error; +use roc_module::symbol::{IdentIds, Symbol}; +use roc_solve_schema::UnificationMode; +use roc_types::{ + subs::{instantiate_rigids, Subs, Variable}, + types::Polarity, +}; + +use crate::DERIVED_SYNTH; + +/// An environment representing the Derived_synth module, for use in building derived +/// implementations. +pub(crate) struct Env<'a> { + /// NB: This **must** be subs for the derive module! + pub subs: &'a mut Subs, + pub exposed_types: &'a ExposedByModule, + pub derived_ident_ids: &'a mut IdentIds, +} + +impl Env<'_> { + pub fn new_symbol(&mut self, name_hint: impl std::string::ToString) -> Symbol { + if cfg!(any( + debug_assertions, + test, + feature = "debug-derived-symbols" + )) { + let mut i = 0; + let hint = name_hint.to_string(); + let debug_name = loop { + i += 1; + let name = if i == 1 { + hint.clone() + } else { + format!("{hint}{i}") + }; + if self.derived_ident_ids.get_id(&name).is_none() { + break name; + } + }; + + let ident_id = self.derived_ident_ids.get_or_insert(&debug_name); + + Symbol::new(DERIVED_SYNTH, ident_id) + } else { + self.unique_symbol() + } + } + + pub fn unique_symbol(&mut self) -> Symbol { + let ident_id = self.derived_ident_ids.gen_unique(); + Symbol::new(DERIVED_SYNTH, ident_id) + } + + pub fn import_builtin_symbol_var(&mut self, symbol: Symbol) -> Variable { + let module_id = symbol.module_id(); + debug_assert!(module_id.is_builtin()); + + let module_types = &self + .exposed_types + .get(&module_id) + .unwrap() + .exposed_types_storage_subs; + let storage_var = module_types.stored_vars_by_symbol.get(&symbol).unwrap(); + let imported = module_types + .storage_subs + .export_variable_to_directly_to_use_site(self.subs, *storage_var); + + instantiate_rigids(self.subs, imported.variable); + + imported.variable + } + + pub fn unify(&mut self, left: Variable, right: Variable) { + use roc_unify::{ + unify::{unify, Unified}, + Env, + }; + + let unified = unify( + // TODO(checkmate): pass checkmate through + &mut with_checkmate!({ + on => Env::new(self.subs, None), + off => Env::new(self.subs), + }), + left, + right, + UnificationMode::EQ, + Polarity::OF_PATTERN, + ); + + match unified { + Unified::Success { + vars: _, + must_implement_ability: _, + lambda_sets_to_specialize, + extra_metadata: _, + } => { + if !lambda_sets_to_specialize.is_empty() { + internal_error!("Did not expect derivers to need to specialize unspecialized lambda sets, but we got some: {:?}", lambda_sets_to_specialize) + } + } + Unified::Failure(..) => { + internal_error!("Unification failed in deriver - that's a deriver bug!") + } + } + } + + pub fn get_specialization_lambda_sets( + &mut self, + specialization_type: Variable, + ability_member: Symbol, + ) -> SpecializationLambdaSets { + use roc_unify::{ + unify::{unify_introduced_ability_specialization, Unified}, + Env, + }; + + let member_signature = self.import_builtin_symbol_var(ability_member); + + let unified = unify_introduced_ability_specialization( + // TODO(checkmate): pass checkmate through + &mut with_checkmate!({ + on => Env::new(self.subs, None), + off => Env::new(self.subs), + }), + member_signature, + specialization_type, + UnificationMode::EQ, + ); + + match unified { + Unified::Success { + vars: _, + must_implement_ability: _, + lambda_sets_to_specialize: _lambda_sets_to_specialize, + extra_metadata: specialization_lsets, + } => { + let specialization_lsets: SpecializationLambdaSets = specialization_lsets + .0 + .into_iter() + .map(|((spec_member, region), var)| { + debug_assert_eq!(spec_member, ability_member); + (region, var) + }) + .collect(); + + // Since we're doing `{foo} ~ a where a implements Encoding`, we may see "lambda sets to + // specialize" for e.g. `{foo}:toEncoder:1`, but these are actually just the + // specialization lambda sets, so we don't need to do any extra work! + // + // If there are other lambda sets to specialize in here, that's unexpected, because + // that means we would have been deriving something like `toEncoder {foo: bar}`, + // and now seen that we needed `toEncoder bar` where `bar` is a concrete type. But + // we only expect `bar` to polymorphic at this stage! + // + // TODO: it would be better if `unify` could prune these for us. See also + // https://github.com/roc-lang/roc/issues/3207; that is a blocker for this TODO. + #[cfg(debug_assertions)] + { + for (spec_var, lambda_sets) in _lambda_sets_to_specialize.drain() { + for lambda_set in lambda_sets { + let belongs_to_specialized_lambda_sets = + specialization_lsets.iter().any(|(_, var)| { + self.subs.get_root_key_without_compacting(*var) + == self.subs.get_root_key_without_compacting(lambda_set) + }); + debug_assert!(belongs_to_specialized_lambda_sets, + "Did not expect derivers to need to specialize unspecialized lambda sets, but we got one: {lambda_set:?} for {spec_var:?}") + } + } + } + specialization_lsets + } + Unified::Failure(..) => { + internal_error!("Unification failed in deriver - that's a deriver bug!") + } + } + } + + /// Creates an extension variable for a tag union or record. + /// + /// Derivers should always construct tag union and record types such that they are closed. + /// If the `open-extension-vars` feature is turned on, flex extension vars will be + /// returned; otherwise, the appropriate closed extension variable for the type will be + /// returned. + #[inline(always)] + pub fn new_ext_var(&mut self, kind: ExtensionKind) -> Variable { + if cfg!(feature = "open-extension-vars") { + self.subs.fresh_unnamed_flex_var() + } else { + match kind { + ExtensionKind::Record => Variable::EMPTY_RECORD, + ExtensionKind::TagUnion => Variable::EMPTY_TAG_UNION, + } + } + } +} + +pub(crate) enum ExtensionKind { + Record, + TagUnion, +} diff --git a/crates/compiler/derive_key/Cargo.toml b/crates/compiler/derive_key/Cargo.toml new file mode 100644 index 0000000000..a8f9a5a826 --- /dev/null +++ b/crates/compiler/derive_key/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "roc_derive_key" + +authors.workspace = true +edition.workspace = true +license.workspace = true +version.workspace = true + +[dependencies] +roc_collections = { path = "../collections" } +roc_error_macros = { path = "../../error_macros" } +roc_module = { path = "../module" } +roc_region = { path = "../region" } +roc_types = { path = "../types" } diff --git a/crates/compiler/derive_key/src/decoding.rs b/crates/compiler/derive_key/src/decoding.rs new file mode 100644 index 0000000000..509a0bf995 --- /dev/null +++ b/crates/compiler/derive_key/src/decoding.rs @@ -0,0 +1,135 @@ +use roc_module::{ident::Lowercase, symbol::Symbol}; +use roc_types::subs::{Content, FlatType, Subs, Variable}; + +use crate::{ + util::{check_derivable_ext_var, debug_name_record, debug_name_tuple}, + DeriveError, +}; + +#[derive(Hash)] +pub enum FlatDecodable { + Immediate(Symbol), + Key(FlatDecodableKey), +} + +#[derive(Hash, PartialEq, Eq, Debug, Clone)] +pub enum FlatDecodableKey { + List(/* takes one variable */), + + // Unfortunate that we must allocate here, c'est la vie + Record(Vec), + Tuple(u32), +} + +impl FlatDecodableKey { + pub(crate) fn debug_name(&self) -> String { + match self { + FlatDecodableKey::List() => "list".to_string(), + FlatDecodableKey::Record(fields) => debug_name_record(fields), + FlatDecodableKey::Tuple(arity) => debug_name_tuple(*arity), + } + } +} + +impl FlatDecodable { + pub(crate) fn from_var(subs: &Subs, var: Variable) -> Result { + use DeriveError::*; + use FlatDecodable::*; + match *subs.get_content_without_compacting(var) { + Content::Structure(flat_type) => match flat_type { + FlatType::Apply(sym, _) => match sym { + Symbol::LIST_LIST => Ok(Key(FlatDecodableKey::List())), + Symbol::STR_STR => Ok(Immediate(Symbol::DECODE_STRING)), + _ => Err(Underivable), + }, + FlatType::Record(fields, ext) => { + let (fields_iter, ext) = fields.unsorted_iterator_and_ext(subs, ext); + + check_derivable_ext_var(subs, ext, |ext| { + matches!(ext, Content::Structure(FlatType::EmptyRecord)) + })?; + + let mut field_names = Vec::with_capacity(fields.len()); + for (field_name, record_field) in fields_iter { + if record_field.is_optional() { + // Can't derive a concrete decoder for optional fields, since those are + // compile-time-polymorphic + return Err(Underivable); + } + field_names.push(field_name.clone()); + } + + field_names.sort(); + + Ok(Key(FlatDecodableKey::Record(field_names))) + } + FlatType::Tuple(elems, ext) => { + let (elems_iter, ext) = elems.sorted_iterator_and_ext(subs, ext); + + check_derivable_ext_var(subs, ext, |ext| { + matches!(ext, Content::Structure(FlatType::EmptyTuple)) + })?; + + Ok(Key(FlatDecodableKey::Tuple(elems_iter.count() as _))) + } + FlatType::TagUnion(_tags, _ext) | FlatType::RecursiveTagUnion(_, _tags, _ext) => { + Err(Underivable) // yet + } + FlatType::FunctionOrTagUnion(_name_index, _, _) => { + Err(Underivable) // yet + } + FlatType::EmptyRecord => Ok(Key(FlatDecodableKey::Record(vec![]))), + FlatType::EmptyTuple => todo!(), + FlatType::EmptyTagUnion => { + Err(Underivable) // yet + } + // + FlatType::Func(..) => Err(Underivable), + }, + Content::Alias(sym, _, real_var, _) => match from_builtin_symbol(sym) { + Some(lambda) => lambda, + // NB: I believe it is okay to unwrap opaques here because derivers are only used + // by the backend, and the backend treats opaques like structural aliases. + None => Self::from_var(subs, real_var), + }, + Content::RangedNumber(range) => { + Self::from_var(subs, range.default_compilation_variable()) + } + // + Content::RecursionVar { structure, .. } => Self::from_var(subs, structure), + // + Content::Error => Err(Underivable), + Content::FlexVar(_) + | Content::RigidVar(_) + | Content::FlexAbleVar(_, _) + | Content::RigidAbleVar(_, _) => Err(UnboundVar), + Content::LambdaSet(_) | Content::ErasedLambda => Err(Underivable), + } + } + + pub(crate) fn from_builtin_symbol(symbol: Symbol) -> Result { + from_builtin_symbol(symbol).unwrap_or(Err(DeriveError::Underivable)) + } +} + +const fn from_builtin_symbol(symbol: Symbol) -> Option> { + use FlatDecodable::*; + match symbol { + Symbol::BOOL_BOOL => Some(Ok(Immediate(Symbol::DECODE_BOOL))), + Symbol::NUM_U8 | Symbol::NUM_UNSIGNED8 => Some(Ok(Immediate(Symbol::DECODE_U8))), + Symbol::NUM_U16 | Symbol::NUM_UNSIGNED16 => Some(Ok(Immediate(Symbol::DECODE_U16))), + Symbol::NUM_U32 | Symbol::NUM_UNSIGNED32 => Some(Ok(Immediate(Symbol::DECODE_U32))), + Symbol::NUM_U64 | Symbol::NUM_UNSIGNED64 => Some(Ok(Immediate(Symbol::DECODE_U64))), + Symbol::NUM_U128 | Symbol::NUM_UNSIGNED128 => Some(Ok(Immediate(Symbol::DECODE_U128))), + Symbol::NUM_I8 | Symbol::NUM_SIGNED8 => Some(Ok(Immediate(Symbol::DECODE_I8))), + Symbol::NUM_I16 | Symbol::NUM_SIGNED16 => Some(Ok(Immediate(Symbol::DECODE_I16))), + Symbol::NUM_I32 | Symbol::NUM_SIGNED32 => Some(Ok(Immediate(Symbol::DECODE_I32))), + Symbol::NUM_I64 | Symbol::NUM_SIGNED64 => Some(Ok(Immediate(Symbol::DECODE_I64))), + Symbol::NUM_I128 | Symbol::NUM_SIGNED128 => Some(Ok(Immediate(Symbol::DECODE_I128))), + Symbol::NUM_DEC | Symbol::NUM_DECIMAL => Some(Ok(Immediate(Symbol::DECODE_DEC))), + Symbol::NUM_F32 | Symbol::NUM_BINARY32 => Some(Ok(Immediate(Symbol::DECODE_F32))), + Symbol::NUM_F64 | Symbol::NUM_BINARY64 => Some(Ok(Immediate(Symbol::DECODE_F64))), + Symbol::NUM_NAT | Symbol::NUM_NATURAL => Some(Err(DeriveError::Underivable)), + _ => None, + } +} diff --git a/crates/compiler/derive_key/src/encoding.rs b/crates/compiler/derive_key/src/encoding.rs new file mode 100644 index 0000000000..423941bc7d --- /dev/null +++ b/crates/compiler/derive_key/src/encoding.rs @@ -0,0 +1,169 @@ +use roc_module::{ + ident::{Lowercase, TagName}, + symbol::Symbol, +}; +use roc_types::subs::{Content, FlatType, GetSubsSlice, Subs, Variable}; + +use crate::{ + util::{check_derivable_ext_var, debug_name_record, debug_name_tag, debug_name_tuple}, + DeriveError, +}; + +#[derive(Hash)] +pub enum FlatEncodable { + Immediate(Symbol), + Key(FlatEncodableKey), +} + +#[derive(Hash, PartialEq, Eq, Debug, Clone)] +pub enum FlatEncodableKey { + List(/* takes one variable */), + Set(/* takes one variable */), + Dict(/* takes two variables */), + // Unfortunate that we must allocate here, c'est la vie + Record(Vec), + Tuple(u32), + TagUnion(Vec<(TagName, u16)>), +} + +impl FlatEncodableKey { + pub(crate) fn debug_name(&self) -> String { + match self { + FlatEncodableKey::List() => "list".to_string(), + FlatEncodableKey::Set() => "set".to_string(), + FlatEncodableKey::Dict() => "dict".to_string(), + FlatEncodableKey::Record(fields) => debug_name_record(fields), + FlatEncodableKey::Tuple(arity) => debug_name_tuple(*arity), + FlatEncodableKey::TagUnion(tags) => debug_name_tag(tags), + } + } +} + +impl FlatEncodable { + pub(crate) fn from_var(subs: &Subs, var: Variable) -> Result { + use DeriveError::*; + use FlatEncodable::*; + match *subs.get_content_without_compacting(var) { + Content::Structure(flat_type) => match flat_type { + FlatType::Apply(sym, _) => match sym { + Symbol::LIST_LIST => Ok(Key(FlatEncodableKey::List())), + Symbol::SET_SET => Ok(Key(FlatEncodableKey::Set())), + Symbol::DICT_DICT => Ok(Key(FlatEncodableKey::Dict())), + Symbol::STR_STR => Ok(Immediate(Symbol::ENCODE_STRING)), + _ => Err(Underivable), + }, + FlatType::Record(fields, ext) => { + let (fields_iter, ext) = fields.unsorted_iterator_and_ext(subs, ext); + + // TODO someday we can put #[cfg(debug_assertions)] around this, but for now let's always do it. + check_derivable_ext_var(subs, ext, |ext| { + matches!(ext, Content::Structure(FlatType::EmptyRecord)) + })?; + + let mut field_names = Vec::with_capacity(fields.len()); + for (field_name, _) in fields_iter { + field_names.push(field_name.clone()); + } + + field_names.sort(); + + Ok(Key(FlatEncodableKey::Record(field_names))) + } + FlatType::Tuple(elems, ext) => { + let (elems_iter, ext) = elems.sorted_iterator_and_ext(subs, ext); + + // TODO someday we can put #[cfg(debug_assertions)] around this, but for now let's always do it. + check_derivable_ext_var(subs, ext, |ext| { + matches!(ext, Content::Structure(FlatType::EmptyTuple)) + })?; + + Ok(Key(FlatEncodableKey::Tuple(elems_iter.count() as _))) + } + FlatType::TagUnion(tags, ext) | FlatType::RecursiveTagUnion(_, tags, ext) => { + // The recursion var doesn't matter, because the derived implementation will only + // look on the surface of the tag union type, and more over the payloads of the + // arguments will be left generic for the monomorphizer to fill in with the + // appropriate type. That is, + // [ A t1, B t1 t2 ] + // and + // [ A t1, B t1 t2 ] as R + // look the same on the surface, because `R` is only somewhere inside of the + // `t`-prefixed payload types. + let (tags_iter, ext) = tags.unsorted_tags_and_ext(subs, ext); + + // TODO someday we can put #[cfg(debug_assertions)] around this, but for now let's always do it. + check_derivable_ext_var(subs, ext.var(), |ext| { + matches!(ext, Content::Structure(FlatType::EmptyTagUnion)) + })?; + + let mut tag_names_and_payload_sizes: Vec<_> = tags_iter + .tags + .into_iter() + .map(|(name, payload_slice)| { + let payload_size = payload_slice.len(); + (name.clone(), payload_size as _) + }) + .collect(); + + tag_names_and_payload_sizes.sort_by(|(t1, _), (t2, _)| t1.cmp(t2)); + + Ok(Key(FlatEncodableKey::TagUnion(tag_names_and_payload_sizes))) + } + FlatType::FunctionOrTagUnion(names_index, _, _) => { + Ok(Key(FlatEncodableKey::TagUnion( + subs.get_subs_slice(names_index) + .iter() + .map(|t| (t.clone(), 0)) + .collect(), + ))) + } + FlatType::EmptyRecord => Ok(Key(FlatEncodableKey::Record(vec![]))), + FlatType::EmptyTagUnion => Ok(Key(FlatEncodableKey::TagUnion(vec![]))), + FlatType::Func(..) => Err(Underivable), + FlatType::EmptyTuple => unreachable!("Somehow Encoding derivation got an expression that's an empty tuple, which shouldn't be possible!"), + }, + Content::Alias(sym, _, real_var, _) => match from_builtin_symbol(sym) { + Some(lambda) => lambda, + // TODO: I believe it is okay to unwrap opaques here because derivers are only used + // by the backend, and the backend treats opaques like structural aliases. + _ => Self::from_var(subs, real_var), + }, + Content::RangedNumber(range) => { + Self::from_var(subs, range.default_compilation_variable()) + } + Content::RecursionVar { structure, .. } => Self::from_var(subs, structure), + Content::Error => Err(Underivable), + Content::FlexVar(_) + | Content::RigidVar(_) + | Content::FlexAbleVar(_, _) + | Content::RigidAbleVar(_, _) => Err(UnboundVar), + Content::LambdaSet(_) | Content::ErasedLambda => Err(Underivable), + } + } + + pub(crate) fn from_builtin_symbol(symbol: Symbol) -> Result { + from_builtin_symbol(symbol).unwrap_or(Err(DeriveError::Underivable)) + } +} + +const fn from_builtin_symbol(symbol: Symbol) -> Option> { + use FlatEncodable::*; + match symbol { + Symbol::BOOL_BOOL => Some(Ok(Immediate(Symbol::ENCODE_BOOL))), + Symbol::NUM_U8 | Symbol::NUM_UNSIGNED8 => Some(Ok(Immediate(Symbol::ENCODE_U8))), + Symbol::NUM_U16 | Symbol::NUM_UNSIGNED16 => Some(Ok(Immediate(Symbol::ENCODE_U16))), + Symbol::NUM_U32 | Symbol::NUM_UNSIGNED32 => Some(Ok(Immediate(Symbol::ENCODE_U32))), + Symbol::NUM_U64 | Symbol::NUM_UNSIGNED64 => Some(Ok(Immediate(Symbol::ENCODE_U64))), + Symbol::NUM_U128 | Symbol::NUM_UNSIGNED128 => Some(Ok(Immediate(Symbol::ENCODE_U128))), + Symbol::NUM_I8 | Symbol::NUM_SIGNED8 => Some(Ok(Immediate(Symbol::ENCODE_I8))), + Symbol::NUM_I16 | Symbol::NUM_SIGNED16 => Some(Ok(Immediate(Symbol::ENCODE_I16))), + Symbol::NUM_I32 | Symbol::NUM_SIGNED32 => Some(Ok(Immediate(Symbol::ENCODE_I32))), + Symbol::NUM_I64 | Symbol::NUM_SIGNED64 => Some(Ok(Immediate(Symbol::ENCODE_I64))), + Symbol::NUM_I128 | Symbol::NUM_SIGNED128 => Some(Ok(Immediate(Symbol::ENCODE_I128))), + Symbol::NUM_DEC | Symbol::NUM_DECIMAL => Some(Ok(Immediate(Symbol::ENCODE_DEC))), + Symbol::NUM_F32 | Symbol::NUM_BINARY32 => Some(Ok(Immediate(Symbol::ENCODE_F32))), + Symbol::NUM_F64 | Symbol::NUM_BINARY64 => Some(Ok(Immediate(Symbol::ENCODE_F64))), + Symbol::NUM_NAT | Symbol::NUM_NATURAL => Some(Err(DeriveError::Underivable)), + _ => None, + } +} diff --git a/crates/compiler/derive_key/src/hash.rs b/crates/compiler/derive_key/src/hash.rs new file mode 100644 index 0000000000..ae4ade208f --- /dev/null +++ b/crates/compiler/derive_key/src/hash.rs @@ -0,0 +1,202 @@ +use roc_module::{ + ident::{Lowercase, TagName}, + symbol::Symbol, +}; +use roc_types::subs::{Content, FlatType, GetSubsSlice, Subs, Variable}; + +use crate::{ + util::{check_derivable_ext_var, debug_name_record, debug_name_tag, debug_name_tuple}, + DeriveError, +}; + +#[derive(Hash)] +pub enum FlatHash { + // `hash` is always of form `hasher, a -> hasher` where `hasher` and `a` are opaque, so all + // immediates must have exactly one lambda set! + SingleLambdaSetImmediate(Symbol), + Key(FlatHashKey), +} + +#[derive(Hash, PartialEq, Eq, Debug, Clone)] +pub enum FlatHashKey { + // Unfortunate that we must allocate here, c'est la vie + Record(Vec), + Tuple(u32), + TagUnion(Vec<(TagName, u16)>), +} + +impl FlatHashKey { + pub(crate) fn debug_name(&self) -> String { + match self { + FlatHashKey::Record(fields) => debug_name_record(fields), + FlatHashKey::Tuple(arity) => debug_name_tuple(*arity), + FlatHashKey::TagUnion(tags) => debug_name_tag(tags), + } + } +} + +impl FlatHash { + pub(crate) fn from_var(subs: &Subs, var: Variable) -> Result { + use DeriveError::*; + use FlatHash::*; + match *subs.get_content_without_compacting(var) { + Content::Structure(flat_type) => match flat_type { + FlatType::Apply(sym, _) => match sym { + Symbol::LIST_LIST => Ok(SingleLambdaSetImmediate(Symbol::HASH_HASH_LIST)), + Symbol::STR_STR => Ok(SingleLambdaSetImmediate(Symbol::HASH_HASH_STR_BYTES)), + _ => Err(Underivable), + }, + FlatType::Record(fields, ext) => { + let (fields_iter, ext) = fields.unsorted_iterator_and_ext(subs, ext); + + check_derivable_ext_var(subs, ext, |ext| { + matches!(ext, Content::Structure(FlatType::EmptyRecord)) + })?; + + let mut field_names = Vec::with_capacity(fields.len()); + for (field_name, record_field) in fields_iter { + if record_field.is_optional() { + // Can't derive a concrete decoder for optional fields, since those are + // compile-time-polymorphic + return Err(Underivable); + } + field_names.push(field_name.clone()); + } + + field_names.sort(); + + Ok(Key(FlatHashKey::Record(field_names))) + } + FlatType::Tuple(elems, ext) => { + let (elems_iter, ext) = elems.sorted_iterator_and_ext(subs, ext); + + check_derivable_ext_var(subs, ext, |ext| { + matches!(ext, Content::Structure(FlatType::EmptyTuple)) + })?; + + Ok(Key(FlatHashKey::Tuple(elems_iter.count() as _))) + } + FlatType::TagUnion(tags, ext) | FlatType::RecursiveTagUnion(_, tags, ext) => { + // The recursion var doesn't matter, because the derived implementation will only + // look on the surface of the tag union type, and more over the payloads of the + // arguments will be left generic for the monomorphizer to fill in with the + // appropriate type. That is, + // [ A t1, B t1 t2 ] + // and + // [ A t1, B t1 t2 ] as R + // look the same on the surface, because `R` is only somewhere inside of the + // `t`-prefixed payload types. + let (tags_iter, ext) = tags.unsorted_tags_and_ext(subs, ext); + + check_derivable_ext_var(subs, ext.var(), |ext| { + matches!(ext, Content::Structure(FlatType::EmptyTagUnion)) + })?; + + let mut tag_names_and_payload_sizes: Vec<_> = tags_iter + .tags + .into_iter() + .map(|(name, payload_slice)| { + let payload_size = payload_slice.len(); + (name.clone(), payload_size as _) + }) + .collect(); + + tag_names_and_payload_sizes.sort_by(|(t1, _), (t2, _)| t1.cmp(t2)); + + Ok(Key(FlatHashKey::TagUnion(tag_names_and_payload_sizes))) + } + FlatType::FunctionOrTagUnion(names_index, _, _) => Ok(Key(FlatHashKey::TagUnion( + subs.get_subs_slice(names_index) + .iter() + .map(|t| (t.clone(), 0)) + .collect(), + ))), + FlatType::EmptyRecord => Ok(Key(FlatHashKey::Record(vec![]))), + FlatType::EmptyTuple => todo!(), + FlatType::EmptyTagUnion => Ok(Key(FlatHashKey::TagUnion(vec![]))), + // + FlatType::Func(..) => Err(Underivable), + }, + Content::Alias(sym, _, real_var, _) => match builtin_symbol_to_hash_lambda(sym) { + Some(lambda) => Ok(lambda), + // NB: I believe it is okay to unwrap opaques here because derivers are only used + // by the backend, and the backend treats opaques like structural aliases. + None => Self::from_var(subs, real_var), + }, + Content::RangedNumber(range) => { + // Find the integer we're going to compile to, that'll tell us what lambda we + // should resolve to. + // + // Note that at this point, we don't need to update the underlying type variable. + // That's because + // + // - If the type variable always had a ground constructor after solving, we would + // have already refined the ranged number during obligation checking. + // + // - If the type variable was generalized, then this branch is only reached + // during monomorphization, at which point we always choose a default layout + // for ranged numbers, without concern for reification to a ground type. + let chosen_width = range.default_compilation_width(); + let lambda = builtin_symbol_to_hash_lambda(chosen_width.symbol()).unwrap(); + Ok(lambda) + } + // + Content::RecursionVar { structure, .. } => Self::from_var(subs, structure), + // + Content::Error => Err(Underivable), + Content::FlexVar(_) + | Content::RigidVar(_) + | Content::FlexAbleVar(_, _) + | Content::RigidAbleVar(_, _) => Err(UnboundVar), + Content::LambdaSet(_) | Content::ErasedLambda => Err(Underivable), + } + } + + pub fn from_builtin_symbol(symbol: Symbol) -> Result { + builtin_symbol_to_hash_lambda(symbol).ok_or(DeriveError::Underivable) + } +} + +const fn builtin_symbol_to_hash_lambda(symbol: Symbol) -> Option { + use FlatHash::*; + match symbol { + Symbol::BOOL_BOOL => Some(SingleLambdaSetImmediate(Symbol::HASH_HASH_BOOL)), + Symbol::NUM_U8 | Symbol::NUM_UNSIGNED8 => { + Some(SingleLambdaSetImmediate(Symbol::HASH_ADD_U8)) + } + Symbol::NUM_U16 | Symbol::NUM_UNSIGNED16 => { + Some(SingleLambdaSetImmediate(Symbol::HASH_ADD_U16)) + } + Symbol::NUM_U32 | Symbol::NUM_UNSIGNED32 => { + Some(SingleLambdaSetImmediate(Symbol::HASH_ADD_U32)) + } + Symbol::NUM_U64 | Symbol::NUM_UNSIGNED64 => { + Some(SingleLambdaSetImmediate(Symbol::HASH_ADD_U64)) + } + Symbol::NUM_U128 | Symbol::NUM_UNSIGNED128 => { + Some(SingleLambdaSetImmediate(Symbol::HASH_ADD_U128)) + } + Symbol::NUM_I8 | Symbol::NUM_SIGNED8 => { + Some(SingleLambdaSetImmediate(Symbol::HASH_HASH_I8)) + } + Symbol::NUM_I16 | Symbol::NUM_SIGNED16 => { + Some(SingleLambdaSetImmediate(Symbol::HASH_HASH_I16)) + } + Symbol::NUM_I32 | Symbol::NUM_SIGNED32 => { + Some(SingleLambdaSetImmediate(Symbol::HASH_HASH_I32)) + } + Symbol::NUM_I64 | Symbol::NUM_SIGNED64 => { + Some(SingleLambdaSetImmediate(Symbol::HASH_HASH_I64)) + } + Symbol::NUM_I128 | Symbol::NUM_SIGNED128 => { + Some(SingleLambdaSetImmediate(Symbol::HASH_HASH_I128)) + } + Symbol::NUM_NAT | Symbol::NUM_NATURAL => { + Some(SingleLambdaSetImmediate(Symbol::HASH_HASH_NAT)) + } + Symbol::NUM_DEC | Symbol::NUM_DECIMAL => { + Some(SingleLambdaSetImmediate(Symbol::HASH_HASH_DEC)) + } + _ => None, + } +} diff --git a/crates/compiler/derive_key/src/inspect.rs b/crates/compiler/derive_key/src/inspect.rs new file mode 100644 index 0000000000..a796efc63d --- /dev/null +++ b/crates/compiler/derive_key/src/inspect.rs @@ -0,0 +1,211 @@ +use roc_module::{ + ident::{Lowercase, TagName}, + symbol::Symbol, +}; +use roc_types::{ + subs::{Content, FlatType, GetSubsSlice, Subs, Variable}, + types::AliasKind, +}; + +use crate::util::{ + check_derivable_ext_var, debug_name_fn, debug_name_record, debug_name_tag, debug_name_tuple, +}; + +#[derive(Hash, Debug)] +pub enum FlatInspectable { + Immediate(Symbol), + Key(FlatInspectableKey), +} + +#[derive(Hash, PartialEq, Eq, Debug, Clone)] +pub enum FlatInspectableKey { + List(/* takes one variable */), + Set(/* takes one variable */), + Dict(/* takes two variables */), + // Unfortunate that we must allocate here, c'est la vie + Record(Vec), + Tuple(u32), + TagUnion(Vec<(TagName, u16)>), + Function(u32 /* arity; +1 for return type */), + /// This means specifically an opaque type where the author hasn't requested that it derive Inspect (or implemented it) + Opaque, + Error, +} + +impl FlatInspectableKey { + pub(crate) fn debug_name(&self) -> String { + match self { + FlatInspectableKey::List() => "list".to_string(), + FlatInspectableKey::Set() => "set".to_string(), + FlatInspectableKey::Dict() => "dict".to_string(), + FlatInspectableKey::Record(fields) => debug_name_record(fields), + FlatInspectableKey::Tuple(arity) => debug_name_tuple(*arity), + FlatInspectableKey::TagUnion(tags) => debug_name_tag(tags), + FlatInspectableKey::Function(arity) => debug_name_fn(*arity), + FlatInspectableKey::Error => "error".to_string(), + FlatInspectableKey::Opaque => "opaque".to_string(), + } + } +} + +impl FlatInspectable { + pub(crate) fn from_var(subs: &Subs, var: Variable) -> FlatInspectable { + use FlatInspectable::*; + + match *subs.get_content_without_compacting(var) { + Content::Structure(flat_type) => match flat_type { + FlatType::Apply(sym, _) => match sym { + Symbol::LIST_LIST => Key(FlatInspectableKey::List()), + Symbol::SET_SET => Key(FlatInspectableKey::Set()), + Symbol::DICT_DICT => Key(FlatInspectableKey::Dict()), + Symbol::STR_STR => Immediate(Symbol::INSPECT_STR), + _ => Immediate(Symbol::INSPECT_OPAQUE), + }, + FlatType::Record(fields, ext) => { + let (fields_iter, ext) = fields.unsorted_iterator_and_ext(subs, ext); + + // TODO someday we can put #[cfg(debug_assertions)] around this, but for now let's always do it. + check_derivable_ext_var(subs, ext, |ext| { + matches!(ext, Content::Structure(FlatType::EmptyRecord)) + }).expect("Compiler error: unexpected nonempty ext var when deriving Inspect for record"); + + let mut field_names = Vec::with_capacity(fields.len()); + for (field_name, _) in fields_iter { + field_names.push(field_name.clone()); + } + + field_names.sort(); + + Key(FlatInspectableKey::Record(field_names)) + } + FlatType::Tuple(elems, ext) => { + let (elems_iter, ext) = elems.sorted_iterator_and_ext(subs, ext); + + // TODO someday we can put #[cfg(debug_assertions)] around this, but for now let's always do it. + check_derivable_ext_var(subs, ext, |ext| { + matches!(ext, Content::Structure(FlatType::EmptyTuple)) + }).expect("Compiler error: unexpected nonempty ext var when deriving Inspect for tuple"); + + Key(FlatInspectableKey::Tuple(elems_iter.count() as _)) + } + FlatType::TagUnion(tags, ext) | FlatType::RecursiveTagUnion(_, tags, ext) => { + // The recursion var doesn't matter, because the derived implementation will only + // look on the surface of the tag union type, and more over the payloads of the + // arguments will be left generic for the monomorphizer to fill in with the + // appropriate type. That is, + // [ A t1, B t1 t2 ] + // and + // [ A t1, B t1 t2 ] as R + // look the same on the surface, because `R` is only somewhere inside of the + // `t`-prefixed payload types. + let (tags_iter, ext) = tags.unsorted_tags_and_ext(subs, ext); + + // TODO someday we can put #[cfg(debug_assertions)] around this, but for now let's always do it. + check_derivable_ext_var(subs, ext.var(), |ext| { + matches!(ext, Content::Structure(FlatType::EmptyTagUnion)) + }).expect("Compiler error: unexpected nonempty ext var when deriving Inspect for tag union"); + + let mut tag_names_and_payload_sizes: Vec<_> = tags_iter + .tags + .into_iter() + .map(|(name, payload_slice)| { + let payload_size = payload_slice.len(); + (name.clone(), payload_size as _) + }) + .collect(); + + tag_names_and_payload_sizes.sort_by(|(t1, _), (t2, _)| t1.cmp(t2)); + + Key(FlatInspectableKey::TagUnion(tag_names_and_payload_sizes)) + } + FlatType::FunctionOrTagUnion(names_index, _, _) => { + Key(FlatInspectableKey::TagUnion( + subs.get_subs_slice(names_index) + .iter() + .map(|t| (t.clone(), 0)) + .collect(), + )) + } + FlatType::EmptyRecord => Key(FlatInspectableKey::Record(Vec::new())), + FlatType::EmptyTagUnion => Key(FlatInspectableKey::TagUnion(Vec::new())), + FlatType::Func(..) => { + Immediate(Symbol::INSPECT_FUNCTION) + } + FlatType::EmptyTuple => unreachable!("Somehow Inspect derivation got an expression that's an empty tuple, which shouldn't be possible!"), + }, + Content::Alias(sym, _, real_var, kind) => match Self::from_builtin_alias(sym) { + Some(lambda) => lambda, + + _ => { + match kind { + AliasKind::Structural => { + Self::from_var(subs, real_var) + } + // Special case, an unbound `Frac *` will become a `Dec`. + AliasKind::Opaque if matches!(*subs.get_content_without_compacting(real_var), Content::FlexVar(_) | Content::FlexAbleVar(_, _)) => { + Immediate(Symbol::INSPECT_DEC) + } + AliasKind::Opaque if sym.is_builtin() => { + Self::from_var(subs, real_var) + } + AliasKind::Opaque => { + // There are two cases in which `Inspect` can be derived for an opaque + // type. + // 1. An opaque type claims to implement `Inspect` and asks us to + // auto-derive it. E.g. + // + // ```text + // Op := {} implements [Inspect] + // ``` + // + // In this case, we generate a synthetic implementation during + // canonicalization that defers to `inspect`ing the inner type. As + // such, this case is never reached in this branch. + // + // 2. An opaque type does not explicitly claim to implement + // `Inspect`. In this case, we print a default opaque string for + // the opaque type. + Immediate(Symbol::INSPECT_OPAQUE) + } + } + } + + }, + Content::RangedNumber(range) => { + Self::from_var(subs, range.default_compilation_variable()) + } + Content::RecursionVar { structure, .. } => Self::from_var(subs, structure), + Content::Error => Key(FlatInspectableKey::Error), + Content::FlexVar(_) + | Content::RigidVar(_) + | Content::FlexAbleVar(_, _) + | Content::RigidAbleVar(_, _) + | Content::LambdaSet(_) | Content::ErasedLambda => { + unreachable!("There must have been a bug in the solver, because we're trying to derive Inspect on a non-concrete type."); + } + } + } + + pub(crate) const fn from_builtin_alias(symbol: Symbol) -> Option { + use FlatInspectable::*; + + match symbol { + Symbol::BOOL_BOOL => Some(Immediate(Symbol::INSPECT_BOOL)), + Symbol::NUM_U8 | Symbol::NUM_UNSIGNED8 => Some(Immediate(Symbol::INSPECT_U8)), + Symbol::NUM_U16 | Symbol::NUM_UNSIGNED16 => Some(Immediate(Symbol::INSPECT_U16)), + Symbol::NUM_U32 | Symbol::NUM_UNSIGNED32 => Some(Immediate(Symbol::INSPECT_U32)), + Symbol::NUM_U64 | Symbol::NUM_UNSIGNED64 => Some(Immediate(Symbol::INSPECT_U64)), + Symbol::NUM_U128 | Symbol::NUM_UNSIGNED128 => Some(Immediate(Symbol::INSPECT_U128)), + Symbol::NUM_I8 | Symbol::NUM_SIGNED8 => Some(Immediate(Symbol::INSPECT_I8)), + Symbol::NUM_I16 | Symbol::NUM_SIGNED16 => Some(Immediate(Symbol::INSPECT_I16)), + Symbol::NUM_I32 | Symbol::NUM_SIGNED32 => Some(Immediate(Symbol::INSPECT_I32)), + Symbol::NUM_I64 | Symbol::NUM_SIGNED64 => Some(Immediate(Symbol::INSPECT_I64)), + Symbol::NUM_I128 | Symbol::NUM_SIGNED128 => Some(Immediate(Symbol::INSPECT_I128)), + Symbol::NUM_DEC | Symbol::NUM_DECIMAL => Some(Immediate(Symbol::INSPECT_DEC)), + Symbol::NUM_F32 | Symbol::NUM_BINARY32 => Some(Immediate(Symbol::INSPECT_F32)), + Symbol::NUM_F64 | Symbol::NUM_BINARY64 => Some(Immediate(Symbol::INSPECT_F64)), + Symbol::NUM_NAT | Symbol::NUM_NATURAL => Some(Immediate(Symbol::INSPECT_NAT)), + _ => None, + } + } +} diff --git a/crates/compiler/derive_key/src/lib.rs b/crates/compiler/derive_key/src/lib.rs new file mode 100644 index 0000000000..53db35816e --- /dev/null +++ b/crates/compiler/derive_key/src/lib.rs @@ -0,0 +1,172 @@ +//! To avoid duplicating derived implementations for the same type, derived implementations are +//! addressed by a key of their type content. However, different derived implementations can be +//! reused based on different properties of the type. For example: +//! +//! - `Eq` does not care about surface type representations; its derived implementations can be +//! uniquely addressed by the [`Layout`][crate::layout::Layout] of a type. +//! - `Encoding` must care about surface type representations; for example, `{ a: "" }` and +//! `{ b: "" }` have different derived implementations. However, it does not need to distinguish +//! between e.g. required and optional record fields. +//! - `Decoding` is like encoding, but has some differences. For one, it *does* need to distinguish +//! between required and optional record fields. +//! +//! For these reasons the content keying is based on a strategy as well, which are the variants of +//! [`DeriveKey`]. + +pub mod decoding; +pub mod encoding; +pub mod hash; +pub mod inspect; +mod util; + +use decoding::{FlatDecodable, FlatDecodableKey}; +use encoding::{FlatEncodable, FlatEncodableKey}; +use hash::{FlatHash, FlatHashKey}; + +use inspect::{FlatInspectable, FlatInspectableKey}; +use roc_module::symbol::Symbol; +use roc_types::subs::{Subs, Variable}; + +#[derive(Debug, PartialEq, Eq)] +pub enum DeriveError { + /// Unbound variable present in the type-to-derive. It may be possible to derive for this type + /// once the unbound variable is resolved. + UnboundVar, + /// The type is underivable for the given ability member. + Underivable, +} + +#[derive(Hash, PartialEq, Eq, Debug, Clone)] +#[repr(u8)] +pub enum DeriveKey { + ToEncoder(FlatEncodableKey), + Decoder(FlatDecodableKey), + Hash(FlatHashKey), + ToInspector(FlatInspectableKey), +} + +impl DeriveKey { + pub fn debug_name(&self) -> String { + match self { + DeriveKey::ToEncoder(key) => format!("toEncoder_{}", key.debug_name()), + DeriveKey::Decoder(key) => format!("decoder_{}", key.debug_name()), + DeriveKey::Hash(key) => format!("hash_{}", key.debug_name()), + DeriveKey::ToInspector(key) => format!("toInspector_{}", key.debug_name()), + } + } +} + +#[derive(Hash, Clone, PartialEq, Eq, Debug)] +pub enum Derived { + /// If a derived implementation name is well-known ahead-of-time, we can inline the symbol + /// directly rather than associating a key for an implementation to be made later on. + /// + /// Immediates refer to ability members that are "inlined" at the derivation call site. + Immediate(Symbol), + /// Like an [Derived::Immediate], but with the additional constraint that the immediate + /// symbol is statically known to have exactly one lamdba set. + /// This unlocks some optimization opportunities, as regioned lambda sets do not need to be + /// chased. + SingleLambdaSetImmediate(Symbol), + /// Key of the derived implementation to use. This allows association of derived implementation + /// names to a key, when the key is known ahead-of-time but the implementation (and it's name) + /// is yet-to-be-made. + Key(DeriveKey), +} + +/// The builtin ability member to derive. +#[derive(Clone, Copy, Debug)] +pub enum DeriveBuiltin { + ToEncoder, + Decoder, + Hash, + IsEq, + ToInspector, +} + +impl TryFrom for DeriveBuiltin { + type Error = Symbol; + + fn try_from(value: Symbol) -> Result { + match value { + Symbol::ENCODE_TO_ENCODER => Ok(DeriveBuiltin::ToEncoder), + Symbol::DECODE_DECODER => Ok(DeriveBuiltin::Decoder), + Symbol::HASH_HASH => Ok(DeriveBuiltin::Hash), + Symbol::BOOL_IS_EQ => Ok(DeriveBuiltin::IsEq), + Symbol::INSPECT_TO_INSPECTOR => Ok(DeriveBuiltin::ToInspector), + _ => Err(value), + } + } +} + +impl Derived { + pub fn builtin( + builtin: DeriveBuiltin, + subs: &Subs, + var: Variable, + ) -> Result { + match builtin { + DeriveBuiltin::ToEncoder => match encoding::FlatEncodable::from_var(subs, var)? { + FlatEncodable::Immediate(imm) => Ok(Derived::Immediate(imm)), + FlatEncodable::Key(repr) => Ok(Derived::Key(DeriveKey::ToEncoder(repr))), + }, + DeriveBuiltin::Decoder => match decoding::FlatDecodable::from_var(subs, var)? { + FlatDecodable::Immediate(imm) => Ok(Derived::Immediate(imm)), + FlatDecodable::Key(repr) => Ok(Derived::Key(DeriveKey::Decoder(repr))), + }, + DeriveBuiltin::Hash => match hash::FlatHash::from_var(subs, var)? { + FlatHash::SingleLambdaSetImmediate(imm) => { + Ok(Derived::SingleLambdaSetImmediate(imm)) + } + FlatHash::Key(repr) => Ok(Derived::Key(DeriveKey::Hash(repr))), + }, + DeriveBuiltin::IsEq => { + // If obligation checking passes, we always lower derived implementations of `isEq` + // to the `Eq` low-level, to be fulfilled by the backends. + Ok(Derived::SingleLambdaSetImmediate( + Symbol::BOOL_STRUCTURAL_EQ, + )) + } + DeriveBuiltin::ToInspector => match FlatInspectable::from_var(subs, var) { + FlatInspectable::Immediate(imm) => Ok(Derived::Immediate(imm)), + FlatInspectable::Key(repr) => Ok(Derived::Key(DeriveKey::ToInspector(repr))), + }, + } + } + + pub fn builtin_with_builtin_symbol( + builtin: DeriveBuiltin, + symbol: Symbol, + ) -> Result { + match builtin { + DeriveBuiltin::ToEncoder => match encoding::FlatEncodable::from_builtin_symbol(symbol)? + { + FlatEncodable::Immediate(imm) => Ok(Derived::Immediate(imm)), + FlatEncodable::Key(repr) => Ok(Derived::Key(DeriveKey::ToEncoder(repr))), + }, + DeriveBuiltin::Decoder => match decoding::FlatDecodable::from_builtin_symbol(symbol)? { + FlatDecodable::Immediate(imm) => Ok(Derived::Immediate(imm)), + FlatDecodable::Key(repr) => Ok(Derived::Key(DeriveKey::Decoder(repr))), + }, + DeriveBuiltin::Hash => match hash::FlatHash::from_builtin_symbol(symbol)? { + FlatHash::SingleLambdaSetImmediate(imm) => { + Ok(Derived::SingleLambdaSetImmediate(imm)) + } + FlatHash::Key(repr) => Ok(Derived::Key(DeriveKey::Hash(repr))), + }, + DeriveBuiltin::IsEq => { + // If obligation checking passes, we always lower derived implementations of `isEq` + // to the `Eq` low-level, to be fulfilled by the backends. + Ok(Derived::SingleLambdaSetImmediate( + Symbol::BOOL_STRUCTURAL_EQ, + )) + } + DeriveBuiltin::ToInspector => { + match inspect::FlatInspectable::from_builtin_alias(symbol).unwrap() { + FlatInspectable::Immediate(imm) => Ok(Derived::Immediate(imm)), + FlatInspectable::Key(repr) => Ok(Derived::Key(DeriveKey::ToInspector(repr))), + } + } + } + } +} diff --git a/crates/compiler/derive_key/src/util.rs b/crates/compiler/derive_key/src/util.rs new file mode 100644 index 0000000000..a6229c3e25 --- /dev/null +++ b/crates/compiler/derive_key/src/util.rs @@ -0,0 +1,66 @@ +use roc_module::ident::{Lowercase, TagName}; +use roc_types::subs::{Content, Subs, Variable}; + +use crate::DeriveError; + +pub(crate) fn check_derivable_ext_var( + subs: &Subs, + ext_var: Variable, + is_empty_ext: impl Fn(&Content) -> bool, +) -> Result<(), DeriveError> { + let ext_content = subs.get_content_without_compacting(ext_var); + if is_empty_ext(ext_content) + || matches!( + ext_content, + // It's fine to have either a flex/rigid or flex-able/rigid-able in the extension. + // Since we don't know the rest of the type concretely, any implementation (derived or + // not) would only be able to work on the concrete part of the type regardless. So, + // just admit them, and they will be excluded from the deriving scheme. + Content::FlexVar(_) + | Content::FlexAbleVar(..) + | Content::RigidVar(_) + | Content::RigidAbleVar(..) + ) + { + Ok(()) + } else { + match ext_content { + Content::FlexVar(_) => Err(DeriveError::UnboundVar), + _ => Err(DeriveError::Underivable), + } + } +} + +pub(crate) fn debug_name_record(fields: &[Lowercase]) -> String { + let mut str = String::from('{'); + fields.iter().enumerate().for_each(|(i, f)| { + if i > 0 { + str.push(','); + } + str.push_str(f.as_str()); + }); + str.push('}'); + str +} + +pub(crate) fn debug_name_tuple(arity: u32) -> String { + format!("(arity:{arity})") +} + +pub(crate) fn debug_name_tag(tags: &[(TagName, u16)]) -> String { + let mut str = String::from('['); + tags.iter().enumerate().for_each(|(i, (tag, arity))| { + if i > 0 { + str.push(','); + } + str.push_str(tag.0.as_str()); + str.push(' '); + str.push_str(&arity.to_string()); + }); + str.push(']'); + str +} + +pub(crate) fn debug_name_fn(arity: u32) -> String { + format!("(arity:{arity} -> _)") +} diff --git a/crates/compiler/exhaustive/Cargo.toml b/crates/compiler/exhaustive/Cargo.toml new file mode 100644 index 0000000000..17d96fcd40 --- /dev/null +++ b/crates/compiler/exhaustive/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "roc_exhaustive" +description = "Provides exhaustiveness checking for Roc." + +authors.workspace = true +edition.workspace = true +license.workspace = true +version.workspace = true + +[dependencies] +roc_collections = { path = "../collections" } +roc_error_macros = { path = "../../error_macros" } +roc_module = { path = "../module" } +roc_problem = { path = "../problem" } +roc_region = { path = "../region" } diff --git a/crates/compiler/exhaustive/src/lib.rs b/crates/compiler/exhaustive/src/lib.rs new file mode 100644 index 0000000000..c4b36af214 --- /dev/null +++ b/crates/compiler/exhaustive/src/lib.rs @@ -0,0 +1,905 @@ +//! Exhaustiveness checking, based on [Warnings for pattern matching](http://moscova.inria.fr/~maranget/papers/warn/warn.pdf) +//! (Luc Maranget, 2007). + +use roc_collections::all::{HumanIndex, MutMap}; +use roc_error_macros::internal_error; +use roc_module::{ + ident::{Lowercase, TagIdIntType, TagName}, + symbol::Symbol, +}; +use roc_problem::Severity; +use roc_region::all::Region; + +use self::Pattern::*; + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct Union { + pub alternatives: Vec, + pub render_as: RenderAs, +} + +impl Union { + pub fn newtype_wrapper(name: CtorName, arity: usize) -> Self { + let alternatives = vec![Ctor { + name, + tag_id: TagId(0), + arity, + }]; + + Union { + alternatives, + render_as: RenderAs::Tag, + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub enum RenderAs { + Tag, + Opaque, + Record(Vec), + Tuple, + Guard, +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash, Copy)] +pub struct TagId(pub TagIdIntType); + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub enum CtorName { + Tag(TagName), + Opaque(Symbol), +} + +impl CtorName { + pub fn is_tag(&self, tag_name: &TagName) -> bool { + match self { + Self::Tag(test) => test == tag_name, + _ => false, + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct Ctor { + pub name: CtorName, + pub tag_id: TagId, + pub arity: usize, +} + +#[derive(Clone, Debug, PartialEq)] +pub enum Pattern { + Anything, + Literal(Literal), + Ctor(Union, TagId, std::vec::Vec), + List(ListArity, std::vec::Vec), +} + +/// The arity of list pattern. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum ListArity { + /// A list pattern of an exact size. + Exact(usize), + /// A list pattern matching a variable size, where `Slice(before, after)` refers to the number + /// of elements that must be present before and after the variable rest pattern, respectively. + /// + /// For example, + /// [..] => Slice(0, 0) + /// [A, .., B] => Slice(1, 1) + /// [A, B, ..] => Slice(2, 0) + /// [.., A, B] => Slice(0, 2) + Slice(usize, usize), +} + +impl ListArity { + /// The trivially-exhaustive list pattern `[..]` + const ANY: ListArity = ListArity::Slice(0, 0); + + pub fn min_len(&self) -> usize { + match self { + ListArity::Exact(n) => *n, + ListArity::Slice(l, r) => l + r, + } + } + + /// Could this list pattern include list pattern arity `other`? + fn covers_arities_of(&self, other: &Self) -> bool { + self.covers_length(other.min_len()) + } + + pub fn covers_length(&self, length: usize) -> bool { + match self { + ListArity::Exact(l) => { + // [_, _, _] can only cover [_, _, _] + *l == length + } + ListArity::Slice(head, tail) => { + // [_, _, .., _] can cover infinite arities >=3 , including + // [_, _, .., _], [_, .., _, _], [_, _, .., _, _], [_, _, _, .., _, _], and so on + head + tail <= length + } + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum Literal { + Int([u8; 16]), + U128([u8; 16]), + Bit(bool), + Byte(u8), + /// Stores the float bits + Float(u64), + Decimal([u8; 16]), + Str(Box), +} + +/// Error + +#[derive(Clone, Debug, PartialEq)] +pub enum Error { + Incomplete(Region, Context, Vec), + Redundant { + overall_region: Region, + branch_region: Region, + index: HumanIndex, + }, + Unmatchable { + overall_region: Region, + branch_region: Region, + index: HumanIndex, + }, +} + +impl Error { + pub fn severity(&self) -> Severity { + use Severity::*; + match self { + Error::Incomplete(..) => RuntimeError, + Error::Redundant { .. } => Warning, + Error::Unmatchable { .. } => Warning, + } + } + + pub fn region(&self) -> Region { + match self { + Error::Incomplete(region, _, _) => *region, + Error::Redundant { branch_region, .. } => *branch_region, + Error::Unmatchable { branch_region, .. } => *branch_region, + } + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Context { + BadArg, + BadDestruct, + BadCase, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Guard { + HasGuard, + NoGuard, +} + +/// Check + +pub fn check( + region: Region, + context: Context, + matrix: Vec>, +) -> Result<(), Vec> { + let mut errors = Vec::new(); + let bad_patterns = is_exhaustive(&matrix, 1); + if !bad_patterns.is_empty() { + // TODO i suspect this is like a concat in in practice? code below can panic + // if this debug_assert! ever fails, the theory is disproven + debug_assert!(bad_patterns.iter().map(|v| v.len()).sum::() == bad_patterns.len()); + let heads = bad_patterns.into_iter().map(|mut v| v.remove(0)).collect(); + errors.push(Error::Incomplete(region, context, heads)); + return Err(errors); + } + Ok(()) +} + +/// EXHAUSTIVE PATTERNS + +/// INVARIANTS: +/// +/// The initial rows "matrix" are all of length 1 +/// The initial count of items per row "n" is also 1 +/// The resulting rows are examples of missing patterns +fn is_exhaustive(matrix: &RefPatternMatrix, n: usize) -> PatternMatrix { + let ctors = if matrix.is_empty() { + return vec![std::iter::repeat(Anything).take(n).collect()]; + } else if n == 0 { + return vec![]; + } else { + collect_ctors(matrix) + }; + + match ctors { + CollectedCtors::NonExhaustiveAny => { + let new_matrix: Vec<_> = matrix + .iter() + .filter_map(|row| specialize_row_by_anything(row)) + .collect(); + let mut rest = is_exhaustive(&new_matrix, n - 1); + + for row in rest.iter_mut() { + row.push(Anything); + } + + rest + } + CollectedCtors::Ctors(ctors) => { + debug_assert!(!ctors.is_empty()); + + let num_seen = ctors.len(); + let alts = ctors.iter().next().unwrap().1; + + let alt_list = &alts.alternatives; + let num_alts = alt_list.len(); + + if num_seen < num_alts { + let new_matrix: Vec<_> = matrix + .iter() + .filter_map(|row| specialize_row_by_anything(row)) + .collect(); + let rest: Vec> = is_exhaustive(&new_matrix, n - 1); + + let last = alt_list + .iter() + .filter_map(|r| is_missing(alts.clone(), &ctors, r)); + + let mut result = Vec::new(); + + for last_option in last { + for mut row in rest.clone() { + row.push(last_option.clone()); + + result.push(row); + } + } + + result + } else { + let is_alt_exhaustive = |Ctor { arity, tag_id, .. }| { + let new_matrix: Vec<_> = matrix + .iter() + .filter_map(|r| specialize_row_by_ctor(tag_id, arity, r.to_owned())) + .collect(); + let rest: Vec> = is_exhaustive(&new_matrix, arity + n - 1); + + let mut result = Vec::with_capacity(rest.len()); + for row in rest { + result.push(recover_ctor(alts.clone(), tag_id, arity, row)); + } + + result + }; + + alt_list + .iter() + .cloned() + .flat_map(is_alt_exhaustive) + .collect() + } + } + CollectedCtors::NonExhaustiveList(alt_lists) => { + let is_alt_exhaustive = |arity: ListArity| { + let new_matrix: Vec<_> = matrix + .iter() + .filter_map(|row| specialize_row_by_list(arity, row.to_owned())) + .collect(); + + let rest = is_exhaustive(&new_matrix, arity.min_len() + n - 1); + + rest.into_iter() + .map(move |row_not_covered| recover_list(arity, row_not_covered)) + }; + + alt_lists.into_iter().flat_map(is_alt_exhaustive).collect() + } + } +} + +fn is_missing(union: Union, ctors: &MutMap, ctor: &Ctor) -> Option { + let Ctor { arity, tag_id, .. } = ctor; + + if ctors.contains_key(tag_id) { + None + } else { + let anythings = std::iter::repeat(Anything).take(*arity).collect(); + Some(Pattern::Ctor(union, *tag_id, anythings)) + } +} + +fn recover_ctor( + union: Union, + tag_id: TagId, + arity: usize, + mut patterns: Vec, +) -> Vec { + let args = patterns.split_off(patterns.len() - arity); + let mut rest = patterns; + + rest.push(Ctor(union, tag_id, args)); + + rest +} + +fn recover_list(arity: ListArity, mut patterns: Vec) -> Vec { + let list_elems = patterns.split_off(patterns.len() - arity.min_len()); + let mut rest = patterns; + + rest.push(List(arity, list_elems)); + + rest +} + +/// Check if a new row "vector" is useful given previous rows "matrix" +pub fn is_useful(mut old_matrix: PatternMatrix, mut vector: Row) -> bool { + let mut matrix = Vec::with_capacity(old_matrix.len()); + + // this loop ping-pongs the rows between old_matrix and matrix + 'outer: loop { + match vector.pop() { + _ if old_matrix.is_empty() => { + // No rows are the same as the new vector! The vector is useful! + break true; + } + None => { + // There is nothing left in the new vector, but we still have + // rows that match the same things. This is not a useful vector! + break false; + } + Some(first_pattern) => { + // NOTE: if there are bugs in this code, look at the ordering of the row/matrix + + match first_pattern { + // keep checking rows that start with this Ctor or Anything + Ctor(_, id, args) => { + specialize_matrix_by_ctor(id, args.len(), &mut old_matrix, &mut matrix); + + std::mem::swap(&mut old_matrix, &mut matrix); + + vector.extend(args); + } + + List(arity, args) => { + // Check if there any specialized constructor of this list pattern + // that is useful. + let spec_list_ctors = build_list_ctors_covering_patterns( + arity, + filter_matrix_list_ctors(&old_matrix), + ); + debug_assert!(!spec_list_ctors.is_empty()); + + if spec_list_ctors.len() == 1 { + specialize_matrix_by_list( + spec_list_ctors[0], + &mut old_matrix, + &mut matrix, + ); + + std::mem::swap(&mut old_matrix, &mut matrix); + + vector.extend(args); + } else { + // TODO turn this into an iteration over the outer loop rather than bouncing + for list_ctor in spec_list_ctors { + let mut old_matrix = old_matrix.clone(); + let mut spec_matrix = Vec::with_capacity(old_matrix.len()); + + specialize_matrix_by_list( + list_ctor, + &mut old_matrix, + &mut spec_matrix, + ); + + let mut vector = vector.clone(); + specialize_row_with_polymorphic_list( + &mut vector, + &args, + arity, + list_ctor, + ); + + if is_useful(spec_matrix, vector) { + return true; + } + } + + return false; + } + } + + Anything => { + // check if all alternatives appear in matrix + match is_complete(&old_matrix) { + Complete::No => { + // This Anything is useful because some Ctors are missing. + // But what if a previous row has an Anything? + // If so, this one is not useful. + for mut row in old_matrix.drain(..) { + if let Some(Anything) = row.pop() { + matrix.push(row); + } + } + + std::mem::swap(&mut old_matrix, &mut matrix); + } + Complete::Yes(alternatives) => { + // All Ctors are covered, so this Anything is not needed for any + // of those. But what if some of those Ctors have subpatterns + // that make them less general? If so, this actually is useful! + for alternative in alternatives { + let Ctor { arity, tag_id, .. } = alternative; + + let mut old_matrix = old_matrix.clone(); + let mut matrix = vec![]; + specialize_matrix_by_ctor( + tag_id, + arity, + &mut old_matrix, + &mut matrix, + ); + + let mut vector = vector.clone(); + vector.extend(std::iter::repeat(Anything).take(arity)); + + if is_useful(matrix, vector) { + break 'outer true; + } + } + + break false; + } + } + } + + Literal(literal) => { + // keep checking rows that start with this Literal or Anything + + for mut row in old_matrix.drain(..) { + let head = row.pop(); + let patterns = row; + + match head { + Some(Literal(lit)) => { + if lit == literal { + matrix.push(patterns); + } else { + // do nothing + } + } + Some(Anything) => matrix.push(patterns), + + Some(List(..)) => internal_error!("After type checking, lists and literals should never align in exhaustiveness checking"), + + Some(Ctor(_, _, _)) => panic!( + r#"Compiler bug! After type checking, constructors and literals should never align in pattern match exhaustiveness checks."# + ), + + None => panic!( + "Compiler error! Empty matrices should not get specialized." + ), + } + } + std::mem::swap(&mut old_matrix, &mut matrix); + } + } + } + } + } +} + +// Specialize rows in the matrix that match a list's constructor(s). +// +// See the docs on [build_list_ctors_covering_patterns] for more information on how list +// constructors are built up. +fn specialize_matrix_by_list( + spec_arity: ListArity, + old_matrix: &mut PatternMatrix, + spec_matrix: &mut PatternMatrix, +) { + for row in old_matrix.drain(..) { + if let Some(spec_row) = specialize_row_by_list(spec_arity, row) { + spec_matrix.push(spec_row); + } + } +} + +fn specialize_row_with_polymorphic_list( + row: &mut Vec, + list_element_patterns: &[Pattern], + polymorphic_list_ctor: ListArity, + specialized_list_ctor: ListArity, +) { + let min_len = specialized_list_ctor.min_len(); + if list_element_patterns.len() > min_len { + row.extend(list_element_patterns.iter().cloned()); + } + + let (patterns_before, patterns_after) = match polymorphic_list_ctor { + ListArity::Slice(before, after) => ( + &list_element_patterns[..before], + &list_element_patterns[list_element_patterns.len() - after..], + ), + ListArity::Exact(_) => (list_element_patterns, &[] as &[Pattern]), + }; + + let middle_any_patterns_needed = + specialized_list_ctor.min_len() - polymorphic_list_ctor.min_len(); + let middle_patterns = std::iter::repeat(Anything).take(middle_any_patterns_needed); + + row.extend( + (patterns_before.iter().cloned()) + .chain(middle_patterns) + .chain(patterns_after.iter().cloned()), + ); +} + +// Specialize a row that matches a list's constructor(s). +// +// See the docs on [build_list_ctors_covering_patterns] for more information on how list +// constructors are built up. +fn specialize_row_by_list(spec_arity: ListArity, mut row: Row) -> Option { + let head = row.pop(); + let mut spec_patterns = row; + + match head { + Some(List(this_arity, args)) => { + if this_arity.covers_arities_of(&spec_arity) { + // This pattern covers the constructor we are specializing, so add on the + // specialized fields of this pattern relative to the given constructor. + if spec_arity.min_len() != this_arity.min_len() { + // This list pattern covers the list we are specializing, so it must be + // a variable-length slice, i.e. of the form `[before, .., after]`. + // + // Hence, the list we're specializing for must have at least a larger minimum length. + // So we fill the middle part with enough wildcards to reach the length of + // list constructor we're specializing for. + debug_assert!(spec_arity.min_len() > this_arity.min_len()); + match this_arity { + ListArity::Exact(_) => internal_error!("exact-sized lists cannot cover lists of other minimum length"), + ListArity::Slice(before, after) => { + let before = &args[..before]; + let after = &args[this_arity.min_len() - after..]; + let num_extra_wildcards = spec_arity.min_len() - this_arity.min_len(); + let extra_wildcards = std::iter::repeat(&Anything).take(num_extra_wildcards); + + let new_pats = (before.iter().chain(extra_wildcards).chain(after)).cloned(); + + spec_patterns.extend(new_pats); + } + } + } else { + debug_assert_eq!(this_arity.min_len(), spec_arity.min_len()); + + spec_patterns.extend(args); + } + + Some(spec_patterns) + } else { + None + } + } + Some(Anything) => { + // The specialized fields for a `Anything` pattern with a list constructor is just + // `Anything` repeated for the number of times we want to see the list pattern. + spec_patterns.extend(std::iter::repeat(Anything).take(spec_arity.min_len())); + Some(spec_patterns) + } + Some(Ctor(..)) => internal_error!("After type checking, lists and constructors should never align in exhaustiveness checking"), + Some(Literal(..)) => internal_error!("After type checking, lists and literals should never align in exhaustiveness checking"), + None => internal_error!("Empty matrices should not get specialized"), + } +} + +/// INVARIANT: (length row == N) ==> (length result == arity + N - 1) +fn specialize_matrix_by_ctor( + tag_id: TagId, + arity: usize, + old_matrix: &mut PatternMatrix, + matrix: &mut PatternMatrix, +) { + for row in old_matrix.drain(..) { + if let Some(spec_row) = specialize_row_by_ctor(tag_id, arity, row) { + matrix.push(spec_row); + } + } +} + +/// INVARIANT: (length row == N) ==> (length result == arity + N - 1) +fn specialize_row_by_ctor(tag_id: TagId, arity: usize, mut row: Row) -> Option { + let head = row.pop(); + let mut spec_patterns = row; + + match head { + Some(Ctor(_, id, args)) => { + if id == tag_id { + spec_patterns.extend(args); + Some(spec_patterns) + } else { + None + } + } + Some(Anything) => { + spec_patterns.extend(std::iter::repeat(Anything).take(arity)); + Some(spec_patterns) + } + Some(List(..)) => { + internal_error!(r#"After type checking, a constructor can never align with a list"#) + } + Some(Literal(_)) => internal_error!( + r#"After type checking, a constructor can never align with a literal: that should be a type error!"# + ), + None => internal_error!("Empty matrices should not get specialized."), + } +} + +/// INVARIANT: (length row == N) ==> (length result == N-1) +fn specialize_row_by_anything(row: &RefRow) -> Option { + let mut row = row.to_vec(); + + match row.pop() { + Some(Anything) => Some(row), + _ => None, + } +} + +/// ALL CONSTRUCTORS ARE PRESENT? + +pub enum Complete { + Yes(Vec), + No, +} + +fn is_complete(matrix: &RefPatternMatrix) -> Complete { + let ctors = collect_ctors(matrix); + match ctors { + CollectedCtors::NonExhaustiveAny | CollectedCtors::NonExhaustiveList(_) => Complete::No, + CollectedCtors::Ctors(ctors) => { + let length = ctors.len(); + let mut it = ctors.into_iter(); + + match it.next() { + None => Complete::No, + Some((_, Union { alternatives, .. })) => { + if length == alternatives.len() { + Complete::Yes(alternatives) + } else { + Complete::No + } + } + } + } + } +} + +/// COLLECT CTORS + +type RefPatternMatrix = [Vec]; +type PatternMatrix = Vec>; +type RefRow = [Pattern]; +type Row = Vec; + +enum CollectedCtors { + NonExhaustiveAny, + NonExhaustiveList(Vec), + Ctors(MutMap), +} + +fn collect_ctors(matrix: &RefPatternMatrix) -> CollectedCtors { + if matrix.is_empty() { + return CollectedCtors::NonExhaustiveAny; + } + + let first_row = &matrix[0]; + + if let Some(ctor) = first_row.last() { + match ctor { + Anything => CollectedCtors::NonExhaustiveAny, + Pattern::Literal(_) => CollectedCtors::NonExhaustiveAny, + List(_, _) => { + let list_ctors = build_list_ctors_covering_patterns( + ListArity::ANY, + filter_matrix_list_ctors(matrix), + ); + + CollectedCtors::NonExhaustiveList(list_ctors) + } + Pattern::Ctor(_, _, _) => { + let mut ctors = MutMap::default(); + + for row in matrix { + if let Some(Ctor(union, id, _)) = row.last() { + ctors.insert(*id, union.clone()); + } + } + + CollectedCtors::Ctors(ctors) + } + } + } else { + CollectedCtors::NonExhaustiveAny + } +} + +/// Largely derived from Rust's list-pattern exhaustiveness checking algorithm: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_mir_build/thir/pattern/usefulness/index.html +/// Dual-licensed under MIT and Apache licenses. +/// Thank you, Rust contributors. +/// +/// Calculates the list constructors that are covered by a given [slice constructor][ListArity::Slice], +/// relative to other list constructors matched by a series of patterns. +/// +/// This is relevant for both exhaustiveness and redundancy checking; to understand the motivation, +/// let's start with the exhaustiveness checking case. +/// +/// # Exhaustiveness Checking +/// +/// All list constructors are exausted by the pattern [..], which actually represents the infinite +/// series of constructors +/// [] +/// [_] +/// [_, _] +/// ... +/// +/// But we don't need to enumerate that infinite series to check if a series of list patterns is exhaustive - +/// we only need to enumerate a finite number of constructors, up to the largest exact-size list +/// pattern not covered by the patterns, or the largest slice pattern covered by the patterns. +/// +/// ## Exact-sized patterns +/// +/// Say we have patterns +/// [_] -> .. +/// [_, _] -> .. +/// To exhaustiveness-check these patterns, we only need to build the subset of `[..]` constructors +/// [] +/// [_] +/// [_, _] +/// [_, _, _, ..] +/// to cover all list constructors that may or may not be matched by the patterns (in this case +/// not, because `[]` is not matched, and the last constructor `[_, _, _, ..]` is not matched). +/// +/// We include `[_, _, _, ..]` here because during exhaustiveness checking, we specialize list +/// patterns **by exact size**, not by ranges. That means that is we stopped enumerating the +/// constructors needed at `[_, _, ..]`, when specializing the list patterns against `[_, _, ..]`, +/// we would see that the last pattern `[_, _] -> ..` exhausts it. +/// +/// So, in the presence of exact-size constructors, we want to include a slice constructor that is +/// larger than all other exact-size list pattern. +/// +/// ## Slice patterns +/// +/// Say we have patterns +/// [1] -> .. +/// [2, ..] -> .. +/// now it's enough to just build +/// [] +/// [_, ..] +/// as possible constructors, since the last constructor `[_, ..]` will specialize both patterns to +/// [1] -> .. +/// [2] -> .. +/// and if these patterns are exhaustive w.r.t. their arguments (`1` and `2`, which they are not, +/// since number literals are not exhaustive), then the whole pattern must be exhaustive, since the +/// largest slice constructor `[_, ..]` will cover the remaining infinite number of list constructors. +/// +/// You can see that this holds with slice constructors that match elements at their head and tail +/// as well: +/// [{}, ..] -> .. +/// [.., {}] -> .. +/// Here again it's enough to just build the constructors [] and [_, ..] to match against - +/// notice that above slices of arity `1`, the patterns above do not provide any more information, +/// since they match any additional elements at the tail and head, respectively. +/// +/// So, if they are exhaustive at arity `1`, they must be exhaustive at any higher arity. +/// +/// In fact, in this case, if we are matching against `List {}`, the second pattern redundant! +/// +/// # Redundancy checking +/// +/// Redundancy checking (in general, and for list patterns) is the same as exhaustiveness checking, +/// except that instead of checking whether `[..]` is covered by all patterns, we want to check if +/// the list constructor of a pattern introduces any more information than previous patterns we've +/// seen. +/// +/// Let's say we're redundancy checking the pattern marked by `*` +/// [] -> .. +/// [_] -> .. +/// (*) [.., _] -> .. +/// +/// The list constructors this pattern introduces are the infinite series [_], [_, _], ... +/// But the only ones relevant, relevant to the patterns we've already seen, are +/// [_] +/// [_, _] +/// (Notice that the enumeration algorithm is the same as for `[..]` in the presence of exact-size +/// slices, just that the starting size differs - due to the tail matched by this pattern) +/// +/// During checking we'll see that the `[_, _]` pattern is not already covered, so `[.., _]` is in +/// fact not redundant. +/// +/// On the other hand, suppose we have +/// [] -> .. +/// [_, ..] -> .. +/// (*) [.., _] -> .. +/// +/// Again enumerating the relevant constructors of `[.., _]` relative to the other patterns, we find +/// them to be +/// [] +/// [.., _] +/// the first is already matched by the first pattern `[] -> ..`, and the latter specialized to +/// `[_]`, which in fact is covered by the second pattern `[_, ..] -> ..`. So the pattern marked by (*) +/// is indeed redundant. +/// +/// # All together +/// +/// So the idea to cover the infinite # of list constructors enumerated by a [slice][ListArity::Slice], +/// while specializing to the constructors that the user has provided, is as follows: +/// - Build [exact][ListArity::Exact] constructor variants for everything up to the max slice +/// constructor size, L. +/// - Then, the infinite # of list constructors is covered by the [0..L) exact-size constructors, and +/// the last slice constructor, that covers size [L..∞). +/// +/// If we might only see [exact][ListArity::Exact] constructors along the way, we want to pick the +/// max slice size L that is larger than all of those exact size constructors. +/// +/// But for slice constructors, we can just pick the largest slice, since that will cover slices of +/// that size, and any larger size. +/// +/// Putting that together, we calculate L via +/// +/// L = max(max_exact_len + 1, max_prefix_len + max_suffix_len) +fn build_list_ctors_covering_patterns( + list_arity: ListArity, + list_pattern_arities: impl IntoIterator, +) -> std::vec::Vec { + match list_arity { + ListArity::Exact(_) => { + // Exact-size lists can only cover themselves.. + vec![list_arity] + } + ListArity::Slice(prefix_len, suffix_len) => { + let min_len = prefix_len + suffix_len; + + let mut max_exact_len = 0; + let mut max_prefix_len = prefix_len; + let mut max_suffix_len = suffix_len; + + for arity in list_pattern_arities { + match arity { + ListArity::Exact(n) => max_exact_len = max_exact_len.max(n), + ListArity::Slice(prefix, suffix) => { + max_prefix_len = max_prefix_len.max(prefix); + max_suffix_len = max_suffix_len.max(suffix); + } + } + } + + let (inf_cover_prefix, inf_cover_suffix) = { + if max_exact_len + 1 >= max_prefix_len + max_suffix_len { + max_prefix_len = max_exact_len + 1 - max_suffix_len; + } + (max_prefix_len, max_suffix_len) + }; + let l = inf_cover_prefix + inf_cover_suffix; + + let exact_size_lists = (min_len..l) // exclusive + .map(ListArity::Exact); + + exact_size_lists + .chain([ListArity::Slice(inf_cover_prefix, inf_cover_suffix)]) + .collect() + } + } +} + +fn filter_matrix_list_ctors(matrix: &RefPatternMatrix) -> impl Iterator + '_ { + matrix.iter().filter_map(|ctor| match ctor.last() { + Some(List(ar, _)) => Some(*ar), + _ => None, + }) +} diff --git a/crates/compiler/fmt/Cargo.toml b/crates/compiler/fmt/Cargo.toml new file mode 100644 index 0000000000..d72ce03678 --- /dev/null +++ b/crates/compiler/fmt/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "roc_fmt" +description = "The roc code formatter." + +authors.workspace = true +edition.workspace = true +license.workspace = true +version.workspace = true + +[dependencies] +roc_collections = { path = "../collections" } +roc_module = { path = "../module" } +roc_parse = { path = "../parse" } +roc_region = { path = "../region" } + +bumpalo.workspace = true diff --git a/crates/compiler/fmt/src/annotation.rs b/crates/compiler/fmt/src/annotation.rs new file mode 100644 index 0000000000..56762d9c31 --- /dev/null +++ b/crates/compiler/fmt/src/annotation.rs @@ -0,0 +1,787 @@ +use crate::{ + collection::{fmt_collection, Braces}, + spaces::{fmt_comments_only, fmt_spaces, NewlineAt, INDENT}, + Buf, +}; +use roc_parse::ast::{ + AbilityImpls, AssignedField, Collection, Expr, ExtractSpaces, ImplementsAbilities, + ImplementsAbility, ImplementsClause, RecordBuilderField, Tag, TypeAnnotation, TypeHeader, +}; +use roc_parse::ident::UppercaseIdent; +use roc_region::all::Loc; + +/// Does an AST node need parens around it? +/// +/// Usually not, but there are a few cases where it may be required +/// +/// 1. In a function type, function types are in parens +/// +/// a -> b, c -> d +/// (a -> b), c -> d +/// +/// 2. In applications, applications are in brackets +/// This is true in patterns, type annotations and expressions +/// +/// Just (Just a) +/// List (List a) +/// reverse (reverse l) +/// +/// 3. In a chain of binary operators, things like nested defs require parens. +/// +/// a + ( +/// x = 3 +/// x + 1 +/// ) +#[derive(PartialEq, Eq, Clone, Copy, Debug)] +pub enum Parens { + NotNeeded, + InFunctionType, + InApply, + InOperator, +} + +/// In an AST node, do we show newlines around it +/// +/// Sometimes, we only want to show comments, at other times +/// we also want to show newlines. By default the formatter +/// takes care of inserting newlines, but sometimes the user's +/// newlines are taken into account. +#[derive(PartialEq, Eq, Clone, Copy, Debug)] +pub enum Newlines { + No, + Yes, +} + +impl Newlines { + pub fn from_bool(yes: bool) -> Self { + if yes { + Self::Yes + } else { + Self::No + } + } +} + +pub trait Formattable { + fn is_multiline(&self) -> bool; + + fn format_with_options(&self, buf: &mut Buf, _parens: Parens, _newlines: Newlines, indent: u16); + + fn format(&self, buf: &mut Buf, indent: u16) { + self.format_with_options(buf, Parens::NotNeeded, Newlines::No, indent); + } +} + +/// A reference to a formattable value is also formattable +impl<'a, T> Formattable for &'a T +where + T: Formattable, +{ + fn is_multiline(&self) -> bool { + (*self).is_multiline() + } + + fn format_with_options(&self, buf: &mut Buf, parens: Parens, newlines: Newlines, indent: u16) { + (*self).format_with_options(buf, parens, newlines, indent) + } + + fn format(&self, buf: &mut Buf, indent: u16) { + (*self).format(buf, indent) + } +} + +pub fn is_collection_multiline(collection: &Collection<'_, T>) -> bool { + // if there are any comments, they must go on their own line + // because otherwise they'd comment out the closing delimiter + !collection.final_comments().is_empty() || + // if any of the items in the collection are multiline, + // then the whole collection must be multiline + collection.items.iter().any(Formattable::is_multiline) +} + +/// A Located formattable value is also formattable +impl Formattable for Loc +where + T: Formattable, +{ + fn is_multiline(&self) -> bool { + self.value.is_multiline() + } + + fn format_with_options(&self, buf: &mut Buf, parens: Parens, newlines: Newlines, indent: u16) { + self.value + .format_with_options(buf, parens, newlines, indent) + } + + fn format(&self, buf: &mut Buf, indent: u16) { + self.value.format(buf, indent) + } +} + +impl<'a> Formattable for UppercaseIdent<'a> { + fn is_multiline(&self) -> bool { + false + } + + fn format_with_options( + &self, + buf: &mut Buf, + _parens: Parens, + _newlines: Newlines, + _indent: u16, + ) { + buf.push_str((*self).into()) + } +} + +impl<'a> Formattable for TypeAnnotation<'a> { + fn is_multiline(&self) -> bool { + use roc_parse::ast::TypeAnnotation::*; + + match self { + // Return whether these spaces contain any Newlines + SpaceBefore(_, spaces) | SpaceAfter(_, spaces) => { + debug_assert!(!spaces.is_empty()); + + // "spaces" always contain either a newline or comment, and comments have newlines + true + } + + Wildcard | Inferred | BoundVariable(_) | Malformed(_) => false, + Function(args, result) => { + result.value.is_multiline() + || args.iter().any(|loc_arg| loc_arg.value.is_multiline()) + } + Apply(_, _, args) => args.iter().any(|loc_arg| loc_arg.value.is_multiline()), + As(lhs, _, _) => lhs.value.is_multiline(), + + Where(annot, has_clauses) => { + annot.is_multiline() || has_clauses.iter().any(|has| has.is_multiline()) + } + + Tuple { elems: fields, ext } => { + match ext { + Some(ann) if ann.value.is_multiline() => return true, + _ => {} + } + + fields.items.iter().any(|field| field.value.is_multiline()) + } + + Record { fields, ext } => { + match ext { + Some(ann) if ann.value.is_multiline() => return true, + _ => {} + } + + fields.items.iter().any(|field| field.value.is_multiline()) + } + + TagUnion { tags, ext } => { + match ext { + Some(ann) if ann.value.is_multiline() => return true, + _ => {} + } + + tags.iter().any(|tag| tag.value.is_multiline()) + } + } + } + + fn format_with_options(&self, buf: &mut Buf, parens: Parens, newlines: Newlines, indent: u16) { + use roc_parse::ast::TypeAnnotation::*; + + let self_is_multiline = self.is_multiline(); + + match self { + Function(args, ret) => { + let needs_parens = parens != Parens::NotNeeded; + + buf.indent(indent); + + if needs_parens { + buf.push('(') + } + + let mut it = args.iter().enumerate().peekable(); + + while let Some((index, argument)) = it.next() { + let is_first = index == 0; + let is_multiline = &argument.value.is_multiline(); + + if !is_first && !is_multiline && self_is_multiline { + buf.newline(); + } + + argument.value.format_with_options( + buf, + Parens::InFunctionType, + Newlines::Yes, + indent, + ); + + if it.peek().is_some() { + buf.push_str(","); + if !self_is_multiline { + buf.spaces(1); + } + } + } + + if self_is_multiline { + buf.newline(); + buf.indent(indent); + } else { + buf.spaces(1); + } + + buf.push_str("->"); + buf.spaces(1); + + ret.value + .format_with_options(buf, Parens::InFunctionType, Newlines::No, indent); + + if needs_parens { + buf.push(')') + } + } + Apply(pkg, name, arguments) => { + buf.indent(indent); + let write_parens = parens == Parens::InApply && !arguments.is_empty(); + + if write_parens { + buf.push('(') + } + + if !pkg.is_empty() { + buf.push_str(pkg); + buf.push('.'); + } + + buf.push_str(name); + + let needs_indent = except_last(arguments).any(|a| a.is_multiline()) + || arguments + .last() + .map(|a| { + a.is_multiline() + && (!a.extract_spaces().before.is_empty() + || !is_outdentable(&a.value)) + }) + .unwrap_or_default(); + + let arg_indent = if needs_indent { + indent + INDENT + } else { + indent + }; + + for arg in arguments.iter() { + if needs_indent { + let arg = arg.extract_spaces(); + fmt_spaces(buf, arg.before.iter(), arg_indent); + buf.ensure_ends_with_newline(); + arg.item.format_with_options( + buf, + Parens::InApply, + Newlines::Yes, + arg_indent, + ); + fmt_spaces(buf, arg.after.iter(), arg_indent); + } else { + buf.spaces(1); + arg.format_with_options(buf, Parens::InApply, Newlines::No, arg_indent); + } + } + + if write_parens { + buf.push(')') + } + } + BoundVariable(v) => { + buf.indent(indent); + buf.push_str(v) + } + Wildcard => { + buf.indent(indent); + buf.push('*') + } + Inferred => { + buf.indent(indent); + buf.push('_') + } + + TagUnion { tags, ext } => { + fmt_collection(buf, indent, Braces::Square, *tags, newlines); + + if let Some(loc_ext_ann) = *ext { + loc_ext_ann.value.format(buf, indent); + } + } + + Tuple { elems: fields, ext } => { + fmt_collection(buf, indent, Braces::Round, *fields, newlines); + + if let Some(loc_ext_ann) = *ext { + loc_ext_ann.value.format(buf, indent); + } + } + + Record { fields, ext } => { + fmt_collection(buf, indent, Braces::Curly, *fields, newlines); + + if let Some(loc_ext_ann) = *ext { + loc_ext_ann.value.format(buf, indent); + } + } + + As(lhs, _spaces, TypeHeader { name, vars }) => { + // TODO use _spaces? + lhs.value + .format_with_options(buf, Parens::InFunctionType, Newlines::No, indent); + buf.spaces(1); + buf.push_str("as"); + buf.spaces(1); + buf.push_str(name.value); + for var in *vars { + buf.spaces(1); + var.value + .format_with_options(buf, Parens::NotNeeded, Newlines::No, indent); + } + } + + Where(annot, implements_clauses) => { + annot.format_with_options(buf, parens, newlines, indent); + if implements_clauses + .iter() + .any(|implements| implements.is_multiline()) + { + buf.newline(); + buf.indent(indent); + } else { + buf.spaces(1); + } + for (i, has) in implements_clauses.iter().enumerate() { + buf.push_str(if i == 0 { + roc_parse::keyword::WHERE + } else { + "," + }); + buf.spaces(1); + has.format_with_options(buf, parens, newlines, indent); + } + } + + SpaceBefore(ann, spaces) => { + buf.ensure_ends_with_newline(); + fmt_comments_only(buf, spaces.iter(), NewlineAt::Bottom, indent); + ann.format_with_options(buf, parens, newlines, indent) + } + SpaceAfter(ann, spaces) => { + ann.format_with_options(buf, parens, newlines, indent); + fmt_comments_only(buf, spaces.iter(), NewlineAt::Bottom, indent); + } + Malformed(raw) => { + buf.indent(indent); + buf.push_str(raw) + } + } + } +} + +fn is_outdentable(ann: &TypeAnnotation) -> bool { + matches!( + ann.extract_spaces().item, + TypeAnnotation::Tuple { .. } | TypeAnnotation::Record { .. } + ) +} + +/// Fields are subtly different on the type and term level: +/// +/// > type: { x : Int, y : Bool } +/// > term: { x: 100, y: True } +/// +/// So we need two instances, each having the specific separator +impl<'a> Formattable for AssignedField<'a, TypeAnnotation<'a>> { + fn is_multiline(&self) -> bool { + is_multiline_assigned_field_help(self) + } + + fn format_with_options(&self, buf: &mut Buf, _parens: Parens, newlines: Newlines, indent: u16) { + // we abuse the `Newlines` type to decide between multiline or single-line layout + format_assigned_field_help(self, buf, indent, 1, newlines == Newlines::Yes); + } +} + +impl<'a> Formattable for AssignedField<'a, Expr<'a>> { + fn is_multiline(&self) -> bool { + is_multiline_assigned_field_help(self) + } + + fn format_with_options(&self, buf: &mut Buf, _parens: Parens, newlines: Newlines, indent: u16) { + // we abuse the `Newlines` type to decide between multiline or single-line layout + format_assigned_field_help(self, buf, indent, 0, newlines == Newlines::Yes); + } +} + +fn is_multiline_assigned_field_help(afield: &AssignedField<'_, T>) -> bool { + use self::AssignedField::*; + + match afield { + RequiredValue(_, spaces, ann) | OptionalValue(_, spaces, ann) => { + !spaces.is_empty() || ann.value.is_multiline() + } + LabelOnly(_) => false, + AssignedField::SpaceBefore(_, _) | AssignedField::SpaceAfter(_, _) => true, + Malformed(text) => text.chars().any(|c| c == '\n'), + } +} + +fn format_assigned_field_help( + zelf: &AssignedField, + buf: &mut Buf, + indent: u16, + separator_spaces: usize, + is_multiline: bool, +) where + T: Formattable, +{ + use self::AssignedField::*; + + match zelf { + RequiredValue(name, spaces, ann) => { + if is_multiline { + buf.newline(); + } + + buf.indent(indent); + buf.push_str(name.value); + + if !spaces.is_empty() { + fmt_spaces(buf, spaces.iter(), indent); + } + + buf.spaces(separator_spaces); + buf.push(':'); + buf.spaces(1); + ann.value.format(buf, indent); + } + OptionalValue(name, spaces, ann) => { + if is_multiline { + buf.newline(); + buf.indent(indent); + } + + buf.push_str(name.value); + + if !spaces.is_empty() { + fmt_spaces(buf, spaces.iter(), indent); + } + + buf.spaces(separator_spaces); + buf.push('?'); + buf.spaces(1); + ann.value.format(buf, indent); + } + LabelOnly(name) => { + if is_multiline { + buf.newline(); + buf.indent(indent); + } + + buf.push_str(name.value); + } + AssignedField::SpaceBefore(sub_field, spaces) => { + fmt_comments_only(buf, spaces.iter(), NewlineAt::Bottom, indent); + format_assigned_field_help(sub_field, buf, indent, separator_spaces, is_multiline); + } + AssignedField::SpaceAfter(sub_field, spaces) => { + format_assigned_field_help(sub_field, buf, indent, separator_spaces, is_multiline); + fmt_comments_only(buf, spaces.iter(), NewlineAt::Bottom, indent); + } + Malformed(raw) => { + buf.push_str(raw); + } + } +} + +impl<'a> Formattable for RecordBuilderField<'a> { + fn is_multiline(&self) -> bool { + is_multiline_record_builder_field_help(self) + } + + fn format_with_options(&self, buf: &mut Buf, _parens: Parens, newlines: Newlines, indent: u16) { + // we abuse the `Newlines` type to decide between multiline or single-line layout + format_record_builder_field_help(self, buf, indent, newlines == Newlines::Yes); + } +} + +fn is_multiline_record_builder_field_help(afield: &RecordBuilderField<'_>) -> bool { + use self::RecordBuilderField::*; + + match afield { + Value(_, spaces, ann) => !spaces.is_empty() || ann.value.is_multiline(), + ApplyValue(_, colon_spaces, arrow_spaces, ann) => { + !colon_spaces.is_empty() || !arrow_spaces.is_empty() || ann.value.is_multiline() + } + LabelOnly(_) => false, + SpaceBefore(_, _) | SpaceAfter(_, _) => true, + Malformed(text) => text.chars().any(|c| c == '\n'), + } +} + +fn format_record_builder_field_help( + zelf: &RecordBuilderField, + buf: &mut Buf, + indent: u16, + is_multiline: bool, +) { + use self::RecordBuilderField::*; + + match zelf { + Value(name, spaces, ann) => { + if is_multiline { + buf.newline(); + } + + buf.indent(indent); + buf.push_str(name.value); + + if !spaces.is_empty() { + fmt_spaces(buf, spaces.iter(), indent); + } + + buf.push(':'); + buf.spaces(1); + ann.value.format(buf, indent); + } + ApplyValue(name, colon_spaces, arrow_spaces, ann) => { + if is_multiline { + buf.newline(); + buf.indent(indent); + } + + buf.push_str(name.value); + + if !colon_spaces.is_empty() { + fmt_spaces(buf, colon_spaces.iter(), indent); + } + + buf.push(':'); + buf.spaces(1); + + if !arrow_spaces.is_empty() { + fmt_spaces(buf, arrow_spaces.iter(), indent); + } + + buf.push_str("<-"); + buf.spaces(1); + ann.value.format(buf, indent); + } + LabelOnly(name) => { + if is_multiline { + buf.newline(); + buf.indent(indent); + } + + buf.push_str(name.value); + } + SpaceBefore(sub_field, spaces) => { + fmt_comments_only(buf, spaces.iter(), NewlineAt::Bottom, indent); + format_record_builder_field_help(sub_field, buf, indent, is_multiline); + } + SpaceAfter(sub_field, spaces) => { + format_record_builder_field_help(sub_field, buf, indent, is_multiline); + fmt_comments_only(buf, spaces.iter(), NewlineAt::Bottom, indent); + } + Malformed(raw) => { + buf.push_str(raw); + } + } +} + +impl<'a> Formattable for Tag<'a> { + fn is_multiline(&self) -> bool { + use self::Tag::*; + + match self { + Apply { args, .. } => args.iter().any(|arg| arg.value.is_multiline()), + Tag::SpaceBefore(_, _) | Tag::SpaceAfter(_, _) => true, + Malformed(text) => text.chars().any(|c| c == '\n'), + } + } + + fn format_with_options( + &self, + buf: &mut Buf, + _parens: Parens, + _newlines: Newlines, + indent: u16, + ) { + let is_multiline = self.is_multiline(); + + match self { + Tag::Apply { name, args } => { + buf.indent(indent); + buf.push_str(name.value); + if is_multiline { + let arg_indent = indent + INDENT; + + for arg in *args { + buf.newline(); + arg.value.format_with_options( + buf, + Parens::InApply, + Newlines::No, + arg_indent, + ); + } + } else { + for arg in *args { + buf.spaces(1); + arg.format_with_options(buf, Parens::InApply, Newlines::No, indent); + } + } + } + Tag::SpaceBefore(_, _) | Tag::SpaceAfter(_, _) => unreachable!(), + Tag::Malformed(raw) => { + buf.indent(indent); + buf.push_str(raw); + } + } + } +} + +impl<'a> Formattable for ImplementsClause<'a> { + fn is_multiline(&self) -> bool { + // No, always put abilities in an "implements" clause on one line + false + } + + fn format_with_options(&self, buf: &mut Buf, parens: Parens, newlines: Newlines, indent: u16) { + buf.push_str(self.var.value.extract_spaces().item); + buf.spaces(1); + buf.push_str(roc_parse::keyword::IMPLEMENTS); + buf.spaces(1); + + for (i, ab) in self.abilities.iter().enumerate() { + if i > 0 { + buf.spaces(1); + buf.push('&'); + buf.spaces(1); + } + ab.format_with_options(buf, parens, newlines, indent); + } + } +} + +impl<'a> Formattable for AbilityImpls<'a> { + fn is_multiline(&self) -> bool { + match self { + AbilityImpls::SpaceBefore(_, _) | AbilityImpls::SpaceAfter(_, _) => true, + AbilityImpls::AbilityImpls(impls) => is_collection_multiline(impls), + } + } + + fn format_with_options(&self, buf: &mut Buf, parens: Parens, newlines: Newlines, indent: u16) { + match self { + AbilityImpls::AbilityImpls(impls) => { + if newlines == Newlines::Yes { + buf.newline(); + buf.indent(indent); + } + fmt_collection(buf, indent, Braces::Curly, *impls, Newlines::No); + } + AbilityImpls::SpaceBefore(impls, spaces) => { + buf.newline(); + buf.indent(indent); + fmt_comments_only(buf, spaces.iter(), NewlineAt::Bottom, indent); + impls.format_with_options(buf, parens, Newlines::No, indent); + } + AbilityImpls::SpaceAfter(impls, spaces) => { + impls.format_with_options(buf, parens, newlines, indent); + fmt_comments_only(buf, spaces.iter(), NewlineAt::Bottom, indent); + } + } + } +} + +impl<'a> Formattable for ImplementsAbility<'a> { + fn is_multiline(&self) -> bool { + match self { + ImplementsAbility::SpaceAfter(..) | ImplementsAbility::SpaceBefore(..) => true, + ImplementsAbility::ImplementsAbility { ability, impls } => { + ability.is_multiline() || impls.map(|i| i.is_multiline()).unwrap_or(false) + } + } + } + + fn format_with_options(&self, buf: &mut Buf, parens: Parens, newlines: Newlines, indent: u16) { + match self { + ImplementsAbility::ImplementsAbility { ability, impls } => { + if newlines == Newlines::Yes { + buf.newline(); + buf.indent(indent); + } + ability.format_with_options(buf, parens, newlines, indent); + if let Some(impls) = impls { + buf.spaces(1); + impls.format_with_options(buf, parens, newlines, indent); + } + } + ImplementsAbility::SpaceBefore(ab, spaces) => { + buf.newline(); + buf.indent(indent); + fmt_comments_only(buf, spaces.iter(), NewlineAt::Bottom, indent); + ab.format_with_options(buf, parens, Newlines::No, indent) + } + ImplementsAbility::SpaceAfter(ab, spaces) => { + ab.format_with_options(buf, parens, newlines, indent); + fmt_comments_only(buf, spaces.iter(), NewlineAt::Bottom, indent); + } + } + } +} + +impl<'a> Formattable for ImplementsAbilities<'a> { + fn is_multiline(&self) -> bool { + match self { + ImplementsAbilities::SpaceAfter(..) | ImplementsAbilities::SpaceBefore(..) => true, + ImplementsAbilities::Implements(has_abilities) => { + is_collection_multiline(has_abilities) + } + } + } + + fn format_with_options(&self, buf: &mut Buf, parens: Parens, newlines: Newlines, indent: u16) { + match self { + ImplementsAbilities::Implements(has_abilities) => { + if newlines == Newlines::Yes { + buf.newline(); + buf.indent(indent); + } + buf.push_str(roc_parse::keyword::IMPLEMENTS); + buf.spaces(1); + fmt_collection(buf, indent, Braces::Square, *has_abilities, Newlines::No); + } + ImplementsAbilities::SpaceBefore(has_abilities, spaces) => { + buf.newline(); + buf.indent(indent); + fmt_comments_only(buf, spaces.iter(), NewlineAt::Bottom, indent); + has_abilities.format_with_options(buf, parens, Newlines::No, indent) + } + ImplementsAbilities::SpaceAfter(has_abilities, spaces) => { + has_abilities.format_with_options(buf, parens, newlines, indent); + fmt_comments_only(buf, spaces.iter(), NewlineAt::Bottom, indent); + } + } + } +} + +pub fn except_last(items: &[T]) -> impl Iterator { + if items.is_empty() { + items.iter() + } else { + items[..items.len() - 1].iter() + } +} diff --git a/crates/compiler/fmt/src/collection.rs b/crates/compiler/fmt/src/collection.rs new file mode 100644 index 0000000000..3d241dafca --- /dev/null +++ b/crates/compiler/fmt/src/collection.rs @@ -0,0 +1,145 @@ +use roc_parse::ast::{Collection, CommentOrNewline, ExtractSpaces}; + +use crate::{ + annotation::{is_collection_multiline, Formattable, Newlines}, + spaces::{fmt_comments_only, NewlineAt, INDENT}, + Buf, +}; + +#[derive(PartialEq, Eq, Clone, Copy, Debug)] +pub enum Braces { + Round, + Square, + Curly, +} + +pub fn fmt_collection<'a, 'buf, T: ExtractSpaces<'a> + Formattable>( + buf: &mut Buf<'buf>, + indent: u16, + braces: Braces, + items: Collection<'a, T>, + newline: Newlines, +) where + >::Item: Formattable, +{ + let start = match braces { + Braces::Round => '(', + Braces::Curly => '{', + Braces::Square => '[', + }; + + let end = match braces { + Braces::Round => ')', + Braces::Curly => '}', + Braces::Square => ']', + }; + + if is_collection_multiline(&items) { + let braces_indent = indent; + let item_indent = braces_indent + INDENT; + if newline == Newlines::Yes { + buf.ensure_ends_with_newline(); + } + buf.indent(braces_indent); + buf.push(start); + + for (index, item) in items.iter().enumerate() { + let is_first_item = index == 0; + let item = item.extract_spaces(); + let is_only_newlines = item.before.iter().all(|s| s.is_newline()); + + if item.before.is_empty() || is_only_newlines { + buf.ensure_ends_with_newline(); + } else { + if is_first_item { + // The first item in a multiline collection always begins with exactly + // one newline (so the delimiter is at the end of its own line), + // and that newline appears before the first comment (if there is one). + buf.ensure_ends_with_newline(); + } else { + if item.before.starts_with(&[CommentOrNewline::Newline]) { + buf.ensure_ends_with_newline(); + } + + if item + .before + .starts_with(&[CommentOrNewline::Newline, CommentOrNewline::Newline]) + { + // If there's a comment, and it's not on the first item, + // and it's preceded by at least one blank line, maintain 1 blank line. + // (We already ensured that it ends in a newline, so this will turn that + // into a blank line.) + + buf.newline(); + } + } + + fmt_comments_only(buf, item.before.iter(), NewlineAt::None, item_indent); + + if !is_only_newlines { + if item.before.ends_with(&[CommentOrNewline::Newline]) { + buf.newline(); + } + + buf.newline(); + } + } + + buf.indent(item_indent); + item.item.format(buf, item_indent); + + buf.push(','); + + if !item.after.is_empty() { + if item.after.iter().any(|s| s.is_newline()) { + buf.newline(); + } + + fmt_comments_only(buf, item.after.iter(), NewlineAt::None, item_indent); + } + } + + if items.final_comments().iter().any(|s| s.is_newline()) { + buf.newline(); + } + + if items + .final_comments() + .starts_with(&[CommentOrNewline::Newline, CommentOrNewline::Newline]) + { + buf.newline(); + } + + fmt_comments_only( + buf, + items.final_comments().iter(), + NewlineAt::None, + item_indent, + ); + + buf.ensure_ends_with_newline(); + buf.indent(braces_indent); + } else { + // is_multiline == false + // there is no comment to add + buf.indent(indent); + buf.push(start); + let mut iter = items.iter().enumerate().peekable(); + while let Some((index, item)) = iter.next() { + if braces == Braces::Curly || index != 0 { + buf.spaces(1); + } + + item.format(buf, indent); + if iter.peek().is_some() { + buf.push(','); + } + } + + if !items.is_empty() && braces == Braces::Curly { + buf.spaces(1); + } + } + + buf.push(end); +} diff --git a/crates/compiler/fmt/src/def.rs b/crates/compiler/fmt/src/def.rs new file mode 100644 index 0000000000..456ff34b34 --- /dev/null +++ b/crates/compiler/fmt/src/def.rs @@ -0,0 +1,451 @@ +use crate::annotation::{Formattable, Newlines, Parens}; +use crate::pattern::fmt_pattern; +use crate::spaces::{fmt_default_newline, fmt_spaces, INDENT}; +use crate::Buf; +use roc_parse::ast::{ + AbilityMember, Defs, Expr, ExtractSpaces, Pattern, Spaces, StrLiteral, TypeAnnotation, TypeDef, + TypeHeader, ValueDef, +}; +use roc_region::all::Loc; + +/// A Located formattable value is also formattable + +impl<'a> Formattable for Defs<'a> { + fn is_multiline(&self) -> bool { + !self.tags.is_empty() + } + + fn format_with_options( + &self, + buf: &mut Buf, + _parens: Parens, + _newlines: Newlines, + indent: u16, + ) { + let mut prev_spaces = true; + + for (index, def) in self.defs().enumerate() { + let spaces_before = &self.spaces[self.space_before[index].indices()]; + let spaces_after = &self.spaces[self.space_after[index].indices()]; + + if prev_spaces { + fmt_spaces(buf, spaces_before.iter(), indent); + } else { + fmt_default_newline(buf, spaces_before, indent); + } + + match def { + Ok(type_def) => type_def.format(buf, indent), + Err(value_def) => value_def.format(buf, indent), + } + + fmt_spaces(buf, spaces_after.iter(), indent); + + prev_spaces = !spaces_after.is_empty(); + } + } +} + +impl<'a> Formattable for TypeDef<'a> { + fn is_multiline(&self) -> bool { + use roc_parse::ast::TypeDef::*; + + match self { + Alias { ann, .. } => ann.is_multiline(), + Opaque { typ, .. } => typ.is_multiline(), + Ability { members, .. } => members.iter().any(|d| d.is_multiline()), + } + } + + fn format_with_options(&self, buf: &mut Buf, _parens: Parens, newlines: Newlines, indent: u16) { + use roc_parse::ast::TypeDef::*; + + match self { + Alias { + header: TypeHeader { name, vars }, + ann, + } => { + buf.indent(indent); + buf.push_str(name.value); + + for var in *vars { + buf.spaces(1); + + let need_parens = matches!(var.value, Pattern::Apply(..)); + + if need_parens { + buf.push_str("("); + } + + fmt_pattern(buf, &var.value, indent, Parens::NotNeeded); + buf.indent(indent); + + if need_parens { + buf.push_str(")"); + } + } + + buf.push_str(" :"); + buf.spaces(1); + + ann.format(buf, indent) + } + Opaque { + header, + typ: ann, + derived: has_abilities, + } => { + let ann_is_where_clause = + matches!(ann.extract_spaces().item, TypeAnnotation::Where(..)); + + // Always put the has-abilities clause on a newline if the opaque annotation + // contains a where-has clause. + let has_abilities_multiline = if let Some(has_abilities) = has_abilities { + !has_abilities.value.is_empty() && ann_is_where_clause + } else { + false + }; + + let make_multiline = ann.is_multiline() || has_abilities_multiline; + + fmt_general_def(header, buf, indent, ":=", &ann.value, newlines); + + if let Some(has_abilities) = has_abilities { + buf.spaces(1); + + has_abilities.format_with_options( + buf, + Parens::NotNeeded, + Newlines::from_bool(make_multiline), + indent + INDENT, + ); + } + } + Ability { + header: TypeHeader { name, vars }, + loc_implements: _, + members, + } => { + buf.indent(indent); + buf.push_str(name.value); + for var in *vars { + buf.spaces(1); + fmt_pattern(buf, &var.value, indent, Parens::NotNeeded); + buf.indent(indent); + } + buf.spaces(1); + buf.push_str(roc_parse::keyword::IMPLEMENTS); + + if !self.is_multiline() { + debug_assert_eq!(members.len(), 1); + buf.spaces(1); + members[0].format_with_options( + buf, + Parens::NotNeeded, + Newlines::No, + indent + INDENT, + ); + } else { + for member in members.iter() { + member.format_with_options( + buf, + Parens::NotNeeded, + Newlines::Yes, + indent + INDENT, + ); + } + } + } + } + } +} + +impl<'a> Formattable for TypeHeader<'a> { + fn is_multiline(&self) -> bool { + self.vars.iter().any(|v| v.is_multiline()) + } + + fn format_with_options( + &self, + buf: &mut Buf, + _parens: Parens, + _newlines: Newlines, + indent: u16, + ) { + buf.indent(indent); + buf.push_str(self.name.value); + + for var in self.vars.iter() { + buf.spaces(1); + fmt_pattern(buf, &var.value, indent, Parens::NotNeeded); + buf.indent(indent); + } + } +} + +impl<'a> Formattable for ValueDef<'a> { + fn is_multiline(&self) -> bool { + use roc_parse::ast::ValueDef::*; + + match self { + Annotation(loc_pattern, loc_annotation) => { + loc_pattern.is_multiline() || loc_annotation.is_multiline() + } + Body(loc_pattern, loc_expr) => loc_pattern.is_multiline() || loc_expr.is_multiline(), + AnnotatedBody { .. } => true, + Expect { condition, .. } => condition.is_multiline(), + ExpectFx { condition, .. } => condition.is_multiline(), + Dbg { condition, .. } => condition.is_multiline(), + } + } + + fn format_with_options(&self, buf: &mut Buf, _parens: Parens, newlines: Newlines, indent: u16) { + use roc_parse::ast::ValueDef::*; + match self { + Annotation(loc_pattern, loc_annotation) => { + fmt_general_def( + loc_pattern, + buf, + indent, + ":", + &loc_annotation.value, + newlines, + ); + } + Body(loc_pattern, loc_expr) => { + fmt_body(buf, &loc_pattern.value, &loc_expr.value, indent); + } + Dbg { condition, .. } => fmt_dbg_in_def(buf, condition, self.is_multiline(), indent), + Expect { condition, .. } => fmt_expect(buf, condition, self.is_multiline(), indent), + ExpectFx { condition, .. } => { + fmt_expect_fx(buf, condition, self.is_multiline(), indent) + } + AnnotatedBody { + ann_pattern, + ann_type, + comment, + body_pattern, + body_expr, + } => { + fmt_general_def(ann_pattern, buf, indent, ":", &ann_type.value, newlines); + + if let Some(comment_str) = comment { + buf.push_str(" #"); + buf.spaces(1); + buf.push_str(comment_str.trim()); + } + + buf.newline(); + fmt_body(buf, &body_pattern.value, &body_expr.value, indent); + } + } + } +} + +fn fmt_general_def( + lhs: L, + buf: &mut Buf, + indent: u16, + sep: &str, + rhs: &TypeAnnotation, + newlines: Newlines, +) { + lhs.format(buf, indent); + buf.indent(indent); + + if rhs.is_multiline() { + buf.spaces(1); + buf.push_str(sep); + buf.spaces(1); + + let should_outdent = should_outdent(rhs); + + if should_outdent { + match rhs { + TypeAnnotation::SpaceBefore(sub_def, _) => { + sub_def.format_with_options(buf, Parens::NotNeeded, Newlines::No, indent); + } + _ => { + rhs.format_with_options(buf, Parens::NotNeeded, Newlines::No, indent); + } + } + } else { + rhs.format_with_options(buf, Parens::NotNeeded, newlines, indent + INDENT); + } + } else { + buf.spaces(1); + buf.push_str(sep); + buf.spaces(1); + rhs.format_with_options(buf, Parens::NotNeeded, Newlines::No, indent); + } +} + +fn should_outdent(mut rhs: &TypeAnnotation) -> bool { + loop { + match rhs { + TypeAnnotation::SpaceBefore(sub_def, spaces) => { + let is_only_newlines = spaces.iter().all(|s| s.is_newline()); + if !is_only_newlines || !sub_def.is_multiline() { + return false; + } + rhs = sub_def; + } + TypeAnnotation::Where(ann, _clauses) => { + if !ann.is_multiline() { + return false; + } + rhs = &ann.value; + } + TypeAnnotation::Record { .. } | TypeAnnotation::TagUnion { .. } => return true, + _ => return false, + } + } +} + +fn fmt_dbg_in_def<'a>( + buf: &mut Buf, + condition: &'a Loc>, + is_multiline: bool, + indent: u16, +) { + buf.ensure_ends_with_newline(); + buf.indent(indent); + buf.push_str("dbg"); + + let return_indent = if is_multiline { + buf.newline(); + indent + INDENT + } else { + buf.spaces(1); + indent + }; + + condition.format(buf, return_indent); +} + +fn fmt_expect<'a>(buf: &mut Buf, condition: &'a Loc>, is_multiline: bool, indent: u16) { + buf.ensure_ends_with_newline(); + buf.indent(indent); + buf.push_str("expect"); + + let return_indent = if is_multiline { + buf.newline(); + indent + INDENT + } else { + buf.spaces(1); + indent + }; + + condition.format(buf, return_indent); +} + +fn fmt_expect_fx<'a>(buf: &mut Buf, condition: &'a Loc>, is_multiline: bool, indent: u16) { + buf.ensure_ends_with_newline(); + buf.indent(indent); + buf.push_str("expect-fx"); + + let return_indent = if is_multiline { + buf.newline(); + indent + INDENT + } else { + buf.spaces(1); + indent + }; + + condition.format(buf, return_indent); +} + +pub fn fmt_value_def(buf: &mut Buf, def: &roc_parse::ast::ValueDef, indent: u16) { + def.format(buf, indent); +} + +pub fn fmt_type_def(buf: &mut Buf, def: &roc_parse::ast::TypeDef, indent: u16) { + def.format(buf, indent); +} + +pub fn fmt_defs(buf: &mut Buf, defs: &Defs, indent: u16) { + defs.format(buf, indent); +} + +pub fn fmt_body<'a>(buf: &mut Buf, pattern: &'a Pattern<'a>, body: &'a Expr<'a>, indent: u16) { + pattern.format_with_options(buf, Parens::InApply, Newlines::No, indent); + buf.indent(indent); + buf.push_str(" ="); + + if body.is_multiline() { + match body { + Expr::SpaceBefore(sub_def, spaces) => { + let should_outdent = match sub_def { + Expr::Record { .. } | Expr::List { .. } => { + let is_only_newlines = spaces.iter().all(|s| s.is_newline()); + is_only_newlines && sub_def.is_multiline() + } + _ => false, + }; + + if should_outdent { + buf.spaces(1); + sub_def.format_with_options(buf, Parens::NotNeeded, Newlines::Yes, indent); + } else { + body.format_with_options( + buf, + Parens::NotNeeded, + Newlines::Yes, + indent + INDENT, + ); + } + } + Expr::Defs(..) | Expr::BinOps(_, _) | Expr::Backpassing(..) => { + // Binop chains always get a newline. Otherwise you can have things like: + // + // something = foo + // |> bar baz + // + // By always inserting a newline, this becomes: + // + // something = + // foo + // |> bar baz + // + // This makes it clear what the binop is applying to! + buf.newline(); + body.format_with_options(buf, Parens::NotNeeded, Newlines::Yes, indent + INDENT); + } + Expr::When(..) | Expr::Str(StrLiteral::Block(_)) => { + buf.ensure_ends_with_newline(); + body.format_with_options(buf, Parens::NotNeeded, Newlines::Yes, indent + INDENT); + } + _ => { + buf.spaces(1); + body.format_with_options(buf, Parens::NotNeeded, Newlines::Yes, indent); + } + } + } else { + buf.spaces(1); + body.format_with_options(buf, Parens::NotNeeded, Newlines::Yes, indent); + } +} + +impl<'a> Formattable for AbilityMember<'a> { + fn is_multiline(&self) -> bool { + self.name.value.is_multiline() || self.typ.is_multiline() + } + + fn format_with_options( + &self, + buf: &mut Buf, + _parens: Parens, + _newlines: Newlines, + indent: u16, + ) { + let Spaces { before, item, .. } = self.name.value.extract_spaces(); + fmt_spaces(buf, before.iter(), indent); + + buf.indent(indent); + buf.push_str(item); + buf.spaces(1); + buf.push(':'); + buf.spaces(1); + self.typ.value.format(buf, indent + INDENT); + } +} diff --git a/crates/compiler/fmt/src/expr.rs b/crates/compiler/fmt/src/expr.rs new file mode 100644 index 0000000000..39e6bce4ee --- /dev/null +++ b/crates/compiler/fmt/src/expr.rs @@ -0,0 +1,1673 @@ +use crate::annotation::{except_last, is_collection_multiline, Formattable, Newlines, Parens}; +use crate::collection::{fmt_collection, Braces}; +use crate::def::fmt_defs; +use crate::pattern::fmt_pattern; +use crate::spaces::{ + count_leading_newlines, fmt_comments_only, fmt_spaces, fmt_spaces_no_blank_lines, NewlineAt, + INDENT, +}; +use crate::Buf; +use roc_module::called_via::{self, BinOp}; +use roc_parse::ast::{ + AssignedField, Base, Collection, CommentOrNewline, Expr, ExtractSpaces, Pattern, + RecordBuilderField, WhenBranch, +}; +use roc_parse::ast::{StrLiteral, StrSegment}; +use roc_parse::ident::Accessor; +use roc_region::all::Loc; + +impl<'a> Formattable for Expr<'a> { + fn is_multiline(&self) -> bool { + use roc_parse::ast::Expr::*; + // TODO cache these answers using a Map, so + // we don't have to traverse subexpressions repeatedly + + match self { + // Return whether these spaces contain any Newlines + SpaceBefore(_sub_expr, spaces) | SpaceAfter(_sub_expr, spaces) => { + debug_assert!(!spaces.is_empty()); + + // "spaces" always contain either a newline or comment, and comments have newlines + true + } + + // These expressions never have newlines + Float(..) + | Num(..) + | NonBase10Int { .. } + | SingleQuote(_) + | RecordAccess(_, _) + | AccessorFunction(_) + | TupleAccess(_, _) + | Var { .. } + | Underscore { .. } + | MalformedIdent(_, _) + | MalformedClosure + | Tag(_) + | OpaqueRef(_) + | IngestedFile(_, _) + | Crash => false, + + // These expressions always have newlines + Defs(_, _) | When(_, _) => true, + + List(items) => is_collection_multiline(items), + + Str(literal) => is_str_multiline(literal), + Apply(loc_expr, args, _) => { + loc_expr.is_multiline() || args.iter().any(|loc_arg| loc_arg.is_multiline()) + } + + Expect(condition, continuation) => { + condition.is_multiline() || continuation.is_multiline() + } + Dbg(condition, continuation) => condition.is_multiline() || continuation.is_multiline(), + LowLevelDbg(_, _) => unreachable!( + "LowLevelDbg should only exist after desugaring, not during formatting" + ), + + If(branches, final_else) => { + final_else.is_multiline() + || branches + .iter() + .any(|(c, t)| c.is_multiline() || t.is_multiline()) + } + + BinOps(lefts, loc_right) => { + lefts.iter().any(|(loc_expr, _)| loc_expr.is_multiline()) + || loc_right.is_multiline() + } + + UnaryOp(loc_subexpr, _) + | PrecedenceConflict(roc_parse::ast::PrecedenceConflict { + expr: loc_subexpr, .. + }) + | MultipleRecordBuilders(loc_subexpr) + | UnappliedRecordBuilder(loc_subexpr) => loc_subexpr.is_multiline(), + + ParensAround(subexpr) => subexpr.is_multiline(), + + Closure(loc_patterns, loc_body) => { + // check the body first because it's more likely to be multiline + loc_body.is_multiline() + || loc_patterns + .iter() + .any(|loc_pattern| loc_pattern.is_multiline()) + } + Backpassing(loc_patterns, loc_body, loc_ret) => { + // check the body first because it's more likely to be multiline + loc_body.is_multiline() + || loc_ret.is_multiline() + || loc_patterns + .iter() + .any(|loc_pattern| loc_pattern.is_multiline()) + } + + Record(fields) => is_collection_multiline(fields), + Tuple(fields) => is_collection_multiline(fields), + RecordUpdate { fields, .. } => is_collection_multiline(fields), + RecordBuilder(fields) => is_collection_multiline(fields), + } + } + + fn format_with_options(&self, buf: &mut Buf, parens: Parens, newlines: Newlines, indent: u16) { + use self::Expr::*; + + let apply_needs_parens = parens == Parens::InApply; + + match self { + SpaceBefore(sub_expr, spaces) => { + format_spaces(buf, spaces, newlines, indent); + sub_expr.format_with_options(buf, parens, newlines, indent); + } + SpaceAfter(sub_expr, spaces) => { + sub_expr.format_with_options(buf, parens, newlines, indent); + format_spaces(buf, spaces, newlines, indent); + } + ParensAround(sub_expr) => { + if parens == Parens::NotNeeded && !sub_expr_requests_parens(sub_expr) { + sub_expr.format_with_options(buf, Parens::NotNeeded, newlines, indent); + } else { + let should_add_newlines = match sub_expr { + Expr::Closure(..) + | Expr::SpaceBefore(..) + | Expr::SpaceAfter(Closure(..), ..) => false, + _ => sub_expr.is_multiline(), + }; + + buf.indent(indent); + buf.push('('); + if should_add_newlines { + buf.newline(); + } + + let next_indent = if starts_with_newline(sub_expr) || should_add_newlines { + match sub_expr { + Expr::Closure(..) | Expr::SpaceAfter(Closure(..), ..) => indent, + _ => indent + INDENT, + } + } else { + indent + }; + + sub_expr.format_with_options( + buf, + Parens::NotNeeded, + Newlines::Yes, + next_indent, + ); + + if !matches!(sub_expr, Expr::SpaceAfter(..)) && should_add_newlines { + buf.newline(); + } + buf.indent(indent); + buf.push(')'); + } + } + Str(literal) => { + fmt_str_literal(buf, *literal, indent); + } + Var { module_name, ident } => { + buf.indent(indent); + if !module_name.is_empty() { + buf.push_str(module_name); + buf.push('.'); + } + + buf.push_str(ident); + } + Underscore(name) => { + buf.indent(indent); + buf.push('_'); + buf.push_str(name); + } + Crash => { + buf.indent(indent); + buf.push_str("crash"); + } + Apply(loc_expr, loc_args, _) => { + // Sadly this assertion fails in practice. The fact that the parser produces code like this is going to + // confuse the formatter, because it depends on being able to "see" spaces that logically come before the inner + // expr in several places - which is necessarily the case when the `loc_expr` of the apply itself has + // SpaceBefore. + // + // TODO: enforce in the type system that spaces must be pushed to the "outside". + // In other words, Expr::Apply should look something like the following, and there shouldn't be Expr::SpaceBefore and ::SpaceAfter. + // + // ``` + // Apply(&'a SpaceAfter>>, &'a [&'a SpaceBefore>>], CalledVia), + // ``` + // + // assert!(loc_expr.extract_spaces().before.is_empty(), "{:#?}", self); + + buf.indent(indent); + if apply_needs_parens && !loc_args.is_empty() { + buf.push('('); + } + + // should_reflow_outdentable, aka should we transform this: + // + // ``` + // foo bar + // [ + // 1, + // 2, + // ] + // ``` + // + // Into this: + // + // ``` + // foo bar [ + // 1, + // 2, + // ] + // ``` + let should_reflow_outdentable = loc_expr.extract_spaces().after.is_empty() + && except_last(loc_args).all(|a| !a.is_multiline()) + && loc_args + .last() + .map(|a| { + a.extract_spaces().item.is_multiline() + && matches!( + a.value.extract_spaces().item, + Expr::Tuple(_) + | Expr::List(_) + | Expr::Record(_) + | Expr::RecordBuilder(_) + ) + && a.extract_spaces().before == [CommentOrNewline::Newline] + }) + .unwrap_or_default(); + + let needs_indent = !should_reflow_outdentable + && (!loc_expr.extract_spaces().after.is_empty() + || except_last(loc_args).any(|a| a.is_multiline()) + || loc_args + .last() + .map(|a| { + a.is_multiline() + && (!a.extract_spaces().before.is_empty() + || !is_outdentable(&a.value)) + }) + .unwrap_or_default()); + + let arg_indent = if needs_indent { + indent + INDENT + } else { + indent + }; + + let expr_needs_parens = + matches!(loc_expr.value.extract_spaces().item, Expr::Closure(..)) + && !loc_args.is_empty(); + + if expr_needs_parens { + buf.push('('); + } + + loc_expr.format_with_options(buf, Parens::InApply, Newlines::Yes, indent); + + if expr_needs_parens { + buf.indent(indent); + buf.push(')'); + } + + for loc_arg in loc_args.iter() { + if should_reflow_outdentable { + buf.spaces(1); + + // Ignore any comments+newlines before/after. + // We checked above that there's only a single newline before the last arg, + // which we're intentionally ignoring. + + let arg = loc_arg.extract_spaces(); + arg.item.format_with_options( + buf, + Parens::InApply, + Newlines::Yes, + arg_indent, + ); + } else if needs_indent { + let arg = loc_arg.extract_spaces(); + fmt_spaces(buf, arg.before.iter(), arg_indent); + buf.ensure_ends_with_newline(); + arg.item.format_with_options( + buf, + Parens::InApply, + Newlines::Yes, + arg_indent, + ); + fmt_spaces(buf, arg.after.iter(), arg_indent); + } else { + buf.spaces(1); + loc_arg.format_with_options( + buf, + Parens::InApply, + Newlines::Yes, + arg_indent, + ); + } + } + + if apply_needs_parens && !loc_args.is_empty() { + buf.push(')'); + } + } + &Num(string) => { + buf.indent(indent); + buf.push_str(string); + } + &Float(string) => { + buf.indent(indent); + buf.push_str(string); + } + Tag(string) | OpaqueRef(string) => { + buf.indent(indent); + buf.push_str(string) + } + SingleQuote(string) => { + buf.indent(indent); + format_sq_literal(buf, string); + } + &NonBase10Int { + base, + string, + is_negative, + } => { + buf.indent(indent); + if is_negative { + buf.push('-'); + } + + match base { + Base::Hex => buf.push_str("0x"), + Base::Octal => buf.push_str("0o"), + Base::Binary => buf.push_str("0b"), + Base::Decimal => { /* nothing */ } + } + + buf.push_str(string); + } + Record(fields) => { + fmt_record_like( + buf, + None, + *fields, + indent, + format_assigned_field_multiline, + assigned_field_to_space_before, + ); + } + RecordUpdate { update, fields } => { + fmt_record_like( + buf, + Some(*update), + *fields, + indent, + format_assigned_field_multiline, + assigned_field_to_space_before, + ); + } + RecordBuilder(fields) => { + fmt_record_like( + buf, + None, + *fields, + indent, + format_record_builder_field_multiline, + record_builder_field_to_space_before, + ); + } + Closure(loc_patterns, loc_ret) => { + fmt_closure(buf, loc_patterns, loc_ret, indent); + } + Backpassing(loc_patterns, loc_body, loc_ret) => { + fmt_backpassing(buf, loc_patterns, loc_body, loc_ret, indent); + } + Defs(defs, ret) => { + { + let indent = if parens == Parens::InOperator { + buf.indent(indent); + buf.push('('); + buf.newline(); + indent + INDENT + } else { + indent + }; + + // It should theoretically be impossible to *parse* an empty defs list. + // (Canonicalization can remove defs later, but that hasn't happened yet!) + debug_assert!(!defs.is_empty()); + + fmt_defs(buf, defs, indent); + + match &ret.value { + SpaceBefore(sub_expr, spaces) => { + buf.spaces(1); + fmt_spaces(buf, spaces.iter(), indent); + + buf.indent(indent); + + sub_expr.format_with_options( + buf, + Parens::NotNeeded, + Newlines::Yes, + indent, + ); + } + _ => { + buf.ensure_ends_with_newline(); + buf.indent(indent); + // Even if there were no defs, which theoretically should never happen, + // still print the return value. + ret.format_with_options(buf, Parens::NotNeeded, Newlines::Yes, indent); + } + } + } + + if parens == Parens::InOperator { + buf.ensure_ends_with_newline(); + buf.indent(indent); + buf.push(')'); + } + } + Expect(condition, continuation) => { + fmt_expect(buf, condition, continuation, self.is_multiline(), indent); + } + Dbg(condition, continuation) => { + fmt_dbg(buf, condition, continuation, self.is_multiline(), indent); + } + LowLevelDbg(_, _) => unreachable!( + "LowLevelDbg should only exist after desugaring, not during formatting" + ), + If(branches, final_else) => { + fmt_if(buf, branches, final_else, self.is_multiline(), indent); + } + When(loc_condition, branches) => fmt_when(buf, loc_condition, branches, indent), + Tuple(items) => fmt_collection(buf, indent, Braces::Round, *items, Newlines::No), + List(items) => fmt_collection(buf, indent, Braces::Square, *items, Newlines::No), + BinOps(lefts, right) => fmt_binops(buf, lefts, right, false, indent), + UnaryOp(sub_expr, unary_op) => { + buf.indent(indent); + match &unary_op.value { + called_via::UnaryOp::Negate => { + buf.push('-'); + } + called_via::UnaryOp::Not => { + buf.push('!'); + } + } + + let needs_newline = match &sub_expr.value { + SpaceBefore(..) => true, + Str(text) => is_str_multiline(text), + _ => false, + }; + let needs_parens = + needs_newline && matches!(unary_op.value, called_via::UnaryOp::Negate); + + if needs_parens { + // Unary negation can't be followed by whitespace (which is what a newline is) - so + // we need to wrap the negated value in parens. + buf.push('('); + } + + let inner_indent = if needs_parens { + indent + INDENT + } else { + indent + }; + + sub_expr.format_with_options(buf, Parens::InApply, newlines, inner_indent); + + if needs_parens { + buf.push(')'); + } + } + AccessorFunction(key) => { + buf.indent(indent); + buf.push('.'); + match key { + Accessor::RecordField(key) => buf.push_str(key), + Accessor::TupleIndex(key) => buf.push_str(key), + } + } + RecordAccess(expr, key) => { + expr.format_with_options(buf, Parens::InApply, Newlines::Yes, indent); + buf.push('.'); + buf.push_str(key); + } + TupleAccess(expr, key) => { + expr.format_with_options(buf, Parens::InApply, Newlines::Yes, indent); + buf.push('.'); + buf.push_str(key); + } + MalformedIdent(str, _) => { + buf.indent(indent); + buf.push_str(str) + } + MalformedClosure => {} + PrecedenceConflict { .. } => {} + MultipleRecordBuilders { .. } => {} + UnappliedRecordBuilder { .. } => {} + IngestedFile(_, _) => {} + } + } +} + +fn is_str_multiline(literal: &StrLiteral) -> bool { + use roc_parse::ast::StrLiteral::*; + + match literal { + PlainLine(string) => { + // When a PlainLine contains '\n' or '"', format as a block string + string.contains('"') || string.contains('\n') + } + Line(_) => { + // If this had any newlines, it'd have parsed as Block. + false + } + Block(_) => { + // Block strings are always formatted on multiple lines, + // even if the string is only a single line. + true + } + } +} + +fn needs_unicode_escape(ch: char) -> bool { + matches!(ch, '\u{0000}'..='\u{001f}' | '\u{007f}'..='\u{009f}') +} + +pub(crate) fn format_sq_literal(buf: &mut Buf, s: &str) { + buf.push('\''); + for c in s.chars() { + if c == '"' { + buf.push_char_literal('"') + } else { + match c { + '"' => buf.push_str("\""), + '\'' => buf.push_str("\\\'"), + '\t' => buf.push_str("\\t"), + '\r' => buf.push_str("\\r"), + '\n' => buf.push_str("\\n"), + '\\' => buf.push_str("\\\\"), + _ => { + if needs_unicode_escape(c) { + buf.push_str(&format!("\\u({:x})", c as u32)) + } else { + buf.push_char_literal(c) + } + } + } + } + } + buf.push('\''); +} + +fn is_outdentable(expr: &Expr) -> bool { + matches!( + expr.extract_spaces().item, + Expr::Tuple(_) + | Expr::List(_) + | Expr::Record(_) + | Expr::RecordBuilder(_) + | Expr::Closure(..) + ) +} + +fn starts_with_newline(expr: &Expr) -> bool { + use roc_parse::ast::Expr::*; + + match expr { + SpaceBefore(_, comment_or_newline) => { + matches!(comment_or_newline.first(), Some(CommentOrNewline::Newline)) + } + _ => false, + } +} + +fn format_str_segment(seg: &StrSegment, buf: &mut Buf, indent: u16) { + use StrSegment::*; + + match seg { + Plaintext(string) => { + // Lines in block strings will end with Plaintext ending in "\n" to indicate + // a line break in the input string + match string.strip_suffix('\n') { + Some(string_without_newline) => { + buf.push_str_allow_spaces(string_without_newline); + buf.newline(); + } + None => buf.push_str_allow_spaces(string), + } + } + Unicode(loc_str) => { + buf.push_str("\\u("); + buf.push_str(loc_str.value); // e.g. "00A0" in "\u(00A0)" + buf.push(')'); + } + EscapedChar(escaped) => { + buf.push('\\'); + buf.push(escaped.to_parsed_char()); + } + Interpolated(loc_expr) => { + buf.push_str("\\("); + // e.g. (name) in "Hi, \(name)!" + loc_expr.value.format_with_options( + buf, + Parens::NotNeeded, // We already printed parens! + Newlines::No, // Interpolations can never have newlines + indent, + ); + buf.push(')'); + } + } +} + +fn push_op(buf: &mut Buf, op: BinOp) { + match op { + called_via::BinOp::Caret => buf.push('^'), + called_via::BinOp::Star => buf.push('*'), + called_via::BinOp::Slash => buf.push('/'), + called_via::BinOp::DoubleSlash => buf.push_str("//"), + called_via::BinOp::Percent => buf.push('%'), + called_via::BinOp::Plus => buf.push('+'), + called_via::BinOp::Minus => buf.push('-'), + called_via::BinOp::Equals => buf.push_str("=="), + called_via::BinOp::NotEquals => buf.push_str("!="), + called_via::BinOp::LessThan => buf.push('<'), + called_via::BinOp::GreaterThan => buf.push('>'), + called_via::BinOp::LessThanOrEq => buf.push_str("<="), + called_via::BinOp::GreaterThanOrEq => buf.push_str(">="), + called_via::BinOp::And => buf.push_str("&&"), + called_via::BinOp::Or => buf.push_str("||"), + called_via::BinOp::Pizza => buf.push_str("|>"), + called_via::BinOp::Assignment => unreachable!(), + called_via::BinOp::IsAliasType => unreachable!(), + called_via::BinOp::IsOpaqueType => unreachable!(), + called_via::BinOp::Backpassing => unreachable!(), + } +} + +pub fn fmt_str_literal(buf: &mut Buf, literal: StrLiteral, indent: u16) { + use roc_parse::ast::StrLiteral::*; + + match literal { + PlainLine(string) => { + // When a PlainLine contains '\n' or '"', format as a block string + if string.contains('"') || string.contains('\n') { + buf.ensure_ends_with_newline(); + buf.indent(indent); + buf.push_str("\"\"\""); + buf.push_newline_literal(); + for line in string.split('\n') { + buf.indent(indent); + buf.push_str_allow_spaces(line); + buf.push_newline_literal(); + } + buf.indent(indent); + buf.push_str("\"\"\""); + } else { + buf.indent(indent); + buf.push('"'); + buf.push_str_allow_spaces(string); + buf.push('"'); + }; + } + Line(segments) => { + buf.indent(indent); + buf.push('"'); + for seg in segments.iter() { + format_str_segment(seg, buf, 0) + } + buf.push('"'); + } + Block(lines) => { + // Block strings will always be formatted with """ on new lines + buf.ensure_ends_with_newline(); + buf.indent(indent); + buf.push_str("\"\"\""); + buf.push_newline_literal(); + + for segments in lines.iter() { + for seg in segments.iter() { + // only add indent if the line isn't empty + if *seg != StrSegment::Plaintext("\n") { + buf.indent(indent); + format_str_segment(seg, buf, indent); + } else { + buf.push_newline_literal(); + } + } + + buf.push_newline_literal(); + } + buf.indent(indent); + buf.push_str("\"\"\""); + } + } +} + +fn fmt_binops<'a>( + buf: &mut Buf, + lefts: &'a [(Loc>, Loc)], + loc_right_side: &'a Loc>, + part_of_multi_line_binops: bool, + indent: u16, +) { + let is_multiline = part_of_multi_line_binops + || loc_right_side.value.is_multiline() + || lefts.iter().any(|(expr, _)| expr.value.is_multiline()); + + for (loc_left_side, loc_binop) in lefts { + let binop = loc_binop.value; + + loc_left_side.format_with_options(buf, Parens::InOperator, Newlines::No, indent); + + if is_multiline { + buf.ensure_ends_with_newline(); + buf.indent(indent); + } else { + buf.spaces(1); + } + + push_op(buf, binop); + + buf.spaces(1); + } + + loc_right_side.format_with_options(buf, Parens::InOperator, Newlines::Yes, indent); +} + +fn format_spaces(buf: &mut Buf, spaces: &[CommentOrNewline], newlines: Newlines, indent: u16) { + match newlines { + Newlines::Yes => { + fmt_spaces(buf, spaces.iter(), indent); + } + Newlines::No => { + fmt_comments_only(buf, spaces.iter(), NewlineAt::Bottom, indent); + } + } +} + +fn is_when_patterns_multiline(when_branch: &WhenBranch) -> bool { + let patterns = when_branch.patterns; + let (first_pattern, rest) = patterns.split_first().unwrap(); + + let is_multiline_patterns = if let Some((last_pattern, inner_patterns)) = rest.split_last() { + !first_pattern.value.extract_spaces().after.is_empty() + || !last_pattern.value.extract_spaces().before.is_empty() + || inner_patterns.iter().any(|p| { + let spaces = p.value.extract_spaces(); + !spaces.before.is_empty() || !spaces.after.is_empty() + }) + } else { + false + }; + + is_multiline_patterns +} + +fn fmt_when<'a>( + buf: &mut Buf, + loc_condition: &'a Loc>, + branches: &[&'a WhenBranch<'a>], + indent: u16, +) { + let is_multiline_condition = loc_condition.is_multiline(); + buf.ensure_ends_with_newline(); + buf.indent(indent); + buf.push_str("when"); + if is_multiline_condition { + let condition_indent = indent + INDENT; + + match &loc_condition.value { + Expr::SpaceBefore(expr_below, spaces_above_expr) => { + fmt_comments_only( + buf, + spaces_above_expr.iter(), + NewlineAt::Top, + condition_indent, + ); + buf.newline(); + match &expr_below { + Expr::SpaceAfter(expr_above, spaces_below_expr) => { + // If any of the spaces is a newline, add a newline at the top. + // Otherwise leave it as just a comment. + let newline_at = if spaces_below_expr + .iter() + .any(|spaces| matches!(spaces, CommentOrNewline::Newline)) + { + NewlineAt::Top + } else { + NewlineAt::None + }; + + expr_above.format(buf, condition_indent); + fmt_comments_only( + buf, + spaces_below_expr.iter(), + newline_at, + condition_indent, + ); + buf.newline(); + } + _ => { + expr_below.format(buf, condition_indent); + } + } + } + _ => { + buf.newline(); + loc_condition.format(buf, condition_indent); + buf.newline(); + } + } + buf.indent(indent); + } else { + buf.spaces(1); + loc_condition.format(buf, indent); + buf.spaces(1); + } + buf.push_str("is"); + buf.newline(); + + let mut prev_branch_was_multiline = false; + + for (branch_index, branch) in branches.iter().enumerate() { + let expr = &branch.value; + let patterns = &branch.patterns; + let is_multiline_expr = expr.is_multiline(); + let is_multiline_patterns = is_when_patterns_multiline(branch); + + for (pattern_index, pattern) in patterns.iter().enumerate() { + if pattern_index == 0 { + match &pattern.value { + Pattern::SpaceBefore(sub_pattern, spaces) => { + let added_blank_line; + + if branch_index > 0 // Never render newlines before the first branch. + && matches!(spaces.first(), Some(CommentOrNewline::Newline)) + { + if prev_branch_was_multiline { + // Multiline branches always get a full blank line after them. + buf.ensure_ends_with_blank_line(); + added_blank_line = true; + } else { + buf.ensure_ends_with_newline(); + added_blank_line = false; + } + } else { + added_blank_line = false; + } + + // Write comments (which may have been attached to the previous + // branch's expr, if there was a previous branch). + fmt_comments_only(buf, spaces.iter(), NewlineAt::Bottom, indent + INDENT); + + if branch_index > 0 { + if prev_branch_was_multiline && !added_blank_line { + // Multiline branches always get a full blank line after them + // (which we may already have added before a comment). + buf.ensure_ends_with_blank_line(); + } else { + buf.ensure_ends_with_newline(); + } + } + + fmt_pattern(buf, sub_pattern, indent + INDENT, Parens::NotNeeded); + } + other => { + if branch_index > 0 { + if prev_branch_was_multiline { + // Multiline branches always get a full blank line after them. + buf.ensure_ends_with_blank_line(); + } else { + buf.ensure_ends_with_newline(); + } + } + + fmt_pattern(buf, other, indent + INDENT, Parens::NotNeeded); + } + } + } else { + if is_multiline_patterns { + buf.ensure_ends_with_newline(); + buf.indent(indent + INDENT); + buf.push('|'); + } else { + buf.push_str(" |"); + } + + buf.spaces(1); + + fmt_pattern(buf, &pattern.value, indent + INDENT, Parens::NotNeeded); + } + } + + if let Some(guard_expr) = &branch.guard { + buf.push_str(" if"); + buf.spaces(1); + guard_expr.format_with_options(buf, Parens::NotNeeded, Newlines::Yes, indent + INDENT); + } + + buf.push_str(" ->"); + + match expr.value { + Expr::SpaceBefore(nested, spaces) => { + fmt_spaces_no_blank_lines(buf, spaces.iter(), indent + (INDENT * 2)); + + if is_multiline_expr { + buf.ensure_ends_with_newline(); + } else { + buf.spaces(1); + } + + nested.format_with_options( + buf, + Parens::NotNeeded, + Newlines::Yes, + indent + 2 * INDENT, + ); + } + _ => { + if is_multiline_expr { + buf.ensure_ends_with_newline(); + } else { + buf.spaces(1); + } + + expr.format_with_options( + buf, + Parens::NotNeeded, + Newlines::Yes, + indent + 2 * INDENT, + ); + } + } + + prev_branch_was_multiline = is_multiline_expr || is_multiline_patterns; + } +} + +fn fmt_dbg<'a>( + buf: &mut Buf, + condition: &'a Loc>, + continuation: &'a Loc>, + is_multiline: bool, + indent: u16, +) { + buf.ensure_ends_with_newline(); + buf.indent(indent); + buf.push_str("dbg"); + + let return_indent = if is_multiline { + buf.newline(); + indent + INDENT + } else { + buf.spaces(1); + indent + }; + + condition.format(buf, return_indent); + + // Always put a blank line after the `dbg` line(s) + buf.ensure_ends_with_blank_line(); + + continuation.format(buf, indent); +} + +fn fmt_expect<'a>( + buf: &mut Buf, + condition: &'a Loc>, + continuation: &'a Loc>, + is_multiline: bool, + indent: u16, +) { + buf.ensure_ends_with_newline(); + buf.indent(indent); + buf.push_str("expect"); + + let return_indent = if is_multiline { + buf.newline(); + indent + INDENT + } else { + buf.spaces(1); + indent + }; + + condition.format(buf, return_indent); + + // Always put a blank line after the `expect` line(s) + buf.ensure_ends_with_blank_line(); + + continuation.format(buf, indent); +} + +fn fmt_if<'a>( + buf: &mut Buf, + branches: &'a [(Loc>, Loc>)], + final_else: &'a Loc>, + is_multiline: bool, + indent: u16, +) { + // let is_multiline_then = loc_then.is_multiline(); + // let is_multiline_else = final_else.is_multiline(); + // let is_multiline_condition = loc_condition.is_multiline(); + // let is_multiline = is_multiline_then || is_multiline_else || is_multiline_condition; + + let return_indent = if is_multiline { + indent + INDENT + } else { + indent + }; + + for (i, (loc_condition, loc_then)) in branches.iter().enumerate() { + let is_multiline_condition = loc_condition.is_multiline(); + + buf.indent(indent); + + if i > 0 { + buf.push_str("else"); + buf.spaces(1); + } + + buf.push_str("if"); + + if is_multiline_condition { + match &loc_condition.value { + Expr::SpaceBefore(expr_below, spaces_before_expr) => { + fmt_comments_only( + buf, + spaces_before_expr.iter(), + NewlineAt::Top, + return_indent, + ); + buf.newline(); + + match &expr_below { + Expr::SpaceAfter(expr_above, spaces_after_expr) => { + expr_above.format(buf, return_indent); + + // If any of the spaces is a newline, add a newline at the top. + // Otherwise leave it as just a comment. + let newline_at = if spaces_after_expr + .iter() + .any(|spaces| matches!(spaces, CommentOrNewline::Newline)) + { + NewlineAt::Top + } else { + NewlineAt::None + }; + + fmt_comments_only( + buf, + spaces_after_expr.iter(), + newline_at, + return_indent, + ); + buf.newline(); + } + + _ => { + expr_below.format(buf, return_indent); + } + } + } + + Expr::SpaceAfter(expr_above, spaces_below_expr) => { + buf.newline(); + expr_above.format(buf, return_indent); + fmt_comments_only(buf, spaces_below_expr.iter(), NewlineAt::Top, return_indent); + buf.newline(); + } + + _ => { + buf.newline(); + loc_condition.format(buf, return_indent); + buf.newline(); + } + } + buf.indent(indent); + } else { + buf.spaces(1); + loc_condition.format_with_options(buf, Parens::NotNeeded, Newlines::Yes, indent); + buf.spaces(1); + } + + buf.push_str("then"); + + if is_multiline { + match &loc_then.value { + Expr::SpaceBefore(expr_below, spaces_below) => { + // we want exactly one newline, user-inserted extra newlines are ignored. + buf.newline(); + fmt_comments_only(buf, spaces_below.iter(), NewlineAt::Bottom, return_indent); + + match &expr_below { + Expr::SpaceAfter(expr_above, spaces_above) => { + expr_above.format(buf, return_indent); + + // If any of the spaces is a newline, add a newline at the top. + // Otherwise leave it as just a comment. + let newline_at = if spaces_above + .iter() + .any(|spaces| matches!(spaces, CommentOrNewline::Newline)) + { + NewlineAt::Top + } else { + NewlineAt::None + }; + + fmt_comments_only(buf, spaces_above.iter(), newline_at, return_indent); + buf.newline(); + } + + _ => { + expr_below.format(buf, return_indent); + } + } + } + _ => { + buf.newline(); + loc_then.format(buf, return_indent); + buf.newline(); + } + } + } else { + buf.push_str(""); + buf.spaces(1); + loc_then.format(buf, return_indent); + } + } + + buf.indent(indent); + if is_multiline { + buf.push_str("else"); + buf.newline(); + } else { + buf.push_str(" else"); + buf.spaces(1); + } + + final_else.format(buf, return_indent); +} + +fn fmt_closure<'a>( + buf: &mut Buf, + loc_patterns: &'a [Loc>], + loc_ret: &'a Loc>, + indent: u16, +) { + use self::Expr::*; + + buf.indent(indent); + buf.push('\\'); + + let arguments_are_multiline = loc_patterns + .iter() + .any(|loc_pattern| loc_pattern.is_multiline()); + + // If the arguments are multiline, go down a line and indent. + let indent = if arguments_are_multiline { + indent + INDENT + } else { + indent + }; + + let mut it = loc_patterns.iter().peekable(); + + while let Some(loc_pattern) = it.next() { + loc_pattern.format(buf, indent); + + if it.peek().is_some() { + buf.indent(indent); + if arguments_are_multiline { + buf.push(','); + buf.newline(); + } else { + buf.push_str(","); + buf.spaces(1); + } + } + } + + if arguments_are_multiline { + buf.newline(); + buf.indent(indent); + } else { + buf.spaces(1); + } + + buf.push_str("->"); + + let is_multiline = loc_ret.value.is_multiline(); + + // If the body is multiline, go down a line and indent. + let body_indent = if is_multiline { + indent + INDENT + } else { + indent + }; + + // the body of the Closure can be on the same line, or + // on a new line. If it's on the same line, insert a space. + + match &loc_ret.value { + SpaceBefore(_, _) => { + // the body starts with (first comment and then) a newline + // do nothing + } + _ => { + // add a space after the `->` + buf.spaces(1); + } + }; + + if is_multiline { + match &loc_ret.value { + SpaceBefore(sub_expr, spaces) => { + let should_outdent = match sub_expr { + Record { .. } | List { .. } => { + let is_only_newlines = spaces.iter().all(|s| s.is_newline()); + is_only_newlines && sub_expr.is_multiline() + } + _ => false, + }; + + if should_outdent { + buf.spaces(1); + sub_expr.format_with_options(buf, Parens::NotNeeded, Newlines::Yes, indent); + } else { + loc_ret.format_with_options(buf, Parens::NotNeeded, Newlines::Yes, body_indent); + } + } + Record { .. } | List { .. } => { + loc_ret.format_with_options(buf, Parens::NotNeeded, Newlines::Yes, indent); + } + _ => { + loc_ret.format_with_options(buf, Parens::NotNeeded, Newlines::Yes, body_indent); + } + } + } else { + loc_ret.format_with_options(buf, Parens::NotNeeded, Newlines::Yes, body_indent); + } +} + +fn fmt_backpassing<'a>( + buf: &mut Buf, + loc_patterns: &'a [Loc>], + loc_body: &'a Loc>, + loc_ret: &'a Loc>, + indent: u16, +) { + use self::Expr::*; + + let arguments_are_multiline = loc_patterns + .iter() + .any(|loc_pattern| loc_pattern.is_multiline()); + + // If the arguments are multiline, go down a line and indent. + let indent = if arguments_are_multiline { + indent + INDENT + } else { + indent + }; + + let mut it = loc_patterns.iter().peekable(); + + while let Some(loc_pattern) = it.next() { + let needs_parens = if pattern_needs_parens_when_backpassing(&loc_pattern.value) { + Parens::InApply + } else { + Parens::NotNeeded + }; + + loc_pattern.format_with_options(buf, needs_parens, Newlines::No, indent); + + if it.peek().is_some() { + if arguments_are_multiline { + buf.push(','); + buf.newline(); + } else { + buf.push_str(","); + buf.spaces(1); + } + } + } + + if arguments_are_multiline { + buf.newline(); + buf.indent(indent); + } else { + buf.spaces(1); + } + + buf.push_str("<-"); + + let is_multiline = loc_ret.value.is_multiline(); + + // If the body is multiline, go down a line and indent. + let body_indent = if is_multiline { + indent + INDENT + } else { + indent + }; + + // the body of the Backpass can be on the same line, or + // on a new line. If it's on the same line, insert a space. + + match &loc_body.value { + SpaceBefore(_, _) => { + // the body starts with (first comment and then) a newline + // do nothing + } + _ => { + // add a space after the `<-` + buf.spaces(1); + } + }; + + loc_body.format_with_options(buf, Parens::NotNeeded, Newlines::Yes, body_indent); + loc_ret.format_with_options(buf, Parens::NotNeeded, Newlines::Yes, indent); +} + +fn pattern_needs_parens_when_backpassing(pat: &Pattern) -> bool { + match pat { + Pattern::Apply(_, _) => true, + Pattern::SpaceBefore(a, _) | Pattern::SpaceAfter(a, _) => { + pattern_needs_parens_when_backpassing(a) + } + _ => false, + } +} + +fn fmt_record_like<'a, Field, Format, ToSpaceBefore>( + buf: &mut Buf, + update: Option<&'a Loc>>, + fields: Collection<'a, Loc>, + indent: u16, + format_field_multiline: Format, + to_space_before: ToSpaceBefore, +) where + Field: Formattable, + Format: Fn(&mut Buf, &Field, u16, &str), + ToSpaceBefore: Fn(&'a Field) -> Option<(&'a Field, &'a [CommentOrNewline<'a>])>, +{ + let loc_fields = fields.items; + let final_comments = fields.final_comments(); + buf.indent(indent); + if loc_fields.is_empty() && final_comments.iter().all(|c| c.is_newline()) && update.is_none() { + buf.push_str("{}"); + } else { + buf.push('{'); + + match update { + None => {} + // We are presuming this to be a Var() + // If it wasnt a Var() we would not have made + // it this far. For example "{ 4 & hello = 9 }" + // doesnt make sense. + Some(record_var) => { + buf.spaces(1); + record_var.format(buf, indent); + buf.push_str(" &"); + } + } + + let is_multiline = loc_fields.iter().any(|loc_field| loc_field.is_multiline()) + || !final_comments.is_empty(); + + if is_multiline { + let field_indent = indent + INDENT; + for (index, field) in loc_fields.iter().enumerate() { + // comma addition is handled by the `format_field_multiline` function + // since we can have stuff like: + // { x # comment + // , y + // } + // In this case, we have to move the comma before the comment. + + let is_first_item = index == 0; + if let Some((_sub_field, spaces)) = to_space_before(&field.value) { + let is_only_newlines = spaces.iter().all(|s| s.is_newline()); + if !is_first_item + && !is_only_newlines + && count_leading_newlines(spaces.iter()) > 1 + { + buf.newline(); + } + + fmt_comments_only(buf, spaces.iter(), NewlineAt::Top, field_indent); + + if !is_only_newlines && count_leading_newlines(spaces.iter().rev()) > 0 { + buf.newline(); + } + } + + format_field_multiline(buf, &field.value, field_indent, ""); + } + + if count_leading_newlines(final_comments.iter()) > 1 { + buf.newline(); + } + + fmt_comments_only(buf, final_comments.iter(), NewlineAt::Top, field_indent); + + buf.newline(); + } else { + // is_multiline == false + buf.spaces(1); + let field_indent = indent; + let mut iter = loc_fields.iter().peekable(); + while let Some(field) = iter.next() { + field.format_with_options(buf, Parens::NotNeeded, Newlines::No, field_indent); + + if iter.peek().is_some() { + buf.push_str(","); + buf.spaces(1); + } + } + buf.spaces(1); + // if we are here, that means that `final_comments` is empty, thus we don't have + // to add a comment. Anyway, it is not possible to have a single line record with + // a comment in it. + }; + + // closes the initial bracket + buf.indent(indent); + buf.push('}'); + } +} + +fn format_assigned_field_multiline( + buf: &mut Buf, + field: &AssignedField, + indent: u16, + separator_prefix: &str, +) where + T: Formattable, +{ + use self::AssignedField::*; + match field { + RequiredValue(name, spaces, ann) => { + buf.newline(); + buf.indent(indent); + buf.push_str(name.value); + + if !spaces.is_empty() { + fmt_spaces(buf, spaces.iter(), indent); + buf.indent(indent); + } + + buf.push_str(separator_prefix); + buf.push_str(":"); + buf.spaces(1); + ann.value.format(buf, indent); + buf.push(','); + } + OptionalValue(name, spaces, ann) => { + buf.newline(); + buf.indent(indent); + buf.push_str(name.value); + + if !spaces.is_empty() { + fmt_spaces(buf, spaces.iter(), indent); + buf.indent(indent); + } + + buf.push_str(separator_prefix); + buf.push_str("?"); + buf.spaces(1); + ann.value.format(buf, indent); + buf.push(','); + } + LabelOnly(name) => { + buf.newline(); + buf.indent(indent); + buf.push_str(name.value); + buf.push(','); + } + AssignedField::SpaceBefore(sub_field, _spaces) => { + // We have something like that: + // ``` + // # comment + // field, + // ``` + // we'd like to preserve this + + format_assigned_field_multiline(buf, sub_field, indent, separator_prefix); + } + AssignedField::SpaceAfter(sub_field, spaces) => { + // We have something like that: + // ``` + // field # comment + // , otherfield + // ``` + // we'd like to transform it into: + // ``` + // field, + // # comment + // otherfield + // ``` + format_assigned_field_multiline(buf, sub_field, indent, separator_prefix); + fmt_comments_only(buf, spaces.iter(), NewlineAt::Top, indent); + } + Malformed(raw) => { + buf.push_str(raw); + } + } +} + +fn assigned_field_to_space_before<'a, T>( + field: &'a AssignedField<'a, T>, +) -> Option<(&AssignedField<'a, T>, &'a [CommentOrNewline<'a>])> { + match field { + AssignedField::SpaceBefore(sub_field, spaces) => Some((sub_field, spaces)), + _ => None, + } +} + +fn format_record_builder_field_multiline( + buf: &mut Buf, + field: &RecordBuilderField, + indent: u16, + separator_prefix: &str, +) { + use self::RecordBuilderField::*; + match field { + Value(name, spaces, ann) => { + buf.newline(); + buf.indent(indent); + buf.push_str(name.value); + + if !spaces.is_empty() { + fmt_spaces(buf, spaces.iter(), indent); + buf.indent(indent); + } + + buf.push_str(separator_prefix); + buf.push_str(":"); + + if ann.value.is_multiline() { + buf.newline(); + ann.value.format(buf, indent + INDENT); + } else { + buf.spaces(1); + ann.value.format(buf, indent); + } + + buf.push(','); + } + ApplyValue(name, colon_spaces, arrow_spaces, ann) => { + buf.newline(); + buf.indent(indent); + buf.push_str(name.value); + + if !colon_spaces.is_empty() { + fmt_spaces(buf, colon_spaces.iter(), indent); + buf.indent(indent); + } + + buf.push_str(separator_prefix); + buf.push(':'); + buf.spaces(1); + + if !arrow_spaces.is_empty() { + fmt_spaces(buf, arrow_spaces.iter(), indent); + buf.indent(indent + INDENT); + } + + buf.push_str("<-"); + + if ann.value.is_multiline() { + buf.newline(); + ann.value.format(buf, indent + INDENT); + } else { + buf.spaces(1); + ann.value.format(buf, indent); + } + buf.push(','); + } + LabelOnly(name) => { + buf.newline(); + buf.indent(indent); + buf.push_str(name.value); + buf.push(','); + } + SpaceBefore(sub_field, _spaces) => { + // We have something like that: + // ``` + // # comment + // field, + // ``` + // we'd like to preserve this + + format_record_builder_field_multiline(buf, sub_field, indent, separator_prefix); + } + SpaceAfter(sub_field, spaces) => { + // We have something like that: + // ``` + // field # comment + // , otherfield + // ``` + // we'd like to transform it into: + // ``` + // field, + // # comment + // otherfield + // ``` + format_record_builder_field_multiline(buf, sub_field, indent, separator_prefix); + fmt_comments_only(buf, spaces.iter(), NewlineAt::Top, indent); + } + Malformed(raw) => { + buf.push_str(raw); + } + } +} + +fn record_builder_field_to_space_before<'a>( + field: &'a RecordBuilderField<'a>, +) -> Option<(&RecordBuilderField<'a>, &'a [CommentOrNewline<'a>])> { + match field { + RecordBuilderField::SpaceBefore(sub_field, spaces) => Some((sub_field, spaces)), + _ => None, + } +} + +fn sub_expr_requests_parens(expr: &Expr<'_>) -> bool { + match expr { + Expr::BinOps(left_side, _) => { + left_side + .iter() + .any(|(_, loc_binop)| match loc_binop.value { + BinOp::Caret + | BinOp::Star + | BinOp::Slash + | BinOp::DoubleSlash + | BinOp::Percent + | BinOp::Plus + | BinOp::Minus + | BinOp::Equals + | BinOp::NotEquals + | BinOp::LessThan + | BinOp::GreaterThan + | BinOp::LessThanOrEq + | BinOp::GreaterThanOrEq + | BinOp::And + | BinOp::Or + | BinOp::Pizza => true, + BinOp::Assignment + | BinOp::IsAliasType + | BinOp::IsOpaqueType + | BinOp::Backpassing => false, + }) + } + Expr::If(_, _) => true, + Expr::SpaceBefore(e, _) => sub_expr_requests_parens(e), + Expr::SpaceAfter(e, _) => sub_expr_requests_parens(e), + _ => false, + } +} diff --git a/crates/compiler/fmt/src/lib.rs b/crates/compiler/fmt/src/lib.rs new file mode 100644 index 0000000000..88a4974bf0 --- /dev/null +++ b/crates/compiler/fmt/src/lib.rs @@ -0,0 +1,168 @@ +//! The roc code formatter. +#![warn(clippy::dbg_macro)] +// See github.com/roc-lang/roc/issues/800 for discussion of the large_enum_variant check. +#![allow(clippy::large_enum_variant)] +pub mod annotation; +pub mod collection; +pub mod def; +pub mod expr; +pub mod module; +pub mod pattern; +pub mod spaces; + +use bumpalo::{collections::String, Bump}; +use roc_parse::ast::Module; + +#[derive(Debug)] +pub struct Ast<'a> { + pub module: Module<'a>, + pub defs: roc_parse::ast::Defs<'a>, +} + +#[derive(Debug)] +pub struct Buf<'a> { + text: String<'a>, + spaces_to_flush: usize, + newlines_to_flush: usize, + beginning_of_line: bool, +} + +impl<'a> Buf<'a> { + pub fn new_in(arena: &'a Bump) -> Buf<'a> { + Buf { + text: String::new_in(arena), + spaces_to_flush: 0, + newlines_to_flush: 0, + beginning_of_line: true, + } + } + + pub fn as_str(&'a self) -> &'a str { + self.text.as_str() + } + + pub fn into_bump_str(self) -> &'a str { + self.text.into_bump_str() + } + + pub fn indent(&mut self, indent: u16) { + if self.beginning_of_line { + self.spaces_to_flush = indent as usize; + } + self.beginning_of_line = false; + } + + pub fn push(&mut self, ch: char) { + debug_assert!(!self.beginning_of_line); + debug_assert!( + ch != '\n', + "Don't call buf.push('\\n') - rather, call buf.newline()" + ); + debug_assert!( + ch != ' ', + "Don't call buf.push(' ') - rather, call buf.spaces(1)" + ); + + self.flush_spaces(); + + self.text.push(ch); + } + + pub fn push_str_allow_spaces(&mut self, s: &str) { + debug_assert!( + !self.beginning_of_line, + "push_str: `{s}` with text:\n{}", + self.text + ); + + self.flush_spaces(); + + self.text.push_str(s); + } + + pub fn push_str(&mut self, s: &str) { + debug_assert!( + !self.beginning_of_line, + "push_str: `{s}` with text:\n{}", + self.text + ); + debug_assert!(!s.contains('\n')); + debug_assert!(!s.ends_with(' ')); + + if !s.is_empty() { + self.flush_spaces(); + } + + self.text.push_str(s); + } + + pub fn push_char_literal(&mut self, c: char) { + self.flush_spaces(); + + self.text.push(c); + } + + pub fn spaces(&mut self, count: usize) { + self.spaces_to_flush += count; + } + + /// Only for use in emitting newlines in block strings, which don't follow the rule of + /// having at most two newlines in a row. + pub fn push_newline_literal(&mut self) { + self.spaces_to_flush = 0; + self.newlines_to_flush += 1; + self.beginning_of_line = true; + } + + pub fn newline(&mut self) { + self.spaces_to_flush = 0; + self.newlines_to_flush = std::cmp::min(self.newlines_to_flush + 1, 2); + self.beginning_of_line = true; + } + + /// Ensures the current buffer ends in a newline, if it didn't already. + /// Doesn't add a newline if the buffer already ends in one. + pub fn ensure_ends_with_newline(&mut self) { + if !self.text.is_empty() && self.newlines_to_flush == 0 { + self.newline() + } + } + + pub fn ensure_ends_with_blank_line(&mut self) { + if !self.text.is_empty() && self.newlines_to_flush < 2 { + self.spaces_to_flush = 0; + self.newlines_to_flush = 2; + self.beginning_of_line = true; + } + } + + fn flush_spaces(&mut self) { + for _ in 0..self.newlines_to_flush { + self.text.push('\n'); + } + self.newlines_to_flush = 0; + + for _ in 0..self.spaces_to_flush { + self.text.push(' '); + } + self.spaces_to_flush = 0; + } + + /// Ensures the text ends in a newline with no whitespace preceding it. + pub fn fmt_end_of_file(&mut self) { + self.ensure_ends_with_newline(); + self.flush_spaces(); + } + + pub fn ends_with_space(&self) -> bool { + self.spaces_to_flush > 0 || self.text.ends_with(' ') + } + + pub fn ends_with_newline(&self) -> bool { + self.newlines_to_flush > 0 || self.text.ends_with('\n') + } + + fn is_empty(&self) -> bool { + self.spaces_to_flush == 0 && self.text.is_empty() + } +} diff --git a/crates/compiler/fmt/src/module.rs b/crates/compiler/fmt/src/module.rs new file mode 100644 index 0000000000..47cc2b7731 --- /dev/null +++ b/crates/compiler/fmt/src/module.rs @@ -0,0 +1,511 @@ +use crate::annotation::{is_collection_multiline, Formattable, Newlines, Parens}; +use crate::collection::{fmt_collection, Braces}; +use crate::expr::fmt_str_literal; +use crate::spaces::RemoveSpaces; +use crate::spaces::{fmt_comments_only, fmt_default_spaces, fmt_spaces, NewlineAt, INDENT}; +use crate::Buf; +use bumpalo::Bump; +use roc_parse::ast::{Collection, Header, Module, Spaced, Spaces}; +use roc_parse::header::{ + AppHeader, ExposedName, ExposesKeyword, GeneratesKeyword, HostedHeader, ImportsEntry, + ImportsKeyword, InterfaceHeader, Keyword, KeywordItem, ModuleName, PackageEntry, PackageHeader, + PackageKeyword, PackageName, PackagesKeyword, PlatformHeader, PlatformRequires, + ProvidesKeyword, ProvidesTo, RequiresKeyword, To, ToKeyword, TypedIdent, WithKeyword, +}; +use roc_parse::ident::UppercaseIdent; +use roc_region::all::Loc; + +pub fn fmt_module<'a>(buf: &mut Buf<'_>, module: &'a Module<'a>) { + fmt_comments_only(buf, module.comments.iter(), NewlineAt::Bottom, 0); + match &module.header { + Header::Interface(header) => { + fmt_interface_header(buf, header); + } + Header::App(header) => { + fmt_app_header(buf, header); + } + Header::Package(header) => { + fmt_package_header(buf, header); + } + Header::Platform(header) => { + fmt_platform_header(buf, header); + } + Header::Hosted(header) => { + fmt_hosted_header(buf, header); + } + } +} + +macro_rules! keywords { + ($($name:ident),* $(,)?) => { + $( + impl Formattable for $name { + fn is_multiline(&self) -> bool { + false + } + + fn format_with_options( + &self, + buf: &mut Buf<'_>, + _parens: crate::annotation::Parens, + _newlines: Newlines, + indent: u16, + ) { + buf.indent(indent); + buf.push_str($name::KEYWORD); + } + } + + impl<'a> RemoveSpaces<'a> for $name { + fn remove_spaces(&self, _arena: &'a Bump) -> Self { + *self + } + } + )* + } +} + +keywords! { + ExposesKeyword, + ImportsKeyword, + WithKeyword, + GeneratesKeyword, + PackageKeyword, + PackagesKeyword, + RequiresKeyword, + ProvidesKeyword, + ToKeyword, +} + +impl Formattable for Option { + fn is_multiline(&self) -> bool { + if let Some(v) = self { + v.is_multiline() + } else { + false + } + } + + fn format_with_options( + &self, + buf: &mut Buf, + parens: crate::annotation::Parens, + newlines: Newlines, + indent: u16, + ) { + if let Some(v) = self { + v.format_with_options(buf, parens, newlines, indent); + } + } +} + +impl<'a> Formattable for ProvidesTo<'a> { + fn is_multiline(&self) -> bool { + if let Some(types) = &self.types { + if is_collection_multiline(types) { + return true; + } + } + self.provides_keyword.is_multiline() + || is_collection_multiline(&self.entries) + || self.to_keyword.is_multiline() + } + + fn format_with_options( + &self, + buf: &mut Buf, + _parens: crate::annotation::Parens, + _newlines: Newlines, + indent: u16, + ) { + self.provides_keyword.format(buf, indent); + fmt_provides(buf, self.entries, self.types, indent); + self.to_keyword.format(buf, indent); + fmt_to(buf, self.to.value, indent); + } +} + +impl<'a> Formattable for PlatformRequires<'a> { + fn is_multiline(&self) -> bool { + is_collection_multiline(&self.rigids) || self.signature.is_multiline() + } + + fn format_with_options( + &self, + buf: &mut Buf, + _parens: crate::annotation::Parens, + _newlines: Newlines, + indent: u16, + ) { + fmt_requires(buf, self, indent); + } +} + +impl<'a, V: Formattable> Formattable for Spaces<'a, V> { + fn is_multiline(&self) -> bool { + !self.before.is_empty() || !self.after.is_empty() || self.item.is_multiline() + } + + fn format_with_options( + &self, + buf: &mut Buf, + parens: crate::annotation::Parens, + newlines: Newlines, + indent: u16, + ) { + fmt_default_spaces(buf, self.before, indent); + self.item.format_with_options(buf, parens, newlines, indent); + fmt_default_spaces(buf, self.after, indent); + } +} + +impl<'a, K: Formattable, V: Formattable> Formattable for KeywordItem<'a, K, V> { + fn is_multiline(&self) -> bool { + self.keyword.is_multiline() || self.item.is_multiline() + } + + fn format_with_options(&self, buf: &mut Buf, parens: Parens, newlines: Newlines, indent: u16) { + self.keyword + .format_with_options(buf, parens, newlines, indent); + self.item.format_with_options(buf, parens, newlines, indent); + } +} + +pub fn fmt_interface_header<'a>(buf: &mut Buf, header: &'a InterfaceHeader<'a>) { + buf.indent(0); + buf.push_str("interface"); + let indent = INDENT; + fmt_default_spaces(buf, header.before_name, indent); + + // module name + buf.indent(indent); + buf.push_str(header.name.value.as_str()); + + header.exposes.keyword.format(buf, indent); + fmt_exposes(buf, header.exposes.item, indent); + header.imports.keyword.format(buf, indent); + fmt_imports(buf, header.imports.item, indent); +} + +pub fn fmt_hosted_header<'a>(buf: &mut Buf, header: &'a HostedHeader<'a>) { + buf.indent(0); + buf.push_str("hosted"); + let indent = INDENT; + fmt_default_spaces(buf, header.before_name, indent); + + buf.push_str(header.name.value.as_str()); + + header.exposes.keyword.format(buf, indent); + fmt_exposes(buf, header.exposes.item, indent); + header.imports.keyword.format(buf, indent); + fmt_imports(buf, header.imports.item, indent); + header.generates.format(buf, indent); + header.generates_with.keyword.format(buf, indent); + fmt_exposes(buf, header.generates_with.item, indent); +} + +pub fn fmt_app_header<'a>(buf: &mut Buf, header: &'a AppHeader<'a>) { + buf.indent(0); + buf.push_str("app"); + let indent = INDENT; + fmt_default_spaces(buf, header.before_name, indent); + + fmt_str_literal(buf, header.name.value, indent); + + if let Some(packages) = &header.packages { + packages.keyword.format(buf, indent); + fmt_packages(buf, packages.item, indent); + } + if let Some(imports) = &header.imports { + imports.keyword.format(buf, indent); + fmt_imports(buf, imports.item, indent); + } + header.provides.format(buf, indent); +} + +pub fn fmt_package_header<'a>(buf: &mut Buf, header: &'a PackageHeader<'a>) { + buf.indent(0); + buf.push_str("package"); + let indent = INDENT; + fmt_default_spaces(buf, header.before_name, indent); + + fmt_package_name(buf, header.name.value, indent); + + header.exposes.keyword.format(buf, indent); + fmt_exposes(buf, header.exposes.item, indent); + header.packages.keyword.format(buf, indent); + fmt_packages(buf, header.packages.item, indent); +} + +pub fn fmt_platform_header<'a>(buf: &mut Buf, header: &'a PlatformHeader<'a>) { + buf.indent(0); + buf.push_str("platform"); + let indent = INDENT; + fmt_default_spaces(buf, header.before_name, indent); + + fmt_package_name(buf, header.name.value, indent); + + header.requires.format(buf, indent); + header.exposes.keyword.format(buf, indent); + fmt_exposes(buf, header.exposes.item, indent); + header.packages.keyword.format(buf, indent); + fmt_packages(buf, header.packages.item, indent); + header.imports.keyword.format(buf, indent); + fmt_imports(buf, header.imports.item, indent); + header.provides.keyword.format(buf, indent); + fmt_provides(buf, header.provides.item, None, indent); +} + +fn fmt_requires(buf: &mut Buf, requires: &PlatformRequires, indent: u16) { + fmt_collection(buf, indent, Braces::Curly, requires.rigids, Newlines::No); + + buf.push_str(" {"); + buf.spaces(1); + requires.signature.value.format(buf, indent); + buf.push_str(" }"); +} + +impl<'a> Formattable for TypedIdent<'a> { + fn is_multiline(&self) -> bool { + false + } + + fn format_with_options( + &self, + buf: &mut Buf, + _parens: Parens, + _newlines: Newlines, + indent: u16, + ) { + buf.indent(indent); + buf.push_str(self.ident.value); + fmt_default_spaces(buf, self.spaces_before_colon, indent); + buf.push(':'); + buf.spaces(1); + + self.ann.value.format(buf, indent); + } +} + +fn fmt_package_name(buf: &mut Buf, name: PackageName, indent: u16) { + buf.indent(indent); + buf.push('"'); + buf.push_str_allow_spaces(name.to_str()); + buf.push('"'); +} + +impl<'a, T: Formattable> Formattable for Spaced<'a, T> { + fn is_multiline(&self) -> bool { + use Spaced::*; + + match self { + Item(formattable) => formattable.is_multiline(), + SpaceBefore(formattable, spaces) | SpaceAfter(formattable, spaces) => { + !spaces.is_empty() || formattable.is_multiline() + } + } + } + + fn format_with_options( + &self, + buf: &mut Buf, + parens: crate::annotation::Parens, + newlines: Newlines, + indent: u16, + ) { + match self { + Spaced::Item(item) => { + item.format_with_options(buf, parens, newlines, indent); + } + Spaced::SpaceBefore(item, spaces) => { + fmt_spaces(buf, spaces.iter(), indent); + item.format_with_options(buf, parens, newlines, indent); + } + Spaced::SpaceAfter(item, spaces) => { + item.format_with_options(buf, parens, newlines, indent); + fmt_spaces(buf, spaces.iter(), indent); + } + } + } +} + +fn fmt_imports<'a>( + buf: &mut Buf, + loc_entries: Collection<'a, Loc>>>, + indent: u16, +) { + fmt_collection(buf, indent, Braces::Square, loc_entries, Newlines::No) +} + +fn fmt_provides<'a>( + buf: &mut Buf, + loc_exposed_names: Collection<'a, Loc>>>, + loc_provided_types: Option>>>>, + indent: u16, +) { + fmt_collection(buf, indent, Braces::Square, loc_exposed_names, Newlines::No); + if let Some(loc_provided) = loc_provided_types { + fmt_default_spaces(buf, &[], indent); + fmt_collection(buf, indent, Braces::Curly, loc_provided, Newlines::No); + } +} + +fn fmt_to(buf: &mut Buf, to: To, indent: u16) { + match to { + To::ExistingPackage(name) => { + buf.push_str(name); + } + To::NewPackage(package_name) => fmt_package_name(buf, package_name, indent), + } +} + +fn fmt_exposes( + buf: &mut Buf, + loc_entries: Collection<'_, Loc>>, + indent: u16, +) { + fmt_collection(buf, indent, Braces::Square, loc_entries, Newlines::No) +} + +pub trait FormatName { + fn format(&self, buf: &mut Buf); +} + +impl<'a> FormatName for &'a str { + fn format(&self, buf: &mut Buf) { + buf.push_str(self) + } +} + +impl<'a> FormatName for ModuleName<'a> { + fn format(&self, buf: &mut Buf) { + buf.push_str(self.as_str()); + } +} + +impl<'a> Formattable for ModuleName<'a> { + fn is_multiline(&self) -> bool { + false + } + + fn format_with_options( + &self, + buf: &mut Buf, + _parens: Parens, + _newlines: Newlines, + _indent: u16, + ) { + buf.push_str(self.as_str()); + } +} + +impl<'a> Formattable for ExposedName<'a> { + fn is_multiline(&self) -> bool { + false + } + + fn format_with_options( + &self, + buf: &mut Buf, + _parens: Parens, + _newlines: Newlines, + indent: u16, + ) { + buf.indent(indent); + buf.push_str(self.as_str()); + } +} + +impl<'a> FormatName for ExposedName<'a> { + fn format(&self, buf: &mut Buf) { + buf.push_str(self.as_str()); + } +} + +fn fmt_packages<'a>( + buf: &mut Buf, + loc_entries: Collection<'a, Loc>>>, + indent: u16, +) { + fmt_collection(buf, indent, Braces::Curly, loc_entries, Newlines::No) +} + +impl<'a> Formattable for PackageEntry<'a> { + fn is_multiline(&self) -> bool { + false + } + + fn format_with_options( + &self, + buf: &mut Buf, + _parens: Parens, + _newlines: Newlines, + indent: u16, + ) { + fmt_packages_entry(buf, self, indent); + } +} + +impl<'a> Formattable for ImportsEntry<'a> { + fn is_multiline(&self) -> bool { + false + } + + fn format_with_options( + &self, + buf: &mut Buf, + _parens: Parens, + _newlines: Newlines, + indent: u16, + ) { + fmt_imports_entry(buf, self, indent); + } +} +fn fmt_packages_entry(buf: &mut Buf, entry: &PackageEntry, indent: u16) { + buf.push_str(entry.shorthand); + buf.push(':'); + fmt_default_spaces(buf, entry.spaces_after_shorthand, indent); + fmt_package_name(buf, entry.package_name.value, indent); +} + +fn fmt_imports_entry(buf: &mut Buf, entry: &ImportsEntry, indent: u16) { + use roc_parse::header::ImportsEntry::*; + + buf.indent(indent); + + match entry { + Module(module, loc_exposes_entries) => { + buf.push_str(module.as_str()); + + if !loc_exposes_entries.is_empty() { + buf.push('.'); + + fmt_collection( + buf, + indent, + Braces::Curly, + *loc_exposes_entries, + Newlines::No, + ) + } + } + + Package(pkg, name, entries) => { + buf.push_str(pkg); + buf.push('.'); + buf.push_str(name.as_str()); + + if !entries.is_empty() { + buf.push('.'); + + fmt_collection(buf, indent, Braces::Curly, *entries, Newlines::No) + } + } + + IngestedFile(file_name, typed_ident) => { + fmt_str_literal(buf, *file_name, indent); + buf.push_str_allow_spaces(" as "); + typed_ident.format(buf, 0); + } + } +} diff --git a/crates/compiler/fmt/src/pattern.rs b/crates/compiler/fmt/src/pattern.rs new file mode 100644 index 0000000000..0410f7db6d --- /dev/null +++ b/crates/compiler/fmt/src/pattern.rs @@ -0,0 +1,289 @@ +use crate::annotation::{Formattable, Newlines, Parens}; +use crate::expr::{fmt_str_literal, format_sq_literal}; +use crate::spaces::{fmt_comments_only, fmt_spaces, NewlineAt, INDENT}; +use crate::Buf; +use roc_parse::ast::{Base, CommentOrNewline, Pattern, PatternAs}; + +pub fn fmt_pattern<'a>(buf: &mut Buf, pattern: &'a Pattern<'a>, indent: u16, parens: Parens) { + pattern.format_with_options(buf, parens, Newlines::No, indent); +} + +impl<'a> Formattable for PatternAs<'a> { + fn is_multiline(&self) -> bool { + self.spaces_before.iter().any(|s| s.is_comment()) + } + + fn format_with_options( + &self, + buf: &mut Buf, + _parens: Parens, + _newlines: Newlines, + indent: u16, + ) { + buf.indent(indent); + + if !buf.ends_with_space() { + buf.spaces(1); + } + + buf.push_str("as"); + buf.spaces(1); + + // these spaces "belong" to the identifier, which can never be multiline + fmt_comments_only(buf, self.spaces_before.iter(), NewlineAt::Bottom, indent); + + buf.indent(indent); + buf.push_str(self.identifier.value); + } +} + +impl<'a> Formattable for Pattern<'a> { + fn is_multiline(&self) -> bool { + // Theory: a pattern should only be multiline when it contains a comment + match self { + Pattern::SpaceBefore(_, spaces) | Pattern::SpaceAfter(_, spaces) => { + debug_assert!(!spaces.is_empty()); + + spaces.iter().any(|s| s.is_comment()) + } + + Pattern::RecordDestructure(fields) => fields.iter().any(|f| f.is_multiline()), + Pattern::RequiredField(_, subpattern) => subpattern.is_multiline(), + + Pattern::OptionalField(_, expr) => expr.is_multiline(), + + Pattern::As(pattern, pattern_as) => pattern.is_multiline() || pattern_as.is_multiline(), + Pattern::ListRest(opt_pattern_as) => match opt_pattern_as { + None => false, + Some((list_rest_spaces, pattern_as)) => { + list_rest_spaces.iter().any(|s| s.is_comment()) || pattern_as.is_multiline() + } + }, + + Pattern::Identifier(_) + | Pattern::Tag(_) + | Pattern::OpaqueRef(_) + | Pattern::Apply(_, _) + | Pattern::NumLiteral(..) + | Pattern::NonBase10Literal { .. } + | Pattern::FloatLiteral(..) + | Pattern::StrLiteral(_) + | Pattern::SingleQuote(_) + | Pattern::Underscore(_) + | Pattern::Malformed(_) + | Pattern::MalformedIdent(_, _) + | Pattern::QualifiedIdentifier { .. } => false, + + Pattern::Tuple(patterns) | Pattern::List(patterns) => { + patterns.iter().any(|p| p.is_multiline()) + } + } + } + + fn format_with_options(&self, buf: &mut Buf, parens: Parens, newlines: Newlines, indent: u16) { + use self::Pattern::*; + + match self { + Identifier(string) => { + buf.indent(indent); + buf.push_str(string) + } + Tag(name) | OpaqueRef(name) => { + buf.indent(indent); + buf.push_str(name); + } + Apply(loc_pattern, loc_arg_patterns) => { + buf.indent(indent); + // Sometimes, an Apply pattern needs parens around it. + // In particular when an Apply's argument is itself an Apply (> 0) arguments + let parens = !loc_arg_patterns.is_empty() && parens == Parens::InApply; + + if parens { + buf.push('('); + } + + loc_pattern.format_with_options(buf, Parens::InApply, Newlines::No, indent); + + for loc_arg in loc_arg_patterns.iter() { + buf.spaces(1); + loc_arg.format_with_options(buf, Parens::InApply, Newlines::No, indent); + } + + if parens { + buf.push(')'); + } + } + RecordDestructure(loc_patterns) => { + buf.indent(indent); + buf.push_str("{"); + + if !loc_patterns.is_empty() { + buf.spaces(1); + let mut it = loc_patterns.iter().peekable(); + while let Some(loc_pattern) = it.next() { + loc_pattern.format(buf, indent); + + if it.peek().is_some() { + buf.push_str(","); + buf.spaces(1); + } + } + buf.spaces(1); + } + + buf.push_str("}"); + } + + RequiredField(name, loc_pattern) => { + buf.indent(indent); + buf.push_str(name); + buf.push_str(":"); + buf.spaces(1); + loc_pattern.format(buf, indent); + } + + OptionalField(name, loc_pattern) => { + buf.indent(indent); + buf.push_str(name); + buf.push_str(" ?"); + buf.spaces(1); + loc_pattern.format(buf, indent); + } + + &NumLiteral(string) => { + buf.indent(indent); + buf.push_str(string); + } + &NonBase10Literal { + base, + string, + is_negative, + } => { + buf.indent(indent); + if is_negative { + buf.push('-'); + } + + match base { + Base::Hex => buf.push_str("0x"), + Base::Octal => buf.push_str("0o"), + Base::Binary => buf.push_str("0b"), + Base::Decimal => { /* nothing */ } + } + + buf.push_str(string); + } + &FloatLiteral(string) => { + buf.indent(indent); + buf.push_str(string); + } + StrLiteral(literal) => fmt_str_literal(buf, *literal, indent), + SingleQuote(string) => { + buf.indent(indent); + format_sq_literal(buf, string); + } + Underscore(name) => { + buf.indent(indent); + buf.push('_'); + buf.push_str(name); + } + Tuple(loc_patterns) => { + buf.indent(indent); + buf.push_str("("); + + let mut it = loc_patterns.iter().peekable(); + while let Some(loc_pattern) = it.next() { + loc_pattern.format(buf, indent); + + if it.peek().is_some() { + buf.push_str(","); + buf.spaces(1); + } + } + + buf.push_str(")"); + } + List(loc_patterns) => { + buf.indent(indent); + buf.push_str("["); + + let mut it = loc_patterns.iter().peekable(); + while let Some(loc_pattern) = it.next() { + loc_pattern.format(buf, indent); + + if it.peek().is_some() { + buf.push_str(","); + buf.spaces(1); + } + } + + buf.push_str("]"); + } + ListRest(opt_pattern_as) => { + buf.indent(indent); + buf.push_str(".."); + + if let Some((list_rest_spaces, pattern_as)) = opt_pattern_as { + // these spaces "belong" to the `..`, which can never be multiline + fmt_comments_only(buf, list_rest_spaces.iter(), NewlineAt::Bottom, indent); + + pattern_as.format(buf, indent + INDENT); + } + } + + As(pattern, pattern_as) => { + fmt_pattern(buf, &pattern.value, indent, parens); + + pattern_as.format(buf, indent + INDENT); + } + + // Space + SpaceBefore(sub_pattern, spaces) => { + if !sub_pattern.is_multiline() { + fmt_comments_only(buf, spaces.iter(), NewlineAt::Bottom, indent) + } else { + fmt_spaces(buf, spaces.iter(), indent); + } + + sub_pattern.format_with_options(buf, parens, newlines, indent); + } + SpaceAfter(sub_pattern, spaces) => { + sub_pattern.format_with_options(buf, parens, newlines, indent); + + if starts_with_inline_comment(spaces.iter()) { + buf.spaces(1); + } + + if !sub_pattern.is_multiline() { + fmt_comments_only(buf, spaces.iter(), NewlineAt::Bottom, indent) + } else { + fmt_spaces(buf, spaces.iter(), indent); + } + } + + // Malformed + Malformed(string) | MalformedIdent(string, _) => { + buf.indent(indent); + buf.push_str(string); + } + QualifiedIdentifier { module_name, ident } => { + buf.indent(indent); + if !module_name.is_empty() { + buf.push_str(module_name); + buf.push('.'); + } + + buf.push_str(ident); + } + } + } +} + +fn starts_with_inline_comment<'a, I: IntoIterator>>( + spaces: I, +) -> bool { + matches!( + spaces.into_iter().next(), + Some(CommentOrNewline::LineComment(_)) + ) +} diff --git a/crates/compiler/fmt/src/spaces.rs b/crates/compiler/fmt/src/spaces.rs new file mode 100644 index 0000000000..255b18ae56 --- /dev/null +++ b/crates/compiler/fmt/src/spaces.rs @@ -0,0 +1,938 @@ +use bumpalo::collections::vec::Vec; +use bumpalo::Bump; +use roc_module::called_via::{BinOp, UnaryOp}; +use roc_parse::{ + ast::{ + AbilityImpls, AbilityMember, AssignedField, Collection, CommentOrNewline, Defs, Expr, + Header, Implements, ImplementsAbilities, ImplementsAbility, ImplementsClause, Module, + Pattern, RecordBuilderField, Spaced, Spaces, StrLiteral, StrSegment, Tag, TypeAnnotation, + TypeDef, TypeHeader, ValueDef, WhenBranch, + }, + header::{ + AppHeader, ExposedName, HostedHeader, ImportsEntry, InterfaceHeader, KeywordItem, + ModuleName, PackageEntry, PackageHeader, PackageName, PlatformHeader, PlatformRequires, + ProvidesTo, To, TypedIdent, + }, + ident::{BadIdent, UppercaseIdent}, +}; +use roc_region::all::{Loc, Position, Region}; + +use crate::{Ast, Buf}; + +/// The number of spaces to indent. +pub const INDENT: u16 = 4; + +pub fn fmt_default_spaces(buf: &mut Buf, spaces: &[CommentOrNewline], indent: u16) { + if spaces.is_empty() { + buf.spaces(1); + } else { + fmt_spaces(buf, spaces.iter(), indent); + } +} +pub fn fmt_default_newline(buf: &mut Buf, spaces: &[CommentOrNewline], indent: u16) { + if spaces.is_empty() { + buf.newline(); + } else { + fmt_spaces(buf, spaces.iter(), indent); + } +} + +/// Like fmt_spaces, but disallows two consecutive newlines. +pub fn fmt_spaces_no_blank_lines<'a, 'buf, I>(buf: &mut Buf<'buf>, spaces: I, indent: u16) +where + I: Iterator>, +{ + fmt_spaces_max_consecutive_newlines(buf, spaces, 1, indent) +} + +pub fn fmt_spaces<'a, 'buf, I>(buf: &mut Buf<'buf>, spaces: I, indent: u16) +where + I: Iterator>, +{ + fmt_spaces_max_consecutive_newlines(buf, spaces, 2, indent) +} + +fn fmt_spaces_max_consecutive_newlines<'a, 'buf, I>( + buf: &mut Buf<'buf>, + spaces: I, + max_consecutive_newlines: usize, + indent: u16, +) where + I: Iterator>, +{ + use self::CommentOrNewline::*; + + // Only ever print two newlines back to back. + // (Two newlines renders as one blank line.) + let mut consecutive_newlines = 0; + + for space in spaces { + match space { + Newline => { + if consecutive_newlines < max_consecutive_newlines { + buf.newline(); + + // Don't bother incrementing it if we're already over the limit. + // There's no upside, and it might eventually overflow. + consecutive_newlines += 1; + } + } + LineComment(comment) => { + buf.indent(indent); + fmt_comment(buf, comment); + buf.newline(); + + consecutive_newlines = 1; + } + DocComment(docs) => { + buf.indent(indent); + fmt_docs(buf, docs); + buf.newline(); + + consecutive_newlines = 1; + } + } + } +} + +#[derive(Eq, PartialEq, Debug)] +pub enum NewlineAt { + Top, + Bottom, + Both, + None, +} + +/// Like format_spaces, but remove newlines and keep only comments. +/// The `new_line_at` argument describes how new lines should be inserted +/// at the beginning or at the end of the block +/// in the case of there is some comment in the `spaces` argument. +pub fn fmt_comments_only<'a, 'buf, I>( + buf: &mut Buf<'buf>, + spaces: I, + new_line_at: NewlineAt, + indent: u16, +) where + I: Iterator>, +{ + use self::CommentOrNewline::*; + use NewlineAt::*; + + let mut comment_seen = false; + + for space in spaces { + match space { + Newline => {} + LineComment(comment) => { + if comment_seen || new_line_at == Top || new_line_at == Both { + buf.newline(); + } + buf.indent(indent); + fmt_comment(buf, comment); + comment_seen = true; + } + DocComment(docs) => { + if comment_seen || new_line_at == Top || new_line_at == Both { + buf.newline(); + } + buf.indent(indent); + fmt_docs(buf, docs); + comment_seen = true; + } + } + } + if comment_seen && (new_line_at == Bottom || new_line_at == Both) { + buf.newline(); + } +} + +fn fmt_comment(buf: &mut Buf, comment: &str) { + // The '#' in a comment should always be preceded by a newline or a space, + // unless it's the very beginning of the buffer. + if !buf.is_empty() && !buf.ends_with_space() && !buf.ends_with_newline() { + buf.spaces(1); + } + + buf.push('#'); + if !comment.starts_with(' ') { + buf.spaces(1); + } + buf.push_str(comment.trim_end()); +} + +pub fn count_leading_newlines<'a, I>(data: I) -> u16 +where + I: Iterator>, +{ + let mut count = 0; + let mut allow_counting = false; + + for (index, val) in data.enumerate() { + let is_first = index == 0; + let is_newline = matches!(val, CommentOrNewline::Newline); + + if is_first && is_newline { + allow_counting = true + } + + if is_newline && allow_counting { + count += 1; + } else { + break; + } + } + + count +} + +fn fmt_docs(buf: &mut Buf, docs: &str) { + // The "##" in a doc comment should always be preceded by a newline or a space, + // unless it's the very beginning of the buffer. + if !buf.is_empty() && !buf.ends_with_space() && !buf.ends_with_newline() { + buf.spaces(1); + } + + buf.push_str("##"); + if !docs.is_empty() { + buf.spaces(1); + } + buf.push_str(docs.trim_end()); +} + +/// RemoveSpaces normalizes the ast to something that we _expect_ to be invariant under formatting. +/// +/// Currently this consists of: +/// * Removing newlines +/// * Removing comments +/// * Removing parens in Exprs +/// +/// Long term, we actuall want this transform to preserve comments (so we can assert they're maintained by formatting) +/// - but there are currently several bugs where they're _not_ preserved. +/// TODO: ensure formatting retains comments +pub trait RemoveSpaces<'a> { + fn remove_spaces(&self, arena: &'a Bump) -> Self; +} + +impl<'a> RemoveSpaces<'a> for Ast<'a> { + fn remove_spaces(&self, arena: &'a Bump) -> Self { + Ast { + module: self.module.remove_spaces(arena), + defs: self.defs.remove_spaces(arena), + } + } +} + +impl<'a> RemoveSpaces<'a> for Defs<'a> { + fn remove_spaces(&self, arena: &'a Bump) -> Self { + let mut defs = self.clone(); + + defs.spaces.clear(); + defs.space_before.clear(); + defs.space_after.clear(); + + for type_def in defs.type_defs.iter_mut() { + *type_def = type_def.remove_spaces(arena); + } + + for value_def in defs.value_defs.iter_mut() { + *value_def = value_def.remove_spaces(arena); + } + + for region_def in defs.regions.iter_mut() { + *region_def = region_def.remove_spaces(arena); + } + + defs + } +} + +impl<'a, V: RemoveSpaces<'a>> RemoveSpaces<'a> for Spaces<'a, V> { + fn remove_spaces(&self, arena: &'a Bump) -> Self { + Spaces { + before: &[], + item: self.item.remove_spaces(arena), + after: &[], + } + } +} + +impl<'a, K: RemoveSpaces<'a>, V: RemoveSpaces<'a>> RemoveSpaces<'a> for KeywordItem<'a, K, V> { + fn remove_spaces(&self, arena: &'a Bump) -> Self { + KeywordItem { + keyword: self.keyword.remove_spaces(arena), + item: self.item.remove_spaces(arena), + } + } +} + +impl<'a> RemoveSpaces<'a> for ProvidesTo<'a> { + fn remove_spaces(&self, arena: &'a Bump) -> Self { + ProvidesTo { + provides_keyword: self.provides_keyword.remove_spaces(arena), + entries: self.entries.remove_spaces(arena), + types: self.types.remove_spaces(arena), + to_keyword: self.to_keyword.remove_spaces(arena), + to: self.to.remove_spaces(arena), + } + } +} + +impl<'a> RemoveSpaces<'a> for Module<'a> { + fn remove_spaces(&self, arena: &'a Bump) -> Self { + let header = match &self.header { + Header::Interface(header) => Header::Interface(InterfaceHeader { + before_name: &[], + name: header.name.remove_spaces(arena), + exposes: header.exposes.remove_spaces(arena), + imports: header.imports.remove_spaces(arena), + }), + Header::App(header) => Header::App(AppHeader { + before_name: &[], + name: header.name.remove_spaces(arena), + packages: header.packages.remove_spaces(arena), + imports: header.imports.remove_spaces(arena), + provides: header.provides.remove_spaces(arena), + }), + Header::Package(header) => Header::Package(PackageHeader { + before_name: &[], + name: header.name.remove_spaces(arena), + exposes: header.exposes.remove_spaces(arena), + packages: header.packages.remove_spaces(arena), + }), + Header::Platform(header) => Header::Platform(PlatformHeader { + before_name: &[], + name: header.name.remove_spaces(arena), + requires: header.requires.remove_spaces(arena), + exposes: header.exposes.remove_spaces(arena), + packages: header.packages.remove_spaces(arena), + imports: header.imports.remove_spaces(arena), + provides: header.provides.remove_spaces(arena), + }), + Header::Hosted(header) => Header::Hosted(HostedHeader { + before_name: &[], + name: header.name.remove_spaces(arena), + exposes: header.exposes.remove_spaces(arena), + imports: header.imports.remove_spaces(arena), + generates: header.generates.remove_spaces(arena), + generates_with: header.generates_with.remove_spaces(arena), + }), + }; + Module { + comments: &[], + header, + } + } +} + +impl<'a> RemoveSpaces<'a> for Region { + fn remove_spaces(&self, _arena: &'a Bump) -> Self { + Region::zero() + } +} + +impl<'a> RemoveSpaces<'a> for &'a str { + fn remove_spaces(&self, _arena: &'a Bump) -> Self { + self + } +} + +impl<'a, T: RemoveSpaces<'a> + Copy> RemoveSpaces<'a> for Spaced<'a, T> { + fn remove_spaces(&self, arena: &'a Bump) -> Self { + match *self { + Spaced::Item(a) => Spaced::Item(a.remove_spaces(arena)), + Spaced::SpaceBefore(a, _) => a.remove_spaces(arena), + Spaced::SpaceAfter(a, _) => a.remove_spaces(arena), + } + } +} + +impl<'a> RemoveSpaces<'a> for ExposedName<'a> { + fn remove_spaces(&self, _arena: &'a Bump) -> Self { + *self + } +} + +impl<'a> RemoveSpaces<'a> for ModuleName<'a> { + fn remove_spaces(&self, _arena: &'a Bump) -> Self { + *self + } +} + +impl<'a> RemoveSpaces<'a> for PackageName<'a> { + fn remove_spaces(&self, _arena: &'a Bump) -> Self { + *self + } +} + +impl<'a> RemoveSpaces<'a> for To<'a> { + fn remove_spaces(&self, arena: &'a Bump) -> Self { + match *self { + To::ExistingPackage(a) => To::ExistingPackage(a), + To::NewPackage(a) => To::NewPackage(a.remove_spaces(arena)), + } + } +} + +impl<'a> RemoveSpaces<'a> for TypedIdent<'a> { + fn remove_spaces(&self, arena: &'a Bump) -> Self { + TypedIdent { + ident: self.ident.remove_spaces(arena), + spaces_before_colon: &[], + ann: self.ann.remove_spaces(arena), + } + } +} + +impl<'a> RemoveSpaces<'a> for PlatformRequires<'a> { + fn remove_spaces(&self, arena: &'a Bump) -> Self { + PlatformRequires { + rigids: self.rigids.remove_spaces(arena), + signature: self.signature.remove_spaces(arena), + } + } +} + +impl<'a> RemoveSpaces<'a> for UppercaseIdent<'a> { + fn remove_spaces(&self, _arena: &'a Bump) -> Self { + *self + } +} + +impl<'a> RemoveSpaces<'a> for PackageEntry<'a> { + fn remove_spaces(&self, arena: &'a Bump) -> Self { + PackageEntry { + shorthand: self.shorthand, + spaces_after_shorthand: &[], + package_name: self.package_name.remove_spaces(arena), + } + } +} + +impl<'a> RemoveSpaces<'a> for ImportsEntry<'a> { + fn remove_spaces(&self, arena: &'a Bump) -> Self { + match *self { + ImportsEntry::Module(a, b) => ImportsEntry::Module(a, b.remove_spaces(arena)), + ImportsEntry::Package(a, b, c) => ImportsEntry::Package(a, b, c.remove_spaces(arena)), + ImportsEntry::IngestedFile(a, b) => { + ImportsEntry::IngestedFile(a, b.remove_spaces(arena)) + } + } + } +} + +impl<'a, T: RemoveSpaces<'a>> RemoveSpaces<'a> for Option { + fn remove_spaces(&self, arena: &'a Bump) -> Self { + self.as_ref().map(|a| a.remove_spaces(arena)) + } +} + +impl<'a, T: RemoveSpaces<'a> + std::fmt::Debug> RemoveSpaces<'a> for Loc { + fn remove_spaces(&self, arena: &'a Bump) -> Self { + let res = self.value.remove_spaces(arena); + Loc::at(Region::zero(), res) + } +} + +impl<'a, A: RemoveSpaces<'a>, B: RemoveSpaces<'a>> RemoveSpaces<'a> for (A, B) { + fn remove_spaces(&self, arena: &'a Bump) -> Self { + (self.0.remove_spaces(arena), self.1.remove_spaces(arena)) + } +} + +impl<'a, T: RemoveSpaces<'a>> RemoveSpaces<'a> for Collection<'a, T> { + fn remove_spaces(&self, arena: &'a Bump) -> Self { + let mut items = Vec::with_capacity_in(self.items.len(), arena); + for item in self.items { + items.push(item.remove_spaces(arena)); + } + Collection::with_items(items.into_bump_slice()) + } +} + +impl<'a, T: RemoveSpaces<'a> + std::fmt::Debug> RemoveSpaces<'a> for &'a [T] { + fn remove_spaces(&self, arena: &'a Bump) -> Self { + let mut items = Vec::with_capacity_in(self.len(), arena); + for item in *self { + let res = item.remove_spaces(arena); + items.push(res); + } + items.into_bump_slice() + } +} + +impl<'a> RemoveSpaces<'a> for UnaryOp { + fn remove_spaces(&self, _arena: &'a Bump) -> Self { + *self + } +} + +impl<'a> RemoveSpaces<'a> for BinOp { + fn remove_spaces(&self, _arena: &'a Bump) -> Self { + *self + } +} + +impl<'a, T: RemoveSpaces<'a>> RemoveSpaces<'a> for &'a T { + fn remove_spaces(&self, arena: &'a Bump) -> Self { + arena.alloc((*self).remove_spaces(arena)) + } +} + +impl<'a> RemoveSpaces<'a> for TypeDef<'a> { + fn remove_spaces(&self, arena: &'a Bump) -> Self { + use TypeDef::*; + + match *self { + Alias { + header: TypeHeader { name, vars }, + ann, + } => Alias { + header: TypeHeader { + name: name.remove_spaces(arena), + vars: vars.remove_spaces(arena), + }, + ann: ann.remove_spaces(arena), + }, + Opaque { + header: TypeHeader { name, vars }, + typ, + derived, + } => Opaque { + header: TypeHeader { + name: name.remove_spaces(arena), + vars: vars.remove_spaces(arena), + }, + typ: typ.remove_spaces(arena), + derived: derived.remove_spaces(arena), + }, + Ability { + header: TypeHeader { name, vars }, + loc_implements: loc_has, + members, + } => Ability { + header: TypeHeader { + name: name.remove_spaces(arena), + vars: vars.remove_spaces(arena), + }, + loc_implements: loc_has.remove_spaces(arena), + members: members.remove_spaces(arena), + }, + } + } +} + +impl<'a> RemoveSpaces<'a> for ValueDef<'a> { + fn remove_spaces(&self, arena: &'a Bump) -> Self { + use ValueDef::*; + + match *self { + Annotation(a, b) => Annotation(a.remove_spaces(arena), b.remove_spaces(arena)), + Body(a, b) => Body( + arena.alloc(a.remove_spaces(arena)), + arena.alloc(b.remove_spaces(arena)), + ), + AnnotatedBody { + ann_pattern, + ann_type, + comment: _, + body_pattern, + body_expr, + } => AnnotatedBody { + ann_pattern: arena.alloc(ann_pattern.remove_spaces(arena)), + ann_type: arena.alloc(ann_type.remove_spaces(arena)), + comment: None, + body_pattern: arena.alloc(body_pattern.remove_spaces(arena)), + body_expr: arena.alloc(body_expr.remove_spaces(arena)), + }, + Dbg { + condition, + preceding_comment: _, + } => Dbg { + condition: arena.alloc(condition.remove_spaces(arena)), + preceding_comment: Region::zero(), + }, + Expect { + condition, + preceding_comment: _, + } => Expect { + condition: arena.alloc(condition.remove_spaces(arena)), + preceding_comment: Region::zero(), + }, + ExpectFx { + condition, + preceding_comment: _, + } => ExpectFx { + condition: arena.alloc(condition.remove_spaces(arena)), + preceding_comment: Region::zero(), + }, + } + } +} + +impl<'a> RemoveSpaces<'a> for Implements<'a> { + fn remove_spaces(&self, _arena: &'a Bump) -> Self { + Implements::Implements + } +} + +impl<'a> RemoveSpaces<'a> for AbilityMember<'a> { + fn remove_spaces(&self, arena: &'a Bump) -> Self { + AbilityMember { + name: self.name.remove_spaces(arena), + typ: self.typ.remove_spaces(arena), + } + } +} + +impl<'a> RemoveSpaces<'a> for WhenBranch<'a> { + fn remove_spaces(&self, arena: &'a Bump) -> Self { + WhenBranch { + patterns: self.patterns.remove_spaces(arena), + value: self.value.remove_spaces(arena), + guard: self.guard.remove_spaces(arena), + } + } +} + +impl<'a, T: RemoveSpaces<'a> + Copy + std::fmt::Debug> RemoveSpaces<'a> for AssignedField<'a, T> { + fn remove_spaces(&self, arena: &'a Bump) -> Self { + match *self { + AssignedField::RequiredValue(a, _, c) => AssignedField::RequiredValue( + a.remove_spaces(arena), + arena.alloc([]), + arena.alloc(c.remove_spaces(arena)), + ), + AssignedField::OptionalValue(a, _, c) => AssignedField::OptionalValue( + a.remove_spaces(arena), + arena.alloc([]), + arena.alloc(c.remove_spaces(arena)), + ), + AssignedField::LabelOnly(a) => AssignedField::LabelOnly(a.remove_spaces(arena)), + AssignedField::Malformed(a) => AssignedField::Malformed(a), + AssignedField::SpaceBefore(a, _) => a.remove_spaces(arena), + AssignedField::SpaceAfter(a, _) => a.remove_spaces(arena), + } + } +} + +impl<'a> RemoveSpaces<'a> for RecordBuilderField<'a> { + fn remove_spaces(&self, arena: &'a Bump) -> Self { + match *self { + RecordBuilderField::Value(a, _, c) => RecordBuilderField::Value( + a.remove_spaces(arena), + &[], + arena.alloc(c.remove_spaces(arena)), + ), + RecordBuilderField::ApplyValue(a, _, _, c) => RecordBuilderField::ApplyValue( + a.remove_spaces(arena), + &[], + &[], + arena.alloc(c.remove_spaces(arena)), + ), + RecordBuilderField::LabelOnly(a) => { + RecordBuilderField::LabelOnly(a.remove_spaces(arena)) + } + RecordBuilderField::Malformed(a) => RecordBuilderField::Malformed(a), + RecordBuilderField::SpaceBefore(a, _) => a.remove_spaces(arena), + RecordBuilderField::SpaceAfter(a, _) => a.remove_spaces(arena), + } + } +} + +impl<'a> RemoveSpaces<'a> for StrLiteral<'a> { + fn remove_spaces(&self, arena: &'a Bump) -> Self { + match *self { + StrLiteral::PlainLine(t) => StrLiteral::PlainLine(t), + StrLiteral::Line(t) => StrLiteral::Line(t.remove_spaces(arena)), + StrLiteral::Block(t) => StrLiteral::Block(t.remove_spaces(arena)), + } + } +} + +impl<'a> RemoveSpaces<'a> for StrSegment<'a> { + fn remove_spaces(&self, arena: &'a Bump) -> Self { + match *self { + StrSegment::Plaintext(t) => StrSegment::Plaintext(t), + StrSegment::Unicode(t) => StrSegment::Unicode(t.remove_spaces(arena)), + StrSegment::EscapedChar(c) => StrSegment::EscapedChar(c), + StrSegment::Interpolated(t) => StrSegment::Interpolated(t.remove_spaces(arena)), + } + } +} + +impl<'a> RemoveSpaces<'a> for Expr<'a> { + fn remove_spaces(&self, arena: &'a Bump) -> Self { + match *self { + Expr::Float(a) => Expr::Float(a), + Expr::Num(a) => Expr::Num(a), + Expr::NonBase10Int { + string, + base, + is_negative, + } => Expr::NonBase10Int { + string, + base, + is_negative, + }, + Expr::Str(a) => Expr::Str(a.remove_spaces(arena)), + Expr::IngestedFile(a, b) => Expr::IngestedFile(a, b), + Expr::RecordAccess(a, b) => Expr::RecordAccess(arena.alloc(a.remove_spaces(arena)), b), + Expr::AccessorFunction(a) => Expr::AccessorFunction(a), + Expr::TupleAccess(a, b) => Expr::TupleAccess(arena.alloc(a.remove_spaces(arena)), b), + Expr::List(a) => Expr::List(a.remove_spaces(arena)), + Expr::RecordUpdate { update, fields } => Expr::RecordUpdate { + update: arena.alloc(update.remove_spaces(arena)), + fields: fields.remove_spaces(arena), + }, + Expr::Record(a) => Expr::Record(a.remove_spaces(arena)), + Expr::RecordBuilder(a) => Expr::RecordBuilder(a.remove_spaces(arena)), + Expr::Tuple(a) => Expr::Tuple(a.remove_spaces(arena)), + Expr::Var { module_name, ident } => Expr::Var { module_name, ident }, + Expr::Underscore(a) => Expr::Underscore(a), + Expr::Tag(a) => Expr::Tag(a), + Expr::OpaqueRef(a) => Expr::OpaqueRef(a), + Expr::Closure(a, b) => Expr::Closure( + arena.alloc(a.remove_spaces(arena)), + arena.alloc(b.remove_spaces(arena)), + ), + Expr::Crash => Expr::Crash, + Expr::Defs(a, b) => { + let mut defs = a.clone(); + defs.space_before = vec![Default::default(); defs.len()]; + defs.space_after = vec![Default::default(); defs.len()]; + defs.regions = vec![Region::zero(); defs.len()]; + defs.spaces.clear(); + + for type_def in defs.type_defs.iter_mut() { + *type_def = type_def.remove_spaces(arena); + } + + for value_def in defs.value_defs.iter_mut() { + *value_def = value_def.remove_spaces(arena); + } + + Expr::Defs(arena.alloc(defs), arena.alloc(b.remove_spaces(arena))) + } + Expr::Backpassing(a, b, c) => Expr::Backpassing( + arena.alloc(a.remove_spaces(arena)), + arena.alloc(b.remove_spaces(arena)), + arena.alloc(c.remove_spaces(arena)), + ), + Expr::Expect(a, b) => Expr::Expect( + arena.alloc(a.remove_spaces(arena)), + arena.alloc(b.remove_spaces(arena)), + ), + Expr::Dbg(a, b) => Expr::Dbg( + arena.alloc(a.remove_spaces(arena)), + arena.alloc(b.remove_spaces(arena)), + ), + Expr::LowLevelDbg(_, _) => unreachable!( + "LowLevelDbg should only exist after desugaring, not during formatting" + ), + Expr::Apply(a, b, c) => Expr::Apply( + arena.alloc(a.remove_spaces(arena)), + b.remove_spaces(arena), + c, + ), + Expr::BinOps(a, b) => { + Expr::BinOps(a.remove_spaces(arena), arena.alloc(b.remove_spaces(arena))) + } + Expr::UnaryOp(a, b) => { + Expr::UnaryOp(arena.alloc(a.remove_spaces(arena)), b.remove_spaces(arena)) + } + Expr::If(a, b) => Expr::If(a.remove_spaces(arena), arena.alloc(b.remove_spaces(arena))), + Expr::When(a, b) => { + Expr::When(arena.alloc(a.remove_spaces(arena)), b.remove_spaces(arena)) + } + Expr::ParensAround(a) => { + // The formatter can remove redundant parentheses, so also remove these when normalizing for comparison. + a.remove_spaces(arena) + } + Expr::MalformedIdent(a, b) => Expr::MalformedIdent(a, remove_spaces_bad_ident(b)), + Expr::MalformedClosure => Expr::MalformedClosure, + Expr::PrecedenceConflict(a) => Expr::PrecedenceConflict(a), + Expr::MultipleRecordBuilders(a) => Expr::MultipleRecordBuilders(a), + Expr::UnappliedRecordBuilder(a) => Expr::UnappliedRecordBuilder(a), + Expr::SpaceBefore(a, _) => a.remove_spaces(arena), + Expr::SpaceAfter(a, _) => a.remove_spaces(arena), + Expr::SingleQuote(a) => Expr::Num(a), + } + } +} + +fn remove_spaces_bad_ident(ident: BadIdent) -> BadIdent { + match ident { + BadIdent::Start(_) => BadIdent::Start(Position::zero()), + BadIdent::Space(e, _) => BadIdent::Space(e, Position::zero()), + BadIdent::UnderscoreAlone(_) => BadIdent::UnderscoreAlone(Position::zero()), + BadIdent::UnderscoreInMiddle(_) => BadIdent::UnderscoreInMiddle(Position::zero()), + BadIdent::UnderscoreAtStart { + position: _, + declaration_region, + } => BadIdent::UnderscoreAtStart { + position: Position::zero(), + declaration_region, + }, + BadIdent::QualifiedTag(_) => BadIdent::QualifiedTag(Position::zero()), + BadIdent::WeirdAccessor(_) => BadIdent::WeirdAccessor(Position::zero()), + BadIdent::WeirdDotAccess(_) => BadIdent::WeirdDotAccess(Position::zero()), + BadIdent::WeirdDotQualified(_) => BadIdent::WeirdDotQualified(Position::zero()), + BadIdent::StrayDot(_) => BadIdent::StrayDot(Position::zero()), + BadIdent::BadOpaqueRef(_) => BadIdent::BadOpaqueRef(Position::zero()), + BadIdent::QualifiedTupleAccessor(_) => BadIdent::QualifiedTupleAccessor(Position::zero()), + } +} + +impl<'a> RemoveSpaces<'a> for Pattern<'a> { + fn remove_spaces(&self, arena: &'a Bump) -> Self { + match *self { + Pattern::Identifier(a) => Pattern::Identifier(a), + Pattern::Tag(a) => Pattern::Tag(a), + Pattern::OpaqueRef(a) => Pattern::OpaqueRef(a), + Pattern::Apply(a, b) => Pattern::Apply( + arena.alloc(a.remove_spaces(arena)), + arena.alloc(b.remove_spaces(arena)), + ), + Pattern::RecordDestructure(a) => Pattern::RecordDestructure(a.remove_spaces(arena)), + Pattern::RequiredField(a, b) => { + Pattern::RequiredField(a, arena.alloc(b.remove_spaces(arena))) + } + Pattern::OptionalField(a, b) => { + Pattern::OptionalField(a, arena.alloc(b.remove_spaces(arena))) + } + Pattern::As(pattern, pattern_as) => { + Pattern::As(arena.alloc(pattern.remove_spaces(arena)), pattern_as) + } + Pattern::NumLiteral(a) => Pattern::NumLiteral(a), + Pattern::NonBase10Literal { + string, + base, + is_negative, + } => Pattern::NonBase10Literal { + string, + base, + is_negative, + }, + Pattern::FloatLiteral(a) => Pattern::FloatLiteral(a), + Pattern::StrLiteral(a) => Pattern::StrLiteral(a), + Pattern::Underscore(a) => Pattern::Underscore(a), + Pattern::Malformed(a) => Pattern::Malformed(a), + Pattern::MalformedIdent(a, b) => Pattern::MalformedIdent(a, remove_spaces_bad_ident(b)), + Pattern::QualifiedIdentifier { module_name, ident } => { + Pattern::QualifiedIdentifier { module_name, ident } + } + Pattern::SpaceBefore(a, _) => a.remove_spaces(arena), + Pattern::SpaceAfter(a, _) => a.remove_spaces(arena), + Pattern::SingleQuote(a) => Pattern::SingleQuote(a), + Pattern::List(pats) => Pattern::List(pats.remove_spaces(arena)), + Pattern::Tuple(pats) => Pattern::Tuple(pats.remove_spaces(arena)), + Pattern::ListRest(opt_pattern_as) => Pattern::ListRest(opt_pattern_as), + } + } +} + +impl<'a> RemoveSpaces<'a> for TypeAnnotation<'a> { + fn remove_spaces(&self, arena: &'a Bump) -> Self { + match *self { + TypeAnnotation::Function(a, b) => TypeAnnotation::Function( + arena.alloc(a.remove_spaces(arena)), + arena.alloc(b.remove_spaces(arena)), + ), + TypeAnnotation::Apply(a, b, c) => TypeAnnotation::Apply(a, b, c.remove_spaces(arena)), + TypeAnnotation::BoundVariable(a) => TypeAnnotation::BoundVariable(a), + TypeAnnotation::As(a, _, TypeHeader { name, vars }) => TypeAnnotation::As( + arena.alloc(a.remove_spaces(arena)), + &[], + TypeHeader { + name: name.remove_spaces(arena), + vars: vars.remove_spaces(arena), + }, + ), + TypeAnnotation::Tuple { elems: fields, ext } => TypeAnnotation::Tuple { + elems: fields.remove_spaces(arena), + ext: ext.remove_spaces(arena), + }, + TypeAnnotation::Record { fields, ext } => TypeAnnotation::Record { + fields: fields.remove_spaces(arena), + ext: ext.remove_spaces(arena), + }, + TypeAnnotation::TagUnion { ext, tags } => TypeAnnotation::TagUnion { + ext: ext.remove_spaces(arena), + tags: tags.remove_spaces(arena), + }, + TypeAnnotation::Inferred => TypeAnnotation::Inferred, + TypeAnnotation::Wildcard => TypeAnnotation::Wildcard, + TypeAnnotation::Where(annot, has_clauses) => TypeAnnotation::Where( + arena.alloc(annot.remove_spaces(arena)), + arena.alloc(has_clauses.remove_spaces(arena)), + ), + TypeAnnotation::SpaceBefore(a, _) => a.remove_spaces(arena), + TypeAnnotation::SpaceAfter(a, _) => a.remove_spaces(arena), + TypeAnnotation::Malformed(a) => TypeAnnotation::Malformed(a), + } + } +} + +impl<'a> RemoveSpaces<'a> for ImplementsClause<'a> { + fn remove_spaces(&self, arena: &'a Bump) -> Self { + ImplementsClause { + var: self.var.remove_spaces(arena), + abilities: self.abilities.remove_spaces(arena), + } + } +} + +impl<'a> RemoveSpaces<'a> for Tag<'a> { + fn remove_spaces(&self, arena: &'a Bump) -> Self { + match *self { + Tag::Apply { name, args } => Tag::Apply { + name: name.remove_spaces(arena), + args: args.remove_spaces(arena), + }, + Tag::Malformed(a) => Tag::Malformed(a), + Tag::SpaceBefore(a, _) => a.remove_spaces(arena), + Tag::SpaceAfter(a, _) => a.remove_spaces(arena), + } + } +} + +impl<'a> RemoveSpaces<'a> for AbilityImpls<'a> { + fn remove_spaces(&self, arena: &'a Bump) -> Self { + match *self { + AbilityImpls::AbilityImpls(impls) => { + AbilityImpls::AbilityImpls(impls.remove_spaces(arena)) + } + AbilityImpls::SpaceBefore(has, _) | AbilityImpls::SpaceAfter(has, _) => { + has.remove_spaces(arena) + } + } + } +} + +impl<'a> RemoveSpaces<'a> for ImplementsAbility<'a> { + fn remove_spaces(&self, arena: &'a Bump) -> Self { + match *self { + ImplementsAbility::ImplementsAbility { ability, impls } => { + ImplementsAbility::ImplementsAbility { + ability: ability.remove_spaces(arena), + impls: impls.remove_spaces(arena), + } + } + ImplementsAbility::SpaceBefore(has, _) | ImplementsAbility::SpaceAfter(has, _) => { + has.remove_spaces(arena) + } + } + } +} + +impl<'a> RemoveSpaces<'a> for ImplementsAbilities<'a> { + fn remove_spaces(&self, arena: &'a Bump) -> Self { + match *self { + ImplementsAbilities::Implements(derived) => { + ImplementsAbilities::Implements(derived.remove_spaces(arena)) + } + ImplementsAbilities::SpaceBefore(derived, _) + | ImplementsAbilities::SpaceAfter(derived, _) => derived.remove_spaces(arena), + } + } +} diff --git a/crates/compiler/gen_dev/Cargo.toml b/crates/compiler/gen_dev/Cargo.toml new file mode 100644 index 0000000000..12680ffdb3 --- /dev/null +++ b/crates/compiler/gen_dev/Cargo.toml @@ -0,0 +1,38 @@ +[package] +name = "roc_gen_dev" +description = "The development backend for the Roc compiler" + +authors.workspace = true +edition.workspace = true +license.workspace = true +version.workspace = true + +[dependencies] +roc_builtins = { path = "../builtins" } +roc_collections = { path = "../collections" } +roc_error_macros = { path = "../../error_macros" } +roc_module = { path = "../module" } +roc_mono = { path = "../mono" } +roc_problem = { path = "../problem" } +roc_region = { path = "../region" } +roc_solve = { path = "../solve" } +roc_target = { path = "../roc_target" } +roc_types = { path = "../types" } +roc_unify = { path = "../unify" } + +bumpalo.workspace = true +object.workspace = true +packed_struct.workspace = true +target-lexicon.workspace = true + +[dev-dependencies] +roc_can = { path = "../can" } +roc_parse = { path = "../parse" } +roc_std = { path = "../../roc_std" } + +bumpalo.workspace = true +capstone.workspace = true + +[features] +target-aarch64 = [] +target-x86_64 = [] diff --git a/crates/compiler/gen_dev/README.md b/crates/compiler/gen_dev/README.md new file mode 100644 index 0000000000..22fb5b8dbb --- /dev/null +++ b/crates/compiler/gen_dev/README.md @@ -0,0 +1,176 @@ +# Dev Backend + +The dev backend is focused on generating decent binaries extremely fast. +It goes from Roc's [Mono IR](https://github.com/roc-lang/roc/blob/main/crates/compiler/mono/src/ir.rs) to an object file ready to be linked. + +## General Process + +The backend is essentially defined as two recursive match statement over the Mono IR. +The first pass is used to do simple linear scan lifetime analysis. +In the future it may be expanded to add a few other quick optimizations. +The second pass is the actual meat of the backend that generates the byte buffer of output binary. +The process is pretty simple, but can get quite complex when you have to deal with memory layouts, function calls, and multiple architectures. + +## Core Abstractions + +This library is built with a number of core traits/generic types that may look quite weird at first glance. +The reason for all of the generics and traits is to allow Rust to optimize each target-specific backend. +Instead of needing an `if linux ...` or `if arm ...` statement everywhere within the backend, +Rust should be able to compile each specific target (`linux-arm`, `darwin-x86_64`, etc) as a static optimized backend without branches on target or dynamic dispatch. + +**Note:** links below are to files, not specific lines. Just look up the specific type in the file. + +### Backend + +[Backend](https://github.com/roc-lang/roc/blob/main/crates/compiler/gen_dev/src/lib.rs) is the core abstraction. +It understands Roc's Mono IR and some high level ideas about the generation process. +The main job of Backend is to do high level optimizations (like lazy literal loading) and parse the Mono IR. +Every target specific backend must implement this trait. + +### Backend64Bit + +[Backend64Bit](https://github.com/roc-lang/roc/blob/main/crates/compiler/gen_dev/src/generic64/mod.rs) is more or less what it sounds like. +It is the backend that understands 64-bit architectures. +Currently it is the only backend implementation, but a 32-bit implementation will probably come in the future. +This backend understands that the unit of data movement is 64-bit. +It also knows about things common to all 64-bit architectures (general purpose registers, stack, float regs, etc). + +If you look at the signature for Backend64Bit, it is actually quite complex. +Backend64Bit is generic over things like the register type, assembler, and calling convention. +This enables to backend to support multiple architectures and operating systems. +For example, the `windows-x86_64` would use the x86 register set, the x86 assembler, and the x86 windows calling convention. +`darwin-x86_64` and `linux-x86_64` would use the same register set and assembler, but they would use the System V AMD64 ABI calling convention. +Backend64Bit is generic over these types instead of containing these types within it's struct to avoid the cost of dynamic dispatch. + +### Assembler + +[Assembler](https://github.com/roc-lang/roc/blob/main/crates/compiler/gen_dev/src/generic64/mod.rs) is the trait for generating assembly bytes. +It defines a set of RISC-like assembly calls that must be implemented for each architecture. +A lot of these calls may not map one to one with actual assembly instructions for each architecture. +Instead, they are a general abstraction over functionality shared between all architectures. +This will grow regularly as more Roc builtins are added. +Here are example implementations for [arm](https://github.com/roc-lang/roc/blob/main/crates/compiler/gen_dev/src/generic64/aarch64.rs) and [x86_64](https://github.com/roc-lang/roc/blob/main/crates/compiler/gen_dev/src/generic64/x86_64.rs). + +### CallConv + +[CallConv](https://github.com/roc-lang/roc/blob/main/crates/compiler/gen_dev/src/generic64/mod.rs) is the abstraction over calling conventions. +It deals with register and stack specific information related to passing and returning arguments. +Here are example implementations for [arm](https://github.com/roc-lang/roc/blob/main/crates/compiler/gen_dev/src/generic64/aarch64.rs) and [x86_64](https://github.com/roc-lang/roc/blob/main/crates/compiler/gen_dev/src/generic64/x86_64.rs). + +## Adding New Features + +Adding a new builtin to the dev backend can be pretty simple. +Here is [an example](https://github.com/roc-lang/roc/pull/893/files) of adding `Num.Sub`. + +This is the general procedure I follow with some helpful links: + +1. Find a feature that is just n+1. + For example, since we already have integers, adding a builtin that functions on them should be n+1. + On the other hand, since we don't yet have booleans/conditionals, adding if statements may not yet be n+1. + A good place to look for missing features is in the test files for generation in [test_gen](https://github.com/roc-lang/roc/tree/main/crates/compiler/test_gen). Any test that is not enabled for the `gen-dev` feature still needs to be added to the dev backend. Eventually all features should be enabled for the dev backend. +1. Pick/write the simplest test case you can find for the new feature. + Just add `feature = "gen-dev"` to the `cfg` line for the test case. +1. Uncomment the code to print out procedures [from here](https://github.com/roc-lang/roc/blob/b03ed18553569314a420d5bf1fb0ead4b6b5ecda/compiler/test_gen/src/helpers/dev.rs#L76) and run the test. + It should fail and print out the Mono IR for this test case. + Seeing the actual Mono IR tends to be very helpful for complex additions. +1. Generally it will fail in one of the match statements in the [Backend](https://github.com/roc-lang/roc/blob/main/crates/compiler/gen_dev/src/lib.rs) trait. + Add the correct pattern matching and likely new function for your new builtin. + This will break the compile until you add the same function to places that implement the trait, + like [Backend64Bit](https://github.com/roc-lang/roc/blob/main/crates/compiler/gen_dev/src/generic64/mod.rs). +1. Keep following the chain down. + To implement the function in Backend64Bit, you may need to add new assembly calls. + Feel free to ignore backends that aren't x86_64 for now and just add `unimplemented!`. + See the helpful resources section below for guides on figuring out assembly bytes. +1. Hopefully at some point everything compiles and the test is passing. + If so, yay. Now add more tests for the same feature and make sure you didn't miss the edge cases. +1. If things aren't working, reach out on Zulip. Get advice, maybe even pair. +1. Make a PR. + +## Debugging x86_64 backend output + +While working on the x86_64 backend it may be useful to inspect the assembly output of a given piece of Roc code. With the right tools, you can do this rather easily. You'll need `objdump` to follow along. + +We'll try to explore the x86 assembly output of some lines of Roc code: + +```elixir +app "dbg" + provides [main] to "." + +main = + (List.len [1]) + 41 +``` + +If this file exists somewhere in the repo as `dbg.roc`, we'll be able to compile an object file by issuing the following command: + +```console +# `cargo run --` can be replaces with calling the compiled `roc` cli binary. +$ cargo run -- build --dev main.roc --no-link +``` + +Which will produce a minimal `dbg.o` object file containing the output assembly code. This object file can be inspected by using `objdump` in the following way: + +```console +$ objdump -M intel -dS dbg.o +dbg.o: file format elf64-x86-64 + +Disassembly of section .text.700000006: + +0000000000000000 : + 0: 55 push rbp + 1: 48 89 e5 mov rbp,rsp + 4: 48 8b 85 18 00 00 00 mov rax,QWORD PTR [rbp+0x18] + b: 5d pop rbp + c: c3 ret + +Disassembly of section .text.400000013: + +0000000000000000 : + 0: 55 push rbp +# .. more output .. + +Disassembly of section .text.1000000000: + +0000000000000000 : + 0: 55 push rbp +# .. more output .. +``` + +The output lines contain the hexadecimal representation of the x86 opcodes and fields followed by the `intel` assembly syntax. This setup is very useful for figuring out the causes of invalid pointer references (or equivalent) when running the resulting x86 assembly. + +## Helpful Resources + +- [Compiler Explorer](https://godbolt.org/) - + Generates assembly from most languages. + Really good for getting a reference for what is required to do something. + Can answer questions like "how would x be implemented in arm assembly?" +- [objdump](https://www.tutorialspoint.com/unix_commands/objdump.htm) - + Super super useful commandline tool. + Lets you inspect exactly what is generated in a binary. + Can inspect assembly, relocations, and more. + I use this all the time for debugging and inspecting C sample apps. + May write a larger tutorial for this because it can be seriously helpful. + As a note, when dealing with relocations, please make sure to compile with PIC. +- [Online Assembler](https://defuse.ca/online-x86-assembler.htm#disassembly) - + Useful for seeing the actual bytes generated by assembly instructions. + A lot of time it gives on out of multiple options because x86_64 has many ways to do things. + Also, sometimes it doesn't seem to generate things quite as you expect. +- [Alternative Online Assembler](http://shell-storm.org/online/Online-Assembler-and-Disassembler/) - + Like previous but with more architecture options. +- [x86 and amd64 instruction reference](https://web.archive.org/web/20230221053750/https://www.felixcloutier.com/x86/) - + Great for looking up x86_64 instructions and there bytes. + Definitely missing information if you aren't used to reading it. +- [Intel 64 ISA Reference](https://community.intel.com/legacyfs/online/drupal_files/managed/a4/60/325383-sdm-vol-2abcd.pdf) - + Super dense manual. + Contains everything you would need to know for x86_64. + Also is like 2000 pages. +- [ARM architecture reference manual](https://developer.arm.com/documentation/ddi0487/latest/) - + Same thing as the intel manual, but for ARM. + It is huge, but quite useful. + Links in the pdf make it easy to jump around and understand our arm enum. +- [A ToC of the 20 part linker essay](https://lwn.net/Articles/276782/) - + Lots of information on linkers by the author of the gold linker. +- If there is anything else basic that you want to know, + there is a good chance it is include in lectures from compiler courses. + Definitely look at some of the free moocs, lectures, or youtube class recordings on the subject. +- If you have any specific questions feel free to ping Brendan Hansknecht on Zulip. +- If you have any other resource that you find useful, please add them here. diff --git a/crates/compiler/gen_dev/src/generic64/aarch64.rs b/crates/compiler/gen_dev/src/generic64/aarch64.rs new file mode 100644 index 0000000000..cdb7a35d31 --- /dev/null +++ b/crates/compiler/gen_dev/src/generic64/aarch64.rs @@ -0,0 +1,5558 @@ +use crate::generic64::{storage::StorageManager, Assembler, CallConv, RegTrait}; +use crate::{ + pointer_layouts, single_register_floats, single_register_int_builtins, + single_register_integers, single_register_layouts, Relocation, +}; +use bumpalo::collections::Vec; +use packed_struct::prelude::*; +use roc_builtins::bitcode::{FloatWidth, IntWidth}; +use roc_error_macros::internal_error; +use roc_module::symbol::Symbol; +use roc_mono::layout::{ + Builtin, InLayout, LayoutInterner, LayoutRepr, STLayoutInterner, UnionLayout, +}; + +use super::{CompareOperation, RegisterWidth}; + +#[derive(Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Debug)] +#[allow(dead_code)] +pub enum AArch64GeneralReg { + X0 = 0, + X1 = 1, + X2 = 2, + X3 = 3, + X4 = 4, + X5 = 5, + X6 = 6, + X7 = 7, + XR = 8, + X9 = 9, + X10 = 10, + X11 = 11, + X12 = 12, + X13 = 13, + X14 = 14, + X15 = 15, + IP0 = 16, + IP1 = 17, + PR = 18, + X19 = 19, + X20 = 20, + X21 = 21, + X22 = 22, + X23 = 23, + X24 = 24, + X25 = 25, + X26 = 26, + X27 = 27, + X28 = 28, + FP = 29, + LR = 30, + /// This can mean Zero or Stack Pointer depending on the context. + ZRSP = 31, +} + +impl AArch64GeneralReg { + #[cfg(test)] + const fn as_str_32bit(&self) -> &str { + match self { + AArch64GeneralReg::X0 => "w0", + AArch64GeneralReg::X1 => "w1", + AArch64GeneralReg::X2 => "w2", + AArch64GeneralReg::X3 => "w3", + AArch64GeneralReg::X4 => "w4", + AArch64GeneralReg::X5 => "w5", + AArch64GeneralReg::X6 => "w6", + AArch64GeneralReg::X7 => "w7", + AArch64GeneralReg::XR => "wr", + AArch64GeneralReg::X9 => "w9", + AArch64GeneralReg::X10 => "w10", + AArch64GeneralReg::X11 => "w11", + AArch64GeneralReg::X12 => "w12", + AArch64GeneralReg::X13 => "w13", + AArch64GeneralReg::X14 => "w14", + AArch64GeneralReg::X15 => "w15", + AArch64GeneralReg::IP0 => "ip0", + AArch64GeneralReg::IP1 => "ip1", + AArch64GeneralReg::PR => "pr", + AArch64GeneralReg::X19 => "w19", + AArch64GeneralReg::X20 => "w20", + AArch64GeneralReg::X21 => "w21", + AArch64GeneralReg::X22 => "w22", + AArch64GeneralReg::X23 => "w23", + AArch64GeneralReg::X24 => "w24", + AArch64GeneralReg::X25 => "w25", + AArch64GeneralReg::X26 => "w26", + AArch64GeneralReg::X27 => "w27", + AArch64GeneralReg::X28 => "w28", + AArch64GeneralReg::FP => "fp", + AArch64GeneralReg::LR => "lr", + AArch64GeneralReg::ZRSP => "zrsp", + } + } + + const fn as_str_64bit(&self) -> &str { + match self { + AArch64GeneralReg::X0 => "x0", + AArch64GeneralReg::X1 => "x1", + AArch64GeneralReg::X2 => "x2", + AArch64GeneralReg::X3 => "x3", + AArch64GeneralReg::X4 => "x4", + AArch64GeneralReg::X5 => "x5", + AArch64GeneralReg::X6 => "x6", + AArch64GeneralReg::X7 => "x7", + AArch64GeneralReg::XR => "xr", + AArch64GeneralReg::X9 => "x9", + AArch64GeneralReg::X10 => "x10", + AArch64GeneralReg::X11 => "x11", + AArch64GeneralReg::X12 => "x12", + AArch64GeneralReg::X13 => "x13", + AArch64GeneralReg::X14 => "x14", + AArch64GeneralReg::X15 => "x15", + AArch64GeneralReg::IP0 => "ip0", + AArch64GeneralReg::IP1 => "ip1", + AArch64GeneralReg::PR => "pr", + AArch64GeneralReg::X19 => "x19", + AArch64GeneralReg::X20 => "x20", + AArch64GeneralReg::X21 => "x21", + AArch64GeneralReg::X22 => "x22", + AArch64GeneralReg::X23 => "x23", + AArch64GeneralReg::X24 => "x24", + AArch64GeneralReg::X25 => "x25", + AArch64GeneralReg::X26 => "x26", + AArch64GeneralReg::X27 => "x27", + AArch64GeneralReg::X28 => "x28", + AArch64GeneralReg::FP => "fp", + AArch64GeneralReg::LR => "lr", + AArch64GeneralReg::ZRSP => "zrsp", + } + } +} + +impl RegTrait for AArch64GeneralReg { + fn value(&self) -> u8 { + *self as u8 + } +} + +impl std::fmt::Display for AArch64GeneralReg { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "{}", self.as_str_64bit()) + } +} + +impl AArch64GeneralReg { + #[inline(always)] + fn id(&self) -> u8 { + *self as u8 + } +} + +#[derive(Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Debug)] +#[allow(dead_code)] +pub enum AArch64FloatReg { + V0 = 0, + V1 = 1, + V2 = 2, + V3 = 3, + V4 = 4, + V5 = 5, + V6 = 6, + V7 = 7, + V8 = 8, + V9 = 9, + V10 = 10, + V11 = 11, + V12 = 12, + V13 = 13, + V14 = 14, + V15 = 15, + V16 = 16, + V17 = 17, + V18 = 18, + V19 = 19, + V20 = 20, + V21 = 21, + V22 = 22, + V23 = 23, + V24 = 24, + V25 = 25, + V26 = 26, + V27 = 27, + V28 = 28, + V29 = 29, + V30 = 30, + V31 = 31, +} +impl RegTrait for AArch64FloatReg { + fn value(&self) -> u8 { + *self as u8 + } +} +impl std::fmt::Display for AArch64FloatReg { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!( + f, + "{}", + match self { + AArch64FloatReg::V0 => "v0", + AArch64FloatReg::V1 => "v1", + AArch64FloatReg::V2 => "v2", + AArch64FloatReg::V3 => "v3", + AArch64FloatReg::V4 => "v4", + AArch64FloatReg::V5 => "v5", + AArch64FloatReg::V6 => "v6", + AArch64FloatReg::V7 => "v7", + AArch64FloatReg::V8 => "v8", + AArch64FloatReg::V9 => "v9", + AArch64FloatReg::V10 => "v10", + AArch64FloatReg::V11 => "v11", + AArch64FloatReg::V12 => "v12", + AArch64FloatReg::V13 => "v13", + AArch64FloatReg::V14 => "v14", + AArch64FloatReg::V15 => "v15", + AArch64FloatReg::V16 => "v16", + AArch64FloatReg::V17 => "v17", + AArch64FloatReg::V18 => "v18", + AArch64FloatReg::V19 => "v19", + AArch64FloatReg::V20 => "v20", + AArch64FloatReg::V21 => "v21", + AArch64FloatReg::V22 => "v22", + AArch64FloatReg::V23 => "v23", + AArch64FloatReg::V24 => "v24", + AArch64FloatReg::V25 => "v25", + AArch64FloatReg::V26 => "v26", + AArch64FloatReg::V27 => "v27", + AArch64FloatReg::V28 => "v28", + AArch64FloatReg::V29 => "v29", + AArch64FloatReg::V30 => "v30", + AArch64FloatReg::V31 => "v31", + } + ) + } +} + +impl AArch64FloatReg { + #[inline(always)] + fn id(&self) -> u8 { + *self as u8 + } +} + +#[derive(Copy, Clone)] +pub struct AArch64Assembler {} + +// AArch64Call may need to eventually be split by OS, +// but I think with how we use it, they may all be the same. +#[derive(Copy, Clone)] +pub struct AArch64Call {} + +const STACK_ALIGNMENT: u8 = 16; + +impl CallConv for AArch64Call { + const BASE_PTR_REG: AArch64GeneralReg = AArch64GeneralReg::FP; + const STACK_PTR_REG: AArch64GeneralReg = AArch64GeneralReg::ZRSP; + + const GENERAL_PARAM_REGS: &'static [AArch64GeneralReg] = &[ + AArch64GeneralReg::X0, + AArch64GeneralReg::X1, + AArch64GeneralReg::X2, + AArch64GeneralReg::X3, + AArch64GeneralReg::X4, + AArch64GeneralReg::X5, + AArch64GeneralReg::X6, + AArch64GeneralReg::X7, + ]; + const GENERAL_RETURN_REGS: &'static [AArch64GeneralReg] = Self::GENERAL_PARAM_REGS; + const GENERAL_DEFAULT_FREE_REGS: &'static [AArch64GeneralReg] = &[ + // The regs we want to use first should be at the end of this vec. + // We will use pop to get which reg to use next + + // Don't use frame pointer: AArch64GeneralReg::FP, + // Don't user indirect result location: AArch64GeneralReg::XR, + // Don't use platform register: AArch64GeneralReg::PR, + // Don't use link register: AArch64GeneralReg::LR, + // Don't use zero register/stack pointer: AArch64GeneralReg::ZRSP, + // Don't use x15: we use it as a scratch register in our assembly + + // Use callee saved regs last. + AArch64GeneralReg::X19, + AArch64GeneralReg::X20, + AArch64GeneralReg::X21, + AArch64GeneralReg::X22, + AArch64GeneralReg::X23, + AArch64GeneralReg::X24, + AArch64GeneralReg::X25, + AArch64GeneralReg::X26, + AArch64GeneralReg::X27, + AArch64GeneralReg::X28, + // Use caller saved regs first. + AArch64GeneralReg::X0, + AArch64GeneralReg::X1, + AArch64GeneralReg::X2, + AArch64GeneralReg::X3, + AArch64GeneralReg::X4, + AArch64GeneralReg::X5, + AArch64GeneralReg::X6, + AArch64GeneralReg::X7, + AArch64GeneralReg::X9, + AArch64GeneralReg::X10, + AArch64GeneralReg::X11, + AArch64GeneralReg::X12, + AArch64GeneralReg::X13, + AArch64GeneralReg::X14, + // AArch64GeneralReg::X15, used in our assembly as a temporary register + AArch64GeneralReg::IP0, + AArch64GeneralReg::IP1, + ]; + + // The first eight registers, v0-v7, are used to pass argument values + // into a subroutine and to return result values from a function. + const FLOAT_PARAM_REGS: &'static [AArch64FloatReg] = &[ + AArch64FloatReg::V0, + AArch64FloatReg::V1, + AArch64FloatReg::V2, + AArch64FloatReg::V3, + AArch64FloatReg::V4, + AArch64FloatReg::V5, + AArch64FloatReg::V6, + AArch64FloatReg::V7, + ]; + const FLOAT_RETURN_REGS: &'static [AArch64FloatReg] = Self::FLOAT_PARAM_REGS; + const FLOAT_DEFAULT_FREE_REGS: &'static [AArch64FloatReg] = &[ + // + AArch64FloatReg::V31, + AArch64FloatReg::V30, + AArch64FloatReg::V29, + AArch64FloatReg::V28, + AArch64FloatReg::V27, + AArch64FloatReg::V26, + AArch64FloatReg::V25, + AArch64FloatReg::V24, + // + AArch64FloatReg::V23, + AArch64FloatReg::V22, + AArch64FloatReg::V21, + AArch64FloatReg::V20, + AArch64FloatReg::V19, + AArch64FloatReg::V18, + AArch64FloatReg::V17, + AArch64FloatReg::V16, + // + AArch64FloatReg::V15, + AArch64FloatReg::V14, + AArch64FloatReg::V13, + AArch64FloatReg::V12, + AArch64FloatReg::V11, + AArch64FloatReg::V10, + AArch64FloatReg::V9, + AArch64FloatReg::V8, + // + AArch64FloatReg::V7, + AArch64FloatReg::V6, + AArch64FloatReg::V5, + AArch64FloatReg::V4, + AArch64FloatReg::V3, + AArch64FloatReg::V2, + AArch64FloatReg::V1, + AArch64FloatReg::V0, + ]; + + /// 16 is the size of the pushed return address and base pointer. These are usually stored like so + /// + /// 213560: d101c3ff sub sp, sp, #0x70 + /// 213564: f90037fe str x30, [sp, #104] + /// 213568: f90033fd str x29, [sp, #96] + const SHADOW_SPACE_SIZE: u8 = 16; + + // These are registers that a called function must save and restore if it wants to use them. + #[inline(always)] + fn general_callee_saved(reg: &AArch64GeneralReg) -> bool { + matches!( + reg, + AArch64GeneralReg::X19 + | AArch64GeneralReg::X20 + | AArch64GeneralReg::X21 + | AArch64GeneralReg::X22 + | AArch64GeneralReg::X23 + | AArch64GeneralReg::X24 + | AArch64GeneralReg::X25 + | AArch64GeneralReg::X26 + | AArch64GeneralReg::X27 + | AArch64GeneralReg::X28 + ) + } + #[inline(always)] + fn float_callee_saved(reg: &AArch64FloatReg) -> bool { + // Registers v8-v15 must be preserved by a callee across subroutine calls; + matches!( + reg, + AArch64FloatReg::V8 + | AArch64FloatReg::V9 + | AArch64FloatReg::V10 + | AArch64FloatReg::V11 + | AArch64FloatReg::V12 + | AArch64FloatReg::V13 + | AArch64FloatReg::V14 + | AArch64FloatReg::V15 + ) + } + + #[inline(always)] + fn setup_stack( + buf: &mut Vec<'_, u8>, + saved_general_regs: &[AArch64GeneralReg], + saved_float_regs: &[AArch64FloatReg], + requested_stack_size: i32, + fn_call_stack_size: i32, + ) -> i32 { + let frame_pointer_link_register = 16; + + // Full size is upcast to i64 to make sure we don't overflow here. + let full_stack_size = match requested_stack_size + .checked_add(8 * (saved_general_regs.len() + saved_float_regs.len()) as i32) + // space for the frame pointer FP and the link register LR + .and_then(|size| size.checked_add(frame_pointer_link_register)) + // extra space for arguments that did not fit into registers + .and_then(|size| size.checked_add(fn_call_stack_size)) + { + Some(size) => size, + _ => internal_error!("Ran out of stack space"), + }; + + const fn next_multiple_of(lhs: i32, rhs: i32) -> i32 { + match lhs % rhs { + 0 => lhs, + r => lhs + (rhs - r), + } + } + + let aligned_stack_size = next_multiple_of(full_stack_size, STACK_ALIGNMENT as i32); + + if aligned_stack_size > 0 { + // sub sp, sp, #0x10 + AArch64Assembler::sub_reg64_reg64_imm32( + buf, + AArch64GeneralReg::ZRSP, + AArch64GeneralReg::ZRSP, + aligned_stack_size, + ); + + // All the following stores could be optimized by using `STP` to store pairs. + let w = aligned_stack_size; + AArch64Assembler::mov_stack32_reg64(buf, w - 0x10, AArch64GeneralReg::FP); + AArch64Assembler::mov_stack32_reg64(buf, w - 0x08, AArch64GeneralReg::LR); + + // update the frame pointer + AArch64Assembler::add_reg64_reg64_imm32( + buf, + AArch64GeneralReg::FP, + AArch64GeneralReg::ZRSP, + w - frame_pointer_link_register, + ); + + let mut offset = aligned_stack_size - fn_call_stack_size - frame_pointer_link_register; + for reg in saved_general_regs { + AArch64Assembler::mov_base32_reg64(buf, -offset, *reg); + offset -= 8; + } + for reg in saved_float_regs { + AArch64Assembler::mov_base32_freg64(buf, -offset, *reg); + offset -= 8; + } + aligned_stack_size + } else { + 0 + } + } + + #[inline(always)] + fn cleanup_stack( + buf: &mut Vec<'_, u8>, + saved_general_regs: &[AArch64GeneralReg], + saved_float_regs: &[AArch64FloatReg], + aligned_stack_size: i32, + fn_call_stack_size: i32, + ) { + let frame_pointer_link_register = 16; + + if aligned_stack_size > 0 { + // All the following stores could be optimized by using `STP` to store pairs. + let mut offset = aligned_stack_size - fn_call_stack_size - frame_pointer_link_register; + + for reg in saved_general_regs { + AArch64Assembler::mov_reg64_base32(buf, *reg, -offset); + offset -= 8; + } + + for reg in saved_float_regs { + AArch64Assembler::mov_freg64_base32(buf, *reg, -offset); + offset -= 8; + } + + let w = aligned_stack_size; + AArch64Assembler::mov_reg64_stack32(buf, AArch64GeneralReg::FP, w - 0x10); + AArch64Assembler::mov_reg64_stack32(buf, AArch64GeneralReg::LR, w - 0x08); + + AArch64Assembler::add_reg64_reg64_imm32( + buf, + AArch64GeneralReg::ZRSP, + AArch64GeneralReg::ZRSP, + aligned_stack_size, + ); + } + } + + #[inline(always)] + fn load_args<'a>( + buf: &mut Vec<'a, u8>, + storage_manager: &mut AArch64StorageManager<'a, '_>, + layout_interner: &mut STLayoutInterner<'a>, + args: &'a [(InLayout<'a>, Symbol)], + ret_layout: &InLayout<'a>, + ) { + // loading arguments occurs at an offset (but storing arguments does not) + let mut state = AArch64CallLoadArgs { + general_i: 0, + float_i: 0, + // 16 is the size of the pushed return address and base pointer. + argument_offset: AArch64Call::SHADOW_SPACE_SIZE as i32, + }; + + if AArch64Call::returns_via_arg_pointer(layout_interner, ret_layout) { + storage_manager.ret_pointer_arg(AArch64GeneralReg::XR); + } + + for (in_layout, sym) in args.iter() { + state.load_arg(buf, storage_manager, layout_interner, *sym, *in_layout); + } + } + + #[inline(always)] + fn store_args<'a>( + buf: &mut Vec<'a, u8>, + storage_manager: &mut AArch64StorageManager<'a, '_>, + layout_interner: &mut STLayoutInterner<'a>, + dst: &Symbol, + args: &[Symbol], + arg_layouts: &[InLayout<'a>], + ret_layout: &InLayout<'a>, + ) { + if Self::returns_via_arg_pointer(layout_interner, ret_layout) { + // Save space on the stack for the result we will be return. + let base_offset = + storage_manager.claim_stack_area_layout(layout_interner, *dst, *ret_layout); + + // Set the xr (x8) register to the address base + offset. + AArch64Assembler::add_reg64_reg64_imm32( + buf, + AArch64GeneralReg::XR, + AArch64Call::BASE_PTR_REG, + base_offset, + ); + } + + // storing arguments does not have a stack offset (loading arguments does) + let mut state = AArch64CallStoreArgs { + general_i: 0, + float_i: 0, + tmp_stack_offset: 0, + }; + + for (sym, in_layout) in args.iter().zip(arg_layouts.iter()) { + state.store_arg(buf, storage_manager, layout_interner, *sym, *in_layout); + } + + storage_manager.update_fn_call_stack_size(state.tmp_stack_offset as u32); + } + + fn return_complex_symbol<'a>( + buf: &mut Vec<'a, u8>, + storage_manager: &mut AArch64StorageManager<'a, '_>, + layout_interner: &mut STLayoutInterner<'a>, + sym: &Symbol, + layout: &InLayout<'a>, + ) { + type ASM = AArch64Assembler; + + match layout_interner.get_repr(*layout) { + single_register_layouts!() => { + internal_error!("single register layouts are not complex symbols"); + } + _ if layout_interner.stack_size(*layout) == 0 => {} + _ if !Self::returns_via_arg_pointer(layout_interner, layout) => { + let (base_offset, size) = storage_manager.stack_offset_and_size(sym); + debug_assert_eq!(base_offset % 8, 0); + if size <= 8 { + ASM::mov_reg64_base32(buf, Self::GENERAL_RETURN_REGS[0], base_offset); + } else if size <= 16 { + ASM::mov_reg64_base32(buf, Self::GENERAL_RETURN_REGS[0], base_offset); + ASM::mov_reg64_base32(buf, Self::GENERAL_RETURN_REGS[1], base_offset + 8); + } else { + internal_error!( + "types that don't return via arg pointer must be less than 16 bytes" + ); + } + } + _ => { + // This is a large type returned via the arg pointer. + storage_manager.copy_symbol_to_arg_pointer(buf, sym, layout); + + // Also set the return reg to the arg pointer. + storage_manager.load_to_specified_general_reg( + buf, + &Symbol::RET_POINTER, + Self::GENERAL_RETURN_REGS[0], + ); + } + } + } + + fn load_returned_complex_symbol<'a>( + buf: &mut Vec<'a, u8>, + storage_manager: &mut AArch64StorageManager<'a, '_>, + layout_interner: &mut STLayoutInterner<'a>, + sym: &Symbol, + layout: &InLayout<'a>, + ) { + match layout_interner.get_repr(*layout) { + single_register_layouts!() => { + internal_error!("single register layouts are not complex symbols"); + } + _ if layout_interner.stack_size(*layout) == 0 => { + storage_manager.no_data(sym); + } + _ if !Self::returns_via_arg_pointer(layout_interner, layout) => { + let size = layout_interner.stack_size(*layout); + let offset = + storage_manager.claim_stack_area_layout(layout_interner, *sym, *layout); + if size <= 8 { + AArch64Assembler::mov_base32_reg64(buf, offset, Self::GENERAL_RETURN_REGS[0]); + } else if size <= 16 { + AArch64Assembler::mov_base32_reg64(buf, offset, Self::GENERAL_RETURN_REGS[0]); + AArch64Assembler::mov_base32_reg64( + buf, + offset + 8, + Self::GENERAL_RETURN_REGS[1], + ); + } else { + internal_error!( + "types that don't return via arg pointer must be less than 16 bytes" + ); + } + } + _ => { + // This should have been recieved via an arg pointer. + // That means the value is already loaded onto the stack area we allocated before the call. + // Nothing to do. + } + } + } + + fn setjmp(buf: &mut Vec<'_, u8>) { + use AArch64GeneralReg::*; + type ASM = AArch64Assembler; + + // based on the musl libc setjmp implementation + // + // 0000000000211570 <__setjmp>: + // 211570: a9005013 stp x19, x20, [x0] + // 211574: a9015815 stp x21, x22, [x0, #16] + // 211578: a9026017 stp x23, x24, [x0, #32] + // 21157c: a9036819 stp x25, x26, [x0, #48] + // 211580: a904701b stp x27, x28, [x0, #64] + // 211584: a905781d stp x29, x30, [x0, #80] + // 211588: 910003e2 mov x2, sp + // 21158c: f9003402 str x2, [x0, #104] + // 211590: 6d072408 stp d8, d9, [x0, #112] + // 211594: 6d082c0a stp d10, d11, [x0, #128] + // 211598: 6d09340c stp d12, d13, [x0, #144] + // 21159c: 6d0a3c0e stp d14, d15, [x0, #160] + // 2115a0: d2800000 mov x0, #0x0 // #0 + // 2115a4: d65f03c0 ret + + let env = X0; + + // store caller-saved (i.e. non-volatile) registers + ASM::mov_mem64_offset32_reg64(buf, env, 0x00, X19); + ASM::mov_mem64_offset32_reg64(buf, env, 0x08, X20); + ASM::mov_mem64_offset32_reg64(buf, env, 0x10, X21); + ASM::mov_mem64_offset32_reg64(buf, env, 0x18, X22); + ASM::mov_mem64_offset32_reg64(buf, env, 0x20, X23); + ASM::mov_mem64_offset32_reg64(buf, env, 0x28, X24); + ASM::mov_mem64_offset32_reg64(buf, env, 0x30, X25); + ASM::mov_mem64_offset32_reg64(buf, env, 0x38, X26); + ASM::mov_mem64_offset32_reg64(buf, env, 0x40, X27); + ASM::mov_mem64_offset32_reg64(buf, env, 0x48, X28); + ASM::mov_mem64_offset32_reg64(buf, env, 0x50, FP); + ASM::mov_mem64_offset32_reg64(buf, env, 0x58, LR); + + // mov of sp requires an addition with zero + ASM::add_reg64_reg64_imm32(buf, X2, ZRSP, 0); + ASM::mov_mem64_offset32_reg64(buf, env, 104, X2); + + ASM::mov_mem64_offset32_freg64(buf, env, 112, AArch64FloatReg::V8); + ASM::mov_mem64_offset32_freg64(buf, env, 120, AArch64FloatReg::V9); + ASM::mov_mem64_offset32_freg64(buf, env, 128, AArch64FloatReg::V10); + ASM::mov_mem64_offset32_freg64(buf, env, 136, AArch64FloatReg::V11); + ASM::mov_mem64_offset32_freg64(buf, env, 144, AArch64FloatReg::V12); + ASM::mov_mem64_offset32_freg64(buf, env, 152, AArch64FloatReg::V13); + ASM::mov_mem64_offset32_freg64(buf, env, 160, AArch64FloatReg::V14); + ASM::mov_mem64_offset32_freg64(buf, env, 168, AArch64FloatReg::V15); + + ASM::mov_reg64_imm64(buf, X0, 0); + + ASM::ret(buf) + } + + fn longjmp(buf: &mut Vec<'_, u8>) { + use AArch64GeneralReg::*; + type ASM = AArch64Assembler; + + // 0000000000211534 <_longjmp>: + // 211534: a9405013 ldp x19, x20, [x0] + // 211538: a9415815 ldp x21, x22, [x0, #16] + // 21153c: a9426017 ldp x23, x24, [x0, #32] + // 211540: a9436819 ldp x25, x26, [x0, #48] + // 211544: a944701b ldp x27, x28, [x0, #64] + // 211548: a945781d ldp x29, x30, [x0, #80] + // 21154c: f9403402 ldr x2, [x0, #104] + // 211550: 9100005f mov sp, x2 + // 211554: 6d472408 ldp d8, d9, [x0, #112] + // 211558: 6d482c0a ldp d10, d11, [x0, #128] + // 21155c: 6d49340c ldp d12, d13, [x0, #144] + // 211560: 6d4a3c0e ldp d14, d15, [x0, #160] + // 211564: 7100003f cmp w1, #0x0 + // 211568: 1a9f1420 csinc w0, w1, wzr, ne // ne = any + // 21156c: d61f03c0 br x30 + + // load the caller-saved registers + let env = X0; + + ASM::mov_reg64_mem64_offset32(buf, X19, env, 0x00); + ASM::mov_reg64_mem64_offset32(buf, X20, env, 0x08); + ASM::mov_reg64_mem64_offset32(buf, X21, env, 0x10); + ASM::mov_reg64_mem64_offset32(buf, X22, env, 0x18); + ASM::mov_reg64_mem64_offset32(buf, X23, env, 0x20); + ASM::mov_reg64_mem64_offset32(buf, X24, env, 0x28); + ASM::mov_reg64_mem64_offset32(buf, X25, env, 0x30); + ASM::mov_reg64_mem64_offset32(buf, X26, env, 0x38); + ASM::mov_reg64_mem64_offset32(buf, X27, env, 0x40); + ASM::mov_reg64_mem64_offset32(buf, X28, env, 0x48); + ASM::mov_reg64_mem64_offset32(buf, FP, env, 0x50); + ASM::mov_reg64_mem64_offset32(buf, LR, env, 0x58); + + ASM::mov_reg64_mem64_offset32(buf, X2, env, 104); + ASM::add_reg64_reg64_imm32(buf, ZRSP, X2, 0); + + ASM::mov_freg64_mem64_offset32(buf, AArch64FloatReg::V8, env, 112); + ASM::mov_freg64_mem64_offset32(buf, AArch64FloatReg::V9, env, 120); + ASM::mov_freg64_mem64_offset32(buf, AArch64FloatReg::V10, env, 128); + ASM::mov_freg64_mem64_offset32(buf, AArch64FloatReg::V11, env, 136); + ASM::mov_freg64_mem64_offset32(buf, AArch64FloatReg::V12, env, 144); + ASM::mov_freg64_mem64_offset32(buf, AArch64FloatReg::V13, env, 152); + ASM::mov_freg64_mem64_offset32(buf, AArch64FloatReg::V14, env, 160); + ASM::mov_freg64_mem64_offset32(buf, AArch64FloatReg::V15, env, 168); + + // Move the string pointer into X0 + // Move the panic tag into X1 + ASM::mov_reg64_reg64(buf, X0, X1); + ASM::mov_reg64_reg64(buf, X1, X10); + + // Break to the value of the link register + jmp_reg64(buf, LR); + } + + fn roc_panic(buf: &mut Vec<'_, u8>, relocs: &mut Vec<'_, Relocation>) { + use AArch64GeneralReg::*; + type ASM = AArch64Assembler; + + // move the first argument to roc_panic (a *RocStr) into x9 + ASM::mov_reg64_reg64(buf, X9, X0); + + // move the crash tag into the second return register. We add 1 to it because the 0 value + // is already used for "no crash occurred" + ASM::add_reg64_reg64_imm32(buf, X10, X1, 1); + + // the setlongjmp_buffer + ASM::data_pointer(buf, relocs, String::from("setlongjmp_buffer"), X0); + + // the value to return from the longjmp. It is a pointer to the last 3 words of the setlongjmp_buffer + // they represent the error message. (168 + 8) which is after V15 register. + ASM::mov_reg64_imm64(buf, X1, 176); + ASM::add_reg64_reg64_reg64(buf, X1, X1, X0); + + for offset in [0, 8, 16] { + ASM::mov_reg64_mem64_offset32(buf, X11, X9, offset); + ASM::mov_mem64_offset32_reg64(buf, X1, offset, X11); + } + + Self::longjmp(buf) + } +} + +fn copy_symbol_to_stack_offset<'a, CC>( + buf: &mut Vec<'a, u8>, + storage_manager: &mut AArch64StorageManager<'a, '_>, + sym: Symbol, + tmp_reg: AArch64GeneralReg, + stack_offset: i32, +) -> u32 +where + CC: CallConv, +{ + type ASM = AArch64Assembler; + + let mut copied = 0; + let (base_offset, size) = storage_manager.stack_offset_and_size(&sym); + + if size - copied >= 8 { + for _ in (0..(size - copied)).step_by(8) { + ASM::mov_reg64_base32(buf, tmp_reg, base_offset + copied as i32); + ASM::mov_stack32_reg64(buf, stack_offset + copied as i32, tmp_reg); + + copied += 8; + } + } + + if size - copied >= 4 { + for _ in (0..(size - copied)).step_by(4) { + ASM::mov_reg32_base32(buf, tmp_reg, base_offset + copied as i32); + ASM::mov_stack32_reg32(buf, stack_offset + copied as i32, tmp_reg); + + copied += 4; + } + } + + if size - copied >= 2 { + for _ in (0..(size - copied)).step_by(2) { + ASM::mov_reg16_base32(buf, tmp_reg, base_offset + copied as i32); + ASM::mov_stack32_reg16(buf, stack_offset + copied as i32, tmp_reg); + + copied += 2; + } + } + + if size - copied >= 1 { + for _ in (0..(size - copied)).step_by(1) { + ASM::mov_reg8_base32(buf, tmp_reg, base_offset + copied as i32); + ASM::mov_stack32_reg8(buf, stack_offset + copied as i32, tmp_reg); + + copied += 1; + } + } + + size +} + +impl AArch64Call { + fn returns_via_arg_pointer<'a>( + interner: &STLayoutInterner<'a>, + ret_layout: &InLayout<'a>, + ) -> bool { + // TODO: This will need to be more complex/extended to fully support the calling convention. + // details here: https://github.com/hjl-tools/x86-psABI/wiki/x86-64-psABI-1.0.pdf + interner.stack_size(*ret_layout) > 16 + } +} + +type AArch64StorageManager<'a, 'r> = + StorageManager<'a, 'r, AArch64GeneralReg, AArch64FloatReg, AArch64Assembler, AArch64Call>; + +struct AArch64CallLoadArgs { + general_i: usize, + float_i: usize, + argument_offset: i32, +} + +impl AArch64CallLoadArgs { + fn load_arg<'a>( + &mut self, + buf: &mut Vec<'a, u8>, + storage_manager: &mut AArch64StorageManager<'a, '_>, + layout_interner: &mut STLayoutInterner<'a>, + sym: Symbol, + in_layout: InLayout<'a>, + ) { + use Builtin::{Decimal, Int}; + + let stack_size = layout_interner.stack_size(in_layout); + match layout_interner.get_repr(in_layout) { + single_register_integers!() => self.load_arg_general(storage_manager, sym), + pointer_layouts!() => self.load_arg_general(storage_manager, sym), + single_register_floats!() => self.load_arg_float(storage_manager, sym), + LayoutRepr::Builtin(Int(IntWidth::U128 | IntWidth::I128) | Decimal) => { + self.load_arg_general_128bit(buf, storage_manager, layout_interner, sym, in_layout); + } + _ if stack_size == 0 => { + storage_manager.no_data(&sym); + } + _ if stack_size > 16 => { + match AArch64Call::GENERAL_PARAM_REGS.get(self.general_i) { + Some(ptr_reg) => { + // if there is a general purpose register available, use it to store a pointer to the value + let base_offset = storage_manager.claim_stack_area_layout( + layout_interner, + sym, + in_layout, + ); + + let tmp_reg = AArch64GeneralReg::X15; + + super::x86_64::copy_to_base_offset::<_, _, AArch64Assembler>( + buf, + base_offset, + stack_size, + *ptr_reg, + tmp_reg, + 0, + ); + + self.general_i += 1; + } + None => { + // else, pass the value implicitly by copying to the stack (of the new frame) + storage_manager.complex_stack_arg(&sym, self.argument_offset, stack_size); + self.argument_offset += stack_size as i32; + } + } + } + LayoutRepr::LambdaSet(lambda_set) => self.load_arg( + buf, + storage_manager, + layout_interner, + sym, + lambda_set.runtime_representation(), + ), + LayoutRepr::Struct { .. } | LayoutRepr::Union(UnionLayout::NonRecursive(_)) => { + if stack_size <= 8 { + self.load_arg_general_64bit( + buf, + storage_manager, + layout_interner, + sym, + in_layout, + ); + } else if stack_size <= 16 { + self.load_arg_general_128bit( + buf, + storage_manager, + layout_interner, + sym, + in_layout, + ); + } else { + unreachable!("covered by an earlier branch") + } + } + _ => { + todo!( + "Loading args with layout {:?}", + layout_interner.dbg(in_layout) + ); + } + } + } + + fn load_arg_general( + &mut self, + storage_manager: &mut AArch64StorageManager<'_, '_>, + sym: Symbol, + ) { + if let Some(reg) = AArch64Call::GENERAL_PARAM_REGS.get(self.general_i) { + storage_manager.general_reg_arg(&sym, *reg); + self.general_i += 1; + } else { + storage_manager.primitive_stack_arg(&sym, self.argument_offset); + self.argument_offset += 8; + } + } + + fn load_arg_general_64bit( + &mut self, + buf: &mut Vec, + storage_manager: &mut AArch64StorageManager<'_, '_>, + layout_interner: &mut STLayoutInterner<'_>, + sym: Symbol, + in_layout: InLayout<'_>, + ) { + type ASM = AArch64Assembler; + + let reg1 = AArch64Call::GENERAL_PARAM_REGS.get(self.general_i); + + match reg1 { + Some(reg1) => { + let offset = + storage_manager.claim_stack_area_layout(layout_interner, sym, in_layout); + + ASM::mov_base32_reg64(buf, offset, *reg1); + + self.general_i += 1; + } + None => { + storage_manager.complex_stack_arg(&sym, self.argument_offset, 8); + self.argument_offset += 8; + } + } + } + + fn load_arg_general_128bit( + &mut self, + buf: &mut Vec, + storage_manager: &mut AArch64StorageManager<'_, '_>, + layout_interner: &mut STLayoutInterner<'_>, + sym: Symbol, + in_layout: InLayout<'_>, + ) { + type ASM = AArch64Assembler; + + let reg1 = AArch64Call::GENERAL_PARAM_REGS.get(self.general_i); + let reg2 = AArch64Call::GENERAL_PARAM_REGS.get(self.general_i + 1); + + match (reg1, reg2) { + (Some(reg1), Some(reg2)) => { + let offset = + storage_manager.claim_stack_area_layout(layout_interner, sym, in_layout); + + ASM::mov_base32_reg64(buf, offset, *reg1); + ASM::mov_base32_reg64(buf, offset + 8, *reg2); + + self.general_i += 2; + } + _ => { + storage_manager.complex_stack_arg(&sym, self.argument_offset, 16); + self.argument_offset += 16; + } + } + } + + fn load_arg_float(&mut self, storage_manager: &mut AArch64StorageManager<'_, '_>, sym: Symbol) { + if let Some(reg) = AArch64Call::FLOAT_PARAM_REGS.get(self.float_i) { + storage_manager.float_reg_arg(&sym, *reg); + self.float_i += 1; + } else { + storage_manager.primitive_stack_arg(&sym, self.argument_offset); + self.argument_offset += 8; + } + } +} + +struct AArch64CallStoreArgs { + general_i: usize, + float_i: usize, + tmp_stack_offset: i32, +} + +impl AArch64CallStoreArgs { + const GENERAL_PARAM_REGS: &'static [AArch64GeneralReg] = AArch64Call::GENERAL_PARAM_REGS; + + const FLOAT_PARAM_REGS: &'static [AArch64FloatReg] = AArch64Call::FLOAT_PARAM_REGS; + const FLOAT_RETURN_REGS: &'static [AArch64FloatReg] = AArch64Call::FLOAT_RETURN_REGS; + + fn store_arg<'a>( + &mut self, + buf: &mut Vec<'a, u8>, + storage_manager: &mut AArch64StorageManager<'a, '_>, + layout_interner: &mut STLayoutInterner<'a>, + sym: Symbol, + in_layout: InLayout<'a>, + ) { + type CC = AArch64Call; + type ASM = AArch64Assembler; + + // we use the return register as a temporary register; it will be overwritten anyway + let tmp_reg = AArch64GeneralReg::X15; + + match layout_interner.get_repr(in_layout) { + single_register_integers!() => self.store_arg_general(buf, storage_manager, sym), + pointer_layouts!() => self.store_arg_general(buf, storage_manager, sym), + single_register_floats!() => self.store_arg_float(buf, storage_manager, sym), + LayoutRepr::I128 | LayoutRepr::U128 | LayoutRepr::DEC => { + self.store_arg_128bit(buf, storage_manager, sym) + } + _ if layout_interner.stack_size(in_layout) == 0 => {} + _ if layout_interner.stack_size(in_layout) > 16 => { + match Self::GENERAL_PARAM_REGS.get(self.general_i) { + Some(reg) => { + // if there is a general purpose register available, use it to store a pointer to the value + let (base_offset, _size) = storage_manager.stack_offset_and_size(&sym); + + ASM::add_reg64_reg64_imm32(buf, *reg, AArch64GeneralReg::FP, base_offset); + + self.general_i += 1; + } + None => { + // else, pass the value implicitly by copying to the stack (of the new frame) + let stack_offset = self.tmp_stack_offset; + + let size = copy_symbol_to_stack_offset::( + buf, + storage_manager, + sym, + tmp_reg, + stack_offset, + ); + + self.tmp_stack_offset += size as i32; + } + } + } + LayoutRepr::LambdaSet(lambda_set) => self.store_arg( + buf, + storage_manager, + layout_interner, + sym, + lambda_set.runtime_representation(), + ), + LayoutRepr::Struct { .. } | LayoutRepr::Union(UnionLayout::NonRecursive(_)) => { + let stack_size = layout_interner.stack_size(in_layout); + if stack_size <= 8 { + self.store_arg_64bit(buf, storage_manager, sym); + } else if stack_size <= 16 { + self.store_arg_128bit(buf, storage_manager, sym); + } else { + unreachable!("covered by earlier branch"); + } + } + _ => { + todo!( + "calling with arg type, {:?}", + layout_interner.dbg(in_layout) + ); + } + } + } + + fn store_arg_64bit<'a>( + &mut self, + buf: &mut Vec<'a, u8>, + storage_manager: &mut AArch64StorageManager<'a, '_>, + sym: Symbol, + ) { + type ASM = AArch64Assembler; + + let (offset, _) = storage_manager.stack_offset_and_size(&sym); + + match Self::GENERAL_PARAM_REGS.get(self.general_i) { + Some(reg) => { + ASM::mov_reg64_base32(buf, *reg, offset); + + self.general_i += 1; + } + None => { + let tmp = AArch64GeneralReg::X15; + ASM::mov_reg64_base32(buf, tmp, offset); + ASM::mov_stack32_reg64(buf, self.tmp_stack_offset, tmp); + + self.tmp_stack_offset += 8; + } + } + } + + fn store_arg_128bit<'a>( + &mut self, + buf: &mut Vec<'a, u8>, + storage_manager: &mut AArch64StorageManager<'a, '_>, + sym: Symbol, + ) { + type ASM = AArch64Assembler; + + let (offset, _) = storage_manager.stack_offset_and_size(&sym); + + if self.general_i + 1 < Self::GENERAL_PARAM_REGS.len() { + let reg1 = Self::GENERAL_PARAM_REGS[self.general_i]; + let reg2 = Self::GENERAL_PARAM_REGS[self.general_i + 1]; + + ASM::mov_reg64_base32(buf, reg1, offset); + ASM::mov_reg64_base32(buf, reg2, offset + 8); + + self.general_i += 2; + } else { + let reg = AArch64GeneralReg::X15; + ASM::mov_reg64_base32(buf, reg, offset); + ASM::mov_stack32_reg64(buf, self.tmp_stack_offset, reg); + + ASM::mov_reg64_base32(buf, reg, offset + 8); + ASM::mov_stack32_reg64(buf, self.tmp_stack_offset + 8, reg); + + self.tmp_stack_offset += 16; + } + } + + fn store_arg_general<'a>( + &mut self, + buf: &mut Vec<'a, u8>, + storage_manager: &mut AArch64StorageManager<'a, '_>, + sym: Symbol, + ) { + match Self::GENERAL_PARAM_REGS.get(self.general_i) { + Some(reg) => { + storage_manager.load_to_specified_general_reg(buf, &sym, *reg); + self.general_i += 1; + } + None => { + let tmp = AArch64GeneralReg::X15; + + storage_manager.load_to_specified_general_reg(buf, &sym, tmp); + AArch64Assembler::mov_stack32_reg64(buf, self.tmp_stack_offset, tmp); + + self.tmp_stack_offset += 8; + } + } + } + + fn store_arg_float<'a>( + &mut self, + buf: &mut Vec<'a, u8>, + storage_manager: &mut AArch64StorageManager<'a, '_>, + sym: Symbol, + ) { + match Self::FLOAT_PARAM_REGS.get(self.float_i) { + Some(reg) => { + storage_manager.load_to_specified_float_reg(buf, &sym, *reg); + self.float_i += 1; + } + None => { + // Copy to stack using return reg as buffer. + let tmp = Self::FLOAT_RETURN_REGS[0]; + + storage_manager.load_to_specified_float_reg(buf, &sym, tmp); + AArch64Assembler::mov_stack32_freg64(buf, self.tmp_stack_offset, tmp); + + self.tmp_stack_offset += 8; + } + } + } +} + +impl Assembler for AArch64Assembler { + #[inline(always)] + fn abs_reg64_reg64(buf: &mut Vec<'_, u8>, dst: AArch64GeneralReg, src: AArch64GeneralReg) { + cmp_reg64_imm12(buf, src, 0); + cneg_reg64_reg64_cond(buf, dst, src, ConditionCode::MI); + } + + #[inline(always)] + fn abs_freg64_freg64( + buf: &mut Vec<'_, u8>, + _relocs: &mut Vec<'_, Relocation>, + dst: AArch64FloatReg, + src: AArch64FloatReg, + ) { + fabs_freg_freg(buf, FloatWidth::F64, dst, src); + } + + #[inline(always)] + fn add_reg64_reg64_imm32( + buf: &mut Vec<'_, u8>, + dst: AArch64GeneralReg, + src: AArch64GeneralReg, + imm32: i32, + ) { + if imm32 < 0 { + Self::sub_reg64_reg64_imm32(buf, dst, src, -imm32); + } else if imm32 < 0xFFF { + add_reg64_reg64_imm12(buf, dst, src, imm32 as u16); + } else { + todo!("immediate additions with values greater than 12bits"); + } + } + #[inline(always)] + fn add_reg64_reg64_reg64( + buf: &mut Vec<'_, u8>, + dst: AArch64GeneralReg, + src1: AArch64GeneralReg, + src2: AArch64GeneralReg, + ) { + add_reg64_reg64_reg64(buf, dst, src1, src2); + } + #[inline(always)] + fn add_freg32_freg32_freg32( + buf: &mut Vec<'_, u8>, + dst: AArch64FloatReg, + src1: AArch64FloatReg, + src2: AArch64FloatReg, + ) { + fadd_freg_freg_freg(buf, FloatWidth::F32, dst, src1, src2); + } + #[inline(always)] + fn add_freg64_freg64_freg64( + buf: &mut Vec<'_, u8>, + dst: AArch64FloatReg, + src1: AArch64FloatReg, + src2: AArch64FloatReg, + ) { + fadd_freg_freg_freg(buf, FloatWidth::F64, dst, src1, src2); + } + + #[inline(always)] + fn call(buf: &mut Vec<'_, u8>, relocs: &mut Vec<'_, Relocation>, fn_name: String) { + let inst = 0b1001_0100_0000_0000_0000_0000_0000_0000u32; + buf.extend(inst.to_le_bytes()); + relocs.push(Relocation::LinkedFunction { + offset: buf.len() as u64 - 4, + name: fn_name, + }); + } + + #[inline(always)] + fn function_pointer( + buf: &mut Vec<'_, u8>, + relocs: &mut Vec<'_, Relocation>, + fn_name: String, + dst: AArch64GeneralReg, + ) { + // a function pointer is the same as a data pointer on AArch64 + Self::data_pointer(buf, relocs, fn_name, dst) + } + + #[inline(always)] + fn data_pointer( + buf: &mut Vec<'_, u8>, + relocs: &mut Vec<'_, Relocation>, + fn_name: String, + dst: AArch64GeneralReg, + ) { + // an `adrp` instruction and an addition to add in the lower bits + buf.extend((0x9000_0000u32 | dst.id() as u32).to_le_bytes()); + Self::add_reg64_reg64_imm32(buf, dst, dst, 0); + + // with elf + // + // 700: 90000001 adrp x1, 0x0 + // 0000000000000700: R_AARCH64_ADR_PREL_PG_HI21 .rodata+0x650 + // 704: 91000021 add x1, x1, #0x0 + // 0000000000000704: R_AARCH64_ADD_ABS_LO12_NC .rodata+0x650 + // + // with macho + // + // 4dc: 90000001 adrp x1, 0x0 + // 00000000000004dc: ARM64_RELOC_PAGE21 ___unnamed_6 + // 4e0: 91000021 add x1, x1, #0x0 + // 00000000000004e0: ARM64_RELOC_PAGEOFF12 ___unnamed_6 + + relocs.push(Relocation::LinkedData { + offset: buf.len() as u64 - 8, + name: fn_name, + }); + } + + #[inline(always)] + fn imul_reg64_reg64_reg64( + buf: &mut Vec<'_, u8>, + dst: AArch64GeneralReg, + src1: AArch64GeneralReg, + src2: AArch64GeneralReg, + ) { + mul_reg64_reg64_reg64(buf, dst, src1, src2); + } + + fn umul_reg64_reg64_reg64<'a, ASM, CC>( + buf: &mut Vec<'a, u8>, + _storage_manager: &mut StorageManager<'a, '_, AArch64GeneralReg, AArch64FloatReg, ASM, CC>, + dst: AArch64GeneralReg, + src1: AArch64GeneralReg, + src2: AArch64GeneralReg, + ) where + ASM: Assembler, + CC: CallConv, + { + mul_reg64_reg64_reg64(buf, dst, src1, src2); + } + + fn idiv_reg64_reg64_reg64<'a, ASM, CC>( + buf: &mut Vec<'a, u8>, + _storage_manager: &mut StorageManager<'a, '_, AArch64GeneralReg, AArch64FloatReg, ASM, CC>, + dst: AArch64GeneralReg, + src1: AArch64GeneralReg, + src2: AArch64GeneralReg, + ) where + ASM: Assembler, + CC: CallConv, + { + sdiv_reg64_reg64_reg64(buf, dst, src1, src2); + } + + fn udiv_reg64_reg64_reg64<'a, ASM, CC>( + buf: &mut Vec<'a, u8>, + _storage_manager: &mut StorageManager<'a, '_, AArch64GeneralReg, AArch64FloatReg, ASM, CC>, + dst: AArch64GeneralReg, + src1: AArch64GeneralReg, + src2: AArch64GeneralReg, + ) where + ASM: Assembler, + CC: CallConv, + { + udiv_reg64_reg64_reg64(buf, dst, src1, src2); + } + + fn irem_reg64_reg64_reg64<'a, ASM, CC>( + buf: &mut Vec<'a, u8>, + _storage_manager: &mut StorageManager<'a, '_, AArch64GeneralReg, AArch64FloatReg, ASM, CC>, + dst: AArch64GeneralReg, + src1: AArch64GeneralReg, + src2: AArch64GeneralReg, + ) where + ASM: Assembler, + CC: CallConv, + { + sdiv_reg64_reg64_reg64(buf, dst, src1, src2); + mul_reg64_reg64_reg64(buf, dst, dst, src2); + subs_reg64_reg64_reg64(buf, dst, src1, dst); + } + + fn urem_reg64_reg64_reg64<'a, ASM, CC>( + buf: &mut Vec<'a, u8>, + _storage_manager: &mut StorageManager<'a, '_, AArch64GeneralReg, AArch64FloatReg, ASM, CC>, + dst: AArch64GeneralReg, + src1: AArch64GeneralReg, + src2: AArch64GeneralReg, + ) where + ASM: Assembler, + CC: CallConv, + { + udiv_reg64_reg64_reg64(buf, dst, src1, src2); + mul_reg64_reg64_reg64(buf, dst, dst, src2); + subs_reg64_reg64_reg64(buf, dst, src1, dst); + } + + #[inline(always)] + fn mul_freg32_freg32_freg32( + buf: &mut Vec<'_, u8>, + dst: AArch64FloatReg, + src1: AArch64FloatReg, + src2: AArch64FloatReg, + ) { + fmul_freg_freg_freg(buf, FloatWidth::F32, dst, src1, src2); + } + #[inline(always)] + fn mul_freg64_freg64_freg64( + buf: &mut Vec<'_, u8>, + dst: AArch64FloatReg, + src1: AArch64FloatReg, + src2: AArch64FloatReg, + ) { + fmul_freg_freg_freg(buf, FloatWidth::F64, dst, src1, src2); + } + + #[inline(always)] + fn div_freg32_freg32_freg32( + buf: &mut Vec<'_, u8>, + dst: AArch64FloatReg, + src1: AArch64FloatReg, + src2: AArch64FloatReg, + ) { + fdiv_freg_freg_freg(buf, FloatWidth::F32, dst, src1, src2); + } + #[inline(always)] + fn div_freg64_freg64_freg64( + buf: &mut Vec<'_, u8>, + dst: AArch64FloatReg, + src1: AArch64FloatReg, + src2: AArch64FloatReg, + ) { + fdiv_freg_freg_freg(buf, FloatWidth::F64, dst, src1, src2); + } + + #[inline(always)] + fn jmp_imm32(buf: &mut Vec<'_, u8>, offset: i32) -> usize { + if (-(1 << 27)..(1 << 27)).contains(&offset) { + b_imm26(buf, offset); + } else { + todo!("jump offsets over 27 bits for AArch64: {:#x}", offset); + } + + // on aarch64, jumps are calculated from the start of the jmp instruction + buf.len() - 4 + } + + #[inline(always)] + fn tail_call(buf: &mut Vec<'_, u8>) -> u64 { + Self::jmp_imm32(buf, 0); + buf.len() as u64 - 4 // TODO is 4 the correct offset in ARM? + } + + #[inline(always)] + fn jne_reg64_imm64_imm32<'a, ASM, CC>( + buf: &mut Vec<'a, u8>, + _storage_manager: &mut StorageManager<'a, '_, AArch64GeneralReg, AArch64FloatReg, ASM, CC>, + reg: AArch64GeneralReg, + imm: u64, + offset: i32, + ) -> usize + where + ASM: Assembler, + CC: CallConv, + { + if imm < (1 << 12) { + cmp_reg64_imm12(buf, reg, imm as u16); + } else { + let tmp = AArch64GeneralReg::X15; + Self::mov_reg64_imm64(buf, tmp, i64::from_ne_bytes(imm.to_ne_bytes())); + cmp_reg64_reg64(buf, reg, tmp); + } + + if (-(1 << 20)..(1 << 20)).contains(&offset) { + b_cond_imm19(buf, ConditionCode::NE, offset); + } else { + todo!("jump offsets over 20 bits for AArch64: {:#x}", offset); + } + + // on aarch64, jumps are calculated from the start of the jmp instruction + buf.len() - 4 + } + + #[inline(always)] + fn mov_freg32_imm32( + buf: &mut Vec<'_, u8>, + _relocs: &mut Vec<'_, Relocation>, + dst: AArch64FloatReg, + imm: f32, + ) { + // See https://stackoverflow.com/a/64608524 + if imm == 0.0 && !imm.is_sign_negative() { + movi_freg_zero(buf, dst); + return; + } + + match encode_f32_to_imm8(imm) { + Some(imm8) => { + fmov_freg_imm8(buf, FloatWidth::F32, dst, imm8); + } + None => { + let tmp = AArch64GeneralReg::X15; + Self::mov_reg64_imm64(buf, tmp, i32::from_ne_bytes(imm.to_ne_bytes()) as i64); + Self::mov_freg32_reg32(buf, dst, tmp); + } + } + } + #[inline(always)] + fn mov_freg64_imm64( + buf: &mut Vec<'_, u8>, + _relocs: &mut Vec<'_, Relocation>, + dst: AArch64FloatReg, + imm: f64, + ) { + // See https://stackoverflow.com/a/64608524 + if imm == 0.0 && !imm.is_sign_negative() { + movi_freg_zero(buf, dst); + return; + } + + match encode_f64_to_imm8(imm) { + Some(imm8) => { + fmov_freg_imm8(buf, FloatWidth::F64, dst, imm8); + } + None => { + let tmp = AArch64GeneralReg::X15; + Self::mov_reg64_imm64(buf, tmp, i64::from_ne_bytes(imm.to_ne_bytes())); + Self::mov_freg64_reg64(buf, dst, tmp) + } + } + } + #[inline(always)] + fn mov_reg64_imm64(buf: &mut Vec<'_, u8>, dst: AArch64GeneralReg, imm: i64) { + let mut remaining = imm as u64; + movz_reg64_imm16(buf, dst, remaining as u16, 0); + remaining >>= 16; + if remaining > 0 { + movk_reg64_imm16(buf, dst, remaining as u16, 1); + } + remaining >>= 16; + if remaining > 0 { + movk_reg64_imm16(buf, dst, remaining as u16, 2); + } + remaining >>= 16; + if remaining > 0 { + movk_reg64_imm16(buf, dst, remaining as u16, 3); + } + } + + #[inline(always)] + fn mov_freg64_freg64(buf: &mut Vec<'_, u8>, dst: AArch64FloatReg, src: AArch64FloatReg) { + if dst != src { + fmov_freg_freg(buf, FloatWidth::F64, dst, src); + } + } + + #[inline(always)] + fn mov_reg32_freg32(buf: &mut Vec<'_, u8>, dst: AArch64GeneralReg, src: AArch64FloatReg) { + fmov_reg_freg(buf, FloatWidth::F32, dst, src) + } + #[inline(always)] + fn mov_reg64_freg64(buf: &mut Vec<'_, u8>, dst: AArch64GeneralReg, src: AArch64FloatReg) { + fmov_reg_freg(buf, FloatWidth::F64, dst, src) + } + + #[inline(always)] + fn mov_freg32_reg32(buf: &mut Vec<'_, u8>, dst: AArch64FloatReg, src: AArch64GeneralReg) { + fmov_freg_reg(buf, FloatWidth::F32, dst, src) + } + #[inline(always)] + fn mov_freg64_reg64(buf: &mut Vec<'_, u8>, dst: AArch64FloatReg, src: AArch64GeneralReg) { + fmov_freg_reg(buf, FloatWidth::F64, dst, src) + } + + #[inline(always)] + fn mov_reg_reg( + buf: &mut Vec<'_, u8>, + register_width: RegisterWidth, + dst: AArch64GeneralReg, + src: AArch64GeneralReg, + ) { + if dst != src { + // TODO can we be more fine-grained here? there is a 32-bit version of this instruction + // but that does not really help for W8 and W16 + match register_width { + RegisterWidth::W8 => mov_reg64_reg64(buf, dst, src), + RegisterWidth::W16 => mov_reg64_reg64(buf, dst, src), + RegisterWidth::W32 => mov_reg64_reg64(buf, dst, src), + RegisterWidth::W64 => mov_reg64_reg64(buf, dst, src), + } + } + } + + #[inline(always)] + fn movsx_reg_reg( + buf: &mut Vec<'_, u8>, + input_width: RegisterWidth, + dst: AArch64GeneralReg, + src: AArch64GeneralReg, + ) { + sign_extend(buf, input_width, dst, src) + } + + #[inline(always)] + fn movzx_reg_reg( + buf: &mut Vec<'_, u8>, + input_width: RegisterWidth, + dst: AArch64GeneralReg, + src: AArch64GeneralReg, + ) { + // moving with zero extension is the default in arm + Self::mov_reg_reg(buf, input_width, dst, src) + } + + #[inline(always)] + fn mov_freg64_base32(buf: &mut Vec<'_, u8>, dst: AArch64FloatReg, offset: i32) { + Self::mov_freg64_mem64_offset32(buf, dst, AArch64GeneralReg::FP, offset) + } + + #[inline(always)] + fn mov_reg_mem_offset32( + buf: &mut Vec<'_, u8>, + register_width: RegisterWidth, + dst: AArch64GeneralReg, + src: AArch64GeneralReg, + offset: i32, + ) { + if (-256..256).contains(&offset) { + ldur_reg_reg_imm9(buf, register_width, dst, src, offset as i16); + } else if (0..=u16::MAX as i32).contains(&offset) { + debug_assert!(offset % 8 == 0); + ldr_reg_reg_imm12(buf, register_width, dst, src, (offset as u16) >> 3); + } else { + let tmp = AArch64GeneralReg::X15; + Self::mov_reg64_imm64(buf, tmp, offset as i64); + Self::add_reg64_reg64_reg64(buf, tmp, tmp, src); + ldr_reg_reg_imm12(buf, register_width, dst, tmp, 0); + } + } + + #[inline(always)] + fn mov_reg_base32( + buf: &mut Vec<'_, u8>, + register_width: RegisterWidth, + dst: AArch64GeneralReg, + offset: i32, + ) { + Self::mov_reg_mem_offset32(buf, register_width, dst, AArch64GeneralReg::FP, offset) + } + + #[inline(always)] + fn mov_base32_freg64(buf: &mut Vec<'_, u8>, offset: i32, src: AArch64FloatReg) { + Self::mov_mem64_offset32_freg64(buf, AArch64GeneralReg::FP, offset, src) + } + #[inline(always)] + fn mov_base32_freg32(buf: &mut Vec<'_, u8>, offset: i32, src: AArch64FloatReg) { + Self::mov_mem64_offset32_freg64(buf, AArch64GeneralReg::FP, offset, src) + } + #[inline(always)] + fn movesd_mem64_offset32_freg64( + buf: &mut Vec<'_, u8>, + ptr: AArch64GeneralReg, + offset: i32, + src: AArch64FloatReg, + ) { + Self::mov_mem64_offset32_freg64(buf, ptr, offset, src) + } + + #[inline(always)] + fn mov_base32_reg( + buf: &mut Vec<'_, u8>, + register_width: RegisterWidth, + offset: i32, + src: AArch64GeneralReg, + ) { + Self::mov_mem_offset32_reg(buf, register_width, AArch64GeneralReg::FP, offset, src) + } + + fn mov_mem_offset32_reg( + buf: &mut Vec<'_, u8>, + register_width: RegisterWidth, + dst: AArch64GeneralReg, + offset: i32, + src: AArch64GeneralReg, + ) { + if (-256..256).contains(&offset) { + stur_reg_reg_imm9(buf, register_width, src, dst, offset as i16); + } else if (0..=u16::MAX as i32).contains(&offset) { + debug_assert_eq!(offset % 8, 0); + str_reg_reg_imm12(buf, register_width, src, dst, (offset as u16) >> 3); + } else { + let tmp = AArch64GeneralReg::X15; + Self::mov_reg64_imm64(buf, tmp, offset as i64); + Self::add_reg64_reg64_reg64(buf, tmp, tmp, dst); + str_reg_reg_imm12(buf, register_width, src, tmp, 0); + } + } + + #[inline(always)] + fn mov_mem64_offset32_freg64( + buf: &mut Vec<'_, u8>, + dst: AArch64GeneralReg, + offset: i32, + src: AArch64FloatReg, + ) { + if (-256..256).contains(&offset) { + stur_freg64_reg64_imm9(buf, src, dst, offset as i16) + } else if (0..=u16::MAX as i32).contains(&offset) { + debug_assert!(offset % 8 == 0); + str_freg64_reg64_imm12(buf, src, dst, (offset as u16) >> 3); + } else { + let tmp = AArch64GeneralReg::X15; + Self::mov_reg64_imm64(buf, tmp, offset as i64); + Self::add_reg64_reg64_reg64(buf, tmp, tmp, dst); + str_freg64_reg64_imm12(buf, src, tmp, 0); + } + } + + #[inline(always)] + fn movsx_reg_base32( + buf: &mut Vec<'_, u8>, + register_width: RegisterWidth, + dst: AArch64GeneralReg, + offset: i32, + ) { + use RegisterWidth::*; + + // move to destination (zero extends) + Self::mov_reg_base32(buf, register_width, dst, offset); + + // then sign-extend if needed + match register_width { + W8 | W16 | W32 => sign_extend(buf, register_width, dst, dst), + W64 => { /* do nothing */ } + } + } + + #[inline(always)] + fn movzx_reg_base32( + buf: &mut Vec<'_, u8>, + register_width: RegisterWidth, + dst: AArch64GeneralReg, + offset: i32, + ) { + use RegisterWidth::*; + + // move to destination (zero extends) + Self::mov_reg_base32(buf, register_width, dst, offset); + + // then sign-extend if needed + match register_width { + W8 | W16 => zero_extend(buf, register_width, dst, dst), + W32 | W64 => { /* do nothing */ } + } + } + + #[inline(always)] + fn mov_freg64_stack32(buf: &mut Vec<'_, u8>, dst: AArch64FloatReg, offset: i32) { + Self::mov_freg64_mem64_offset32(buf, dst, AArch64GeneralReg::ZRSP, offset) + } + #[inline(always)] + fn mov_reg64_stack32(buf: &mut Vec<'_, u8>, dst: AArch64GeneralReg, offset: i32) { + Self::mov_reg_mem_offset32( + buf, + RegisterWidth::W64, + dst, + AArch64GeneralReg::ZRSP, + offset, + ) + } + #[inline(always)] + fn mov_stack32_freg64(buf: &mut Vec<'_, u8>, offset: i32, src: AArch64FloatReg) { + Self::mov_mem64_offset32_freg64(buf, AArch64GeneralReg::ZRSP, offset, src) + } + + #[inline(always)] + fn mov_stack32_reg( + buf: &mut Vec<'_, u8>, + _register_width: RegisterWidth, + offset: i32, + src: AArch64GeneralReg, + ) { + Self::mov_mem64_offset32_reg64(buf, AArch64GeneralReg::ZRSP, offset, src) + } + #[inline(always)] + fn neg_reg64_reg64(buf: &mut Vec<'_, u8>, dst: AArch64GeneralReg, src: AArch64GeneralReg) { + neg_reg64_reg64(buf, dst, src); + } + + #[inline(always)] + fn sub_reg64_reg64_imm32( + buf: &mut Vec<'_, u8>, + dst: AArch64GeneralReg, + src: AArch64GeneralReg, + imm32: i32, + ) { + if imm32 < 0 { + Self::add_reg64_reg64_imm32(buf, dst, src, -imm32) + } else if imm32 < 0xFFF { + sub_reg64_reg64_imm12(buf, dst, src, imm32 as u16); + } else { + todo!("immediate subtractions with values greater than 12bits"); + } + } + #[inline(always)] + fn sub_reg64_reg64_reg64( + buf: &mut Vec<'_, u8>, + dst: AArch64GeneralReg, + src1: AArch64GeneralReg, + src2: AArch64GeneralReg, + ) { + sub_reg64_reg64_reg64(buf, dst, src1, src2); + } + + #[inline(always)] + fn eq_reg_reg_reg( + buf: &mut Vec<'_, u8>, + _register_width: RegisterWidth, + dst: AArch64GeneralReg, + src1: AArch64GeneralReg, + src2: AArch64GeneralReg, + ) { + cmp_reg64_reg64(buf, src1, src2); + cset_reg64_cond(buf, dst, ConditionCode::EQ); + } + + #[inline(always)] + fn neq_reg_reg_reg( + buf: &mut Vec<'_, u8>, + _register_width: RegisterWidth, + dst: AArch64GeneralReg, + src1: AArch64GeneralReg, + src2: AArch64GeneralReg, + ) { + cmp_reg64_reg64(buf, src1, src2); + cset_reg64_cond(buf, dst, ConditionCode::NE); + } + + fn eq_freg_freg_reg64( + buf: &mut Vec<'_, u8>, + dst: AArch64GeneralReg, + src1: AArch64FloatReg, + src2: AArch64FloatReg, + width: FloatWidth, + ) { + fcmp_freg_freg(buf, width, src1, src2); + cset_reg64_cond(buf, dst, ConditionCode::EQ); + } + + fn neq_freg_freg_reg64( + buf: &mut Vec<'_, u8>, + dst: AArch64GeneralReg, + src1: AArch64FloatReg, + src2: AArch64FloatReg, + width: FloatWidth, + ) { + fcmp_freg_freg(buf, width, src1, src2); + cset_reg64_cond(buf, dst, ConditionCode::NE); + } + + #[inline(always)] + fn cmp_freg_freg_reg64( + buf: &mut Vec<'_, u8>, + dst: AArch64GeneralReg, + src1: AArch64FloatReg, + src2: AArch64FloatReg, + width: FloatWidth, + operation: CompareOperation, + ) { + fcmp_freg_freg(buf, width, src1, src2); + + let cond = match operation { + CompareOperation::LessThan => ConditionCode::MI, + CompareOperation::LessThanOrEqual => ConditionCode::LS, + CompareOperation::GreaterThan => ConditionCode::GT, + CompareOperation::GreaterThanOrEqual => ConditionCode::GE, + }; + cset_reg64_cond(buf, dst, cond); + } + + #[inline(always)] + fn is_nan_freg_reg64( + buf: &mut Vec<'_, u8>, + dst: AArch64GeneralReg, + src: AArch64FloatReg, + width: FloatWidth, + ) { + fcmp_freg_freg(buf, width, src, src); + cset_reg64_cond(buf, dst, ConditionCode::NE); + } + + #[inline(always)] + fn to_float_freg64_reg64(buf: &mut Vec<'_, u8>, dst: AArch64FloatReg, src: AArch64GeneralReg) { + scvtf_freg_reg64(buf, FloatWidth::F64, dst, src); + } + + #[inline(always)] + fn to_float_freg32_reg64(buf: &mut Vec<'_, u8>, dst: AArch64FloatReg, src: AArch64GeneralReg) { + scvtf_freg_reg64(buf, FloatWidth::F32, dst, src); + } + + #[inline(always)] + fn to_float_freg32_freg64(buf: &mut Vec<'_, u8>, dst: AArch64FloatReg, src: AArch64FloatReg) { + fcvt_freg32_freg64(buf, dst, src); + } + + #[inline(always)] + fn to_float_freg64_freg32(buf: &mut Vec<'_, u8>, dst: AArch64FloatReg, src: AArch64FloatReg) { + fcvt_freg64_freg32(buf, dst, src); + } + + #[inline(always)] + fn set_if_overflow(buf: &mut Vec<'_, u8>, dst: AArch64GeneralReg) { + cset_reg64_cond(buf, dst, ConditionCode::VS) + } + + #[inline(always)] + fn ret(buf: &mut Vec<'_, u8>) { + ret_reg64(buf, AArch64GeneralReg::LR) + } + + fn and_reg64_reg64_reg64( + buf: &mut Vec<'_, u8>, + dst: AArch64GeneralReg, + src1: AArch64GeneralReg, + src2: AArch64GeneralReg, + ) { + and_reg64_reg64_reg64(buf, dst, src1, src2); + } + + fn or_reg64_reg64_reg64( + buf: &mut Vec<'_, u8>, + dst: AArch64GeneralReg, + src1: AArch64GeneralReg, + src2: AArch64GeneralReg, + ) { + orr_reg64_reg64_reg64(buf, dst, src1, src2); + } + + fn xor_reg64_reg64_reg64( + buf: &mut Vec<'_, u8>, + dst: AArch64GeneralReg, + src1: AArch64GeneralReg, + src2: AArch64GeneralReg, + ) { + eor_reg64_reg64_reg64(buf, dst, src1, src2); + } + + fn shl_reg64_reg64_reg64<'a, ASM, CC>( + buf: &mut Vec<'a, u8>, + _storage_manager: &mut StorageManager<'a, '_, AArch64GeneralReg, AArch64FloatReg, ASM, CC>, + dst: AArch64GeneralReg, + src1: AArch64GeneralReg, + src2: AArch64GeneralReg, + ) where + ASM: Assembler, + CC: CallConv, + { + lsl_reg64_reg64_reg64(buf, dst, src1, src2); + } + + fn shr_reg64_reg64_reg64<'a, ASM, CC>( + buf: &mut Vec<'a, u8>, + _storage_manager: &mut StorageManager<'a, '_, AArch64GeneralReg, AArch64FloatReg, ASM, CC>, + dst: AArch64GeneralReg, + src1: AArch64GeneralReg, + src2: AArch64GeneralReg, + ) where + ASM: Assembler, + CC: CallConv, + { + lsr_reg64_reg64_reg64(buf, dst, src1, src2); + } + + fn sar_reg64_reg64_reg64<'a, ASM, CC>( + buf: &mut Vec<'a, u8>, + _storage_manager: &mut StorageManager<'a, '_, AArch64GeneralReg, AArch64FloatReg, ASM, CC>, + dst: AArch64GeneralReg, + src1: AArch64GeneralReg, + src2: AArch64GeneralReg, + ) where + ASM: Assembler, + CC: CallConv, + { + asr_reg64_reg64_reg64(buf, dst, src1, src2); + } + + fn sqrt_freg64_freg64(buf: &mut Vec<'_, u8>, dst: AArch64FloatReg, src: AArch64FloatReg) { + fsqrt_freg_freg(buf, FloatWidth::F64, dst, src); + } + + fn sqrt_freg32_freg32(buf: &mut Vec<'_, u8>, dst: AArch64FloatReg, src: AArch64FloatReg) { + fsqrt_freg_freg(buf, FloatWidth::F32, dst, src); + } + + fn signed_compare_reg64( + buf: &mut Vec<'_, u8>, + _register_width: RegisterWidth, + operation: CompareOperation, + dst: AArch64GeneralReg, + src1: AArch64GeneralReg, + src2: AArch64GeneralReg, + ) { + cmp_reg64_reg64(buf, src1, src2); + let cond = match operation { + CompareOperation::LessThan => ConditionCode::LT, + CompareOperation::LessThanOrEqual => ConditionCode::LE, + CompareOperation::GreaterThan => ConditionCode::GT, + CompareOperation::GreaterThanOrEqual => ConditionCode::GE, + }; + cset_reg64_cond(buf, dst, cond); + } + + fn unsigned_compare_reg64( + buf: &mut Vec<'_, u8>, + _register_width: RegisterWidth, + operation: CompareOperation, + dst: AArch64GeneralReg, + src1: AArch64GeneralReg, + src2: AArch64GeneralReg, + ) { + cmp_reg64_reg64(buf, src1, src2); + let cond = match operation { + CompareOperation::LessThan => ConditionCode::CCLO, + CompareOperation::LessThanOrEqual => ConditionCode::LS, + CompareOperation::GreaterThan => ConditionCode::HI, + CompareOperation::GreaterThanOrEqual => ConditionCode::CSHS, + }; + cset_reg64_cond(buf, dst, cond); + } + + fn mov_freg64_mem64_offset32( + buf: &mut Vec<'_, u8>, + dst: AArch64FloatReg, + src: AArch64GeneralReg, + offset: i32, + ) { + if (-256..256).contains(&offset) { + ldur_freg64_reg64_imm9(buf, dst, src, offset as i16) + } else if (0..=u16::MAX as i32).contains(&offset) { + debug_assert!(offset % 8 == 0); + ldr_freg64_reg64_imm12(buf, dst, src, (offset as u16) >> 3); + } else { + let tmp = AArch64GeneralReg::X15; + Self::mov_reg64_imm64(buf, tmp, offset as i64); + Self::add_reg64_reg64_reg64(buf, tmp, tmp, src); + ldr_freg64_reg64_imm12(buf, dst, tmp, 0); + } + } + + fn mov_freg32_mem32_offset32( + buf: &mut Vec<'_, u8>, + dst: AArch64FloatReg, + src: AArch64GeneralReg, + offset: i32, + ) { + if offset < 0 { + ldur_freg64_reg64_imm9(buf, dst, src, offset as i16) + } else if (0..=u16::MAX as i32).contains(&offset) { + debug_assert!(offset % 8 == 0); + ldr_freg64_reg64_imm12(buf, dst, src, (offset as u16) >> 3); + } else { + todo!("base offsets over 32k for AArch64"); + } + } +} + +impl AArch64Assembler {} + +// Instructions +// ARM manual section C3 +// https://developer.arm.com/documentation/ddi0487/ga +// Map all instructions to a packed struct. + +trait Aarch64Bytes: PackedStruct { + #[inline(always)] + fn bytes(&self) -> [u8; 4] { + let mut bytes: [u8; 4] = [0, 0, 0, 0]; + + self.pack_to_slice(&mut bytes).unwrap(); + + bytes.reverse(); + + bytes + } +} + +#[derive(PackedStruct, Debug)] +#[packed_struct(endian = "msb")] +pub struct MoveWideImmediate { + sf: bool, + opc: Integer>, + fixed: Integer>, // = 0b100101, + hw: Integer>, + imm16: u16, + reg_d: Integer>, // AArch64GeneralReg +} + +impl Aarch64Bytes for MoveWideImmediate {} + +pub struct MoveWideImmediateParams { + opc: u8, + rd: AArch64GeneralReg, + imm16: u16, + hw: u8, + sf: bool, +} + +impl MoveWideImmediate { + #[inline(always)] + fn new( + MoveWideImmediateParams { + opc, + rd, + imm16, + hw, + sf, + }: MoveWideImmediateParams, + ) -> Self { + // TODO: revisit this is we change where we want to check the shift + // currently this is done in the assembler above + // assert!(shift % 16 == 0 && shift <= 48); + debug_assert!(hw <= 0b11); + debug_assert!(opc <= 0b11); + + Self { + reg_d: rd.id().into(), + imm16, + hw: hw.into(), + opc: opc.into(), + sf, + fixed: 0b100101.into(), + } + } +} + +#[derive(PackedStruct, Debug)] +#[packed_struct(endian = "msb")] +pub struct ArithmeticImmediate { + sf: bool, + op: bool, // add or subtract + s: bool, + fixed: Integer>, // = 0b100010, + sh: bool, // shift + imm12: Integer>, + reg_n: Integer>, + reg_d: Integer>, +} + +impl Aarch64Bytes for ArithmeticImmediate {} + +pub struct ArithmeticImmediateParams { + op: bool, + s: bool, + rd: AArch64GeneralReg, + rn: AArch64GeneralReg, + imm12: u16, + sh: bool, +} + +impl ArithmeticImmediate { + #[inline(always)] + fn new( + ArithmeticImmediateParams { + op, + s, + rd, + rn, + imm12, + sh, + }: ArithmeticImmediateParams, + ) -> Self { + debug_assert!(imm12 <= 0xFFF); + + Self { + reg_d: rd.id().into(), + reg_n: rn.id().into(), + imm12: imm12.into(), + sh, + s, + op, + // true for 64 bit addition + // false for 32 bit addition + sf: true, + fixed: 0b100010.into(), + } + } +} + +#[derive(Clone, Copy)] +#[allow(dead_code)] +enum ShiftType { + LSL = 0, + LSR = 1, + ASR = 2, + ROR = 3, +} + +impl ShiftType { + #[inline(always)] + fn id(&self) -> u8 { + *self as u8 + } +} + +#[derive(PackedStruct)] +#[packed_struct(endian = "msb")] +pub struct ArithmeticShifted { + sf: bool, + op: bool, // add or subtract + s: bool, + fixed: Integer>, // = 0b01011, + shift: Integer>, // shift + fixed2: bool, // = 0b0, + reg_m: Integer>, + imm6: Integer>, + reg_n: Integer>, + reg_d: Integer>, +} + +impl Aarch64Bytes for ArithmeticShifted {} + +pub struct ArithmeticShiftedParams { + op: bool, + s: bool, + shift: ShiftType, + imm6: u8, + rm: AArch64GeneralReg, + rn: AArch64GeneralReg, + rd: AArch64GeneralReg, +} + +impl ArithmeticShifted { + #[inline(always)] + fn new( + ArithmeticShiftedParams { + op, + s, + shift, + imm6, + rm, + rn, + rd, + }: ArithmeticShiftedParams, + ) -> Self { + debug_assert!(imm6 <= 0b111111); + + Self { + // true for 64 bit addition + // false for 32 bit addition + sf: true, + fixed: 0b01011.into(), + op, + s, + fixed2: false, + reg_m: rm.id().into(), + shift: shift.id().into(), + imm6: imm6.into(), + reg_d: rd.id().into(), + reg_n: rn.id().into(), + } + } +} + +// ARM manual section C1.2.4 +#[derive(Copy, Clone, PartialEq)] +#[allow(dead_code)] +enum ConditionCode { + /// Equal + EQ = 0b0000, + /// Not equal + NE = 0b0001, + /// CS or HS: Carry set + CSHS = 0b0010, + /// CC or LO: Carry clear + CCLO = 0b0011, + /// Minus, negative + MI = 0b0100, + /// Plus, positive or zero + PL = 0b0101, + /// Overflow + VS = 0b0110, + /// No overflow + VC = 0b0111, + /// Unsigned higher + HI = 0b1000, + /// Unsigned lower or same + LS = 0b1001, + /// Signed greater than or equal + GE = 0b1010, + /// Signed less than + LT = 0b1011, + /// Signed greater than + GT = 0b1100, + /// Signed less than or equal + LE = 0b1101, + /// Always + AL = 0b1110, +} + +impl ConditionCode { + #[inline(always)] + fn id(&self) -> u8 { + *self as u8 + } + + /// The inverse of the condition code. For example, EQ becomes NE. + fn invert(self) -> Self { + match self { + ConditionCode::EQ => ConditionCode::NE, + ConditionCode::NE => ConditionCode::EQ, + ConditionCode::CSHS => ConditionCode::CCLO, + ConditionCode::CCLO => ConditionCode::CSHS, + ConditionCode::MI => ConditionCode::PL, + ConditionCode::PL => ConditionCode::MI, + ConditionCode::VS => ConditionCode::VC, + ConditionCode::VC => ConditionCode::VS, + ConditionCode::HI => ConditionCode::LS, + ConditionCode::LS => ConditionCode::HI, + ConditionCode::GE => ConditionCode::LT, + ConditionCode::LT => ConditionCode::GE, + ConditionCode::GT => ConditionCode::LE, + ConditionCode::LE => ConditionCode::GT, + ConditionCode::AL => ConditionCode::AL, + } + } +} + +impl std::fmt::Display for ConditionCode { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match self { + ConditionCode::EQ => "eq", + ConditionCode::NE => "ne", + ConditionCode::CSHS => "hs", + ConditionCode::CCLO => "lo", + ConditionCode::MI => "mi", + ConditionCode::PL => "pl", + ConditionCode::VS => "vs", + ConditionCode::VC => "vc", + ConditionCode::HI => "hi", + ConditionCode::LS => "ls", + ConditionCode::GE => "ge", + ConditionCode::LT => "lt", + ConditionCode::GT => "gt", + ConditionCode::LE => "le", + ConditionCode::AL => "al", + } + ) + } +} + +#[derive(PackedStruct)] +#[packed_struct(endian = "msb")] +pub struct ConditionalBranchImmediate { + fixed: Integer>, + o1: bool, + imm19: Integer>, + o0: bool, + cond: Integer>, +} + +impl Aarch64Bytes for ConditionalBranchImmediate {} + +pub struct ConditionalBranchImmediateParams { + cond: ConditionCode, + imm19: u32, +} + +impl ConditionalBranchImmediate { + #[inline(always)] + fn new( + ConditionalBranchImmediateParams { cond, imm19 }: ConditionalBranchImmediateParams, + ) -> Self { + debug_assert!(imm19 >> 19 == 0); + + Self { + cond: cond.id().into(), + o0: false, + imm19: imm19.into(), + o1: false, + fixed: 0b0101010.into(), + } + } +} + +#[derive(PackedStruct)] +#[packed_struct(endian = "msb")] +pub struct ConditionalSelect { + sf: bool, + op: bool, + s: bool, + fixed: Integer>, + reg_m: Integer>, + cond: Integer>, + op2: Integer>, + reg_n: Integer>, + reg_d: Integer>, +} + +impl Aarch64Bytes for ConditionalSelect {} + +pub struct ConditionalSelectParams { + op: bool, + s: bool, + cond: ConditionCode, + op2: u8, + rm: AArch64GeneralReg, + rn: AArch64GeneralReg, + rd: AArch64GeneralReg, +} + +impl ConditionalSelect { + #[inline(always)] + fn new( + ConditionalSelectParams { + op, + s, + cond, + op2, + rm, + rn, + rd, + }: ConditionalSelectParams, + ) -> Self { + debug_assert!(op2 <= 0b11); + + Self { + reg_d: rd.id().into(), + reg_n: rn.id().into(), + op2: op2.into(), + cond: cond.id().into(), + reg_m: rm.id().into(), + fixed: 0b11010100.into(), + s, + op, + // true for 64 bit addition + // false for 32 bit addition + sf: true, + } + } +} + +#[derive(PackedStruct)] +#[packed_struct(endian = "msb")] +pub struct DataProcessingTwoSource { + sf: bool, + fixed: bool, + s: bool, + fixed2: Integer>, + reg_m: Integer>, + op: Integer>, + reg_n: Integer>, + reg_d: Integer>, +} + +impl Aarch64Bytes for DataProcessingTwoSource {} + +pub struct DataProcessingTwoSourceParams { + op: u8, + rm: AArch64GeneralReg, + rn: AArch64GeneralReg, + rd: AArch64GeneralReg, +} + +impl DataProcessingTwoSource { + #[inline(always)] + fn new( + DataProcessingTwoSourceParams { op, rm, rn, rd }: DataProcessingTwoSourceParams, + ) -> Self { + debug_assert!(op <= 0b111111); + + Self { + sf: true, + fixed: false, + s: false, + fixed2: 0b11010110.into(), + reg_m: rm.id().into(), + op: op.into(), + reg_n: rn.id().into(), + reg_d: rd.id().into(), + } + } +} + +#[derive(PackedStruct)] +#[packed_struct(endian = "msb")] +pub struct DataProcessingThreeSource { + sf: bool, + op54: Integer>, + fixed: Integer>, + op31: Integer>, + rm: Integer>, + o0: bool, + ra: Integer>, + rn: Integer>, + rd: Integer>, +} + +impl Aarch64Bytes for DataProcessingThreeSource {} + +pub struct DataProcessingThreeSourceParams { + op31: u8, + rm: AArch64GeneralReg, + ra: AArch64GeneralReg, + rn: AArch64GeneralReg, + rd: AArch64GeneralReg, +} + +impl DataProcessingThreeSource { + #[inline(always)] + fn new( + DataProcessingThreeSourceParams { + op31, + rm, + ra, + rn, + rd, + }: DataProcessingThreeSourceParams, + ) -> Self { + debug_assert!(op31 <= 0b111); + + Self { + sf: true, + op54: 0b00.into(), + fixed: 0b011011.into(), + op31: op31.into(), + rm: rm.id().into(), + o0: false, + ra: ra.id().into(), + rn: rn.id().into(), + rd: rd.id().into(), + } + } +} + +#[derive(Debug)] +#[allow(dead_code)] +enum LogicalOp { + AND, + BIC, + ORR, + ORN, + EOR, + EON, + ANDS, + BICS, +} + +#[derive(PackedStruct)] +#[packed_struct(endian = "msb")] +pub struct LogicalShiftedRegister { + sf: bool, + op: Integer>, + fixed: Integer>, // = 0b01010, + shift: Integer>, // shift + n: bool, + reg_m: Integer>, + imm6: Integer>, + reg_n: Integer>, + reg_d: Integer>, +} + +impl Aarch64Bytes for LogicalShiftedRegister {} + +pub struct LogicalShiftedRegisterParams { + op: LogicalOp, + shift: ShiftType, + imm6: u8, + rm: AArch64GeneralReg, + rn: AArch64GeneralReg, + rd: AArch64GeneralReg, +} + +impl LogicalShiftedRegister { + #[inline(always)] + fn new( + LogicalShiftedRegisterParams { + op, + shift, + imm6, + rm, + rn, + rd, + }: LogicalShiftedRegisterParams, + ) -> Self { + debug_assert!(imm6 <= 0b111111); + + let (op, n) = match op { + LogicalOp::AND => (0b00, false), + LogicalOp::BIC => (0b00, true), + LogicalOp::ORR => (0b01, false), + LogicalOp::ORN => (0b01, true), + LogicalOp::EOR => (0b10, false), + LogicalOp::EON => (0b10, true), + LogicalOp::ANDS => (0b11, false), + LogicalOp::BICS => (0b11, true), + }; + + Self { + reg_d: rd.id().into(), + reg_n: rn.id().into(), + imm6: imm6.into(), + reg_m: rm.id().into(), + n, + shift: shift.id().into(), + fixed: 0b01010.into(), + op: op.into(), + // true for 64 bit addition + // false for 32 bit addition + sf: true, + } + } +} + +#[derive(PackedStruct)] +pub struct UnconditionalBranchRegister { + fixed: Integer>, + z: bool, + fixed2: bool, + op: Integer>, + fixed3: Integer>, + fixed4: Integer>, + a: bool, + m: bool, + rn: Integer>, + fixed5: Integer>, +} + +impl Aarch64Bytes for UnconditionalBranchRegister {} + +pub struct UnconditionalBranchRegisterParams { + op: u8, + rn: AArch64GeneralReg, +} + +impl UnconditionalBranchRegister { + #[inline(always)] + fn new( + UnconditionalBranchRegisterParams { op, rn }: UnconditionalBranchRegisterParams, + ) -> Self { + debug_assert!(op <= 0b11); + + Self { + fixed5: 0b00000.into(), + rn: rn.id().into(), + m: false, + a: false, + fixed4: 0b0000.into(), + fixed3: 0b11111.into(), + op: op.into(), + fixed2: false, + z: false, + fixed: 0b1101011.into(), + } + } +} + +#[derive(PackedStruct)] +#[packed_struct(endian = "msb")] +pub struct UnconditionalBranchImmediate { + op: bool, // false=B, true=BL + fixed: Integer>, + imm26: Integer>, +} + +impl Aarch64Bytes for UnconditionalBranchImmediate {} + +pub struct UnconditionalBranchImmediateParams { + op: bool, + imm26: u32, +} + +impl UnconditionalBranchImmediate { + #[inline(always)] + fn new( + UnconditionalBranchImmediateParams { op, imm26 }: UnconditionalBranchImmediateParams, + ) -> Self { + debug_assert!(imm26 <= 0b11_1111_1111_1111_1111_1111_1111); + Self { + op, + fixed: 0b00101.into(), + imm26: imm26.into(), + } + } +} + +#[derive(PackedStruct, Debug)] +#[packed_struct(endian = "msb")] +pub struct SignExtend { + sf: Integer>, + opc: Integer>, + fixed: Integer>, + n: Integer>, + immr: Integer>, + imms: Integer>, + rn: Integer>, + rd: Integer>, +} + +impl Aarch64Bytes for SignExtend {} + +fn sign_extend( + buf: &mut Vec<'_, u8>, + register_width: RegisterWidth, + dst: AArch64GeneralReg, + src: AArch64GeneralReg, +) { + let imms = match register_width { + RegisterWidth::W8 => 0b00_0111, // sxtb + RegisterWidth::W16 => 0b00_1111, // sxth + RegisterWidth::W32 => 0b01_1111, // sxtw + RegisterWidth::W64 => return mov_reg64_reg64(buf, dst, src), + }; + + let inst = SignExtend { + sf: 0b1.into(), + opc: 0b00.into(), + fixed: 0b100110.into(), + n: 0b1.into(), + immr: 0b00_0000.into(), + imms: imms.into(), + rn: src.id().into(), + rd: dst.id().into(), + }; + + buf.extend(inst.bytes()); +} + +fn zero_extend( + buf: &mut Vec<'_, u8>, + register_width: RegisterWidth, + dst: AArch64GeneralReg, + src: AArch64GeneralReg, +) { + let imms = match register_width { + RegisterWidth::W8 => 0b00_0111, // uxtb + RegisterWidth::W16 => 0b00_1111, // uxth + RegisterWidth::W32 => return mov_reg64_reg64(buf, dst, src), + RegisterWidth::W64 => return mov_reg64_reg64(buf, dst, src), + }; + + let inst = SignExtend { + sf: 0b0.into(), + opc: 0b10.into(), + fixed: 0b100110.into(), + n: 0b0.into(), + immr: 0b00_0000.into(), + imms: imms.into(), + rn: src.id().into(), + rd: dst.id().into(), + }; + + buf.extend(inst.bytes()); +} + +// Uses unsigned Offset +// opc = 0b01 means load +// opc = 0b00 means store +#[derive(PackedStruct, Debug)] +#[packed_struct(endian = "msb")] +pub struct LoadStoreRegisterImmediate { + size: Integer>, + fixed: Integer>, // = 0b111, + fixed2: bool, + fixed3: Integer>, + opc: Integer>, + imm12: Integer>, + rn: Integer>, + rt: Integer>, +} + +impl Aarch64Bytes for LoadStoreRegisterImmediate {} + +pub struct LoadStoreRegisterImmediateParams { + size: u8, + imm12: u16, + rn: AArch64GeneralReg, + rt: AArch64GeneralReg, +} + +impl LoadStoreRegisterImmediate { + #[inline(always)] + fn new( + opc: u8, + LoadStoreRegisterImmediateParams { + size, + imm12, + rn, + rt, + }: LoadStoreRegisterImmediateParams, + ) -> Self { + debug_assert!(size <= 0b11); + debug_assert!(imm12 <= 0xFFF); + + Self { + rt: rt.id().into(), + rn: rn.id().into(), + imm12: imm12.into(), + opc: opc.into(), + fixed3: 0b01.into(), + fixed2: false, + fixed: 0b111.into(), + size: size.into(), + } + } + + #[inline(always)] + fn new_load(params: LoadStoreRegisterImmediateParams) -> Self { + Self::new(0b01, params) + } + + #[inline(always)] + fn new_store(params: LoadStoreRegisterImmediateParams) -> Self { + Self::new(0b00, params) + } +} + +#[derive(PackedStruct)] +#[packed_struct(endian = "msb")] +pub struct AdvancedSimdModifiedImmediate { + fixed: bool, + q: bool, + op: bool, + fixed2: Integer>, + a: bool, + b: bool, + c: bool, + cmode: Integer>, + o2: bool, + fixed3: bool, + d: bool, + e: bool, + f: bool, + g: bool, + h: bool, + rd: Integer>, +} + +impl Aarch64Bytes for AdvancedSimdModifiedImmediate {} + +impl AdvancedSimdModifiedImmediate { + #[inline(always)] + fn new(rd: AArch64FloatReg) -> Self { + Self { + fixed: false, + q: false, + op: true, + fixed2: 0b0111100000.into(), + a: false, + b: false, + c: false, + cmode: 0b1110.into(), + o2: false, + fixed3: true, + d: false, + e: false, + f: false, + g: false, + h: false, + rd: rd.id().into(), + } + } +} + +fn encode_float_width(width: FloatWidth) -> u8 { + match width { + FloatWidth::F32 => 0b00, + FloatWidth::F64 => 0b01, + } +} + +#[derive(PackedStruct)] +#[packed_struct(endian = "msb")] +pub struct ConversionBetweenFloatingPointAndInteger { + sf: bool, + fixed: bool, + s: bool, + fixed2: Integer>, + ptype: Integer>, + fixed3: bool, + rmode: Integer>, + opcode: Integer>, + fixed4: Integer>, + rn: Integer>, + rd: Integer>, +} + +impl Aarch64Bytes for ConversionBetweenFloatingPointAndInteger {} + +pub struct ConversionBetweenFloatingPointAndIntegerParams { + ptype: FloatWidth, + rmode: u8, + opcode: u8, + rn: AArch64GeneralReg, + rd: AArch64FloatReg, +} + +impl ConversionBetweenFloatingPointAndInteger { + #[inline(always)] + fn new( + ConversionBetweenFloatingPointAndIntegerParams { + ptype, + rmode, + opcode, + rn, + rd, + }: ConversionBetweenFloatingPointAndIntegerParams, + ) -> Self { + debug_assert!(rmode <= 0b11); + debug_assert!(opcode <= 0b111); + + Self { + sf: true, + fixed: false, + s: false, + fixed2: 0b11110.into(), + ptype: encode_float_width(ptype).into(), + fixed3: true, + rmode: rmode.into(), + opcode: opcode.into(), + fixed4: 0b000000.into(), + rn: rn.id().into(), + rd: rd.id().into(), + } + } +} + +#[derive(PackedStruct)] +#[packed_struct(endian = "msb")] +pub struct FloatingPointDataProcessingOneSource { + m: bool, + fixed: bool, + s: bool, + fixed2: Integer>, + ptype: Integer>, + fixed3: bool, + opcode: Integer>, + fixed4: Integer>, + rn: Integer>, + rd: Integer>, +} + +impl Aarch64Bytes for FloatingPointDataProcessingOneSource {} + +pub struct FloatingPointDataProcessingOneSourceParams { + ptype: FloatWidth, + opcode: u8, + rn: AArch64FloatReg, + rd: AArch64FloatReg, +} + +impl FloatingPointDataProcessingOneSource { + #[inline(always)] + fn new( + FloatingPointDataProcessingOneSourceParams { + ptype, + opcode, + rn, + rd, + }: FloatingPointDataProcessingOneSourceParams, + ) -> Self { + debug_assert!(opcode <= 0b111111); + + Self { + m: false, + fixed: false, + s: false, + fixed2: 0b11110.into(), + ptype: encode_float_width(ptype).into(), + fixed3: true, + opcode: opcode.into(), + fixed4: 0b10000.into(), + rn: rn.id().into(), + rd: rd.id().into(), + } + } +} + +#[derive(PackedStruct)] +#[packed_struct(endian = "msb")] +pub struct FloatingPointCompare { + m: bool, + fixed: bool, + s: bool, + fixed2: Integer>, + ptype: Integer>, + fixed3: bool, + rm: Integer>, + op: Integer>, + fixed4: Integer>, + rn: Integer>, + opcode2: Integer>, +} + +impl Aarch64Bytes for FloatingPointCompare {} + +pub struct FloatingPointCompareParams { + ptype: FloatWidth, + rm: AArch64FloatReg, + rn: AArch64FloatReg, + opcode2: u8, +} + +impl FloatingPointCompare { + #[inline(always)] + fn new( + FloatingPointCompareParams { + ptype, + rm, + rn, + opcode2, + }: FloatingPointCompareParams, + ) -> Self { + debug_assert!(opcode2 <= 0b11111); + + Self { + m: false, + fixed: false, + s: false, + fixed2: 0b11110.into(), + ptype: encode_float_width(ptype).into(), + fixed3: true, + rm: rm.id().into(), + op: 0b00.into(), + fixed4: 0b1000.into(), + rn: rn.id().into(), + opcode2: opcode2.into(), + } + } +} + +#[derive(PackedStruct)] +#[packed_struct(endian = "msb")] +pub struct FloatingPointDataProcessingTwoSource { + m: bool, + fixed: bool, + s: bool, + fixed2: Integer>, + ptype: Integer>, + fixed3: bool, + rm: Integer>, + opcode: Integer>, + fixed4: Integer>, + rn: Integer>, + rd: Integer>, +} + +impl Aarch64Bytes for FloatingPointDataProcessingTwoSource {} + +pub struct FloatingPointDataProcessingTwoSourceParams { + ptype: FloatWidth, + rm: AArch64FloatReg, + opcode: u8, + rn: AArch64FloatReg, + rd: AArch64FloatReg, +} + +impl FloatingPointDataProcessingTwoSource { + #[inline(always)] + fn new( + FloatingPointDataProcessingTwoSourceParams { + ptype, + rm, + opcode, + rn, + rd, + }: FloatingPointDataProcessingTwoSourceParams, + ) -> Self { + debug_assert!(opcode <= 0b1111); + + Self { + m: false, + fixed: false, + s: false, + fixed2: 0b11110.into(), + ptype: encode_float_width(ptype).into(), + fixed3: true, + rm: rm.id().into(), + opcode: opcode.into(), + fixed4: 0b10.into(), + rn: rn.id().into(), + rd: rd.id().into(), + } + } +} + +#[derive(PackedStruct)] +#[packed_struct(endian = "msb")] +pub struct FloatingPointImmediate { + m: bool, + fixed: bool, + s: bool, + fixed2: Integer>, + ptype: Integer>, + fixed3: bool, + imm8: u8, + fixed4: Integer>, + imm5: Integer>, + rd: Integer>, +} + +impl Aarch64Bytes for FloatingPointImmediate {} + +pub struct FloatingPointImmediateParams { + ptype: FloatWidth, + imm8: u8, + rd: AArch64FloatReg, +} + +impl FloatingPointImmediate { + #[inline(always)] + fn new(FloatingPointImmediateParams { ptype, imm8, rd }: FloatingPointImmediateParams) -> Self { + Self { + m: false, + fixed: false, + s: false, + fixed2: 0b11110.into(), + ptype: encode_float_width(ptype).into(), + fixed3: true, + imm8, + fixed4: 0b100.into(), + imm5: 0b00000.into(), + rd: rd.id().into(), + } + } +} + +// Below here are the functions for all of the base assembly instructions. +// Their names are based on the instruction and operators combined. +// You should call `buf.reserve()` if you push or extend more than once. +// Unit tests are added at the bottom of the file to ensure correct asm generation. +// Please keep these in alphanumeric order. +// Floating-point (and advanced SIMD) instructions are at the bottom. + +// ARM manual section C6 + +/// `ADD Xd, Xn, imm12` -> Add Xn and imm12 and place the result into Xd. +#[inline(always)] +fn add_reg64_reg64_imm12( + buf: &mut Vec<'_, u8>, + dst: AArch64GeneralReg, + src: AArch64GeneralReg, + imm12: u16, +) { + let inst = ArithmeticImmediate::new(ArithmeticImmediateParams { + op: false, + s: false, + sh: false, + imm12, + rd: dst, + rn: src, + }); + + buf.extend(inst.bytes()); +} + +/// `ADD Xd, Xm, Xn` -> Add Xm and Xn and place the result into Xd. +#[inline(always)] +fn add_reg64_reg64_reg64( + buf: &mut Vec<'_, u8>, + dst: AArch64GeneralReg, + src1: AArch64GeneralReg, + src2: AArch64GeneralReg, +) { + let inst = ArithmeticShifted::new(ArithmeticShiftedParams { + op: false, + s: false, + shift: ShiftType::LSL, + imm6: 0, + rm: src2, + rn: src1, + rd: dst, + }); + + buf.extend(inst.bytes()); +} + +/// `AND Xd, Xn, Xm` -> Bitwise AND Xn and Xm and place the result into Xd. +#[inline(always)] +fn and_reg64_reg64_reg64( + buf: &mut Vec<'_, u8>, + dst: AArch64GeneralReg, + src1: AArch64GeneralReg, + src2: AArch64GeneralReg, +) { + let inst = LogicalShiftedRegister::new(LogicalShiftedRegisterParams { + op: LogicalOp::AND, + shift: ShiftType::LSL, + imm6: 0, + rm: src2, + rn: src1, + rd: dst, + }); + + buf.extend(inst.bytes()); +} + +/// `ASR Xd, Xn, Xn` -> Arithmetic shift right Xn by Xm and place the result into Xd. +#[inline(always)] +fn asr_reg64_reg64_reg64( + buf: &mut Vec<'_, u8>, + dst: AArch64GeneralReg, + src1: AArch64GeneralReg, + src2: AArch64GeneralReg, +) { + let inst = DataProcessingTwoSource::new(DataProcessingTwoSourceParams { + op: 0b001010, + rm: src2, + rn: src1, + rd: dst, + }); + + buf.extend(inst.bytes()); +} + +/// `B.cond imm19` -> Jump to PC + imm19 if cond is met. +#[inline(always)] +fn b_cond_imm19(buf: &mut Vec<'_, u8>, cond: ConditionCode, imm19: i32) { + // Since instructions are 4 bytes, the branch instructions assume the last 2 bits are 0 + debug_assert!(imm19 & 0b11 == 0, "branch location must be 4-byte aligned"); + let shifted = imm19 >> 2; + let unsigned = shifted as u32; + // Our offset is only 19 bits, so we need to remove the first 13 bits + let left_removed = (unsigned << 13) >> 13; + // Check that imm19 wasn't too big + if imm19 >= 0 { + // Removing the first 13 bits should not have changed the value + debug_assert!(left_removed == unsigned); + } else { + // If imm19 was negative, left_removed will be sign-extended by the instruction + debug_assert!(left_removed | 0b1111_1111_1111_1100_0000_0000_0000_0000 == unsigned); + } + + let inst = ConditionalBranchImmediate::new(ConditionalBranchImmediateParams { + cond, + imm19: left_removed, + }); + + buf.extend(inst.bytes()); +} + +/// `B imm26` -> Jump to PC + imm26. +#[inline(always)] +fn b_imm26(buf: &mut Vec<'_, u8>, imm26: i32) { + // Since instructions are 4 bytes, the branch instructions assume the last 2 bits are 0 + debug_assert!(imm26 & 0b11 == 0, "branch location must be 4-byte aligned"); + let shifted = imm26 >> 2; + let unsigned = shifted as u32; + // Our offset is only 26 bits, so we need to remove the first 6 bits + let left_removed = (unsigned << 6) >> 6; + // Check that imm26 wasn't too big + if imm26 >= 0 { + // Removing the first 6 bits should not have changed the value + debug_assert!(left_removed == unsigned); + } else { + // If imm26 was negative, left_removed will be sign-extended by the instruction + debug_assert!(left_removed | 0b1111_1110_0000_0000_0000_0000_0000_0000 == unsigned); + } + + let inst = UnconditionalBranchImmediate::new(UnconditionalBranchImmediateParams { + op: false, + imm26: left_removed, + }); + + buf.extend(inst.bytes()); +} + +/// `CMP Xn, imm12` -> Compare Xn and imm12, setting condition flags. +#[inline(always)] +fn cmp_reg64_imm12(buf: &mut Vec<'_, u8>, src: AArch64GeneralReg, imm12: u16) { + subs_reg64_reg64_imm12(buf, AArch64GeneralReg::ZRSP, src, imm12); +} + +/// `CMP Xn, Xm` -> Compare Xn and Xm, setting condition flags. +#[inline(always)] +fn cmp_reg64_reg64(buf: &mut Vec<'_, u8>, src1: AArch64GeneralReg, src2: AArch64GeneralReg) { + subs_reg64_reg64_reg64(buf, AArch64GeneralReg::ZRSP, src1, src2); +} + +/// `CNEG Xd, Xn, cond` -> If cond is true, then Xd = -Xn, else Xd = Xn. +#[inline(always)] +fn cneg_reg64_reg64_cond( + buf: &mut Vec<'_, u8>, + dst: AArch64GeneralReg, + src: AArch64GeneralReg, + cond: ConditionCode, +) { + csneg_reg64_reg64_reg64_cond(buf, dst, src, src, cond.invert()); +} + +/// `CSET Xd, cond` -> If cond is true, then Xd = 1, else Xd = 0. +#[inline(always)] +fn cset_reg64_cond(buf: &mut Vec<'_, u8>, dst: AArch64GeneralReg, cond: ConditionCode) { + csinc_reg64_reg64_reg64_cond( + buf, + dst, + AArch64GeneralReg::ZRSP, + AArch64GeneralReg::ZRSP, + cond.invert(), + ); +} + +/// `CSINC Xd, Xn, Xm, cond` -> If cond is true, then Xd = Xn, else Xd = Xm + 1. +#[inline(always)] +fn csinc_reg64_reg64_reg64_cond( + buf: &mut Vec<'_, u8>, + dst: AArch64GeneralReg, + src1: AArch64GeneralReg, + src2: AArch64GeneralReg, + cond: ConditionCode, +) { + let inst = ConditionalSelect::new(ConditionalSelectParams { + op: false, + s: false, + cond, + op2: 0b01, + rm: src2, + rn: src1, + rd: dst, + }); + + buf.extend(inst.bytes()); +} + +/// `CSNEG Xd, Xn, Xm, cond` -> If cond is true, then Xd = Xn, else Xd = -Xm. +#[inline(always)] +fn csneg_reg64_reg64_reg64_cond( + buf: &mut Vec<'_, u8>, + dst: AArch64GeneralReg, + src1: AArch64GeneralReg, + src2: AArch64GeneralReg, + cond: ConditionCode, +) { + let inst = ConditionalSelect::new(ConditionalSelectParams { + op: true, + s: false, + cond, + op2: 0b01, + rm: src2, + rn: src1, + rd: dst, + }); + + buf.extend(inst.bytes()); +} + +/// `EOR Xd, Xn, Xm` -> Bitwise XOR Xn and Xm and place the result into Xd. +#[inline(always)] +fn eor_reg64_reg64_reg64( + buf: &mut Vec<'_, u8>, + dst: AArch64GeneralReg, + src1: AArch64GeneralReg, + src2: AArch64GeneralReg, +) { + let inst = LogicalShiftedRegister::new(LogicalShiftedRegisterParams { + op: LogicalOp::EOR, + shift: ShiftType::LSL, + imm6: 0, + rm: src2, + rn: src1, + rd: dst, + }); + + buf.extend(inst.bytes()); +} + +/// `LDR Xt, [Xn, #offset]` -> Load Xn + Offset Xt. ZRSP is SP. +/// Note: imm12 is the offest divided by 8. +#[inline(always)] +fn ldr_reg_reg_imm12( + buf: &mut Vec<'_, u8>, + register_width: RegisterWidth, + dst: AArch64GeneralReg, + base: AArch64GeneralReg, + imm12: u16, +) { + let inst = LoadStoreRegisterImmediate::new_load(LoadStoreRegisterImmediateParams { + size: register_width as u8, + imm12, + rn: base, + rt: dst, + }); + + buf.extend(inst.bytes()); +} + +#[inline(always)] +fn ldur_reg_reg_imm9( + buf: &mut Vec<'_, u8>, + register_width: RegisterWidth, + dst: AArch64GeneralReg, + base: AArch64GeneralReg, + imm9: i16, +) { + // the value must fit in 8 bits (1 bit for the sign) + assert!((-256..256).contains(&imm9)); + + let imm9 = u16::from_ne_bytes(imm9.to_ne_bytes()); + #[allow(clippy::identity_op)] + let imm12 = (imm9 & 0b0001_1111_1111) << 2 | 0b00; + + let inst = LoadStoreRegisterImmediate { + size: (register_width as u8).into(), // 64-bit + fixed: 0b111.into(), + fixed2: false, + fixed3: 0b00.into(), + opc: 0b01.into(), // load + imm12: imm12.into(), + rn: base.id().into(), + rt: dst.id().into(), + }; + + buf.extend(inst.bytes()); +} + +/// `LDR Xt, [Xn, #offset]` -> Load Xn + Offset Xt. ZRSP is SP. +/// Note: imm12 is the offest divided by 8. +#[inline(always)] +fn ldr_freg64_reg64_imm12( + buf: &mut Vec<'_, u8>, + dst: AArch64FloatReg, + base: AArch64GeneralReg, + imm12: u16, +) { + let inst = LoadStoreRegisterImmediate { + size: 0b11.into(), // 64-bit + fixed: 0b111.into(), + fixed2: true, + fixed3: 0b01.into(), + opc: 0b01.into(), // load + imm12: imm12.into(), + rn: base.id().into(), + rt: dst.id().into(), + }; + + buf.extend(inst.bytes()); +} + +#[inline(always)] +fn ldur_freg64_reg64_imm9( + buf: &mut Vec<'_, u8>, + dst: AArch64FloatReg, + base: AArch64GeneralReg, + imm9: i16, +) { + // the value must fit in 8 bits (1 bit for the sign) + assert!((-256..256).contains(&imm9)); + + let imm9 = u16::from_ne_bytes(imm9.to_ne_bytes()); + let imm12 = (imm9 & 0b0001_1111_1111) << 2; + + let inst = LoadStoreRegisterImmediate { + size: 0b11.into(), // 64-bit + fixed: 0b111.into(), + fixed2: true, + fixed3: 0b00.into(), + opc: 0b01.into(), // load + imm12: imm12.into(), + rn: base.id().into(), + rt: dst.id().into(), + }; + + buf.extend(inst.bytes()); +} + +/// `LSL Xd, Xn, Xm` -> Logical shift Xn left by Xm and place the result into Xd. +#[inline(always)] +fn lsl_reg64_reg64_reg64( + buf: &mut Vec<'_, u8>, + dst: AArch64GeneralReg, + src1: AArch64GeneralReg, + src2: AArch64GeneralReg, +) { + let inst = DataProcessingTwoSource::new(DataProcessingTwoSourceParams { + op: 0b001000, + rm: src2, + rn: src1, + rd: dst, + }); + + buf.extend(inst.bytes()); +} + +/// `LSR Xd, Xn, Xm` -> Logical shift Xn right by Xm and place the result into Xd. +#[inline(always)] +fn lsr_reg64_reg64_reg64( + buf: &mut Vec<'_, u8>, + dst: AArch64GeneralReg, + src1: AArch64GeneralReg, + src2: AArch64GeneralReg, +) { + let inst = DataProcessingTwoSource::new(DataProcessingTwoSourceParams { + op: 0b001001, + rm: src2, + rn: src1, + rd: dst, + }); + + buf.extend(inst.bytes()); +} + +/// `MADD Xd, Xn, Xm, Xa` -> Multiply Xn and Xm, add Xa, and place the result into Xd. +#[inline(always)] +fn madd_reg64_reg64_reg64_reg64( + buf: &mut Vec<'_, u8>, + dst: AArch64GeneralReg, + src1: AArch64GeneralReg, + src2: AArch64GeneralReg, + src3: AArch64GeneralReg, +) { + let inst = DataProcessingThreeSource::new(DataProcessingThreeSourceParams { + op31: 0b000000, + rm: src2, + ra: src3, + rn: src1, + rd: dst, + }); + + buf.extend(inst.bytes()); +} + +/// `MOV Xd, Xm` -> Move Xm to Xd. +#[inline(always)] +fn mov_reg64_reg64(buf: &mut Vec<'_, u8>, dst: AArch64GeneralReg, src: AArch64GeneralReg) { + // MOV is equivalent to `ORR Xd, XZR, Xm` in AARCH64. + orr_reg64_reg64_reg64(buf, dst, AArch64GeneralReg::ZRSP, src); +} + +/// `MOVK Xd, imm16` -> Keeps Xd and moves an optionally shifted imm16 to Xd. +#[inline(always)] +fn movk_reg64_imm16(buf: &mut Vec<'_, u8>, dst: AArch64GeneralReg, imm16: u16, hw: u8) { + let inst = MoveWideImmediate::new(MoveWideImmediateParams { + opc: 0b11, + rd: dst, + imm16, + hw, + sf: true, + }); + + buf.extend(inst.bytes()); +} + +/// `MOVZ Xd, imm16` -> Zeros Xd and moves an optionally shifted imm16 to Xd. +#[inline(always)] +fn movz_reg64_imm16(buf: &mut Vec<'_, u8>, dst: AArch64GeneralReg, imm16: u16, hw: u8) { + let inst = MoveWideImmediate::new(MoveWideImmediateParams { + opc: 0b10, + rd: dst, + imm16, + hw, + sf: true, + }); + + buf.extend(inst.bytes()); +} + +/// `MUL Xd, Xn, Xm` -> Multiply Xn and Xm and place the result into Xd. +#[inline(always)] +fn mul_reg64_reg64_reg64( + buf: &mut Vec<'_, u8>, + dst: AArch64GeneralReg, + src1: AArch64GeneralReg, + src2: AArch64GeneralReg, +) { + madd_reg64_reg64_reg64_reg64(buf, dst, src1, src2, AArch64GeneralReg::ZRSP); +} + +/// `NEG Xd, Xm` -> Negate Xm and place the result into Xd. +#[inline(always)] +fn neg_reg64_reg64(buf: &mut Vec<'_, u8>, dst: AArch64GeneralReg, src: AArch64GeneralReg) { + sub_reg64_reg64_reg64(buf, dst, AArch64GeneralReg::ZRSP, src); +} + +/// `ORR Xd, Xn, Xm` -> Bitwise OR Xn and Xm and place the result into Xd. +#[inline(always)] +fn orr_reg64_reg64_reg64( + buf: &mut Vec<'_, u8>, + dst: AArch64GeneralReg, + src1: AArch64GeneralReg, + src2: AArch64GeneralReg, +) { + let inst = LogicalShiftedRegister::new(LogicalShiftedRegisterParams { + op: LogicalOp::ORR, + shift: ShiftType::LSL, + imm6: 0, + rm: src2, + rn: src1, + rd: dst, + }); + + buf.extend(inst.bytes()); +} + +/// `SDIV Xd, Xn, Xm` -> Divide Xn by Xm and place the result into Xd. +/// Xn, Xm, and Xd are signed integers. +#[inline(always)] +fn sdiv_reg64_reg64_reg64( + buf: &mut Vec<'_, u8>, + dst: AArch64GeneralReg, + src1: AArch64GeneralReg, + src2: AArch64GeneralReg, +) { + let inst = DataProcessingTwoSource::new(DataProcessingTwoSourceParams { + op: 0b000011, + rm: src2, + rn: src1, + rd: dst, + }); + + buf.extend(inst.bytes()); +} + +fn stur_reg_reg_imm9( + buf: &mut Vec<'_, u8>, + register_width: RegisterWidth, + src: AArch64GeneralReg, + base: AArch64GeneralReg, + imm9: i16, +) { + // the value must fit in 8 bits (1 bit for the sign) + assert!((-256..256).contains(&imm9)); + + let imm9 = u16::from_ne_bytes(imm9.to_ne_bytes()); + let imm12 = (imm9 & 0b0001_1111_1111) << 2; + + let inst = LoadStoreRegisterImmediate { + size: (register_width as u8).into(), // 64-bit + fixed: 0b111.into(), + fixed2: false, + fixed3: 0b00.into(), + opc: 0b00.into(), // store + imm12: imm12.into(), + rn: base.id().into(), + rt: src.id().into(), + }; + + buf.extend(inst.bytes()); +} + +/// `STR Xt, [Xn, #offset]` -> Store Xt to Xn + Offset. ZRSP is SP. +/// Note: imm12 is the offest divided by 8. +#[inline(always)] +fn str_reg_reg_imm12( + buf: &mut Vec<'_, u8>, + register_width: RegisterWidth, + src: AArch64GeneralReg, + base: AArch64GeneralReg, + imm12: u16, +) { + let inst = LoadStoreRegisterImmediate::new_store(LoadStoreRegisterImmediateParams { + size: register_width as u8, + imm12, + rn: base, + rt: src, + }); + + buf.extend(inst.bytes()); +} + +#[inline(always)] +fn stur_freg64_reg64_imm9( + buf: &mut Vec<'_, u8>, + src: AArch64FloatReg, + base: AArch64GeneralReg, + imm9: i16, +) { + // the value must fit in 8 bits (1 bit for the sign) + assert!((-256..256).contains(&imm9)); + + let imm9 = u16::from_ne_bytes(imm9.to_ne_bytes()); + let imm12 = (imm9 & 0b0001_1111_1111) << 2; + + let inst = LoadStoreRegisterImmediate { + size: 0b11.into(), // 64-bit + fixed: 0b111.into(), + fixed2: true, + fixed3: 0b00.into(), + opc: 0b00.into(), // store + imm12: imm12.into(), + rn: base.id().into(), + rt: src.id().into(), + }; + + buf.extend(inst.bytes()); +} + +#[inline(always)] +fn str_freg64_reg64_imm12( + buf: &mut Vec<'_, u8>, + src: AArch64FloatReg, + base: AArch64GeneralReg, + imm12: u16, +) { + let inst = LoadStoreRegisterImmediate { + size: 0b11.into(), // 64-bit + fixed: 0b111.into(), + fixed2: true, + fixed3: 0b01.into(), + opc: 0b00.into(), // store + imm12: imm12.into(), + rn: base.id().into(), + rt: src.id().into(), + }; + + buf.extend(inst.bytes()); +} + +/// `SUB Xd, Xn, imm12` -> Subtract Xn and imm12 and place the result into Xd. +#[inline(always)] +fn sub_reg64_reg64_imm12( + buf: &mut Vec<'_, u8>, + dst: AArch64GeneralReg, + src: AArch64GeneralReg, + imm12: u16, +) { + let inst = ArithmeticImmediate::new(ArithmeticImmediateParams { + op: true, + s: false, + rd: dst, + rn: src, + imm12, + sh: false, + }); + + buf.extend(inst.bytes()); +} + +/// `SUB Xd, Xm, Xn` -> Subtract Xm and Xn and place the result into Xd. +#[inline(always)] +fn sub_reg64_reg64_reg64( + buf: &mut Vec<'_, u8>, + dst: AArch64GeneralReg, + src1: AArch64GeneralReg, + src2: AArch64GeneralReg, +) { + let inst = ArithmeticShifted::new(ArithmeticShiftedParams { + op: true, + s: false, + shift: ShiftType::LSL, + imm6: 0, + rm: src2, + rn: src1, + rd: dst, + }); + + buf.extend(inst.bytes()); +} + +/// `SUBS Xd, Xn, imm12` -> Subtract Xn and imm12 and place the result into Xd. Set condition flags. +#[inline(always)] +fn subs_reg64_reg64_imm12( + buf: &mut Vec<'_, u8>, + dst: AArch64GeneralReg, + src: AArch64GeneralReg, + imm12: u16, +) { + let inst = ArithmeticImmediate::new(ArithmeticImmediateParams { + op: true, + s: true, + rd: dst, + rn: src, + imm12, + sh: false, + }); + + buf.extend(inst.bytes()); +} + +/// `SUBS Xd, Xn, Xm` -> Subtract Xn and Xm and place the result into Xd. Set condition flags. +#[inline(always)] +fn subs_reg64_reg64_reg64( + buf: &mut Vec<'_, u8>, + dst: AArch64GeneralReg, + src1: AArch64GeneralReg, + src2: AArch64GeneralReg, +) { + subs_reg64_reg64_reg64_with_shift(buf, dst, src1, src2, (ShiftType::LSL, 0)) +} + +/// `SUBS Xd, Xn, Xm` -> Subtract Xn and Xm and place the result into Xd. Set condition flags. +#[inline(always)] +fn subs_reg64_reg64_reg64_with_shift( + buf: &mut Vec<'_, u8>, + dst: AArch64GeneralReg, + src1: AArch64GeneralReg, + src2: AArch64GeneralReg, + (shift, amount): (ShiftType, u8), +) { + let inst = ArithmeticShifted::new(ArithmeticShiftedParams { + op: true, + s: true, + shift, + imm6: amount, + rm: src2, + rn: src1, + rd: dst, + }); + + buf.extend(inst.bytes()); +} + +/// `RET Xn` -> Return to the address stored in Xn. +#[inline(always)] +fn ret_reg64(buf: &mut Vec<'_, u8>, xn: AArch64GeneralReg) { + let inst = + UnconditionalBranchRegister::new(UnconditionalBranchRegisterParams { op: 0b10, rn: xn }); + + buf.extend(inst.bytes()); +} + +/// `UDIV Xd, Xn, Xm` -> Divide Xn by Xm and place the result into Xd. +/// Xn, Xm, and Xd are unsigned integers. +#[inline(always)] +fn udiv_reg64_reg64_reg64( + buf: &mut Vec<'_, u8>, + dst: AArch64GeneralReg, + src1: AArch64GeneralReg, + src2: AArch64GeneralReg, +) { + let inst = DataProcessingTwoSource::new(DataProcessingTwoSourceParams { + op: 0b000010, + rm: src2, + rn: src1, + rd: dst, + }); + + buf.extend(inst.bytes()); +} + +// Floating point (and advanced SIMD) instructions +// ARM manual section C7 + +/// `FABS Sd/Dd, Sn/Dn` -> Take the absolute value of Sn/Dn and place the result into Sd/Dd. +#[inline(always)] +fn fabs_freg_freg( + buf: &mut Vec<'_, u8>, + ftype: FloatWidth, + dst: AArch64FloatReg, + src: AArch64FloatReg, +) { + let inst = + FloatingPointDataProcessingOneSource::new(FloatingPointDataProcessingOneSourceParams { + opcode: 0b000001, + ptype: ftype, + rd: dst, + rn: src, + }); + + buf.extend(inst.bytes()); +} + +/// `FADD Sd/Dd, Sn/Dn, Sm/Dm` -> Add Sn/Dn and Sm/Dm and place the result into Sd/Dd. +#[inline(always)] +fn fadd_freg_freg_freg( + buf: &mut Vec<'_, u8>, + ftype: FloatWidth, + dst: AArch64FloatReg, + src1: AArch64FloatReg, + src2: AArch64FloatReg, +) { + let inst = + FloatingPointDataProcessingTwoSource::new(FloatingPointDataProcessingTwoSourceParams { + opcode: 0b0010, + ptype: ftype, + rd: dst, + rn: src1, + rm: src2, + }); + + buf.extend(inst.bytes()); +} + +/// `FCMP Sn/Dn, Sm/Dm` -> Compare Sn/Dn and Sm/Dm, setting condition flags. +#[inline(always)] +fn fcmp_freg_freg( + buf: &mut Vec<'_, u8>, + ftype: FloatWidth, + src1: AArch64FloatReg, + src2: AArch64FloatReg, +) { + let inst = FloatingPointCompare::new(FloatingPointCompareParams { + ptype: ftype, + rn: src1, + rm: src2, + opcode2: 0b00000, + }); + + buf.extend(inst.bytes()); +} + +/// `FCVT Sd, Dn` -> Convert 64-bit float Dn to 32-bit float Sd. +#[inline(always)] +fn fcvt_freg32_freg64(buf: &mut Vec<'_, u8>, dst: AArch64FloatReg, src: AArch64FloatReg) { + let inst = + FloatingPointDataProcessingOneSource::new(FloatingPointDataProcessingOneSourceParams { + opcode: 0b000100, + ptype: FloatWidth::F64, + rd: dst, + rn: src, + }); + + buf.extend(inst.bytes()); +} + +/// `FCVT Dd, Sn` -> Convert 32-bit float Sn to 64-bit float Dd. +#[inline(always)] +fn fcvt_freg64_freg32(buf: &mut Vec<'_, u8>, dst: AArch64FloatReg, src: AArch64FloatReg) { + let inst = + FloatingPointDataProcessingOneSource::new(FloatingPointDataProcessingOneSourceParams { + opcode: 0b000101, + ptype: FloatWidth::F32, + rd: dst, + rn: src, + }); + + buf.extend(inst.bytes()); +} + +/// `FDIV Sd/Dd, Sn/Dn, Sm/Dm` -> Divide Sn/Dn by Sm/Dm and place the result into Sd/Dd. +#[inline(always)] +fn fdiv_freg_freg_freg( + buf: &mut Vec<'_, u8>, + ftype: FloatWidth, + dst: AArch64FloatReg, + src1: AArch64FloatReg, + src2: AArch64FloatReg, +) { + let inst = + FloatingPointDataProcessingTwoSource::new(FloatingPointDataProcessingTwoSourceParams { + opcode: 0b0001, + ptype: ftype, + rd: dst, + rn: src1, + rm: src2, + }); + + buf.extend(inst.bytes()); +} + +#[derive(PackedStruct)] +#[packed_struct(endian = "msb")] +pub struct FMovGeneral { + sf: bool, + fixed: Integer>, + ftype: Integer>, + fixed2: bool, + rmode: Integer>, + opcode: Integer>, + fixed3: Integer>, + rn: Integer>, + rd: Integer>, +} + +impl Aarch64Bytes for FMovGeneral {} + +fn fmov_freg_reg( + buf: &mut Vec<'_, u8>, + ftype: FloatWidth, + dst: AArch64FloatReg, + src: AArch64GeneralReg, +) { + let inst = FMovGeneral { + sf: match ftype { + FloatWidth::F32 => false, + FloatWidth::F64 => true, + }, + fixed: 0b0011110.into(), + ftype: match ftype { + FloatWidth::F32 => 0b00.into(), + FloatWidth::F64 => 0b01.into(), + }, + fixed2: true, + rmode: 0b00.into(), + opcode: 0b111.into(), + fixed3: 0b000000.into(), + rn: src.id().into(), + rd: dst.id().into(), + }; + + buf.extend(inst.bytes()); +} + +fn fmov_reg_freg( + buf: &mut Vec<'_, u8>, + ftype: FloatWidth, + dst: AArch64GeneralReg, + src: AArch64FloatReg, +) { + let inst = FMovGeneral { + sf: match ftype { + FloatWidth::F32 => false, + FloatWidth::F64 => true, + }, + fixed: 0b0011110.into(), + ftype: match ftype { + FloatWidth::F32 => 0b00.into(), + FloatWidth::F64 => 0b01.into(), + }, + fixed2: true, + rmode: 0b00.into(), + opcode: 0b110.into(), + fixed3: 0b000000.into(), + rn: src.id().into(), + rd: dst.id().into(), + }; + + buf.extend(inst.bytes()); +} + +/// `FMOV Sd/Dd, Sn/Dn` -> Move Sn/Dn to Sd/Dd. +#[inline(always)] +fn fmov_freg_freg( + buf: &mut Vec<'_, u8>, + ftype: FloatWidth, + dst: AArch64FloatReg, + src: AArch64FloatReg, +) { + let inst = + FloatingPointDataProcessingOneSource::new(FloatingPointDataProcessingOneSourceParams { + opcode: 0b000000, + ptype: ftype, + rd: dst, + rn: src, + }); + + buf.extend(inst.bytes()); +} + +/// Encode a 32-bit float into an 8-bit immediate for FMOV. +/// See Table C2-1 in the ARM manual for a table of every float that can be encoded in 8 bits. +/// If the float cannot be encoded, return None. +/// This operation is the inverse of VFPExpandImm in the ARM manual. +#[inline(always)] +fn encode_f32_to_imm8(imm: f32) -> Option { + let n = 32; + let e = 8; // number of exponent bits in a 32-bit float + let f = n - e - 1; // 23: number of fraction bits in a 32-bit float + + let bits = imm.to_bits(); + + let sign = (bits >> (n - 1)) & 1; // bits<31> + let exp = (bits >> f) & ((1 << e) - 1); // bits<30:23> + let frac = bits & ((1 << f) - 1); // bits<22:0> + + let exp_first = (exp >> (e - 1)) & 1; // exp<7> + let exp_middle = (exp >> 2) & ((1 << (e - 3)) - 1); // exp<6:2> + let exp_last = exp & 0b11; // exp<1:0> + if exp_first == 0 && exp_middle != ((1 << (e - 3)) - 1) { + // If exp_first is 0, exp_middle must be all 1s. + return None; + } + if exp_first == 1 && exp_middle != 0 { + // If exp_first is 1, exp_middle must be all 0s. + return None; + } + + let frac_begin = frac >> (f - 4); // frac<22:19> + let frac_end = frac & ((1 << (f - 4)) - 1); // frac<18:0> + if frac_end != 0 { + // frac_end must be all 0s. + return None; + } + + // The sign is the same. + let ret_sign = sign << 7; + // The first bit of the exponent is inverted. + let ret_exp_first = (exp_first ^ 1) << 6; + // The rest of the exponent is the same as the last 2 bits of the original exponent. + let ret_exp_last = exp_last << 4; + // The fraction is the same as the first 4 bits of the original fraction. + let ret_frac = frac_begin; + + Some((ret_sign | ret_exp_first | ret_exp_last | ret_frac) as u8) +} + +/// Encode a 64-bit float into an 8-bit immediate for FMOV. +/// See Table C2-1 in the ARM manual for a table of every float that can be encoded in 8 bits. +/// If the float cannot be encoded, return None. +/// This operation is the inverse of VFPExpandImm in the ARM manual. +#[inline(always)] +fn encode_f64_to_imm8(imm: f64) -> Option { + let n = 64; + let e = 11; // number of exponent bits in a 64-bit float + let f = n - e - 1; // 52: number of fraction bits in a 64-bit float + + let bits = imm.to_bits(); + + let sign = (bits >> (n - 1)) & 1; // bits<63> + let exp = (bits >> f) & ((1 << e) - 1); // bits<62:52> + let frac = bits & ((1 << f) - 1); // bits<51:0> + + let exp_first = (exp >> (e - 1)) & 1; // exp<10> + let exp_middle = (exp >> 2) & ((1 << (e - 3)) - 1); // exp<9:2> + let exp_last = exp & 0b11; // exp<0:1> + if exp_first == 0 && exp_middle != ((1 << (e - 3)) - 1) { + // If exp_first is 0, exp_middle must be all 1s. + return None; + } + if exp_first == 1 && exp_middle != 0 { + // If exp_first is 1, exp_middle must be all 0s. + return None; + } + + let frac_begin = frac >> (f - 4); // frac<51:48> + let frac_end = frac & ((1 << (f - 4)) - 1); // frac<47:0> + if frac_end != 0 { + // frac_end must be all 0s. + return None; + } + + // The sign is the same. + let ret_sign = sign << 7; + // The first bit of the exponent is inverted. + let ret_exp_first = (exp_first ^ 1) << 6; + // The rest of the exponent is the same as the last 2 bits of the original exponent. + let ret_exp_last = exp_last << 4; + // The fraction is the same as the first 4 bits of the original fraction. + let ret_frac = frac_begin; + + Some((ret_sign | ret_exp_first | ret_exp_last | ret_frac) as u8) +} + +/// `FMOV Sd/Dd, imm8` -> Move imm8 to a float register. +/// imm8 is a float encoded using encode_f32_to_imm8 or encode_f64_to_imm8. +#[inline(always)] +fn fmov_freg_imm8(buf: &mut Vec<'_, u8>, ftype: FloatWidth, dst: AArch64FloatReg, imm8: u8) { + let inst = FloatingPointImmediate::new(FloatingPointImmediateParams { + ptype: ftype, + rd: dst, + imm8, + }); + + buf.extend(inst.bytes()); +} + +/// `FMUL Sd/Dd, Sn/Dn, Sm/Dm` -> Multiply Sn/Dn by Sm/Dm and store the result in Sd/Dd. +#[inline(always)] +fn fmul_freg_freg_freg( + buf: &mut Vec<'_, u8>, + ftype: FloatWidth, + dst: AArch64FloatReg, + src1: AArch64FloatReg, + src2: AArch64FloatReg, +) { + let inst = + FloatingPointDataProcessingTwoSource::new(FloatingPointDataProcessingTwoSourceParams { + opcode: 0b0000, + ptype: ftype, + rd: dst, + rn: src1, + rm: src2, + }); + + buf.extend(inst.bytes()); +} + +/// `FSQRT Sd/Dd, Sn/Dn` -> Compute the square root of Sn/Dn and store the result in Sd/Dd. +#[inline(always)] +fn fsqrt_freg_freg( + buf: &mut Vec<'_, u8>, + ftype: FloatWidth, + dst: AArch64FloatReg, + src: AArch64FloatReg, +) { + let inst = + FloatingPointDataProcessingOneSource::new(FloatingPointDataProcessingOneSourceParams { + opcode: 0b000011, + ptype: ftype, + rd: dst, + rn: src, + }); + + buf.extend(inst.bytes()); +} + +/// Currently, we're only using MOVI to set a float register to 0.0. +/// `MOVI Dd, #0.0` -> Move 0.0 to Dd +#[inline(always)] +fn movi_freg_zero(buf: &mut Vec<'_, u8>, dst: AArch64FloatReg) { + let inst = AdvancedSimdModifiedImmediate::new(dst); + + buf.extend(inst.bytes()); +} + +/// `SCVTF Sd/Dd, Xn` -> Convert Xn to a float and store the result in Sd/Dd. +#[inline(always)] +fn scvtf_freg_reg64( + buf: &mut Vec<'_, u8>, + ftype: FloatWidth, + dst: AArch64FloatReg, + src: AArch64GeneralReg, +) { + let inst = ConversionBetweenFloatingPointAndInteger::new( + ConversionBetweenFloatingPointAndIntegerParams { + opcode: 0b010, + rmode: 0b00, + ptype: ftype, + rd: dst, + rn: src, + }, + ); + + buf.extend(inst.bytes()); +} + +#[inline(always)] +fn jmp_reg64(buf: &mut Vec<'_, u8>, base: AArch64GeneralReg) { + let inst = 0b11010110000111110000000000000000 | ((base as u32) << 5); + + buf.extend(inst.to_le_bytes()); +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{disassembler_test, generic64::aarch64::subs_reg64_reg64_reg64_with_shift}; + use capstone::prelude::*; + + enum ZRSPKind { + UsesZR, + UsesSP, + } + use ZRSPKind::*; + + impl AArch64GeneralReg { + fn capstone_string(&self, zrsp_kind: ZRSPKind) -> String { + match self { + AArch64GeneralReg::XR => "x8".to_owned(), + AArch64GeneralReg::IP0 => "x16".to_owned(), + AArch64GeneralReg::IP1 => "x17".to_owned(), + AArch64GeneralReg::PR => "x18".to_owned(), + AArch64GeneralReg::FP => "x29".to_owned(), + AArch64GeneralReg::LR => "x30".to_owned(), + AArch64GeneralReg::ZRSP => match zrsp_kind { + UsesZR => "xzr".to_owned(), + UsesSP => "sp".to_owned(), + }, + _ => format!("{self}"), + } + } + + fn capstone_string_32bit(&self, zrsp_kind: ZRSPKind) -> String { + match self { + AArch64GeneralReg::XR => "w8".to_owned(), + AArch64GeneralReg::IP0 => "w16".to_owned(), + AArch64GeneralReg::IP1 => "w17".to_owned(), + AArch64GeneralReg::PR => "w18".to_owned(), + AArch64GeneralReg::FP => "w29".to_owned(), + AArch64GeneralReg::LR => "w30".to_owned(), + AArch64GeneralReg::ZRSP => match zrsp_kind { + UsesZR => "wzr".to_owned(), + UsesSP => "sp".to_owned(), + }, + _ => self.as_str_32bit().to_string(), + } + } + } + + impl AArch64FloatReg { + fn capstone_string(&self, float_type: FloatWidth) -> String { + match float_type { + FloatWidth::F32 => format!("s{}", self.id()), + FloatWidth::F64 => format!("d{}", self.id()), + } + } + } + + fn signed_hex_i16(value: i16) -> String { + let sign = if value < 0 { "-" } else { "" }; + + if value.unsigned_abs() < 16 { + format!("#{sign}{}", value.unsigned_abs()) + } else { + format!("#{sign}0x{:x}", value.unsigned_abs()) + } + } + + const TEST_U16: u16 = 0x1234; + //const TEST_I32: i32 = 0x12345678; + //const TEST_I64: i64 = 0x12345678_9ABCDEF0; + + const ALL_REGISTER_WIDTHS: &[RegisterWidth] = &[ + RegisterWidth::W8, + RegisterWidth::W16, + RegisterWidth::W32, + RegisterWidth::W64, + ]; + + const ALL_GENERAL_REGS: &[AArch64GeneralReg] = &[ + AArch64GeneralReg::X0, + AArch64GeneralReg::X1, + AArch64GeneralReg::X2, + AArch64GeneralReg::X3, + AArch64GeneralReg::X4, + AArch64GeneralReg::X5, + AArch64GeneralReg::X6, + AArch64GeneralReg::X7, + AArch64GeneralReg::XR, + AArch64GeneralReg::X9, + AArch64GeneralReg::X10, + AArch64GeneralReg::X11, + AArch64GeneralReg::X12, + AArch64GeneralReg::X13, + AArch64GeneralReg::X14, + AArch64GeneralReg::X15, + AArch64GeneralReg::IP0, + AArch64GeneralReg::IP1, + AArch64GeneralReg::PR, + AArch64GeneralReg::X19, + AArch64GeneralReg::X20, + AArch64GeneralReg::X21, + AArch64GeneralReg::X22, + AArch64GeneralReg::X23, + AArch64GeneralReg::X24, + AArch64GeneralReg::X25, + AArch64GeneralReg::X26, + AArch64GeneralReg::X27, + AArch64GeneralReg::X28, + AArch64GeneralReg::FP, + AArch64GeneralReg::LR, + AArch64GeneralReg::ZRSP, + ]; + + const ALL_FLOAT_REGS: &[AArch64FloatReg] = &[ + AArch64FloatReg::V0, + AArch64FloatReg::V1, + AArch64FloatReg::V2, + AArch64FloatReg::V3, + AArch64FloatReg::V4, + AArch64FloatReg::V5, + AArch64FloatReg::V6, + AArch64FloatReg::V7, + AArch64FloatReg::V8, + AArch64FloatReg::V9, + AArch64FloatReg::V10, + AArch64FloatReg::V11, + AArch64FloatReg::V12, + AArch64FloatReg::V13, + AArch64FloatReg::V14, + AArch64FloatReg::V15, + AArch64FloatReg::V16, + AArch64FloatReg::V17, + AArch64FloatReg::V18, + AArch64FloatReg::V19, + AArch64FloatReg::V20, + AArch64FloatReg::V21, + AArch64FloatReg::V22, + AArch64FloatReg::V23, + AArch64FloatReg::V24, + AArch64FloatReg::V25, + AArch64FloatReg::V26, + AArch64FloatReg::V27, + AArch64FloatReg::V28, + AArch64FloatReg::V29, + AArch64FloatReg::V30, + AArch64FloatReg::V31, + ]; + + const ALL_FLOAT_TYPES: &[FloatWidth] = &[FloatWidth::F32, FloatWidth::F64]; + + const ALL_CONDITIONS: &[ConditionCode] = &[ + ConditionCode::EQ, + ConditionCode::NE, + ConditionCode::CSHS, + ConditionCode::CCLO, + ConditionCode::MI, + ConditionCode::PL, + ConditionCode::VS, + ConditionCode::VC, + ConditionCode::HI, + ConditionCode::LS, + ConditionCode::GE, + ConditionCode::LT, + ConditionCode::GT, + ConditionCode::LE, + ConditionCode::AL, + ]; + + fn setup_capstone_and_arena( + arena: &bumpalo::Bump, + ) -> (bumpalo::collections::Vec, Capstone) { + let buf = bumpalo::vec![in arena]; + let cs = Capstone::new() + .arm64() + .mode(arch::arm64::ArchMode::Arm) + .detail(true) + .build() + .expect("Failed to create Capstone object"); + (buf, cs) + } + + // Many of these instructions are aliases for each other, + // and depending on their arguments, they might get disassembled to a different instruction. + // That's why we need `if` expressions in some of these tests. + // The "alias conditions" for each instruction are listed in the ARM manual. + + #[test] + fn test_add_reg64_reg64_reg64() { + disassembler_test!( + add_reg64_reg64_reg64, + |reg1: AArch64GeneralReg, reg2: AArch64GeneralReg, reg3: AArch64GeneralReg| format!( + "add {}, {}, {}", + reg1.capstone_string(UsesZR), + reg2.capstone_string(UsesZR), + reg3.capstone_string(UsesZR) + ), + ALL_GENERAL_REGS, + ALL_GENERAL_REGS, + ALL_GENERAL_REGS + ); + } + + #[test] + fn test_add_reg64_reg64_imm12() { + disassembler_test!( + add_reg64_reg64_imm12, + |reg1: AArch64GeneralReg, reg2: AArch64GeneralReg, imm| format!( + "add {}, {}, #0x{:x}", + reg1.capstone_string(UsesSP), + reg2.capstone_string(UsesSP), + imm + ), + ALL_GENERAL_REGS, + ALL_GENERAL_REGS, + [0x123] + ); + } + + #[test] + fn test_and_reg64_reg64_reg64() { + disassembler_test!( + and_reg64_reg64_reg64, + |reg1: AArch64GeneralReg, reg2: AArch64GeneralReg, reg3: AArch64GeneralReg| format!( + "and {}, {}, {}", + reg1.capstone_string(UsesZR), + reg2.capstone_string(UsesZR), + reg3.capstone_string(UsesZR) + ), + ALL_GENERAL_REGS, + ALL_GENERAL_REGS, + ALL_GENERAL_REGS + ); + } + + #[test] + fn test_asr_reg64_reg64_reg64() { + disassembler_test!( + asr_reg64_reg64_reg64, + |reg1: AArch64GeneralReg, reg2: AArch64GeneralReg, reg3: AArch64GeneralReg| format!( + "asr {}, {}, {}", + reg1.capstone_string(UsesZR), + reg2.capstone_string(UsesZR), + reg3.capstone_string(UsesZR) + ), + ALL_GENERAL_REGS, + ALL_GENERAL_REGS, + ALL_GENERAL_REGS + ); + } + + #[test] + fn test_b_cond_imm19() { + disassembler_test!( + b_cond_imm19, + |cond: ConditionCode, imm: i32| format!("b.{} #0x{:x}", cond, imm as i64), + ALL_CONDITIONS, + [0x120, -0x120, (1 << 20) - 4, -(1 << 20)] + ); + } + + #[test] + fn test_b_imm26() { + disassembler_test!( + b_imm26, + |imm| format!("b #0x{:x}", imm as i64), + [0x120, -0x120, (1 << 27) - 4, -(1 << 27)] + ); + } + + #[test] + fn test_cmp_reg64_imm12() { + disassembler_test!( + cmp_reg64_imm12, + |reg1: AArch64GeneralReg, imm| format!( + "cmp {}, #0x{:x}", + reg1.capstone_string(UsesSP), + imm + ), + ALL_GENERAL_REGS, + [0x123] + ); + } + + #[test] + fn test_cmp_reg64_reg64() { + disassembler_test!( + cmp_reg64_reg64, + |reg1: AArch64GeneralReg, reg2: AArch64GeneralReg| format!( + "cmp {}, {}", + reg1.capstone_string(UsesZR), + reg2.capstone_string(UsesZR) + ), + ALL_GENERAL_REGS, + ALL_GENERAL_REGS + ); + } + + #[test] + fn test_cneg_reg64_reg64_cond() { + disassembler_test!( + cneg_reg64_reg64_cond, + |reg1: AArch64GeneralReg, reg2: AArch64GeneralReg, cond: ConditionCode| { + if cond == ConditionCode::AL { + format!( + "csneg {}, {}, {}, {}", + reg1.capstone_string(UsesZR), + reg2.capstone_string(UsesZR), + reg2.capstone_string(UsesZR), + cond.invert() + ) + } else { + format!( + "cneg {}, {}, {}", + reg1.capstone_string(UsesZR), + reg2.capstone_string(UsesZR), + cond + ) + } + }, + ALL_GENERAL_REGS, + ALL_GENERAL_REGS, + ALL_CONDITIONS + ); + } + + #[test] + fn test_cset() { + disassembler_test!( + cset_reg64_cond, + |reg1: AArch64GeneralReg, cond: ConditionCode| { + if cond == ConditionCode::AL { + format!( + "csinc {}, xzr, xzr, {}", + reg1.capstone_string(UsesZR), + cond.invert() + ) + } else { + format!("cset {}, {}", reg1.capstone_string(UsesZR), cond) + } + }, + ALL_GENERAL_REGS, + ALL_CONDITIONS + ); + } + + #[test] + fn test_csinc() { + disassembler_test!( + csinc_reg64_reg64_reg64_cond, + |reg1: AArch64GeneralReg, + reg2: AArch64GeneralReg, + reg3: AArch64GeneralReg, + cond: ConditionCode| { + if reg3 != AArch64GeneralReg::ZRSP + && cond != ConditionCode::AL + && reg2 != AArch64GeneralReg::ZRSP + && reg2 == reg3 + { + format!( + "cinc {}, {}, {}", + reg1.capstone_string(UsesZR), + reg2.capstone_string(UsesZR), + cond.invert() + ) + } else if reg3 == AArch64GeneralReg::ZRSP + && cond != ConditionCode::AL + && reg2 == AArch64GeneralReg::ZRSP + { + format!("cset {}, {}", reg1.capstone_string(UsesZR), cond.invert()) + } else { + format!( + "csinc {}, {}, {}, {}", + reg1.capstone_string(UsesZR), + reg2.capstone_string(UsesZR), + reg3.capstone_string(UsesZR), + cond + ) + } + }, + ALL_GENERAL_REGS, + ALL_GENERAL_REGS, + ALL_GENERAL_REGS, + ALL_CONDITIONS + ) + } + + #[test] + fn test_csneg() { + disassembler_test!( + csneg_reg64_reg64_reg64_cond, + |reg1: AArch64GeneralReg, + reg2: AArch64GeneralReg, + reg3: AArch64GeneralReg, + cond: ConditionCode| { + if cond != ConditionCode::AL && reg2 == reg3 { + format!( + "cneg {}, {}, {}", + reg1.capstone_string(UsesZR), + reg2.capstone_string(UsesZR), + cond.invert() + ) + } else { + format!( + "csneg {}, {}, {}, {}", + reg1.capstone_string(UsesZR), + reg2.capstone_string(UsesZR), + reg3.capstone_string(UsesZR), + cond + ) + } + }, + ALL_GENERAL_REGS, + ALL_GENERAL_REGS, + ALL_GENERAL_REGS, + ALL_CONDITIONS + ) + } + + #[test] + fn test_eor_reg64_reg64_reg64() { + disassembler_test!( + eor_reg64_reg64_reg64, + |reg1: AArch64GeneralReg, reg2: AArch64GeneralReg, reg3: AArch64GeneralReg| format!( + "eor {}, {}, {}", + reg1.capstone_string(UsesZR), + reg2.capstone_string(UsesZR), + reg3.capstone_string(UsesZR) + ), + ALL_GENERAL_REGS, + ALL_GENERAL_REGS, + ALL_GENERAL_REGS + ); + } + + #[test] + fn test_ldr_reg64_reg64_imm12() { + disassembler_test!( + ldr_reg_reg_imm12, + |_, reg1: AArch64GeneralReg, reg2: AArch64GeneralReg, imm| format!( + "ldr {}, [{}, #0x{:x}]", + reg1.capstone_string(UsesZR), + reg2.capstone_string(UsesSP), + imm << 3 + ), + [RegisterWidth::W64], + ALL_GENERAL_REGS, + ALL_GENERAL_REGS, + [0x123] + ); + } + + #[test] + fn test_ldr_freg64_reg64_imm12() { + disassembler_test!( + ldr_freg64_reg64_imm12, + |reg1: AArch64FloatReg, reg2: AArch64GeneralReg, imm| format!( + "ldr {}, [{}, #0x{:x}]", + reg1.capstone_string(FloatWidth::F64), + reg2.capstone_string(UsesSP), + imm << 3 + ), + ALL_FLOAT_REGS, + ALL_GENERAL_REGS, + [0x123] + ); + } + + #[test] + fn test_ludr_freg64_reg64_imm9() { + disassembler_test!( + ldur_freg64_reg64_imm9, + |reg1: AArch64FloatReg, reg2: AArch64GeneralReg, imm| format!( + "ldur {}, [{}, {}]", + reg1.capstone_string(FloatWidth::F64), + reg2.capstone_string(UsesSP), + signed_hex_i16(imm) + ), + ALL_FLOAT_REGS, + ALL_GENERAL_REGS, + [4, -4] + ); + } + + #[test] + fn test_ldur_reg64_reg64_imm9() { + disassembler_test!( + ldur_reg_reg_imm9, + |_, reg1: AArch64GeneralReg, reg2: AArch64GeneralReg, imm| format!( + "ldur {}, [{}, {}]", + reg1.capstone_string(UsesZR), + reg2.capstone_string(UsesSP), + signed_hex_i16(imm), + ), + [RegisterWidth::W64], + ALL_GENERAL_REGS, + ALL_GENERAL_REGS, + [0x010, -0x010, 4, -4] + ); + } + + #[test] + fn test_lsl_reg64_reg64_reg64() { + disassembler_test!( + lsl_reg64_reg64_reg64, + |reg1: AArch64GeneralReg, reg2: AArch64GeneralReg, reg3: AArch64GeneralReg| format!( + "lsl {}, {}, {}", + reg1.capstone_string(UsesZR), + reg2.capstone_string(UsesZR), + reg3.capstone_string(UsesZR) + ), + ALL_GENERAL_REGS, + ALL_GENERAL_REGS, + ALL_GENERAL_REGS + ); + } + + #[test] + fn test_lsr_reg64_reg64_reg64() { + disassembler_test!( + lsr_reg64_reg64_reg64, + |reg1: AArch64GeneralReg, reg2: AArch64GeneralReg, reg3: AArch64GeneralReg| format!( + "lsr {}, {}, {}", + reg1.capstone_string(UsesZR), + reg2.capstone_string(UsesZR), + reg3.capstone_string(UsesZR) + ), + ALL_GENERAL_REGS, + ALL_GENERAL_REGS, + ALL_GENERAL_REGS + ); + } + + #[test] + fn test_madd_reg64_reg64_reg64_reg64() { + disassembler_test!( + madd_reg64_reg64_reg64_reg64, + |reg1: AArch64GeneralReg, + reg2: AArch64GeneralReg, + reg3: AArch64GeneralReg, + reg4: AArch64GeneralReg| { + if reg4 == AArch64GeneralReg::ZRSP { + format!( + "mul {}, {}, {}", + reg1.capstone_string(UsesZR), + reg2.capstone_string(UsesZR), + reg3.capstone_string(UsesZR) + ) + } else { + format!( + "madd {}, {}, {}, {}", + reg1.capstone_string(UsesZR), + reg2.capstone_string(UsesZR), + reg3.capstone_string(UsesZR), + reg4.capstone_string(UsesZR) + ) + } + }, + ALL_GENERAL_REGS, + ALL_GENERAL_REGS, + ALL_GENERAL_REGS, + ALL_GENERAL_REGS + ); + } + + #[test] + fn test_mov_reg64_reg64() { + disassembler_test!( + mov_reg64_reg64, + |reg1: AArch64GeneralReg, reg2: AArch64GeneralReg| format!( + "mov {}, {}", + reg1.capstone_string(UsesZR), + reg2.capstone_string(UsesZR), + ), + ALL_GENERAL_REGS, + ALL_GENERAL_REGS + ); + } + + #[test] + fn test_movk_reg64_imm16() { + disassembler_test!( + movk_reg64_imm16, + |reg1: AArch64GeneralReg, imm, hw| format!( + "movk {}, #0x{:x}{}", + reg1.capstone_string(UsesZR), + imm, + if hw > 0 { + format!(", lsl #{}", hw * 16) + } else { + "".to_owned() + } + ), + ALL_GENERAL_REGS, + [TEST_U16], + [0, 1, 2, 3] + ); + } + + #[test] + fn test_movz_reg64_imm16() { + disassembler_test!( + movz_reg64_imm16, + |reg1: AArch64GeneralReg, imm, hw| format!( + "mov {}, #0x{:x}{}", + reg1.capstone_string(UsesZR), + imm, + "0000".repeat(hw as usize) + ), + ALL_GENERAL_REGS, + [TEST_U16], + [0, 1, 2, 3] + ); + } + + #[test] + fn test_mul_reg64_reg64_reg64() { + disassembler_test!( + mul_reg64_reg64_reg64, + |reg1: AArch64GeneralReg, reg2: AArch64GeneralReg, reg3: AArch64GeneralReg| format!( + "mul {}, {}, {}", + reg1.capstone_string(UsesZR), + reg2.capstone_string(UsesZR), + reg3.capstone_string(UsesZR) + ), + ALL_GENERAL_REGS, + ALL_GENERAL_REGS, + ALL_GENERAL_REGS + ); + } + + #[test] + fn test_neg_reg64_reg64() { + disassembler_test!( + neg_reg64_reg64, + |reg1: AArch64GeneralReg, reg2: AArch64GeneralReg| format!( + "neg {}, {}", + reg1.capstone_string(UsesZR), + reg2.capstone_string(UsesZR) + ), + ALL_GENERAL_REGS, + ALL_GENERAL_REGS + ); + } + + #[test] + fn test_orr_reg64_reg64_reg64() { + disassembler_test!( + orr_reg64_reg64_reg64, + |reg1: AArch64GeneralReg, reg2: AArch64GeneralReg, reg3: AArch64GeneralReg| { + if reg2 == AArch64GeneralReg::ZRSP { + format!( + "mov {}, {}", + reg1.capstone_string(UsesZR), + reg3.capstone_string(UsesZR), + ) + } else { + format!( + "orr {}, {}, {}", + reg1.capstone_string(UsesZR), + reg2.capstone_string(UsesZR), + reg3.capstone_string(UsesZR), + ) + } + }, + ALL_GENERAL_REGS, + ALL_GENERAL_REGS, + ALL_GENERAL_REGS + ); + } + + #[test] + fn test_sdiv_reg64_reg64_reg64() { + disassembler_test!( + sdiv_reg64_reg64_reg64, + |reg1: AArch64GeneralReg, reg2: AArch64GeneralReg, reg3: AArch64GeneralReg| format!( + "sdiv {}, {}, {}", + reg1.capstone_string(UsesZR), + reg2.capstone_string(UsesZR), + reg3.capstone_string(UsesZR) + ), + ALL_GENERAL_REGS, + ALL_GENERAL_REGS, + ALL_GENERAL_REGS + ); + } + + #[test] + fn test_str_reg64_reg64_imm12() { + disassembler_test!( + str_reg_reg_imm12, + |_, reg1: AArch64GeneralReg, reg2: AArch64GeneralReg, imm| format!( + "str {}, [{}, #0x{:x}]", + reg1.capstone_string(UsesZR), + reg2.capstone_string(UsesSP), + imm << 3 + ), + [RegisterWidth::W64], + ALL_GENERAL_REGS, + ALL_GENERAL_REGS, + [0x123] + ); + } + + #[test] + fn test_str_freg64_reg64_imm12() { + disassembler_test!( + str_freg64_reg64_imm12, + |reg1: AArch64FloatReg, reg2: AArch64GeneralReg, imm| format!( + "str {}, [{}, #0x{:x}]", + reg1.capstone_string(FloatWidth::F64), + reg2.capstone_string(UsesSP), + imm << 3 + ), + ALL_FLOAT_REGS, + ALL_GENERAL_REGS, + [0x123] + ); + } + + #[test] + fn test_stur_freg64_reg64_imm9() { + disassembler_test!( + stur_freg64_reg64_imm9, + |reg1: AArch64FloatReg, reg2: AArch64GeneralReg, imm| format!( + "stur {}, [{}, {}]", + reg1.capstone_string(FloatWidth::F64), + reg2.capstone_string(UsesSP), + signed_hex_i16(imm), + ), + ALL_FLOAT_REGS, + ALL_GENERAL_REGS, + [4, -4] + ); + } + + #[test] + fn test_str_reg64_reg64_imm9() { + disassembler_test!( + stur_reg_reg_imm9, + |_, reg1: AArch64GeneralReg, reg2: AArch64GeneralReg, imm| format!( + "stur {}, [{}, {}]", // ! indicates writeback + reg1.capstone_string(UsesZR), + reg2.capstone_string(UsesSP), + signed_hex_i16(imm), + ), + [RegisterWidth::W64], + ALL_GENERAL_REGS, + ALL_GENERAL_REGS, + [4, -4] + ); + } + + #[test] + fn test_sub_reg64_reg64_imm12() { + disassembler_test!( + sub_reg64_reg64_imm12, + |reg1: AArch64GeneralReg, reg2: AArch64GeneralReg, imm| format!( + "sub {}, {}, #0x{:x}", + reg1.capstone_string(UsesSP), + reg2.capstone_string(UsesSP), + imm + ), + ALL_GENERAL_REGS, + ALL_GENERAL_REGS, + [0x123] + ); + } + + #[test] + fn test_sub_reg64_reg64_reg64() { + disassembler_test!( + sub_reg64_reg64_reg64, + |reg1: AArch64GeneralReg, reg2: AArch64GeneralReg, reg3: AArch64GeneralReg| { + if reg2 == AArch64GeneralReg::ZRSP { + // When the second register is ZR, it gets disassembled as neg, + // which is an alias for sub. + format!( + "neg {}, {}", + reg1.capstone_string(UsesZR), + reg3.capstone_string(UsesZR) + ) + } else { + format!( + "sub {}, {}, {}", + reg1.capstone_string(UsesZR), + reg2.capstone_string(UsesZR), + reg3.capstone_string(UsesZR) + ) + } + }, + ALL_GENERAL_REGS, + ALL_GENERAL_REGS, + ALL_GENERAL_REGS + ); + } + + #[test] + + fn test_subs_reg64_reg64_imm12() { + disassembler_test!( + subs_reg64_reg64_imm12, + |reg1: AArch64GeneralReg, reg2: AArch64GeneralReg, imm| { + if reg1 == AArch64GeneralReg::ZRSP { + // When the first register is SP, it gets disassembled as cmp, + // which is an alias for subs. + format!("cmp {}, #0x{:x}", reg2.capstone_string(UsesSP), imm) + } else { + format!( + "subs {}, {}, #0x{:x}", + reg1.capstone_string(UsesZR), + reg2.capstone_string(UsesSP), + imm + ) + } + }, + ALL_GENERAL_REGS, + ALL_GENERAL_REGS, + [0x123] + ); + } + #[test] + fn test_subs_reg64_reg64_reg64_with_shift() { + disassembler_test!( + subs_reg64_reg64_reg64_with_shift, + |reg1: AArch64GeneralReg, reg2: AArch64GeneralReg, reg3: AArch64GeneralReg, _| { + format!( + "subs {}, {}, {}, asr #63", + reg1.capstone_string(UsesZR), + reg2.capstone_string(UsesZR), + reg3.capstone_string(UsesZR) + ) + }, + [AArch64GeneralReg::X0, AArch64GeneralReg::X1], + [AArch64GeneralReg::X0, AArch64GeneralReg::X1], + [AArch64GeneralReg::X0, AArch64GeneralReg::X1], + [(ShiftType::ASR, 63)] + ); + } + + #[test] + fn test_subs_reg64_reg64_reg64() { + disassembler_test!( + subs_reg64_reg64_reg64, + |reg1: AArch64GeneralReg, reg2: AArch64GeneralReg, reg3: AArch64GeneralReg| { + if reg1 == AArch64GeneralReg::ZRSP { + // When the first register is SP, it gets disassembled as cmp, + // which is an alias for subs. + format!( + "cmp {}, {}", + reg2.capstone_string(UsesZR), + reg3.capstone_string(UsesZR) + ) + } else if reg2 == AArch64GeneralReg::ZRSP { + // When the second register is ZR, it gets disassembled as negs, + // which is an alias for subs. + format!( + "negs {}, {}", + reg1.capstone_string(UsesZR), + reg3.capstone_string(UsesZR) + ) + } else { + format!( + "subs {}, {}, {}", + reg1.capstone_string(UsesZR), + reg2.capstone_string(UsesZR), + reg3.capstone_string(UsesZR) + ) + } + }, + ALL_GENERAL_REGS, + ALL_GENERAL_REGS, + ALL_GENERAL_REGS + ); + } + + #[test] + fn test_ret_reg64() { + disassembler_test!( + ret_reg64, + |reg1: AArch64GeneralReg| if reg1 == AArch64GeneralReg::LR { + "ret".to_owned() + } else { + format!("ret {}", reg1.capstone_string(UsesZR)) + }, + ALL_GENERAL_REGS + ); + } + + #[test] + fn test_udiv_reg64_reg64_reg64() { + disassembler_test!( + udiv_reg64_reg64_reg64, + |reg1: AArch64GeneralReg, reg2: AArch64GeneralReg, reg3: AArch64GeneralReg| format!( + "udiv {}, {}, {}", + reg1.capstone_string(UsesZR), + reg2.capstone_string(UsesZR), + reg3.capstone_string(UsesZR) + ), + ALL_GENERAL_REGS, + ALL_GENERAL_REGS, + ALL_GENERAL_REGS + ); + } + + // Float instructions + + #[test] + fn test_fabs_freg_freg() { + disassembler_test!( + fabs_freg_freg, + |ftype: FloatWidth, reg1: AArch64FloatReg, reg2: AArch64FloatReg| format!( + "fabs {}, {}", + reg1.capstone_string(ftype), + reg2.capstone_string(ftype) + ), + ALL_FLOAT_TYPES, + ALL_FLOAT_REGS, + ALL_FLOAT_REGS + ); + } + + #[test] + fn test_fadd_freg_freg_freg() { + disassembler_test!( + fadd_freg_freg_freg, + |ftype: FloatWidth, + reg1: AArch64FloatReg, + reg2: AArch64FloatReg, + reg3: AArch64FloatReg| format!( + "fadd {}, {}, {}", + reg1.capstone_string(ftype), + reg2.capstone_string(ftype), + reg3.capstone_string(ftype) + ), + ALL_FLOAT_TYPES, + ALL_FLOAT_REGS, + ALL_FLOAT_REGS, + ALL_FLOAT_REGS + ); + } + + #[test] + fn test_fcmp_freg_freg() { + disassembler_test!( + fcmp_freg_freg, + |ftype: FloatWidth, reg1: AArch64FloatReg, reg2: AArch64FloatReg| format!( + "fcmp {}, {}", + reg1.capstone_string(ftype), + reg2.capstone_string(ftype) + ), + ALL_FLOAT_TYPES, + ALL_FLOAT_REGS, + ALL_FLOAT_REGS + ); + } + + #[test] + fn test_fcvt_freg32_freg64() { + disassembler_test!( + fcvt_freg32_freg64, + |reg1: AArch64FloatReg, reg2: AArch64FloatReg| format!( + "fcvt {}, {}", + reg1.capstone_string(FloatWidth::F32), + reg2.capstone_string(FloatWidth::F64) + ), + ALL_FLOAT_REGS, + ALL_FLOAT_REGS + ); + } + + #[test] + fn test_fcvt_freg64_freg32() { + disassembler_test!( + fcvt_freg64_freg32, + |reg1: AArch64FloatReg, reg2: AArch64FloatReg| format!( + "fcvt {}, {}", + reg1.capstone_string(FloatWidth::F64), + reg2.capstone_string(FloatWidth::F32) + ), + ALL_FLOAT_REGS, + ALL_FLOAT_REGS + ); + } + + #[test] + fn test_fdiv_freg_freg_freg() { + disassembler_test!( + fdiv_freg_freg_freg, + |ftype: FloatWidth, + reg1: AArch64FloatReg, + reg2: AArch64FloatReg, + reg3: AArch64FloatReg| format!( + "fdiv {}, {}, {}", + reg1.capstone_string(ftype), + reg2.capstone_string(ftype), + reg3.capstone_string(ftype) + ), + ALL_FLOAT_TYPES, + ALL_FLOAT_REGS, + ALL_FLOAT_REGS, + ALL_FLOAT_REGS + ); + } + + #[test] + fn test_fmov_freg_freg() { + disassembler_test!( + fmov_freg_freg, + |ftype: FloatWidth, reg1: AArch64FloatReg, reg2: AArch64FloatReg| format!( + "fmov {}, {}", + reg1.capstone_string(ftype), + reg2.capstone_string(ftype) + ), + ALL_FLOAT_TYPES, + ALL_FLOAT_REGS, + ALL_FLOAT_REGS + ); + } + + #[test] + fn test_fmov_freg_reg() { + disassembler_test!( + fmov_freg_reg, + |ftype: FloatWidth, reg1: AArch64FloatReg, reg2: AArch64GeneralReg| format!( + "fmov {}, {}", + reg1.capstone_string(ftype), + match ftype { + FloatWidth::F32 => reg2.capstone_string_32bit(UsesZR), + FloatWidth::F64 => reg2.capstone_string(UsesZR), + } + ), + ALL_FLOAT_TYPES, + ALL_FLOAT_REGS, + ALL_GENERAL_REGS + ); + } + + #[test] + fn test_fmov_reg_freg() { + disassembler_test!( + fmov_reg_freg, + |ftype: FloatWidth, reg1: AArch64GeneralReg, reg2: AArch64FloatReg| format!( + "fmov {}, {}", + match ftype { + FloatWidth::F32 => reg1.capstone_string_32bit(UsesZR), + FloatWidth::F64 => reg1.capstone_string(UsesZR), + }, + reg2.capstone_string(ftype), + ), + ALL_FLOAT_TYPES, + ALL_GENERAL_REGS, + ALL_FLOAT_REGS + ); + } + + #[test] + #[allow(clippy::unusual_byte_groupings)] + fn test_encode_f32_to_imm8() { + // See ARM manual Table C2-1: A64 Floating-point modified immediate constants + assert_eq!(encode_f32_to_imm8(2.0), Some(0b0_000_0000)); + assert_eq!(encode_f32_to_imm8(4.0), Some(0b0_001_0000)); + assert_eq!(encode_f32_to_imm8(8.0), Some(0b0_010_0000)); + assert_eq!(encode_f32_to_imm8(16.0), Some(0b0_011_0000)); + assert_eq!(encode_f32_to_imm8(0.125), Some(0b0_100_0000)); + assert_eq!(encode_f32_to_imm8(0.25), Some(0b0_101_0000)); + assert_eq!(encode_f32_to_imm8(0.5), Some(0b0_110_0000)); + assert_eq!(encode_f32_to_imm8(1.0), Some(0b0_111_0000)); + + assert_eq!(encode_f32_to_imm8(2.125), Some(0b0_000_0001)); + assert_eq!(encode_f32_to_imm8(2.25), Some(0b0_000_0010)); + assert_eq!(encode_f32_to_imm8(2.375), Some(0b0_000_0011)); + assert_eq!(encode_f32_to_imm8(2.5), Some(0b0_000_0100)); + assert_eq!(encode_f32_to_imm8(2.625), Some(0b0_000_0101)); + assert_eq!(encode_f32_to_imm8(2.75), Some(0b0_000_0110)); + assert_eq!(encode_f32_to_imm8(2.875), Some(0b0_000_0111)); + assert_eq!(encode_f32_to_imm8(3.0), Some(0b0_000_1000)); + assert_eq!(encode_f32_to_imm8(3.125), Some(0b0_000_1001)); + assert_eq!(encode_f32_to_imm8(3.25), Some(0b0_000_1010)); + assert_eq!(encode_f32_to_imm8(3.375), Some(0b0_000_1011)); + assert_eq!(encode_f32_to_imm8(3.5), Some(0b0_000_1100)); + assert_eq!(encode_f32_to_imm8(3.625), Some(0b0_000_1101)); + assert_eq!(encode_f32_to_imm8(3.75), Some(0b0_000_1110)); + assert_eq!(encode_f32_to_imm8(3.875), Some(0b0_000_1111)); + + assert_eq!(encode_f32_to_imm8(-2.0), Some(0b1_000_0000)); + assert_eq!(encode_f32_to_imm8(-0.25), Some(0b1_101_0000)); + assert_eq!(encode_f32_to_imm8(-2.5), Some(0b1_000_0100)); + assert_eq!(encode_f32_to_imm8(-3.375), Some(0b1_000_1011)); + + assert_eq!(encode_f32_to_imm8(1.9375), Some(0b0_111_1111)); + assert_eq!(encode_f32_to_imm8(-1.9375), Some(0b1_111_1111)); + + assert_eq!(encode_f32_to_imm8(23.0), Some(0b0_011_0111)); + assert_eq!(encode_f32_to_imm8(-23.0), Some(0b1_011_0111)); + + assert_eq!(encode_f32_to_imm8(0.0), None); + assert_eq!(encode_f32_to_imm8(-0.0), None); + assert_eq!(encode_f32_to_imm8(32.0), None); + assert_eq!(encode_f32_to_imm8(-32.0), None); + assert_eq!(encode_f32_to_imm8(0.0625), None); + assert_eq!(encode_f32_to_imm8(-0.0625), None); + assert_eq!(encode_f32_to_imm8(0.3), None); + assert_eq!(encode_f32_to_imm8(-0.3), None); + } + + #[test] + #[allow(clippy::unusual_byte_groupings)] + fn test_encode_f64_to_imm8() { + // See ARM manual Table C2-1: A64 Floating-point modified immediate constants + assert_eq!(encode_f64_to_imm8(2.0), Some(0b0_000_0000)); + assert_eq!(encode_f64_to_imm8(4.0), Some(0b0_001_0000)); + assert_eq!(encode_f64_to_imm8(8.0), Some(0b0_010_0000)); + assert_eq!(encode_f64_to_imm8(16.0), Some(0b0_011_0000)); + assert_eq!(encode_f64_to_imm8(0.125), Some(0b0_100_0000)); + assert_eq!(encode_f64_to_imm8(0.25), Some(0b0_101_0000)); + assert_eq!(encode_f64_to_imm8(0.5), Some(0b0_110_0000)); + assert_eq!(encode_f64_to_imm8(1.0), Some(0b0_111_0000)); + + assert_eq!(encode_f64_to_imm8(2.125), Some(0b0_000_0001)); + assert_eq!(encode_f64_to_imm8(2.25), Some(0b0_000_0010)); + assert_eq!(encode_f64_to_imm8(2.375), Some(0b0_000_0011)); + assert_eq!(encode_f64_to_imm8(2.5), Some(0b0_000_0100)); + assert_eq!(encode_f64_to_imm8(2.625), Some(0b0_000_0101)); + assert_eq!(encode_f64_to_imm8(2.75), Some(0b0_000_0110)); + assert_eq!(encode_f64_to_imm8(2.875), Some(0b0_000_0111)); + assert_eq!(encode_f64_to_imm8(3.0), Some(0b0_000_1000)); + assert_eq!(encode_f64_to_imm8(3.125), Some(0b0_000_1001)); + assert_eq!(encode_f64_to_imm8(3.25), Some(0b0_000_1010)); + assert_eq!(encode_f64_to_imm8(3.375), Some(0b0_000_1011)); + assert_eq!(encode_f64_to_imm8(3.5), Some(0b0_000_1100)); + assert_eq!(encode_f64_to_imm8(3.625), Some(0b0_000_1101)); + assert_eq!(encode_f64_to_imm8(3.75), Some(0b0_000_1110)); + assert_eq!(encode_f64_to_imm8(3.875), Some(0b0_000_1111)); + + assert_eq!(encode_f64_to_imm8(-2.0), Some(0b1_000_0000)); + assert_eq!(encode_f64_to_imm8(-0.25), Some(0b1_101_0000)); + assert_eq!(encode_f64_to_imm8(-2.5), Some(0b1_000_0100)); + assert_eq!(encode_f64_to_imm8(-3.375), Some(0b1_000_1011)); + + assert_eq!(encode_f64_to_imm8(1.9375), Some(0b0_111_1111)); + assert_eq!(encode_f64_to_imm8(-1.9375), Some(0b1_111_1111)); + + assert_eq!(encode_f64_to_imm8(23.0), Some(0b0_011_0111)); + assert_eq!(encode_f64_to_imm8(-23.0), Some(0b1_011_0111)); + + assert_eq!(encode_f64_to_imm8(0.0), None); + assert_eq!(encode_f64_to_imm8(-0.0), None); + assert_eq!(encode_f64_to_imm8(32.0), None); + assert_eq!(encode_f64_to_imm8(-32.0), None); + assert_eq!(encode_f64_to_imm8(0.0625), None); + assert_eq!(encode_f64_to_imm8(-0.0625), None); + assert_eq!(encode_f64_to_imm8(0.3), None); + assert_eq!(encode_f64_to_imm8(-0.3), None); + } + + #[test] + fn test_fmov_freg_imm8() { + disassembler_test!( + |buf: &mut Vec<'_, u8>, ftype: FloatWidth, dst: AArch64FloatReg, imm: f32| { + // We need to encode the float immediate to 8 bits first. + let encoded = match ftype { + FloatWidth::F32 => encode_f32_to_imm8(imm), + FloatWidth::F64 => encode_f64_to_imm8(imm as f64), + }; + fmov_freg_imm8(buf, ftype, dst, encoded.unwrap()) + }, + |ftype: FloatWidth, reg: AArch64FloatReg, imm: f32| format!( + "fmov {}, #{:.8}", + reg.capstone_string(ftype), + imm + ), + ALL_FLOAT_TYPES, + ALL_FLOAT_REGS, + [ + // These are all of the possible values that can be encoded in an 8-bit float immediate. + // See ARM manual Table C2-1: A64 Floating-point modified immediate constants. + 2.0, 4.0, 8.0, 16.0, 0.125, 0.25, 0.5, 1.0, 2.125, 4.25, 8.5, 17.0, 0.1328125, + 0.265625, 0.53125, 1.0625, 2.25, 4.5, 9.0, 18.0, 0.140625, 0.28125, 0.5625, 1.125, + 2.375, 4.75, 9.5, 19.0, 0.1484375, 0.296875, 0.59375, 1.1875, 2.5, 5.0, 10.0, 20.0, + 0.15625, 0.3125, 0.625, 1.25, 2.625, 5.25, 10.5, 21.0, 0.1640625, 0.328125, + 0.65625, 1.3125, 2.75, 5.5, 11.0, 22.0, 0.171875, 0.34375, 0.6875, 1.375, 2.875, + 5.75, 11.5, 23.0, 0.1796875, 0.359375, 0.71875, 1.4375, 3.0, 6.0, 12.0, 24.0, + 0.1875, 0.375, 0.75, 1.5, 3.125, 6.25, 12.5, 25.0, 0.1953125, 0.390625, 0.78125, + 1.5625, 3.25, 6.5, 13.0, 26.0, 0.203125, 0.40625, 0.8125, 1.625, 3.375, 6.75, 13.5, + 27.0, 0.2109375, 0.421875, 0.84375, 1.6875, 3.5, 7.0, 14.0, 28.0, 0.21875, 0.4375, + 0.875, 1.75, 3.625, 7.25, 14.5, 29.0, 0.2265625, 0.453125, 0.90625, 1.8125, 3.75, + 7.5, 15.0, 30.0, 0.234375, 0.46875, 0.9375, 1.875, 3.875, 7.75, 15.5, 31.0, + 0.2421875, 0.484375, 0.96875, 1.9375, -2.0, -4.0, -8.0, -16.0, -0.125, -0.25, -0.5, + -1.0, -2.125, -4.25, -8.5, -17.0, -0.1328125, -0.265625, -0.53125, -1.0625, -2.25, + -4.5, -9.0, -18.0, -0.140625, -0.28125, -0.5625, -1.125, -2.375, -4.75, -9.5, + -19.0, -0.1484375, -0.296875, -0.59375, -1.1875, -2.5, -5.0, -10.0, -20.0, + -0.15625, -0.3125, -0.625, -1.25, -2.625, -5.25, -10.5, -21.0, -0.1640625, + -0.328125, -0.65625, -1.3125, -2.75, -5.5, -11.0, -22.0, -0.171875, -0.34375, + -0.6875, -1.375, -2.875, -5.75, -11.5, -23.0, -0.1796875, -0.359375, -0.71875, + -1.4375, -3.0, -6.0, -12.0, -24.0, -0.1875, -0.375, -0.75, -1.5, -3.125, -6.25, + -12.5, -25.0, -0.1953125, -0.390625, -0.78125, -1.5625, -3.25, -6.5, -13.0, -26.0, + -0.203125, -0.40625, -0.8125, -1.625, -3.375, -6.75, -13.5, -27.0, -0.2109375, + -0.421875, -0.84375, -1.6875, -3.5, -7.0, -14.0, -28.0, -0.21875, -0.4375, -0.875, + -1.75, -3.625, -7.25, -14.5, -29.0, -0.2265625, -0.453125, -0.90625, -1.8125, + -3.75, -7.5, -15.0, -30.0, -0.234375, -0.46875, -0.9375, -1.875, -3.875, -7.75, + -15.5, -31.0, -0.2421875, -0.484375, -0.96875, -1.9375, + ] + ); + } + + #[test] + fn test_fmul_freg_freg_freg() { + disassembler_test!( + fmul_freg_freg_freg, + |ftype: FloatWidth, + reg1: AArch64FloatReg, + reg2: AArch64FloatReg, + reg3: AArch64FloatReg| format!( + "fmul {}, {}, {}", + reg1.capstone_string(ftype), + reg2.capstone_string(ftype), + reg3.capstone_string(ftype) + ), + ALL_FLOAT_TYPES, + ALL_FLOAT_REGS, + ALL_FLOAT_REGS, + ALL_FLOAT_REGS + ); + } + + #[test] + fn test_fsqrt_freg_freg() { + disassembler_test!( + fsqrt_freg_freg, + |ftype: FloatWidth, reg1: AArch64FloatReg, reg2: AArch64FloatReg| format!( + "fsqrt {}, {}", + reg1.capstone_string(ftype), + reg2.capstone_string(ftype) + ), + ALL_FLOAT_TYPES, + ALL_FLOAT_REGS, + ALL_FLOAT_REGS + ); + } + + #[test] + fn test_movi_freg_zero() { + disassembler_test!( + movi_freg_zero, + |reg: AArch64FloatReg| format!( + "movi {}, #0000000000000000", + reg.capstone_string(FloatWidth::F64) + ), + ALL_FLOAT_REGS + ); + } + + #[test] + fn test_scvtf_freg_reg64() { + disassembler_test!( + scvtf_freg_reg64, + |ftype: FloatWidth, reg1: AArch64FloatReg, reg2: AArch64GeneralReg| format!( + "scvtf {}, {}", + reg1.capstone_string(ftype), + reg2.capstone_string(UsesZR) + ), + ALL_FLOAT_TYPES, + ALL_FLOAT_REGS, + ALL_GENERAL_REGS + ); + } + + #[test] + fn test_sign_extend() { + disassembler_test!( + sign_extend, + |w, reg1: AArch64GeneralReg, reg2: AArch64GeneralReg| format!( + "{} {}, {}", + match w { + RegisterWidth::W8 => "sxtb", + RegisterWidth::W16 => "sxth", + RegisterWidth::W32 => "sxtw", + RegisterWidth::W64 => "mov", + }, + reg1.capstone_string(UsesZR), + match w { + RegisterWidth::W8 => reg2.capstone_string_32bit(UsesZR), + RegisterWidth::W16 => reg2.capstone_string_32bit(UsesZR), + RegisterWidth::W32 => reg2.capstone_string_32bit(UsesZR), + RegisterWidth::W64 => reg2.capstone_string(UsesZR), + } + ), + ALL_REGISTER_WIDTHS, + ALL_GENERAL_REGS, + ALL_GENERAL_REGS + ); + } + + #[test] + fn test_zero_extend() { + disassembler_test!( + zero_extend, + |w, reg1: AArch64GeneralReg, reg2: AArch64GeneralReg| format!( + "{} {}, {}", + match w { + RegisterWidth::W8 => "uxtb", + RegisterWidth::W16 => "uxth", + RegisterWidth::W32 => "mov", + RegisterWidth::W64 => "mov", + }, + match w { + RegisterWidth::W8 => reg1.capstone_string_32bit(UsesZR), + RegisterWidth::W16 => reg1.capstone_string_32bit(UsesZR), + RegisterWidth::W32 => reg1.capstone_string(UsesZR), + RegisterWidth::W64 => reg1.capstone_string(UsesZR), + }, + match w { + RegisterWidth::W8 => reg2.capstone_string_32bit(UsesZR), + RegisterWidth::W16 => reg2.capstone_string_32bit(UsesZR), + RegisterWidth::W32 => reg2.capstone_string(UsesZR), + RegisterWidth::W64 => reg2.capstone_string(UsesZR), + } + ), + ALL_REGISTER_WIDTHS, + ALL_GENERAL_REGS, + ALL_GENERAL_REGS + ); + } +} diff --git a/crates/compiler/gen_dev/src/generic64/disassembler_test_macro.rs b/crates/compiler/gen_dev/src/generic64/disassembler_test_macro.rs new file mode 100644 index 0000000000..a1dbd4b430 --- /dev/null +++ b/crates/compiler/gen_dev/src/generic64/disassembler_test_macro.rs @@ -0,0 +1,100 @@ +pub fn merge_instructions_without_line_numbers(instructions: capstone::Instructions) -> String { + instructions + .iter() + .map(|inst| { + inst.to_string() + .trim() + .split(' ') + .skip(1) + .map(|x| x.trim()) + .collect::>() + .join(" ") + }) + .collect::>() + .join("\n") +} + +#[macro_export] +macro_rules! disassembler_test { + // TODO: Not sure if there is a better way to merge these together, + // but I like the end use of this a lot better than the old tests. + ($assemble_fn: expr, $format_fn: expr) => {{ + use $crate::generic64::disassembler_test_macro::merge_instructions_without_line_numbers; + let arena = bumpalo::Bump::new(); + let (mut buf, cs) = setup_capstone_and_arena(&arena); + $assemble_fn(&mut buf); + let instructions = cs.disasm_all(&buf, 0).unwrap(); + assert_eq!( + $format_fn(), + merge_instructions_without_line_numbers(instructions) + ); + }}; + ($assemble_fn: expr, $format_fn: expr, $iter:expr) => {{ + use $crate::generic64::disassembler_test_macro::merge_instructions_without_line_numbers; + let arena = bumpalo::Bump::new(); + let (mut buf, cs) = setup_capstone_and_arena(&arena); + for i in $iter.iter() { + buf.clear(); + $assemble_fn(&mut buf, *i); + let instructions = cs.disasm_all(&buf, 0).unwrap(); + assert_eq!( + $format_fn(*i), + merge_instructions_without_line_numbers(instructions) + ); + } + }}; + ($assemble_fn: expr, $format_fn: expr, $iter:expr, $iter2:expr) => {{ + use $crate::generic64::disassembler_test_macro::merge_instructions_without_line_numbers; + let arena = bumpalo::Bump::new(); + let (mut buf, cs) = setup_capstone_and_arena(&arena); + for i in $iter.iter() { + for i2 in $iter2.iter() { + buf.clear(); + $assemble_fn(&mut buf, *i, *i2); + let instructions = cs.disasm_all(&buf, 0).unwrap(); + assert_eq!( + $format_fn(*i, *i2), + merge_instructions_without_line_numbers(instructions) + ); + } + } + }}; + ($assemble_fn: expr, $format_fn: expr, $iter:expr, $iter2:expr, $iter3:expr) => {{ + use $crate::generic64::disassembler_test_macro::merge_instructions_without_line_numbers; + let arena = bumpalo::Bump::new(); + let (mut buf, cs) = setup_capstone_and_arena(&arena); + for i in $iter.iter() { + for i2 in $iter2.iter() { + for i3 in $iter3.iter() { + buf.clear(); + $assemble_fn(&mut buf, *i, *i2, *i3); + let instructions = cs.disasm_all(&buf, 0).unwrap(); + assert_eq!( + $format_fn(*i, *i2, *i3), + merge_instructions_without_line_numbers(instructions) + ); + } + } + } + }}; + ($assemble_fn: expr, $format_fn: expr, $iter:expr, $iter2:expr, $iter3:expr, $iter4:expr) => {{ + use $crate::generic64::disassembler_test_macro::merge_instructions_without_line_numbers; + let arena = bumpalo::Bump::new(); + let (mut buf, cs) = setup_capstone_and_arena(&arena); + for i in $iter.iter() { + for i2 in $iter2.iter() { + for i3 in $iter3.iter() { + for i4 in $iter4.iter() { + buf.clear(); + $assemble_fn(&mut buf, *i, *i2, *i3, *i4); + let instructions = cs.disasm_all(&buf, 0).unwrap(); + assert_eq!( + $format_fn(*i, *i2, *i3, *i4), + merge_instructions_without_line_numbers(instructions) + ); + } + } + } + } + }}; +} diff --git a/crates/compiler/gen_dev/src/generic64/mod.rs b/crates/compiler/gen_dev/src/generic64/mod.rs new file mode 100644 index 0000000000..73a1146e2c --- /dev/null +++ b/crates/compiler/gen_dev/src/generic64/mod.rs @@ -0,0 +1,5441 @@ +use crate::{ + pointer_layouts, single_register_floats, single_register_int_builtins, + single_register_integers, Backend, Env, Relocation, +}; +use bumpalo::collections::{CollectIn, Vec}; +use roc_builtins::bitcode::{self, FloatWidth, IntWidth}; +use roc_collections::all::MutMap; +use roc_error_macros::{internal_error, todo_lambda_erasure}; +use roc_module::symbol::{Interns, ModuleId, Symbol}; +use roc_mono::code_gen_help::{CallerProc, CodeGenHelp, HelperOp}; +use roc_mono::ir::{ + BranchInfo, HigherOrderLowLevel, JoinPointId, ListLiteralElement, Literal, Param, ProcLayout, + SelfRecursive, Stmt, +}; +use roc_mono::layout::{ + Builtin, InLayout, LambdaName, Layout, LayoutIds, LayoutInterner, LayoutRepr, STLayoutInterner, + TagIdIntType, UnionLayout, +}; +use roc_mono::low_level::HigherOrder; +use roc_target::TargetInfo; +use std::marker::PhantomData; + +pub(crate) mod aarch64; +#[cfg(test)] +mod disassembler_test_macro; +pub(crate) mod storage; +pub(crate) mod x86_64; + +use storage::{RegStorage, StorageManager}; + +// TODO: on all number functions double check and deal with over/underflow. + +// NOTE: must fit in 27 bits and aligned to 4 for aarch64 +const JUMP_PLACEHOLDER: i32 = 0x0011_1100; + +#[derive(Debug, Clone, Copy)] +pub enum RegisterWidth { + W8 = 0b00, + W16 = 0b01, + W32 = 0b10, + W64 = 0b11, +} + +impl RegisterWidth { + fn try_from_layout(layout: LayoutRepr) -> Option { + match layout { + LayoutRepr::BOOL | LayoutRepr::I8 | LayoutRepr::U8 => Some(RegisterWidth::W8), + LayoutRepr::I16 | LayoutRepr::U16 => Some(RegisterWidth::W16), + LayoutRepr::U32 | LayoutRepr::I32 => Some(RegisterWidth::W32), + LayoutRepr::I64 | LayoutRepr::U64 => Some(RegisterWidth::W64), + _ => None, + } + } +} + +pub trait CallConv>: + Sized + Copy +{ + const BASE_PTR_REG: GeneralReg; + const STACK_PTR_REG: GeneralReg; + + const GENERAL_PARAM_REGS: &'static [GeneralReg]; + const GENERAL_RETURN_REGS: &'static [GeneralReg]; + const GENERAL_DEFAULT_FREE_REGS: &'static [GeneralReg]; + + const FLOAT_PARAM_REGS: &'static [FloatReg]; + const FLOAT_RETURN_REGS: &'static [FloatReg]; + const FLOAT_DEFAULT_FREE_REGS: &'static [FloatReg]; + + const SHADOW_SPACE_SIZE: u8; + + fn general_callee_saved(reg: &GeneralReg) -> bool; + #[inline(always)] + fn general_caller_saved(reg: &GeneralReg) -> bool { + !Self::general_callee_saved(reg) + } + fn float_callee_saved(reg: &FloatReg) -> bool; + #[inline(always)] + fn float_caller_saved(reg: &FloatReg) -> bool { + !Self::float_callee_saved(reg) + } + + fn setup_stack( + buf: &mut Vec<'_, u8>, + saved_general_regs: &[GeneralReg], + saved_float_regs: &[FloatReg], + requested_stack_size: i32, + fn_call_stack_size: i32, + ) -> i32; + fn cleanup_stack( + buf: &mut Vec<'_, u8>, + general_saved_regs: &[GeneralReg], + float_saved_regs: &[FloatReg], + aligned_stack_size: i32, + fn_call_stack_size: i32, + ); + + /// load_args updates the storage manager to know where every arg is stored. + fn load_args<'a>( + buf: &mut Vec<'a, u8>, + storage_manager: &mut StorageManager<'a, '_, GeneralReg, FloatReg, ASM, Self>, + layout_interner: &mut STLayoutInterner<'a>, + args: &'a [(InLayout<'a>, Symbol)], + // ret_layout is needed because if it is a complex type, we pass a pointer as the first arg. + ret_layout: &InLayout<'a>, + ); + + /// store_args stores the args in registers and on the stack for function calling. + /// It also updates the amount of temporary stack space needed in the storage manager. + fn store_args<'a>( + buf: &mut Vec<'a, u8>, + storage_manager: &mut StorageManager<'a, '_, GeneralReg, FloatReg, ASM, Self>, + layout_interner: &mut STLayoutInterner<'a>, + dst: &Symbol, + args: &[Symbol], + arg_layouts: &[InLayout<'a>], + // ret_layout is needed because if it is a complex type, we pass a pointer as the first arg. + ret_layout: &InLayout<'a>, + ); + + /// return_complex_symbol returns the specified complex/non-primative symbol. + /// It uses the layout to determine how the data should be returned. + fn return_complex_symbol<'a>( + buf: &mut Vec<'a, u8>, + storage_manager: &mut StorageManager<'a, '_, GeneralReg, FloatReg, ASM, Self>, + layout_interner: &mut STLayoutInterner<'a>, + sym: &Symbol, + layout: &InLayout<'a>, + ); + + /// load_returned_complex_symbol loads a complex symbol that was returned from a function call. + /// It uses the layout to determine how the data should be loaded into the symbol. + fn load_returned_complex_symbol<'a>( + buf: &mut Vec<'a, u8>, + storage_manager: &mut StorageManager<'a, '_, GeneralReg, FloatReg, ASM, Self>, + layout_interner: &mut STLayoutInterner<'a>, + sym: &Symbol, + layout: &InLayout<'a>, + ); + + fn setjmp(buf: &mut Vec<'_, u8>); + fn longjmp(buf: &mut Vec<'_, u8>); + fn roc_panic(buf: &mut Vec<'_, u8>, relocs: &mut Vec<'_, Relocation>); +} + +pub enum CompareOperation { + LessThan, + LessThanOrEqual, + GreaterThan, + GreaterThanOrEqual, +} + +/// Assembler contains calls to the backend assembly generator. +/// These calls do not necessarily map directly to a single assembly instruction. +/// They are higher level in cases where an instruction would not be common and shared between multiple architectures. +/// Thus, some backends will need to use mulitiple instructions to preform a single one of this calls. +/// Generally, I prefer explicit sources, as opposed to dst being one of the sources. Ex: `x = x + y` would be `add x, x, y` instead of `add x, y`. +/// dst should always come before sources. +pub trait Assembler: Sized + Copy { + fn abs_reg64_reg64(buf: &mut Vec<'_, u8>, dst: GeneralReg, src: GeneralReg); + fn abs_freg64_freg64( + buf: &mut Vec<'_, u8>, + relocs: &mut Vec<'_, Relocation>, + dst: FloatReg, + src: FloatReg, + ); + + fn add_reg64_reg64_imm32(buf: &mut Vec<'_, u8>, dst: GeneralReg, src1: GeneralReg, imm32: i32); + fn add_freg32_freg32_freg32( + buf: &mut Vec<'_, u8>, + dst: FloatReg, + src1: FloatReg, + src2: FloatReg, + ); + fn add_freg64_freg64_freg64( + buf: &mut Vec<'_, u8>, + dst: FloatReg, + src1: FloatReg, + src2: FloatReg, + ); + fn add_reg64_reg64_reg64( + buf: &mut Vec<'_, u8>, + dst: GeneralReg, + src1: GeneralReg, + src2: GeneralReg, + ); + + fn and_reg64_reg64_reg64( + buf: &mut Vec<'_, u8>, + dst: GeneralReg, + src1: GeneralReg, + src2: GeneralReg, + ); + + fn or_reg64_reg64_reg64( + buf: &mut Vec<'_, u8>, + dst: GeneralReg, + src1: GeneralReg, + src2: GeneralReg, + ); + + fn xor_reg64_reg64_reg64( + buf: &mut Vec<'_, u8>, + dst: GeneralReg, + src1: GeneralReg, + src2: GeneralReg, + ); + + fn shl_reg64_reg64_reg64<'a, ASM, CC>( + buf: &mut Vec<'a, u8>, + storage_manager: &mut StorageManager<'a, '_, GeneralReg, FloatReg, ASM, CC>, + dst: GeneralReg, + src1: GeneralReg, + src2: GeneralReg, + ) where + ASM: Assembler, + CC: CallConv; + + fn shr_reg64_reg64_reg64<'a, ASM, CC>( + buf: &mut Vec<'a, u8>, + storage_manager: &mut StorageManager<'a, '_, GeneralReg, FloatReg, ASM, CC>, + dst: GeneralReg, + src1: GeneralReg, + src2: GeneralReg, + ) where + ASM: Assembler, + CC: CallConv; + + fn sar_reg64_reg64_reg64<'a, ASM, CC>( + buf: &mut Vec<'a, u8>, + storage_manager: &mut StorageManager<'a, '_, GeneralReg, FloatReg, ASM, CC>, + dst: GeneralReg, + src1: GeneralReg, + src2: GeneralReg, + ) where + ASM: Assembler, + CC: CallConv; + + fn call(buf: &mut Vec<'_, u8>, relocs: &mut Vec<'_, Relocation>, fn_name: String); + + fn function_pointer( + buf: &mut Vec<'_, u8>, + relocs: &mut Vec<'_, Relocation>, + fn_name: String, + dst: GeneralReg, + ); + + fn data_pointer( + buf: &mut Vec<'_, u8>, + relocs: &mut Vec<'_, Relocation>, + fn_name: String, + dst: GeneralReg, + ); + + /// Jumps by an offset of offset bytes unconditionally. + /// It should always generate the same number of bytes to enable replacement if offset changes. + /// It returns the base offset to calculate the jump from (generally the instruction after the jump). + fn jmp_imm32(buf: &mut Vec<'_, u8>, offset: i32) -> usize; + + /// Updates a jump instruction to a new offset and returns the number of bytes written. + fn update_jmp_imm32_offset( + buf: &mut Vec<'_, u8>, + jmp_location: u64, + base_offset: u64, + target_offset: u64, + ) { + let old_buf_len = buf.len(); + + // write the jmp at the back of buf + let jmp_offset = target_offset as i32 - base_offset as i32; + Self::jmp_imm32(buf, jmp_offset); + + // move the new jmp instruction into position + buf.copy_within(old_buf_len.., jmp_location as usize); + + // wipe the jmp we created at the end + buf.truncate(old_buf_len) + } + + fn tail_call(buf: &mut Vec<'_, u8>) -> u64; + + /// Jumps by an offset of offset bytes if reg is not equal to imm. + /// It should always generate the same number of bytes to enable replacement if offset changes. + /// It returns the base offset to calculate the jump from (generally the instruction after the jump). + fn jne_reg64_imm64_imm32<'a, ASM, CC>( + buf: &mut Vec<'a, u8>, + storage_manager: &mut StorageManager<'a, '_, GeneralReg, FloatReg, ASM, CC>, + reg: GeneralReg, + imm: u64, + offset: i32, + ) -> usize + where + ASM: Assembler, + CC: CallConv; + + fn mov_freg32_imm32( + buf: &mut Vec<'_, u8>, + relocs: &mut Vec<'_, Relocation>, + dst: FloatReg, + imm: f32, + ); + fn mov_freg64_imm64( + buf: &mut Vec<'_, u8>, + relocs: &mut Vec<'_, Relocation>, + dst: FloatReg, + imm: f64, + ); + fn mov_reg64_imm64(buf: &mut Vec<'_, u8>, dst: GeneralReg, imm: i64); + fn mov_freg64_freg64(buf: &mut Vec<'_, u8>, dst: FloatReg, src: FloatReg); + + fn mov_reg32_freg32(buf: &mut Vec<'_, u8>, dst: GeneralReg, src: FloatReg); + fn mov_reg64_freg64(buf: &mut Vec<'_, u8>, dst: GeneralReg, src: FloatReg); + + fn mov_freg32_reg32(buf: &mut Vec<'_, u8>, dst: FloatReg, src: GeneralReg); + fn mov_freg64_reg64(buf: &mut Vec<'_, u8>, dst: FloatReg, src: GeneralReg); + + fn mov_reg_reg( + buf: &mut Vec<'_, u8>, + register_width: RegisterWidth, + dst: GeneralReg, + src: GeneralReg, + ); + fn mov_reg64_reg64(buf: &mut Vec<'_, u8>, dst: GeneralReg, src: GeneralReg) { + Self::mov_reg_reg(buf, RegisterWidth::W64, dst, src); + } + fn mov_reg32_reg32(buf: &mut Vec<'_, u8>, dst: GeneralReg, src: GeneralReg) { + Self::mov_reg_reg(buf, RegisterWidth::W32, dst, src); + } + fn mov_reg16_reg16(buf: &mut Vec<'_, u8>, dst: GeneralReg, src: GeneralReg) { + Self::mov_reg_reg(buf, RegisterWidth::W16, dst, src); + } + fn mov_reg8_reg8(buf: &mut Vec<'_, u8>, dst: GeneralReg, src: GeneralReg) { + Self::mov_reg_reg(buf, RegisterWidth::W8, dst, src); + } + + // move with sign extension + fn movsx_reg_reg( + buf: &mut Vec<'_, u8>, + input_width: RegisterWidth, + dst: GeneralReg, + src: GeneralReg, + ); + + // move with zero extension + fn movzx_reg_reg( + buf: &mut Vec<'_, u8>, + input_width: RegisterWidth, + dst: GeneralReg, + src: GeneralReg, + ); + + // base32 is similar to stack based instructions but they reference the base/frame pointer. + fn mov_freg64_base32(buf: &mut Vec<'_, u8>, dst: FloatReg, offset: i32); + + fn mov_reg_base32( + buf: &mut Vec<'_, u8>, + register_width: RegisterWidth, + dst: GeneralReg, + offset: i32, + ); + + fn mov_reg64_base32(buf: &mut Vec<'_, u8>, dst: GeneralReg, offset: i32) { + Self::mov_reg_base32(buf, RegisterWidth::W64, dst, offset) + } + fn mov_reg32_base32(buf: &mut Vec<'_, u8>, dst: GeneralReg, offset: i32) { + Self::mov_reg_base32(buf, RegisterWidth::W32, dst, offset) + } + fn mov_reg16_base32(buf: &mut Vec<'_, u8>, dst: GeneralReg, offset: i32) { + Self::mov_reg_base32(buf, RegisterWidth::W16, dst, offset) + } + fn mov_reg8_base32(buf: &mut Vec<'_, u8>, dst: GeneralReg, offset: i32) { + Self::mov_reg_base32(buf, RegisterWidth::W8, dst, offset) + } + + fn mov_base32_freg64(buf: &mut Vec<'_, u8>, offset: i32, src: FloatReg); + fn mov_base32_freg32(buf: &mut Vec<'_, u8>, offset: i32, src: FloatReg); + + fn mov_base32_reg( + buf: &mut Vec<'_, u8>, + register_width: RegisterWidth, + offset: i32, + src: GeneralReg, + ); + + fn mov_base32_reg64(buf: &mut Vec<'_, u8>, offset: i32, src: GeneralReg) { + Self::mov_base32_reg(buf, RegisterWidth::W64, offset, src) + } + fn mov_base32_reg32(buf: &mut Vec<'_, u8>, offset: i32, src: GeneralReg) { + Self::mov_base32_reg(buf, RegisterWidth::W32, offset, src) + } + fn mov_base32_reg16(buf: &mut Vec<'_, u8>, offset: i32, src: GeneralReg) { + Self::mov_base32_reg(buf, RegisterWidth::W16, offset, src) + } + fn mov_base32_reg8(buf: &mut Vec<'_, u8>, offset: i32, src: GeneralReg) { + Self::mov_base32_reg(buf, RegisterWidth::W8, offset, src) + } + + // move from memory (a pointer) to register + fn mov_reg_mem_offset32( + buf: &mut Vec<'_, u8>, + register_width: RegisterWidth, + dst: GeneralReg, + src: GeneralReg, + offset: i32, + ); + + fn mov_reg64_mem64_offset32( + buf: &mut Vec<'_, u8>, + dst: GeneralReg, + src: GeneralReg, + offset: i32, + ) { + Self::mov_reg_mem_offset32(buf, RegisterWidth::W64, dst, src, offset) + } + fn mov_reg32_mem32_offset32( + buf: &mut Vec<'_, u8>, + dst: GeneralReg, + src: GeneralReg, + offset: i32, + ) { + Self::mov_reg_mem_offset32(buf, RegisterWidth::W32, dst, src, offset) + } + fn mov_reg16_mem16_offset32( + buf: &mut Vec<'_, u8>, + dst: GeneralReg, + src: GeneralReg, + offset: i32, + ) { + Self::mov_reg_mem_offset32(buf, RegisterWidth::W16, dst, src, offset) + } + fn mov_reg8_mem8_offset32( + buf: &mut Vec<'_, u8>, + dst: GeneralReg, + src: GeneralReg, + offset: i32, + ) { + Self::mov_reg_mem_offset32(buf, RegisterWidth::W8, dst, src, offset) + } + + fn mov_freg64_mem64_offset32( + buf: &mut Vec<'_, u8>, + dst: FloatReg, + src: GeneralReg, + offset: i32, + ); + fn mov_freg32_mem32_offset32( + buf: &mut Vec<'_, u8>, + dst: FloatReg, + src: GeneralReg, + offset: i32, + ); + + // move from register to memory + fn mov_mem_offset32_reg( + buf: &mut Vec<'_, u8>, + register_width: RegisterWidth, + dst: GeneralReg, + offset: i32, + src: GeneralReg, + ); + + fn mov_mem64_offset32_reg64( + buf: &mut Vec<'_, u8>, + dst: GeneralReg, + offset: i32, + src: GeneralReg, + ) { + Self::mov_mem_offset32_reg(buf, RegisterWidth::W64, dst, offset, src) + } + fn mov_mem32_offset32_reg32( + buf: &mut Vec<'_, u8>, + dst: GeneralReg, + offset: i32, + src: GeneralReg, + ) { + Self::mov_mem_offset32_reg(buf, RegisterWidth::W32, dst, offset, src) + } + fn mov_mem16_offset32_reg16( + buf: &mut Vec<'_, u8>, + dst: GeneralReg, + offset: i32, + src: GeneralReg, + ) { + Self::mov_mem_offset32_reg(buf, RegisterWidth::W16, dst, offset, src) + } + fn mov_mem8_offset32_reg8( + buf: &mut Vec<'_, u8>, + dst: GeneralReg, + offset: i32, + src: GeneralReg, + ) { + Self::mov_mem_offset32_reg(buf, RegisterWidth::W8, dst, offset, src) + } + + fn movesd_mem64_offset32_freg64( + buf: &mut Vec<'_, u8>, + ptr: GeneralReg, + offset: i32, + src: FloatReg, + ); + + /// Sign extends the data at `offset` with `size` as it copies it to `dst` + /// size must be less than or equal to 8. + fn movsx_reg_base32( + buf: &mut Vec<'_, u8>, + register_width: RegisterWidth, + dst: GeneralReg, + offset: i32, + ); + + fn movzx_reg_base32( + buf: &mut Vec<'_, u8>, + register_width: RegisterWidth, + dst: GeneralReg, + offset: i32, + ); + + fn mov_mem64_offset32_freg64( + buf: &mut Vec<'_, u8>, + dst: GeneralReg, + offset: i32, + src: FloatReg, + ); + fn mov_freg64_stack32(buf: &mut Vec<'_, u8>, dst: FloatReg, offset: i32); + fn mov_reg64_stack32(buf: &mut Vec<'_, u8>, dst: GeneralReg, offset: i32); + fn mov_stack32_freg64(buf: &mut Vec<'_, u8>, offset: i32, src: FloatReg); + + fn mov_stack32_reg( + buf: &mut Vec<'_, u8>, + register_width: RegisterWidth, + offset: i32, + src: GeneralReg, + ); + + fn mov_stack32_reg64(buf: &mut Vec<'_, u8>, offset: i32, src: GeneralReg) { + Self::mov_stack32_reg(buf, RegisterWidth::W64, offset, src) + } + fn mov_stack32_reg32(buf: &mut Vec<'_, u8>, offset: i32, src: GeneralReg) { + Self::mov_stack32_reg(buf, RegisterWidth::W32, offset, src) + } + fn mov_stack32_reg16(buf: &mut Vec<'_, u8>, offset: i32, src: GeneralReg) { + Self::mov_stack32_reg(buf, RegisterWidth::W16, offset, src) + } + fn mov_stack32_reg8(buf: &mut Vec<'_, u8>, offset: i32, src: GeneralReg) { + Self::mov_stack32_reg(buf, RegisterWidth::W8, offset, src) + } + + fn sqrt_freg64_freg64(buf: &mut Vec<'_, u8>, dst: FloatReg, src: FloatReg); + fn sqrt_freg32_freg32(buf: &mut Vec<'_, u8>, dst: FloatReg, src: FloatReg); + + fn neg_reg64_reg64(buf: &mut Vec<'_, u8>, dst: GeneralReg, src: GeneralReg); + fn mul_freg32_freg32_freg32( + buf: &mut Vec<'_, u8>, + dst: FloatReg, + src1: FloatReg, + src2: FloatReg, + ); + fn mul_freg64_freg64_freg64( + buf: &mut Vec<'_, u8>, + dst: FloatReg, + src1: FloatReg, + src2: FloatReg, + ); + fn div_freg32_freg32_freg32( + buf: &mut Vec<'_, u8>, + dst: FloatReg, + src1: FloatReg, + src2: FloatReg, + ); + fn div_freg64_freg64_freg64( + buf: &mut Vec<'_, u8>, + dst: FloatReg, + src1: FloatReg, + src2: FloatReg, + ); + fn imul_reg64_reg64_reg64( + buf: &mut Vec<'_, u8>, + dst: GeneralReg, + src1: GeneralReg, + src2: GeneralReg, + ); + fn umul_reg64_reg64_reg64<'a, ASM, CC>( + buf: &mut Vec<'a, u8>, + storage_manager: &mut StorageManager<'a, '_, GeneralReg, FloatReg, ASM, CC>, + dst: GeneralReg, + src1: GeneralReg, + src2: GeneralReg, + ) where + ASM: Assembler, + CC: CallConv; + + fn idiv_reg64_reg64_reg64<'a, ASM, CC>( + buf: &mut Vec<'a, u8>, + storage_manager: &mut StorageManager<'a, '_, GeneralReg, FloatReg, ASM, CC>, + dst: GeneralReg, + src1: GeneralReg, + src2: GeneralReg, + ) where + ASM: Assembler, + CC: CallConv; + + fn udiv_reg64_reg64_reg64<'a, ASM, CC>( + buf: &mut Vec<'a, u8>, + storage_manager: &mut StorageManager<'a, '_, GeneralReg, FloatReg, ASM, CC>, + dst: GeneralReg, + src1: GeneralReg, + src2: GeneralReg, + ) where + ASM: Assembler, + CC: CallConv; + + fn irem_reg64_reg64_reg64<'a, ASM, CC>( + buf: &mut Vec<'a, u8>, + storage_manager: &mut StorageManager<'a, '_, GeneralReg, FloatReg, ASM, CC>, + dst: GeneralReg, + src1: GeneralReg, + src2: GeneralReg, + ) where + ASM: Assembler, + CC: CallConv; + + fn urem_reg64_reg64_reg64<'a, ASM, CC>( + buf: &mut Vec<'a, u8>, + storage_manager: &mut StorageManager<'a, '_, GeneralReg, FloatReg, ASM, CC>, + dst: GeneralReg, + src1: GeneralReg, + src2: GeneralReg, + ) where + ASM: Assembler, + CC: CallConv; + + fn sub_reg64_reg64_imm32(buf: &mut Vec<'_, u8>, dst: GeneralReg, src1: GeneralReg, imm32: i32); + fn sub_reg64_reg64_reg64( + buf: &mut Vec<'_, u8>, + dst: GeneralReg, + src1: GeneralReg, + src2: GeneralReg, + ); + + fn eq_reg_reg_reg( + buf: &mut Vec<'_, u8>, + register_width: RegisterWidth, + dst: GeneralReg, + src1: GeneralReg, + src2: GeneralReg, + ); + + fn eq_reg64_reg64_reg64( + buf: &mut Vec<'_, u8>, + dst: GeneralReg, + src1: GeneralReg, + src2: GeneralReg, + ) { + Self::eq_reg_reg_reg(buf, RegisterWidth::W64, dst, src1, src2) + } + + fn neq_reg_reg_reg( + buf: &mut Vec<'_, u8>, + register_width: RegisterWidth, + dst: GeneralReg, + src1: GeneralReg, + src2: GeneralReg, + ); + + fn signed_compare_reg64( + buf: &mut Vec<'_, u8>, + register_width: RegisterWidth, + operation: CompareOperation, + dst: GeneralReg, + src1: GeneralReg, + src2: GeneralReg, + ); + + fn unsigned_compare_reg64( + buf: &mut Vec<'_, u8>, + register_width: RegisterWidth, + operation: CompareOperation, + dst: GeneralReg, + src1: GeneralReg, + src2: GeneralReg, + ); + + fn eq_freg_freg_reg64( + buf: &mut Vec<'_, u8>, + dst: GeneralReg, + src1: FloatReg, + src2: FloatReg, + width: FloatWidth, + ); + + fn neq_freg_freg_reg64( + buf: &mut Vec<'_, u8>, + dst: GeneralReg, + src1: FloatReg, + src2: FloatReg, + width: FloatWidth, + ); + + fn cmp_freg_freg_reg64( + buf: &mut Vec<'_, u8>, + dst: GeneralReg, + src1: FloatReg, + src2: FloatReg, + width: FloatWidth, + operation: CompareOperation, + ); + + fn is_nan_freg_reg64(buf: &mut Vec<'_, u8>, dst: GeneralReg, src: FloatReg, width: FloatWidth); + + fn to_float_freg32_reg64(buf: &mut Vec<'_, u8>, dst: FloatReg, src: GeneralReg); + + fn to_float_freg64_reg64(buf: &mut Vec<'_, u8>, dst: FloatReg, src: GeneralReg); + + fn to_float_freg32_freg64(buf: &mut Vec<'_, u8>, dst: FloatReg, src: FloatReg); + + fn to_float_freg64_freg32(buf: &mut Vec<'_, u8>, dst: FloatReg, src: FloatReg); + + fn set_if_overflow(buf: &mut Vec<'_, u8>, dst: GeneralReg); + + fn ret(buf: &mut Vec<'_, u8>); +} + +pub trait RegTrait: + Copy + PartialEq + Eq + std::hash::Hash + std::fmt::Debug + std::fmt::Display + 'static +{ + fn value(&self) -> u8; +} + +pub struct Backend64Bit< + 'a, + 'r, + GeneralReg: RegTrait, + FloatReg: RegTrait, + ASM: Assembler, + CC: CallConv, +> { + // TODO: A number of the uses of MutMap could probably be some form of linear mutmap + // They are likely to be small enough that it is faster to use a vec and linearly scan it or keep it sorted and binary search. + phantom_asm: PhantomData, + phantom_cc: PhantomData, + env: &'r Env<'a>, + layout_interner: &'r mut STLayoutInterner<'a>, + interns: &'r mut Interns, + helper_proc_gen: CodeGenHelp<'a>, + helper_proc_symbols: Vec<'a, (Symbol, ProcLayout<'a>)>, + caller_procs: Vec<'a, CallerProc<'a>>, + buf: Vec<'a, u8>, + relocs: Vec<'a, Relocation>, + proc_name: Option, + is_self_recursive: Option, + + last_seen_map: MutMap>, + layout_map: MutMap>, + free_map: MutMap<*const Stmt<'a>, Vec<'a, Symbol>>, + + literal_map: MutMap, *const InLayout<'a>)>, + join_map: MutMap>, + + storage_manager: StorageManager<'a, 'r, GeneralReg, FloatReg, ASM, CC>, +} + +/// new creates a new backend that will output to the specific Object. +pub fn new_backend_64bit< + 'a, + 'r, + GeneralReg: RegTrait, + FloatReg: RegTrait, + ASM: Assembler, + CC: CallConv, +>( + env: &'r Env<'a>, + target_info: TargetInfo, + interns: &'r mut Interns, + layout_interner: &'r mut STLayoutInterner<'a>, +) -> Backend64Bit<'a, 'r, GeneralReg, FloatReg, ASM, CC> { + Backend64Bit { + phantom_asm: PhantomData, + phantom_cc: PhantomData, + env, + interns, + layout_interner, + helper_proc_gen: CodeGenHelp::new(env.arena, target_info, env.module_id), + helper_proc_symbols: bumpalo::vec![in env.arena], + caller_procs: bumpalo::vec![in env.arena], + proc_name: None, + is_self_recursive: None, + buf: bumpalo::vec![in env.arena], + relocs: bumpalo::vec![in env.arena], + last_seen_map: MutMap::default(), + layout_map: MutMap::default(), + free_map: MutMap::default(), + literal_map: MutMap::default(), + join_map: MutMap::default(), + storage_manager: storage::new_storage_manager(env, target_info), + } +} + +macro_rules! quadword_and_smaller { + () => { + IntWidth::I64 + | IntWidth::U64 + | IntWidth::I32 + | IntWidth::U32 + | IntWidth::I16 + | IntWidth::U16 + | IntWidth::I8 + | IntWidth::U8 + }; +} + +impl< + 'a, + 'r, + GeneralReg: RegTrait, + FloatReg: RegTrait, + ASM: Assembler, + CC: CallConv, + > Backend<'a> for Backend64Bit<'a, 'r, GeneralReg, FloatReg, ASM, CC> +{ + fn env(&self) -> &Env<'a> { + self.env + } + fn interns(&self) -> &Interns { + self.interns + } + fn interns_mut(&mut self) -> &mut Interns { + self.interns + } + fn interner(&self) -> &STLayoutInterner<'a> { + self.layout_interner + } + fn relocations_mut(&mut self) -> &mut Vec<'a, Relocation> { + &mut self.relocs + } + fn target_info(&self) -> TargetInfo { + self.storage_manager.target_info + } + fn module_interns_helpers_mut( + &mut self, + ) -> ( + ModuleId, + &mut STLayoutInterner<'a>, + &mut Interns, + &mut CodeGenHelp<'a>, + &mut Vec<'a, CallerProc<'a>>, + ) { + ( + self.env.module_id, + self.layout_interner, + self.interns, + &mut self.helper_proc_gen, + &mut self.caller_procs, + ) + } + fn helper_proc_gen_mut(&mut self) -> &mut CodeGenHelp<'a> { + &mut self.helper_proc_gen + } + fn helper_proc_symbols_mut(&mut self) -> &mut Vec<'a, (Symbol, ProcLayout<'a>)> { + &mut self.helper_proc_symbols + } + fn helper_proc_symbols(&self) -> &Vec<'a, (Symbol, ProcLayout<'a>)> { + &self.helper_proc_symbols + } + fn caller_procs(&self) -> &Vec<'a, CallerProc<'a>> { + &self.caller_procs + } + + fn reset(&mut self, name: String, is_self_recursive: SelfRecursive) { + self.proc_name = Some(name); + self.is_self_recursive = Some(is_self_recursive); + self.last_seen_map.clear(); + self.layout_map.clear(); + self.join_map.clear(); + self.free_map.clear(); + self.buf.clear(); + self.storage_manager.reset(); + } + + fn literal_map(&mut self) -> &mut MutMap, *const InLayout<'a>)> { + &mut self.literal_map + } + + fn last_seen_map(&mut self) -> &mut MutMap> { + &mut self.last_seen_map + } + + fn layout_map(&mut self) -> &mut MutMap> { + &mut self.layout_map + } + + fn set_free_map(&mut self, map: MutMap<*const Stmt<'a>, Vec<'a, Symbol>>) { + self.free_map = map; + } + + fn free_map(&mut self) -> &mut MutMap<*const Stmt<'a>, Vec<'a, Symbol>> { + &mut self.free_map + } + + fn finalize(&mut self) -> (Vec, Vec) { + let mut out = bumpalo::vec![in self.env.arena]; + + // Setup stack. + let (used_general_regs, used_float_regs) = self + .storage_manager + .used_callee_saved_regs + .as_vecs(self.env.arena); + + let aligned_stack_size = CC::setup_stack( + &mut out, + &used_general_regs, + &used_float_regs, + self.storage_manager.stack_size() as i32, + self.storage_manager.fn_call_stack_size() as i32, + ); + let setup_offset = out.len(); + + // Deal with jumps to the return address. + let old_relocs = std::mem::replace(&mut self.relocs, bumpalo::vec![in self.env.arena]); + + // Check if their is an unnessary jump to return right at the end of the function. + let mut end_jmp_size = 0; + for reloc in old_relocs + .iter() + .filter(|reloc| matches!(reloc, Relocation::JmpToReturn { .. })) + { + if let Relocation::JmpToReturn { + inst_loc, + inst_size, + .. + } = reloc + { + if *inst_loc as usize + *inst_size as usize == self.buf.len() { + end_jmp_size = *inst_size as usize; + break; + } + } + } + + // Update jumps to returns. + let ret_offset = self.buf.len() - end_jmp_size; + for reloc in old_relocs + .iter() + .filter(|reloc| matches!(reloc, Relocation::JmpToReturn { .. })) + { + if let Relocation::JmpToReturn { + inst_loc, + inst_size, + offset, + } = reloc + { + if *inst_loc as usize + *inst_size as usize != self.buf.len() { + ASM::update_jmp_imm32_offset( + &mut self.buf, + *inst_loc, + *offset, + ret_offset as u64, + ); + } + } + } + + // Add function body. + out.extend(&self.buf[..self.buf.len() - end_jmp_size]); + + // Cleanup stack. + CC::cleanup_stack( + &mut out, + &used_general_regs, + &used_float_regs, + aligned_stack_size, + self.storage_manager.fn_call_stack_size() as i32, + ); + ASM::ret(&mut out); + + // Update other relocs to include stack setup offset. + let mut out_relocs = bumpalo::vec![in self.env.arena]; + out_relocs.extend( + old_relocs + .into_iter() + .filter(|reloc| !matches!(reloc, Relocation::JmpToReturn { .. })) + .map(|reloc| match reloc { + Relocation::LocalData { offset, data } => Relocation::LocalData { + offset: offset + setup_offset as u64, + data, + }, + Relocation::LinkedData { offset, name } => Relocation::LinkedData { + offset: offset + setup_offset as u64, + name, + }, + Relocation::LinkedFunction { offset, name } => Relocation::LinkedFunction { + offset: offset + setup_offset as u64, + name, + }, + Relocation::JmpToReturn { .. } => unreachable!(), + }), + ); + (out, out_relocs) + } + + fn load_args(&mut self, args: &'a [(InLayout<'a>, Symbol)], ret_layout: &InLayout<'a>) { + CC::load_args( + &mut self.buf, + &mut self.storage_manager, + self.layout_interner, + args, + ret_layout, + ); + } + + /// Used for generating wrappers for malloc/realloc/free + fn build_wrapped_jmp(&mut self) -> (&'a [u8], u64) { + let mut out = bumpalo::vec![in self.env.arena]; + let offset = ASM::tail_call(&mut out); + + (out.into_bump_slice(), offset) + } + + fn build_roc_setjmp(&mut self) -> &'a [u8] { + let mut out = bumpalo::vec![in self.env.arena]; + + CC::setjmp(&mut out); + + out.into_bump_slice() + } + + fn build_roc_longjmp(&mut self) -> &'a [u8] { + let mut out = bumpalo::vec![in self.env.arena]; + + CC::longjmp(&mut out); + + out.into_bump_slice() + } + + fn build_roc_panic(&mut self) -> (&'a [u8], Vec<'a, Relocation>) { + let mut out = bumpalo::vec![in self.env.arena]; + let mut relocs = bumpalo::vec![in self.env.arena]; + + CC::roc_panic(&mut out, &mut relocs); + + (out.into_bump_slice(), relocs) + } + + fn build_fn_pointer(&mut self, dst: &Symbol, fn_name: String) { + let reg = self.storage_manager.claim_general_reg(&mut self.buf, dst); + + ASM::function_pointer(&mut self.buf, &mut self.relocs, fn_name, reg) + } + + fn build_data_pointer(&mut self, dst: &Symbol, data_name: String) { + let reg = self.storage_manager.claim_general_reg(&mut self.buf, dst); + + // now, this gives a pointer to the value + ASM::data_pointer(&mut self.buf, &mut self.relocs, data_name, reg); + } + + fn build_fn_call( + &mut self, + dst: &Symbol, + fn_name: String, + args: &[Symbol], + arg_layouts: &[InLayout<'a>], + ret_layout: &InLayout<'a>, + ) { + // Save used caller saved regs. + self.storage_manager + .push_used_caller_saved_regs_to_stack(&mut self.buf); + + // Put values in param regs or on top of the stack. + CC::store_args( + &mut self.buf, + &mut self.storage_manager, + self.layout_interner, + dst, + args, + arg_layouts, + ret_layout, + ); + + // Call function and generate reloc. + ASM::call(&mut self.buf, &mut self.relocs, fn_name); + + self.move_return_value(dst, ret_layout) + } + + fn move_return_value(&mut self, dst: &Symbol, ret_layout: &InLayout<'a>) { + // move return value to dst. + let ret_repr = self.interner().get_repr(*ret_layout); + match ret_repr { + single_register_integers!() => { + let width = RegisterWidth::try_from_layout(ret_repr).unwrap(); + + let dst_reg = self.storage_manager.claim_general_reg(&mut self.buf, dst); + ASM::movzx_reg_reg(&mut self.buf, width, dst_reg, CC::GENERAL_RETURN_REGS[0]); + } + single_register_floats!() => { + let dst_reg = self.storage_manager.claim_float_reg(&mut self.buf, dst); + ASM::mov_freg64_freg64(&mut self.buf, dst_reg, CC::FLOAT_RETURN_REGS[0]); + } + // Note that on windows there is only 1 general return register so we can't use this optimisation + LayoutRepr::I128 | LayoutRepr::U128 if CC::GENERAL_RETURN_REGS.len() > 1 => { + let offset = self.storage_manager.claim_stack_area_layout( + self.layout_interner, + *dst, + Layout::U128, + ); + + ASM::mov_base32_reg64(&mut self.buf, offset, CC::GENERAL_RETURN_REGS[0]); + ASM::mov_base32_reg64(&mut self.buf, offset + 8, CC::GENERAL_RETURN_REGS[1]); + } + pointer_layouts!() => { + let dst_reg = self.storage_manager.claim_general_reg(&mut self.buf, dst); + ASM::mov_reg64_reg64(&mut self.buf, dst_reg, CC::GENERAL_RETURN_REGS[0]); + } + LayoutRepr::LambdaSet(lambda_set) => { + self.move_return_value(dst, &lambda_set.runtime_representation()) + } + LayoutRepr::Union(UnionLayout::NonRecursive(_)) => { + CC::load_returned_complex_symbol( + &mut self.buf, + &mut self.storage_manager, + self.layout_interner, + dst, + ret_layout, + ); + } + _ => { + CC::load_returned_complex_symbol( + &mut self.buf, + &mut self.storage_manager, + self.layout_interner, + dst, + ret_layout, + ); + } + } + } + + fn build_switch( + &mut self, + layout_ids: &mut LayoutIds<'a>, + cond_symbol: &Symbol, + _cond_layout: &InLayout<'a>, // cond_layout must be a integer due to potential jump table optimizations. + branches: &'a [(u64, BranchInfo<'a>, Stmt<'a>)], + default_branch: &(BranchInfo<'a>, &'a Stmt<'a>), + ret_layout: &InLayout<'a>, + ) { + // Switches are a little complex due to keeping track of jumps. + // In general I am trying to not have to loop over things multiple times or waste memory. + // The basic plan is to make jumps to nowhere and then correct them once we know the correct address. + let cond_reg = self + .storage_manager + .load_to_general_reg(&mut self.buf, cond_symbol); + + // this state is updated destructively in the branches. We don't want the branches to + // influence each other, so we must clone here. + let mut base_storage = self.storage_manager.clone(); + let base_literal_map = self.literal_map.clone(); + + let mut max_branch_stack_size = 0; + let mut ret_jumps = bumpalo::vec![in self.env.arena]; + let mut tmp = bumpalo::vec![in self.env.arena]; + for (val, _branch_info, stmt) in branches.iter() { + // TODO: look into branch info and if it matters here. + tmp.clear(); + // Create jump to next branch if cond_sym not equal to value. + // Since we don't know the offset yet, set it to 0 and overwrite later. + let jne_location = self.buf.len(); + let start_offset = ASM::jne_reg64_imm64_imm32( + &mut self.buf, + &mut self.storage_manager, + cond_reg, + *val, + 0, + ); + + // Build all statements in this branch. Using storage as from before any branch. + self.storage_manager = base_storage.clone(); + self.literal_map = base_literal_map.clone(); + self.build_stmt(layout_ids, stmt, ret_layout); + + // Build unconditional jump to the end of this switch. + // Since we don't know the offset yet, set it to 0 and overwrite later. + let jmp_location = self.buf.len(); + let jmp_offset = ASM::jmp_imm32(&mut self.buf, JUMP_PLACEHOLDER); + ret_jumps.push((jmp_location, jmp_offset)); + + // Overwrite the original jne with the correct offset. + let end_offset = self.buf.len(); + let jne_offset = end_offset - start_offset; + ASM::jne_reg64_imm64_imm32( + &mut tmp, + &mut self.storage_manager, + cond_reg, + *val, + jne_offset as i32, + ); + for (i, byte) in tmp.iter().enumerate() { + self.buf[jne_location + i] = *byte; + } + + // Update important storage information to avoid overwrites. + max_branch_stack_size = + std::cmp::max(max_branch_stack_size, self.storage_manager.stack_size()); + base_storage.update_fn_call_stack_size(self.storage_manager.fn_call_stack_size()); + + // make sure that used callee-saved registers get saved/restored even if used in only + // one of the branches of the switch + base_storage + .used_callee_saved_regs + .extend(&self.storage_manager.used_callee_saved_regs); + } + self.storage_manager = base_storage; + self.literal_map = base_literal_map; + self.storage_manager + .update_stack_size(max_branch_stack_size); + let (_branch_info, stmt) = default_branch; + self.build_stmt(layout_ids, stmt, ret_layout); + + // Update all return jumps to jump past the default case. + let ret_offset = self.buf.len(); + for (jmp_location, start_offset) in ret_jumps.into_iter() { + ASM::update_jmp_imm32_offset( + &mut self.buf, + jmp_location as u64, + start_offset as u64, + ret_offset as u64, + ); + } + } + + fn build_join( + &mut self, + layout_ids: &mut LayoutIds<'a>, + id: &JoinPointId, + parameters: &'a [Param<'a>], + body: &'a Stmt<'a>, + remainder: &'a Stmt<'a>, + ret_layout: &InLayout<'a>, + ) { + // Free everything to the stack to make sure they don't get messed up when looping back to this point. + // TODO: look into a nicer solution. + self.storage_manager.free_all_to_stack(&mut self.buf); + + // Ensure all the joinpoint parameters have storage locations. + // On jumps to the joinpoint, we will overwrite those locations as a way to "pass parameters" to the joinpoint. + self.storage_manager + .setup_joinpoint(self.layout_interner, &mut self.buf, id, parameters); + + self.join_map.insert(*id, bumpalo::vec![in self.env.arena]); + + // Build remainder of function first. It is what gets run and jumps to join. + self.build_stmt(layout_ids, remainder, ret_layout); + + let join_location = self.buf.len() as u64; + + // Build all statements in body. + self.build_stmt(layout_ids, body, ret_layout); + + // Overwrite the all jumps to the joinpoint with the correct offset. + for (jmp_location, start_offset) in self + .join_map + .remove(id) + .unwrap_or_else(|| internal_error!("join point not defined")) + { + // join_location: byte offset where the body of the joinpoint starts + // jmp_location: byte offset where the jump instruction starts + // start_offset: byte offset where the jump instruction ends + + ASM::update_jmp_imm32_offset(&mut self.buf, jmp_location, start_offset, join_location); + } + } + + fn build_jump( + &mut self, + id: &JoinPointId, + args: &[Symbol], + arg_layouts: &[InLayout<'a>], + _ret_layout: &InLayout<'a>, + ) { + self.storage_manager + .setup_jump(self.layout_interner, &mut self.buf, id, args, arg_layouts); + + let jmp_location = self.buf.len(); + let start_offset = ASM::jmp_imm32(&mut self.buf, JUMP_PLACEHOLDER); + + if let Some(vec) = self.join_map.get_mut(id) { + vec.push((jmp_location as u64, start_offset as u64)) + } else { + internal_error!("Jump: unknown point specified to jump to: {:?}", id); + } + } + + fn build_num_abs(&mut self, dst: &Symbol, src: &Symbol, layout: &InLayout<'a>) { + match self.interner().get_repr(*layout) { + LayoutRepr::Builtin(Builtin::Int(IntWidth::I64 | IntWidth::U64)) => { + let dst_reg = self.storage_manager.claim_general_reg(&mut self.buf, dst); + let src_reg = self.storage_manager.load_to_general_reg(&mut self.buf, src); + ASM::abs_reg64_reg64(&mut self.buf, dst_reg, src_reg); + } + LayoutRepr::Builtin(Builtin::Float(FloatWidth::F64)) => { + let dst_reg = self.storage_manager.claim_float_reg(&mut self.buf, dst); + let src_reg = self.storage_manager.load_to_float_reg(&mut self.buf, src); + ASM::abs_freg64_freg64(&mut self.buf, &mut self.relocs, dst_reg, src_reg); + } + x => todo!("NumAbs: layout, {:?}", x), + } + } + + fn build_num_add(&mut self, dst: &Symbol, src1: &Symbol, src2: &Symbol, layout: &InLayout<'a>) { + match self.layout_interner.get_repr(*layout) { + LayoutRepr::Builtin(Builtin::Int(quadword_and_smaller!())) => { + let dst_reg = self.storage_manager.claim_general_reg(&mut self.buf, dst); + let src1_reg = self + .storage_manager + .load_to_general_reg(&mut self.buf, src1); + let src2_reg = self + .storage_manager + .load_to_general_reg(&mut self.buf, src2); + ASM::add_reg64_reg64_reg64(&mut self.buf, dst_reg, src1_reg, src2_reg); + } + LayoutRepr::Builtin(Builtin::Float(FloatWidth::F64)) => { + let dst_reg = self.storage_manager.claim_float_reg(&mut self.buf, dst); + let src1_reg = self.storage_manager.load_to_float_reg(&mut self.buf, src1); + let src2_reg = self.storage_manager.load_to_float_reg(&mut self.buf, src2); + ASM::add_freg64_freg64_freg64(&mut self.buf, dst_reg, src1_reg, src2_reg); + } + LayoutRepr::Builtin(Builtin::Float(FloatWidth::F32)) => { + let dst_reg = self.storage_manager.claim_float_reg(&mut self.buf, dst); + let src1_reg = self.storage_manager.load_to_float_reg(&mut self.buf, src1); + let src2_reg = self.storage_manager.load_to_float_reg(&mut self.buf, src2); + ASM::add_freg32_freg32_freg32(&mut self.buf, dst_reg, src1_reg, src2_reg); + } + LayoutRepr::Builtin(Builtin::Decimal) => { + self.build_fn_call( + dst, + bitcode::DEC_ADD_OR_PANIC.to_string(), + &[*src1, *src2], + &[Layout::DEC, Layout::DEC], + &Layout::DEC, + ); + } + x => todo!("NumAdd: layout, {:?}", x), + } + } + + fn build_num_add_saturated( + &mut self, + dst: Symbol, + src1: Symbol, + src2: Symbol, + layout: InLayout<'a>, + ) { + match self.layout_interner.get_repr(layout) { + LayoutRepr::Builtin(Builtin::Int(width @ quadword_and_smaller!())) => { + let intrinsic = bitcode::NUM_ADD_SATURATED_INT[width].to_string(); + self.build_fn_call(&dst, intrinsic, &[src1, src2], &[layout, layout], &layout); + } + LayoutRepr::Builtin(Builtin::Float(FloatWidth::F64)) => { + let dst_reg = self.storage_manager.claim_float_reg(&mut self.buf, &dst); + let src1_reg = self.storage_manager.load_to_float_reg(&mut self.buf, &src1); + let src2_reg = self.storage_manager.load_to_float_reg(&mut self.buf, &src2); + ASM::add_freg64_freg64_freg64(&mut self.buf, dst_reg, src1_reg, src2_reg); + } + LayoutRepr::Builtin(Builtin::Float(FloatWidth::F32)) => { + let dst_reg = self.storage_manager.claim_float_reg(&mut self.buf, &dst); + let src1_reg = self.storage_manager.load_to_float_reg(&mut self.buf, &src1); + let src2_reg = self.storage_manager.load_to_float_reg(&mut self.buf, &src2); + ASM::add_freg32_freg32_freg32(&mut self.buf, dst_reg, src1_reg, src2_reg); + } + LayoutRepr::Builtin(Builtin::Decimal) => { + let intrinsic = bitcode::DEC_ADD_SATURATED.to_string(); + self.build_fn_call(&dst, intrinsic, &[src1, src2], &[layout, layout], &layout); + } + x => todo!("NumAddSaturated: layout, {:?}", x), + } + } + + fn build_num_add_checked( + &mut self, + dst: &Symbol, + src1: &Symbol, + src2: &Symbol, + num_layout: &InLayout<'a>, + return_layout: &InLayout<'a>, + ) { + let function_name = match self.interner().get_repr(*num_layout) { + LayoutRepr::Builtin(Builtin::Int(width)) => &bitcode::NUM_ADD_CHECKED_INT[width], + LayoutRepr::Builtin(Builtin::Float(width)) => &bitcode::NUM_ADD_CHECKED_FLOAT[width], + LayoutRepr::Builtin(Builtin::Decimal) => bitcode::DEC_ADD_WITH_OVERFLOW, + x => internal_error!("NumAddChecked is not defined for {:?}", x), + }; + + self.build_fn_call( + dst, + function_name.to_string(), + &[*src1, *src2], + &[*num_layout, *num_layout], + return_layout, + ) + } + + fn build_num_sub_checked( + &mut self, + dst: &Symbol, + src1: &Symbol, + src2: &Symbol, + num_layout: &InLayout<'a>, + return_layout: &InLayout<'a>, + ) { + let function_name = match self.interner().get_repr(*num_layout) { + LayoutRepr::Builtin(Builtin::Int(width)) => &bitcode::NUM_SUB_CHECKED_INT[width], + LayoutRepr::Builtin(Builtin::Float(width)) => &bitcode::NUM_SUB_CHECKED_FLOAT[width], + LayoutRepr::Builtin(Builtin::Decimal) => bitcode::DEC_SUB_WITH_OVERFLOW, + x => internal_error!("NumSubChecked is not defined for {:?}", x), + }; + + self.build_fn_call( + dst, + function_name.to_string(), + &[*src1, *src2], + &[*num_layout, *num_layout], + return_layout, + ) + } + + fn build_num_mul(&mut self, dst: &Symbol, src1: &Symbol, src2: &Symbol, layout: &InLayout<'a>) { + // for the time being, `num_mul` is implemented as wrapping multiplication. In roc, the normal + // `mul` should panic on overflow, but we just don't do that yet + self.build_num_mul_wrap(dst, src1, src2, layout) + } + + fn build_num_mul_wrap( + &mut self, + dst: &Symbol, + src1: &Symbol, + src2: &Symbol, + layout: &InLayout<'a>, + ) { + use Builtin::Int; + + match self.layout_interner.get_repr(*layout) { + LayoutRepr::Builtin(Int( + IntWidth::I64 | IntWidth::I32 | IntWidth::I16 | IntWidth::I8, + )) => { + let dst_reg = self.storage_manager.claim_general_reg(&mut self.buf, dst); + let src1_reg = self + .storage_manager + .load_to_general_reg(&mut self.buf, src1); + let src2_reg = self + .storage_manager + .load_to_general_reg(&mut self.buf, src2); + ASM::imul_reg64_reg64_reg64(&mut self.buf, dst_reg, src1_reg, src2_reg); + } + LayoutRepr::Builtin(Int( + IntWidth::U64 | IntWidth::U32 | IntWidth::U16 | IntWidth::U8, + )) => { + let dst_reg = self.storage_manager.claim_general_reg(&mut self.buf, dst); + let src1_reg = self + .storage_manager + .load_to_general_reg(&mut self.buf, src1); + let src2_reg = self + .storage_manager + .load_to_general_reg(&mut self.buf, src2); + + ASM::umul_reg64_reg64_reg64( + &mut self.buf, + &mut self.storage_manager, + dst_reg, + src1_reg, + src2_reg, + ); + } + LayoutRepr::Builtin(Builtin::Int(IntWidth::I128 | IntWidth::U128)) => { + let int_width = match *layout { + Layout::I128 => IntWidth::I128, + Layout::U128 => IntWidth::U128, + _ => unreachable!(), + }; + + self.build_fn_call( + dst, + bitcode::NUM_MUL_WRAP_INT[int_width].to_string(), + &[*src1, *src2], + &[*layout, *layout], + layout, + ); + } + LayoutRepr::Builtin(Builtin::Float(FloatWidth::F64)) => { + let dst_reg = self.storage_manager.claim_float_reg(&mut self.buf, dst); + let src1_reg = self.storage_manager.load_to_float_reg(&mut self.buf, src1); + let src2_reg = self.storage_manager.load_to_float_reg(&mut self.buf, src2); + ASM::mul_freg64_freg64_freg64(&mut self.buf, dst_reg, src1_reg, src2_reg); + } + LayoutRepr::Builtin(Builtin::Float(FloatWidth::F32)) => { + let dst_reg = self.storage_manager.claim_float_reg(&mut self.buf, dst); + let src1_reg = self.storage_manager.load_to_float_reg(&mut self.buf, src1); + let src2_reg = self.storage_manager.load_to_float_reg(&mut self.buf, src2); + ASM::mul_freg32_freg32_freg32(&mut self.buf, dst_reg, src1_reg, src2_reg); + } + x => todo!("NumMulWrap: layout, {:?}", x), + } + } + + fn build_num_mul_saturated( + &mut self, + dst: Symbol, + src1: Symbol, + src2: Symbol, + layout: InLayout<'a>, + ) { + match self.layout_interner.get_repr(layout) { + LayoutRepr::Builtin(Builtin::Int(width @ quadword_and_smaller!())) => { + let intrinsic = bitcode::NUM_MUL_SATURATED_INT[width].to_string(); + self.build_fn_call(&dst, intrinsic, &[src1, src2], &[layout, layout], &layout); + } + LayoutRepr::Builtin(Builtin::Float(FloatWidth::F64)) => { + let dst_reg = self.storage_manager.claim_float_reg(&mut self.buf, &dst); + let src1_reg = self.storage_manager.load_to_float_reg(&mut self.buf, &src1); + let src2_reg = self.storage_manager.load_to_float_reg(&mut self.buf, &src2); + ASM::mul_freg64_freg64_freg64(&mut self.buf, dst_reg, src1_reg, src2_reg); + } + LayoutRepr::Builtin(Builtin::Float(FloatWidth::F32)) => { + let dst_reg = self.storage_manager.claim_float_reg(&mut self.buf, &dst); + let src1_reg = self.storage_manager.load_to_float_reg(&mut self.buf, &src1); + let src2_reg = self.storage_manager.load_to_float_reg(&mut self.buf, &src2); + ASM::mul_freg32_freg32_freg32(&mut self.buf, dst_reg, src1_reg, src2_reg); + } + LayoutRepr::Builtin(Builtin::Decimal) => { + let intrinsic = bitcode::DEC_MUL_SATURATED.to_string(); + self.build_fn_call(&dst, intrinsic, &[src1, src2], &[layout, layout], &layout); + } + x => todo!("NumMulSaturated: layout, {:?}", x), + } + } + + fn build_num_mul_checked( + &mut self, + dst: &Symbol, + src1: &Symbol, + src2: &Symbol, + num_layout: &InLayout<'a>, + return_layout: &InLayout<'a>, + ) { + let function_name = match self.interner().get_repr(*num_layout) { + LayoutRepr::Builtin(Builtin::Int(width)) => &bitcode::NUM_MUL_CHECKED_INT[width], + LayoutRepr::Builtin(Builtin::Float(width)) => &bitcode::NUM_MUL_CHECKED_FLOAT[width], + LayoutRepr::Builtin(Builtin::Decimal) => bitcode::DEC_MUL_WITH_OVERFLOW, + x => internal_error!("NumMulChecked is not defined for {:?}", x), + }; + + self.build_fn_call( + dst, + function_name.to_string(), + &[*src1, *src2], + &[*num_layout, *num_layout], + return_layout, + ) + } + + fn build_num_div(&mut self, dst: &Symbol, src1: &Symbol, src2: &Symbol, layout: &InLayout<'a>) { + match self.layout_interner.get_repr(*layout) { + LayoutRepr::Builtin(Builtin::Int( + IntWidth::I64 | IntWidth::I32 | IntWidth::I16 | IntWidth::I8, + )) => { + let dst_reg = self.storage_manager.claim_general_reg(&mut self.buf, dst); + let src1_reg = self + .storage_manager + .load_to_general_reg(&mut self.buf, src1); + let src2_reg = self + .storage_manager + .load_to_general_reg(&mut self.buf, src2); + + ASM::idiv_reg64_reg64_reg64( + &mut self.buf, + &mut self.storage_manager, + dst_reg, + src1_reg, + src2_reg, + ); + } + LayoutRepr::Builtin(Builtin::Int( + IntWidth::U64 | IntWidth::U32 | IntWidth::U16 | IntWidth::U8, + )) => { + let dst_reg = self.storage_manager.claim_general_reg(&mut self.buf, dst); + let src1_reg = self + .storage_manager + .load_to_general_reg(&mut self.buf, src1); + let src2_reg = self + .storage_manager + .load_to_general_reg(&mut self.buf, src2); + + ASM::udiv_reg64_reg64_reg64( + &mut self.buf, + &mut self.storage_manager, + dst_reg, + src1_reg, + src2_reg, + ); + } + LayoutRepr::Builtin(Builtin::Float(FloatWidth::F64)) => { + let dst_reg = self.storage_manager.claim_float_reg(&mut self.buf, dst); + let src1_reg = self.storage_manager.load_to_float_reg(&mut self.buf, src1); + let src2_reg = self.storage_manager.load_to_float_reg(&mut self.buf, src2); + ASM::div_freg64_freg64_freg64(&mut self.buf, dst_reg, src1_reg, src2_reg); + } + LayoutRepr::Builtin(Builtin::Float(FloatWidth::F32)) => { + let dst_reg = self.storage_manager.claim_float_reg(&mut self.buf, dst); + let src1_reg = self.storage_manager.load_to_float_reg(&mut self.buf, src1); + let src2_reg = self.storage_manager.load_to_float_reg(&mut self.buf, src2); + ASM::div_freg32_freg32_freg32(&mut self.buf, dst_reg, src1_reg, src2_reg); + } + x => todo!("NumDiv: layout, {:?}", x), + } + } + + fn build_num_div_ceil( + &mut self, + dst: &Symbol, + src1: &Symbol, + src2: &Symbol, + layout: &InLayout<'a>, + ) { + match self.layout_interner.get_repr(*layout) { + LayoutRepr::Builtin(Builtin::Int(int_width)) => self.build_fn_call( + dst, + bitcode::NUM_DIV_CEIL[int_width].to_string(), + &[*src1, *src2], + &[*layout, *layout], + layout, + ), + x => todo!("NumDivCeilUnchecked: layout, {:?}", x), + } + } + + fn build_num_rem(&mut self, dst: &Symbol, src1: &Symbol, src2: &Symbol, layout: &InLayout<'a>) { + match self.layout_interner.get_repr(*layout) { + LayoutRepr::Builtin(Builtin::Int( + IntWidth::I64 | IntWidth::I32 | IntWidth::I16 | IntWidth::I8, + )) => { + let dst_reg = self.storage_manager.claim_general_reg(&mut self.buf, dst); + let src1_reg = self + .storage_manager + .load_to_general_reg(&mut self.buf, src1); + let src2_reg = self + .storage_manager + .load_to_general_reg(&mut self.buf, src2); + + ASM::irem_reg64_reg64_reg64( + &mut self.buf, + &mut self.storage_manager, + dst_reg, + src1_reg, + src2_reg, + ); + } + LayoutRepr::Builtin(Builtin::Int( + IntWidth::U64 | IntWidth::U32 | IntWidth::U16 | IntWidth::U8, + )) => { + let dst_reg = self.storage_manager.claim_general_reg(&mut self.buf, dst); + let src1_reg = self + .storage_manager + .load_to_general_reg(&mut self.buf, src1); + let src2_reg = self + .storage_manager + .load_to_general_reg(&mut self.buf, src2); + + ASM::urem_reg64_reg64_reg64( + &mut self.buf, + &mut self.storage_manager, + dst_reg, + src1_reg, + src2_reg, + ); + } + x => todo!("NumDiv: layout, {:?}", x), + } + } + + fn build_num_neg(&mut self, dst: &Symbol, src: &Symbol, layout: &InLayout<'a>) { + match self.layout_interner.get_repr(*layout) { + LayoutRepr::Builtin(Builtin::Int(IntWidth::I64 | IntWidth::U64)) => { + let dst_reg = self.storage_manager.claim_general_reg(&mut self.buf, dst); + let src_reg = self.storage_manager.load_to_general_reg(&mut self.buf, src); + ASM::neg_reg64_reg64(&mut self.buf, dst_reg, src_reg); + } + x => todo!("NumNeg: layout, {:?}", x), + } + } + + fn build_num_sub(&mut self, dst: &Symbol, src1: &Symbol, src2: &Symbol, layout: &InLayout<'a>) { + // for the time being, `num_sub` is implemented as wrapping subtraction. In roc, the normal + // `sub` should panic on overflow, but we just don't do that yet + self.build_num_sub_wrap(dst, src1, src2, layout) + } + + fn build_num_sub_wrap( + &mut self, + dst: &Symbol, + src1: &Symbol, + src2: &Symbol, + layout: &InLayout<'a>, + ) { + match self.layout_interner.get_repr(*layout) { + LayoutRepr::Builtin(Builtin::Int(quadword_and_smaller!())) => { + let dst_reg = self.storage_manager.claim_general_reg(&mut self.buf, dst); + let src1_reg = self + .storage_manager + .load_to_general_reg(&mut self.buf, src1); + let src2_reg = self + .storage_manager + .load_to_general_reg(&mut self.buf, src2); + ASM::sub_reg64_reg64_reg64(&mut self.buf, dst_reg, src1_reg, src2_reg); + } + x => todo!("NumSubWrap: layout, {:?}", x), + } + } + + fn build_eq(&mut self, dst: &Symbol, src1: &Symbol, src2: &Symbol, arg_layout: &InLayout<'a>) { + let repr = self.interner().get_repr(*arg_layout); + match repr { + single_register_int_builtins!() | LayoutRepr::BOOL => { + let width = match repr { + LayoutRepr::BOOL | LayoutRepr::I8 | LayoutRepr::U8 => RegisterWidth::W8, + LayoutRepr::I16 | LayoutRepr::U16 => RegisterWidth::W16, + LayoutRepr::U32 | LayoutRepr::I32 => RegisterWidth::W32, + LayoutRepr::I64 | LayoutRepr::U64 => RegisterWidth::W64, + _ => unreachable!(), + }; + + let dst_reg = self.storage_manager.claim_general_reg(&mut self.buf, dst); + let src1_reg = self + .storage_manager + .load_to_general_reg(&mut self.buf, src1); + let src2_reg = self + .storage_manager + .load_to_general_reg(&mut self.buf, src2); + ASM::eq_reg_reg_reg(&mut self.buf, width, dst_reg, src1_reg, src2_reg); + } + LayoutRepr::U128 | LayoutRepr::I128 | LayoutRepr::DEC => { + let dst_reg = self.storage_manager.claim_general_reg(&mut self.buf, dst); + + // put the arguments on the stack + let (src1_offset, _) = self.storage_manager.stack_offset_and_size(src1); + let (src2_offset, _) = self.storage_manager.stack_offset_and_size(src2); + + let tmp1_symbol = self.debug_symbol("eq_tmp1"); + let tmp2_symbol = self.debug_symbol("eq_tmp2"); + + let buf = &mut self.buf; + + let tmp1 = self.storage_manager.claim_general_reg(buf, &tmp1_symbol); + let tmp2 = self.storage_manager.claim_general_reg(buf, &tmp2_symbol); + + // move the upper 8 bytes of both arguments into a register + ASM::mov_reg64_base32(buf, tmp1, src1_offset); + ASM::mov_reg64_base32(buf, tmp2, src2_offset); + + // store the result in our destination + ASM::eq_reg64_reg64_reg64(buf, dst_reg, tmp1, tmp2); + + // move the lower 8 bytes of both arguments into a register + ASM::mov_reg64_base32(buf, tmp1, src1_offset + 8); + ASM::mov_reg64_base32(buf, tmp2, src2_offset + 8); + + // store the result in tmp1 + ASM::eq_reg64_reg64_reg64(buf, tmp1, tmp1, tmp2); + + // now and dst and tmp1, storing the result in dst + ASM::and_reg64_reg64_reg64(buf, dst_reg, dst_reg, tmp1); + + self.storage_manager.free_symbol(&tmp1_symbol); + self.storage_manager.free_symbol(&tmp2_symbol); + } + LayoutRepr::F32 | LayoutRepr::F64 => { + let float_width = if repr == LayoutRepr::F32 { + FloatWidth::F32 + } else { + FloatWidth::F64 + }; + + let buf = &mut self.buf; + + let dst_reg = self.storage_manager.claim_general_reg(buf, dst); + + let src_reg1 = self.storage_manager.load_to_float_reg(buf, src1); + let src_reg2 = self.storage_manager.load_to_float_reg(buf, src2); + + ASM::eq_freg_freg_reg64(&mut self.buf, dst_reg, src_reg1, src_reg2, float_width) + } + LayoutRepr::STR => { + // use a zig call + self.build_fn_call( + dst, + bitcode::STR_EQUAL.to_string(), + &[*src1, *src2], + &[Layout::STR, Layout::STR], + &Layout::BOOL, + ); + + // mask the result; we pass booleans around as 64-bit values, but branch on 0x0 and 0x1. + // Zig gives back values where not all of the upper bits are zero, so we must clear them ourselves + let tmp = &Symbol::DEV_TMP; + let tmp_reg = self.storage_manager.claim_general_reg(&mut self.buf, tmp); + ASM::mov_reg64_imm64(&mut self.buf, tmp_reg, true as i64); + + let width = RegisterWidth::W8; // we're comparing booleans + let dst_reg = self.storage_manager.load_to_general_reg(&mut self.buf, dst); + ASM::eq_reg_reg_reg(&mut self.buf, width, dst_reg, dst_reg, tmp_reg); + + self.free_symbol(tmp); + } + LayoutRepr::Union(UnionLayout::NonRecursive([])) => { + // This instruction will never execute, but we need a value the symbol + let dst_reg = self.storage_manager.claim_general_reg(&mut self.buf, dst); + ASM::mov_reg64_imm64(&mut self.buf, dst_reg, 1); + } + _ => { + let ident_ids = self + .interns + .all_ident_ids + .get_mut(&self.env.module_id) + .unwrap(); + + // generate a proc + + let (eq_symbol, eq_linker_data) = self.helper_proc_gen.gen_refcount_proc( + ident_ids, + self.layout_interner, + *arg_layout, + HelperOp::Eq, + ); + + let fn_name = self.lambda_name_to_string( + LambdaName::no_niche(eq_symbol), + [*arg_layout, *arg_layout].into_iter(), + None, + Layout::U8, + ); + + self.helper_proc_symbols.extend(eq_linker_data); + + self.build_fn_call( + dst, + fn_name, + &[*src1, *src2], + &[*arg_layout, *arg_layout], + &Layout::U8, + ) + } + } + } + + fn build_neq(&mut self, dst: &Symbol, src1: &Symbol, src2: &Symbol, arg_layout: &InLayout<'a>) { + match self.interner().get_repr(*arg_layout) { + single_register_int_builtins!() | LayoutRepr::BOOL => { + let width = match *arg_layout { + Layout::BOOL | Layout::I8 | Layout::U8 => RegisterWidth::W8, + Layout::I16 | Layout::U16 => RegisterWidth::W16, + Layout::U32 | Layout::I32 => RegisterWidth::W32, + Layout::I64 | Layout::U64 => RegisterWidth::W64, + _ => unreachable!(), + }; + + let dst_reg = self.storage_manager.claim_general_reg(&mut self.buf, dst); + let src1_reg = self + .storage_manager + .load_to_general_reg(&mut self.buf, src1); + let src2_reg = self + .storage_manager + .load_to_general_reg(&mut self.buf, src2); + ASM::neq_reg_reg_reg(&mut self.buf, width, dst_reg, src1_reg, src2_reg); + } + LayoutRepr::STR => { + self.build_fn_call( + dst, + bitcode::STR_EQUAL.to_string(), + &[*src1, *src2], + &[Layout::STR, Layout::STR], + &Layout::BOOL, + ); + + // negate the result + let tmp = &Symbol::DEV_TMP; + let tmp_reg = self.storage_manager.claim_general_reg(&mut self.buf, tmp); + ASM::mov_reg64_imm64(&mut self.buf, tmp_reg, true as i64); + + let width = RegisterWidth::W8; // we're comparing booleans + let dst_reg = self.storage_manager.load_to_general_reg(&mut self.buf, dst); + ASM::neq_reg_reg_reg(&mut self.buf, width, dst_reg, dst_reg, tmp_reg); + + self.free_symbol(tmp) + } + LayoutRepr::Union(UnionLayout::NonRecursive([])) => { + // This instruction will never execute, but we need a value the symbol + let dst_reg = self.storage_manager.claim_general_reg(&mut self.buf, dst); + ASM::mov_reg64_imm64(&mut self.buf, dst_reg, 1); + } + _ => { + // defer to equality + + self.build_eq(dst, src1, src2, arg_layout); + + let dst_reg = self.storage_manager.load_to_general_reg(&mut self.buf, dst); + + self.storage_manager + .with_tmp_general_reg(&mut self.buf, |_, buf, tmp| { + ASM::mov_reg64_imm64(buf, tmp, -1); + ASM::xor_reg64_reg64_reg64(buf, dst_reg, tmp, dst_reg); + + ASM::mov_reg64_imm64(buf, tmp, 1); + ASM::and_reg64_reg64_reg64(buf, dst_reg, tmp, dst_reg); + }) + } + } + } + + fn build_not(&mut self, dst: &Symbol, src: &Symbol, arg_layout: &InLayout<'a>) { + match self.interner().get_repr(*arg_layout) { + LayoutRepr::BOOL => { + let dst_reg = self.storage_manager.claim_general_reg(&mut self.buf, dst); + let src_reg = self.storage_manager.load_to_general_reg(&mut self.buf, src); + + ASM::mov_reg64_imm64(&mut self.buf, dst_reg, 1); + ASM::xor_reg64_reg64_reg64(&mut self.buf, src_reg, src_reg, dst_reg); + + // we may need to mask out other bits in the end? but a boolean should be 0 or 1. + // if that invariant is upheld, this mask should not be required + // ASM::and_reg64_reg64_reg64(&mut self.buf, src_reg, src_reg, dst_reg); + + ASM::mov_reg64_reg64(&mut self.buf, dst_reg, src_reg); + } + x => todo!("Not: layout, {:?}", x), + } + } + + fn build_num_to_frac( + &mut self, + dst: &Symbol, + src: &Symbol, + arg_layout: &InLayout<'a>, + ret_layout: &InLayout<'a>, + ) { + match *ret_layout { + Layout::F32 => self.num_to_f32(dst, src, arg_layout), + Layout::F64 => self.num_to_f64(dst, src, arg_layout), + Layout::DEC => self.num_to_dec(dst, src, arg_layout), + + other => todo!("NumToFrac: layout {other:?} is not Frac"), + } + } + + fn build_num_is_nan(&mut self, dst: &Symbol, src: &Symbol, arg_layout: &InLayout<'a>) { + let float_width = match *arg_layout { + Layout::F32 => FloatWidth::F32, + Layout::F64 => FloatWidth::F64, + _ => unreachable!(), + }; + + let dst_reg = self.storage_manager.claim_general_reg(&mut self.buf, dst); + let src_reg = self.storage_manager.load_to_float_reg(&mut self.buf, src); + + ASM::is_nan_freg_reg64(&mut self.buf, dst_reg, src_reg, float_width); + } + + fn build_num_is_infinite(&mut self, dst: &Symbol, src: &Symbol, arg_layout: &InLayout<'a>) { + let dst_reg = self.storage_manager.claim_general_reg(&mut self.buf, dst); + let src_reg = self.storage_manager.load_to_float_reg(&mut self.buf, src); + + self.storage_manager.with_tmp_general_reg( + &mut self.buf, + |_storage_manager, buf, mask_reg| { + match *arg_layout { + Layout::F32 => { + ASM::mov_reg64_imm64(buf, mask_reg, 0x7fff_ffff); + ASM::xor_reg64_reg64_reg64(buf, dst_reg, dst_reg, dst_reg); // zero out dst reg + ASM::mov_reg32_freg32(buf, dst_reg, src_reg); + ASM::and_reg64_reg64_reg64(buf, dst_reg, dst_reg, mask_reg); + + ASM::mov_reg64_imm64(buf, mask_reg, 0x7f80_0000); + ASM::eq_reg_reg_reg(buf, RegisterWidth::W32, dst_reg, dst_reg, mask_reg); + } + Layout::F64 => { + ASM::mov_reg64_imm64(buf, mask_reg, 0x7fff_ffff_ffff_ffff); + ASM::mov_reg64_freg64(buf, dst_reg, src_reg); + ASM::and_reg64_reg64_reg64(buf, dst_reg, dst_reg, mask_reg); + + ASM::mov_reg64_imm64(buf, mask_reg, 0x7ff0_0000_0000_0000); + ASM::eq_reg_reg_reg(buf, RegisterWidth::W64, dst_reg, dst_reg, mask_reg); + } + _ => unreachable!(), + } + }, + ); + } + + fn build_num_is_finite(&mut self, dst: &Symbol, src: &Symbol, arg_layout: &InLayout<'a>) { + let dst_reg = self.storage_manager.claim_general_reg(&mut self.buf, dst); + let src_reg = self.storage_manager.load_to_float_reg(&mut self.buf, src); + + self.storage_manager.with_tmp_general_reg( + &mut self.buf, + |_storage_manager, buf, mask_reg| { + match *arg_layout { + Layout::F32 => { + ASM::mov_reg64_imm64(buf, mask_reg, 0x7f80_0000); + ASM::xor_reg64_reg64_reg64(buf, dst_reg, dst_reg, dst_reg); // zero out dst reg + ASM::mov_reg32_freg32(buf, dst_reg, src_reg); + ASM::and_reg64_reg64_reg64(buf, dst_reg, dst_reg, mask_reg); + ASM::neq_reg_reg_reg(buf, RegisterWidth::W32, dst_reg, dst_reg, mask_reg); + } + Layout::F64 => { + ASM::mov_reg64_imm64(buf, mask_reg, 0x7ff0_0000_0000_0000); + ASM::mov_reg64_freg64(buf, dst_reg, src_reg); + ASM::and_reg64_reg64_reg64(buf, dst_reg, dst_reg, mask_reg); + ASM::neq_reg_reg_reg(buf, RegisterWidth::W64, dst_reg, dst_reg, mask_reg); + } + _ => unreachable!(), + } + }, + ); + } + + fn build_num_cmp( + &mut self, + dst: &Symbol, + src1: &Symbol, + src2: &Symbol, + arg_layout: &InLayout<'a>, + ) { + // This implements the expression: + // (x != y) as u8 + (x < y) as u8 + // For x==y: (false as u8) + (false as u8) = 0 = RocOrder::Eq + // For x>y: (true as u8) + (false as u8) = 1 = RocOrder::Gt + // For x, + ) { + self.compare(CompareOperation::LessThan, dst, src1, src2, arg_layout) + } + + fn build_num_gt( + &mut self, + dst: &Symbol, + src1: &Symbol, + src2: &Symbol, + arg_layout: &InLayout<'a>, + ) { + self.compare(CompareOperation::GreaterThan, dst, src1, src2, arg_layout) + } + + fn build_num_lte( + &mut self, + dst: &Symbol, + src1: &Symbol, + src2: &Symbol, + arg_layout: &InLayout<'a>, + ) { + self.compare( + CompareOperation::LessThanOrEqual, + dst, + src1, + src2, + arg_layout, + ) + } + + fn build_num_gte( + &mut self, + dst: &Symbol, + src1: &Symbol, + src2: &Symbol, + arg_layout: &InLayout<'a>, + ) { + self.compare( + CompareOperation::GreaterThanOrEqual, + dst, + src1, + src2, + arg_layout, + ) + } + + fn build_indirect_inc(&mut self, layout: InLayout<'a>) -> Symbol { + let ident_ids = self + .interns + .all_ident_ids + .get_mut(&self.env.module_id) + .unwrap(); + + let (refcount_proc_name, linker_data) = self.helper_proc_gen.gen_refcount_proc( + ident_ids, + self.layout_interner, + layout, + HelperOp::IndirectInc, + ); + + self.helper_proc_symbols_mut().extend(linker_data); + + refcount_proc_name + } + + fn build_indirect_dec(&mut self, layout: InLayout<'a>) -> Symbol { + let ident_ids = self + .interns + .all_ident_ids + .get_mut(&self.env.module_id) + .unwrap(); + + let (refcount_proc_name, linker_data) = self.helper_proc_gen.gen_refcount_proc( + ident_ids, + self.layout_interner, + layout, + HelperOp::IndirectDec, + ); + + self.helper_proc_symbols_mut().extend(linker_data); + + refcount_proc_name + } + + fn build_higher_order_lowlevel( + &mut self, + dst: &Symbol, + higher_order: &HigherOrderLowLevel<'a>, + ret_layout: InLayout<'a>, + ) { + let ident_ids = self + .interns + .all_ident_ids + .get_mut(&self.env.module_id) + .unwrap(); + + let caller_proc = match higher_order.op { + HigherOrder::ListMap { .. } + | HigherOrder::ListMap2 { .. } + | HigherOrder::ListMap3 { .. } + | HigherOrder::ListMap4 { .. } => CallerProc::new_list_map( + self.env.arena, + self.env.module_id, + ident_ids, + self.layout_interner, + &higher_order.passed_function, + higher_order.closure_env_layout, + ), + HigherOrder::ListSortWith { .. } => CallerProc::new_compare( + self.env.arena, + self.env.module_id, + ident_ids, + self.layout_interner, + &higher_order.passed_function, + higher_order.closure_env_layout, + ), + }; + + let caller = self.debug_symbol("caller"); + let caller_string = self.lambda_name_to_string( + LambdaName::no_niche(caller_proc.proc_symbol), + std::iter::empty(), + None, + Layout::UNIT, + ); + + self.caller_procs.push(caller_proc); + + let argument_layouts = match higher_order.closure_env_layout { + None => higher_order.passed_function.argument_layouts, + Some(_) => &higher_order.passed_function.argument_layouts[1..], + }; + + // function pointer to a function that takes a pointer, and increments + let inc_n_data = if let Some(closure_env_layout) = higher_order.closure_env_layout { + self.increment_fn_pointer(closure_env_layout) + } else { + // null pointer + self.load_literal_i64(&Symbol::DEV_TMP, 0); + Symbol::DEV_TMP + }; + + let data = self.debug_symbol("data"); + if let Some(_closure_data_layout) = higher_order.closure_env_layout { + let data_symbol = higher_order.passed_function.captured_environment; + self.storage_manager + .ensure_symbol_on_stack(&mut self.buf, &data_symbol); + let (new_elem_offset, _) = self.storage_manager.stack_offset_and_size(&data_symbol); + + // Load address of output element into register. + let reg = self.storage_manager.claim_general_reg(&mut self.buf, &data); + ASM::add_reg64_reg64_imm32(&mut self.buf, reg, CC::BASE_PTR_REG, new_elem_offset); + } else { + // use a null pointer + self.load_literal(&data, &Layout::U64, &Literal::Int(0u128.to_be_bytes())); + } + + let ptr = Layout::U64; + let usize_ = Layout::U64; + + match higher_order.op { + HigherOrder::ListMap { xs } => { + let old_element_layout = argument_layouts[0]; + let new_element_layout = higher_order.passed_function.return_layout; + + let input_list_layout = LayoutRepr::Builtin(Builtin::List(old_element_layout)); + let input_list_in_layout = self + .layout_interner + .insert_direct_no_semantic(input_list_layout); + + let alignment = self.debug_symbol("alignment"); + let old_element_width = self.debug_symbol("old_element_width"); + let new_element_width = self.debug_symbol("new_element_width"); + + self.load_layout_alignment(new_element_layout, alignment); + + self.load_layout_stack_size(old_element_layout, old_element_width); + self.load_layout_stack_size(new_element_layout, new_element_width); + + self.build_fn_pointer(&caller, caller_string); + + // we pass a null pointer when the data is not owned. the zig code must not call this! + let data_is_owned = higher_order.closure_env_layout.is_some() + && higher_order.passed_function.owns_captured_environment; + + self.load_literal( + &Symbol::DEV_TMP2, + &Layout::BOOL, + &Literal::Bool(data_is_owned), + ); + + // list: RocList, + // caller: Caller1, + // data: Opaque, + // inc_n_data: IncN, + // data_is_owned: bool, + // alignment: u32, + // old_element_width: usize, + // new_element_width: usize, + + let arguments = [ + xs, + caller, + data, + inc_n_data, + Symbol::DEV_TMP2, + alignment, + old_element_width, + new_element_width, + ]; + + let layouts = [ + input_list_in_layout, + ptr, + ptr, + ptr, + Layout::BOOL, + Layout::U32, + usize_, + usize_, + ]; + + self.build_fn_call_stack_return( + bitcode::LIST_MAP.to_string(), + &arguments, + &layouts, + ret_layout, + *dst, + ); + + self.free_symbol(&Symbol::DEV_TMP); + self.free_symbol(&Symbol::DEV_TMP2); + } + HigherOrder::ListMap2 { xs, ys } => { + let old_element_layout1 = argument_layouts[0]; + let old_element_layout2 = argument_layouts[1]; + let new_element_layout = higher_order.passed_function.return_layout; + + let input_list_layout1 = LayoutRepr::Builtin(Builtin::List(old_element_layout1)); + let input_list_in_layout1 = self + .layout_interner + .insert_direct_no_semantic(input_list_layout1); + + let input_list_layout2 = LayoutRepr::Builtin(Builtin::List(old_element_layout2)); + let input_list_in_layout2 = self + .layout_interner + .insert_direct_no_semantic(input_list_layout2); + + let alignment = self.debug_symbol("alignment"); + let old_element_width1 = self.debug_symbol("old_element_width1"); + let old_element_width2 = self.debug_symbol("old_element_width2"); + let new_element_width = self.debug_symbol("new_element_width"); + + self.load_layout_alignment(new_element_layout, alignment); + + self.load_layout_stack_size(old_element_layout1, old_element_width1); + self.load_layout_stack_size(old_element_layout2, old_element_width2); + + self.load_layout_stack_size(new_element_layout, new_element_width); + + let dec1 = self.decrement_fn_pointer(old_element_layout1); + let dec2 = self.decrement_fn_pointer(old_element_layout2); + + self.build_fn_pointer(&caller, caller_string); + + // we pass a null pointer when the data is not owned. the zig code must not call this! + let data_is_owned = higher_order.closure_env_layout.is_some() + && higher_order.passed_function.owns_captured_environment; + + self.load_literal( + &Symbol::DEV_TMP2, + &Layout::BOOL, + &Literal::Bool(data_is_owned), + ); + + // list1: RocList, + // list2: RocList, + // caller: Caller1, + // data: Opaque, + // inc_n_data: IncN, + // data_is_owned: bool, + // alignment: u32, + // old_element_width1: usize, + // old_element_width2: usize, + // new_element_width: usize, + + let arguments = [ + xs, + ys, + caller, + data, + inc_n_data, + Symbol::DEV_TMP2, + alignment, + old_element_width1, + old_element_width2, + new_element_width, + dec1, + dec2, + ]; + + let layouts = [ + input_list_in_layout1, + input_list_in_layout2, + ptr, + ptr, + ptr, + Layout::BOOL, + Layout::U32, + usize_, + usize_, + usize_, + ptr, // dec1 + ptr, // dec2 + ]; + + self.build_fn_call_stack_return( + bitcode::LIST_MAP2.to_string(), + &arguments, + &layouts, + ret_layout, + *dst, + ); + + self.free_symbol(&Symbol::DEV_TMP); + self.free_symbol(&Symbol::DEV_TMP2); + } + HigherOrder::ListMap3 { xs, ys, zs } => { + let old_element_layout1 = argument_layouts[0]; + let old_element_layout2 = argument_layouts[1]; + let old_element_layout3 = argument_layouts[2]; + let new_element_layout = higher_order.passed_function.return_layout; + + let input_list_layout1 = LayoutRepr::Builtin(Builtin::List(old_element_layout1)); + let input_list_in_layout1 = self + .layout_interner + .insert_direct_no_semantic(input_list_layout1); + + let input_list_layout2 = LayoutRepr::Builtin(Builtin::List(old_element_layout2)); + let input_list_in_layout2 = self + .layout_interner + .insert_direct_no_semantic(input_list_layout2); + + let input_list_layout3 = LayoutRepr::Builtin(Builtin::List(old_element_layout3)); + let input_list_in_layout3 = self + .layout_interner + .insert_direct_no_semantic(input_list_layout3); + + let alignment = self.debug_symbol("alignment"); + let old_element_width1 = self.debug_symbol("old_element_width1"); + let old_element_width2 = self.debug_symbol("old_element_width2"); + let old_element_width3 = self.debug_symbol("old_element_width3"); + let new_element_width = self.debug_symbol("new_element_width"); + + self.load_layout_alignment(new_element_layout, alignment); + + self.load_layout_stack_size(old_element_layout1, old_element_width1); + self.load_layout_stack_size(old_element_layout2, old_element_width2); + self.load_layout_stack_size(old_element_layout3, old_element_width3); + + self.load_layout_stack_size(new_element_layout, new_element_width); + + let dec1 = self.decrement_fn_pointer(old_element_layout1); + let dec2 = self.decrement_fn_pointer(old_element_layout2); + let dec3 = self.decrement_fn_pointer(old_element_layout3); + + self.build_fn_pointer(&caller, caller_string); + + // we pass a null pointer when the data is not owned. the zig code must not call this! + let data_is_owned = higher_order.closure_env_layout.is_some() + && higher_order.passed_function.owns_captured_environment; + + self.load_literal( + &Symbol::DEV_TMP2, + &Layout::BOOL, + &Literal::Bool(data_is_owned), + ); + + // list1: RocList, + // list2: RocList, + // caller: Caller1, + // data: Opaque, + // inc_n_data: IncN, + // data_is_owned: bool, + // alignment: u32, + // old_element_width1: usize, + // old_element_width2: usize, + // new_element_width: usize, + + let arguments = [ + xs, + ys, + zs, + caller, + data, + inc_n_data, + Symbol::DEV_TMP2, + alignment, + old_element_width1, + old_element_width2, + old_element_width3, + new_element_width, + dec1, + dec2, + dec3, + ]; + + let layouts = [ + input_list_in_layout1, + input_list_in_layout2, + input_list_in_layout3, + ptr, + ptr, + ptr, + Layout::BOOL, + Layout::U32, + usize_, // old_element_width_1 + usize_, // old_element_width_2 + usize_, // old_element_width_3 + usize_, // new_element_width + ptr, // dec1 + ptr, // dec2 + ptr, // dec3 + ]; + + self.build_fn_call_stack_return( + bitcode::LIST_MAP3.to_string(), + &arguments, + &layouts, + ret_layout, + *dst, + ); + + self.free_symbol(&Symbol::DEV_TMP); + self.free_symbol(&Symbol::DEV_TMP2); + } + HigherOrder::ListMap4 { xs, ys, zs, ws } => { + let old_element_layout1 = argument_layouts[0]; + let old_element_layout2 = argument_layouts[1]; + let old_element_layout3 = argument_layouts[2]; + let old_element_layout4 = argument_layouts[3]; + let new_element_layout = higher_order.passed_function.return_layout; + + let input_list_layout1 = LayoutRepr::Builtin(Builtin::List(old_element_layout1)); + let input_list_in_layout1 = self + .layout_interner + .insert_direct_no_semantic(input_list_layout1); + + let input_list_layout2 = LayoutRepr::Builtin(Builtin::List(old_element_layout2)); + let input_list_in_layout2 = self + .layout_interner + .insert_direct_no_semantic(input_list_layout2); + + let input_list_layout3 = LayoutRepr::Builtin(Builtin::List(old_element_layout3)); + let input_list_in_layout3 = self + .layout_interner + .insert_direct_no_semantic(input_list_layout3); + + let input_list_layout4 = LayoutRepr::Builtin(Builtin::List(old_element_layout4)); + let input_list_in_layout4 = self + .layout_interner + .insert_direct_no_semantic(input_list_layout4); + + let alignment = self.debug_symbol("alignment"); + let old_element_width1 = self.debug_symbol("old_element_width1"); + let old_element_width2 = self.debug_symbol("old_element_width2"); + let old_element_width3 = self.debug_symbol("old_element_width3"); + let old_element_width4 = self.debug_symbol("old_element_width4"); + let new_element_width = self.debug_symbol("new_element_width"); + + self.load_layout_alignment(new_element_layout, alignment); + + self.load_layout_stack_size(old_element_layout1, old_element_width1); + self.load_layout_stack_size(old_element_layout2, old_element_width2); + self.load_layout_stack_size(old_element_layout3, old_element_width3); + self.load_layout_stack_size(old_element_layout4, old_element_width4); + + self.load_layout_stack_size(new_element_layout, new_element_width); + + let dec1 = self.decrement_fn_pointer(old_element_layout1); + let dec2 = self.decrement_fn_pointer(old_element_layout2); + let dec3 = self.decrement_fn_pointer(old_element_layout3); + let dec4 = self.decrement_fn_pointer(old_element_layout4); + + self.build_fn_pointer(&caller, caller_string); + + // we pass a null pointer when the data is not owned. the zig code must not call this! + let data_is_owned = higher_order.closure_env_layout.is_some() + && higher_order.passed_function.owns_captured_environment; + + self.load_literal( + &Symbol::DEV_TMP2, + &Layout::BOOL, + &Literal::Bool(data_is_owned), + ); + + let arguments = [ + xs, + ys, + zs, + ws, + caller, + data, + inc_n_data, + Symbol::DEV_TMP2, + alignment, + old_element_width1, + old_element_width2, + old_element_width3, + old_element_width4, + new_element_width, + dec1, + dec2, + dec3, + dec4, + ]; + + let layouts = [ + input_list_in_layout1, + input_list_in_layout2, + input_list_in_layout3, + input_list_in_layout4, + ptr, + ptr, + ptr, + Layout::BOOL, + Layout::U32, + usize_, // old_element_width_1 + usize_, // old_element_width_2 + usize_, // old_element_width_3 + usize_, // old_element_width_4 + usize_, // new_element_width + ptr, // dec1 + ptr, // dec2 + ptr, // dec3 + ptr, // dec4 + ]; + + self.build_fn_call_stack_return( + bitcode::LIST_MAP4.to_string(), + &arguments, + &layouts, + ret_layout, + *dst, + ); + + self.free_symbol(&Symbol::DEV_TMP); + self.free_symbol(&Symbol::DEV_TMP2); + } + HigherOrder::ListSortWith { xs } => { + let element_layout = argument_layouts[0]; + + let input_list_layout = LayoutRepr::Builtin(Builtin::List(element_layout)); + let input_list_in_layout = self + .layout_interner + .insert_direct_no_semantic(input_list_layout); + + let alignment = self.debug_symbol("alignment"); + let element_width = self.debug_symbol("old_element_width"); + + self.load_layout_alignment(element_layout, alignment); + self.load_layout_stack_size(element_layout, element_width); + + self.build_fn_pointer(&caller, caller_string); + + // we pass a null pointer when the data is not owned. the zig code must not call this! + let data_is_owned = higher_order.closure_env_layout.is_some() + && higher_order.passed_function.owns_captured_environment; + + self.load_literal( + &Symbol::DEV_TMP2, + &Layout::BOOL, + &Literal::Bool(data_is_owned), + ); + + // input: RocList, + // caller: CompareFn, + // data: Opaque, + // inc_n_data: IncN, + // data_is_owned: bool, + // alignment: u32, + // element_width: usize, + + let arguments = [ + xs, + caller, + data, + inc_n_data, + Symbol::DEV_TMP2, + alignment, + element_width, + ]; + + let layouts = [ + input_list_in_layout, + ptr, + ptr, + ptr, + Layout::BOOL, + Layout::U32, + usize_, + ]; + + self.build_fn_call_stack_return( + bitcode::LIST_SORT_WITH.to_string(), + &arguments, + &layouts, + ret_layout, + *dst, + ); + + self.free_symbol(&Symbol::DEV_TMP); + self.free_symbol(&Symbol::DEV_TMP2); + } + } + } + + fn build_list_len(&mut self, dst: &Symbol, list: &Symbol) { + self.storage_manager.list_len(&mut self.buf, dst, list); + } + + fn build_list_with_capacity( + &mut self, + dst: &Symbol, + capacity: Symbol, + capacity_layout: InLayout<'a>, + elem_layout: InLayout<'a>, + ret_layout: &InLayout<'a>, + ) { + // List alignment argument (u32). + self.load_layout_alignment(*ret_layout, Symbol::DEV_TMP); + + // Load element_width argument (usize). + self.load_layout_stack_size(elem_layout, Symbol::DEV_TMP2); + + // Setup the return location. + let base_offset = + self.storage_manager + .claim_stack_area_layout(self.layout_interner, *dst, *ret_layout); + + let lowlevel_args = [ + capacity, + // alignment + Symbol::DEV_TMP, + // element_width + Symbol::DEV_TMP2, + ]; + let lowlevel_arg_layouts = [capacity_layout, Layout::U32, Layout::U64]; + + self.build_fn_call( + &Symbol::DEV_TMP3, + bitcode::LIST_WITH_CAPACITY.to_string(), + &lowlevel_args, + &lowlevel_arg_layouts, + ret_layout, + ); + self.free_symbol(&Symbol::DEV_TMP); + self.free_symbol(&Symbol::DEV_TMP2); + + // Copy from list to the output record. + self.storage_manager.copy_symbol_to_stack_offset( + self.layout_interner, + &mut self.buf, + base_offset, + &Symbol::DEV_TMP3, + ret_layout, + ); + + self.free_symbol(&Symbol::DEV_TMP3); + } + + fn build_list_reserve( + &mut self, + dst: &Symbol, + args: &'a [Symbol], + arg_layouts: &[InLayout<'a>], + ret_layout: &InLayout<'a>, + ) { + let list = args[0]; + let list_layout = arg_layouts[0]; + let spare = args[1]; + let spare_layout = arg_layouts[1]; + + // Load list alignment argument (u32). + self.load_layout_alignment(list_layout, Symbol::DEV_TMP); + + // Load element_width argument (usize). + self.load_layout_stack_size(*ret_layout, Symbol::DEV_TMP2); + + // Load UpdateMode.Immutable argument (0u8) + let u8_layout = Layout::U8; + let update_mode = 0u8; + self.load_literal( + &Symbol::DEV_TMP3, + &u8_layout, + &Literal::Int((update_mode as i128).to_ne_bytes()), + ); + + // Setup the return location. + let base_offset = + self.storage_manager + .claim_stack_area_layout(self.layout_interner, *dst, *ret_layout); + + let lowlevel_args = bumpalo::vec![ + in self.env.arena; + list, + // alignment + Symbol::DEV_TMP, + spare, + // element_width + Symbol::DEV_TMP2, + // update_mode + Symbol::DEV_TMP3, + + ]; + let lowlevel_arg_layouts = [ + list_layout, + Layout::U32, + spare_layout, + Layout::U64, + u8_layout, + ]; + + self.build_fn_call( + &Symbol::DEV_TMP4, + bitcode::LIST_RESERVE.to_string(), + &lowlevel_args, + &lowlevel_arg_layouts, + ret_layout, + ); + self.free_symbol(&Symbol::DEV_TMP); + self.free_symbol(&Symbol::DEV_TMP2); + self.free_symbol(&Symbol::DEV_TMP3); + + // Return list value from fn call + self.storage_manager.copy_symbol_to_stack_offset( + self.layout_interner, + &mut self.buf, + base_offset, + &Symbol::DEV_TMP4, + ret_layout, + ); + + self.free_symbol(&Symbol::DEV_TMP4); + } + + fn build_list_append_unsafe( + &mut self, + dst: &Symbol, + args: &'a [Symbol], + arg_layouts: &[InLayout<'a>], + ret_layout: &InLayout<'a>, + ) { + let list = args[0]; + let list_layout = arg_layouts[0]; + let elem = args[1]; + let elem_layout = arg_layouts[1]; + + // Have to pass the input element by pointer, so put it on the stack and load it's address. + self.storage_manager + .ensure_symbol_on_stack(&mut self.buf, &elem); + let (new_elem_offset, _) = self.storage_manager.stack_offset_and_size(&elem); + + // Load address of output element into register. + let reg = self + .storage_manager + .claim_general_reg(&mut self.buf, &Symbol::DEV_TMP); + ASM::add_reg64_reg64_imm32(&mut self.buf, reg, CC::BASE_PTR_REG, new_elem_offset); + + // Load element_witdh argument (usize). + self.load_layout_stack_size(elem_layout, Symbol::DEV_TMP2); + + // Setup the return location. + let base_offset = + self.storage_manager + .claim_stack_area_layout(self.layout_interner, *dst, *ret_layout); + + let lowlevel_args = [ + list, + // element + Symbol::DEV_TMP, + // element_width + Symbol::DEV_TMP2, + ]; + let lowlevel_arg_layouts = [list_layout, Layout::U64, Layout::U64]; + + self.build_fn_call( + &Symbol::DEV_TMP3, + bitcode::LIST_APPEND_UNSAFE.to_string(), + &lowlevel_args, + &lowlevel_arg_layouts, + ret_layout, + ); + self.free_symbol(&Symbol::DEV_TMP); + self.free_symbol(&Symbol::DEV_TMP2); + + // Return list value from fn call + self.storage_manager.copy_symbol_to_stack_offset( + self.layout_interner, + &mut self.buf, + base_offset, + &Symbol::DEV_TMP3, + ret_layout, + ); + + self.free_symbol(&Symbol::DEV_TMP3); + } + + fn build_list_get_unsafe( + &mut self, + dst: &Symbol, + list: &Symbol, + index: &Symbol, + ret_layout: &InLayout<'a>, + ) { + let (base_offset, _) = self.storage_manager.stack_offset_and_size(list); + let index_reg = self + .storage_manager + .load_to_general_reg(&mut self.buf, index); + let ret_stack_size = self.layout_interner.stack_size(*ret_layout); + // TODO: This can be optimized with smarter instructions. + // Also can probably be moved into storage manager at least partly. + self.storage_manager.with_tmp_general_reg( + &mut self.buf, + |storage_manager, buf, list_ptr| { + ASM::mov_reg64_base32(buf, list_ptr, base_offset); + storage_manager.with_tmp_general_reg(buf, |storage_manager, buf, tmp| { + // calculate `element_width * index` + ASM::mov_reg64_imm64(buf, tmp, ret_stack_size as i64); + ASM::imul_reg64_reg64_reg64(buf, tmp, tmp, index_reg); + + // add the offset to the list pointer, store in `tmp` + ASM::add_reg64_reg64_reg64(buf, tmp, tmp, list_ptr); + let element_ptr = tmp; + + Self::ptr_read( + buf, + storage_manager, + self.layout_interner, + element_ptr, + 0, + *ret_layout, + *dst, + ); + }); + }, + ); + } + + fn build_list_replace_unsafe( + &mut self, + dst: &Symbol, + args: &'a [Symbol], + arg_layouts: &[InLayout<'a>], + ret_layout: &InLayout<'a>, + ) { + // We want to delegate to the zig builtin, but it takes some extra parameters. + // Firstly, it takes the alignment of the list. + // Secondly, it takes the stack size of an element. + // Thirdly, it takes a pointer that it will write the output element to. + let list = args[0]; + let list_layout = arg_layouts[0]; + let index = args[1]; + let index_layout = arg_layouts[1]; + let elem = args[2]; + let elem_layout = arg_layouts[2]; + + // Load list alignment argument (u32). + self.load_layout_alignment(list_layout, Symbol::DEV_TMP); + + // Have to pass the input element by pointer, so put it on the stack and load it's address. + self.storage_manager + .ensure_symbol_on_stack(&mut self.buf, &elem); + let u64_layout = Layout::U64; + let (new_elem_offset, _) = self.storage_manager.stack_offset_and_size(&elem); + // Load address of output element into register. + let reg = self + .storage_manager + .claim_general_reg(&mut self.buf, &Symbol::DEV_TMP2); + ASM::add_reg64_reg64_imm32(&mut self.buf, reg, CC::BASE_PTR_REG, new_elem_offset); + + // Load the elements size. + self.load_layout_stack_size(elem_layout, Symbol::DEV_TMP3); + + // Setup the return location. + let base_offset = + self.storage_manager + .claim_stack_area_layout(self.layout_interner, *dst, *ret_layout); + + let ret_fields = + if let LayoutRepr::Struct(field_layouts) = self.layout_interner.get_repr(*ret_layout) { + field_layouts + } else { + internal_error!( + "Expected replace to return a struct instead found: {:?}", + ret_layout + ) + }; + + // Only return list and old element. + debug_assert_eq!(ret_fields.len(), 2); + + let (out_list_offset, out_elem_offset) = if ret_fields[0] == elem_layout { + ( + base_offset + self.layout_interner.stack_size(ret_fields[0]) as i32, + base_offset, + ) + } else { + ( + base_offset, + base_offset + self.layout_interner.stack_size(ret_fields[0]) as i32, + ) + }; + + // Load address of output element into register. + let reg = self + .storage_manager + .claim_general_reg(&mut self.buf, &Symbol::DEV_TMP4); + ASM::add_reg64_reg64_imm32(&mut self.buf, reg, CC::BASE_PTR_REG, out_elem_offset); + + let lowlevel_args = bumpalo::vec![ + in self.env.arena; + list, + Symbol::DEV_TMP, + index, + Symbol::DEV_TMP2, + Symbol::DEV_TMP3, + Symbol::DEV_TMP4, + ]; + let lowlevel_arg_layouts = [ + list_layout, + Layout::U32, + index_layout, + u64_layout, + u64_layout, + u64_layout, + ]; + + self.build_fn_call( + &Symbol::DEV_TMP5, + bitcode::LIST_REPLACE.to_string(), + &lowlevel_args, + &lowlevel_arg_layouts, + &list_layout, + ); + self.free_symbol(&Symbol::DEV_TMP); + self.free_symbol(&Symbol::DEV_TMP2); + self.free_symbol(&Symbol::DEV_TMP3); + self.free_symbol(&Symbol::DEV_TMP4); + + // Copy from list to the output record. + self.storage_manager.copy_symbol_to_stack_offset( + self.layout_interner, + &mut self.buf, + out_list_offset, + &Symbol::DEV_TMP5, + &list_layout, + ); + + self.free_symbol(&Symbol::DEV_TMP5); + } + + fn build_list_concat( + &mut self, + dst: &Symbol, + args: &'a [Symbol], + arg_layouts: &[InLayout<'a>], + elem_layout: InLayout<'a>, + ret_layout: &InLayout<'a>, + ) { + let list_a = args[0]; + let list_a_layout = arg_layouts[0]; + let list_b = args[1]; + let list_b_layout = arg_layouts[1]; + + // Load list alignment argument (u32). + self.load_layout_alignment(elem_layout, Symbol::DEV_TMP); + + // Load element_width argument (usize). + self.load_layout_stack_size(elem_layout, Symbol::DEV_TMP2); + + // Setup the return location. + let base_offset = + self.storage_manager + .claim_stack_area_layout(self.layout_interner, *dst, *ret_layout); + + let lowlevel_args = bumpalo::vec![ + in self.env.arena; + list_a, + list_b, + // alignment + Symbol::DEV_TMP, + // element_width + Symbol::DEV_TMP2, + ]; + let lowlevel_arg_layouts = [list_a_layout, list_b_layout, Layout::U32, Layout::U64]; + + self.build_fn_call( + &Symbol::DEV_TMP3, + bitcode::LIST_CONCAT.to_string(), + &lowlevel_args, + &lowlevel_arg_layouts, + ret_layout, + ); + + self.free_symbol(&Symbol::DEV_TMP); + self.free_symbol(&Symbol::DEV_TMP2); + + // Return list value from fn call + self.storage_manager.copy_symbol_to_stack_offset( + self.layout_interner, + &mut self.buf, + base_offset, + &Symbol::DEV_TMP3, + ret_layout, + ); + + self.free_symbol(&Symbol::DEV_TMP3); + } + + fn build_list_prepend( + &mut self, + dst: &Symbol, + args: &'a [Symbol], + arg_layouts: &[InLayout<'a>], + ret_layout: &InLayout<'a>, + ) { + let list = args[0]; + let list_layout = arg_layouts[0]; + let elem = args[1]; + let elem_layout = arg_layouts[1]; + + // List alignment argument (u32). + self.load_layout_alignment(*ret_layout, Symbol::DEV_TMP); + + // Have to pass the input element by pointer, so put it on the stack and load it's address. + self.storage_manager + .ensure_symbol_on_stack(&mut self.buf, &elem); + let (new_elem_offset, _) = self.storage_manager.stack_offset_and_size(&elem); + + // Load address of input element into register. + let reg = self + .storage_manager + .claim_general_reg(&mut self.buf, &Symbol::DEV_TMP2); + ASM::add_reg64_reg64_imm32(&mut self.buf, reg, CC::BASE_PTR_REG, new_elem_offset); + + // Load element_witdh argument (usize). + self.load_layout_stack_size(elem_layout, Symbol::DEV_TMP3); + + // Setup the return location. + let base_offset = + self.storage_manager + .claim_stack_area_layout(self.layout_interner, *dst, *ret_layout); + + let lowlevel_args = [ + list, + // alignment + Symbol::DEV_TMP, + // element + Symbol::DEV_TMP2, + // element_width + Symbol::DEV_TMP3, + ]; + let lowlevel_arg_layouts = [list_layout, Layout::U32, Layout::U64, Layout::U64]; + + self.build_fn_call( + &Symbol::DEV_TMP4, + bitcode::LIST_PREPEND.to_string(), + &lowlevel_args, + &lowlevel_arg_layouts, + ret_layout, + ); + self.free_symbol(&Symbol::DEV_TMP); + self.free_symbol(&Symbol::DEV_TMP2); + self.free_symbol(&Symbol::DEV_TMP3); + + // Return list value from fn call + self.storage_manager.copy_symbol_to_stack_offset( + self.layout_interner, + &mut self.buf, + base_offset, + &Symbol::DEV_TMP4, + ret_layout, + ); + + self.free_symbol(&Symbol::DEV_TMP4); + } + + fn build_ptr_cast(&mut self, dst: &Symbol, src: &Symbol) { + let src_reg = self.storage_manager.load_to_general_reg(&mut self.buf, src); + let dst_reg = self.storage_manager.claim_general_reg(&mut self.buf, dst); + + ASM::mov_reg64_reg64(&mut self.buf, dst_reg, src_reg) + } + + fn create_empty_array(&mut self, sym: &Symbol) { + let base_offset = self + .storage_manager + .claim_stack_area_with_alignment(*sym, 24, 8); + + self.storage_manager + .with_tmp_general_reg(&mut self.buf, |_storage_manager, buf, reg| { + ASM::mov_reg64_imm64(buf, reg, 0); + ASM::mov_base32_reg64(buf, base_offset, reg); + ASM::mov_base32_reg64(buf, base_offset + 8, reg); + ASM::mov_base32_reg64(buf, base_offset + 16, reg); + }); + } + + fn create_array( + &mut self, + sym: &Symbol, + element_in_layout: &InLayout<'a>, + elements: &[ListLiteralElement<'a>], + ) { + let element_layout = self.layout_interner.get_repr(*element_in_layout); + let element_width = self.layout_interner.stack_size(*element_in_layout) as u64; + let element_alignment = self.layout_interner.alignment_bytes(*element_in_layout) as u64; + + // load the total size of the data we want to store (excludes refcount) + let data_bytes_symbol = self.debug_symbol("data_bytes"); + let data_bytes = element_width * elements.len() as u64; + self.load_literal( + &data_bytes_symbol, + &Layout::U64, + &Literal::Int((data_bytes as i128).to_ne_bytes()), + ); + + // Load allocation alignment (u32) + let element_alignment_symbol = self.debug_symbol("element_alignment"); + self.load_layout_alignment(*element_in_layout, element_alignment_symbol); + + let allocation_symbol = self.debug_symbol("list_allocation"); + self.allocate_with_refcount( + allocation_symbol, + data_bytes_symbol, + element_alignment_symbol, + ); + + self.free_symbol(&data_bytes_symbol); + self.free_symbol(&element_alignment_symbol); + + enum Origin { + S(Symbol), + L(Symbol), + } + + let mut element_symbols = std::vec::Vec::new(); + + // NOTE: this realizes all the list elements on the stack before they are put into the + // list. This turns out to be important. Creating the literals as we go causes issues with + // register usage. + // + // Of course this is inefficient when there are many elements (causes lots of stack + // spillage. + for (i, elem) in elements.iter().enumerate() { + match elem { + ListLiteralElement::Symbol(sym) => { + self.load_literal_symbols(&[*sym]); + element_symbols.push(Origin::S(*sym)); + } + ListLiteralElement::Literal(lit) => { + let sym = self.debug_symbol(&format!("lit_{i}")); + self.load_literal(&sym, element_in_layout, lit); + element_symbols.push(Origin::L(sym)); + } + } + } + + // The pointer already points to the first element + let ptr_reg = self + .storage_manager + .load_to_general_reg(&mut self.buf, &allocation_symbol); + + // Copy everything into output array. + let mut element_offset = 0; + for elem in element_symbols { + let element_symbol = match elem { + Origin::S(s) | Origin::L(s) => s, + }; + + Self::ptr_write( + &mut self.buf, + &mut self.storage_manager, + self.layout_interner, + ptr_reg, + element_offset, + element_width, + element_layout, + element_symbol, + ); + + element_offset += element_width as i32; + if let Origin::L(element_symbol) = elem { + self.free_symbol(&element_symbol); + } + } + + // Setup list on stack. + self.storage_manager.with_tmp_general_reg( + &mut self.buf, + |storage_manager, buf, tmp_reg| { + let alignment = Ord::max(8, element_alignment) as u32; + let base_offset = + storage_manager.claim_stack_area_with_alignment(*sym, 24, alignment); + ASM::mov_base32_reg64(buf, base_offset, ptr_reg); + + ASM::mov_reg64_imm64(buf, tmp_reg, elements.len() as i64); + ASM::mov_base32_reg64(buf, base_offset + 8, tmp_reg); + ASM::mov_base32_reg64(buf, base_offset + 16, tmp_reg); + }, + ); + self.free_symbol(&allocation_symbol); + } + + fn create_struct(&mut self, sym: &Symbol, layout: &InLayout<'a>, fields: &'a [Symbol]) { + self.storage_manager.create_struct( + self.layout_interner, + &mut self.buf, + sym, + layout, + fields, + ); + } + + fn load_struct_at_index( + &mut self, + sym: &Symbol, + structure: &Symbol, + index: u64, + field_layouts: &'a [InLayout<'a>], + ) { + self.storage_manager.load_field_at_index( + self.layout_interner, + sym, + structure, + index, + field_layouts, + ); + } + + fn load_union_at_index( + &mut self, + sym: &Symbol, + structure: &Symbol, + tag_id: TagIdIntType, + index: u64, + union_layout: &UnionLayout<'a>, + ) { + match union_layout { + UnionLayout::NonRecursive(tag_layouts) => { + self.storage_manager.load_field_at_index( + self.layout_interner, + sym, + structure, + index, + tag_layouts[tag_id as usize], + ); + } + UnionLayout::NonNullableUnwrapped(field_layouts) => { + let element_layout = field_layouts[index as usize]; + + let ptr_reg = self + .storage_manager + .load_to_general_reg(&mut self.buf, structure); + + let mut offset = 0; + for field in &field_layouts[..index as usize] { + offset += self.layout_interner.stack_size(*field); + } + + Self::ptr_read( + &mut self.buf, + &mut self.storage_manager, + self.layout_interner, + ptr_reg, + offset as i32, + element_layout, + *sym, + ); + } + UnionLayout::NullableUnwrapped { + nullable_id, + other_fields, + } => { + debug_assert_ne!(tag_id, *nullable_id as TagIdIntType); + + let element_layout = other_fields[index as usize]; + + let ptr_reg = self + .storage_manager + .load_to_general_reg(&mut self.buf, structure); + + let mut offset = 0; + for field in &other_fields[..index as usize] { + offset += self.layout_interner.stack_size(*field); + } + + Self::ptr_read( + &mut self.buf, + &mut self.storage_manager, + self.layout_interner, + ptr_reg, + offset as i32, + element_layout, + *sym, + ); + } + + UnionLayout::NullableWrapped { + nullable_id, + other_tags, + } => { + debug_assert_ne!(tag_id, *nullable_id as TagIdIntType); + + let other_fields = if tag_id < *nullable_id { + other_tags[tag_id as usize] + } else { + other_tags[tag_id as usize - 1] + }; + + let element_layout = other_fields[index as usize]; + + let ptr_reg = self + .storage_manager + .load_to_general_reg(&mut self.buf, structure); + + let (mask_symbol, mask_reg) = self.clear_tag_id(ptr_reg); + + let mut offset = 0; + for field in &other_fields[..index as usize] { + offset += self.layout_interner.stack_size(*field); + } + + Self::ptr_read( + &mut self.buf, + &mut self.storage_manager, + self.layout_interner, + mask_reg, + offset as i32, + element_layout, + *sym, + ); + + self.free_symbol(&mask_symbol) + } + UnionLayout::Recursive(tag_layouts) => { + let other_fields = tag_layouts[tag_id as usize]; + let element_layout = other_fields[index as usize]; + + let ptr_reg = self + .storage_manager + .load_to_general_reg(&mut self.buf, structure); + + // mask out the tag id bits + let (unmasked_symbol, unmasked_reg) = + if union_layout.stores_tag_id_as_data(self.storage_manager.target_info) { + (None, ptr_reg) + } else { + let (mask_symbol, mask_reg) = self.clear_tag_id(ptr_reg); + (Some(mask_symbol), mask_reg) + }; + + let mut offset = 0; + for field in &other_fields[..index as usize] { + offset += self.layout_interner.stack_size(*field); + } + + Self::ptr_read( + &mut self.buf, + &mut self.storage_manager, + self.layout_interner, + unmasked_reg, + offset as i32, + element_layout, + *sym, + ); + + if let Some(unmasked_symbol) = unmasked_symbol { + self.free_symbol(&unmasked_symbol); + } + } + } + } + + fn load_union_field_ptr_at_index( + &mut self, + sym: &Symbol, + structure: &Symbol, + tag_id: TagIdIntType, + index: u64, + union_layout: &UnionLayout<'a>, + ) { + let ptr_reg = self + .storage_manager + .load_to_general_reg(&mut self.buf, structure); + + let sym_reg = self.storage_manager.claim_general_reg(&mut self.buf, sym); + + match union_layout { + UnionLayout::NonRecursive(_) => { + unreachable!("operation not supported") + } + UnionLayout::NonNullableUnwrapped(field_layouts) => { + let mut offset = 0; + for field in &field_layouts[..index as usize] { + offset += self.layout_interner.stack_size(*field); + } + + ASM::add_reg64_reg64_imm32(&mut self.buf, sym_reg, ptr_reg, offset as i32); + } + UnionLayout::NullableUnwrapped { + nullable_id, + other_fields, + } => { + debug_assert_ne!(tag_id, *nullable_id as TagIdIntType); + + let mut offset = 0; + for field in &other_fields[..index as usize] { + offset += self.layout_interner.stack_size(*field); + } + + ASM::add_reg64_reg64_imm32(&mut self.buf, sym_reg, ptr_reg, offset as i32); + } + + UnionLayout::NullableWrapped { + nullable_id, + other_tags, + } => { + debug_assert_ne!(tag_id, *nullable_id as TagIdIntType); + + let other_fields = if tag_id < *nullable_id { + other_tags[tag_id as usize] + } else { + other_tags[tag_id as usize - 1] + }; + + let (mask_symbol, mask_reg) = self.clear_tag_id(ptr_reg); + + let mut offset = 0; + for field in &other_fields[..index as usize] { + offset += self.layout_interner.stack_size(*field); + } + + ASM::add_reg64_reg64_imm32(&mut self.buf, sym_reg, mask_reg, offset as i32); + + self.free_symbol(&mask_symbol); + } + UnionLayout::Recursive(tag_layouts) => { + let other_fields = tag_layouts[tag_id as usize]; + + let ptr_reg = self + .storage_manager + .load_to_general_reg(&mut self.buf, structure); + + // mask out the tag id bits + let (unmasked_symbol, unmasked_reg) = + if union_layout.stores_tag_id_as_data(self.storage_manager.target_info) { + (None, ptr_reg) + } else { + let (mask_symbol, mask_reg) = self.clear_tag_id(ptr_reg); + (Some(mask_symbol), mask_reg) + }; + + let mut offset = 0; + for field in &other_fields[..index as usize] { + offset += self.layout_interner.stack_size(*field); + } + + ASM::add_reg64_reg64_imm32(&mut self.buf, sym_reg, unmasked_reg, offset as i32); + + if let Some(unmasked_symbol) = unmasked_symbol { + self.free_symbol(&unmasked_symbol); + } + } + } + } + + fn build_ptr_store( + &mut self, + sym: Symbol, + ptr: Symbol, + value: Symbol, + element_layout: InLayout<'a>, + ) { + let ptr_reg = self + .storage_manager + .load_to_general_reg(&mut self.buf, &ptr); + + let element_width = self.layout_interner.stack_size(element_layout) as u64; + let element_offset = 0; + + let layout = self.layout_interner.get_repr(element_layout); + + Self::ptr_write( + &mut self.buf, + &mut self.storage_manager, + self.layout_interner, + ptr_reg, + element_offset, + element_width, + layout, + value, + ); + + if value == Symbol::DEV_TMP { + self.free_symbol(&value); + } + + // box is just a pointer on the stack + let base_offset = self.storage_manager.claim_pointer_stack_area(sym); + ASM::mov_base32_reg64(&mut self.buf, base_offset, ptr_reg); + } + + fn build_ptr_load(&mut self, sym: Symbol, ptr: Symbol, element_layout: InLayout<'a>) { + let ptr_reg = self + .storage_manager + .load_to_general_reg(&mut self.buf, &ptr); + + let offset = 0; + + Self::ptr_read( + &mut self.buf, + &mut self.storage_manager, + self.layout_interner, + ptr_reg, + offset, + element_layout, + sym, + ); + } + + fn build_ptr_clear_tag_id(&mut self, sym: Symbol, ptr: Symbol) { + let buf = &mut self.buf; + + let ptr_reg = self.storage_manager.load_to_general_reg(buf, &ptr); + let sym_reg = self.storage_manager.claim_general_reg(buf, &sym); + + ASM::mov_reg64_imm64(buf, sym_reg, !0b111); + ASM::and_reg64_reg64_reg64(buf, sym_reg, sym_reg, ptr_reg); + } + + fn build_alloca(&mut self, sym: Symbol, value: Option, element_layout: InLayout<'a>) { + // 1. acquire some stack space + let element_width = self.interner().stack_size(element_layout); + let allocation = self.debug_symbol("stack_allocation"); + let ptr = self.debug_symbol("ptr"); + + if element_width == 0 { + self.storage_manager.claim_pointer_stack_area(sym); + return; + } + + let base_offset = self.storage_manager.claim_stack_area_layout( + self.layout_interner, + allocation, + element_layout, + ); + + let ptr_reg = self.storage_manager.claim_general_reg(&mut self.buf, &ptr); + + ASM::mov_reg64_reg64(&mut self.buf, ptr_reg, CC::BASE_PTR_REG); + ASM::add_reg64_reg64_imm32(&mut self.buf, ptr_reg, ptr_reg, base_offset); + + if let Some(value) = value { + self.build_ptr_store(sym, ptr, value, element_layout); + } else { + // this is now a pointer to uninitialized memory! + let r = self.storage_manager.claim_general_reg(&mut self.buf, &sym); + ASM::mov_reg64_reg64(&mut self.buf, r, ptr_reg); + } + } + + fn expr_box( + &mut self, + sym: Symbol, + value: Symbol, + element_layout: InLayout<'a>, + reuse: Option, + ) { + let element_width_symbol = Symbol::DEV_TMP; + self.load_layout_stack_size(element_layout, element_width_symbol); + + // Load allocation alignment (u32) + let element_alignment_symbol = Symbol::DEV_TMP2; + self.load_layout_alignment(Layout::U32, element_alignment_symbol); + + let allocation = self.debug_symbol("allocation"); + + match reuse { + None => { + self.allocate_with_refcount( + allocation, + element_width_symbol, + element_alignment_symbol, + ); + } + Some(reuse) => { + self.allocate_with_refcount_if_null(allocation, reuse, element_layout); + } + }; + + self.free_symbol(&element_width_symbol); + self.free_symbol(&element_alignment_symbol); + + self.build_ptr_store(sym, allocation, value, element_layout); + + self.free_symbol(&allocation); + } + + fn expr_unbox(&mut self, dst: Symbol, ptr: Symbol, element_layout: InLayout<'a>) { + self.build_ptr_load(dst, ptr, element_layout) + } + + fn get_tag_id(&mut self, sym: &Symbol, structure: &Symbol, union_layout: &UnionLayout<'a>) { + let layout_interner: &mut STLayoutInterner<'a> = self.layout_interner; + let _buf: &mut Vec<'a, u8> = &mut self.buf; + match union_layout { + UnionLayout::NonRecursive(tags) => { + self.storage_manager.load_union_tag_id_nonrecursive( + layout_interner, + &mut self.buf, + sym, + structure, + tags, + ); + } + UnionLayout::NonNullableUnwrapped(_) => { + let dst_reg = self.storage_manager.claim_general_reg(&mut self.buf, sym); + ASM::mov_reg64_imm64(&mut self.buf, dst_reg, 0); + } + UnionLayout::NullableUnwrapped { nullable_id, .. } => { + // simple is_null check on the pointer + let tmp = Symbol::DEV_TMP5; + let reg = self.storage_manager.claim_general_reg(&mut self.buf, &tmp); + ASM::mov_reg64_imm64(&mut self.buf, reg, 0); + + let dst_reg = self.storage_manager.claim_general_reg(&mut self.buf, sym); + let src1_reg = reg; + let src2_reg = self + .storage_manager + .load_to_general_reg(&mut self.buf, structure); + + match *nullable_id { + true => { + ASM::eq_reg_reg_reg( + &mut self.buf, + RegisterWidth::W64, + dst_reg, + src1_reg, + src2_reg, + ); + } + false => { + ASM::neq_reg_reg_reg( + &mut self.buf, + RegisterWidth::W64, + dst_reg, + src1_reg, + src2_reg, + ); + } + } + + self.free_symbol(&tmp); + } + UnionLayout::NullableWrapped { + nullable_id, + other_tags, + } => { + let number_of_tags = other_tags.len() + 1; + + let dst_reg = self.storage_manager.claim_general_reg(&mut self.buf, sym); + + // build a table to index into with the value that we find + let nullable_id = *nullable_id as usize; + let it = std::iter::once(nullable_id) + .chain(0..nullable_id) + .chain(nullable_id + 1..number_of_tags); + + let table = self.debug_symbol("tag_id_table"); + let table_offset = self.storage_manager.claim_stack_area_with_alignment( + table, + (number_of_tags * 2) as _, + 8, + ); + + let mut offset = table_offset; + for i in it { + ASM::mov_reg64_imm64(&mut self.buf, dst_reg, i as i64); + ASM::mov_base32_reg16(&mut self.buf, offset, dst_reg); + + offset += 2; + } + + self.free_symbol(&table); + + // mask the 3 lowest bits + let tmp = Symbol::DEV_TMP5; + let reg = self.storage_manager.claim_general_reg(&mut self.buf, &tmp); + ASM::mov_reg64_imm64(&mut self.buf, reg, 0b111); + + let src1_reg = reg; + let src2_reg = self + .storage_manager + .load_to_general_reg(&mut self.buf, structure); + + ASM::and_reg64_reg64_reg64(&mut self.buf, dst_reg, src1_reg, src2_reg); + + // we're indexing into an array of u16, so double this index + // also the stack grows down, so negate the index + ASM::mov_reg64_imm64(&mut self.buf, reg, 2); + ASM::umul_reg64_reg64_reg64( + &mut self.buf, + &mut self.storage_manager, + dst_reg, + dst_reg, + reg, + ); + + // index into the table + ASM::add_reg64_reg64_reg64(&mut self.buf, dst_reg, dst_reg, CC::BASE_PTR_REG); + + // load the 16-bit value at the pointer + ASM::mov_reg16_mem16_offset32(&mut self.buf, dst_reg, dst_reg, table_offset); + + // keep only the lowest 16 bits + ASM::mov_reg64_imm64(&mut self.buf, reg, 0xFFFF); + ASM::and_reg64_reg64_reg64(&mut self.buf, dst_reg, dst_reg, reg); + + self.free_symbol(&tmp); + } + + UnionLayout::Recursive(_) => { + let dst_reg = self.storage_manager.claim_general_reg(&mut self.buf, sym); + + let target_info = self.storage_manager.target_info; + if union_layout.stores_tag_id_as_data(target_info) { + let offset = union_layout.tag_id_offset(self.interner()).unwrap() as i32; + + let ptr_reg = self + .storage_manager + .load_to_general_reg(&mut self.buf, structure); + + match union_layout.tag_id_layout() { + Layout::U8 => { + ASM::mov_reg8_mem8_offset32(&mut self.buf, dst_reg, ptr_reg, offset); + ASM::movzx_reg_reg(&mut self.buf, RegisterWidth::W8, dst_reg, dst_reg) + } + Layout::U16 => { + ASM::mov_reg16_mem16_offset32(&mut self.buf, dst_reg, ptr_reg, offset); + ASM::movzx_reg_reg(&mut self.buf, RegisterWidth::W16, dst_reg, dst_reg) + } + _ => unreachable!(), + } + } else { + // mask the 3 lowest bits + let tmp = Symbol::DEV_TMP5; + let reg = self.storage_manager.claim_general_reg(&mut self.buf, &tmp); + ASM::mov_reg64_imm64(&mut self.buf, reg, 0b111); + + let src1_reg = reg; + let src2_reg = self + .storage_manager + .load_to_general_reg(&mut self.buf, structure); + + ASM::and_reg64_reg64_reg64(&mut self.buf, dst_reg, src1_reg, src2_reg); + + self.free_symbol(&tmp); + } + } + }; + } + + fn tag( + &mut self, + sym: &Symbol, + fields: &'a [Symbol], + union_layout: &UnionLayout<'a>, + tag_id: TagIdIntType, + reuse: Option, + ) { + let layout_interner: &mut STLayoutInterner<'a> = self.layout_interner; + let buf: &mut Vec<'a, u8> = &mut self.buf; + + let (data_size, data_alignment) = union_layout.data_size_and_alignment(layout_interner); + + match union_layout { + UnionLayout::NonRecursive(field_layouts) => { + let id_offset = data_size - data_alignment; + let base_offset = self.storage_manager.claim_stack_area_with_alignment( + *sym, + data_size, + data_alignment, + ); + let mut current_offset = base_offset; + + let it = fields.iter().zip(field_layouts[tag_id as usize].iter()); + for (field, field_layout) in it { + self.storage_manager.copy_symbol_to_stack_offset( + layout_interner, + buf, + current_offset, + field, + field_layout, + ); + let field_size = layout_interner.stack_size(*field_layout); + current_offset += field_size as i32; + } + + // put the tag id in the right place + self.storage_manager + .with_tmp_general_reg(buf, |_symbol_storage, buf, reg| { + ASM::mov_reg64_imm64(buf, reg, tag_id as i64); + + let total_id_offset = base_offset as u32 + id_offset; + debug_assert_eq!(total_id_offset % data_alignment, 0); + + // pick the right instruction based on the alignment of the tag id + if field_layouts.len() <= u8::MAX as _ { + ASM::mov_base32_reg8(buf, total_id_offset as i32, reg); + } else { + ASM::mov_base32_reg16(buf, total_id_offset as i32, reg); + } + }); + } + UnionLayout::NonNullableUnwrapped(field_layouts) => { + // construct the payload as a struct on the stack + let temp_sym = Symbol::DEV_TMP5; + let layout = self + .layout_interner + .insert_direct_no_semantic(LayoutRepr::Struct(field_layouts)); + + self.load_literal_symbols(fields); + self.storage_manager.create_struct( + self.layout_interner, + &mut self.buf, + &temp_sym, + &layout, + fields, + ); + + // now effectively box this struct + self.expr_box(*sym, temp_sym, layout, reuse); + + self.free_symbol(&temp_sym); + } + UnionLayout::NullableUnwrapped { + nullable_id, + other_fields, + } => { + if tag_id == *nullable_id as TagIdIntType { + // it's just a null pointer + self.load_literal_i64(sym, 0); + } else { + // construct the payload as a struct on the stack + let temp_sym = Symbol::DEV_TMP5; + let layout = self + .layout_interner + .insert_direct_no_semantic(LayoutRepr::Struct(other_fields)); + + self.load_literal_symbols(fields); + self.storage_manager.create_struct( + self.layout_interner, + &mut self.buf, + &temp_sym, + &layout, + fields, + ); + + // now effectively box this struct + self.expr_box(*sym, temp_sym, layout, reuse); + + self.free_symbol(&temp_sym); + } + } + UnionLayout::NullableWrapped { + nullable_id, + other_tags, + } => { + let nullable_id = *nullable_id; + + if tag_id == nullable_id as TagIdIntType { + // it's just a null pointer + self.load_literal_i64(sym, 0); + } else { + let (largest_variant_fields, _largest_variant_size) = other_tags + .iter() + .map(|fields| { + let struct_layout = self + .layout_interner + .insert_direct_no_semantic(LayoutRepr::Struct(fields)); + + ( + struct_layout, + self.layout_interner.stack_size(struct_layout), + ) + }) + .max_by(|(_, a), (_, b)| a.cmp(b)) + .unwrap(); + + let largest_variant = + if union_layout.stores_tag_id_as_data(self.storage_manager.target_info) { + self.layout_interner + .insert_direct_no_semantic(LayoutRepr::Struct( + self.env.arena.alloc([largest_variant_fields, Layout::U8]), + )) + } else { + largest_variant_fields + }; + + let other_fields = if tag_id < nullable_id { + other_tags[tag_id as usize] + } else { + other_tags[tag_id as usize - 1] + }; + + // construct the payload as a struct on the stack + let temp_sym = Symbol::DEV_TMP5; + let layout = self + .layout_interner + .insert_direct_no_semantic(LayoutRepr::Struct(other_fields)); + + self.load_literal_symbols(fields); + self.storage_manager.create_struct( + self.layout_interner, + &mut self.buf, + &temp_sym, + &layout, + fields, + ); + + let tag_id_symbol = self.debug_symbol("tag_id"); + + // index zero is taken up by the nullable tag, so any tags before it in the + // ordering need to be incremented by one + let pointer_tag = if tag_id < nullable_id { + tag_id + 1 + } else { + tag_id + }; + + // finally, we need to tag the pointer + if union_layout.stores_tag_id_as_data(self.storage_manager.target_info) { + // allocate space on the stack for the whole tag payload + let scratch_space = self.debug_symbol("scratch_space"); + let (data_size, data_alignment) = + union_layout.data_size_and_alignment(self.layout_interner); + let to_offset = self.storage_manager.claim_stack_area_with_alignment( + scratch_space, + data_size, + data_alignment, + ); + + // copy the inner struct into the reserved space + let (from_offset, variant_size) = + self.storage_manager.stack_offset_and_size(&temp_sym); + self.storage_manager.copy_to_stack_offset( + &mut self.buf, + variant_size, + from_offset, + to_offset, + ); + + // create the tag id + { + self.load_literal_i64(&tag_id_symbol, tag_id as _); + let tag_id_offset = union_layout.tag_id_offset(self.interner()); + + self.storage_manager + .ensure_symbol_on_stack(&mut self.buf, &tag_id_symbol); + let (from_offset, tag_id_size) = + self.storage_manager.stack_offset_and_size(&tag_id_symbol); + let to_offset = to_offset + tag_id_offset.unwrap() as i32; + + // move the tag id into position + self.storage_manager.copy_to_stack_offset( + &mut self.buf, + tag_id_size, + from_offset, + to_offset, + ); + } + + // now effectively box whole data + id struct + self.expr_box(*sym, scratch_space, largest_variant, reuse); + + self.free_symbol(&tag_id_symbol); + self.free_symbol(&scratch_space); + self.free_symbol(&temp_sym); + } else { + // now effectively box this struct + let untagged_pointer_symbol = self.debug_symbol("untagged_pointer"); + self.expr_box(untagged_pointer_symbol, temp_sym, layout, reuse); + + self.free_symbol(&temp_sym); + + self.load_literal_i64(&tag_id_symbol, pointer_tag as _); + + self.build_int_bitwise_or( + sym, + &untagged_pointer_symbol, + &tag_id_symbol, + IntWidth::U64, + ); + + self.free_symbol(&untagged_pointer_symbol); + self.free_symbol(&tag_id_symbol); + self.free_symbol(&temp_sym); + } + } + } + UnionLayout::Recursive(tags) => { + self.load_literal_symbols(fields); + + let tag_id_symbol = self.debug_symbol("tag_id_symbol"); + let other_fields = tags[tag_id as usize]; + + let stores_tag_id_as_data = + union_layout.stores_tag_id_as_data(self.storage_manager.target_info); + + let (largest_variant_fields, _largest_variant_size) = tags + .iter() + .map(|fields| { + let struct_layout = self + .layout_interner + .insert_direct_no_semantic(LayoutRepr::Struct(fields)); + + ( + struct_layout, + self.layout_interner.stack_size(struct_layout), + ) + }) + .max_by(|(_, a), (_, b)| a.cmp(b)) + .unwrap(); + + let largest_variant = if stores_tag_id_as_data { + self.layout_interner + .insert_direct_no_semantic(LayoutRepr::Struct( + self.env.arena.alloc([largest_variant_fields, Layout::U8]), + )) + } else { + largest_variant_fields + }; + + // construct the payload as a struct on the stack + let data_struct_layout = self + .layout_interner + .insert_direct_no_semantic(LayoutRepr::Struct(other_fields)); + + if stores_tag_id_as_data { + self.load_literal_symbols(fields); + + // create the data as a record + let inner_struct_symbol = self.debug_symbol("inner_struct_symbol"); + self.storage_manager.create_struct( + self.layout_interner, + &mut self.buf, + &inner_struct_symbol, + &data_struct_layout, + fields, + ); + + // allocate space on the stack for the whole tag payload + let scratch_space = self.debug_symbol("scratch_space"); + let (data_size, data_alignment) = + union_layout.data_size_and_alignment(self.layout_interner); + let to_offset = self.storage_manager.claim_stack_area_with_alignment( + scratch_space, + data_size, + data_alignment, + ); + + // copy the inner struct into the reserved space + let (from_offset, variant_size) = self + .storage_manager + .stack_offset_and_size(&inner_struct_symbol); + self.storage_manager.copy_to_stack_offset( + &mut self.buf, + variant_size, + from_offset, + to_offset, + ); + + // create the tag id + { + self.load_literal_i64(&tag_id_symbol, tag_id as _); + let tag_id_offset = union_layout.tag_id_offset(self.interner()); + + self.storage_manager + .ensure_symbol_on_stack(&mut self.buf, &tag_id_symbol); + let (from_offset, tag_id_size) = + self.storage_manager.stack_offset_and_size(&tag_id_symbol); + let to_offset = to_offset + tag_id_offset.unwrap() as i32; + + // move the tag id into position + self.storage_manager.copy_to_stack_offset( + &mut self.buf, + tag_id_size, + from_offset, + to_offset, + ); + } + + // now effectively box whole data + id struct + self.expr_box(*sym, scratch_space, largest_variant, reuse); + + self.free_symbol(&tag_id_symbol); + self.free_symbol(&inner_struct_symbol); + } else { + self.load_literal_symbols(fields); + let whole_struct_symbol = self.debug_symbol("whole_struct_symbol"); + self.storage_manager.create_struct( + self.layout_interner, + &mut self.buf, + &whole_struct_symbol, + &data_struct_layout, + fields, + ); + + let (data_size, data_alignment) = + union_layout.data_size_and_alignment(self.layout_interner); + let scratch_space = self.debug_symbol("scratch_space"); + let to_offset = self.storage_manager.claim_stack_area_with_alignment( + scratch_space, + data_size, + data_alignment, + ); + + // this is a cheaty copy, because the destination may be wider than the source + let (from_offset, _) = + self.storage_manager.stack_offset_and_size(&scratch_space); + self.storage_manager.copy_to_stack_offset( + &mut self.buf, + data_size, + from_offset, + to_offset, + ); + + // now effectively box this struct + let untagged_pointer_symbol = self.debug_symbol("untagged_pointer"); + self.expr_box( + untagged_pointer_symbol, + whole_struct_symbol, + largest_variant, + reuse, + ); + + self.free_symbol(&whole_struct_symbol); + + // finally, we need to tag the pointer + debug_assert!(tag_id < 8); + self.load_literal_i64(&tag_id_symbol, tag_id as _); + + self.build_int_bitwise_or( + sym, + &untagged_pointer_symbol, + &tag_id_symbol, + IntWidth::U64, + ); + + self.free_symbol(&untagged_pointer_symbol); + self.free_symbol(&tag_id_symbol); + } + } + } + } + + fn load_literal(&mut self, sym: &Symbol, layout: &InLayout<'a>, lit: &Literal<'a>) { + let layout = self.layout_interner.get_repr(*layout); + + if let LayoutRepr::LambdaSet(lambda_set) = layout { + return self.load_literal(sym, &lambda_set.runtime_representation(), lit); + } + + match (lit, layout) { + ( + Literal::Int(x), + LayoutRepr::Builtin(Builtin::Int( + IntWidth::U8 + | IntWidth::U16 + | IntWidth::U32 + | IntWidth::U64 + | IntWidth::I8 + | IntWidth::I16 + | IntWidth::I32 + | IntWidth::I64, + )), + ) => { + let reg = self.storage_manager.claim_general_reg(&mut self.buf, sym); + let val = *x; + ASM::mov_reg64_imm64(&mut self.buf, reg, i128::from_ne_bytes(val) as i64); + } + ( + Literal::Int(bytes) | Literal::U128(bytes), + LayoutRepr::Builtin(Builtin::Int(IntWidth::I128 | IntWidth::U128)), + ) => { + self.storage_manager.with_tmp_general_reg( + &mut self.buf, + |storage_manager, buf, reg| { + let base_offset = storage_manager.claim_stack_area_layout( + self.layout_interner, + *sym, + Layout::U128, + ); + + let mut num_bytes = [0; 8]; + num_bytes.copy_from_slice(&bytes[..8]); + let num = i64::from_ne_bytes(num_bytes); + ASM::mov_reg64_imm64(buf, reg, num); + ASM::mov_base32_reg64(buf, base_offset, reg); + + num_bytes.copy_from_slice(&bytes[8..16]); + let num = i64::from_ne_bytes(num_bytes); + ASM::mov_reg64_imm64(buf, reg, num); + ASM::mov_base32_reg64(buf, base_offset + 8, reg); + }, + ); + } + (Literal::Byte(x), LayoutRepr::Builtin(Builtin::Int(IntWidth::U8 | IntWidth::I8))) => { + let reg = self.storage_manager.claim_general_reg(&mut self.buf, sym); + let val = *x; + ASM::mov_reg64_imm64(&mut self.buf, reg, val as i64); + } + (Literal::Bool(x), LayoutRepr::Builtin(Builtin::Bool)) => { + let reg = self.storage_manager.claim_general_reg(&mut self.buf, sym); + ASM::mov_reg64_imm64(&mut self.buf, reg, *x as i64); + } + (Literal::Float(x), LayoutRepr::Builtin(Builtin::Float(FloatWidth::F64))) => { + let freg = self.storage_manager.claim_float_reg(&mut self.buf, sym); + let val = *x; + ASM::mov_freg64_imm64(&mut self.buf, &mut self.relocs, freg, val); + } + (Literal::Float(x), LayoutRepr::Builtin(Builtin::Float(FloatWidth::F32))) => { + let freg = self.storage_manager.claim_float_reg(&mut self.buf, sym); + let val = *x as f32; + ASM::mov_freg32_imm32(&mut self.buf, &mut self.relocs, freg, val); + } + (Literal::Decimal(bytes), LayoutRepr::Builtin(Builtin::Decimal)) => { + self.storage_manager.with_tmp_general_reg( + &mut self.buf, + |storage_manager, buf, reg| { + let base_offset = storage_manager.claim_stack_area_layout( + self.layout_interner, + *sym, + Layout::DEC, + ); + + let mut num_bytes = [0; 8]; + num_bytes.copy_from_slice(&bytes[..8]); + let num = i64::from_ne_bytes(num_bytes); + ASM::mov_reg64_imm64(buf, reg, num); + ASM::mov_base32_reg64(buf, base_offset, reg); + + num_bytes.copy_from_slice(&bytes[8..16]); + let num = i64::from_ne_bytes(num_bytes); + ASM::mov_reg64_imm64(buf, reg, num); + ASM::mov_base32_reg64(buf, base_offset + 8, reg); + }, + ); + } + (Literal::Str(x), LayoutRepr::Builtin(Builtin::Str)) => { + if x.len() < 24 { + // Load small string. + self.storage_manager.with_tmp_general_reg( + &mut self.buf, + |storage_manager, buf, reg| { + let base_offset = storage_manager.claim_stack_area_layout( + self.layout_interner, + *sym, + Layout::STR, + ); + + let mut bytes = [0; 24]; + bytes[..x.len()].copy_from_slice(x.as_bytes()); + bytes[23] = (x.len() as u8) | 0b1000_0000; + + let mut num_bytes = [0; 8]; + num_bytes.copy_from_slice(&bytes[..8]); + let num = i64::from_ne_bytes(num_bytes); + ASM::mov_reg64_imm64(buf, reg, num); + ASM::mov_base32_reg64(buf, base_offset, reg); + + num_bytes.copy_from_slice(&bytes[8..16]); + let num = i64::from_ne_bytes(num_bytes); + ASM::mov_reg64_imm64(buf, reg, num); + ASM::mov_base32_reg64(buf, base_offset + 8, reg); + + num_bytes.copy_from_slice(&bytes[16..]); + let num = i64::from_ne_bytes(num_bytes); + ASM::mov_reg64_imm64(buf, reg, num); + ASM::mov_base32_reg64(buf, base_offset + 16, reg); + }, + ); + } else { + // load large string (pretend it's a `List U8`). We should move this data into + // the binary eventually because our RC algorithm won't free this value + let elements: Vec<_> = x + .as_bytes() + .iter() + .map(|b| ListLiteralElement::Literal(Literal::Byte(*b))) + .collect_in(self.storage_manager.env.arena); + + self.create_array(sym, &Layout::U8, elements.into_bump_slice()) + } + } + _ => todo!("loading literal {:?} with layout {:?}", lit, layout), + } + } + + fn free_symbol(&mut self, sym: &Symbol) { + self.join_map.remove(&JoinPointId(*sym)); + self.storage_manager.free_symbol(sym); + } + + fn return_symbol(&mut self, sym: &Symbol, layout: &InLayout<'a>) { + let repr = self.layout_interner.get_repr(*layout); + if self.storage_manager.is_stored_primitive(sym) { + // Just load it to the correct type of reg as a stand alone value. + match repr { + single_register_integers!() | pointer_layouts!() => { + self.storage_manager.load_to_specified_general_reg( + &mut self.buf, + sym, + CC::GENERAL_RETURN_REGS[0], + ); + } + single_register_floats!() => { + self.storage_manager.load_to_specified_float_reg( + &mut self.buf, + sym, + CC::FLOAT_RETURN_REGS[0], + ); + } + LayoutRepr::LambdaSet(lambda_set) => { + self.return_symbol(sym, &lambda_set.runtime_representation()) + } + LayoutRepr::Struct([]) => { + // there is nothing to do here + } + LayoutRepr::Union(UnionLayout::NonRecursive(_)) + | LayoutRepr::Builtin(_) + | LayoutRepr::Struct(_) + | LayoutRepr::Erased(_) => { + internal_error!( + "All primitive values should fit in a single register {repr:?}" + ); + } + } + } else { + CC::return_complex_symbol( + &mut self.buf, + &mut self.storage_manager, + self.layout_interner, + sym, + layout, + ) + } + let inst_loc = self.buf.len() as u64; + let offset = ASM::jmp_imm32(&mut self.buf, JUMP_PLACEHOLDER) as u64; + self.relocs.push(Relocation::JmpToReturn { + inst_loc, + inst_size: self.buf.len() as u64 - inst_loc, + offset, + }); + } + + fn build_int_bitwise_and( + &mut self, + dst: &Symbol, + src1: &Symbol, + src2: &Symbol, + int_width: IntWidth, + ) { + let buf = &mut self.buf; + + match int_width { + IntWidth::U128 | IntWidth::I128 => todo!(), + _ => { + let dst_reg = self.storage_manager.claim_general_reg(buf, dst); + let src1_reg = self.storage_manager.load_to_general_reg(buf, src1); + let src2_reg = self.storage_manager.load_to_general_reg(buf, src2); + ASM::and_reg64_reg64_reg64(buf, dst_reg, src1_reg, src2_reg); + } + } + } + + fn build_int_bitwise_or( + &mut self, + dst: &Symbol, + src1: &Symbol, + src2: &Symbol, + int_width: IntWidth, + ) { + let buf = &mut self.buf; + + match int_width { + IntWidth::U128 | IntWidth::I128 => todo!(), + _ => { + let dst_reg = self.storage_manager.claim_general_reg(buf, dst); + let src1_reg = self.storage_manager.load_to_general_reg(buf, src1); + let src2_reg = self.storage_manager.load_to_general_reg(buf, src2); + ASM::or_reg64_reg64_reg64(buf, dst_reg, src1_reg, src2_reg); + } + } + } + + fn build_int_bitwise_xor( + &mut self, + dst: &Symbol, + src1: &Symbol, + src2: &Symbol, + int_width: IntWidth, + ) { + let buf = &mut self.buf; + + match int_width { + IntWidth::U128 | IntWidth::I128 => todo!(), + _ => { + let dst_reg = self.storage_manager.claim_general_reg(buf, dst); + let src1_reg = self.storage_manager.load_to_general_reg(buf, src1); + let src2_reg = self.storage_manager.load_to_general_reg(buf, src2); + ASM::xor_reg64_reg64_reg64(buf, dst_reg, src1_reg, src2_reg); + } + } + } + + fn build_int_shift_left( + &mut self, + dst: &Symbol, + src1: &Symbol, + src2: &Symbol, + int_width: IntWidth, + ) { + let buf = &mut self.buf; + + match int_width { + IntWidth::U128 | IntWidth::I128 => todo!(), + _ => { + let dst_reg = self.storage_manager.claim_general_reg(buf, dst); + let src1_reg = self.storage_manager.load_to_general_reg(buf, src1); + let src2_reg = self.storage_manager.load_to_general_reg(buf, src2); + + ASM::shl_reg64_reg64_reg64( + buf, + &mut self.storage_manager, + dst_reg, + src1_reg, + src2_reg, + ); + } + } + } + + fn build_int_shift_right( + &mut self, + dst: &Symbol, + src1: &Symbol, + src2: &Symbol, + int_width: IntWidth, + ) { + let buf = &mut self.buf; + + match int_width { + IntWidth::U128 | IntWidth::I128 => todo!(), + _ => { + let dst_reg = self.storage_manager.claim_general_reg(buf, dst); + let src1_reg = self.storage_manager.load_to_general_reg(buf, src1); + let src2_reg = self.storage_manager.load_to_general_reg(buf, src2); + + // to get sign extension "for free", we move our bits to the left + // so the integers sign bit is stored in the register's sign bit. + // Then we arithmetic shift right, getting the correct sign extension behavior, + // then shift logical right to get the bits back into the position they should + // be for our particular integer width + let sign_extend_shift_amount = 64 - (int_width.stack_size() as i64 * 8); + + if sign_extend_shift_amount > 0 { + self.storage_manager.with_tmp_general_reg( + buf, + |storage_manager, buf, tmp_reg| { + ASM::mov_reg64_imm64(buf, tmp_reg, sign_extend_shift_amount); + ASM::shl_reg64_reg64_reg64( + buf, + storage_manager, + src1_reg, + src1_reg, + tmp_reg, + ); + }, + ) + } + + ASM::sar_reg64_reg64_reg64( + buf, + &mut self.storage_manager, + dst_reg, + src1_reg, + src2_reg, + ); + + if sign_extend_shift_amount > 0 { + // shift back if needed + self.storage_manager.with_tmp_general_reg( + &mut self.buf, + |storage_manager, buf, tmp_reg| { + ASM::mov_reg64_imm64(buf, tmp_reg, sign_extend_shift_amount); + ASM::shr_reg64_reg64_reg64( + buf, + storage_manager, + dst_reg, + dst_reg, + tmp_reg, + ); + }, + ) + } + } + } + } + + fn build_int_shift_right_zero_fill( + &mut self, + dst: &Symbol, + src1: &Symbol, + src2: &Symbol, + int_width: IntWidth, + ) { + let buf = &mut self.buf; + + match int_width { + IntWidth::U128 | IntWidth::I128 => { + let layout = match int_width { + IntWidth::I128 => Layout::I128, + IntWidth::U128 => Layout::U128, + _ => unreachable!(), + }; + + self.build_fn_call( + dst, + bitcode::NUM_SHIFT_RIGHT_ZERO_FILL[int_width].to_string(), + &[*src1, *src2], + &[layout, layout], + &layout, + ); + } + _ => { + let dst_reg = self.storage_manager.claim_general_reg(buf, dst); + let src1_reg = self.storage_manager.load_to_general_reg(buf, src1); + let src2_reg = self.storage_manager.load_to_general_reg(buf, src2); + + ASM::shr_reg64_reg64_reg64( + buf, + &mut self.storage_manager, + dst_reg, + src1_reg, + src2_reg, + ); + } + } + } + + fn build_num_sqrt(&mut self, dst: Symbol, src: Symbol, float_width: FloatWidth) { + let buf = &mut self.buf; + + let dst_reg = self.storage_manager.claim_float_reg(buf, &dst); + let src_reg = self.storage_manager.load_to_float_reg(buf, &src); + + match float_width { + FloatWidth::F32 => ASM::sqrt_freg32_freg32(buf, dst_reg, src_reg), + FloatWidth::F64 => ASM::sqrt_freg64_freg64(buf, dst_reg, src_reg), + } + } + + fn build_num_int_cast( + &mut self, + dst: &Symbol, + src: &Symbol, + source: IntWidth, + target: IntWidth, + ) { + use IntWidth::*; + + let buf = &mut self.buf; + + match (source, target) { + (U128, U64) => { + let dst_reg = self.storage_manager.claim_general_reg(buf, dst); + + let (offset, _size) = self.storage_manager.stack_offset_and_size(src); + + ASM::mov_reg64_base32(buf, dst_reg, offset + 8); + + return; + } + (U64, U128) => { + let src_reg = self.storage_manager.load_to_general_reg(buf, src); + + let base_offset = self.storage_manager.claim_stack_area_layout( + self.layout_interner, + *dst, + Layout::U128, + ); + + let tmp = Symbol::DEV_TMP; + let tmp_reg = self.storage_manager.claim_general_reg(buf, &tmp); + + // move a zero into the lower 8 bytes + ASM::mov_reg64_imm64(buf, tmp_reg, 0x0); + ASM::mov_base32_reg64(buf, base_offset, tmp_reg); + + ASM::mov_base32_reg64(buf, base_offset + 8, src_reg); + + self.free_symbol(&tmp); + + return; + } + + _ => {} + } + + let dst_reg = self.storage_manager.claim_general_reg(buf, dst); + let src_reg = self.storage_manager.load_to_general_reg(buf, src); + + if source.stack_size() == target.stack_size() { + match source.stack_size() { + 8 => ASM::mov_reg64_reg64(buf, dst_reg, src_reg), + 4 => ASM::mov_reg32_reg32(buf, dst_reg, src_reg), + 2 => ASM::mov_reg16_reg16(buf, dst_reg, src_reg), + 1 => ASM::mov_reg8_reg8(buf, dst_reg, src_reg), + _ => todo!("int cast from {source:?} to {target:?}"), + } + } else { + match (source, target) { + // -- CASTING UP -- + (I8 | U8, U16 | U32 | U64) => { + // zero out the register + ASM::xor_reg64_reg64_reg64(buf, dst_reg, dst_reg, dst_reg); + + // move the 8-bit integer + ASM::mov_reg_reg(buf, RegisterWidth::W8, dst_reg, src_reg); + } + (U16, U32 | U64) => { + // zero out the register + ASM::xor_reg64_reg64_reg64(buf, dst_reg, dst_reg, dst_reg); + + // move the 16-bit integer + ASM::mov_reg_reg(buf, RegisterWidth::W16, dst_reg, src_reg); + } + (U32, U64) => { + // zero out the register + ASM::xor_reg64_reg64_reg64(buf, dst_reg, dst_reg, dst_reg); + + // move the 32-bit integer + ASM::mov_reg_reg(buf, RegisterWidth::W32, dst_reg, src_reg); + } + (I8, I16 | I32 | I64) => { + ASM::movsx_reg_reg(buf, RegisterWidth::W8, dst_reg, src_reg) + } + (I16, I32 | I64) => ASM::movsx_reg_reg(buf, RegisterWidth::W16, dst_reg, src_reg), + (I32, I64) => ASM::movsx_reg_reg(buf, RegisterWidth::W32, dst_reg, src_reg), + // -- CASTING DOWN -- + (U64 | I64, I32 | U32) => { + // move as a 32-bit integer (leaving any other bits behind) + ASM::mov_reg_reg(buf, RegisterWidth::W32, dst_reg, src_reg); + } + (U64 | I64 | U32 | I32, I16 | U16) => { + // move as a 16-bit integer (leaving any other bits behind) + ASM::mov_reg_reg(buf, RegisterWidth::W16, dst_reg, src_reg); + } + (U64 | I64 | U32 | I32 | U16 | I16, I8 | U8) => { + // move as an 8-bit integer (leaving any other bits behind) + ASM::mov_reg_reg(buf, RegisterWidth::W8, dst_reg, src_reg); + } + _ => todo!("int cast from {source:?} to {target:?}"), + } + } + } +} + +/// This impl block is for ir related instructions that need backend specific information. +/// For example, loading a symbol for doing a computation. +impl< + 'a, + 'r, + FloatReg: RegTrait, + GeneralReg: RegTrait, + ASM: Assembler, + CC: CallConv, + > Backend64Bit<'a, 'r, GeneralReg, FloatReg, ASM, CC> +{ + fn build_fn_call_stack_return( + &mut self, + function_name: String, + arguments: &[Symbol; N], + layouts: &[InLayout<'a>; N], + ret_layout: InLayout<'a>, + dst: Symbol, + ) { + // Setup the return location. + let base_offset = + self.storage_manager + .claim_stack_area_layout(self.layout_interner, dst, ret_layout); + + let tmp = self.debug_symbol("call_with_stack_return_result"); + + self.build_fn_call( + &tmp, + function_name, + arguments.as_slice(), + layouts.as_slice(), + &ret_layout, + ); + + // Return list value from fn call + self.storage_manager.copy_symbol_to_stack_offset( + self.layout_interner, + &mut self.buf, + base_offset, + &tmp, + &ret_layout, + ); + + self.free_symbol(&tmp); + } + + fn clear_tag_id(&mut self, ptr_reg: GeneralReg) -> (Symbol, GeneralReg) { + let unmasked_symbol = self.debug_symbol("unmasked"); + let unmasked_reg = self + .storage_manager + .claim_general_reg(&mut self.buf, &unmasked_symbol); + + ASM::mov_reg64_imm64(&mut self.buf, unmasked_reg, (!0b111) as _); + + ASM::and_reg64_reg64_reg64(&mut self.buf, unmasked_reg, ptr_reg, unmasked_reg); + + (unmasked_symbol, unmasked_reg) + } + + fn num_to_f32(&mut self, dst: &Symbol, src: &Symbol, arg_layout: &InLayout<'a>) { + let dst_reg = self.storage_manager.claim_float_reg(&mut self.buf, dst); + match self.layout_interner.get_repr(*arg_layout) { + LayoutRepr::Builtin(Builtin::Int(IntWidth::I32 | IntWidth::I64)) => { + let src_reg = self.storage_manager.load_to_general_reg(&mut self.buf, src); + ASM::to_float_freg32_reg64(&mut self.buf, dst_reg, src_reg); + } + LayoutRepr::Builtin(Builtin::Float(FloatWidth::F64)) => { + let src_reg = self.storage_manager.load_to_float_reg(&mut self.buf, src); + ASM::to_float_freg32_freg64(&mut self.buf, dst_reg, src_reg); + } + LayoutRepr::Builtin(Builtin::Float(FloatWidth::F32)) => { + let src_reg = self.storage_manager.load_to_float_reg(&mut self.buf, src); + ASM::mov_freg64_freg64(&mut self.buf, dst_reg, src_reg); + } + arg => todo!("NumToFrac: layout, arg {arg:?}, ret {:?}", Layout::F32), + } + } + + fn num_to_f64(&mut self, dst: &Symbol, src: &Symbol, arg_layout: &InLayout<'a>) { + let dst_reg = self.storage_manager.claim_float_reg(&mut self.buf, dst); + match self.layout_interner.get_repr(*arg_layout) { + LayoutRepr::Builtin(Builtin::Int(IntWidth::I32 | IntWidth::I64)) => { + let src_reg = self.storage_manager.load_to_general_reg(&mut self.buf, src); + ASM::to_float_freg64_reg64(&mut self.buf, dst_reg, src_reg); + } + LayoutRepr::Builtin(Builtin::Float(FloatWidth::F32)) => { + let src_reg = self.storage_manager.load_to_float_reg(&mut self.buf, src); + ASM::to_float_freg64_freg32(&mut self.buf, dst_reg, src_reg); + } + LayoutRepr::Builtin(Builtin::Float(FloatWidth::F64)) => { + let src_reg = self.storage_manager.load_to_float_reg(&mut self.buf, src); + ASM::mov_freg64_freg64(&mut self.buf, dst_reg, src_reg); + } + arg => todo!("NumToFrac: layout, arg {arg:?}, ret {:?}", Layout::F64), + } + } + + fn num_to_dec(&mut self, dst: &Symbol, src: &Symbol, arg_layout: &InLayout<'a>) { + match self.layout_interner.get_repr(*arg_layout) { + LayoutRepr::Builtin(Builtin::Int(int_width)) => { + self.build_fn_call( + dst, + bitcode::DEC_FROM_INT[int_width].to_string(), + &[*src], + &[*arg_layout], + &Layout::DEC, + ); + } + + arg => todo!("NumToFrac: layout, arg {arg:?}, ret {:?}", Layout::DEC), + } + } + + fn compare_128bit( + &mut self, + op: CompareOperation, + dst: &Symbol, + src1: &Symbol, + src2: &Symbol, + width: IntWidth, + ) { + let intrinsic = match op { + CompareOperation::LessThan => &bitcode::NUM_LESS_THAN[width], + CompareOperation::LessThanOrEqual => &bitcode::NUM_LESS_THAN_OR_EQUAL[width], + CompareOperation::GreaterThan => &bitcode::NUM_GREATER_THAN[width], + CompareOperation::GreaterThanOrEqual => &bitcode::NUM_GREATER_THAN_OR_EQUAL[width], + }; + + self.build_fn_call( + dst, + intrinsic.to_string(), + &[*src1, *src2], + &[Layout::U128, Layout::U128], + &Layout::U8, + ); + } + + fn compare( + &mut self, + op: CompareOperation, + dst: &Symbol, + src1: &Symbol, + src2: &Symbol, + arg_layout: &InLayout<'a>, + ) { + match self.interner().get_repr(*arg_layout) { + single_register_integers!() => { + let buf = &mut self.buf; + + let dst = self.storage_manager.claim_general_reg(buf, dst); + let src1 = self.storage_manager.load_to_general_reg(buf, src1); + let src2 = self.storage_manager.load_to_general_reg(buf, src2); + + let int_width = arg_layout.try_int_width().unwrap(); + let register_width = match int_width.stack_size() { + 8 => RegisterWidth::W64, + 4 => RegisterWidth::W32, + 2 => RegisterWidth::W16, + 1 => RegisterWidth::W8, + _ => unreachable!(), + }; + + if int_width.is_signed() { + ASM::signed_compare_reg64(buf, register_width, op, dst, src1, src2) + } else { + ASM::unsigned_compare_reg64(buf, register_width, op, dst, src1, src2) + } + } + LayoutRepr::F32 | LayoutRepr::F64 => { + let float_width = match *arg_layout { + Layout::F32 => FloatWidth::F32, + Layout::F64 => FloatWidth::F64, + _ => unreachable!(), + }; + + let dst_reg = self.storage_manager.claim_general_reg(&mut self.buf, dst); + let src1_reg = self.storage_manager.load_to_float_reg(&mut self.buf, src1); + let src2_reg = self.storage_manager.load_to_float_reg(&mut self.buf, src2); + + ASM::cmp_freg_freg_reg64( + &mut self.buf, + dst_reg, + src1_reg, + src2_reg, + float_width, + op, + ); + } + LayoutRepr::Builtin(Builtin::Int(width @ (IntWidth::I128 | IntWidth::U128))) => { + self.compare_128bit(op, dst, src1, src2, width); + } + LayoutRepr::Builtin(Builtin::Decimal) => { + self.compare_128bit(op, dst, src1, src2, IntWidth::I128); + } + x => todo!("NumLt: layout, {:?}", x), + } + } + + fn allocate_with_refcount( + &mut self, + dst: Symbol, + data_bytes: Symbol, + element_alignment: Symbol, + ) { + self.build_fn_call( + &dst, + bitcode::UTILS_ALLOCATE_WITH_REFCOUNT.to_string(), + &[data_bytes, element_alignment], + &[Layout::U64, Layout::U32], + &Layout::U64, + ); + } + + fn allocate_with_refcount_if_null(&mut self, dst: Symbol, src: Symbol, layout: InLayout) { + let src_reg = self + .storage_manager + .load_to_general_reg(&mut self.buf, &src); + + // dummy mov that is supposed to move src into dst, and is filled in later because don't know yet which + // register the other branch will pick for dst. We must pass two different registers here + // otherwise the whole instruction is skipped! + let dst_reg = self.storage_manager.claim_general_reg(&mut self.buf, &dst); + let mov_start_index = self.buf.len(); + ASM::mov_reg64_reg64(&mut self.buf, dst_reg, src_reg); + + // jump to where the pointer is valid, because it is already valid if non-zero + let jmp_start_index = self.buf.len(); + let jmp_end_index = + ASM::jne_reg64_imm64_imm32(&mut self.buf, &mut self.storage_manager, src_reg, 0x0, 0); + + self.free_symbol(&dst); + + // so, the pointer is NULL, allocate + + let data_bytes = self.debug_symbol("data_bytes"); + self.load_layout_stack_size(layout, data_bytes); + + let element_alignment = self.debug_symbol("element_alignment"); + self.load_layout_alignment(layout, element_alignment); + + self.allocate_with_refcount(dst, data_bytes, element_alignment); + + self.free_symbol(&data_bytes); + self.free_symbol(&element_alignment); + + let mut tmp = bumpalo::vec![in self.env.arena]; + + // update the jump + let destination_index = self.buf.len(); + ASM::jne_reg64_imm64_imm32( + &mut tmp, + &mut self.storage_manager, + src_reg, + 0x0, + (destination_index - jmp_end_index) as i32, + ); + + self.buf[jmp_start_index..][..tmp.len()].copy_from_slice(tmp.as_slice()); + + // figure out what register was actually used + let dst_reg = self + .storage_manager + .load_to_general_reg(&mut self.buf, &dst); + + tmp.clear(); + ASM::mov_reg64_reg64(&mut tmp, dst_reg, src_reg); + + self.buf[mov_start_index..][..tmp.len()].copy_from_slice(tmp.as_slice()); + } + + fn unbox_str_or_list( + buf: &mut Vec<'a, u8>, + storage_manager: &mut StorageManager<'a, 'r, GeneralReg, FloatReg, ASM, CC>, + dst: Symbol, + ptr_reg: GeneralReg, + tmp_reg: GeneralReg, + offset: i32, + ) { + let base_offset = storage_manager.claim_stack_area_with_alignment(dst, 24, 8); + + ASM::mov_reg64_mem64_offset32(buf, tmp_reg, ptr_reg, offset); + ASM::mov_base32_reg64(buf, base_offset, tmp_reg); + + ASM::mov_reg64_mem64_offset32(buf, tmp_reg, ptr_reg, offset + 8); + ASM::mov_base32_reg64(buf, base_offset + 8, tmp_reg); + + ASM::mov_reg64_mem64_offset32(buf, tmp_reg, ptr_reg, offset + 16); + ASM::mov_base32_reg64(buf, base_offset + 16, tmp_reg); + } + + fn unbox_to_stack( + buf: &mut Vec<'a, u8>, + storage_manager: &mut StorageManager<'a, 'r, GeneralReg, FloatReg, ASM, CC>, + dst: Symbol, + stack_size: u32, + ptr_reg: GeneralReg, + tmp_reg: GeneralReg, + read_offset: i32, + ) { + let mut copied = 0; + let size = stack_size as i32; + + if size == 0 { + storage_manager.no_data(&dst); + return; + } + + // 16 is a guess here. In practice this is the highest alignment we encounter in roc datastructures + let base_offset = storage_manager.claim_stack_area_with_alignment(dst, stack_size, 16); + + if size - copied >= 8 { + for _ in (0..(size - copied)).step_by(8) { + ASM::mov_reg64_mem64_offset32(buf, tmp_reg, ptr_reg, read_offset + copied); + ASM::mov_base32_reg64(buf, base_offset + copied, tmp_reg); + + copied += 8; + } + } + + if size - copied >= 4 { + for _ in (0..(size - copied)).step_by(4) { + ASM::mov_reg32_mem32_offset32(buf, tmp_reg, ptr_reg, read_offset + copied); + ASM::mov_base32_reg32(buf, base_offset + copied, tmp_reg); + + copied += 4; + } + } + + if size - copied >= 2 { + for _ in (0..(size - copied)).step_by(2) { + ASM::mov_reg16_mem16_offset32(buf, tmp_reg, ptr_reg, read_offset + copied); + ASM::mov_base32_reg16(buf, base_offset + copied, tmp_reg); + + copied += 2; + } + } + + if size - copied >= 1 { + for _ in (0..(size - copied)).step_by(1) { + ASM::mov_reg8_mem8_offset32(buf, tmp_reg, ptr_reg, read_offset + copied); + ASM::mov_base32_reg8(buf, base_offset + copied, tmp_reg); + + copied += 1; + } + } + } + + fn ptr_read( + buf: &mut Vec<'a, u8>, + storage_manager: &mut StorageManager<'a, 'r, GeneralReg, FloatReg, ASM, CC>, + layout_interner: &STLayoutInterner<'a>, + ptr_reg: GeneralReg, + offset: i32, + element_in_layout: InLayout<'a>, + dst: Symbol, + ) { + match layout_interner.get_repr(element_in_layout) { + LayoutRepr::Builtin(builtin) => match builtin { + Builtin::Int(int_width) => match int_width { + IntWidth::I128 | IntWidth::U128 => { + // can we treat this as 2 u64's? + storage_manager.with_tmp_general_reg( + buf, + |storage_manager, buf, tmp_reg| { + let base_offset = storage_manager.claim_stack_area_layout( + layout_interner, + dst, + Layout::U128, + ); + + ASM::mov_reg64_mem64_offset32(buf, tmp_reg, ptr_reg, offset); + ASM::mov_base32_reg64(buf, base_offset, tmp_reg); + + ASM::mov_reg64_mem64_offset32(buf, tmp_reg, ptr_reg, offset + 8); + ASM::mov_base32_reg64(buf, base_offset + 8, tmp_reg); + }, + ); + } + IntWidth::I64 | IntWidth::U64 => { + let dst_reg = storage_manager.claim_general_reg(buf, &dst); + ASM::mov_reg64_mem64_offset32(buf, dst_reg, ptr_reg, offset); + } + IntWidth::I32 | IntWidth::U32 => { + let dst_reg = storage_manager.claim_general_reg(buf, &dst); + ASM::mov_reg32_mem32_offset32(buf, dst_reg, ptr_reg, offset); + } + IntWidth::I16 | IntWidth::U16 => { + let dst_reg = storage_manager.claim_general_reg(buf, &dst); + ASM::xor_reg64_reg64_reg64(buf, dst_reg, dst_reg, dst_reg); + ASM::mov_reg16_mem16_offset32(buf, dst_reg, ptr_reg, offset); + } + IntWidth::I8 | IntWidth::U8 => { + let dst_reg = storage_manager.claim_general_reg(buf, &dst); + ASM::xor_reg64_reg64_reg64(buf, dst_reg, dst_reg, dst_reg); + ASM::mov_reg8_mem8_offset32(buf, dst_reg, ptr_reg, offset); + } + }, + Builtin::Float(FloatWidth::F64) => { + let dst_reg = storage_manager.claim_float_reg(buf, &dst); + ASM::mov_freg64_mem64_offset32(buf, dst_reg, ptr_reg, offset); + } + Builtin::Float(FloatWidth::F32) => { + let dst_reg = storage_manager.claim_float_reg(buf, &dst); + ASM::mov_freg32_mem32_offset32(buf, dst_reg, ptr_reg, offset); + } + Builtin::Bool => { + // the same as an 8-bit integer + let dst_reg = storage_manager.claim_general_reg(buf, &dst); + + ASM::xor_reg64_reg64_reg64(buf, dst_reg, dst_reg, dst_reg); + ASM::mov_reg8_mem8_offset32(buf, dst_reg, ptr_reg, offset); + } + Builtin::Decimal => { + // same as 128-bit integer + storage_manager.with_tmp_general_reg(buf, |storage_manager, buf, tmp_reg| { + let base_offset = storage_manager.claim_stack_area_layout( + layout_interner, + dst, + Layout::DEC, + ); + + ASM::mov_reg64_mem64_offset32(buf, tmp_reg, ptr_reg, offset); + ASM::mov_base32_reg64(buf, base_offset, tmp_reg); + + ASM::mov_reg64_mem64_offset32(buf, tmp_reg, ptr_reg, offset + 8); + ASM::mov_base32_reg64(buf, base_offset + 8, tmp_reg); + }); + } + Builtin::Str | Builtin::List(_) => { + storage_manager.with_tmp_general_reg(buf, |storage_manager, buf, tmp_reg| { + Self::unbox_str_or_list( + buf, + storage_manager, + dst, + ptr_reg, + tmp_reg, + offset, + ); + }); + } + }, + + pointer_layouts!() => { + // the same as 64-bit integer (for 64-bit targets) + let dst_reg = storage_manager.claim_general_reg(buf, &dst); + ASM::mov_reg64_mem64_offset32(buf, dst_reg, ptr_reg, offset); + } + + LayoutRepr::Struct { .. } => { + // put it on the stack + let stack_size = layout_interner.stack_size(element_in_layout); + + storage_manager.with_tmp_general_reg(buf, |storage_manager, buf, tmp_reg| { + Self::unbox_to_stack( + buf, + storage_manager, + dst, + stack_size, + ptr_reg, + tmp_reg, + offset, + ); + }); + } + + LayoutRepr::Union(UnionLayout::NonRecursive(_)) => { + // put it on the stack + let stack_size = layout_interner.stack_size(element_in_layout); + + storage_manager.with_tmp_general_reg(buf, |storage_manager, buf, tmp_reg| { + Self::unbox_to_stack( + buf, + storage_manager, + dst, + stack_size, + ptr_reg, + tmp_reg, + offset, + ); + }); + } + + LayoutRepr::LambdaSet(lambda_set) => { + Self::ptr_read( + buf, + storage_manager, + layout_interner, + ptr_reg, + offset, + lambda_set.runtime_representation(), + dst, + ); + } + + LayoutRepr::Erased(_) => todo_lambda_erasure!(), + } + } + + #[allow(clippy::too_many_arguments)] + fn ptr_write( + buf: &mut Vec<'a, u8>, + storage_manager: &mut StorageManager<'a, 'r, GeneralReg, FloatReg, ASM, CC>, + layout_interner: &STLayoutInterner<'a>, + ptr_reg: GeneralReg, + element_offset: i32, + element_width: u64, + element_layout: LayoutRepr<'a>, + value: Symbol, + ) { + match element_layout { + LayoutRepr::Builtin(Builtin::Int(IntWidth::I64 | IntWidth::U64)) => { + let sym_reg = storage_manager.load_to_general_reg(buf, &value); + ASM::mov_mem64_offset32_reg64(buf, ptr_reg, element_offset, sym_reg); + } + LayoutRepr::Builtin(Builtin::Int(IntWidth::I32 | IntWidth::U32)) => { + let sym_reg = storage_manager.load_to_general_reg(buf, &value); + ASM::mov_mem32_offset32_reg32(buf, ptr_reg, element_offset, sym_reg); + } + LayoutRepr::Builtin(Builtin::Int(IntWidth::I16 | IntWidth::U16)) => { + let sym_reg = storage_manager.load_to_general_reg(buf, &value); + ASM::mov_mem16_offset32_reg16(buf, ptr_reg, element_offset, sym_reg); + } + LayoutRepr::Builtin(Builtin::Int(IntWidth::I8 | IntWidth::U8) | Builtin::Bool) => { + let sym_reg = storage_manager.load_to_general_reg(buf, &value); + ASM::mov_mem8_offset32_reg8(buf, ptr_reg, element_offset, sym_reg); + } + LayoutRepr::Builtin(Builtin::Float(FloatWidth::F64 | FloatWidth::F32)) => { + let sym_reg = storage_manager.load_to_float_reg(buf, &value); + ASM::movesd_mem64_offset32_freg64(buf, ptr_reg, element_offset, sym_reg); + } + pointer_layouts!() => { + let sym_reg = storage_manager.load_to_general_reg(buf, &value); + ASM::mov_mem64_offset32_reg64(buf, ptr_reg, element_offset, sym_reg); + } + LayoutRepr::LambdaSet(lambda_set) => { + let repr = layout_interner.get_repr(lambda_set.runtime_representation()); + + Self::ptr_write( + buf, + storage_manager, + layout_interner, + ptr_reg, + element_offset, + element_width, + repr, + value, + ); + } + + _other => { + if element_width == 0 { + return; + } + + let (from_offset, stack_size) = storage_manager.stack_offset_and_size(&value); + + // on x86_64 this is too strict. + // debug_assert_eq!(from_offset % 8, 0); + + storage_manager.with_tmp_general_reg(buf, |_storage_manager, buf, tmp_reg| { + let mut copied = 0; + let size = stack_size as i32; + + if size - copied >= 8 { + for _ in (0..(size - copied)).step_by(8) { + ASM::mov_reg64_base32(buf, tmp_reg, from_offset + copied); + ASM::mov_mem64_offset32_reg64( + buf, + ptr_reg, + element_offset + copied, + tmp_reg, + ); + + copied += 8; + } + } + + if size - copied >= 4 { + for _ in (0..(size - copied)).step_by(4) { + ASM::mov_reg32_base32(buf, tmp_reg, from_offset + copied); + ASM::mov_mem32_offset32_reg32( + buf, + ptr_reg, + element_offset + copied, + tmp_reg, + ); + + copied += 4; + } + } + + if size - copied >= 2 { + for _ in (0..(size - copied)).step_by(2) { + ASM::mov_reg16_base32(buf, tmp_reg, from_offset + copied); + ASM::mov_mem16_offset32_reg16( + buf, + ptr_reg, + element_offset + copied, + tmp_reg, + ); + + copied += 2; + } + } + + if size - copied >= 1 { + for _ in (0..(size - copied)).step_by(1) { + ASM::mov_reg8_base32(buf, tmp_reg, from_offset + copied); + ASM::mov_mem8_offset32_reg8( + buf, + ptr_reg, + element_offset + copied, + tmp_reg, + ); + + copied += 1; + } + } + }); + } + } + } + + /// Loads the alignment bytes of `layout` into the given `symbol` + fn load_layout_alignment(&mut self, layout: InLayout<'_>, symbol: Symbol) { + let u32_layout = Layout::U32; + let alignment = self.layout_interner.alignment_bytes(layout); + let alignment_literal = Literal::Int((alignment as i128).to_ne_bytes()); + + self.load_literal(&symbol, &u32_layout, &alignment_literal); + } + + /// Loads the stack size of `layout` into the given `symbol` + fn load_layout_stack_size(&mut self, layout: InLayout<'_>, symbol: Symbol) { + let u64_layout = Layout::U64; + let width = self.layout_interner.stack_size(layout); + let width_literal = Literal::Int((width as i128).to_ne_bytes()); + + self.load_literal(&symbol, &u64_layout, &width_literal); + } +} + +#[macro_export] +macro_rules! sign_extended_int_builtins { + () => { + Layout::I8 | Layout::I16 | Layout::I32 | Layout::I64 | Layout::I128 + }; +} + +#[macro_export] +macro_rules! zero_extended_int_builtins { + () => { + Layout::U8 | Layout::U16 | Layout::U32 | Layout::U64 | Layout::U128 + }; +} + +#[macro_export] +macro_rules! single_register_int_builtins { + () => { + LayoutRepr::I8 + | LayoutRepr::I16 + | LayoutRepr::I32 + | LayoutRepr::I64 + | LayoutRepr::U8 + | LayoutRepr::U16 + | LayoutRepr::U32 + | LayoutRepr::U64 + }; +} + +#[macro_export] +macro_rules! single_register_integers { + () => { + LayoutRepr::BOOL | single_register_int_builtins!() | LayoutRepr::OPAQUE_PTR + }; +} + +#[macro_export] +macro_rules! single_register_floats { + () => { + LayoutRepr::F32 | LayoutRepr::F64 + }; +} + +#[macro_export] +macro_rules! single_register_layouts { + () => { + single_register_integers!() | single_register_floats!() + }; +} + +#[macro_export] +macro_rules! pointer_layouts { + () => { + LayoutRepr::Ptr(_) + | LayoutRepr::RecursivePointer(_) + | LayoutRepr::Union( + UnionLayout::Recursive(_) + | UnionLayout::NonNullableUnwrapped(_) + | UnionLayout::NullableWrapped { .. } + | UnionLayout::NullableUnwrapped { .. }, + ) + | LayoutRepr::FunctionPointer(_) + }; +} diff --git a/crates/compiler/gen_dev/src/generic64/storage.rs b/crates/compiler/gen_dev/src/generic64/storage.rs new file mode 100644 index 0000000000..1c6b48e054 --- /dev/null +++ b/crates/compiler/gen_dev/src/generic64/storage.rs @@ -0,0 +1,1683 @@ +use crate::{ + generic64::{Assembler, CallConv, RegTrait}, + pointer_layouts, sign_extended_int_builtins, single_register_floats, + single_register_int_builtins, single_register_integers, single_register_layouts, Env, +}; +use bumpalo::collections::{CollectIn, Vec}; +use roc_builtins::bitcode::{FloatWidth, IntWidth}; +use roc_collections::all::{MutMap, MutSet}; +use roc_error_macros::{internal_error, todo_lambda_erasure}; +use roc_module::symbol::Symbol; +use roc_mono::{ + ir::{JoinPointId, Param}, + layout::{ + Builtin, InLayout, Layout, LayoutInterner, LayoutRepr, STLayoutInterner, UnionLayout, + }, +}; +use roc_target::TargetInfo; +use std::cmp::max; +use std::marker::PhantomData; +use std::rc::Rc; + +use RegStorage::*; +use StackStorage::*; +use Storage::*; + +use super::RegisterWidth; + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum RegStorage { + General(GeneralReg), + Float(FloatReg), +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +enum StackStorage { + /// Primitives are 8 bytes or less. That generally live in registers but can move stored on the stack. + /// Their data, when on the stack, must always be 8 byte aligned and will be moved as a block. + /// They are never part of a struct, union, or more complex value. + /// The rest of the bytes should be the sign extension due to how these are loaded. + Primitive { + // Offset from the base pointer in bytes. + base_offset: i32, + // Optional register also holding the value. + reg: Option>, + }, + /// Referenced Primitives are primitives within a complex structures. + /// They have no guarantees about the bits around them and cannot simply be loaded as an 8 byte value. + /// For example, a U8 in a struct must be loaded as a single byte and sign extended. + /// If it was loaded as an 8 byte value, a bunch of garbage data would be loaded with the U8. + /// After loading, they should just be stored in a register, removing the reference. + ReferencedPrimitive { + // Offset from the base pointer in bytes. + base_offset: i32, + // Size on the stack in bytes. + size: u32, + // Whether or not the data is need to be sign extended on load. + // If not, it must be zero extended. + sign_extend: bool, + }, + /// Complex data (lists, unions, structs, str) stored on the stack. + /// Note, this is also used for referencing a value within a struct/union. + /// It has no alignment guarantees. + /// When a primitive value is being loaded from this, it should be moved into a register. + /// To start, the primitive can just be loaded as a ReferencePrimitive. + Complex { + // Offset from the base pointer in bytes. + base_offset: i32, + // Size on the stack in bytes. + size: u32, + // TODO: investigate if storing a reg here for special values is worth it. + // For example, the ptr in list.get/list.set + // Instead, it would probably be better to change the incoming IR to load the pointer once and then use it multiple times. + }, +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +enum Storage { + Reg(RegStorage), + Stack(StackStorage), + NoData, +} + +#[derive(Clone)] +pub struct StorageManager< + 'a, + 'r, + GeneralReg: RegTrait, + FloatReg: RegTrait, + ASM: Assembler, + CC: CallConv, +> { + phantom_cc: PhantomData, + phantom_asm: PhantomData, + pub(crate) env: &'r Env<'a>, + pub(crate) target_info: TargetInfo, + // Data about where each symbol is stored. + symbol_storage_map: MutMap>, + + // A map from symbol to its owning allocation. + // This is only used for complex data on the stack and its references. + // In the case that subdata is still referenced from an overall structure, + // We can't free the entire structure until the subdata is no longer needed. + // If a symbol has only one reference, we can free it. + allocation_map: MutMap>, + + // The storage for parameters of a join point. + // When jumping to the join point, the parameters should be setup to match this. + join_param_map: MutMap>>, + + // This should probably be smarter than a vec. + // There are certain registers we should always use first. With pushing and popping, this could get mixed. + general_free_regs: Vec<'a, GeneralReg>, + float_free_regs: Vec<'a, FloatReg>, + + // The last major thing we need is a way to decide what reg to free when all of them are full. + // Theoretically we want a basic lru cache for the currently loaded symbols. + // For now just a vec of used registers and the symbols they contain. + general_used_regs: Vec<'a, (GeneralReg, Symbol)>, + float_used_regs: Vec<'a, (FloatReg, Symbol)>, + + pub(crate) used_callee_saved_regs: UsedCalleeRegisters, + + free_stack_chunks: Vec<'a, (i32, u32)>, + stack_size: u32, + + /// Amount of extra stack space needed to pass arguments for a function call + /// This is usually zero, and only used when the argument passing registers are all used + fn_call_stack_size: u32, +} + +pub fn new_storage_manager< + 'a, + 'r, + GeneralReg: RegTrait, + FloatReg: RegTrait, + ASM: Assembler, + CC: CallConv, +>( + env: &'r Env<'a>, + target_info: TargetInfo, +) -> StorageManager<'a, 'r, GeneralReg, FloatReg, ASM, CC> { + StorageManager { + phantom_asm: PhantomData, + phantom_cc: PhantomData, + env, + target_info, + symbol_storage_map: MutMap::default(), + allocation_map: MutMap::default(), + join_param_map: MutMap::default(), + general_free_regs: bumpalo::vec![in env.arena], + general_used_regs: bumpalo::vec![in env.arena], + // must be saved on entering a function, and restored before returning + used_callee_saved_regs: UsedCalleeRegisters::default(), + float_free_regs: bumpalo::vec![in env.arena], + float_used_regs: bumpalo::vec![in env.arena], + free_stack_chunks: bumpalo::vec![in env.arena], + stack_size: 0, + fn_call_stack_size: 0, + } +} + +// optimization idea: use a bitset +#[derive(Debug, Clone)] +pub(crate) struct UsedCalleeRegisters { + general: MutSet, + float: MutSet, +} + +impl UsedCalleeRegisters { + fn clear(&mut self) { + self.general.clear(); + self.float.clear(); + } + + fn insert_general(&mut self, reg: GeneralReg) -> bool { + self.general.insert(reg) + } + + fn insert_float(&mut self, reg: FloatReg) -> bool { + self.float.insert(reg) + } + + pub(crate) fn extend(&mut self, other: &Self) { + self.general.extend(other.general.iter().copied()); + self.float.extend(other.float.iter().copied()); + } + + pub(crate) fn as_vecs<'a>( + &self, + arena: &'a bumpalo::Bump, + ) -> (Vec<'a, GeneralReg>, Vec<'a, FloatReg>) { + ( + self.general.iter().copied().collect_in(arena), + self.float.iter().copied().collect_in(arena), + ) + } +} + +impl Default for UsedCalleeRegisters { + fn default() -> Self { + Self { + general: Default::default(), + float: Default::default(), + } + } +} + +impl< + 'a, + 'r, + FloatReg: RegTrait, + GeneralReg: RegTrait, + ASM: Assembler, + CC: CallConv, + > StorageManager<'a, 'r, GeneralReg, FloatReg, ASM, CC> +{ + pub fn reset(&mut self) { + self.symbol_storage_map.clear(); + self.allocation_map.clear(); + self.join_param_map.clear(); + self.used_callee_saved_regs.clear(); + self.general_free_regs.clear(); + self.general_used_regs.clear(); + self.general_free_regs + .extend_from_slice(CC::GENERAL_DEFAULT_FREE_REGS); + self.float_free_regs.clear(); + self.float_used_regs.clear(); + self.float_free_regs + .extend_from_slice(CC::FLOAT_DEFAULT_FREE_REGS); + self.used_callee_saved_regs.clear(); + self.free_stack_chunks.clear(); + self.stack_size = 0; + self.fn_call_stack_size = 0; + } + + pub fn stack_size(&self) -> u32 { + self.stack_size + } + + pub fn fn_call_stack_size(&self) -> u32 { + self.fn_call_stack_size + } + + /// Returns true if the symbol is storing a primitive value. + pub fn is_stored_primitive(&self, sym: &Symbol) -> bool { + matches!( + self.get_storage_for_sym(sym), + Reg(_) | Stack(Primitive { .. } | ReferencedPrimitive { .. }) + ) + } + + /// Get a general register from the free list. + /// Will free data to the stack if necessary to get the register. + fn get_general_reg(&mut self, buf: &mut Vec<'a, u8>) -> GeneralReg { + if let Some(reg) = self.general_free_regs.pop() { + if CC::general_callee_saved(®) { + self.used_callee_saved_regs.insert_general(reg); + } + reg + } else if !self.general_used_regs.is_empty() { + let (reg, sym) = self.general_used_regs.remove(0); + self.free_to_stack(buf, &sym, General(reg)); + reg + } else { + internal_error!("completely out of general purpose registers"); + } + } + + /// Get a float register from the free list. + /// Will free data to the stack if necessary to get the register. + fn get_float_reg(&mut self, buf: &mut Vec<'a, u8>) -> FloatReg { + if let Some(reg) = self.float_free_regs.pop() { + if CC::float_callee_saved(®) { + self.used_callee_saved_regs.insert_float(reg); + } + reg + } else if !self.float_used_regs.is_empty() { + let (reg, sym) = self.float_used_regs.remove(0); + self.free_to_stack(buf, &sym, Float(reg)); + reg + } else { + internal_error!("completely out of float registers"); + } + } + + /// Claims a general reg for a specific symbol. + /// They symbol should not already have storage. + pub fn claim_general_reg(&mut self, buf: &mut Vec<'a, u8>, sym: &Symbol) -> GeneralReg { + debug_assert_eq!( + self.symbol_storage_map.get(sym), + None, + "Symbol {sym:?} is already in the storage map!" + ); + let reg = self.get_general_reg(buf); + self.general_used_regs.push((reg, *sym)); + self.symbol_storage_map.insert(*sym, Reg(General(reg))); + reg + } + + /// Claims a float reg for a specific symbol. + /// They symbol should not already have storage. + pub fn claim_float_reg(&mut self, buf: &mut Vec<'a, u8>, sym: &Symbol) -> FloatReg { + debug_assert_eq!(self.symbol_storage_map.get(sym), None); + let reg = self.get_float_reg(buf); + self.float_used_regs.push((reg, *sym)); + self.symbol_storage_map.insert(*sym, Reg(Float(reg))); + reg + } + + /// This claims a temporary general register and enables is used in the passed in function. + /// Temporary registers are not safe across call instructions. + pub fn with_tmp_general_reg, GeneralReg)>( + &mut self, + buf: &mut Vec<'a, u8>, + callback: F, + ) { + let reg = self.get_general_reg(buf); + callback(self, buf, reg); + self.general_free_regs.push(reg); + } + + #[allow(dead_code)] + /// This claims a temporary float register and enables is used in the passed in function. + /// Temporary registers are not safe across call instructions. + pub fn with_tmp_float_reg, FloatReg)>( + &mut self, + buf: &mut Vec<'a, u8>, + callback: F, + ) { + let reg = self.get_float_reg(buf); + callback(self, buf, reg); + self.float_free_regs.push(reg); + } + + /// Loads a symbol into a general reg and returns that register. + /// The symbol must already be stored somewhere. + /// Will fail on values stored in float regs. + /// Will fail for values that don't fit in a single register. + pub fn load_to_general_reg(&mut self, buf: &mut Vec<'a, u8>, sym: &Symbol) -> GeneralReg { + let storage = self.remove_storage_for_sym(sym); + match storage { + Reg(General(reg)) + | Stack(Primitive { + reg: Some(General(reg)), + .. + }) => { + self.symbol_storage_map.insert(*sym, storage); + reg + } + Reg(Float(_)) + | Stack(Primitive { + reg: Some(Float(_)), + .. + }) => { + internal_error!("Cannot load floating point symbol into GeneralReg: {sym:?}") + } + Stack(Primitive { + reg: None, + base_offset, + }) => { + debug_assert_eq!(base_offset % 8, 0); + let reg = self.get_general_reg(buf); + ASM::mov_reg64_base32(buf, reg, base_offset); + self.general_used_regs.push((reg, *sym)); + self.symbol_storage_map.insert( + *sym, + Stack(Primitive { + base_offset, + reg: Some(General(reg)), + }), + ); + reg + } + Stack(ReferencedPrimitive { + base_offset, + size, + sign_extend, + }) => { + let reg = self.get_general_reg(buf); + + let register_width = match size { + 8 => RegisterWidth::W64, + 4 => RegisterWidth::W32, + 2 => RegisterWidth::W16, + 1 => RegisterWidth::W8, + _ => internal_error!("Invalid size: {size}"), + }; + + if sign_extend { + ASM::movsx_reg_base32(buf, register_width, reg, base_offset); + } else { + ASM::movzx_reg_base32(buf, register_width, reg, base_offset); + } + self.general_used_regs.push((reg, *sym)); + self.symbol_storage_map.insert(*sym, Reg(General(reg))); + self.free_reference(sym); + reg + } + Stack(Complex { size, .. }) => { + internal_error!( + "Cannot load large values (size {size}) into general registers: {sym:?}", + ) + } + NoData => { + internal_error!("Cannot load no data into general registers: {}", sym) + } + } + } + + /// Loads a symbol into a float reg and returns that register. + /// The symbol must already be stored somewhere. + /// Will fail on values stored in general regs. + /// Will fail for values that don't fit in a single register. + pub fn load_to_float_reg(&mut self, buf: &mut Vec<'a, u8>, sym: &Symbol) -> FloatReg { + let storage = self.remove_storage_for_sym(sym); + match storage { + Reg(Float(reg)) + | Stack(Primitive { + reg: Some(Float(reg)), + .. + }) => { + self.symbol_storage_map.insert(*sym, storage); + reg + } + Reg(General(_)) + | Stack(Primitive { + reg: Some(General(_)), + .. + }) => { + internal_error!("Cannot load general symbol into FloatReg: {}", sym) + } + Stack(Primitive { + reg: None, + base_offset, + }) => { + debug_assert_eq!(base_offset % 8, 0); + let reg = self.get_float_reg(buf); + ASM::mov_freg64_base32(buf, reg, base_offset); + self.float_used_regs.push((reg, *sym)); + self.symbol_storage_map.insert( + *sym, + Stack(Primitive { + base_offset, + reg: Some(Float(reg)), + }), + ); + reg + } + Stack(ReferencedPrimitive { + base_offset, size, .. + }) if base_offset % 8 == 0 && size == 8 => { + // The primitive is aligned and the data is exactly 8 bytes, treat it like regular stack. + let reg = self.get_float_reg(buf); + ASM::mov_freg64_base32(buf, reg, base_offset); + self.float_used_regs.push((reg, *sym)); + self.symbol_storage_map.insert(*sym, Reg(Float(reg))); + self.free_reference(sym); + reg + } + Stack(ReferencedPrimitive { .. }) => { + todo!("loading referenced primitives") + } + Stack(Complex { .. }) => { + internal_error!("Cannot load large values into float registers: {}", sym) + } + NoData => { + internal_error!("Cannot load no data into general registers: {}", sym) + } + } + } + + /// Loads the symbol to the specified register. + /// It will fail if the symbol is stored in a float register. + /// This is only made to be used in special cases where exact regs are needed (function args and returns). + /// It will not try to free the register first. + /// This will not track the symbol change (it makes no assumptions about the new reg). + pub fn load_to_specified_general_reg( + &self, + buf: &mut Vec<'a, u8>, + sym: &Symbol, + reg: GeneralReg, + ) { + match self.get_storage_for_sym(sym) { + Reg(General(old_reg)) + | Stack(Primitive { + reg: Some(General(old_reg)), + .. + }) => { + if *old_reg == reg { + return; + } + ASM::mov_reg64_reg64(buf, reg, *old_reg); + } + Reg(Float(_)) + | Stack(Primitive { + reg: Some(Float(_)), + .. + }) => { + internal_error!("Cannot load floating point symbol into GeneralReg: {sym:?}",) + } + Stack(Primitive { + reg: None, + base_offset, + }) => { + debug_assert_eq!(base_offset % 8, 0); + ASM::mov_reg64_base32(buf, reg, *base_offset); + } + Stack(ReferencedPrimitive { + base_offset, + size, + sign_extend, + }) => { + debug_assert!(*size <= 8); + + let register_width = match size { + 8 => RegisterWidth::W64, + 4 => RegisterWidth::W32, + 2 => RegisterWidth::W16, + 1 => RegisterWidth::W8, + _ => internal_error!("Invalid size: {size}"), + }; + + if *sign_extend { + ASM::movsx_reg_base32(buf, register_width, reg, *base_offset) + } else { + ASM::movzx_reg_base32(buf, register_width, reg, *base_offset) + } + } + Stack(Complex { size, .. }) => { + internal_error!( + "Cannot load large values (size {size}) into general registers: {sym:?}", + ) + } + NoData => { + internal_error!("Cannot load no data into general registers: {:?}", sym) + } + } + } + + /// Loads the symbol to the specified register. + /// It will fail if the symbol is stored in a general register. + /// This is only made to be used in special cases where exact regs are needed (function args and returns). + /// It will not try to free the register first. + /// This will not track the symbol change (it makes no assumptions about the new reg). + pub fn load_to_specified_float_reg(&self, buf: &mut Vec<'a, u8>, sym: &Symbol, reg: FloatReg) { + match self.get_storage_for_sym(sym) { + Reg(Float(old_reg)) + | Stack(Primitive { + reg: Some(Float(old_reg)), + .. + }) => { + if *old_reg == reg { + return; + } + ASM::mov_freg64_freg64(buf, reg, *old_reg); + } + Reg(General(_)) + | Stack(Primitive { + reg: Some(General(_)), + .. + }) => { + internal_error!("Cannot load general symbol into FloatReg: {}", sym) + } + Stack(Primitive { + reg: None, + base_offset, + }) => { + debug_assert_eq!(base_offset % 8, 0); + ASM::mov_freg64_base32(buf, reg, *base_offset); + } + Stack(ReferencedPrimitive { + base_offset, size, .. + }) if base_offset % 8 == 0 && *size == 8 => { + // The primitive is aligned and the data is exactly 8 bytes, treat it like regular stack. + ASM::mov_freg64_base32(buf, reg, *base_offset); + } + Stack(ReferencedPrimitive { .. }) => { + todo!("loading referenced primitives") + } + Stack(Complex { .. }) => { + internal_error!("Cannot load large values into float registers: {}", sym) + } + NoData => { + internal_error!("Cannot load no data into general registers: {}", sym) + } + } + } + + /// Loads a field from a struct or tag union. + /// This is lazy by default. It will not copy anything around. + pub fn load_field_at_index( + &mut self, + layout_interner: &mut STLayoutInterner<'a>, + sym: &Symbol, + structure: &Symbol, + index: u64, + field_layouts: &'a [InLayout<'a>], + ) { + debug_assert!(index < field_layouts.len() as u64); + + let storage = *self.get_storage_for_sym(structure); + + if let NoData = storage { + return self.no_data(sym); + } + + // This must be removed and reinserted for ownership and mutability reasons. + let owned_data = self.remove_allocation_for_sym(structure); + self.allocation_map + .insert(*structure, Rc::clone(&owned_data)); + + match storage { + Stack(Complex { base_offset, size }) => { + let mut data_offset = base_offset; + for layout in field_layouts.iter().take(index as usize) { + let field_size = layout_interner.stack_size(*layout); + data_offset += field_size as i32; + } + + // check that the record completely contains the field + debug_assert!(data_offset <= base_offset + size as i32,); + + let layout = field_layouts[index as usize]; + let size = layout_interner.stack_size(layout); + self.allocation_map.insert(*sym, owned_data); + self.symbol_storage_map.insert( + *sym, + Stack(if is_primitive(layout_interner, layout) { + ReferencedPrimitive { + base_offset: data_offset, + size, + sign_extend: matches!(layout, sign_extended_int_builtins!()), + } + } else { + Complex { + base_offset: data_offset, + size, + } + }), + ); + } + storage => { + internal_error!( + "Cannot load field from data with storage type: {:?}", + storage + ); + } + } + } + + pub fn load_union_tag_id_nonrecursive( + &mut self, + layout_interner: &mut STLayoutInterner<'a>, + _buf: &mut Vec<'a, u8>, + sym: &Symbol, + structure: &Symbol, + tags: &[&[InLayout]], + ) { + let union_layout = UnionLayout::NonRecursive(tags); + + // This must be removed and reinserted for ownership and mutability reasons. + let owned_data = self.remove_allocation_for_sym(structure); + self.allocation_map + .insert(*structure, Rc::clone(&owned_data)); + + let (union_offset, _) = self.stack_offset_and_size(structure); + + let (data_size, data_alignment) = union_layout.data_size_and_alignment(layout_interner); + let id_offset = data_size - data_alignment; + let discriminant = union_layout.discriminant(); + + let size = discriminant.stack_size(); + self.allocation_map.insert(*sym, owned_data); + self.symbol_storage_map.insert( + *sym, + Stack(ReferencedPrimitive { + base_offset: union_offset + id_offset as i32, + size, + sign_extend: false, // tag ids are always unsigned + }), + ); + } + + // Loads the dst to be the later 64 bits of a list (its length). + pub fn list_len(&mut self, _buf: &mut Vec<'a, u8>, dst: &Symbol, list: &Symbol) { + let owned_data = self.remove_allocation_for_sym(list); + self.allocation_map.insert(*list, Rc::clone(&owned_data)); + self.allocation_map.insert(*dst, owned_data); + let (list_offset, _) = self.stack_offset_and_size(list); + self.symbol_storage_map.insert( + *dst, + Stack(ReferencedPrimitive { + base_offset: list_offset + 8, + size: 8, + sign_extend: false, + }), + ); + } + + /// Creates a struct on the stack, moving the data in fields into the struct. + pub fn create_struct( + &mut self, + layout_interner: &mut STLayoutInterner<'a>, + buf: &mut Vec<'a, u8>, + sym: &Symbol, + layout: &InLayout<'a>, + fields: &'a [Symbol], + ) { + let struct_size = layout_interner.stack_size(*layout); + if struct_size == 0 { + self.symbol_storage_map.insert(*sym, NoData); + return; + } + let base_offset = self.claim_stack_area_layout(layout_interner, *sym, *layout); + + let mut in_layout = *layout; + let layout = loop { + match layout_interner.get_repr(in_layout) { + LayoutRepr::LambdaSet(inner) => in_layout = inner.runtime_representation(), + other => break other, + } + }; + + if let LayoutRepr::Struct(field_layouts) = layout { + let mut current_offset = base_offset; + for (field, field_layout) in fields.iter().zip(field_layouts.iter()) { + let field_size = layout_interner.stack_size(*field_layout); + self.copy_symbol_to_stack_offset( + layout_interner, + buf, + current_offset, + field, + field_layout, + ); + current_offset += field_size as i32; + } + } else { + // This is a single element struct. Just copy the single field to the stack. + debug_assert_eq!(fields.len(), 1); + self.copy_symbol_to_stack_offset( + layout_interner, + buf, + base_offset, + &fields[0], + &in_layout, + ); + } + } + + /// Copies a complex symbol on the stack to the arg pointer. + pub fn copy_symbol_to_arg_pointer( + &mut self, + buf: &mut Vec<'a, u8>, + sym: &Symbol, + _layout: &InLayout<'a>, + ) { + let ret_reg = self.load_to_general_reg(buf, &Symbol::RET_POINTER); + let (base_offset, size) = self.stack_offset_and_size(sym); + debug_assert!(base_offset % 8 == 0); + debug_assert!(size % 8 == 0); + self.with_tmp_general_reg(buf, |_storage_manager, buf, tmp_reg| { + for i in (0..size as i32).step_by(8) { + ASM::mov_reg64_base32(buf, tmp_reg, base_offset + i); + ASM::mov_mem64_offset32_reg64(buf, ret_reg, i, tmp_reg); + } + }); + } + + /// Copies a symbol to the specified stack offset. This is used for things like filling structs. + /// The offset is not guarenteed to be perfectly aligned, it follows Roc's alignment plan. + /// This means that, for example 2 I32s might be back to back on the stack. + /// Always interact with the stack using aligned 64bit movement. + pub fn copy_symbol_to_stack_offset( + &mut self, + layout_interner: &mut STLayoutInterner<'a>, + buf: &mut Vec<'a, u8>, + to_offset: i32, + sym: &Symbol, + layout: &InLayout<'a>, + ) { + match layout_interner.get_repr(*layout) { + LayoutRepr::Builtin(builtin) => match builtin { + Builtin::Int(int_width) => match int_width { + IntWidth::I128 | IntWidth::U128 => { + let (from_offset, size) = self.stack_offset_and_size(sym); + debug_assert_eq!(from_offset % 8, 0); + debug_assert_eq!(size % 8, 0); + debug_assert_eq!(size, layout_interner.stack_size(*layout)); + self.copy_to_stack_offset(buf, size, from_offset, to_offset) + } + IntWidth::I64 | IntWidth::U64 => { + debug_assert_eq!(to_offset % 8, 0); + let reg = self.load_to_general_reg(buf, sym); + ASM::mov_base32_reg64(buf, to_offset, reg); + } + IntWidth::I32 | IntWidth::U32 => { + debug_assert_eq!(to_offset % 4, 0); + let reg = self.load_to_general_reg(buf, sym); + ASM::mov_base32_reg32(buf, to_offset, reg); + } + IntWidth::I16 | IntWidth::U16 => { + debug_assert_eq!(to_offset % 2, 0); + let reg = self.load_to_general_reg(buf, sym); + ASM::mov_base32_reg16(buf, to_offset, reg); + } + IntWidth::I8 | IntWidth::U8 => { + let reg = self.load_to_general_reg(buf, sym); + ASM::mov_base32_reg8(buf, to_offset, reg); + } + }, + + Builtin::Float(float_width) => match float_width { + FloatWidth::F64 => { + debug_assert_eq!(to_offset % 8, 0); + let reg = self.load_to_float_reg(buf, sym); + ASM::mov_base32_freg64(buf, to_offset, reg); + } + FloatWidth::F32 => { + debug_assert_eq!(to_offset % 4, 0); + let reg = self.load_to_float_reg(buf, sym); + ASM::mov_base32_freg64(buf, to_offset, reg); + } + }, + Builtin::Bool => { + // same as 8-bit integer, but we special-case true/false because these symbols + // are thunks and literal values + match *sym { + Symbol::BOOL_FALSE => { + let reg = self.claim_general_reg(buf, sym); + ASM::mov_reg64_imm64(buf, reg, false as i64) + } + Symbol::BOOL_TRUE => { + let reg = self.claim_general_reg(buf, sym); + ASM::mov_reg64_imm64(buf, reg, true as i64) + } + _ => { + let reg = self.load_to_general_reg(buf, sym); + ASM::mov_base32_reg8(buf, to_offset, reg); + } + } + } + Builtin::Decimal => { + let (from_offset, size) = self.stack_offset_and_size(sym); + debug_assert_eq!(from_offset % 8, 0); + debug_assert_eq!(size % 8, 0); + debug_assert_eq!(size, layout_interner.stack_size(*layout)); + self.copy_to_stack_offset(buf, size, from_offset, to_offset) + } + Builtin::Str | Builtin::List(_) => { + let (from_offset, size) = self.stack_offset_and_size(sym); + debug_assert_eq!(size, layout_interner.stack_size(*layout)); + self.copy_to_stack_offset(buf, size, from_offset, to_offset) + } + }, + LayoutRepr::LambdaSet(lambda_set) => { + // like its runtime representation + self.copy_symbol_to_stack_offset( + layout_interner, + buf, + to_offset, + sym, + &lambda_set.runtime_representation(), + ) + } + LayoutRepr::Struct { .. } | LayoutRepr::Union(UnionLayout::NonRecursive(_)) => { + let (from_offset, size) = self.stack_offset_and_size(sym); + debug_assert_eq!(size, layout_interner.stack_size(*layout)); + + self.copy_to_stack_offset(buf, size, from_offset, to_offset) + } + LayoutRepr::Erased(_) => todo_lambda_erasure!(), + pointer_layouts!() => { + // like a 64-bit integer + debug_assert_eq!(to_offset % 8, 0); + let reg = self.load_to_general_reg(buf, sym); + ASM::mov_base32_reg64(buf, to_offset, reg); + } + } + } + + pub fn copy_to_stack_offset( + &mut self, + buf: &mut Vec<'a, u8>, + size: u32, + from_offset: i32, + to_offset: i32, + ) { + let mut copied = 0; + let size = size as i32; + + self.with_tmp_general_reg(buf, |_storage_manager, buf, reg| { + // on targets beside x86, misaligned copies might be a problem + for _ in 0..size % 8 { + ASM::mov_reg8_base32(buf, reg, from_offset + copied); + ASM::mov_base32_reg8(buf, to_offset + copied, reg); + + copied += 1; + } + + if size - copied >= 8 { + for _ in (0..(size - copied)).step_by(8) { + ASM::mov_reg64_base32(buf, reg, from_offset + copied); + ASM::mov_base32_reg64(buf, to_offset + copied, reg); + + copied += 8; + } + } + + if size - copied >= 4 { + for _ in (0..(size - copied)).step_by(4) { + ASM::mov_reg32_base32(buf, reg, from_offset + copied); + ASM::mov_base32_reg32(buf, to_offset + copied, reg); + + copied += 4; + } + } + + if size - copied >= 2 { + for _ in (0..(size - copied)).step_by(2) { + ASM::mov_reg16_base32(buf, reg, from_offset + copied); + ASM::mov_base32_reg16(buf, to_offset + copied, reg); + + copied += 2; + } + } + + if size - copied >= 1 { + for _ in (0..(size - copied)).step_by(1) { + ASM::mov_reg8_base32(buf, reg, from_offset + copied); + ASM::mov_base32_reg8(buf, to_offset + copied, reg); + + copied += 1; + } + } + }); + } + + #[allow(dead_code)] + /// Ensures that a register is free. If it is not free, data will be moved to make it free. + pub fn ensure_reg_free( + &mut self, + buf: &mut Vec<'a, u8>, + wanted_reg: RegStorage, + ) { + match wanted_reg { + General(reg) => { + if self.general_free_regs.contains(®) { + return; + } + match self + .general_used_regs + .iter() + .position(|(used_reg, _sym)| reg == *used_reg) + { + Some(position) => { + let (used_reg, sym) = self.general_used_regs.remove(position); + self.free_to_stack(buf, &sym, wanted_reg); + self.general_free_regs.push(used_reg); + } + None => { + internal_error!("wanted register ({:?}) is not used or free", wanted_reg); + } + } + } + Float(reg) => { + if self.float_free_regs.contains(®) { + return; + } + match self + .float_used_regs + .iter() + .position(|(used_reg, _sym)| reg == *used_reg) + { + Some(position) => { + let (used_reg, sym) = self.float_used_regs.remove(position); + self.free_to_stack(buf, &sym, wanted_reg); + self.float_free_regs.push(used_reg); + } + None => { + internal_error!("wanted register ({:?}) is not used or free", wanted_reg); + } + } + } + } + } + + pub fn ensure_symbol_on_stack(&mut self, buf: &mut Vec<'a, u8>, sym: &Symbol) { + match self.remove_storage_for_sym(sym) { + Reg(reg_storage) => { + let base_offset = self.claim_stack_size_with_alignment(8, 8); + match reg_storage { + General(reg) => ASM::mov_base32_reg64(buf, base_offset, reg), + Float(reg) => ASM::mov_base32_freg64(buf, base_offset, reg), + } + self.symbol_storage_map.insert( + *sym, + Stack(Primitive { + base_offset, + reg: Some(reg_storage), + }), + ); + } + x => { + self.symbol_storage_map.insert(*sym, x); + } + } + } + + /// Frees all symbols to the stack setuping up a clean slate. + pub fn free_all_to_stack(&mut self, buf: &mut Vec<'a, u8>) { + let mut free_list = bumpalo::vec![in self.env.arena]; + for (sym, storage) in self.symbol_storage_map.iter() { + match storage { + Reg(reg_storage) + | Stack(Primitive { + reg: Some(reg_storage), + .. + }) => { + free_list.push((*sym, *reg_storage)); + } + _ => {} + } + } + for (sym, reg_storage) in free_list { + match reg_storage { + General(reg) => { + self.general_free_regs.push(reg); + self.general_used_regs.retain(|(r, _)| *r != reg); + } + Float(reg) => { + self.float_free_regs.push(reg); + self.float_used_regs.retain(|(r, _)| *r != reg); + } + } + self.free_to_stack(buf, &sym, reg_storage); + } + } + + /// Frees `wanted_reg` which is currently owned by `sym` by making sure the value is loaded on the stack. + /// Note, used and free regs are expected to be updated outside of this function. + fn free_to_stack( + &mut self, + buf: &mut Vec<'a, u8>, + sym: &Symbol, + wanted_reg: RegStorage, + ) { + match self.remove_storage_for_sym(sym) { + Reg(reg_storage) => { + debug_assert_eq!(reg_storage, wanted_reg); + let base_offset = self.claim_stack_size_with_alignment(8, 8); + match reg_storage { + General(reg) => ASM::mov_base32_reg64(buf, base_offset, reg), + Float(reg) => ASM::mov_base32_freg64(buf, base_offset, reg), + } + self.symbol_storage_map.insert( + *sym, + Stack(Primitive { + base_offset, + reg: None, + }), + ); + } + Stack(Primitive { + reg: Some(reg_storage), + base_offset, + }) => { + debug_assert_eq!(reg_storage, wanted_reg); + self.symbol_storage_map.insert( + *sym, + Stack(Primitive { + base_offset, + reg: None, + }), + ); + } + NoData + | Stack(Complex { .. } | Primitive { reg: None, .. } | ReferencedPrimitive { .. }) => { + internal_error!("Cannot free reg from symbol without a reg: {}", sym) + } + } + } + + /// gets the stack offset and size of the specified symbol. + /// the symbol must already be stored on the stack. + pub fn stack_offset_and_size(&self, sym: &Symbol) -> (i32, u32) { + match self.get_storage_for_sym(sym) { + Stack(Primitive { base_offset, .. }) => (*base_offset, 8), + Stack( + ReferencedPrimitive { + base_offset, size, .. + } + | Complex { base_offset, size }, + ) => (*base_offset, *size), + NoData => (0, 0), + storage => { + internal_error!( + "Data not on the stack for sym {:?} with storage {:?}", + sym, + storage + ) + } + } + } + + /// Specifies a symbol is loaded at the specified general register. + pub fn general_reg_arg(&mut self, sym: &Symbol, reg: GeneralReg) { + self.symbol_storage_map.insert(*sym, Reg(General(reg))); + self.general_free_regs.retain(|r| *r != reg); + self.general_used_regs.push((reg, *sym)); + } + + /// Specifies a symbol is loaded at the specified float register. + pub fn float_reg_arg(&mut self, sym: &Symbol, reg: FloatReg) { + self.symbol_storage_map.insert(*sym, Reg(Float(reg))); + self.float_free_regs.retain(|r| *r != reg); + self.float_used_regs.push((reg, *sym)); + } + + /// Specifies a primitive is loaded at the specific base offset. + pub fn primitive_stack_arg(&mut self, sym: &Symbol, base_offset: i32) { + self.symbol_storage_map.insert( + *sym, + Stack(Primitive { + base_offset, + reg: None, + }), + ); + self.allocation_map.insert(*sym, Rc::new((base_offset, 8))); + } + + /// Specifies a complex is loaded at the specific base offset. + pub fn complex_stack_arg(&mut self, sym: &Symbol, base_offset: i32, size: u32) { + self.symbol_storage_map + .insert(*sym, Stack(Complex { base_offset, size })); + self.allocation_map + .insert(*sym, Rc::new((base_offset, size))); + } + + /// Specifies a no data exists. + pub fn no_data(&mut self, sym: &Symbol) { + self.symbol_storage_map.insert(*sym, NoData); + } + + /// Loads the arg pointer symbol to the specified general reg. + pub fn ret_pointer_arg(&mut self, reg: GeneralReg) { + self.symbol_storage_map + .insert(Symbol::RET_POINTER, Reg(General(reg))); + self.general_free_regs.retain(|x| *x != reg); + self.general_used_regs.push((reg, Symbol::RET_POINTER)); + } + + /// updates the stack size to the max of its current value and the tmp size needed. + pub fn update_stack_size(&mut self, tmp_size: u32) { + self.stack_size = max(self.stack_size, tmp_size); + } + + /// updates the function call stack size to the max of its current value and the size need for this call. + pub fn update_fn_call_stack_size(&mut self, tmp_size: u32) { + self.fn_call_stack_size = max(self.fn_call_stack_size, tmp_size); + } + + fn joinpoint_argument_stack_storage( + &mut self, + layout_interner: &mut STLayoutInterner<'a>, + symbol: Symbol, + layout: InLayout<'a>, + ) { + match layout_interner.get_repr(layout) { + single_register_layouts!() | pointer_layouts!() => { + let base_offset = self.claim_stack_size_with_alignment(8, 8); + self.symbol_storage_map.insert( + symbol, + Stack(Primitive { + base_offset, + reg: None, + }), + ); + self.allocation_map + .insert(symbol, Rc::new((base_offset, 8))); + } + LayoutRepr::LambdaSet(lambda_set) => self.joinpoint_argument_stack_storage( + layout_interner, + symbol, + lambda_set.runtime_representation(), + ), + _ => { + let stack_size = layout_interner.stack_size(layout); + if stack_size == 0 { + self.no_data(&symbol); + } else { + self.claim_stack_area_layout(layout_interner, symbol, layout); + } + } + } + } + + /// Setups a join point. + /// To do this, each of the join pionts params are given a storage location. + /// Then those locations are stored. + /// Later jumps to the join point can overwrite the stored locations to pass parameters. + pub fn setup_joinpoint( + &mut self, + layout_interner: &mut STLayoutInterner<'a>, + _buf: &mut Vec<'a, u8>, + id: &JoinPointId, + params: &'a [Param<'a>], + ) { + let mut param_storage = bumpalo::vec![in self.env.arena]; + param_storage.reserve(params.len()); + for Param { symbol, layout } in params { + // Claim a location for every join point parameter to be loaded at. + // Put everything on the stack for simplicity. + self.joinpoint_argument_stack_storage(layout_interner, *symbol, *layout); + + param_storage.push(*self.get_storage_for_sym(symbol)); + } + self.join_param_map.insert(*id, param_storage); + } + + fn jump_argument_stack_storage( + &mut self, + layout_interner: &mut STLayoutInterner<'a>, + buf: &mut Vec<'a, u8>, + symbol: Symbol, + layout: InLayout<'a>, + base_offset: i32, + ) { + match layout_interner.get_repr(layout) { + single_register_integers!() | pointer_layouts!() => { + let reg = self.load_to_general_reg(buf, &symbol); + ASM::mov_base32_reg64(buf, base_offset, reg); + } + single_register_floats!() => { + let reg = self.load_to_float_reg(buf, &symbol); + ASM::mov_base32_freg64(buf, base_offset, reg); + } + LayoutRepr::LambdaSet(lambda_set) => { + self.jump_argument_stack_storage( + layout_interner, + buf, + symbol, + lambda_set.runtime_representation(), + base_offset, + ); + } + _ => { + internal_error!( + r"cannot load non-primitive layout ({:?}) to primitive stack location", + layout_interner.dbg(layout) + ) + } + } + } + + /// Setup jump loads the parameters for the joinpoint. + /// This enables the jump to correctly passe arguments to the joinpoint. + pub fn setup_jump( + &mut self, + layout_interner: &mut STLayoutInterner<'a>, + buf: &mut Vec<'a, u8>, + id: &JoinPointId, + args: &[Symbol], + arg_layouts: &[InLayout<'a>], + ) { + // TODO: remove was use here and for current_storage to deal with borrow checker. + // See if we can do this better. + let param_storage = match self.join_param_map.remove(id) { + Some(storages) => storages, + None => internal_error!("Jump: unknown point specified to jump to: {:?}", id), + }; + + let it = args.iter().zip(arg_layouts).zip(param_storage.iter()); + for ((sym, layout), wanted_storage) in it { + // Note: it is possible that the storage we want to move to is in use by one of the args we want to pass. + if self.get_storage_for_sym(sym) == wanted_storage { + continue; + } + match wanted_storage { + Reg(_) => { + internal_error!("Register storage is not allowed for jumping to joinpoint") + } + Stack(Complex { base_offset, .. }) => { + // TODO: This might be better not to call. + // Maybe we want a more memcpy like method to directly get called here. + // That would also be capable of asserting the size. + // Maybe copy stack to stack or something. + self.copy_symbol_to_stack_offset( + layout_interner, + buf, + *base_offset, + sym, + layout, + ); + } + Stack(Primitive { + base_offset, + reg: None, + }) => { + self.jump_argument_stack_storage( + layout_interner, + buf, + *sym, + *layout, + *base_offset, + ); + } + NoData => {} + Stack(Primitive { reg: Some(_), .. }) => { + internal_error!( + "primitives with register storage are not allowed for jumping to joinpoint" + ) + } + Stack(ReferencedPrimitive { .. }) => { + internal_error!( + "referenced primitive stack storage is not allowed for jumping to joinpoint" + ) + } + } + } + self.join_param_map.insert(*id, param_storage); + } + + /// Claim space on the stack for a certain layout. Size and alignment are handled + /// + /// This function: + /// + /// - deals with updating symbol storage. + /// - should only be used for complex data and not primitives. + /// - returns the base offset of the stack area. + pub(crate) fn claim_stack_area_layout( + &mut self, + layout_interner: &STLayoutInterner<'_>, + sym: Symbol, + layout: InLayout<'_>, + ) -> i32 { + let (size, alignment) = layout_interner.stack_size_and_alignment(layout); + self.claim_stack_area_with_alignment(sym, size, Ord::max(alignment, 8)) + } + + /// Claim space on the stack of a certain size and alignment. + /// + /// This function: + /// + /// - deals with updating symbol storage. + /// - should only be used for complex data and not primitives. + /// - returns the base offset of the stack area. + pub fn claim_stack_area_with_alignment( + &mut self, + sym: Symbol, + size: u32, + alignment: u32, + ) -> i32 { + let base_offset = self.claim_stack_size_with_alignment(size, alignment); + self.symbol_storage_map + .insert(sym, Stack(Complex { base_offset, size })); + self.allocation_map + .insert(sym, Rc::new((base_offset, size))); + base_offset + } + + pub fn claim_pointer_stack_area(&mut self, sym: Symbol) -> i32 { + // pointers are 8 bytes wide with an alignment of 8 + let base_offset = self.claim_stack_size_with_alignment(8, 8); + + self.symbol_storage_map.insert( + sym, + Stack(Primitive { + base_offset, + reg: None, + }), + ); + + base_offset + } + + fn claim_stack_size_with_alignment_help( + free_stack_chunks: &mut Vec<'a, (i32, u32)>, + stack_size: &mut u32, + amount: u32, + alignment: u32, + ) -> i32 { + debug_assert_ne!(amount, 0); + + pub const fn next_multiple_of(lhs: u32, rhs: u32) -> u32 { + match lhs % rhs { + 0 => lhs, + r => lhs + (rhs - r), + } + } + + pub const fn is_multiple_of(lhs: i32, rhs: i32) -> bool { + match rhs { + 0 => false, + _ => lhs % rhs == 0, + } + } + + // round value to the alignment. + let amount = next_multiple_of(amount, alignment); + + let chunk_fits = |(offset, size): &(i32, u32)| { + *size >= amount && is_multiple_of(*offset, alignment as i32) + }; + + // padding on the stack to make sure an allocation is aligned + let padding = next_multiple_of(*stack_size, alignment) - *stack_size; + + if let Some(fitting_chunk) = free_stack_chunks + .iter() + .enumerate() + .filter(|(_, chunk)| chunk_fits(chunk)) + .min_by_key(|(_, (_, size))| size) + { + let (pos, (offset, size)) = fitting_chunk; + let (offset, size) = (*offset, *size); + if size == amount { + // fill the whole chunk precisely + free_stack_chunks.remove(pos); + offset + } else { + // use part of this chunk + let (prev_offset, prev_size) = free_stack_chunks[pos]; + free_stack_chunks[pos] = (prev_offset + amount as i32, prev_size - amount); + prev_offset + } + } else if let Some(new_size) = stack_size.checked_add(padding + amount) { + // Since stack size is u32, but the max offset is i32, if we pass i32 max, we have overflowed. + if new_size > i32::MAX as u32 { + internal_error!("Ran out of stack space"); + } else { + *stack_size = new_size; + -(*stack_size as i32) + } + } else { + internal_error!("Ran out of stack space"); + } + } + + fn claim_stack_size_with_alignment(&mut self, amount: u32, alignment: u32) -> i32 { + // because many instructions work on 8 bytes, we play it safe and make all stack + // allocations a multiple of 8 bits wide. This is of course slightly suboptimal + // if you want to improve this, good luck! + let alignment = Ord::max(8, alignment); + + // the helper is just for testing in this case + Self::claim_stack_size_with_alignment_help( + &mut self.free_stack_chunks, + &mut self.stack_size, + amount, + alignment, + ) + } + + pub fn free_symbol(&mut self, sym: &Symbol) { + if self.join_param_map.remove(&JoinPointId(*sym)).is_some() { + // This is a join point and will not be in the storage map. + return; + } + match self.symbol_storage_map.remove(sym) { + // Free stack chunck if this is the last reference to the chunk. + Some(Stack(Primitive { base_offset, .. })) => { + self.free_stack_chunk(base_offset, 8); + } + Some(Stack(Complex { .. } | ReferencedPrimitive { .. })) => { + self.free_reference(sym); + } + _ => {} + } + for i in 0..self.general_used_regs.len() { + let (reg, saved_sym) = self.general_used_regs[i]; + if saved_sym == *sym { + self.general_free_regs.push(reg); + self.general_used_regs.remove(i); + break; + } + } + for i in 0..self.float_used_regs.len() { + let (reg, saved_sym) = self.float_used_regs[i]; + if saved_sym == *sym { + self.float_free_regs.push(reg); + self.float_used_regs.remove(i); + break; + } + } + } + + /// Frees an reference and release an allocation if it is no longer used. + fn free_reference(&mut self, sym: &Symbol) { + let owned_data = self.remove_allocation_for_sym(sym); + if Rc::strong_count(&owned_data) == 1 { + self.free_stack_chunk(owned_data.0, owned_data.1); + } + } + + fn free_stack_chunk(&mut self, base_offset: i32, size: u32) { + let loc = (base_offset, size); + // Note: this position current points to the offset following the specified location. + // If loc was inserted at this position, it would shift the data at this position over by 1. + let pos = self + .free_stack_chunks + .binary_search(&loc) + .unwrap_or_else(|e| e); + + // Check for overlap with previous and next free chunk. + let merge_with_prev = if pos > 0 { + if let Some((prev_offset, prev_size)) = self.free_stack_chunks.get(pos - 1) { + let prev_end = *prev_offset + *prev_size as i32; + if prev_end > base_offset { + internal_error!("Double free? A previously freed stack location overlaps with the currently freed stack location."); + } + prev_end == base_offset + } else { + false + } + } else { + false + }; + let merge_with_next = if let Some((next_offset, _)) = self.free_stack_chunks.get(pos) { + let current_end = base_offset + size as i32; + if current_end > *next_offset { + internal_error!("Double free? A previously freed stack location overlaps with the currently freed stack location."); + } + current_end == *next_offset + } else { + false + }; + + match (merge_with_prev, merge_with_next) { + (true, true) => { + let (prev_offset, prev_size) = self.free_stack_chunks[pos - 1]; + let (_, next_size) = self.free_stack_chunks[pos]; + self.free_stack_chunks[pos - 1] = (prev_offset, prev_size + size + next_size); + self.free_stack_chunks.remove(pos); + } + (true, false) => { + let (prev_offset, prev_size) = self.free_stack_chunks[pos - 1]; + self.free_stack_chunks[pos - 1] = (prev_offset, prev_size + size); + } + (false, true) => { + let (_, next_size) = self.free_stack_chunks[pos]; + self.free_stack_chunks[pos] = (base_offset, next_size + size); + } + (false, false) => self.free_stack_chunks.insert(pos, loc), + } + } + + pub fn push_used_caller_saved_regs_to_stack(&mut self, buf: &mut Vec<'a, u8>) { + let old_general_used_regs = std::mem::replace( + &mut self.general_used_regs, + bumpalo::vec![in self.env.arena], + ); + for (reg, saved_sym) in old_general_used_regs.into_iter() { + if CC::general_caller_saved(®) { + self.general_free_regs.push(reg); + self.free_to_stack(buf, &saved_sym, General(reg)); + } else { + self.general_used_regs.push((reg, saved_sym)); + } + } + let old_float_used_regs = + std::mem::replace(&mut self.float_used_regs, bumpalo::vec![in self.env.arena]); + for (reg, saved_sym) in old_float_used_regs.into_iter() { + if CC::float_caller_saved(®) { + self.float_free_regs.push(reg); + self.free_to_stack(buf, &saved_sym, Float(reg)); + } else { + self.float_used_regs.push((reg, saved_sym)); + } + } + } + + #[allow(dead_code)] + /// Gets the allocated area for a symbol. The index symbol must be defined. + fn get_allocation_for_sym(&self, sym: &Symbol) -> &Rc<(i32, u32)> { + if let Some(allocation) = self.allocation_map.get(sym) { + allocation + } else { + internal_error!("Unknown symbol: {:?}", sym); + } + } + + /// Removes and returns the allocated area for a symbol. They index symbol must be defined. + fn remove_allocation_for_sym(&mut self, sym: &Symbol) -> Rc<(i32, u32)> { + if let Some(allocation) = self.allocation_map.remove(sym) { + allocation + } else { + internal_error!("Unknown symbol: {:?}", sym); + } + } + + /// Gets a value from storage. The index symbol must be defined. + fn get_storage_for_sym(&self, sym: &Symbol) -> &Storage { + if let Some(storage) = self.symbol_storage_map.get(sym) { + storage + } else { + internal_error!("Unknown symbol: {:?}", sym); + } + } + + /// Removes and returns a value from storage. They index symbol must be defined. + fn remove_storage_for_sym(&mut self, sym: &Symbol) -> Storage { + if let Some(storage) = self.symbol_storage_map.remove(sym) { + storage + } else { + internal_error!("Unknown symbol: {:?}", sym); + } + } +} + +fn is_primitive(layout_interner: &mut STLayoutInterner<'_>, layout: InLayout<'_>) -> bool { + match layout_interner.get_repr(layout) { + single_register_layouts!() => true, + pointer_layouts!() => true, + LayoutRepr::LambdaSet(lambda_set) => { + is_primitive(layout_interner, lambda_set.runtime_representation()) + } + _ => false, + } +} + +#[cfg(test)] +mod tests { + use crate::generic64::x86_64::{ + X86_64Assembler, X86_64FloatReg, X86_64GeneralReg, X86_64SystemV, + }; + + use super::*; + + type SystemVStorageManager<'a, 'r> = + StorageManager<'a, 'r, X86_64GeneralReg, X86_64FloatReg, X86_64Assembler, X86_64SystemV>; + + fn claim_helper( + mut free_stack_chunks: Vec<'_, (i32, u32)>, + mut stack_size: u32, + amount: u32, + alignment: u32, + ) -> (u32, i32, Vec<'_, (i32, u32)>) { + let offset = SystemVStorageManager::claim_stack_size_with_alignment_help( + &mut free_stack_chunks, + &mut stack_size, + amount, + alignment, + ); + + (stack_size, offset, free_stack_chunks) + } + + #[test] + fn claim_stack_memory() { + use bumpalo::vec; + let arena = bumpalo::Bump::new(); + + assert_eq!( + claim_helper(vec![in &arena;], 24, 8, 8), + (32, -32, vec![in &arena;]), + ); + + assert_eq!( + claim_helper(vec![in &arena;], 8, 8, 8), + (16, -16, vec![in &arena;]), + ); + + assert_eq!( + claim_helper(vec![in &arena; (-16, 8)], 56, 4, 4), + (56, -16, vec![in &arena; (-12, 4)]) + ); + + assert_eq!( + claim_helper(vec![in &arena; (-24, 16)], 32, 8, 8), + (32, -24, vec![in &arena; (-16, 8)]) + ); + + assert_eq!( + claim_helper(vec![in &arena; ], 0, 2, 1), + (2, -2, vec![in &arena;]) + ); + + assert_eq!( + claim_helper(vec![in &arena; (-8, 2)], 16, 2, 1), + (16, -8, vec![in &arena; ]) + ); + } +} diff --git a/crates/compiler/gen_dev/src/generic64/x86_64.rs b/crates/compiler/gen_dev/src/generic64/x86_64.rs new file mode 100644 index 0000000000..e44eab3ace --- /dev/null +++ b/crates/compiler/gen_dev/src/generic64/x86_64.rs @@ -0,0 +1,5236 @@ +use crate::generic64::{storage::StorageManager, Assembler, CallConv, RegTrait}; +use crate::{ + pointer_layouts, single_register_floats, single_register_int_builtins, + single_register_integers, single_register_layouts, Relocation, +}; +use bumpalo::collections::Vec; +use roc_builtins::bitcode::{FloatWidth, IntWidth}; +use roc_error_macros::internal_error; +use roc_module::symbol::Symbol; +use roc_mono::layout::{ + Builtin, InLayout, Layout, LayoutInterner, LayoutRepr, STLayoutInterner, UnionLayout, +}; + +use super::{CompareOperation, RegisterWidth}; + +// Not sure exactly how I want to represent registers. +// If we want max speed, we would likely make them structs that impl the same trait to avoid ifs. +#[derive(Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Debug)] +pub enum X86_64GeneralReg { + RAX = 0, + RCX = 1, + RDX = 2, + RBX = 3, + RSP = 4, + RBP = 5, + RSI = 6, + RDI = 7, + R8 = 8, + R9 = 9, + R10 = 10, + R11 = 11, + R12 = 12, + R13 = 13, + R14 = 14, + R15 = 15, +} +impl RegTrait for X86_64GeneralReg { + fn value(&self) -> u8 { + *self as u8 + } +} +impl std::fmt::Display for X86_64GeneralReg { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!( + f, + "{}", + match self { + X86_64GeneralReg::RAX => "rax", + X86_64GeneralReg::RBX => "rbx", + X86_64GeneralReg::RCX => "rcx", + X86_64GeneralReg::RDX => "rdx", + X86_64GeneralReg::RBP => "rbp", + X86_64GeneralReg::RSP => "rsp", + X86_64GeneralReg::RDI => "rdi", + X86_64GeneralReg::RSI => "rsi", + X86_64GeneralReg::R8 => "r8", + X86_64GeneralReg::R9 => "r9", + X86_64GeneralReg::R10 => "r10", + X86_64GeneralReg::R11 => "r11", + X86_64GeneralReg::R12 => "r12", + X86_64GeneralReg::R13 => "r13", + X86_64GeneralReg::R14 => "r14", + X86_64GeneralReg::R15 => "r15", + } + ) + } +} + +#[derive(Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Debug)] +pub enum X86_64FloatReg { + XMM0 = 0, + XMM1 = 1, + XMM2 = 2, + XMM3 = 3, + XMM4 = 4, + XMM5 = 5, + XMM6 = 6, + XMM7 = 7, + XMM8 = 8, + XMM9 = 9, + XMM10 = 10, + XMM11 = 11, + XMM12 = 12, + XMM13 = 13, + XMM14 = 14, + XMM15 = 15, +} +impl RegTrait for X86_64FloatReg { + fn value(&self) -> u8 { + *self as u8 + } +} +impl std::fmt::Display for X86_64FloatReg { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!( + f, + "{}", + match self { + X86_64FloatReg::XMM0 => "xmm0", + X86_64FloatReg::XMM1 => "xmm1", + X86_64FloatReg::XMM2 => "xmm2", + X86_64FloatReg::XMM3 => "xmm3", + X86_64FloatReg::XMM4 => "xmm4", + X86_64FloatReg::XMM5 => "xmm5", + X86_64FloatReg::XMM6 => "xmm6", + X86_64FloatReg::XMM7 => "xmm7", + X86_64FloatReg::XMM8 => "xmm8", + X86_64FloatReg::XMM9 => "xmm9", + X86_64FloatReg::XMM10 => "xmm10", + X86_64FloatReg::XMM11 => "xmm11", + X86_64FloatReg::XMM12 => "xmm12", + X86_64FloatReg::XMM13 => "xmm13", + X86_64FloatReg::XMM14 => "xmm14", + X86_64FloatReg::XMM15 => "xmm15", + } + ) + } +} + +#[derive(Copy, Clone)] +pub struct X86_64Assembler {} +#[derive(Copy, Clone)] +pub struct X86_64WindowsFastcall {} +#[derive(Copy, Clone)] +pub struct X86_64SystemV {} + +const STACK_ALIGNMENT: u8 = 16; + +impl CallConv for X86_64SystemV { + const BASE_PTR_REG: X86_64GeneralReg = X86_64GeneralReg::RBP; + const STACK_PTR_REG: X86_64GeneralReg = X86_64GeneralReg::RSP; + + const GENERAL_PARAM_REGS: &'static [X86_64GeneralReg] = &[ + X86_64GeneralReg::RDI, + X86_64GeneralReg::RSI, + X86_64GeneralReg::RDX, + X86_64GeneralReg::RCX, + X86_64GeneralReg::R8, + X86_64GeneralReg::R9, + ]; + const GENERAL_RETURN_REGS: &'static [X86_64GeneralReg] = + &[X86_64GeneralReg::RAX, X86_64GeneralReg::RDX]; + const GENERAL_DEFAULT_FREE_REGS: &'static [X86_64GeneralReg] = &[ + // The regs we want to use first should be at the end of this vec. + // We will use pop to get which reg to use next + // Use callee saved regs last. + X86_64GeneralReg::RBX, + // Don't use frame pointer: X86_64GeneralReg::RBP, + X86_64GeneralReg::R12, + X86_64GeneralReg::R13, + X86_64GeneralReg::R14, + X86_64GeneralReg::R15, + // Use caller saved regs first. + X86_64GeneralReg::RAX, + X86_64GeneralReg::RCX, + X86_64GeneralReg::RDX, + // Don't use stack pointer: X86_64GeneralReg::RSP, + X86_64GeneralReg::RSI, + X86_64GeneralReg::RDI, + X86_64GeneralReg::R8, + X86_64GeneralReg::R9, + X86_64GeneralReg::R10, + X86_64GeneralReg::R11, + ]; + + const FLOAT_PARAM_REGS: &'static [X86_64FloatReg] = &[ + X86_64FloatReg::XMM0, + X86_64FloatReg::XMM1, + X86_64FloatReg::XMM2, + X86_64FloatReg::XMM3, + X86_64FloatReg::XMM4, + X86_64FloatReg::XMM5, + X86_64FloatReg::XMM6, + X86_64FloatReg::XMM7, + ]; + const FLOAT_RETURN_REGS: &'static [X86_64FloatReg] = + &[X86_64FloatReg::XMM0, X86_64FloatReg::XMM1]; + const FLOAT_DEFAULT_FREE_REGS: &'static [X86_64FloatReg] = &[ + // The regs we want to use first should be at the end of this vec. + // We will use pop to get which reg to use next + // No callee saved regs. + // Use caller saved regs first. + X86_64FloatReg::XMM15, + X86_64FloatReg::XMM14, + X86_64FloatReg::XMM13, + X86_64FloatReg::XMM12, + X86_64FloatReg::XMM11, + X86_64FloatReg::XMM10, + X86_64FloatReg::XMM9, + X86_64FloatReg::XMM8, + X86_64FloatReg::XMM7, + X86_64FloatReg::XMM6, + X86_64FloatReg::XMM5, + X86_64FloatReg::XMM4, + X86_64FloatReg::XMM3, + X86_64FloatReg::XMM2, + X86_64FloatReg::XMM1, + X86_64FloatReg::XMM0, + ]; + const SHADOW_SPACE_SIZE: u8 = 0; + + // These are registers that a called function must save and restore if it wants to use them. + #[inline(always)] + fn general_callee_saved(reg: &X86_64GeneralReg) -> bool { + matches!( + reg, + X86_64GeneralReg::RBX + | X86_64GeneralReg::RBP + | X86_64GeneralReg::R12 + | X86_64GeneralReg::R13 + | X86_64GeneralReg::R14 + | X86_64GeneralReg::R15 + ) + } + + #[inline(always)] + fn float_callee_saved(_reg: &X86_64FloatReg) -> bool { + false + } + + #[inline(always)] + fn setup_stack( + buf: &mut Vec<'_, u8>, + saved_general_regs: &[X86_64GeneralReg], + saved_float_regs: &[X86_64FloatReg], + requested_stack_size: i32, + fn_call_stack_size: i32, + ) -> i32 { + x86_64_generic_setup_stack( + buf, + saved_general_regs, + saved_float_regs, + requested_stack_size, + fn_call_stack_size, + ) + } + + #[inline(always)] + fn cleanup_stack( + buf: &mut Vec<'_, u8>, + saved_general_regs: &[X86_64GeneralReg], + saved_float_regs: &[X86_64FloatReg], + aligned_stack_size: i32, + fn_call_stack_size: i32, + ) { + x86_64_generic_cleanup_stack( + buf, + saved_general_regs, + saved_float_regs, + aligned_stack_size, + fn_call_stack_size, + ) + } + + #[inline(always)] + fn load_args<'a>( + buf: &mut Vec<'a, u8>, + storage_manager: &mut StorageManager< + 'a, + '_, + X86_64GeneralReg, + X86_64FloatReg, + X86_64Assembler, + X86_64SystemV, + >, + layout_interner: &mut STLayoutInterner<'a>, + args: &'a [(InLayout<'a>, Symbol)], + ret_layout: &InLayout<'a>, + ) { + let returns_via_pointer = + X86_64SystemV::returns_via_arg_pointer(layout_interner, ret_layout); + + let mut state = X64_64SystemVLoadArgs { + general_i: usize::from(returns_via_pointer), + float_i: 0, + // 16 is the size of the pushed return address and base pointer. + argument_offset: X86_64SystemV::SHADOW_SPACE_SIZE as i32 + 16, + }; + + if returns_via_pointer { + storage_manager.ret_pointer_arg(X86_64SystemV::GENERAL_PARAM_REGS[0]); + } + + for (in_layout, sym) in args.iter() { + state.load_arg(buf, storage_manager, layout_interner, *sym, *in_layout); + } + } + + #[inline(always)] + fn store_args<'a>( + buf: &mut Vec<'a, u8>, + storage_manager: &mut StorageManager< + 'a, + '_, + X86_64GeneralReg, + X86_64FloatReg, + X86_64Assembler, + X86_64SystemV, + >, + layout_interner: &mut STLayoutInterner<'a>, + dst: &Symbol, + args: &[Symbol], + arg_layouts: &[InLayout<'a>], + ret_layout: &InLayout<'a>, + ) { + let mut general_i = 0; + + if Self::returns_via_arg_pointer(layout_interner, ret_layout) { + // Save space on the stack for the result we will be return. + let base_offset = + storage_manager.claim_stack_area_layout(layout_interner, *dst, *ret_layout); + // Set the first reg to the address base + offset. + let ret_reg = Self::GENERAL_PARAM_REGS[general_i]; + general_i += 1; + X86_64Assembler::add_reg64_reg64_imm32( + buf, + ret_reg, + X86_64GeneralReg::RBP, + base_offset, + ); + } + + let mut state = X64_64SystemVStoreArgs { + general_i, + float_i: 0, + tmp_stack_offset: Self::SHADOW_SPACE_SIZE as i32, + }; + + for (sym, in_layout) in args.iter().zip(arg_layouts.iter()) { + state.store_arg(buf, storage_manager, layout_interner, *sym, *in_layout); + } + + storage_manager.update_fn_call_stack_size(state.tmp_stack_offset as u32); + } + + fn return_complex_symbol<'a>( + buf: &mut Vec<'a, u8>, + storage_manager: &mut StorageManager< + 'a, + '_, + X86_64GeneralReg, + X86_64FloatReg, + X86_64Assembler, + X86_64SystemV, + >, + layout_interner: &mut STLayoutInterner<'a>, + sym: &Symbol, + layout: &InLayout<'a>, + ) { + match layout_interner.get_repr(*layout) { + single_register_layouts!() => { + internal_error!("single register layouts are not complex symbols"); + } + _ if layout_interner.stack_size(*layout) == 0 => {} + _ if !Self::returns_via_arg_pointer(layout_interner, layout) => { + let (base_offset, size) = storage_manager.stack_offset_and_size(sym); + + if size <= 8 { + X86_64Assembler::mov_reg64_base32( + buf, + Self::GENERAL_RETURN_REGS[0], + base_offset, + ); + } else if size <= 16 { + X86_64Assembler::mov_reg64_base32( + buf, + Self::GENERAL_RETURN_REGS[0], + base_offset, + ); + X86_64Assembler::mov_reg64_base32( + buf, + Self::GENERAL_RETURN_REGS[1], + base_offset + 8, + ); + } else { + internal_error!( + "types that don't return via arg pointer must be less than 16 bytes" + ); + } + } + _ => { + // This is a large type returned via the arg pointer. + storage_manager.copy_symbol_to_arg_pointer(buf, sym, layout); + // Also set the return reg to the arg pointer. + storage_manager.load_to_specified_general_reg( + buf, + &Symbol::RET_POINTER, + Self::GENERAL_RETURN_REGS[0], + ); + } + } + } + + fn load_returned_complex_symbol<'a>( + buf: &mut Vec<'a, u8>, + storage_manager: &mut StorageManager< + 'a, + '_, + X86_64GeneralReg, + X86_64FloatReg, + X86_64Assembler, + X86_64SystemV, + >, + layout_interner: &mut STLayoutInterner<'a>, + sym: &Symbol, + layout: &InLayout<'a>, + ) { + match layout_interner.get_repr(*layout) { + single_register_layouts!() => { + internal_error!("single register layouts are not complex symbols"); + } + _ if layout_interner.stack_size(*layout) == 0 => { + storage_manager.no_data(sym); + } + _ if !Self::returns_via_arg_pointer(layout_interner, layout) => { + let size = layout_interner.stack_size(*layout); + let offset = + storage_manager.claim_stack_area_layout(layout_interner, *sym, *layout); + if size <= 8 { + X86_64Assembler::mov_base32_reg64(buf, offset, Self::GENERAL_RETURN_REGS[0]); + } else if size <= 16 { + X86_64Assembler::mov_base32_reg64(buf, offset, Self::GENERAL_RETURN_REGS[0]); + X86_64Assembler::mov_base32_reg64( + buf, + offset + 8, + Self::GENERAL_RETURN_REGS[1], + ); + } else { + internal_error!( + "types that don't return via arg pointer must be less than 16 bytes" + ); + } + } + _ => { + // This should have been received via an arg pointer. + // That means the value is already loaded onto the stack area we allocated before the call. + // Nothing to do. + } + } + } + + fn setjmp(buf: &mut Vec<'_, u8>) { + use X86_64GeneralReg::*; + type ASM = X86_64Assembler; + + // based on the musl libc setjmp implementation + // + // 000000000020237c <__setjmp>: + // 20237c: 48 89 1f mov QWORD PTR [rdi],rbx + // 20237f: 48 89 6f 08 mov QWORD PTR [rdi+0x8],rbp + // 202383: 4c 89 67 10 mov QWORD PTR [rdi+0x10],r12 + // 202387: 4c 89 6f 18 mov QWORD PTR [rdi+0x18],r13 + // 20238b: 4c 89 77 20 mov QWORD PTR [rdi+0x20],r14 + // 20238f: 4c 89 7f 28 mov QWORD PTR [rdi+0x28],r15 + // 202393: 48 8d 54 24 08 lea rdx,[rsp+0x8] + // 202398: 48 89 57 30 mov QWORD PTR [rdi+0x30],rdx + // 20239c: 48 8b 14 24 mov rdx,QWORD PTR [rsp] + // 2023a0: 48 89 57 38 mov QWORD PTR [rdi+0x38],rdx + // 2023a4: 31 c0 xor eax,eax + // 2023a6: c3 ret + + let env = RDI; + + // store caller-saved (i.e. non-volatile) registers + ASM::mov_mem64_offset32_reg64(buf, env, 0x00, RBX); + ASM::mov_mem64_offset32_reg64(buf, env, 0x08, RBP); + ASM::mov_mem64_offset32_reg64(buf, env, 0x10, R12); + ASM::mov_mem64_offset32_reg64(buf, env, 0x18, R13); + ASM::mov_mem64_offset32_reg64(buf, env, 0x20, R14); + ASM::mov_mem64_offset32_reg64(buf, env, 0x28, R15); + + // go one value up (as if setjmp wasn't called) + lea_reg64_offset8(buf, RDX, RSP, 0x8); + + // store the new stack pointer + ASM::mov_mem64_offset32_reg64(buf, env, 0x30, RDX); + + // store the address we'll resume at + ASM::mov_reg64_mem64_offset32(buf, RDX, RSP, 0); + ASM::mov_mem64_offset32_reg64(buf, env, 0x38, RDX); + + // zero out eax, so we return 0 (we do a 64-bit xor for convenience) + ASM::xor_reg64_reg64_reg64(buf, RAX, RAX, RAX); + + ASM::ret(buf) + } + + fn longjmp(buf: &mut Vec<'_, u8>) { + use X86_64GeneralReg::*; + type ASM = X86_64Assembler; + + // 202358: 31 c0 xor eax,eax + // 20235a: 83 fe 01 cmp esi,0x1 + // 20235d: 11 f0 adc eax,esi + // 20235f: 48 8b 1f mov rbx,QWORD PTR [rdi] + // 202362: 48 8b 6f 08 mov rbp,QWORD PTR [rdi+0x8] + // 202366: 4c 8b 67 10 mov r12,QWORD PTR [rdi+0x10] + // 20236a: 4c 8b 6f 18 mov r13,QWORD PTR [rdi+0x18] + // 20236e: 4c 8b 77 20 mov r14,QWORD PTR [rdi+0x20] + // 202372: 4c 8b 7f 28 mov r15,QWORD PTR [rdi+0x28] + // 202376: 48 8b 67 30 mov rsp,QWORD PTR [rdi+0x30] + // 20237a: ff 67 38 jmp QWORD PTR [rdi+0x38] + + // make sure something nonzero is returned ?! + ASM::mov_reg64_reg64(buf, RAX, RSI); + + // load the caller-saved registers + let env = RDI; + ASM::mov_reg64_mem64_offset32(buf, RBX, env, 0x00); + ASM::mov_reg64_mem64_offset32(buf, RBP, env, 0x08); + ASM::mov_reg64_mem64_offset32(buf, R12, env, 0x10); + ASM::mov_reg64_mem64_offset32(buf, R13, env, 0x18); + ASM::mov_reg64_mem64_offset32(buf, R14, env, 0x20); + ASM::mov_reg64_mem64_offset32(buf, R15, env, 0x28); + + // value of rsp before the setjmp call + ASM::mov_reg64_mem64_offset32(buf, RSP, env, 0x30); + + jmp_reg64_offset8(buf, env, 0x38) + } + + fn roc_panic(buf: &mut Vec<'_, u8>, relocs: &mut Vec<'_, Relocation>) { + use X86_64GeneralReg::*; + type ASM = X86_64Assembler; + + // move the first argument to roc_panic (a *RocStr) into r8 + ASM::add_reg64_reg64_imm32(buf, R8, RSP, 8); + + // move the crash tag into the second return register. We add 1 to it because the 0 value + // is already used for "no crash occurred" + ASM::add_reg64_reg64_imm32(buf, RDX, RDI, 1); + + // the setlongjmp_buffer + ASM::data_pointer(buf, relocs, String::from("setlongjmp_buffer"), RDI); + + // the value to return from the longjmp. It is a pointer to the last 3 words of the setlongjmp_buffer + // they represent the errore message. + ASM::mov_reg64_imm64(buf, RSI, 0x40); + ASM::add_reg64_reg64_reg64(buf, RSI, RSI, RDI); + + for offset in [0, 8, 16] { + ASM::mov_reg64_mem64_offset32(buf, R9, R8, offset); + ASM::mov_mem64_offset32_reg64(buf, RSI, offset, R9); + } + + Self::longjmp(buf) + } +} + +fn copy_symbol_to_stack_offset<'a, CC>( + buf: &mut Vec<'a, u8>, + storage_manager: &mut X86_64StorageManager<'a, '_, CC>, + sym: Symbol, + tmp_reg: X86_64GeneralReg, + stack_offset: i32, +) -> u32 +where + CC: CallConv, +{ + type ASM = X86_64Assembler; + + let mut copied = 0; + let (base_offset, size) = storage_manager.stack_offset_and_size(&sym); + + if size - copied >= 8 { + for _ in 0..(size - copied) / 8 { + ASM::mov_reg64_base32(buf, tmp_reg, base_offset + copied as i32); + ASM::mov_stack32_reg64(buf, stack_offset + copied as i32, tmp_reg); + + copied += 8; + } + } + + if size - copied >= 4 { + for _ in 0..(size - copied) / 4 { + ASM::mov_reg32_base32(buf, tmp_reg, base_offset + copied as i32); + ASM::mov_stack32_reg32(buf, stack_offset + copied as i32, tmp_reg); + + copied += 4; + } + } + + if size - copied >= 2 { + for _ in 0..(size - copied) / 2 { + ASM::mov_reg16_base32(buf, tmp_reg, base_offset + copied as i32); + ASM::mov_stack32_reg16(buf, stack_offset + copied as i32, tmp_reg); + + copied += 2; + } + } + + if size - copied >= 1 { + for _ in 0..(size - copied) { + ASM::mov_reg8_base32(buf, tmp_reg, base_offset + copied as i32); + ASM::mov_stack32_reg8(buf, stack_offset + copied as i32, tmp_reg); + + copied += 1; + } + } + + size +} + +pub(crate) fn copy_to_base_offset( + buf: &mut Vec<'_, u8>, + dst_base_offset: i32, + stack_size: u32, + ptr_reg: GeneralReg, + tmp_reg: GeneralReg, + read_offset: i32, +) where + FloatReg: RegTrait, + GeneralReg: RegTrait, + ASM: Assembler, +{ + let mut copied = 0; + let size = stack_size as i32; + let base_offset = dst_base_offset; + + if size - copied >= 8 { + for _ in (0..(size - copied)).step_by(8) { + ASM::mov_reg64_mem64_offset32(buf, tmp_reg, ptr_reg, read_offset + copied); + ASM::mov_base32_reg64(buf, base_offset + copied, tmp_reg); + + copied += 8; + } + } + + if size - copied >= 4 { + for _ in (0..(size - copied)).step_by(4) { + ASM::mov_reg32_mem32_offset32(buf, tmp_reg, ptr_reg, read_offset + copied); + ASM::mov_base32_reg32(buf, base_offset + copied, tmp_reg); + + copied += 4; + } + } + + if size - copied >= 2 { + for _ in (0..(size - copied)).step_by(2) { + ASM::mov_reg16_mem16_offset32(buf, tmp_reg, ptr_reg, read_offset + copied); + ASM::mov_base32_reg16(buf, base_offset + copied, tmp_reg); + + copied += 2; + } + } + + if size - copied >= 1 { + for _ in (0..(size - copied)).step_by(1) { + ASM::mov_reg8_mem8_offset32(buf, tmp_reg, ptr_reg, read_offset + copied); + ASM::mov_base32_reg8(buf, base_offset + copied, tmp_reg); + + copied += 1; + } + } +} + +struct X64_64SystemVStoreArgs { + general_i: usize, + float_i: usize, + tmp_stack_offset: i32, +} + +impl X64_64SystemVStoreArgs { + const GENERAL_PARAM_REGS: &'static [X86_64GeneralReg] = X86_64SystemV::GENERAL_PARAM_REGS; + const GENERAL_RETURN_REGS: &'static [X86_64GeneralReg] = X86_64SystemV::GENERAL_RETURN_REGS; + + const FLOAT_PARAM_REGS: &'static [X86_64FloatReg] = X86_64SystemV::FLOAT_PARAM_REGS; + const FLOAT_RETURN_REGS: &'static [X86_64FloatReg] = X86_64SystemV::FLOAT_RETURN_REGS; + + fn store_arg<'a>( + &mut self, + buf: &mut Vec<'a, u8>, + storage_manager: &mut X86_64StorageManager<'a, '_, X86_64SystemV>, + layout_interner: &mut STLayoutInterner<'a>, + sym: Symbol, + in_layout: InLayout<'a>, + ) { + type ASM = X86_64Assembler; + + // we use the return register as a temporary register; it will be overwritten anyway + let tmp_reg = Self::GENERAL_RETURN_REGS[0]; + + match layout_interner.get_repr(in_layout) { + single_register_integers!() => self.store_arg_general(buf, storage_manager, sym), + pointer_layouts!() => self.store_arg_general(buf, storage_manager, sym), + single_register_floats!() => self.store_arg_float(buf, storage_manager, sym), + LayoutRepr::I128 | LayoutRepr::U128 | LayoutRepr::DEC => { + let (offset, _) = storage_manager.stack_offset_and_size(&sym); + + if self.general_i + 1 < Self::GENERAL_PARAM_REGS.len() { + let reg1 = Self::GENERAL_PARAM_REGS[self.general_i]; + let reg2 = Self::GENERAL_PARAM_REGS[self.general_i + 1]; + + ASM::mov_reg64_base32(buf, reg1, offset); + ASM::mov_reg64_base32(buf, reg2, offset + 8); + + self.general_i += 2; + } else { + // Copy to stack using return reg as buffer. + let reg = Self::GENERAL_RETURN_REGS[0]; + + ASM::mov_reg64_base32(buf, reg, offset); + ASM::mov_stack32_reg64(buf, self.tmp_stack_offset, reg); + + ASM::mov_reg64_base32(buf, reg, offset + 8); + ASM::mov_stack32_reg64(buf, self.tmp_stack_offset + 8, reg); + + self.tmp_stack_offset += 16; + } + } + _ if layout_interner.stack_size(in_layout) == 0 => {} + _ if layout_interner.stack_size(in_layout) > 16 => { + // TODO: Double check this. + // Just copy onto the stack. + let stack_offset = self.tmp_stack_offset; + + let size = + copy_symbol_to_stack_offset(buf, storage_manager, sym, tmp_reg, stack_offset); + + self.tmp_stack_offset += size as i32; + } + LayoutRepr::LambdaSet(lambda_set) => self.store_arg( + buf, + storage_manager, + layout_interner, + sym, + lambda_set.runtime_representation(), + ), + LayoutRepr::Struct { .. } => { + let stack_size = layout_interner.stack_size(in_layout); + if stack_size <= 8 { + self.store_arg_64bit(buf, storage_manager, sym); + } else if stack_size <= 16 { + self.store_arg_128bit(buf, storage_manager, sym); + } else { + unreachable!("covered by earlier branch"); + } + } + LayoutRepr::Union(UnionLayout::NonRecursive(_)) => { + let stack_offset = self.tmp_stack_offset; + + let size = + copy_symbol_to_stack_offset(buf, storage_manager, sym, tmp_reg, stack_offset); + + self.tmp_stack_offset += size as i32; + } + _ => { + todo!( + "calling with arg type, {:?}", + layout_interner.dbg(in_layout) + ); + } + } + } + + fn store_arg_general<'a>( + &mut self, + buf: &mut Vec<'a, u8>, + storage_manager: &mut X86_64StorageManager<'a, '_, X86_64SystemV>, + sym: Symbol, + ) { + match Self::GENERAL_PARAM_REGS.get(self.general_i) { + Some(reg) => { + storage_manager.load_to_specified_general_reg(buf, &sym, *reg); + self.general_i += 1; + } + None => { + // Copy to stack using return reg as buffer. + let tmp = Self::GENERAL_RETURN_REGS[0]; + + storage_manager.load_to_specified_general_reg(buf, &sym, tmp); + X86_64Assembler::mov_stack32_reg64(buf, self.tmp_stack_offset, tmp); + + self.tmp_stack_offset += 8; + } + } + } + + fn store_arg_float<'a>( + &mut self, + buf: &mut Vec<'a, u8>, + storage_manager: &mut X86_64StorageManager<'a, '_, X86_64SystemV>, + sym: Symbol, + ) { + match Self::FLOAT_PARAM_REGS.get(self.float_i) { + Some(reg) => { + storage_manager.load_to_specified_float_reg(buf, &sym, *reg); + self.float_i += 1; + } + None => { + // Copy to stack using return reg as buffer. + let tmp = Self::FLOAT_RETURN_REGS[0]; + + storage_manager.load_to_specified_float_reg(buf, &sym, tmp); + X86_64Assembler::mov_stack32_freg64(buf, self.tmp_stack_offset, tmp); + + self.tmp_stack_offset += 8; + } + } + } + + fn store_arg_64bit<'a>( + &mut self, + buf: &mut Vec<'a, u8>, + storage_manager: &mut X86_64StorageManager<'a, '_, X86_64SystemV>, + sym: Symbol, + ) { + type ASM = X86_64Assembler; + + let (offset, _) = storage_manager.stack_offset_and_size(&sym); + + match Self::GENERAL_PARAM_REGS.get(self.general_i) { + Some(reg) => { + ASM::mov_reg64_base32(buf, *reg, offset); + + self.general_i += 1; + } + None => { + // Copy to stack using return reg as buffer. + let reg = X86_64GeneralReg::RAX; + + ASM::mov_reg64_base32(buf, reg, offset); + ASM::mov_stack32_reg64(buf, self.tmp_stack_offset, reg); + + self.tmp_stack_offset += 8; + } + } + } + + fn store_arg_128bit<'a>( + &mut self, + buf: &mut Vec<'a, u8>, + storage_manager: &mut X86_64StorageManager<'a, '_, X86_64SystemV>, + sym: Symbol, + ) { + type ASM = X86_64Assembler; + + let (offset, _) = storage_manager.stack_offset_and_size(&sym); + + if self.general_i + 1 < Self::GENERAL_PARAM_REGS.len() { + let reg1 = Self::GENERAL_PARAM_REGS[self.general_i]; + let reg2 = Self::GENERAL_PARAM_REGS[self.general_i + 1]; + + ASM::mov_reg64_base32(buf, reg1, offset); + ASM::mov_reg64_base32(buf, reg2, offset + 8); + + self.general_i += 2; + } else { + // Copy to stack using return reg as buffer. + let reg = X86_64GeneralReg::RAX; + + ASM::mov_reg64_base32(buf, reg, offset); + ASM::mov_stack32_reg64(buf, self.tmp_stack_offset, reg); + + ASM::mov_reg64_base32(buf, reg, offset + 8); + ASM::mov_stack32_reg64(buf, self.tmp_stack_offset + 8, reg); + + self.tmp_stack_offset += 16; + } + } +} + +struct X64_64WindowsFastCallStoreArgs { + general_i: usize, + float_i: usize, + tmp_stack_offset: i32, +} + +impl X64_64WindowsFastCallStoreArgs { + const GENERAL_PARAM_REGS: &'static [X86_64GeneralReg] = + X86_64WindowsFastcall::GENERAL_PARAM_REGS; + const GENERAL_RETURN_REGS: &'static [X86_64GeneralReg] = + X86_64WindowsFastcall::GENERAL_RETURN_REGS; + + const FLOAT_PARAM_REGS: &'static [X86_64FloatReg] = X86_64WindowsFastcall::FLOAT_PARAM_REGS; + const FLOAT_RETURN_REGS: &'static [X86_64FloatReg] = X86_64WindowsFastcall::FLOAT_RETURN_REGS; + + fn store_arg<'a>( + &mut self, + buf: &mut Vec<'a, u8>, + storage_manager: &mut X86_64StorageManager<'a, '_, X86_64WindowsFastcall>, + layout_interner: &mut STLayoutInterner<'a>, + sym: Symbol, + in_layout: InLayout<'a>, + ) { + type ASM = X86_64Assembler; + + // we use the return register as a temporary register; it will be overwritten anyway + let tmp_reg = Self::GENERAL_RETURN_REGS[0]; + + match layout_interner.get_repr(in_layout) { + single_register_integers!() => self.store_arg_general(buf, storage_manager, sym), + pointer_layouts!() => self.store_arg_general(buf, storage_manager, sym), + single_register_floats!() => self.store_arg_float(buf, storage_manager, sym), + LayoutRepr::I128 | LayoutRepr::U128 => { + let (offset, _) = storage_manager.stack_offset_and_size(&sym); + + if self.general_i + 1 < Self::GENERAL_PARAM_REGS.len() { + let reg1 = Self::GENERAL_PARAM_REGS[self.general_i]; + let reg2 = Self::GENERAL_PARAM_REGS[self.general_i + 1]; + + ASM::mov_reg64_base32(buf, reg1, offset); + ASM::mov_reg64_base32(buf, reg2, offset + 8); + + self.general_i += 2; + } else { + // Copy to stack using return reg as buffer. + let reg = Self::GENERAL_RETURN_REGS[0]; + + ASM::mov_reg64_base32(buf, reg, offset); + ASM::mov_stack32_reg64(buf, self.tmp_stack_offset, reg); + + ASM::mov_reg64_base32(buf, reg, offset + 8); + ASM::mov_stack32_reg64(buf, self.tmp_stack_offset + 8, reg); + + self.tmp_stack_offset += 16; + } + } + _ if layout_interner.stack_size(in_layout) == 0 => {} + _ if layout_interner.stack_size(in_layout) > 16 => { + // Reference: https://learn.microsoft.com/en-us/cpp/build/x64-calling-convention?view=msvc-170#parameter-passing + match Self::GENERAL_PARAM_REGS.get(self.general_i) { + Some(reg) => { + // if there is a general purpose register available, use it to store a pointer to the value + let (base_offset, _size) = storage_manager.stack_offset_and_size(&sym); + + ASM::add_reg64_reg64_imm32(buf, *reg, X86_64GeneralReg::RBP, base_offset); + + self.general_i += 1; + } + None => { + // else, pass the value implicitly by copying to the stack (of the new frame) + let stack_offset = self.tmp_stack_offset; + + let size = copy_symbol_to_stack_offset( + buf, + storage_manager, + sym, + tmp_reg, + stack_offset, + ); + + self.tmp_stack_offset += size as i32; + } + } + } + LayoutRepr::LambdaSet(lambda_set) => self.store_arg( + buf, + storage_manager, + layout_interner, + sym, + lambda_set.runtime_representation(), + ), + LayoutRepr::Struct { .. } => { + // for now, just also store this on the stack + let stack_offset = self.tmp_stack_offset; + + let size = + copy_symbol_to_stack_offset(buf, storage_manager, sym, tmp_reg, stack_offset); + + self.tmp_stack_offset += size as i32; + } + LayoutRepr::Union(UnionLayout::NonRecursive(_)) => { + let stack_offset = self.tmp_stack_offset; + + let size = + copy_symbol_to_stack_offset(buf, storage_manager, sym, tmp_reg, stack_offset); + + self.tmp_stack_offset += size as i32; + } + _ => { + todo!( + "calling with arg type, {:?}", + layout_interner.dbg(in_layout) + ); + } + } + } + + fn store_arg_general<'a>( + &mut self, + buf: &mut Vec<'a, u8>, + storage_manager: &mut X86_64StorageManager<'a, '_, X86_64WindowsFastcall>, + sym: Symbol, + ) { + match Self::GENERAL_PARAM_REGS.get(self.general_i) { + Some(reg) => { + storage_manager.load_to_specified_general_reg(buf, &sym, *reg); + self.general_i += 1; + } + None => { + // Copy to stack using return reg as buffer. + let tmp = Self::GENERAL_RETURN_REGS[0]; + + storage_manager.load_to_specified_general_reg(buf, &sym, tmp); + X86_64Assembler::mov_stack32_reg64(buf, self.tmp_stack_offset, tmp); + + self.tmp_stack_offset += 8; + } + } + } + + fn store_arg_float<'a>( + &mut self, + buf: &mut Vec<'a, u8>, + storage_manager: &mut X86_64StorageManager<'a, '_, X86_64WindowsFastcall>, + sym: Symbol, + ) { + match Self::FLOAT_PARAM_REGS.get(self.float_i) { + Some(reg) => { + storage_manager.load_to_specified_float_reg(buf, &sym, *reg); + self.float_i += 1; + } + None => { + // Copy to stack using return reg as buffer. + let tmp = Self::FLOAT_RETURN_REGS[0]; + + storage_manager.load_to_specified_float_reg(buf, &sym, tmp); + X86_64Assembler::mov_stack32_freg64(buf, self.tmp_stack_offset, tmp); + + self.tmp_stack_offset += 8; + } + } + } +} + +type X86_64StorageManager<'a, 'r, CallConv> = + StorageManager<'a, 'r, X86_64GeneralReg, X86_64FloatReg, X86_64Assembler, CallConv>; + +struct X64_64SystemVLoadArgs { + general_i: usize, + float_i: usize, + argument_offset: i32, +} + +impl X64_64SystemVLoadArgs { + fn load_arg<'a>( + &mut self, + buf: &mut Vec, + storage_manager: &mut X86_64StorageManager<'a, '_, X86_64SystemV>, + layout_interner: &mut STLayoutInterner<'a>, + sym: Symbol, + in_layout: InLayout<'a>, + ) { + let stack_size = layout_interner.stack_size(in_layout); + match layout_interner.get_repr(in_layout) { + single_register_integers!() => self.load_arg_general(storage_manager, sym), + pointer_layouts!() => self.load_arg_general(storage_manager, sym), + single_register_floats!() => self.load_arg_float(storage_manager, sym), + _ if stack_size == 0 => { + storage_manager.no_data(&sym); + } + _ if stack_size > 16 => { + // TODO: Double check this. + storage_manager.complex_stack_arg(&sym, self.argument_offset, stack_size); + self.argument_offset += stack_size as i32; + } + LayoutRepr::LambdaSet(lambda_set) => self.load_arg( + buf, + storage_manager, + layout_interner, + sym, + lambda_set.runtime_representation(), + ), + LayoutRepr::Struct { .. } => { + if stack_size <= 8 { + self.load_arg_general_64bit( + buf, + storage_manager, + layout_interner, + sym, + in_layout, + ); + } else if stack_size <= 16 { + self.load_arg_general_128bit( + buf, + storage_manager, + layout_interner, + sym, + in_layout, + ); + } else { + unreachable!("covered by an earlier branch") + } + } + LayoutRepr::Builtin(Builtin::Int(IntWidth::U128 | IntWidth::I128)) => { + self.load_arg_general_128bit(buf, storage_manager, layout_interner, sym, in_layout); + } + LayoutRepr::Builtin(Builtin::Decimal) => { + self.load_arg_general_128bit(buf, storage_manager, layout_interner, sym, in_layout); + } + LayoutRepr::Union(UnionLayout::NonRecursive(_)) => { + // for now, just also store this on the stack + storage_manager.complex_stack_arg(&sym, self.argument_offset, stack_size); + self.argument_offset += stack_size as i32; + } + _ => { + todo!( + "Loading args with layout {:?}", + layout_interner.dbg(in_layout) + ); + } + } + } + + fn load_arg_general( + &mut self, + storage_manager: &mut X86_64StorageManager<'_, '_, X86_64SystemV>, + sym: Symbol, + ) { + if let Some(reg) = X86_64SystemV::GENERAL_PARAM_REGS.get(self.general_i) { + storage_manager.general_reg_arg(&sym, *reg); + self.general_i += 1; + } else { + storage_manager.primitive_stack_arg(&sym, self.argument_offset); + self.argument_offset += 8; + } + } + + fn load_arg_general_64bit( + &mut self, + buf: &mut Vec, + storage_manager: &mut X86_64StorageManager<'_, '_, X86_64SystemV>, + layout_interner: &mut STLayoutInterner<'_>, + sym: Symbol, + in_layout: InLayout<'_>, + ) { + type ASM = X86_64Assembler; + + let reg1 = X86_64SystemV::GENERAL_PARAM_REGS.get(self.general_i); + + match reg1 { + Some(reg1) => { + let offset = + storage_manager.claim_stack_area_layout(layout_interner, sym, in_layout); + + ASM::mov_base32_reg64(buf, offset, *reg1); + + self.general_i += 1; + } + None => { + storage_manager.complex_stack_arg(&sym, self.argument_offset, 8); + self.argument_offset += 8; + } + } + } + + fn load_arg_general_128bit( + &mut self, + buf: &mut Vec, + storage_manager: &mut X86_64StorageManager<'_, '_, X86_64SystemV>, + layout_interner: &mut STLayoutInterner<'_>, + sym: Symbol, + in_layout: InLayout<'_>, + ) { + type ASM = X86_64Assembler; + + let reg1 = X86_64SystemV::GENERAL_PARAM_REGS.get(self.general_i); + let reg2 = X86_64SystemV::GENERAL_PARAM_REGS.get(self.general_i + 1); + + match (reg1, reg2) { + (Some(reg1), Some(reg2)) => { + let offset = + storage_manager.claim_stack_area_layout(layout_interner, sym, in_layout); + + ASM::mov_base32_reg64(buf, offset, *reg1); + ASM::mov_base32_reg64(buf, offset + 8, *reg2); + + self.general_i += 2; + } + _ => { + storage_manager.complex_stack_arg(&sym, self.argument_offset, 16); + self.argument_offset += 16; + } + } + } + + fn load_arg_float( + &mut self, + storage_manager: &mut X86_64StorageManager<'_, '_, X86_64SystemV>, + sym: Symbol, + ) { + if let Some(reg) = X86_64SystemV::FLOAT_PARAM_REGS.get(self.float_i) { + storage_manager.float_reg_arg(&sym, *reg); + self.float_i += 1; + } else { + storage_manager.primitive_stack_arg(&sym, self.argument_offset); + self.argument_offset += 8; + } + } +} + +struct X64_64WindowsFastCallLoadArgs { + general_i: usize, + float_i: usize, + argument_offset: i32, +} + +impl X64_64WindowsFastCallLoadArgs { + fn load_arg<'a>( + &mut self, + buf: &mut Vec<'a, u8>, + storage_manager: &mut X86_64StorageManager<'a, '_, X86_64WindowsFastcall>, + layout_interner: &mut STLayoutInterner<'a>, + sym: Symbol, + in_layout: InLayout<'a>, + ) { + type ASM = X86_64Assembler; + + let stack_size = layout_interner.stack_size(in_layout); + match layout_interner.get_repr(in_layout) { + single_register_integers!() => self.load_arg_general(storage_manager, sym), + pointer_layouts!() => self.load_arg_general(storage_manager, sym), + single_register_floats!() => self.load_arg_float(storage_manager, sym), + _ if stack_size == 0 => { + storage_manager.no_data(&sym); + } + _ if stack_size > 16 => { + // Reference: https://learn.microsoft.com/en-us/cpp/build/x64-calling-convention?view=msvc-170#parameter-passing + match X86_64WindowsFastcall::GENERAL_PARAM_REGS.get(self.general_i) { + Some(ptr_reg) => { + // if there is a general purpose register available, use it to store a pointer to the value + let base_offset = storage_manager.claim_stack_area_layout( + layout_interner, + sym, + in_layout, + ); + let tmp_reg = X86_64WindowsFastcall::GENERAL_RETURN_REGS[0]; + + copy_to_base_offset::<_, _, ASM>( + buf, + base_offset, + stack_size, + *ptr_reg, + tmp_reg, + 0, + ); + + self.general_i += 1; + } + None => { + // else, pass the value implicitly by copying to the stack (of the new frame) + storage_manager.complex_stack_arg(&sym, self.argument_offset, stack_size); + self.argument_offset += stack_size as i32; + } + } + } + LayoutRepr::LambdaSet(lambda_set) => self.load_arg( + buf, + storage_manager, + layout_interner, + sym, + lambda_set.runtime_representation(), + ), + LayoutRepr::Struct { .. } => { + // for now, just also store this on the stack + storage_manager.complex_stack_arg(&sym, self.argument_offset, stack_size); + self.argument_offset += stack_size as i32; + } + LayoutRepr::Builtin(Builtin::Int(IntWidth::U128 | IntWidth::I128)) => { + self.load_arg_general_128bit(buf, storage_manager, sym); + } + LayoutRepr::Builtin(Builtin::Decimal) => { + self.load_arg_general_128bit(buf, storage_manager, sym); + } + LayoutRepr::Union(UnionLayout::NonRecursive(_)) => { + // for now, just also store this on the stack + storage_manager.complex_stack_arg(&sym, self.argument_offset, stack_size); + self.argument_offset += stack_size as i32; + } + _ => { + todo!( + "Loading args with layout {:?}", + layout_interner.dbg(in_layout) + ); + } + } + } + + fn load_arg_general( + &mut self, + storage_manager: &mut X86_64StorageManager<'_, '_, X86_64WindowsFastcall>, + sym: Symbol, + ) { + if let Some(reg) = X86_64WindowsFastcall::GENERAL_PARAM_REGS.get(self.general_i) { + storage_manager.general_reg_arg(&sym, *reg); + self.general_i += 1; + } else { + storage_manager.primitive_stack_arg(&sym, self.argument_offset); + self.argument_offset += 8; + } + } + + fn load_arg_general_128bit( + &mut self, + buf: &mut Vec, + storage_manager: &mut X86_64StorageManager<'_, '_, X86_64WindowsFastcall>, + sym: Symbol, + ) { + type ASM = X86_64Assembler; + + let reg1 = X86_64WindowsFastcall::GENERAL_PARAM_REGS.get(self.general_i); + let reg2 = X86_64WindowsFastcall::GENERAL_PARAM_REGS.get(self.general_i + 1); + + match (reg1, reg2) { + (Some(reg1), Some(reg2)) => { + let offset = storage_manager.claim_stack_area_with_alignment(sym, 16, 16); + + ASM::mov_base32_reg64(buf, offset, *reg1); + ASM::mov_base32_reg64(buf, offset + 8, *reg2); + + self.general_i += 2; + } + _ => { + storage_manager.complex_stack_arg(&sym, self.argument_offset, 16); + self.argument_offset += 16; + } + } + } + + fn load_arg_float( + &mut self, + storage_manager: &mut X86_64StorageManager<'_, '_, X86_64WindowsFastcall>, + sym: Symbol, + ) { + if let Some(reg) = X86_64WindowsFastcall::FLOAT_PARAM_REGS.get(self.float_i) { + storage_manager.float_reg_arg(&sym, *reg); + self.float_i += 1; + } else { + storage_manager.primitive_stack_arg(&sym, self.argument_offset); + self.argument_offset += 8; + } + } +} + +impl X86_64SystemV { + fn returns_via_arg_pointer<'a>( + interner: &STLayoutInterner<'a>, + ret_layout: &InLayout<'a>, + ) -> bool { + // TODO: This will need to be more complex/extended to fully support the calling convention. + // details here: https://github.com/hjl-tools/x86-psABI/wiki/x86-64-psABI-1.0.pdf + interner.stack_size(*ret_layout) > 16 + } +} + +impl CallConv for X86_64WindowsFastcall { + const BASE_PTR_REG: X86_64GeneralReg = X86_64GeneralReg::RBP; + const STACK_PTR_REG: X86_64GeneralReg = X86_64GeneralReg::RSP; + + const GENERAL_PARAM_REGS: &'static [X86_64GeneralReg] = &[ + X86_64GeneralReg::RCX, + X86_64GeneralReg::RDX, + X86_64GeneralReg::R8, + X86_64GeneralReg::R9, + ]; + const GENERAL_RETURN_REGS: &'static [X86_64GeneralReg] = &[X86_64GeneralReg::RAX]; + const GENERAL_DEFAULT_FREE_REGS: &'static [X86_64GeneralReg] = &[ + // The regs we want to use first should be at the end of this vec. + // We will use pop to get which reg to use next + + // Don't use stack pointer: X86_64GeneralReg::RSP, + // Don't use frame pointer: X86_64GeneralReg::RBP, + + // Use callee saved regs last. + X86_64GeneralReg::RBX, + X86_64GeneralReg::RSI, + X86_64GeneralReg::RDI, + X86_64GeneralReg::R12, + X86_64GeneralReg::R13, + X86_64GeneralReg::R14, + X86_64GeneralReg::R15, + // Use caller saved regs first. + X86_64GeneralReg::RAX, + X86_64GeneralReg::RCX, + X86_64GeneralReg::RDX, + X86_64GeneralReg::R8, + X86_64GeneralReg::R9, + X86_64GeneralReg::R10, + X86_64GeneralReg::R11, + ]; + const FLOAT_PARAM_REGS: &'static [X86_64FloatReg] = &[ + X86_64FloatReg::XMM0, + X86_64FloatReg::XMM1, + X86_64FloatReg::XMM2, + X86_64FloatReg::XMM3, + ]; + const FLOAT_RETURN_REGS: &'static [X86_64FloatReg] = &[X86_64FloatReg::XMM0]; + const FLOAT_DEFAULT_FREE_REGS: &'static [X86_64FloatReg] = &[ + // The regs we want to use first should be at the end of this vec. + // We will use pop to get which reg to use next + // Use callee saved regs last. + X86_64FloatReg::XMM15, + X86_64FloatReg::XMM15, + X86_64FloatReg::XMM13, + X86_64FloatReg::XMM12, + X86_64FloatReg::XMM11, + X86_64FloatReg::XMM10, + X86_64FloatReg::XMM9, + X86_64FloatReg::XMM8, + X86_64FloatReg::XMM7, + X86_64FloatReg::XMM6, + // Use caller saved regs first. + X86_64FloatReg::XMM5, + X86_64FloatReg::XMM4, + X86_64FloatReg::XMM3, + X86_64FloatReg::XMM2, + X86_64FloatReg::XMM1, + X86_64FloatReg::XMM0, + ]; + const SHADOW_SPACE_SIZE: u8 = 32; + + // These are registers that a called function must save and restore if it wants to use them. + // + // Refer https://learn.microsoft.com/en-us/cpp/build/x64-calling-convention?view=msvc-170#callercallee-saved-registers + // > The x64 ABI considers registers RBX, RBP, RDI, RSI, RSP, R12, R13, R14, R15, and XMM6-XMM15 nonvolatile. + // > They must be saved and restored by a function that uses them. + #[inline(always)] + fn general_callee_saved(reg: &X86_64GeneralReg) -> bool { + matches!( + reg, + X86_64GeneralReg::RBX + | X86_64GeneralReg::RBP + | X86_64GeneralReg::RSI + | X86_64GeneralReg::RSP + | X86_64GeneralReg::RDI + | X86_64GeneralReg::R12 + | X86_64GeneralReg::R13 + | X86_64GeneralReg::R14 + | X86_64GeneralReg::R15 + ) + } + + // Refer https://learn.microsoft.com/en-us/cpp/build/x64-calling-convention?view=msvc-170#callercallee-saved-registers + // > The x64 ABI considers registers RBX, RBP, RDI, RSI, RSP, R12, R13, R14, R15, and XMM6-XMM15 nonvolatile. + // > They must be saved and restored by a function that uses them. + #[inline(always)] + fn float_callee_saved(reg: &X86_64FloatReg) -> bool { + matches!( + reg, + X86_64FloatReg::XMM6 + | X86_64FloatReg::XMM7 + | X86_64FloatReg::XMM8 + | X86_64FloatReg::XMM9 + | X86_64FloatReg::XMM10 + | X86_64FloatReg::XMM11 + | X86_64FloatReg::XMM12 + | X86_64FloatReg::XMM13 + | X86_64FloatReg::XMM14 + | X86_64FloatReg::XMM15 + ) + } + + #[inline(always)] + fn setup_stack( + buf: &mut Vec<'_, u8>, + saved_general_regs: &[X86_64GeneralReg], + saved_float_regs: &[X86_64FloatReg], + requested_stack_size: i32, + fn_call_stack_size: i32, + ) -> i32 { + x86_64_generic_setup_stack( + buf, + saved_general_regs, + saved_float_regs, + requested_stack_size, + fn_call_stack_size, + ) + } + + #[inline(always)] + fn cleanup_stack( + buf: &mut Vec<'_, u8>, + saved_general_regs: &[X86_64GeneralReg], + saved_float_regs: &[X86_64FloatReg], + aligned_stack_size: i32, + fn_call_stack_size: i32, + ) { + x86_64_generic_cleanup_stack( + buf, + saved_general_regs, + saved_float_regs, + aligned_stack_size, + fn_call_stack_size, + ) + } + + #[inline(always)] + fn load_args<'a>( + buf: &mut Vec<'a, u8>, + storage_manager: &mut X86_64StorageManager<'a, '_, X86_64WindowsFastcall>, + layout_interner: &mut STLayoutInterner<'a>, + args: &'a [(InLayout<'a>, Symbol)], + ret_layout: &InLayout<'a>, + ) { + let returns_via_pointer = + X86_64WindowsFastcall::returns_via_arg_pointer(layout_interner, ret_layout); + + let mut state = X64_64WindowsFastCallLoadArgs { + general_i: usize::from(returns_via_pointer), + float_i: 0, + // 16 is the size of the pushed return address and base pointer. + argument_offset: X86_64WindowsFastcall::SHADOW_SPACE_SIZE as i32 + 16, + }; + + if returns_via_pointer { + storage_manager.ret_pointer_arg(X86_64WindowsFastcall::GENERAL_PARAM_REGS[0]); + } + + for (in_layout, sym) in args.iter() { + state.load_arg(buf, storage_manager, layout_interner, *sym, *in_layout); + } + } + + #[inline(always)] + fn store_args<'a>( + buf: &mut Vec<'a, u8>, + storage_manager: &mut StorageManager< + 'a, + '_, + X86_64GeneralReg, + X86_64FloatReg, + X86_64Assembler, + X86_64WindowsFastcall, + >, + layout_interner: &mut STLayoutInterner<'a>, + dst: &Symbol, + args: &[Symbol], + arg_layouts: &[InLayout<'a>], + ret_layout: &InLayout<'a>, + ) { + let mut general_i = 0; + + if Self::returns_via_arg_pointer(layout_interner, ret_layout) { + // Save space on the stack for the result we will be return. + let base_offset = + storage_manager.claim_stack_area_layout(layout_interner, *dst, *ret_layout); + + // Set the first reg to the address base + offset. + let ret_reg = Self::GENERAL_PARAM_REGS[general_i]; + general_i += 1; + X86_64Assembler::add_reg64_reg64_imm32( + buf, + ret_reg, + X86_64GeneralReg::RBP, + base_offset, + ); + } + + let mut state = X64_64WindowsFastCallStoreArgs { + general_i, + float_i: 0, + tmp_stack_offset: Self::SHADOW_SPACE_SIZE as i32, + }; + + for (sym, in_layout) in args.iter().zip(arg_layouts.iter()) { + state.store_arg(buf, storage_manager, layout_interner, *sym, *in_layout); + } + + storage_manager.update_fn_call_stack_size(state.tmp_stack_offset as u32); + } + + fn return_complex_symbol<'a>( + buf: &mut Vec<'a, u8>, + storage_manager: &mut StorageManager< + 'a, + '_, + X86_64GeneralReg, + X86_64FloatReg, + X86_64Assembler, + X86_64WindowsFastcall, + >, + layout_interner: &mut STLayoutInterner<'a>, + sym: &Symbol, + layout: &InLayout<'a>, + ) { + match layout_interner.get_repr(*layout) { + single_register_layouts!() => { + internal_error!("single register layouts are not complex symbols"); + } + // For windows (and zig 0.9 changes in zig 0.10) we need to match what zig does, + // in this case uses RAX & RDX to return the value + LayoutRepr::I128 | LayoutRepr::U128 => { + let (base_offset, size) = storage_manager.stack_offset_and_size(sym); + debug_assert_eq!(base_offset % 8, 0); + debug_assert_eq!(size, 16); + + X86_64Assembler::mov_reg64_base32(buf, X86_64GeneralReg::RAX, base_offset); + X86_64Assembler::mov_reg64_base32(buf, X86_64GeneralReg::RDX, base_offset + 0x08); + } + _ if layout_interner.stack_size(*layout) == 0 => {} + _ if !Self::returns_via_arg_pointer(layout_interner, layout) => { + let (base_offset, size) = storage_manager.stack_offset_and_size(sym); + debug_assert_eq!(base_offset % 8, 0); + if size <= 8 { + X86_64Assembler::mov_reg64_base32( + buf, + Self::GENERAL_RETURN_REGS[0], + base_offset, + ); + } else { + internal_error!( + "types that don't return via arg pointer must be less than 8 bytes" + ); + } + } + _ => { + // This is a large type returned via the arg pointer. + storage_manager.copy_symbol_to_arg_pointer(buf, sym, layout); + // Also set the return reg to the arg pointer. + storage_manager.load_to_specified_general_reg( + buf, + &Symbol::RET_POINTER, + Self::GENERAL_RETURN_REGS[0], + ); + } + } + } + + fn load_returned_complex_symbol<'a>( + buf: &mut Vec<'a, u8>, + storage_manager: &mut StorageManager< + 'a, + '_, + X86_64GeneralReg, + X86_64FloatReg, + X86_64Assembler, + X86_64WindowsFastcall, + >, + layout_interner: &mut STLayoutInterner<'a>, + sym: &Symbol, + layout: &InLayout<'a>, + ) { + match layout_interner.get_repr(*layout) { + single_register_layouts!() => { + internal_error!("single register layouts are not complex symbols"); + } + // For windows (and zig 0.9 changes in zig 0.10) we need to match what zig does, + // in this case uses RAX & RDX to return the value + LayoutRepr::I128 | LayoutRepr::U128 => { + let offset = + storage_manager.claim_stack_area_layout(layout_interner, *sym, *layout); + X86_64Assembler::mov_base32_reg64(buf, offset, X86_64GeneralReg::RAX); + X86_64Assembler::mov_base32_reg64(buf, offset + 0x08, X86_64GeneralReg::RDX); + } + _ if layout_interner.stack_size(*layout) == 0 => { + storage_manager.no_data(sym); + } + _ if !Self::returns_via_arg_pointer(layout_interner, layout) => { + let size = layout_interner.stack_size(*layout); + let offset = + storage_manager.claim_stack_area_layout(layout_interner, *sym, *layout); + if size <= 8 { + X86_64Assembler::mov_base32_reg64(buf, offset, Self::GENERAL_RETURN_REGS[0]); + } else { + internal_error!( + "types that don't return via arg pointer must be less than 8 bytes" + ); + } + } + _ => { + // This should have been received via an arg pointer. + // That means the value is already loaded onto the stack area we allocated before the call. + // Nothing to do. + } + } + } + + fn setjmp(buf: &mut Vec<'_, u8>) { + use X86_64GeneralReg::*; + type ASM = X86_64Assembler; + + // input: + // + // rcx: pointer to the jmp_buf + // rdx: stack pointer + + // mingw_getsp: + // lea rax [ rsp + 8 ] + // ret + // + // _setjmp: + // mov [rcx + 0x00] rdx + // mov [rcx + 0x08] rbx + // mov [rcx + 0x18] rbp # note 0x10 is not used yet! + // mov [rcx + 0x20] rsi + // mov [rcx + 0x28] rdi + // mov [rcx + 0x30] r12 + // mov [rcx + 0x38] r13 + // mov [rcx + 0x40] r14 + // mov [rcx + 0x48] r15 + // lea r8 [rsp + 0x08] + // mov [rcx + 0x10] r8 + // mov r8 [rsp] + // mov [rcx + 0x50] r8 + // + // stmxcsr [rcx + 0x58] + // fnstcw word ptr [rcx + 0x5C] + // + // mobdxq xmmword ptr [rcx + 0x60], xmm6 + // mobdxq xmmword ptr [rcx + 0x70], xmm7 + // mobdxq xmmword ptr [rcx + 0x80], xmm8 + // mobdxq xmmword ptr [rcx + 0x90], xmm9 + // mobdxq xmmword ptr [rcx + 0xa0], xmm10 + // mobdxq xmmword ptr [rcx + 0xb0], xmm11 + // mobdxq xmmword ptr [rcx + 0xc0], xmm12 + // mobdxq xmmword ptr [rcx + 0xd0], xmm13 + // mobdxq xmmword ptr [rcx + 0xe0], xmm14 + // mobdxq xmmword ptr [rcx + 0xf0], xmm15 + // + // xor eax, eax + // ret + + let result_pointer = RCX; + let env = RDX; + debug_assert_eq!(env, Self::GENERAL_PARAM_REGS[1]); + + ASM::mov_mem64_offset32_reg64(buf, env, 0x00, RDX); + ASM::mov_mem64_offset32_reg64(buf, env, 0x08, RBX); + // NOTE: 0x10 is unused here! + ASM::mov_mem64_offset32_reg64(buf, env, 0x18, RBP); + ASM::mov_mem64_offset32_reg64(buf, env, 0x20, RSI); + ASM::mov_mem64_offset32_reg64(buf, env, 0x28, RDI); + ASM::mov_mem64_offset32_reg64(buf, env, 0x30, R12); + ASM::mov_mem64_offset32_reg64(buf, env, 0x38, R13); + ASM::mov_mem64_offset32_reg64(buf, env, 0x40, R14); + ASM::mov_mem64_offset32_reg64(buf, env, 0x48, R15); + + // go one value up (as if setjmp wasn't called) + lea_reg64_offset8(buf, R8, RSP, 0x8); + ASM::mov_mem64_offset32_reg64(buf, env, 0x10, R8); + + // store the current stack pointer + ASM::mov_reg64_mem64_offset32(buf, R8, RSP, 0); + ASM::mov_mem64_offset32_reg64(buf, env, 0x50, R8); + + // zero out the fields of the result pointer + ASM::mov_reg64_imm64(buf, R8, 0x00); + ASM::mov_mem64_offset32_reg64(buf, result_pointer, 0x00, R8); + ASM::mov_mem64_offset32_reg64(buf, result_pointer, 0x08, R8); + + // now the windows implementation goes on to store xmm registers and sse2 stuff. + // we skip that for now + + // store the result pointer into the env so that longjmp can retrieve it + ASM::mov_mem64_offset32_reg64(buf, env, 0x58, result_pointer); + + ASM::ret(buf) + } + + fn longjmp(_buf: &mut Vec<'_, u8>) { + // do nothing, longjmp is part of roc_panic + } + + fn roc_panic(buf: &mut Vec<'_, u8>, relocs: &mut Vec<'_, Relocation>) { + use X86_64GeneralReg::*; + type ASM = X86_64Assembler; + + // a *const RocStr + let roc_str_ptr = RCX; + debug_assert_eq!(roc_str_ptr, Self::GENERAL_PARAM_REGS[0]); + + // a 32-bit integer + let panic_tag = RDX; + debug_assert_eq!(panic_tag, Self::GENERAL_PARAM_REGS[1]); + + // move the crash tag into a temporary register. We add 1 to it because the 0 value + // is already used for "no crash occurred" + ASM::add_reg64_reg64_imm32(buf, R10, panic_tag, 0x01); + + // the setlongjmp_buffer + let env = R8; + ASM::data_pointer(buf, relocs, String::from("setlongjmp_buffer"), env); + + // move the roc_str bytes into the setlongjmp_buffer + for offset in [0, 8, 16] { + ASM::mov_reg64_mem64_offset32(buf, R9, roc_str_ptr, offset); + ASM::mov_mem64_offset32_reg64(buf, env, 0x60 + offset, R9); + } + + // now, time to move all the registers back to how they were + ASM::mov_reg64_mem64_offset32(buf, RDX, env, 0x00); + ASM::mov_reg64_mem64_offset32(buf, RBX, env, 0x08); + // again 0x10 is skipped here + ASM::mov_reg64_mem64_offset32(buf, RBP, env, 0x18); + ASM::mov_reg64_mem64_offset32(buf, RSI, env, 0x20); + ASM::mov_reg64_mem64_offset32(buf, RDI, env, 0x28); + ASM::mov_reg64_mem64_offset32(buf, R12, env, 0x30); + ASM::mov_reg64_mem64_offset32(buf, R13, env, 0x38); + ASM::mov_reg64_mem64_offset32(buf, R14, env, 0x40); + ASM::mov_reg64_mem64_offset32(buf, R15, env, 0x48); + + // value of rsp before setjmp call + ASM::mov_reg64_mem64_offset32(buf, RSP, env, 0x10); + + // set up the return values. The windows fastcall calling convention has only one return + // register, and we need to return two values, so we use some space in the setlongjmp_buffer + let result_pointer = R9; + ASM::mov_reg64_mem64_offset32(buf, result_pointer, env, 0x58); + + // a pointer to the error message + ASM::add_reg64_reg64_imm32(buf, R11, env, 0x60); + + // write a pointer to the error message into result_pointer + ASM::mov_mem64_offset32_reg64(buf, result_pointer, 0x00, R11); + + // write the panic tag (now in R10) into the result_pointer + ASM::mov_mem64_offset32_reg64(buf, result_pointer, 0x08, R10); + + jmp_reg64_offset8(buf, env, 0x50) + } +} + +impl X86_64WindowsFastcall { + fn returns_via_arg_pointer<'a>( + interner: &STLayoutInterner<'a>, + ret_layout: &InLayout<'a>, + ) -> bool { + // TODO: This is not fully correct there are some exceptions for "vector" types. + // details here: https://docs.microsoft.com/en-us/cpp/build/x64-calling-convention?view=msvc-160#return-values + match *ret_layout { + Layout::I128 | Layout::U128 => false, + _ => interner.stack_size(*ret_layout) > 8, + } + } +} + +#[inline(always)] +fn x86_64_generic_setup_stack( + buf: &mut Vec<'_, u8>, + saved_general_regs: &[X86_64GeneralReg], + saved_float_regs: &[X86_64FloatReg], + requested_stack_size: i32, + fn_call_stack_size: i32, +) -> i32 { + X86_64Assembler::push_reg64(buf, X86_64GeneralReg::RBP); + X86_64Assembler::mov_reg64_reg64(buf, X86_64GeneralReg::RBP, X86_64GeneralReg::RSP); + + let full_stack_size = match requested_stack_size + .checked_add(8 * (saved_general_regs.len() + saved_float_regs.len()) as i32) + .and_then(|size| size.checked_add(fn_call_stack_size)) + { + Some(size) => size, + _ => internal_error!("Ran out of stack space"), + }; + let alignment = if full_stack_size <= 0 { + 0 + } else { + full_stack_size % STACK_ALIGNMENT as i32 + }; + let offset = if alignment == 0 { + 0 + } else { + STACK_ALIGNMENT - alignment as u8 + }; + if let Some(aligned_stack_size) = full_stack_size.checked_add(offset as i32) { + if aligned_stack_size > 0 { + X86_64Assembler::sub_reg64_reg64_imm32( + buf, + X86_64GeneralReg::RSP, + X86_64GeneralReg::RSP, + aligned_stack_size, + ); + + // Put values at the top of the stack to avoid conflicts with previously saved variables. + let mut offset = aligned_stack_size - fn_call_stack_size; + for reg in saved_general_regs { + X86_64Assembler::mov_base32_reg64(buf, -offset, *reg); + offset -= 8; + } + for reg in saved_float_regs { + X86_64Assembler::mov_base32_freg64(buf, -offset, *reg); + offset -= 8; + } + aligned_stack_size + } else { + 0 + } + } else { + internal_error!("Ran out of stack space"); + } +} + +#[inline(always)] +#[allow(clippy::unnecessary_wraps)] +fn x86_64_generic_cleanup_stack( + buf: &mut Vec<'_, u8>, + saved_general_regs: &[X86_64GeneralReg], + saved_float_regs: &[X86_64FloatReg], + aligned_stack_size: i32, + fn_call_stack_size: i32, +) { + if aligned_stack_size > 0 { + let mut offset = aligned_stack_size - fn_call_stack_size; + for reg in saved_general_regs { + X86_64Assembler::mov_reg64_base32(buf, *reg, -offset); + offset -= 8; + } + for reg in saved_float_regs { + X86_64Assembler::mov_freg64_base32(buf, *reg, -offset); + offset -= 8; + } + X86_64Assembler::add_reg64_reg64_imm32( + buf, + X86_64GeneralReg::RSP, + X86_64GeneralReg::RSP, + aligned_stack_size, + ); + } + //X86_64Assembler::mov_reg64_reg64(buf, X86_64GeneralReg::RSP, X86_64GeneralReg::RBP); + X86_64Assembler::pop_reg64(buf, X86_64GeneralReg::RBP); +} + +type Reg64 = X86_64GeneralReg; + +fn binop_move_src_to_dst_reg64(buf: &mut Vec<'_, u8>, f: F, dst: Reg64, src1: Reg64, src2: Reg64) +where + F: FnOnce(&mut Vec<'_, u8>, X86_64GeneralReg, X86_64GeneralReg), +{ + if dst == src1 { + f(buf, dst, src2); + } else if dst == src2 { + f(buf, dst, src1); + } else { + mov_reg64_reg64(buf, dst, src1); + f(buf, dst, src2); + } +} + +impl Assembler for X86_64Assembler { + // These functions should map to the raw assembly functions below. + // In some cases, that means you can just directly call one of the direct assembly functions. + #[inline(always)] + fn abs_reg64_reg64(buf: &mut Vec<'_, u8>, dst: X86_64GeneralReg, src: X86_64GeneralReg) { + mov_reg64_reg64(buf, dst, src); + neg_reg64(buf, dst); + cmovl_reg64_reg64(buf, dst, src); + } + + #[inline(always)] + fn abs_freg64_freg64( + buf: &mut Vec<'_, u8>, + relocs: &mut Vec<'_, Relocation>, + dst: X86_64FloatReg, + src: X86_64FloatReg, + ) { + movsd_freg64_rip_offset32(buf, dst, 0); + + // TODO: make sure this constant only loads once instead of every call to abs + relocs.push(Relocation::LocalData { + offset: buf.len() as u64 - 4, + data: 0x7fffffffffffffffu64.to_le_bytes().to_vec(), + }); + + andpd_freg64_freg64(buf, dst, src); + } + + #[inline(always)] + fn add_reg64_reg64_imm32( + buf: &mut Vec<'_, u8>, + dst: X86_64GeneralReg, + src1: X86_64GeneralReg, + imm32: i32, + ) { + mov_reg64_reg64(buf, dst, src1); + add_reg64_imm32(buf, dst, imm32); + } + + #[inline(always)] + fn add_reg64_reg64_reg64(buf: &mut Vec<'_, u8>, dst: Reg64, src1: Reg64, src2: Reg64) { + binop_move_src_to_dst_reg64(buf, add_reg64_reg64, dst, src1, src2) + } + + #[inline(always)] + fn add_freg32_freg32_freg32( + buf: &mut Vec<'_, u8>, + dst: X86_64FloatReg, + src1: X86_64FloatReg, + src2: X86_64FloatReg, + ) { + if dst == src1 { + addss_freg32_freg32(buf, dst, src2); + } else if dst == src2 { + addss_freg32_freg32(buf, dst, src1); + } else { + movss_freg32_freg32(buf, dst, src1); + addss_freg32_freg32(buf, dst, src2); + } + } + #[inline(always)] + fn add_freg64_freg64_freg64( + buf: &mut Vec<'_, u8>, + dst: X86_64FloatReg, + src1: X86_64FloatReg, + src2: X86_64FloatReg, + ) { + if dst == src1 { + addsd_freg64_freg64(buf, dst, src2); + } else if dst == src2 { + addsd_freg64_freg64(buf, dst, src1); + } else { + movsd_freg64_freg64(buf, dst, src1); + addsd_freg64_freg64(buf, dst, src2); + } + } + + #[inline(always)] + fn call(buf: &mut Vec<'_, u8>, relocs: &mut Vec<'_, Relocation>, fn_name: String) { + buf.extend([0xE8, 0x00, 0x00, 0x00, 0x00]); + relocs.push(Relocation::LinkedFunction { + offset: buf.len() as u64 - 4, + name: fn_name, + }); + } + + #[inline(always)] + fn function_pointer( + buf: &mut Vec<'_, u8>, + relocs: &mut Vec<'_, Relocation>, + fn_name: String, + dst: X86_64GeneralReg, + ) { + lea_reg64(buf, dst); + + relocs.push(Relocation::LinkedFunction { + offset: buf.len() as u64 - 4, + name: fn_name, + }); + } + + #[inline(always)] + fn data_pointer( + buf: &mut Vec<'_, u8>, + relocs: &mut Vec<'_, Relocation>, + fn_name: String, + dst: X86_64GeneralReg, + ) { + lea_reg64(buf, dst); + + relocs.push(Relocation::LinkedData { + offset: buf.len() as u64 - 4, + name: fn_name, + }); + + // on X86_64, we actually get a pointer to a pointer + // so we just dereference to get just a pointer to the data + X86_64Assembler::mov_reg64_mem64_offset32(buf, dst, dst, 0); + } + + #[inline(always)] + fn imul_reg64_reg64_reg64( + buf: &mut Vec<'_, u8>, + dst: X86_64GeneralReg, + src1: X86_64GeneralReg, + src2: X86_64GeneralReg, + ) { + mov_reg64_reg64(buf, dst, src1); + imul_reg64_reg64(buf, dst, src2); + } + + fn umul_reg64_reg64_reg64<'a, ASM, CC>( + buf: &mut Vec<'a, u8>, + storage_manager: &mut StorageManager<'a, '_, X86_64GeneralReg, X86_64FloatReg, ASM, CC>, + dst: X86_64GeneralReg, + src1: X86_64GeneralReg, + src2: X86_64GeneralReg, + ) where + ASM: Assembler, + CC: CallConv, + { + use crate::generic64::RegStorage; + + storage_manager.ensure_reg_free(buf, RegStorage::General(X86_64GeneralReg::RAX)); + storage_manager.ensure_reg_free(buf, RegStorage::General(X86_64GeneralReg::RDX)); + + mov_reg64_reg64(buf, X86_64GeneralReg::RAX, src1); + mul_reg64_reg64(buf, src2); + mov_reg64_reg64(buf, dst, X86_64GeneralReg::RAX); + } + + fn mul_freg32_freg32_freg32( + buf: &mut Vec<'_, u8>, + dst: X86_64FloatReg, + src1: X86_64FloatReg, + src2: X86_64FloatReg, + ) { + if dst == src1 { + mulss_freg32_freg32(buf, dst, src2); + } else if dst == src2 { + mulss_freg32_freg32(buf, dst, src1); + } else { + movss_freg32_freg32(buf, dst, src1); + mulss_freg32_freg32(buf, dst, src2); + } + } + #[inline(always)] + fn mul_freg64_freg64_freg64( + buf: &mut Vec<'_, u8>, + dst: X86_64FloatReg, + src1: X86_64FloatReg, + src2: X86_64FloatReg, + ) { + if dst == src1 { + mulsd_freg64_freg64(buf, dst, src2); + } else if dst == src2 { + mulsd_freg64_freg64(buf, dst, src1); + } else { + movsd_freg64_freg64(buf, dst, src1); + mulsd_freg64_freg64(buf, dst, src2); + } + } + + fn div_freg32_freg32_freg32( + buf: &mut Vec<'_, u8>, + dst: X86_64FloatReg, + src1: X86_64FloatReg, + src2: X86_64FloatReg, + ) { + if dst == src1 { + divss_freg32_freg32(buf, dst, src2); + } else if dst == src2 { + divss_freg32_freg32(buf, dst, src1); + } else { + movsd_freg64_freg64(buf, dst, src1); + divss_freg32_freg32(buf, dst, src2); + } + } + + fn div_freg64_freg64_freg64( + buf: &mut Vec<'_, u8>, + dst: X86_64FloatReg, + src1: X86_64FloatReg, + src2: X86_64FloatReg, + ) { + if dst == src1 { + divsd_freg64_freg64(buf, dst, src2); + } else if dst == src2 { + divsd_freg64_freg64(buf, dst, src1); + } else { + movsd_freg64_freg64(buf, dst, src1); + divsd_freg64_freg64(buf, dst, src2); + } + } + + fn idiv_reg64_reg64_reg64<'a, ASM, CC>( + buf: &mut Vec<'a, u8>, + storage_manager: &mut StorageManager<'a, '_, X86_64GeneralReg, X86_64FloatReg, ASM, CC>, + dst: X86_64GeneralReg, + src1: X86_64GeneralReg, + src2: X86_64GeneralReg, + ) where + ASM: Assembler, + CC: CallConv, + { + use crate::generic64::RegStorage; + + storage_manager.ensure_reg_free(buf, RegStorage::General(X86_64GeneralReg::RAX)); + storage_manager.ensure_reg_free(buf, RegStorage::General(X86_64GeneralReg::RDX)); + + mov_reg64_reg64(buf, X86_64GeneralReg::RAX, src1); + idiv_reg64_reg64(buf, src2); + mov_reg64_reg64(buf, dst, X86_64GeneralReg::RAX); + } + + fn udiv_reg64_reg64_reg64<'a, ASM, CC>( + buf: &mut Vec<'a, u8>, + storage_manager: &mut StorageManager<'a, '_, X86_64GeneralReg, X86_64FloatReg, ASM, CC>, + dst: X86_64GeneralReg, + src1: X86_64GeneralReg, + src2: X86_64GeneralReg, + ) where + ASM: Assembler, + CC: CallConv, + { + use crate::generic64::RegStorage; + + storage_manager.ensure_reg_free(buf, RegStorage::General(X86_64GeneralReg::RAX)); + storage_manager.ensure_reg_free(buf, RegStorage::General(X86_64GeneralReg::RDX)); + + mov_reg64_reg64(buf, X86_64GeneralReg::RAX, src1); + udiv_reg64_reg64(buf, src2); + mov_reg64_reg64(buf, dst, X86_64GeneralReg::RAX); + } + + fn irem_reg64_reg64_reg64<'a, ASM, CC>( + buf: &mut Vec<'a, u8>, + storage_manager: &mut StorageManager<'a, '_, X86_64GeneralReg, X86_64FloatReg, ASM, CC>, + dst: X86_64GeneralReg, + src1: X86_64GeneralReg, + src2: X86_64GeneralReg, + ) where + ASM: Assembler, + CC: CallConv, + { + use crate::generic64::RegStorage; + + storage_manager.ensure_reg_free(buf, RegStorage::General(X86_64GeneralReg::RAX)); + storage_manager.ensure_reg_free(buf, RegStorage::General(X86_64GeneralReg::RDX)); + + mov_reg64_reg64(buf, X86_64GeneralReg::RAX, src1); + idiv_reg64_reg64(buf, src2); + mov_reg64_reg64(buf, dst, X86_64GeneralReg::RDX); + } + + fn urem_reg64_reg64_reg64<'a, ASM, CC>( + buf: &mut Vec<'a, u8>, + storage_manager: &mut StorageManager<'a, '_, X86_64GeneralReg, X86_64FloatReg, ASM, CC>, + dst: X86_64GeneralReg, + src1: X86_64GeneralReg, + src2: X86_64GeneralReg, + ) where + ASM: Assembler, + CC: CallConv, + { + use crate::generic64::RegStorage; + + storage_manager.ensure_reg_free(buf, RegStorage::General(X86_64GeneralReg::RAX)); + storage_manager.ensure_reg_free(buf, RegStorage::General(X86_64GeneralReg::RDX)); + + mov_reg64_reg64(buf, X86_64GeneralReg::RAX, src1); + udiv_reg64_reg64(buf, src2); + mov_reg64_reg64(buf, dst, X86_64GeneralReg::RDX); + } + + #[inline(always)] + fn jmp_imm32(buf: &mut Vec<'_, u8>, offset: i32) -> usize { + jmp_imm32(buf, offset); + + // on x86_64, jumps are calculated from the end of the jmp instruction + buf.len() + } + + #[inline(always)] + fn tail_call(buf: &mut Vec<'_, u8>) -> u64 { + Self::jmp_imm32(buf, 0); + buf.len() as u64 - 4 + } + + #[inline(always)] + fn jne_reg64_imm64_imm32<'a, ASM, CC>( + buf: &mut Vec<'a, u8>, + storage_manager: &mut StorageManager<'a, '_, X86_64GeneralReg, X86_64FloatReg, ASM, CC>, + reg: X86_64GeneralReg, + imm: u64, + offset: i32, + ) -> usize + where + ASM: Assembler, + CC: CallConv, + { + buf.reserve(13); + if imm > i32::MAX as u64 { + storage_manager.with_tmp_general_reg(buf, |_, buf, tmp| { + mov_reg64_imm64(buf, tmp, imm as _); + cmp_reg64_reg64(buf, RegisterWidth::W64, reg, tmp); + }) + } else { + cmp_reg64_imm32(buf, reg, imm as i32); + } + + jne_imm32(buf, offset); + + // on x86_64, jumps are calculated from the end of the jmp instruction + buf.len() + } + + #[inline(always)] + fn mov_freg32_imm32( + buf: &mut Vec<'_, u8>, + relocs: &mut Vec<'_, Relocation>, + dst: X86_64FloatReg, + imm: f32, + ) { + movss_freg32_rip_offset32(buf, dst, 0); + relocs.push(Relocation::LocalData { + offset: buf.len() as u64 - 4, + data: imm.to_le_bytes().to_vec(), + }); + } + #[inline(always)] + fn mov_freg64_imm64( + buf: &mut Vec<'_, u8>, + relocs: &mut Vec<'_, Relocation>, + dst: X86_64FloatReg, + imm: f64, + ) { + movsd_freg64_rip_offset32(buf, dst, 0); + relocs.push(Relocation::LocalData { + offset: buf.len() as u64 - 4, + data: imm.to_le_bytes().to_vec(), + }); + } + #[inline(always)] + fn mov_reg64_imm64(buf: &mut Vec<'_, u8>, dst: X86_64GeneralReg, imm: i64) { + mov_reg64_imm64(buf, dst, imm); + } + #[inline(always)] + fn mov_freg64_freg64(buf: &mut Vec<'_, u8>, dst: X86_64FloatReg, src: X86_64FloatReg) { + movsd_freg64_freg64(buf, dst, src); + } + + #[inline(always)] + fn mov_reg32_freg32(buf: &mut Vec<'_, u8>, dst: X86_64GeneralReg, src: X86_64FloatReg) { + movd_reg32_freg32(buf, dst, src); + } + #[inline(always)] + fn mov_reg64_freg64(buf: &mut Vec<'_, u8>, dst: X86_64GeneralReg, src: X86_64FloatReg) { + movq_reg64_freg64(buf, dst, src); + } + + #[inline(always)] + fn mov_freg32_reg32(_buf: &mut Vec<'_, u8>, _dst: X86_64FloatReg, _src: X86_64GeneralReg) { + unimplemented!("`mov_freg32_reg32` is not currently used by the x86 backend") + } + #[inline(always)] + fn mov_freg64_reg64(_buf: &mut Vec<'_, u8>, _dst: X86_64FloatReg, _src: X86_64GeneralReg) { + unimplemented!("`mov_freg64_reg64` is not currently used by the x86 backend") + } + + #[inline(always)] + fn mov_reg_reg( + buf: &mut Vec<'_, u8>, + register_width: RegisterWidth, + dst: X86_64GeneralReg, + src: X86_64GeneralReg, + ) { + mov_reg_reg(buf, register_width, dst, src); + } + + #[inline(always)] + fn movsx_reg_reg( + buf: &mut Vec<'_, u8>, + input_width: RegisterWidth, + dst: X86_64GeneralReg, + src: X86_64GeneralReg, + ) { + use RegisterWidth::*; + + match input_width { + W8 | W16 | W32 => raw_movsx_reg_reg(buf, input_width, dst, src), + W64 => mov_reg_reg(buf, input_width, dst, src), + } + } + + #[inline(always)] + fn movzx_reg_reg( + buf: &mut Vec<'_, u8>, + input_width: RegisterWidth, + dst: X86_64GeneralReg, + src: X86_64GeneralReg, + ) { + use RegisterWidth::*; + + match input_width { + W8 | W16 => raw_movzx_reg_reg(buf, input_width, dst, src), + W32 | W64 => mov_reg_reg(buf, input_width, dst, src), + } + } + + #[inline(always)] + fn mov_freg64_mem64_offset32( + buf: &mut Vec<'_, u8>, + dst: X86_64FloatReg, + src: X86_64GeneralReg, + offset: i32, + ) { + movsd_freg64_base64_offset32(buf, dst, src, offset) + } + + #[inline(always)] + fn mov_freg32_mem32_offset32( + buf: &mut Vec<'_, u8>, + dst: X86_64FloatReg, + src: X86_64GeneralReg, + offset: i32, + ) { + movss_freg32_base32_offset32(buf, dst, src, offset) + } + + #[inline(always)] + fn mov_freg64_base32(buf: &mut Vec<'_, u8>, dst: X86_64FloatReg, offset: i32) { + movsd_freg64_base64_offset32(buf, dst, X86_64GeneralReg::RBP, offset) + } + + #[inline(always)] + fn mov_reg_base32( + buf: &mut Vec<'_, u8>, + register_width: RegisterWidth, + dst: X86_64GeneralReg, + offset: i32, + ) { + use RegisterWidth::*; + + match register_width { + W8 => mov_reg8_base8_offset32(buf, dst, X86_64GeneralReg::RBP, offset), + W16 => mov_reg16_base16_offset32(buf, dst, X86_64GeneralReg::RBP, offset), + W32 => mov_reg32_base32_offset32(buf, dst, X86_64GeneralReg::RBP, offset), + W64 => mov_reg64_base64_offset32(buf, dst, X86_64GeneralReg::RBP, offset), + } + } + + #[inline(always)] + fn mov_base32_freg64(buf: &mut Vec<'_, u8>, offset: i32, src: X86_64FloatReg) { + movsd_base64_offset32_freg64(buf, X86_64GeneralReg::RBP, offset, src) + } + + #[inline(always)] + fn mov_base32_freg32(buf: &mut Vec<'_, u8>, offset: i32, src: X86_64FloatReg) { + movss_base32_offset32_freg32(buf, X86_64GeneralReg::RBP, offset, src) + } + + #[inline(always)] + fn movesd_mem64_offset32_freg64( + buf: &mut Vec<'_, u8>, + ptr: X86_64GeneralReg, + offset: i32, + src: X86_64FloatReg, + ) { + movsd_base64_offset32_freg64(buf, ptr, offset, src) + } + + #[inline(always)] + fn mov_base32_reg( + buf: &mut Vec<'_, u8>, + register_width: RegisterWidth, + offset: i32, + src: X86_64GeneralReg, + ) { + use RegisterWidth::*; + + match register_width { + W8 => mov_base8_offset32_reg8(buf, X86_64GeneralReg::RBP, offset, src), + W16 => mov_base16_offset32_reg16(buf, X86_64GeneralReg::RBP, offset, src), + W32 => mov_base32_offset32_reg32(buf, X86_64GeneralReg::RBP, offset, src), + W64 => mov_base64_offset32_reg64(buf, X86_64GeneralReg::RBP, offset, src), + } + } + + #[inline(always)] + fn mov_reg_mem_offset32( + buf: &mut Vec<'_, u8>, + register_width: RegisterWidth, + dst: X86_64GeneralReg, + src: X86_64GeneralReg, + offset: i32, + ) { + match register_width { + RegisterWidth::W8 => mov_reg8_base8_offset32(buf, dst, src, offset), + RegisterWidth::W16 => mov_reg16_base16_offset32(buf, dst, src, offset), + RegisterWidth::W32 => mov_reg32_base32_offset32(buf, dst, src, offset), + RegisterWidth::W64 => mov_reg64_base64_offset32(buf, dst, src, offset), + } + } + + #[inline(always)] + fn mov_mem_offset32_reg( + buf: &mut Vec<'_, u8>, + register_width: RegisterWidth, + dst: X86_64GeneralReg, + offset: i32, + src: X86_64GeneralReg, + ) { + match register_width { + RegisterWidth::W8 => mov_base8_offset32_reg8(buf, dst, offset, src), + RegisterWidth::W16 => mov_base16_offset32_reg16(buf, dst, offset, src), + RegisterWidth::W32 => mov_base32_offset32_reg32(buf, dst, offset, src), + RegisterWidth::W64 => mov_base64_offset32_reg64(buf, dst, offset, src), + } + } + + #[inline(always)] + fn movsx_reg_base32( + buf: &mut Vec<'_, u8>, + register_width: RegisterWidth, + dst: X86_64GeneralReg, + offset: i32, + ) { + use RegisterWidth::*; + + match register_width { + W64 => Self::mov_reg64_base32(buf, dst, offset), + W32 => movsx_reg64_base32_offset32(buf, dst, X86_64GeneralReg::RBP, offset), + W16 => movsx_reg64_base16_offset32(buf, dst, X86_64GeneralReg::RBP, offset), + W8 => movsx_reg64_base8_offset32(buf, dst, X86_64GeneralReg::RBP, offset), + } + } + #[inline(always)] + fn movzx_reg_base32( + buf: &mut Vec<'_, u8>, + register_width: RegisterWidth, + dst: X86_64GeneralReg, + offset: i32, + ) { + use RegisterWidth::*; + + match register_width { + W64 => Self::mov_reg64_base32(buf, dst, offset), + W32 => { + // The Intel documentation (3.4.1.1 General-Purpose Registers in 64-Bit Mode in manual Basic Architecture)) + // 32-bit operands generate a 32-bit result, zero-extended to a 64-bit result in the destination general-purpose register. + Self::mov_reg64_base32(buf, dst, offset) + } + W16 => movzx_reg64_base16_offset32(buf, dst, X86_64GeneralReg::RBP, offset), + W8 => movzx_reg64_base8_offset32(buf, dst, X86_64GeneralReg::RBP, offset), + } + } + + #[inline(always)] + fn mov_mem64_offset32_freg64( + buf: &mut Vec<'_, u8>, + dst: X86_64GeneralReg, + offset: i32, + src: X86_64FloatReg, + ) { + movsd_base64_offset32_freg64(buf, dst, offset, src) + } + + #[inline(always)] + fn mov_freg64_stack32(buf: &mut Vec<'_, u8>, dst: X86_64FloatReg, offset: i32) { + movsd_freg64_base64_offset32(buf, dst, X86_64GeneralReg::RSP, offset) + } + #[inline(always)] + fn mov_reg64_stack32(buf: &mut Vec<'_, u8>, dst: X86_64GeneralReg, offset: i32) { + mov_reg64_base64_offset32(buf, dst, X86_64GeneralReg::RSP, offset) + } + #[inline(always)] + fn mov_stack32_freg64(buf: &mut Vec<'_, u8>, offset: i32, src: X86_64FloatReg) { + movsd_base64_offset32_freg64(buf, X86_64GeneralReg::RSP, offset, src) + } + #[inline(always)] + fn mov_stack32_reg( + buf: &mut Vec<'_, u8>, + register_width: RegisterWidth, + offset: i32, + src: X86_64GeneralReg, + ) { + mov_base_offset32_reg(buf, register_width, X86_64GeneralReg::RSP, offset, src) + } + + #[inline(always)] + fn neg_reg64_reg64(buf: &mut Vec<'_, u8>, dst: X86_64GeneralReg, src: X86_64GeneralReg) { + mov_reg64_reg64(buf, dst, src); + neg_reg64(buf, dst); + } + + #[inline(always)] + fn sub_reg64_reg64_imm32( + buf: &mut Vec<'_, u8>, + dst: X86_64GeneralReg, + src1: X86_64GeneralReg, + imm32: i32, + ) { + mov_reg64_reg64(buf, dst, src1); + sub_reg64_imm32(buf, dst, imm32); + } + #[inline(always)] + fn sub_reg64_reg64_reg64( + buf: &mut Vec<'_, u8>, + dst: X86_64GeneralReg, + src1: X86_64GeneralReg, + src2: X86_64GeneralReg, + ) { + mov_reg64_reg64(buf, dst, src1); + sub_reg64_reg64(buf, dst, src2); + } + + #[inline(always)] + fn eq_reg_reg_reg( + buf: &mut Vec<'_, u8>, + register_width: RegisterWidth, + dst: X86_64GeneralReg, + src1: X86_64GeneralReg, + src2: X86_64GeneralReg, + ) { + cmp_reg64_reg64(buf, register_width, src1, src2); + sete_reg64(buf, dst); + } + + #[inline(always)] + fn neq_reg_reg_reg( + buf: &mut Vec<'_, u8>, + register_width: RegisterWidth, + dst: X86_64GeneralReg, + src1: X86_64GeneralReg, + src2: X86_64GeneralReg, + ) { + cmp_reg64_reg64(buf, register_width, src1, src2); + setne_reg64(buf, dst); + } + + #[inline(always)] + fn signed_compare_reg64( + buf: &mut Vec<'_, u8>, + register_width: RegisterWidth, + operation: CompareOperation, + dst: X86_64GeneralReg, + src1: X86_64GeneralReg, + src2: X86_64GeneralReg, + ) { + cmp_reg64_reg64(buf, register_width, src1, src2); + + match operation { + CompareOperation::LessThan => setl_reg64(buf, dst), + CompareOperation::LessThanOrEqual => setle_reg64(buf, dst), + CompareOperation::GreaterThan => setg_reg64(buf, dst), + CompareOperation::GreaterThanOrEqual => setge_reg64(buf, dst), + } + } + + fn unsigned_compare_reg64( + buf: &mut Vec<'_, u8>, + register_width: RegisterWidth, + operation: CompareOperation, + dst: X86_64GeneralReg, + src1: X86_64GeneralReg, + src2: X86_64GeneralReg, + ) { + cmp_reg64_reg64(buf, register_width, src1, src2); + + match operation { + CompareOperation::LessThan => setb_reg64(buf, dst), + CompareOperation::LessThanOrEqual => setbe_reg64(buf, dst), + CompareOperation::GreaterThan => seta_reg64(buf, dst), + CompareOperation::GreaterThanOrEqual => setae_reg64(buf, dst), + } + } + + fn eq_freg_freg_reg64( + buf: &mut Vec<'_, u8>, + dst: X86_64GeneralReg, + src1: X86_64FloatReg, + src2: X86_64FloatReg, + width: FloatWidth, + ) { + match width { + FloatWidth::F32 => cmp_freg32_freg32(buf, src1, src2), + FloatWidth::F64 => cmp_freg64_freg64(buf, src1, src2), + } + + sete_reg64(buf, dst); + } + + fn neq_freg_freg_reg64( + buf: &mut Vec<'_, u8>, + dst: X86_64GeneralReg, + src1: X86_64FloatReg, + src2: X86_64FloatReg, + width: FloatWidth, + ) { + match width { + FloatWidth::F32 => cmp_freg32_freg32(buf, src1, src2), + FloatWidth::F64 => cmp_freg64_freg64(buf, src1, src2), + } + + setne_reg64(buf, dst); + } + + #[inline(always)] + fn cmp_freg_freg_reg64( + buf: &mut Vec<'_, u8>, + dst: X86_64GeneralReg, + src1: X86_64FloatReg, + src2: X86_64FloatReg, + width: FloatWidth, + operation: CompareOperation, + ) { + use CompareOperation::*; + + let (arg1, arg2) = match operation { + LessThan | LessThanOrEqual => (src1, src2), + GreaterThan | GreaterThanOrEqual => (src2, src1), + }; + + match width { + FloatWidth::F32 => cmp_freg32_freg32(buf, arg2, arg1), + FloatWidth::F64 => cmp_freg64_freg64(buf, arg2, arg1), + } + + match operation { + LessThan | GreaterThan => seta_reg64(buf, dst), + LessThanOrEqual | GreaterThanOrEqual => setae_reg64(buf, dst), + }; + } + + #[inline(always)] + fn is_nan_freg_reg64( + buf: &mut Vec<'_, u8>, + dst: X86_64GeneralReg, + src: X86_64FloatReg, + width: FloatWidth, + ) { + match width { + FloatWidth::F32 => cmp_freg32_freg32(buf, src, src), + FloatWidth::F64 => cmp_freg64_freg64(buf, src, src), + } + + setp_reg64(buf, dst) + } + + #[inline(always)] + fn to_float_freg32_reg64(buf: &mut Vec<'_, u8>, dst: X86_64FloatReg, src: X86_64GeneralReg) { + cvtsi2ss_freg64_reg64(buf, dst, src); + } + + #[inline(always)] + fn to_float_freg32_freg64(buf: &mut Vec<'_, u8>, dst: X86_64FloatReg, src: X86_64FloatReg) { + cvtsd2ss_freg32_freg64(buf, dst, src); + } + + #[inline(always)] + fn to_float_freg64_freg32(buf: &mut Vec<'_, u8>, dst: X86_64FloatReg, src: X86_64FloatReg) { + cvtss2sd_freg64_freg32(buf, dst, src); + } + + #[inline(always)] + fn to_float_freg64_reg64(buf: &mut Vec<'_, u8>, dst: X86_64FloatReg, src: X86_64GeneralReg) { + cvtsi2sd_freg64_reg64(buf, dst, src); + } + + #[inline(always)] + fn ret(buf: &mut Vec<'_, u8>) { + ret(buf); + } + + fn set_if_overflow(buf: &mut Vec<'_, u8>, dst: X86_64GeneralReg) { + seto_reg64(buf, dst); + } + + fn and_reg64_reg64_reg64(buf: &mut Vec<'_, u8>, dst: Reg64, src1: Reg64, src2: Reg64) { + binop_move_src_to_dst_reg64(buf, and_reg64_reg64, dst, src1, src2) + } + + fn or_reg64_reg64_reg64(buf: &mut Vec<'_, u8>, dst: Reg64, src1: Reg64, src2: Reg64) { + binop_move_src_to_dst_reg64(buf, or_reg64_reg64, dst, src1, src2) + } + + fn xor_reg64_reg64_reg64(buf: &mut Vec<'_, u8>, dst: Reg64, src1: Reg64, src2: Reg64) { + binop_move_src_to_dst_reg64(buf, xor_reg64_reg64, dst, src1, src2) + } + + fn shl_reg64_reg64_reg64<'a, ASM, CC>( + buf: &mut Vec<'a, u8>, + storage_manager: &mut StorageManager<'a, '_, X86_64GeneralReg, X86_64FloatReg, ASM, CC>, + dst: X86_64GeneralReg, + src1: X86_64GeneralReg, + src2: X86_64GeneralReg, + ) where + ASM: Assembler, + CC: CallConv, + { + shift_reg64_reg64_reg64(buf, storage_manager, shl_reg64_reg64, dst, src1, src2) + } + + fn shr_reg64_reg64_reg64<'a, ASM, CC>( + buf: &mut Vec<'a, u8>, + storage_manager: &mut StorageManager<'a, '_, X86_64GeneralReg, X86_64FloatReg, ASM, CC>, + dst: X86_64GeneralReg, + src1: X86_64GeneralReg, + src2: X86_64GeneralReg, + ) where + ASM: Assembler, + CC: CallConv, + { + shift_reg64_reg64_reg64(buf, storage_manager, shr_reg64_reg64, dst, src1, src2) + } + + fn sar_reg64_reg64_reg64<'a, ASM, CC>( + buf: &mut Vec<'a, u8>, + storage_manager: &mut StorageManager<'a, '_, X86_64GeneralReg, X86_64FloatReg, ASM, CC>, + dst: X86_64GeneralReg, + src1: X86_64GeneralReg, + src2: X86_64GeneralReg, + ) where + ASM: Assembler, + CC: CallConv, + { + shift_reg64_reg64_reg64(buf, storage_manager, sar_reg64_reg64, dst, src1, src2) + } + + fn sqrt_freg64_freg64(buf: &mut Vec<'_, u8>, dst: X86_64FloatReg, src: X86_64FloatReg) { + sqrtsd_freg64_freg64(buf, dst, src) + } + + fn sqrt_freg32_freg32(buf: &mut Vec<'_, u8>, dst: X86_64FloatReg, src: X86_64FloatReg) { + sqrtss_freg32_freg32(buf, dst, src) + } +} + +fn shift_reg64_reg64_reg64<'a, ASM, CC>( + buf: &mut Vec<'a, u8>, + storage_manager: &mut StorageManager<'a, '_, X86_64GeneralReg, X86_64FloatReg, ASM, CC>, + shift_function: fn(buf: &mut Vec<'_, u8>, X86_64GeneralReg), + dst: X86_64GeneralReg, + src1: X86_64GeneralReg, + src2: X86_64GeneralReg, +) where + ASM: Assembler, + CC: CallConv, +{ + macro_rules! helper { + ($buf:expr, $dst:expr, $src1:expr, $src2:expr) => {{ + mov_reg64_reg64($buf, $dst, $src1); + mov_reg64_reg64($buf, X86_64GeneralReg::RCX, $src2); + + shift_function($buf, $dst) + }}; + } + + // if RCX is one of our input registers, we need to move some stuff around + if let X86_64GeneralReg::RCX = dst { + storage_manager.with_tmp_general_reg(buf, |_, buf, tmp| { + helper!(buf, tmp, src1, src2); + + mov_reg64_reg64(buf, dst, tmp); + }) + } else if let X86_64GeneralReg::RCX = src2 { + storage_manager.with_tmp_general_reg(buf, |_, buf, tmp| { + mov_reg64_reg64(buf, tmp, src2); + + helper!(buf, dst, src1, tmp); + }) + } else { + helper!(buf, dst, src1, src2) + } +} + +impl X86_64Assembler { + #[inline(always)] + fn pop_reg64(buf: &mut Vec<'_, u8>, reg: X86_64GeneralReg) { + pop_reg64(buf, reg); + } + + #[inline(always)] + fn push_reg64(buf: &mut Vec<'_, u8>, reg: X86_64GeneralReg) { + push_reg64(buf, reg); + } +} + +const GRP_4: u8 = 0x66; + +const REX: u8 = 0x40; + +// see https://wiki.osdev.org/X86-64_Instruction_Encoding#Encoding +/// If set, 64-bit operand size is used +const REX_PREFIX_W: u8 = 0b1000; +/// Extension to the MODRM.reg +/// Permits access to additional registers +const REX_PREFIX_R: u8 = 0b0100; +#[allow(unused)] +/// Extension to the SIB.index field +const REX_PREFIX_X: u8 = 0b0010; +/// Extension to the MODRM.rm +const REX_PREFIX_B: u8 = 0b0001; + +/// Wide REX (64-bit) +const REX_W: u8 = REX | REX_PREFIX_W; + +#[inline(always)] +fn add_rm_extension(reg: T, byte: u8) -> u8 { + if reg.value() > 7 { + byte | REX_PREFIX_B + } else { + byte + } +} + +#[inline(always)] +fn add_opcode_extension(reg: X86_64GeneralReg, byte: u8) -> u8 { + add_rm_extension(reg, byte) +} + +#[inline(always)] +fn add_reg_extension(reg: T, byte: u8) -> u8 { + if reg.value() > 7 { + byte | REX_PREFIX_R + } else { + byte + } +} + +#[inline(always)] +fn binop_reg8_reg8(op_code: u8, buf: &mut Vec, dst: X86_64GeneralReg, src: X86_64GeneralReg) { + let dst_high = dst as u8 > 7; + let dst_mod = dst as u8 % 8; + let src_high = src as u8 > 7; + let src_mod = src as u8 % 8; + + if dst_high || src_high { + let rex = add_rm_extension(dst, REX); + let rex = add_reg_extension(src, rex); + + buf.extend([rex, op_code, 0xC0 | dst_mod | (src_mod << 3)]) + } else { + let rex_prefix = [ + X86_64GeneralReg::RBP, + X86_64GeneralReg::RSP, + X86_64GeneralReg::RSI, + X86_64GeneralReg::RDI, + ]; + + if rex_prefix.contains(&src) || rex_prefix.contains(&dst) { + buf.push(0x40); + } + + buf.extend([op_code, 0xC0 | dst_mod | (src_mod << 3)]); + } +} + +#[inline(always)] +fn binop_reg16_reg16( + op_code: u8, + buf: &mut Vec<'_, u8>, + dst: X86_64GeneralReg, + src: X86_64GeneralReg, +) { + let dst_high = dst as u8 > 7; + let dst_mod = dst as u8 % 8; + let src_high = src as u8 > 7; + let src_mod = (src as u8 % 8) << 3; + + if dst_high || src_high { + let rex = add_rm_extension(dst, REX); + let rex = add_reg_extension(src, rex); + + buf.extend([0x66, rex, op_code, 0xC0 | dst_mod | src_mod]) + } else { + buf.extend([0x66, op_code, 0xC0 | dst_mod | src_mod]); + } +} + +#[inline(always)] +fn binop_reg32_reg32( + op_code: u8, + buf: &mut Vec<'_, u8>, + dst: X86_64GeneralReg, + src: X86_64GeneralReg, +) { + let dst_high = dst as u8 > 7; + let dst_mod = dst as u8 % 8; + let src_high = src as u8 > 7; + let src_mod = (src as u8 % 8) << 3; + + if dst_high || src_high { + let rex = add_rm_extension(dst, REX); + let rex = add_reg_extension(src, rex); + + buf.extend([rex, op_code, 0xC0 | dst_mod | src_mod]) + } else { + buf.extend([op_code, 0xC0 | dst_mod | src_mod]); + } +} + +#[inline(always)] +fn binop_reg64_reg64( + op_code: u8, + buf: &mut Vec<'_, u8>, + dst: X86_64GeneralReg, + src: X86_64GeneralReg, +) { + let rex = add_rm_extension(dst, REX_W); + let rex = add_reg_extension(src, rex); + let dst_mod = dst as u8 % 8; + let src_mod = (src as u8 % 8) << 3; + buf.extend([rex, op_code, 0xC0 | dst_mod | src_mod]); +} + +#[inline(always)] +fn extended_binop_reg64_reg64( + op_code1: u8, + op_code2: u8, + buf: &mut Vec<'_, u8>, + dst: X86_64GeneralReg, + src: X86_64GeneralReg, +) { + let rex = add_rm_extension(dst, REX_W); + let rex = add_reg_extension(src, rex); + let dst_mod = dst as u8 % 8; + let src_mod = (src as u8 % 8) << 3; + buf.extend([rex, op_code1, op_code2, 0xC0 | dst_mod | src_mod]); +} + +// Below here are the functions for all of the assembly instructions. +// Their names are based on the instruction and operators combined. +// You should call `buf.reserve()` if you push or extend more than once. +// Unit tests are added at the bottom of the file to ensure correct asm generation. +// Please keep these in alphanumeric order. +/// `ADD r/m64, imm32` -> Add imm32 sign-extended to 64-bits from r/m64. +#[inline(always)] +fn add_reg64_imm32(buf: &mut Vec<'_, u8>, dst: X86_64GeneralReg, imm: i32) { + // This can be optimized if the immediate is 1 byte. + let rex = add_rm_extension(dst, REX_W); + let dst_mod = dst as u8 % 8; + buf.reserve(7); + buf.extend([rex, 0x81, 0xC0 | dst_mod]); + buf.extend(imm.to_le_bytes()); +} + +/// `ADD r/m64,r64` -> Add r64 to r/m64. +#[inline(always)] +fn add_reg64_reg64(buf: &mut Vec<'_, u8>, dst: X86_64GeneralReg, src: X86_64GeneralReg) { + binop_reg64_reg64(0x01, buf, dst, src); +} + +/// `AND r/m64,r64` -> Bitwise logical and r64 to r/m64. +#[inline(always)] +fn and_reg64_reg64(buf: &mut Vec<'_, u8>, dst: X86_64GeneralReg, src: X86_64GeneralReg) { + // NOTE: src and dst are flipped by design + binop_reg64_reg64(0x23, buf, src, dst); +} + +/// `OR r/m64,r64` -> Bitwise logical or r64 to r/m64. +#[inline(always)] +fn or_reg64_reg64(buf: &mut Vec<'_, u8>, dst: X86_64GeneralReg, src: X86_64GeneralReg) { + // NOTE: src and dst are flipped by design + binop_reg64_reg64(0x0B, buf, src, dst); +} + +/// `XOR r/m64,r64` -> Bitwise logical exclusive or r64 to r/m64. +#[inline(always)] +fn xor_reg64_reg64(buf: &mut Vec<'_, u8>, dst: X86_64GeneralReg, src: X86_64GeneralReg) { + // NOTE: src and dst are flipped by design + binop_reg64_reg64(0x33, buf, src, dst); +} + +/// `SHL r/m64, CL` -> Multiply r/m64 by 2, CL times. +#[inline(always)] +fn shl_reg64_reg64(buf: &mut Vec<'_, u8>, dst: X86_64GeneralReg) { + let rex = add_rm_extension(dst, REX_W); + let rex = add_reg_extension(dst, rex); + + let dst_mod = dst as u8 % 8; + buf.extend([rex, 0xD3, 0xC0 | (4 << 3) | dst_mod]); +} + +/// `SHR r/m64, CL` -> Unsigned divide r/m64 by 2, CL times. +#[inline(always)] +fn shr_reg64_reg64(buf: &mut Vec<'_, u8>, dst: X86_64GeneralReg) { + let rex = add_rm_extension(dst, REX_W); + let rex = add_reg_extension(dst, rex); + + let dst_mod = dst as u8 % 8; + buf.extend([rex, 0xD3, 0xC0 | (5 << 3) | dst_mod]); +} + +/// `SAR r/m64, CL` -> Signed divide r/m64 by 2, CL times. +#[inline(always)] +fn sar_reg64_reg64(buf: &mut Vec<'_, u8>, dst: X86_64GeneralReg) { + let rex = add_rm_extension(dst, REX_W); + let rex = add_reg_extension(dst, rex); + + let dst_mod = dst as u8 % 8; + buf.extend([rex, 0xD3, 0xC0 | (7 << 3) | dst_mod]); +} + +/// `ADDSD xmm1,xmm2/m64` -> Add the low double-precision floating-point value from xmm2/mem to xmm1 and store the result in xmm1. +#[inline(always)] +fn addsd_freg64_freg64(buf: &mut Vec<'_, u8>, dst: X86_64FloatReg, src: X86_64FloatReg) { + let dst_high = dst as u8 > 7; + let dst_mod = dst as u8 % 8; + let src_high = src as u8 > 7; + let src_mod = src as u8 % 8; + if dst_high || src_high { + buf.extend([ + 0xF2, + 0x40 | ((dst_high as u8) << 2) | (src_high as u8), + 0x0F, + 0x58, + 0xC0 | (dst_mod << 3) | (src_mod), + ]) + } else { + buf.extend([0xF2, 0x0F, 0x58, 0xC0 | (dst_mod << 3) | (src_mod)]) + } +} + +/// `ADDSS xmm1,xmm2/m64` -> Add the low single-precision floating-point value from xmm2/mem to xmm1 and store the result in xmm1. +#[inline(always)] +fn addss_freg32_freg32(buf: &mut Vec<'_, u8>, dst: X86_64FloatReg, src: X86_64FloatReg) { + let dst_high = dst as u8 > 7; + let dst_mod = dst as u8 % 8; + let src_high = src as u8 > 7; + let src_mod = src as u8 % 8; + if dst_high || src_high { + buf.extend([ + 0xF3, + 0x40 | ((dst_high as u8) << 2) | (src_high as u8), + 0x0F, + 0x58, + 0xC0 | (dst_mod << 3) | (src_mod), + ]) + } else { + buf.extend([0xF3, 0x0F, 0x58, 0xC0 | (dst_mod << 3) | (src_mod)]) + } +} + +/// `MULSD xmm1,xmm2/m64` -> Multiply the low double-precision floating-point value from xmm2/mem to xmm1 and store the result in xmm1. +#[inline(always)] +fn mulsd_freg64_freg64(buf: &mut Vec<'_, u8>, dst: X86_64FloatReg, src: X86_64FloatReg) { + let dst_high = dst as u8 > 7; + let dst_mod = dst as u8 % 8; + let src_high = src as u8 > 7; + let src_mod = src as u8 % 8; + if dst_high || src_high { + buf.extend([ + 0xF2, + 0x40 | ((dst_high as u8) << 2) | (src_high as u8), + 0x0F, + 0x59, + 0xC0 | (dst_mod << 3) | (src_mod), + ]) + } else { + buf.extend([0xF2, 0x0F, 0x59, 0xC0 | (dst_mod << 3) | (src_mod)]) + } +} + +/// `DIVSS xmm1,xmm2/m64` -> Divide the low single-precision floating-point value from xmm2/mem to xmm1 and store the result in xmm1. +#[inline(always)] +fn divss_freg32_freg32(buf: &mut Vec<'_, u8>, dst: X86_64FloatReg, src: X86_64FloatReg) { + let dst_high = dst as u8 > 7; + let dst_mod = dst as u8 % 8; + let src_high = src as u8 > 7; + let src_mod = src as u8 % 8; + if dst_high || src_high { + buf.extend([ + 0xF3, + 0x40 | ((dst_high as u8) << 2) | (src_high as u8), + 0x0F, + 0x5E, + 0xC0 | (dst_mod << 3) | (src_mod), + ]) + } else { + buf.extend([0xF3, 0x0F, 0x5E, 0xC0 | (dst_mod << 3) | (src_mod)]) + } +} + +/// `DIVSD xmm1,xmm2/m64` -> Divide the low double-precision floating-point value from xmm2/mem to xmm1 and store the result in xmm1. +#[inline(always)] +fn divsd_freg64_freg64(buf: &mut Vec<'_, u8>, dst: X86_64FloatReg, src: X86_64FloatReg) { + let dst_high = dst as u8 > 7; + let dst_mod = dst as u8 % 8; + let src_high = src as u8 > 7; + let src_mod = src as u8 % 8; + if dst_high || src_high { + buf.extend([ + 0xF2, + 0x40 | ((dst_high as u8) << 2) | (src_high as u8), + 0x0F, + 0x5E, + 0xC0 | (dst_mod << 3) | (src_mod), + ]) + } else { + buf.extend([0xF2, 0x0F, 0x5E, 0xC0 | (dst_mod << 3) | (src_mod)]) + } +} + +/// `ADDSS xmm1,xmm2/m64` -> Add the low single-precision floating-point value from xmm2/mem to xmm1 and store the result in xmm1. +#[inline(always)] +fn mulss_freg32_freg32(buf: &mut Vec<'_, u8>, dst: X86_64FloatReg, src: X86_64FloatReg) { + let dst_high = dst as u8 > 7; + let dst_mod = dst as u8 % 8; + let src_high = src as u8 > 7; + let src_mod = src as u8 % 8; + if dst_high || src_high { + buf.extend([ + 0xF3, + 0x40 | ((dst_high as u8) << 2) | (src_high as u8), + 0x0F, + 0x59, + 0xC0 | (dst_mod << 3) | (src_mod), + ]) + } else { + buf.extend([0xF3, 0x0F, 0x59, 0xC0 | (dst_mod << 3) | (src_mod)]) + } +} + +#[inline(always)] +fn andpd_freg64_freg64(buf: &mut Vec<'_, u8>, dst: X86_64FloatReg, src: X86_64FloatReg) { + let dst_high = dst as u8 > 7; + let dst_mod = dst as u8 % 8; + let src_high = src as u8 > 7; + let src_mod = src as u8 % 8; + + if dst_high || src_high { + buf.extend([ + 0x66, + 0x40 | ((dst_high as u8) << 2) | (src_high as u8), + 0x0F, + 0x54, + 0xC0 | (dst_mod << 3) | (src_mod), + ]) + } else { + buf.extend([0x66, 0x0F, 0x54, 0xC0 | (dst_mod << 3) | (src_mod)]) + } +} + +/// r/m64 AND imm8 (sign-extended). +#[inline(always)] +fn and_reg64_imm8(buf: &mut Vec<'_, u8>, dst: X86_64GeneralReg, imm: i8) { + let rex = add_rm_extension(dst, REX_W); + let dst_mod = dst as u8 % 8; + buf.extend([rex, 0x83, 0xE0 | dst_mod, imm as u8]); +} + +/// `CMOVL r64,r/m64` -> Move if less (SF≠ OF). +#[inline(always)] +fn cmovl_reg64_reg64(buf: &mut Vec<'_, u8>, dst: X86_64GeneralReg, src: X86_64GeneralReg) { + let rex = add_reg_extension(dst, REX_W); + let rex = add_rm_extension(src, rex); + let dst_mod = (dst as u8 % 8) << 3; + let src_mod = src as u8 % 8; + buf.extend([rex, 0x0F, 0x4C, 0xC0 | dst_mod | src_mod]); +} + +/// `CMP r/m64,i32` -> Compare i32 to r/m64. +#[inline(always)] +fn cmp_reg64_imm32(buf: &mut Vec<'_, u8>, dst: X86_64GeneralReg, imm: i32) { + let rex = add_rm_extension(dst, REX_W); + let dst_mod = dst as u8 % 8; + buf.reserve(7); + buf.extend([rex, 0x81, 0xF8 | dst_mod]); + buf.extend(imm.to_le_bytes()); +} + +/// `CMP r/m64,r64` -> Compare r64 to r/m64. +#[inline(always)] +fn cmp_reg64_reg64( + buf: &mut Vec<'_, u8>, + register_width: RegisterWidth, + dst: X86_64GeneralReg, + src: X86_64GeneralReg, +) { + match register_width { + RegisterWidth::W8 => binop_reg64_reg64(0x38, buf, dst, src), + RegisterWidth::W16 => binop_reg16_reg16(0x39, buf, dst, src), + RegisterWidth::W32 => binop_reg32_reg32(0x39, buf, dst, src), + RegisterWidth::W64 => binop_reg64_reg64(0x39, buf, dst, src), + } +} + +#[inline(always)] +fn cmp_freg64_freg64(buf: &mut Vec<'_, u8>, src1: X86_64FloatReg, src2: X86_64FloatReg) { + let src1_high = src1 as u8 > 7; + let src1_mod = src1 as u8 % 8; + + let src2_high = src2 as u8 > 7; + let src2_mod = src2 as u8 % 8; + + if src1_high || src2_high { + buf.extend([ + 0x66, + 0x40 | ((src1_high as u8) << 2) | (src2_high as u8), + 0x0F, + 0x2E, + 0xC0 | (src1_mod << 3) | (src2_mod), + ]) + } else { + buf.extend([0x66, 0x0F, 0x2E, 0xC0 | (src1_mod << 3) | (src2_mod)]) + } +} + +#[inline(always)] +fn cmp_freg32_freg32(buf: &mut Vec<'_, u8>, src1: X86_64FloatReg, src2: X86_64FloatReg) { + let src1_high = src1 as u8 > 7; + let src1_mod = src1 as u8 % 8; + + let src2_high = src2 as u8 > 7; + let src2_mod = src2 as u8 % 8; + + if src1_high || src2_high { + buf.extend([ + 0x65, + 0x40 | ((src1_high as u8) << 2) | (src2_high as u8), + 0x0F, + 0x2E, + 0xC0 | (src1_mod << 3) | (src2_mod), + ]) + } else { + buf.extend([0x65, 0x0F, 0x2E, 0xC0 | (src1_mod << 3) | (src2_mod)]) + } +} + +#[inline(always)] +fn sqrtsd_freg64_freg64(buf: &mut Vec<'_, u8>, dst: X86_64FloatReg, src: X86_64FloatReg) { + let dst_high = dst as u8 > 7; + let dst_mod = dst as u8 % 8; + + let src_high = src as u8 > 7; + let src_mod = src as u8 % 8; + + if dst_high || src_high { + buf.extend([ + 0xF2, + 0x40 | ((dst_high as u8) << 2) | (src_high as u8), + 0x0F, + 0x51, + 0xC0 | (dst_mod << 3) | (src_mod), + ]) + } else { + buf.extend([0xF2, 0x0F, 0x51, 0xC0 | (dst_mod << 3) | (src_mod)]) + } +} + +#[inline(always)] +fn sqrtss_freg32_freg32(buf: &mut Vec<'_, u8>, dst: X86_64FloatReg, src: X86_64FloatReg) { + let dst_high = dst as u8 > 7; + let dst_mod = dst as u8 % 8; + + let src_high = src as u8 > 7; + let src_mod = src as u8 % 8; + + if dst_high || src_high { + buf.extend([ + 0xF3, + 0x40 | ((dst_high as u8) << 2) | (src_high as u8), + 0x0F, + 0x51, + 0xC0 | (dst_mod << 3) | (src_mod), + ]) + } else { + buf.extend([0xF3, 0x0F, 0x51, 0xC0 | (dst_mod << 3) | (src_mod)]) + } +} + +/// `TEST r/m64,r64` -> AND r64 with r/m64; set SF, ZF, PF according to result. +#[allow(dead_code)] +#[inline(always)] +fn test_reg64_reg64(buf: &mut Vec<'_, u8>, dst: X86_64GeneralReg, src: X86_64GeneralReg) { + binop_reg64_reg64(0x85, buf, dst, src); +} + +/// `IMUL r64,r/m64` -> Signed Multiply r/m64 to r64. +#[inline(always)] +fn imul_reg64_reg64(buf: &mut Vec<'_, u8>, dst: X86_64GeneralReg, src: X86_64GeneralReg) { + // IMUL is strange, the parameters are reversed from must other binary ops. + // The final encoding is (src, dst) instead of (dst, src). + extended_binop_reg64_reg64(0x0F, 0xAF, buf, src, dst); +} + +/// `MUL r/m64` -> Unsigned Multiply r/m64 to r64. +#[inline(always)] +fn mul_reg64_reg64(buf: &mut Vec<'_, u8>, src: X86_64GeneralReg) { + let mut rex = REX_W; + rex = add_reg_extension(src, rex); + + if src.value() > 7 { + rex |= REX_PREFIX_B; + } + + buf.extend([rex, 0xF7, 0b1110_0000 | (src as u8 % 8)]); +} + +/// `IDIV r/m64` -> Signed divide RDX:RAX by r/m64, with result stored in RAX ← Quotient, RDX ← Remainder. +#[inline(always)] +fn idiv_reg64_reg64(buf: &mut Vec<'_, u8>, src: X86_64GeneralReg) { + let mut rex = REX_W; + rex = add_reg_extension(src, rex); + + if src.value() > 7 { + rex |= REX_PREFIX_B; + } + + // The CQO instruction can be used to produce a double quadword dividend + // from a quadword before a quadword division. + // + // The CQO instruction (available in 64-bit mode only) copies the sign (bit 63) + // of the value in the RAX register into every bit position in the RDX register + buf.extend([0x48, 0x99]); + + buf.extend([rex, 0xF7, 0b1111_1000 | (src as u8 % 8)]); +} + +/// `DIV r/m64` -> Unsigned divide RDX:RAX by r/m64, with result stored in RAX ← Quotient, RDX ← Remainder. +#[inline(always)] +fn udiv_reg64_reg64(buf: &mut Vec<'_, u8>, src: X86_64GeneralReg) { + let mut rex = REX_W; + rex = add_reg_extension(src, rex); + + if src.value() > 7 { + rex |= REX_PREFIX_B; + } + + // The CQO instruction can be used to produce a double quadword dividend + // from a quadword before a quadword division. + // + // The CQO instruction (available in 64-bit mode only) copies the sign (bit 63) + // of the value in the RAX register into every bit position in the RDX register + buf.extend([0x48, 0x99]); + + // adds a cqo (convert doubleword to quadword) + buf.extend([rex, 0xF7, 0b1111_0000 | (src as u8 % 8)]); +} + +/// Jump near, relative, RIP = RIP + 32-bit displacement sign extended to 64-bits. +#[inline(always)] +fn jmp_imm32(buf: &mut Vec<'_, u8>, imm: i32) { + buf.reserve(5); + buf.push(0xE9); + buf.extend(imm.to_le_bytes()); +} + +#[inline(always)] +fn jmp_reg64_offset8(buf: &mut Vec<'_, u8>, base: X86_64GeneralReg, offset: i8) { + let rex = add_rm_extension(base, REX_W); + + #[allow(clippy::unusual_byte_groupings)] + buf.extend([rex, 0xff, 0b01_100_000 | (base as u8 % 8)]); + + // Using RSP or R12 requires a secondary index byte. + if base == X86_64GeneralReg::RSP || base == X86_64GeneralReg::R12 { + buf.push(0x24); + } + + buf.extend(offset.to_le_bytes()) +} + +/// Jump near if not equal (ZF=0). +#[inline(always)] +fn jne_imm32(buf: &mut Vec<'_, u8>, imm: i32) { + buf.reserve(6); + buf.push(0x0F); + buf.push(0x85); + buf.extend(imm.to_le_bytes()); +} + +/// `MOV r/m64, imm32` -> Move imm32 sign extended to 64-bits to r/m64. +#[inline(always)] +fn mov_reg64_imm32(buf: &mut Vec<'_, u8>, dst: X86_64GeneralReg, imm: i32) { + let rex = add_rm_extension(dst, REX_W); + let dst_mod = dst as u8 % 8; + buf.reserve(7); + buf.extend([rex, 0xC7, 0xC0 | dst_mod]); + buf.extend(imm.to_le_bytes()); +} + +/// `MOV r64, imm64` -> Move imm64 to r64. +#[inline(always)] +fn mov_reg64_imm64(buf: &mut Vec<'_, u8>, dst: X86_64GeneralReg, imm: i64) { + if imm <= i32::MAX as i64 && imm >= i32::MIN as i64 { + mov_reg64_imm32(buf, dst, imm as i32) + } else { + let rex = add_opcode_extension(dst, REX_W); + let dst_mod = dst as u8 % 8; + buf.reserve(10); + buf.extend([rex, 0xB8 | dst_mod]); + buf.extend(imm.to_le_bytes()); + } +} + +/// `LEA r64, m` -> Store effective address for m in register r64. +#[inline(always)] +fn lea_reg64(buf: &mut Vec<'_, u8>, dst: X86_64GeneralReg) { + let rex = add_opcode_extension(dst, REX_W); + let rex = add_reg_extension(dst, rex); + let dst_mod = dst as u8 % 8; + + #[allow(clippy::unusual_byte_groupings)] + buf.extend([ + rex, + 0x8d, + 0b00_000_101 | (dst_mod << 3), + 0x00, + 0x00, + 0x00, + 0x00, + ]) +} + +/// `LEA r64, m` -> Store effective address for m in register r64. +#[inline(always)] +fn lea_reg64_offset8( + buf: &mut Vec<'_, u8>, + dst: X86_64GeneralReg, + src: X86_64GeneralReg, + offset: i8, +) { + let rex = add_rm_extension(src, REX_W); + let rex = add_reg_extension(dst, rex); + + let dst_mod = dst as u8 % 8; + let src_mod = src as u8 % 8; + + #[allow(clippy::unusual_byte_groupings)] + // the upper bits 0b01 of the mod_rm byte indicate 8-bit displacement + buf.extend([rex, 0x8d, 0b01_000_000 | (dst_mod << 3) | src_mod]); + + // Using RSP or R12 requires a secondary index byte. + if src == X86_64GeneralReg::RSP || src == X86_64GeneralReg::R12 { + buf.push(0x24); + } + + buf.push(offset as u8); +} + +fn raw_mov_reg_reg( + buf: &mut Vec<'_, u8>, + register_width: RegisterWidth, + dst: X86_64GeneralReg, + src: X86_64GeneralReg, +) { + match register_width { + RegisterWidth::W8 => binop_reg8_reg8(0x88, buf, dst, src), + RegisterWidth::W16 => binop_reg16_reg16(0x89, buf, dst, src), + RegisterWidth::W32 => binop_reg32_reg32(0x89, buf, dst, src), + RegisterWidth::W64 => binop_reg64_reg64(0x89, buf, dst, src), + } +} + +#[allow(unused)] +fn raw_movsx_reg_reg( + buf: &mut Vec, + input_width: RegisterWidth, + dst: X86_64GeneralReg, + src: X86_64GeneralReg, +) { + let dst_high = dst as u8 > 7; + let dst_mod = dst as u8 % 8; + let src_high = src as u8 > 7; + let src_mod = src as u8 % 8; + + // NOTE src and dst seem to be flipped here. It works this way though + let mod_rm = 0xC0 | (dst_mod << 3) | src_mod; + + let rex = add_rm_extension(src, REX_W); + let rex = add_reg_extension(dst, rex); + + match input_width { + RegisterWidth::W8 => { + buf.extend([rex, 0x0f, 0xbe, mod_rm]); + } + RegisterWidth::W16 => { + buf.extend([rex, 0x0f, 0xbf, mod_rm]); + } + RegisterWidth::W32 => { + buf.extend([rex, 0x63, mod_rm]); + } + RegisterWidth::W64 => { /* do nothing */ } + } +} + +#[allow(unused)] +fn raw_movzx_reg_reg( + buf: &mut Vec, + input_width: RegisterWidth, + dst: X86_64GeneralReg, + src: X86_64GeneralReg, +) { + let dst_high = dst as u8 > 7; + let dst_mod = dst as u8 % 8; + let src_high = src as u8 > 7; + let src_mod = src as u8 % 8; + + // NOTE src and dst seem to be flipped here. It works this way though + let mod_rm = 0xC0 | (dst_mod << 3) | src_mod; + + let rex = add_rm_extension(src, REX_W); + let rex = add_reg_extension(dst, rex); + + match input_width { + RegisterWidth::W8 => { + buf.extend([rex, 0x0f, 0xb6, mod_rm]); + } + RegisterWidth::W16 => { + buf.extend([rex, 0x0f, 0xb7, mod_rm]); + } + RegisterWidth::W32 | RegisterWidth::W64 => { /* do nothing */ } + } +} + +/// `MOV r/m64,r64` -> Move r64 to r/m64. +/// This will not generate anything if dst and src are the same. +#[inline(always)] +fn mov_reg64_reg64(buf: &mut Vec<'_, u8>, dst: X86_64GeneralReg, src: X86_64GeneralReg) { + mov_reg_reg(buf, RegisterWidth::W64, dst, src) +} + +#[inline(always)] +fn mov_reg_reg( + buf: &mut Vec<'_, u8>, + register_width: RegisterWidth, + dst: X86_64GeneralReg, + src: X86_64GeneralReg, +) { + if dst != src { + raw_mov_reg_reg(buf, register_width, dst, src); + } +} + +// The following base and stack based operations could be optimized based on how many bytes the offset actually is. + +#[inline(always)] +fn mov_base_offset32_reg( + buf: &mut Vec<'_, u8>, + register_width: RegisterWidth, + base: X86_64GeneralReg, + offset: i32, + src: X86_64GeneralReg, +) { + match register_width { + RegisterWidth::W8 => mov_base16_offset32_reg16(buf, base, offset, src), + RegisterWidth::W16 => mov_base16_offset32_reg16(buf, base, offset, src), + RegisterWidth::W32 => mov_base32_offset32_reg32(buf, base, offset, src), + RegisterWidth::W64 => mov_base64_offset32_reg64(buf, base, offset, src), + } +} + +/// `MOV r/m64,r64` -> Move r64 to r/m64, where m64 references a base + offset. +#[inline(always)] +fn mov_base64_offset32_reg64( + buf: &mut Vec<'_, u8>, + base: X86_64GeneralReg, + offset: i32, + src: X86_64GeneralReg, +) { + let rex = add_rm_extension(base, REX_W); + let rex = add_reg_extension(src, rex); + let src_mod = (src as u8 % 8) << 3; + let base_mod = base as u8 % 8; + buf.reserve(8); + buf.extend([rex, 0x89, 0x80 | src_mod | base_mod]); + // Using RSP or R12 requires a secondary index byte. + if base == X86_64GeneralReg::RSP || base == X86_64GeneralReg::R12 { + buf.push(0x24); + } + buf.extend(offset.to_le_bytes()); +} + +/// `MOV r/m32,r32` -> Move r32 to r/m32, where m32 references a base + offset. +#[inline(always)] +fn mov_base32_offset32_reg32( + buf: &mut Vec<'_, u8>, + base: X86_64GeneralReg, + offset: i32, + src: X86_64GeneralReg, +) { + let rex = add_rm_extension(base, REX); + let rex = add_reg_extension(src, rex); + let src_mod = (src as u8 % 8) << 3; + let base_mod = base as u8 % 8; + buf.reserve(8); + buf.extend([rex, 0x89, 0x80 | src_mod | base_mod]); + // Using RSP or R12 requires a secondary index byte. + if base == X86_64GeneralReg::RSP || base == X86_64GeneralReg::R12 { + buf.push(0x24); + } + buf.extend(offset.to_le_bytes()); +} + +/// `MOV r/m16,r16` -> Move r16 to r/m16, where m16 references a base + offset. +#[inline(always)] +fn mov_base16_offset32_reg16( + buf: &mut Vec<'_, u8>, + base: X86_64GeneralReg, + offset: i32, + src: X86_64GeneralReg, +) { + let rex = add_rm_extension(base, REX); + let rex = add_reg_extension(src, rex); + let src_mod = (src as u8 % 8) << 3; + let base_mod = base as u8 % 8; + buf.reserve(8); + buf.extend([GRP_4, rex, 0x89, 0x80 | src_mod | base_mod]); + // Using RSP or R12 requires a secondary index byte. + if base == X86_64GeneralReg::RSP || base == X86_64GeneralReg::R12 { + buf.push(0x24); + } + buf.extend(offset.to_le_bytes()); +} + +/// `MOV r/m8,r8` -> Move r8 to r/m8, where m8 references a base + offset. +#[inline(always)] +fn mov_base8_offset32_reg8( + buf: &mut Vec<'_, u8>, + base: X86_64GeneralReg, + offset: i32, + src: X86_64GeneralReg, +) { + let rex = add_rm_extension(base, REX); + let rex = add_reg_extension(src, rex); + let src_mod = (src as u8 % 8) << 3; + let base_mod = base as u8 % 8; + buf.reserve(8); + buf.extend([rex, 0x88, 0x80 | src_mod | base_mod]); + // Using RSP or R12 requires a secondary index byte. + if base == X86_64GeneralReg::RSP || base == X86_64GeneralReg::R12 { + buf.push(0x24); + } + buf.extend(offset.to_le_bytes()); +} + +#[inline(always)] +fn mov_reg_base_offset32( + buf: &mut Vec<'_, u8>, + register_width: RegisterWidth, + dst: X86_64GeneralReg, + base: X86_64GeneralReg, + offset: i32, +) { + use RegisterWidth::*; + + let rex = match register_width { + W64 => REX_W, + _ => REX, + }; + + let rex = add_rm_extension(base, rex); + let rex = add_reg_extension(dst, rex); + + let dst_mod = (dst as u8 % 8) << 3; + let base_mod = base as u8 % 8; + let operands = 0x80 | dst_mod | base_mod; + + buf.reserve(8); + + let instruction = match register_width { + W8 => 0x8A, + W16 | W32 | W64 => 0x8B, + }; + + match register_width { + W16 => buf.extend([GRP_4, rex, instruction, operands]), + _ => buf.extend([rex, instruction, operands]), + }; + + // Using RSP or R12 requires a secondary index byte. + if base == X86_64GeneralReg::RSP || base == X86_64GeneralReg::R12 { + buf.push(0x24); + } + buf.extend(offset.to_le_bytes()); +} + +/// `MOV r64,r/m64` -> Move r/m64 to r64, where m64 references a base + offset. +#[inline(always)] +fn mov_reg64_base64_offset32( + buf: &mut Vec<'_, u8>, + dst: X86_64GeneralReg, + base: X86_64GeneralReg, + offset: i32, +) { + mov_reg_base_offset32(buf, RegisterWidth::W64, dst, base, offset) +} + +/// `MOV r/m32,r32` -> Move r32 to r/m32. +#[inline(always)] +fn mov_reg32_base32_offset32( + buf: &mut Vec<'_, u8>, + dst: X86_64GeneralReg, + base: X86_64GeneralReg, + offset: i32, +) { + mov_reg_base_offset32(buf, RegisterWidth::W32, dst, base, offset) +} + +/// `MOV r/m16,r16` -> Move r16 to r/m16. +#[inline(always)] +fn mov_reg16_base16_offset32( + buf: &mut Vec<'_, u8>, + dst: X86_64GeneralReg, + base: X86_64GeneralReg, + offset: i32, +) { + mov_reg_base_offset32(buf, RegisterWidth::W16, dst, base, offset) +} + +/// `MOV r/m8,r8` -> Move r8 to r/m8. +#[inline(always)] +fn mov_reg8_base8_offset32( + buf: &mut Vec<'_, u8>, + dst: X86_64GeneralReg, + base: X86_64GeneralReg, + offset: i32, +) { + mov_reg_base_offset32(buf, RegisterWidth::W8, dst, base, offset) +} + +#[inline(always)] +fn movsx_reg64_base_offset32( + buf: &mut Vec<'_, u8>, + dst: X86_64GeneralReg, + base: X86_64GeneralReg, + offset: i32, + opcode: &[u8], +) { + let rex = add_rm_extension(base, REX_W); + let rex = add_reg_extension(dst, rex); + let dst_mod = (dst as u8 % 8) << 3; + let base_mod = base as u8 % 8; + buf.reserve(9); + + // our output is a 64-bit value, so rex is always needed + buf.push(rex); + buf.extend(opcode); + buf.push(0x80 | dst_mod | base_mod); + + // Using RSP or R12 requires a secondary index byte. + if base == X86_64GeneralReg::RSP || base == X86_64GeneralReg::R12 { + buf.push(0x24); + } + buf.extend(offset.to_le_bytes()); +} + +/// `MOVSX r64,r/m32` -> Move r/m32 with sign extention to r64, where m32 references a base + offset. +#[inline(always)] +fn movsx_reg64_base32_offset32( + buf: &mut Vec<'_, u8>, + dst: X86_64GeneralReg, + base: X86_64GeneralReg, + offset: i32, +) { + movsx_reg64_base_offset32(buf, dst, base, offset, &[0x63]) +} + +/// `MOVSX r64,r/m16` -> Move r/m16 with sign extention to r64, where m16 references a base + offset. +#[inline(always)] +fn movsx_reg64_base16_offset32( + buf: &mut Vec<'_, u8>, + dst: X86_64GeneralReg, + base: X86_64GeneralReg, + offset: i32, +) { + movsx_reg64_base_offset32(buf, dst, base, offset, &[0x0F, 0xBF]) +} + +/// `MOVSX r64,r/m8` -> Move r/m8 with sign extention to r64, where m8 references a base + offset. +#[inline(always)] +fn movsx_reg64_base8_offset32( + buf: &mut Vec<'_, u8>, + dst: X86_64GeneralReg, + base: X86_64GeneralReg, + offset: i32, +) { + movsx_reg64_base_offset32(buf, dst, base, offset, &[0x0F, 0xBE]) +} + +#[inline(always)] +fn movzx_reg64_base_offset32( + buf: &mut Vec<'_, u8>, + dst: X86_64GeneralReg, + base: X86_64GeneralReg, + offset: i32, + opcode: u8, +) { + let rex = add_rm_extension(base, REX_W); + let rex = add_reg_extension(dst, rex); + let dst_mod = (dst as u8 % 8) << 3; + let base_mod = base as u8 % 8; + buf.reserve(9); + buf.extend([rex, 0x0F, opcode, 0x80 | dst_mod | base_mod]); + // Using RSP or R12 requires a secondary index byte. + if base == X86_64GeneralReg::RSP || base == X86_64GeneralReg::R12 { + buf.push(0x24); + } + buf.extend(offset.to_le_bytes()); +} + +/// `MOVZX r64,r/m8` -> Move r/m8 with zero extention to r64, where m8 references a base + offset. +#[inline(always)] +fn movzx_reg64_base8_offset32( + buf: &mut Vec<'_, u8>, + dst: X86_64GeneralReg, + base: X86_64GeneralReg, + offset: i32, +) { + movzx_reg64_base_offset32(buf, dst, base, offset, 0xB6) +} + +/// `MOVZX r64,r/m16` -> Move r/m16 with zero extention to r64, where m16 references a base + offset. +#[inline(always)] +fn movzx_reg64_base16_offset32( + buf: &mut Vec<'_, u8>, + dst: X86_64GeneralReg, + base: X86_64GeneralReg, + offset: i32, +) { + movzx_reg64_base_offset32(buf, dst, base, offset, 0xB7) +} + +#[inline(always)] +fn movd_reg32_freg32(buf: &mut Vec<'_, u8>, dst: X86_64GeneralReg, src: X86_64FloatReg) { + let dst_high = dst as u8 > 7; + let dst_mod = dst as u8 % 8; + let src_high = src as u8 > 7; + let src_mod = src as u8 % 8; + if dst_high || src_high { + let rex = add_rm_extension(dst, REX); + let rex = add_reg_extension(src, rex); + + buf.extend([0x66, rex, 0x0F, 0x7E, 0xC0 | (src_mod << 3) | (dst_mod)]) + } else { + buf.extend([0x66, 0x0F, 0x7E, 0xC0 | (src_mod << 3) | (dst_mod)]) + } +} + +#[inline(always)] +fn movq_reg64_freg64(buf: &mut Vec<'_, u8>, dst: X86_64GeneralReg, src: X86_64FloatReg) { + let dst_mod = dst as u8 % 8; + let src_mod = src as u8 % 8; + + let rex = add_rm_extension(dst, REX_W); + let rex = add_reg_extension(src, rex); + + buf.extend([0x66, rex, 0x0F, 0x7E, 0xC0 | (src_mod << 3) | (dst_mod)]); +} + +/// `MOVSD xmm1,xmm2` -> Move scalar double-precision floating-point value from xmm2 to xmm1 register. +/// This will not generate anything if dst and src are the same. +#[inline(always)] +fn movsd_freg64_freg64(buf: &mut Vec<'_, u8>, dst: X86_64FloatReg, src: X86_64FloatReg) { + if dst != src { + raw_movsd_freg64_freg64(buf, dst, src); + } +} + +/// `MOVSD xmm1,xmm2` -> Move scalar double-precision floating-point value from xmm2 to xmm1 register. +/// This will always generate the move. It is used for verification. +#[inline(always)] +fn raw_movsd_freg64_freg64(buf: &mut Vec<'_, u8>, dst: X86_64FloatReg, src: X86_64FloatReg) { + let dst_high = dst as u8 > 7; + let dst_mod = dst as u8 % 8; + let src_high = src as u8 > 7; + let src_mod = src as u8 % 8; + if dst_high || src_high { + buf.extend([ + 0xF2, + 0x40 | ((dst_high as u8) << 2) | (src_high as u8), + 0x0F, + 0x10, + 0xC0 | (dst_mod << 3) | (src_mod), + ]) + } else { + buf.extend([0xF2, 0x0F, 0x10, 0xC0 | (dst_mod << 3) | (src_mod)]) + } +} + +/// `MOVSS xmm1,xmm2` -> Move scalar low single-precision floating-point value from xmm2 to xmm1 register. +/// This will not generate anything if dst and src are the same. +#[inline(always)] +fn movss_freg32_freg32(buf: &mut Vec<'_, u8>, dst: X86_64FloatReg, src: X86_64FloatReg) { + if dst != src { + raw_movss_freg32_freg32(buf, dst, src); + } +} + +/// `MOVSS xmm1,xmm2` -> Move scalar low single-precision floating-point from xmm2 to xmm1 register. +/// This will always generate the move. It is used for verification. +#[inline(always)] +fn raw_movss_freg32_freg32(buf: &mut Vec<'_, u8>, dst: X86_64FloatReg, src: X86_64FloatReg) { + let dst_high = dst as u8 > 7; + let dst_mod = dst as u8 % 8; + let src_high = src as u8 > 7; + let src_mod = src as u8 % 8; + if dst_high || src_high { + buf.extend([ + 0xF3, + 0x40 | ((dst_high as u8) << 2) | (src_high as u8), + 0x0F, + 0x10, + 0xC0 | (dst_mod << 3) | (src_mod), + ]) + } else { + buf.extend([0xF3, 0x0F, 0x10, 0xC0 | (dst_mod << 3) | (src_mod)]) + } +} + +// `MOVSS xmm, m32` -> Load scalar single-precision floating-point value from m32 to xmm register. +#[inline(always)] +fn movss_freg32_rip_offset32(buf: &mut Vec<'_, u8>, dst: X86_64FloatReg, offset: u32) { + let dst_mod = dst as u8 % 8; + if dst as u8 > 7 { + buf.reserve(9); + buf.extend([0xF3, 0x44, 0x0F, 0x10, 0x05 | (dst_mod << 3)]); + } else { + buf.reserve(8); + buf.extend([0xF3, 0x0F, 0x10, 0x05 | (dst_mod << 3)]); + } + buf.extend(offset.to_le_bytes()); +} + +// `MOVSD xmm, m64` -> Load scalar double-precision floating-point value from m64 to xmm register. +#[inline(always)] +fn movsd_freg64_rip_offset32(buf: &mut Vec<'_, u8>, dst: X86_64FloatReg, offset: u32) { + let dst_mod = dst as u8 % 8; + if dst as u8 > 7 { + buf.reserve(9); + buf.extend([0xF2, 0x44, 0x0F, 0x10, 0x05 | (dst_mod << 3)]); + } else { + buf.reserve(8); + buf.extend([0xF2, 0x0F, 0x10, 0x05 | (dst_mod << 3)]); + } + buf.extend(offset.to_le_bytes()); +} + +// `MOVSD r/m64,xmm1` -> Move xmm1 to r/m64. where m64 references the base pointer. +#[inline(always)] +fn movsd_base64_offset32_freg64( + buf: &mut Vec<'_, u8>, + base: X86_64GeneralReg, + offset: i32, + src: X86_64FloatReg, +) { + let rex = add_rm_extension(base, REX_W); + let rex = add_reg_extension(src, rex); + let src_mod = (src as u8 % 8) << 3; + let base_mod = base as u8 % 8; + buf.reserve(10); + buf.push(0xF2); + if src as u8 > 7 || base as u8 > 7 { + buf.push(rex); + } + buf.extend([0x0F, 0x11, 0x80 | src_mod | base_mod]); + // Using RSP or R12 requires a secondary index byte. + if base == X86_64GeneralReg::RSP || base == X86_64GeneralReg::R12 { + buf.push(0x24); + } + buf.extend(offset.to_le_bytes()); +} + +// `MOVSS r/m64,xmm1` -> Move xmm1 to r/m64. where m64 references the base pointer. +#[inline(always)] +fn movss_base32_offset32_freg32( + buf: &mut Vec<'_, u8>, + base: X86_64GeneralReg, + offset: i32, + src: X86_64FloatReg, +) { + let rex = add_rm_extension(base, REX_W); + let rex = add_reg_extension(src, rex); + let src_mod = (src as u8 % 8) << 3; + let base_mod = base as u8 % 8; + buf.reserve(10); + buf.push(0xF3); + if src as u8 > 7 || base as u8 > 7 { + buf.push(rex); + } + buf.extend([0x0F, 0x11, 0x80 | src_mod | base_mod]); + // Using RSP or R12 requires a secondary index byte. + if base == X86_64GeneralReg::RSP || base == X86_64GeneralReg::R12 { + buf.push(0x24); + } + buf.extend(offset.to_le_bytes()); +} + +/// `MOVSD xmm1,r/m64` -> Move r/m64 to xmm1. where m64 references the base pointer. +#[inline(always)] +fn movsd_freg64_base64_offset32( + buf: &mut Vec<'_, u8>, + dst: X86_64FloatReg, + base: X86_64GeneralReg, + offset: i32, +) { + let rex = add_rm_extension(base, REX_W); + let rex = add_reg_extension(dst, rex); + let dst_mod = (dst as u8 % 8) << 3; + let base_mod = base as u8 % 8; + buf.reserve(10); + buf.push(0xF2); + if dst as u8 > 7 || base as u8 > 7 { + buf.push(rex); + } + buf.extend([0x0F, 0x10, 0x80 | dst_mod | base_mod]); + // Using RSP or R12 requires a secondary index byte. + if base == X86_64GeneralReg::RSP || base == X86_64GeneralReg::R12 { + buf.push(0x24); + } + buf.extend(offset.to_le_bytes()); +} + +/// `MOVSS xmm1,r/m32` -> Move r/m32 to xmm1. where m64 references the base pointer. +#[inline(always)] +fn movss_freg32_base32_offset32( + buf: &mut Vec<'_, u8>, + dst: X86_64FloatReg, + base: X86_64GeneralReg, + offset: i32, +) { + let rex = add_rm_extension(base, REX_W); + let rex = add_reg_extension(dst, rex); + let dst_mod = (dst as u8 % 8) << 3; + let base_mod = base as u8 % 8; + buf.reserve(10); + buf.push(0xF3); + if dst as u8 > 7 || base as u8 > 7 { + buf.push(rex); + } + buf.extend([0x0F, 0x10, 0x80 | dst_mod | base_mod]); + // Using RSP or R12 requires a secondary index byte. + if base == X86_64GeneralReg::RSP || base == X86_64GeneralReg::R12 { + buf.push(0x24); + } + buf.extend(offset.to_le_bytes()); +} + +/// `NEG r/m64` -> Two's complement negate r/m64. +#[inline(always)] +fn neg_reg64(buf: &mut Vec<'_, u8>, reg: X86_64GeneralReg) { + let rex = add_rm_extension(reg, REX_W); + let reg_mod = reg as u8 % 8; + buf.extend([rex, 0xF7, 0xD8 | reg_mod]); +} + +// helper function for `set*` instructions +#[inline(always)] +fn set_reg64_help(op_code: u8, buf: &mut Vec<'_, u8>, reg: X86_64GeneralReg) { + // XOR needs 3 bytes, actual SETE instruction need 3 or 4 bytes + buf.reserve(7); + + // Actually apply the SETE instruction + let reg_mod = reg as u8 % 8; + use X86_64GeneralReg::*; + match reg { + RAX | RCX | RDX | RBX => buf.extend([0x0F, op_code, 0xC0 | reg_mod]), + RSP | RBP | RSI | RDI => buf.extend([REX, 0x0F, op_code, 0xC0 | reg_mod]), + R8 | R9 | R10 | R11 | R12 | R13 | R14 | R15 => { + buf.extend([REX | 1, 0x0F, op_code, 0xC0 | reg_mod]) + } + } + + // We and reg with 1 because the SETE instruction only applies + // to the lower bits of the register + and_reg64_imm8(buf, reg, 1); +} + +#[inline(always)] +fn cvtsi2_help( + buf: &mut Vec<'_, u8>, + op_code1: u8, + op_code2: u8, + dst: T, + src: U, +) { + let rex = add_rm_extension(src, REX_W); + let rex = add_reg_extension(dst, rex); + let mod1 = (dst.value() % 8) << 3; + let mod2 = src.value() % 8; + + buf.extend([op_code1, rex, 0x0F, op_code2, 0xC0 | mod1 | mod2]) +} + +#[inline(always)] +fn cvtsx2_help( + buf: &mut Vec<'_, u8>, + op_code1: u8, + op_code2: u8, + dst: T, + src: V, +) { + let mod1 = (dst.value() % 8) << 3; + let mod2 = src.value() % 8; + + buf.extend([op_code1, 0x0F, op_code2, 0xC0 | mod1 | mod2]) +} + +/// `SETE r/m64` -> Set Byte on Condition - zero/equal (ZF=1) +#[inline(always)] +fn sete_reg64(buf: &mut Vec<'_, u8>, reg: X86_64GeneralReg) { + set_reg64_help(0x94, buf, reg); +} + +/// `CVTSS2SD xmm` -> Convert one single-precision floating-point value in xmm/m32 to one double-precision floating-point value in xmm. +#[inline(always)] +fn cvtss2sd_freg64_freg32(buf: &mut Vec<'_, u8>, dst: X86_64FloatReg, src: X86_64FloatReg) { + cvtsx2_help(buf, 0xF3, 0x5A, dst, src) +} + +/// `CVTSD2SS xmm` -> Convert one double-precision floating-point value in xmm to one single-precision floating-point value and merge with high bits. +#[inline(always)] +fn cvtsd2ss_freg32_freg64(buf: &mut Vec<'_, u8>, dst: X86_64FloatReg, src: X86_64FloatReg) { + cvtsx2_help(buf, 0xF2, 0x5A, dst, src) +} + +/// `CVTSI2SD r/m64` -> Convert one signed quadword integer from r/m64 to one double-precision floating-point value in xmm. +#[inline(always)] +fn cvtsi2sd_freg64_reg64(buf: &mut Vec<'_, u8>, dst: X86_64FloatReg, src: X86_64GeneralReg) { + cvtsi2_help(buf, 0xF2, 0x2A, dst, src) +} + +/// `CVTSI2SS r/m64` -> Convert one signed quadword integer from r/m64 to one single-precision floating-point value in xmm. +#[allow(dead_code)] +#[inline(always)] +fn cvtsi2ss_freg64_reg64(buf: &mut Vec<'_, u8>, dst: X86_64FloatReg, src: X86_64GeneralReg) { + cvtsi2_help(buf, 0xF3, 0x2A, dst, src) +} + +/// `CVTTSS2SI xmm/m32` -> Convert one single-precision floating-point value from xmm/m32 to one signed quadword integer in r64 using truncation. +#[allow(dead_code)] +#[inline(always)] +fn cvttss2si_reg64_freg64(buf: &mut Vec<'_, u8>, dst: X86_64GeneralReg, src: X86_64FloatReg) { + cvtsi2_help(buf, 0xF3, 0x2C, dst, src) +} + +/// `SETNE r/m64` -> Set byte if not equal (ZF=0). +#[inline(always)] +fn setne_reg64(buf: &mut Vec<'_, u8>, reg: X86_64GeneralReg) { + set_reg64_help(0x95, buf, reg); +} + +/// `SETL r/m64` -> Set byte if less (SF≠ OF). +#[inline(always)] +fn setl_reg64(buf: &mut Vec<'_, u8>, reg: X86_64GeneralReg) { + set_reg64_help(0x9c, buf, reg); +} + +/// `SETB r/m64` -> Set byte if less (SF≠ OF). +#[inline(always)] +fn setb_reg64(buf: &mut Vec<'_, u8>, reg: X86_64GeneralReg) { + set_reg64_help(0x92, buf, reg); +} + +/// `SETG r/m64` -> Set byte if greater (ZF=0 and SF=OF). +#[inline(always)] +fn setg_reg64(buf: &mut Vec<'_, u8>, reg: X86_64GeneralReg) { + set_reg64_help(0x9f, buf, reg); +} + +/// `SETA r/m64` -> Set byte if above (CF=0 and ZF=0). +#[inline(always)] +fn seta_reg64(buf: &mut Vec<'_, u8>, reg: X86_64GeneralReg) { + set_reg64_help(0x97, buf, reg); +} + +/// `SETAE r/m64` -> Set byte if above or equal (CF=0). +#[inline(always)] +fn setae_reg64(buf: &mut Vec<'_, u8>, reg: X86_64GeneralReg) { + set_reg64_help(0x93, buf, reg); +} + +/// `SETBE r/m64` -> Set byte if below or equal (CF=1 or ZF=1). +#[inline(always)] +fn setbe_reg64(buf: &mut Vec<'_, u8>, reg: X86_64GeneralReg) { + set_reg64_help(0x96, buf, reg); +} + +/// `SETLE r/m64` -> Set byte if less or equal (ZF=1 or SF ≠ OF). +#[inline(always)] +fn setle_reg64(buf: &mut Vec<'_, u8>, reg: X86_64GeneralReg) { + set_reg64_help(0x9e, buf, reg); +} + +/// `SETGE r/m64` -> Set byte if greater or equal (SF=OF). +#[inline(always)] +fn setge_reg64(buf: &mut Vec<'_, u8>, reg: X86_64GeneralReg) { + set_reg64_help(0x9d, buf, reg); +} + +/// `SETO r/m64` -> Set byte if overflow flag is set. +#[inline(always)] +fn seto_reg64(buf: &mut Vec<'_, u8>, reg: X86_64GeneralReg) { + set_reg64_help(0x90, buf, reg); +} + +/// `SETP r/m64` -> Set byte if parity (PF=1). +#[inline(always)] +fn setp_reg64(buf: &mut Vec<'_, u8>, reg: X86_64GeneralReg) { + set_reg64_help(0x9A, buf, reg); +} + +/// `RET` -> Near return to calling procedure. +#[inline(always)] +fn ret(buf: &mut Vec<'_, u8>) { + buf.push(0xC3); +} + +/// `SUB r/m64, imm32` -> Subtract imm32 sign-extended to 64-bits from r/m64. +#[inline(always)] +fn sub_reg64_imm32(buf: &mut Vec<'_, u8>, dst: X86_64GeneralReg, imm: i32) { + // This can be optimized if the immediate is 1 byte. + let rex = add_rm_extension(dst, REX_W); + let dst_mod = dst as u8 % 8; + buf.reserve(7); + buf.extend([rex, 0x81, 0xE8 | dst_mod]); + buf.extend(imm.to_le_bytes()); +} + +/// `SUB r/m64,r64` -> Sub r64 to r/m64. +#[inline(always)] +fn sub_reg64_reg64(buf: &mut Vec<'_, u8>, dst: X86_64GeneralReg, src: X86_64GeneralReg) { + binop_reg64_reg64(0x29, buf, dst, src); +} + +/// `POP r64` -> Pop top of stack into r64; increment stack pointer. Cannot encode 32-bit operand size. +#[inline(always)] +fn pop_reg64(buf: &mut Vec<'_, u8>, reg: X86_64GeneralReg) { + let reg_mod = reg as u8 % 8; + if reg as u8 > 7 { + let rex = add_opcode_extension(reg, REX); + buf.extend([rex, 0x58 | reg_mod]); + } else { + buf.push(0x58 | reg_mod); + } +} + +/// `PUSH r64` -> Push r64, +#[inline(always)] +fn push_reg64(buf: &mut Vec<'_, u8>, reg: X86_64GeneralReg) { + let reg_mod = reg as u8 % 8; + if reg as u8 > 7 { + let rex = add_opcode_extension(reg, REX); + buf.extend([rex, 0x50 | reg_mod]); + } else { + buf.push(0x50 | reg_mod); + } +} + +// When writing tests, it is a good idea to test both a number and unnumbered register. +// This is because R8-R15 often have special instruction prefixes. +#[cfg(test)] +mod tests { + use super::*; + use crate::disassembler_test; + use capstone::prelude::*; + + impl X86_64GeneralReg { + #[allow(dead_code)] + fn low_32bits_string(&self) -> &str { + match self { + X86_64GeneralReg::RAX => "eax", + X86_64GeneralReg::RBX => "ebx", + X86_64GeneralReg::RCX => "ecx", + X86_64GeneralReg::RDX => "edx", + X86_64GeneralReg::RBP => "ebp", + X86_64GeneralReg::RSP => "esp", + X86_64GeneralReg::RSI => "esi", + X86_64GeneralReg::RDI => "edi", + X86_64GeneralReg::R8 => "r8d", + X86_64GeneralReg::R9 => "r9d", + X86_64GeneralReg::R10 => "r10d", + X86_64GeneralReg::R11 => "r11d", + X86_64GeneralReg::R12 => "r12d", + X86_64GeneralReg::R13 => "r13d", + X86_64GeneralReg::R14 => "r14d", + X86_64GeneralReg::R15 => "r15d", + } + } + + #[allow(dead_code)] + fn low_16bits_string(&self) -> &str { + match self { + X86_64GeneralReg::RAX => "ax", + X86_64GeneralReg::RBX => "bx", + X86_64GeneralReg::RCX => "cx", + X86_64GeneralReg::RDX => "dx", + X86_64GeneralReg::RBP => "bp", + X86_64GeneralReg::RSP => "sp", + X86_64GeneralReg::RSI => "si", + X86_64GeneralReg::RDI => "di", + X86_64GeneralReg::R8 => "r8w", + X86_64GeneralReg::R9 => "r9w", + X86_64GeneralReg::R10 => "r10w", + X86_64GeneralReg::R11 => "r11w", + X86_64GeneralReg::R12 => "r12w", + X86_64GeneralReg::R13 => "r13w", + X86_64GeneralReg::R14 => "r14w", + X86_64GeneralReg::R15 => "r15w", + } + } + + #[allow(dead_code)] + fn low_8bits_string(&self) -> &str { + match self { + X86_64GeneralReg::RAX => "al", + X86_64GeneralReg::RBX => "bl", + X86_64GeneralReg::RCX => "cl", + X86_64GeneralReg::RDX => "dl", + X86_64GeneralReg::RBP => "bpl", + X86_64GeneralReg::RSP => "spl", + X86_64GeneralReg::RSI => "sil", + X86_64GeneralReg::RDI => "dil", + + X86_64GeneralReg::R8 => "r8b", + X86_64GeneralReg::R9 => "r9b", + X86_64GeneralReg::R10 => "r10b", + X86_64GeneralReg::R11 => "r11b", + X86_64GeneralReg::R12 => "r12b", + X86_64GeneralReg::R13 => "r13b", + X86_64GeneralReg::R14 => "r14b", + X86_64GeneralReg::R15 => "r15b", + } + } + } + const TEST_I32: i32 = 0x12345678; + const TEST_I64: i64 = 0x1234_5678_9ABC_DEF0; + + const ALL_REGISTER_WIDTHS: &[RegisterWidth] = &[ + RegisterWidth::W8, + RegisterWidth::W16, + RegisterWidth::W32, + RegisterWidth::W64, + ]; + + const ALL_GENERAL_REGS: &[X86_64GeneralReg] = &[ + X86_64GeneralReg::RAX, + X86_64GeneralReg::RBX, + X86_64GeneralReg::RCX, + X86_64GeneralReg::RDX, + X86_64GeneralReg::RBP, + X86_64GeneralReg::RSP, + X86_64GeneralReg::RSI, + X86_64GeneralReg::RDI, + X86_64GeneralReg::R8, + X86_64GeneralReg::R9, + X86_64GeneralReg::R10, + X86_64GeneralReg::R11, + X86_64GeneralReg::R12, + X86_64GeneralReg::R13, + X86_64GeneralReg::R14, + X86_64GeneralReg::R15, + ]; + const ALL_FLOAT_REGS: &[X86_64FloatReg] = &[ + X86_64FloatReg::XMM0, + X86_64FloatReg::XMM1, + X86_64FloatReg::XMM2, + X86_64FloatReg::XMM3, + X86_64FloatReg::XMM4, + X86_64FloatReg::XMM5, + X86_64FloatReg::XMM6, + X86_64FloatReg::XMM7, + X86_64FloatReg::XMM8, + X86_64FloatReg::XMM9, + X86_64FloatReg::XMM10, + X86_64FloatReg::XMM11, + X86_64FloatReg::XMM12, + X86_64FloatReg::XMM13, + X86_64FloatReg::XMM14, + X86_64FloatReg::XMM15, + ]; + + fn setup_capstone_and_arena( + arena: &bumpalo::Bump, + ) -> (bumpalo::collections::Vec, Capstone) { + let buf = bumpalo::vec![in arena]; + let cs = Capstone::new() + .x86() + .mode(arch::x86::ArchMode::Mode64) + .syntax(arch::x86::ArchSyntax::Intel) + .detail(true) + .build() + .expect("Failed to create Capstone object"); + (buf, cs) + } + + #[test] + fn test_add_reg64_imm32() { + disassembler_test!( + add_reg64_imm32, + |reg, imm| format!("add {reg}, 0x{imm:x}"), + ALL_GENERAL_REGS, + [TEST_I32] + ); + } + + #[test] + fn test_add_reg64_reg64() { + disassembler_test!( + add_reg64_reg64, + |reg1, reg2| format!("add {reg1}, {reg2}"), + ALL_GENERAL_REGS, + ALL_GENERAL_REGS + ); + } + + #[test] + fn test_sub_reg64_reg64() { + disassembler_test!( + sub_reg64_reg64, + |reg1, reg2| format!("sub {reg1}, {reg2}"), + ALL_GENERAL_REGS, + ALL_GENERAL_REGS + ); + } + + #[test] + fn test_addsd_freg64_freg64() { + disassembler_test!( + addsd_freg64_freg64, + |reg1, reg2| format!("addsd {reg1}, {reg2}"), + ALL_FLOAT_REGS, + ALL_FLOAT_REGS + ); + } + + #[test] + fn test_addss_freg32_freg32() { + disassembler_test!( + addss_freg32_freg32, + |reg1, reg2| format!("addss {reg1}, {reg2}"), + ALL_FLOAT_REGS, + ALL_FLOAT_REGS + ); + } + + #[test] + fn test_andpd_freg64_freg64() { + disassembler_test!( + andpd_freg64_freg64, + |reg1, reg2| format!("andpd {reg1}, {reg2}"), + ALL_FLOAT_REGS, + ALL_FLOAT_REGS + ); + } + + #[test] + fn test_and_reg64_reg64() { + disassembler_test!( + and_reg64_reg64, + |reg1, reg2| format!("and {reg1}, {reg2}"), + ALL_GENERAL_REGS, + ALL_GENERAL_REGS + ); + } + + #[test] + fn test_or_reg64_reg64() { + disassembler_test!( + or_reg64_reg64, + |reg1, reg2| format!("or {reg1}, {reg2}"), + ALL_GENERAL_REGS, + ALL_GENERAL_REGS + ); + } + + #[test] + fn test_xor_reg64_reg64() { + disassembler_test!( + xor_reg64_reg64, + |reg1, reg2| format!("xor {reg1}, {reg2}"), + ALL_GENERAL_REGS, + ALL_GENERAL_REGS + ); + } + + #[test] + fn test_shl_reg64_reg64() { + disassembler_test!( + shl_reg64_reg64, + |reg| format!("shl {reg}, cl"), + ALL_GENERAL_REGS + ); + } + + #[test] + fn test_shr_reg64_reg64() { + disassembler_test!( + shr_reg64_reg64, + |reg| format!("shr {reg}, cl"), + ALL_GENERAL_REGS + ); + } + + #[test] + fn test_sar_reg64_reg64() { + disassembler_test!( + sar_reg64_reg64, + |reg| format!("sar {reg}, cl"), + ALL_GENERAL_REGS + ); + } + + #[test] + fn test_cmovl_reg64_reg64() { + disassembler_test!( + cmovl_reg64_reg64, + |reg1, reg2| format!("cmovl {reg1}, {reg2}"), + ALL_GENERAL_REGS, + ALL_GENERAL_REGS + ); + } + + #[test] + fn test_cmp_reg64_imm32() { + disassembler_test!( + cmp_reg64_imm32, + |reg, imm| format!("cmp {reg}, 0x{imm:x}"), + ALL_GENERAL_REGS, + [TEST_I32] + ); + } + + #[test] + fn test_imul_reg64_reg64() { + disassembler_test!( + imul_reg64_reg64, + |reg1, reg2| format!("imul {reg1}, {reg2}"), + ALL_GENERAL_REGS, + ALL_GENERAL_REGS + ); + } + + #[test] + fn test_mul_reg64_reg64() { + disassembler_test!( + mul_reg64_reg64, + |reg| format!("mul {reg}"), + ALL_GENERAL_REGS + ); + } + + #[test] + fn test_mulsd_freg64_freg64() { + disassembler_test!( + mulsd_freg64_freg64, + |reg1, reg2| format!("mulsd {reg1}, {reg2}"), + ALL_FLOAT_REGS, + ALL_FLOAT_REGS + ); + } + + #[test] + fn test_mulss_freg32_freg32() { + disassembler_test!( + mulss_freg32_freg32, + |reg1, reg2| format!("mulss {reg1}, {reg2}"), + ALL_FLOAT_REGS, + ALL_FLOAT_REGS + ); + } + + #[test] + fn test_idiv_reg64_reg64() { + disassembler_test!( + idiv_reg64_reg64, + |reg| format!("cqo\nidiv {reg}"), + ALL_GENERAL_REGS + ); + } + + #[test] + fn test_div_reg64_reg64() { + disassembler_test!( + udiv_reg64_reg64, + |reg| format!("cqo\ndiv {reg}"), + ALL_GENERAL_REGS + ); + } + + #[test] + fn test_divsd_freg64_freg64() { + disassembler_test!( + divsd_freg64_freg64, + |reg1, reg2| format!("divsd {reg1}, {reg2}"), + ALL_FLOAT_REGS, + ALL_FLOAT_REGS + ); + } + + #[test] + fn test_divss_freg32_freg32() { + disassembler_test!( + divss_freg32_freg32, + |reg1, reg2| format!("divss {reg1}, {reg2}"), + ALL_FLOAT_REGS, + ALL_FLOAT_REGS + ); + } + + #[test] + fn test_jmp_imm32() { + const INST_SIZE: i32 = 5; + disassembler_test!( + jmp_imm32, + |imm| format!("jmp 0x{:x}", imm + INST_SIZE), + [TEST_I32] + ); + } + + #[test] + fn test_jmp_reg64_offset8() { + disassembler_test!( + jmp_reg64_offset8, + |base, offset| if offset < 0x10 { + format!("jmp qword ptr [{base} + {offset:x}]") + } else { + format!("jmp qword ptr [{base} + 0x{offset:x}]") + }, + ALL_GENERAL_REGS, + [0x8, 0x10] + ); + } + + #[test] + fn test_jne_imm32() { + const INST_SIZE: i32 = 6; + disassembler_test!( + jne_imm32, + |imm| format!("jne 0x{:x}", imm + INST_SIZE), + [TEST_I32] + ); + } + + #[test] + fn test_mov_reg64_imm32() { + disassembler_test!( + mov_reg64_imm32, + |reg, imm| format!("mov {reg}, 0x{imm:x}"), + ALL_GENERAL_REGS, + [TEST_I32] + ); + } + + #[test] + fn test_mov_reg64_imm64() { + disassembler_test!( + mov_reg64_imm64, + |reg, imm| format!("movabs {reg}, 0x{imm:x}"), + ALL_GENERAL_REGS, + [TEST_I64] + ); + disassembler_test!( + mov_reg64_imm64, + |reg, imm| format!("mov {reg}, 0x{imm:x}"), + ALL_GENERAL_REGS, + [TEST_I32 as i64] + ); + } + + #[test] + fn test_lea_reg64() { + disassembler_test!( + lea_reg64, + |reg| format!("lea {reg}, [rip]"), + ALL_GENERAL_REGS + ); + } + + #[test] + fn test_lea_reg64_offset32() { + disassembler_test!( + lea_reg64_offset8, + |dst, src, offset| { + if offset < 16 { + format!("lea {dst}, [{src} + {offset:x}]") + } else { + format!("lea {dst}, [{src} + 0x{offset:x}]") + } + }, + ALL_GENERAL_REGS, + ALL_GENERAL_REGS, + [0x8i8, 0x10i8] + ); + } + + #[test] + fn test_mov_reg64_reg64() { + disassembler_test!( + raw_mov_reg_reg, + |w, reg1, reg2| { + match w { + RegisterWidth::W8 => format!( + "mov {}, {}", + X86_64GeneralReg::low_8bits_string(®1), + X86_64GeneralReg::low_8bits_string(®2) + ), + RegisterWidth::W16 => format!( + "mov {}, {}", + X86_64GeneralReg::low_16bits_string(®1), + X86_64GeneralReg::low_16bits_string(®2) + ), + RegisterWidth::W32 => format!( + "mov {}, {}", + X86_64GeneralReg::low_32bits_string(®1), + X86_64GeneralReg::low_32bits_string(®2) + ), + RegisterWidth::W64 => format!("mov {reg1}, {reg2}"), + } + }, + ALL_REGISTER_WIDTHS, + ALL_GENERAL_REGS, + ALL_GENERAL_REGS + ); + } + + #[test] + fn test_movsx_reg64_reg64() { + disassembler_test!( + raw_movsx_reg_reg, + |w, reg1, reg2| { + match w { + RegisterWidth::W8 => format!( + "movsx {}, {}", + reg1, + X86_64GeneralReg::low_8bits_string(®2) + ), + RegisterWidth::W16 => format!( + "movsx {}, {}", + reg1, + X86_64GeneralReg::low_16bits_string(®2) + ), + RegisterWidth::W32 => format!( + "movsxd {}, {}", + reg1, + X86_64GeneralReg::low_32bits_string(®2) + ), + RegisterWidth::W64 => String::new(), + } + }, + ALL_REGISTER_WIDTHS, + ALL_GENERAL_REGS, + ALL_GENERAL_REGS + ); + } + + #[test] + fn test_movzx_reg64_reg64() { + disassembler_test!( + raw_movzx_reg_reg, + |w, reg1, reg2| { + match w { + RegisterWidth::W8 => format!( + "movzx {}, {}", + reg1, + X86_64GeneralReg::low_8bits_string(®2) + ), + RegisterWidth::W16 => format!( + "movzx {}, {}", + reg1, + X86_64GeneralReg::low_16bits_string(®2) + ), + RegisterWidth::W32 => String::new(), + RegisterWidth::W64 => String::new(), + } + }, + ALL_REGISTER_WIDTHS, + ALL_GENERAL_REGS, + ALL_GENERAL_REGS + ); + } + + #[test] + fn test_movsd_freg64_base64_offset32() { + disassembler_test!( + movsd_freg64_base64_offset32, + |reg1, reg2, imm| format!("movsd {reg1}, qword ptr [{reg2} + 0x{imm:x}]"), + ALL_FLOAT_REGS, + ALL_GENERAL_REGS, + [TEST_I32] + ); + } + + #[test] + fn test_movss_freg32_base32_offset32() { + disassembler_test!( + movss_freg32_base32_offset32, + |reg1, reg2, imm| format!("movss {reg1}, dword ptr [{reg2} + 0x{imm:x}]"), + ALL_FLOAT_REGS, + ALL_GENERAL_REGS, + [TEST_I32] + ); + } + + #[test] + fn test_movsd_base64_offset32_freg64() { + disassembler_test!( + movsd_base64_offset32_freg64, + |reg1, imm, reg2| format!("movsd qword ptr [{reg1} + 0x{imm:x}], {reg2}"), + ALL_GENERAL_REGS, + [TEST_I32], + ALL_FLOAT_REGS + ); + } + + #[test] + fn test_movss_base64_offset32_freg64() { + disassembler_test!( + movss_base32_offset32_freg32, + |reg1, imm, reg2| format!("movss dword ptr [{} + 0x{:x}], {}", reg1, imm, reg2), + ALL_GENERAL_REGS, + [TEST_I32], + ALL_FLOAT_REGS + ); + } + + #[test] + fn test_mov_reg64_base64_offset32() { + disassembler_test!( + mov_reg64_base64_offset32, + |reg1, reg2, imm| format!("mov {reg1}, qword ptr [{reg2} + 0x{imm:x}]"), + ALL_GENERAL_REGS, + ALL_GENERAL_REGS, + [TEST_I32] + ); + } + + #[test] + fn test_mov_reg32_base32_offset32() { + disassembler_test!( + mov_reg32_base32_offset32, + |reg1, reg2, imm| format!( + "mov {}, dword ptr [{} + 0x{:x}]", + X86_64GeneralReg::low_32bits_string(®1), + reg2, + imm + ), + ALL_GENERAL_REGS, + ALL_GENERAL_REGS, + [TEST_I32] + ); + } + + #[test] + fn test_mov_reg16_base16_offset32() { + disassembler_test!( + mov_reg16_base16_offset32, + |reg1, reg2, imm| format!( + "mov {}, word ptr [{} + 0x{:x}]", + X86_64GeneralReg::low_16bits_string(®1), + reg2, + imm + ), + ALL_GENERAL_REGS, + ALL_GENERAL_REGS, + [TEST_I32] + ); + } + + #[test] + fn test_mov_reg8_base8_offset32() { + disassembler_test!( + mov_reg8_base8_offset32, + |reg1, reg2, imm| format!( + "mov {}, byte ptr [{} + 0x{:x}]", + X86_64GeneralReg::low_8bits_string(®1), + reg2, + imm + ), + ALL_GENERAL_REGS, + ALL_GENERAL_REGS, + [TEST_I32] + ); + } + + #[test] + fn test_mov_base64_offset32_reg64() { + disassembler_test!( + mov_base64_offset32_reg64, + |reg1, imm, reg2| format!("mov qword ptr [{reg1} + 0x{imm:x}], {reg2}"), + ALL_GENERAL_REGS, + [TEST_I32], + ALL_GENERAL_REGS + ); + } + + #[test] + fn test_mov_base32_offset32_reg32() { + disassembler_test!( + mov_base32_offset32_reg32, + |reg1, imm, reg2| format!( + "mov dword ptr [{} + 0x{:x}], {}", + reg1, + imm, + X86_64GeneralReg::low_32bits_string(®2), + ), + ALL_GENERAL_REGS, + [TEST_I32], + ALL_GENERAL_REGS + ); + } + + #[test] + fn test_mov_base16_offset32_reg16() { + disassembler_test!( + mov_base16_offset32_reg16, + |reg1, imm, reg2| format!( + "mov word ptr [{} + 0x{:x}], {}", + reg1, + imm, + X86_64GeneralReg::low_16bits_string(®2), + ), + ALL_GENERAL_REGS, + [TEST_I32], + ALL_GENERAL_REGS + ); + } + + #[test] + fn test_mov_base8_offset32_reg8() { + disassembler_test!( + mov_base8_offset32_reg8, + |reg1, imm, reg2| format!( + "mov byte ptr [{} + 0x{:x}], {}", + reg1, + imm, + X86_64GeneralReg::low_8bits_string(®2), + ), + ALL_GENERAL_REGS, + [TEST_I32], + ALL_GENERAL_REGS + ); + } + + #[test] + fn test_movsx_reg64_base32_offset32() { + disassembler_test!( + movsx_reg64_base32_offset32, + |reg1, reg2, imm| format!("movsxd {reg1}, dword ptr [{reg2} + 0x{imm:x}]"), + ALL_GENERAL_REGS, + ALL_GENERAL_REGS, + [TEST_I32] + ); + } + + #[test] + fn test_movsx_reg64_base16_offset32() { + disassembler_test!( + movsx_reg64_base16_offset32, + |reg1, reg2, imm| format!("movsx {reg1}, word ptr [{reg2} + 0x{imm:x}]"), + ALL_GENERAL_REGS, + ALL_GENERAL_REGS, + [TEST_I32] + ); + } + + #[test] + fn test_movsx_reg64_base8_offset32() { + disassembler_test!( + movsx_reg64_base8_offset32, + |reg1, reg2, imm| format!("movsx {reg1}, byte ptr [{reg2} + 0x{imm:x}]"), + ALL_GENERAL_REGS, + ALL_GENERAL_REGS, + [TEST_I32] + ); + } + + #[test] + fn test_movzx_reg64_base16_offset32() { + disassembler_test!( + movzx_reg64_base16_offset32, + |reg1, reg2, imm| format!("movzx {reg1}, word ptr [{reg2} + 0x{imm:x}]"), + ALL_GENERAL_REGS, + ALL_GENERAL_REGS, + [TEST_I32] + ); + } + + #[test] + fn test_movzx_reg64_base8_offset32() { + disassembler_test!( + movzx_reg64_base8_offset32, + |reg1, reg2, imm| format!("movzx {reg1}, byte ptr [{reg2} + 0x{imm:x}]"), + ALL_GENERAL_REGS, + ALL_GENERAL_REGS, + [TEST_I32] + ); + } + + #[test] + fn test_movd_reg32_freg32() { + disassembler_test!( + movd_reg32_freg32, + |dst: X86_64GeneralReg, src| format!("movd {}, {}", dst.low_32bits_string(), src), + ALL_GENERAL_REGS, + ALL_FLOAT_REGS + ); + } + + #[test] + fn test_movq_reg64_freg64() { + disassembler_test!( + movq_reg64_freg64, + |dst, src| format!("movq {dst}, {src}"), + ALL_GENERAL_REGS, + ALL_FLOAT_REGS + ); + } + + #[test] + fn test_movsd_freg64_freg64() { + disassembler_test!( + raw_movsd_freg64_freg64, + |reg1, reg2| format!("movsd {reg1}, {reg2}"), + ALL_FLOAT_REGS, + ALL_FLOAT_REGS + ); + } + + #[test] + fn test_movss_freg32_freg32() { + disassembler_test!( + raw_movss_freg32_freg32, + |reg1, reg2| format!("movss {reg1}, {reg2}"), + ALL_FLOAT_REGS, + ALL_FLOAT_REGS + ); + } + + #[test] + fn test_movss_freg32_rip_offset32() { + disassembler_test!( + movss_freg32_rip_offset32, + |reg, imm| format!("movss {reg}, dword ptr [rip + 0x{imm:x}]"), + ALL_FLOAT_REGS, + [TEST_I32 as u32] + ); + } + + #[test] + fn test_movsd_freg64_rip_offset32() { + disassembler_test!( + movsd_freg64_rip_offset32, + |reg, imm| format!("movsd {reg}, qword ptr [rip + 0x{imm:x}]"), + ALL_FLOAT_REGS, + [TEST_I32 as u32] + ); + } + + #[test] + fn test_neg_reg64() { + disassembler_test!(neg_reg64, |reg| format!("neg {reg}"), ALL_GENERAL_REGS); + } + + #[test] + fn test_cvtsi2_help() { + const CVTSI2SS_CODE: u8 = 0x2A; + const CVTTSS2SI_CODE: u8 = 0x2C; + disassembler_test!( + |buf, r1, r2| cvtsi2_help(buf, 0xF3, CVTSI2SS_CODE, r1, r2), + |reg1, reg2| format!("cvtsi2ss {reg1}, {reg2}"), + ALL_FLOAT_REGS, + ALL_GENERAL_REGS + ); + disassembler_test!( + |buf, r1, r2| cvtsi2_help(buf, 0xF3, CVTTSS2SI_CODE, r1, r2), + |reg1, reg2| format!("cvttss2si {reg1}, {reg2}"), + ALL_GENERAL_REGS, + ALL_FLOAT_REGS + ); + } + + #[test] + fn test_cvtsx2_help() { + const CVTSS2SD_CODE: u8 = 0x5A; + disassembler_test!( + |buf, r1, r2| cvtsi2_help(buf, 0xF3, CVTSS2SD_CODE, r1, r2), + |reg1, reg2| format!("cvtss2sd {reg1}, {reg2}"), + ALL_FLOAT_REGS, + ALL_FLOAT_REGS + ); + } + + #[test] + fn test_set_reg64_help() { + disassembler_test!( + |buf, reg| set_reg64_help(0x94, buf, reg), + |reg: X86_64GeneralReg| format!("sete {}\nand {}, 1", reg.low_8bits_string(), reg), + ALL_GENERAL_REGS + ); + } + + #[test] + fn test_ret() { + disassembler_test!(ret, || "ret"); + } + + #[test] + fn test_sub_reg64_imm32() { + disassembler_test!( + sub_reg64_imm32, + |reg, imm| format!("sub {reg}, 0x{imm:x}"), + ALL_GENERAL_REGS, + [TEST_I32] + ); + } + + #[test] + fn test_pop_reg64() { + disassembler_test!(pop_reg64, |reg| format!("pop {reg}"), ALL_GENERAL_REGS); + } + + #[test] + fn test_push_reg64() { + disassembler_test!(push_reg64, |reg| format!("push {reg}"), ALL_GENERAL_REGS); + } + + #[test] + fn test_sqrt_freg64_freg64() { + disassembler_test!( + sqrtsd_freg64_freg64, + |dst, src| format!("sqrtsd {dst}, {src}"), + ALL_FLOAT_REGS, + ALL_FLOAT_REGS + ); + } + + #[test] + fn test_sqrt_freg32_freg32() { + disassembler_test!( + sqrtss_freg32_freg32, + |dst, src| format!("sqrtss {dst}, {src}"), + ALL_FLOAT_REGS, + ALL_FLOAT_REGS + ); + } + + #[test] + fn test_int_cmp() { + disassembler_test!( + cmp_reg64_reg64, + |_, dst: X86_64GeneralReg, src: X86_64GeneralReg| format!( + "cmp {}, {}", + dst.low_8bits_string(), + src.low_8bits_string() + ), + [RegisterWidth::W8], + ALL_GENERAL_REGS, + ALL_GENERAL_REGS + ); + + disassembler_test!( + cmp_reg64_reg64, + |_, dst: X86_64GeneralReg, src: X86_64GeneralReg| format!( + "cmp {}, {}", + dst.low_16bits_string(), + src.low_16bits_string() + ), + [RegisterWidth::W16], + ALL_GENERAL_REGS, + ALL_GENERAL_REGS + ); + + disassembler_test!( + cmp_reg64_reg64, + |_, dst: X86_64GeneralReg, src: X86_64GeneralReg| format!( + "cmp {}, {}", + dst.low_32bits_string(), + src.low_32bits_string() + ), + [RegisterWidth::W32], + ALL_GENERAL_REGS, + ALL_GENERAL_REGS + ); + + disassembler_test!( + cmp_reg64_reg64, + |_, dst: X86_64GeneralReg, src: X86_64GeneralReg| format!("cmp {dst}, {src}",), + [RegisterWidth::W64], + ALL_GENERAL_REGS, + ALL_GENERAL_REGS + ); + } +} diff --git a/crates/compiler/gen_dev/src/lib.rs b/crates/compiler/gen_dev/src/lib.rs new file mode 100644 index 0000000000..cc1f9c9c62 --- /dev/null +++ b/crates/compiler/gen_dev/src/lib.rs @@ -0,0 +1,2539 @@ +//! Provides the compiler backend to generate Roc binaries fast, for a nice +//! developer experience. See [README.md](./compiler/gen_dev/README.md) for +//! more information. +#![warn(clippy::dbg_macro)] +// See github.com/roc-lang/roc/issues/800 for discussion of the large_enum_variant check. +#![allow(clippy::large_enum_variant, clippy::upper_case_acronyms)] + +use std::collections::hash_map::Entry; + +use bumpalo::{collections::Vec, Bump}; +use roc_builtins::bitcode::{self, FloatWidth, IntWidth}; +use roc_collections::all::{MutMap, MutSet}; +use roc_error_macros::{internal_error, todo_lambda_erasure}; +use roc_module::ident::ModuleName; +use roc_module::low_level::{LowLevel, LowLevelWrapperType}; +use roc_module::symbol::{Interns, ModuleId, Symbol}; +use roc_mono::code_gen_help::{CallerProc, CodeGenHelp}; +use roc_mono::ir::{ + BranchInfo, CallType, CrashTag, Expr, HigherOrderLowLevel, JoinPointId, ListLiteralElement, + Literal, ModifyRc, Param, Proc, ProcLayout, SelfRecursive, Stmt, +}; +use roc_mono::layout::{ + Builtin, InLayout, LambdaName, Layout, LayoutIds, LayoutInterner, LayoutRepr, STLayoutInterner, + TagIdIntType, UnionLayout, +}; +use roc_mono::list_element_layout; + +mod generic64; +mod object_builder; +pub use object_builder::build_module; +use roc_target::TargetInfo; +mod run_roc; + +#[derive(Debug, Clone, Copy)] +pub enum AssemblyBackendMode { + /// Assumes primitives (roc_alloc, roc_panic, etc) are provided by the host + Binary, + /// Provides a testing implementation of primitives (roc_alloc, roc_panic, etc) + Test, + /// Provides a testing implementation of primitives (roc_alloc, roc_panic, etc) + Repl, +} + +impl AssemblyBackendMode { + fn generate_allocators(self) -> bool { + match self { + AssemblyBackendMode::Binary => false, + AssemblyBackendMode::Test => true, + AssemblyBackendMode::Repl => true, + } + } + + fn generate_roc_panic(self) -> bool { + match self { + AssemblyBackendMode::Binary => false, + AssemblyBackendMode::Test => true, + AssemblyBackendMode::Repl => true, + } + } + + fn generate_roc_dbg(self) -> bool { + match self { + AssemblyBackendMode::Binary => false, + AssemblyBackendMode::Test => true, + AssemblyBackendMode::Repl => true, + } + } +} + +pub struct Env<'a> { + pub arena: &'a Bump, + pub module_id: ModuleId, + pub exposed_to_host: MutSet, + pub lazy_literals: bool, + pub mode: AssemblyBackendMode, +} + +// These relocations likely will need a length. +// They may even need more definition, but this should be at least good enough for how we will use elf. +#[derive(Debug, Clone)] +#[allow(dead_code)] +pub enum Relocation { + LocalData { + offset: u64, + // This should probably technically be a bumpalo::Vec. + // The problem is that it currently is built in a place that can't access the arena. + data: std::vec::Vec, + }, + LinkedFunction { + offset: u64, + name: String, + }, + LinkedData { + offset: u64, + name: String, + }, + JmpToReturn { + inst_loc: u64, + inst_size: u64, + offset: u64, + }, +} + +#[repr(u8)] +enum UpdateMode { + Immutable = 0, +} + +struct ListArgument<'a> { + element_layout: InLayout<'a>, + + alignment: Symbol, + element_width: Symbol, +} + +// Track when a variable is last used (and hence when it can be disregarded). This is non-trivial +// in the presence of join points. Consider this example: +// +// let len = 3 +// +// joinpoint f = \a -> +// joinpoint g = \b -> +// # len is used here +// in +// ... +// in +// ... +// +// we have to keep `len` alive until after the joinpoint goes out of scope! +#[derive(Debug, Default)] +struct LastSeenMap<'a> { + last_seen: MutMap>, + join_map: MutMap]>, +} + +impl<'a> LastSeenMap<'a> { + fn set_last_seen(&mut self, symbol: Symbol, stmt: &'a Stmt<'a>) { + self.last_seen.insert(symbol, stmt); + } + + /// scan_ast runs through the ast and fill the last seen map. + /// This must iterate through the ast in the same way that build_stmt does. i.e. then before else. + fn scan_ast(root: &'a Stmt<'a>) -> MutMap> { + let mut this: Self = Default::default(); + + this.scan_ast_help(root); + + this.last_seen + } + + fn scan_ast_help(&mut self, stmt: &'a Stmt<'a>) { + match stmt { + Stmt::Let(sym, expr, _, following) => { + self.set_last_seen(*sym, stmt); + match expr { + Expr::Literal(_) => {} + Expr::NullPointer => {} + + Expr::Call(call) => self.scan_ast_call(call, stmt), + + Expr::Tag { + arguments, reuse, .. + } => { + if let Some(ru) = reuse { + self.set_last_seen(ru.symbol, stmt); + } + + for sym in *arguments { + self.set_last_seen(*sym, stmt); + } + } + Expr::ErasedMake { value, callee } => { + value.map(|v| self.set_last_seen(v, stmt)); + self.set_last_seen(*callee, stmt); + } + Expr::ErasedLoad { symbol, field: _ } => { + self.set_last_seen(*symbol, stmt); + } + Expr::Struct(syms) => { + for sym in *syms { + self.set_last_seen(*sym, stmt); + } + } + Expr::StructAtIndex { structure, .. } => { + self.set_last_seen(*structure, stmt); + } + Expr::GetTagId { structure, .. } => { + self.set_last_seen(*structure, stmt); + } + Expr::UnionAtIndex { structure, .. } => { + self.set_last_seen(*structure, stmt); + } + Expr::GetElementPointer { structure, .. } => { + self.set_last_seen(*structure, stmt); + } + Expr::Array { elems, .. } => { + for elem in *elems { + if let ListLiteralElement::Symbol(sym) = elem { + self.set_last_seen(*sym, stmt); + } + } + } + Expr::Reset { symbol, .. } | Expr::ResetRef { symbol, .. } => { + self.set_last_seen(*symbol, stmt); + } + Expr::Alloca { initializer, .. } => { + if let Some(initializer) = initializer { + self.set_last_seen(*initializer, stmt); + } + } + Expr::RuntimeErrorFunction(_) => {} + Expr::FunctionPointer { .. } => todo_lambda_erasure!(), + Expr::EmptyArray => {} + } + self.scan_ast_help(following); + } + + Stmt::Switch { + cond_symbol, + branches, + default_branch, + .. + } => { + self.set_last_seen(*cond_symbol, stmt); + for (_, _, branch) in *branches { + self.scan_ast_help(branch); + } + self.scan_ast_help(default_branch.1); + } + Stmt::Ret(sym) => { + self.set_last_seen(*sym, stmt); + } + Stmt::Refcounting(modify, following) => { + let sym = modify.get_symbol(); + + self.set_last_seen(sym, stmt); + self.scan_ast_help(following); + } + Stmt::Join { + parameters, + body: continuation, + remainder, + id: JoinPointId(sym), + .. + } => { + self.set_last_seen(*sym, stmt); + self.join_map.insert(JoinPointId(*sym), parameters); + self.scan_ast_help(remainder); + + for (symbol, symbol_stmt) in Self::scan_ast(continuation) { + match self.last_seen.entry(symbol) { + Entry::Occupied(mut occupied) => { + // lives for the joinpoint + occupied.insert(stmt); + } + Entry::Vacant(vacant) => { + // lives for some time within the continuation + vacant.insert(symbol_stmt); + } + } + } + + for param in *parameters { + self.set_last_seen(param.symbol, stmt); + } + } + Stmt::Jump(JoinPointId(sym), symbols) => { + if let Some(parameters) = self.join_map.get(&JoinPointId(*sym)) { + // Keep the parameters around. They will be overwritten when jumping. + for param in *parameters { + self.set_last_seen(param.symbol, stmt); + } + } + for sym in *symbols { + self.set_last_seen(*sym, stmt); + } + } + + Stmt::Dbg { .. } => todo!("dbg not implemented in the dev backend"), + Stmt::Expect { .. } => todo!("expect is not implemented in the dev backend"), + Stmt::ExpectFx { .. } => todo!("expect-fx is not implemented in the dev backend"), + + Stmt::Crash(msg, _crash_tag) => { + self.set_last_seen(*msg, stmt); + } + } + } + + fn scan_ast_call(&mut self, call: &roc_mono::ir::Call, stmt: &'a roc_mono::ir::Stmt<'a>) { + let roc_mono::ir::Call { + call_type, + arguments, + } = call; + + for sym in *arguments { + self.set_last_seen(*sym, stmt); + } + + match call_type { + CallType::ByName { .. } => {} + CallType::ByPointer { .. } => {} + CallType::LowLevel { .. } => {} + CallType::HigherOrder { .. } => {} + CallType::Foreign { .. } => {} + } + } +} + +trait Backend<'a> { + fn env(&self) -> &Env<'a>; + fn interns(&self) -> &Interns; + fn interns_mut(&mut self) -> &mut Interns; + fn interner(&self) -> &STLayoutInterner<'a>; + fn relocations_mut(&mut self) -> &mut Vec<'a, Relocation>; + fn target_info(&self) -> TargetInfo; + + fn interner_mut(&mut self) -> &mut STLayoutInterner<'a> { + self.module_interns_helpers_mut().1 + } + + fn debug_symbol(&mut self, name: &str) -> Symbol { + let module_id = self.env().module_id; + + self.debug_symbol_in(module_id, name) + } + + fn debug_symbol_in(&mut self, module_id: ModuleId, name: &str) -> Symbol { + let ident_ids = self + .interns_mut() + .all_ident_ids + .get_mut(&module_id) + .unwrap(); + + let ident_id = ident_ids.add_str(name); + Symbol::new(module_id, ident_id) + } + + // This method is suboptimal, but it seems to be the only way to make rust understand + // that all of these values can be mutable at the same time. By returning them together, + // rust understands that they are part of a single use of mutable self. + fn module_interns_helpers_mut( + &mut self, + ) -> ( + ModuleId, + &mut STLayoutInterner<'a>, + &mut Interns, + &mut CodeGenHelp<'a>, + &mut Vec<'a, CallerProc<'a>>, + ); + + fn lambda_name_to_string<'b, I>( + &self, + name: LambdaName, + arguments: I, + _lambda_set: Option, + result: InLayout, + ) -> String + where + I: Iterator>, + { + use std::fmt::Write; + use std::hash::{BuildHasher, Hash, Hasher}; + + let symbol = name.name(); + + let mut buf = String::with_capacity(1024); + + for a in arguments { + write!(buf, "{:?}", self.interner().dbg_stable(a)).expect("capacity"); + } + + // lambda set should not matter; it should already be added as an argument + // but the niche of the lambda name may be the only thing differentiating two different + // implementations of a function with the same symbol + write!(buf, "{:?}", name.niche().dbg_stable(self.interner())).expect("capacity"); + + write!(buf, "{:?}", self.interner().dbg_stable(result)).expect("capacity"); + + // NOTE: due to randomness, this will not be consistent between runs + let mut state = roc_collections::all::BuildHasher::default().build_hasher(); + buf.hash(&mut state); + + let interns = self.interns(); + let ident_string = symbol.as_str(interns); + let module_string = interns.module_ids.get_name(symbol.module_id()).unwrap(); + + // the functions from the generates #help module (refcounting, equality) is always suffixed + // with 1. That is fine, they are always unique anyway. + if ident_string.contains("#help") { + format!("{module_string}_{ident_string}_1") + } else { + format!("{}_{}_{}", module_string, ident_string, state.finish()) + } + } + + fn defined_in_app_module(&self, symbol: Symbol) -> bool { + symbol + .module_string(self.interns()) + .starts_with(ModuleName::APP) + } + + fn list_argument(&mut self, list_layout: InLayout<'a>) -> ListArgument<'a> { + let element_layout = match self.interner().get_repr(list_layout) { + LayoutRepr::Builtin(Builtin::List(e)) => e, + _ => unreachable!(), + }; + + let (element_width_int, alignment_int) = + self.interner().stack_size_and_alignment(element_layout); + + let alignment = self.debug_symbol("alignment"); + self.load_literal_i32(&alignment, Ord::max(alignment_int, 8) as i32); + + let element_width = self.debug_symbol("element_width"); + self.load_literal_i64(&element_width, element_width_int as i64); + + ListArgument { + element_layout, + alignment, + element_width, + } + } + + fn increment_fn_pointer(&mut self, layout: InLayout<'a>) -> Symbol { + let box_layout = self + .interner_mut() + .insert_direct_no_semantic(LayoutRepr::Ptr(layout)); + + let element_increment = self.debug_symbol("element_increment"); + let element_increment_symbol = self.build_indirect_inc(layout); + + let element_increment_string = self.lambda_name_to_string( + LambdaName::no_niche(element_increment_symbol), + [box_layout].into_iter(), + None, + Layout::UNIT, + ); + + self.build_fn_pointer(&element_increment, element_increment_string); + + element_increment + } + + fn decrement_fn_pointer(&mut self, layout: InLayout<'a>) -> Symbol { + let box_layout = self + .interner_mut() + .insert_direct_no_semantic(LayoutRepr::Ptr(layout)); + + let element_decrement = self.debug_symbol("element_decrement"); + let element_decrement_symbol = self.build_indirect_dec(layout); + + let element_decrement_string = self.lambda_name_to_string( + LambdaName::no_niche(element_decrement_symbol), + [box_layout].into_iter(), + None, + Layout::UNIT, + ); + + self.build_fn_pointer(&element_decrement, element_decrement_string); + + element_decrement + } + + fn helper_proc_gen_mut(&mut self) -> &mut CodeGenHelp<'a>; + + fn helper_proc_symbols_mut(&mut self) -> &mut Vec<'a, (Symbol, ProcLayout<'a>)>; + + fn helper_proc_symbols(&self) -> &Vec<'a, (Symbol, ProcLayout<'a>)>; + fn caller_procs(&self) -> &Vec<'a, CallerProc<'a>>; + + /// reset resets any registers or other values that may be occupied at the end of a procedure. + /// It also passes basic procedure information to the builder for setup of the next function. + fn reset(&mut self, name: String, is_self_recursive: SelfRecursive); + + /// finalize does any setup and cleanup that should happen around the procedure. + /// finalize does setup because things like stack size and jump locations are not know until the function is written. + /// For example, this can store the frame pointer and setup stack space. + /// finalize is run at the end of build_proc when all internal code is finalized. + fn finalize(&mut self) -> (Vec, Vec); + + // load_args is used to let the backend know what the args are. + // The backend should track these args so it can use them as needed. + fn load_args(&mut self, args: &'a [(InLayout<'a>, Symbol)], ret_layout: &InLayout<'a>); + + /// Used for generating wrappers for malloc/realloc/free + fn build_wrapped_jmp(&mut self) -> (&'a [u8], u64); + + // use for roc_panic + fn build_roc_setjmp(&mut self) -> &'a [u8]; + fn build_roc_longjmp(&mut self) -> &'a [u8]; + fn build_roc_panic(&mut self) -> (&'a [u8], Vec<'a, Relocation>); + + /// build_proc creates a procedure and outputs it to the wrapped object writer. + /// Returns the procedure bytes, its relocations, and the names of the refcounting functions it references. + fn build_proc( + &mut self, + proc: Proc<'a>, + layout_ids: &mut LayoutIds<'a>, + ) -> (Vec, Vec, Vec<'a, (Symbol, String)>) { + let proc_name = self.lambda_name_to_string( + proc.name, + proc.args.iter().map(|t| t.0), + proc.closure_data_layout, + proc.ret_layout, + ); + + let body = self.env().arena.alloc(proc.body); + + self.reset(proc_name, proc.is_self_recursive); + self.load_args(proc.args, &proc.ret_layout); + for (layout, sym) in proc.args { + self.set_layout_map(*sym, layout); + } + self.scan_ast(body); + self.create_free_map(); + self.build_stmt(layout_ids, body, &proc.ret_layout); + + let mut helper_proc_names = bumpalo::vec![in self.env().arena]; + helper_proc_names.reserve(self.helper_proc_symbols().len()); + for (rc_proc_sym, rc_proc_layout) in self.helper_proc_symbols() { + let name = layout_ids + .get_toplevel(*rc_proc_sym, rc_proc_layout) + .to_symbol_string(*rc_proc_sym, self.interns()); + + helper_proc_names.push((*rc_proc_sym, name)); + } + + for caller_proc in self.caller_procs() { + let proc_layout = caller_proc.proc_layout; + let proc_symbol = caller_proc.proc_symbol; + let name = layout_ids + .get_toplevel(proc_symbol, &proc_layout) + .to_symbol_string(proc_symbol, self.interns()); + + helper_proc_names.push((proc_symbol, name)); + } + + let (bytes, relocs) = self.finalize(); + (bytes, relocs, helper_proc_names) + } + + /// build_stmt builds a statement and outputs at the end of the buffer. + fn build_stmt( + &mut self, + layout_ids: &mut LayoutIds<'a>, + stmt: &Stmt<'a>, + ret_layout: &InLayout<'a>, + ) { + match stmt { + Stmt::Let(sym, expr, layout, following) => { + self.build_expr(sym, expr, layout); + self.set_layout_map(*sym, layout); + self.free_symbols(stmt); + self.build_stmt(layout_ids, following, ret_layout); + } + Stmt::Ret(sym) => { + self.load_literal_symbols(&[*sym]); + self.return_symbol(sym, ret_layout); + self.free_symbols(stmt); + } + Stmt::Refcounting(ModifyRc::Free(symbol), following) => { + let dst = Symbol::DEV_TMP; + + let layout = *self.layout_map().get(symbol).unwrap(); + let alignment_bytes = self.interner().allocation_alignment_bytes(layout); + let alignment = self.debug_symbol("alignment"); + self.load_literal_i32(&alignment, alignment_bytes as i32); + + // NOTE: UTILS_FREE_DATA_PTR clears any tag id bits + + self.build_fn_call( + &dst, + bitcode::UTILS_FREE_DATA_PTR.to_string(), + &[*symbol, alignment], + &[Layout::I64, Layout::I32], + &Layout::UNIT, + ); + + self.free_symbol(&dst); + self.free_symbol(&alignment); + + self.build_stmt(layout_ids, following, ret_layout) + } + Stmt::Refcounting(modify, following) => { + let sym = modify.get_symbol(); + let layout = *self.layout_map().get(&sym).unwrap(); + + // Expand the Refcounting statement into more detailed IR with a function call + // If this layout requires a new RC proc, we get enough info to create a linker symbol + // for it. Here we don't create linker symbols at this time, but in Wasm backend, we do. + let (rc_stmt, new_specializations) = { + let (module_id, layout_interner, interns, rc_proc_gen, _) = + self.module_interns_helpers_mut(); + let ident_ids = interns.all_ident_ids.get_mut(&module_id).unwrap(); + + rc_proc_gen.expand_refcount_stmt( + ident_ids, + layout_interner, + layout, + modify, + following, + ) + }; + + for spec in new_specializations.into_iter() { + self.helper_proc_symbols_mut().push(spec); + } + + self.build_stmt(layout_ids, rc_stmt, ret_layout) + } + Stmt::Switch { + cond_symbol, + cond_layout, + branches, + default_branch, + ret_layout, + } => { + self.load_literal_symbols(&[*cond_symbol]); + self.build_switch( + layout_ids, + cond_symbol, + cond_layout, + branches, + default_branch, + ret_layout, + ); + self.free_symbols(stmt); + } + Stmt::Join { + id, + parameters, + body, + remainder, + } => { + for param in parameters.iter() { + self.set_layout_map(param.symbol, ¶m.layout); + } + self.build_join(layout_ids, id, parameters, body, remainder, ret_layout); + self.free_symbols(stmt); + } + Stmt::Jump(id, args) => { + self.load_literal_symbols(args); + let mut arg_layouts: bumpalo::collections::Vec> = + bumpalo::vec![in self.env().arena]; + arg_layouts.reserve(args.len()); + let layout_map = self.layout_map(); + for arg in *args { + if let Some(layout) = layout_map.get(arg) { + arg_layouts.push(*layout); + } else { + internal_error!("the argument, {:?}, has no know layout", arg); + } + } + self.build_jump(id, args, arg_layouts.into_bump_slice(), ret_layout); + self.free_symbols(stmt); + } + Stmt::Crash(msg, crash_tag) => self.roc_panic(*msg, *crash_tag), + x => todo!("the statement, {:?}", x), + } + } + + fn roc_panic(&mut self, msg: Symbol, crash_tag: CrashTag) { + let error_message = self.debug_symbol("error_message"); + + self.load_literal( + &error_message, + &Layout::U32, + &Literal::Int((crash_tag as u128).to_ne_bytes()), + ); + + // Now that the arguments are needed, load them if they are literals. + let arguments = &[msg, error_message]; + self.load_literal_symbols(arguments); + self.build_fn_call( + &Symbol::DEV_TMP2, + String::from("roc_panic"), + arguments, + &[Layout::STR, Layout::U32], + &Layout::UNIT, + ); + + self.free_symbol(&error_message); + self.free_symbol(&Symbol::DEV_TMP2); + } + + // build_switch generates a instructions for a switch statement. + fn build_switch( + &mut self, + layout_ids: &mut LayoutIds<'a>, + cond_symbol: &Symbol, + cond_layout: &InLayout<'a>, + branches: &'a [(u64, BranchInfo<'a>, Stmt<'a>)], + default_branch: &(BranchInfo<'a>, &'a Stmt<'a>), + ret_layout: &InLayout<'a>, + ); + + // build_join generates a instructions for a join statement. + fn build_join( + &mut self, + layout_ids: &mut LayoutIds<'a>, + id: &JoinPointId, + parameters: &'a [Param<'a>], + body: &'a Stmt<'a>, + remainder: &'a Stmt<'a>, + ret_layout: &InLayout<'a>, + ); + + // build_jump generates a instructions for a jump statement. + fn build_jump( + &mut self, + id: &JoinPointId, + args: &[Symbol], + arg_layouts: &[InLayout<'a>], + ret_layout: &InLayout<'a>, + ); + + /// build_expr builds the expressions for the specified symbol. + /// The builder must keep track of the symbol because it may be referred to later. + fn build_expr(&mut self, sym: &Symbol, expr: &Expr<'a>, layout: &InLayout<'a>) { + match expr { + Expr::Literal(lit) => { + if self.env().lazy_literals { + self.literal_map().insert(*sym, (lit, layout)); + } else { + self.load_literal(sym, layout, lit); + } + } + Expr::Call(roc_mono::ir::Call { + call_type, + arguments, + }) => { + match call_type { + CallType::ByName { + name: func_sym, + arg_layouts, + ret_layout, + .. + } => { + if let LowLevelWrapperType::CanBeReplacedBy(lowlevel) = + LowLevelWrapperType::from_symbol(func_sym.name()) + { + return self.build_run_low_level( + sym, + &lowlevel, + arguments, + arg_layouts, + ret_layout, + ); + } else if func_sym.name().is_builtin() { + // These builtins can be built through `build_fn_call` as well, but the + // implementation in `build_builtin` inlines some of the symbols. + return self.build_builtin( + sym, + *func_sym, + arguments, + arg_layouts, + ret_layout, + ); + } + + let fn_name = self.lambda_name_to_string( + *func_sym, + arg_layouts.iter().copied(), + None, + *ret_layout, + ); + + // Now that the arguments are needed, load them if they are literals. + self.load_literal_symbols(arguments); + self.build_fn_call(sym, fn_name, arguments, arg_layouts, ret_layout) + } + + CallType::ByPointer { .. } => { + todo_lambda_erasure!() + } + + CallType::LowLevel { op: lowlevel, .. } => { + let mut arg_layouts: bumpalo::collections::Vec> = + bumpalo::vec![in self.env().arena]; + arg_layouts.reserve(arguments.len()); + let layout_map = self.layout_map(); + for arg in *arguments { + if let Some(layout) = layout_map.get(arg) { + arg_layouts.push(*layout); + } else { + internal_error!("the argument, {:?}, has no know layout", arg); + } + } + + self.build_run_low_level( + sym, + lowlevel, + arguments, + arg_layouts.into_bump_slice(), + layout, + ) + } + CallType::HigherOrder(higher_order) => { + self.build_higher_order_lowlevel(sym, higher_order, *layout) + } + CallType::Foreign { + foreign_symbol, + ret_layout, + } => { + let mut arg_layouts: bumpalo::collections::Vec> = + bumpalo::vec![in self.env().arena]; + arg_layouts.reserve(arguments.len()); + let layout_map = self.layout_map(); + for arg in *arguments { + if let Some(layout) = layout_map.get(arg) { + arg_layouts.push(*layout); + } else { + internal_error!("the argument, {:?}, has no know layout", arg); + } + } + + self.load_literal_symbols(arguments); + self.build_fn_call( + sym, + foreign_symbol.as_str().to_string(), + arguments, + arg_layouts.into_bump_slice(), + ret_layout, + ); + } + } + } + Expr::EmptyArray => { + self.create_empty_array(sym); + } + Expr::Array { elem_layout, elems } => { + let mut syms = bumpalo::vec![in self.env().arena]; + for sym in elems.iter().filter_map(|x| match x { + ListLiteralElement::Symbol(sym) => Some(sym), + _ => None, + }) { + syms.push(*sym); + } + self.create_array(sym, elem_layout, elems); + } + Expr::Struct(fields) => { + self.load_literal_symbols(fields); + self.create_struct(sym, layout, fields); + } + Expr::StructAtIndex { + index, + field_layouts, + structure, + } => { + self.load_struct_at_index(sym, structure, *index, field_layouts); + } + Expr::UnionAtIndex { + structure, + tag_id, + union_layout, + index, + } => { + self.load_union_at_index(sym, structure, *tag_id, *index, union_layout); + } + Expr::GetElementPointer { + structure, + union_layout, + indices, + .. + } => { + debug_assert!(indices.len() >= 2); + self.load_union_field_ptr_at_index( + sym, + structure, + indices[0] as u16, + indices[1], + union_layout, + ); + } + Expr::GetTagId { + structure, + union_layout, + } => { + self.get_tag_id(sym, structure, union_layout); + } + Expr::Tag { + tag_layout, + tag_id, + arguments, + reuse, + } => { + self.load_literal_symbols(arguments); + let reuse = reuse.map(|ru| ru.symbol); + self.tag(sym, arguments, tag_layout, *tag_id, reuse); + } + Expr::NullPointer => { + self.load_literal_i64(sym, 0); + } + Expr::FunctionPointer { .. } => todo_lambda_erasure!(), + Expr::ErasedMake { .. } => todo_lambda_erasure!(), + Expr::ErasedLoad { .. } => todo_lambda_erasure!(), + Expr::Reset { symbol, .. } => { + let layout = *self.layout_map().get(symbol).unwrap(); + + // Expand the Refcounting statement into more detailed IR with a function call + // If this layout requires a new RC proc, we get enough info to create a linker symbol + // for it. Here we don't create linker symbols at this time, but in Wasm backend, we do. + let (new_expr, new_specializations) = { + let (module_id, layout_interner, interns, rc_proc_gen, _) = + self.module_interns_helpers_mut(); + let ident_ids = interns.all_ident_ids.get_mut(&module_id).unwrap(); + + rc_proc_gen.call_reset_refcount(ident_ids, layout_interner, layout, *symbol) + }; + + for spec in new_specializations.into_iter() { + self.helper_proc_symbols_mut().push(spec); + } + + self.build_expr(sym, &new_expr, &Layout::BOOL) + } + Expr::ResetRef { symbol, .. } => { + let layout = *self.layout_map().get(symbol).unwrap(); + + // Expand the Refcounting statement into more detailed IR with a function call + // If this layout requires a new RC proc, we get enough info to create a linker symbol + // for it. Here we don't create linker symbols at this time, but in Wasm backend, we do. + let (new_expr, new_specializations) = { + let (module_id, layout_interner, interns, rc_proc_gen, _) = + self.module_interns_helpers_mut(); + let ident_ids = interns.all_ident_ids.get_mut(&module_id).unwrap(); + + rc_proc_gen.call_resetref_refcount(ident_ids, layout_interner, layout, *symbol) + }; + + for spec in new_specializations.into_iter() { + self.helper_proc_symbols_mut().push(spec); + } + + self.build_expr(sym, &new_expr, &Layout::BOOL) + } + Expr::Alloca { + initializer, + element_layout, + } => { + self.build_alloca(*sym, *initializer, *element_layout); + } + Expr::RuntimeErrorFunction(_) => todo!(), + } + } + + /// build_run_low_level builds the low level opertation and outputs to the specified symbol. + /// The builder must keep track of the symbol because it may be referred to later. + fn build_run_low_level( + &mut self, + sym: &Symbol, + lowlevel: &LowLevel, + args: &'a [Symbol], + arg_layouts: &[InLayout<'a>], + ret_layout: &InLayout<'a>, + ) { + // Now that the arguments are needed, load them if they are literals. + self.load_literal_symbols(args); + match lowlevel { + LowLevel::NumAbs => { + debug_assert_eq!( + 1, + args.len(), + "NumAbs: expected to have exactly one argument" + ); + debug_assert_eq!( + arg_layouts[0], *ret_layout, + "NumAbs: expected to have the same argument and return layout" + ); + self.build_num_abs(sym, &args[0], ret_layout) + } + LowLevel::NumAdd => { + debug_assert_eq!( + 2, + args.len(), + "NumAdd: expected to have exactly two argument" + ); + debug_assert_eq!( + arg_layouts[0], arg_layouts[1], + "NumAdd: expected all arguments of to have the same layout" + ); + debug_assert_eq!( + arg_layouts[0], *ret_layout, + "NumAdd: expected to have the same argument and return layout" + ); + self.build_num_add(sym, &args[0], &args[1], ret_layout) + } + LowLevel::NumAddSaturated => { + self.build_num_add_saturated(*sym, args[0], args[1], *ret_layout); + } + LowLevel::NumAddWrap => { + debug_assert_eq!( + 2, + args.len(), + "NumAdd: expected to have exactly two argument" + ); + debug_assert_eq!( + arg_layouts[0], arg_layouts[1], + "NumAdd: expected all arguments of to have the same layout" + ); + debug_assert_eq!( + arg_layouts[0], *ret_layout, + "NumAdd: expected to have the same argument and return layout" + ); + self.build_num_add(sym, &args[0], &args[1], ret_layout) + } + LowLevel::NumAddChecked => { + self.build_num_add_checked(sym, &args[0], &args[1], &arg_layouts[0], ret_layout) + } + LowLevel::NumSubChecked => { + self.build_num_sub_checked(sym, &args[0], &args[1], &arg_layouts[0], ret_layout) + } + LowLevel::NumAcos => self.build_fn_call( + sym, + bitcode::NUM_ACOS[FloatWidth::F64].to_string(), + args, + arg_layouts, + ret_layout, + ), + LowLevel::NumAsin => self.build_fn_call( + sym, + bitcode::NUM_ASIN[FloatWidth::F64].to_string(), + args, + arg_layouts, + ret_layout, + ), + LowLevel::NumAtan => self.build_fn_call( + sym, + bitcode::NUM_ATAN[FloatWidth::F64].to_string(), + args, + arg_layouts, + ret_layout, + ), + LowLevel::NumMul => self.build_num_mul(sym, &args[0], &args[1], ret_layout), + LowLevel::NumMulWrap => self.build_num_mul_wrap(sym, &args[0], &args[1], ret_layout), + LowLevel::NumMulSaturated => { + self.build_num_mul_saturated(*sym, args[0], args[1], *ret_layout); + } + LowLevel::NumMulChecked => { + self.build_num_mul_checked(sym, &args[0], &args[1], &arg_layouts[0], ret_layout) + } + LowLevel::NumDivTruncUnchecked | LowLevel::NumDivFrac => { + debug_assert_eq!( + 2, + args.len(), + "NumDiv: expected to have exactly two argument" + ); + debug_assert_eq!( + arg_layouts[0], arg_layouts[1], + "NumDiv: expected all arguments of to have the same layout" + ); + debug_assert_eq!( + arg_layouts[0], *ret_layout, + "NumDiv: expected to have the same argument and return layout" + ); + self.build_num_div(sym, &args[0], &args[1], ret_layout) + } + + LowLevel::NumDivCeilUnchecked => { + self.build_num_div_ceil(sym, &args[0], &args[1], ret_layout) + } + LowLevel::NumRemUnchecked => self.build_num_rem(sym, &args[0], &args[1], ret_layout), + LowLevel::NumNeg => { + debug_assert_eq!( + 1, + args.len(), + "NumNeg: expected to have exactly one argument" + ); + debug_assert_eq!( + arg_layouts[0], *ret_layout, + "NumNeg: expected to have the same argument and return layout" + ); + self.build_num_neg(sym, &args[0], ret_layout) + } + LowLevel::NumPowInt => self.build_fn_call( + sym, + bitcode::NUM_POW_INT[IntWidth::I64].to_string(), + args, + arg_layouts, + ret_layout, + ), + LowLevel::NumSub => { + debug_assert_eq!( + 2, + args.len(), + "NumSub: expected to have exactly two argument" + ); + debug_assert_eq!( + arg_layouts[0], arg_layouts[1], + "NumSub: expected all arguments of to have the same layout" + ); + debug_assert_eq!( + arg_layouts[0], *ret_layout, + "NumSub: expected to have the same argument and return layout" + ); + self.build_num_sub(sym, &args[0], &args[1], ret_layout) + } + LowLevel::NumSubWrap => { + debug_assert_eq!( + 2, + args.len(), + "NumSubWrap: expected to have exactly two argument" + ); + debug_assert_eq!( + arg_layouts[0], arg_layouts[1], + "NumSubWrap: expected all arguments of to have the same layout" + ); + debug_assert_eq!( + arg_layouts[0], *ret_layout, + "NumSubWrap: expected to have the same argument and return layout" + ); + self.build_num_sub_wrap(sym, &args[0], &args[1], ret_layout) + } + LowLevel::NumSubSaturated => match self.interner().get_repr(*ret_layout) { + LayoutRepr::Builtin(Builtin::Int(int_width)) => self.build_fn_call( + sym, + bitcode::NUM_SUB_SATURATED_INT[int_width].to_string(), + args, + arg_layouts, + ret_layout, + ), + LayoutRepr::Builtin(Builtin::Float(FloatWidth::F32)) => { + self.build_num_sub(sym, &args[0], &args[1], ret_layout) + } + LayoutRepr::Builtin(Builtin::Float(FloatWidth::F64)) => { + // saturated sub is just normal sub + self.build_num_sub(sym, &args[0], &args[1], ret_layout) + } + LayoutRepr::Builtin(Builtin::Decimal) => { + // self.load_args_and_call_zig(backend, bitcode::DEC_SUB_SATURATED) + todo!() + } + _ => internal_error!("invalid return type"), + }, + LowLevel::NumBitwiseAnd => { + if let LayoutRepr::Builtin(Builtin::Int(int_width)) = + self.interner().get_repr(*ret_layout) + { + self.build_int_bitwise_and(sym, &args[0], &args[1], int_width) + } else { + internal_error!("bitwise and on a non-integer") + } + } + LowLevel::NumBitwiseOr => { + if let LayoutRepr::Builtin(Builtin::Int(int_width)) = + self.interner().get_repr(*ret_layout) + { + self.build_int_bitwise_or(sym, &args[0], &args[1], int_width) + } else { + internal_error!("bitwise or on a non-integer") + } + } + LowLevel::NumBitwiseXor => { + if let LayoutRepr::Builtin(Builtin::Int(int_width)) = + self.interner().get_repr(*ret_layout) + { + self.build_int_bitwise_xor(sym, &args[0], &args[1], int_width) + } else { + internal_error!("bitwise xor on a non-integer") + } + } + LowLevel::And => { + if let LayoutRepr::Builtin(Builtin::Bool) = self.interner().get_repr(*ret_layout) { + self.build_int_bitwise_and(sym, &args[0], &args[1], IntWidth::U8) + } else { + internal_error!("bitwise and on a non-integer") + } + } + LowLevel::Or => { + if let LayoutRepr::Builtin(Builtin::Bool) = self.interner().get_repr(*ret_layout) { + self.build_int_bitwise_or(sym, &args[0], &args[1], IntWidth::U8) + } else { + internal_error!("bitwise or on a non-integer") + } + } + LowLevel::NumShiftLeftBy => { + if let LayoutRepr::Builtin(Builtin::Int(int_width)) = + self.interner().get_repr(*ret_layout) + { + self.build_int_shift_left(sym, &args[0], &args[1], int_width) + } else { + internal_error!("shift left on a non-integer") + } + } + LowLevel::NumShiftRightBy => { + if let LayoutRepr::Builtin(Builtin::Int(int_width)) = + self.interner().get_repr(*ret_layout) + { + self.build_int_shift_right(sym, &args[0], &args[1], int_width) + } else { + internal_error!("shift right on a non-integer") + } + } + LowLevel::NumShiftRightZfBy => { + if let LayoutRepr::Builtin(Builtin::Int(int_width)) = + self.interner().get_repr(*ret_layout) + { + self.build_int_shift_right_zero_fill(sym, &args[0], &args[1], int_width) + } else { + internal_error!("shift right zero-fill on a non-integer") + } + } + LowLevel::Eq => { + debug_assert_eq!(2, args.len(), "Eq: expected to have exactly two argument"); + + let a = Layout::runtime_representation_in(arg_layouts[0], self.interner()); + let b = Layout::runtime_representation_in(arg_layouts[1], self.interner()); + + debug_assert!( + self.interner().eq_repr(a, b), + "Eq: expected all arguments to have the same layout, but {} != {}", + self.interner().dbg(a), + self.interner().dbg(b), + ); + + self.build_eq(sym, &args[0], &args[1], &arg_layouts[0]) + } + LowLevel::NotEq => { + debug_assert_eq!( + 2, + args.len(), + "NotEq: expected to have exactly two argument" + ); + + let a = Layout::runtime_representation_in(arg_layouts[0], self.interner()); + let b = Layout::runtime_representation_in(arg_layouts[1], self.interner()); + + debug_assert!( + self.interner().eq_repr(a, b), + "NotEq: expected all arguments to have the same layout, but {} != {}", + self.interner().dbg(a), + self.interner().dbg(b), + ); + debug_assert!( + self.interner().eq_repr(Layout::BOOL, *ret_layout,), + "NotEq: expected to have return layout of type Bool" + ); + self.build_neq(sym, &args[0], &args[1], &arg_layouts[0]) + } + LowLevel::Not => { + debug_assert_eq!(1, args.len(), "Not: expected to have exactly one argument"); + debug_assert!( + self.interner().eq_repr(Layout::BOOL, *ret_layout,), + "Not: expected to have return layout of type Bool" + ); + self.build_not(sym, &args[0], &arg_layouts[0]) + } + LowLevel::NumLt => { + debug_assert_eq!( + 2, + args.len(), + "NumLt: expected to have exactly two argument" + ); + debug_assert!( + self.interner().eq_repr(arg_layouts[0], arg_layouts[1],), + "NumLt: expected all arguments of to have the same layout" + ); + debug_assert!( + self.interner().eq_repr(Layout::BOOL, *ret_layout,), + "NumLt: expected to have return layout of type Bool" + ); + self.build_num_lt(sym, &args[0], &args[1], &arg_layouts[0]) + } + LowLevel::NumGt => { + debug_assert_eq!( + 2, + args.len(), + "NumGt: expected to have exactly two argument" + ); + debug_assert!( + self.interner().eq_repr(arg_layouts[0], arg_layouts[1],), + "NumGt: expected all arguments of to have the same layout" + ); + debug_assert!( + self.interner().eq_repr(Layout::BOOL, *ret_layout,), + "NumGt: expected to have return layout of type Bool" + ); + self.build_num_gt(sym, &args[0], &args[1], &arg_layouts[0]) + } + LowLevel::NumToFrac => { + debug_assert_eq!( + 1, + args.len(), + "NumToFrac: expected to have exactly one argument" + ); + + debug_assert!( + matches!(*ret_layout, Layout::F32 | Layout::F64 | Layout::DEC), + "NumToFrac: expected to have return layout of type Float" + ); + self.build_num_to_frac(sym, &args[0], &arg_layouts[0], ret_layout) + } + LowLevel::NumIsNan => { + debug_assert_eq!( + 1, + args.len(), + "NumIsNan: expected to have exactly one argument" + ); + + debug_assert!( + self.interner().eq_repr(Layout::BOOL, *ret_layout,), + "NumIsNan: expected to have return layout of type Bool" + ); + self.build_num_is_nan(sym, &args[0], &arg_layouts[0]) + } + LowLevel::NumIsInfinite => { + debug_assert_eq!( + 1, + args.len(), + "NumIsInfinite: expected to have exactly one argument" + ); + + debug_assert!( + self.interner().eq_repr(Layout::BOOL, *ret_layout,), + "NumIsInfinite: expected to have return layout of type Bool" + ); + self.build_num_is_infinite(sym, &args[0], &arg_layouts[0]) + } + LowLevel::NumIsFinite => { + debug_assert_eq!( + 1, + args.len(), + "NumIsFinite: expected to have exactly one argument" + ); + + debug_assert!( + self.interner().eq_repr(Layout::BOOL, *ret_layout,), + "NumIsFinite: expected to have return layout of type Bool" + ); + self.build_num_is_finite(sym, &args[0], &arg_layouts[0]) + } + LowLevel::NumLte => { + debug_assert_eq!( + 2, + args.len(), + "NumLte: expected to have exactly two argument" + ); + debug_assert_eq!( + arg_layouts[0], arg_layouts[1], + "NumLte: expected all arguments of to have the same layout" + ); + debug_assert!( + self.interner().eq_repr(Layout::BOOL, *ret_layout,), + "NumLte: expected to have return layout of type Bool" + ); + self.build_num_lte(sym, &args[0], &args[1], &arg_layouts[0]) + } + LowLevel::NumGte => { + debug_assert_eq!( + 2, + args.len(), + "NumGte: expected to have exactly two argument" + ); + debug_assert_eq!( + arg_layouts[0], arg_layouts[1], + "NumGte: expected all arguments of to have the same layout" + ); + debug_assert!( + self.interner().eq_repr(Layout::BOOL, *ret_layout,), + "NumGte: expected to have return layout of type Bool" + ); + self.build_num_gte(sym, &args[0], &args[1], &arg_layouts[0]) + } + LowLevel::NumLogUnchecked => { + let float_width = match arg_layouts[0] { + Layout::F64 => FloatWidth::F64, + Layout::F32 => FloatWidth::F32, + _ => unreachable!("invalid layout for sqrt"), + }; + + self.build_fn_call( + sym, + bitcode::NUM_LOG[float_width].to_string(), + args, + arg_layouts, + ret_layout, + ) + } + LowLevel::NumSqrtUnchecked => { + let float_width = match arg_layouts[0] { + Layout::F64 => FloatWidth::F64, + Layout::F32 => FloatWidth::F32, + _ => unreachable!("invalid layout for sqrt"), + }; + + self.build_num_sqrt(*sym, args[0], float_width); + } + LowLevel::NumRound => self.build_fn_call( + sym, + bitcode::NUM_ROUND_F64[IntWidth::I64].to_string(), + args, + arg_layouts, + ret_layout, + ), + LowLevel::ListLen => { + debug_assert_eq!( + 1, + args.len(), + "ListLen: expected to have exactly one argument" + ); + self.build_list_len(sym, &args[0]) + } + LowLevel::ListWithCapacity => { + debug_assert_eq!( + 1, + args.len(), + "ListWithCapacity: expected to have exactly one argument" + ); + let elem_layout = list_element_layout!(self.interner(), *ret_layout); + self.build_list_with_capacity(sym, args[0], arg_layouts[0], elem_layout, ret_layout) + } + LowLevel::ListReserve => { + debug_assert_eq!( + 2, + args.len(), + "ListReserve: expected to have exactly two arguments" + ); + self.build_list_reserve(sym, args, arg_layouts, ret_layout) + } + LowLevel::ListAppendUnsafe => { + debug_assert_eq!( + 2, + args.len(), + "ListAppendUnsafe: expected to have exactly two arguments" + ); + self.build_list_append_unsafe(sym, args, arg_layouts, ret_layout) + } + LowLevel::ListGetUnsafe => { + debug_assert_eq!( + 2, + args.len(), + "ListGetUnsafe: expected to have exactly two arguments" + ); + self.build_list_get_unsafe(sym, &args[0], &args[1], ret_layout) + } + LowLevel::ListReplaceUnsafe => { + debug_assert_eq!( + 3, + args.len(), + "ListReplaceUnsafe: expected to have exactly three arguments" + ); + self.build_list_replace_unsafe(sym, args, arg_layouts, ret_layout) + } + LowLevel::ListConcat => { + debug_assert_eq!( + 2, + args.len(), + "ListConcat: expected to have exactly two arguments" + ); + let elem_layout = list_element_layout!(self.interner(), *ret_layout); + self.build_list_concat(sym, args, arg_layouts, elem_layout, ret_layout) + } + LowLevel::ListPrepend => { + debug_assert_eq!( + 2, + args.len(), + "ListPrepend: expected to have exactly two arguments" + ); + self.build_list_prepend(sym, args, arg_layouts, ret_layout) + } + LowLevel::StrConcat => self.build_fn_call( + sym, + bitcode::STR_CONCAT.to_string(), + args, + arg_layouts, + ret_layout, + ), + LowLevel::StrJoinWith => self.build_fn_call( + sym, + bitcode::STR_JOIN_WITH.to_string(), + args, + arg_layouts, + ret_layout, + ), + LowLevel::StrSplit => self.build_fn_call( + sym, + bitcode::STR_SPLIT.to_string(), + args, + arg_layouts, + ret_layout, + ), + LowLevel::StrStartsWith => self.build_fn_call( + sym, + bitcode::STR_STARTS_WITH.to_string(), + args, + arg_layouts, + ret_layout, + ), + LowLevel::StrStartsWithScalar => self.build_fn_call( + sym, + bitcode::STR_STARTS_WITH_SCALAR.to_string(), + args, + arg_layouts, + ret_layout, + ), + LowLevel::StrAppendScalar => self.build_fn_call( + sym, + bitcode::STR_APPEND_SCALAR.to_string(), + args, + arg_layouts, + ret_layout, + ), + LowLevel::StrEndsWith => self.build_fn_call( + sym, + bitcode::STR_ENDS_WITH.to_string(), + args, + arg_layouts, + ret_layout, + ), + LowLevel::StrCountGraphemes => self.build_fn_call( + sym, + bitcode::STR_COUNT_GRAPEHEME_CLUSTERS.to_string(), + args, + arg_layouts, + ret_layout, + ), + LowLevel::StrSubstringUnsafe => self.build_fn_call( + sym, + bitcode::STR_SUBSTRING_UNSAFE.to_string(), + args, + arg_layouts, + ret_layout, + ), + LowLevel::StrToUtf8 => self.build_fn_call( + sym, + bitcode::STR_TO_UTF8.to_string(), + args, + arg_layouts, + ret_layout, + ), + LowLevel::StrCountUtf8Bytes => self.build_fn_call( + sym, + bitcode::STR_COUNT_UTF8_BYTES.to_string(), + args, + arg_layouts, + ret_layout, + ), + LowLevel::StrFromUtf8Range => { + let update_mode = self.debug_symbol("update_mode"); + self.load_literal_i8(&update_mode, UpdateMode::Immutable as i8); + + self.build_fn_call( + sym, + bitcode::STR_FROM_UTF8_RANGE.to_string(), + &[args[0], args[1], args[2], update_mode], + &[arg_layouts[0], arg_layouts[1], arg_layouts[2], Layout::U8], + ret_layout, + ) + } + LowLevel::StrRepeat => self.build_fn_call( + sym, + bitcode::STR_REPEAT.to_string(), + args, + arg_layouts, + ret_layout, + ), + LowLevel::StrTrim => self.build_fn_call( + sym, + bitcode::STR_TRIM.to_string(), + args, + arg_layouts, + ret_layout, + ), + LowLevel::StrTrimStart => self.build_fn_call( + sym, + bitcode::STR_TRIM_START.to_string(), + args, + arg_layouts, + ret_layout, + ), + LowLevel::StrTrimEnd => self.build_fn_call( + sym, + bitcode::STR_TRIM_END.to_string(), + args, + arg_layouts, + ret_layout, + ), + LowLevel::StrReserve => self.build_fn_call( + sym, + bitcode::STR_RESERVE.to_string(), + args, + arg_layouts, + ret_layout, + ), + LowLevel::StrWithCapacity => self.build_fn_call( + sym, + bitcode::STR_WITH_CAPACITY.to_string(), + args, + arg_layouts, + ret_layout, + ), + LowLevel::StrToScalars => self.build_fn_call( + sym, + bitcode::STR_TO_SCALARS.to_string(), + args, + arg_layouts, + ret_layout, + ), + LowLevel::StrGetUnsafe => self.build_fn_call( + sym, + bitcode::STR_GET_UNSAFE.to_string(), + args, + arg_layouts, + ret_layout, + ), + LowLevel::StrGetScalarUnsafe => self.build_fn_call( + sym, + bitcode::STR_GET_SCALAR_UNSAFE.to_string(), + args, + arg_layouts, + ret_layout, + ), + LowLevel::StrToNum => { + let number_layout = match self.interner().get_repr(*ret_layout) { + LayoutRepr::Struct(field_layouts) => field_layouts[0], // TODO: why is it sometimes a struct? + _ => unreachable!(), + }; + + // match on the return layout to figure out which zig builtin we need + let intrinsic = match self.interner().get_repr(number_layout) { + LayoutRepr::Builtin(Builtin::Int(int_width)) => &bitcode::STR_TO_INT[int_width], + LayoutRepr::Builtin(Builtin::Float(float_width)) => { + &bitcode::STR_TO_FLOAT[float_width] + } + LayoutRepr::Builtin(Builtin::Decimal) => bitcode::DEC_FROM_STR, + _ => unreachable!(), + }; + + self.build_fn_call(sym, intrinsic.to_string(), args, arg_layouts, ret_layout) + } + LowLevel::PtrCast => { + debug_assert_eq!( + 1, + args.len(), + "RefCountGetPtr: expected to have exactly one argument" + ); + + debug_assert_eq!( + self.interner().stack_size_and_alignment(arg_layouts[0]), + (8, 8), + "cannot pointer cast from source: {}", + self.interner().dbg(arg_layouts[0]) + ); + + debug_assert_eq!( + self.interner().stack_size_and_alignment(*ret_layout), + (8, 8), + "cannot pointer cast to target: {}", + self.interner().dbg(*ret_layout) + ); + + self.build_ptr_cast(sym, &args[0]) + } + LowLevel::PtrStore => { + let args0 = args[0]; + let args1 = args[1]; + + let element_layout = match self.interner().get_repr(arg_layouts[0]) { + LayoutRepr::Ptr(inner) => inner, + _ => unreachable!( + "cannot write to {:?} in *{args0:?} = {args1:?}", + self.interner().dbg(arg_layouts[0]) + ), + }; + + self.build_ptr_store(*sym, args0, args1, element_layout); + } + LowLevel::PtrLoad => { + self.build_ptr_load(*sym, args[0], *ret_layout); + } + + LowLevel::PtrClearTagId => { + self.build_ptr_clear_tag_id(*sym, args[0]); + } + + LowLevel::RefCountDecRcPtr => self.build_fn_call( + sym, + bitcode::UTILS_DECREF_RC_PTR.to_string(), + args, + arg_layouts, + ret_layout, + ), + LowLevel::RefCountIncRcPtr => self.build_fn_call( + sym, + bitcode::UTILS_INCREF_RC_PTR.to_string(), + args, + arg_layouts, + ret_layout, + ), + LowLevel::RefCountDecDataPtr => self.build_fn_call( + sym, + bitcode::UTILS_DECREF_DATA_PTR.to_string(), + args, + arg_layouts, + ret_layout, + ), + LowLevel::RefCountIncDataPtr => self.build_fn_call( + sym, + bitcode::UTILS_INCREF_DATA_PTR.to_string(), + args, + arg_layouts, + ret_layout, + ), + LowLevel::RefCountIsUnique => self.build_fn_call( + sym, + bitcode::UTILS_IS_UNIQUE.to_string(), + args, + arg_layouts, + ret_layout, + ), + LowLevel::SetJmp => self.build_fn_call( + sym, + String::from("roc_setjmp"), + args, + arg_layouts, + ret_layout, + ), + LowLevel::LongJmp => self.build_fn_call( + sym, + String::from("roc_longjmp"), + args, + arg_layouts, + ret_layout, + ), + LowLevel::SetLongJmpBuffer => { + self.build_data_pointer(sym, String::from("setlongjmp_buffer")); + } + LowLevel::DictPseudoSeed => self.build_fn_call( + sym, + bitcode::UTILS_DICT_PSEUDO_SEED.to_string(), + args, + arg_layouts, + ret_layout, + ), + LowLevel::NumToStr => { + let arg_layout = arg_layouts[0]; + let intrinsic = match self.interner().get_repr(arg_layout) { + LayoutRepr::Builtin(Builtin::Int(width)) => &bitcode::STR_FROM_INT[width], + LayoutRepr::Builtin(Builtin::Float(width)) => &bitcode::STR_FROM_FLOAT[width], + LayoutRepr::Builtin(Builtin::Decimal) => bitcode::DEC_TO_STR, + x => internal_error!("NumToStr is not defined for {:?}", x), + }; + + self.build_fn_call(sym, intrinsic.to_string(), args, arg_layouts, ret_layout) + } + LowLevel::StrIsEmpty => { + let intrinsic = bitcode::STR_IS_EMPTY.to_string(); + self.build_fn_call(sym, intrinsic, args, arg_layouts, ret_layout); + } + LowLevel::NumIntCast => { + let source_width = match self.interner().get_repr(arg_layouts[0]) { + LayoutRepr::Builtin(Builtin::Int(width)) => width, + _ => unreachable!(), + }; + + let target_width = match self.interner().get_repr(*ret_layout) { + LayoutRepr::Builtin(Builtin::Int(width)) => width, + _ => unreachable!(), + }; + + self.build_num_int_cast(sym, &args[0], source_width, target_width) + } + LowLevel::NumIsMultipleOf => { + let int_width = arg_layouts[0].try_int_width().unwrap(); + let intrinsic = bitcode::NUM_IS_MULTIPLE_OF[int_width].to_string(); + self.build_fn_call(sym, intrinsic, args, arg_layouts, ret_layout); + } + LowLevel::ListSublist => { + // list: RocList, + // alignment: u32, + // element_width: usize, + // start: usize, + // len: usize, + // dec: Dec, + + let list = args[0]; + let start = args[1]; + let len = args[2]; + + let list_layout = arg_layouts[0]; + let list_argument = self.list_argument(list_layout); + let element_layout = list_argument.element_layout; + + let args = [ + list, + list_argument.alignment, + list_argument.element_width, + start, + len, + self.decrement_fn_pointer(element_layout), + ]; + + let layout_usize = Layout::U64; + + let arg_layouts = [ + arg_layouts[0], + Layout::U32, + layout_usize, + arg_layouts[1], + arg_layouts[2], + layout_usize, + ]; + + let intrinsic = bitcode::LIST_SUBLIST.to_string(); + self.build_fn_call(sym, intrinsic, &args, &arg_layouts, &list_layout); + } + LowLevel::ListSwap => { + let list = args[0]; + let i = args[1]; + let j = args[2]; + + let list_layout = arg_layouts[0]; + let list_argument = self.list_argument(list_layout); + + let update_mode = self.debug_symbol("update_mode"); + self.load_literal_i8(&update_mode, UpdateMode::Immutable as i8); + + let layout_usize = Layout::U64; + + // list: RocList, + // alignment: u32, + // element_width: usize, + // index_1: usize, + // index_2: usize, + // update_mode: UpdateMode, + + self.build_fn_call( + sym, + bitcode::LIST_SWAP.to_string(), + &[ + list, + list_argument.alignment, + list_argument.element_width, + i, + j, + update_mode, + ], + &[ + list_layout, + Layout::U32, + layout_usize, + layout_usize, + layout_usize, + Layout::U8, + ], + ret_layout, + ); + } + LowLevel::ListReleaseExcessCapacity => { + let list = args[0]; + + let list_layout = arg_layouts[0]; + let list_argument = self.list_argument(list_layout); + + let update_mode = self.debug_symbol("update_mode"); + self.load_literal_i8(&update_mode, UpdateMode::Immutable as i8); + + let layout_usize = Layout::U64; + + // list: RocList, + // alignment: u32, + // element_width: usize, + // update_mode: UpdateMode, + + self.build_fn_call( + sym, + bitcode::LIST_RELEASE_EXCESS_CAPACITY.to_string(), + &[ + list, + list_argument.alignment, + list_argument.element_width, + update_mode, + ], + &[list_layout, Layout::U32, layout_usize, Layout::U8], + ret_layout, + ); + } + + LowLevel::ListDropAt => { + let list = args[0]; + let drop_index = args[1]; + + let list_layout = arg_layouts[0]; + let list_argument = self.list_argument(list_layout); + let element_layout = list_argument.element_layout; + + let update_mode = self.debug_symbol("update_mode"); + self.load_literal_i8(&update_mode, UpdateMode::Immutable as i8); + + let layout_usize = Layout::U64; + let element_decrement = self.decrement_fn_pointer(element_layout); + + // list: RocList, + // alignment: u32, + // element_width: usize, + // drop_index: usize, + // dec: Dec, + + self.build_fn_call( + sym, + bitcode::LIST_DROP_AT.to_string(), + &[ + list, + list_argument.alignment, + list_argument.element_width, + drop_index, + element_decrement, + ], + &[ + list_layout, + Layout::U32, + layout_usize, + layout_usize, + layout_usize, + ], + ret_layout, + ); + } + + LowLevel::NumCompare => { + self.build_num_cmp(sym, &args[0], &args[1], &arg_layouts[0]); + } + + x => todo!("low level, {:?}", x), + } + } + + /// Builds a builtin functions that do not map directly to a low level + /// If the builtin is simple enough, it will be inlined. + fn build_builtin( + &mut self, + sym: &Symbol, + func_name: LambdaName, + args: &'a [Symbol], + arg_layouts: &[InLayout<'a>], + ret_layout: &InLayout<'a>, + ) { + match func_name.name() { + Symbol::NUM_IS_ZERO => { + debug_assert_eq!( + 1, + args.len(), + "NumIsZero: expected to have exactly one argument" + ); + debug_assert!( + self.interner().eq_repr(Layout::BOOL, *ret_layout,), + "NumIsZero: expected to have return layout of type Bool" + ); + + self.load_literal_symbols(args); + self.load_literal( + &Symbol::DEV_TMP, + &arg_layouts[0], + &Literal::Int(0i128.to_ne_bytes()), + ); + self.build_eq(sym, &args[0], &Symbol::DEV_TMP, &arg_layouts[0]); + self.free_symbol(&Symbol::DEV_TMP) + } + Symbol::LIST_GET + | Symbol::LIST_SET + | Symbol::LIST_REPLACE + | Symbol::LIST_APPEND + | Symbol::LIST_APPEND_IF_OK + | Symbol::LIST_PREPEND_IF_OK => { + // TODO: This is probably simple enough to be worth inlining. + let fn_name = self.lambda_name_to_string( + func_name, + arg_layouts.iter().copied(), + None, + *ret_layout, + ); + // Now that the arguments are needed, load them if they are literals. + self.load_literal_symbols(args); + self.build_fn_call(sym, fn_name, args, arg_layouts, ret_layout) + } + Symbol::BOOL_TRUE => { + const LITERAL: &Literal<'static> = &Literal::Bool(true); + const BOOL_LAYOUT: &InLayout<'static> = &Layout::BOOL; + + if self.env().lazy_literals { + self.literal_map().insert(*sym, (LITERAL, BOOL_LAYOUT)); + } else { + self.load_literal(sym, BOOL_LAYOUT, LITERAL); + } + } + Symbol::BOOL_FALSE => { + const LITERAL: &Literal<'static> = &Literal::Bool(false); + const BOOL_LAYOUT: &InLayout<'static> = &Layout::BOOL; + + if self.env().lazy_literals { + self.literal_map().insert(*sym, (LITERAL, BOOL_LAYOUT)); + } else { + self.load_literal(sym, BOOL_LAYOUT, LITERAL); + } + } + Symbol::STR_IS_VALID_SCALAR => { + // just call the function + let fn_name = self.lambda_name_to_string( + func_name, + arg_layouts.iter().copied(), + None, + *ret_layout, + ); + // Now that the arguments are needed, load them if they are literals. + self.load_literal_symbols(args); + self.build_fn_call(sym, fn_name, args, arg_layouts, ret_layout) + } + _other => { + // just call the function + let fn_name = self.lambda_name_to_string( + func_name, + arg_layouts.iter().copied(), + None, + *ret_layout, + ); + // Now that the arguments are needed, load them if they are literals. + self.load_literal_symbols(args); + self.build_fn_call(sym, fn_name, args, arg_layouts, ret_layout) + } + } + } + + /// build_fn_call creates a call site for a function. + /// This includes dealing with things like saving regs and propagating the returned value. + fn build_fn_call( + &mut self, + dst: &Symbol, + fn_name: String, + args: &[Symbol], + arg_layouts: &[InLayout<'a>], + ret_layout: &InLayout<'a>, + ); + + fn build_fn_pointer(&mut self, dst: &Symbol, fn_name: String); + fn build_data_pointer(&mut self, dst: &Symbol, data_name: String); + + /// Move a returned value into `dst` + fn move_return_value(&mut self, dst: &Symbol, ret_layout: &InLayout<'a>); + + /// build_num_abs stores the absolute value of src into dst. + fn build_num_int_cast( + &mut self, + dst: &Symbol, + src: &Symbol, + source: IntWidth, + target: IntWidth, + ); + + /// build_num_abs stores the absolute value of src into dst. + fn build_num_abs(&mut self, dst: &Symbol, src: &Symbol, layout: &InLayout<'a>); + + /// build_num_add stores the sum of src1 and src2 into dst. + fn build_num_add(&mut self, dst: &Symbol, src1: &Symbol, src2: &Symbol, layout: &InLayout<'a>); + + /// build_num_add_saturated stores the sum of src1 and src2 into dst. + fn build_num_add_saturated( + &mut self, + dst: Symbol, + src1: Symbol, + src2: Symbol, + layout: InLayout<'a>, + ); + + /// build_num_add_checked stores the sum of src1 and src2 into dst. + fn build_num_add_checked( + &mut self, + dst: &Symbol, + src1: &Symbol, + src2: &Symbol, + num_layout: &InLayout<'a>, + return_layout: &InLayout<'a>, + ); + + /// build_num_sub_checked stores the sum of src1 and src2 into dst. + fn build_num_sub_checked( + &mut self, + dst: &Symbol, + src1: &Symbol, + src2: &Symbol, + num_layout: &InLayout<'a>, + return_layout: &InLayout<'a>, + ); + + /// build_num_mul stores `src1 * src2` into dst. + fn build_num_mul(&mut self, dst: &Symbol, src1: &Symbol, src2: &Symbol, layout: &InLayout<'a>); + + /// build_num_mul_wrap stores `src1 * src2` into dst. + fn build_num_mul_wrap( + &mut self, + dst: &Symbol, + src1: &Symbol, + src2: &Symbol, + layout: &InLayout<'a>, + ); + + fn build_num_mul_saturated( + &mut self, + dst: Symbol, + src1: Symbol, + src2: Symbol, + layout: InLayout<'a>, + ); + + fn build_num_mul_checked( + &mut self, + dst: &Symbol, + src1: &Symbol, + src2: &Symbol, + num_layout: &InLayout<'a>, + return_layout: &InLayout<'a>, + ); + + /// build_num_mul stores `src1 / src2` into dst. + fn build_num_div(&mut self, dst: &Symbol, src1: &Symbol, src2: &Symbol, layout: &InLayout<'a>); + + fn build_num_div_ceil( + &mut self, + dst: &Symbol, + src1: &Symbol, + src2: &Symbol, + layout: &InLayout<'a>, + ); + + /// build_num_mul stores `src1 % src2` into dst. + fn build_num_rem(&mut self, dst: &Symbol, src1: &Symbol, src2: &Symbol, layout: &InLayout<'a>); + + /// build_num_neg stores the negated value of src into dst. + fn build_num_neg(&mut self, dst: &Symbol, src: &Symbol, layout: &InLayout<'a>); + + /// build_num_sub stores the `src1 - src2` difference into dst. + fn build_num_sub(&mut self, dst: &Symbol, src1: &Symbol, src2: &Symbol, layout: &InLayout<'a>); + + /// build_num_sub_wrap stores the `src1 - src2` difference into dst. + fn build_num_sub_wrap( + &mut self, + dst: &Symbol, + src1: &Symbol, + src2: &Symbol, + layout: &InLayout<'a>, + ); + + /// stores the `src1 & src2` into dst. + fn build_int_bitwise_and( + &mut self, + dst: &Symbol, + src1: &Symbol, + src2: &Symbol, + int_width: IntWidth, + ); + + /// stores the `src1 | src2` into dst. + fn build_int_bitwise_or( + &mut self, + dst: &Symbol, + src1: &Symbol, + src2: &Symbol, + int_width: IntWidth, + ); + + /// stores the `src1 ^ src2` into dst. + fn build_int_bitwise_xor( + &mut self, + dst: &Symbol, + src1: &Symbol, + src2: &Symbol, + int_width: IntWidth, + ); + + /// stores the `Num.shiftLeftBy src1 src2` into dst. + fn build_int_shift_left( + &mut self, + dst: &Symbol, + src1: &Symbol, + src2: &Symbol, + int_width: IntWidth, + ); + + /// stores the `Num.shiftRightBy src1 src2` into dst. + fn build_int_shift_right( + &mut self, + dst: &Symbol, + src1: &Symbol, + src2: &Symbol, + int_width: IntWidth, + ); + + /// stores the `Num.shiftRightZfBy src1 src2` into dst. + fn build_int_shift_right_zero_fill( + &mut self, + dst: &Symbol, + src1: &Symbol, + src2: &Symbol, + int_width: IntWidth, + ); + + /// build_eq stores the result of `src1 == src2` into dst. + fn build_eq(&mut self, dst: &Symbol, src1: &Symbol, src2: &Symbol, arg_layout: &InLayout<'a>); + + /// build_neq stores the result of `src1 != src2` into dst. + fn build_neq(&mut self, dst: &Symbol, src1: &Symbol, src2: &Symbol, arg_layout: &InLayout<'a>); + + /// build_not stores the result of `!src` into dst. + fn build_not(&mut self, dst: &Symbol, src: &Symbol, arg_layout: &InLayout<'a>); + + fn build_num_cmp( + &mut self, + dst: &Symbol, + src1: &Symbol, + src2: &Symbol, + arg_layout: &InLayout<'a>, + ); + + /// build_num_lt stores the result of `src1 < src2` into dst. + fn build_num_lt( + &mut self, + dst: &Symbol, + src1: &Symbol, + src2: &Symbol, + arg_layout: &InLayout<'a>, + ); + + /// build_num_gt stores the result of `src1 > src2` into dst. + fn build_num_gt( + &mut self, + dst: &Symbol, + src1: &Symbol, + src2: &Symbol, + arg_layout: &InLayout<'a>, + ); + + /// build_num_to_frac convert Number to Frac + fn build_num_to_frac( + &mut self, + dst: &Symbol, + src: &Symbol, + arg_layout: &InLayout<'a>, + ret_layout: &InLayout<'a>, + ); + + /// build_num_is_nan check is a Frac is NaN + fn build_num_is_nan(&mut self, dst: &Symbol, src: &Symbol, arg_layout: &InLayout<'a>); + + /// build_num_is_infinite check is a Frac is infinite + fn build_num_is_infinite(&mut self, dst: &Symbol, src: &Symbol, arg_layout: &InLayout<'a>); + + /// build_num_is_finite check is a Frac is finite + fn build_num_is_finite(&mut self, dst: &Symbol, src: &Symbol, arg_layout: &InLayout<'a>); + + /// build_num_lte stores the result of `src1 <= src2` into dst. + fn build_num_lte( + &mut self, + dst: &Symbol, + src1: &Symbol, + src2: &Symbol, + arg_layout: &InLayout<'a>, + ); + + /// build_num_gte stores the result of `src1 >= src2` into dst. + fn build_num_gte( + &mut self, + dst: &Symbol, + src1: &Symbol, + src2: &Symbol, + arg_layout: &InLayout<'a>, + ); + + /// build_sqrt stores the result of `sqrt(src)` into dst. + fn build_num_sqrt(&mut self, dst: Symbol, src: Symbol, float_width: FloatWidth); + + /// build_list_len returns the length of a list. + fn build_list_len(&mut self, dst: &Symbol, list: &Symbol); + + /// generate a call to a higher-order lowlevel + fn build_higher_order_lowlevel( + &mut self, + dst: &Symbol, + holl: &HigherOrderLowLevel<'a>, + ret_layout: InLayout<'a>, + ); + + fn build_indirect_inc(&mut self, layout: InLayout<'a>) -> Symbol; + fn build_indirect_dec(&mut self, layout: InLayout<'a>) -> Symbol; + + /// build_list_with_capacity creates and returns a list with the given capacity. + fn build_list_with_capacity( + &mut self, + dst: &Symbol, + capacity: Symbol, + capacity_layout: InLayout<'a>, + elem_layout: InLayout<'a>, + ret_layout: &InLayout<'a>, + ); + + /// build_list_reserve enlarges a list to at least accommodate the given capacity. + fn build_list_reserve( + &mut self, + dst: &Symbol, + args: &'a [Symbol], + arg_layouts: &[InLayout<'a>], + ret_layout: &InLayout<'a>, + ); + + /// build_list_append_unsafe returns a new list with a given element appended. + fn build_list_append_unsafe( + &mut self, + dst: &Symbol, + args: &'a [Symbol], + arg_layouts: &[InLayout<'a>], + ret_layout: &InLayout<'a>, + ); + + /// build_list_get_unsafe loads the element from the list at the index. + fn build_list_get_unsafe( + &mut self, + dst: &Symbol, + list: &Symbol, + index: &Symbol, + ret_layout: &InLayout<'a>, + ); + + /// build_list_replace_unsafe returns the old element and new list with the list having the new element inserted. + fn build_list_replace_unsafe( + &mut self, + dst: &Symbol, + args: &'a [Symbol], + arg_layouts: &[InLayout<'a>], + ret_layout: &InLayout<'a>, + ); + + /// build_list_concat returns a new list containing the two argument lists concatenated. + fn build_list_concat( + &mut self, + dst: &Symbol, + args: &'a [Symbol], + arg_layouts: &[InLayout<'a>], + elem_layout: InLayout<'a>, + ret_layout: &InLayout<'a>, + ); + + /// build_list_prepend returns a new list with a given element prepended. + fn build_list_prepend( + &mut self, + dst: &Symbol, + args: &'a [Symbol], + arg_layouts: &[InLayout<'a>], + ret_layout: &InLayout<'a>, + ); + + /// build_refcount_getptr loads the pointer to the reference count of src into dst. + fn build_ptr_cast(&mut self, dst: &Symbol, src: &Symbol); + + fn build_ptr_store( + &mut self, + sym: Symbol, + ptr: Symbol, + value: Symbol, + element_layout: InLayout<'a>, + ); + + fn build_ptr_load(&mut self, sym: Symbol, ptr: Symbol, element_layout: InLayout<'a>); + + fn build_ptr_clear_tag_id(&mut self, sym: Symbol, ptr: Symbol); + + fn build_alloca(&mut self, sym: Symbol, value: Option, element_layout: InLayout<'a>); + + /// literal_map gets the map from symbol to literal and layout, used for lazy loading and literal folding. + fn literal_map(&mut self) -> &mut MutMap, *const InLayout<'a>)>; + + fn load_literal_symbols(&mut self, syms: &[Symbol]) { + if self.env().lazy_literals { + for sym in syms { + if let Some((lit, layout)) = self.literal_map().remove(sym) { + // This operation is always safe but complicates lifetimes. + // The map is reset when building a procedure and then used for that single procedure. + // Since the lifetime is shorter than the entire backend, we need to use a pointer. + let (lit, layout) = unsafe { (*lit, *layout) }; + self.load_literal(sym, &layout, &lit); + } + } + } + } + + /// load_literal sets a symbol to be equal to a literal. + fn load_literal(&mut self, sym: &Symbol, layout: &InLayout<'a>, lit: &Literal<'a>); + + fn load_literal_i64(&mut self, sym: &Symbol, value: i64) { + let literal = Literal::Int((value as i128).to_ne_bytes()); + + self.load_literal(sym, &Layout::I64, &literal) + } + + fn load_literal_i32(&mut self, sym: &Symbol, value: i32) { + let literal = Literal::Int((value as i128).to_ne_bytes()); + + self.load_literal(sym, &Layout::I32, &literal) + } + + fn load_literal_i16(&mut self, sym: &Symbol, value: i16) { + let literal = Literal::Int((value as i128).to_ne_bytes()); + + self.load_literal(sym, &Layout::I16, &literal) + } + + fn load_literal_i8(&mut self, sym: &Symbol, value: i8) { + let literal = Literal::Int((value as i128).to_ne_bytes()); + + self.load_literal(sym, &Layout::I8, &literal) + } + + /// create_empty_array creates an empty array with nullptr, zero length, and zero capacity. + fn create_empty_array(&mut self, sym: &Symbol); + + /// create_array creates an array filling it with the specified objects. + fn create_array( + &mut self, + sym: &Symbol, + elem_layout: &InLayout<'a>, + elems: &'a [ListLiteralElement<'a>], + ); + + /// create_struct creates a struct with the elements specified loaded into it as data. + fn create_struct(&mut self, sym: &Symbol, layout: &InLayout<'a>, fields: &'a [Symbol]); + + /// load_struct_at_index loads into `sym` the value at `index` in `structure`. + fn load_struct_at_index( + &mut self, + sym: &Symbol, + structure: &Symbol, + index: u64, + field_layouts: &'a [InLayout<'a>], + ); + + /// load_union_at_index loads into `sym` the value at `index` for `tag_id`. + fn load_union_at_index( + &mut self, + sym: &Symbol, + structure: &Symbol, + tag_id: TagIdIntType, + index: u64, + union_layout: &UnionLayout<'a>, + ); + + /// load_union_at_index loads into `sym` the value at `index` for `tag_id`. + fn load_union_field_ptr_at_index( + &mut self, + sym: &Symbol, + structure: &Symbol, + tag_id: TagIdIntType, + index: u64, + union_layout: &UnionLayout<'a>, + ); + + /// get_tag_id loads the tag id from a the union. + fn get_tag_id(&mut self, sym: &Symbol, structure: &Symbol, union_layout: &UnionLayout<'a>); + + /// tag sets the tag for a union. + fn tag( + &mut self, + sym: &Symbol, + args: &'a [Symbol], + tag_layout: &UnionLayout<'a>, + tag_id: TagIdIntType, + reuse: Option, + ); + + /// load a value from a pointer + fn expr_unbox(&mut self, sym: Symbol, ptr: Symbol, element_layout: InLayout<'a>); + + /// store a refcounted value on the heap + fn expr_box( + &mut self, + sym: Symbol, + value: Symbol, + element_layout: InLayout<'a>, + reuse: Option, + ); + + /// return_symbol moves a symbol to the correct return location for the backend and adds a jump to the end of the function. + fn return_symbol(&mut self, sym: &Symbol, layout: &InLayout<'a>); + + /// free_symbols will free all symbols for the given statement. + fn free_symbols(&mut self, stmt: &Stmt<'a>) { + if let Some(syms) = self.free_map().remove(&(stmt as *const Stmt<'a>)) { + for sym in syms { + // println!("Freeing symbol: {:?}", sym); + self.free_symbol(&sym); + } + } + } + + /// free_symbol frees any registers or stack space used to hold a symbol. + fn free_symbol(&mut self, sym: &Symbol); + + /// last_seen_map gets the map from symbol to when it is last seen in the function. + fn last_seen_map(&mut self) -> &mut MutMap>; + + /// set_layout_map sets the layout for a specific symbol. + fn set_layout_map(&mut self, sym: Symbol, layout: &InLayout<'a>) { + if let Some(old_layout) = self.layout_map().insert(sym, *layout) { + // Layout map already contains the symbol. We should never need to overwrite. + // If the layout is not the same, that is a bug. + if &old_layout != layout { + internal_error!( + "Overwriting layout for symbol, {:?}: got {:?}, want {:?}", + sym, + layout, + old_layout + ) + } + } + } + + /// layout_map gets the map from symbol to layout. + fn layout_map(&mut self) -> &mut MutMap>; + + fn create_free_map(&mut self) { + let mut free_map = MutMap::default(); + let arena = self.env().arena; + for (sym, stmt) in self.last_seen_map() { + let vals = free_map + .entry(*stmt) + .or_insert_with(|| bumpalo::vec![in arena]); + vals.push(*sym); + } + self.set_free_map(free_map); + } + + /// free_map gets the map statement to the symbols that are free after they run. + fn free_map(&mut self) -> &mut MutMap<*const Stmt<'a>, Vec<'a, Symbol>>; + + /// set_free_map sets the free map to the given map. + fn set_free_map(&mut self, map: MutMap<*const Stmt<'a>, Vec<'a, Symbol>>); + + /// scan_ast runs through the ast and fill the last seen map. + /// This must iterate through the ast in the same way that build_stmt does. i.e. then before else. + fn scan_ast(&mut self, stmt: &'a Stmt<'a>) { + *self.last_seen_map() = LastSeenMap::scan_ast(stmt); + } +} diff --git a/crates/compiler/gen_dev/src/object_builder.rs b/crates/compiler/gen_dev/src/object_builder.rs new file mode 100644 index 0000000000..79e5e3356d --- /dev/null +++ b/crates/compiler/gen_dev/src/object_builder.rs @@ -0,0 +1,1081 @@ +use crate::generic64::{aarch64, new_backend_64bit, x86_64}; +use crate::{AssemblyBackendMode, Backend, Env, Relocation}; +use bumpalo::collections::Vec; +use object::write::{self, SectionId, SymbolId}; +use object::write::{Object, StandardSection, StandardSegment, Symbol, SymbolSection}; +use object::{ + Architecture, BinaryFormat, Endianness, RelocationEncoding, RelocationKind, SectionKind, + SymbolFlags, SymbolKind, SymbolScope, +}; +use roc_collections::all::MutMap; +use roc_error_macros::internal_error; +use roc_module::symbol; +use roc_module::symbol::Interns; +use roc_mono::ir::{Call, CallSpecId, Expr, UpdateModeId}; +use roc_mono::ir::{Proc, ProcLayout, Stmt}; +use roc_mono::layout::{LambdaName, Layout, LayoutIds, LayoutInterner, STLayoutInterner}; +use roc_target::TargetInfo; +use target_lexicon::{Architecture as TargetArch, BinaryFormat as TargetBF, Triple}; + +// This is used by some code below which is currently commented out. +// See that code for more details! +// const VERSION: &str = env!("CARGO_PKG_VERSION"); + +/// build_module is the high level builder/delegator. +/// It takes the request to build a module and output the object file for the module. +pub fn build_module<'a, 'r>( + env: &'r Env<'a>, + interns: &'r mut Interns, + layout_interner: &'r mut STLayoutInterner<'a>, + target: &Triple, + procedures: MutMap<(symbol::Symbol, ProcLayout<'a>), Proc<'a>>, +) -> Object<'a> { + match target { + Triple { + architecture: TargetArch::X86_64, + binary_format: TargetBF::Elf, + .. + } if cfg!(feature = "target-x86_64") => { + let backend = new_backend_64bit::< + x86_64::X86_64GeneralReg, + x86_64::X86_64FloatReg, + x86_64::X86_64Assembler, + x86_64::X86_64SystemV, + >(env, TargetInfo::default_x86_64(), interns, layout_interner); + // Newer version of `ld` require `.note.GNU-stack` for security reasons. + // It specifies that we will not execute code stored on the stack. + let mut object = + Object::new(BinaryFormat::Elf, Architecture::X86_64, Endianness::Little); + object.add_section( + vec![], + b".note.GNU-stack".to_vec(), + SectionKind::Elf(object::elf::SHT_PROGBITS), + ); + build_object(procedures, backend, object) + } + Triple { + architecture: TargetArch::X86_64, + binary_format: TargetBF::Macho, + .. + } if cfg!(feature = "target-x86_64") => { + let backend = new_backend_64bit::< + x86_64::X86_64GeneralReg, + x86_64::X86_64FloatReg, + x86_64::X86_64Assembler, + x86_64::X86_64SystemV, + >(env, TargetInfo::default_x86_64(), interns, layout_interner); + build_object( + procedures, + backend, + Object::new( + BinaryFormat::MachO, + Architecture::X86_64, + Endianness::Little, + ), + ) + } + Triple { + architecture: TargetArch::X86_64, + binary_format: TargetBF::Coff, + .. + } if cfg!(feature = "target-x86_64") => { + let backend = new_backend_64bit::< + x86_64::X86_64GeneralReg, + x86_64::X86_64FloatReg, + x86_64::X86_64Assembler, + x86_64::X86_64WindowsFastcall, + >(env, TargetInfo::default_x86_64(), interns, layout_interner); + build_object( + procedures, + backend, + Object::new(BinaryFormat::Coff, Architecture::X86_64, Endianness::Little), + ) + } + Triple { + architecture: TargetArch::Aarch64(_), + binary_format: TargetBF::Elf, + .. + } if cfg!(feature = "target-aarch64") => { + let backend = + new_backend_64bit::< + aarch64::AArch64GeneralReg, + aarch64::AArch64FloatReg, + aarch64::AArch64Assembler, + aarch64::AArch64Call, + >(env, TargetInfo::default_aarch64(), interns, layout_interner); + build_object( + procedures, + backend, + Object::new(BinaryFormat::Elf, Architecture::Aarch64, Endianness::Little), + ) + } + Triple { + architecture: TargetArch::Aarch64(_), + binary_format: TargetBF::Macho, + .. + } if cfg!(feature = "target-aarch64") => { + let backend = + new_backend_64bit::< + aarch64::AArch64GeneralReg, + aarch64::AArch64FloatReg, + aarch64::AArch64Assembler, + aarch64::AArch64Call, + >(env, TargetInfo::default_aarch64(), interns, layout_interner); + build_object( + procedures, + backend, + Object::new( + BinaryFormat::MachO, + Architecture::Aarch64, + Endianness::Little, + ), + ) + } + x => unimplemented!("the target, {:?}", x), + } +} + +fn define_setlongjmp_buffer(output: &mut Object) -> SymbolId { + let bss_section = output.section_id(StandardSection::Data); + + // 8 registers + 3 words for a RocStr + // TODO 50 is the wrong size here, look at implementation and put correct value in here + const SIZE: usize = (8 + 50) * core::mem::size_of::(); + + let symbol = Symbol { + name: b"setlongjmp_buffer".to_vec(), + value: 0, + size: SIZE as u64, + kind: SymbolKind::Data, + scope: SymbolScope::Linkage, + weak: false, + section: SymbolSection::Section(bss_section), + flags: SymbolFlags::None, + }; + + let symbol_id = output.add_symbol(symbol); + output.add_symbol_data(symbol_id, bss_section, &[0x00; SIZE], 8); + + symbol_id +} + +fn generate_setjmp<'a, B: Backend<'a>>(backend: &mut B, output: &mut Object) { + let text_section = output.section_id(StandardSection::Text); + let proc_symbol = Symbol { + name: b"roc_setjmp".to_vec(), + value: 0, + size: 0, + kind: SymbolKind::Text, + scope: SymbolScope::Dynamic, + weak: false, + section: SymbolSection::Section(text_section), + flags: SymbolFlags::None, + }; + let proc_id = output.add_symbol(proc_symbol); + let proc_data = backend.build_roc_setjmp(); + + output.add_symbol_data(proc_id, text_section, proc_data, 16); +} + +fn generate_longjmp<'a, B: Backend<'a>>(backend: &mut B, output: &mut Object) { + let text_section = output.section_id(StandardSection::Text); + let proc_symbol = Symbol { + name: b"roc_longjmp".to_vec(), + value: 0, + size: 0, + kind: SymbolKind::Text, + scope: SymbolScope::Dynamic, + weak: false, + section: SymbolSection::Section(text_section), + flags: SymbolFlags::None, + }; + let proc_id = output.add_symbol(proc_symbol); + let proc_data = backend.build_roc_longjmp(); + + output.add_symbol_data(proc_id, text_section, proc_data, 16); +} + +// a roc_panic to be used in tests; relies on setjmp/longjmp +fn generate_roc_panic<'a, B: Backend<'a>>(backend: &mut B, output: &mut Object) { + let text_section = output.section_id(StandardSection::Text); + let proc_symbol = Symbol { + name: b"roc_panic".to_vec(), + value: 0, + size: 0, + kind: SymbolKind::Text, + scope: SymbolScope::Dynamic, + weak: false, + section: SymbolSection::Section(text_section), + flags: SymbolFlags::None, + }; + let proc_id = output.add_symbol(proc_symbol); + let (proc_data, relocs) = backend.build_roc_panic(); + + let proc_offset = output.add_symbol_data(proc_id, text_section, proc_data, 16); + + for r in relocs { + let relocation = match r { + Relocation::LinkedData { offset, name } => { + if let Some(sym_id) = output.symbol_id(name.as_bytes()) { + if cfg!(all(target_arch = "aarch64", target_os = "linux")) { + // 700: 90000001 adrp x1, 0x0 + // 0000000000000700: R_AARCH64_ADR_PREL_PG_HI21 .rodata+0x650 + let relocation = write::Relocation { + offset: offset + proc_offset, + size: 21, + kind: RelocationKind::Elf(object::elf::R_AARCH64_ADR_PREL_PG_HI21), + encoding: RelocationEncoding::Generic, + symbol: sym_id, + addend: 0, + }; + + output.add_relocation(text_section, relocation).unwrap(); + + // 704: 91000021 add x1, x1, #0x0 + // 0000000000000704: R_AARCH64_ADD_ABS_LO12_NC .rodata+0x650 + write::Relocation { + offset: offset + proc_offset + 4, + size: 12, + kind: RelocationKind::Elf(object::elf::R_AARCH64_ADD_ABS_LO12_NC), + encoding: RelocationEncoding::Generic, + symbol: sym_id, + addend: 0, + } + } else if cfg!(all(target_arch = "aarch64", target_os = "macos")) { + // 4dc: 90000001 adrp x1, 0x0 + // 00000000000004dc: ARM64_RELOC_PAGE21 ___unnamed_6 + let relocation = write::Relocation { + offset: offset + proc_offset, + size: 32, + kind: RelocationKind::MachO { + value: object::macho::ARM64_RELOC_PAGE21, + relative: true, + }, + encoding: RelocationEncoding::Generic, + symbol: sym_id, + addend: 0, + }; + + output.add_relocation(text_section, relocation).unwrap(); + + // 4e0: 91000021 add x1, x1, #0x0 + // 00000000000004e0: ARM64_RELOC_PAGEOFF12 ___unnamed_6 + write::Relocation { + offset: offset + proc_offset + 4, + size: 32, + kind: RelocationKind::MachO { + value: object::macho::ARM64_RELOC_PAGEOFF12, + relative: false, + }, + encoding: RelocationEncoding::Generic, + symbol: sym_id, + addend: 0, + } + } else { + write::Relocation { + offset: offset + proc_offset, + size: 32, + kind: RelocationKind::GotRelative, + encoding: RelocationEncoding::Generic, + symbol: sym_id, + addend: -4, + } + } + } else { + internal_error!("failed to find data symbol for {:?}", name); + } + } + Relocation::LocalData { .. } + | Relocation::LinkedFunction { .. } + | Relocation::JmpToReturn { .. } => { + unreachable!("not currently created by build_roc_panic") + } + }; + + output.add_relocation(text_section, relocation).unwrap(); + } +} + +fn generate_roc_dbg<'a, B: Backend<'a>>(_backend: &mut B, output: &mut Object) { + let text_section = output.section_id(StandardSection::Text); + let proc_symbol = Symbol { + name: "roc_dbg".as_bytes().to_vec(), + value: 0, + size: 0, + kind: SymbolKind::Text, + scope: SymbolScope::Dynamic, + weak: false, + section: SymbolSection::Section(text_section), + flags: SymbolFlags::None, + }; + let _proc_id = output.add_symbol(proc_symbol); + // TODO: Actually generate an impl instead of just an empty symbol. + // At a minimum, it should just be a ret. +} + +fn generate_wrapper<'a, B: Backend<'a>>( + backend: &mut B, + output: &mut Object, + wrapper_name: String, + wraps: String, +) { + let text_section = output.section_id(StandardSection::Text); + let proc_symbol = Symbol { + name: wrapper_name.as_bytes().to_vec(), + value: 0, + size: 0, + kind: SymbolKind::Text, + scope: SymbolScope::Dynamic, + weak: false, + section: SymbolSection::Section(text_section), + flags: SymbolFlags::None, + }; + let proc_id = output.add_symbol(proc_symbol); + let (proc_data, offset) = backend.build_wrapped_jmp(); + let proc_offset = output.add_symbol_data(proc_id, text_section, proc_data, 16); + + let name = wraps.as_str().as_bytes(); + // If the symbol is an undefined zig builtin, we need to add it here. + let symbol = Symbol { + name: name.to_vec(), + value: 0, + size: 0, + kind: SymbolKind::Text, + scope: SymbolScope::Dynamic, + weak: true, + section: SymbolSection::Undefined, + flags: SymbolFlags::None, + }; + output.add_symbol(symbol); + if let Some(sym_id) = output.symbol_id(name) { + let reloc = create_relocation(backend.target_info(), sym_id, offset + proc_offset); + + match output.add_relocation(text_section, reloc) { + Ok(obj) => obj, + Err(e) => internal_error!("{:?}", e), + } + } else { + internal_error!("failed to find fn symbol for {:?}", wraps); + } +} + +fn create_relocation(target_info: TargetInfo, symbol: SymbolId, offset: u64) -> write::Relocation { + let (encoding, size, addend, kind) = match target_info.architecture { + roc_target::Architecture::Aarch32 => todo!(), + roc_target::Architecture::Aarch64 => { + if cfg!(target_os = "macos") { + ( + RelocationEncoding::Generic, + 26, + 0, + RelocationKind::MachO { + value: 2, + relative: true, + }, + ) + } else { + ( + RelocationEncoding::AArch64Call, + 26, + 0, + RelocationKind::PltRelative, + ) + } + } + roc_target::Architecture::Wasm32 => todo!(), + roc_target::Architecture::X86_32 => todo!(), + roc_target::Architecture::X86_64 => ( + RelocationEncoding::X86Branch, + 32, + -4, + RelocationKind::PltRelative, + ), + }; + + write::Relocation { + offset, + size, + kind, + encoding, + symbol, + addend, + } +} + +fn build_object<'a, B: Backend<'a>>( + procedures: MutMap<(symbol::Symbol, ProcLayout<'a>), Proc<'a>>, + mut backend: B, + mut output: Object<'a>, +) -> Object<'a> { + let data_section = output.section_id(StandardSection::Data); + + let arena = backend.env().arena; + + /* + // Commented out because we couldn't figure out how to get it to work on mac - see https://github.com/roc-lang/roc/pull/1323 + let comment = output.add_section(vec![], b".comment".to_vec(), SectionKind::OtherString); + output.append_section_data( + comment, + format!("\0roc dev backend version {} \0", VERSION).as_bytes(), + 1, + ); + */ + + if backend.env().mode.generate_roc_panic() { + define_setlongjmp_buffer(&mut output); + + generate_roc_panic(&mut backend, &mut output); + generate_setjmp(&mut backend, &mut output); + generate_longjmp(&mut backend, &mut output); + } + + if backend.env().mode.generate_roc_dbg() { + generate_roc_dbg(&mut backend, &mut output); + } + + if backend.env().mode.generate_allocators() { + generate_wrapper( + &mut backend, + &mut output, + "roc_alloc".into(), + "malloc".into(), + ); + generate_wrapper( + &mut backend, + &mut output, + "roc_realloc".into(), + "realloc".into(), + ); + generate_wrapper( + &mut backend, + &mut output, + "roc_dealloc".into(), + "free".into(), + ); + + // Extra symbols only required on unix systems. + if matches!(output.format(), BinaryFormat::Elf | BinaryFormat::MachO) { + generate_wrapper( + &mut backend, + &mut output, + "roc_getppid".into(), + "getppid".into(), + ); + generate_wrapper(&mut backend, &mut output, "roc_mmap".into(), "mmap".into()); + generate_wrapper( + &mut backend, + &mut output, + "roc_shm_open".into(), + "shm_open".into(), + ); + } else if matches!(output.format(), BinaryFormat::Coff) { + // TODO figure out why this symbol is required, it should not be required + // Without this it does not build on Windows + generate_wrapper( + &mut backend, + &mut output, + "roc_getppid".into(), + "malloc".into(), + ); + } + } + + // Setup layout_ids for procedure calls. + let mut layout_ids = LayoutIds::default(); + let mut procs = Vec::with_capacity_in(procedures.len(), arena); + + // Names and linker data for user procedures + for ((sym, layout), proc) in procedures { + debug_assert_eq!(sym, proc.name.name()); + + if backend.env().exposed_to_host.contains(&sym) { + let exposed_proc = build_exposed_proc(&mut backend, &proc); + let exposed_generic_proc = build_exposed_generic_proc(&mut backend, &proc); + + let mode = backend.env().mode; + + let (module_id, layout_interner, interns, code_gen_help, _) = + backend.module_interns_helpers_mut(); + + let ident_ids = interns.all_ident_ids.get_mut(&module_id).unwrap(); + + match mode { + AssemblyBackendMode::Test => { + let test_helper = roc_mono::code_gen_help::test_helper( + code_gen_help, + ident_ids, + layout_interner, + &proc, + ); + + #[cfg(debug_assertions)] + { + let module_id = exposed_generic_proc.name.name().module_id(); + let ident_ids = backend + .interns_mut() + .all_ident_ids + .get_mut(&module_id) + .unwrap(); + module_id.register_debug_idents(ident_ids); + } + + // println!("{}", test_helper.to_pretty(backend.interner(), 200, true)); + + build_proc_symbol( + &mut output, + &mut layout_ids, + &mut procs, + &mut backend, + layout, + test_helper, + Exposed::TestMain, + ); + } + AssemblyBackendMode::Repl => { + let repl_helper = roc_mono::code_gen_help::repl_helper( + code_gen_help, + ident_ids, + layout_interner, + &proc, + ); + + #[cfg(debug_assertions)] + { + let module_id = exposed_generic_proc.name.name().module_id(); + let ident_ids = backend + .interns_mut() + .all_ident_ids + .get_mut(&module_id) + .unwrap(); + module_id.register_debug_idents(ident_ids); + } + + // println!("{}", repl_helper.to_pretty(backend.interner(), 200, true)); + + build_proc_symbol( + &mut output, + &mut layout_ids, + &mut procs, + &mut backend, + layout, + repl_helper, + Exposed::TestMain, + ); + } + AssemblyBackendMode::Binary => { /* do nothing */ } + } + + build_proc_symbol( + &mut output, + &mut layout_ids, + &mut procs, + &mut backend, + layout, + exposed_proc, + Exposed::Exposed, + ); + + build_proc_symbol( + &mut output, + &mut layout_ids, + &mut procs, + &mut backend, + layout, + exposed_generic_proc, + Exposed::ExposedGeneric, + ); + } + + build_proc_symbol( + &mut output, + &mut layout_ids, + &mut procs, + &mut backend, + layout, + proc, + Exposed::NotExposed, + ) + } + + // Build procedures from user code + let mut relocations = bumpalo::vec![in arena]; + for (fn_name, section_id, proc_id, proc) in procs { + build_proc( + &mut output, + &mut backend, + &mut relocations, + &mut layout_ids, + data_section, + fn_name, + section_id, + proc_id, + proc, + ) + } + + // Generate IR for specialized helper procs (refcounting & equality) + let empty = bumpalo::collections::Vec::new_in(arena); + let mut helper_symbols_and_layouts = + std::mem::replace(backend.helper_proc_symbols_mut(), empty); + + let helper_procs = { + let (module_id, _interner, interns, helper_proc_gen, caller_procs) = + backend.module_interns_helpers_mut(); + + let mut owned_caller_procs = bumpalo::collections::Vec::new_in(arena); + std::mem::swap(caller_procs, &mut owned_caller_procs); + + let ident_ids = interns.all_ident_ids.get_mut(&module_id).unwrap(); + let mut helper_procs = helper_proc_gen.take_procs(); + + for caller_proc in owned_caller_procs { + helper_symbols_and_layouts.push((caller_proc.proc_symbol, caller_proc.proc_layout)); + helper_procs.push(caller_proc.proc); + } + + if false { + module_id.register_debug_idents(ident_ids); + + for p in &helper_procs { + println!("{}", p.to_pretty(_interner, 200, true)); + } + } + + helper_procs + }; + + let mut helper_names_symbols_procs = Vec::with_capacity_in(helper_procs.len(), arena); + + debug_assert_eq!(helper_symbols_and_layouts.len(), helper_procs.len()); + + // Names and linker data for helpers + for ((sym, layout), proc) in helper_symbols_and_layouts.into_iter().zip(helper_procs) { + debug_assert_eq!(sym, proc.name.name()); + + let fn_name = backend.lambda_name_to_string( + LambdaName::no_niche(sym), + layout.arguments.iter().copied(), + None, + layout.result, + ); + + if let Some(proc_id) = output.symbol_id(fn_name.as_bytes()) { + if let SymbolSection::Section(section_id) = output.symbol(proc_id).section { + helper_names_symbols_procs.push((fn_name, section_id, proc_id, proc)); + continue; + } + } else { + // The symbol isn't defined yet and will just be used by other rc procs. + let section_id = output.add_section( + output.segment_name(StandardSegment::Text).to_vec(), + format_symbol_name(sym), + SectionKind::Text, + ); + + let rc_symbol = Symbol { + name: fn_name.as_bytes().to_vec(), + value: 0, + size: 0, + kind: SymbolKind::Text, + scope: SymbolScope::Linkage, + weak: false, + section: SymbolSection::Section(section_id), + flags: SymbolFlags::None, + }; + let proc_id = output.add_symbol(rc_symbol); + helper_names_symbols_procs.push((fn_name, section_id, proc_id, proc)); + continue; + } + internal_error!("failed to create rc fn for symbol {:?}", sym); + } + + // Build helpers + for (fn_name, section_id, proc_id, proc) in helper_names_symbols_procs { + build_proc( + &mut output, + &mut backend, + &mut relocations, + &mut layout_ids, + data_section, + fn_name, + section_id, + proc_id, + proc, + ) + } + + // Relocations for all procedures (user code & helpers) + for (section_id, reloc) in relocations { + match output.add_relocation(section_id, reloc) { + Ok(obj) => obj, + Err(e) => internal_error!("{:?}", e), + } + } + output +} + +fn build_exposed_proc<'a, B: Backend<'a>>(backend: &mut B, proc: &Proc<'a>) -> Proc<'a> { + let arena = backend.env().arena; + let interns = backend.interns(); + + let sym = proc.name.name(); + let platform = sym.module_id(); + + let fn_name = sym.as_str(interns).to_string(); + let generic_proc_name = backend.debug_symbol_in(platform, &fn_name); + let s4 = backend.debug_symbol_in(platform, "s4"); + + let call_args = bumpalo::collections::Vec::from_iter_in(proc.args.iter().map(|t| t.1), arena); + let call_layouts = + bumpalo::collections::Vec::from_iter_in(proc.args.iter().map(|t| t.0), arena); + let call = Call { + call_type: roc_mono::ir::CallType::ByName { + name: proc.name, + ret_layout: proc.ret_layout, + arg_layouts: call_layouts.into_bump_slice(), + specialization_id: CallSpecId::BACKEND_DUMMY, + }, + arguments: call_args.into_bump_slice(), + }; + + let body = Stmt::Let( + s4, + Expr::Call(call), + proc.ret_layout, + arena.alloc(Stmt::Ret(s4)), + ); + + Proc { + name: LambdaName::no_niche(generic_proc_name), + args: proc.args, + body, + closure_data_layout: None, + ret_layout: proc.ret_layout, + is_self_recursive: roc_mono::ir::SelfRecursive::NotSelfRecursive, + is_erased: proc.is_erased, + } +} + +fn build_exposed_generic_proc<'a, B: Backend<'a>>(backend: &mut B, proc: &Proc<'a>) -> Proc<'a> { + let arena = backend.env().arena; + let interns = backend.interns(); + + let sym = proc.name.name(); + let platform = sym.module_id(); + + let fn_name = sym.as_str(interns).to_string(); + let generic_proc_name = backend.debug_symbol_in(platform, &fn_name); + let arg_generic = backend.debug_symbol_in(platform, "arg_generic"); + + let s1 = backend.debug_symbol_in(platform, "s1"); + let s2 = backend.debug_symbol_in(platform, "s2"); + let s3 = backend.debug_symbol_in(platform, "s3"); + + let box_layout = backend + .interner_mut() + .insert_direct_no_semantic(roc_mono::layout::LayoutRepr::Ptr(proc.ret_layout)); + + let mut args = bumpalo::collections::Vec::new_in(arena); + args.extend(proc.args); + args.push((box_layout, arg_generic)); + + let call_args = bumpalo::collections::Vec::from_iter_in(proc.args.iter().map(|t| t.1), arena); + let call_layouts = + bumpalo::collections::Vec::from_iter_in(proc.args.iter().map(|t| t.0), arena); + let call = Call { + call_type: roc_mono::ir::CallType::ByName { + name: proc.name, + ret_layout: proc.ret_layout, + arg_layouts: call_layouts.into_bump_slice(), + specialization_id: CallSpecId::BACKEND_DUMMY, + }, + arguments: call_args.into_bump_slice(), + }; + + let box_write = Call { + call_type: roc_mono::ir::CallType::LowLevel { + op: roc_module::low_level::LowLevel::PtrStore, + update_mode: UpdateModeId::BACKEND_DUMMY, + }, + arguments: arena.alloc([arg_generic, s1]), + }; + + let body = Stmt::Let( + s1, + Expr::Call(call), + proc.ret_layout, + arena.alloc( + // + Stmt::Let( + s2, + Expr::Call(box_write), + box_layout, + arena.alloc( + // + Stmt::Let( + s3, + Expr::Struct(&[]), + Layout::UNIT, + arena.alloc( + // + Stmt::Ret(s3), + ), + ), + ), + ), + ), + ); + + Proc { + name: LambdaName::no_niche(generic_proc_name), + args: args.into_bump_slice(), + body, + closure_data_layout: None, + ret_layout: roc_mono::layout::Layout::UNIT, + is_self_recursive: roc_mono::ir::SelfRecursive::NotSelfRecursive, + is_erased: proc.is_erased, + } +} + +#[allow(clippy::enum_variant_names)] +enum Exposed { + ExposedGeneric, + Exposed, + NotExposed, + TestMain, +} + +fn build_proc_symbol<'a, B: Backend<'a>>( + output: &mut Object<'a>, + layout_ids: &mut LayoutIds<'a>, + procs: &mut Vec<'a, (String, SectionId, SymbolId, Proc<'a>)>, + backend: &mut B, + layout: ProcLayout<'a>, + proc: Proc<'a>, + exposed: Exposed, +) { + let sym = proc.name.name(); + + let section_id = output.add_section( + output.segment_name(StandardSegment::Text).to_vec(), + format_symbol_name(sym), + SectionKind::Text, + ); + + let fn_name = match exposed { + Exposed::ExposedGeneric => layout_ids + .get_toplevel(sym, &layout) + .to_exposed_generic_symbol_string(sym, backend.interns()), + Exposed::Exposed => layout_ids + .get_toplevel(sym, &layout) + .to_exposed_symbol_string(sym, backend.interns()), + Exposed::NotExposed => backend.lambda_name_to_string( + proc.name, + layout.arguments.iter().copied(), + None, + layout.result, + ), + Exposed::TestMain => String::from("test_main"), + }; + + let proc_symbol = Symbol { + name: fn_name.as_bytes().to_vec(), + value: 0, + size: 0, + kind: SymbolKind::Text, + // TODO: Depending on whether we are building a static or dynamic lib, this should change. + // We should use Dynamic -> anyone, Linkage -> static link, Compilation -> this module only. + scope: match exposed { + Exposed::ExposedGeneric | Exposed::Exposed | Exposed::TestMain => SymbolScope::Dynamic, + Exposed::NotExposed => SymbolScope::Linkage, + }, + weak: false, + section: SymbolSection::Section(section_id), + flags: SymbolFlags::None, + }; + let proc_id = output.add_symbol(proc_symbol); + procs.push((fn_name, section_id, proc_id, proc)); +} + +#[allow(clippy::too_many_arguments)] +fn build_proc<'a, B: Backend<'a>>( + output: &mut Object, + backend: &mut B, + relocations: &mut Vec<'a, (SectionId, object::write::Relocation)>, + layout_ids: &mut LayoutIds<'a>, + data_section: SectionId, + fn_name: String, + section_id: SectionId, + proc_id: SymbolId, + proc: Proc<'a>, +) { + let mut local_data_index = 0; + let target_info = backend.target_info(); + let (proc_data, relocs, rc_proc_names) = backend.build_proc(proc, layout_ids); + let proc_offset = output.add_symbol_data(proc_id, section_id, &proc_data, 16); + for reloc in relocs.iter() { + let elfreloc = match reloc { + Relocation::LocalData { offset, data } => { + let data_symbol = write::Symbol { + name: format!("{fn_name}.data{local_data_index}") + .as_bytes() + .to_vec(), + value: 0, + size: 0, + kind: SymbolKind::Data, + scope: SymbolScope::Compilation, + weak: false, + section: SymbolSection::Section(data_section), + flags: SymbolFlags::None, + }; + local_data_index += 1; + let data_id = output.add_symbol(data_symbol); + output.add_symbol_data(data_id, data_section, data, 4); + write::Relocation { + offset: offset + proc_offset, + size: 32, + kind: RelocationKind::Relative, + encoding: RelocationEncoding::Generic, + symbol: data_id, + addend: -4, + } + } + Relocation::LinkedData { offset, name } => { + add_undefined_rc_proc(output, name, &rc_proc_names); + + if let Some(sym_id) = output.symbol_id(name.as_bytes()) { + if cfg!(all(target_arch = "aarch64", target_os = "linux")) { + // 700: 90000001 adrp x1, 0x0 + // 0000000000000700: R_AARCH64_ADR_PREL_PG_HI21 .rodata+0x650 + let r = write::Relocation { + offset: proc_offset + offset, + size: 21, + kind: RelocationKind::Elf(object::elf::R_AARCH64_ADR_PREL_PG_HI21), + encoding: RelocationEncoding::Generic, + symbol: sym_id, + addend: -4, + }; + + relocations.push((section_id, r)); + + // 704: 91000021 add x1, x1, #0x0 + // 0000000000000704: R_AARCH64_ADD_ABS_LO12_NC .rodata+0x650 + write::Relocation { + offset: proc_offset + offset + 4, + size: 12, + kind: RelocationKind::Elf(object::elf::R_AARCH64_ADD_ABS_LO12_NC), + encoding: RelocationEncoding::Generic, + symbol: sym_id, + addend: 0, + } + } else if cfg!(all(target_arch = "aarch64", target_os = "macos")) { + // 4ed0: 90000000 adrp x0, 0x4000 <_std.unicode.utf8Decode4+0x16c> + // 0000000000004ed0: ARM64_RELOC_PAGE21 ___unnamed_11 + let r = write::Relocation { + offset: proc_offset + offset, + size: 21, + kind: RelocationKind::MachO { + value: object::macho::ARM64_RELOC_PAGE21, + relative: true, + }, + encoding: RelocationEncoding::Generic, + symbol: sym_id, + addend: 0, + }; + + relocations.push((section_id, r)); + + // 4ed4: 91000000 add x0, x0, #0x0 + // 0000000000004ed4: ARM64_RELOC_PAGEOFF12 ___unnamed_11 + write::Relocation { + offset: proc_offset + offset + 4, + size: 12, + kind: RelocationKind::MachO { + value: object::macho::ARM64_RELOC_PAGEOFF12, + relative: false, + }, + encoding: RelocationEncoding::Generic, + symbol: sym_id, + addend: 0, + } + } else { + write::Relocation { + offset: offset + proc_offset, + size: 32, + kind: RelocationKind::GotRelative, + encoding: RelocationEncoding::Generic, + symbol: sym_id, + addend: -4, + } + } + } else { + internal_error!("failed to find data symbol for {:?}", name); + } + } + Relocation::LinkedFunction { offset, name } => { + // If the symbol is an undefined roc function, we need to add it here. + if output.symbol_id(name.as_bytes()).is_none() && name.starts_with("roc_") { + let builtin_symbol = Symbol { + name: name.as_bytes().to_vec(), + value: 0, + size: 0, + kind: SymbolKind::Text, + scope: SymbolScope::Linkage, + weak: false, + section: SymbolSection::Undefined, + flags: SymbolFlags::None, + }; + output.add_symbol(builtin_symbol); + } + + add_undefined_rc_proc(output, name, &rc_proc_names); + + if let Some(sym_id) = output.symbol_id(name.as_bytes()) { + create_relocation(target_info, sym_id, offset + proc_offset) + } else { + internal_error!("failed to find fn symbol for {:?}", name); + } + } + Relocation::JmpToReturn { .. } => unreachable!(), + }; + relocations.push((section_id, elfreloc)); + } +} + +fn add_undefined_rc_proc( + output: &mut Object<'_>, + name: &String, + rc_proc_names: &Vec<'_, (symbol::Symbol, String)>, +) { + // If the symbol is an undefined reference counting procedure, we need to add it here. + if output.symbol_id(name.as_bytes()).is_none() { + for (sym, rc_name) in rc_proc_names.iter() { + if name == rc_name { + let section_id = output.add_section( + output.segment_name(StandardSegment::Text).to_vec(), + format_symbol_name(*sym), + SectionKind::Text, + ); + + let rc_symbol = Symbol { + name: name.as_bytes().to_vec(), + value: 0, + size: 0, + kind: SymbolKind::Text, + scope: SymbolScope::Linkage, + weak: false, + section: SymbolSection::Section(section_id), + flags: SymbolFlags::None, + }; + output.add_symbol(rc_symbol); + } + } + } +} + +fn format_symbol_name(sym: roc_module::symbol::Symbol) -> std::vec::Vec { + let name = format!(".text.{:x}", sym.as_u64()); + let length = Ord::min(name.len(), 16); + + name.as_bytes()[..length].to_vec() +} diff --git a/crates/compiler/gen_dev/src/run_roc.rs b/crates/compiler/gen_dev/src/run_roc.rs new file mode 100644 index 0000000000..a0d20c0d12 --- /dev/null +++ b/crates/compiler/gen_dev/src/run_roc.rs @@ -0,0 +1,35 @@ +#[macro_export] +/// run_jit_function_raw runs an unwrapped jit function. +/// The function could throw an exception and break things, or worse, it could not throw an exception and break things. +/// This functions is generally a bad idea with an untrused backend, but is being used for now for development purposes. +macro_rules! run_jit_function_raw { + ($lib: expr, $main_fn_name: expr, $ty:ty, $transform:expr) => {{ + let v: std::vec::Vec = std::vec::Vec::new(); + run_jit_function_raw!($lib, $main_fn_name, $ty, $transform, v) + }}; + + ($lib: expr, $main_fn_name: expr, $ty:ty, $transform:expr, $errors:expr) => {{ + unsafe { + let main: libloading::Symbol $ty> = $lib + .get($main_fn_name.as_bytes()) + .ok() + .ok_or(format!("Unable to JIT compile `{}`", $main_fn_name)) + .expect("errored"); + + let result = main(); + + if !$errors.is_empty() { + dbg!(&$errors); + + assert_eq!( + $errors, + std::vec::Vec::new(), + "Encountered errors: {:?}", + $errors + ); + } + + $transform(result) + } + }}; +} diff --git a/compiler/gen_dev/src/todo.md b/crates/compiler/gen_dev/src/todo.md similarity index 89% rename from compiler/gen_dev/src/todo.md rename to crates/compiler/gen_dev/src/todo.md index 9035db42ea..577acc3915 100644 --- a/compiler/gen_dev/src/todo.md +++ b/crates/compiler/gen_dev/src/todo.md @@ -1,4 +1,4 @@ -# Important things to add (Not necessarily in order) +# Important things to add (not necessarily in order) - Expand to way more builtins and assembly calls. - Deal with function calling, basic layouts, and all the fun basics of argument passing. @@ -10,7 +10,7 @@ Otherwise, many will always be inlined. - Add basic const folding? It should be really easy to add as an optimization. It should be a nice optimization for little cost. Just be sure to make it optional, otherwise our tests will do nothing. -- Automatically build the zig builtins .o file and make it available here. +- Automatically build the Zig builtins .o file and make it available here. We will need to link against it and use it whenever we call specific builtins. - Add unwind tables and landing pads. - Add ability to wrap functions with exceptions or return a results. diff --git a/crates/compiler/gen_llvm/Cargo.toml b/crates/compiler/gen_llvm/Cargo.toml new file mode 100644 index 0000000000..031c418bfc --- /dev/null +++ b/crates/compiler/gen_llvm/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "roc_gen_llvm" +description = "The LLVM backend for the Roc compiler" + +authors.workspace = true +edition.workspace = true +license.workspace = true +version.workspace = true + +[dependencies] +morphic_lib = { path = "../../vendor/morphic_lib" } +roc_alias_analysis = { path = "../alias_analysis" } +roc_bitcode_bc = { path = "../builtins/bitcode/bc" } +roc_builtins = { path = "../builtins" } +roc_collections = { path = "../collections" } +roc_debug_flags = { path = "../debug_flags" } +roc_error_macros = { path = "../../error_macros" } +roc_module = { path = "../module" } +roc_mono = { path = "../mono" } +roc_region = { path = "../region" } +roc_std = { path = "../../roc_std" } +roc_target = { path = "../roc_target" } + +bumpalo.workspace = true +inkwell.workspace = true +target-lexicon.workspace = true diff --git a/crates/compiler/gen_llvm/src/lib.rs b/crates/compiler/gen_llvm/src/lib.rs new file mode 100644 index 0000000000..e0d901a9b9 --- /dev/null +++ b/crates/compiler/gen_llvm/src/lib.rs @@ -0,0 +1,13 @@ +//! Provides the LLVM backend to generate Roc binaries. Used to generate a +//! binary with the fastest possible execution speed. +#![warn(clippy::dbg_macro)] +// See github.com/roc-lang/roc/issues/800 for discussion of the large_enum_variant check. +#![allow(clippy::large_enum_variant)] +// we actually want to compare against the literal float bits +#![allow(clippy::float_cmp)] +// Not a useful lint for us +#![allow(clippy::too_many_arguments)] + +pub mod llvm; + +pub mod run_roc; diff --git a/crates/compiler/gen_llvm/src/llvm/align.rs b/crates/compiler/gen_llvm/src/llvm/align.rs new file mode 100644 index 0000000000..bf7327353b --- /dev/null +++ b/crates/compiler/gen_llvm/src/llvm/align.rs @@ -0,0 +1,133 @@ +use roc_builtins::bitcode::{FloatWidth, IntWidth}; +use roc_mono::layout::{ + round_up_to_alignment, Builtin, Discriminant, InLayout, LayoutInterner, LayoutRepr, + STLayoutInterner, UnionLayout, +}; +use roc_target::Architecture; + +pub trait LlvmAlignment<'a> { + /// Alignment of a layout in bytes, as known and expected by LLVM. + fn llvm_alignment_bytes(&self, interner: &STLayoutInterner<'a>) -> u32; +} + +impl LlvmAlignment<'_> for IntWidth { + fn llvm_alignment_bytes(&self, interner: &STLayoutInterner) -> u32 { + match self { + IntWidth::U8 | IntWidth::I8 => 1, + IntWidth::U16 | IntWidth::I16 => 2, + IntWidth::U32 | IntWidth::I32 => 4, + IntWidth::U64 | IntWidth::I64 => 8, + IntWidth::U128 | IntWidth::I128 => { + // 128-bit integers are not consistently represented by LLVM. + // - AArch64 uses 16-byte alignment (https://godbolt.org/z/dYrfG5o4b) + // - x86-64 uses 8-byte alignment (https://godbolt.org/z/qj5Mann6b) + let arch = interner.target_info().architecture; + match arch { + Architecture::X86_64 => 8, + _ => 16, + } + } + } + } +} + +impl<'a> LlvmAlignment<'a> for FloatWidth { + fn llvm_alignment_bytes(&self, interner: &STLayoutInterner<'a>) -> u32 { + self.alignment_bytes(interner.target_info()) + } +} + +impl<'a> LlvmAlignment<'a> for Discriminant { + fn llvm_alignment_bytes(&self, _interner: &STLayoutInterner<'a>) -> u32 { + self.alignment_bytes() + } +} + +impl<'a> LlvmAlignment<'a> for Builtin<'a> { + fn llvm_alignment_bytes(&self, interner: &STLayoutInterner<'a>) -> u32 { + use std::mem::align_of; + use Builtin::*; + + let ptr_width = interner.target_info().ptr_width() as u32; + + // for our data structures, what counts is the alignment of the `( ptr, len )` tuple, and + // since both of those are one pointer size, the alignment of that structure is a pointer + // size + match self { + Int(int_width) => (*int_width).llvm_alignment_bytes(interner), + Float(float_width) => float_width.llvm_alignment_bytes(interner), + Bool => align_of::() as u32, + Decimal => IntWidth::I128.llvm_alignment_bytes(interner), + // we often treat these as i128 (64-bit systems) + // or i64 (32-bit systems). + // + // In webassembly, For that to be safe + // they must be aligned to allow such access + List(_) => ptr_width, + Str => ptr_width, + } + } +} + +impl<'a> LlvmAlignment<'a> for UnionLayout<'a> { + fn llvm_alignment_bytes(&self, interner: &STLayoutInterner<'a>) -> u32 { + use UnionLayout::*; + + match self { + NonRecursive(tags) => { + let max_alignment = tags + .iter() + .flat_map(|layouts| { + layouts + .iter() + .map(|layout| layout.llvm_alignment_bytes(interner)) + }) + .max(); + + let discriminant = self.discriminant(); + match max_alignment { + Some(align) => round_up_to_alignment( + align.max(discriminant.llvm_alignment_bytes(interner)), + discriminant.llvm_alignment_bytes(interner), + ), + None => { + // none of the tags had any payload, but the tag id still contains information + discriminant.llvm_alignment_bytes(interner) + } + } + } + Recursive(_) + | NullableWrapped { .. } + | NullableUnwrapped { .. } + | NonNullableUnwrapped(_) => interner.target_info().ptr_width() as u32, + } + } +} + +impl<'a> LlvmAlignment<'a> for LayoutRepr<'a> { + fn llvm_alignment_bytes(&self, interner: &STLayoutInterner<'a>) -> u32 { + use LayoutRepr::*; + match self { + Struct(field_layouts) => field_layouts + .iter() + .map(|x| x.llvm_alignment_bytes(interner)) + .max() + .unwrap_or(0), + + Union(variant) => variant.llvm_alignment_bytes(interner), + LambdaSet(lambda_set) => lambda_set + .runtime_representation() + .llvm_alignment_bytes(interner), + Builtin(builtin) => builtin.llvm_alignment_bytes(interner), + RecursivePointer(_) | Ptr(_) | FunctionPointer(_) | Erased(_) => { + interner.target_info().ptr_width() as u32 + } + } + } +} + +impl<'a> LlvmAlignment<'a> for InLayout<'a> { + fn llvm_alignment_bytes(&self, interner: &STLayoutInterner<'a>) -> u32 { + interner.get_repr(*self).llvm_alignment_bytes(interner) + } +} diff --git a/crates/compiler/gen_llvm/src/llvm/bitcode.rs b/crates/compiler/gen_llvm/src/llvm/bitcode.rs new file mode 100644 index 0000000000..3e8c8da91c --- /dev/null +++ b/crates/compiler/gen_llvm/src/llvm/bitcode.rs @@ -0,0 +1,1129 @@ +/// Helpers for interacting with the zig that generates bitcode +use crate::debug_info_init; +use crate::llvm::build::{ + complex_bitcast_check_size, load_roc_value, to_cc_return, CCReturn, Env, C_CALL_CONV, + FAST_CALL_CONV, +}; +use crate::llvm::convert::basic_type_from_layout; +use crate::llvm::refcounting::{ + decrement_refcount_layout, increment_n_refcount_layout, increment_refcount_layout, +}; +use inkwell::attributes::{Attribute, AttributeLoc}; +use inkwell::types::{BasicType, BasicTypeEnum, StructType}; +use inkwell::values::{ + BasicValueEnum, CallSiteValue, FunctionValue, InstructionValue, IntValue, PointerValue, + StructValue, +}; +use inkwell::AddressSpace; +use roc_error_macros::internal_error; +use roc_module::symbol::Symbol; +use roc_mono::layout::{ + Builtin, InLayout, LambdaSet, LayoutIds, LayoutInterner, LayoutRepr, STLayoutInterner, +}; + +use super::build::{create_entry_block_alloca, BuilderExt}; +use super::convert::{zig_list_type, zig_str_type}; +use super::struct_::struct_from_fields; + +pub fn call_bitcode_fn<'ctx>( + env: &Env<'_, 'ctx, '_>, + args: &[BasicValueEnum<'ctx>], + fn_name: &str, +) -> BasicValueEnum<'ctx> { + call_bitcode_fn_help(env, args, fn_name) + .try_as_basic_value() + .left() + .unwrap_or_else(|| { + panic!("LLVM error: Did not get return value from bitcode function {fn_name:?}") + }) +} + +pub fn call_void_bitcode_fn<'ctx>( + env: &Env<'_, 'ctx, '_>, + args: &[BasicValueEnum<'ctx>], + fn_name: &str, +) -> InstructionValue<'ctx> { + call_bitcode_fn_help(env, args, fn_name) + .try_as_basic_value() + .right() + .unwrap_or_else(|| panic!("LLVM error: Tried to call void bitcode function, but got return value from bitcode function, {fn_name:?}")) +} + +fn call_bitcode_fn_help<'ctx>( + env: &Env<'_, 'ctx, '_>, + args: &[BasicValueEnum<'ctx>], + fn_name: &str, +) -> CallSiteValue<'ctx> { + let it = args.iter().map(|x| (*x).into()); + let arguments = bumpalo::collections::Vec::from_iter_in(it, env.arena); + + let fn_val = env + .module + .get_function(fn_name) + .unwrap_or_else(|| panic!("Unrecognized builtin function: {fn_name:?} - if you're working on the Roc compiler, do you need to rebuild the bitcode? See compiler/builtins/bitcode/README.md")); + + let call = env + .builder + .new_build_call(fn_val, &arguments, "call_builtin"); + + // Attributes that we propagate from the zig builtin parameters, to the arguments we give to the + // call. It is undefined behavior in LLVM to have an attribute on a parameter, and then call + // the function where that parameter is not present. For many (e.g. nonnull) it can be inferred + // but e.g. byval and sret cannot and must be explicitly provided. + let propagate = [ + Attribute::get_named_enum_kind_id("nonnull"), + Attribute::get_named_enum_kind_id("nocapture"), + Attribute::get_named_enum_kind_id("readonly"), + Attribute::get_named_enum_kind_id("noalias"), + Attribute::get_named_enum_kind_id("sret"), + Attribute::get_named_enum_kind_id("byval"), + ]; + + for i in 0..fn_val.count_params() { + let attributes = fn_val.attributes(AttributeLoc::Param(i)); + + for attribute in attributes { + let kind_id = attribute.get_enum_kind_id(); + + if propagate.contains(&kind_id) { + call.add_attribute(AttributeLoc::Param(i), attribute) + } + } + } + + call.set_call_convention(fn_val.get_call_conventions()); + call +} + +pub fn call_bitcode_fn_fixing_for_convention<'a, 'ctx, 'env>( + env: &Env<'a, 'ctx, 'env>, + layout_interner: &STLayoutInterner<'a>, + bitcode_return_type: StructType<'ctx>, + args: &[BasicValueEnum<'ctx>], + return_layout: InLayout<'a>, + fn_name: &str, +) -> BasicValueEnum<'ctx> { + // Calling zig bitcode, so we must follow C calling conventions. + let cc_return = to_cc_return(env, layout_interner, return_layout); + match cc_return { + CCReturn::Return => { + // We'll get a return value + call_bitcode_fn(env, args, fn_name) + } + CCReturn::ByPointer => { + // We need to pass the return value by pointer. + let roc_return_type = basic_type_from_layout( + env, + layout_interner, + layout_interner.get_repr(return_layout), + ); + + let cc_return_type: BasicTypeEnum<'ctx> = bitcode_return_type.into(); + + // when we write an i128 into this (happens in NumToInt), zig expects this pointer to + // be 16-byte aligned. Not doing so is UB and will immediately fail on CI + let cc_return_value_ptr = env.builder.new_build_alloca(cc_return_type, "return_value"); + cc_return_value_ptr + .as_instruction() + .unwrap() + .set_alignment(16) + .unwrap(); + + let fixed_args: Vec> = [cc_return_value_ptr.into()] + .iter() + .chain(args) + .copied() + .collect(); + call_void_bitcode_fn(env, &fixed_args, fn_name); + + let cc_return_value = + env.builder + .new_build_load(cc_return_type, cc_return_value_ptr, "read_result"); + if roc_return_type.size_of() == cc_return_type.size_of() { + cc_return_value + } else { + // We need to convert the C-callconv return type, which may be larger than the Roc + // return type, into the Roc return type. + complex_bitcast_check_size( + env, + cc_return_value, + roc_return_type, + "c_value_to_roc_value", + ) + } + } + CCReturn::Void => { + internal_error!("Tried to call valued bitcode function, but it has no return type") + } + } +} + +const ARGUMENT_SYMBOLS: [Symbol; 8] = [ + Symbol::ARG_1, + Symbol::ARG_2, + Symbol::ARG_3, + Symbol::ARG_4, + Symbol::ARG_5, + Symbol::ARG_6, + Symbol::ARG_7, + Symbol::ARG_8, +]; + +pub(crate) fn build_transform_caller<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + function: FunctionValue<'ctx>, + closure_data_layout: LambdaSet<'a>, + argument_layouts: &[InLayout<'a>], + result_layout: InLayout<'a>, +) -> FunctionValue<'ctx> { + let fn_name: &str = &format!( + "{}_zig_function_caller", + function.get_name().to_string_lossy() + ); + + match env.module.get_function(fn_name) { + Some(function_value) => function_value, + None => build_transform_caller_help( + env, + layout_interner, + function, + closure_data_layout, + argument_layouts, + result_layout, + fn_name, + ), + } +} + +fn build_transform_caller_help<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + roc_function: FunctionValue<'ctx>, + closure_data_layout: LambdaSet<'a>, + argument_layouts: &[InLayout<'a>], + result_layout: InLayout<'a>, + fn_name: &str, +) -> FunctionValue<'ctx> { + debug_assert!(argument_layouts.len() <= 7); + + let block = env.builder.get_insert_block().expect("to be in a function"); + let di_location = env.builder.get_current_debug_location().unwrap(); + + let arg_type = env.context.i8_type().ptr_type(AddressSpace::default()); + + let function_value = crate::llvm::refcounting::build_header_help( + env, + fn_name, + env.context.void_type().into(), + &(bumpalo::vec![in env.arena; BasicTypeEnum::PointerType(arg_type); argument_layouts.len() + 2]), + ); + + // called from zig, must use C calling convention + function_value.set_call_conventions(C_CALL_CONV); + + let kind_id = Attribute::get_named_enum_kind_id("alwaysinline"); + debug_assert!(kind_id > 0); + let attr = env.context.create_enum_attribute(kind_id, 0); + function_value.add_attribute(AttributeLoc::Function, attr); + + let entry = env.context.append_basic_block(function_value, "entry"); + env.builder.position_at_end(entry); + + debug_info_init!(env, function_value); + + let mut it = function_value.get_param_iter(); + let closure_ptr = it.next().unwrap().into_pointer_value(); + closure_ptr.set_name(Symbol::ARG_1.as_str(&env.interns)); + + let arguments = + bumpalo::collections::Vec::from_iter_in(it.take(argument_layouts.len()), env.arena); + + for (argument, name) in arguments.iter().zip(ARGUMENT_SYMBOLS[1..].iter()) { + argument.set_name(name.as_str(&env.interns)); + } + + let mut arguments_cast = + bumpalo::collections::Vec::with_capacity_in(arguments.len(), env.arena); + + for (argument_ptr, layout) in arguments.iter().zip(argument_layouts) { + let basic_type = + basic_type_from_layout(env, layout_interner, layout_interner.get_repr(*layout)) + .ptr_type(AddressSpace::default()); + + let cast_ptr = env.builder.new_build_pointer_cast( + argument_ptr.into_pointer_value(), + basic_type, + "cast_ptr_to_tag_build_transform_caller_help", + ); + + let argument = load_roc_value( + env, + layout_interner, + layout_interner.get_repr(*layout), + cast_ptr, + "zig_helper_load_opaque", + ); + + arguments_cast.push(argument); + } + + match ( + closure_data_layout + .is_represented(layout_interner) + .is_some(), + closure_data_layout.runtime_representation(), + ) { + (false, _) => { + // the function doesn't expect a closure argument, nothing to add + } + (true, layout) => { + let closure_type = + basic_type_from_layout(env, layout_interner, layout_interner.get_repr(layout)) + .ptr_type(AddressSpace::default()); + + let closure_cast = env.builder.new_build_pointer_cast( + closure_ptr, + closure_type, + "cast_opaque_closure", + ); + + let closure_data = load_roc_value( + env, + layout_interner, + layout_interner.get_repr(layout), + closure_cast, + "load_closure", + ); + + arguments_cast.push(closure_data); + } + } + + let result = crate::llvm::build::call_direct_roc_function( + env, + layout_interner, + roc_function, + layout_interner.get_repr(result_layout), + arguments_cast.as_slice(), + ); + + let result_u8_ptr = function_value + .get_nth_param(argument_layouts.len() as u32 + 1) + .unwrap() + .into_pointer_value(); + + crate::llvm::build::store_roc_value_opaque( + env, + layout_interner, + result_layout, + result_u8_ptr, + result, + ); + env.builder.new_build_return(None); + + env.builder.position_at_end(block); + env.builder.set_current_debug_location(di_location); + + function_value +} + +enum Mode { + Inc, + IncN, + Dec, +} + +/// a function that accepts two arguments: the value to increment, and an amount to increment by +pub fn build_inc_n_wrapper<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + layout_ids: &mut LayoutIds<'a>, + layout: InLayout<'a>, +) -> FunctionValue<'ctx> { + build_rc_wrapper(env, layout_interner, layout_ids, layout, Mode::IncN) +} + +/// a function that accepts two arguments: the value to increment; increments by 1 +pub fn build_inc_wrapper<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + layout_ids: &mut LayoutIds<'a>, + layout: InLayout<'a>, +) -> FunctionValue<'ctx> { + build_rc_wrapper(env, layout_interner, layout_ids, layout, Mode::Inc) +} + +pub fn build_dec_wrapper<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + layout_ids: &mut LayoutIds<'a>, + layout: InLayout<'a>, +) -> FunctionValue<'ctx> { + build_rc_wrapper(env, layout_interner, layout_ids, layout, Mode::Dec) +} + +fn build_rc_wrapper<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + layout_ids: &mut LayoutIds<'a>, + layout: InLayout<'a>, + rc_operation: Mode, +) -> FunctionValue<'ctx> { + let block = env.builder.get_insert_block().expect("to be in a function"); + let di_location = env.builder.get_current_debug_location().unwrap(); + + let symbol = Symbol::GENERIC_RC_REF; + let fn_name = layout_ids + .get(symbol, &layout_interner.get_repr(layout)) + .to_symbol_string(symbol, &env.interns); + + let fn_name = match rc_operation { + Mode::IncN => format!("{fn_name}_inc_n"), + Mode::Inc => format!("{fn_name}_inc"), + Mode::Dec => format!("{fn_name}_dec"), + }; + + let function_value = match env.module.get_function(fn_name.as_str()) { + Some(function_value) => function_value, + None => { + let arg_type = env.context.i8_type().ptr_type(AddressSpace::default()); + + let function_value = match rc_operation { + Mode::Inc | Mode::Dec => crate::llvm::refcounting::build_header_help( + env, + &fn_name, + env.context.void_type().into(), + &[arg_type.into()], + ), + Mode::IncN => crate::llvm::refcounting::build_header_help( + env, + &fn_name, + env.context.void_type().into(), + &[arg_type.into(), env.ptr_int().into()], + ), + }; + + // called from zig, must use C calling convention + function_value.set_call_conventions(C_CALL_CONV); + + let kind_id = Attribute::get_named_enum_kind_id("alwaysinline"); + debug_assert!(kind_id > 0); + let attr = env.context.create_enum_attribute(kind_id, 0); + function_value.add_attribute(AttributeLoc::Function, attr); + + let entry = env.context.append_basic_block(function_value, "entry"); + env.builder.position_at_end(entry); + + debug_info_init!(env, function_value); + + let mut it = function_value.get_param_iter(); + let generic_value_ptr = it.next().unwrap().into_pointer_value(); + + generic_value_ptr.set_name(Symbol::ARG_1.as_str(&env.interns)); + + let value_type = + basic_type_from_layout(env, layout_interner, layout_interner.get_repr(layout)); + let value_ptr_type = value_type.ptr_type(AddressSpace::default()); + let value_ptr = env.builder.new_build_pointer_cast( + generic_value_ptr, + value_ptr_type, + "load_opaque", + ); + + // even though this looks like a `load_roc_value`, that gives segfaults in practice. + // I suspect it has something to do with the lifetime of the alloca that is created by + // `load_roc_value` + let value = if layout_interner.is_passed_by_reference(layout) { + value_ptr.into() + } else { + env.builder + .new_build_load(value_type, value_ptr, "load_opaque") + }; + + match rc_operation { + Mode::Inc => { + let n = 1; + increment_refcount_layout(env, layout_interner, layout_ids, n, value, layout); + } + Mode::IncN => { + let n = it.next().unwrap().into_int_value(); + n.set_name(Symbol::ARG_2.as_str(&env.interns)); + + increment_n_refcount_layout(env, layout_interner, layout_ids, n, value, layout); + } + Mode::Dec => { + decrement_refcount_layout(env, layout_interner, layout_ids, value, layout); + } + } + + env.builder.new_build_return(None); + + function_value + } + }; + + env.builder.position_at_end(block); + env.builder.set_current_debug_location(di_location); + + function_value +} + +pub fn build_eq_wrapper<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + layout_ids: &mut LayoutIds<'a>, + layout: InLayout<'a>, +) -> FunctionValue<'ctx> { + let block = env.builder.get_insert_block().expect("to be in a function"); + let di_location = env.builder.get_current_debug_location().unwrap(); + + let symbol = Symbol::GENERIC_EQ_REF; + let fn_name = layout_ids + .get(symbol, &layout_interner.get_repr(layout)) + .to_symbol_string(symbol, &env.interns); + + let function_value = match env.module.get_function(fn_name.as_str()) { + Some(function_value) => function_value, + None => { + let arg_type = env.context.i8_type().ptr_type(AddressSpace::default()); + + let function_value = crate::llvm::refcounting::build_header_help( + env, + &fn_name, + env.context.bool_type().into(), + &[arg_type.into(), arg_type.into()], + ); + + // called from zig, must use C calling convention + function_value.set_call_conventions(C_CALL_CONV); + + let kind_id = Attribute::get_named_enum_kind_id("alwaysinline"); + debug_assert!(kind_id > 0); + let attr = env.context.create_enum_attribute(kind_id, 0); + function_value.add_attribute(AttributeLoc::Function, attr); + + let entry = env.context.append_basic_block(function_value, "entry"); + env.builder.position_at_end(entry); + + debug_info_init!(env, function_value); + + let mut it = function_value.get_param_iter(); + let value_ptr1 = it.next().unwrap().into_pointer_value(); + let value_ptr2 = it.next().unwrap().into_pointer_value(); + + value_ptr1.set_name(Symbol::ARG_1.as_str(&env.interns)); + value_ptr2.set_name(Symbol::ARG_2.as_str(&env.interns)); + + let value_type = + basic_type_from_layout(env, layout_interner, layout_interner.get_repr(layout)) + .ptr_type(AddressSpace::default()); + + let value_cast1 = + env.builder + .new_build_pointer_cast(value_ptr1, value_type, "load_opaque"); + + let value_cast2 = + env.builder + .new_build_pointer_cast(value_ptr2, value_type, "load_opaque"); + + // load_roc_value(env, *element_layout, elem_ptr, "get_elem") + let value1 = load_roc_value( + env, + layout_interner, + layout_interner.get_repr(layout), + value_cast1, + "load_opaque", + ); + let value2 = load_roc_value( + env, + layout_interner, + layout_interner.get_repr(layout), + value_cast2, + "load_opaque", + ); + + let result = crate::llvm::compare::generic_eq( + env, + layout_interner, + layout_ids, + value1, + value2, + layout, + layout, + ); + + env.builder.new_build_return(Some(&result)); + + function_value + } + }; + + env.builder.position_at_end(block); + env.builder.set_current_debug_location(di_location); + + function_value +} + +pub fn build_compare_wrapper<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + layout_ids: &mut LayoutIds<'a>, + roc_function: FunctionValue<'ctx>, + closure_data_layout: LambdaSet<'a>, + layout: InLayout<'a>, +) -> FunctionValue<'ctx> { + let block = env.builder.get_insert_block().expect("to be in a function"); + let di_location = env.builder.get_current_debug_location().unwrap(); + + let fn_name: &str = &format!( + "{}_compare_wrapper", + roc_function.get_name().to_string_lossy() + ); + + let function_value = match env.module.get_function(fn_name) { + Some(function_value) => function_value, + None => { + let arg_type = env.context.i8_type().ptr_type(AddressSpace::default()); + + let function_value = crate::llvm::refcounting::build_header_help( + env, + fn_name, + env.context.i8_type().into(), + &[arg_type.into(), arg_type.into(), arg_type.into()], + ); + + // called from zig, must use C calling convention + function_value.set_call_conventions(C_CALL_CONV); + + // we expose this function to zig; must use c calling convention + function_value.set_call_conventions(C_CALL_CONV); + + let kind_id = Attribute::get_named_enum_kind_id("alwaysinline"); + debug_assert!(kind_id > 0); + let attr = env.context.create_enum_attribute(kind_id, 0); + function_value.add_attribute(AttributeLoc::Function, attr); + + let entry = env.context.append_basic_block(function_value, "entry"); + env.builder.position_at_end(entry); + + debug_info_init!(env, function_value); + + let mut it = function_value.get_param_iter(); + let closure_ptr = it.next().unwrap().into_pointer_value(); + let value_ptr1 = it.next().unwrap().into_pointer_value(); + let value_ptr2 = it.next().unwrap().into_pointer_value(); + + closure_ptr.set_name(Symbol::ARG_1.as_str(&env.interns)); + value_ptr1.set_name(Symbol::ARG_2.as_str(&env.interns)); + value_ptr2.set_name(Symbol::ARG_3.as_str(&env.interns)); + + let value_type = + basic_type_from_layout(env, layout_interner, layout_interner.get_repr(layout)); + let value_ptr_type = value_type.ptr_type(AddressSpace::default()); + + let value_cast1 = + env.builder + .new_build_pointer_cast(value_ptr1, value_ptr_type, "load_opaque"); + + let value_cast2 = + env.builder + .new_build_pointer_cast(value_ptr2, value_ptr_type, "load_opaque"); + + let value1 = load_roc_value( + env, + layout_interner, + layout_interner.get_repr(layout), + value_cast1, + "load_opaque", + ); + let value2 = load_roc_value( + env, + layout_interner, + layout_interner.get_repr(layout), + value_cast2, + "load_opaque", + ); + + increment_refcount_layout(env, layout_interner, layout_ids, 1, value1, layout); + increment_refcount_layout(env, layout_interner, layout_ids, 1, value2, layout); + + let default = [value1.into(), value2.into()]; + + let closure_data_repr = closure_data_layout.runtime_representation(); + + let arguments_cast = match layout_interner.get_repr(closure_data_repr) { + LayoutRepr::Struct(&[]) => { + // nothing to add + &default + } + _ => { + let closure_type = basic_type_from_layout( + env, + layout_interner, + layout_interner.get_repr(closure_data_repr), + ); + let closure_ptr_type = closure_type.ptr_type(AddressSpace::default()); + + let closure_cast = env.builder.new_build_pointer_cast( + closure_ptr, + closure_ptr_type, + "load_opaque", + ); + + let closure_data = + env.builder + .new_build_load(closure_type, closure_cast, "load_opaque"); + + env.arena + .alloc([value1.into(), value2.into(), closure_data.into()]) + as &[_] + } + }; + + let call = env.builder.new_build_call( + roc_function, + arguments_cast, + "call_user_defined_compare_function", + ); + + let result = call.try_as_basic_value().left().unwrap(); + + // IMPORTANT! we call a user function, so it has the fast calling convention + call.set_call_convention(FAST_CALL_CONV); + + env.builder.new_build_return(Some(&result)); + + function_value + } + }; + + env.builder.position_at_end(block); + env.builder.set_current_debug_location(di_location); + + function_value +} + +enum BitcodeReturnValue<'ctx> { + List(PointerValue<'ctx>), + Str(PointerValue<'ctx>), + Basic, +} + +impl<'ctx> BitcodeReturnValue<'ctx> { + fn call_and_load_64bit<'a, 'env>( + &self, + env: &Env<'a, 'ctx, 'env>, + arguments: &[BasicValueEnum<'ctx>], + fn_name: &str, + ) -> BasicValueEnum<'ctx> { + match self { + BitcodeReturnValue::List(result) => { + call_void_bitcode_fn(env, arguments, fn_name); + env.builder + .new_build_load(zig_list_type(env), *result, "load_list") + } + BitcodeReturnValue::Str(result) => { + call_void_bitcode_fn(env, arguments, fn_name); + + // we keep a string in the alloca + (*result).into() + } + BitcodeReturnValue::Basic => call_bitcode_fn(env, arguments, fn_name), + } + } + fn call_and_load_wasm<'a, 'env>( + &self, + env: &Env<'a, 'ctx, 'env>, + arguments: &[BasicValueEnum<'ctx>], + fn_name: &str, + ) -> BasicValueEnum<'ctx> { + match self { + BitcodeReturnValue::List(result) => { + call_void_bitcode_fn(env, arguments, fn_name); + env.builder + .new_build_load(zig_list_type(env), *result, "load_list") + } + BitcodeReturnValue::Str(result) => { + call_void_bitcode_fn(env, arguments, fn_name); + env.builder + .new_build_load(zig_str_type(env), *result, "load_list") + } + BitcodeReturnValue::Basic => call_bitcode_fn(env, arguments, fn_name), + } + } +} + +pub(crate) enum BitcodeReturns { + List, + Str, + Basic, +} + +impl BitcodeReturns { + fn additional_arguments(&self) -> usize { + match self { + BitcodeReturns::List | BitcodeReturns::Str => 1, + BitcodeReturns::Basic => 0, + } + } + + fn return_value_64bit<'a, 'ctx>( + &self, + env: &Env<'a, 'ctx, '_>, + arguments: &mut bumpalo::collections::Vec<'a, BasicValueEnum<'ctx>>, + ) -> BitcodeReturnValue<'ctx> { + match self { + BitcodeReturns::List => { + let list_type = super::convert::zig_list_type(env); + + let parent = env + .builder + .get_insert_block() + .and_then(|b| b.get_parent()) + .unwrap(); + + let result = + create_entry_block_alloca(env, parent, list_type.into(), "list_alloca"); + + arguments.push(result.into()); + + BitcodeReturnValue::List(result) + } + BitcodeReturns::Str => { + let str_type = super::convert::zig_str_type(env); + + let parent = env + .builder + .get_insert_block() + .and_then(|b| b.get_parent()) + .unwrap(); + + let result = create_entry_block_alloca(env, parent, str_type.into(), "str_alloca"); + + arguments.push(result.into()); + + BitcodeReturnValue::Str(result) + } + BitcodeReturns::Basic => BitcodeReturnValue::Basic, + } + } + + fn return_value_wasm<'a, 'ctx>( + &self, + env: &Env<'a, 'ctx, '_>, + arguments: &mut bumpalo::collections::Vec<'a, BasicValueEnum<'ctx>>, + ) -> BitcodeReturnValue<'ctx> { + // Wasm follows 64bit despite being 32bit. + // This wrapper is just for clarity at call sites. + self.return_value_64bit(env, arguments) + } + + fn call_and_load_32bit<'ctx>( + &self, + env: &Env<'_, 'ctx, '_>, + arguments: &[BasicValueEnum<'ctx>], + fn_name: &str, + ) -> BasicValueEnum<'ctx> { + let value = call_bitcode_fn(env, arguments, fn_name); + + match self { + BitcodeReturns::List => { + receive_zig_roc_list_32bit(env, value.into_struct_value()).into() + } + BitcodeReturns::Str => receive_zig_roc_str_32bit(env, value.into_struct_value()).into(), + BitcodeReturns::Basic => value, + } + } +} + +fn ptr_len_cap<'ctx>( + env: &Env<'_, 'ctx, '_>, + value: StructValue<'ctx>, +) -> (PointerValue<'ctx>, IntValue<'ctx>, IntValue<'ctx>) { + let ptr_and_len = env + .builder + .build_extract_value(value, 0, "get_list_cap") + .unwrap() + .into_int_value(); + + let upper_word = { + let shift = env.builder.new_build_right_shift( + ptr_and_len, + env.context.i64_type().const_int(32, false), + false, + "list_ptr_shift", + ); + + env.builder + .new_build_int_cast(shift, env.context.i32_type(), "list_ptr_int") + }; + + let lower_word = + env.builder + .new_build_int_cast(ptr_and_len, env.context.i32_type(), "list_len"); + + let len = upper_word; + + let ptr = env.builder.new_build_int_to_ptr( + lower_word, + env.context.i8_type().ptr_type(AddressSpace::default()), + "list_ptr", + ); + + let cap = env + .builder + .build_extract_value(value, 1, "get_list_cap") + .unwrap() + .into_int_value(); + + (ptr, len, cap) +} + +/// Converts the { i64, i32 } struct that zig returns into `list.RocList = type { i8*, i32, i32 }` +fn receive_zig_roc_list_32bit<'ctx>( + env: &Env<'_, 'ctx, '_>, + value: StructValue<'ctx>, +) -> StructValue<'ctx> { + let list_type = super::convert::zig_list_type(env); + + let (ptr, len, cap) = ptr_len_cap(env, value); + + struct_from_fields( + env, + list_type, + [(0, ptr.into()), (1, len.into()), (2, cap.into())].into_iter(), + ) +} + +/// Converts the { i64, i32 } struct that zig returns into `list.RocList = type { i8*, i32, i32 }` +fn receive_zig_roc_str_32bit<'ctx>( + env: &Env<'_, 'ctx, '_>, + value: StructValue<'ctx>, +) -> StructValue<'ctx> { + let str_type = super::convert::zig_str_type(env); + + let (ptr, len, cap) = ptr_len_cap(env, value); + + struct_from_fields( + env, + str_type, + [(0, ptr.into()), (1, len.into()), (2, cap.into())].into_iter(), + ) +} + +pub(crate) fn pass_list_to_zig_64bit<'ctx>( + env: &Env<'_, 'ctx, '_>, + list: BasicValueEnum<'ctx>, +) -> PointerValue<'ctx> { + let parent = env + .builder + .get_insert_block() + .and_then(|b| b.get_parent()) + .unwrap(); + + let list_type = super::convert::zig_list_type(env); + let list_alloca = create_entry_block_alloca(env, parent, list_type.into(), "list_alloca"); + + env.builder.new_build_store(list_alloca, list); + + list_alloca +} + +pub(crate) fn pass_list_to_zig_wasm<'ctx>( + env: &Env<'_, 'ctx, '_>, + list: BasicValueEnum<'ctx>, +) -> PointerValue<'ctx> { + let parent = env + .builder + .get_insert_block() + .and_then(|b| b.get_parent()) + .unwrap(); + + let list_type = super::convert::zig_list_type(env); + let list_alloca = create_entry_block_alloca(env, parent, list_type.into(), "list_alloca"); + + env.builder.new_build_store(list_alloca, list); + + list_alloca +} + +pub(crate) fn pass_string_to_zig_wasm<'ctx>( + env: &Env<'_, 'ctx, '_>, + string: BasicValueEnum<'ctx>, +) -> PointerValue<'ctx> { + let parent = env + .builder + .get_insert_block() + .and_then(|b| b.get_parent()) + .unwrap(); + + let string_type = super::convert::zig_str_type(env); + let string_alloca = create_entry_block_alloca(env, parent, string_type.into(), "string_alloca"); + + env.builder.new_build_store(string_alloca, string); + + string_alloca +} + +fn pass_string_to_zig_64bit<'ctx>( + _env: &Env<'_, 'ctx, '_>, + string: BasicValueEnum<'ctx>, +) -> PointerValue<'ctx> { + // we must pass strings by-pointer, and that is already how they are stored + string.into_pointer_value() +} + +pub(crate) fn pass_list_or_string_to_zig_32bit<'ctx>( + env: &Env<'_, 'ctx, '_>, + list_or_string: StructValue<'ctx>, +) -> (IntValue<'ctx>, IntValue<'ctx>) { + let ptr = env + .builder + .build_extract_value(list_or_string, Builtin::WRAPPER_PTR, "list_ptr") + .unwrap() + .into_pointer_value(); + + let ptr = env + .builder + .new_build_ptr_to_int(ptr, env.context.i32_type(), "ptr_to_i32"); + + let len = env + .builder + .build_extract_value(list_or_string, Builtin::WRAPPER_LEN, "list_len") + .unwrap() + .into_int_value(); + + let cap = env + .builder + .build_extract_value(list_or_string, Builtin::WRAPPER_CAPACITY, "list_cap") + .unwrap() + .into_int_value(); + + let int_64_type = env.context.i64_type(); + let len = env + .builder + .new_build_int_z_extend(len, int_64_type, "list_len_64"); + let ptr = env + .builder + .new_build_int_z_extend(ptr, int_64_type, "list_ptr_64"); + + let len_shift = + env.builder + .new_build_left_shift(len, int_64_type.const_int(32, false), "list_len_shift"); + let ptr_len = env.builder.new_build_or(len_shift, ptr, "list_ptr_len"); + + (ptr_len, cap) +} + +pub(crate) fn call_str_bitcode_fn<'ctx>( + env: &Env<'_, 'ctx, '_>, + strings: &[BasicValueEnum<'ctx>], + other_arguments: &[BasicValueEnum<'ctx>], + returns: BitcodeReturns, + fn_name: &str, +) -> BasicValueEnum<'ctx> { + use bumpalo::collections::Vec; + use roc_target::Architecture::*; + + match env.target_info.architecture { + Aarch32 | X86_32 => { + let mut arguments: Vec = + Vec::with_capacity_in(other_arguments.len() + 2 * strings.len(), env.arena); + + for string in strings { + let (a, b) = pass_list_or_string_to_zig_32bit(env, string.into_struct_value()); + arguments.push(a.into()); + arguments.push(b.into()); + } + + arguments.extend(other_arguments); + + returns.call_and_load_32bit(env, &arguments, fn_name) + } + X86_64 | Aarch64 => { + let capacity = other_arguments.len() + strings.len() + returns.additional_arguments(); + let mut arguments: Vec = Vec::with_capacity_in(capacity, env.arena); + + let return_value = returns.return_value_64bit(env, &mut arguments); + + for string in strings { + arguments.push(pass_string_to_zig_64bit(env, *string).into()); + } + + arguments.extend(other_arguments); + + return_value.call_and_load_64bit(env, &arguments, fn_name) + } + Wasm32 => { + let capacity = other_arguments.len() + strings.len() + returns.additional_arguments(); + let mut arguments: Vec = Vec::with_capacity_in(capacity, env.arena); + + let return_value = returns.return_value_wasm(env, &mut arguments); + + for string in strings { + arguments.push(pass_string_to_zig_wasm(env, *string).into()); + } + + arguments.extend(other_arguments); + + return_value.call_and_load_wasm(env, &arguments, fn_name) + } + } +} + +pub(crate) fn call_list_bitcode_fn<'ctx>( + env: &Env<'_, 'ctx, '_>, + lists: &[StructValue<'ctx>], + other_arguments: &[BasicValueEnum<'ctx>], + returns: BitcodeReturns, + fn_name: &str, +) -> BasicValueEnum<'ctx> { + use bumpalo::collections::Vec; + use roc_target::Architecture::*; + + match env.target_info.architecture { + Aarch32 | X86_32 => { + let mut arguments: Vec = + Vec::with_capacity_in(other_arguments.len() + 2 * lists.len(), env.arena); + + for list in lists { + let (a, b) = pass_list_or_string_to_zig_32bit(env, *list); + arguments.push(a.into()); + arguments.push(b.into()); + } + + arguments.extend(other_arguments); + + returns.call_and_load_32bit(env, &arguments, fn_name) + } + X86_64 | Aarch64 => { + let capacity = other_arguments.len() + lists.len() + returns.additional_arguments(); + let mut arguments: Vec = Vec::with_capacity_in(capacity, env.arena); + + let return_value = returns.return_value_64bit(env, &mut arguments); + + for list in lists { + arguments.push(pass_list_to_zig_64bit(env, (*list).into()).into()); + } + + arguments.extend(other_arguments); + + return_value.call_and_load_64bit(env, &arguments, fn_name) + } + Wasm32 => { + let capacity = other_arguments.len() + lists.len() + returns.additional_arguments(); + let mut arguments: Vec = Vec::with_capacity_in(capacity, env.arena); + + let return_value = returns.return_value_wasm(env, &mut arguments); + + for list in lists { + arguments.push(pass_list_to_zig_wasm(env, (*list).into()).into()); + } + + arguments.extend(other_arguments); + + return_value.call_and_load_wasm(env, &arguments, fn_name) + } + } +} diff --git a/crates/compiler/gen_llvm/src/llvm/build.rs b/crates/compiler/gen_llvm/src/llvm/build.rs new file mode 100644 index 0000000000..8500e42607 --- /dev/null +++ b/crates/compiler/gen_llvm/src/llvm/build.rs @@ -0,0 +1,6997 @@ +use crate::llvm::bitcode::call_bitcode_fn; +use crate::llvm::build_list::{self, allocate_list, empty_polymorphic_list}; +use crate::llvm::convert::{ + argument_type_from_layout, basic_type_from_builtin, basic_type_from_layout, zig_str_type, +}; +use crate::llvm::expect::{clone_to_shared_memory, SharedMemoryPointer}; +use crate::llvm::memcpy::build_memcpy; +use crate::llvm::refcounting::{ + build_reset, decrement_refcount_layout, increment_refcount_layout, PointerToRefcount, +}; +use crate::llvm::struct_::{struct_from_fields, RocStruct}; +use crate::llvm::{erased, fn_ptr}; +use bumpalo::collections::Vec; +use bumpalo::Bump; +use inkwell::attributes::{Attribute, AttributeLoc}; +use inkwell::basic_block::BasicBlock; +use inkwell::builder::Builder; +use inkwell::context::Context; +use inkwell::debug_info::{ + AsDIScope, DICompileUnit, DIFlagsConstants, DISubprogram, DebugInfoBuilder, +}; +use inkwell::memory_buffer::MemoryBuffer; +use inkwell::module::{Linkage, Module}; +use inkwell::passes::{PassManager, PassManagerBuilder}; +use inkwell::types::{ + AnyType, BasicMetadataTypeEnum, BasicType, BasicTypeEnum, FloatMathType, FunctionType, + IntMathType, IntType, PointerMathType, StructType, +}; +use inkwell::values::{ + BasicMetadataValueEnum, BasicValue, BasicValueEnum, CallSiteValue, FloatMathValue, + FunctionValue, InstructionOpcode, InstructionValue, IntMathValue, IntValue, PhiValue, + PointerMathValue, PointerValue, StructValue, +}; +use inkwell::{AddressSpace, IntPredicate}; +use inkwell::{FloatPredicate, OptimizationLevel}; +use morphic_lib::{ + CalleeSpecVar, FuncName, FuncSpec, FuncSpecSolutions, ModSolutions, UpdateMode, UpdateModeVar, +}; +use roc_builtins::bitcode::{self, FloatWidth, IntWidth}; +use roc_collections::all::{MutMap, MutSet}; +use roc_debug_flags::dbg_do; +#[cfg(debug_assertions)] +use roc_debug_flags::ROC_PRINT_LLVM_FN_VERIFICATION; +use roc_error_macros::{internal_error, todo_lambda_erasure}; +use roc_module::symbol::{Interns, Symbol}; +use roc_mono::ir::{ + BranchInfo, CallType, CrashTag, EntryPoint, GlueLayouts, HostExposedLambdaSet, + HostExposedLambdaSets, ListLiteralElement, ModifyRc, OptLevel, ProcLayout, SingleEntryPoint, +}; +use roc_mono::layout::{ + Builtin, InLayout, LambdaName, LambdaSet, Layout, LayoutIds, LayoutInterner, LayoutRepr, Niche, + RawFunctionLayout, STLayoutInterner, TagIdIntType, UnionLayout, +}; +use roc_std::RocDec; +use roc_target::{PtrWidth, TargetInfo}; +use std::convert::TryInto; +use std::path::Path; +use target_lexicon::{Aarch64Architecture, Architecture, OperatingSystem, Triple}; + +use super::convert::{struct_type_from_union_layout, RocUnion}; +use super::intrinsics::{ + add_intrinsics, LLVM_FRAME_ADDRESS, LLVM_MEMSET_I32, LLVM_MEMSET_I64, LLVM_SETJMP, + LLVM_STACK_SAVE, +}; +use super::lowlevel::run_higher_order_low_level; +use super::scope::Scope; + +pub(crate) trait BuilderExt<'ctx> { + fn new_build_struct_gep( + &self, + struct_type: StructType<'ctx>, + ptr: PointerValue<'ctx>, + index: u32, + name: &str, + ) -> PointerValue<'ctx>; + + fn new_build_alloca>(&self, ty: T, name: &str) -> PointerValue<'ctx>; + + fn new_build_store>( + &self, + ptr: PointerValue<'ctx>, + value: V, + ) -> InstructionValue<'ctx>; + + fn new_build_load( + &self, + element_type: impl BasicType<'ctx>, + ptr: PointerValue<'ctx>, + name: &str, + ) -> BasicValueEnum<'ctx>; + + unsafe fn new_build_in_bounds_gep( + &self, + element_type: impl BasicType<'ctx>, + ptr: PointerValue<'ctx>, + ordered_indexes: &[IntValue<'ctx>], + name: &str, + ) -> PointerValue<'ctx>; + + fn new_build_cast, V: BasicValue<'ctx>>( + &self, + op: InstructionOpcode, + from_value: V, + to_type: T, + name: &str, + ) -> BasicValueEnum<'ctx>; + + fn new_build_bitcast(&self, val: V, ty: T, name: &str) -> BasicValueEnum<'ctx> + where + T: BasicType<'ctx>, + V: BasicValue<'ctx>; + + fn new_build_pointer_cast>( + &self, + from: T, + to: T::BaseType, + name: &str, + ) -> T; + + fn new_build_int_cast>( + &self, + int: T, + int_type: T::BaseType, + name: &str, + ) -> T; + + fn new_build_int_cast_sign_flag>( + &self, + int: T, + int_type: T::BaseType, + is_signed: bool, + name: &str, + ) -> T; + + fn new_build_float_cast>( + &self, + float: T, + float_type: T::BaseType, + name: &str, + ) -> T; + + fn new_build_call( + &self, + function: FunctionValue<'ctx>, + args: &[BasicMetadataValueEnum<'ctx>], + name: &str, + ) -> CallSiteValue<'ctx>; + + fn new_build_indirect_call( + &self, + function_type: FunctionType<'ctx>, + function_pointer: PointerValue<'ctx>, + args: &[BasicMetadataValueEnum<'ctx>], + name: &str, + ) -> CallSiteValue<'ctx>; + + fn new_build_ptr_to_int>( + &self, + ptr: T, + int_type: >::PtrConvType, + name: &str, + ) -> <>::PtrConvType as IntMathType<'ctx>>::ValueType; + + fn new_build_int_to_ptr>( + &self, + int: T, + ptr_type: >::PtrConvType, + name: &str, + ) -> <>::PtrConvType as PointerMathType<'ctx>>::ValueType; + + fn new_build_is_null>( + &self, + ptr: T, + name: &str, + ) -> <>::PtrConvType as IntMathType<'ctx>>::ValueType; + + fn new_build_is_not_null>( + &self, + ptr: T, + name: &str, + ) -> <>::PtrConvType as IntMathType<'ctx>>::ValueType; + + fn new_build_phi>(&self, type_: T, name: &str) -> PhiValue<'ctx>; + + fn new_build_select, IMV: IntMathValue<'ctx>>( + &self, + condition: IMV, + then: BV, + else_: BV, + name: &str, + ) -> BasicValueEnum<'ctx>; + + fn new_build_and>(&self, lhs: T, rhs: T, name: &str) -> T; + + fn new_build_or>(&self, lhs: T, rhs: T, name: &str) -> T; + + fn new_build_xor>(&self, lhs: T, rhs: T, name: &str) -> T; + + fn new_build_not>(&self, value: T, name: &str) -> T; + + fn new_build_int_neg>(&self, value: T, name: &str) -> T; + + fn new_build_int_add>(&self, lhs: T, rhs: T, name: &str) -> T; + + fn new_build_int_sub>(&self, lhs: T, rhs: T, name: &str) -> T; + + fn new_build_int_mul>(&self, lhs: T, rhs: T, name: &str) -> T; + + fn new_build_int_signed_rem>(&self, lhs: T, rhs: T, name: &str) -> T; + + fn new_build_int_unsigned_rem>(&self, lhs: T, rhs: T, name: &str) -> T; + + fn new_build_int_signed_div>(&self, lhs: T, rhs: T, name: &str) -> T; + + fn new_build_int_unsigned_div>(&self, lhs: T, rhs: T, name: &str) -> T; + + fn new_build_int_compare>( + &self, + op: IntPredicate, + lhs: T, + rhs: T, + name: &str, + ) -> >::ValueType; + + fn new_build_float_neg>(&self, value: T, name: &str) -> T; + + fn new_build_float_add>(&self, lhs: T, rhs: T, name: &str) -> T; + + fn new_build_float_sub>(&self, lhs: T, rhs: T, name: &str) -> T; + + fn new_build_float_mul>(&self, lhs: T, rhs: T, name: &str) -> T; + + fn new_build_float_div>(&self, lhs: T, rhs: T, name: &str) -> T; + + fn new_build_float_compare>( + &self, + op: FloatPredicate, + lhs: T, + rhs: T, + name: &str, + ) -> <>::MathConvType as IntMathType<'ctx>>::ValueType; + + fn new_build_right_shift>( + &self, + lhs: T, + rhs: T, + sign_extend: bool, + name: &str, + ) -> T; + + fn new_build_left_shift>(&self, lhs: T, rhs: T, name: &str) -> T; + + fn new_build_int_z_extend>( + &self, + int_value: T, + int_type: T::BaseType, + name: &str, + ) -> T; + + fn new_build_signed_int_to_float>( + &self, + int: T, + float_type: >::MathConvType, + name: &str, + ) -> <>::MathConvType as FloatMathType<'ctx>>::ValueType; + + fn new_build_unsigned_int_to_float>( + &self, + int: T, + float_type: >::MathConvType, + name: &str, + ) -> <>::MathConvType as FloatMathType<'ctx>>::ValueType; + + fn new_build_return(&self, value: Option<&dyn BasicValue<'ctx>>) -> InstructionValue<'ctx>; + + fn new_build_switch( + &self, + value: IntValue<'ctx>, + else_block: BasicBlock<'ctx>, + cases: &[(IntValue<'ctx>, BasicBlock<'ctx>)], + ) -> InstructionValue<'ctx>; + + fn new_build_conditional_branch( + &self, + comparison: IntValue<'ctx>, + then_block: BasicBlock<'ctx>, + else_block: BasicBlock<'ctx>, + ) -> InstructionValue<'ctx>; + + fn new_build_unconditional_branch( + &self, + destination_block: BasicBlock<'ctx>, + ) -> InstructionValue<'ctx>; + + fn new_build_unreachable(&self) -> InstructionValue<'ctx>; + + fn new_build_free(&self, ptr: PointerValue<'ctx>) -> InstructionValue<'ctx>; +} + +impl<'ctx> BuilderExt<'ctx> for Builder<'ctx> { + fn new_build_struct_gep( + &self, + struct_type: StructType<'ctx>, + ptr: PointerValue<'ctx>, + index: u32, + name: &str, + ) -> PointerValue<'ctx> { + // debug_assert_eq!( + // ptr.get_type().get_element_type().into_struct_type(), + // struct_type + // ); + self.build_struct_gep(struct_type, ptr, index, name) + .unwrap() + } + + fn new_build_alloca>(&self, ty: T, name: &str) -> PointerValue<'ctx> { + self.build_alloca(ty, name).unwrap() + } + + fn new_build_store>( + &self, + ptr: PointerValue<'ctx>, + value: V, + ) -> InstructionValue<'ctx> { + self.build_store(ptr, value).unwrap() + } + + fn new_build_load( + &self, + element_type: impl BasicType<'ctx>, + ptr: PointerValue<'ctx>, + name: &str, + ) -> BasicValueEnum<'ctx> { + // debug_assert_eq!( + // ptr.get_type().get_element_type(), + // element_type.as_any_type_enum() + // ); + self.build_load(element_type, ptr, name).unwrap() + } + + unsafe fn new_build_in_bounds_gep( + &self, + element_type: impl BasicType<'ctx>, + ptr: PointerValue<'ctx>, + ordered_indexes: &[IntValue<'ctx>], + name: &str, + ) -> PointerValue<'ctx> { + // debug_assert_eq!( + // ptr.get_type().get_element_type(), + // element_type.as_any_type_enum() + // ); + + self.build_in_bounds_gep(element_type, ptr, ordered_indexes, name) + .unwrap() + } + + fn new_build_cast, V: BasicValue<'ctx>>( + &self, + op: InstructionOpcode, + from_value: V, + to_type: T, + name: &str, + ) -> BasicValueEnum<'ctx> { + self.build_cast(op, from_value, to_type, name).unwrap() + } + + fn new_build_bitcast(&self, val: V, ty: T, name: &str) -> BasicValueEnum<'ctx> + where + T: BasicType<'ctx>, + V: BasicValue<'ctx>, + { + self.build_bitcast(val, ty, name).unwrap() + } + + fn new_build_pointer_cast>( + &self, + from: T, + to: T::BaseType, + name: &str, + ) -> T { + self.build_pointer_cast(from, to, name).unwrap() + } + + fn new_build_int_cast>( + &self, + int: T, + int_type: T::BaseType, + name: &str, + ) -> T { + self.build_int_cast(int, int_type, name).unwrap() + } + + fn new_build_int_cast_sign_flag>( + &self, + int: T, + int_type: T::BaseType, + is_signed: bool, + name: &str, + ) -> T { + self.build_int_cast_sign_flag(int, int_type, is_signed, name) + .unwrap() + } + + fn new_build_float_cast>( + &self, + float: T, + float_type: T::BaseType, + name: &str, + ) -> T { + self.build_float_cast(float, float_type, name).unwrap() + } + + fn new_build_call( + &self, + function: FunctionValue<'ctx>, + args: &[BasicMetadataValueEnum<'ctx>], + name: &str, + ) -> CallSiteValue<'ctx> { + self.build_call(function, args, name).unwrap() + } + + fn new_build_indirect_call( + &self, + function_type: FunctionType<'ctx>, + function_pointer: PointerValue<'ctx>, + args: &[BasicMetadataValueEnum<'ctx>], + name: &str, + ) -> CallSiteValue<'ctx> { + self.build_indirect_call(function_type, function_pointer, args, name) + .unwrap() + } + + fn new_build_ptr_to_int>( + &self, + ptr: T, + int_type: >::PtrConvType, + name: &str, + ) -> <>::PtrConvType as IntMathType<'ctx>>::ValueType { + self.build_ptr_to_int(ptr, int_type, name).unwrap() + } + + fn new_build_int_to_ptr>( + &self, + int: T, + ptr_type: >::PtrConvType, + name: &str, + ) -> <>::PtrConvType as PointerMathType<'ctx>>::ValueType { + self.build_int_to_ptr(int, ptr_type, name).unwrap() + } + + fn new_build_is_null>( + &self, + ptr: T, + name: &str, + ) -> <>::PtrConvType as IntMathType<'ctx>>::ValueType { + self.build_is_null(ptr, name).unwrap() + } + + fn new_build_is_not_null>( + &self, + ptr: T, + name: &str, + ) -> <>::PtrConvType as IntMathType<'ctx>>::ValueType { + self.build_is_not_null(ptr, name).unwrap() + } + + fn new_build_phi>(&self, type_: T, name: &str) -> PhiValue<'ctx> { + self.build_phi(type_, name).unwrap() + } + + fn new_build_select, IMV: IntMathValue<'ctx>>( + &self, + condition: IMV, + then: BV, + else_: BV, + name: &str, + ) -> BasicValueEnum<'ctx> { + self.build_select(condition, then, else_, name).unwrap() + } + + fn new_build_and>(&self, lhs: T, rhs: T, name: &str) -> T { + self.build_and(lhs, rhs, name).unwrap() + } + + fn new_build_or>(&self, lhs: T, rhs: T, name: &str) -> T { + self.build_or(lhs, rhs, name).unwrap() + } + + fn new_build_xor>(&self, lhs: T, rhs: T, name: &str) -> T { + self.build_xor(lhs, rhs, name).unwrap() + } + + fn new_build_not>(&self, value: T, name: &str) -> T { + self.build_not(value, name).unwrap() + } + + fn new_build_int_neg>(&self, value: T, name: &str) -> T { + self.build_int_neg(value, name).unwrap() + } + + fn new_build_int_add>(&self, lhs: T, rhs: T, name: &str) -> T { + self.build_int_add(lhs, rhs, name).unwrap() + } + + fn new_build_int_sub>(&self, lhs: T, rhs: T, name: &str) -> T { + self.build_int_sub(lhs, rhs, name).unwrap() + } + + fn new_build_int_mul>(&self, lhs: T, rhs: T, name: &str) -> T { + self.build_int_mul(lhs, rhs, name).unwrap() + } + + fn new_build_int_signed_rem>(&self, lhs: T, rhs: T, name: &str) -> T { + self.build_int_signed_rem(lhs, rhs, name).unwrap() + } + + fn new_build_int_unsigned_rem>(&self, lhs: T, rhs: T, name: &str) -> T { + self.build_int_unsigned_rem(lhs, rhs, name).unwrap() + } + + fn new_build_int_signed_div>(&self, lhs: T, rhs: T, name: &str) -> T { + self.build_int_signed_div(lhs, rhs, name).unwrap() + } + + fn new_build_int_unsigned_div>(&self, lhs: T, rhs: T, name: &str) -> T { + self.build_int_unsigned_div(lhs, rhs, name).unwrap() + } + + fn new_build_int_compare>( + &self, + op: IntPredicate, + lhs: T, + rhs: T, + name: &str, + ) -> >::ValueType { + self.build_int_compare(op, lhs, rhs, name).unwrap() + } + + fn new_build_float_neg>(&self, value: T, name: &str) -> T { + self.build_float_neg(value, name).unwrap() + } + + fn new_build_float_add>(&self, lhs: T, rhs: T, name: &str) -> T { + self.build_float_add(lhs, rhs, name).unwrap() + } + + fn new_build_float_sub>(&self, lhs: T, rhs: T, name: &str) -> T { + self.build_float_sub(lhs, rhs, name).unwrap() + } + + fn new_build_float_mul>(&self, lhs: T, rhs: T, name: &str) -> T { + self.build_float_mul(lhs, rhs, name).unwrap() + } + + fn new_build_float_div>(&self, lhs: T, rhs: T, name: &str) -> T { + self.build_float_div(lhs, rhs, name).unwrap() + } + + fn new_build_float_compare>( + &self, + op: FloatPredicate, + lhs: T, + rhs: T, + name: &str, + ) -> <>::MathConvType as IntMathType<'ctx>>::ValueType { + self.build_float_compare(op, lhs, rhs, name).unwrap() + } + + fn new_build_right_shift>( + &self, + lhs: T, + rhs: T, + sign_extend: bool, + name: &str, + ) -> T { + self.build_right_shift(lhs, rhs, sign_extend, name).unwrap() + } + + fn new_build_left_shift>(&self, lhs: T, rhs: T, name: &str) -> T { + self.build_left_shift(lhs, rhs, name).unwrap() + } + + fn new_build_int_z_extend>( + &self, + int_value: T, + int_type: T::BaseType, + name: &str, + ) -> T { + self.build_int_z_extend(int_value, int_type, name).unwrap() + } + + fn new_build_signed_int_to_float>( + &self, + int: T, + float_type: >::MathConvType, + name: &str, + ) -> <>::MathConvType as FloatMathType<'ctx>>::ValueType { + self.build_signed_int_to_float(int, float_type, name) + .unwrap() + } + + fn new_build_unsigned_int_to_float>( + &self, + int: T, + float_type: >::MathConvType, + name: &str, + ) -> <>::MathConvType as FloatMathType<'ctx>>::ValueType { + self.build_unsigned_int_to_float(int, float_type, name) + .unwrap() + } + + fn new_build_return(&self, value: Option<&dyn BasicValue<'ctx>>) -> InstructionValue<'ctx> { + self.build_return(value).unwrap() + } + + fn new_build_switch( + &self, + value: IntValue<'ctx>, + else_block: BasicBlock<'ctx>, + cases: &[(IntValue<'ctx>, BasicBlock<'ctx>)], + ) -> InstructionValue<'ctx> { + self.build_switch(value, else_block, cases).unwrap() + } + + fn new_build_conditional_branch( + &self, + comparison: IntValue<'ctx>, + then_block: BasicBlock<'ctx>, + else_block: BasicBlock<'ctx>, + ) -> InstructionValue<'ctx> { + self.build_conditional_branch(comparison, then_block, else_block) + .unwrap() + } + + fn new_build_unconditional_branch( + &self, + destination_block: BasicBlock<'ctx>, + ) -> InstructionValue<'ctx> { + self.build_unconditional_branch(destination_block).unwrap() + } + + fn new_build_unreachable(&self) -> InstructionValue<'ctx> { + self.build_unreachable().unwrap() + } + + fn new_build_free(&self, ptr: PointerValue<'ctx>) -> InstructionValue<'ctx> { + self.build_free(ptr).unwrap() + } +} + +#[inline(always)] +fn print_fn_verification_output() -> bool { + dbg_do!(ROC_PRINT_LLVM_FN_VERIFICATION, { + return true; + }); + false +} + +#[macro_export] +macro_rules! debug_info_init { + ($env:expr, $function_value:expr) => {{ + use inkwell::debug_info::AsDIScope; + + let func_scope = $function_value.get_subprogram().expect("subprogram"); + let lexical_block = $env.dibuilder.create_lexical_block( + /* scope */ func_scope.as_debug_info_scope(), + /* file */ $env.compile_unit.get_file(), + /* line_no */ 0, + /* column_no */ 0, + ); + + let loc = $env.dibuilder.create_debug_location( + $env.context, + /* line */ 0, + /* column */ 0, + /* current_scope */ lexical_block.as_debug_info_scope(), + /* inlined_at */ None, + ); + $env.builder.set_current_debug_location(loc); + }}; +} + +#[derive(Debug, Clone, Copy)] +pub enum LlvmBackendMode { + /// Assumes primitives (roc_alloc, roc_panic, etc) are provided by the host + Binary, + BinaryDev, + /// Creates a test wrapper around the main roc function to catch and report panics. + /// Provides a testing implementation of primitives (roc_alloc, roc_panic, etc) + BinaryGlue, + GenTest, + WasmGenTest, + CliTest, +} + +impl LlvmBackendMode { + pub(crate) fn has_host(self) -> bool { + match self { + LlvmBackendMode::Binary => true, + LlvmBackendMode::BinaryDev => true, + LlvmBackendMode::BinaryGlue => false, + LlvmBackendMode::GenTest => false, + LlvmBackendMode::WasmGenTest => true, + LlvmBackendMode::CliTest => false, + } + } + + /// In other words, catches exceptions and returns a result + fn returns_roc_result(self) -> bool { + match self { + LlvmBackendMode::Binary => false, + LlvmBackendMode::BinaryDev => false, + LlvmBackendMode::BinaryGlue => false, + LlvmBackendMode::GenTest => true, + LlvmBackendMode::WasmGenTest => true, + LlvmBackendMode::CliTest => true, + } + } + + pub(crate) fn runs_expects(self) -> bool { + match self { + LlvmBackendMode::Binary => false, + LlvmBackendMode::BinaryDev => true, + LlvmBackendMode::BinaryGlue => false, + LlvmBackendMode::GenTest => false, + LlvmBackendMode::WasmGenTest => false, + LlvmBackendMode::CliTest => true, + } + } +} + +pub struct Env<'a, 'ctx, 'env> { + pub arena: &'a Bump, + pub context: &'ctx Context, + pub builder: &'env Builder<'ctx>, + pub dibuilder: &'env DebugInfoBuilder<'ctx>, + pub compile_unit: &'env DICompileUnit<'ctx>, + pub module: &'ctx Module<'ctx>, + pub interns: Interns, + pub target_info: TargetInfo, + pub mode: LlvmBackendMode, + pub exposed_to_host: MutSet, +} + +impl<'a, 'ctx, 'env> Env<'a, 'ctx, 'env> { + /// The integer type representing a pointer + /// + /// on 64-bit systems, this is i64 + /// on 32-bit systems, this is i32 + pub fn ptr_int(&self) -> IntType<'ctx> { + let ctx = self.context; + + match self.target_info.ptr_width() { + roc_target::PtrWidth::Bytes4 => ctx.i32_type(), + roc_target::PtrWidth::Bytes8 => ctx.i64_type(), + } + } + + /// The integer type representing twice the width of a pointer + /// + /// on 64-bit systems, this is i128 + /// on 32-bit systems, this is i64 + pub fn twice_ptr_int(&self) -> IntType<'ctx> { + let ctx = self.context; + + match self.target_info.ptr_width() { + roc_target::PtrWidth::Bytes4 => ctx.i64_type(), + roc_target::PtrWidth::Bytes8 => ctx.i128_type(), + } + } + + pub fn small_str_bytes(&self) -> u32 { + self.target_info.ptr_width() as u32 * 3 + } + + pub fn build_intrinsic_call( + &self, + intrinsic_name: &'static str, + args: &[BasicValueEnum<'ctx>], + ) -> CallSiteValue<'ctx> { + let fn_val = self + .module + .get_function(intrinsic_name) + .unwrap_or_else(|| panic!("Unrecognized intrinsic function: {intrinsic_name}")); + + let mut arg_vals: Vec = + Vec::with_capacity_in(args.len(), self.arena); + + for arg in args.iter() { + arg_vals.push((*arg).into()); + } + + let call = self + .builder + .new_build_call(fn_val, arg_vals.into_bump_slice(), "call"); + + call.set_call_convention(fn_val.get_call_conventions()); + + call + } + + pub fn call_intrinsic( + &self, + intrinsic_name: &'static str, + args: &[BasicValueEnum<'ctx>], + ) -> BasicValueEnum<'ctx> { + let call = self.build_intrinsic_call(intrinsic_name, args); + + call.try_as_basic_value().left().unwrap_or_else(|| { + panic!("LLVM error: Invalid call by name for intrinsic {intrinsic_name}") + }) + } + + pub fn alignment_type(&self) -> IntType<'ctx> { + self.context.i32_type() + } + + pub fn alignment_const(&self, alignment: u32) -> IntValue<'ctx> { + self.alignment_type().const_int(alignment as u64, false) + } + + pub fn alignment_intvalue( + &self, + layout_interner: &STLayoutInterner<'a>, + element_layout: InLayout<'a>, + ) -> BasicValueEnum<'ctx> { + let alignment = layout_interner.alignment_bytes(element_layout); + let alignment_iv = self.alignment_const(alignment); + + alignment_iv.into() + } + + pub fn call_alloc( + &self, + number_of_bytes: IntValue<'ctx>, + alignment: u32, + ) -> PointerValue<'ctx> { + let function = self.module.get_function("roc_alloc").unwrap(); + let alignment = self.alignment_const(alignment); + let call = self.builder.new_build_call( + function, + &[number_of_bytes.into(), alignment.into()], + "roc_alloc", + ); + + call.set_call_convention(C_CALL_CONV); + + call.try_as_basic_value() + .left() + .unwrap() + .into_pointer_value() + // TODO check if alloc returned null; if so, runtime error for OOM! + } + + pub fn call_dealloc(&self, ptr: PointerValue<'ctx>, alignment: u32) -> InstructionValue<'ctx> { + let function = self.module.get_function("roc_dealloc").unwrap(); + let alignment = self.alignment_const(alignment); + let call = + self.builder + .new_build_call(function, &[ptr.into(), alignment.into()], "roc_dealloc"); + + call.set_call_convention(C_CALL_CONV); + + call.try_as_basic_value().right().unwrap() + } + + pub fn call_memset( + &self, + bytes_ptr: PointerValue<'ctx>, + filler: IntValue<'ctx>, + length: IntValue<'ctx>, + ) -> CallSiteValue<'ctx> { + let false_val = self.context.bool_type().const_int(0, false); + + let intrinsic_name = match self.target_info.ptr_width() { + roc_target::PtrWidth::Bytes8 => LLVM_MEMSET_I64, + roc_target::PtrWidth::Bytes4 => LLVM_MEMSET_I32, + }; + + self.build_intrinsic_call( + intrinsic_name, + &[ + bytes_ptr.into(), + filler.into(), + length.into(), + false_val.into(), + ], + ) + } + + pub fn call_panic( + &self, + env: &Env<'a, 'ctx, 'env>, + message: BasicValueEnum<'ctx>, + tag: CrashTag, + ) { + let function = self.module.get_function("roc_panic").unwrap(); + let tag_id = self.context.i32_type().const_int(tag as u32 as u64, false); + + let msg = self.string_to_arg(env, message); + + let call = self + .builder + .new_build_call(function, &[msg.into(), tag_id.into()], "roc_panic"); + + call.set_call_convention(C_CALL_CONV); + } + + pub fn call_dbg( + &self, + env: &Env<'a, 'ctx, 'env>, + location: BasicValueEnum<'ctx>, + message: BasicValueEnum<'ctx>, + ) { + let function = self.module.get_function("roc_dbg").unwrap(); + + let loc = self.string_to_arg(env, location); + let msg = self.string_to_arg(env, message); + + let call = self + .builder + .new_build_call(function, &[loc.into(), msg.into()], "roc_dbg"); + + call.set_call_convention(C_CALL_CONV); + } + + fn string_to_arg( + &self, + env: &Env<'a, 'ctx, 'env>, + string: BasicValueEnum<'ctx>, + ) -> BasicValueEnum<'ctx> { + match env.target_info.ptr_width() { + PtrWidth::Bytes4 => { + // we need to pass the string by reference, but we currently hold the value. + let alloca = env + .builder + .new_build_alloca(string.get_type(), "alloca_string"); + env.builder.new_build_store(alloca, string); + alloca.into() + } + PtrWidth::Bytes8 => { + // string is already held by reference + string + } + } + } + + pub fn new_debug_info(module: &Module<'ctx>) -> (DebugInfoBuilder<'ctx>, DICompileUnit<'ctx>) { + module.create_debug_info_builder( + true, + /* language */ inkwell::debug_info::DWARFSourceLanguage::C, + /* filename */ "roc_app", + /* directory */ ".", + /* producer */ "my llvm compiler frontend", + /* is_optimized */ false, + /* compiler command line flags */ "", + /* runtime_ver */ 0, + /* split_name */ "", + /* kind */ inkwell::debug_info::DWARFEmissionKind::Full, + /* dwo_id */ 0, + /* split_debug_inling */ false, + /* debug_info_for_profiling */ false, + "", + "", + ) + } + + pub fn new_subprogram(&self, function_name: &str) -> DISubprogram<'ctx> { + let dibuilder = self.dibuilder; + let compile_unit = self.compile_unit; + + let ditype = dibuilder + .create_basic_type( + "type_name", + 0_u64, + 0x00, + inkwell::debug_info::DIFlags::PUBLIC, + ) + .unwrap(); + + let subroutine_type = dibuilder.create_subroutine_type( + compile_unit.get_file(), + /* return type */ Some(ditype.as_type()), + /* parameter types */ &[], + inkwell::debug_info::DIFlags::PUBLIC, + ); + + dibuilder.create_function( + /* scope */ compile_unit.get_file().as_debug_info_scope(), + /* func name */ function_name, + /* linkage_name */ None, + /* file */ compile_unit.get_file(), + /* line_no */ 0, + /* DIType */ subroutine_type, + /* is_local_to_unit */ true, + /* is_definition */ true, + /* scope_line */ 0, + /* flags */ inkwell::debug_info::DIFlags::PUBLIC, + /* is_optimized */ false, + ) + } +} + +pub fn module_from_builtins<'ctx>( + target: &target_lexicon::Triple, + ctx: &'ctx Context, + module_name: &str, +) -> Module<'ctx> { + // In the build script for the builtins module, we compile the builtins into LLVM bitcode + + let bitcode_bytes: &[u8] = if target == &target_lexicon::Triple::host() { + include_bytes!("../../../builtins/bitcode/zig-out/builtins-host.bc") + } else { + match target { + Triple { + architecture: Architecture::Wasm32, + .. + } => { + include_bytes!("../../../builtins/bitcode/zig-out/builtins-wasm32.bc") + } + Triple { + architecture: Architecture::X86_32(_), + operating_system: OperatingSystem::Linux, + .. + } => { + include_bytes!("../../../builtins/bitcode/zig-out/builtins-x86.bc") + } + Triple { + architecture: Architecture::X86_64, + operating_system: OperatingSystem::Linux, + .. + } => { + include_bytes!("../../../builtins/bitcode/zig-out/builtins-x86_64.bc") + } + Triple { + architecture: Architecture::Aarch64(Aarch64Architecture::Aarch64), + operating_system: OperatingSystem::Linux, + .. + } => { + include_bytes!("../../../builtins/bitcode/zig-out/builtins-aarch64.bc") + } + Triple { + architecture: Architecture::X86_64, + operating_system: OperatingSystem::Windows, + .. + } => { + include_bytes!("../../../builtins/bitcode/zig-out/builtins-windows-x86_64.bc") + } + _ => panic!("The zig builtins are not currently built for this target: {target:?}"), + } + }; + + let memory_buffer = MemoryBuffer::create_from_memory_range(bitcode_bytes, module_name); + + let module = Module::parse_bitcode_from_buffer(&memory_buffer, ctx) + .unwrap_or_else(|err| panic!("Unable to import builtins bitcode. LLVM error: {err:?}")); + + // Add LLVM intrinsics. + add_intrinsics(ctx, &module); + + module +} + +pub fn construct_optimization_passes<'a>( + module: &'a Module, + opt_level: OptLevel, +) -> (PassManager>, PassManager>) { + let mpm = PassManager::create(()); + let fpm = PassManager::create(module); + + // remove unused global values (e.g. those defined by zig, but unused in user code) + mpm.add_global_dce_pass(); + + mpm.add_always_inliner_pass(); + + // tail-call elimination is always on + fpm.add_instruction_combining_pass(); + fpm.add_tail_call_elimination_pass(); + + let pmb = PassManagerBuilder::create(); + match opt_level { + OptLevel::Development | OptLevel::Normal => { + pmb.set_optimization_level(OptimizationLevel::None); + } + OptLevel::Size => { + pmb.set_optimization_level(OptimizationLevel::Default); + // TODO: For some usecase, like embedded, it is useful to expose this and tune it. + pmb.set_inliner_with_threshold(50); + } + OptLevel::Optimize => { + pmb.set_optimization_level(OptimizationLevel::Aggressive); + // this threshold seems to do what we want + pmb.set_inliner_with_threshold(750); + } + } + + // Add optimization passes for Size and Optimize. + if matches!(opt_level, OptLevel::Size | OptLevel::Optimize) { + // TODO figure out which of these actually help + + // function passes + + fpm.add_cfg_simplification_pass(); + mpm.add_cfg_simplification_pass(); + + fpm.add_jump_threading_pass(); + mpm.add_jump_threading_pass(); + + fpm.add_memcpy_optimize_pass(); // this one is very important + + fpm.add_licm_pass(); + + // turn invoke into call + // TODO: is this pass needed. It theoretically prunes unused exception handling info. + // This seems unrelated to the comment above. It also seems to be missing in llvm-16. + // mpm.add_prune_eh_pass(); + + // remove unused global values (often the `_wrapper` can be removed) + mpm.add_global_dce_pass(); + + mpm.add_function_inlining_pass(); + } + + pmb.populate_module_pass_manager(&mpm); + pmb.populate_function_pass_manager(&fpm); + + fpm.initialize(); + + // For now, we have just one of each + (mpm, fpm) +} + +fn promote_to_main_function<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + mod_solutions: &'a ModSolutions, + symbol: Symbol, + top_level: ProcLayout<'a>, +) -> (&'static str, FunctionValue<'ctx>) { + let it = top_level.arguments.iter().copied(); + let bytes = roc_alias_analysis::func_name_bytes_help(symbol, it, Niche::NONE, top_level.result); + let func_name = FuncName(&bytes); + let func_solutions = mod_solutions.func_solutions(func_name).unwrap(); + + let mut it = func_solutions.specs(); + let func_spec = it.next().unwrap(); + debug_assert!( + it.next().is_none(), + "we expect only one specialization of this symbol" + ); + + // NOTE fake layout; it is only used for debug prints + let roc_main_fn = function_value_by_func_spec(env, FuncBorrowSpec::Some(*func_spec), symbol); + + let main_fn_name = "$Test.main"; + + // Add main to the module. + let main_fn = expose_function_to_host_help_c_abi( + env, + layout_interner, + main_fn_name, + roc_main_fn, + top_level.arguments, + top_level.result, + main_fn_name, + ); + + (main_fn_name, main_fn) +} + +fn promote_to_wasm_test_wrapper<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + mod_solutions: &'a ModSolutions, + symbol: Symbol, + top_level: ProcLayout<'a>, +) -> (&'static str, FunctionValue<'ctx>) { + // generates roughly + // + // fn test_wrapper() -> *T { + // result = roc_main(); + // ptr = roc_malloc(size_of::) + // *ptr = result + // ret ptr; + // } + + let main_fn_name = "test_wrapper"; + + let it = top_level.arguments.iter().copied(); + let bytes = roc_alias_analysis::func_name_bytes_help(symbol, it, Niche::NONE, top_level.result); + let func_name = FuncName(&bytes); + let func_solutions = mod_solutions.func_solutions(func_name).unwrap(); + + let mut it = func_solutions.specs(); + let func_spec = it.next().unwrap(); + debug_assert!( + it.next().is_none(), + "we expect only one specialization of this symbol" + ); + + // NOTE fake layout; it is only used for debug prints + let roc_main_fn = function_value_by_func_spec(env, FuncBorrowSpec::Some(*func_spec), symbol); + + let output_type = match roc_main_fn.get_type().get_return_type() { + Some(return_type) => { + let output_type = return_type.ptr_type(AddressSpace::default()); + output_type.into() + } + None => { + assert_eq!(roc_main_fn.get_type().get_param_types().len(), 1); + let output_type = roc_main_fn.get_type().get_param_types()[0]; + output_type + } + }; + + let main_fn = { + let c_function_spec = FunctionSpec::cconv(env, CCReturn::Return, Some(output_type), &[]); + + let c_function = add_func( + env.context, + env.module, + main_fn_name, + c_function_spec, + Linkage::External, + ); + + let subprogram = env.new_subprogram(main_fn_name); + c_function.set_subprogram(subprogram); + + // STEP 2: build the exposed function's body + let builder = env.builder; + let context = env.context; + + let entry = context.append_basic_block(c_function, "entry"); + builder.position_at_end(entry); + + let roc_main_fn_result = call_direct_roc_function( + env, + layout_interner, + roc_main_fn, + layout_interner.get_repr(top_level.result), + &[], + ); + + // For consistency, we always return with a heap-allocated value + let (size, alignment) = layout_interner.stack_size_and_alignment(top_level.result); + let number_of_bytes = env.ptr_int().const_int(size as _, false); + let void_ptr = env.call_alloc(number_of_bytes, alignment); + + let ptr = + builder.new_build_pointer_cast(void_ptr, output_type.into_pointer_type(), "cast_ptr"); + + store_roc_value( + env, + layout_interner, + layout_interner.get_repr(top_level.result), + ptr, + roc_main_fn_result, + ); + + builder.new_build_return(Some(&ptr)); + + c_function + }; + + (main_fn_name, main_fn) +} + +fn int_with_precision<'ctx>( + env: &Env<'_, 'ctx, '_>, + value: i128, + int_width: IntWidth, +) -> IntValue<'ctx> { + use IntWidth::*; + + match int_width { + U128 | I128 => const_i128(env, value), + U64 | I64 => env.context.i64_type().const_int(value as u64, false), + U32 | I32 => env.context.i32_type().const_int(value as u64, false), + U16 | I16 => env.context.i16_type().const_int(value as u64, false), + U8 | I8 => env.context.i8_type().const_int(value as u64, false), + } +} + +fn float_with_precision<'ctx>( + env: &Env<'_, 'ctx, '_>, + value: f64, + float_width: FloatWidth, +) -> BasicValueEnum<'ctx> { + match float_width { + FloatWidth::F64 => env.context.f64_type().const_float(value).into(), + FloatWidth::F32 => env.context.f32_type().const_float(value).into(), + } +} + +pub fn build_exp_literal<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + parent: FunctionValue<'ctx>, + layout: InLayout<'_>, + literal: &roc_mono::ir::Literal<'a>, +) -> BasicValueEnum<'ctx> { + use roc_mono::ir::Literal::*; + + match literal { + Int(bytes) => match layout_interner.get_repr(layout) { + LayoutRepr::Builtin(Builtin::Bool) => env + .context + .bool_type() + .const_int(i128::from_ne_bytes(*bytes) as u64, false) + .into(), + LayoutRepr::Builtin(Builtin::Int(int_width)) => { + int_with_precision(env, i128::from_ne_bytes(*bytes), int_width).into() + } + _ => panic!("Invalid layout for int literal = {layout:?}"), + }, + + U128(bytes) => const_u128(env, u128::from_ne_bytes(*bytes)).into(), + + Float(float) => match layout_interner.get_repr(layout) { + LayoutRepr::Builtin(Builtin::Float(float_width)) => { + float_with_precision(env, *float, float_width) + } + _ => panic!("Invalid layout for float literal = {layout:?}"), + }, + + Decimal(bytes) => { + let (upper_bits, lower_bits) = RocDec::from_ne_bytes(*bytes).as_bits(); + env.context + .i128_type() + .const_int_arbitrary_precision(&[lower_bits, upper_bits as u64]) + .into() + } + Bool(b) => env.context.bool_type().const_int(*b as u64, false).into(), + Byte(b) => env.context.i8_type().const_int(*b as u64, false).into(), + Str(str_literal) => build_string_literal(env, parent, str_literal), + } +} + +fn build_string_literal<'ctx>( + env: &Env<'_, 'ctx, '_>, + parent: FunctionValue<'ctx>, + str_literal: &str, +) -> BasicValueEnum<'ctx> { + if str_literal.len() < env.small_str_bytes() as usize { + match env.small_str_bytes() { + 24 => small_str_ptr_width_8(env, parent, str_literal).into(), + 12 => small_str_ptr_width_4(env, str_literal).into(), + _ => unreachable!("incorrect small_str_bytes"), + } + } else { + let ptr = define_global_str_literal_ptr(env, str_literal); + let number_of_elements = env.ptr_int().const_int(str_literal.len() as u64, false); + + let alloca = const_str_alloca_ptr(env, parent, ptr, number_of_elements, number_of_elements); + + match env.target_info.ptr_width() { + PtrWidth::Bytes4 => { + env.builder + .new_build_load(zig_str_type(env), alloca, "load_const_str") + } + PtrWidth::Bytes8 => alloca.into(), + } + } +} + +fn const_str_alloca_ptr<'ctx>( + env: &Env<'_, 'ctx, '_>, + parent: FunctionValue<'ctx>, + ptr: PointerValue<'ctx>, + len: IntValue<'ctx>, + cap: IntValue<'ctx>, +) -> PointerValue<'ctx> { + let typ = zig_str_type(env); + + let value = typ.const_named_struct(&[ptr.into(), len.into(), cap.into()]); + + let alloca = create_entry_block_alloca(env, parent, typ.into(), "const_str_store"); + + env.builder.new_build_store(alloca, value); + + alloca +} + +fn small_str_ptr_width_8<'ctx>( + env: &Env<'_, 'ctx, '_>, + parent: FunctionValue<'ctx>, + str_literal: &str, +) -> PointerValue<'ctx> { + debug_assert_eq!(env.target_info.ptr_width() as u8, 8); + + let mut array = [0u8; 24]; + + array[..str_literal.len()].copy_from_slice(str_literal.as_bytes()); + + array[env.small_str_bytes() as usize - 1] = str_literal.len() as u8 | roc_std::RocStr::MASK; + + let word1 = u64::from_ne_bytes(array[0..8].try_into().unwrap()); + let word2 = u64::from_ne_bytes(array[8..16].try_into().unwrap()); + let word3 = u64::from_ne_bytes(array[16..24].try_into().unwrap()); + + let ptr = env.ptr_int().const_int(word1, false); + let len = env.ptr_int().const_int(word2, false); + let cap = env.ptr_int().const_int(word3, false); + + let address_space = AddressSpace::default(); + let ptr_type = env.context.i8_type().ptr_type(address_space); + let ptr = env.builder.new_build_int_to_ptr(ptr, ptr_type, "to_u8_ptr"); + + const_str_alloca_ptr(env, parent, ptr, len, cap) +} + +fn small_str_ptr_width_4<'ctx>(env: &Env<'_, 'ctx, '_>, str_literal: &str) -> StructValue<'ctx> { + debug_assert_eq!(env.target_info.ptr_width() as u8, 4); + + let mut array = [0u8; 12]; + + array[..str_literal.len()].copy_from_slice(str_literal.as_bytes()); + + array[env.small_str_bytes() as usize - 1] = str_literal.len() as u8 | roc_std::RocStr::MASK; + + let word1 = u32::from_ne_bytes(array[0..4].try_into().unwrap()); + let word2 = u32::from_ne_bytes(array[4..8].try_into().unwrap()); + let word3 = u32::from_ne_bytes(array[8..12].try_into().unwrap()); + + let ptr = env.ptr_int().const_int(word1 as u64, false); + let len = env.ptr_int().const_int(word2 as u64, false); + let cap = env.ptr_int().const_int(word3 as u64, false); + + let address_space = AddressSpace::default(); + let ptr_type = env.context.i8_type().ptr_type(address_space); + let ptr = env.builder.new_build_int_to_ptr(ptr, ptr_type, "to_u8_ptr"); + + struct_from_fields( + env, + zig_str_type(env), + [(0, ptr.into()), (1, len.into()), (2, cap.into())].into_iter(), + ) +} + +pub(crate) fn build_exp_call<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + layout_ids: &mut LayoutIds<'a>, + func_spec_solutions: &FuncSpecSolutions, + scope: &mut Scope<'a, 'ctx>, + parent: FunctionValue<'ctx>, + layout: InLayout<'a>, + call: &roc_mono::ir::Call<'a>, +) -> BasicValueEnum<'ctx> { + let roc_mono::ir::Call { + call_type, + arguments, + } = call; + + match call_type { + CallType::ByName { + name, + specialization_id, + ret_layout, + .. + } => { + let mut arg_tuples: Vec = + Vec::with_capacity_in(arguments.len(), env.arena); + + for symbol in arguments.iter() { + arg_tuples.push(scope.load_symbol(symbol)); + } + + let bytes = specialization_id.to_bytes(); + let callee_var = CalleeSpecVar(&bytes); + let func_spec = func_spec_solutions.callee_spec(callee_var).unwrap(); + + roc_call_direct_with_args( + env, + layout_interner, + *ret_layout, + *name, + FuncBorrowSpec::Some(func_spec), + arg_tuples.into_bump_slice(), + ) + } + + CallType::ByPointer { + pointer, + arg_layouts, + ret_layout, + } => { + let mut args: Vec = Vec::with_capacity_in(arguments.len(), env.arena); + + for symbol in arguments.iter() { + args.push(scope.load_symbol(symbol)); + } + + let pointer = scope.load_symbol(pointer).into_pointer_value(); + + roc_call_erased_with_args( + env, + layout_interner, + pointer, + arg_layouts, + *ret_layout, + args.into_bump_slice(), + ) + } + + CallType::LowLevel { op, update_mode } => { + let bytes = update_mode.to_bytes(); + let update_var = UpdateModeVar(&bytes); + let update_mode = func_spec_solutions + .update_mode(update_var) + .unwrap_or(UpdateMode::Immutable); + + crate::llvm::lowlevel::run_low_level( + env, + layout_interner, + layout_ids, + scope, + parent, + layout, + *op, + arguments, + update_mode, + ) + } + + CallType::HigherOrder(higher_order) => { + let bytes = higher_order.passed_function.specialization_id.to_bytes(); + let callee_var = CalleeSpecVar(&bytes); + let func_spec = func_spec_solutions.callee_spec(callee_var).unwrap(); + + run_higher_order_low_level( + env, + layout_interner, + layout_ids, + scope, + layout, + func_spec, + higher_order, + ) + } + + CallType::Foreign { + foreign_symbol, + ret_layout, + } => build_foreign_symbol( + env, + layout_interner, + scope, + foreign_symbol, + arguments, + *ret_layout, + ), + } +} + +fn struct_pointer_from_fields<'a, 'ctx, 'env, I>( + env: &Env<'a, 'ctx, 'env>, + layout_interner: &STLayoutInterner<'a>, + struct_type: StructType<'ctx>, + input_pointer: PointerValue<'ctx>, + values: I, +) where + I: Iterator, BasicValueEnum<'ctx>))>, +{ + let struct_ptr = env + .builder + .new_build_bitcast( + input_pointer, + struct_type.ptr_type(AddressSpace::default()), + "struct_ptr", + ) + .into_pointer_value(); + + // Insert field exprs into struct_val + for (index, (field_layout, field_value)) in values { + let field_ptr = env.builder.new_build_struct_gep( + struct_type, + struct_ptr, + index as u32, + "field_struct_gep", + ); + + store_roc_value( + env, + layout_interner, + layout_interner.get_repr(field_layout), + field_ptr, + field_value, + ); + } +} + +pub(crate) fn build_exp_expr<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + layout_ids: &mut LayoutIds<'a>, + func_spec_solutions: &FuncSpecSolutions, + scope: &mut Scope<'a, 'ctx>, + parent: FunctionValue<'ctx>, + layout: InLayout<'a>, + expr: &roc_mono::ir::Expr<'a>, +) -> BasicValueEnum<'ctx> { + use roc_mono::ir::Expr::*; + + match expr { + Literal(literal) => build_exp_literal(env, layout_interner, parent, layout, literal), + NullPointer => { + let basic_type = + basic_type_from_layout(env, layout_interner, layout_interner.get_repr(layout)); + + debug_assert!(basic_type.is_pointer_type()); + basic_type.into_pointer_type().const_zero().into() + } + + Call(call) => build_exp_call( + env, + layout_interner, + layout_ids, + func_spec_solutions, + scope, + parent, + layout, + call, + ), + + Struct(sorted_fields) => RocStruct::build( + env, + layout_interner, + layout_interner.get_repr(layout), + scope, + sorted_fields, + ) + .into(), + + Tag { + arguments, + tag_layout: union_layout, + tag_id, + reuse, + } => { + let reuse_ptr = reuse.map(|ru| scope.load_symbol(&ru.symbol).into_pointer_value()); + + build_tag( + env, + layout_interner, + scope, + union_layout, + *tag_id, + arguments, + reuse_ptr, + parent, + ) + } + + FunctionPointer { lambda_name } => { + let alloca = fn_ptr::build(env, *lambda_name); + alloca.into() + } + ErasedMake { value, callee } => { + let value = value.map(|sym| scope.load_symbol(&sym).into_pointer_value()); + let callee = scope.load_symbol(callee).into_pointer_value(); + erased::build(env, value, callee).into() + } + ErasedLoad { symbol, field } => { + let value = scope.load_symbol(symbol).into_struct_value(); + let wanted_llvm_type = + basic_type_from_layout(env, layout_interner, layout_interner.get_repr(layout)) + .into_pointer_type(); + + erased::load(env, value, *field, wanted_llvm_type).into() + } + + Reset { + symbol, + update_mode, + } => { + let bytes = update_mode.to_bytes(); + let update_var = UpdateModeVar(&bytes); + let update_mode = func_spec_solutions + .update_mode(update_var) + .unwrap_or(UpdateMode::Immutable); + + let (tag_ptr, layout) = scope.load_symbol_and_layout(symbol); + let tag_ptr = tag_ptr.into_pointer_value(); + + // reset is only generated for union values + let union_layout = match layout_interner.get_repr(layout) { + LayoutRepr::Union(ul) => ul, + _ => unreachable!(), + }; + + let ctx = env.context; + let check_if_null = ctx.append_basic_block(parent, "check_if_null"); + let check_if_unique = ctx.append_basic_block(parent, "check_if_unique"); + let cont_block = ctx.append_basic_block(parent, "cont"); + + env.builder.new_build_unconditional_branch(check_if_null); + + env.builder.position_at_end(check_if_null); + + env.builder.new_build_conditional_branch( + // have llvm optimizations clean this up + if layout_interner.is_nullable(layout) { + env.builder.new_build_is_null(tag_ptr, "is_tag_null") + } else { + env.context.bool_type().const_int(false as _, false) + }, + cont_block, + check_if_unique, + ); + + env.builder.position_at_end(check_if_unique); + + let then_block = ctx.append_basic_block(parent, "then_reset"); + let else_block = ctx.append_basic_block(parent, "else_decref"); + + let refcount_ptr = PointerToRefcount::from_ptr_to_data( + env, + if union_layout.stores_tag_id_in_pointer(env.target_info) { + tag_pointer_clear_tag_id(env, tag_ptr) + } else { + tag_ptr + }, + ); + + let is_unique = match update_mode { + UpdateMode::InPlace => env.context.bool_type().const_int(1, false), + UpdateMode::Immutable => refcount_ptr.is_1(env), + }; + + env.builder + .new_build_conditional_branch(is_unique, then_block, else_block); + + { + // reset, when used on a unique reference, eagerly decrements the components of the + // referenced value, and returns the location of the now-invalid cell + env.builder.position_at_end(then_block); + + let reset_function = build_reset(env, layout_interner, layout_ids, union_layout); + let call = + env.builder + .new_build_call(reset_function, &[tag_ptr.into()], "call_reset"); + + call.set_call_convention(FAST_CALL_CONV); + + let _ = call.try_as_basic_value(); + + env.builder.new_build_unconditional_branch(cont_block); + } + { + // If reset is used on a shared, non-reusable reference, it behaves + // like dec and returns NULL, which instructs reuse to behave like ctor + env.builder.position_at_end(else_block); + refcount_ptr.decrement(env, layout_interner, layout_interner.get_repr(layout)); + env.builder.new_build_unconditional_branch(cont_block); + } + { + env.builder.position_at_end(cont_block); + let phi = env.builder.new_build_phi(tag_ptr.get_type(), "branch"); + + let null_ptr = tag_ptr.get_type().const_null(); + phi.add_incoming(&[ + (&null_ptr, check_if_null), + (&tag_ptr, then_block), + (&null_ptr, else_block), + ]); + + phi.as_basic_value() + } + } + ResetRef { + symbol, + update_mode, + } => { + let bytes = update_mode.to_bytes(); + let update_var = UpdateModeVar(&bytes); + let update_mode = func_spec_solutions + .update_mode(update_var) + .unwrap_or(UpdateMode::Immutable); + + let (tag_ptr, layout) = scope.load_symbol_and_layout(symbol); + let tag_ptr = tag_ptr.into_pointer_value(); + + let ctx = env.context; + let check_if_null = ctx.append_basic_block(parent, "check_if_null"); + let check_if_unique = ctx.append_basic_block(parent, "check_if_unique"); + let cont_block = ctx.append_basic_block(parent, "cont"); + + env.builder.new_build_unconditional_branch(check_if_null); + + env.builder.position_at_end(check_if_null); + + env.builder.new_build_conditional_branch( + // have llvm optimizations clean this up + if layout_interner.is_nullable(layout) { + env.builder.new_build_is_null(tag_ptr, "is_tag_null") + } else { + env.context.bool_type().const_int(false as _, false) + }, + cont_block, + check_if_unique, + ); + + env.builder.position_at_end(check_if_unique); + + let not_unique_block = ctx.append_basic_block(parent, "else_decref"); + + // reset is only generated for union values + let union_layout = match layout_interner.get_repr(layout) { + LayoutRepr::Union(ul) => ul, + _ => unreachable!(), + }; + + let refcount_ptr = PointerToRefcount::from_ptr_to_data( + env, + if union_layout.stores_tag_id_in_pointer(env.target_info) { + tag_pointer_clear_tag_id(env, tag_ptr) + } else { + tag_ptr + }, + ); + + let is_unique = match update_mode { + UpdateMode::InPlace => env.context.bool_type().const_int(1, false), + UpdateMode::Immutable => refcount_ptr.is_1(env), + }; + + let parent_block = env.builder.get_insert_block().unwrap(); + + env.builder + .new_build_conditional_branch(is_unique, cont_block, not_unique_block); + + { + // If reset is used on a shared, non-reusable reference, it behaves + // like dec and returns NULL, which instructs reuse to behave like ctor + env.builder.position_at_end(not_unique_block); + refcount_ptr.decrement(env, layout_interner, layout_interner.get_repr(layout)); + env.builder.new_build_unconditional_branch(cont_block); + } + { + env.builder.position_at_end(cont_block); + let phi = env.builder.new_build_phi(tag_ptr.get_type(), "branch"); + + let null_ptr = tag_ptr.get_type().const_null(); + phi.add_incoming(&[ + (&null_ptr, check_if_null), + (&tag_ptr, parent_block), + (&null_ptr, not_unique_block), + ]); + + phi.as_basic_value() + } + } + + StructAtIndex { + index, structure, .. + } => { + let (value, layout) = scope.load_symbol_and_layout(structure); + let struct_val = RocStruct::from(value); + + struct_val.load_at_index( + env, + layout_interner, + layout_interner.get_repr(layout), + *index, + ) + } + + EmptyArray => empty_polymorphic_list(env), + Array { elem_layout, elems } => { + list_literal(env, layout_interner, parent, scope, *elem_layout, elems) + } + RuntimeErrorFunction(_) => todo!(), + + UnionAtIndex { + tag_id, + structure, + index, + union_layout, + } => { + // cast the argument bytes into the desired shape for this tag + let (argument, structure_layout) = scope.load_symbol_and_layout(structure); + + match union_layout { + UnionLayout::NonRecursive(tag_layouts) => { + debug_assert!(argument.is_pointer_value()); + + let field_layouts = tag_layouts[*tag_id as usize]; + + let struct_layout = LayoutRepr::struct_(field_layouts); + let struct_type = basic_type_from_layout(env, layout_interner, struct_layout); + + let opaque_data_ptr = env.builder.new_build_struct_gep( + basic_type_from_layout( + env, + layout_interner, + layout_interner.get_repr(structure_layout), + ) + .into_struct_type(), + argument.into_pointer_value(), + RocUnion::TAG_DATA_INDEX, + "get_opaque_data_ptr", + ); + + let data_ptr = env.builder.new_build_pointer_cast( + opaque_data_ptr, + struct_type.ptr_type(AddressSpace::default()), + "to_data_pointer", + ); + + let element_ptr = env.builder.new_build_struct_gep( + struct_type.into_struct_type(), + data_ptr, + *index as _, + "get_opaque_data_ptr", + ); + + load_roc_value( + env, + layout_interner, + layout_interner.get_repr(field_layouts[*index as usize]), + element_ptr, + "load_element", + ) + } + UnionLayout::Recursive(tag_layouts) => { + debug_assert!(argument.is_pointer_value()); + + let field_layouts = tag_layouts[*tag_id as usize]; + + let ptr = tag_pointer_clear_tag_id(env, argument.into_pointer_value()); + let target_loaded_type = basic_type_from_layout( + env, + layout_interner, + layout_interner.get_repr(layout), + ); + + lookup_at_index_ptr( + env, + layout_interner, + field_layouts, + *index as usize, + ptr, + None, + target_loaded_type, + ) + } + UnionLayout::NonNullableUnwrapped(field_layouts) => { + let struct_layout = LayoutRepr::struct_(field_layouts); + + let struct_type = basic_type_from_layout(env, layout_interner, struct_layout); + let target_loaded_type = basic_type_from_layout( + env, + layout_interner, + layout_interner.get_repr(layout), + ); + + lookup_at_index_ptr( + env, + layout_interner, + field_layouts, + *index as usize, + argument.into_pointer_value(), + Some(struct_type.into_struct_type()), + target_loaded_type, + ) + } + UnionLayout::NullableWrapped { + nullable_id, + other_tags, + } => { + debug_assert!(argument.is_pointer_value()); + debug_assert_ne!(*tag_id, *nullable_id); + + let tag_index = if *tag_id < *nullable_id { + *tag_id + } else { + tag_id - 1 + }; + + let field_layouts = other_tags[tag_index as usize]; + + let ptr = tag_pointer_clear_tag_id(env, argument.into_pointer_value()); + let target_loaded_type = basic_type_from_layout( + env, + layout_interner, + layout_interner.get_repr(layout), + ); + + lookup_at_index_ptr( + env, + layout_interner, + field_layouts, + *index as usize, + ptr, + None, + target_loaded_type, + ) + } + UnionLayout::NullableUnwrapped { + nullable_id, + other_fields, + } => { + debug_assert!(argument.is_pointer_value()); + debug_assert_ne!(*tag_id != 0, *nullable_id); + + let field_layouts = other_fields; + let struct_layout = LayoutRepr::struct_(field_layouts); + + let struct_type = basic_type_from_layout(env, layout_interner, struct_layout); + let target_loaded_type = basic_type_from_layout( + env, + layout_interner, + layout_interner.get_repr(layout), + ); + + lookup_at_index_ptr( + env, + layout_interner, + field_layouts, + // the tag id is not stored + *index as usize, + argument.into_pointer_value(), + Some(struct_type.into_struct_type()), + target_loaded_type, + ) + } + } + } + + GetElementPointer { + structure, + indices, + union_layout, + .. + } => { + debug_assert!(indices.len() >= 2); + let tag_id = indices[0]; + let index = indices[1] as usize; + // cast the argument bytes into the desired shape for this tag + let argument = scope.load_symbol(structure); + let ret_repr = layout_interner.get_repr(layout); + + let pointer_value = match union_layout { + UnionLayout::NonRecursive(_) => unreachable!(), + UnionLayout::Recursive(tag_layouts) => { + debug_assert!(argument.is_pointer_value()); + + let field_layouts = tag_layouts[tag_id as usize]; + + let ptr = tag_pointer_clear_tag_id(env, argument.into_pointer_value()); + let target_loaded_type = basic_type_from_layout(env, layout_interner, ret_repr); + + union_field_ptr_at_index( + env, + layout_interner, + field_layouts, + None, + index, + ptr, + target_loaded_type, + ) + } + UnionLayout::NonNullableUnwrapped(field_layouts) => { + let struct_layout = LayoutRepr::struct_(field_layouts); + + let struct_type = basic_type_from_layout(env, layout_interner, struct_layout); + let target_loaded_type = basic_type_from_layout(env, layout_interner, ret_repr); + + union_field_ptr_at_index( + env, + layout_interner, + field_layouts, + Some(struct_type.into_struct_type()), + index, + argument.into_pointer_value(), + target_loaded_type, + ) + } + UnionLayout::NullableWrapped { + nullable_id, + other_tags, + } => { + debug_assert!(argument.is_pointer_value()); + debug_assert_ne!(tag_id as u16, *nullable_id); + + let tag_id = tag_id as u16; + let tag_index = if tag_id < *nullable_id { + tag_id + } else { + tag_id - 1 + }; + + let field_layouts = other_tags[tag_index as usize]; + + let ptr = tag_pointer_clear_tag_id(env, argument.into_pointer_value()); + let target_loaded_type = basic_type_from_layout(env, layout_interner, ret_repr); + + union_field_ptr_at_index( + env, + layout_interner, + field_layouts, + None, + index, + ptr, + target_loaded_type, + ) + } + UnionLayout::NullableUnwrapped { + nullable_id, + other_fields, + } => { + debug_assert!(argument.is_pointer_value()); + debug_assert_ne!(tag_id != 0, *nullable_id); + + let field_layouts = other_fields; + let struct_layout = LayoutRepr::struct_(field_layouts); + + let struct_type = basic_type_from_layout(env, layout_interner, struct_layout); + let target_loaded_type = basic_type_from_layout(env, layout_interner, ret_repr); + + union_field_ptr_at_index( + env, + layout_interner, + field_layouts, + Some(struct_type.into_struct_type()), + // the tag id is not stored + index, + argument.into_pointer_value(), + target_loaded_type, + ) + } + }; + + pointer_value.into() + } + + GetTagId { + structure, + union_layout, + } => { + // cast the argument bytes into the desired shape for this tag + let (argument, _structure_layout) = scope.load_symbol_and_layout(structure); + + get_tag_id(env, layout_interner, parent, union_layout, argument).into() + } + + Alloca { + initializer, + element_layout, + } => { + let element_type = basic_type_from_layout( + env, + layout_interner, + layout_interner.get_repr(*element_layout), + ); + let ptr = entry_block_alloca_zerofill(env, element_type, "stack_value"); + + if let Some(initializer) = initializer { + env.builder + .new_build_store(ptr, scope.load_symbol(initializer)); + } + + ptr.into() + } + } +} + +fn build_wrapped_tag<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + scope: &Scope<'a, 'ctx>, + union_layout: &UnionLayout<'a>, + tag_id: u8, + arguments: &[Symbol], + tag_field_layouts: &[InLayout<'a>], + tags: &[&[InLayout<'a>]], + reuse_allocation: Option>, + parent: FunctionValue<'ctx>, +) -> BasicValueEnum<'ctx> { + let builder = env.builder; + + let tag_id_layout = union_layout.tag_id_layout(); + + let (field_types, field_values) = + build_tag_fields(env, layout_interner, scope, tag_field_layouts, arguments); + + let union_struct_type = struct_type_from_union_layout(env, layout_interner, union_layout); + + // Create the struct_type + let raw_data_ptr = allocate_tag( + env, + layout_interner, + parent, + reuse_allocation, + union_layout, + tags, + ); + let struct_type = env.context.struct_type(&field_types, false); + + if union_layout.stores_tag_id_as_data(env.target_info) { + let tag_id_ptr = builder.new_build_struct_gep( + union_struct_type, + raw_data_ptr, + RocUnion::TAG_ID_INDEX, + "tag_id_index", + ); + + let tag_id_type = basic_type_from_layout( + env, + layout_interner, + layout_interner.get_repr(tag_id_layout), + ) + .into_int_type(); + + env.builder + .new_build_store(tag_id_ptr, tag_id_type.const_int(tag_id as u64, false)); + + let opaque_struct_ptr = builder.new_build_struct_gep( + union_struct_type, + raw_data_ptr, + RocUnion::TAG_DATA_INDEX, + "tag_data_index", + ); + + struct_pointer_from_fields( + env, + layout_interner, + struct_type, + opaque_struct_ptr, + field_values.into_iter().enumerate(), + ); + + raw_data_ptr.into() + } else { + struct_pointer_from_fields( + env, + layout_interner, + struct_type, + raw_data_ptr, + field_values.into_iter().enumerate(), + ); + + tag_pointer_set_tag_id(env, tag_id, raw_data_ptr).into() + } +} + +pub fn entry_block_alloca_zerofill<'ctx>( + env: &Env<'_, 'ctx, '_>, + basic_type: BasicTypeEnum<'ctx>, + name: &str, +) -> PointerValue<'ctx> { + let parent = env + .builder + .get_insert_block() + .unwrap() + .get_parent() + .unwrap(); + + create_entry_block_alloca(env, parent, basic_type, name) +} + +fn build_tag_field_value<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + value: BasicValueEnum<'ctx>, + tag_field_layout: InLayout<'a>, +) -> BasicValueEnum<'ctx> { + if let LayoutRepr::RecursivePointer(_) = layout_interner.get_repr(tag_field_layout) { + debug_assert!(value.is_pointer_value()); + + // we store recursive pointers as `i64*` + env.builder + .new_build_pointer_cast( + value.into_pointer_value(), + env.context.i64_type().ptr_type(AddressSpace::default()), + "cast_recursive_pointer", + ) + .into() + } else if layout_interner.is_passed_by_reference(tag_field_layout) { + debug_assert!(value.is_pointer_value()); + + // NOTE: we rely on this being passed to `store_roc_value` so that + // the value is memcpy'd + value + } else { + // this check fails for recursive tag unions, but can be helpful while debugging + // debug_assert_eq!(tag_field_layout, val_layout); + + value + } +} + +fn build_tag_fields<'a, 'r, 'ctx, 'env>( + env: &'r Env<'a, 'ctx, 'env>, + layout_interner: &'r STLayoutInterner<'a>, + scope: &'r Scope<'a, 'ctx>, + fields: &[InLayout<'a>], + arguments: &[Symbol], +) -> ( + std::vec::Vec>, + std::vec::Vec<(InLayout<'a>, BasicValueEnum<'ctx>)>, +) { + debug_assert_eq!(fields.len(), arguments.len()); + + let capacity = fields.len(); + let mut field_types = std::vec::Vec::with_capacity(capacity); + let mut field_values = std::vec::Vec::with_capacity(capacity); + + for (field_symbol, tag_field_layout) in arguments.iter().zip(fields.iter()) { + let field_type = basic_type_from_layout( + env, + layout_interner, + layout_interner.get_repr(*tag_field_layout), + ); + field_types.push(field_type); + + let raw_value: BasicValueEnum<'ctx> = scope.load_symbol(field_symbol); + let field_value = build_tag_field_value(env, layout_interner, raw_value, *tag_field_layout); + + field_values.push((*tag_field_layout, field_value)); + } + + (field_types, field_values) +} + +fn build_tag<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + scope: &Scope<'a, 'ctx>, + union_layout: &UnionLayout<'a>, + tag_id: TagIdIntType, + arguments: &[Symbol], + reuse_allocation: Option>, + parent: FunctionValue<'ctx>, +) -> BasicValueEnum<'ctx> { + let union_size = union_layout.number_of_tags(); + + match union_layout { + UnionLayout::NonRecursive(tags) => { + debug_assert!(union_size > 1); + + let data_layout_repr = LayoutRepr::Struct(tags[tag_id as usize]); + let data = RocStruct::build(env, layout_interner, data_layout_repr, scope, arguments); + + let roc_union = RocUnion::tagged_from_slices(layout_interner, env.context, tags); + + let tag_alloca = env + .builder + .new_build_alloca(roc_union.struct_type(), "tag_alloca"); + + roc_union.write_struct_data( + env, + layout_interner, + tag_alloca, + data, + data_layout_repr, + Some(tag_id as _), + ); + + tag_alloca.into() + } + UnionLayout::Recursive(tags) => { + debug_assert!(union_size > 1); + + let tag_field_layouts = &tags[tag_id as usize]; + + build_wrapped_tag( + env, + layout_interner, + scope, + union_layout, + tag_id as _, + arguments, + tag_field_layouts, + tags, + reuse_allocation, + parent, + ) + } + UnionLayout::NullableWrapped { + nullable_id, + other_tags: tags, + } => { + let tag_field_layouts = { + use std::cmp::Ordering::*; + match tag_id.cmp(&(*nullable_id as _)) { + Equal => { + let layout = LayoutRepr::Union(*union_layout); + + return basic_type_from_layout(env, layout_interner, layout) + .into_pointer_type() + .const_null() + .into(); + } + Less => &tags[tag_id as usize], + Greater => &tags[tag_id as usize - 1], + } + }; + + build_wrapped_tag( + env, + layout_interner, + scope, + union_layout, + tag_id as _, + arguments, + tag_field_layouts, + tags, + reuse_allocation, + parent, + ) + } + UnionLayout::NonNullableUnwrapped(fields) => { + debug_assert_eq!(union_size, 1); + debug_assert_eq!(tag_id, 0); + debug_assert_eq!(arguments.len(), fields.len()); + + let (field_types, field_values) = + build_tag_fields(env, layout_interner, scope, fields, arguments); + + // Create the struct_type + let data_ptr = reserve_with_refcount_union_as_block_of_memory( + env, + layout_interner, + *union_layout, + &[fields], + ); + + let struct_type = env + .context + .struct_type(env.arena.alloc_slice_fill_iter(field_types), false); + + struct_pointer_from_fields( + env, + layout_interner, + struct_type, + data_ptr, + field_values.into_iter().enumerate(), + ); + + data_ptr.into() + } + UnionLayout::NullableUnwrapped { + nullable_id, + other_fields, + } => { + let roc_union = + RocUnion::untagged_from_slices(layout_interner, env.context, &[other_fields]); + + if tag_id == *nullable_id as u16 { + let output_type = roc_union.struct_type().ptr_type(AddressSpace::default()); + + return output_type.const_null().into(); + } + + // this tag id is not the nullable one. For the type to be recursive, the other + // constructor must have at least one argument! + debug_assert!(!arguments.is_empty()); + + debug_assert!(union_size == 2); + + // Create the struct_type + let data_ptr = allocate_tag( + env, + layout_interner, + parent, + reuse_allocation, + union_layout, + &[other_fields], + ); + + let data_layout_repr = LayoutRepr::Struct(other_fields); + let data = RocStruct::build(env, layout_interner, data_layout_repr, scope, arguments); + + roc_union.write_struct_data( + env, + layout_interner, + data_ptr, + data, + data_layout_repr, + None, + ); + + data_ptr.into() + } + } +} + +fn tag_pointer_set_tag_id<'ctx>( + env: &Env<'_, 'ctx, '_>, + tag_id: u8, + pointer: PointerValue<'ctx>, +) -> PointerValue<'ctx> { + // we only have 3 bits, so can encode only 0..7 (or on 32-bit targets, 2 bits to encode 0..3) + debug_assert!((tag_id as u32) < env.target_info.ptr_width() as u32); + + let tag_id_intval = env.ptr_int().const_int(tag_id as u64, false); + + let cast_pointer = env.builder.new_build_pointer_cast( + pointer, + env.context.i8_type().ptr_type(AddressSpace::default()), + "cast_to_i8_ptr", + ); + + // NOTE: assumes the lower bits of `cast_pointer` are all 0 + let indexed_pointer = unsafe { + env.builder.new_build_in_bounds_gep( + env.context.i8_type(), + cast_pointer, + &[tag_id_intval], + "indexed_pointer", + ) + }; + + env.builder + .new_build_pointer_cast(indexed_pointer, pointer.get_type(), "cast_from_i8_ptr") +} + +pub fn tag_pointer_tag_id_bits_and_mask(target_info: TargetInfo) -> (u64, u64) { + match target_info.ptr_width() { + roc_target::PtrWidth::Bytes8 => (3, 0b0000_0111), + roc_target::PtrWidth::Bytes4 => (2, 0b0000_0011), + } +} + +pub fn tag_pointer_read_tag_id<'ctx>( + env: &Env<'_, 'ctx, '_>, + pointer: PointerValue<'ctx>, +) -> IntValue<'ctx> { + let (_, mask) = tag_pointer_tag_id_bits_and_mask(env.target_info); + let ptr_int = env.ptr_int(); + + let as_int = env.builder.new_build_ptr_to_int(pointer, ptr_int, "to_int"); + let mask_intval = env.ptr_int().const_int(mask, false); + + let masked = env.builder.new_build_and(as_int, mask_intval, "mask"); + + env.builder + .new_build_int_cast_sign_flag(masked, env.context.i8_type(), false, "to_u8") +} + +pub fn tag_pointer_clear_tag_id<'ctx>( + env: &Env<'_, 'ctx, '_>, + pointer: PointerValue<'ctx>, +) -> PointerValue<'ctx> { + let (_, tag_id_bits_mask) = tag_pointer_tag_id_bits_and_mask(env.target_info); + + let as_int = env + .builder + .new_build_ptr_to_int(pointer, env.ptr_int(), "to_int"); + + let mask = env.ptr_int().const_int(tag_id_bits_mask, false); + + let current_tag_id = env.builder.new_build_and(as_int, mask, "masked"); + + let index = env.builder.new_build_int_neg(current_tag_id, "index"); + + let cast_pointer = env.builder.new_build_pointer_cast( + pointer, + env.context.i8_type().ptr_type(AddressSpace::default()), + "cast_to_i8_ptr", + ); + + let indexed_pointer = unsafe { + env.builder.new_build_in_bounds_gep( + env.context.i8_type(), + cast_pointer, + &[index], + "new_ptr", + ) + }; + + env.builder + .new_build_pointer_cast(indexed_pointer, pointer.get_type(), "cast_from_i8_ptr") +} + +fn allocate_tag<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + parent: FunctionValue<'ctx>, + reuse_allocation: Option>, + union_layout: &UnionLayout<'a>, + tags: &[&[InLayout<'a>]], +) -> PointerValue<'ctx> { + match reuse_allocation { + Some(ptr) => { + // check if its a null pointer + let is_null_ptr = env.builder.new_build_is_null(ptr, "is_null_ptr"); + let ctx = env.context; + let then_block = ctx.append_basic_block(parent, "then_allocate_fresh"); + let else_block = ctx.append_basic_block(parent, "else_reuse"); + let cont_block = ctx.append_basic_block(parent, "cont"); + + env.builder + .new_build_conditional_branch(is_null_ptr, then_block, else_block); + + let raw_ptr = { + env.builder.position_at_end(then_block); + let raw_ptr = reserve_with_refcount_union_as_block_of_memory( + env, + layout_interner, + *union_layout, + tags, + ); + env.builder.new_build_unconditional_branch(cont_block); + raw_ptr + }; + + let reuse_ptr = { + env.builder.position_at_end(else_block); + + let cleared = tag_pointer_clear_tag_id(env, ptr); + + env.builder.new_build_unconditional_branch(cont_block); + + cleared + }; + + { + env.builder.position_at_end(cont_block); + let phi = env.builder.new_build_phi(raw_ptr.get_type(), "branch"); + + phi.add_incoming(&[(&raw_ptr, then_block), (&reuse_ptr, else_block)]); + + phi.as_basic_value().into_pointer_value() + } + } + None => reserve_with_refcount_union_as_block_of_memory( + env, + layout_interner, + *union_layout, + tags, + ), + } +} + +pub fn get_tag_id<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + parent: FunctionValue<'ctx>, + union_layout: &UnionLayout<'a>, + argument: BasicValueEnum<'ctx>, +) -> IntValue<'ctx> { + let builder = env.builder; + + let tag_id_layout = union_layout.tag_id_layout(); + let tag_id_int_type = basic_type_from_layout( + env, + layout_interner, + layout_interner.get_repr(tag_id_layout), + ) + .into_int_type(); + + match union_layout { + UnionLayout::NonRecursive(_) => { + debug_assert!(argument.is_pointer_value(), "{argument:?}"); + + let argument_ptr = argument.into_pointer_value(); + get_tag_id_wrapped(env, layout_interner, *union_layout, argument_ptr) + } + UnionLayout::Recursive(_) => { + let argument_ptr = argument.into_pointer_value(); + + if union_layout.stores_tag_id_as_data(env.target_info) { + get_tag_id_wrapped(env, layout_interner, *union_layout, argument_ptr) + } else { + tag_pointer_read_tag_id(env, argument_ptr) + } + } + UnionLayout::NonNullableUnwrapped(_) => tag_id_int_type.const_zero(), + UnionLayout::NullableWrapped { nullable_id, .. } => { + let argument_ptr = argument.into_pointer_value(); + let is_null = env.builder.new_build_is_null(argument_ptr, "is_null"); + + let ctx = env.context; + let then_block = ctx.append_basic_block(parent, "then"); + let else_block = ctx.append_basic_block(parent, "else"); + let cont_block = ctx.append_basic_block(parent, "cont"); + + let result = builder.new_build_alloca(tag_id_int_type, "result"); + + env.builder + .new_build_conditional_branch(is_null, then_block, else_block); + + { + env.builder.position_at_end(then_block); + let tag_id = tag_id_int_type.const_int(*nullable_id as u64, false); + env.builder.new_build_store(result, tag_id); + env.builder.new_build_unconditional_branch(cont_block); + } + + { + env.builder.position_at_end(else_block); + + let tag_id = if union_layout.stores_tag_id_as_data(env.target_info) { + get_tag_id_wrapped(env, layout_interner, *union_layout, argument_ptr) + } else { + tag_pointer_read_tag_id(env, argument_ptr) + }; + env.builder.new_build_store(result, tag_id); + env.builder.new_build_unconditional_branch(cont_block); + } + + env.builder.position_at_end(cont_block); + + env.builder + .new_build_load(tag_id_int_type, result, "load_result") + .into_int_value() + } + UnionLayout::NullableUnwrapped { nullable_id, .. } => { + let argument_ptr = argument.into_pointer_value(); + let is_null = env.builder.new_build_is_null(argument_ptr, "is_null"); + + let then_value = tag_id_int_type.const_int(*nullable_id as u64, false); + let else_value = tag_id_int_type.const_int(!*nullable_id as u64, false); + + env.builder + .new_build_select(is_null, then_value, else_value, "select_tag_id") + .into_int_value() + } + } +} + +fn lookup_at_index_ptr<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + field_layouts: &[InLayout<'a>], + index: usize, + value: PointerValue<'ctx>, + struct_type: Option>, + target_loaded_type: BasicTypeEnum<'ctx>, +) -> BasicValueEnum<'ctx> { + let elem_ptr = union_field_ptr_at_index_help( + env, + layout_interner, + field_layouts, + struct_type, + index, + value, + ); + + let field_layout = field_layouts[index]; + let result = load_roc_value( + env, + layout_interner, + layout_interner.get_repr(field_layout), + elem_ptr, + "load_at_index_ptr_old", + ); + + // A recursive pointer in the loaded structure is stored as a `i64*`, but the loaded layout + // might want a more precise structure. As such, cast it to the refined type if needed. + cast_if_necessary_for_opaque_recursive_pointers(env.builder, result, target_loaded_type) +} + +fn union_field_ptr_at_index_help<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + field_layouts: &'a [InLayout<'a>], + opt_struct_type: Option>, + index: usize, + value: PointerValue<'ctx>, +) -> PointerValue<'ctx> { + let builder = env.builder; + + let struct_type = match opt_struct_type { + Some(st) => st, + None => { + let struct_layout = LayoutRepr::struct_(field_layouts); + basic_type_from_layout(env, layout_interner, struct_layout).into_struct_type() + } + }; + + let data_ptr = env.builder.new_build_pointer_cast( + value, + struct_type.ptr_type(AddressSpace::default()), + "cast_lookup_at_index_ptr", + ); + + builder.new_build_struct_gep( + struct_type, + data_ptr, + index as u32, + "at_index_struct_gep_data", + ) +} + +fn union_field_ptr_at_index<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + field_layouts: &'a [InLayout<'a>], + opt_struct_type: Option>, + index: usize, + value: PointerValue<'ctx>, + target_loaded_type: BasicTypeEnum<'ctx>, +) -> PointerValue<'ctx> { + let result = union_field_ptr_at_index_help( + env, + layout_interner, + field_layouts, + opt_struct_type, + index, + value, + ); + + // A recursive pointer in the loaded structure is stored as a `i64*`, but the loaded layout + // might want a more precise structure. As such, cast it to the refined type if needed. + let from_value: BasicValueEnum = result.into(); + let to_type: BasicTypeEnum = target_loaded_type; + cast_if_necessary_for_opaque_recursive_pointers(env.builder, from_value, to_type) + .into_pointer_value() +} + +pub fn reserve_with_refcount<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + layout: InLayout<'a>, +) -> PointerValue<'ctx> { + let stack_size = layout_interner.stack_size(layout); + let alignment_bytes = layout_interner.alignment_bytes(layout); + + let basic_type = basic_type_from_layout(env, layout_interner, layout_interner.get_repr(layout)); + + reserve_with_refcount_help(env, basic_type, stack_size, alignment_bytes) +} + +fn reserve_with_refcount_union_as_block_of_memory<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + union_layout: UnionLayout<'a>, + fields: &[&[InLayout<'a>]], +) -> PointerValue<'ctx> { + let ptr_bytes = env.target_info; + + let roc_union = if union_layout.stores_tag_id_as_data(ptr_bytes) { + RocUnion::tagged_from_slices(layout_interner, env.context, fields) + } else { + RocUnion::untagged_from_slices(layout_interner, env.context, fields) + }; + + reserve_with_refcount_help( + env, + roc_union.struct_type(), + roc_union.tag_width(), + roc_union.tag_alignment(), + ) +} + +fn reserve_with_refcount_help<'a, 'ctx, 'env>( + env: &Env<'a, 'ctx, 'env>, + basic_type: impl BasicType<'ctx>, + stack_size: u32, + alignment_bytes: u32, +) -> PointerValue<'ctx> { + let len_type = env.ptr_int(); + + let value_bytes_intvalue = len_type.const_int(stack_size as u64, false); + + allocate_with_refcount_help(env, basic_type, alignment_bytes, value_bytes_intvalue) +} + +pub fn allocate_with_refcount<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + layout: InLayout<'a>, + value: BasicValueEnum<'ctx>, +) -> PointerValue<'ctx> { + let data_ptr = reserve_with_refcount(env, layout_interner, layout); + + // store the value in the pointer + env.builder.new_build_store(data_ptr, value); + + data_ptr +} + +pub fn allocate_with_refcount_help<'a, 'ctx, 'env>( + env: &Env<'a, 'ctx, 'env>, + value_type: impl BasicType<'ctx>, + alignment_bytes: u32, + number_of_data_bytes: IntValue<'ctx>, +) -> PointerValue<'ctx> { + let ptr = call_bitcode_fn( + env, + &[ + number_of_data_bytes.into(), + env.alignment_const(alignment_bytes).into(), + ], + roc_builtins::bitcode::UTILS_ALLOCATE_WITH_REFCOUNT, + ) + .into_pointer_value(); + + let ptr_type = value_type.ptr_type(AddressSpace::default()); + + env.builder + .new_build_pointer_cast(ptr, ptr_type, "alloc_cast_to_desired") +} + +fn list_literal<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + parent: FunctionValue<'ctx>, + scope: &Scope<'a, 'ctx>, + element_layout: InLayout<'a>, + elems: &[ListLiteralElement], +) -> BasicValueEnum<'ctx> { + let ctx = env.context; + let builder = env.builder; + + let element_type = basic_type_from_layout( + env, + layout_interner, + layout_interner.get_repr(element_layout), + ); + + let list_length = elems.len(); + let list_length_intval = env.ptr_int().const_int(list_length as _, false); + + // TODO re-enable, currently causes morphic segfaults because it tries to update + // constants in-place... + // if element_type.is_int_type() { + if false { + let element_type = element_type.into_int_type(); + let element_width = layout_interner.stack_size(element_layout); + let size = list_length * element_width as usize; + let alignment = layout_interner + .alignment_bytes(element_layout) + .max(env.target_info.ptr_width() as u32); + + let mut is_all_constant = true; + let zero_elements = + (env.target_info.ptr_width() as u8 as f64 / element_width as f64).ceil() as usize; + + // runtime-evaluated elements + let mut runtime_evaluated_elements = Vec::with_capacity_in(list_length, env.arena); + + // set up a global that contains all the literal elements of the array + // any variables or expressions are represented as `undef` + let global = { + let mut global_elements = Vec::with_capacity_in(list_length, env.arena); + + // Add zero bytes that represent the refcount + // + // - if all elements are const, then we store the whole list as a constant. + // It then needs a refcount before the first element. + // - but if the list is not all constants, then we will just copy the constant values, + // and we do not need that refcount at the start + // + // In the latter case, we won't store the zeros in the globals + // (we slice them off again below) + for _ in 0..zero_elements { + global_elements.push(element_type.const_zero()); + } + + // Copy the elements from the list literal into the array + for (index, element) in elems.iter().enumerate() { + match element { + ListLiteralElement::Literal(literal) => { + let val = build_exp_literal( + env, + layout_interner, + parent, + element_layout, + literal, + ); + global_elements.push(val.into_int_value()); + } + ListLiteralElement::Symbol(symbol) => { + let val = scope.load_symbol(symbol); + + // here we'd like to furthermore check for intval.is_const(). + // if all elements are const for LLVM, we could make the array a constant. + // BUT morphic does not know about this, and could allow us to modify that + // array in-place. That would cause a segfault. So, we'll have to find + // constants ourselves and cannot lean on LLVM here. + + is_all_constant = false; + + runtime_evaluated_elements.push((index, val)); + + global_elements.push(element_type.get_undef()); + } + }; + } + + let const_elements = if is_all_constant { + global_elements.into_bump_slice() + } else { + &global_elements[zero_elements..] + }; + + // use None for the address space (e.g. Const does not work) + let typ = element_type.array_type(const_elements.len() as u32); + let global = env.module.add_global(typ, None, "roc__list_literal"); + + global.set_constant(true); + global.set_alignment(alignment); + global.set_unnamed_addr(true); + global.set_linkage(inkwell::module::Linkage::Private); + + global.set_initializer(&element_type.const_array(const_elements)); + global.as_pointer_value() + }; + + if is_all_constant { + // all elements are constants, so we can use the memory in the constants section directly + // here we make a pointer to the first actual element (skipping the 0 bytes that + // represent the refcount) + let zero = env.ptr_int().const_zero(); + let offset = env.ptr_int().const_int(zero_elements as _, false); + + let ptr = unsafe { + env.builder.new_build_in_bounds_gep( + element_type, + global, + &[zero, offset], + "first_element_pointer", + ) + }; + + super::build_list::store_list(env, ptr, list_length_intval).into() + } else { + // some of our elements are non-constant, so we must allocate space on the heap + let ptr = allocate_list(env, layout_interner, element_layout, list_length_intval); + + // then, copy the relevant segment from the constant section into the heap + env.builder + .build_memcpy( + ptr, + alignment, + global, + alignment, + env.ptr_int().const_int(size as _, false), + ) + .unwrap(); + + // then replace the `undef`s with the values that we evaluate at runtime + for (index, val) in runtime_evaluated_elements { + let index_val = ctx.i64_type().const_int(index as u64, false); + let elem_ptr = unsafe { + builder.new_build_in_bounds_gep(element_type, ptr, &[index_val], "index") + }; + + builder.new_build_store(elem_ptr, val); + } + + super::build_list::store_list(env, ptr, list_length_intval).into() + } + } else { + let ptr = allocate_list(env, layout_interner, element_layout, list_length_intval); + + // Copy the elements from the list literal into the array + for (index, element) in elems.iter().enumerate() { + let val = match element { + ListLiteralElement::Literal(literal) => { + build_exp_literal(env, layout_interner, parent, element_layout, literal) + } + ListLiteralElement::Symbol(symbol) => scope.load_symbol(symbol), + }; + let index_val = ctx.i64_type().const_int(index as u64, false); + let elem_ptr = unsafe { + builder.new_build_in_bounds_gep(element_type, ptr, &[index_val], "index") + }; + + store_roc_value( + env, + layout_interner, + layout_interner.get_repr(element_layout), + elem_ptr, + val, + ); + } + + super::build_list::store_list(env, ptr, list_length_intval).into() + } +} + +pub fn load_roc_value<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + layout: LayoutRepr<'a>, + source: PointerValue<'ctx>, + name: &str, +) -> BasicValueEnum<'ctx> { + let basic_type = basic_type_from_layout(env, layout_interner, layout); + + if layout.is_passed_by_reference(layout_interner) { + let alloca = entry_block_alloca_zerofill(env, basic_type, name); + + store_roc_value(env, layout_interner, layout, alloca, source.into()); + + alloca.into() + } else { + env.builder.new_build_load(basic_type, source, name) + } +} + +pub fn use_roc_value<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + layout: LayoutRepr<'a>, + source: BasicValueEnum<'ctx>, + name: &str, +) -> BasicValueEnum<'ctx> { + if layout.is_passed_by_reference(layout_interner) { + let alloca = entry_block_alloca_zerofill( + env, + basic_type_from_layout(env, layout_interner, layout), + name, + ); + + env.builder.new_build_store(alloca, source); + + alloca.into() + } else { + source + } +} + +pub fn store_roc_value_opaque<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + layout: InLayout<'a>, + opaque_destination: PointerValue<'ctx>, + value: BasicValueEnum<'ctx>, +) { + let target_type = + basic_type_from_layout(env, layout_interner, layout_interner.get_repr(layout)) + .ptr_type(AddressSpace::default()); + let destination = env.builder.new_build_pointer_cast( + opaque_destination, + target_type, + "store_roc_value_opaque", + ); + + store_roc_value( + env, + layout_interner, + layout_interner.get_repr(layout), + destination, + value, + ) +} + +pub fn store_roc_value<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + layout: LayoutRepr<'a>, + destination: PointerValue<'ctx>, + value: BasicValueEnum<'ctx>, +) { + if layout.is_passed_by_reference(layout_interner) { + debug_assert!(value.is_pointer_value()); + + build_memcpy( + env, + layout_interner, + layout, + destination, + value.into_pointer_value(), + ); + } else { + env.builder.new_build_store(destination, value); + } +} + +pub(crate) fn build_exp_stmt<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + layout_ids: &mut LayoutIds<'a>, + func_spec_solutions: &FuncSpecSolutions, + scope: &mut Scope<'a, 'ctx>, + parent: FunctionValue<'ctx>, + stmt: &roc_mono::ir::Stmt<'a>, +) -> BasicValueEnum<'ctx> { + use roc_mono::ir::Stmt::*; + + match stmt { + Let(first_symbol, first_expr, first_layout, mut cont) => { + let mut queue = Vec::new_in(env.arena); + + queue.push((first_symbol, first_expr, first_layout)); + + while let Let(symbol, expr, layout, new_cont) = cont { + queue.push((symbol, expr, layout)); + + cont = new_cont; + } + + let mut stack = Vec::with_capacity_in(queue.len(), env.arena); + + for (symbol, expr, layout) in queue { + debug_assert!(!matches!( + layout_interner.get_repr(*layout), + LayoutRepr::RecursivePointer(_) + )); + + let val = build_exp_expr( + env, + layout_interner, + layout_ids, + func_spec_solutions, + scope, + parent, + *layout, + expr, + ); + + // Make a new scope which includes the binding we just encountered. + // This should be done *after* compiling the bound expr, since any + // recursive (in the LetRec sense) bindings should already have + // been extracted as procedures. Nothing in here should need to + // access itself! + // scope = scope.clone(); + + scope.insert(*symbol, *layout, val); + stack.push(*symbol); + } + + let result = build_exp_stmt( + env, + layout_interner, + layout_ids, + func_spec_solutions, + scope, + parent, + cont, + ); + + for symbol in stack { + scope.remove(&symbol); + } + + result + } + Ret(symbol) => { + let (value, layout) = scope.load_symbol_and_layout(symbol); + + build_return( + env, + layout_interner, + layout_interner.get_repr(layout), + value, + parent, + ); + + env.context.i8_type().const_zero().into() + } + + Switch { + branches, + default_branch, + ret_layout, + cond_layout, + cond_symbol, + } => { + let ret_type = + basic_type_from_layout(env, layout_interner, layout_interner.get_repr(*ret_layout)); + + let switch_args = SwitchArgsIr { + cond_layout: *cond_layout, + cond_symbol: *cond_symbol, + branches, + default_branch: default_branch.1, + ret_type, + }; + + build_switch_ir( + env, + layout_interner, + layout_ids, + func_spec_solutions, + scope, + parent, + switch_args, + ) + } + Join { + id, + parameters, + remainder, + body: continuation, + } => { + let builder = env.builder; + let context = env.context; + + // create new block + let cont_block = context.append_basic_block(parent, "joinpointcont"); + + let mut joinpoint_args = std::vec::Vec::with_capacity(parameters.len()); + { + let current = builder.get_insert_block().unwrap(); + builder.position_at_end(cont_block); + + for param in parameters.iter() { + let basic_type = basic_type_from_layout( + env, + layout_interner, + layout_interner.get_repr(param.layout), + ); + + let phi_type = if layout_interner.is_passed_by_reference(param.layout) { + basic_type.ptr_type(AddressSpace::default()).into() + } else { + basic_type + }; + + let phi_node = env.builder.new_build_phi(phi_type, "joinpointarg"); + joinpoint_args.push(phi_node); + } + + builder.position_at_end(current); + } + + // store this join point + scope.insert_join_point(*id, cont_block, joinpoint_args); + + // construct the blocks that may jump to this join point + build_exp_stmt( + env, + layout_interner, + layout_ids, + func_spec_solutions, + scope, + parent, + remainder, + ); + + let phi_block = builder.get_insert_block().unwrap(); + + // put the cont block at the back + builder.position_at_end(cont_block); + + // bind the values + scope + .bind_parameters_to_join_point(*id, parameters.iter()) + .expect("join point not found, but it was inserted above"); + + // put the continuation in + let result = build_exp_stmt( + env, + layout_interner, + layout_ids, + func_spec_solutions, + scope, + parent, + continuation, + ); + + // remove this join point again + scope.remove_join_point(*id); + + cont_block.move_after(phi_block).unwrap(); + + result + } + + Jump(join_point, arguments) => { + let builder = env.builder; + let context = env.context; + let (cont_block, argument_phi_values) = scope.get_join_point(*join_point).unwrap(); + + let current_block = builder.get_insert_block().unwrap(); + + for (phi_value, argument) in argument_phi_values.iter().zip(arguments.iter()) { + let (value, _) = scope.load_symbol_and_layout(argument); + + phi_value.add_incoming(&[(&value, current_block)]); + } + + builder.new_build_unconditional_branch(*cont_block); + + // This doesn't currently do anything + context.i64_type().const_zero().into() + } + + Refcounting(modify, cont) => { + use ModifyRc::*; + + match modify { + Inc(symbol, inc_amount) => { + let (value, layout) = scope.load_symbol_and_layout(symbol); + if layout_interner.contains_refcounted(layout) { + increment_refcount_layout( + env, + layout_interner, + layout_ids, + *inc_amount, + value, + layout, + ); + } + + build_exp_stmt( + env, + layout_interner, + layout_ids, + func_spec_solutions, + scope, + parent, + cont, + ) + } + Dec(symbol) => { + let (value, layout) = scope.load_symbol_and_layout(symbol); + + if layout_interner.contains_refcounted(layout) { + decrement_refcount_layout(env, layout_interner, layout_ids, value, layout); + } + + build_exp_stmt( + env, + layout_interner, + layout_ids, + func_spec_solutions, + scope, + parent, + cont, + ) + } + DecRef(symbol) => { + let (value, layout) = scope.load_symbol_and_layout(symbol); + + match layout_interner.runtime_representation(layout) { + LayoutRepr::Builtin(Builtin::Str) => todo!(), + LayoutRepr::Builtin(Builtin::List(element_layout)) => { + debug_assert!(value.is_struct_value()); + let element_layout = layout_interner.get_repr(element_layout); + let alignment = element_layout.alignment_bytes(layout_interner); + + build_list::decref(env, value.into_struct_value(), alignment); + } + + other_layout if other_layout.is_refcounted(layout_interner) => { + if value.is_pointer_value() { + let clear_tag_id = match other_layout { + LayoutRepr::Union(union_layout) => { + union_layout.stores_tag_id_in_pointer(env.target_info) + } + _ => false, + }; + + let value_ptr = if clear_tag_id { + tag_pointer_clear_tag_id(env, value.into_pointer_value()) + } else { + value.into_pointer_value() + }; + + let then_block = env.context.append_basic_block(parent, "then"); + let done_block = env.context.append_basic_block(parent, "done"); + + let condition = env + .builder + .new_build_is_not_null(value_ptr, "box_is_not_null"); + env.builder.new_build_conditional_branch( + condition, then_block, done_block, + ); + + { + env.builder.position_at_end(then_block); + let refcount_ptr = + PointerToRefcount::from_ptr_to_data(env, value_ptr); + refcount_ptr.decrement( + env, + layout_interner, + layout_interner.get_repr(layout), + ); + + env.builder.new_build_unconditional_branch(done_block); + } + + env.builder.position_at_end(done_block); + } else { + eprint!("we're likely leaking memory; see issue #985 for details"); + } + } + _ => { + // nothing to do + } + } + + build_exp_stmt( + env, + layout_interner, + layout_ids, + func_spec_solutions, + scope, + parent, + cont, + ) + } + + Free(symbol) => { + // unconditionally deallocate the symbol + let (value, layout) = scope.load_symbol_and_layout(symbol); + let alignment = layout_interner.allocation_alignment_bytes(layout); + + debug_assert!(value.is_pointer_value()); + let value = value.into_pointer_value(); + + let clear_tag_id = match layout_interner.runtime_representation(layout) { + LayoutRepr::Union(union) => union.stores_tag_id_in_pointer(env.target_info), + _ => false, + }; + + let ptr = if clear_tag_id { + tag_pointer_clear_tag_id(env, value) + } else { + value + }; + + let rc_ptr = PointerToRefcount::from_ptr_to_data(env, ptr); + rc_ptr.deallocate(env, alignment); + + build_exp_stmt( + env, + layout_interner, + layout_ids, + func_spec_solutions, + scope, + parent, + cont, + ) + } + } + } + + Dbg { + symbol, + variable: _, + remainder, + } => { + if env.mode.runs_expects() { + // TODO: Change location to `filename:line_number` + // let region = unsafe { std::mem::transmute::<_, roc_region::all::Region>(*symbol) }; + let location = + build_string_literal(env, parent, symbol.module_string(&env.interns)); + let message = scope.load_symbol(symbol); + env.call_dbg(env, location, message); + } + + build_exp_stmt( + env, + layout_interner, + layout_ids, + func_spec_solutions, + scope, + parent, + remainder, + ) + } + + Expect { + condition: cond_symbol, + region, + lookups, + variables, + remainder, + } => { + let bd = env.builder; + let context = env.context; + + let (cond, _cond_layout) = scope.load_symbol_and_layout(cond_symbol); + + let condition = bd.new_build_int_compare( + IntPredicate::EQ, + cond.into_int_value(), + context.bool_type().const_int(1, false), + "is_true", + ); + + let then_block = context.append_basic_block(parent, "then_block"); + let throw_block = context.append_basic_block(parent, "throw_block"); + + bd.new_build_conditional_branch(condition, then_block, throw_block); + + if env.mode.runs_expects() { + bd.position_at_end(throw_block); + + match env.target_info.ptr_width() { + roc_target::PtrWidth::Bytes8 => { + let shared_memory = SharedMemoryPointer::get(env); + + clone_to_shared_memory( + env, + layout_interner, + scope, + layout_ids, + &shared_memory, + *cond_symbol, + *region, + lookups, + variables, + ); + + if let LlvmBackendMode::BinaryDev = env.mode { + crate::llvm::expect::notify_parent_expect(env, &shared_memory); + } + + bd.new_build_unconditional_branch(then_block); + } + roc_target::PtrWidth::Bytes4 => { + // temporary WASM implementation + throw_internal_exception(env, parent, "An expectation failed!"); + } + } + } else { + bd.position_at_end(throw_block); + bd.new_build_unconditional_branch(then_block); + } + + bd.position_at_end(then_block); + + build_exp_stmt( + env, + layout_interner, + layout_ids, + func_spec_solutions, + scope, + parent, + remainder, + ) + } + + ExpectFx { + condition: cond_symbol, + region, + lookups, + variables, + remainder, + } => { + let bd = env.builder; + let context = env.context; + + let (cond, _cond_layout) = scope.load_symbol_and_layout(cond_symbol); + + let condition = bd.new_build_int_compare( + IntPredicate::EQ, + cond.into_int_value(), + context.bool_type().const_int(1, false), + "is_true", + ); + + let then_block = context.append_basic_block(parent, "then_block"); + let throw_block = context.append_basic_block(parent, "throw_block"); + + bd.new_build_conditional_branch(condition, then_block, throw_block); + + if env.mode.runs_expects() { + bd.position_at_end(throw_block); + + match env.target_info.ptr_width() { + roc_target::PtrWidth::Bytes8 => { + let shared_memory = SharedMemoryPointer::get(env); + + clone_to_shared_memory( + env, + layout_interner, + scope, + layout_ids, + &shared_memory, + *cond_symbol, + *region, + lookups, + variables, + ); + + bd.new_build_unconditional_branch(then_block); + } + roc_target::PtrWidth::Bytes4 => { + // temporary WASM implementation + throw_internal_exception(env, parent, "An expectation failed!"); + } + } + } else { + bd.position_at_end(throw_block); + bd.new_build_unconditional_branch(then_block); + } + + bd.position_at_end(then_block); + + build_exp_stmt( + env, + layout_interner, + layout_ids, + func_spec_solutions, + scope, + parent, + remainder, + ) + } + + Crash(sym, tag) => { + throw_exception(env, scope, sym, *tag); + + // unused value (must return a BasicValue) + let zero = env.context.i64_type().const_zero(); + zero.into() + } + } +} + +fn build_return<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + layout: LayoutRepr<'a>, + value: BasicValueEnum<'ctx>, + parent: FunctionValue<'ctx>, +) { + match RocReturn::from_layout(layout_interner, layout) { + RocReturn::Return => { + if let Some(block) = env.builder.get_insert_block() { + if block.get_terminator().is_none() { + env.builder.new_build_return(Some(&value)); + } + } + } + RocReturn::ByPointer => { + // we need to write our value into the final argument of the current function + let parameters = parent.get_params(); + let out_parameter = parameters.last().unwrap(); + debug_assert!(out_parameter.is_pointer_value()); + + let destination = out_parameter.into_pointer_value(); + if layout.is_passed_by_reference(layout_interner) { + debug_assert!( + value.is_pointer_value(), + "{:?}: {:?}\n{:?}", + parent.get_name(), + value, + layout + ); + + // What we want to do here is + // + // let value_ptr = value.into_pointer_value(); + // if value_ptr.get_first_use().is_some() { + // value_ptr.replace_all_uses_with(destination); + // + // In other words, if the source pointer is used, + // then we just subsitute the source for the input pointer, done. + // + // Only that does not work if the source is not written to. + // A simple example is the identity function + // + // A slightly more complex case that will also make the above not + // work is when the source pointer is only incremented, but not + // written to. Then there is a first_use, but it's still invalid to + // subsitute source with destination + // + // Hence, we explicitly memcpy source to destination, and rely on + // LLVM optimizing away any inefficiencies. + build_memcpy( + env, + layout_interner, + layout, + destination, + value.into_pointer_value(), + ); + } else { + env.builder.new_build_store(destination, value); + } + + if let Some(block) = env.builder.get_insert_block() { + match block.get_terminator() { + None => { + env.builder.new_build_return(None); + } + Some(terminator) => { + terminator.remove_from_basic_block(); + env.builder.new_build_return(None); + } + } + } + } + } +} + +fn equivalent_type_constructors(t1: &BasicTypeEnum, t2: &BasicTypeEnum) -> bool { + use BasicTypeEnum::*; + match (t1, t2) { + (ArrayType(_), ArrayType(_)) => true, + (ArrayType(_), _) => false, + (FloatType(_), FloatType(_)) => true, + (FloatType(_), _) => false, + (IntType(_), IntType(_)) => true, + (IntType(_), _) => false, + (PointerType(_), PointerType(_)) => true, + (PointerType(_), _) => false, + (StructType(_), StructType(_)) => true, + (StructType(_), _) => false, + (VectorType(_), VectorType(_)) => true, + (VectorType(_), _) => false, + } +} + +/// Cast a value to another value of the same size, but only if their types are not equivalent. +/// This is needed to allow us to interoperate between recursive pointers in unions that are +/// opaque, and well-typed. +/// +/// This will no longer be necessary and should be removed after we employ opaque pointers from +/// LLVM. +pub fn cast_if_necessary_for_opaque_recursive_pointers<'ctx>( + builder: &Builder<'ctx>, + from_value: BasicValueEnum<'ctx>, + to_type: BasicTypeEnum<'ctx>, +) -> BasicValueEnum<'ctx> { + if from_value.get_type() != to_type + // Only perform the cast if the target types are transmutable. + && equivalent_type_constructors(&from_value.get_type(), &to_type) + { + complex_bitcast( + builder, + from_value, + to_type, + "bitcast_for_opaque_recursive_pointer", + ) + } else { + from_value + } +} + +/// Cast a value to another value of the same (or smaller?) size +pub fn cast_basic_basic<'ctx>( + builder: &Builder<'ctx>, + from_value: BasicValueEnum<'ctx>, + to_type: BasicTypeEnum<'ctx>, +) -> BasicValueEnum<'ctx> { + complex_bitcast(builder, from_value, to_type, "cast_basic_basic") +} + +pub fn complex_bitcast_struct_struct<'ctx>( + builder: &Builder<'ctx>, + from_value: StructValue<'ctx>, + to_type: StructType<'ctx>, + name: &str, +) -> StructValue<'ctx> { + complex_bitcast(builder, from_value.into(), to_type.into(), name).into_struct_value() +} + +pub fn cast_block_of_memory_to_tag<'ctx>( + builder: &Builder<'ctx>, + from_value: StructValue<'ctx>, + to_type: BasicTypeEnum<'ctx>, +) -> StructValue<'ctx> { + complex_bitcast( + builder, + from_value.into(), + to_type, + "block_of_memory_to_tag", + ) + .into_struct_value() +} + +/// Cast a value to another value of the same (or smaller?) size +pub fn complex_bitcast<'ctx>( + builder: &Builder<'ctx>, + from_value: BasicValueEnum<'ctx>, + to_type: BasicTypeEnum<'ctx>, + name: &str, +) -> BasicValueEnum<'ctx> { + use BasicTypeEnum::*; + + if let (PointerType(_), PointerType(_)) = (from_value.get_type(), to_type) { + // we can't use the more straightforward bitcast in all cases + // it seems like a bitcast only works on integers and pointers + // and crucially does not work not on arrays + return builder + .new_build_pointer_cast( + from_value.into_pointer_value(), + to_type.into_pointer_type(), + name, + ) + .into(); + } + + complex_bitcast_from_bigger_than_to(builder, from_value, to_type, name) +} + +/// Check the size of the input and output types. Pretending we have more bytes at a pointer than +/// we actually do can lead to faulty optimizations and weird segfaults/crashes +pub fn complex_bitcast_check_size<'ctx>( + env: &Env<'_, 'ctx, '_>, + from_value: BasicValueEnum<'ctx>, + to_type: BasicTypeEnum<'ctx>, + name: &str, +) -> BasicValueEnum<'ctx> { + use BasicTypeEnum::*; + + if let (PointerType(_), PointerType(_)) = (from_value.get_type(), to_type) { + // we can't use the more straightforward bitcast in all cases + // it seems like a bitcast only works on integers and pointers + // and crucially does not work not on arrays + return env + .builder + .new_build_pointer_cast( + from_value.into_pointer_value(), + to_type.into_pointer_type(), + name, + ) + .into(); + } + + let block = env.builder.get_insert_block().expect("to be in a function"); + let parent = block.get_parent().expect("to be in a function"); + let then_block = env.context.append_basic_block(parent, "then"); + let else_block = env.context.append_basic_block(parent, "else"); + let cont_block = env.context.append_basic_block(parent, "cont"); + + let from_size = from_value.get_type().size_of().unwrap(); + let to_size = to_type.size_of().unwrap(); + + let condition = env.builder.new_build_int_compare( + IntPredicate::UGT, + from_size, + to_size, + "from_size >= to_size", + ); + + env.builder + .new_build_conditional_branch(condition, then_block, else_block); + + let then_answer = { + env.builder.position_at_end(then_block); + let result = complex_bitcast_from_bigger_than_to(env.builder, from_value, to_type, name); + env.builder.new_build_unconditional_branch(cont_block); + result + }; + + let else_answer = { + env.builder.position_at_end(else_block); + let result = complex_bitcast_to_bigger_than_from(env.builder, from_value, to_type, name); + env.builder.new_build_unconditional_branch(cont_block); + result + }; + + env.builder.position_at_end(cont_block); + + let result = env.builder.new_build_phi(then_answer.get_type(), "answer"); + + result.add_incoming(&[(&then_answer, then_block), (&else_answer, else_block)]); + + result.as_basic_value() +} + +fn complex_bitcast_from_bigger_than_to<'ctx>( + builder: &Builder<'ctx>, + from_value: BasicValueEnum<'ctx>, + to_type: BasicTypeEnum<'ctx>, + name: &str, +) -> BasicValueEnum<'ctx> { + // store the value in memory + let argument_pointer = builder.new_build_alloca(from_value.get_type(), "cast_alloca"); + builder.new_build_store(argument_pointer, from_value); + + // then read it back as a different type + let to_type_pointer = builder.new_build_pointer_cast( + argument_pointer, + to_type.ptr_type(inkwell::AddressSpace::default()), + name, + ); + + builder.new_build_load(to_type, to_type_pointer, "cast_value") +} + +fn complex_bitcast_to_bigger_than_from<'ctx>( + builder: &Builder<'ctx>, + from_value: BasicValueEnum<'ctx>, + to_type: BasicTypeEnum<'ctx>, + name: &str, +) -> BasicValueEnum<'ctx> { + // reserve space in memory with the return type. This way, if the return type is bigger + // than the input type, we don't access invalid memory when later taking a pointer to + // the cast value + let storage = builder.new_build_alloca(to_type, "cast_alloca"); + + // then cast the pointer to our desired type + let from_type_pointer = builder.new_build_pointer_cast( + storage, + from_value + .get_type() + .ptr_type(inkwell::AddressSpace::default()), + name, + ); + + // store the value in memory + builder.new_build_store(from_type_pointer, from_value); + + // then read it back as a different type + builder.new_build_load(to_type, storage, "cast_value") +} + +/// get the tag id out of a pointer to a wrapped (i.e. stores the tag id at runtime) layout +fn get_tag_id_wrapped<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + union_layout: UnionLayout<'a>, + from_value: PointerValue<'ctx>, +) -> IntValue<'ctx> { + let union_struct_type = struct_type_from_union_layout(env, layout_interner, &union_layout); + let tag_id_type = basic_type_from_layout( + env, + layout_interner, + layout_interner.get_repr(union_layout.tag_id_layout()), + ); + + let tag_id_ptr = env.builder.new_build_struct_gep( + union_struct_type, + from_value, + RocUnion::TAG_ID_INDEX, + "tag_id_ptr", + ); + + env.builder + .new_build_load(tag_id_type, tag_id_ptr, "load_tag_id") + .into_int_value() +} + +pub fn get_tag_id_non_recursive<'ctx>( + env: &Env<'_, 'ctx, '_>, + tag: StructValue<'ctx>, +) -> IntValue<'ctx> { + env.builder + .build_extract_value(tag, RocUnion::TAG_ID_INDEX, "get_tag_id") + .unwrap() + .into_int_value() +} + +struct SwitchArgsIr<'a, 'ctx> { + pub cond_symbol: Symbol, + pub cond_layout: InLayout<'a>, + pub branches: &'a [(u64, BranchInfo<'a>, roc_mono::ir::Stmt<'a>)], + pub default_branch: &'a roc_mono::ir::Stmt<'a>, + pub ret_type: BasicTypeEnum<'ctx>, +} + +fn const_i128<'ctx>(env: &Env<'_, 'ctx, '_>, value: i128) -> IntValue<'ctx> { + // truncate the lower 64 bits + let value = value as u128; + let a = value as u64; + + // get the upper 64 bits + let b = (value >> 64) as u64; + + env.context + .i128_type() + .const_int_arbitrary_precision(&[a, b]) +} + +fn const_u128<'ctx>(env: &Env<'_, 'ctx, '_>, value: u128) -> IntValue<'ctx> { + // truncate the lower 64 bits + let value = value; + let a = value as u64; + + // get the upper 64 bits + let b = (value >> 64) as u64; + + env.context + .i128_type() + .const_int_arbitrary_precision(&[a, b]) +} + +fn build_switch_ir<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + layout_ids: &mut LayoutIds<'a>, + func_spec_solutions: &FuncSpecSolutions, + scope: &Scope<'a, 'ctx>, + parent: FunctionValue<'ctx>, + switch_args: SwitchArgsIr<'a, 'ctx>, +) -> BasicValueEnum<'ctx> { + let arena = env.arena; + let builder = env.builder; + let context = env.context; + let SwitchArgsIr { + branches, + cond_symbol, + mut cond_layout, + default_branch, + ret_type, + .. + } = switch_args; + + let mut copy = scope.clone(); + let scope = &mut copy; + + let cond_symbol = &cond_symbol; + let (cond_value, stored_layout) = scope.load_symbol_and_layout(cond_symbol); + + debug_assert_eq!( + basic_type_from_layout(env, layout_interner, layout_interner.get_repr(cond_layout)), + basic_type_from_layout( + env, + layout_interner, + layout_interner.get_repr(stored_layout) + ), + "This switch matches on {cond_layout:?}, but the matched-on symbol {cond_symbol:?} has layout {stored_layout:?}" + ); + + let cont_block = context.append_basic_block(parent, "cont"); + + // Build the condition + let cond = match layout_interner.get_repr(cond_layout) { + LayoutRepr::Builtin(Builtin::Float(float_width)) => { + // float matches are done on the bit pattern + cond_layout = Layout::float_width(float_width); + + let int_type = match float_width { + FloatWidth::F32 => env.context.i32_type(), + FloatWidth::F64 => env.context.i64_type(), + }; + + builder + .new_build_bitcast(cond_value, int_type, "") + .into_int_value() + } + LayoutRepr::Union(variant) => { + cond_layout = variant.tag_id_layout(); + + get_tag_id(env, layout_interner, parent, &variant, cond_value) + } + LayoutRepr::Builtin(_) => cond_value.into_int_value(), + other => todo!("Build switch value from layout: {:?}", other), + }; + + // Build the cases + let mut incoming = Vec::with_capacity_in(branches.len(), arena); + + if let LayoutRepr::Builtin(Builtin::Bool) = layout_interner.get_repr(cond_layout) { + match (branches, default_branch) { + ([(0, _, false_branch)], true_branch) | ([(1, _, true_branch)], false_branch) => { + let then_block = context.append_basic_block(parent, "then_block"); + let else_block = context.append_basic_block(parent, "else_block"); + + builder.new_build_conditional_branch(cond, then_block, else_block); + + { + builder.position_at_end(then_block); + + let branch_val = build_exp_stmt( + env, + layout_interner, + layout_ids, + func_spec_solutions, + scope, + parent, + true_branch, + ); + + if then_block.get_terminator().is_none() { + builder.new_build_unconditional_branch(cont_block); + incoming.push((branch_val, then_block)); + } + } + + { + builder.position_at_end(else_block); + + let branch_val = build_exp_stmt( + env, + layout_interner, + layout_ids, + func_spec_solutions, + scope, + parent, + false_branch, + ); + + if else_block.get_terminator().is_none() { + builder.new_build_unconditional_branch(cont_block); + incoming.push((branch_val, else_block)); + } + } + } + + _ => { + unreachable!() + } + } + } else { + let default_block = context.append_basic_block(parent, "default"); + let mut cases = Vec::with_capacity_in(branches.len(), arena); + + for (int, _, _) in branches.iter() { + // Switch constants must all be same type as switch value! + // e.g. this is incorrect, and will trigger a LLVM warning: + // + // switch i8 %apple1, label %default [ + // i64 2, label %branch2 + // i64 0, label %branch0 + // i64 1, label %branch1 + // ] + // + // they either need to all be i8, or i64 + let condition_int_type = cond.get_type(); + + let int_val = if condition_int_type == context.i128_type() { + const_i128(env, *int as i128) + } else { + condition_int_type.const_int(*int, false) + }; + + let block = context.append_basic_block(parent, format!("branch{int}").as_str()); + + cases.push((int_val, block)); + } + + builder.new_build_switch(cond, default_block, &cases); + + for ((_, _, branch_expr), (_, block)) in branches.iter().zip(cases) { + builder.position_at_end(block); + + let branch_val = build_exp_stmt( + env, + layout_interner, + layout_ids, + func_spec_solutions, + scope, + parent, + branch_expr, + ); + + if block.get_terminator().is_none() { + builder.new_build_unconditional_branch(cont_block); + incoming.push((branch_val, block)); + } + } + + // The block for the conditional's default branch. + builder.position_at_end(default_block); + + let default_val = build_exp_stmt( + env, + layout_interner, + layout_ids, + func_spec_solutions, + scope, + parent, + default_branch, + ); + + if default_block.get_terminator().is_none() { + builder.new_build_unconditional_branch(cont_block); + incoming.push((default_val, default_block)); + } + } + + // emit merge block + if incoming.is_empty() { + unsafe { + cont_block.delete().unwrap(); + } + // produce unused garbage value + context.i64_type().const_zero().into() + } else { + builder.position_at_end(cont_block); + + let phi = builder.new_build_phi(ret_type, "branch"); + + for (branch_val, block) in incoming { + phi.add_incoming(&[(&Into::::into(branch_val), block)]); + } + + phi.as_basic_value() + } +} + +/// Creates a new stack allocation instruction in the entry block of the function. +pub fn create_entry_block_alloca<'ctx>( + env: &Env<'_, 'ctx, '_>, + parent: FunctionValue<'_>, + basic_type: BasicTypeEnum<'ctx>, + name: &str, +) -> PointerValue<'ctx> { + let builder = env.context.create_builder(); + let entry = parent.get_first_basic_block().unwrap(); + + match entry.get_first_instruction() { + Some(first_instr) => builder.position_before(&first_instr), + None => builder.position_at_end(entry), + } + + builder.new_build_alloca(basic_type, name) +} + +fn expose_function_to_host<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + symbol: Symbol, + roc_function: FunctionValue<'ctx>, + arguments: &'a [InLayout<'a>], + niche: Niche<'a>, + return_layout: InLayout<'a>, + layout_ids: &mut LayoutIds<'a>, +) { + let ident_string = symbol.as_str(&env.interns); + + let proc_layout = ProcLayout { + arguments, + result: return_layout, + niche, + }; + + let c_function_name: String = layout_ids + .get_toplevel(symbol, &proc_layout) + .to_exposed_symbol_string(symbol, &env.interns); + + expose_function_to_host_help_c_abi( + env, + layout_interner, + ident_string, + roc_function, + arguments, + return_layout, + &c_function_name, + ); +} + +fn expose_function_to_host_help_c_abi_generic<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + roc_function: FunctionValue<'ctx>, + arguments: &[InLayout<'a>], + return_layout: InLayout<'a>, + c_function_name: &str, +) -> FunctionValue<'ctx> { + // NOTE we ingore env.mode here + + let mut cc_argument_types = Vec::with_capacity_in(arguments.len(), env.arena); + for layout in arguments { + cc_argument_types.push(to_cc_type(env, layout_interner, *layout)); + } + + // STEP 1: turn `f : a,b,c -> d` into `f : a,b,c, &d -> {}` + // let mut argument_types = roc_function.get_type().get_param_types(); + let mut argument_types = cc_argument_types; + + match roc_function.get_type().get_return_type() { + None => { + // this function already returns by-pointer + let output_type = roc_function.get_type().get_param_types().pop().unwrap(); + argument_types.insert(0, output_type); + } + Some(return_type) => { + let output_type = return_type.ptr_type(AddressSpace::default()); + argument_types.insert(0, output_type.into()); + } + } + // This is not actually a function that returns a value but then became + // return-by-pointer do to the calling convention. Instead, here we + // explicitly are forcing the passing of values via the first parameter + // pointer, since they are generic and hence opaque to anything outside roc. + let c_function_spec = FunctionSpec::cconv(env, CCReturn::Void, None, &argument_types); + + let c_function = add_func( + env.context, + env.module, + c_function_name, + c_function_spec, + Linkage::External, + ); + + let subprogram = env.new_subprogram(c_function_name); + c_function.set_subprogram(subprogram); + + // STEP 2: build the exposed function's body + let builder = env.builder; + let context = env.context; + + let entry = context.append_basic_block(c_function, "entry"); + + builder.position_at_end(entry); + + debug_info_init!(env, c_function); + + // drop the first argument, which is the pointer we write the result into + let args_vector = c_function.get_params(); + let mut args = args_vector.as_slice(); + + // drop the output parameter + args = &args[1..]; + + let mut arguments_for_call = Vec::with_capacity_in(args.len(), env.arena); + + let it = args.iter().zip(roc_function.get_type().get_param_types()); + for (arg, fastcc_type) in it { + let arg_type = arg.get_type(); + if arg_type == fastcc_type { + // the C and Fast calling conventions agree + arguments_for_call.push(*arg); + } else { + // not pretty, but seems to cover all our current cases + if arg_type.is_pointer_type() && !fastcc_type.is_pointer_type() { + // bitcast the ptr + let fastcc_ptr = env.builder.new_build_pointer_cast( + arg.into_pointer_value(), + fastcc_type.ptr_type(AddressSpace::default()), + "bitcast_arg", + ); + + let loaded = env + .builder + .new_build_load(fastcc_type, fastcc_ptr, "load_arg"); + arguments_for_call.push(loaded); + } else { + let as_cc_type = env.builder.new_build_pointer_cast( + arg.into_pointer_value(), + fastcc_type.into_pointer_type(), + "to_cc_type_ptr", + ); + arguments_for_call.push(as_cc_type.into()); + } + } + } + + let arguments_for_call = &arguments_for_call.into_bump_slice(); + + let call_result = if env.mode.returns_roc_result() { + debug_assert_eq!(args.len(), roc_function.get_params().len()); + + let roc_wrapper_function = + make_exception_catcher(env, layout_interner, roc_function, return_layout); + debug_assert_eq!( + arguments_for_call.len(), + roc_wrapper_function.get_params().len() + ); + + builder.position_at_end(entry); + + let wrapped_layout = roc_call_result_layout(env.arena, return_layout); + call_direct_roc_function( + env, + layout_interner, + roc_function, + wrapped_layout, + arguments_for_call, + ) + } else { + call_direct_roc_function( + env, + layout_interner, + roc_function, + layout_interner.get_repr(return_layout), + arguments_for_call, + ) + }; + + let output_arg_index = 0; + + let output_arg = c_function + .get_nth_param(output_arg_index as u32) + .unwrap() + .into_pointer_value(); + + store_roc_value( + env, + layout_interner, + layout_interner.get_repr(return_layout), + output_arg, + call_result, + ); + builder.new_build_return(None); + + c_function +} + +fn expose_function_to_host_help_c_abi_gen_test<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + ident_string: &str, + roc_function: FunctionValue<'ctx>, + arguments: &[InLayout<'a>], + return_layout: InLayout<'a>, + c_function_name: &str, +) -> FunctionValue<'ctx> { + // a tagged union to indicate to the test loader that a panic occurred. + // especially when running 32-bit binaries on a 64-bit machine, there + // does not seem to be a smarter solution + let wrapper_return_type = roc_call_result_type( + env, + basic_type_from_layout( + env, + layout_interner, + layout_interner.get_repr(return_layout), + ), + ); + + let mut cc_argument_types = Vec::with_capacity_in(arguments.len(), env.arena); + for layout in arguments { + cc_argument_types.push(to_cc_type(env, layout_interner, *layout)); + } + + // STEP 1: turn `f : a,b,c -> d` into `f : a,b,c, &d -> {}` if the C abi demands it + let mut argument_types = cc_argument_types; + let return_type = wrapper_return_type; + + let c_function_spec = { + let output_type = return_type.ptr_type(AddressSpace::default()); + argument_types.push(output_type.into()); + FunctionSpec::cconv(env, CCReturn::Void, None, &argument_types) + }; + + let c_function = add_func( + env.context, + env.module, + c_function_name, + c_function_spec, + Linkage::External, + ); + + let subprogram = env.new_subprogram(c_function_name); + c_function.set_subprogram(subprogram); + + // STEP 2: build the exposed function's body + let builder = env.builder; + let context = env.context; + + let entry = context.append_basic_block(c_function, "entry"); + + builder.position_at_end(entry); + + debug_info_init!(env, c_function); + + // drop the final argument, which is the pointer we write the result into + let args_vector = c_function.get_params(); + let mut args = args_vector.as_slice(); + let args_length = args.len(); + + args = &args[..args.len() - 1]; + + let mut arguments_for_call = Vec::with_capacity_in(args.len(), env.arena); + + let it = args + .iter() + .zip(roc_function.get_type().get_param_types()) + .zip(arguments); + for ((arg, fastcc_type), layout) in it { + let arg_type = arg.get_type(); + if arg_type == fastcc_type { + // the C and Fast calling conventions agree + arguments_for_call.push(*arg); + } else { + match layout_interner.get_repr(*layout) { + repr @ LayoutRepr::Builtin(Builtin::List(_)) => { + let list_type = basic_type_from_layout(env, layout_interner, repr); + + let loaded = env.builder.new_build_load( + list_type, + arg.into_pointer_value(), + "load_list_pointer", + ); + let cast = + complex_bitcast_check_size(env, loaded, fastcc_type, "to_fastcc_type_1"); + arguments_for_call.push(cast); + } + _ => { + let cast = + complex_bitcast_check_size(env, *arg, fastcc_type, "to_fastcc_type_1"); + arguments_for_call.push(cast); + } + } + } + } + + let arguments_for_call = &arguments_for_call.into_bump_slice(); + + let (call_result, call_result_layout) = { + let last_block = builder.get_insert_block().unwrap(); + + let roc_wrapper_function = + make_exception_catcher(env, layout_interner, roc_function, return_layout); + + builder.position_at_end(last_block); + + let wrapper_result = roc_call_result_layout(env.arena, return_layout); + + let roc_value = call_direct_roc_function( + env, + layout_interner, + roc_wrapper_function, + wrapper_result, + arguments_for_call, + ); + + (roc_value, wrapper_result) + }; + + let output_arg_index = args_length - 1; + + let output_arg = c_function + .get_nth_param(output_arg_index as u32) + .unwrap() + .into_pointer_value(); + + store_roc_value( + env, + layout_interner, + call_result_layout, + output_arg, + call_result, + ); + builder.new_build_return(None); + + // STEP 3: build a {} -> u64 function that gives the size of the return type + let size_function_spec = FunctionSpec::cconv( + env, + CCReturn::Return, + Some(env.context.i64_type().as_basic_type_enum()), + &[], + ); + + let size_function_name: String = format!("roc__{ident_string}_size"); + + let size_function = add_func( + env.context, + env.module, + size_function_name.as_str(), + size_function_spec, + Linkage::External, + ); + + let subprogram = env.new_subprogram(&size_function_name); + size_function.set_subprogram(subprogram); + + let entry = context.append_basic_block(size_function, "entry"); + + builder.position_at_end(entry); + + debug_info_init!(env, size_function); + + let size: BasicValueEnum = return_type.size_of().unwrap().into(); + builder.new_build_return(Some(&size)); + + c_function +} + +fn expose_function_to_host_help_c_abi_v2<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + roc_function: FunctionValue<'ctx>, + arguments: &[InLayout<'a>], + return_layout: InLayout<'a>, + c_function_name: &str, +) -> FunctionValue<'ctx> { + let it = arguments + .iter() + .map(|l| to_cc_type(env, layout_interner, *l)); + let argument_types = Vec::from_iter_in(it, env.arena); + + let return_type = basic_type_from_layout( + env, + layout_interner, + layout_interner.get_repr(return_layout), + ); + + let cc_return = to_cc_return(env, layout_interner, return_layout); + let roc_return = + RocReturn::from_layout(layout_interner, layout_interner.get_repr(return_layout)); + + let c_function_spec = FunctionSpec::cconv(env, cc_return, Some(return_type), &argument_types); + + let c_function = add_func( + env.context, + env.module, + c_function_name, + c_function_spec, + Linkage::External, + ); + + let c_abi_roc_str_type = env.context.struct_type( + &[ + env.context + .i8_type() + .ptr_type(AddressSpace::default()) + .into(), + env.ptr_int().into(), + env.ptr_int().into(), + ], + false, + ); + + // a temporary solution to be able to pass RocStr by-value from a host language. + { + let extra = match cc_return { + CCReturn::Return => 0, + CCReturn::ByPointer => 1, + CCReturn::Void => 0, + }; + + for (i, layout) in arguments.iter().enumerate() { + if let LayoutRepr::Builtin(Builtin::Str) = layout_interner.get_repr(*layout) { + // Indicate to LLVM that this argument is semantically passed by-value + // even though technically (because of its size) it is passed by-reference + let byval_attribute_id = Attribute::get_named_enum_kind_id("byval"); + debug_assert!(byval_attribute_id > 0); + + // if ret_typ is a pointer type. We need the base type here. + let ret_typ = c_function.get_type().get_param_types()[i + extra]; + let ret_base_typ = if ret_typ.is_pointer_type() { + c_abi_roc_str_type.as_any_type_enum() + } else { + ret_typ.as_any_type_enum() + }; + + let byval_attribute = env + .context + .create_type_attribute(byval_attribute_id, ret_base_typ); + c_function.add_attribute(AttributeLoc::Param((i + extra) as u32), byval_attribute); + } + } + } + + let subprogram = env.new_subprogram(c_function_name); + c_function.set_subprogram(subprogram); + + // STEP 2: build the exposed function's body + let builder = env.builder; + let context = env.context; + + let entry = context.append_basic_block(c_function, "entry"); + builder.position_at_end(entry); + + let params = c_function.get_params(); + + let param_types = Vec::from_iter_in(roc_function.get_type().get_param_types(), env.arena); + + let (params, param_types) = match (&roc_return, &cc_return) { + // Drop the "return pointer" if it exists on the roc function + // and the c function does not return via pointer + (RocReturn::ByPointer, CCReturn::Return) => { + // Roc currently puts the return pointer at the end of the argument list. + // As such, we drop the last element here instead of the first. + ( + ¶ms[..], + ¶m_types[..param_types.len().saturating_sub(1)], + ) + } + // Drop the return pointer the other way, if the C function returns by pointer but Roc + // doesn't + (RocReturn::Return, CCReturn::ByPointer) => (¶ms[1..], ¶m_types[..]), + (RocReturn::ByPointer, CCReturn::ByPointer) => { + // Both return by pointer but Roc puts it at the end and C puts it at the beginning + ( + ¶ms[1..], + ¶m_types[..param_types.len().saturating_sub(1)], + ) + } + (RocReturn::Return, CCReturn::Void) => { + // the roc function returns a unit value. like `{}` or `{ { {}, {} }, {} }`. + // In C, this is modelled as a function returning void + (¶ms[..], ¶m_types[..]) + } + (RocReturn::ByPointer, CCReturn::Void) => { + // the roc function returns a unit value. like `{}` or `{ { {}, {} }, {} }`. + // In C, this is modelled as a function returning void + ( + ¶ms[..], + ¶m_types[..param_types.len().saturating_sub(1)], + ) + } + _ => (¶ms[..], ¶m_types[..]), + }; + + debug_assert_eq!( + params.len(), + param_types.len(), + "when exposing a function to the host, params.len() was {}, but param_types.len() was {}", + params.len(), + param_types.len() + ); + + let it = params + .iter() + .zip(param_types) + .zip(arguments) + .enumerate() + .map(|(i, ((arg, fastcc_type), layout))| { + let arg_type = arg.get_type(); + if arg_type == *fastcc_type { + // the C and Fast calling conventions agree + *arg + } else { + // not pretty, but seems to cover all our current cases + if arg_type.is_pointer_type() && !fastcc_type.is_pointer_type() { + // On x86_*, Modify the argument to specify it is passed by value and nonnull + // Aarch*, just passes in the pointer directly. + if matches!( + env.target_info.architecture, + roc_target::Architecture::X86_32 | roc_target::Architecture::X86_64 + ) { + let c_abi_type = match layout_interner.get_repr(*layout) { + LayoutRepr::Builtin(Builtin::Str | Builtin::List(_)) => { + c_abi_roc_str_type + } + _ => todo!("figure out what the C type is"), + }; + + let byval = context.create_type_attribute( + Attribute::get_named_enum_kind_id("byval"), + c_abi_type.as_any_type_enum(), + ); + let nonnull = context.create_type_attribute( + Attribute::get_named_enum_kind_id("nonnull"), + c_abi_type.as_any_type_enum(), + ); + // C return pointer goes at the beginning of params, and we must skip it if it exists. + let returns_pointer = matches!(cc_return, CCReturn::ByPointer); + let param_index = i as u32 + returns_pointer as u32; + + c_function.add_attribute(AttributeLoc::Param(param_index), byval); + c_function.add_attribute(AttributeLoc::Param(param_index), nonnull); + } + // bitcast the ptr + let fastcc_ptr = env.builder.new_build_pointer_cast( + arg.into_pointer_value(), + fastcc_type.ptr_type(AddressSpace::default()), + "bitcast_arg", + ); + + env.builder + .new_build_load(*fastcc_type, fastcc_ptr, "load_arg") + } else { + complex_bitcast_check_size(env, *arg, *fastcc_type, "to_fastcc_type_2") + } + } + }); + + let arguments = Vec::from_iter_in(it, env.arena); + + let value = call_direct_roc_function( + env, + layout_interner, + roc_function, + layout_interner.get_repr(return_layout), + arguments.as_slice(), + ); + + match cc_return { + CCReturn::Return => match roc_return { + RocReturn::Return => { + env.builder.new_build_return(Some(&value)); + } + RocReturn::ByPointer => { + let loaded = env.builder.new_build_load( + return_type, + value.into_pointer_value(), + "load_result", + ); + env.builder.new_build_return(Some(&loaded)); + } + }, + CCReturn::ByPointer => { + let out_ptr = c_function.get_nth_param(0).unwrap().into_pointer_value(); + match roc_return { + RocReturn::Return => { + env.builder.new_build_store(out_ptr, value); + } + RocReturn::ByPointer => { + // TODO: ideally, in this case, we should pass the C return pointer directly + // into the call_roc_function rather than forcing an extra alloca, load, and + // store! + let value = env.builder.new_build_load( + return_type, + value.into_pointer_value(), + "load_roc_result", + ); + env.builder.new_build_store(out_ptr, value); + } + } + env.builder.new_build_return(None); + } + CCReturn::Void => { + env.builder.new_build_return(None); + } + } + + c_function +} + +fn expose_function_to_host_help_c_abi<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + ident_string: &str, + roc_function: FunctionValue<'ctx>, + arguments: &[InLayout<'a>], + return_layout: InLayout<'a>, + c_function_name: &str, +) -> FunctionValue<'ctx> { + match env.mode { + LlvmBackendMode::GenTest | LlvmBackendMode::WasmGenTest | LlvmBackendMode::CliTest => { + return expose_function_to_host_help_c_abi_gen_test( + env, + layout_interner, + ident_string, + roc_function, + arguments, + return_layout, + c_function_name, + ) + } + + LlvmBackendMode::Binary | LlvmBackendMode::BinaryDev | LlvmBackendMode::BinaryGlue => {} + } + + // a generic version that writes the result into a passed *u8 pointer + expose_function_to_host_help_c_abi_generic( + env, + layout_interner, + roc_function, + arguments, + return_layout, + &format!("{c_function_name}_generic"), + ); + + let c_function = expose_function_to_host_help_c_abi_v2( + env, + layout_interner, + roc_function, + arguments, + return_layout, + c_function_name, + ); + + // STEP 3: build a {} -> u64 function that gives the size of the return type + let size_function_spec = FunctionSpec::cconv( + env, + CCReturn::Return, + Some(env.context.i64_type().as_basic_type_enum()), + &[], + ); + let size_function_name: String = format!("{c_function_name}_size"); + + let size_function = add_func( + env.context, + env.module, + size_function_name.as_str(), + size_function_spec, + Linkage::External, + ); + + let subprogram = env.new_subprogram(&size_function_name); + size_function.set_subprogram(subprogram); + + let entry = env.context.append_basic_block(size_function, "entry"); + + env.builder.position_at_end(entry); + + debug_info_init!(env, size_function); + + let return_type = match env.mode { + LlvmBackendMode::GenTest | LlvmBackendMode::WasmGenTest | LlvmBackendMode::CliTest => { + roc_call_result_type(env, roc_function.get_type().get_return_type().unwrap()).into() + } + + LlvmBackendMode::Binary | LlvmBackendMode::BinaryDev | LlvmBackendMode::BinaryGlue => { + basic_type_from_layout( + env, + layout_interner, + layout_interner.get_repr(return_layout), + ) + } + }; + + let size: BasicValueEnum = return_type.size_of().unwrap().into(); + env.builder.new_build_return(Some(&size)); + + c_function +} + +pub fn get_sjlj_buffer<'ctx>(env: &Env<'_, 'ctx, '_>) -> PointerValue<'ctx> { + // The size of jump_buf is target-dependent. + // - AArch64 needs 3 machine-sized words + // - LLVM says the following about the SJLJ intrinsic: + // + // [It is] a five word buffer in which the calling context is saved. + // The front end places the frame pointer in the first word, and the + // target implementation of this intrinsic should place the destination + // address for a llvm.eh.sjlj.longjmp in the second word. + // The following three words are available for use in a target-specific manner. + // + // So, let's create a 5-word buffer. + let word_type = match env.target_info.ptr_width() { + PtrWidth::Bytes4 => env.context.i32_type(), + PtrWidth::Bytes8 => env.context.i64_type(), + }; + let type_ = word_type.array_type(5); + + let global = match env.module.get_global("roc_sjlj_buffer") { + Some(global) => global, + None => env.module.add_global(type_, None, "roc_sjlj_buffer"), + }; + + global.set_initializer(&type_.const_zero()); + + env.builder.new_build_pointer_cast( + global.as_pointer_value(), + env.context.i32_type().ptr_type(AddressSpace::default()), + "cast_sjlj_buffer", + ) +} + +pub fn build_setjmp_call<'ctx>(env: &Env<'_, 'ctx, '_>) -> BasicValueEnum<'ctx> { + let jmp_buf = get_sjlj_buffer(env); + if cfg!(target_arch = "aarch64") { + // Due to https://github.com/roc-lang/roc/issues/2965, we use a setjmp we linked in from Zig + call_bitcode_fn(env, &[jmp_buf.into()], bitcode::UTILS_SETJMP) + } else { + // Anywhere else, use the LLVM intrinsic. + // https://llvm.org/docs/ExceptionHandling.html#llvm-eh-sjlj-setjmp + + let buf_type = env + .context + .i8_type() + .ptr_type(AddressSpace::default()) + .array_type(5); + + let jmp_buf_i8p_arr = env.builder.new_build_pointer_cast( + jmp_buf, + buf_type.ptr_type(AddressSpace::default()), + "jmp_buf [5 x i8*]", + ); + + // LLVM asks us to please store the frame pointer in the first word. + let frame_address = env.call_intrinsic( + LLVM_FRAME_ADDRESS, + &[env.context.i32_type().const_zero().into()], + ); + + let zero = env.context.i32_type().const_zero(); + let fa_index = env.context.i32_type().const_zero(); + let fa = unsafe { + env.builder.new_build_in_bounds_gep( + buf_type, + jmp_buf_i8p_arr, + &[zero, fa_index], + "frame address index", + ) + }; + env.builder.new_build_store(fa, frame_address); + + // LLVM says that the target implementation of the setjmp intrinsic will put the + // destination address at index 1, and that the remaining three words are for ad-hoc target + // usage. But for whatever reason, on x86, it appears we need a stacksave in those words. + let ss_index = env.context.i32_type().const_int(2, false); + let ss = unsafe { + env.builder.new_build_in_bounds_gep( + buf_type, + jmp_buf_i8p_arr, + &[zero, ss_index], + "name", + ) + }; + let stack_save = env.call_intrinsic(LLVM_STACK_SAVE, &[]); + env.builder.new_build_store(ss, stack_save); + + let jmp_buf_i8p = env + .builder + .new_build_pointer_cast( + jmp_buf, + env.context.i8_type().ptr_type(AddressSpace::default()), + "jmp_buf i8*", + ) + .into(); + env.call_intrinsic(LLVM_SETJMP, &[jmp_buf_i8p]) + } +} + +/// Pointer to RocStr which is the panic message. +pub fn get_panic_msg_ptr<'ctx>(env: &Env<'_, 'ctx, '_>) -> PointerValue<'ctx> { + let str_typ = zig_str_type(env); + + let global_name = "roc_panic_msg_str"; + let global = env.module.get_global(global_name).unwrap_or_else(|| { + let global = env.module.add_global(str_typ, None, global_name); + global.set_initializer(&str_typ.const_zero()); + global + }); + + global.as_pointer_value() +} + +/// Pointer to the panic tag. +/// Only non-zero values must be written into here. +pub fn get_panic_tag_ptr<'ctx>(env: &Env<'_, 'ctx, '_>) -> PointerValue<'ctx> { + let i64_typ = env.context.i64_type(); + + let global_name = "roc_panic_msg_tag"; + let global = env.module.get_global(global_name).unwrap_or_else(|| { + let global = env.module.add_global(i64_typ, None, global_name); + global.set_initializer(&i64_typ.const_zero()); + global + }); + + global.as_pointer_value() +} + +fn set_jump_and_catch_long_jump<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + parent: FunctionValue<'ctx>, + // The roc function to call + roc_function: FunctionValue<'ctx>, + roc_arguments: &[BasicValueEnum<'ctx>], + roc_return_layout: InLayout<'a>, +) -> BasicValueEnum<'ctx> { + let context = env.context; + let builder = env.builder; + + let return_type = basic_type_from_layout( + env, + layout_interner, + layout_interner.get_repr(roc_return_layout), + ); + let call_result_return_conv = { + let layout = roc_call_result_layout(env.arena, roc_return_layout); + RocReturn::from_layout(layout_interner, layout) + }; + let call_result_type = roc_call_result_type(env, return_type.as_basic_type_enum()); + let result_alloca = builder.new_build_alloca(call_result_type, "result"); + + let then_block = context.append_basic_block(parent, "then_block"); + let catch_block = context.append_basic_block(parent, "catch_block"); + let cont_block = context.append_basic_block(parent, "cont_block"); + + let panicked_u32 = build_setjmp_call(env); + let panicked_bool = env.builder.new_build_int_compare( + IntPredicate::NE, + panicked_u32.into_int_value(), + panicked_u32.get_type().into_int_type().const_zero(), + "to_bool", + ); + + env.builder + .new_build_conditional_branch(panicked_bool, catch_block, then_block); + + // all went well + { + builder.position_at_end(then_block); + + let call_result = call_direct_roc_function( + env, + layout_interner, + roc_function, + layout_interner.get_repr(roc_return_layout), + roc_arguments, + ); + + let return_value = + make_good_roc_result(env, layout_interner, roc_return_layout, call_result); + + builder.new_build_store(result_alloca, return_value); + + env.builder.new_build_unconditional_branch(cont_block); + } + + // something went wrong + { + builder.position_at_end(catch_block); + + // RocStr* global + let error_msg_ptr = get_panic_msg_ptr(env); + // i64* global + let error_tag_ptr = get_panic_tag_ptr(env); + + let return_value = { + let v1 = call_result_type.const_zero(); + + // tag must be non-zero, indicating failure + let tag = + builder.new_build_load(env.context.i64_type(), error_tag_ptr, "load_panic_tag"); + + let v2 = builder.build_insert_value(v1, tag, 0, "set_error").unwrap(); + + let v3 = builder + .build_insert_value(v2, error_msg_ptr, 1, "set_exception") + .unwrap(); + v3 + }; + + builder.new_build_store(result_alloca, return_value); + + env.builder.new_build_unconditional_branch(cont_block); + } + + env.builder.position_at_end(cont_block); + + match call_result_return_conv { + RocReturn::Return => builder.new_build_load( + call_result_type, + result_alloca, + "set_jump_and_catch_long_jump_load_result", + ), + RocReturn::ByPointer => result_alloca.into(), + } +} + +fn make_exception_catcher<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + roc_function: FunctionValue<'ctx>, + return_layout: InLayout<'a>, +) -> FunctionValue<'ctx> { + let wrapper_function_name = format!("{}_catcher", roc_function.get_name().to_str().unwrap()); + + let function_value = make_exception_catching_wrapper( + env, + layout_interner, + roc_function, + return_layout, + &wrapper_function_name, + ); + + function_value.set_linkage(Linkage::Internal); + + function_value +} + +fn roc_call_result_layout<'a>(arena: &'a Bump, return_layout: InLayout<'a>) -> LayoutRepr<'a> { + let elements = [Layout::U64, Layout::STR_PTR, return_layout]; + + LayoutRepr::struct_(arena.alloc(elements)) +} + +// TODO: coalesce with `roc_call_result_layout`? +fn roc_call_result_type<'ctx>( + env: &Env<'_, 'ctx, '_>, + return_type: BasicTypeEnum<'ctx>, +) -> StructType<'ctx> { + env.context.struct_type( + &[ + env.context.i64_type().into(), + zig_str_type(env).ptr_type(AddressSpace::default()).into(), + return_type, + ], + false, + ) +} + +fn make_good_roc_result<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + return_layout: InLayout<'a>, + return_value: BasicValueEnum<'ctx>, +) -> BasicValueEnum<'ctx> { + let context = env.context; + let builder = env.builder; + + let return_type = basic_type_from_layout( + env, + layout_interner, + layout_interner.get_repr(return_layout), + ); + + let v1 = roc_call_result_type( + env, + basic_type_from_layout( + env, + layout_interner, + layout_interner.get_repr(return_layout), + ), + ) + .const_zero(); + + let v2 = builder + .build_insert_value(v1, context.i64_type().const_zero(), 0, "set_no_error") + .unwrap(); + + let v3 = if layout_interner.is_passed_by_reference(return_layout) { + let loaded = env.builder.new_build_load( + return_type, + return_value.into_pointer_value(), + "load_call_result_passed_by_ptr", + ); + builder + .build_insert_value(v2, loaded, 2, "set_call_result") + .unwrap() + } else { + builder + .build_insert_value(v2, return_value, 2, "set_call_result") + .unwrap() + }; + + v3.into_struct_value().into() +} + +fn make_exception_catching_wrapper<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + roc_function: FunctionValue<'ctx>, + return_layout: InLayout<'a>, + wrapper_function_name: &str, +) -> FunctionValue<'ctx> { + // build the C calling convention wrapper + + let context = env.context; + let builder = env.builder; + + // TODO: pass these, and the roc function, in directly? + let wrapper_return_layout = roc_call_result_layout(env.arena, return_layout); + + let wrapper_return_type = roc_call_result_type( + env, + basic_type_from_layout( + env, + layout_interner, + layout_interner.get_repr(return_layout), + ), + ); + + let roc_function_type = roc_function.get_type(); + let argument_types = + match RocReturn::from_layout(layout_interner, layout_interner.get_repr(return_layout)) { + RocReturn::Return => roc_function_type.get_param_types(), + RocReturn::ByPointer => { + // Our fastcc passes the return pointer as the last parameter. Remove it from the + // argument types used for the wrapper, since the wrapper's return type will go here + // when we build the wrapper function spec below. + let mut types = roc_function_type.get_param_types(); + types.pop(); + + types + } + }; + + let wrapper_return_conv = RocReturn::from_layout(layout_interner, wrapper_return_layout); + + let wrapper_function_spec = FunctionSpec::fastcc( + env, + wrapper_return_conv, + wrapper_return_type.into(), + Vec::from_iter_in(argument_types, env.arena), + ); + + // Add main to the module. + let wrapper_function = add_func( + env.context, + env.module, + wrapper_function_name, + wrapper_function_spec, + Linkage::External, + ); + + let subprogram = env.new_subprogram(wrapper_function_name); + wrapper_function.set_subprogram(subprogram); + + // The exposed main function must adhere to the C calling convention, but the wrapper can still be fastcc. + wrapper_function.set_call_conventions(FAST_CALL_CONV); + + // invoke instead of call, so that we can catch any exceptions thrown in Roc code + let roc_function_arguments = { + let mut params = wrapper_function.get_params(); + match wrapper_return_conv { + RocReturn::Return => { /* passthrough */ } + RocReturn::ByPointer => { + params.pop(); + } + } + params + }; + + let basic_block = context.append_basic_block(wrapper_function, "entry"); + builder.position_at_end(basic_block); + + debug_info_init!(env, wrapper_function); + + let wrapper_return_result = set_jump_and_catch_long_jump( + env, + layout_interner, + wrapper_function, + roc_function, + &roc_function_arguments, + return_layout, + ); + + build_return( + env, + layout_interner, + wrapper_return_layout, + wrapper_return_result, + wrapper_function, + ); + + wrapper_function +} + +pub(crate) fn build_proc_headers<'a, 'r, 'ctx>( + env: &'r Env<'a, 'ctx, '_>, + layout_interner: &'r STLayoutInterner<'a>, + mod_solutions: &'a ModSolutions, + procedures: MutMap<(Symbol, ProcLayout<'a>), roc_mono::ir::Proc<'a>>, + scope: &mut Scope<'a, 'ctx>, + layout_ids: &mut LayoutIds<'a>, + // alias_analysis_solutions: AliasAnalysisSolutions, +) -> std::vec::Vec<( + roc_mono::ir::Proc<'a>, + std::vec::Vec<(&'a FuncSpecSolutions, FunctionValue<'ctx>)>, +)> { + // Populate Procs further and get the low-level Expr from the canonical Expr + let mut headers = std::vec::Vec::with_capacity(procedures.len()); + for ((symbol, layout), proc) in procedures { + let name_bytes = roc_alias_analysis::func_name_bytes(&proc); + let func_name = FuncName(&name_bytes); + + let func_solutions = mod_solutions.func_solutions(func_name).unwrap(); + + let it = func_solutions.specs(); + let mut function_values = std::vec::Vec::with_capacity(it.size_hint().0); + + let is_erased = proc.is_erased; + debug_assert!(!is_erased || func_solutions.specs().count() == 1); + + for specialization in it { + let func_spec = if is_erased { + FuncBorrowSpec::Erased + } else { + FuncBorrowSpec::Some(*specialization) + }; + + let fn_val = + build_proc_header(env, layout_interner, func_spec, symbol, &proc, layout_ids); + + if proc.args.is_empty() { + // this is a 0-argument thunk, i.e. a top-level constant definition + // it must be in-scope everywhere in the module! + scope.insert_top_level_thunk(symbol, layout, fn_val); + } + + let func_spec_solutions = func_solutions.spec(specialization).unwrap(); + + function_values.push((func_spec_solutions, fn_val)); + } + headers.push((proc, function_values)); + } + + headers +} + +pub fn build_procedures<'a>( + env: &Env<'a, '_, '_>, + layout_interner: &STLayoutInterner<'a>, + opt_level: OptLevel, + procedures: MutMap<(Symbol, ProcLayout<'a>), roc_mono::ir::Proc<'a>>, + host_exposed_lambda_sets: HostExposedLambdaSets<'a>, + entry_point: EntryPoint<'a>, + debug_output_file: Option<&Path>, + glue_layouts: &GlueLayouts<'a>, +) { + let mod_solutions = build_procedures_help( + env, + layout_interner, + opt_level, + procedures, + host_exposed_lambda_sets, + entry_point, + debug_output_file, + ); + + let niche = Niche::NONE; + + for (symbol, top_level) in glue_layouts.getters.iter().copied() { + let it = top_level.arguments.iter().copied(); + let bytes = roc_alias_analysis::func_name_bytes_help(symbol, it, niche, top_level.result); + let func_name = FuncName(&bytes); + let func_solutions = mod_solutions.func_solutions(func_name).unwrap(); + + let mut it = func_solutions.specs(); + let Some(func_spec) = it.next() else { + // TODO this means a function was not considered host-exposed in mono + continue; + }; + debug_assert!( + it.next().is_none(), + "we expect only one specialization of this symbol" + ); + + // NOTE fake layout; it is only used for debug prints + let getter_fn = function_value_by_func_spec(env, FuncBorrowSpec::Some(*func_spec), symbol); + + let name = getter_fn.get_name().to_str().unwrap(); + let getter_name = symbol.as_str(&env.interns); + + // Add the getter function to the module. + let _ = expose_function_to_host_help_c_abi( + env, + layout_interner, + name, + getter_fn, + top_level.arguments, + top_level.result, + getter_name, + ); + } +} + +pub fn build_wasm_test_wrapper<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + opt_level: OptLevel, + procedures: MutMap<(Symbol, ProcLayout<'a>), roc_mono::ir::Proc<'a>>, + entry_point: SingleEntryPoint<'a>, +) -> (&'static str, FunctionValue<'ctx>) { + let mod_solutions = build_procedures_help( + env, + layout_interner, + opt_level, + procedures, + vec![], + EntryPoint::Single(entry_point), + Some(&std::env::temp_dir().join("test.ll")), + ); + + promote_to_wasm_test_wrapper( + env, + layout_interner, + mod_solutions, + entry_point.symbol, + entry_point.layout, + ) +} + +pub fn build_procedures_return_main<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + opt_level: OptLevel, + procedures: MutMap<(Symbol, ProcLayout<'a>), roc_mono::ir::Proc<'a>>, + host_exposed_lambda_sets: HostExposedLambdaSets<'a>, + entry_point: SingleEntryPoint<'a>, +) -> (&'static str, FunctionValue<'ctx>) { + let mod_solutions = build_procedures_help( + env, + layout_interner, + opt_level, + procedures, + host_exposed_lambda_sets, + EntryPoint::Single(entry_point), + Some(&std::env::temp_dir().join("test.ll")), + ); + + promote_to_main_function( + env, + layout_interner, + mod_solutions, + entry_point.symbol, + entry_point.layout, + ) +} + +pub fn build_procedures_expose_expects<'a>( + env: &Env<'a, '_, '_>, + layout_interner: &STLayoutInterner<'a>, + opt_level: OptLevel, + expects: &'a [Symbol], + procedures: MutMap<(Symbol, ProcLayout<'a>), roc_mono::ir::Proc<'a>>, +) -> Vec<'a, &'a str> { + let entry_point = EntryPoint::Expects { symbols: expects }; + + let mod_solutions = build_procedures_help( + env, + layout_interner, + opt_level, + procedures, + vec![], + entry_point, + Some(&std::env::temp_dir().join("test.ll")), + ); + + let captures_niche = Niche::NONE; + + let top_level = ProcLayout { + arguments: &[], + result: Layout::UNIT, + niche: captures_niche, + }; + + let mut expect_names = Vec::with_capacity_in(expects.len(), env.arena); + + for symbol in expects.iter().copied() { + let it = top_level.arguments.iter().copied(); + let bytes = + roc_alias_analysis::func_name_bytes_help(symbol, it, captures_niche, top_level.result); + let func_name = FuncName(&bytes); + let func_solutions = mod_solutions.func_solutions(func_name).unwrap(); + + let mut it = func_solutions.specs(); + let func_spec = match it.next() { + Some(spec) => spec, + None => panic!("no specialization for expect {symbol}"), + }; + + debug_assert!( + it.next().is_none(), + "we expect only one specialization of this symbol" + ); + + // NOTE fake layout; it is only used for debug prints + let roc_main_fn = + function_value_by_func_spec(env, FuncBorrowSpec::Some(*func_spec), symbol); + + let name = roc_main_fn.get_name().to_str().unwrap(); + + let expect_name = &format!("Expect_{name}"); + let expect_name = env.arena.alloc_str(expect_name); + expect_names.push(&*expect_name); + + // Add main to the module. + let _ = expose_function_to_host_help_c_abi( + env, + layout_interner, + name, + roc_main_fn, + top_level.arguments, + top_level.result, + &format!("Expect_{name}"), + ); + } + + expect_names +} + +fn build_procedures_help<'a>( + env: &Env<'a, '_, '_>, + layout_interner: &STLayoutInterner<'a>, + opt_level: OptLevel, + procedures: MutMap<(Symbol, ProcLayout<'a>), roc_mono::ir::Proc<'a>>, + host_exposed_lambda_sets: HostExposedLambdaSets<'a>, + entry_point: EntryPoint<'a>, + debug_output_file: Option<&Path>, +) -> &'a ModSolutions { + let mut layout_ids = roc_mono::layout::LayoutIds::default(); + let mut scope = Scope::default(); + + let it1 = procedures.iter().map(|x| x.1); + let it2 = host_exposed_lambda_sets.iter().map(|(_, _, hels)| hels); + + let solutions = match roc_alias_analysis::spec_program( + env.arena, + layout_interner, + opt_level, + entry_point, + it1, + it2, + ) { + Err(e) => panic!("Error in alias analysis: {e}"), + Ok(solutions) => solutions, + }; + + let solutions = env.arena.alloc(solutions); + + let mod_solutions = solutions + .mod_solutions(roc_alias_analysis::MOD_APP) + .unwrap(); + + // Add all the Proc headers to the module. + // We have to do this in a separate pass first, + // because their bodies may reference each other. + let headers = build_proc_headers( + env, + layout_interner, + mod_solutions, + procedures, + &mut scope, + &mut layout_ids, + ); + + let (_, function_pass) = construct_optimization_passes(env.module, opt_level); + + for (proc, fn_vals) in headers { + for (func_spec_solutions, fn_val) in fn_vals { + let mut current_scope = scope.clone(); + + // only have top-level thunks for this proc's module in scope + // this retain is not needed for correctness, but will cause less confusion when debugging + let home = proc.name.name().module_id(); + current_scope.retain_top_level_thunks_for_module(home); + + build_proc( + env, + layout_interner, + &mut layout_ids, + func_spec_solutions, + scope.clone(), + &proc, + fn_val, + ); + + // call finalize() before any code generation/verification + env.dibuilder.finalize(); + + if fn_val.verify(true) { + function_pass.run_on(&fn_val); + } else { + let mode = "NON-OPTIMIZED"; + + eprintln!( + "\n\nFunction {:?} failed LLVM verification in {} build. Its content was:\n", + fn_val.get_name().to_str().unwrap(), + mode, + ); + + fn_val.print_to_stderr(); + + if let Some(app_ll_file) = debug_output_file { + env.module.print_to_file(app_ll_file).unwrap(); + + panic!( + r"😱 LLVM errors when defining function {:?}; I wrote the full LLVM IR to {:?}", + fn_val.get_name().to_str().unwrap(), + app_ll_file, + ); + } else { + env.module.print_to_stderr(); + + panic!( + "The preceding code was from {:?}, which failed LLVM verification in {} build.", + fn_val.get_name().to_str().unwrap(), + mode, + ) + } + } + } + } + + use LlvmBackendMode::*; + match env.mode { + GenTest | WasmGenTest | CliTest => { /* no host, or exposing types is not supported */ } + Binary | BinaryDev | BinaryGlue => { + for (proc_name, alias_name, hels) in host_exposed_lambda_sets.iter() { + let ident_string = proc_name.name().as_str(&env.interns); + let fn_name: String = format!("{}_{}", ident_string, hels.id.0); + + expose_alias_to_host( + env, + layout_interner, + mod_solutions, + &fn_name, + *alias_name, + hels, + ) + } + } + } + + mod_solutions +} + +pub enum FuncBorrowSpec { + /// This function has an specialization due to alias analysis. + Some(FuncSpec), + /// This function does not have a specialization due to alias analysis, + /// because it is type-erased, and thus has no statically determined AA specialization. + Erased, +} + +fn func_spec_name<'a>( + arena: &'a Bump, + interns: &Interns, + symbol: Symbol, + func_spec: FuncBorrowSpec, +) -> bumpalo::collections::String<'a> { + use std::fmt::Write; + + let mut buf = bumpalo::collections::String::with_capacity_in(1, arena); + + let ident_string = symbol.as_str(interns); + let module_string = interns.module_ids.get_name(symbol.module_id()).unwrap(); + write!(buf, "{module_string}_{ident_string}_").unwrap(); + + match func_spec { + FuncBorrowSpec::Some(func_spec) => { + for byte in func_spec.0.iter() { + write!(buf, "{byte:x?}").unwrap(); + } + } + FuncBorrowSpec::Erased => write!(buf, "erased").unwrap(), + } + + buf +} + +fn build_proc_header<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + func_spec: FuncBorrowSpec, + symbol: Symbol, + proc: &roc_mono::ir::Proc<'a>, + layout_ids: &mut LayoutIds<'a>, +) -> FunctionValue<'ctx> { + let args = proc.args; + let arena = env.arena; + + let fn_name = func_spec_name(env.arena, &env.interns, symbol, func_spec); + + let ret_type = basic_type_from_layout( + env, + layout_interner, + layout_interner.get_repr(proc.ret_layout), + ); + let mut arg_basic_types = Vec::with_capacity_in(args.len(), arena); + + for (layout, _) in args.iter() { + let arg_type = + argument_type_from_layout(env, layout_interner, layout_interner.get_repr(*layout)); + + arg_basic_types.push(arg_type); + } + + let roc_return = + RocReturn::from_layout(layout_interner, layout_interner.get_repr(proc.ret_layout)); + let fn_spec = FunctionSpec::fastcc(env, roc_return, ret_type, arg_basic_types); + + let fn_val = add_func( + env.context, + env.module, + fn_name.as_str(), + fn_spec, + Linkage::Internal, + ); + + let subprogram = env.new_subprogram(&fn_name); + fn_val.set_subprogram(subprogram); + + if env.exposed_to_host.contains(&symbol) { + let arguments = Vec::from_iter_in(proc.args.iter().map(|(layout, _)| *layout), env.arena); + expose_function_to_host( + env, + layout_interner, + symbol, + fn_val, + arguments.into_bump_slice(), + proc.name.niche(), + proc.ret_layout, + layout_ids, + ); + } + + if false { + let kind_id = Attribute::get_named_enum_kind_id("alwaysinline"); + debug_assert!(kind_id > 0); + let enum_attr = env.context.create_enum_attribute(kind_id, 0); + fn_val.add_attribute(AttributeLoc::Function, enum_attr); + } + + if false { + let kind_id = Attribute::get_named_enum_kind_id("noinline"); + debug_assert!(kind_id > 0); + let enum_attr = env.context.create_enum_attribute(kind_id, 0); + fn_val.add_attribute(AttributeLoc::Function, enum_attr); + } + + fn_val +} + +fn expose_alias_to_host<'a>( + env: &Env<'a, '_, '_>, + layout_interner: &STLayoutInterner<'a>, + mod_solutions: &'a ModSolutions, + fn_name: &str, + alias_symbol: Symbol, + hels: &HostExposedLambdaSet<'a>, +) { + match hels.raw_function_layout { + RawFunctionLayout::Function(arguments, closure, result) => { + // define closure size and return value size, e.g. + // + // * roc__mainForHost_1_Update_size() -> i64 + // * roc__mainForHost_1_Update_result_size() -> i64 + + let it = hels.proc_layout.arguments.iter().copied(); + let bytes = roc_alias_analysis::func_name_bytes_help( + hels.symbol, + it, + Niche::NONE, + hels.proc_layout.result, + ); + let func_name = FuncName(&bytes); + let func_solutions = mod_solutions.func_solutions(func_name).unwrap(); + + let mut it = func_solutions.specs(); + let evaluator = match it.next() { + Some(func_spec) => { + debug_assert!( + it.next().is_none(), + "we expect only one specialization of this symbol" + ); + + function_value_by_func_spec(env, FuncBorrowSpec::Some(*func_spec), hels.symbol) + } + None => { + // morphic did not generate a specialization for this function, + // therefore it must actually be unused. + // An example is our closure callers + panic!("morphic did not specialize {:?}", hels.symbol); + } + }; + + build_closure_caller( + env, + layout_interner, + fn_name, + evaluator, + alias_symbol, + arguments, + result, + closure, + result, + ) + } + + RawFunctionLayout::ErasedFunction(..) => todo_lambda_erasure!(), + RawFunctionLayout::ZeroArgumentThunk(result) => { + // Define only the return value size, since this is a thunk + // + // * roc__mainForHost_1_Update_result_size() -> i64 + + let result_type = + basic_type_from_layout(env, layout_interner, layout_interner.get_repr(result)); + + build_host_exposed_alias_size_help( + env, + fn_name, + alias_symbol, + Some("result"), + result_type, + ); + } + } +} + +fn build_closure_caller<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + def_name: &str, + evaluator: FunctionValue<'ctx>, + alias_symbol: Symbol, + arguments: &[InLayout<'a>], + return_layout: InLayout<'a>, + lambda_set: LambdaSet<'a>, + result: InLayout<'a>, +) { + let mut argument_types = Vec::with_capacity_in(arguments.len() + 3, env.arena); + + for layout in arguments { + let arg_type = + basic_type_from_layout(env, layout_interner, layout_interner.get_repr(*layout)); + let arg_ptr_type = arg_type.ptr_type(AddressSpace::default()); + + argument_types.push(arg_ptr_type.into()); + } + + let closure_argument_type = { + let basic_type = basic_type_from_layout( + env, + layout_interner, + layout_interner.get_repr(lambda_set.runtime_representation()), + ); + + basic_type.ptr_type(AddressSpace::default()) + }; + argument_types.push(closure_argument_type.into()); + + let context = &env.context; + let builder = env.builder; + + let result_type = + basic_type_from_layout(env, layout_interner, layout_interner.get_repr(result)); + + let output_type = { result_type.ptr_type(AddressSpace::default()) }; + argument_types.push(output_type.into()); + + // STEP 1: build function header + + // e.g. `roc__mainForHost_0_caller` (def_name is `mainForHost_0`) + let function_name = format!("roc__{def_name}_caller"); + + let function_spec = FunctionSpec::cconv(env, CCReturn::Void, None, &argument_types); + + let function_value = add_func( + env.context, + env.module, + function_name.as_str(), + function_spec, + Linkage::External, + ); + + // STEP 2: build function body + + let entry = context.append_basic_block(function_value, "entry"); + + builder.position_at_end(entry); + + let mut evaluator_arguments = function_value.get_params(); + + // the final parameter is the output pointer, pop it + let output = evaluator_arguments.pop().unwrap().into_pointer_value(); + + // NOTE this may be incorrect in the long run + // here we load any argument that is a pointer + let closure_layout = lambda_set.runtime_representation(); + let layouts_it = arguments.iter().chain(std::iter::once(&closure_layout)); + for (param, layout) in evaluator_arguments.iter_mut().zip(layouts_it) { + if param.is_pointer_value() && !layout_interner.is_passed_by_reference(*layout) { + let basic_type = + basic_type_from_layout(env, layout_interner, layout_interner.get_repr(*layout)); + *param = builder.new_build_load(basic_type, param.into_pointer_value(), "load_param"); + } + } + + if env.mode.returns_roc_result() { + let call_result = set_jump_and_catch_long_jump( + env, + layout_interner, + function_value, + evaluator, + &evaluator_arguments, + return_layout, + ); + + builder.new_build_store(output, call_result); + } else { + let call_result = call_direct_roc_function( + env, + layout_interner, + evaluator, + layout_interner.get_repr(return_layout), + &evaluator_arguments, + ); + + if layout_interner.is_passed_by_reference(return_layout) { + build_memcpy( + env, + layout_interner, + layout_interner.get_repr(return_layout), + output, + call_result.into_pointer_value(), + ); + } else { + builder.new_build_store(output, call_result); + } + }; + + builder.new_build_return(None); + + // STEP 3: build a {} -> u64 function that gives the size of the return type + build_host_exposed_alias_size_help(env, def_name, alias_symbol, Some("result"), result_type); + + // STEP 4: build a {} -> u64 function that gives the size of the closure + build_host_exposed_alias_size( + env, + layout_interner, + def_name, + alias_symbol, + lambda_set.runtime_representation(), + ); +} + +fn build_host_exposed_alias_size<'a, 'r>( + env: &'r Env<'a, '_, '_>, + layout_interner: &'r STLayoutInterner<'a>, + def_name: &str, + alias_symbol: Symbol, + layout: InLayout<'a>, +) { + build_host_exposed_alias_size_help( + env, + def_name, + alias_symbol, + None, + basic_type_from_layout(env, layout_interner, layout_interner.get_repr(layout)), + ) +} + +fn build_host_exposed_alias_size_help<'a, 'ctx>( + env: &'a Env<'a, 'ctx, '_>, + def_name: &str, + _alias_symbol: Symbol, + opt_label: Option<&str>, + basic_type: BasicTypeEnum<'ctx>, +) { + let builder = env.builder; + let context = env.context; + + let i64 = env.context.i64_type().as_basic_type_enum(); + let size_function_spec = FunctionSpec::cconv(env, CCReturn::Return, Some(i64), &[]); + let size_function_name: String = if let Some(label) = opt_label { + format!("roc__{def_name}_{label}_size") + } else { + format!("roc__{def_name}_size",) + }; + + let size_function = add_func( + env.context, + env.module, + size_function_name.as_str(), + size_function_spec, + Linkage::External, + ); + + let entry = context.append_basic_block(size_function, "entry"); + + builder.position_at_end(entry); + + let size: BasicValueEnum = basic_type.size_of().unwrap().into(); + builder.new_build_return(Some(&size)); +} + +fn build_proc<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + layout_ids: &mut LayoutIds<'a>, + func_spec_solutions: &FuncSpecSolutions, + mut scope: Scope<'a, 'ctx>, + proc: &roc_mono::ir::Proc<'a>, + fn_val: FunctionValue<'ctx>, +) { + let args = proc.args; + let context = &env.context; + + // Add a basic block for the entry point + let entry = context.append_basic_block(fn_val, "entry"); + let builder = env.builder; + + builder.position_at_end(entry); + + debug_info_init!(env, fn_val); + + // Add args to scope + for (arg_val, (layout, arg_symbol)) in fn_val.get_param_iter().zip(args) { + arg_val.set_name(arg_symbol.as_str(&env.interns)); + scope.insert(*arg_symbol, *layout, arg_val); + } + + let body = build_exp_stmt( + env, + layout_interner, + layout_ids, + func_spec_solutions, + &mut scope, + fn_val, + &proc.body, + ); + + // only add a return if codegen did not already add one + if let Some(block) = builder.get_insert_block() { + if block.get_terminator().is_none() { + builder.new_build_return(Some(&body)); + } + } +} + +pub fn verify_fn(fn_val: FunctionValue<'_>) { + if !fn_val.verify(print_fn_verification_output()) { + unsafe { + fn_val.delete(); + } + + panic!("Invalid generated fn_val.") + } +} + +pub(crate) fn function_value_by_func_spec<'ctx>( + env: &Env<'_, 'ctx, '_>, + func_spec: FuncBorrowSpec, + symbol: Symbol, +) -> FunctionValue<'ctx> { + let fn_name = func_spec_name(env.arena, &env.interns, symbol, func_spec); + let fn_name = fn_name.as_str(); + + function_value_by_name_help(env, symbol, fn_name) +} + +fn function_value_by_name_help<'ctx>( + env: &Env<'_, 'ctx, '_>, + symbol: Symbol, + fn_name: &str, +) -> FunctionValue<'ctx> { + env.module.get_function(fn_name).unwrap_or_else(|| { + if symbol.is_builtin() { + panic!("Unrecognized builtin function: {fn_name:?} (symbol: {symbol:?})") + } else { + panic!("Unrecognized non-builtin function: {fn_name:?} (symbol: {symbol:?})") + } + }) +} + +#[inline(always)] +fn roc_call_direct_with_args<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + result_layout: InLayout<'a>, + name: LambdaName<'a>, + func_spec: FuncBorrowSpec, + arguments: &[BasicValueEnum<'ctx>], +) -> BasicValueEnum<'ctx> { + let fn_val = function_value_by_func_spec(env, func_spec, name.name()); + + call_direct_roc_function( + env, + layout_interner, + fn_val, + layout_interner.get_repr(result_layout), + arguments, + ) +} + +#[inline(always)] +fn roc_call_erased_with_args<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + pointer: PointerValue<'ctx>, + argument_layouts: &[InLayout<'a>], + result_layout: InLayout<'a>, + arguments: &[BasicValueEnum<'ctx>], +) -> BasicValueEnum<'ctx> { + let function_type = + fn_ptr::function_type(env, layout_interner, argument_layouts, result_layout); + let function_ptr_type = function_type.ptr_type(AddressSpace::default()); + + let function_pointer = fn_ptr::cast_to_function_ptr_type(env, pointer, function_ptr_type); + + let build_call = |arguments: &[BasicMetadataValueEnum<'ctx>]| { + env.builder + .new_build_indirect_call(function_type, function_pointer, arguments, "call") + }; + + call_roc_function_help( + env, + layout_interner, + build_call, + function_type, + layout_interner.get_repr(result_layout), + arguments, + ) +} + +pub(crate) fn call_direct_roc_function<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + roc_function: FunctionValue<'ctx>, + result_layout: LayoutRepr<'a>, + arguments: &[BasicValueEnum<'ctx>], +) -> BasicValueEnum<'ctx> { + let function_type = roc_function.get_type(); + + let build_call = |arguments: &[BasicMetadataValueEnum<'ctx>]| { + env.builder.new_build_call(roc_function, arguments, "call") + }; + debug_assert_eq!(roc_function.get_call_conventions(), FAST_CALL_CONV); + + call_roc_function_help( + env, + layout_interner, + build_call, + function_type, + result_layout, + arguments, + ) +} + +fn call_roc_function_help<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + build_call: impl FnOnce(&[BasicMetadataValueEnum<'ctx>]) -> CallSiteValue<'ctx>, + roc_function_type: FunctionType<'ctx>, + result_layout: LayoutRepr<'a>, + arguments: &[BasicValueEnum<'ctx>], +) -> BasicValueEnum<'ctx> { + let pass_by_pointer = roc_function_type.get_param_types().len() == arguments.len() + 1; + + match RocReturn::from_layout(layout_interner, result_layout) { + RocReturn::ByPointer if !pass_by_pointer => { + // WARNING this is a hack!! + let it = arguments.iter().map(|x| (*x).into()); + let mut arguments = Vec::from_iter_in(it, env.arena); + arguments.pop(); + + let result_type = basic_type_from_layout(env, layout_interner, result_layout); + let result_alloca = env.builder.new_build_alloca(result_type, "result_value"); + + arguments.push(result_alloca.into()); + + debug_assert_eq!(roc_function_type.get_param_types().len(), arguments.len()); + let call = build_call(&arguments); + + // roc functions should have the fast calling convention + call.set_call_convention(FAST_CALL_CONV); + + env.builder + .new_build_load(result_type, result_alloca, "load_result") + } + RocReturn::ByPointer => { + let it = arguments.iter().map(|x| (*x).into()); + let mut arguments = Vec::from_iter_in(it, env.arena); + + let result_type = basic_type_from_layout(env, layout_interner, result_layout); + let result_alloca = entry_block_alloca_zerofill(env, result_type, "result_value"); + + arguments.push(result_alloca.into()); + + debug_assert_eq!(roc_function_type.get_param_types().len(), arguments.len()); + let call = build_call(&arguments); + + // roc functions should have the fast calling convention + call.set_call_convention(FAST_CALL_CONV); + + if result_layout.is_passed_by_reference(layout_interner) { + result_alloca.into() + } else { + env.builder.new_build_load( + result_type, + result_alloca, + "return_by_pointer_load_result", + ) + } + } + RocReturn::Return => { + debug_assert_eq!(roc_function_type.get_param_types().len(), arguments.len()); + let it = arguments.iter().map(|x| (*x).into()); + let arguments = Vec::from_iter_in(it, env.arena); + + let call = build_call(&arguments); + + // roc functions should have the fast calling convention + call.set_call_convention(FAST_CALL_CONV); + + call.try_as_basic_value() + .left() + .unwrap_or_else(|| internal_error!("LLVM error: Invalid call by name",)) + } + } +} + +/// Translates a target_lexicon::Triple to a LLVM calling convention u32 +/// as described in https://llvm.org/doxygen/namespacellvm_1_1CallingConv.html +pub fn get_call_conventions(cc: target_lexicon::CallingConvention) -> u32 { + use target_lexicon::CallingConvention::*; + + // For now, we're returning 0 for the C calling convention on all of these. + // Not sure if we should be picking something more specific! + match cc { + SystemV => C_CALL_CONV, + WasmBasicCAbi => C_CALL_CONV, + WindowsFastcall => C_CALL_CONV, + AppleAarch64 => C_CALL_CONV, + _ => C_CALL_CONV, + } +} + +/// Source: https://llvm.org/doxygen/namespacellvm_1_1CallingConv.html +pub const C_CALL_CONV: u32 = 0; +pub const FAST_CALL_CONV: u32 = 8; +pub const COLD_CALL_CONV: u32 = 9; + +pub struct RocFunctionCall<'ctx> { + pub caller: PointerValue<'ctx>, + pub data: PointerValue<'ctx>, + pub inc_n_data: PointerValue<'ctx>, + pub data_is_owned: IntValue<'ctx>, +} + +pub(crate) fn roc_function_call<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + layout_ids: &mut LayoutIds<'a>, + transform: FunctionValue<'ctx>, + closure_data: BasicValueEnum<'ctx>, + lambda_set: LambdaSet<'a>, + closure_data_is_owned: bool, + argument_layouts: &[InLayout<'a>], + result_layout: InLayout<'a>, +) -> RocFunctionCall<'ctx> { + use crate::llvm::bitcode::{build_inc_n_wrapper, build_transform_caller}; + + let closure_data_type = basic_type_from_layout( + env, + layout_interner, + layout_interner.get_repr(lambda_set.runtime_representation()), + ); + + let closure_data_ptr = env + .builder + .new_build_alloca(closure_data_type, "closure_data_ptr"); + + store_roc_value( + env, + layout_interner, + layout_interner.get_repr(lambda_set.runtime_representation()), + closure_data_ptr, + closure_data, + ); + + let stepper_caller = build_transform_caller( + env, + layout_interner, + transform, + lambda_set, + argument_layouts, + result_layout, + ) + .as_global_value() + .as_pointer_value(); + + let inc_closure_data = build_inc_n_wrapper( + env, + layout_interner, + layout_ids, + lambda_set.runtime_representation(), + ) + .as_global_value() + .as_pointer_value(); + + let closure_data_is_owned = env + .context + .bool_type() + .const_int(closure_data_is_owned as u64, false); + + RocFunctionCall { + caller: stepper_caller, + inc_n_data: inc_closure_data, + data_is_owned: closure_data_is_owned, + data: closure_data_ptr, + } +} + +/// A type that is valid according to the C ABI +/// +/// As an example, structs that fit inside an integer type should +/// (this does not currently happen here) be coerced to that integer type. +fn to_cc_type<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + layout: InLayout<'a>, +) -> BasicTypeEnum<'ctx> { + let layout_repr = layout_interner.runtime_representation(layout); + match layout_repr { + LayoutRepr::Builtin(builtin) => to_cc_type_builtin(env, &builtin), + LayoutRepr::Struct(_) => { + let stack_type = basic_type_from_layout(env, layout_interner, layout_repr); + + if layout_repr.is_passed_by_reference(layout_interner) { + stack_type.ptr_type(AddressSpace::default()).into() + } else { + stack_type + } + } + _ => { + // TODO this is almost certainly incorrect for bigger structs + basic_type_from_layout(env, layout_interner, layout_interner.get_repr(layout)) + } + } +} + +fn to_cc_type_builtin<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + builtin: &Builtin<'a>, +) -> BasicTypeEnum<'ctx> { + match builtin { + Builtin::Int(_) | Builtin::Float(_) | Builtin::Bool | Builtin::Decimal => { + basic_type_from_builtin(env, builtin) + } + Builtin::Str | Builtin::List(_) => { + let address_space = AddressSpace::default(); + let field_types: [BasicTypeEnum; 3] = [ + env.context.i8_type().ptr_type(address_space).into(), + env.ptr_int().into(), + env.ptr_int().into(), + ]; + + let struct_type = env.context.struct_type(&field_types, false); + + struct_type.ptr_type(address_space).into() + } + } +} + +#[derive(Debug, Clone, Copy)] +pub(crate) enum RocReturn { + /// Return as normal + Return, + /// require an extra argument, a pointer + /// where the result is written into returns void + ByPointer, +} + +impl RocReturn { + fn roc_return_by_pointer(interner: &STLayoutInterner, layout: LayoutRepr) -> bool { + layout.is_passed_by_reference(interner) + } + + pub(crate) fn from_layout<'a>( + layout_interner: &STLayoutInterner<'a>, + layout: LayoutRepr<'a>, + ) -> Self { + if Self::roc_return_by_pointer(layout_interner, layout) { + RocReturn::ByPointer + } else { + RocReturn::Return + } + } +} + +#[derive(Debug, Clone, Copy)] +pub enum CCReturn { + /// Return as normal + Return, + /// require an extra argument, a pointer + /// where the result is written into + /// returns void + ByPointer, + /// The return type is zero-sized + Void, +} + +#[derive(Debug, Clone, Copy)] +pub(crate) struct FunctionSpec<'ctx> { + /// The function type + pub typ: FunctionType<'ctx>, + call_conv: u32, + + /// We only care about this for C-call-conv functions, because this may take + /// ownership of a register due to the convention. For example, on AArch64, + /// values returned-by-pointer use the x8 register. + /// But for internal functions we don't need to worry about that and we don't + /// want the convention, since it might eat a register and cause a spill! + cconv_stack_return_type: Option>, +} + +impl<'ctx> FunctionSpec<'ctx> { + fn attach_attributes(&self, ctx: &Context, fn_val: FunctionValue<'ctx>) { + fn_val.set_call_conventions(self.call_conv); + + if let Some(stack_return_type) = self.cconv_stack_return_type { + // Indicate to LLVM that this argument holds the return value of the function. + let sret_attribute_id = Attribute::get_named_enum_kind_id("sret"); + debug_assert!(sret_attribute_id > 0); + let sret_attribute = + ctx.create_type_attribute(sret_attribute_id, stack_return_type.as_any_type_enum()); + fn_val.add_attribute(AttributeLoc::Param(0), sret_attribute); + } + } + + /// C-calling convention + pub fn cconv<'a, 'env>( + env: &Env<'a, 'ctx, 'env>, + cc_return: CCReturn, + return_type: Option>, + argument_types: &[BasicTypeEnum<'ctx>], + ) -> FunctionSpec<'ctx> { + let (typ, opt_sret_parameter) = match cc_return { + CCReturn::ByPointer => { + // turn the output type into a pointer type. Make it the first argument to the function + let output_type = return_type.unwrap().ptr_type(AddressSpace::default()); + + let mut arguments: Vec<'_, BasicTypeEnum> = + bumpalo::vec![in env.arena; output_type.into()]; + arguments.extend(argument_types); + + let arguments = function_arguments(env, &arguments); + + ( + env.context.void_type().fn_type(&arguments, false), + Some(return_type.unwrap()), + ) + } + CCReturn::Return => { + let arguments = function_arguments(env, argument_types); + (return_type.unwrap().fn_type(&arguments, false), None) + } + CCReturn::Void => { + // NOTE: there may be a valid return type, but it is zero-sized. + // for instance just `{}` or something more complex like `{ { {}, {} }, {} }` + let arguments = function_arguments(env, argument_types); + (env.context.void_type().fn_type(&arguments, false), None) + } + }; + + Self { + typ, + call_conv: C_CALL_CONV, + cconv_stack_return_type: opt_sret_parameter, + } + } + + /// Fastcc calling convention + pub fn fastcc<'a, 'env>( + env: &Env<'a, 'ctx, 'env>, + roc_return: RocReturn, + return_type: BasicTypeEnum<'ctx>, + mut argument_types: Vec>, + ) -> FunctionSpec<'ctx> { + let typ = match roc_return { + RocReturn::Return => { + return_type.fn_type(&function_arguments(env, &argument_types), false) + } + RocReturn::ByPointer => { + argument_types.push(return_type.ptr_type(AddressSpace::default()).into()); + env.context + .void_type() + .fn_type(&function_arguments(env, &argument_types), false) + } + }; + + Self { + typ, + call_conv: FAST_CALL_CONV, + cconv_stack_return_type: None, + } + } + + pub fn known_fastcc(fn_type: FunctionType<'ctx>) -> FunctionSpec<'ctx> { + Self { + typ: fn_type, + call_conv: FAST_CALL_CONV, + cconv_stack_return_type: None, + } + } + + pub fn intrinsic(fn_type: FunctionType<'ctx>) -> Self { + // LLVM intrinsics always use the C calling convention, because + // they are implemented in C libraries + Self { + typ: fn_type, + call_conv: C_CALL_CONV, + cconv_stack_return_type: None, + } + } +} + +/// According to the C ABI, how should we return a value with the given layout? +pub fn to_cc_return<'a>( + env: &Env<'a, '_, '_>, + layout_interner: &STLayoutInterner<'a>, + layout: InLayout<'a>, +) -> CCReturn { + let return_size = layout_interner.stack_size(layout); + let pass_result_by_pointer = match env.target_info.operating_system { + roc_target::OperatingSystem::Windows => { + return_size >= 2 * env.target_info.ptr_width() as u32 + } + roc_target::OperatingSystem::Unix => return_size > 2 * env.target_info.ptr_width() as u32, + roc_target::OperatingSystem::Wasi => return_size > 2 * env.target_info.ptr_width() as u32, + }; + + if return_size == 0 { + CCReturn::Void + } else if pass_result_by_pointer { + CCReturn::ByPointer + } else { + CCReturn::Return + } +} + +fn function_arguments<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + arguments: &[BasicTypeEnum<'ctx>], +) -> Vec<'a, BasicMetadataTypeEnum<'ctx>> { + let it = arguments.iter().map(|x| (*x).into()); + Vec::from_iter_in(it, env.arena) +} + +fn build_foreign_symbol<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + scope: &mut Scope<'a, 'ctx>, + foreign: &roc_module::ident::ForeignSymbol, + argument_symbols: &[Symbol], + ret_layout: InLayout<'a>, +) -> BasicValueEnum<'ctx> { + let builder = env.builder; + let context = env.context; + + let fastcc_function_name = format!("{}_fastcc_wrapper", foreign.as_str()); + + let (fastcc_function, arguments) = match env.module.get_function(fastcc_function_name.as_str()) + { + Some(function_value) => { + let mut arguments = Vec::with_capacity_in(argument_symbols.len(), env.arena); + + for symbol in argument_symbols { + let (value, _) = scope.load_symbol_and_layout(symbol); + + arguments.push(value); + } + + (function_value, arguments) + } + None => { + // Here we build two functions: + // + // - an C_CALL_CONV extern that will be provided by the host, e.g. `roc_fx_putLine` + // This is just a type signature that we make available to the linker, + // and can use in the wrapper + // - a FAST_CALL_CONV wrapper that we make here, e.g. `roc_fx_putLine_fastcc_wrapper` + + let return_type = + basic_type_from_layout(env, layout_interner, layout_interner.get_repr(ret_layout)); + let roc_return = + RocReturn::from_layout(layout_interner, layout_interner.get_repr(ret_layout)); + let cc_return = to_cc_return(env, layout_interner, ret_layout); + + let mut cc_argument_types = + Vec::with_capacity_in(argument_symbols.len() + 1, env.arena); + let mut fastcc_argument_types = + Vec::with_capacity_in(argument_symbols.len(), env.arena); + let mut arguments = Vec::with_capacity_in(argument_symbols.len(), env.arena); + + for symbol in argument_symbols { + let (value, layout) = scope.load_symbol_and_layout(symbol); + + cc_argument_types.push(to_cc_type(env, layout_interner, layout)); + + let basic_type = argument_type_from_layout( + env, + layout_interner, + layout_interner.get_repr(layout), + ); + fastcc_argument_types.push(basic_type); + + arguments.push(value); + } + + let cc_type = + FunctionSpec::cconv(env, cc_return, Some(return_type), &cc_argument_types); + let cc_function = get_foreign_symbol(env, foreign.clone(), cc_type); + + let fastcc_type = + FunctionSpec::fastcc(env, roc_return, return_type, fastcc_argument_types); + + let fastcc_function = add_func( + env.context, + env.module, + &fastcc_function_name, + fastcc_type, + Linkage::Internal, + ); + + let old = builder.get_insert_block().unwrap(); + + let entry = context.append_basic_block(fastcc_function, "entry"); + { + builder.position_at_end(entry); + + let mut fastcc_parameters = fastcc_function.get_params(); + let mut cc_arguments = + Vec::with_capacity_in(fastcc_parameters.len() + 1, env.arena); + + let return_pointer = match roc_return { + RocReturn::Return => env.builder.new_build_alloca(return_type, "return_value"), + RocReturn::ByPointer => fastcc_parameters.pop().unwrap().into_pointer_value(), + }; + + if let CCReturn::ByPointer = cc_return { + cc_arguments.push(return_pointer.into()); + } + + let it = fastcc_parameters.into_iter().zip(cc_argument_types.iter()); + for (param, cc_type) in it { + if param.get_type() == *cc_type { + cc_arguments.push(param.into()); + } else { + // not pretty, but seems to cover all our current case + if cc_type.is_pointer_type() && !param.get_type().is_pointer_type() { + // we need to pass this value by-reference; put it into an alloca + // and bitcast the reference + + let param_alloca = env + .builder + .new_build_alloca(param.get_type(), "param_alloca"); + env.builder.new_build_store(param_alloca, param); + + let as_cc_type = env.builder.new_build_pointer_cast( + param_alloca, + cc_type.into_pointer_type(), + "to_cc_type_ptr", + ); + + cc_arguments.push(as_cc_type.into()); + } else { + // eprintln!("C type: {:?}", cc_type); + // eprintln!("Fastcc type: {:?}", param.get_type()); + // todo!("C <-> Fastcc interaction that we haven't seen before") + + let as_cc_type = env.builder.new_build_pointer_cast( + param.into_pointer_value(), + cc_type.into_pointer_type(), + "to_cc_type_ptr", + ); + cc_arguments.push(as_cc_type.into()); + } + } + } + + let call = env + .builder + .new_build_call(cc_function, &cc_arguments, "tmp"); + call.set_call_convention(C_CALL_CONV); + + match roc_return { + RocReturn::Return => { + let return_value = match cc_return { + CCReturn::Return => call.try_as_basic_value().left().unwrap(), + + CCReturn::ByPointer => env.builder.new_build_load( + return_type, + return_pointer, + "read_result", + ), + CCReturn::Void => return_type.const_zero(), + }; + + builder.new_build_return(Some(&return_value)); + } + RocReturn::ByPointer => { + match cc_return { + CCReturn::Return => { + let result = call.try_as_basic_value().left().unwrap(); + env.builder.new_build_store(return_pointer, result); + } + + CCReturn::ByPointer | CCReturn::Void => { + // the return value (if any) is already written to the return pointer + } + } + + builder.new_build_return(None); + } + } + } + + builder.position_at_end(old); + + (fastcc_function, arguments) + } + }; + + call_direct_roc_function( + env, + layout_interner, + fastcc_function, + layout_interner.get_repr(ret_layout), + &arguments, + ) +} + +fn define_global_str_literal_ptr<'ctx>( + env: &Env<'_, 'ctx, '_>, + message: &str, +) -> PointerValue<'ctx> { + let global = define_global_str_literal(env, message); + + let ptr = env.builder.new_build_pointer_cast( + global.as_pointer_value(), + env.context.i8_type().ptr_type(AddressSpace::default()), + "to_opaque", + ); + + // a pointer to the first actual data (skipping over the refcount) + let ptr = unsafe { + env.builder.new_build_in_bounds_gep( + env.context.i8_type(), + ptr, + &[env + .ptr_int() + .const_int(env.target_info.ptr_width() as u64, false)], + "get_rc_ptr", + ) + }; + + ptr +} + +fn define_global_str_literal<'ctx>( + env: &Env<'_, 'ctx, '_>, + message: &str, +) -> inkwell::values::GlobalValue<'ctx> { + let module = env.module; + + // hash the name so we don't re-define existing messages + let name = { + use std::collections::hash_map::DefaultHasher; + use std::hash::{Hash, Hasher}; + + let mut hasher = DefaultHasher::new(); + message.hash(&mut hasher); + let hash = hasher.finish(); + + format!("_str_literal_{hash}") + }; + + match module.get_global(&name) { + Some(current) => current, + + None => { + let size = message.bytes().len() + env.target_info.ptr_width() as usize; + let mut bytes = Vec::with_capacity_in(size, env.arena); + + // insert NULL bytes for the refcount + for _ in 0..env.target_info.ptr_width() as usize { + bytes.push(env.context.i8_type().const_zero()); + } + + // then add the data bytes + for b in message.bytes() { + bytes.push(env.context.i8_type().const_int(b as u64, false)); + } + + // use None for the address space (e.g. Const does not work) + let typ = env.context.i8_type().array_type(bytes.len() as u32); + let global = module.add_global(typ, None, &name); + + global.set_initializer(&env.context.i8_type().const_array(bytes.into_bump_slice())); + + // mimic the `global_string` function; we cannot use it directly because it assumes + // strings are NULL-terminated, which means we can't store the refcount (which is 8 + // NULL bytes) + global.set_constant(true); + global.set_alignment(env.target_info.ptr_width() as u32); + global.set_unnamed_addr(true); + global.set_linkage(inkwell::module::Linkage::Private); + + global + } + } +} + +pub(crate) fn throw_internal_exception<'ctx>( + env: &Env<'_, 'ctx, '_>, + parent: FunctionValue<'ctx>, + message: &str, +) { + let builder = env.builder; + + let str = build_string_literal(env, parent, message); + + env.call_panic(env, str, CrashTag::Roc); + + builder.new_build_unreachable(); +} + +pub(crate) fn throw_exception<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + scope: &mut Scope<'a, 'ctx>, + message: &Symbol, + tag: CrashTag, +) { + let msg_val = scope.load_symbol(message); + + env.call_panic(env, msg_val, tag); + + env.builder.new_build_unreachable(); +} + +fn get_foreign_symbol<'ctx>( + env: &Env<'_, 'ctx, '_>, + foreign_symbol: roc_module::ident::ForeignSymbol, + function_spec: FunctionSpec<'ctx>, +) -> FunctionValue<'ctx> { + let module = env.module; + + match module.get_function(foreign_symbol.as_str()) { + Some(gvalue) => gvalue, + None => { + let foreign_function = add_func( + env.context, + module, + foreign_symbol.as_str(), + function_spec, + Linkage::External, + ); + + foreign_function + } + } +} + +/// Add a function to a module, after asserting that the function is unique. +/// We never want to define the same function twice in the same module! +/// The result can be bugs that are difficult to track down. +pub(crate) fn add_func<'ctx>( + ctx: &Context, + module: &Module<'ctx>, + name: &str, + spec: FunctionSpec<'ctx>, + linkage: Linkage, +) -> FunctionValue<'ctx> { + if cfg!(debug_assertions) { + if let Some(func) = module.get_function(name) { + panic!("Attempting to redefine LLVM function {name}, which was already defined in this module as:\n\n{func:#?}"); + } + } + + let fn_val = module.add_function(name, spec.typ, Some(linkage)); + + spec.attach_attributes(ctx, fn_val); + + fn_val +} diff --git a/crates/compiler/gen_llvm/src/llvm/build_list.rs b/crates/compiler/gen_llvm/src/llvm/build_list.rs new file mode 100644 index 0000000000..eff73dca32 --- /dev/null +++ b/crates/compiler/gen_llvm/src/llvm/build_list.rs @@ -0,0 +1,870 @@ +use crate::llvm::bitcode::build_dec_wrapper; +use crate::llvm::build::{allocate_with_refcount_help, cast_basic_basic, Env, RocFunctionCall}; +use crate::llvm::convert::basic_type_from_layout; +use inkwell::builder::Builder; +use inkwell::types::{BasicType, PointerType}; +use inkwell::values::{BasicValueEnum, FunctionValue, IntValue, PointerValue, StructValue}; +use inkwell::{AddressSpace, IntPredicate}; +use morphic_lib::UpdateMode; +use roc_builtins::bitcode; +use roc_module::symbol::Symbol; +use roc_mono::layout::{ + Builtin, InLayout, Layout, LayoutIds, LayoutInterner, LayoutRepr, STLayoutInterner, +}; + +use super::bitcode::{call_list_bitcode_fn, BitcodeReturns}; +use super::build::{ + create_entry_block_alloca, load_roc_value, store_roc_value, use_roc_value, BuilderExt, +}; +use super::convert::zig_list_type; +use super::scope::Scope; +use super::struct_::struct_from_fields; + +fn call_list_bitcode_fn_1<'ctx>( + env: &Env<'_, 'ctx, '_>, + list: StructValue<'ctx>, + other_arguments: &[BasicValueEnum<'ctx>], + fn_name: &str, +) -> BasicValueEnum<'ctx> { + call_list_bitcode_fn(env, &[list], other_arguments, BitcodeReturns::List, fn_name) +} + +pub(crate) fn list_symbol_to_c_abi<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + scope: &Scope<'a, 'ctx>, + symbol: Symbol, +) -> PointerValue<'ctx> { + let parent = env + .builder + .get_insert_block() + .and_then(|b| b.get_parent()) + .unwrap(); + + let list_type = zig_list_type(env); + let list_alloca = create_entry_block_alloca(env, parent, list_type.into(), "list_alloca"); + + let list = scope.load_symbol(&symbol); + env.builder.new_build_store(list_alloca, list); + + list_alloca +} + +pub(crate) fn pass_update_mode<'ctx>( + env: &Env<'_, 'ctx, '_>, + update_mode: UpdateMode, +) -> BasicValueEnum<'ctx> { + match update_mode { + UpdateMode::Immutable => env.context.i8_type().const_zero().into(), + UpdateMode::InPlace => env.context.i8_type().const_int(1, false).into(), + } +} + +fn pass_element_as_opaque<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + element: BasicValueEnum<'ctx>, + layout: InLayout<'a>, +) -> BasicValueEnum<'ctx> { + let element_type = + basic_type_from_layout(env, layout_interner, layout_interner.get_repr(layout)); + let element_ptr = env + .builder + .new_build_alloca(element_type, "element_to_pass_as_opaque"); + store_roc_value( + env, + layout_interner, + layout_interner.get_repr(layout), + element_ptr, + element, + ); + + env.builder + .new_build_pointer_cast( + element_ptr, + env.context.i8_type().ptr_type(AddressSpace::default()), + "pass_element_as_opaque", + ) + .into() +} + +pub(crate) fn layout_width<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + layout: InLayout<'a>, +) -> BasicValueEnum<'ctx> { + env.ptr_int() + .const_int(layout_interner.stack_size(layout) as u64, false) + .into() +} + +pub(crate) fn pass_as_opaque<'ctx>( + env: &Env<'_, 'ctx, '_>, + ptr: PointerValue<'ctx>, +) -> BasicValueEnum<'ctx> { + env.builder + .new_build_pointer_cast( + ptr, + env.context.i8_type().ptr_type(AddressSpace::default()), + "pass_as_opaque", + ) + .into() +} + +pub(crate) fn list_with_capacity<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + capacity: IntValue<'ctx>, + element_layout: InLayout<'a>, +) -> BasicValueEnum<'ctx> { + call_list_bitcode_fn( + env, + &[], + &[ + capacity.into(), + env.alignment_intvalue(layout_interner, element_layout), + layout_width(env, layout_interner, element_layout), + ], + BitcodeReturns::List, + bitcode::LIST_WITH_CAPACITY, + ) +} + +pub(crate) fn list_get_unsafe<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + element_layout: InLayout<'a>, + elem_index: IntValue<'ctx>, + wrapper_struct: StructValue<'ctx>, +) -> BasicValueEnum<'ctx> { + let builder = env.builder; + + let elem_type = basic_type_from_layout( + env, + layout_interner, + layout_interner.get_repr(element_layout), + ); + let ptr_type = elem_type.ptr_type(AddressSpace::default()); + // Load the pointer to the array data + let array_data_ptr = load_list_ptr(builder, wrapper_struct, ptr_type); + + // Assume the bounds have already been checked earlier + // (e.g. by List.get or List.first, which wrap List.#getUnsafe) + let elem_ptr = unsafe { + builder.new_build_in_bounds_gep( + elem_type, + array_data_ptr, + &[elem_index], + "list_get_element", + ) + }; + + load_roc_value( + env, + layout_interner, + layout_interner.get_repr(element_layout), + elem_ptr, + "list_get_load_element", + ) +} + +/// List.reserve : List elem, Nat -> List elem +pub(crate) fn list_reserve<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + list: BasicValueEnum<'ctx>, + spare: BasicValueEnum<'ctx>, + element_layout: InLayout<'a>, + update_mode: UpdateMode, +) -> BasicValueEnum<'ctx> { + call_list_bitcode_fn_1( + env, + list.into_struct_value(), + &[ + env.alignment_intvalue(layout_interner, element_layout), + spare, + layout_width(env, layout_interner, element_layout), + pass_update_mode(env, update_mode), + ], + bitcode::LIST_RESERVE, + ) +} + +/// List.releaseExcessCapacity : List elem -> List elem +pub(crate) fn list_release_excess_capacity<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + list: BasicValueEnum<'ctx>, + element_layout: InLayout<'a>, + update_mode: UpdateMode, +) -> BasicValueEnum<'ctx> { + call_list_bitcode_fn_1( + env, + list.into_struct_value(), + &[ + env.alignment_intvalue(layout_interner, element_layout), + layout_width(env, layout_interner, element_layout), + pass_update_mode(env, update_mode), + ], + bitcode::LIST_RELEASE_EXCESS_CAPACITY, + ) +} + +/// List.appendUnsafe : List elem, elem -> List elem +pub(crate) fn list_append_unsafe<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + original_wrapper: StructValue<'ctx>, + element: BasicValueEnum<'ctx>, + element_layout: InLayout<'a>, +) -> BasicValueEnum<'ctx> { + call_list_bitcode_fn_1( + env, + original_wrapper, + &[ + pass_element_as_opaque(env, layout_interner, element, element_layout), + layout_width(env, layout_interner, element_layout), + ], + bitcode::LIST_APPEND_UNSAFE, + ) +} + +/// List.prepend : List elem, elem -> List elem +pub(crate) fn list_prepend<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + original_wrapper: StructValue<'ctx>, + element: BasicValueEnum<'ctx>, + element_layout: InLayout<'a>, +) -> BasicValueEnum<'ctx> { + call_list_bitcode_fn_1( + env, + original_wrapper, + &[ + env.alignment_intvalue(layout_interner, element_layout), + pass_element_as_opaque(env, layout_interner, element, element_layout), + layout_width(env, layout_interner, element_layout), + ], + bitcode::LIST_PREPEND, + ) +} + +/// List.swap : List elem, Nat, Nat -> List elem +pub(crate) fn list_swap<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + original_wrapper: StructValue<'ctx>, + index_1: IntValue<'ctx>, + index_2: IntValue<'ctx>, + element_layout: InLayout<'a>, + update_mode: UpdateMode, +) -> BasicValueEnum<'ctx> { + call_list_bitcode_fn_1( + env, + original_wrapper, + &[ + env.alignment_intvalue(layout_interner, element_layout), + layout_width(env, layout_interner, element_layout), + index_1.into(), + index_2.into(), + pass_update_mode(env, update_mode), + ], + bitcode::LIST_SWAP, + ) +} + +/// List.sublist : List elem, { start : Nat, len : Nat } -> List elem +pub(crate) fn list_sublist<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + layout_ids: &mut LayoutIds<'a>, + original_wrapper: StructValue<'ctx>, + start: IntValue<'ctx>, + len: IntValue<'ctx>, + element_layout: InLayout<'a>, +) -> BasicValueEnum<'ctx> { + let dec_element_fn = build_dec_wrapper(env, layout_interner, layout_ids, element_layout); + call_list_bitcode_fn_1( + env, + original_wrapper, + &[ + env.alignment_intvalue(layout_interner, element_layout), + layout_width(env, layout_interner, element_layout), + start.into(), + len.into(), + dec_element_fn.as_global_value().as_pointer_value().into(), + ], + bitcode::LIST_SUBLIST, + ) +} + +/// List.dropAt : List elem, Nat -> List elem +pub(crate) fn list_drop_at<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + layout_ids: &mut LayoutIds<'a>, + original_wrapper: StructValue<'ctx>, + count: IntValue<'ctx>, + element_layout: InLayout<'a>, +) -> BasicValueEnum<'ctx> { + let dec_element_fn = build_dec_wrapper(env, layout_interner, layout_ids, element_layout); + call_list_bitcode_fn_1( + env, + original_wrapper, + &[ + env.alignment_intvalue(layout_interner, element_layout), + layout_width(env, layout_interner, element_layout), + count.into(), + dec_element_fn.as_global_value().as_pointer_value().into(), + ], + bitcode::LIST_DROP_AT, + ) +} + +/// List.replace_unsafe : List elem, Nat, elem -> { list: List elem, value: elem } +pub(crate) fn list_replace_unsafe<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + _layout_ids: &mut LayoutIds<'a>, + list: BasicValueEnum<'ctx>, + index: IntValue<'ctx>, + element: BasicValueEnum<'ctx>, + element_layout: InLayout<'a>, + update_mode: UpdateMode, +) -> BasicValueEnum<'ctx> { + let element_type = basic_type_from_layout( + env, + layout_interner, + layout_interner.get_repr(element_layout), + ); + let element_ptr = env + .builder + .new_build_alloca(element_type, "output_element_as_opaque"); + + // Assume the bounds have already been checked earlier + // (e.g. by List.replace or List.set, which wrap List.#replaceUnsafe) + let new_list = match update_mode { + UpdateMode::InPlace => call_list_bitcode_fn_1( + env, + list.into_struct_value(), + &[ + index.into(), + pass_element_as_opaque(env, layout_interner, element, element_layout), + layout_width(env, layout_interner, element_layout), + pass_as_opaque(env, element_ptr), + ], + bitcode::LIST_REPLACE_IN_PLACE, + ), + UpdateMode::Immutable => call_list_bitcode_fn_1( + env, + list.into_struct_value(), + &[ + env.alignment_intvalue(layout_interner, element_layout), + index.into(), + pass_element_as_opaque(env, layout_interner, element, element_layout), + layout_width(env, layout_interner, element_layout), + pass_as_opaque(env, element_ptr), + ], + bitcode::LIST_REPLACE, + ), + }; + + // Load the element and returned list into a struct. + let old_element = env + .builder + .new_build_load(element_type, element_ptr, "load_element"); + + // the list has the same alignment as a usize / ptr. The element comes first in the struct if + // its alignment is bigger than that of a list. + let element_align = layout_interner.alignment_bytes(element_layout); + let element_first = element_align > env.target_info.ptr_width() as u32; + + let fields = if element_first { + [element_layout, Layout::LIST_U8 /* any list works */] + } else { + [Layout::LIST_U8 /* any list works */, element_layout] + }; + // TODO: have use_roc_value take LayoutRepr + let result_layout = LayoutRepr::Struct(env.arena.alloc(fields)); + let result_struct_type = + basic_type_from_layout(env, layout_interner, result_layout).into_struct_type(); + + let result = result_struct_type.const_zero(); + + let (list_index, element_index) = if element_first { (1, 0) } else { (0, 1) }; + + let result = env + .builder + .build_insert_value(result, new_list, list_index, "insert_list") + .unwrap(); + + let result = env + .builder + .build_insert_value(result, old_element, element_index, "insert_value") + .unwrap(); + + use_roc_value( + env, + layout_interner, + result_layout, + result.into_struct_value().into(), + "use_replace_result_record", + ) +} + +fn bounds_check_comparison<'ctx>( + builder: &Builder<'ctx>, + elem_index: IntValue<'ctx>, + len: IntValue<'ctx>, +) -> IntValue<'ctx> { + // Note: Check for index < length as the "true" condition, + // to avoid misprediction. (In practice this should usually pass, + // and CPUs generally default to predicting that a forward jump + // shouldn't be taken; that is, they predict "else" won't be taken.) + builder.new_build_int_compare(IntPredicate::ULT, elem_index, len, "bounds_check") +} + +/// List.len : List * -> Nat +pub(crate) fn list_len<'ctx>( + builder: &Builder<'ctx>, + wrapper_struct: StructValue<'ctx>, +) -> IntValue<'ctx> { + builder + .build_extract_value(wrapper_struct, Builtin::WRAPPER_LEN, "list_len") + .unwrap() + .into_int_value() +} + +pub(crate) fn list_capacity_or_ref_ptr<'ctx>( + builder: &Builder<'ctx>, + wrapper_struct: StructValue<'ctx>, +) -> IntValue<'ctx> { + builder + .build_extract_value( + wrapper_struct, + Builtin::WRAPPER_CAPACITY, + "list_capacity_or_ref_ptr", + ) + .unwrap() + .into_int_value() +} + +// Gets a pointer to just after the refcount for a list or seamless slice. +// The value is just after the refcount so that normal lists and seamless slices can share code paths easily. +pub(crate) fn list_refcount_ptr<'ctx>( + env: &Env<'_, 'ctx, '_>, + wrapper_struct: StructValue<'ctx>, +) -> PointerValue<'ctx> { + call_list_bitcode_fn( + env, + &[wrapper_struct], + &[], + BitcodeReturns::Basic, + bitcode::LIST_REFCOUNT_PTR, + ) + .into_pointer_value() +} + +pub(crate) fn destructure<'ctx>( + builder: &Builder<'ctx>, + wrapper_struct: StructValue<'ctx>, +) -> (PointerValue<'ctx>, IntValue<'ctx>, IntValue<'ctx>) { + let length = builder + .build_extract_value(wrapper_struct, Builtin::WRAPPER_LEN, "list_len") + .unwrap() + .into_int_value(); + + let capacity = builder + .build_extract_value(wrapper_struct, Builtin::WRAPPER_CAPACITY, "list_cap") + .unwrap() + .into_int_value(); + + // a `*mut u8` pointer + let generic_ptr = builder + .build_extract_value(wrapper_struct, Builtin::WRAPPER_PTR, "read_list_ptr") + .unwrap() + .into_pointer_value(); + + (generic_ptr, length, capacity) +} + +/// List.sortWith : List a, (a, a -> Ordering) -> List a +pub(crate) fn list_sort_with<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + roc_function_call: RocFunctionCall<'ctx>, + compare_wrapper: PointerValue<'ctx>, + list: BasicValueEnum<'ctx>, + element_layout: InLayout<'a>, +) -> BasicValueEnum<'ctx> { + call_list_bitcode_fn_1( + env, + list.into_struct_value(), + &[ + compare_wrapper.into(), + pass_as_opaque(env, roc_function_call.data), + roc_function_call.inc_n_data.into(), + roc_function_call.data_is_owned.into(), + env.alignment_intvalue(layout_interner, element_layout), + layout_width(env, layout_interner, element_layout), + ], + bitcode::LIST_SORT_WITH, + ) +} + +/// List.map : List before, (before -> after) -> List after +pub(crate) fn list_map<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + roc_function_call: RocFunctionCall<'ctx>, + list: BasicValueEnum<'ctx>, + element_layout: InLayout<'a>, + return_layout: InLayout<'a>, +) -> BasicValueEnum<'ctx> { + call_list_bitcode_fn_1( + env, + list.into_struct_value(), + &[ + roc_function_call.caller.into(), + pass_as_opaque(env, roc_function_call.data), + roc_function_call.inc_n_data.into(), + roc_function_call.data_is_owned.into(), + env.alignment_intvalue(layout_interner, return_layout), + layout_width(env, layout_interner, element_layout), + layout_width(env, layout_interner, return_layout), + ], + bitcode::LIST_MAP, + ) +} + +pub(crate) fn list_map2<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + layout_ids: &mut LayoutIds<'a>, + roc_function_call: RocFunctionCall<'ctx>, + list1: BasicValueEnum<'ctx>, + list2: BasicValueEnum<'ctx>, + element1_layout: InLayout<'a>, + element2_layout: InLayout<'a>, + return_layout: InLayout<'a>, +) -> BasicValueEnum<'ctx> { + let dec_a = build_dec_wrapper(env, layout_interner, layout_ids, element1_layout); + let dec_b = build_dec_wrapper(env, layout_interner, layout_ids, element2_layout); + + call_list_bitcode_fn( + env, + &[list1.into_struct_value(), list2.into_struct_value()], + &[ + roc_function_call.caller.into(), + pass_as_opaque(env, roc_function_call.data), + roc_function_call.inc_n_data.into(), + roc_function_call.data_is_owned.into(), + env.alignment_intvalue(layout_interner, return_layout), + layout_width(env, layout_interner, element1_layout), + layout_width(env, layout_interner, element2_layout), + layout_width(env, layout_interner, return_layout), + dec_a.as_global_value().as_pointer_value().into(), + dec_b.as_global_value().as_pointer_value().into(), + ], + BitcodeReturns::List, + bitcode::LIST_MAP2, + ) +} + +pub(crate) fn list_map3<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + layout_ids: &mut LayoutIds<'a>, + roc_function_call: RocFunctionCall<'ctx>, + list1: BasicValueEnum<'ctx>, + list2: BasicValueEnum<'ctx>, + list3: BasicValueEnum<'ctx>, + element1_layout: InLayout<'a>, + element2_layout: InLayout<'a>, + element3_layout: InLayout<'a>, + result_layout: InLayout<'a>, +) -> BasicValueEnum<'ctx> { + let dec_a = build_dec_wrapper(env, layout_interner, layout_ids, element1_layout); + let dec_b = build_dec_wrapper(env, layout_interner, layout_ids, element2_layout); + let dec_c = build_dec_wrapper(env, layout_interner, layout_ids, element3_layout); + + call_list_bitcode_fn( + env, + &[ + list1.into_struct_value(), + list2.into_struct_value(), + list3.into_struct_value(), + ], + &[ + roc_function_call.caller.into(), + pass_as_opaque(env, roc_function_call.data), + roc_function_call.inc_n_data.into(), + roc_function_call.data_is_owned.into(), + env.alignment_intvalue(layout_interner, result_layout), + layout_width(env, layout_interner, element1_layout), + layout_width(env, layout_interner, element2_layout), + layout_width(env, layout_interner, element3_layout), + layout_width(env, layout_interner, result_layout), + dec_a.as_global_value().as_pointer_value().into(), + dec_b.as_global_value().as_pointer_value().into(), + dec_c.as_global_value().as_pointer_value().into(), + ], + BitcodeReturns::List, + bitcode::LIST_MAP3, + ) +} + +pub(crate) fn list_map4<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + layout_ids: &mut LayoutIds<'a>, + roc_function_call: RocFunctionCall<'ctx>, + list1: BasicValueEnum<'ctx>, + list2: BasicValueEnum<'ctx>, + list3: BasicValueEnum<'ctx>, + list4: BasicValueEnum<'ctx>, + element1_layout: InLayout<'a>, + element2_layout: InLayout<'a>, + element3_layout: InLayout<'a>, + element4_layout: InLayout<'a>, + result_layout: InLayout<'a>, +) -> BasicValueEnum<'ctx> { + let dec_a = build_dec_wrapper(env, layout_interner, layout_ids, element1_layout); + let dec_b = build_dec_wrapper(env, layout_interner, layout_ids, element2_layout); + let dec_c = build_dec_wrapper(env, layout_interner, layout_ids, element3_layout); + let dec_d = build_dec_wrapper(env, layout_interner, layout_ids, element4_layout); + + call_list_bitcode_fn( + env, + &[ + list1.into_struct_value(), + list2.into_struct_value(), + list3.into_struct_value(), + list4.into_struct_value(), + ], + &[ + roc_function_call.caller.into(), + pass_as_opaque(env, roc_function_call.data), + roc_function_call.inc_n_data.into(), + roc_function_call.data_is_owned.into(), + env.alignment_intvalue(layout_interner, result_layout), + layout_width(env, layout_interner, element1_layout), + layout_width(env, layout_interner, element2_layout), + layout_width(env, layout_interner, element3_layout), + layout_width(env, layout_interner, element4_layout), + layout_width(env, layout_interner, result_layout), + dec_a.as_global_value().as_pointer_value().into(), + dec_b.as_global_value().as_pointer_value().into(), + dec_c.as_global_value().as_pointer_value().into(), + dec_d.as_global_value().as_pointer_value().into(), + ], + BitcodeReturns::List, + bitcode::LIST_MAP4, + ) +} + +/// List.concat : List elem, List elem -> List elem +pub(crate) fn list_concat<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + list1: BasicValueEnum<'ctx>, + list2: BasicValueEnum<'ctx>, + element_layout: InLayout<'a>, +) -> BasicValueEnum<'ctx> { + call_list_bitcode_fn( + env, + &[list1.into_struct_value(), list2.into_struct_value()], + &[ + env.alignment_intvalue(layout_interner, element_layout), + layout_width(env, layout_interner, element_layout), + ], + BitcodeReturns::List, + bitcode::LIST_CONCAT, + ) +} + +pub(crate) fn incrementing_elem_loop<'a, 'r, 'ctx, 'env, LoopFn>( + env: &Env<'a, 'ctx, 'env>, + layout_interner: &'r STLayoutInterner<'a>, + parent: FunctionValue<'ctx>, + element_layout: InLayout<'a>, + ptr: PointerValue<'ctx>, + len: IntValue<'ctx>, + index_name: &str, + mut loop_fn: LoopFn, +) -> PointerValue<'ctx> +where + LoopFn: FnMut(&'r STLayoutInterner<'a>, IntValue<'ctx>, BasicValueEnum<'ctx>), +{ + let builder = env.builder; + + let element_type = basic_type_from_layout( + env, + layout_interner, + layout_interner.get_repr(element_layout), + ); + + incrementing_index_loop( + env, + layout_interner, + parent, + len, + index_name, + |layout_interner, index| { + // The pointer to the element in the list + let element_ptr = unsafe { + builder.new_build_in_bounds_gep(element_type, ptr, &[index], "load_index") + }; + + let elem = load_roc_value( + env, + layout_interner, + layout_interner.get_repr(element_layout), + element_ptr, + "incrementing_element_loop_load", + ); + + loop_fn(layout_interner, index, elem); + }, + ) +} + +// This helper simulates a basic for loop, where +// and index increments up from 0 to some end value +pub(crate) fn incrementing_index_loop<'a, 'r, 'ctx, 'env, LoopFn>( + env: &Env<'a, 'ctx, 'env>, + layout_interner: &'r STLayoutInterner<'a>, + parent: FunctionValue<'ctx>, + end: IntValue<'ctx>, + index_name: &str, + mut loop_fn: LoopFn, +) -> PointerValue<'ctx> +where + LoopFn: FnMut(&'r STLayoutInterner<'a>, IntValue<'ctx>), +{ + let ctx = env.context; + let builder = env.builder; + + // constant 1usize + let one = env.ptr_int().const_int(1, false); + let zero = env.ptr_int().const_zero(); + + // allocate a stack slot for the current index + let index_alloca = builder.new_build_alloca(env.ptr_int(), index_name); + builder.new_build_store(index_alloca, zero); + + let loop_bb = ctx.append_basic_block(parent, "loop"); + let after_loop_bb = ctx.append_basic_block(parent, "after_loop"); + + let loop_end_cond = bounds_check_comparison(builder, zero, end); + builder.new_build_conditional_branch(loop_end_cond, loop_bb, after_loop_bb); + + { + builder.position_at_end(loop_bb); + + let current_index = builder + .new_build_load(env.ptr_int(), index_alloca, "index") + .into_int_value(); + let next_index = builder.new_build_int_add(current_index, one, "next_index"); + builder.new_build_store(index_alloca, next_index); + + // The body of the loop + loop_fn(layout_interner, current_index); + + // #index < end + let loop_end_cond = bounds_check_comparison(builder, next_index, end); + + builder.new_build_conditional_branch(loop_end_cond, loop_bb, after_loop_bb); + } + + builder.position_at_end(after_loop_bb); + + index_alloca +} + +pub(crate) fn empty_polymorphic_list<'ctx>(env: &Env<'_, 'ctx, '_>) -> BasicValueEnum<'ctx> { + let struct_type = zig_list_type(env); + + // The pointer should be null (aka zero) and the length should be zero, + // so the whole struct should be a const_zero + BasicValueEnum::StructValue(struct_type.const_zero()) +} + +pub(crate) fn load_list<'ctx>( + builder: &Builder<'ctx>, + wrapper_struct: StructValue<'ctx>, + ptr_type: PointerType<'ctx>, +) -> (IntValue<'ctx>, PointerValue<'ctx>) { + let ptr = load_list_ptr(builder, wrapper_struct, ptr_type); + + let length = builder + .build_extract_value(wrapper_struct, Builtin::WRAPPER_LEN, "list_len") + .unwrap() + .into_int_value(); + + (length, ptr) +} + +pub(crate) fn load_list_ptr<'ctx>( + builder: &Builder<'ctx>, + wrapper_struct: StructValue<'ctx>, + ptr_type: PointerType<'ctx>, +) -> PointerValue<'ctx> { + // a `*mut u8` pointer + let generic_ptr = builder + .build_extract_value(wrapper_struct, Builtin::WRAPPER_PTR, "read_list_ptr") + .unwrap() + .into_pointer_value(); + + // cast to the expected pointer type + cast_basic_basic(builder, generic_ptr.into(), ptr_type.into()).into_pointer_value() +} + +pub(crate) fn allocate_list<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + elem_layout: InLayout<'a>, + number_of_elements: IntValue<'ctx>, +) -> PointerValue<'ctx> { + let builder = env.builder; + + let len_type = env.ptr_int(); + let elem_bytes = layout_interner.stack_size(elem_layout) as u64; + let bytes_per_element = len_type.const_int(elem_bytes, false); + let number_of_data_bytes = + builder.new_build_int_mul(bytes_per_element, number_of_elements, "data_length"); + + let basic_type = + basic_type_from_layout(env, layout_interner, layout_interner.get_repr(elem_layout)); + let alignment_bytes = layout_interner.alignment_bytes(elem_layout); + allocate_with_refcount_help(env, basic_type, alignment_bytes, number_of_data_bytes) +} + +pub(crate) fn store_list<'ctx>( + env: &Env<'_, 'ctx, '_>, + pointer_to_first_element: PointerValue<'ctx>, + len: IntValue<'ctx>, +) -> StructValue<'ctx> { + let ptr = pass_as_opaque(env, pointer_to_first_element); + let cap = len; + + struct_from_fields( + env, + zig_list_type(env), + [ + (Builtin::WRAPPER_PTR as usize, ptr), + (Builtin::WRAPPER_LEN as usize, len.into()), + (Builtin::WRAPPER_CAPACITY as usize, cap.into()), + ] + .into_iter(), + ) +} + +pub(crate) fn decref<'ctx>( + env: &Env<'_, 'ctx, '_>, + wrapper_struct: StructValue<'ctx>, + alignment: u32, +) { + let refcount_ptr = list_refcount_ptr(env, wrapper_struct); + + crate::llvm::refcounting::decref_pointer_check_null(env, refcount_ptr, alignment); +} diff --git a/crates/compiler/gen_llvm/src/llvm/build_str.rs b/crates/compiler/gen_llvm/src/llvm/build_str.rs new file mode 100644 index 0000000000..97924c7ce5 --- /dev/null +++ b/crates/compiler/gen_llvm/src/llvm/build_str.rs @@ -0,0 +1,63 @@ +use crate::llvm::build::Env; +use inkwell::values::{BasicValueEnum, PointerValue}; +use roc_builtins::bitcode; +use roc_mono::layout::{InLayout, Layout, LayoutRepr, STLayoutInterner}; + +use super::bitcode::{call_str_bitcode_fn, BitcodeReturns}; +use super::build::load_roc_value; + +pub static CHAR_LAYOUT: InLayout = Layout::U8; + +pub(crate) fn decode_from_utf8_result<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + pointer: PointerValue<'ctx>, +) -> BasicValueEnum<'ctx> { + let layout = LayoutRepr::Struct(env.arena.alloc([ + Layout::usize(env.target_info), + Layout::STR, + Layout::BOOL, + Layout::U8, + ])); + + load_roc_value( + env, + layout_interner, + layout, + pointer, + "load_decode_from_utf8_result", + ) +} + +/// Dec.toStr : Dec -> Str + +/// Str.equal : Str, Str -> Bool +pub(crate) fn str_equal<'ctx>( + env: &Env<'_, 'ctx, '_>, + value1: BasicValueEnum<'ctx>, + value2: BasicValueEnum<'ctx>, +) -> BasicValueEnum<'ctx> { + call_str_bitcode_fn( + env, + &[value1, value2], + &[], + BitcodeReturns::Basic, + bitcode::STR_EQUAL, + ) +} + +// Gets a pointer to just after the refcount for a list or seamless slice. +// The value is just after the refcount so that normal lists and seamless slices can share code paths easily. +pub(crate) fn str_refcount_ptr<'ctx>( + env: &Env<'_, 'ctx, '_>, + value: BasicValueEnum<'ctx>, +) -> PointerValue<'ctx> { + call_str_bitcode_fn( + env, + &[value], + &[], + BitcodeReturns::Basic, + bitcode::STR_REFCOUNT_PTR, + ) + .into_pointer_value() +} diff --git a/crates/compiler/gen_llvm/src/llvm/compare.rs b/crates/compiler/gen_llvm/src/llvm/compare.rs new file mode 100644 index 0000000000..de9ac95073 --- /dev/null +++ b/crates/compiler/gen_llvm/src/llvm/compare.rs @@ -0,0 +1,1510 @@ +use crate::llvm::build::{get_tag_id, tag_pointer_clear_tag_id, Env, FAST_CALL_CONV}; +use crate::llvm::build_list::{list_len, load_list_ptr}; +use crate::llvm::build_str::str_equal; +use crate::llvm::convert::basic_type_from_layout; +use bumpalo::collections::Vec; +use inkwell::types::BasicType; +use inkwell::values::{BasicValueEnum, FunctionValue, IntValue, PointerValue, StructValue}; +use inkwell::{AddressSpace, FloatPredicate, IntPredicate}; +use roc_builtins::bitcode; +use roc_builtins::bitcode::{FloatWidth, IntWidth}; +use roc_error_macros::internal_error; +use roc_module::symbol::Symbol; +use roc_mono::layout::{ + Builtin, InLayout, LayoutIds, LayoutInterner, LayoutRepr, STLayoutInterner, UnionLayout, +}; + +use super::build::{load_roc_value, BuilderExt}; +use super::convert::{argument_type_from_layout, argument_type_from_union_layout}; +use super::lowlevel::dec_binop_with_unchecked; +use super::struct_; + +pub fn generic_eq<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + layout_ids: &mut LayoutIds<'a>, + lhs_val: BasicValueEnum<'ctx>, + rhs_val: BasicValueEnum<'ctx>, + lhs_layout: InLayout<'a>, + rhs_layout: InLayout<'a>, +) -> BasicValueEnum<'ctx> { + build_eq( + env, + layout_interner, + layout_ids, + lhs_val, + rhs_val, + layout_interner.get_repr(lhs_layout), + layout_interner.get_repr(rhs_layout), + ) +} + +pub fn generic_neq<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + layout_ids: &mut LayoutIds<'a>, + lhs_val: BasicValueEnum<'ctx>, + rhs_val: BasicValueEnum<'ctx>, + lhs_layout: InLayout<'a>, + rhs_layout: InLayout<'a>, +) -> BasicValueEnum<'ctx> { + build_neq( + env, + layout_interner, + layout_ids, + lhs_val, + rhs_val, + lhs_layout, + rhs_layout, + ) +} + +fn build_eq_builtin<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + layout_ids: &mut LayoutIds<'a>, + lhs_val: BasicValueEnum<'ctx>, + rhs_val: BasicValueEnum<'ctx>, + builtin_layout: LayoutRepr<'a>, + builtin: &Builtin<'a>, +) -> BasicValueEnum<'ctx> { + let int_cmp = |pred, label| { + let int_val = env.builder.new_build_int_compare( + pred, + lhs_val.into_int_value(), + rhs_val.into_int_value(), + label, + ); + + BasicValueEnum::IntValue(int_val) + }; + + let float_cmp = |pred, label| { + let int_val = env.builder.new_build_float_compare( + pred, + lhs_val.into_float_value(), + rhs_val.into_float_value(), + label, + ); + + BasicValueEnum::IntValue(int_val) + }; + + match builtin { + Builtin::Int(int_width) => { + use IntWidth::*; + + let name = match int_width { + I128 => "eq_i128", + I64 => "eq_i64", + I32 => "eq_i32", + I16 => "eq_i16", + I8 => "eq_i8", + U128 => "eq_u128", + U64 => "eq_u64", + U32 => "eq_u32", + U16 => "eq_u16", + U8 => "eq_u8", + }; + + int_cmp(IntPredicate::EQ, name) + } + + Builtin::Float(float_width) => { + use FloatWidth::*; + + let name = match float_width { + F64 => "eq_f64", + F32 => "eq_f32", + }; + + float_cmp(FloatPredicate::OEQ, name) + } + + Builtin::Bool => int_cmp(IntPredicate::EQ, "eq_i1"), + Builtin::Decimal => dec_binop_with_unchecked(env, bitcode::DEC_EQ, lhs_val, rhs_val), + + Builtin::Str => str_equal(env, lhs_val, rhs_val), + Builtin::List(elem) => build_list_eq( + env, + layout_interner, + layout_ids, + builtin_layout, + layout_interner.get_repr(*elem), + lhs_val.into_struct_value(), + rhs_val.into_struct_value(), + ), + } +} + +fn build_eq<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + layout_ids: &mut LayoutIds<'a>, + lhs_val: BasicValueEnum<'ctx>, + rhs_val: BasicValueEnum<'ctx>, + lhs_layout: LayoutRepr<'a>, + _rhs_layout: LayoutRepr<'a>, +) -> BasicValueEnum<'ctx> { + match lhs_layout { + LayoutRepr::Builtin(builtin) => build_eq_builtin( + env, + layout_interner, + layout_ids, + lhs_val, + rhs_val, + lhs_layout, + &builtin, + ), + + LayoutRepr::Struct(field_layouts) => build_struct_eq( + env, + layout_interner, + layout_ids, + lhs_layout, + field_layouts, + lhs_val, + rhs_val, + ), + + LayoutRepr::LambdaSet(_) => unreachable!("cannot compare closures"), + LayoutRepr::FunctionPointer(_) => unreachable!("cannot compare function pointers"), + LayoutRepr::Erased(_) => unreachable!("cannot compare erased types"), + + LayoutRepr::Union(union_layout) => build_tag_eq( + env, + layout_interner, + layout_ids, + lhs_layout, + &union_layout, + lhs_val, + rhs_val, + ), + + LayoutRepr::Ptr(inner_layout) => build_box_eq( + env, + layout_interner, + layout_ids, + lhs_layout, + inner_layout, + lhs_val, + rhs_val, + ), + + LayoutRepr::RecursivePointer(rec_layout) => { + let layout = rec_layout; + + let bt = basic_type_from_layout(env, layout_interner, layout_interner.get_repr(layout)); + + // cast the i64 pointer to a pointer to block of memory + let field1_cast = env.builder.new_build_pointer_cast( + lhs_val.into_pointer_value(), + bt.into_pointer_type(), + "i64_to_opaque", + ); + + let field2_cast = env.builder.new_build_pointer_cast( + rhs_val.into_pointer_value(), + bt.into_pointer_type(), + "i64_to_opaque", + ); + + let union_layout = match layout_interner.get_repr(rec_layout) { + LayoutRepr::Union(union_layout) => { + debug_assert!(!matches!(union_layout, UnionLayout::NonRecursive(..))); + union_layout + } + _ => internal_error!(), + }; + + build_tag_eq( + env, + layout_interner, + layout_ids, + layout_interner.get_repr(rec_layout), + &union_layout, + field1_cast.into(), + field2_cast.into(), + ) + } + } +} + +fn build_neq_builtin<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + layout_ids: &mut LayoutIds<'a>, + lhs_val: BasicValueEnum<'ctx>, + rhs_val: BasicValueEnum<'ctx>, + builtin_layout: InLayout<'a>, + builtin: &Builtin<'a>, +) -> BasicValueEnum<'ctx> { + let int_cmp = |pred, label| { + let int_val = env.builder.new_build_int_compare( + pred, + lhs_val.into_int_value(), + rhs_val.into_int_value(), + label, + ); + + BasicValueEnum::IntValue(int_val) + }; + + let float_cmp = |pred, label| { + let int_val = env.builder.new_build_float_compare( + pred, + lhs_val.into_float_value(), + rhs_val.into_float_value(), + label, + ); + + BasicValueEnum::IntValue(int_val) + }; + + match builtin { + Builtin::Int(int_width) => { + use IntWidth::*; + + let name = match int_width { + I128 => "neq_i128", + I64 => "neq_i64", + I32 => "neq_i32", + I16 => "neq_i16", + I8 => "neq_i8", + U128 => "neq_u128", + U64 => "neq_u64", + U32 => "neq_u32", + U16 => "neq_u16", + U8 => "neq_u8", + }; + + int_cmp(IntPredicate::NE, name) + } + + Builtin::Float(float_width) => { + use FloatWidth::*; + + let name = match float_width { + F64 => "neq_f64", + F32 => "neq_f32", + }; + + float_cmp(FloatPredicate::ONE, name) + } + + Builtin::Bool => int_cmp(IntPredicate::NE, "neq_i1"), + Builtin::Decimal => dec_binop_with_unchecked(env, bitcode::DEC_NEQ, lhs_val, rhs_val), + + Builtin::Str => { + let is_equal = str_equal(env, lhs_val, rhs_val).into_int_value(); + let result: IntValue = env.builder.new_build_not(is_equal, "negate"); + + result.into() + } + Builtin::List(elem) => { + let is_equal = build_list_eq( + env, + layout_interner, + layout_ids, + layout_interner.get_repr(builtin_layout), + layout_interner.get_repr(*elem), + lhs_val.into_struct_value(), + rhs_val.into_struct_value(), + ) + .into_int_value(); + + let result: IntValue = env.builder.new_build_not(is_equal, "negate"); + + result.into() + } + } +} + +fn build_neq<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + layout_ids: &mut LayoutIds<'a>, + lhs_val: BasicValueEnum<'ctx>, + rhs_val: BasicValueEnum<'ctx>, + lhs_layout: InLayout<'a>, + rhs_layout: InLayout<'a>, +) -> BasicValueEnum<'ctx> { + if lhs_layout != rhs_layout { + panic!( + "Inequality of different layouts; did you have a type mismatch?\n{lhs_layout:?} != {rhs_layout:?}" + ); + } + + match layout_interner.get_repr(lhs_layout) { + LayoutRepr::Builtin(builtin) => build_neq_builtin( + env, + layout_interner, + layout_ids, + lhs_val, + rhs_val, + lhs_layout, + &builtin, + ), + + LayoutRepr::Struct(field_layouts) => { + let is_equal = build_struct_eq( + env, + layout_interner, + layout_ids, + layout_interner.get_repr(lhs_layout), + field_layouts, + lhs_val, + rhs_val, + ) + .into_int_value(); + + let result: IntValue = env.builder.new_build_not(is_equal, "negate"); + + result.into() + } + + LayoutRepr::Union(union_layout) => { + let is_equal = build_tag_eq( + env, + layout_interner, + layout_ids, + layout_interner.get_repr(lhs_layout), + &union_layout, + lhs_val, + rhs_val, + ) + .into_int_value(); + + let result: IntValue = env.builder.new_build_not(is_equal, "negate"); + + result.into() + } + + LayoutRepr::Ptr(inner_layout) => { + let is_equal = build_box_eq( + env, + layout_interner, + layout_ids, + layout_interner.get_repr(lhs_layout), + inner_layout, + lhs_val, + rhs_val, + ) + .into_int_value(); + + let result: IntValue = env.builder.new_build_not(is_equal, "negate"); + + result.into() + } + + LayoutRepr::RecursivePointer(_) => { + unreachable!("recursion pointers should never be compared directly") + } + LayoutRepr::LambdaSet(_) => unreachable!("cannot compare closure"), + LayoutRepr::FunctionPointer(_) => unreachable!("cannot compare function pointers"), + LayoutRepr::Erased(_) => unreachable!("cannot compare erased types"), + } +} + +fn build_list_eq<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + layout_ids: &mut LayoutIds<'a>, + list_layout: LayoutRepr<'a>, + element_layout: LayoutRepr<'a>, + list1: StructValue<'ctx>, + list2: StructValue<'ctx>, +) -> BasicValueEnum<'ctx> { + let block = env.builder.get_insert_block().expect("to be in a function"); + let di_location = env.builder.get_current_debug_location().unwrap(); + + let symbol = Symbol::LIST_EQ; + let element_layout = if let LayoutRepr::RecursivePointer(rec) = element_layout { + layout_interner.get_repr(rec) + } else { + element_layout + }; + let fn_name = layout_ids + .get(symbol, &element_layout) + .to_symbol_string(symbol, &env.interns); + + let function = match env.module.get_function(fn_name.as_str()) { + Some(function_value) => function_value, + None => { + let arg_type = basic_type_from_layout(env, layout_interner, list_layout); + + let function_value = crate::llvm::refcounting::build_header_help( + env, + &fn_name, + env.context.bool_type().into(), + &[arg_type, arg_type], + ); + + build_list_eq_help( + env, + layout_interner, + layout_ids, + function_value, + element_layout, + ); + + function_value + } + }; + + env.builder.position_at_end(block); + env.builder.set_current_debug_location(di_location); + let call = env + .builder + .new_build_call(function, &[list1.into(), list2.into()], "list_eq"); + + call.set_call_convention(FAST_CALL_CONV); + + call.try_as_basic_value().left().unwrap() +} + +fn build_list_eq_help<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + layout_ids: &mut LayoutIds<'a>, + parent: FunctionValue<'ctx>, + element_layout: LayoutRepr<'a>, +) { + let ctx = env.context; + let builder = env.builder; + + { + use inkwell::debug_info::AsDIScope; + + let func_scope = parent.get_subprogram().unwrap(); + let lexical_block = env.dibuilder.create_lexical_block( + /* scope */ func_scope.as_debug_info_scope(), + /* file */ env.compile_unit.get_file(), + /* line_no */ 0, + /* column_no */ 0, + ); + + let loc = env.dibuilder.create_debug_location( + ctx, + /* line */ 0, + /* column */ 0, + /* current_scope */ lexical_block.as_debug_info_scope(), + /* inlined_at */ None, + ); + builder.set_current_debug_location(loc); + } + + // Add args to scope + let mut it = parent.get_param_iter(); + let list1 = it.next().unwrap().into_struct_value(); + let list2 = it.next().unwrap().into_struct_value(); + + list1.set_name(Symbol::ARG_1.as_str(&env.interns)); + list2.set_name(Symbol::ARG_2.as_str(&env.interns)); + + let entry = ctx.append_basic_block(parent, "entry"); + env.builder.position_at_end(entry); + + let return_true = ctx.append_basic_block(parent, "return_true"); + let return_false = ctx.append_basic_block(parent, "return_false"); + + // first, check whether the length is equal + + let len1 = list_len(env.builder, list1); + let len2 = list_len(env.builder, list2); + + let length_equal: IntValue = + env.builder + .new_build_int_compare(IntPredicate::EQ, len1, len2, "bounds_check"); + + let then_block = ctx.append_basic_block(parent, "then"); + + env.builder + .new_build_conditional_branch(length_equal, then_block, return_false); + + { + // the length is equal; check elements pointwise + env.builder.position_at_end(then_block); + + let builder = env.builder; + let element_type = basic_type_from_layout(env, layout_interner, element_layout); + let ptr_type = element_type.ptr_type(AddressSpace::default()); + let ptr1 = load_list_ptr(env.builder, list1, ptr_type); + let ptr2 = load_list_ptr(env.builder, list2, ptr_type); + + // we know that len1 == len2 + let end = len1; + + // allocate a stack slot for the current index + let index_alloca = builder.new_build_alloca(env.ptr_int(), "index"); + builder.new_build_store(index_alloca, env.ptr_int().const_zero()); + + let loop_bb = ctx.append_basic_block(parent, "loop"); + let body_bb = ctx.append_basic_block(parent, "body"); + let increment_bb = ctx.append_basic_block(parent, "increment"); + + // the "top" of the loop + builder.new_build_unconditional_branch(loop_bb); + builder.position_at_end(loop_bb); + + let curr_index = builder + .new_build_load(env.ptr_int(), index_alloca, "index") + .into_int_value(); + + // #index < end + let loop_end_cond = + builder.new_build_int_compare(IntPredicate::ULT, curr_index, end, "bounds_check"); + + // if we're at the end, and all elements were equal so far, return true + // otherwise check the current elements for equality + builder.new_build_conditional_branch(loop_end_cond, body_bb, return_true); + + { + // loop body + builder.position_at_end(body_bb); + + let elem1 = { + let elem_ptr = unsafe { + builder.new_build_in_bounds_gep(element_type, ptr1, &[curr_index], "load_index") + }; + load_roc_value(env, layout_interner, element_layout, elem_ptr, "get_elem") + }; + + let elem2 = { + let elem_ptr = unsafe { + builder.new_build_in_bounds_gep(element_type, ptr2, &[curr_index], "load_index") + }; + load_roc_value(env, layout_interner, element_layout, elem_ptr, "get_elem") + }; + + let are_equal = build_eq( + env, + layout_interner, + layout_ids, + elem1, + elem2, + element_layout, + element_layout, + ) + .into_int_value(); + + // if the elements are equal, increment the index and check the next element + // otherwise, return false + builder.new_build_conditional_branch(are_equal, increment_bb, return_false); + } + + { + env.builder.position_at_end(increment_bb); + + // constant 1isize + let one = env.ptr_int().const_int(1, false); + + let next_index = builder.new_build_int_add(curr_index, one, "nextindex"); + + builder.new_build_store(index_alloca, next_index); + + // jump back to the top of the loop + builder.new_build_unconditional_branch(loop_bb); + } + } + + { + env.builder.position_at_end(return_true); + env.builder + .new_build_return(Some(&env.context.bool_type().const_int(1, false))); + } + + { + env.builder.position_at_end(return_false); + env.builder + .new_build_return(Some(&env.context.bool_type().const_int(0, false))); + } +} + +fn build_struct_eq<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + layout_ids: &mut LayoutIds<'a>, + struct_layout: LayoutRepr<'a>, + field_layouts: &'a [InLayout<'a>], + struct1: BasicValueEnum<'ctx>, + struct2: BasicValueEnum<'ctx>, +) -> BasicValueEnum<'ctx> { + let block = env.builder.get_insert_block().expect("to be in a function"); + let di_location = env.builder.get_current_debug_location().unwrap(); + + let symbol = Symbol::GENERIC_EQ; + let fn_name = layout_ids + .get(symbol, &struct_layout) + .to_symbol_string(symbol, &env.interns); + + let function = match env.module.get_function(fn_name.as_str()) { + Some(function_value) => function_value, + None => { + let arg_type = argument_type_from_layout(env, layout_interner, struct_layout); + + let function_value = crate::llvm::refcounting::build_header_help( + env, + &fn_name, + env.context.bool_type().into(), + &[arg_type, arg_type], + ); + + build_struct_eq_help( + env, + layout_interner, + layout_ids, + function_value, + struct_layout, + field_layouts, + ); + + function_value + } + }; + + env.builder.position_at_end(block); + env.builder.set_current_debug_location(di_location); + let call = env + .builder + .new_build_call(function, &[struct1.into(), struct2.into()], "struct_eq"); + + call.set_call_convention(FAST_CALL_CONV); + + call.try_as_basic_value().left().unwrap() +} + +fn build_struct_eq_help<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + layout_ids: &mut LayoutIds<'a>, + parent: FunctionValue<'ctx>, + struct_layout: LayoutRepr<'a>, + field_layouts: &[InLayout<'a>], +) { + let ctx = env.context; + let builder = env.builder; + + { + use inkwell::debug_info::AsDIScope; + + let func_scope = parent.get_subprogram().unwrap(); + let lexical_block = env.dibuilder.create_lexical_block( + /* scope */ func_scope.as_debug_info_scope(), + /* file */ env.compile_unit.get_file(), + /* line_no */ 0, + /* column_no */ 0, + ); + + let loc = env.dibuilder.create_debug_location( + ctx, + /* line */ 0, + /* column */ 0, + /* current_scope */ lexical_block.as_debug_info_scope(), + /* inlined_at */ None, + ); + builder.set_current_debug_location(loc); + } + + // Add args to scope + let mut it = parent.get_param_iter(); + let struct1 = it.next().unwrap(); + let struct2 = it.next().unwrap(); + + struct1.set_name(Symbol::ARG_1.as_str(&env.interns)); + struct2.set_name(Symbol::ARG_2.as_str(&env.interns)); + + let entry = ctx.append_basic_block(parent, "entry"); + let start = ctx.append_basic_block(parent, "start"); + env.builder.position_at_end(entry); + env.builder.new_build_unconditional_branch(start); + + let return_true = ctx.append_basic_block(parent, "return_true"); + let return_false = ctx.append_basic_block(parent, "return_false"); + + let mut current = start; + + for (index, field_layout) in field_layouts.iter().enumerate() { + env.builder.position_at_end(current); + + let field1 = struct_::RocStruct::from(struct1).load_at_index( + env, + layout_interner, + struct_layout, + index as _, + ); + + let field2 = struct_::RocStruct::from(struct2).load_at_index( + env, + layout_interner, + struct_layout, + index as _, + ); + + let are_equal = if let LayoutRepr::RecursivePointer(rec_layout) = + layout_interner.get_repr(*field_layout) + { + debug_assert!( + matches!(layout_interner.get_repr(rec_layout), LayoutRepr::Union(union_layout) if !matches!(union_layout, UnionLayout::NonRecursive(..))) + ); + + let field_layout = rec_layout; + + let bt = basic_type_from_layout( + env, + layout_interner, + layout_interner.get_repr(field_layout), + ); + + // cast the i64 pointer to a pointer to block of memory + let field1_cast = env.builder.new_build_pointer_cast( + field1.into_pointer_value(), + bt.into_pointer_type(), + "i64_to_opaque", + ); + + let field2_cast = env.builder.new_build_pointer_cast( + field2.into_pointer_value(), + bt.into_pointer_type(), + "i64_to_opaque", + ); + + build_eq( + env, + layout_interner, + layout_ids, + field1_cast.into(), + field2_cast.into(), + layout_interner.get_repr(field_layout), + layout_interner.get_repr(field_layout), + ) + .into_int_value() + } else { + build_eq( + env, + layout_interner, + layout_ids, + field1, + field2, + layout_interner.get_repr(*field_layout), + layout_interner.get_repr(*field_layout), + ) + .into_int_value() + }; + + current = ctx.append_basic_block(parent, &format!("eq_step_{index}")); + + env.builder + .new_build_conditional_branch(are_equal, current, return_false); + } + + env.builder.position_at_end(current); + env.builder.new_build_unconditional_branch(return_true); + + { + env.builder.position_at_end(return_true); + env.builder + .new_build_return(Some(&env.context.bool_type().const_int(1, false))); + } + + { + env.builder.position_at_end(return_false); + env.builder + .new_build_return(Some(&env.context.bool_type().const_int(0, false))); + } +} + +fn build_tag_eq<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + layout_ids: &mut LayoutIds<'a>, + tag_layout: LayoutRepr<'a>, + union_layout: &UnionLayout<'a>, + tag1: BasicValueEnum<'ctx>, + tag2: BasicValueEnum<'ctx>, +) -> BasicValueEnum<'ctx> { + let block = env.builder.get_insert_block().expect("to be in a function"); + let di_location = env.builder.get_current_debug_location().unwrap(); + + let symbol = Symbol::GENERIC_EQ; + let fn_name = layout_ids + .get(symbol, &tag_layout) + .to_symbol_string(symbol, &env.interns); + + let function = match env.module.get_function(fn_name.as_str()) { + Some(function_value) => function_value, + None => { + let arg_type = argument_type_from_union_layout(env, layout_interner, union_layout); + + let function_value = crate::llvm::refcounting::build_header_help( + env, + &fn_name, + env.context.bool_type().into(), + &[arg_type, arg_type], + ); + + build_tag_eq_help( + env, + layout_interner, + layout_ids, + function_value, + union_layout, + ); + + function_value + } + }; + + env.builder.position_at_end(block); + env.builder.set_current_debug_location(di_location); + let call = env + .builder + .new_build_call(function, &[tag1.into(), tag2.into()], "tag_eq"); + + call.set_call_convention(FAST_CALL_CONV); + + call.try_as_basic_value().left().unwrap() +} + +fn build_tag_eq_help<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + layout_ids: &mut LayoutIds<'a>, + parent: FunctionValue<'ctx>, + union_layout: &UnionLayout<'a>, +) { + let ctx = env.context; + let builder = env.builder; + + { + use inkwell::debug_info::AsDIScope; + + let func_scope = parent.get_subprogram().unwrap(); + let lexical_block = env.dibuilder.create_lexical_block( + /* scope */ func_scope.as_debug_info_scope(), + /* file */ env.compile_unit.get_file(), + /* line_no */ 0, + /* column_no */ 0, + ); + + let loc = env.dibuilder.create_debug_location( + ctx, + /* line */ 0, + /* column */ 0, + /* current_scope */ lexical_block.as_debug_info_scope(), + /* inlined_at */ None, + ); + builder.set_current_debug_location(loc); + } + + // Add args to scope + let mut it = parent.get_param_iter(); + let tag1 = it.next().unwrap(); + let tag2 = it.next().unwrap(); + + tag1.set_name(Symbol::ARG_1.as_str(&env.interns)); + tag2.set_name(Symbol::ARG_2.as_str(&env.interns)); + + let entry = ctx.append_basic_block(parent, "entry"); + + let return_true = ctx.append_basic_block(parent, "return_true"); + let return_false = ctx.append_basic_block(parent, "return_false"); + + { + env.builder.position_at_end(return_false); + env.builder + .new_build_return(Some(&env.context.bool_type().const_int(0, false))); + } + + { + env.builder.position_at_end(return_true); + env.builder + .new_build_return(Some(&env.context.bool_type().const_int(1, false))); + } + + env.builder.position_at_end(entry); + + use UnionLayout::*; + + match union_layout { + NonRecursive(&[]) => { + // we're comparing empty tag unions; this code is effectively unreachable + env.builder.new_build_unreachable(); + } + NonRecursive(tags) => { + let ptr_equal = env.builder.new_build_int_compare( + IntPredicate::EQ, + env.builder + .new_build_ptr_to_int(tag1.into_pointer_value(), env.ptr_int(), "pti"), + env.builder + .new_build_ptr_to_int(tag2.into_pointer_value(), env.ptr_int(), "pti"), + "compare_pointers", + ); + + let compare_tag_ids = ctx.append_basic_block(parent, "compare_tag_ids"); + + env.builder + .new_build_conditional_branch(ptr_equal, return_true, compare_tag_ids); + + env.builder.position_at_end(compare_tag_ids); + + let id1 = get_tag_id(env, layout_interner, parent, union_layout, tag1); + let id2 = get_tag_id(env, layout_interner, parent, union_layout, tag2); + + // clear the tag_id so we get a pointer to the actual data + let tag1 = tag1.into_pointer_value(); + let tag2 = tag2.into_pointer_value(); + + let compare_tag_fields = ctx.append_basic_block(parent, "compare_tag_fields"); + + let same_tag = + env.builder + .new_build_int_compare(IntPredicate::EQ, id1, id2, "compare_tag_id"); + + env.builder + .new_build_conditional_branch(same_tag, compare_tag_fields, return_false); + + env.builder.position_at_end(compare_tag_fields); + + // switch on all the tag ids + + let mut cases = Vec::with_capacity_in(tags.len(), env.arena); + + for (tag_id, field_layouts) in tags.iter().enumerate() { + let block = env.context.append_basic_block(parent, "tag_id_modify"); + env.builder.position_at_end(block); + + let struct_layout = LayoutRepr::struct_(field_layouts); + + let answer = eq_ptr_to_struct( + env, + layout_interner, + layout_ids, + struct_layout, + field_layouts, + tag1, + tag2, + ); + + env.builder.new_build_return(Some(&answer)); + + cases.push((id1.get_type().const_int(tag_id as u64, false), block)); + } + + env.builder.position_at_end(compare_tag_fields); + + match cases.pop() { + Some((_, default)) => { + env.builder.new_build_switch(id1, default, &cases); + } + None => { + // we're comparing empty tag unions; this code is effectively unreachable + env.builder.new_build_unreachable(); + } + } + } + Recursive(tags) => { + let ptr_equal = env.builder.new_build_int_compare( + IntPredicate::EQ, + env.builder + .new_build_ptr_to_int(tag1.into_pointer_value(), env.ptr_int(), "pti"), + env.builder + .new_build_ptr_to_int(tag2.into_pointer_value(), env.ptr_int(), "pti"), + "compare_pointers", + ); + + let compare_tag_ids = ctx.append_basic_block(parent, "compare_tag_ids"); + + env.builder + .new_build_conditional_branch(ptr_equal, return_true, compare_tag_ids); + + env.builder.position_at_end(compare_tag_ids); + + let id1 = get_tag_id(env, layout_interner, parent, union_layout, tag1); + let id2 = get_tag_id(env, layout_interner, parent, union_layout, tag2); + + // clear the tag_id so we get a pointer to the actual data + let tag1 = tag_pointer_clear_tag_id(env, tag1.into_pointer_value()); + let tag2 = tag_pointer_clear_tag_id(env, tag2.into_pointer_value()); + + let compare_tag_fields = ctx.append_basic_block(parent, "compare_tag_fields"); + + let same_tag = + env.builder + .new_build_int_compare(IntPredicate::EQ, id1, id2, "compare_tag_id"); + + env.builder + .new_build_conditional_branch(same_tag, compare_tag_fields, return_false); + + env.builder.position_at_end(compare_tag_fields); + + // switch on all the tag ids + + let mut cases = Vec::with_capacity_in(tags.len(), env.arena); + + for (tag_id, field_layouts) in tags.iter().enumerate() { + let block = env.context.append_basic_block(parent, "tag_id_modify"); + env.builder.position_at_end(block); + + let struct_layout = LayoutRepr::struct_(field_layouts); + + let answer = eq_ptr_to_struct( + env, + layout_interner, + layout_ids, + struct_layout, + field_layouts, + tag1, + tag2, + ); + + env.builder.new_build_return(Some(&answer)); + + cases.push((id1.get_type().const_int(tag_id as u64, false), block)); + } + + env.builder.position_at_end(compare_tag_fields); + + let default = cases.pop().unwrap().1; + + env.builder.new_build_switch(id1, default, &cases); + } + NullableUnwrapped { other_fields, .. } => { + let ptr_equal = env.builder.new_build_int_compare( + IntPredicate::EQ, + env.builder + .new_build_ptr_to_int(tag1.into_pointer_value(), env.ptr_int(), "pti"), + env.builder + .new_build_ptr_to_int(tag2.into_pointer_value(), env.ptr_int(), "pti"), + "compare_pointers", + ); + + let check_for_null = ctx.append_basic_block(parent, "check_for_null"); + let compare_other = ctx.append_basic_block(parent, "compare_other"); + + env.builder + .new_build_conditional_branch(ptr_equal, return_true, check_for_null); + + // check for NULL + + env.builder.position_at_end(check_for_null); + + let is_null_1 = env + .builder + .new_build_is_null(tag1.into_pointer_value(), "is_null"); + + let is_null_2 = env + .builder + .new_build_is_null(tag2.into_pointer_value(), "is_null"); + + let either_null = env + .builder + .new_build_or(is_null_1, is_null_2, "either_null"); + + // logic: the pointers are not the same, if one is NULL, the other one is not + // therefore the two tags are not equal + env.builder + .new_build_conditional_branch(either_null, return_false, compare_other); + + // compare the non-null case + + env.builder.position_at_end(compare_other); + + let struct_layout = LayoutRepr::struct_(other_fields); + + let answer = eq_ptr_to_struct( + env, + layout_interner, + layout_ids, + struct_layout, + other_fields, + tag1.into_pointer_value(), + tag2.into_pointer_value(), + ); + + env.builder.new_build_return(Some(&answer)); + } + NullableWrapped { + other_tags, + nullable_id, + } => { + let ptr_equal = env.builder.new_build_int_compare( + IntPredicate::EQ, + env.builder + .new_build_ptr_to_int(tag1.into_pointer_value(), env.ptr_int(), "pti"), + env.builder + .new_build_ptr_to_int(tag2.into_pointer_value(), env.ptr_int(), "pti"), + "compare_pointers", + ); + + let check_for_null = ctx.append_basic_block(parent, "check_for_null"); + let compare_other = ctx.append_basic_block(parent, "compare_other"); + + env.builder + .new_build_conditional_branch(ptr_equal, return_true, check_for_null); + + // check for NULL + + env.builder.position_at_end(check_for_null); + + let is_null_1 = env + .builder + .new_build_is_null(tag1.into_pointer_value(), "is_null"); + + let is_null_2 = env + .builder + .new_build_is_null(tag2.into_pointer_value(), "is_null"); + + // Logic: + // + // NULL and NULL => equal + // NULL and not => not equal + // not and NULL => not equal + // not and not => more work required + + let i8_type = env.context.i8_type(); + + let sum = env.builder.new_build_int_add( + env.builder + .new_build_int_cast_sign_flag(is_null_1, i8_type, false, "to_u8"), + env.builder + .new_build_int_cast_sign_flag(is_null_2, i8_type, false, "to_u8"), + "sum_is_null", + ); + + env.builder.new_build_switch( + sum, + compare_other, + &[ + (i8_type.const_int(2, false), return_true), + (i8_type.const_int(1, false), return_false), + ], + ); + + // compare the non-null case + + env.builder.position_at_end(compare_other); + + let id1 = get_tag_id(env, layout_interner, parent, union_layout, tag1); + let id2 = get_tag_id(env, layout_interner, parent, union_layout, tag2); + + // clear the tag_id so we get a pointer to the actual data + let tag1 = tag_pointer_clear_tag_id(env, tag1.into_pointer_value()); + let tag2 = tag_pointer_clear_tag_id(env, tag2.into_pointer_value()); + + let compare_tag_fields = ctx.append_basic_block(parent, "compare_tag_fields"); + + let same_tag = + env.builder + .new_build_int_compare(IntPredicate::EQ, id1, id2, "compare_tag_id"); + + env.builder + .new_build_conditional_branch(same_tag, compare_tag_fields, return_false); + + env.builder.position_at_end(compare_tag_fields); + + // switch on all the tag ids + + let tags = other_tags; + let mut cases = Vec::with_capacity_in(tags.len(), env.arena); + + for (i, field_layouts) in tags.iter().enumerate() { + let tag_id = if i >= (*nullable_id as _) { i + 1 } else { i }; + + let block = env.context.append_basic_block(parent, "tag_id_modify"); + env.builder.position_at_end(block); + + let struct_layout = LayoutRepr::struct_(field_layouts); + + let answer = eq_ptr_to_struct( + env, + layout_interner, + layout_ids, + struct_layout, + field_layouts, + tag1, + tag2, + ); + + env.builder.new_build_return(Some(&answer)); + + cases.push((id1.get_type().const_int(tag_id as u64, false), block)); + } + + env.builder.position_at_end(compare_tag_fields); + + let default = cases.pop().unwrap().1; + + env.builder.new_build_switch(id1, default, &cases); + } + NonNullableUnwrapped(field_layouts) => { + let ptr_equal = env.builder.new_build_int_compare( + IntPredicate::EQ, + env.builder + .new_build_ptr_to_int(tag1.into_pointer_value(), env.ptr_int(), "pti"), + env.builder + .new_build_ptr_to_int(tag2.into_pointer_value(), env.ptr_int(), "pti"), + "compare_pointers", + ); + + let compare_fields = ctx.append_basic_block(parent, "compare_fields"); + + env.builder + .new_build_conditional_branch(ptr_equal, return_true, compare_fields); + + env.builder.position_at_end(compare_fields); + + let struct_layout = LayoutRepr::struct_(field_layouts); + + let answer = eq_ptr_to_struct( + env, + layout_interner, + layout_ids, + struct_layout, + field_layouts, + tag1.into_pointer_value(), + tag2.into_pointer_value(), + ); + + env.builder.new_build_return(Some(&answer)); + } + } +} + +fn eq_ptr_to_struct<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + layout_ids: &mut LayoutIds<'a>, + struct_layout: LayoutRepr<'a>, + field_layouts: &'a [InLayout<'a>], + tag1: PointerValue<'ctx>, + tag2: PointerValue<'ctx>, +) -> IntValue<'ctx> { + let wrapper_type = basic_type_from_layout(env, layout_interner, struct_layout); + debug_assert!(wrapper_type.is_struct_type()); + + // cast the opaque pointer to a pointer of the correct shape + let struct1_ptr = env.builder.new_build_pointer_cast( + tag1, + wrapper_type.ptr_type(AddressSpace::default()), + "opaque_to_correct", + ); + + let struct2_ptr = env.builder.new_build_pointer_cast( + tag2, + wrapper_type.ptr_type(AddressSpace::default()), + "opaque_to_correct", + ); + + let struct1 = load_roc_value( + env, + layout_interner, + struct_layout, + struct1_ptr, + "load_struct1", + ); + let struct2 = load_roc_value( + env, + layout_interner, + struct_layout, + struct2_ptr, + "load_struct2", + ); + + build_struct_eq( + env, + layout_interner, + layout_ids, + struct_layout, + field_layouts, + struct1, + struct2, + ) + .into_int_value() +} + +/// ---- + +fn build_box_eq<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + layout_ids: &mut LayoutIds<'a>, + box_layout: LayoutRepr<'a>, + inner_layout: InLayout<'a>, + tag1: BasicValueEnum<'ctx>, + tag2: BasicValueEnum<'ctx>, +) -> BasicValueEnum<'ctx> { + let block = env.builder.get_insert_block().expect("to be in a function"); + let di_location = env.builder.get_current_debug_location().unwrap(); + + let symbol = Symbol::GENERIC_EQ; + let fn_name = layout_ids + .get(symbol, &box_layout) + .to_symbol_string(symbol, &env.interns); + + let function = match env.module.get_function(fn_name.as_str()) { + Some(function_value) => function_value, + None => { + let arg_type = basic_type_from_layout(env, layout_interner, box_layout); + + let function_value = crate::llvm::refcounting::build_header_help( + env, + &fn_name, + env.context.bool_type().into(), + &[arg_type, arg_type], + ); + + build_box_eq_help( + env, + layout_interner, + layout_ids, + function_value, + inner_layout, + ); + + function_value + } + }; + + env.builder.position_at_end(block); + env.builder.set_current_debug_location(di_location); + let call = env + .builder + .new_build_call(function, &[tag1.into(), tag2.into()], "box_eq"); + + call.set_call_convention(FAST_CALL_CONV); + + call.try_as_basic_value().left().unwrap() +} + +fn build_box_eq_help<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + layout_ids: &mut LayoutIds<'a>, + parent: FunctionValue<'ctx>, + inner_layout: InLayout<'a>, +) { + let ctx = env.context; + let builder = env.builder; + + { + use inkwell::debug_info::AsDIScope; + + let func_scope = parent.get_subprogram().unwrap(); + let lexical_block = env.dibuilder.create_lexical_block( + /* scope */ func_scope.as_debug_info_scope(), + /* file */ env.compile_unit.get_file(), + /* line_no */ 0, + /* column_no */ 0, + ); + + let loc = env.dibuilder.create_debug_location( + ctx, + /* line */ 0, + /* column */ 0, + /* current_scope */ lexical_block.as_debug_info_scope(), + /* inlined_at */ None, + ); + builder.set_current_debug_location(loc); + } + + // Add args to scope + let mut it = parent.get_param_iter(); + let box1 = it.next().unwrap(); + let box2 = it.next().unwrap(); + + box1.set_name(Symbol::ARG_1.as_str(&env.interns)); + box2.set_name(Symbol::ARG_2.as_str(&env.interns)); + + let entry = ctx.append_basic_block(parent, "entry"); + env.builder.position_at_end(entry); + + let return_true = ctx.append_basic_block(parent, "return_true"); + env.builder.position_at_end(return_true); + env.builder + .new_build_return(Some(&env.context.bool_type().const_all_ones())); + + env.builder.position_at_end(entry); + + let ptr_equal = env.builder.new_build_int_compare( + IntPredicate::EQ, + env.builder + .new_build_ptr_to_int(box1.into_pointer_value(), env.ptr_int(), "pti"), + env.builder + .new_build_ptr_to_int(box2.into_pointer_value(), env.ptr_int(), "pti"), + "compare_pointers", + ); + + let check_null_then_compare_inner_values = + ctx.append_basic_block(parent, "check_null_then_compare_inner_values"); + + env.builder.new_build_conditional_branch( + ptr_equal, + return_true, + check_null_then_compare_inner_values, + ); + + env.builder + .position_at_end(check_null_then_compare_inner_values); + + // Check for nullability, then compare inner values + + let box1_is_null = env + .builder + .new_build_is_null(box1.into_pointer_value(), "box1_is_null"); + let check_box2_is_null = ctx.append_basic_block(parent, "check_if_box2_is_null"); + let return_false = ctx.append_basic_block(parent, "return_false"); + let compare_inner_values = ctx.append_basic_block(parent, "compare_inner_values"); + + env.builder + .new_build_conditional_branch(box1_is_null, return_false, check_box2_is_null); + + { + env.builder.position_at_end(check_box2_is_null); + let box2_is_null = env + .builder + .new_build_is_null(box2.into_pointer_value(), "box2_is_null"); + env.builder + .new_build_conditional_branch(box2_is_null, return_false, compare_inner_values); + } + + { + env.builder.position_at_end(return_false); + env.builder + .new_build_return(Some(&env.context.bool_type().const_zero())); + } + + // Compare the inner values. + env.builder.position_at_end(compare_inner_values); + + // clear the tag_id so we get a pointer to the actual data + let box1 = box1.into_pointer_value(); + let box2 = box2.into_pointer_value(); + + let value1 = load_roc_value( + env, + layout_interner, + layout_interner.get_repr(inner_layout), + box1, + "load_box1", + ); + let value2 = load_roc_value( + env, + layout_interner, + layout_interner.get_repr(inner_layout), + box2, + "load_box2", + ); + + let is_equal = build_eq( + env, + layout_interner, + layout_ids, + value1, + value2, + layout_interner.get_repr(inner_layout), + layout_interner.get_repr(inner_layout), + ); + + env.builder.new_build_return(Some(&is_equal)); +} diff --git a/crates/compiler/gen_llvm/src/llvm/convert.rs b/crates/compiler/gen_llvm/src/llvm/convert.rs new file mode 100644 index 0000000000..78a02bf460 --- /dev/null +++ b/crates/compiler/gen_llvm/src/llvm/convert.rs @@ -0,0 +1,546 @@ +use crate::llvm::build::{BuilderExt, Env, FunctionSpec, RocReturn}; +use crate::llvm::erased; +use crate::llvm::memcpy::build_memcpy; +use bumpalo::collections::{CollectIn, Vec as AVec}; +use inkwell::context::Context; +use inkwell::types::{BasicType, BasicTypeEnum, FloatType, IntType, StructType}; +use inkwell::values::PointerValue; +use inkwell::AddressSpace; +use roc_builtins::bitcode::{FloatWidth, IntWidth}; +use roc_mono::layout::{ + round_up_to_alignment, Builtin, FunctionPointer, InLayout, Layout, LayoutInterner, LayoutRepr, + STLayoutInterner, UnionLayout, +}; +use roc_target::TargetInfo; + +use super::struct_::RocStruct; + +pub fn basic_type_from_layout<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + layout: LayoutRepr<'_>, +) -> BasicTypeEnum<'ctx> { + use LayoutRepr::*; + + match layout { + Struct(sorted_fields, ..) => { + basic_type_from_record(env, layout_interner, sorted_fields).into() + } + LambdaSet(lambda_set) => basic_type_from_layout( + env, + layout_interner, + layout_interner.get_repr(lambda_set.runtime_representation()), + ), + + Ptr(inner_layout) => { + let inner_type = basic_type_from_layout( + env, + layout_interner, + layout_interner.get_repr(inner_layout), + ); + + inner_type.ptr_type(AddressSpace::default()).into() + } + Union(union_layout) => basic_type_from_union_layout(env, layout_interner, &union_layout), + + RecursivePointer(_) => env + .context + .i64_type() + .ptr_type(AddressSpace::default()) + .as_basic_type_enum(), + + FunctionPointer(self::FunctionPointer { args, ret }) => { + let args = args.iter().map(|arg| { + basic_type_from_layout(env, layout_interner, layout_interner.get_repr(*arg)) + }); + + let ret_repr = layout_interner.get_repr(ret); + let ret = basic_type_from_layout(env, layout_interner, ret_repr); + + let roc_return = RocReturn::from_layout(layout_interner, ret_repr); + + let fn_spec = FunctionSpec::fastcc(env, roc_return, ret, args.collect_in(env.arena)); + + fn_spec.typ.ptr_type(AddressSpace::default()).into() + } + Erased(_) => erased::basic_type(env).into(), + + Builtin(builtin) => basic_type_from_builtin(env, &builtin), + } +} + +fn basic_type_from_record<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + fields: &[InLayout<'_>], +) -> StructType<'ctx> { + let mut field_types = AVec::with_capacity_in(fields.len(), env.arena); + + for field_layout in fields.iter() { + let typ = basic_type_from_layout( + env, + layout_interner, + layout_interner.get_repr(*field_layout), + ); + + field_types.push(typ); + } + + env.context + .struct_type(field_types.into_bump_slice(), false) +} + +pub fn struct_type_from_union_layout<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + union_layout: &UnionLayout<'_>, +) -> StructType<'ctx> { + use UnionLayout::*; + + match union_layout { + NonRecursive([]) => env.context.struct_type(&[], false), + NonRecursive(tags) => { + RocUnion::tagged_from_slices(layout_interner, env.context, tags).struct_type() + } + Recursive(tags) + | NullableWrapped { + other_tags: tags, .. + } => { + if union_layout.stores_tag_id_as_data(env.target_info) { + RocUnion::tagged_from_slices(layout_interner, env.context, tags).struct_type() + } else { + RocUnion::untagged_from_slices(layout_interner, env.context, tags).struct_type() + } + } + NullableUnwrapped { other_fields, .. } => { + RocUnion::untagged_from_slices(layout_interner, env.context, &[other_fields]) + .struct_type() + } + NonNullableUnwrapped(fields) => { + RocUnion::untagged_from_slices(layout_interner, env.context, &[fields]).struct_type() + } + } +} + +fn basic_type_from_union_layout<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + union_layout: &UnionLayout<'_>, +) -> BasicTypeEnum<'ctx> { + use UnionLayout::*; + + let struct_type = struct_type_from_union_layout(env, layout_interner, union_layout); + + match union_layout { + NonRecursive(_) => struct_type.into(), + Recursive(_) + | NonNullableUnwrapped(_) + | NullableWrapped { .. } + | NullableUnwrapped { .. } => struct_type.ptr_type(AddressSpace::default()).into(), + } +} + +pub fn basic_type_from_builtin<'ctx>( + env: &Env<'_, 'ctx, '_>, + builtin: &Builtin<'_>, +) -> BasicTypeEnum<'ctx> { + use Builtin::*; + + let context = env.context; + + match builtin { + Int(int_width) => int_type_from_int_width(env, *int_width).as_basic_type_enum(), + Float(float_width) => float_type_from_float_width(env, *float_width).as_basic_type_enum(), + Bool => context.bool_type().as_basic_type_enum(), + Decimal => context.i128_type().as_basic_type_enum(), + List(_) => zig_list_type(env).into(), + Str => zig_str_type(env).into(), + } +} + +/// Turn a layout into a BasicType that we use in LLVM function arguments. +/// +/// This makes it possible to pass values as something different from how they are typically stored. +/// Current differences +/// +/// - tag unions are passed by-reference. That means that +/// * `f : [Some I64, None] -> I64` is typed `{ { i64, i8 }, i64 }* -> i64` +/// * `f : { x : [Some I64, None] } -> I64 is typed `{ { { i64, i8 }, i64 } } -> i64` +/// +/// Ideas exist to have (bigger than 2 register) records also be passed by-reference, but this +/// is not currently implemented +pub fn argument_type_from_layout<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + layout: LayoutRepr<'a>, +) -> BasicTypeEnum<'ctx> { + use LayoutRepr::*; + + // TODO: can this just be "basic_type_from_layout => ptr if passed_by_ref"? + match layout { + LambdaSet(lambda_set) => argument_type_from_layout( + env, + layout_interner, + layout_interner.get_repr(lambda_set.runtime_representation()), + ), + Union(union_layout) => argument_type_from_union_layout(env, layout_interner, &union_layout), + Builtin(_) => { + let base = basic_type_from_layout(env, layout_interner, layout); + + if layout.is_passed_by_reference(layout_interner) { + base.ptr_type(AddressSpace::default()).into() + } else { + base + } + } + Struct(_) => argument_type_from_struct_layout(env, layout_interner, layout), + _ => basic_type_from_layout(env, layout_interner, layout), + } +} + +/// Some records are passed by-reference. +fn argument_type_from_struct_layout<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + struct_layout: LayoutRepr<'a>, +) -> BasicTypeEnum<'ctx> { + debug_assert!(matches!(struct_layout, LayoutRepr::Struct(_))); + let stack_type = basic_type_from_layout(env, layout_interner, struct_layout); + + if struct_layout.is_passed_by_reference(layout_interner) { + stack_type.ptr_type(AddressSpace::default()).into() + } else { + stack_type + } +} + +/// Non-recursive tag unions are stored on the stack, but passed by-reference +pub fn argument_type_from_union_layout<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + union_layout: &UnionLayout<'_>, +) -> BasicTypeEnum<'ctx> { + let heap_type = basic_type_from_union_layout(env, layout_interner, union_layout); + + if let UnionLayout::NonRecursive(_) = union_layout { + heap_type.ptr_type(AddressSpace::default()).into() + } else { + heap_type + } +} + +pub fn int_type_from_int_width<'ctx>( + env: &Env<'_, 'ctx, '_>, + int_width: IntWidth, +) -> IntType<'ctx> { + use IntWidth::*; + + match int_width { + U128 | I128 => env.context.i128_type(), + U64 | I64 => env.context.i64_type(), + U32 | I32 => env.context.i32_type(), + U16 | I16 => env.context.i16_type(), + U8 | I8 => env.context.i8_type(), + } +} + +pub fn float_type_from_float_width<'ctx>( + env: &Env<'_, 'ctx, '_>, + float_width: FloatWidth, +) -> FloatType<'ctx> { + use FloatWidth::*; + + match float_width { + F64 => env.context.f64_type(), + F32 => env.context.f32_type(), + } +} + +fn alignment_type(context: &Context, alignment: u32) -> BasicTypeEnum { + match alignment { + 0 => context.struct_type(&[], false).into(), + 1 => context.i8_type().into(), + 2 => context.i16_type().into(), + 4 => context.i32_type().into(), + 8 => context.i64_type().into(), + 16 => context.i128_type().into(), + _ => unimplemented!("weird alignment: {alignment}"), + } +} + +#[derive(Debug, Clone, Copy)] +enum TagType { + I8, + I16, +} + +#[derive(Debug, Clone, Copy)] +pub(crate) struct RocUnion<'ctx> { + struct_type: StructType<'ctx>, + data_align: u32, + data_width: u32, + tag_type: Option, +} + +fn is_multiple_of(big: u32, small: u32) -> bool { + match small { + 0 => true, // 0 is a multiple of all n, because n * 0 = 0 + n => big % n == 0, + } +} + +impl<'ctx> RocUnion<'ctx> { + pub const TAG_ID_INDEX: u32 = 2; + pub const TAG_DATA_INDEX: u32 = 1; + + fn new( + context: &'ctx Context, + data_align: u32, + data_width: u32, + tag_type: Option, + ) -> Self { + let bytes = round_up_to_alignment(data_width, data_align); + + let byte_array_type = if is_multiple_of(bytes, 8) && is_multiple_of(data_align, 8) { + context + .i64_type() + .array_type(bytes / 8) + .as_basic_type_enum() + } else { + context.i8_type().array_type(bytes).as_basic_type_enum() + }; + + let alignment_array_type = alignment_type(context, data_align) + .array_type(0) + .as_basic_type_enum(); + + let struct_type = if let Some(tag_type) = tag_type { + let tag_width = match tag_type { + TagType::I8 => 1, + TagType::I16 => 2, + }; + + let tag_padding = round_up_to_alignment(tag_width, data_align) - tag_width; + let tag_padding_type = context + .i8_type() + .array_type(tag_padding) + .as_basic_type_enum(); + + context.struct_type( + &[ + alignment_array_type, + byte_array_type, + match tag_type { + TagType::I8 => context.i8_type().into(), + TagType::I16 => context.i16_type().into(), + }, + tag_padding_type, + ], + false, + ) + } else { + context.struct_type(&[alignment_array_type, byte_array_type], false) + }; + + Self { + struct_type, + data_align, + data_width, + tag_type, + } + } + + pub fn struct_type(&self) -> StructType<'ctx> { + self.struct_type + } + + pub fn tagged_from_slices( + interner: &STLayoutInterner, + context: &'ctx Context, + layouts: &[&[InLayout<'_>]], + ) -> Self { + let tag_type = match layouts.len() { + 0 => unreachable!("zero-element tag union is not represented as a RocUnion"), + 1..=255 => TagType::I8, + _ => TagType::I16, + }; + + let (data_width, data_align) = Layout::stack_size_and_alignment_slices(interner, layouts); + + Self::new(context, data_align, data_width, Some(tag_type)) + } + + pub fn untagged_from_slices( + interner: &STLayoutInterner, + context: &'ctx Context, + layouts: &[&[InLayout<'_>]], + ) -> Self { + let (data_width, data_align) = Layout::stack_size_and_alignment_slices(interner, layouts); + + Self::new(context, data_align, data_width, None) + } + + pub fn data_width(&self) -> u32 { + self.data_width + } + + pub fn tag_alignment(&self) -> u32 { + let tag_id_alignment = match self.tag_type { + None => 0, + Some(TagType::I8) => 1, + Some(TagType::I16) => 2, + }; + + self.data_align.max(tag_id_alignment) + } + + pub fn tag_width(&self) -> u32 { + let tag_id_width = match self.tag_type { + None => 0, + Some(TagType::I8) => 1, + Some(TagType::I16) => 2, + }; + + let mut width = self.data_width; + + // add padding between data and the tag id + width = round_up_to_alignment(width, tag_id_width); + + // add tag id + width += tag_id_width; + + // add padding after the tag id + width = round_up_to_alignment(width, self.tag_alignment()); + + width + } + + pub fn write_struct_data<'a, 'env>( + &self, + env: &Env<'a, 'ctx, 'env>, + layout_interner: &STLayoutInterner<'a>, + // The allocation of the tag to write into. + tag_alloca: PointerValue<'ctx>, + // The data to write into the union. + data: RocStruct<'ctx>, + data_layout: LayoutRepr<'a>, + tag_id: Option, + ) { + debug_assert_eq!(tag_id.is_some(), self.tag_type.is_some()); + + let data_buffer = env.builder.new_build_struct_gep( + self.struct_type(), + tag_alloca, + Self::TAG_DATA_INDEX, + "data_buffer", + ); + + match data { + // NOTE: the data may be smaller than the buffer, so there might be uninitialized + // bytes in the buffer. We should never touch those, but e.g. valgrind might not + // realize that. If that comes up, the solution is to just fill it with zeros + RocStruct::ByValue(value) => { + let cast_pointer = env.builder.new_build_pointer_cast( + data_buffer, + value.get_type().ptr_type(AddressSpace::default()), + "to_data_ptr", + ); + env.builder.new_build_store(cast_pointer, value); + } + RocStruct::ByReference(payload_data_ptr) => { + let cast_tag_pointer = env.builder.new_build_pointer_cast( + data_buffer, + payload_data_ptr.get_type(), + "to_data_ptr", + ); + + build_memcpy( + env, + layout_interner, + data_layout, + cast_tag_pointer, + payload_data_ptr, + ); + } + } + + // set the tag id + // + // NOTE: setting the tag id initially happened before writing the data into it. + // That turned out to expose UB. More info at https://github.com/roc-lang/roc/issues/3554 + if let Some(tag_id) = tag_id { + let tag_id_type = match self.tag_type.unwrap() { + TagType::I8 => env.context.i8_type(), + TagType::I16 => env.context.i16_type(), + }; + + let tag_id_ptr = env.builder.new_build_struct_gep( + self.struct_type(), + tag_alloca, + Self::TAG_ID_INDEX, + "tag_id_ptr", + ); + + let tag_id = tag_id_type.const_int(tag_id as u64, false); + + env.builder.new_build_store(tag_id_ptr, tag_id); + } + } +} + +/// The int type that the C ABI turns our RocList/RocStr into +pub fn str_list_int(ctx: &Context, target_info: TargetInfo) -> IntType<'_> { + match target_info.ptr_width() { + roc_target::PtrWidth::Bytes4 => ctx.i64_type(), + roc_target::PtrWidth::Bytes8 => ctx.i128_type(), + } +} + +pub fn zig_list_type<'ctx>(env: &Env<'_, 'ctx, '_>) -> StructType<'ctx> { + env.module.get_struct_type("list.RocList").unwrap() +} + +pub fn zig_str_type<'ctx>(env: &Env<'_, 'ctx, '_>) -> StructType<'ctx> { + env.module.get_struct_type("str.RocStr").unwrap() +} + +pub fn zig_dec_type<'ctx>(env: &Env<'_, 'ctx, '_>) -> StructType<'ctx> { + env.module.get_struct_type("dec.RocDec").unwrap() +} + +pub fn zig_has_tag_id_type<'ctx>(env: &Env<'_, 'ctx, '_>) -> StructType<'ctx> { + let u8_ptr_t = env.context.i8_type().ptr_type(AddressSpace::default()); + + env.context + .struct_type(&[env.context.bool_type().into(), u8_ptr_t.into()], false) +} + +pub fn zig_num_parse_result_type<'ctx>( + env: &Env<'_, 'ctx, '_>, + type_name: &str, +) -> StructType<'ctx> { + let name = format!("num.NumParseResult({type_name})"); + + match env.module.get_struct_type(&name) { + Some(zig_type) => zig_type, + None => panic!("zig does not define the `{name}` type!"), + } +} + +pub fn zig_to_int_checked_result_type<'ctx>( + env: &Env<'_, 'ctx, '_>, + type_name: &str, +) -> StructType<'ctx> { + let name = format!("num.ToIntCheckedResult({type_name})"); + + match env.module.get_struct_type(&name) { + Some(zig_type) => zig_type, + None => panic!("zig does not define the `{name}` type!"), + } +} + +pub fn zig_with_overflow_roc_dec<'ctx>(env: &Env<'_, 'ctx, '_>) -> StructType<'ctx> { + env.module + .get_struct_type("utils.WithOverflow(dec.RocDec)") + .unwrap() +} diff --git a/crates/compiler/gen_llvm/src/llvm/erased.rs b/crates/compiler/gen_llvm/src/llvm/erased.rs new file mode 100644 index 0000000000..375b2b8e59 --- /dev/null +++ b/crates/compiler/gen_llvm/src/llvm/erased.rs @@ -0,0 +1,131 @@ +use inkwell::{ + types::{PointerType, StructType}, + values::{PointerValue, StructValue}, + AddressSpace, +}; +use roc_mono::ir::ErasedField; + +use super::build::{BuilderExt, Env}; + +pub fn opaque_ptr_type<'ctx>(env: &Env<'_, 'ctx, '_>) -> PointerType<'ctx> { + env.context.i8_type().ptr_type(AddressSpace::default()) +} + +fn refcounter_type<'ctx>(env: &Env<'_, 'ctx, '_>) -> PointerType<'ctx> { + let return_void = env.context.void_type(); + let arg_ty = opaque_ptr_type(env); + + return_void + .fn_type(&[arg_ty.into()], false) + .ptr_type(AddressSpace::default()) +} + +/// Erased is laid out like +/// +/// ```text +/// struct Erased { +/// value: void*, +/// callee: void*, +/// refcounter_inc: (void* -> void) *, +/// refcounter_dec: (void* -> void) *, +/// } +/// ``` +pub fn basic_type<'ctx>(env: &Env<'_, 'ctx, '_>) -> StructType<'ctx> { + let opaque_ptr_ty = opaque_ptr_type(env); + let refcounter_ptr_ty = refcounter_type(env); + + env.context.struct_type( + &[ + opaque_ptr_ty.into(), + opaque_ptr_ty.into(), + refcounter_ptr_ty.into(), + refcounter_ptr_ty.into(), + ], + false, + ) +} + +fn bitcast_to_opaque_ptr<'ctx>( + env: &Env<'_, 'ctx, '_>, + value: PointerValue<'ctx>, +) -> PointerValue<'ctx> { + env.builder + .new_build_bitcast( + value, + env.context.i8_type().ptr_type(AddressSpace::default()), + "to_opaque_ptr", + ) + .into_pointer_value() +} + +pub fn build<'ctx>( + env: &Env<'_, 'ctx, '_>, + value: Option>, + callee: PointerValue<'ctx>, +) -> StructValue<'ctx> { + let struct_type = basic_type(env); + + let struct_value = struct_type.const_zero().into(); + + let struct_value = match value { + Some(value) => { + let value = bitcast_to_opaque_ptr(env, value); + env.builder + .build_insert_value(struct_value, value, 0, "insert_value") + .unwrap() + } + None => struct_value, + }; + + let callee = bitcast_to_opaque_ptr(env, callee); + let struct_value = env + .builder + .build_insert_value(struct_value, callee, 1, "insert_callee") + .unwrap(); + + // TODO: insert refcounter + + struct_value.into_struct_value() +} + +pub fn load<'ctx>( + env: &Env<'_, 'ctx, '_>, + erasure: StructValue<'ctx>, + field: ErasedField, + as_type: PointerType<'ctx>, +) -> PointerValue<'ctx> { + let index = match field { + ErasedField::Value => 0, + ErasedField::ValuePtr => 0, + ErasedField::Callee => 1, + }; + + let value = env + .builder + .build_extract_value(erasure, index, "extract_erased_value") + .unwrap() + .into_pointer_value(); + + let value = env + .builder + .new_build_bitcast(value, as_type, "bitcast_to_type") + .into_pointer_value(); + + value +} + +pub fn load_refcounter<'ctx>( + env: &Env<'_, 'ctx, '_>, + erasure: StructValue<'ctx>, + mode: super::refcounting::Mode, +) -> PointerValue<'ctx> { + let index = match mode { + super::refcounting::Mode::Inc => 2, + super::refcounting::Mode::Dec => 3, + }; + + env.builder + .build_extract_value(erasure, index, "extract_refcounter") + .unwrap() + .into_pointer_value() +} diff --git a/crates/compiler/gen_llvm/src/llvm/expect.rs b/crates/compiler/gen_llvm/src/llvm/expect.rs new file mode 100644 index 0000000000..8f48ec09d9 --- /dev/null +++ b/crates/compiler/gen_llvm/src/llvm/expect.rs @@ -0,0 +1,1105 @@ +use crate::debug_info_init; +use crate::llvm::bitcode::call_str_bitcode_fn; +use crate::llvm::build::{get_tag_id, store_roc_value, tag_pointer_clear_tag_id, Env}; +use crate::llvm::build_list::{self, incrementing_elem_loop}; +use crate::llvm::convert::{basic_type_from_layout, RocUnion}; +use inkwell::builder::Builder; +use inkwell::module::Linkage; +use inkwell::types::{BasicMetadataTypeEnum, BasicType, BasicTypeEnum}; +use inkwell::values::{BasicValueEnum, FunctionValue, IntValue, PointerValue}; +use inkwell::AddressSpace; +use roc_builtins::bitcode; +use roc_error_macros::{internal_error, todo_lambda_erasure}; +use roc_module::symbol::Symbol; +use roc_mono::ir::LookupType; +use roc_mono::layout::{ + Builtin, InLayout, LayoutIds, LayoutInterner, LayoutRepr, STLayoutInterner, UnionLayout, +}; +use roc_region::all::Region; + +use super::build::BuilderExt; +use super::build::{add_func, FunctionSpec, LlvmBackendMode}; +use super::convert::struct_type_from_union_layout; +use super::scope::Scope; +use super::struct_::RocStruct; + +pub(crate) struct SharedMemoryPointer<'ctx>(PointerValue<'ctx>); + +impl<'ctx> SharedMemoryPointer<'ctx> { + pub(crate) fn get<'a, 'env>(env: &Env<'a, 'ctx, 'env>) -> Self { + let start_function = if let LlvmBackendMode::BinaryDev = env.mode { + bitcode::UTILS_EXPECT_FAILED_START_SHARED_FILE + } else { + bitcode::UTILS_EXPECT_FAILED_START_SHARED_BUFFER + }; + + let func = env.module.get_function(start_function).unwrap(); + + let call_result = env + .builder + .new_build_call(func, &[], "call_expect_start_failed"); + + let ptr = call_result + .try_as_basic_value() + .left() + .unwrap() + .into_pointer_value(); + + Self(ptr) + } +} + +#[derive(Debug, Clone, Copy)] +struct Cursors<'ctx> { + offset: IntValue<'ctx>, + extra_offset: IntValue<'ctx>, +} + +fn pointer_at_offset<'ctx>( + bd: &Builder<'ctx>, + element_type: impl BasicType<'ctx>, + ptr: PointerValue<'ctx>, + offset: IntValue<'ctx>, +) -> PointerValue<'ctx> { + unsafe { bd.new_build_in_bounds_gep(element_type, ptr, &[offset], "offset_ptr") } +} + +/// Writes the module and region into the buffer +fn write_header<'ctx>( + env: &Env<'_, 'ctx, '_>, + ptr: PointerValue<'ctx>, + mut offset: IntValue<'ctx>, + condition: Symbol, + region: Region, +) -> IntValue<'ctx> { + let region_start = env + .context + .i32_type() + .const_int(region.start().offset as _, false); + + let region_end = env + .context + .i32_type() + .const_int(region.end().offset as _, false); + + let module_id: u32 = unsafe { std::mem::transmute(condition.module_id()) }; + let module_id = env.context.i32_type().const_int(module_id as _, false); + + offset = build_copy(env, ptr, offset, region_start.into()); + offset = build_copy(env, ptr, offset, region_end.into()); + offset = build_copy(env, ptr, offset, module_id.into()); + + offset +} + +/// Read the first two 32-bit values from the shared memory, +/// representing the total number of expect frames and the next free position +fn read_state<'ctx>( + env: &Env<'_, 'ctx, '_>, + ptr: PointerValue<'ctx>, +) -> (IntValue<'ctx>, IntValue<'ctx>) { + let ptr_type = env.ptr_int().ptr_type(AddressSpace::default()); + let ptr = env.builder.new_build_pointer_cast(ptr, ptr_type, ""); + + let one = env.ptr_int().const_int(1, false); + let offset_ptr = pointer_at_offset(env.builder, env.ptr_int(), ptr, one); + + let count = env.builder.new_build_load(env.ptr_int(), ptr, "load_count"); + let offset = env + .builder + .new_build_load(env.ptr_int(), offset_ptr, "load_offset"); + + (count.into_int_value(), offset.into_int_value()) +} + +fn write_state<'ctx>( + env: &Env<'_, 'ctx, '_>, + ptr: PointerValue<'ctx>, + count: IntValue<'ctx>, + offset: IntValue<'ctx>, +) { + let ptr_type = env.ptr_int().ptr_type(AddressSpace::default()); + let ptr = env.builder.new_build_pointer_cast(ptr, ptr_type, ""); + + let one = env.ptr_int().const_int(1, false); + let offset_ptr = pointer_at_offset(env.builder, env.ptr_int(), ptr, one); + + env.builder.new_build_store(ptr, count); + env.builder.new_build_store(offset_ptr, offset); +} + +fn offset_add<'ctx>( + builder: &Builder<'ctx>, + current: IntValue<'ctx>, + extra: u32, +) -> IntValue<'ctx> { + let intval = current.get_type().const_int(extra as _, false); + builder.new_build_int_add(current, intval, "offset_add") +} + +pub(crate) fn notify_parent_expect(env: &Env, shared_memory: &SharedMemoryPointer) { + let func = env + .module + .get_function(bitcode::NOTIFY_PARENT_EXPECT) + .unwrap(); + + env.builder.new_build_call( + func, + &[shared_memory.0.into()], + "call_expect_failed_finalize", + ); +} + +// Shape of expect frame: +// +// === +// Fixed-size header +// === +// /-- ptr_lookup_1 (ptr_size) +// | var_lookup_1 (u32) +// | .. +// | ptr_lookup_n (ptr_size) +// | var_lookup_n (u32) +// \-> lookup_val_1 (varsize) +// .. +// lookup_val_n (varsize) +// +pub(crate) fn clone_to_shared_memory<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + scope: &Scope<'a, 'ctx>, + layout_ids: &mut LayoutIds<'a>, + shared_memory: &SharedMemoryPointer<'ctx>, + condition: Symbol, + region: Region, + lookups: &[Symbol], + lookup_variables: &[LookupType], +) { + let original_ptr = shared_memory.0; + + let (count, mut offset) = read_state(env, original_ptr); + + offset = write_header(env, original_ptr, offset, condition, region); + + let after_header = offset; + + let space_for_offsets = env.ptr_int().const_int( + (lookups.len() * env.target_info.ptr_size() + lookups.len() * std::mem::size_of::()) + as _, + false, + ); + + let mut lookup_starts = bumpalo::collections::Vec::with_capacity_in(lookups.len(), env.arena); + + offset = env + .builder + .new_build_int_add(offset, space_for_offsets, "offset"); + + for lookup in lookups.iter() { + lookup_starts.push(offset); + + let (value, layout) = scope.load_symbol_and_layout(lookup); + + let stack_size = env + .ptr_int() + .const_int(layout_interner.stack_size(layout) as u64, false); + + let mut extra_offset = env.builder.new_build_int_add(offset, stack_size, "offset"); + + let cursors = Cursors { + offset, + extra_offset, + }; + + extra_offset = build_clone( + env, + layout_interner, + layout_ids, + original_ptr, + cursors, + value, + layout_interner.get_repr(layout), + ); + + offset = extra_offset; + } + + { + let mut offset = after_header; + + for (lookup_start, lookup_var) in lookup_starts.into_iter().zip(lookup_variables) { + // Store the pointer to the value + { + build_copy(env, original_ptr, offset, lookup_start.into()); + + let ptr_width = env + .ptr_int() + .const_int(env.target_info.ptr_size() as _, false); + + offset = env.builder.new_build_int_add(offset, ptr_width, "offset"); + } + + // Store the specialized variable of the value + { + let ptr = unsafe { + env.builder.new_build_in_bounds_gep( + env.context.i8_type(), + original_ptr, + &[offset], + "at_current_offset", + ) + }; + + let u32_ptr = env.context.i32_type().ptr_type(AddressSpace::default()); + let ptr = env + .builder + .new_build_pointer_cast(ptr, u32_ptr, "cast_ptr_type"); + + let var_value = env + .context + .i32_type() + .const_int(lookup_var.index() as _, false); + + env.builder.new_build_store(ptr, var_value); + + let var_size = env + .ptr_int() + .const_int(std::mem::size_of::() as _, false); + + offset = env.builder.new_build_int_add(offset, var_size, "offset"); + } + } + } + + let one = env.ptr_int().const_int(1, false); + let new_count = env.builder.new_build_int_add(count, one, "inc"); + write_state(env, original_ptr, new_count, offset) +} + +fn build_clone<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + layout_ids: &mut LayoutIds<'a>, + ptr: PointerValue<'ctx>, + cursors: Cursors<'ctx>, + value: BasicValueEnum<'ctx>, + layout: LayoutRepr<'a>, +) -> IntValue<'ctx> { + match layout { + LayoutRepr::Builtin(builtin) => build_clone_builtin( + env, + layout_interner, + layout_ids, + ptr, + cursors, + value, + builtin, + ), + + LayoutRepr::Struct(field_layouts) => build_clone_struct( + env, + layout_interner, + layout_ids, + ptr, + cursors, + value, + layout, + field_layouts, + ), + + // Since we will never actually display functions (and hence lambda sets) + // we just write nothing to the buffer + LayoutRepr::LambdaSet(_) => cursors.extra_offset, + + LayoutRepr::Union(union_layout) => { + if layout.safe_to_memcpy(layout_interner) { + let ptr = unsafe { + env.builder.new_build_in_bounds_gep( + env.context.i8_type(), + ptr, + &[cursors.offset], + "at_current_offset", + ) + }; + + let ptr_type = value.get_type().ptr_type(AddressSpace::default()); + let ptr = env + .builder + .new_build_pointer_cast(ptr, ptr_type, "cast_ptr_type"); + + store_roc_value(env, layout_interner, layout, ptr, value); + + cursors.extra_offset + } else { + build_clone_tag( + env, + layout_interner, + layout_ids, + ptr, + cursors, + value, + union_layout, + ) + } + } + + LayoutRepr::Ptr(_) => { + unreachable!("for internal use only") + } + + LayoutRepr::RecursivePointer(rec_layout) => { + let layout = rec_layout; + + let bt = basic_type_from_layout(env, layout_interner, layout_interner.get_repr(layout)); + + // cast the i64 pointer to a pointer to block of memory + let field1_cast = env.builder.new_build_pointer_cast( + value.into_pointer_value(), + bt.into_pointer_type(), + "i64_to_opaque", + ); + + let union_layout = match layout_interner.get_repr(rec_layout) { + LayoutRepr::Union(union_layout) => { + debug_assert!(!matches!(union_layout, UnionLayout::NonRecursive(..))); + union_layout + } + _ => internal_error!(), + }; + + build_clone_tag( + env, + layout_interner, + layout_ids, + ptr, + cursors, + field1_cast.into(), + union_layout, + ) + } + LayoutRepr::FunctionPointer(_) => todo_lambda_erasure!(), + LayoutRepr::Erased(_) => todo_lambda_erasure!(), + } +} + +fn build_clone_struct<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + layout_ids: &mut LayoutIds<'a>, + ptr: PointerValue<'ctx>, + cursors: Cursors<'ctx>, + value: BasicValueEnum<'ctx>, + struct_layout: LayoutRepr<'a>, + field_layouts: &[InLayout<'a>], +) -> IntValue<'ctx> { + if struct_layout.safe_to_memcpy(layout_interner) { + build_copy(env, ptr, cursors.offset, value) + } else { + let mut cursors = cursors; + + let structure = RocStruct::from(value); + + for (i, field_layout) in field_layouts.iter().enumerate() { + let field = structure.load_at_index(env, layout_interner, struct_layout, i as _); + + let new_extra = build_clone( + env, + layout_interner, + layout_ids, + ptr, + cursors, + field, + layout_interner.get_repr(*field_layout), + ); + + let field_width = env + .ptr_int() + .const_int(layout_interner.stack_size(*field_layout) as u64, false); + + cursors.extra_offset = new_extra; + cursors.offset = env + .builder + .new_build_int_add(cursors.offset, field_width, "offset"); + } + + cursors.extra_offset + } +} + +fn build_clone_tag<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + layout_ids: &mut LayoutIds<'a>, + ptr: PointerValue<'ctx>, + cursors: Cursors<'ctx>, + value: BasicValueEnum<'ctx>, + union_layout: UnionLayout<'a>, +) -> IntValue<'ctx> { + let layout = LayoutRepr::Union(union_layout); + let layout_id = layout_ids.get(Symbol::CLONE, &layout); + let fn_name = layout_id.to_symbol_string(Symbol::CLONE, &env.interns); + + let function = match env.module.get_function(fn_name.as_str()) { + Some(function_value) => function_value, + None => { + let block = env.builder.get_insert_block().expect("to be in a function"); + let di_location = env.builder.get_current_debug_location().unwrap(); + + let function_type = env.ptr_int().fn_type( + &[ + env.context + .i8_type() + .ptr_type(AddressSpace::default()) + .into(), + env.ptr_int().into(), + env.ptr_int().into(), + BasicMetadataTypeEnum::from(value.get_type()), + ], + false, + ); + + let function_value = add_func( + env.context, + env.module, + &fn_name, + FunctionSpec::known_fastcc(function_type), + Linkage::Private, + ); + + let subprogram = env.new_subprogram(&fn_name); + function_value.set_subprogram(subprogram); + + env.dibuilder.finalize(); + + build_clone_tag_help( + env, + layout_interner, + layout_ids, + union_layout, + function_value, + ); + + env.builder.position_at_end(block); + env.builder.set_current_debug_location(di_location); + + function_value + } + }; + + let call = env.builder.new_build_call( + function, + &[ + ptr.into(), + cursors.offset.into(), + cursors.extra_offset.into(), + value.into(), + ], + "build_clone_tag", + ); + + call.set_call_convention(function.get_call_conventions()); + + let result = call.try_as_basic_value().left().unwrap(); + + result.into_int_value() +} + +fn load_tag_data<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + union_layout: UnionLayout<'a>, + tag_value: PointerValue<'ctx>, + tag_type: BasicTypeEnum<'ctx>, +) -> BasicValueEnum<'ctx> { + let union_struct_type = struct_type_from_union_layout(env, layout_interner, &union_layout); + + let raw_data_ptr = env.builder.new_build_struct_gep( + union_struct_type, + tag_value, + RocUnion::TAG_DATA_INDEX, + "tag_data", + ); + + let data_ptr = env.builder.new_build_pointer_cast( + raw_data_ptr, + tag_type.ptr_type(AddressSpace::default()), + "data_ptr", + ); + + env.builder.new_build_load(tag_type, data_ptr, "load_data") +} + +fn clone_tag_payload_and_id<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + layout_ids: &mut LayoutIds<'a>, + ptr: PointerValue<'ctx>, + cursors: Cursors<'ctx>, + roc_union: RocUnion<'ctx>, + tag_id: usize, + payload_layout: LayoutRepr<'a>, + opaque_payload_ptr: PointerValue<'ctx>, +) -> IntValue<'ctx> { + let payload_type = basic_type_from_layout(env, layout_interner, payload_layout); + + let payload_ptr = env.builder.new_build_pointer_cast( + opaque_payload_ptr, + payload_type.ptr_type(AddressSpace::default()), + "cast_payload_ptr", + ); + + let payload = env + .builder + .new_build_load(payload_type, payload_ptr, "payload"); + + // NOTE: `answer` includes any extra_offset that the tag payload may have needed + // (e.g. because it includes a list). That is what we want to return, but not what + // we need to write the padding and offset of this tag + let answer = build_clone( + env, + layout_interner, + layout_ids, + ptr, + cursors, + payload, + payload_layout, + ); + + // include padding between data and tag id + let tag_id_internal_offset = roc_union.data_width(); + + let tag_id_offset = offset_add(env.builder, cursors.offset, tag_id_internal_offset); + + // write the tag id + let value = env.context.i8_type().const_int(tag_id as _, false); + build_copy(env, ptr, tag_id_offset, value.into()); + + // NOTE: padding after tag id (is taken care of by the cursor) + + answer +} + +fn build_clone_tag_help<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + layout_ids: &mut LayoutIds<'a>, + union_layout: UnionLayout<'a>, + fn_val: FunctionValue<'ctx>, +) { + use bumpalo::collections::Vec; + + let context = &env.context; + let builder = env.builder; + + // Add a basic block for the entry point + let entry = context.append_basic_block(fn_val, "entry"); + + builder.position_at_end(entry); + + debug_info_init!(env, fn_val); + + // Add args to scope + // let arg_symbol = Symbol::ARG_1; + // tag_value.set_name(arg_symbol.as_str(&env.interns)); + + let mut it = fn_val.get_param_iter(); + + let ptr = it.next().unwrap().into_pointer_value(); + let offset = it.next().unwrap().into_int_value(); + let extra_offset = it.next().unwrap().into_int_value(); + let tag_value = it.next().unwrap(); + + let cursors = Cursors { + offset, + extra_offset, + }; + + let parent = fn_val; + + debug_assert!(tag_value.is_pointer_value()); + + use UnionLayout::*; + + match union_layout { + NonRecursive(&[]) => { + // we're comparing empty tag unions; this code is effectively unreachable + env.builder.new_build_unreachable(); + } + NonRecursive(tags) => { + let id = get_tag_id(env, layout_interner, parent, &union_layout, tag_value); + + let switch_block = env.context.append_basic_block(parent, "switch_block"); + env.builder.new_build_unconditional_branch(switch_block); + + let mut cases = Vec::with_capacity_in(tags.len(), env.arena); + + for (tag_id, field_layouts) in tags.iter().enumerate() { + let block = env.context.append_basic_block(parent, "tag_id_modify"); + env.builder.position_at_end(block); + + let roc_union = RocUnion::tagged_from_slices(layout_interner, env.context, tags); + + // load the tag payload (if any) + let payload_layout = LayoutRepr::struct_(field_layouts); + + let opaque_payload_ptr = env.builder.new_build_struct_gep( + roc_union.struct_type(), + tag_value.into_pointer_value(), + RocUnion::TAG_DATA_INDEX, + "data_buffer", + ); + + let answer = clone_tag_payload_and_id( + env, + layout_interner, + layout_ids, + ptr, + cursors, + roc_union, + tag_id, + payload_layout, + opaque_payload_ptr, + ); + + env.builder.new_build_return(Some(&answer)); + + cases.push((id.get_type().const_int(tag_id as u64, false), block)); + } + + env.builder.position_at_end(switch_block); + + match cases.pop() { + Some((_, default)) => { + env.builder.new_build_switch(id, default, &cases); + } + None => { + // we're serializing an empty tag union; this code is effectively unreachable + env.builder.new_build_unreachable(); + } + } + } + Recursive(tags) => { + let id = get_tag_id(env, layout_interner, parent, &union_layout, tag_value); + + let switch_block = env.context.append_basic_block(parent, "switch_block"); + env.builder.new_build_unconditional_branch(switch_block); + + let mut cases = Vec::with_capacity_in(tags.len(), env.arena); + + for (tag_id, field_layouts) in tags.iter().enumerate() { + let block = env.context.append_basic_block(parent, "tag_id_modify"); + env.builder.position_at_end(block); + + // write the "pointer" of the current offset + write_pointer_with_tag_id(env, ptr, offset, extra_offset, union_layout, tag_id); + + let tag_value = tag_pointer_clear_tag_id(env, tag_value.into_pointer_value()); + + let layout = LayoutRepr::struct_(field_layouts); + let layout = if union_layout.stores_tag_id_in_pointer(env.target_info) { + layout + } else { + // [...fields, tag ID] + let mut fields = Vec::from_iter_in(field_layouts.iter().copied(), env.arena); + fields.push(union_layout.tag_id_layout()); + LayoutRepr::struct_(fields.into_bump_slice()) + }; + + let basic_type = basic_type_from_layout(env, layout_interner, layout); + let data = load_tag_data(env, layout_interner, union_layout, tag_value, basic_type); + + let (width, _) = union_layout.data_size_and_alignment(layout_interner); + + let cursors = Cursors { + offset: extra_offset, + extra_offset: env.builder.new_build_int_add( + extra_offset, + env.ptr_int().const_int(width as _, false), + "new_offset", + ), + }; + + let answer = + build_clone(env, layout_interner, layout_ids, ptr, cursors, data, layout); + + env.builder.new_build_return(Some(&answer)); + + cases.push((id.get_type().const_int(tag_id as u64, false), block)); + } + + env.builder.position_at_end(switch_block); + + match cases.pop() { + Some((_, default)) => { + env.builder.new_build_switch(id, default, &cases); + } + None => { + // we're serializing an empty tag union; this code is effectively unreachable + env.builder.new_build_unreachable(); + } + } + } + NonNullableUnwrapped(fields) => { + let tag_value = tag_value.into_pointer_value(); + + build_copy(env, ptr, offset, extra_offset.into()); + + let layout = LayoutRepr::struct_(fields); + let basic_type = basic_type_from_layout(env, layout_interner, layout); + + let (width, _) = union_layout.data_size_and_alignment(layout_interner); + + let cursors = Cursors { + offset: extra_offset, + extra_offset: env.builder.new_build_int_add( + extra_offset, + env.ptr_int().const_int(width as _, false), + "new_offset", + ), + }; + + let data = load_tag_data(env, layout_interner, union_layout, tag_value, basic_type); + + let answer = build_clone(env, layout_interner, layout_ids, ptr, cursors, data, layout); + + env.builder.new_build_return(Some(&answer)); + } + NullableWrapped { + nullable_id, + other_tags, + } => { + let switch_block = env.context.append_basic_block(parent, "switch_block"); + let null_block = env.context.append_basic_block(parent, "null_block"); + + let id = get_tag_id(env, layout_interner, parent, &union_layout, tag_value); + + let comparison = env + .builder + .new_build_is_null(tag_value.into_pointer_value(), "is_null"); + + env.builder + .new_build_conditional_branch(comparison, null_block, switch_block); + + { + let mut cases = Vec::with_capacity_in(other_tags.len(), env.arena); + + for i in 0..other_tags.len() + 1 { + if i == nullable_id as usize { + continue; + } + + let block = env.context.append_basic_block(parent, "tag_id_modify"); + env.builder.position_at_end(block); + + // write the "pointer" of the current offset + write_pointer_with_tag_id(env, ptr, offset, extra_offset, union_layout, i); + + let fields = if i >= nullable_id as _ { + other_tags[i - 1] + } else { + other_tags[i] + }; + + let layout = LayoutRepr::struct_(fields); + let basic_type = basic_type_from_layout(env, layout_interner, layout); + + let (width, _) = union_layout.data_size_and_alignment(layout_interner); + + let cursors = Cursors { + offset: extra_offset, + extra_offset: env.builder.new_build_int_add( + extra_offset, + env.ptr_int().const_int(width as _, false), + "new_offset", + ), + }; + + let tag_value = tag_pointer_clear_tag_id(env, tag_value.into_pointer_value()); + let data = + load_tag_data(env, layout_interner, union_layout, tag_value, basic_type); + + let answer = + build_clone(env, layout_interner, layout_ids, ptr, cursors, data, layout); + + env.builder.new_build_return(Some(&answer)); + + cases.push((id.get_type().const_int(i as u64, false), block)); + } + + env.builder.position_at_end(switch_block); + + match cases.pop() { + Some((_, default)) => { + env.builder.new_build_switch(id, default, &cases); + } + None => { + // we're serializing an empty tag union; this code is effectively unreachable + env.builder.new_build_unreachable(); + } + } + } + + { + env.builder.position_at_end(null_block); + + let value = env.ptr_int().const_zero(); + build_copy(env, ptr, offset, value.into()); + + env.builder.new_build_return(Some(&extra_offset)); + } + } + NullableUnwrapped { other_fields, .. } => { + let other_block = env.context.append_basic_block(parent, "other_block"); + let null_block = env.context.append_basic_block(parent, "null_block"); + + let comparison = env + .builder + .new_build_is_null(tag_value.into_pointer_value(), "is_null"); + + env.builder + .new_build_conditional_branch(comparison, null_block, other_block); + + { + env.builder.position_at_end(null_block); + + let value = env.ptr_int().const_zero(); + build_copy(env, ptr, offset, value.into()); + + env.builder.new_build_return(Some(&extra_offset)); + } + + { + env.builder.position_at_end(other_block); + + // write the "pointer" af the current offset + build_copy(env, ptr, offset, extra_offset.into()); + + let layout = LayoutRepr::struct_(other_fields); + let basic_type = basic_type_from_layout(env, layout_interner, layout); + + let cursors = Cursors { + offset: extra_offset, + extra_offset: env.builder.new_build_int_add( + extra_offset, + env.ptr_int() + .const_int(layout.stack_size(layout_interner) as _, false), + "new_offset", + ), + }; + + let data = load_tag_data( + env, + layout_interner, + union_layout, + tag_value.into_pointer_value(), + basic_type, + ); + + let answer = + build_clone(env, layout_interner, layout_ids, ptr, cursors, data, layout); + + env.builder.new_build_return(Some(&answer)); + } + } + } +} + +fn write_pointer_with_tag_id<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + ptr: PointerValue<'ctx>, + offset: IntValue<'ctx>, + extra_offset: IntValue<'ctx>, + union_layout: UnionLayout<'a>, + tag_id: usize, +) { + if union_layout.stores_tag_id_in_pointer(env.target_info) { + // first, store tag id as u32 + let tag_id_intval = env.context.i32_type().const_int(tag_id as _, false); + build_copy(env, ptr, offset, tag_id_intval.into()); + + // increment offset by 4 + let four = env.ptr_int().const_int(4, false); + let offset = env.builder.new_build_int_add(offset, four, ""); + + // cast to u32 + let extra_offset = env + .builder + .new_build_int_cast(extra_offset, env.context.i32_type(), ""); + + build_copy(env, ptr, offset, extra_offset.into()); + } else { + build_copy(env, ptr, offset, extra_offset.into()); + } +} + +fn build_copy<'ctx>( + env: &Env<'_, 'ctx, '_>, + ptr: PointerValue<'ctx>, + offset: IntValue<'ctx>, + value: BasicValueEnum<'ctx>, +) -> IntValue<'ctx> { + let ptr = unsafe { + env.builder.new_build_in_bounds_gep( + env.context.i8_type(), + ptr, + &[offset], + "at_current_offset", + ) + }; + + let ptr_type = value.get_type().ptr_type(AddressSpace::default()); + let ptr = env + .builder + .new_build_pointer_cast(ptr, ptr_type, "cast_ptr_type"); + + env.builder.new_build_store(ptr, value); + + let width = value.get_type().size_of().unwrap(); + env.builder.new_build_int_add(offset, width, "new_offset") +} + +fn build_clone_builtin<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + layout_ids: &mut LayoutIds<'a>, + ptr: PointerValue<'ctx>, + cursors: Cursors<'ctx>, + value: BasicValueEnum<'ctx>, + builtin: Builtin<'a>, +) -> IntValue<'ctx> { + use Builtin::*; + + match builtin { + Int(_) | Float(_) | Bool | Decimal => { + build_copy(env, ptr, cursors.offset, value); + + cursors.extra_offset + } + + Builtin::Str => { + // + + call_str_bitcode_fn( + env, + &[value], + &[ + ptr.into(), + cursors.offset.into(), + cursors.extra_offset.into(), + ], + crate::llvm::bitcode::BitcodeReturns::Basic, + bitcode::STR_CLONE_TO, + ) + .into_int_value() + } + Builtin::List(elem) => { + let bd = env.builder; + + let list = value.into_struct_value(); + let (elements, len, _cap) = build_list::destructure(env.builder, list); + + let mut offset = cursors.offset; + + // we only copy the elements we actually have (and skip extra capacity) + offset = build_copy(env, ptr, offset, cursors.extra_offset.into()); + offset = build_copy(env, ptr, offset, len.into()); + offset = build_copy(env, ptr, offset, len.into()); + + let (element_width, _element_align) = layout_interner.stack_size_and_alignment(elem); + let element_width = env.ptr_int().const_int(element_width as _, false); + + let elements_width = bd.new_build_int_mul(element_width, len, "elements_width"); + + // We clone the elements into the extra_offset address. + let _ = offset; + let elements_start_offset = cursors.extra_offset; + + if layout_interner.safe_to_memcpy(elem) { + // NOTE we are not actually sure the dest is properly aligned + let dest = pointer_at_offset(bd, env.context.i8_type(), ptr, elements_start_offset); + let src = bd.new_build_pointer_cast( + elements, + env.context.i8_type().ptr_type(AddressSpace::default()), + "to_bytes_pointer", + ); + bd.build_memcpy(dest, 1, src, 1, elements_width).unwrap(); + + bd.new_build_int_add(elements_start_offset, elements_width, "new_offset") + } else { + let element_type = + basic_type_from_layout(env, layout_interner, layout_interner.get_repr(elem)); + let elements = bd.new_build_pointer_cast( + elements, + element_type.ptr_type(AddressSpace::default()), + "elements", + ); + + // if the element has any pointers, we clone them to this offset + let rest_offset = bd.new_build_alloca(env.ptr_int(), "rest_offset"); + + let element_stack_size = env + .ptr_int() + .const_int(layout_interner.stack_size(elem) as u64, false); + let rest_start_offset = bd.new_build_int_add( + cursors.extra_offset, + bd.new_build_int_mul(len, element_stack_size, "elements_width"), + "rest_start_offset", + ); + bd.new_build_store(rest_offset, rest_start_offset); + + let body = |layout_interner: &STLayoutInterner<'a>, index, element| { + let current_offset = + bd.new_build_int_mul(element_stack_size, index, "current_offset"); + let current_offset = bd.new_build_int_add( + elements_start_offset, + current_offset, + "current_offset", + ); + let current_extra_offset = + bd.new_build_load(env.ptr_int(), rest_offset, "element_offset"); + + let offset = current_offset; + let extra_offset = current_extra_offset.into_int_value(); + + let cursors = Cursors { + offset, + extra_offset, + }; + + let elem_layout = layout_interner.get_repr(elem); + let new_offset = build_clone( + env, + layout_interner, + layout_ids, + ptr, + cursors, + element, + elem_layout, + ); + + bd.new_build_store(rest_offset, new_offset); + }; + + let parent = env + .builder + .get_insert_block() + .and_then(|b| b.get_parent()) + .unwrap(); + + incrementing_elem_loop( + env, + layout_interner, + parent, + elem, + elements, + len, + "index", + body, + ); + + bd.new_build_load(env.ptr_int(), rest_offset, "rest_start_offset") + .into_int_value() + } + } + } +} diff --git a/crates/compiler/gen_llvm/src/llvm/externs.rs b/crates/compiler/gen_llvm/src/llvm/externs.rs new file mode 100644 index 0000000000..c79a1a8e23 --- /dev/null +++ b/crates/compiler/gen_llvm/src/llvm/externs.rs @@ -0,0 +1,298 @@ +use crate::llvm::bitcode::call_void_bitcode_fn; +use crate::llvm::build::{add_func, get_panic_msg_ptr, get_panic_tag_ptr, BuilderExt, C_CALL_CONV}; +use crate::llvm::build::{CCReturn, Env, FunctionSpec}; +use crate::llvm::convert::zig_str_type; +use inkwell::module::Linkage; +use inkwell::types::BasicType; +use inkwell::AddressSpace; +use roc_builtins::bitcode; + +use super::build::get_sjlj_buffer; +use super::intrinsics::LLVM_LONGJMP; + +/// Define functions for roc_alloc, roc_realloc, and roc_dealloc +/// which use libc implementations (malloc, realloc, and free) +pub fn add_default_roc_externs(env: &Env<'_, '_, '_>) { + let ctx = env.context; + let module = env.module; + let builder = env.builder; + + let usize_type = env.ptr_int(); + let i8_ptr_type = ctx.i8_type().ptr_type(AddressSpace::default()); + + match env.mode { + super::build::LlvmBackendMode::CliTest => { + // expose this function + if let Some(fn_val) = module.get_function("set_shared_buffer") { + fn_val.set_linkage(Linkage::External); + } + } + _ => { + // remove this function from the module + if let Some(fn_val) = module.get_function("set_shared_buffer") { + unsafe { fn_val.delete() }; + } + } + } + + if !env.mode.has_host() { + // roc_alloc + { + // The type of this function (but not the implementation) should have + // already been defined by the builtins, which rely on it. + let fn_val = module.get_function("roc_alloc").unwrap(); + fn_val.set_linkage(Linkage::Internal); + + let mut params = fn_val.get_param_iter(); + let size_arg = params.next().unwrap(); + let _alignment_arg = params.next().unwrap(); + + debug_assert!(params.next().is_none()); + + // Add a basic block for the entry point + let entry = ctx.append_basic_block(fn_val, "entry"); + + builder.position_at_end(entry); + + // Call libc malloc() + let retval = builder + .build_array_malloc(ctx.i8_type(), size_arg.into_int_value(), "call_malloc") + .unwrap(); + + builder.new_build_return(Some(&retval)); + + if cfg!(debug_assertions) { + crate::llvm::build::verify_fn(fn_val); + } + } + + // roc_realloc + { + let libc_realloc_val = { + let fn_spec = FunctionSpec::cconv( + env, + CCReturn::Return, + Some(i8_ptr_type.as_basic_type_enum()), + &[ + // ptr: *void + i8_ptr_type.into(), + // size: usize + usize_type.into(), + ], + ); + let fn_val = add_func(env.context, module, "realloc", fn_spec, Linkage::External); + + let mut params = fn_val.get_param_iter(); + let ptr_arg = params.next().unwrap(); + let size_arg = params.next().unwrap(); + + debug_assert!(params.next().is_none()); + + ptr_arg.set_name("ptr"); + size_arg.set_name("size"); + + if cfg!(debug_assertions) { + crate::llvm::build::verify_fn(fn_val); + } + + fn_val + }; + + // The type of this function (but not the implementation) should have + // already been defined by the builtins, which rely on it. + let fn_val = module.get_function("roc_realloc").unwrap(); + let mut params = fn_val.get_param_iter(); + let ptr_arg = params.next().unwrap(); + let new_size_arg = params.next().unwrap(); + let _old_size_arg = params.next().unwrap(); + let _alignment_arg = params.next().unwrap(); + + debug_assert!(params.next().is_none()); + + // Add a basic block for the entry point + let entry = ctx.append_basic_block(fn_val, "entry"); + + builder.position_at_end(entry); + + // Call libc realloc() + let call = builder.new_build_call( + libc_realloc_val, + &[ptr_arg.into(), new_size_arg.into()], + "call_libc_realloc", + ); + + call.set_call_convention(C_CALL_CONV); + + let retval = call.try_as_basic_value().left().unwrap(); + + builder.new_build_return(Some(&retval)); + + if cfg!(debug_assertions) { + crate::llvm::build::verify_fn(fn_val); + } + } + + // roc_dealloc + { + // The type of this function (but not the implementation) should have + // already been defined by the builtins, which rely on it. + let fn_val = module.get_function("roc_dealloc").unwrap(); + fn_val.set_linkage(Linkage::Internal); + + let mut params = fn_val.get_param_iter(); + let ptr_arg = params.next().unwrap(); + let _alignment_arg = params.next().unwrap(); + + debug_assert!(params.next().is_none()); + + // Add a basic block for the entry point + let entry = ctx.append_basic_block(fn_val, "entry"); + + builder.position_at_end(entry); + + // Call libc free() + builder.new_build_free(ptr_arg.into_pointer_value()); + + builder.new_build_return(None); + + if cfg!(debug_assertions) { + crate::llvm::build::verify_fn(fn_val); + } + } + + // TODO: generate a valid impl of dbg here. + unreachable_function(env, "roc_dbg"); + + match env.target_info.operating_system { + roc_target::OperatingSystem::Windows => { + // We don't need these functions on Windows + } + _ => { + unreachable_function(env, "roc_getppid"); + unreachable_function(env, "roc_mmap"); + unreachable_function(env, "roc_shm_open"); + } + } + + add_sjlj_roc_panic(env) + } +} + +fn unreachable_function(env: &Env, name: &str) { + // The type of this function (but not the implementation) should have + // already been defined by the builtins, which rely on it. + let fn_val = match env.module.get_function(name) { + Some(f) => f, + None => panic!("extern function {name} is not defined by the builtins"), + }; + + // Add a basic block for the entry point + let entry = env.context.append_basic_block(fn_val, "entry"); + + env.builder.position_at_end(entry); + + env.builder.new_build_unreachable(); + + if cfg!(debug_assertions) { + crate::llvm::build::verify_fn(fn_val); + } +} + +pub fn add_sjlj_roc_panic(env: &Env<'_, '_, '_>) { + let ctx = env.context; + let module = env.module; + let builder = env.builder; + + // roc_panic + { + // The type of this function (but not the implementation) should have + // already been defined by the builtins, which rely on it. + let fn_val = module.get_function("roc_panic").unwrap(); + let mut params = fn_val.get_param_iter(); + let roc_str_arg = params.next().unwrap(); + + // normally, roc_panic is marked as external so it can be provided by the host. But when we + // define it here in LLVM IR, we never want it to be linked by the host (that would + // overwrite which implementation is used. + fn_val.set_linkage(Linkage::Internal); + + let tag_id_arg = params.next().unwrap(); + + debug_assert!(params.next().is_none()); + + let subprogram = env.new_subprogram("roc_panic"); + fn_val.set_subprogram(subprogram); + + env.dibuilder.finalize(); + + // Add a basic block for the entry point + let entry = ctx.append_basic_block(fn_val, "entry"); + + builder.position_at_end(entry); + + // write our error message to the RocStr pointer + { + let loaded_roc_str = match env.target_info.ptr_width() { + roc_target::PtrWidth::Bytes4 => roc_str_arg, + // On 64-bit we pass RocStrs by reference internally + roc_target::PtrWidth::Bytes8 => { + let str_typ = zig_str_type(env); + builder.new_build_load( + str_typ, + roc_str_arg.into_pointer_value(), + "load_roc_str", + ) + } + }; + + env.builder + .new_build_store(get_panic_msg_ptr(env), loaded_roc_str); + } + + // write the panic tag. + // increment by 1, since the tag we'll get from the Roc program is 0-based, + // but we use 0 for marking a successful call. + { + let cast_tag_id = builder.new_build_int_z_extend( + tag_id_arg.into_int_value(), + env.context.i64_type(), + "zext_panic_tag", + ); + + let inc_tag_id = builder.new_build_int_add( + cast_tag_id, + env.context.i64_type().const_int(1, false), + "inc_panic_tag", + ); + + env.builder + .new_build_store(get_panic_tag_ptr(env), inc_tag_id); + } + + build_longjmp_call(env); + + builder.new_build_unreachable(); + + if cfg!(debug_assertions) { + crate::llvm::build::verify_fn(fn_val); + } + } +} + +pub fn build_longjmp_call(env: &Env) { + let jmp_buf = get_sjlj_buffer(env); + if cfg!(target_arch = "aarch64") { + // Call the Zig-linked longjmp: `void longjmp(i32*, i32)` + let tag = env.context.i32_type().const_int(1, false); + let _call = + call_void_bitcode_fn(env, &[jmp_buf.into(), tag.into()], bitcode::UTILS_LONGJMP); + } else { + // Call the LLVM-intrinsic longjmp: `void @llvm.eh.sjlj.longjmp(i8* %setjmp_buf)` + let jmp_buf_i8p = env.builder.new_build_pointer_cast( + jmp_buf, + env.context.i8_type().ptr_type(AddressSpace::default()), + "jmp_buf i8*", + ); + let _call = env.build_intrinsic_call(LLVM_LONGJMP, &[jmp_buf_i8p.into()]); + } +} diff --git a/crates/compiler/gen_llvm/src/llvm/fn_ptr.rs b/crates/compiler/gen_llvm/src/llvm/fn_ptr.rs new file mode 100644 index 0000000000..1d081f0a02 --- /dev/null +++ b/crates/compiler/gen_llvm/src/llvm/fn_ptr.rs @@ -0,0 +1,50 @@ +use bumpalo::collections::CollectIn; +use inkwell::{ + types::{FunctionType, PointerType}, + values::{FunctionValue, PointerValue}, +}; + +use roc_mono::layout::{InLayout, LambdaName, LayoutInterner, STLayoutInterner}; + +use super::{ + build::{ + function_value_by_func_spec, BuilderExt, Env, FuncBorrowSpec, FunctionSpec, RocReturn, + }, + convert::{argument_type_from_layout, basic_type_from_layout}, +}; + +pub fn function_type<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + arguments: &[InLayout<'a>], + return_type: InLayout<'a>, +) -> FunctionType<'ctx> { + let args = arguments + .iter() + .map(|arg| argument_type_from_layout(env, layout_interner, layout_interner.get_repr(*arg))); + + let ret_repr = layout_interner.get_repr(return_type); + let ret = basic_type_from_layout(env, layout_interner, ret_repr); + + let roc_return = RocReturn::from_layout(layout_interner, ret_repr); + + let fn_spec = FunctionSpec::fastcc(env, roc_return, ret, args.collect_in(env.arena)); + + fn_spec.typ +} + +pub fn build<'a, 'ctx>(env: &Env<'a, 'ctx, '_>, lambda_name: LambdaName<'a>) -> PointerValue<'ctx> { + let func_value: FunctionValue<'ctx> = + function_value_by_func_spec(env, FuncBorrowSpec::Erased, lambda_name.name()); + func_value.as_global_value().as_pointer_value() +} + +pub fn cast_to_function_ptr_type<'ctx>( + env: &Env<'_, 'ctx, '_>, + pointer: PointerValue<'ctx>, + function_pointer_type: PointerType<'ctx>, +) -> PointerValue<'ctx> { + env.builder + .new_build_bitcast(pointer, function_pointer_type, "cast_to_function_ptr") + .into_pointer_value() +} diff --git a/crates/compiler/gen_llvm/src/llvm/intrinsics.rs b/crates/compiler/gen_llvm/src/llvm/intrinsics.rs new file mode 100644 index 0000000000..c11866746b --- /dev/null +++ b/crates/compiler/gen_llvm/src/llvm/intrinsics.rs @@ -0,0 +1,174 @@ +use inkwell::{ + context::Context, + module::{Linkage, Module}, + types::FunctionType, + values::FunctionValue, + AddressSpace, +}; +use roc_builtins::{ + bitcode::{FloatWidth, IntWidth, IntrinsicName}, + llvm_int_intrinsic, +}; + +use super::build::{add_func, FunctionSpec}; + +#[allow(dead_code)] +fn add_float_intrinsic<'ctx, F>( + ctx: &'ctx Context, + module: &Module<'ctx>, + name: &IntrinsicName, + construct_type: F, +) where + F: Fn(inkwell::types::FloatType<'ctx>) -> inkwell::types::FunctionType<'ctx>, +{ + macro_rules! check { + ($width:expr, $typ:expr) => { + let full_name = &name[$width]; + + if let Some(_) = module.get_function(full_name) { + // zig defined this function already + } else { + add_intrinsic(ctx, module, full_name, construct_type($typ)); + } + }; + } + + check!(FloatWidth::F32, ctx.f32_type()); + check!(FloatWidth::F64, ctx.f64_type()); +} + +fn add_int_intrinsic<'ctx, F>( + ctx: &'ctx Context, + module: &Module<'ctx>, + name: &IntrinsicName, + construct_type: F, +) where + F: Fn(inkwell::types::IntType<'ctx>) -> inkwell::types::FunctionType<'ctx>, +{ + macro_rules! check { + ($width:expr, $typ:expr) => { + let full_name = &name[$width]; + + if let Some(_) = module.get_function(full_name) { + // zig defined this function already + } else { + add_intrinsic(ctx, module, full_name, construct_type($typ)); + } + }; + } + + check!(IntWidth::U8, ctx.i8_type()); + check!(IntWidth::U16, ctx.i16_type()); + check!(IntWidth::U32, ctx.i32_type()); + check!(IntWidth::U64, ctx.i64_type()); + check!(IntWidth::U128, ctx.i128_type()); + + check!(IntWidth::I8, ctx.i8_type()); + check!(IntWidth::I16, ctx.i16_type()); + check!(IntWidth::I32, ctx.i32_type()); + check!(IntWidth::I64, ctx.i64_type()); + check!(IntWidth::I128, ctx.i128_type()); +} + +pub(crate) fn add_intrinsics<'ctx>(ctx: &'ctx Context, module: &Module<'ctx>) { + // List of all supported LLVM intrinsics: + // + // https://releases.llvm.org/10.0.0/docs/LangRef.html#standard-c-library-intrinsics + let i1_type = ctx.bool_type(); + let i8_type = ctx.i8_type(); + let i8_ptr_type = i8_type.ptr_type(AddressSpace::default()); + let i32_type = ctx.i32_type(); + let void_type = ctx.void_type(); + + if let Some(func) = module.get_function("__muloti4") { + func.set_linkage(Linkage::WeakAny); + } + + add_intrinsic( + ctx, + module, + LLVM_SETJMP, + i32_type.fn_type(&[i8_ptr_type.into()], false), + ); + + add_intrinsic( + ctx, + module, + LLVM_LONGJMP, + void_type.fn_type(&[i8_ptr_type.into()], false), + ); + + add_intrinsic( + ctx, + module, + LLVM_FRAME_ADDRESS, + i8_ptr_type.fn_type(&[i32_type.into()], false), + ); + + add_intrinsic( + ctx, + module, + LLVM_STACK_SAVE, + i8_ptr_type.fn_type(&[], false), + ); + + add_int_intrinsic(ctx, module, &LLVM_ADD_WITH_OVERFLOW, |t| { + let fields = [t.into(), i1_type.into()]; + ctx.struct_type(&fields, false) + .fn_type(&[t.into(), t.into()], false) + }); + + add_int_intrinsic(ctx, module, &LLVM_SUB_WITH_OVERFLOW, |t| { + let fields = [t.into(), i1_type.into()]; + ctx.struct_type(&fields, false) + .fn_type(&[t.into(), t.into()], false) + }); + + add_int_intrinsic(ctx, module, &LLVM_MUL_WITH_OVERFLOW, |t| { + let fields = [t.into(), i1_type.into()]; + ctx.struct_type(&fields, false) + .fn_type(&[t.into(), t.into()], false) + }); + + add_int_intrinsic(ctx, module, &LLVM_ADD_SATURATED, |t| { + t.fn_type(&[t.into(), t.into()], false) + }); + + add_int_intrinsic(ctx, module, &LLVM_SUB_SATURATED, |t| { + t.fn_type(&[t.into(), t.into()], false) + }); +} + +pub static LLVM_MEMSET_I64: &str = "llvm.memset.p0i8.i64"; +pub static LLVM_MEMSET_I32: &str = "llvm.memset.p0i8.i32"; + +pub static LLVM_FRAME_ADDRESS: &str = "llvm.frameaddress.p0"; +pub static LLVM_STACK_SAVE: &str = "llvm.stacksave"; + +pub static LLVM_SETJMP: &str = "llvm.eh.sjlj.setjmp"; +pub static LLVM_LONGJMP: &str = "llvm.eh.sjlj.longjmp"; + +pub const LLVM_ADD_WITH_OVERFLOW: IntrinsicName = + llvm_int_intrinsic!("llvm.sadd.with.overflow", "llvm.uadd.with.overflow"); +pub const LLVM_SUB_WITH_OVERFLOW: IntrinsicName = + llvm_int_intrinsic!("llvm.ssub.with.overflow", "llvm.usub.with.overflow"); +pub const LLVM_MUL_WITH_OVERFLOW: IntrinsicName = + llvm_int_intrinsic!("llvm.smul.with.overflow", "llvm.umul.with.overflow"); + +pub const LLVM_ADD_SATURATED: IntrinsicName = llvm_int_intrinsic!("llvm.sadd.sat", "llvm.uadd.sat"); +pub const LLVM_SUB_SATURATED: IntrinsicName = llvm_int_intrinsic!("llvm.ssub.sat", "llvm.usub.sat"); + +fn add_intrinsic<'ctx>( + context: &Context, + module: &Module<'ctx>, + intrinsic_name: &str, + fn_type: FunctionType<'ctx>, +) -> FunctionValue<'ctx> { + add_func( + context, + module, + intrinsic_name, + FunctionSpec::intrinsic(fn_type), + Linkage::External, + ) +} diff --git a/crates/compiler/gen_llvm/src/llvm/lowlevel.rs b/crates/compiler/gen_llvm/src/llvm/lowlevel.rs new file mode 100644 index 0000000000..ea9fbdecf3 --- /dev/null +++ b/crates/compiler/gen_llvm/src/llvm/lowlevel.rs @@ -0,0 +1,3146 @@ +use inkwell::{ + attributes::{Attribute, AttributeLoc}, + module::Linkage, + types::{BasicType, IntType}, + values::{ + BasicValue, BasicValueEnum, FloatValue, FunctionValue, InstructionOpcode, IntValue, + StructValue, + }, + AddressSpace, IntPredicate, +}; +use morphic_lib::{FuncSpec, UpdateMode}; +use roc_builtins::bitcode::{self, FloatWidth, IntWidth}; +use roc_error_macros::internal_error; +use roc_module::{low_level::LowLevel, symbol::Symbol}; +use roc_mono::{ + ir::HigherOrderLowLevel, + layout::{ + Builtin, InLayout, LambdaSet, Layout, LayoutIds, LayoutInterner, LayoutRepr, + STLayoutInterner, + }, + list_element_layout, +}; +use roc_target::{PtrWidth, TargetInfo}; + +use crate::llvm::{ + bitcode::{ + call_bitcode_fn, call_bitcode_fn_fixing_for_convention, call_list_bitcode_fn, + call_str_bitcode_fn, call_void_bitcode_fn, pass_list_or_string_to_zig_32bit, + pass_string_to_zig_wasm, BitcodeReturns, + }, + build::{ + cast_basic_basic, complex_bitcast_check_size, create_entry_block_alloca, + function_value_by_func_spec, load_roc_value, roc_function_call, tag_pointer_clear_tag_id, + BuilderExt, FuncBorrowSpec, RocReturn, + }, + build_list::{ + list_append_unsafe, list_concat, list_drop_at, list_get_unsafe, list_len, list_map, + list_map2, list_map3, list_map4, list_prepend, list_release_excess_capacity, + list_replace_unsafe, list_reserve, list_sort_with, list_sublist, list_swap, + list_symbol_to_c_abi, list_with_capacity, pass_update_mode, + }, + compare::{generic_eq, generic_neq}, + convert::{ + self, argument_type_from_layout, basic_type_from_layout, zig_num_parse_result_type, + zig_to_int_checked_result_type, + }, + intrinsics::{ + // These instrinsics do not generate calls to libc and are safe to keep. + // If we find that any of them generate calls to libc on some platforms, we need to define them as zig bitcode. + LLVM_ADD_SATURATED, + LLVM_ADD_WITH_OVERFLOW, + LLVM_MUL_WITH_OVERFLOW, + LLVM_SUB_SATURATED, + LLVM_SUB_WITH_OVERFLOW, + }, + refcounting::PointerToRefcount, +}; + +use super::{build::Env, convert::zig_dec_type}; +use super::{ + build::{throw_internal_exception, use_roc_value, FAST_CALL_CONV}, + convert::zig_with_overflow_roc_dec, + scope::Scope, +}; + +pub(crate) fn run_low_level<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + layout_ids: &mut LayoutIds<'a>, + scope: &Scope<'a, 'ctx>, + parent: FunctionValue<'ctx>, + layout: InLayout<'a>, + op: LowLevel, + args: &[Symbol], + update_mode: UpdateMode, +) -> BasicValueEnum<'ctx> { + use LowLevel::*; + + debug_assert!(!op.is_higher_order()); + + macro_rules! arguments { + () => {}; + ($($x:ident),+ $(,)?) => { + // look at that, a usage for if let ... else + let [$($x),+] = match &args { + [$($x),+] => { + [ $(scope.load_symbol($x)),+ ] + } + _ => { + // we could get fancier with reporting here, but this macro is used a bunch + // so I want to keep the expansion small (for now) + internal_error!("lowlevel operation has incorrect number of arguments!") + } + }; + }; + } + + macro_rules! arguments_with_layouts { + () => {}; + ($(($x:ident, $y:ident)),+ $(,)?) => { + // look at that, a usage for if let ... else + let [$(($x, $y)),+] = match &args { + [$($x),+] => { + [ $(scope.load_symbol_and_layout($x)),+ ] + } + _ => { + // we could get fancier with reporting here, but this macro is used a bunch + // so I want to keep the expansion small (for now) + internal_error!("lowlevel operation has incorrect number of arguments!") + } + }; + }; + } + + match op { + StrConcat => { + // Str.concat : Str, Str -> Str + arguments!(string1, string2); + + call_str_bitcode_fn( + env, + &[string1, string2], + &[], + BitcodeReturns::Str, + bitcode::STR_CONCAT, + ) + } + StrJoinWith => { + // Str.joinWith : List Str, Str -> Str + arguments!(list, string); + + match env.target_info.ptr_width() { + PtrWidth::Bytes4 => { + // list and string are both stored as structs on the stack on 32-bit targets + call_str_bitcode_fn( + env, + &[list, string], + &[], + BitcodeReturns::Str, + bitcode::STR_JOIN_WITH, + ) + } + PtrWidth::Bytes8 => { + // on 64-bit targets, strings are stored as pointers, but that is not what zig expects + + call_list_bitcode_fn( + env, + &[list.into_struct_value()], + &[string], + BitcodeReturns::Str, + bitcode::STR_JOIN_WITH, + ) + } + } + } + StrToScalars => { + // Str.toScalars : Str -> List U32 + arguments!(string); + + call_str_bitcode_fn( + env, + &[string], + &[], + BitcodeReturns::List, + bitcode::STR_TO_SCALARS, + ) + } + StrStartsWith => { + // Str.startsWith : Str, Str -> Bool + arguments!(string, prefix); + + call_str_bitcode_fn( + env, + &[string, prefix], + &[], + BitcodeReturns::Basic, + bitcode::STR_STARTS_WITH, + ) + } + StrStartsWithScalar => { + // Str.startsWithScalar : Str, U32 -> Bool + arguments!(string, prefix); + + call_str_bitcode_fn( + env, + &[string], + &[prefix], + BitcodeReturns::Basic, + bitcode::STR_STARTS_WITH_SCALAR, + ) + } + StrEndsWith => { + // Str.startsWith : Str, Str -> Bool + arguments!(string, prefix); + + call_str_bitcode_fn( + env, + &[string, prefix], + &[], + BitcodeReturns::Basic, + bitcode::STR_ENDS_WITH, + ) + } + StrToNum => { + // Str.toNum : Str -> Result (Num *) {} + arguments!(string); + + let number_layout = match layout_interner.get_repr(layout) { + LayoutRepr::Struct(field_layouts) => field_layouts[0], // TODO: why is it sometimes a struct? + _ => unreachable!(), + }; + + // match on the return layout to figure out which zig builtin we need + let intrinsic = match layout_interner.get_repr(number_layout) { + LayoutRepr::Builtin(Builtin::Int(int_width)) => &bitcode::STR_TO_INT[int_width], + LayoutRepr::Builtin(Builtin::Float(float_width)) => { + &bitcode::STR_TO_FLOAT[float_width] + } + LayoutRepr::Builtin(Builtin::Decimal) => bitcode::DEC_FROM_STR, + _ => unreachable!(), + }; + + use roc_target::Architecture::*; + let result = match env.target_info.architecture { + Aarch32 | X86_32 => { + let zig_function = env.module.get_function(intrinsic).unwrap(); + let zig_function_type = zig_function.get_type(); + + match zig_function_type.get_return_type() { + Some(_) => call_str_bitcode_fn( + env, + &[string], + &[], + BitcodeReturns::Basic, + intrinsic, + ), + None => { + let return_type_name = match layout_interner.get_repr(number_layout) { + LayoutRepr::Builtin(Builtin::Int(int_width)) => { + int_width.type_name() + } + LayoutRepr::Builtin(Builtin::Decimal) => { + // zig picks 128 for dec.RocDec + "i128" + } + _ => unreachable!(), + }; + + let return_type = zig_num_parse_result_type(env, return_type_name); + + let zig_return_alloca = create_entry_block_alloca( + env, + parent, + return_type.into(), + "str_to_num", + ); + + let (a, b) = + pass_list_or_string_to_zig_32bit(env, string.into_struct_value()); + + call_void_bitcode_fn( + env, + &[zig_return_alloca.into(), a.into(), b.into()], + intrinsic, + ); + + let roc_return_type = basic_type_from_layout( + env, + layout_interner, + layout_interner.get_repr(layout), + ) + .ptr_type(AddressSpace::default()); + + let roc_return_alloca = env.builder.new_build_pointer_cast( + zig_return_alloca, + roc_return_type, + "cast_to_roc", + ); + + load_roc_value( + env, + layout_interner, + layout_interner.get_repr(layout), + roc_return_alloca, + "str_to_num_result", + ) + } + } + } + Aarch64 | X86_64 => { + let (type_name, width) = { + match layout_interner.get_repr(number_layout) { + LayoutRepr::Builtin(Builtin::Int(int_width)) => { + (int_width.type_name(), int_width.stack_size()) + } + LayoutRepr::Builtin(Builtin::Decimal) => { + // zig picks 128 for dec.RocDec + ("i128", 16) + } + LayoutRepr::Builtin(Builtin::Float(float_width)) => { + (float_width.type_name(), float_width.stack_size()) + } + _ => { + unreachable!("other layout types are non-numeric") + } + } + }; + + use roc_target::OperatingSystem::*; + let cc_return_by_pointer = match env.target_info.operating_system { + Windows => { + // there is just one return register on Windows + (width + 1) as usize > env.target_info.ptr_size() + } + _ => { + // on other systems we have two return registers + (width + 1) as usize > 2 * env.target_info.ptr_size() + } + }; + + if cc_return_by_pointer { + let bitcode_return_type = zig_num_parse_result_type(env, type_name); + + call_bitcode_fn_fixing_for_convention( + env, + layout_interner, + bitcode_return_type, + &[string], + layout, + intrinsic, + ) + } else { + call_bitcode_fn(env, &[string], intrinsic) + } + } + Wasm32 => { + let return_type_name = match layout_interner.get_repr(number_layout) { + LayoutRepr::Builtin(Builtin::Float(float_width)) => float_width.type_name(), + LayoutRepr::Builtin(Builtin::Int(int_width)) => int_width.type_name(), + LayoutRepr::Builtin(Builtin::Decimal) => { + // zig picks 128 for dec.RocDec + "i128" + } + _ => unreachable!(), + }; + + let return_type = zig_num_parse_result_type(env, return_type_name); + + let zig_return_alloca = + create_entry_block_alloca(env, parent, return_type.into(), "str_to_num"); + + call_void_bitcode_fn( + env, + &[ + zig_return_alloca.into(), + pass_string_to_zig_wasm(env, string).into(), + ], + intrinsic, + ); + + let roc_return_type = basic_type_from_layout( + env, + layout_interner, + layout_interner.get_repr(layout), + ) + .ptr_type(AddressSpace::default()); + + let roc_return_alloca = env.builder.new_build_pointer_cast( + zig_return_alloca, + roc_return_type, + "cast_to_roc", + ); + + load_roc_value( + env, + layout_interner, + layout_interner.get_repr(layout), + roc_return_alloca, + "str_to_num_result", + ) + } + }; + + // zig passes the result as a packed integer sometimes, instead of a struct. So we cast if needed. + // We check the type as expected in an argument position, since that is how we actually will use it. + let expected_type = + argument_type_from_layout(env, layout_interner, layout_interner.get_repr(layout)); + let actual_type = result.get_type(); + + if expected_type != actual_type { + complex_bitcast_check_size(env, result, expected_type, "str_to_num_cast") + } else { + result + } + } + StrFromInt => { + // Str.fromInt : Int -> Str + debug_assert_eq!(args.len(), 1); + + let (int, int_layout) = scope.load_symbol_and_layout(&args[0]); + let int = int.into_int_value(); + + let int_width = match layout_interner.get_repr(int_layout) { + LayoutRepr::Builtin(Builtin::Int(int_width)) => int_width, + _ => unreachable!(), + }; + + call_str_bitcode_fn( + env, + &[], + &[int.into()], + BitcodeReturns::Str, + &bitcode::STR_FROM_INT[int_width], + ) + } + StrFromFloat => { + // Str.fromFloat : Frac * -> Str + debug_assert_eq!(args.len(), 1); + + let (float, float_layout) = scope.load_symbol_and_layout(&args[0]); + + let float_width = match layout_interner.get_repr(float_layout) { + LayoutRepr::Builtin(Builtin::Float(float_width)) => float_width, + _ => unreachable!(), + }; + + call_str_bitcode_fn( + env, + &[], + &[float], + BitcodeReturns::Str, + &bitcode::STR_FROM_FLOAT[float_width], + ) + } + StrFromUtf8Range => { + let result_type = env.module.get_struct_type("str.FromUtf8Result").unwrap(); + let result_ptr = env + .builder + .new_build_alloca(result_type, "alloca_utf8_validate_bytes_result"); + + use roc_target::Architecture::*; + match env.target_info.architecture { + Aarch32 | X86_32 => { + arguments!(list, start, count); + let (a, b) = pass_list_or_string_to_zig_32bit(env, list.into_struct_value()); + + call_void_bitcode_fn( + env, + &[ + result_ptr.into(), + a.into(), + b.into(), + start, + count, + pass_update_mode(env, update_mode), + ], + bitcode::STR_FROM_UTF8_RANGE, + ); + } + Aarch64 | X86_64 | Wasm32 => { + arguments!(_list, start, count); + + // we use the symbol here instead + let list = args[0]; + + call_void_bitcode_fn( + env, + &[ + result_ptr.into(), + list_symbol_to_c_abi(env, scope, list).into(), + start, + count, + pass_update_mode(env, update_mode), + ], + bitcode::STR_FROM_UTF8_RANGE, + ); + } + } + + crate::llvm::build_str::decode_from_utf8_result(env, layout_interner, result_ptr) + } + StrToUtf8 => { + // Str.fromInt : Str -> List U8 + arguments!(string); + + call_str_bitcode_fn( + env, + &[string], + &[], + BitcodeReturns::List, + bitcode::STR_TO_UTF8, + ) + } + StrRepeat => { + // Str.repeat : Str, Nat -> Str + arguments!(string, count); + + call_str_bitcode_fn( + env, + &[string], + &[count], + BitcodeReturns::Str, + bitcode::STR_REPEAT, + ) + } + StrSplit => { + // Str.split : Str, Str -> List Str + arguments!(string, delimiter); + + call_str_bitcode_fn( + env, + &[string, delimiter], + &[], + BitcodeReturns::List, + bitcode::STR_SPLIT, + ) + } + StrIsEmpty => { + // Str.isEmpty : Str -> Str + arguments!(string); + + // the builtin will always return an u64 + let length = call_str_bitcode_fn( + env, + &[string], + &[], + BitcodeReturns::Basic, + bitcode::STR_NUMBER_OF_BYTES, + ) + .into_int_value(); + + // cast to the appropriate usize of the current build + let byte_count = env.builder.new_build_int_cast_sign_flag( + length, + env.ptr_int(), + false, + "len_as_usize", + ); + + let is_zero = env.builder.new_build_int_compare( + IntPredicate::EQ, + byte_count, + env.ptr_int().const_zero(), + "str_len_is_zero", + ); + BasicValueEnum::IntValue(is_zero) + } + StrCountGraphemes => { + // Str.countGraphemes : Str -> Nat + arguments!(string); + + call_str_bitcode_fn( + env, + &[string], + &[], + BitcodeReturns::Basic, + bitcode::STR_COUNT_GRAPEHEME_CLUSTERS, + ) + } + StrGetScalarUnsafe => { + // Str.getScalarUnsafe : Str, Nat -> { bytesParsed : Nat, scalar : U32 } + arguments!(string, index); + + let roc_return_type = + basic_type_from_layout(env, layout_interner, layout_interner.get_repr(layout)); + + use roc_target::Architecture::*; + use roc_target::OperatingSystem::*; + match env.target_info { + TargetInfo { + operating_system: Windows, + .. + } => { + let result = env.builder.new_build_alloca(roc_return_type, "result"); + + call_void_bitcode_fn( + env, + &[result.into(), string, index], + bitcode::STR_GET_SCALAR_UNSAFE, + ); + + let cast_result = env.builder.new_build_pointer_cast( + result, + roc_return_type.ptr_type(AddressSpace::default()), + "cast", + ); + + env.builder + .new_build_load(roc_return_type, cast_result, "load_result") + } + TargetInfo { + architecture: Wasm32, + .. + } => { + let result = env.builder.new_build_alloca(roc_return_type, "result"); + + call_void_bitcode_fn( + env, + &[ + result.into(), + pass_string_to_zig_wasm(env, string).into(), + index, + ], + bitcode::STR_GET_SCALAR_UNSAFE, + ); + + let cast_result = env.builder.new_build_pointer_cast( + result, + roc_return_type.ptr_type(AddressSpace::default()), + "cast", + ); + + env.builder + .new_build_load(roc_return_type, cast_result, "load_result") + } + TargetInfo { + operating_system: Unix, + .. + } => { + let result = call_str_bitcode_fn( + env, + &[string], + &[index], + BitcodeReturns::Basic, + bitcode::STR_GET_SCALAR_UNSAFE, + ); + + // zig will pad the struct to the alignment boundary, or bitpack it on 32-bit + // targets. So we have to cast it to the format that the roc code expects + let alloca = env + .builder + .new_build_alloca(result.get_type(), "to_roc_record"); + env.builder.new_build_store(alloca, result); + + env.builder + .new_build_load(roc_return_type, alloca, "to_roc_record") + } + TargetInfo { + operating_system: Wasi, + .. + } => unimplemented!(), + } + } + StrCountUtf8Bytes => { + // Str.countUtf8Bytes : Str -> Nat + arguments!(string); + + call_str_bitcode_fn( + env, + &[string], + &[], + BitcodeReturns::Basic, + bitcode::STR_COUNT_UTF8_BYTES, + ) + } + StrGetCapacity => { + // Str.capacity : Str -> Nat + arguments!(string); + + call_bitcode_fn(env, &[string], bitcode::STR_CAPACITY) + } + StrSubstringUnsafe => { + // Str.substringUnsafe : Str, Nat, Nat -> Str + arguments!(string, start, length); + + call_str_bitcode_fn( + env, + &[string], + &[start, length], + BitcodeReturns::Str, + bitcode::STR_SUBSTRING_UNSAFE, + ) + } + StrReserve => { + // Str.reserve : Str, Nat -> Str + arguments!(string, capacity); + + call_str_bitcode_fn( + env, + &[string], + &[capacity], + BitcodeReturns::Str, + bitcode::STR_RESERVE, + ) + } + StrReleaseExcessCapacity => { + // Str.releaseExcessCapacity: Str -> Str + arguments!(string); + + call_str_bitcode_fn( + env, + &[string], + &[], + BitcodeReturns::Str, + bitcode::STR_RELEASE_EXCESS_CAPACITY, + ) + } + StrAppendScalar => { + // Str.appendScalar : Str, U32 -> Str + arguments!(string, capacity); + + call_str_bitcode_fn( + env, + &[string], + &[capacity], + BitcodeReturns::Str, + bitcode::STR_APPEND_SCALAR, + ) + } + StrTrim => { + // Str.trim : Str -> Str + arguments!(string); + + call_str_bitcode_fn(env, &[string], &[], BitcodeReturns::Str, bitcode::STR_TRIM) + } + StrTrimStart => { + // Str.trim : Str -> Str + arguments!(string); + + call_str_bitcode_fn( + env, + &[string], + &[], + BitcodeReturns::Str, + bitcode::STR_TRIM_START, + ) + } + StrTrimEnd => { + // Str.trim : Str -> Str + arguments!(string); + + call_str_bitcode_fn( + env, + &[string], + &[], + BitcodeReturns::Str, + bitcode::STR_TRIM_END, + ) + } + StrWithCapacity => { + // Str.withCapacity : Nat -> Str + arguments!(str_len); + + call_str_bitcode_fn( + env, + &[], + &[str_len], + BitcodeReturns::Str, + bitcode::STR_WITH_CAPACITY, + ) + } + StrGraphemes => { + // Str.graphemes : Str -> List Str + arguments!(string); + + call_str_bitcode_fn( + env, + &[string], + &[], + BitcodeReturns::List, + bitcode::STR_GRAPHEMES, + ) + } + ListLen => { + // List.len : List * -> Nat + arguments!(list); + + list_len(env.builder, list.into_struct_value()).into() + } + ListGetCapacity => { + // List.capacity: List a -> Nat + arguments!(list); + + call_list_bitcode_fn( + env, + &[list.into_struct_value()], + &[], + BitcodeReturns::Basic, + bitcode::LIST_CAPACITY, + ) + } + ListWithCapacity => { + // List.withCapacity : Nat -> List a + arguments!(list_len); + + let result_layout = layout; + list_with_capacity( + env, + layout_interner, + list_len.into_int_value(), + list_element_layout!(layout_interner, result_layout), + ) + } + ListConcat => { + debug_assert_eq!(args.len(), 2); + + let (first_list, list_layout) = scope.load_symbol_and_layout(&args[0]); + + let second_list = scope.load_symbol(&args[1]); + + let element_layout = list_element_layout!(layout_interner, list_layout); + + list_concat( + env, + layout_interner, + first_list, + second_list, + element_layout, + ) + } + ListAppendUnsafe => { + // List.appendUnsafe : List elem, elem -> List elem + debug_assert_eq!(args.len(), 2); + + let original_wrapper = scope.load_symbol(&args[0]).into_struct_value(); + let (elem, elem_layout) = scope.load_symbol_and_layout(&args[1]); + + list_append_unsafe(env, layout_interner, original_wrapper, elem, elem_layout) + } + ListPrepend => { + // List.prepend : List elem, elem -> List elem + debug_assert_eq!(args.len(), 2); + + let original_wrapper = scope.load_symbol(&args[0]).into_struct_value(); + let (elem, elem_layout) = scope.load_symbol_and_layout(&args[1]); + + list_prepend(env, layout_interner, original_wrapper, elem, elem_layout) + } + ListReserve => { + // List.reserve : List elem, Nat -> List elem + debug_assert_eq!(args.len(), 2); + + let (list, list_layout) = scope.load_symbol_and_layout(&args[0]); + let element_layout = list_element_layout!(layout_interner, list_layout); + let spare = scope.load_symbol(&args[1]); + + list_reserve( + env, + layout_interner, + list, + spare, + element_layout, + update_mode, + ) + } + ListReleaseExcessCapacity => { + // List.releaseExcessCapacity: List elem -> List elem + debug_assert_eq!(args.len(), 1); + + let (list, list_layout) = scope.load_symbol_and_layout(&args[0]); + let element_layout = list_element_layout!(layout_interner, list_layout); + + list_release_excess_capacity(env, layout_interner, list, element_layout, update_mode) + } + ListSwap => { + // List.swap : List elem, Nat, Nat -> List elem + debug_assert_eq!(args.len(), 3); + + let (list, list_layout) = scope.load_symbol_and_layout(&args[0]); + let original_wrapper = list.into_struct_value(); + + let index_1 = scope.load_symbol(&args[1]); + let index_2 = scope.load_symbol(&args[2]); + + let element_layout = list_element_layout!(layout_interner, list_layout); + list_swap( + env, + layout_interner, + original_wrapper, + index_1.into_int_value(), + index_2.into_int_value(), + element_layout, + update_mode, + ) + } + ListSublist => { + debug_assert_eq!(args.len(), 3); + + let (list, list_layout) = scope.load_symbol_and_layout(&args[0]); + let original_wrapper = list.into_struct_value(); + + let start = scope.load_symbol(&args[1]); + let len = scope.load_symbol(&args[2]); + + let element_layout = list_element_layout!(layout_interner, list_layout); + list_sublist( + env, + layout_interner, + layout_ids, + original_wrapper, + start.into_int_value(), + len.into_int_value(), + element_layout, + ) + } + ListDropAt => { + // List.dropAt : List elem, Nat -> List elem + debug_assert_eq!(args.len(), 2); + + let (list, list_layout) = scope.load_symbol_and_layout(&args[0]); + let original_wrapper = list.into_struct_value(); + + let count = scope.load_symbol(&args[1]); + + let element_layout = list_element_layout!(layout_interner, list_layout); + list_drop_at( + env, + layout_interner, + layout_ids, + original_wrapper, + count.into_int_value(), + element_layout, + ) + } + StrGetUnsafe => { + // Str.getUnsafe : Str, Nat -> u8 + arguments!(wrapper_struct, elem_index); + + call_str_bitcode_fn( + env, + &[wrapper_struct], + &[elem_index], + BitcodeReturns::Basic, + bitcode::STR_GET_UNSAFE, + ) + } + ListGetUnsafe => { + // List.getUnsafe : List elem, Nat -> elem + arguments_with_layouts!((wrapper_struct, list_layout), (element_index, _l)); + + list_get_unsafe( + env, + layout_interner, + list_element_layout!(layout_interner, list_layout), + element_index.into_int_value(), + wrapper_struct.into_struct_value(), + ) + } + ListReplaceUnsafe => { + arguments_with_layouts!((list, _l1), (index, _l2), (element, element_layout)); + + list_replace_unsafe( + env, + layout_interner, + layout_ids, + list, + index.into_int_value(), + element, + element_layout, + update_mode, + ) + } + ListIsUnique => { + // List.isUnique : List a -> Bool + arguments!(list); + + call_list_bitcode_fn( + env, + &[list.into_struct_value()], + &[], + BitcodeReturns::Basic, + bitcode::LIST_IS_UNIQUE, + ) + } + NumToStr => { + // Num.toStr : Num a -> Str + arguments_with_layouts!((num, num_layout)); + + match layout_interner.get_repr(num_layout) { + LayoutRepr::Builtin(Builtin::Int(int_width)) => { + let int = num.into_int_value(); + + call_str_bitcode_fn( + env, + &[], + &[int.into()], + BitcodeReturns::Str, + &bitcode::STR_FROM_INT[int_width], + ) + } + LayoutRepr::Builtin(Builtin::Float(_float_width)) => { + let (float, float_layout) = scope.load_symbol_and_layout(&args[0]); + + let float_width = match layout_interner.get_repr(float_layout) { + LayoutRepr::Builtin(Builtin::Float(float_width)) => float_width, + _ => unreachable!(), + }; + + call_str_bitcode_fn( + env, + &[], + &[float], + BitcodeReturns::Str, + &bitcode::STR_FROM_FLOAT[float_width], + ) + } + LayoutRepr::Builtin(Builtin::Decimal) => dec_to_str(env, num), + _ => unreachable!(), + } + } + NumAbs + | NumNeg + | NumRound + | NumSqrtUnchecked + | NumLogUnchecked + | NumSin + | NumCos + | NumTan + | NumCeiling + | NumFloor + | NumToFrac + | NumIsNan + | NumIsInfinite + | NumIsFinite + | NumAtan + | NumAcos + | NumAsin + | NumToIntChecked + | NumCountLeadingZeroBits + | NumCountTrailingZeroBits + | NumCountOneBits => { + arguments_with_layouts!((arg, arg_layout)); + + match layout_interner.get_repr(arg_layout) { + LayoutRepr::Builtin(arg_builtin) => { + use roc_mono::layout::Builtin::*; + + match arg_builtin { + Int(int_width) => { + let int_type = convert::int_type_from_int_width(env, int_width); + build_int_unary_op( + env, + layout_interner, + parent, + arg.into_int_value(), + int_width, + int_type, + op, + layout, + ) + } + Float(float_width) => build_float_unary_op( + env, + layout_interner, + layout, + arg.into_float_value(), + op, + float_width, + ), + Decimal => { + build_dec_unary_op(env, layout_interner, parent, arg, layout, op) + } + + _ => { + unreachable!("Compiler bug: tried to run numeric operation {:?} on invalid builtin layout: ({:?})", op, arg_layout); + } + } + } + _ => { + unreachable!( + "Compiler bug: tried to run numeric operation {:?} on invalid layout: {:?}", + op, arg_layout + ); + } + } + } + NumBytesToU16 => { + arguments!(list, position); + + call_list_bitcode_fn( + env, + &[list.into_struct_value()], + &[position], + BitcodeReturns::Basic, + bitcode::NUM_BYTES_TO_U16, + ) + } + NumBytesToU32 => { + arguments!(list, position); + + call_list_bitcode_fn( + env, + &[list.into_struct_value()], + &[position], + BitcodeReturns::Basic, + bitcode::NUM_BYTES_TO_U32, + ) + } + NumBytesToU64 => { + arguments!(list, position); + + call_list_bitcode_fn( + env, + &[list.into_struct_value()], + &[position], + BitcodeReturns::Basic, + bitcode::NUM_BYTES_TO_U64, + ) + } + NumBytesToU128 => { + arguments!(list, position); + + call_list_bitcode_fn( + env, + &[list.into_struct_value()], + &[position], + BitcodeReturns::Basic, + bitcode::NUM_BYTES_TO_U128, + ) + } + NumCompare => { + arguments_with_layouts!((lhs_arg, lhs_layout), (rhs_arg, rhs_layout)); + + use inkwell::FloatPredicate; + match ( + layout_interner.get_repr(lhs_layout), + layout_interner.get_repr(rhs_layout), + ) { + (LayoutRepr::Builtin(lhs_builtin), LayoutRepr::Builtin(rhs_builtin)) + if lhs_builtin == rhs_builtin => + { + use roc_mono::layout::Builtin::*; + + let tag_eq = env.context.i8_type().const_int(0_u64, false); + let tag_gt = env.context.i8_type().const_int(1_u64, false); + let tag_lt = env.context.i8_type().const_int(2_u64, false); + + match lhs_builtin { + Int(int_width) => { + let are_equal = env.builder.new_build_int_compare( + IntPredicate::EQ, + lhs_arg.into_int_value(), + rhs_arg.into_int_value(), + "int_eq", + ); + + let predicate = if int_width.is_signed() { + IntPredicate::SLT + } else { + IntPredicate::ULT + }; + + let is_less_than = env.builder.new_build_int_compare( + predicate, + lhs_arg.into_int_value(), + rhs_arg.into_int_value(), + "int_compare", + ); + + let step1 = env.builder.new_build_select( + is_less_than, + tag_lt, + tag_gt, + "lt_or_gt", + ); + + env.builder.new_build_select( + are_equal, + tag_eq, + step1.into_int_value(), + "lt_or_gt", + ) + } + Float(_) => { + let are_equal = env.builder.new_build_float_compare( + FloatPredicate::OEQ, + lhs_arg.into_float_value(), + rhs_arg.into_float_value(), + "float_eq", + ); + let is_less_than = env.builder.new_build_float_compare( + FloatPredicate::OLT, + lhs_arg.into_float_value(), + rhs_arg.into_float_value(), + "float_compare", + ); + + let step1 = env.builder.new_build_select( + is_less_than, + tag_lt, + tag_gt, + "lt_or_gt", + ); + + env.builder.new_build_select( + are_equal, + tag_eq, + step1.into_int_value(), + "lt_or_gt", + ) + } + Decimal => { + // + call_bitcode_fn( + env, + &[lhs_arg, rhs_arg], + &bitcode::NUM_COMPARE[IntWidth::I128], + ) + } + + _ => { + unreachable!("Compiler bug: tried to run numeric operation {:?} on invalid builtin layout: ({:?})", op, lhs_layout); + } + } + } + _ => { + unreachable!("Compiler bug: tried to run numeric operation {:?} on invalid layouts. The 2 layouts were: ({:?}) and ({:?})", op, lhs_layout, rhs_layout); + } + } + } + + NumAdd | NumSub | NumMul | NumLt | NumLte | NumGt | NumGte | NumRemUnchecked + | NumIsMultipleOf | NumAddWrap | NumAddChecked | NumAddSaturated | NumDivFrac + | NumDivTruncUnchecked | NumDivCeilUnchecked | NumPow | NumPowInt | NumSubWrap + | NumSubChecked | NumSubSaturated | NumMulWrap | NumMulSaturated | NumMulChecked => { + arguments_with_layouts!((lhs_arg, lhs_layout), (rhs_arg, rhs_layout)); + + build_num_binop( + env, + layout_interner, + parent, + lhs_arg, + lhs_layout, + rhs_arg, + rhs_layout, + layout, + op, + ) + } + NumBitwiseAnd | NumBitwiseOr | NumBitwiseXor => { + arguments_with_layouts!((lhs_arg, lhs_layout), (rhs_arg, rhs_layout)); + + debug_assert_eq!(lhs_layout, rhs_layout); + let int_width = intwidth_from_layout(lhs_layout); + + build_int_binop( + env, + layout_interner, + parent, + int_width, + lhs_arg.into_int_value(), + rhs_arg.into_int_value(), + op, + ) + } + NumShiftLeftBy | NumShiftRightBy | NumShiftRightZfBy => { + arguments_with_layouts!((lhs_arg, lhs_layout), (rhs_arg, rhs_layout)); + + let int_width = intwidth_from_layout(lhs_layout); + + debug_assert_eq!(rhs_layout, Layout::U8); + let rhs_arg = if rhs_layout != lhs_layout { + // LLVM shift intrinsics expect the left and right sides to have the same type, so + // here we cast up `rhs` to the lhs type. Since the rhs was checked to be a U8, + // this cast isn't lossy. + let rhs_arg = env.builder.new_build_int_cast( + rhs_arg.into_int_value(), + lhs_arg.get_type().into_int_type(), + "cast_for_shift", + ); + rhs_arg.into() + } else { + rhs_arg + }; + + build_int_binop( + env, + layout_interner, + parent, + int_width, + lhs_arg.into_int_value(), + rhs_arg.into_int_value(), + op, + ) + } + NumIntCast => { + arguments!(arg); + + let to = basic_type_from_layout(env, layout_interner, layout_interner.get_repr(layout)) + .into_int_type(); + let to_signed = intwidth_from_layout(layout).is_signed(); + + env.builder + .new_build_int_cast_sign_flag(arg.into_int_value(), to, to_signed, "inc_cast") + .into() + } + NumToFloatCast => { + arguments_with_layouts!((arg, arg_layout)); + + match layout_interner.get_repr(arg_layout) { + LayoutRepr::Builtin(Builtin::Int(width)) => { + // Converting from int to float + let int_val = arg.into_int_value(); + let dest = basic_type_from_layout( + env, + layout_interner, + layout_interner.get_repr(layout), + ) + .into_float_type(); + + if width.is_signed() { + env.builder + .new_build_signed_int_to_float(int_val, dest, "signed_int_to_float") + .into() + } else { + env.builder + .new_build_unsigned_int_to_float(int_val, dest, "unsigned_int_to_float") + .into() + } + } + LayoutRepr::Builtin(Builtin::Float(_)) => { + // Converting from float to float - e.g. F64 to F32, or vice versa + let dest = basic_type_from_layout( + env, + layout_interner, + layout_interner.get_repr(layout), + ) + .into_float_type(); + + env.builder + .new_build_float_cast(arg.into_float_value(), dest, "cast_float_to_float") + .into() + } + LayoutRepr::Builtin(Builtin::Decimal) => { + todo!("Support converting Dec values to floats."); + } + other => { + unreachable!("Tried to do a float cast to non-float layout {:?}", other); + } + } + } + NumToFloatChecked => { + // NOTE: There's a NumToIntChecked implementation above, + // which could be useful to look at when implementing this. + todo!("implement checked float conversion"); + } + I128OfDec => { + arguments!(dec); + dec_unary_op(env, bitcode::DEC_TO_I128, dec) + } + Eq => { + arguments_with_layouts!((lhs_arg, lhs_layout), (rhs_arg, rhs_layout)); + + generic_eq( + env, + layout_interner, + layout_ids, + lhs_arg, + rhs_arg, + lhs_layout, + rhs_layout, + ) + } + NotEq => { + arguments_with_layouts!((lhs_arg, lhs_layout), (rhs_arg, rhs_layout)); + + generic_neq( + env, + layout_interner, + layout_ids, + lhs_arg, + rhs_arg, + lhs_layout, + rhs_layout, + ) + } + And => { + // The (&&) operator + arguments!(lhs_arg, rhs_arg); + + let bool_val = env.builder.new_build_and( + lhs_arg.into_int_value(), + rhs_arg.into_int_value(), + "bool_and", + ); + + BasicValueEnum::IntValue(bool_val) + } + Or => { + // The (||) operator + arguments!(lhs_arg, rhs_arg); + + let bool_val = env.builder.new_build_or( + lhs_arg.into_int_value(), + rhs_arg.into_int_value(), + "bool_or", + ); + + BasicValueEnum::IntValue(bool_val) + } + Not => { + // The (!) operator + arguments!(arg); + + let bool_val = env.builder.new_build_not(arg.into_int_value(), "bool_not"); + BasicValueEnum::IntValue(bool_val) + } + Hash => { + unimplemented!() + } + + ListMap | ListMap2 | ListMap3 | ListMap4 | ListSortWith => { + unreachable!("these are higher order, and are handled elsewhere") + } + + BoxExpr | UnboxExpr => { + unreachable!("The {:?} operation is turned into mono Expr", op) + } + + PtrCast => { + arguments!(data_ptr); + + let target_type = + basic_type_from_layout(env, layout_interner, layout_interner.get_repr(layout)) + .into_pointer_type(); + + debug_assert!(data_ptr.is_pointer_value()); + + env.builder + .new_build_pointer_cast(data_ptr.into_pointer_value(), target_type, "ptr_cast") + .into() + } + + PtrStore => { + arguments!(ptr, value); + + env.builder.new_build_store(ptr.into_pointer_value(), value); + + // ptr + env.context.struct_type(&[], false).const_zero().into() + } + + PtrLoad => { + arguments!(ptr); + + let ret_repr = layout_interner.get_repr(layout); + let element_type = basic_type_from_layout(env, layout_interner, ret_repr); + + env.builder + .new_build_load(element_type, ptr.into_pointer_value(), "ptr_load") + } + + PtrClearTagId => { + arguments!(ptr); + + tag_pointer_clear_tag_id(env, ptr.into_pointer_value()).into() + } + + RefCountIncRcPtr | RefCountDecRcPtr | RefCountIncDataPtr | RefCountDecDataPtr => { + unreachable!("Not used in LLVM backend: {:?}", op); + } + + RefCountIsUnique => { + arguments_with_layouts!((data_ptr, data_layout)); + + let ptr = env.builder.new_build_pointer_cast( + data_ptr.into_pointer_value(), + env.context.i8_type().ptr_type(AddressSpace::default()), + "cast_to_i8_ptr", + ); + + let value_ptr = match layout_interner.runtime_representation(data_layout) { + LayoutRepr::Union(union_layout) + if union_layout.stores_tag_id_in_pointer(env.target_info) => + { + tag_pointer_clear_tag_id(env, ptr) + } + _ => ptr, + }; + + let refcount_ptr = PointerToRefcount::from_ptr_to_data(env, value_ptr); + + BasicValueEnum::IntValue(refcount_ptr.is_1(env)) + } + + Unreachable => { + match RocReturn::from_layout(layout_interner, layout_interner.get_repr(layout)) { + RocReturn::Return => { + let basic_type = basic_type_from_layout( + env, + layout_interner, + layout_interner.get_repr(layout), + ); + basic_type.const_zero() + } + RocReturn::ByPointer => { + let basic_type = basic_type_from_layout( + env, + layout_interner, + layout_interner.get_repr(layout), + ); + let ptr = env + .builder + .new_build_alloca(basic_type, "unreachable_alloca"); + env.builder.new_build_store(ptr, basic_type.const_zero()); + + ptr.into() + } + } + } + DictPseudoSeed => { + // Dict.pseudoSeed : {} -> u64 + + call_bitcode_fn(env, &[], bitcode::UTILS_DICT_PSEUDO_SEED) + } + + SetJmp | LongJmp | SetLongJmpBuffer => unreachable!("only inserted in dev backend codegen"), + } +} + +fn intwidth_from_layout(layout: InLayout) -> IntWidth { + layout.to_int_width() +} + +fn build_int_binop<'ctx>( + env: &Env<'_, 'ctx, '_>, + layout_interner: &STLayoutInterner<'_>, + parent: FunctionValue<'ctx>, + int_width: IntWidth, + lhs: IntValue<'ctx>, + rhs: IntValue<'ctx>, + op: LowLevel, +) -> BasicValueEnum<'ctx> { + use inkwell::IntPredicate::*; + use roc_module::low_level::LowLevel::*; + + let bd = env.builder; + + match op { + NumAdd => { + let result = env + .call_intrinsic( + &LLVM_ADD_WITH_OVERFLOW[int_width], + &[lhs.into(), rhs.into()], + ) + .into_struct_value(); + + throw_on_overflow(env, parent, result, "integer addition overflowed!") + } + NumAddWrap => bd.new_build_int_add(lhs, rhs, "add_int_wrap").into(), + NumAddChecked => { + let with_overflow = env.call_intrinsic( + &LLVM_ADD_WITH_OVERFLOW[int_width], + &[lhs.into(), rhs.into()], + ); + + let layout = Layout::from_int_width(int_width); + let layout_repr = LayoutRepr::Struct(env.arena.alloc([layout, Layout::BOOL])); + + use_roc_value( + env, + layout_interner, + layout_repr, + with_overflow, + "num_add_with_overflow", + ) + } + NumAddSaturated => { + env.call_intrinsic(&LLVM_ADD_SATURATED[int_width], &[lhs.into(), rhs.into()]) + } + NumSub => { + let result = env + .call_intrinsic( + &LLVM_SUB_WITH_OVERFLOW[int_width], + &[lhs.into(), rhs.into()], + ) + .into_struct_value(); + + throw_on_overflow(env, parent, result, "integer subtraction overflowed!") + } + NumSubWrap => bd.new_build_int_sub(lhs, rhs, "sub_int").into(), + NumSubChecked => { + let with_overflow = env.call_intrinsic( + &LLVM_SUB_WITH_OVERFLOW[int_width], + &[lhs.into(), rhs.into()], + ); + + let layout = Layout::from_int_width(int_width); + let layout_repr = LayoutRepr::Struct(env.arena.alloc([layout, Layout::BOOL])); + + use_roc_value( + env, + layout_interner, + layout_repr, + with_overflow, + "num_sub_with_overflow", + ) + } + NumSubSaturated => { + env.call_intrinsic(&LLVM_SUB_SATURATED[int_width], &[lhs.into(), rhs.into()]) + } + NumMul => { + let result = env + .call_intrinsic( + &LLVM_MUL_WITH_OVERFLOW[int_width], + &[lhs.into(), rhs.into()], + ) + .into_struct_value(); + + throw_on_overflow(env, parent, result, "integer multiplication overflowed!") + } + NumMulWrap => bd.new_build_int_mul(lhs, rhs, "mul_int").into(), + NumMulSaturated => call_bitcode_fn( + env, + &[lhs.into(), rhs.into()], + &bitcode::NUM_MUL_SATURATED_INT[int_width], + ), + NumMulChecked => { + let with_overflow = env.call_intrinsic( + &LLVM_MUL_WITH_OVERFLOW[int_width], + &[lhs.into(), rhs.into()], + ); + + let layout = Layout::from_int_width(int_width); + let layout_repr = LayoutRepr::Struct(env.arena.alloc([layout, Layout::BOOL])); + + use_roc_value( + env, + layout_interner, + layout_repr, + with_overflow, + "num_mul_with_overflow", + ) + } + NumGt => { + if int_width.is_signed() { + bd.new_build_int_compare(SGT, lhs, rhs, "gt_int").into() + } else { + bd.new_build_int_compare(UGT, lhs, rhs, "gt_uint").into() + } + } + NumGte => { + if int_width.is_signed() { + bd.new_build_int_compare(SGE, lhs, rhs, "gte_int").into() + } else { + bd.new_build_int_compare(UGE, lhs, rhs, "gte_uint").into() + } + } + NumLt => { + if int_width.is_signed() { + bd.new_build_int_compare(SLT, lhs, rhs, "lt_int").into() + } else { + bd.new_build_int_compare(ULT, lhs, rhs, "lt_uint").into() + } + } + NumLte => { + if int_width.is_signed() { + bd.new_build_int_compare(SLE, lhs, rhs, "lte_int").into() + } else { + bd.new_build_int_compare(ULE, lhs, rhs, "lte_uint").into() + } + } + NumRemUnchecked => { + if int_width.is_signed() { + bd.new_build_int_signed_rem(lhs, rhs, "rem_int").into() + } else { + bd.new_build_int_unsigned_rem(lhs, rhs, "rem_uint").into() + } + } + NumIsMultipleOf => { + // this builds the following construct + // + // if (rhs == 0 || rhs == -1) { + // // lhs is a multiple of rhs iff + // // + // // - rhs == -1 + // // - both rhs and lhs are 0 + // // + // // the -1 case is important for overflow reasons `isize::MIN % -1` crashes in rust + // (rhs == -1) || (lhs == 0) + // } else { + // let rem = lhs % rhs; + // rem == 0 + // } + // + // NOTE we'd like the branches to be swapped for better branch prediction, + // but llvm normalizes to the above ordering in -O3 + let zero = rhs.get_type().const_zero(); + let neg_1 = rhs.get_type().const_int(-1i64 as u64, false); + let is_signed = int_width.is_signed(); + + let special_block = env.context.append_basic_block(parent, "special_block"); + let default_block = env.context.append_basic_block(parent, "default_block"); + let cont_block = env.context.append_basic_block(parent, "branchcont"); + + if is_signed { + bd.new_build_switch( + rhs, + default_block, + &[(zero, special_block), (neg_1, special_block)], + ) + } else { + bd.new_build_switch(rhs, default_block, &[(zero, special_block)]) + }; + + let condition_rem = { + bd.position_at_end(default_block); + + let rem = if is_signed { + bd.new_build_int_signed_rem(lhs, rhs, "int_rem") + } else { + bd.new_build_int_unsigned_rem(lhs, rhs, "uint_rem") + }; + let result = bd.new_build_int_compare(IntPredicate::EQ, rem, zero, "is_zero_rem"); + + bd.new_build_unconditional_branch(cont_block); + result + }; + + let condition_special = { + bd.position_at_end(special_block); + + let is_zero = bd.new_build_int_compare(IntPredicate::EQ, lhs, zero, "is_zero_lhs"); + + let result = if is_signed { + let is_neg_one = + bd.new_build_int_compare(IntPredicate::EQ, rhs, neg_1, "is_neg_one_rhs"); + + bd.new_build_or(is_neg_one, is_zero, "cond") + } else { + is_zero + }; + + bd.new_build_unconditional_branch(cont_block); + + result + }; + + { + bd.position_at_end(cont_block); + + let phi = bd.new_build_phi(env.context.bool_type(), "branch"); + + phi.add_incoming(&[ + (&condition_rem, default_block), + (&condition_special, special_block), + ]); + + phi.as_basic_value() + } + } + NumPowInt => call_bitcode_fn( + env, + &[lhs.into(), rhs.into()], + &bitcode::NUM_POW_INT[int_width], + ), + NumDivTruncUnchecked => { + if int_width.is_signed() { + bd.new_build_int_signed_div(lhs, rhs, "div_int").into() + } else { + bd.new_build_int_unsigned_div(lhs, rhs, "div_uint").into() + } + } + NumDivCeilUnchecked => call_bitcode_fn( + env, + &[lhs.into(), rhs.into()], + &bitcode::NUM_DIV_CEIL[int_width], + ), + NumBitwiseAnd => bd.new_build_and(lhs, rhs, "int_bitwise_and").into(), + NumBitwiseXor => bd.new_build_xor(lhs, rhs, "int_bitwise_xor").into(), + NumBitwiseOr => bd.new_build_or(lhs, rhs, "int_bitwise_or").into(), + NumShiftLeftBy => bd.new_build_left_shift(lhs, rhs, "int_shift_left").into(), + NumShiftRightBy => bd + .new_build_right_shift(lhs, rhs, true, "int_shift_right") + .into(), + NumShiftRightZfBy => bd + .new_build_right_shift(lhs, rhs, false, "int_shift_right_zf") + .into(), + + _ => { + unreachable!("Unrecognized int binary operation: {:?}", op); + } + } +} + +pub fn build_num_binop<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + parent: FunctionValue<'ctx>, + lhs_arg: BasicValueEnum<'ctx>, + lhs_layout: InLayout<'a>, + rhs_arg: BasicValueEnum<'ctx>, + rhs_layout: InLayout<'a>, + return_layout: InLayout<'a>, + op: LowLevel, +) -> BasicValueEnum<'ctx> { + match ( + layout_interner.get_repr(lhs_layout), + layout_interner.get_repr(rhs_layout), + ) { + (LayoutRepr::Builtin(lhs_builtin), LayoutRepr::Builtin(rhs_builtin)) + if lhs_builtin == rhs_builtin => + { + use roc_mono::layout::Builtin::*; + + match lhs_builtin { + Int(int_width) => build_int_binop( + env, + layout_interner, + parent, + int_width, + lhs_arg.into_int_value(), + rhs_arg.into_int_value(), + op, + ), + + Float(float_width) => build_float_binop( + env, + float_width, + lhs_arg.into_float_value(), + rhs_arg.into_float_value(), + op, + ), + + Decimal => build_dec_binop( + env, + layout_interner, + parent, + lhs_arg, + rhs_arg, + return_layout, + op, + ), + _ => { + unreachable!("Compiler bug: tried to run numeric operation {:?} on invalid builtin layout: ({:?})", op, lhs_layout); + } + } + } + _ => { + unreachable!("Compiler bug: tried to run numeric operation {:?} on invalid layouts. The 2 layouts were: ({:?}) and ({:?})", op, lhs_layout, rhs_layout); + } + } +} + +fn build_float_binop<'ctx>( + env: &Env<'_, 'ctx, '_>, + float_width: FloatWidth, + lhs: FloatValue<'ctx>, + rhs: FloatValue<'ctx>, + op: LowLevel, +) -> BasicValueEnum<'ctx> { + use inkwell::FloatPredicate::*; + use roc_module::low_level::LowLevel::*; + + let bd = env.builder; + + match op { + NumAdd => bd.new_build_float_add(lhs, rhs, "add_float").into(), + NumAddChecked => { + let context = env.context; + + let result = bd.new_build_float_add(lhs, rhs, "add_float"); + + let is_finite = + call_bitcode_fn(env, &[result.into()], &bitcode::NUM_IS_FINITE[float_width]) + .into_int_value(); + let is_infinite = bd.new_build_not(is_finite, "negate"); + + let struct_type = context.struct_type( + &[context.f64_type().into(), context.bool_type().into()], + false, + ); + + let struct_value = { + let v1 = struct_type.const_zero(); + let v2 = bd.build_insert_value(v1, result, 0, "set_result").unwrap(); + let v3 = bd + .build_insert_value(v2, is_infinite, 1, "set_is_infinite") + .unwrap(); + + v3.into_struct_value() + }; + + struct_value.into() + } + NumAddWrap => unreachable!("wrapping addition is not defined on floats"), + NumSub => bd.new_build_float_sub(lhs, rhs, "sub_float").into(), + NumSubChecked => { + let context = env.context; + + let result = bd.new_build_float_sub(lhs, rhs, "sub_float"); + + let is_finite = + call_bitcode_fn(env, &[result.into()], &bitcode::NUM_IS_FINITE[float_width]) + .into_int_value(); + let is_infinite = bd.new_build_not(is_finite, "negate"); + + let struct_type = context.struct_type( + &[context.f64_type().into(), context.bool_type().into()], + false, + ); + + let struct_value = { + let v1 = struct_type.const_zero(); + let v2 = bd.build_insert_value(v1, result, 0, "set_result").unwrap(); + let v3 = bd + .build_insert_value(v2, is_infinite, 1, "set_is_infinite") + .unwrap(); + + v3.into_struct_value() + }; + + struct_value.into() + } + NumSubWrap => unreachable!("wrapping subtraction is not defined on floats"), + NumMul => bd.new_build_float_mul(lhs, rhs, "mul_float").into(), + NumMulSaturated => bd.new_build_float_mul(lhs, rhs, "mul_float").into(), + NumMulChecked => { + let context = env.context; + + let result = bd.new_build_float_mul(lhs, rhs, "mul_float"); + + let is_finite = + call_bitcode_fn(env, &[result.into()], &bitcode::NUM_IS_FINITE[float_width]) + .into_int_value(); + let is_infinite = bd.new_build_not(is_finite, "negate"); + + let struct_type = context.struct_type( + &[context.f64_type().into(), context.bool_type().into()], + false, + ); + + let struct_value = { + let v1 = struct_type.const_zero(); + let v2 = bd.build_insert_value(v1, result, 0, "set_result").unwrap(); + let v3 = bd + .build_insert_value(v2, is_infinite, 1, "set_is_infinite") + .unwrap(); + + v3.into_struct_value() + }; + + struct_value.into() + } + NumMulWrap => unreachable!("wrapping multiplication is not defined on floats"), + NumGt => bd.new_build_float_compare(OGT, lhs, rhs, "float_gt").into(), + NumGte => bd + .new_build_float_compare(OGE, lhs, rhs, "float_gte") + .into(), + NumLt => bd.new_build_float_compare(OLT, lhs, rhs, "float_lt").into(), + NumLte => bd + .new_build_float_compare(OLE, lhs, rhs, "float_lte") + .into(), + NumDivFrac => bd.new_build_float_div(lhs, rhs, "div_float").into(), + NumPow => call_bitcode_fn( + env, + &[lhs.into(), rhs.into()], + &bitcode::NUM_POW[float_width], + ), + _ => { + unreachable!("Unrecognized int binary operation: {:?}", op); + } + } +} + +fn throw_on_overflow<'ctx>( + env: &Env<'_, 'ctx, '_>, + parent: FunctionValue<'ctx>, + result: StructValue<'ctx>, // of the form { value: T, has_overflowed: bool } + message: &str, +) -> BasicValueEnum<'ctx> { + let bd = env.builder; + let context = env.context; + + let has_overflowed = bd.build_extract_value(result, 1, "has_overflowed").unwrap(); + + let condition = bd.new_build_int_compare( + IntPredicate::EQ, + has_overflowed.into_int_value(), + context.bool_type().const_zero(), + "has_not_overflowed", + ); + + let then_block = context.append_basic_block(parent, "then_block"); + let throw_block = context.append_basic_block(parent, "throw_block"); + + bd.new_build_conditional_branch(condition, then_block, throw_block); + + bd.position_at_end(throw_block); + + throw_because_overflow(env, message); + + bd.position_at_end(then_block); + + bd.build_extract_value(result, 0, "operation_result") + .unwrap() +} + +fn throw_because_overflow(env: &Env<'_, '_, '_>, message: &str) { + let block = env.builder.get_insert_block().expect("to be in a function"); + let di_location = env.builder.get_current_debug_location().unwrap(); + + let function_name = "throw_on_overflow"; + let function = match env.module.get_function(function_name) { + Some(function_value) => function_value, + None => { + let function_type = env.context.void_type().fn_type(&[], false); + let function_value = + env.module + .add_function(function_name, function_type, Some(Linkage::Internal)); + + function_value.set_call_conventions(FAST_CALL_CONV); + + // prevent inlining of this function + let kind_id = Attribute::get_named_enum_kind_id("noinline"); + debug_assert!(kind_id > 0); + let enum_attr = env.context.create_enum_attribute(kind_id, 0); + function_value.add_attribute(AttributeLoc::Function, enum_attr); + + // calling this function is unlikely + let kind_id = Attribute::get_named_enum_kind_id("cold"); + debug_assert!(kind_id > 0); + let enum_attr = env.context.create_enum_attribute(kind_id, 0); + function_value.add_attribute(AttributeLoc::Function, enum_attr); + + // this function never returns + let kind_id = Attribute::get_named_enum_kind_id("noreturn"); + debug_assert!(kind_id > 0); + let enum_attr = env.context.create_enum_attribute(kind_id, 0); + function_value.add_attribute(AttributeLoc::Function, enum_attr); + + // Add a basic block for the entry point + let entry = env.context.append_basic_block(function_value, "entry"); + + env.builder.position_at_end(entry); + + // ends in unreachable, so no return is needed + throw_internal_exception(env, function_value, message); + + function_value + } + }; + + env.builder.position_at_end(block); + env.builder.set_current_debug_location(di_location); + + let call = env.builder.new_build_call(function, &[], "overflow"); + call.set_call_convention(FAST_CALL_CONV); + + env.builder.new_build_unreachable(); +} + +fn dec_split_into_words<'ctx>( + env: &Env<'_, 'ctx, '_>, + value: IntValue<'ctx>, +) -> (IntValue<'ctx>, IntValue<'ctx>) { + let int_64 = env.context.i128_type().const_int(64, false); + let int_64_type = env.context.i64_type(); + + let left_bits_i128 = env + .builder + .new_build_right_shift(value, int_64, false, "left_bits_i128"); + + ( + env.builder.new_build_int_cast(value, int_64_type, ""), + env.builder + .new_build_int_cast(left_bits_i128, int_64_type, ""), + ) +} + +fn dec_alloca<'ctx>(env: &Env<'_, 'ctx, '_>, value: IntValue<'ctx>) -> BasicValueEnum<'ctx> { + use roc_target::Architecture::*; + use roc_target::OperatingSystem::*; + match env.target_info.operating_system { + Windows => { + let dec_type = zig_dec_type(env); + + let alloca = env.builder.new_build_alloca(dec_type, "dec_alloca"); + + let instruction = alloca.as_instruction_value().unwrap(); + instruction.set_alignment(16).unwrap(); + + let ptr = env.builder.new_build_pointer_cast( + alloca, + value.get_type().ptr_type(AddressSpace::default()), + "cast_to_i128_ptr", + ); + + env.builder.new_build_store(ptr, value); + + alloca.into() + } + Unix => { + if matches!(env.target_info.architecture, X86_32 | X86_64) { + internal_error!("X86 unix does not pass with a dec alloc instead it splits into high and low halves"); + } + let i64_type = env.context.i64_type(); + let alloca = env + .builder + .build_array_alloca(i64_type, i64_type.const_int(2, false), "dec_alloca") + .unwrap(); + let instruction = alloca.as_instruction_value().unwrap(); + instruction.set_alignment(16).unwrap(); + let ptr = env.builder.new_build_pointer_cast( + alloca, + value.get_type().ptr_type(AddressSpace::default()), + "cast_to_i128_ptr", + ); + env.builder.new_build_store(ptr, value); + env.builder + .new_build_load(i64_type.array_type(2), alloca, "load as array") + } + Wasi => unimplemented!(), + } +} + +fn dec_to_str<'ctx>(env: &Env<'_, 'ctx, '_>, dec: BasicValueEnum<'ctx>) -> BasicValueEnum<'ctx> { + use roc_target::Architecture::*; + use roc_target::OperatingSystem::*; + + let dec = dec.into_int_value(); + + match env.target_info { + TargetInfo { + architecture: X86_64 | X86_32, + operating_system: Unix, + } => { + let (low, high) = dec_split_into_words(env, dec); + + call_str_bitcode_fn( + env, + &[], + &[low.into(), high.into()], + BitcodeReturns::Str, + bitcode::DEC_TO_STR, + ) + } + TargetInfo { + architecture: Wasm32, + operating_system: Unix, + } => call_str_bitcode_fn( + env, + &[], + &[dec.into()], + BitcodeReturns::Str, + bitcode::DEC_TO_STR, + ), + _ => call_str_bitcode_fn( + env, + &[], + &[dec_alloca(env, dec)], + BitcodeReturns::Str, + bitcode::DEC_TO_STR, + ), + } +} + +fn dec_unary_op<'ctx>( + env: &Env<'_, 'ctx, '_>, + fn_name: &str, + dec: BasicValueEnum<'ctx>, +) -> BasicValueEnum<'ctx> { + use roc_target::Architecture::*; + use roc_target::OperatingSystem::*; + + let dec = dec.into_int_value(); + match env.target_info { + TargetInfo { + architecture: X86_64 | X86_32, + operating_system: Unix, + } => { + let (low, high) = dec_split_into_words(env, dec); + call_bitcode_fn(env, &[low.into(), high.into()], fn_name) + } + TargetInfo { + architecture: Wasm32, + operating_system: Unix, + } => call_bitcode_fn(env, &[dec.into()], fn_name), + _ => call_bitcode_fn(env, &[dec_alloca(env, dec)], fn_name), + } +} + +fn dec_binop_with_overflow<'ctx>( + env: &Env<'_, 'ctx, '_>, + fn_name: &str, + lhs: BasicValueEnum<'ctx>, + rhs: BasicValueEnum<'ctx>, +) -> StructValue<'ctx> { + use roc_target::Architecture::*; + use roc_target::OperatingSystem::*; + + let lhs = lhs.into_int_value(); + let rhs = rhs.into_int_value(); + + let return_type = zig_with_overflow_roc_dec(env); + let return_alloca = env.builder.new_build_alloca(return_type, "return_alloca"); + + match env.target_info { + TargetInfo { + architecture: X86_64 | X86_32, + operating_system: Unix, + } => { + let (lhs_low, lhs_high) = dec_split_into_words(env, lhs); + let (rhs_low, rhs_high) = dec_split_into_words(env, rhs); + call_void_bitcode_fn( + env, + &[ + return_alloca.into(), + lhs_low.into(), + lhs_high.into(), + rhs_low.into(), + rhs_high.into(), + ], + fn_name, + ); + } + TargetInfo { + architecture: Wasm32, + operating_system: Unix, + } => { + call_void_bitcode_fn( + env, + &[return_alloca.into(), lhs.into(), rhs.into()], + fn_name, + ); + } + _ => { + call_void_bitcode_fn( + env, + &[ + return_alloca.into(), + dec_alloca(env, lhs), + dec_alloca(env, rhs), + ], + fn_name, + ); + } + }; + + env.builder + .new_build_load(return_type, return_alloca, "load_dec") + .into_struct_value() +} + +pub(crate) fn dec_binop_with_unchecked<'ctx>( + env: &Env<'_, 'ctx, '_>, + fn_name: &str, + lhs: BasicValueEnum<'ctx>, + rhs: BasicValueEnum<'ctx>, +) -> BasicValueEnum<'ctx> { + use roc_target::Architecture::*; + use roc_target::OperatingSystem::*; + + let lhs = lhs.into_int_value(); + let rhs = rhs.into_int_value(); + + match env.target_info { + TargetInfo { + architecture: X86_64 | X86_32, + operating_system: Unix, + } => { + let (lhs_low, lhs_high) = dec_split_into_words(env, lhs); + let (rhs_low, rhs_high) = dec_split_into_words(env, rhs); + call_bitcode_fn( + env, + &[ + lhs_low.into(), + lhs_high.into(), + rhs_low.into(), + rhs_high.into(), + ], + fn_name, + ) + } + TargetInfo { + architecture: Wasm32, + operating_system: Unix, + } => call_bitcode_fn(env, &[lhs.into(), rhs.into()], fn_name), + _ => call_bitcode_fn(env, &[dec_alloca(env, lhs), dec_alloca(env, rhs)], fn_name), + } +} + +/// Zig returns a nominal `WithOverflow(Dec)` struct (see [zig_with_overflow_roc_dec]), +/// but the Roc side may flatten the overflow struct. LLVM does not admit comparisons +/// between the two representations, so always cast to the Roc representation. +fn change_with_overflow_to_roc_type<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + val: impl BasicValue<'ctx>, + return_layout: InLayout<'a>, +) -> BasicValueEnum<'ctx> { + let return_type = convert::basic_type_from_layout( + env, + layout_interner, + layout_interner.get_repr(return_layout), + ); + let casted = cast_basic_basic(env.builder, val.as_basic_value_enum(), return_type); + + use_roc_value( + env, + layout_interner, + layout_interner.get_repr(return_layout), + casted, + "use_dec_with_overflow", + ) +} + +fn build_dec_unary_op<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + _layout_interner: &STLayoutInterner<'a>, + _parent: FunctionValue<'ctx>, + arg: BasicValueEnum<'ctx>, + _return_layout: InLayout<'a>, + op: LowLevel, +) -> BasicValueEnum<'ctx> { + use roc_module::low_level::LowLevel::*; + + match op { + NumAbs => dec_unary_op(env, bitcode::DEC_ABS, arg), + NumAcos => dec_unary_op(env, bitcode::DEC_ACOS, arg), + NumAsin => dec_unary_op(env, bitcode::DEC_ASIN, arg), + NumAtan => dec_unary_op(env, bitcode::DEC_ATAN, arg), + NumCos => dec_unary_op(env, bitcode::DEC_COS, arg), + NumSin => dec_unary_op(env, bitcode::DEC_SIN, arg), + NumTan => dec_unary_op(env, bitcode::DEC_TAN, arg), + + _ => { + unreachable!("Unrecognized dec unary operation: {:?}", op); + } + } +} + +fn build_dec_binop<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + parent: FunctionValue<'ctx>, + lhs: BasicValueEnum<'ctx>, + rhs: BasicValueEnum<'ctx>, + return_layout: InLayout<'a>, + op: LowLevel, +) -> BasicValueEnum<'ctx> { + use roc_module::low_level::LowLevel::*; + + match op { + NumAddChecked => { + let val = dec_binop_with_overflow(env, bitcode::DEC_ADD_WITH_OVERFLOW, lhs, rhs); + change_with_overflow_to_roc_type(env, layout_interner, val, return_layout) + } + NumSubChecked => { + let val = dec_binop_with_overflow(env, bitcode::DEC_SUB_WITH_OVERFLOW, lhs, rhs); + change_with_overflow_to_roc_type(env, layout_interner, val, return_layout) + } + NumMulChecked => { + let val = dec_binop_with_overflow(env, bitcode::DEC_MUL_WITH_OVERFLOW, lhs, rhs); + change_with_overflow_to_roc_type(env, layout_interner, val, return_layout) + } + NumAdd => build_dec_binop_throw_on_overflow( + env, + parent, + bitcode::DEC_ADD_WITH_OVERFLOW, + lhs, + rhs, + "decimal addition overflowed", + ), + NumSub => build_dec_binop_throw_on_overflow( + env, + parent, + bitcode::DEC_SUB_WITH_OVERFLOW, + lhs, + rhs, + "decimal subtraction overflowed", + ), + NumMul => build_dec_binop_throw_on_overflow( + env, + parent, + bitcode::DEC_MUL_WITH_OVERFLOW, + lhs, + rhs, + "decimal multiplication overflowed", + ), + NumDivFrac => dec_binop_with_unchecked(env, bitcode::DEC_DIV, lhs, rhs), + + NumLt => call_bitcode_fn(env, &[lhs, rhs], &bitcode::NUM_LESS_THAN[IntWidth::I128]), + NumGt => call_bitcode_fn(env, &[lhs, rhs], &bitcode::NUM_GREATER_THAN[IntWidth::I128]), + NumLte => call_bitcode_fn( + env, + &[lhs, rhs], + &bitcode::NUM_LESS_THAN_OR_EQUAL[IntWidth::I128], + ), + NumGte => call_bitcode_fn( + env, + &[lhs, rhs], + &bitcode::NUM_GREATER_THAN_OR_EQUAL[IntWidth::I128], + ), + _ => { + unreachable!("Unrecognized dec binary operation: {:?}", op); + } + } +} + +fn build_dec_binop_throw_on_overflow<'ctx>( + env: &Env<'_, 'ctx, '_>, + parent: FunctionValue<'ctx>, + operation: &str, + lhs: BasicValueEnum<'ctx>, + rhs: BasicValueEnum<'ctx>, + message: &str, +) -> BasicValueEnum<'ctx> { + let result = dec_binop_with_overflow(env, operation, lhs, rhs); + + let value = throw_on_overflow(env, parent, result, message).into_struct_value(); + + env.builder.build_extract_value(value, 0, "num").unwrap() +} + +fn int_type_signed_min(int_type: IntType) -> IntValue { + let width = int_type.get_bit_width(); + + debug_assert!(width <= 128); + let shift = 128 - width as usize; + + if shift < 64 { + let min = i128::MIN >> shift; + let a = min as u64; + let b = (min >> 64) as u64; + + int_type.const_int_arbitrary_precision(&[b, a]) + } else { + int_type.const_int((i128::MIN >> shift) as u64, false) + } +} + +fn build_int_unary_op<'a, 'ctx, 'env>( + env: &Env<'a, 'ctx, 'env>, + layout_interner: &STLayoutInterner<'a>, + parent: FunctionValue<'ctx>, + arg: IntValue<'ctx>, + arg_width: IntWidth, + arg_int_type: IntType<'ctx>, + op: LowLevel, + return_layout: InLayout<'a>, +) -> BasicValueEnum<'ctx> { + use roc_module::low_level::LowLevel::*; + + let bd = env.builder; + + match op { + NumNeg => { + // integer abs overflows when applied to the minimum value of a signed type + int_neg_raise_on_overflow(env, arg, arg_int_type) + } + NumAbs => { + // integer abs overflows when applied to the minimum value of a signed type + int_abs_raise_on_overflow(env, arg, arg_int_type) + } + NumToFrac => { + // This is an Int, so we need to convert it. + + match layout_interner.get_repr(return_layout) { + LayoutRepr::Builtin(Builtin::Float(float_width)) => { + let target_float_type = convert::float_type_from_float_width(env, float_width); + + bd.new_build_cast( + InstructionOpcode::SIToFP, + arg, + target_float_type, + "i64_to_f64", + ) + } + LayoutRepr::Builtin(Builtin::Decimal) => { + call_bitcode_fn(env, &[arg.into()], &bitcode::DEC_FROM_INT[arg_width]) + } + _ => internal_error!("There can only be floats here!"), + } + } + NumToIntChecked => { + // return_layout : Result N [OutOfBounds]* ~ { result: N, out_of_bounds: bool } + + let target_int_width = match layout_interner.get_repr(return_layout) { + LayoutRepr::Struct(field_layouts) if field_layouts.len() == 2 => { + debug_assert!(layout_interner.eq_repr(field_layouts[1], Layout::BOOL)); + field_layouts[0].to_int_width() + } + layout => internal_error!( + "There can only be a result layout here, found {:?}!", + layout + ), + }; + + let arg_always_fits_in_target = (arg_width.stack_size() < target_int_width.stack_size() + && ( + // If the arg is unsigned, it will always fit in either a signed or unsigned + // int of a larger width. + !arg_width.is_signed() + || + // Otherwise if the arg is signed, it will always fit in a signed int of a + // larger width. + (target_int_width.is_signed() ) + ) ) + || // Or if the two types are the same, they trivially fit. + arg_width == target_int_width; + + // How the return type needs to be stored on the stack. + let return_type_stack_type = convert::basic_type_from_layout( + env, + layout_interner, + layout_interner.get_repr(return_layout), + ) + .into_struct_type(); + // How the return type is actually used, in the Roc calling convention. + let return_type_use_type = convert::argument_type_from_layout( + env, + layout_interner, + layout_interner.get_repr(return_layout), + ); + + if arg_always_fits_in_target { + // This is guaranteed to succeed so we can just make it an int cast and let LLVM + // optimize it away. + let target_int_type = convert::int_type_from_int_width(env, target_int_width); + let target_int_val: BasicValueEnum<'ctx> = env + .builder + .new_build_int_cast_sign_flag( + arg, + target_int_type, + target_int_width.is_signed(), + "int_cast", + ) + .into(); + + let r = return_type_stack_type.const_zero(); + let r = bd + .build_insert_value(r, target_int_val, 0, "converted_int") + .unwrap(); + let r = bd + .build_insert_value(r, env.context.bool_type().const_zero(), 1, "out_of_bounds") + .unwrap(); + + r.into_struct_value().into() + } else { + let intrinsic = if !arg_width.is_signed() { + // We are trying to convert from unsigned to signed/unsigned of same or lesser width, e.g. + // u16 -> i16, u16 -> i8, or u16 -> u8. We only need to check that the argument + // value fits in the MAX target type value. + &bitcode::NUM_INT_TO_INT_CHECKING_MAX[target_int_width][arg_width] + } else { + // We are trying to convert from signed to signed/unsigned of same or lesser width, e.g. + // i16 -> u16, i16 -> i8, or i16 -> u8. We need to check that the argument value fits in + // the MAX and MIN target type. + &bitcode::NUM_INT_TO_INT_CHECKING_MAX_AND_MIN[target_int_width][arg_width] + }; + + let result = match env.target_info.ptr_width() { + PtrWidth::Bytes4 => { + let zig_function = env.module.get_function(intrinsic).unwrap(); + let zig_function_type = zig_function.get_type(); + + match zig_function_type.get_return_type() { + Some(_) => call_str_bitcode_fn( + env, + &[], + &[arg.into()], + BitcodeReturns::Basic, + intrinsic, + ), + None => { + let return_type = zig_to_int_checked_result_type( + env, + target_int_width.type_name(), + ); + + let zig_return_alloca = create_entry_block_alloca( + env, + parent, + return_type.into(), + "num_to_int", + ); + + call_void_bitcode_fn( + env, + &[zig_return_alloca.into(), arg.into()], + intrinsic, + ); + + let roc_return_type = basic_type_from_layout( + env, + layout_interner, + layout_interner.get_repr(return_layout), + ) + .ptr_type(AddressSpace::default()); + + let roc_return_alloca = env.builder.new_build_pointer_cast( + zig_return_alloca, + roc_return_type, + "cast_to_roc", + ); + + load_roc_value( + env, + layout_interner, + layout_interner.get_repr(return_layout), + roc_return_alloca, + "num_to_int", + ) + } + } + } + PtrWidth::Bytes8 => { + if target_int_width.stack_size() as usize > env.target_info.ptr_size() { + let bitcode_return_type = + zig_to_int_checked_result_type(env, target_int_width.type_name()); + + call_bitcode_fn_fixing_for_convention( + env, + layout_interner, + bitcode_return_type, + &[arg.into()], + return_layout, + intrinsic, + ) + } else { + call_bitcode_fn(env, &[arg.into()], intrinsic) + } + } + }; + + complex_bitcast_check_size(env, result, return_type_use_type, "cast_bitpacked") + } + } + NumCountLeadingZeroBits => call_bitcode_fn( + env, + &[arg.into()], + &bitcode::NUM_COUNT_LEADING_ZERO_BITS[arg_width], + ), + NumCountTrailingZeroBits => call_bitcode_fn( + env, + &[arg.into()], + &bitcode::NUM_COUNT_TRAILING_ZERO_BITS[arg_width], + ), + NumCountOneBits => { + call_bitcode_fn(env, &[arg.into()], &bitcode::NUM_COUNT_ONE_BITS[arg_width]) + } + _ => { + unreachable!("Unrecognized int unary operation: {:?}", op); + } + } +} + +fn int_neg_raise_on_overflow<'ctx>( + env: &Env<'_, 'ctx, '_>, + arg: IntValue<'ctx>, + int_type: IntType<'ctx>, +) -> BasicValueEnum<'ctx> { + let builder = env.builder; + + let min_val = int_type_signed_min(int_type); + let condition = builder.new_build_int_compare(IntPredicate::EQ, arg, min_val, "is_min_val"); + + let block = env.builder.get_insert_block().expect("to be in a function"); + let parent = block.get_parent().expect("to be in a function"); + let then_block = env.context.append_basic_block(parent, "then"); + let else_block = env.context.append_basic_block(parent, "else"); + + env.builder + .new_build_conditional_branch(condition, then_block, else_block); + + builder.position_at_end(then_block); + + throw_internal_exception( + env, + parent, + "integer negation overflowed because its argument is the minimum value", + ); + + builder.position_at_end(else_block); + + builder.new_build_int_neg(arg, "negate_int").into() +} + +fn int_abs_raise_on_overflow<'ctx>( + env: &Env<'_, 'ctx, '_>, + arg: IntValue<'ctx>, + int_type: IntType<'ctx>, +) -> BasicValueEnum<'ctx> { + let builder = env.builder; + + let min_val = int_type_signed_min(int_type); + let condition = builder.new_build_int_compare(IntPredicate::EQ, arg, min_val, "is_min_val"); + + let block = env.builder.get_insert_block().expect("to be in a function"); + let parent = block.get_parent().expect("to be in a function"); + let then_block = env.context.append_basic_block(parent, "then"); + let else_block = env.context.append_basic_block(parent, "else"); + + env.builder + .new_build_conditional_branch(condition, then_block, else_block); + + builder.position_at_end(then_block); + + throw_internal_exception( + env, + parent, + "integer absolute overflowed because its argument is the minimum value", + ); + + builder.position_at_end(else_block); + + int_abs_with_overflow(env, arg, int_type) +} + +fn int_abs_with_overflow<'ctx>( + env: &Env<'_, 'ctx, '_>, + arg: IntValue<'ctx>, + int_type: IntType<'ctx>, +) -> BasicValueEnum<'ctx> { + // This is how libc's abs() is implemented - it uses no branching! + // + // abs = \arg -> + // shifted = arg >>> 63 + // + // (xor arg shifted) - shifted + + let bd = env.builder; + let shifted_name = "abs_shift_right"; + let shifted_alloca = { + let bits_to_shift = int_type.get_bit_width() as u64 - 1; + let shift_val = int_type.const_int(bits_to_shift, false); + let shifted = bd.new_build_right_shift(arg, shift_val, true, shifted_name); + let alloca = bd.new_build_alloca(int_type, "#int_abs_help"); + + // shifted = arg >>> 63 + bd.new_build_store(alloca, shifted); + + alloca + }; + + let xored_arg = bd.new_build_xor( + arg, + bd.new_build_load(int_type, shifted_alloca, shifted_name) + .into_int_value(), + "xor_arg_shifted", + ); + + BasicValueEnum::IntValue( + bd.new_build_int_sub( + xored_arg, + bd.new_build_load(int_type, shifted_alloca, shifted_name) + .into_int_value(), + "sub_xored_shifted", + ), + ) +} + +fn build_float_unary_op<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + layout: InLayout<'a>, + arg: FloatValue<'ctx>, + op: LowLevel, + float_width: FloatWidth, // arg width +) -> BasicValueEnum<'ctx> { + use roc_module::low_level::LowLevel::*; + + let bd = env.builder; + + // TODO: Handle different sized floats + match op { + NumNeg => bd.new_build_float_neg(arg, "negate_float").into(), + NumAbs => call_bitcode_fn(env, &[arg.into()], &bitcode::NUM_FABS[float_width]), + NumSqrtUnchecked => call_bitcode_fn(env, &[arg.into()], &bitcode::NUM_SQRT[float_width]), + NumLogUnchecked => call_bitcode_fn(env, &[arg.into()], &bitcode::NUM_LOG[float_width]), + NumToFrac => { + let return_width = match layout_interner.get_repr(layout) { + LayoutRepr::Builtin(Builtin::Float(return_width)) => return_width, + _ => internal_error!("Layout for returning is not Float : {:?}", layout), + }; + match (float_width, return_width) { + (FloatWidth::F32, FloatWidth::F32) => arg.into(), + (FloatWidth::F32, FloatWidth::F64) => bd.new_build_cast( + InstructionOpcode::FPExt, + arg, + env.context.f64_type(), + "f32_to_f64", + ), + (FloatWidth::F64, FloatWidth::F32) => bd.new_build_cast( + InstructionOpcode::FPTrunc, + arg, + env.context.f32_type(), + "f64_to_f32", + ), + (FloatWidth::F64, FloatWidth::F64) => arg.into(), + } + } + NumCeiling => { + let int_width = match layout_interner.get_repr(layout) { + LayoutRepr::Builtin(Builtin::Int(int_width)) => int_width, + _ => internal_error!("Ceiling return layout is not int: {:?}", layout), + }; + match float_width { + FloatWidth::F32 => { + call_bitcode_fn(env, &[arg.into()], &bitcode::NUM_CEILING_F32[int_width]) + } + FloatWidth::F64 => { + call_bitcode_fn(env, &[arg.into()], &bitcode::NUM_CEILING_F64[int_width]) + } + } + } + NumFloor => { + let int_width = match layout_interner.get_repr(layout) { + LayoutRepr::Builtin(Builtin::Int(int_width)) => int_width, + _ => internal_error!("Floor return layout is not int: {:?}", layout), + }; + match float_width { + FloatWidth::F32 => { + call_bitcode_fn(env, &[arg.into()], &bitcode::NUM_FLOOR_F32[int_width]) + } + FloatWidth::F64 => { + call_bitcode_fn(env, &[arg.into()], &bitcode::NUM_FLOOR_F64[int_width]) + } + } + } + NumRound => { + let int_width = match layout_interner.get_repr(layout) { + LayoutRepr::Builtin(Builtin::Int(int_width)) => int_width, + _ => internal_error!("Round return layout is not int: {:?}", layout), + }; + match float_width { + FloatWidth::F32 => { + call_bitcode_fn(env, &[arg.into()], &bitcode::NUM_ROUND_F32[int_width]) + } + FloatWidth::F64 => { + call_bitcode_fn(env, &[arg.into()], &bitcode::NUM_ROUND_F64[int_width]) + } + } + } + NumIsNan => call_bitcode_fn(env, &[arg.into()], &bitcode::NUM_IS_NAN[float_width]), + NumIsInfinite => { + call_bitcode_fn(env, &[arg.into()], &bitcode::NUM_IS_INFINITE[float_width]) + } + NumIsFinite => call_bitcode_fn(env, &[arg.into()], &bitcode::NUM_IS_FINITE[float_width]), + + // trigonometry + NumSin => call_bitcode_fn(env, &[arg.into()], &bitcode::NUM_SIN[float_width]), + NumCos => call_bitcode_fn(env, &[arg.into()], &bitcode::NUM_COS[float_width]), + NumTan => call_bitcode_fn(env, &[arg.into()], &bitcode::NUM_TAN[float_width]), + + NumAtan => call_bitcode_fn(env, &[arg.into()], &bitcode::NUM_ATAN[float_width]), + NumAcos => call_bitcode_fn(env, &[arg.into()], &bitcode::NUM_ACOS[float_width]), + NumAsin => call_bitcode_fn(env, &[arg.into()], &bitcode::NUM_ASIN[float_width]), + + _ => { + unreachable!("Unrecognized int unary operation: {:?}", op); + } + } +} + +pub(crate) fn run_higher_order_low_level<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + layout_ids: &mut LayoutIds<'a>, + scope: &Scope<'a, 'ctx>, + return_layout: InLayout<'a>, + func_spec: FuncSpec, + higher_order: &HigherOrderLowLevel<'a>, +) -> BasicValueEnum<'ctx> { + use roc_mono::ir::PassedFunction; + use roc_mono::low_level::HigherOrder::*; + + let HigherOrderLowLevel { + op, + passed_function, + .. + } = higher_order; + + let PassedFunction { + argument_layouts: _, + return_layout: result_layout, + owns_captured_environment: function_owns_closure_data, + name: function_name, + captured_environment, + .. + } = *passed_function; + + // macros because functions cause lifetime issues related to the `env` or `layout_ids` + macro_rules! function_details { + () => {{ + let function = function_value_by_func_spec( + env, + FuncBorrowSpec::Some(func_spec), + function_name.name(), + ); + + let (closure, closure_layout) = + load_symbol_and_lambda_set(layout_interner, scope, &captured_environment); + + (function, closure, closure_layout) + }}; + } + + match op { + ListMap { xs } => { + // List.map : List before, (before -> after) -> List after + let (list, list_layout) = scope.load_symbol_and_layout(xs); + + let (function, closure, closure_layout) = function_details!(); + + match ( + layout_interner.get_repr(list_layout), + layout_interner.get_repr(return_layout), + ) { + ( + LayoutRepr::Builtin(Builtin::List(element_layout)), + LayoutRepr::Builtin(Builtin::List(result_layout)), + ) => { + let argument_layouts = &[element_layout]; + + let roc_function_call = roc_function_call( + env, + layout_interner, + layout_ids, + function, + closure, + closure_layout, + function_owns_closure_data, + argument_layouts, + result_layout, + ); + + list_map( + env, + layout_interner, + roc_function_call, + list, + element_layout, + result_layout, + ) + } + _ => unreachable!("invalid list layout"), + } + } + ListMap2 { xs, ys } => { + let (list1, list1_layout) = scope.load_symbol_and_layout(xs); + let (list2, list2_layout) = scope.load_symbol_and_layout(ys); + + let (function, closure, closure_layout) = function_details!(); + + match ( + layout_interner.get_repr(list1_layout), + layout_interner.get_repr(list2_layout), + layout_interner.get_repr(return_layout), + ) { + ( + LayoutRepr::Builtin(Builtin::List(element1_layout)), + LayoutRepr::Builtin(Builtin::List(element2_layout)), + LayoutRepr::Builtin(Builtin::List(result_layout)), + ) => { + let argument_layouts = &[element1_layout, element2_layout]; + + let roc_function_call = roc_function_call( + env, + layout_interner, + layout_ids, + function, + closure, + closure_layout, + function_owns_closure_data, + argument_layouts, + result_layout, + ); + + list_map2( + env, + layout_interner, + layout_ids, + roc_function_call, + list1, + list2, + element1_layout, + element2_layout, + result_layout, + ) + } + _ => unreachable!("invalid list layout"), + } + } + ListMap3 { xs, ys, zs } => { + let (list1, list1_layout) = scope.load_symbol_and_layout(xs); + let (list2, list2_layout) = scope.load_symbol_and_layout(ys); + let (list3, list3_layout) = scope.load_symbol_and_layout(zs); + + let (function, closure, closure_layout) = function_details!(); + + match ( + layout_interner.get_repr(list1_layout), + layout_interner.get_repr(list2_layout), + layout_interner.get_repr(list3_layout), + layout_interner.get_repr(return_layout), + ) { + ( + LayoutRepr::Builtin(Builtin::List(element1_layout)), + LayoutRepr::Builtin(Builtin::List(element2_layout)), + LayoutRepr::Builtin(Builtin::List(element3_layout)), + LayoutRepr::Builtin(Builtin::List(result_layout)), + ) => { + let argument_layouts = &[element1_layout, element2_layout, element3_layout]; + + let roc_function_call = roc_function_call( + env, + layout_interner, + layout_ids, + function, + closure, + closure_layout, + function_owns_closure_data, + argument_layouts, + result_layout, + ); + + list_map3( + env, + layout_interner, + layout_ids, + roc_function_call, + list1, + list2, + list3, + element1_layout, + element2_layout, + element3_layout, + result_layout, + ) + } + _ => unreachable!("invalid list layout"), + } + } + ListMap4 { xs, ys, zs, ws } => { + let (list1, list1_layout) = scope.load_symbol_and_layout(xs); + let (list2, list2_layout) = scope.load_symbol_and_layout(ys); + let (list3, list3_layout) = scope.load_symbol_and_layout(zs); + let (list4, list4_layout) = scope.load_symbol_and_layout(ws); + + let (function, closure, closure_layout) = function_details!(); + + match ( + layout_interner.get_repr(list1_layout), + layout_interner.get_repr(list2_layout), + layout_interner.get_repr(list3_layout), + layout_interner.get_repr(list4_layout), + layout_interner.get_repr(return_layout), + ) { + ( + LayoutRepr::Builtin(Builtin::List(element1_layout)), + LayoutRepr::Builtin(Builtin::List(element2_layout)), + LayoutRepr::Builtin(Builtin::List(element3_layout)), + LayoutRepr::Builtin(Builtin::List(element4_layout)), + LayoutRepr::Builtin(Builtin::List(result_layout)), + ) => { + let argument_layouts = &[ + element1_layout, + element2_layout, + element3_layout, + element4_layout, + ]; + + let roc_function_call = roc_function_call( + env, + layout_interner, + layout_ids, + function, + closure, + closure_layout, + function_owns_closure_data, + argument_layouts, + result_layout, + ); + + list_map4( + env, + layout_interner, + layout_ids, + roc_function_call, + list1, + list2, + list3, + list4, + element1_layout, + element2_layout, + element3_layout, + element4_layout, + result_layout, + ) + } + _ => unreachable!("invalid list layout"), + } + } + ListSortWith { xs } => { + // List.sortWith : List a, (a, a -> Ordering) -> List a + let (list, list_layout) = scope.load_symbol_and_layout(xs); + + let (function, closure, closure_layout) = function_details!(); + + match layout_interner.get_repr(list_layout) { + LayoutRepr::Builtin(Builtin::List(element_layout)) => { + use crate::llvm::bitcode::build_compare_wrapper; + + let argument_layouts = &[element_layout, element_layout]; + + let compare_wrapper = build_compare_wrapper( + env, + layout_interner, + layout_ids, + function, + closure_layout, + element_layout, + ) + .as_global_value() + .as_pointer_value(); + + let roc_function_call = roc_function_call( + env, + layout_interner, + layout_ids, + function, + closure, + closure_layout, + function_owns_closure_data, + argument_layouts, + result_layout, + ); + + list_sort_with( + env, + layout_interner, + roc_function_call, + compare_wrapper, + list, + element_layout, + ) + } + _ => unreachable!("invalid list layout"), + } + } + } +} + +fn load_symbol_and_lambda_set<'a, 'ctx>( + layout_interner: &STLayoutInterner<'a>, + scope: &Scope<'a, 'ctx>, + symbol: &Symbol, +) -> (BasicValueEnum<'ctx>, LambdaSet<'a>) { + let (ptr, layout) = scope.load_symbol_and_layout(symbol); + match layout_interner.get_repr(layout) { + LayoutRepr::LambdaSet(lambda_set) => (ptr, lambda_set), + other => panic!("Not a lambda set: {other:?}, {ptr:?}"), + } +} diff --git a/crates/compiler/gen_llvm/src/llvm/memcpy.rs b/crates/compiler/gen_llvm/src/llvm/memcpy.rs new file mode 100644 index 0000000000..7e82782bb1 --- /dev/null +++ b/crates/compiler/gen_llvm/src/llvm/memcpy.rs @@ -0,0 +1,23 @@ +use inkwell::{types::BasicType, values::PointerValue}; +use roc_mono::layout::{LayoutRepr, STLayoutInterner}; + +use super::{align::LlvmAlignment, build::Env, convert::basic_type_from_layout}; + +pub fn build_memcpy<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + layout: LayoutRepr<'a>, + destination: PointerValue<'ctx>, + source: PointerValue<'ctx>, +) { + let align_bytes = layout.llvm_alignment_bytes(layout_interner); + let width = basic_type_from_layout(env, layout_interner, layout) + .size_of() + .unwrap(); + if align_bytes > 0 { + // There is actually something to memcpy. + env.builder + .build_memcpy(destination, align_bytes, source, align_bytes, width) + .unwrap(); + } +} diff --git a/crates/compiler/gen_llvm/src/llvm/mod.rs b/crates/compiler/gen_llvm/src/llvm/mod.rs new file mode 100644 index 0000000000..d606191338 --- /dev/null +++ b/crates/compiler/gen_llvm/src/llvm/mod.rs @@ -0,0 +1,18 @@ +pub mod bitcode; +pub mod build; +pub mod build_list; +pub mod build_str; +pub mod compare; +pub mod convert; +mod expect; +pub mod externs; +mod intrinsics; +mod lowlevel; +pub mod refcounting; + +mod align; +mod erased; +mod fn_ptr; +mod memcpy; +mod scope; +mod struct_; diff --git a/crates/compiler/gen_llvm/src/llvm/refcounting.rs b/crates/compiler/gen_llvm/src/llvm/refcounting.rs new file mode 100644 index 0000000000..d676858257 --- /dev/null +++ b/crates/compiler/gen_llvm/src/llvm/refcounting.rs @@ -0,0 +1,1872 @@ +use crate::debug_info_init; +use crate::llvm::bitcode::call_void_bitcode_fn; +use crate::llvm::build::BuilderExt; +use crate::llvm::build::{ + add_func, cast_basic_basic, get_tag_id, tag_pointer_clear_tag_id, Env, FAST_CALL_CONV, +}; +use crate::llvm::build_list::{ + incrementing_elem_loop, list_capacity_or_ref_ptr, list_refcount_ptr, load_list, +}; +use crate::llvm::build_str::str_refcount_ptr; +use crate::llvm::convert::{basic_type_from_layout, zig_str_type, RocUnion}; +use crate::llvm::struct_::RocStruct; +use bumpalo::collections::Vec; +use inkwell::basic_block::BasicBlock; +use inkwell::module::Linkage; +use inkwell::types::{AnyTypeEnum, BasicMetadataTypeEnum, BasicType, BasicTypeEnum}; +use inkwell::values::{BasicValueEnum, FunctionValue, InstructionValue, IntValue, PointerValue}; +use inkwell::{AddressSpace, IntPredicate}; +use roc_module::symbol::Interns; +use roc_module::symbol::Symbol; +use roc_mono::ir::ErasedField; +use roc_mono::layout::{ + Builtin, InLayout, Layout, LayoutIds, LayoutInterner, LayoutRepr, STLayoutInterner, UnionLayout, +}; + +use super::build::{cast_if_necessary_for_opaque_recursive_pointers, load_roc_value, FunctionSpec}; +use super::convert::{argument_type_from_layout, argument_type_from_union_layout}; +use super::erased; + +pub struct PointerToRefcount<'ctx> { + value: PointerValue<'ctx>, +} + +impl<'ctx> PointerToRefcount<'ctx> { + /// # Safety + /// + /// the invariant is that the given pointer really points to the refcount, + /// not the data, and only is the start of the allocated buffer if the + /// alignment works out that way. + pub unsafe fn from_ptr<'a, 'env>(env: &Env<'a, 'ctx, 'env>, ptr: PointerValue<'ctx>) -> Self { + // must make sure it's a pointer to usize + let refcount_type = env.ptr_int(); + + let value = env.builder.new_build_pointer_cast( + ptr, + refcount_type.ptr_type(AddressSpace::default()), + "to_refcount_ptr", + ); + + Self { value } + } + + pub fn from_ptr_to_data<'a, 'env>( + env: &Env<'a, 'ctx, 'env>, + data_ptr: PointerValue<'ctx>, + ) -> Self { + let builder = env.builder; + // pointer to usize + let refcount_type = env.ptr_int(); + let refcount_ptr_type = refcount_type.ptr_type(AddressSpace::default()); + + let ptr_as_usize_ptr = + builder.new_build_pointer_cast(data_ptr, refcount_ptr_type, "as_usize_ptr"); + + // get a pointer to index -1 + let index_intvalue = refcount_type.const_int(-1_i64 as u64, false); + let refcount_ptr = unsafe { + builder.new_build_in_bounds_gep( + env.ptr_int(), + ptr_as_usize_ptr, + &[index_intvalue], + "get_rc_ptr", + ) + }; + + Self { + value: refcount_ptr, + } + } + + pub fn is_1<'a, 'env>(&self, env: &Env<'a, 'ctx, 'env>) -> IntValue<'ctx> { + let current = self.get_refcount(env); + let one = match env.target_info.ptr_width() { + roc_target::PtrWidth::Bytes4 => { + env.context.i32_type().const_int(i32::MIN as u64, false) + } + roc_target::PtrWidth::Bytes8 => { + env.context.i64_type().const_int(i64::MIN as u64, false) + } + }; + + env.builder + .new_build_int_compare(IntPredicate::EQ, current, one, "is_one") + } + + fn get_refcount<'a, 'env>(&self, env: &Env<'a, 'ctx, 'env>) -> IntValue<'ctx> { + env.builder + .new_build_load(env.ptr_int(), self.value, "get_refcount") + .into_int_value() + } + + pub fn set_refcount<'a, 'env>(&self, env: &Env<'a, 'ctx, 'env>, refcount: IntValue<'ctx>) { + env.builder.new_build_store(self.value, refcount); + } + + fn modify<'a, 'env>( + &self, + mode: CallMode<'ctx>, + layout: LayoutRepr<'a>, + env: &Env<'a, 'ctx, 'env>, + layout_interner: &STLayoutInterner<'a>, + ) { + match mode { + CallMode::Inc(inc_amount) => self.increment(inc_amount, env), + CallMode::Dec => self.decrement(env, layout_interner, layout), + } + } + + fn increment<'a, 'env>(&self, amount: IntValue<'ctx>, env: &Env<'a, 'ctx, 'env>) { + incref_pointer(env, self.value, amount); + } + + pub fn decrement<'a, 'env>( + &self, + env: &Env<'a, 'ctx, 'env>, + layout_interner: &STLayoutInterner<'a>, + layout: LayoutRepr<'a>, + ) { + let alignment = layout + .allocation_alignment_bytes(layout_interner) + .max(env.target_info.ptr_width() as u32); + + let context = env.context; + let block = env.builder.get_insert_block().expect("to be in a function"); + let di_location = env.builder.get_current_debug_location().unwrap(); + + let fn_name = &format!("decrement_refcounted_ptr_{alignment}"); + + let function = match env.module.get_function(fn_name) { + Some(function_value) => function_value, + None => { + // inc and dec return void + let fn_type = context.void_type().fn_type( + &[env.ptr_int().ptr_type(AddressSpace::default()).into()], + false, + ); + + let function_value = add_func( + env.context, + env.module, + fn_name, + FunctionSpec::known_fastcc(fn_type), + Linkage::Internal, + ); + + let subprogram = env.new_subprogram(fn_name); + function_value.set_subprogram(subprogram); + + Self::build_decrement_function_body(env, function_value, alignment); + + function_value + } + }; + + let refcount_ptr = self.value; + + env.builder.position_at_end(block); + env.builder.set_current_debug_location(di_location); + + let call = env + .builder + .new_build_call(function, &[refcount_ptr.into()], fn_name); + + call.set_call_convention(FAST_CALL_CONV); + } + + fn build_decrement_function_body<'a, 'env>( + env: &Env<'a, 'ctx, 'env>, + parent: FunctionValue<'ctx>, + alignment: u32, + ) { + let builder = env.builder; + let ctx = env.context; + + let entry = ctx.append_basic_block(parent, "entry"); + builder.position_at_end(entry); + + debug_info_init!(env, parent); + + decref_pointer( + env, + parent.get_nth_param(0).unwrap().into_pointer_value(), + alignment, + ); + + builder.new_build_return(None); + } + + pub fn deallocate<'a, 'env>( + &self, + env: &Env<'a, 'ctx, 'env>, + alignment: u32, + ) -> InstructionValue<'ctx> { + free_pointer(env, self.value, alignment) + } +} + +fn incref_pointer<'ctx>( + env: &Env<'_, 'ctx, '_>, + pointer: PointerValue<'ctx>, + amount: IntValue<'ctx>, +) { + call_void_bitcode_fn( + env, + &[ + env.builder + .new_build_pointer_cast( + pointer, + env.ptr_int().ptr_type(AddressSpace::default()), + "to_isize_ptr", + ) + .into(), + amount.into(), + ], + roc_builtins::bitcode::UTILS_INCREF_RC_PTR, + ); +} + +fn free_pointer<'ctx>( + env: &Env<'_, 'ctx, '_>, + pointer: PointerValue<'ctx>, + alignment: u32, +) -> InstructionValue<'ctx> { + let alignment = env.context.i32_type().const_int(alignment as _, false); + call_void_bitcode_fn( + env, + &[ + env.builder + .new_build_pointer_cast( + pointer, + env.ptr_int().ptr_type(AddressSpace::default()), + "to_isize_ptr", + ) + .into(), + alignment.into(), + ], + roc_builtins::bitcode::UTILS_FREE_RC_PTR, + ) +} + +fn decref_pointer<'ctx>(env: &Env<'_, 'ctx, '_>, pointer: PointerValue<'ctx>, alignment: u32) { + let alignment = env.context.i32_type().const_int(alignment as _, false); + call_void_bitcode_fn( + env, + &[ + env.builder + .new_build_pointer_cast( + pointer, + env.ptr_int().ptr_type(AddressSpace::default()), + "to_isize_ptr", + ) + .into(), + alignment.into(), + ], + roc_builtins::bitcode::UTILS_DECREF_RC_PTR, + ); +} + +/// Assumes a pointer to the refcount +pub fn decref_pointer_check_null<'ctx>( + env: &Env<'_, 'ctx, '_>, + pointer: PointerValue<'ctx>, + alignment: u32, +) { + let alignment = env.context.i32_type().const_int(alignment as _, false); + call_void_bitcode_fn( + env, + &[ + env.builder + .new_build_pointer_cast( + pointer, + env.context.i8_type().ptr_type(AddressSpace::default()), + "to_i8_ptr", + ) + .into(), + alignment.into(), + ], + roc_builtins::bitcode::UTILS_DECREF_CHECK_NULL, + ); +} + +fn modify_refcount_struct<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + layout_ids: &mut LayoutIds<'a>, + struct_layout: InLayout<'a>, + field_layouts: &'a [InLayout<'a>], + mode: Mode, +) -> FunctionValue<'ctx> { + let block = env.builder.get_insert_block().expect("to be in a function"); + let di_location = env.builder.get_current_debug_location().unwrap(); + + let (_, fn_name) = function_name_from_mode( + layout_ids, + &env.interns, + "increment_struct", + "decrement_struct", + layout_interner.get_repr(struct_layout), + mode, + ); + + let function = match env.module.get_function(fn_name.as_str()) { + Some(function_value) => function_value, + None => { + let arg_type = argument_type_from_layout( + env, + layout_interner, + layout_interner.get_repr(struct_layout), + ); + let function_value = build_header(env, arg_type, mode, &fn_name); + + modify_refcount_struct_help( + env, + layout_interner, + layout_ids, + mode, + struct_layout, + field_layouts, + function_value, + ); + + function_value + } + }; + + env.builder.position_at_end(block); + env.builder.set_current_debug_location(di_location); + + function +} + +#[allow(clippy::too_many_arguments)] +fn modify_refcount_struct_help<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + layout_ids: &mut LayoutIds<'a>, + mode: Mode, + struct_layout: InLayout<'a>, + field_layouts: &[InLayout<'a>], + fn_val: FunctionValue<'ctx>, +) { + let builder = env.builder; + let ctx = env.context; + + // Add a basic block for the entry point + let entry = ctx.append_basic_block(fn_val, "entry"); + + builder.position_at_end(entry); + + debug_info_init!(env, fn_val); + + // Add args to scope + let arg_symbol = Symbol::ARG_1; + let arg_val = fn_val.get_param_iter().next().unwrap(); + + arg_val.set_name(arg_symbol.as_str(&env.interns)); + + let wrapper_struct = RocStruct::from(arg_val); + + for (i, field_layout) in field_layouts.iter().enumerate() { + if layout_interner.contains_refcounted(*field_layout) { + let field_value = wrapper_struct.load_at_index( + env, + layout_interner, + layout_interner.get_repr(struct_layout), + i as _, + ); + + modify_refcount_layout_help( + env, + layout_interner, + layout_ids, + mode.to_call_mode(fn_val), + field_value, + *field_layout, + ); + } + } + // this function returns void + builder.new_build_return(None); +} + +fn modify_refcount_erased<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + layout_ids: &mut LayoutIds<'a>, + mode: Mode, +) -> FunctionValue<'ctx> { + let block = env.builder.get_insert_block().expect("to be in a function"); + let di_location = env.builder.get_current_debug_location().unwrap(); + + let (_, fn_name) = function_name_from_mode( + layout_ids, + &env.interns, + "increment_erased", + "decrement_erased", + layout_interner.get_repr(Layout::ERASED), + mode, + ); + + let function = match env.module.get_function(fn_name.as_str()) { + Some(function_value) => function_value, + None => { + let arg_type = erased::basic_type(env); + let function_value = build_header(env, arg_type.into(), mode, &fn_name); + + modify_refcount_erased_help(env, mode, function_value); + + function_value + } + }; + + env.builder.position_at_end(block); + env.builder.set_current_debug_location(di_location); + + function +} + +fn modify_refcount_erased_help<'ctx>( + env: &Env<'_, 'ctx, '_>, + mode: Mode, + fn_val: FunctionValue<'ctx>, +) { + let builder = env.builder; + let ctx = env.context; + + // Add a basic block for the entry point + let entry = ctx.append_basic_block(fn_val, "entry"); + + builder.position_at_end(entry); + + debug_info_init!(env, fn_val); + + // Add args to scope + let arg_symbol = Symbol::ARG_1; + let arg_val = fn_val.get_param_iter().next().unwrap().into_struct_value(); + + arg_val.set_name(arg_symbol.as_str(&env.interns)); + + let refcounter = erased::load_refcounter(env, arg_val, mode); + let refcounter_is_null = env + .builder + .new_build_is_null(refcounter, "refcounter_unset"); + + let call_refcounter_block = ctx.append_basic_block(fn_val, "call_refcounter"); + let noop_block = ctx.append_basic_block(fn_val, "noop"); + + builder.new_build_conditional_branch(refcounter_is_null, noop_block, call_refcounter_block); + { + builder.position_at_end(call_refcounter_block); + let opaque_ptr_type = erased::opaque_ptr_type(env); + let value = erased::load(env, arg_val, ErasedField::Value, opaque_ptr_type); + + builder.new_build_indirect_call( + env.context + .void_type() + .fn_type(&[opaque_ptr_type.into()], false), + refcounter, + &[value.into()], + "call_refcounter", + ); + + builder.new_build_return(None); + } + + { + builder.position_at_end(noop_block); + builder.new_build_return(None); + } +} + +pub fn increment_refcount_layout<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + layout_ids: &mut LayoutIds<'a>, + inc_amount: u64, + value: BasicValueEnum<'ctx>, + layout: InLayout<'a>, +) { + let amount = env.ptr_int().const_int(inc_amount, false); + increment_n_refcount_layout(env, layout_interner, layout_ids, amount, value, layout); +} + +pub fn increment_n_refcount_layout<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + layout_ids: &mut LayoutIds<'a>, + amount: IntValue<'ctx>, + value: BasicValueEnum<'ctx>, + layout: InLayout<'a>, +) { + modify_refcount_layout( + env, + layout_interner, + layout_ids, + CallMode::Inc(amount), + value, + layout, + ); +} + +pub fn decrement_refcount_layout<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + layout_ids: &mut LayoutIds<'a>, + value: BasicValueEnum<'ctx>, + layout: InLayout<'a>, +) { + modify_refcount_layout( + env, + layout_interner, + layout_ids, + CallMode::Dec, + value, + layout, + ); +} + +fn modify_refcount_builtin<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + layout_ids: &mut LayoutIds<'a>, + mode: Mode, + layout: InLayout<'a>, + builtin: &Builtin<'a>, +) -> Option> { + use Builtin::*; + + match builtin { + List(element_layout) => { + let function = + modify_refcount_list(env, layout_interner, layout_ids, mode, *element_layout); + + Some(function) + } + + Str => Some(modify_refcount_str( + env, + layout_interner, + layout_ids, + mode, + layout, + )), + + _ => { + debug_assert!(!builtin.is_refcounted()); + None + } + } +} + +fn modify_refcount_layout<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + layout_ids: &mut LayoutIds<'a>, + call_mode: CallMode<'ctx>, + value: BasicValueEnum<'ctx>, + layout: InLayout<'a>, +) { + modify_refcount_layout_help(env, layout_interner, layout_ids, call_mode, value, layout); +} + +fn modify_refcount_layout_help<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + layout_ids: &mut LayoutIds<'a>, + call_mode: CallMode<'ctx>, + value: BasicValueEnum<'ctx>, + layout: InLayout<'a>, +) { + let mode = match call_mode { + CallMode::Inc(_) => Mode::Inc, + CallMode::Dec => Mode::Dec, + }; + + let function = + match modify_refcount_layout_build_function(env, layout_interner, layout_ids, mode, layout) + { + Some(f) => f, + None => return, + }; + + match layout_interner.get_repr(layout) { + LayoutRepr::RecursivePointer(rec_layout) => { + let layout = rec_layout; + + let bt = basic_type_from_layout(env, layout_interner, layout_interner.get_repr(layout)); + + // cast the i64 pointer to a pointer to block of memory + let field_cast = env.builder.new_build_pointer_cast( + value.into_pointer_value(), + bt.into_pointer_type(), + "i64_to_opaque", + ); + + call_help(env, function, call_mode, field_cast.into()); + } + _ => { + call_help(env, function, call_mode, value); + } + } +} + +fn call_help<'ctx>( + env: &Env<'_, 'ctx, '_>, + function: FunctionValue<'ctx>, + call_mode: CallMode<'ctx>, + value: BasicValueEnum<'ctx>, +) -> inkwell::values::CallSiteValue<'ctx> { + let value = cast_if_necessary_for_opaque_recursive_pointers( + env.builder, + value, + function.get_params()[0].get_type(), + ); + + let call = match call_mode { + CallMode::Inc(inc_amount) => { + env.builder + .new_build_call(function, &[value.into(), inc_amount.into()], "increment") + } + CallMode::Dec => env + .builder + .new_build_call(function, &[value.into()], "decrement"), + }; + + call.set_call_convention(FAST_CALL_CONV); + + call +} + +fn modify_refcount_layout_build_function<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + layout_ids: &mut LayoutIds<'a>, + mode: Mode, + layout: InLayout<'a>, +) -> Option> { + use LayoutRepr::*; + + match layout_interner.get_repr(layout) { + Builtin(builtin) => { + modify_refcount_builtin(env, layout_interner, layout_ids, mode, layout, &builtin) + } + + Ptr(_inner) => { + debug_assert_eq!(true, false); + + None + } + + Union(variant) => { + use UnionLayout::*; + + match variant { + NonRecursive(&[]) => { + // void type, nothing to refcount here + None + } + + NonRecursive(tags) => { + let function = + modify_refcount_nonrecursive(env, layout_interner, layout_ids, mode, tags); + + Some(function) + } + + _ => { + let function = build_rec_union(env, layout_interner, layout_ids, mode, variant); + + Some(function) + } + } + } + + Struct(field_layouts) => { + let function = modify_refcount_struct( + env, + layout_interner, + layout_ids, + layout, + field_layouts, + mode, + ); + + Some(function) + } + + LayoutRepr::RecursivePointer(rec_layout) => { + let layout = rec_layout; + + let function = modify_refcount_layout_build_function( + env, + layout_interner, + layout_ids, + mode, + layout, + )?; + + Some(function) + } + LambdaSet(lambda_set) => modify_refcount_layout_build_function( + env, + layout_interner, + layout_ids, + mode, + lambda_set.runtime_representation(), + ), + FunctionPointer(_) => None, + Erased(_) => { + let function = modify_refcount_erased(env, layout_interner, layout_ids, mode); + + Some(function) + } + } +} + +fn modify_refcount_list<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + layout_ids: &mut LayoutIds<'a>, + mode: Mode, + element_layout: InLayout<'a>, +) -> FunctionValue<'ctx> { + let block = env.builder.get_insert_block().expect("to be in a function"); + let di_location = env.builder.get_current_debug_location().unwrap(); + + let element_layout = + if let LayoutRepr::RecursivePointer(rec) = layout_interner.get_repr(element_layout) { + rec + } else { + element_layout + }; + + let list_layout = LayoutRepr::Builtin(Builtin::List(element_layout)); + let (_, fn_name) = function_name_from_mode( + layout_ids, + &env.interns, + "increment_list", + "decrement_list", + list_layout, + mode, + ); + + let function = match env.module.get_function(fn_name.as_str()) { + Some(function_value) => function_value, + None => { + let basic_type = argument_type_from_layout(env, layout_interner, list_layout); + let function_value = build_header(env, basic_type, mode, &fn_name); + + modify_refcount_list_help( + env, + layout_interner, + layout_ids, + mode, + list_layout, + element_layout, + function_value, + ); + + function_value + } + }; + + env.builder.position_at_end(block); + env.builder.set_current_debug_location(di_location); + + function +} + +fn mode_to_call_mode(function: FunctionValue<'_>, mode: Mode) -> CallMode<'_> { + match mode { + Mode::Dec => CallMode::Dec, + Mode::Inc => CallMode::Inc(function.get_nth_param(1).unwrap().into_int_value()), + } +} + +fn modify_refcount_list_help<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + layout_ids: &mut LayoutIds<'a>, + mode: Mode, + layout: LayoutRepr<'a>, + element_layout: InLayout<'a>, + fn_val: FunctionValue<'ctx>, +) { + let builder = env.builder; + let ctx = env.context; + + // Add a basic block for the entry point + let entry = ctx.append_basic_block(fn_val, "entry"); + + builder.position_at_end(entry); + + debug_info_init!(env, fn_val); + + // Add args to scope + let arg_symbol = Symbol::ARG_1; + let arg_val = fn_val.get_param_iter().next().unwrap(); + + arg_val.set_name(arg_symbol.as_str(&env.interns)); + + let parent = fn_val; + let original_wrapper = arg_val.into_struct_value(); + + // We use the raw capacity to ensure we always decrement the refcount of seamless slices. + let capacity = list_capacity_or_ref_ptr(builder, original_wrapper); + + let is_non_empty = builder.new_build_int_compare( + IntPredicate::UGT, + capacity, + env.ptr_int().const_zero(), + "cap > 0", + ); + + // build blocks + let modification_list_block = ctx.append_basic_block(parent, "modification_list_block"); + let cont_block = ctx.append_basic_block(parent, "modify_rc_list_cont"); + + builder.new_build_conditional_branch(is_non_empty, modification_list_block, cont_block); + + builder.position_at_end(modification_list_block); + + if layout_interner.contains_refcounted(element_layout) { + let ptr_type = basic_type_from_layout( + env, + layout_interner, + layout_interner.get_repr(element_layout), + ) + .ptr_type(AddressSpace::default()); + + let (len, ptr) = load_list(env.builder, original_wrapper, ptr_type); + + let loop_fn = |layout_interner, _index, element| { + modify_refcount_layout_help( + env, + layout_interner, + layout_ids, + mode.to_call_mode(fn_val), + element, + element_layout, + ); + }; + + incrementing_elem_loop( + env, + layout_interner, + parent, + element_layout, + ptr, + len, + "modify_rc_index", + loop_fn, + ); + } + + let refcount_ptr = + PointerToRefcount::from_ptr_to_data(env, list_refcount_ptr(env, original_wrapper)); + let call_mode = mode_to_call_mode(fn_val, mode); + refcount_ptr.modify(call_mode, layout, env, layout_interner); + + builder.new_build_unconditional_branch(cont_block); + + builder.position_at_end(cont_block); + + // this function returns void + builder.new_build_return(None); +} + +fn modify_refcount_str<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + layout_ids: &mut LayoutIds<'a>, + mode: Mode, + layout: InLayout<'a>, +) -> FunctionValue<'ctx> { + let block = env.builder.get_insert_block().expect("to be in a function"); + let di_location = env.builder.get_current_debug_location().unwrap(); + + let (_, fn_name) = function_name_from_mode( + layout_ids, + &env.interns, + "increment_str", + "decrement_str", + layout_interner.get_repr(layout), + mode, + ); + + let function = match env.module.get_function(fn_name.as_str()) { + Some(function_value) => function_value, + None => { + let basic_type = + argument_type_from_layout(env, layout_interner, layout_interner.get_repr(layout)); + let function_value = build_header(env, basic_type, mode, &fn_name); + + modify_refcount_str_help(env, layout_interner, mode, layout, function_value); + + function_value + } + }; + + env.builder.position_at_end(block); + env.builder.set_current_debug_location(di_location); + + function +} + +fn modify_refcount_str_help<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + mode: Mode, + layout: InLayout<'a>, + fn_val: FunctionValue<'ctx>, +) { + let builder = env.builder; + let ctx = env.context; + + // Add a basic block for the entry point + let entry = ctx.append_basic_block(fn_val, "entry"); + + builder.position_at_end(entry); + + debug_info_init!(env, fn_val); + + // Add args to scope + let arg_symbol = Symbol::ARG_1; + let arg_val = fn_val.get_param_iter().next().unwrap(); + + arg_val.set_name(arg_symbol.as_str(&env.interns)); + + let parent = fn_val; + + let str_type = zig_str_type(env); + let str_wrapper = if LayoutRepr::Builtin(Builtin::Str).is_passed_by_reference(layout_interner) { + env.builder + .new_build_load(str_type, arg_val.into_pointer_value(), "load_str_to_stack") + } else { + // it's already a struct, just do nothing + debug_assert!(arg_val.is_struct_value()); + arg_val + }; + let str_wrapper = str_wrapper.into_struct_value(); + + let capacity = builder + .build_extract_value(str_wrapper, Builtin::WRAPPER_CAPACITY, "read_str_capacity") + .unwrap() + .into_int_value(); + + // Small strings have 1 as the first bit of capacity, making them negative. + // Thus, to check for big and non empty, just needs a signed len > 0. + let is_big_and_non_empty = builder.new_build_int_compare( + IntPredicate::SGT, + capacity, + env.ptr_int().const_zero(), + "is_big_str", + ); + + // the block we'll always jump to when we're done + let cont_block = ctx.append_basic_block(parent, "modify_rc_str_cont"); + let modification_block = ctx.append_basic_block(parent, "modify_rc"); + + builder.new_build_conditional_branch(is_big_and_non_empty, modification_block, cont_block); + builder.position_at_end(modification_block); + + let refcount_ptr = PointerToRefcount::from_ptr_to_data(env, str_refcount_ptr(env, arg_val)); + let call_mode = mode_to_call_mode(fn_val, mode); + refcount_ptr.modify( + call_mode, + layout_interner.get_repr(layout), + env, + layout_interner, + ); + + builder.new_build_unconditional_branch(cont_block); + + builder.position_at_end(cont_block); + + // this function returns void + builder.new_build_return(None); +} + +/// Build an increment or decrement function for a specific layout +fn build_header<'ctx>( + env: &Env<'_, 'ctx, '_>, + arg_type: BasicTypeEnum<'ctx>, + mode: Mode, + fn_name: &str, +) -> FunctionValue<'ctx> { + match mode { + Mode::Inc => build_header_help( + env, + fn_name, + env.context.void_type().into(), + &[arg_type, env.ptr_int().into()], + ), + Mode::Dec => build_header_help(env, fn_name, env.context.void_type().into(), &[arg_type]), + } +} + +/// Build an increment or decrement function for a specific layout +pub fn build_header_help<'ctx>( + env: &Env<'_, 'ctx, '_>, + fn_name: &str, + return_type: AnyTypeEnum<'ctx>, + arguments: &[BasicTypeEnum<'ctx>], +) -> FunctionValue<'ctx> { + use inkwell::types::AnyTypeEnum::*; + + let it = arguments.iter().map(|x| BasicMetadataTypeEnum::from(*x)); + let vec = Vec::from_iter_in(it, env.arena); + let arguments = vec.as_slice(); + + let fn_type = match return_type { + ArrayType(t) => t.fn_type(arguments, false), + FloatType(t) => t.fn_type(arguments, false), + FunctionType(_) => unreachable!("functions cannot return functions"), + IntType(t) => t.fn_type(arguments, false), + PointerType(t) => t.fn_type(arguments, false), + StructType(t) => t.fn_type(arguments, false), + VectorType(t) => t.fn_type(arguments, false), + VoidType(t) => t.fn_type(arguments, false), + }; + + // this should be `Linkage::Private`, but that will remove all of the code for the inc/dec + // functions on windows. LLVM just does not emit the assembly for them. Investigate why this is + let linkage = if let roc_target::OperatingSystem::Windows = env.target_info.operating_system { + Linkage::External + } else { + Linkage::Private + }; + + let fn_val = add_func( + env.context, + env.module, + fn_name, + FunctionSpec::known_fastcc(fn_type), + linkage, + ); + + let subprogram = env.new_subprogram(fn_name); + fn_val.set_subprogram(subprogram); + + env.dibuilder.finalize(); + + fn_val +} + +#[derive(Clone, Copy)] +pub enum Mode { + Inc, + Dec, +} + +impl Mode { + fn to_call_mode(self, function: FunctionValue<'_>) -> CallMode<'_> { + match self { + Mode::Inc => { + let amount = function.get_nth_param(1).unwrap().into_int_value(); + + CallMode::Inc(amount) + } + Mode::Dec => CallMode::Dec, + } + } +} + +#[derive(Clone, Copy)] +enum CallMode<'ctx> { + Inc(IntValue<'ctx>), + Dec, +} + +fn build_rec_union<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + layout_ids: &mut LayoutIds<'a>, + mode: Mode, + union_layout: UnionLayout<'a>, +) -> FunctionValue<'ctx> { + let layout = LayoutRepr::Union(union_layout); + + let (_, fn_name) = function_name_from_mode( + layout_ids, + &env.interns, + "increment_rec_union", + "decrement_rec_union", + layout, + mode, + ); + + let function = match env.module.get_function(fn_name.as_str()) { + Some(function_value) => function_value, + None => { + let block = env.builder.get_insert_block().expect("to be in a function"); + let di_location = env.builder.get_current_debug_location().unwrap(); + + let basic_type = basic_type_from_layout(env, layout_interner, layout); + let function_value = build_header(env, basic_type, mode, &fn_name); + + build_rec_union_help( + env, + layout_interner, + layout_ids, + mode, + union_layout, + function_value, + ); + + env.builder.position_at_end(block); + env.builder.set_current_debug_location(di_location); + + function_value + } + }; + + function +} + +#[allow(clippy::too_many_arguments)] +fn build_rec_union_help<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + layout_ids: &mut LayoutIds<'a>, + mode: Mode, + union_layout: UnionLayout<'a>, + fn_val: FunctionValue<'ctx>, +) { + let tags = union_layout_tags(env.arena, &union_layout); + debug_assert!(!tags.tags.is_empty()); + + let context = &env.context; + let builder = env.builder; + + // Add a basic block for the entry point + let entry = context.append_basic_block(fn_val, "entry"); + + builder.position_at_end(entry); + + debug_info_init!(env, fn_val); + + // Add args to scope + let arg_symbol = Symbol::ARG_1; + + let arg_val = fn_val.get_param_iter().next().unwrap(); + + arg_val.set_name(arg_symbol.as_str(&env.interns)); + + let parent = fn_val; + + debug_assert!(arg_val.is_pointer_value()); + let current_tag_id = get_tag_id(env, layout_interner, fn_val, &union_layout, arg_val); + let value_ptr = if union_layout.stores_tag_id_in_pointer(env.target_info) { + tag_pointer_clear_tag_id(env, arg_val.into_pointer_value()) + } else { + arg_val.into_pointer_value() + }; + + let should_recurse_block = env.context.append_basic_block(parent, "should_recurse"); + + let ctx = env.context; + if union_layout.is_nullable() { + let is_null = env.builder.new_build_is_null(value_ptr, "is_null"); + + let then_block = ctx.append_basic_block(parent, "then"); + + env.builder + .new_build_conditional_branch(is_null, then_block, should_recurse_block); + + { + env.builder.position_at_end(then_block); + env.builder.new_build_return(None); + } + } else { + env.builder + .new_build_unconditional_branch(should_recurse_block); + } + + env.builder.position_at_end(should_recurse_block); + + // to increment/decrement the cons-cell itself + let refcount_ptr = PointerToRefcount::from_ptr_to_data(env, value_ptr); + let call_mode = mode_to_call_mode(fn_val, mode); + + let layout = LayoutRepr::Union(union_layout); + + match mode { + Mode::Inc => { + // inc is cheap; we never recurse + refcount_ptr.modify(call_mode, layout, env, layout_interner); + env.builder.new_build_return(None); + } + + Mode::Dec => { + let do_recurse_block = env.context.append_basic_block(parent, "do_recurse"); + let no_recurse_block = env.context.append_basic_block(parent, "no_recurse"); + + builder.new_build_conditional_branch( + refcount_ptr.is_1(env), + do_recurse_block, + no_recurse_block, + ); + + { + env.builder.position_at_end(no_recurse_block); + + refcount_ptr.modify(call_mode, layout, env, layout_interner); + env.builder.new_build_return(None); + } + + { + env.builder.position_at_end(do_recurse_block); + + build_rec_union_recursive_decrement( + env, + layout_interner, + layout_ids, + parent, + fn_val, + union_layout, + tags, + value_ptr, + current_tag_id, + refcount_ptr, + do_recurse_block, + DecOrReuse::Dec, + ) + } + } + } +} + +enum DecOrReuse { + Dec, + Reuse, +} + +fn fields_need_no_refcounting(interner: &STLayoutInterner, field_layouts: &[InLayout]) -> bool { + !field_layouts.iter().any(|x| { + let x = interner.get_repr(*x); + x.is_refcounted(interner) || x.contains_refcounted(interner) + }) +} + +#[allow(clippy::too_many_arguments)] +fn build_rec_union_recursive_decrement<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + layout_ids: &mut LayoutIds<'a>, + parent: FunctionValue<'ctx>, + decrement_fn: FunctionValue<'ctx>, + union_layout: UnionLayout<'a>, + tags: UnionLayoutTags<'a>, + value_ptr: PointerValue<'ctx>, + current_tag_id: IntValue<'ctx>, + refcount_ptr: PointerToRefcount<'ctx>, + match_block: BasicBlock<'ctx>, + decrement_or_reuse: DecOrReuse, +) { + let mode = Mode::Dec; + let call_mode = mode_to_call_mode(decrement_fn, mode); + let builder = env.builder; + + let UnionLayoutTags { nullable_id, tags } = tags; + + // next, make a jump table for all possible values of the tag_id + let mut cases = Vec::with_capacity_in(tags.len(), env.arena); + + let tag_id_int_type = basic_type_from_layout( + env, + layout_interner, + layout_interner.get_repr(union_layout.tag_id_layout()), + ) + .into_int_type(); + + for (tag_id, field_layouts) in tags.iter().enumerate() { + let tag_id = match nullable_id { + Some(null_id) if tag_id as u16 >= null_id => { + // This tag comes after the nullable tag, so its ID is one higher than the + // enumeration says. + tag_id + 1 + } + _ => tag_id, + }; + + // if none of the fields are or contain anything refcounted, just move on + if fields_need_no_refcounting(layout_interner, field_layouts) { + continue; + } + + let block = env.context.append_basic_block(parent, "tag_id_decrement"); + + env.builder.position_at_end(block); + + let fields_struct = LayoutRepr::struct_(field_layouts); + let wrapper_type = basic_type_from_layout(env, layout_interner, fields_struct); + + // cast the opaque pointer to a pointer of the correct shape + let struct_ptr = env.builder.new_build_pointer_cast( + value_ptr, + wrapper_type.ptr_type(AddressSpace::default()), + "opaque_to_correct_recursive_decrement", + ); + + // defer actually performing the refcount modifications until after the current cell has + // been decremented, see below + let mut deferred_rec = Vec::new_in(env.arena); + let mut deferred_nonrec = Vec::new_in(env.arena); + + for (i, field_layout) in field_layouts.iter().enumerate() { + if let LayoutRepr::RecursivePointer(_) = layout_interner.get_repr(*field_layout) { + // this field has type `*i64`, but is really a pointer to the data we want + let elem_pointer = env.builder.new_build_struct_gep( + wrapper_type.into_struct_type(), + struct_ptr, + i as u32, + "gep_recursive_pointer", + ); + + let ptr_as_i64_ptr = env.builder.new_build_load( + env.context.i64_type().ptr_type(AddressSpace::default()), + elem_pointer, + "load_recursive_pointer", + ); + + debug_assert!(ptr_as_i64_ptr.is_pointer_value()); + + // therefore we must cast it to our desired type + let union_layout = LayoutRepr::Union(union_layout); + let union_type = basic_type_from_layout(env, layout_interner, union_layout); + let recursive_field_ptr = cast_basic_basic(env.builder, ptr_as_i64_ptr, union_type); + + deferred_rec.push(recursive_field_ptr); + } else if layout_interner.contains_refcounted(*field_layout) { + let elem_pointer = env.builder.new_build_struct_gep( + wrapper_type.into_struct_type(), + struct_ptr, + i as u32, + "gep_recursive_pointer", + ); + + let field = load_roc_value( + env, + layout_interner, + layout_interner.get_repr(*field_layout), + elem_pointer, + "decrement_struct_field", + ); + + deferred_nonrec.push((field, field_layout)); + } + } + + // OPTIMIZATION + // + // We really would like `inc/dec` to be tail-recursive; it gives roughly a 2X speedup on linked + // lists. To achieve it, we must first load all fields that we want to inc/dec (done above) + // and store them on the stack, then modify (and potentially free) the current cell, then + // actually inc/dec the fields. + + match decrement_or_reuse { + DecOrReuse::Reuse => {} + DecOrReuse::Dec => { + let union_layout = LayoutRepr::Union(union_layout); + refcount_ptr.modify(call_mode, union_layout, env, layout_interner); + } + } + + for (field, field_layout) in deferred_nonrec { + modify_refcount_layout_help( + env, + layout_interner, + layout_ids, + mode.to_call_mode(decrement_fn), + field, + *field_layout, + ); + } + + for ptr in deferred_rec { + // recursively decrement the field + let call = call_help(env, decrement_fn, mode.to_call_mode(decrement_fn), ptr); + call.set_tail_call(true); + } + + // this function returns void + builder.new_build_return(None); + + cases.push((tag_id_int_type.const_int(tag_id as u64, false), block)); + } + + env.builder.position_at_end(match_block); + + cases.reverse(); + + if matches!( + union_layout, + UnionLayout::NullableUnwrapped { .. } | UnionLayout::NonNullableUnwrapped { .. } + ) { + debug_assert!(cases.len() <= 1, "{cases:?}"); + + if cases.is_empty() { + // The only other layout doesn't need refcounting. Pass through. + builder.new_build_return(None); + } else { + // in this case, don't switch, because the `else` branch below would try to read the (nonexistent) tag id + let (_, only_branch) = cases.pop().unwrap(); + env.builder.new_build_unconditional_branch(only_branch); + } + } else { + let default_block = env.context.append_basic_block(parent, "switch_default"); + + // switch on it + env.builder + .new_build_switch(current_tag_id, default_block, &cases); + + { + env.builder.position_at_end(default_block); + + // increment/decrement the cons-cell itself + if let DecOrReuse::Dec = decrement_or_reuse { + let union_layout = LayoutRepr::Union(union_layout); + refcount_ptr.modify(call_mode, union_layout, env, layout_interner); + } + } + + // this function returns void + builder.new_build_return(None); + } +} + +#[derive(Debug)] +struct UnionLayoutTags<'a> { + nullable_id: Option, + tags: &'a [&'a [InLayout<'a>]], +} + +fn union_layout_tags<'a>( + arena: &'a bumpalo::Bump, + union_layout: &UnionLayout<'a>, +) -> UnionLayoutTags<'a> { + use UnionLayout::*; + + match union_layout { + NullableWrapped { + other_tags, + nullable_id, + } => UnionLayoutTags { + nullable_id: Some(*nullable_id), + tags: other_tags, + }, + NullableUnwrapped { + other_fields, + nullable_id, + } => UnionLayoutTags { + nullable_id: Some(*nullable_id as u16), + tags: arena.alloc([*other_fields]), + }, + NonNullableUnwrapped(fields) => UnionLayoutTags { + nullable_id: None, + tags: arena.alloc([*fields]), + }, + Recursive(tags) => UnionLayoutTags { + nullable_id: None, + tags, + }, + NonRecursive(tags) => UnionLayoutTags { + nullable_id: None, + tags, + }, + } +} + +pub fn build_reset<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + layout_ids: &mut LayoutIds<'a>, + union_layout: UnionLayout<'a>, +) -> FunctionValue<'ctx> { + let mode = Mode::Dec; + + let union_layout_repr = LayoutRepr::Union(union_layout); + let layout_id = layout_ids.get(Symbol::DEC, &union_layout_repr); + let fn_name = layout_id.to_symbol_string(Symbol::DEC, &env.interns); + let fn_name = format!("{fn_name}_reset"); + + let dec_function = build_rec_union(env, layout_interner, layout_ids, Mode::Dec, union_layout); + + let function = match env.module.get_function(fn_name.as_str()) { + Some(function_value) => function_value, + None => { + let block = env.builder.get_insert_block().expect("to be in a function"); + let di_location = env.builder.get_current_debug_location().unwrap(); + + let basic_type = basic_type_from_layout(env, layout_interner, union_layout_repr); + let function_value = build_header(env, basic_type, mode, &fn_name); + + build_reuse_rec_union_help( + env, + layout_interner, + layout_ids, + union_layout, + function_value, + dec_function, + ); + + env.builder.position_at_end(block); + env.builder.set_current_debug_location(di_location); + + function_value + } + }; + + function +} + +#[allow(clippy::too_many_arguments)] +fn build_reuse_rec_union_help<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + layout_ids: &mut LayoutIds<'a>, + union_layout: UnionLayout<'a>, + reset_function: FunctionValue<'ctx>, + dec_function: FunctionValue<'ctx>, +) { + let tags = union_layout_tags(env.arena, &union_layout); + + debug_assert!(!tags.tags.is_empty()); + + let context = &env.context; + let builder = env.builder; + + // Add a basic block for the entry point + let entry = context.append_basic_block(reset_function, "entry"); + + builder.position_at_end(entry); + + debug_info_init!(env, reset_function); + + // Add args to scope + let arg_symbol = Symbol::ARG_1; + + let arg_val = reset_function.get_param_iter().next().unwrap(); + + arg_val.set_name(arg_symbol.as_str(&env.interns)); + + let parent = reset_function; + + debug_assert!(arg_val.is_pointer_value()); + let current_tag_id = get_tag_id(env, layout_interner, reset_function, &union_layout, arg_val); + let value_ptr = tag_pointer_clear_tag_id(env, arg_val.into_pointer_value()); + + // to increment/decrement the cons-cell itself + let refcount_ptr = PointerToRefcount::from_ptr_to_data(env, value_ptr); + let call_mode = CallMode::Dec; + + let should_recurse_block = env.context.append_basic_block(parent, "should_recurse"); + + let ctx = env.context; + if union_layout.is_nullable() { + let is_null = env.builder.new_build_is_null(value_ptr, "is_null"); + + let then_block = ctx.append_basic_block(parent, "then"); + + env.builder + .new_build_conditional_branch(is_null, then_block, should_recurse_block); + + { + env.builder.position_at_end(then_block); + env.builder.new_build_return(None); + } + } else { + env.builder + .new_build_unconditional_branch(should_recurse_block); + } + + env.builder.position_at_end(should_recurse_block); + + let layout = LayoutRepr::Union(union_layout); + + let do_recurse_block = env.context.append_basic_block(parent, "do_recurse"); + let no_recurse_block = env.context.append_basic_block(parent, "no_recurse"); + + builder.new_build_conditional_branch( + refcount_ptr.is_1(env), + do_recurse_block, + no_recurse_block, + ); + + { + env.builder.position_at_end(no_recurse_block); + + refcount_ptr.modify(call_mode, layout, env, layout_interner); + env.builder.new_build_return(None); + } + + { + env.builder.position_at_end(do_recurse_block); + + build_rec_union_recursive_decrement( + env, + layout_interner, + layout_ids, + parent, + dec_function, + union_layout, + tags, + value_ptr, + current_tag_id, + refcount_ptr, + do_recurse_block, + DecOrReuse::Reuse, + ) + } +} + +fn function_name_from_mode<'a>( + layout_ids: &mut LayoutIds<'a>, + interns: &Interns, + if_inc: &'static str, + if_dec: &'static str, + layout: LayoutRepr<'a>, + mode: Mode, +) -> (&'static str, String) { + // NOTE this is not a typo, we always determine the layout ID + // using the DEC symbol. Anything that is incrementing must also be + // decremented, so `dec` is used on more layouts. That can cause the + // layout ids of the inc and dec versions to be different, which is + // rather confusing, so now `inc_x` always corresponds to `dec_x` + let layout_id = layout_ids.get(Symbol::DEC, &layout); + match mode { + Mode::Inc => (if_inc, layout_id.to_symbol_string(Symbol::INC, interns)), + Mode::Dec => (if_dec, layout_id.to_symbol_string(Symbol::DEC, interns)), + } +} + +fn modify_refcount_nonrecursive<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + layout_ids: &mut LayoutIds<'a>, + mode: Mode, + fields: &'a [&'a [InLayout<'a>]], +) -> FunctionValue<'ctx> { + let union_layout = UnionLayout::NonRecursive(fields); + let layout = LayoutRepr::Union(union_layout); + + let block = env.builder.get_insert_block().expect("to be in a function"); + let di_location = env.builder.get_current_debug_location().unwrap(); + + let (_, fn_name) = function_name_from_mode( + layout_ids, + &env.interns, + "increment_union", + "decrement_union", + layout, + mode, + ); + + let function = match env.module.get_function(fn_name.as_str()) { + Some(function_value) => function_value, + None => { + let basic_type = argument_type_from_union_layout(env, layout_interner, &union_layout); + let function_value = build_header(env, basic_type, mode, &fn_name); + + modify_refcount_nonrecursive_help( + env, + layout_interner, + layout_ids, + mode, + fields, + function_value, + ); + + function_value + } + }; + + env.builder.position_at_end(block); + env.builder.set_current_debug_location(di_location); + + function +} + +fn modify_refcount_nonrecursive_help<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + layout_ids: &mut LayoutIds<'a>, + mode: Mode, + tags: &'a [&'a [InLayout<'a>]], + fn_val: FunctionValue<'ctx>, +) { + debug_assert!(!tags.is_empty()); + + let context = &env.context; + let builder = env.builder; + + // Add a basic block for the entry point + let entry = context.append_basic_block(fn_val, "entry"); + + builder.position_at_end(entry); + + debug_info_init!(env, fn_val); + + // Add args to scope + let arg_symbol = Symbol::ARG_1; + let arg_ptr = fn_val.get_param_iter().next().unwrap().into_pointer_value(); + + arg_ptr.set_name(arg_symbol.as_str(&env.interns)); + + let parent = fn_val; + + let before_block = env.builder.get_insert_block().expect("to be in a function"); + + let union_layout = UnionLayout::NonRecursive(tags); + let layout = LayoutRepr::Union(union_layout); + let union_struct_type = basic_type_from_layout(env, layout_interner, layout).into_struct_type(); + + // read the tag_id + let tag_id_ptr = env.builder.new_build_struct_gep( + union_struct_type, + arg_ptr, + RocUnion::TAG_ID_INDEX, + "tag_id_ptr", + ); + + let tag_id = env + .builder + .new_build_load( + basic_type_from_layout( + env, + layout_interner, + layout_interner.get_repr(union_layout.tag_id_layout()), + ), + tag_id_ptr, + "load_tag_id", + ) + .into_int_value(); + + let tag_id_u8 = + env.builder + .new_build_int_cast_sign_flag(tag_id, env.context.i8_type(), false, "tag_id_u8"); + + // next, make a jump table for all possible values of the tag_id + let mut cases = Vec::with_capacity_in(tags.len(), env.arena); + + let merge_block = env + .context + .append_basic_block(parent, "modify_rc_union_merge"); + + for (tag_id, field_layouts) in tags.iter().enumerate() { + // if none of the fields are or contain anything refcounted, just move on + if !field_layouts.iter().any(|x| { + let x = layout_interner.get_repr(*x); + x.is_refcounted(layout_interner) || x.contains_refcounted(layout_interner) + }) { + continue; + } + + let block = env.context.append_basic_block(parent, "tag_id_modify"); + env.builder.position_at_end(block); + + let fields_struct = LayoutRepr::struct_(field_layouts); + let data_struct_type = basic_type_from_layout(env, layout_interner, fields_struct); + + debug_assert!(data_struct_type.is_struct_type()); + let data_struct_type = data_struct_type.into_struct_type(); + let opaque_tag_data_ptr = env.builder.new_build_struct_gep( + union_struct_type, + arg_ptr, + RocUnion::TAG_DATA_INDEX, + "field_ptr", + ); + + let cast_tag_data_pointer = env.builder.new_build_pointer_cast( + opaque_tag_data_ptr, + data_struct_type.ptr_type(AddressSpace::default()), + "cast_to_concrete_tag", + ); + + for (i, field_layout) in field_layouts.iter().enumerate() { + if let LayoutRepr::RecursivePointer(union_layout) = + layout_interner.get_repr(*field_layout) + { + // This field is a pointer to the recursive pointer. + let field_ptr = env.builder.new_build_struct_gep( + data_struct_type, + cast_tag_data_pointer, + i as u32, + "modify_tag_field", + ); + + // This is the actual pointer to the recursive data. + let field_value = env.builder.new_build_load( + env.context.i64_type().ptr_type(AddressSpace::default()), + field_ptr, + "load_recursive_pointer", + ); + + debug_assert!(field_value.is_pointer_value()); + + // therefore we must cast it to our desired type + let union_type = basic_type_from_layout( + env, + layout_interner, + layout_interner.get_repr(union_layout), + ); + let recursive_ptr_field_value = + cast_basic_basic(env.builder, field_value, union_type); + + modify_refcount_layout_help( + env, + layout_interner, + layout_ids, + mode.to_call_mode(fn_val), + recursive_ptr_field_value, + *field_layout, + ) + } else if layout_interner.contains_refcounted(*field_layout) { + let field_ptr = env.builder.new_build_struct_gep( + data_struct_type, + cast_tag_data_pointer, + i as u32, + "modify_tag_field", + ); + + let field_value = if layout_interner.is_passed_by_reference(*field_layout) { + field_ptr.into() + } else { + env.builder.new_build_load( + basic_type_from_layout( + env, + layout_interner, + layout_interner.get_repr(*field_layout), + ), + field_ptr, + "field_value", + ) + }; + + modify_refcount_layout_help( + env, + layout_interner, + layout_ids, + mode.to_call_mode(fn_val), + field_value, + *field_layout, + ); + } + } + + env.builder.new_build_unconditional_branch(merge_block); + + cases.push((env.context.i8_type().const_int(tag_id as u64, false), block)); + } + + env.builder.position_at_end(before_block); + + env.builder.new_build_switch(tag_id_u8, merge_block, &cases); + + env.builder.position_at_end(merge_block); + + // this function returns void + builder.new_build_return(None); +} diff --git a/crates/compiler/gen_llvm/src/llvm/scope.rs b/crates/compiler/gen_llvm/src/llvm/scope.rs new file mode 100644 index 0000000000..a771700e66 --- /dev/null +++ b/crates/compiler/gen_llvm/src/llvm/scope.rs @@ -0,0 +1,99 @@ +use inkwell::{ + basic_block::BasicBlock, + values::{BasicValueEnum, FunctionValue, PhiValue}, +}; +use roc_collections::ImMap; +use roc_module::symbol::{ModuleId, Symbol}; +use roc_mono::{ + ir::{JoinPointId, Param, ProcLayout}, + layout::InLayout, +}; + +#[derive(Default, Debug, Clone, PartialEq, Eq)] +pub(crate) struct Scope<'a, 'ctx> { + symbols: ImMap, BasicValueEnum<'ctx>)>, + top_level_thunks: ImMap, FunctionValue<'ctx>)>, + join_points: ImMap, Vec>)>, +} + +#[derive(Debug)] +pub(crate) struct JoinPointNotFound; + +impl<'a, 'ctx> Scope<'a, 'ctx> { + pub fn insert(&mut self, symbol: Symbol, layout: InLayout<'a>, value: BasicValueEnum<'ctx>) { + self.symbols.insert(symbol, (layout, value)); + } + + pub fn load_symbol(&self, symbol: &Symbol) -> BasicValueEnum<'ctx> { + match self.symbols.get(symbol) { + Some((_, ptr)) => *ptr, + + None => panic!("There was no entry for {symbol:?} {symbol} in scope {self:?}"), + } + } + + pub fn load_symbol_and_layout(&self, symbol: &Symbol) -> (BasicValueEnum<'ctx>, InLayout<'a>) { + match self.symbols.get(symbol) { + Some((layout, ptr)) => (*ptr, *layout), + None => panic!("There was no entry for {symbol:?} in scope {self:?}"), + } + } + + pub fn insert_top_level_thunk( + &mut self, + symbol: Symbol, + layout: ProcLayout<'a>, + function_value: FunctionValue<'ctx>, + ) { + self.top_level_thunks + .insert(symbol, (layout, function_value)); + } + + pub fn remove(&mut self, symbol: &Symbol) { + self.symbols.remove(symbol); + } + + pub fn retain_top_level_thunks_for_module(&mut self, module_id: ModuleId) { + self.top_level_thunks + .retain(|s, _| s.module_id() == module_id); + } + + pub fn insert_join_point( + &mut self, + join_point_id: JoinPointId, + bb: BasicBlock<'ctx>, + phis: Vec>, + ) { + self.join_points.insert(join_point_id, (bb, phis)); + } + + pub fn remove_join_point(&mut self, join_point_id: JoinPointId) { + self.join_points.remove(&join_point_id); + } + + pub fn get_join_point( + &self, + join_point_id: JoinPointId, + ) -> Option<&(BasicBlock<'ctx>, Vec>)> { + self.join_points.get(&join_point_id) + } + + pub fn bind_parameters_to_join_point( + &mut self, + join_point_id: JoinPointId, + parameters: impl IntoIterator>, + ) -> Result<(), JoinPointNotFound> { + let ref_join_points = &self + .join_points + .get(&join_point_id) + .ok_or(JoinPointNotFound)? + .1; + + for (phi_value, param) in ref_join_points.iter().zip(parameters.into_iter()) { + let value = phi_value.as_basic_value(); + self.symbols.insert(param.symbol, (param.layout, value)); + } + + Ok(()) + } +} diff --git a/crates/compiler/gen_llvm/src/llvm/struct_.rs b/crates/compiler/gen_llvm/src/llvm/struct_.rs new file mode 100644 index 0000000000..0b46ad6df6 --- /dev/null +++ b/crates/compiler/gen_llvm/src/llvm/struct_.rs @@ -0,0 +1,259 @@ +//! Representation of structs in the generated LLVM IR. + +use bumpalo::collections::Vec as AVec; +use inkwell::{ + types::StructType, + values::{BasicValue, BasicValueEnum, PointerValue, StructValue}, +}; +use roc_module::symbol::Symbol; +use roc_mono::layout::{InLayout, LayoutInterner, LayoutRepr, STLayoutInterner}; + +use crate::llvm::build::{load_roc_value, use_roc_value}; + +use super::{ + build::{BuilderExt, Env}, + convert::basic_type_from_layout, + scope::Scope, +}; + +#[derive(Debug)] +pub(crate) enum RocStruct<'ctx> { + /// The roc struct should be passed by rvalue. + ByValue(StructValue<'ctx>), + + /// The roc struct should be passed by reference. + ByReference(PointerValue<'ctx>), +} + +impl<'ctx> From> for BasicValueEnum<'ctx> { + fn from(roc_struct: RocStruct<'ctx>) -> Self { + roc_struct.as_basic_value_enum() + } +} + +impl<'ctx> From> for RocStruct<'ctx> { + #[track_caller] + fn from(basic_value: BasicValueEnum<'ctx>) -> Self { + match basic_value { + BasicValueEnum::StructValue(struct_value) => RocStruct::ByValue(struct_value), + BasicValueEnum::PointerValue(struct_ptr) => RocStruct::ByReference(struct_ptr), + _ => panic!("Expected struct or pointer value"), + } + } +} + +impl<'ctx> RocStruct<'ctx> { + pub fn build<'a>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + layout_repr: LayoutRepr<'a>, + scope: &Scope<'a, 'ctx>, + sorted_fields: &[Symbol], + ) -> Self { + let BuildStruct { + struct_type, + struct_val, + } = build_struct_helper(env, layout_interner, scope, sorted_fields); + + let passed_by_ref = layout_repr.is_passed_by_reference(layout_interner); + + if passed_by_ref { + let alloca = env.builder.new_build_alloca(struct_type, "struct_alloca"); + env.builder.new_build_store(alloca, struct_val); + RocStruct::ByReference(alloca) + } else { + RocStruct::ByValue(struct_val) + } + } + + pub fn as_basic_value_enum(&self) -> BasicValueEnum<'ctx> { + match self { + RocStruct::ByValue(struct_val) => struct_val.as_basic_value_enum(), + RocStruct::ByReference(struct_ptr) => struct_ptr.as_basic_value_enum(), + } + } + + /// Compile an [roc_mono::ir::Expr::StructAtIndex] expression. + pub fn load_at_index<'a>( + &self, + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + struct_layout: LayoutRepr<'a>, + index: u64, + ) -> BasicValueEnum<'ctx> { + let layout = if let LayoutRepr::LambdaSet(lambda_set) = struct_layout { + layout_interner.get_repr(lambda_set.runtime_representation()) + } else { + struct_layout + }; + + match (self, layout) { + (Self::ByValue(argument), LayoutRepr::Struct(field_layouts)) => { + index_struct_value(env, layout_interner, field_layouts, *argument, index) + } + (Self::ByReference(ptr), LayoutRepr::Struct(field_layouts)) => { + let struct_type = basic_type_from_layout(env, layout_interner, struct_layout); + index_struct_ptr( + env, + layout_interner, + struct_type.into_struct_type(), + field_layouts, + *ptr, + index, + ) + } + (other, layout) => { + unreachable!( + "can only index into struct layout\nValue: {:?}\nLayout: {:?}\nIndex: {:?}", + other, layout, index + ) + } + } + } +} + +fn index_struct_value<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + field_layouts: &[InLayout<'a>], + argument: StructValue<'ctx>, + index: u64, +) -> BasicValueEnum<'ctx> { + debug_assert!(!field_layouts.is_empty()); + + let field_value = get_field_from_value( + env, + argument, + index as _, + env.arena + .alloc(format!("struct_field_access_record_{index}")), + ); + + let field_layout = field_layouts[index as usize]; + + use_roc_value( + env, + layout_interner, + layout_interner.get_repr(field_layout), + field_value, + "struct_field", + ) +} + +fn index_struct_ptr<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + struct_type: StructType<'ctx>, + field_layouts: &[InLayout<'a>], + ptr: PointerValue<'ctx>, + index: u64, +) -> BasicValueEnum<'ctx> { + debug_assert!(!field_layouts.is_empty()); + + let field_layout = field_layouts[index as usize]; + let field_repr = layout_interner.get_repr(field_layout); + + let name = format!("struct_field_access_record_{index}"); + let field_value = env + .builder + .new_build_struct_gep(struct_type, ptr, index as u32, &name); + + load_roc_value( + env, + layout_interner, + field_repr, + field_value, + "struct_field", + ) +} + +fn get_field_from_value<'ctx>( + env: &Env<'_, 'ctx, '_>, + argument: StructValue<'ctx>, + index: u32, + name: &str, +) -> BasicValueEnum<'ctx> { + env.builder + .build_extract_value(argument, index, name) + .unwrap() +} + +struct BuildStruct<'ctx> { + struct_type: StructType<'ctx>, + struct_val: StructValue<'ctx>, +} + +fn build_struct_helper<'a, 'ctx>( + env: &Env<'a, 'ctx, '_>, + layout_interner: &STLayoutInterner<'a>, + scope: &Scope<'a, 'ctx>, + sorted_fields: &[Symbol], +) -> BuildStruct<'ctx> { + let ctx = env.context; + + // Determine types + let num_fields = sorted_fields.len(); + let mut field_types = AVec::with_capacity_in(num_fields, env.arena); + let mut field_vals = AVec::with_capacity_in(num_fields, env.arena); + + for symbol in sorted_fields.iter() { + // Zero-sized fields have no runtime representation. + // The layout of the struct expects them to be dropped! + let (field_expr, field_layout) = scope.load_symbol_and_layout(symbol); + if !layout_interner + .get_repr(field_layout) + .is_dropped_because_empty() + { + let field_type = basic_type_from_layout( + env, + layout_interner, + layout_interner.get_repr(field_layout), + ); + field_types.push(field_type); + + if layout_interner.is_passed_by_reference(field_layout) { + let field_value = env.builder.new_build_load( + field_type, + field_expr.into_pointer_value(), + "load_tag_to_put_in_struct", + ); + + field_vals.push(field_value); + } else { + field_vals.push(field_expr); + } + } + } + + // Create the struct_type + let struct_type = ctx.struct_type(field_types.into_bump_slice(), false); + let struct_val = struct_from_fields(env, struct_type, field_vals.into_iter().enumerate()); + + BuildStruct { + struct_type, + struct_val, + } +} + +pub fn struct_from_fields<'a, 'ctx, 'env, I>( + env: &Env<'a, 'ctx, 'env>, + struct_type: StructType<'ctx>, + values: I, +) -> StructValue<'ctx> +where + I: Iterator)>, +{ + let mut struct_value = struct_type.const_zero().into(); + + // Insert field exprs into struct_val + for (index, field_val) in values { + let index: u32 = index as u32; + + struct_value = env + .builder + .build_insert_value(struct_value, field_val, index, "insert_record_field") + .unwrap(); + } + + struct_value.into_struct_value() +} diff --git a/crates/compiler/gen_llvm/src/run_roc.rs b/crates/compiler/gen_llvm/src/run_roc.rs new file mode 100644 index 0000000000..1c8ffd5662 --- /dev/null +++ b/crates/compiler/gen_llvm/src/run_roc.rs @@ -0,0 +1,176 @@ +use std::mem::MaybeUninit; + +use roc_mono::ir::CrashTag; +use roc_std::RocStr; + +/// This must have the same size as the repr() of RocCallResult! +pub const ROC_CALL_RESULT_DISCRIMINANT_SIZE: usize = std::mem::size_of::(); + +#[repr(C)] +pub struct RocCallResult { + tag: u64, + error_msg: *mut RocStr, + value: MaybeUninit, +} + +impl RocCallResult { + pub fn new(value: T) -> Self { + Self { + tag: 0, + error_msg: std::ptr::null_mut(), + value: MaybeUninit::new(value), + } + } +} + +impl Default for RocCallResult { + fn default() -> Self { + Self { + tag: 0, + error_msg: std::ptr::null_mut(), + value: MaybeUninit::new(Default::default()), + } + } +} + +impl From> for Result { + fn from(call_result: RocCallResult) -> Self { + match call_result.tag { + 0 => Ok(unsafe { call_result.value.assume_init() }), + n => Err({ + let msg: &RocStr = unsafe { &*call_result.error_msg }; + let tag = (n - 1) as u32; + let tag = tag + .try_into() + .unwrap_or_else(|_| panic!("received illegal tag: {tag}")); + + (msg.as_str().to_owned(), tag) + }), + } + } +} + +#[macro_export] +macro_rules! run_roc_dylib { + ($lib:expr, $main_fn_name:expr, $argument_type:ty, $return_type:ty) => {{ + use inkwell::context::Context; + use roc_builtins::bitcode; + use roc_gen_llvm::run_roc::RocCallResult; + use std::mem::MaybeUninit; + + // NOTE: return value is not first argument currently (may/should change in the future) + type Main = unsafe extern "C" fn($argument_type, *mut RocCallResult<$return_type>); + + unsafe { + let main: libloading::Symbol
= $lib + .get($main_fn_name.as_bytes()) + .ok() + .ok_or(format!("Unable to JIT compile `{}`", $main_fn_name)) + .expect("errored"); + + main + } + }}; +} + +#[macro_export] +macro_rules! try_run_jit_function { + ($lib: expr, $main_fn_name: expr, $ty:ty, $transform:expr) => {{ + let v: String = String::new(); + try_run_jit_function!($lib, $main_fn_name, $ty, $transform, v) + }}; + + ($lib: expr, $main_fn_name: expr, $ty:ty, $transform:expr) => {{ + try_run_jit_function!($lib, $main_fn_name, $ty, $transform, &[]) + }}; + ($lib: expr, $main_fn_name: expr, $ty:ty, $transform:expr, $expect_failures:expr) => {{ + use inkwell::context::Context; + use roc_builtins::bitcode; + use roc_gen_llvm::run_roc::RocCallResult; + use std::mem::MaybeUninit; + + unsafe { + let main: libloading::Symbol)> = $lib + .get($main_fn_name.as_bytes()) + .ok() + .ok_or(format!("Unable to JIT compile `{}`", $main_fn_name)) + .expect("errored"); + + let mut main_result = MaybeUninit::uninit(); + main(main_result.as_mut_ptr()); + + main_result.assume_init().into() + } + }}; +} + +#[macro_export] +macro_rules! run_jit_function { + ($lib: expr, $main_fn_name: expr, $ty:ty, $transform:expr) => {{ + let v: String = String::new(); + run_jit_function!($lib, $main_fn_name, $ty, $transform, v) + }}; + + ($lib: expr, $main_fn_name: expr, $ty:ty, $transform:expr, $errors:expr) => {{ + run_jit_function!($lib, $main_fn_name, $ty, $transform, $errors, &[]) + }}; + ($lib: expr, $main_fn_name: expr, $ty:ty, $transform:expr, $errors:expr, $expect_failures:expr) => {{ + let result = + $crate::try_run_jit_function!($lib, $main_fn_name, $ty, $transform, $expect_failures); + + match result { + Ok(success) => { + // only if there are no exceptions thrown, check for errors + assert!($errors.is_empty(), "Encountered errors:\n{}", $errors); + + $transform(success) + } + Err((error_msg, _)) => { + eprintln!("This Roc code crashed with: \"{error_msg}\""); + + Expr::MalformedClosure + } + } + }}; +} + +/// In the repl, we don't know the type that is returned; it it's large enough to not fit in 2 +/// registers (i.e. size bigger than 16 bytes on 64-bit systems), then we use this macro. +/// It explicitly allocates a buffer that the roc main function can write its result into. +#[macro_export] +macro_rules! run_jit_function_dynamic_type { + ($lib: expr, $main_fn_name: expr, $bytes:expr, $transform:expr) => {{ + let v: String = String::new(); + run_jit_function_dynamic_type!($lib, $main_fn_name, $bytes, $transform, v) + }}; + + ($lib: expr, $main_fn_name: expr, $bytes:expr, $transform:expr, $errors:expr) => {{ + use inkwell::context::Context; + use roc_gen_llvm::run_roc::RocCallResult; + + unsafe { + let main: libloading::Symbol = $lib + .get($main_fn_name.as_bytes()) + .ok() + .ok_or(format!("Unable to JIT compile `{}`", $main_fn_name)) + .expect("errored"); + + const CALL_RESULT_WIDTH: usize = std::mem::size_of::>(); + let size = CALL_RESULT_WIDTH + $bytes; + let layout = std::alloc::Layout::from_size_align(size, 16).unwrap(); + let output = std::alloc::alloc(layout); + main(output); + + let call_result: RocCallResult<()> = unsafe { std::ptr::read_unaligned(output.cast()) }; + let result = Result::from(call_result); + + match result { + Ok(()) => $transform(output.add(CALL_RESULT_WIDTH) as usize), + Err((msg, _crash_tag)) => { + eprintln!("{}", msg); + panic!("Roc hit an error"); + } + } + } + }}; +} diff --git a/compiler/gen_wasm/.gitignore b/crates/compiler/gen_wasm/.gitignore similarity index 100% rename from compiler/gen_wasm/.gitignore rename to crates/compiler/gen_wasm/.gitignore diff --git a/crates/compiler/gen_wasm/Cargo.toml b/crates/compiler/gen_wasm/Cargo.toml new file mode 100644 index 0000000000..a25ccf0e23 --- /dev/null +++ b/crates/compiler/gen_wasm/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "roc_gen_wasm" +description = "Provides the WASM backend to generate Roc binaries." + +authors.workspace = true +edition.workspace = true +license.workspace = true +version.workspace = true + +[dependencies] +roc_builtins = { path = "../builtins" } +roc_collections = { path = "../collections" } +roc_error_macros = { path = "../../error_macros" } +roc_module = { path = "../module" } +roc_mono = { path = "../mono" } +roc_std = { path = "../../roc_std" } +roc_target = { path = "../roc_target" } +roc_wasm_module = { path = "../../wasm_module" } + +bitvec.workspace = true +bumpalo.workspace = true diff --git a/crates/compiler/gen_wasm/README.md b/crates/compiler/gen_wasm/README.md new file mode 100644 index 0000000000..5957c02cb3 --- /dev/null +++ b/crates/compiler/gen_wasm/README.md @@ -0,0 +1,225 @@ +# Development backend for WebAssembly + +## Structured control flow + +One of the security features of WebAssembly is that it does not allow unrestricted "jumps" to anywhere you like. It does not have an instruction for that. All of the [control instructions][control-inst] can only implement "structured" control flow, and have names like `if`, `loop`, `block` that you'd normally associate with high-level languages. There are branch (`br`) instructions that can jump to labelled blocks within the same function, but the blocks have to be nested in sensible ways. + +[control-inst]: https://webassembly.github.io/spec/core/syntax/instructions.html#control-instructions + +This way of representing control flow is similar to parts of the Roc AST like `When`, `If` and `LetRec`. But Mono IR converts this to jumps and join points, which are more of a Control Flow Graph than a tree. We need to map back from graph to a tree again in the Wasm backend. + +Our solution is to wrap all joinpoint/jump graphs in an outer `loop`, with nested `block`s inside it. + +## Stack machine vs register machine + +Wasm's instruction set is based on a stack-machine VM. Whereas CPU instructions have named registers that they operate on, Wasm has no named registers at all. The instructions don't contain register names. Instructions can only operate on whatever data is at the top of the stack. + +For example the instruction `i64.add` takes two operands. It pops the top two arguments off the VM stack and pushes the result back. + +In the [spec][spec-instructions], every instruction has a type signature! This is not something you would see for CPU instructions. The type signature for i64.add is `[i64 i64] → [i64]` because it pushes two i64's and pops an i64. + +[spec-instructions]: https://webassembly.github.io/spec/core/appendix/index-instructions.html + +This means that WebAssembly has a concept of type checking. When you load a .wasm file as a chunk of bytes into a Wasm runtime (like a browser or [wasmer](https://wasmer.io/)), the runtime will first _validate_ those bytes. They have some fast way of checking whether the types being pushed and popped are consistent. So if you try to do the i64.add instruction when you have floats on the stack, it will fail validation. + +Note that the instruction makes no mention of any source or destination registers, because there is no such thing. It just pops two values and pushes one. (This architecture choice helps to keep WebAssembly programs quite compact. There are no extra bytes specifying source and destination registers.) + +Implications of the stack machine for Roc: + +- There is no such thing as register allocation, since there are no registers! There is no reason to maintain hashmaps of what registers are free or not. And there is no need to do a pass over the IR to find the "last seen" occurrence of a symbol in the IR. That means we don't need the `Backend` methods `scan_ast`, `scan_ast_call`, `set_last_seen`, `last_seen_map`, `free_map`, `free_symbols`, `free_symbol`, `set_free_map`. + +- There is no random access to the stack. All instructions operate on the data at the _top_ of the stack. There is no instruction that says "get the value at index 17 in the stack". If such an instruction did exist, it wouldn't be a stack machine. And there is no way to "free up some of the slots in the stack". You have to consume the stuff at the top, then the stuff further down. However Wasm has a concept of local variables, which do allow random access. See below. + +## Local variables + +WebAssembly functions can have any number of local variables. They are declared at the beginning of the function, along with their types (just like C). WebAssembly has 4 value types: `i32`, `i64`, `f32`, `f64`. + +In this backend, each symbol in the Mono IR gets one WebAssembly local. To illustrate, let's translate a simple Roc example to WebAssembly text format. +The WebAssembly code below is completely unoptimised and uses far more locals than necessary. But that does help to illustrate the concept of locals. + +```coffee +app "test" provides [main] to "./platform" + +main = + 1 + 2 + 4 +``` + +### Direct translation of Mono IR + +The Mono IR contains two functions, `Num.add` and `main`, so we generate two corresponding WebAssembly functions. +Since it has a Symbol for every expression, the simplest thing is to create a local for each one. +The code ends up being quite bloated, with lots of `local.set` and `local.get` instructions. + +I've added comments on each line to show what is on the stack and in the locals at each point in the program. + +```text + (func (;0;) (param i64 i64) (result i64) ; declare function index 0 (Num.add) with two i64 parameters and an i64 result + local.get 0 ; load param 0 stack=[param0] + local.get 1 ; load param 1 stack=[param0, param1] + i64.add ; pop two values, add, and push result stack=[param0 + param1] + return) ; return the value at the top of the stack + + (func (;1;) (result i64) ; declare function index 1 (main) with no parameters and an i64 result + (local i64 i64 i64 i64) ; declare 4 local variables, all with type i64, one for each symbol in the Mono IR + i64.const 1 ; stack=[1] + local.set 0 ; stack=[] local0=1 + i64.const 2 ; stack=[2] local0=1 + local.set 1 ; stack=[] local0=1 local1=2 + local.get 0 ; stack=[1] local0=1 local1=2 + local.get 1 ; stack=[1,2] local0=1 local1=2 + call 0 ; stack=[3] local0=1 local1=2 + local.set 2 ; stack=[] local0=1 local1=2 local2=3 + i64.const 4 ; stack=[4] local0=1 local1=2 local2=3 + local.set 3 ; stack=[] local0=1 local1=2 local2=3 local3=4 + local.get 2 ; stack=[3] local0=1 local1=2 local2=3 local3=4 + local.get 3 ; stack=[3,4] local0=1 local1=2 local2=3 local3=4 + call 0 ; stack=[7] local0=1 local1=2 local2=3 local3=4 + return) ; return the value at the top of the stack +``` + +### Handwritten equivalent + +This code doesn't actually require any locals at all. +(It also doesn't need the `return` instructions, but that's less of a problem.) + +```text + (func (;0;) (param i64 i64) (result i64) + local.get 0 + local.get 1 + i64.add) + (func (;1;) (result i64) + i64.const 1 + i64.const 2 + call 0 + i64.const 4 + call 0) +``` + +### Reducing sets and gets + +For our example code, we don't need any locals because the WebAssembly virtual machine effectively _stores_ intermediate results in a stack. Since it's already storing those values, there is no need for us to create locals. If you compare the two versions, you'll see that the `local.set` and `local.get` instructions have simply been deleted and the other instructions are in the same order. + +But sometimes we really do need locals! We may need to use the same symbol twice, or values could end up on the stack in the wrong order and need to be swapped around by setting a local and getting it again. + +The hard part is knowing when we need a local, and when we don't. For that, the `WasmBackend` needs to have some understanding of the stack machine. + +To help with this, the `CodeBuilder` maintains a vector that represents the stack. For every instruction the backend generates, `CodeBuilder` simulates the right number of pushes and pops for that instruction, so that we always know the state of the VM stack at every point in the program. + +When the `WasmBackend` generates code for a `Let` statement, it can "label" the top of the stack with the relevant `Symbol`. Then at any later point in the program, when we need to retrieve a list of symbols in a certain order, we can check whether they already happen to be at the top of the stack in that order (as they were in our example above.) + +In practice it should be very common for values to appear on the VM stack in the right order, because in the Mono IR, statements occur in dependency order! We should only generate locals when the dependency graph is a little more complicated, and we actually need them. + +```text + ┌─────────────────┐ ┌─────────────┐ + │ │ │ │ + │ ├─────► Storage ├──────┐ + │ │ │ │ │ + │ │ └─────────────┘ │ + │ │ Manage state about │ + │ │ how/where symbol │ Delegate part of + │ WasmBackend │ values are stored │ state management + │ │ │ for values on + │ │ │ the VM stack + │ │ │ + │ │ Generate ┌────────▼──────┐ + │ │ instructions │ │ + │ ├─────────────────► CodeBuilder │ + │ │ │ │ + └─────────────────┘ └───────────────┘ +``` + +## Memory + +WebAssembly programs have a "linear memory" for storing data, which is a block of memory assigned to it by the host. You can assign a min and max size to the memory, and the WebAssembly program can request 64kB pages from the host, just like a "normal" program would request pages from the OS. Addresses start at zero and go up to whatever the current size is. Zero is a perfectly normal address like any other, and dereferencing it is not a segfault. But addresses beyond the current memory size are out of bounds and dereferencing them will cause a panic. + +The program has full read/write access to the memory and can divide it into whatever sections it wants. Most programs will want to do the traditional split of static memory, stack memory and heap memory. + +The WebAssembly module structure includes a data section that will be copied into the linear memory at a specified offset on initialisation, so you can use that for string literals etc. But the division of the rest of memory into "stack" and "heap" areas is not a first-class concept. It is up to the compiler to generate instructions to do whatever it wants with that memory. + +## Stack machine vs stack memory + +**There are two entirely different meanings of the word "stack" that are relevant to the WebAssembly backend.** It's unfortunate that the word "stack" is so overloaded. I guess it's just a useful data structure. The worst thing is that both of them tend to be referred to as just "the stack"! We need more precise terms. + +When we are talking about the instruction set, I'll use the term _machine stack_ or _VM stack_. This is the implicit data structure that WebAssembly instructions operate on. In the examples above, it's where `i64.add` gets its arguments and stores its result. I think of it as an abstraction over CPU registers, that WebAssembly uses in order to be portable and compact. + +When we are talking about how we store values in _memory_, I'll use the term _stack memory_ rather than just "the stack". It feels clunky but it's the best I can think of. + +Of course our program can use another area of memory as a heap as well. WebAssembly doesn't mind how you divide up your memory. It just gives you some memory and some instructions for loading and storing. + +## Calling conventions & stack memory + +In WebAssembly you call a function by pushing arguments to the stack and then issuing a `call` instruction, which specifies a function index. The VM knows how many values to pop off the stack by examining the _type_ of the function. In our example earlier, `Num.add` had the type `[i64 i64] → [i64]` so it expects to find two i64's on the stack and pushes one i64 back as the result. Remember, the runtime engine will validate the module before running it, and if your generated code is trying to call a function at a point in the program where the wrong value types are on the stack, it will fail validation. + +Function arguments are restricted to the four value types, `i32`, `i64`, `f32` and `f64`. If those are all we need, then there is _no need for any stack memory_, stack pointer, etc. We saw this in our example earlier. We just said `call 0`. We didn't need any instructions to create a stack frame with a return address, and there was no "jump" instruction. Essentially, WebAssembly has a first-class concept of function calls, so you don't build it up from lower-level primitives. You could think of this as an abstraction over calling conventions. + +That's all great for primitive values but what happens when we want to pass more complex data structures between functions? + +Well, remember, "stack memory" is not a special kind of memory in WebAssembly, and is separate from the VM stack. It's just an area of our memory where we implement a stack data structure. But there are some conventions that it makes sense to follow so that we can easily link to Wasm code generated from Zig or other languages. + +### Observations from compiled C code + +- `global 0` is used as the stack pointer, and its value is normally copied to a `local` as well (presumably because locals tend to be assigned to CPU registers) +- Stack memory grows downwards +- If a C function returns a struct, the compiled WebAssembly function has no return value, but instead has an extra _argument_. The argument is an `i32` pointer to space allocated in the caller's stack, that the called function can write to. +- There is no maximum number of arguments for a WebAssembly function, and arguments are not passed via _stack memory_. This makes sense because the _VM stack_ has no size limit. It's like having a CPU with an unlimited number of registers. +- Stack memory is only used for allocating local variables, not for passing arguments. And it's only used for values that cannot be stored in one of WebAssembly's primitive values (`i32`, `i64`, `f32`, `f64`). + +These observations are based on experiments compiling C to WebAssembly via the Emscripten toolchain (which is built on top of clang). It's also in line with what the WebAssembly project describes [here](https://github.com/WebAssembly/design/blob/main/Rationale.md#locals). + +## Modules vs Instances + +What's the difference between a Module and an Instance in WebAssembly? + +Well, if I compare it to running a program on Linux, it's like the difference between an ELF binary and the executable image in memory that you get when you _load_ that ELF file. The ELF file is essentially a _specification_ for how to create the executable image. In order to start executing the program, the OS has to actually allocate a stack and a heap, and load the text and data. If you run multiple copies of the same program, they will each have their own memory and their own execution state. (More detail [here](https://wiki.osdev.org/ELF#Loading_ELF_Binaries)). + +The Module is like the ELF file, and the Instance is like the executable image. + +The Module is a _specification_ for how to create an Instance of the program. The Module says how much memory the program needs, but the Instance actually _contains_ that memory. In order to run the Wasm program, the VM needs to create an instance, allocate some memory for it, and copy the data section into that memory. If you run many copies of the same Wasm program, you will have one Module but many Instances. Each instance will have its own separate area of memory, and its own execution state. + +## Modules, object files, and linking + +A WebAssembly module is equivalent to an executable file. It doesn't normally need relocations since at the WebAssembly layer, there is no Address Space Layout Randomisation. If it has relocations then it's an object file. + +The [official spec](https://webassembly.github.io/spec/core/binary/modules.html#sections) lists the sections that are part of the final module. It doesn't mention any sections for relocations or symbol names, but it does support "custom" sections. Conventions to use those for linking are documented in the WebAssembly `tool-conventions` repo [here](https://github.com/WebAssembly/tool-conventions/blob/main/Linking.md) and it mentions that LLVM is using those conventions. + +## Linking host-to-app calls + +We implement a few linking operations in the Wasm backend. The most important are host-to-app calls. + +In the host .wasm file, `roc__mainForHost_1_exposed` is defined as a Wasm Import, as if it were an external JavaScript function. But when we link the host and app, we need to make it an internal function instead. + +There are a few important facts to note about the Wasm binary format: + +- Function calls refer to the callee by its function index in the file. +- If we move a function from one index to another, all of its call sites need to be updated. So we want to minimise this to make linking fast. +- If we _remove_ a function, then all functions above it will implicitly have their indices shifted down by 1! This is not good for speed. We should try to _swap_ rather than remove. +- JavaScript imports always get the lower indices. + +With that background, here are the linking steps for a single app function that gets called by the host: + +- Remove `roc__mainForHost_1_exposed` from the imports, updating all call sites to the new index, which is somewhere in the app. +- Swap the _last_ JavaScript import into the slot where `roc__mainForHost_1_exposed` was, updating all of its call sites in the host. +- Insert an internally-defined dummy function at the index where the last JavaScript import used to be. + +The diagram below illustrates this process. + +> The diagram has a tiny number of functions just to make it easier to draw! Our mock host for integration tests has 48 imports and 648 defined functions. + +  + +![Diagram showing how host-to-app calls are linked.](./docs/host-to-app-calls.svg) + +## Tips for debugging Wasm code generation + +In general, WebAssembly runtimes often have terrible error messages. Especially command-line ones. And most especially Wasm3, which we use nonetheless because it's fast. + +- Install the WABT (WebAssembly Binary Toolkit) + - We have a debug setting to dump out the test binary. In `gen_wasm/src/lib.rs`, set `DEBUG_LOG_SETTINGS.keep_test_binary` to `true` + - Run `wasm-validate` to make sure the module is valid WebAssembly + - Use `wasm-objdump` with options `-d`, `-x`, or `-s` depending on the issue +- Browsers are **much** better for debugging Wasm than any of the command line tools. + - I highly recommend this, even if you are more comfortable with the command line than the browser! + - Browsers have by far the best error messages and debugging tools. There is nothing comparable on the command line. + - We have a web page that can run gen_wasm unit tests: + crates/compiler/test_gen/src/helpers/debug-wasm-test.html + - The page itself contains instructions explaining how to open the browser debug tools. No web dev background should be required. If there's something useful missing, let Brian Carroll know or add him as a reviewer on a PR. diff --git a/compiler/gen_wasm/docs/host-to-app-calls.svg b/crates/compiler/gen_wasm/docs/host-to-app-calls.svg similarity index 100% rename from compiler/gen_wasm/docs/host-to-app-calls.svg rename to crates/compiler/gen_wasm/docs/host-to-app-calls.svg diff --git a/crates/compiler/gen_wasm/src/backend.rs b/crates/compiler/gen_wasm/src/backend.rs new file mode 100644 index 0000000000..329fb106b9 --- /dev/null +++ b/crates/compiler/gen_wasm/src/backend.rs @@ -0,0 +1,2135 @@ +use bitvec::vec::BitVec; +use bumpalo::collections::{String, Vec}; + +use roc_builtins::bitcode::{self, FloatWidth, IntWidth}; +use roc_collections::all::MutMap; +use roc_error_macros::{internal_error, todo_lambda_erasure}; +use roc_module::low_level::{LowLevel, LowLevelWrapperType}; +use roc_module::symbol::{Interns, Symbol}; +use roc_mono::code_gen_help::{CodeGenHelp, HelperOp, REFCOUNT_MAX}; +use roc_mono::ir::{ + BranchInfo, CallType, CrashTag, Expr, JoinPointId, ListLiteralElement, Literal, ModifyRc, + Param, Proc, ProcLayout, Stmt, +}; +use roc_mono::layout::{ + Builtin, InLayout, Layout, LayoutIds, LayoutInterner, LayoutRepr, STLayoutInterner, + TagIdIntType, UnionLayout, +}; +use roc_std::RocDec; + +use roc_wasm_module::linking::{DataSymbol, WasmObjectSymbol}; +use roc_wasm_module::sections::{ + ConstExpr, DataMode, DataSegment, Export, Global, GlobalType, Import, ImportDesc, Limits, + MemorySection, NameSection, +}; +use roc_wasm_module::{ + round_up_to_alignment, Align, ExportType, LocalId, Signature, SymInfo, ValueType, WasmModule, +}; + +use crate::code_builder::CodeBuilder; +use crate::layout::{ReturnMethod, WasmLayout}; +use crate::low_level::{call_higher_order_lowlevel, LowLevelCall}; +use crate::storage::{AddressValue, Storage, StoredValue, StoredVarKind}; +use crate::{ + copy_memory, CopyMemoryConfig, Env, DEBUG_SETTINGS, MEMORY_NAME, PTR_SIZE, PTR_TYPE, + TARGET_INFO, +}; + +#[derive(Clone, Copy, Debug)] +pub enum ProcSource { + Roc, + Helper, + /// Wrapper function for higher-order calls from Zig to Roc + HigherOrderMapper(usize), + HigherOrderCompare(usize), +} + +#[derive(Debug)] +pub struct ProcLookupData<'a> { + pub name: Symbol, + pub layout: ProcLayout<'a>, + pub source: ProcSource, +} + +pub struct WasmBackend<'a, 'r> { + pub env: &'r Env<'a>, + pub(crate) layout_interner: &'r mut STLayoutInterner<'a>, + interns: &'r mut Interns, + + // Module-level data + module: WasmModule<'a>, + layout_ids: LayoutIds<'a>, + pub fn_index_offset: u32, + import_fn_count: u32, + called_fns: BitVec, + pub proc_lookup: Vec<'a, ProcLookupData<'a>>, + host_lookup: Vec<'a, (&'a str, u32)>, + helper_proc_gen: CodeGenHelp<'a>, + can_relocate_heap: bool, + + // Function-level data + pub code_builder: CodeBuilder<'a>, + pub storage: Storage<'a>, + + /// how many blocks deep are we (used for jumps) + block_depth: u32, + joinpoint_label_map: MutMap)>, +} + +impl<'a, 'r> WasmBackend<'a, 'r> { + #[allow(clippy::too_many_arguments)] + pub fn new( + env: &'r Env<'a>, + layout_interner: &'r mut STLayoutInterner<'a>, + interns: &'r mut Interns, + layout_ids: LayoutIds<'a>, + proc_lookup: Vec<'a, ProcLookupData<'a>>, + host_to_app_map: Vec<'a, (&'a str, u32)>, + mut module: WasmModule<'a>, + fn_index_offset: u32, + helper_proc_gen: CodeGenHelp<'a>, + ) -> Self { + let has_heap_base = module.linking.find_internal_symbol("__heap_base").is_ok(); + let has_heap_end = module.linking.find_internal_symbol("__heap_end").is_ok(); + + // We don't want to import any Memory or Tables + module.import.imports.retain(|import| { + !matches!( + import.description, + ImportDesc::Mem { .. } | ImportDesc::Table { .. } + ) + }); + + // Relocate calls from host to app + // This will change function indices in the host, so we need to do it before get_host_function_lookup + module.link_host_to_app_calls(env.arena, host_to_app_map); + + let host_lookup = module.get_host_function_lookup(env.arena); + + if module.names.function_names.is_empty() { + module.names = NameSection::from_imports_and_linking_data( + env.arena, + &module.import, + &module.linking, + ) + } + + let import_fn_count = module.import.function_count(); + let host_function_count = import_fn_count + + module.code.dead_import_dummy_count as usize + + module.code.function_count as usize; + let mut called_fns = BitVec::repeat(false, host_function_count); + called_fns.extend(std::iter::repeat(true).take(proc_lookup.len())); + + WasmBackend { + env, + layout_interner, + interns, + + // Module-level data + module, + layout_ids, + fn_index_offset, + import_fn_count: import_fn_count as u32, + called_fns, + proc_lookup, + host_lookup, + helper_proc_gen, + can_relocate_heap: has_heap_base && has_heap_end, + + // Function-level data + block_depth: 0, + joinpoint_label_map: MutMap::default(), + code_builder: CodeBuilder::new(env.arena), + storage: Storage::new(env.arena), + } + } + + /// A Wasm module's memory is all in one contiguous block, unlike native executables. + /// The standard layout is: constant data, then stack, then heap. + /// Since they're all in one block, they can't grow independently. Only the highest one can grow. + /// Also, there's no "invalid region" below the stack, so stack overflow will overwrite constants! + /// TODO: Detect stack overflow in function prologue... at least in Roc code... + fn set_memory_layout(&mut self, stack_size: u32) { + let mut stack_heap_boundary = self.module.data.end_addr + stack_size; + stack_heap_boundary = round_up_to_alignment!(stack_heap_boundary, MemorySection::PAGE_SIZE); + + // Stack pointer + // This should be an imported global in the host + // In the final binary, it's an internally defined global + let sp_type = GlobalType { + value_type: ValueType::I32, + is_mutable: true, + }; + { + // Check that __stack_pointer is the only imported global + // If there were more, we'd have to relocate them, and we don't + let imported_globals = Vec::from_iter_in( + self.module + .import + .imports + .iter() + .filter(|import| matches!(import.description, ImportDesc::Global { .. })), + self.env.arena, + ); + if imported_globals.len() != 1 + || imported_globals[0] + != &(Import { + module: "env", + name: "__stack_pointer", + description: ImportDesc::Global { ty: sp_type }, + }) + { + panic!("I can't link this host file. I expected it to have one imported Global called env.__stack_pointer") + } + } + self.module + .import + .imports + .retain(|import| !matches!(import.description, ImportDesc::Global { .. })); + + self.module.global.append(Global { + ty: sp_type, + init: ConstExpr::I32(stack_heap_boundary as i32), + }); + + // Set the initial size of the memory + self.module.memory = MemorySection::new( + self.env.arena, + stack_heap_boundary + MemorySection::PAGE_SIZE, + ); + + // Export the memory so that JS can interact with it + self.module.export.append(Export { + name: MEMORY_NAME, + ty: ExportType::Mem, + index: 0, + }); + + // Set the constant that malloc uses to know where the heap begins + // this should be done after we know how much constant data we have (e.g. string literals) + if self.can_relocate_heap { + self.module + .relocate_internal_symbol("__heap_base", stack_heap_boundary) + .unwrap(); + self.module + .relocate_internal_symbol( + "__heap_end", + stack_heap_boundary + MemorySection::PAGE_SIZE, + ) + .unwrap(); + } + } + + /// If the host has some `extern` global variables, we need to create them in the final binary + /// and make them visible to JavaScript by exporting them + fn export_globals(&mut self) { + for (sym_index, sym) in self.module.linking.symbol_table.iter().enumerate() { + match sym { + SymInfo::Data(DataSymbol::Imported { name, .. }) + if *name != "__heap_base" && *name != "__heap_end" => + { + let global_value_addr = self.module.data.end_addr; + self.module.data.end_addr += PTR_SIZE; + + self.module.reloc_code.apply_relocs_u32( + &mut self.module.code.bytes, + sym_index as u32, + global_value_addr, + ); + + let global_index = self.module.global.count; + self.module.global.append(Global { + ty: GlobalType { + value_type: ValueType::I32, + is_mutable: false, + }, + init: ConstExpr::I32(global_value_addr as i32), + }); + + self.module.export.append(Export { + name, + ty: ExportType::Global, + index: global_index, + }); + } + _ => {} + } + } + } + + pub fn get_helpers(&mut self) -> Vec<'a, Proc<'a>> { + self.helper_proc_gen.take_procs() + } + + pub fn register_helper_proc( + &mut self, + symbol: Symbol, + layout: ProcLayout<'a>, + source: ProcSource, + ) -> u32 { + let proc_index = self.proc_lookup.len(); + let wasm_fn_index = self.fn_index_offset + proc_index as u32; + + let name = self + .layout_ids + .get_toplevel(symbol, &layout) + .to_symbol_string(symbol, self.interns); + let name = String::from_str_in(&name, self.env.arena).into_bump_str(); + + self.proc_lookup.push(ProcLookupData { + name: symbol, + layout, + source, + }); + + self.called_fns.push(true); + + let linker_symbol = SymInfo::Function(WasmObjectSymbol::ExplicitlyNamed { + flags: 0, + index: wasm_fn_index, + name, + }); + self.module.linking.symbol_table.push(linker_symbol); + + wasm_fn_index + } + + pub fn finalize(mut self) -> (WasmModule<'a>, BitVec) { + self.set_memory_layout(self.env.stack_bytes); + self.export_globals(); + + self.maybe_call_host_main(); + let fn_table_size = 1 + self.module.element.max_table_index(); + self.module.table.function_table.limits = Limits::MinMax(fn_table_size, fn_table_size); + (self.module, self.called_fns) + } + + /// If the host has a `main` function then we need to insert a `_start` to call it. + /// This is something linkers do, and this backend is also a linker! + fn maybe_call_host_main(&mut self) { + const START: &str = "_start"; + + // If _start exists, just export it. Trust it to call main. + if let Ok(start_sym_index) = self.module.linking.find_internal_symbol(START) { + let start_fn_index = match self.module.linking.symbol_table[start_sym_index] { + SymInfo::Function(WasmObjectSymbol::ExplicitlyNamed { index, .. }) => index, + _ => panic!("linker symbol `{START}` is not a function"), + }; + self.module.export.append(Export { + name: START, + ty: ExportType::Func, + index: start_fn_index, + }); + return; + } + + // _start doesn't exist. Check for a `main` and create a _start that calls it. + // Note: if `main` is prefixed with some other module name, we won't find it! + let main_symbol_index = match self.module.linking.find_internal_symbol("main") { + Ok(x) => x, + Err(_) => return, + }; + + let main_fn_index: u32 = match &self.module.linking.symbol_table[main_symbol_index] { + SymInfo::Function(WasmObjectSymbol::ExplicitlyNamed { index, .. }) => *index, + _ => { + return; + } + }; + + self.module.add_function_signature(Signature { + param_types: bumpalo::vec![in self.env.arena], + ret_type: None, + }); + + self.module.export.append(Export { + name: START, + ty: ExportType::Func, + index: self.module.code.function_count, + }); + + self.code_builder.i32_const(0); // argc=0 + self.code_builder.i32_const(0); // argv=NULL + self.code_builder.call(main_fn_index); + self.code_builder.drop_(); + self.code_builder.build_fn_header_and_footer(&[], 0, None); + self.reset(); + + self.called_fns.set(main_fn_index as usize, true); + } + + /// Register the debug names of Symbols in a global lookup table + /// so that they have meaningful names when you print them. + /// Particularly useful after generating IR for refcount procedures + #[cfg(debug_assertions)] + pub fn register_symbol_debug_names(&self) { + let module_id = self.env.module_id; + let ident_ids = self.interns.all_ident_ids.get(&module_id).unwrap(); + self.env.module_id.register_debug_idents(ident_ids); + } + + #[cfg(not(debug_assertions))] + pub fn register_symbol_debug_names(&self) {} + + pub fn get_fn_ptr(&mut self, fn_index: u32) -> i32 { + self.module.element.get_or_insert_fn(fn_index) + } + + /// Create an IR Symbol for an anonymous value (such as ListLiteral) + pub fn create_symbol(&mut self, debug_name: &str) -> Symbol { + let ident_ids = self + .interns + .all_ident_ids + .get_mut(&self.env.module_id) + .unwrap(); + + let ident_id = ident_ids.add_str(debug_name); + Symbol::new(self.env.module_id, ident_id) + } + + /// Reset function-level data + fn reset(&mut self) { + self.code_builder.insert_into_module(&mut self.module); + self.code_builder.clear(); + self.storage.clear(); + self.joinpoint_label_map.clear(); + assert_eq!(self.block_depth, 0); + } + + /********************************************************** + + PROCEDURE + + ***********************************************************/ + + pub fn build_proc(&mut self, proc: &Proc<'a>) { + if DEBUG_SETTINGS.proc_start_end { + println!("\ngenerating procedure {:?}\n", proc.name); + } + + self.append_proc_debug_name(proc.name.name()); + + self.start_proc(proc); + + self.stmt(&proc.body); + + self.finalize_proc(); + self.reset(); + + if DEBUG_SETTINGS.proc_start_end { + println!("\nfinished generating {:?}\n", proc.name); + } + } + + fn start_proc(&mut self, proc: &Proc<'a>) { + use ReturnMethod::*; + let ret_layout = WasmLayout::new(self.layout_interner, proc.ret_layout); + + let ret_type = match ret_layout.return_method() { + Primitive(ty, _) => Some(ty), + NoReturnValue => None, + WriteToPointerArg => { + self.storage.arg_types.push(PTR_TYPE); + None + } + }; + + // Create a block so we can exit the function without skipping stack frame "pop" code. + // We never use the `return` instruction. Instead, we break from this block. + self.start_block(); + + self.storage.allocate_args( + self.layout_interner, + proc.args, + &mut self.code_builder, + self.env.arena, + ); + + if let Some(ty) = ret_type { + let ret_var = self.storage.create_anonymous_local(ty); + self.storage.return_var = Some(ret_var); + } + + self.module.add_function_signature(Signature { + param_types: self.storage.arg_types.clone(), + ret_type, + }); + } + + fn finalize_proc(&mut self) { + // end the block from start_proc, to ensure all paths pop stack memory (if any) + self.end_block(); + + if let Some(ret_var) = self.storage.return_var { + self.code_builder.get_local(ret_var); + } + + // Write local declarations and stack frame push/pop code + self.code_builder.build_fn_header_and_footer( + &self.storage.local_types, + self.storage.stack_frame_size, + self.storage.stack_frame_pointer, + ); + + if DEBUG_SETTINGS.storage_map { + println!("\nStorage:"); + for (sym, storage) in self.storage.symbol_storage_map.iter() { + println!("{sym:?} => {storage:?}"); + } + } + } + + fn append_proc_debug_name(&mut self, sym: Symbol) { + let proc_index = self + .proc_lookup + .iter() + .position(|ProcLookupData { name, .. }| *name == sym) + .unwrap(); + let wasm_fn_index = self.fn_index_offset + proc_index as u32; + + let name = String::from_str_in(sym.as_str(self.interns), self.env.arena).into_bump_str(); + self.module.names.append_function(wasm_fn_index, name); + } + + /// Build a wrapper around a Roc procedure so that it can be called from Zig builtins List.map* + /// + /// The generic Zig code passes *pointers* to all of the argument values (e.g. on the heap in a List). + /// Numbers up to 64 bits are passed by value, so we need to load them from the provided pointer. + /// Everything else is passed by reference, so we can just pass the pointer through. + /// + /// NOTE: If the builtins expected the return pointer first and closure data last, we could eliminate the wrapper + /// when all args are pass-by-reference and non-zero size. But currently we need it to swap those around. + pub fn build_higher_order_mapper( + &mut self, + wrapper_lookup_idx: usize, + inner_lookup_idx: usize, + ) { + use Align::*; + use ValueType::*; + + let ProcLookupData { + name: wrapper_name, + layout: wrapper_proc_layout, + .. + } = self.proc_lookup[wrapper_lookup_idx]; + let wrapper_arg_layouts = wrapper_proc_layout.arguments; + + // Our convention is that the last arg of the wrapper is the heap return pointer + let heap_return_ptr_id = LocalId(wrapper_arg_layouts.len() as u32 - 1); + let inner_ret_layout = match wrapper_arg_layouts + .last() + .map(|l| self.layout_interner.get_repr(*l)) + { + Some(LayoutRepr::Ptr(inner)) => WasmLayout::new(self.layout_interner, inner), + x => internal_error!("Higher-order wrapper: invalid return layout {:?}", x), + }; + + let ret_type_and_size = match inner_ret_layout.return_method() { + ReturnMethod::NoReturnValue => None, + ReturnMethod::Primitive(ty, size) => { + // If the inner function returns a primitive, load the address to store it at + // After the call, it will be under the call result in the value stack + self.code_builder.get_local(heap_return_ptr_id); + Some((ty, size)) + } + ReturnMethod::WriteToPointerArg => { + // If the inner function writes to a return pointer, load its address + self.code_builder.get_local(heap_return_ptr_id); + None + } + }; + + // Load all the arguments for the inner function + for (i, wrapper_arg) in wrapper_arg_layouts.iter().enumerate() { + let is_closure_data = i == 0; // Skip closure data (first for wrapper, last for inner). We'll handle it below. + let is_return_pointer = i == wrapper_arg_layouts.len() - 1; // Skip return pointer (may not be an arg for inner. And if it is, swaps from end to start) + if is_closure_data || is_return_pointer { + continue; + } + + let inner_layout = match self.layout_interner.get_repr(*wrapper_arg) { + LayoutRepr::Ptr(inner) => inner, + x => internal_error!("Expected a Ptr layout, got {:?}", x), + }; + if self.layout_interner.stack_size(inner_layout) == 0 { + continue; + } + + // Load the argument pointer. If it's a primitive value, dereference it too. + self.code_builder.get_local(LocalId(i as u32)); + self.dereference_boxed_value(inner_layout); + } + + // If the inner function has closure data, it's the last arg of the inner fn + let closure_data_layout = wrapper_arg_layouts[0]; + if self.layout_interner.stack_size(closure_data_layout) > 0 { + // The closure data exists, and will have been passed in to the wrapper as a + // one-element struct. + let inner_closure_data_layout = match self.layout_interner.get_repr(closure_data_layout) + { + LayoutRepr::Struct([inner]) => inner, + other => internal_error!( + "Expected a boxed layout for wrapped closure data, got {:?}", + other + ), + }; + self.code_builder.get_local(LocalId(0)); + // Since the closure data is wrapped in a one-element struct, we've been passed in the + // pointer to that struct in the stack memory. To get the closure data we just need to + // dereference the pointer. + self.dereference_boxed_value(*inner_closure_data_layout); + } + + // Call the wrapped inner function + let inner_wasm_fn_index = self.fn_index_offset + inner_lookup_idx as u32; + self.code_builder.call(inner_wasm_fn_index); + + // If the inner function returns a primitive, store it to the address we loaded at the very beginning + if let Some((ty, size)) = ret_type_and_size { + match (ty, size) { + (I64, 8) => self.code_builder.i64_store(Bytes8, 0), + (I32, 4) => self.code_builder.i32_store(Bytes4, 0), + (I32, 2) => self.code_builder.i32_store16(Bytes2, 0), + (I32, 1) => self.code_builder.i32_store8(Bytes1, 0), + (F32, 4) => self.code_builder.f32_store(Bytes4, 0), + (F64, 8) => self.code_builder.f64_store(Bytes8, 0), + _ => { + internal_error!("Cannot store {:?} with alignment of {:?}", ty, size); + } + } + } + + // Write empty function header (local variables array with zero length) + self.code_builder.build_fn_header_and_footer(&[], 0, None); + + self.module.add_function_signature(Signature { + param_types: bumpalo::vec![in self.env.arena; I32; wrapper_arg_layouts.len()], + ret_type: None, + }); + + self.append_proc_debug_name(wrapper_name); + self.reset(); + } + + /// Build a wrapper around a Roc comparison proc so that it can be called from higher-order Zig builtins. + /// Comparison procedure signature is: closure_data, a, b -> Order (u8) + /// + /// The generic Zig code passes *pointers* to all of the argument values (e.g. on the heap in a List). + /// Numbers up to 64 bits are passed by value, so we need to load them from the provided pointer. + /// Everything else is passed by reference, so we can just pass the pointer through. + pub fn build_higher_order_compare( + &mut self, + wrapper_lookup_idx: usize, + inner_lookup_idx: usize, + ) { + use ValueType::*; + + let ProcLookupData { + name: wrapper_name, + layout: wrapper_proc_layout, + .. + } = self.proc_lookup[wrapper_lookup_idx]; + let closure_data_layout = wrapper_proc_layout.arguments[0]; + let value_layout = wrapper_proc_layout.arguments[1]; + + if self.layout_interner.stack_size(closure_data_layout) > 0 { + self.code_builder.get_local(LocalId(0)); + } + + let inner_layout = match self.layout_interner.get_repr(value_layout) { + LayoutRepr::Ptr(inner) => inner, + x => internal_error!("Expected a Ptr layout, got {:?}", x), + }; + self.code_builder.get_local(LocalId(1)); + self.dereference_boxed_value(inner_layout); + self.code_builder.get_local(LocalId(2)); + self.dereference_boxed_value(inner_layout); + + // Call the wrapped inner function + let inner_wasm_fn_index = self.fn_index_offset + inner_lookup_idx as u32; + self.code_builder.call(inner_wasm_fn_index); + + // Write empty function header (local variables array with zero length) + self.code_builder.build_fn_header_and_footer(&[], 0, None); + + self.module.add_function_signature(Signature { + param_types: bumpalo::vec![in self.env.arena; I32; 3], + ret_type: Some(ValueType::I32), + }); + + self.append_proc_debug_name(wrapper_name); + self.reset(); + } + + fn dereference_boxed_value(&mut self, inner: InLayout) { + use Align::*; + + match self.layout_interner.get_repr(inner) { + LayoutRepr::Builtin(Builtin::Int(IntWidth::U8 | IntWidth::I8)) => { + self.code_builder.i32_load8_u(Bytes1, 0); + } + LayoutRepr::Builtin(Builtin::Int(IntWidth::U16 | IntWidth::I16)) => { + self.code_builder.i32_load16_u(Bytes2, 0); + } + LayoutRepr::Builtin(Builtin::Int(IntWidth::U32 | IntWidth::I32)) => { + self.code_builder.i32_load(Bytes4, 0); + } + LayoutRepr::Builtin(Builtin::Int(IntWidth::U64 | IntWidth::I64)) => { + self.code_builder.i64_load(Bytes8, 0); + } + LayoutRepr::Builtin(Builtin::Float(FloatWidth::F32)) => { + self.code_builder.f32_load(Bytes4, 0); + } + LayoutRepr::Builtin(Builtin::Float(FloatWidth::F64)) => { + self.code_builder.f64_load(Bytes8, 0); + } + LayoutRepr::Builtin(Builtin::Bool) => { + self.code_builder.i32_load8_u(Bytes1, 0); + } + _ => { + // Any other layout is a pointer, which we've already loaded. Nothing to do! + } + } + } + + /********************************************************** + + STATEMENTS + + ***********************************************************/ + + fn stmt(&mut self, stmt: &Stmt<'a>) { + match stmt { + Stmt::Let(_, _, _, _) => self.stmt_let(stmt), + + Stmt::Ret(sym) => self.stmt_ret(*sym), + + Stmt::Switch { + cond_symbol, + cond_layout, + branches, + default_branch, + ret_layout: _, + } => self.stmt_switch(*cond_symbol, *cond_layout, branches, default_branch), + + Stmt::Join { + id, + parameters, + body, + remainder, + } => self.stmt_join(*id, parameters, body, remainder), + + Stmt::Jump(id, arguments) => self.stmt_jump(*id, arguments), + + Stmt::Refcounting(modify, following) => match modify { + ModifyRc::Free(symbol) => self.stmt_refcounting_free(*symbol, following), + _ => self.stmt_refcounting(modify, following), + }, + + Stmt::Dbg { .. } => todo!("dbg is not implemented in the wasm backend"), + Stmt::Expect { .. } => todo!("expect is not implemented in the wasm backend"), + Stmt::ExpectFx { .. } => todo!("expect-fx is not implemented in the wasm backend"), + + Stmt::Crash(sym, tag) => self.stmt_crash(*sym, *tag), + } + } + + fn start_block(&mut self) { + // Wasm blocks can have result types, but we don't use them. + // You need the right type on the stack when you jump from an inner block to an outer one. + // The rules are confusing, and implementing them would add complexity and slow down code gen. + // Instead we use local variables to move a value from an inner block to an outer one. + self.block_depth += 1; + self.code_builder.block(); + } + + fn start_loop(&mut self) { + self.block_depth += 1; + self.code_builder.loop_(); + } + + fn end_block(&mut self) { + self.block_depth -= 1; + self.code_builder.end(); + } + + fn stmt_let(&mut self, stmt: &Stmt<'a>) { + let mut current_stmt = stmt; + while let Stmt::Let(sym, expr, layout, following) = current_stmt { + if DEBUG_SETTINGS.let_stmt_ir { + print!("\nlet {:?} = {}", sym, expr.to_pretty(200, true)); + } + + let kind = match following { + Stmt::Ret(ret_sym) if *sym == *ret_sym => StoredVarKind::ReturnValue, + _ => StoredVarKind::Variable, + }; + + self.stmt_let_store_expr(*sym, *layout, expr, kind); + + current_stmt = *following; + } + + self.stmt(current_stmt); + } + + fn stmt_let_store_expr( + &mut self, + sym: Symbol, + layout: InLayout<'a>, + expr: &Expr<'a>, + kind: StoredVarKind, + ) { + let sym_storage = self + .storage + .allocate_var(self.layout_interner, layout, sym, kind); + + self.expr(sym, expr, layout, &sym_storage); + + if let StoredValue::Local { local_id, .. } = sym_storage { + if !self.code_builder.is_set(local_id) { + self.code_builder.set_local(local_id); + } + } + } + + fn stmt_ret(&mut self, sym: Symbol) { + use crate::storage::StoredValue::*; + + match self.storage.get(&sym) { + StackMemory { + location, + size, + alignment_bytes, + .. + } => { + let (from_ptr, from_offset) = + location.local_and_offset(self.storage.stack_frame_pointer); + copy_memory( + &mut self.code_builder, + CopyMemoryConfig { + from_ptr, + from_offset, + to_ptr: LocalId(0), + to_offset: 0, + size: *size, + alignment_bytes: *alignment_bytes, + }, + ); + } + + _ => { + self.storage.load_symbols(&mut self.code_builder, &[sym]); + + // If we have a return value, store it to the return variable + // This avoids complications with block result types when returning from nested blocks + if let Some(ret_var) = self.storage.return_var { + self.code_builder.set_local(ret_var); + } + } + } + // jump to the "stack frame pop" code at the end of the function + self.code_builder.br(self.block_depth - 1); + } + + fn stmt_switch( + &mut self, + cond_symbol: Symbol, + cond_layout: InLayout<'a>, + branches: &'a [(u64, BranchInfo<'a>, Stmt<'a>)], + default_branch: &(BranchInfo<'a>, &'a Stmt<'a>), + ) { + // NOTE currently implemented as a series of conditional jumps + // We may be able to improve this in the future with `Select` + // or `BrTable` + + // create a block for each branch except the default + for _ in 0..branches.len() { + self.start_block() + } + + let is_bool = matches!(cond_layout, Layout::BOOL); + let cond_type = WasmLayout::new(self.layout_interner, cond_layout).arg_types()[0]; + + // then, we jump whenever the value under scrutiny is equal to the value of a branch + for (i, (value, _, _)) in branches.iter().enumerate() { + // put the cond_symbol on the top of the stack + self.storage + .load_symbols(&mut self.code_builder, &[cond_symbol]); + + if is_bool { + // We already have a bool, don't need to compare against a const to get one + if *value == 0 { + self.code_builder.i32_eqz(); + } + } else { + match cond_type { + ValueType::I32 => { + self.code_builder.i32_const(*value as i32); + self.code_builder.i32_eq(); + } + ValueType::I64 => { + self.code_builder.i64_const(*value as i64); + self.code_builder.i64_eq(); + } + ValueType::F32 => { + self.code_builder.f32_const(f32::from_bits(*value as u32)); + self.code_builder.f32_eq(); + } + ValueType::F64 => { + self.code_builder.f64_const(f64::from_bits(*value)); + self.code_builder.f64_eq(); + } + } + } + + // "break" out of `i` surrounding blocks + self.code_builder.br_if(i as u32); + } + + // if we never jumped because a value matched, we're in the default case + self.stmt(default_branch.1); + + // now put in the actual body of each branch in order + // (the first branch would have broken out of 1 block, + // hence we must generate its code first) + for (_, _, branch) in branches.iter() { + self.end_block(); + + self.stmt(branch); + } + } + + fn stmt_join( + &mut self, + id: JoinPointId, + parameters: &'a [Param<'a>], + body: &'a Stmt<'a>, + remainder: &'a Stmt<'a>, + ) { + // make locals for join pointer parameters + let mut jp_param_storages = Vec::with_capacity_in(parameters.len(), self.env.arena); + for parameter in parameters.iter() { + let param_storage = self.storage.allocate_var( + self.layout_interner, + parameter.layout, + parameter.symbol, + StoredVarKind::Variable, + ); + jp_param_storages.push(param_storage); + } + + self.start_block(); + + self.joinpoint_label_map + .insert(id, (self.block_depth, jp_param_storages)); + + self.stmt(remainder); + + self.end_block(); + self.start_loop(); + + self.stmt(body); + + // ends the loop + self.end_block(); + } + + fn stmt_jump(&mut self, id: JoinPointId, arguments: &'a [Symbol]) { + let (target, param_storages) = self.joinpoint_label_map[&id].clone(); + + for (arg_symbol, param_storage) in arguments.iter().zip(param_storages.iter()) { + let arg_storage = self.storage.get(arg_symbol).clone(); + self.storage + .clone_value(&mut self.code_builder, param_storage, &arg_storage); + } + + // jump + let levels = self.block_depth - target; + self.code_builder.br(levels); + } + + fn stmt_refcounting(&mut self, modify: &ModifyRc, following: &'a Stmt<'a>) { + let value = modify.get_symbol(); + let layout = self.storage.symbol_layouts[&value]; + + let ident_ids = self + .interns + .all_ident_ids + .get_mut(&self.env.module_id) + .unwrap(); + + let (rc_stmt, new_specializations) = self.helper_proc_gen.expand_refcount_stmt( + ident_ids, + self.layout_interner, + layout, + modify, + following, + ); + + if false { + self.register_symbol_debug_names(); + println!( + "## rc_stmt:\n{}\n{:?}", + rc_stmt.to_pretty(self.layout_interner, 200, true), + rc_stmt + ); + } + + // If any new specializations were created, register their symbol data + for (spec_sym, spec_layout) in new_specializations.into_iter() { + self.register_helper_proc(spec_sym, spec_layout, ProcSource::Helper); + } + + self.stmt(rc_stmt); + } + + fn stmt_refcounting_free(&mut self, value: Symbol, following: &'a Stmt<'a>) { + let layout = self.storage.symbol_layouts[&value]; + let alignment = self.layout_interner.allocation_alignment_bytes(layout); + + // Get pointer and offset + let value_storage = self.storage.get(&value).to_owned(); + let (tag_local_id, tag_offset) = match value_storage { + StoredValue::StackMemory { location, .. } => { + location.local_and_offset(self.storage.stack_frame_pointer) + } + StoredValue::Local { local_id, .. } => (local_id, 0), + }; + + // load pointer, and add the offset to the pointer + self.code_builder.get_local(tag_local_id); + + if tag_offset > 0 { + self.code_builder.i32_const(tag_offset as i32); + self.code_builder.i32_add(); + } + + // NOTE: UTILS_FREE_DATA_PTR clears any tag id bits + + // push the allocation's alignment + self.code_builder.i32_const(alignment as i32); + + self.call_host_fn_after_loading_args(bitcode::UTILS_FREE_DATA_PTR); + + self.stmt(following); + } + + pub fn stmt_internal_error(&mut self, msg: &'a str) { + let msg_sym = self.create_symbol("panic_str"); + let msg_storage = self.storage.allocate_var( + self.layout_interner, + Layout::STR, + msg_sym, + StoredVarKind::Variable, + ); + + // Store the message as a RocStr on the stack + let (local_id, offset) = match msg_storage { + StoredValue::StackMemory { location, .. } => { + location.local_and_offset(self.storage.stack_frame_pointer) + } + _ => internal_error!("String must always have stack memory"), + }; + self.expr_string_literal(msg, local_id, offset); + + self.stmt_crash(msg_sym, CrashTag::Roc); + } + + pub fn stmt_crash(&mut self, msg: Symbol, tag: CrashTag) { + // load the pointer + self.storage.load_symbols(&mut self.code_builder, &[msg]); + self.code_builder.i32_const(tag as _); + self.call_host_fn_after_loading_args("roc_panic"); + + self.code_builder.unreachable_(); + } + + /********************************************************** + + EXPRESSIONS + + ***********************************************************/ + + fn expr(&mut self, sym: Symbol, expr: &Expr<'a>, layout: InLayout<'a>, storage: &StoredValue) { + match expr { + Expr::Literal(lit) => self.expr_literal(lit, storage), + + Expr::NullPointer => self.expr_null_pointer(), + + Expr::Call(roc_mono::ir::Call { + call_type, + arguments, + }) => self.expr_call(call_type, arguments, sym, layout, storage), + + Expr::Struct(fields) => self.expr_struct(sym, layout, storage, fields), + + Expr::StructAtIndex { + index, + field_layouts, + structure, + } => self.expr_struct_at_index(sym, *index, field_layouts, *structure), + + Expr::Array { elems, elem_layout } => { + self.expr_array(sym, storage, *elem_layout, elems) + } + + Expr::EmptyArray => self.expr_empty_array(sym, storage), + + Expr::Tag { + tag_layout: union_layout, + tag_id, + arguments, + reuse, + } => { + let reuse = reuse.map(|ru| ru.symbol); + self.expr_tag(union_layout, *tag_id, arguments, storage, reuse) + } + + Expr::GetTagId { + structure, + union_layout, + } => self.expr_get_tag_id(*structure, union_layout, storage), + + Expr::UnionAtIndex { + structure, + tag_id, + union_layout, + index, + } => self.expr_union_at_index(*structure, *tag_id, union_layout, *index, sym), + + Expr::GetElementPointer { + structure, + union_layout, + indices, + .. + } => { + debug_assert!(indices.len() >= 2); + self.expr_union_field_ptr_at_index( + *structure, + indices[0] as u16, + union_layout, + indices[1], + storage, + ) + } + + Expr::FunctionPointer { .. } => todo_lambda_erasure!(), + Expr::ErasedMake { .. } => todo_lambda_erasure!(), + Expr::ErasedLoad { .. } => todo_lambda_erasure!(), + + Expr::Reset { symbol: arg, .. } => self.expr_reset(*arg, sym, storage), + + Expr::ResetRef { symbol: arg, .. } => self.expr_resetref(*arg, sym, storage), + + Expr::Alloca { + initializer, + element_layout, + } => self.expr_alloca(*initializer, *element_layout, storage), + + Expr::RuntimeErrorFunction(_) => { + todo!("Expression `{}`", expr.to_pretty(100, false)) + } + } + } + + /******************************************************************* + * Literals + *******************************************************************/ + + fn expr_literal(&mut self, lit: &Literal<'a>, storage: &StoredValue) { + let invalid_error = || { + internal_error!( + "Literal value {:?} implements invalid storage {:?}", + lit, + storage + ) + }; + + match storage { + StoredValue::Local { + value_type, + local_id, + .. + } => { + match (lit, value_type) { + (Literal::Float(x), ValueType::F64) => self.code_builder.f64_const(*x), + (Literal::Float(x), ValueType::F32) => self.code_builder.f32_const(*x as f32), + (Literal::Int(x), ValueType::I64) => { + self.code_builder.i64_const(i128::from_ne_bytes(*x) as i64) + } + (Literal::Int(x), ValueType::I32) => { + self.code_builder.i32_const(i128::from_ne_bytes(*x) as i32) + } + (Literal::Bool(x), ValueType::I32) => self.code_builder.i32_const(*x as i32), + (Literal::Byte(x), ValueType::I32) => self.code_builder.i32_const(*x as i32), + _ => invalid_error(), + }; + self.code_builder.set_local(*local_id); + } + + StoredValue::StackMemory { location, .. } => { + let mut write128 = |lower_bits, upper_bits| { + let (local_id, offset) = + location.local_and_offset(self.storage.stack_frame_pointer); + + self.code_builder.get_local(local_id); + self.code_builder.i64_const(lower_bits); + self.code_builder.i64_store(Align::Bytes8, offset); + + self.code_builder.get_local(local_id); + self.code_builder.i64_const(upper_bits); + self.code_builder.i64_store(Align::Bytes8, offset + 8); + }; + + match lit { + Literal::Decimal(bytes) => { + let (upper_bits, lower_bits) = RocDec::from_ne_bytes(*bytes).as_bits(); + write128(lower_bits as i64, upper_bits); + } + Literal::Int(x) | Literal::U128(x) => { + let lower_bits = (i128::from_ne_bytes(*x) & 0xffff_ffff_ffff_ffff) as i64; + let upper_bits = (i128::from_ne_bytes(*x) >> 64) as i64; + write128(lower_bits, upper_bits); + } + Literal::Float(_) => { + // Also not implemented in LLVM backend (nor in Rust!) + todo!("f128 type"); + } + Literal::Str(string) => { + let (local_id, offset) = + location.local_and_offset(self.storage.stack_frame_pointer); + + self.expr_string_literal(string, local_id, offset); + } + // Bools and bytes should not be stored in the stack frame + Literal::Bool(_) | Literal::Byte(_) => invalid_error(), + } + } + }; + } + + fn expr_string_literal(&mut self, string: &str, local_id: LocalId, offset: u32) { + let len = string.len(); + if len < 12 { + // Construct the bytes of the small string + let mut bytes = [0; 12]; + bytes[0..len].clone_from_slice(string.as_bytes()); + bytes[11] = 0x80 | (len as u8); + + // Transform into two integers, to minimise number of instructions + let bytes_split: &([u8; 8], [u8; 4]) = unsafe { std::mem::transmute(&bytes) }; + let int64 = i64::from_le_bytes(bytes_split.0); + let int32 = i32::from_le_bytes(bytes_split.1); + + // Write the integers to memory + self.code_builder.get_local(local_id); + self.code_builder.i64_const(int64); + self.code_builder.i64_store(Align::Bytes4, offset); + self.code_builder.get_local(local_id); + self.code_builder.i32_const(int32); + self.code_builder.i32_store(Align::Bytes4, offset + 8); + } else { + let bytes = string.as_bytes(); + let elements_addr = self.store_bytes_in_data_section(bytes); + + // ptr + self.code_builder.get_local(local_id); + self.code_builder.i32_const(elements_addr as i32); + self.code_builder.i32_store(Align::Bytes4, offset); + + // len + self.code_builder.get_local(local_id); + self.code_builder.i32_const(string.len() as i32); + self.code_builder.i32_store(Align::Bytes4, offset + 4); + + // capacity + self.code_builder.get_local(local_id); + self.code_builder.i32_const(string.len() as i32); + self.code_builder.i32_store(Align::Bytes4, offset + 8); + }; + } + + /// Create a string constant in the module data section + /// Return the data we need for code gen: linker symbol index and memory address + fn store_bytes_in_data_section(&mut self, bytes: &[u8]) -> u32 { + // Place the segment at a 4-byte aligned offset + let segment_addr = round_up_to_alignment!(self.module.data.end_addr, PTR_SIZE); + let elements_addr = segment_addr + PTR_SIZE; + let length_with_refcount = 4 + bytes.len(); + self.module.data.end_addr = segment_addr + length_with_refcount as u32; + + let mut segment = DataSegment { + mode: DataMode::active_at(segment_addr), + init: Vec::with_capacity_in(length_with_refcount, self.env.arena), + }; + + // Prefix the string bytes with "infinite" refcount + let refcount_max_bytes: [u8; 4] = (REFCOUNT_MAX as i32).to_le_bytes(); + segment.init.extend_from_slice(&refcount_max_bytes); + segment.init.extend_from_slice(bytes); + + self.module.data.append_segment(segment); + + elements_addr + } + + fn expr_null_pointer(&mut self) { + self.code_builder.i32_const(0); + } + + /******************************************************************* + * Call expressions + *******************************************************************/ + + fn expr_call( + &mut self, + call_type: &CallType<'a>, + arguments: &'a [Symbol], + ret_sym: Symbol, + ret_layout: InLayout<'a>, + ret_storage: &StoredValue, + ) { + match call_type { + CallType::ByName { + name: func_sym, + arg_layouts, + ret_layout: result, + .. + } => { + let proc_layout = ProcLayout { + arguments: arg_layouts, + result: *result, + niche: func_sym.niche(), + }; + self.expr_call_by_name( + func_sym.name(), + &proc_layout, + arguments, + ret_sym, + ret_layout, + ret_storage, + ) + } + + CallType::ByPointer { .. } => { + todo_lambda_erasure!() + } + + CallType::LowLevel { op: lowlevel, .. } => { + self.expr_call_low_level(*lowlevel, arguments, ret_sym, ret_layout, ret_storage) + } + + CallType::HigherOrder(higher_order_lowlevel) => { + call_higher_order_lowlevel(self, ret_sym, &ret_layout, higher_order_lowlevel) + } + + CallType::Foreign { + foreign_symbol, + ret_layout, + } => { + let name = foreign_symbol.as_str(); + let wasm_layout = WasmLayout::new(self.layout_interner, *ret_layout); + self.storage.load_symbols_for_call( + &mut self.code_builder, + arguments, + ret_sym, + &wasm_layout, + ); + self.call_host_fn_after_loading_args(name) + } + } + } + + fn expr_call_by_name( + &mut self, + func_sym: Symbol, + proc_layout: &ProcLayout<'a>, + arguments: &'a [Symbol], + ret_sym: Symbol, + ret_layout: InLayout<'a>, + ret_storage: &StoredValue, + ) { + let wasm_layout = WasmLayout::new(self.layout_interner, ret_layout); + + // If this function is just a lowlevel wrapper, then inline it + if let LowLevelWrapperType::CanBeReplacedBy(lowlevel) = + LowLevelWrapperType::from_symbol(func_sym) + { + return self.expr_call_low_level(lowlevel, arguments, ret_sym, ret_layout, ret_storage); + } + + self.storage.load_symbols_for_call( + &mut self.code_builder, + arguments, + ret_sym, + &wasm_layout, + ); + + let roc_proc_index = self + .proc_lookup + .iter() + .position(|lookup| lookup.name == func_sym && &lookup.layout == proc_layout) + .unwrap_or_else(|| { + internal_error!( + "Could not find procedure {:?} with proc_layout:\n{:#?}\nKnown procedures:\n{:#?}", + func_sym, + proc_layout, + self.proc_lookup + ); + }); + + let wasm_fn_index = self.fn_index_offset + roc_proc_index as u32; + + self.code_builder.call(wasm_fn_index); + } + + fn expr_call_low_level( + &mut self, + lowlevel: LowLevel, + arguments: &'a [Symbol], + ret_symbol: Symbol, + ret_layout: InLayout<'a>, + ret_storage: &StoredValue, + ) { + let low_level_call = LowLevelCall { + lowlevel, + arguments, + ret_symbol, + ret_layout, + ret_layout_raw: self.layout_interner.get_repr(ret_layout), + ret_storage: ret_storage.to_owned(), + }; + low_level_call.generate(self); + } + + /// Generate a call instruction to a host function or Zig builtin. + pub fn call_host_fn_after_loading_args(&mut self, name: &str) { + let (_, fn_index) = self + .host_lookup + .iter() + .find(|(fn_name, _)| *fn_name == name) + .unwrap_or_else(|| panic!("The Roc app tries to call `{name}` but I can't find it!")); + + self.called_fns.set(*fn_index as usize, true); + + if *fn_index < self.import_fn_count { + self.code_builder.call_import(*fn_index); + } else { + self.code_builder.call(*fn_index); + } + } + + /// Call a helper procedure that implements `==` for a data structure (not numbers or Str) + /// If this is the first call for this Layout, it will generate the IR for the procedure. + /// Call stack is expr_call_low_level -> LowLevelCall::generate -> call_eq_specialized + /// It's a bit circuitous, but the alternative is to give low_level.rs `pub` access to + /// interns, helper_proc_gen, and expr(). That just seemed all wrong. + pub fn call_eq_specialized( + &mut self, + arguments: &'a [Symbol], + arg_layout: InLayout<'a>, + ret_symbol: Symbol, + ret_storage: &StoredValue, + ) { + let ident_ids = self + .interns + .all_ident_ids + .get_mut(&self.env.module_id) + .unwrap(); + + // Get an IR expression for the call to the specialized procedure + let (specialized_call_expr, new_specializations) = self + .helper_proc_gen + .call_specialized_equals(ident_ids, self.layout_interner, arg_layout, arguments); + + // If any new specializations were created, register their symbol data + for (spec_sym, spec_layout) in new_specializations.into_iter() { + self.register_helper_proc(spec_sym, spec_layout, ProcSource::Helper); + } + + // Generate Wasm code for the IR call expression + self.expr( + ret_symbol, + self.env.arena.alloc(specialized_call_expr), + Layout::BOOL, + ret_storage, + ); + } + + /******************************************************************* + * Structs + *******************************************************************/ + + fn expr_struct( + &mut self, + sym: Symbol, + layout: InLayout<'a>, + storage: &StoredValue, + fields: &'a [Symbol], + ) { + match self.layout_interner.get_repr(layout) { + LayoutRepr::Struct { .. } => { + match storage { + StoredValue::StackMemory { location, size, .. } => { + if *size > 0 { + let (local_id, struct_offset) = + location.local_and_offset(self.storage.stack_frame_pointer); + let mut field_offset = struct_offset; + for field in fields.iter() { + field_offset += self.storage.copy_value_to_memory( + &mut self.code_builder, + local_id, + field_offset, + *field, + ); + } + } else { + // Zero-size struct. No code to emit. + // These values are purely conceptual, they only exist internally in the compiler + } + } + _ => { + internal_error!("Cannot create struct {:?} with storage {:?}", sym, storage) + } + }; + } + LayoutRepr::LambdaSet(lambdaset) => { + self.expr_struct(sym, lambdaset.runtime_representation(), storage, fields) + } + _ => { + if !fields.is_empty() { + // Struct expression but not Struct layout => single element. Copy it. + let field_storage = self.storage.get(&fields[0]).to_owned(); + self.storage + .clone_value(&mut self.code_builder, storage, &field_storage); + } else { + // Empty record. Nothing to do. + } + } + } + } + + fn expr_struct_at_index( + &mut self, + sym: Symbol, + index: u64, + field_layouts: &'a [InLayout<'a>], + structure: Symbol, + ) { + let (from_addr_val, mut offset) = match self.storage.get(&structure) { + StoredValue::StackMemory { location, .. } => { + let (local_id, offset) = + location.local_and_offset(self.storage.stack_frame_pointer); + (AddressValue::NotLoaded(local_id), offset) + } + + StoredValue::Local { + value_type, + local_id, + .. + } => { + debug_assert!(matches!(value_type, ValueType::I32)); + (AddressValue::NotLoaded(*local_id), 0) + } + }; + for field in field_layouts.iter().take(index as usize) { + offset += self.layout_interner.stack_size(*field); + } + self.storage + .copy_value_from_memory(&mut self.code_builder, sym, from_addr_val, offset); + } + + /******************************************************************* + * Arrays + *******************************************************************/ + + pub fn expr_array( + &mut self, + sym: Symbol, + storage: &StoredValue, + elem_layout: InLayout<'a>, + elems: &'a [ListLiteralElement<'a>], + ) { + if let StoredValue::StackMemory { location, .. } = storage { + let size = self.layout_interner.stack_size(elem_layout) * (elems.len() as u32); + + // Allocate heap space and store its address in a local variable + let heap_local_id = self.storage.create_anonymous_local(PTR_TYPE); + let heap_alignment = self.layout_interner.alignment_bytes(elem_layout); + self.allocate_with_refcount(Some(size), heap_alignment, 1); + self.code_builder.set_local(heap_local_id); + + let (stack_local_id, stack_offset) = + location.local_and_offset(self.storage.stack_frame_pointer); + + // elements pointer + self.code_builder.get_local(stack_local_id); + self.code_builder.get_local(heap_local_id); + self.code_builder.i32_store(Align::Bytes4, stack_offset); + + // length of the list + self.code_builder.get_local(stack_local_id); + self.code_builder.i32_const(elems.len() as i32); + self.code_builder + .i32_store(Align::Bytes4, stack_offset + 4 * Builtin::WRAPPER_LEN); + + // capacity of the list + self.code_builder.get_local(stack_local_id); + self.code_builder.i32_const(elems.len() as i32); + self.code_builder + .i32_store(Align::Bytes4, stack_offset + 4 * Builtin::WRAPPER_CAPACITY); + + let mut elem_offset = 0; + + for (i, elem) in elems.iter().enumerate() { + let elem_sym = match elem { + ListLiteralElement::Literal(lit) => { + // This has no Symbol but our storage methods expect one. + // Let's just pretend it was defined in a `Let`. + let debug_name = format!("{sym:?}_{i}"); + let elem_sym = self.create_symbol(&debug_name); + let expr = Expr::Literal(*lit); + + self.stmt_let_store_expr( + elem_sym, + elem_layout, + &expr, + StoredVarKind::Variable, + ); + + elem_sym + } + + ListLiteralElement::Symbol(elem_sym) => *elem_sym, + }; + + elem_offset += self.storage.copy_value_to_memory( + &mut self.code_builder, + heap_local_id, + elem_offset, + elem_sym, + ); + } + } else { + internal_error!("Unexpected storage for Array {:?}: {:?}", sym, storage) + } + } + + fn expr_empty_array(&mut self, sym: Symbol, storage: &StoredValue) { + if let StoredValue::StackMemory { location, .. } = storage { + let (local_id, offset) = location.local_and_offset(self.storage.stack_frame_pointer); + + // Store 12 bytes of zeros { elements: null, length: 0, capacity: 0 } + debug_assert_eq!(Builtin::LIST_WORDS, 3); + self.code_builder.get_local(local_id); + self.code_builder.i64_const(0); + self.code_builder.i64_store(Align::Bytes4, offset); + self.code_builder.get_local(local_id); + self.code_builder.i32_const(0); + self.code_builder.i32_store(Align::Bytes4, offset + 8); + } else { + internal_error!("Unexpected storage for {:?}", sym) + } + } + + /******************************************************************* + * Tag Unions + *******************************************************************/ + + fn expr_tag( + &mut self, + union_layout: &UnionLayout<'a>, + tag_id: TagIdIntType, + arguments: &'a [Symbol], + stored: &StoredValue, + maybe_reused: Option, + ) { + if union_layout.tag_is_null(tag_id) { + self.code_builder.i32_const(0); + return; + } + + let stores_tag_id_as_data = union_layout.stores_tag_id_as_data(TARGET_INFO); + let stores_tag_id_in_pointer = union_layout.stores_tag_id_in_pointer(TARGET_INFO); + let (data_size, data_alignment) = + union_layout.data_size_and_alignment(self.layout_interner); + + let (local_id, data_offset) = match stored { + StoredValue::StackMemory { location, .. } => { + location.local_and_offset(self.storage.stack_frame_pointer) + } + StoredValue::Local { local_id, .. } => { + // Tag is stored as a heap pointer. + if let Some(reused) = maybe_reused { + // Reuse an existing heap allocation, if one is available (not NULL at runtime) + self.storage.load_symbols(&mut self.code_builder, &[reused]); + self.code_builder.if_(); + { + self.storage.load_symbols(&mut self.code_builder, &[reused]); + self.code_builder.set_local(*local_id); + } + self.code_builder.else_(); + { + self.allocate_with_refcount(Some(data_size), data_alignment, 1); + self.code_builder.set_local(*local_id); + } + self.code_builder.end(); + } else { + // Call the allocator to get a memory address. + self.allocate_with_refcount(Some(data_size), data_alignment, 1); + self.code_builder.set_local(*local_id); + } + (*local_id, 0) + } + }; + + // Write the field values to memory + let mut field_offset = data_offset; + for field_symbol in arguments.iter() { + field_offset += self.storage.copy_value_to_memory( + &mut self.code_builder, + local_id, + field_offset, + *field_symbol, + ); + } + + // Store the tag ID (if any) + if stores_tag_id_as_data { + let id_offset = data_offset + union_layout.tag_id_offset(self.layout_interner).unwrap(); + + let id_align = union_layout.discriminant().alignment_bytes(); + let id_align = Align::from(id_align); + + self.code_builder.get_local(local_id); + + match id_align { + Align::Bytes1 => { + self.code_builder.i32_const(tag_id as i32); + self.code_builder.i32_store8(id_align, id_offset); + } + Align::Bytes2 => { + self.code_builder.i32_const(tag_id as i32); + self.code_builder.i32_store16(id_align, id_offset); + } + Align::Bytes4 => { + self.code_builder.i32_const(tag_id as i32); + self.code_builder.i32_store(id_align, id_offset); + } + Align::Bytes8 => { + self.code_builder.i64_const(tag_id as i64); + self.code_builder.i64_store(id_align, id_offset); + } + } + } else if stores_tag_id_in_pointer && tag_id != 0 { + self.code_builder.get_local(local_id); + self.code_builder.i32_const(tag_id as i32); + self.code_builder.i32_or(); + self.code_builder.set_local(local_id); + } + } + + fn expr_get_tag_id( + &mut self, + structure: Symbol, + union_layout: &UnionLayout<'a>, + stored_value: &StoredValue, + ) { + use UnionLayout::*; + + let block_result_id = match union_layout { + NonRecursive(_) => None, + Recursive(_) => None, + NonNullableUnwrapped(_) => { + self.code_builder.i32_const(0); + return; + } + NullableWrapped { nullable_id, .. } => { + let local_id = match stored_value { + StoredValue::Local { local_id, .. } => *local_id, + _ => internal_error!("ensure_value_has_local didn't work"), + }; + + // load pointer + self.storage + .load_symbols(&mut self.code_builder, &[structure]); + + // null check + self.code_builder.i32_eqz(); + self.code_builder.if_(); + self.code_builder.i32_const(*nullable_id as i32); + self.code_builder.set_local(local_id); + self.code_builder.else_(); + Some(local_id) + } + NullableUnwrapped { nullable_id, .. } => { + self.code_builder.i32_const(!(*nullable_id) as i32); + self.code_builder.i32_const(*nullable_id as i32); + self.storage + .load_symbols(&mut self.code_builder, &[structure]); + self.code_builder.select(); + None + } + }; + + if union_layout.stores_tag_id_as_data(TARGET_INFO) { + let id_offset = union_layout.tag_id_offset(self.layout_interner).unwrap(); + + let id_align = union_layout.discriminant().alignment_bytes(); + let id_align = Align::from(id_align); + + self.storage + .load_symbols(&mut self.code_builder, &[structure]); + + use roc_mono::layout::Discriminant::*; + match union_layout.discriminant() { + U0 | U1 | U8 => self.code_builder.i32_load8_u(id_align, id_offset), + U16 => self.code_builder.i32_load16_u(id_align, id_offset), + } + } else if union_layout.stores_tag_id_in_pointer(TARGET_INFO) { + self.storage + .load_symbols(&mut self.code_builder, &[structure]); + self.code_builder.i32_const(3); + self.code_builder.i32_and(); + } + + if let Some(local_id) = block_result_id { + self.code_builder.set_local(local_id); + self.code_builder.end(); + } + } + + fn expr_union_at_index( + &mut self, + structure: Symbol, + tag_id: TagIdIntType, + union_layout: &UnionLayout<'a>, + index: u64, + symbol: Symbol, + ) { + use UnionLayout::*; + + debug_assert!(!union_layout.tag_is_null(tag_id)); + + let tag_index = tag_id as usize; + let field_layouts = match union_layout { + NonRecursive(tags) => tags[tag_index], + Recursive(tags) => tags[tag_index], + NonNullableUnwrapped(layouts) => *layouts, + NullableWrapped { + other_tags, + nullable_id, + } => { + let index = if tag_index > *nullable_id as usize { + tag_index - 1 + } else { + tag_index + }; + other_tags[index] + } + NullableUnwrapped { other_fields, .. } => *other_fields, + }; + + let field_offset: u32 = field_layouts + .iter() + .take(index as usize) + .map(|field_layout| self.layout_interner.stack_size(*field_layout)) + .sum(); + + // Get pointer and offset to the tag's data + let structure_storage = self.storage.get(&structure).to_owned(); + let (tag_local_id, tag_offset) = match structure_storage { + StoredValue::StackMemory { location, .. } => { + location.local_and_offset(self.storage.stack_frame_pointer) + } + StoredValue::Local { local_id, .. } => (local_id, 0), + }; + + let stores_tag_id_in_pointer = union_layout.stores_tag_id_in_pointer(TARGET_INFO); + + let from_addr_val = if stores_tag_id_in_pointer { + self.code_builder.get_local(tag_local_id); + self.code_builder.i32_const(-4); // 11111111...1100 + self.code_builder.i32_and(); + AddressValue::Loaded + } else { + AddressValue::NotLoaded(tag_local_id) + }; + + let from_offset = tag_offset + field_offset; + self.storage.copy_value_from_memory( + &mut self.code_builder, + symbol, + from_addr_val, + from_offset, + ); + } + + fn expr_union_field_ptr_at_index( + &mut self, + structure: Symbol, + tag_id: TagIdIntType, + union_layout: &UnionLayout<'a>, + index: u64, + storage: &StoredValue, + ) { + use UnionLayout::*; + + debug_assert!(!union_layout.tag_is_null(tag_id)); + + let tag_index = tag_id as usize; + let field_layouts = match union_layout { + NonRecursive(tags) => tags[tag_index], + Recursive(tags) => tags[tag_index], + NonNullableUnwrapped(layouts) => *layouts, + NullableWrapped { + other_tags, + nullable_id, + } => { + let index = if tag_index > *nullable_id as usize { + tag_index - 1 + } else { + tag_index + }; + other_tags[index] + } + NullableUnwrapped { other_fields, .. } => *other_fields, + }; + + let field_offset: u32 = field_layouts + .iter() + .take(index as usize) + .map(|field_layout| self.layout_interner.stack_size(*field_layout)) + .sum(); + + // Get pointer and offset to the tag's data + let (tag_local_id, tag_offset) = match self.storage.get(&structure) { + StoredValue::StackMemory { location, .. } => { + location.local_and_offset(self.storage.stack_frame_pointer) + } + StoredValue::Local { local_id, .. } => (*local_id, 0), + }; + + let stores_tag_id_in_pointer = union_layout.stores_tag_id_in_pointer(TARGET_INFO); + + let from_offset = tag_offset + field_offset; + + self.code_builder.get_local(tag_local_id); + + if stores_tag_id_in_pointer { + self.code_builder.i32_const(-4); // 11111111...1100 + self.code_builder.i32_and(); + } + + self.code_builder.i32_const(from_offset as _); + self.code_builder.i32_add(); + + let symbol_local = match storage { + StoredValue::Local { local_id, .. } => *local_id, + _ => internal_error!("A heap pointer will always be an i32"), + }; + + self.code_builder.set_local(symbol_local); + } + + /******************************************************************* + * Box + *******************************************************************/ + + pub(crate) fn ptr_load(&mut self, ret_sym: Symbol, arg_sym: Symbol) { + let (from_addr_val, from_offset) = match self.storage.get(&arg_sym) { + StoredValue::Local { local_id, .. } => (AddressValue::NotLoaded(*local_id), 0), + StoredValue::StackMemory { location, .. } => { + let (local_id, offset) = + location.local_and_offset(self.storage.stack_frame_pointer); + (AddressValue::NotLoaded(local_id), offset) + } + }; + + // Copy the value + self.storage.copy_value_from_memory( + &mut self.code_builder, + ret_sym, + from_addr_val, + from_offset, + ); + } + + /******************************************************************* + * Refcounting & Heap allocation + *******************************************************************/ + + /// Allocate heap space and write an initial refcount + /// If the data size is known at compile time, pass it in comptime_data_size. + /// If size is only known at runtime, push *data* size to the VM stack first. + /// Leaves the *data* address on the VM stack + fn allocate_with_refcount( + &mut self, + comptime_data_size: Option, + alignment_bytes: u32, + initial_refcount: u32, + ) { + if !self.can_relocate_heap { + // This will probably only happen for test hosts. + panic!("The app tries to allocate heap memory but the host doesn't support that. It needs to export symbols __heap_base and __heap_end"); + } + // Add extra bytes for the refcount + let extra_bytes = alignment_bytes.max(PTR_SIZE); + + if let Some(data_size) = comptime_data_size { + // Data size known at compile time and passed as an argument + self.code_builder + .i32_const((data_size + extra_bytes) as i32); + } else { + // Data size known only at runtime and is on top of VM stack + self.code_builder.i32_const(extra_bytes as i32); + self.code_builder.i32_add(); + } + + // Provide a constant for the alignment argument + self.code_builder.i32_const(alignment_bytes as i32); + + // Call the foreign function. (Zig and C calling conventions are the same for this signature) + self.call_host_fn_after_loading_args("roc_alloc"); + + // Save the allocation address to a temporary local variable + let local_id = self.storage.create_anonymous_local(ValueType::I32); + self.code_builder.tee_local(local_id); + + // Write the initial refcount + let refcount_offset = extra_bytes - PTR_SIZE; + let encoded_refcount = (initial_refcount as i32) - 1 + i32::MIN; + self.code_builder.i32_const(encoded_refcount); + self.code_builder.i32_store(Align::Bytes4, refcount_offset); + + // Put the data address on the VM stack + self.code_builder.get_local(local_id); + self.code_builder.i32_const(extra_bytes as i32); + self.code_builder.i32_add(); + } + + fn expr_reset(&mut self, argument: Symbol, ret_symbol: Symbol, ret_storage: &StoredValue) { + let ident_ids = self + .interns + .all_ident_ids + .get_mut(&self.env.module_id) + .unwrap(); + + // Get an IR expression for the call to the specialized procedure + let layout = self.storage.symbol_layouts[&argument]; + let (specialized_call_expr, new_specializations) = self + .helper_proc_gen + .call_reset_refcount(ident_ids, self.layout_interner, layout, argument); + + // If any new specializations were created, register their symbol data + for (spec_sym, spec_layout) in new_specializations.into_iter() { + self.register_helper_proc(spec_sym, spec_layout, ProcSource::Helper); + } + + // Generate Wasm code for the IR call expression + self.expr( + ret_symbol, + self.env.arena.alloc(specialized_call_expr), + Layout::BOOL, + ret_storage, + ); + } + + fn expr_resetref(&mut self, argument: Symbol, ret_symbol: Symbol, ret_storage: &StoredValue) { + let ident_ids = self + .interns + .all_ident_ids + .get_mut(&self.env.module_id) + .unwrap(); + + // Get an IR expression for the call to the specialized procedure + let layout = self.storage.symbol_layouts[&argument]; + let (specialized_call_expr, new_specializations) = self + .helper_proc_gen + .call_resetref_refcount(ident_ids, self.layout_interner, layout, argument); + + // If any new specializations were created, register their symbol data + for (spec_sym, spec_layout) in new_specializations.into_iter() { + self.register_helper_proc(spec_sym, spec_layout, ProcSource::Helper); + } + + // Generate Wasm code for the IR call expression + self.expr( + ret_symbol, + self.env.arena.alloc(specialized_call_expr), + Layout::BOOL, + ret_storage, + ); + } + + fn expr_alloca( + &mut self, + initializer: Option, + element_layout: InLayout<'a>, + ret_storage: &StoredValue, + ) { + // Alloca : a -> Ptr a + let (size, alignment_bytes) = self + .layout_interner + .stack_size_and_alignment(element_layout); + + let (frame_ptr, offset) = self + .storage + .allocate_anonymous_stack_memory(size, alignment_bytes); + + // write the default value into the stack memory + if let Some(initializer) = initializer { + self.storage.copy_value_to_memory( + &mut self.code_builder, + frame_ptr, + offset, + initializer, + ); + } + + // create a local variable for the pointer + let ptr_local_id = match ret_storage { + StoredValue::Local { local_id, .. } => *local_id, + _ => internal_error!("A pointer will always be an i32"), + }; + + self.code_builder.get_local(frame_ptr); + self.code_builder.i32_const(offset as i32); + self.code_builder.i32_add(); + self.code_builder.set_local(ptr_local_id); + } + + /// Generate a refcount helper procedure and return a pointer (table index) to it + /// This allows it to be indirectly called from Zig code + pub fn get_refcount_fn_index(&mut self, layout: InLayout<'a>, op: HelperOp) -> u32 { + let ident_ids = self + .interns + .all_ident_ids + .get_mut(&self.env.module_id) + .unwrap(); + + let (proc_symbol, new_specializations) = + self.helper_proc_gen + .gen_refcount_proc(ident_ids, self.layout_interner, layout, op); + + // If any new specializations were created, register their symbol data + for (spec_sym, spec_layout) in new_specializations.into_iter() { + self.register_helper_proc(spec_sym, spec_layout, ProcSource::Helper); + } + + let layout_repr = self.layout_interner.runtime_representation(layout); + let same_layout = + |layout| self.layout_interner.runtime_representation(layout) == layout_repr; + let proc_index = self + .proc_lookup + .iter() + .position(|lookup| { + lookup.name == proc_symbol && same_layout(lookup.layout.arguments[0]) + }) + .unwrap(); + + self.fn_index_offset + proc_index as u32 + } +} diff --git a/crates/compiler/gen_wasm/src/code_builder.rs b/crates/compiler/gen_wasm/src/code_builder.rs new file mode 100644 index 0000000000..ecec4ad2b0 --- /dev/null +++ b/crates/compiler/gen_wasm/src/code_builder.rs @@ -0,0 +1,580 @@ +use bitvec::vec::BitVec; +use bumpalo::collections::vec::Vec; +use bumpalo::Bump; +use core::panic; +use roc_wasm_module::linking::IndexRelocType; + +use roc_error_macros::internal_error; +use roc_wasm_module::opcodes::{OpCode, OpCode::*}; +use roc_wasm_module::serialize::SerialBuffer; +use roc_wasm_module::{ + round_up_to_alignment, Align, LocalId, RelocationEntry, ValueType, WasmModule, + FRAME_ALIGNMENT_BYTES, STACK_POINTER_GLOBAL_ID, +}; +use std::iter::repeat; + +use crate::DEBUG_SETTINGS; + +macro_rules! log_instruction { + ($($x: expr),+) => { + if DEBUG_SETTINGS.instructions { println!($($x,)*); } + }; +} + +// An instruction (local.set or local.tee) to be inserted into the function code +#[derive(Debug)] +struct Insertion { + at: usize, + start: usize, + end: usize, +} + +macro_rules! instruction_no_args { + ($method_name: ident, $opcode: expr) => { + pub fn $method_name(&mut self) { + self.inst($opcode); + } + }; +} + +macro_rules! instruction_memargs { + ($method_name: ident, $opcode: expr) => { + pub fn $method_name(&mut self, align: Align, offset: u32) { + self.inst_mem($opcode, align, offset); + } + }; +} + +#[derive(Debug)] +pub struct CodeBuilder<'a> { + pub arena: &'a Bump, + + /// The main container for the instructions + code: Vec<'a, u8>, + + /// Instruction bytes to be inserted into the code when finalizing the function + /// (Used for setting locals when we realise they are used multiple times) + insert_bytes: Vec<'a, u8>, + + /// Code locations where the insert_bytes should go + insertions: Vec<'a, Insertion>, + + /// Bytes for local variable declarations and stack-frame setup code. + /// We can't write this until we've finished the main code. But it goes + /// before it in the final output, so we need a separate vector. + preamble: Vec<'a, u8>, + + /// Encoded bytes for the inner length of the function, locals + code. + /// ("inner" because it doesn't include its own length!) + /// Again, we can't write this until we've finished the code and preamble, + /// but it goes before them in the binary, so it's a separate vector. + inner_length: Vec<'a, u8>, + + /// Relocations for calls to JS imports + /// When we remove unused imports, the live ones are re-indexed + import_relocations: Vec<'a, (usize, u32)>, + + /// Keep track of which local variables have been set + set_locals: BitVec, +} + +#[allow(clippy::new_without_default)] +impl<'a> CodeBuilder<'a> { + pub fn new(arena: &'a Bump) -> Self { + CodeBuilder { + arena, + code: Vec::with_capacity_in(1024, arena), + insertions: Vec::with_capacity_in(32, arena), + insert_bytes: Vec::with_capacity_in(64, arena), + preamble: Vec::with_capacity_in(32, arena), + inner_length: Vec::with_capacity_in(5, arena), + import_relocations: Vec::with_capacity_in(0, arena), + set_locals: BitVec::with_capacity(64), + } + } + + pub fn clear(&mut self) { + self.code.clear(); + self.insertions.clear(); + self.insert_bytes.clear(); + self.preamble.clear(); + self.inner_length.clear(); + self.import_relocations.clear(); + self.set_locals.clear(); + } + + /********************************************************** + + FUNCTION HEADER + + ***********************************************************/ + + /// Generate bytes to declare the function's local variables + fn build_local_declarations(&mut self, local_types: &[ValueType]) { + // reserve one byte for num_batches + self.preamble.push(0); + + if local_types.is_empty() { + return; + } + + // Write declarations in batches of the same ValueType + let mut num_batches: u32 = 0; + let mut batch_type = local_types[0]; + let mut batch_size = 0; + for t in local_types { + if *t == batch_type { + batch_size += 1; + } else { + self.preamble.encode_u32(batch_size); + self.preamble.push(batch_type as u8); + batch_type = *t; + batch_size = 1; + num_batches += 1; + } + } + self.preamble.encode_u32(batch_size); + self.preamble.push(batch_type as u8); + num_batches += 1; + + // Go back and write the number of batches at the start + if num_batches < 128 { + self.preamble[0] = num_batches as u8; + } else { + // We need more than 1 byte to encode num_batches! + // This is a ridiculous edge case, so just pad to 5 bytes for simplicity + let old_len = self.preamble.len(); + self.preamble.resize(old_len + 4, 0); + self.preamble.copy_within(1..old_len, 5); + self.preamble.overwrite_padded_u32(0, num_batches); + } + } + + /// Generate instruction bytes to grab a frame of stack memory on entering the function + fn build_stack_frame_push(&mut self, frame_size: i32, frame_pointer: LocalId) { + // Can't use the usual instruction methods because they push to self.code. + // This is the only case where we push instructions somewhere different. + self.preamble.push(GETGLOBAL as u8); + self.preamble.encode_u32(STACK_POINTER_GLOBAL_ID); + self.preamble.push(I32CONST as u8); + self.preamble.encode_i32(frame_size); + self.preamble.push(I32SUB as u8); + self.preamble.push(TEELOCAL as u8); + self.preamble.encode_u32(frame_pointer.0); + self.preamble.push(SETGLOBAL as u8); + self.preamble.encode_u32(STACK_POINTER_GLOBAL_ID); + } + + /// Generate instruction bytes to release a frame of stack memory on leaving the function + fn build_stack_frame_pop(&mut self, frame_size: i32, frame_pointer: LocalId) { + self.get_local(frame_pointer); + self.i32_const(frame_size); + self.i32_add(); + self.set_global(STACK_POINTER_GLOBAL_ID); + } + + /// Build the function header: local declarations, stack frame push/pop code, and function length + /// After this, all bytes have been generated (but not yet serialized) and we know the final size. + pub fn build_fn_header_and_footer( + &mut self, + local_types: &[ValueType], + frame_size: i32, + frame_pointer: Option, + ) { + self.build_local_declarations(local_types); + + if frame_size != 0 { + if let Some(frame_ptr_id) = frame_pointer { + let aligned_size = round_up_to_alignment!(frame_size, FRAME_ALIGNMENT_BYTES); + self.build_stack_frame_push(aligned_size, frame_ptr_id); + self.build_stack_frame_pop(aligned_size, frame_ptr_id); // footer + } + } + + self.code.push(END as u8); + + let inner_len = self.preamble.len() + self.code.len() + self.insert_bytes.len(); + self.inner_length.encode_u32(inner_len as u32); + + // Sort insertions. They are not created in order of assignment, but in order of *second* usage. + self.insertions.sort_by_key(|ins| ins.at); + } + + /********************************************************** + + SERIALIZE + + ***********************************************************/ + + pub fn size(&self) -> usize { + self.inner_length.len() + self.preamble.len() + self.code.len() + self.insert_bytes.len() + } + + /// Serialize all byte vectors in the right order + /// Insert relocations for imported functions + pub fn insert_into_module(&self, module: &mut WasmModule<'a>) { + let fn_offset = module.code.bytes.len(); + module.code.function_count += 1; + module.code.function_offsets.push(fn_offset as u32); + + // Insertions are chunks of code we generated out-of-order. + // Now insert them at the correct offsets. + let buffer = &mut module.code.bytes; + buffer.extend_from_slice(&self.inner_length); + buffer.extend_from_slice(&self.preamble); + + let code_offset = buffer.len(); + let mut code_pos = 0; + for Insertion { at, start, end } in self.insertions.iter() { + buffer.extend_from_slice(&self.code[code_pos..*at]); + code_pos = *at; + buffer.extend_from_slice(&self.insert_bytes[*start..*end]); + } + + buffer.extend_from_slice(&self.code[code_pos..self.code.len()]); + + // Create linker relocations for calls to imported functions, whose indices may change during DCE. + let relocs = &mut module.reloc_code.entries; + let mut skip = 0; + for (reloc_code_pos, reloc_fn) in self.import_relocations.iter() { + let mut insertion_bytes = 0; + for (i, insertion) in self.insertions.iter().enumerate().skip(skip) { + if insertion.at >= *reloc_code_pos { + break; + } + insertion_bytes = insertion.end; + skip = i; + } + // Adjust for (1) the offset of this function in the Code section and (2) our own Insertions. + let offset = reloc_code_pos + code_offset + insertion_bytes; + let symbol_index = module + .linking + .find_imported_fn_sym_index(*reloc_fn) + .unwrap(); + relocs.push(RelocationEntry::Index { + type_id: IndexRelocType::FunctionIndexLeb, + offset: offset as u32, + symbol_index, + }); + } + } + + /********************************************************** + + INSTRUCTION HELPER METHODS + + ***********************************************************/ + + /// Base method for generating instructions + /// Emits the opcode and simulates VM stack push/pop + fn inst_base(&mut self, opcode: OpCode) { + self.code.push(opcode as u8); + } + + /// Plain instruction without any immediates + fn inst(&mut self, opcode: OpCode) { + self.inst_base(opcode); + log_instruction!("{opcode:?}"); + } + + /// Block instruction + fn inst_block(&mut self, opcode: OpCode) { + self.inst_base(opcode); + + // We don't support block result types. Too hard to track types through arbitrary control flow. + // This results in slightly more instructions but not much. (Rust does the same thing!) + self.code.push(ValueType::VOID); + + log_instruction!("{opcode:?}"); + } + + fn inst_imm32(&mut self, opcode: OpCode, immediate: u32) { + self.inst_base(opcode); + self.code.encode_u32(immediate); + log_instruction!("{:10}\t{}", format!("{opcode:?}"), immediate); + } + + fn inst_mem(&mut self, opcode: OpCode, align: Align, offset: u32) { + self.inst_base(opcode); + self.code.push(align as u8); + self.code.encode_u32(offset); + log_instruction!("{:10} {:?} {}", format!("{opcode:?}"), align, offset); + } + + /********************************************************** + + INSTRUCTION METHODS + + One method for each Wasm instruction (in same order as the spec) + macros are for compactness & readability for the most common cases + Patterns that don't repeat very much don't have macros + + ***********************************************************/ + + instruction_no_args!(unreachable_, UNREACHABLE); + instruction_no_args!(nop, NOP); + + pub fn block(&mut self) { + self.inst_block(BLOCK); + } + pub fn loop_(&mut self) { + self.inst_block(LOOP); + } + pub fn if_(&mut self) { + self.inst_block(IF); + } + pub fn else_(&mut self) { + self.inst(ELSE); + } + pub fn end(&mut self) { + self.inst(END); + } + pub fn br(&mut self, levels: u32) { + self.inst_imm32(BR, levels); + } + pub fn br_if(&mut self, levels: u32) { + self.inst_imm32(BRIF, levels); + } + #[allow(dead_code)] + fn br_table() { + unimplemented!("br_table instruction is not currently used"); + } + + instruction_no_args!(return_, RETURN); + + pub fn call(&mut self, function_index: u32) { + self.inst_base(CALL); + self.code.encode_padded_u32(function_index); + log_instruction!("{:10}\t{}", format!("{CALL:?}"), function_index); + } + + pub fn call_import(&mut self, function_index: u32) { + self.import_relocations + .push((self.code.len(), function_index)); + self.call(function_index) + } + + #[allow(dead_code)] + fn call_indirect() { + unimplemented!( + "There is no plan to implement call_indirect. Roc doesn't use function pointers" + ); + } + + instruction_no_args!(drop_, DROP); + instruction_no_args!(select, SELECT); + + pub fn get_local(&mut self, id: LocalId) { + self.inst_imm32(GETLOCAL, id.0); + } + pub fn set_local(&mut self, id: LocalId) { + self.inst_imm32(SETLOCAL, id.0); + let index = id.0 as usize; + let len = self.set_locals.len(); + if index >= len { + self.set_locals.extend(repeat(false).take(index + 1 - len)); + } + self.set_locals.set(index, true); + } + /// Check if a local variable has been set + /// This is not a Wasm instruction, just a helper method + pub fn is_set(&self, id: LocalId) -> bool { + let index = id.0 as usize; + (index < self.set_locals.len()) && self.set_locals[index] + } + pub fn tee_local(&mut self, id: LocalId) { + self.inst_imm32(TEELOCAL, id.0); + } + pub fn get_global(&mut self, id: u32) { + self.inst_imm32(GETGLOBAL, id); + } + pub fn set_global(&mut self, id: u32) { + self.inst_imm32(SETGLOBAL, id); + } + + instruction_memargs!(i32_load, I32LOAD); + instruction_memargs!(i64_load, I64LOAD); + instruction_memargs!(f32_load, F32LOAD); + instruction_memargs!(f64_load, F64LOAD); + instruction_memargs!(i32_load8_s, I32LOAD8S); + instruction_memargs!(i32_load8_u, I32LOAD8U); + instruction_memargs!(i32_load16_s, I32LOAD16S); + instruction_memargs!(i32_load16_u, I32LOAD16U); + instruction_memargs!(i64_load8_s, I64LOAD8S); + instruction_memargs!(i64_load8_u, I64LOAD8U); + instruction_memargs!(i64_load16_s, I64LOAD16S); + instruction_memargs!(i64_load16_u, I64LOAD16U); + instruction_memargs!(i64_load32_s, I64LOAD32S); + instruction_memargs!(i64_load32_u, I64LOAD32U); + instruction_memargs!(i32_store, I32STORE); + instruction_memargs!(i64_store, I64STORE); + instruction_memargs!(f32_store, F32STORE); + instruction_memargs!(f64_store, F64STORE); + instruction_memargs!(i32_store8, I32STORE8); + instruction_memargs!(i32_store16, I32STORE16); + instruction_memargs!(i64_store8, I64STORE8); + instruction_memargs!(i64_store16, I64STORE16); + instruction_memargs!(i64_store32, I64STORE32); + + pub fn memory_size(&mut self) { + self.inst(CURRENTMEMORY); + self.code.push(0); + } + pub fn memory_grow(&mut self) { + self.inst(GROWMEMORY); + self.code.push(0); + } + + fn log_const(&self, opcode: OpCode, x: T) + where + T: std::fmt::Debug + std::fmt::Display, + { + log_instruction!("{:10}\t{}", format!("{opcode:?}"), x); + } + pub fn i32_const(&mut self, x: i32) { + self.inst_base(I32CONST); + self.code.encode_i32(x); + self.log_const(I32CONST, x); + } + pub fn i64_const(&mut self, x: i64) { + self.inst_base(I64CONST); + self.code.encode_i64(x); + self.log_const(I64CONST, x); + } + pub fn f32_const(&mut self, x: f32) { + self.inst_base(F32CONST); + self.code.encode_f32(x); + self.log_const(F32CONST, x); + } + pub fn f64_const(&mut self, x: f64) { + self.inst_base(F64CONST); + self.code.encode_f64(x); + self.log_const(F64CONST, x); + } + + // TODO: Consider creating unified methods for numerical ops like 'eq' and 'add', + // passing the ValueType as an argument. Could simplify lowlevel code gen. + instruction_no_args!(i32_eqz, I32EQZ); + instruction_no_args!(i32_eq, I32EQ); + instruction_no_args!(i32_ne, I32NE); + instruction_no_args!(i32_lt_s, I32LTS); + instruction_no_args!(i32_lt_u, I32LTU); + instruction_no_args!(i32_gt_s, I32GTS); + instruction_no_args!(i32_gt_u, I32GTU); + instruction_no_args!(i32_le_s, I32LES); + instruction_no_args!(i32_le_u, I32LEU); + instruction_no_args!(i32_ge_s, I32GES); + instruction_no_args!(i32_ge_u, I32GEU); + instruction_no_args!(i64_eqz, I64EQZ); + instruction_no_args!(i64_eq, I64EQ); + instruction_no_args!(i64_ne, I64NE); + instruction_no_args!(i64_lt_s, I64LTS); + instruction_no_args!(i64_lt_u, I64LTU); + instruction_no_args!(i64_gt_s, I64GTS); + instruction_no_args!(i64_gt_u, I64GTU); + instruction_no_args!(i64_le_s, I64LES); + instruction_no_args!(i64_le_u, I64LEU); + instruction_no_args!(i64_ge_s, I64GES); + instruction_no_args!(i64_ge_u, I64GEU); + instruction_no_args!(f32_eq, F32EQ); + instruction_no_args!(f32_ne, F32NE); + instruction_no_args!(f32_lt, F32LT); + instruction_no_args!(f32_gt, F32GT); + instruction_no_args!(f32_le, F32LE); + instruction_no_args!(f32_ge, F32GE); + instruction_no_args!(f64_eq, F64EQ); + instruction_no_args!(f64_ne, F64NE); + instruction_no_args!(f64_lt, F64LT); + instruction_no_args!(f64_gt, F64GT); + instruction_no_args!(f64_le, F64LE); + instruction_no_args!(f64_ge, F64GE); + instruction_no_args!(i32_clz, I32CLZ); + instruction_no_args!(i32_ctz, I32CTZ); + instruction_no_args!(i32_popcnt, I32POPCNT); + instruction_no_args!(i32_add, I32ADD); + instruction_no_args!(i32_sub, I32SUB); + instruction_no_args!(i32_mul, I32MUL); + instruction_no_args!(i32_div_s, I32DIVS); + instruction_no_args!(i32_div_u, I32DIVU); + instruction_no_args!(i32_rem_s, I32REMS); + instruction_no_args!(i32_rem_u, I32REMU); + instruction_no_args!(i32_and, I32AND); + instruction_no_args!(i32_or, I32OR); + instruction_no_args!(i32_xor, I32XOR); + instruction_no_args!(i32_shl, I32SHL); + instruction_no_args!(i32_shr_s, I32SHRS); + instruction_no_args!(i32_shr_u, I32SHRU); + instruction_no_args!(i32_rotl, I32ROTL); + instruction_no_args!(i32_rotr, I32ROTR); + instruction_no_args!(i64_clz, I64CLZ); + instruction_no_args!(i64_ctz, I64CTZ); + instruction_no_args!(i64_popcnt, I64POPCNT); + instruction_no_args!(i64_add, I64ADD); + instruction_no_args!(i64_sub, I64SUB); + instruction_no_args!(i64_mul, I64MUL); + instruction_no_args!(i64_div_s, I64DIVS); + instruction_no_args!(i64_div_u, I64DIVU); + instruction_no_args!(i64_rem_s, I64REMS); + instruction_no_args!(i64_rem_u, I64REMU); + instruction_no_args!(i64_and, I64AND); + instruction_no_args!(i64_or, I64OR); + instruction_no_args!(i64_xor, I64XOR); + instruction_no_args!(i64_shl, I64SHL); + instruction_no_args!(i64_shr_s, I64SHRS); + instruction_no_args!(i64_shr_u, I64SHRU); + instruction_no_args!(i64_rotl, I64ROTL); + instruction_no_args!(i64_rotr, I64ROTR); + instruction_no_args!(f32_abs, F32ABS); + instruction_no_args!(f32_neg, F32NEG); + instruction_no_args!(f32_ceil, F32CEIL); + instruction_no_args!(f32_floor, F32FLOOR); + instruction_no_args!(f32_trunc, F32TRUNC); + instruction_no_args!(f32_nearest, F32NEAREST); + instruction_no_args!(f32_sqrt, F32SQRT); + instruction_no_args!(f32_add, F32ADD); + instruction_no_args!(f32_sub, F32SUB); + instruction_no_args!(f32_mul, F32MUL); + instruction_no_args!(f32_div, F32DIV); + instruction_no_args!(f32_min, F32MIN); + instruction_no_args!(f32_max, F32MAX); + instruction_no_args!(f32_copysign, F32COPYSIGN); + instruction_no_args!(f64_abs, F64ABS); + instruction_no_args!(f64_neg, F64NEG); + instruction_no_args!(f64_ceil, F64CEIL); + instruction_no_args!(f64_floor, F64FLOOR); + instruction_no_args!(f64_trunc, F64TRUNC); + instruction_no_args!(f64_nearest, F64NEAREST); + instruction_no_args!(f64_sqrt, F64SQRT); + instruction_no_args!(f64_add, F64ADD); + instruction_no_args!(f64_sub, F64SUB); + instruction_no_args!(f64_mul, F64MUL); + instruction_no_args!(f64_div, F64DIV); + instruction_no_args!(f64_min, F64MIN); + instruction_no_args!(f64_max, F64MAX); + instruction_no_args!(f64_copysign, F64COPYSIGN); + instruction_no_args!(i32_wrap_i64, I32WRAPI64); + instruction_no_args!(i32_trunc_s_f32, I32TRUNCSF32); + instruction_no_args!(i32_trunc_u_f32, I32TRUNCUF32); + instruction_no_args!(i32_trunc_s_f64, I32TRUNCSF64); + instruction_no_args!(i32_trunc_u_f64, I32TRUNCUF64); + instruction_no_args!(i64_extend_s_i32, I64EXTENDSI32); + instruction_no_args!(i64_extend_u_i32, I64EXTENDUI32); + instruction_no_args!(i64_trunc_s_f32, I64TRUNCSF32); + instruction_no_args!(i64_trunc_u_f32, I64TRUNCUF32); + instruction_no_args!(i64_trunc_s_f64, I64TRUNCSF64); + instruction_no_args!(i64_trunc_u_f64, I64TRUNCUF64); + instruction_no_args!(f32_convert_s_i32, F32CONVERTSI32); + instruction_no_args!(f32_convert_u_i32, F32CONVERTUI32); + instruction_no_args!(f32_convert_s_i64, F32CONVERTSI64); + instruction_no_args!(f32_convert_u_i64, F32CONVERTUI64); + instruction_no_args!(f32_demote_f64, F32DEMOTEF64); + instruction_no_args!(f64_convert_s_i32, F64CONVERTSI32); + instruction_no_args!(f64_convert_u_i32, F64CONVERTUI32); + instruction_no_args!(f64_convert_s_i64, F64CONVERTSI64); + instruction_no_args!(f64_convert_u_i64, F64CONVERTUI64); + instruction_no_args!(f64_promote_f32, F64PROMOTEF32); + instruction_no_args!(i32_reinterpret_f32, I32REINTERPRETF32); + instruction_no_args!(i64_reinterpret_f64, I64REINTERPRETF64); + instruction_no_args!(f32_reinterpret_i32, F32REINTERPRETI32); + instruction_no_args!(f64_reinterpret_i64, F64REINTERPRETI64); +} diff --git a/crates/compiler/gen_wasm/src/layout.rs b/crates/compiler/gen_wasm/src/layout.rs new file mode 100644 index 0000000000..dd37543a5b --- /dev/null +++ b/crates/compiler/gen_wasm/src/layout.rs @@ -0,0 +1,165 @@ +use roc_builtins::bitcode::{FloatWidth, IntWidth}; +use roc_error_macros::todo_lambda_erasure; +use roc_mono::layout::{InLayout, LayoutInterner, LayoutRepr, STLayoutInterner, UnionLayout}; + +use crate::{PTR_SIZE, PTR_TYPE}; +use roc_wasm_module::ValueType; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ReturnMethod { + /// This layout is returned from a Wasm function "normally" as a Primitive + Primitive(ValueType, u32), + /// This layout is returned by writing to a pointer passed as the first argument + WriteToPointerArg, + /// This layout is empty and requires no return value or argument (e.g. refcount helpers) + NoReturnValue, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum StackMemoryFormat { + /// Record, Str, List, etc. + DataStructure, + Int128, + Decimal, +} + +// See README for background information on Wasm locals, memory and function calls +#[derive(Debug, Clone)] +pub enum WasmLayout { + // Primitive number value, without any stack memory. + // For example, Roc i8 is represented as Primitive(ValueType::I32, 1) + Primitive(ValueType, u32), + + // Local pointer to stack memory + StackMemory { + size: u32, + alignment_bytes: u32, + format: StackMemoryFormat, + }, +} + +impl WasmLayout { + pub fn new<'a>(interner: &STLayoutInterner<'a>, layout: InLayout<'a>) -> Self { + use roc_mono::layout::Builtin::*; + use UnionLayout::*; + use ValueType::*; + + let (size, alignment_bytes) = interner.stack_size_and_alignment(layout); + + match interner.get_repr(layout) { + LayoutRepr::Builtin(Int(int_width)) => { + use IntWidth::*; + + match int_width { + I32 | U32 | I16 | U16 | I8 | U8 => Self::Primitive(ValueType::I32, size), + I64 | U64 => Self::Primitive(ValueType::I64, size), + I128 | U128 => Self::StackMemory { + size, + alignment_bytes, + format: StackMemoryFormat::Int128, + }, + } + } + + LayoutRepr::Builtin(Bool) => Self::Primitive(I32, size), + + LayoutRepr::Builtin(Float(float_width)) => { + use FloatWidth::*; + + match float_width { + F32 => Self::Primitive(ValueType::F32, size), + F64 => Self::Primitive(ValueType::F64, size), + } + } + + LayoutRepr::Builtin(Decimal) => Self::StackMemory { + size, + alignment_bytes, + format: StackMemoryFormat::Decimal, + }, + + LayoutRepr::LambdaSet(lambda_set) => { + WasmLayout::new(interner, lambda_set.runtime_representation()) + } + + LayoutRepr::Builtin(Str | List(_)) + | LayoutRepr::Struct { .. } + | LayoutRepr::Union(NonRecursive(_)) => Self::StackMemory { + size, + alignment_bytes, + format: StackMemoryFormat::DataStructure, + }, + + LayoutRepr::Union( + Recursive(_) + | NonNullableUnwrapped(_) + | NullableWrapped { .. } + | NullableUnwrapped { .. }, + ) + | LayoutRepr::Ptr(_) + | LayoutRepr::RecursivePointer(_) => Self::Primitive(PTR_TYPE, PTR_SIZE), + LayoutRepr::FunctionPointer(_) => todo_lambda_erasure!(), + LayoutRepr::Erased(_) => todo_lambda_erasure!(), + } + } + + /// The `ValueType`s to use for this layout when calling a Wasm function + /// One Roc argument can become 0, 1, or 2 Wasm arguments + pub fn arg_types(&self) -> &'static [ValueType] { + use ValueType::*; + + match self { + // 1 Roc argument => 1 Wasm argument (same for all calling conventions) + Self::Primitive(I32, _) => &[I32], + Self::Primitive(I64, _) => &[I64], + Self::Primitive(F32, _) => &[F32], + Self::Primitive(F64, _) => &[F64], + + // 1 Roc argument => 0-2 Wasm arguments (depending on size and calling convention) + Self::StackMemory { size, format, .. } => stack_memory_arg_types(*size, *format), + } + } + + pub fn return_method(&self) -> ReturnMethod { + match self { + Self::Primitive(ty, size) => ReturnMethod::Primitive(*ty, *size), + Self::StackMemory { size, format, .. } => stack_memory_return_method(*size, *format), + } + } +} + +/// The Wasm argument types to use when passing structs or 128-bit numbers +pub fn stack_memory_arg_types(size: u32, format: StackMemoryFormat) -> &'static [ValueType] { + use StackMemoryFormat::*; + use ValueType::*; + + match format { + Int128 | Decimal => &[I64, I64], + + DataStructure => { + if size == 0 { + // Zero-size Roc values like `{}` => no Wasm arguments + &[] + } else { + &[I32] // Always pass structs by reference (pointer to stack memory) + } + } + } +} + +pub fn stack_memory_return_method(size: u32, format: StackMemoryFormat) -> ReturnMethod { + use ReturnMethod::*; + use StackMemoryFormat::*; + + match format { + Int128 | Decimal => WriteToPointerArg, + + DataStructure => { + if size == 0 { + NoReturnValue + } else { + WriteToPointerArg + } + } + } +} diff --git a/crates/compiler/gen_wasm/src/lib.rs b/crates/compiler/gen_wasm/src/lib.rs new file mode 100644 index 0000000000..2192e666b5 --- /dev/null +++ b/crates/compiler/gen_wasm/src/lib.rs @@ -0,0 +1,301 @@ +//! Provides the WASM backend to generate Roc binaries. +mod backend; +mod code_builder; +mod layout; +mod low_level; +mod storage; + +// Helpers for interfacing to a Wasm module from outside +pub mod wasm32_result; +pub mod wasm32_sized; + +use bitvec::prelude::BitVec; +use bumpalo::collections::Vec; +use bumpalo::{self, Bump}; + +use roc_collections::all::{MutMap, MutSet}; +use roc_module::symbol::{Interns, ModuleId, Symbol}; +use roc_mono::code_gen_help::CodeGenHelp; +use roc_mono::ir::{Proc, ProcLayout}; +use roc_mono::layout::{LayoutIds, STLayoutInterner}; +use roc_target::TargetInfo; +use roc_wasm_module::parse::ParseError; +use roc_wasm_module::{Align, LocalId, ValueType, WasmModule}; + +use crate::backend::{ProcLookupData, ProcSource, WasmBackend}; +use crate::code_builder::CodeBuilder; + +const TARGET_INFO: TargetInfo = TargetInfo::default_wasm32(); +const PTR_SIZE: u32 = { + let value = TARGET_INFO.ptr_width() as u32; + + // const assert that our pointer width is actually 4 + // the code relies on the pointer width being exactly 4 + assert!(value == 4); + + value +}; +const PTR_TYPE: ValueType = ValueType::I32; + +pub const MEMORY_NAME: &str = "memory"; +pub const BUILTINS_IMPORT_MODULE_NAME: &str = "env"; +pub const STACK_POINTER_NAME: &str = "__stack_pointer"; + +pub struct Env<'a> { + pub arena: &'a Bump, + pub module_id: ModuleId, + pub exposed_to_host: MutSet, + pub stack_bytes: u32, +} + +impl Env<'_> { + pub const DEFAULT_STACK_BYTES: u32 = 1024 * 1024; +} + +/// Parse the preprocessed host binary +/// If successful, the module can be passed to build_app_binary +pub fn parse_host<'a>(arena: &'a Bump, host_bytes: &[u8]) -> Result, ParseError> { + let require_relocatable = true; + WasmModule::preload(arena, host_bytes, require_relocatable) +} + +/// Generate a Wasm module in binary form, ready to write to a file. Entry point from roc_build. +/// env environment data from previous compiler stages +/// interns names of functions and variables (as memory-efficient interned strings) +/// host_module parsed module from a Wasm object file containing all of the non-Roc code +/// procedures Roc code in monomorphized intermediate representation +pub fn build_app_binary<'a, 'r>( + env: &'r Env<'a>, + layout_interner: &'r mut STLayoutInterner<'a>, + interns: &'r mut Interns, + host_module: WasmModule<'a>, + procedures: MutMap<(Symbol, ProcLayout<'a>), Proc<'a>>, +) -> std::vec::Vec { + let (mut wasm_module, called_fns, _) = + build_app_module(env, layout_interner, interns, host_module, procedures); + + wasm_module.eliminate_dead_code(env.arena, called_fns); + + let mut buffer = std::vec::Vec::with_capacity(wasm_module.size()); + wasm_module.serialize(&mut buffer); + buffer +} + +/// Generate an unserialized Wasm module +/// Shared by all consumers of gen_wasm: roc_build, roc_repl_wasm, and test_gen +/// (roc_repl_wasm and test_gen will add more generated code for a wrapper function +/// that defines a common interface to `main`, independent of return type.) +pub fn build_app_module<'a, 'r>( + env: &'r Env<'a>, + layout_interner: &'r mut STLayoutInterner<'a>, + interns: &'r mut Interns, + host_module: WasmModule<'a>, + procedures: MutMap<(Symbol, ProcLayout<'a>), Proc<'a>>, +) -> (WasmModule<'a>, BitVec, u32) { + let mut layout_ids = LayoutIds::default(); + let mut procs = Vec::with_capacity_in(procedures.len(), env.arena); + let mut proc_lookup = Vec::with_capacity_in(procedures.len() * 2, env.arena); + let mut host_to_app_map = Vec::with_capacity_in(env.exposed_to_host.len(), env.arena); + let mut maybe_main_fn_index = None; + + // Adjust Wasm function indices to account for functions from the object file + let fn_index_offset: u32 = + host_module.import.function_count() as u32 + host_module.code.function_count; + + // Pre-pass over the procedure names & layouts + // Create a lookup to tell us the final index of each proc in the output file + for (i, ((sym, proc_layout), proc)) in procedures.into_iter().enumerate() { + let fn_index = fn_index_offset + i as u32; + procs.push(proc); + if env.exposed_to_host.contains(&sym) { + maybe_main_fn_index = Some(fn_index); + + let exposed_name = layout_ids + .get_toplevel(sym, &proc_layout) + .to_exposed_symbol_string(sym, interns); + + let exposed_name_bump: &'a str = env.arena.alloc_str(&exposed_name); + + host_to_app_map.push((exposed_name_bump, fn_index)); + } + + proc_lookup.push(ProcLookupData { + name: sym, + layout: proc_layout, + source: ProcSource::Roc, + }); + } + + let mut backend = WasmBackend::new( + env, + layout_interner, + interns, + layout_ids, + proc_lookup, + host_to_app_map, + host_module, + fn_index_offset, + CodeGenHelp::new(env.arena, TargetInfo::default_wasm32(), env.module_id), + ); + + if DEBUG_SETTINGS.user_procs_ir { + println!("## procs"); + for proc in procs.iter() { + println!("{}", proc.to_pretty(backend.layout_interner, 200, true)); + // println!("{:?}", proc); + } + } + + // Generate procs from user code + for proc in procs.iter() { + backend.build_proc(proc); + } + + // Generate specialized helpers for refcounting & equality + let helper_procs = backend.get_helpers(); + + backend.register_symbol_debug_names(); + + if DEBUG_SETTINGS.helper_procs_ir { + println!("## helper_procs"); + for proc in helper_procs.iter() { + println!("{}", proc.to_pretty(backend.layout_interner, 200, true)); + // println!("{:#?}", proc); + } + } + + // Generate Wasm for helpers and Zig/Roc wrappers + let sources = Vec::from_iter_in( + backend + .proc_lookup + .iter() + .map(|ProcLookupData { source, .. }| *source), + env.arena, + ); + let mut helper_iter = helper_procs.iter(); + for (idx, source) in sources.iter().enumerate() { + use ProcSource::*; + match source { + Roc => { /* already generated */ } + Helper => backend.build_proc(helper_iter.next().unwrap()), + HigherOrderMapper(inner_idx) => backend.build_higher_order_mapper(idx, *inner_idx), + HigherOrderCompare(inner_idx) => backend.build_higher_order_compare(idx, *inner_idx), + } + } + + let (module, called_fns) = backend.finalize(); + let main_function_index = + maybe_main_fn_index.expect("The app must expose at least one value to the host"); + + (module, called_fns, main_function_index) +} + +pub struct CopyMemoryConfig { + from_ptr: LocalId, + from_offset: u32, + to_ptr: LocalId, + to_offset: u32, + size: u32, + alignment_bytes: u32, +} + +pub fn copy_memory(code_builder: &mut CodeBuilder, config: CopyMemoryConfig) { + if config.from_ptr == config.to_ptr && config.from_offset == config.to_offset { + return; + } + if config.size == 0 { + return; + } + + let alignment = Align::from(config.alignment_bytes); + let mut i = 0; + while config.size - i >= 8 { + code_builder.get_local(config.to_ptr); + code_builder.get_local(config.from_ptr); + code_builder.i64_load(alignment, i + config.from_offset); + code_builder.i64_store(alignment, i + config.to_offset); + i += 8; + } + if config.size - i >= 4 { + code_builder.get_local(config.to_ptr); + code_builder.get_local(config.from_ptr); + code_builder.i32_load(alignment, i + config.from_offset); + code_builder.i32_store(alignment, i + config.to_offset); + i += 4; + } + while config.size - i > 0 { + code_builder.get_local(config.to_ptr); + code_builder.get_local(config.from_ptr); + code_builder.i32_load8_u(alignment, i + config.from_offset); + code_builder.i32_store8(alignment, i + config.to_offset); + i += 1; + } +} + +pub struct WasmDebugSettings { + proc_start_end: bool, + user_procs_ir: bool, + helper_procs_ir: bool, + let_stmt_ir: bool, + instructions: bool, + storage_map: bool, + pub keep_test_binary: bool, +} + +pub const DEBUG_SETTINGS: WasmDebugSettings = WasmDebugSettings { + proc_start_end: false && cfg!(debug_assertions), + user_procs_ir: false && cfg!(debug_assertions), // Note: we also have `ROC_PRINT_IR_AFTER_REFCOUNT=1 cargo test-gen-wasm` + helper_procs_ir: false && cfg!(debug_assertions), + let_stmt_ir: false && cfg!(debug_assertions), + instructions: false && cfg!(debug_assertions), + storage_map: false && cfg!(debug_assertions), + keep_test_binary: false && cfg!(debug_assertions), +}; + +#[cfg(test)] +mod dummy_platform_functions { + // `cargo test` produces an executable. At least on Windows, this means that extern symbols must be defined. This crate imports roc_std which + // defines a bunch of externs, and uses the three below. We provide dummy implementations because these functions are not called. + use core::ffi::c_void; + + /// # Safety + /// This is only marked unsafe to typecheck without warnings in the rest of the code here. + #[no_mangle] + pub unsafe extern "C" fn roc_alloc(_size: usize, _alignment: u32) -> *mut c_void { + unimplemented!("It is not valid to call roc alloc from within the compiler. Please use the \"platform\" feature if this is a platform.") + } + + /// # Safety + /// This is only marked unsafe to typecheck without warnings in the rest of the code here. + #[no_mangle] + pub unsafe extern "C" fn roc_realloc( + _ptr: *mut c_void, + _new_size: usize, + _old_size: usize, + _alignment: u32, + ) -> *mut c_void { + unimplemented!("It is not valid to call roc realloc from within the compiler. Please use the \"platform\" feature if this is a platform.") + } + + /// # Safety + /// This is only marked unsafe to typecheck without warnings in the rest of the code here. + #[no_mangle] + pub unsafe extern "C" fn roc_dealloc(_ptr: *mut c_void, _alignment: u32) { + unimplemented!("It is not valid to call roc dealloc from within the compiler. Please use the \"platform\" feature if this is a platform.") + } + + #[no_mangle] + pub unsafe extern "C" fn roc_panic(_c_ptr: *mut c_void, _tag_id: u32) { + unimplemented!("It is not valid to call roc panic from within the compiler. Please use the \"platform\" feature if this is a platform.") + } + + #[no_mangle] + pub unsafe extern "C" fn roc_dbg(_loc: *mut c_void, _msg: *mut c_void) { + unimplemented!("It is not valid to call roc dbg from within the compiler. Please use the \"platform\" feature if this is a platform.") + } + + #[no_mangle] + pub fn roc_memset(_dst: *mut c_void, _c: i32, _n: usize) -> *mut c_void { + unimplemented!("It is not valid to call roc memset from within the compiler. Please use the \"platform\" feature if this is a platform.") + } +} diff --git a/crates/compiler/gen_wasm/src/low_level.rs b/crates/compiler/gen_wasm/src/low_level.rs new file mode 100644 index 0000000000..cb66ab10c2 --- /dev/null +++ b/crates/compiler/gen_wasm/src/low_level.rs @@ -0,0 +1,2806 @@ +use bumpalo::collections::Vec; +use bumpalo::Bump; +use roc_builtins::bitcode::{self, FloatWidth, IntWidth}; +use roc_error_macros::{internal_error, todo_lambda_erasure}; +use roc_module::low_level::LowLevel; +use roc_module::symbol::Symbol; +use roc_mono::code_gen_help::HelperOp; +use roc_mono::ir::{HigherOrderLowLevel, PassedFunction, ProcLayout}; +use roc_mono::layout::{Builtin, InLayout, Layout, LayoutInterner, LayoutRepr, UnionLayout}; +use roc_mono::low_level::HigherOrder; + +use crate::backend::{ProcLookupData, ProcSource, WasmBackend}; +use crate::layout::{StackMemoryFormat, WasmLayout}; +use crate::storage::{AddressValue, StackMemoryLocation, StoredValue}; +use crate::PTR_TYPE; +use roc_wasm_module::{Align, LocalId, ValueType}; + +/// Number types used for Wasm code gen +/// Unlike other enums, this contains no details about layout or storage. +/// Its purpose is to help simplify the arms of the main lowlevel `match` below. +/// +/// Note: Wasm I32 is used for Roc I8, I16, I32, U8, U16, and U32, since it's +/// the smallest integer supported in the Wasm instruction set. +/// We may choose different instructions for signed and unsigned integers, +/// but they share the same Wasm value type. +#[derive(Clone, Copy, Debug, PartialEq)] +enum CodeGenNumType { + I32, // Supported in Wasm instruction set + I64, // Supported in Wasm instruction set + F32, // Supported in Wasm instruction set + F64, // Supported in Wasm instruction set + I128, // Bytes in memory, needs Zig builtins + Decimal, // Bytes in memory, needs Zig builtins +} + +impl CodeGenNumType { + pub fn for_symbol(backend: &WasmBackend<'_, '_>, symbol: Symbol) -> Self { + Self::from(backend.storage.get(&symbol)) + } +} + +const UPDATE_MODE_IMMUTABLE: i32 = 0; + +impl From> for CodeGenNumType { + fn from(layout: InLayout<'_>) -> CodeGenNumType { + use CodeGenNumType::*; + + let not_num_error = + || internal_error!("Tried to perform a Num low-level operation on {:?}", layout); + match layout { + Layout::BOOL => I32, + Layout::U8 => I32, + Layout::U16 => I32, + Layout::U32 => I32, + Layout::U64 => I64, + Layout::U128 => I128, + Layout::I8 => I32, + Layout::I16 => I32, + Layout::I32 => I32, + Layout::I64 => I64, + Layout::I128 => I128, + Layout::F32 => F32, + Layout::F64 => F64, + Layout::DEC => Decimal, + _ => not_num_error(), + } + } +} + +impl From for CodeGenNumType { + fn from(value_type: ValueType) -> CodeGenNumType { + match value_type { + ValueType::I32 => CodeGenNumType::I32, + ValueType::I64 => CodeGenNumType::I64, + ValueType::F32 => CodeGenNumType::F32, + ValueType::F64 => CodeGenNumType::F64, + } + } +} + +impl From for CodeGenNumType { + fn from(format: StackMemoryFormat) -> CodeGenNumType { + match format { + StackMemoryFormat::Int128 => CodeGenNumType::I128, + StackMemoryFormat::Decimal => CodeGenNumType::Decimal, + StackMemoryFormat::DataStructure => { + internal_error!("Tried to perform a Num low-level operation on a data structure") + } + } + } +} + +impl From for CodeGenNumType { + fn from(wasm_layout: WasmLayout) -> CodeGenNumType { + match wasm_layout { + WasmLayout::Primitive(value_type, _) => CodeGenNumType::from(value_type), + WasmLayout::StackMemory { format, .. } => CodeGenNumType::from(format), + } + } +} + +impl From<&StoredValue> for CodeGenNumType { + fn from(stored: &StoredValue) -> CodeGenNumType { + use StoredValue::*; + match stored { + Local { value_type, .. } => CodeGenNumType::from(*value_type), + StackMemory { format, .. } => CodeGenNumType::from(*format), + } + } +} + +fn layout_is_signed_int(layout: InLayout) -> bool { + matches!( + layout, + Layout::I8 | Layout::I16 | Layout::I32 | Layout::I64 | Layout::I128 + ) +} + +fn symbol_is_signed_int(backend: &WasmBackend<'_, '_>, symbol: Symbol) -> bool { + layout_is_signed_int(backend.storage.symbol_layouts[&symbol]) +} + +pub struct LowLevelCall<'a> { + pub lowlevel: LowLevel, + pub arguments: &'a [Symbol], + pub ret_symbol: Symbol, + pub ret_layout: InLayout<'a>, + pub ret_layout_raw: LayoutRepr<'a>, + pub ret_storage: StoredValue, +} + +impl<'a> LowLevelCall<'a> { + /// Load symbol values for a Zig call or numerical operation + /// For numerical ops, this just pushes the arguments to the Wasm VM's value stack + /// It implements the calling convention used by Zig for both numbers and structs + /// Result is the type signature of the call + fn load_args(&self, backend: &mut WasmBackend<'a, '_>) { + backend.storage.load_symbols_for_call( + &mut backend.code_builder, + self.arguments, + self.ret_symbol, + &WasmLayout::new(backend.layout_interner, self.ret_layout), + ); + } + + fn load_args_and_call_zig(&self, backend: &mut WasmBackend<'a, '_>, name: &'a str) { + self.load_args(backend); + backend.call_host_fn_after_loading_args(name); + } + + /// Wrap an integer that should have less than 32 bits, but is represented in Wasm as i32. + /// This may seem like deliberately introducing an error! + /// But we want all targets to behave the same, and hash algos rely on wrapping. + /// Discussion: https://github.com/roc-lang/roc/pull/2117#discussion_r760723063 + fn wrap_small_int(&self, backend: &mut WasmBackend<'a, '_>, int_width: IntWidth) { + let bits = 8 * int_width.stack_size() as i32; + let shift = 32 - bits; + if shift <= 0 { + return; + } + + backend.code_builder.i32_const(shift); + backend.code_builder.i32_shl(); + backend.code_builder.i32_const(shift); + if int_width.is_signed() { + backend.code_builder.i32_shr_s(); + } else { + backend.code_builder.i32_shr_u(); + } + } + + /// Main entrypoint from WasmBackend + pub fn generate(&self, backend: &mut WasmBackend<'a, '_>) { + use CodeGenNumType::*; + use LowLevel::*; + + let panic_ret_type = || { + internal_error!( + "Invalid return layout for {:?}: {:?}", + self.lowlevel, + self.ret_layout + ) + }; + + match self.lowlevel { + // Str + StrConcat => self.load_args_and_call_zig(backend, bitcode::STR_CONCAT), + StrToScalars => self.load_args_and_call_zig(backend, bitcode::STR_TO_SCALARS), + StrGetUnsafe => self.load_args_and_call_zig(backend, bitcode::STR_GET_UNSAFE), + StrJoinWith => self.load_args_and_call_zig(backend, bitcode::STR_JOIN_WITH), + StrIsEmpty => match backend.storage.get(&self.arguments[0]) { + StoredValue::StackMemory { location, .. } => { + let (local_id, offset) = + location.local_and_offset(backend.storage.stack_frame_pointer); + backend.code_builder.get_local(local_id); + backend.code_builder.i32_load8_u(Align::Bytes1, offset + 11); + backend.code_builder.i32_const(0x80); + backend.code_builder.i32_eq(); + } + _ => internal_error!("invalid storage for Str"), + }, + StrStartsWith => self.load_args_and_call_zig(backend, bitcode::STR_STARTS_WITH), + StrStartsWithScalar => { + self.load_args_and_call_zig(backend, bitcode::STR_STARTS_WITH_SCALAR) + } + StrEndsWith => self.load_args_and_call_zig(backend, bitcode::STR_ENDS_WITH), + StrSplit => self.load_args_and_call_zig(backend, bitcode::STR_SPLIT), + StrCountGraphemes => { + self.load_args_and_call_zig(backend, bitcode::STR_COUNT_GRAPEHEME_CLUSTERS) + } + StrCountUtf8Bytes => { + self.load_args_and_call_zig(backend, bitcode::STR_COUNT_UTF8_BYTES) + } + StrGetCapacity => self.load_args_and_call_zig(backend, bitcode::STR_CAPACITY), + StrToNum => { + let number_layout = match backend.layout_interner.get_repr(self.ret_layout) { + LayoutRepr::Struct(field_layouts) => field_layouts[0], + _ => { + internal_error!("Unexpected mono layout {:?} for StrToNum", self.ret_layout) + } + }; + // match on the return layout to figure out which zig builtin we need + let intrinsic = match backend.layout_interner.get_repr(number_layout) { + LayoutRepr::Builtin(Builtin::Int(int_width)) => &bitcode::STR_TO_INT[int_width], + LayoutRepr::Builtin(Builtin::Float(float_width)) => { + &bitcode::STR_TO_FLOAT[float_width] + } + LayoutRepr::Builtin(Builtin::Decimal) => bitcode::DEC_FROM_STR, + rest => internal_error!("Unexpected layout {:?} for StrToNum", rest), + }; + + self.load_args_and_call_zig(backend, intrinsic); + } + StrFromInt => self.num_to_str(backend), + StrFromFloat => self.num_to_str(backend), + StrFromUtf8Range => { + /* + Low-level op returns a struct with all the data for both Ok and Err. + Roc AST wrapper converts this to a tag union, with app-dependent tag IDs. + + output: *FromUtf8Result i32 + arg: RocList i32 + start i32 + count i32 + update_mode: UpdateMode i32 + */ + + // loads arg, start, count + backend.storage.load_symbols_for_call( + &mut backend.code_builder, + self.arguments, + self.ret_symbol, + &WasmLayout::new(backend.layout_interner, self.ret_layout), + ); + backend.code_builder.i32_const(UPDATE_MODE_IMMUTABLE); + backend.call_host_fn_after_loading_args(bitcode::STR_FROM_UTF8_RANGE); + } + StrTrimStart => self.load_args_and_call_zig(backend, bitcode::STR_TRIM_START), + StrTrimEnd => self.load_args_and_call_zig(backend, bitcode::STR_TRIM_END), + StrToUtf8 => self.load_args_and_call_zig(backend, bitcode::STR_TO_UTF8), + StrReserve => self.load_args_and_call_zig(backend, bitcode::STR_RESERVE), + StrReleaseExcessCapacity => { + self.load_args_and_call_zig(backend, bitcode::STR_RELEASE_EXCESS_CAPACITY) + } + StrRepeat => self.load_args_and_call_zig(backend, bitcode::STR_REPEAT), + StrAppendScalar => self.load_args_and_call_zig(backend, bitcode::STR_APPEND_SCALAR), + StrTrim => self.load_args_and_call_zig(backend, bitcode::STR_TRIM), + StrGetScalarUnsafe => { + self.load_args_and_call_zig(backend, bitcode::STR_GET_SCALAR_UNSAFE) + } + StrSubstringUnsafe => { + self.load_args_and_call_zig(backend, bitcode::STR_SUBSTRING_UNSAFE) + } + StrWithCapacity => self.load_args_and_call_zig(backend, bitcode::STR_WITH_CAPACITY), + StrGraphemes => self.load_args_and_call_zig(backend, bitcode::STR_GRAPHEMES), + + // List + ListLen => match backend.storage.get(&self.arguments[0]) { + StoredValue::StackMemory { location, .. } => { + let (local_id, offset) = + location.local_and_offset(backend.storage.stack_frame_pointer); + backend.code_builder.get_local(local_id); + // List is stored as (pointer, length, capacity), + // with each of those fields being 4 bytes on wasm. + // So the length is 4 bytes after the start of the struct. + // + // WRAPPER_LEN represents the index of the length field + // (which is 1 as of the writing of this comment). If the field order + // ever changes, WRAPPER_LEN should be updated and this logic should + // continue to work even though this comment may become inaccurate. + backend + .code_builder + .i32_load(Align::Bytes4, offset + (4 * Builtin::WRAPPER_LEN)); + } + _ => internal_error!("invalid storage for List"), + }, + + ListGetCapacity => self.load_args_and_call_zig(backend, bitcode::LIST_CAPACITY), + + ListIsUnique => self.load_args_and_call_zig(backend, bitcode::LIST_IS_UNIQUE), + + ListMap | ListMap2 | ListMap3 | ListMap4 | ListSortWith => { + internal_error!("HigherOrder lowlevels should not be handled here") + } + + ListGetUnsafe => { + let list: Symbol = self.arguments[0]; + let index: Symbol = self.arguments[1]; + + // Calculate byte offset in list + backend + .storage + .load_symbols(&mut backend.code_builder, &[index]); + let elem_size = backend.layout_interner.stack_size(self.ret_layout); + backend.code_builder.i32_const(elem_size as i32); + backend.code_builder.i32_mul(); // index*size + + // Calculate base heap pointer + if let StoredValue::StackMemory { location, .. } = backend.storage.get(&list) { + let (fp, offset) = + location.local_and_offset(backend.storage.stack_frame_pointer); + backend.code_builder.get_local(fp); + backend.code_builder.i32_load(Align::Bytes4, offset); + } else { + internal_error!("Lists are always stored in stack memory"); + } + + // Get pointer to target element and save it to a local var + backend.code_builder.i32_add(); // base + index*size + let elem_local = backend.storage.create_anonymous_local(PTR_TYPE); + backend.code_builder.set_local(elem_local); + + // Copy element value from heap to stack + backend.storage.copy_value_from_memory( + &mut backend.code_builder, + self.ret_symbol, + AddressValue::NotLoaded(elem_local), + 0, + ); + + // Increment refcount + if self.ret_layout_raw.is_refcounted(backend.layout_interner) { + let inc_fn = backend.get_refcount_fn_index(self.ret_layout, HelperOp::Inc); + backend.code_builder.get_local(elem_local); + backend.code_builder.i32_const(1); + backend.code_builder.call(inc_fn); + } + } + ListReplaceUnsafe => { + // List.replace_unsafe : List elem, Nat, elem -> { list: List elem, value: elem } + + let list: Symbol = self.arguments[0]; + let index: Symbol = self.arguments[1]; + let new_elem: Symbol = self.arguments[2]; + + // Find the return struct in the stack frame + let (ret_local, ret_offset) = match &self.ret_storage { + StoredValue::StackMemory { location, .. } => { + location.local_and_offset(backend.storage.stack_frame_pointer) + } + _ => internal_error!("Invalid return value storage for ListReplaceUnsafe"), + }; + + // Byte offsets of each field in the return struct + let (ret_list_offset, ret_elem_offset, elem_layout) = match self.ret_layout_raw { + LayoutRepr::Struct(&[f1, f2]) => { + let l1 = backend.layout_interner.get_repr(f1); + let l2 = backend.layout_interner.get_repr(f2); + match (l1, l2) { + (LayoutRepr::Builtin(Builtin::List(list_elem)), _) + if l2 == backend.layout_interner.get_repr(list_elem) => + { + let list_offset = 0; + let elem_offset = LayoutRepr::Builtin(Builtin::List(list_elem)) + .stack_size(backend.layout_interner); + (list_offset, elem_offset, f2) + } + (_, LayoutRepr::Builtin(Builtin::List(list_elem))) + if l1 == backend.layout_interner.get_repr(list_elem) => + { + let list_offset = l1.stack_size(backend.layout_interner); + let elem_offset = 0; + (list_offset, elem_offset, f1) + } + _ => internal_error!("Invalid return layout for ListReplaceUnsafe"), + } + } + _ => internal_error!("Invalid return layout for ListReplaceUnsafe"), + }; + + let (elem_width, elem_alignment) = backend + .layout_interner + .stack_size_and_alignment(elem_layout); + + // Ensure the new element is stored in memory so we can pass a pointer to Zig + let (new_elem_local, new_elem_offset, _) = + ensure_symbol_is_in_memory(backend, new_elem, elem_layout, backend.env.arena); + + // Load all the arguments for Zig + // (List return pointer) i32 + // list: RocList, i32 + // alignment: u32, i32 + // index: usize, i32 + // element: Opaque, i32 + // element_width: usize, i32 + // out_element: ?[*]u8, i32 + + let code_builder = &mut backend.code_builder; + + code_builder.get_local(ret_local); + if (ret_offset + ret_list_offset) > 0 { + code_builder.i32_const((ret_offset + ret_list_offset) as i32); + code_builder.i32_add(); + } + + backend.storage.load_symbols(code_builder, &[list]); + code_builder.i32_const(elem_alignment as i32); + backend.storage.load_symbols(code_builder, &[index]); + + code_builder.get_local(new_elem_local); + if new_elem_offset > 0 { + code_builder.i32_const(new_elem_offset as i32); + code_builder.i32_add(); + } + + code_builder.i32_const(elem_width as i32); + + code_builder.get_local(ret_local); + if (ret_offset + ret_elem_offset) > 0 { + code_builder.i32_const((ret_offset + ret_elem_offset) as i32); + code_builder.i32_add(); + } + + // There is an in-place version of this but we don't use it for dev backends. No morphic_lib analysis. + backend.call_host_fn_after_loading_args(bitcode::LIST_REPLACE); + } + ListWithCapacity => { + // List.withCapacity : Nat -> List elem + + let capacity: Symbol = self.arguments[0]; + let elem_layout = unwrap_list_elem_layout(self.ret_layout_raw); + let elem_layout = backend.layout_interner.get_repr(elem_layout); + let (elem_width, elem_align) = + elem_layout.stack_size_and_alignment(backend.layout_interner); + + // Zig arguments Wasm types + // (return pointer) i32 + // capacity: usize i32 + // alignment: u32 i32 + // element_width: usize i32 + + backend + .storage + .load_symbols(&mut backend.code_builder, &[self.ret_symbol, capacity]); + backend.code_builder.i32_const(elem_align as i32); + backend.code_builder.i32_const(elem_width as i32); + + backend.call_host_fn_after_loading_args(bitcode::LIST_WITH_CAPACITY); + } + ListConcat => { + // List.concat : List elem, List elem -> List elem + // Zig arguments Wasm types + // (return pointer) i32 + // list_a: RocList i32 + // list_b: RocList i32 + // alignment: u32 i32 + // element_width: usize i32 + + // Load the arguments that have symbols + backend.storage.load_symbols_for_call( + &mut backend.code_builder, + self.arguments, + self.ret_symbol, + &WasmLayout::new(backend.layout_interner, self.ret_layout), + ); + + // Load monomorphization constants + let elem_layout = unwrap_list_elem_layout(self.ret_layout_raw); + let elem_layout = backend.layout_interner.get_repr(elem_layout); + let (elem_width, elem_align) = + elem_layout.stack_size_and_alignment(backend.layout_interner); + backend.code_builder.i32_const(elem_align as i32); + backend.code_builder.i32_const(elem_width as i32); + + backend.call_host_fn_after_loading_args(bitcode::LIST_CONCAT); + } + + ListReserve => { + // List.reserve : List elem, Nat -> List elem + + let list: Symbol = self.arguments[0]; + let spare: Symbol = self.arguments[1]; + + let elem_layout = unwrap_list_elem_layout(self.ret_layout_raw); + let elem_layout = backend.layout_interner.get_repr(elem_layout); + let (elem_width, elem_align) = + elem_layout.stack_size_and_alignment(backend.layout_interner); + + // Zig arguments Wasm types + // (return pointer) i32 + // list: RocList i32 + // alignment: u32 i32 + // spare: usize i32 + // element_width: usize i32 + // update_mode: UpdateMode i32 + + // return pointer and list + backend.storage.load_symbols_for_call( + &mut backend.code_builder, + &[list], + self.ret_symbol, + &WasmLayout::new(backend.layout_interner, self.ret_layout), + ); + + backend.code_builder.i32_const(elem_align as i32); + + backend + .storage + .load_symbols(&mut backend.code_builder, &[spare]); + + backend.code_builder.i32_const(elem_width as i32); + + backend.code_builder.i32_const(UPDATE_MODE_IMMUTABLE); + + backend.call_host_fn_after_loading_args(bitcode::LIST_RESERVE); + } + + ListReleaseExcessCapacity => { + // List.releaseExcessCapacity : List elem -> List elem + + let list: Symbol = self.arguments[0]; + + let elem_layout = unwrap_list_elem_layout(self.ret_layout_raw); + let elem_layout = backend.layout_interner.get_repr(elem_layout); + let (elem_width, elem_align) = + elem_layout.stack_size_and_alignment(backend.layout_interner); + + // Zig arguments Wasm types + // (return pointer) i32 + // list: RocList i32 + // alignment: u32 i32 + // element_width: usize i32 + // update_mode: UpdateMode i32 + + // return pointer and list + backend.storage.load_symbols_for_call( + &mut backend.code_builder, + &[list], + self.ret_symbol, + &WasmLayout::new(backend.layout_interner, self.ret_layout), + ); + + backend.code_builder.i32_const(elem_align as i32); + + backend.code_builder.i32_const(elem_width as i32); + + backend.code_builder.i32_const(UPDATE_MODE_IMMUTABLE); + + backend.call_host_fn_after_loading_args(bitcode::LIST_RELEASE_EXCESS_CAPACITY); + } + + ListAppendUnsafe => { + // List.append : List elem, elem -> List elem + + let list: Symbol = self.arguments[0]; + let elem: Symbol = self.arguments[1]; + + let elem_layout = unwrap_list_elem_layout(self.ret_layout_raw); + let elem_width = backend.layout_interner.stack_size(elem_layout); + let (elem_local, elem_offset, _) = + ensure_symbol_is_in_memory(backend, elem, elem_layout, backend.env.arena); + + // Zig arguments Wasm types + // (return pointer) i32 + // list: RocList i32 + // element: Opaque i32 + // element_width: usize i32 + + // return pointer and list + backend.storage.load_symbols_for_call( + &mut backend.code_builder, + &[list], + self.ret_symbol, + &WasmLayout::new(backend.layout_interner, self.ret_layout), + ); + + backend.code_builder.get_local(elem_local); + if elem_offset > 0 { + backend.code_builder.i32_const(elem_offset as i32); + backend.code_builder.i32_add(); + } + + backend.code_builder.i32_const(elem_width as i32); + + backend.call_host_fn_after_loading_args(bitcode::LIST_APPEND_UNSAFE); + } + ListPrepend => { + // List.prepend : List elem, elem -> List elem + + let list: Symbol = self.arguments[0]; + let elem: Symbol = self.arguments[1]; + + let elem_layout = unwrap_list_elem_layout(self.ret_layout_raw); + let (elem_width, elem_align) = backend + .layout_interner + .stack_size_and_alignment(elem_layout); + let (elem_local, elem_offset, _) = + ensure_symbol_is_in_memory(backend, elem, elem_layout, backend.env.arena); + + // Zig arguments Wasm types + // (return pointer) i32 + // list: RocList i32 + // alignment: u32 i32 + // element: Opaque i32 + // element_width: usize i32 + + // return pointer and list + backend.storage.load_symbols_for_call( + &mut backend.code_builder, + &[list], + self.ret_symbol, + &WasmLayout::new(backend.layout_interner, self.ret_layout), + ); + + backend.code_builder.i32_const(elem_align as i32); + + backend.code_builder.get_local(elem_local); + if elem_offset > 0 { + backend.code_builder.i32_const(elem_offset as i32); + backend.code_builder.i32_add(); + } + backend.code_builder.i32_const(elem_width as i32); + + backend.call_host_fn_after_loading_args(bitcode::LIST_PREPEND); + } + ListSublist => { + // As a low-level, record is destructured + // List.sublist : List elem, start : Nat, len : Nat -> List elem + + let list: Symbol = self.arguments[0]; + let start: Symbol = self.arguments[1]; + let len: Symbol = self.arguments[2]; + + let elem_layout = unwrap_list_elem_layout(self.ret_layout_raw); + let (elem_width, elem_align) = backend + .layout_interner + .stack_size_and_alignment(elem_layout); + + // The refcount function receives a pointer to an element in the list + // This is the same as a Struct containing the element + let in_memory_layout = + backend + .layout_interner + .insert_direct_no_semantic(LayoutRepr::Struct( + backend.env.arena.alloc([elem_layout]), + )); + let dec_fn = backend.get_refcount_fn_index(in_memory_layout, HelperOp::Dec); + let dec_fn_ptr = backend.get_fn_ptr(dec_fn); + + // Zig arguments Wasm types + // (return pointer) i32 + // list: RocList, i32 + // alignment: u32, i32 + // element_width: usize, i32 + // start: usize, i32 + // len: usize, i32 + // dec: Dec, i32 + + backend.storage.load_symbols_for_call( + &mut backend.code_builder, + &[list], + self.ret_symbol, + &WasmLayout::new(backend.layout_interner, self.ret_layout), + ); + + backend.code_builder.i32_const(elem_align as i32); + backend.code_builder.i32_const(elem_width as i32); + backend + .storage + .load_symbols(&mut backend.code_builder, &[start, len]); + backend.code_builder.i32_const(dec_fn_ptr); + + backend.call_host_fn_after_loading_args(bitcode::LIST_SUBLIST); + } + ListDropAt => { + // List.dropAt : List elem, Nat -> List elem + let list: Symbol = self.arguments[0]; + let drop_index: Symbol = self.arguments[1]; + + let elem_layout = unwrap_list_elem_layout(self.ret_layout_raw); + let (elem_width, elem_align) = backend + .layout_interner + .stack_size_and_alignment(elem_layout); + + // The refcount function receives a pointer to an element in the list + // This is the same as a Struct containing the element + let in_memory_layout = + backend + .layout_interner + .insert_direct_no_semantic(LayoutRepr::Struct( + backend.env.arena.alloc([elem_layout]), + )); + let dec_fn = backend.get_refcount_fn_index(in_memory_layout, HelperOp::Dec); + let dec_fn_ptr = backend.get_fn_ptr(dec_fn); + + // Zig arguments Wasm types + // (return pointer) i32 + // list: RocList, i32 + // element_width: usize, i32 + // alignment: u32, i32 + // drop_index: usize, i32 + // dec: Dec, i32 + + // Load the return pointer and the list + backend.storage.load_symbols_for_call( + &mut backend.code_builder, + &[list], + self.ret_symbol, + &WasmLayout::new(backend.layout_interner, self.ret_layout), + ); + + backend.code_builder.i32_const(elem_width as i32); + backend.code_builder.i32_const(elem_align as i32); + backend + .storage + .load_symbols(&mut backend.code_builder, &[drop_index]); + backend.code_builder.i32_const(dec_fn_ptr); + + backend.call_host_fn_after_loading_args(bitcode::LIST_DROP_AT); + } + ListSwap => { + // List.swap : List elem, Nat, Nat -> List elem + let list: Symbol = self.arguments[0]; + let index_1: Symbol = self.arguments[1]; + let index_2: Symbol = self.arguments[2]; + + let elem_layout = unwrap_list_elem_layout(self.ret_layout_raw); + let (elem_width, elem_align) = backend + .layout_interner + .stack_size_and_alignment(elem_layout); + + // Zig arguments Wasm types + // (return pointer) i32 + // list: RocList, i32 + // alignment: u32, i32 + // element_width: usize, i32 + // index_1: usize, i32 + // index_2: usize, i32 + // update_mode: UpdateMode, i32 + + // Load the return pointer and the list + backend.storage.load_symbols_for_call( + &mut backend.code_builder, + &[list], + self.ret_symbol, + &WasmLayout::new(backend.layout_interner, self.ret_layout), + ); + + backend.code_builder.i32_const(elem_align as i32); + backend.code_builder.i32_const(elem_width as i32); + backend + .storage + .load_symbols(&mut backend.code_builder, &[index_1, index_2]); + backend.code_builder.i32_const(UPDATE_MODE_IMMUTABLE); + + backend.call_host_fn_after_loading_args(bitcode::LIST_SWAP); + } + + // Num + NumAdd => match self.ret_layout_raw { + LayoutRepr::Builtin(Builtin::Int(width)) => { + self.load_args_and_call_zig(backend, &bitcode::NUM_ADD_OR_PANIC_INT[width]) + } + LayoutRepr::Builtin(Builtin::Float(width)) => match width { + FloatWidth::F32 => { + self.load_args(backend); + backend.code_builder.f32_add() + } + FloatWidth::F64 => { + self.load_args(backend); + backend.code_builder.f64_add() + } + }, + LayoutRepr::Builtin(Builtin::Decimal) => { + self.load_args_and_call_zig(backend, bitcode::DEC_ADD_OR_PANIC) + } + _ => panic_ret_type(), + }, + + NumAddWrap => match self.ret_layout_raw { + LayoutRepr::Builtin(Builtin::Int(width)) => match width { + IntWidth::I128 | IntWidth::U128 => { + // TODO: don't panic + self.load_args_and_call_zig(backend, &bitcode::NUM_ADD_OR_PANIC_INT[width]) + } + IntWidth::I64 | IntWidth::U64 => { + self.load_args(backend); + backend.code_builder.i64_add() + } + IntWidth::I32 | IntWidth::U32 => { + self.load_args(backend); + backend.code_builder.i32_add() + } + _ => { + self.load_args(backend); + backend.code_builder.i32_add(); + self.wrap_small_int(backend, width); + } + }, + LayoutRepr::Builtin(Builtin::Float(width)) => match width { + FloatWidth::F32 => { + self.load_args(backend); + backend.code_builder.f32_add() + } + FloatWidth::F64 => { + self.load_args(backend); + backend.code_builder.f64_add() + } + }, + LayoutRepr::Builtin(Builtin::Decimal) => { + // TODO: don't panic + self.load_args_and_call_zig(backend, bitcode::DEC_ADD_OR_PANIC) + } + _ => panic_ret_type(), + }, + + NumToStr => self.num_to_str(backend), + NumAddChecked => { + let arg_layout = backend.storage.symbol_layouts[&self.arguments[0]]; + match backend.layout_interner.get_repr(arg_layout) { + LayoutRepr::Builtin(Builtin::Int(width)) => { + self.load_args_and_call_zig(backend, &bitcode::NUM_ADD_CHECKED_INT[width]) + } + LayoutRepr::Builtin(Builtin::Float(width)) => { + self.load_args_and_call_zig(backend, &bitcode::NUM_ADD_CHECKED_FLOAT[width]) + } + LayoutRepr::Builtin(Builtin::Decimal) => { + self.load_args_and_call_zig(backend, bitcode::DEC_ADD_WITH_OVERFLOW) + } + x => internal_error!("NumAddChecked is not defined for {:?}", x), + } + } + NumAddSaturated => match self.ret_layout_raw { + LayoutRepr::Builtin(Builtin::Int(width)) => { + self.load_args_and_call_zig(backend, &bitcode::NUM_ADD_SATURATED_INT[width]) + } + LayoutRepr::Builtin(Builtin::Float(FloatWidth::F32)) => { + self.load_args(backend); + backend.code_builder.f32_add() + } + LayoutRepr::Builtin(Builtin::Float(FloatWidth::F64)) => { + self.load_args(backend); + backend.code_builder.f64_add() + } + LayoutRepr::Builtin(Builtin::Decimal) => { + self.load_args_and_call_zig(backend, bitcode::DEC_ADD_SATURATED) + } + _ => panic_ret_type(), + }, + + NumSub => match self.ret_layout_raw { + LayoutRepr::Builtin(Builtin::Int(width)) => { + self.load_args_and_call_zig(backend, &bitcode::NUM_SUB_OR_PANIC_INT[width]) + } + LayoutRepr::Builtin(Builtin::Float(width)) => match width { + FloatWidth::F32 => { + self.load_args(backend); + backend.code_builder.f32_sub() + } + FloatWidth::F64 => { + self.load_args(backend); + backend.code_builder.f64_sub() + } + }, + LayoutRepr::Builtin(Builtin::Decimal) => { + self.load_args_and_call_zig(backend, bitcode::DEC_SUB_OR_PANIC) + } + _ => panic_ret_type(), + }, + + NumSubWrap => match self.ret_layout_raw { + LayoutRepr::Builtin(Builtin::Int(width)) => match width { + IntWidth::I128 | IntWidth::U128 => { + // TODO: don't panic + self.load_args_and_call_zig(backend, &bitcode::NUM_SUB_OR_PANIC_INT[width]) + } + IntWidth::I64 | IntWidth::U64 => { + self.load_args(backend); + backend.code_builder.i64_sub() + } + IntWidth::I32 | IntWidth::U32 => { + self.load_args(backend); + backend.code_builder.i32_sub() + } + _ => { + self.load_args(backend); + backend.code_builder.i32_sub(); + self.wrap_small_int(backend, width); + } + }, + LayoutRepr::Builtin(Builtin::Float(width)) => match width { + FloatWidth::F32 => { + self.load_args(backend); + backend.code_builder.f32_sub() + } + FloatWidth::F64 => { + self.load_args(backend); + backend.code_builder.f64_sub() + } + }, + LayoutRepr::Builtin(Builtin::Decimal) => { + // TODO: don't panic + self.load_args_and_call_zig(backend, bitcode::DEC_SUB_OR_PANIC) + } + _ => panic_ret_type(), + }, + NumSubChecked => { + let arg_layout = backend.storage.symbol_layouts[&self.arguments[0]]; + match backend.layout_interner.get_repr(arg_layout) { + LayoutRepr::Builtin(Builtin::Int(width)) => { + self.load_args_and_call_zig(backend, &bitcode::NUM_SUB_CHECKED_INT[width]) + } + LayoutRepr::Builtin(Builtin::Float(width)) => { + self.load_args_and_call_zig(backend, &bitcode::NUM_SUB_CHECKED_FLOAT[width]) + } + LayoutRepr::Builtin(Builtin::Decimal) => { + self.load_args_and_call_zig(backend, bitcode::DEC_SUB_WITH_OVERFLOW) + } + x => internal_error!("NumSubChecked is not defined for {:?}", x), + } + } + NumSubSaturated => match self.ret_layout_raw { + LayoutRepr::Builtin(Builtin::Int(width)) => { + self.load_args_and_call_zig(backend, &bitcode::NUM_SUB_SATURATED_INT[width]) + } + LayoutRepr::Builtin(Builtin::Float(FloatWidth::F32)) => { + self.load_args(backend); + backend.code_builder.f32_sub() + } + LayoutRepr::Builtin(Builtin::Float(FloatWidth::F64)) => { + self.load_args(backend); + backend.code_builder.f64_sub() + } + LayoutRepr::Builtin(Builtin::Decimal) => { + self.load_args_and_call_zig(backend, bitcode::DEC_SUB_SATURATED) + } + _ => panic_ret_type(), + }, + + NumMul => match self.ret_layout_raw { + LayoutRepr::Builtin(Builtin::Int(width)) => { + self.load_args_and_call_zig(backend, &bitcode::NUM_MUL_OR_PANIC_INT[width]) + } + LayoutRepr::Builtin(Builtin::Float(width)) => match width { + FloatWidth::F32 => { + self.load_args(backend); + backend.code_builder.f32_mul() + } + FloatWidth::F64 => { + self.load_args(backend); + backend.code_builder.f64_mul() + } + }, + LayoutRepr::Builtin(Builtin::Decimal) => { + self.load_args_and_call_zig(backend, bitcode::DEC_MUL_OR_PANIC) + } + _ => panic_ret_type(), + }, + NumMulWrap => match self.ret_layout_raw { + LayoutRepr::Builtin(Builtin::Int(width)) => match width { + IntWidth::I128 | IntWidth::U128 => { + self.load_args_and_call_zig(backend, &bitcode::NUM_MUL_WRAP_INT[width]) + } + IntWidth::I64 | IntWidth::U64 => { + self.load_args(backend); + backend.code_builder.i64_mul() + } + IntWidth::I32 | IntWidth::U32 => { + self.load_args(backend); + backend.code_builder.i32_mul() + } + _ => { + self.load_args(backend); + backend.code_builder.i32_mul(); + self.wrap_small_int(backend, width); + } + }, + LayoutRepr::Builtin(Builtin::Float(width)) => match width { + FloatWidth::F32 => { + self.load_args(backend); + backend.code_builder.f32_mul() + } + FloatWidth::F64 => { + self.load_args(backend); + backend.code_builder.f64_mul() + } + }, + LayoutRepr::Builtin(Builtin::Decimal) => { + // TODO: don't panic + self.load_args_and_call_zig(backend, bitcode::DEC_MUL_OR_PANIC) + } + _ => panic_ret_type(), + }, + NumMulSaturated => match self.ret_layout_raw { + LayoutRepr::Builtin(Builtin::Int(width)) => { + self.load_args_and_call_zig(backend, &bitcode::NUM_MUL_SATURATED_INT[width]) + } + LayoutRepr::Builtin(Builtin::Float(FloatWidth::F32)) => { + self.load_args(backend); + backend.code_builder.f32_mul() + } + LayoutRepr::Builtin(Builtin::Float(FloatWidth::F64)) => { + self.load_args(backend); + backend.code_builder.f64_mul() + } + LayoutRepr::Builtin(Builtin::Decimal) => { + self.load_args_and_call_zig(backend, bitcode::DEC_MUL_SATURATED) + } + _ => panic_ret_type(), + }, + + NumMulChecked => { + let arg_layout = backend.storage.symbol_layouts[&self.arguments[0]]; + match backend.layout_interner.get_repr(arg_layout) { + LayoutRepr::Builtin(Builtin::Int(width)) => { + self.load_args_and_call_zig(backend, &bitcode::NUM_MUL_CHECKED_INT[width]) + } + LayoutRepr::Builtin(Builtin::Float(width)) => { + self.load_args_and_call_zig(backend, &bitcode::NUM_MUL_CHECKED_FLOAT[width]) + } + LayoutRepr::Builtin(Builtin::Decimal) => { + self.load_args_and_call_zig(backend, bitcode::DEC_MUL_WITH_OVERFLOW) + } + x => internal_error!("NumMulChecked is not defined for {:?}", x), + } + } + NumGt => { + self.load_args(backend); + match CodeGenNumType::for_symbol(backend, self.arguments[0]) { + I32 => { + if symbol_is_signed_int(backend, self.arguments[0]) { + backend.code_builder.i32_gt_s() + } else { + backend.code_builder.i32_gt_u() + } + } + I64 => { + if symbol_is_signed_int(backend, self.arguments[0]) { + backend.code_builder.i64_gt_s() + } else { + backend.code_builder.i64_gt_u() + } + } + F32 => backend.code_builder.f32_gt(), + F64 => backend.code_builder.f64_gt(), + I128 => { + let intrinsic = if symbol_is_signed_int(backend, self.arguments[0]) { + &bitcode::NUM_GREATER_THAN[IntWidth::I128] + } else { + &bitcode::NUM_GREATER_THAN[IntWidth::U128] + }; + + self.load_args_and_call_zig(backend, intrinsic); + } + Decimal => { + // same as i128 + self.load_args_and_call_zig( + backend, + &bitcode::NUM_GREATER_THAN[IntWidth::I128], + ); + } + } + } + NumGte => { + self.load_args(backend); + match CodeGenNumType::for_symbol(backend, self.arguments[0]) { + I32 => { + if symbol_is_signed_int(backend, self.arguments[0]) { + backend.code_builder.i32_ge_s() + } else { + backend.code_builder.i32_ge_u() + } + } + I64 => { + if symbol_is_signed_int(backend, self.arguments[0]) { + backend.code_builder.i64_ge_s() + } else { + backend.code_builder.i64_ge_u() + } + } + F32 => backend.code_builder.f32_ge(), + F64 => backend.code_builder.f64_ge(), + I128 => { + let intrinsic = if symbol_is_signed_int(backend, self.arguments[0]) { + &bitcode::NUM_GREATER_THAN_OR_EQUAL[IntWidth::I128] + } else { + &bitcode::NUM_GREATER_THAN_OR_EQUAL[IntWidth::U128] + }; + + self.load_args_and_call_zig(backend, intrinsic); + } + Decimal => { + // same as i128 + self.load_args_and_call_zig( + backend, + &bitcode::NUM_GREATER_THAN_OR_EQUAL[IntWidth::I128], + ); + } + } + } + NumLt => { + self.load_args(backend); + match CodeGenNumType::for_symbol(backend, self.arguments[0]) { + I32 => { + if symbol_is_signed_int(backend, self.arguments[0]) { + backend.code_builder.i32_lt_s() + } else { + backend.code_builder.i32_lt_u() + } + } + I64 => { + if symbol_is_signed_int(backend, self.arguments[0]) { + backend.code_builder.i64_lt_s() + } else { + backend.code_builder.i64_lt_u() + } + } + F32 => backend.code_builder.f32_lt(), + F64 => backend.code_builder.f64_lt(), + I128 => { + let intrinsic = if symbol_is_signed_int(backend, self.arguments[0]) { + &bitcode::NUM_LESS_THAN[IntWidth::I128] + } else { + &bitcode::NUM_LESS_THAN[IntWidth::U128] + }; + + self.load_args_and_call_zig(backend, intrinsic); + } + Decimal => { + // same as i128 + self.load_args_and_call_zig( + backend, + &bitcode::NUM_LESS_THAN[IntWidth::I128], + ); + } + } + } + NumLte => { + self.load_args(backend); + let layout = backend.storage.symbol_layouts[&self.arguments[0]]; + match CodeGenNumType::from(layout) { + I32 => { + if layout_is_signed_int(layout) { + backend.code_builder.i32_le_s() + } else { + backend.code_builder.i32_le_u() + } + } + I64 => { + if layout_is_signed_int(layout) { + backend.code_builder.i64_le_s() + } else { + backend.code_builder.i64_le_u() + } + } + F32 => backend.code_builder.f32_le(), + F64 => backend.code_builder.f64_le(), + I128 => { + let intrinsic = if symbol_is_signed_int(backend, self.arguments[0]) { + &bitcode::NUM_LESS_THAN_OR_EQUAL[IntWidth::I128] + } else { + &bitcode::NUM_LESS_THAN_OR_EQUAL[IntWidth::U128] + }; + + self.load_args_and_call_zig(backend, intrinsic); + } + Decimal => { + // same as i128 + self.load_args_and_call_zig( + backend, + &bitcode::NUM_LESS_THAN_OR_EQUAL[IntWidth::I128], + ); + } + } + } + NumCompare => { + let layout = backend.storage.symbol_layouts[&self.arguments[0]]; + let is_signed = layout_is_signed_int(layout); + + // This implements the expression: + // (x != y) as u8 + (x < y) as u8 + // For x==y: (false as u8) + (false as u8) = 0 = RocOrder::Eq + // For x>y: (true as u8) + (false as u8) = 1 = RocOrder::Gt + // For x { + self.load_args(backend); + backend.code_builder.i32_ne(); + self.load_args(backend); + if is_signed { + backend.code_builder.i32_lt_s() + } else { + backend.code_builder.i32_lt_u() + } + backend.code_builder.i32_add(); + } + I64 => { + self.load_args(backend); + backend.code_builder.i64_ne(); + self.load_args(backend); + if is_signed { + backend.code_builder.i64_lt_s() + } else { + backend.code_builder.i64_lt_u() + } + backend.code_builder.i32_add(); + } + F32 => { + self.load_args(backend); + backend.code_builder.f32_ne(); + self.load_args(backend); + backend.code_builder.f32_lt(); + backend.code_builder.i32_add(); + } + F64 => { + self.load_args(backend); + backend.code_builder.f64_ne(); + self.load_args(backend); + backend.code_builder.f64_lt(); + backend.code_builder.i32_add(); + } + I128 | Decimal => { + self.load_args(backend); + self.load_args_and_call_zig(backend, &bitcode::NUM_COMPARE[IntWidth::I128]); + } + } + } + NumDivFrac => { + self.load_args(backend); + match CodeGenNumType::for_symbol(backend, self.arguments[0]) { + F32 => backend.code_builder.f32_div(), + F64 => backend.code_builder.f64_div(), + Decimal => self.load_args_and_call_zig(backend, bitcode::DEC_DIV), + x => todo!("{:?} for {:?}", self.lowlevel, x), + } + } + NumDivTruncUnchecked => { + self.load_args(backend); + let is_signed = symbol_is_signed_int(backend, self.arguments[0]); + match CodeGenNumType::for_symbol(backend, self.arguments[0]) { + I32 => { + if is_signed { + backend.code_builder.i32_div_s() + } else { + backend.code_builder.i32_div_u() + } + } + I64 => { + if is_signed { + backend.code_builder.i64_div_s() + } else { + backend.code_builder.i64_div_u() + } + } + x => todo!("{:?} for {:?}", self.lowlevel, x), + } + } + NumDivCeilUnchecked => match self.ret_layout_raw { + LayoutRepr::Builtin(Builtin::Int(width)) => { + self.load_args_and_call_zig(backend, &bitcode::NUM_DIV_CEIL[width]) + } + _ => panic_ret_type(), + }, + + NumRemUnchecked => { + self.load_args(backend); + match CodeGenNumType::for_symbol(backend, self.arguments[0]) { + I32 => backend.code_builder.i32_rem_s(), + I64 => backend.code_builder.i64_rem_s(), + _ => todo!("{:?} for {:?}", self.lowlevel, self.ret_layout), + } + } + NumIsMultipleOf => { + // this builds the following construct + // if (rhs != 0 && rhs != -1) { + // let rem = lhs % rhs; + // rem == 0 + // } else { + // // lhs is a multiple of rhs iff + // // - rhs == -1 + // // - both rhs and lhs are 0 + // // the -1 case is important for overflow reasons `isize::MIN % -1` crashes in rust + // (lhs == 0) || (rhs == -1) + // } + let lhs = self.arguments[0]; + let rhs = self.arguments[1]; + let layout = backend.storage.symbol_layouts[&lhs]; + let is_signed = layout_is_signed_int(layout); + let code_builder = &mut backend.code_builder; + match CodeGenNumType::from(layout) { + I64 => { + let tmp = backend.storage.create_anonymous_local(ValueType::I32); + backend.storage.load_symbols(code_builder, &[rhs]); + code_builder.i64_const(0); + code_builder.i64_ne(); // rhs != 0 + + if is_signed { + backend.storage.load_symbols(code_builder, &[rhs]); + code_builder.i64_const(-1); + code_builder.i64_ne(); // rhs != -1 + code_builder.i32_and(); // rhs != 0 && rhs != -1 + } + code_builder.if_(); + { + backend.storage.load_symbols(code_builder, &[lhs, rhs]); + if is_signed { + code_builder.i64_rem_s(); + } else { + code_builder.i64_rem_u(); + } + code_builder.i64_eqz(); + code_builder.set_local(tmp); + } + code_builder.else_(); + { + backend.storage.load_symbols(code_builder, &[lhs]); + code_builder.i64_eqz(); // lhs == 0 + if is_signed { + backend.storage.load_symbols(code_builder, &[rhs]); + code_builder.i64_const(-1); + code_builder.i64_eq(); // rhs == -1 + code_builder.i32_or(); // (lhs == 0) || (rhs == -1) + } + code_builder.set_local(tmp); + } + code_builder.end(); + code_builder.get_local(tmp); + } + + I32 => { + let tmp = backend.storage.create_anonymous_local(ValueType::I32); + backend.storage.load_symbols(code_builder, &[rhs]); + code_builder.i32_const(0); + code_builder.i32_ne(); // rhs != 0 + + if is_signed { + backend.storage.load_symbols(code_builder, &[rhs]); + code_builder.i32_const(-1); + code_builder.i32_ne(); // rhs != -1 + code_builder.i32_and(); // rhs != 0 && rhs != -1 + } + code_builder.if_(); + { + backend.storage.load_symbols(code_builder, &[lhs, rhs]); + if is_signed { + code_builder.i32_rem_s(); + } else { + code_builder.i32_rem_u(); + } + code_builder.i32_eqz(); + code_builder.set_local(tmp); + } + code_builder.else_(); + { + backend.storage.load_symbols(code_builder, &[lhs]); + code_builder.i32_eqz(); // lhs == 0 + if is_signed { + backend.storage.load_symbols(code_builder, &[rhs]); + code_builder.i32_const(-1); + code_builder.i32_eq(); // rhs == -1 + code_builder.i32_or(); // (lhs == 0) || (rhs == -1) + } + code_builder.set_local(tmp); + } + code_builder.end(); + code_builder.get_local(tmp); + } + + _ => panic_ret_type(), + } + } + NumAbs => { + const PANIC_MSG: &str = + "integer absolute overflowed because its argument is the minimum value"; + + self.load_args(backend); + + match CodeGenNumType::from(self.ret_layout) { + I32 => { + if !layout_is_signed_int(self.ret_layout) { + return; + } + backend.code_builder.i32_const(i32::MIN); + backend.code_builder.i32_eq(); + backend.code_builder.if_(); + backend.stmt_internal_error(PANIC_MSG); + backend.code_builder.end(); + + // x + self.load_args(backend); + + // -x + backend.code_builder.i32_const(0); + self.load_args(backend); + backend.code_builder.i32_sub(); + + // x >= 0 + self.load_args(backend); + backend.code_builder.i32_const(0); + backend.code_builder.i32_ge_s(); + + // (x >= 0) ? x : -x + backend.code_builder.select(); + } + I64 => { + if !layout_is_signed_int(self.ret_layout) { + return; + } + backend.code_builder.i64_const(i64::MIN); + backend.code_builder.i64_eq(); + backend.code_builder.if_(); + backend.stmt_internal_error(PANIC_MSG); + backend.code_builder.end(); + + // x + self.load_args(backend); + + // -x + backend.code_builder.i64_const(0); + self.load_args(backend); + backend.code_builder.i64_sub(); + + // x >= 0 + self.load_args(backend); + backend.code_builder.i64_const(0); + backend.code_builder.i64_ge_s(); + + // (x >= 0) ? x : -x + backend.code_builder.select(); + } + F32 => backend.code_builder.f32_abs(), + F64 => backend.code_builder.f64_abs(), + _ => todo!("{:?} for {:?}", self.lowlevel, self.ret_layout), + } + } + NumNeg => { + const PANIC_MSG: &str = + "integer negation overflowed because its argument is the minimum value"; + + self.load_args(backend); + match CodeGenNumType::from(self.ret_layout) { + I32 => { + backend.code_builder.i32_const(i32::MIN); + backend.code_builder.i32_eq(); + backend.code_builder.if_(); + backend.stmt_internal_error(PANIC_MSG); + backend.code_builder.end(); + + backend.code_builder.i32_const(0); + self.load_args(backend); + backend.code_builder.i32_sub(); + } + I64 => { + backend.code_builder.i64_const(i64::MIN); + backend.code_builder.i64_eq(); + backend.code_builder.if_(); + backend.stmt_internal_error(PANIC_MSG); + backend.code_builder.end(); + + backend.code_builder.i64_const(0); + self.load_args(backend); + backend.code_builder.i64_sub(); + } + F32 => backend.code_builder.f32_neg(), + F64 => backend.code_builder.f64_neg(), + _ => todo!("{:?} for {:?}", self.lowlevel, self.ret_layout), + } + } + NumSin => match self.ret_layout_raw { + LayoutRepr::Builtin(Builtin::Float(width)) => { + self.load_args_and_call_zig(backend, &bitcode::NUM_SIN[width]); + } + LayoutRepr::Builtin(Builtin::Decimal) => { + self.load_args_and_call_zig(backend, bitcode::DEC_SIN); + } + _ => panic_ret_type(), + }, + NumCos => match self.ret_layout_raw { + LayoutRepr::Builtin(Builtin::Float(width)) => { + self.load_args_and_call_zig(backend, &bitcode::NUM_COS[width]); + } + LayoutRepr::Builtin(Builtin::Decimal) => { + self.load_args_and_call_zig(backend, bitcode::DEC_COS); + } + _ => panic_ret_type(), + }, + NumTan => match self.ret_layout_raw { + LayoutRepr::Builtin(Builtin::Float(width)) => { + self.load_args_and_call_zig(backend, &bitcode::NUM_TAN[width]); + } + LayoutRepr::Builtin(Builtin::Decimal) => { + self.load_args_and_call_zig(backend, bitcode::DEC_TAN); + } + _ => panic_ret_type(), + }, + NumSqrtUnchecked => { + self.load_args(backend); + match self.ret_layout_raw { + LayoutRepr::Builtin(Builtin::Float(FloatWidth::F32)) => { + backend.code_builder.f32_sqrt() + } + LayoutRepr::Builtin(Builtin::Float(FloatWidth::F64)) => { + backend.code_builder.f64_sqrt() + } + _ => panic_ret_type(), + } + } + NumLogUnchecked => match self.ret_layout_raw { + LayoutRepr::Builtin(Builtin::Float(width)) => { + self.load_args_and_call_zig(backend, &bitcode::NUM_LOG[width]); + } + _ => panic_ret_type(), + }, + NumToFrac => { + self.load_args(backend); + let ret_type = CodeGenNumType::from(self.ret_layout); + let arg_type = CodeGenNumType::for_symbol(backend, self.arguments[0]); + let arg_is_signed = symbol_is_signed_int(backend, self.arguments[0]); + match (ret_type, arg_type) { + (F32, I32) => backend.code_builder.f32_convert_s_i32(), + (F32, I64) => backend.code_builder.f32_convert_s_i64(), + (F32, F32) => {} + (F32, F64) => backend.code_builder.f32_demote_f64(), + + (F64, I32) => backend.code_builder.f64_convert_s_i32(), + (F64, I64) => backend.code_builder.f64_convert_s_i64(), + (F64, F32) => backend.code_builder.f64_promote_f32(), + (F64, F64) => {} + + (Decimal, I32) => { + let int_width = match arg_is_signed { + true => IntWidth::I32, + false => IntWidth::U32, + }; + + self.load_args_and_call_zig(backend, &bitcode::DEC_FROM_INT[int_width]); + } + (Decimal, I64) => { + let int_width = match arg_is_signed { + true => IntWidth::I64, + false => IntWidth::U64, + }; + + self.load_args_and_call_zig(backend, &bitcode::DEC_FROM_INT[int_width]); + } + (Decimal, F32) => { + self.load_args_and_call_zig( + backend, + &bitcode::DEC_FROM_FLOAT[FloatWidth::F32], + ); + } + (Decimal, F64) => { + self.load_args_and_call_zig( + backend, + &bitcode::DEC_FROM_FLOAT[FloatWidth::F64], + ); + } + (Decimal, Decimal) => {} + + _ => todo!("{:?}: {:?} -> {:?}", self.lowlevel, arg_type, ret_type), + } + } + NumPow => match self.ret_layout_raw { + LayoutRepr::Builtin(Builtin::Float(width)) => { + self.load_args_and_call_zig(backend, &bitcode::NUM_POW[width]); + } + _ => panic_ret_type(), + }, + + NumCountLeadingZeroBits => match backend + .layout_interner + .get_repr(backend.storage.symbol_layouts[&self.arguments[0]]) + { + LayoutRepr::Builtin(Builtin::Int(width)) => { + self.load_args_and_call_zig( + backend, + &bitcode::NUM_COUNT_LEADING_ZERO_BITS[width], + ); + } + _ => panic_ret_type(), + }, + NumCountTrailingZeroBits => match backend + .layout_interner + .get_repr(backend.storage.symbol_layouts[&self.arguments[0]]) + { + LayoutRepr::Builtin(Builtin::Int(width)) => { + self.load_args_and_call_zig( + backend, + &bitcode::NUM_COUNT_TRAILING_ZERO_BITS[width], + ); + } + _ => panic_ret_type(), + }, + NumCountOneBits => match backend + .layout_interner + .get_repr(backend.storage.symbol_layouts[&self.arguments[0]]) + { + LayoutRepr::Builtin(Builtin::Int(width)) => { + self.load_args_and_call_zig(backend, &bitcode::NUM_COUNT_ONE_BITS[width]); + } + _ => panic_ret_type(), + }, + NumRound => { + self.load_args(backend); + let arg_type = CodeGenNumType::for_symbol(backend, self.arguments[0]); + let ret_type = CodeGenNumType::from(self.ret_layout); + + let width = match ret_type { + CodeGenNumType::I32 => IntWidth::I32, + CodeGenNumType::I64 => IntWidth::I64, + CodeGenNumType::I128 => todo!("{:?} for I128", self.lowlevel), + _ => internal_error!("Invalid return type for round: {:?}", ret_type), + }; + + match arg_type { + F32 => self.load_args_and_call_zig(backend, &bitcode::NUM_ROUND_F32[width]), + F64 => self.load_args_and_call_zig(backend, &bitcode::NUM_ROUND_F64[width]), + _ => internal_error!("Invalid argument type for round: {:?}", arg_type), + } + } + NumCeiling | NumFloor => { + self.load_args(backend); + let arg_type = CodeGenNumType::for_symbol(backend, self.arguments[0]); + let ret_type = CodeGenNumType::from(self.ret_layout); + match (arg_type, self.lowlevel) { + (F32, NumCeiling) => { + backend.code_builder.f32_ceil(); + } + (F64, NumCeiling) => { + backend.code_builder.f64_ceil(); + } + (F32, NumFloor) => { + backend.code_builder.f32_floor(); + } + (F64, NumFloor) => { + backend.code_builder.f64_floor(); + } + _ => internal_error!("Invalid argument type for ceiling: {:?}", arg_type), + } + match (ret_type, arg_type) { + // TODO: unsigned truncation + (I32, F32) => backend.code_builder.i32_trunc_s_f32(), + (I32, F64) => backend.code_builder.i32_trunc_s_f64(), + (I64, F32) => backend.code_builder.i64_trunc_s_f32(), + (I64, F64) => backend.code_builder.i64_trunc_s_f64(), + (I128, _) => todo!("{:?} for I128", self.lowlevel), + _ => panic_ret_type(), + } + } + NumPowInt => { + self.load_args(backend); + let base_type = CodeGenNumType::for_symbol(backend, self.arguments[0]); + let exponent_type = CodeGenNumType::for_symbol(backend, self.arguments[1]); + let ret_type = CodeGenNumType::from(self.ret_layout); + + debug_assert!(base_type == exponent_type); + debug_assert!(exponent_type == ret_type); + + let width = match ret_type { + CodeGenNumType::I32 => IntWidth::I32, + CodeGenNumType::I64 => IntWidth::I64, + CodeGenNumType::I128 => todo!("{:?} for I128", self.lowlevel), + _ => internal_error!("Invalid return type for pow: {:?}", ret_type), + }; + + self.load_args_and_call_zig(backend, &bitcode::NUM_POW_INT[width]) + } + + NumIsNan => num_is_nan(backend, self.arguments[0]), + NumIsInfinite => num_is_infinite(backend, self.arguments[0]), + NumIsFinite => num_is_finite(backend, self.arguments[0]), + + NumAtan => match self.ret_layout_raw { + LayoutRepr::Builtin(Builtin::Float(width)) => { + self.load_args_and_call_zig(backend, &bitcode::NUM_ATAN[width]); + } + LayoutRepr::Builtin(Builtin::Decimal) => { + self.load_args_and_call_zig(backend, bitcode::DEC_ATAN); + } + _ => panic_ret_type(), + }, + NumAcos => match self.ret_layout_raw { + LayoutRepr::Builtin(Builtin::Float(width)) => { + self.load_args_and_call_zig(backend, &bitcode::NUM_ACOS[width]); + } + LayoutRepr::Builtin(Builtin::Decimal) => { + self.load_args_and_call_zig(backend, bitcode::DEC_ACOS); + } + _ => panic_ret_type(), + }, + NumAsin => match self.ret_layout_raw { + LayoutRepr::Builtin(Builtin::Float(width)) => { + self.load_args_and_call_zig(backend, &bitcode::NUM_ASIN[width]); + } + LayoutRepr::Builtin(Builtin::Decimal) => { + self.load_args_and_call_zig(backend, bitcode::DEC_ASIN); + } + _ => panic_ret_type(), + }, + NumBytesToU16 => self.load_args_and_call_zig(backend, bitcode::NUM_BYTES_TO_U16), + NumBytesToU32 => self.load_args_and_call_zig(backend, bitcode::NUM_BYTES_TO_U32), + NumBytesToU64 => self.load_args_and_call_zig(backend, bitcode::NUM_BYTES_TO_U64), + NumBytesToU128 => self.load_args_and_call_zig(backend, bitcode::NUM_BYTES_TO_U128), + NumBitwiseAnd => { + self.load_args(backend); + match CodeGenNumType::from(self.ret_layout) { + I32 => backend.code_builder.i32_and(), + I64 => backend.code_builder.i64_and(), + I128 => todo!("{:?} for I128", self.lowlevel), + _ => panic_ret_type(), + } + } + NumBitwiseXor => { + self.load_args(backend); + match CodeGenNumType::from(self.ret_layout) { + I32 => backend.code_builder.i32_xor(), + I64 => backend.code_builder.i64_xor(), + I128 => todo!("{:?} for I128", self.lowlevel), + _ => panic_ret_type(), + } + } + NumBitwiseOr => { + self.load_args(backend); + match CodeGenNumType::from(self.ret_layout) { + I32 => backend.code_builder.i32_or(), + I64 => backend.code_builder.i64_or(), + I128 => todo!("{:?} for I128", self.lowlevel), + _ => panic_ret_type(), + } + } + NumShiftLeftBy => { + let num = self.arguments[0]; + let bits = self.arguments[1]; + backend + .storage + .load_symbols(&mut backend.code_builder, &[num, bits]); + match CodeGenNumType::from(self.ret_layout) { + I32 => backend.code_builder.i32_shl(), + I64 => { + backend.code_builder.i64_extend_u_i32(); + backend.code_builder.i64_shl(); + } + I128 => todo!("{:?} for I128", self.lowlevel), + _ => panic_ret_type(), + } + } + NumShiftRightBy => { + let num = self.arguments[0]; + let bits = self.arguments[1]; + match CodeGenNumType::from(self.ret_layout) { + I32 => { + // In most languages this operation is for signed numbers, but Roc defines it on all integers. + // So the argument is implicitly converted to signed before the shift operator. + // We need to make that conversion explicit for i8 and i16, which use Wasm's i32 type. + let bit_width = + 8 * self.ret_layout_raw.stack_size(backend.layout_interner) as i32; + if bit_width < 32 && !symbol_is_signed_int(backend, num) { + // Sign-extend the number by shifting left and right again + backend + .storage + .load_symbols(&mut backend.code_builder, &[num]); + backend.code_builder.i32_const(32 - bit_width); + backend.code_builder.i32_shl(); + backend.code_builder.i32_const(32 - bit_width); + backend.code_builder.i32_shr_s(); + backend + .storage + .load_symbols(&mut backend.code_builder, &[bits]); + + // Do the actual bitshift operation + backend.code_builder.i32_shr_s(); + + // Restore to unsigned + backend.code_builder.i32_const((1 << bit_width) - 1); + backend.code_builder.i32_and(); + } else { + backend + .storage + .load_symbols(&mut backend.code_builder, &[num, bits]); + backend.code_builder.i32_shr_s(); + } + } + I64 => { + backend + .storage + .load_symbols(&mut backend.code_builder, &[num, bits]); + backend.code_builder.i64_extend_u_i32(); + backend.code_builder.i64_shr_s(); + } + I128 => todo!("{:?} for I128", self.lowlevel), + _ => panic_ret_type(), + } + } + NumShiftRightZfBy => { + let num = self.arguments[0]; + let bits = self.arguments[1]; + match CodeGenNumType::from(self.ret_layout) { + I32 => { + // In most languages this operation is for unsigned numbers, but Roc defines it on all integers. + // So the argument is implicitly converted to unsigned before the shift operator. + // We need to make that conversion explicit for i8 and i16, which use Wasm's i32 type. + let bit_width = 8 * self.ret_layout_raw.stack_size(backend.layout_interner); + if bit_width < 32 && symbol_is_signed_int(backend, num) { + let mask = (1 << bit_width) - 1; + + backend + .storage + .load_symbols(&mut backend.code_builder, &[num]); + + backend.code_builder.i32_const(mask); + backend.code_builder.i32_and(); + + backend + .storage + .load_symbols(&mut backend.code_builder, &[bits]); + } else { + backend + .storage + .load_symbols(&mut backend.code_builder, &[num, bits]); + } + + backend.code_builder.i32_shr_u(); + } + I64 => { + backend + .storage + .load_symbols(&mut backend.code_builder, &[num, bits]); + backend.code_builder.i64_extend_u_i32(); + backend.code_builder.i64_shr_u(); + } + I128 => self.load_args_and_call_zig(backend, "__lshrti3"), // from compiler_rt + _ => panic_ret_type(), + } + } + NumIntCast => { + let arg_layout = backend.storage.symbol_layouts[&self.arguments[0]]; + let arg_type = CodeGenNumType::from(arg_layout); + let arg_width = match backend.layout_interner.get_repr(arg_layout) { + LayoutRepr::Builtin(Builtin::Int(w)) => w, + LayoutRepr::Builtin(Builtin::Bool) => IntWidth::U8, + x => internal_error!("Num.intCast is not defined for {:?}", x), + }; + + let ret_type = CodeGenNumType::from(self.ret_layout); + let ret_width = match self.ret_layout_raw { + LayoutRepr::Builtin(Builtin::Int(w)) => w, + x => internal_error!("Num.intCast is not defined for {:?}", x), + }; + + match (ret_type, arg_type) { + (I32, I32) => { + self.load_args(backend); + self.wrap_small_int(backend, ret_width); + } + (I32, I64) => { + self.load_args(backend); + backend.code_builder.i32_wrap_i64(); + self.wrap_small_int(backend, ret_width); + } + (I32, I128) => { + self.load_args(backend); + backend.code_builder.i32_load(Align::Bytes4, 0); + } + (I64, I32) => { + self.load_args(backend); + if arg_width.is_signed() { + backend.code_builder.i64_extend_s_i32() + } else { + backend.code_builder.i64_extend_u_i32() + } + } + (I64, I64) => { + self.load_args(backend); + } + (I64, I128) => { + let (frame_ptr, offset) = match backend.storage.get(&self.arguments[0]) { + StoredValue::StackMemory { location, .. } => { + location.local_and_offset(backend.storage.stack_frame_pointer) + } + _ => internal_error!("I128 should be in stack memory"), + }; + backend.code_builder.get_local(frame_ptr); + backend.code_builder.i64_load(Align::Bytes8, offset); + } + (I128, I64) => { + // Symbols are loaded as if for a call, so the i128 "return address" and i64 value are on the value stack + self.load_args(backend); + backend.code_builder.i64_store(Align::Bytes8, 0); + + // Zero the most significant 64 bits + let (frame_ptr, offset) = match &self.ret_storage { + StoredValue::StackMemory { location, .. } => { + location.local_and_offset(backend.storage.stack_frame_pointer) + } + _ => internal_error!("I128 should be in stack memory"), + }; + backend.code_builder.get_local(frame_ptr); + backend.code_builder.i64_const(0); + backend.code_builder.i64_store(Align::Bytes8, offset + 8); + } + + _ => todo!("{:?}: {:?} -> {:?}", self.lowlevel, arg_type, ret_type), + } + } + NumToFloatCast => { + self.load_args(backend); + let arg_layout = backend.storage.symbol_layouts[&self.arguments[0]]; + let arg_signed = match backend.layout_interner.get_repr(arg_layout) { + LayoutRepr::Builtin(Builtin::Int(w)) => w.is_signed(), + LayoutRepr::Builtin(Builtin::Float(_)) => true, // unused + LayoutRepr::Builtin(Builtin::Decimal) => true, + x => internal_error!("Num.intCast is not defined for {:?}", x), + }; + let ret_type = CodeGenNumType::from(self.ret_layout); + let arg_type = CodeGenNumType::from(arg_layout); + + match (ret_type, arg_type) { + (F32, F32) => {} + (F32, F64) => backend.code_builder.f32_demote_f64(), + (F32, I32) => { + if arg_signed { + backend.code_builder.f32_convert_s_i32() + } else { + backend.code_builder.f32_convert_u_i32() + } + } + (F32, I64) => { + if arg_signed { + backend.code_builder.f32_convert_s_i64() + } else { + backend.code_builder.f32_convert_u_i64() + } + } + (F64, F64) => {} + (F64, I32) => { + if arg_signed { + backend.code_builder.f64_convert_s_i32() + } else { + backend.code_builder.f64_convert_u_i32() + } + } + (F64, I64) => { + if arg_signed { + backend.code_builder.f64_convert_s_i64() + } else { + backend.code_builder.f64_convert_u_i64() + } + } + _ => todo!("{:?}: {:?} -> {:?}", self.lowlevel, arg_type, ret_type), + } + } + NumToIntChecked => { + let arg_layout = backend.storage.symbol_layouts[&self.arguments[0]]; + + let (arg_width, ret_width) = match ( + backend.layout_interner.get_repr(arg_layout), + self.ret_layout_raw, + ) { + ( + LayoutRepr::Builtin(Builtin::Int(arg_width)), + LayoutRepr::Struct(&[ret, ..]), + ) => match backend.layout_interner.get_repr(ret) { + LayoutRepr::Builtin(Builtin::Int(ret_width)) => (arg_width, ret_width), + _ => { + internal_error!( + "NumToIntChecked is not defined for signature {:?} -> {:?}", + arg_layout, + self.ret_layout + ); + } + }, + _ => { + internal_error!( + "NumToIntChecked is not defined for signature {:?} -> {:?}", + arg_layout, + self.ret_layout + ); + } + }; + + if arg_width.is_signed() { + self.load_args_and_call_zig( + backend, + &bitcode::NUM_INT_TO_INT_CHECKING_MAX_AND_MIN[ret_width][arg_width], + ) + } else { + self.load_args_and_call_zig( + backend, + &bitcode::NUM_INT_TO_INT_CHECKING_MAX[ret_width][arg_width], + ) + } + } + NumToFloatChecked => { + todo!("implement toF32Checked and toF64Checked"); + } + I128OfDec => self.load_args_and_call_zig(backend, bitcode::DEC_TO_I128), + And => { + self.load_args(backend); + backend.code_builder.i32_and(); + } + Or => { + self.load_args(backend); + backend.code_builder.i32_or(); + } + Not => { + self.load_args(backend); + backend.code_builder.i32_eqz(); + } + RefCountIncRcPtr => self.load_args_and_call_zig(backend, bitcode::UTILS_INCREF_RC_PTR), + RefCountDecRcPtr => self.load_args_and_call_zig(backend, bitcode::UTILS_DECREF_RC_PTR), + RefCountIncDataPtr => { + self.load_args_and_call_zig(backend, bitcode::UTILS_INCREF_DATA_PTR) + } + RefCountDecDataPtr => { + self.load_args_and_call_zig(backend, bitcode::UTILS_DECREF_DATA_PTR) + } + RefCountIsUnique => self.load_args_and_call_zig(backend, bitcode::UTILS_IS_UNIQUE), + + PtrCast => { + let code_builder = &mut backend.code_builder; + backend.storage.load_symbols(code_builder, self.arguments); + } + + PtrStore => { + // PtrStore : Ptr a, a -> {} + let ptr = self.arguments[0]; + let value = self.arguments[1]; + + let (ptr_local_id, offset) = match backend.storage.get(&ptr) { + StoredValue::Local { local_id, .. } => (*local_id, 0), + _ => internal_error!("A pointer will always be an i32"), + }; + + // copy the argument to the pointer address + backend.storage.copy_value_to_memory( + &mut backend.code_builder, + ptr_local_id, + offset, + value, + ); + } + PtrLoad => backend.ptr_load(self.ret_symbol, self.arguments[0]), + PtrClearTagId => { + let ptr = self.arguments[0]; + + let ptr_local_id = match backend.storage.get(&ptr) { + StoredValue::Local { local_id, .. } => *local_id, + _ => internal_error!("A pointer will always be an i32"), + }; + + backend.code_builder.get_local(ptr_local_id); + + backend.code_builder.i32_const(-4); // 11111111...1100 + backend.code_builder.i32_and(); + } + + Hash => todo!("{:?}", self.lowlevel), + + Eq | NotEq => self.eq_or_neq(backend), + + BoxExpr | UnboxExpr => { + unreachable!("The {:?} operation is turned into mono Expr", self.lowlevel) + } + + Unreachable => match self.ret_storage { + StoredValue::Local { value_type, .. } => match value_type { + ValueType::I32 => backend.code_builder.i32_const(0), + ValueType::I64 => backend.code_builder.i64_const(0), + ValueType::F32 => backend.code_builder.f32_const(0.0), + ValueType::F64 => backend.code_builder.f64_const(0.0), + }, + StoredValue::StackMemory { .. } => { /* do nothing */ } + }, + DictPseudoSeed => self.load_args_and_call_zig(backend, bitcode::UTILS_DICT_PSEUDO_SEED), + + SetJmp | LongJmp | SetLongJmpBuffer => { + unreachable!("only inserted in dev backend codegen") + } + } + } + + /// Equality and inequality + /// These can operate on any data type (except functions) so they're more complex than other operators. + fn eq_or_neq(&self, backend: &mut WasmBackend<'a, '_>) { + let arg_layout = backend + .layout_interner + .runtime_representation_in(backend.storage.symbol_layouts[&self.arguments[0]]); + let arg_layout_raw = backend.layout_interner.get_repr(arg_layout); + let other_arg_layout = backend + .layout_interner + .runtime_representation(backend.storage.symbol_layouts[&self.arguments[1]]); + debug_assert_eq!( + arg_layout_raw, other_arg_layout, + "Cannot do `==` comparison on different types: {arg_layout:?} vs {other_arg_layout:?}" + ); + + let invert_result = matches!(self.lowlevel, LowLevel::NotEq); + + match arg_layout_raw { + LayoutRepr::Builtin( + Builtin::Int(_) | Builtin::Float(_) | Builtin::Bool | Builtin::Decimal, + ) => self.eq_or_neq_number(backend), + + LayoutRepr::Builtin(Builtin::Str) => { + self.load_args_and_call_zig(backend, bitcode::STR_EQUAL); + if invert_result { + backend.code_builder.i32_eqz(); + } + } + + // Empty record is always equal to empty record. + // There are no runtime arguments to check, so just emit true or false. + LayoutRepr::Struct(field_layouts) if field_layouts.is_empty() => { + backend.code_builder.i32_const(!invert_result as i32); + } + + // Void is always equal to void. This is the type for the contents of the empty list in `[] == []` + // This instruction will never execute, but we need an i32 for module validation + LayoutRepr::Union(UnionLayout::NonRecursive(tags)) if tags.is_empty() => { + backend.code_builder.i32_const(!invert_result as i32); + } + + LayoutRepr::Builtin(Builtin::List(_)) + | LayoutRepr::Struct { .. } + | LayoutRepr::Union(_) + | LayoutRepr::LambdaSet(_) + | LayoutRepr::Ptr(_) => { + // Don't want Zig calling convention here, we're calling internal Roc functions + backend + .storage + .load_symbols(&mut backend.code_builder, self.arguments); + + backend.call_eq_specialized( + self.arguments, + arg_layout, + self.ret_symbol, + &self.ret_storage, + ); + + if invert_result { + backend.code_builder.i32_eqz(); + } + } + + LayoutRepr::RecursivePointer(_) => { + internal_error!( + "Tried to apply `==` to RecursivePointer values {:?}", + self.arguments, + ) + } + + LayoutRepr::FunctionPointer(_) => todo_lambda_erasure!(), + LayoutRepr::Erased(_) => todo_lambda_erasure!(), + } + } + + fn eq_or_neq_number(&self, backend: &mut WasmBackend<'a, '_>) { + use StoredValue::*; + + match backend.storage.get(&self.arguments[0]).to_owned() { + Local { value_type, .. } => { + self.load_args(backend); + match self.lowlevel { + LowLevel::Eq => match value_type { + ValueType::I32 => backend.code_builder.i32_eq(), + ValueType::I64 => backend.code_builder.i64_eq(), + ValueType::F32 => backend.code_builder.f32_eq(), + ValueType::F64 => backend.code_builder.f64_eq(), + }, + LowLevel::NotEq => match value_type { + ValueType::I32 => backend.code_builder.i32_ne(), + ValueType::I64 => backend.code_builder.i64_ne(), + ValueType::F32 => backend.code_builder.f32_ne(), + ValueType::F64 => backend.code_builder.f64_ne(), + }, + _ => internal_error!("{:?} ended up in Equality code", self.lowlevel), + } + } + StackMemory { + format, + location: location0, + .. + } => { + if let StackMemory { + location: location1, + .. + } = backend.storage.get(&self.arguments[1]).to_owned() + { + self.eq_num128(backend, format, [location0, location1]); + if matches!(self.lowlevel, LowLevel::NotEq) { + backend.code_builder.i32_eqz(); + } + } + } + } + } + + /// Equality for 12-bit numbers. Checks if they're finite and contain the same bytes + /// Takes care of loading the arguments + fn eq_num128( + &self, + backend: &mut WasmBackend<'a, '_>, + format: StackMemoryFormat, + locations: [StackMemoryLocation; 2], + ) { + match format { + StackMemoryFormat::Decimal => Self::eq_num128_bytes(backend, locations), + + StackMemoryFormat::Int128 => Self::eq_num128_bytes(backend, locations), + + StackMemoryFormat::DataStructure => { + internal_error!("Data structure equality is handled elsewhere") + } + } + } + + /// Check that two 128-bit numbers contain the same bytes + /// Loads *half* an argument at a time + /// (Don't call "load arguments" or "load symbols" helpers before this, it'll just waste instructions) + fn eq_num128_bytes(backend: &mut WasmBackend<'a, '_>, locations: [StackMemoryLocation; 2]) { + let (local0, offset0) = locations[0].local_and_offset(backend.storage.stack_frame_pointer); + let (local1, offset1) = locations[1].local_and_offset(backend.storage.stack_frame_pointer); + + // Load & compare the first half of each argument + backend.code_builder.get_local(local0); + backend.code_builder.i64_load(Align::Bytes8, offset0); + backend.code_builder.get_local(local1); + backend.code_builder.i64_load(Align::Bytes8, offset1); + backend.code_builder.i64_eq(); + + // Load & compare the second half of each argument + backend.code_builder.get_local(local0); + backend.code_builder.i64_load(Align::Bytes8, offset0 + 8); + backend.code_builder.get_local(local1); + backend.code_builder.i64_load(Align::Bytes8, offset1 + 8); + backend.code_builder.i64_eq(); + + // First half matches AND second half matches + backend.code_builder.i32_and(); + } + + fn num_to_str(&self, backend: &mut WasmBackend<'a, '_>) { + let arg_layout = backend.storage.symbol_layouts[&self.arguments[0]]; + match backend.layout_interner.runtime_representation(arg_layout) { + LayoutRepr::Builtin(Builtin::Int(width)) => { + self.load_args_and_call_zig(backend, &bitcode::STR_FROM_INT[width]) + } + LayoutRepr::Builtin(Builtin::Float(width)) => match width { + FloatWidth::F32 => { + self.load_args(backend); + backend.code_builder.f64_promote_f32(); + self.load_args_and_call_zig(backend, &bitcode::STR_FROM_FLOAT[width]); + } + FloatWidth::F64 => { + self.load_args_and_call_zig(backend, &bitcode::STR_FROM_FLOAT[width]); + } + }, + LayoutRepr::Builtin(Builtin::Decimal) => { + self.load_args_and_call_zig(backend, bitcode::DEC_TO_STR) + } + x => internal_error!("NumToStr is not defined for {:?}", x), + } + } +} + +/// Helper for NumIsNan op +fn num_is_nan(backend: &mut WasmBackend<'_, '_>, argument: Symbol) { + use StoredValue::*; + let stored = backend.storage.get(&argument).to_owned(); + match stored { + Local { value_type, .. } => { + backend + .storage + .load_symbols(&mut backend.code_builder, &[argument]); + match value_type { + // Integers are never NaN. Just return False. + ValueType::I32 | ValueType::I64 => backend.code_builder.i32_const(0), + ValueType::F32 => { + backend.code_builder.i32_reinterpret_f32(); + backend.code_builder.i32_const(0x7f80_0000); + backend.code_builder.i32_and(); + backend.code_builder.i32_const(0x7f80_0000); + backend.code_builder.i32_eq(); // Exponents are all ones + + backend + .storage + .load_symbols(&mut backend.code_builder, &[argument]); + backend.code_builder.i32_reinterpret_f32(); + backend.code_builder.i32_const(0x007f_ffff); + backend.code_builder.i32_and(); + backend.code_builder.i32_const(0); + backend.code_builder.i32_ne(); // Mantissa is non-zero + backend.code_builder.i32_and(); + } + ValueType::F64 => { + backend.code_builder.i64_reinterpret_f64(); + backend.code_builder.i64_const(0x7ff0_0000_0000_0000); + backend.code_builder.i64_and(); + backend.code_builder.i64_const(0x7ff0_0000_0000_0000); + backend.code_builder.i64_eq(); // Exponents are all ones + + backend + .storage + .load_symbols(&mut backend.code_builder, &[argument]); + backend.code_builder.i64_reinterpret_f64(); + backend.code_builder.i64_const(0x000f_ffff_ffff_ffff); + backend.code_builder.i64_and(); + backend.code_builder.i64_const(0); + backend.code_builder.i64_ne(); // Mantissa is non-zero + backend.code_builder.i32_and(); + } + } + } + StackMemory { format, .. } => { + match format { + // Integers and fixed-point numbers are NaN. Just return False. + StackMemoryFormat::Int128 | StackMemoryFormat::Decimal => { + backend.code_builder.i32_const(0) + } + + StackMemoryFormat::DataStructure => { + internal_error!("Tried to perform NumIsInfinite on a data structure") + } + } + } + } +} + +/// Helper for NumIsInfinite op +fn num_is_infinite(backend: &mut WasmBackend<'_, '_>, argument: Symbol) { + use StoredValue::*; + let stored = backend.storage.get(&argument).to_owned(); + match stored { + Local { value_type, .. } => { + backend + .storage + .load_symbols(&mut backend.code_builder, &[argument]); + match value_type { + // Integers are never infinite. Just return False. + ValueType::I32 | ValueType::I64 => backend.code_builder.i32_const(0), + ValueType::F32 => { + backend.code_builder.i32_reinterpret_f32(); + backend.code_builder.i32_const(0x7fff_ffff); + backend.code_builder.i32_and(); + backend.code_builder.i32_const(0x7f80_0000); + backend.code_builder.i32_eq(); + } + ValueType::F64 => { + backend.code_builder.i64_reinterpret_f64(); + backend.code_builder.i64_const(0x7fff_ffff_ffff_ffff); + backend.code_builder.i64_and(); + backend.code_builder.i64_const(0x7ff0_0000_0000_0000); + backend.code_builder.i64_eq(); + } + } + } + StackMemory { format, .. } => { + match format { + // Integers and fixed-point numbers are never infinite. Just return False. + StackMemoryFormat::Int128 | StackMemoryFormat::Decimal => { + backend.code_builder.i32_const(0) + } + + StackMemoryFormat::DataStructure => { + internal_error!("Tried to perform NumIsInfinite on a data structure") + } + } + } + } +} + +/// Helper for NumIsFinite op, and also part of Eq/NotEq +fn num_is_finite(backend: &mut WasmBackend<'_, '_>, argument: Symbol) { + use StoredValue::*; + let stored = backend.storage.get(&argument).to_owned(); + match stored { + Local { value_type, .. } => { + backend + .storage + .load_symbols(&mut backend.code_builder, &[argument]); + match value_type { + // Integers are always finite. Just return True. + ValueType::I32 | ValueType::I64 => backend.code_builder.i32_const(1), + ValueType::F32 => { + backend.code_builder.i32_reinterpret_f32(); + backend.code_builder.i32_const(0x7f80_0000); + backend.code_builder.i32_and(); + backend.code_builder.i32_const(0x7f80_0000); + backend.code_builder.i32_ne(); + } + ValueType::F64 => { + backend.code_builder.i64_reinterpret_f64(); + backend.code_builder.i64_const(0x7ff0_0000_0000_0000); + backend.code_builder.i64_and(); + backend.code_builder.i64_const(0x7ff0_0000_0000_0000); + backend.code_builder.i64_ne(); + } + } + } + StackMemory { format, .. } => { + match format { + // Integers and fixed-point numbers are always finite. Just return True. + StackMemoryFormat::Int128 | StackMemoryFormat::Decimal => { + backend.code_builder.i32_const(1) + } + + StackMemoryFormat::DataStructure => { + internal_error!("Tried to perform NumIsFinite on a data structure") + } + } + } + } +} + +pub fn call_higher_order_lowlevel<'a>( + backend: &mut WasmBackend<'a, '_>, + return_sym: Symbol, + return_layout: &InLayout<'a>, + higher_order: &'a HigherOrderLowLevel<'a>, +) { + use HigherOrder::*; + + let HigherOrderLowLevel { + op, + passed_function, + .. + } = higher_order; + + let PassedFunction { + name: fn_name, + argument_layouts, + return_layout: result_layout, + owns_captured_environment, + captured_environment, + .. + } = passed_function; + + // The zig lowlevel builtins expect the passed functions' closure data to always + // be sent as an opaque pointer. On the Roc side, however, we need to call the passed function + // with the Roc representation of the closure data. There are three possible cases for that + // representation: + // + // 1. The closure data is a struct + // 2. The closure data is an unwrapped value + // 3. There is no closure data + // + // To uniformly deal with the first two cases, always put the closure data, when it exists, + // into a one-element struct. That way, we get a pointer (i32) that can be passed to the zig lowlevels. + // The wrapper around the passed function will access the actual closure data in the struct. + let (closure_data_layout, closure_data_exists) = match backend + .layout_interner + .get_repr(backend.storage.symbol_layouts[captured_environment]) + { + LayoutRepr::LambdaSet(lambda_set) => { + if lambda_set.is_represented(backend.layout_interner).is_some() { + (lambda_set.runtime_representation(), true) + } else { + // Closure data is a lambda set, which *itself* has no closure data! + // The higher-order wrapper doesn't need to pass this down, that's + // handled in other ways in the IR. Here just pretend it's Unit. + (Layout::UNIT, false) + } + } + LayoutRepr::Struct(&[]) => (Layout::UNIT, false), + x => internal_error!("Closure data has an invalid layout\n{:?}", x), + }; + + let (wrapped_captured_environment, wrapped_captures_layout) = if closure_data_exists { + // If there is closure data, make sure we put in a struct it before passing it to the + // external builtin impl. That way it's always an `i32` pointer. + let wrapped_closure_data_sym = backend.create_symbol("wrapped_captures"); + let wrapped_captures_layout = + backend + .layout_interner + .insert_direct_no_semantic(LayoutRepr::struct_( + backend.env.arena.alloc([closure_data_layout]), + )); + + // make sure that the wrapping struct is available in stack memory, so we can hand out a + // pointer to it. + let wrapped_storage = backend.storage.allocate_var( + backend.layout_interner, + wrapped_captures_layout, + wrapped_closure_data_sym, + crate::storage::StoredVarKind::Variable, + ); + + let (wrapped_storage_local_ptr, wrapped_storage_offset) = match wrapped_storage { + StoredValue::StackMemory { location, .. } => { + location.local_and_offset(backend.storage.stack_frame_pointer) + } + other => internal_error!( + "Struct should be allocated in stack memory, but it's in {:?}", + other + ), + }; + + // copy the actual closure data into the first and only element of the wrapping struct. + backend.storage.copy_value_to_memory( + &mut backend.code_builder, + wrapped_storage_local_ptr, + wrapped_storage_offset, + *captured_environment, + ); + + (wrapped_closure_data_sym, wrapped_captures_layout) + } else { + // If we don't capture anything, pass along the captured environment as-is - the wrapper + // function will take care not to unwrap this. + (*captured_environment, closure_data_layout) + }; + + // We create a wrapper around the passed function, which just unboxes the arguments. + // This allows Zig builtins to have a generic pointer-based interface. + let helper_proc_source = { + let passed_proc_layout = ProcLayout { + arguments: argument_layouts, + result: *result_layout, + niche: fn_name.niche(), + }; + let passed_proc_index = backend + .proc_lookup + .iter() + .position(|ProcLookupData { name, layout, .. }| { + *name == fn_name.name() && layout == &passed_proc_layout + }) + .unwrap(); + match op { + ListSortWith { .. } => ProcSource::HigherOrderCompare(passed_proc_index), + ListMap { .. } | ListMap2 { .. } | ListMap3 { .. } | ListMap4 { .. } => { + ProcSource::HigherOrderMapper(passed_proc_index) + } + } + }; + let wrapper_sym = backend.create_symbol(&format!("#wrap#{fn_name:?}")); + let wrapper_layout = { + let mut wrapper_arg_layouts: Vec> = + Vec::with_capacity_in(argument_layouts.len() + 1, backend.env.arena); + + let n_non_closure_args = if closure_data_exists { + argument_layouts.len() - 1 + } else { + argument_layouts.len() + }; + + let boxed_closure_arg_layouts = + argument_layouts.iter().take(n_non_closure_args).map(|lay| { + backend + .layout_interner + .insert_direct_no_semantic(LayoutRepr::Ptr(*lay)) + }); + + wrapper_arg_layouts.push(wrapped_captures_layout); + wrapper_arg_layouts.extend(boxed_closure_arg_layouts); + + match helper_proc_source { + ProcSource::HigherOrderMapper(_) => { + // Our convention for mappers is that they write to the heap via the last argument + wrapper_arg_layouts.push( + backend + .layout_interner + .insert_direct_no_semantic(LayoutRepr::Ptr(*result_layout)), + ); + ProcLayout { + arguments: wrapper_arg_layouts.into_bump_slice(), + result: Layout::UNIT, + niche: fn_name.niche(), + } + } + ProcSource::HigherOrderCompare(_) => ProcLayout { + arguments: wrapper_arg_layouts.into_bump_slice(), + result: *result_layout, + niche: fn_name.niche(), + }, + ProcSource::Roc | ProcSource::Helper => { + internal_error!("Should never reach here for {:?}", helper_proc_source) + } + } + }; + + let wrapper_fn_idx = + backend.register_helper_proc(wrapper_sym, wrapper_layout, helper_proc_source); + let wrapper_fn_ptr = backend.get_fn_ptr(wrapper_fn_idx); + let inc_fn_ptr = if !closure_data_exists { + // Our code gen would ignore the Unit arg, but the Zig builtin passes a pointer for it! + // That results in an exception (type signature mismatch in indirect call). + // The workaround is to use I32 layout, treating the (ignored) pointer as an integer. + let inc_fn = backend.get_refcount_fn_index(Layout::I32, HelperOp::Inc); + backend.get_fn_ptr(inc_fn) + } else { + let inc_fn = backend.get_refcount_fn_index(wrapped_captures_layout, HelperOp::Inc); + backend.get_fn_ptr(inc_fn) + }; + + match op { + ListMap { xs } => list_map_n( + bitcode::LIST_MAP, + backend, + &[*xs], + return_sym, + *return_layout, + wrapper_fn_ptr, + inc_fn_ptr, + closure_data_exists, + wrapped_captured_environment, + *owns_captured_environment, + ), + + ListMap2 { xs, ys } => list_map_n( + bitcode::LIST_MAP2, + backend, + &[*xs, *ys], + return_sym, + *return_layout, + wrapper_fn_ptr, + inc_fn_ptr, + closure_data_exists, + wrapped_captured_environment, + *owns_captured_environment, + ), + + ListMap3 { xs, ys, zs } => list_map_n( + bitcode::LIST_MAP3, + backend, + &[*xs, *ys, *zs], + return_sym, + *return_layout, + wrapper_fn_ptr, + inc_fn_ptr, + closure_data_exists, + wrapped_captured_environment, + *owns_captured_environment, + ), + + ListMap4 { xs, ys, zs, ws } => list_map_n( + bitcode::LIST_MAP4, + backend, + &[*xs, *ys, *zs, *ws], + return_sym, + *return_layout, + wrapper_fn_ptr, + inc_fn_ptr, + closure_data_exists, + wrapped_captured_environment, + *owns_captured_environment, + ), + + ListSortWith { xs } => { + let elem_layout = unwrap_list_elem_layout( + backend + .layout_interner + .get_repr(backend.storage.symbol_layouts[xs]), + ); + let elem_layout = backend.layout_interner.get_repr(elem_layout); + let (element_width, alignment) = + elem_layout.stack_size_and_alignment(backend.layout_interner); + + let cb = &mut backend.code_builder; + + // (return pointer) i32 + // input: RocList, i32 + // caller: CompareFn, i32 + // data: Opaque, i32 + // inc_n_data: IncN, i32 + // data_is_owned: bool, i32 + // alignment: u32, i32 + // element_width: usize, i32 + + backend.storage.load_symbols(cb, &[return_sym, *xs]); + cb.i32_const(wrapper_fn_ptr); + if closure_data_exists { + backend + .storage + .load_symbols(cb, &[wrapped_captured_environment]); + } else { + // load_symbols assumes that a zero-size arg should be eliminated in code gen, + // but that's a specialization that our Zig code doesn't have! Pass a null pointer. + cb.i32_const(0); + } + cb.i32_const(inc_fn_ptr); + cb.i32_const(*owns_captured_environment as i32); + cb.i32_const(alignment as i32); + cb.i32_const(element_width as i32); + + backend.call_host_fn_after_loading_args(bitcode::LIST_SORT_WITH); + } + } +} + +fn unwrap_list_elem_layout(list_layout: LayoutRepr) -> InLayout { + match list_layout { + LayoutRepr::Builtin(Builtin::List(x)) => x, + e => internal_error!("expected List layout, got {:?}", e), + } +} + +#[allow(clippy::too_many_arguments)] +fn list_map_n<'a>( + zig_fn_name: &'static str, + backend: &mut WasmBackend<'a, '_>, + arg_symbols: &[Symbol], + return_sym: Symbol, + return_layout: InLayout<'a>, + wrapper_fn_ptr: i32, + inc_fn_ptr: i32, + closure_data_exists: bool, + captured_environment: Symbol, + owns_captured_environment: bool, +) { + let arg_elem_layouts = Vec::from_iter_in( + arg_symbols.iter().map(|sym| { + unwrap_list_elem_layout( + backend + .layout_interner + .get_repr(backend.storage.symbol_layouts[sym]), + ) + }), + backend.env.arena, + ); + + let elem_ret = unwrap_list_elem_layout(backend.layout_interner.get_repr(return_layout)); + let elem_ret = backend.layout_interner.get_repr(elem_ret); + let (elem_ret_size, elem_ret_align) = + elem_ret.stack_size_and_alignment(backend.layout_interner); + + let cb = &mut backend.code_builder; + + let mut args_vec = Vec::with_capacity_in(arg_symbols.len() + 1, backend.env.arena); + args_vec.push(return_sym); + args_vec.extend_from_slice(arg_symbols); + backend.storage.load_symbols(cb, &args_vec); + + cb.i32_const(wrapper_fn_ptr); + if closure_data_exists { + backend.storage.load_symbols(cb, &[captured_environment]); + } else { + // load_symbols assumes that a zero-size arg should be eliminated in code gen, + // but that's a specialization that our Zig code doesn't have! Pass a null pointer. + cb.i32_const(0); + } + cb.i32_const(inc_fn_ptr); + cb.i32_const(owns_captured_environment as i32); + cb.i32_const(elem_ret_align as i32); + for el in arg_elem_layouts.iter() { + cb.i32_const(backend.layout_interner.stack_size(*el) as i32); + } + cb.i32_const(elem_ret_size as i32); + + // If we have lists of different lengths, we may need to decrement + if arg_elem_layouts.len() > 1 { + for el in arg_elem_layouts.iter() { + // The dec function will be passed a pointer to the element within the list, not the element itself! + // Here we wrap the layout in a Struct to ensure we get the right code gen + let el_ptr = backend + .layout_interner + .insert_direct_no_semantic(LayoutRepr::Struct(backend.env.arena.alloc([*el]))); + let idx = backend.get_refcount_fn_index(el_ptr, HelperOp::Dec); + let ptr = backend.get_fn_ptr(idx); + backend.code_builder.i32_const(ptr); + } + }; + + backend.call_host_fn_after_loading_args(zig_fn_name); +} + +fn ensure_symbol_is_in_memory<'a>( + backend: &mut WasmBackend<'a, '_>, + symbol: Symbol, + layout: InLayout<'a>, + arena: &'a Bump, +) -> (LocalId, u32, InLayout<'a>) { + // Ensure the new element is stored in memory so we can pass a pointer to Zig + match backend.storage.get(&symbol) { + StoredValue::StackMemory { location, .. } => { + let (local, offset) = location.local_and_offset(backend.storage.stack_frame_pointer); + (local, offset, layout) + } + _ => { + let (width, alignment) = backend.layout_interner.stack_size_and_alignment(layout); + let (frame_ptr, offset) = backend + .storage + .allocate_anonymous_stack_memory(width, alignment); + backend.storage.copy_value_to_memory( + &mut backend.code_builder, + frame_ptr, + offset, + symbol, + ); + let in_memory_layout = backend + .layout_interner + .insert_direct_no_semantic(LayoutRepr::Struct(arena.alloc([layout]))); + (frame_ptr, offset, in_memory_layout) + } + } +} diff --git a/crates/compiler/gen_wasm/src/storage.rs b/crates/compiler/gen_wasm/src/storage.rs new file mode 100644 index 0000000000..1276570363 --- /dev/null +++ b/crates/compiler/gen_wasm/src/storage.rs @@ -0,0 +1,586 @@ +use bumpalo::collections::Vec; +use bumpalo::Bump; + +use roc_collections::all::MutMap; +use roc_error_macros::internal_error; +use roc_module::symbol::Symbol; +use roc_mono::layout::{InLayout, STLayoutInterner}; + +use crate::code_builder::CodeBuilder; +use crate::layout::{stack_memory_arg_types, ReturnMethod, StackMemoryFormat, WasmLayout}; +use crate::{copy_memory, CopyMemoryConfig, PTR_TYPE}; +use roc_wasm_module::{round_up_to_alignment, Align, LocalId, ValueType}; + +pub enum StoredVarKind { + Variable, + ReturnValue, +} + +#[derive(Debug, Clone)] +pub enum StackMemoryLocation { + FrameOffset(u32), + PointerArg(LocalId), +} + +impl StackMemoryLocation { + pub fn local_and_offset(&self, stack_frame_pointer: Option) -> (LocalId, u32) { + match self { + Self::PointerArg(local_id) => (*local_id, 0), + Self::FrameOffset(offset) => (stack_frame_pointer.unwrap(), *offset), + } + } +} + +#[derive(Debug, Clone)] +pub enum StoredValue { + /// A local variable in the Wasm function (primitives only) + Local { + local_id: LocalId, + value_type: ValueType, + size: u32, + }, + + /// A Struct, or other non-primitive value, stored in stack memory + StackMemory { + location: StackMemoryLocation, + size: u32, + alignment_bytes: u32, + format: StackMemoryFormat, + }, +} + +pub enum AddressValue { + /// The address value has been loaded to the VM stack + Loaded, + /// The address value is in a local variable + NotLoaded(LocalId), +} + +/// Helper structure for WasmBackend, to keep track of how values are stored, +/// including the VM stack, local variables, and linear memory +#[derive(Debug)] +pub struct Storage<'a> { + pub return_var: Option, + pub arg_types: Vec<'a, ValueType>, + pub local_types: Vec<'a, ValueType>, + pub symbol_layouts: MutMap>, + pub symbol_storage_map: MutMap, + pub stack_frame_pointer: Option, + pub stack_frame_size: i32, +} + +impl<'a> Storage<'a> { + pub fn new(arena: &'a Bump) -> Self { + Storage { + return_var: None, + arg_types: Vec::with_capacity_in(8, arena), + local_types: Vec::with_capacity_in(32, arena), + symbol_layouts: MutMap::default(), + symbol_storage_map: MutMap::default(), + stack_frame_pointer: None, + stack_frame_size: 0, + } + } + + pub fn clear(&mut self) { + self.return_var = None; + self.arg_types.clear(); + self.local_types.clear(); + self.symbol_layouts.clear(); + self.symbol_storage_map.clear(); + self.stack_frame_pointer = None; + self.stack_frame_size = 0; + } + + /// Internal use only. See `allocate` or `create_anonymous_local` + fn get_next_local_id(&self) -> LocalId { + LocalId((self.arg_types.len() + self.local_types.len()) as u32) + } + + pub fn create_anonymous_local(&mut self, value_type: ValueType) -> LocalId { + let id = self.get_next_local_id(); + self.local_types.push(value_type); + id + } + + pub fn allocate_anonymous_stack_memory( + &mut self, + size: u32, + alignment_bytes: u32, + ) -> (LocalId, u32) { + let offset = self.allocate_stack_memory(size, alignment_bytes); + let fp = self.stack_frame_pointer.unwrap(); + (fp, offset) + } + + fn allocate_stack_memory(&mut self, size: u32, alignment_bytes: u32) -> u32 { + // Note: We need a stack frame pointer even if size is zero. + // e.g. when passing an empty record to a Zig builtin, we pass the frame pointer + if self.stack_frame_pointer.is_none() { + let next_local_id = self.get_next_local_id(); + self.stack_frame_pointer = Some(next_local_id); + self.local_types.push(PTR_TYPE); + } + + let offset = round_up_to_alignment!(self.stack_frame_size, alignment_bytes as i32); + + self.stack_frame_size = offset + (size as i32); + + offset as u32 + } + + /// Allocate storage for a Roc variable + /// + /// Wasm primitives (i32, i64, f32, f64) are allocated local variables. + /// Data structures are stored in memory, with an offset and size in the stack frame. + pub fn allocate_var( + &mut self, + interner: &STLayoutInterner<'a>, + layout: InLayout<'a>, + symbol: Symbol, + kind: StoredVarKind, + ) -> StoredValue { + let wasm_layout = WasmLayout::new(interner, layout); + self.symbol_layouts.insert(symbol, layout); + + let storage = match wasm_layout { + WasmLayout::Primitive(value_type, size) => { + let local_id = self.get_next_local_id(); + self.local_types.push(value_type); + StoredValue::Local { + local_id, + value_type, + size, + } + } + WasmLayout::StackMemory { + size, + alignment_bytes, + format, + } => { + let location = match kind { + StoredVarKind::Variable => { + let offset = self.allocate_stack_memory(size, alignment_bytes); + StackMemoryLocation::FrameOffset(offset) + } + + StoredVarKind::ReturnValue => StackMemoryLocation::PointerArg(LocalId(0)), + }; + + StoredValue::StackMemory { + location, + size, + alignment_bytes, + format, + } + } + }; + + self.symbol_storage_map.insert(symbol, storage.clone()); + + storage + } + + /// Allocate storage for a Roc procedure argument + /// Each argument is also a local variable. Their indices come before other locals. + /// Structs and Tags are passed as pointers into the caller's frame + /// 128-bit numbers are passed as two i64's, but we immediately store them in the + /// stack frame, because it's a lot easier to keep track of the data flow. + pub fn allocate_args( + &mut self, + interner: &STLayoutInterner<'a>, + args: &[(InLayout<'a>, Symbol)], + code_builder: &mut CodeBuilder, + arena: &'a Bump, + ) { + let mut wide_number_args = Vec::with_capacity_in(args.len(), arena); + let mut has_zero_size_arg = false; + + for (layout, symbol) in args { + self.symbol_layouts.insert(*symbol, *layout); + let wasm_layout = WasmLayout::new(interner, *layout); + let local_index = self.arg_types.len() as u32; + + let storage = match wasm_layout { + WasmLayout::Primitive(value_type, size) => { + self.arg_types.push(value_type); + StoredValue::Local { + local_id: LocalId(local_index), + value_type, + size, + } + } + WasmLayout::StackMemory { + size, + alignment_bytes, + format, + } => { + use StackMemoryFormat::*; + + self.arg_types + .extend_from_slice(stack_memory_arg_types(size, format)); + + let location = match format { + Int128 | Decimal => { + // passed as two i64's but stored in the stack frame + wide_number_args.push(local_index); + let loc = + StackMemoryLocation::FrameOffset(self.stack_frame_size as u32); + self.stack_frame_size += size as i32; + loc + } + DataStructure => { + if size == 0 { + // An argument with zero size is purely conceptual, and will not exist in Wasm. + // However we need to track the symbol, so we treat it like a local variable. + has_zero_size_arg = true; + StackMemoryLocation::FrameOffset(0) + } else { + StackMemoryLocation::PointerArg(LocalId(local_index)) + } + } + }; + + StoredValue::StackMemory { + location, + size, + alignment_bytes, + format, + } + } + }; + + self.symbol_storage_map.insert(*symbol, storage.clone()); + } + + // If any arguments are 128-bit numbers, store them in the stack frame + // This makes it easier to keep track of which symbols are on the Wasm value stack + // The frame pointer will be the next local after the arguments + if self.stack_frame_size > 0 || has_zero_size_arg { + let frame_ptr = LocalId(self.arg_types.len() as u32); + self.stack_frame_pointer = Some(frame_ptr); + self.local_types.push(PTR_TYPE); + + let mut offset = 0; + for arg_index in wide_number_args.iter().copied() { + code_builder.get_local(frame_ptr); + code_builder.get_local(LocalId(arg_index)); + code_builder.i64_store(Align::Bytes8, offset); + + code_builder.get_local(frame_ptr); + code_builder.get_local(LocalId(arg_index + 1)); + code_builder.i64_store(Align::Bytes8, offset + 8); + + offset += 16; + } + } + } + + /// Get storage info for a given symbol + pub fn get(&self, sym: &Symbol) -> &StoredValue { + self.symbol_storage_map.get(sym).unwrap_or_else(|| { + internal_error!( + "Symbol {:?} not found in function scope:\n{:?}", + sym, + self.symbol_storage_map + ) + }) + } + + /// Load a single symbol using the C Calling Convention + fn load_symbol_ccc(&mut self, code_builder: &mut CodeBuilder, sym: Symbol) { + let storage = self.get(&sym).to_owned(); + match storage { + StoredValue::Local { local_id, .. } => { + code_builder.get_local(local_id); + } + + StoredValue::StackMemory { + location, + format, + size, + .. + } => { + if size == 0 { + return; + } + + let (local_id, offset) = location.local_and_offset(self.stack_frame_pointer); + + code_builder.get_local(local_id); + + if format == StackMemoryFormat::DataStructure { + if offset != 0 { + code_builder.i32_const(offset as i32); + code_builder.i32_add(); + } + } else { + // It's one of the 128-bit numbers, all of which we load as two i64's + code_builder.i64_load(Align::Bytes8, offset); + code_builder.get_local(local_id); + code_builder.i64_load(Align::Bytes8, offset + 8); + } + } + } + } + + /// stack memory values are returned by pointer. e.g. a roc function + /// + /// add : I128, I128 -> I128 + /// + /// is given the wasm type + /// + /// add : (i32, i64, i64, i64, i64) -> nil + /// + /// The returned value is written to the address passed as the first argument + fn load_return_address_ccc(&mut self, code_builder: &mut CodeBuilder, sym: Symbol) { + let storage = self.get(&sym).to_owned(); + match storage { + StoredValue::Local { .. } => { + internal_error!("these storage types are not returned by writing to a pointer") + } + StoredValue::StackMemory { location, size, .. } => { + if size == 0 { + return; + } + + let (local_id, offset) = location.local_and_offset(self.stack_frame_pointer); + + code_builder.get_local(local_id); + if offset != 0 { + code_builder.i32_const(offset as i32); + code_builder.i32_add(); + } + } + } + } + + /// Load symbols to the top of the VM stack + pub fn load_symbols(&mut self, code_builder: &mut CodeBuilder, symbols: &[Symbol]) { + for sym in symbols.iter() { + self.load_symbol_ccc(code_builder, *sym); + } + } + + /// Load symbols for a function call + pub fn load_symbols_for_call( + &mut self, + code_builder: &mut CodeBuilder, + arguments: &[Symbol], + return_symbol: Symbol, + return_layout: &WasmLayout, + ) { + if return_layout.return_method() == ReturnMethod::WriteToPointerArg { + self.load_return_address_ccc(code_builder, return_symbol); + }; + + for arg in arguments { + self.load_symbol_ccc(code_builder, *arg); + } + } + + /// Generate code to copy a StoredValue to an arbitrary memory location + /// (defined by a pointer and offset). + pub fn copy_value_to_memory( + &mut self, + code_builder: &mut CodeBuilder, + to_ptr: LocalId, + to_offset: u32, + from_symbol: Symbol, + ) -> u32 { + let from_storage = self.get(&from_symbol).to_owned(); + match from_storage { + StoredValue::StackMemory { + location, + size, + alignment_bytes, + .. + } => { + if size > 0 { + let (from_ptr, from_offset) = + location.local_and_offset(self.stack_frame_pointer); + copy_memory( + code_builder, + CopyMemoryConfig { + from_ptr, + from_offset, + to_ptr, + to_offset, + size, + alignment_bytes, + }, + ); + } + size + } + + StoredValue::Local { + value_type, size, .. + } => { + use roc_wasm_module::Align::*; + code_builder.get_local(to_ptr); + self.load_symbols(code_builder, &[from_symbol]); + match (value_type, size) { + (ValueType::I64, 8) => code_builder.i64_store(Bytes8, to_offset), + (ValueType::I32, 4) => code_builder.i32_store(Bytes4, to_offset), + (ValueType::I32, 2) => code_builder.i32_store16(Bytes2, to_offset), + (ValueType::I32, 1) => code_builder.i32_store8(Bytes1, to_offset), + (ValueType::F32, 4) => code_builder.f32_store(Bytes4, to_offset), + (ValueType::F64, 8) => code_builder.f64_store(Bytes8, to_offset), + _ => { + internal_error!( + "Cannot store {:?} with alignment of {:?}", + value_type, + size + ); + } + } + size + } + } + } + + /// Generate code to copy a StoredValue from an arbitrary memory location + /// (defined by a pointer and offset). + pub fn copy_value_from_memory( + &mut self, + code_builder: &mut CodeBuilder, + to_symbol: Symbol, + from_addr: AddressValue, + from_offset: u32, + ) { + let to_storage = self.get(&to_symbol).to_owned(); + match to_storage { + StoredValue::StackMemory { + location, + size, + alignment_bytes, + .. + } => { + if self.stack_frame_pointer.is_none() { + self.stack_frame_pointer = Some(self.get_next_local_id()); + } + + let from_ptr = match from_addr { + AddressValue::NotLoaded(ptr) => ptr, + AddressValue::Loaded => { + // The `from` address is on the VM stack but we want it in a local for copying + let tmp_local = self.create_anonymous_local(PTR_TYPE); + code_builder.set_local(tmp_local); + tmp_local + } + }; + + let (to_ptr, to_offset) = location.local_and_offset(self.stack_frame_pointer); + copy_memory( + code_builder, + CopyMemoryConfig { + from_ptr, + from_offset, + to_ptr, + to_offset, + size, + alignment_bytes, + }, + ); + } + + StoredValue::Local { + value_type, + size, + local_id, + } => { + use roc_wasm_module::Align::*; + + if let AddressValue::NotLoaded(from_ptr) = from_addr { + code_builder.get_local(from_ptr); + } + + match (value_type, size) { + (ValueType::I64, 8) => code_builder.i64_load(Bytes8, from_offset), + (ValueType::I32, 4) => code_builder.i32_load(Bytes4, from_offset), + (ValueType::I32, 2) => code_builder.i32_load16_s(Bytes2, from_offset), + (ValueType::I32, 1) => code_builder.i32_load8_s(Bytes1, from_offset), + (ValueType::F32, 4) => code_builder.f32_load(Bytes4, from_offset), + (ValueType::F64, 8) => code_builder.f64_load(Bytes8, from_offset), + _ => { + internal_error!( + "Cannot store {:?} with alignment of {:?}", + value_type, + size + ); + } + }; + + code_builder.set_local(local_id); + } + } + } + + /// Generate code to copy from one StoredValue to another + /// Copies the _entire_ value. For struct fields etc., see `copy_value_to_memory` + pub fn clone_value( + &mut self, + code_builder: &mut CodeBuilder, + to: &StoredValue, + from: &StoredValue, + ) { + use StoredValue::*; + + match (to, from) { + ( + Local { + local_id: to_local_id, + value_type: to_value_type, + size: to_size, + }, + Local { + local_id: from_local_id, + value_type: from_value_type, + size: from_size, + }, + ) => { + debug_assert!(to_value_type == from_value_type); + debug_assert!(to_size == from_size); + code_builder.get_local(*from_local_id); + code_builder.set_local(*to_local_id); + } + + ( + StackMemory { + location: to_location, + size: to_size, + alignment_bytes: to_alignment_bytes, + .. + }, + StackMemory { + location: from_location, + size: from_size, + alignment_bytes: from_alignment_bytes, + .. + }, + ) => { + let (from_ptr, from_offset) = + from_location.local_and_offset(self.stack_frame_pointer); + let (to_ptr, to_offset) = to_location.local_and_offset(self.stack_frame_pointer); + debug_assert!(*to_size == *from_size); + debug_assert!(*to_alignment_bytes == *from_alignment_bytes); + copy_memory( + code_builder, + CopyMemoryConfig { + from_ptr, + from_offset, + to_ptr, + to_offset, + size: *from_size, + alignment_bytes: *from_alignment_bytes, + }, + ); + } + + _ => { + internal_error!("Cannot copy storage from {:?} to {:?}", from, to); + } + } + } +} diff --git a/crates/compiler/gen_wasm/src/wasm32_result.rs b/crates/compiler/gen_wasm/src/wasm32_result.rs new file mode 100644 index 0000000000..7e593245c4 --- /dev/null +++ b/crates/compiler/gen_wasm/src/wasm32_result.rs @@ -0,0 +1,296 @@ +/* +Generate a wrapper function to expose a generic interface from a Wasm module for any result type. +The wrapper function ensures the value is written to memory and returns its address as i32. +The user needs to analyse the Wasm module's memory to decode the result. +*/ + +use bumpalo::{collections::Vec, Bump}; + +use roc_builtins::bitcode::{FloatWidth, IntWidth}; +use roc_mono::layout::{Builtin, InLayout, LayoutInterner, LayoutRepr, UnionLayout}; +use roc_std::{RocBox, RocDec, RocList, RocOrder, RocResult, RocStr, I128, U128}; +use roc_wasm_module::{ + linking::SymInfo, linking::WasmObjectSymbol, Align, Export, ExportType, LocalId, Signature, + ValueType, WasmModule, +}; + +use crate::code_builder::CodeBuilder; +use crate::wasm32_sized::Wasm32Sized; + +/// Type-driven wrapper generation +pub trait Wasm32Result { + fn insert_wrapper<'a>( + arena: &'a Bump, + module: &mut WasmModule<'a>, + wrapper_name: &'static str, + main_function_index: u32, + ) { + insert_wrapper_metadata(arena, module, wrapper_name); + let mut code_builder = CodeBuilder::new(arena); + Self::build_wrapper_body(&mut code_builder, main_function_index); + code_builder.insert_into_module(module); + } + + fn build_wrapper_body(code_builder: &mut CodeBuilder, main_function_index: u32); +} + +/// Layout-driven wrapper generation +pub fn insert_wrapper_for_layout<'a>( + arena: &'a Bump, + interner: &impl LayoutInterner<'a>, + module: &mut WasmModule<'a>, + wrapper_name: &'static str, + main_fn_index: u32, + layout: InLayout<'a>, +) { + let mut stack_data_structure = || { + let size = interner.stack_size(layout); + if size == 0 { + <() as Wasm32Result>::insert_wrapper(arena, module, wrapper_name, main_fn_index); + } else { + insert_wrapper_metadata(arena, module, wrapper_name); + let mut code_builder = CodeBuilder::new(arena); + build_wrapper_body_stack_memory(&mut code_builder, main_fn_index, size as usize); + code_builder.insert_into_module(module); + } + }; + + match interner.get_repr(layout) { + LayoutRepr::Builtin(Builtin::Int(IntWidth::U8 | IntWidth::I8)) => { + i8::insert_wrapper(arena, module, wrapper_name, main_fn_index); + } + LayoutRepr::Builtin(Builtin::Int(IntWidth::U16 | IntWidth::I16)) => { + i16::insert_wrapper(arena, module, wrapper_name, main_fn_index); + } + LayoutRepr::Builtin(Builtin::Int(IntWidth::U32 | IntWidth::I32)) => { + i32::insert_wrapper(arena, module, wrapper_name, main_fn_index); + } + LayoutRepr::Builtin(Builtin::Int(IntWidth::U64 | IntWidth::I64)) => { + i64::insert_wrapper(arena, module, wrapper_name, main_fn_index); + } + LayoutRepr::Builtin(Builtin::Float(FloatWidth::F32)) => { + f32::insert_wrapper(arena, module, wrapper_name, main_fn_index); + } + LayoutRepr::Builtin(Builtin::Float(FloatWidth::F64)) => { + f64::insert_wrapper(arena, module, wrapper_name, main_fn_index); + } + LayoutRepr::Builtin(Builtin::Bool) => { + bool::insert_wrapper(arena, module, wrapper_name, main_fn_index); + } + LayoutRepr::Union(UnionLayout::NonRecursive(_)) => stack_data_structure(), + LayoutRepr::Union(_) => { + i32::insert_wrapper(arena, module, wrapper_name, main_fn_index); + } + _ => stack_data_structure(), + } +} + +fn insert_wrapper_metadata<'a>( + arena: &'a Bump, + module: &mut WasmModule<'a>, + wrapper_name: &'static str, +) { + let index = (module.import.function_count() as u32) + + module.code.dead_import_dummy_count + + module.code.function_count; + + module.add_function_signature(Signature { + param_types: Vec::with_capacity_in(0, arena), + ret_type: Some(ValueType::I32), + }); + + module.export.append(Export { + name: wrapper_name, + ty: ExportType::Func, + index, + }); + + let linker_symbol = SymInfo::Function(WasmObjectSymbol::ExplicitlyNamed { + flags: 0, + index, + name: wrapper_name, + }); + module.linking.symbol_table.push(linker_symbol); +} + +macro_rules! build_wrapper_body_primitive { + ($store_instruction: ident, $align: expr) => { + fn build_wrapper_body(code_builder: &mut CodeBuilder, main_function_index: u32) { + let frame_pointer_id = LocalId(0); + let frame_pointer = Some(frame_pointer_id); + let local_types = &[ValueType::I32]; + let frame_size = 8; + + code_builder.get_local(frame_pointer_id); + code_builder.call(main_function_index); + code_builder.$store_instruction($align, 0); + code_builder.get_local(frame_pointer_id); + + code_builder.build_fn_header_and_footer(local_types, frame_size, frame_pointer); + } + }; +} + +macro_rules! wasm_result_primitive { + ($type_name: ident, $store_instruction: ident, $align: expr) => { + impl Wasm32Result for $type_name { + build_wrapper_body_primitive!($store_instruction, $align); + } + }; +} + +fn build_wrapper_body_stack_memory( + code_builder: &mut CodeBuilder, + main_function_index: u32, + size: usize, +) { + let local_id = LocalId(0); + let local_types = &[ValueType::I32]; + let frame_pointer = Some(local_id); + + code_builder.get_local(local_id); + code_builder.call(main_function_index); + code_builder.get_local(local_id); + code_builder.build_fn_header_and_footer(local_types, size as i32, frame_pointer); +} + +macro_rules! wasm_result_stack_memory { + ($type_name: ident) => { + impl Wasm32Result for $type_name { + fn build_wrapper_body(code_builder: &mut CodeBuilder, main_function_index: u32) { + build_wrapper_body_stack_memory( + code_builder, + main_function_index, + $type_name::ACTUAL_WIDTH, + ) + } + } + }; +} + +wasm_result_primitive!(bool, i32_store8, Align::Bytes1); +wasm_result_primitive!(RocOrder, i32_store8, Align::Bytes1); + +wasm_result_primitive!(u8, i32_store8, Align::Bytes1); +wasm_result_primitive!(i8, i32_store8, Align::Bytes1); +wasm_result_primitive!(u16, i32_store16, Align::Bytes2); +wasm_result_primitive!(i16, i32_store16, Align::Bytes2); +wasm_result_primitive!(u32, i32_store, Align::Bytes4); +wasm_result_primitive!(i32, i32_store, Align::Bytes4); +wasm_result_primitive!(char, i32_store, Align::Bytes4); +wasm_result_primitive!(u64, i64_store, Align::Bytes8); +wasm_result_primitive!(i64, i64_store, Align::Bytes8); +wasm_result_primitive!(usize, i32_store, Align::Bytes4); + +wasm_result_primitive!(f32, f32_store, Align::Bytes4); +wasm_result_primitive!(f64, f64_store, Align::Bytes8); + +wasm_result_stack_memory!(u128); +wasm_result_stack_memory!(i128); +wasm_result_stack_memory!(U128); +wasm_result_stack_memory!(I128); +wasm_result_stack_memory!(RocDec); + +impl Wasm32Result for RocStr { + fn build_wrapper_body(code_builder: &mut CodeBuilder, main_function_index: u32) { + build_wrapper_body_stack_memory(code_builder, main_function_index, 12) + } +} + +impl Wasm32Result for RocList { + fn build_wrapper_body(code_builder: &mut CodeBuilder, main_function_index: u32) { + build_wrapper_body_stack_memory(code_builder, main_function_index, 12) + } +} + +impl Wasm32Result for RocBox { + fn build_wrapper_body(code_builder: &mut CodeBuilder, main_function_index: u32) { + // treat box as if it's just a isize value + ::build_wrapper_body(code_builder, main_function_index) + } +} + +impl Wasm32Result for RocResult { + fn build_wrapper_body(code_builder: &mut CodeBuilder, main_function_index: u32) { + build_wrapper_body_stack_memory( + code_builder, + main_function_index, + Ord::max(T::ACTUAL_WIDTH, E::ACTUAL_WIDTH) + + Ord::max(T::ALIGN_OF_WASM, E::ALIGN_OF_WASM), + ) + } +} + +impl Wasm32Result for &'_ T { + build_wrapper_body_primitive!(i32_store, Align::Bytes4); +} + +impl Wasm32Result for [T; N] +where + T: Wasm32Result + Wasm32Sized, +{ + fn build_wrapper_body(code_builder: &mut CodeBuilder, main_function_index: u32) { + build_wrapper_body_stack_memory(code_builder, main_function_index, N * T::ACTUAL_WIDTH) + } +} + +impl Wasm32Result for () { + fn build_wrapper_body(code_builder: &mut CodeBuilder, main_function_index: u32) { + code_builder.call(main_function_index); + code_builder.get_global(0); + code_builder.build_fn_header_and_footer(&[], 0, None); + } +} + +impl Wasm32Result for std::convert::Infallible { + fn build_wrapper_body(code_builder: &mut CodeBuilder, main_function_index: u32) { + code_builder.call(main_function_index); + code_builder.get_global(0); + code_builder.build_fn_header_and_footer(&[], 0, None); + } +} + +impl Wasm32Result for (T, U) +where + T: Wasm32Result + Wasm32Sized, + U: Wasm32Result + Wasm32Sized, +{ + fn build_wrapper_body(code_builder: &mut CodeBuilder, main_function_index: u32) { + build_wrapper_body_stack_memory( + code_builder, + main_function_index, + T::ACTUAL_WIDTH + U::ACTUAL_WIDTH, + ) + } +} + +impl Wasm32Result for (T, U, V) +where + T: Wasm32Result + Wasm32Sized, + U: Wasm32Result + Wasm32Sized, + V: Wasm32Result + Wasm32Sized, +{ + fn build_wrapper_body(code_builder: &mut CodeBuilder, main_function_index: u32) { + build_wrapper_body_stack_memory( + code_builder, + main_function_index, + T::ACTUAL_WIDTH + U::ACTUAL_WIDTH + V::ACTUAL_WIDTH, + ) + } +} + +impl Wasm32Result for (T, U, V, W) +where + T: Wasm32Result + Wasm32Sized, + U: Wasm32Result + Wasm32Sized, + V: Wasm32Result + Wasm32Sized, + W: Wasm32Result + Wasm32Sized, +{ + fn build_wrapper_body(code_builder: &mut CodeBuilder, main_function_index: u32) { + build_wrapper_body_stack_memory( + code_builder, + main_function_index, + T::ACTUAL_WIDTH + U::ACTUAL_WIDTH + V::ACTUAL_WIDTH + W::ACTUAL_WIDTH, + ) + } +} diff --git a/crates/compiler/gen_wasm/src/wasm32_sized.rs b/crates/compiler/gen_wasm/src/wasm32_sized.rs new file mode 100644 index 0000000000..05ac6d95ec --- /dev/null +++ b/crates/compiler/gen_wasm/src/wasm32_sized.rs @@ -0,0 +1,115 @@ +use roc_std::{RocBox, RocDec, RocList, RocOrder, RocResult, RocStr, I128, U128}; + +pub trait Wasm32Sized: Sized { + const SIZE_OF_WASM: usize; + const ALIGN_OF_WASM: usize; + const ACTUAL_WIDTH: usize = + if (Self::ALIGN_OF_WASM == 0) || (Self::SIZE_OF_WASM % Self::ALIGN_OF_WASM) == 0 { + Self::SIZE_OF_WASM + } else { + Self::SIZE_OF_WASM + (Self::ALIGN_OF_WASM - (Self::SIZE_OF_WASM % Self::ALIGN_OF_WASM)) + }; +} + +macro_rules! wasm32_sized_primitive { + ($($type_name:ident ,)+) => { + $( + impl Wasm32Sized for $type_name { + const SIZE_OF_WASM: usize = core::mem::size_of::<$type_name>(); + const ALIGN_OF_WASM: usize = core::mem::align_of::<$type_name>(); + } + )* + } +} + +wasm32_sized_primitive!(u8, i8, u16, i16, u32, i32, char, u64, i64, u128, i128, f32, f64, bool,); +wasm32_sized_primitive!(RocDec, RocOrder, I128, U128,); + +impl Wasm32Sized for () { + const SIZE_OF_WASM: usize = 0; + const ALIGN_OF_WASM: usize = 0; +} + +impl Wasm32Sized for std::convert::Infallible { + const SIZE_OF_WASM: usize = 0; + const ALIGN_OF_WASM: usize = 0; +} + +impl Wasm32Sized for RocStr { + const SIZE_OF_WASM: usize = 12; + const ALIGN_OF_WASM: usize = 4; +} + +impl Wasm32Sized for RocList { + const SIZE_OF_WASM: usize = 12; + const ALIGN_OF_WASM: usize = 4; +} + +impl Wasm32Sized for RocBox { + const SIZE_OF_WASM: usize = 4; + const ALIGN_OF_WASM: usize = 4; +} + +impl Wasm32Sized for RocResult { + const ALIGN_OF_WASM: usize = max(&[T::ALIGN_OF_WASM, E::ALIGN_OF_WASM]); + const SIZE_OF_WASM: usize = max(&[T::ACTUAL_WIDTH, E::ACTUAL_WIDTH]) + 1; +} + +impl Wasm32Sized for &'_ T { + const SIZE_OF_WASM: usize = 4; + const ALIGN_OF_WASM: usize = 4; +} + +impl Wasm32Sized for [T; N] { + const SIZE_OF_WASM: usize = N * T::SIZE_OF_WASM; + const ALIGN_OF_WASM: usize = T::ALIGN_OF_WASM; +} + +impl Wasm32Sized for usize { + const SIZE_OF_WASM: usize = 4; + const ALIGN_OF_WASM: usize = 4; +} + +impl Wasm32Sized for isize { + const SIZE_OF_WASM: usize = 4; + const ALIGN_OF_WASM: usize = 4; +} + +impl Wasm32Sized for (T, U) { + const SIZE_OF_WASM: usize = T::SIZE_OF_WASM + U::SIZE_OF_WASM; + const ALIGN_OF_WASM: usize = max(&[T::ALIGN_OF_WASM, U::ALIGN_OF_WASM]); +} + +impl Wasm32Sized for (T, U, V) { + const SIZE_OF_WASM: usize = T::SIZE_OF_WASM + U::SIZE_OF_WASM + V::SIZE_OF_WASM; + const ALIGN_OF_WASM: usize = max(&[T::ALIGN_OF_WASM, U::ALIGN_OF_WASM, V::ALIGN_OF_WASM]); +} + +impl Wasm32Sized for (T, U, V, W) { + const SIZE_OF_WASM: usize = + T::SIZE_OF_WASM + U::SIZE_OF_WASM + V::SIZE_OF_WASM + W::SIZE_OF_WASM; + const ALIGN_OF_WASM: usize = max(&[ + T::ALIGN_OF_WASM, + U::ALIGN_OF_WASM, + V::ALIGN_OF_WASM, + W::ALIGN_OF_WASM, + ]); +} + +const fn max(alignments: &[usize]) -> usize { + assert!(!alignments.is_empty()); + + let mut largest = 0; + let mut i = 0; + while i < alignments.len() { + largest = if largest > alignments[i] { + largest + } else { + alignments[i] + }; + + i += 1; + } + + largest +} diff --git a/crates/compiler/ident/Cargo.toml b/crates/compiler/ident/Cargo.toml new file mode 100644 index 0000000000..0c53fa90a9 --- /dev/null +++ b/crates/compiler/ident/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "roc_ident" +description = "Implements data structures used for efficiently representing small strings, like identifiers." + +authors.workspace = true +edition.workspace = true +license.workspace = true +version.workspace = true diff --git a/crates/compiler/ident/src/lib.rs b/crates/compiler/ident/src/lib.rs new file mode 100644 index 0000000000..873e985b1d --- /dev/null +++ b/crates/compiler/ident/src/lib.rs @@ -0,0 +1,429 @@ +//! Implements data structures used for efficiently representing small strings, like identifiers. +#![warn(clippy::dbg_macro)] + +use core::cmp::Ordering; +use core::convert::From; +use core::{fmt, mem, ptr, slice}; +use std::alloc::{alloc, dealloc, Layout}; +use std::os::raw::c_char; + +/// A string which can store identifiers using the small string optimization. +/// It relies on the invariant that it cannot store null characters to store +/// an extra character; if the last byte is 0, that means it's a large string. +/// +/// Because the msbyte of the length is always 0, this can only store up to +/// 2^56 bytes on a 64-bit target, or 2^28 bytes in a 32-bit target. That's +/// way more than enough for an identifier! +/// +/// If it's a small string, that discriminant byte is used to store the length, +/// except it stores it as (255 - length) so that it will be in the range +/// 192 - 255 (all of which are invalid UTF-8 when in the final position of +/// a UTF-8 string). This design works on little-endian targets, but a different +/// design for storing length might be necessary on big-endian targets. + +#[repr(C)] +pub struct IdentStr { + elements: *const u8, + length: usize, +} + +impl IdentStr { + // Reserve 1 byte for the discriminant + const SMALL_STR_BYTES: usize = std::mem::size_of::() - 1; + + #[inline(always)] + pub const fn len(&self) -> usize { + let bytes = self.length.to_ne_bytes(); + let last_byte = bytes[mem::size_of::() - 1]; + + // We always perform this subtraction so that the following + // conditionals can all be cmov instructions. + let small_str_variable_len = (u8::MAX - last_byte) as usize; + + // The numbers 192 - 255 (0xC0 - 0xFF) are not valid as the final + // byte of a UTF-8 string. Hence they are unused and we can use them + // to store the length of a small string! + // + // Reference: https://en.wikipedia.org/wiki/UTF-8#Codepage_layout + if last_byte >= 0xC0 { + small_str_variable_len + } else if last_byte == 0 { + // This is a big string, so return its length. + self.length + } else { + // This is a valid UTF-8 character, meaning the entire struct must + // be in use for storing characters. + mem::size_of::() + } + } + + pub const fn is_empty(&self) -> bool { + self.length == 0 + } + + pub const fn is_small_str(&self) -> bool { + let bytes = self.length.to_ne_bytes(); + let last_byte = bytes[mem::size_of::() - 1]; + + last_byte != 0 + } + + pub fn get(&self, index: usize) -> Option<&u8> { + self.as_bytes().get(index) + } + + pub fn get_bytes(&self) -> *const u8 { + if self.is_small_str() { + self.get_small_str_ptr() + } else { + self.elements + } + } + + fn get_small_str_ptr(&self) -> *const u8 { + (self as *const IdentStr).cast() + } + + #[inline(always)] + const fn small_str_from_bytes(slice: &[u8]) -> Self { + assert!(slice.len() <= Self::SMALL_STR_BYTES); + + let len = slice.len(); + let mut bytes = [0; mem::size_of::()]; + + // Copy the bytes from the slice into bytes. + // while because for/Iterator does not work in const context + let mut i = 0; + while i < len { + bytes[i] = slice[i]; + i += 1; + } + + // Write length and small string bit to last byte of length. + bytes[Self::SMALL_STR_BYTES] = u8::MAX - len as u8; + + unsafe { mem::transmute::<[u8; mem::size_of::()], Self>(bytes) } + } + + #[allow(clippy::should_implement_trait)] + pub fn from_str(str: &str) -> Self { + let slice = str.as_bytes(); + let len = slice.len(); + + match len.cmp(&mem::size_of::()) { + Ordering::Less => Self::small_str_from_bytes(slice), + Ordering::Equal => { + // This fits in a small string, and is exactly long enough to + // take up the entire available struct + let mut bytes = [0; mem::size_of::()]; + + // Copy the bytes from the slice into the answer + bytes.copy_from_slice(slice); + + unsafe { mem::transmute::<[u8; mem::size_of::()], Self>(bytes) } + } + Ordering::Greater => { + // This needs a big string + let align = mem::align_of::(); + let elements = unsafe { + let layout = Layout::from_size_align_unchecked(len, align); + alloc(layout) + }; + + // Turn the new elements into a slice, and copy the existing + // slice's bytes into it. + unsafe { + let dest_slice = slice::from_raw_parts_mut(elements, len); + + dest_slice.copy_from_slice(slice); + } + + Self { + length: len, + elements, + } + } + } + } + + #[inline(always)] + pub fn as_slice(&self) -> &[u8] { + use core::slice::from_raw_parts; + + if self.is_empty() { + &[] + } else if self.is_small_str() { + unsafe { from_raw_parts(self.get_small_str_ptr(), self.len()) } + } else { + unsafe { from_raw_parts(self.elements, self.length) } + } + } + + #[inline(always)] + pub fn as_str(&self) -> &str { + let slice = self.as_slice(); + + unsafe { core::str::from_utf8_unchecked(slice) } + } + + /// Write a CStr (null-terminated) representation of this IdentStr into + /// the given buffer. + /// + /// # Safety + /// This assumes the given buffer has enough space, so make sure you only + /// pass in a pointer to an allocation that's at least as long as this Str! + pub unsafe fn write_c_str(&self, buf: *mut c_char) { + let bytes = self.as_bytes(); + ptr::copy_nonoverlapping(bytes.as_ptr().cast(), buf, bytes.len()); + + // null-terminate + *buf.add(self.len()) = 0; + } +} + +impl Default for IdentStr { + fn default() -> Self { + Self { + length: 0, + elements: core::ptr::null_mut(), + } + } +} + +impl std::ops::Deref for IdentStr { + type Target = str; + + fn deref(&self) -> &Self::Target { + self.as_str() + } +} + +impl From<&str> for IdentStr { + fn from(str: &str) -> Self { + Self::from_str(str) + } +} + +impl From for String { + fn from(ident_str: IdentStr) -> Self { + if ident_str.is_small_str() { + // Copy it to a heap allocation + ident_str.as_str().to_string() + } else { + // Reuse the existing heap allocation + let string = unsafe { + String::from_raw_parts( + ident_str.as_ptr() as *mut u8, + ident_str.len(), + ident_str.len(), + ) + }; + + // Make sure not to drop the IdentStr, since now there's + // a String referencing its heap-allocated contents. + std::mem::forget(ident_str); + + string + } + } +} + +impl From for IdentStr { + fn from(string: String) -> Self { + if string.len() <= Self::SMALL_STR_BYTES { + Self::from_str(string.as_str()) + } else { + // Take over the string's heap allocation + let length = string.len(); + let elements = string.as_ptr(); + + // Make sure the existing string doesn't get dropped. + std::mem::forget(string); + + Self { elements, length } + } + } +} + +impl fmt::Debug for IdentStr { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + // IdentStr { is_small_str: false, storage: Refcounted(3), elements: [1,2,3,4] } + f.debug_struct("IdentStr") + //.field("is_small_str", &self.is_small_str()) + .field("string", &self.as_str()) + //.field("elements", &self.as_slice()) + .finish() + } +} + +impl fmt::Display for IdentStr { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + // IdentStr { is_small_str: false, storage: Refcounted(3), elements: [1,2,3,4] } + f.write_str(self.as_str()) + } +} + +unsafe impl std::marker::Sync for IdentStr {} +unsafe impl std::marker::Send for IdentStr {} + +impl PartialEq for IdentStr { + fn eq(&self, other: &Self) -> bool { + self.as_slice() == other.as_slice() + } +} + +impl Eq for IdentStr {} + +impl PartialOrd for IdentStr { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for IdentStr { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.as_str().cmp(other.as_str()) + } +} + +impl std::hash::Hash for IdentStr { + fn hash(&self, hasher: &mut H) + where + H: std::hash::Hasher, + { + self.as_str().hash(hasher) + } +} + +impl Clone for IdentStr { + fn clone(&self) -> Self { + if self.is_empty() || self.is_small_str() { + // we can just copy the bytes + Self { + elements: self.elements, + length: self.length, + } + } else { + Self::from_str(self.as_str()) + } + } +} + +impl Drop for IdentStr { + fn drop(&mut self) { + if !self.is_empty() && !self.is_small_str() { + let align = mem::align_of::(); + unsafe { + let layout = Layout::from_size_align_unchecked(self.length, align); + dealloc(self.elements as *mut _, layout); + } + } + } +} + +#[test] +fn default() { + let answer = IdentStr::default(); + + assert_eq!(answer.len(), 0); + assert_eq!(answer, answer); + assert_eq!(answer.clone(), answer); + assert_eq!(answer, answer); + assert_eq!(answer.as_str(), ""); + assert_eq!(answer.as_str(), ""); +} + +#[test] +fn big_str() { + for &string in &[ + "0123456789abcdefg", + "0123456789abcdefgh", + "0123456789abcdefghi", + ] { + let answer = IdentStr::from(string); + + assert_eq!(answer.len(), string.len()); + assert_eq!(answer, answer); + assert_eq!(answer.clone(), answer); + assert_eq!(answer.clone(), answer.clone()); + assert_eq!(answer.as_str(), string); + assert_eq!(answer.clone().as_str(), string); + } +} + +#[cfg(target_pointer_width = "64")] +#[test] +fn small_var_length() { + for &string in &[ + "", + "0", + "01", + "012", + "0123", + "01234", + "012345", + "0123456", + "01234567", + "012345678", + "0123456789", + "0123456789a", + "0123456789ab", + "0123456789abc", + "0123456789abcd", + "0123456789abcde ", + ] { + let answer = IdentStr::from(string); + + assert_eq!(answer.len(), string.len()); + assert_eq!(answer, answer); + assert_eq!(answer.clone(), answer); + assert_eq!(answer.clone(), answer.clone()); + assert_eq!(answer.as_str(), string); + assert_eq!(answer.clone().as_str(), string); + } +} + +#[cfg(target_pointer_width = "32")] +#[test] +fn small_var_length() { + for &string in &[ + "", "0", "01", "012", "0123", "01234", "012345", "0123456", "01234567", + ] { + let answer = IdentStr::from(string); + + assert_eq!(answer.len(), string.len()); + assert_eq!(answer, answer); + assert_eq!(answer.clone(), answer); + assert_eq!(answer.clone(), answer.clone()); + assert_eq!(answer.as_str(), string); + assert_eq!(answer.clone().as_str(), string); + } +} + +#[cfg(target_pointer_width = "64")] +#[test] +fn small_max_length() { + let string = "0123456789abcdef"; + let answer = IdentStr::from(string); + + assert_eq!(answer.len(), string.len()); + assert_eq!(answer, answer); + assert_eq!(answer.clone(), answer); + assert_eq!(answer, answer); + assert_eq!(answer.as_str(), string); + assert_eq!(answer.as_str(), string); +} + +#[cfg(target_pointer_width = "32")] +#[test] +fn small_max_length() { + let string = "01234567"; + let answer = IdentStr::from(string); + + assert_eq!(answer.len(), string.len()); + assert_eq!(answer, answer); + assert_eq!(answer.clone(), answer); + assert_eq!(answer.clone(), answer.clone()); + assert_eq!(answer.as_str(), string); + assert_eq!(answer.clone().as_str(), string); +} diff --git a/crates/compiler/late_solve/Cargo.toml b/crates/compiler/late_solve/Cargo.toml new file mode 100644 index 0000000000..6b1a2a5311 --- /dev/null +++ b/crates/compiler/late_solve/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "roc_late_solve" +description = "Provides type unification and solving primitives from the perspective of the compiler backend." + +authors.workspace = true +edition.workspace = true +license.workspace = true +version.workspace = true + +[dependencies] +roc_can = { path = "../can" } +roc_checkmate = { path = "../checkmate" } +roc_collections = { path = "../collections" } +roc_derive = { path = "../derive" } +roc_error_macros = { path = "../../error_macros" } +roc_module = { path = "../module" } +roc_solve = { path = "../solve" } +roc_solve_schema = { path = "../solve_schema" } +roc_types = { path = "../types" } +roc_unify = { path = "../unify" } + +bumpalo.workspace = true diff --git a/crates/compiler/late_solve/src/lib.rs b/crates/compiler/late_solve/src/lib.rs new file mode 100644 index 0000000000..a03c9f3cde --- /dev/null +++ b/crates/compiler/late_solve/src/lib.rs @@ -0,0 +1,420 @@ +//! Crate roc_late_solve exposes type unification and solving primitives from the perspective of +//! the compiler backend. + +use std::sync::{Arc, RwLock}; + +use bumpalo::Bump; +use roc_can::abilities::AbilitiesStore; +use roc_can::module::ExposedByModule; +use roc_checkmate::with_checkmate; +use roc_collections::MutMap; +use roc_derive::SharedDerivedModule; +use roc_error_macros::internal_error; +use roc_module::symbol::ModuleId; +use roc_module::symbol::Symbol; +use roc_solve::ability::AbilityResolver; +use roc_solve::specialize::{compact_lambda_sets_of_vars, Phase}; +use roc_solve::Pools; +use roc_solve::{DerivedEnv, SolveEnv}; +use roc_solve_schema::UnificationMode; +use roc_types::subs::{get_member_lambda_sets_at_region, Content, FlatType, LambdaSet}; +use roc_types::subs::{ExposedTypesStorageSubs, Subs, Variable}; +use roc_types::types::Polarity; +use roc_unify::unify::MetaCollector; +use roc_unify::unify::Unified; +use roc_unify::Env as UEnv; + +pub use roc_solve::ability::{ResolveError, Resolved}; +pub use roc_types::subs::instantiate_rigids; + +pub mod storage; + +#[derive(Debug)] +pub struct UnificationFailed; + +/// A global view of all abilities across all modules in a program under compilation. +/// [WorldAbilities::insert] adds a solved abilities store for a module to the global map. +/// Use [WorldAbilities::clone_ref] to get a thread-safe, reference-counted copy of the global map. +/// Note that this is indeed a map shared amongst everything during a compilation. +#[derive(Default, Debug)] +pub struct WorldAbilities { + world: Arc>>, +} + +impl WorldAbilities { + pub fn insert( + &mut self, + module: ModuleId, + store: AbilitiesStore, + exposed_types: ExposedTypesStorageSubs, + ) { + let old_store = self + .world + .write() + .unwrap() + .insert(module, (store, exposed_types)); + + debug_assert!(old_store.is_none(), "{module:?} abilities not new"); + } + + #[inline(always)] + pub fn with_module_exposed_type( + &self, + module: ModuleId, + mut f: impl FnMut(&ExposedTypesStorageSubs) -> T, + ) -> T { + let world = self.world.read().unwrap(); + let (_, exposed_types) = world.get(&module).expect("module not in the world"); + + f(exposed_types) + } + + #[inline(always)] + pub fn with_module_abilities_store(&self, module: ModuleId, mut f: F) -> T + where + F: FnMut(&AbilitiesStore) -> T, + { + let world = self.world.read().unwrap(); + let (module_store, _module_types) = world + .get(&module) + .unwrap_or_else(|| internal_error!("Module {:?} not found in world abilities", module)); + f(module_store) + } + + pub fn clone_ref(&self) -> Self { + Self { + world: Arc::clone(&self.world), + } + } +} + +pub enum AbilitiesView<'a> { + World(&'a WorldAbilities), + Module(&'a AbilitiesStore), +} + +impl AbilitiesView<'_> { + #[inline(always)] + pub fn with_module_abilities_store(&self, module: ModuleId, mut f: F) -> T + where + F: FnMut(&AbilitiesStore) -> T, + { + match self { + AbilitiesView::World(wa) => wa.with_module_abilities_store(module, f), + AbilitiesView::Module(store) => f(store), + } + } +} + +pub struct LateResolver<'a> { + home: ModuleId, + abilities: &'a AbilitiesView<'a>, +} + +impl<'a> AbilityResolver for LateResolver<'a> { + fn member_parent_and_signature_var( + &self, + ability_member: roc_module::symbol::Symbol, + home_subs: &mut Subs, + ) -> Option<(roc_module::symbol::Symbol, Variable)> { + let (parent_ability, signature_var) = + self.abilities + .with_module_abilities_store(ability_member.module_id(), |store| { + store + .member_def(ability_member) + .map(|def| (def.parent_ability, def.signature_var())) + })?; + + let parent_ability_module = parent_ability.module_id(); + debug_assert_eq!(parent_ability_module, ability_member.module_id()); + + let signature_var = match (parent_ability_module == self.home, self.abilities) { + (false, AbilitiesView::World(world)) => { + // Need to copy the type from an external module into our home subs + world.with_module_exposed_type(parent_ability_module, |external_types| { + let stored_signature_var = + external_types.stored_ability_member_vars.get(&signature_var).expect("Ability member is in an external store, but its signature variables are not stored accordingly!"); + + let home_copy = external_types + .storage_subs + .export_variable_to(home_subs, *stored_signature_var); + + home_copy.variable + }) + } + _ => signature_var, + }; + + Some((parent_ability, signature_var)) + } + + fn get_implementation( + &self, + impl_key: roc_can::abilities::ImplKey, + ) -> Option { + self.abilities + .with_module_abilities_store(impl_key.opaque.module_id(), |store| { + store.get_implementation(impl_key).copied() + }) + } +} + +pub fn resolve_ability_specialization( + home: ModuleId, + subs: &mut Subs, + abilities: &AbilitiesView, + ability_member: Symbol, + specialization_var: Variable, +) -> Result { + let late_resolver = LateResolver { home, abilities }; + roc_solve::ability::resolve_ability_specialization( + subs, + &late_resolver, + ability_member, + specialization_var, + ) +} + +pub struct LatePhase<'a> { + home: ModuleId, + abilities: &'a AbilitiesView<'a>, +} + +impl Phase for LatePhase<'_> { + const IS_LATE: bool = true; + + #[inline(always)] + fn with_module_abilities_store(&self, module: ModuleId, f: F) -> T + where + F: FnMut(&AbilitiesStore) -> T, + { + self.abilities.with_module_abilities_store(module, f) + } + + #[inline(always)] + fn copy_lambda_set_ambient_function_to_home_subs( + &self, + external_lambda_set_var: Variable, + external_module_id: ModuleId, + target_subs: &mut Subs, + ) -> Variable { + match (external_module_id == self.home, self.abilities) { + (true, _) | (false, AbilitiesView::Module(_)) => { + // The lambda set (and hence its ambient function) should be available in the + // current subs. + let LambdaSet { + ambient_function, .. + } = target_subs.get_lambda_set(external_lambda_set_var); + ambient_function + } + (false, AbilitiesView::World(wa)) => { + let mut world = wa.world.write().unwrap(); + let (_module_store, module_types) = world.get_mut(&external_module_id).unwrap(); + + let storage_lambda_set_var = *module_types + .stored_specialization_lambda_set_vars + .get(&external_lambda_set_var) + .unwrap(); + let LambdaSet { + ambient_function, .. + } = module_types + .storage_subs + .as_inner() + .get_lambda_set(storage_lambda_set_var); + + let copied = module_types + .storage_subs + // TODO: I think this is always okay, but revisit later when we're in a more + // stable position to see if we can get rid of the bookkeeping done as a result + // of this. + .export_variable_to_directly_to_use_site(target_subs, ambient_function); + let our_ambient_function_var = copied.variable; + + debug_assert!(matches!( + target_subs.get_content_without_compacting(our_ambient_function_var), + Content::Structure(FlatType::Func(..)) + )); + + our_ambient_function_var + } + } + } + + #[inline(always)] + fn get_and_copy_ability_member_ambient_function( + &self, + ability_member: roc_module::symbol::Symbol, + region: u8, + target_subs: &mut Subs, + ) -> Variable { + match self.abilities { + AbilitiesView::Module(abilities_store) => { + // No option other than that the var must be in our module store. + // Even if the specialization lambda set comes from another module, + // we should have taken care to import it before starting solving in this module. + let member_def = abilities_store + .member_def(ability_member) + .unwrap_or_else(|| { + internal_error!( + "{:?} is not resolved, or not an ability member!", + ability_member + ) + }); + let member_var = member_def.signature_var(); + + let region_lset = get_member_lambda_sets_at_region(target_subs, member_var, region); + + let LambdaSet { + ambient_function, .. + } = target_subs.get_lambda_set(region_lset); + + ambient_function + } + AbilitiesView::World(wa) => { + let member_home = ability_member.module_id(); + let mut world = wa.world.write().unwrap(); + let (module_store, module_types) = world.get_mut(&member_home).unwrap(); + + let member_def = module_store.member_def(ability_member).unwrap_or_else(|| { + internal_error!( + "{:?} is not resolved, or not an ability member!", + ability_member + ) + }); + let member_var = member_def.signature_var(); + let storage_member_var = module_types + .stored_ability_member_vars + .get(&member_var) + .unwrap(); + + let storage_lambda_set_var = get_member_lambda_sets_at_region( + module_types.storage_subs.as_inner(), + *storage_member_var, + region, + ); + + let LambdaSet { + ambient_function, .. + } = module_types + .storage_subs + .as_inner() + .get_lambda_set(storage_lambda_set_var); + + let copied = module_types + .storage_subs + // TODO: I think this is always okay, but revisit later when we're in a more + // stable position to see if we can get rid of the bookkeeping done as a result + // of this. + .export_variable_to_directly_to_use_site(target_subs, ambient_function); + + let our_ambient_function_var = copied.variable; + instantiate_rigids(target_subs, our_ambient_function_var); + + debug_assert!(matches!( + target_subs.get_content_without_compacting(our_ambient_function_var), + Content::Structure(FlatType::Func(..)) + )); + + our_ambient_function_var + } + } + } +} + +#[derive(Debug, Default)] +struct ChangedVariableCollector { + changed: Vec, +} + +impl MetaCollector for ChangedVariableCollector { + const UNIFYING_SPECIALIZATION: bool = false; + const IS_LATE: bool = true; + + #[inline(always)] + fn record_specialization_lambda_set(&mut self, _member: Symbol, _region: u8, _var: Variable) {} + + #[inline(always)] + fn record_changed_variable(&mut self, subs: &Subs, var: Variable) { + self.changed.push(subs.get_root_key_without_compacting(var)) + } + + #[inline(always)] + fn union(&mut self, other: Self) { + self.changed.extend(other.changed) + } +} + +/// Unifies two variables and performs lambda set compaction. +/// Ranks and other ability demands are disregarded. +#[allow(clippy::too_many_arguments)] +pub fn unify( + home: ModuleId, + arena: &Bump, + subs: &mut Subs, + abilities: &AbilitiesView, + derived_module: &SharedDerivedModule, + exposed_by_module: &ExposedByModule, + left: Variable, + right: Variable, +) -> Result, UnificationFailed> { + debug_assert_ne!( + home, + ModuleId::DERIVED_SYNTH, + "derived module can only unify its subs in its own context!" + ); + let unified = roc_unify::unify::unify_with_collector::( + // TODO(checkmate): pass checkmate through + &mut with_checkmate!({ + on => UEnv::new(subs, None), + off => UEnv::new(subs), + }), + left, + right, + UnificationMode::EQ, + Polarity::Pos, + ); + + match unified { + Unified::Success { + vars: _, + must_implement_ability: _, + lambda_sets_to_specialize, + extra_metadata, + } => { + let mut pools = Pools::default(); + + let late_phase = LatePhase { home, abilities }; + let derived_env = DerivedEnv { + derived_module, + exposed_types: exposed_by_module, + }; + + let must_implement_constraints = { + let mut env = SolveEnv { + subs, + derived_env: &derived_env, + arena, + pools: &mut pools, + + #[cfg(debug_assertions)] + checkmate: &mut None, + }; + + compact_lambda_sets_of_vars(&mut env, lambda_sets_to_specialize, &late_phase) + }; + + // At this point we can't do anything with must-implement constraints, since we're no + // longer solving. We must assume that they were totally caught during solving. + // After we land https://github.com/roc-lang/roc/issues/3207 this concern should totally + // go away. + let _ = must_implement_constraints; + // Pools are only used to keep track of variable ranks for generalization purposes. + // Since we break generalization during monomorphization, `pools` is irrelevant + // here. We only need it for `compact_lambda_sets_of_vars`, which is also used in a + // solving context where pools are relevant. + + Ok(extra_metadata.changed) + } + Unified::Failure(..) => Err(UnificationFailed), + } +} diff --git a/crates/compiler/late_solve/src/storage.rs b/crates/compiler/late_solve/src/storage.rs new file mode 100644 index 0000000000..d39292b248 --- /dev/null +++ b/crates/compiler/late_solve/src/storage.rs @@ -0,0 +1,75 @@ +use roc_types::subs::StorageSubs; +use roc_types::subs::{storage_copy_var_to, Subs, Variable, VariableMapCache}; +use std::iter::Iterator; + +/// Storage for types to be sent to an external module, and written to only by one module's subs. +/// Maintains a cache so that independent writes can re-use types previously inserted into the +/// storage. +#[derive(Clone, Debug)] +pub struct ExternalModuleStorage { + storage: StorageSubs, + /// Variable they expose -> variable we record into storage + variable_mapping_cache: VariableMapCache, +} + +pub struct ExternalModuleStorageSnapshot { + mapping_cache_len: usize, +} + +impl ExternalModuleStorage { + pub fn new(subs: Subs) -> Self { + Self { + storage: StorageSubs::new(subs), + variable_mapping_cache: VariableMapCache::default(), + } + } + + pub fn extend_with_variable(&mut self, source: &Subs, variable: Variable) -> Variable { + storage_copy_var_to( + &mut self.variable_mapping_cache, + source, + self.storage.as_inner_mut(), + variable, + ) + } + + pub fn into_storage_subs(self) -> StorageSubs { + self.storage + } + + /// Invalidates the whole cache given a sequence of variables that should no longer be indexed + /// from the cache. + pub fn invalidate_cache(&mut self, changed_variables: &[Variable]) { + for var in changed_variables { + for cache in self.variable_mapping_cache.0.iter_mut().rev() { + cache.remove(var); + } + } + } + + /// Invalidates the whole cache. + /// Should only be called if you need to invalidate the cache but don't have a snapshot. + /// Generally you should prefer to create a snapshot and invalidate that snapshot, which avoids + /// unnecessary cache invalidation. + pub fn invalidate_whole_cache(&mut self) { + debug_assert_eq!(self.variable_mapping_cache.0.len(), 1); + self.variable_mapping_cache.0.last_mut().unwrap().clear(); + } + + /// Creates a snapshot of the cache, making it suitable for new ephemeral entries. + /// The cache can be rolled back to the state it was in prior to the snapshot with [rollback_cache]. + pub fn snapshot_cache(&mut self) -> ExternalModuleStorageSnapshot { + self.variable_mapping_cache.0.push(Default::default()); + ExternalModuleStorageSnapshot { + mapping_cache_len: self.variable_mapping_cache.0.len(), + } + } + + pub fn rollback_cache(&mut self, snapshot: ExternalModuleStorageSnapshot) { + debug_assert_eq!( + self.variable_mapping_cache.0.len(), + snapshot.mapping_cache_len + ); + self.variable_mapping_cache.0.pop(); + } +} diff --git a/crates/compiler/load/Cargo.toml b/crates/compiler/load/Cargo.toml new file mode 100644 index 0000000000..477c96c5ea --- /dev/null +++ b/crates/compiler/load/Cargo.toml @@ -0,0 +1,50 @@ +[package] +name = "roc_load" +description = "Used to load a .roc file and coordinate the compiler pipeline, including parsing, type checking, and code generation." + +authors.workspace = true +edition.workspace = true +license.workspace = true +version.workspace = true + +[dependencies] +roc_can = { path = "../can" } +roc_collections = { path = "../collections" } +roc_load_internal = { path = "../load_internal" } +roc_module = { path = "../module" } +roc_packaging = { path = "../../packaging" } +roc_reporting = { path = "../../reporting" } +roc_solve = { path = "../solve" } +roc_target = { path = "../roc_target" } +roc_types = { path = "../types" } + +bumpalo.workspace = true + +[build-dependencies] +roc_builtins = { path = "../builtins" } +roc_can = { path = "../can" } +roc_module = { path = "../module" } +roc_packaging = { path = "../../packaging" } +roc_reporting = { path = "../../reporting" } +roc_solve = { path = "../solve" } +roc_target = { path = "../roc_target" } +roc_error_macros = { path = "../../error_macros" } + +bumpalo.workspace = true + +[target.'cfg(not(windows))'.build-dependencies] +roc_load_internal = { path = "../load_internal" } + +[dev-dependencies] +roc_constrain = { path = "../constrain" } +roc_derive = { path = "../derive" } +roc_parse = { path = "../parse" } +roc_problem = { path = "../problem" } +roc_region = { path = "../region" } +roc_solve_problem = { path = "../solve_problem" } +ven_pretty = { path = "../../vendor/pretty" } +roc_test_utils = { path = "../../test_utils" } + +indoc.workspace = true +insta.workspace = true +pretty_assertions.workspace = true diff --git a/crates/compiler/load/build.rs b/crates/compiler/load/build.rs new file mode 100644 index 0000000000..bb963c50b4 --- /dev/null +++ b/crates/compiler/load/build.rs @@ -0,0 +1,133 @@ +use std::path::{Path, PathBuf}; + +#[cfg(not(windows))] +use bumpalo::Bump; +use roc_error_macros::internal_error; +use roc_module::symbol::ModuleId; + +const SKIP_SUBS_CACHE: bool = { + match option_env!("ROC_SKIP_SUBS_CACHE") { + Some(s) => s.len() == 1 && s.as_bytes()[0] == b'1', + None => false, + } +}; + +// IFTTT: crates/compiler/load/src/lib.rs +const MODULES: &[(ModuleId, &str)] = &[ + (ModuleId::BOOL, "Bool.roc"), + (ModuleId::DICT, "Dict.roc"), + (ModuleId::SET, "Set.roc"), + (ModuleId::RESULT, "Result.roc"), + (ModuleId::NUM, "Num.roc"), + (ModuleId::LIST, "List.roc"), + (ModuleId::STR, "Str.roc"), + (ModuleId::BOX, "Box.roc"), + (ModuleId::ENCODE, "Encode.roc"), + (ModuleId::DECODE, "Decode.roc"), + (ModuleId::HASH, "Hash.roc"), + (ModuleId::INSPECT, "Inspect.roc"), + (ModuleId::JSON, "TotallyNotJson.roc"), +]; + +fn main() { + for (module_id, filename) in MODULES { + write_subs_for_module(*module_id, filename); + } +} + +fn write_subs_for_module(module_id: ModuleId, filename: &str) { + // Tell Cargo that if the given file changes, to rerun this build script. + let filepath = PathBuf::from("..") + .join("builtins") + .join("roc") + .join(filename); + println!("cargo:rerun-if-changed={}", filepath.to_str().unwrap()); + + let mut output_path = PathBuf::from(std::env::var("OUT_DIR").unwrap()); + output_path.extend([filename]); + output_path.set_extension("dat"); + + #[cfg(not(windows))] + if SKIP_SUBS_CACHE { + write_types_for_module_dummy(&output_path) + } else { + write_types_for_module_real(module_id, filename, &output_path) + } + + #[cfg(windows)] + { + let _ = SKIP_SUBS_CACHE; + let _ = module_id; + write_types_for_module_dummy(&output_path) + } +} + +fn write_types_for_module_dummy(output_path: &Path) { + // write out a dummy file + std::fs::write(output_path, []).unwrap(); +} + +#[cfg(not(windows))] +fn write_types_for_module_real(module_id: ModuleId, filename: &str, output_path: &Path) { + use roc_can::module::TypeState; + use roc_load_internal::file::{LoadingProblem, Threading}; + use roc_packaging::cache::RocCacheDir; + use roc_reporting::cli::report_problems; + + let arena = Bump::new(); + let cwd = std::env::current_dir().unwrap(); + let source = roc_builtins::roc::module_source(module_id); + let target_info = roc_target::TargetInfo::default_x86_64(); + let function_kind = roc_solve::FunctionKind::LambdaSet; + + let res_module = roc_load_internal::file::load_and_typecheck_str( + &arena, + PathBuf::from(filename), + source, + cwd, + Default::default(), + target_info, + function_kind, + roc_reporting::report::RenderTarget::ColorTerminal, + roc_reporting::report::DEFAULT_PALETTE, + RocCacheDir::Disallowed, + Threading::AllAvailable, + ); + + let mut module = match res_module { + Ok(v) => v, + Err(LoadingProblem::FormattedReport(report)) => { + internal_error!("{}", report); + } + Err(other) => { + internal_error!("build_file failed with error:\n{:?}", other); + } + }; + + let problems = report_problems( + &module.sources, + &module.interns, + &mut module.can_problems, + &mut module.type_problems, + ); + + if problems.errors + problems.warnings > 0 { + internal_error!("Problems were found! Refusing to build cached subs."); + } + + let subs = module.solved.into_inner(); + let exposed_vars_by_symbol: Vec<_> = module.exposed_to_host.into_iter().collect(); + let abilities = module.abilities_store; + let solved_implementations = module.resolved_implementations; + + let mut file = std::fs::File::create(output_path).unwrap(); + + let type_state = TypeState { + subs, + exposed_vars_by_symbol, + abilities, + solved_implementations, + }; + + type_state.serialize(&mut file).unwrap(); +} diff --git a/crates/compiler/load/src/lib.rs b/crates/compiler/load/src/lib.rs new file mode 100644 index 0000000000..b1dc5665be --- /dev/null +++ b/crates/compiler/load/src/lib.rs @@ -0,0 +1,271 @@ +//! Used to load a .roc file and coordinate the compiler pipeline, including +//! parsing, type checking, and [code generation](https://en.wikipedia.org/wiki/Code_generation_(compiler)). +use bumpalo::Bump; +use roc_can::module::{ExposedByModule, TypeState}; +use roc_collections::all::MutMap; +use roc_module::symbol::ModuleId; +use roc_packaging::cache::RocCacheDir; +use roc_reporting::report::{Palette, RenderTarget}; +use roc_target::TargetInfo; +use std::path::PathBuf; + +const SKIP_SUBS_CACHE: bool = { + match option_env!("ROC_SKIP_SUBS_CACHE") { + Some(s) => s.len() == 1 && s.as_bytes()[0] == b'1', + None => false, + } +}; + +pub use roc_load_internal::docs; +pub use roc_load_internal::file::{ + ExecutionMode, ExpectMetadata, LoadConfig, LoadResult, LoadStart, LoadingProblem, Phase, + Threading, +}; +pub use roc_load_internal::module::{ + CheckedModule, EntryPoint, Expectations, ExposedToHost, LoadedModule, MonomorphizedModule, +}; +pub use roc_solve::FunctionKind; + +#[allow(clippy::too_many_arguments)] +fn load<'a>( + arena: &'a Bump, + load_start: LoadStart<'a>, + exposed_types: ExposedByModule, + roc_cache_dir: RocCacheDir<'_>, + load_config: LoadConfig, +) -> Result, LoadingProblem<'a>> { + let cached_types = read_cached_types(); + + roc_load_internal::file::load( + arena, + load_start, + exposed_types, + cached_types, + roc_cache_dir, + load_config, + ) +} + +/// Load using only a single thread; used when compiling to webassembly +#[allow(clippy::too_many_arguments)] +pub fn load_single_threaded<'a>( + arena: &'a Bump, + load_start: LoadStart<'a>, + target_info: TargetInfo, + function_kind: FunctionKind, + render: RenderTarget, + palette: Palette, + roc_cache_dir: RocCacheDir<'_>, + exec_mode: ExecutionMode, +) -> Result, LoadingProblem<'a>> { + let cached_subs = read_cached_types(); + let exposed_types = ExposedByModule::default(); + + roc_load_internal::file::load_single_threaded( + arena, + load_start, + exposed_types, + target_info, + function_kind, + cached_subs, + render, + palette, + exec_mode, + roc_cache_dir, + ) +} + +#[derive(Debug)] +#[allow(clippy::large_enum_variant)] +pub enum LoadMonomorphizedError<'a> { + LoadingProblem(LoadingProblem<'a>), + /// Errors in the module that should be reported, without compiling the executable. + /// Relevant in check-and-then-build mode. + ErrorModule(LoadedModule), +} + +impl<'a> From> for LoadMonomorphizedError<'a> { + fn from(problem: LoadingProblem<'a>) -> Self { + Self::LoadingProblem(problem) + } +} + +// HACK only relevant because of some uses of `map_err` that decay into this error, but call `todo` - +// rustc seems to be unhappy with that. +impl<'a> From<()> for LoadMonomorphizedError<'a> { + fn from(_: ()) -> Self { + todo!() + } +} + +#[allow(clippy::too_many_arguments)] +pub fn load_and_monomorphize_from_str<'a>( + arena: &'a Bump, + filename: PathBuf, + src: &'a str, + src_dir: PathBuf, + roc_cache_dir: RocCacheDir<'_>, + load_config: LoadConfig, +) -> Result, LoadMonomorphizedError<'a>> { + use LoadResult::*; + + let load_start = LoadStart::from_str(arena, filename, src, roc_cache_dir, src_dir)?; + let exposed_types = ExposedByModule::default(); + + match load(arena, load_start, exposed_types, roc_cache_dir, load_config)? { + Monomorphized(module) => Ok(module), + TypeChecked(module) => Err(LoadMonomorphizedError::ErrorModule(module)), + } +} + +pub fn load_and_monomorphize<'a>( + arena: &'a Bump, + filename: PathBuf, + roc_cache_dir: RocCacheDir<'_>, + load_config: LoadConfig, +) -> Result, LoadMonomorphizedError<'a>> { + use LoadResult::*; + + let load_start = LoadStart::from_path( + arena, + filename, + load_config.render, + roc_cache_dir, + load_config.palette, + )?; + + let exposed_types = ExposedByModule::default(); + + match load(arena, load_start, exposed_types, roc_cache_dir, load_config)? { + Monomorphized(module) => Ok(module), + TypeChecked(module) => Err(LoadMonomorphizedError::ErrorModule(module)), + } +} + +pub fn load_and_typecheck<'a>( + arena: &'a Bump, + filename: PathBuf, + roc_cache_dir: RocCacheDir<'_>, + load_config: LoadConfig, +) -> Result> { + use LoadResult::*; + + let load_start = LoadStart::from_path( + arena, + filename, + load_config.render, + roc_cache_dir, + load_config.palette, + )?; + + let exposed_types = ExposedByModule::default(); + + match load(arena, load_start, exposed_types, roc_cache_dir, load_config)? { + Monomorphized(_) => unreachable!(""), + TypeChecked(module) => Ok(module), + } +} + +#[allow(clippy::too_many_arguments)] +pub fn load_and_typecheck_str<'a>( + arena: &'a Bump, + filename: PathBuf, + source: &'a str, + src_dir: PathBuf, + target_info: TargetInfo, + function_kind: FunctionKind, + render: RenderTarget, + roc_cache_dir: RocCacheDir<'_>, + palette: Palette, +) -> Result> { + use LoadResult::*; + + let load_start = LoadStart::from_str(arena, filename, source, roc_cache_dir, src_dir)?; + + // NOTE: this function is meant for tests, and so we use single-threaded + // solving so we don't use too many threads per-test. That gives higher + // throughput for the test run overall + match load_single_threaded( + arena, + load_start, + target_info, + function_kind, + render, + palette, + roc_cache_dir, + ExecutionMode::Check, + )? { + Monomorphized(_) => unreachable!(""), + TypeChecked(module) => Ok(module), + } +} + +macro_rules! include_bytes_align_as { + ($align_ty:ty, $path:expr) => {{ + // const block expression to encapsulate the static + + #[repr(C)] + pub struct AlignedAs { + pub _align: [Align; 0], + pub bytes: Bytes, + } + + // this assignment is made possible by CoerceUnsized + static ALIGNED: &AlignedAs<$align_ty, [u8]> = &AlignedAs { + _align: [], + bytes: *include_bytes!($path), + }; + + &ALIGNED.bytes + }}; +} + +// IFTTT: crates/compiler/load/build.rs + +fn deserialize_help(bytes: &[u8]) -> TypeState { + let (state, _offset) = TypeState::deserialize(bytes); + debug_assert_eq!(bytes.len(), _offset); + + state +} + +fn read_cached_types() -> MutMap { + let mod_bool = include_bytes_align_as!(u128, concat!(env!("OUT_DIR"), "/Bool.dat")); + let mod_dict = include_bytes_align_as!(u128, concat!(env!("OUT_DIR"), "/Dict.dat")); + let mod_set = include_bytes_align_as!(u128, concat!(env!("OUT_DIR"), "/Set.dat")); + let mod_result = include_bytes_align_as!(u128, concat!(env!("OUT_DIR"), "/Result.dat")); + let mod_num = include_bytes_align_as!(u128, concat!(env!("OUT_DIR"), "/Num.dat")); + let mod_list = include_bytes_align_as!(u128, concat!(env!("OUT_DIR"), "/List.dat")); + let mod_str = include_bytes_align_as!(u128, concat!(env!("OUT_DIR"), "/Str.dat")); + let mod_box = include_bytes_align_as!(u128, concat!(env!("OUT_DIR"), "/Box.dat")); + let mod_encode = include_bytes_align_as!(u128, concat!(env!("OUT_DIR"), "/Encode.dat")); + let mod_decode = include_bytes_align_as!(u128, concat!(env!("OUT_DIR"), "/Decode.dat")); + let mod_hash = include_bytes_align_as!(u128, concat!(env!("OUT_DIR"), "/Hash.dat")); + let mod_inspect = include_bytes_align_as!(u128, concat!(env!("OUT_DIR"), "/Inspect.dat")); + + let mut output = MutMap::default(); + + // Wasm seems to re-order definitions between build time and runtime, but only in release mode. + // That is very strange, but we can solve it separately + if !cfg!(target_family = "wasm") && !cfg!(windows) && !SKIP_SUBS_CACHE { + output.insert(ModuleId::BOOL, deserialize_help(mod_bool)); + + output.insert(ModuleId::RESULT, deserialize_help(mod_result)); + output.insert(ModuleId::NUM, deserialize_help(mod_num)); + + output.insert(ModuleId::LIST, deserialize_help(mod_list)); + output.insert(ModuleId::STR, deserialize_help(mod_str)); + output.insert(ModuleId::BOX, deserialize_help(mod_box)); + + output.insert(ModuleId::DICT, deserialize_help(mod_dict)); + output.insert(ModuleId::SET, deserialize_help(mod_set)); + + output.insert(ModuleId::ENCODE, deserialize_help(mod_encode)); + output.insert(ModuleId::DECODE, deserialize_help(mod_decode)); + + output.insert(ModuleId::HASH, deserialize_help(mod_hash)); + output.insert(ModuleId::INSPECT, deserialize_help(mod_inspect)); + } + + output +} diff --git a/crates/compiler/load/tests/helpers/mod.rs b/crates/compiler/load/tests/helpers/mod.rs new file mode 100644 index 0000000000..cb782897d1 --- /dev/null +++ b/crates/compiler/load/tests/helpers/mod.rs @@ -0,0 +1,272 @@ +extern crate bumpalo; + +use self::bumpalo::Bump; +use roc_can::abilities::AbilitiesStore; +use roc_can::constraint::{Constraint, Constraints}; +use roc_can::env::Env; +use roc_can::expected::Expected; +use roc_can::expr::{canonicalize_expr, Expr, Output, PendingDerives}; +use roc_can::operator; +use roc_can::scope::Scope; +use roc_collections::all::{ImMap, MutMap, SendSet}; +use roc_constrain::expr::constrain_expr; +use roc_derive::SharedDerivedModule; +use roc_module::symbol::{IdentIds, Interns, ModuleId, ModuleIds}; +use roc_parse::parser::{SourceError, SyntaxError}; +use roc_problem::can::Problem; +use roc_region::all::Loc; +use roc_solve::module::SolveConfig; +use roc_solve::solve::RunSolveOutput; +use roc_solve::{solve, Aliases, FunctionKind}; +use roc_solve_problem::TypeError; +use roc_types::subs::{Content, Subs, VarStore, Variable}; +use roc_types::types::Types; +use std::hash::Hash; +use std::path::{Path, PathBuf}; + +pub fn test_home() -> ModuleId { + ModuleIds::default().get_or_insert(&"Test".into()) +} + +#[allow(dead_code)] +#[allow(clippy::too_many_arguments)] +pub fn infer_expr( + subs: Subs, + problems: &mut Vec, + types: Types, + constraints: &Constraints, + constraint: Constraint, + pending_derives: PendingDerives, + aliases: &mut Aliases, + abilities_store: &mut AbilitiesStore, + derived_module: SharedDerivedModule, + expr_var: Variable, +) -> (Content, Subs) { + let config = SolveConfig { + types, + constraints, + root_constraint: constraint, + home: ModuleId::ATTR, + pending_derives, + exposed_by_module: &Default::default(), + derived_module, + function_kind: FunctionKind::LambdaSet, + #[cfg(debug_assertions)] + checkmate: None, + }; + + let RunSolveOutput { solved, .. } = + solve::run(config, problems, subs, aliases, abilities_store); + + let content = *solved.inner().get_content_without_compacting(expr_var); + + (content, solved.into_inner()) +} + +/// Used in the with_larger_debug_stack() function, for tests that otherwise +/// run out of stack space in debug builds (but don't in --release builds) +#[allow(dead_code)] +const EXPANDED_STACK_SIZE: usize = 4 * 1024 * 1024; + +/// Without this, some tests pass in `cargo test --release` but fail without +/// the --release flag because they run out of stack space. This increases +/// stack size for debug builds only, while leaving the stack space at the default +/// amount for release builds. +#[allow(dead_code)] +#[cfg(debug_assertions)] +pub fn with_larger_debug_stack(run_test: F) +where + F: FnOnce(), + F: Send, + F: 'static, +{ + std::thread::Builder::new() + .stack_size(EXPANDED_STACK_SIZE) + .spawn(run_test) + .expect("Error while spawning expanded dev stack size thread") + .join() + .expect("Error while joining expanded dev stack size thread") +} + +/// In --release builds, don't increase the stack size. Run the test normally. +/// This way, we find out if any of our tests are blowing the stack even after +/// optimizations in release builds. +#[allow(dead_code)] +#[cfg(not(debug_assertions))] +#[inline(always)] +pub fn with_larger_debug_stack(run_test: F) +where + F: FnOnce(), + F: Send, + F: 'static, +{ + run_test() +} + +#[allow(dead_code)] +pub fn can_expr<'a>(arena: &'a Bump, expr_str: &'a str) -> Result> { + can_expr_with(arena, test_home(), expr_str) +} + +pub struct CanExprOut { + pub loc_expr: Loc, + pub output: Output, + pub problems: Vec, + pub home: ModuleId, + pub interns: Interns, + pub var_store: VarStore, + pub var: Variable, + pub constraint: Constraint, + pub constraints: Constraints, + pub types: Types, +} + +#[derive(Debug)] +pub struct ParseErrOut<'a> { + pub fail: SourceError<'a, SyntaxError<'a>>, + pub home: ModuleId, + pub interns: Interns, +} + +#[allow(dead_code)] +pub fn can_expr_with<'a>( + arena: &'a Bump, + home: ModuleId, + expr_str: &'a str, +) -> Result> { + let loc_expr = match roc_parse::test_helpers::parse_loc_with(arena, expr_str) { + Ok(e) => e, + Err(fail) => { + let interns = Interns::default(); + + return Err(ParseErrOut { + fail, + interns, + home, + }); + } + }; + + let mut types = Types::new(); + let mut constraints = Constraints::new(); + + let mut var_store = VarStore::default(); + let var = var_store.fresh(); + let var_index = constraints.push_variable(var); + let expected = constraints.push_expected_type(Expected::NoExpectation(var_index)); + let mut module_ids = ModuleIds::default(); + + // ensure the Test module is accessible in our tests + module_ids.get_or_insert(&"Test".into()); + + // Desugar operators (convert them to Apply calls, taking into account + // operator precedence and associativity rules), before doing other canonicalization. + // + // If we did this *during* canonicalization, then each time we + // visited a BinOp node we'd recursively try to apply this to each of its nested + // operators, and then again on *their* nested operators, ultimately applying the + // rules multiple times unnecessarily. + let loc_expr = operator::desugar_expr(arena, &loc_expr); + + let mut scope = Scope::new(home, IdentIds::default(), Default::default()); + + let dep_idents = IdentIds::exposed_builtins(0); + let mut env = Env::new(arena, home, &dep_idents, &module_ids); + let (loc_expr, output) = canonicalize_expr( + &mut env, + &mut var_store, + &mut scope, + loc_expr.region, + &loc_expr.value, + ); + + let constraint = constrain_expr( + &mut types, + &mut constraints, + &mut roc_constrain::expr::Env { + rigids: MutMap::default(), + home, + resolutions_to_make: vec![], + }, + loc_expr.region, + &loc_expr.value, + expected, + ); + + let mut all_ident_ids = IdentIds::exposed_builtins(1); + all_ident_ids.insert(home, scope.locals.ident_ids); + + let interns = Interns { + module_ids: env.module_ids.clone(), + all_ident_ids, + }; + + Ok(CanExprOut { + loc_expr, + output, + problems: env.problems, + home: env.home, + var_store, + interns, + var, + constraint, + constraints, + types, + }) +} + +#[allow(dead_code)] +pub fn mut_map_from_pairs(pairs: I) -> MutMap +where + I: IntoIterator, + K: Hash + Eq, +{ + let mut answer = MutMap::default(); + + for (key, value) in pairs { + answer.insert(key, value); + } + + answer +} + +#[allow(dead_code)] +pub fn im_map_from_pairs(pairs: I) -> ImMap +where + I: IntoIterator, + K: Hash + Eq + Clone, + V: Clone, +{ + let mut answer = ImMap::default(); + + for (key, value) in pairs { + answer.insert(key, value); + } + + answer +} + +#[allow(dead_code)] +pub fn send_set_from(elems: I) -> SendSet +where + I: IntoIterator, + V: Hash + Eq + Clone, +{ + let mut answer = SendSet::default(); + + for elem in elems { + answer.insert(elem); + } + + answer +} + +#[allow(dead_code)] +pub fn fixtures_dir() -> PathBuf { + Path::new("tests").join("fixtures").join("build") +} + +#[allow(dead_code)] +pub fn builtins_dir() -> PathBuf { + PathBuf::new().join("builtins") +} diff --git a/crates/compiler/load/tests/test_reporting.rs b/crates/compiler/load/tests/test_reporting.rs new file mode 100644 index 0000000000..331eee480b --- /dev/null +++ b/crates/compiler/load/tests/test_reporting.rs @@ -0,0 +1,14042 @@ +#[macro_use] +extern crate pretty_assertions; +extern crate bumpalo; +extern crate indoc; +extern crate roc_reporting; + +mod helpers; + +#[cfg(test)] +mod test_reporting { + use crate::helpers::{can_expr, infer_expr, test_home, CanExprOut, ParseErrOut}; + use bumpalo::Bump; + use indoc::indoc; + use roc_can::abilities::AbilitiesStore; + use roc_can::expr::PendingDerives; + use roc_load::{self, ExecutionMode, LoadConfig, LoadedModule, LoadingProblem, Threading}; + use roc_module::symbol::{Interns, ModuleId}; + use roc_packaging::cache::RocCacheDir; + use roc_parse::module::parse_header; + use roc_parse::state::State; + use roc_parse::test_helpers::parse_expr_with; + use roc_problem::Severity; + use roc_region::all::LineInfo; + use roc_reporting::report::{ + can_problem, parse_problem, type_problem, RenderTarget, Report, ANSI_STYLE_CODES, + DEFAULT_PALETTE, + }; + use roc_reporting::report::{RocDocAllocator, RocDocBuilder}; + use roc_solve::FunctionKind; + use roc_solve_problem::TypeError; + use roc_types::subs::Subs; + use std::path::PathBuf; + + fn filename_from_string(str: &str) -> PathBuf { + let mut filename = PathBuf::new(); + filename.push(str); + + filename + } + + fn to_simple_report(doc: RocDocBuilder) -> Report { + Report { + title: "".to_string(), + doc, + filename: filename_from_string(r"/code/proj/Main.roc"), + severity: Severity::RuntimeError, + } + } + + fn promote_expr_to_module(src: &str) -> String { + let mut buffer = String::from("app \"test\" provides [main] to \"./platform\"\n\nmain =\n"); + + for line in src.lines() { + // indent the body! + buffer.push_str(" "); + buffer.push_str(line); + buffer.push('\n'); + } + + buffer + } + + fn maybe_save_parse_test_case(test_name: &str, src: &str, is_expr: bool) { + // First check if the env var indicates we should migrate tests + if std::env::var("ROC_MIGRATE_REPORTING_TESTS").is_err() { + return; + } + + // Check if we have parse errors + let arena = Bump::new(); + let has_error = if is_expr { + parse_expr_with(&arena, src).is_err() + } else { + parse_header(&arena, State::new(src.trim().as_bytes())).is_err() + // TODO: also parse the module defs + }; + + if !has_error { + return; + } + + let mut path = PathBuf::from(std::env!("ROC_WORKSPACE_DIR")); + path.push("crates"); + path.push("compiler"); + path.push("parse"); + path.push("tests"); + path.push("snapshots"); + path.push("fail"); + let kind = if is_expr { "expr" } else { "header" }; + path.push(format!("{test_name}.{kind}.roc")); + + std::fs::write(path, src).unwrap(); + } + + fn run_load_and_infer<'a>( + subdir: &str, + arena: &'a Bump, + src: &'a str, + ) -> (String, Result>) { + use std::fs::File; + use std::io::Write; + + let module_src = if src.starts_with("app") { + maybe_save_parse_test_case(subdir, src, false); + // this is already a module + src.to_string() + } else { + maybe_save_parse_test_case(subdir, src, true); + // this is an expression, promote it to a module + promote_expr_to_module(src) + }; + + let loaded = { + // Use a deterministic temporary directory. + // We can't have all tests use "tmp" because tests run in parallel, + // so append the test name to the tmp path. + let tmp = format!("tmp/{subdir}"); + let dir = roc_test_utils::TmpDir::new(&tmp); + + let filename = PathBuf::from("Test.roc"); + let file_path = dir.path().join(filename); + let full_file_path = file_path.clone(); + let mut file = File::create(file_path).unwrap(); + writeln!(file, "{module_src}").unwrap(); + let load_config = LoadConfig { + target_info: roc_target::TargetInfo::default_x86_64(), + render: RenderTarget::Generic, + palette: DEFAULT_PALETTE, + threading: Threading::Single, + exec_mode: ExecutionMode::Check, + function_kind: FunctionKind::LambdaSet, + }; + let result = roc_load::load_and_typecheck( + arena, + full_file_path, + RocCacheDir::Disallowed, + load_config, + ); + drop(file); + + result + }; + + (module_src, loaded) + } + + #[allow(clippy::type_complexity)] + fn infer_expr_help_new<'a>( + subdir: &str, + arena: &'a Bump, + expr_src: &'a str, + ) -> Result< + ( + String, + Vec, + Vec, + ModuleId, + Interns, + ), + LoadingProblem<'a>, + > { + let (module_src, result) = run_load_and_infer(subdir, arena, expr_src); + let LoadedModule { + module_id: home, + mut can_problems, + mut type_problems, + interns, + .. + } = result?; + + let can_problems = can_problems.remove(&home).unwrap_or_default(); + let type_problems = type_problems.remove(&home).unwrap_or_default(); + + Ok((module_src, type_problems, can_problems, home, interns)) + } + + fn list_reports_new(subdir: &str, arena: &Bump, src: &str, finalize_render: F) -> String + where + F: FnOnce(RocDocBuilder<'_>, &mut String), + { + use ven_pretty::DocAllocator; + + let filename = filename_from_string(r"/code/proj/Main.roc"); + + let mut buf = String::new(); + + match infer_expr_help_new(subdir, arena, src) { + Err(LoadingProblem::FormattedReport(fail)) => fail, + Ok((module_src, type_problems, can_problems, home, interns)) => { + let lines = LineInfo::new(&module_src); + let src_lines: Vec<&str> = module_src.split('\n').collect(); + let mut reports = Vec::new(); + + let alloc = RocDocAllocator::new(&src_lines, home, &interns); + + for problem in can_problems { + let report = can_problem(&alloc, &lines, filename.clone(), problem.clone()); + reports.push(report); + } + + for problem in type_problems { + if let Some(report) = + type_problem(&alloc, &lines, filename.clone(), problem.clone()) + { + reports.push(report); + } + } + + let has_reports = !reports.is_empty(); + + let doc = alloc + .stack(reports.into_iter().map(|v| v.pretty(&alloc))) + .append(if has_reports { + alloc.line() + } else { + alloc.nil() + }); + + finalize_render(doc, &mut buf); + buf + } + Err(other) => { + panic!("failed to load: {other:?}"); + } + } + } + + fn infer_expr_help<'a>( + arena: &'a Bump, + expr_src: &'a str, + ) -> Result< + ( + Vec, + Vec, + ModuleId, + Interns, + ), + ParseErrOut<'a>, + > { + let CanExprOut { + loc_expr: _, + output, + var_store, + var, + constraints, + constraint, + home, + interns, + problems: can_problems, + mut types, + .. + } = can_expr(arena, expr_src)?; + let mut subs = Subs::new_from_varstore(var_store); + + for named in output.introduced_variables.named { + subs.rigid_var(named.variable, named.name); + } + + for var in output.introduced_variables.wildcards { + subs.rigid_var(var.value, "*".into()); + } + + let mut solve_aliases = roc_solve::Aliases::default(); + + for (name, alias) in output.aliases { + solve_aliases.insert(&mut types, name, alias); + } + + let mut unify_problems = Vec::new(); + let mut abilities_store = AbilitiesStore::default(); + let (_content, _subs) = infer_expr( + subs, + &mut unify_problems, + types, + &constraints, + constraint, + // Use `new_report_problem_as` in order to get proper derives. + // TODO: remove the non-new reporting test infra. + PendingDerives::default(), + &mut solve_aliases, + &mut abilities_store, + Default::default(), + var, + ); + + Ok((unify_problems, can_problems, home, interns)) + } + + fn list_reports(arena: &Bump, src: &str, buf: &mut String, callback: F) + where + F: FnOnce(RocDocBuilder<'_>, &mut String), + { + use ven_pretty::DocAllocator; + + let src_lines: Vec<&str> = src.split('\n').collect(); + let lines = LineInfo::new(src); + + let filename = filename_from_string(r"/code/proj/Main.roc"); + + match infer_expr_help(arena, src) { + Err(parse_err) => { + let ParseErrOut { + fail, + home, + interns, + } = parse_err; + + let alloc = RocDocAllocator::new(&src_lines, home, &interns); + + let problem = fail.into_file_error(filename.clone()); + let doc = parse_problem(&alloc, &lines, filename, 0, problem); + + callback(doc.pretty(&alloc).append(alloc.line()), buf) + } + Ok((type_problems, can_problems, home, interns)) => { + let mut reports = Vec::new(); + + let alloc = RocDocAllocator::new(&src_lines, home, &interns); + + for problem in can_problems { + let report = can_problem(&alloc, &lines, filename.clone(), problem.clone()); + reports.push(report); + } + + for problem in type_problems { + if let Some(report) = + type_problem(&alloc, &lines, filename.clone(), problem.clone()) + { + reports.push(report); + } + } + + let has_reports = !reports.is_empty(); + + let doc = alloc + .stack(reports.into_iter().map(|v| v.pretty(&alloc))) + .append(if has_reports { + alloc.line() + } else { + alloc.nil() + }); + + callback(doc, buf) + } + } + } + + fn list_header_reports(arena: &Bump, src: &str, buf: &mut String, callback: F) + where + F: FnOnce(RocDocBuilder<'_>, &mut String), + { + use ven_pretty::DocAllocator; + + let state = State::new(src.as_bytes()); + + let filename = filename_from_string(r"/code/proj/Main.roc"); + let src_lines: Vec<&str> = src.split('\n').collect(); + let lines = LineInfo::new(src); + + match roc_parse::module::parse_header(arena, state) { + Err(fail) => { + let interns = Interns::default(); + let home = crate::helpers::test_home(); + + let alloc = RocDocAllocator::new(&src_lines, home, &interns); + + use roc_parse::parser::SyntaxError; + let problem = fail + .map_problem(SyntaxError::Header) + .into_file_error(filename.clone()); + let doc = parse_problem(&alloc, &lines, filename, 0, problem); + + callback(doc.pretty(&alloc).append(alloc.line()), buf) + } + Ok(_) => todo!(), + } + } + + fn report_header_problem_as(src: &str, expected_rendering: &str) { + let mut buf: String = String::new(); + let arena = Bump::new(); + + let callback = |doc: RocDocBuilder<'_>, buf: &mut String| { + doc.1 + .render_raw(70, &mut roc_reporting::report::CiWrite::new(buf)) + .expect("list_reports") + }; + + list_header_reports(&arena, src, &mut buf, callback); + + // convenient to copy-paste the generated message + if buf != expected_rendering { + for line in buf.split('\n') { + println!(" {line}"); + } + } + + assert_eq!(buf, expected_rendering); + } + + fn color_report_problem_as(src: &str, expected_rendering: &str) { + let mut buf: String = String::new(); + let arena = Bump::new(); + + let callback = |doc: RocDocBuilder<'_>, buf: &mut String| { + doc.1 + .render_raw( + 70, + &mut roc_reporting::report::ColorWrite::new( + &roc_reporting::report::DEFAULT_PALETTE, + buf, + ), + ) + .expect("list_reports") + }; + + list_reports(&arena, src, &mut buf, callback); + + let readable = human_readable(&buf); + + assert_eq!(readable, expected_rendering); + } + + /// Do not call this directly! Use the test_report macro below! + fn __new_report_problem_as(test_name: &str, src: &str, check_render: impl FnOnce(&str)) { + let arena = Bump::new(); + + let finalize_render = |doc: RocDocBuilder<'_>, buf: &mut String| { + doc.1 + .render_raw(70, &mut roc_reporting::report::CiWrite::new(buf)) + .expect("list_reports") + }; + + let buf = list_reports_new(test_name, &arena, src, finalize_render); + + check_render(buf.as_str()); + } + + macro_rules! test_report { + ($(#[$meta:meta])* $test_name:ident, $program:expr, @$output:literal) => { + test_report!($(#[$meta])* $test_name, $program, |golden| insta::assert_snapshot!(golden, @$output) ); + }; + ($(#[$meta:meta])* $test_name: ident, $program:expr, $expecting:expr) => { + #[test] + $(#[$meta])* + fn $test_name() { + __new_report_problem_as(std::stringify!($test_name), $program, $expecting) + } + } + } + + macro_rules! test_no_problem { + ($(#[$meta:meta])* $test_name: ident, $program:expr) => { + #[test] + $(#[$meta])* + fn $test_name() { + __new_report_problem_as(std::stringify!($test_name), $program, |golden| pretty_assertions::assert_eq!(golden, "")) + } + } + } + + fn human_readable(str: &str) -> String { + str.replace(ANSI_STYLE_CODES.red, "") + .replace(ANSI_STYLE_CODES.white, "") + .replace(ANSI_STYLE_CODES.blue, "") + .replace(ANSI_STYLE_CODES.yellow, "") + .replace(ANSI_STYLE_CODES.green, "") + .replace(ANSI_STYLE_CODES.cyan, "") + .replace(ANSI_STYLE_CODES.magenta, "") + .replace(ANSI_STYLE_CODES.reset, "") + .replace(ANSI_STYLE_CODES.bold, "") + .replace(ANSI_STYLE_CODES.underline, "") + } + + test_report!( + value_not_exposed, + indoc!( + r#" + List.isempty 1 2 + "# + ), + @r###" + ── NOT EXPOSED ─────────────────────────────────────────── /code/proj/Main.roc ─ + + The List module does not expose `isempty`: + + 4│ List.isempty 1 2 + ^^^^^^^^^^^^ + + Did you mean one of these? + + List.isEmpty + List.set + List.get + List.keepIf + "### + ); + + test_report!( + report_unused_def, + indoc!( + r#" + x = 1 + y = 2 + + x + "# + ), + @r###" + ── UNUSED DEFINITION ───────────────────────────────────── /code/proj/Main.roc ─ + + `y` is not used anywhere in your code. + + 5│ y = 2 + ^ + + If you didn't intend on using `y` then remove it so future readers of + your code don't wonder why it is there. + "### + ); + + test_report!( + report_shadowing, + indoc!( + r#" + i = 1 + + s = \i -> + i + 1 + + s i + "# + ), + @r###" + ── DUPLICATE NAME ──────────────────────────────────────── /code/proj/Main.roc ─ + + The `i` name is first defined here: + + 4│ i = 1 + ^ + + But then it's defined a second time here: + + 6│ s = \i -> + ^ + + Since these variables have the same name, it's easy to use the wrong + one by accident. Give one of them a new name. + "### + ); + + test_report!( + report_shadowing_in_annotation, + indoc!( + r#" + Booly : [Yes, No] + + Booly : [Yes, No, Maybe] + + x : List Booly + x = [] + + x + "# + ), + @r###" + ── DUPLICATE NAME ──────────────────────────────────────── /code/proj/Main.roc ─ + + The `Booly` name is first defined here: + + 4│ Booly : [Yes, No] + ^^^^^^^^^^^^^^^^^ + + But then it's defined a second time here: + + 6│ Booly : [Yes, No, Maybe] + ^^^^^^^^^^^^^^^^^^^^^^^^ + + Since these aliases have the same name, it's easy to use the wrong one + by accident. Give one of them a new name. + "### + ); + + test_report!( + report_precedence_problem_single_line, + indoc!( + r#"x = 1 + y = + if selectedId != thisId == adminsId then + 4 + + else + 5 + + { x, y } + "# + ), + @r###" + ── SYNTAX PROBLEM ──────────────────────────────────────── /code/proj/Main.roc ─ + + Using != and == together requires parentheses, to clarify how they + should be grouped. + + 6│ if selectedId != thisId == adminsId then + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + "### + ); + + test_report!( + #[ignore = "Blocked on https://github.com/roc-lang/roc/issues/3385"] + unrecognized_name, + indoc!( + r#" + foo = { x: 1 == 1, y: 0x4 } + + baz = 3 + + main : Str + main = + when foo.y is + 4 -> bar baz "yay" + _ -> "nay" + + main + "# + ), + @r#" + ── UNRECOGNIZED NAME ───────────────────────────────────── /code/proj/Main.roc ─ + + Nothing is named `bar` in this scope. + + 8│ 4 -> bar baz "yay" + ^^^ + + Did you mean one of these? + + baz + Str + Err + main + "# + ); + + test_report!( + lowercase_primitive_tag_bool, + indoc!( + r#" + if true then 1 else 2 + "# + ), + @r###" + ── UNRECOGNIZED NAME ───────────────────────────────────── /code/proj/Main.roc ─ + + Nothing is named `true` in this scope. + + 4│ if true then 1 else 2 + ^^^^ + + Did you mean one of these? + + Frac + Num + Str + Err + "### + ); + + test_report!( + report_precedence_problem_multiline, + indoc!( + r#" + if + 1 + == 2 + == 3 + then + 2 + + else + 3 + "# + ), + @r###" + ── SYNTAX PROBLEM ──────────────────────────────────────── /code/proj/Main.roc ─ + + Using more than one == like this requires parentheses, to clarify how + things should be grouped. + + 5│> 1 + 6│> == 2 + 7│> == 3 + "### + ); + + test_report!( + unused_arg_and_unused_def, + indoc!( + r#" + y = 9 + + box = \class, htmlChildren -> + div [class] [] + + div = \_, _ -> 4 + + box "wizard" [] + "# + ), + @r###" + ── UNUSED ARGUMENT ─────────────────────────────────────── /code/proj/Main.roc ─ + + `box` doesn't use `htmlChildren`. + + 6│ box = \class, htmlChildren -> + ^^^^^^^^^^^^ + + If you don't need `htmlChildren`, then you can just remove it. However, + if you really do need `htmlChildren` as an argument of `box`, prefix it + with an underscore, like this: "_`htmlChildren`". Adding an underscore + at the start of a variable name is a way of saying that the variable + is not used. + + ── UNUSED DEFINITION ───────────────────────────────────── /code/proj/Main.roc ─ + + `y` is not used anywhere in your code. + + 4│ y = 9 + ^ + + If you didn't intend on using `y` then remove it so future readers of + your code don't wonder why it is there. + "### + ); + + #[test] + fn report_value_color() { + let src: &str = indoc!( + r#" + activityIndicatorLarge = div + + view activityIndicatorLarge + "# + ); + + let arena = Bump::new(); + let (_type_problems, _can_problems, home, interns) = + infer_expr_help(&arena, src).expect("parse error"); + + let mut buf = String::new(); + let src_lines: Vec<&str> = src.split('\n').collect(); + + let alloc = RocDocAllocator::new(&src_lines, home, &interns); + + let symbol = interns.symbol(test_home(), "activityIndicatorLarge".into()); + + to_simple_report(alloc.symbol_unqualified(symbol)).render_color_terminal( + &mut buf, + &alloc, + &DEFAULT_PALETTE, + ); + + assert_eq!(human_readable(&buf), "activityIndicatorLarge"); + } + + #[test] + fn report_module_color() { + let src: &str = indoc!( + r#" + x = 1 + y = 2 + + x + "# + ); + + let arena = Bump::new(); + let (_type_problems, _can_problems, home, mut interns) = + infer_expr_help(&arena, src).expect("parse error"); + + let mut buf = String::new(); + let src_lines: Vec<&str> = src.split('\n').collect(); + let module_id = interns.module_id(&"Util.Int".into()); + + let alloc = RocDocAllocator::new(&src_lines, home, &interns); + to_simple_report(alloc.module(module_id)).render_color_terminal( + &mut buf, + &alloc, + &DEFAULT_PALETTE, + ); + + assert_eq!(human_readable(&buf), "Util.Int"); + } + + #[test] + fn report_region_in_color() { + color_report_problem_as( + indoc!( + r#" + isDisabled = \user -> user.isAdmin + + theAdmin + |> isDisabled + "# + ), + indoc!( + r#" + ── UNRECOGNIZED NAME ───────────────────────────────────── /code/proj/Main.roc ─ + + Nothing is named `theAdmin` in this scope. + + 3 theAdmin + ^^^^^^^^ + + Did you mean one of these? + + Ok + List + Err + Box + "# + ), + ); + } + + test_report!( + if_condition_not_bool, + indoc!( + r#" + if "foo" then 2 else 3 + "# + ), + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + This `if` condition needs to be a Bool: + + 4│ if "foo" then 2 else 3 + ^^^^^ + + Right now it’s a string of type: + + Str + + But I need every `if` condition to evaluate to a Bool—either `Bool.true` + or `Bool.false`. + "### + ); + + test_report!( + when_if_guard, + indoc!( + r#" + when 1 is + 2 if 1 -> 0x0 + _ -> 0x1 + "# + ), + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + This `if` guard condition needs to be a Bool: + + 4│ when 1 is + 5│> 2 if 1 -> 0x0 + 6│ _ -> 0x1 + + Right now it’s a number of type: + + Num * + + But I need every `if` guard condition to evaluate to a Bool—either + `Bool.true` or `Bool.false`. + "### + ); + + test_report!( + if_2_branch_mismatch, + indoc!( + r#" + if Bool.true then 2 else "foo" + "# + ), + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + This `if` has an `else` branch with a different type from its `then` branch: + + 4│ if Bool.true then 2 else "foo" + ^^^^^ + + The `else` branch is a string of type: + + Str + + but the `then` branch has the type: + + Num * + + All branches in an `if` must have the same type! + "### + ); + + test_report!( + if_3_branch_mismatch, + indoc!( + r#" + if Bool.true then 2 else if Bool.false then 2 else "foo" + "# + ), + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + The 3rd branch of this `if` does not match all the previous branches: + + 4│ if Bool.true then 2 else if Bool.false then 2 else "foo" + ^^^^^ + + The 3rd branch is a string of type: + + Str + + But all the previous branches have type: + + Num * + + All branches in an `if` must have the same type! + "### + ); + + test_report!( + when_branch_mismatch, + indoc!( + r#" + when 1 is + 2 -> "foo" + 3 -> {} + _ -> "" + "# + ), + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + The 2nd branch of this `when` does not match all the previous branches: + + 4│ when 1 is + 5│ 2 -> "foo" + 6│> 3 -> {} + 7│ _ -> "" + + The 2nd branch is a record of type: + + {} + + But all the previous branches have type: + + Str + + All branches of a `when` must have the same type! + "### + ); + + test_report!( + tuple_exhaustiveness_bad, + indoc!( + r#" + Color : [Red, Blue] + + value : (Color, Color) + value = (Red, Red) + + when value is + (Blue, Blue) -> "foo" + (Red, Blue) -> "foo" + (Blue, Red) -> "foo" + #(Red, Red) -> "foo" + "# + ), + @r###" + ── UNSAFE PATTERN ──────────────────────────────────────── /code/proj/Main.roc ─ + + This `when` does not cover all the possibilities: + + 9│> when value is + 10│> (Blue, Blue) -> "foo" + 11│> (Red, Blue) -> "foo" + 12│> (Blue, Red) -> "foo" + + Other possibilities include: + + ( Red, Red ) + + I would have to crash if I saw one of those! Add branches for them! + "### + ); + + test_report!( + tuple_exhaustiveness_good, + indoc!( + r#" + Color : [Red, Blue] + + value : (Color, Color) + value = (Red, Red) + + when value is + (Blue, Blue) -> "foo" + (Red, Blue) -> "foo" + (Blue, Red) -> "foo" + (Red, Red) -> "foo" + "# + ), + @"" // No error + ); + + test_report!( + elem_in_list, + indoc!( + r#" + [1, 3, "foo"] + "# + ), + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + This list contains elements with different types: + + 4│ [1, 3, "foo"] + ^^^^^ + + Its 3rd element is a string of type: + + Str + + However, the preceding elements in the list all have the type: + + Num * + + Every element in a list must have the same type! + "### + ); + + test_report!( + unwrap_num_elem_in_list, + indoc!( + r#" + [1, 2.2, 0x3] + "# + ), + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + This list contains elements with different types: + + 4│ [1, 2.2, 0x3] + ^^^ + + Its 3rd element is an integer of type: + + Int * + + However, the preceding elements in the list all have the type: + + Frac * + + Every element in a list must have the same type! + + Tip: You can convert between integers and fractions using functions + like `Num.toFrac` and `Num.round`. + "### + ); + + test_report!( + record_update_value, + indoc!( + r#" + x : { foo : {} } + x = { foo: {} } + + { x & foo: "bar" } + "# + ), + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + I cannot update the `.foo` field like this: + + 7│ { x & foo: "bar" } + ^^^^^ + + You are trying to update `.foo` to be a string of type: + + Str + + But it should be: + + {} + + Record update syntax does not allow you to change the type of fields. + You can achieve that with record literal syntax. + "### + ); + + test_report!( + circular_type, + indoc!( + r#" + f = \g -> g g + + f + "# + ), + @r###" + ── CIRCULAR TYPE ───────────────────────────────────────── /code/proj/Main.roc ─ + + I'm inferring a weird self-referential type for `f`: + + 4│ f = \g -> g g + ^ + + Here is my best effort at writing down the type. You will see ∞ for + parts of the type that repeat something already printed out + infinitely. + + (∞ -> a) -> a + "### + ); + + test_report!( + polymorphic_recursion, + indoc!( + r#" + f = \x -> f [x] + + f + "# + ), + @r###" + ── CIRCULAR TYPE ───────────────────────────────────────── /code/proj/Main.roc ─ + + I'm inferring a weird self-referential type for `f`: + + 4│ f = \x -> f [x] + ^ + + Here is my best effort at writing down the type. You will see ∞ for + parts of the type that repeat something already printed out + infinitely. + + List ∞ -> * + "### + ); + + test_report!( + polymorphic_mutual_recursion, + indoc!( + r#" + f = \x -> g x + g = \x -> f [x] + + f + "# + ), + @r###" + ── CIRCULAR TYPE ───────────────────────────────────────── /code/proj/Main.roc ─ + + I'm inferring a weird self-referential type for `f`: + + 4│ f = \x -> g x + ^ + + Here is my best effort at writing down the type. You will see ∞ for + parts of the type that repeat something already printed out + infinitely. + + List ∞ -> * + + ── CIRCULAR TYPE ───────────────────────────────────────── /code/proj/Main.roc ─ + + I'm inferring a weird self-referential type for `g`: + + 5│ g = \x -> f [x] + ^ + + Here is my best effort at writing down the type. You will see ∞ for + parts of the type that repeat something already printed out + infinitely. + + List ∞ -> * + "### + ); + + test_report!( + polymorphic_mutual_recursion_annotated, + indoc!( + r#" + f : a -> List a + f = \x -> g x + g = \x -> f [x] + + f + "# + ), + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + This expression is used in an unexpected way: + + 5│ f = \x -> g x + ^^^ + + This `g` call produces: + + List List a + + But you are trying to use it as: + + List a + + Tip: The type annotation uses the type variable `a` to say that this + definition can produce any type of value. But in the body I see that + it will only produce a `List` value of a single specific type. Maybe + change the type annotation to be more specific? Maybe change the code + to be more general? + "### + ); + + test_report!( + polymorphic_mutual_recursion_dually_annotated_lie, + indoc!( + r#" + f : a -> List a + f = \x -> g x + g : b -> List b + g = \x -> f [x] + + f + "# + ), + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + This expression is used in an unexpected way: + + 7│ g = \x -> f [x] + ^^^^^ + + This `f` call produces: + + List List b + + But you are trying to use it as: + + List b + + Tip: The type annotation uses the type variable `b` to say that this + definition can produce any type of value. But in the body I see that + it will only produce a `List` value of a single specific type. Maybe + change the type annotation to be more specific? Maybe change the code + to be more general? + "### + ); + + test_report!( + polymorphic_recursion_inference_var, + indoc!( + r#" + f : _ + f = \x -> f [x] + + f + "# + ), + @r###" + ── CIRCULAR TYPE ───────────────────────────────────────── /code/proj/Main.roc ─ + + I'm inferring a weird self-referential type for `f`: + + 5│ f = \x -> f [x] + ^ + + Here is my best effort at writing down the type. You will see ∞ for + parts of the type that repeat something already printed out + infinitely. + + List ∞ -> * + "### + ); + + test_report!( + polymorphic_recursion_with_deep_inference_var, + indoc!( + r#" + f : _ -> List _ + f = \x -> f [x] + + f + "# + ), + @r###" + ── CIRCULAR TYPE ───────────────────────────────────────── /code/proj/Main.roc ─ + + I'm inferring a weird self-referential type for `f`: + + 5│ f = \x -> f [x] + ^ + + Here is my best effort at writing down the type. You will see ∞ for + parts of the type that repeat something already printed out + infinitely. + + List ∞ -> List * + "### + ); + + test_report!( + mutual_polymorphic_recursion_with_inference_var, + indoc!( + r#" + f : _ -> List _ + f = \x -> g x + g = \x -> f [x] + + f + "# + ), + // TODO: the second error is duplicated because when solving `f : _ -> List _`, we + // introduce the variable for `f` twice: once to solve `f` without generalization, + // and then a second time to properly generalize it. When a def is unannotated + // (like in `g`) the same variable gets used both times, because the type of `g` is + // only an unbound type variable. However, for `f`, we run `type_to_var` twice, + // receiving two separate variables, and the second variable doesn't have the cycle + // error already recorded for the first. + // The way to resolve this is to always give type annotation signatures an extra + // variables they can put themselves in, and to run the constraint algorithm + // against that extra variable, rather than possibly having to translate a `Type` + // again. + @r###" + ── CIRCULAR TYPE ───────────────────────────────────────── /code/proj/Main.roc ─ + + I'm inferring a weird self-referential type for `f`: + + 5│ f = \x -> g x + ^ + + Here is my best effort at writing down the type. You will see ∞ for + parts of the type that repeat something already printed out + infinitely. + + List ∞ -> List * + + ── CIRCULAR TYPE ───────────────────────────────────────── /code/proj/Main.roc ─ + + I'm inferring a weird self-referential type for `g`: + + 6│ g = \x -> f [x] + ^ + + Here is my best effort at writing down the type. You will see ∞ for + parts of the type that repeat something already printed out + infinitely. + + List ∞ -> List * + "### + ); + + test_report!( + mutual_polymorphic_recursion_with_inference_var_second, + indoc!( + r#" + f = \x -> g x + g : _ -> List _ + g = \x -> f [x] + + f + "# + ), + @r###" + ── CIRCULAR TYPE ───────────────────────────────────────── /code/proj/Main.roc ─ + + I'm inferring a weird self-referential type for `f`: + + 4│ f = \x -> g x + ^ + + Here is my best effort at writing down the type. You will see ∞ for + parts of the type that repeat something already printed out + infinitely. + + List ∞ -> List * + + ── CIRCULAR TYPE ───────────────────────────────────────── /code/proj/Main.roc ─ + + I'm inferring a weird self-referential type for `g`: + + 6│ g = \x -> f [x] + ^ + + Here is my best effort at writing down the type. You will see ∞ for + parts of the type that repeat something already printed out + infinitely. + + List ∞ -> List * + "### + ); + + test_report!( + record_field_mismatch, + indoc!( + r#" + bar = { bar : 0x3 } + + f : { foo : Num.Int * } -> [Yes, No] + f = \_ -> Yes + + f bar + "# + ), + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + This 1st argument to `f` has an unexpected type: + + 9│ f bar + ^^^ + + This `bar` value is a: + + { bar : Int * } + + But `f` needs its 1st argument to be: + + { foo : Int * } + + Tip: Seems like a record field typo. Maybe `bar` should be `foo`? + + Tip: Can more type annotations be added? Type annotations always help + me give more specific messages, and I think they could help a lot in + this case + "### + ); + + test_report!( + tag_mismatch, + indoc!( + r#" + f : [Red, Green] -> [Yes, No] + f = \_ -> Yes + + f Blue + "# + ), + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + This 1st argument to `f` has an unexpected type: + + 7│ f Blue + ^^^^ + + This `Blue` tag has the type: + + [Blue] + + But `f` needs its 1st argument to be: + + [ + Green, + Red, + ] + + Tip: Seems like a tag typo. Maybe `Blue` should be `Red`? + + Tip: Can more type annotations be added? Type annotations always help + me give more specific messages, and I think they could help a lot in + this case + "### + ); + + test_report!( + tag_with_arguments_mismatch, + indoc!( + r#" + f : [Red (Num.Int *), Green Str] -> Str + f = \_ -> "yes" + + f (Blue 3.14) + "# + ), + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + This 1st argument to `f` has an unexpected type: + + 7│ f (Blue 3.14) + ^^^^^^^^^ + + This `Blue` tag application has the type: + + [Blue (Frac *)] + + But `f` needs its 1st argument to be: + + [ + Green Str, + Red (Int *), + ] + + Tip: Seems like a tag typo. Maybe `Blue` should be `Red`? + + Tip: Can more type annotations be added? Type annotations always help + me give more specific messages, and I think they could help a lot in + this case + "### + ); + + test_report!( + from_annotation_if, + indoc!( + r#" + x : Num.Int * + x = if Bool.true then 3.14 else 4 + + x + "# + ), + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + Something is off with the `then` branch of this `if` expression: + + 4│ x : Num.Int * + 5│ x = if Bool.true then 3.14 else 4 + ^^^^ + + This branch is a fraction of type: + + Frac * + + But the type annotation on `x` says it should be: + + Int * + + Tip: You can convert between integers and fractions using functions + like `Num.toFrac` and `Num.round`. + "### + ); + + test_report!( + from_annotation_when, + indoc!( + r#" + x : Num.Int * + x = + when True is + _ -> 3.14 + + x + "# + ), + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + Something is off with the body of the `x` definition: + + 4│ x : Num.Int * + 5│ x = + 6│> when True is + 7│> _ -> 3.14 + + This `when` expression produces: + + Frac * + + But the type annotation on `x` says it should be: + + Int * + + Tip: You can convert between integers and fractions using functions + like `Num.toFrac` and `Num.round`. + "### + ); + + test_report!( + from_annotation_function, + indoc!( + r#" + x : Num.Int * -> Num.Int * + x = \_ -> 3.14 + + x + "# + ), + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + Something is off with the body of the `x` definition: + + 4│ x : Num.Int * -> Num.Int * + 5│ x = \_ -> 3.14 + ^^^^ + + The body is a fraction of type: + + Frac * + + But the type annotation on `x` says it should be: + + Int * + + Tip: You can convert between integers and fractions using functions + like `Num.toFrac` and `Num.round`. + "### + ); + + test_report!( + fncall_value, + indoc!( + r#" + x : Num.I64 + x = 42 + + x 3 + "# + ), + @r###" + ── TOO MANY ARGS ───────────────────────────────────────── /code/proj/Main.roc ─ + + The `x` value is not a function, but it was given 1 argument: + + 7│ x 3 + ^ + + Are there any missing commas? Or missing parentheses? + "### + ); + + test_report!( + fncall_overapplied, + indoc!( + r#" + f : Num.I64 -> Num.I64 + f = \_ -> 42 + + f 1 2 + "# + ), + @r###" + ── TOO MANY ARGS ───────────────────────────────────────── /code/proj/Main.roc ─ + + The `f` function expects 1 argument, but it got 2 instead: + + 7│ f 1 2 + ^ + + Are there any missing commas? Or missing parentheses? + "### + ); + + test_report!( + fncall_underapplied, + indoc!( + r#" + f : Num.I64, Num.I64 -> Num.I64 + f = \_, _ -> 42 + + f 1 + "# + ), + @r###" + ── TOO FEW ARGS ────────────────────────────────────────── /code/proj/Main.roc ─ + + The `f` function expects 2 arguments, but it got only 1: + + 7│ f 1 + ^ + + Roc does not allow functions to be partially applied. Use a closure to + make partial application explicit. + "### + ); + + test_report!( + pattern_when_condition, + indoc!( + r#" + when 1 is + {} -> 42 + "# + ), + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + The branches of this `when` expression don't match the condition: + + 4│> when 1 is + 5│ {} -> 42 + + The `when` condition is a number of type: + + Num * + + But the branch patterns have type: + + {}a + + The branches must be cases of the `when` condition's type! + "### + ); + + test_report!( + pattern_when_pattern, + indoc!( + r#" + when 1 is + 2 -> 3 + {} -> 42 + "# + ), + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + The 2nd pattern in this `when` does not match the previous ones: + + 6│ {} -> 42 + ^^ + + The 2nd pattern is trying to match record values of type: + + {}a + + But all the previous branches match: + + Num * + "### + ); + + test_report!( + pattern_guard_mismatch_alias, + indoc!( + r#" + when { foo: 1 } is + { foo: True } -> 42 + "# + ), + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + The branches of this `when` expression don't match the condition: + + 4│> when { foo: 1 } is + 5│ { foo: True } -> 42 + + The `when` condition is a record of type: + + { foo : Num * } + + But the branch patterns have type: + + { foo : [True] } + + The branches must be cases of the `when` condition's type! + "### + ); + + test_report!( + pattern_guard_mismatch, + indoc!( + r#" + when { foo: "" } is + { foo: True } -> 42 + "# + ), + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + The branches of this `when` expression don't match the condition: + + 4│> when { foo: "" } is + 5│ { foo: True } -> 42 + + The `when` condition is a record of type: + + { foo : Str } + + But the branch patterns have type: + + { foo : [True] } + + The branches must be cases of the `when` condition's type! + "### + ); + + // needs some improvement, but the principle works + test_report!( + pattern_guard_does_not_bind_label, + indoc!( + r#" + when { foo: 1 } is + { foo: _ } -> foo + "# + ), + @r###" + ── UNRECOGNIZED NAME ───────────────────────────────────── /code/proj/Main.roc ─ + + Nothing is named `foo` in this scope. + + 5│ { foo: _ } -> foo + ^^^ + + Did you mean one of these? + + Box + Bool + U8 + F64 + "### + ); + + test_report! { + pattern_guard_can_be_shadowed_above, + indoc!( + r#" + foo = 3 + + when { foo: 1 } is + { foo: 2 } -> foo + _ -> foo + "# + ), + @"" // should give no error + } + + test_report! { + pattern_guard_can_be_shadowed_below, + indoc!( + r#" + when { foo: 1 } is + { foo: 2 } -> + foo = 3 + + foo + _ -> 3 + "# + ), + // should give no error + @"" + } + + test_report!( + pattern_or_pattern_mismatch, + indoc!( + r#" + when { foo: 1 } is + {} | 1 -> 3 + "# + ), + // Just putting this here. We should probably handle or-patterns better + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + The 2nd pattern in this branch does not match the previous ones: + + 5│ {} | 1 -> 3 + ^ + + The 2nd pattern is trying to match numbers: + + Num * + + But all the previous branches match: + + {}a + "### + ); + + test_report!( + pattern_let_mismatch, + indoc!( + r#" + (Foo x) = 42 + + x + "# + ), + // Maybe this should specifically say the pattern doesn't work? + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + This expression is used in an unexpected way: + + 4│ (Foo x) = 42 + ^^ + + It is a number of type: + + Num * + + But you are trying to use it as: + + [Foo *] + "### + ); + + test_report!( + from_annotation_complex_pattern, + indoc!( + r#" + { x } : { x : Num.Int * } + { x } = { x: 4.0 } + + x + "# + ), + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + Something is off with the body of this definition: + + 4│ { x } : { x : Num.Int * } + 5│ { x } = { x: 4.0 } + ^^^^^^^^^^ + + The body is a record of type: + + { x : Frac * } + + But the type annotation says it should be: + + { x : Int * } + + Tip: You can convert between integers and fractions using functions + like `Num.toFrac` and `Num.round`. + "### + ); + + test_report!( + malformed_int_pattern, + indoc!( + r#" + when 1 is + 100A -> 3 + _ -> 4 + "# + ), + @r###" + ── SYNTAX PROBLEM ──────────────────────────────────────── /code/proj/Main.roc ─ + + This integer pattern is malformed: + + 5│ 100A -> 3 + ^^^^ + + Tip: Learn more about number literals at TODO + "### + ); + + test_report!( + malformed_float_pattern, + indoc!( + r#" + when 1 is + 2.X -> 3 + _ -> 4 + "# + ), + @r###" + ── SYNTAX PROBLEM ──────────────────────────────────────── /code/proj/Main.roc ─ + + This float pattern is malformed: + + 5│ 2.X -> 3 + ^^^ + + Tip: Learn more about number literals at TODO + "### + ); + + test_report!( + malformed_hex_pattern, + indoc!( + r#" + when 1 is + 0xZ -> 3 + _ -> 4 + "# + ), + @r###" + ── SYNTAX PROBLEM ──────────────────────────────────────── /code/proj/Main.roc ─ + + This hex integer pattern is malformed: + + 5│ 0xZ -> 3 + ^^^ + + Tip: Learn more about number literals at TODO + "### + ); + + test_report!( + malformed_oct_pattern, + indoc!( + r#" + when 1 is + 0o9 -> 3 + _ -> 4 + "# + ), + @r###" + ── SYNTAX PROBLEM ──────────────────────────────────────── /code/proj/Main.roc ─ + + This octal integer pattern is malformed: + + 5│ 0o9 -> 3 + ^^^ + + Tip: Learn more about number literals at TODO + "### + ); + + test_report!( + malformed_bin_pattern, + indoc!( + r#" + when 1 is + 0b4 -> 3 + _ -> 4 + "# + ), + @r###" + ── SYNTAX PROBLEM ──────────────────────────────────────── /code/proj/Main.roc ─ + + This binary integer pattern is malformed: + + 5│ 0b4 -> 3 + ^^^ + + Tip: Learn more about number literals at TODO + "### + ); + + test_report!( + missing_fields, + indoc!( + r#" + x : { a : Num.Int *, b : Num.Frac *, c : Str } + x = { b: 4.0 } + + x + "# + ), + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + Something is off with the body of the `x` definition: + + 4│ x : { a : Num.Int *, b : Num.Frac *, c : Str } + 5│ x = { b: 4.0 } + ^^^^^^^^^^ + + The body is a record of type: + + { b : Frac * } + + But the type annotation on `x` says it should be: + + { + a : Int *, + b : Frac *, + c : Str, + } + + Tip: Looks like the c and a fields are missing. + "### + ); + + // this previously reported the message below, not sure which is better + // + // Something is off with the body of the `f` definition: + // + // 1│ f : a, b -> a + // 2│ f = \x, y -> if Bool.true then x else y + // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + // + // The body is an anonymous function of type: + // + // a, a -> a + // + // But the type annotation on `f` says it should be: + // + // a, b -> a + test_report!( + bad_double_rigid, + indoc!( + r#" + f : a, b -> a + f = \x, y -> if Bool.true then x else y + + f + "# + ), + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + Something is off with the `else` branch of this `if` expression: + + 4│ f : a, b -> a + 5│ f = \x, y -> if Bool.true then x else y + ^ + + This `y` value is a: + + b + + But the type annotation on `f` says it should be: + + a + + Tip: Your type annotation uses `b` and `a` as separate type variables. + Your code seems to be saying they are the same though. Maybe they + should be the same in your type annotation? Maybe your code uses them + in a weird way? + "### + ); + + test_report!( + bad_rigid_function, + indoc!( + r#" + f : Str -> msg + f = \_ -> Foo + + f + "# + ), + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + Something is off with the body of the `f` definition: + + 4│ f : Str -> msg + 5│ f = \_ -> Foo + ^^^ + + This `Foo` tag has the type: + + [Foo] + + But the type annotation on `f` says it should be: + + msg + + Tip: The type annotation uses the type variable `msg` to say that this + definition can produce any type of value. But in the body I see that + it will only produce a tag value of a single specific type. Maybe + change the type annotation to be more specific? Maybe change the code + to be more general? + "### + ); + + test_report!( + bad_rigid_value, + indoc!( + r#" + f : msg + f = 0x3 + + f + "# + ), + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + Something is off with the body of the `f` definition: + + 4│ f : msg + 5│ f = 0x3 + ^^^ + + The body is an integer of type: + + Int * + + But the type annotation on `f` says it should be: + + msg + + Tip: The type annotation uses the type variable `msg` to say that this + definition can produce any type of value. But in the body I see that + it will only produce a `Int` value of a single specific type. Maybe + change the type annotation to be more specific? Maybe change the code + to be more general? + "### + ); + + // TODO improve tag suggestions + test_report!( + typo_lowercase_ok, + indoc!( + r#" + f : Str -> [Ok Num.I64, InvalidFoo] + f = \_ -> ok 4 + + f + "# + ), + @r###" + ── UNRECOGNIZED NAME ───────────────────────────────────── /code/proj/Main.roc ─ + + Nothing is named `ok` in this scope. + + 5│ f = \_ -> ok 4 + ^^ + + Did you mean one of these? + + Ok + U8 + Box + Eq + "### + ); + + // these error messages seem pretty helpful + test_report!( + typo_uppercase_ok, + indoc!( + r#" + f : Str -> Num.I64 + f = \_ -> + ok = 3 + + Ok + + f + "# + ), + @r###" + ── UNUSED DEFINITION ───────────────────────────────────── /code/proj/Main.roc ─ + + `ok` is not used anywhere in your code. + + 6│ ok = 3 + ^^ + + If you didn't intend on using `ok` then remove it so future readers of + your code don't wonder why it is there. + + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + Something is off with the body of the `f` definition: + + 4│ f : Str -> Num.I64 + 5│ f = \_ -> + 6│ ok = 3 + 7│ + 8│ Ok + ^^ + + This `Ok` tag has the type: + + [Ok] + + But the type annotation on `f` says it should be: + + I64 + "### + ); + + // invalid recursion + test_report!( + circular_definition_self, + indoc!( + r#" + f = f + + f + "# + ), + @r###" + ── CIRCULAR DEFINITION ─────────────────────────────────── /code/proj/Main.roc ─ + + `f` is defined directly in terms of itself: + + 4│ f = f + ^^^^^ + + Roc evaluates values strictly, so running this program would enter an + infinite loop! + + Hint: Did you mean to define `f` as a function? + "### + ); + + // invalid mutual recursion + test_report!( + circular_definition, + indoc!( + r#" + foo = bar + + bar = foo + + foo + "# + ), + @r###" + ── CIRCULAR DEFINITION ─────────────────────────────────── /code/proj/Main.roc ─ + + The `foo` definition is causing a very tricky infinite loop: + + 4│ foo = bar + ^^^ + + The `foo` value depends on itself through the following chain of + definitions: + + ┌─────┐ + │ foo + │ ↓ + │ bar + └─────┘ + "### + ); + + test_report!( + update_empty_record, + indoc!( + r#" + x = {} + + { x & foo: 3 } + "# + ), + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + This `x` record doesn’t have a `foo` field: + + 6│ { x & foo: 3 } + ^^^^^^ + + In fact, `x` is a record with no fields at all! + "### + ); + + test_report!( + update_record, + indoc!( + r#" + x = { fo: 3, bar: 4 } + + { x & foo: 3 } + "# + ), + // TODO also suggest fields with the correct type + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + This `x` record doesn’t have a `foo` field: + + 6│ { x & foo: 3 } + ^^^^^^ + + There may be a typo. These `x` fields are the most similar: + + { + fo : Num *, + bar : Num *, + } + + Maybe `foo:` should be `fo:` instead? + "### + ); + + test_report!( + update_record_ext, + indoc!( + r#" + f : { fo: Num.I64 }ext -> Num.I64 + f = \r -> + r2 = { r & foo: r.fo } + + r2.fo + + f + "# + ), + // TODO also suggest fields with the correct type + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + This `r` record doesn’t have a `foo` field: + + 6│ r2 = { r & foo: r.fo } + ^^^^^^^^^ + + There may be a typo. These `r` fields are the most similar: + + { + fo : I64, + }ext + + Maybe `foo:` should be `fo:` instead? + "### + ); + + test_report!( + update_record_snippet, + indoc!( + r#" + x = { fo: 3, bar: 4, baz: 3, spam: 42, foobar: 3 } + + { x & foo: 3 } + "# + ), + // TODO also suggest fields with the correct type + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + This `x` record doesn’t have a `foo` field: + + 6│ { x & foo: 3 } + ^^^^^^ + + There may be a typo. These `x` fields are the most similar: + + { + fo : Num *, + foobar : Num *, + bar : Num *, + baz : Num *, + … + } + + Maybe `foo:` should be `fo:` instead? + "### + ); + + test_report!( + plus_on_str, + indoc!( + r#" + 0x4 + "foo" + "# + ), + // TODO also suggest fields with the correct type + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + This 2nd argument to + has an unexpected type: + + 4│ 0x4 + "foo" + ^^^^^ + + The argument is a string of type: + + Str + + But + needs its 2nd argument to be: + + Int * + "### + ); + + test_report!( + int_frac, + indoc!( + r#" + 0x4 + 3.14 + "# + ), + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + This 2nd argument to + has an unexpected type: + + 4│ 0x4 + 3.14 + ^^^^ + + The argument is a fraction of type: + + Frac * + + But + needs its 2nd argument to be: + + Int * + + Tip: You can convert between integers and fractions using functions + like `Num.toFrac` and `Num.round`. + "### + ); + + test_report!( + boolean_tag, + indoc!( + r#" + 42 + True + "# + ), + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + This 2nd argument to + has an unexpected type: + + 4│ 42 + True + ^^^^ + + This `True` tag has the type: + + [True] + + But + needs its 2nd argument to be: + + Num * + "### + ); + + test_report!( + tag_missing, + indoc!( + r#" + f : [A] -> [A, B] + f = \a -> a + + f + "# + ), + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + Something is off with the body of the `f` definition: + + 4│ f : [A] -> [A, B] + 5│ f = \a -> a + ^ + + This `a` value is a: + + […] + + But the type annotation on `f` says it should be: + + [B, …] + + Tip: Looks like a closed tag union does not have the `B` tag. + + Tip: Closed tag unions can't grow, because that might change the size + in memory. Can you use an open tag union? + "### + ); + + test_report!( + tags_missing, + indoc!( + r#" + f : [A] -> [A, B, C] + f = \a -> a + + f + "# + ), + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + Something is off with the body of the `f` definition: + + 4│ f : [A] -> [A, B, C] + 5│ f = \a -> a + ^ + + This `a` value is a: + + […] + + But the type annotation on `f` says it should be: + + [ + B, + C, + … + ] + + Tip: Looks like a closed tag union does not have the `B` and `C` tags. + + Tip: Closed tag unions can't grow, because that might change the size + in memory. Can you use an open tag union? + "### + ); + + test_report!( + patterns_fn_not_exhaustive, + indoc!( + r#" + Either : [Left {}, Right Str] + + x : Either + x = Left {} + + f : Either -> {} + f = \Left v -> v + + f x + "# + ), + @r###" + ── UNSAFE PATTERN ──────────────────────────────────────── /code/proj/Main.roc ─ + + This pattern does not cover all the possibilities: + + 10│ f = \Left v -> v + ^^^^^^ + + Other possibilities include: + + Right _ + + I would have to crash if I saw one of those! So rather than pattern + matching in function arguments, put a `when` in the function body to + account for all possibilities. + + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + Something is off with the body of the `f` definition: + + 9│ f : Either -> {} + 10│ f = \Left v -> v + ^^^^^^^^^^^^ + + The body is an anonymous function of type: + + […] -> {} + + But the type annotation on `f` says it should be: + + [Right Str, …] -> {} + + Tip: Looks like a closed tag union does not have the `Right` tag. + + Tip: Closed tag unions can't grow, because that might change the size + in memory. Can you use an open tag union? + "### + ); + + test_report!( + patterns_let_not_exhaustive, + indoc!( + r#" + x : [Left {}, Right Str] + x = Left {} + + + (Left y) = x + + y + "# + ), + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + This expression is used in an unexpected way: + + 8│ (Left y) = x + ^ + + This `x` value is a: + + [Right Str, …] + + But you are trying to use it as: + + […] + + Tip: Looks like a closed tag union does not have the `Right` tag. + + Tip: Closed tag unions can't grow, because that might change the size + in memory. Can you use an open tag union? + "### + ); + + test_report!( + patterns_when_not_exhaustive, + indoc!( + r#" + when 0x1 is + 2 -> 0x3 + "# + ), + @r###" + ── UNSAFE PATTERN ──────────────────────────────────────── /code/proj/Main.roc ─ + + This `when` does not cover all the possibilities: + + 4│> when 0x1 is + 5│> 2 -> 0x3 + + Other possibilities include: + + _ + + I would have to crash if I saw one of those! Add branches for them! + "### + ); + + test_report!( + patterns_bool_not_exhaustive, + indoc!( + r#" + x : [Red, Green] + x = Green + + when x is + Red -> 3 + "# + ), + @r###" + ── UNSAFE PATTERN ──────────────────────────────────────── /code/proj/Main.roc ─ + + This `when` does not cover all the possibilities: + + 7│> when x is + 8│> Red -> 3 + + Other possibilities include: + + Green + + I would have to crash if I saw one of those! Add branches for them! + "### + ); + + test_report!( + patterns_enum_not_exhaustive, + indoc!( + r#" + x : [Red, Green, Blue] + x = Red + + when x is + Red -> 0 + Green -> 1 + "# + ), + @r###" + ── UNSAFE PATTERN ──────────────────────────────────────── /code/proj/Main.roc ─ + + This `when` does not cover all the possibilities: + + 7│> when x is + 8│> Red -> 0 + 9│> Green -> 1 + + Other possibilities include: + + Blue + + I would have to crash if I saw one of those! Add branches for them! + "### + ); + + test_report!( + patterns_remote_data_not_exhaustive, + indoc!( + r#" + RemoteData e a : [NotAsked, Loading, Failure e, Success a] + + x : RemoteData Num.I64 Str + + when x is + NotAsked -> 3 + "# + ), + @r###" + ── UNSAFE PATTERN ──────────────────────────────────────── /code/proj/Main.roc ─ + + This `when` does not cover all the possibilities: + + 8│> when x is + 9│> NotAsked -> 3 + + Other possibilities include: + + Failure _ + Loading + Success _ + + I would have to crash if I saw one of those! Add branches for them! + "### + ); + + test_report!( + patterns_record_not_exhaustive, + indoc!( + r#" + x = { a: 3 } + + when x is + { a: 4 } -> 4 + "# + ), + // Tip: Looks like a record field guard is not exhaustive. Learn more about record pattern matches at TODO. + @r###" + ── UNSAFE PATTERN ──────────────────────────────────────── /code/proj/Main.roc ─ + + This `when` does not cover all the possibilities: + + 6│> when x is + 7│> { a: 4 } -> 4 + + Other possibilities include: + + { a } + + I would have to crash if I saw one of those! Add branches for them! + "### + ); + + test_report!( + patterns_record_guard_not_exhaustive, + indoc!( + r#" + y : [Nothing, Just Num.I64] + y = Just 4 + x = { a: y, b: 42} + + when x is + { a: Nothing } -> 4 + { a: Just 3 } -> 4 + "# + ), + @r###" + ── UNSAFE PATTERN ──────────────────────────────────────── /code/proj/Main.roc ─ + + This `when` does not cover all the possibilities: + + 8│> when x is + 9│> { a: Nothing } -> 4 + 10│> { a: Just 3 } -> 4 + + Other possibilities include: + + { a: Just _ } + + I would have to crash if I saw one of those! Add branches for them! + "### + ); + + test_report!( + patterns_nested_tag_not_exhaustive, + indoc!( + r#" + when Record Nothing 1 is + Record (Nothing) b -> b + Record (Just 3) b -> b + "# + ), + @r###" + ── UNSAFE PATTERN ──────────────────────────────────────── /code/proj/Main.roc ─ + + This `when` does not cover all the possibilities: + + 4│> when Record Nothing 1 is + 5│> Record (Nothing) b -> b + 6│> Record (Just 3) b -> b + + Other possibilities include: + + Record (Just _) _ + + I would have to crash if I saw one of those! Add branches for them! + "### + ); + + test_report!( + patterns_int_redundant, + indoc!( + r#" + when 0x1 is + 2 -> 3 + 2 -> 4 + _ -> 5 + "# + ), + @r###" + ── REDUNDANT PATTERN ───────────────────────────────────── /code/proj/Main.roc ─ + + The 2nd pattern is redundant: + + 4│ when 0x1 is + 5│ 2 -> 3 + 6│> 2 -> 4 + 7│ _ -> 5 + + Any value of this shape will be handled by a previous pattern, so this + one should be removed. + "### + ); + + test_report!( + unify_alias_other, + indoc!( + r#" + Foo a : { x : Num.Int a } + + f : Foo a -> Num.Int a + f = \r -> r.x + + f { y: 3.14 } + "# + ), + // de-aliases the alias to give a better error message + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + This 1st argument to `f` has an unexpected type: + + 9│ f { y: 3.14 } + ^^^^^^^^^^^ + + The argument is a record of type: + + { y : Frac * } + + But `f` needs its 1st argument to be: + + { x : Int a } + + Tip: Seems like a record field typo. Maybe `y` should be `x`? + + Tip: Can more type annotations be added? Type annotations always help + me give more specific messages, and I think they could help a lot in + this case + "### + ); + + test_report!( + #[ignore] + cyclic_alias, + indoc!( + r#" + Foo : { x : Bar } + Bar : { y : Foo } + + f : Foo + + f + "# + ), + // should not report Bar as unused! + @r###" + ── CYCLIC ALIAS ────────────────────────────────────────── /code/proj/Main.roc ─ + + The `Foo` alias is self-recursive in an invalid way: + + 4│ Foo : { x : Bar } + ^^^ + + Recursion in aliases is only allowed if recursion happens behind a + tagged union, at least one variant of which is not recursive. + "### + ); + + test_report!( + self_recursive_alias, + indoc!( + r#" + Foo : { x : Foo } + + f : Foo + f = 3 + + f + "# + ), + // should not report Bar as unused! + @r###" + ── CYCLIC ALIAS ────────────────────────────────────────── /code/proj/Main.roc ─ + + The `Foo` alias is self-recursive in an invalid way: + + 4│ Foo : { x : Foo } + ^^^ + + Recursion in aliases is only allowed if recursion happens behind a + tagged union, at least one variant of which is not recursive. + "### + ); + + test_report!( + record_duplicate_field_same_type, + indoc!( + r#" + { x: 4, y: 3, x: 4 } + "# + ), + @r###" + ── DUPLICATE FIELD NAME ────────────────────────────────── /code/proj/Main.roc ─ + + This record defines the `.x` field twice! + + 4│ { x: 4, y: 3, x: 4 } + ^^^^ ^^^^ + + In the rest of the program, I will only use the latter definition: + + 4│ { x: 4, y: 3, x: 4 } + ^^^^ + + For clarity, remove the previous `.x` definitions from this record. + "### + ); + + test_report!( + record_duplicate_field_different_types, + indoc!( + r#" + { x: 4, y: 3, x: "foo" } + "# + ), + @r###" + ── DUPLICATE FIELD NAME ────────────────────────────────── /code/proj/Main.roc ─ + + This record defines the `.x` field twice! + + 4│ { x: 4, y: 3, x: "foo" } + ^^^^ ^^^^^^^^ + + In the rest of the program, I will only use the latter definition: + + 4│ { x: 4, y: 3, x: "foo" } + ^^^^^^^^ + + For clarity, remove the previous `.x` definitions from this record. + "### + ); + + test_report!( + record_duplicate_field_multiline, + indoc!( + r#" + { + x: 4, + y: 3, + x: "foo" + } + "# + ), + @r###" + ── DUPLICATE FIELD NAME ────────────────────────────────── /code/proj/Main.roc ─ + + This record defines the `.x` field twice! + + 4│ { + 5│> x: 4, + 6│ y: 3, + 7│> x: "foo" + 8│ } + + In the rest of the program, I will only use the latter definition: + + 4│ { + 5│ x: 4, + 6│ y: 3, + 7│> x: "foo" + 8│ } + + For clarity, remove the previous `.x` definitions from this record. + "### + ); + + test_report!( + record_update_duplicate_field_multiline, + indoc!( + r#" + \r -> + { r & + x: 4, + y: 3, + x: "foo" + } + "# + ), + @r###" + ── DUPLICATE FIELD NAME ────────────────────────────────── /code/proj/Main.roc ─ + + This record defines the `.x` field twice! + + 5│ { r & + 6│> x: 4, + 7│ y: 3, + 8│> x: "foo" + 9│ } + + In the rest of the program, I will only use the latter definition: + + 5│ { r & + 6│ x: 4, + 7│ y: 3, + 8│> x: "foo" + 9│ } + + For clarity, remove the previous `.x` definitions from this record. + "### + ); + + test_report!( + record_type_duplicate_field, + indoc!( + r#" + a : { foo : Num.I64, bar : {}, foo : Str } + a = { bar: {}, foo: "foo" } + + a + "# + ), + @r###" + ── DUPLICATE FIELD NAME ────────────────────────────────── /code/proj/Main.roc ─ + + This record type defines the `.foo` field twice! + + 4│ a : { foo : Num.I64, bar : {}, foo : Str } + ^^^^^^^^^^^^^ ^^^^^^^^^ + + In the rest of the program, I will only use the latter definition: + + 4│ a : { foo : Num.I64, bar : {}, foo : Str } + ^^^^^^^^^ + + For clarity, remove the previous `.foo` definitions from this record + type. + "### + ); + + test_report!( + tag_union_duplicate_tag, + indoc!( + r#" + a : [Foo Num.I64, Bar {}, Foo Str] + a = Foo "foo" + + a + "# + ), + @r###" + ── DUPLICATE TAG NAME ──────────────────────────────────── /code/proj/Main.roc ─ + + This tag union type defines the `Foo` tag twice! + + 4│ a : [Foo Num.I64, Bar {}, Foo Str] + ^^^^^^^^^^^ ^^^^^^^ + + In the rest of the program, I will only use the latter definition: + + 4│ a : [Foo Num.I64, Bar {}, Foo Str] + ^^^^^^^ + + For clarity, remove the previous `Foo` definitions from this tag union + type. + "### + ); + + test_report!( + annotation_definition_mismatch, + indoc!( + r#" + bar : Num.I64 + foo = \x -> x + + # NOTE: neither bar or foo are defined at this point + 4 + "# + ), + @r###" + ── NAMING PROBLEM ──────────────────────────────────────── /code/proj/Main.roc ─ + + This annotation does not match the definition immediately following + it: + + 4│> bar : Num.I64 + 5│> foo = \x -> x + + Is it a typo? If not, put either a newline or comment between them. + "### + ); + + test_report!( + annotation_newline_body_is_fine, + indoc!( + r#" + bar : Num.I64 + + foo = \x -> x + + foo bar + "# + ), + @"" + ); + + test_report!( + invalid_alias_rigid_var_pattern, + indoc!( + r#" + MyAlias 1 : Num.I64 + + 4 + "# + ), + @r###" + ── SYNTAX PROBLEM ──────────────────────────────────────── /code/proj/Main.roc ─ + + This definition of `MyAlias` has an unexpected pattern: + + 4│ MyAlias 1 : Num.I64 + ^ + + Only type variables like `a` or `value` can occur in this position. + + ── UNUSED DEFINITION ───────────────────────────────────── /code/proj/Main.roc ─ + + `MyAlias` is not used anywhere in your code. + + 4│ MyAlias 1 : Num.I64 + ^^^^^^^^^^^^^^^^^^^ + + If you didn't intend on using `MyAlias` then remove it so future readers + of your code don't wonder why it is there. + "### + ); + + test_report!( + invalid_opaque_rigid_var_pattern, + indoc!( + r#" + Age 1 := Num.I64 + + a : Age + a + "# + ), + @r###" + ── SYNTAX PROBLEM ──────────────────────────────────────── /code/proj/Main.roc ─ + + This definition of `Age` has an unexpected pattern: + + 4│ Age 1 := Num.I64 + ^ + + Only type variables like `a` or `value` can occur in this position. + "### + ); + + test_report!( + invalid_num, + indoc!( + r#" + a : Num.Num Num.I64 Num.F64 + a = 3 + + a + "# + ), + @r###" + ── TOO MANY TYPE ARGUMENTS ─────────────────────────────── /code/proj/Main.roc ─ + + The `Num` opaque expects 1 type argument, but it got 2 instead: + + 4│ a : Num.Num Num.I64 Num.F64 + ^^^^^^^^^^^^^^^^^^^^^^^ + + Are there missing parentheses? + "### + ); + + test_report!( + invalid_num_fn, + indoc!( + r#" + f : Str -> Num.Num Num.I64 Num.F64 + f = \_ -> 3 + + f + "# + ), + @r###" + ── TOO MANY TYPE ARGUMENTS ─────────────────────────────── /code/proj/Main.roc ─ + + The `Num` opaque expects 1 type argument, but it got 2 instead: + + 4│ f : Str -> Num.Num Num.I64 Num.F64 + ^^^^^^^^^^^^^^^^^^^^^^^ + + Are there missing parentheses? + "### + ); + + test_report!( + too_few_type_arguments, + indoc!( + r#" + Pair a b : [Pair a b] + + x : Pair Num.I64 + x = Pair 2 3 + + x + "# + ), + @r###" + ── TOO FEW TYPE ARGUMENTS ──────────────────────────────── /code/proj/Main.roc ─ + + The `Pair` alias expects 2 type arguments, but it got 1 instead: + + 6│ x : Pair Num.I64 + ^^^^^^^^^^^^ + + Are there missing parentheses? + "### + ); + + test_report!( + too_many_type_arguments, + indoc!( + r#" + Pair a b : [Pair a b] + + x : Pair Num.I64 Num.I64 Num.I64 + x = 3 + + x + "# + ), + @r###" + ── TOO MANY TYPE ARGUMENTS ─────────────────────────────── /code/proj/Main.roc ─ + + The `Pair` alias expects 2 type arguments, but it got 3 instead: + + 6│ x : Pair Num.I64 Num.I64 Num.I64 + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + Are there missing parentheses? + "### + ); + + test_report!( + phantom_type_variable, + indoc!( + r#" + Foo a : [Foo] + + f : Foo Num.I64 + + f + "# + ), + @r###" + ── UNUSED TYPE ALIAS PARAMETER ─────────────────────────── /code/proj/Main.roc ─ + + The `a` type parameter is not used in the `Foo` alias definition: + + 4│ Foo a : [Foo] + ^ + + Roc does not allow unused type parameters! + + Tip: If you want an unused type parameter (a so-called "phantom + type"), read the guide section on phantom values. + "### + ); + + test_report!( + elm_function_syntax, + indoc!( + r#" + f x y = x + "# + ), + @r###" + ── ARGUMENTS BEFORE EQUALS ────────────────── tmp/elm_function_syntax/Test.roc ─ + + I am partway through parsing a definition, but I got stuck here: + + 1│ app "test" provides [main] to "./platform" + 2│ + 3│ main = + 4│ f x y = x + ^^^ + + Looks like you are trying to define a function. In roc, functions are + always written as a lambda, like increment = \n -> n + 1. + "### + ); + + test_report!( + two_different_cons, + indoc!( + r#" + ConsList a : [Cons a (ConsList a), Nil] + + x : ConsList {} + x = Cons {} (Cons "foo" Nil) + + x + "# + ), + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + Something is off with the body of the `x` definition: + + 6│ x : ConsList {} + 7│ x = Cons {} (Cons "foo" Nil) + ^^^^^^^^^^^^^^^^^^^^^^^^ + + This `Cons` tag application has the type: + + [ + Cons {} [ + Cons Str [ + Cons {} a, + Nil, + ]b as a, + Nil, + ]b, + Nil, + ]b + + But the type annotation on `x` says it should be: + + [ + Cons {} a, + Nil, + ] as a + "### + ); + + test_report!( + mutually_recursive_types_with_type_error, + indoc!( + r#" + AList a b : [ACons a (BList a b), ANil] + BList a b : [BCons a (AList a b), BNil] + + x : AList Num.I64 Num.I64 + x = ACons 0 (BCons 1 (ACons "foo" BNil )) + + y : BList a a + y = BNil + + { x, y } + "# + ), + // TODO render tag unions across multiple lines + // TODO do not show recursion var if the recursion var does not render on the surface of a type + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + Something is off with the body of the `x` definition: + + 7│ x : AList Num.I64 Num.I64 + 8│ x = ACons 0 (BCons 1 (ACons "foo" BNil )) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + This `ACons` tag application has the type: + + [ + ACons (Int Signed64) [ + BCons (Int Signed64) [ + ACons Str [ + BCons I64 [ + ACons I64 (BList I64 I64), + ANil, + ]b as ∞, + BNil, + ]c, + ANil, + ]b, + BNil, + ]c, + ANil, + ]b + + But the type annotation on `x` says it should be: + + [ + ACons I64 (BList I64 I64), + ANil, + ] as a + "### + ); + + test_report!( + integer_out_of_range, + indoc!( + r#" + x = 170_141_183_460_469_231_731_687_303_715_884_105_728_000 + + y = -170_141_183_460_469_231_731_687_303_715_884_105_728_000 + + h = 0xFFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF + l = -0xFFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF + + minlit = -170_141_183_460_469_231_731_687_303_715_884_105_728 + maxlit = 340_282_366_920_938_463_463_374_607_431_768_211_455 + + x + y + h + l + minlit + maxlit + "# + ), + @r###" + ── SYNTAX PROBLEM ──────────────────────────────────────── /code/proj/Main.roc ─ + + This integer literal is too big: + + 4│ x = 170_141_183_460_469_231_731_687_303_715_884_105_728_000 + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + The largest number representable in Roc is the maximum U128 value, + 340_282_366_920_938_463_463_374_607_431_768_211_455. + + Tip: Learn more about number literals at TODO + + ── SYNTAX PROBLEM ──────────────────────────────────────── /code/proj/Main.roc ─ + + This integer literal is too small: + + 6│ y = -170_141_183_460_469_231_731_687_303_715_884_105_728_000 + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + The smallest number representable in Roc is the minimum I128 value, + -170_141_183_460_469_231_731_687_303_715_884_105_728. + + Tip: Learn more about number literals at TODO + + ── SYNTAX PROBLEM ──────────────────────────────────────── /code/proj/Main.roc ─ + + This integer literal is too big: + + 8│ h = 0xFFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + The largest number representable in Roc is the maximum U128 value, + 340_282_366_920_938_463_463_374_607_431_768_211_455. + + Tip: Learn more about number literals at TODO + + ── SYNTAX PROBLEM ──────────────────────────────────────── /code/proj/Main.roc ─ + + This integer literal is too small: + + 9│ l = -0xFFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + The smallest number representable in Roc is the minimum I128 value, + -170_141_183_460_469_231_731_687_303_715_884_105_728. + + Tip: Learn more about number literals at TODO + + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + This 2nd argument to + has an unexpected type: + + 14│ x + y + h + l + minlit + maxlit + ^^^^^^ + + This `maxlit` value is a: + + U128 + + But + needs its 2nd argument to be: + + I128 or Dec + "### + ); + + // have to deal with some whitespace issues because of the format! macro + test_report!( + float_out_of_range, + indoc!( + r#" + overflow = 11.7976931348623157e308 + underflow = -11.7976931348623157e308 + + overflow + underflow + "# + ), + @r###" + ── SYNTAX PROBLEM ──────────────────────────────────────── /code/proj/Main.roc ─ + + This float literal is too big: + + 4│ overflow = 11.7976931348623157e308 + ^^^^^^^^^^^^^^^^^^^^^^^ + + Roc uses signed 64-bit floating points, allowing values between + -1.7976931348623157e308 and 1.7976931348623157e308 + + Tip: Learn more about number literals at TODO + + ── SYNTAX PROBLEM ──────────────────────────────────────── /code/proj/Main.roc ─ + + This float literal is too small: + + 5│ underflow = -11.7976931348623157e308 + ^^^^^^^^^^^^^^^^^^^^^^^^ + + Roc uses signed 64-bit floating points, allowing values between + -1.7976931348623157e308 and 1.7976931348623157e308 + + Tip: Learn more about number literals at TODO + "### + ); + + // the generated messages here are incorrect. Waiting for a rust nightly feature to land, + // see https://github.com/rust-lang/rust/issues/22639 + // this test is here to spot regressions in error reporting + test_report!( + integer_malformed, + indoc!( + r#" + dec = 100A + + hex = 0xZZZ + + oct = 0o9 + + bin = 0b2 + + dec + hex + oct + bin + "# + ), + @r###" + ── SYNTAX PROBLEM ──────────────────────────────────────── /code/proj/Main.roc ─ + + This integer literal contains an invalid digit: + + 4│ dec = 100A + ^^^^ + + Integer literals can only contain the digits + 0-9, or have an integer suffix. + + Tip: Learn more about number literals at TODO + + ── SYNTAX PROBLEM ──────────────────────────────────────── /code/proj/Main.roc ─ + + This hex integer literal contains an invalid digit: + + 6│ hex = 0xZZZ + ^^^^^ + + Hexadecimal (base-16) integer literals can only contain the digits + 0-9, a-f and A-F, or have an integer suffix. + + Tip: Learn more about number literals at TODO + + ── SYNTAX PROBLEM ──────────────────────────────────────── /code/proj/Main.roc ─ + + This octal integer literal contains an invalid digit: + + 8│ oct = 0o9 + ^^^ + + Octal (base-8) integer literals can only contain the digits + 0-7, or have an integer suffix. + + Tip: Learn more about number literals at TODO + + ── SYNTAX PROBLEM ──────────────────────────────────────── /code/proj/Main.roc ─ + + This binary integer literal contains an invalid digit: + + 10│ bin = 0b2 + ^^^ + + Binary (base-2) integer literals can only contain the digits + 0 and 1, or have an integer suffix. + + Tip: Learn more about number literals at TODO + "### + ); + + test_report!( + integer_empty, + indoc!( + r#" + dec = 20 + + hex = 0x + + oct = 0o + + bin = 0b + + dec + hex + oct + bin + "# + ), + @r###" + ── SYNTAX PROBLEM ──────────────────────────────────────── /code/proj/Main.roc ─ + + This hex integer literal contains no digits: + + 6│ hex = 0x + ^^ + + Hexadecimal (base-16) integer literals must contain at least one of + the digits 0-9, a-f and A-F, or have an integer suffix. + + Tip: Learn more about number literals at TODO + + ── SYNTAX PROBLEM ──────────────────────────────────────── /code/proj/Main.roc ─ + + This octal integer literal contains no digits: + + 8│ oct = 0o + ^^ + + Octal (base-8) integer literals must contain at least one of the + digits 0-7, or have an integer suffix. + + Tip: Learn more about number literals at TODO + + ── SYNTAX PROBLEM ──────────────────────────────────────── /code/proj/Main.roc ─ + + This binary integer literal contains no digits: + + 10│ bin = 0b + ^^ + + Binary (base-2) integer literals must contain at least one of the + digits 0 and 1, or have an integer suffix. + + Tip: Learn more about number literals at TODO + "### + ); + + test_report!( + float_malformed, + indoc!( + r#" + x = 3.0A + + x + "# + ), + @r###" + ── SYNTAX PROBLEM ──────────────────────────────────────── /code/proj/Main.roc ─ + + This float literal contains an invalid digit: + + 4│ x = 3.0A + ^^^^ + + Floating point literals can only contain the digits 0-9, or use + scientific notation 10e4, or have a float suffix. + + Tip: Learn more about number literals at TODO + "### + ); + + test_report!( + invalid_record_update, + indoc!( + r#" + foo = { bar: 3 } + updateNestedRecord = { foo.bar & x: 4 } + + example = { age: 42 } + + # these should work + y = { Test.example & age: 3 } + x = { example & age: 4 } + + { updateNestedRecord, foo, x, y } + "# + ), + @r###" + ── SYNTAX PROBLEM ──────────────────────────────────────── /code/proj/Main.roc ─ + + This expression cannot be updated: + + 5│ updateNestedRecord = { foo.bar & x: 4 } + ^^^^^^^ + + Only variables can be updated with record update syntax. + + ── MODULE NOT IMPORTED ─────────────────────────────────── /code/proj/Main.roc ─ + + The `Test` module is not imported: + + 10│ y = { Test.example & age: 3 } + ^^^^^^^^^^^^ + + Is there an import missing? Perhaps there is a typo. Did you mean one + of these? + + Set + List + Dict + Hash + + ── SYNTAX PROBLEM ──────────────────────────────────────── /code/proj/Main.roc ─ + + This expression cannot be updated: + + 10│ y = { Test.example & age: 3 } + ^^^^^^^^^^^^ + + Only variables can be updated with record update syntax. + "### + ); + + test_report!( + module_not_imported, + indoc!( + r#" + Foo.test + "# + ), + @r###" + ── MODULE NOT IMPORTED ─────────────────────────────────── /code/proj/Main.roc ─ + + The `Foo` module is not imported: + + 4│ Foo.test + ^^^^^^^^ + + Is there an import missing? Perhaps there is a typo. Did you mean one + of these? + + Box + Bool + Num + Set + "### + ); + + test_report!( + optional_record_default_type_error, + indoc!( + r#" + \{ x, y ? True } -> x + y + "# + ), + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + This 2nd argument to + has an unexpected type: + + 4│ \{ x, y ? True } -> x + y + ^ + + This `y` value is a: + + [True] + + But + needs its 2nd argument to be: + + Num a + "### + ); + + test_report!( + optional_record_default_with_signature, + indoc!( + r#" + f : { x : Num.I64, y ? Num.I64 } -> Num.I64 + f = \{ x, y ? "foo" } -> (\g, _ -> g) x y + + f + "# + ), + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + The 1st argument to `f` is weird: + + 5│ f = \{ x, y ? "foo" } -> (\g, _ -> g) x y + ^^^^^^^^^^^^^^^^ + + The argument is a pattern that matches record values of type: + + { y ? Str, … } + + But the annotation on `f` says the 1st argument should be: + + { y ? I64, … } + "### + ); + + test_report!( + optional_record_invalid_let_binding, + indoc!( + r#" + \rec -> + { x, y } : { x : Num.I64, y ? Str } + { x, y } = rec + + { x, y } + "# + ), + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + Something is off with the body of this definition: + + 5│> { x, y } : { x : Num.I64, y ? Str } + 6│> { x, y } = rec + + The body is a value of type: + + { y : Str, … } + + But the type annotation says it should be: + + { y ? Str, … } + + Tip: To extract the `.y` field it must be non-optional, but the type + says this field is optional. Learn more about optional fields at TODO. + "### + ); + + test_report!( + optional_record_invalid_function, + indoc!( + r#" + f : { x : Num.I64, y ? Num.I64 } -> Num.I64 + f = \{ x, y } -> x + y + + f + "# + ), + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + The 1st argument to `f` is weird: + + 5│ f = \{ x, y } -> x + y + ^^^^^^^^ + + The argument is a pattern that matches record values of type: + + { y : I64, … } + + But the annotation on `f` says the 1st argument should be: + + { y ? I64, … } + + Tip: To extract the `.y` field it must be non-optional, but the type + says this field is optional. Learn more about optional fields at TODO. + "### + ); + + test_report!( + optional_record_invalid_when, + indoc!( + r#" + f : { x : Num.I64, y ? Num.I64 } -> Num.I64 + f = \r -> + when r is + { x, y } -> x + y + + f + "# + ), + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + The branches of this `when` expression don't match the condition: + + 6│> when r is + 7│ { x, y } -> x + y + + This `r` value is a: + + { y ? I64, … } + + But the branch patterns have type: + + { y : I64, … } + + The branches must be cases of the `when` condition's type! + + Tip: To extract the `.y` field it must be non-optional, but the type + says this field is optional. Learn more about optional fields at TODO. + "### + ); + + test_report!( + optional_record_invalid_access, + indoc!( + r#" + f : { x : Num.I64, y ? Num.I64 } -> Num.I64 + f = \r -> r.y + + f + "# + ), + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + This expression is used in an unexpected way: + + 5│ f = \r -> r.y + ^^^ + + This `r` value is a: + + { y ? I64, … } + + But you are trying to use it as: + + { y : I64, … } + + Tip: To extract the `.y` field it must be non-optional, but the type + says this field is optional. Learn more about optional fields at TODO. + "### + ); + + test_report!( + optional_record_invalid_accessor, + indoc!( + r#" + f : { x : Num.I64, y ? Num.I64 } -> Num.I64 + f = \r -> .y r + + f + "# + ), + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + This 1st argument to this function has an unexpected type: + + 5│ f = \r -> .y r + ^ + + This `r` value is a: + + { y ? I64, … } + + But this function needs its 1st argument to be: + + { y : I64, … } + + Tip: To extract the `.y` field it must be non-optional, but the type + says this field is optional. Learn more about optional fields at TODO. + "### + ); + + test_report!( + guard_mismatch_with_annotation, + indoc!( + r#" + f : { x : Num.I64, y : Num.I64 } -> Num.I64 + f = \r -> + when r is + { x, y : "foo" } -> x + 0 + _ -> 0 + + f + "# + ), + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + The branches of this `when` expression don't match the condition: + + 6│> when r is + 7│ { x, y : "foo" } -> x + 0 + 8│ _ -> 0 + + This `r` value is a: + + { y : I64, … } + + But the branch patterns have type: + + { y : Str, … } + + The branches must be cases of the `when` condition's type! + "### + ); + + test_report!( + optional_field_mismatch_with_annotation, + indoc!( + r#" + f : { x : Num.I64, y ? Num.I64 } -> Num.I64 + f = \r -> + when r is + { x, y ? "foo" } -> (\g, _ -> g) x y + _ -> 0 + + f + "# + ), + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + The branches of this `when` expression don't match the condition: + + 6│> when r is + 7│ { x, y ? "foo" } -> (\g, _ -> g) x y + 8│ _ -> 0 + + This `r` value is a: + + { y ? I64, … } + + But the branch patterns have type: + + { y ? Str, … } + + The branches must be cases of the `when` condition's type! + "### + ); + + test_report!( + incorrect_optional_field, + indoc!( + r#" + { x: 5, y ? 42 } + "# + ), + @r###" + ── BAD OPTIONAL VALUE ──────────────────────────────────── /code/proj/Main.roc ─ + + This record uses an optional value for the `.y` field in an incorrect + context! + + 4│ { x: 5, y ? 42 } + ^^^^^^ + + You can only use optional values in record destructuring, like: + + { answer ? 42, otherField } = myRecord + "### + ); + + test_report!( + first_wildcard_is_required, + indoc!( + r#" + when Foo 1 2 3 is + Foo _ 1 _ -> 1 + _ -> 2 + "# + ), + @"" + ); + + test_report!( + second_wildcard_is_redundant, + indoc!( + r#" + when Foo 1 2 3 is + Foo _ 1 _ -> 1 + _ -> 2 + _ -> 3 + "# + ), + @r###" + ── REDUNDANT PATTERN ───────────────────────────────────── /code/proj/Main.roc ─ + + The 3rd pattern is redundant: + + 4│ when Foo 1 2 3 is + 5│ Foo _ 1 _ -> 1 + 6│ _ -> 2 + 7│ _ -> 3 + ^ + + Any value of this shape will be handled by a previous pattern, so this + one should be removed. + "### + ); + + test_report!( + alias_using_alias, + indoc!( + r#" + # The color of a node. Leaves are considered Black. + NodeColor : [Red, Black] + + RBTree k v : [Node NodeColor k v (RBTree k v) (RBTree k v), Empty] + + # Create an empty dictionary. + empty : RBTree k v + empty = + Empty + + empty + "# + ), + @"" + ); + + test_report!( + unused_argument, + indoc!( + r#" + f = \foo -> 1 + + f + "# + ), + @r###" + ── UNUSED ARGUMENT ─────────────────────────────────────── /code/proj/Main.roc ─ + + `f` doesn't use `foo`. + + 4│ f = \foo -> 1 + ^^^ + + If you don't need `foo`, then you can just remove it. However, if you + really do need `foo` as an argument of `f`, prefix it with an underscore, + like this: "_`foo`". Adding an underscore at the start of a variable + name is a way of saying that the variable is not used. + "### + ); + + test_report!( + qualified_tag, + indoc!( + r#" + Foo.Bar + "# + ), + @r###" + ── SYNTAX PROBLEM ──────────────────────────────────────── /code/proj/Main.roc ─ + + I am trying to parse a qualified name here: + + 4│ Foo.Bar + ^ + + This looks like a qualified tag name to me, but tags cannot be + qualified! Maybe you wanted a qualified name, something like + Json.Decode.string? + "### + ); + + test_report!( + module_ident_ends_with_dot, + indoc!( + r#" + Foo.Bar. + "# + ), + @r###" + ── SYNTAX PROBLEM ──────────────────────────────────────── /code/proj/Main.roc ─ + + I am trying to parse a qualified name here: + + 4│ Foo.Bar. + ^ + + I was expecting to see an identifier next, like height. A complete + qualified name looks something like Json.Decode.string. + "### + ); + + test_report!( + record_access_ends_with_dot, + indoc!( + r#" + foo.bar. + "# + ), + @r###" + ── SYNTAX PROBLEM ──────────────────────────────────────── /code/proj/Main.roc ─ + + I am trying to parse a record field access here: + + 4│ foo.bar. + ^ + + So I expect to see a lowercase letter next, like .name or .height. + "### + ); + + test_report!( + type_annotation_double_colon, + indoc!( + r#" + f :: I64 + f = 42 + + f + "# + ), + @r###" + ── UNKNOWN OPERATOR ──────────────── tmp/type_annotation_double_colon/Test.roc ─ + + This looks like an operator, but it's not one I recognize! + + 1│ app "test" provides [main] to "./platform" + 2│ + 3│ main = + 4│ f :: I64 + ^^ + + I have no specific suggestion for this operator, see TODO for the full + list of operators in Roc. + "### + ); + + // NOTE: VERY BAD ERROR MESSAGE + // + // looks like `x y` are considered argument to the add, even though they are + // on a lower indentation level + test_report!( + double_equals_in_def, + indoc!( + r#" + x = 3 + y = + x == 5 + Num.add 1 2 + + { x, y } + "# + ), + @r###" + ── TOO MANY ARGS ───────────────────────────────────────── /code/proj/Main.roc ─ + + This value is not a function, but it was given 3 arguments: + + 6│ x == 5 + ^ + + Are there any missing commas? Or missing parentheses? + "### + ); + + test_report!( + tag_union_open, + indoc!( + r#" + f : [ + "# + ), + @r###" + ── UNFINISHED TAG UNION TYPE ───────────────────── tmp/tag_union_open/Test.roc ─ + + I am partway through parsing a tag union type, but I got stuck here: + + 4│ f : [ + 5│ + 6│ + ^ + + I was expecting to see a closing square bracket before this, so try + adding a ] and see if that helps? + "### + ); + + test_report!( + tag_union_end, + indoc!( + r#" + f : [Yes, + "# + ), + @r###" + ── UNFINISHED TAG UNION TYPE ────────────────────── tmp/tag_union_end/Test.roc ─ + + I am partway through parsing a tag union type, but I got stuck here: + + 4│ f : [Yes, + 5│ + 6│ + ^ + + I was expecting to see a closing square bracket before this, so try + adding a ] and see if that helps? + "### + ); + + test_report!( + tag_union_lowercase_tag_name, + indoc!( + r#" + f : [lowercase] + "# + ), + @r###" + ── WEIRD TAG NAME ────────────────── tmp/tag_union_lowercase_tag_name/Test.roc ─ + + I am partway through parsing a tag union type, but I got stuck here: + + 4│ f : [lowercase] + ^ + + I was expecting to see a tag name. + + Hint: Tag names start with an uppercase letter, like Err or Green. + "### + ); + + test_report!( + tag_union_second_lowercase_tag_name, + indoc!( + r#" + f : [Good, bad] + "# + ), + @r###" + ── WEIRD TAG NAME ─────────── tmp/tag_union_second_lowercase_tag_name/Test.roc ─ + + I am partway through parsing a tag union type, but I got stuck here: + + 4│ f : [Good, bad] + ^ + + I was expecting to see a tag name. + + Hint: Tag names start with an uppercase letter, like Err or Green. + "### + ); + + test_report!( + record_type_open, + indoc!( + r#" + f : { + "# + ), + @r###" + ── UNFINISHED RECORD TYPE ────────────────────── tmp/record_type_open/Test.roc ─ + + I am partway through parsing a record type, but I got stuck here: + + 4│ f : { + 5│ + 6│ + ^ + + I was expecting to see a closing curly brace before this, so try + adding a } and see if that helps? + "### + ); + + test_report!( + record_type_open_indent, + indoc!( + r#" + f : { + foo : I64, + "# + ), + @r###" + ── UNFINISHED RECORD TYPE ─────────────── tmp/record_type_open_indent/Test.roc ─ + + I am partway through parsing a record type, but I got stuck here: + + 4│ f : { + 5│ foo : I64, + 6│ + 7│ + ^ + + I was expecting to see a closing curly brace before this, so try + adding a } and see if that helps? + "### + ); + + test_report!( + record_type_end, + indoc!( + r#" + f : { a: Int, + "# + ), + @r###" + ── UNFINISHED RECORD TYPE ─────────────────────── tmp/record_type_end/Test.roc ─ + + I am partway through parsing a record type, but I got stuck here: + + 4│ f : { a: Int, + 5│ + 6│ + ^ + + I was expecting to see a closing curly brace before this, so try + adding a } and see if that helps? + "### + ); + + test_report!( + record_type_keyword_field_name, + indoc!( + r#" + f : { if : I64 } + "# + ), + @r###" + ── UNFINISHED RECORD TYPE ──────── tmp/record_type_keyword_field_name/Test.roc ─ + + I just started parsing a record type, but I got stuck on this field + name: + + 4│ f : { if : I64 } + ^^ + + Looks like you are trying to use `if` as a field name, but that is a + reserved word. Try using a different name! + "### + ); + + // a case where the message cannot be as good as elm's + test_report!( + record_type_missing_comma, + indoc!( + r#" + f : { foo bar } + "# + ), + @r###" + ── UNFINISHED RECORD TYPE ───────────── tmp/record_type_missing_comma/Test.roc ─ + + I am partway through parsing a record type, but I got stuck here: + + 4│ f : { foo bar } + ^ + + I was expecting to see a colon, question mark, comma or closing curly + brace. + "### + ); + + // a case where the message cannot be as good as elm's + test_report!( + record_type_tab, + "f : { foo \t }", + @r###" + ── TAB CHARACTER ──────────────────────────────── tmp/record_type_tab/Test.roc ─ + + I encountered a tab character: + + 4│ f : { foo } + ^ + + Tab characters are not allowed, use spaces instead. + "### + ); + + test_report!( + comment_with_tab, + "# comment with a \t\n4", + @r###" + ── TAB CHARACTER ─────────────────────────────── tmp/comment_with_tab/Test.roc ─ + + I encountered a tab character: + + 4│ # comment with a + ^ + + Tab characters are not allowed, use spaces instead. + "### + ); + + test_report!( + comment_with_control_character, + "# comment with a \x07\n", + @r###" + ── ASCII CONTROL CHARACTER ─────── tmp/comment_with_control_character/Test.roc ─ + + I encountered an ASCII control character: + + 4│ # comment with a + ^ + + ASCII control characters are not allowed. + "### + ); + + test_report!( + record_type_carriage_return, + "f : { \r foo }", + @r###" + ── MISPLACED CARRIAGE RETURN ──────── tmp/record_type_carriage_return/Test.roc ─ + + I encountered a stray carriage return (\r): + + 4│ f : { foo } + ^ + + A carriage return (\r) has to be followed by a newline (\n). + "### + ); + + // TODO bad error message + test_report!( + type_in_parens_start, + indoc!( + r#" + f : ( + "# + ), + @r###" + ── UNFINISHED PARENTHESES ────────────────── tmp/type_in_parens_start/Test.roc ─ + + I am partway through parsing a type in parentheses, but I got stuck + here: + + 4│ f : ( + 5│ + 6│ + ^ + + I was expecting to see a closing parenthesis before this, so try + adding a ) and see if that helps? + "### + ); + + test_report!( + type_in_parens_end, + indoc!( + r#" + f : ( I64 + "# + ), + @r###" + ── UNFINISHED PARENTHESES ──────────────────── tmp/type_in_parens_end/Test.roc ─ + + I am partway through parsing a type in parentheses, but I got stuck + here: + + 4│ f : ( I64 + 5│ + 6│ + ^ + + I was expecting to see a closing parenthesis before this, so try + adding a ) and see if that helps? + "### + ); + + test_report!( + type_apply_double_dot, + indoc!( + r#" + f : Foo..Bar + + f + "# + ), + @r###" + ── SYNTAX PROBLEM ──────────────────────────────────────── /code/proj/Main.roc ─ + + I am confused by this type name: + + 4│ f : Foo..Bar + ^^^^^^^^ + + Type names start with an uppercase letter, and can optionally be + qualified by a module name, like Bool or Http.Request.Request. + "### + ); + // ── DOUBLE DOT ────────────────────────────────────────────────────────────────── + // + // I encountered two dots in a row: + // + // 1│ f : Foo..Bar + // ^ + // + // Try removing one of them. + + test_report!( + type_apply_trailing_dot, + indoc!( + r#" + f : Foo.Bar. + + f + "# + ), + @r###" + ── SYNTAX PROBLEM ──────────────────────────────────────── /code/proj/Main.roc ─ + + I am confused by this type name: + + 4│ f : Foo.Bar. + ^^^^^^^^ + + Type names start with an uppercase letter, and can optionally be + qualified by a module name, like Bool or Http.Request.Request. + "### + ); + // ── TRAILING DOT ──────────────────────────────────────────────────────────────── + // + // I encountered a dot with nothing after it: + // + // 1│ f : Foo.Bar. + // ^ + // + // Dots are used to refer to a type in a qualified way, like + // Num.I64 or List.List a. Try adding a type name next. + + test_report!( + type_apply_stray_dot, + indoc!( + r#" + f : . + "# + ), + @r###" + ── UNFINISHED TYPE ───────────────────────── tmp/type_apply_stray_dot/Test.roc ─ + + I just started parsing a type, but I got stuck here: + + 4│ f : . + ^ + + I am expecting a type next, like Bool or List a. + "### + ); + + test_report!( + type_apply_start_with_number, + indoc!( + r#" + f : Foo.1 + + f + "# + ), + @r###" + ── SYNTAX PROBLEM ──────────────────────────────────────── /code/proj/Main.roc ─ + + I am confused by this type name: + + 4│ f : Foo.1 + ^^^^^ + + Type names start with an uppercase letter, and can optionally be + qualified by a module name, like Bool or Http.Request.Request. + "### + ); + // ── WEIRD QUALIFIED NAME ──────────────────────────────────────────────────────── + // + // I encountered a number at the start of a qualified name segment: + // + // 1│ f : Foo.1 + // ^ + // + // All parts of a qualified type name must start with an uppercase + // letter, like Num.I64 or List.List a. + + test_report!( + type_apply_start_with_lowercase, + indoc!( + r#" + f : Foo.foo + + f + "# + ), + @r###" + ── SYNTAX PROBLEM ──────────────────────────────────────── /code/proj/Main.roc ─ + + I am confused by this type name: + + 4│ f : Foo.foo + ^^^^^^^ + + Type names start with an uppercase letter, and can optionally be + qualified by a module name, like Bool or Http.Request.Request. + "### + ); + + test_report!( + def_missing_final_expression, + indoc!( + r#" + f : Foo.foo + "# + ), + @r###" + ── MISSING FINAL EXPRESSION ──────── tmp/def_missing_final_expression/Test.roc ─ + + I am partway through parsing a definition, but I got stuck here: + + 1│ app "test" provides [main] to "./platform" + 2│ + 3│ main = + 4│ f : Foo.foo + ^ + + This definition is missing a final expression. A nested definition + must be followed by either another definition, or an expression + + x = 4 + y = 2 + + x + y + "### + ); + + test_report!( + expression_indentation_end, + indoc!( + r#" + f <- Foo.foo + "# + ), + @r###" + ── INDENT ENDS AFTER EXPRESSION ────── tmp/expression_indentation_end/Test.roc ─ + + I am partway through parsing an expression, but I got stuck here: + + 1│ app "test" provides [main] to "./platform" + 2│ + 3│ main = + 4│ f <- Foo.foo + ^ + + Looks like the indentation ends prematurely here. Did you mean to have + another expression after this line? + "### + ); + + test_report!( + type_inline_alias, + indoc!( + r#" + f : I64 as + f = 0 + + f + "# + ), + @r###" + ── UNFINISHED INLINE ALIAS ──────────────────── tmp/type_inline_alias/Test.roc ─ + + I just started parsing an inline type alias, but I got stuck here: + + 4│ f : I64 as + ^ + + Note: I may be confused by indentation + "### + ); + + test_report!( + type_double_comma, + indoc!( + r#" + f : I64,,I64 -> I64 + f = 0 + + f + "# + ), + @r###" + ── DOUBLE COMMA ─────────────────────────────── tmp/type_double_comma/Test.roc ─ + + I just started parsing a function argument type, but I encountered two + commas in a row: + + 4│ f : I64,,I64 -> I64 + ^ + + Try removing one of them. + "### + ); + + test_report!( + type_argument_no_arrow, + indoc!( + r#" + f : I64, I64 + f = 0 + + f + "# + ), + @r###" + ── UNFINISHED TYPE ─────────────────────── tmp/type_argument_no_arrow/Test.roc ─ + + I am partway through parsing a type, but I got stuck here: + + 4│ f : I64, I64 + ^ + + Note: I may be confused by indentation + "### + ); + + // TODO could do better by pointing out we're parsing a function type + test_report!( + type_argument_arrow_then_nothing, + indoc!( + r#" + f : I64, I64 -> + f = 0 + + f + "# + ), + @r###" + ── UNFINISHED TYPE ───────────── tmp/type_argument_arrow_then_nothing/Test.roc ─ + + I just started parsing a type, but I got stuck here: + + 4│ f : I64, I64 -> + ^ + + Note: I may be confused by indentation + "### + ); + + // TODO could do better by pointing out we're parsing a function type + test_report!( + dict_type_formatting, + indoc!( + r#" + app "dict" imports [ Dict ] provides [main] to "./platform" + + myDict : Dict.Dict Num.I64 Str + myDict = Dict.insert (Dict.empty {}) "foo" 42 + + main = myDict + "# + ), + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + Something is off with the body of the `myDict` definition: + + 3│ myDict : Dict.Dict Num.I64 Str + 4│ myDict = Dict.insert (Dict.empty {}) "foo" 42 + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + This `insert` call produces: + + Dict Str (Num *) + + But the type annotation on `myDict` says it should be: + + Dict I64 Str + "### + ); + + test_report!( + alias_type_diff, + indoc!( + r#" + app "test" imports [Set.{ Set }] provides [main] to "./platform" + + HSet a : Set a + + foo : Str -> HSet {} + + myDict : HSet Str + myDict = foo "bar" + + main = myDict + "# + ), + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + Something is off with the body of the `myDict` definition: + + 7│ myDict : HSet Str + 8│ myDict = foo "bar" + ^^^^^^^^^ + + This `foo` call produces: + + HSet {} + + But the type annotation on `myDict` says it should be: + + HSet Str + "### + ); + + // this should get better with time + test_report!( + if_guard_without_condition, + indoc!( + r#" + when Just 4 is + Just if -> + 4 + + _ -> + 2 + "# + ), + @r###" + ── IF GUARD NO CONDITION ───────────── tmp/if_guard_without_condition/Test.roc ─ + + I just started parsing an if guard, but there is no guard condition: + + 4│ when Just 4 is + 5│ Just if -> + ^ + + Try adding an expression before the arrow! + "### + ); + + test_report!( + empty_or_pattern, + indoc!( + r#" + when Just 4 is + Just 4 | -> + 4 + + _ -> + 2 + "# + ), + @r###" + ── UNFINISHED PATTERN ────────────────────────── tmp/empty_or_pattern/Test.roc ─ + + I just started parsing a pattern, but I got stuck here: + + 5│ Just 4 | -> + ^ + + Note: I may be confused by indentation + "### + ); + + // TODO check if "what_is_next" is a keyword + test_report!( + pattern_binds_keyword, + indoc!( + r#" + when Just 4 is + Just when -> + 4 + + _ -> + 2 + "# + ), + @r###" + ── MISSING ARROW ────────────────────────── tmp/pattern_binds_keyword/Test.roc ─ + + I am partway through parsing a `when` expression, but got stuck here: + + 4│ when Just 4 is + 5│ Just when -> + ^ + + I was expecting to see an arrow next. + + Note: Sometimes I get confused by indentation, so try to make your `when` + look something like this: + + when List.first plants is + Ok n -> + n + + Err _ -> + 200 + + Notice the indentation. All patterns are aligned, and each branch is + indented a bit more than the corresponding pattern. That is important! + "### + ); + + // this should get better with time + test_report!( + when_missing_arrow, + indoc!( + r#" + when 5 is + 1 -> 2 + _ + "# + ), + @r###" + ── UNFINISHED WHEN ─────────────────────────── tmp/when_missing_arrow/Test.roc ─ + + I was partway through parsing a `when` expression, but I got stuck here: + + 4│ when 5 is + 5│ 1 -> 2 + 6│ _ + ^ + + I was expecting to see a pattern next + + Note: Here is an example of a valid `when` expression for reference. + + when List.first plants is + Ok n -> + n + + Err _ -> + 200 + + Notice the indentation. All patterns are aligned, and each branch is + indented a bit more than the corresponding pattern. That is important! + "### + ); + + test_report!( + lambda_double_comma, + indoc!( + r#" + \a,,b -> 1 + "# + ), + @r###" + ── UNFINISHED ARGUMENT LIST ───────────────── tmp/lambda_double_comma/Test.roc ─ + + I am partway through parsing a function argument list, but I got stuck + at this comma: + + 4│ \a,,b -> 1 + ^ + + I was expecting an argument pattern before this, so try adding an + argument before the comma and see if that helps? + "### + ); + + test_report!( + lambda_leading_comma, + indoc!( + r#" + \,b -> 1 + "# + ), + @r###" + ── UNFINISHED ARGUMENT LIST ──────────────── tmp/lambda_leading_comma/Test.roc ─ + + I am partway through parsing a function argument list, but I got stuck + at this comma: + + 4│ \,b -> 1 + ^ + + I was expecting an argument pattern before this, so try adding an + argument before the comma and see if that helps? + "### + ); + + // this should get better with time + // TODO this formerly gave + // + // ── UNFINISHED WHEN ───────────────────────────────────────────────────────────── + // + // I was partway through parsing a `when` expression, but I got stuck here: + // + // 3│ _ -> 2 + // ^ + // + // I suspect this is a pattern that is not indented enough? (by 2 spaces) + // + // but that requires parsing the next pattern blindly, irrespective of indentation. Can + // we find an efficient solution that doesn't require parsing an extra pattern for + // every `when`, i.e. we want a good error message for the test case above, but for + // a valid `when`, we don't want to do extra work, e.g. here + // + // x + // when x is + // n -> n + // + // 4 + // + // We don't want to parse the `4` and say it's an outdented pattern! + test_report!( + when_outdented_branch, + indoc!( + r#" + when 4 is + 5 -> 2 + 2 -> 2 + "# + ), + @r###" + ── NOT END OF FILE ──────────────────────── tmp/when_outdented_branch/Test.roc ─ + + I expected to reach the end of the file, but got stuck here: + + 6│ 2 -> 2 + ^ + "### + ); + + test_report!( + when_over_indented_underscore, + indoc!( + r#" + when 4 is + 5 -> 2 + _ -> 2 + "# + ), + @r###" + ── UNEXPECTED ARROW ─────────────── tmp/when_over_indented_underscore/Test.roc ─ + + I am parsing a `when` expression right now, but this arrow is confusing + me: + + 5│ 5 -> 2 + 6│ _ -> 2 + ^^ + + It makes sense to see arrows around here, so I suspect it is something + earlier. Maybe this pattern is indented a bit farther from the + previous patterns? + + Note: Here is an example of a valid `when` expression for reference. + + when List.first plants is + Ok n -> + n + + Err _ -> + 200 + + Notice the indentation. All patterns are aligned, and each branch is + indented a bit more than the corresponding pattern. That is important! + "### + ); + + test_report!( + when_over_indented_int, + indoc!( + r#" + when 4 is + 5 -> Num.neg + 2 -> 2 + "# + ), + @r###" + ── UNEXPECTED ARROW ────────────────────── tmp/when_over_indented_int/Test.roc ─ + + I am parsing a `when` expression right now, but this arrow is confusing + me: + + 5│ 5 -> Num.neg + 6│ 2 -> 2 + ^^ + + It makes sense to see arrows around here, so I suspect it is something + earlier. Maybe this pattern is indented a bit farther from the + previous patterns? + + Note: Here is an example of a valid `when` expression for reference. + + when List.first plants is + Ok n -> + n + + Err _ -> + 200 + + Notice the indentation. All patterns are aligned, and each branch is + indented a bit more than the corresponding pattern. That is important! + "### + ); + + // TODO I think we can do better here + test_report!( + if_outdented_then, + indoc!( + r#" + x = + if 5 == 5 + then 2 else 3 + + x + "# + ), + @r###" + ── UNFINISHED IF ────────────────────────────── tmp/if_outdented_then/Test.roc ─ + + I was partway through parsing an `if` expression, but I got stuck here: + + 5│ if 5 == 5 + ^ + + I was expecting to see the `then` keyword next. + "### + ); + + // this should get better with time + test_report!( + if_missing_else, + indoc!( + r#" + if 5 == 5 then 2 + "# + ), + @r###" + ── UNFINISHED IF ──────────────────────────────── tmp/if_missing_else/Test.roc ─ + + I was partway through parsing an `if` expression, but I got stuck here: + + 4│ if 5 == 5 then 2 + ^ + + I was expecting to see the `else` keyword next. + "### + ); + + test_report!( + list_double_comma, + indoc!( + r#" + [1, 2, , 3] + "# + ), + @r###" + ── UNFINISHED LIST ──────────────────────────── tmp/list_double_comma/Test.roc ─ + + I am partway through started parsing a list, but I got stuck here: + + 4│ [1, 2, , 3] + ^ + + I was expecting to see a list entry before this comma, so try adding a + list entry and see if that helps? + "### + ); + + test_report!( + list_without_end, + indoc!( + r#" + [1, 2, + "# + ), + @r###" + ── UNFINISHED LIST ───────────────────────────── tmp/list_without_end/Test.roc ─ + + I am partway through started parsing a list, but I got stuck here: + + 4│ [1, 2, + 5│ + 6│ + ^ + + I was expecting to see a closing square bracket before this, so try + adding a ] and see if that helps? + + Note: When I get stuck like this, it usually means that there is a + missing parenthesis or bracket somewhere earlier. It could also be a + stray keyword or operator. + "### + ); + + test_report!( + number_double_dot, + indoc!( + r#" + 1.1.1 + "# + ), + @r###" + ── SYNTAX PROBLEM ──────────────────────────────────────── /code/proj/Main.roc ─ + + This float literal contains an invalid digit: + + 4│ 1.1.1 + ^^^^^ + + Floating point literals can only contain the digits 0-9, or use + scientific notation 10e4, or have a float suffix. + + Tip: Learn more about number literals at TODO + "### + ); + + test_report!( + unicode_not_hex, + r#""abc\u(zzzz)def""#, + @r###" + ── WEIRD CODE POINT ───────────────────────────── tmp/unicode_not_hex/Test.roc ─ + + I am partway through parsing a unicode code point, but I got stuck + here: + + 4│ "abc\u(zzzz)def" + ^ + + I was expecting a hexadecimal number, like \u(1100) or \u(00FF). + + Learn more about working with unicode in roc at TODO + "### + ); + + test_report!( + unicode_too_large, + r#""abc\u(110000)def""#, + @r###" + ── INVALID UNICODE ─────────────────────────────────────── /code/proj/Main.roc ─ + + This unicode code point is invalid: + + 4│ "abc\u(110000)def" + ^^^^^^ + + Learn more about working with unicode in roc at TODO + "### + ); + + test_report!( + weird_escape, + r#""abc\qdef""#, + @r###" + ── WEIRD ESCAPE ──────────────────────────────────── tmp/weird_escape/Test.roc ─ + + I was partway through parsing a string literal, but I got stuck here: + + 4│ "abc\qdef" + ^^ + + This is not an escape sequence I recognize. After a backslash, I am + looking for one of these: + + - A newline: \n + - A caret return: \r + - A tab: \t + - An escaped quote: \" + - An escaped backslash: \\ + - A unicode code point: \u(00FF) + - An interpolated string: \(myVariable) + "### + ); + + test_report!( + single_quote_too_long, + r#"'abcdef'"#, + @r###" + ── INVALID SCALAR ───────────────────────── tmp/single_quote_too_long/Test.roc ─ + + I am part way through parsing this scalar literal (character literal), + but it's too long to fit in a U32 so it's not a valid scalar. + + 4│ 'abcdef' + ^ + + You could change it to something like 'a' or '\n'. Note, roc strings + use double quotes, like "hello". + "### + ); + + test_report!( + single_no_end, + r#""there is no end"#, + @r###" + ── ENDLESS STRING ───────────────────────────────── tmp/single_no_end/Test.roc ─ + + I cannot find the end of this string: + + 4│ "there is no end + ^ + + You could change it to something like "to be or not to be" or even + just "". + "### + ); + + test_report!( + multi_no_end, + r#""""there is no end"#, + @r###" + ── ENDLESS STRING ────────────────────────────────── tmp/multi_no_end/Test.roc ─ + + I cannot find the end of this block string: + + 4│ """there is no end + ^ + + You could change it to something like """to be or not to be""" or even + just """""". + "### + ); + + test_report!( + multi_insufficient_indent, + " \"\"\"\n testing\n \"\"\"", // 4 space indent on the start, 2 space on the `testing` line + @r###" + ── INSUFFICIENT INDENT IN MULTI-LINE STRING ─ ..._insufficient_indent/Test.roc ─ + + This multiline string is not sufficiently indented: + + 4│ """ + 5│ testing + ^ + + Lines in a multi-line string must be indented at least as much as the + beginning """. This extra indentation is automatically removed from + the string during compilation. + "### + ); + + test_report!( + dbg_without_final_expression, + indoc!( + r#" + dbg 42 + "# + ), + @r###" + ── INDENT ENDS AFTER EXPRESSION ──── tmp/dbg_without_final_expression/Test.roc ─ + + I am partway through parsing a dbg statement, but I got stuck here: + + 4│ dbg 42 + ^ + + I was expecting a final expression, like so + + dbg 42 + "done" + "### + ); + + test_report!( + expect_without_final_expression, + indoc!( + r#" + expect 1 + 1 == 2 + "# + ), + @r###" + ── INDENT ENDS AFTER EXPRESSION ─ tmp/expect_without_final_expression/Test.roc ─ + + I am partway through parsing an expect statement, but I got stuck + here: + + 4│ expect 1 + 1 == 2 + ^ + + I was expecting a final expression, like so + + expect 1 + 1 == 2 + "done" + "### + ); + + // https://github.com/roc-lang/roc/issues/1714 + test_report!( + interpolate_concat_is_transparent_1714, + indoc!( + r#" + greeting = "Privet" + + if Bool.true then 1 else "\(greeting), World!" + "#, + ), + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + This `if` has an `else` branch with a different type from its `then` branch: + + 6│ if Bool.true then 1 else "\(greeting), World!" + ^^^^^^^^^^^^^^^^^^^^^ + + The `else` branch is a string of type: + + Str + + but the `then` branch has the type: + + Num * + + All branches in an `if` must have the same type! + "### + ); + + macro_rules! comparison_binop_transparency_tests { + ($($op:expr, $name:ident),* $(,)?) => { + $( + test_report!( + $name, + &format!(r#"if Bool.true then "abc" else 1 {} 2"#, $op), + |golden| assert_eq!(golden, format!( +r#"── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + +This `if` has an `else` branch with a different type from its `then` branch: + +4│ if Bool.true then "abc" else 1 {} 2 + ^^{}^^ + +This comparison produces: + + Bool + +but the `then` branch has the type: + + Str + +All branches in an `if` must have the same type! +"#, + $op, "^".repeat($op.len()) + )) + ); + )* + } + } + + comparison_binop_transparency_tests! { + "<", lt_binop_is_transparent, + ">", gt_binop_is_transparent, + "==", eq_binop_is_transparent, + "!=", neq_binop_is_transparent, + "<=", leq_binop_is_transparent, + ">=", geq_binop_is_transparent, + } + + test_report!( + keyword_record_field_access, + indoc!( + r#" + foo = {} + + foo.if + "# + ), + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + This `foo` record doesn’t have a `if` field: + + 6│ foo.if + ^^^^^^ + + In fact, `foo` is a record with no fields at all! + "### + ); + + test_report!( + keyword_qualified_import, + indoc!( + r#" + Num.if + "# + ), + @r###" + ── NOT EXPOSED ─────────────────────────────────────────── /code/proj/Main.roc ─ + + The Num module does not expose `if`: + + 4│ Num.if + ^^^^^^ + + Did you mean one of these? + + Num.sin + Num.div + Num.min + Num.e + "### + ); + + test_report!( + stray_dot_expr, + indoc!( + r#" + Num.add . 23 + "# + ), + @r###" + ── SYNTAX PROBLEM ──────────────────────────────────────── /code/proj/Main.roc ─ + + I am trying to parse a record field access here: + + 4│ Num.add . 23 + ^ + + So I expect to see a lowercase letter next, like .name or .height. + "### + ); + + test_report!( + opaque_ref_field_access, + indoc!( + r#" + @UUID.bar + "# + ), + @r###" + ── SYNTAX PROBLEM ──────────────────────────────────────── /code/proj/Main.roc ─ + + I am very confused by this field access: + + 4│ @UUID.bar + ^^^^ + + It looks like a record field access on an opaque reference. + "### + ); + + test_report!( + weird_accessor, + indoc!( + r#" + .foo.bar + "# + ), + @r###" + ── SYNTAX PROBLEM ──────────────────────────────────────── /code/proj/Main.roc ─ + + I am very confused by this field access + + 4│ .foo.bar + ^^^^^^^^ + + It looks like a field access on an accessor. I parse.client.name as + (.client).name. Maybe use an anonymous function like + (\r -> r.client.name) instead? + "### + ); + + test_report!( + closure_underscore_ident, + indoc!( + r#" + \the_answer -> 100 + "# + ), + @r###" + ── NAMING PROBLEM ──────────────────────────────────────── /code/proj/Main.roc ─ + + I am trying to parse an identifier here: + + 4│ \the_answer -> 100 + ^ + + Underscores are not allowed in identifiers. Use camelCase instead! + "### + ); + + test_report!( + #[ignore] + double_binop, + indoc!( + r#" + key >= 97 && <= 122 + "# + ), + @r#" + "# + ); + + test_report!( + #[ignore] + case_of, + indoc!( + r#" + case 1 of + 1 -> True + _ -> False + "# + ), + @r###" + ── UNKNOWN OPERATOR ───────────────────────────────────── tmp/case_of/Test.roc ─ + + This looks like an operator, but it's not one I recognize! + + 1│ app "test" provides [main] to "./platform" + 2│ + 3│ main = + 4│ case 1 of + 5│ 1 -> True + ^^ + + The arrow -> is used to define cases in a `when` expression: + + when color is + Red -> "stop!" + Green -> "go!" + + And to define a function: + + increment : I64 -> I64 + increment = \n -> n + 1 + + "### + ); + + test_report!( + argument_without_space, + indoc!( + r#" + ["foo", bar("")] + "# + ), + @r###" + ── UNRECOGNIZED NAME ───────────────────────────────────── /code/proj/Main.roc ─ + + Nothing is named `bar` in this scope. + + 4│ ["foo", bar("")] + ^^^ + + Did you mean one of these? + + Nat + Str + Err + U8 + "### + ); + + test_report!( + invalid_operator, + indoc!( + r#" + main = + 5 ** 3 + "# + ), + @r###" + ── UNKNOWN OPERATOR ──────────────────────────── tmp/invalid_operator/Test.roc ─ + + This looks like an operator, but it's not one I recognize! + + 1│ app "test" provides [main] to "./platform" + 2│ + 3│ main = + 4│ main = + 5│ 5 ** 3 + ^^ + + I have no specific suggestion for this operator, see TODO for the full + list of operators in Roc. + "### + ); + + test_report!( + double_plus, + indoc!( + r#" + main = + [] ++ [] + "# + ), + @r###" + ── UNKNOWN OPERATOR ───────────────────────────────── tmp/double_plus/Test.roc ─ + + This looks like an operator, but it's not one I recognize! + + 1│ app "test" provides [main] to "./platform" + 2│ + 3│ main = + 4│ main = + 5│ [] ++ [] + ^^ + + To concatenate two lists or strings, try using List.concat or + Str.concat instead. + "### + ); + + test_report!( + inline_hastype, + indoc!( + r#" + main = + (\x -> x) : I64 + + 3 + "# + ), + @r###" + ── UNKNOWN OPERATOR ────────────────────────────── tmp/inline_hastype/Test.roc ─ + + This looks like an operator, but it's not one I recognize! + + 1│ app "test" provides [main] to "./platform" + 2│ + 3│ main = + 4│ main = + 5│ (\x -> x) : I64 + ^ + + The has-type operator : can only occur in a definition's type + signature, like + + increment : I64 -> I64 + increment = \x -> x + 1 + "### + ); + + // this is still bad, but changing the order and progress of other parsers should improve it + // down the line + test_report!( + wild_case_arrow, + indoc!( + r#" + main = 5 -> 3 + "# + ), + |golden| pretty_assertions::assert_eq!( + golden, + &format!( + r###"── UNKNOWN OPERATOR ───────────────────────────── tmp/wild_case_arrow/Test.roc ─ + +This looks like an operator, but it's not one I recognize! + +1│ app "test" provides [main] to "./platform" +2│ +3│ main = +4│ main = 5 -> 3 + ^^ + +Looks like you are trying to define a function.{} + +In roc, functions are always written as a lambda, like{} + + increment = \n -> n + 1"###, + ' ', ' ' + ) + ) + ); + + #[test] + fn provides_to_identifier() { + report_header_problem_as( + indoc!( + r#" + app "test-base64" + packages { pf: "platform/main.roc" } + imports [pf.Task, Base64] + provides [main, @Foo] to pf + "# + ), + indoc!( + r#" + ── WEIRD PROVIDES ──────────────────────────────────────── /code/proj/Main.roc ─ + + I am partway through parsing a provides list, but I got stuck here: + + 3│ imports [pf.Task, Base64] + 4│ provides [main, @Foo] to pf + ^ + + I was expecting a type name, value name or function name next, like + + provides [Animal, default, tame] + "# + ), + ) + } + + #[test] + fn missing_provides_in_app_header() { + report_header_problem_as( + indoc!( + r#" + app "broken" + packages { + pf: "https://github.com/roc-lang/basic-cli/releases/download/0.5.0/Cufzl36_SnJ4QbOoEmiJ5dIpUxBvdB3NEySvuH82Wio.tar.br", + } + imports [ + pf.Stdout, + ] + + main = + Stdout.line "answer" + "# + ), + indoc!( + r#" + ── WEIRD PROVIDES ──────────────────────────────────────── /code/proj/Main.roc ─ + + I am partway through parsing a header, but I got stuck here: + + 7│ ] + ^ + + I am expecting the `provides` keyword next, like + + provides [Animal, default, tame] + "# + ), + ) + } + + #[test] + fn provides_missing_to_in_app_header() { + report_header_problem_as( + indoc!( + r#" + app "broken" + provides [main] + "# + ), + indoc!( + r#" + ── WEIRD PROVIDES ──────────────────────────────────────── /code/proj/Main.roc ─ + + I am partway through parsing a header, but I got stuck here: + + 1│ app "broken" + 2│ provides [main] + ^ + + I am expecting the `to` keyword next, like: + + to pf + "# + ), + ) + } + + #[test] + fn provides_to_missing_platform_in_app_header() { + report_header_problem_as( + indoc!( + r#" + app "broken" + provides [main] to + "# + ), + indoc!( + r#" + ── WEIRD PROVIDES ──────────────────────────────────────── /code/proj/Main.roc ─ + + I am partway through parsing a header, but I got stuck here: + + 1│ app "broken" + 2│ provides [main] to + ^ + + I am expecting the platform name next, like: + + to pf + "# + ), + ) + } + + #[test] + fn platform_requires_rigids() { + report_header_problem_as( + indoc!( + r#" + platform "folkertdev/foo" + requires { main : Effect {} } + exposes [] + packages {} + imports [Task] + provides [mainForHost] + effects fx.Effect + { + putChar : I64 -> Effect {}, + putLine : Str -> Effect {}, + getLine : Effect Str + } + "# + ), + indoc!( + r#" + ── BAD REQUIRES ────────────────────────────────────────── /code/proj/Main.roc ─ + + I am partway through parsing a header, but I got stuck here: + + 1│ platform "folkertdev/foo" + 2│ requires { main : Effect {} } + ^ + + I am expecting a list of type names like `{}` or `{ Model }` next. A full + `requires` definition looks like + + requires { Model, Msg } {main : Effect {}} + "# + ), + ) + } + + #[test] + fn missing_imports() { + report_header_problem_as( + indoc!( + r#" + interface Foobar + exposes [main, Foo] + "# + ), + indoc!( + r#" + ── WEIRD IMPORTS ───────────────────────────────────────── /code/proj/Main.roc ─ + + I am partway through parsing a header, but I got stuck here: + + 2│ exposes [main, Foo] + ^ + + I am expecting the `imports` keyword next, like + + imports [Animal, default, tame] + "# + ), + ) + } + + #[test] + fn exposes_identifier() { + report_header_problem_as( + indoc!( + r#" + interface Foobar + exposes [main, @Foo] + imports [pf.Task, Base64] + "# + ), + indoc!( + r#" + ── WEIRD EXPOSES ───────────────────────────────────────── /code/proj/Main.roc ─ + + I am partway through parsing an `exposes` list, but I got stuck here: + + 1│ interface Foobar + 2│ exposes [main, @Foo] + ^ + + I was expecting a type name, value name or function name next, like + + exposes [Animal, default, tame] + "# + ), + ) + } + + #[test] + fn invalid_module_name() { + report_header_problem_as( + indoc!( + r#" + interface foobar + exposes [main, @Foo] + imports [pf.Task, Base64] + "# + ), + indoc!( + r#" + ── WEIRD MODULE NAME ───────────────────────────────────── /code/proj/Main.roc ─ + + I am partway through parsing a header, but got stuck here: + + 1│ interface foobar + ^ + + I am expecting a module name next, like BigNum or Main. Module names + must start with an uppercase letter. + "# + ), + ) + } + + #[test] + fn invalid_app_name() { + report_header_problem_as( + indoc!( + r#" + app foobar + exposes [main, @Foo] + imports [pf.Task, Base64] + "# + ), + indoc!( + r#" + ── WEIRD APP NAME ──────────────────────────────────────── /code/proj/Main.roc ─ + + I am partway through parsing a header, but got stuck here: + + 1│ app foobar + ^ + + I am expecting an application name next, like app "main" or + app "editor". App names are surrounded by quotation marks. + "# + ), + ) + } + + test_report!( + apply_unary_negative, + indoc!( + r#" + foo = 3 + + -foo 1 2 + "# + ), + @r###" + ── TOO MANY ARGS ───────────────────────────────────────── /code/proj/Main.roc ─ + + This value is not a function, but it was given 2 arguments: + + 6│ -foo 1 2 + ^^^^ + + Are there any missing commas? Or missing parentheses? + "### + ); + + test_report!( + apply_unary_not, + indoc!( + r#" + foo = Bool.true + + !foo 1 2 + "# + ), + @r###" + ── TOO MANY ARGS ───────────────────────────────────────── /code/proj/Main.roc ─ + + This value is not a function, but it was given 2 arguments: + + 6│ !foo 1 2 + ^^^^ + + Are there any missing commas? Or missing parentheses? + "### + ); + + test_report!( + applied_tag_function, + indoc!( + r#" + x : List [Foo Str] + x = List.map [1, 2] Foo + + x + "# + ), + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + Something is off with the body of the `x` definition: + + 4│ x : List [Foo Str] + 5│ x = List.map [1, 2] Foo + ^^^^^^^^^^^^^^^^^^^ + + This `map` call produces: + + List [Foo (Num *)] + + But the type annotation on `x` says it should be: + + List [Foo Str] + "### + ); + + test_report!( + pattern_in_parens_open, + indoc!( + r#" + \( a + "# + ), + @r###" + ── UNFINISHED PARENTHESES ──────────────── tmp/pattern_in_parens_open/Test.roc ─ + + I am partway through parsing a pattern in parentheses, but I got stuck + here: + + 4│ \( a + 5│ + 6│ + ^ + + I was expecting to see a closing parenthesis before this, so try + adding a ) and see if that helps? + "### + ); + + test_report!( + pattern_in_parens_end_comma, + indoc!( + r#" + \( a, + "# + ), + @r###" + ── UNFINISHED PARENTHESES ─────────── tmp/pattern_in_parens_end_comma/Test.roc ─ + + I am partway through parsing a pattern in parentheses, but I got stuck + here: + + 4│ \( a, + 5│ + 6│ + ^ + + I was expecting to see a closing parenthesis before this, so try + adding a ) and see if that helps? + "### + ); + + test_report!( + pattern_in_parens_end, + indoc!( + r#" + \( a + "# + ), + @r###" + ── UNFINISHED PARENTHESES ───────────────── tmp/pattern_in_parens_end/Test.roc ─ + + I am partway through parsing a pattern in parentheses, but I got stuck + here: + + 4│ \( a + 5│ + 6│ + ^ + + I was expecting to see a closing parenthesis before this, so try + adding a ) and see if that helps? + "### + ); + + test_report!( + unfinished_closure_pattern_in_parens, + indoc!( + r#" + x = \( a + ) + "# + ), + @r###" + ── UNFINISHED FUNCTION ───── tmp/unfinished_closure_pattern_in_parens/Test.roc ─ + + I was partway through parsing a function, but I got stuck here: + + 4│ x = \( a + 5│ ) + ^ + + I just saw a pattern, so I was expecting to see a -> next. + "### + ); + + test_report!( + pattern_in_parens_indent_open, + indoc!( + r#" + \( + "# + ), + @r###" + ── UNFINISHED PARENTHESES ───────── tmp/pattern_in_parens_indent_open/Test.roc ─ + + I am partway through parsing a pattern in parentheses, but I got stuck + here: + + 4│ \( + 5│ + 6│ + ^ + + I was expecting to see a closing parenthesis before this, so try + adding a ) and see if that helps? + "### + ); + + test_report!( + backpassing_type_error, + indoc!( + r#" + x <- List.map ["a", "b"] + + x + 1 + "# + ), + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + This 2nd argument to `map` has an unexpected type: + + 4│> x <- List.map ["a", "b"] + 5│> + 6│> x + 1 + + The argument is an anonymous function of type: + + Num * -> Num * + + But `map` needs its 2nd argument to be: + + Str -> Num * + "### + ); + + test_report!( + expect_expr_type_error, + indoc!( + r#" + expect "foobar" + + 4 + "# + ), + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + This `expect` condition needs to be a Bool: + + 4│ expect "foobar" + ^^^^^^^^ + + Right now it’s a string of type: + + Str + + But I need every `expect` condition to evaluate to a Bool—either + `Bool.true` or `Bool.false`. + "### + ); + + test_report!( + num_too_general_wildcard, + indoc!( + r#" + mult : Num.Num *, Num.F64 -> Num.F64 + mult = \a, b -> a * b + + mult 0 0 + "# + ), + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + This 2nd argument to * has an unexpected type: + + 5│ mult = \a, b -> a * b + ^ + + This `b` value is a: + + F64 + + But * needs its 2nd argument to be: + + Num * + + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + Something is off with the body of the `mult` definition: + + 4│ mult : Num.Num *, Num.F64 -> Num.F64 + 5│ mult = \a, b -> a * b + ^^^^^ + + This `mul` call produces: + + Num * + + But the type annotation on `mult` says it should be: + + F64 + "### + ); + + test_report!( + num_too_general_named, + indoc!( + r#" + mult : Num.Num a, Num.F64 -> Num.F64 + mult = \a, b -> a * b + + mult 0 0 + "# + ), + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + This 2nd argument to * has an unexpected type: + + 5│ mult = \a, b -> a * b + ^ + + This `b` value is a: + + F64 + + But * needs its 2nd argument to be: + + Num a + + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + Something is off with the body of the `mult` definition: + + 4│ mult : Num.Num a, Num.F64 -> Num.F64 + 5│ mult = \a, b -> a * b + ^^^^^ + + This `mul` call produces: + + Num a + + But the type annotation on `mult` says it should be: + + F64 + "### + ); + + test_report!( + inference_var_not_enough_in_alias, + indoc!( + r#" + Result a b : [Ok a, Err b] + + canIGo : _ -> Result _ + canIGo = \color -> + when color is + "green" -> Ok "go!" + "yellow" -> Err (SlowIt "whoa, let's slow down!") + "red" -> Err (StopIt "absolutely not") + _ -> Err (UnknownColor "this is a weird stoplight") + canIGo + "# + ), + @r###" + ── DUPLICATE NAME ──────────────────────────────────────── /code/proj/Main.roc ─ + + This alias has the same name as a builtin: + + 4│ Result a b : [Ok a, Err b] + ^^^^^^^^^^^^^^^^^^^^^^^^^^ + + All builtin aliases are in scope by default, so I need this alias to + have a different name! + + ── TOO FEW TYPE ARGUMENTS ──────────────────────────────── /code/proj/Main.roc ─ + + The `Result` alias expects 2 type arguments, but it got 1 instead: + + 6│ canIGo : _ -> Result _ + ^^^^^^^^ + + Are there missing parentheses? + "### + ); + + test_report!( + inference_var_too_many_in_alias, + indoc!( + r#" + Result a b : [Ok a, Err b] + + canIGo : _ -> Result _ _ _ + canIGo = \color -> + when color is + "green" -> Ok "go!" + "yellow" -> Err (SlowIt "whoa, let's slow down!") + "red" -> Err (StopIt "absolutely not") + _ -> Err (UnknownColor "this is a weird stoplight") + canIGo + "# + ), + @r###" + ── DUPLICATE NAME ──────────────────────────────────────── /code/proj/Main.roc ─ + + This alias has the same name as a builtin: + + 4│ Result a b : [Ok a, Err b] + ^^^^^^^^^^^^^^^^^^^^^^^^^^ + + All builtin aliases are in scope by default, so I need this alias to + have a different name! + + ── TOO MANY TYPE ARGUMENTS ─────────────────────────────── /code/proj/Main.roc ─ + + The `Result` alias expects 2 type arguments, but it got 3 instead: + + 6│ canIGo : _ -> Result _ _ _ + ^^^^^^^^^^^^ + + Are there missing parentheses? + "### + ); + + test_report!( + inference_var_conflict_in_rigid_links, + indoc!( + r#" + f : a -> (_ -> b) where a implements Eq + f = \x -> \y -> if x == y then x else y + f + "# + ), + // TODO: We should tell the user that we inferred `_` as `a` + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + Something is off with the body of the `f` definition: + + 4│ f : a -> (_ -> b) where a implements Eq + 5│ f = \x -> \y -> if x == y then x else y + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + The body is an anonymous function of type: + + a -> a where a implements Eq, a implements Eq + + But the type annotation on `f` says it should be: + + a -> b where a implements Eq + + Tip: Your type annotation uses `b` and `a` as separate type variables. + Your code seems to be saying they are the same though. Maybe they + should be the same in your type annotation? Maybe your code uses them + in a weird way? + "### + ); + + test_report!( + error_wildcards_are_related, + indoc!( + r#" + f : * -> * + f = \x -> x + + f + "# + ), + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + Something is off with the body of the `f` definition: + + 4│ f : * -> * + 5│ f = \x -> x + ^ + + The type annotation on `f` says this `x` value should have the type: + + * + + However, the type of this `x` value is connected to another type in a + way that isn't reflected in this annotation. + + Tip: Any connection between types must use a named type variable, not + a `*`! Maybe the annotation on `f` should have a named type variable in + place of the `*`? + "### + ); + + test_report!( + error_nested_wildcards_are_related, + indoc!( + r#" + f : a, b, * -> {x: a, y: b, z: *} + f = \x, y, z -> {x, y, z} + + f + "# + ), + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + Something is off with the body of the `f` definition: + + 4│ f : a, b, * -> {x: a, y: b, z: *} + 5│ f = \x, y, z -> {x, y, z} + ^^^^^^^^^ + + The type annotation on `f` says the body is a record should have the + type: + + { + x : a, + y : b, + z : *, + } + + However, the type of the body is a record is connected to another type + in a way that isn't reflected in this annotation. + + Tip: Any connection between types must use a named type variable, not + a `*`! Maybe the annotation on `f` should have a named type variable in + place of the `*`? + "### + ); + + test_report!( + error_wildcards_are_related_in_nested_defs, + indoc!( + r#" + f : a, b, * -> * + f = \_, _, x2 -> + inner : * -> * + inner = \y -> y + inner x2 + + f + "# + ), + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + Something is off with the body of the `inner` definition: + + 6│ inner : * -> * + 7│ inner = \y -> y + ^ + + The type annotation on `inner` says this `y` value should have the type: + + * + + However, the type of this `y` value is connected to another type in a + way that isn't reflected in this annotation. + + Tip: Any connection between types must use a named type variable, not + a `*`! Maybe the annotation on `inner` should have a named type variable + in place of the `*`? + "### + ); + + test_report!( + error_inline_alias_not_an_alias, + indoc!( + r#" + f : List elem -> [Nil, Cons elem a] as a + "# + ), + @r###" + ── NOT AN INLINE ALIAS ────────── tmp/error_inline_alias_not_an_alias/Test.roc ─ + + The inline type after this `as` is not a type alias: + + 4│ f : List elem -> [Nil, Cons elem a] as a + ^ + + Inline alias types must start with an uppercase identifier and be + followed by zero or more type arguments, like Point or List a. + "### + ); + + test_report!( + error_inline_alias_qualified, + indoc!( + r#" + f : List elem -> [Nil, Cons elem a] as Module.LinkedList a + "# + ), + @r###" + ── QUALIFIED ALIAS NAME ──────────── tmp/error_inline_alias_qualified/Test.roc ─ + + This type alias has a qualified name: + + 4│ f : List elem -> [Nil, Cons elem a] as Module.LinkedList a + ^ + + An alias introduces a new name to the current scope, so it must be + unqualified. + "### + ); + + test_report!( + error_inline_alias_argument_uppercase, + indoc!( + r#" + f : List elem -> [Nil, Cons elem a] as LinkedList U + "# + ), + @r###" + ── TYPE ARGUMENT NOT LOWERCASE ─ ...r_inline_alias_argument_uppercase/Test.roc ─ + + This alias type argument is not lowercase: + + 4│ f : List elem -> [Nil, Cons elem a] as LinkedList U + ^ + + All type arguments must be lowercase. + "### + ); + + test_report!( + mismatched_single_tag_arg, + indoc!( + r#" + isEmpty = + \email -> + Email str = email + Str.isEmpty str + + isEmpty (Name "boo") + "# + ), + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + This 1st argument to `isEmpty` has an unexpected type: + + 9│ isEmpty (Name "boo") + ^^^^^^^^^^ + + This `Name` tag application has the type: + + [Name Str] + + But `isEmpty` needs its 1st argument to be: + + [Email Str] + + Tip: Seems like a tag typo. Maybe `Name` should be `Email`? + + Tip: Can more type annotations be added? Type annotations always help + me give more specific messages, and I think they could help a lot in + this case + "### + ); + + test_report!( + issue_2326, + indoc!( + r#" + C a b : a -> D a b + D a b : { a, b } + + f : C a Num.Nat -> D a Num.Nat + f = \c -> c 6 + f + "# + ), + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + This 1st argument to `c` has an unexpected type: + + 8│ f = \c -> c 6 + ^ + + The argument is a number of type: + + Num * + + But `c` needs its 1st argument to be: + + a + + Tip: The type annotation uses the type variable `a` to say that this + definition can produce any type of value. But in the body I see that + it will only produce a `Num` value of a single specific type. Maybe + change the type annotation to be more specific? Maybe change the code + to be more general? + "### + ); + + test_report!( + issue_2380_annotations_only, + indoc!( + r#" + F : F + a : F + a + "# + ), + @r###" + ── CYCLIC ALIAS ────────────────────────────────────────── /code/proj/Main.roc ─ + + The `F` alias is self-recursive in an invalid way: + + 4│ F : F + ^ + + Recursion in aliases is only allowed if recursion happens behind a + tagged union, at least one variant of which is not recursive. + "### + ); + + test_report!( + issue_2380_typed_body, + indoc!( + r#" + F : F + a : F + a = 1 + a + "# + ), + @r###" + ── CYCLIC ALIAS ────────────────────────────────────────── /code/proj/Main.roc ─ + + The `F` alias is self-recursive in an invalid way: + + 4│ F : F + ^ + + Recursion in aliases is only allowed if recursion happens behind a + tagged union, at least one variant of which is not recursive. + "### + ); + + test_report!( + issue_2380_alias_with_vars, + indoc!( + r#" + F a b : F a b + a : F Str Str + a + "# + ), + @r###" + ── CYCLIC ALIAS ────────────────────────────────────────── /code/proj/Main.roc ─ + + The `F` alias is self-recursive in an invalid way: + + 4│ F a b : F a b + ^ + + Recursion in aliases is only allowed if recursion happens behind a + tagged union, at least one variant of which is not recursive. + "### + ); + + test_report!( + issue_2167_record_field_optional_and_required_mismatch, + indoc!( + r#" + Job : [Job { inputs : List Str }] + job : { inputs ? List Str } -> Job + job = \{ inputs } -> + Job { inputs } + + job { inputs: ["build", "test"] } + "# + ), + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + The 1st argument to `job` is weird: + + 6│ job = \{ inputs } -> + ^^^^^^^^^^ + + The argument is a pattern that matches record values of type: + + { inputs : List Str } + + But the annotation on `job` says the 1st argument should be: + + { inputs ? List Str } + + Tip: To extract the `.inputs` field it must be non-optional, but the + type says this field is optional. Learn more about optional fields at + TODO. + "### + ); + + test_report!( + unify_recursive_with_nonrecursive, + indoc!( + r#" + Job : [Job { inputs : List Job }] + + job : { inputs : List Str } -> Job + job = \{ inputs } -> + Job { inputs } + + job { inputs: ["build", "test"] } + "# + ), + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + Something is off with the body of the `job` definition: + + 6│ job : { inputs : List Str } -> Job + 7│ job = \{ inputs } -> + 8│ Job { inputs } + ^^^^^^^^^^^^^^ + + This `Job` tag application has the type: + + [Job { inputs : List Str }] + + But the type annotation on `job` says it should be: + + [Job { inputs : List a }]a as a + "### + ); + + test_report!( + nested_datatype, + indoc!( + r#" + Nested a : [Chain a (Nested (List a)), Term] + + s : Nested Str + + s + "# + ), + @r###" + ── NESTED DATATYPE ─────────────────────────────────────── /code/proj/Main.roc ─ + + `Nested` is a nested datatype. Here is one recursive usage of it: + + 4│ Nested a : [Chain a (Nested (List a)), Term] + ^^^^^^^^^^^^^^^ + + But recursive usages of `Nested` must match its definition: + + 4│ Nested a : [Chain a (Nested (List a)), Term] + ^^^^^^^^ + + Nested datatypes are not supported in Roc. + + Hint: Consider rewriting the definition of `Nested` to use the recursive type with the same arguments. + "### + ); + + test_report!( + nested_datatype_inline, + indoc!( + r#" + f : {} -> [Chain a (Nested (List a)), Term] as Nested a + + f + "# + ), + @r###" + ── NESTED DATATYPE ─────────────────────────────────────── /code/proj/Main.roc ─ + + `Nested` is a nested datatype. Here is one recursive usage of it: + + 4│ f : {} -> [Chain a (Nested (List a)), Term] as Nested a + ^^^^^^^^^^^^^^^ + + But recursive usages of `Nested` must match its definition: + + 4│ f : {} -> [Chain a (Nested (List a)), Term] as Nested a + ^^^^^^^^ + + Nested datatypes are not supported in Roc. + + Hint: Consider rewriting the definition of `Nested` to use the recursive type with the same arguments. + "### + ); + + macro_rules! mismatched_suffix_tests { + ($($number:expr, $suffix:expr, $name:ident)*) => {$( + test_report!( + $name, + &{ + let number = $number.to_string(); + let mut typ = $suffix.to_string(); + typ.get_mut(0..1).unwrap().make_ascii_uppercase(); + let bad_type = if $suffix == "u8" { "I8" } else { "U8" }; + + format!(indoc!( + r#" + use : Num.{} -> Num.U8 + use {}{} + "# + ), bad_type, number, $suffix) + }, + |golden| { + let number = $number.to_string(); + let mut typ = $suffix.to_string(); + typ.get_mut(0..1).unwrap().make_ascii_uppercase(); + let bad_type = if $suffix == "u8" { "I8" } else { "U8" }; + let carets = "^".repeat(number.len() + $suffix.len()); + let kind = match $suffix { + "dec"|"f32"|"f64" => "a fraction", + _ => "an integer", + }; + + let real = format!(indoc!( + r#" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + This 1st argument to `use` has an unexpected type: + + 5│ use {}{} + {} + + The argument is {} of type: + + {} + + But `use` needs its 1st argument to be: + + {} + "# + ), number, $suffix, carets, kind, typ, bad_type); + + assert_eq!(golden, real); + } + ); + )*} + } + + mismatched_suffix_tests! { + 1, "u8", mismatched_suffix_u8 + 1, "u16", mismatched_suffix_u16 + 1, "u32", mismatched_suffix_u32 + 1, "u64", mismatched_suffix_u64 + 1, "u128", mismatched_suffix_u128 + 1, "i8", mismatched_suffix_i8 + 1, "i16", mismatched_suffix_i16 + 1, "i32", mismatched_suffix_i32 + 1, "i64", mismatched_suffix_i64 + 1, "i128", mismatched_suffix_i128 + 1, "nat", mismatched_suffix_nat + 1, "dec", mismatched_suffix_dec + 1, "f32", mismatched_suffix_f32 + 1, "f64", mismatched_suffix_f64 + } + + macro_rules! mismatched_suffix_tests_in_pattern { + ($($number:expr, $suffix:expr, $name:ident)*) => {$( + test_report!( + $name, + &{ + let number = $number.to_string(); + let mut typ = $suffix.to_string(); + typ.get_mut(0..1).unwrap().make_ascii_uppercase(); + let bad_suffix = if $suffix == "u8" { "i8" } else { "u8" }; + + format!(indoc!( + r#" + when {}{} is + {}{} -> 1 + _ -> 1 + "# + ), number, bad_suffix, number, $suffix) + }, + |golden| { + let number = $number.to_string(); + let mut typ = $suffix.to_string(); + typ.get_mut(0..1).unwrap().make_ascii_uppercase(); + let bad_suffix = if $suffix == "u8" { "i8" } else { "u8" }; + let bad_type = if $suffix == "u8" { "I8" } else { "U8" }; + + let real = format!(indoc!( + r#" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + The branches of this `when` expression don't match the condition: + + 4│> when {}{} is + 5│ {}{} -> 1 + 6│ _ -> 1 + + The `when` condition is an integer of type: + + {} + + But the branch patterns have type: + + {} + + The branches must be cases of the `when` condition's type! + "# + ), number, bad_suffix, number, $suffix, bad_type, typ); + + assert_eq!(golden, real); + } + ); + )*} + } + + mismatched_suffix_tests_in_pattern! { + 1, "u8", mismatched_suffix_u8_pattern + 1, "u16", mismatched_suffix_u16_pattern + 1, "u32", mismatched_suffix_u32_pattern + 1, "u64", mismatched_suffix_u64_pattern + 1, "u128", mismatched_suffix_u128_pattern + 1, "i8", mismatched_suffix_i8_pattern + 1, "i16", mismatched_suffix_i16_pattern + 1, "i32", mismatched_suffix_i32_pattern + 1, "i64", mismatched_suffix_i64_pattern + 1, "i128", mismatched_suffix_i128_pattern + 1, "nat", mismatched_suffix_nat_pattern + 1, "dec", mismatched_suffix_dec_pattern + 1, "f32", mismatched_suffix_f32_pattern + 1, "f64", mismatched_suffix_f64_pattern + } + + test_report!( + bad_numeric_literal_suffix, + indoc!( + r#" + 1u256 + "# + ), + // TODO: link to number suffixes + @r###" + ── SYNTAX PROBLEM ──────────────────────────────────────── /code/proj/Main.roc ─ + + This integer literal contains an invalid digit: + + 4│ 1u256 + ^^^^^ + + Integer literals can only contain the digits + 0-9, or have an integer suffix. + + Tip: Learn more about number literals at TODO + "### + ); + + test_report!( + numer_literal_multi_suffix, + indoc!( + r#" + 1u8u8 + "# + ), + // TODO: link to number suffixes + @r###" + ── SYNTAX PROBLEM ──────────────────────────────────────── /code/proj/Main.roc ─ + + This integer literal contains an invalid digit: + + 4│ 1u8u8 + ^^^^^ + + Integer literals can only contain the digits + 0-9, or have an integer suffix. + + Tip: Learn more about number literals at TODO + "### + ); + + test_report!( + int_literal_has_float_suffix, + indoc!( + r#" + 0b1f32 + "# + ), + @r###" + ── CONFLICTING NUMBER SUFFIX ───────────────────────────── /code/proj/Main.roc ─ + + This number literal is an integer, but it has a float suffix: + + 4│ 0b1f32 + ^^^^^^ + "### + ); + + test_report!( + float_literal_has_int_suffix, + indoc!( + r#" + 1.0u8 + "# + ), + @r###" + ── CONFLICTING NUMBER SUFFIX ───────────────────────────── /code/proj/Main.roc ─ + + This number literal is a float, but it has an integer suffix: + + 4│ 1.0u8 + ^^^^^ + "### + ); + + test_report!( + u8_overflow, + "256u8", + @r###" + ── NUMBER OVERFLOWS SUFFIX ─────────────────────────────── /code/proj/Main.roc ─ + + This integer literal overflows the type indicated by its suffix: + + 4│ 256u8 + ^^^^^ + + Tip: The suffix indicates this integer is a U8, whose maximum value is + 255. + "### + ); + + test_report!( + negative_u8, + "-1u8", + @r###" + ── NUMBER UNDERFLOWS SUFFIX ────────────────────────────── /code/proj/Main.roc ─ + + This integer literal underflows the type indicated by its suffix: + + 4│ -1u8 + ^^^^ + + Tip: The suffix indicates this integer is a U8, whose minimum value is + 0. + "### + ); + + test_report!( + u16_overflow, + "65536u16", + @r###" + ── NUMBER OVERFLOWS SUFFIX ─────────────────────────────── /code/proj/Main.roc ─ + + This integer literal overflows the type indicated by its suffix: + + 4│ 65536u16 + ^^^^^^^^ + + Tip: The suffix indicates this integer is a U16, whose maximum value + is 65535. + "### + ); + + test_report!( + negative_u16, + "-1u16", + @r###" + ── NUMBER UNDERFLOWS SUFFIX ────────────────────────────── /code/proj/Main.roc ─ + + This integer literal underflows the type indicated by its suffix: + + 4│ -1u16 + ^^^^^ + + Tip: The suffix indicates this integer is a U16, whose minimum value + is 0. + "### + ); + + test_report!( + u32_overflow, + "4_294_967_296u32", + @r###" + ── NUMBER OVERFLOWS SUFFIX ─────────────────────────────── /code/proj/Main.roc ─ + + This integer literal overflows the type indicated by its suffix: + + 4│ 4_294_967_296u32 + ^^^^^^^^^^^^^^^^ + + Tip: The suffix indicates this integer is a U32, whose maximum value + is 4_294_967_295. + "### + ); + + test_report!( + negative_u32, + "-1u32", + @r###" + ── NUMBER UNDERFLOWS SUFFIX ────────────────────────────── /code/proj/Main.roc ─ + + This integer literal underflows the type indicated by its suffix: + + 4│ -1u32 + ^^^^^ + + Tip: The suffix indicates this integer is a U32, whose minimum value + is 0. + "### + ); + + test_report!( + u64_overflow, + "18_446_744_073_709_551_616u64", + @r###" + ── NUMBER OVERFLOWS SUFFIX ─────────────────────────────── /code/proj/Main.roc ─ + + This integer literal overflows the type indicated by its suffix: + + 4│ 18_446_744_073_709_551_616u64 + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + Tip: The suffix indicates this integer is a U64, whose maximum value + is 18_446_744_073_709_551_615. + "### + ); + + test_report!( + negative_u64, + "-1u64", + @r###" + ── NUMBER UNDERFLOWS SUFFIX ────────────────────────────── /code/proj/Main.roc ─ + + This integer literal underflows the type indicated by its suffix: + + 4│ -1u64 + ^^^^^ + + Tip: The suffix indicates this integer is a U64, whose minimum value + is 0. + "### + ); + + test_report!( + negative_u128, + "-1u128", + @r###" + ── NUMBER UNDERFLOWS SUFFIX ────────────────────────────── /code/proj/Main.roc ─ + + This integer literal underflows the type indicated by its suffix: + + 4│ -1u128 + ^^^^^^ + + Tip: The suffix indicates this integer is a U128, whose minimum value + is 0. + "### + ); + + test_report!( + i8_overflow, + "128i8", + @r###" + ── NUMBER OVERFLOWS SUFFIX ─────────────────────────────── /code/proj/Main.roc ─ + + This integer literal overflows the type indicated by its suffix: + + 4│ 128i8 + ^^^^^ + + Tip: The suffix indicates this integer is a I8, whose maximum value is + 127. + "### + ); + + test_report!( + i8_underflow, + "-129i8", + @r###" + ── NUMBER UNDERFLOWS SUFFIX ────────────────────────────── /code/proj/Main.roc ─ + + This integer literal underflows the type indicated by its suffix: + + 4│ -129i8 + ^^^^^^ + + Tip: The suffix indicates this integer is a I8, whose minimum value is + -128. + "### + ); + + test_report!( + i16_overflow, + "32768i16", + @r###" + ── NUMBER OVERFLOWS SUFFIX ─────────────────────────────── /code/proj/Main.roc ─ + + This integer literal overflows the type indicated by its suffix: + + 4│ 32768i16 + ^^^^^^^^ + + Tip: The suffix indicates this integer is a I16, whose maximum value + is 32767. + "### + ); + + test_report!( + i16_underflow, + "-32769i16", + @r###" + ── NUMBER UNDERFLOWS SUFFIX ────────────────────────────── /code/proj/Main.roc ─ + + This integer literal underflows the type indicated by its suffix: + + 4│ -32769i16 + ^^^^^^^^^ + + Tip: The suffix indicates this integer is a I16, whose minimum value + is -32768. + "### + ); + + test_report!( + i32_overflow, + "2_147_483_648i32", + @r###" + ── NUMBER OVERFLOWS SUFFIX ─────────────────────────────── /code/proj/Main.roc ─ + + This integer literal overflows the type indicated by its suffix: + + 4│ 2_147_483_648i32 + ^^^^^^^^^^^^^^^^ + + Tip: The suffix indicates this integer is a I32, whose maximum value + is 2_147_483_647. + "### + ); + + test_report!( + i32_underflow, + "-2_147_483_649i32", + @r###" + ── NUMBER UNDERFLOWS SUFFIX ────────────────────────────── /code/proj/Main.roc ─ + + This integer literal underflows the type indicated by its suffix: + + 4│ -2_147_483_649i32 + ^^^^^^^^^^^^^^^^^ + + Tip: The suffix indicates this integer is a I32, whose minimum value + is -2_147_483_648. + "### + ); + + test_report!( + i64_overflow, + "9_223_372_036_854_775_808i64", + @r###" + ── NUMBER OVERFLOWS SUFFIX ─────────────────────────────── /code/proj/Main.roc ─ + + This integer literal overflows the type indicated by its suffix: + + 4│ 9_223_372_036_854_775_808i64 + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + Tip: The suffix indicates this integer is a I64, whose maximum value + is 9_223_372_036_854_775_807. + "### + ); + + test_report!( + i64_underflow, + "-9_223_372_036_854_775_809i64", + @r###" + ── NUMBER UNDERFLOWS SUFFIX ────────────────────────────── /code/proj/Main.roc ─ + + This integer literal underflows the type indicated by its suffix: + + 4│ -9_223_372_036_854_775_809i64 + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + Tip: The suffix indicates this integer is a I64, whose minimum value + is -9_223_372_036_854_775_808. + "### + ); + + test_report!( + i128_overflow, + "170_141_183_460_469_231_731_687_303_715_884_105_728i128", + @r###" + ── NUMBER OVERFLOWS SUFFIX ─────────────────────────────── /code/proj/Main.roc ─ + + This integer literal overflows the type indicated by its suffix: + + 4│ 170_141_183_460_469_231_731_687_303_715_884_105_728i128 + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + Tip: The suffix indicates this integer is a I128, whose maximum value + is 170_141_183_460_469_231_731_687_303_715_884_105_727. + "### + ); + + test_report!( + list_get_negative_number, + indoc!( + r#" + List.get [1,2,3] -1 + "# + ), + // TODO: this error message could be improved, e.g. something like "This argument can + // be used as ... because of its literal value" + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + This 2nd argument to `get` has an unexpected type: + + 4│ List.get [1,2,3] -1 + ^^ + + The argument is a number of type: + + I8, I16, F32, I32, F64, I64, I128, or Dec + + But `get` needs its 2nd argument to be: + + Nat + "### + ); + + test_report!( + list_get_negative_number_indirect, + indoc!( + r#" + a = -9_223_372_036_854 + List.get [1,2,3] a + "# + ), + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + This 2nd argument to `get` has an unexpected type: + + 5│ List.get [1,2,3] a + ^ + + This `a` value is a: + + F64, I64, I128, or Dec + + But `get` needs its 2nd argument to be: + + Nat + "### + ); + + test_report!( + list_get_negative_number_double_indirect, + indoc!( + r#" + a = -9_223_372_036_854 + b = a + List.get [1,2,3] b + "# + ), + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + This 2nd argument to `get` has an unexpected type: + + 6│ List.get [1,2,3] b + ^ + + This `b` value is a: + + F64, I64, I128, or Dec + + But `get` needs its 2nd argument to be: + + Nat + "### + ); + + test_report!( + compare_unsigned_to_signed, + indoc!( + r#" + when -1 is + 1u8 -> 1 + _ -> 1 + "# + ), + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + The branches of this `when` expression don't match the condition: + + 4│> when -1 is + 5│ 1u8 -> 1 + 6│ _ -> 1 + + The `when` condition is a number of type: + + I8, I16, F32, I32, F64, I64, I128, or Dec + + But the branch patterns have type: + + U8 + + The branches must be cases of the `when` condition's type! + "### + ); + + test_report!( + recursive_type_alias_is_newtype, + indoc!( + r#" + R a : [Only (R a)] + + v : R Str + v + "# + ), + @r###" + ── CYCLIC ALIAS ────────────────────────────────────────── /code/proj/Main.roc ─ + + The `R` alias is self-recursive in an invalid way: + + 4│ R a : [Only (R a)] + ^ + + Recursion in aliases is only allowed if recursion happens behind a + tagged union, at least one variant of which is not recursive. + "### + ); + + test_report!( + recursive_type_alias_is_newtype_deep, + indoc!( + r#" + R a : [Only { very: [Deep (R a)] }] + + v : R Str + v + "# + ), + @r###" + ── CYCLIC ALIAS ────────────────────────────────────────── /code/proj/Main.roc ─ + + The `R` alias is self-recursive in an invalid way: + + 4│ R a : [Only { very: [Deep (R a)] }] + ^ + + Recursion in aliases is only allowed if recursion happens behind a + tagged union, at least one variant of which is not recursive. + "### + ); + + test_report!( + recursive_type_alias_is_newtype_mutual, + indoc!( + r#" + Foo a : [Thing (Bar a)] + Bar a : [Stuff (Foo a)] + + v : Bar Str + v + "# + ), + @r###" + ── CYCLIC ALIAS ────────────────────────────────────────── /code/proj/Main.roc ─ + + The `Foo` alias is recursive in an invalid way: + + 4│ Foo a : [Thing (Bar a)] + ^^^ + + The `Foo` alias depends on itself through the following chain of + definitions: + + ┌─────┐ + │ Foo + │ ↓ + │ Bar + └─────┘ + + Recursion in aliases is only allowed if recursion happens behind a + tagged union, at least one variant of which is not recursive. + "### + ); + + test_report!( + issue_2458, + indoc!( + r#" + Result a b : [Ok a, Err b] + + Foo a : [Blah (Result (Bar a) [])] + Bar a : Foo a + + v : Bar Str + v + "# + ), + @r###" + ── DUPLICATE NAME ──────────────────────────────────────── /code/proj/Main.roc ─ + + This alias has the same name as a builtin: + + 4│ Result a b : [Ok a, Err b] + ^^^^^^^^^^^^^^^^^^^^^^^^^^ + + All builtin aliases are in scope by default, so I need this alias to + have a different name! + "### + ); + + test_report!( + opaque_type_not_in_scope, + indoc!( + r#" + @Age 21 + "# + ), + @r###" + ── OPAQUE TYPE NOT DEFINED ─────────────────────────────── /code/proj/Main.roc ─ + + The opaque type Age referenced here is not defined: + + 4│ @Age 21 + ^^^^ + + Note: It looks like there are no opaque types declared in this scope yet! + "### + ); + + test_report!( + opaque_reference_not_opaque_type, + indoc!( + r#" + Age : Num.U8 + + @Age 21 + "# + ), + @r###" + ── OPAQUE TYPE NOT DEFINED ─────────────────────────────── /code/proj/Main.roc ─ + + The opaque type Age referenced here is not defined: + + 6│ @Age 21 + ^^^^ + + Note: There is an alias of the same name: + + 4│ Age : Num.U8 + ^^^ + + Note: It looks like there are no opaque types declared in this scope yet! + + ── UNUSED DEFINITION ───────────────────────────────────── /code/proj/Main.roc ─ + + `Age` is not used anywhere in your code. + + 4│ Age : Num.U8 + ^^^^^^^^^^^^ + + If you didn't intend on using `Age` then remove it so future readers of + your code don't wonder why it is there. + "### + ); + + test_report!( + qualified_opaque_reference, + indoc!( + r#" + OtherModule.@Age 21 + "# + ), + // TODO: get rid of the first error. Consider parsing OtherModule.@Age to completion + // and checking it during can. The reason the error appears is because it is parsed as + // Apply(Error(OtherModule), [@Age, 21]) + @r###" + ── OPAQUE TYPE NOT DEFINED ─────────────────────────────── /code/proj/Main.roc ─ + + The opaque type Age referenced here is not defined: + + 4│ OtherModule.@Age 21 + ^^^^ + + Note: It looks like there are no opaque types declared in this scope yet! + + ── SYNTAX PROBLEM ──────────────────────────────────────── /code/proj/Main.roc ─ + + I am trying to parse a qualified name here: + + 4│ OtherModule.@Age 21 + ^ + + I was expecting to see an identifier next, like height. A complete + qualified name looks something like Json.Decode.string. + "### + ); + + test_report!( + opaque_used_outside_declaration_scope, + indoc!( + r#" + age = + Age := Num.U8 + 21u8 + + @Age age + "# + ), + // TODO(opaques): there is a potential for a better error message here, if the usage of + // `@Age` can be linked to the declaration of `Age` inside `age`, and a suggestion to + // raise that declaration to the outer scope. + @r###" + ── UNUSED DEFINITION ───────────────────────────────────── /code/proj/Main.roc ─ + + `Age` is not used anywhere in your code. + + 5│ Age := Num.U8 + ^^^^^^^^^^^^^ + + If you didn't intend on using `Age` then remove it so future readers of + your code don't wonder why it is there. + + ── OPAQUE TYPE NOT DEFINED ─────────────────────────────── /code/proj/Main.roc ─ + + The opaque type Age referenced here is not defined: + + 8│ @Age age + ^^^^ + + Note: It looks like there are no opaque types declared in this scope yet! + "### + ); + + test_report!( + unimported_modules_reported, + indoc!( + r#" + alt : Task.Task {} [] + alt = "whatever man you don't even know my type" + alt + "# + ), + @r###" + ── MODULE NOT IMPORTED ─────────────────────────────────── /code/proj/Main.roc ─ + + The `Task` module is not imported: + + 4│ alt : Task.Task {} [] + ^^^^^^^^^^^^^^^ + + Is there an import missing? Perhaps there is a typo. Did you mean one + of these? + + Hash + List + Num + Box + "### + ); + + test_report!( + opaque_mismatch_check, + indoc!( + r#" + Age := Num.U8 + + n : Age + n = @Age "" + + n + "# + ), + // TODO(opaques): error could be improved by saying that the opaque definition demands + // that the argument be a U8, and linking to the definitin! + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + This expression is used in an unexpected way: + + 7│ n = @Age "" + ^^ + + This argument to an opaque type has type: + + Str + + But you are trying to use it as: + + U8 + "### + ); + + test_report!( + opaque_mismatch_infer, + indoc!( + r#" + F n := n + + if Bool.true + then @F "" + else @F {} + "# + ), + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + This expression is used in an unexpected way: + + 8│ else @F {} + ^^ + + This argument to an opaque type has type: + + {} + + But you are trying to use it as: + + Str + "### + ); + + test_report!( + opaque_creation_is_not_wrapped, + indoc!( + r#" + F n := n + + v : F Str + v = "" + + v + "# + ), + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + Something is off with the body of the `v` definition: + + 6│ v : F Str + 7│ v = "" + ^^ + + The body is a string of type: + + Str + + But the type annotation on `v` says it should be: + + F Str + + Tip: Type comparisons between an opaque type are only ever equal if + both types are the same opaque type. Did you mean to create an opaque + type by wrapping it? If I have an opaque type Age := U32 I can create + an instance of this opaque type by doing @Age 23. + "### + ); + + test_report!( + opaque_mismatch_pattern_check, + indoc!( + r#" + Age := Num.U8 + + f : Age -> Num.U8 + f = \Age n -> n + + f + "# + ), + // TODO(opaques): error could be improved by saying that the user-provided pattern + // probably wants to change "Age" to "@Age"! + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + The 1st argument to `f` is weird: + + 7│ f = \Age n -> n + ^^^^^ + + The argument is a pattern that matches a `Age` tag of type: + + [Age *] + + But the annotation on `f` says the 1st argument should be: + + Age + + Tip: Type comparisons between an opaque type are only ever equal if + both types are the same opaque type. Did you mean to create an opaque + type by wrapping it? If I have an opaque type Age := U32 I can create + an instance of this opaque type by doing @Age 23. + "### + ); + + test_report!( + opaque_mismatch_pattern_infer, + indoc!( + r#" + F n := n + + \x -> + when x is + @F A -> "" + @F {} -> "" + "# + ), + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + The 2nd pattern in this `when` does not match the previous ones: + + 9│ @F {} -> "" + ^^^^^ + + The 2nd pattern is trying to matchF unwrappings of type: + + F {}a + + But all the previous branches match: + + F [A] + "### + ); + + test_report!( + opaque_pattern_match_not_exhaustive_tag, + indoc!( + r#" + F n := n + + v : F [A, B, C] + + when v is + @F A -> "" + @F B -> "" + "# + ), + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + The branches of this `when` expression don't match the condition: + + 8│> when v is + 9│ @F A -> "" + 10│ @F B -> "" + + This `v` value is a: + + F [C, …] + + But the branch patterns have type: + + F […] + + The branches must be cases of the `when` condition's type! + + Tip: Looks like the branches are missing coverage of the `C` tag. + + Tip: Maybe you need to add a catch-all branch, like `_`? + "### + ); + + test_report!( + opaque_pattern_match_not_exhaustive_int, + indoc!( + r#" + F n := n + + v : F Num.U8 + + when v is + @F 1 -> "" + @F 2 -> "" + "# + ), + @r###" + ── UNSAFE PATTERN ──────────────────────────────────────── /code/proj/Main.roc ─ + + This `when` does not cover all the possibilities: + + 8│> when v is + 9│> @F 1 -> "" + 10│> @F 2 -> "" + + Other possibilities include: + + @F _ + + I would have to crash if I saw one of those! Add branches for them! + "### + ); + + test_report!( + let_polymorphism_with_scoped_type_variables, + indoc!( + r#" + f : a -> a + f = \x -> + y : a -> a + y = \z -> z + + n = y 1u8 + x1 = y x + (\_ -> x1) n + + f + "# + ), + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + This 1st argument to `y` has an unexpected type: + + 9│ n = y 1u8 + ^^^ + + The argument is an integer of type: + + U8 + + But `y` needs its 1st argument to be: + + a + + Tip: The type annotation uses the type variable `a` to say that this + definition can produce any type of value. But in the body I see that + it will only produce a `U8` value of a single specific type. Maybe + change the type annotation to be more specific? Maybe change the code + to be more general? + "### + ); + + test_report!( + non_exhaustive_with_guard, + indoc!( + r#" + x : [A] + when x is + A if Bool.true -> "" + "# + ), + @r###" + ── UNSAFE PATTERN ──────────────────────────────────────── /code/proj/Main.roc ─ + + This `when` does not cover all the possibilities: + + 5│> when x is + 6│> A if Bool.true -> "" + + Other possibilities include: + + A (note the lack of an if clause) + + I would have to crash if I saw one of those! Add branches for them! + "### + ); + + test_report!( + invalid_record_extension_type, + indoc!( + r#" + f : { x : Num.Nat }[] + f + "# + ), + @r###" + ── INVALID_EXTENSION_TYPE ──────────────────────────────── /code/proj/Main.roc ─ + + This record extension type is invalid: + + 4│ f : { x : Num.Nat }[] + ^^ + + Note: A record extension variable can only contain a type variable or + another record. + "### + ); + + test_report!( + invalid_tag_extension_type, + indoc!( + r#" + f : [A]Str + f + "# + ), + @r###" + ── INVALID_EXTENSION_TYPE ──────────────────────────────── /code/proj/Main.roc ─ + + This tag union extension type is invalid: + + 4│ f : [A]Str + ^^^ + + Note: A tag union extension variable can only contain a type variable + or another tag union. + "### + ); + + test_report!( + unknown_type, + indoc!( + r#" + Type : [Constructor UnknownType] + + insertHelper : UnknownType, Type -> Type + insertHelper = \h, m -> + when m is + Constructor _ -> Constructor h + + insertHelper + "# + ), + @r###" + ── UNRECOGNIZED NAME ───────────────────────────────────── /code/proj/Main.roc ─ + + Nothing is named `UnknownType` in this scope. + + 4│ Type : [Constructor UnknownType] + ^^^^^^^^^^^ + + Did you mean one of these? + + Type + Unsigned8 + Unsigned16 + Unsigned64 + + ── UNRECOGNIZED NAME ───────────────────────────────────── /code/proj/Main.roc ─ + + Nothing is named `UnknownType` in this scope. + + 6│ insertHelper : UnknownType, Type -> Type + ^^^^^^^^^^^ + + Did you mean one of these? + + Type + Unsigned8 + Unsigned16 + Unsigned64 + "### + ); + + test_report!( + ability_first_demand_not_indented_enough, + indoc!( + r#" + MEq implements + eq : a, a -> U64 where a implements MEq + + 1 + "# + ), + @r###" + ── UNFINISHED ABILITY ── tmp/ability_first_demand_not_indented_enough/Test.roc ─ + + I was partway through parsing an ability definition, but I got stuck + here: + + 4│ MEq implements + 5│ eq : a, a -> U64 where a implements MEq + ^ + + I suspect this line is not indented enough (by 1 spaces) + "### + ); + + test_report!( + ability_demands_not_indented_with_first, + indoc!( + r#" + MEq implements + eq : a, a -> U64 where a implements MEq + neq : a, a -> U64 where a implements MEq + + 1 + "# + ), + @r#" + ── UNFINISHED ABILITY ─── tmp/ability_demands_not_indented_with_first/Test.roc ─ + + I was partway through parsing an ability definition, but I got stuck + here: + + 5│ eq : a, a -> U64 where a implements MEq + 6│ neq : a, a -> U64 where a implements MEq + ^ + + I suspect this line is indented too much (by 4 spaces)"# + ); + + test_report!( + ability_demand_value_has_args, + indoc!( + r#" + MEq implements + eq b c : a, a -> U64 where a implements MEq + + 1 + "# + ), + @r#" + ── UNFINISHED ABILITY ───────────── tmp/ability_demand_value_has_args/Test.roc ─ + + I was partway through parsing an ability definition, but I got stuck + here: + + 4│ MEq implements + 5│ eq b c : a, a -> U64 where a implements MEq + ^ + + I was expecting to see a : annotating the signature of this value + next."# + ); + + test_report!( + ability_non_signature_expression, + indoc!( + r#" + MEq implements + 123 + + 1 + "# + ), + @r###" + ── UNFINISHED ABILITY ────────── tmp/ability_non_signature_expression/Test.roc ─ + + I was partway through parsing an ability definition, but I got stuck + here: + + 4│ MEq implements + 5│ 123 + ^ + + I was expecting to see a value signature next. + "### + ); + + test_report!( + wildcard_in_alias, + indoc!( + r#" + I : Num.Int * + a : I + a + "# + ), + @r###" + ── UNBOUND TYPE VARIABLE ───────────────────────────────── /code/proj/Main.roc ─ + + The definition of `I` has an unbound type variable: + + 4│ I : Num.Int * + ^ + + Tip: Type variables must be bound before the `:`. Perhaps you intended + to add a type parameter to this type? + "### + ); + + test_report!( + wildcard_in_opaque, + indoc!( + r#" + I := Num.Int * + a : I + a + "# + ), + @r###" + ── UNBOUND TYPE VARIABLE ───────────────────────────────── /code/proj/Main.roc ─ + + The definition of `I` has an unbound type variable: + + 4│ I := Num.Int * + ^ + + Tip: Type variables must be bound before the `:=`. Perhaps you intended + to add a type parameter to this type? + "### + ); + + test_report!( + multiple_wildcards_in_alias, + indoc!( + r#" + I : [A (Num.Int *), B (Num.Int *)] + a : I + a + "# + ), + @r###" + ── UNBOUND TYPE VARIABLE ───────────────────────────────── /code/proj/Main.roc ─ + + The definition of `I` has 2 unbound type variables. + + Here is one occurrence: + + 4│ I : [A (Num.Int *), B (Num.Int *)] + ^ + + Tip: Type variables must be bound before the `:`. Perhaps you intended + to add a type parameter to this type? + "### + ); + + test_report!( + inference_var_in_alias, + indoc!( + r#" + I : Num.Int _ + a : I + a + "# + ), + @r###" + ── UNBOUND TYPE VARIABLE ───────────────────────────────── /code/proj/Main.roc ─ + + The definition of `I` has an unbound type variable: + + 4│ I : Num.Int _ + ^ + + Tip: Type variables must be bound before the `:`. Perhaps you intended + to add a type parameter to this type? + "### + ); + + test_report!( + unbound_var_in_alias, + indoc!( + r#" + I : Num.Int a + a : I + a + "# + ), + @r###" + ── UNBOUND TYPE VARIABLE ───────────────────────────────── /code/proj/Main.roc ─ + + The definition of `I` has an unbound type variable: + + 4│ I : Num.Int a + ^ + + Tip: Type variables must be bound before the `:`. Perhaps you intended + to add a type parameter to this type? + "### + ); + + test_report!( + ability_bad_type_parameter, + indoc!( + r#" + app "test" provides [] to "./platform" + + MHash a b c implements + hash : a -> U64 where a implements MHash + "# + ), + @r###" + ── ABILITY HAS TYPE VARIABLES ──────────────────────────── /code/proj/Main.roc ─ + + The definition of the `MHash` ability includes type variables: + + 3│ MHash a b c implements + ^^^^^ + + Abilities cannot depend on type variables, but their member values + can! + + ── UNUSED DEFINITION ───────────────────────────────────── /code/proj/Main.roc ─ + + `MHash` is not used anywhere in your code. + + 3│ MHash a b c implements + ^^^^^ + + If you didn't intend on using `MHash` then remove it so future readers + of your code don't wonder why it is there. + "### + ); + + test_report!( + alias_in_implements_clause, + indoc!( + r#" + app "test" provides [hash] to "./platform" + + MHash implements hash : a, b -> Num.U64 where a implements MHash, b implements Bool.Bool + "# + ), + @r###" + ── IMPLEMENTS CLAUSE IS NOT AN ABILITY ─────────────────── /code/proj/Main.roc ─ + + The type referenced in this "implements" clause is not an ability: + + 3│ MHash implements hash : a, b -> Num.U64 where a implements MHash, b implements Bool.Bool + ^^^^^^^^^ + "### + ); + + test_report!( + shadowed_type_variable_in_has_clause, + indoc!( + r#" + app "test" provides [ab1] to "./platform" + + Ab1 implements ab1 : a -> {} where a implements Ab1, a implements Ab1 + "# + ), + @r#" + ── DUPLICATE NAME ──────────────────────────────────────── /code/proj/Main.roc ─ + + The `a` name is first defined here: + + 3│ Ab1 implements ab1 : a -> {} where a implements Ab1, a implements Ab1 + ^^^^^^^^^^^^^^^^ + + But then it's defined a second time here: + + 3│ Ab1 implements ab1 : a -> {} where a implements Ab1, a implements Ab1 + ^^^^^^^^^^^^^^^^ + + Since these variables have the same name, it's easy to use the wrong + one by accident. Give one of them a new name. + "# + ); + + test_report!( + ability_shadows_ability, + indoc!( + r#" + app "test" provides [ab] to "./platform" + + Ability implements ab : a -> U64 where a implements Ability + + Ability implements ab1 : a -> U64 where a implements Ability + "# + ), + @r#" + ── DUPLICATE NAME ──────────────────────────────────────── /code/proj/Main.roc ─ + + The `Ability` name is first defined here: + + 3│ Ability implements ab : a -> U64 where a implements Ability + ^^^^^^^ + + But then it's defined a second time here: + + 5│ Ability implements ab1 : a -> U64 where a implements Ability + ^^^^^^^ + + Since these abilities have the same name, it's easy to use the wrong + one by accident. Give one of them a new name. + "# + ); + + test_report!( + ability_member_does_not_bind_ability, + indoc!( + r#" + app "test" provides [] to "./platform" + + Ability implements ab : {} -> {} + "# + ), + @r#" + ── ABILITY MEMBER MISSING IMPLEMENTS CLAUSE ────────────── /code/proj/Main.roc ─ + + The definition of the ability member `ab` does not include an `implements` + clause binding a type variable to the ability `Ability`: + + 3│ Ability implements ab : {} -> {} + ^^ + + Ability members must include an `implements` clause binding a type + variable to an ability, like + + a implements Ability + + Otherwise, the function does not need to be part of the ability! + + ── UNUSED DEFINITION ───────────────────────────────────── /code/proj/Main.roc ─ + + `Ability` is not used anywhere in your code. + + 3│ Ability implements ab : {} -> {} + ^^^^^^^ + + If you didn't intend on using `Ability` then remove it so future readers + of your code don't wonder why it is there. + "# + ); + + test_report!( + ability_member_binds_parent_twice, + indoc!( + r#" + app "test" provides [] to "./platform" + + MEq implements eq : a, b -> Bool.Bool where a implements MEq, b implements MEq + "# + ), + @r#" + ── ABILITY MEMBER BINDS MULTIPLE VARIABLES ─────────────── /code/proj/Main.roc ─ + + The definition of the ability member `eq` includes multiple variables + bound to the `MEq`` ability:` + + 3│ MEq implements eq : a, b -> Bool.Bool where a implements MEq, b implements MEq + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + Ability members can only bind one type variable to their parent + ability. Otherwise, I wouldn't know what type implements an ability by + looking at specializations! + + Hint: Did you mean to only bind `a` to `MEq`? + "# + ); + + test_report!( + has_clause_not_on_toplevel, + indoc!( + r#" + app "test" provides [f] to "./platform" + + MHash implements hash : (a where a implements MHash) -> Num.U64 + + f : a -> Num.U64 where a implements MHash + "# + ), + @r###" + ── ILLEGAL IMPLEMENTS CLAUSE ───────────────────────────── /code/proj/Main.roc ─ + + An `implements` clause is not allowed here: + + 3│ MHash implements hash : (a where a implements MHash) -> Num.U64 + ^^^^^^^^^^^^^^^^^^ + + `implements` clauses can only be specified on the top-level type + annotations. + + ── ABILITY MEMBER MISSING IMPLEMENTS CLAUSE ────────────── /code/proj/Main.roc ─ + + The definition of the ability member `hash` does not include an + `implements` clause binding a type variable to the ability `MHash`: + + 3│ MHash implements hash : (a where a implements MHash) -> Num.U64 + ^^^^ + + Ability members must include an `implements` clause binding a type + variable to an ability, like + + a implements MHash + + Otherwise, the function does not need to be part of the ability! + "### + ); + + test_report!( + ability_specialization_does_not_match_type, + indoc!( + r#" + app "test" provides [hash] to "./platform" + + MHash implements hash : a -> U64 where a implements MHash + + Id := U32 implements [MHash {hash}] + + hash = \@Id n -> n + "# + ), + @r#" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + Something is off with this specialization of `hash`: + + 7│ hash = \@Id n -> n + ^^^^ + + This value is a declared specialization of type: + + Id -> U32 + + But the type annotation on `hash` says it must match: + + Id -> U64 + "# + ); + + test_report!( + ability_specialization_is_incomplete, + indoc!( + r#" + app "test" provides [eq, le] to "./platform" + + MEq implements + eq : a, a -> Bool where a implements MEq + le : a, a -> Bool where a implements MEq + + Id := U64 implements [MEq {eq}] + + eq = \@Id m, @Id n -> m == n + "# + ), + @r###" + ── INCOMPLETE ABILITY IMPLEMENTATION ───────────────────── /code/proj/Main.roc ─ + + This type does not fully implement the `MEq` ability: + + 7│ Id := U64 implements [MEq {eq}] + ^^^^^^^^ + + The following necessary members are missing implementations: + + le + "### + ); + + test_report!( + ability_specialization_is_unused, + indoc!( + r#" + app "test" provides [hash] to "./platform" + + MHash implements + hash : a -> U64 where a implements MHash + + hash = \_ -> 0u64 + "# + ), + @r###" + ── UNUSED DEFINITION ───────────────────────────────────── /code/proj/Main.roc ─ + + `hash` is not used anywhere in your code. + + 6│ hash = \_ -> 0u64 + ^^^^ + + If you didn't intend on using `hash` then remove it so future readers of + your code don't wonder why it is there. + "### + ); + + test_report!( + ability_specialization_is_duplicated, + indoc!( + r#" + app "test" provides [hash, One, Two] to "./platform" + + MHash implements + hash : a -> U64 where a implements MHash + + One := {} implements [MHash {hash}] + Two := {} implements [MHash {hash}] + + hash = \_ -> 0u64 + "# + ), + // TODO: the error message here could be seriously improved! + @r###" + ── OVERLOADED SPECIALIZATION ───────────────────────────── /code/proj/Main.roc ─ + + This ability member specialization is already claimed to specialize + another opaque type: + + 7│ Two := {} implements [MHash {hash}] + ^^^^ + + Previously, we found it to specialize `hash` for `One`. + + Ability specializations can only provide implementations for one + opaque type, since all opaque types are different! + + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + This specialization of `hash` is overly general: + + 9│ hash = \_ -> 0u64 + ^^^^ + + This value is a declared specialization of type: + + * -> U64 + + But the type annotation on `hash` says it must match: + + a -> U64 where a implements MHash + + Note: The specialized type is too general, and does not provide a + concrete type where a type variable is bound to an ability. + + Specializations can only be made for concrete types. If you have a + generic implementation for this value, perhaps you don't need an + ability? + "### + ); + + test_report!( + ability_specialization_is_duplicated_with_type_mismatch, + indoc!( + r#" + app "test" provides [hash, One, Two] to "./platform" + + MHash implements + hash : a -> U64 where a implements MHash + + One := {} implements [MHash {hash}] + Two := {} implements [MHash {hash}] + + hash = \@One _ -> 0u64 + "# + ), + @r###" + ── OVERLOADED SPECIALIZATION ───────────────────────────── /code/proj/Main.roc ─ + + This ability member specialization is already claimed to specialize + another opaque type: + + 7│ Two := {} implements [MHash {hash}] + ^^^^ + + Previously, we found it to specialize `hash` for `One`. + + Ability specializations can only provide implementations for one + opaque type, since all opaque types are different! + "### + ); + + test_report!( + ability_specialization_conflicting_specialization_types, + indoc!( + r#" + app "test" provides [eq] to "./platform" + + MEq implements + eq : a, a -> Bool where a implements MEq + + You := {} implements [MEq {eq}] + AndI := {} + + eq = \@You {}, @AndI {} -> False + "# + ), + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + Something is off with this specialization of `eq`: + + 9│ eq = \@You {}, @AndI {} -> False + ^^ + + This value is a declared specialization of type: + + You, AndI -> [False] + + But the type annotation on `eq` says it must match: + + You, You -> Bool + + Tip: Did you mean to use `Bool.false` rather than `False`? + "### + ); + + test_report!( + ability_specialization_checked_against_annotation, + indoc!( + r#" + app "test" provides [hash] to "./platform" + + MHash implements + hash : a -> U64 where a implements MHash + + Id := U64 implements [MHash {hash}] + + hash : Id -> U32 + hash = \@Id n -> n + "# + ), + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + Something is off with the body of the `hash` definition: + + 8│ hash : Id -> U32 + 9│ hash = \@Id n -> n + ^ + + This `n` value is a: + + U64 + + But the type annotation on `hash` says it should be: + + U32 + "### + ); + + test_report!( + ability_specialization_called_with_non_specializing, + indoc!( + r#" + app "test" provides [noGoodVeryBadTerrible] to "./platform" + + MHash implements + hash : a -> U64 where a implements MHash + + Id := U64 implements [MHash {hash}] + + hash = \@Id n -> n + + User := {} + + noGoodVeryBadTerrible = + { + nope: hash (@User {}), + notYet: hash (A 1), + } + "# + ), + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + This expression has a type that does not implement the abilities it's expected to: + + 15│ notYet: hash (A 1), + ^^^ + + I can't generate an implementation of the `MHash` ability for + + [A (Num *)] + + Only builtin abilities can have generated implementations! + + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + This expression has a type that does not implement the abilities it's expected to: + + 14│ nope: hash (@User {}), + ^^^^^^^^ + + The type `User` does not fully implement the ability `MHash`. + "### + ); + + test_report!( + ability_not_on_toplevel, + indoc!( + r#" + app "test" provides [main] to "./platform" + + main = + MHash implements + hash : a -> U64 where a implements MHash + + 123 + "# + ), + @r#" + ── ABILITY NOT ON TOP-LEVEL ────────────────────────────── /code/proj/Main.roc ─ + + This ability definition is not on the top-level of a module: + + 4│> MHash implements + 5│> hash : a -> U64 where a implements MHash + + Abilities can only be defined on the top-level of a Roc module. + "# + ); + + test_report!( + expression_generalization_to_ability_is_an_error, + indoc!( + r#" + app "test" provides [hash, hashable] to "./platform" + + MHash implements + hash : a -> U64 where a implements MHash + + Id := U64 implements [MHash {hash}] + hash = \@Id n -> n + + hashable : a where a implements MHash + hashable = @Id 15 + "# + ), + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + Something is off with the body of the `hashable` definition: + + 9│ hashable : a where a implements MHash + 10│ hashable = @Id 15 + ^^^^^^ + + This Id opaque wrapping has the type: + + Id + + But the type annotation on `hashable` says it should be: + + a where a implements MHash + + Note: The type variable `a` says it can take on any value that + implements the ability `MHash`. + + But, I see that the type is only ever used as a a `Id` value. Can you + replace `a` with a more specific type? + "### + ); + + test_report!( + ability_value_annotations_are_an_error, + indoc!( + r#" + app "test" provides [result] to "./platform" + + MHash implements + hash : a -> U64 where a implements MHash + + mulMHashes : MHash, MHash -> U64 + mulMHashes = \x, y -> hash x * hash y + + Id := U64 implements [MHash {hash: hashId}] + hashId = \@Id n -> n + + Three := {} implements [MHash {hash: hashThree}] + hashThree = \@Three _ -> 3 + + result = mulMHashes (@Id 100) (@Three {}) + "# + ), + @r###" + ── ABILITY USED AS TYPE ────────────────────────────────── /code/proj/Main.roc ─ + + You are attempting to use the ability `MHash` as a type directly: + + 6│ mulMHashes : MHash, MHash -> U64 + ^^^^^ + + Abilities can only be used in type annotations to constrain type + variables. + + Hint: Perhaps you meant to include an `implements` annotation, like + + a implements MHash + + ── ABILITY USED AS TYPE ────────────────────────────────── /code/proj/Main.roc ─ + + You are attempting to use the ability `MHash` as a type directly: + + 6│ mulMHashes : MHash, MHash -> U64 + ^^^^^ + + Abilities can only be used in type annotations to constrain type + variables. + + Hint: Perhaps you meant to include an `implements` annotation, like + + b implements MHash + "### + ); + + test_report!( + branches_have_more_cases_than_condition, + indoc!( + r#" + foo : Bool -> Str + foo = \bool -> + when bool is + True -> "true" + False -> "false" + Wat -> "surprise!" + foo + "# + ), + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + The branches of this `when` expression don't match the condition: + + 6│> when bool is + 7│ True -> "true" + 8│ False -> "false" + 9│ Wat -> "surprise!" + + This `bool` value is a: + + Bool + + But the branch patterns have type: + + [ + False, + True, + Wat, + ] + + The branches must be cases of the `when` condition's type! + "### + ); + + // from https://github.com/roc-lang/roc/commit/1372737f5e53ee5bb96d7e1b9593985e5537023a + // There was a bug where this reported UnusedArgument("val") + // since it was used only in the returned function only. + // + // we want this to not give any warnings/errors! + test_report!( + always_function, + indoc!( + r#" + always = \val -> \_ -> val + + always + "# + ), + @"" + ); + + test_report!( + imports_missing_comma, + indoc!( + r#" + app "test-missing-comma" + packages { pf: "platform/main.roc" } + imports [pf.Task Base64] + provides [main, @Foo] to pf + "# + ), + @r#" + ── WEIRD IMPORTS ────────────────────────── tmp/imports_missing_comma/Test.roc ─ + + I am partway through parsing a imports list, but I got stuck here: + + 2│ packages { pf: "platform/main.roc" } + 3│ imports [pf.Task Base64] + ^ + + I am expecting a comma or end of list, like + + imports [Shape, Vector]"# + ); + + test_report!( + not_enough_cases_for_open_union, + indoc!( + r#" + foo : [A, B]a -> Str + foo = \it -> + when it is + A -> "" + foo + "# + ), + @r#" + ── UNSAFE PATTERN ──────────────────────────────────────── /code/proj/Main.roc ─ + + This `when` does not cover all the possibilities: + + 6│> when it is + 7│> A -> "" + + Other possibilities include: + + B + _ + + I would have to crash if I saw one of those! Add branches for them! + "# + ); + + test_report!( + issue_2778_specialization_is_not_a_redundant_pattern, + indoc!( + r#" + formatColor = \color -> + when color is + Red -> "red" + Yellow -> "yellow" + _ -> "unknown" + + Red |> formatColor |> Str.concat (formatColor Orange) + "# + ), + @"" // no problem + ); + + test_report!( + nested_specialization, + indoc!( + r#" + app "test" provides [main] to "./platform" + + Default implements default : {} -> a where a implements Default + + main = + A := {} implements [Default {default}] + default = \{} -> @A {} + default {} + "# + ), + @r#" + ── SPECIALIZATION NOT ON TOP-LEVEL ─────────────────────── /code/proj/Main.roc ─ + + This specialization of the `default` ability member is in a nested + scope: + + 7│ default = \{} -> @A {} + ^^^^^^^ + + Specializations can only be defined on the top-level of a module. + "# + ); + + test_report!( + recursion_var_specialization_error, + indoc!( + r#" + Job a : [Job (List (Job a))] + + job : Job Str + + when job is + Job lst -> lst == "" + "# + ), + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + This 2nd argument to == has an unexpected type: + + 9│ Job lst -> lst == "" + ^^ + + The argument is a string of type: + + Str + + But == needs its 2nd argument to be: + + List [Job ∞] as ∞ + "### + ); + + test_report!( + #[ignore] + type_error_in_apply_is_circular, + indoc!( + r#" + app "test" imports [Set] provides [go] to "./platform" + + S a : { set : Set.Set a } + + go : a, S a -> Result (List a) * + go = \goal, model -> + if goal == goal + then Ok [] + else + new = { model & set : Set.remove goal model.set } + go goal new + "# + ), + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + This 1st argument to `remove` has an unexpected type: + + 10│ new = { model & set : Set.remove goal model.set } + ^^^^ + + This `goal` value is a: + + a + + But `remove` needs the 1st argument to be: + + Set k + + Tip: The type annotation uses the type variable `a` to say that this + definition can produce any type of value. But in the body I see that + it will only produce a `Set` value of a single specific type. Maybe + change the type annotation to be more specific? Maybe change the code + to be more general? + + ── CIRCULAR TYPE ───────────────────────────────────────── /code/proj/Main.roc ─ + + I'm inferring a weird self-referential type for `new`: + + 10│ new = { model & set : Set.remove goal model.set } + ^^^ + + Here is my best effort at writing down the type. You will see ∞ for + parts of the type that repeat something already printed out + infinitely. + + { set : Set ∞ } + + ── CIRCULAR TYPE ───────────────────────────────────────── /code/proj/Main.roc ─ + + I'm inferring a weird self-referential type for `goal`: + + 6│ go = \goal, model -> + ^^^^ + + Here is my best effort at writing down the type. You will see ∞ for + parts of the type that repeat something already printed out + infinitely. + + Set ∞ + "### + ); + + test_report!( + cycle_through_non_function, + indoc!( + r#" + force : ({} -> I64) -> I64 + force = \eval -> eval {} + + t1 = \_ -> force (\_ -> t2) + + t2 = t1 {} + + t2 + "# + ), + @r#" + ── CIRCULAR DEFINITION ─────────────────────────────────── /code/proj/Main.roc ─ + + The `t1` definition is causing a very tricky infinite loop: + + 7│ t1 = \_ -> force (\_ -> t2) + ^^ + + The `t1` value depends on itself through the following chain of + definitions: + + ┌─────┐ + │ t1 + │ ↓ + │ t2 + └─────┘ + "# + ); + + test_report!( + function_does_not_implement_encoding, + indoc!( + r#" + app "test" imports [] provides [main] to "./platform" + + main = Encode.toEncoder \x -> x + "# + ), + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + This expression has a type that does not implement the abilities it's expected to: + + 3│ main = Encode.toEncoder \x -> x + ^^^^^^^ + + I can't generate an implementation of the `Encoding` ability for + + a -> a + + Note: `Encoding` cannot be generated for functions. + "### + ); + + test_report!( + nested_opaque_does_not_implement_encoding, + indoc!( + r#" + app "test" imports [] provides [main] to "./platform" + + A := {} + main = Encode.toEncoder { x: @A {} } + "# + ), + // TODO: this error message is quite unfortunate. We should remove the duplication, and + // also support regions that point to things in other modules. See also https://github.com/roc-lang/roc/issues/3056. + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + This expression has a type that does not implement the abilities it's expected to: + + 4│ main = Encode.toEncoder { x: @A {} } + ^^^^^^^^^^^^ + + I can't generate an implementation of the `Encoding` ability for + + { x : A } + + In particular, an implementation for + + A + + cannot be generated. + + Tip: `A` does not implement `Encoding`. Consider adding a custom + implementation or `implements Encode.Encoding` to the definition of `A`. + "### + ); + + test_report!( + cycle_through_non_function_top_level, + indoc!( + r#" + app "test" provides [t2] to "./platform" + + force : ({} -> I64) -> I64 + force = \eval -> eval {} + + t1 = \_ -> force (\_ -> t2) + + t2 = t1 {} + "# + ), + @r#" + ── CIRCULAR DEFINITION ─────────────────────────────────── /code/proj/Main.roc ─ + + The `t1` definition is causing a very tricky infinite loop: + + 6│ t1 = \_ -> force (\_ -> t2) + ^^ + + The `t1` value depends on itself through the following chain of + definitions: + + ┌─────┐ + │ t1 + │ ↓ + │ t2 + └─────┘ + "# + ); + + test_report!( + opaque_ability_impl_not_found_shorthand_syntax, + indoc!( + r#" + app "test" provides [A] to "./platform" + + MEq implements eq : a, a -> U64 where a implements MEq + + A := U8 implements [MEq {eq}] + "# + ), + @r###" + ── IMPLEMENTATION NOT FOUND ────────────────────────────── /code/proj/Main.roc ─ + + An implementation of `eq` could not be found in this scope: + + 5│ A := U8 implements [MEq {eq}] + ^^ + + Tip: consider adding a value of name `eq` in this scope, or using + another variable that implements this ability member, like + { eq: myeq } + + ── INCOMPLETE ABILITY IMPLEMENTATION ───────────────────── /code/proj/Main.roc ─ + + This type does not fully implement the `MEq` ability: + + 5│ A := U8 implements [MEq {eq}] + ^^^^^^^^ + + The following necessary members are missing implementations: + + eq + "### + ); + + test_report!( + opaque_ability_impl_not_found, + indoc!( + r#" + app "test" provides [A, myMEq] to "./platform" + + MEq implements eq : a, a -> Bool where a implements MEq + + A := U8 implements [ MEq {eq: aMEq} ] + + myMEq = \m, n -> m == n + "# + ), + @r###" + ── UNRECOGNIZED NAME ───────────────────────────────────── /code/proj/Main.roc ─ + + Nothing is named `aMEq` in this scope. + + 5│ A := U8 implements [ MEq {eq: aMEq} ] + ^^^^ + + Did you mean one of these? + + MEq + Eq + myMEq + eq + + ── INCOMPLETE ABILITY IMPLEMENTATION ───────────────────── /code/proj/Main.roc ─ + + This type does not fully implement the `MEq` ability: + + 5│ A := U8 implements [ MEq {eq: aMEq} ] + ^^^^^^^^^^^^^^ + + The following necessary members are missing implementations: + + eq + "### + ); + + test_report!( + opaque_ability_impl_optional, + indoc!( + r#" + app "test" provides [A, myMEq] to "./platform" + + MEq implements eq : a, a -> Bool where a implements MEq + + A := U8 implements [ MEq {eq ? aMEq} ] + + myMEq = \m, n -> m == n + "# + ), + @r###" + ── OPTIONAL ABILITY IMPLEMENTATION ─────────────────────── /code/proj/Main.roc ─ + + Ability implementations cannot be optional: + + 5│ A := U8 implements [ MEq {eq ? aMEq} ] + ^^^^^^^^^ + + Custom implementations must be supplied fully. + + + + ── INCOMPLETE ABILITY IMPLEMENTATION ───────────────────── /code/proj/Main.roc ─ + + This type does not fully implement the `MEq` ability: + + 5│ A := U8 implements [ MEq {eq ? aMEq} ] + ^^^^^^^^^^^^^^^ + + The following necessary members are missing implementations: + + eq + "### + ); + + test_report!( + opaque_builtin_ability_impl_optional, + indoc!( + r#" + app "test" + imports [] + provides [A, myEncoder] to "./platform" + + A := U8 implements [ Encoding {toEncoder ? myEncoder} ] + + myEncoder = 1 + "# + ), + @r###" + ── OPTIONAL ABILITY IMPLEMENTATION ─────────────────────── /code/proj/Main.roc ─ + + Ability implementations cannot be optional: + + 5│ A := U8 implements [ Encoding {toEncoder ? myEncoder} ] + ^^^^^^^^^^^^^^^^^^^^^ + + Custom implementations must be supplied fully. + + Hint: if you want this implementation to be derived, don't include a + record of implementations. For example, implements [Encoding] will + attempt to derive `Encoding` + + ── INCOMPLETE ABILITY IMPLEMENTATION ───────────────────── /code/proj/Main.roc ─ + + This type does not fully implement the `Encoding` ability: + + 5│ A := U8 implements [ Encoding {toEncoder ? myEncoder} ] + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + The following necessary members are missing implementations: + + toEncoder + "### + ); + + test_report!( + opaque_ability_impl_qualified, + indoc!( + r#" + app "test" provides [A] to "./platform" + + MEq implements eq : a, a -> Bool where a implements MEq + + A := U8 implements [ MEq {eq : Bool.eq} ] + "# + ), + @r###" + ── QUALIFIED ABILITY IMPLEMENTATION ────────────────────── /code/proj/Main.roc ─ + + This ability implementation is qualified: + + 5│ A := U8 implements [ MEq {eq : Bool.eq} ] + ^^^^^^^ + + Custom implementations must be defined in the local scope, and + unqualified. + + ── INCOMPLETE ABILITY IMPLEMENTATION ───────────────────── /code/proj/Main.roc ─ + + This type does not fully implement the `MEq` ability: + + 5│ A := U8 implements [ MEq {eq : Bool.eq} ] + ^^^^^^^^^^^^^^^^^^ + + The following necessary members are missing implementations: + + eq + "### + ); + + test_report!( + opaque_ability_impl_not_identifier, + indoc!( + r#" + app "test" provides [A] to "./platform" + + MEq implements eq : a, a -> Bool where a implements MEq + + A := U8 implements [ MEq {eq : \m, n -> m == n} ] + "# + ), + @r###" + ── ABILITY IMPLEMENTATION NOT IDENTIFIER ───────────────── /code/proj/Main.roc ─ + + This ability implementation is not an identifier: + + 5│ A := U8 implements [ MEq {eq : \m, n -> m == n} ] + ^^^^^^^^^^^^^^^ + + Custom ability implementations defined in this position can only be + unqualified identifiers, not arbitrary expressions. + + Tip: consider defining this expression as a variable. + + ── INCOMPLETE ABILITY IMPLEMENTATION ───────────────────── /code/proj/Main.roc ─ + + This type does not fully implement the `MEq` ability: + + 5│ A := U8 implements [ MEq {eq : \m, n -> m == n} ] + ^^^^^^^^^^^^^^^^^^^^^^^^^^ + + The following necessary members are missing implementations: + + eq + "### + ); + + test_report!( + opaque_ability_impl_duplicate, + indoc!( + r#" + app "test" provides [A] to "./platform" + + MEq implements eq : a, a -> Bool where a implements MEq + + A := U8 implements [ MEq {eq: eqA, eq: eqA} ] + + eqA = \@A m, @A n -> m == n + "# + ), + @r###" + ── DUPLICATE IMPLEMENTATION ────────────────────────────── /code/proj/Main.roc ─ + + This ability member implementation is duplicate: + + 5│ A := U8 implements [ MEq {eq: eqA, eq: eqA} ] + ^^^^^^^ + + The first implementation was defined here: + + 5│ A := U8 implements [ MEq {eq: eqA, eq: eqA} ] + ^^^^^^^ + + Only one custom implementation can be defined for an ability member. + "### + ); + + test_report!( + implements_type_not_ability, + indoc!( + r#" + app "test" provides [A, Foo] to "./platform" + + Foo := {} + + A := U8 implements [ Foo {} ] + "# + ), + @r###" + ── NOT AN ABILITY ──────────────────────────────────────── /code/proj/Main.roc ─ + + This identifier is not an ability in scope: + + 5│ A := U8 implements [ Foo {} ] + ^^^ + + Only abilities can be implemented. + "### + ); + + test_report!( + derive_non_builtin_ability, + indoc!( + r#" + app "test" provides [A] to "./platform" + + Ab implements ab : a -> a where a implements Ab + + A := {} implements [Ab] + "# + ), + @r###" + ── ILLEGAL DERIVE ──────────────────────────────────────── /code/proj/Main.roc ─ + + This ability cannot be derived: + + 5│ A := {} implements [Ab] + ^^ + + Only builtin abilities can be derived. + + Note: The builtin abilities are `Encoding`, `Decoding`, `Hash`, `Eq`, `Inspect` + "### + ); + + test_report!( + has_encoding_for_function, + indoc!( + r#" + app "test" imports [] provides [A] to "./platform" + + A a := a -> a implements [Encode.Encoding] + "# + ), + @r###" + ── INCOMPLETE ABILITY IMPLEMENTATION ───────────────────── /code/proj/Main.roc ─ + + I can't derive an implementation of the `Encoding` ability for `A`: + + 3│ A a := a -> a implements [Encode.Encoding] + ^^^^^^^^^^^^^^^ + + Note: `Encoding` cannot be generated for functions. + + Tip: You can define a custom implementation of `Encoding` for `A`. + "### + ); + + test_report!( + has_encoding_for_non_encoding_alias, + indoc!( + r#" + app "test" imports [] provides [A] to "./platform" + + A := B implements [Encode.Encoding] + + B := {} + "# + ), + @r###" + ── INCOMPLETE ABILITY IMPLEMENTATION ───────────────────── /code/proj/Main.roc ─ + + I can't derive an implementation of the `Encoding` ability for `A`: + + 3│ A := B implements [Encode.Encoding] + ^^^^^^^^^^^^^^^ + + Tip: `B` does not implement `Encoding`. Consider adding a custom + implementation or `implements Encode.Encoding` to the definition of `B`. + + Tip: You can define a custom implementation of `Encoding` for `A`. + "### + ); + + test_report!( + has_encoding_for_other_has_encoding, + indoc!( + r#" + app "test" imports [] provides [A] to "./platform" + + A := B implements [Encode.Encoding] + + B := {} implements [Encode.Encoding] + "# + ), + @"" // no error + ); + + test_report!( + has_encoding_for_recursive_deriving, + indoc!( + r#" + app "test" imports [] provides [MyNat] to "./platform" + + MyNat := [S MyNat, Z] implements [Encode.Encoding] + "# + ), + @"" // no error + ); + + test_report!( + shadowing_top_level_scope, + indoc!( + r#" + app "test" provides [ main ] to "./platform" + + main = 1 + + main = \n -> n + 2 + "# + ), + @r###" + ── DUPLICATE NAME ──────────────────────────────────────── /code/proj/Main.roc ─ + + The `main` name is first defined here: + + 3│ main = 1 + ^^^^ + + But then it's defined a second time here: + + 5│ main = \n -> n + 2 + ^^^^ + + Since these variables have the same name, it's easy to use the wrong + one by accident. Give one of them a new name. + + ── UNNECESSARY DEFINITION ──────────────────────────────── /code/proj/Main.roc ─ + + This destructure assignment doesn't introduce any new variables: + + 5│ main = \n -> n + 2 + ^^^^ + + If you don't need to use the value on the right-hand-side of this + assignment, consider removing the assignment. Since Roc is purely + functional, assignments that don't introduce variables cannot affect a + program's behavior! + "### + ); + + test_report!( + issue_1755, + indoc!( + r#" + Handle := {} + + await : Result a err, (a -> Result b err) -> Result b err + open : {} -> Result Handle * + close : Handle -> Result {} * + + withOpen : (Handle -> Result {} *) -> Result {} * + withOpen = \callback -> + handle <- await (open {}) + {} <- await (callback handle) + close handle + + withOpen + "# + ), + @r#" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + Something is off with the body of the `withOpen` definition: + + 10│ withOpen : (Handle -> Result {} *) -> Result {} * + 11│ withOpen = \callback -> + 12│> handle <- await (open {}) + 13│> {} <- await (callback handle) + 14│> close handle + + The type annotation on `withOpen` says this `await` call should have the + type: + + Result {} * + + However, the type of this `await` call is connected to another type in a + way that isn't reflected in this annotation. + + Tip: Any connection between types must use a named type variable, not + a `*`! Maybe the annotation on `withOpen` should have a named type + variable in place of the `*`? + "# + ); + + test_report!( + recursive_body_and_annotation_with_inference_disagree, + indoc!( + r#" + f : _ -> (_ -> Str) + f = \_ -> if Bool.true then {} else f {} + + f + "# + ), + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + This expression is used in an unexpected way: + + 5│ f = \_ -> if Bool.true then {} else f {} + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + It is a value of type: + + {} + + But you are trying to use it as: + + * -> Str + "### + ); + + test_report!( + same_phantom_types_unify, + indoc!( + r#" + F a b := b + + foo : F Str Str -> {} + + x : F Str Str + + foo x + "# + ), + @r"" // okay + ); + + test_report!( + different_phantom_types, + indoc!( + r#" + F a b := b + + foo : F Str Str -> {} + + x : F U8 Str + + foo x + "# + ), + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + This 1st argument to `foo` has an unexpected type: + + 10│ foo x + ^ + + This `x` value is a: + + F U8 Str + + But `foo` needs its 1st argument to be: + + F Str Str + "### + ); + + test_report!( + #[ignore = "TODO This should be a type error"] + phantom_type_bound_to_ability_not_implementing, + indoc!( + r#" + app "test" provides [x] to "./platform" + + Foo implements foo : a -> a where a implements Foo + + F a b := b where a implements Foo + + MHash := {} + + x : F MHash {} + "# + ), + @r###" + "### + ); + + test_report!( + int_literals_cannot_fit_in_same_type, + indoc!( + r#" + 0x80000000000000000000000000000000 == -0x80000000000000000000000000000000 + "# + ), + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + This 2nd argument to == has an unexpected type: + + 4│ 0x80000000000000000000000000000000 == -0x80000000000000000000000000000000 + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + The argument is an integer of type: + + I128 + + But == needs its 2nd argument to be: + + U128 + "### + ); + + test_report!( + num_literals_cannot_fit_in_same_type, + indoc!( + r#" + 170141183460469231731687303715884105728 == -170141183460469231731687303715884105728 + "# + ), + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + This 2nd argument to == has an unexpected type: + + 4│ 170141183460469231731687303715884105728 == -170141183460469231731687303715884105728 + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + The argument is a number of type: + + I128 or Dec + + But == needs its 2nd argument to be: + + U128 + "### + ); + + test_report!( + recursive_alias_cannot_leak_into_recursive_opaque, + indoc!( + r#" + OList := [Nil, Cons {} OList] + + AList : [Nil, Cons {} AList] + + alist : AList + + olist : OList + olist = + when alist is + Nil -> @OList Nil + Cons _ lst -> lst + + olist + "# + ), + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + Something is off with the 2nd branch of this `when` expression: + + 10│ olist : OList + 11│ olist = + 12│> when alist is + 13│> Nil -> @OList Nil + 14│> Cons _ lst -> lst + + This `lst` value is a: + + [ + Cons {} ∞, + Nil, + ] as ∞ + + But the type annotation on `olist` says it should be: + + OList + + Tip: Type comparisons between an opaque type are only ever equal if + both types are the same opaque type. Did you mean to create an opaque + type by wrapping it? If I have an opaque type Age := U32 I can create + an instance of this opaque type by doing @Age 23. + "### + ); + + test_report!( + opaque_wrap_function_mismatch, + indoc!( + r#" + A := U8 + List.map [1u16, 2u16, 3u16] @A + "# + ), + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + This 2nd argument to `map` has an unexpected type: + + 5│ List.map [1u16, 2u16, 3u16] @A + ^^ + + This A opaque wrapping has the type: + + U8 -> A + + But `map` needs its 2nd argument to be: + + U16 -> A + "### + ); + + test_report!( + symbols_not_bound_in_all_patterns, + indoc!( + r#" + when A "" is + A x | B y -> x + "# + ), + @r###" + ── NAME NOT BOUND IN ALL PATTERNS ──────────────────────── /code/proj/Main.roc ─ + + `x` is not bound in all patterns of this `when` branch + + 5│ A x | B y -> x + ^ + + Identifiers introduced in a `when` branch must be bound in all patterns + of the branch. Otherwise, the program would crash when it tries to use + an identifier that wasn't bound! + + ── NAME NOT BOUND IN ALL PATTERNS ──────────────────────── /code/proj/Main.roc ─ + + `y` is not bound in all patterns of this `when` branch + + 5│ A x | B y -> x + ^ + + Identifiers introduced in a `when` branch must be bound in all patterns + of the branch. Otherwise, the program would crash when it tries to use + an identifier that wasn't bound! + + ── UNUSED DEFINITION ───────────────────────────────────── /code/proj/Main.roc ─ + + `y` is not used in this `when` branch. + + 5│ A x | B y -> x + ^ + + If you don't need to use `y`, prefix it with an underscore, like "_y", + or replace it with just an "_". + "### + ); + + test_report!( + flip_flop_catch_all_branches_not_exhaustive, + indoc!( + r#" + \x -> when x is + A B _ -> "" + A _ C -> "" + "# + ), + @r###" + ── UNSAFE PATTERN ──────────────────────────────────────── /code/proj/Main.roc ─ + + This `when` does not cover all the possibilities: + + 4│> \x -> when x is + 5│> A B _ -> "" + 6│> A _ C -> "" + + Other possibilities include: + + A _ _ + + I would have to crash if I saw one of those! Add branches for them! + "### + ); + + test_report!( + forgot_to_remove_underscore, + indoc!( + r#" + \_foo -> foo + "# + ), + |golden| pretty_assertions::assert_eq!( + golden, + indoc!( + r###"── UNRECOGNIZED NAME ───────────────────────────────────── /code/proj/Main.roc ─ + + Nothing is named `foo` in this scope. + + 4│ \_foo -> foo + ^^^ + + There is an ignored identifier of a similar name here: + + 4│ \_foo -> foo + ^^^^ + + Did you mean to remove the leading underscore? + + If not, did you mean one of these? + + Box + Bool + U8 + F64 + "### + ), + ) + ); + + test_report!( + call_with_underscore_identifier, + indoc!( + r#" + f = \x, y, z -> x + y + z + + f 1 _ 1 + "# + ), + |golden| pretty_assertions::assert_eq!( + golden, + indoc!( + r###"── SYNTAX PROBLEM ──────────────────────────────────────── /code/proj/Main.roc ─ + + An underscore is being used as a variable here: + + 6│ f 1 _ 1 + ^ + + An underscore can be used to ignore a value when pattern matching, but + it cannot be used as a variable. + "### + ), + ) + ); + + test_report!( + call_with_declared_identifier_starting_with_underscore, + indoc!( + r#" + f = \x, y, z -> x + y + z + + \a, _b -> f a _b 1 + "# + ), + |golden| pretty_assertions::assert_eq!( + golden, + indoc!( + r###"── SYNTAX PROBLEM ──────────────────────────────────────── /code/proj/Main.roc ─ + + This variable's name starts with an underscore: + + 6│ \a, _b -> f a _b 1 + ^^ + + But then it is used here: + + 6│ \a, _b -> f a _b 1 + ^^ + + A variable's name can only start with an underscore if the variable is + unused. Since you are using this variable, you could remove the + underscore from its name in both places. + "### + ), + ) + ); + + test_report!( + call_with_undeclared_identifier_starting_with_underscore, + indoc!( + r#" + f = \x, y, z -> x + y + z + + \a, _b -> f a _r 1 + "# + ), + |golden| pretty_assertions::assert_eq!( + golden, + indoc!( + r###" + ── SYNTAX PROBLEM ──────────────────────────────────────── /code/proj/Main.roc ─ + + This variable's name starts with an underscore: + + 6│ \a, _b -> f a _r 1 + ^^ + + A variable's name can only start with an underscore if the variable is + unused. But it looks like the variable is being used here! + "### + ), + ) + ); + + test_report!( + underscore_in_middle_of_identifier, + indoc!( + r#" + f = \x, y, z -> x + y + z + + \a, _b -> f a var_name 1 + "# + ), + |golden| pretty_assertions::assert_eq!( + golden, + indoc!( + r###" + ── SYNTAX PROBLEM ──────────────────────────────────────── /code/proj/Main.roc ─ + + Underscores are not allowed in identifier names: + + 6│ \a, _b -> f a var_name 1 + ^^^^^^^^ + + I recommend using camelCase. It's the standard style in Roc code! + "### + ), + ) + ); + + // Record Builders + + test_report!( + optional_field_in_record_builder, + indoc!( + r#" + { + a: <- apply "a", + b, + c ? "optional" + } + "# + ), + @r###" + ── BAD RECORD BUILDER ────────── tmp/optional_field_in_record_builder/Test.roc ─ + + I am partway through parsing a record builder, and I found an optional + field: + + 1│ app "test" provides [main] to "./platform" + 2│ + 3│ main = + 4│ { + 5│ a: <- apply "a", + 6│ b, + 7│ c ? "optional" + ^^^^^^^^^^^^^^ + + Optional fields can only appear when you destructure a record. + "### + ); + + test_report!( + record_update_builder, + indoc!( + r#" + { rec & + a: <- apply "a", + b: 3 + } + "# + ), + @r###" + ── BAD RECORD UPDATE ────────────────────── tmp/record_update_builder/Test.roc ─ + + I am partway through parsing a record update, and I found a record + builder field: + + 1│ app "test" provides [main] to "./platform" + 2│ + 3│ main = + 4│ { rec & + 5│ a: <- apply "a", + ^^^^^^^^^^^^^^^ + + Record builders cannot be updated like records. + "### + ); + + test_report!( + multiple_record_builders, + indoc!( + r#" + succeed + { a: <- apply "a" } + { b: <- apply "b" } + "# + ), + @r###" + ── MULTIPLE RECORD BUILDERS ────────────────────────────── /code/proj/Main.roc ─ + + This function is applied to multiple record builders: + + 4│> succeed + 5│> { a: <- apply "a" } + 6│> { b: <- apply "b" } + + Note: Functions can only take at most one record builder! + + Tip: You can combine them or apply them separately. + + "### + ); + + test_report!( + unapplied_record_builder, + indoc!( + r#" + { a: <- apply "a" } + "# + ), + @r###" + ── UNAPPLIED RECORD BUILDER ────────────────────────────── /code/proj/Main.roc ─ + + This record builder was not applied to a function: + + 4│ { a: <- apply "a" } + ^^^^^^^^^^^^^^^^^^^ + + However, we need a function to construct the record. + + Note: Functions must be applied directly. The pipe operator (|>) cannot be used. + "### + ); + + test_report!( + record_builder_apply_non_function, + indoc!( + r#" + succeed = \_ -> crash "" + + succeed { + a: <- "a", + } + "# + ), + @r###" + ── TOO MANY ARGS ───────────────────────────────────────── /code/proj/Main.roc ─ + + This value is not a function, but it was given 1 argument: + + 7│ a: <- "a", + ^^^ + + Tip: Remove `<-` to assign the field directly. + "### + ); + + // Skipping test because opaque types defined in the same module + // do not fail with the special opaque type error + // + // test_report!( + // record_builder_apply_opaque, + // indoc!( + // r#" + // succeed = \_ -> crash "" + + // Decode := {} + + // get : Str -> Decode + // get = \_ -> @Decode {} + + // succeed { + // a: <- get "a", + // # missing |> apply ^ + // } + // "# + // ), + // @r###" + // ── TOO MANY ARGS ───────────────────────────────────────── /code/proj/Main.roc ─ + + // This value is an opaque type, so it cannot be called with an argument: + + // 12│ a: <- get "a", + // ^^^^^^^ + + // Hint: Did you mean to apply it to a function first? + // "### + // ); + + test_report!( + destructure_assignment_introduces_no_variables_nested, + indoc!( + r#" + Pair _ _ = Pair 0 1 + + _ = Pair 0 1 + + {} = {} + + Foo = Foo + + 0 + "# + ), + @r###" + ── UNNECESSARY DEFINITION ──────────────────────────────── /code/proj/Main.roc ─ + + This destructure assignment doesn't introduce any new variables: + + 4│ Pair _ _ = Pair 0 1 + ^^^^ + + If you don't need to use the value on the right-hand-side of this + assignment, consider removing the assignment. Since Roc is purely + functional, assignments that don't introduce variables cannot affect a + program's behavior! + + ── UNNECESSARY DEFINITION ──────────────────────────────── /code/proj/Main.roc ─ + + This destructure assignment doesn't introduce any new variables: + + 6│ _ = Pair 0 1 + ^ + + If you don't need to use the value on the right-hand-side of this + assignment, consider removing the assignment. Since Roc is purely + functional, assignments that don't introduce variables cannot affect a + program's behavior! + + ── UNNECESSARY DEFINITION ──────────────────────────────── /code/proj/Main.roc ─ + + This destructure assignment doesn't introduce any new variables: + + 8│ {} = {} + ^^ + + If you don't need to use the value on the right-hand-side of this + assignment, consider removing the assignment. Since Roc is purely + functional, assignments that don't introduce variables cannot affect a + program's behavior! + + ── UNNECESSARY DEFINITION ──────────────────────────────── /code/proj/Main.roc ─ + + This destructure assignment doesn't introduce any new variables: + + 10│ Foo = Foo + ^^^ + + If you don't need to use the value on the right-hand-side of this + assignment, consider removing the assignment. Since Roc is purely + functional, assignments that don't introduce variables cannot affect a + program's behavior! + "### + ); + + test_report!( + destructure_assignment_introduces_no_variables_nested_toplevel, + indoc!( + r#" + app "test" provides [] to "./platform" + + Pair _ _ = Pair 0 1 + + _ = Pair 0 1 + + {} = {} + + Foo = Foo + "# + ), + @r###" + ── UNNECESSARY DEFINITION ──────────────────────────────── /code/proj/Main.roc ─ + + This destructure assignment doesn't introduce any new variables: + + 3│ Pair _ _ = Pair 0 1 + ^^^^^^^^ + + If you don't need to use the value on the right-hand-side of this + assignment, consider removing the assignment. Since Roc is purely + functional, assignments that don't introduce variables cannot affect a + program's behavior! + + ── UNNECESSARY DEFINITION ──────────────────────────────── /code/proj/Main.roc ─ + + This destructure assignment doesn't introduce any new variables: + + 5│ _ = Pair 0 1 + ^ + + If you don't need to use the value on the right-hand-side of this + assignment, consider removing the assignment. Since Roc is purely + functional, assignments that don't introduce variables cannot affect a + program's behavior! + + ── UNNECESSARY DEFINITION ──────────────────────────────── /code/proj/Main.roc ─ + + This destructure assignment doesn't introduce any new variables: + + 7│ {} = {} + ^^ + + If you don't need to use the value on the right-hand-side of this + assignment, consider removing the assignment. Since Roc is purely + functional, assignments that don't introduce variables cannot affect a + program's behavior! + + ── UNNECESSARY DEFINITION ──────────────────────────────── /code/proj/Main.roc ─ + + This destructure assignment doesn't introduce any new variables: + + 9│ Foo = Foo + ^^^ + + If you don't need to use the value on the right-hand-side of this + assignment, consider removing the assignment. Since Roc is purely + functional, assignments that don't introduce variables cannot affect a + program's behavior! + "### + ); + + test_report!( + unused_shadow_specialization, + indoc!( + r#" + app "test" provides [hash, Id] to "./platform" + + MHash implements hash : a -> U64 where a implements MHash + + Id := {} + + hash = \@Id _ -> 0 + "# + ), + @r###" + ── UNUSED DEFINITION ───────────────────────────────────── /code/proj/Main.roc ─ + + `hash` is not used anywhere in your code. + + 7│ hash = \@Id _ -> 0 + ^^^^ + + If you didn't intend on using `hash` then remove it so future readers of + your code don't wonder why it is there. + "### + ); + + test_report!( + specialization_for_wrong_type, + indoc!( + r#" + app "test" provides [hash, Id, Id2] to "./platform" + + MHash implements hash : a -> U64 where a implements MHash + + Id := {} implements [MHash {hash}] + Id2 := {} + + hash = \@Id2 _ -> 0 + "# + ), + @r###" + ── WRONG SPECIALIZATION TYPE ───────────────────────────── /code/proj/Main.roc ─ + + This specialization of `hash` is not for the expected type: + + 8│ hash = \@Id2 _ -> 0 + ^^^^ + + It was previously claimed to be a specialization for `Id`, but was + determined to actually specialize `Id2`! + "### + ); + + test_report!( + mismatched_record_annotation, + indoc!( + r#" + x : { y : Str } + x = {} + + x + "# + ), + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + Something is off with the body of the `x` definition: + + 4│ x : { y : Str } + 5│ x = {} + ^^ + + The body is a record of type: + + {} + + But the type annotation on `x` says it should be: + + { y : Str } + + Tip: Looks like the y field is missing. + "### + ); + + test_report!( + cyclic_opaque, + indoc!( + r#" + Recursive := [Infinitely Recursive] + + 0 + "# + ), + @r###" + ── CYCLIC ALIAS ────────────────────────────────────────── /code/proj/Main.roc ─ + + The `Recursive` opaque is self-recursive in an invalid way: + + 4│ Recursive := [Infinitely Recursive] + ^^^^^^^^^ + + Recursion in opaquees is only allowed if recursion happens behind a + tagged union, at least one variant of which is not recursive. + "### + ); + + test_report!( + derive_decoding_for_function, + indoc!( + r#" + app "test" imports [] provides [A] to "./platform" + + A a := a -> a implements [Decode.Decoding] + "# + ), + @r###" + ── INCOMPLETE ABILITY IMPLEMENTATION ───────────────────── /code/proj/Main.roc ─ + + I can't derive an implementation of the `Decoding` ability for `A`: + + 3│ A a := a -> a implements [Decode.Decoding] + ^^^^^^^^^^^^^^^ + + Note: `Decoding` cannot be generated for functions. + + Tip: You can define a custom implementation of `Decoding` for `A`. + "### + ); + + test_report!( + derive_decoding_for_non_decoding_opaque, + indoc!( + r#" + app "test" imports [] provides [A] to "./platform" + + A := B implements [Decode.Decoding] + + B := {} + "# + ), + @r###" + ── INCOMPLETE ABILITY IMPLEMENTATION ───────────────────── /code/proj/Main.roc ─ + + I can't derive an implementation of the `Decoding` ability for `A`: + + 3│ A := B implements [Decode.Decoding] + ^^^^^^^^^^^^^^^ + + Tip: `B` does not implement `Decoding`. Consider adding a custom + implementation or `implements Decode.Decoding` to the definition of `B`. + + Tip: You can define a custom implementation of `Decoding` for `A`. + + "### + ); + + test_report!( + derive_decoding_for_other_has_decoding, + indoc!( + r#" + app "test" imports [] provides [A] to "./platform" + + A := B implements [Decode.Decoding] + + B := {} implements [Decode.Decoding] + "# + ), + @"" // no error + ); + + test_report!( + derive_decoding_for_recursive_deriving, + indoc!( + r#" + app "test" imports [] provides [MyNat] to "./platform" + + MyNat := [S MyNat, Z] implements [Decode.Decoding] + "# + ), + @"" // no error + ); + + test_report!( + function_cannot_derive_encoding, + indoc!( + r#" + app "test" imports [Decode.{decoder}] provides [main] to "./platform" + + main = + myDecoder : Decoder (a -> a) fmt where fmt implements DecoderFormatting + myDecoder = decoder + + myDecoder + "# + ), + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + This expression has a type that does not implement the abilities it's expected to: + + 5│ myDecoder = decoder + ^^^^^^^ + + I can't generate an implementation of the `Decoding` ability for + + a -> a + + Note: `Decoding` cannot be generated for functions. + "### + ); + + test_report!( + nested_opaque_cannot_derive_encoding, + indoc!( + r#" + app "test" imports [Decode.{decoder}] provides [main] to "./platform" + + A := {} + + main = + myDecoder : Decoder {x : A} fmt where fmt implements DecoderFormatting + myDecoder = decoder + + myDecoder + "# + ), + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + This expression has a type that does not implement the abilities it's expected to: + + 7│ myDecoder = decoder + ^^^^^^^ + + I can't generate an implementation of the `Decoding` ability for + + { x : A } + + In particular, an implementation for + + A + + cannot be generated. + + Tip: `A` does not implement `Decoding`. Consider adding a custom + implementation or `implements Decode.Decoding` to the definition of `A`. + "### + ); + + test_report!( + anonymous_function_does_not_use_param, + indoc!( + r#" + (\x -> 5) 1 + "# + ), + @r###" + ── UNUSED ARGUMENT ─────────────────────────────────────── /code/proj/Main.roc ─ + + This function doesn't use `x`. + + 4│ (\x -> 5) 1 + ^ + + If you don't need `x`, then you can just remove it. However, if you + really do need `x` as an argument of this function, prefix it with an + underscore, like this: "_`x`". Adding an underscore at the start of a + variable name is a way of saying that the variable is not used. + "### + ); + + test_report!( + expected_tag_has_too_many_args, + indoc!( + r#" + app "test" provides [fromBytes] to "./platform" + + u8 : [Good (List U8), Bad [DecodeProblem]] + + fromBytes = + when u8 is + Good _ _ -> + Ok "foo" + + Bad _ -> + Ok "foo" + "# + ), + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + The branches of this `when` expression don't match the condition: + + 6│> when u8 is + 7│ Good _ _ -> + 8│ Ok "foo" + 9│ + 10│ Bad _ -> + 11│ Ok "foo" + + This `u8` value is a: + + [Good …, …] + + But the branch patterns have type: + + [Good … *, …] + + The branches must be cases of the `when` condition's type! + "### + ); + + test_report!( + create_value_with_optional_record_field_type, + indoc!( + r#" + f : {a: Str, b ? Str} + f = {a: "b", b: ""} + f + "# + ), + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + Something is off with the body of the `f` definition: + + 4│ f : {a: Str, b ? Str} + 5│ f = {a: "b", b: ""} + ^^^^^^^^^^^^^^^ + + The body is a record of type: + + { + a : Str, + b : Str, + } + + But the type annotation on `f` says it should be: + + { + a : Str, + b ? Str, + } + + Tip: To extract the `.b` field it must be non-optional, but the type + says this field is optional. Learn more about optional fields at TODO. + "### + ); + + test_report!( + create_value_with_conditionally_optional_record_field_type, + indoc!( + r#" + f : {a: Str, b ? Str} + f = if Bool.true then {a: ""} else {a: "b", b: ""} + f + "# + ), + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + Something is off with the `then` branch of this `if` expression: + + 4│ f : {a: Str, b ? Str} + 5│ f = if Bool.true then {a: ""} else {a: "b", b: ""} + ^^^^^^^ + + This branch is a record of type: + + { a : Str } + + But the type annotation on `f` says it should be: + + { + a : Str, + b ? Str, + } + + Tip: Looks like the b field is missing. + "### + ); + + test_report!( + unused_def_in_branch_pattern, + indoc!( + r#" + when A "" is + A foo -> "" + "# + ), + @r###" + ── UNUSED DEFINITION ───────────────────────────────────── /code/proj/Main.roc ─ + + `foo` is not used in this `when` branch. + + 5│ A foo -> "" + ^^^ + + If you don't need to use `foo`, prefix it with an underscore, like + "_foo", or replace it with just an "_". + "### + ); + + test_report!( + infer_decoded_record_error_with_function_field, + indoc!( + r#" + app "test" imports [TotallyNotJson] provides [main] to "./platform" + + main = + decoded = Str.toUtf8 "{\"first\":\"ab\",\"second\":\"cd\"}" |> Decode.fromBytes TotallyNotJson.json + when decoded is + Ok rcd -> rcd.first rcd.second + _ -> "something went wrong" + "# + ), + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + This expression has a type that does not implement the abilities it's expected to: + + 6│ Ok rcd -> rcd.first rcd.second + ^^^^^^^^^ + + I can't generate an implementation of the `Decoding` ability for + + * -> * + + Note: `Decoding` cannot be generated for functions. + "### + ); + + test_report!( + record_with_optional_field_types_cannot_derive_decoding, + indoc!( + r#" + app "test" imports [Decode.{decoder}] provides [main] to "./platform" + + main = + myDecoder : Decoder {x : Str, y ? Str} fmt where fmt implements DecoderFormatting + myDecoder = decoder + + myDecoder + "# + ), + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + This expression has a type that does not implement the abilities it's expected to: + + 5│ myDecoder = decoder + ^^^^^^^ + + I can't generate an implementation of the `Decoding` ability for + + { + x : Str, + y ? Str, + } + + Note: I can't derive decoding for a record with an optional field, + which in this case is `.y`. Optional record fields are polymorphic over + records that may or may not contain them at compile time, but are not + a concept that extends to runtime! + Maybe you wanted to use a `Result`? + "### + ); + + test_report!( + uninhabited_type_is_trivially_exhaustive, + indoc!( + r#" + x : Result {} [] + + when x is + Ok {} -> "" + "# + ), + // no problem! + @r###" + "### + ); + + test_report!( + uninhabited_type_is_trivially_exhaustive_nested, + indoc!( + r#" + x : Result (Result [A, B] []) [] + + when x is + Ok (Ok A) -> "" + Ok (Ok B) -> "" + "# + ), + // no problem! + @r###" + "### + ); + + test_report!( + branch_patterns_missing_nested_case, + indoc!( + r#" + x : Result (Result [A, B] {}) {} + + when x is + Ok (Ok A) -> "" + Ok (Err _) -> "" + Err _ -> "" + "# + ), + @r###" + ── UNSAFE PATTERN ──────────────────────────────────────── /code/proj/Main.roc ─ + + This `when` does not cover all the possibilities: + + 6│> when x is + 7│> Ok (Ok A) -> "" + 8│> Ok (Err _) -> "" + 9│> Err _ -> "" + + Other possibilities include: + + Ok (Ok B) + + I would have to crash if I saw one of those! Add branches for them! + "### + ); + + test_report!( + branch_patterns_missing_nested_case_with_trivially_exhausted_variant, + indoc!( + r#" + x : Result (Result [A, B] []) [] + + when x is + Ok (Ok A) -> "" + "# + ), + @r###" + ── UNSAFE PATTERN ──────────────────────────────────────── /code/proj/Main.roc ─ + + This `when` does not cover all the possibilities: + + 6│> when x is + 7│> Ok (Ok A) -> "" + + Other possibilities include: + + Ok (Ok B) + + I would have to crash if I saw one of those! Add branches for them! + "### + ); + + test_report!( + uninhabited_err_branch_is_redundant_when_err_is_matched, + indoc!( + r#" + x : Result {} [] + + when x is + Ok {} -> "" + Err _ -> "" + "# + ), + @r###" + ── UNMATCHABLE PATTERN ─────────────────────────────────── /code/proj/Main.roc ─ + + The 2nd pattern will never be matched: + + 6│ when x is + 7│ Ok {} -> "" + 8│ Err _ -> "" + ^^^^^ + + It's impossible to create a value of this shape, so this pattern can + be safely removed! + "### + ); + + test_report!( + uninhabited_err_branch_is_redundant_when_err_is_matched_nested, + indoc!( + r#" + x : Result (Result {} []) [] + + when x is + Ok (Ok {}) -> "" + Ok (Err _) -> "" + Err _ -> "" + "# + ), + @r###" + ── UNMATCHABLE PATTERN ─────────────────────────────────── /code/proj/Main.roc ─ + + The 2nd pattern will never be matched: + + 6│ when x is + 7│ Ok (Ok {}) -> "" + 8│> Ok (Err _) -> "" + 9│ Err _ -> "" + + It's impossible to create a value of this shape, so this pattern can + be safely removed! + + ── UNMATCHABLE PATTERN ─────────────────────────────────── /code/proj/Main.roc ─ + + The 3rd pattern will never be matched: + + 6│ when x is + 7│ Ok (Ok {}) -> "" + 8│ Ok (Err _) -> "" + 9│ Err _ -> "" + ^^^^^ + + It's impossible to create a value of this shape, so this pattern can + be safely removed! + "### + ); + + test_report!( + custom_type_conflicts_with_builtin, + indoc!( + r#" + Nat := [ S Nat, Z ] + + "" + "# + ), + @r###" + ── DUPLICATE NAME ──────────────────────────────────────── /code/proj/Main.roc ─ + + This opaque type has the same name as a builtin: + + 4│ Nat := [ S Nat, Z ] + ^^^^^^^^^^^^^^^^^^^ + + All builtin opaque types are in scope by default, so I need this + opaque type to have a different name! + "### + ); + + test_report!( + unused_value_import, + indoc!( + r#" + app "test" imports [List.{ concat }] provides [main] to "./platform" + + main = "" + "# + ), + @r###" + ── UNUSED IMPORT ───────────────────────────────────────── /code/proj/Main.roc ─ + + `List.concat` is not used in this module. + + 1│ app "test" imports [List.{ concat }] provides [main] to "./platform" + ^^^^^^ + + Since `List.concat` isn't used, you don't need to import it. + "### + ); + + test_report!( + #[ignore = "https://github.com/roc-lang/roc/issues/4096"] + unnecessary_builtin_module_import, + indoc!( + r#" + app "test" imports [Str] provides [main] to "./platform" + + main = Str.concat "" "" + "# + ), + @r###" + "### + ); + + test_report!( + #[ignore = "https://github.com/roc-lang/roc/issues/4096"] + unnecessary_builtin_type_import, + indoc!( + r#" + app "test" imports [Decode.{ DecodeError }] provides [main, E] to "./platform" + + E : DecodeError + + main = "" + "# + ), + @r###" + "### + ); + + test_report!( + invalid_toplevel_cycle, + indoc!( + r#" + app "test" imports [] provides [main] to "./platform" + + main = + if Bool.true then {} else main + "# + ), + @r###" + ── CIRCULAR DEFINITION ─────────────────────────────────── /code/proj/Main.roc ─ + + `main` is defined directly in terms of itself: + + 3│> main = + 4│> if Bool.true then {} else main + + Roc evaluates values strictly, so running this program would enter an + infinite loop! + + Hint: Did you mean to define `main` as a function? + "### + ); + + test_report!( + bool_vs_true_tag, + indoc!( + r#" + if True then "" else "" + "# + ), + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + This `if` condition needs to be a Bool: + + 4│ if True then "" else "" + ^^^^ + + This `True` tag has the type: + + [True] + + But I need every `if` condition to evaluate to a Bool—either `Bool.true` + or `Bool.false`. + + Tip: Did you mean to use `Bool.true` rather than `True`? + "### + ); + + test_report!( + bool_vs_false_tag, + indoc!( + r#" + if False then "" else "" + "# + ), + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + This `if` condition needs to be a Bool: + + 4│ if False then "" else "" + ^^^^^ + + This `False` tag has the type: + + [False] + + But I need every `if` condition to evaluate to a Bool—either `Bool.true` + or `Bool.false`. + + Tip: Did you mean to use `Bool.false` rather than `False`? + "### + ); + + test_report!( + derive_hash_for_function, + indoc!( + r#" + app "test" provides [A] to "./platform" + + A a := a -> a implements [Hash] + "# + ), + @r###" + ── INCOMPLETE ABILITY IMPLEMENTATION ───────────────────── /code/proj/Main.roc ─ + + I can't derive an implementation of the `Hash` ability for `A`: + + 3│ A a := a -> a implements [Hash] + ^^^^ + + Note: `Hash` cannot be generated for functions. + + Tip: You can define a custom implementation of `Hash` for `A`. + "### + ); + + test_report!( + derive_hash_for_non_hash_opaque, + indoc!( + r#" + app "test" provides [A] to "./platform" + + A := B implements [Hash] + + B := {} + "# + ), + @r###" + ── INCOMPLETE ABILITY IMPLEMENTATION ───────────────────── /code/proj/Main.roc ─ + + I can't derive an implementation of the `Hash` ability for `A`: + + 3│ A := B implements [Hash] + ^^^^ + + Tip: `B` does not implement `Hash`. Consider adding a custom + implementation or `implements Hash.Hash` to the definition of `B`. + + Tip: You can define a custom implementation of `Hash` for `A`. + + "### + ); + + test_report!( + derive_hash_for_other_has_hash, + indoc!( + r#" + app "test" provides [A] to "./platform" + + A := B implements [Hash] + + B := {} implements [Hash] + "# + ), + @"" // no error + ); + + test_report!( + derive_hash_for_recursive_deriving, + indoc!( + r#" + app "test" provides [MyNat] to "./platform" + + MyNat := [S MyNat, Z] implements [Hash] + "# + ), + @"" // no error + ); + + test_report!( + derive_hash_for_record, + indoc!( + r#" + app "test" provides [main] to "./platform" + + foo : a -> {} where a implements Hash + + main = foo {a: "", b: 1} + "# + ), + @"" // no error + ); + + test_report!( + derive_hash_for_tag, + indoc!( + r#" + app "test" provides [main] to "./platform" + + foo : a -> {} where a implements Hash + + t : [A {}, B U8 U64, C Str] + + main = foo t + "# + ), + @"" // no error + ); + + test_report!( + cannot_derive_hash_for_function, + indoc!( + r#" + app "test" provides [main] to "./platform" + + foo : a -> {} where a implements Hash + + main = foo (\x -> x) + "# + ), + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + This expression has a type that does not implement the abilities it's expected to: + + 5│ main = foo (\x -> x) + ^^^^^^^ + + I can't generate an implementation of the `Hash` ability for + + a -> a + + Note: `Hash` cannot be generated for functions. + "### + ); + + test_report!( + cannot_derive_hash_for_structure_containing_function, + indoc!( + r#" + app "test" provides [main] to "./platform" + + foo : a -> {} where a implements Hash + + main = foo (A (\x -> x) B) + "# + ), + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + This expression has a type that does not implement the abilities it's expected to: + + 5│ main = foo (A (\x -> x) B) + ^^^^^^^^^^^^^ + + I can't generate an implementation of the `Hash` ability for + + [A (a -> a) [B]a] + + In particular, an implementation for + + a -> a + + cannot be generated. + + Note: `Hash` cannot be generated for functions. + "### + ); + + test_no_problem!( + derive_hash_for_tuple, + indoc!( + r#" + app "test" provides [main] to "./platform" + + foo : a -> {} where a implements Hash + + main = foo ("", 1) + "# + ) + ); + + test_report!( + cannot_hash_tuple_with_non_hash_element, + indoc!( + r#" + app "test" provides [main] to "./platform" + + foo : a -> {} where a implements Hash + + main = foo ("", \{} -> {}) + "# + ), + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + This expression has a type that does not implement the abilities it's expected to: + + 5│ main = foo ("", \{} -> {}) + ^^^^^^^^^^^^^^^ + + I can't generate an implementation of the `Hash` ability for + + ( + Str, + {}a -> {}, + )a + + In particular, an implementation for + + {}a -> {} + + cannot be generated. + + Note: `Hash` cannot be generated for functions. + "### + ); + + test_report!( + shift_by_negative, + indoc!( + r#" + { + a: Num.shiftLeftBy 1 -1, + b: Num.shiftRightBy 1 -1, + c: Num.shiftRightZfBy 1 -1, + } + "# + ), + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + This 2nd argument to `shiftRightZfBy` has an unexpected type: + + 7│ c: Num.shiftRightZfBy 1 -1, + ^^ + + The argument is a number of type: + + I8, I16, F32, I32, F64, I64, I128, or Dec + + But `shiftRightZfBy` needs its 2nd argument to be: + + U8 + + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + This 2nd argument to `shiftRightBy` has an unexpected type: + + 6│ b: Num.shiftRightBy 1 -1, + ^^ + + The argument is a number of type: + + I8, I16, F32, I32, F64, I64, I128, or Dec + + But `shiftRightBy` needs its 2nd argument to be: + + U8 + + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + This 2nd argument to `shiftLeftBy` has an unexpected type: + + 5│ a: Num.shiftLeftBy 1 -1, + ^^ + + The argument is a number of type: + + I8, I16, F32, I32, F64, I64, I128, or Dec + + But `shiftLeftBy` needs its 2nd argument to be: + + U8 + "### + ); + + test_report!( + big_char_does_not_fit_in_u8, + indoc!( + r#" + digits : List U8 + digits = List.range { start: At '0', end: At '9' } + + List.contains digits '☃' + "# + ), + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + This 2nd argument to `contains` has an unexpected type: + + 7│ List.contains digits '☃' + ^^^^^ + + The argument is a Unicode scalar value of type: + + U16, I32, U32, I64, Nat, U64, I128, or U128 + + But `contains` needs its 2nd argument to be: + + Int Unsigned8 + "### + ); + + test_report!( + derive_eq_for_function, + indoc!( + r#" + app "test" provides [A] to "./platform" + + A a := a -> a implements [Eq] + "# + ), + @r###" + ── INCOMPLETE ABILITY IMPLEMENTATION ───────────────────── /code/proj/Main.roc ─ + + I can't derive an implementation of the `Eq` ability for `A`: + + 3│ A a := a -> a implements [Eq] + ^^ + + Note: `Eq` cannot be generated for functions. + + Tip: You can define a custom implementation of `Eq` for `A`. + "### + ); + + test_report!( + big_char_does_not_fit_in_u8_pattern, + indoc!( + r#" + x : U8 + + when x is + '☃' -> "" + _ -> "" + "# + ), + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + The branches of this `when` expression don't match the condition: + + 6│> when x is + 7│ '☃' -> "" + 8│ _ -> "" + + This `x` value is a: + + U8 + + But the branch patterns have type: + + U16, I32, U32, I64, Nat, U64, I128, or U128 + + The branches must be cases of the `when` condition's type! + "### + ); + + test_report!( + derive_eq_for_f32, + indoc!( + r#" + app "test" provides [A] to "./platform" + + A := F32 implements [Eq] + "# + ), + @r###" + ── INCOMPLETE ABILITY IMPLEMENTATION ───────────────────── /code/proj/Main.roc ─ + + I can't derive an implementation of the `Eq` ability for `A`: + + 3│ A := F32 implements [Eq] + ^^ + + Note: I can't derive `Bool.isEq` for floating-point types. That's + because Roc's floating-point numbers cannot be compared for total + equality - in Roc, `NaN` is never comparable to `NaN`. If a type + doesn't support total equality, it cannot support the `Eq` ability! + + Tip: You can define a custom implementation of `Eq` for `A`. + "### + ); + + test_report!( + derive_eq_for_f64, + indoc!( + r#" + app "test" provides [A] to "./platform" + + A := F64 implements [Eq] + "# + ), + @r###" + ── INCOMPLETE ABILITY IMPLEMENTATION ───────────────────── /code/proj/Main.roc ─ + + I can't derive an implementation of the `Eq` ability for `A`: + + 3│ A := F64 implements [Eq] + ^^ + + Note: I can't derive `Bool.isEq` for floating-point types. That's + because Roc's floating-point numbers cannot be compared for total + equality - in Roc, `NaN` is never comparable to `NaN`. If a type + doesn't support total equality, it cannot support the `Eq` ability! + + Tip: You can define a custom implementation of `Eq` for `A`. + "### + ); + + test_report!( + derive_eq_for_non_eq_opaque, + indoc!( + r#" + app "test" provides [A] to "./platform" + + A := B implements [Eq] + + B := {} + "# + ), + @r###" + ── INCOMPLETE ABILITY IMPLEMENTATION ───────────────────── /code/proj/Main.roc ─ + + I can't derive an implementation of the `Eq` ability for `A`: + + 3│ A := B implements [Eq] + ^^ + + Tip: `B` does not implement `Eq`. Consider adding a custom implementation + or `implements Bool.Eq` to the definition of `B`. + + Tip: You can define a custom implementation of `Eq` for `A`. + + "### + ); + + test_report!( + derive_eq_for_other_has_eq, + indoc!( + r#" + app "test" provides [A] to "./platform" + + A := B implements [Eq] + + B := {} implements [Eq] + "# + ), + @"" // no error + ); + + test_report!( + derive_eq_for_recursive_deriving, + indoc!( + r#" + app "test" provides [MyNat] to "./platform" + + MyNat := [S MyNat, Z] implements [Eq] + "# + ), + @"" // no error + ); + + test_report!( + derive_eq_for_record, + indoc!( + r#" + app "test" provides [main] to "./platform" + + foo : a -> {} where a implements Eq + + main = foo {a: "", b: 1} + "# + ), + @"" // no error + ); + + test_report!( + derive_eq_for_tag, + indoc!( + r#" + app "test" provides [main] to "./platform" + + foo : a -> {} where a implements Eq + + t : [A {}, B U8 U64, C Str] + + main = foo t + "# + ), + @"" // no error + ); + + test_report!( + cannot_derive_eq_for_function, + indoc!( + r#" + app "test" provides [main] to "./platform" + + foo : a -> {} where a implements Eq + + main = foo (\x -> x) + "# + ), + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + This expression has a type that does not implement the abilities it's expected to: + + 5│ main = foo (\x -> x) + ^^^^^^^ + + I can't generate an implementation of the `Eq` ability for + + a -> a + + Note: `Eq` cannot be generated for functions. + "### + ); + + test_report!( + cannot_derive_eq_for_structure_containing_function, + indoc!( + r#" + app "test" provides [main] to "./platform" + + foo : a -> {} where a implements Eq + + main = foo (A (\x -> x) B) + "# + ), + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + This expression has a type that does not implement the abilities it's expected to: + + 5│ main = foo (A (\x -> x) B) + ^^^^^^^^^^^^^ + + I can't generate an implementation of the `Eq` ability for + + [A (a -> a) [B]a] + + In particular, an implementation for + + a -> a + + cannot be generated. + + Note: `Eq` cannot be generated for functions. + "### + ); + + test_report!( + cannot_eq_functions, + indoc!( + r#" + (\x -> x) == (\x -> x) + "# + ), + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + This expression has a type that does not implement the abilities it's expected to: + + 4│ (\x -> x) == (\x -> x) + ^^^^^^^ + + I can't generate an implementation of the `Eq` ability for + + a -> a + + Note: `Eq` cannot be generated for functions. + "### + ); + + test_report!( + cannot_not_eq_functions, + indoc!( + r#" + (\x -> x) == (\x -> x) + "# + ), + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + This expression has a type that does not implement the abilities it's expected to: + + 4│ (\x -> x) == (\x -> x) + ^^^^^^^ + + I can't generate an implementation of the `Eq` ability for + + a -> a + + Note: `Eq` cannot be generated for functions. + "### + ); + + test_no_problem!( + derive_eq_for_tuple, + indoc!( + r#" + app "test" provides [main] to "./platform" + + foo : a -> {} where a implements Eq + + main = foo ("", 1) + "# + ) + ); + + test_report!( + cannot_eq_tuple_with_non_eq_element, + indoc!( + r#" + app "test" provides [main] to "./platform" + + foo : a -> {} where a implements Eq + + main = foo ("", 1.0f64) + "# + ), + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + This expression has a type that does not implement the abilities it's expected to: + + 5│ main = foo ("", 1.0f64) + ^^^^^^^^^^^^ + + I can't generate an implementation of the `Eq` ability for + + ( + Str, + F64, + )a + + In particular, an implementation for + + F64 + + cannot be generated. + + Note: I can't derive `Bool.isEq` for floating-point types. That's + because Roc's floating-point numbers cannot be compared for total + equality - in Roc, `NaN` is never comparable to `NaN`. If a type + doesn't support total equality, it cannot support the `Eq` ability! + "### + ); + + test_report!( + cannot_import_structural_eq_not_eq, + indoc!( + r#" + { + a: Bool.structuralEq, + b: Bool.structuralNotEq, + } + "# + ), + @r###" + ── NOT EXPOSED ─────────────────────────────────────────── /code/proj/Main.roc ─ + + The Bool module does not expose `structuralEq`: + + 5│ a: Bool.structuralEq, + ^^^^^^^^^^^^^^^^^ + + Did you mean one of these? + + Bool.true + Bool.isNotEq + Bool.false + Bool.isEq + + ── NOT EXPOSED ─────────────────────────────────────────── /code/proj/Main.roc ─ + + The Bool module does not expose `structuralNotEq`: + + 6│ b: Bool.structuralNotEq, + ^^^^^^^^^^^^^^^^^^^^ + + Did you mean one of these? + + Bool.isNotEq + Bool.true + Bool.boolIsEq + Bool.false + "### + ); + + test_report!( + expand_ability_from_type_alias_mismatch, + indoc!( + r#" + app "test" provides [f] to "./platform" + + F a : a where a implements Hash + + f : F ({} -> {}) + "# + ), + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + This expression has a type that does not implement the abilities it's expected to: + + 5│ f : F ({} -> {}) + ^^^^^^^^ + + I can't generate an implementation of the `Hash` ability for + + {} -> {} + + Note: `Hash` cannot be generated for functions. + "### + ); + + test_report!( + demanded_vs_optional_record_field, + indoc!( + r#" + foo : { a : Str } -> Str + foo = \{ a ? "" } -> a + foo + "# + ), + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + The 1st argument to `foo` is weird: + + 5│ foo = \{ a ? "" } -> a + ^^^^^^^^^^ + + The argument is a pattern that matches record values of type: + + { a ? Str } + + But the annotation on `foo` says the 1st argument should be: + + { a : Str } + "### + ); + + test_report!( + underivable_opaque_doesnt_error_for_derived_bodies, + indoc!( + r#" + app "test" provides [main] to "./platform" + + F := U8 -> U8 implements [Hash, Eq, Encoding] + + main = "" + "# + ), + @r###" + ── INCOMPLETE ABILITY IMPLEMENTATION ───────────────────── /code/proj/Main.roc ─ + + I can't derive an implementation of the `Hash` ability for `F`: + + 3│ F := U8 -> U8 implements [Hash, Eq, Encoding] + ^^^^ + + Note: `Hash` cannot be generated for functions. + + Tip: You can define a custom implementation of `Hash` for `F`. + + ── INCOMPLETE ABILITY IMPLEMENTATION ───────────────────── /code/proj/Main.roc ─ + + I can't derive an implementation of the `Eq` ability for `F`: + + 3│ F := U8 -> U8 implements [Hash, Eq, Encoding] + ^^ + + Note: `Eq` cannot be generated for functions. + + Tip: You can define a custom implementation of `Eq` for `F`. + + ── INCOMPLETE ABILITY IMPLEMENTATION ───────────────────── /code/proj/Main.roc ─ + + I can't derive an implementation of the `Encoding` ability for `F`: + + 3│ F := U8 -> U8 implements [Hash, Eq, Encoding] + ^^^^^^^^ + + Note: `Encoding` cannot be generated for functions. + + Tip: You can define a custom implementation of `Encoding` for `F`. + "### + ); + + test_report!( + duplicate_ability_in_has_clause, + indoc!( + r#" + f : a -> {} where a implements Hash & Hash + + f + "# + ), + @r###" + ── DUPLICATE BOUND ABILITY ─────────────────────────────── /code/proj/Main.roc ─ + + I already saw that this type variable is bound to the `Hash` ability + once before: + + 4│ f : a -> {} where a implements Hash & Hash + ^^^^ + + Abilities only need to bound to a type variable once in an `implements` + clause! + "### + ); + + test_report!( + rigid_able_bounds_must_be_a_superset_of_flex_bounds, + indoc!( + r#" + app "test" provides [main] to "./platform" + + g : x -> x where x implements Decoding & Encoding + + main : x -> x where x implements Encoding + main = \x -> g x + "# + ), + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + This 1st argument to `g` has an unexpected type: + + 6│ main = \x -> g x + ^ + + This `x` value is a: + + x where x implements Encoding + + But `g` needs its 1st argument to be: + + x where x implements Encoding & Decoding + + Note: The type variable `x` says it can take on any value that + implements only the ability `Encoding`. + + But, I see that it's also used as if it implements the ability + `Decoding`. Can you use `x` without that ability? If not, consider adding + it to the `implements` clause of `x`. + "### + ); + + test_report!( + rigid_able_bounds_must_be_a_superset_of_flex_bounds_multiple, + indoc!( + r#" + app "test" provides [main] to "./platform" + + g : x -> x where x implements Decoding & Encoding & Hash + + main : x -> x where x implements Encoding + main = \x -> g x + "# + ), + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + This 1st argument to `g` has an unexpected type: + + 6│ main = \x -> g x + ^ + + This `x` value is a: + + x where x implements Encoding + + But `g` needs its 1st argument to be: + + x where x implements Hash & Encoding & Decoding + + Note: The type variable `x` says it can take on any value that + implements only the ability `Encoding`. + + But, I see that it's also used as if it implements the abilities `Hash` + and `Decoding`. Can you use `x` without those abilities? If not, consider + adding them to the `implements` clause of `x`. + "### + ); + + test_report!( + rigid_able_bounds_must_be_a_superset_of_flex_bounds_with_indirection, + indoc!( + r#" + app "test" provides [main] to "./platform" + + f : x -> x where x implements Hash + g : x -> x where x implements Decoding & Encoding + + main : x -> x where x implements Hash & Encoding + main = \x -> g (f x) + "# + ), + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + This 1st argument to `g` has an unexpected type: + + 7│ main = \x -> g (f x) + ^^^ + + This `f` call produces: + + x where x implements Hash & Encoding + + But `g` needs its 1st argument to be: + + x where x implements Encoding & Decoding + + Note: The type variable `x` says it can take on any value that + implements only the abilities `Hash` and `Encoding`. + + But, I see that it's also used as if it implements the ability + `Decoding`. Can you use `x` without that ability? If not, consider adding + it to the `implements` clause of `x`. + "### + ); + + test_report!( + list_pattern_not_terminated, + indoc!( + r#" + when [] is + [1, 2, -> "" + "# + ), + @r###" + ── UNFINISHED LIST PATTERN ────────── tmp/list_pattern_not_terminated/Test.roc ─ + + I am partway through parsing a list pattern, but I got stuck here: + + 5│ [1, 2, -> "" + ^ + + I was expecting to see a closing square brace before this, so try + adding a ] and see if that helps? + "### + ); + + test_report!( + list_pattern_weird_rest_pattern, + indoc!( + r#" + when [] is + [...] -> "" + "# + ), + @r###" + ── INCORRECT REST PATTERN ─────── tmp/list_pattern_weird_rest_pattern/Test.roc ─ + + It looks like you may trying to write a list rest pattern, but it's + not the form I expect: + + 5│ [...] -> "" + ^ + + List rest patterns, which match zero or more elements in a list, are + denoted with .. - is that what you meant? + "### + ); + + test_report!( + unnecessary_extension_variable, + indoc!( + r#" + f : {} -> [A, B]* + f + "# + ), + @r###" + ── UNNECESSARY WILDCARD ────────────────────────────────── /code/proj/Main.roc ─ + + This type annotation has a wildcard type variable (`*`) that isn't + needed. + + 4│ f : {} -> [A, B]* + ^ + + Annotations for tag unions which are constants, or which are returned + from functions, work the same way with or without a `*` at the end. (The + `*` means something different when the tag union is an argument to a + function, though!) + + You can safely remove this to make the code more concise without + changing what it means. + "### + ); + + test_report!( + multiple_list_patterns_start_and_end, + indoc!( + r#" + when [] is + [.., A, ..] -> "" + "# + ), + @r###" + ── MULTIPLE LIST REST PATTERNS ─────────────────────────── /code/proj/Main.roc ─ + + This list pattern match has multiple rest patterns: + + 5│ [.., A, ..] -> "" + ^^ + + I only support compiling list patterns with one .. pattern! Can you + remove this additional one? + + ── UNSAFE PATTERN ──────────────────────────────────────── /code/proj/Main.roc ─ + + This `when` does not cover all the possibilities: + + 4│> when [] is + 5│> [.., A, ..] -> "" + + Other possibilities include: + + _ + + I would have to crash if I saw one of those! Add branches for them! + "### + ); + + test_report!( + multiple_list_patterns_in_a_row, + indoc!( + r#" + when [] is + [A, .., .., B] -> "" + "# + ), + @r###" + ── MULTIPLE LIST REST PATTERNS ─────────────────────────── /code/proj/Main.roc ─ + + This list pattern match has multiple rest patterns: + + 5│ [A, .., .., B] -> "" + ^^ + + I only support compiling list patterns with one .. pattern! Can you + remove this additional one? + + ── UNSAFE PATTERN ──────────────────────────────────────── /code/proj/Main.roc ─ + + This `when` does not cover all the possibilities: + + 4│> when [] is + 5│> [A, .., .., B] -> "" + + Other possibilities include: + + _ + + I would have to crash if I saw one of those! Add branches for them! + "### + ); + + test_report!( + mismatch_within_list_pattern, + indoc!( + r#" + when [] is + [A, 1u8] -> "" + "# + ), + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + This list element doesn't match the types of other elements in the + pattern: + + 5│ [A, 1u8] -> "" + ^^^ + + It matches integers: + + U8 + + But the other elements in this list pattern match + + [A] + "### + ); + + test_report!( + mismatch_list_pattern_vs_condition, + indoc!( + r#" + when [A, B] is + ["foo", "bar"] -> "" + "# + ), + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + The branches of this `when` expression don't match the condition: + + 4│> when [A, B] is + 5│ ["foo", "bar"] -> "" + + The `when` condition is a list of type: + + List [ + A, + B, + ] + + But the branch patterns have type: + + List Str + + The branches must be cases of the `when` condition's type! + "### + ); + + test_report!( + list_match_non_exhaustive_only_empty, + indoc!( + r#" + l : List [A] + + when l is + [] -> "" + "# + ), + @r###" + ── UNSAFE PATTERN ──────────────────────────────────────── /code/proj/Main.roc ─ + + This `when` does not cover all the possibilities: + + 6│> when l is + 7│> [] -> "" + + Other possibilities include: + + [_, ..] + + I would have to crash if I saw one of those! Add branches for them! + "### + ); + + test_no_problem!( + list_match_spread_exhaustive, + indoc!( + r#" + l : List [A] + + when l is + [..] -> "" + "# + ) + ); + + test_report!( + list_match_non_exhaustive_infinite, + indoc!( + r#" + l : List [A] + + when l is + [] -> "" + [A] -> "" + [A, A] -> "" + "# + ), + @r###" + ── UNSAFE PATTERN ──────────────────────────────────────── /code/proj/Main.roc ─ + + This `when` does not cover all the possibilities: + + 6│> when l is + 7│> [] -> "" + 8│> [A] -> "" + 9│> [A, A] -> "" + + Other possibilities include: + + [_, _, _, ..] + + I would have to crash if I saw one of those! Add branches for them! + "### + ); + + test_no_problem!( + list_match_spread_required_front_back, + indoc!( + r#" + l : List [A, B] + + when l is + [A, ..] -> "" + [.., A] -> "" + [..] -> "" + "# + ) + ); + + test_report!( + list_match_spread_redundant_front_back, + indoc!( + r#" + l : List [A] + + when l is + [A, ..] -> "" + [.., A] -> "" + [..] -> "" + "# + ), + @r###" + ── REDUNDANT PATTERN ───────────────────────────────────── /code/proj/Main.roc ─ + + The 2nd pattern is redundant: + + 6│ when l is + 7│ [A, ..] -> "" + 8│> [.., A] -> "" + 9│ [..] -> "" + + Any value of this shape will be handled by a previous pattern, so this + one should be removed. + "### + ); + + test_no_problem!( + list_match_spread_as, + indoc!( + r#" + l : List [A, B] + + when l is + [A, .. as rest] | [.. as rest, A] -> rest + [.. as rest] -> rest + "# + ) + ); + + test_no_problem!( + list_match_exhaustive_empty_and_rest_with_unary_head, + indoc!( + r#" + l : List [A] + + when l is + [] -> "" + [_, ..] -> "" + "# + ) + ); + + test_no_problem!( + list_match_exhaustive_empty_and_rest_with_exhausted_head, + indoc!( + r#" + l : List [A, B] + + when l is + [] -> "" + [A, ..] -> "" + [B, ..] -> "" + "# + ) + ); + + test_report!( + list_match_exhaustive_empty_and_rest_with_nonexhaustive_head, + indoc!( + r#" + l : List [A, B] + + when l is + [] -> "" + [A, ..] -> "" + "# + ), + @r###" + ── UNSAFE PATTERN ──────────────────────────────────────── /code/proj/Main.roc ─ + + This `when` does not cover all the possibilities: + + 6│> when l is + 7│> [] -> "" + 8│> [A, ..] -> "" + + Other possibilities include: + + [B, ..] + + I would have to crash if I saw one of those! Add branches for them! + "### + ); + + test_report!( + list_match_no_small_sizes_and_non_exhaustive_head, + indoc!( + r#" + l : List [A, B] + + when l is + [A, B, ..] -> "" + "# + ), + @r###" + ── UNSAFE PATTERN ──────────────────────────────────────── /code/proj/Main.roc ─ + + This `when` does not cover all the possibilities: + + 6│> when l is + 7│> [A, B, ..] -> "" + + Other possibilities include: + + [] + [_] + [_, A, ..] + + I would have to crash if I saw one of those! Add branches for them! + "### + ); + + test_no_problem!( + list_match_exhaustive_empty_and_rest_with_exhausted_tail, + indoc!( + r#" + l : List [A, B] + + when l is + [] -> "" + [.., A] -> "" + [.., B] -> "" + "# + ) + ); + + test_report!( + list_match_exhaustive_empty_and_rest_with_nonexhaustive_tail, + indoc!( + r#" + l : List [A, B] + + when l is + [] -> "" + [.., A] -> "" + "# + ), + @r###" + ── UNSAFE PATTERN ──────────────────────────────────────── /code/proj/Main.roc ─ + + This `when` does not cover all the possibilities: + + 6│> when l is + 7│> [] -> "" + 8│> [.., A] -> "" + + Other possibilities include: + + [.., B] + + I would have to crash if I saw one of those! Add branches for them! + "### + ); + + test_report!( + list_match_no_small_sizes_and_non_exhaustive_tail, + indoc!( + r#" + l : List [A, B] + + when l is + [.., B, A] -> "" + "# + ), + @r###" + ── UNSAFE PATTERN ──────────────────────────────────────── /code/proj/Main.roc ─ + + This `when` does not cover all the possibilities: + + 6│> when l is + 7│> [.., B, A] -> "" + + Other possibilities include: + + [] + [_] + [.., _, B] + + I would have to crash if I saw one of those! Add branches for them! + "### + ); + + test_no_problem!( + list_match_exhaustive_empty_and_rest_with_exhausted_head_and_tail, + indoc!( + r#" + l : List [A, B] + + when l is + [] -> "" + [A] -> "" + [B] -> "" + [A, .., A] -> "" + [A, .., B] -> "" + [B, .., A] -> "" + [B, .., B] -> "" + "# + ) + ); + + test_report!( + list_match_exhaustive_empty_and_rest_with_nonexhaustive_head_and_tail, + indoc!( + r#" + l : List [A, B] + + when l is + [] -> "" + [_] -> "" + [A, .., B] -> "" + [B, .., A] -> "" + "# + ), + @r###" + ── UNSAFE PATTERN ──────────────────────────────────────── /code/proj/Main.roc ─ + + This `when` does not cover all the possibilities: + + 6│> when l is + 7│> [] -> "" + 8│> [_] -> "" + 9│> [A, .., B] -> "" + 10│> [B, .., A] -> "" + + Other possibilities include: + + [_, .., _] + + I would have to crash if I saw one of those! Add branches for them! + "### + ); + + test_report!( + list_match_no_small_sizes_and_non_exhaustive_head_and_tail, + indoc!( + r#" + l : List [A, B] + + when l is + [A, .., B] -> "" + [B, .., A] -> "" + [B, .., B] -> "" + "# + ), + @r###" + ── UNSAFE PATTERN ──────────────────────────────────────── /code/proj/Main.roc ─ + + This `when` does not cover all the possibilities: + + 6│> when l is + 7│> [A, .., B] -> "" + 8│> [B, .., A] -> "" + 9│> [B, .., B] -> "" + + Other possibilities include: + + [] + [_] + [A, .., A] + + I would have to crash if I saw one of those! Add branches for them! + "### + ); + + test_report!( + list_match_exhaustive_big_sizes_but_not_small_sizes, + indoc!( + r#" + l : List [A] + + when l is + [A, A, A, .., A, A, A] -> "" + [A, A, A, .., A, A] -> "" + [A, A, .., A, A] -> "" + "# + ), + @r###" + ── UNSAFE PATTERN ──────────────────────────────────────── /code/proj/Main.roc ─ + + This `when` does not cover all the possibilities: + + 6│> when l is + 7│> [A, A, A, .., A, A, A] -> "" + 8│> [A, A, A, .., A, A] -> "" + 9│> [A, A, .., A, A] -> "" + + Other possibilities include: + + [] + [_] + [_, _] + [_, _, _] + + I would have to crash if I saw one of those! Add branches for them! + "### + ); + + test_no_problem!( + list_match_nested_list_exhaustive, + indoc!( + r#" + l : List (List [A]) + + when l is + [] -> "" + [[]] -> "" + [[A, ..]] -> "" + [[..], .., [..]] -> "" + "# + ) + ); + + test_report!( + list_match_nested_list_not_exhaustive, + indoc!( + r#" + l : List (List [A, B]) + + when l is + [] -> "" + [[]] -> "" + [[A, ..]] -> "" + [[..], .., [.., B]] -> "" + "# + ), + @r###" + ── UNSAFE PATTERN ──────────────────────────────────────── /code/proj/Main.roc ─ + + This `when` does not cover all the possibilities: + + 6│> when l is + 7│> [] -> "" + 8│> [[]] -> "" + 9│> [[A, ..]] -> "" + 10│> [[..], .., [.., B]] -> "" + + Other possibilities include: + + [[B, ..]] + [_, .., []] + [_, .., [.., A]] + + I would have to crash if I saw one of those! Add branches for them! + "### + ); + + test_report!( + list_match_redundant_exact_size, + indoc!( + r#" + l : List [A] + + when l is + [] -> "" + [_] -> "" + [_] -> "" + [..] -> "" + "# + ), + @r###" + ── REDUNDANT PATTERN ───────────────────────────────────── /code/proj/Main.roc ─ + + The 3rd pattern is redundant: + + 6│ when l is + 7│ [] -> "" + 8│ [_] -> "" + 9│> [_] -> "" + 10│ [..] -> "" + + Any value of this shape will be handled by a previous pattern, so this + one should be removed. + "### + ); + + test_report!( + list_match_redundant_any_slice, + indoc!( + r#" + l : List [A] + + when l is + [] -> "" + [_, ..] -> "" + [..] -> "" + "# + ), + @r###" + ── REDUNDANT PATTERN ───────────────────────────────────── /code/proj/Main.roc ─ + + The 3rd pattern is redundant: + + 6│ when l is + 7│ [] -> "" + 8│ [_, ..] -> "" + 9│ [..] -> "" + ^^^^ + + Any value of this shape will be handled by a previous pattern, so this + one should be removed. + "### + ); + + test_report!( + list_match_redundant_suffix_slice_with_sized_prefix, + indoc!( + r#" + l : List [A] + + when l is + [] -> "" + [_, ..] -> "" + [.., _] -> "" + "# + ), + @r###" + ── REDUNDANT PATTERN ───────────────────────────────────── /code/proj/Main.roc ─ + + The 3rd pattern is redundant: + + 6│ when l is + 7│ [] -> "" + 8│ [_, ..] -> "" + 9│ [.., _] -> "" + ^^^^^^^ + + Any value of this shape will be handled by a previous pattern, so this + one should be removed. + "### + ); + + test_report!( + list_match_redundant_based_on_ctors, + indoc!( + r#" + l : List {} + + when l is + [{}, .., _] -> "" + [_, .., {}] -> "" + [..] -> "" + "# + ), + @r###" + ── REDUNDANT PATTERN ───────────────────────────────────── /code/proj/Main.roc ─ + + The 2nd pattern is redundant: + + 6│ when l is + 7│ [{}, .., _] -> "" + 8│> [_, .., {}] -> "" + 9│ [..] -> "" + + Any value of this shape will be handled by a previous pattern, so this + one should be removed. + "### + ); + + test_no_problem!( + list_match_with_guard, + indoc!( + r#" + l : List [A] + + when l is + [ A, .. ] if Bool.true -> "" + [ A, .. ] -> "" + _ -> "" + "# + ) + ); + + test_report!( + suggest_binding_rigid_var_to_ability, + indoc!( + r#" + app "test" provides [f] to "./p" + + f : List e -> List e + f = \l -> if l == l then l else l + "# + ), + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + This expression has a type that does not implement the abilities it's expected to: + + 4│ f = \l -> if l == l then l else l + ^ + + I can't generate an implementation of the `Eq` ability for + + List e + + In particular, an implementation for + + e + + cannot be generated. + + Tip: This type variable is not bound to `Eq`. Consider adding an + `implements` clause to bind the type variable, like + `where e implements Bool.Eq` + "### + ); + + test_report!( + crash_given_non_string, + indoc!( + r#" + crash {} + "# + ), + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + This value passed to `crash` is not a string: + + 4│ crash {} + ^^ + + The value is a record of type: + + {} + + But I can only `crash` with messages of type + + Str + "### + ); + + test_report!( + crash_unapplied, + indoc!( + r#" + crash + "# + ), + @r###" + ── UNAPPLIED CRASH ─────────────────────────────────────── /code/proj/Main.roc ─ + + This `crash` doesn't have a message given to it: + + 4│ crash + ^^^^^ + + `crash` must be passed a message to crash with at the exact place it's + used. `crash` can't be used as a value that's passed around, like + functions can be - it must be applied immediately! + "### + ); + + test_report!( + crash_overapplied, + indoc!( + r#" + crash "" "" + "# + ), + @r###" + ── OVERAPPLIED CRASH ───────────────────────────────────── /code/proj/Main.roc ─ + + This `crash` has too many values given to it: + + 4│ crash "" "" + ^^^^^ + + `crash` must be given exacly one message to crash with. + "### + ); + + test_no_problem!( + resolve_eq_for_unbound_num, + indoc!( + r#" + app "test" provides [main] to "./platform" + + n : Num * + + main = n == 1 + "# + ) + ); + + test_report!( + resolve_eq_for_unbound_num_float, + indoc!( + r#" + app "test" provides [main] to "./platform" + + n : Num * + + main = n == 1f64 + "# + ), + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + This expression has a type that does not implement the abilities it's expected to: + + 5│ main = n == 1f64 + ^^^^ + + I can't generate an implementation of the `Eq` ability for + + FloatingPoint ? + + Note: I can't derive `Bool.isEq` for floating-point types. That's + because Roc's floating-point numbers cannot be compared for total + equality - in Roc, `NaN` is never comparable to `NaN`. If a type + doesn't support total equality, it cannot support the `Eq` ability! + "### + ); + + test_no_problem!( + resolve_hash_for_unbound_num, + indoc!( + r#" + app "test" provides [main] to "./platform" + + n : Num * + + main = \hasher -> Hash.hash hasher n + "# + ) + ); + + test_report!( + self_recursive_not_reached, + indoc!( + r#" + app "test" provides [f] to "./platform" + f = h {} + h = \{} -> 1 + g = \{} -> if Bool.true then "" else g {} + "# + ), + @r###" + ── DEFINITION ONLY USED IN RECURSION ───────────────────── /code/proj/Main.roc ─ + + This definition is only used in recursion with itself: + + 4│ g = \{} -> if Bool.true then "" else g {} + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + If you don't intend to use or export this definition, it should be + removed! + "### + ); + + test_no_problem!( + self_recursive_not_reached_but_exposed, + indoc!( + r#" + app "test" provides [g] to "./platform" + g = \{} -> if Bool.true then "" else g {} + "# + ) + ); + + test_report!( + mutual_recursion_not_reached, + indoc!( + r#" + app "test" provides [h] to "./platform" + h = "" + f = \{} -> if Bool.true then "" else g {} + g = \{} -> if Bool.true then "" else f {} + "# + ), + @r###" + ── DEFINITIONs ONLY USED IN RECURSION ──────────────────── /code/proj/Main.roc ─ + + These 2 definitions are only used in mutual recursion with themselves: + + 3│> f = \{} -> if Bool.true then "" else g {} + 4│> g = \{} -> if Bool.true then "" else f {} + + If you don't intend to use or export any of them, they should all be + removed! + "### + ); + + test_report!( + mutual_recursion_not_reached_but_exposed, + indoc!( + r#" + app "test" provides [f] to "./platform" + f = \{} -> if Bool.true then "" else g {} + g = \{} -> if Bool.true then "" else f {} + "# + ), + @r###" + "### + ); + + test_report!( + self_recursive_not_reached_nested, + indoc!( + r#" + app "test" provides [main] to "./platform" + main = + g = \{} -> if Bool.true then "" else g {} + "" + "# + ), + @r###" + ── DEFINITION ONLY USED IN RECURSION ───────────────────── /code/proj/Main.roc ─ + + This definition is only used in recursion with itself: + + 3│ g = \{} -> if Bool.true then "" else g {} + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + If you don't intend to use or export this definition, it should be + removed! + "### + ); + + test_no_problem!( + self_recursive_not_reached_but_exposed_nested, + indoc!( + r#" + app "test" provides [main] to "./platform" + main = + g = \{} -> if Bool.true then "" else g {} + g + "# + ) + ); + + test_report!( + mutual_recursion_not_reached_nested, + indoc!( + r#" + app "test" provides [main] to "./platform" + main = + f = \{} -> if Bool.true then "" else g {} + g = \{} -> if Bool.true then "" else f {} + "" + "# + ), + @r###" + ── DEFINITIONs ONLY USED IN RECURSION ──────────────────── /code/proj/Main.roc ─ + + These 2 definitions are only used in mutual recursion with themselves: + + 3│> f = \{} -> if Bool.true then "" else g {} + 4│> g = \{} -> if Bool.true then "" else f {} + + If you don't intend to use or export any of them, they should all be + removed! + "### + ); + + test_report!( + mutual_recursion_not_reached_but_exposed_nested, + indoc!( + r#" + app "test" provides [main] to "./platform" + main = + f = \{} -> if Bool.true then "" else g {} + g = \{} -> if Bool.true then "" else f {} + f + "# + ), + @r###" + "### + ); + + // TODO(weakening-reports) + test_report!( + concat_different_types, + indoc!( + r#" + empty = [] + one = List.concat [1] empty + str = List.concat ["blah"] empty + + {one, str} + "#), + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + This 2nd argument to `concat` has an unexpected type: + + 6│ str = List.concat ["blah"] empty + ^^^^^ + + This `empty` value is a: + + List (Num *) + + But `concat` needs its 2nd argument to be: + + List Str + "### + ); + + test_report!( + implicit_inferred_open_in_output_position_cannot_grow, + indoc!( + r#" + app "test" provides [main] to "./platform" + + main : {} -> [One] + main = \{} -> + if Bool.true + then One + else Two + "# + ), + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + Something is off with the `else` branch of this `if` expression: + + 3│ main : {} -> [One] + 4│ main = \{} -> + 5│ if Bool.true + 6│ then One + 7│ else Two + ^^^ + + This `Two` tag has the type: + + [Two] + + But the type annotation on `main` says it should be: + + [One] + "### + ); + + test_report!( + implicit_inferred_open_in_output_position_cannot_grow_alias, + indoc!( + r#" + app "test" provides [main] to "./platform" + + R : [One] + + main : {} -> R + main = \{} -> + if Bool.true + then One + else Two + "# + ), + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + Something is off with the `else` branch of this `if` expression: + + 5│ main : {} -> R + 6│ main = \{} -> + 7│ if Bool.true + 8│ then One + 9│ else Two + ^^^ + + This `Two` tag has the type: + + [Two] + + But the type annotation on `main` says it should be: + + [One] + "### + ); + + test_report!( + implicit_inferred_open_in_output_position_cannot_grow_nested, + indoc!( + r#" + app "test" provides [main] to "./platform" + + main : List [One, Two] -> List [One] + main = \tags -> + List.map tags \tag -> + when tag is + One -> One + Two -> Two + "# + ), + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + Something is off with the body of the `main` definition: + + 3│ main : List [One, Two] -> List [One] + 4│ main = \tags -> + 5│> List.map tags \tag -> + 6│> when tag is + 7│> One -> One + 8│> Two -> Two + + This `map` call produces: + + List [Two, …] + + But the type annotation on `main` says it should be: + + List […] + "### + ); + + test_report!( + implicit_inferred_open_in_output_position_cannot_grow_nested_alias, + indoc!( + r#" + app "test" provides [main] to "./platform" + + R : [One] + + main : List [One, Two] -> List R + main = \tags -> + List.map tags \tag -> + when tag is + One -> One + Two -> Two + "# + ), + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + Something is off with the body of the `main` definition: + + 5│ main : List [One, Two] -> List R + 6│ main = \tags -> + 7│> List.map tags \tag -> + 8│> when tag is + 9│> One -> One + 10│> Two -> Two + + This `map` call produces: + + List [Two, …] + + But the type annotation on `main` says it should be: + + List […] + "### + ); + + test_no_problem!( + explicit_inferred_open_in_output_position_can_grow, + indoc!( + r#" + app "test" provides [main] to "./platform" + + main : List [One, Two] -> List [One]_ + main = \tags -> + List.map tags \tag -> + when tag is + One -> One + Two -> Two + "# + ) + ); + + test_report!( + derive_decoding_for_nat, + indoc!( + r#" + app "test" imports [Decode.{decoder}] provides [main] to "./platform" + + main = + myDecoder : Decoder Nat fmt where fmt implements DecoderFormatting + myDecoder = decoder + + myDecoder + "# + ), + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + This expression has a type that does not implement the abilities it's expected to: + + 5│ myDecoder = decoder + ^^^^^^^ + + I can't generate an implementation of the `Decoding` ability for + + Nat + + Note: Decoding to a Nat is not supported. Consider decoding to a + fixed-sized unsigned integer, like U64, then converting to a Nat if + needed. + "### + ); + + test_report!( + derive_encoding_for_nat, + indoc!( + r#" + app "test" imports [] provides [main] to "./platform" + + x : Nat + + main = Encode.toEncoder x + "# + ), + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + This expression has a type that does not implement the abilities it's expected to: + + 5│ main = Encode.toEncoder x + ^ + + I can't generate an implementation of the `Encoding` ability for + + Int Natural + + In particular, an implementation for + + Natural + + cannot be generated. + + Tip: `Natural` does not implement `Encoding`. + "### + ); + + test_no_problem!( + derive_decoding_for_tuple, + indoc!( + r#" + app "test" imports [Decode.{decoder}] provides [main] to "./platform" + + main = + myDecoder : Decoder (U32, Str) fmt where fmt implements DecoderFormatting + myDecoder = decoder + + myDecoder + "# + ) + ); + + test_report!( + cannot_decode_tuple_with_non_decode_element, + indoc!( + r#" + app "test" imports [Decode.{decoder}] provides [main] to "./platform" + + main = + myDecoder : Decoder (U32, {} -> {}) fmt where fmt implements DecoderFormatting + myDecoder = decoder + + myDecoder + "# + ), + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + This expression has a type that does not implement the abilities it's expected to: + + 5│ myDecoder = decoder + ^^^^^^^ + + I can't generate an implementation of the `Decoding` ability for + + U32, {} -> {} + + Note: `Decoding` cannot be generated for functions. + "### + ); + + test_no_problem!( + derive_encoding_for_tuple, + indoc!( + r#" + app "test" imports [] provides [main] to "./platform" + + x : (U32, Str) + + main = Encode.toEncoder x + "# + ) + ); + + test_report!( + cannot_encode_tuple_with_non_encode_element, + indoc!( + r#" + app "test" imports [] provides [main] to "./platform" + + x : (U32, {} -> {}) + + main = Encode.toEncoder x + "# + ), + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + This expression has a type that does not implement the abilities it's expected to: + + 5│ main = Encode.toEncoder x + ^ + + I can't generate an implementation of the `Encoding` ability for + + U32, {} -> {} + + Note: `Encoding` cannot be generated for functions. + "### + ); + + test_report!( + exhaustiveness_check_function_or_tag_union_issue_4994, + indoc!( + r#" + app "test" provides [main] to "./platform" + + x : U8 + + ifThenCase = + when x is + 0 -> Red + 1 -> Yellow + 2 -> Purple + 3 -> Zulip + _ -> Green + + main = + when ifThenCase is + Red -> "red" + Green -> "green" + Yellow -> "yellow" + Zulip -> "zulip" + "# + ), + @r###" + ── UNSAFE PATTERN ──────────────────────────────────────── /code/proj/Main.roc ─ + + This `when` does not cover all the possibilities: + + 14│> when ifThenCase is + 15│> Red -> "red" + 16│> Green -> "green" + 17│> Yellow -> "yellow" + 18│> Zulip -> "zulip" + + Other possibilities include: + + Purple + _ + + I would have to crash if I saw one of those! Add branches for them! + "### + ); + + test_no_problem!( + openness_constraint_opens_under_tuple, + indoc!( + r#" + x : [A, B, C] + when (x, 1u8) is + (A, _) -> Bool.true + (B, _) -> Bool.true + _ -> Bool.true + "# + ) + ); + + test_report!( + apply_opaque_as_function, + indoc!( + r#" + app "test" provides [main] to "./platform" + + Parser a := Str -> a + + parser : Parser Str + parser = @Parser \s -> Str.concat s "asd" + + main : Str + main = parser "hi" + "# + ), + @r###" + ── TOO MANY ARGS ───────────────────────────────────────── /code/proj/Main.roc ─ + + The `parser` value is an opaque type, so it cannot be called with an + argument: + + 9│ main = parser "hi" + ^^^^^^ + + I can't call an opaque type because I don't know what it is! Maybe you + meant to unwrap it first? + "### + ); + + test_report!( + function_arity_mismatch_too_few, + indoc!( + r#" + app "test" provides [f] to "./platform" + + f : U8, U8 -> U8 + f = \x -> x + "# + ), + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + Something is off with the body of the `f` definition: + + 3│ f : U8, U8 -> U8 + 4│ f = \x -> x + ^^^^^^^ + + The body is an anonymous function of type: + + (U8 -> U8) + + But the type annotation on `f` says it should be: + + (U8, U8 -> U8) + + Tip: It looks like it takes too few arguments. I was expecting 1 more. + "### + ); + + test_report!( + function_arity_mismatch_too_many, + indoc!( + r#" + app "test" provides [f] to "./platform" + + f : U8, U8 -> U8 + f = \x, y, z -> x + y + z + "# + ), + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + Something is off with the body of the `f` definition: + + 3│ f : U8, U8 -> U8 + 4│ f = \x, y, z -> x + y + z + ^^^^^^^^^^^^^^^^^^^^^ + + The body is an anonymous function of type: + + (U8, U8, Int Unsigned8 -> U8) + + But the type annotation on `f` says it should be: + + (U8, U8 -> U8) + + Tip: It looks like it takes too many arguments. I'm seeing 1 extra. + "### + ); + + test_report!( + function_arity_mismatch_nested_too_few, + indoc!( + r#" + app "test" provides [main] to "./platform" + + main = + f : U8, U8 -> U8 + f = \x -> x + + f + "# + ), + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + Something is off with the body of the `f` definition: + + 4│ f : U8, U8 -> U8 + 5│ f = \x -> x + ^^^^^^^ + + The body is an anonymous function of type: + + (U8 -> U8) + + But the type annotation on `f` says it should be: + + (U8, U8 -> U8) + + Tip: It looks like it takes too few arguments. I was expecting 1 more. + "### + ); + + test_report!( + function_arity_mismatch_nested_too_many, + indoc!( + r#" + app "test" provides [main] to "./platform" + + main = + f : U8, U8 -> U8 + f = \x, y, z -> x + y + z + + f + "# + ), + @r###" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + Something is off with the body of the `f` definition: + + 4│ f : U8, U8 -> U8 + 5│ f = \x, y, z -> x + y + z + ^^^^^^^^^^^^^^^^^^^^^ + + The body is an anonymous function of type: + + (U8, U8, Int Unsigned8 -> U8) + + But the type annotation on `f` says it should be: + + (U8, U8 -> U8) + + Tip: It looks like it takes too many arguments. I'm seeing 1 extra. + "### + ); + + test_report!( + pizza_parens_right, + indoc!( + r#" + 2 |> (Num.sub 3) + "# + ), + @r###" + ── TOO FEW ARGS ────────────────────────────────────────── /code/proj/Main.roc ─ + + The `sub` function expects 2 arguments, but it got only 1: + + 4│ 2 |> (Num.sub 3) + ^^^^^^^ + + Roc does not allow functions to be partially applied. Use a closure to + make partial application explicit. + "### + ); + + test_report!( + pizza_parens_middle, + indoc!( + r#" + 2 |> (Num.sub 3) |> Num.sub 3 + "# + ), + @r###" + ── TOO FEW ARGS ────────────────────────────────────────── /code/proj/Main.roc ─ + + The `sub` function expects 2 arguments, but it got only 1: + + 4│ 2 |> (Num.sub 3) |> Num.sub 3 + ^^^^^^^ + + Roc does not allow functions to be partially applied. Use a closure to + make partial application explicit. + "### + ); +} diff --git a/compiler/load_internal/.gitignore b/crates/compiler/load_internal/.gitignore similarity index 100% rename from compiler/load_internal/.gitignore rename to crates/compiler/load_internal/.gitignore diff --git a/crates/compiler/load_internal/Cargo.toml b/crates/compiler/load_internal/Cargo.toml new file mode 100644 index 0000000000..eec4cd8ed7 --- /dev/null +++ b/crates/compiler/load_internal/Cargo.toml @@ -0,0 +1,47 @@ +[package] +name = "roc_load_internal" +description = "The internal implementation of roc_load, separate from roc_load to support caching." + +authors.workspace = true +edition.workspace = true +license.workspace = true +version.workspace = true + +[dependencies] +roc_builtins = { path = "../builtins" } +roc_can = { path = "../can" } +roc_checkmate = { path = "../checkmate" } +roc_collections = { path = "../collections" } +roc_constrain = { path = "../constrain" } +roc_debug_flags = { path = "../debug_flags" } +roc_derive = { path = "../derive" } +roc_derive_key = { path = "../derive_key" } +roc_error_macros = { path = "../../error_macros" } +roc_late_solve = { path = "../late_solve" } +roc_module = { path = "../module" } +roc_mono = { path = "../mono" } +roc_packaging = { path = "../../packaging" } +roc_parse = { path = "../parse" } +roc_problem = { path = "../problem" } +roc_region = { path = "../region" } +roc_reporting = { path = "../../reporting" } +roc_solve = { path = "../solve" } +roc_solve_problem = { path = "../solve_problem" } +roc_target = { path = "../roc_target" } +roc_tracing = { path = "../../tracing" } +roc_types = { path = "../types" } +roc_unify = { path = "../unify" } + +ven_pretty = { path = "../../vendor/pretty" } + +bumpalo.workspace = true +crossbeam.workspace = true +parking_lot.workspace = true +tempfile.workspace = true + +[dev-dependencies] +roc_test_utils = { path = "../../test_utils" } + +indoc.workspace = true +maplit.workspace = true +pretty_assertions.workspace = true diff --git a/crates/compiler/load_internal/src/docs.rs b/crates/compiler/load_internal/src/docs.rs new file mode 100644 index 0000000000..b84f701bf2 --- /dev/null +++ b/crates/compiler/load_internal/src/docs.rs @@ -0,0 +1,707 @@ +use crate::docs::DocEntry::DetachedDoc; +use crate::docs::TypeAnnotation::{Apply, BoundVariable, Function, NoTypeAnn, Record, TagUnion}; +use roc_can::scope::Scope; +use roc_collections::VecSet; +use roc_module::ident::ModuleName; +use roc_module::symbol::{IdentIds, ModuleId, ModuleIds, Symbol}; +use roc_parse::ast::AssignedField; +use roc_parse::ast::{self, ExtractSpaces, TypeHeader}; +use roc_parse::ast::{CommentOrNewline, TypeDef, ValueDef}; + +// Documentation generation requirements + +#[derive(Debug)] +pub struct ModuleDocumentation { + pub name: String, + pub entries: Vec, + pub scope: Scope, + pub exposed_symbols: VecSet, +} + +#[derive(Debug, Clone)] +pub enum DocEntry { + DocDef(DocDef), + DetachedDoc(String), +} + +#[derive(Debug, Clone)] +pub struct DocDef { + pub name: String, + pub symbol: Symbol, + pub type_vars: Vec, + pub type_annotation: TypeAnnotation, + pub docs: Option, +} + +#[derive(Debug, Clone)] +pub enum TypeAnnotation { + TagUnion { + tags: Vec, + extension: Box, + }, + Function { + args: Vec, + output: Box, + }, + ObscuredTagUnion, + ObscuredRecord, + BoundVariable(String), + Apply { + name: String, + parts: Vec, + }, + Record { + fields: Vec, + extension: Box, + }, + Tuple { + elems: Vec, + extension: Box, + }, + Ability { + members: Vec, + }, + Wildcard, + NoTypeAnn, + Where { + ann: Box, + implements: Vec, + }, + As { + ann: Box, + name: String, + vars: Vec, + }, +} + +#[derive(Debug, Clone)] +pub struct ImplementsClause { + pub name: String, + pub abilities: Vec, +} + +#[derive(Debug, Clone)] +pub enum RecordField { + RecordField { + name: String, + type_annotation: TypeAnnotation, + }, + OptionalField { + name: String, + type_annotation: TypeAnnotation, + }, + LabelOnly { + name: String, + }, +} + +#[derive(Debug, Clone)] +pub struct AbilityMember { + pub name: String, + pub type_annotation: TypeAnnotation, + pub able_variables: Vec<(String, Vec)>, + pub docs: Option, +} + +#[derive(Debug, Clone)] +pub struct Tag { + pub name: String, + pub values: Vec, +} + +#[allow(clippy::too_many_arguments)] +pub fn generate_module_docs( + scope: Scope, + home: ModuleId, + module_ids: &ModuleIds, + module_name: ModuleName, + parsed_defs: &roc_parse::ast::Defs, + exposed_module_ids: &[ModuleId], + exposed_symbols: VecSet, + header_comments: &[CommentOrNewline<'_>], +) -> ModuleDocumentation { + let entries = generate_entry_docs( + home, + &scope.locals.ident_ids, + module_ids, + parsed_defs, + exposed_module_ids, + header_comments, + ); + + ModuleDocumentation { + name: module_name.as_str().to_string(), + scope, + entries, + exposed_symbols, + } +} + +fn detached_docs_from_comments_and_new_lines<'a>( + comments_or_new_lines: impl Iterator>, +) -> Vec { + let mut detached_docs: Vec = Vec::new(); + + let mut docs = String::new(); + + for comment_or_new_line in comments_or_new_lines { + match comment_or_new_line { + CommentOrNewline::DocComment(doc_str) => { + docs.push_str(doc_str); + docs.push('\n'); + } + + CommentOrNewline::LineComment(_) | CommentOrNewline::Newline => { + if !docs.is_empty() { + detached_docs.push(docs.clone()); + } + + docs = String::new(); + } + } + } + + detached_docs +} + +fn generate_entry_docs( + home: ModuleId, + ident_ids: &IdentIds, + module_ids: &ModuleIds, + defs: &roc_parse::ast::Defs<'_>, + exposed_module_ids: &[ModuleId], + header_comments: &[CommentOrNewline<'_>], +) -> Vec { + use roc_parse::ast::Pattern; + + let mut acc = Vec::with_capacity(defs.tags.len() + 1); + + if let Some(docs) = comments_or_new_lines_to_docs(header_comments) { + acc.push(DetachedDoc(docs)); + } + + let mut before_comments_or_new_lines: Option<&[CommentOrNewline]> = None; + let mut scratchpad = Vec::new(); + + for (index, either_index) in defs.tags.iter().enumerate() { + let spaces_before = &defs.spaces[defs.space_before[index].indices()]; + + scratchpad.clear(); + scratchpad.extend( + before_comments_or_new_lines + .take() + .iter() + .flat_map(|e| e.iter()), + ); + scratchpad.extend(spaces_before); + + let docs = comments_or_new_lines_to_docs(&scratchpad); + + match either_index.split() { + Err(value_index) => match &defs.value_defs[value_index.index()] { + ValueDef::Annotation(loc_pattern, loc_ann) => { + if let Pattern::Identifier(identifier) = loc_pattern.value { + // Check if this module exposes the def + if let Some(ident_id) = ident_ids.get_id(identifier) { + let name = identifier.to_string(); + let doc_def = DocDef { + name, + symbol: Symbol::new(home, ident_id), + type_annotation: type_to_docs(false, loc_ann.value), + type_vars: Vec::new(), + docs, + }; + acc.push(DocEntry::DocDef(doc_def)); + } + } + } + + ValueDef::AnnotatedBody { + ann_pattern, + ann_type, + .. + } => { + if let Pattern::Identifier(identifier) = ann_pattern.value { + // Check if this module exposes the def + if let Some(ident_id) = ident_ids.get_id(identifier) { + let doc_def = DocDef { + name: identifier.to_string(), + type_annotation: type_to_docs(false, ann_type.value), + type_vars: Vec::new(), + symbol: Symbol::new(home, ident_id), + docs, + }; + acc.push(DocEntry::DocDef(doc_def)); + } + } + } + + ValueDef::Body(_, _) => { + // TODO generate docs for un-annotated bodies + } + + ValueDef::Dbg { .. } => { + // Don't generate docs for `dbg`s + } + + ValueDef::Expect { .. } => { + // Don't generate docs for `expect`s + } + + ValueDef::ExpectFx { .. } => { + // Don't generate docs for `expect-fx`s + } + }, + Ok(type_index) => match &defs.type_defs[type_index.index()] { + TypeDef::Alias { + header: TypeHeader { name, vars }, + ann, + } => { + let mut type_vars = Vec::new(); + + for var in vars.iter() { + if let Pattern::Identifier(ident_name) = var.value { + type_vars.push(ident_name.to_string()); + } + } + + let type_annotation = + // If this alias contains an unexposed type, then don't try to render a + // type annotation for it. You're not allowed to see that! + // (This comes up when exporting an alias like Task ok err : InnerTask ok err + // where Task is exposed but InnerTask isn't.) + if contains_unexposed_type(&ann.value, exposed_module_ids, module_ids) { + TypeAnnotation::NoTypeAnn + } else { + type_to_docs(false, ann.value) + }; + + let ident_id = ident_ids.get_id(name.value).unwrap(); + let doc_def = DocDef { + name: name.value.to_string(), + type_annotation, + type_vars, + docs, + symbol: Symbol::new(home, ident_id), + }; + acc.push(DocEntry::DocDef(doc_def)); + } + + TypeDef::Opaque { + header: TypeHeader { name, vars }, + .. + } => { + let mut type_vars = Vec::new(); + + for var in vars.iter() { + if let Pattern::Identifier(ident_name) = var.value { + type_vars.push(ident_name.to_string()); + } + } + + let ident_id = ident_ids.get_id(name.value).unwrap(); + let doc_def = DocDef { + name: name.value.to_string(), + type_annotation: TypeAnnotation::NoTypeAnn, + type_vars, + docs, + symbol: Symbol::new(home, ident_id), + }; + acc.push(DocEntry::DocDef(doc_def)); + } + + TypeDef::Ability { + header: TypeHeader { name, vars }, + members, + .. + } => { + let mut type_vars = Vec::new(); + + for var in vars.iter() { + if let Pattern::Identifier(ident_name) = var.value { + type_vars.push(ident_name.to_string()); + } + } + + let members = members + .iter() + .map(|mem| { + let extracted = mem.name.value.extract_spaces(); + let (type_annotation, able_variables) = + ability_member_type_to_docs(mem.typ.value); + + AbilityMember { + name: extracted.item.to_string(), + type_annotation, + able_variables, + docs: comments_or_new_lines_to_docs(extracted.before), + } + }) + .collect(); + + let ident_id = ident_ids.get_id(name.value).unwrap(); + let doc_def = DocDef { + name: name.value.to_string(), + type_annotation: TypeAnnotation::Ability { members }, + symbol: Symbol::new(home, ident_id), + type_vars, + docs, + }; + acc.push(DocEntry::DocDef(doc_def)); + } + }, + } + + let spaces_after = &defs.spaces[defs.space_after[index].indices()]; + before_comments_or_new_lines = Some(spaces_after); + } + + let it = before_comments_or_new_lines.iter().flat_map(|e| e.iter()); + + for detached_doc in detached_docs_from_comments_and_new_lines(it) { + acc.push(DetachedDoc(detached_doc)); + } + + acc +} + +/// Does this type contain any types which are not exposed outside the package? +/// (If so, we shouldn't try to render a type annotation for it.) +fn contains_unexposed_type( + ann: &ast::TypeAnnotation, + exposed_module_ids: &[ModuleId], + module_ids: &ModuleIds, +) -> bool { + use ast::TypeAnnotation::*; + + match ann { + // Apply is the one case that can directly return true. + Apply(module_name, _ident, loc_args) => { + // If the *ident* was unexposed, we would have gotten a naming error + // during canonicalization, so all we need to check is the module. + let module_id = module_ids.get_id(&(*module_name).into()).unwrap(); + + !exposed_module_ids.contains(&module_id) + || loc_args.iter().any(|loc_arg| { + contains_unexposed_type(&loc_arg.value, exposed_module_ids, module_ids) + }) + } + Malformed(_) | Inferred | Wildcard | BoundVariable(_) => false, + Function(loc_args, loc_ret) => { + contains_unexposed_type(&loc_ret.value, exposed_module_ids, module_ids) + || loc_args.iter().any(|loc_arg| { + contains_unexposed_type(&loc_arg.value, exposed_module_ids, module_ids) + }) + } + Record { fields, ext } => { + if let Some(loc_ext) = ext { + if contains_unexposed_type(&loc_ext.value, exposed_module_ids, module_ids) { + return true; + } + } + + let mut fields_to_process = + Vec::from_iter(fields.iter().map(|loc_field| loc_field.value)); + + while let Some(field) = fields_to_process.pop() { + match field { + AssignedField::RequiredValue(_field, _spaces, loc_val) + | AssignedField::OptionalValue(_field, _spaces, loc_val) => { + if contains_unexposed_type(&loc_val.value, exposed_module_ids, module_ids) { + return true; + } + } + AssignedField::Malformed(_) | AssignedField::LabelOnly(_) => { + // contains no unexposed types, so continue + } + AssignedField::SpaceBefore(field, _) | AssignedField::SpaceAfter(field, _) => { + fields_to_process.push(*field); + } + } + } + + false + } + Tuple { elems: fields, ext } => { + if let Some(loc_ext) = ext { + if contains_unexposed_type(&loc_ext.value, exposed_module_ids, module_ids) { + return true; + } + } + + fields.iter().any(|loc_field| { + contains_unexposed_type(&loc_field.value, exposed_module_ids, module_ids) + }) + } + TagUnion { ext, tags } => { + use ast::Tag; + + if let Some(loc_ext) = ext { + if contains_unexposed_type(&loc_ext.value, exposed_module_ids, module_ids) { + return true; + } + } + + let mut tags_to_process = Vec::from_iter(tags.iter().map(|loc_tag| loc_tag.value)); + + while let Some(tag) = tags_to_process.pop() { + match tag { + Tag::Apply { name: _, args } => { + for loc_ann in args.iter() { + if contains_unexposed_type( + &loc_ann.value, + exposed_module_ids, + module_ids, + ) { + return true; + } + } + } + Tag::Malformed(_) => { + // contains no unexposed types, so continue + } + Tag::SpaceBefore(tag, _) | Tag::SpaceAfter(tag, _) => { + tags_to_process.push(*tag); + } + } + } + + false + } + Where(loc_ann, _loc_has_clauses) => { + // We assume all the abilities in the `implements` clause are from exported modules. + // TODO don't assume this! Instead, look them up and verify. + contains_unexposed_type(&loc_ann.value, exposed_module_ids, module_ids) + } + As(loc_ann, _spaces, _type_header) => { + contains_unexposed_type(&loc_ann.value, exposed_module_ids, module_ids) + } + SpaceBefore(ann, _) | ast::TypeAnnotation::SpaceAfter(ann, _) => { + contains_unexposed_type(ann, exposed_module_ids, module_ids) + } + } +} + +fn type_to_docs(in_func_type_ann: bool, type_annotation: ast::TypeAnnotation) -> TypeAnnotation { + match type_annotation { + ast::TypeAnnotation::TagUnion { tags, ext } => { + let mut tags_to_render: Vec = Vec::new(); + + for tag in tags.iter() { + if let Some(tag_ann) = tag_to_doc(in_func_type_ann, tag.value) { + tags_to_render.push(tag_ann); + } + } + + let extension = match ext { + None => NoTypeAnn, + Some(ext_type_ann) => type_to_docs(in_func_type_ann, ext_type_ann.value), + }; + + TagUnion { + tags: tags_to_render, + extension: Box::new(extension), + } + } + ast::TypeAnnotation::BoundVariable(var_name) => BoundVariable(var_name.to_string()), + ast::TypeAnnotation::Apply(module_name, type_name, type_ann_parts) => { + let mut name = String::new(); + + if !module_name.is_empty() { + name.push_str(module_name); + name.push('.'); + } + + name.push_str(type_name); + + let mut parts: Vec = Vec::new(); + + for type_ann_part in type_ann_parts { + parts.push(type_to_docs(in_func_type_ann, type_ann_part.value)); + } + + Apply { name, parts } + } + ast::TypeAnnotation::Record { fields, ext } => { + let mut doc_fields = Vec::new(); + + for field in fields.items { + if let Some(doc_field) = record_field_to_doc(in_func_type_ann, field.value) { + doc_fields.push(doc_field); + } + } + let extension = match ext { + None => NoTypeAnn, + Some(ext_type_ann) => type_to_docs(in_func_type_ann, ext_type_ann.value), + }; + + Record { + fields: doc_fields, + extension: Box::new(extension), + } + } + ast::TypeAnnotation::SpaceBefore(&sub_type_ann, _) => { + type_to_docs(in_func_type_ann, sub_type_ann) + } + ast::TypeAnnotation::SpaceAfter(&sub_type_ann, _) => { + type_to_docs(in_func_type_ann, sub_type_ann) + } + ast::TypeAnnotation::Function(ast_arg_anns, output_ann) => { + let mut doc_arg_anns = Vec::new(); + + for ast_arg_ann in ast_arg_anns { + doc_arg_anns.push(type_to_docs(true, ast_arg_ann.value)); + } + + Function { + args: doc_arg_anns, + output: Box::new(type_to_docs(true, output_ann.value)), + } + } + ast::TypeAnnotation::Wildcard => TypeAnnotation::Wildcard, + ast::TypeAnnotation::As(loc_ann, _comments, type_header) => TypeAnnotation::As { + ann: Box::new(type_to_docs(in_func_type_ann, loc_ann.value)), + name: type_header.name.value.to_string(), + vars: type_header + .vars + .iter() + .filter_map(|loc_pattern| match loc_pattern.value { + ast::Pattern::Identifier(ident) => Some(ident.to_string()), + _ => None, + }) + .collect(), + }, + ast::TypeAnnotation::Tuple { elems, ext } => { + let mut doc_elems = Vec::new(); + + for loc_ann in elems.items { + doc_elems.push(type_to_docs(in_func_type_ann, loc_ann.value)); + } + + let extension = match ext { + None => NoTypeAnn, + Some(ext_type_ann) => type_to_docs(in_func_type_ann, ext_type_ann.value), + }; + + TypeAnnotation::Tuple { + elems: doc_elems, + extension: Box::new(extension), + } + } + ast::TypeAnnotation::Where(loc_ann, implements) => TypeAnnotation::Where { + ann: Box::new(type_to_docs(in_func_type_ann, loc_ann.value)), + implements: implements + .iter() + .map(|clause| { + let abilities = clause + .value + .abilities + .iter() + .map(|ability| type_to_docs(in_func_type_ann, ability.value)) + .collect(); + + ImplementsClause { + name: clause.value.var.value.item().to_string(), + abilities, + } + }) + .collect(), + }, + ast::TypeAnnotation::Malformed(_) | ast::TypeAnnotation::Inferred => { + TypeAnnotation::NoTypeAnn + } + } +} + +fn ability_member_type_to_docs( + type_annotation: ast::TypeAnnotation, +) -> (TypeAnnotation, Vec<(String, Vec)>) { + match type_annotation { + ast::TypeAnnotation::Where(ta, has_clauses) => { + let ta = type_to_docs(false, ta.value); + let has_clauses = has_clauses + .iter() + .map(|hc| { + let ast::ImplementsClause { var, abilities } = hc.value; + ( + var.value.extract_spaces().item.to_string(), + abilities + .iter() + .map(|ability| type_to_docs(false, ability.value)) + .collect(), + ) + }) + .collect(); + + (ta, has_clauses) + } + _ => (type_to_docs(false, type_annotation), vec![]), + } +} + +fn record_field_to_doc( + in_func_ann: bool, + field: ast::AssignedField<'_, ast::TypeAnnotation>, +) -> Option { + match field { + AssignedField::RequiredValue(name, _, type_ann) => Some(RecordField::RecordField { + name: name.value.to_string(), + type_annotation: type_to_docs(in_func_ann, type_ann.value), + }), + AssignedField::SpaceBefore(&sub_field, _) => record_field_to_doc(in_func_ann, sub_field), + AssignedField::SpaceAfter(&sub_field, _) => record_field_to_doc(in_func_ann, sub_field), + AssignedField::OptionalValue(name, _, type_ann) => Some(RecordField::OptionalField { + name: name.value.to_string(), + type_annotation: type_to_docs(in_func_ann, type_ann.value), + }), + AssignedField::LabelOnly(label) => Some(RecordField::LabelOnly { + name: label.value.to_string(), + }), + AssignedField::Malformed(_) => None, + } +} + +// The Option here represents if it is malformed. +fn tag_to_doc(in_func_ann: bool, tag: ast::Tag) -> Option { + match tag { + ast::Tag::Apply { name, args } => Some(Tag { + name: name.value.to_string(), + values: { + let mut type_vars = Vec::new(); + + for arg in args { + type_vars.push(type_to_docs(in_func_ann, arg.value)); + } + + type_vars + }, + }), + ast::Tag::SpaceBefore(&sub_tag, _) => tag_to_doc(in_func_ann, sub_tag), + ast::Tag::SpaceAfter(&sub_tag, _) => tag_to_doc(in_func_ann, sub_tag), + ast::Tag::Malformed(_) => None, + } +} + +fn comments_or_new_lines_to_docs<'a>( + comments_or_new_lines: &'a [roc_parse::ast::CommentOrNewline<'a>], +) -> Option { + let mut docs = String::new(); + + for comment_or_new_line in comments_or_new_lines.iter() { + match comment_or_new_line { + CommentOrNewline::DocComment(doc_str) => { + docs.push_str(doc_str); + docs.push('\n'); + } + CommentOrNewline::Newline | CommentOrNewline::LineComment(_) => { + docs = String::new(); + } + } + } + + if docs.is_empty() { + None + } else { + Some(docs) + } +} diff --git a/crates/compiler/load_internal/src/file.rs b/crates/compiler/load_internal/src/file.rs new file mode 100644 index 0000000000..d24f8b96e2 --- /dev/null +++ b/crates/compiler/load_internal/src/file.rs @@ -0,0 +1,6669 @@ +#![allow(clippy::too_many_arguments)] + +use crate::docs::ModuleDocumentation; +use crate::module::{ + CheckedModule, ConstrainedModule, EntryPoint, Expectations, ExposedToHost, + FoundSpecializationsModule, LateSpecializationsModule, LoadedModule, ModuleHeader, + ModuleTiming, MonomorphizedModule, ParsedModule, ToplevelExpects, TypeCheckedModule, +}; +use crate::module_cache::ModuleCache; +use bumpalo::{collections::CollectIn, Bump}; +use crossbeam::channel::{bounded, Sender}; +use crossbeam::deque::{Injector, Stealer, Worker}; +use crossbeam::thread; +use parking_lot::Mutex; +use roc_builtins::roc::module_source; +use roc_can::abilities::{AbilitiesStore, PendingAbilitiesStore, ResolvedImpl}; +use roc_can::constraint::{Constraint as ConstraintSoa, Constraints, TypeOrVar}; +use roc_can::expr::{DbgLookup, Declarations, ExpectLookup, PendingDerives}; +use roc_can::module::{ + canonicalize_module_defs, ExposedByModule, ExposedForModule, ExposedModuleTypes, Module, + ResolvedImplementations, TypeState, +}; +use roc_collections::{default_hasher, BumpMap, MutMap, MutSet, VecMap, VecSet}; +use roc_constrain::module::constrain_module; +use roc_debug_flags::dbg_do; +#[cfg(debug_assertions)] +use roc_debug_flags::{ + ROC_CHECK_MONO_IR, ROC_PRINT_IR_AFTER_DROP_SPECIALIZATION, ROC_PRINT_IR_AFTER_REFCOUNT, + ROC_PRINT_IR_AFTER_RESET_REUSE, ROC_PRINT_IR_AFTER_SPECIALIZATION, ROC_PRINT_IR_AFTER_TRMC, + ROC_PRINT_LOAD_LOG, +}; +use roc_derive::SharedDerivedModule; +use roc_error_macros::internal_error; +use roc_late_solve::{AbilitiesView, WorldAbilities}; +use roc_module::ident::{Ident, ModuleName, QualifiedModuleName}; +use roc_module::symbol::{ + IdentIds, IdentIdsByModule, Interns, ModuleId, ModuleIds, PQModuleName, PackageModuleIds, + PackageQualified, Symbol, +}; +use roc_mono::ir::{ + CapturedSymbols, ExternalSpecializations, GlueLayouts, HostExposedLambdaSets, PartialProc, + Proc, ProcLayout, Procs, ProcsBase, UpdateModeIds, UsageTrackingMap, +}; +use roc_mono::layout::{ + GlobalLayoutInterner, LambdaName, Layout, LayoutCache, LayoutProblem, Niche, STLayoutInterner, +}; +use roc_mono::reset_reuse; +use roc_mono::{drop_specialization, inc_dec}; +use roc_packaging::cache::RocCacheDir; +use roc_parse::ast::{ + self, CommentOrNewline, Expr, ExtractSpaces, Pattern, Spaced, StrLiteral, ValueDef, +}; +use roc_parse::header::{ + ExposedName, HeaderType, ImportsEntry, PackageEntry, PackageHeader, PlatformHeader, To, + TypedIdent, +}; +use roc_parse::module::module_defs; +use roc_parse::parser::{FileError, Parser, SourceError, SyntaxError}; +use roc_problem::Severity; +use roc_region::all::{LineInfo, Loc, Region}; +#[cfg(not(target_family = "wasm"))] +use roc_reporting::report::to_https_problem_report_string; +use roc_reporting::report::{to_file_problem_report_string, Palette, RenderTarget}; +use roc_solve::module::{extract_module_owned_implementations, SolveConfig, Solved, SolvedModule}; +use roc_solve::FunctionKind; +use roc_solve_problem::TypeError; +use roc_target::TargetInfo; +use roc_types::subs::{CopiedImport, ExposedTypesStorageSubs, Subs, VarStore, Variable}; +use roc_types::types::{Alias, Types}; +use std::collections::hash_map::Entry::{Occupied, Vacant}; +use std::collections::HashMap; +use std::io; +use std::iter; +use std::ops::ControlFlow; +use std::path::{Path, PathBuf}; +use std::str::from_utf8_unchecked; +use std::sync::Arc; +use std::{env, fs}; +#[cfg(not(target_family = "wasm"))] +use { + roc_packaging::cache::{self}, + roc_packaging::https::{PackageMetadata, Problem}, +}; + +pub use crate::work::Phase; +use crate::work::{DepCycle, Dependencies}; + +#[cfg(target_family = "wasm")] +use crate::wasm_instant::{Duration, Instant}; +#[cfg(not(target_family = "wasm"))] +use std::time::{Duration, Instant}; + +/// Filename extension for normal Roc modules +const ROC_FILE_EXTENSION: &str = "roc"; + +/// The . in between module names like Foo.Bar.Baz +const MODULE_SEPARATOR: char = '.'; + +const EXPANDED_STACK_SIZE: usize = 8 * 1024 * 1024; + +macro_rules! log { + ($($arg:tt)*) => (dbg_do!(ROC_PRINT_LOAD_LOG, println!($($arg)*))) +} + +#[derive(Debug)] +pub struct LoadConfig { + pub target_info: TargetInfo, + pub render: RenderTarget, + pub palette: Palette, + pub threading: Threading, + pub exec_mode: ExecutionMode, + pub function_kind: FunctionKind, +} + +#[derive(Debug, Clone, Copy)] +pub enum ExecutionMode { + Check, + Executable, + /// Like [`ExecutionMode::Executable`], but stops in the presence of type errors. + ExecutableIfCheck, + /// Test is like [`ExecutionMode::ExecutableIfCheck`], but rather than producing a proper + /// executable, run tests. + Test, +} + +impl ExecutionMode { + fn goal_phase(&self) -> Phase { + use ExecutionMode::*; + + match self { + Executable => Phase::MakeSpecializations, + Check | ExecutableIfCheck | Test => Phase::SolveTypes, + } + } + + fn build_if_checks(&self) -> bool { + matches!(self, Self::ExecutableIfCheck | Self::Test) + } +} + +type SharedIdentIdsByModule = Arc>; + +fn start_phase<'a>( + module_id: ModuleId, + phase: Phase, + arena: &'a Bump, + state: &mut State<'a>, +) -> Vec> { + // we blindly assume all dependencies are met + + use crate::work::PrepareStartPhase::*; + match state.dependencies.prepare_start_phase(module_id, phase) { + Continue => { + // fall through + } + Done => { + // no more work to do + return vec![]; + } + Recurse(new) => { + return new + .into_iter() + .flat_map(|(module_id, phase)| start_phase(module_id, phase, arena, state)) + .collect() + } + } + + let task = { + match phase { + Phase::LoadHeader => { + let opt_dep_name = state.module_cache.module_names.get(&module_id); + + match opt_dep_name { + None => { + panic!("Module {module_id:?} is not in module_cache.module_names") + } + Some(dep_name) => { + let module_name = dep_name.clone(); + + BuildTask::LoadModule { + module_name, + // Provide mutexes of ModuleIds and IdentIds by module, + // so other modules can populate them as they load. + module_ids: Arc::clone(&state.arc_modules), + shorthands: Arc::clone(&state.arc_shorthands), + ident_ids_by_module: Arc::clone(&state.ident_ids_by_module), + } + } + } + } + Phase::Parse => { + // parse the file + let header = state.module_cache.headers.remove(&module_id).unwrap(); + + BuildTask::Parse { header } + } + Phase::CanonicalizeAndConstrain => { + // canonicalize the file + let parsed = state.module_cache.parsed.remove(&module_id).unwrap(); + + let deps_by_name = &parsed.deps_by_name; + let num_deps = deps_by_name.len(); + let mut dep_idents: IdentIdsByModule = IdentIds::exposed_builtins(num_deps); + + let State { + ident_ids_by_module, + .. + } = &state; + + { + let ident_ids_by_module = (*ident_ids_by_module).lock(); + + // Populate dep_idents with each of their IdentIds, + // which we'll need during canonicalization to translate + // identifier strings into IdentIds, which we need to build Symbols. + // We only include the modules we care about (the ones we import). + // + // At the end of this loop, dep_idents contains all the information to + // resolve a symbol from another module: if it's in here, that means + // we have both imported the module and the ident was exported by that mdoule. + for dep_id in deps_by_name.values() { + // We already verified that these are all present, + // so unwrapping should always succeed here. + let idents = ident_ids_by_module.get(dep_id).unwrap(); + + dep_idents.insert(*dep_id, idents.clone()); + } + } + + // Clone the module_ids we'll need for canonicalization. + // This should be small, and cloning it should be quick. + // We release the lock as soon as we're done cloning, so we don't have + // to lock the global module_ids while canonicalizing any given module. + let qualified_module_ids = Arc::clone(&state.arc_modules); + let qualified_module_ids = { (*qualified_module_ids).lock().clone() }; + + let module_ids = qualified_module_ids.into_module_ids(); + + let exposed_symbols = state + .exposed_symbols_by_module + .get(&module_id) + .expect("Could not find listener ID in exposed_symbols_by_module") + .clone(); + + let mut aliases = MutMap::default(); + let mut abilities_store = PendingAbilitiesStore::default(); + + for imported in parsed.imported_modules.keys() { + match state.module_cache.aliases.get(imported) { + None => unreachable!( + r"imported module {:?} did not register its aliases, so {:?} cannot use them", + imported, parsed.module_id, + ), + Some(new) => { + aliases.extend(new.iter().filter_map(|(s, (exposed, a))| { + // only pass this on if it's exposed, or the alias is a transitive import + if *exposed || s.module_id() != *imported { + Some((*s, a.clone())) + } else { + None + } + })); + } + } + + match state.module_cache.pending_abilities.get(imported) { + None => unreachable!( + r"imported module {:?} did not register its abilities, so {:?} cannot use them", + imported, parsed.module_id, + ), + Some(import_store) => { + let exposed_symbols = state + .exposed_symbols_by_module + .get(imported) + .unwrap_or_else(|| { + internal_error!( + "Could not find exposed symbols of imported {:?}", + imported + ) + }); + + // Add the declared abilities from the modules we import; + // we may not know all their types yet since type-solving happens in + // parallel, but we'll fill that in during type-checking our module. + abilities_store + .union(import_store.closure_from_imported(exposed_symbols)); + } + } + } + + let skip_constraint_gen = { + // Give this its own scope to make sure that the Guard from the lock() is dropped + // immediately after contains_key returns + state.cached_types.lock().contains_key(&module_id) + }; + + BuildTask::CanonicalizeAndConstrain { + parsed, + dep_idents, + exposed_symbols, + module_ids, + aliases, + abilities_store, + skip_constraint_gen, + exposed_module_ids: state.exposed_modules, + } + } + + Phase::SolveTypes => { + let constrained = state.module_cache.constrained.remove(&module_id).unwrap(); + + let ConstrainedModule { + module, + ident_ids, + module_timing, + constraints, + constraint, + var_store, + imported_modules, + declarations, + dep_idents, + pending_derives, + types, + .. + } = constrained; + + let derived_module = SharedDerivedModule::clone(&state.derived_module); + + #[cfg(debug_assertions)] + let checkmate = if roc_checkmate::is_checkmate_enabled() { + Some(roc_checkmate::Collector::new()) + } else { + None + }; + + BuildTask::solve_module( + module, + ident_ids, + module_timing, + types, + constraints, + constraint, + state.function_kind, + pending_derives, + var_store, + imported_modules, + &state.exposed_types, + dep_idents, + declarations, + state.cached_types.clone(), + derived_module, + // + #[cfg(debug_assertions)] + checkmate, + ) + } + Phase::FindSpecializations => { + let typechecked = state.module_cache.typechecked.remove(&module_id).unwrap(); + + let TypeCheckedModule { + layout_cache, + module_id, + module_timing, + solved_subs, + decls, + ident_ids, + abilities_store, + expectations, + // + #[cfg(debug_assertions)] + checkmate: _, + } = typechecked; + + let mut imported_module_thunks = bumpalo::collections::Vec::new_in(arena); + + if let Some(imports) = state.module_cache.imports.get(&module_id) { + for imported in imports.iter() { + imported_module_thunks.extend( + state.module_cache.top_level_thunks[imported] + .iter() + .copied(), + ); + } + } + + let derived_module = SharedDerivedModule::clone(&state.derived_module); + + let build_expects = + matches!(state.exec_mode, ExecutionMode::Test) && expectations.is_some(); + + BuildTask::BuildPendingSpecializations { + layout_cache, + module_id, + module_timing, + solved_subs, + imported_module_thunks: imported_module_thunks.into_bump_slice(), + decls, + ident_ids, + exposed_to_host: state.exposed_to_host.clone(), + abilities_store, + // TODO: awful, how can we get rid of the clone? + exposed_by_module: state.exposed_types.clone(), + derived_module, + expectations, + build_expects, + } + } + Phase::MakeSpecializations => { + let mut specializations_we_must_make = state + .module_cache + .external_specializations_requested + .remove(&module_id) + .unwrap_or_default(); + + if module_id == ModuleId::DERIVED_GEN { + // The derived gen module must also fulfill also specializations asked of the + // derived synth module. + let derived_synth_specializations = state + .module_cache + .external_specializations_requested + .remove(&ModuleId::DERIVED_SYNTH) + .unwrap_or_default(); + specializations_we_must_make.extend(derived_synth_specializations) + } + + let ( + mut ident_ids, + mut subs, + expectations, + mut procs_base, + layout_cache, + mut module_timing, + ) = if state.make_specializations_pass.current_pass() == 1 + && module_id == ModuleId::DERIVED_GEN + { + // This is the first time the derived module is introduced into the load + // graph. It has no abilities of its own or anything, just generate fresh + // information for it. + ( + IdentIds::default(), + Subs::default(), + None, // no expectations for derived module + ProcsBase::default(), + LayoutCache::new(state.layout_interner.fork(), state.target_info), + ModuleTiming::new(Instant::now()), + ) + } else if state.make_specializations_pass.current_pass() == 1 { + let found_specializations = state + .module_cache + .found_specializations + .remove(&module_id) + .unwrap(); + + let FoundSpecializationsModule { + ident_ids, + subs, + procs_base, + layout_cache, + module_timing, + abilities_store, + expectations, + } = found_specializations; + let our_exposed_types = state + .exposed_types + .get(&module_id) + .unwrap_or_else(|| { + internal_error!("Exposed types for {:?} missing", module_id) + }) + .clone(); + + // Add our abilities to the world. + state.world_abilities.insert( + module_id, + abilities_store, + our_exposed_types.exposed_types_storage_subs, + ); + + ( + ident_ids, + subs, + expectations, + procs_base, + layout_cache, + module_timing, + ) + } else { + let LateSpecializationsModule { + ident_ids, + subs, + expectations, + module_timing, + layout_cache, + procs_base, + } = state + .module_cache + .late_specializations + .remove(&module_id) + .unwrap(); + + ( + ident_ids, + subs, + expectations, + procs_base, + layout_cache, + module_timing, + ) + }; + + if module_id == ModuleId::DERIVED_GEN { + load_derived_partial_procs( + module_id, + arena, + &mut subs, + &mut ident_ids, + &state.derived_module, + &mut module_timing, + state.target_info, + &state.exposed_types, + &mut procs_base, + &mut state.world_abilities, + ); + } + + let derived_module = SharedDerivedModule::clone(&state.derived_module); + + BuildTask::MakeSpecializations { + module_id, + ident_ids, + subs, + procs_base, + layout_cache, + specializations_we_must_make, + module_timing, + world_abilities: state.world_abilities.clone_ref(), + // TODO: awful, how can we get rid of the clone? + exposed_by_module: state.exposed_types.clone(), + derived_module, + expectations, + } + } + } + }; + + vec![task] +} + +/// Values used to render expect output +pub struct ExpectMetadata<'a> { + pub interns: Interns, + pub layout_interner: STLayoutInterner<'a>, + pub expectations: VecMap, +} + +type LocExpects = VecMap>; +type LocDbgs = VecMap; + +/// A message sent out _from_ a worker thread, +/// representing a result of work done, or a request for further work +#[derive(Debug)] +enum Msg<'a> { + Many(Vec>), + Header(ModuleHeader<'a>), + Parsed(ParsedModule<'a>), + CanonicalizedAndConstrained(CanAndCon), + SolvedTypes { + module_id: ModuleId, + ident_ids: IdentIds, + solved_module: SolvedModule, + solved_subs: Solved, + decls: Declarations, + dep_idents: IdentIdsByModule, + module_timing: ModuleTiming, + abilities_store: AbilitiesStore, + loc_expects: LocExpects, + loc_dbgs: LocDbgs, + + #[cfg(debug_assertions)] + checkmate: Option, + }, + FinishedAllTypeChecking { + solved_subs: Solved, + exposed_vars_by_symbol: Vec<(Symbol, Variable)>, + exposed_aliases_by_symbol: MutMap, + exposed_types_storage: ExposedTypesStorageSubs, + resolved_implementations: ResolvedImplementations, + dep_idents: IdentIdsByModule, + documentation: VecMap, + abilities_store: AbilitiesStore, + + #[cfg(debug_assertions)] + checkmate: Option, + }, + FoundSpecializations { + module_id: ModuleId, + ident_ids: IdentIds, + layout_cache: LayoutCache<'a>, + procs_base: ProcsBase<'a>, + solved_subs: Solved, + module_timing: ModuleTiming, + abilities_store: AbilitiesStore, + toplevel_expects: ToplevelExpects, + expectations: Option, + }, + MadeSpecializations { + module_id: ModuleId, + ident_ids: IdentIds, + layout_cache: LayoutCache<'a>, + external_specializations_requested: BumpMap>, + procs_base: ProcsBase<'a>, + procedures: MutMap<(Symbol, ProcLayout<'a>), Proc<'a>>, + host_exposed_lambda_sets: HostExposedLambdaSets<'a>, + update_mode_ids: UpdateModeIds, + module_timing: ModuleTiming, + subs: Subs, + expectations: Option, + }, + + /// The task is to only typecheck AND monomorphize modules + /// all modules are now monomorphized, we are done + FinishedAllSpecialization { + subs: Subs, + /// The layout interner after all passes in mono are done. + /// DO NOT use the one on state; that is left in an empty state after specialization is complete! + layout_interner: STLayoutInterner<'a>, + exposed_to_host: ExposedToHost, + module_expectations: VecMap, + }, + + FailedToParse(FileError<'a, SyntaxError<'a>>), + FailedToReadFile { + filename: PathBuf, + error: io::ErrorKind, + }, + + FailedToLoad(LoadingProblem<'a>), + IncorrectModuleName(FileError<'a, IncorrectModuleName<'a>>), +} + +#[derive(Debug)] +struct CanAndCon { + constrained_module: ConstrainedModule, + canonicalization_problems: Vec, + module_docs: Option, +} + +#[derive(Debug)] +enum PlatformPath<'a> { + NotSpecified, + Valid(To<'a>), + RootIsInterface, + RootIsHosted, + RootIsPlatformModule, +} + +#[derive(Debug)] +struct PlatformData<'a> { + module_id: ModuleId, + provides: &'a [(Loc>, Loc>)], + is_prebuilt: bool, +} + +#[derive(Debug, Clone, Copy)] +enum MakeSpecializationsPass { + Pass(u8), +} + +impl MakeSpecializationsPass { + fn inc(&mut self) { + match self { + &mut Self::Pass(n) => { + *self = Self::Pass( + n.checked_add(1) + .expect("way too many specialization passes!"), + ) + } + } + } + + fn current_pass(&self) -> u8 { + match self { + MakeSpecializationsPass::Pass(n) => *n, + } + } +} + +#[derive(Debug)] +struct State<'a> { + pub root_id: ModuleId, + pub root_subs: Option, + pub cache_dir: PathBuf, + /// If the root is an app module, the shorthand specified in its header's `to` field + pub opt_platform_shorthand: Option<&'a str>, + pub platform_data: Option>, + pub exposed_types: ExposedByModule, + pub platform_path: PlatformPath<'a>, + pub target_info: TargetInfo, + pub(self) function_kind: FunctionKind, + + /// Note: only packages and platforms actually expose any modules; + /// for all others, this will be empty. + pub exposed_modules: &'a [ModuleId], + + pub module_cache: ModuleCache<'a>, + pub dependencies: Dependencies<'a>, + pub procedures: MutMap<(Symbol, ProcLayout<'a>), Proc<'a>>, + pub host_exposed_lambda_sets: HostExposedLambdaSets<'a>, + pub toplevel_expects: ToplevelExpects, + pub exposed_to_host: ExposedToHost, + + /// This is the "final" list of IdentIds, after canonicalization and constraint gen + /// have completed for a given module. + pub constrained_ident_ids: IdentIdsByModule, + + /// From now on, these will be used by multiple threads; time to make an Arc>! + pub arc_modules: Arc>>, + pub arc_shorthands: Arc>>, + pub derived_module: SharedDerivedModule, + + pub ident_ids_by_module: SharedIdentIdsByModule, + + pub declarations_by_id: MutMap, + + pub exposed_symbols_by_module: MutMap>, + + pub timings: MutMap, + + // Each thread gets its own layout cache. When one "pending specializations" + // pass completes, it returns its layout cache so another thread can use it. + // We don't bother trying to union them all together to maximize cache hits, + // since the unioning process could potentially take longer than the savings. + // (Granted, this has not been attempted or measured!) + pub layout_caches: std::vec::Vec>, + + pub render: RenderTarget, + pub palette: Palette, + pub exec_mode: ExecutionMode, + + /// All abilities across all modules. + pub world_abilities: WorldAbilities, + + make_specializations_pass: MakeSpecializationsPass, + + // cached types (used for builtin modules, could include packages in the future too) + cached_types: CachedTypeState, + + layout_interner: GlobalLayoutInterner<'a>, +} + +type CachedTypeState = Arc>>; + +impl<'a> State<'a> { + fn goal_phase(&self) -> Phase { + self.exec_mode.goal_phase() + } + + fn new( + root_id: ModuleId, + opt_platform_shorthand: Option<&'a str>, + target_info: TargetInfo, + function_kind: FunctionKind, + exposed_types: ExposedByModule, + arc_modules: Arc>>, + ident_ids_by_module: SharedIdentIdsByModule, + cached_types: MutMap, + render: RenderTarget, + palette: Palette, + number_of_workers: usize, + exec_mode: ExecutionMode, + ) -> Self { + let arc_shorthands = Arc::new(Mutex::new(MutMap::default())); + let cache_dir = roc_packaging::cache::roc_cache_dir(); + let dependencies = Dependencies::new(exec_mode.goal_phase()); + + Self { + root_id, + root_subs: None, + opt_platform_shorthand, + cache_dir, + target_info, + function_kind, + platform_data: None, + platform_path: PlatformPath::NotSpecified, + module_cache: ModuleCache::default(), + dependencies, + procedures: MutMap::default(), + host_exposed_lambda_sets: std::vec::Vec::new(), + toplevel_expects: ToplevelExpects::default(), + exposed_to_host: ExposedToHost::default(), + exposed_modules: &[], + exposed_types, + arc_modules, + arc_shorthands, + derived_module: Default::default(), + constrained_ident_ids: IdentIds::exposed_builtins(0), + ident_ids_by_module, + declarations_by_id: MutMap::default(), + exposed_symbols_by_module: MutMap::default(), + timings: MutMap::default(), + layout_caches: std::vec::Vec::with_capacity(number_of_workers), + cached_types: Arc::new(Mutex::new(cached_types)), + render, + palette, + exec_mode, + make_specializations_pass: MakeSpecializationsPass::Pass(1), + world_abilities: Default::default(), + layout_interner: GlobalLayoutInterner::with_capacity(128, target_info), + } + } +} + +fn report_timing( + buf: &mut impl std::fmt::Write, + label: &str, + duration: Duration, +) -> std::fmt::Result { + writeln!( + buf, + " {:9.3} ms {}", + duration.as_secs_f64() * 1000.0, + label, + ) +} + +impl std::fmt::Display for ModuleTiming { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let module_timing = self; + + report_timing(f, "Read .roc file from disk", module_timing.read_roc_file)?; + report_timing(f, "Parse header", module_timing.parse_header)?; + report_timing(f, "Parse body", module_timing.parse_body)?; + report_timing(f, "Canonicalize", module_timing.canonicalize)?; + report_timing(f, "Constrain", module_timing.constrain)?; + report_timing(f, "Solve", module_timing.solve)?; + report_timing( + f, + "Find Specializations", + module_timing.find_specializations, + )?; + let multiple_make_specializations_passes = module_timing.make_specializations.len() > 1; + for (i, pass_time) in module_timing.make_specializations.iter().enumerate() { + let suffix = if multiple_make_specializations_passes { + format!(" (Pass {i})") + } else { + String::new() + }; + report_timing(f, &format!("Make Specializations{suffix}"), *pass_time)?; + } + report_timing(f, "Other", module_timing.other())?; + f.write_str("\n")?; + report_timing(f, "Total", module_timing.total())?; + + Ok(()) + } +} + +/// A message sent _to_ a worker thread, describing the work to be done +#[derive(Debug)] +#[allow(dead_code)] +enum BuildTask<'a> { + LoadModule { + module_name: PQModuleName<'a>, + module_ids: Arc>>, + shorthands: Arc>>, + ident_ids_by_module: SharedIdentIdsByModule, + }, + Parse { + header: ModuleHeader<'a>, + }, + CanonicalizeAndConstrain { + parsed: ParsedModule<'a>, + module_ids: ModuleIds, + dep_idents: IdentIdsByModule, + exposed_symbols: VecSet, + aliases: MutMap, + abilities_store: PendingAbilitiesStore, + exposed_module_ids: &'a [ModuleId], + skip_constraint_gen: bool, + }, + Solve { + module: Module, + ident_ids: IdentIds, + exposed_for_module: ExposedForModule, + module_timing: ModuleTiming, + types: Types, + constraints: Constraints, + constraint: ConstraintSoa, + function_kind: FunctionKind, + pending_derives: PendingDerives, + var_store: VarStore, + declarations: Declarations, + dep_idents: IdentIdsByModule, + cached_subs: CachedTypeState, + derived_module: SharedDerivedModule, + + #[cfg(debug_assertions)] + checkmate: Option, + }, + BuildPendingSpecializations { + module_timing: ModuleTiming, + layout_cache: LayoutCache<'a>, + solved_subs: Solved, + imported_module_thunks: &'a [Symbol], + module_id: ModuleId, + ident_ids: IdentIds, + decls: Declarations, + exposed_to_host: ExposedToHost, + exposed_by_module: ExposedByModule, + abilities_store: AbilitiesStore, + derived_module: SharedDerivedModule, + expectations: Option, + build_expects: bool, + }, + MakeSpecializations { + module_id: ModuleId, + ident_ids: IdentIds, + subs: Subs, + procs_base: ProcsBase<'a>, + layout_cache: LayoutCache<'a>, + specializations_we_must_make: Vec>, + module_timing: ModuleTiming, + exposed_by_module: ExposedByModule, + world_abilities: WorldAbilities, + derived_module: SharedDerivedModule, + expectations: Option, + }, +} + +#[derive(Debug)] +enum WorkerMsg { + Shutdown, + TaskAdded, +} + +#[derive(Debug)] +pub struct IncorrectModuleName<'a> { + pub module_id: ModuleId, + pub found: Loc>, + pub expected: PQModuleName<'a>, +} + +#[derive(Debug)] +pub enum LoadingProblem<'a> { + FileProblem { + filename: PathBuf, + error: io::ErrorKind, + }, + ParsingFailed(FileError<'a, SyntaxError<'a>>), + UnexpectedHeader(String), + + ErrJoiningWorkerThreads, + TriedToImportAppModule, + + /// a formatted report + FormattedReport(String), + + ImportCycle(PathBuf, Vec), + IncorrectModuleName(FileError<'a, IncorrectModuleName<'a>>), + CouldNotFindCacheDir, + ChannelProblem(ChannelProblem), +} + +#[derive(Debug)] +pub enum ChannelProblem { + FailedToEnqueueTask(Box), + FailedToSendRootMsg, + FailedToSendWorkerShutdownMsg, + ChannelDisconnected, + FailedToSendManyMsg, + FailedToSendFinishedSpecializationsMsg, + FailedToSendTaskMsg, + FailedToSendFinishedTypeCheckingMsg, +} + +#[derive(Debug)] +pub struct PanicReportInfo { + can_problems: MutMap>, + type_problems: MutMap>, + sources: MutMap)>, + interns: Interns, +} + +pub enum Phases { + /// Parse, canonicalize, check types + TypeCheck, + /// Parse, canonicalize, check types, monomorphize + Monomorphize, +} + +type MsgSender<'a> = Sender>; + +/// Add a task to the queue, and notify all the listeners. +fn enqueue_task<'a>( + injector: &Injector>, + listeners: &[Sender], + task: BuildTask<'a>, + state: &State<'a>, +) -> Result<(), LoadingProblem<'a>> { + injector.push(task); + + for listener in listeners { + listener.send(WorkerMsg::TaskAdded).map_err(|_| { + let module_ids = { (*state.arc_modules).lock().clone() }.into_module_ids(); + + let interns = Interns { + module_ids, + all_ident_ids: state.constrained_ident_ids.clone(), + }; + + LoadingProblem::ChannelProblem(ChannelProblem::FailedToEnqueueTask(Box::new( + PanicReportInfo { + can_problems: state.module_cache.can_problems.clone(), + type_problems: state.module_cache.type_problems.clone(), + interns, + sources: state + .module_cache + .sources + .iter() + .map(|(key, (path, str_ref))| { + (*key, (path.clone(), str_ref.to_string().into_boxed_str())) + }) + .collect(), + }, + ))) + })?; + } + + Ok(()) +} + +pub fn load_and_typecheck_str<'a>( + arena: &'a Bump, + filename: PathBuf, + source: &'a str, + src_dir: PathBuf, + exposed_types: ExposedByModule, + target_info: TargetInfo, + function_kind: FunctionKind, + render: RenderTarget, + palette: Palette, + roc_cache_dir: RocCacheDir<'_>, + threading: Threading, +) -> Result> { + use LoadResult::*; + + let load_start = LoadStart::from_str(arena, filename, source, roc_cache_dir, src_dir)?; + + // this function is used specifically in the case + // where we want to regenerate the cached data + let cached_subs = MutMap::default(); + + let load_config = LoadConfig { + target_info, + render, + palette, + threading, + exec_mode: ExecutionMode::Check, + function_kind, + }; + + match load( + arena, + load_start, + exposed_types, + cached_subs, + roc_cache_dir, + load_config, + )? { + Monomorphized(_) => unreachable!(), + TypeChecked(module) => Ok(module), + } +} + +#[derive(Clone, Copy)] +pub enum PrintTarget { + ColorTerminal, + Generic, +} + +pub struct LoadStart<'a> { + arc_modules: Arc>>, + ident_ids_by_module: SharedIdentIdsByModule, + root_id: ModuleId, + opt_platform_shorthand: Option<&'a str>, + root_msg: Msg<'a>, + src_dir: PathBuf, +} + +impl<'a> LoadStart<'a> { + pub fn from_path( + arena: &'a Bump, + filename: PathBuf, + render: RenderTarget, + roc_cache_dir: RocCacheDir<'_>, + palette: Palette, + ) -> Result> { + let arc_modules = Arc::new(Mutex::new(PackageModuleIds::default())); + let root_exposed_ident_ids = IdentIds::exposed_builtins(0); + let ident_ids_by_module = Arc::new(Mutex::new(root_exposed_ident_ids)); + let mut src_dir = filename.parent().unwrap().to_path_buf(); + + // Load the root module synchronously; we can't proceed until we have its id. + let header_output = { + let root_start_time = Instant::now(); + + let res_loaded = load_filename( + arena, + filename, + true, + None, + None, + Arc::clone(&arc_modules), + Arc::clone(&ident_ids_by_module), + roc_cache_dir, + root_start_time, + ); + + match res_loaded { + Ok(header_output) => adjust_header_paths(header_output, &mut src_dir), + + Err(problem) => { + let module_ids = Arc::try_unwrap(arc_modules) + .unwrap_or_else(|_| { + panic!("There were still outstanding Arc references to module_ids") + }) + .into_inner() + .into_module_ids(); + + let report = report_loading_problem(problem, module_ids, render, palette); + + // TODO try to gracefully recover and continue + // instead of changing the control flow to exit. + return Err(LoadingProblem::FormattedReport(report)); + } + } + }; + + Ok(LoadStart { + arc_modules, + ident_ids_by_module, + src_dir, + root_id: header_output.module_id, + root_msg: header_output.msg, + opt_platform_shorthand: header_output.opt_platform_shorthand, + }) + } + + pub fn from_str( + arena: &'a Bump, + filename: PathBuf, + src: &'a str, + roc_cache_dir: RocCacheDir<'_>, + mut src_dir: PathBuf, + ) -> Result> { + let arc_modules = Arc::new(Mutex::new(PackageModuleIds::default())); + let root_exposed_ident_ids = IdentIds::exposed_builtins(0); + let ident_ids_by_module = Arc::new(Mutex::new(root_exposed_ident_ids)); + + // Load the root module synchronously; we can't proceed until we have its id. + let HeaderOutput { + module_id: root_id, + msg: root_msg, + opt_platform_shorthand: opt_platform_id, + } = { + let root_start_time = Instant::now(); + + let header_output = load_from_str( + arena, + filename, + src, + Arc::clone(&arc_modules), + Arc::clone(&ident_ids_by_module), + roc_cache_dir, + root_start_time, + )?; + + adjust_header_paths(header_output, &mut src_dir) + }; + + Ok(LoadStart { + arc_modules, + src_dir, + ident_ids_by_module, + root_id, + root_msg, + opt_platform_shorthand: opt_platform_id, + }) + } +} + +fn adjust_header_paths<'a>( + header_output: HeaderOutput<'a>, + src_dir: &mut PathBuf, +) -> HeaderOutput<'a> { + if let Msg::Header(ModuleHeader { + module_id: header_id, + header_type, + .. + }) = &header_output.msg + { + debug_assert_eq!(*header_id, header_output.module_id); + + if let HeaderType::Interface { name, .. } = header_type { + // Interface modules can have names like Foo.Bar.Baz, + // in which case we need to adjust the src_dir to + // remove the "Bar/Baz" directories in order to correctly + // resolve this interface module's imports! + let dirs_to_pop = name.as_str().matches('.').count(); + + for _ in 0..dirs_to_pop { + src_dir.pop(); + } + } + } + + header_output +} + +pub enum LoadResult<'a> { + TypeChecked(LoadedModule), + Monomorphized(MonomorphizedModule<'a>), +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum Threading { + Single, + AllAvailable, + AtMost(usize), +} + +/// The loading process works like this, starting from the given filename (e.g. "main.roc"): +/// +/// 1. Open the file. +/// 2. Parse the module's header. +/// 3. For each of its imports, send a message on the channel to the coordinator thread, which +/// will repeat this process to load that module - starting with step 1. +/// 4. Add everything we were able to import unqualified to the module's default scope. +/// 5. Parse the module's defs. +/// 6. Canonicalize the module. +/// 7. Before type checking, block on waiting for type checking to complete on all imports. +/// (Since Roc doesn't allow cyclic dependencies, this cannot deadlock.) +/// 8. Type check the module and create type annotations for its top-level declarations. +/// 9. Report the completed type annotation to the coordinator thread, so other modules +/// that are waiting in step 7 can unblock. +/// +/// The loaded_modules argument specifies which modules have already been loaded. +/// It typically contains *at least* the standard modules, but is empty when loading +/// the standard modules themselves. +/// +/// If we're just type-checking everything (e.g. running `roc check` at the command line), +/// we can stop there. However, if we're generating code, then there are additional steps. +/// +/// 10. After reporting the completed type annotation, we have all the information necessary +/// to monomorphize. However, since we want to monomorphize in parallel without +/// duplicating work, we do monomorphization in two steps. First, we go through and +/// determine all the specializations this module *wants*. We compute the hashes +/// and report them to the coordinator thread, along with the mono::expr::Expr values of +/// the current function's body. At this point, we have not yet begun to assemble Procs; +/// all we've done is send a list of requested specializations to the coordinator. +/// 11. The coordinator works through the specialization requests in parallel, adding them +/// to a global map once they're finished. Performing one specialization may result +/// in requests for others; these are added to the queue and worked through as normal. +/// This process continues until *both* all modules have reported that they've finished +/// adding specialization requests to the queue, *and* the queue is empty (including +/// of any requests that were added in the course of completing other requests). Now +/// we have a map of specializations, and everything was assembled in parallel with +/// no unique specialization ever getting assembled twice (meaning no wasted effort). +/// +/// a. Note that this might mean that we have to specialize certain modules multiple times. +/// When might this happen? Well, abilities can introduce implicit edges in the dependency +/// graph, and even cycles. For example, suppose module Ab provides "ability1" and a function +/// "f" that uses "ability1", and module App implements "ability1" and calls "f" with the +/// implementing type. Then the specialization of "Ab#f" depends on the specialization of +/// "ability1" back in the App module. +/// 12. Now that we have our final map of specializations, we can proceed to code gen! +/// As long as the specializations are stored in a per-ModuleId map, we can also +/// parallelize this code gen. (e.g. in dev builds, building separate LLVM modules +/// and then linking them together, and possibly caching them by the hash of their +/// specializations, so if none of their specializations changed, we don't even need +/// to rebuild the module and can link in the cached one directly.) +pub fn load<'a>( + arena: &'a Bump, + load_start: LoadStart<'a>, + exposed_types: ExposedByModule, + cached_types: MutMap, + roc_cache_dir: RocCacheDir<'_>, + load_config: LoadConfig, +) -> Result, LoadingProblem<'a>> { + enum Threads { + Single, + Many(usize), + } + + let threads = { + if cfg!(target_family = "wasm") { + // When compiling to wasm, we cannot spawn extra threads + // so we have a single-threaded implementation + Threads::Single + } else { + match std::thread::available_parallelism().map(|v| v.get()) { + Err(_) => Threads::Single, + Ok(0) => unreachable!("NonZeroUsize"), + Ok(1) => Threads::Single, + Ok(reported) => match load_config.threading { + Threading::Single => Threads::Single, + Threading::AllAvailable => Threads::Many(reported), + Threading::AtMost(at_most) => Threads::Many(Ord::min(reported, at_most)), + }, + } + } + }; + + match threads { + Threads::Single => load_single_threaded( + arena, + load_start, + exposed_types, + load_config.target_info, + load_config.function_kind, + cached_types, + load_config.render, + load_config.palette, + load_config.exec_mode, + roc_cache_dir, + ), + Threads::Many(threads) => load_multi_threaded( + arena, + load_start, + exposed_types, + load_config.target_info, + load_config.function_kind, + cached_types, + load_config.render, + load_config.palette, + threads, + load_config.exec_mode, + roc_cache_dir, + ), + } +} + +/// Load using only a single thread; used when compiling to webassembly +pub fn load_single_threaded<'a>( + arena: &'a Bump, + load_start: LoadStart<'a>, + exposed_types: ExposedByModule, + target_info: TargetInfo, + function_kind: FunctionKind, + cached_types: MutMap, + render: RenderTarget, + palette: Palette, + exec_mode: ExecutionMode, + roc_cache_dir: RocCacheDir<'_>, +) -> Result, LoadingProblem<'a>> { + let LoadStart { + arc_modules, + ident_ids_by_module, + root_id, + root_msg, + src_dir, + opt_platform_shorthand, + .. + } = load_start; + + let (msg_tx, msg_rx) = bounded(1024); + + msg_tx + .send(root_msg) + .map_err(|_| LoadingProblem::ChannelProblem(ChannelProblem::FailedToSendRootMsg))?; + + let number_of_workers = 1; + let mut state = State::new( + root_id, + opt_platform_shorthand, + target_info, + function_kind, + exposed_types, + arc_modules, + ident_ids_by_module, + cached_types, + render, + palette, + number_of_workers, + exec_mode, + ); + + // We'll add tasks to this, and then worker threads will take tasks from it. + let injector = Injector::new(); + + let (worker_msg_tx, worker_msg_rx) = bounded(1024); + let worker_listener = worker_msg_tx; + let worker_listeners = arena.alloc([worker_listener]); + + let worker = Worker::new_fifo(); + let stealer = worker.stealer(); + let stealers = &[stealer]; + + // now we just manually interleave stepping the state "thread" and the worker "thread" + loop { + match state_thread_step( + arena, + state, + &src_dir, + worker_listeners, + &injector, + &msg_tx, + &msg_rx, + ) { + Ok(ControlFlow::Break(done)) => return Ok(done), + Ok(ControlFlow::Continue(new_state)) => { + state = new_state; + } + Err(e) => return Err(e), + } + + // then check if the worker can step + let control_flow = worker_task_step( + arena, + &worker, + &injector, + stealers, + &worker_msg_rx, + &msg_tx, + &src_dir, + roc_cache_dir, + target_info, + ); + + match control_flow { + Ok(ControlFlow::Break(())) => panic!("the worker should not break!"), + Ok(ControlFlow::Continue(())) => { + // progress was made + } + Err(e) => return Err(e), + } + } +} + +fn state_thread_step<'a>( + arena: &'a Bump, + state: State<'a>, + src_dir: &Path, + worker_listeners: &'a [Sender], + injector: &Injector>, + msg_tx: &crossbeam::channel::Sender>, + msg_rx: &crossbeam::channel::Receiver>, +) -> Result, State<'a>>, LoadingProblem<'a>> { + match msg_rx.try_recv() { + Ok(msg) => { + match msg { + Msg::FinishedAllTypeChecking { + solved_subs, + exposed_vars_by_symbol, + exposed_aliases_by_symbol, + exposed_types_storage, + resolved_implementations, + dep_idents, + documentation, + abilities_store, + + #[cfg(debug_assertions)] + checkmate, + } => { + // We're done! There should be no more messages pending. + debug_assert!(msg_rx.is_empty()); + + let exposed_aliases_by_symbol = exposed_aliases_by_symbol + .into_iter() + .map(|(k, (_, v))| (k, v)) + .collect(); + + let typechecked = finish( + state, + solved_subs, + exposed_aliases_by_symbol, + exposed_vars_by_symbol, + exposed_types_storage, + resolved_implementations, + dep_idents, + documentation, + abilities_store, + // + #[cfg(debug_assertions)] + checkmate, + ); + + Ok(ControlFlow::Break(LoadResult::TypeChecked(typechecked))) + } + Msg::FinishedAllSpecialization { + subs, + layout_interner, + exposed_to_host, + module_expectations, + } => { + // We're done! There should be no more messages pending. + debug_assert!(msg_rx.is_empty()); + + let monomorphized = finish_specialization( + arena, + state, + subs, + layout_interner, + exposed_to_host, + module_expectations, + )?; + + Ok(ControlFlow::Break(LoadResult::Monomorphized(monomorphized))) + } + Msg::FailedToReadFile { filename, error } => { + let buf = to_file_problem_report_string(&filename, error); + Err(LoadingProblem::FormattedReport(buf)) + } + + Msg::FailedToParse(problem) => { + let module_ids = (*state.arc_modules).lock().clone().into_module_ids(); + let buf = to_parse_problem_report( + problem, + module_ids, + state.constrained_ident_ids, + state.render, + state.palette, + ); + Err(LoadingProblem::FormattedReport(buf)) + } + Msg::IncorrectModuleName(FileError { + problem: SourceError { problem, bytes }, + filename, + }) => { + let module_ids = (*state.arc_modules).lock().clone().into_module_ids(); + let buf = to_incorrect_module_name_report( + module_ids, + state.constrained_ident_ids, + problem, + filename, + bytes, + state.render, + ); + Err(LoadingProblem::FormattedReport(buf)) + } + msg => { + // This is where most of the main thread's work gets done. + // Everything up to this point has been setting up the threading + // system which lets this logic work efficiently. + let arc_modules = state.arc_modules.clone(); + + let render = state.render; + let palette = state.palette; + + let res_state = update( + state, + src_dir, + msg, + msg_tx.clone(), + injector, + worker_listeners, + arena, + ); + + match res_state { + Ok(new_state) => Ok(ControlFlow::Continue(new_state)), + Err(LoadingProblem::ParsingFailed(problem)) => { + let module_ids = Arc::try_unwrap(arc_modules) + .unwrap_or_else(|_| { + panic!( + r"There were still outstanding Arc references to module_ids" + ) + }) + .into_inner() + .into_module_ids(); + + // if parsing failed, this module did not add anything to IdentIds + let root_exposed_ident_ids = IdentIds::exposed_builtins(0); + let buf = to_parse_problem_report( + problem, + module_ids, + root_exposed_ident_ids, + render, + palette, + ); + Err(LoadingProblem::FormattedReport(buf)) + } + Err(LoadingProblem::ImportCycle(filename, cycle)) => { + let module_ids = arc_modules.lock().clone().into_module_ids(); + + let root_exposed_ident_ids = IdentIds::exposed_builtins(0); + let buf = to_import_cycle_report( + module_ids, + root_exposed_ident_ids, + cycle, + filename, + render, + ); + return Err(LoadingProblem::FormattedReport(buf)); + } + Err(LoadingProblem::IncorrectModuleName(FileError { + problem: SourceError { problem, bytes }, + filename, + })) => { + let module_ids = arc_modules.lock().clone().into_module_ids(); + + let root_exposed_ident_ids = IdentIds::exposed_builtins(0); + let buf = to_incorrect_module_name_report( + module_ids, + root_exposed_ident_ids, + problem, + filename, + bytes, + render, + ); + return Err(LoadingProblem::FormattedReport(buf)); + } + Err(e) => Err(e), + } + } + } + } + Err(err) => match err { + crossbeam::channel::TryRecvError::Empty => Ok(ControlFlow::Continue(state)), + crossbeam::channel::TryRecvError::Disconnected => Err(LoadingProblem::ChannelProblem( + ChannelProblem::ChannelDisconnected, + )), + }, + } +} + +pub fn report_loading_problem( + problem: LoadingProblem<'_>, + module_ids: ModuleIds, + render: RenderTarget, + palette: Palette, +) -> String { + match problem { + LoadingProblem::ParsingFailed(problem) => { + // if parsing failed, this module did not add anything to IdentIds + let root_exposed_ident_ids = IdentIds::exposed_builtins(0); + + to_parse_problem_report(problem, module_ids, root_exposed_ident_ids, render, palette) + } + LoadingProblem::ImportCycle(filename, cycle) => { + let root_exposed_ident_ids = IdentIds::exposed_builtins(0); + + to_import_cycle_report(module_ids, root_exposed_ident_ids, cycle, filename, render) + } + LoadingProblem::IncorrectModuleName(FileError { + problem: SourceError { problem, bytes }, + filename, + }) => { + let root_exposed_ident_ids = IdentIds::exposed_builtins(0); + + to_incorrect_module_name_report( + module_ids, + root_exposed_ident_ids, + problem, + filename, + bytes, + render, + ) + } + LoadingProblem::FormattedReport(report) => report, + LoadingProblem::FileProblem { filename, error } => { + to_file_problem_report_string(&filename, error) + } + err => todo!("Loading error: {:?}", err), + } +} + +fn load_multi_threaded<'a>( + arena: &'a Bump, + load_start: LoadStart<'a>, + exposed_types: ExposedByModule, + target_info: TargetInfo, + function_kind: FunctionKind, + cached_types: MutMap, + render: RenderTarget, + palette: Palette, + available_threads: usize, + exec_mode: ExecutionMode, + roc_cache_dir: RocCacheDir<'_>, +) -> Result, LoadingProblem<'a>> { + let LoadStart { + arc_modules, + ident_ids_by_module, + root_id, + root_msg, + src_dir, + opt_platform_shorthand, + .. + } = load_start; + + let (msg_tx, msg_rx) = bounded(1024); + msg_tx + .send(root_msg) + .map_err(|_| LoadingProblem::ChannelProblem(ChannelProblem::FailedToSendRootMsg))?; + + // Reserve one CPU for the main thread, and let all the others be eligible + // to spawn workers. + let available_workers = available_threads - 1; + + let num_workers = match env::var("ROC_NUM_WORKERS") { + Ok(env_str) => env_str + .parse::() + .unwrap_or(available_workers) + .min(available_workers), + Err(_) => available_workers, + }; + + assert!( + num_workers >= 1, + "`load_multi_threaded` needs at least one worker" + ); + + let mut state = State::new( + root_id, + opt_platform_shorthand, + target_info, + function_kind, + exposed_types, + arc_modules, + ident_ids_by_module, + cached_types, + render, + palette, + num_workers, + exec_mode, + ); + + // an arena for every worker, stored in an arena-allocated bumpalo vec to make the lifetimes work + let arenas = std::iter::repeat_with(Bump::new).take(num_workers); + let worker_arenas = arena.alloc(bumpalo::collections::Vec::from_iter_in(arenas, arena)); + + // We'll add tasks to this, and then worker threads will take tasks from it. + let injector = Injector::new(); + + // We need to allocate worker *queues* on the main thread and then move them + // into the worker threads, because those workers' stealers need to be + // shared between all threads, and this coordination work is much easier + // on the main thread. + let mut worker_queues = bumpalo::collections::Vec::with_capacity_in(num_workers, arena); + let mut stealers = bumpalo::collections::Vec::with_capacity_in(num_workers, arena); + + for _ in 0..num_workers { + let worker = Worker::new_fifo(); + + stealers.push(worker.stealer()); + worker_queues.push(worker); + } + + // Get a reference to the completed stealers, so we can send that + // reference to each worker. (Slices are Sync, but bumpalo Vecs are not.) + let stealers = stealers.into_bump_slice(); + let it = worker_arenas.iter_mut(); + + let mut can_problems_recorded = MutMap::default(); + let mut type_problems_recorded = MutMap::default(); + let mut sources_recorded = MutMap::default(); + let mut interns_recorded = Interns::default(); + + { + let thread_result = thread::scope(|thread_scope| { + let mut worker_listeners = + bumpalo::collections::Vec::with_capacity_in(num_workers, arena); + + for worker_arena in it { + let msg_tx = msg_tx.clone(); + let worker = worker_queues.pop().unwrap(); + + let (worker_msg_tx, worker_msg_rx) = bounded(1024); + worker_listeners.push(worker_msg_tx); + + // We only want to move a *reference* to the main task queue's + // injector in the thread, not the injector itself + // (since other threads need to reference it too). Same with src_dir. + let injector = &injector; + let src_dir = &src_dir; + + // Record this thread's handle so the main thread can join it later. + let res_join_handle = thread_scope + .builder() + .stack_size(EXPANDED_STACK_SIZE) + .spawn(move |_| { + // will process messages until we run out + worker_task( + worker_arena, + worker, + injector, + stealers, + worker_msg_rx, + msg_tx, + src_dir, + roc_cache_dir, + target_info, + ) + }); + + res_join_handle.unwrap_or_else(|_| { + panic!("Join handle panicked!"); + }); + } + + // We've now distributed one worker queue to each thread. + // There should be no queues left to distribute! + debug_assert!(worker_queues.is_empty()); + drop(worker_queues); + + // Grab a reference to these Senders outside the loop, so we can share + // it across each iteration of the loop. + let worker_listeners = worker_listeners.into_bump_slice(); + let msg_tx = msg_tx.clone(); + + macro_rules! shut_down_worker_threads { + () => { + for listener in worker_listeners { + // We intentionally don't propagate this Result, because even if + // shutting down a worker failed (which can happen if a a panic + // occurred on that thread), we want to continue shutting down + // the others regardless. + if listener.send(WorkerMsg::Shutdown).is_err() { + log!("There was an error trying to shutdown a worker thread. One reason this can happen is if the thread panicked."); + } + } + }; + } + + // The root module will have already queued up messages to process, + // and processing those messages will in turn queue up more messages. + loop { + match state_thread_step( + arena, + state, + &src_dir, + worker_listeners, + &injector, + &msg_tx, + &msg_rx, + ) { + Ok(ControlFlow::Break(load_result)) => { + shut_down_worker_threads!(); + + return Ok(load_result); + } + Ok(ControlFlow::Continue(new_state)) => { + state = new_state; + continue; + } + Err(LoadingProblem::ChannelProblem(ChannelProblem::FailedToEnqueueTask( + info, + ))) => { + let PanicReportInfo { + can_problems, + type_problems, + sources, + interns, + } = *info; + + // Record these for later. + can_problems_recorded = can_problems; + type_problems_recorded = type_problems; + sources_recorded = sources; + interns_recorded = interns; + + shut_down_worker_threads!(); + + return Err(LoadingProblem::ChannelProblem( + ChannelProblem::FailedToEnqueueTask(Box::new(PanicReportInfo { + // This return value never gets used, so don't bother + // cloning these in order to be able to return them. + // Really, anything could go here. + can_problems: Default::default(), + type_problems: Default::default(), + sources: Default::default(), + interns: Default::default(), + })), + )); + } + Err(e) => { + shut_down_worker_threads!(); + + return Err(e); + } + } + } + }); + + thread_result.unwrap_or_else(|_| { + // This most likely means a panic occurred in one of the threads. + // Therefore, print all the error info we've accumulated, and note afterwards + // that there was a compiler crash. + // + // Unfortunately, this often has no information to report if there's a panic in mono. + // Consequently, the following ends up being more misleading than helpful. + // + // roc_reporting::cli::report_problems( + // &sources_recorded, + // &mut interns_recorded, + // &mut can_problems_recorded, + // &mut type_problems_recorded, + // ) + // .print_to_stdout(Duration::default()); // TODO determine total elapsed time and use it here + + Err(LoadingProblem::FormattedReport( + concat!( + "\n\nThere was an unrecoverable error in the Roc compiler. The `roc check` ", + "command can sometimes give a more helpful error report than other commands.\n\n" + ) + .to_string(), + )) + }) + } +} + +fn worker_task_step<'a>( + worker_arena: &'a Bump, + worker: &Worker>, + injector: &Injector>, + stealers: &[Stealer>], + worker_msg_rx: &crossbeam::channel::Receiver, + msg_tx: &MsgSender<'a>, + src_dir: &Path, + roc_cache_dir: RocCacheDir<'_>, + target_info: TargetInfo, +) -> Result, LoadingProblem<'a>> { + match worker_msg_rx.try_recv() { + Ok(msg) => { + match msg { + WorkerMsg::Shutdown => { + // We've finished all our work. It's time to + // shut down the thread, so when the main thread + // blocks on joining with all the worker threads, + // it can finally exit too! + Ok(ControlFlow::Break(())) + } + WorkerMsg::TaskAdded => { + // Find a task - either from this thread's queue, + // or from the main queue, or from another worker's + // queue - and run it. + // + // There might be no tasks to work on! That could + // happen if another thread is working on a task + // which will later result in more tasks being + // added. In that case, do nothing, and keep waiting + // until we receive a Shutdown message. + if let Some(task) = find_task(worker, injector, stealers) { + let result = run_task( + task, + worker_arena, + src_dir, + msg_tx.clone(), + roc_cache_dir, + target_info, + ); + + match result { + Ok(()) => {} + Err(LoadingProblem::ChannelProblem(problem)) => { + panic!("Channel problem: {problem:?}"); + } + Err(LoadingProblem::ParsingFailed(problem)) => { + msg_tx.send(Msg::FailedToParse(problem)).unwrap(); + } + Err(LoadingProblem::FileProblem { filename, error }) => { + msg_tx + .send(Msg::FailedToReadFile { filename, error }) + .unwrap(); + } + Err(LoadingProblem::IncorrectModuleName(err)) => { + msg_tx.send(Msg::IncorrectModuleName(err)).unwrap(); + } + Err(other) => { + return Err(other); + } + } + } + + Ok(ControlFlow::Continue(())) + } + } + } + Err(err) => match err { + crossbeam::channel::TryRecvError::Empty => Ok(ControlFlow::Continue(())), + crossbeam::channel::TryRecvError::Disconnected => Ok(ControlFlow::Break(())), + }, + } +} + +fn worker_task<'a>( + worker_arena: &'a Bump, + worker: Worker>, + injector: &Injector>, + stealers: &[Stealer>], + worker_msg_rx: crossbeam::channel::Receiver, + msg_tx: MsgSender<'a>, + src_dir: &Path, + roc_cache_dir: RocCacheDir<'_>, + target_info: TargetInfo, +) -> Result<(), LoadingProblem<'a>> { + // Keep listening until we receive a Shutdown msg + for msg in worker_msg_rx.iter() { + match msg { + WorkerMsg::Shutdown => { + // We've finished all our work. It's time to + // shut down the thread, so when the main thread + // blocks on joining with all the worker threads, + // it can finally exit too! + return Ok(()); + } + WorkerMsg::TaskAdded => { + // Find a task - either from this thread's queue, + // or from the main queue, or from another worker's + // queue - and run it. + // + // There might be no tasks to work on! That could + // happen if another thread is working on a task + // which will later result in more tasks being + // added. In that case, do nothing, and keep waiting + // until we receive a Shutdown message. + if let Some(task) = find_task(&worker, injector, stealers) { + log!( + ">>> {}", + match &task { + BuildTask::LoadModule { module_name, .. } => { + format!("BuildTask::LoadModule({module_name:?})") + } + BuildTask::Parse { header } => { + format!("BuildTask::Parse({})", header.module_path.display()) + } + BuildTask::CanonicalizeAndConstrain { parsed, .. } => format!( + "BuildTask::CanonicalizeAndConstrain({})", + parsed.module_path.display() + ), + BuildTask::Solve { module, .. } => { + format!("BuildTask::Solve({:?})", module.module_id) + } + BuildTask::BuildPendingSpecializations { module_id, .. } => { + format!("BuildTask::BuildPendingSpecializations({module_id:?})") + } + BuildTask::MakeSpecializations { module_id, .. } => { + format!("BuildTask::MakeSpecializations({module_id:?})") + } + } + ); + + let result = run_task( + task, + worker_arena, + src_dir, + msg_tx.clone(), + roc_cache_dir, + target_info, + ); + + match result { + Ok(()) => {} + Err(LoadingProblem::ChannelProblem(problem)) => { + panic!("Channel problem: {problem:?}"); + } + Err(LoadingProblem::ParsingFailed(problem)) => { + msg_tx.send(Msg::FailedToParse(problem)).unwrap(); + } + Err(LoadingProblem::FileProblem { filename, error }) => { + msg_tx + .send(Msg::FailedToReadFile { filename, error }) + .unwrap(); + } + Err(LoadingProblem::IncorrectModuleName(err)) => { + msg_tx.send(Msg::IncorrectModuleName(err)).unwrap(); + } + Err(other) => { + return Err(other); + } + } + } + } + } + } + + Ok(()) +} + +fn start_tasks<'a>( + arena: &'a Bump, + state: &mut State<'a>, + work: MutSet<(ModuleId, Phase)>, + injector: &Injector>, + worker_listeners: &'a [Sender], +) -> Result<(), LoadingProblem<'a>> { + for (module_id, phase) in work { + let tasks = start_phase(module_id, phase, arena, state); + + for task in tasks { + enqueue_task(injector, worker_listeners, task, state)? + } + } + + Ok(()) +} + +macro_rules! debug_print_ir { + ($state:expr, $interner:expr, $flag:path) => { + dbg_do!($flag, { + let procs_string = $state + .procedures + .values() + .map(|proc| proc.to_pretty($interner, 200, true)) + .collect::>(); + + let result = procs_string.join("\n"); + + eprintln!("{}", result); + }) + }; +} + +macro_rules! debug_check_ir { + ($state:expr, $arena:expr, $interner:expr, $flag:path) => { + dbg_do!($flag, { + use roc_mono::debug::{check_procs, format_problems}; + + let interns = Interns { + module_ids: $state.arc_modules.lock().clone().into_module_ids(), + all_ident_ids: $state.constrained_ident_ids.clone(), + }; + + let procedures = &$state.procedures; + + let problems = check_procs($arena, &mut $interner, procedures); + if !problems.is_empty() { + let formatted = format_problems(&interns, &$interner, problems); + eprintln!("IR PROBLEMS FOUND:\n{formatted}"); + } + }) + }; +} + +/// Report modules that are imported, but from which nothing is used +fn report_unused_imported_modules( + state: &mut State<'_>, + module_id: ModuleId, + constrained_module: &ConstrainedModule, +) { + let mut unused_imported_modules = constrained_module.imported_modules.clone(); + let mut unused_imports = constrained_module.module.exposed_imports.clone(); + + for symbol in constrained_module.module.referenced_values.iter() { + unused_imported_modules.remove(&symbol.module_id()); + unused_imports.remove(symbol); + } + + for symbol in constrained_module.module.referenced_types.iter() { + unused_imported_modules.remove(&symbol.module_id()); + unused_imports.remove(symbol); + } + + let existing = match state.module_cache.can_problems.entry(module_id) { + Vacant(entry) => entry.insert(std::vec::Vec::new()), + Occupied(entry) => entry.into_mut(), + }; + + for (unused, region) in unused_imported_modules.drain() { + if !unused.is_builtin() { + existing.push(roc_problem::can::Problem::UnusedModuleImport( + unused, region, + )); + } + } + + for (unused, region) in unused_imports.drain() { + existing.push(roc_problem::can::Problem::UnusedImport(unused, region)); + } +} + +fn extend_header_with_builtin(header: &mut ModuleHeader, module: ModuleId) { + header + .package_qualified_imported_modules + .insert(PackageQualified::Unqualified(module)); + + header.imported_modules.insert(module, Region::zero()); + + let types = Symbol::builtin_types_in_scope(module) + .iter() + .map(|(name, info)| (Ident::from(*name), *info)); + header.exposed_imports.extend(types); +} + +fn update<'a>( + mut state: State<'a>, + src_dir: &Path, + msg: Msg<'a>, + msg_tx: MsgSender<'a>, + injector: &Injector>, + worker_listeners: &'a [Sender], + arena: &'a Bump, +) -> Result, LoadingProblem<'a>> { + use self::Msg::*; + + match msg { + Many(messages) => { + // enqueue all these message + for msg in messages { + msg_tx.send(msg).map_err(|_| { + LoadingProblem::ChannelProblem(ChannelProblem::FailedToSendManyMsg) + })?; + } + + Ok(state) + } + Header(header) => { + use HeaderType::*; + + log!("loaded header for {:?}", header.module_id); + let home = header.module_id; + + let mut work = MutSet::default(); + + // Register the package's path under its shorthand + // (e.g. for { pf: "blah" }, register that "pf" should resolve to "blah") + { + let mut shorthands = (*state.arc_shorthands).lock(); + + for (shorthand, package_name) in header.packages.iter() { + let package_str = package_name.as_str(); + let shorthand_path = if package_str.starts_with("https://") { + #[cfg(not(target_family = "wasm"))] + { + let url = package_str; + match PackageMetadata::try_from(url) { + Ok(url_metadata) => { + // This was a valid URL + let root_module_dir = state + .cache_dir + .join(url_metadata.cache_subdir) + .join(url_metadata.content_hash); + let root_module = root_module_dir.join( + url_metadata.root_module_filename.unwrap_or("main.roc"), + ); + + ShorthandPath::FromHttpsUrl { + root_module_dir, + root_module, + } + } + Err(url_err) => { + let buf = to_https_problem_report_string( + url, + Problem::InvalidUrl(url_err), + ); + return Err(LoadingProblem::FormattedReport(buf)); + } + } + } + + #[cfg(target_family = "wasm")] + { + panic!("Specifying packages via URLs is curently unsupported in wasm."); + } + } else { + // This wasn't a URL, so it must be a filesystem path. + let root_module: PathBuf = src_dir.join(package_str); + let root_module_dir = root_module.parent().unwrap_or_else(|| { + if root_module.is_file() { + // Files must have parents! + internal_error!("Somehow I got a file path to a real file on the filesystem that has no parent!"); + } else { + // TODO make this a nice report + todo!( + "platform module {:?} was not a file.", + package_str + ) + } + }).into(); + + ShorthandPath::RelativeToSrc { + root_module_dir, + root_module, + } + }; + + log!( + "New package shorthand: {:?} => {:?}", + shorthand, + shorthand_path + ); + + shorthands.insert(shorthand, shorthand_path); + } + + match header.header_type { + App { to_platform, .. } => { + state.platform_path = PlatformPath::Valid(to_platform); + } + Package { + config_shorthand, + exposes_ids, + .. + } => { + if header.is_root_module { + state.exposed_modules = exposes_ids; + } + + work.extend(state.dependencies.notify_package(config_shorthand)); + } + Platform { + config_shorthand, + provides, + exposes_ids, + .. + } => { + let undefined_shorthands: Vec<_> = header + .package_qualified_imported_modules + .iter() + .filter(|pqim| match pqim { + PackageQualified::Unqualified(_) => false, + PackageQualified::Qualified(shorthand, _) => { + !(header.packages.contains_key(shorthand) + || shorthand == &config_shorthand) + } + }) + .collect(); + + // shorthands must be defined by the module! + assert!( + undefined_shorthands.is_empty(), + "{undefined_shorthands:?} not in {:?} ", + &header.packages + ); + + work.extend(state.dependencies.notify_package(config_shorthand)); + + let is_prebuilt = if header.is_root_module { + debug_assert!(matches!( + state.platform_path, + PlatformPath::NotSpecified + )); + state.platform_path = PlatformPath::RootIsPlatformModule; + + // If the root module is this platform, then the platform is the very + // thing we're rebuilding! + false + } else { + // platforms from HTTPS URLs are always prebuilt + matches!( + shorthands.get(config_shorthand), + Some(ShorthandPath::FromHttpsUrl { .. }) + ) + }; + + // If we're building an app module, and this was the platform + // specified in its header's `to` field, record it as our platform. + if state.opt_platform_shorthand == Some(config_shorthand) { + debug_assert!(state.platform_data.is_none()); + + state.platform_data = Some(PlatformData { + module_id: header.module_id, + provides, + is_prebuilt, + }); + } + + if header.is_root_module { + state.exposed_modules = exposes_ids; + } + } + Builtin { .. } | Interface { .. } => { + if header.is_root_module { + debug_assert!(matches!( + state.platform_path, + PlatformPath::NotSpecified + )); + state.platform_path = PlatformPath::RootIsInterface; + } + } + Hosted { .. } => { + if header.is_root_module { + debug_assert!(matches!( + state.platform_path, + PlatformPath::NotSpecified + )); + state.platform_path = PlatformPath::RootIsHosted; + } + } + } + } + + // store an ID to name mapping, so we know the file to read when fetching dependencies' headers + for (name, id) in header.deps_by_name.iter() { + state.module_cache.module_names.insert(*id, name.clone()); + } + + // This was a dependency. Write it down and keep processing messages. + let mut exposed_symbols: VecSet = VecSet::with_capacity(header.exposes.len()); + + // TODO can we avoid this loop by storing them as a Set in Header to begin with? + for symbol in header.exposes.iter() { + exposed_symbols.insert(*symbol); + } + + // NOTE we currently re-parse the headers when a module is imported twice. + // We need a proper solution that marks a phase as in-progress so it's not repeated + // debug_assert!(!state.exposed_symbols_by_module.contains_key(&home)); + + state + .exposed_symbols_by_module + .insert(home, exposed_symbols); + + // add the prelude + let mut header = header; + + if !header.module_id.is_builtin() { + let header = &mut header; + + extend_header_with_builtin(header, ModuleId::NUM); + extend_header_with_builtin(header, ModuleId::BOOL); + extend_header_with_builtin(header, ModuleId::STR); + extend_header_with_builtin(header, ModuleId::LIST); + extend_header_with_builtin(header, ModuleId::RESULT); + extend_header_with_builtin(header, ModuleId::DICT); + extend_header_with_builtin(header, ModuleId::SET); + extend_header_with_builtin(header, ModuleId::BOX); + extend_header_with_builtin(header, ModuleId::ENCODE); + extend_header_with_builtin(header, ModuleId::DECODE); + extend_header_with_builtin(header, ModuleId::HASH); + extend_header_with_builtin(header, ModuleId::INSPECT); + } + + state + .module_cache + .imports + .entry(header.module_id) + .or_default() + .extend( + header + .package_qualified_imported_modules + .iter() + .map(|x| *x.as_inner()), + ); + + let added_deps_result = state.dependencies.add_module( + header.module_id, + &header.package_qualified_imported_modules, + state.exec_mode.goal_phase(), + ); + + let new_work = match added_deps_result { + Ok(work) => work, + Err(DepCycle { cycle }) => { + return Err(LoadingProblem::ImportCycle( + header.module_path.clone(), + cycle, + )); + } + }; + + work.extend(new_work); + + state.module_cache.headers.insert(header.module_id, header); + + start_tasks(arena, &mut state, work, injector, worker_listeners)?; + + let work = state.dependencies.notify(home, Phase::LoadHeader); + + start_tasks(arena, &mut state, work, injector, worker_listeners)?; + + Ok(state) + } + Parsed(parsed) => { + state + .module_cache + .sources + .insert(parsed.module_id, (parsed.module_path.clone(), parsed.src)); + + let module_id = parsed.module_id; + + state.module_cache.parsed.insert(module_id, parsed); + + let work = state.dependencies.notify(module_id, Phase::Parse); + + start_tasks(arena, &mut state, work, injector, worker_listeners)?; + + Ok(state) + } + + CanonicalizedAndConstrained(CanAndCon { + constrained_module, + canonicalization_problems, + module_docs, + }) => { + let module_id = constrained_module.module.module_id; + log!("generated constraints for {:?}", module_id); + state + .module_cache + .can_problems + .insert(module_id, canonicalization_problems); + + if let Some(docs) = module_docs { + state.module_cache.documentation.insert(module_id, docs); + } + + report_unused_imported_modules(&mut state, module_id, &constrained_module); + + state + .module_cache + .aliases + .insert(module_id, constrained_module.module.aliases.clone()); + + state + .module_cache + .pending_abilities + .insert(module_id, constrained_module.module.abilities_store.clone()); + + state + .module_cache + .constrained + .insert(module_id, constrained_module); + + let work = state + .dependencies + .notify(module_id, Phase::CanonicalizeAndConstrain); + + start_tasks(arena, &mut state, work, injector, worker_listeners)?; + + Ok(state) + } + SolvedTypes { + module_id, + ident_ids, + solved_module, + solved_subs, + decls, + dep_idents, + mut module_timing, + abilities_store, + loc_expects, + loc_dbgs, + + #[cfg(debug_assertions)] + checkmate, + } => { + log!("solved types for {:?}", module_id); + module_timing.end_time = Instant::now(); + + state + .module_cache + .type_problems + .insert(module_id, solved_module.problems); + + let should_include_expects = (!loc_expects.is_empty() || !loc_dbgs.is_empty()) && { + let modules = state.arc_modules.lock(); + modules + .package_eq(module_id, state.root_id) + .expect("root or this module is not yet known - that's a bug!") + }; + + let opt_expectations = if should_include_expects { + let (path, _) = state.module_cache.sources.get(&module_id).unwrap(); + + Some(Expectations { + expectations: loc_expects, + dbgs: loc_dbgs, + subs: solved_subs.clone().into_inner(), + path: path.to_owned(), + ident_ids: ident_ids.clone(), + }) + } else { + None + }; + + let work = state.dependencies.notify(module_id, Phase::SolveTypes); + + // if there is a platform, the `platform` module provides host-exposed, + // otherwise the App module exposes host-exposed + let is_host_exposed = match state.platform_data { + None => module_id == state.root_id, + Some(ref platform_data) => module_id == platform_data.module_id, + }; + + let add_to_host_exposed = is_host_exposed && + // During testing, we don't need to expose anything to the host. + !matches!(state.exec_mode, ExecutionMode::Test); + + if add_to_host_exposed { + state.exposed_to_host.top_level_values.extend( + solved_module + .exposed_vars_by_symbol + .iter() + .filter_map(|(k, v)| { + if abilities_store.is_specialization_name(*k) { + None + } else { + Some((*k, *v)) + } + }), + ); + + state + .exposed_to_host + .closure_types + .extend(solved_module.aliases.keys().copied()); + } + + let finish_type_checking = is_host_exposed && + (state.goal_phase() == Phase::SolveTypes) + // If we're running in check-and-then-build mode, only exit now there are errors. + && (!state.exec_mode.build_if_checks() || state.module_cache.has_errors()); + + if finish_type_checking { + debug_assert!(work.is_empty()); + debug_assert!(state.dependencies.solved_all()); + + state.timings.insert(module_id, module_timing); + + if state.exec_mode.build_if_checks() { + // We there may outstanding modules in the typecheked cache whose ident IDs + // aren't registered; transfer all of their idents over to the state, since + // we're now done and ready to report errors. + for ( + module_id, + TypeCheckedModule { + ident_ids, + module_timing, + .. + }, + ) in state.module_cache.typechecked.drain() + { + state.constrained_ident_ids.insert(module_id, ident_ids); + state.timings.insert(module_id, module_timing); + } + } + + let documentation = { + let mut empty = VecMap::default(); + std::mem::swap(&mut empty, &mut state.module_cache.documentation); + + empty + }; + + msg_tx + .send(Msg::FinishedAllTypeChecking { + solved_subs, + exposed_vars_by_symbol: solved_module.exposed_vars_by_symbol, + exposed_aliases_by_symbol: solved_module.aliases, + exposed_types_storage: solved_module.exposed_types, + resolved_implementations: solved_module.solved_implementations, + dep_idents, + documentation, + abilities_store, + + #[cfg(debug_assertions)] + checkmate, + }) + .map_err(|_| { + LoadingProblem::ChannelProblem( + ChannelProblem::FailedToSendFinishedTypeCheckingMsg, + ) + })?; + + // bookkeeping + state.declarations_by_id.insert(module_id, decls); + state.constrained_ident_ids.insert(module_id, ident_ids); + + // As far as type-checking goes, once we've solved + // the originally requested module, we're all done! + return Ok(state); + } else { + state.exposed_types.insert( + module_id, + ExposedModuleTypes { + exposed_types_storage_subs: solved_module.exposed_types, + resolved_implementations: solved_module.solved_implementations, + }, + ); + + if state.goal_phase() > Phase::SolveTypes || state.exec_mode.build_if_checks() { + let layout_cache = state.layout_caches.pop().unwrap_or_else(|| { + LayoutCache::new(state.layout_interner.fork(), state.target_info) + }); + + let typechecked = TypeCheckedModule { + module_id, + layout_cache, + module_timing, + solved_subs, + decls, + ident_ids, + abilities_store, + expectations: opt_expectations, + + #[cfg(debug_assertions)] + checkmate, + }; + + state + .module_cache + .typechecked + .insert(module_id, typechecked); + } else { + state.module_cache.checked.insert( + module_id, + CheckedModule { + solved_subs, + decls, + abilities_store, + }, + ); + state.constrained_ident_ids.insert(module_id, ident_ids); + state.timings.insert(module_id, module_timing); + } + + let work = if is_host_exposed && state.exec_mode.build_if_checks() { + debug_assert!( + work.is_empty(), + "work left over after host exposed is checked" + ); + + // Load the find + make specializations portion of the dependency graph. + state + .dependencies + .load_find_and_make_specializations_after_check() + } else { + work + }; + + start_tasks(arena, &mut state, work, injector, worker_listeners)?; + } + + Ok(state) + } + FoundSpecializations { + module_id, + procs_base, + solved_subs, + ident_ids, + layout_cache, + module_timing, + abilities_store, + toplevel_expects, + expectations, + } => { + log!("found specializations for {:?}", module_id); + + let subs = solved_subs.into_inner(); + + state.toplevel_expects.pure.extend(toplevel_expects.pure); + state.toplevel_expects.fx.extend(toplevel_expects.fx); + + state + .module_cache + .top_level_thunks + .entry(module_id) + .or_default() + .extend(procs_base.module_thunks.iter().copied()); + + let found_specializations_module = FoundSpecializationsModule { + ident_ids, + layout_cache, + procs_base, + subs, + module_timing, + abilities_store, + expectations, + }; + + state + .module_cache + .found_specializations + .insert(module_id, found_specializations_module); + + let work = state + .dependencies + .notify(module_id, Phase::FindSpecializations); + + start_tasks(arena, &mut state, work, injector, worker_listeners)?; + + Ok(state) + } + MadeSpecializations { + module_id, + ident_ids, + mut update_mode_ids, + subs, + procs_base, + procedures, + host_exposed_lambda_sets, + external_specializations_requested, + module_timing, + layout_cache, + expectations, + .. + } => { + debug_assert!( + state.goal_phase() == Phase::MakeSpecializations + || state.exec_mode.build_if_checks() + ); + + log!("made specializations for {:?}", module_id); + + // in the future, layouts will be in SoA form and we'll want to hold on to this data + let _ = layout_cache; + + state.procedures.extend(procedures); + state + .host_exposed_lambda_sets + .extend(host_exposed_lambda_sets); + state.module_cache.late_specializations.insert( + module_id, + LateSpecializationsModule { + ident_ids, + module_timing, + subs, + layout_cache, + procs_base, + expectations, + }, + ); + + let work = state + .dependencies + .notify(module_id, Phase::MakeSpecializations); + + for (module_id, requested) in external_specializations_requested { + let existing = match state + .module_cache + .external_specializations_requested + .entry(module_id) + { + Vacant(entry) => entry.insert(vec![]), + Occupied(entry) => entry.into_mut(), + }; + + existing.push(requested); + } + + enum NextStep { + Done, + RelaunchPhase, + MakingInPhase, + } + + let all_work_done = work.is_empty() && state.dependencies.solved_all(); + let next_step = if all_work_done { + if state + .module_cache + .external_specializations_requested + .is_empty() + { + NextStep::Done + } else { + NextStep::RelaunchPhase + } + } else { + NextStep::MakingInPhase + }; + + match next_step { + NextStep::Done => { + // We are all done with specializations across all modules. + // Insert post-specialization operations and report our completion. + + if !state + .module_cache + .external_specializations_requested + .is_empty() + { + internal_error!( + "No more work left, but external specializations left over: {:?}", + state.module_cache.external_specializations_requested + ); + } + + let mut module_expectations = + VecMap::with_capacity(state.module_cache.module_names.len()); + + // Flush late-specialization module information to the top-level of the state + // where it will be visible to others, since we don't need late specialization + // anymore. + for ( + module_id, + LateSpecializationsModule { + ident_ids, + subs, + module_timing, + layout_cache: _layout_cache, + procs_base: _, + expectations, + }, + ) in state.module_cache.late_specializations.drain() + { + state.constrained_ident_ids.insert(module_id, ident_ids); + if module_id == state.root_id { + state.root_subs = Some(subs); + } + state.timings.insert(module_id, module_timing); + if let Some(expectations) = expectations { + module_expectations.insert(module_id, expectations); + } + + #[cfg(debug_assertions)] + { + log_layout_stats(module_id, &_layout_cache); + } + } + + let layout_interner = { + let mut taken = GlobalLayoutInterner::with_capacity(0, state.target_info); + std::mem::swap(&mut state.layout_interner, &mut taken); + taken + }; + + #[allow(unused_mut)] + let mut layout_interner = layout_interner + .unwrap() + .expect("outstanding references to global layout interener, but we just drained all layout caches"); + + log!("specializations complete from {:?}", module_id); + + debug_print_ir!(state, &layout_interner, ROC_PRINT_IR_AFTER_SPECIALIZATION); + debug_check_ir!(state, arena, layout_interner, ROC_CHECK_MONO_IR); + + let ident_ids = state.constrained_ident_ids.get_mut(&module_id).unwrap(); + + roc_mono::tail_recursion::apply_trmc( + arena, + &mut layout_interner, + module_id, + ident_ids, + &mut state.procedures, + ); + + debug_print_ir!(state, &layout_interner, ROC_PRINT_IR_AFTER_TRMC); + + inc_dec::insert_inc_dec_operations( + arena, + &layout_interner, + &mut state.procedures, + ); + + debug_print_ir!(state, &layout_interner, ROC_PRINT_IR_AFTER_REFCOUNT); + + drop_specialization::specialize_drops( + arena, + &mut layout_interner, + module_id, + ident_ids, + &mut state.procedures, + ); + + debug_print_ir!( + state, + &layout_interner, + ROC_PRINT_IR_AFTER_DROP_SPECIALIZATION + ); + + reset_reuse::insert_reset_reuse_operations( + arena, + &layout_interner, + module_id, + state.target_info, + ident_ids, + &mut update_mode_ids, + &mut state.procedures, + ); + + debug_print_ir!(state, &layout_interner, ROC_PRINT_IR_AFTER_RESET_REUSE); + + // This is not safe with the new non-recursive RC updates that we do for tag unions + // + // Proc::optimize_refcount_operations( + // arena, + // module_id, + // &mut ident_ids, + // &mut state.procedures, + // ); + + // use the subs of the root module; + // this is used in the repl to find the type of `main` + let subs = state.root_subs.clone().unwrap(); + + msg_tx + .send(Msg::FinishedAllSpecialization { + subs, + layout_interner, + exposed_to_host: state.exposed_to_host.clone(), + module_expectations, + }) + .map_err(|_| { + LoadingProblem::ChannelProblem( + ChannelProblem::FailedToSendFinishedSpecializationsMsg, + ) + })?; + + Ok(state) + } + + NextStep::RelaunchPhase => { + // We passed through the dependency graph of modules to be specialized, but + // there are still specializations left over. Restart the make specializations + // phase in reverse topological order. + // + // This happens due to abilities. In detail, consider + // + // # Default module + // interface Default exposes [default, getDefault] + // + // Default implements default : {} -> a where a implements Default + // + // getDefault = \{} -> default {} + // + // # App module + // app "test" provides [main] imports [Default.{default, getDefault}] + // + // Foo := {} + // + // default = \{} -> @Foo {} + // + // main = + // f : Foo + // f = getDefault {} + // f + // + // The syntactic make specializations graph (based on imports) will be + // App -> Default, and in a pass will build the specializations `App#main` and + // `Default#getDefault for Foo`. But now notice that `Default#getDefault` will + // have gained an implicit dependency on the specialized `default` for `Foo`, + // `App#Foo#default`. So for abilities, the syntactic import graph is not + // enough to express the entire dependency graph. + // + // The simplest way to resolve these leftover, possibly circular + // specializations is to relaunch the make-specializations phase in the import + // order until there are no more specializations left to be made. This is a bit + // unfortunate in that we may look again into modules that don't need any + // specializations made, but there are also some nice properties: + // + // - no more specializations will be made than needed + // - the number of phase relaunches scales linearly with the largest number of + // "bouncing back and forth" between ability calls, which is likely to be + // small in practice + // - the phases will always terminate. suppose they didn't; then there must be + // an infinite chain of calls all of which have different layouts. In Roc + // this can only be true if the calls are all mutually recursive, and + // furthermore are polymorphically recursive. But polymorphic recursion is + // illegal in Roc, will have been enforced during type inference. + + if state + .module_cache + .external_specializations_requested + .is_empty() + { + internal_error!( + "No specializations left over, but we were told to loop making specializations" + ); + } + + log!( + "re-launching make-specializations: pass {}", + state.make_specializations_pass.current_pass() + 1 + ); + + state.make_specializations_pass.inc(); + + let work = state.dependencies.reload_make_specialization_pass(); + + start_tasks(arena, &mut state, work, injector, worker_listeners)?; + + Ok(state) + } + + NextStep::MakingInPhase => { + start_tasks(arena, &mut state, work, injector, worker_listeners)?; + + Ok(state) + } + } + } + Msg::FailedToLoad(problem) => { + // TODO report the error and continue instead of erroring out + Err(problem) + } + Msg::FinishedAllTypeChecking { .. } => { + unreachable!(); + } + Msg::FinishedAllSpecialization { .. } => { + unreachable!(); + } + Msg::FailedToParse(_) => { + unreachable!(); + } + Msg::FailedToReadFile { .. } => { + unreachable!(); + } + Msg::IncorrectModuleName(..) => { + internal_error!(); + } + } +} + +#[cfg(debug_assertions)] +fn log_layout_stats(module_id: ModuleId, layout_cache: &LayoutCache) { + let (cache_stats, raw_function_cache_stats) = layout_cache.statistics(); + roc_tracing::info!( + module = ?module_id, + insertions = cache_stats.insertions, + hits = cache_stats.hits, + misses = cache_stats.misses, + non_insertable = cache_stats.non_insertable, + non_reusable = cache_stats.non_reusable, + "cache stats" + ); + roc_tracing::info!( + module = ?module_id, + insertions = raw_function_cache_stats.insertions, + hits = raw_function_cache_stats.hits, + misses = raw_function_cache_stats.misses, + non_insertable = raw_function_cache_stats.non_insertable, + non_reusable = raw_function_cache_stats.non_reusable, + "raw function cache stats" + ); +} + +fn finish_specialization<'a>( + arena: &'a Bump, + state: State<'a>, + subs: Subs, + layout_interner: STLayoutInterner<'a>, + exposed_to_host: ExposedToHost, + module_expectations: VecMap, +) -> Result, LoadingProblem<'a>> { + if false { + println!( + "total Type clones: {} ", + roc_types::types::get_type_clone_count() + ); + } + let module_ids = Arc::try_unwrap(state.arc_modules) + .unwrap_or_else(|_| panic!("There were still outstanding Arc references to module_ids")) + .into_inner() + .into_module_ids(); + + let mut all_ident_ids = state.constrained_ident_ids; + + // Associate the ident IDs from the derived synth module + let (_, derived_synth_ident_ids) = Arc::try_unwrap(state.derived_module) + .unwrap_or_else(|_| internal_error!("Outstanding references to the derived module")) + .into_inner() + .unwrap() + .decompose(); + ModuleId::DERIVED_SYNTH.register_debug_idents(&derived_synth_ident_ids); + all_ident_ids.insert(ModuleId::DERIVED_SYNTH, derived_synth_ident_ids); + + let mut interns = Interns { + module_ids, + all_ident_ids, + }; + + let entry_point = { + let interns: &mut Interns = &mut interns; + match state.exec_mode { + ExecutionMode::Test => Ok(EntryPoint::Test), + ExecutionMode::Executable | ExecutionMode::ExecutableIfCheck => { + use PlatformPath::*; + + let platform_path = match &state.platform_path { + Valid(To::ExistingPackage(shorthand)) => { + match state.arc_shorthands.lock().get(shorthand) { + Some(shorthand_path) => shorthand_path.root_module().to_path_buf(), + None => unreachable!(), + } + } + Valid(To::NewPackage(p_or_p)) => PathBuf::from(p_or_p.as_str()), + other => { + let buf = to_missing_platform_report(state.root_id, other); + return Err(LoadingProblem::FormattedReport(buf)); + } + }; + + let exposed_symbols_and_layouts = match state.platform_data { + None => { + let src = &state.exposed_to_host.top_level_values; + let mut buf = bumpalo::collections::Vec::with_capacity_in(src.len(), arena); + + for &symbol in src.keys() { + let proc_layout = + proc_layout_for(state.procedures.keys().copied(), symbol); + + buf.push((symbol, proc_layout)); + } + + buf.into_bump_slice() + } + Some(PlatformData { + module_id, + provides, + .. + }) => { + let ident_ids = interns.all_ident_ids.get_mut(&module_id).unwrap(); + let mut buf = + bumpalo::collections::Vec::with_capacity_in(provides.len(), arena); + + for (loc_name, _loc_typed_ident) in provides { + let ident_id = ident_ids.get_or_insert(loc_name.value.as_str()); + let symbol = Symbol::new(module_id, ident_id); + let proc_layout = + proc_layout_for(state.procedures.keys().copied(), symbol); + + buf.push((symbol, proc_layout)); + } + + buf.into_bump_slice() + } + }; + + Ok(EntryPoint::Executable { + exposed_to_host: exposed_symbols_and_layouts, + platform_path, + }) + } + ExecutionMode::Check => unreachable!(), + } + }?; + + let State { + toplevel_expects, + procedures, + host_exposed_lambda_sets, + module_cache, + platform_data, + .. + } = state; + + let ModuleCache { + type_problems, + can_problems, + sources, + .. + } = module_cache; + + let sources: MutMap)> = sources + .into_iter() + .map(|(id, (path, src))| (id, (path, src.into()))) + .collect(); + + let module_id = state.root_id; + let uses_prebuilt_platform = match platform_data { + Some(data) => data.is_prebuilt, + // If there's no platform data (e.g. because we're building an interface module) + // then there's no prebuilt platform either! + None => false, + }; + + Ok(MonomorphizedModule { + can_problems, + type_problems, + expectations: module_expectations, + exposed_to_host, + module_id, + subs, + interns, + layout_interner, + procedures, + host_exposed_lambda_sets, + entry_point, + sources, + timings: state.timings, + toplevel_expects, + glue_layouts: GlueLayouts { getters: vec![] }, + uses_prebuilt_platform, + }) +} + +fn proc_layout_for<'a>( + mut proc_symbols: impl Iterator)>, + symbol: Symbol, +) -> ProcLayout<'a> { + match proc_symbols.find(|(s, _)| *s == symbol) { + Some((_, layout)) => layout, + None => { + // the entry point is not specialized. This can happen if the repl output + // is a function value + roc_mono::ir::ProcLayout { + arguments: &[], + result: Layout::UNIT, + niche: Niche::NONE, + } + } + } +} + +#[allow(clippy::too_many_arguments)] +fn finish( + mut state: State, + solved: Solved, + exposed_aliases_by_symbol: MutMap, + exposed_vars_by_symbol: Vec<(Symbol, Variable)>, + exposed_types_storage: ExposedTypesStorageSubs, + resolved_implementations: ResolvedImplementations, + dep_idents: IdentIdsByModule, + mut documentation: VecMap, + abilities_store: AbilitiesStore, + // + #[cfg(debug_assertions)] checkmate: Option, +) -> LoadedModule { + let module_ids = Arc::try_unwrap(state.arc_modules) + .unwrap_or_else(|_| panic!("There were still outstanding Arc references to module_ids")) + .into_inner() + .into_module_ids(); + + // Associate the ident IDs from the derived synth module + let (_, derived_synth_ident_ids) = Arc::try_unwrap(state.derived_module) + .unwrap_or_else(|_| internal_error!("Outstanding references to the derived module")) + .into_inner() + .unwrap() + .decompose(); + ModuleId::DERIVED_SYNTH.register_debug_idents(&derived_synth_ident_ids); + state + .constrained_ident_ids + .insert(ModuleId::DERIVED_SYNTH, derived_synth_ident_ids); + + let interns = Interns { + module_ids, + all_ident_ids: state.constrained_ident_ids, + }; + + let sources = state + .module_cache + .sources + .into_iter() + .map(|(id, (path, src))| (id, (path, src.into()))) + .collect(); + + let exposed_values = exposed_vars_by_symbol.iter().map(|x| x.0).collect(); + + let declarations_by_id = state.declarations_by_id; + + roc_checkmate::dump_checkmate!(checkmate); + + let mut docs_by_module = Vec::with_capacity(state.exposed_modules.len()); + + for module_id in state.exposed_modules.iter() { + let docs = documentation.remove(module_id).unwrap_or_else(|| { + panic!("A module was exposed but didn't have an entry in `documentation` somehow: {module_id:?}"); + }); + + docs_by_module.push(docs); + } + + debug_assert_eq!(documentation.len(), 0); + + LoadedModule { + module_id: state.root_id, + interns, + solved, + can_problems: state.module_cache.can_problems, + type_problems: state.module_cache.type_problems, + declarations_by_id, + typechecked: state.module_cache.checked, + dep_idents, + exposed_aliases: exposed_aliases_by_symbol, + exposed_values, + exposed_to_host: exposed_vars_by_symbol.into_iter().collect(), + exposed_types_storage, + resolved_implementations, + sources, + timings: state.timings, + docs_by_module, + abilities_store, + } +} + +/// Load a `package` or `platform` module from disk +fn load_package_from_disk<'a>( + arena: &'a Bump, + filename: &Path, + shorthand: &'a str, + app_module_id: ModuleId, + module_ids: Arc>>, + ident_ids_by_module: SharedIdentIdsByModule, +) -> Result, LoadingProblem<'a>> { + let module_start_time = Instant::now(); + let file_io_start = module_start_time; + let read_result = fs::read(filename); + let file_io_duration = file_io_start.elapsed(); + + match read_result { + Ok(bytes_vec) => { + let parse_start = Instant::now(); + let bytes = arena.alloc(bytes_vec); + let parse_state = roc_parse::state::State::new(bytes); + let parsed = roc_parse::module::parse_header(arena, parse_state.clone()); + let parse_header_duration = parse_start.elapsed(); + + // Insert the first entries for this module's timings + let mut pkg_module_timing = ModuleTiming::new(module_start_time); + + pkg_module_timing.read_roc_file = file_io_duration; + pkg_module_timing.parse_header = parse_header_duration; + + match parsed { + Ok(( + ast::Module { + header: ast::Header::Interface(header), + .. + }, + _parse_state, + )) => Err(LoadingProblem::UnexpectedHeader(format!( + "expected platform/package module, got Interface with header\n{header:?}" + ))), + Ok(( + ast::Module { + header: ast::Header::Hosted(header), + .. + }, + _parse_state, + )) => Err(LoadingProblem::UnexpectedHeader(format!( + "expected platform/package module, got Hosted module with header\n{header:?}" + ))), + Ok(( + ast::Module { + header: ast::Header::App(header), + .. + }, + _parse_state, + )) => Err(LoadingProblem::UnexpectedHeader(format!( + "expected platform/package module, got App with header\n{header:?}" + ))), + Ok(( + ast::Module { + header: ast::Header::Package(header), + comments, + }, + parser_state, + )) => { + let (_, _, package_module_msg) = build_package_header( + arena, + Some(shorthand), + false, // since we have an app module ID, the app module must be the root + filename.to_path_buf(), + parser_state, + module_ids, + ident_ids_by_module, + &header, + comments, + pkg_module_timing, + )?; + + Ok(Msg::Header(package_module_msg)) + } + Ok(( + ast::Module { + header: ast::Header::Platform(header), + comments, + }, + parser_state, + )) => { + let exposes_ids = get_exposes_ids( + header.exposes.item.items, + arena, + &module_ids, + &ident_ids_by_module, + ); + + // make a `platform` module that ultimately exposes `main` to the host + let (_, _, platform_module_msg) = build_platform_header( + arena, + Some(shorthand), + Some(app_module_id), + filename.to_path_buf(), + parser_state, + module_ids, + exposes_ids.into_bump_slice(), + ident_ids_by_module, + &header, + comments, + pkg_module_timing, + )?; + + Ok(Msg::Header(platform_module_msg)) + } + Err(fail) => Err(LoadingProblem::ParsingFailed( + fail.map_problem(SyntaxError::Header) + .into_file_error(filename.to_path_buf()), + )), + } + } + + Err(err) => Err(LoadingProblem::FileProblem { + filename: filename.to_path_buf(), + error: err.kind(), + }), + } +} + +fn get_exposes_ids<'a>( + entries: &'a [Loc>>], + arena: &'a Bump, + module_ids: &Arc>>, + ident_ids_by_module: &Arc>, +) -> bumpalo::collections::Vec<'a, ModuleId> { + let mut exposes_ids = bumpalo::collections::Vec::with_capacity_in(entries.len(), arena); + + // Lock just long enough to perform the minimal operations necessary. + let mut module_ids = (**module_ids).lock(); + let mut ident_ids_by_module = (**ident_ids_by_module).lock(); + + // TODO can we "iterate unspaced" instead of calling unspace here? + for entry in unspace(arena, entries) { + let module_id = + module_ids.get_or_insert(&PQModuleName::Unqualified(entry.value.as_str().into())); + + // Ensure this module has an entry in the ident_ids_by_module map. + ident_ids_by_module.get_or_insert(module_id); + + exposes_ids.push(module_id); + } + + exposes_ids +} + +fn load_builtin_module_help<'a>( + arena: &'a Bump, + filename: &str, + src_bytes: &'a str, +) -> (HeaderInfo<'a>, roc_parse::state::State<'a>) { + let is_root_module = false; + let opt_shorthand = None; + + let filename = PathBuf::from(filename); + + let parse_state = roc_parse::state::State::new(src_bytes.as_bytes()); + let parsed = roc_parse::module::parse_header(arena, parse_state.clone()); + + match parsed { + Ok(( + ast::Module { + header: ast::Header::Interface(header), + comments, + }, + parse_state, + )) => { + let info = HeaderInfo { + filename, + is_root_module, + opt_shorthand, + packages: &[], + imports: unspace(arena, header.imports.item.items), + header_type: HeaderType::Builtin { + name: header.name.value, + exposes: unspace(arena, header.exposes.item.items), + generates_with: &[], + }, + module_comments: comments, + }; + + (info, parse_state) + } + Ok(_) => panic!("invalid header format for builtin module"), + Err(e) => panic!("Hit a parse error in the header of {filename:?}:\n{e:?}"), + } +} + +fn load_builtin_module<'a>( + arena: &'a Bump, + module_ids: Arc>>, + ident_ids_by_module: SharedIdentIdsByModule, + module_timing: ModuleTiming, + module_id: ModuleId, + module_name: &str, +) -> Result<(ModuleId, Msg<'a>), LoadingProblem<'a>> { + let src_bytes = module_source(module_id); + + let (info, parse_state) = load_builtin_module_help(arena, module_name, src_bytes); + + let (module_id, _, header) = build_header( + arena, + info, + parse_state, + module_ids, + ident_ids_by_module, + module_timing, + )?; + Ok((module_id, Msg::Header(header))) +} + +/// Load a module by its module name, rather than by its filename +fn load_module<'a>( + arena: &'a Bump, + src_dir: &Path, + module_name: PQModuleName<'a>, + module_ids: Arc>>, + arc_shorthands: Arc>>, + roc_cache_dir: RocCacheDir<'_>, + ident_ids_by_module: SharedIdentIdsByModule, +) -> Result, LoadingProblem<'a>> { + let module_start_time = Instant::now(); + + let parse_start = Instant::now(); + let parse_header_duration = parse_start.elapsed(); + + // Insert the first entries for this module's timings + let mut module_timing = ModuleTiming::new(module_start_time); + + module_timing.read_roc_file = Default::default(); + module_timing.parse_header = parse_header_duration; + + macro_rules! load_builtins { + ($($name:literal, $module_id:path)*) => { + match module_name.as_inner().as_str() { + $( + $name => { + let (module_id, msg) = load_builtin_module( + arena, + module_ids, + ident_ids_by_module, + module_timing, + $module_id, + concat!($name, ".roc") + )?; + + return Ok(HeaderOutput { module_id, msg, opt_platform_shorthand: None }); + } + )* + _ => { /* fall through */ } + }} + } + + load_builtins! { + "Result", ModuleId::RESULT + "List", ModuleId::LIST + "Str", ModuleId::STR + "Dict", ModuleId::DICT + "Set", ModuleId::SET + "Num", ModuleId::NUM + "Bool", ModuleId::BOOL + "Box", ModuleId::BOX + "Encode", ModuleId::ENCODE + "Decode", ModuleId::DECODE + "Hash", ModuleId::HASH + "Inspect", ModuleId::INSPECT + "TotallyNotJson", ModuleId::JSON + } + + let (filename, opt_shorthand) = module_name_to_path(src_dir, &module_name, arc_shorthands); + + load_filename( + arena, + filename, + false, + opt_shorthand, + Some(module_name), + module_ids, + ident_ids_by_module, + roc_cache_dir, + module_start_time, + ) +} + +#[derive(Debug)] +enum ShorthandPath { + /// e.g. "/home/rtfeldman/.cache/roc/0.1.0/oUkxSOI9zFGtSoIaMB40QPdrXphr1p1780eiui2iO9Mz" + FromHttpsUrl { + /// e.g. "/home/rtfeldman/.cache/roc/0.1.0/oUkxSOI9zFGtSoIaMB40QPdrXphr1p1780eiui2iO9Mz" + root_module_dir: PathBuf, + /// e.g. "/home/rtfeldman/.cache/roc/0.1.0/oUkxSOI9zFGtSoIaMB40QPdrXphr1p1780eiui2iO9Mz/main.roc" + root_module: PathBuf, + }, + RelativeToSrc { + /// e.g. "/home/rtfeldman/my-roc-code/examples/cli/cli-platform/" + root_module_dir: PathBuf, + /// e.g. "/home/rtfeldman/my-roc-code/examples/cli/cli-platform/main.roc" + root_module: PathBuf, + }, +} + +impl ShorthandPath { + pub fn root_module(&self) -> &Path { + match self { + ShorthandPath::FromHttpsUrl { root_module, .. } + | ShorthandPath::RelativeToSrc { root_module, .. } => root_module.as_path(), + } + } + + pub fn root_module_dir(&self) -> &Path { + match self { + ShorthandPath::FromHttpsUrl { + root_module_dir, .. + } + | ShorthandPath::RelativeToSrc { + root_module_dir, .. + } => root_module_dir.as_path(), + } + } +} + +fn module_name_to_path<'a>( + src_dir: &Path, + module_name: &PQModuleName<'a>, + arc_shorthands: Arc>>, +) -> (PathBuf, Option<&'a str>) { + let mut filename; + let opt_shorthand; + + match module_name { + PQModuleName::Unqualified(name) => { + filename = src_dir.to_path_buf(); + + opt_shorthand = None; + // Convert dots in module name to directories + for part in name.split(MODULE_SEPARATOR) { + filename.push(part); + } + } + PQModuleName::Qualified(shorthand, name) => { + opt_shorthand = Some(*shorthand); + let shorthands = arc_shorthands.lock(); + filename = shorthands + .get(shorthand) + .expect("All shorthands should have been validated by now.") + .root_module_dir() + .to_path_buf(); + + // Convert dots in module name to directories + for part in name.split(MODULE_SEPARATOR) { + filename.push(part); + } + } + } + + // End with .roc + filename.set_extension(ROC_FILE_EXTENSION); + + (filename, opt_shorthand) +} + +/// Find a task according to the following algorithm: +/// +/// 1. Look in a local Worker queue. If it has a task, pop it off the queue and return it. +/// 2. If that queue was empty, ask the global queue for a task. +/// 3. If the global queue is also empty, iterate through each Stealer (each Worker queue has a +/// corresponding Stealer, which can steal from it. Stealers can be shared across threads.) +/// +/// Based on https://docs.rs/crossbeam/0.7.3/crossbeam/deque/index.html#examples +fn find_task(local: &Worker, global: &Injector, stealers: &[Stealer]) -> Option { + // Pop a task from the local queue, if not empty. + local.pop().or_else(|| { + // Otherwise, we need to look for a task elsewhere. + iter::repeat_with(|| { + // Try stealing a task from the global queue. + global + .steal() + // Or try stealing a task from one of the other threads. + .or_else(|| stealers.iter().map(|s| s.steal()).collect()) + }) + // Loop while no task was stolen and any steal operation needs to be retried. + .find(|s| !s.is_retry()) + // Extract the stolen task, if there is one. + .and_then(|s| s.success()) + }) +} + +fn verify_interface_matches_file_path<'a>( + interface_name: Loc>, + path: &Path, + state: &roc_parse::state::State<'a>, +) -> Result<(), LoadingProblem<'a>> { + let module_parts = interface_name.value.as_str().split(MODULE_SEPARATOR).rev(); + + let mut is_mismatched = false; + let mut opt_path = Some(path); + for part in module_parts { + match opt_path.and_then(|path| path.file_stem().map(|fi| (path, fi))) { + None => { + is_mismatched = true; + break; + } + Some((path, fi)) => { + if fi != part { + is_mismatched = true; + break; + } + opt_path = path.parent(); + } + } + } + + if !is_mismatched { + return Ok(()); + } + + use roc_parse::parser::EHeader; + let syntax_problem = + SyntaxError::Header(EHeader::InconsistentModuleName(interface_name.region)); + let problem = LoadingProblem::ParsingFailed(FileError { + problem: SourceError::new(syntax_problem, state), + filename: path.to_path_buf(), + }); + Err(problem) +} + +#[derive(Debug)] +struct HeaderOutput<'a> { + module_id: ModuleId, + msg: Msg<'a>, + /// Only comes up if we're parsing an app module + opt_platform_shorthand: Option<&'a str>, +} + +fn parse_header<'a>( + arena: &'a Bump, + read_file_duration: Duration, + filename: PathBuf, + is_root_module: bool, + opt_shorthand: Option<&'a str>, + opt_expected_module_name: Option>, + module_ids: Arc>>, + ident_ids_by_module: SharedIdentIdsByModule, + src_bytes: &'a [u8], + roc_cache_dir: RocCacheDir<'_>, + start_time: Instant, +) -> Result, LoadingProblem<'a>> { + let parse_start = Instant::now(); + let parse_state = roc_parse::state::State::new(src_bytes); + let parsed = roc_parse::module::parse_header(arena, parse_state.clone()); + let parse_header_duration = parse_start.elapsed(); + + // Insert the first entries for this module's timings + let mut module_timing = ModuleTiming::new(start_time); + + module_timing.read_roc_file = read_file_duration; + module_timing.parse_header = parse_header_duration; + + match parsed { + Ok(( + ast::Module { + header: ast::Header::Interface(header), + comments, + }, + parse_state, + )) => { + verify_interface_matches_file_path(header.name, &filename, &parse_state)?; + + let header_name_region = header.name.region; + let info = HeaderInfo { + filename, + is_root_module, + opt_shorthand, + packages: &[], + imports: unspace(arena, header.imports.item.items), + header_type: HeaderType::Interface { + name: header.name.value, + exposes: unspace(arena, header.exposes.item.items), + }, + module_comments: comments, + }; + + let (module_id, module_name, header) = build_header( + arena, + info, + parse_state.clone(), + module_ids, + ident_ids_by_module, + module_timing, + )?; + + if let Some(expected_module_name) = opt_expected_module_name { + if expected_module_name != module_name { + let problem = SourceError::new( + IncorrectModuleName { + module_id, + found: Loc::at(header_name_region, module_name), + expected: expected_module_name, + }, + &parse_state, + ); + let problem = LoadingProblem::IncorrectModuleName(FileError { + problem, + filename: header.module_path, + }); + return Err(problem); + } + } + + Ok(HeaderOutput { + module_id, + msg: Msg::Header(header), + opt_platform_shorthand: None, + }) + } + Ok(( + ast::Module { + header: ast::Header::Hosted(header), + comments, + }, + parse_state, + )) => { + let info = HeaderInfo { + filename, + is_root_module, + opt_shorthand, + packages: &[], + imports: unspace(arena, header.imports.item.items), + header_type: HeaderType::Hosted { + name: header.name.value, + exposes: unspace(arena, header.exposes.item.items), + generates: header.generates.item, + generates_with: unspace(arena, header.generates_with.item.items), + }, + module_comments: comments, + }; + + let (module_id, _, header) = build_header( + arena, + info, + parse_state, + module_ids, + ident_ids_by_module, + module_timing, + )?; + + Ok(HeaderOutput { + module_id, + msg: Msg::Header(header), + opt_platform_shorthand: None, + }) + } + Ok(( + ast::Module { + header: ast::Header::App(header), + comments, + }, + parse_state, + )) => { + let mut app_file_dir = filename.clone(); + app_file_dir.pop(); + + let packages = if let Some(packages) = header.packages { + unspace(arena, packages.item.items) + } else { + &[] + }; + + let mut provides = bumpalo::collections::Vec::new_in(arena); + + provides.extend(unspace(arena, header.provides.entries.items)); + + if let Some(provided_types) = header.provides.types { + for provided_type in unspace(arena, provided_types.items) { + let string: &str = provided_type.value.into(); + let exposed_name = ExposedName::new(string); + + provides.push(Loc::at(provided_type.region, exposed_name)); + } + } + + let info = HeaderInfo { + filename, + is_root_module, + opt_shorthand, + packages, + imports: if let Some(imports) = header.imports { + unspace(arena, imports.item.items) + } else { + &[] + }, + header_type: HeaderType::App { + provides: provides.into_bump_slice(), + output_name: header.name.value, + to_platform: header.provides.to.value, + }, + module_comments: comments, + }; + + let (module_id, _, resolved_header) = build_header( + arena, + info, + parse_state, + module_ids.clone(), + ident_ids_by_module.clone(), + module_timing, + )?; + + let mut messages = Vec::with_capacity(packages.len() + 1); + + // It's important that the app header is first in the list! + messages.push(Msg::Header(resolved_header)); + + load_packages( + packages, + &mut messages, + roc_cache_dir, + app_file_dir, + arena, + module_id, + module_ids, + ident_ids_by_module, + ); + + // Look at the app module's `to` keyword to determine which package was the platform. + match header.provides.to.value { + To::ExistingPackage(shorthand) => { + if !packages + .iter() + .any(|loc_package_entry| loc_package_entry.value.shorthand == shorthand) + { + todo!("Gracefully handle platform shorthand after `to` that didn't map to a shorthand specified in `packages`"); + } + + Ok(HeaderOutput { + module_id, + msg: Msg::Many(messages), + opt_platform_shorthand: Some(shorthand), + }) + } + To::NewPackage(_package_name) => Ok(HeaderOutput { + module_id, + msg: Msg::Many(messages), + opt_platform_shorthand: None, + }), + } + } + Ok(( + ast::Module { + header: ast::Header::Package(header), + comments, + }, + parse_state, + )) => { + let (module_id, _, header) = build_package_header( + arena, + None, + is_root_module, + filename, + parse_state, + module_ids, + ident_ids_by_module, + &header, + comments, + module_timing, + )?; + + Ok(HeaderOutput { + module_id, + msg: Msg::Header(header), + opt_platform_shorthand: None, + }) + } + + Ok(( + ast::Module { + header: ast::Header::Platform(header), + comments, + }, + parse_state, + )) => { + let exposes_ids = get_exposes_ids( + header.exposes.item.items, + arena, + &module_ids, + &ident_ids_by_module, + ); + + let (module_id, _, header) = build_platform_header( + arena, + None, + None, + filename, + parse_state, + module_ids, + exposes_ids.into_bump_slice(), + ident_ids_by_module, + &header, + comments, + module_timing, + )?; + + Ok(HeaderOutput { + module_id, + msg: Msg::Header(header), + opt_platform_shorthand: None, + }) + } + Err(fail) => Err(LoadingProblem::ParsingFailed( + fail.map_problem(SyntaxError::Header) + .into_file_error(filename), + )), + } +} + +fn load_packages<'a>( + packages: &[Loc>], + load_messages: &mut Vec>, + roc_cache_dir: RocCacheDir, + cwd: PathBuf, + arena: &'a Bump, + module_id: ModuleId, + module_ids: Arc>>, + ident_ids_by_module: SharedIdentIdsByModule, +) { + // Load all the packages + for Loc { value: entry, .. } in packages.iter() { + let PackageEntry { + shorthand, + package_name: + Loc { + value: package_name, + .. + }, + .. + } = entry; + + let src = package_name.to_str(); + + // find the `package` or `platform` module on disk, + // downloading it into a cache dir first if necessary. + let root_module_path = if src.starts_with("https://") { + #[cfg(not(target_family = "wasm"))] + { + // If this is a HTTPS package, synchronously download it + // to the cache before proceeding. + + // TODO we should do this async; however, with the current + // architecture of file.rs (which doesn't use async/await), + // this would be very difficult! + match cache::install_package(roc_cache_dir, src) { + Ok((package_dir, opt_root_module)) => { + // You can optionally specify the root module using the URL fragment, + // e.g. #foo.roc + // (defaults to main.roc) + match opt_root_module { + Some(root_module) => package_dir.join(root_module), + None => package_dir.join("main.roc"), + } + } + Err(problem) => { + let buf = to_https_problem_report_string(src, problem); + + load_messages.push(Msg::FailedToLoad(LoadingProblem::FormattedReport(buf))); + return; + } + } + } + + #[cfg(target_family = "wasm")] + { + panic!("Specifying packages via URLs is curently unsupported in wasm."); + } + } else { + cwd.join(src) + }; + + match load_package_from_disk( + arena, + &root_module_path, + shorthand, + module_id, + module_ids.clone(), + ident_ids_by_module.clone(), + ) { + Ok(msg) => { + load_messages.push(msg); + } + Err(problem) => { + load_messages.push(Msg::FailedToLoad(problem)); + } + } + } +} + +/// Load a module by its filename +fn load_filename<'a>( + arena: &'a Bump, + filename: PathBuf, + is_root_module: bool, + opt_shorthand: Option<&'a str>, + opt_expected_module_name: Option>, + module_ids: Arc>>, + ident_ids_by_module: SharedIdentIdsByModule, + roc_cache_dir: RocCacheDir<'_>, + module_start_time: Instant, +) -> Result, LoadingProblem<'a>> { + let file_io_start = Instant::now(); + let file = fs::read(&filename); + let file_io_duration = file_io_start.elapsed(); + + match file { + Ok(bytes) => parse_header( + arena, + file_io_duration, + filename, + is_root_module, + opt_shorthand, + opt_expected_module_name, + module_ids, + ident_ids_by_module, + arena.alloc(bytes), + roc_cache_dir, + module_start_time, + ), + Err(err) => Err(LoadingProblem::FileProblem { + filename, + error: err.kind(), + }), + } +} + +/// Load a module from a str +/// the `filename` is never read, but used for the module name +fn load_from_str<'a>( + arena: &'a Bump, + filename: PathBuf, + src: &'a str, + module_ids: Arc>>, + ident_ids_by_module: SharedIdentIdsByModule, + roc_cache_dir: RocCacheDir<'_>, + module_start_time: Instant, +) -> Result, LoadingProblem<'a>> { + let file_io_start = Instant::now(); + let file_io_duration = file_io_start.elapsed(); + + parse_header( + arena, + file_io_duration, + filename, + false, + None, + None, + module_ids, + ident_ids_by_module, + src.as_bytes(), + roc_cache_dir, + module_start_time, + ) +} + +#[derive(Debug)] +struct HeaderInfo<'a> { + filename: PathBuf, + is_root_module: bool, + opt_shorthand: Option<&'a str>, + packages: &'a [Loc>], + imports: &'a [Loc>], + header_type: HeaderType<'a>, + module_comments: &'a [CommentOrNewline<'a>], +} + +fn build_header<'a>( + arena: &'a Bump, + info: HeaderInfo<'a>, + parse_state: roc_parse::state::State<'a>, + module_ids: Arc>>, + ident_ids_by_module: SharedIdentIdsByModule, + module_timing: ModuleTiming, +) -> Result<(ModuleId, PQModuleName<'a>, ModuleHeader<'a>), LoadingProblem<'a>> { + let HeaderInfo { + filename, + is_root_module, + opt_shorthand, + packages, + imports, + header_type, + module_comments: header_comments, + } = info; + + let mut imported_modules: MutMap = MutMap::default(); + let exposed_values = header_type.exposed_or_provided_values(); + let num_exposes = exposed_values.len(); + let mut deps_by_name: MutMap = + HashMap::with_capacity_and_hasher(num_exposes, default_hasher()); + let declared_name: ModuleName = match &header_type { + HeaderType::App { .. } => ModuleName::APP.into(), + HeaderType::Platform { + opt_app_module_id, .. + } => { + // Add standard imports, if there is an app module. + // (There might not be, e.g. when running `roc check myplatform.roc` or + // when generating bindings.) + if let Some(app_module_id) = opt_app_module_id { + imported_modules.insert(*app_module_id, Region::zero()); + deps_by_name.insert( + PQModuleName::Unqualified(ModuleName::APP.into()), + *app_module_id, + ); + } + + // Platform modules do not have names. This is important because otherwise + // those names end up in host-generated symbols, and they may contain + // characters that hosts might not allow in their function names. + String::new().into() + } + HeaderType::Package { .. } => { + // Package modules do not have names. + String::new().into() + } + HeaderType::Interface { name, .. } + | HeaderType::Builtin { name, .. } + | HeaderType::Hosted { name, .. } => { + // TODO check to see if name is consistent with filename. + // If it isn't, report a problem! + + name.as_str().into() + } + }; + + let mut imported: Vec<(QualifiedModuleName, Vec>, Region)> = + Vec::with_capacity(imports.len()); + let mut scope_size = 0; + + let mut defined_values = vec![]; + for loc_entry in imports { + if let Some((qualified_module_name, exposed)) = exposed_from_import(&loc_entry.value) { + scope_size += num_exposes; + + imported.push((qualified_module_name, exposed, loc_entry.region)); + } + if let Some(value) = value_def_from_imports(arena, &filename, loc_entry)? { + defined_values.push(value); + } + } + + let mut exposed: Vec = Vec::with_capacity(num_exposes); + + // Make sure the module_ids has ModuleIds for all our deps, + // then record those ModuleIds in can_module_ids for later. + let mut scope: MutMap = + HashMap::with_capacity_and_hasher(scope_size, default_hasher()); + let home: ModuleId; + let name: PQModuleName; + let symbols_from_requires; + + let ident_ids = { + // Lock just long enough to perform the minimal operations necessary. + let mut module_ids = (*module_ids).lock(); + let mut ident_ids_by_module = (*ident_ids_by_module).lock(); + + name = match opt_shorthand { + Some(shorthand) => PQModuleName::Qualified(shorthand, declared_name), + None => PQModuleName::Unqualified(declared_name), + }; + home = module_ids.get_or_insert(&name); + + // Ensure this module has an entry in the ident_ids_by_module map. + ident_ids_by_module.get_or_insert(home); + + // For each of our imports, add an entry to deps_by_name + // + // e.g. for `imports [pf.Foo.{ bar }]`, add `Foo` to deps_by_name + // + // Also build a list of imported_values_to_expose (like `bar` above.) + for (qualified_module_name, exposed_idents, region) in imported.into_iter() { + let pq_module_name = qualified_module_name.into_pq_module_name(opt_shorthand); + + let module_id = module_ids.get_or_insert(&pq_module_name); + + imported_modules.insert(module_id, region); + + deps_by_name.insert(pq_module_name, module_id); + + // Add the new exposed idents to the dep module's IdentIds, so + // once that module later gets loaded, its lookups will resolve + // to the same symbols as the ones we're using here. + let ident_ids = ident_ids_by_module.get_or_insert(module_id); + + for loc_ident in exposed_idents { + let ident_id = ident_ids.get_or_insert(loc_ident.value.as_str()); + let symbol = Symbol::new(module_id, ident_id); + + // Since this value is exposed, add it to our module's default scope. + debug_assert!(!scope.contains_key(&loc_ident.value)); + + scope.insert(loc_ident.value, (symbol, loc_ident.region)); + } + } + + symbols_from_requires = if let HeaderType::Platform { + requires, + requires_types, + opt_app_module_id, + .. + } = header_type + { + // If we don't have an app module id (e.g. because we're doing + // `roc check myplatform.roc` or because we're generating glue code), + // insert the `requires` symbols into the platform module's IdentIds. + // + // Otherwise, get them from the app module's IdentIds, because it + // should already have a symbol for each `requires` entry, and we + // want to make sure we're referencing the same symbols! + let module_id = opt_app_module_id.unwrap_or(home); + let mut symbols_from_requires = Vec::with_capacity(requires.len()); + let ident_ids = ident_ids_by_module.get_or_insert(module_id); + + for Loc { + value: entry, + region: _, + } in requires + { + let ident: Ident = entry.ident.value.into(); + let ident_id = ident_ids.get_or_insert(entry.ident.value); + let symbol = Symbol::new(module_id, ident_id); + + // Since this value is exposed, add it to our module's default scope. + debug_assert!(!scope.contains_key(&ident.clone())); + + scope.insert(ident, (symbol, entry.ident.region)); + symbols_from_requires.push((Loc::at(entry.ident.region, symbol), entry.ann)); + } + + for entry in requires_types { + let str_entry: &str = entry.value.into(); + let ident: Ident = str_entry.into(); + let ident_id = ident_ids.get_or_insert(str_entry); + let symbol = Symbol::new(module_id, ident_id); + + // Since this value is exposed, add it to our module's default scope. + debug_assert!(!scope.contains_key(&ident)); + scope.insert(ident, (symbol, entry.region)); + } + + symbols_from_requires + } else { + Vec::new() + }; + + let ident_ids = ident_ids_by_module.get_mut(&home).unwrap(); + + for loc_exposed in exposed_values.iter() { + // Use get_or_insert here because the ident_ids may already + // created an IdentId for this, when it was imported exposed + // in a dependent module. + // + // For example, if module A implements [B.{ foo }], then + // when we get here for B, `foo` will already have + // an IdentId. We must reuse that! + let ident_id = ident_ids.get_or_insert(loc_exposed.value.as_str()); + let symbol = Symbol::new(home, ident_id); + + exposed.push(symbol); + } + + // Generate IdentIds entries for all values this module provides, + // and treat them as `exposes` values for later purposes. + // This way, when we encounter them in Defs later, they already + // have an IdentIds entry. + // + // We must *not* add them to scope yet, or else the Defs will + // incorrectly think they're shadowing them! + if let HeaderType::Platform { provides, .. } = header_type { + exposed.reserve(provides.len()); + + for (loc_name, _loc_typed_ident) in provides.iter() { + // Use get_or_insert here because the ident_ids may already + // created an IdentId for this, when it was imported exposed + // in a dependent module. + // + // For example, if module A implements [B.{ foo }], then + // when we get here for B, `foo` will already have + // an IdentId. We must reuse that! + let ident_id = ident_ids.get_or_insert(loc_name.value.as_str()); + let symbol = Symbol::new(home, ident_id); + + exposed.push(symbol); + } + } + + if cfg!(debug_assertions) { + home.register_debug_idents(ident_ids); + } + + ident_ids.clone() + }; + + // This module depends on all the modules it exposes. We need to load those in order + // to do things like report errors for them, generate docs for them, etc. + if info.is_root_module { + if let HeaderType::Platform { + exposes, + exposes_ids, + .. + } + | HeaderType::Package { + exposes, + exposes_ids, + .. + } = header_type + { + for (loc_module_name, module_id) in exposes.iter().zip(exposes_ids.iter().copied()) { + let module_name_str = loc_module_name.value.as_str(); + let pq_module_name = PackageQualified::Unqualified(module_name_str.into()); + + // We should never change an entry here. Either we should have no entry, + // or if we do have one, it should be unchanged by this insertion. + debug_assert_eq!( + &module_id, + deps_by_name.get(&pq_module_name).unwrap_or(&module_id), + "Already had a deps_by_name entry for {:?}, but it was {:?} rather than the expected {:?}", + pq_module_name, + deps_by_name.get(&pq_module_name).unwrap(), + module_id, + ); + deps_by_name.insert(pq_module_name, module_id); + } + } + } + + let package_entries = packages + .iter() + .map(|Loc { value: pkg, .. }| (pkg.shorthand, pkg.package_name.value)) + .collect::>(); + + // Send the deps to the coordinator thread for processing, + // then continue on to parsing and canonicalizing defs. + // + // We always need to send these, even if deps is empty, + // because the coordinator thread needs to receive this message + // to decrement its "pending" count. + let mut package_qualified_imported_modules = MutSet::default(); + for (pq_module_name, module_id) in &deps_by_name { + match pq_module_name { + PackageQualified::Unqualified(_) => { + package_qualified_imported_modules + .insert(PackageQualified::Unqualified(*module_id)); + } + PackageQualified::Qualified(shorthand, _) => { + package_qualified_imported_modules + .insert(PackageQualified::Qualified(shorthand, *module_id)); + } + } + } + + // make sure when we run the bulitin modules in /compiler/builtins/roc that we + // mark these modules as Builtin. Otherwise the builtin functions are not instantiated + // and we just have a bunch of definitions with runtime errors in their bodies + let header_type = { + match header_type { + HeaderType::Interface { name, exposes } if home.is_builtin() => HeaderType::Builtin { + name, + exposes, + generates_with: &[], + }, + _ => header_type, + } + }; + + Ok(( + home, + name, + ModuleHeader { + module_id: home, + module_path: filename, + is_root_module, + exposed_ident_ids: ident_ids, + packages: package_entries, + imported_modules, + package_qualified_imported_modules, + deps_by_name, + exposes: exposed, + parse_state, + exposed_imports: scope, + symbols_from_requires, + header_type, + header_comments, + module_timing, + defined_values, + }, + )) +} + +impl<'a> BuildTask<'a> { + // TODO trim down these arguments - possibly by moving Constraint into Module + fn solve_module( + module: Module, + ident_ids: IdentIds, + module_timing: ModuleTiming, + types: Types, + constraints: Constraints, + constraint: ConstraintSoa, + function_kind: FunctionKind, + pending_derives: PendingDerives, + var_store: VarStore, + imported_modules: MutMap, + exposed_types: &ExposedByModule, + dep_idents: IdentIdsByModule, + declarations: Declarations, + cached_subs: CachedTypeState, + derived_module: SharedDerivedModule, + + #[cfg(debug_assertions)] checkmate: Option, + ) -> Self { + let exposed_by_module = exposed_types.retain_modules(imported_modules.keys()); + + let exposed_for_module = + ExposedForModule::new(module.referenced_values.iter(), exposed_by_module); + + // Next, solve this module in the background. + Self::Solve { + module, + ident_ids, + exposed_for_module, + types, + constraints, + constraint, + function_kind, + pending_derives, + var_store, + declarations, + dep_idents, + module_timing, + cached_subs, + derived_module, + + #[cfg(debug_assertions)] + checkmate, + } + } +} + +fn synth_import(subs: &mut Subs, content: roc_types::subs::Content) -> Variable { + use roc_types::subs::{Descriptor, Mark, OptVariable, Rank}; + subs.fresh(Descriptor { + content, + rank: Rank::import(), + mark: Mark::NONE, + copy: OptVariable::NONE, + }) +} + +fn synth_list_len_type(subs: &mut Subs) -> Variable { + use roc_types::subs::{Content, FlatType, LambdaSet, OptVariable, SubsSlice, UnionLabels}; + + // List.len : List a -> Nat + let a = synth_import(subs, Content::FlexVar(None)); + let a_slice = SubsSlice::extend_new(&mut subs.variables, [a]); + let list_a = synth_import( + subs, + Content::Structure(FlatType::Apply(Symbol::LIST_LIST, a_slice)), + ); + let fn_var = synth_import(subs, Content::Error); + let solved_list_len = UnionLabels::insert_into_subs(subs, [(Symbol::LIST_LEN, [])]); + let clos_list_len = synth_import( + subs, + Content::LambdaSet(LambdaSet { + solved: solved_list_len, + recursion_var: OptVariable::NONE, + unspecialized: SubsSlice::default(), + ambient_function: fn_var, + }), + ); + let fn_args_slice = SubsSlice::extend_new(&mut subs.variables, [list_a]); + subs.set_content( + fn_var, + Content::Structure(FlatType::Func(fn_args_slice, clos_list_len, Variable::NAT)), + ); + fn_var +} + +pub fn add_imports( + my_module: ModuleId, + constraints: &mut Constraints, + subs: &mut Subs, + mut pending_abilities: PendingAbilitiesStore, + exposed_for_module: &ExposedForModule, + def_types: &mut Vec<(Symbol, Loc)>, + imported_rigid_vars: &mut Vec, + imported_flex_vars: &mut Vec, +) -> (Vec, AbilitiesStore) { + let mut import_variables = Vec::new(); + + let mut cached_symbol_vars = VecMap::default(); + + let imported_values = exposed_for_module.imported_values.iter().copied(); + + for symbol in imported_values { + import_variable_for_symbol( + subs, + constraints, + def_types, + &mut import_variables, + imported_rigid_vars, + imported_flex_vars, + &mut cached_symbol_vars, + &exposed_for_module.exposed_by_module, + symbol, + OnSymbolNotFound::AssertIsBuiltin, + ); + } + + // Patch used symbols from circular dependencies. + if my_module == ModuleId::NUM { + // Num needs List.len, but List imports Num. + let list_len_type_var = synth_list_len_type(subs); + let list_len_type_index = constraints.push_variable(list_len_type_var); + def_types.push((Symbol::LIST_LEN, Loc::at_zero(list_len_type_index))); + import_variables.push(list_len_type_var); + } + + // Fill in the implementation information of the abilities from the modules we import, which we + // now know because all imported modules should be solved by now. + // + // TODO: see if we can reduce the amount of specializations we need to import. + // One idea is to just always assume external modules fulfill their specialization obligations + // and save lambda set resolution for mono. + for (_, module_types) in exposed_for_module.exposed_by_module.iter_all() { + for (impl_key, resolved_impl) in module_types.resolved_implementations.iter() { + pending_abilities.import_implementation(*impl_key, resolved_impl); + } + } + + struct Ctx<'a> { + subs: &'a mut Subs, + exposed_by_module: &'a ExposedByModule, + imported_variables: &'a mut Vec, + imported_rigids: &'a mut Vec, + imported_flex: &'a mut Vec, + } + + let abilities_store = pending_abilities.resolve_for_module( + my_module, + &mut Ctx { + subs, + exposed_by_module: &exposed_for_module.exposed_by_module, + imported_variables: &mut import_variables, + imported_rigids: imported_rigid_vars, + imported_flex: imported_flex_vars, + }, + |ctx, symbol| match cached_symbol_vars.get(&symbol).copied() { + Some(var) => var, + None => { + import_variable_for_symbol( + ctx.subs, + constraints, + def_types, + ctx.imported_variables, + ctx.imported_rigids, + ctx.imported_flex, + &mut cached_symbol_vars, + &exposed_for_module.exposed_by_module, + symbol, + OnSymbolNotFound::AbilityMemberMustBeAvailable, + ); + + *cached_symbol_vars.get(&symbol).unwrap() + } + }, + |ctx, module, lset_var| match ctx.exposed_by_module.get(&module) { + Some(ExposedModuleTypes { + exposed_types_storage_subs: exposed_types, + resolved_implementations: _, + }) => { + let var = exposed_types + .stored_specialization_lambda_set_vars + .get(&lset_var) + .expect("Lambda set var from other module not available"); + + let copied_import = exposed_types + .storage_subs + .export_variable_to(ctx.subs, *var); + + #[allow(clippy::let_and_return)] + let copied_import_var = extend_imports_data_with_copied_import( + copied_import, + ctx.imported_variables, + ctx.imported_rigids, + ctx.imported_flex, + ); + + copied_import_var + } + None => internal_error!("Imported module {:?} is not available", module), + }, + ); + + (import_variables, abilities_store) +} + +enum OnSymbolNotFound { + AssertIsBuiltin, + AbilityMemberMustBeAvailable, +} + +fn extend_imports_data_with_copied_import( + copied_import: CopiedImport, + imported_variables: &mut Vec, + imported_rigids: &mut Vec, + imported_flex: &mut Vec, +) -> Variable { + // not a typo; rigids are turned into flex during type inference, but when imported we must + // consider them rigid variables + imported_rigids.extend(copied_import.rigid); + imported_flex.extend(copied_import.flex); + + // Rigid vars bound to abilities are also treated like rigids. + imported_rigids.extend(copied_import.rigid_able); + imported_flex.extend(copied_import.flex_able); + + imported_variables.extend(copied_import.registered); + + copied_import.variable +} + +fn import_variable_for_symbol( + subs: &mut Subs, + constraints: &mut Constraints, + def_types: &mut Vec<(Symbol, Loc)>, + imported_variables: &mut Vec, + imported_rigids: &mut Vec, + imported_flex: &mut Vec, + cached_symbol_vars: &mut VecMap, + exposed_by_module: &ExposedByModule, + symbol: Symbol, + on_symbol_not_found: OnSymbolNotFound, +) { + let module_id = symbol.module_id(); + match exposed_by_module.get(&module_id) { + Some(ExposedModuleTypes { + exposed_types_storage_subs: exposed_types, + resolved_implementations: _, + }) => { + let variable = match exposed_types + .stored_vars_by_symbol + .iter() + .find(|(s, _)| **s == symbol) + { + None => { + use OnSymbolNotFound::*; + match on_symbol_not_found { + AssertIsBuiltin => { + // Today we define builtins in each module that uses them + // so even though they have a different module name from + // the surrounding module, they are not technically imported + debug_assert!(symbol.is_builtin()); + return; + } + AbilityMemberMustBeAvailable => { + internal_error!("Import ability member {:?} not available", symbol); + } + } + } + Some((_, x)) => *x, + }; + + let copied_import = exposed_types + .storage_subs + .export_variable_to(subs, variable); + + let copied_import_var = extend_imports_data_with_copied_import( + copied_import, + imported_variables, + imported_rigids, + imported_flex, + ); + + let copied_import_index = constraints.push_variable(copied_import_var); + + def_types.push((symbol, Loc::at_zero(copied_import_index))); + + cached_symbol_vars.insert(symbol, copied_import_var); + } + None => { + internal_error!("Imported module {:?} is not available", module_id) + } + } +} + +struct SolveResult { + solved: Solved, + solved_implementations: ResolvedImplementations, + exposed_vars_by_symbol: Vec<(Symbol, Variable)>, + problems: Vec, + abilities_store: AbilitiesStore, + + #[cfg(debug_assertions)] + checkmate: Option, +} + +#[allow(clippy::complexity)] +fn run_solve_solve( + exposed_for_module: ExposedForModule, + mut types: Types, + mut constraints: Constraints, + constraint: ConstraintSoa, + function_kind: FunctionKind, + pending_derives: PendingDerives, + var_store: VarStore, + module: Module, + derived_module: SharedDerivedModule, + + #[cfg(debug_assertions)] checkmate: Option, +) -> SolveResult { + let Module { + exposed_symbols, + aliases, + rigid_variables, + abilities_store: pending_abilities, + .. + } = module; + + let mut imported_rigid_vars: Vec = Vec::new(); + let mut imported_flex_vars: Vec = Vec::new(); + let mut def_types: Vec<(Symbol, Loc)> = Vec::new(); + + let mut subs = Subs::new_from_varstore(var_store); + + let (import_variables, abilities_store) = add_imports( + module.module_id, + &mut constraints, + &mut subs, + pending_abilities, + &exposed_for_module, + &mut def_types, + &mut imported_rigid_vars, + &mut imported_flex_vars, + ); + + let actual_constraint = constraints.let_import_constraint( + imported_rigid_vars, + imported_flex_vars, + def_types, + constraint, + &import_variables, + ); + + let mut solve_aliases = roc_solve::Aliases::with_capacity(aliases.len()); + for (name, (_, alias)) in aliases.iter() { + solve_aliases.insert(&mut types, *name, alias.clone()); + } + + let (solve_output, solved_implementations, exposed_vars_by_symbol) = { + let module_id = module.module_id; + + let solve_config = SolveConfig { + home: module_id, + types, + constraints: &constraints, + root_constraint: actual_constraint, + function_kind, + pending_derives, + exposed_by_module: &exposed_for_module.exposed_by_module, + derived_module, + #[cfg(debug_assertions)] + checkmate, + }; + + let solve_output = roc_solve::module::run_solve( + solve_config, + rigid_variables, + subs, + solve_aliases, + abilities_store, + ); + + let solved_implementations = + extract_module_owned_implementations(module_id, &solve_output.resolved_abilities_store); + + let is_specialization_symbol = |sym| { + solved_implementations + .values() + .any(|resolved_impl| match resolved_impl { + ResolvedImpl::Impl(specialization) => specialization.symbol == sym, + ResolvedImpl::Error => false, + }) + }; + + // Expose anything that is explicitly exposed by the header, or is a specialization of an + // ability. + let exposed_vars_by_symbol: Vec<_> = solve_output + .scope + .vars_by_symbol() + .filter(|(k, _)| { + exposed_symbols.contains(k) + || is_specialization_symbol(*k) + || k.is_exposed_for_builtin_derivers() + }) + .collect(); + + (solve_output, solved_implementations, exposed_vars_by_symbol) + }; + + let roc_solve::module::SolveOutput { + subs, + scope: _, + errors, + resolved_abilities_store, + + #[cfg(debug_assertions)] + checkmate, + } = solve_output; + + SolveResult { + solved: subs, + solved_implementations, + exposed_vars_by_symbol, + problems: errors, + abilities_store: resolved_abilities_store, + + #[cfg(debug_assertions)] + checkmate, + } +} + +fn run_solve<'a>( + module: Module, + ident_ids: IdentIds, + mut module_timing: ModuleTiming, + exposed_for_module: ExposedForModule, + types: Types, + constraints: Constraints, + constraint: ConstraintSoa, + function_kind: FunctionKind, + pending_derives: PendingDerives, + var_store: VarStore, + decls: Declarations, + dep_idents: IdentIdsByModule, + cached_types: CachedTypeState, + derived_module: SharedDerivedModule, + + #[cfg(debug_assertions)] checkmate: Option, +) -> Msg<'a> { + let solve_start = Instant::now(); + + let module_id = module.module_id; + + // TODO remove when we write builtins in roc + let aliases = module.aliases.clone(); + + let mut module = module; + let loc_expects = std::mem::take(&mut module.loc_expects); + let loc_dbgs = std::mem::take(&mut module.loc_dbgs); + let module = module; + + let solve_result = { + if module_id.is_builtin() { + match cached_types.lock().remove(&module_id) { + None => run_solve_solve( + exposed_for_module, + types, + constraints, + constraint, + function_kind, + pending_derives, + var_store, + module, + derived_module, + // + #[cfg(debug_assertions)] + checkmate, + ), + Some(TypeState { + subs, + exposed_vars_by_symbol, + abilities, + solved_implementations, + }) => SolveResult { + solved: Solved(subs), + solved_implementations, + exposed_vars_by_symbol, + problems: vec![], + abilities_store: abilities, + + #[cfg(debug_assertions)] + checkmate: None, + }, + } + } else { + run_solve_solve( + exposed_for_module, + types, + constraints, + constraint, + function_kind, + pending_derives, + var_store, + module, + derived_module, + // + #[cfg(debug_assertions)] + checkmate, + ) + } + }; + + let SolveResult { + solved: mut solved_subs, + solved_implementations, + exposed_vars_by_symbol, + problems, + abilities_store, + + #[cfg(debug_assertions)] + checkmate, + } = solve_result; + + let exposed_types = roc_solve::module::exposed_types_storage_subs( + module_id, + &mut solved_subs, + &exposed_vars_by_symbol, + &solved_implementations, + &abilities_store, + ); + + let solved_module = SolvedModule { + exposed_vars_by_symbol, + problems, + aliases, + solved_implementations, + exposed_types, + }; + + // Record the final timings + let solve_end = Instant::now(); + module_timing.solve = solve_end.duration_since(solve_start); + + // Send the subs to the main thread for processing, + Msg::SolvedTypes { + module_id, + solved_subs, + ident_ids, + decls, + dep_idents, + solved_module, + module_timing, + abilities_store, + loc_expects, + loc_dbgs, + + #[cfg(debug_assertions)] + checkmate, + } +} + +fn unspace<'a, T: Copy>(arena: &'a Bump, items: &[Loc>]) -> &'a [Loc] { + bumpalo::collections::Vec::from_iter_in( + items + .iter() + .map(|item| Loc::at(item.region, item.value.extract_spaces().item)), + arena, + ) + .into_bump_slice() +} + +fn build_package_header<'a>( + arena: &'a Bump, + opt_shorthand: Option<&'a str>, + is_root_module: bool, + filename: PathBuf, + parse_state: roc_parse::state::State<'a>, + module_ids: Arc>>, + ident_ids_by_module: SharedIdentIdsByModule, + header: &PackageHeader<'a>, + comments: &'a [CommentOrNewline<'a>], + module_timing: ModuleTiming, +) -> Result<(ModuleId, PQModuleName<'a>, ModuleHeader<'a>), LoadingProblem<'a>> { + let exposes = bumpalo::collections::Vec::from_iter_in( + unspace(arena, header.exposes.item.items).iter().copied(), + arena, + ); + let packages = unspace(arena, header.packages.item.items); + let exposes_ids = get_exposes_ids( + header.exposes.item.items, + arena, + &module_ids, + &ident_ids_by_module, + ); + let header_type = HeaderType::Package { + // A config_shorthand of "" should be fine + config_shorthand: opt_shorthand.unwrap_or_default(), + exposes: exposes.into_bump_slice(), + exposes_ids: exposes_ids.into_bump_slice(), + }; + + let info = HeaderInfo { + filename, + is_root_module, + opt_shorthand, + packages, + imports: &[], + header_type, + module_comments: comments, + }; + + build_header( + arena, + info, + parse_state, + module_ids, + ident_ids_by_module, + module_timing, + ) +} + +fn build_platform_header<'a>( + arena: &'a Bump, + opt_shorthand: Option<&'a str>, + opt_app_module_id: Option, + filename: PathBuf, + parse_state: roc_parse::state::State<'a>, + module_ids: Arc>>, + exposes_ids: &'a [ModuleId], + ident_ids_by_module: SharedIdentIdsByModule, + header: &PlatformHeader<'a>, + comments: &'a [CommentOrNewline<'a>], + module_timing: ModuleTiming, +) -> Result<(ModuleId, PQModuleName<'a>, ModuleHeader<'a>), LoadingProblem<'a>> { + // If we have an app module, then it's the root module; + // otherwise, we must be the root. + let is_root_module = opt_app_module_id.is_none(); + + let requires = arena.alloc([Loc::at( + header.requires.item.signature.region, + header.requires.item.signature.extract_spaces().item, + )]); + let provides = bumpalo::collections::Vec::from_iter_in( + unspace(arena, header.provides.item.items) + .iter() + .copied() + .zip(requires.iter().copied()), + arena, + ); + let exposes = bumpalo::collections::Vec::from_iter_in( + unspace(arena, header.exposes.item.items).iter().copied(), + arena, + ); + let requires_types = unspace(arena, header.requires.item.rigids.items); + let imports = unspace(arena, header.imports.item.items); + + let header_type = HeaderType::Platform { + // A config_shorthand of "" should be fine + config_shorthand: opt_shorthand.unwrap_or_default(), + exposes_ids, + opt_app_module_id, + provides: provides.into_bump_slice(), + exposes: exposes.into_bump_slice(), + requires, + requires_types, + }; + + let info = HeaderInfo { + filename, + is_root_module, + opt_shorthand, + packages: &[], + imports, + header_type, + module_comments: comments, + }; + + build_header( + arena, + info, + parse_state, + module_ids, + ident_ids_by_module, + module_timing, + ) +} + +#[allow(clippy::unnecessary_wraps)] +fn canonicalize_and_constrain<'a>( + arena: &'a Bump, + module_ids: &ModuleIds, + dep_idents: IdentIdsByModule, + exposed_symbols: VecSet, + aliases: MutMap, + imported_abilities_state: PendingAbilitiesStore, + parsed: ParsedModule<'a>, + skip_constraint_gen: bool, + exposed_module_ids: &[ModuleId], +) -> CanAndCon { + let canonicalize_start = Instant::now(); + + let ParsedModule { + module_id, + header_type, + exposed_ident_ids, + parsed_defs, + exposed_imports, + imported_modules, + mut module_timing, + symbols_from_requires, + .. + } = parsed; + + // _before has an underscore because it's unused in --release builds + let _before = roc_types::types::get_type_clone_count(); + + let parsed_defs_for_docs = parsed_defs.clone(); + let parsed_defs = arena.alloc(parsed_defs); + + let mut var_store = VarStore::default(); + let module_output = canonicalize_module_defs( + arena, + parsed_defs, + &header_type, + module_id, + module_ids, + exposed_ident_ids, + &dep_idents, + aliases, + imported_abilities_state, + exposed_imports, + exposed_symbols, + &symbols_from_requires, + &mut var_store, + ); + let mut types = Types::new(); + + // _after has an underscore because it's unused in --release builds + let _after = roc_types::types::get_type_clone_count(); + + log!( + "canonicalize of {:?} cloned Type {} times ({} -> {})", + module_id, + _after - _before, + _before, + _after + ); + + let canonicalize_end = Instant::now(); + + module_timing.canonicalize = canonicalize_end.duration_since(canonicalize_start); + + // Generate documentation information + // TODO: store timing information? + let module_docs = match header_type { + HeaderType::App { .. } => None, + HeaderType::Platform { .. } | HeaderType::Package { .. } => { + // TODO: actually generate docs for platform and package modules. + None + } + HeaderType::Interface { name, .. } + | HeaderType::Builtin { name, .. } + | HeaderType::Hosted { name, .. } + if exposed_module_ids.contains(&parsed.module_id) => + { + let mut scope = module_output.scope.clone(); + scope.add_docs_imports(); + let docs = crate::docs::generate_module_docs( + scope, + module_id, + module_ids, + name.as_str().into(), + &parsed_defs_for_docs, + exposed_module_ids, + module_output.exposed_symbols.clone(), + parsed.header_comments, + ); + + Some(docs) + } + HeaderType::Interface { .. } | HeaderType::Builtin { .. } | HeaderType::Hosted { .. } => { + // This module isn't exposed by the platform, so don't generate docs for it! + None + } + }; + + // _before has an underscore because it's unused in --release builds + let _before = roc_types::types::get_type_clone_count(); + + let mut constraints = Constraints::new(); + + let constraint = if skip_constraint_gen { + roc_can::constraint::Constraint::True + } else { + constrain_module( + &mut types, + &mut constraints, + module_output.symbols_from_requires, + &module_output.scope.abilities_store, + &module_output.declarations, + module_id, + ) + }; + + // _after has an underscore because it's unused in --release builds + let _after = roc_types::types::get_type_clone_count(); + + log!( + "constraint gen of {:?} cloned Type {} times ({} -> {})", + module_id, + _after - _before, + _before, + _after + ); + + // scope has imported aliases, but misses aliases from inner scopes + // module_output.aliases does have those aliases, so we combine them + let mut aliases: MutMap = module_output + .aliases + .into_iter() + .map(|(k, v)| (k, (true, v))) + .collect(); + + for (name, alias) in module_output.scope.aliases { + match aliases.entry(name) { + Occupied(_) => { + // do nothing + } + Vacant(vacant) => { + let should_include_builtin = matches!( + name.module_id(), + ModuleId::ENCODE + | ModuleId::DECODE + | ModuleId::DICT + | ModuleId::SET + | ModuleId::HASH + | ModuleId::INSPECT + ); + + if !name.is_builtin() || should_include_builtin { + vacant.insert((false, alias)); + } + } + } + } + + let module = Module { + module_id, + exposed_imports: module_output.exposed_imports, + exposed_symbols: module_output.exposed_symbols, + referenced_values: module_output.referenced_values, + referenced_types: module_output.referenced_types, + aliases, + rigid_variables: module_output.rigid_variables, + abilities_store: module_output.scope.abilities_store, + loc_expects: module_output.loc_expects, + loc_dbgs: module_output.loc_dbgs, + }; + + let constrained_module = ConstrainedModule { + module, + declarations: module_output.declarations, + imported_modules, + var_store, + constraints, + constraint, + ident_ids: module_output.scope.locals.ident_ids, + dep_idents, + module_timing, + types, + pending_derives: module_output.pending_derives, + }; + + CanAndCon { + constrained_module, + canonicalization_problems: module_output.problems, + module_docs, + } +} + +fn parse<'a>(arena: &'a Bump, header: ModuleHeader<'a>) -> Result, LoadingProblem<'a>> { + let mut module_timing = header.module_timing; + let parse_start = Instant::now(); + let source = header.parse_state.original_bytes(); + let parse_state = header.parse_state; + let mut parsed_defs = match module_defs().parse(arena, parse_state.clone(), 0) { + Ok((_, success, _state)) => success, + Err((_, fail)) => { + return Err(LoadingProblem::ParsingFailed( + fail.into_file_error(header.module_path, &parse_state), + )); + } + }; + for value in header.defined_values.into_iter() { + // TODO: should these have a region? + parsed_defs.push_value_def(value, Region::zero(), &[], &[]); + } + + // Record the parse end time once, to avoid checking the time a second time + // immediately afterward (for the beginning of canonicalization). + let parse_end = Instant::now(); + + module_timing.parse_body = parse_end.duration_since(parse_start); + + let imported_modules = header.imported_modules; + + // SAFETY: By this point we've already incrementally verified that there + // are no UTF-8 errors in these bytes. If there had been any UTF-8 errors, + // we'd have bailed out before now. + let src = unsafe { from_utf8_unchecked(source) }; + + let ModuleHeader { + module_id, + deps_by_name, + exposed_ident_ids, + exposed_imports, + module_path, + header_type, + symbols_from_requires, + header_comments: header_docs, + .. + } = header; + + let parsed = ParsedModule { + module_id, + module_path, + src, + module_timing, + deps_by_name, + imported_modules, + exposed_ident_ids, + exposed_imports, + parsed_defs, + symbols_from_requires, + header_type, + header_comments: header_docs, + }; + + Ok(Msg::Parsed(parsed)) +} + +fn exposed_from_import<'a>( + entry: &ImportsEntry<'a>, +) -> Option<(QualifiedModuleName<'a>, Vec>)> { + use roc_parse::header::ImportsEntry::*; + + match entry { + Module(module_name, exposes) => { + let mut exposed = Vec::with_capacity(exposes.len()); + + for loc_entry in exposes.iter() { + exposed.push(loc_entry.map(ident_from_exposed)); + } + + let qualified_module_name = QualifiedModuleName { + opt_package: None, + module: module_name.as_str().into(), + }; + + Some((qualified_module_name, exposed)) + } + + Package(package_name, module_name, exposes) => { + let mut exposed = Vec::with_capacity(exposes.len()); + + for loc_entry in exposes.iter() { + exposed.push(loc_entry.map(ident_from_exposed)); + } + + let qualified_module_name = QualifiedModuleName { + opt_package: Some(package_name), + module: module_name.as_str().into(), + }; + + Some((qualified_module_name, exposed)) + } + + IngestedFile(_, _) => None, + } +} + +fn value_def_from_imports<'a>( + arena: &'a Bump, + header_path: &Path, + entry: &Loc>, +) -> Result>, LoadingProblem<'a>> { + use roc_parse::header::ImportsEntry::*; + + let value = match entry.value { + Module(_, _) => None, + Package(_, _, _) => None, + IngestedFile(ingested_path, typed_ident) => { + let file_path = if let StrLiteral::PlainLine(ingested_path) = ingested_path { + let mut file_path = header_path.to_path_buf(); + // Remove the header file name and push the new path. + file_path.pop(); + file_path.push(ingested_path); + + match fs::metadata(&file_path) { + Ok(md) => { + if md.is_dir() { + return Err(LoadingProblem::FileProblem { + filename: file_path, + // TODO: change to IsADirectory once that is stable. + error: io::ErrorKind::InvalidInput, + }); + } + file_path + } + Err(e) => { + return Err(LoadingProblem::FileProblem { + filename: file_path, + error: e.kind(), + }); + } + } + } else { + todo!( + "Only plain strings are supported. Other cases should be made impossible here" + ); + }; + let typed_ident = typed_ident.extract_spaces().item; + let ident = arena.alloc(typed_ident.ident.map_owned(Pattern::Identifier)); + let ann_type = arena.alloc(typed_ident.ann); + Some(ValueDef::AnnotatedBody { + ann_pattern: ident, + ann_type, + comment: None, + body_pattern: ident, + body_expr: arena + .alloc(entry.with_value(Expr::IngestedFile(arena.alloc(file_path), ann_type))), + }) + } + }; + + Ok(value) +} + +fn ident_from_exposed(entry: &Spaced<'_, ExposedName<'_>>) -> Ident { + entry.extract_spaces().item.as_str().into() +} + +fn make_specializations<'a>( + arena: &'a Bump, + home: ModuleId, + mut ident_ids: IdentIds, + mut subs: Subs, + procs_base: ProcsBase<'a>, + mut layout_cache: LayoutCache<'a>, + specializations_we_must_make: Vec>, + mut module_timing: ModuleTiming, + target_info: TargetInfo, + world_abilities: WorldAbilities, + exposed_by_module: &ExposedByModule, + derived_module: SharedDerivedModule, + mut expectations: Option, +) -> Msg<'a> { + let make_specializations_start = Instant::now(); + let mut update_mode_ids = UpdateModeIds::new(); + // do the thing + let mut mono_env = roc_mono::ir::Env { + arena, + subs: &mut subs, + expectation_subs: expectations.as_mut().map(|e| &mut e.subs), + home, + ident_ids: &mut ident_ids, + target_info, + update_mode_ids: &mut update_mode_ids, + // call_specialization_counter=0 is reserved + call_specialization_counter: 1, + abilities: AbilitiesView::World(&world_abilities), + exposed_by_module, + derived_module: &derived_module, + struct_indexing: UsageTrackingMap::default(), + }; + + let mut procs = Procs::new_in(arena); + + let host_exposed_symbols: bumpalo::collections::Vec<_> = + procs_base.get_host_exposed_symbols().collect_in(arena); + + for (symbol, partial_proc) in procs_base.partial_procs.into_iter() { + procs.partial_procs.insert(symbol, partial_proc); + } + + procs.host_exposed_symbols = host_exposed_symbols.into_bump_slice(); + procs.module_thunks = procs_base.module_thunks; + procs.runtime_errors = procs_base.runtime_errors; + procs.imported_module_thunks = procs_base.imported_module_thunks; + + // TODO: for now this final specialization pass is sequential, + // with no parallelization at all. We should try to parallelize + // this, but doing so will require a redesign of Procs. + procs = roc_mono::ir::specialize_all( + &mut mono_env, + procs, + specializations_we_must_make, + procs_base.host_specializations, + &mut layout_cache, + ); + + let external_specializations_requested = procs.externals_we_need.clone(); + let (procedures, host_exposed_lambda_sets, restored_procs_base) = + procs.get_specialized_procs_without_rc(); + + // Turn `Bytes.Decode.IdentId(238)` into `Bytes.Decode.238`, we rely on this in mono tests + mono_env.home.register_debug_idents(mono_env.ident_ids); + + let make_specializations_end = Instant::now(); + module_timing + .make_specializations + .push(make_specializations_end.duration_since(make_specializations_start)); + + Msg::MadeSpecializations { + module_id: home, + ident_ids, + layout_cache, + procs_base: restored_procs_base, + procedures, + host_exposed_lambda_sets, + update_mode_ids, + subs, + expectations, + external_specializations_requested, + module_timing, + } +} + +fn build_pending_specializations<'a>( + arena: &'a Bump, + solved_subs: Solved, + imported_module_thunks: &'a [Symbol], + home: ModuleId, + mut ident_ids: IdentIds, + declarations: Declarations, + mut module_timing: ModuleTiming, + mut layout_cache: LayoutCache<'a>, + target_info: TargetInfo, + exposed_to_host: ExposedToHost, + exposed_by_module: &ExposedByModule, + abilities_store: AbilitiesStore, + derived_module: SharedDerivedModule, + mut expectations: Option, + build_expects: bool, +) -> Msg<'a> { + let find_specializations_start = Instant::now(); + + let mut module_thunks = bumpalo::collections::Vec::new_in(arena); + let mut toplevel_expects = ToplevelExpects::default(); + + let mut procs_base = ProcsBase { + partial_procs: BumpMap::default(), + module_thunks: &[], + host_specializations: roc_mono::ir::HostSpecializations::new(), + runtime_errors: BumpMap::default(), + imported_module_thunks, + }; + + let mut update_mode_ids = UpdateModeIds::new(); + let mut subs = solved_subs.into_inner(); + let mut mono_env = roc_mono::ir::Env { + arena, + subs: &mut subs, + expectation_subs: expectations.as_mut().map(|e| &mut e.subs), + home, + ident_ids: &mut ident_ids, + target_info, + update_mode_ids: &mut update_mode_ids, + // call_specialization_counter=0 is reserved + call_specialization_counter: 1, + // NB: for getting pending specializations the module view is enough because we only need + // to know the types and abilities in our modules. Only for building *all* specializations + // do we need a global view. + abilities: AbilitiesView::Module(&abilities_store), + exposed_by_module, + derived_module: &derived_module, + struct_indexing: UsageTrackingMap::default(), + }; + + let layout_cache_snapshot = layout_cache.snapshot(); + + let get_host_annotation = |declarations: &Declarations, index: usize| { + declarations + .host_exposed_annotations + .get(&index) + .map(|(v, _)| v) + .copied() + }; + + // Add modules' decls to Procs + for index in 0..declarations.len() { + use roc_can::expr::DeclarationTag::*; + + let symbol = declarations.symbols[index].value; + let expr_var = declarations.variables[index]; + + let is_host_exposed = exposed_to_host.top_level_values.contains_key(&symbol); + + let body = declarations.expressions[index].clone(); + + let tag = declarations.declarations[index]; + match tag { + Value => { + // If this is an exposed symbol, we need to + // register it as such. Otherwise, since it + // never gets called by Roc code, it will never + // get specialized! + if is_host_exposed { + let layout_result = + layout_cache.raw_from_var(mono_env.arena, expr_var, mono_env.subs); + + // cannot specialize when e.g. main's type contains type variables + if let Err(e) = layout_result { + match e { + LayoutProblem::Erroneous => { + let message = "top level function has erroneous type"; + procs_base.runtime_errors.insert(symbol, message); + continue; + } + LayoutProblem::UnresolvedTypeVar(v) => { + let message = format!( + "top level function has unresolved type variable {v:?}" + ); + procs_base + .runtime_errors + .insert(symbol, mono_env.arena.alloc(message)); + continue; + } + } + } + + procs_base.host_specializations.insert_host_exposed( + mono_env.subs, + LambdaName::no_niche(symbol), + get_host_annotation(&declarations, index), + expr_var, + ); + } + + match body.value { + roc_can::expr::Expr::RecordAccessor(accessor_data) => { + let fresh_record_symbol = mono_env.unique_symbol(); + let closure_data = accessor_data.to_closure_data(fresh_record_symbol); + register_toplevel_function_into_procs_base( + &mut mono_env, + &mut procs_base, + closure_data.name, + expr_var, + closure_data.arguments, + closure_data.return_type, + *closure_data.loc_body, + false, + ); + } + _ => { + // mark this symbols as a top-level thunk before any other work on the procs + module_thunks.push(symbol); + + let proc = PartialProc { + annotation: expr_var, + // This is a 0-arity thunk, so it has no arguments. + pattern_symbols: &[], + // This is a top-level definition, so it cannot capture anything + captured_symbols: CapturedSymbols::None, + body: body.value, + body_var: expr_var, + // This is a 0-arity thunk, so it cannot be recursive + is_self_recursive: false, + }; + + procs_base.partial_procs.insert(symbol, proc); + } + } + } + Function(f_index) | Recursive(f_index) | TailRecursive(f_index) => { + let function_def = &declarations.function_bodies[f_index.index()].value; + // this is a top-level definition, it should not capture anything + debug_assert!( + function_def.captured_symbols.is_empty(), + "{:?}", + (symbol, symbol.module_id(), &function_def.captured_symbols) + ); + + // If this is an exposed symbol, we need to + // register it as such. Otherwise, since it + // never gets called by Roc code, it will never + // get specialized! + if is_host_exposed { + let layout_result = + layout_cache.raw_from_var(mono_env.arena, expr_var, mono_env.subs); + + // cannot specialize when e.g. main's type contains type variables + if let Err(e) = layout_result { + match e { + LayoutProblem::Erroneous => { + let message = "top level function has erroneous type"; + procs_base.runtime_errors.insert(symbol, message); + continue; + } + LayoutProblem::UnresolvedTypeVar(v) => { + let message = format!( + "top level function has unresolved type variable {v:?}" + ); + procs_base + .runtime_errors + .insert(symbol, mono_env.arena.alloc(message)); + continue; + } + } + } + + procs_base.host_specializations.insert_host_exposed( + mono_env.subs, + LambdaName::no_niche(symbol), + get_host_annotation(&declarations, index), + expr_var, + ); + } + + let is_recursive = matches!(tag, Recursive(_) | TailRecursive(_)); + + register_toplevel_function_into_procs_base( + &mut mono_env, + &mut procs_base, + symbol, + expr_var, + function_def.arguments.clone(), + function_def.return_type, + body, + is_recursive, + ); + } + Destructure(d_index) => { + let loc_pattern = &declarations.destructs[d_index.index()].loc_pattern; + + use roc_can::pattern::Pattern; + let symbol = match &loc_pattern.value { + Pattern::Identifier(_) => { + debug_assert!(false, "identifier ended up in Destructure {symbol:?}"); + symbol + } + Pattern::AbilityMemberSpecialization { ident, specializes } => { + debug_assert!( + false, + "ability member ended up in Destructure {ident:?} specializes {specializes:?}" + ); + symbol + } + Pattern::Shadowed(_, _, shadowed) => { + // this seems to work for now + *shadowed + } + _ => todo!("top-level destrucuture patterns are not implemented"), + }; + + // mark this symbols as a top-level thunk before any other work on the procs + module_thunks.push(symbol); + + // If this is an exposed symbol, we need to + // register it as such. Otherwise, since it + // never gets called by Roc code, it will never + // get specialized! + if is_host_exposed { + let layout_result = + layout_cache.raw_from_var(mono_env.arena, expr_var, mono_env.subs); + + // cannot specialize when e.g. main's type contains type variables + if let Err(e) = layout_result { + match e { + LayoutProblem::Erroneous => { + let message = "top level function has erroneous type"; + procs_base.runtime_errors.insert(symbol, message); + continue; + } + LayoutProblem::UnresolvedTypeVar(v) => { + let message = format!( + "top level function has unresolved type variable {v:?}" + ); + procs_base + .runtime_errors + .insert(symbol, mono_env.arena.alloc(message)); + continue; + } + } + } + + procs_base.host_specializations.insert_host_exposed( + mono_env.subs, + LambdaName::no_niche(symbol), + get_host_annotation(&declarations, index), + expr_var, + ); + } + + let proc = PartialProc { + annotation: expr_var, + // This is a 0-arity thunk, so it has no arguments. + pattern_symbols: &[], + // This is a top-level definition, so it cannot capture anything + captured_symbols: CapturedSymbols::None, + body: body.value, + body_var: expr_var, + // This is a 0-arity thunk, so it cannot be recursive + is_self_recursive: false, + }; + + procs_base.partial_procs.insert(symbol, proc); + } + MutualRecursion { .. } => { + // the declarations of this group will be treaded individually by later iterations + } + Expectation => { + // skip expectations if we're not going to run them + if !build_expects { + continue; + } + + // mark this symbol as a top-level thunk before any other work on the procs + module_thunks.push(symbol); + + let expr_var = Variable::EMPTY_RECORD; + + let is_host_exposed = true; + + // If this is an exposed symbol, we need to + // register it as such. Otherwise, since it + // never gets called by Roc code, it will never + // get specialized! + if is_host_exposed { + let layout_result = + layout_cache.raw_from_var(mono_env.arena, expr_var, mono_env.subs); + + // cannot specialize when e.g. main's type contains type variables + if let Err(e) = layout_result { + match e { + LayoutProblem::Erroneous => { + let message = "top level function has erroneous type"; + procs_base.runtime_errors.insert(symbol, message); + continue; + } + LayoutProblem::UnresolvedTypeVar(v) => { + let message = format!( + "top level function has unresolved type variable {v:?}" + ); + procs_base + .runtime_errors + .insert(symbol, mono_env.arena.alloc(message)); + continue; + } + } + } + + procs_base.host_specializations.insert_host_exposed( + mono_env.subs, + LambdaName::no_niche(symbol), + None, + expr_var, + ); + } + + let body = roc_can::expr::toplevel_expect_to_inline_expect_pure(body); + + let proc = PartialProc { + annotation: expr_var, + // This is a 0-arity thunk, so it has no arguments. + pattern_symbols: &[], + // This is a top-level definition, so it cannot capture anything + captured_symbols: CapturedSymbols::None, + body: body.value, + body_var: expr_var, + // This is a 0-arity thunk, so it cannot be recursive + is_self_recursive: false, + }; + + // extend the region of the expect expression with the region of the preceding + // comment, so it is shown in failure/panic messages + let name_region = declarations.symbols[index].region; + let expr_region = declarations.expressions[index].region; + let region = Region::span_across(&name_region, &expr_region); + + toplevel_expects.pure.insert(symbol, region); + procs_base.partial_procs.insert(symbol, proc); + } + ExpectationFx => { + // skip expectations if we're not going to run them + if !build_expects { + continue; + } + + // mark this symbol as a top-level thunk before any other work on the procs + module_thunks.push(symbol); + + let expr_var = Variable::EMPTY_RECORD; + + let is_host_exposed = true; + + // If this is an exposed symbol, we need to + // register it as such. Otherwise, since it + // never gets called by Roc code, it will never + // get specialized! + if is_host_exposed { + let layout_result = + layout_cache.raw_from_var(mono_env.arena, expr_var, mono_env.subs); + + // cannot specialize when e.g. main's type contains type variables + if let Err(e) = layout_result { + match e { + LayoutProblem::Erroneous => { + let message = "top level function has erroneous type"; + procs_base.runtime_errors.insert(symbol, message); + continue; + } + LayoutProblem::UnresolvedTypeVar(v) => { + let message = format!( + "top level function has unresolved type variable {v:?}" + ); + procs_base + .runtime_errors + .insert(symbol, mono_env.arena.alloc(message)); + continue; + } + } + } + + procs_base.host_specializations.insert_host_exposed( + mono_env.subs, + LambdaName::no_niche(symbol), + None, + expr_var, + ); + } + + let body = roc_can::expr::toplevel_expect_to_inline_expect_fx(body); + + let proc = PartialProc { + annotation: expr_var, + // This is a 0-arity thunk, so it has no arguments. + pattern_symbols: &[], + // This is a top-level definition, so it cannot capture anything + captured_symbols: CapturedSymbols::None, + body: body.value, + body_var: expr_var, + // This is a 0-arity thunk, so it cannot be recursive + is_self_recursive: false, + }; + + // extend the region of the expect expression with the region of the preceding + // comment, so it is shown in failure/panic messages + let name_region = declarations.symbols[index].region; + let expr_region = declarations.expressions[index].region; + let region = Region::span_across(&name_region, &expr_region); + + toplevel_expects.fx.insert(symbol, region); + procs_base.partial_procs.insert(symbol, proc); + } + } + } + + layout_cache.rollback_to(layout_cache_snapshot); + + procs_base.module_thunks = module_thunks.into_bump_slice(); + + let find_specializations_end = Instant::now(); + module_timing.find_specializations = + find_specializations_end.duration_since(find_specializations_start); + + Msg::FoundSpecializations { + module_id: home, + solved_subs: Solved(subs), + ident_ids, + layout_cache, + procs_base, + module_timing, + abilities_store, + toplevel_expects, + expectations, + } +} + +fn register_toplevel_function_into_procs_base<'a>( + mono_env: &mut roc_mono::ir::Env<'a, '_>, + procs_base: &mut ProcsBase<'a>, + symbol: Symbol, + expr_var: Variable, + arguments: Vec<( + Variable, + roc_can::expr::AnnotatedMark, + Loc, + )>, + return_type: Variable, + body: Loc, + is_recursive: bool, +) { + let partial_proc = PartialProc::from_named_function( + mono_env, + expr_var, + arguments, + body, + CapturedSymbols::None, + is_recursive, + return_type, + ); + + procs_base.partial_procs.insert(symbol, partial_proc); +} + +/// Loads derived ability members up for specialization into the Derived module, prior to making +/// their specializations. +// TODO: right now, this runs sequentially, and no other modules are mono'd in parallel to the +// derived module. +fn load_derived_partial_procs<'a>( + home: ModuleId, + arena: &'a Bump, + subs: &mut Subs, + ident_ids: &mut IdentIds, + derived_module: &SharedDerivedModule, + module_timing: &mut ModuleTiming, + target_info: TargetInfo, + exposed_by_module: &ExposedByModule, + procs_base: &mut ProcsBase<'a>, + world_abilities: &mut WorldAbilities, +) { + debug_assert_eq!(home, ModuleId::DERIVED_GEN); + + let load_derived_procs_start = Instant::now(); + + let mut new_module_thunks = bumpalo::collections::Vec::new_in(arena); + + let mut update_mode_ids = UpdateModeIds::new(); + + let derives_to_add = { + let mut derived_module = derived_module.lock().unwrap(); + + derived_module.iter_load_for_gen_module(subs, |symbol| { + !procs_base.partial_procs.contains_key(&symbol) + }) + }; + + // TODO: we can be even lazier here if we move `add_def_to_module` to happen in mono. Also, the + // timings would be more accurate. + for (derived_symbol, (derived_expr, derived_expr_var)) in derives_to_add.into_iter() { + let mut mono_env = roc_mono::ir::Env { + arena, + subs, + // There are no derived expectations. + expectation_subs: None, + home, + ident_ids, + target_info, + update_mode_ids: &mut update_mode_ids, + // call_specialization_counter=0 is reserved + call_specialization_counter: 1, + // NB: for getting pending specializations the module view is enough because we only need + // to know the types and abilities in our modules. Only for building *all* specializations + // do we need a global view. + abilities: AbilitiesView::World(world_abilities), + exposed_by_module, + derived_module, + struct_indexing: UsageTrackingMap::default(), + }; + + let partial_proc = match derived_expr { + roc_can::expr::Expr::Closure(roc_can::expr::ClosureData { + function_type, + arguments, + loc_body, + captured_symbols, + return_type, + recursive, + .. + }) => { + debug_assert!(captured_symbols.is_empty()); + PartialProc::from_named_function( + &mut mono_env, + function_type, + arguments.clone(), + *loc_body, + CapturedSymbols::None, + recursive.is_recursive(), + return_type, + ) + } + _ => { + // mark this symbols as a top-level thunk before any other work on the procs + new_module_thunks.push(derived_symbol); + + PartialProc { + annotation: derived_expr_var, + // This is a 0-arity thunk, so it has no arguments. + pattern_symbols: &[], + // This is a top-level definition, so it cannot capture anything + captured_symbols: CapturedSymbols::None, + body: derived_expr, + body_var: derived_expr_var, + // This is a 0-arity thunk, so it cannot be recursive + is_self_recursive: false, + } + } + }; + + procs_base + .partial_procs + .insert(derived_symbol, partial_proc); + } + + if !new_module_thunks.is_empty() { + new_module_thunks.extend(procs_base.module_thunks); + procs_base.module_thunks = new_module_thunks.into_bump_slice(); + } + + let load_derived_procs_end = Instant::now(); + + module_timing.find_specializations = + load_derived_procs_end.duration_since(load_derived_procs_start); +} + +fn run_task<'a>( + task: BuildTask<'a>, + arena: &'a Bump, + src_dir: &Path, + msg_tx: MsgSender<'a>, + roc_cache_dir: RocCacheDir<'_>, + target_info: TargetInfo, +) -> Result<(), LoadingProblem<'a>> { + use BuildTask::*; + + let msg = match task { + LoadModule { + module_name, + module_ids, + shorthands, + ident_ids_by_module, + } => load_module( + arena, + src_dir, + module_name, + module_ids, + shorthands, + roc_cache_dir, + ident_ids_by_module, + ) + .map(|HeaderOutput { msg, .. }| msg), + Parse { header } => parse(arena, header), + CanonicalizeAndConstrain { + parsed, + module_ids, + dep_idents, + exposed_symbols, + aliases, + abilities_store, + skip_constraint_gen, + exposed_module_ids, + } => { + let can_and_con = canonicalize_and_constrain( + arena, + &module_ids, + dep_idents, + exposed_symbols, + aliases, + abilities_store, + parsed, + skip_constraint_gen, + exposed_module_ids, + ); + + Ok(Msg::CanonicalizedAndConstrained(can_and_con)) + } + Solve { + module, + module_timing, + exposed_for_module, + types, + constraints, + constraint, + function_kind, + pending_derives, + var_store, + ident_ids, + declarations, + dep_idents, + cached_subs, + derived_module, + + #[cfg(debug_assertions)] + checkmate, + } => Ok(run_solve( + module, + ident_ids, + module_timing, + exposed_for_module, + types, + constraints, + constraint, + function_kind, + pending_derives, + var_store, + declarations, + dep_idents, + cached_subs, + derived_module, + // + #[cfg(debug_assertions)] + checkmate, + )), + BuildPendingSpecializations { + module_id, + ident_ids, + decls, + module_timing, + layout_cache, + solved_subs, + imported_module_thunks, + exposed_to_host, + abilities_store, + exposed_by_module, + derived_module, + expectations, + build_expects, + } => Ok(build_pending_specializations( + arena, + solved_subs, + imported_module_thunks, + module_id, + ident_ids, + decls, + module_timing, + layout_cache, + target_info, + exposed_to_host, + &exposed_by_module, + abilities_store, + derived_module, + expectations, + build_expects, + )), + MakeSpecializations { + module_id, + ident_ids, + subs, + procs_base, + layout_cache, + specializations_we_must_make, + module_timing, + world_abilities, + exposed_by_module, + derived_module, + expectations, + } => Ok(make_specializations( + arena, + module_id, + ident_ids, + subs, + procs_base, + layout_cache, + specializations_we_must_make, + module_timing, + target_info, + world_abilities, + &exposed_by_module, + derived_module, + expectations, + )), + }?; + + msg_tx + .send(msg) + .map_err(|_| LoadingProblem::ChannelProblem(ChannelProblem::FailedToSendTaskMsg))?; + + Ok(()) +} + +fn to_import_cycle_report( + module_ids: ModuleIds, + all_ident_ids: IdentIdsByModule, + import_cycle: Vec, + filename: PathBuf, + render: RenderTarget, +) -> String { + use roc_reporting::report::{Report, RocDocAllocator, DEFAULT_PALETTE}; + use ven_pretty::DocAllocator; + + // import_cycle looks like CycleModule, Import1, ..., ImportN, CycleModule + // In a self-referential case, it just looks like CycleModule, CycleModule. + debug_assert!(import_cycle.len() >= 2); + let source_of_cycle = import_cycle.first().unwrap(); + + // We won't be printing any lines for this report, so this is okay. + // TODO: it would be nice to show how each module imports another in the cycle. + let src_lines = &[]; + + let interns = Interns { + module_ids, + all_ident_ids, + }; + let alloc = RocDocAllocator::new(src_lines, *source_of_cycle, &interns); + + let doc = alloc.stack([ + alloc.concat([ + alloc.reflow("I can't compile "), + alloc.module(*source_of_cycle), + alloc.reflow( + " because it depends on itself through the following chain of module imports:", + ), + ]), + roc_reporting::report::cycle( + &alloc, + 4, + alloc.module(*source_of_cycle), + import_cycle + .into_iter() + .skip(1) + .map(|module| alloc.module(module)) + .collect(), + ), + alloc.reflow("Cyclic dependencies are not allowed in Roc! Can you restructure a module in this import chain so that it doesn't have to depend on itself?") + ]); + + let report = Report { + filename, + doc, + title: "IMPORT CYCLE".to_string(), + severity: Severity::RuntimeError, + }; + + let mut buf = String::new(); + let palette = DEFAULT_PALETTE; + report.render(render, &mut buf, &alloc, &palette); + buf +} + +fn to_incorrect_module_name_report<'a>( + module_ids: ModuleIds, + all_ident_ids: IdentIdsByModule, + problem: IncorrectModuleName<'a>, + filename: PathBuf, + src: &'a [u8], + render: RenderTarget, +) -> String { + use roc_reporting::report::{Report, RocDocAllocator, DEFAULT_PALETTE}; + use ven_pretty::DocAllocator; + + let IncorrectModuleName { + module_id, + found, + expected, + } = problem; + + // SAFETY: if the module was not UTF-8, that would be reported as a parsing problem, rather + // than an incorrect module name problem (the latter can happen only after parsing). + let src = unsafe { from_utf8_unchecked(src) }; + let src_lines = src.lines().collect::>(); + let lines = LineInfo::new(src); + + let interns = Interns { + module_ids, + all_ident_ids, + }; + let alloc = RocDocAllocator::new(&src_lines, module_id, &interns); + + let doc = alloc.stack([ + alloc.reflow("This module has a different name than I expected:"), + alloc.region(lines.convert_region(found.region)), + alloc.reflow("Based on the nesting and use of this module, I expect it to have name"), + alloc.pq_module_name(expected).indent(4), + ]); + + let report = Report { + filename, + doc, + title: "INCORRECT MODULE NAME".to_string(), + severity: Severity::RuntimeError, + }; + + let mut buf = String::new(); + let palette = DEFAULT_PALETTE; + report.render(render, &mut buf, &alloc, &palette); + buf +} + +fn to_parse_problem_report<'a>( + problem: FileError<'a, SyntaxError<'a>>, + mut module_ids: ModuleIds, + all_ident_ids: IdentIdsByModule, + render: RenderTarget, + palette: Palette, +) -> String { + use roc_reporting::report::{parse_problem, RocDocAllocator}; + + // TODO this is not in fact safe + let src = unsafe { from_utf8_unchecked(problem.problem.bytes) }; + let src_lines = src.lines().collect::>(); + // let mut src_lines: Vec<&str> = problem.prefix.lines().collect(); + // src_lines.extend(src.lines().skip(1)); + + let module_id = module_ids.get_or_insert(&"find module name somehow?".into()); + + let interns = Interns { + module_ids, + all_ident_ids, + }; + + // Report parsing and canonicalization problems + let alloc = RocDocAllocator::new(&src_lines, module_id, &interns); + + let starting_line = 0; + + let lines = LineInfo::new(src); + + let report = parse_problem( + &alloc, + &lines, + problem.filename.clone(), + starting_line, + problem, + ); + + let mut buf = String::new(); + + report.render(render, &mut buf, &alloc, &palette); + + buf +} + +fn to_missing_platform_report(module_id: ModuleId, other: &PlatformPath) -> String { + use roc_reporting::report::{Report, RocDocAllocator, DEFAULT_PALETTE}; + use ven_pretty::DocAllocator; + use PlatformPath::*; + + // Report parsing and canonicalization problems + let interns = Interns::default(); + let alloc = RocDocAllocator::new(&[], module_id, &interns); + + let report = { + match other { + Valid(_) => unreachable!(), + NotSpecified => { + let doc = alloc.stack([ + alloc.reflow("I could not find a platform based on your input file."), + alloc.reflow(r"Does the module header contain an entry that looks like this:"), + alloc + .parser_suggestion(" packages { pf: \"platform\" }") + .indent(4), + alloc.reflow("See also TODO."), + ]); + + Report { + filename: "UNKNOWN.roc".into(), + doc, + title: "NO PLATFORM".to_string(), + severity: Severity::RuntimeError, + } + } + RootIsInterface => { + let doc = alloc.stack([ + alloc.reflow( + r"The input file is an `interface` module, but only `app` modules can be run.", + ), + alloc.reflow(r"Tip: You can use `roc check` or `roc test` to verify an interface module like this one."), + ]); + + Report { + filename: "UNKNOWN.roc".into(), + doc, + title: "NO PLATFORM".to_string(), + severity: Severity::RuntimeError, + } + } + RootIsHosted => { + let doc = alloc.stack([ + alloc.reflow( + r"The input file is a `hosted` module, but only `app` modules can be run.", + ), + alloc.reflow(r"Tip: You can use `roc check` or `roc test` to verify a hosted module like this one."), + ]); + + Report { + filename: "UNKNOWN.roc".into(), + doc, + title: "NO PLATFORM".to_string(), + severity: Severity::RuntimeError, + } + } + RootIsPlatformModule => { + let doc = alloc.stack([ + alloc.reflow( + r"The input file is a `platform` module, but only `app` modules can be run.", + ), + alloc.reflow(r"Tip: You can use `roc check` or `roc test` to verify a platform module like this one."), + ]); + + Report { + filename: "UNKNOWN.roc".into(), + doc, + title: "NO PLATFORM".to_string(), + severity: Severity::RuntimeError, + } + } + } + }; + + let palette = DEFAULT_PALETTE; + let mut buf = String::new(); + report.render_color_terminal(&mut buf, &alloc, &palette); + + buf +} diff --git a/crates/compiler/load_internal/src/lib.rs b/crates/compiler/load_internal/src/lib.rs new file mode 100644 index 0000000000..24226d4c6b --- /dev/null +++ b/crates/compiler/load_internal/src/lib.rs @@ -0,0 +1,30 @@ +//! The internal implementation of roc_load, separate from roc_load to support caching. +#![warn(clippy::dbg_macro)] +// See github.com/roc-lang/roc/issues/800 for discussion of the large_enum_variant check. +#![allow(clippy::large_enum_variant)] + +use roc_module::symbol::ModuleId; +pub mod docs; +pub mod file; +pub mod module; +mod module_cache; +mod work; + +#[cfg(target_family = "wasm")] +mod wasm_instant; + +pub const BUILTIN_MODULES: &[(ModuleId, &str)] = &[ + (ModuleId::BOOL, "Bool"), + (ModuleId::RESULT, "Result"), + (ModuleId::NUM, "Num"), + (ModuleId::LIST, "List"), + (ModuleId::STR, "Str"), + (ModuleId::DICT, "Dict"), + (ModuleId::SET, "Set"), + (ModuleId::BOX, "Box"), + (ModuleId::ENCODE, "Encode"), + (ModuleId::DECODE, "Decode"), + (ModuleId::HASH, "Hash"), + (ModuleId::INSPECT, "Inspect"), + (ModuleId::JSON, "TotallyNotJson"), +]; diff --git a/crates/compiler/load_internal/src/module.rs b/crates/compiler/load_internal/src/module.rs new file mode 100644 index 0000000000..c106c4083c --- /dev/null +++ b/crates/compiler/load_internal/src/module.rs @@ -0,0 +1,300 @@ +use crate::docs::ModuleDocumentation; +use roc_can::constraint::{Constraint as ConstraintSoa, Constraints}; +use roc_can::expr::{DbgLookup, ExpectLookup}; +use roc_can::{ + abilities::AbilitiesStore, + expr::{Declarations, PendingDerives}, + module::{Module, ResolvedImplementations}, +}; +use roc_collections::{MutMap, MutSet, VecMap}; +use roc_module::ident::Ident; +use roc_module::symbol::{ + IdentIds, IdentIdsByModule, Interns, ModuleId, PQModuleName, PackageQualified, Symbol, +}; +use roc_mono::ir::{GlueLayouts, HostExposedLambdaSets, LambdaSetId, Proc, ProcLayout, ProcsBase}; +use roc_mono::layout::{LayoutCache, STLayoutInterner}; +use roc_parse::ast::{CommentOrNewline, Defs, TypeAnnotation, ValueDef}; +use roc_parse::header::{HeaderType, PackageName}; +use roc_region::all::{Loc, Region}; +use roc_solve::module::Solved; +use roc_solve_problem::TypeError; +use roc_types::subs::{ExposedTypesStorageSubs, Subs, VarStore, Variable}; +use roc_types::types::{Alias, Types}; +use std::path::PathBuf; + +#[cfg(target_family = "wasm")] +use crate::wasm_instant::{Duration, Instant}; +#[cfg(not(target_family = "wasm"))] +use std::time::{Duration, Instant}; + +#[derive(Debug)] +pub struct LoadedModule { + pub module_id: ModuleId, + pub interns: Interns, + pub solved: Solved, + pub can_problems: MutMap>, + pub type_problems: MutMap>, + pub declarations_by_id: MutMap, + pub exposed_to_host: MutMap, + pub dep_idents: IdentIdsByModule, + pub exposed_aliases: MutMap, + pub exposed_values: Vec, + pub exposed_types_storage: ExposedTypesStorageSubs, + pub resolved_implementations: ResolvedImplementations, + pub sources: MutMap)>, + pub timings: MutMap, + pub docs_by_module: Vec<(ModuleId, ModuleDocumentation)>, + pub abilities_store: AbilitiesStore, + pub typechecked: MutMap, +} + +impl LoadedModule { + pub fn total_problems(&self) -> usize { + let mut total = 0; + + for problems in self.can_problems.values() { + total += problems.len(); + } + + for problems in self.type_problems.values() { + total += problems.len(); + } + + total + } + + pub fn exposed_values_str(&self) -> Vec<&str> { + self.exposed_values + .iter() + .map(|symbol| symbol.as_str(&self.interns)) + .collect() + } + + pub fn exposed_aliases_str(&self) -> Vec<&str> { + self.exposed_aliases + .keys() + .map(|symbol| symbol.as_str(&self.interns)) + .collect() + } +} + +#[derive(Debug)] +pub(crate) struct ModuleHeader<'a> { + pub(crate) module_id: ModuleId, + pub(crate) module_path: PathBuf, + pub(crate) is_root_module: bool, + pub(crate) exposed_ident_ids: IdentIds, + pub(crate) deps_by_name: MutMap, ModuleId>, + pub(crate) packages: MutMap<&'a str, PackageName<'a>>, + pub(crate) imported_modules: MutMap, + pub(crate) package_qualified_imported_modules: MutSet>, + pub(crate) exposes: Vec, + pub(crate) exposed_imports: MutMap, + pub(crate) parse_state: roc_parse::state::State<'a>, + pub(crate) header_type: HeaderType<'a>, + pub(crate) header_comments: &'a [CommentOrNewline<'a>], + pub(crate) symbols_from_requires: Vec<(Loc, Loc>)>, + pub(crate) module_timing: ModuleTiming, + pub(crate) defined_values: Vec>, +} + +#[derive(Debug)] +pub(crate) struct ConstrainedModule { + pub(crate) module: Module, + pub(crate) declarations: Declarations, + pub(crate) imported_modules: MutMap, + pub(crate) constraints: Constraints, + pub(crate) constraint: ConstraintSoa, + pub(crate) ident_ids: IdentIds, + pub(crate) var_store: VarStore, + pub(crate) dep_idents: IdentIdsByModule, + pub(crate) module_timing: ModuleTiming, + pub(crate) types: Types, + // Rather than adding pending derives as constraints, hand them directly to solve because they + // must be solved at the end of a module. + pub(crate) pending_derives: PendingDerives, +} + +#[derive(Debug)] +pub struct TypeCheckedModule<'a> { + pub module_id: ModuleId, + pub layout_cache: LayoutCache<'a>, + pub module_timing: ModuleTiming, + pub solved_subs: Solved, + pub decls: Declarations, + pub ident_ids: IdentIds, + pub abilities_store: AbilitiesStore, + pub expectations: Option, + + #[cfg(debug_assertions)] + pub checkmate: Option, +} + +#[derive(Debug)] +pub struct CheckedModule { + pub solved_subs: Solved, + pub decls: Declarations, + pub abilities_store: AbilitiesStore, +} + +#[derive(Debug)] +pub(crate) struct FoundSpecializationsModule<'a> { + pub(crate) ident_ids: IdentIds, + pub(crate) layout_cache: LayoutCache<'a>, + pub(crate) procs_base: ProcsBase<'a>, + pub(crate) subs: Subs, + pub(crate) module_timing: ModuleTiming, + pub(crate) abilities_store: AbilitiesStore, + pub(crate) expectations: Option, +} + +#[derive(Debug)] +pub(crate) struct LateSpecializationsModule<'a> { + pub(crate) ident_ids: IdentIds, + pub(crate) subs: Subs, + pub(crate) module_timing: ModuleTiming, + pub(crate) layout_cache: LayoutCache<'a>, + pub(crate) procs_base: ProcsBase<'a>, + pub(crate) expectations: Option, +} + +#[derive(Debug, Default)] +pub struct ToplevelExpects { + pub pure: VecMap, + pub fx: VecMap, +} + +#[derive(Debug)] +pub struct MonomorphizedModule<'a> { + pub module_id: ModuleId, + pub interns: Interns, + pub subs: Subs, + pub layout_interner: STLayoutInterner<'a>, + pub can_problems: MutMap>, + pub type_problems: MutMap>, + pub procedures: MutMap<(Symbol, ProcLayout<'a>), Proc<'a>>, + pub host_exposed_lambda_sets: HostExposedLambdaSets<'a>, + pub toplevel_expects: ToplevelExpects, + pub entry_point: EntryPoint<'a>, + pub exposed_to_host: ExposedToHost, + pub sources: MutMap)>, + pub timings: MutMap, + pub expectations: VecMap, + pub uses_prebuilt_platform: bool, + pub glue_layouts: GlueLayouts<'a>, +} + +#[derive(Debug)] +pub struct ParsedModule<'a> { + pub module_id: ModuleId, + pub module_path: PathBuf, + pub src: &'a str, + pub module_timing: ModuleTiming, + pub deps_by_name: MutMap, ModuleId>, + pub imported_modules: MutMap, + pub exposed_ident_ids: IdentIds, + pub exposed_imports: MutMap, + pub parsed_defs: Defs<'a>, + pub symbols_from_requires: Vec<(Loc, Loc>)>, + pub header_type: HeaderType<'a>, + pub header_comments: &'a [CommentOrNewline<'a>], +} + +#[derive(Debug)] +pub enum EntryPoint<'a> { + Executable { + exposed_to_host: &'a [(Symbol, ProcLayout<'a>)], + platform_path: PathBuf, + }, + Test, +} + +#[derive(Debug)] +pub struct Expectations { + pub subs: roc_types::subs::Subs, + pub path: PathBuf, + pub expectations: VecMap>, + pub dbgs: VecMap, + pub ident_ids: IdentIds, +} + +#[derive(Clone, Debug, Default)] +pub struct ExposedToHost { + /// usually `mainForHost` + pub top_level_values: MutMap, + /// exposed closure types, typically `Fx` + pub closure_types: Vec, + /// lambda_sets + pub lambda_sets: Vec<(Symbol, LambdaSetId)>, + pub getters: Vec, +} + +#[derive(Debug)] +pub struct ModuleTiming { + pub read_roc_file: Duration, + pub parse_header: Duration, + pub parse_body: Duration, + pub canonicalize: Duration, + pub constrain: Duration, + pub solve: Duration, + pub find_specializations: Duration, + // indexed by make specializations pass + pub make_specializations: Vec, + // TODO pub monomorphize: Duration, + /// Total duration will always be more than the sum of the other fields, due + /// to things like state lookups in between phases, waiting on other threads, etc. + pub start_time: Instant, + pub end_time: Instant, +} + +impl ModuleTiming { + pub fn new(start_time: Instant) -> Self { + ModuleTiming { + read_roc_file: Duration::default(), + parse_header: Duration::default(), + parse_body: Duration::default(), + canonicalize: Duration::default(), + constrain: Duration::default(), + solve: Duration::default(), + find_specializations: Duration::default(), + make_specializations: Vec::with_capacity(2), + start_time, + end_time: start_time, // just for now; we'll overwrite this at the end + } + } + + pub fn total(&self) -> Duration { + self.end_time.duration_since(self.start_time) + } + + /// Subtract all the other fields from total_start_to_finish + pub fn other(&self) -> Duration { + let Self { + read_roc_file, + parse_header, + parse_body, + canonicalize, + constrain, + solve, + find_specializations, + make_specializations, + start_time, + end_time, + } = self; + + let calculate = |d: Option| -> Option { + make_specializations + .iter() + .fold(d, |d, pass_time| d?.checked_sub(*pass_time))? + .checked_sub(*find_specializations)? + .checked_sub(*solve)? + .checked_sub(*constrain)? + .checked_sub(*canonicalize)? + .checked_sub(*parse_body)? + .checked_sub(*parse_header)? + .checked_sub(*read_roc_file) + }; + + calculate(Some(end_time.duration_since(*start_time))).unwrap_or_default() + } +} diff --git a/crates/compiler/load_internal/src/module_cache.rs b/crates/compiler/load_internal/src/module_cache.rs new file mode 100644 index 0000000000..4f42900083 --- /dev/null +++ b/crates/compiler/load_internal/src/module_cache.rs @@ -0,0 +1,113 @@ +use crate::docs::ModuleDocumentation; +use crate::module::{ + CheckedModule, ConstrainedModule, FoundSpecializationsModule, LateSpecializationsModule, + ModuleHeader, ParsedModule, TypeCheckedModule, +}; +use roc_can::abilities::PendingAbilitiesStore; +use roc_collections::{MutMap, MutSet, VecMap}; +use roc_module::ident::ModuleName; +use roc_module::symbol::{ModuleId, PQModuleName, Symbol}; +use roc_mono::ir::ExternalSpecializations; +use roc_problem::Severity; +use roc_solve_problem::TypeError; +use roc_types::types::Alias; +use std::path::PathBuf; + +/// Struct storing various intermediate stages by their ModuleId +#[derive(Debug)] +pub(crate) struct ModuleCache<'a> { + pub(crate) module_names: MutMap>, + + /// Phases + pub(crate) headers: MutMap>, + pub(crate) parsed: MutMap>, + pub(crate) aliases: MutMap>, + pub(crate) pending_abilities: MutMap, + pub(crate) constrained: MutMap, + pub(crate) typechecked: MutMap>, + pub(crate) checked: MutMap, + pub(crate) found_specializations: MutMap>, + pub(crate) late_specializations: MutMap>, + pub(crate) external_specializations_requested: + MutMap>>, + + /// Various information + pub(crate) imports: MutMap>, + pub(crate) top_level_thunks: MutMap>, + pub(crate) documentation: VecMap, + pub(crate) can_problems: MutMap>, + pub(crate) type_problems: MutMap>, + + pub(crate) sources: MutMap, +} + +impl<'a> ModuleCache<'a> { + pub(crate) fn has_can_errors(&self) -> bool { + self.can_problems + .values() + .flatten() + .any(|problem| problem.severity() == Severity::RuntimeError) + } + + pub(crate) fn has_type_errors(&self) -> bool { + self.type_problems + .values() + .flatten() + .any(|problem| problem.severity() == Severity::RuntimeError) + } + + pub fn has_errors(&self) -> bool { + self.has_can_errors() || self.has_type_errors() + } +} + +impl Default for ModuleCache<'_> { + fn default() -> Self { + let mut module_names = MutMap::default(); + + macro_rules! insert_builtins { + ($($name:ident,)*) => {$( + module_names.insert( + ModuleId::$name, + PQModuleName::Unqualified(ModuleName::from(ModuleName::$name)), + ); + )*} + } + + insert_builtins! { + RESULT, + LIST, + STR, + DICT, + SET, + BOOL, + NUM, + BOX, + ENCODE, + DECODE, + HASH, + INSPECT, + JSON, + } + + Self { + module_names, + headers: Default::default(), + parsed: Default::default(), + aliases: Default::default(), + pending_abilities: Default::default(), + constrained: Default::default(), + typechecked: Default::default(), + checked: Default::default(), + found_specializations: Default::default(), + late_specializations: Default::default(), + external_specializations_requested: Default::default(), + imports: Default::default(), + top_level_thunks: Default::default(), + documentation: Default::default(), + can_problems: Default::default(), + type_problems: Default::default(), + sources: Default::default(), + } + } +} diff --git a/crates/compiler/load_internal/src/wasm_instant.rs b/crates/compiler/load_internal/src/wasm_instant.rs new file mode 100644 index 0000000000..4450877443 --- /dev/null +++ b/crates/compiler/load_internal/src/wasm_instant.rs @@ -0,0 +1,46 @@ +#![cfg(target_family = "wasm")] +/* +For the Web REPL (repl_www), we build the compiler as a Wasm module. +Instant is the only thing in the compiler that would need a special implementation for this. +There is a WASI implementation for it, but we are targeting the browser, not WASI! +It's possible to write browser versions of WASI's low-level ABI but we'd rather avoid it. +Instead we use these dummy implementations, which should just disappear at compile time. +*/ + +#[derive(Debug, Clone, Copy)] +pub struct Instant; + +impl Instant { + pub fn now() -> Self { + Instant + } + pub fn duration_since(&self, _: Instant) -> Duration { + Duration + } + pub fn elapsed(&self) -> Duration { + Duration + } +} + +#[derive(Debug, Clone, Copy)] +pub struct Duration; + +impl Duration { + pub fn checked_sub(&self, _: Duration) -> Option { + Some(Duration) + } + + pub fn as_secs_f64(&self) -> f64 { + 0.0 + } +} + +impl Default for Duration { + fn default() -> Self { + Duration + } +} + +impl std::ops::AddAssign for Duration { + fn add_assign(&mut self, _: Duration) {} +} diff --git a/crates/compiler/load_internal/src/work.rs b/crates/compiler/load_internal/src/work.rs new file mode 100644 index 0000000000..6524a3abfa --- /dev/null +++ b/crates/compiler/load_internal/src/work.rs @@ -0,0 +1,576 @@ +use roc_collections::{ + all::{MutMap, MutSet}, + VecMap, +}; +use roc_module::symbol::{ModuleId, PackageQualified}; + +use std::collections::hash_map::Entry; + +/// NOTE the order of definition of the phases is used by the ord instance +/// make sure they are ordered from first to last! +#[derive(PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Debug)] +pub enum Phase { + LoadHeader, + Parse, + CanonicalizeAndConstrain, + SolveTypes, + FindSpecializations, + MakeSpecializations, +} + +/// NOTE keep up to date manually, from ParseAndGenerateConstraints to the highest phase we support +const PHASES: [Phase; 6] = [ + Phase::LoadHeader, + Phase::Parse, + Phase::CanonicalizeAndConstrain, + Phase::SolveTypes, + Phase::FindSpecializations, + Phase::MakeSpecializations, +]; + +#[derive(Debug)] +enum Status { + NotStarted, + Pending, + Done, +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +enum Job<'a> { + Step(ModuleId, Phase), + ResolveShorthand(&'a str), +} + +#[derive(Default, Debug)] +struct MakeSpecializationInfo { + /// Modules to make specializations for after they are made for this module + succ: MutSet, + /// Whether this module depends on specializations being made for another module + has_pred: bool, +} + +#[derive(Debug)] +struct MakeSpecializationsDependents(MutMap); + +impl MakeSpecializationsDependents { + /// Gets the info entry for a module, or creates a default one. + fn entry(&mut self, module_id: ModuleId) -> &mut MakeSpecializationInfo { + self.0.entry(module_id).or_default() + } + + fn mark_has_pred(&mut self, module_id: ModuleId) { + self.entry(module_id).has_pred = true; + } + + fn add_succ(&mut self, module_id: ModuleId, succ: impl IntoIterator) { + // Add make specialization dependents + let entry = self.entry(module_id); + debug_assert!( + entry.succ.is_empty(), + "already added successors for module '{module_id:?}'" + ); + + entry.succ.extend(succ); + + // The module for derives implicitly depends on every other module + entry.succ.insert(ModuleId::DERIVED_GEN); + } +} + +impl Default for MakeSpecializationsDependents { + fn default() -> Self { + let mut map: MutMap = Default::default(); + + // The module for derives is always at the base as the last module to specialize + map.insert( + ModuleId::DERIVED_GEN, + MakeSpecializationInfo { + succ: Default::default(), + // NB: invariant - the derived module depends on every other module, and + // work can never be initiated for just the derived module! + has_pred: true, + }, + ); + + Self(map) + } +} + +#[derive(Debug)] +pub struct Dependencies<'a> { + waiting_for: MutMap, MutSet>>, + notifies: MutMap, MutSet>>, + status: MutMap, Status>, + + make_specializations_dependents: MakeSpecializationsDependents, +} + +pub struct DepCycle { + pub cycle: Vec, +} + +impl<'a> Dependencies<'a> { + pub fn new(goal_phase: Phase) -> Self { + let mut deps = Self { + waiting_for: Default::default(), + notifies: Default::default(), + status: Default::default(), + make_specializations_dependents: Default::default(), + }; + + if goal_phase >= Phase::MakeSpecializations { + // Module for deriving is always implicitly loaded into the work graph, but it only + // comes into play for make specializations. + deps.add_to_status_for_phase(ModuleId::DERIVED_GEN, Phase::MakeSpecializations); + } + + deps + } + + /// Add all the dependencies for a module, return (module, phase) pairs that can make progress + pub fn add_module( + &mut self, + module_id: ModuleId, + dependencies: &MutSet>, + goal_phase: Phase, + ) -> Result, DepCycle> { + use Phase::*; + + let mut output = MutSet::default(); + + for dep in dependencies.iter() { + // Do a BFS to check if we have an import cycle; if we do, calculate the cycle and + // report the error. Although the worst case here is that we do a quadratic amount of + // work for all modules added in a batch compilation, in practice, most dependencies + // inserted here have not been seen by [Dependencies] yet, so their import chain is + // size 0. + if self.has_import_dependency(*dep.as_inner(), module_id) { + let mut rev_cycle = self.calculate_reverse_import_path(*dep.as_inner(), module_id); + rev_cycle.push(module_id); + rev_cycle.reverse(); + let cycle = rev_cycle; + + return Err(DepCycle { cycle }); + } + + let has_package_dependency = self.add_package_dependency(dep, Phase::LoadHeader); + + let dep = *dep.as_inner(); + + if !has_package_dependency { + // loading can start immediately on this dependency + output.insert((dep, Phase::LoadHeader)); + } + + // to parse and generate constraints, the headers of all dependencies must be loaded! + // otherwise, we don't know whether an imported symbol is actually exposed + self.add_dependency_help(module_id, dep, Phase::Parse, Phase::LoadHeader); + + // to canonicalize a module, all its dependencies must be canonicalized + self.add_dependency(module_id, dep, Phase::CanonicalizeAndConstrain); + + // to typecheck a module, all its dependencies must be type checked already + self.add_dependency(module_id, dep, Phase::SolveTypes); + + if goal_phase >= FindSpecializations { + self.add_dependency(module_id, dep, Phase::FindSpecializations); + } + + if goal_phase >= MakeSpecializations { + self.add_dependency(dep, module_id, Phase::MakeSpecializations); + // The module for derives implicitly depends on every other module + self.add_dependency(ModuleId::DERIVED_GEN, module_id, Phase::MakeSpecializations); + + // `dep` depends on `module_id` making specializations first + self.make_specializations_dependents.mark_has_pred(dep); + } + } + + // Add "make specialization" dependents. Even if we're not targeting making + // specializations right now, we may re-enter to do so later. + self.make_specializations_dependents + .add_succ(module_id, dependencies.iter().map(|dep| *dep.as_inner())); + + // add dependencies for self + // phase i + 1 of a file always depends on phase i being completed + { + let mut i = 0; + while PHASES[i] < goal_phase { + self.add_dependency_help(module_id, module_id, PHASES[i + 1], PHASES[i]); + i += 1; + } + } + + self.add_to_status_for_all_phases(module_id, goal_phase); + + Ok(output) + } + + fn has_import_dependency(&self, module_id: ModuleId, target: ModuleId) -> bool { + if module_id.is_builtin() { + return false; + } + let mut stack = vec![module_id]; + while let Some(module) = stack.pop() { + if module.is_builtin() { + continue; + } + if module == target { + return true; + } + if let Some(dependencies) = self.make_specializations_dependents.0.get(&module) { + stack.extend(dependencies.succ.iter()); + } + } + false + } + + fn calculate_reverse_import_path( + &self, + module_id: ModuleId, + target: ModuleId, + ) -> Vec { + let mut stack = vec![module_id]; + let mut backlinks = VecMap::with_capacity(16); + let mut found_import = false; + while let Some(module) = stack.pop() { + if module == target { + found_import = true; + break; + } + if let Some(dependencies) = self.make_specializations_dependents.0.get(&module) { + for import in dependencies.succ.iter() { + backlinks.insert(*import, module); + stack.push(*import); + } + } + } + if !found_import { + roc_error_macros::internal_error!("calculate_import_path should only be called when an import path is known to exist!"); + } + + let mut source = target; + let mut rev_path = vec![source]; + while let Some(&parent) = backlinks.get(&source) { + rev_path.push(parent); + source = parent; + } + rev_path + } + + /// Adds a status for the given module for exactly one phase. + fn add_to_status_for_phase(&mut self, module_id: ModuleId, phase: Phase) { + if let Entry::Vacant(entry) = self.status.entry(Job::Step(module_id, phase)) { + entry.insert(Status::NotStarted); + } + } + + /// Adds a status for the given module for all phases up to and including the goal phase. + fn add_to_status_for_all_phases(&mut self, module_id: ModuleId, goal_phase: Phase) { + for phase in PHASES.iter() { + if *phase > goal_phase { + break; + } + + self.add_to_status_for_phase(module_id, *phase); + } + } + + /// Propagate a notification, return (module, phase) pairs that can make progress + pub fn notify(&mut self, module_id: ModuleId, phase: Phase) -> MutSet<(ModuleId, Phase)> { + self.notify_help(Job::Step(module_id, phase)) + } + + /// Propagate a notification, return (module, phase) pairs that can make progress + pub fn notify_package(&mut self, shorthand: &'a str) -> MutSet<(ModuleId, Phase)> { + self.notify_help(Job::ResolveShorthand(shorthand)) + } + + fn notify_help(&mut self, key: Job<'a>) -> MutSet<(ModuleId, Phase)> { + self.status.insert(key.clone(), Status::Done); + + let mut output = MutSet::default(); + + if let Some(to_notify) = self.notifies.get(&key) { + for notify_key in to_notify { + let mut is_empty = false; + if let Some(waiting_for_pairs) = self.waiting_for.get_mut(notify_key) { + waiting_for_pairs.remove(&key); + is_empty = waiting_for_pairs.is_empty(); + } + + if is_empty { + self.waiting_for.remove(notify_key); + + if let Job::Step(module, phase) = *notify_key { + output.insert((module, phase)); + } + } + } + } + + self.notifies.remove(&key); + + output + } + + fn add_package_dependency( + &mut self, + module: &PackageQualified<'a, ModuleId>, + next_phase: Phase, + ) -> bool { + match module { + PackageQualified::Unqualified(_) => { + // no dependency, we can just start loading the file + false + } + PackageQualified::Qualified(shorthand, module_id) => { + let job = Job::ResolveShorthand(shorthand); + let next_step = Job::Step(*module_id, next_phase); + match self.status.get(&job) { + None | Some(Status::NotStarted) | Some(Status::Pending) => { + // this shorthand is not resolved, add a dependency + { + let entry = self.waiting_for.entry(next_step.clone()).or_default(); + + entry.insert(job.clone()); + } + + { + let entry = self.notifies.entry(job).or_default(); + + entry.insert(next_step); + } + + true + } + Some(Status::Done) => { + // shorthand is resolved; no dependency + false + } + } + } + } + } + + /// A waits for B, and B will notify A when it completes the phase + fn add_dependency(&mut self, a: ModuleId, b: ModuleId, phase: Phase) { + self.add_dependency_help(a, b, phase, phase); + } + + /// phase_a of module a is waiting for phase_b of module_b + fn add_dependency_help(&mut self, a: ModuleId, b: ModuleId, phase_a: Phase, phase_b: Phase) { + // no need to wait if the dependency is already done! + if let Some(Status::Done) = self.status.get(&Job::Step(b, phase_b)) { + return; + } + + let key = Job::Step(a, phase_a); + let value = Job::Step(b, phase_b); + match self.waiting_for.get_mut(&key) { + Some(existing) => { + existing.insert(value); + } + None => { + let mut set = MutSet::default(); + set.insert(value); + self.waiting_for.insert(key, set); + } + } + + let key = Job::Step(b, phase_b); + let value = Job::Step(a, phase_a); + match self.notifies.get_mut(&key) { + Some(existing) => { + existing.insert(value); + } + None => { + let mut set = MutSet::default(); + set.insert(value); + self.notifies.insert(key, set); + } + } + } + + pub fn solved_all(&self) -> bool { + debug_assert_eq!(self.notifies.is_empty(), self.waiting_for.is_empty()); + + for status in self.status.values() { + match status { + Status::Done => { + continue; + } + _ => { + return false; + } + } + } + + true + } + + pub fn prepare_start_phase(&mut self, module_id: ModuleId, phase: Phase) -> PrepareStartPhase { + match self.status.get_mut(&Job::Step(module_id, phase)) { + Some(current @ Status::NotStarted) => { + // start this phase! + *current = Status::Pending; + PrepareStartPhase::Continue + } + Some(Status::Pending) => { + // don't start this task again! + PrepareStartPhase::Done + } + Some(Status::Done) => { + // don't start this task again, but tell those waiting for it they can continue + let new = self.notify(module_id, phase); + + PrepareStartPhase::Recurse(new) + } + None => match phase { + Phase::LoadHeader => { + // this is fine, mark header loading as pending + self.status + .insert(Job::Step(module_id, Phase::LoadHeader), Status::Pending); + + PrepareStartPhase::Continue + } + _ => unreachable!( + "Pair {:?} is not in dependencies.status, that should never happen!", + (module_id, phase) + ), + }, + } + } + + /// Loads the dependency graph to find and make specializations, and returns the next jobs to + /// be run. + /// + /// This should be used when the compiler wants to build or run a Roc executable if and only if + /// previous stages succeed; in such cases we load the dependency graph dynamically. + pub fn load_find_and_make_specializations_after_check(&mut self) -> MutSet<(ModuleId, Phase)> { + let mut output = MutSet::default(); + + // Take out the specialization dependency graph, as this should not be modified as we + // reload the build graph. We'll make sure the state is unaffected at the end of this call. + let mut make_specializations_dependents = MakeSpecializationsDependents::default(); + let default_make_specializations_dependents_len = make_specializations_dependents.0.len(); + std::mem::swap( + &mut self.make_specializations_dependents, + &mut make_specializations_dependents, + ); + + for (&module, info) in make_specializations_dependents.0.iter_mut() { + debug_assert!(self.status.get_mut(&Job::Step(module, Phase::FindSpecializations)).is_none(), "should only have targeted solving types, but there is already a goal to find specializations"); + debug_assert!(self.status.get_mut(&Job::Step(module, Phase::MakeSpecializations)).is_none(), "should only have targeted solving types, but there is already a goal to make specializations"); + debug_assert!( + module == ModuleId::DERIVED_GEN || info.succ.contains(&ModuleId::DERIVED_GEN), + "derived module not accounted for in {:?}", + (module, info) + ); + + let mut has_find_specialization_dep = false; + for &module_dep in info.succ.iter() { + // The modules in `succ` are the modules for which specializations should be made + // after the current one. But, their specializations should be found before the + // current one. + if module_dep != ModuleId::DERIVED_GEN { + // We never find specializations for DERIVED_GEN + self.add_dependency(module, module_dep, Phase::FindSpecializations); + has_find_specialization_dep = true; + } + + self.add_dependency(module_dep, module, Phase::MakeSpecializations); + self.add_dependency(ModuleId::DERIVED_GEN, module, Phase::MakeSpecializations); + + // That `module_dep` can't make its specializations until the current module does + // should already be accounted for in `make_specializations_dependents`, which we + // populated when initially building the graph. + } + + if module != ModuleId::DERIVED_GEN { + self.add_to_status_for_phase(module, Phase::FindSpecializations); + self.add_dependency_help( + module, + module, + Phase::MakeSpecializations, + Phase::FindSpecializations, + ); + } + self.add_to_status_for_phase(module, Phase::MakeSpecializations); + + if !has_find_specialization_dep && module != ModuleId::DERIVED_GEN { + // We don't depend on any other modules having their specializations found first, + // so start finding specializations from this module. + output.insert((module, Phase::FindSpecializations)); + } + } + + std::mem::swap( + &mut self.make_specializations_dependents, + &mut make_specializations_dependents, + ); + debug_assert_eq!( + make_specializations_dependents.0.len(), + default_make_specializations_dependents_len, + "more modules were added to the graph: {make_specializations_dependents:?}" + ); + + output + } + + /// Load the entire "make specializations" dependency graph and start from the top. + pub fn reload_make_specialization_pass(&mut self) -> MutSet<(ModuleId, Phase)> { + let mut output = MutSet::default(); + + let mut make_specializations_dependents = MakeSpecializationsDependents::default(); + let default_make_specializations_dependents_len = make_specializations_dependents.0.len(); + std::mem::swap( + &mut self.make_specializations_dependents, + &mut make_specializations_dependents, + ); + + for (&module, _) in make_specializations_dependents.0.iter() { + let job = Job::Step(module, Phase::MakeSpecializations); + let status = self.status.get_mut(&job).unwrap(); + debug_assert!( + matches!(status, Status::Done), + "all previous make specializations should be done before reloading" + ); + *status = Status::NotStarted; + } + + // `add_dependency` borrows self as mut so we move `make_specializations_dependents` out + // for our local use. `add_dependency` should never grow the make specializations + // dependency graph. + for (&module, MakeSpecializationInfo { succ, has_pred }) in + make_specializations_dependents.0.iter() + { + for &dependent in succ { + self.add_dependency(dependent, module, Phase::MakeSpecializations); + } + + self.add_to_status_for_phase(module, Phase::MakeSpecializations); + if !has_pred { + output.insert((module, Phase::MakeSpecializations)); + } + } + + std::mem::swap( + &mut self.make_specializations_dependents, + &mut make_specializations_dependents, + ); + debug_assert_eq!( + make_specializations_dependents.0.len(), + default_make_specializations_dependents_len, + "more modules were added to the graph: {make_specializations_dependents:?}" + ); + + output + } +} + +pub enum PrepareStartPhase { + Continue, + Done, + Recurse(MutSet<(ModuleId, Phase)>), +} diff --git a/crates/compiler/load_internal/tests/fixtures/build/app_with_deps/AStar.roc b/crates/compiler/load_internal/tests/fixtures/build/app_with_deps/AStar.roc new file mode 100644 index 0000000000..69a1db59e0 --- /dev/null +++ b/crates/compiler/load_internal/tests/fixtures/build/app_with_deps/AStar.roc @@ -0,0 +1,111 @@ +interface AStar + exposes [initialModel, reconstructPath, updateCost, cheapestOpen, astar, findPath] + imports [] + + +# a port of https://github.com/krisajenkins/elm-astar/blob/2.1.3/src/AStar/Generalised.elm + +Model position : + { evaluated : Set position + , openSet : Set position + , costs : Map.Map position F64 + , cameFrom : Map.Map position position + } + + +initialModel : position -> Model position +initialModel = \start -> + { evaluated : Set.empty {} + , openSet : Set.single start + , costs : Dict.single start 0.0 + , cameFrom : Map.empty + } + + +cheapestOpen : (position -> F64), Model position -> Result position [KeyNotFound]* +cheapestOpen = \costFunction, model -> + + folder = \resSmallestSoFar, position -> + when Map.get model.costs position is + Err e -> + Err e + + Ok cost -> + positionCost = costFunction position + + when resSmallestSoFar is + Err _ -> Ok { position, cost: cost + positionCost } + Ok smallestSoFar -> + if positionCost + cost < smallestSoFar.cost then + Ok { position, cost: cost + positionCost } + + else + Ok smallestSoFar + + Set.walk model.openSet (Err KeyNotFound) folder + |> Result.map (\x -> x.position) + + + +reconstructPath : Map position position, position -> List position +reconstructPath = \cameFrom, goal -> + when Map.get cameFrom goal is + Err KeyNotFound -> + [] + + Ok next -> + List.append (reconstructPath cameFrom next) goal + +updateCost : position, position, Model position -> Model position +updateCost = \current, neighbour, model -> + newCameFrom = Map.insert model.cameFrom neighbour current + + newCosts = Map.insert model.costs neighbour distanceTo + + distanceTo = reconstructPath newCameFrom neighbour + |> List.len + |> Num.toFrac + + newModel = { model & costs : newCosts , cameFrom : newCameFrom } + + when Map.get model.costs neighbour is + Err KeyNotFound -> + newModel + + Ok previousDistance -> + if distanceTo < previousDistance then + newModel + + else + model + + +findPath : { costFunction: (position, position -> F64), moveFunction: (position -> Set position), start : position, end : position } -> Result (List position) [KeyNotFound]* +findPath = \{ costFunction, moveFunction, start, end } -> + astar costFunction moveFunction end (initialModel start) + + +astar : (position, position -> F64), (position -> Set position), position, Model position -> [Err [KeyNotFound]*, Ok (List position)]* +astar = \costFn, moveFn, goal, model -> + when cheapestOpen (\position -> costFn goal position) model is + Err _ -> + Err KeyNotFound + + Ok current -> + if current == goal then + Ok (reconstructPath model.cameFrom goal) + + else + + modelPopped = { model & openSet : Set.remove model.openSet current, evaluated : Set.insert model.evaluated current } + + neighbours = moveFn current + + newNeighbours = Set.difference neighbours modelPopped.evaluated + + modelWithNeighbours = { modelPopped & openSet : Set.union modelPopped.openSet newNeighbours } + + modelWithCosts = Set.walk newNeighbours modelWithNeighbours (\md, nb -> updateCost current nb md) + + astar costFn moveFn goal modelWithCosts + diff --git a/compiler/load_internal/tests/fixtures/build/app_with_deps/Dep1.roc b/crates/compiler/load_internal/tests/fixtures/build/app_with_deps/Dep1.roc similarity index 100% rename from compiler/load_internal/tests/fixtures/build/app_with_deps/Dep1.roc rename to crates/compiler/load_internal/tests/fixtures/build/app_with_deps/Dep1.roc diff --git a/compiler/load_internal/tests/fixtures/build/app_with_deps/Dep2.roc b/crates/compiler/load_internal/tests/fixtures/build/app_with_deps/Dep2.roc similarity index 100% rename from compiler/load_internal/tests/fixtures/build/app_with_deps/Dep2.roc rename to crates/compiler/load_internal/tests/fixtures/build/app_with_deps/Dep2.roc diff --git a/crates/compiler/load_internal/tests/fixtures/build/app_with_deps/Dep3/Blah.roc b/crates/compiler/load_internal/tests/fixtures/build/app_with_deps/Dep3/Blah.roc new file mode 100644 index 0000000000..b868374a0d --- /dev/null +++ b/crates/compiler/load_internal/tests/fixtures/build/app_with_deps/Dep3/Blah.roc @@ -0,0 +1,10 @@ +interface Dep3.Blah + exposes [one, two, foo, bar] + imports [Dep3.Other] + +one = 1 + +two = 2 + +foo = "foo from Dep3" +bar = Dep3.Other.bar diff --git a/crates/compiler/load_internal/tests/fixtures/build/app_with_deps/Dep3/Other.roc b/crates/compiler/load_internal/tests/fixtures/build/app_with_deps/Dep3/Other.roc new file mode 100644 index 0000000000..67df17c8a7 --- /dev/null +++ b/crates/compiler/load_internal/tests/fixtures/build/app_with_deps/Dep3/Other.roc @@ -0,0 +1,6 @@ +interface Dep3.Other + exposes [foo, bar] + imports [] + +foo = "foo from Dep3.Other" +bar = "bar from Dep3.Other" diff --git a/compiler/load_internal/tests/fixtures/build/app_with_deps/ImportAlias.roc b/crates/compiler/load_internal/tests/fixtures/build/app_with_deps/ImportAlias.roc similarity index 100% rename from compiler/load_internal/tests/fixtures/build/app_with_deps/ImportAlias.roc rename to crates/compiler/load_internal/tests/fixtures/build/app_with_deps/ImportAlias.roc diff --git a/compiler/load_internal/tests/fixtures/build/app_with_deps/ManualAttr.roc b/crates/compiler/load_internal/tests/fixtures/build/app_with_deps/ManualAttr.roc similarity index 100% rename from compiler/load_internal/tests/fixtures/build/app_with_deps/ManualAttr.roc rename to crates/compiler/load_internal/tests/fixtures/build/app_with_deps/ManualAttr.roc diff --git a/compiler/load_internal/tests/fixtures/build/app_with_deps/OneDep.roc b/crates/compiler/load_internal/tests/fixtures/build/app_with_deps/OneDep.roc similarity index 100% rename from compiler/load_internal/tests/fixtures/build/app_with_deps/OneDep.roc rename to crates/compiler/load_internal/tests/fixtures/build/app_with_deps/OneDep.roc diff --git a/crates/compiler/load_internal/tests/fixtures/build/app_with_deps/Primary.roc b/crates/compiler/load_internal/tests/fixtures/build/app_with_deps/Primary.roc new file mode 100644 index 0000000000..2538822d83 --- /dev/null +++ b/crates/compiler/load_internal/tests/fixtures/build/app_with_deps/Primary.roc @@ -0,0 +1,30 @@ +interface Primary + exposes [blah2, blah3, str, alwaysThree, identity, z, w, succeed, withDefault, yay] + imports [Dep1, Dep2.{ two }, Dep3.Blah.{ bar }, Res] + +blah2 = Dep2.two +blah3 = bar + +str = Dep1.str + +alwaysThree = \_ -> Dep1.three + +identity = \a -> a + +z = identity (alwaysThree {}) + +w : Dep1.Identity {} +w = Identity {} + +succeed : a -> Dep1.Identity a +succeed = \x -> Identity x + +withDefault = Res.withDefault + +yay : Res.Res {} err +yay = + ok = Ok "foo" + + f = \_ -> {} + + Res.map ok f diff --git a/crates/compiler/load_internal/tests/fixtures/build/app_with_deps/Quicksort.roc b/crates/compiler/load_internal/tests/fixtures/build/app_with_deps/Quicksort.roc new file mode 100644 index 0000000000..459dd59a0a --- /dev/null +++ b/crates/compiler/load_internal/tests/fixtures/build/app_with_deps/Quicksort.roc @@ -0,0 +1,49 @@ +app "quicksort" provides [swap, partition, partitionHelp, quicksort] to "./platform" + +quicksort : List (Num a), Nat, Nat -> List (Num a) +quicksort = \list, low, high -> + when partition low high list is + Pair partitionIndex partitioned -> + partitioned + |> quicksort low (partitionIndex - 1) + |> quicksort (partitionIndex + 1) high + + +swap : Nat, Nat, List a -> List a +swap = \i, j, list -> + when Pair (List.get list i) (List.get list j) is + Pair (Ok atI) (Ok atJ) -> + list + |> List.set i atJ + |> List.set j atI + + _ -> + [] + + +partition : Nat, Nat, List (Num a) -> [Pair Nat (List (Num a))] +partition = \low, high, initialList -> + when List.get initialList high is + Ok pivot -> + when partitionHelp (low - 1) low initialList high pivot is + Pair newI newList -> + Pair (newI + 1) (swap (newI + 1) high newList) + + Err _ -> + Pair (low - 1) initialList + + +partitionHelp : Nat, Nat, List (Num a), Nat, (Num a) -> [Pair Nat (List (Num a))] +partitionHelp = \i, j, list, high, pivot -> + if j < high then + when List.get list j is + Ok value -> + if value <= pivot then + partitionHelp (i + 1) (j + 1) (swap (i + 1) j list) high pivot + else + partitionHelp i (j + 1) list high pivot + + Err _ -> + Pair i list + else + Pair i list diff --git a/crates/compiler/load_internal/tests/fixtures/build/app_with_deps/QuicksortMultiDef.roc b/crates/compiler/load_internal/tests/fixtures/build/app_with_deps/QuicksortMultiDef.roc new file mode 100644 index 0000000000..8a4d8ea78f --- /dev/null +++ b/crates/compiler/load_internal/tests/fixtures/build/app_with_deps/QuicksortMultiDef.roc @@ -0,0 +1,57 @@ +app "quicksort" provides [quicksort] to "./platform" + +quicksortHelp : List (Num a), Nat, Nat -> List (Num a) +quicksortHelp = \list, low, high -> + if low < high then + when partition low high list is + Pair partitionIndex partitioned -> + partitioned + |> quicksortHelp low (partitionIndex - 1) + |> quicksortHelp (partitionIndex + 1) high + else + list + + +swap : Nat, Nat, List a -> List a +swap = \i, j, list -> + when Pair (List.get list i) (List.get list j) is + Pair (Ok atI) (Ok atJ) -> + list + |> List.set i atJ + |> List.set j atI + + _ -> + [] + +partition : Nat, Nat, List (Num a) -> [Pair Nat (List (Num a))] +partition = \low, high, initialList -> + when List.get initialList high is + Ok pivot -> + when partitionHelp (low - 1) low initialList high pivot is + Pair newI newList -> + Pair (newI + 1) (swap (newI + 1) high newList) + + Err _ -> + Pair (low - 1) initialList + + +partitionHelp : Nat, Nat, List (Num a), Nat, (Num a) -> [Pair Nat (List (Num a))] +partitionHelp = \i, j, list, high, pivot -> + if j < high then + when List.get list j is + Ok value -> + if value <= pivot then + partitionHelp (i + 1) (j + 1) (swap (i + 1) j list) high pivot + else + partitionHelp i (j + 1) list high pivot + + Err _ -> + Pair i list + else + Pair i list + + + +quicksort = \originalList -> + n = List.len originalList + quicksortHelp originalList 0 (n - 1) diff --git a/compiler/load_internal/tests/fixtures/build/app_with_deps/Records.roc b/crates/compiler/load_internal/tests/fixtures/build/app_with_deps/Records.roc similarity index 100% rename from compiler/load_internal/tests/fixtures/build/app_with_deps/Records.roc rename to crates/compiler/load_internal/tests/fixtures/build/app_with_deps/Records.roc diff --git a/compiler/load_internal/tests/fixtures/build/app_with_deps/Res.roc b/crates/compiler/load_internal/tests/fixtures/build/app_with_deps/Res.roc similarity index 100% rename from compiler/load_internal/tests/fixtures/build/app_with_deps/Res.roc rename to crates/compiler/load_internal/tests/fixtures/build/app_with_deps/Res.roc diff --git a/compiler/load_internal/tests/fixtures/build/app_with_deps/WithBuiltins.roc b/crates/compiler/load_internal/tests/fixtures/build/app_with_deps/WithBuiltins.roc similarity index 100% rename from compiler/load_internal/tests/fixtures/build/app_with_deps/WithBuiltins.roc rename to crates/compiler/load_internal/tests/fixtures/build/app_with_deps/WithBuiltins.roc diff --git a/crates/compiler/load_internal/tests/fixtures/build/interface_with_deps/AStar.roc b/crates/compiler/load_internal/tests/fixtures/build/interface_with_deps/AStar.roc new file mode 100644 index 0000000000..b9b44ddbd6 --- /dev/null +++ b/crates/compiler/load_internal/tests/fixtures/build/interface_with_deps/AStar.roc @@ -0,0 +1,111 @@ +interface AStar + exposes [initialModel, reconstructPath, updateCost, cheapestOpen, astar, findPath] + imports [] + + +# a port of https://github.com/krisajenkins/elm-astar/blob/2.1.3/src/AStar/Generalised.elm + +Model position : + { evaluated : Set position + , openSet : Set position + , costs : Dict.Dict position F64 + , cameFrom : Dict.Dict position position + } + + +initialModel : position -> Model position where position implements Hash & Eq +initialModel = \start -> + { evaluated : Set.empty {} + , openSet : Set.single start + , costs : Dict.single start 0.0 + , cameFrom : Dict.empty {} + } + + +cheapestOpen : (position -> F64), Model position -> Result position [KeyNotFound] where position implements Hash & Eq +cheapestOpen = \costFunction, model -> + + folder = \resSmallestSoFar, position -> + when Dict.get model.costs position is + Err e -> + Err e + + Ok cost -> + positionCost = costFunction position + + when resSmallestSoFar is + Err _ -> Ok { position, cost: cost + positionCost } + Ok smallestSoFar -> + if positionCost + cost < smallestSoFar.cost then + Ok { position, cost: cost + positionCost } + + else + Ok smallestSoFar + + Set.walk model.openSet (Err KeyNotFound) folder + |> Result.map (\x -> x.position) + + + +reconstructPath : Dict position position, position -> List position where position implements Hash & Eq +reconstructPath = \cameFrom, goal -> + when Dict.get cameFrom goal is + Err KeyNotFound -> + [] + + Ok next -> + List.append (reconstructPath cameFrom next) goal + +updateCost : position, position, Model position -> Model position where position implements Hash & Eq +updateCost = \current, neighbour, model -> + newCameFrom = Dict.insert model.cameFrom neighbour current + + newCosts = Dict.insert model.costs neighbour distanceTo + + distanceTo = reconstructPath newCameFrom neighbour + |> List.len + |> Num.toFrac + + newModel = { model & costs : newCosts , cameFrom : newCameFrom } + + when Dict.get model.costs neighbour is + Err KeyNotFound -> + newModel + + Ok previousDistance -> + if distanceTo < previousDistance then + newModel + + else + model + + +findPath : { costFunction: (position, position -> F64), moveFunction: (position -> Set position), start : position, end : position } -> Result (List position) [KeyNotFound] where position implements Hash & Eq +findPath = \{ costFunction, moveFunction, start, end } -> + astar costFunction moveFunction end (initialModel start) + + +astar : (position, position -> F64), (position -> Set position), position, Model position -> [Err [KeyNotFound], Ok (List position)] where position implements Hash & Eq +astar = \costFn, moveFn, goal, model -> + when cheapestOpen (\position -> costFn goal position) model is + Err _ -> + Err KeyNotFound + + Ok current -> + if current == goal then + Ok (reconstructPath model.cameFrom goal) + + else + + modelPopped = { model & openSet : Set.remove model.openSet current, evaluated : Set.insert model.evaluated current } + + neighbours = moveFn current + + newNeighbours = Set.difference neighbours modelPopped.evaluated + + modelWithNeighbours = { modelPopped & openSet : Set.union modelPopped.openSet newNeighbours } + + modelWithCosts = Set.walk newNeighbours modelWithNeighbours (\md, nb -> updateCost current nb md) + + astar costFn moveFn goal modelWithCosts + diff --git a/compiler/load_internal/tests/fixtures/build/interface_with_deps/Dep1.roc b/crates/compiler/load_internal/tests/fixtures/build/interface_with_deps/Dep1.roc similarity index 100% rename from compiler/load_internal/tests/fixtures/build/interface_with_deps/Dep1.roc rename to crates/compiler/load_internal/tests/fixtures/build/interface_with_deps/Dep1.roc diff --git a/compiler/load_internal/tests/fixtures/build/interface_with_deps/Dep2.roc b/crates/compiler/load_internal/tests/fixtures/build/interface_with_deps/Dep2.roc similarity index 100% rename from compiler/load_internal/tests/fixtures/build/interface_with_deps/Dep2.roc rename to crates/compiler/load_internal/tests/fixtures/build/interface_with_deps/Dep2.roc diff --git a/compiler/load_internal/tests/fixtures/build/app_with_deps/Dep3/Blah.roc b/crates/compiler/load_internal/tests/fixtures/build/interface_with_deps/Dep3/Blah.roc similarity index 100% rename from compiler/load_internal/tests/fixtures/build/app_with_deps/Dep3/Blah.roc rename to crates/compiler/load_internal/tests/fixtures/build/interface_with_deps/Dep3/Blah.roc diff --git a/compiler/load_internal/tests/fixtures/build/interface_with_deps/ImportAlias.roc b/crates/compiler/load_internal/tests/fixtures/build/interface_with_deps/ImportAlias.roc similarity index 100% rename from compiler/load_internal/tests/fixtures/build/interface_with_deps/ImportAlias.roc rename to crates/compiler/load_internal/tests/fixtures/build/interface_with_deps/ImportAlias.roc diff --git a/crates/compiler/load_internal/tests/fixtures/build/interface_with_deps/IngestedFile.roc b/crates/compiler/load_internal/tests/fixtures/build/interface_with_deps/IngestedFile.roc new file mode 100644 index 0000000000..efcbf44618 --- /dev/null +++ b/crates/compiler/load_internal/tests/fixtures/build/interface_with_deps/IngestedFile.roc @@ -0,0 +1,5 @@ +interface IngestedFile + exposes [str] + imports ["IngestedFile.roc" as foo : Str] + +str = foo diff --git a/crates/compiler/load_internal/tests/fixtures/build/interface_with_deps/IngestedFileBytes.roc b/crates/compiler/load_internal/tests/fixtures/build/interface_with_deps/IngestedFileBytes.roc new file mode 100644 index 0000000000..2eea73d88b --- /dev/null +++ b/crates/compiler/load_internal/tests/fixtures/build/interface_with_deps/IngestedFileBytes.roc @@ -0,0 +1,5 @@ +interface IngestedFileBytes + exposes [str] + imports ["IngestedFileBytes.roc" as foo : List U8] + +str = Str.fromUtf8 foo |> Result.withDefault "" diff --git a/compiler/load_internal/tests/fixtures/build/interface_with_deps/ManualAttr.roc b/crates/compiler/load_internal/tests/fixtures/build/interface_with_deps/ManualAttr.roc similarity index 100% rename from compiler/load_internal/tests/fixtures/build/interface_with_deps/ManualAttr.roc rename to crates/compiler/load_internal/tests/fixtures/build/interface_with_deps/ManualAttr.roc diff --git a/compiler/load_internal/tests/fixtures/build/interface_with_deps/OneDep.roc b/crates/compiler/load_internal/tests/fixtures/build/interface_with_deps/OneDep.roc similarity index 100% rename from compiler/load_internal/tests/fixtures/build/interface_with_deps/OneDep.roc rename to crates/compiler/load_internal/tests/fixtures/build/interface_with_deps/OneDep.roc diff --git a/crates/compiler/load_internal/tests/fixtures/build/interface_with_deps/Primary.roc b/crates/compiler/load_internal/tests/fixtures/build/interface_with_deps/Primary.roc new file mode 100644 index 0000000000..2538822d83 --- /dev/null +++ b/crates/compiler/load_internal/tests/fixtures/build/interface_with_deps/Primary.roc @@ -0,0 +1,30 @@ +interface Primary + exposes [blah2, blah3, str, alwaysThree, identity, z, w, succeed, withDefault, yay] + imports [Dep1, Dep2.{ two }, Dep3.Blah.{ bar }, Res] + +blah2 = Dep2.two +blah3 = bar + +str = Dep1.str + +alwaysThree = \_ -> Dep1.three + +identity = \a -> a + +z = identity (alwaysThree {}) + +w : Dep1.Identity {} +w = Identity {} + +succeed : a -> Dep1.Identity a +succeed = \x -> Identity x + +withDefault = Res.withDefault + +yay : Res.Res {} err +yay = + ok = Ok "foo" + + f = \_ -> {} + + Res.map ok f diff --git a/compiler/load_internal/tests/fixtures/build/interface_with_deps/Quicksort.roc b/crates/compiler/load_internal/tests/fixtures/build/interface_with_deps/Quicksort.roc similarity index 100% rename from compiler/load_internal/tests/fixtures/build/interface_with_deps/Quicksort.roc rename to crates/compiler/load_internal/tests/fixtures/build/interface_with_deps/Quicksort.roc diff --git a/compiler/load_internal/tests/fixtures/build/interface_with_deps/Records.roc b/crates/compiler/load_internal/tests/fixtures/build/interface_with_deps/Records.roc similarity index 100% rename from compiler/load_internal/tests/fixtures/build/interface_with_deps/Records.roc rename to crates/compiler/load_internal/tests/fixtures/build/interface_with_deps/Records.roc diff --git a/compiler/load_internal/tests/fixtures/build/interface_with_deps/Res.roc b/crates/compiler/load_internal/tests/fixtures/build/interface_with_deps/Res.roc similarity index 100% rename from compiler/load_internal/tests/fixtures/build/interface_with_deps/Res.roc rename to crates/compiler/load_internal/tests/fixtures/build/interface_with_deps/Res.roc diff --git a/compiler/load_internal/tests/fixtures/build/interface_with_deps/WithBuiltins.roc b/crates/compiler/load_internal/tests/fixtures/build/interface_with_deps/WithBuiltins.roc similarity index 100% rename from compiler/load_internal/tests/fixtures/build/interface_with_deps/WithBuiltins.roc rename to crates/compiler/load_internal/tests/fixtures/build/interface_with_deps/WithBuiltins.roc diff --git a/compiler/load_internal/tests/fixtures/build/no_deps/MissingDep.roc b/crates/compiler/load_internal/tests/fixtures/build/no_deps/MissingDep.roc similarity index 100% rename from compiler/load_internal/tests/fixtures/build/no_deps/MissingDep.roc rename to crates/compiler/load_internal/tests/fixtures/build/no_deps/MissingDep.roc diff --git a/crates/compiler/load_internal/tests/fixtures/build/no_deps/MissingIngestedFile.roc b/crates/compiler/load_internal/tests/fixtures/build/no_deps/MissingIngestedFile.roc new file mode 100644 index 0000000000..9216ba87be --- /dev/null +++ b/crates/compiler/load_internal/tests/fixtures/build/no_deps/MissingIngestedFile.roc @@ -0,0 +1,8 @@ +interface MissingIngestedFile + exposes [unit] + imports ["ThisFileIsMissing" as data: List U8] + +Unit : [Unit] + +unit : Unit +unit = Unit diff --git a/compiler/load_internal/tests/fixtures/build/no_deps/Principal.roc b/crates/compiler/load_internal/tests/fixtures/build/no_deps/Principal.roc similarity index 100% rename from compiler/load_internal/tests/fixtures/build/no_deps/Principal.roc rename to crates/compiler/load_internal/tests/fixtures/build/no_deps/Principal.roc diff --git a/compiler/load_internal/tests/fixtures/build/no_deps/Unit.roc b/crates/compiler/load_internal/tests/fixtures/build/no_deps/Unit.roc similarity index 100% rename from compiler/load_internal/tests/fixtures/build/no_deps/Unit.roc rename to crates/compiler/load_internal/tests/fixtures/build/no_deps/Unit.roc diff --git a/compiler/load_internal/tests/helpers/mod.rs b/crates/compiler/load_internal/tests/helpers/mod.rs similarity index 100% rename from compiler/load_internal/tests/helpers/mod.rs rename to crates/compiler/load_internal/tests/helpers/mod.rs diff --git a/crates/compiler/load_internal/tests/test_load.rs b/crates/compiler/load_internal/tests/test_load.rs new file mode 100644 index 0000000000..cdc751ce61 --- /dev/null +++ b/crates/compiler/load_internal/tests/test_load.rs @@ -0,0 +1,1150 @@ +#![cfg(test)] + +#[macro_use] +extern crate indoc; +#[macro_use] +extern crate pretty_assertions; +#[macro_use] +extern crate maplit; + +extern crate bumpalo; +extern crate roc_collections; +extern crate roc_load_internal; +extern crate roc_module; + +mod helpers; + +use crate::helpers::fixtures_dir; +use bumpalo::Bump; +use roc_can::module::ExposedByModule; +use roc_load_internal::file::{ + ExecutionMode, LoadConfig, LoadResult, LoadStart, LoadingProblem, Threading, +}; +use roc_load_internal::module::LoadedModule; +use roc_module::ident::ModuleName; +use roc_module::symbol::{Interns, ModuleId}; +use roc_packaging::cache::RocCacheDir; +use roc_problem::can::Problem; +use roc_region::all::LineInfo; +use roc_reporting::report::RenderTarget; +use roc_reporting::report::RocDocAllocator; +use roc_reporting::report::{can_problem, DEFAULT_PALETTE}; +use roc_solve::FunctionKind; +use roc_target::TargetInfo; +use roc_types::pretty_print::name_and_print_var; +use roc_types::pretty_print::DebugPrint; +use std::collections::HashMap; +use std::path::PathBuf; + +fn load_and_typecheck( + arena: &Bump, + filename: PathBuf, + exposed_types: ExposedByModule, + target_info: TargetInfo, + function_kind: FunctionKind, +) -> Result { + use LoadResult::*; + + let load_start = LoadStart::from_path( + arena, + filename, + RenderTarget::Generic, + RocCacheDir::Disallowed, + DEFAULT_PALETTE, + )?; + let load_config = LoadConfig { + target_info, + function_kind, + render: RenderTarget::Generic, + palette: DEFAULT_PALETTE, + threading: Threading::Single, + exec_mode: ExecutionMode::Check, + }; + + match roc_load_internal::file::load( + arena, + load_start, + exposed_types, + Default::default(), // these tests will re-compile the builtins + RocCacheDir::Disallowed, + load_config, + )? { + Monomorphized(_) => unreachable!(""), + TypeChecked(module) => Ok(module), + } +} + +const TARGET_INFO: roc_target::TargetInfo = roc_target::TargetInfo::default_x86_64(); + +// HELPERS + +fn format_can_problems( + problems: Vec, + home: ModuleId, + interns: &Interns, + filename: PathBuf, + src: &str, +) -> String { + use ven_pretty::DocAllocator; + + let src_lines: Vec<&str> = src.split('\n').collect(); + let lines = LineInfo::new(src); + let alloc = RocDocAllocator::new(&src_lines, home, interns); + let reports = problems + .into_iter() + .map(|problem| can_problem(&alloc, &lines, filename.clone(), problem).pretty(&alloc)); + + let mut buf = String::new(); + alloc + .stack(reports) + .append(alloc.line()) + .1 + .render_raw(70, &mut roc_reporting::report::CiWrite::new(&mut buf)) + .unwrap(); + buf +} + +fn multiple_modules(subdir: &str, files: Vec<(&str, &str)>) -> Result { + let arena = Bump::new(); + let arena = &arena; + + match multiple_modules_help(subdir, arena, files) { + Err(io_error) => panic!("IO trouble: {io_error:?}"), + Ok(Err(LoadingProblem::FormattedReport(buf))) => Err(buf), + Ok(Err(loading_problem)) => Err(format!("{loading_problem:?}")), + Ok(Ok(mut loaded_module)) => { + let home = loaded_module.module_id; + let (filepath, src) = loaded_module.sources.get(&home).unwrap(); + + let can_problems = loaded_module.can_problems.remove(&home).unwrap_or_default(); + if !can_problems.is_empty() { + return Err(format_can_problems( + can_problems, + home, + &loaded_module.interns, + filepath.clone(), + src, + )); + } + + assert!(loaded_module + .type_problems + .remove(&home) + .unwrap_or_default() + .is_empty(),); + + Ok(loaded_module) + } + } +} + +fn multiple_modules_help<'a>( + subdir: &str, + arena: &'a Bump, + mut files: Vec<(&str, &str)>, +) -> Result>, std::io::Error> { + use std::fs::{self, File}; + use std::io::Write; + + let mut file_handles: Vec<_> = Vec::new(); + + // Use a deterministic temporary directory. + // We can't have all tests use "tmp" because tests run in parallel, + // so append the test name to the tmp path. + let tmp = format!("tmp/{subdir}"); + let dir = roc_test_utils::TmpDir::new(&tmp); + + let app_module = files.pop().unwrap(); + + for (name, source) in files { + let mut filename = PathBuf::from(name); + filename.set_extension("roc"); + let file_path = dir.path().join(filename.clone()); + + // Create any necessary intermediate directories (e.g. /platform) + fs::create_dir_all(file_path.parent().unwrap())?; + + let mut file = File::create(file_path)?; + writeln!(file, "{source}")?; + file_handles.push(file); + } + + let result = { + let (name, source) = app_module; + + let filename = PathBuf::from(name); + let file_path = dir.path().join(filename); + let full_file_path = file_path.clone(); + let mut file = File::create(file_path)?; + writeln!(file, "{source}")?; + file_handles.push(file); + + load_and_typecheck( + arena, + full_file_path, + Default::default(), + TARGET_INFO, + FunctionKind::LambdaSet, + ) + }; + + Ok(result) +} + +fn load_fixture( + dir_name: &str, + module_name: &str, + subs_by_module: ExposedByModule, +) -> LoadedModule { + let src_dir = fixtures_dir().join(dir_name); + let filename = src_dir.join(format!("{module_name}.roc")); + let arena = Bump::new(); + let loaded = load_and_typecheck( + &arena, + filename, + subs_by_module, + TARGET_INFO, + FunctionKind::LambdaSet, + ); + let mut loaded_module = match loaded { + Ok(x) => x, + Err(roc_load_internal::file::LoadingProblem::FormattedReport(report)) => { + println!("{report}"); + panic!("{}", report); + } + Err(e) => panic!("{e:?}"), + }; + + let home = loaded_module.module_id; + + assert_eq!( + loaded_module.can_problems.remove(&home).unwrap_or_default(), + Vec::new() + ); + assert!(loaded_module + .type_problems + .remove(&home) + .unwrap_or_default() + .is_empty()); + + let expected_name = loaded_module + .interns + .module_ids + .get_name(loaded_module.module_id) + .expect("Test ModuleID not found in module_ids"); + + // App module names are hardcoded and not based on anything user-specified + if expected_name.as_str() != ModuleName::APP { + assert_eq!(&expected_name.as_str(), &module_name); + } + + loaded_module +} + +fn expect_types(mut loaded_module: LoadedModule, mut expected_types: HashMap<&str, &str>) { + let home = loaded_module.module_id; + let mut subs = loaded_module.solved.into_inner(); + + assert_eq!( + loaded_module.can_problems.remove(&home).unwrap_or_default(), + Vec::new() + ); + assert!(loaded_module + .type_problems + .remove(&home) + .unwrap_or_default() + .is_empty()); + + let debug_print = DebugPrint::NOTHING; + + let interns = &loaded_module.interns; + let declarations = loaded_module.declarations_by_id.remove(&home).unwrap(); + for index in 0..declarations.len() { + use roc_can::expr::DeclarationTag::*; + + match declarations.declarations[index] { + Value | Function(_) | Recursive(_) | TailRecursive(_) => { + let symbol = declarations.symbols[index].value; + let expr_var = declarations.variables[index]; + + let actual_str = + name_and_print_var(expr_var, &mut subs, home, interns, debug_print); + let fully_qualified = symbol.fully_qualified(interns, home).to_string(); + let expected_type = expected_types + .remove(fully_qualified.as_str()) + .unwrap_or_else(|| { + panic!("Defs included an unexpected symbol: {fully_qualified:?}") + }); + + assert_eq!((&symbol, expected_type), (&symbol, actual_str.as_str())); + } + Destructure(d_index) => { + let pattern_vars = &declarations.destructs[d_index.index()].pattern_vars; + for (symbol, expr_var) in pattern_vars.iter() { + let actual_str = + name_and_print_var(*expr_var, &mut subs, home, interns, debug_print); + + let fully_qualified = symbol.fully_qualified(interns, home).to_string(); + let expected_type = expected_types + .remove(fully_qualified.as_str()) + .unwrap_or_else(|| { + panic!("Defs included an unexpected symbol: {fully_qualified:?}") + }); + + assert_eq!((&symbol, expected_type), (&symbol, actual_str.as_str())); + } + } + MutualRecursion { cycle_mark, .. } => { + assert!(!cycle_mark.is_illegal(&subs)); + } + Expectation => { + // at least at the moment this does not happen + panic!("Unexpected expectation in module declarations"); + } + ExpectationFx => { + // at least at the moment this does not happen + panic!("Unexpected expectation in module declarations"); + } + }; + } + + assert_eq!( + expected_types, + HashMap::default(), + "Some expected types were not found in the defs" + ); +} + +// TESTS + +#[test] +fn import_transitive_alias() { + // this had a bug where NodeColor was HostExposed, and it's `actual_var` conflicted + // with variables in the importee + let modules = vec![ + ( + "RBTree", + indoc!( + r#" + interface RBTree exposes [RedBlackTree, empty] imports [] + + # The color of a node. Leaves are considered Black. + NodeColor : [Red, Black] + + RedBlackTree k v : [Node NodeColor k v (RedBlackTree k v) (RedBlackTree k v), Empty] + + # Create an empty dictionary. + empty : RedBlackTree k v + empty = + Empty + "# + ), + ), + ( + "Other", + indoc!( + r#" + interface Other exposes [empty] imports [RBTree] + + empty : RBTree.RedBlackTree I64 I64 + empty = RBTree.empty + "# + ), + ), + ]; + + assert!(multiple_modules("import_transitive_alias", modules).is_ok()); +} + +#[test] +fn interface_with_deps() { + let subs_by_module = Default::default(); + let src_dir = fixtures_dir().join("interface_with_deps"); + let filename = src_dir.join("Primary.roc"); + let arena = Bump::new(); + let loaded = load_and_typecheck( + &arena, + filename, + subs_by_module, + TARGET_INFO, + FunctionKind::LambdaSet, + ); + + let mut loaded_module = loaded.expect("Test module failed to load"); + let home = loaded_module.module_id; + + assert_eq!( + loaded_module.can_problems.remove(&home).unwrap_or_default(), + Vec::new() + ); + assert!(loaded_module + .type_problems + .remove(&home) + .unwrap_or_default() + .is_empty(),); + + let mut def_count = 0; + let declarations = loaded_module.declarations_by_id.remove(&home).unwrap(); + for index in 0..declarations.len() { + use roc_can::expr::DeclarationTag::*; + + match declarations.declarations[index] { + Value | Function(_) | Recursive(_) | TailRecursive(_) => { + def_count += 1; + } + Destructure(_) => { + def_count += 1; + } + MutualRecursion { .. } => { /* do nothing, not a def */ } + Expectation | ExpectationFx => { /* do nothing, not a def */ } + } + } + + let expected_name = loaded_module + .interns + .module_ids + .get_name(loaded_module.module_id) + .expect("Test ModuleID not found in module_ids"); + + assert_eq!(expected_name.as_str(), "Primary"); + assert_eq!(def_count, 10); +} + +#[test] +fn load_unit() { + let subs_by_module = Default::default(); + let loaded_module = load_fixture("no_deps", "Unit", subs_by_module); + + expect_types( + loaded_module, + hashmap! { + "unit" => "Unit", + }, + ); +} + +#[test] +fn import_alias() { + let subs_by_module = Default::default(); + let loaded_module = load_fixture("interface_with_deps", "ImportAlias", subs_by_module); + + expect_types( + loaded_module, + hashmap! { + "unit" => "Dep1.Unit", + }, + ); +} + +#[test] +fn test_load_and_typecheck() { + let subs_by_module = Default::default(); + let loaded_module = load_fixture("interface_with_deps", "WithBuiltins", subs_by_module); + + expect_types( + loaded_module, + hashmap! { + "floatTest" => "F64", + "divisionFn" => "Frac a, Frac a -> Frac a", + "x" => "Frac *", + "divisionTest" => "F64", + "intTest" => "I64", + "constantNum" => "Num *", + "divisionTest" => "F64", + "divDep1ByDep2" => "Frac a", + "fromDep2" => "Frac a", + }, + ); +} + +#[test] +fn iface_quicksort() { + let subs_by_module = Default::default(); + let loaded_module = load_fixture("interface_with_deps", "Quicksort", subs_by_module); + + expect_types( + loaded_module, + hashmap! { + "swap" => "Nat, Nat, List a -> List a", + "partition" => "Nat, Nat, List (Num a) -> [Pair Nat (List (Num a))]", + "partitionHelp" => "Nat, Nat, List (Num a), Nat, Num a -> [Pair Nat (List (Num a))]", + "quicksort" => "List (Num a), Nat, Nat -> List (Num a)", + }, + ); +} + +#[test] +fn quicksort_one_def() { + let subs_by_module = Default::default(); + let loaded_module = load_fixture("app_with_deps", "QuicksortMultiDef", subs_by_module); + + expect_types( + loaded_module, + hashmap! { + "swap" => "Nat, Nat, List a -> List a", + "partition" => "Nat, Nat, List (Num a) -> [Pair Nat (List (Num a))]", + "partitionHelp" => "Nat, Nat, List (Num a), Nat, Num a -> [Pair Nat (List (Num a))]", + "quicksortHelp" => "List (Num a), Nat, Nat -> List (Num a)", + "quicksort" => "List (Num a) -> List (Num a)", + }, + ); +} + +#[test] +fn app_quicksort() { + let subs_by_module = Default::default(); + let loaded_module = load_fixture("app_with_deps", "Quicksort", subs_by_module); + + expect_types( + loaded_module, + hashmap! { + "swap" => "Nat, Nat, List a -> List a", + "partition" => "Nat, Nat, List (Num a) -> [Pair Nat (List (Num a))]", + "partitionHelp" => "Nat, Nat, List (Num a), Nat, Num a -> [Pair Nat (List (Num a))]", + "quicksort" => "List (Num a), Nat, Nat -> List (Num a)", + }, + ); +} + +#[test] +fn load_astar() { + let subs_by_module = Default::default(); + let loaded_module = load_fixture("interface_with_deps", "AStar", subs_by_module); + + expect_types( + loaded_module, + hashmap! { + "findPath" => "{ costFunction : position, position -> F64, end : position, moveFunction : position -> Set position, start : position } -> Result (List position) [KeyNotFound] where position implements Hash & Eq", + "initialModel" => "position -> Model position where position implements Hash & Eq", + "reconstructPath" => "Dict position position, position -> List position where position implements Hash & Eq", + "updateCost" => "position, position, Model position -> Model position where position implements Hash & Eq", + "cheapestOpen" => "(position -> F64), Model position -> Result position [KeyNotFound] where position implements Hash & Eq", + "astar" => "(position, position -> F64), (position -> Set position), position, Model position -> [Err [KeyNotFound], Ok (List position)] where position implements Hash & Eq", + }, + ); +} + +#[test] +fn load_principal_types() { + let subs_by_module = Default::default(); + let loaded_module = load_fixture("no_deps", "Principal", subs_by_module); + + expect_types( + loaded_module, + hashmap! { + "intVal" => "Str", + "identity" => "a -> a", + }, + ); +} + +#[test] +fn iface_dep_types() { + let subs_by_module = Default::default(); + let loaded_module = load_fixture("interface_with_deps", "Primary", subs_by_module); + + expect_types( + loaded_module, + hashmap! { + "blah2" => "Frac *", + "blah3" => "Str", + "str" => "Str", + "alwaysThree" => "* -> Frac *", + "identity" => "a -> a", + "z" => "Frac *", + "w" => "Dep1.Identity {}", + "succeed" => "a -> Dep1.Identity a", + "yay" => "Res.Res {} err", + "withDefault" => "Res.Res a err, a -> a", + }, + ); +} + +#[test] +fn app_dep_types() { + let subs_by_module = Default::default(); + let loaded_module = load_fixture("app_with_deps", "Primary", subs_by_module); + + expect_types( + loaded_module, + hashmap! { + "blah2" => "Frac *", + "blah3" => "Str", + "str" => "Str", + "alwaysThree" => "* -> Frac *", + "identity" => "a -> a", + "z" => "Frac *", + "w" => "Dep1.Identity {}", + "succeed" => "a -> Dep1.Identity a", + "yay" => "Res.Res {} err", + "withDefault" => "Res.Res a err, a -> a", + }, + ); +} + +#[test] +fn imported_dep_regression() { + let subs_by_module = Default::default(); + let loaded_module = load_fixture("interface_with_deps", "OneDep", subs_by_module); + + expect_types( + loaded_module, + hashmap! { + "str" => "Str", + }, + ); +} + +#[test] +fn ingested_file() { + let subs_by_module = Default::default(); + let loaded_module = load_fixture("interface_with_deps", "IngestedFile", subs_by_module); + + expect_types( + loaded_module, + hashmap! { + "foo" => "Str", + "str" => "Str", + }, + ); +} + +#[test] +fn ingested_file_bytes() { + let subs_by_module = Default::default(); + let loaded_module = load_fixture("interface_with_deps", "IngestedFileBytes", subs_by_module); + + expect_types( + loaded_module, + hashmap! { + "foo" => "List U8", + "str" => "Str", + }, + ); +} + +#[test] +fn parse_problem() { + let modules = vec![( + "Main", + indoc!( + r#" + interface Main exposes [main] imports [] + + main = [ + "# + ), + )]; + + match multiple_modules("parse_problem", modules) { + Err(report) => assert_eq!( + report, + indoc!( + " + ── UNFINISHED LIST ──────────────────────────────────── tmp/parse_problem/Main ─ + + I am partway through started parsing a list, but I got stuck here: + + 3│ main = [ + 4│ + 5│ + ^ + + I was expecting to see a closing square bracket before this, so try + adding a ] and see if that helps? + + Note: When I get stuck like this, it usually means that there is a + missing parenthesis or bracket somewhere earlier. It could also be a + stray keyword or operator." + ) + ), + Ok(_) => unreachable!("we expect failure here"), + } +} + +#[test] +#[should_panic(expected = "FILE NOT FOUND")] +fn file_not_found() { + let subs_by_module = Default::default(); + let loaded_module = load_fixture("interface_with_deps", "invalid$name", subs_by_module); + + expect_types( + loaded_module, + hashmap! { + "str" => "Str", + }, + ); +} + +#[test] +#[should_panic(expected = "FILE NOT FOUND")] +fn imported_file_not_found() { + let subs_by_module = Default::default(); + let loaded_module = load_fixture("no_deps", "MissingDep", subs_by_module); + + expect_types( + loaded_module, + hashmap! { + "str" => "Str", + }, + ); +} + +#[test] +#[should_panic(expected = "FILE NOT FOUND")] +fn ingested_file_not_found() { + let subs_by_module = Default::default(); + let loaded_module = load_fixture("no_deps", "MissingIngestedFile", subs_by_module); + + expect_types( + loaded_module, + hashmap! { + "str" => "Str", + }, + ); +} + +#[test] +fn platform_does_not_exist() { + let modules = vec![( + "Main", + indoc!( + r#" + app "example" + packages { pf: "./zzz-does-not-exist/main.roc" } + imports [] + provides [main] to pf + + main = "" + "# + ), + )]; + + match multiple_modules("platform_does_not_exist", modules) { + Err(report) => { + // TODO restore this assert once it can pass. + // assert!(report.contains("FILE NOT FOUND"), "report=({})", report); + assert!( + report.contains("zzz-does-not-exist/main.roc"), + "report=({report})" + ); + } + Ok(_) => unreachable!("we expect failure here"), + } +} + +#[test] +fn platform_parse_error() { + let modules = vec![ + ( + "platform/main.roc", + indoc!( + r#" + platform "hello-c" + requires {} { main : Str } + exposes [] + packages {} + imports [] + provides [mainForHost] + blah 1 2 3 # causing a parse error on purpose + + mainForHost : Str + "# + ), + ), + ( + "Main", + indoc!( + r#" + app "hello-world" + packages { pf: "platform/main.roc" } + imports [] + provides [main] to pf + + main = "Hello, World!\n" + "# + ), + ), + ]; + + match multiple_modules("platform_parse_error", modules) { + Err(report) => { + assert!(report.contains("NOT END OF FILE")); + assert!(report.contains("blah 1 2 3 # causing a parse error on purpose")); + } + Ok(_) => unreachable!("we expect failure here"), + } +} + +#[test] +// See https://github.com/roc-lang/roc/issues/2413 +fn platform_exposes_main_return_by_pointer_issue() { + let modules = vec![ + ( + "platform/main.roc", + indoc!( + r#" + platform "hello-world" + requires {} { main : { content: Str, other: Str } } + exposes [] + packages {} + imports [] + provides [mainForHost] + + mainForHost : { content: Str, other: Str } + mainForHost = main + "# + ), + ), + ( + "Main", + indoc!( + r#" + app "hello-world" + packages { pf: "platform/main.roc" } + imports [] + provides [main] to pf + + main = { content: "Hello, World!\n", other: "" } + "# + ), + ), + ]; + + assert!(multiple_modules("platform_exposes_main_return_by_pointer_issue", modules).is_ok()); +} + +#[test] +fn opaque_wrapped_unwrapped_outside_defining_module() { + let modules = vec![ + ( + "Age", + indoc!( + r#" + interface Age exposes [Age] imports [] + + Age := U32 + "# + ), + ), + ( + "Main", + indoc!( + r#" + interface Main exposes [twenty, readAge] imports [Age.{ Age }] + + twenty = @Age 20 + + readAge = \@Age n -> n + "# + ), + ), + ]; + + let err = + multiple_modules("opaque_wrapped_unwrapped_outside_defining_module", modules).unwrap_err(); + assert_eq!( + err, + indoc!( + r#" + ── OPAQUE TYPE DECLARED OUTSIDE SCOPE ─ ...rapped_outside_defining_module/Main ─ + + The unwrapped opaque type Age referenced here: + + 3│ twenty = @Age 20 + ^^^^ + + is imported from another module: + + 1│ interface Main exposes [twenty, readAge] imports [Age.{ Age }] + ^^^ + + Note: Opaque types can only be wrapped and unwrapped in the module they are defined in! + + ── OPAQUE TYPE DECLARED OUTSIDE SCOPE ─ ...rapped_outside_defining_module/Main ─ + + The unwrapped opaque type Age referenced here: + + 5│ readAge = \@Age n -> n + ^^^^ + + is imported from another module: + + 1│ interface Main exposes [twenty, readAge] imports [Age.{ Age }] + ^^^ + + Note: Opaque types can only be wrapped and unwrapped in the module they are defined in! + + ── UNUSED IMPORT ─── tmp/opaque_wrapped_unwrapped_outside_defining_module/Main ─ + + Nothing from Age is used in this module. + + 1│ interface Main exposes [twenty, readAge] imports [Age.{ Age }] + ^^^^^^^^^^^ + + Since Age isn't used, you don't need to import it. + "# + ), + "\n{}", + err + ); +} + +#[test] +fn issue_2863_module_type_does_not_exist() { + let modules = vec![ + ( + "platform/main.roc", + indoc!( + r#" + platform "testplatform" + requires {} { main : Str } + exposes [] + packages {} + imports [] + provides [mainForHost] + + mainForHost : Str + mainForHost = main + "# + ), + ), + ( + "Main", + indoc!( + r#" + app "test" + packages { pf: "platform/main.roc" } + provides [main] to pf + + main : DoesNotExist + main = 1 + "# + ), + ), + ]; + + match multiple_modules("issue_2863_module_type_does_not_exist", modules) { + Err(report) => { + assert_eq!( + report, + indoc!( + " + ── UNRECOGNIZED NAME ────────── tmp/issue_2863_module_type_does_not_exist/Main ─ + + Nothing is named `DoesNotExist` in this scope. + + 5│ main : DoesNotExist + ^^^^^^^^^^^^ + + Did you mean one of these? + + Decoding + Dict + Result + DecodeError + " + ) + ) + } + Ok(_) => unreachable!("we expect failure here"), + } +} + +#[test] +fn import_builtin_in_platform_and_check_app() { + let modules = vec![ + ( + "platform/main.roc", + indoc!( + r#" + platform "testplatform" + requires {} { main : Str } + exposes [] + packages {} + imports [Str] + provides [mainForHost] + + mainForHost : Str + mainForHost = main + "# + ), + ), + ( + "Main", + indoc!( + r#" + app "test" + packages { pf: "platform/main.roc" } + provides [main] to pf + + main = "" + "# + ), + ), + ]; + + let result = multiple_modules("import_builtin_in_platform_and_check_app", modules); + assert!(result.is_ok(), "should check"); +} + +#[test] +fn module_doesnt_match_file_path() { + let modules = vec![( + "Age", + indoc!( + r#" + interface NotAge exposes [Age] imports [] + + Age := U32 + "# + ), + )]; + + let err = multiple_modules("module_doesnt_match_file_path", modules).unwrap_err(); + assert_eq!( + err, + indoc!( + r#" + ── WEIRD MODULE NAME ─────────────────── tmp/module_doesnt_match_file_path/Age ─ + + This module name does not correspond with the file path it is defined + in: + + 1│ interface NotAge exposes [Age] imports [] + ^^^^^^ + + Module names must correspond with the file paths they are defined in. + For example, I expect to see BigNum defined in BigNum.roc, or Math.Sin + defined in Math/Sin.roc."# + ), + "\n{}", + err + ); +} + +#[test] +fn module_cyclic_import_itself() { + let modules = vec![( + "Age", + indoc!( + r#" + interface Age exposes [] imports [Age] + "# + ), + )]; + + let err = multiple_modules("module_cyclic_import_itself", modules).unwrap_err(); + assert_eq!( + err, + indoc!( + r#" + ── IMPORT CYCLE ────────────────────────── tmp/module_cyclic_import_itself/Age ─ + + I can't compile Age because it depends on itself through the following + chain of module imports: + + ┌─────┐ + │ Age + │ ↓ + │ Age + └─────┘ + + Cyclic dependencies are not allowed in Roc! Can you restructure a + module in this import chain so that it doesn't have to depend on + itself?"# + ), + "\n{}", + err + ); +} + +#[test] +fn module_cyclic_import_transitive() { + let modules = vec![ + ( + "Age", + indoc!( + r#" + interface Age exposes [] imports [Person] + "# + ), + ), + ( + "Person", + indoc!( + r#" + interface Person exposes [] imports [Age] + "# + ), + ), + ]; + + let err = multiple_modules("module_cyclic_import_transitive", modules).unwrap_err(); + assert_eq!( + err, + indoc!( + r#" + ── IMPORT CYCLE ────────────────── tmp/module_cyclic_import_transitive/Age.roc ─ + + I can't compile Age because it depends on itself through the following + chain of module imports: + + ┌─────┐ + │ Age + │ ↓ + │ Person + │ ↓ + │ Age + └─────┘ + + Cyclic dependencies are not allowed in Roc! Can you restructure a + module in this import chain so that it doesn't have to depend on + itself?"# + ), + "\n{}", + err + ); +} + +#[test] +fn nested_module_has_incorrect_name() { + let modules = vec![ + ( + "Dep/Foo.roc", + indoc!( + r#" + interface Foo exposes [] imports [] + "# + ), + ), + ( + "I.roc", + indoc!( + r#" + interface I exposes [] imports [Dep.Foo] + "# + ), + ), + ]; + + let err = multiple_modules("nested_module_has_incorrect_name", modules).unwrap_err(); + assert_eq!( + err, + indoc!( + r#" + ── INCORRECT MODULE NAME ──── tmp/nested_module_has_incorrect_name/Dep/Foo.roc ─ + + This module has a different name than I expected: + + 1│ interface Foo exposes [] imports [] + ^^^ + + Based on the nesting and use of this module, I expect it to have name + + Dep.Foo"# + ), + "\n{}", + err + ); +} diff --git a/crates/compiler/module/Cargo.toml b/crates/compiler/module/Cargo.toml new file mode 100644 index 0000000000..6be6b4e716 --- /dev/null +++ b/crates/compiler/module/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "roc_module" +description = "Implements data structures used for efficiently representing unique modules and identifiers in Roc programs." + +authors.workspace = true +edition.workspace = true +license.workspace = true +version.workspace = true + +[dependencies] +roc_collections = { path = "../collections" } +roc_error_macros = { path = "../../error_macros" } +roc_ident = { path = "../ident" } +roc_region = { path = "../region" } + +bumpalo.workspace = true +snafu.workspace = true +static_assertions.workspace = true + +[features] +debug-symbols = [] +default = [] diff --git a/crates/compiler/module/src/called_via.rs b/crates/compiler/module/src/called_via.rs new file mode 100644 index 0000000000..3cb7394be1 --- /dev/null +++ b/crates/compiler/module/src/called_via.rs @@ -0,0 +1,290 @@ +use self::Associativity::*; +use self::BinOp::*; +use std::cmp::Ordering; +use std::fmt; + +const PRECEDENCES: [(BinOp, u8); 20] = [ + (Caret, 8), + (Star, 7), + (Slash, 7), + (DoubleSlash, 6), + (Percent, 6), + (Plus, 5), + (Minus, 5), + (Pizza, 4), + (Equals, 3), + (NotEquals, 3), + (LessThan, 2), + (GreaterThan, 2), + (LessThanOrEq, 2), + (GreaterThanOrEq, 2), + (And, 1), + (Or, 0), + // These should never come up + (Assignment, 255), + (IsAliasType, 255), + (IsOpaqueType, 255), + (Backpassing, 255), +]; + +const ASSOCIATIVITIES: [(BinOp, Associativity); 20] = [ + (Caret, RightAssociative), + (Star, LeftAssociative), + (Slash, LeftAssociative), + (DoubleSlash, LeftAssociative), + (Percent, LeftAssociative), + (Plus, LeftAssociative), + (Minus, LeftAssociative), + (Pizza, LeftAssociative), + (Equals, NonAssociative), + (NotEquals, NonAssociative), + (LessThan, NonAssociative), + (GreaterThan, NonAssociative), + (LessThanOrEq, NonAssociative), + (GreaterThanOrEq, NonAssociative), + (And, RightAssociative), + (Or, RightAssociative), + // These should never come up + (Assignment, LeftAssociative), + (IsAliasType, LeftAssociative), + (IsOpaqueType, LeftAssociative), + (Backpassing, LeftAssociative), +]; + +const DISPLAY_STRINGS: [(BinOp, &str); 20] = [ + (Caret, "^"), + (Star, "*"), + (Slash, "/"), + (DoubleSlash, "//"), + (Percent, "%"), + (Plus, "+"), + (Minus, "-"), + (Pizza, "|>"), + (Equals, "=="), + (NotEquals, "!="), + (LessThan, "<"), + (GreaterThan, ">"), + (LessThanOrEq, "<="), + (GreaterThanOrEq, ">="), + (And, "&&"), + (Or, "||"), + (Assignment, "="), + (IsAliasType, ":"), + (IsOpaqueType, ":="), + (Backpassing, "<-"), +]; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum CalledVia { + /// Calling with space, e.g. (foo bar) + Space, + + /// Calling with an operator, e.g. (bar |> foo) or (1 + 2) + BinOp(BinOp), + + /// Calling with a unary operator, e.g. (!foo bar baz) or (-foo bar baz) + UnaryOp(UnaryOp), + + /// This call is the result of desugaring string interpolation, + /// e.g. "\(first) \(last)" is transformed into Str.concat (Str.concat first " ") last. + StringInterpolation, + + /// This call is the result of desugaring a Record Builder field. + /// e.g. succeed { a <- get "a" } is transformed into (get "a") (succeed \a -> { a }) + RecordBuilder, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum UnaryOp { + /// (-), e.g. (-x) + Negate, + /// (!), e.g. (!x) + Not, +} + +impl std::fmt::Display for UnaryOp { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + UnaryOp::Negate => write!(f, "-"), + UnaryOp::Not => write!(f, "!"), + } + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum BinOp { + // highest precedence + Caret, + Star, + Slash, + DoubleSlash, + Percent, + Plus, + Minus, + Pizza, + Equals, + NotEquals, + LessThan, + GreaterThan, + LessThanOrEq, + GreaterThanOrEq, + And, + Or, + Assignment, + IsAliasType, + IsOpaqueType, + Backpassing, + // lowest precedence +} + +impl BinOp { + /// how wide this operator is when typed out + pub fn width(self) -> u16 { + match self { + Caret | Star | Slash | Percent | Plus | Minus | LessThan | GreaterThan => 1, + DoubleSlash | Equals | NotEquals | LessThanOrEq | GreaterThanOrEq | And | Or + | Pizza => 2, + Assignment | IsAliasType | IsOpaqueType | Backpassing => unreachable!(), + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum ArgSide { + Left, + Right, +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum Associativity { + /// left-associative operators: + /// + /// arithmetic: * / // % + - + /// application: |> + LeftAssociative, + + /// right-associative operators: + /// + /// exponentiation: ^ + /// boolean: && || + /// application: <| + RightAssociative, + + /// non-associative operators: + /// + /// comparison: == > >= < <= + NonAssociative, +} + +impl BinOp { + pub fn associativity(self) -> Associativity { + // The compiler should never pass any of these to this function! + debug_assert_ne!(self, Assignment); + debug_assert_ne!(self, IsAliasType); + debug_assert_ne!(self, IsOpaqueType); + debug_assert_ne!(self, Backpassing); + + const ASSOCIATIVITY_TABLE: [Associativity; 20] = generate_associativity_table(); + + ASSOCIATIVITY_TABLE[self as usize] + } + + fn precedence(self) -> u8 { + // The compiler should never pass any of these to this function! + debug_assert_ne!(self, Assignment); + debug_assert_ne!(self, IsAliasType); + debug_assert_ne!(self, IsOpaqueType); + debug_assert_ne!(self, Backpassing); + + const PRECEDENCE_TABLE: [u8; 20] = generate_precedence_table(); + + PRECEDENCE_TABLE[self as usize] + } +} + +impl PartialOrd for BinOp { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for BinOp { + fn cmp(&self, other: &Self) -> Ordering { + self.precedence().cmp(&other.precedence()) + } +} + +impl std::fmt::Display for BinOp { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + debug_assert_ne!(*self, Assignment); + debug_assert_ne!(*self, IsAliasType); + debug_assert_ne!(*self, IsOpaqueType); + debug_assert_ne!(*self, Backpassing); + + const DISPLAY_TABLE: [&str; 20] = generate_display_table(); + + write!(f, "{}", DISPLAY_TABLE[*self as usize]) + } +} + +const fn generate_precedence_table() -> [u8; 20] { + let mut table = [0u8; 20]; + let mut i = 0; + + while i < PRECEDENCES.len() { + table[(PRECEDENCES[i].0) as usize] = PRECEDENCES[i].1; + i += 1; + } + + table +} + +const fn generate_associativity_table() -> [Associativity; 20] { + let mut table = [NonAssociative; 20]; + let mut i = 0; + + while i < ASSOCIATIVITIES.len() { + table[(ASSOCIATIVITIES[i].0) as usize] = ASSOCIATIVITIES[i].1; + i += 1; + } + + table +} + +const fn generate_display_table() -> [&'static str; 20] { + let mut table = [""; 20]; + let mut i = 0; + + while i < DISPLAY_STRINGS.len() { + table[(DISPLAY_STRINGS[i].0) as usize] = DISPLAY_STRINGS[i].1; + i += 1; + } + + table +} + +#[cfg(test)] +mod tests { + use super::{BinOp, ASSOCIATIVITIES, DISPLAY_STRINGS, PRECEDENCES}; + + fn index_is_binop_u8(iter: impl Iterator, table_name: &'static str) { + for (index, op) in iter.enumerate() { + assert_eq!(op as usize, index, "{op} was found at index {index} in {table_name}, but it should have been at index {} instead.", op as usize); + } + } + + #[test] + fn indices_are_correct_in_precedences() { + index_is_binop_u8(PRECEDENCES.iter().map(|(op, _)| *op), "PRECEDENCES") + } + + #[test] + fn indices_are_correct_in_associativities() { + index_is_binop_u8(ASSOCIATIVITIES.iter().map(|(op, _)| *op), "ASSOCIATIVITIES") + } + + #[test] + fn indices_are_correct_in_display_string() { + index_is_binop_u8(DISPLAY_STRINGS.iter().map(|(op, _)| *op), "DISPLAY_STRINGS") + } +} diff --git a/crates/compiler/module/src/ident.rs b/crates/compiler/module/src/ident.rs new file mode 100644 index 0000000000..558846915b --- /dev/null +++ b/crates/compiler/module/src/ident.rs @@ -0,0 +1,348 @@ +pub use roc_ident::IdentStr; +use std::fmt::{self, Debug}; + +use crate::symbol::PQModuleName; + +/// This could be uppercase or lowercase, qualified or unqualified. +#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Default)] +pub struct Ident(pub IdentStr); + +impl Ident { + pub fn as_inline_str(&self) -> &IdentStr { + &self.0 + } + + #[inline(always)] + pub fn as_str(&self) -> &str { + self.0.as_str() + } +} + +pub struct QualifiedModuleName<'a> { + pub opt_package: Option<&'a str>, + pub module: ModuleName, +} + +impl<'a> QualifiedModuleName<'a> { + pub fn into_pq_module_name(self, opt_shorthand: Option<&'a str>) -> PQModuleName<'a> { + if self.is_builtin() { + // If this is a builtin, it must be unqualified, and we should *never* prefix it + // with the package shorthand! The user intended to import the module as-is here. + debug_assert!(self.opt_package.is_none()); + PQModuleName::Unqualified(self.module) + } else { + match self.opt_package { + None => match opt_shorthand { + Some(shorthand) => PQModuleName::Qualified(shorthand, self.module), + None => PQModuleName::Unqualified(self.module), + }, + Some(package) => PQModuleName::Qualified(package, self.module), + } + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct ModuleName(IdentStr); + +impl std::ops::Deref for ModuleName { + type Target = str; + + fn deref(&self) -> &Self::Target { + self.0.as_str() + } +} + +/// An uncapitalized identifier, such as a field name or local variable +#[derive(Clone, Default, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct Lowercase(IdentStr); + +/// A capitalized identifier, such as a tag name or module name +#[derive(Clone, Default, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct Uppercase(IdentStr); + +/// A string representing a foreign (linked-in) symbol +#[derive(Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Debug)] +pub struct ForeignSymbol(IdentStr); + +pub type TagIdIntType = u16; + +/// Tags have no module, but tend to be short strings (since they're +/// never qualified), so we store them as ident strings. +/// +/// This is allows canonicalization to happen in parallel without locks. +/// If tags had a Symbol representation, then each module would have to +/// deal with contention on a global mutex around translating tag strings +/// into integers. (Record field labels work the same way, for the same reason.) +#[derive(Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct TagName(pub Uppercase); + +roc_error_macros::assert_sizeof_non_wasm!(TagName, 16); +roc_error_macros::assert_sizeof_wasm!(TagName, 8); + +impl TagName { + pub fn as_ident_str(&self) -> IdentStr { + self.0.as_ident_str().clone() + } +} + +impl Debug for TagName { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + Debug::fmt(&self.0, f) + } +} + +impl From<&str> for TagName { + fn from(string: &str) -> Self { + Self(string.into()) + } +} + +impl ModuleName { + // NOTE: After adding one of these, go to `impl ModuleId` and + // add a corresponding ModuleId to there! + pub const APP: &'static str = "#UserApp"; // app modules have this hardcoded name + pub const BOOL: &'static str = "Bool"; + pub const STR: &'static str = "Str"; + pub const NUM: &'static str = "Num"; + pub const LIST: &'static str = "List"; + pub const DICT: &'static str = "Dict"; + pub const SET: &'static str = "Set"; + pub const RESULT: &'static str = "Result"; + pub const BOX: &'static str = "Box"; + pub const ENCODE: &'static str = "Encode"; + pub const DECODE: &'static str = "Decode"; + pub const HASH: &'static str = "Hash"; + pub const INSPECT: &'static str = "Inspect"; + pub const JSON: &'static str = "TotallyNotJson"; + + pub fn as_str(&self) -> &str { + self.0.as_str() + } + + pub fn as_ident_str(&self) -> &IdentStr { + &self.0 + } + + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } +} + +impl AsRef for ModuleName { + #[inline(always)] + fn as_ref(&self) -> &str { + self.as_str() + } +} + +impl<'a> From<&'a str> for ModuleName { + fn from(string: &'a str) -> Self { + Self(string.into()) + } +} + +impl From for ModuleName { + fn from(string: IdentStr) -> Self { + Self(string.as_str().into()) + } +} + +impl From> for ModuleName { + fn from(string: Box) -> Self { + Self((string.as_ref()).into()) + } +} + +impl From for ModuleName { + fn from(string: String) -> Self { + Self(string.into()) + } +} + +impl From for Box { + fn from(name: ModuleName) -> Self { + name.0.to_string().into() + } +} + +impl fmt::Display for ModuleName { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fmt::Display::fmt(&self.0, f) + } +} + +impl ForeignSymbol { + pub fn as_str(&self) -> &str { + self.0.as_str() + } + + pub fn as_inline_str(&self) -> &IdentStr { + &self.0 + } +} + +impl<'a> From<&'a str> for ForeignSymbol { + fn from(string: &'a str) -> Self { + Self(string.into()) + } +} + +impl From for ForeignSymbol { + fn from(string: String) -> Self { + Self(string.into()) + } +} + +impl Uppercase { + pub fn as_str(&self) -> &str { + self.0.as_str() + } + + pub fn as_ident_str(&self) -> &IdentStr { + &self.0 + } +} + +impl<'a> From<&'a str> for Uppercase { + fn from(string: &'a str) -> Self { + Self(string.into()) + } +} + +impl From for Uppercase { + fn from(string: String) -> Self { + Self(string.into()) + } +} + +impl Lowercase { + pub fn as_str(&self) -> &str { + self.0.as_str() + } +} + +impl From for String { + fn from(lowercase: Lowercase) -> Self { + lowercase.0.into() + } +} + +impl From for Box { + fn from(lowercase: Lowercase) -> Self { + let string: String = lowercase.0.into(); + + string.into() + } +} + +impl<'a> From<&'a str> for Lowercase { + fn from(string: &'a str) -> Self { + Self(string.into()) + } +} + +impl<'a> From<&'a Lowercase> for &'a str { + fn from(lowercase: &'a Lowercase) -> Self { + lowercase.as_str() + } +} + +impl From for Lowercase { + fn from(string: String) -> Self { + Self(string.into()) + } +} + +impl AsRef for Ident { + #[inline(always)] + fn as_ref(&self) -> &str { + self.0.as_str() + } +} + +impl<'a> From<&'a str> for Ident { + fn from(string: &'a str) -> Self { + Self(string.into()) + } +} + +impl From> for Ident { + fn from(string: Box) -> Self { + Self((string.as_ref()).into()) + } +} + +impl From for Ident { + fn from(string: String) -> Self { + Self(string.into()) + } +} + +impl From for Ident { + fn from(string: IdentStr) -> Self { + Self(string) + } +} + +impl From for IdentStr { + fn from(ident: Ident) -> Self { + ident.0 + } +} + +impl<'a> From<&'a Ident> for &'a IdentStr { + fn from(ident: &'a Ident) -> Self { + &ident.0 + } +} + +impl From for Box { + fn from(ident: Ident) -> Self { + ident.0.to_string().into() + } +} + +impl fmt::Display for Ident { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fmt::Display::fmt(&self.0, f) + } +} + +/// Rather than displaying as this: +/// +/// Lowercase("foo") +/// +/// ...instead display as this: +/// +/// 'foo' +impl fmt::Debug for Lowercase { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "'{}'", self.0) + } +} + +impl fmt::Display for Lowercase { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fmt::Display::fmt(&self.0, f) + } +} + +/// Rather than displaying as this: +/// +/// Uppercase("Foo") +/// +/// ...instead display as this: +/// +/// 'Foo' +impl fmt::Debug for Uppercase { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "'{}'", self.0) + } +} + +impl fmt::Display for Uppercase { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fmt::Display::fmt(&self.0, f) + } +} diff --git a/crates/compiler/module/src/lib.rs b/crates/compiler/module/src/lib.rs new file mode 100644 index 0000000000..0014a4183d --- /dev/null +++ b/crates/compiler/module/src/lib.rs @@ -0,0 +1,11 @@ +//! Implements data structures used for efficiently representing unique modules +//! and identifiers in Roc programs. +#![warn(clippy::dbg_macro)] +// See github.com/roc-lang/roc/issues/800 for discussion of the large_enum_variant check. +#![allow(clippy::large_enum_variant, clippy::upper_case_acronyms)] + +pub mod called_via; +pub mod ident; +pub mod low_level; +pub mod module_err; +pub mod symbol; diff --git a/crates/compiler/module/src/low_level.rs b/crates/compiler/module/src/low_level.rs new file mode 100644 index 0000000000..ae81591b17 --- /dev/null +++ b/crates/compiler/module/src/low_level.rs @@ -0,0 +1,365 @@ +use crate::symbol::Symbol; + +/// Low-level operations that get translated directly into e.g. LLVM instructions. +/// These are always wrapped when exposed to end users, and can only make it +/// into an Expr when added directly by can::builtins +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum LowLevel { + StrConcat, + StrJoinWith, + StrIsEmpty, + StrStartsWith, + StrStartsWithScalar, + StrEndsWith, + StrSplit, + StrCountGraphemes, + StrCountUtf8Bytes, + StrFromInt, + StrFromUtf8Range, + StrToUtf8, + StrRepeat, + StrFromFloat, + StrTrim, + StrTrimStart, + StrTrimEnd, + StrToNum, + StrToScalars, + StrGetUnsafe, + StrSubstringUnsafe, + StrReserve, + StrAppendScalar, + StrGetScalarUnsafe, + StrGetCapacity, + StrWithCapacity, + StrGraphemes, + StrReleaseExcessCapacity, + ListLen, + ListWithCapacity, + ListReserve, + ListReleaseExcessCapacity, + ListAppendUnsafe, + ListGetUnsafe, + ListReplaceUnsafe, + ListConcat, + ListPrepend, + ListMap, + ListMap2, + ListMap3, + ListMap4, + ListSortWith, + ListSublist, + ListDropAt, + ListSwap, + ListIsUnique, + ListGetCapacity, + NumAdd, + NumAddWrap, + NumAddChecked, + NumAddSaturated, + NumSub, + NumSubWrap, + NumSubChecked, + NumSubSaturated, + NumMul, + NumMulWrap, + NumMulSaturated, + NumMulChecked, + NumGt, + NumGte, + NumLt, + NumLte, + NumCompare, + NumDivFrac, + NumDivTruncUnchecked, + NumDivCeilUnchecked, + NumRemUnchecked, + NumIsMultipleOf, + NumAbs, + NumNeg, + NumSin, + NumCos, + NumTan, + NumSqrtUnchecked, + NumLogUnchecked, + NumRound, + NumToFrac, + NumPow, + NumCeiling, + NumPowInt, + NumFloor, + NumIsNan, + NumIsInfinite, + NumIsFinite, + NumAtan, + NumAcos, + NumAsin, + NumBytesToU16, + NumBytesToU32, + NumBytesToU64, + NumBytesToU128, + NumBitwiseAnd, + NumBitwiseXor, + NumBitwiseOr, + NumShiftLeftBy, + NumShiftRightBy, + NumShiftRightZfBy, + NumIntCast, + NumToFloatCast, + NumToIntChecked, + NumToFloatChecked, + NumToStr, + NumCountLeadingZeroBits, + NumCountTrailingZeroBits, + NumCountOneBits, + I128OfDec, + Eq, + NotEq, + And, + Or, + Not, + Hash, + PtrCast, + PtrStore, + PtrLoad, + PtrClearTagId, + RefCountIncRcPtr, + RefCountDecRcPtr, + RefCountIncDataPtr, + RefCountDecDataPtr, + RefCountIsUnique, + BoxExpr, + UnboxExpr, + Unreachable, + DictPseudoSeed, + SetJmp, + LongJmp, + SetLongJmpBuffer, +} + +macro_rules! higher_order { + () => { + ListMap | ListMap2 | ListMap3 | ListMap4 | ListSortWith + }; +} + +impl LowLevel { + /// is one of the arguments always a function? + /// An example is List.map. + pub fn is_higher_order(&self) -> bool { + use LowLevel::*; + + matches!(self, higher_order!()) + } + + pub fn function_argument_position(&self) -> usize { + use LowLevel::*; + + match self { + ListMap => 1, + ListMap2 => 2, + ListMap3 => 3, + ListMap4 => 4, + ListSortWith => 1, + _ => unreachable!(), + } + } +} + +/// Some wrapper functions can just be replaced by lowlevels in the backend for performance. +/// For example, Num.add should be an instruction, not a function call. +/// Variant names are chosen to help explain what to do when adding new lowlevels +#[derive(PartialEq, Eq)] +pub enum LowLevelWrapperType { + /// This wrapper function contains no logic and we can remove it in code gen + CanBeReplacedBy(LowLevel), + NotALowLevelWrapper, +} + +impl LowLevelWrapperType { + pub fn from_symbol(symbol: Symbol) -> LowLevelWrapperType { + for_symbol_help(symbol) + } +} + +/// We use a rust macro to ensure that every LowLevel gets handled +macro_rules! map_symbol_to_lowlevel { + ($($lowlevel:ident <= $symbol:ident),* $(,)?) => { + + fn for_symbol_help(symbol: Symbol) -> LowLevelWrapperType { + use $crate::low_level::LowLevelWrapperType::*; + + // expands to a big (but non-exhaustive) match on symbols and maps them to a lowlevel + match symbol { + $( + Symbol::$symbol => CanBeReplacedBy(LowLevel::$lowlevel), + )* + + _ => NotALowLevelWrapper, + } + } + + fn _enforce_exhaustiveness(lowlevel: LowLevel) -> Symbol { + // when adding a new lowlevel, this match will stop being exhaustive, and give a + // compiler error. Most likely, you are adding a new lowlevel that maps directly to a + // symbol. For instance, you want to have `List.foo` to stand for the `ListFoo` + // lowlevel. In that case, see below in the invocation of `map_symbol_to_lowlevel!` + // + // Below, we explicitly handle some exceptions to the pattern where a lowlevel maps + // directly to a symbol. If you are unsure if your lowlevel is an exception, assume + // that it isn't and just see if that works. + match lowlevel { + $( + LowLevel::$lowlevel => Symbol::$symbol, + )* + + // these are higher-order lowlevels. these need the surrounding + // function to provide enough type information for code generation + LowLevel::ListMap => unreachable!(), + LowLevel::ListMap2 => unreachable!(), + LowLevel::ListMap3 => unreachable!(), + LowLevel::ListMap4 => unreachable!(), + LowLevel::ListSortWith => unreachable!(), + + // (un)boxing is handled in a custom way + LowLevel::BoxExpr => unreachable!(), + LowLevel::UnboxExpr => unreachable!(), + + // these functions return polymorphic values + LowLevel::NumIntCast => unreachable!(), + LowLevel::NumToFloatCast => unreachable!(), + LowLevel::NumToIntChecked => unreachable!(), + LowLevel::NumToFloatChecked => unreachable!(), + + + // these are used internally and not tied to a symbol + LowLevel::Hash => unimplemented!(), + LowLevel::PtrCast => unimplemented!(), + LowLevel::PtrStore => unimplemented!(), + LowLevel::PtrLoad => unimplemented!(), + LowLevel::PtrClearTagId => unimplemented!(), + LowLevel::RefCountIncRcPtr => unimplemented!(), + LowLevel::RefCountDecRcPtr=> unimplemented!(), + LowLevel::RefCountIncDataPtr => unimplemented!(), + LowLevel::RefCountDecDataPtr=> unimplemented!(), + LowLevel::RefCountIsUnique => unimplemented!(), + + LowLevel::SetJmp => unimplemented!(), + LowLevel::LongJmp => unimplemented!(), + LowLevel::SetLongJmpBuffer => unimplemented!(), + + // these are not implemented, not sure why + LowLevel::StrFromInt => unimplemented!(), + LowLevel::StrFromFloat => unimplemented!(), + } + } + }; +} + +// here is where we actually specify the mapping for the fast majority of cases that follow the +// pattern of a symbol mapping directly to a lowlevel. In other words, most lowlevels (left) are generated +// by only one specific symbol (right) +map_symbol_to_lowlevel! { + StrConcat <= STR_CONCAT, + StrJoinWith <= STR_JOIN_WITH, + StrIsEmpty <= STR_IS_EMPTY, + StrStartsWith <= STR_STARTS_WITH, + StrStartsWithScalar <= STR_STARTS_WITH_SCALAR, + StrEndsWith <= STR_ENDS_WITH, + StrSplit <= STR_SPLIT, + StrCountGraphemes <= STR_COUNT_GRAPHEMES, + StrCountUtf8Bytes <= STR_COUNT_UTF8_BYTES, + StrFromUtf8Range <= STR_FROM_UTF8_RANGE_LOWLEVEL, + StrToUtf8 <= STR_TO_UTF8, + StrRepeat <= STR_REPEAT, + StrTrim <= STR_TRIM, + StrTrimStart <= STR_TRIM_START, + StrTrimEnd <= STR_TRIM_END, + StrToScalars <= STR_TO_SCALARS, + StrGetUnsafe <= STR_GET_UNSAFE, + StrSubstringUnsafe <= STR_SUBSTRING_UNSAFE, + StrReserve <= STR_RESERVE, + StrAppendScalar <= STR_APPEND_SCALAR_UNSAFE, + StrGetScalarUnsafe <= STR_GET_SCALAR_UNSAFE, + StrToNum <= STR_TO_NUM, + StrGetCapacity <= STR_CAPACITY, + StrWithCapacity <= STR_WITH_CAPACITY, + StrGraphemes <= STR_GRAPHEMES, + StrReleaseExcessCapacity <= STR_RELEASE_EXCESS_CAPACITY, + ListLen <= LIST_LEN, + ListGetCapacity <= LIST_CAPACITY, + ListWithCapacity <= LIST_WITH_CAPACITY, + ListReserve <= LIST_RESERVE, + ListReleaseExcessCapacity <= LIST_RELEASE_EXCESS_CAPACITY, + ListIsUnique <= LIST_IS_UNIQUE, + ListAppendUnsafe <= LIST_APPEND_UNSAFE, + ListPrepend <= LIST_PREPEND, + ListGetUnsafe <= LIST_GET_UNSAFE, + ListReplaceUnsafe <= LIST_REPLACE_UNSAFE, + ListConcat <= LIST_CONCAT, + ListSublist <= LIST_SUBLIST_LOWLEVEL, + ListDropAt <= LIST_DROP_AT, + ListSwap <= LIST_SWAP, + NumAdd <= NUM_ADD, + NumAddWrap <= NUM_ADD_WRAP, + NumAddChecked <= NUM_ADD_CHECKED_LOWLEVEL, + NumAddSaturated <= NUM_ADD_SATURATED, + NumSub <= NUM_SUB, + NumSubWrap <= NUM_SUB_WRAP, + NumSubChecked <= NUM_SUB_CHECKED_LOWLEVEL, + NumSubSaturated <= NUM_SUB_SATURATED, + NumMul <= NUM_MUL, + NumMulWrap <= NUM_MUL_WRAP, + NumMulSaturated <= NUM_MUL_SATURATED, + NumMulChecked <= NUM_MUL_CHECKED_LOWLEVEL, + NumGt <= NUM_GT, + NumGte <= NUM_GTE, + NumLt <= NUM_LT, + NumLte <= NUM_LTE, + NumCompare <= NUM_COMPARE, + NumDivFrac <= NUM_DIV_FRAC, + NumDivCeilUnchecked <= NUM_DIV_CEIL, + NumDivTruncUnchecked <= NUM_DIV_TRUNC, + NumRemUnchecked <= NUM_REM, + NumIsMultipleOf <= NUM_IS_MULTIPLE_OF, + NumAbs <= NUM_ABS, + NumNeg <= NUM_NEG, + NumSin <= NUM_SIN, + NumCos <= NUM_COS, + NumTan <= NUM_TAN, + NumSqrtUnchecked <= NUM_SQRT, + NumLogUnchecked <= NUM_LOG, + NumRound <= NUM_ROUND, + NumToFrac <= NUM_TO_FRAC, + NumIsNan <= NUM_IS_NAN, + NumIsInfinite <= NUM_IS_INFINITE, + NumIsFinite <= NUM_IS_FINITE, + NumPow <= NUM_POW, + NumCeiling <= NUM_CEILING, + NumPowInt <= NUM_POW_INT, + NumFloor <= NUM_FLOOR, + NumAtan <= NUM_ATAN, + NumAcos <= NUM_ACOS, + NumAsin <= NUM_ASIN, + NumBytesToU16 <= NUM_BYTES_TO_U16_LOWLEVEL, + NumBytesToU32 <= NUM_BYTES_TO_U32_LOWLEVEL, + NumBytesToU64 <= NUM_BYTES_TO_U64_LOWLEVEL, + NumBytesToU128 <= NUM_BYTES_TO_U128_LOWLEVEL, + NumBitwiseAnd <= NUM_BITWISE_AND, + NumBitwiseXor <= NUM_BITWISE_XOR, + NumBitwiseOr <= NUM_BITWISE_OR, + NumShiftLeftBy <= NUM_SHIFT_LEFT, + NumShiftRightBy <= NUM_SHIFT_RIGHT, + NumShiftRightZfBy <= NUM_SHIFT_RIGHT_ZERO_FILL, + NumToStr <= NUM_TO_STR, + NumCountLeadingZeroBits <= NUM_COUNT_LEADING_ZERO_BITS, + NumCountTrailingZeroBits <= NUM_COUNT_TRAILING_ZERO_BITS, + NumCountOneBits <= NUM_COUNT_ONE_BITS, + I128OfDec <= I128_OF_DEC, + Eq <= BOOL_STRUCTURAL_EQ, + NotEq <= BOOL_STRUCTURAL_NOT_EQ, + And <= BOOL_AND, + Or <= BOOL_OR, + Not <= BOOL_NOT, + Unreachable <= LIST_UNREACHABLE, + DictPseudoSeed <= DICT_PSEUDO_SEED, +} diff --git a/compiler/module/src/module_err.rs b/crates/compiler/module/src/module_err.rs similarity index 100% rename from compiler/module/src/module_err.rs rename to crates/compiler/module/src/module_err.rs diff --git a/crates/compiler/module/src/symbol.rs b/crates/compiler/module/src/symbol.rs new file mode 100644 index 0000000000..7fee728ad4 --- /dev/null +++ b/crates/compiler/module/src/symbol.rs @@ -0,0 +1,1648 @@ +use crate::ident::{Ident, ModuleName}; +use crate::module_err::{IdentIdNotFoundSnafu, ModuleIdNotFoundSnafu, ModuleResult}; +use roc_collections::{SmallStringInterner, VecMap}; +use roc_error_macros::internal_error; +use roc_ident::IdentStr; +use roc_region::all::Region; +use snafu::OptionExt; +use std::num::NonZeroU32; +use std::{fmt, u32}; + +// the packed(4) is needed for faster equality comparisons. With it, the structure is +// treated as a single u64, and comparison is one instruction +// +// example::eq_sym64: +// cmp rdi, rsi +// sete al +// ret +// +// while without it we get 2 extra instructions +// +// example::eq_sym64: +// xor edi, edx +// xor esi, ecx +// or esi, edi +// sete al +// ret +// +// #[repr(packed)] gives you #[repr(packed(1))], and then all your reads are unaligned +// so we set the alignment to (the natural) 4 +#[derive(Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] +#[repr(packed(4))] +pub struct Symbol { + ident_id: u32, + module_id: NonZeroU32, +} + +/// An Option will use the 0 that is not used by the NonZeroU32 module_id field to encode +/// the Nothing case. An Option hence takes no more space than a Symbol. +#[allow(dead_code)] +const SYMBOL_HAS_NICHE: () = + assert!(std::mem::size_of::() == std::mem::size_of::>()); + +// When this is `true` (which it normally should be), Symbol's Debug::fmt implementation +// attempts to pretty print debug symbols using interns recorded using +// register_debug_idents calls (which should be made in debug mode). +// Set it to false if you want to see the raw ModuleId and IdentId ints, +// but please set it back to true before checking in the result! +#[cfg(any(debug_assertions, feature = "debug-symbols"))] +const PRETTY_PRINT_DEBUG_SYMBOLS: bool = true; + +pub const DERIVABLE_ABILITIES: &[(Symbol, &[Symbol])] = &[ + (Symbol::ENCODE_ENCODING, &[Symbol::ENCODE_TO_ENCODER]), + (Symbol::DECODE_DECODING, &[Symbol::DECODE_DECODER]), + (Symbol::HASH_HASH_ABILITY, &[Symbol::HASH_HASH]), + (Symbol::BOOL_EQ, &[Symbol::BOOL_IS_EQ]), + ( + Symbol::INSPECT_INSPECT_ABILITY, + &[Symbol::INSPECT_TO_INSPECTOR], + ), +]; + +/// In Debug builds only, Symbol has a name() method that lets +/// you look up its name in a global intern table. This table is +/// behind a mutex, so it is neither populated nor available in release builds. +impl Symbol { + // NOTE: the define_builtins! macro adds a bunch of constants to this impl, + // + // e.g. pub const NUM_NUM: Symbol = … + + pub const fn new(module_id: ModuleId, ident_id: IdentId) -> Symbol { + // The bit layout of the inside of a Symbol is: + // + // |------ 32 bits -----|------ 32 bits -----| + // | ident_id | module_id | + // |--------------------|--------------------| + // + // module_id comes second because we need to query it more often, + // and this way we can get it by truncating the u64 to u32, + // whereas accessing the first slot requires a bit shift first. + + Self { + module_id: module_id.0, + ident_id: ident_id.0, + } + } + + pub const fn module_id(self) -> ModuleId { + ModuleId(self.module_id) + } + + pub const fn ident_id(self) -> IdentId { + IdentId(self.ident_id) + } + + pub const fn is_builtin(self) -> bool { + self.module_id().is_builtin() + } + + pub fn is_derivable_ability(self) -> bool { + self.derivable_ability().is_some() + } + + pub fn derivable_ability(self) -> Option<&'static (Symbol, &'static [Symbol])> { + DERIVABLE_ABILITIES.iter().find(|(name, _)| *name == self) + } + + /// A symbol that should never be exposed to userspace, but needs to be exposed + /// to compiled modules for deriving abilities for structural types. + pub fn is_exposed_for_builtin_derivers(&self) -> bool { + matches!( + self, + // The `structuralEq` call used deriving structural equality, which will wrap the `Eq` + // low-level implementation. + &Self::BOOL_STRUCTURAL_EQ + ) + } + + pub fn module_string<'a>(&self, interns: &'a Interns) -> &'a ModuleName { + interns + .module_ids + .get_name(self.module_id()) + .unwrap_or_else(|| { + internal_error!( + "module_string could not find IdentIds for module {:?} in {:?}", + self.module_id(), + interns + ) + }) + } + + pub fn as_str(self, interns: &Interns) -> &str { + let ident_ids = interns + .all_ident_ids + .get(&self.module_id()) + .unwrap_or_else(|| { + internal_error!( + "ident_string could not find IdentIds for module {:?} in {:?}", + self.module_id(), + interns + ) + }); + + ident_ids.get_name(self.ident_id()).unwrap_or_else(|| { + internal_error!( + "ident_string's IdentIds did not contain an entry for {} in module {:?}", + self.ident_id().0, + self.module_id() + ) + }) + } + + pub const fn as_u64(self) -> u64 { + u64::from_ne_bytes(self.to_ne_bytes()) + } + + pub fn fully_qualified(self, interns: &Interns, home: ModuleId) -> ModuleName { + let module_id = self.module_id(); + + if module_id == home { + ModuleName::from(self.as_str(interns)) + } else { + // TODO do this without format! to avoid allocation for short strings + format!( + "{}.{}", + self.module_string(interns).as_str(), + self.as_str(interns) + ) + .into() + } + } + + pub const fn to_ne_bytes(self) -> [u8; 8] { + // repr(packed(4)) is repr(c), and with the fields as defined will not having padding. + unsafe { std::mem::transmute(self) } + } + + #[cfg(debug_assertions)] + pub fn contains(self, needle: &str) -> bool { + format!("{self:?}").contains(needle) + } +} + +/// Rather than displaying as this: +/// +/// Symbol("Foo.bar") +/// +/// ...instead display as this: +/// +/// `Foo.bar` +impl fmt::Debug for Symbol { + #[cfg(any(debug_assertions, feature = "debug-symbols"))] + #[allow(clippy::print_in_format_impl)] + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + if PRETTY_PRINT_DEBUG_SYMBOLS { + let module_id = self.module_id(); + let ident_id = self.ident_id(); + + match DEBUG_IDENT_IDS_BY_MODULE_ID.lock() { + Ok(names) => match &names.get(&(module_id.to_zero_indexed() as u32)) { + Some(ident_ids) => match ident_ids.get_name(ident_id) { + Some(ident_str) => write!(f, "`{module_id:?}.{ident_str}`"), + None => fallback_debug_fmt(*self, f), + }, + None => fallback_debug_fmt(*self, f), + }, + Err(err) => { + // Print and return Err rather than panicking, because this + // might be used in a panic error message, and if we panick + // while we're already panicking it'll kill the process + // without printing any of the errors! + use std::io::Write; + + let mut stderr = std::io::stderr(); + writeln!(stderr, "DEBUG INFO: Failed to acquire lock for Debug reading from DEBUG_IDENT_IDS_BY_MODULE_ID, presumably because a thread panicked: {err:?}").unwrap(); + + fallback_debug_fmt(*self, f) + } + } + } else { + fallback_debug_fmt(*self, f) + } + } + + #[cfg(not(any(debug_assertions, feature = "debug-symbols")))] + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fallback_debug_fmt(*self, f) + } +} + +impl fmt::Display for Symbol { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let module_id = self.module_id(); + let ident_id = self.ident_id(); + + match ident_id { + IdentId(value) => write!(f, "{module_id:?}.{value:?}"), + } + } +} + +impl From for u64 { + fn from(symbol: Symbol) -> Self { + symbol.as_u64() + } +} + +fn fallback_debug_fmt(symbol: Symbol, f: &mut fmt::Formatter) -> fmt::Result { + let module_id = symbol.module_id(); + let ident_id = symbol.ident_id(); + + write!(f, "`{module_id:?}.{ident_id:?}`") +} + +/// This is used in Debug builds only, to let us have a Debug instance +/// which displays not only the Module ID, but also the Module Name which +/// corresponds to that ID. +/// +#[cfg(any(debug_assertions, feature = "debug-symbols"))] +static DEBUG_MODULE_ID_NAMES: std::sync::Mutex = + std::sync::Mutex::new(roc_collections::SmallStringInterner::new()); + +#[derive(Debug, Default, Clone)] +pub struct Interns { + pub module_ids: ModuleIds, + pub all_ident_ids: IdentIdsByModule, +} + +impl Interns { + pub fn module_id(&mut self, name: &ModuleName) -> ModuleId { + self.module_ids.get_or_insert(name) + } + + pub fn module_name(&self, module_id: ModuleId) -> &ModuleName { + self.module_ids.get_name(module_id).unwrap_or_else(|| { + internal_error!( + "Unable to find interns entry for module_id {:?} in Interns {:?}", + module_id, + self + ) + }) + } + + pub fn symbol(&self, module_id: ModuleId, ident: IdentStr) -> Symbol { + let ident: Ident = ident.into(); + + match self.all_ident_ids.get(&module_id) { + Some(ident_ids) => match ident_ids.get_id(ident.as_str()) { + Some(ident_id) => Symbol::new(module_id, ident_id), + None => { + internal_error!( + "Interns::symbol could not find ident entry for {:?} for module {:?}", + ident, + module_id + ); + } + }, + None => { + internal_error!( + "Interns::symbol could not find entry for module {:?} in Interns {:?}", + module_id, + self + ); + } + } + } + + pub fn from_index(module_id: ModuleId, ident_id: u32) -> Symbol { + Symbol::new(module_id, IdentId(ident_id)) + } +} + +pub fn get_module_ident_ids<'a>( + all_ident_ids: &'a IdentIdsByModule, + module_id: &ModuleId, +) -> ModuleResult<&'a IdentIds> { + all_ident_ids + .get(module_id) + .with_context(|| ModuleIdNotFoundSnafu { + module_id: format!("{module_id:?}"), + all_ident_ids: format!("{all_ident_ids:?}"), + }) +} + +pub fn get_module_ident_ids_mut<'a>( + all_ident_ids: &'a mut IdentIdsByModule, + module_id: &ModuleId, +) -> ModuleResult<&'a mut IdentIds> { + all_ident_ids + .get_mut(module_id) + .with_context(|| ModuleIdNotFoundSnafu { + module_id: format!("{module_id:?}"), + all_ident_ids: "I could not return all_ident_ids here because of borrowing issues.", + }) +} + +/// This is used in Debug builds only, to let us have a Debug instance +/// which displays not only the Module ID, but also the Module Name which +/// corresponds to that ID. +#[cfg(any(debug_assertions, feature = "debug-symbols"))] +static DEBUG_IDENT_IDS_BY_MODULE_ID: std::sync::Mutex> = + // This stores a u32 key instead of a ModuleId key so that if there's + // a problem with ModuleId's Debug implementation, logging this for diagnostic + // purposes won't recursively trigger ModuleId's Debug instance in the course of printing + // this out. + std::sync::Mutex::new(roc_collections::VecMap::new()); + +/// A globally unique ID that gets assigned to each module as it is loaded. +#[derive(Copy, Clone, PartialEq, Eq, Hash)] +pub struct ModuleId(NonZeroU32); + +impl ModuleId { + // NOTE: the define_builtins! macro adds a bunch of constants to this impl, + // + // e.g. pub const NUM: ModuleId = … + + const fn from_zero_indexed(mut id: usize) -> Self { + id += 1; + + // only happens on overflow + debug_assert!(id != 0); + + ModuleId(unsafe { NonZeroU32::new_unchecked(id as u32) }) + } + + const fn to_zero_indexed(self) -> usize { + (self.0.get() - 1) as usize + } + + #[cfg(any(debug_assertions, feature = "debug-symbols"))] + pub fn register_debug_idents(self, ident_ids: &IdentIds) { + let mut all = DEBUG_IDENT_IDS_BY_MODULE_ID.lock().expect("Failed to acquire lock for Debug interning into DEBUG_MODULE_ID_NAMES, presumably because a thread panicked."); + + all.insert(self.to_zero_indexed() as u32, ident_ids.clone()); + } + + #[cfg(not(any(debug_assertions, feature = "debug-symbols")))] + pub fn register_debug_idents(self, _ident_ids: &IdentIds) { + // This is a no-op that should get DCE'd + } + + pub fn to_ident_str(self, interns: &Interns) -> &ModuleName { + interns + .module_ids + .get_name(self) + .unwrap_or_else(|| internal_error!("Could not find ModuleIds for {:?}", self)) + } +} + +impl fmt::Debug for ModuleId { + /// In debug builds, whenever we create a new ModuleId, we record is name in + /// a global interning table so that Debug can look it up later. That table + /// needs a global mutex, so we don't do this in release builds. This means + /// the Debug impl in release builds only shows the number, not the name (which + /// it does not have available, due to having never stored it in the mutexed intern table.) + #[cfg(any(debug_assertions, feature = "debug-symbols"))] + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + // Originally, this printed both name and numeric ID, but the numeric ID + // didn't seem to add anything useful. Feel free to temporarily re-add it + // if it's helpful in debugging! + let names = + DEBUG_MODULE_ID_NAMES + .lock() + .expect("Failed to acquire lock for Debug reading from DEBUG_MODULE_ID_NAMES, presumably because a thread panicked."); + + if PRETTY_PRINT_DEBUG_SYMBOLS { + match names.try_get(self.to_zero_indexed()) { + Some(str_ref) => write!(f, "{str_ref}"), + None => { + internal_error!( + "Could not find a Debug name for module ID {} in {:?}", + self.0, + names, + ); + } + } + } else { + write!(f, "{}", self.0) + } + } + + /// In release builds, all we have access to is the number, so only display that. + #[cfg(not(any(debug_assertions, feature = "debug-symbols")))] + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + self.0.fmt(f) + } +} + +/// pf.Task +/// 1. build mapping from short name to package +/// 2. when adding new modules from package we need to register them in some other map (this module id goes with short name) (shortname, module-name) -> moduleId +/// 3. pass this around to other modules getting headers parsed. when parsing interfaces we need to use this map to reference shortnames +/// 4. throw away short names. stash the module id in the can env under the resolved module name +/// 5. test: + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum PackageQualified<'a, T> { + Unqualified(T), + Qualified(&'a str, T), +} + +/// Package-qualified module name +pub type PQModuleName<'a> = PackageQualified<'a, ModuleName>; + +impl<'a, T> PackageQualified<'a, T> { + pub fn as_inner(&self) -> &T { + match self { + PackageQualified::Unqualified(name) => name, + PackageQualified::Qualified(_, name) => name, + } + } +} + +#[derive(Debug, Clone)] +pub struct PackageModuleIds<'a> { + by_id: Vec>, +} + +impl<'a> PackageModuleIds<'a> { + pub fn get_or_insert(&mut self, module_name: &PQModuleName<'a>) -> ModuleId { + if let Some(module_id) = self.get_id(module_name) { + return module_id; + } + + // didn't find it, so we'll add it + let module_id = ModuleId::from_zero_indexed(self.by_id.len()); + self.by_id.push(module_name.clone()); + if cfg!(any(debug_assertions, feature = "debug-symbols")) { + Self::insert_debug_name(module_id, module_name); + } + + module_id + } + + pub fn into_module_ids(self) -> ModuleIds { + let by_id: Vec = self + .by_id + .into_iter() + .map(|pqname| pqname.as_inner().clone()) + .collect(); + + ModuleIds { by_id } + } + + #[cfg(any(debug_assertions, feature = "debug-symbols"))] + fn insert_debug_name(module_id: ModuleId, module_name: &PQModuleName) { + let mut names = DEBUG_MODULE_ID_NAMES.lock().expect("Failed to acquire lock for Debug interning into DEBUG_MODULE_ID_NAMES, presumably because a thread panicked."); + + if names.try_get(module_id.to_zero_indexed()).is_none() { + match module_name { + PQModuleName::Unqualified(module) => { + names.insert(module.as_str()); + } + PQModuleName::Qualified(package, module) => { + names.insert(&format!("{}.{}", package, module.as_str())); + } + } + } + } + + #[cfg(not(any(debug_assertions, feature = "debug-symbols")))] + fn insert_debug_name(_module_id: ModuleId, _module_name: &PQModuleName) { + // By design, this is a no-op in release builds! + } + + pub fn get_id(&self, module_name: &PQModuleName<'a>) -> Option { + for (index, name) in self.by_id.iter().enumerate() { + if name == module_name { + return Some(ModuleId::from_zero_indexed(index)); + } + } + + None + } + + pub fn get_name(&self, id: ModuleId) -> Option<&PQModuleName> { + self.by_id.get(id.to_zero_indexed()) + } + + pub fn available_modules(&self) -> impl Iterator { + self.by_id.iter() + } + + /// Returns true iff two modules belong to the same package. + /// Returns [None] if one module is unknown. + pub fn package_eq(&self, left: ModuleId, right: ModuleId) -> Option { + if left.is_builtin() ^ right.is_builtin() { + return Some(false); + } + let result = match (self.get_name(left)?, self.get_name(right)?) { + (PQModuleName::Unqualified(_), PQModuleName::Unqualified(_)) => true, + (PQModuleName::Qualified(pkg1, _), PQModuleName::Qualified(pkg2, _)) => pkg1 == pkg2, + _ => false, + }; + Some(result) + } +} + +/// Stores a mapping between ModuleId and InlinableString. +#[derive(Debug, Clone)] +pub struct ModuleIds { + /// Each ModuleId is an index into this Vec + by_id: Vec, +} + +impl ModuleIds { + pub fn get_or_insert(&mut self, module_name: &ModuleName) -> ModuleId { + if let Some(module_id) = self.get_id(module_name) { + return module_id; + } + + // didn't find it, so we'll add it + let module_id = ModuleId::from_zero_indexed(self.by_id.len()); + self.by_id.push(module_name.clone()); + if cfg!(any(debug_assertions, feature = "debug-symbols")) { + Self::insert_debug_name(module_id, module_name); + } + + module_id + } + + #[cfg(any(debug_assertions, feature = "debug-symbols"))] + fn insert_debug_name(module_id: ModuleId, module_name: &ModuleName) { + let mut names = DEBUG_MODULE_ID_NAMES.lock().expect("Failed to acquire lock for Debug interning into DEBUG_MODULE_ID_NAMES, presumably because a thread panicked."); + + // TODO make sure modules are never added more than once! + if names.try_get(module_id.to_zero_indexed()).is_none() { + names.insert(module_name.as_str()); + } + } + + #[cfg(not(any(debug_assertions, feature = "debug-symbols")))] + fn insert_debug_name(_module_id: ModuleId, _module_name: &ModuleName) { + // By design, this is a no-op in release builds! + } + + #[inline] + pub fn get_id(&self, module_name: &ModuleName) -> Option { + for (index, name) in self.by_id.iter().enumerate() { + if name == module_name { + return Some(ModuleId::from_zero_indexed(index)); + } + } + + None + } + + pub fn get_name(&self, id: ModuleId) -> Option<&ModuleName> { + self.by_id.get(id.to_zero_indexed()) + } + + pub fn available_modules(&self) -> impl Iterator { + self.by_id.iter() + } +} + +/// An ID that is assigned to interned string identifiers within a module. +/// By turning these strings into numbers, post-canonicalization processes +/// like unification and optimization can run a lot faster. +/// +/// This ID is unique within a given module, not globally - so to turn this back into +/// a string, you would need a ModuleId, an IdentId, and a Map>. +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] +pub struct IdentId(u32); + +impl IdentId { + pub const fn index(self) -> usize { + self.0 as usize + } + + /// # Safety + /// + /// The index is not guaranteed to know to exist. + pub unsafe fn from_index(index: u32) -> Self { + Self(index) + } +} + +/// Stores a mapping between Ident and IdentId. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct IdentIds { + pub interner: SmallStringInterner, +} + +impl IdentIds { + pub fn ident_strs(&self) -> impl Iterator { + self.interner + .iter() + .enumerate() + .map(|(index, ident)| (IdentId(index as u32), ident)) + } + + pub fn add_str(&mut self, ident_name: &str) -> IdentId { + IdentId(self.interner.insert(ident_name) as u32) + } + + pub fn duplicate_ident(&mut self, ident_id: IdentId) -> IdentId { + IdentId(self.interner.duplicate(ident_id.0 as usize) as u32) + } + + pub fn get_or_insert(&mut self, name: &str) -> IdentId { + match self.get_id(name) { + Some(id) => id, + None => self.add_str(name), + } + } + + // necessary when the name of a value is changed in the editor + // TODO fix when same ident_name is present multiple times, see issue #2548 + pub fn update_key(&mut self, old_name: &str, new_name: &str) -> Result { + match self.interner.find_and_update(old_name, new_name) { + Some(index) => Ok(IdentId(index as u32)), + None => Err(format!("The identifier {old_name:?} is not in IdentIds")), + } + } + + /// Generates a unique, new name that's just a strigified integer + /// (e.g. "1" or "5"), using an internal counter. Since valid Roc variable + /// names cannot begin with a number, this has no chance of colliding + /// with actual user-defined variables. + /// + /// This is used, for example, during canonicalization of an Expr::Closure + /// to generate a unique symbol to refer to that closure. + pub fn gen_unique(&mut self) -> IdentId { + IdentId(self.interner.insert_index_str() as u32) + } + + #[inline(always)] + pub fn get_id(&self, ident_name: &str) -> Option { + self.interner + .find_index(ident_name) + .map(|i| IdentId(i as u32)) + } + + #[inline(always)] + pub fn get_id_many<'a>(&'a self, ident_name: &'a str) -> impl Iterator + 'a { + self.interner + .find_indices(ident_name) + .map(|i| IdentId(i as u32)) + } + + pub fn get_name(&self, id: IdentId) -> Option<&str> { + self.interner.try_get(id.0 as usize) + } + + pub fn get_name_str_res(&self, ident_id: IdentId) -> ModuleResult<&str> { + self.get_name(ident_id) + .with_context(|| IdentIdNotFoundSnafu { + ident_id, + ident_ids_str: format!("{self:?}"), + }) + } + + pub fn len(&self) -> usize { + self.interner.len() + } + + pub fn is_empty(&self) -> bool { + self.interner.is_empty() + } +} + +#[derive(Debug, Default, Clone)] +pub struct IdentIdsByModule(VecMap); + +impl IdentIdsByModule { + pub fn get_or_insert(&mut self, module_id: ModuleId) -> &mut IdentIds { + self.0.get_or_insert(module_id, IdentIds::default) + } + + pub fn get_mut(&mut self, key: &ModuleId) -> Option<&mut IdentIds> { + self.0.get_mut(key) + } + + pub fn get(&self, key: &ModuleId) -> Option<&IdentIds> { + self.0.get(key) + } + + pub fn insert(&mut self, key: ModuleId, value: IdentIds) -> Option { + self.0.insert(key, value) + } + + pub fn keys(&self) -> impl Iterator { + self.0.keys() + } + + pub fn len(&self) -> usize { + self.0.len() + } + + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } +} + +// BUILTINS + +const fn offset_helper(mut array: [u32; N]) -> [u32; N] { + let mut sum = 0u32; + + let mut i = 0; + while i < N { + // In rust 1.60 change to: (array[i], sum) = (sum, sum + array[i]); + let temp = array[i]; + array[i] = sum; + sum += temp; + + i += 1; + } + + array +} + +const fn byte_slice_equality(a: &[u8], b: &[u8]) -> bool { + if a.len() != b.len() { + return false; + } + + let mut i = 0; + while i < a.len() { + if a[i] != b[i] { + return false; + } + + i += 1; + } + + true +} + +const fn find_duplicates(array: [&str; N]) -> Option<(usize, usize)> { + let mut i = 0; + while i < N { + let needle = array[i]; + let mut j = i + 1; + while j < N { + if byte_slice_equality(needle.as_bytes(), array[j].as_bytes()) { + return Some((i, j)); + } + + j += 1; + } + + i += 1; + } + + None +} + +const fn check_indices(array: [u32; N]) -> Option<(u32, usize)> { + let mut i = 0; + while i < N { + if array[i] as usize != i { + return Some((array[i], i)); + } + + i += 1; + } + + None +} + +macro_rules! define_builtins { + { + $( + $module_id:literal $module_const:ident: $module_name:literal => { + $( + $(#[$ident_meta:meta])* + $ident_id:literal $ident_const:ident: $ident_name:literal + $(exposed_apply_type=$exposed_apply_type:literal)? + $(exposed_type=$exposed_type:literal)? + $(in_scope_for_hints=$in_scope_for_hints:literal)? + )* + $(unexposed $u_ident_id:literal $u_ident_const:ident: $u_ident_name:literal)* + } + )+ + num_modules: $total:literal + } => { + impl<'a> super::ident::QualifiedModuleName<'a> { + pub fn is_builtin(&self) -> bool { + self.opt_package.is_none() && ($($module_name == self.module.as_str() ||)+ false) + } + } + + impl IdentIds { + pub fn exposed_builtins(extra_capacity: usize) -> IdentIdsByModule { + let mut exposed_idents_by_module = VecMap::with_capacity(extra_capacity + $total); + + $( + let module_id = ModuleId::$module_const; + debug_assert!(!exposed_idents_by_module.contains_key(&module_id), r"Error setting up Builtins: when setting up module {} {:?} - the module ID {} is already present in the map. Check the map for duplicate module IDs!", $module_id, $module_name, $module_id); + + let ident_ids = { + const TOTAL : usize = (&[ $($ident_name),* ] as &[&str]).len(); + const NAMES : [ &str; TOTAL] = [ $($ident_name),* ]; + const LENGTHS: [ u16; TOTAL] = [ $($ident_name.len() as u16),* ]; + const OFFSETS: [ u32; TOTAL] = offset_helper([ $($ident_name.len() as u32),* ]); + const BUFFER: &str = concat!($($ident_name),*); + + const LENGTH_CHECK: Option<(u32, usize)> = check_indices([ $($ident_id),* ]); + const DUPLICATE_CHECK: Option<(usize, usize)> = find_duplicates(NAMES); + + if cfg!(debug_assertions) { + match LENGTH_CHECK { + None => (), + Some((given, expected)) => internal_error!( + "Symbol {} : {} should have index {} based on the insertion order, try {} : {} instead", + given, NAMES[expected], expected, expected, NAMES[expected], + ), + } + }; + + if cfg!(debug_assertions) { + match DUPLICATE_CHECK { + None => (), + Some((first, second)) => internal_error!( + "Symbol {} : {} is duplicated at position {}, try removing the duplicate", + first, NAMES[first], second + ), + } + }; + + // Safety: all lengths are non-negative and smaller than 2^15 + let interner = unsafe { + SmallStringInterner::from_parts ( + BUFFER.as_bytes().to_vec(), + LENGTHS.to_vec(), + OFFSETS.to_vec(), + )}; + + IdentIds{ interner } + }; + + if cfg!(any(debug_assertions, feature = "debug-symbols")) { + let name = PQModuleName::Unqualified($module_name.into()); + PackageModuleIds::insert_debug_name(module_id, &name); + module_id.register_debug_idents(&ident_ids); + } + + + exposed_idents_by_module.insert( + module_id, + ident_ids + ); + )+ + + debug_assert!(exposed_idents_by_module.len() == $total, "Error setting up Builtins: `total:` is set to the wrong amount. It was set to {} but {} modules were set up.", $total, exposed_idents_by_module.len()); + + IdentIdsByModule(exposed_idents_by_module) + } + } + + impl ModuleId { + pub const fn is_builtin(self) -> bool { + // This is a builtin ModuleId iff it's below the + // total number of builtin modules, since they + // take up the first $total ModuleId numbers. + self.to_zero_indexed() < $total + } + + $( + pub const $module_const: ModuleId = ModuleId::from_zero_indexed($module_id); + )+ + } + + impl Default for ModuleIds { + fn default() -> Self { + // +1 because the user will be compiling at least 1 non-builtin module! + let capacity = $total + 1; + + let mut by_id = Vec::with_capacity(capacity); + + let mut insert_both = |id: ModuleId, name_str: &'static str| { + let name: ModuleName = name_str.into(); + + if cfg!(any(debug_assertions, feature = "debug-symbols")) { + Self::insert_debug_name(id, &name); + } + + by_id.push(name); + }; + + $( + insert_both(ModuleId::$module_const, $module_name); + )+ + + ModuleIds { by_id } + } + } + + impl<'a> Default for PackageModuleIds<'a> { + fn default() -> Self { + // +1 because the user will be compiling at least 1 non-builtin module! + let capacity = $total + 1; + + let mut by_id = Vec::with_capacity(capacity); + + let mut insert_both = |id: ModuleId, name_str: &'static str| { + let raw_name: IdentStr = name_str.into(); + let name = PQModuleName::Unqualified(raw_name.into()); + + if cfg!(any(debug_assertions, feature = "debug-symbols")) { + Self::insert_debug_name(id, &name); + } + + by_id.push(name); + }; + + $( + insert_both(ModuleId::$module_const, $module_name); + )+ + + PackageModuleIds { by_id } + } + } + + impl Symbol { + $( + $( + $(#[$ident_meta])* + pub const $ident_const: Symbol = Symbol::new(ModuleId::$module_const, IdentId($ident_id)); + )* + $( + pub const $u_ident_const: Symbol = Symbol::new(ModuleId::$module_const, IdentId($u_ident_id)); + )* + )+ + + /// The default `Apply` types that should be in scope, + /// and what symbols they should resolve to. + /// + /// This is for type aliases that don't have a concrete Roc representation and as such + /// we hide their implementation, like `Str` and `List`. + pub fn apply_types_in_scope() -> VecMap { + let mut scope = VecMap::default(); + + $( + $( + $( + // All types should be exposed, and all non-types + // should not be exposed. (Types are uppercase.) + // + // We only check this in debug builds so that in + // release builds, this condition is either `if true` + // or `if false` and will get optimized out. + debug_assert_eq!($exposed_apply_type, $ident_name.chars().next().unwrap().is_uppercase()); + + if $exposed_apply_type { + scope.insert($ident_name.into(), (Symbol::new(ModuleId::$module_const, IdentId($ident_id)), Region::zero())); + } + )? + )* + )+ + + scope + } + + /// Types from a builtin module that should always be added to the default scope. + #[track_caller] + pub fn builtin_types_in_scope(module_id: ModuleId) -> &'static [(&'static str, (Symbol, Region))] { + match module_id { + $( + ModuleId::$module_const => { + const LIST : &'static [(&'static str, (Symbol, Region))] = &[ + $( + $( + if $exposed_type { + ($ident_name, (Symbol::new(ModuleId::$module_const, IdentId($ident_id)), Region::zero())) + } else { + unreachable!() + }, + )? + )* + ]; + LIST + } + )+ + m => roc_error_macros::internal_error!("{:?} is not a builtin module!", m), + } + } + + /// Symbols that should be added to the default scope, for hints as suggestions of + /// names you might want to use. + /// + /// TODO: this is a hack to get tag names to show up in error messages as suggestions, + /// really we should be extracting tag names from candidate type aliases in scope. + pub fn symbols_in_scope_for_hints() -> VecMap { + let mut scope = VecMap::default(); + + $( + $( + $( + if $in_scope_for_hints { + scope.insert($ident_name.into(), (Symbol::new(ModuleId::$module_const, IdentId($ident_id)), Region::zero())); + } + )? + )* + )+ + + scope + } + } + }; +} + +// NOTE: Some of these builtins have a # in their names. +// This is because they are for compiler use only, and should not cause +// namespace conflicts with userspace! +define_builtins! { + 0 ATTR: "#Attr" => { + 0 UNDERSCORE: "_" // the _ used in pattern matches. This is Symbol 0. + 1 ATTR_ATTR: "Attr" // the #Attr.Attr type alias, used in uniqueness types. + 2 ARG_1: "#arg1" + 3 ARG_2: "#arg2" + 4 ARG_3: "#arg3" + 5 ARG_4: "#arg4" + 6 ARG_5: "#arg5" + 7 ARG_6: "#arg6" + 8 ARG_7: "#arg7" + 9 ARG_8: "#arg8" + 10 INC: "#inc" // internal function that increments the refcount + 11 DEC: "#dec" // internal function that increments the refcount + 12 ARG_CLOSURE: "#arg_closure" // symbol used to store the closure record + 13 LIST_EQ: "#list_eq" // internal function that checks list equality + + 14 GENERIC_HASH: "#generic_hash" // hash of arbitrary layouts + 15 GENERIC_HASH_REF: "#generic_hash_by_ref" // hash of arbitrary layouts, passed as an opaque pointer + + 16 GENERIC_EQ_REF: "#generic_eq_by_ref" // equality of arbitrary layouts, passed as an opaque pointer + 17 GENERIC_RC_REF: "#generic_rc_by_ref" // refcount of arbitrary layouts, passed as an opaque pointer + + 18 GENERIC_EQ: "#generic_eq" // internal function that checks generic equality + + // a user-defined function that we need to capture in a closure + // see e.g. Set.walk + 19 USER_FUNCTION: "#user_function" + + // A caller (wrapper) that we pass to zig for it to be able to call Roc functions + 20 ZIG_FUNCTION_CALLER: "#zig_function_caller" + + // a caller (wrapper) for comparison + 21 GENERIC_COMPARE_REF: "#generic_compare_ref" + + // used to initialize parameters in borrow.rs + 22 EMPTY_PARAM: "#empty_param" + + // used by the dev backend to store the pointer to where to store large return types + 23 RET_POINTER: "#ret_pointer" + + // used in wasm dev backend to mark temporary values in the VM stack + 24 WASM_TMP: "#wasm_tmp" + + // the _ used in mono when a specialized symbol is deleted + 25 REMOVED_SPECIALIZATION: "#removed_specialization" + + // used in dev backend + 26 DEV_TMP: "#dev_tmp" + 27 DEV_TMP2: "#dev_tmp2" + 28 DEV_TMP3: "#dev_tmp3" + 29 DEV_TMP4: "#dev_tmp4" + 30 DEV_TMP5: "#dev_tmp5" + + 31 ATTR_INVALID: "#attr_invalid" + + 32 CLONE: "#clone" // internal function that clones a value into a buffer + } + // Fake module for synthesizing and storing derived implementations + 1 DERIVED_SYNTH: "#Derived" => { + } + // Fake module from which derived implementations are code-generated + 2 DERIVED_GEN: "#Derived_gen" => { + } + 3 NUM: "Num" => { + 0 NUM_NUM: "Num" exposed_type=true // the Num.Num type alias + 1 NUM_I128: "I128" exposed_type=true // the Num.I128 type alias + 2 NUM_U128: "U128" exposed_type=true // the Num.U128 type alias + 3 NUM_I64: "I64" exposed_type=true // the Num.I64 type alias + 4 NUM_U64: "U64" exposed_type=true // the Num.U64 type alias + 5 NUM_I32: "I32" exposed_type=true // the Num.I32 type alias + 6 NUM_U32: "U32" exposed_type=true // the Num.U32 type alias + 7 NUM_I16: "I16" exposed_type=true // the Num.I16 type alias + 8 NUM_U16: "U16" exposed_type=true // the Num.U16 type alias + 9 NUM_I8: "I8" exposed_type=true // the Num.I8 type alias + 10 NUM_U8: "U8" exposed_type=true // the Num.U8 type alias + 11 NUM_INTEGER: "Integer" exposed_type=true // Int : Num Integer + 12 NUM_F64: "F64" exposed_type=true // the Num.F64 type alias + 13 NUM_F32: "F32" exposed_type=true // the Num.F32 type alias + 14 NUM_FLOATINGPOINT: "FloatingPoint" exposed_type=true // Float : Num FloatingPoint + 15 NUM_MAX_F32: "maxF32" + 16 NUM_MIN_F32: "minF32" + 17 NUM_ABS: "abs" + 18 NUM_NEG: "neg" + 19 NUM_ADD: "add" + 20 NUM_SUB: "sub" + 21 NUM_MUL: "mul" + 22 NUM_LT: "isLt" + 23 NUM_LTE: "isLte" + 24 NUM_GT: "isGt" + 25 NUM_GTE: "isGte" + 26 NUM_TO_FRAC: "toFrac" + 27 NUM_SIN: "sin" + 28 NUM_COS: "cos" + 29 NUM_TAN: "tan" + 30 NUM_IS_ZERO: "isZero" + 31 NUM_IS_EVEN: "isEven" + 32 NUM_IS_ODD: "isOdd" + 33 NUM_IS_POSITIVE: "isPositive" + 34 NUM_IS_NEGATIVE: "isNegative" + 35 NUM_REM: "rem" + 36 NUM_REM_CHECKED: "remChecked" + 37 NUM_DIV_FRAC: "div" + 38 NUM_DIV_FRAC_CHECKED: "divChecked" + 39 NUM_DIV_TRUNC: "divTrunc" + 40 NUM_DIV_TRUNC_CHECKED: "divTruncChecked" + 41 NUM_SQRT: "sqrt" + 42 NUM_SQRT_CHECKED: "sqrtChecked" + 43 NUM_LOG: "log" + 44 NUM_LOG_CHECKED: "logChecked" + 45 NUM_ROUND: "round" + 46 NUM_COMPARE: "compare" + 47 NUM_POW: "pow" + 48 NUM_CEILING: "ceiling" + 49 NUM_POW_INT: "powInt" + 50 NUM_FLOOR: "floor" + 51 NUM_ADD_WRAP: "addWrap" + 52 NUM_ADD_CHECKED: "addChecked" + 53 NUM_ADD_SATURATED: "addSaturated" + 54 NUM_ATAN: "atan" + 55 NUM_ACOS: "acos" + 56 NUM_ASIN: "asin" + 57 NUM_SIGNED128: "Signed128" exposed_type=true + 58 NUM_SIGNED64: "Signed64" exposed_type=true + 59 NUM_SIGNED32: "Signed32" exposed_type=true + 60 NUM_SIGNED16: "Signed16" exposed_type=true + 61 NUM_SIGNED8: "Signed8" exposed_type=true + 62 NUM_UNSIGNED128: "Unsigned128" exposed_type=true + 63 NUM_UNSIGNED64: "Unsigned64" exposed_type=true + 64 NUM_UNSIGNED32: "Unsigned32" exposed_type=true + 65 NUM_UNSIGNED16: "Unsigned16" exposed_type=true + 66 NUM_UNSIGNED8: "Unsigned8" exposed_type=true + 67 NUM_BINARY64: "Binary64" exposed_type=true + 68 NUM_BINARY32: "Binary32" exposed_type=true + 69 NUM_BITWISE_AND: "bitwiseAnd" + 70 NUM_BITWISE_XOR: "bitwiseXor" + 71 NUM_BITWISE_OR: "bitwiseOr" + 72 NUM_SHIFT_LEFT: "shiftLeftBy" + 73 NUM_SHIFT_RIGHT: "shiftRightBy" + 74 NUM_SHIFT_RIGHT_ZERO_FILL: "shiftRightZfBy" + 75 NUM_SUB_WRAP: "subWrap" + 76 NUM_SUB_CHECKED: "subChecked" + 77 NUM_SUB_SATURATED: "subSaturated" + 78 NUM_MUL_WRAP: "mulWrap" + 79 NUM_MUL_CHECKED: "mulChecked" + 80 NUM_MUL_SATURATED: "mulSaturated" + 81 NUM_INT: "Int" exposed_type=true + 82 NUM_FRAC: "Frac" exposed_type=true + 83 NUM_NATURAL: "Natural" exposed_type=true + 84 NUM_NAT: "Nat" exposed_type=true + 85 NUM_INT_CAST: "intCast" + 86 NUM_IS_MULTIPLE_OF: "isMultipleOf" + 87 NUM_DECIMAL: "Decimal" exposed_type=true + 88 NUM_DEC: "Dec" exposed_type=true // the Num.Dectype alias + 89 NUM_BYTES_TO_U16: "bytesToU16" + 90 NUM_BYTES_TO_U32: "bytesToU32" + 91 NUM_BYTES_TO_U64: "bytesToU64" + 92 NUM_BYTES_TO_U128: "bytesToU128" + 93 NUM_CAST_TO_NAT: "#castToNat" + 94 NUM_DIV_CEIL: "divCeil" + 95 NUM_DIV_CEIL_CHECKED: "divCeilChecked" + 96 NUM_TO_STR: "toStr" + 97 NUM_MIN_I8: "minI8" + 98 NUM_MAX_I8: "maxI8" + 99 NUM_MIN_U8: "minU8" + 100 NUM_MAX_U8: "maxU8" + 101 NUM_MIN_I16: "minI16" + 102 NUM_MAX_I16: "maxI16" + 103 NUM_MIN_U16: "minU16" + 104 NUM_MAX_U16: "maxU16" + 105 NUM_MIN_I32: "minI32" + 106 NUM_MAX_I32: "maxI32" + 107 NUM_MIN_U32: "minU32" + 108 NUM_MAX_U32: "maxU32" + 109 NUM_MIN_I64: "minI64" + 110 NUM_MAX_I64: "maxI64" + 111 NUM_MIN_U64: "minU64" + 112 NUM_MAX_U64: "maxU64" + 113 NUM_MIN_I128: "minI128" + 114 NUM_MAX_I128: "maxI128" + 115 NUM_MIN_U128: "minU128" + 116 NUM_MAX_U128: "maxU128" + 117 NUM_TO_I8: "toI8" + 118 NUM_TO_I8_CHECKED: "toI8Checked" + 119 NUM_TO_I16: "toI16" + 120 NUM_TO_I16_CHECKED: "toI16Checked" + 121 NUM_TO_I32: "toI32" + 122 NUM_TO_I32_CHECKED: "toI32Checked" + 123 NUM_TO_I64: "toI64" + 124 NUM_TO_I64_CHECKED: "toI64Checked" + 125 NUM_TO_I128: "toI128" + 126 NUM_TO_I128_CHECKED: "toI128Checked" + 127 NUM_TO_U8: "toU8" + 128 NUM_TO_U8_CHECKED: "toU8Checked" + 129 NUM_TO_U16: "toU16" + 130 NUM_TO_U16_CHECKED: "toU16Checked" + 131 NUM_TO_U32: "toU32" + 132 NUM_TO_U32_CHECKED: "toU32Checked" + 133 NUM_TO_U64: "toU64" + 134 NUM_TO_U64_CHECKED: "toU64Checked" + 135 NUM_TO_U128: "toU128" + 136 NUM_TO_U128_CHECKED: "toU128Checked" + 137 NUM_TO_NAT: "toNat" + 138 NUM_TO_NAT_CHECKED: "toNatChecked" + 139 NUM_TO_F32: "toF32" + 140 NUM_TO_F32_CHECKED: "toF32Checked" + 141 NUM_TO_F64: "toF64" + 142 NUM_TO_F64_CHECKED: "toF64Checked" + 143 NUM_MAX_F64: "maxF64" + 144 NUM_MIN_F64: "minF64" + 145 NUM_ADD_CHECKED_LOWLEVEL: "addCheckedLowlevel" + 146 NUM_SUB_CHECKED_LOWLEVEL: "subCheckedLowlevel" + 147 NUM_MUL_CHECKED_LOWLEVEL: "mulCheckedLowlevel" + 148 NUM_BYTES_TO_U16_LOWLEVEL: "bytesToU16Lowlevel" + 149 NUM_BYTES_TO_U32_LOWLEVEL: "bytesToU32Lowlevel" + 150 NUM_BYTES_TO_U64_LOWLEVEL: "bytesToU64Lowlevel" + 151 NUM_BYTES_TO_U128_LOWLEVEL: "bytesToU128Lowlevel" + 152 NUM_COUNT_LEADING_ZERO_BITS: "countLeadingZeroBits" + 153 NUM_COUNT_TRAILING_ZERO_BITS: "countTrailingZeroBits" + 154 NUM_COUNT_ONE_BITS: "countOneBits" + 155 NUM_ABS_DIFF: "absDiff" + 156 NUM_IS_NAN: "isNaN" + 157 NUM_IS_INFINITE: "isInfinite" + 158 NUM_IS_FINITE: "isFinite" + 159 NUM_MIN: "min" + 160 NUM_MAX: "max" + 161 NUM_E: "e" + 162 NUM_PI: "pi" + 163 NUM_TAU: "tau" + 164 NUM_BITWISE_NOT: "bitwiseNot" + } + 4 BOOL: "Bool" => { + 0 BOOL_BOOL: "Bool" exposed_type=true // the Bool.Bool type alias + 1 BOOL_FALSE: "false" + 2 BOOL_TRUE: "true" + 3 BOOL_AND: "and" + 4 BOOL_OR: "or" + 5 BOOL_NOT: "not" + 6 BOOL_XOR: "xor" + 7 BOOL_NEQ: "isNotEq" + 8 BOOL_EQ: "Eq" exposed_type=true + 9 BOOL_IS_EQ: "isEq" + 10 BOOL_IS_EQ_IMPL: "boolIsEq" + unexposed 11 BOOL_STRUCTURAL_EQ: "structuralEq" + unexposed 12 BOOL_STRUCTURAL_NOT_EQ: "structuralNotEq" + } + 5 STR: "Str" => { + 0 STR_STR: "Str" exposed_apply_type=true // the Str.Str type alias + 1 STR_IS_EMPTY: "isEmpty" + 2 STR_APPEND: "#append" // unused + 3 STR_CONCAT: "concat" + 4 STR_JOIN_WITH: "joinWith" + 5 STR_SPLIT: "split" + 6 STR_COUNT_GRAPHEMES: "countGraphemes" + 7 STR_STARTS_WITH: "startsWith" + 8 STR_ENDS_WITH: "endsWith" + 9 STR_FROM_UTF8: "fromUtf8" + 10 STR_UT8_PROBLEM: "Utf8Problem" // the Utf8Problem type alias + 11 STR_UT8_BYTE_PROBLEM: "Utf8ByteProblem" // the Utf8ByteProblem type alias + 12 STR_TO_UTF8: "toUtf8" + 13 STR_STARTS_WITH_SCALAR: "startsWithScalar" + 14 STR_ALIAS_ANALYSIS_STATIC: "#aliasAnalysisStatic" // string with the static lifetime + 15 STR_FROM_UTF8_RANGE: "fromUtf8Range" + 16 STR_REPEAT: "repeat" + 17 STR_TRIM: "trim" + 18 STR_TRIM_START: "trimStart" + 19 STR_TRIM_END: "trimEnd" + 20 STR_TO_DEC: "toDec" + 21 STR_TO_F64: "toF64" + 22 STR_TO_F32: "toF32" + 23 STR_TO_NAT: "toNat" + 24 STR_TO_U128: "toU128" + 25 STR_TO_I128: "toI128" + 26 STR_TO_U64: "toU64" + 27 STR_TO_I64: "toI64" + 28 STR_TO_U32: "toU32" + 29 STR_TO_I32: "toI32" + 30 STR_TO_U16: "toU16" + 31 STR_TO_I16: "toI16" + 32 STR_TO_U8: "toU8" + 33 STR_TO_I8: "toI8" + 34 STR_TO_SCALARS: "toScalars" + 35 STR_GET_UNSAFE: "getUnsafe" + 36 STR_COUNT_UTF8_BYTES: "countUtf8Bytes" + 37 STR_SUBSTRING_UNSAFE: "substringUnsafe" + 38 STR_SPLIT_FIRST: "splitFirst" + 39 STR_SPLIT_LAST: "splitLast" + 40 STR_WALK_UTF8_WITH_INDEX: "walkUtf8WithIndex" + 41 STR_RESERVE: "reserve" + 42 STR_APPEND_SCALAR_UNSAFE: "appendScalarUnsafe" + 43 STR_APPEND_SCALAR: "appendScalar" + 44 STR_GET_SCALAR_UNSAFE: "getScalarUnsafe" + 45 STR_WALK_SCALARS: "walkScalars" + 46 STR_WALK_SCALARS_UNTIL: "walkScalarsUntil" + 47 STR_TO_NUM: "strToNum" + 48 STR_FROM_UTF8_RANGE_LOWLEVEL: "fromUtf8RangeLowlevel" + 49 STR_CAPACITY: "capacity" + 50 STR_REPLACE_EACH: "replaceEach" + 51 STR_REPLACE_FIRST: "replaceFirst" + 52 STR_REPLACE_LAST: "replaceLast" + 53 STR_WITH_CAPACITY: "withCapacity" + 54 STR_WITH_PREFIX: "withPrefix" + 55 STR_GRAPHEMES: "graphemes" + 56 STR_IS_VALID_SCALAR: "isValidScalar" + 57 STR_RELEASE_EXCESS_CAPACITY: "releaseExcessCapacity" + 58 STR_WALK_UTF8: "walkUtf8" + 59 STR_CONTAINS: "contains" + } + 6 LIST: "List" => { + 0 LIST_LIST: "List" exposed_apply_type=true // the List.List type alias + 1 LIST_IS_EMPTY: "isEmpty" + 2 LIST_GET: "get" + 3 LIST_SET: "set" + 4 LIST_APPEND: "append" + 5 LIST_MAP: "map" + 6 LIST_LEN: "len" + 7 LIST_WALK_BACKWARDS: "walkBackwards" + 8 LIST_CONCAT: "concat" + 9 LIST_FIRST: "first" + 10 LIST_SINGLE: "single" + 11 LIST_REPEAT: "repeat" + 12 LIST_REVERSE: "reverse" + 13 LIST_PREPEND: "prepend" + 14 LIST_JOIN: "join" + 15 LIST_KEEP_IF: "keepIf" + 16 LIST_CONTAINS: "contains" + 17 LIST_SUM: "sum" + 18 LIST_WALK: "walk" + 19 LIST_LAST: "last" + 20 LIST_KEEP_OKS: "keepOks" + 21 LIST_KEEP_ERRS: "keepErrs" + 22 LIST_MAP_WITH_INDEX: "mapWithIndex" + 23 LIST_MAP2: "map2" + 24 LIST_MAP3: "map3" + 25 LIST_PRODUCT: "product" + 26 LIST_WALK_UNTIL: "walkUntil" + 27 LIST_RANGE: "range" + 28 LIST_SORT_WITH: "sortWith" + 29 LIST_CHUNKS_OF: "chunksOf" + 30 LIST_SWAP: "swap" + 31 LIST_DROP_AT: "dropAt" + 32 LIST_DROP_LAST: "dropLast" + 33 LIST_MIN: "min" + 34 LIST_MIN_LT: "#minlt" + 35 LIST_MAX: "max" + 36 LIST_MAX_GT: "#maxGt" + 37 LIST_MAP4: "map4" + 38 LIST_DROP_FIRST: "dropFirst" + 39 LIST_JOIN_MAP: "joinMap" + 40 LIST_JOIN_MAP_CONCAT: "#joinMapConcat" + 41 LIST_ANY: "any" + 42 LIST_TAKE_FIRST: "takeFirst" + 43 LIST_TAKE_LAST: "takeLast" + 44 LIST_FIND_FIRST: "findFirst" + 45 LIST_FIND_LAST: "findLast" + 46 LIST_FIND_FIRST_INDEX: "findFirstIndex" + 47 LIST_FIND_LAST_INDEX: "findLastIndex" + 48 LIST_FIND_RESULT: "#find_result" // symbol used in the definition of List.findFirst + 49 LIST_SUBLIST: "sublist" + 50 LIST_INTERSPERSE: "intersperse" + 51 LIST_INTERSPERSE_CLOS: "#intersperseClos" + 52 LIST_SPLIT: "split" + 53 LIST_SPLIT_FIRST: "splitFirst" + 54 LIST_SPLIT_LAST: "splitLast" + 55 LIST_SPLIT_CLOS: "#splitClos" + 56 LIST_ALL: "all" + 57 LIST_DROP_IF: "dropIf" + 58 LIST_DROP_IF_PREDICATE: "#dropIfPred" + 59 LIST_SORT_ASC: "sortAsc" + 60 LIST_SORT_DESC: "sortDesc" + 61 LIST_SORT_DESC_COMPARE: "#sortDescCompare" + 62 LIST_STARTS_WITH: "startsWith" + 63 LIST_ENDS_WITH: "endsWith" + 64 LIST_REPLACE: "replace" + 65 LIST_IS_UNIQUE: "#isUnique" + 66 LIST_GET_UNSAFE: "getUnsafe" + 67 LIST_REPLACE_UNSAFE: "replaceUnsafe" + 68 LIST_WITH_CAPACITY: "withCapacity" + 69 LIST_UNREACHABLE: "unreachable" + 70 LIST_RESERVE: "reserve" + 71 LIST_APPEND_UNSAFE: "appendUnsafe" + 72 LIST_SUBLIST_LOWLEVEL: "sublistLowlevel" + 73 LIST_CAPACITY: "capacity" + 74 LIST_MAP_TRY: "mapTry" + 75 LIST_WALK_TRY: "walkTry" + 76 LIST_WALK_BACKWARDS_UNTIL: "walkBackwardsUntil" + 77 LIST_COUNT_IF: "countIf" + 78 LIST_WALK_FROM: "walkFrom" + 79 LIST_WALK_FROM_UNTIL: "walkFromUntil" + 80 LIST_ITER_HELP: "iterHelp" + 81 LIST_RELEASE_EXCESS_CAPACITY: "releaseExcessCapacity" + 82 LIST_UPDATE: "update" + 83 LIST_WALK_WITH_INDEX: "walkWithIndex" + 84 LIST_APPEND_IF_OK: "appendIfOk" + 85 LIST_PREPEND_IF_OK: "prependIfOk" + } + 7 RESULT: "Result" => { + 0 RESULT_RESULT: "Result" exposed_type=true // the Result.Result type alias + 1 RESULT_OK: "Ok" in_scope_for_hints=true // Result.Result a e = [Ok a, Err e] + 2 RESULT_ERR: "Err" in_scope_for_hints=true // Result.Result a e = [Ok a, Err e] + 3 RESULT_MAP: "map" + 4 RESULT_MAP_ERR: "mapErr" + 5 RESULT_WITH_DEFAULT: "withDefault" + 6 RESULT_TRY: "try" + 7 RESULT_IS_OK: "isOk" + 8 RESULT_IS_ERR: "isErr" + 9 RESULT_ON_ERR: "onErr" + } + 8 DICT: "Dict" => { + 0 DICT_DICT: "Dict" exposed_type=true // the Dict.Dict type alias + 1 DICT_EMPTY: "empty" + 2 DICT_SINGLE: "single" + 3 DICT_CLEAR: "clear" + 4 DICT_LEN: "len" + 5 DICT_GET: "get" + 6 DICT_GET_RESULT: "#get_result" // symbol used in the definition of Dict.get + 7 DICT_CONTAINS: "contains" + 8 DICT_INSERT: "insert" + 9 DICT_REMOVE: "remove" + + 10 DICT_WALK: "walk" + 11 DICT_WALK_UNTIL: "walkUntil" + 12 DICT_FROM_LIST: "fromList" + 13 DICT_TO_LIST: "toList" + 14 DICT_KEYS: "keys" + 15 DICT_VALUES: "values" + + 16 DICT_INSERT_ALL: "insertAll" // union + 17 DICT_KEEP_SHARED: "keepShared" // intersection + 18 DICT_REMOVE_ALL: "removeAll" // difference + + 19 DICT_WITH_CAPACITY: "withCapacity" + 20 DICT_CAPACITY: "capacity" + 21 DICT_UPDATE: "update" + + 22 DICT_LIST_GET_UNSAFE: "listGetUnsafe" + 23 DICT_PSEUDO_SEED: "pseudoSeed" + 24 DICT_IS_EMPTY: "isEmpty" + 25 DICT_MAP: "map" + 26 DICT_JOINMAP: "joinMap" + } + 9 SET: "Set" => { + 0 SET_SET: "Set" exposed_type=true // the Set.Set type alias + 1 SET_EMPTY: "empty" + 2 SET_SINGLE: "single" + 3 SET_LEN: "len" + 4 SET_INSERT: "insert" + 5 SET_REMOVE: "remove" + 6 SET_UNION: "union" + 7 SET_DIFFERENCE: "difference" + 8 SET_INTERSECTION: "intersection" + 9 SET_TO_LIST: "toList" + 10 SET_FROM_LIST: "fromList" + 11 SET_WALK: "walk" + 12 SET_WALK_UNTIL: "walkUntil" + 13 SET_WALK_USER_FUNCTION: "#walk_user_function" + 14 SET_CONTAINS: "contains" + 15 SET_TO_DICT: "toDict" + 16 SET_CAPACITY: "capacity" + 17 SET_IS_EMPTY: "isEmpty" + 18 SET_MAP: "map" + 19 SET_JOIN_MAP: "joinMap" + } + 10 BOX: "Box" => { + 0 BOX_BOX_TYPE: "Box" exposed_apply_type=true // the Box.Box opaque type + 1 BOX_BOX_FUNCTION: "box" // Box.box + 2 BOX_UNBOX: "unbox" + } + 11 ENCODE: "Encode" => { + 0 ENCODE_ENCODER: "Encoder" exposed_type=true + 1 ENCODE_ENCODING: "Encoding" exposed_type=true + 2 ENCODE_TO_ENCODER: "toEncoder" + 3 ENCODE_ENCODERFORMATTING: "EncoderFormatting" exposed_type=true + 4 ENCODE_U8: "u8" + 5 ENCODE_U16: "u16" + 6 ENCODE_U32: "u32" + 7 ENCODE_U64: "u64" + 8 ENCODE_U128: "u128" + 9 ENCODE_I8: "i8" + 10 ENCODE_I16: "i16" + 11 ENCODE_I32: "i32" + 12 ENCODE_I64: "i64" + 13 ENCODE_I128: "i128" + 14 ENCODE_F32: "f32" + 15 ENCODE_F64: "f64" + 16 ENCODE_DEC: "dec" + 17 ENCODE_BOOL: "bool" + 18 ENCODE_STRING: "string" + 19 ENCODE_LIST: "list" + 20 ENCODE_RECORD: "record" + 21 ENCODE_TUPLE: "tuple" + 22 ENCODE_TAG: "tag" + 23 ENCODE_CUSTOM: "custom" + 24 ENCODE_APPEND_WITH: "appendWith" + 25 ENCODE_APPEND: "append" + 26 ENCODE_TO_BYTES: "toBytes" + } + 12 DECODE: "Decode" => { + 0 DECODE_DECODE_ERROR: "DecodeError" exposed_type=true + 1 DECODE_DECODE_RESULT: "DecodeResult" exposed_type=true + 2 DECODE_DECODER_OPAQUE: "Decoder" exposed_type=true + 3 DECODE_DECODING: "Decoding" exposed_type=true + 4 DECODE_DECODER: "decoder" + 5 DECODE_DECODERFORMATTING: "DecoderFormatting" exposed_type=true + 6 DECODE_U8: "u8" + 7 DECODE_U16: "u16" + 8 DECODE_U32: "u32" + 9 DECODE_U64: "u64" + 10 DECODE_U128: "u128" + 11 DECODE_I8: "i8" + 12 DECODE_I16: "i16" + 13 DECODE_I32: "i32" + 14 DECODE_I64: "i64" + 15 DECODE_I128: "i128" + 16 DECODE_F32: "f32" + 17 DECODE_F64: "f64" + 18 DECODE_DEC: "dec" + 19 DECODE_BOOL: "bool" + 20 DECODE_STRING: "string" + 21 DECODE_LIST: "list" + 22 DECODE_RECORD: "record" + 23 DECODE_TUPLE: "tuple" + 24 DECODE_CUSTOM: "custom" + 25 DECODE_DECODE_WITH: "decodeWith" + 26 DECODE_FROM_BYTES_PARTIAL: "fromBytesPartial" + 27 DECODE_FROM_BYTES: "fromBytes" + 28 DECODE_MAP_RESULT: "mapResult" + } + 13 HASH: "Hash" => { + 0 HASH_HASH_ABILITY: "Hash" exposed_type=true + 1 HASH_HASH: "hash" + 2 HASH_HASHER: "Hasher" exposed_type=true + 3 HASH_ADD_BYTES: "addBytes" + 4 HASH_ADD_U8: "addU8" + 5 HASH_ADD_U16: "addU16" + 6 HASH_ADD_U32: "addU32" + 7 HASH_ADD_U64: "addU64" + 8 HASH_ADD_U128: "addU128" + 9 HASH_HASH_BOOL: "hashBool" + 10 HASH_HASH_I8: "hashI8" + 11 HASH_HASH_I16: "hashI16" + 12 HASH_HASH_I32: "hashI32" + 13 HASH_HASH_I64: "hashI64" + 14 HASH_HASH_I128: "hashI128" + 15 HASH_HASH_NAT: "hashNat" + 16 I128_OF_DEC: "i128OfDec" + 17 HASH_HASH_DEC: "hashDec" + 18 HASH_COMPLETE: "complete" + 19 HASH_HASH_STR_BYTES: "hashStrBytes" + 20 HASH_HASH_LIST: "hashList" + 21 HASH_HASH_UNORDERED: "hashUnordered" + } + 14 INSPECT: "Inspect" => { + 0 INSPECT_INSPECT_ABILITY: "Inspect" exposed_type=true + 1 INSPECT_INSPECTOR: "Inspector" exposed_type=true + 2 INSPECT_INSPECT_FORMATTER: "InspectFormatter" exposed_type=true + 3 INSPECT_ELEM_WALKER: "ElemWalker" exposed_type=true + 4 INSPECT_KEY_VAL_WALKER: "KeyValWalker" exposed_type=true + 5 INSPECT_INSPECT: "inspect" + 6 INSPECT_INIT: "init" + 7 INSPECT_LIST: "list" + 8 INSPECT_SET: "set" + 9 INSPECT_DICT: "dict" + 10 INSPECT_TAG: "tag" + 11 INSPECT_TUPLE: "tuple" + 12 INSPECT_RECORD: "record" + 13 INSPECT_BOOL: "bool" + 14 INSPECT_STR: "str" + 15 INSPECT_OPAQUE: "opaque" + 16 INSPECT_FUNCTION: "function" + 17 INSPECT_U8: "u8" + 18 INSPECT_I8: "i8" + 19 INSPECT_U16: "u16" + 20 INSPECT_I16: "i16" + 21 INSPECT_U32: "u32" + 22 INSPECT_I32: "i32" + 23 INSPECT_U64: "u64" + 24 INSPECT_I64: "i64" + 25 INSPECT_U128: "u128" + 26 INSPECT_I128: "i128" + 27 INSPECT_F32: "f32" + 28 INSPECT_F64: "f64" + 29 INSPECT_DEC: "dec" + 30 INSPECT_CUSTOM: "custom" + 31 INSPECT_APPLY: "apply" + 32 INSPECT_TO_INSPECTOR: "toInspector" + 33 INSPECT_NAT: "nat" + 34 INSPECT_DBG_FORMATTER: "DbgFormatter" exposed_type=true + 35 INSPECT_TO_DBG_STR: "toDbgStr" + } + 15 JSON: "TotallyNotJson" => { + 0 JSON_JSON: "TotallyNotJson" + 1 JSON_FIELD_NAME_MAPPING: "FieldNameMapping" + 2 JSON_NUMBER_STATE: "NumberState" + 3 JSON_STRING_STATE: "StringState" + 4 JSON_ARRAY_OPENING_STATE: "ArrayOpeningState" + 5 JSON_ARRAY_CLOSING_STATE: "ArrayClosingState" + 6 JSON_OBJECT_STATE: "ObjectState" + } + + num_modules: 16 // Keep this count up to date by hand! (TODO: see the mut_map! macro for how we could determine this count correctly in the macro) +} diff --git a/crates/compiler/mono/Cargo.toml b/crates/compiler/mono/Cargo.toml new file mode 100644 index 0000000000..abd429f82c --- /dev/null +++ b/crates/compiler/mono/Cargo.toml @@ -0,0 +1,36 @@ +[package] +name = "roc_mono" +description = "Roc's main intermediate representation (IR), which is responsible for monomorphization, defunctionalization, inserting ref-count instructions, and transforming a Roc program into a form that is easy to consume by a backend." + +authors.workspace = true +edition.workspace = true +license.workspace = true +version.workspace = true + +[dependencies] +roc_builtins = { path = "../builtins" } +roc_can = { path = "../can" } +roc_collections = { path = "../collections" } +roc_debug_flags = { path = "../debug_flags" } +roc_derive = { path = "../derive" } +roc_derive_key = { path = "../derive_key" } +roc_error_macros = { path = "../../error_macros" } +roc_exhaustive = { path = "../exhaustive" } +roc_late_solve = { path = "../late_solve" } +roc_module = { path = "../module" } +roc_problem = { path = "../problem" } +roc_region = { path = "../region" } +roc_std = { path = "../../roc_std" } +roc_target = { path = "../roc_target" } +roc_tracing = { path = "../../tracing" } +roc_types = { path = "../types" } +roc_solve_schema = { path = "../solve_schema" } +roc_unify = { path = "../unify" } +ven_pretty = { path = "../../vendor/pretty" } + +bitvec.workspace = true +arrayvec.workspace = true +bumpalo.workspace = true +hashbrown.workspace = true +parking_lot.workspace = true +static_assertions.workspace = true diff --git a/crates/compiler/mono/src/code_gen_help/equality.rs b/crates/compiler/mono/src/code_gen_help/equality.rs new file mode 100644 index 0000000000..de3eadf65a --- /dev/null +++ b/crates/compiler/mono/src/code_gen_help/equality.rs @@ -0,0 +1,898 @@ +use bumpalo::collections::vec::Vec; +use roc_module::low_level::LowLevel; +use roc_module::symbol::{IdentIds, Symbol}; + +use crate::ir::{ + BranchInfo, Call, CallType, Expr, JoinPointId, Literal, Param, Stmt, UpdateModeId, +}; +use crate::layout::{ + InLayout, Layout, LayoutInterner, LayoutRepr, STLayoutInterner, TagIdIntType, UnionLayout, +}; + +use super::{let_lowlevel, CodeGenHelp, Context, LAYOUT_BOOL}; + +const ARG_1: Symbol = Symbol::ARG_1; +const ARG_2: Symbol = Symbol::ARG_2; + +pub fn eq_generic<'a>( + root: &mut CodeGenHelp<'a>, + ident_ids: &mut IdentIds, + ctx: &mut Context<'a>, + layout_interner: &mut STLayoutInterner<'a>, + layout: InLayout<'a>, +) -> Stmt<'a> { + use crate::layout::Builtin::*; + use LayoutRepr::*; + let main_body = match layout_interner.get_repr(layout) { + Builtin(Int(_) | Float(_) | Bool | Decimal) | FunctionPointer(_) => { + unreachable!( + "No generated proc for `==`. Use direct code gen for {:?}", + layout + ) + } + Builtin(Str) => { + unreachable!("No generated helper proc for `==` on Str. Use Zig function.") + } + Builtin(List(elem_layout)) => eq_list(root, ident_ids, ctx, layout_interner, elem_layout), + Struct(field_layouts) => eq_struct(root, ident_ids, ctx, layout_interner, field_layouts), + Union(union_layout) => eq_tag_union(root, ident_ids, ctx, layout_interner, union_layout), + Ptr(inner_layout) => eq_boxed(root, ident_ids, ctx, layout_interner, inner_layout), + LambdaSet(_) => unreachable!("`==` is not defined on functions"), + Erased(_) => unreachable!("`==` is not defined on erased types"), + RecursivePointer(_) => { + unreachable!( + "Can't perform `==` on RecursivePointer. Should have been replaced by a tag union." + ) + } + }; + + Stmt::Let( + Symbol::BOOL_TRUE, + Expr::Literal(Literal::Bool(true)), + LAYOUT_BOOL, + root.arena.alloc(Stmt::Let( + Symbol::BOOL_FALSE, + Expr::Literal(Literal::Bool(false)), + LAYOUT_BOOL, + root.arena.alloc(main_body), + )), + ) +} + +fn if_pointers_equal_return_true<'a>( + root: &CodeGenHelp<'a>, + ident_ids: &mut IdentIds, + operands: [Symbol; 2], + following: &'a Stmt<'a>, +) -> Stmt<'a> { + let ptr1_addr = root.create_symbol(ident_ids, "addr1"); + let ptr2_addr = root.create_symbol(ident_ids, "addr2"); + let ptr_eq = root.create_symbol(ident_ids, "eq_addr"); + + Stmt::Let( + ptr1_addr, + Expr::Call(Call { + call_type: CallType::LowLevel { + op: LowLevel::PtrCast, + update_mode: UpdateModeId::BACKEND_DUMMY, + }, + arguments: root.arena.alloc([operands[0]]), + }), + root.layout_isize, + root.arena.alloc(Stmt::Let( + ptr2_addr, + Expr::Call(Call { + call_type: CallType::LowLevel { + op: LowLevel::PtrCast, + update_mode: UpdateModeId::BACKEND_DUMMY, + }, + arguments: root.arena.alloc([operands[1]]), + }), + root.layout_isize, + root.arena.alloc(Stmt::Let( + ptr_eq, + Expr::Call(Call { + call_type: CallType::LowLevel { + op: LowLevel::Eq, + update_mode: UpdateModeId::BACKEND_DUMMY, + }, + arguments: root.arena.alloc([ptr1_addr, ptr2_addr]), + }), + LAYOUT_BOOL, + root.arena.alloc(Stmt::Switch { + cond_symbol: ptr_eq, + cond_layout: LAYOUT_BOOL, + branches: root.arena.alloc([( + 1, + BranchInfo::None, + Stmt::Ret(Symbol::BOOL_TRUE), + )]), + default_branch: (BranchInfo::None, following), + ret_layout: LAYOUT_BOOL, + }), + )), + )), + ) +} + +fn if_false_return_false<'a>( + root: &CodeGenHelp<'a>, + symbol: Symbol, + following: Stmt<'a>, +) -> Stmt<'a> { + Stmt::if_then_else( + root.arena, + symbol, + Layout::BOOL, + following, + root.arena.alloc(Stmt::Ret(Symbol::BOOL_FALSE)), + ) +} + +fn eq_struct<'a>( + root: &mut CodeGenHelp<'a>, + ident_ids: &mut IdentIds, + ctx: &mut Context<'a>, + layout_interner: &mut STLayoutInterner<'a>, + field_layouts: &'a [InLayout<'a>], +) -> Stmt<'a> { + let mut else_stmt = Stmt::Ret(Symbol::BOOL_TRUE); + for (i, layout) in field_layouts.iter().enumerate().rev() { + let field1_sym = root.create_symbol(ident_ids, &format!("field_1_{i}")); + let field1_expr = Expr::StructAtIndex { + index: i as u64, + field_layouts, + structure: ARG_1, + }; + let field1_stmt = |next| Stmt::Let(field1_sym, field1_expr, *layout, next); + + let field2_sym = root.create_symbol(ident_ids, &format!("field_2_{i}")); + let field2_expr = Expr::StructAtIndex { + index: i as u64, + field_layouts, + structure: ARG_2, + }; + let field2_stmt = |next| Stmt::Let(field2_sym, field2_expr, *layout, next); + + let eq_call_expr = root + .call_specialized_op( + ident_ids, + ctx, + layout_interner, + *layout, + root.arena.alloc([field1_sym, field2_sym]), + ) + .unwrap(); + + let eq_call_name = format!("eq_call_{i}"); + let eq_call_sym = root.create_symbol(ident_ids, &eq_call_name); + let eq_call_stmt = |next| Stmt::Let(eq_call_sym, eq_call_expr, LAYOUT_BOOL, next); + + else_stmt = field1_stmt(root.arena.alloc( + // + field2_stmt(root.arena.alloc( + // + eq_call_stmt(root.arena.alloc( + // + if_false_return_false(root, eq_call_sym, else_stmt), + )), + )), + )) + } + + else_stmt +} + +fn eq_tag_union<'a>( + root: &mut CodeGenHelp<'a>, + ident_ids: &mut IdentIds, + ctx: &mut Context<'a>, + layout_interner: &mut STLayoutInterner<'a>, + union_layout: UnionLayout<'a>, +) -> Stmt<'a> { + use UnionLayout::*; + + let parent_rec_ptr_layout = ctx.recursive_union; + if !matches!(union_layout, NonRecursive(_)) { + ctx.recursive_union = Some(union_layout); + } + + let body = match union_layout { + NonRecursive(&[]) => { + // cannot be reached at runtime, but we need to generate valid code + Stmt::Ret(Symbol::BOOL_TRUE) + } + NonRecursive(tags) => eq_tag_union_help( + root, + ident_ids, + ctx, + layout_interner, + union_layout, + tags, + NullableId::None, + ), + + Recursive(tags) => eq_tag_union_help( + root, + ident_ids, + ctx, + layout_interner, + union_layout, + tags, + NullableId::None, + ), + + NonNullableUnwrapped(field_layouts) => { + let tags = root.arena.alloc([field_layouts]); + eq_tag_union_help( + root, + ident_ids, + ctx, + layout_interner, + union_layout, + tags, + NullableId::None, + ) + } + + NullableWrapped { + other_tags, + nullable_id, + } => eq_tag_union_help( + root, + ident_ids, + ctx, + layout_interner, + union_layout, + other_tags, + NullableId::Wrapped(nullable_id), + ), + + NullableUnwrapped { + other_fields, + nullable_id, + } => eq_tag_union_help( + root, + ident_ids, + ctx, + layout_interner, + union_layout, + root.arena.alloc([other_fields]), + NullableId::Unwrapped(nullable_id), + ), + }; + + ctx.recursive_union = parent_rec_ptr_layout; + + body +} + +enum NullableId { + None, + Wrapped(TagIdIntType), + Unwrapped(bool), +} + +fn eq_tag_union_help<'a>( + root: &mut CodeGenHelp<'a>, + ident_ids: &mut IdentIds, + ctx: &mut Context<'a>, + layout_interner: &mut STLayoutInterner<'a>, + union_layout: UnionLayout<'a>, + tag_layouts: &'a [&'a [InLayout<'a>]], + nullable_id: NullableId, +) -> Stmt<'a> { + let tailrec_loop = JoinPointId(root.create_symbol(ident_ids, "tailrec_loop")); + let is_non_recursive = matches!(union_layout, UnionLayout::NonRecursive(_)); + let operands = if is_non_recursive { + [ARG_1, ARG_2] + } else { + [ + root.create_symbol(ident_ids, "a"), + root.create_symbol(ident_ids, "b"), + ] + }; + + let tag_id_layout = union_layout.tag_id_layout(); + + let tag_id_a = root.create_symbol(ident_ids, "tag_id_a"); + let tag_id_a_stmt = |next| { + Stmt::Let( + tag_id_a, + Expr::GetTagId { + structure: operands[0], + union_layout, + }, + tag_id_layout, + next, + ) + }; + + let tag_id_b = root.create_symbol(ident_ids, "tag_id_b"); + let tag_id_b_stmt = |next| { + Stmt::Let( + tag_id_b, + Expr::GetTagId { + structure: operands[1], + union_layout, + }, + tag_id_layout, + next, + ) + }; + + let tag_ids_eq = root.create_symbol(ident_ids, "tag_ids_eq"); + let tag_ids_expr = Expr::Call(Call { + call_type: CallType::LowLevel { + op: LowLevel::Eq, + update_mode: UpdateModeId::BACKEND_DUMMY, + }, + arguments: root.arena.alloc([tag_id_a, tag_id_b]), + }); + let tag_ids_eq_stmt = |next| Stmt::Let(tag_ids_eq, tag_ids_expr, LAYOUT_BOOL, next); + + let if_equal_ids_branches = + root.arena + .alloc([(0, BranchInfo::None, Stmt::Ret(Symbol::BOOL_FALSE))]); + + // + // Switch statement by tag ID + // + + let mut tag_branches = Vec::with_capacity_in(tag_layouts.len(), root.arena); + + // If there's a null tag, check it first. We might not need to load any data from memory. + match nullable_id { + NullableId::Wrapped(id) => { + tag_branches.push((id as u64, BranchInfo::None, Stmt::Ret(Symbol::BOOL_TRUE))) + } + NullableId::Unwrapped(id) => tag_branches.push(( + id as TagIdIntType as u64, + BranchInfo::None, + Stmt::Ret(Symbol::BOOL_TRUE), + )), + _ => (), + } + + let default_tag = if let NullableId::Unwrapped(tag_id) = nullable_id { + (!tag_id) as TagIdIntType + } else { + let mut tag_id: TagIdIntType = 0; + + for field_layouts in tag_layouts.iter().take(tag_layouts.len() - 1) { + if let NullableId::Wrapped(null_id) = nullable_id { + if tag_id == null_id as TagIdIntType { + tag_id += 1; + } + } + + let tag_stmt = eq_tag_fields( + root, + ident_ids, + ctx, + layout_interner, + tailrec_loop, + union_layout, + field_layouts, + operands, + tag_id, + ); + tag_branches.push((tag_id as u64, BranchInfo::None, tag_stmt)); + + tag_id += 1; + } + + tag_id + }; + + let tag_switch_stmt = Stmt::Switch { + cond_symbol: tag_id_a, + cond_layout: tag_id_layout, + branches: tag_branches.into_bump_slice(), + default_branch: ( + BranchInfo::None, + root.arena.alloc(eq_tag_fields( + root, + ident_ids, + ctx, + layout_interner, + tailrec_loop, + union_layout, + tag_layouts.last().unwrap(), + operands, + default_tag, + )), + ), + ret_layout: LAYOUT_BOOL, + }; + + let if_equal_ids_stmt = Stmt::Switch { + cond_symbol: tag_ids_eq, + cond_layout: LAYOUT_BOOL, + branches: if_equal_ids_branches, + default_branch: (BranchInfo::None, root.arena.alloc(tag_switch_stmt)), + ret_layout: LAYOUT_BOOL, + }; + + // + // combine all the statments + // + let compare_values = tag_id_a_stmt(root.arena.alloc( + // + tag_id_b_stmt(root.arena.alloc( + // + tag_ids_eq_stmt(root.arena.alloc( + // + if_equal_ids_stmt, + )), + )), + )); + + if is_non_recursive { + compare_values + } else { + let compare_ptr_or_value = if_pointers_equal_return_true( + root, + ident_ids, + operands, + root.arena.alloc(compare_values), + ); + + let union_layout = + layout_interner.insert_direct_no_semantic(LayoutRepr::Union(union_layout)); + + let loop_params_iter = operands.iter().map(|arg| Param { + symbol: *arg, + layout: union_layout, + }); + + let loop_start = Stmt::Jump(tailrec_loop, root.arena.alloc([ARG_1, ARG_2])); + + Stmt::Join { + id: tailrec_loop, + parameters: root.arena.alloc_slice_fill_iter(loop_params_iter), + body: root.arena.alloc(compare_ptr_or_value), + remainder: root.arena.alloc(loop_start), + } + } +} + +#[allow(clippy::too_many_arguments)] +fn eq_tag_fields<'a>( + root: &mut CodeGenHelp<'a>, + ident_ids: &mut IdentIds, + ctx: &mut Context<'a>, + layout_interner: &mut STLayoutInterner<'a>, + tailrec_loop: JoinPointId, + union_layout: UnionLayout<'a>, + field_layouts: &'a [InLayout<'a>], + operands: [Symbol; 2], + tag_id: TagIdIntType, +) -> Stmt<'a> { + // Find a RecursivePointer to use in the tail recursion loop + // (If there are more than one, the others will use non-tail recursion) + let rec_ptr_index = field_layouts.iter().position(|field| { + matches!( + layout_interner.get_repr(*field), + LayoutRepr::RecursivePointer(_) + ) + }); + + let (tailrec_index, innermost_stmt) = match rec_ptr_index { + None => { + // This tag has no RecursivePointers. Set tailrec_index out of range. + (field_layouts.len(), Stmt::Ret(Symbol::BOOL_TRUE)) + } + + Some(i) => { + // Implement tail recursion on this RecursivePointer, + // in the innermost `else` clause after all other fields have been checked + let field1_sym = root.create_symbol(ident_ids, &format!("field_1_{tag_id}_{i}")); + let field2_sym = root.create_symbol(ident_ids, &format!("field_2_{tag_id}_{i}")); + + let field1_expr = Expr::UnionAtIndex { + union_layout, + tag_id, + index: i as u64, + structure: operands[0], + }; + + let field2_expr = Expr::UnionAtIndex { + union_layout, + tag_id, + index: i as u64, + structure: operands[1], + }; + + let inner = Stmt::Let( + field1_sym, + field1_expr, + field_layouts[i], + root.arena.alloc( + // + Stmt::Let( + field2_sym, + field2_expr, + field_layouts[i], + root.arena.alloc( + // + Stmt::Jump(tailrec_loop, root.arena.alloc([field1_sym, field2_sym])), + ), + ), + ), + ); + + (i, inner) + } + }; + + let mut stmt = innermost_stmt; + for (i, layout) in field_layouts.iter().enumerate().rev() { + if i == tailrec_index { + continue; // the tail-recursive field is handled elsewhere + } + + let field1_sym = root.create_symbol(ident_ids, &format!("field_1_{tag_id}_{i}")); + let field2_sym = root.create_symbol(ident_ids, &format!("field_2_{tag_id}_{i}")); + + let field1_expr = Expr::UnionAtIndex { + union_layout, + tag_id, + index: i as u64, + structure: operands[0], + }; + + let field2_expr = Expr::UnionAtIndex { + union_layout, + tag_id, + index: i as u64, + structure: operands[1], + }; + + let eq_call_expr = root + .call_specialized_op( + ident_ids, + ctx, + layout_interner, + *layout, + root.arena.alloc([field1_sym, field2_sym]), + ) + .unwrap(); + + let eq_call_name = format!("eq_call_{i}"); + let eq_call_sym = root.create_symbol(ident_ids, &eq_call_name); + + stmt = Stmt::Let( + field1_sym, + field1_expr, + field_layouts[i], + root.arena.alloc( + // + Stmt::Let( + field2_sym, + field2_expr, + field_layouts[i], + root.arena.alloc( + // + Stmt::Let( + eq_call_sym, + eq_call_expr, + LAYOUT_BOOL, + root.arena.alloc( + // + if_false_return_false( + root, + eq_call_sym, + // else + stmt, + ), + ), + ), + ), + ), + ), + ) + } + stmt +} + +fn eq_boxed<'a>( + root: &mut CodeGenHelp<'a>, + ident_ids: &mut IdentIds, + ctx: &mut Context<'a>, + layout_interner: &mut STLayoutInterner<'a>, + inner_layout: InLayout<'a>, +) -> Stmt<'a> { + let a = root.create_symbol(ident_ids, "a"); + let b = root.create_symbol(ident_ids, "b"); + let result = root.create_symbol(ident_ids, "result"); + + let a_expr = Expr::ptr_load(&ARG_1); + let b_expr = Expr::ptr_load(&ARG_2); + let eq_call_expr = root + .call_specialized_op( + ident_ids, + ctx, + layout_interner, + inner_layout, + root.arena.alloc([a, b]), + ) + .unwrap(); + + Stmt::Let( + a, + a_expr, + inner_layout, + root.arena.alloc( + // + Stmt::Let( + b, + b_expr, + inner_layout, + root.arena.alloc( + // + Stmt::Let( + result, + eq_call_expr, + LAYOUT_BOOL, + root.arena.alloc(Stmt::Ret(result)), + ), + ), + ), + ), + ) +} + +/// List equality +/// TODO, ListGetUnsafe no longer increments the refcount, so we can use it here. +/// We can't use `ListGetUnsafe` because it increments the refcount, and we don't want that. +/// Another way to dereference a heap pointer is to use `Expr::UnionAtIndex`. +/// To achieve this we use `PtrCast` to cast the element pointer to a "Ptr" layout. +/// Then we can increment the pointer in a loop, dereferencing it each time. +/// (An alternative approach would be to create a new lowlevel like ListPeekUnsafe.) +fn eq_list<'a>( + root: &mut CodeGenHelp<'a>, + ident_ids: &mut IdentIds, + ctx: &mut Context<'a>, + layout_interner: &mut STLayoutInterner<'a>, + elem_layout: InLayout<'a>, +) -> Stmt<'a> { + use LowLevel::*; + let layout_isize = root.layout_isize; + let arena = root.arena; + + // A pointer layout (heap pointer to a single list element) + let ptr_layout = layout_interner.insert_direct_no_semantic(LayoutRepr::Ptr(elem_layout)); + + // Compare lengths + + let len_1 = root.create_symbol(ident_ids, "len_1"); + let len_2 = root.create_symbol(ident_ids, "len_2"); + let len_1_stmt = |next| let_lowlevel(arena, layout_isize, len_1, ListLen, &[ARG_1], next); + let len_2_stmt = |next| let_lowlevel(arena, layout_isize, len_2, ListLen, &[ARG_2], next); + + let eq_len = root.create_symbol(ident_ids, "eq_len"); + let eq_len_stmt = |next| let_lowlevel(arena, LAYOUT_BOOL, eq_len, Eq, &[len_1, len_2], next); + + // if lengths are equal... + + // get element pointers + let elements_1 = root.create_symbol(ident_ids, "elements_1"); + let elements_2 = root.create_symbol(ident_ids, "elements_2"); + let elements_1_expr = Expr::StructAtIndex { + index: 0, + field_layouts: root.arena.alloc([ptr_layout, layout_isize]), + structure: ARG_1, + }; + let elements_2_expr = Expr::StructAtIndex { + index: 0, + field_layouts: root.arena.alloc([ptr_layout, layout_isize]), + structure: ARG_2, + }; + let elements_1_stmt = |next| Stmt::Let(elements_1, elements_1_expr, ptr_layout, next); + let elements_2_stmt = |next| Stmt::Let(elements_2, elements_2_expr, ptr_layout, next); + + // Cast to integers + let start_1 = root.create_symbol(ident_ids, "start_1"); + let start_2 = root.create_symbol(ident_ids, "start_2"); + let start_1_stmt = + |next| let_lowlevel(arena, layout_isize, start_1, PtrCast, &[elements_1], next); + let start_2_stmt = + |next| let_lowlevel(arena, layout_isize, start_2, PtrCast, &[elements_2], next); + + // + // Loop initialisation + // + + // let size = literal int + let size = root.create_symbol(ident_ids, "size"); + let size_expr = Expr::Literal(Literal::Int( + (layout_interner + .get_repr(elem_layout) + .stack_size(layout_interner) as i128) + .to_ne_bytes(), + )); + let size_stmt = |next| Stmt::Let(size, size_expr, layout_isize, next); + + // let list_size = len_1 * size + let list_size = root.create_symbol(ident_ids, "list_size"); + let list_size_stmt = + |next| let_lowlevel(arena, layout_isize, list_size, NumMul, &[len_1, size], next); + + // let end_1 = start_1 + list_size + let end_1 = root.create_symbol(ident_ids, "end_1"); + let end_1_stmt = |next| { + let_lowlevel( + arena, + layout_isize, + end_1, + NumAdd, + &[start_1, list_size], + next, + ) + }; + + // + // Loop name & parameters + // + + let elems_loop = JoinPointId(root.create_symbol(ident_ids, "elems_loop")); + let addr1 = root.create_symbol(ident_ids, "addr1"); + let addr2 = root.create_symbol(ident_ids, "addr2"); + + let param_addr1 = Param { + symbol: addr1, + layout: layout_isize, + }; + + let param_addr2 = Param { + symbol: addr2, + layout: layout_isize, + }; + + // + // if we haven't reached the end yet... + // + + // Cast integers to pointers + let ptr1 = root.create_symbol(ident_ids, "ptr1"); + let ptr2 = root.create_symbol(ident_ids, "ptr2"); + let ptr1_stmt = |next| let_lowlevel(arena, ptr_layout, ptr1, PtrCast, &[addr1], next); + let ptr2_stmt = |next| let_lowlevel(arena, ptr_layout, ptr2, PtrCast, &[addr2], next); + + // Dereference the pointers to get the current elements + let elem1 = root.create_symbol(ident_ids, "elem1"); + let elem2 = root.create_symbol(ident_ids, "elem2"); + let elem1_expr = Expr::ptr_load(arena.alloc(ptr1)); + let elem2_expr = Expr::ptr_load(arena.alloc(ptr2)); + let elem1_stmt = |next| Stmt::Let(elem1, elem1_expr, elem_layout, next); + let elem2_stmt = |next| Stmt::Let(elem2, elem2_expr, elem_layout, next); + + // Compare the two current elements + let eq_elems = root.create_symbol(ident_ids, "eq_elems"); + let eq_elems_args = root.arena.alloc([elem1, elem2]); + let eq_elems_expr = root + .call_specialized_op(ident_ids, ctx, layout_interner, elem_layout, eq_elems_args) + .unwrap(); + + let eq_elems_stmt = |next| Stmt::Let(eq_elems, eq_elems_expr, LAYOUT_BOOL, next); + + // If current elements are equal, loop back again + let next_1 = root.create_symbol(ident_ids, "next_1"); + let next_2 = root.create_symbol(ident_ids, "next_2"); + let next_1_stmt = + |next| let_lowlevel(arena, layout_isize, next_1, NumAdd, &[addr1, size], next); + let next_2_stmt = + |next| let_lowlevel(arena, layout_isize, next_2, NumAdd, &[addr2, size], next); + + let jump_back = Stmt::Jump(elems_loop, root.arena.alloc([next_1, next_2])); + + // + // Control flow + // + + let is_end = root.create_symbol(ident_ids, "is_end"); + let is_end_stmt = + |next| let_lowlevel(arena, LAYOUT_BOOL, is_end, NumGte, &[addr1, end_1], next); + + let if_elems_not_equal = if_false_return_false( + root, + eq_elems, + // else + next_1_stmt(root.arena.alloc( + // + next_2_stmt(root.arena.alloc( + // + jump_back, + )), + )), + ); + + let if_end_of_list = Stmt::if_then_else( + arena, + is_end, + Layout::BOOL, + Stmt::Ret(Symbol::BOOL_TRUE), + root.arena.alloc( + // + ptr1_stmt(root.arena.alloc( + // + ptr2_stmt(root.arena.alloc( + // + elem1_stmt(root.arena.alloc( + // + elem2_stmt(root.arena.alloc( + // + eq_elems_stmt(root.arena.alloc( + // + if_elems_not_equal, + )), + )), + )), + )), + )), + ), + ); + + let joinpoint_loop = Stmt::Join { + id: elems_loop, + parameters: root.arena.alloc([param_addr1, param_addr2]), + body: root.arena.alloc( + // + is_end_stmt( + // + root.arena.alloc(if_end_of_list), + ), + ), + remainder: root + .arena + .alloc(Stmt::Jump(elems_loop, root.arena.alloc([start_1, start_2]))), + }; + + let if_different_lengths = if_false_return_false( + root, + eq_len, + // else + elements_1_stmt(root.arena.alloc( + // + elements_2_stmt(root.arena.alloc( + // + if_pointers_equal_return_true( + root, + ident_ids, + [elements_1, elements_2], + root.arena.alloc( + // + start_1_stmt(root.arena.alloc( + // + start_2_stmt(root.arena.alloc( + // + size_stmt(root.arena.alloc( + // + list_size_stmt(root.arena.alloc( + // + end_1_stmt(root.arena.alloc( + // + joinpoint_loop, + )), + )), + )), + )), + )), + ), + ), + )), + )), + ); + + len_1_stmt(root.arena.alloc( + // + len_2_stmt(root.arena.alloc( + // + eq_len_stmt(root.arena.alloc( + // + if_different_lengths, + )), + )), + )) +} diff --git a/crates/compiler/mono/src/code_gen_help/mod.rs b/crates/compiler/mono/src/code_gen_help/mod.rs new file mode 100644 index 0000000000..f8f76584bc --- /dev/null +++ b/crates/compiler/mono/src/code_gen_help/mod.rs @@ -0,0 +1,1467 @@ +use bumpalo::collections::vec::Vec; +use bumpalo::collections::CollectIn; +use bumpalo::Bump; +use roc_module::low_level::LowLevel; +use roc_module::symbol::{IdentIds, ModuleId, Symbol}; +use roc_target::TargetInfo; + +use crate::ir::{ + BranchInfo, Call, CallSpecId, CallType, Expr, JoinPointId, Literal, ModifyRc, PassedFunction, + Proc, ProcLayout, SelfRecursive, Stmt, UpdateModeId, +}; +use crate::layout::{ + Builtin, InLayout, LambdaName, Layout, LayoutInterner, LayoutRepr, LayoutWrapper, Niche, + STLayoutInterner, UnionLayout, +}; + +mod equality; +mod refcount; + +const LAYOUT_BOOL: InLayout = Layout::BOOL; +const LAYOUT_UNIT: InLayout = Layout::UNIT; + +const ARG_1: Symbol = Symbol::ARG_1; +const ARG_2: Symbol = Symbol::ARG_2; +const ARG_3: Symbol = Symbol::ARG_3; +const ARG_4: Symbol = Symbol::ARG_4; +const ARG_5: Symbol = Symbol::ARG_5; +const ARG_6: Symbol = Symbol::ARG_6; + +/// "Infinite" reference count, for static values +/// Ref counts are encoded as negative numbers where isize::MIN represents 1 +pub const REFCOUNT_MAX: usize = 0; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum HelperOp { + Inc, + Dec, + IndirectInc, + IndirectDec, + DecRef(JoinPointId), + Reset, + ResetRef, + Eq, +} + +impl HelperOp { + fn is_decref(&self) -> bool { + matches!(self, Self::DecRef(_)) + } + + fn is_dec(&self) -> bool { + matches!(self, Self::Dec) + } + + fn is_inc(&self) -> bool { + matches!(self, Self::Inc) + } +} + +#[derive(Debug)] +struct Specialization<'a> { + op: HelperOp, + layout: InLayout<'a>, + symbol: Symbol, + proc: Option>, +} + +#[derive(Debug)] +pub struct Context<'a> { + new_linker_data: Vec<'a, (Symbol, ProcLayout<'a>)>, + recursive_union: Option>, + op: HelperOp, +} + +/// Generate specialized helper procs for code gen +/// ---------------------------------------------- +/// +/// Some low level operations need specialized helper procs to traverse data structures at runtime. +/// This includes refcounting, hashing, and equality checks. +/// +/// For example, when checking List equality, we need to visit each element and compare them. +/// Depending on the type of the list elements, we may need to recurse deeper into each element. +/// For tag unions, we may need branches for different tag IDs, etc. +/// +/// This module creates specialized helper procs for all such operations and types used in the program. +/// +/// The backend drives the process, in two steps: +/// 1) When it sees the relevant node, it calls CodeGenHelp to get the replacement IR. +/// CodeGenHelp returns IR for a call to the helper proc, and remembers the specialization. +/// 2) After the backend has generated code for all user procs, it takes the IR for all of the +/// specialized helpers procs, and generates target code for them too. +/// +pub struct CodeGenHelp<'a> { + arena: &'a Bump, + home: ModuleId, + target_info: TargetInfo, + layout_isize: InLayout<'a>, + specializations: Vec<'a, Specialization<'a>>, + debug_recursion_depth: usize, +} + +impl<'a> CodeGenHelp<'a> { + pub fn new(arena: &'a Bump, target_info: TargetInfo, home: ModuleId) -> Self { + let layout_isize = Layout::isize(target_info); + + CodeGenHelp { + arena, + home, + target_info, + layout_isize, + specializations: Vec::with_capacity_in(16, arena), + debug_recursion_depth: 0, + } + } + + pub fn take_procs(&mut self) -> Vec<'a, Proc<'a>> { + let procs_iter = self + .specializations + .drain(0..) + .map(|spec| spec.proc.unwrap()); + Vec::from_iter_in(procs_iter, self.arena) + } + + // ============================================================================ + // + // CALL GENERATED PROCS + // + // ============================================================================ + + /// Expand a `Refcounting` node to a `Let` node that calls a specialized helper proc. + /// The helper procs themselves are to be generated later with `generate_procs` + pub fn expand_refcount_stmt( + &mut self, + ident_ids: &mut IdentIds, + layout_interner: &mut STLayoutInterner<'a>, + layout: InLayout<'a>, + modify: &ModifyRc, + following: &'a Stmt<'a>, + ) -> (&'a Stmt<'a>, Vec<'a, (Symbol, ProcLayout<'a>)>) { + let op = match modify { + ModifyRc::Inc(..) => HelperOp::Inc, + ModifyRc::Dec(_) => HelperOp::Dec, + ModifyRc::DecRef(_) => { + let jp_decref = JoinPointId(self.create_symbol(ident_ids, "jp_decref")); + HelperOp::DecRef(jp_decref) + } + ModifyRc::Free(_) => unreachable!("free should be handled by the backend directly"), + }; + + let mut ctx = Context { + new_linker_data: Vec::new_in(self.arena), + recursive_union: None, + op, + }; + + let rc_stmt = refcount::refcount_stmt( + self, + ident_ids, + &mut ctx, + layout_interner, + layout, + modify, + following, + ); + (rc_stmt, ctx.new_linker_data) + } + + pub fn call_reset_refcount( + &mut self, + ident_ids: &mut IdentIds, + layout_interner: &mut STLayoutInterner<'a>, + layout: InLayout<'a>, + argument: Symbol, + ) -> (Expr<'a>, Vec<'a, (Symbol, ProcLayout<'a>)>) { + self.call_refcount(ident_ids, layout_interner, layout, argument, false) + } + + /** + Call a resetref operation. It is similar to reset except it does not recursively decrement it's children when unique. + */ + pub fn call_resetref_refcount( + &mut self, + ident_ids: &mut IdentIds, + layout_interner: &mut STLayoutInterner<'a>, + layout: InLayout<'a>, + argument: Symbol, + ) -> (Expr<'a>, Vec<'a, (Symbol, ProcLayout<'a>)>) { + self.call_refcount(ident_ids, layout_interner, layout, argument, true) + } + + /** + Call either a reset or a resetref refcount operation. + */ + fn call_refcount( + &mut self, + ident_ids: &mut IdentIds, + layout_interner: &mut STLayoutInterner<'a>, + layout: InLayout<'a>, + argument: Symbol, + resetref: bool, + ) -> (Expr<'a>, Vec<'a, (Symbol, ProcLayout<'a>)>) { + let mut ctx = Context { + new_linker_data: Vec::new_in(self.arena), + recursive_union: None, + op: if resetref { + HelperOp::ResetRef + } else { + HelperOp::Reset + }, + }; + + let proc_name = self.find_or_create_proc(ident_ids, &mut ctx, layout_interner, layout); + + let arguments = self.arena.alloc([argument]); + let ret_layout = layout; + let arg_layouts = self.arena.alloc([layout]); + let expr = Expr::Call(Call { + call_type: CallType::ByName { + name: LambdaName::no_niche(proc_name), + ret_layout, + arg_layouts, + specialization_id: CallSpecId::BACKEND_DUMMY, + }, + arguments, + }); + + (expr, ctx.new_linker_data) + } + + /// Generate a refcount increment procedure, *without* a Call expression. + /// *This method should be rarely used* - only when the proc is to be called from Zig. + /// Otherwise you want to generate the Proc and the Call together, using another method. + pub fn gen_refcount_proc( + &mut self, + ident_ids: &mut IdentIds, + layout_interner: &mut STLayoutInterner<'a>, + layout: InLayout<'a>, + op: HelperOp, + ) -> (Symbol, Vec<'a, (Symbol, ProcLayout<'a>)>) { + let mut ctx = Context { + new_linker_data: Vec::new_in(self.arena), + recursive_union: None, + op, + }; + + let proc_name = self.find_or_create_proc(ident_ids, &mut ctx, layout_interner, layout); + + (proc_name, ctx.new_linker_data) + } + + /// Replace a generic `Lowlevel::Eq` call with a specialized helper proc. + /// The helper procs themselves are to be generated later with `generate_procs` + pub fn call_specialized_equals( + &mut self, + ident_ids: &mut IdentIds, + layout_interner: &mut STLayoutInterner<'a>, + layout: InLayout<'a>, + arguments: &'a [Symbol], + ) -> (Expr<'a>, Vec<'a, (Symbol, ProcLayout<'a>)>) { + let mut ctx = Context { + new_linker_data: Vec::new_in(self.arena), + recursive_union: None, + op: HelperOp::Eq, + }; + + let expr = self + .call_specialized_op(ident_ids, &mut ctx, layout_interner, layout, arguments) + .unwrap(); + + (expr, ctx.new_linker_data) + } + + // ============================================================================ + // + // CALL SPECIALIZED OP + // + // ============================================================================ + + fn call_specialized_op( + &mut self, + ident_ids: &mut IdentIds, + ctx: &mut Context<'a>, + layout_interner: &mut STLayoutInterner<'a>, + called_layout: InLayout<'a>, + arguments: &'a [Symbol], + ) -> Option> { + use HelperOp::*; + + // debug_assert!(self.debug_recursion_depth < 100); + self.debug_recursion_depth += 1; + + let layout = if matches!( + layout_interner.get_repr(called_layout), + LayoutRepr::RecursivePointer(_) + ) { + let union_layout = ctx.recursive_union.unwrap(); + layout_interner.insert_direct_no_semantic(LayoutRepr::Union(union_layout)) + } else { + called_layout + }; + + if layout_needs_helper_proc(layout_interner, layout, ctx.op) { + let arena = self.arena; + let proc_name = self.find_or_create_proc(ident_ids, ctx, layout_interner, layout); + + let (ret_layout, arg_layouts): (InLayout<'a>, &'a [InLayout<'a>]) = { + let arg = self.replace_rec_ptr(ctx, layout_interner, layout); + let ptr_arg = layout_interner.insert_direct_no_semantic(LayoutRepr::Ptr(arg)); + + match ctx.op { + Dec | DecRef(_) => (LAYOUT_UNIT, self.arena.alloc([arg])), + Reset | ResetRef => (layout, self.arena.alloc([layout])), + Inc => (LAYOUT_UNIT, self.arena.alloc([arg, self.layout_isize])), + IndirectDec => (LAYOUT_UNIT, arena.alloc([ptr_arg])), + IndirectInc => (LAYOUT_UNIT, arena.alloc([ptr_arg, self.layout_isize])), + Eq => (LAYOUT_BOOL, self.arena.alloc([arg, arg])), + } + }; + + Some(Expr::Call(Call { + call_type: CallType::ByName { + name: LambdaName::no_niche(proc_name), + ret_layout, + arg_layouts, + specialization_id: CallSpecId::BACKEND_DUMMY, + }, + arguments, + })) + } else if ctx.op == HelperOp::Eq { + Some(Expr::Call(Call { + call_type: CallType::LowLevel { + op: LowLevel::Eq, + update_mode: UpdateModeId::BACKEND_DUMMY, + }, + arguments, + })) + } else { + None + } + } + + fn find_or_create_proc( + &mut self, + ident_ids: &mut IdentIds, + ctx: &mut Context<'a>, + layout_interner: &mut STLayoutInterner<'a>, + orig_layout: InLayout<'a>, + ) -> Symbol { + use HelperOp::*; + + let layout = self.replace_rec_ptr(ctx, layout_interner, orig_layout); + + let found = self + .specializations + .iter() + .find(|spec| spec.op == ctx.op && spec.layout == layout); + + if let Some(spec) = found { + return spec.symbol; + } + + // Procs can be recursive, so we need to create the symbol before the body is complete + // But with nested recursion, that means Symbols and Procs can end up in different orders. + // We want the same order, especially for function indices in Wasm. So create an empty slot and fill it in later. + let (proc_symbol, proc_layout) = + self.create_proc_symbol(ident_ids, layout_interner, ctx, layout); + ctx.new_linker_data.push((proc_symbol, proc_layout)); + let spec_index = self.specializations.len(); + self.specializations.push(Specialization { + op: ctx.op, + layout, + symbol: proc_symbol, + proc: None, + }); + + // Recursively generate the body of the Proc and sub-procs + let (ret_layout, body) = match ctx.op { + Inc | Dec | DecRef(_) => ( + LAYOUT_UNIT, + refcount::refcount_generic( + self, + ident_ids, + ctx, + layout_interner, + layout, + Symbol::ARG_1, + ), + ), + IndirectInc | IndirectDec => ( + LAYOUT_UNIT, + refcount::refcount_indirect( + self, + ident_ids, + ctx, + layout_interner, + layout, + Symbol::ARG_1, + ), + ), + Reset => ( + layout, + refcount::refcount_reset_proc_body( + self, + ident_ids, + ctx, + layout_interner, + layout, + Symbol::ARG_1, + ), + ), + ResetRef => ( + layout, + refcount::refcount_resetref_proc_body( + self, + ident_ids, + ctx, + layout_interner, + layout, + Symbol::ARG_1, + ), + ), + Eq => ( + LAYOUT_BOOL, + equality::eq_generic(self, ident_ids, ctx, layout_interner, layout), + ), + }; + + let args: &'a [(InLayout<'a>, Symbol)] = { + let roc_value = (layout, ARG_1); + match ctx.op { + Inc => { + let inc_amount = (self.layout_isize, ARG_2); + self.arena.alloc([roc_value, inc_amount]) + } + Dec | DecRef(_) | Reset | ResetRef => self.arena.alloc([roc_value]), + IndirectInc => { + let ptr_layout = + layout_interner.insert_direct_no_semantic(LayoutRepr::Ptr(layout)); + let inc_amount = (self.layout_isize, ARG_2); + self.arena.alloc([(ptr_layout, ARG_1), inc_amount]) + } + IndirectDec => { + let ptr_layout = + layout_interner.insert_direct_no_semantic(LayoutRepr::Ptr(layout)); + self.arena.alloc([(ptr_layout, ARG_1)]) + } + Eq => self.arena.alloc([roc_value, (layout, ARG_2)]), + } + }; + + self.specializations[spec_index].proc = Some(Proc { + name: LambdaName::no_niche(proc_symbol), + args, + body, + closure_data_layout: None, + ret_layout, + is_self_recursive: SelfRecursive::NotSelfRecursive, + is_erased: false, + }); + + proc_symbol + } + + fn create_proc_symbol( + &self, + ident_ids: &mut IdentIds, + layout_interner: &mut STLayoutInterner<'a>, + ctx: &mut Context<'a>, + layout: InLayout<'a>, + ) -> (Symbol, ProcLayout<'a>) { + let debug_name = format!( + "#help{}_{:?}_{:?}", + self.specializations.len(), + ctx.op, + layout + ) + .replace("Builtin", ""); + let proc_symbol: Symbol = self.create_symbol(ident_ids, &debug_name); + + let proc_layout = match ctx.op { + HelperOp::Inc => ProcLayout { + arguments: self.arena.alloc([layout, self.layout_isize]), + result: LAYOUT_UNIT, + niche: Niche::NONE, + }, + HelperOp::Dec => ProcLayout { + arguments: self.arena.alloc([layout]), + result: LAYOUT_UNIT, + niche: Niche::NONE, + }, + HelperOp::IndirectInc => { + let ptr_layout = layout_interner.insert_direct_no_semantic(LayoutRepr::Ptr(layout)); + + ProcLayout { + arguments: self.arena.alloc([ptr_layout, self.layout_isize]), + result: LAYOUT_UNIT, + niche: Niche::NONE, + } + } + HelperOp::IndirectDec => { + let ptr_layout = layout_interner.insert_direct_no_semantic(LayoutRepr::Ptr(layout)); + + ProcLayout { + arguments: self.arena.alloc([ptr_layout]), + result: LAYOUT_UNIT, + niche: Niche::NONE, + } + } + HelperOp::Reset | HelperOp::ResetRef => ProcLayout { + arguments: self.arena.alloc([layout]), + result: layout, + niche: Niche::NONE, + }, + HelperOp::DecRef(_) => unreachable!("No generated Proc for DecRef"), + HelperOp::Eq => ProcLayout { + arguments: self.arena.alloc([layout, layout]), + result: LAYOUT_BOOL, + niche: Niche::NONE, + }, + }; + + (proc_symbol, proc_layout) + } + + fn create_symbol(&self, ident_ids: &mut IdentIds, debug_name: &str) -> Symbol { + let ident_id = ident_ids.add_str(debug_name); + Symbol::new(self.home, ident_id) + } + + // When creating or looking up Specializations, we need to replace RecursivePointer + // with the particular Union layout it represents at this point in the tree. + // For example if a program uses `RoseTree a : [Tree a (List (RoseTree a))]` + // then it could have both `RoseTree I64` and `RoseTree Str`. In this case it + // needs *two* specializations for `List(RecursivePointer)`, not just one. + fn replace_rec_ptr( + &mut self, + ctx: &Context<'a>, + layout_interner: &mut STLayoutInterner<'a>, + layout: InLayout<'a>, + ) -> InLayout<'a> { + let lay = layout_interner.get_repr(layout); + let semantic = layout_interner.get_semantic(layout); + let repr = match lay { + LayoutRepr::Builtin(Builtin::List(v)) => { + let v = self.replace_rec_ptr(ctx, layout_interner, v); + LayoutRepr::Builtin(Builtin::List(v)) + } + + LayoutRepr::Builtin(_) => return layout, + + LayoutRepr::Struct(field_layouts) => { + let mut new_field_layouts = Vec::with_capacity_in(field_layouts.len(), self.arena); + for f in field_layouts.iter() { + new_field_layouts.push(self.replace_rec_ptr(ctx, layout_interner, *f)); + } + LayoutRepr::Struct(new_field_layouts.into_bump_slice()) + } + + LayoutRepr::Union(UnionLayout::NonRecursive(tags)) => { + let mut new_tags = Vec::with_capacity_in(tags.len(), self.arena); + for fields in tags { + let mut new_fields = Vec::with_capacity_in(fields.len(), self.arena); + for field in fields.iter() { + new_fields.push(self.replace_rec_ptr(ctx, layout_interner, *field)) + } + new_tags.push(new_fields.into_bump_slice()); + } + LayoutRepr::Union(UnionLayout::NonRecursive(new_tags.into_bump_slice())) + } + + LayoutRepr::Union(_) => { + // we always fully unroll recursive types. That means tha when we find a + // recursive tag union we can replace it with the layout + return layout; + } + + LayoutRepr::Ptr(inner) => { + let inner = self.replace_rec_ptr(ctx, layout_interner, inner); + LayoutRepr::Ptr(inner) + } + + LayoutRepr::LambdaSet(lambda_set) => { + return self.replace_rec_ptr(ctx, layout_interner, lambda_set.representation) + } + + // This line is the whole point of the function + LayoutRepr::RecursivePointer(_) => LayoutRepr::Union(ctx.recursive_union.unwrap()), + + LayoutRepr::FunctionPointer(_) => return layout, + + LayoutRepr::Erased(_) => return layout, + }; + + layout_interner.insert(Layout::new(LayoutWrapper::Direct(repr), semantic)) + } + + fn union_tail_recursion_fields( + &self, + union_in_layout: InLayout<'a>, + union: UnionLayout<'a>, + ) -> Option>> { + use UnionLayout::*; + match union { + NonRecursive(_) => None, + + Recursive(tags) => self.union_tail_recursion_fields_help(union_in_layout, tags), + + NonNullableUnwrapped(field_layouts) => { + self.union_tail_recursion_fields_help(union_in_layout, &[field_layouts]) + } + + NullableWrapped { + other_tags: tags, .. + } => self.union_tail_recursion_fields_help(union_in_layout, tags), + + NullableUnwrapped { other_fields, .. } => { + self.union_tail_recursion_fields_help(union_in_layout, &[other_fields]) + } + } + } + + fn union_tail_recursion_fields_help( + &self, + in_layout: InLayout<'a>, + tags: &[&'a [InLayout<'a>]], + ) -> Option>> { + let tailrec_indices = tags + .iter() + .map(|fields| fields.iter().position(|f| *f == in_layout)) + .collect_in::>(self.arena); + + if tailrec_indices.iter().any(|i| i.is_some()) { + None + } else { + Some(tailrec_indices) + } + } +} + +pub struct CallerProc<'a> { + pub proc_symbol: Symbol, + pub proc_layout: ProcLayout<'a>, + pub proc: Proc<'a>, +} + +impl<'a> CallerProc<'a> { + fn create_symbol(home: ModuleId, ident_ids: &mut IdentIds, debug_name: &str) -> Symbol { + let ident_id = ident_ids.add_str(debug_name); + Symbol::new(home, ident_id) + } + + fn create_caller_proc_symbol( + home: ModuleId, + ident_ids: &mut IdentIds, + operation: &str, + wrapped_function: Symbol, + ) -> Symbol { + let debug_name = format!("#help_{}_{}_{:?}", "caller", operation, wrapped_function,); + + Self::create_symbol(home, ident_ids, &debug_name) + } + + pub fn new_list_map( + arena: &'a Bump, + home: ModuleId, + ident_ids: &mut IdentIds, + layout_interner: &mut STLayoutInterner<'a>, + passed_function: &PassedFunction<'a>, + capture_layout: Option>, + ) -> Self { + assert!(passed_function.argument_layouts.len() <= 4); + const ARG_SYMBOLS: &[Symbol] = &[ARG_1, ARG_2, ARG_3, ARG_4, ARG_5, ARG_6]; + + let argument_layouts = match capture_layout { + None => passed_function.argument_layouts, + Some(_capture_layout) => { + let capture_layout_index = passed_function.argument_layouts.len() - 1; + + #[cfg(debug_assertions)] + { + let passed_capture_layout = + passed_function.argument_layouts[capture_layout_index]; + let repr = layout_interner.get_repr(passed_capture_layout); + + if let LayoutRepr::LambdaSet(lambda_set) = repr { + assert!(layout_interner + .equiv(_capture_layout, lambda_set.runtime_representation())); + } else { + panic!("unexpected layout for capture argument"); + } + } + + &passed_function.argument_layouts[..capture_layout_index] + } + }; + + let capture_symbol = ARG_SYMBOLS[0]; + let return_symbol = ARG_SYMBOLS[1 + argument_layouts.len()]; + + let mut ctx = Context { + new_linker_data: Vec::new_in(arena), + recursive_union: None, + op: HelperOp::Eq, + }; + + let ptr_capture_layout = if let Some(capture_layout) = capture_layout { + layout_interner.insert_direct_no_semantic(LayoutRepr::Ptr(capture_layout)) + } else { + layout_interner.insert_direct_no_semantic(LayoutRepr::Ptr(Layout::UNIT)) + }; + + let it = argument_layouts + .iter() + .map(|l| layout_interner.insert_direct_no_semantic(LayoutRepr::Ptr(*l))); + let ptr_argument_layouts = Vec::from_iter_in(it, arena); + + let ptr_return_layout = layout_interner + .insert_direct_no_semantic(LayoutRepr::Ptr(passed_function.return_layout)); + + let mut arguments = Vec::with_capacity_in(1 + ptr_argument_layouts.len() + 1, arena); + arguments.push(ptr_capture_layout); + arguments.extend(ptr_argument_layouts.iter().copied()); + arguments.push(ptr_return_layout); + + let proc_layout = ProcLayout { + arguments: arguments.into_bump_slice(), + result: Layout::UNIT, + niche: Niche::NONE, + }; + + let proc_symbol = + Self::create_caller_proc_symbol(home, ident_ids, "map", passed_function.name.name()); + + ctx.new_linker_data.push((proc_symbol, proc_layout)); + + let load_capture = Expr::ptr_load(arena.alloc(capture_symbol)); + + let loaded_capture = Self::create_symbol(home, ident_ids, "loaded_capture"); + let call_result = Self::create_symbol(home, ident_ids, "call_result"); + let unit_symbol = Self::create_symbol(home, ident_ids, "unit_symbol"); + let ignored = Self::create_symbol(home, ident_ids, "ignored"); + + let loaded_arguments = Vec::from_iter_in( + (0..argument_layouts.len()) + .map(|i| Self::create_symbol(home, ident_ids, &format!("loaded_argument_{i}"))), + arena, + ); + + let mut arguments = loaded_arguments.clone(); + if capture_layout.is_some() { + arguments.push(loaded_capture); + } + + let call = Expr::Call(Call { + call_type: CallType::ByName { + name: passed_function.name, + ret_layout: passed_function.return_layout, + arg_layouts: passed_function.argument_layouts, + specialization_id: passed_function.specialization_id, + }, + arguments: arguments.into_bump_slice(), + }); + + let ptr_write = Expr::Call(Call { + call_type: CallType::LowLevel { + op: LowLevel::PtrStore, + update_mode: UpdateModeId::BACKEND_DUMMY, + }, + arguments: arena.alloc([return_symbol, call_result]), + }); + + let mut body = Stmt::Let( + call_result, + call, + passed_function.return_layout, + arena.alloc(Stmt::Let( + ignored, + ptr_write, + ptr_return_layout, + arena.alloc(Stmt::Let( + unit_symbol, + Expr::Struct(&[]), + Layout::UNIT, + arena.alloc(Stmt::Ret(unit_symbol)), + )), + )), + ); + + let it = loaded_arguments + .iter() + .zip(ARG_SYMBOLS.iter().skip(1)) + .zip(argument_layouts.iter()) + .rev(); + + for ((loaded_argument, load_argument), argument_layout) in it { + let load_argument = Expr::ptr_load(arena.alloc(load_argument)); + + body = Stmt::Let( + *loaded_argument, + load_argument, + *argument_layout, + arena.alloc(body), + ); + } + + if let Some(capture_layout) = capture_layout { + body = Stmt::Let( + loaded_capture, + load_capture, + capture_layout, + arena.alloc(body), + ); + } + + let mut arg_symbols = ARG_SYMBOLS.iter(); + let mut args = Vec::with_capacity_in(1 + ptr_argument_layouts.len() + 1, arena); + + args.push((ptr_capture_layout, *arg_symbols.next().unwrap())); + for l in &ptr_argument_layouts { + args.push((*l, *arg_symbols.next().unwrap())); + } + args.push((ptr_return_layout, *arg_symbols.next().unwrap())); + + let proc = Proc { + name: LambdaName::no_niche(proc_symbol), + args: args.into_bump_slice(), + body, + closure_data_layout: None, + ret_layout: Layout::UNIT, + is_self_recursive: SelfRecursive::NotSelfRecursive, + is_erased: false, + }; + + if false { + home.register_debug_idents(ident_ids); + println!("{}", proc.to_pretty(layout_interner, 200, true)); + } + + Self { + proc_symbol, + proc_layout, + proc, + } + } + + pub fn new_compare( + arena: &'a Bump, + home: ModuleId, + ident_ids: &mut IdentIds, + layout_interner: &mut STLayoutInterner<'a>, + passed_function: &PassedFunction<'a>, + capture_layout: Option>, + ) -> Self { + const ARG_SYMBOLS: &[Symbol] = &[ARG_1, ARG_2, ARG_3]; + + let argument_layouts = match capture_layout { + None => passed_function.argument_layouts, + Some(_) => &passed_function.argument_layouts[1..], + }; + + let capture_symbol = ARG_SYMBOLS[0]; + + let mut ctx = Context { + new_linker_data: Vec::new_in(arena), + recursive_union: None, + op: HelperOp::Eq, + }; + + let ptr_capture_layout = if let Some(capture_layout) = capture_layout { + layout_interner.insert_direct_no_semantic(LayoutRepr::Ptr(capture_layout)) + } else { + layout_interner.insert_direct_no_semantic(LayoutRepr::Ptr(Layout::UNIT)) + }; + + let it = argument_layouts + .iter() + .map(|l| layout_interner.insert_direct_no_semantic(LayoutRepr::Ptr(*l))); + let ptr_argument_layouts = Vec::from_iter_in(it, arena); + + let mut arguments = Vec::with_capacity_in(1 + ptr_argument_layouts.len(), arena); + arguments.push(ptr_capture_layout); + arguments.extend(ptr_argument_layouts.iter().copied()); + + let proc_layout = ProcLayout { + arguments: arguments.into_bump_slice(), + result: Layout::UNIT, + niche: Niche::NONE, + }; + + let proc_symbol = Self::create_caller_proc_symbol( + home, + ident_ids, + "compare", + passed_function.name.name(), + ); + + ctx.new_linker_data.push((proc_symbol, proc_layout)); + + let load_capture = Expr::ptr_load(arena.alloc(capture_symbol)); + + let loaded_capture = Self::create_symbol(home, ident_ids, "loaded_capture"); + let call_result = Self::create_symbol(home, ident_ids, "call_result"); + + let loaded_arguments = Vec::from_iter_in( + (0..argument_layouts.len()) + .map(|i| Self::create_symbol(home, ident_ids, &format!("loaded_argument_{i}"))), + arena, + ); + + let mut arguments = loaded_arguments.clone(); + if capture_layout.is_some() { + arguments.push(loaded_capture); + } + + let call = Expr::Call(Call { + call_type: CallType::ByName { + name: passed_function.name, + ret_layout: passed_function.return_layout, + arg_layouts: passed_function.argument_layouts, + specialization_id: passed_function.specialization_id, + }, + arguments: arguments.into_bump_slice(), + }); + + let mut body = Stmt::Let( + call_result, + call, + passed_function.return_layout, + arena.alloc(Stmt::Ret(call_result)), + ); + + let it = loaded_arguments + .iter() + .zip(ARG_SYMBOLS.iter().skip(1)) + .zip(argument_layouts.iter()) + .rev(); + + for ((loaded_argument, load_argument), argument_layout) in it { + let load_argument = Expr::ptr_load(arena.alloc(load_argument)); + + body = Stmt::Let( + *loaded_argument, + load_argument, + *argument_layout, + arena.alloc(body), + ); + } + + if let Some(capture_layout) = capture_layout { + body = Stmt::Let( + loaded_capture, + load_capture, + capture_layout, + arena.alloc(body), + ); + } + + let mut arg_symbols = ARG_SYMBOLS.iter(); + let mut args = Vec::with_capacity_in(1 + ptr_argument_layouts.len(), arena); + + args.push((ptr_capture_layout, *arg_symbols.next().unwrap())); + for l in &ptr_argument_layouts { + args.push((*l, *arg_symbols.next().unwrap())); + } + + let proc = Proc { + name: LambdaName::no_niche(proc_symbol), + args: args.into_bump_slice(), + body, + closure_data_layout: None, + ret_layout: Layout::BOOL, + is_self_recursive: SelfRecursive::NotSelfRecursive, + is_erased: false, + }; + + if false { + home.register_debug_idents(ident_ids); + println!("{}", proc.to_pretty(layout_interner, 200, true)); + } + + Self { + proc_symbol, + proc_layout, + proc, + } + } +} + +fn let_lowlevel<'a>( + arena: &'a Bump, + result_layout: InLayout<'a>, + result: Symbol, + op: LowLevel, + arguments: &[Symbol], + next: &'a Stmt<'a>, +) -> Stmt<'a> { + Stmt::Let( + result, + Expr::Call(Call { + call_type: CallType::LowLevel { + op, + update_mode: UpdateModeId::BACKEND_DUMMY, + }, + arguments: arena.alloc_slice_copy(arguments), + }), + result_layout, + next, + ) +} + +fn layout_needs_helper_proc<'a>( + layout_interner: &STLayoutInterner<'a>, + layout: InLayout<'a>, + op: HelperOp, +) -> bool { + match layout_interner.get_repr(layout) { + LayoutRepr::Builtin( + Builtin::Int(_) | Builtin::Float(_) | Builtin::Bool | Builtin::Decimal, + ) => false, + LayoutRepr::Builtin(Builtin::Str) => { + // Str type can use either Zig functions or generated IR, since it's not generic. + // Eq uses a Zig function, refcount uses generated IR. + // Both are fine, they were just developed at different times. + matches!(op, HelperOp::Inc | HelperOp::Dec | HelperOp::DecRef(_)) + } + LayoutRepr::Builtin(Builtin::List(_)) => true, + LayoutRepr::Struct { .. } => true, // note: we do generate a helper for Unit, with just a Stmt::Ret + LayoutRepr::Union(UnionLayout::NonRecursive(tags)) => !tags.is_empty(), + LayoutRepr::Union(_) => true, + LayoutRepr::LambdaSet(_) => true, + LayoutRepr::RecursivePointer(_) => false, + LayoutRepr::Ptr(_) => false, + LayoutRepr::FunctionPointer(_) => false, + LayoutRepr::Erased(_) => true, + } +} + +pub fn test_helper<'a>( + env: &CodeGenHelp<'a>, + ident_ids: &mut IdentIds, + layout_interner: &mut STLayoutInterner<'a>, + main_proc: &Proc<'a>, +) -> Proc<'a> { + let name = LambdaName::no_niche(env.create_symbol(ident_ids, "test_main")); + + let it = (0..main_proc.args.len()).map(|i| env.create_symbol(ident_ids, &format!("arg_{i}"))); + let arguments = Vec::from_iter_in(it, env.arena).into_bump_slice(); + + let it = arguments + .iter() + .zip(main_proc.args.iter()) + .map(|(s, (l, _))| (*l, *s)); + let args = Vec::from_iter_in(it, env.arena).into_bump_slice(); + + // tag: u64, + // error_msg: *mut RocStr, + // value: MaybeUninit, + let fields = [Layout::U64, Layout::U64, main_proc.ret_layout]; + let repr = LayoutRepr::Struct(env.arena.alloc(fields)); + let output_layout = layout_interner.insert_direct_no_semantic(repr); + let body = test_helper_body( + env, + ident_ids, + layout_interner, + main_proc, + arguments, + output_layout, + ); + + Proc { + name, + args, + body, + closure_data_layout: None, + ret_layout: output_layout, + is_self_recursive: main_proc.is_self_recursive, + is_erased: false, + } +} + +fn test_helper_body<'a>( + env: &CodeGenHelp<'a>, + ident_ids: &mut IdentIds, + layout_interner: &mut STLayoutInterner<'a>, + main_proc: &Proc<'a>, + arguments: &'a [Symbol], + output_layout: InLayout<'a>, +) -> Stmt<'a> { + // let buffer = SetLongJmpBuffer + let buffer_symbol = env.create_symbol(ident_ids, "buffer"); + let buffer_expr = Expr::Call(Call { + call_type: CallType::LowLevel { + op: LowLevel::SetLongJmpBuffer, + update_mode: UpdateModeId::BACKEND_DUMMY, + }, + arguments: &[], + }); + let buffer_stmt = |next| Stmt::Let(buffer_symbol, buffer_expr, Layout::U64, next); + + let field_layouts = env.arena.alloc([Layout::U64, Layout::U64]); + let ret_layout = layout_interner.insert_direct_no_semantic(LayoutRepr::Struct(field_layouts)); + + let setjmp_symbol = env.create_symbol(ident_ids, "setjmp"); + let setjmp_expr = Expr::Call(Call { + call_type: CallType::LowLevel { + op: LowLevel::SetJmp, + update_mode: UpdateModeId::BACKEND_DUMMY, + }, + arguments: env.arena.alloc([buffer_symbol]), + }); + let setjmp_stmt = |next| Stmt::Let(setjmp_symbol, setjmp_expr, ret_layout, next); + + let is_longjmp_symbol = env.create_symbol(ident_ids, "is_longjmp"); + let is_longjmp_expr = Expr::StructAtIndex { + index: 0, + field_layouts, + structure: setjmp_symbol, + }; + let is_longjmp_stmt = |next| Stmt::Let(is_longjmp_symbol, is_longjmp_expr, Layout::U64, next); + + let tag_symbol = env.create_symbol(ident_ids, "tag"); + let tag_expr = Expr::StructAtIndex { + index: 1, + field_layouts, + structure: setjmp_symbol, + }; + let tag_stmt = |next| Stmt::Let(tag_symbol, tag_expr, Layout::U64, next); + + // normal path, no panics + let if_zero_stmt = { + let it = main_proc.args.iter().map(|(a, _)| *a); + let arg_layouts = Vec::from_iter_in(it, env.arena).into_bump_slice(); + + let result_symbol = env.create_symbol(ident_ids, "result"); + let result_expr = Expr::Call(Call { + call_type: CallType::ByName { + name: main_proc.name, + ret_layout: main_proc.ret_layout, + arg_layouts, + specialization_id: CallSpecId::BACKEND_DUMMY, + }, + arguments, + }); + let result = |next| Stmt::Let(result_symbol, result_expr, main_proc.ret_layout, next); + + let ok_tag_symbol = env.create_symbol(ident_ids, "ok_tag"); + let ok_tag_expr = Expr::Literal(Literal::Int((0i128).to_ne_bytes())); + let ok_tag = |next| Stmt::Let(ok_tag_symbol, ok_tag_expr, Layout::U64, next); + + let msg_ptr_symbol = env.create_symbol(ident_ids, "msg_ptr"); + let msg_ptr_expr = Expr::Literal(Literal::Int((0i128).to_ne_bytes())); + let msg_ptr = |next| Stmt::Let(msg_ptr_symbol, msg_ptr_expr, Layout::U64, next); + + // construct the record + let output_symbol = env.create_symbol(ident_ids, "output_ok"); + let fields = [ok_tag_symbol, msg_ptr_symbol, result_symbol]; + let output_expr = Expr::Struct(env.arena.alloc(fields)); + let output = |next| Stmt::Let(output_symbol, output_expr, output_layout, next); + + let arena = env.arena; + result(arena.alloc( + // + ok_tag(arena.alloc( + // + msg_ptr(arena.alloc( + // + output(arena.alloc( + // + Stmt::Ret(output_symbol), + )), + )), + )), + )) + }; + + // a longjmp/panic occurred + let if_nonzero_stmt = { + let alloca_symbol = env.create_symbol(ident_ids, "alloca"); + let alloca_expr = Expr::Alloca { + element_layout: main_proc.ret_layout, + initializer: None, + }; + let alloca = |next| Stmt::Let(alloca_symbol, alloca_expr, Layout::U64, next); + + let load_symbol = env.create_symbol(ident_ids, "load"); + let load_expr = Expr::Call(Call { + call_type: CallType::LowLevel { + op: LowLevel::PtrLoad, + update_mode: UpdateModeId::BACKEND_DUMMY, + }, + arguments: env.arena.alloc([alloca_symbol]), + }); + let load = |next| Stmt::Let(load_symbol, load_expr, main_proc.ret_layout, next); + + // construct the record + let output_symbol = env.create_symbol(ident_ids, "output_err"); + // is_longjmp_symbol is a pointer to the error message + let fields = [tag_symbol, is_longjmp_symbol, load_symbol]; + let output_expr = Expr::Struct(env.arena.alloc(fields)); + let output = |next| Stmt::Let(output_symbol, output_expr, output_layout, next); + + let arena = env.arena; + arena.alloc(alloca(arena.alloc( + // + load(arena.alloc( + // + output(arena.alloc( + // + Stmt::Ret(output_symbol), + )), + )), + ))) + }; + + buffer_stmt(env.arena.alloc( + // + setjmp_stmt(env.arena.alloc( + // + is_longjmp_stmt(env.arena.alloc( + // + tag_stmt(env.arena.alloc( + // + switch_if_zero_else( + env.arena, + is_longjmp_symbol, + output_layout, + if_zero_stmt, + if_nonzero_stmt, + ), + )), + )), + )), + )) +} + +pub fn repl_helper<'a>( + env: &CodeGenHelp<'a>, + ident_ids: &mut IdentIds, + layout_interner: &mut STLayoutInterner<'a>, + main_proc: &Proc<'a>, +) -> Proc<'a> { + let name = LambdaName::no_niche(env.create_symbol(ident_ids, "test_main")); + + // NOTE: main_proc's arguments are ignored here. There can be arguments if the input on the + // repl is a lambda, but then we don't read the output of the evaluation anyway (and just print + // the function type) + + // tag: u64, + // error_msg: *mut RocStr, + // value: MaybeUninit, + let fields = [Layout::U64, Layout::U64, main_proc.ret_layout]; + let repr = LayoutRepr::Struct(env.arena.alloc(fields)); + let output_layout = layout_interner.insert_direct_no_semantic(repr); + + let argument_layout = layout_interner.insert_direct_no_semantic(LayoutRepr::Ptr(output_layout)); + let argument_symbol = env.create_symbol(ident_ids, "output"); + let argument = (argument_layout, argument_symbol); + + let args = &env.arena.alloc([argument])[..]; + + let body = repl_helper_body( + env, + ident_ids, + layout_interner, + main_proc, + argument_symbol, + output_layout, + ); + + Proc { + name, + args, + body, + closure_data_layout: None, + ret_layout: Layout::UNIT, + is_self_recursive: main_proc.is_self_recursive, + is_erased: false, + } +} + +fn repl_helper_body<'a>( + env: &CodeGenHelp<'a>, + ident_ids: &mut IdentIds, + layout_interner: &mut STLayoutInterner<'a>, + main_proc: &Proc<'a>, + output_symbol: Symbol, + output_layout: InLayout<'a>, +) -> Stmt<'a> { + // let buffer = SetLongJmpBuffer + let buffer_symbol = env.create_symbol(ident_ids, "buffer"); + let buffer_expr = Expr::Call(Call { + call_type: CallType::LowLevel { + op: LowLevel::SetLongJmpBuffer, + update_mode: UpdateModeId::BACKEND_DUMMY, + }, + arguments: &[], + }); + let buffer_stmt = |next| Stmt::Let(buffer_symbol, buffer_expr, Layout::U64, next); + + let field_layouts = env.arena.alloc([Layout::U64, Layout::U64]); + let ret_layout = layout_interner.insert_direct_no_semantic(LayoutRepr::Struct(field_layouts)); + + let setjmp_symbol = env.create_symbol(ident_ids, "setjmp"); + let setjmp_expr = Expr::Call(Call { + call_type: CallType::LowLevel { + op: LowLevel::SetJmp, + update_mode: UpdateModeId::BACKEND_DUMMY, + }, + arguments: env.arena.alloc([buffer_symbol]), + }); + let setjmp_stmt = |next| Stmt::Let(setjmp_symbol, setjmp_expr, ret_layout, next); + + let is_longjmp_symbol = env.create_symbol(ident_ids, "is_longjmp"); + let is_longjmp_expr = Expr::StructAtIndex { + index: 0, + field_layouts, + structure: setjmp_symbol, + }; + let is_longjmp_stmt = |next| Stmt::Let(is_longjmp_symbol, is_longjmp_expr, Layout::U64, next); + + let tag_symbol = env.create_symbol(ident_ids, "tag"); + let tag_expr = Expr::StructAtIndex { + index: 1, + field_layouts, + structure: setjmp_symbol, + }; + let tag_stmt = |next| Stmt::Let(tag_symbol, tag_expr, Layout::U64, next); + + // normal path, no panics + let if_zero_stmt = { + let it = main_proc.args.iter().map(|(a, _)| *a); + let arg_layouts = Vec::from_iter_in(it, env.arena).into_bump_slice(); + + let result_symbol = env.create_symbol(ident_ids, "result"); + let result_expr = Expr::Call(Call { + call_type: CallType::ByName { + name: main_proc.name, + ret_layout: main_proc.ret_layout, + arg_layouts, + specialization_id: CallSpecId::BACKEND_DUMMY, + }, + arguments: &[], + }); + let result = |next| Stmt::Let(result_symbol, result_expr, main_proc.ret_layout, next); + + let ok_tag_symbol = env.create_symbol(ident_ids, "ok_tag"); + let ok_tag_expr = Expr::Literal(Literal::Int((0i128).to_ne_bytes())); + let ok_tag = |next| Stmt::Let(ok_tag_symbol, ok_tag_expr, Layout::U64, next); + + let msg_ptr_symbol = env.create_symbol(ident_ids, "msg_ptr"); + let msg_ptr_expr = Expr::Literal(Literal::Int((0i128).to_ne_bytes())); + let msg_ptr = |next| Stmt::Let(msg_ptr_symbol, msg_ptr_expr, Layout::U64, next); + + // construct the record + let result_symbol1 = env.create_symbol(ident_ids, "output_ok"); + let fields = [ok_tag_symbol, msg_ptr_symbol, result_symbol]; + let result_expr = Expr::Struct(env.arena.alloc(fields)); + let output = |next| Stmt::Let(result_symbol1, result_expr, output_layout, next); + + let unit_symbol = env.create_symbol(ident_ids, "unit"); + let unit_expr = Expr::ptr_store(env.arena.alloc([output_symbol, result_symbol1])); + let unit = |next| Stmt::Let(unit_symbol, unit_expr, Layout::UNIT, next); + + let arena = env.arena; + result(arena.alloc( + // + ok_tag(arena.alloc( + // + msg_ptr(arena.alloc( + // + output(arena.alloc( + // + unit(arena.alloc( + // + Stmt::Ret(unit_symbol), + )), + )), + )), + )), + )) + }; + + // a longjmp/panic occurred + let if_nonzero_stmt = { + let alloca_symbol = env.create_symbol(ident_ids, "alloca"); + let alloca_expr = Expr::Alloca { + element_layout: main_proc.ret_layout, + initializer: None, + }; + let alloca = |next| Stmt::Let(alloca_symbol, alloca_expr, Layout::U64, next); + + let load_symbol = env.create_symbol(ident_ids, "load"); + let load_expr = Expr::Call(Call { + call_type: CallType::LowLevel { + op: LowLevel::PtrLoad, + update_mode: UpdateModeId::BACKEND_DUMMY, + }, + arguments: env.arena.alloc([alloca_symbol]), + }); + let load = |next| Stmt::Let(load_symbol, load_expr, main_proc.ret_layout, next); + + // construct the record + let result_symbol1 = env.create_symbol(ident_ids, "output_err"); + // is_longjmp_symbol is a pointer to the error message + let fields = [tag_symbol, is_longjmp_symbol, load_symbol]; + let output_expr = Expr::Struct(env.arena.alloc(fields)); + let output = |next| Stmt::Let(result_symbol1, output_expr, output_layout, next); + + let unit_symbol = env.create_symbol(ident_ids, "unit"); + let unit_expr = Expr::ptr_store(env.arena.alloc([output_symbol, result_symbol1])); + let unit = |next| Stmt::Let(unit_symbol, unit_expr, Layout::UNIT, next); + + let arena = env.arena; + arena.alloc(alloca(arena.alloc( + // + load(arena.alloc( + // + output(arena.alloc( + // + unit(arena.alloc( + // + Stmt::Ret(unit_symbol), + )), + )), + )), + ))) + }; + + buffer_stmt(env.arena.alloc( + // + setjmp_stmt(env.arena.alloc( + // + is_longjmp_stmt(env.arena.alloc( + // + tag_stmt(env.arena.alloc( + // + switch_if_zero_else( + env.arena, + is_longjmp_symbol, + Layout::UNIT, + if_zero_stmt, + if_nonzero_stmt, + ), + )), + )), + )), + )) +} + +fn switch_if_zero_else<'a>( + arena: &'a Bump, + condition_symbol: Symbol, + return_layout: InLayout<'a>, + then_branch_stmt: Stmt<'a>, + else_branch_stmt: &'a Stmt<'a>, +) -> Stmt<'a> { + let then_branch = (0u64, BranchInfo::None, then_branch_stmt); + let else_branch = (BranchInfo::None, else_branch_stmt); + + Stmt::Switch { + cond_symbol: condition_symbol, + cond_layout: Layout::U64, + branches: &*arena.alloc([then_branch]), + default_branch: else_branch, + ret_layout: return_layout, + } +} diff --git a/crates/compiler/mono/src/code_gen_help/refcount.rs b/crates/compiler/mono/src/code_gen_help/refcount.rs new file mode 100644 index 0000000000..2449977b85 --- /dev/null +++ b/crates/compiler/mono/src/code_gen_help/refcount.rs @@ -0,0 +1,1924 @@ +#![allow(clippy::too_many_arguments)] + +use bumpalo::collections::vec::Vec; +use bumpalo::collections::CollectIn; +use roc_error_macros::todo_lambda_erasure; +use roc_module::low_level::{LowLevel, LowLevel::*}; +use roc_module::symbol::{IdentIds, Symbol}; +use roc_target::PtrWidth; + +use crate::code_gen_help::let_lowlevel; +use crate::ir::{ + BranchInfo, Call, CallType, Expr, JoinPointId, Literal, ModifyRc, Param, Stmt, UpdateModeId, +}; +use crate::layout::{ + Builtin, InLayout, Layout, LayoutInterner, LayoutRepr, STLayoutInterner, TagIdIntType, + UnionLayout, +}; + +use super::{CodeGenHelp, Context, HelperOp}; + +const LAYOUT_BOOL: InLayout = Layout::BOOL; +const LAYOUT_UNIT: InLayout = Layout::UNIT; +const LAYOUT_U32: InLayout = Layout::U32; + +pub fn refcount_stmt<'a>( + root: &mut CodeGenHelp<'a>, + ident_ids: &mut IdentIds, + ctx: &mut Context<'a>, + layout_interner: &mut STLayoutInterner<'a>, + layout: InLayout<'a>, + modify: &ModifyRc, + following: &'a Stmt<'a>, +) -> &'a Stmt<'a> { + let arena = root.arena; + + match modify { + ModifyRc::Inc(structure, amount) => { + let layout_isize = root.layout_isize; + + // Define a constant for the amount to increment + let amount_sym = root.create_symbol(ident_ids, "amount"); + let amount_expr = Expr::Literal(Literal::Int((*amount as i128).to_ne_bytes())); + let amount_stmt = |next| Stmt::Let(amount_sym, amount_expr, layout_isize, next); + + // Call helper proc, passing the Roc structure and constant amount + let call_result_empty = root.create_symbol(ident_ids, "call_result_empty"); + let call_expr = root + .call_specialized_op( + ident_ids, + ctx, + layout_interner, + layout, + arena.alloc([*structure, amount_sym]), + ) + .unwrap(); + + let call_stmt = Stmt::Let(call_result_empty, call_expr, LAYOUT_UNIT, following); + arena.alloc(amount_stmt(arena.alloc(call_stmt))) + } + + ModifyRc::Dec(structure) => { + // Call helper proc, passing the Roc structure + let call_result_empty = root.create_symbol(ident_ids, "call_result_empty"); + let call_expr = root + .call_specialized_op( + ident_ids, + ctx, + layout_interner, + layout, + arena.alloc([*structure]), + ) + .unwrap(); + let call_stmt = Stmt::Let(call_result_empty, call_expr, LAYOUT_UNIT, following); + arena.alloc(call_stmt) + } + + ModifyRc::DecRef(structure) => { + match layout_interner.get_repr(layout) { + // Str has no children, so Dec is the same as DecRef. + LayoutRepr::Builtin(Builtin::Str) => { + ctx.op = HelperOp::Dec; + refcount_stmt( + root, + ident_ids, + ctx, + layout_interner, + layout, + modify, + following, + ) + } + + // Struct and non-recursive Unions are stack-only, so DecRef is a no-op + LayoutRepr::Struct { .. } => following, + LayoutRepr::Union(UnionLayout::NonRecursive(_)) => following, + + // Inline the refcounting code instead of making a function. Don't iterate fields, + // and replace any return statements with jumps to the `following` statement. + _ => match ctx.op { + HelperOp::DecRef(jp_decref) => { + let rc_stmt = refcount_generic( + root, + ident_ids, + ctx, + layout_interner, + layout, + *structure, + ); + let join = Stmt::Join { + id: jp_decref, + parameters: &[], + body: following, + remainder: arena.alloc(rc_stmt), + }; + arena.alloc(join) + } + _ => unreachable!(), + }, + } + } + ModifyRc::Free(_) => { + unreachable!("free should be handled by the backend directly") + } + } +} + +pub fn refcount_indirect<'a>( + root: &mut CodeGenHelp<'a>, + ident_ids: &mut IdentIds, + ctx: &mut Context<'a>, + layout_interner: &mut STLayoutInterner<'a>, + element_layout: InLayout<'a>, + structure: Symbol, +) -> Stmt<'a> { + let arena = root.arena; + + let unit = root.create_symbol(ident_ids, "unit"); + let loaded = root.create_symbol(ident_ids, "loaded"); + + let indirect_op = ctx.op; + let direct_op = match ctx.op { + HelperOp::IndirectInc => HelperOp::Inc, + HelperOp::IndirectDec => HelperOp::Dec, + _ => unreachable!(), + }; + + // we've done the indirection, the inner value shoud be inc- or decremented directly + ctx.op = direct_op; + + let mod_args = refcount_args(root, ctx, loaded); + let opt_mod_expr = + root.call_specialized_op(ident_ids, ctx, layout_interner, element_layout, mod_args); + + // set the op back to indirect ; this is important for correct layout generation + ctx.op = indirect_op; + + if let Some(mod_expr) = opt_mod_expr { + Stmt::Let( + loaded, + Expr::ptr_load(arena.alloc(structure)), + element_layout, + arena.alloc( + // + Stmt::Let( + unit, + mod_expr, + Layout::UNIT, + arena.alloc( + // + Stmt::Ret(unit), + ), + ), + ), + ) + } else { + rc_return_stmt(root, ident_ids, ctx) + } +} + +pub fn refcount_generic<'a>( + root: &mut CodeGenHelp<'a>, + ident_ids: &mut IdentIds, + ctx: &mut Context<'a>, + layout_interner: &mut STLayoutInterner<'a>, + layout: InLayout<'a>, + structure: Symbol, +) -> Stmt<'a> { + match layout_interner.get_repr(layout) { + LayoutRepr::Builtin( + Builtin::Int(_) | Builtin::Float(_) | Builtin::Bool | Builtin::Decimal, + ) + | LayoutRepr::FunctionPointer(_) => { + // Generate a dummy function that immediately returns Unit + // Some higher-order Zig builtins *always* call an RC function on List elements. + rc_return_stmt(root, ident_ids, ctx) + } + LayoutRepr::Builtin(Builtin::Str) => refcount_str(root, ident_ids, ctx), + LayoutRepr::Builtin(Builtin::List(elem_layout)) => refcount_list( + root, + ident_ids, + ctx, + layout_interner, + elem_layout, + structure, + ), + LayoutRepr::Struct(field_layouts) => refcount_struct( + root, + ident_ids, + ctx, + layout_interner, + field_layouts, + structure, + ), + LayoutRepr::Union(union_layout) => refcount_union( + root, + ident_ids, + ctx, + layout_interner, + layout, + union_layout, + structure, + ), + LayoutRepr::LambdaSet(lambda_set) => { + let runtime_layout = lambda_set.representation; + refcount_generic( + root, + ident_ids, + ctx, + layout_interner, + runtime_layout, + structure, + ) + } + LayoutRepr::Erased(_) => { + todo_lambda_erasure!() + } + LayoutRepr::RecursivePointer(_) => unreachable!( + "We should never call a refcounting helper on a RecursivePointer layout directly" + ), + LayoutRepr::Ptr(_) => { + unreachable!("We should never call a refcounting helper on a Ptr layout directly") + } + } +} + +pub fn refcount_reset_proc_body<'a>( + root: &mut CodeGenHelp<'a>, + ident_ids: &mut IdentIds, + ctx: &mut Context<'a>, + layout_interner: &mut STLayoutInterner<'a>, + layout: InLayout<'a>, + structure: Symbol, +) -> Stmt<'a> { + let rc_ptr = root.create_symbol(ident_ids, "rc_ptr"); + let rc = root.create_symbol(ident_ids, "rc"); + let refcount_1 = root.create_symbol(ident_ids, "refcount_1"); + let is_unique = root.create_symbol(ident_ids, "is_unique"); + let addr = root.create_symbol(ident_ids, "addr"); + + let union_layout = match layout_interner.get_repr(layout) { + LayoutRepr::Union(u) => u, + _ => unimplemented!("Reset is only implemented for UnionLayout"), + }; + + // Whenever we recurse into a child layout we will want to Decrement + ctx.op = HelperOp::Dec; + ctx.recursive_union = Some(union_layout); + let recursion_ptr = + layout_interner.insert_direct_no_semantic(LayoutRepr::RecursivePointer(layout)); + + // Reset structure is unique. Decrement its children and return a pointer to the allocation. + let then_stmt = { + use UnionLayout::*; + + let tag_layouts; + let mut null_id = None; + match union_layout { + NonRecursive(tags) => { + tag_layouts = tags; + } + Recursive(tags) => { + tag_layouts = tags; + } + NonNullableUnwrapped(field_layouts) => { + tag_layouts = root.arena.alloc([field_layouts]); + } + NullableWrapped { + other_tags: tags, + nullable_id, + } => { + null_id = Some(nullable_id); + tag_layouts = tags; + } + NullableUnwrapped { + other_fields, + nullable_id, + } => { + null_id = Some(nullable_id as TagIdIntType); + tag_layouts = root.arena.alloc([other_fields]); + } + }; + + let tag_id_layout = union_layout.tag_id_layout(); + + let tag_id_sym = root.create_symbol(ident_ids, "tag_id"); + let tag_id_stmt = |next| { + Stmt::Let( + tag_id_sym, + Expr::GetTagId { + structure, + union_layout, + }, + tag_id_layout, + next, + ) + }; + + let rc_contents_stmt = refcount_union_contents( + root, + ident_ids, + ctx, + layout_interner, + union_layout, + tag_layouts, + null_id, + structure, + tag_id_sym, + tag_id_layout, + Stmt::Ret(addr), + ); + + tag_id_stmt(root.arena.alloc( + // + rc_contents_stmt, + )) + }; + + // Reset structure is not unique. Decrement it and return a NULL pointer. + let else_stmt = { + let decrement_unit = root.create_symbol(ident_ids, "decrement_unit"); + let decrement_expr = root + .call_specialized_op( + ident_ids, + ctx, + layout_interner, + layout, + root.arena.alloc([structure]), + ) + .unwrap(); + let decrement_stmt = |next| Stmt::Let(decrement_unit, decrement_expr, LAYOUT_UNIT, next); + + // Null pointer with union layout + let null = root.create_symbol(ident_ids, "null"); + let null_stmt = |next| Stmt::Let(null, Expr::NullPointer, layout, next); + + decrement_stmt(root.arena.alloc( + // + null_stmt(root.arena.alloc( + // + Stmt::Ret(null), + )), + )) + }; + + let if_stmt = Stmt::Switch { + cond_symbol: is_unique, + cond_layout: LAYOUT_BOOL, + branches: root.arena.alloc([(1, BranchInfo::None, then_stmt)]), + default_branch: (BranchInfo::None, root.arena.alloc(else_stmt)), + ret_layout: layout, + }; + + // Uniqueness test + let is_unique_stmt = { + let_lowlevel( + root.arena, + LAYOUT_BOOL, + is_unique, + Eq, + &[rc, refcount_1], + root.arena.alloc(if_stmt), + ) + }; + + // Constant for unique refcount + let refcount_1_encoded = match root.target_info.ptr_width() { + PtrWidth::Bytes4 => i32::MIN as i128, + PtrWidth::Bytes8 => i64::MIN as i128, + } + .to_ne_bytes(); + let refcount_1_expr = Expr::Literal(Literal::Int(refcount_1_encoded)); + let refcount_1_stmt = Stmt::Let( + refcount_1, + refcount_1_expr, + root.layout_isize, + root.arena.alloc(is_unique_stmt), + ); + + // Refcount value + let rc_expr = Expr::ptr_load(root.arena.alloc(rc_ptr)); + + let rc_stmt = Stmt::Let( + rc, + rc_expr, + root.layout_isize, + root.arena.alloc(refcount_1_stmt), + ); + + let mask_lower_bits = match layout_interner.get_repr(layout) { + LayoutRepr::Union(ul) => ul.stores_tag_id_in_pointer(root.target_info), + _ => false, + }; + + // Refcount pointer + let rc_ptr_stmt = { + rc_ptr_from_data_ptr_help( + root, + ident_ids, + structure, + rc_ptr, + mask_lower_bits, + root.arena.alloc(rc_stmt), + addr, + recursion_ptr, + ) + }; + + rc_ptr_stmt +} + +pub fn refcount_resetref_proc_body<'a>( + root: &mut CodeGenHelp<'a>, + ident_ids: &mut IdentIds, + ctx: &mut Context<'a>, + layout_interner: &mut STLayoutInterner<'a>, + layout: InLayout<'a>, + structure: Symbol, +) -> Stmt<'a> { + let rc_ptr = root.create_symbol(ident_ids, "rc_ptr"); + let rc = root.create_symbol(ident_ids, "rc"); + let refcount_1 = root.create_symbol(ident_ids, "refcount_1"); + let is_unique = root.create_symbol(ident_ids, "is_unique"); + let addr = root.create_symbol(ident_ids, "addr"); + + let union_layout = match layout_interner.get_repr(layout) { + LayoutRepr::Union(u) => u, + _ => unimplemented!("Resetref is only implemented for UnionLayout"), + }; + + // Whenever we recurse into a child layout we will want to Decrement + ctx.op = HelperOp::Dec; + ctx.recursive_union = Some(union_layout); + let recursion_ptr = + layout_interner.insert_direct_no_semantic(LayoutRepr::RecursivePointer(layout)); + + // Reset structure is unique. Return a pointer to the allocation. + let then_stmt = Stmt::Ret(addr); + + // Reset structure is not unique. Decrement it and return a NULL pointer. + let else_stmt = { + // Set up the context for a decref. + let jp_decref = JoinPointId(root.create_symbol(ident_ids, "jp_decref")); + ctx.op = HelperOp::DecRef(jp_decref); + + // Generate the decref code. + let rc_stmt = refcount_generic(root, ident_ids, ctx, layout_interner, layout, structure); + + // Null pointer with union layout + let null = root.create_symbol(ident_ids, "null"); + let null_stmt = |next| Stmt::Let(null, Expr::NullPointer, layout, next); + + // Inline the refcounting code instead of making a function. Don't iterate fields, + // and replace any return statements with jumps to the `following` statement. + let join = Stmt::Join { + id: jp_decref, + parameters: &[], + body: root.arena.alloc(root.arena.alloc( + // + null_stmt(root.arena.alloc( + // + Stmt::Ret(null), + )), + )), + remainder: root.arena.alloc(rc_stmt), + }; + + root.arena.alloc(join) + }; + + let if_stmt = Stmt::if_then_else( + root.arena, + is_unique, + layout, + then_stmt, + root.arena.alloc(else_stmt), + ); + + // Uniqueness test + let is_unique_stmt = { + let_lowlevel( + root.arena, + LAYOUT_BOOL, + is_unique, + Eq, + &[rc, refcount_1], + root.arena.alloc(if_stmt), + ) + }; + + // Constant for unique refcount + let refcount_1_encoded = match root.target_info.ptr_width() { + PtrWidth::Bytes4 => i32::MIN as i128, + PtrWidth::Bytes8 => i64::MIN as i128, + } + .to_ne_bytes(); + let refcount_1_expr = Expr::Literal(Literal::Int(refcount_1_encoded)); + let refcount_1_stmt = Stmt::Let( + refcount_1, + refcount_1_expr, + root.layout_isize, + root.arena.alloc(is_unique_stmt), + ); + + // Refcount value + let rc_expr = Expr::ptr_load(root.arena.alloc(rc_ptr)); + + let rc_stmt = Stmt::Let( + rc, + rc_expr, + root.layout_isize, + root.arena.alloc(refcount_1_stmt), + ); + + let mask_lower_bits = match layout_interner.get_repr(layout) { + LayoutRepr::Union(ul) => ul.stores_tag_id_in_pointer(root.target_info), + _ => false, + }; + + // Refcount pointer + let rc_ptr_stmt = { + rc_ptr_from_data_ptr_help( + root, + ident_ids, + structure, + rc_ptr, + mask_lower_bits, + root.arena.alloc(rc_stmt), + addr, + recursion_ptr, + ) + }; + + rc_ptr_stmt +} + +fn rc_return_stmt<'a>( + root: &CodeGenHelp<'a>, + ident_ids: &mut IdentIds, + ctx: &mut Context<'a>, +) -> Stmt<'a> { + if let HelperOp::DecRef(jp_decref) = ctx.op { + Stmt::Jump(jp_decref, &[]) + } else { + let unit = root.create_symbol(ident_ids, "unit"); + let ret_stmt = root.arena.alloc(Stmt::Ret(unit)); + Stmt::Let(unit, Expr::Struct(&[]), LAYOUT_UNIT, ret_stmt) + } +} + +fn refcount_args<'a>(root: &CodeGenHelp<'a>, ctx: &Context<'a>, structure: Symbol) -> &'a [Symbol] { + if ctx.op == HelperOp::Inc { + // second argument is always `amount`, passed down through the call stack + root.arena.alloc([structure, Symbol::ARG_2]) + } else { + root.arena.alloc([structure]) + } +} + +fn rc_ptr_from_data_ptr_help<'a>( + root: &CodeGenHelp<'a>, + ident_ids: &mut IdentIds, + structure: Symbol, + rc_ptr_sym: Symbol, + mask_lower_bits: bool, + following: &'a Stmt<'a>, + addr_sym: Symbol, + recursion_ptr: InLayout<'a>, +) -> Stmt<'a> { + // symbol of a pointer with any tag id bits cleared + let cleared_sym = if mask_lower_bits { + root.create_symbol(ident_ids, "cleared") + } else { + structure + }; + + let clear_tag_id_expr = Expr::Call(Call { + call_type: CallType::LowLevel { + op: LowLevel::PtrClearTagId, + update_mode: UpdateModeId::BACKEND_DUMMY, + }, + arguments: root.arena.alloc([structure]), + }); + let clear_tag_id_stmt = + |next| Stmt::Let(cleared_sym, clear_tag_id_expr, root.layout_isize, next); + + // Typecast the structure pointer to an integer + // Backends expect a number Layout to choose the right "subtract" instruction + let as_int_expr = Expr::Call(Call { + call_type: CallType::LowLevel { + op: LowLevel::PtrCast, + update_mode: UpdateModeId::BACKEND_DUMMY, + }, + arguments: root.arena.alloc([cleared_sym]), + }); + let as_int_stmt = |next| Stmt::Let(addr_sym, as_int_expr, root.layout_isize, next); + + // Pointer size constant + let ptr_size_sym = root.create_symbol(ident_ids, "ptr_size"); + let ptr_size_expr = Expr::Literal(Literal::Int( + (root.target_info.ptr_width() as i128).to_ne_bytes(), + )); + let ptr_size_stmt = |next| Stmt::Let(ptr_size_sym, ptr_size_expr, root.layout_isize, next); + + // Refcount address + let rc_addr_sym = root.create_symbol(ident_ids, "rc_addr"); + let sub_expr = Expr::Call(Call { + call_type: CallType::LowLevel { + op: LowLevel::NumSubSaturated, + update_mode: UpdateModeId::BACKEND_DUMMY, + }, + arguments: root.arena.alloc([addr_sym, ptr_size_sym]), + }); + let sub_stmt = |next| Stmt::Let(rc_addr_sym, sub_expr, Layout::usize(root.target_info), next); + + // Typecast the refcount address from integer to pointer + let cast_expr = Expr::Call(Call { + call_type: CallType::LowLevel { + op: LowLevel::PtrCast, + update_mode: UpdateModeId::BACKEND_DUMMY, + }, + arguments: root.arena.alloc([rc_addr_sym]), + }); + let cast_stmt = |next| Stmt::Let(rc_ptr_sym, cast_expr, recursion_ptr, next); + + let body = as_int_stmt(root.arena.alloc( + // + ptr_size_stmt(root.arena.alloc( + // + sub_stmt(root.arena.alloc( + // + cast_stmt(root.arena.alloc( + // + following, + )), + )), + )), + )); + + if mask_lower_bits { + clear_tag_id_stmt(root.arena.alloc(body)) + } else { + body + } +} + +enum Pointer { + ToData(Symbol), + #[allow(unused)] + ToRefcount(Symbol), +} + +fn modify_refcount<'a>( + root: &CodeGenHelp<'a>, + ident_ids: &mut IdentIds, + ctx: &mut Context<'a>, + ptr: Pointer, + alignment: u32, + following: &'a Stmt<'a>, +) -> Stmt<'a> { + // Call the relevant Zig lowlevel to actually modify the refcount + let zig_call_result = root.create_symbol(ident_ids, "zig_call_result"); + match ctx.op { + HelperOp::Inc => { + let (op, ptr) = match ptr { + Pointer::ToData(s) => (LowLevel::RefCountIncDataPtr, s), + Pointer::ToRefcount(s) => (LowLevel::RefCountIncRcPtr, s), + }; + + let zig_call_expr = Expr::Call(Call { + call_type: CallType::LowLevel { + op, + update_mode: UpdateModeId::BACKEND_DUMMY, + }, + arguments: root.arena.alloc([ptr, Symbol::ARG_2]), + }); + Stmt::Let(zig_call_result, zig_call_expr, LAYOUT_UNIT, following) + } + + HelperOp::Dec | HelperOp::DecRef(_) => { + debug_assert!(alignment >= root.target_info.ptr_width() as u32); + + let (op, ptr) = match ptr { + Pointer::ToData(s) => (LowLevel::RefCountDecDataPtr, s), + Pointer::ToRefcount(s) => (LowLevel::RefCountDecRcPtr, s), + }; + + let alignment_sym = root.create_symbol(ident_ids, "alignment"); + let alignment_expr = Expr::Literal(Literal::Int((alignment as i128).to_ne_bytes())); + let alignment_stmt = |next| Stmt::Let(alignment_sym, alignment_expr, LAYOUT_U32, next); + + let zig_call_expr = Expr::Call(Call { + call_type: CallType::LowLevel { + op, + update_mode: UpdateModeId::BACKEND_DUMMY, + }, + arguments: root.arena.alloc([ptr, alignment_sym]), + }); + let zig_call_stmt = Stmt::Let(zig_call_result, zig_call_expr, LAYOUT_UNIT, following); + + alignment_stmt(root.arena.alloc( + // + zig_call_stmt, + )) + } + + _ => unreachable!(), + } +} + +/// Generate a procedure to modify the reference count of a Str +fn refcount_str<'a>( + root: &CodeGenHelp<'a>, + ident_ids: &mut IdentIds, + ctx: &mut Context<'a>, +) -> Stmt<'a> { + let arena = root.arena; + let string = Symbol::ARG_1; + let layout_isize = root.layout_isize; + let field_layouts = arena.alloc([Layout::OPAQUE_PTR, layout_isize, layout_isize]); + + // Get the last word as a signed int + let last_word = root.create_symbol(ident_ids, "last_word"); + let last_word_expr = Expr::StructAtIndex { + index: 2, + field_layouts, + structure: string, + }; + let last_word_stmt = |next| Stmt::Let(last_word, last_word_expr, layout_isize, next); + + // Zero + let zero = root.create_symbol(ident_ids, "zero"); + let zero_expr = Expr::Literal(Literal::Int(0i128.to_ne_bytes())); + let zero_stmt = |next| Stmt::Let(zero, zero_expr, layout_isize, next); + + // is_big_str = (last_word >= 0); + // Treat last word as isize so that the small string flag is the same as the sign bit + // (assuming a little-endian target, where the sign bit is in the last byte of the word) + let is_big_str = root.create_symbol(ident_ids, "is_big_str"); + let is_big_str_stmt = |next| { + let_lowlevel( + arena, + LAYOUT_BOOL, + is_big_str, + NumGte, + &[last_word, zero], + next, + ) + }; + + // + // Check for seamless slice + // + + // Get the length field as a signed int + let length = root.create_symbol(ident_ids, "length"); + let length_expr = Expr::StructAtIndex { + index: 1, + field_layouts, + structure: string, + }; + let length_stmt = |next| Stmt::Let(length, length_expr, layout_isize, next); + + let alignment = root.target_info.ptr_width() as u32; + + // let is_slice = lowlevel NumLt length zero + let is_slice = root.create_symbol(ident_ids, "is_slice"); + let is_slice_stmt = + |next| let_lowlevel(arena, LAYOUT_BOOL, is_slice, NumLt, &[length, zero], next); + + // + // Branch on seamless slice vs "real" string + // + + let return_unit = arena.alloc(rc_return_stmt(root, ident_ids, ctx)); + + let one = root.create_symbol(ident_ids, "one"); + let one_expr = Expr::Literal(Literal::Int(1i128.to_ne_bytes())); + let one_stmt = |next| Stmt::Let(one, one_expr, layout_isize, next); + + let data_ptr_int = root.create_symbol(ident_ids, "data_ptr_int"); + let data_ptr_int_stmt = |next| { + let_lowlevel( + arena, + layout_isize, + data_ptr_int, + PtrCast, + &[last_word], + next, + ) + }; + + let data_ptr = root.create_symbol(ident_ids, "data_ptr"); + let data_ptr_stmt = |next| { + let_lowlevel( + arena, + layout_isize, + data_ptr, + NumShiftLeftBy, + &[data_ptr_int, one], + next, + ) + }; + + // when the string is a slice, the capacity field is a pointer to the refcount + let slice_branch = one_stmt(arena.alloc( + // + data_ptr_int_stmt(arena.alloc( + // + data_ptr_stmt(arena.alloc( + // + modify_refcount( + root, + ident_ids, + ctx, + Pointer::ToData(data_ptr), + alignment, + return_unit, + ), + )), + )), + )); + + // Characters pointer for a real string + let string_chars = root.create_symbol(ident_ids, "string_chars"); + let string_chars_expr = Expr::StructAtIndex { + index: 0, + field_layouts, + structure: string, + }; + let string_chars_stmt = |next| Stmt::Let(string_chars, string_chars_expr, layout_isize, next); + + let modify_refcount_stmt = modify_refcount( + root, + ident_ids, + ctx, + Pointer::ToData(string_chars), + alignment, + return_unit, + ); + + let string_branch = arena.alloc( + // + string_chars_stmt(arena.alloc( + // + modify_refcount_stmt, + )), + ); + + let if_slice = Stmt::if_then_else( + root.arena, + is_slice, + Layout::UNIT, + slice_branch, + string_branch, + ); + + // + // JoinPoint for slice vs list + // + + let modify_stmt = length_stmt(arena.alloc( + // + is_slice_stmt(arena.alloc( + // + if_slice, + )), + )); + + let if_big_stmt = Stmt::if_then_else( + root.arena, + is_big_str, + Layout::UNIT, + modify_stmt, + root.arena.alloc(rc_return_stmt(root, ident_ids, ctx)), + ); + + // Combine the statements in sequence + last_word_stmt(arena.alloc( + // + zero_stmt(arena.alloc( + // + is_big_str_stmt(arena.alloc( + // + if_big_stmt, + )), + )), + )) +} + +fn refcount_list<'a>( + root: &mut CodeGenHelp<'a>, + ident_ids: &mut IdentIds, + ctx: &mut Context<'a>, + layout_interner: &mut STLayoutInterner<'a>, + elem_layout: InLayout<'a>, + structure: Symbol, +) -> Stmt<'a> { + let layout_isize = root.layout_isize; + let arena = root.arena; + + // A "Ptr" layout (heap pointer to a single list element) + let ptr_layout = layout_interner.insert_direct_no_semantic(LayoutRepr::Ptr(elem_layout)); + + // + // Check if the list is empty + // + + let len = root.create_symbol(ident_ids, "len"); + let len_stmt = |next| let_lowlevel(arena, layout_isize, len, ListLen, &[structure], next); + + // let zero = 0 + let zero = root.create_symbol(ident_ids, "zero"); + let zero_expr = Expr::Literal(Literal::Int(0i128.to_ne_bytes())); + let zero_stmt = |next| Stmt::Let(zero, zero_expr, layout_isize, next); + + // let is_empty = lowlevel Eq len zero + let is_empty = root.create_symbol(ident_ids, "is_empty"); + let is_empty_stmt = |next| let_lowlevel(arena, LAYOUT_BOOL, is_empty, Eq, &[len, zero], next); + + // + // Check for seamless slice + // + + // let capacity = StructAtIndex 2 structure + let capacity = root.create_symbol(ident_ids, "capacity"); + let list_field_layouts = arena.alloc([ptr_layout, layout_isize, layout_isize]); + let capacity_expr = Expr::StructAtIndex { + index: 2, + field_layouts: list_field_layouts, + structure, + }; + let capacity_stmt = |next| Stmt::Let(capacity, capacity_expr, layout_isize, next); + + // let is_slice = lowlevel NumLt capacity zero + let is_slice = root.create_symbol(ident_ids, "is_slice"); + let is_slice_stmt = + |next| let_lowlevel(arena, LAYOUT_BOOL, is_slice, NumLt, &[capacity, zero], next); + + // + // Branch on slice vs list + // + + // let first_element = StructAtIndex 0 structure + let first_element = root.create_symbol(ident_ids, "first_element"); + let first_element_expr = Expr::StructAtIndex { + index: 0, + field_layouts: list_field_layouts, + structure, + }; + let first_element_stmt = |next| Stmt::Let(first_element, first_element_expr, ptr_layout, next); + + let jp_elements = JoinPointId(root.create_symbol(ident_ids, "jp_elements")); + let data_pointer = root.create_symbol(ident_ids, "data_pointer"); + let param_data_pointer = Param { + symbol: data_pointer, + layout: Layout::OPAQUE_PTR, + }; + + let first_element_pointer = root.create_symbol(ident_ids, "first_element_pointer"); + let param_first_element_pointer = Param { + symbol: first_element_pointer, + layout: Layout::OPAQUE_PTR, + }; + + // one = 1 + let one = root.create_symbol(ident_ids, "one"); + let one_expr = Expr::Literal(Literal::Int(1i128.to_ne_bytes())); + let one_stmt = |next| Stmt::Let(one, one_expr, layout_isize, next); + + let slice_data_pointer = root.create_symbol(ident_ids, "slice_data_pointer"); + let slice_data_pointer_stmt = move |next| { + one_stmt(arena.alloc( + // + let_lowlevel( + arena, + layout_isize, + slice_data_pointer, + LowLevel::NumShiftLeftBy, + &[capacity, one], + arena.alloc(next), + ), + )) + }; + + let slice_branch = slice_data_pointer_stmt( + // + Stmt::Jump( + jp_elements, + arena.alloc([slice_data_pointer, first_element]), + ), + ); + + let list_branch = arena.alloc( + // + Stmt::Jump(jp_elements, arena.alloc([first_element, first_element])), + ); + + let switch_slice_list = arena.alloc(first_element_stmt(arena.alloc( + // + Stmt::if_then_else( + root.arena, + is_slice, + Layout::UNIT, + slice_branch, + arena.alloc(list_branch), + ), + ))); + + // + // modify refcount of the list and its elements + // (elements first, to avoid use-after-free for when decrementing) + // + + let alignment = Ord::max( + root.target_info.ptr_width() as u32, + layout_interner.alignment_bytes(elem_layout), + ); + + let ret_stmt = arena.alloc(rc_return_stmt(root, ident_ids, ctx)); + let mut modify_refcount_stmt = + |ptr| modify_refcount(root, ident_ids, ctx, ptr, alignment, ret_stmt); + + let modify_list = modify_refcount_stmt(Pointer::ToData(data_pointer)); + + let is_relevant_op = ctx.op.is_dec() || ctx.op.is_inc(); + let modify_elems_and_list = if is_relevant_op && layout_interner.is_refcounted(elem_layout) { + refcount_list_elems( + root, + ident_ids, + ctx, + layout_interner, + elem_layout, + LAYOUT_UNIT, + ptr_layout, + len, + first_element_pointer, + modify_list, + ) + } else { + modify_list + }; + + // + // JoinPoint for slice vs list + // + + let joinpoint_elems = Stmt::Join { + id: jp_elements, + parameters: arena.alloc([param_data_pointer, param_first_element_pointer]), + body: arena.alloc(modify_elems_and_list), + remainder: arena.alloc(switch_slice_list), + }; + + // + // Do nothing if the list is empty + // + + let non_empty_branch = arena.alloc( + // + capacity_stmt(arena.alloc( + // + is_slice_stmt(arena.alloc( + // + joinpoint_elems, + )), + )), + ); + + let if_empty_stmt = Stmt::if_then_else( + root.arena, + is_empty, + Layout::UNIT, + rc_return_stmt(root, ident_ids, ctx), + non_empty_branch, + ); + + len_stmt(arena.alloc( + // + zero_stmt(arena.alloc( + // + is_empty_stmt(arena.alloc( + // + if_empty_stmt, + )), + )), + )) +} + +fn refcount_list_elems<'a>( + root: &mut CodeGenHelp<'a>, + ident_ids: &mut IdentIds, + ctx: &mut Context<'a>, + layout_interner: &mut STLayoutInterner<'a>, + elem_layout: InLayout<'a>, + ret_layout: InLayout<'a>, + ptr_layout: InLayout<'a>, + length: Symbol, + elements: Symbol, + following: Stmt<'a>, +) -> Stmt<'a> { + use LowLevel::*; + let layout_isize = root.layout_isize; + let arena = root.arena; + + // Cast to integer + let start = root.create_symbol(ident_ids, "start"); + let start_stmt = |next| let_lowlevel(arena, layout_isize, start, PtrCast, &[elements], next); + + // + // Loop initialisation + // + + // let size = literal int + let elem_size = root.create_symbol(ident_ids, "elem_size"); + let elem_size_expr = Expr::Literal(Literal::Int( + (layout_interner.stack_size(elem_layout) as i128).to_ne_bytes(), + )); + let elem_size_stmt = |next| Stmt::Let(elem_size, elem_size_expr, layout_isize, next); + + // let list_size = len * size + let list_size = root.create_symbol(ident_ids, "list_size"); + let list_size_stmt = |next| { + let_lowlevel( + arena, + layout_isize, + list_size, + NumMul, + &[length, elem_size], + next, + ) + }; + + // let end = start + list_size + let end = root.create_symbol(ident_ids, "end"); + let end_stmt = |next| let_lowlevel(arena, layout_isize, end, NumAdd, &[start, list_size], next); + + // + // Loop name & parameter + // + + let elems_loop = JoinPointId(root.create_symbol(ident_ids, "elems_loop")); + + let addr = root.create_symbol(ident_ids, "addr"); + let param_addr = Param { + symbol: addr, + layout: layout_isize, + }; + + // + // if we haven't reached the end yet... + // + + // Cast integer to pointer + let ptr_symbol = root.create_symbol(ident_ids, "ptr"); + let ptr_stmt = |next| let_lowlevel(arena, ptr_layout, ptr_symbol, PtrCast, &[addr], next); + + // Dereference the pointer to get the current element + let elem = root.create_symbol(ident_ids, "elem"); + let elem_expr = Expr::ptr_load(arena.alloc(ptr_symbol)); + let elem_stmt = |next| Stmt::Let(elem, elem_expr, elem_layout, next); + + // + // Modify element refcount + // + + let mod_elem_unit = root.create_symbol(ident_ids, "mod_elem_unit"); + let mod_elem_args = refcount_args(root, ctx, elem); + let mod_elem_expr = root + .call_specialized_op(ident_ids, ctx, layout_interner, elem_layout, mod_elem_args) + .unwrap(); + let mod_elem_stmt = |next| Stmt::Let(mod_elem_unit, mod_elem_expr, LAYOUT_UNIT, next); + + // + // Next loop iteration + // + + let next_addr = root.create_symbol(ident_ids, "next_addr"); + let next_addr_stmt = |next| { + // + let_lowlevel( + arena, + layout_isize, + next_addr, + NumAddSaturated, + &[addr, elem_size], + next, + ) + }; + + // + // Control flow + // + + let is_end = root.create_symbol(ident_ids, "is_end"); + let is_end_stmt = |next| let_lowlevel(arena, LAYOUT_BOOL, is_end, NumGte, &[addr, end], next); + + let if_end_of_list = Stmt::if_then_else( + arena, + is_end, + ret_layout, + following, + arena.alloc(ptr_stmt(arena.alloc( + // + elem_stmt(arena.alloc( + // + mod_elem_stmt(arena.alloc( + // + next_addr_stmt(arena.alloc( + // + Stmt::Jump(elems_loop, arena.alloc([next_addr])), + )), + )), + )), + ))), + ); + + let joinpoint_loop = Stmt::Join { + id: elems_loop, + parameters: arena.alloc([param_addr]), + body: arena.alloc( + // + is_end_stmt( + // + arena.alloc(if_end_of_list), + ), + ), + remainder: root + .arena + .alloc(Stmt::Jump(elems_loop, arena.alloc([start, end]))), + }; + + start_stmt(arena.alloc( + // + elem_size_stmt(arena.alloc( + // + list_size_stmt(arena.alloc( + // + end_stmt(arena.alloc( + // + joinpoint_loop, + )), + )), + )), + )) +} + +fn refcount_struct<'a>( + root: &mut CodeGenHelp<'a>, + ident_ids: &mut IdentIds, + ctx: &mut Context<'a>, + layout_interner: &mut STLayoutInterner<'a>, + field_layouts: &'a [InLayout<'a>], + structure: Symbol, +) -> Stmt<'a> { + let mut stmt = rc_return_stmt(root, ident_ids, ctx); + + for (i, field_layout) in field_layouts.iter().enumerate().rev() { + if layout_interner.contains_refcounted(*field_layout) { + let field_val = root.create_symbol(ident_ids, &format!("field_val_{i}")); + let field_val_expr = Expr::StructAtIndex { + index: i as u64, + field_layouts, + structure, + }; + let field_val_stmt = |next| Stmt::Let(field_val, field_val_expr, *field_layout, next); + + let mod_unit = root.create_symbol(ident_ids, &format!("mod_field_{i}")); + let mod_args = refcount_args(root, ctx, field_val); + let mod_expr = root + .call_specialized_op(ident_ids, ctx, layout_interner, *field_layout, mod_args) + .unwrap(); + let mod_stmt = |next| Stmt::Let(mod_unit, mod_expr, LAYOUT_UNIT, next); + + stmt = field_val_stmt(root.arena.alloc( + // + mod_stmt(root.arena.alloc( + // + stmt, + )), + )) + } + } + + stmt +} + +fn refcount_union<'a>( + root: &mut CodeGenHelp<'a>, + ident_ids: &mut IdentIds, + ctx: &mut Context<'a>, + layout_interner: &mut STLayoutInterner<'a>, + union_in_layout: InLayout<'a>, + union: UnionLayout<'a>, + structure: Symbol, +) -> Stmt<'a> { + use UnionLayout::*; + + let parent_rec_ptr_layout = ctx.recursive_union; + if !matches!(union, NonRecursive(_)) { + ctx.recursive_union = Some(union); + } + + let body = match union { + NonRecursive(tags) => refcount_union_nonrec( + root, + ident_ids, + ctx, + layout_interner, + union, + tags, + structure, + ), + + Recursive(tags) => { + let tailrec_idx = root.union_tail_recursion_fields(union_in_layout, union); + if let (Some(tail_idx), true) = (tailrec_idx, ctx.op.is_dec()) { + refcount_union_tailrec( + root, + ident_ids, + ctx, + layout_interner, + union, + tags, + None, + tail_idx, + structure, + ) + } else { + refcount_union_rec( + root, + ident_ids, + ctx, + layout_interner, + union, + tags, + None, + structure, + ) + } + } + + NonNullableUnwrapped(field_layouts) => { + // We don't do tail recursion on NonNullableUnwrapped. + // Its RecursionPointer is always nested inside a List, Option, or other sub-layout, since + // a direct RecursionPointer is only possible if there's at least one non-recursive variant. + // This nesting makes it harder to do tail recursion, so we just don't. + let tags = root.arena.alloc([field_layouts]); + refcount_union_rec( + root, + ident_ids, + ctx, + layout_interner, + union, + tags, + None, + structure, + ) + } + + NullableWrapped { + other_tags: tags, + nullable_id, + } => { + let null_id = Some(nullable_id); + let tailrec_idx = root.union_tail_recursion_fields(union_in_layout, union); + if let (Some(tail_idx), true) = (tailrec_idx, ctx.op.is_dec()) { + refcount_union_tailrec( + root, + ident_ids, + ctx, + layout_interner, + union, + tags, + null_id, + tail_idx, + structure, + ) + } else { + refcount_union_rec( + root, + ident_ids, + ctx, + layout_interner, + union, + tags, + null_id, + structure, + ) + } + } + + NullableUnwrapped { + other_fields, + nullable_id, + } => { + let null_id = Some(nullable_id as TagIdIntType); + let tags = root.arena.alloc([other_fields]); + let tailrec_idx = root.union_tail_recursion_fields(union_in_layout, union); + if let (Some(tail_idx), true) = (tailrec_idx, ctx.op.is_dec()) { + refcount_union_tailrec( + root, + ident_ids, + ctx, + layout_interner, + union, + tags, + null_id, + tail_idx, + structure, + ) + } else { + refcount_union_rec( + root, + ident_ids, + ctx, + layout_interner, + union, + tags, + null_id, + structure, + ) + } + } + }; + + ctx.recursive_union = parent_rec_ptr_layout; + + body +} + +fn refcount_union_nonrec<'a>( + root: &mut CodeGenHelp<'a>, + ident_ids: &mut IdentIds, + ctx: &mut Context<'a>, + layout_interner: &mut STLayoutInterner<'a>, + union_layout: UnionLayout<'a>, + tag_layouts: &'a [&'a [InLayout<'a>]], + structure: Symbol, +) -> Stmt<'a> { + let tag_id_layout = union_layout.tag_id_layout(); + + let tag_id_sym = root.create_symbol(ident_ids, "tag_id"); + let tag_id_stmt = |next| { + Stmt::Let( + tag_id_sym, + Expr::GetTagId { + structure, + union_layout, + }, + tag_id_layout, + next, + ) + }; + + let continuation = rc_return_stmt(root, ident_ids, ctx); + + if tag_layouts.is_empty() { + continuation + } else { + let switch_stmt = refcount_union_contents( + root, + ident_ids, + ctx, + layout_interner, + union_layout, + tag_layouts, + None, + structure, + tag_id_sym, + tag_id_layout, + continuation, + ); + + tag_id_stmt(root.arena.alloc( + // + switch_stmt, + )) + } +} + +fn refcount_union_contents<'a>( + root: &mut CodeGenHelp<'a>, + ident_ids: &mut IdentIds, + ctx: &mut Context<'a>, + layout_interner: &mut STLayoutInterner<'a>, + union_layout: UnionLayout<'a>, + tag_layouts: &'a [&'a [InLayout<'a>]], + null_id: Option, + structure: Symbol, + tag_id_sym: Symbol, + tag_id_layout: InLayout<'a>, + next_stmt: Stmt<'a>, +) -> Stmt<'a> { + let jp_contents_modified = JoinPointId(root.create_symbol(ident_ids, "jp_contents_modified")); + let mut tag_branches = Vec::with_capacity_in(tag_layouts.len() + 1, root.arena); + + if let Some(id) = null_id { + let ret = rc_return_stmt(root, ident_ids, ctx); + tag_branches.push((id as u64, BranchInfo::None, ret)); + }; + + for (field_layouts, tag_id) in tag_layouts + .iter() + .zip((0..).filter(|tag_id| !matches!(null_id, Some(id) if tag_id == &id))) + { + // After refcounting the fields, jump to modify the union itself + // (Order is important, to avoid use-after-free for Dec) + let following = Stmt::Jump(jp_contents_modified, &[]); + + let field_layouts = field_layouts + .iter() + .copied() + .enumerate() + .collect_in::>(root.arena) + .into_bump_slice(); + + let fields_stmt = refcount_tag_fields( + root, + ident_ids, + ctx, + layout_interner, + union_layout, + field_layouts, + structure, + tag_id, + following, + ); + + tag_branches.push((tag_id as u64, BranchInfo::None, fields_stmt)); + } + + let default_stmt: Stmt<'a> = tag_branches.pop().unwrap().2; + + let tag_id_switch = Stmt::Switch { + cond_symbol: tag_id_sym, + cond_layout: tag_id_layout, + branches: tag_branches.into_bump_slice(), + default_branch: (BranchInfo::None, root.arena.alloc(default_stmt)), + ret_layout: LAYOUT_UNIT, + }; + + if let UnionLayout::NonRecursive(_) = union_layout { + Stmt::Join { + id: jp_contents_modified, + parameters: &[], + body: root.arena.alloc(next_stmt), + remainder: root.arena.alloc(tag_id_switch), + } + } else { + let is_unique = root.create_symbol(ident_ids, "is_unique"); + + let switch_with_unique_check = Stmt::if_then_else( + root.arena, + is_unique, + Layout::UNIT, + tag_id_switch, + root.arena.alloc(Stmt::Jump(jp_contents_modified, &[])), + ); + + let switch_with_unique_check_and_let = let_lowlevel( + root.arena, + Layout::BOOL, + is_unique, + LowLevel::RefCountIsUnique, + &[structure], + root.arena.alloc(switch_with_unique_check), + ); + + Stmt::Join { + id: jp_contents_modified, + parameters: &[], + body: root.arena.alloc(next_stmt), + remainder: root.arena.alloc(switch_with_unique_check_and_let), + } + } +} + +fn refcount_union_rec<'a>( + root: &mut CodeGenHelp<'a>, + ident_ids: &mut IdentIds, + ctx: &mut Context<'a>, + layout_interner: &mut STLayoutInterner<'a>, + union_layout: UnionLayout<'a>, + tag_layouts: &'a [&'a [InLayout<'a>]], + null_id: Option, + structure: Symbol, +) -> Stmt<'a> { + let tag_id_layout = union_layout.tag_id_layout(); + + let tag_id_sym = root.create_symbol(ident_ids, "tag_id"); + let tag_id_stmt = |next| { + Stmt::Let( + tag_id_sym, + Expr::GetTagId { + structure, + union_layout, + }, + tag_id_layout, + next, + ) + }; + + let rc_structure_stmt = { + let alignment = LayoutRepr::Union(union_layout).allocation_alignment_bytes(layout_interner); + let ret_stmt = rc_return_stmt(root, ident_ids, ctx); + + modify_refcount( + root, + ident_ids, + ctx, + Pointer::ToData(structure), + alignment, + root.arena.alloc(ret_stmt), + ) + }; + + let rc_contents_then_structure = if ctx.op.is_dec() { + refcount_union_contents( + root, + ident_ids, + ctx, + layout_interner, + union_layout, + tag_layouts, + null_id, + structure, + tag_id_sym, + tag_id_layout, + rc_structure_stmt, + ) + } else { + rc_structure_stmt + }; + + if ctx.op.is_decref() && null_id.is_none() { + rc_contents_then_structure + } else { + tag_id_stmt(root.arena.alloc( + // + rc_contents_then_structure, + )) + } +} + +// Refcount a recursive union using tail-call elimination to limit stack growth +fn refcount_union_tailrec<'a>( + root: &mut CodeGenHelp<'a>, + ident_ids: &mut IdentIds, + ctx: &mut Context<'a>, + layout_interner: &mut STLayoutInterner<'a>, + union_layout: UnionLayout<'a>, + tag_layouts: &'a [&'a [InLayout<'a>]], + null_id: Option, + tailrec_indices: Vec<'a, Option>, + initial_structure: Symbol, +) -> Stmt<'a> { + let tailrec_loop = JoinPointId(root.create_symbol(ident_ids, "tailrec_loop")); + let current = root.create_symbol(ident_ids, "current"); + let next_ptr = root.create_symbol(ident_ids, "next_ptr"); + let layout = layout_interner.insert_direct_no_semantic(LayoutRepr::Union(union_layout)); + + let tag_id_layout = union_layout.tag_id_layout(); + + let tag_id_sym = root.create_symbol(ident_ids, "tag_id"); + let tag_id_stmt = |next| { + Stmt::Let( + tag_id_sym, + Expr::GetTagId { + structure: current, + union_layout, + }, + tag_id_layout, + next, + ) + }; + + // Do refcounting on the structure itself + // In the control flow, this comes *after* refcounting the fields + // It receives a `next` parameter to pass through to the outer joinpoint + let rc_structure_stmt = { + let next_addr = root.create_symbol(ident_ids, "next_addr"); + + let exit_stmt = rc_return_stmt(root, ident_ids, ctx); + let jump_to_loop = Stmt::Jump(tailrec_loop, root.arena.alloc([next_ptr])); + + let loop_or_exit = Stmt::Switch { + cond_symbol: next_addr, + cond_layout: root.layout_isize, + branches: root.arena.alloc([(0, BranchInfo::None, exit_stmt)]), + default_branch: (BranchInfo::None, root.arena.alloc(jump_to_loop)), + ret_layout: LAYOUT_UNIT, + }; + let loop_or_exit_based_on_next_addr = { + let_lowlevel( + root.arena, + root.layout_isize, + next_addr, + PtrCast, + &[next_ptr], + root.arena.alloc(loop_or_exit), + ) + }; + + let alignment = layout_interner.allocation_alignment_bytes(layout); + modify_refcount( + root, + ident_ids, + ctx, + Pointer::ToData(current), + alignment, + root.arena.alloc(loop_or_exit_based_on_next_addr), + ) + }; + + let rc_contents_then_structure = { + let jp_modify_union = JoinPointId(root.create_symbol(ident_ids, "jp_modify_union")); + let mut tag_branches = Vec::with_capacity_in(tag_layouts.len() + 1, root.arena); + + // If this is null, there is no refcount, no `next`, no fields. Just return. + if let Some(id) = null_id { + let ret = rc_return_stmt(root, ident_ids, ctx); + tag_branches.push((id as u64, BranchInfo::None, ret)); + } + + for ((field_layouts, opt_tailrec_index), tag_id) in tag_layouts + .iter() + .zip(tailrec_indices) + .zip((0..).filter(|tag_id| !matches!(null_id, Some(id) if tag_id == &id))) + { + // After refcounting the fields, jump to modify the union itself. + // The loop param is a pointer to the next union. It gets passed through two jumps. + let (non_tailrec_fields, jump_to_modify_union) = + if let Some(tailrec_index) = opt_tailrec_index { + let mut filtered = Vec::with_capacity_in(field_layouts.len() - 1, root.arena); + let mut tail_stmt = None; + for (i, field) in field_layouts.iter().enumerate() { + if i != tailrec_index { + filtered.push((i, *field)); + } else { + let field_val = + root.create_symbol(ident_ids, &format!("field_{tag_id}_{i}")); + let field_val_expr = Expr::UnionAtIndex { + union_layout, + tag_id, + index: i as u64, + structure: current, + }; + let jump_params = root.arena.alloc([field_val]); + let jump = root.arena.alloc(Stmt::Jump(jp_modify_union, jump_params)); + tail_stmt = Some(Stmt::Let(field_val, field_val_expr, *field, jump)); + } + } + + (filtered.into_bump_slice(), tail_stmt.unwrap()) + } else { + let null = root.create_symbol(ident_ids, "null"); + let null_stmt = |next| Stmt::Let(null, Expr::NullPointer, layout, next); + + let tail_stmt = null_stmt(root.arena.alloc( + // + Stmt::Jump(jp_modify_union, root.arena.alloc([null])), + )); + + let field_layouts = field_layouts + .iter() + .copied() + .enumerate() + .collect_in::>(root.arena) + .into_bump_slice(); + + (field_layouts, tail_stmt) + }; + + let fields_stmt = refcount_tag_fields( + root, + ident_ids, + ctx, + layout_interner, + union_layout, + non_tailrec_fields, + current, + tag_id, + jump_to_modify_union, + ); + + tag_branches.push((tag_id as u64, BranchInfo::None, fields_stmt)); + } + + let default_stmt: Stmt<'a> = tag_branches.pop().unwrap().2; + + let tag_id_switch = Stmt::Switch { + cond_symbol: tag_id_sym, + cond_layout: tag_id_layout, + branches: tag_branches.into_bump_slice(), + default_branch: (BranchInfo::None, root.arena.alloc(default_stmt)), + ret_layout: LAYOUT_UNIT, + }; + + let is_unique = root.create_symbol(ident_ids, "is_unique"); + let null_pointer = root.create_symbol(ident_ids, "null_pointer"); + + let jump_with_null_ptr = Stmt::Let( + null_pointer, + Expr::NullPointer, + layout_interner.insert_direct_no_semantic(LayoutRepr::Union(union_layout)), + root.arena.alloc(Stmt::Jump( + jp_modify_union, + root.arena.alloc([null_pointer]), + )), + ); + + let switch_with_unique_check = Stmt::if_then_else( + root.arena, + is_unique, + Layout::UNIT, + tag_id_switch, + root.arena.alloc(jump_with_null_ptr), + ); + + let switch_with_unique_check_and_let = let_lowlevel( + root.arena, + Layout::BOOL, + is_unique, + LowLevel::RefCountIsUnique, + &[current], + root.arena.alloc(switch_with_unique_check), + ); + + let jp_param = Param { + symbol: next_ptr, + layout, + }; + + Stmt::Join { + id: jp_modify_union, + parameters: root.arena.alloc([jp_param]), + body: root.arena.alloc(rc_structure_stmt), + remainder: root.arena.alloc(switch_with_unique_check_and_let), + } + }; + + let loop_body = tag_id_stmt(root.arena.alloc( + // + rc_contents_then_structure, + )); + + let loop_init = Stmt::Jump(tailrec_loop, root.arena.alloc([initial_structure])); + let union_layout = layout_interner.insert_direct_no_semantic(LayoutRepr::Union(union_layout)); + let loop_param = Param { + symbol: current, + layout: union_layout, + }; + + Stmt::Join { + id: tailrec_loop, + parameters: root.arena.alloc([loop_param]), + body: root.arena.alloc(loop_body), + remainder: root.arena.alloc(loop_init), + } +} + +fn refcount_tag_fields<'a>( + root: &mut CodeGenHelp<'a>, + ident_ids: &mut IdentIds, + ctx: &mut Context<'a>, + layout_interner: &mut STLayoutInterner<'a>, + union_layout: UnionLayout<'a>, + field_layouts: &'a [(usize, InLayout<'a>)], + structure: Symbol, + tag_id: TagIdIntType, + following: Stmt<'a>, +) -> Stmt<'a> { + let mut stmt = following; + + for (i, field_layout) in field_layouts.iter().rev() { + if layout_interner.contains_refcounted(*field_layout) { + let field_val = root.create_symbol(ident_ids, &format!("field_{tag_id}_{i}")); + let field_val_expr = Expr::UnionAtIndex { + union_layout, + tag_id, + index: *i as u64, + structure, + }; + let field_val_stmt = |next| Stmt::Let(field_val, field_val_expr, *field_layout, next); + + let mod_unit = root.create_symbol(ident_ids, &format!("mod_field_{tag_id}_{i}")); + let mod_args = refcount_args(root, ctx, field_val); + let mod_expr = root + .call_specialized_op(ident_ids, ctx, layout_interner, *field_layout, mod_args) + .unwrap(); + let mod_stmt = |next| Stmt::Let(mod_unit, mod_expr, LAYOUT_UNIT, next); + + stmt = field_val_stmt(root.arena.alloc( + // + mod_stmt(root.arena.alloc( + // + stmt, + )), + )) + } + } + + stmt +} diff --git a/crates/compiler/mono/src/debug.rs b/crates/compiler/mono/src/debug.rs new file mode 100644 index 0000000000..203b5ac2a3 --- /dev/null +++ b/crates/compiler/mono/src/debug.rs @@ -0,0 +1,5 @@ +mod checker; +mod report; + +pub use checker::{check_procs, Problem, Problems}; +pub use report::format_problems; diff --git a/crates/compiler/mono/src/debug/checker.rs b/crates/compiler/mono/src/debug/checker.rs new file mode 100644 index 0000000000..28666e7ced --- /dev/null +++ b/crates/compiler/mono/src/debug/checker.rs @@ -0,0 +1,932 @@ +//! Type-checking of the generated [ir][crate::ir::Proc]. + +use bumpalo::Bump; +use roc_collections::{MutMap, VecMap, VecSet}; +use roc_module::symbol::Symbol; + +use crate::{ + ir::{ + Call, CallSpecId, CallType, ErasedField, Expr, HigherOrderLowLevel, JoinPointId, + ListLiteralElement, ModifyRc, Param, Proc, ProcLayout, Stmt, + }, + layout::{ + Builtin, FunctionPointer, InLayout, Layout, LayoutInterner, LayoutRepr, STLayoutInterner, + TagIdIntType, UnionLayout, + }, +}; + +pub enum UseKind { + Ret, + TagExpr, + TagReuse, + TagPayloadArg, + ListElemExpr, + CallArg, + JumpArg, + CrashArg, + SwitchCond, + ExpectCond, + ExpectLookup, + ErasedMake(ErasedField), + Erased, + FunctionPointer, + Alloca, +} + +pub enum ProblemKind<'a> { + RedefinedSymbol { + symbol: Symbol, + old_line: usize, + }, + NoSymbolInScope { + symbol: Symbol, + }, + SymbolUseMismatch { + symbol: Symbol, + def_layout: InLayout<'a>, + def_line: usize, + use_layout: InLayout<'a>, + use_kind: UseKind, + }, + SymbolDefMismatch { + symbol: Symbol, + def_layout: InLayout<'a>, + expr_layout: InLayout<'a>, + }, + BadSwitchConditionLayout { + found_layout: InLayout<'a>, + }, + DuplicateSwitchBranch {}, + RedefinedJoinPoint { + id: JoinPointId, + old_line: usize, + }, + NoJoinPoint { + id: JoinPointId, + }, + JumpArityMismatch { + def_line: usize, + num_needed: usize, + num_given: usize, + }, + CallingUndefinedProc { + symbol: Symbol, + proc_layout: ProcLayout<'a>, + similar: Vec>, + }, + PtrToUndefinedProc { + symbol: Symbol, + }, + DuplicateCallSpecId { + old_call_line: usize, + }, + StructIndexOOB { + structure: Symbol, + def_line: usize, + index: u64, + size: usize, + }, + NotAStruct { + structure: Symbol, + def_line: usize, + }, + IndexingTagIdNotInUnion { + structure: Symbol, + def_line: usize, + tag_id: u16, + union_layout: InLayout<'a>, + }, + TagUnionStructIndexOOB { + structure: Symbol, + def_line: usize, + tag_id: u16, + index: u64, + size: usize, + }, + IndexIntoNullableTag { + structure: Symbol, + def_line: usize, + tag_id: u16, + union_layout: InLayout<'a>, + }, + UnboxNotABox { + symbol: Symbol, + def_line: usize, + }, + CreatingTagIdNotInUnion { + tag_id: u16, + union_layout: InLayout<'a>, + }, + CreateTagPayloadMismatch { + num_needed: usize, + num_given: usize, + }, + ErasedMakeValueNotBoxed { + symbol: Symbol, + def_layout: InLayout<'a>, + def_line: usize, + }, + ErasedMakeCalleeNotFunctionPointer { + symbol: Symbol, + def_layout: InLayout<'a>, + def_line: usize, + }, + ErasedLoadValueNotBoxed { + symbol: Symbol, + target_layout: InLayout<'a>, + }, + ErasedLoadCalleeNotFunctionPointer { + symbol: Symbol, + target_layout: InLayout<'a>, + }, +} + +pub struct Problem<'a> { + pub proc: &'a Proc<'a>, + pub proc_layout: ProcLayout<'a>, + pub line: usize, + pub kind: ProblemKind<'a>, +} + +type Procs<'a> = MutMap<(Symbol, ProcLayout<'a>), Proc<'a>>; +pub struct Problems<'a>(pub(crate) Vec>); + +impl<'a> Problems<'a> { + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } +} + +pub fn check_procs<'a>( + arena: &'a Bump, + interner: &mut STLayoutInterner<'a>, + procs: &Procs<'a>, +) -> Problems<'a> { + let mut problems = Default::default(); + + for ((_, proc_layout), proc) in procs.iter() { + let mut ctx = Ctx { + arena, + interner, + proc, + proc_layout: *proc_layout, + ret_layout: proc.ret_layout, + problems: &mut problems, + call_spec_ids: Default::default(), + procs, + venv: Default::default(), + joinpoints: Default::default(), + line: 0, + }; + ctx.check_proc(proc); + } + + Problems(problems) +} + +type VEnv<'a> = VecMap)>; +type JoinPoints<'a> = VecMap])>; +type CallSpecIds = VecMap; +struct Ctx<'a, 'r> { + arena: &'a Bump, + interner: &'r mut STLayoutInterner<'a>, + problems: &'r mut Vec>, + proc: &'r Proc<'a>, + proc_layout: ProcLayout<'a>, + procs: &'r Procs<'a>, + call_spec_ids: CallSpecIds, + ret_layout: InLayout<'a>, + venv: VEnv<'a>, + joinpoints: JoinPoints<'a>, + line: usize, +} + +impl<'a, 'r> Ctx<'a, 'r> { + fn problem(&mut self, problem_kind: ProblemKind<'a>) { + self.problems.push(Problem { + proc: self.arena.alloc(self.proc.clone()), + proc_layout: self.proc_layout, + line: self.line, + kind: problem_kind, + }) + } + + fn in_scope(&mut self, f: impl FnOnce(&mut Self) -> T) -> T { + let old_venv = self.venv.clone(); + let r = f(self); + self.venv = old_venv; + r + } + + fn resolve(&self, mut layout: InLayout<'a>) -> InLayout<'a> { + // Note that we are more aggressive than the usual `runtime_representation` + // here because we need strict equality, and so cannot unwrap lambda sets + // lazily. + loop { + layout = self.interner.chase_recursive_in(layout); + match self.interner.get_repr(layout) { + LayoutRepr::LambdaSet(ls) => layout = ls.representation, + _ => return layout, + } + } + } + + fn not_equiv(&mut self, layout1: InLayout<'a>, layout2: InLayout<'a>) -> bool { + !self + .interner + .equiv(self.resolve(layout1), self.resolve(layout2)) + } + + fn insert(&mut self, symbol: Symbol, layout: InLayout<'a>) { + if let Some((old_line, _)) = self.venv.insert(symbol, (self.line, layout)) { + self.problem(ProblemKind::RedefinedSymbol { symbol, old_line }) + } + } + + fn check_sym_exists(&mut self, symbol: Symbol) { + if !self.venv.contains_key(&symbol) { + self.problem(ProblemKind::NoSymbolInScope { symbol }) + } + } + + fn with_sym_layout( + &mut self, + symbol: Symbol, + f: impl FnOnce(&mut Self, usize, InLayout<'a>) -> Option, + ) -> Option { + if let Some(&(def_line, layout)) = self.venv.get(&symbol) { + f(self, def_line, layout) + } else { + self.problem(ProblemKind::NoSymbolInScope { symbol }); + None + } + } + + fn check_sym_layout( + &mut self, + symbol: Symbol, + expected_layout: InLayout<'a>, + use_kind: UseKind, + ) { + if let Some(&(def_line, layout)) = self.venv.get(&symbol) { + if self.not_equiv(layout, expected_layout) { + self.problem(ProblemKind::SymbolUseMismatch { + symbol, + def_layout: layout, + def_line, + use_layout: expected_layout, + use_kind, + }); + } + } else { + self.problem(ProblemKind::NoSymbolInScope { symbol }) + } + } + + fn check_proc(&mut self, proc: &Proc<'a>) { + for (lay, arg) in proc.args.iter() { + self.insert(*arg, *lay); + } + + self.check_stmt(&proc.body) + } + + fn check_stmt(&mut self, body: &Stmt<'a>) { + self.line += 1; + + match body { + Stmt::Let(x, e, x_layout, rest) => { + if let Some(e_layout) = self.check_expr(e, *x_layout) { + if self.not_equiv(e_layout, *x_layout) { + self.problem(ProblemKind::SymbolDefMismatch { + symbol: *x, + def_layout: *x_layout, + expr_layout: e_layout, + }) + } + } + self.insert(*x, *x_layout); + self.check_stmt(rest); + } + Stmt::Switch { + cond_symbol, + cond_layout, + branches, + default_branch, + ret_layout: _, + } => { + self.check_sym_layout(*cond_symbol, *cond_layout, UseKind::SwitchCond); + let layout = self.resolve(*cond_layout); + match self.interner.get_repr(layout) { + LayoutRepr::Builtin(Builtin::Int(_)) => {} + LayoutRepr::Builtin(Builtin::Bool) => {} + _ => self.problem(ProblemKind::BadSwitchConditionLayout { + found_layout: *cond_layout, + }), + } + + // TODO: need to adjust line numbers as we step through, and depending on whether + // the switch is printed as true/false or a proper switch. + let mut seen_branches = VecSet::with_capacity(branches.len()); + for (match_no, _branch_info, branch) in branches.iter() { + if seen_branches.insert(match_no) { + self.problem(ProblemKind::DuplicateSwitchBranch {}); + } + self.in_scope(|ctx| ctx.check_stmt(branch)); + } + let (_branch_info, default_branch) = default_branch; + self.in_scope(|ctx| ctx.check_stmt(default_branch)); + } + &Stmt::Ret(sym) => self.check_sym_layout(sym, self.ret_layout, UseKind::Ret), + &Stmt::Refcounting(rc, rest) => { + self.check_modify_rc(rc); + self.check_stmt(rest); + } + &Stmt::Dbg { remainder, .. } => { + self.check_stmt(remainder); + } + &Stmt::Expect { + condition, + region: _, + lookups, + variables: _, + remainder, + } + | &Stmt::ExpectFx { + condition, + region: _, + lookups, + variables: _, + remainder, + } => { + self.check_sym_layout(condition, Layout::BOOL, UseKind::ExpectCond); + for sym in lookups.iter() { + self.check_sym_exists(*sym); + } + self.check_stmt(remainder); + } + &Stmt::Join { + id, + parameters, + body, + remainder, + } => { + if let Some((old_line, _)) = self.joinpoints.insert(id, (self.line, parameters)) { + self.problem(ProblemKind::RedefinedJoinPoint { id, old_line }) + } + self.in_scope(|ctx| { + for Param { symbol, layout } in parameters { + ctx.insert(*symbol, *layout); + } + ctx.check_stmt(body) + }); + self.line += 1; // `in` line + self.check_stmt(remainder); + } + &Stmt::Jump(id, symbols) => { + if let Some(&(def_line, parameters)) = self.joinpoints.get(&id) { + if symbols.len() != parameters.len() { + self.problem(ProblemKind::JumpArityMismatch { + def_line, + num_needed: parameters.len(), + num_given: symbols.len(), + }); + } + for (arg, param) in symbols.iter().zip(parameters.iter()) { + let Param { symbol: _, layout } = param; + self.check_sym_layout(*arg, *layout, UseKind::JumpArg); + } + } else { + self.problem(ProblemKind::NoJoinPoint { id }); + } + } + &Stmt::Crash(sym, _) => self.check_sym_layout(sym, Layout::STR, UseKind::CrashArg), + } + } + + fn check_expr(&mut self, e: &Expr<'a>, target_layout: InLayout<'a>) -> Option> { + match e { + Expr::Literal(_) => None, + Expr::NullPointer => None, + Expr::Call(call) => self.check_call(call), + &Expr::Tag { + tag_layout, + tag_id, + arguments, + reuse, + } => { + let interned_layout = self + .interner + .insert_direct_no_semantic(LayoutRepr::Union(tag_layout)); + + if let Some(reuse_token) = reuse { + self.check_sym_layout(reuse_token.symbol, interned_layout, UseKind::TagReuse); + } + + self.check_tag_expr(interned_layout, tag_layout, tag_id, arguments); + + Some(interned_layout) + } + Expr::Struct(syms) => { + for sym in syms.iter() { + self.check_sym_exists(*sym); + } + // TODO: pass the field order hash down, so we can check this + None + } + &Expr::StructAtIndex { + index, + // TODO: pass the field order hash down, so we can check this + field_layouts: _, + structure, + } => self.check_struct_at_index(structure, index), + Expr::GetTagId { + structure: _, + union_layout, + } => Some(union_layout.tag_id_layout()), + &Expr::UnionAtIndex { + structure, + tag_id, + union_layout, + index, + } => self.with_sym_layout(structure, |ctx, _def_line, layout| { + ctx.check_union_at_index(structure, layout, union_layout, tag_id, index) + }), + &Expr::GetElementPointer { + structure, + union_layout, + indices, + .. + } => self.with_sym_layout(structure, |ctx, _def_line, layout| { + debug_assert!(indices.len() >= 2); + + ctx.check_union_field_ptr_at_index( + structure, + layout, + union_layout, + indices[0] as _, + indices[1], + ) + }), + Expr::Array { elem_layout, elems } => { + for elem in elems.iter() { + match elem { + ListLiteralElement::Literal(_) => {} + ListLiteralElement::Symbol(sym) => { + self.check_sym_layout(*sym, *elem_layout, UseKind::ListElemExpr) + } + } + } + Some( + self.interner + .insert_direct_no_semantic(LayoutRepr::Builtin(Builtin::List( + *elem_layout, + ))), + ) + } + Expr::EmptyArray => { + // TODO don't know what the element layout is + None + } + &Expr::ErasedMake { value, callee } => Some(self.check_erased_make(value, callee)), + &Expr::ErasedLoad { symbol, field } => { + Some(self.check_erased_load(symbol, field, target_layout)) + } + &Expr::FunctionPointer { lambda_name } => { + let lambda_symbol = lambda_name.name(); + let proc = self.procs.iter().find(|((name, proc), _)| { + *name == lambda_symbol && proc.niche == lambda_name.niche() + }); + match proc { + None => { + self.problem(ProblemKind::PtrToUndefinedProc { + symbol: lambda_symbol, + }); + Some(target_layout) + } + Some(((_, proc_layout), _)) => { + let ProcLayout { + arguments, result, .. + } = proc_layout; + + let fn_ptr = + self.interner + .insert_direct_no_semantic(LayoutRepr::FunctionPointer( + FunctionPointer { + args: arguments, + ret: *result, + }, + )); + + Some(fn_ptr) + } + } + } + &Expr::Reset { + symbol, + update_mode: _, + } + | &Expr::ResetRef { + symbol, + update_mode: _, + } => { + self.check_sym_exists(symbol); + None + } + Expr::Alloca { + initializer, + element_layout, + } => { + if let Some(initializer) = initializer { + self.check_sym_exists(*initializer); + self.check_sym_layout(*initializer, *element_layout, UseKind::Alloca); + } + + None + } + Expr::RuntimeErrorFunction(_) => None, + } + } + + fn check_struct_at_index(&mut self, structure: Symbol, index: u64) -> Option> { + self.with_sym_layout(structure, |ctx, def_line, layout| { + let layout = ctx.resolve(layout); + match ctx.interner.get_repr(layout) { + LayoutRepr::Struct(field_layouts) => { + if index as usize >= field_layouts.len() { + ctx.problem(ProblemKind::StructIndexOOB { + structure, + def_line, + index, + size: field_layouts.len(), + }); + None + } else { + Some(field_layouts[index as usize]) + } + } + _ => { + ctx.problem(ProblemKind::NotAStruct { + structure, + def_line, + }); + None + } + } + }) + } + + fn check_union_at_index( + &mut self, + structure: Symbol, + interned_union_layout: InLayout<'a>, + union_layout: UnionLayout<'a>, + tag_id: u16, + index: u64, + ) -> Option> { + let union = self + .interner + .insert_direct_no_semantic(LayoutRepr::Union(union_layout)); + self.with_sym_layout(structure, |ctx, def_line, _layout| { + ctx.check_sym_layout(structure, union, UseKind::TagExpr); + + match get_tag_id_payloads(union_layout, tag_id) { + TagPayloads::IdNotInUnion => { + ctx.problem(ProblemKind::IndexingTagIdNotInUnion { + structure, + def_line, + tag_id, + union_layout: interned_union_layout, + }); + None + } + TagPayloads::Payloads(payloads) => { + if index as usize >= payloads.len() { + ctx.problem(ProblemKind::TagUnionStructIndexOOB { + structure, + def_line, + tag_id, + index, + size: payloads.len(), + }); + return None; + } + let layout = payloads[index as usize]; + Some(layout) + } + } + }) + } + + fn check_union_field_ptr_at_index( + &mut self, + structure: Symbol, + interned_union_layout: InLayout<'a>, + union_layout: UnionLayout<'a>, + tag_id: u16, + index: u64, + ) -> Option> { + let union = self + .interner + .insert_direct_no_semantic(LayoutRepr::Union(union_layout)); + + let field_ptr_layout = match get_tag_id_payloads(union_layout, tag_id) { + TagPayloads::IdNotInUnion => None, + TagPayloads::Payloads(payloads) => payloads.get(index as usize).map(|field_layout| { + self.interner + .insert_direct_no_semantic(LayoutRepr::Ptr(*field_layout)) + }), + }; + + self.with_sym_layout(structure, |ctx, def_line, _layout| { + ctx.check_sym_layout(structure, union, UseKind::TagExpr); + + match get_tag_id_payloads(union_layout, tag_id) { + TagPayloads::IdNotInUnion => { + ctx.problem(ProblemKind::IndexingTagIdNotInUnion { + structure, + def_line, + tag_id, + union_layout: interned_union_layout, + }); + None + } + TagPayloads::Payloads(payloads) => { + if field_ptr_layout.is_none() { + ctx.problem(ProblemKind::TagUnionStructIndexOOB { + structure, + def_line, + tag_id, + index, + size: payloads.len(), + }); + + None + } else { + field_ptr_layout + } + } + } + }) + } + + fn check_call(&mut self, call: &Call<'a>) -> Option> { + let Call { + call_type, + arguments, + } = call; + + match call_type { + CallType::ByName { + name, + ret_layout, + arg_layouts, + specialization_id, + } => { + let proc_layout = ProcLayout { + arguments: arg_layouts, + result: *ret_layout, + niche: name.niche(), + }; + if !self.procs.contains_key(&(name.name(), proc_layout)) { + let similar = self + .procs + .keys() + .filter(|(sym, _)| *sym == name.name()) + .map(|(_, lay)| *lay) + .collect(); + self.problem(ProblemKind::CallingUndefinedProc { + symbol: name.name(), + proc_layout, + similar, + }); + } + for (arg, wanted_layout) in arguments.iter().zip(arg_layouts.iter()) { + self.check_sym_layout(*arg, *wanted_layout, UseKind::CallArg); + } + if let Some(old_call_line) = + self.call_spec_ids.insert(*specialization_id, self.line) + { + self.problem(ProblemKind::DuplicateCallSpecId { old_call_line }); + } + Some(*ret_layout) + } + CallType::ByPointer { + pointer, + ret_layout, + arg_layouts, + } => { + let expected_layout = + self.interner + .insert_direct_no_semantic(LayoutRepr::FunctionPointer(FunctionPointer { + args: arg_layouts, + ret: *ret_layout, + })); + self.check_sym_layout(*pointer, expected_layout, UseKind::FunctionPointer); + for (arg, wanted_layout) in arguments.iter().zip(arg_layouts.iter()) { + self.check_sym_layout(*arg, *wanted_layout, UseKind::CallArg); + } + Some(*ret_layout) + } + CallType::HigherOrder(HigherOrderLowLevel { + op: _, + closure_env_layout: _, + update_mode: _, + passed_function: _, + }) => { + // TODO + None + } + CallType::Foreign { + foreign_symbol: _, + ret_layout, + } => Some(*ret_layout), + CallType::LowLevel { + op: _, + update_mode: _, + } => None, + } + } + + fn check_tag_expr( + &mut self, + interned_union_layout: InLayout<'a>, + union_layout: UnionLayout<'a>, + tag_id: u16, + arguments: &[Symbol], + ) { + match get_tag_id_payloads(union_layout, tag_id) { + TagPayloads::IdNotInUnion => { + self.problem(ProblemKind::CreatingTagIdNotInUnion { + tag_id, + union_layout: interned_union_layout, + }); + } + TagPayloads::Payloads(payloads) => { + if arguments.len() != payloads.len() { + self.problem(ProblemKind::CreateTagPayloadMismatch { + num_needed: payloads.len(), + num_given: arguments.len(), + }); + } + for (arg, wanted_layout) in arguments.iter().zip(payloads.iter()) { + self.check_sym_layout(*arg, *wanted_layout, UseKind::TagPayloadArg); + } + } + } + } + + fn check_modify_rc(&mut self, rc: ModifyRc) { + use ModifyRc::*; + + match rc { + Inc(sym, _) | Dec(sym) | DecRef(sym) | Free(sym) => { + // TODO: also check that sym layout needs refcounting + self.check_sym_exists(sym); + } + } + } + + fn check_erased_make(&mut self, value: Option, callee: Symbol) -> InLayout<'a> { + if let Some(value) = value { + self.with_sym_layout(value, |this, def_line, layout| { + let repr = this.interner.get_repr(layout); + if !matches!( + repr, + LayoutRepr::Union(UnionLayout::NullableUnwrapped { .. }) + ) { + this.problem(ProblemKind::ErasedMakeValueNotBoxed { + symbol: value, + def_layout: layout, + def_line, + }); + } + + Option::<()>::None + }); + } + self.with_sym_layout(callee, |this, def_line, layout| { + let repr = this.interner.get_repr(layout); + if !matches!(repr, LayoutRepr::FunctionPointer(_)) { + this.problem(ProblemKind::ErasedMakeCalleeNotFunctionPointer { + symbol: callee, + def_layout: layout, + def_line, + }); + } + + Option::<()>::None + }); + + Layout::ERASED + } + + fn check_erased_load( + &mut self, + symbol: Symbol, + field: ErasedField, + target_layout: InLayout<'a>, + ) -> InLayout<'a> { + self.check_sym_layout(symbol, Layout::ERASED, UseKind::Erased); + + match field { + ErasedField::Value => { + let repr = self.interner.get_repr(target_layout); + if !matches!( + repr, + LayoutRepr::Union(UnionLayout::NullableUnwrapped { .. }) + ) { + self.problem(ProblemKind::ErasedLoadValueNotBoxed { + symbol, + target_layout, + }); + } + } + ErasedField::ValuePtr => { + let repr = self.interner.get_repr(target_layout); + if !matches!(repr, LayoutRepr::Ptr(_)) { + self.problem(ProblemKind::ErasedLoadValueNotBoxed { + symbol, + target_layout, + }); + } + } + ErasedField::Callee => { + let repr = self.interner.get_repr(target_layout); + if !matches!(repr, LayoutRepr::FunctionPointer(_)) { + self.problem(ProblemKind::ErasedLoadCalleeNotFunctionPointer { + symbol, + target_layout, + }); + } + } + } + + target_layout + } +} + +enum TagPayloads<'a> { + IdNotInUnion, + Payloads(&'a [InLayout<'a>]), +} + +fn get_tag_id_payloads(union_layout: UnionLayout, tag_id: TagIdIntType) -> TagPayloads { + macro_rules! check_tag_id_oob { + ($len:expr) => { + if tag_id as usize >= $len { + return TagPayloads::IdNotInUnion; + } + }; + } + + match union_layout { + UnionLayout::NonRecursive(union) => { + check_tag_id_oob!(union.len()); + let payloads = union[tag_id as usize]; + TagPayloads::Payloads(payloads) + } + UnionLayout::Recursive(union) => { + check_tag_id_oob!(union.len()); + let payloads = union[tag_id as usize]; + TagPayloads::Payloads(payloads) + } + UnionLayout::NonNullableUnwrapped(payloads) => { + if tag_id != 0 { + TagPayloads::Payloads(&[]) + } else { + TagPayloads::Payloads(payloads) + } + } + UnionLayout::NullableWrapped { + nullable_id, + other_tags, + } => { + if tag_id == nullable_id { + TagPayloads::Payloads(&[]) + } else { + let num_tags = other_tags.len() + 1; + check_tag_id_oob!(num_tags); + + let tag_id_idx = if tag_id > nullable_id { + tag_id - 1 + } else { + tag_id + }; + let payloads = other_tags[tag_id_idx as usize]; + TagPayloads::Payloads(payloads) + } + } + UnionLayout::NullableUnwrapped { + nullable_id, + other_fields, + } => { + if tag_id == nullable_id as u16 { + TagPayloads::Payloads(&[]) + } else { + check_tag_id_oob!(2); + TagPayloads::Payloads(other_fields) + } + } + } +} diff --git a/crates/compiler/mono/src/debug/report.rs b/crates/compiler/mono/src/debug/report.rs new file mode 100644 index 0000000000..bca214c267 --- /dev/null +++ b/crates/compiler/mono/src/debug/report.rs @@ -0,0 +1,569 @@ +use std::fmt::Display; + +use roc_module::symbol::{Interns, Symbol}; +use ven_pretty::{text, Arena, DocAllocator, DocBuilder}; + +use crate::{ + ir::{ErasedField, Parens, ProcLayout}, + layout::LayoutInterner, +}; + +use super::{ + checker::{ProblemKind, UseKind}, + Problem, Problems, +}; + +pub fn format_problems<'a, I>( + interns: &Interns, + interner: &I, + problems: Problems<'a>, +) -> impl Display +where + I: LayoutInterner<'a>, +{ + let Problems(problems) = problems; + let f = Arena::new(); + let problem_docs = problems + .into_iter() + .map(|p| format_problem(&f, interns, interner, p)); + let all = f.intersperse(problem_docs, f.hardline()); + all.1.pretty(80).to_string() +} + +type Doc<'d> = DocBuilder<'d, Arena<'d>>; + +const GUTTER_BAR: &str = "│"; +const HEADER_WIDTH: usize = 80; + +fn format_problem<'a, 'd, I>( + f: &'d Arena<'d>, + interns: &'d Interns, + interner: &'d I, + problem: Problem<'a>, +) -> Doc<'d> +where + 'a: 'd, + I: LayoutInterner<'a>, +{ + let Problem { + proc, + proc_layout, + line, + kind, + } = problem; + + let (title, mut docs, last_doc) = format_kind(f, interns, interner, kind); + docs.push((line, last_doc)); + docs.sort_by_key(|(line, _)| *line); + + let src = proc + .to_doc(f, interner, true, Parens::NotNeeded) + .1 + .pretty(80) + .to_string(); + + eprintln!("Full source: {src}"); + + let interpolated_docs = stack( + f, + docs.into_iter() + .map(|(line, doc)| format_sourced_doc(f, line, &src, doc)), + ); + + let header = format_header(f, title); + let proc_loc = format_proc_spec(f, interns, interner, proc.name.name(), proc_layout); + + stack( + f, + [ + header, + f.concat([f.reflow("in "), proc_loc]), + interpolated_docs, + ], + ) +} + +fn format_sourced_doc<'d>(f: &'d Arena<'d>, line: usize, source: &str, doc: Doc<'d>) -> Doc<'d> { + let start_at = line.saturating_sub(1); + let source_lines = source.lines().skip(start_at).take(3); + let max_line_no_width = (start_at.to_string().len()).max((start_at + 3).to_string().len()); + let pretty_lines = source_lines.enumerate().map(|(i, line_src)| { + let line_no = start_at + i; + let line_no_s = line_no.to_string(); + let line_no_len = line_no_s.len(); + f.text(line_no_s) + .append(f.text(" ".repeat(max_line_no_width - line_no_len))) + .append(f.text(GUTTER_BAR)) + .append(f.text(if line_no == line { "> " } else { " " })) + .append(f.text(line_src.to_string())) + }); + let pretty_lines = f.intersperse(pretty_lines, f.hardline()); + stack(f, [pretty_lines, doc]) +} + +fn format_header<'d>(f: &'d Arena<'d>, title: &str) -> Doc<'d> { + let title_width = title.len() + 4; + text!(f, "── {} {}", title, "─".repeat(HEADER_WIDTH - title_width)) +} + +fn format_kind<'a, 'd, I>( + f: &'d Arena<'d>, + interns: &'d Interns, + interner: &I, + kind: ProblemKind<'a>, +) -> (&'static str, Vec<(usize, Doc<'d>)>, Doc<'d>) +where + I: LayoutInterner<'a>, +{ + let title; + let docs_before; + let doc = match kind { + ProblemKind::RedefinedSymbol { symbol, old_line } => { + title = "REDEFINED SYMBOL"; + docs_before = vec![( + old_line, + f.concat([ + format_symbol(f, interns, symbol), + f.reflow(" first defined here"), + ]), + )]; + f.concat([ + format_symbol(f, interns, symbol), + f.reflow(" re-defined here"), + ]) + } + ProblemKind::NoSymbolInScope { symbol } => { + title = "SYMBOL NOT DEFINED"; + docs_before = vec![]; + f.concat([ + format_symbol(f, interns, symbol), + f.reflow(" not found in the present scope"), + ]) + } + ProblemKind::SymbolUseMismatch { + symbol, + def_layout, + def_line, + use_layout, + use_kind, + } => { + title = "SYMBOL LAYOUT DOESN'T MATCH ITS USE"; + docs_before = vec![( + def_line, + f.concat([ + format_symbol(f, interns, symbol), + f.reflow(" defined here with layout "), + interner.to_doc_top(def_layout, f), + ]), + )]; + f.concat([ + format_symbol(f, interns, symbol), + f.reflow(" used as a "), + f.reflow(format_use_kind(use_kind)), + f.reflow(" here with layout "), + interner.to_doc_top(use_layout, f), + ]) + } + ProblemKind::SymbolDefMismatch { + symbol, + def_layout, + expr_layout, + } => { + title = "SYMBOL INITIALIZER HAS THE WRONG LAYOUT"; + docs_before = vec![]; + f.concat([ + format_symbol(f, interns, symbol), + f.reflow(" is defined as "), + interner.to_doc_top(def_layout, f), + f.reflow(" but its initializer is "), + interner.to_doc_top(expr_layout, f), + ]) + } + ProblemKind::BadSwitchConditionLayout { found_layout } => { + title = "BAD SWITCH CONDITION LAYOUT"; + docs_before = vec![]; + f.concat([ + f.reflow("This switch condition is a "), + interner.to_doc_top(found_layout, f), + ]) + } + ProblemKind::DuplicateSwitchBranch {} => { + title = "DUPLICATE SWITCH BRANCH"; + docs_before = vec![]; + f.reflow("The match of switch branch is reached earlier") + } + ProblemKind::RedefinedJoinPoint { id, old_line } => { + title = "DUPLICATE JOIN POINT"; + docs_before = vec![( + old_line, + f.concat([ + f.reflow("The join point "), + f.as_string(id.0), + f.reflow(" was previously defined here"), + ]), + )]; + f.reflow("and is redefined here") + } + ProblemKind::NoJoinPoint { id } => { + title = "JOIN POINT NOT DEFINED"; + docs_before = vec![]; + f.concat([ + f.reflow("The join point "), + f.as_string(id.0), + f.reflow(" was not found in the present scope"), + ]) + } + ProblemKind::JumpArityMismatch { + def_line, + num_needed, + num_given, + } => { + title = "WRONG NUMBER OF ARGUMENTS IN JUMP"; + docs_before = vec![( + def_line, + f.concat([ + f.reflow("This join pont needs "), + f.as_string(num_needed), + f.reflow(" arguments"), + ]), + )]; + f.concat([ + f.reflow("but this jump only gives it "), + f.as_string(num_given), + ]) + } + ProblemKind::CallingUndefinedProc { + symbol, + proc_layout, + similar, + } => { + title = "PROC SPECIALIZATION NOT DEFINED"; + docs_before = vec![]; + let no_spec_doc = stack( + f, + [ + f.reflow("No specialization"), + format_proc_spec(f, interns, interner, symbol, proc_layout), + f.reflow("was found"), + ], + ); + let similar_doc = if similar.is_empty() { + f.nil() + } else { + let similars = similar + .into_iter() + .map(|other| format_proc_spec(f, interns, interner, symbol, other)); + stack( + f, + [f.concat([ + f.reflow("The following specializations of "), + format_symbol(f, interns, symbol), + f.reflow(" were built:"), + stack(f, similars), + ])], + ) + }; + stack(f, [no_spec_doc, similar_doc]) + } + ProblemKind::PtrToUndefinedProc { symbol } => { + title = "PROC SPECIALIZATION NOT DEFINED"; + docs_before = vec![]; + f.concat([ + f.reflow("The proc "), + format_symbol(f, interns, symbol), + f.reflow(" is not defined"), + ]) + } + ProblemKind::DuplicateCallSpecId { old_call_line } => { + title = "DUPLICATE CALL SPEC ID"; + docs_before = vec![(old_call_line, f.reflow("This call has a specialization ID"))]; + f.reflow("...that is the same as the specialization ID of the call here") + } + ProblemKind::StructIndexOOB { + structure, + def_line, + index, + size, + } => { + title = "STRUCT INDEX IS OUT-OF-BOUNDS"; + docs_before = vec![( + def_line, + f.concat([ + f.reflow("The struct "), + format_symbol(f, interns, structure), + f.reflow(" defined here has "), + f.as_string(size), + f.reflow(" fields"), + ]), + )]; + f.concat([ + f.reflow("but is being indexed into field "), + f.as_string(index), + ]) + } + ProblemKind::NotAStruct { + structure, + def_line, + } => { + title = "SYMBOL IS NOT A STRUCT"; + docs_before = vec![( + def_line, + f.concat([ + f.reflow("The value "), + format_symbol(f, interns, structure), + f.reflow(" defined here"), + ]), + )]; + f.reflow("cannot be used as a structure here") + } + ProblemKind::IndexingTagIdNotInUnion { + structure, + def_line, + tag_id, + union_layout, + } => { + title = "TAG ID NOT IN UNION"; + docs_before = vec![( + def_line, + f.concat([ + f.reflow("The union "), + format_symbol(f, interns, structure), + f.reflow(" defined here has layout "), + interner.to_doc_top(union_layout, f), + ]), + )]; + f.concat([f.reflow("which has no tag of id "), f.as_string(tag_id)]) + } + ProblemKind::TagUnionStructIndexOOB { + structure, + def_line, + tag_id, + index, + size, + } => { + title = "UNION ID AND PAYLOAD INDEX IS OUT-OF-BOUNDS"; + docs_before = vec![( + def_line, + f.concat([ + f.reflow("The union "), + format_symbol(f, interns, structure), + f.reflow(" defined here has "), + f.as_string(size), + f.reflow(" payloads at ID "), + f.as_string(tag_id), + ]), + )]; + f.concat([ + f.reflow("but is being indexed into field "), + f.as_string(index), + f.reflow(" here"), + ]) + } + ProblemKind::IndexIntoNullableTag { + structure, + def_line, + tag_id, + union_layout, + } => { + title = "INDEX INTO NULLABLE TAG"; + docs_before = vec![( + def_line, + f.concat([ + f.reflow("The union "), + format_symbol(f, interns, structure), + f.reflow(" defined here has layout "), + interner.to_doc_top(union_layout, f), + ]), + )]; + f.concat([ + f.reflow("but is being indexed into the nullable variant "), + f.as_string(tag_id), + f.reflow(" here"), + ]) + } + ProblemKind::UnboxNotABox { symbol, def_line } => { + title = "ATTEMPTING TO UNBOX A NON-BOX"; + docs_before = vec![( + def_line, + f.concat([format_symbol(f, interns, symbol), f.reflow(" is not a box")]), + )]; + f.reflow("but is being unboxed here") + } + ProblemKind::CreatingTagIdNotInUnion { + tag_id, + union_layout, + } => { + title = "NO SUCH ID FOR TAG UNION"; + docs_before = vec![]; + f.concat([ + f.reflow("The variant "), + f.as_string(tag_id), + f.reflow(" is outside the target union layout "), + interner.to_doc_top(union_layout, f), + ]) + } + ProblemKind::CreateTagPayloadMismatch { + num_needed, + num_given, + } => { + title = "WRONG NUMBER OF ARGUMENTS IN TAG UNION"; + docs_before = vec![]; + f.concat([ + f.reflow("This tag union payload needs "), + f.as_string(num_needed), + f.reflow(" values, but is only given "), + f.as_string(num_given), + ]) + } + ProblemKind::ErasedMakeValueNotBoxed { + symbol, + def_layout, + def_line, + } => { + title = "ERASED VALUE IS NOT BOXED"; + docs_before = vec![( + def_line, + f.concat([ + f.reflow("The value "), + format_symbol(f, interns, symbol), + f.reflow(" defined here"), + ]), + )]; + f.concat([ + f.reflow("must be boxed in order to be erased, but has layout "), + interner.to_doc_top(def_layout, f), + ]) + } + ProblemKind::ErasedMakeCalleeNotFunctionPointer { + symbol, + def_layout, + def_line, + } => { + title = "ERASED CALLEE IS NOT A FUNCTION POINTER"; + docs_before = vec![( + def_line, + f.concat([ + f.reflow("The value "), + format_symbol(f, interns, symbol), + f.reflow(" defined here"), + ]), + )]; + f.concat([ + f.reflow( + "must be a function pointer in order to be an erasure callee, but has layout ", + ), + interner.to_doc_top(def_layout, f), + ]) + } + ProblemKind::ErasedLoadValueNotBoxed { + symbol, + target_layout, + } => { + title = "ERASED VALUE IS NOT BOXED"; + docs_before = vec![]; + f.concat([ + f.reflow("The erased value load "), + format_symbol(f, interns, symbol), + f.reflow(" has layout "), + interner.to_doc_top(target_layout, f), + f.reflow(", but should be boxed!"), + ]) + } + ProblemKind::ErasedLoadCalleeNotFunctionPointer { + symbol, + target_layout, + } => { + title = "ERASED CALLEE IS NOT A FUNCTION POINTER"; + docs_before = vec![]; + f.concat([ + f.reflow("The erased callee load "), + format_symbol(f, interns, symbol), + f.reflow(" has layout "), + interner.to_doc_top(target_layout, f), + f.reflow(", but should be a function pointer!"), + ]) + } + }; + (title, docs_before, doc) +} + +fn format_symbol<'d>(f: &'d Arena<'d>, interns: &'d Interns, symbol: Symbol) -> Doc<'d> { + f.text(symbol.module_string(interns).to_string()) + .append(f.text(".")) + .append(f.text(symbol.as_str(interns))) +} + +fn format_use_kind(use_kind: UseKind) -> &'static str { + match use_kind { + UseKind::Ret => "return value", + UseKind::TagExpr => "tag constructor", + UseKind::TagReuse => "tag reuse", + UseKind::TagPayloadArg => "tag's payload", + UseKind::ListElemExpr => "list element", + UseKind::CallArg => "call argument", + UseKind::JumpArg => "jump argument", + UseKind::CrashArg => "crash message", + UseKind::SwitchCond => "switch condition", + UseKind::ExpectCond => "expect condition", + UseKind::ExpectLookup => "lookup for an expect", + UseKind::ErasedMake(kind) => match kind { + ErasedField::Value => "erased value field", + ErasedField::ValuePtr => "erased value pointer", + ErasedField::Callee => "erased callee field", + }, + UseKind::Erased => "erasure", + UseKind::FunctionPointer => "function pointer", + UseKind::Alloca => "alloca initializer", + } +} + +fn format_proc_spec<'a, 'd, I>( + f: &'d Arena<'d>, + interns: &'d Interns, + interner: &I, + symbol: Symbol, + proc_layout: ProcLayout<'a>, +) -> Doc<'d> +where + I: LayoutInterner<'a>, +{ + f.concat([ + f.as_string(symbol.as_str(interns)), + f.reflow(" : "), + format_proc_layout(f, interner, proc_layout), + ]) +} + +fn format_proc_layout<'a, 'd, I>( + f: &'d Arena<'d>, + interner: &I, + proc_layout: ProcLayout<'a>, +) -> Doc<'d> +where + I: LayoutInterner<'a>, +{ + let ProcLayout { + arguments, + result, + niche: captures_niche, + } = proc_layout; + let args = f.intersperse( + arguments + .iter() + .map(|a| interner.to_doc(*a, f, &mut Default::default(), Parens::InFunction)), + f.reflow(", "), + ); + let fun = f.concat([ + f.concat([f.reflow("("), args, f.reflow(")")]), + f.reflow(" -> "), + interner.to_doc_top(result, f), + ]); + let niche = (f.text("(")) + .append(captures_niche.to_doc(f, interner, &mut Default::default())) + .append(f.text(")")); + f.concat([fun, f.space(), niche]) +} + +fn stack<'d>(f: &'d Arena<'d>, docs: impl IntoIterator>) -> Doc<'d> { + f.intersperse(docs, f.line().append(f.line())) +} diff --git a/crates/compiler/mono/src/drop_specialization.rs b/crates/compiler/mono/src/drop_specialization.rs new file mode 100644 index 0000000000..1317aa53bf --- /dev/null +++ b/crates/compiler/mono/src/drop_specialization.rs @@ -0,0 +1,1687 @@ +// This program was written by Jelle Teeuwissen within a final +// thesis project of the Computing Science master program at Utrecht +// University under supervision of Wouter Swierstra (w.s.swierstra@uu.nl). + +// Implementation based of Drop Specialization from Perceus: Garbage Free Reference Counting with Reuse +// https://www.microsoft.com/en-us/research/uploads/prod/2021/06/perceus-pldi21.pdf + +#![allow(clippy::too_many_arguments)] + +use std::cmp::{self, Ord}; +use std::iter::Iterator; + +use bumpalo::collections::vec::Vec; +use bumpalo::collections::CollectIn; + +use roc_module::low_level::LowLevel; +use roc_module::symbol::{IdentIds, ModuleId, Symbol}; + +use crate::ir::{ + BranchInfo, Call, CallType, ErasedField, Expr, JoinPointId, ListLiteralElement, Literal, + ModifyRc, Proc, ProcLayout, Stmt, UpdateModeId, +}; +use crate::layout::{ + Builtin, InLayout, Layout, LayoutInterner, LayoutRepr, STLayoutInterner, UnionLayout, +}; + +use bumpalo::Bump; + +use roc_collections::MutMap; + +/** +Try to find increments of symbols followed by decrements of the symbol they were indexed out of (their parent). +Then inline the decrement operation of the parent and removing matching pairs of increments and decrements. +*/ +pub fn specialize_drops<'a, 'i>( + arena: &'a Bump, + layout_interner: &'i mut STLayoutInterner<'a>, + home: ModuleId, + ident_ids: &'i mut IdentIds, + procs: &mut MutMap<(Symbol, ProcLayout<'a>), Proc<'a>>, +) { + for ((_symbol, proc_layout), proc) in procs.iter_mut() { + let mut environment = DropSpecializationEnvironment::new(arena, home, proc_layout.result); + specialize_drops_proc(arena, layout_interner, ident_ids, &mut environment, proc); + } +} + +fn specialize_drops_proc<'a, 'i>( + arena: &'a Bump, + layout_interner: &'i mut STLayoutInterner<'a>, + ident_ids: &'i mut IdentIds, + environment: &mut DropSpecializationEnvironment<'a>, + proc: &mut Proc<'a>, +) { + for (layout, symbol) in proc.args.iter().copied() { + environment.add_symbol_layout(symbol, layout); + } + + let new_body = + specialize_drops_stmt(arena, layout_interner, ident_ids, environment, &proc.body); + + proc.body = new_body.clone(); +} + +fn specialize_drops_stmt<'a, 'i>( + arena: &'a Bump, + layout_interner: &'i mut STLayoutInterner<'a>, + ident_ids: &'i mut IdentIds, + environment: &mut DropSpecializationEnvironment<'a>, + stmt: &Stmt<'a>, +) -> &'a Stmt<'a> { + match stmt { + Stmt::Let(binding, expr @ Expr::Call(call), layout, continuation) => { + environment.add_symbol_layout(*binding, *layout); + + macro_rules! alloc_let_with_continuation { + ($environment:expr) => {{ + let new_continuation = specialize_drops_stmt( + arena, + layout_interner, + ident_ids, + $environment, + continuation, + ); + arena.alloc(Stmt::Let(*binding, expr.clone(), *layout, new_continuation)) + }}; + } + + match call.call_type.clone().replace_lowlevel_wrapper() { + CallType::LowLevel { + op: LowLevel::ListGetUnsafe, + .. + } => { + let [structure, index] = match call.arguments { + [structure, index] => [structure, index], + _ => unreachable!("List get should have two arguments"), + }; + + environment.add_list_child_symbol(*structure, *binding, index); + + alloc_let_with_continuation!(environment) + } + // Check whether the increments can be passed to the continuation. + CallType::LowLevel { op, .. } => match low_level_no_rc(&op) { + // It should be safe to pass the increments to the continuation. + RC::NoRc => alloc_let_with_continuation!(environment), + // We probably should not pass the increments to the continuation. + RC::Rc | RC::Uknown => { + let incremented_symbols = environment.incremented_symbols.drain(); + + let new_stmt = alloc_let_with_continuation!(environment); + + // The new_environment might have inserted increments that were set to 0 before. We need to add th + for (symbol, increment) in incremented_symbols.map.into_iter() { + environment + .incremented_symbols + .insert_count(symbol, increment); + } + + new_stmt + } + }, + _ => { + // Calls can modify the RC of the symbol. + // If we move a increment of children after the function, + // the function might deallocate the child before we can use it after the function. + // If we move the decrement of the parent to before the function, + // the parent might be deallocated before the function can use it. + // Thus forget everything about any increments. + + let incremented_symbols = environment.incremented_symbols.drain(); + + let new_stmt = alloc_let_with_continuation!(environment); + + // The new_environment might have inserted increments that were set to 0 before. We need to add th + for (symbol, increment) in incremented_symbols.map.into_iter() { + environment + .incremented_symbols + .insert_count(symbol, increment); + } + + new_stmt + } + } + } + Stmt::Let(_, _, _, _) => { + use Expr::*; + + // to prevent stack overflows, try to use an explicit stack to accumulate a bunch of + // Let statements. Call expressions require more logic and are never put on this stack + let mut stack = vec![]; + + let mut stmt = stmt; + + while let Stmt::Let(binding, expr, layout, continuation) = stmt { + environment.add_symbol_layout(*binding, *layout); + + // update the environment based on the expr + match expr { + Call(_) => { + // Expr::Call is tricky and we are lazy and handle it elsewhere. it + // ends a chain of eligible Let statements. + break; + } + Literal(crate::ir::Literal::Int(i)) => { + environment + .symbol_index + .insert(*binding, i128::from_ne_bytes(*i) as u64); + } + Literal(_) => { /* do nothing */ } + Tag { + tag_id, + arguments: children, + .. + } => { + environment.symbol_tag.insert(*binding, *tag_id); + + for (index, child) in children.iter().enumerate() { + environment.add_union_child(*binding, *child, *tag_id, index as u64); + } + } + Struct(children) => { + for (index, child) in children.iter().enumerate() { + environment.add_struct_child(*binding, *child, index as u64); + } + } + StructAtIndex { + index, structure, .. + } => { + environment.add_struct_child(*structure, *binding, *index); + + // TODO do we need to remove the indexed value to prevent it from being dropped sooner? + // It will only be dropped sooner if the reference count is 1. Which can only happen if there is no increment before. + // So we should be fine. + } + UnionAtIndex { + structure, + tag_id, + index, + .. + } => { + // TODO perhaps we need the union_layout later as well? if so, create a new function/map to store it. + environment.add_union_child(*structure, *binding, *tag_id, *index); + // Generated code might know the tag of the union without switching on it. + // So if we UnionAtIndex, we must know the tag and we can use it to specialize the drop. + environment.symbol_tag.insert(*structure, *tag_id); + } + GetElementPointer { + structure, indices, .. + } => { + // Generated code might know the tag of the union without switching on it. + // So if we GetElementPointer, we must know the tag and we can use it to specialize the drop. + environment.symbol_tag.insert(*structure, indices[0] as u16); + } + Array { + elems: children, .. + } => { + let it = + children + .iter() + .enumerate() + .filter_map(|(index, child)| match child { + ListLiteralElement::Literal(_) => None, + ListLiteralElement::Symbol(s) => Some((index, s)), + }); + + for (index, child) in it { + environment.add_list_child(*binding, *child, index as u64); + } + } + ErasedMake { value, callee: _ } => { + if let Some(value) = value { + environment.add_struct_child(*binding, *value, 0); + } + } + ErasedLoad { symbol, field } => { + match field { + ErasedField::Value => { + environment.add_struct_child(*symbol, *binding, 0); + } + ErasedField::Callee | ErasedField::ValuePtr => { + // nothing to own + } + } + } + Reset { .. } | Expr::ResetRef { .. } => { /* do nothing */ } + RuntimeErrorFunction(_) + | FunctionPointer { .. } + | GetTagId { .. } + | Alloca { .. } + | EmptyArray + | NullPointer => { /* do nothing */ } + } + + // now store the let binding for later + stack.push((*binding, expr.clone(), *layout)); + + // and "recurse" down the statement chain + stmt = continuation; + } + + stack.into_iter().rev().fold( + specialize_drops_stmt(arena, layout_interner, ident_ids, environment, stmt), + |acc, (binding, expr, layout)| arena.alloc(Stmt::Let(binding, expr, layout, acc)), + ) + } + Stmt::Switch { + cond_symbol, + cond_layout, + branches, + default_branch, + ret_layout, + } => { + macro_rules! insert_branch_info { + ($branch_env:expr,$info:expr ) => { + match $info { + BranchInfo::Constructor { + scrutinee: symbol, + tag_id: tag, + .. + } => { + $branch_env.symbol_tag.insert(*symbol, *tag); + } + BranchInfo::List { + scrutinee: symbol, + len, + } => { + $branch_env.list_length.insert(*symbol, *len); + } + _ => (), + } + }; + } + + let new_branches = branches + .iter() + .map(|(label, info, branch)| { + let mut branch_env = environment.clone(); + + insert_branch_info!(branch_env, info); + + let new_branch = specialize_drops_stmt( + arena, + layout_interner, + ident_ids, + &mut branch_env, + branch, + ); + + (*label, info.clone(), new_branch.clone(), branch_env) + }) + .collect_in::>(arena) + .into_bump_slice(); + + let new_default_branch = { + let (info, branch) = default_branch; + + let mut branch_env = environment.clone(); + + insert_branch_info!(branch_env, info); + + let new_branch = specialize_drops_stmt( + arena, + layout_interner, + ident_ids, + &mut branch_env, + branch, + ); + + (info.clone(), new_branch, branch_env) + }; + + // Find consumed increments in each branch and make sure they are consumed in all branches. + // By incrementing them in each branch where they were not consumed. + { + let branch_envs = { + let mut branch_environments = + Vec::with_capacity_in(new_branches.len() + 1, arena); + + for (_, _, _, branch_env) in new_branches.iter() { + branch_environments.push(branch_env); + } + + branch_environments.push(&new_default_branch.2); + + branch_environments + }; + + // Find the lowest symbol count for each symbol in each branch, and update the environment to match. + for (symbol, count) in environment.incremented_symbols.map.iter_mut() { + let consumed = branch_envs + .iter() + .map(|branch_env| { + branch_env.incremented_symbols.map.get(symbol).unwrap_or(&0) + }) + .min() + .unwrap(); + + // Update the existing env to match the lowest count. + *count = *consumed; + } + } + + macro_rules! insert_incs { + ($branch_env:expr, $branch:expr ) => {{ + let symbol_differences = + environment + .incremented_symbols + .map + .iter() + .filter_map(|(symbol, count)| { + let branch_count = $branch_env + .incremented_symbols + .map + .get(symbol) + .unwrap_or(&0); + + match branch_count - count { + 0 => None, + difference => Some((symbol, difference)), + } + }); + + symbol_differences.fold($branch, |new_branch, (symbol, difference)| { + arena.alloc(Stmt::Refcounting( + ModifyRc::Inc(*symbol, difference), + new_branch, + )) + }) + }}; + } + + environment.jump_incremented_symbols = + new_default_branch.2.jump_incremented_symbols.clone(); + + let newer_branches = new_branches + .iter() + .map(|(label, info, branch, branch_env)| { + for (joinpoint, current_incremented_symbols) in + environment.jump_incremented_symbols.iter_mut() + { + let opt_symbols = branch_env.jump_incremented_symbols.get(joinpoint); + if let Some(branch_incremented_symbols) = opt_symbols { + current_incremented_symbols.map.retain(|key, join_count| { + let opt_count = branch_incremented_symbols.map.get(key); + if let Some(count) = opt_count { + *join_count = std::cmp::min(*join_count, *count); + } + + // retain only the Some cases + opt_count.is_some() + }); + } + } + + let new_branch = insert_incs!(branch_env, branch); + + (*label, info.clone(), new_branch.clone()) + }) + .collect_in::>(arena) + .into_bump_slice(); + + let newer_default_branch = { + let (info, branch, branch_env) = new_default_branch; + + let new_branch = insert_incs!(branch_env, branch); + + (info.clone(), new_branch) + }; + + arena.alloc(Stmt::Switch { + cond_symbol: *cond_symbol, + cond_layout: *cond_layout, + branches: newer_branches, + default_branch: newer_default_branch, + ret_layout: *ret_layout, + }) + } + Stmt::Ret(symbol) => arena.alloc(Stmt::Ret(*symbol)), + Stmt::Refcounting(rc, continuation) => match rc { + ModifyRc::Inc(symbol, count) => { + let inc_before = environment.incremented_symbols.contains(symbol); + + // Add a symbol for every increment performed. + environment + .incremented_symbols + .insert_count(*symbol, *count); + + let new_continuation = specialize_drops_stmt( + arena, + layout_interner, + ident_ids, + environment, + continuation, + ); + + if inc_before { + // There were increments before this one, best to let the first one do the increments. + // Or there are no increments left, so we can just continue. + new_continuation + } else { + match environment + .incremented_symbols + .map + .remove(symbol) + .unwrap_or(0) + { + // This is the first increment, but all increments are consumed. So don't insert any. + 0 => new_continuation, + // We still need to do some increments. + new_count => arena.alloc(Stmt::Refcounting( + ModifyRc::Inc(*symbol, new_count), + new_continuation, + )), + } + } + } + ModifyRc::Dec(symbol) => { + // We first check if there are any outstanding increments we can cross of with this decrement. + // Then we check the continuation, since it might have a decrement of a symbol that's a child of this one. + // Afterwards we perform drop specialization. + // In the following example, we don't want to inline `dec b`, we want to remove the `inc a` and `dec a` instead. + // let a = index b + // inc a + // dec a + // dec b + + if environment.incremented_symbols.pop(symbol) { + // This decremented symbol was incremented before, so we can remove it. + specialize_drops_stmt( + arena, + layout_interner, + ident_ids, + environment, + continuation, + ) + } else { + // Collect all children (recursively) that were incremented and make sure that one increment remains in the environment afterwards. + // To prevent + // let a = index b; inc a; dec b; ...; dec a + // from being translated to + // let a = index b; dec b + // As a might get dropped as a result of the decrement of b. + let mut incremented_children = { + let mut todo_children = bumpalo::vec![in arena; *symbol]; + let mut incremented_children = CountingMap::new(); + + while let Some(child) = todo_children.pop() { + if environment.incremented_symbols.pop(&child) { + incremented_children.insert(child); + } else { + todo_children.extend(environment.get_children(&child)); + } + } + + incremented_children + }; + + // This decremented symbol was not incremented before, perhaps the children were. + let in_layout = environment.get_symbol_layout(symbol); + let runtime_repr = layout_interner.runtime_representation(*in_layout); + + let updated_stmt = match runtime_repr { + // Layout has children, try to inline them. + LayoutRepr::Struct(field_layouts) => specialize_struct( + arena, + layout_interner, + ident_ids, + environment, + symbol, + field_layouts, + &mut incremented_children, + continuation, + ), + LayoutRepr::Union(union_layout) => specialize_union( + arena, + layout_interner, + ident_ids, + environment, + symbol, + union_layout, + &mut incremented_children, + continuation, + ), + LayoutRepr::Builtin(Builtin::List(layout)) => specialize_list( + arena, + layout_interner, + ident_ids, + environment, + &mut incremented_children, + symbol, + layout, + continuation, + ), + // TODO: lambda sets should not be reachable, yet they are. + _ => { + let new_continuation = specialize_drops_stmt( + arena, + layout_interner, + ident_ids, + environment, + continuation, + ); + + // No children, keep decrementing the symbol. + arena.alloc(Stmt::Refcounting(ModifyRc::Dec(*symbol), new_continuation)) + } + }; + + // Add back the increments for the children to the environment. + for (child_symbol, symbol_count) in incremented_children.map.into_iter() { + environment + .incremented_symbols + .insert_count(child_symbol, symbol_count) + } + + updated_stmt + } + } + ModifyRc::DecRef(_) | ModifyRc::Free(_) => { + // These operations are not recursive (the children are not touched) + // so inlining is not useful + arena.alloc(Stmt::Refcounting( + *rc, + specialize_drops_stmt( + arena, + layout_interner, + ident_ids, + environment, + continuation, + ), + )) + } + }, + Stmt::Expect { + condition, + region, + lookups, + variables, + remainder, + } => arena.alloc(Stmt::Expect { + condition: *condition, + region: *region, + lookups, + variables, + remainder: specialize_drops_stmt( + arena, + layout_interner, + ident_ids, + environment, + remainder, + ), + }), + Stmt::ExpectFx { + condition, + region, + lookups, + variables, + remainder, + } => arena.alloc(Stmt::ExpectFx { + condition: *condition, + region: *region, + lookups, + variables, + remainder: specialize_drops_stmt( + arena, + layout_interner, + ident_ids, + environment, + remainder, + ), + }), + Stmt::Dbg { + symbol, + variable, + remainder, + } => arena.alloc(Stmt::Dbg { + symbol: *symbol, + variable: *variable, + remainder: specialize_drops_stmt( + arena, + layout_interner, + ident_ids, + environment, + remainder, + ), + }), + Stmt::Join { + id, + parameters, + body, + remainder, + } => { + // We cannot perform this optimization if the joinpoint is recursive. + // E.g. if the body of a recursive joinpoint contains an increment, we do not want to move that increment up to the remainder. + + let mut remainder_environment = environment.clone(); + + let new_remainder = specialize_drops_stmt( + arena, + layout_interner, + ident_ids, + &mut remainder_environment, + remainder, + ); + + let mut body_environment = environment.clone(); + for param in parameters.iter() { + body_environment.add_symbol_layout(param.symbol, param.layout); + } + body_environment.incremented_symbols.clear(); + + let new_body = specialize_drops_stmt( + arena, + layout_interner, + ident_ids, + &mut body_environment, + body, + ); + + let remainder_jump_info = remainder_environment.jump_incremented_symbols.get(id); + + let body_jump_info = body_environment.jump_incremented_symbols.get(id); + + let (newer_body, newer_remainder) = match (remainder_jump_info, body_jump_info) { + // We have info from the remainder, and the body is not recursive. + // Meaning we can pass the incremented_symbols from the remainder to the body. + (Some(jump_info), None) if !jump_info.is_empty() => { + // Update body with incremented symbols from remainder + body_environment.incremented_symbols = jump_info.clone(); + + let newer_body = specialize_drops_stmt( + arena, + layout_interner, + ident_ids, + &mut body_environment, + body, + ); + + // Update remainder + environment.join_incremented_symbols.insert( + *id, + JoinUsage { + join_consumes: jump_info.clone(), + join_returns: body_environment.incremented_symbols, + }, + ); + let newer_remainder = specialize_drops_stmt( + arena, + layout_interner, + ident_ids, + environment, + remainder, + ); + + (newer_body, newer_remainder) + } + _ => { + // Keep the body and remainder as is. + // Update the environment with remainder environment. + + *environment = remainder_environment; + + (new_body, new_remainder) + } + }; + + arena.alloc(Stmt::Join { + id: *id, + parameters, + body: newer_body, + remainder: newer_remainder, + }) + } + Stmt::Jump(joinpoint_id, arguments) => { + match environment.join_incremented_symbols.get(joinpoint_id) { + Some(JoinUsage { + join_consumes, + join_returns, + }) => { + // Consume all symbols that were consumed in the join. + for (symbol, count) in join_consumes.map.iter() { + for _ in 0..*count { + let popped = environment.incremented_symbols.pop(symbol); + debug_assert!( + popped, + "Every incremented symbol should be available from jumps" + ); + } + } + for (symbol, count) in join_returns.map.iter() { + environment + .incremented_symbols + .insert_count(*symbol, *count); + } + } + None => { + // No join usage, let the join know the minimum amount of symbols that were incremented from each jump. + environment + .jump_incremented_symbols + .insert(*joinpoint_id, environment.incremented_symbols.clone()); + } + } + arena.alloc(Stmt::Jump(*joinpoint_id, arguments)) + } + Stmt::Crash(symbol, crash_tag) => arena.alloc(Stmt::Crash(*symbol, *crash_tag)), + } +} + +fn specialize_struct<'a, 'i>( + arena: &'a Bump, + layout_interner: &'i mut STLayoutInterner<'a>, + ident_ids: &'i mut IdentIds, + environment: &mut DropSpecializationEnvironment<'a>, + symbol: &Symbol, + struct_layout: &'a [InLayout], + incremented_children: &mut CountingMap, + continuation: &'a Stmt<'a>, +) -> &'a Stmt<'a> { + match environment.struct_children.get(symbol) { + // TODO all these children might be non reference counting, inlining the dec without any benefit. + // Perhaps only insert children that are reference counted. + Some(children) => { + // TODO perhaps this allocation can be avoided. + let children_clone = children.clone(); + + // Map tracking which index of the struct is contained in which symbol. + // And whether the child no longer has to be decremented. + let mut index_symbols = MutMap::default(); + + for (index, _layout) in struct_layout.iter().enumerate() { + for (child, _i) in children_clone.iter().filter(|(_, i)| *i == index as u64) { + let removed = incremented_children.pop(child); + index_symbols.insert(index, (*child, removed)); + + if removed { + break; + } + } + } + + let mut new_continuation = + specialize_drops_stmt(arena, layout_interner, ident_ids, environment, continuation); + + // Make sure every field is decremented. + // Reversed to ensure that the generated code decrements the fields in the correct order. + for (i, field_layout) in struct_layout.iter().enumerate().rev() { + // Only insert decrements for fields that are/contain refcounted values. + if layout_interner.contains_refcounted(*field_layout) { + new_continuation = match index_symbols.get(&i) { + // This value has been indexed before, use that symbol. + Some((s, popped)) => { + if *popped { + // This symbol was popped, so we can skip the decrement. + new_continuation + } else { + // This symbol was indexed but not decremented, so we will decrement it. + arena.alloc(Stmt::Refcounting(ModifyRc::Dec(*s), new_continuation)) + } + } + + // This value has not been index before, create a new symbol. + None => { + let field_symbol = + environment.create_symbol(ident_ids, &format!("field_val_{i}")); + + let field_val_expr = Expr::StructAtIndex { + index: i as u64, + field_layouts: struct_layout, + structure: *symbol, + }; + + arena.alloc(Stmt::Let( + field_symbol, + field_val_expr, + layout_interner.chase_recursive_in(*field_layout), + arena.alloc(Stmt::Refcounting( + ModifyRc::Dec(field_symbol), + new_continuation, + )), + )) + } + }; + } + } + + new_continuation + } + None => { + // No known children, keep decrementing the symbol. + let new_continuation = + specialize_drops_stmt(arena, layout_interner, ident_ids, environment, continuation); + + arena.alloc(Stmt::Refcounting(ModifyRc::Dec(*symbol), new_continuation)) + } + } +} + +fn specialize_union<'a, 'i>( + arena: &'a Bump, + layout_interner: &'i mut STLayoutInterner<'a>, + ident_ids: &'i mut IdentIds, + environment: &mut DropSpecializationEnvironment<'a>, + symbol: &Symbol, + union_layout: UnionLayout<'a>, + incremented_children: &mut CountingMap, + continuation: &'a Stmt<'a>, +) -> &'a Stmt<'a> { + let current_tag = environment.symbol_tag.get(symbol).copied(); + + macro_rules! keep_original_decrement { + () => {{ + let new_continuation = + specialize_drops_stmt(arena, layout_interner, ident_ids, environment, continuation); + arena.alloc(Stmt::Refcounting(ModifyRc::Dec(*symbol), new_continuation)) + }}; + } + + match get_union_tag_layout(union_layout, current_tag) { + // No known tag, decrement the symbol as usual. + UnionFieldLayouts::Unknown => { + keep_original_decrement!() + } + + // The union is null, so we can skip the decrement. + UnionFieldLayouts::Null => { + specialize_drops_stmt(arena, layout_interner, ident_ids, environment, continuation) + } + + // We know the tag, we can specialize the decrement for the tag. + UnionFieldLayouts::Found { field_layouts, tag } => { + match environment.union_children.get(symbol) { + None => keep_original_decrement!(), + Some(children) => { + // TODO perhaps this allocation can be avoided. + let children_clone = children.clone(); + + // Map tracking which index of the struct is contained in which symbol. + // And whether the child no longer has to be decremented. + let mut index_symbols = MutMap::default(); + + for (index, _layout) in field_layouts.iter().enumerate() { + for (child, t, _i) in children_clone + .iter() + .rev() + .filter(|(_child, _t, i)| *i == index as u64) + { + debug_assert_eq!(tag, *t); + + let removed = incremented_children.pop(child); + index_symbols.entry(index).or_insert((*child, removed)); + + if removed { + break; + } + } + } + + let new_continuation = specialize_drops_stmt( + arena, + layout_interner, + ident_ids, + environment, + continuation, + ); + + type RCFun<'a> = + Option) -> &'a Stmt<'a>>; + let refcount_fields = |layout_interner: &mut STLayoutInterner<'a>, + ident_ids: &mut IdentIds, + rc_popped: RCFun<'a>, + rc_unpopped: RCFun<'a>, + continuation: &'a Stmt<'a>| + -> &'a Stmt<'a> { + let mut new_continuation = continuation; + + // Reversed to ensure that the generated code decrements the fields in the correct order. + for (i, field_layout) in field_layouts.iter().enumerate().rev() { + // Only insert decrements for fields that are/contain refcounted values. + if layout_interner.contains_refcounted(*field_layout) { + new_continuation = match index_symbols.get(&i) { + // This value has been indexed before, use that symbol. + Some((s, popped)) => { + if *popped { + // This symbol was popped, so we can skip the decrement. + match rc_popped { + Some(rc) => rc(arena, *s, new_continuation), + None => new_continuation, + } + } else { + // This symbol was indexed but not decremented, so we will decrement it. + match rc_unpopped { + Some(rc) => rc(arena, *s, new_continuation), + None => new_continuation, + } + } + } + + // This value has not been index before, create a new symbol. + None => match rc_unpopped { + Some(rc) => { + let field_symbol = environment.create_symbol( + ident_ids, + &format!("field_val_{i}"), + ); + + let field_val_expr = Expr::UnionAtIndex { + structure: *symbol, + tag_id: tag, + union_layout, + index: i as u64, + }; + + arena.alloc(Stmt::Let( + field_symbol, + field_val_expr, + layout_interner.chase_recursive_in(*field_layout), + rc(arena, field_symbol, new_continuation), + )) + } + None => new_continuation, + }, + }; + } + } + + new_continuation + }; + + match union_layout { + UnionLayout::NonRecursive(_) => refcount_fields( + layout_interner, + ident_ids, + // Do nothing for the children that were incremented before, as the decrement will cancel out. + None, + // Decrement the children that were not incremented before. And thus don't cancel out. + Some(|arena, symbol, continuation| { + arena.alloc(Stmt::Refcounting(ModifyRc::Dec(symbol), continuation)) + }), + new_continuation, + ), + UnionLayout::Recursive(_) + | UnionLayout::NonNullableUnwrapped(_) + | UnionLayout::NullableWrapped { .. } + | UnionLayout::NullableUnwrapped { .. } => { + branch_uniqueness( + arena, + ident_ids, + layout_interner, + environment, + *symbol, + // If the symbol is unique: + // - drop the children that were not incremented before + // - don't do anything for the children that were incremented before + // - free the parent + |layout_interner, ident_ids, continuation| { + refcount_fields( + layout_interner, + ident_ids, + // Do nothing for the children that were incremented before, as the decrement will cancel out. + None, + // Decrement the children that were not incremented before. And thus don't cancel out. + Some(|arena, symbol, continuation| { + arena.alloc(Stmt::Refcounting( + ModifyRc::Dec(symbol), + continuation, + )) + }), + arena.alloc(Stmt::Refcounting( + // we know for sure that the allocation is unique at + // this point. Therefore we can free (or maybe reuse) + // without checking the refcount again. + ModifyRc::Free(*symbol), + continuation, + )), + ) + }, + // If the symbol is not unique: + // - increment the children that were incremented before + // - don't do anything for the children that were not incremented before + // - decref the parent + |layout_interner, ident_ids, continuation| { + refcount_fields( + layout_interner, + ident_ids, + Some(|arena, symbol, continuation| { + arena.alloc(Stmt::Refcounting( + ModifyRc::Inc(symbol, 1), + continuation, + )) + }), + None, + arena.alloc(Stmt::Refcounting( + ModifyRc::DecRef(*symbol), + continuation, + )), + ) + }, + new_continuation, + ) + } + } + } + } + } + } +} + +fn specialize_list<'a, 'i>( + arena: &'a Bump, + layout_interner: &'i mut STLayoutInterner<'a>, + ident_ids: &'i mut IdentIds, + environment: &mut DropSpecializationEnvironment<'a>, + incremented_children: &mut CountingMap, + symbol: &Symbol, + item_layout: InLayout<'a>, + continuation: &'a Stmt<'a>, +) -> &'a Stmt<'a> { + let current_length = environment.list_length.get(symbol).copied(); + + macro_rules! keep_original_decrement { + () => {{ + let new_continuation = + specialize_drops_stmt(arena, layout_interner, ident_ids, environment, continuation); + arena.alloc(Stmt::Refcounting(ModifyRc::Dec(*symbol), new_continuation)) + }}; + } + + match ( + layout_interner.contains_refcounted(item_layout), + current_length, + ) { + // Only specialize lists if the amount of children is known. + // Otherwise we might have to insert an unbouned number of decrements. + (true, Some(length)) => { + match environment.list_children.get(symbol) { + Some(children) => { + // TODO perhaps this allocation can be avoided. + let children_clone = children.clone(); + + // Map tracking which index of the struct is contained in which symbol. + // And whether the child no longer has to be decremented. + let mut index_symbols = MutMap::default(); + + for index in 0..length { + for (child, i) in children_clone + .iter() + .rev() + .filter(|(_child, i)| *i == index) + { + debug_assert!(length > *i); + + let removed = incremented_children.pop(child); + index_symbols.insert(index, (*child, removed)); + + if removed { + break; + } + } + } + + let new_continuation = specialize_drops_stmt( + arena, + layout_interner, + ident_ids, + environment, + continuation, + ); + + let mut newer_continuation = arena.alloc(Stmt::Refcounting( + ModifyRc::DecRef(*symbol), + new_continuation, + )); + + // Reversed to ensure that the generated code decrements the items in the correct order. + for i in (0..length).rev() { + match index_symbols.get(&i) { + // If the symbol is known, we can decrement it (if incremented before). + Some((s, popped)) => { + if !*popped { + // Decrement the children that were not incremented before. And thus don't cancel out. + newer_continuation = arena.alloc(Stmt::Refcounting( + ModifyRc::Dec(*s), + newer_continuation, + )); + } + + // Do nothing for the children that were incremented before, as the decrement will cancel out. + } + // If the symbol is unknown, we have to get the value from the list. + // Should only happen when list elements are discarded. + None => { + let field_symbol = + environment.create_symbol(ident_ids, &format!("field_val_{i}")); + + let index_symbol = + environment.create_symbol(ident_ids, &format!("index_val_{i}")); + + let dec = arena.alloc(Stmt::Refcounting( + ModifyRc::Dec(field_symbol), + newer_continuation, + )); + + let index = arena.alloc(Stmt::Let( + field_symbol, + Expr::Call(Call { + call_type: CallType::LowLevel { + op: LowLevel::ListGetUnsafe, + update_mode: UpdateModeId::BACKEND_DUMMY, + }, + arguments: arena.alloc([*symbol, index_symbol]), + }), + item_layout, + dec, + )); + + newer_continuation = arena.alloc(Stmt::Let( + index_symbol, + Expr::Literal(Literal::Int(i128::to_ne_bytes(i as i128))), + Layout::isize(layout_interner.target_info()), + index, + )); + } + }; + } + + newer_continuation + } + _ => keep_original_decrement!(), + } + } + _ => { + // List length is unknown or the children are not reference counted, so we can't specialize. + keep_original_decrement!() + } + } +} + +/** +Get the field layouts of a union given a tag. +*/ +fn get_union_tag_layout(union_layout: UnionLayout<'_>, tag: Option) -> UnionFieldLayouts { + match (union_layout, tag) { + (UnionLayout::NonRecursive(union_layouts), Some(tag)) => UnionFieldLayouts::Found { + field_layouts: union_layouts[tag as usize], + tag, + }, + (UnionLayout::Recursive(union_layouts), Some(tag)) => UnionFieldLayouts::Found { + field_layouts: union_layouts[tag as usize], + tag, + }, + (UnionLayout::NonNullableUnwrapped(union_layouts), None) => { + // This union has just a single tag. So the tag is 0. + UnionFieldLayouts::Found { + field_layouts: union_layouts, + tag: 0, + } + } + ( + UnionLayout::NullableWrapped { + nullable_id, + other_tags, + }, + Some(tag), + ) => { + match Ord::cmp(&tag, &nullable_id) { + // tag is less than nullable_id, so the index is the same as the tag. + cmp::Ordering::Less => UnionFieldLayouts::Found { + field_layouts: other_tags[tag as usize], + tag, + }, + // tag and nullable_id are equal, so the union is null. + cmp::Ordering::Equal => UnionFieldLayouts::Null, + // tag is greater than nullable_id, so the index is the tag - 1 (as the nullable tag is in between). + cmp::Ordering::Greater => UnionFieldLayouts::Found { + field_layouts: other_tags[(tag as usize) - 1], + tag, + }, + } + } + ( + UnionLayout::NullableUnwrapped { + nullable_id, + other_fields, + }, + Some(tag), + ) => { + if tag == (nullable_id as u16) { + UnionFieldLayouts::Null + } else { + UnionFieldLayouts::Found { + field_layouts: other_fields, + tag, + } + } + } + (_, _) => UnionFieldLayouts::Unknown, + } +} + +/** +Branch on the uniqueness of a symbol. +Using a joinpoint with the continuation as the body. +*/ +fn branch_uniqueness<'a, 'i, F1, F2>( + arena: &'a Bump, + ident_ids: &'i mut IdentIds, + layout_interner: &'i mut STLayoutInterner<'a>, + environment: &DropSpecializationEnvironment<'a>, + symbol: Symbol, + unique: F1, + not_unique: F2, + continutation: &'a Stmt<'a>, +) -> &'a Stmt<'a> +where + F1: FnOnce(&mut STLayoutInterner<'a>, &mut IdentIds, &'a Stmt<'a>) -> &'a Stmt<'a>, + F2: FnOnce(&mut STLayoutInterner<'a>, &mut IdentIds, &'a Stmt<'a>) -> &'a Stmt<'a>, +{ + match continutation { + // The continuation is a single stmt. So we can insert it inline and skip creating a joinpoint. + Stmt::Ret(_) | Stmt::Jump(_, _) => { + let u = unique(layout_interner, ident_ids, continutation); + let n = not_unique(layout_interner, ident_ids, continutation); + + let switch = |unique_symbol| { + arena.alloc(Stmt::Switch { + cond_symbol: unique_symbol, + cond_layout: Layout::BOOL, + branches: &*arena.alloc([(1, BranchInfo::None, u.clone())]), + default_branch: (BranchInfo::None, n), + ret_layout: environment.layout, + }) + }; + + unique_symbol(arena, ident_ids, environment, symbol, switch) + } + // We put the continuation in a joinpoint. To prevent duplicating the content. + _ => { + let join_id = JoinPointId(environment.create_symbol(ident_ids, "uniqueness_join")); + + let jump = arena.alloc(Stmt::Jump(join_id, arena.alloc([]))); + + let u = unique(layout_interner, ident_ids, jump); + let n = not_unique(layout_interner, ident_ids, jump); + + let switch = |unique_symbol| { + arena.alloc(Stmt::Switch { + cond_symbol: unique_symbol, + cond_layout: Layout::BOOL, + branches: &*arena.alloc([( + 1, + BranchInfo::Unique { + scrutinee: symbol, + unique: true, + }, + u.clone(), + )]), + default_branch: ( + BranchInfo::Unique { + scrutinee: symbol, + unique: false, + }, + n, + ), + ret_layout: environment.layout, + }) + }; + + let unique = unique_symbol(arena, ident_ids, environment, symbol, switch); + + arena.alloc(Stmt::Join { + id: join_id, + parameters: arena.alloc([]), + body: continutation, + remainder: unique, + }) + } + } +} + +fn unique_symbol<'a, 'i>( + arena: &'a Bump, + ident_ids: &'i mut IdentIds, + environment: &DropSpecializationEnvironment<'a>, + symbol: Symbol, + continuation: impl FnOnce(Symbol) -> &'a mut Stmt<'a>, +) -> &'a Stmt<'a> { + let is_unique = environment.create_symbol(ident_ids, "is_unique"); + + arena.alloc(Stmt::Let( + is_unique, + Expr::Call(Call { + call_type: CallType::LowLevel { + op: LowLevel::RefCountIsUnique, + update_mode: UpdateModeId::BACKEND_DUMMY, + }, + arguments: arena.alloc([symbol]), + }), + Layout::BOOL, + continuation(is_unique), + )) +} + +enum UnionFieldLayouts<'a> { + Found { + field_layouts: &'a [InLayout<'a>], + tag: Tag, + }, + Unknown, + Null, +} + +type Index = u64; + +type Parent = Symbol; + +type Child = Symbol; + +type Tag = u16; + +#[derive(Clone)] +struct DropSpecializationEnvironment<'a> { + arena: &'a Bump, + home: ModuleId, + layout: InLayout<'a>, + + symbol_layouts: MutMap>, + + // Keeps track of which parent symbol is indexed by which child symbol for structs + struct_children: MutMap>, + + // Keeps track of which parent symbol is indexed by which child symbol for unions + union_children: MutMap>, + + // Keeps track of which parent symbol is indexed by which child symbol for boxes + box_children: MutMap>, + + // Keeps track of which parent symbol is indexed by which child symbol for lists + list_children: MutMap>, + + // Keeps track of all incremented symbols. + incremented_symbols: CountingMap, + + // Map containing the current known tag of a layout. + symbol_tag: MutMap, + + // Map containing the current known index value of a symbol. + symbol_index: MutMap, + + // Map containing the current known length of a list. + list_length: MutMap, + + // A map containing the minimum number of symbol increments from jumps for a joinpoint. + jump_incremented_symbols: MutMap>, + + // A map containing the expected number of symbol increments from joinpoints for a jump. + join_incremented_symbols: MutMap, +} + +#[derive(Clone)] +struct JoinUsage { + join_consumes: CountingMap, + join_returns: CountingMap, +} + +impl<'a> DropSpecializationEnvironment<'a> { + fn new(arena: &'a Bump, home: ModuleId, layout: InLayout<'a>) -> Self { + Self { + arena, + home, + layout, + symbol_layouts: MutMap::default(), + struct_children: MutMap::default(), + union_children: MutMap::default(), + box_children: MutMap::default(), + list_children: MutMap::default(), + incremented_symbols: CountingMap::new(), + symbol_tag: MutMap::default(), + symbol_index: MutMap::default(), + list_length: MutMap::default(), + jump_incremented_symbols: MutMap::default(), + join_incremented_symbols: MutMap::default(), + } + } + + fn create_symbol(&self, ident_ids: &mut IdentIds, debug_name: &str) -> Symbol { + let ident_id = ident_ids.add_str(debug_name); + Symbol::new(self.home, ident_id) + } + + fn add_symbol_layout(&mut self, symbol: Symbol, layout: InLayout<'a>) { + self.symbol_layouts.insert(symbol, layout); + } + + fn get_symbol_layout(&self, symbol: &Symbol) -> &InLayout<'a> { + self.symbol_layouts + .get(symbol) + .expect("All symbol layouts should be known.") + } + + fn add_struct_child(&mut self, parent: Parent, child: Child, index: Index) { + self.struct_children + .entry(parent) + .or_insert_with(|| Vec::new_in(self.arena)) + .push((child, index)); + } + + fn add_union_child(&mut self, parent: Parent, child: Child, tag: u16, index: Index) { + self.union_children + .entry(parent) + .or_insert_with(|| Vec::new_in(self.arena)) + .push((child, tag, index)); + } + + fn add_list_child(&mut self, parent: Parent, child: Child, index: u64) { + self.list_children + .entry(parent) + .or_insert_with(|| Vec::new_in(self.arena)) + .push((child, index)); + } + + fn add_list_child_symbol(&mut self, parent: Parent, child: Child, index: &Symbol) { + if let Some(index) = self.symbol_index.get(index) { + self.add_list_child(parent, child, *index) + } + } + + fn get_children(&self, parent: &Parent) -> Vec<'a, Symbol> { + let mut res = Vec::new_in(self.arena); + + if let Some(children) = self.struct_children.get(parent) { + res.extend(children.iter().rev().map(|(child, _)| child)); + } + + if let Some(children) = self.union_children.get(parent) { + res.extend(children.iter().rev().map(|(child, _, _)| child)); + } + + if let Some(children) = self.box_children.get(parent) { + res.extend(children.iter().rev()); + } + + if let Some(children) = self.list_children.get(parent) { + res.extend(children.iter().rev().map(|(child, _)| child)); + } + + res + } +} + +/** +Reference count information +*/ +enum RC { + // Rc is important, moving an increment to after this function might break the program. + // E.g. if the function checks for uniqueness and behaves differently based on that. + Rc, + // Rc is not important, moving an increment to after this function should have no effect. + NoRc, + // Rc effect is unknown. + Uknown, +} + +/* +Returns whether the reference count of arguments to this function is relevant to the program. + */ +fn low_level_no_rc(lowlevel: &LowLevel) -> RC { + use LowLevel::*; + + match lowlevel { + Unreachable => RC::Uknown, + ListLen | StrIsEmpty | StrToScalars | StrCountGraphemes | StrGraphemes + | StrCountUtf8Bytes | StrGetCapacity | ListGetCapacity => RC::NoRc, + ListWithCapacity | StrWithCapacity => RC::NoRc, + ListReplaceUnsafe => RC::Rc, + StrGetUnsafe | ListGetUnsafe => RC::NoRc, + ListConcat => RC::Rc, + StrConcat => RC::Rc, + StrSubstringUnsafe => RC::NoRc, + StrReserve => RC::Rc, + StrAppendScalar => RC::Rc, + StrGetScalarUnsafe => RC::NoRc, + StrTrim => RC::Rc, + StrTrimStart => RC::Rc, + StrTrimEnd => RC::Rc, + StrSplit => RC::NoRc, + StrToNum => RC::NoRc, + ListPrepend => RC::Rc, + StrJoinWith => RC::NoRc, + ListMap | ListMap2 | ListMap3 | ListMap4 | ListSortWith => RC::Rc, + + ListAppendUnsafe + | ListReserve + | ListSublist + | ListDropAt + | ListSwap + | ListReleaseExcessCapacity + | StrReleaseExcessCapacity => RC::Rc, + + Eq | NotEq => RC::NoRc, + + And | Or | NumAdd | NumAddWrap | NumAddChecked | NumAddSaturated | NumSub | NumSubWrap + | NumSubChecked | NumSubSaturated | NumMul | NumMulWrap | NumMulSaturated + | NumMulChecked | NumGt | NumGte | NumLt | NumLte | NumCompare | NumDivFrac + | NumDivTruncUnchecked | NumDivCeilUnchecked | NumRemUnchecked | NumIsMultipleOf + | NumPow | NumPowInt | NumBitwiseAnd | NumBitwiseXor | NumBitwiseOr | NumShiftLeftBy + | NumShiftRightBy | NumShiftRightZfBy => RC::NoRc, + + NumToStr + | NumAbs + | NumNeg + | NumSin + | NumCos + | NumTan + | NumSqrtUnchecked + | NumLogUnchecked + | NumRound + | NumCeiling + | NumFloor + | NumToFrac + | Not + | NumIsNan + | NumIsInfinite + | NumIsFinite + | NumAtan + | NumAcos + | NumAsin + | NumIntCast + | NumToIntChecked + | NumToFloatCast + | NumToFloatChecked + | NumCountLeadingZeroBits + | NumCountTrailingZeroBits + | NumCountOneBits => RC::NoRc, + NumBytesToU16 => RC::NoRc, + NumBytesToU32 => RC::NoRc, + NumBytesToU64 => RC::NoRc, + NumBytesToU128 => RC::NoRc, + I128OfDec => RC::NoRc, + DictPseudoSeed => RC::NoRc, + StrStartsWith | StrEndsWith => RC::NoRc, + StrStartsWithScalar => RC::NoRc, + StrFromUtf8Range => RC::Rc, + StrToUtf8 => RC::Rc, + StrRepeat => RC::NoRc, + StrFromInt | StrFromFloat => RC::NoRc, + Hash => RC::NoRc, + + ListIsUnique => RC::Rc, + + BoxExpr | UnboxExpr => { + unreachable!("These lowlevel operations are turned into mono Expr's") + } + + // only inserted for internal purposes. RC should not touch it + PtrStore => RC::NoRc, + PtrLoad => RC::NoRc, + PtrCast => RC::NoRc, + + PtrClearTagId | RefCountIncRcPtr | RefCountDecRcPtr | RefCountIncDataPtr + | RefCountDecDataPtr | RefCountIsUnique => { + unreachable!("Only inserted *after* borrow checking: {:?}", lowlevel); + } + + SetJmp | LongJmp | SetLongJmpBuffer => unreachable!("only inserted in dev backend codegen"), + } +} + +/// Map that contains a count for each key. +/// Keys with a count of 0 are kept around, so that it can be seen that they were once present. +#[derive(Clone, PartialEq, Eq)] +struct CountingMap { + map: MutMap, +} + +impl CountingMap +where + K: Eq + std::hash::Hash + Clone, +{ + fn new() -> Self { + Self { + map: MutMap::default(), + } + } + + fn insert(&mut self, key: K) { + self.insert_count(key, 1); + } + + fn insert_count(&mut self, key: K, count: u64) { + self.map + .entry(key) + .and_modify(|c| *c += count) + .or_insert(count); + } + + fn pop(&mut self, key: &K) -> bool { + match self.map.get_mut(key) { + Some(0) => false, + Some(c) => { + *c -= 1; + true + } + None => false, + } + } + + fn contains(&self, symbol: &K) -> bool { + self.map.contains_key(symbol) + } + + fn drain(&mut self) -> Self { + let res = self.clone(); + for (_, v) in self.map.iter_mut() { + *v = 0; + } + res + } + + fn clear(&mut self) { + self.map.clear() + } + + fn is_empty(&self) -> bool { + self.map.is_empty() + } +} diff --git a/compiler/mono/src/expand_rc.rs b/crates/compiler/mono/src/expand_rc.rs similarity index 100% rename from compiler/mono/src/expand_rc.rs rename to crates/compiler/mono/src/expand_rc.rs diff --git a/crates/compiler/mono/src/inc_dec.rs b/crates/compiler/mono/src/inc_dec.rs new file mode 100644 index 0000000000..96c529bbb1 --- /dev/null +++ b/crates/compiler/mono/src/inc_dec.rs @@ -0,0 +1,1383 @@ +// This program was written by Jelle Teeuwissen within a final +// thesis project of the Computing Science master program at Utrecht +// University under supervision of Wouter Swierstra (w.s.swierstra@uu.nl). + +// Implementation based of Perceus: Garbage Free Reference Counting with Reuse +// https://www.microsoft.com/en-us/research/uploads/prod/2021/06/perceus-pldi21.pdf + +use std::{collections::HashMap, hash::BuildHasherDefault}; + +use bumpalo::collections::{CollectIn, Vec}; +use bumpalo::Bump; +use roc_collections::{all::WyHash, MutMap, MutSet}; +use roc_error_macros::internal_error; +use roc_module::low_level::LowLevel; +use roc_module::{low_level::LowLevelWrapperType, symbol::Symbol}; + +use crate::ir::ErasedField; +use crate::{ + ir::{ + BranchInfo, Call, CallType, Expr, HigherOrderLowLevel, JoinPointId, ListLiteralElement, + ModifyRc, Param, Proc, ProcLayout, Stmt, + }, + layout::{InLayout, LayoutInterner, STLayoutInterner}, + low_level::HigherOrder, +}; + +/** +Insert the reference count operations for procedures. +*/ +pub fn insert_inc_dec_operations<'a>( + arena: &'a Bump, + layout_interner: &STLayoutInterner<'a>, + procedures: &mut HashMap<(Symbol, ProcLayout), Proc<'a>, BuildHasherDefault>, +) { + // All calls to lowlevels are wrapped in another function to help with type inference and return/parameter layouts. + // But this lowlevel might get inlined into the caller of the wrapper and thus removing any reference counting operations. + // Thus, these rc operations are performed on the caller of the wrapper instead, and we skip rc on the lowlevel. + // It might be possible to inline the lowlevels at this point already, + // but previous attempt conflicted as the parameters layouts and return layout do not match. + for ((symbol, _layout), proc) in procedures.iter_mut() { + if matches!( + LowLevelWrapperType::from_symbol(*symbol), + LowLevelWrapperType::NotALowLevelWrapper + ) { + let symbol_rc_types_env = SymbolRcTypesEnv::from_layout_interner(layout_interner); + insert_inc_dec_operations_proc(arena, symbol_rc_types_env, proc); + } + } +} + +/** +Enum indicating whether a symbol should be reference counted or not. +This includes layouts that themselves can be stack allocated but that contain a heap allocated item. +*/ +#[derive(Copy, Clone)] +enum VarRcType { + ReferenceCounted, + NotReferenceCounted, +} + +/* +A map keeping track of which symbols are reference counted and which are not. +Implemented as two sets for efficiency. +*/ +#[derive(Clone, Default)] +struct SymbolRcTypes { + reference_counted: MutSet, + not_reference_counted: MutSet, +} + +impl SymbolRcTypes { + /** + Insert a symbol with the given reference count type in the correct set. + */ + fn insert(&mut self, symbol: Symbol, var_rc_type: VarRcType) { + match var_rc_type { + VarRcType::ReferenceCounted => { + self.reference_counted.insert(symbol); + } + VarRcType::NotReferenceCounted => { + self.not_reference_counted.insert(symbol); + } + } + } + + /** + Get the reference count type of a symbol. + */ + fn get(&self, symbol: &Symbol) -> Option { + if self.reference_counted.contains(symbol) { + debug_assert!(!self.not_reference_counted.contains(symbol)); + Some(VarRcType::ReferenceCounted) + } else if self.not_reference_counted.contains(symbol) { + Some(VarRcType::NotReferenceCounted) + } else { + None + } + } +} + +/** +Environment to keep track which of the symbols should be reference counted and which ones should not. + */ +#[derive(Clone)] +struct SymbolRcTypesEnv<'a, 'i> { + // A map keeping track of which symbols are reference counted and which are not. + symbols_rc_type: SymbolRcTypes, + + layout_interner: &'i STLayoutInterner<'a>, +} + +impl<'a, 'i> SymbolRcTypesEnv<'a, 'i> { + /** + Create a new SymbolRcTypesEnv from a layout interner. + */ + fn from_layout_interner(layout_interner: &'i STLayoutInterner<'a>) -> SymbolRcTypesEnv<'a, 'i> { + SymbolRcTypesEnv { + symbols_rc_type: SymbolRcTypes::default(), + layout_interner, + } + } + + /** + Insert the reference count types of all symbols in a procedure. + */ + fn insert_symbols_rc_type_proc(&mut self, proc: &Proc<'a>) { + // First collect the argument types. + for (layout, symbol) in proc.args.iter() { + self.insert_symbol_layout_rc_type(symbol, layout); + } + + // Then collect the types of the symbols in the body. + self.insert_symbols_rc_type_stmt(&proc.body); + } + + /** + Insert the reference count types of all symbols in a statement. + */ + fn insert_symbols_rc_type_stmt(&mut self, stmt: &Stmt<'a>) { + match stmt { + Stmt::Let( + binding, + // Expressions can be omitted, as they won't create new symbols. + _expr, + layout, + continuation, + ) => { + self.insert_symbol_layout_rc_type(binding, layout); + self.insert_symbols_rc_type_stmt(continuation); + } + Stmt::Switch { + // The switch condition is an integer and thus not reference counted. + cond_symbol: _, + cond_layout: _, + branches, + default_branch, + ret_layout: _, + } => { + // Collect the types of the symbols in all the branches, including the default one. + for (info, stmt) in branches + .iter() + .map(|(_branch, info, stmt)| (info, stmt)) + .chain([(&default_branch.0, default_branch.1)]) + { + if let BranchInfo::Constructor { + scrutinee, + layout, + tag_id: _, + } = info + { + self.insert_symbol_layout_rc_type(scrutinee, layout); + } + + self.insert_symbols_rc_type_stmt(stmt); + } + } + Stmt::Ret(_symbol) => { + // The return does not introduce new symbols. + } + Stmt::Refcounting(_, _) => unreachable!( + "Refcounting operations should not be present in the AST at this point." + ), + Stmt::Expect { remainder, .. } + | Stmt::ExpectFx { remainder, .. } + | Stmt::Dbg { remainder, .. } => { + self.insert_symbols_rc_type_stmt(remainder); + } + Stmt::Join { + id: _, + parameters, + body, + remainder: continuation, + } => { + for parameter in parameters.iter() { + self.insert_symbol_layout_rc_type(¶meter.symbol, ¶meter.layout); + } + + self.insert_symbols_rc_type_stmt(body); + self.insert_symbols_rc_type_stmt(continuation); + } + Stmt::Jump(_, _) => { + // A join point does not introduce new symbols. + } + Stmt::Crash(_, _) => { + // A crash does not introduce new symbols. + } + } + } + + /* + Insert the reference count type of a symbol given its layout. + */ + fn insert_symbol_layout_rc_type(&mut self, symbol: &Symbol, layout: &InLayout) { + // This will reference count the entire struct, even if only one field is reference counted. + // In another pass we can inline these operations, potentially improving reuse. + let contains_refcounted = self.layout_interner.contains_refcounted(*layout); + let rc_type = match contains_refcounted { + true => VarRcType::ReferenceCounted, + false => VarRcType::NotReferenceCounted, + }; + self.symbols_rc_type.insert(*symbol, rc_type); + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum Ownership { + Owned, + Borrowed, +} + +impl Ownership { + fn is_owned(&self) -> bool { + matches!(self, Ownership::Owned) + } + + fn is_borrowed(&self) -> bool { + matches!(self, Ownership::Borrowed) + } +} + +type SymbolsOwnership = MutMap; + +/** +Type containing data about the symbols consumption of a join point. +*/ +type JoinPointConsumption = MutSet; + +/** +The environment for the reference counting pass. +Contains the symbols rc types and the ownership. +*/ +#[derive(Clone)] +struct RefcountEnvironment<'v> { + // Keep track which symbols are reference counted and which are not. + symbols_rc_types: &'v SymbolRcTypes, + // The Koka implementation assumes everything that is not owned to be borrowed. + symbols_ownership: SymbolsOwnership, + jointpoint_closures: MutMap, +} + +impl<'v> RefcountEnvironment<'v> { + /** + Retrieve the rc type of a symbol. + */ + fn get_symbol_rc_type(&mut self, symbol: &Symbol) -> VarRcType { + self.symbols_rc_types + .get(symbol) + .expect("symbol should have rc type") + } + + /* + Retrieve whether the symbol is owned or borrowed. + If it was owned, set it to borrowed (as it is consumed/the symbol can be used as owned only once without incrementing). + If the symbol is not reference counted, do nothing and return None. + */ + fn consume_symbol(&mut self, symbol: &Symbol) -> Option { + if !self.symbols_ownership.contains_key(symbol) { + return None; + } + + // Consume the symbol and return the previous ownership. + Some(self.consume_rc_symbol(*symbol)) + } + + /* + Retrieve whether the symbol is owned or borrowed. + If it was owned, set it to borrowed (as it is consumed/the symbol can be used as owned only once without incrementing). + */ + fn consume_rc_symbol(&mut self, symbol: Symbol) -> Ownership { + // Consume the symbol by setting it to borrowed (if it was owned before), and return the previous ownership. + match self.symbols_ownership.insert(symbol, Ownership::Borrowed) { + Some(ownership) => ownership, + None => internal_error!("Expected symbol {symbol:?} to be in environment"), + } + } + + /** + Retrieve the ownership of a symbol. + If the symbol is not reference counted, it will None. + */ + fn get_symbol_ownership(&self, symbol: &Symbol) -> Option<&Ownership> { + self.symbols_ownership.get(symbol) + } + + /** + Add a symbol to the environment if it is reference counted. + */ + fn add_symbol(&mut self, symbol: Symbol) { + match self.get_symbol_rc_type(&symbol) { + VarRcType::ReferenceCounted => { + self.symbols_ownership.insert(symbol, Ownership::Owned); + } + VarRcType::NotReferenceCounted => { + // If this symbol is not reference counted, we don't need to do anything. + } + } + } + + /** + Remove a symbol from the environment. + Is used when a symbol is no longer in scope (before a let binding). + */ + fn remove_symbol(&mut self, symbol: Symbol) { + self.symbols_ownership.remove(&symbol); + } + + /** + Add a joinpoint id and the consumed closure to the environment. + Used when analyzing a join point. So that a jump can update the environment on call. + */ + fn add_joinpoint_consumption( + &mut self, + joinpoint_id: JoinPointId, + consumption: JoinPointConsumption, + ) { + self.jointpoint_closures.insert(joinpoint_id, consumption); + } + + /** + Get the consumed closure from a join point id. + */ + fn get_joinpoint_consumption(&self, joinpoint_id: JoinPointId) -> &JoinPointConsumption { + self.jointpoint_closures + .get(&joinpoint_id) + .expect("Expected closure to be in environment") + } + + /** + Remove a joinpoint id and the consumed closure from the environment. + Used after analyzing the continuation of a join point. + */ + fn remove_joinpoint_consumption(&mut self, joinpoint_id: JoinPointId) { + let closure = self.jointpoint_closures.remove(&joinpoint_id); + debug_assert!(closure.is_some(), "Expected closure to be in environment"); + } + + /** + Return owned usages. + Collect the usage of all the reference counted symbols in the iterator and return as a map. + */ + fn owned_usages(&self, symbols: impl IntoIterator) -> MutMap { + // A groupby or something similar would be nice here. + let mut symbol_usage = MutMap::default(); + for symbol in symbols { + match self.symbols_rc_types.get(&symbol) { + // If the symbol is reference counted, we need to increment the usage count. + Some(VarRcType::ReferenceCounted) => { + *symbol_usage.entry(symbol).or_default() += 1; + } + // If the symbol is not reference counted, we don't need to do anything. + Some(VarRcType::NotReferenceCounted) => continue, + None => { + internal_error!("symbol {symbol:?} does not have an rc type") + } + } + } + symbol_usage + } + + /** + Filter the given symbols to only contain reference counted symbols. + */ + fn borrowed_usages(&self, symbols: impl IntoIterator) -> MutSet { + symbols + .into_iter() + .filter(|symbol| { + // If the symbol is reference counted, we need to increment the usage count. + // If the symbol is not reference counted, we don't need to do anything. + matches!( + self.symbols_rc_types + .get(symbol) + .expect("Expected symbol to be in the map"), + VarRcType::ReferenceCounted + ) + }) + .collect() + } +} + +/** + Insert the reference counting operations into a statement. +*/ +fn insert_inc_dec_operations_proc<'a>( + arena: &'a Bump, + mut symbol_rc_types_env: SymbolRcTypesEnv<'a, '_>, + proc: &mut Proc<'a>, +) { + // Clone the symbol_rc_types_env and insert the symbols in the current procedure. + // As the symbols should be limited in scope for the current proc. + symbol_rc_types_env.insert_symbols_rc_type_proc(proc); + + let mut environment = RefcountEnvironment { + symbols_rc_types: &symbol_rc_types_env.symbols_rc_type, + symbols_ownership: MutMap::default(), + jointpoint_closures: MutMap::default(), + }; + + // Add all arguments to the environment (if they are reference counted) + let proc_symbols = proc.args.iter().map(|(_layout, symbol)| symbol); + for symbol in proc_symbols.clone() { + environment.add_symbol(*symbol); + } + + // Update the body with reference count statements. + let new_body = insert_refcount_operations_stmt(arena, &mut environment, &proc.body); + + // Insert decrement statements for unused parameters (which are still marked as owned). + let rc_proc_symbols = proc_symbols + .filter(|symbol| environment.symbols_ownership.contains_key(symbol)) + .copied() + .collect_in::>(arena); + let newer_body = + consume_and_insert_dec_stmts(arena, &mut environment, rc_proc_symbols, new_body); + + // Assert that just the arguments are in the environment. And (after decrementing the unused ones) that they are all borrowed. + debug_assert!(environment + .symbols_ownership + .iter() + .all(|(symbol, ownership)| { + // All symbols should be borrowed. + ownership.is_borrowed() && proc.args.iter().any(|(_layout, s)| s == symbol) + })); + + proc.body = newer_body.clone(); +} + +/** +Given an environment, insert the reference counting operations for a statement. +Assuming that a symbol can only be defined once (no binding to the same symbol multiple times). +*/ +fn insert_refcount_operations_stmt<'v, 'a>( + arena: &'a Bump, + environment: &mut RefcountEnvironment<'v>, + stmt: &Stmt<'a>, +) -> &'a Stmt<'a> { + match &stmt { + // The expression borrows the values owned (used) by the continuation. + Stmt::Let(_, _, _, _) => { + // Collect all the subsequent let bindings (including the current one). + // To prevent the stack from overflowing when there are many let bindings. + let mut triples = vec![]; + let mut current_stmt = stmt; + while let Stmt::Let(binding, expr, layout, next_stmt) = current_stmt { + triples.push((binding, expr, layout)); + current_stmt = next_stmt + } + + debug_assert!( + !triples.is_empty(), + "Expected at least one let binding in the vector" + ); + debug_assert!( + !matches!(current_stmt, Stmt::Let(_, _, _, _)), + "All let bindings should be in the vector" + ); + + for (binding, _, _) in triples.iter() { + environment.add_symbol(**binding); // Add the bound symbol to the environment. As it can be used in the continuation. + } + + triples + .into_iter() + .rev() + // First evaluate the continuation and let it consume it's free symbols. + .fold( + insert_refcount_operations_stmt(arena, environment, current_stmt), + |new_stmt, (binding, expr, layout)| { + // If the binding is still owned in the environment, it is not used in the continuation and we can drop it right away. + let new_stmt_without_unused = match environment + .get_symbol_ownership(binding) + { + Some(Ownership::Owned) => insert_dec_stmt(arena, *binding, new_stmt), + _ => new_stmt, + }; + + // And as the symbol should not be in scope before this let binding, remove it from the environment. + environment.remove_symbol(*binding); + + insert_refcount_operations_binding( + arena, + environment, + binding, + expr, + layout, + new_stmt_without_unused, + ) + }, + ) + } + Stmt::Switch { + cond_symbol, + cond_layout, + branches, + default_branch, + ret_layout, + } => { + let new_branches = branches + .iter() + .map(|(label, info, branch)| { + let mut branch_env = environment.clone(); + + let new_branch = + insert_refcount_operations_stmt(arena, &mut branch_env, branch); + + (*label, info.clone(), new_branch, branch_env) + }) + .collect_in::>(arena); + + let new_default_branch = { + let (info, branch) = default_branch; + + let mut branch_env = environment.clone(); + let new_branch = insert_refcount_operations_stmt(arena, &mut branch_env, branch); + + (info.clone(), new_branch, branch_env) + }; + + // Determine what symbols are consumed in some of the branches. + // So we can make sure they are consumed in the current environment and all branches. + let consume_symbols = { + let branch_envs = { + let mut branch_environments = + Vec::with_capacity_in(new_branches.len() + 1, arena); + + for (_, _, _, branch_env) in new_branches.iter() { + branch_environments.push(branch_env); + } + + branch_environments.push(&new_default_branch.2); + + branch_environments + }; + + { + let mut consume_symbols = MutSet::default(); + + // If the symbol is currently borrowed, it must be borrowed in all branches and we don't have to do anything. + for (symbol, _) in environment + .symbols_ownership + .iter() + .filter(|(_, o)| o.is_owned()) + { + let consumed = + branch_envs + .iter() + .any(|branch_env: &&RefcountEnvironment<'v>| { + match branch_env.get_symbol_ownership(symbol) { + None => internal_error!( + "symbol {symbol:?} in the current env should be in the branch's env" + ), + Some(ownership) => matches!(ownership, Ownership::Borrowed), + } + }); + if consumed { + // If the symbol is currently owned, and not in a some branches, it must be consumed in all branches + consume_symbols.insert(*symbol); + } + // Otherwise it can stay owned. + } + + consume_symbols + } + }; + + // Given the consume_symbols we can determine what additional symbols should be dropped in each branch. + let msg = "All symbols defined in the current environment should be in the environment of the branches."; + let newer_branches = Vec::from_iter_in( + new_branches + .into_iter() + .map(|(label, info, branch, branch_env)| { + // If the symbol is owned in the branch, it is not used in the branch and we can drop it. + let consume_symbols_branch = + consume_symbols.iter().copied().filter(|consume_symbol| { + branch_env + .get_symbol_ownership(consume_symbol) + .expect(msg) + .is_owned() + }); + + let newer_branch = insert_dec_stmts(arena, consume_symbols_branch, branch); + (label, info, newer_branch.clone()) + }), + arena, + ) + .into_bump_slice(); + + let newer_default_branch = { + let (info, branch, branch_env) = new_default_branch; + // If the symbol is owned in the branch, it is not used in the branch and we can drop it. + let msg = "All symbols defined in the current environment should be in the environment of the branches."; + let consume_symbols_branch = + consume_symbols.iter().copied().filter(|consume_symbol| { + branch_env + .get_symbol_ownership(consume_symbol) + .expect(msg) + .is_owned() + }); + + let newer_branch = insert_dec_stmts(arena, consume_symbols_branch, branch); + + (info, newer_branch) + }; + + // In addition to updating the branches, we need to update the current environment. + for consume_symbol in consume_symbols.iter() { + environment.consume_symbol(consume_symbol); + } + + arena.alloc(Stmt::Switch { + cond_symbol: *cond_symbol, + cond_layout: *cond_layout, + branches: newer_branches, + default_branch: newer_default_branch, + ret_layout: *ret_layout, + }) + } + Stmt::Ret(s) => { + let ownership = environment.consume_symbol(s); + debug_assert!(matches!(ownership, None | Some(Ownership::Owned))); // the return value should be owned or not reference counted at the return. + return arena.alloc(Stmt::Ret(*s)); + } + Stmt::Refcounting(_, _) => unreachable!("refcounting should not be in the AST yet"), + Stmt::Expect { + condition, + region, + lookups, + variables, + remainder, + } => { + let new_remainder = insert_refcount_operations_stmt(arena, environment, remainder); + + let newer_remainder = consume_and_insert_dec_stmts( + arena, + environment, + environment.borrowed_usages(lookups.iter().copied()), + new_remainder, + ); + + arena.alloc(Stmt::Expect { + condition: *condition, + region: *region, + lookups, + variables, + remainder: newer_remainder, + }) + } + Stmt::ExpectFx { + condition, + region, + lookups, + variables, + remainder, + } => { + let new_remainder = insert_refcount_operations_stmt(arena, environment, remainder); + + let newer_remainder = consume_and_insert_dec_stmts( + arena, + environment, + environment.borrowed_usages(lookups.iter().copied()), + new_remainder, + ); + + arena.alloc(Stmt::ExpectFx { + condition: *condition, + region: *region, + lookups, + variables, + remainder: newer_remainder, + }) + } + Stmt::Dbg { + symbol, + variable, + remainder, + } => { + let new_remainder = insert_refcount_operations_stmt(arena, environment, remainder); + + let newer_remainder = consume_and_insert_dec_stmts( + arena, + environment, + environment.borrowed_usages([*symbol]), + new_remainder, + ); + + arena.alloc(Stmt::Dbg { + symbol: *symbol, + variable: *variable, + remainder: newer_remainder, + }) + } + Stmt::Join { + id: joinpoint_id, + parameters, + body, + remainder, + } => { + // Assuming that the values in the closure of the body of this jointpoint are already bound. + // Assuming that all symbols are still owned. (So that we can determine what symbols got consumed in the join point.) + debug_assert!(environment + .symbols_ownership + .iter() + .all(|(_, ownership)| ownership.is_owned())); + + let mut body_env = environment.clone(); + + let parameter_symbols_set = parameters + .iter() + .map(|Param { symbol, .. }| *symbol) + .collect::>(); + for symbol in parameter_symbols_set.iter().copied() { + body_env.add_symbol(symbol) + } + + /* + We use a fixed point iteration to determine what symbols are consumed in the join point. + We need to do this because the join point might be called recursively. + If we consume symbols in the closure, like y in the example below: + + add = \x, y -> + jp 1 \acc, count -> + if count == 0 + then acc + y + else jump 1 (acc+1) (count-1) + jump 1 0 x + + If we were to just use an empty consumption without iteration, + the analysis will not know that y is still used in the jump and thus will drop if after the else. + */ + + let mut joinpoint_consumption = MutSet::default(); + + let (new_body, mut new_body_environment) = loop { + // Copy the env to make sure each iteration has a fresh environment. + let mut current_body_env = body_env.clone(); + + current_body_env + .add_joinpoint_consumption(*joinpoint_id, joinpoint_consumption.clone()); + let new_body = insert_refcount_operations_stmt(arena, &mut current_body_env, body); + current_body_env.remove_joinpoint_consumption(*joinpoint_id); + + // We save the parameters consumed by this join point. So we can do the same when we jump to this joinpoint. + // This includes parameter symbols, this might help with unused closure symbols. + let current_joinpoint_consumption = { + let consumed_symbols = current_body_env + .symbols_ownership + .iter() + .filter_map(|(symbol, ownership)| { + ownership.is_borrowed().then_some(*symbol) + }) + .collect::>(); + + consumed_symbols + .difference(¶meter_symbols_set) + .copied() + .collect::>() + }; + + if joinpoint_consumption == current_joinpoint_consumption { + break (new_body, current_body_env); + } else { + debug_assert!( + current_joinpoint_consumption.is_superset(&joinpoint_consumption), + "The current consumption should be a superset of the previous consumption. + As the consumption should only ever increase. + Otherwise we will be looping forever." + ); + joinpoint_consumption = current_joinpoint_consumption; + } + }; + + // Insert decrement statements for unused parameters (which are still marked as owned). + // If the parameters are never dead, this could be skipped. + let dead_symbols = parameters + .iter() + .filter_map(|Param { symbol, .. }| { + new_body_environment + .symbols_ownership + .contains_key(symbol) + .then_some(*symbol) + }) + .collect_in::>(arena); + let newer_body = consume_and_insert_dec_stmts( + arena, + &mut new_body_environment, + dead_symbols, + new_body, + ); + + environment.add_joinpoint_consumption(*joinpoint_id, joinpoint_consumption); + let new_remainder = insert_refcount_operations_stmt(arena, environment, remainder); + environment.remove_joinpoint_consumption(*joinpoint_id); + + arena.alloc(Stmt::Join { + id: *joinpoint_id, + parameters, + body: newer_body, + remainder: new_remainder, + }) + } + Stmt::Jump(joinpoint_id, arguments) => { + let consumed_symbols = environment.get_joinpoint_consumption(*joinpoint_id); + for consumed_symbol in consumed_symbols.clone().iter() { + environment.consume_symbol(consumed_symbol); + } + + let new_jump = arena.alloc(Stmt::Jump(*joinpoint_id, arguments)); + + // Note that this should only insert increments if a later join point has a current parameter as consumed closure. + consume_and_insert_inc_stmts( + arena, + environment, + environment.owned_usages(arguments.iter().copied()), + new_jump, + ) + } + Stmt::Crash(symbol, crash_tag) => { + // We don't have to worry about reference counting *after* the crash. + // But we do need to make sure the symbol of the crash is live until the crash. + // So we insert increment statements for the symbol (if it is reference counted) + let new_crash = arena.alloc(Stmt::Crash(*symbol, *crash_tag)); + + consume_and_insert_inc_stmts( + arena, + environment, + environment.owned_usages([*symbol]), + new_crash, + ) + } + } +} + +fn insert_refcount_operations_binding<'a>( + arena: &'a Bump, + environment: &mut RefcountEnvironment, + binding: &Symbol, + expr: &Expr<'a>, + layout: &InLayout<'a>, + stmt: &'a Stmt<'a>, +) -> &'a Stmt<'a> { + macro_rules! dec_borrowed { + ($symbols:expr, $stmt:expr) => { + // Insert decrement operations for borrowed symbols if they are currently owned. + consume_and_insert_dec_stmts( + arena, + environment, + environment.borrowed_usages($symbols), + stmt, + ) + }; + } + + macro_rules! new_let { + ($stmt:expr) => { + arena.alloc(Stmt::Let(*binding, expr.clone(), *layout, $stmt)) + }; + } + + macro_rules! inc_owned { + ($symbols:expr, $stmt:expr) => { + // Insert increment operations for the owned symbols used in the expression. + consume_and_insert_inc_stmts( + arena, + environment, + environment.owned_usages($symbols), + $stmt, + ) + }; + } + + match expr { + Expr::Literal(_) + | Expr::NullPointer + | Expr::FunctionPointer { .. } + | Expr::EmptyArray + | Expr::RuntimeErrorFunction(_) => { + // Literals, empty arrays, and runtime errors are not (and have nothing) reference counted. + new_let!(stmt) + } + + Expr::Tag { arguments, .. } | Expr::Struct(arguments) => { + let new_let = new_let!(stmt); + + inc_owned!(arguments.iter().copied(), new_let) + } + + Expr::ErasedMake { value, callee: _ } => { + let new_let = new_let!(stmt); + + if let Some(value) = value { + inc_owned!([*value], new_let) + } else { + new_let + } + } + + Expr::ErasedLoad { symbol, field } => { + let new_let = new_let!(stmt); + + match field { + ErasedField::Value => inc_owned!([*symbol], new_let), + ErasedField::Callee | ErasedField::ValuePtr => new_let, + } + } + + Expr::GetTagId { structure, .. } + | Expr::StructAtIndex { structure, .. } + | Expr::UnionAtIndex { structure, .. } + | Expr::GetElementPointer { structure, .. } => { + // All structures are alive at this point and don't have to be copied in order to take an index out/get tag id/copy values to the stack. + // But we do want to make sure to decrement this item if it is the last reference. + let new_stmt = dec_borrowed!([*structure], stmt); + + // Add an increment operation for the binding if it is reference counted and if the expression creates a new reference to a value. + let newer_stmt = if matches!( + environment.get_symbol_rc_type(binding), + VarRcType::ReferenceCounted + ) { + match expr { + Expr::StructAtIndex { .. } + | Expr::UnionAtIndex { .. } + | Expr::GetElementPointer { .. } => { + insert_inc_stmt(arena, *binding, 1, new_stmt) + } + // No usage of an element of a reference counted symbol. No need to increment. + Expr::GetTagId { .. } => new_stmt, + _ => unreachable!("Unexpected expression type"), + } + } else { + // If the symbol is not reference counted, we don't need to increment it. + new_stmt + }; + + new_let!(newer_stmt) + } + Expr::Array { + elem_layout: _, + elems, + } => { + // For an array creation, we insert all the used elements. + let new_let = new_let!(stmt); + + inc_owned!( + elems.iter().filter_map(|element| match element { + // Literal elements are not reference counted. + ListLiteralElement::Literal(_) => None, + // Symbol elements might be reference counted. + ListLiteralElement::Symbol(symbol) => Some(*symbol), + }), + new_let + ) + } + Expr::Call(Call { + arguments, + call_type, + }) => { + match call_type.clone().replace_lowlevel_wrapper() { + // A by name call refers to a normal function call. + // Normal functions take all their parameters as owned, so we can mark them all as such. + CallType::ByName { .. } => { + let new_let = new_let!(stmt); + + inc_owned!(arguments.iter().copied(), new_let) + } + // A normal Roc function call, but we don't actually know where its target is. + // As such, we assume that it takes all parameters as owned, as will the function + // itself. + CallType::ByPointer { .. } => { + let new_let = new_let!(stmt); + + inc_owned!(arguments.iter().copied(), new_let) + } + CallType::Foreign { .. } => { + // Foreign functions should be responsible for their own memory management. + // But previously they were assumed to be called with borrowed parameters, so we do the same now. + let new_stmt = dec_borrowed!(arguments.iter().copied(), stmt); + + new_let!(new_stmt) + } + // Doesn't include higher order + CallType::LowLevel { + op: operator, + update_mode: _, + } => match operator { + // List get unsafe is a special case, because it returns a reference to the list element. + // This means that we have to increment the reference count of this element. + LowLevel::ListGetUnsafe => { + let structure = match arguments { + [structure, _index] => *structure, + _ => unreachable!("List get should have two arguments"), + }; + let new_stmt = dec_borrowed!([structure], stmt); + let newer_stmt = if matches!( + environment.get_symbol_rc_type(binding), + VarRcType::ReferenceCounted + ) { + insert_inc_stmt(arena, *binding, 1, new_stmt) + } else { + new_stmt + }; + new_let!(newer_stmt) + } + // Otherwise, perform regular reference counting using the lowlevel borrow signature. + _ => { + let borrow_signature = lowlevel_borrow_signature(arena, operator); + let arguments_with_borrow_signature = arguments + .iter() + .copied() + .zip(borrow_signature.iter().copied()); + let owned_arguments = arguments_with_borrow_signature.clone().filter_map( + |(symbol, ownership)| ownership.is_owned().then_some(symbol), + ); + let borrowed_arguments = + arguments_with_borrow_signature.filter_map(|(symbol, ownership)| { + ownership.is_borrowed().then_some(symbol) + }); + let new_stmt = dec_borrowed!(borrowed_arguments, stmt); + let new_let = new_let!(new_stmt); + inc_owned!(owned_arguments, new_let) + } + }, + CallType::HigherOrder(HigherOrderLowLevel { + op: operator, + + closure_env_layout: _, + + /// update mode of the higher order lowlevel itself + update_mode: _, + + passed_function, + }) => { + // Functions always take their arguments as owned. + // (Except lowlevels, but those are wrapped in functions that take their arguments as owned and perform rc.) + + // This should always be true, not sure where this could be set to false. + debug_assert!(passed_function.owns_captured_environment); + + // define macro that inserts a decref statement for a symbol amount of symbols + macro_rules! decref_lists { + ($stmt:expr, $symbol:expr) => { + arena.alloc(Stmt::Refcounting(ModifyRc::DecRef($symbol), $stmt)) + }; + + ($stmt:expr, $symbol:expr, $($symbols:expr),+) => {{ + decref_lists!(decref_lists!($stmt, $symbol), $($symbols),+) + }}; + } + + match operator { + HigherOrder::ListMap { xs } => { + if let [_xs_symbol, _function_symbol, closure_symbol] = &arguments { + let new_stmt = dec_borrowed!([*closure_symbol], stmt); + let new_stmt = decref_lists!(new_stmt, *xs); + + let new_let = new_let!(new_stmt); + + inc_owned!([*xs].into_iter(), new_let) + } else { + panic!("ListMap should have 3 arguments"); + } + } + HigherOrder::ListMap2 { xs, ys } => { + if let [_xs_symbol, _ys_symbol, _function_symbol, closure_symbol] = + &arguments + { + let new_stmt = dec_borrowed!([*closure_symbol], stmt); + let new_stmt = decref_lists!(new_stmt, *xs, *ys); + + let new_let = new_let!(new_stmt); + + inc_owned!([*xs, *ys].into_iter(), new_let) + } else { + panic!("ListMap2 should have 4 arguments"); + } + } + HigherOrder::ListMap3 { xs, ys, zs } => { + if let [_xs_symbol, _ys_symbol, _zs_symbol, _function_symbol, closure_symbol] = + &arguments + { + let new_stmt = dec_borrowed!([*closure_symbol], stmt); + let new_stmt = decref_lists!(new_stmt, *xs, *ys, *zs); + + let new_let = new_let!(new_stmt); + + inc_owned!([*xs, *ys, *zs].into_iter(), new_let) + } else { + panic!("ListMap3 should have 5 arguments"); + } + } + HigherOrder::ListMap4 { xs, ys, zs, ws } => { + if let [_xs_symbol, _ys_symbol, _zs_symbol, _ws_symbol, _function_symbol, closure_symbol] = + &arguments + { + let new_stmt = dec_borrowed!([*closure_symbol], stmt); + let new_stmt = decref_lists!(new_stmt, *xs, *ys, *zs, *ws); + + let new_let = new_let!(new_stmt); + + inc_owned!([*xs, *ys, *zs, *ws].into_iter(), new_let) + } else { + panic!("ListMap4 should have 6 arguments"); + } + } + HigherOrder::ListSortWith { xs } => { + // TODO if non-unique, elements have been consumed, must still consume the list itself + if let [_xs_symbol, _function_symbol, closure_symbol] = &arguments { + let new_stmt = dec_borrowed!([*closure_symbol], stmt); + let new_let = new_let!(new_stmt); + + inc_owned!([*xs].into_iter(), new_let) + } else { + panic!("ListSortWith should have 3 arguments"); + } + } + } + } + } + } + Expr::Reset { .. } | Expr::ResetRef { .. } => { + unreachable!("Reset(ref) should not exist at this point") + } + Expr::Alloca { initializer, .. } => { + let new_let = new_let!(stmt); + + inc_owned!(initializer.as_ref().copied().into_iter(), new_let) + } + } +} + +/** +Insert increment statements for the given symbols compensating for the ownership. +*/ +fn consume_and_insert_inc_stmts<'a>( + arena: &'a Bump, + environment: &mut RefcountEnvironment, + usage: impl IntoIterator, + continuation: &'a Stmt<'a>, +) -> &'a Stmt<'a> { + usage + .into_iter() + .fold(continuation, |continuation, (symbol, usage_count)| { + consume_and_insert_inc_stmt(arena, environment, symbol, usage_count, continuation) + }) +} + +/** +Insert an increment statement for the given symbol compensating for the ownership. +*/ +fn consume_and_insert_inc_stmt<'a>( + arena: &'a Bump, + environment: &mut RefcountEnvironment, + symbol: Symbol, + usage_count: u64, + continuation: &'a Stmt<'a>, +) -> &'a Stmt<'a> { + debug_assert!(usage_count > 0, "Usage count must be positive"); + let new_count = match environment.consume_rc_symbol(symbol) { + // If the symbol is borrowed, we need to increment the reference count for each usage. + Ownership::Borrowed => usage_count, + // If the symbol is owned, we need to increment the reference count for each usage except one. + Ownership::Owned => usage_count - 1, + }; + + insert_inc_stmt(arena, symbol, new_count, continuation) +} + +/** +Insert a increment statement for the given symbol. +*/ +fn insert_inc_stmt<'a>( + arena: &'a Bump, + symbol: Symbol, + count: u64, + continuation: &'a Stmt<'a>, +) -> &'a Stmt<'a> { + match count { + 0 => continuation, + positive_count => arena.alloc(Stmt::Refcounting( + ModifyRc::Inc(symbol, positive_count), + continuation, + )), + } +} + +/** +Insert decrement statements for the given symbols if they are owned. +*/ +fn consume_and_insert_dec_stmts<'a>( + arena: &'a Bump, + environment: &mut RefcountEnvironment, + symbols: impl IntoIterator, + continuation: &'a Stmt<'a>, +) -> &'a Stmt<'a> { + symbols + .into_iter() + .fold(continuation, |continuation, symbol| { + consume_and_insert_dec_stmt(arena, environment, symbol, continuation) + }) +} + +/** +Insert a decrement statement for the given symbol if it is owned. +*/ +fn consume_and_insert_dec_stmt<'a>( + arena: &'a Bump, + environment: &mut RefcountEnvironment, + symbol: Symbol, + continuation: &'a Stmt<'a>, +) -> &'a Stmt<'a> { + match environment.consume_rc_symbol(symbol) { + // If the symbol is borrowed, don't have to decrement the reference count. + Ownership::Borrowed => continuation, + // If the symbol is owned, we do need to decrement the reference count. + Ownership::Owned => insert_dec_stmt(arena, symbol, continuation), + } +} + +/** +Insert decrement statements for the given symbols. +*/ +fn insert_dec_stmts<'a>( + arena: &'a Bump, + symbols: impl Iterator, + continuation: &'a Stmt<'a>, +) -> &'a Stmt<'a> { + symbols.fold(continuation, |continuation, symbol| { + insert_dec_stmt(arena, symbol, continuation) + }) +} + +/** +Insert a decrement statement for the given symbol. +*/ +fn insert_dec_stmt<'a>( + arena: &'a Bump, + symbol: Symbol, + continuation: &'a Stmt<'a>, +) -> &'a Stmt<'a> { + arena.alloc(Stmt::Refcounting(ModifyRc::Dec(symbol), continuation)) +} + +/** + * Retrieve the borrow signature of a low-level operation. + */ +fn lowlevel_borrow_signature(arena: &Bump, op: LowLevel) -> &[Ownership] { + use LowLevel::*; + + // TODO is true or false more efficient for non-refcounted layouts? + let irrelevant = Ownership::Owned; + let function = irrelevant; + let closure_data = irrelevant; + let owned = Ownership::Owned; + let borrowed = Ownership::Borrowed; + + // Here we define the borrow signature of low-level operations + // + // - arguments with non-refcounted layouts (ints, floats) are `irrelevant` + // - arguments that we may want to update destructively must be Owned + // - other refcounted arguments are Borrowed + match op { + Unreachable => arena.alloc_slice_copy(&[irrelevant]), + DictPseudoSeed => arena.alloc_slice_copy(&[irrelevant]), + ListLen | StrIsEmpty | StrToScalars | StrCountGraphemes | StrGraphemes + | StrCountUtf8Bytes | StrGetCapacity | ListGetCapacity => { + arena.alloc_slice_copy(&[borrowed]) + } + ListWithCapacity | StrWithCapacity => arena.alloc_slice_copy(&[irrelevant]), + ListReplaceUnsafe => arena.alloc_slice_copy(&[owned, irrelevant, irrelevant]), + StrGetUnsafe | ListGetUnsafe => arena.alloc_slice_copy(&[borrowed, irrelevant]), + ListConcat => arena.alloc_slice_copy(&[owned, owned]), + StrConcat => arena.alloc_slice_copy(&[owned, borrowed]), + StrSubstringUnsafe => arena.alloc_slice_copy(&[borrowed, irrelevant, irrelevant]), + StrReserve => arena.alloc_slice_copy(&[owned, irrelevant]), + StrAppendScalar => arena.alloc_slice_copy(&[owned, irrelevant]), + StrGetScalarUnsafe => arena.alloc_slice_copy(&[borrowed, irrelevant]), + StrTrim => arena.alloc_slice_copy(&[owned]), + StrTrimStart => arena.alloc_slice_copy(&[owned]), + StrTrimEnd => arena.alloc_slice_copy(&[owned]), + StrSplit => arena.alloc_slice_copy(&[borrowed, borrowed]), + StrToNum => arena.alloc_slice_copy(&[borrowed]), + ListPrepend => arena.alloc_slice_copy(&[owned, owned]), + StrJoinWith => arena.alloc_slice_copy(&[borrowed, borrowed]), + ListMap => arena.alloc_slice_copy(&[owned, function, closure_data]), + ListMap2 => arena.alloc_slice_copy(&[owned, owned, function, closure_data]), + ListMap3 => arena.alloc_slice_copy(&[owned, owned, owned, function, closure_data]), + ListMap4 => arena.alloc_slice_copy(&[owned, owned, owned, owned, function, closure_data]), + ListSortWith => arena.alloc_slice_copy(&[owned, function, closure_data]), + + ListAppendUnsafe => arena.alloc_slice_copy(&[owned, owned]), + ListReserve => arena.alloc_slice_copy(&[owned, irrelevant]), + ListSublist => arena.alloc_slice_copy(&[owned, irrelevant, irrelevant]), + ListDropAt => arena.alloc_slice_copy(&[owned, irrelevant]), + ListSwap => arena.alloc_slice_copy(&[owned, irrelevant, irrelevant]), + ListReleaseExcessCapacity => arena.alloc_slice_copy(&[owned]), + StrReleaseExcessCapacity => arena.alloc_slice_copy(&[owned]), + + Eq | NotEq => arena.alloc_slice_copy(&[borrowed, borrowed]), + + And | Or | NumAdd | NumAddWrap | NumAddChecked | NumAddSaturated | NumSub | NumSubWrap + | NumSubChecked | NumSubSaturated | NumMul | NumMulWrap | NumMulSaturated + | NumMulChecked | NumGt | NumGte | NumLt | NumLte | NumCompare | NumDivFrac + | NumDivTruncUnchecked | NumDivCeilUnchecked | NumRemUnchecked | NumIsMultipleOf + | NumPow | NumPowInt | NumBitwiseAnd | NumBitwiseXor | NumBitwiseOr | NumShiftLeftBy + | NumShiftRightBy | NumShiftRightZfBy => arena.alloc_slice_copy(&[irrelevant, irrelevant]), + + NumToStr + | NumAbs + | NumNeg + | NumSin + | NumCos + | NumTan + | NumSqrtUnchecked + | NumLogUnchecked + | NumRound + | NumCeiling + | NumFloor + | NumToFrac + | Not + | NumIsNan + | NumIsInfinite + | NumIsFinite + | NumAtan + | NumAcos + | NumAsin + | NumIntCast + | NumToIntChecked + | NumToFloatCast + | NumToFloatChecked + | NumCountLeadingZeroBits + | NumCountTrailingZeroBits + | NumCountOneBits + | I128OfDec => arena.alloc_slice_copy(&[irrelevant]), + NumBytesToU16 => arena.alloc_slice_copy(&[borrowed, irrelevant]), + NumBytesToU32 => arena.alloc_slice_copy(&[borrowed, irrelevant]), + NumBytesToU64 => arena.alloc_slice_copy(&[borrowed, irrelevant]), + NumBytesToU128 => arena.alloc_slice_copy(&[borrowed, irrelevant]), + StrStartsWith | StrEndsWith => arena.alloc_slice_copy(&[borrowed, borrowed]), + StrStartsWithScalar => arena.alloc_slice_copy(&[borrowed, irrelevant]), + StrFromUtf8Range => arena.alloc_slice_copy(&[owned, irrelevant, irrelevant]), + StrToUtf8 => arena.alloc_slice_copy(&[owned]), + StrRepeat => arena.alloc_slice_copy(&[borrowed, irrelevant]), + StrFromInt | StrFromFloat => arena.alloc_slice_copy(&[irrelevant]), + Hash => arena.alloc_slice_copy(&[borrowed, irrelevant]), + + ListIsUnique => arena.alloc_slice_copy(&[borrowed]), + + BoxExpr | UnboxExpr => { + unreachable!("These lowlevel operations are turned into mono Expr's") + } + + PtrStore => arena.alloc_slice_copy(&[owned, owned]), + PtrLoad => arena.alloc_slice_copy(&[owned]), + PtrCast => arena.alloc_slice_copy(&[owned]), + + SetJmp | LongJmp | SetLongJmpBuffer => { + unreachable!("only inserted in dev backend codegen") + } + + PtrClearTagId | RefCountIncRcPtr | RefCountDecRcPtr | RefCountIncDataPtr + | RefCountDecDataPtr | RefCountIsUnique => { + unreachable!("Only inserted *after* borrow checking: {:?}", op); + } + } +} diff --git a/crates/compiler/mono/src/ir.rs b/crates/compiler/mono/src/ir.rs new file mode 100644 index 0000000000..07fd3880fa --- /dev/null +++ b/crates/compiler/mono/src/ir.rs @@ -0,0 +1,10502 @@ +#![allow(clippy::manual_map)] + +use crate::ir::erased::{build_erased_function, ResolvedErasedLambda}; +use crate::ir::literal::{make_num_literal, IntOrFloatValue}; +use crate::layout::{ + self, Builtin, ClosureCallOptions, ClosureDataKind, ClosureRepresentation, EnumDispatch, + InLayout, LambdaName, LambdaSet, Layout, LayoutCache, LayoutInterner, LayoutProblem, + LayoutRepr, Niche, RawFunctionLayout, TLLayoutInterner, TagIdIntType, UnionLayout, + WrappedVariant, +}; +use bumpalo::collections::{CollectIn, Vec}; +use bumpalo::Bump; +use roc_can::abilities::SpecializationId; +use roc_can::expr::{AnnotatedMark, ClosureData, ExpectLookup}; +use roc_can::module::ExposedByModule; +use roc_collections::all::{default_hasher, BumpMap, BumpMapDefault, MutMap}; +use roc_collections::VecMap; +use roc_debug_flags::dbg_do; +#[cfg(debug_assertions)] +use roc_debug_flags::{ + ROC_PRINT_IR_AFTER_DROP_SPECIALIZATION, ROC_PRINT_IR_AFTER_REFCOUNT, + ROC_PRINT_IR_AFTER_RESET_REUSE, ROC_PRINT_IR_AFTER_SPECIALIZATION, ROC_PRINT_RUNTIME_ERROR_GEN, +}; +use roc_derive::SharedDerivedModule; +use roc_error_macros::{internal_error, todo_abilities, todo_lambda_erasure}; +use roc_late_solve::storage::{ExternalModuleStorage, ExternalModuleStorageSnapshot}; +use roc_late_solve::{resolve_ability_specialization, AbilitiesView, Resolved, UnificationFailed}; +use roc_module::ident::{ForeignSymbol, Lowercase, TagName}; +use roc_module::low_level::{LowLevel, LowLevelWrapperType}; +use roc_module::symbol::{IdentIds, ModuleId, Symbol}; +use roc_problem::can::{RuntimeError, ShadowKind}; +use roc_region::all::{Loc, Region}; +use roc_std::RocDec; +use roc_target::TargetInfo; +use roc_types::subs::{ + instantiate_rigids, storage_copy_var_to, Content, ExhaustiveMark, FlatType, RedundantMark, + StorageSubs, Subs, Variable, VariableSubsSlice, +}; +use std::collections::HashMap; +use ven_pretty::{text, BoxAllocator, DocAllocator, DocBuilder}; + +use pattern::{from_can_pattern, store_pattern, Pattern}; + +pub use literal::{ListLiteralElement, Literal}; + +mod boxed; +mod decision_tree; +mod erased; +mod literal; +mod pattern; + +#[inline(always)] +pub fn pretty_print_ir_symbols() -> bool { + dbg_do!(ROC_PRINT_IR_AFTER_SPECIALIZATION, { + return true; + }); + dbg_do!(ROC_PRINT_IR_AFTER_RESET_REUSE, { + return true; + }); + dbg_do!(ROC_PRINT_IR_AFTER_REFCOUNT, { + return true; + }); + dbg_do!(ROC_PRINT_IR_AFTER_DROP_SPECIALIZATION, { + return true; + }); + false +} + +// if your changes cause this number to go down, great! +// please change it to the lower number. +// if it went up, maybe check that the change is really required +roc_error_macros::assert_sizeof_wasm!(Literal, 24); +roc_error_macros::assert_sizeof_wasm!(Expr, 48); +roc_error_macros::assert_sizeof_wasm!(Stmt, 64); +roc_error_macros::assert_sizeof_wasm!(ProcLayout, 20); +roc_error_macros::assert_sizeof_wasm!(Call, 44); +roc_error_macros::assert_sizeof_wasm!(CallType, 36); + +roc_error_macros::assert_sizeof_non_wasm!(Literal, 3 * 8); +roc_error_macros::assert_sizeof_non_wasm!(Expr, 9 * 8); +roc_error_macros::assert_sizeof_non_wasm!(Stmt, 12 * 8); +roc_error_macros::assert_sizeof_non_wasm!(ProcLayout, 5 * 8); +roc_error_macros::assert_sizeof_non_wasm!(Call, 9 * 8); +roc_error_macros::assert_sizeof_non_wasm!(CallType, 7 * 8); + +fn runtime_error<'a>(env: &mut Env<'a, '_>, msg: &'a str) -> Stmt<'a> { + let sym = env.unique_symbol(); + Stmt::Let( + sym, + Expr::Literal(Literal::Str(msg)), + Layout::STR, + env.arena.alloc(Stmt::Crash(sym, CrashTag::Roc)), + ) +} + +macro_rules! return_on_layout_error { + ($env:expr, $layout_result:expr, $context_msg:expr) => { + match $layout_result { + Ok(cached) => cached, + Err(error) => return_on_layout_error_help!($env, error, $context_msg), + } + }; +} + +macro_rules! return_on_layout_error_help { + ($env:expr, $error:expr, $context_msg:expr) => {{ + match $error { + LayoutProblem::UnresolvedTypeVar(_) => { + return runtime_error( + $env, + $env.arena + .alloc(format!("UnresolvedTypeVar: {}", $context_msg,)), + ) + } + LayoutProblem::Erroneous => { + return runtime_error( + $env, + $env.arena.alloc(format!("Erroneous: {}", $context_msg,)), + ) + } + } + }}; +} + +#[derive(Debug, Clone, Copy)] +pub enum OptLevel { + Development, + Normal, + Size, + Optimize, +} + +#[derive(Debug, Clone, Copy)] +pub struct SingleEntryPoint<'a> { + pub symbol: Symbol, + pub layout: ProcLayout<'a>, +} + +#[derive(Debug, Clone, Copy)] +pub enum EntryPoint<'a> { + Single(SingleEntryPoint<'a>), + Expects { symbols: &'a [Symbol] }, +} + +#[derive(Clone, Copy, Debug)] +pub struct PartialProcId(usize); + +#[derive(Clone, Debug)] +pub struct PartialProcs<'a> { + /// maps a function name (symbol) to an index + symbols: Vec<'a, Symbol>, + + partial_procs: Vec<'a, PartialProc<'a>>, +} + +impl<'a> PartialProcs<'a> { + fn new_in(arena: &'a Bump) -> Self { + Self { + symbols: Vec::new_in(arena), + partial_procs: Vec::new_in(arena), + } + } + fn contains_key(&self, symbol: Symbol) -> bool { + self.symbol_to_id(symbol).is_some() + } + + fn symbol_to_id(&self, symbol: Symbol) -> Option { + self.symbols + .iter() + .position(|s| *s == symbol) + .map(PartialProcId) + } + + fn get_symbol(&self, symbol: Symbol) -> Option<&PartialProc<'a>> { + let id = self.symbol_to_id(symbol)?; + + Some(self.get_id(id)) + } + + fn get_id(&self, id: PartialProcId) -> &PartialProc<'a> { + &self.partial_procs[id.0] + } + + pub fn insert(&mut self, symbol: Symbol, partial_proc: PartialProc<'a>) -> PartialProcId { + debug_assert!( + !self.contains_key(symbol), + "The {symbol:?} is inserted as a partial proc twice: that's a bug!", + ); + + let id = PartialProcId(self.symbols.len()); + + self.symbols.push(symbol); + self.partial_procs.push(partial_proc); + + id + } + + pub fn drain(self) -> impl Iterator)> { + debug_assert_eq!(self.symbols.len(), self.partial_procs.len()); + + self.symbols.into_iter().zip(self.partial_procs) + } +} + +#[derive(Clone, Debug)] +pub struct PartialProc<'a> { + pub annotation: Variable, + pub pattern_symbols: &'a [Symbol], + pub captured_symbols: CapturedSymbols<'a>, + pub body: roc_can::expr::Expr, + pub body_var: Variable, + pub is_self_recursive: bool, +} + +impl<'a> PartialProc<'a> { + #[allow(clippy::too_many_arguments)] + pub fn from_named_function( + env: &mut Env<'a, '_>, + annotation: Variable, + loc_args: std::vec::Vec<(Variable, AnnotatedMark, Loc)>, + loc_body: Loc, + captured_symbols: CapturedSymbols<'a>, + is_self_recursive: bool, + ret_var: Variable, + ) -> PartialProc<'a> { + let number_of_arguments = loc_args.len(); + + match patterns_to_when(env, loc_args, ret_var, loc_body) { + Ok((_, pattern_symbols, body)) => { + // a named closure. Since these aren't specialized by the surrounding + // context, we can't add pending specializations for them yet. + // (If we did, all named polymorphic functions would immediately error + // on trying to convert a flex var to a Layout.) + let pattern_symbols = pattern_symbols.into_bump_slice(); + PartialProc { + annotation, + pattern_symbols, + captured_symbols, + body: body.value, + body_var: ret_var, + is_self_recursive, + } + } + + Err(error) => { + let mut pattern_symbols = Vec::with_capacity_in(number_of_arguments, env.arena); + + for _ in 0..number_of_arguments { + pattern_symbols.push(env.unique_symbol()); + } + + PartialProc { + annotation, + pattern_symbols: pattern_symbols.into_bump_slice(), + captured_symbols: CapturedSymbols::None, + body: roc_can::expr::Expr::RuntimeError(error.value), + body_var: ret_var, + is_self_recursive: false, + } + } + } + } +} + +#[derive(Clone, Copy, Debug)] +struct AbilityMember(Symbol); + +/// A table of aliases of ability member symbols. +#[derive(Clone, Debug)] +struct AbilityAliases(BumpMap); + +impl AbilityAliases { + fn new_in(arena: &Bump) -> Self { + Self(BumpMap::new_in(arena)) + } + + fn insert(&mut self, symbol: Symbol, member: AbilityMember) { + self.0.insert(symbol, member); + } + + fn get(&self, symbol: Symbol) -> Option<&AbilityMember> { + self.0.get(&symbol) + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)] +pub enum CapturedSymbols<'a> { + #[default] + None, + Captured(&'a [(Symbol, Variable)]), +} + +impl<'a> CapturedSymbols<'a> { + fn captures(&self) -> bool { + match self { + CapturedSymbols::None => false, + CapturedSymbols::Captured(_) => true, + } + } +} + +#[derive(Clone, Debug, PartialEq)] +pub struct Proc<'a> { + pub name: LambdaName<'a>, + pub args: &'a [(InLayout<'a>, Symbol)], + pub body: Stmt<'a>, + pub closure_data_layout: Option>, + pub ret_layout: InLayout<'a>, + pub is_self_recursive: SelfRecursive, + pub is_erased: bool, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct HostExposedLambdaSet<'a> { + pub id: LambdaSetId, + /// Symbol of the exposed function + pub symbol: Symbol, + pub proc_layout: ProcLayout<'a>, + pub raw_function_layout: RawFunctionLayout<'a>, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum SelfRecursive { + NotSelfRecursive, + SelfRecursive(JoinPointId), +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Parens { + NotNeeded, + InTypeParam, + InFunction, +} + +impl<'a> Proc<'a> { + pub fn to_doc<'b, D, A, I>( + &'b self, + alloc: &'b D, + interner: &'b I, + pretty: bool, + _parens: Parens, + ) -> DocBuilder<'b, D, A> + where + D: DocAllocator<'b, A>, + D::Doc: Clone, + A: Clone, + I: LayoutInterner<'a>, + { + let args_doc = self.args.iter().map(|(layout, symbol)| { + let arg_doc = symbol_to_doc(alloc, *symbol, pretty); + if pretty_print_ir_symbols() { + arg_doc + .append(alloc.reflow(": ")) + .append(interner.to_doc_top(*layout, alloc)) + } else { + arg_doc + } + }); + + if pretty_print_ir_symbols() { + alloc + .text("procedure : ") + .append(symbol_to_doc(alloc, self.name.name(), pretty)) + .append(" ") + .append(interner.to_doc_top(self.ret_layout, alloc)) + .append(alloc.hardline()) + .append(alloc.text("procedure = ")) + .append(symbol_to_doc(alloc, self.name.name(), pretty)) + .append(" (") + .append(alloc.intersperse(args_doc, ", ")) + .append("):") + .append(alloc.hardline()) + .append(self.body.to_doc(alloc, interner, pretty).indent(4)) + } else { + alloc + .text("procedure ") + .append(symbol_to_doc(alloc, self.name.name(), pretty)) + .append(" (") + .append(alloc.intersperse(args_doc, ", ")) + .append("):") + .append(alloc.hardline()) + .append(self.body.to_doc(alloc, interner, pretty).indent(4)) + } + } + + pub fn to_pretty(&self, interner: &I, width: usize, pretty: bool) -> String + where + I: LayoutInterner<'a>, + { + let allocator = BoxAllocator; + let mut w = std::vec::Vec::new(); + self.to_doc::<_, (), _>(&allocator, interner, pretty, Parens::NotNeeded) + .1 + .render(width, &mut w) + .unwrap(); + w.push(b'\n'); + String::from_utf8(w).unwrap() + } +} + +/// A host-exposed function must be specialized; it's a seed for subsequent specializations +#[derive(Clone, Debug)] +pub struct HostSpecializations<'a> { + /// Not a bumpalo vec because bumpalo is not thread safe + /// Separate array so we can search for membership quickly + /// If it's a value and not a lambda, the value is recorded as LambdaName::no_niche. + symbol_or_lambdas: std::vec::Vec>, + /// For each symbol, a variable that stores the unsolved (!) annotation + annotations: std::vec::Vec>, + /// For each symbol, what types to specialize it for, points into the storage_subs + types_to_specialize: std::vec::Vec, + storage_subs: StorageSubs, +} + +impl Default for HostSpecializations<'_> { + fn default() -> Self { + Self::new() + } +} + +impl<'a> HostSpecializations<'a> { + pub fn new() -> Self { + Self { + symbol_or_lambdas: std::vec::Vec::new(), + annotations: std::vec::Vec::new(), + storage_subs: StorageSubs::new(Subs::default()), + types_to_specialize: std::vec::Vec::new(), + } + } + + pub fn is_empty(&self) -> bool { + self.symbol_or_lambdas.is_empty() + } + + pub fn insert_host_exposed( + &mut self, + env_subs: &mut Subs, + symbol_or_lambda: LambdaName<'a>, + annotation: Option, + variable: Variable, + ) { + let variable = self.storage_subs.extend_with_variable(env_subs, variable); + + match self + .symbol_or_lambdas + .iter() + .position(|s| *s == symbol_or_lambda) + { + None => { + self.symbol_or_lambdas.push(symbol_or_lambda); + self.types_to_specialize.push(variable); + self.annotations.push(annotation); + } + Some(_) => { + // we assume that only one specialization of a function is directly exposed to the + // host. Other host-exposed symbols may (transitively) specialize this symbol, + // but then the existing specialization mechanism will find those specializations + panic!("A host-exposed symbol can only be exposed once"); + } + } + } + + fn decompose( + self, + ) -> ( + StorageSubs, + impl Iterator, Variable, Option)>, + ) { + let it1 = self.symbol_or_lambdas.into_iter(); + + let it2 = self.types_to_specialize.into_iter(); + let it3 = self.annotations.into_iter(); + + ( + self.storage_subs, + it1.zip(it2).zip(it3).map(|((a, b), c)| (a, b, c)), + ) + } +} + +/// Specializations of this module's symbols that other modules need. +/// One struct represents one pair of modules, e.g. what module A wants of module B. +#[derive(Clone, Debug)] +pub struct ExternalSpecializations<'a> { + /// Not a bumpalo vec because bumpalo is not thread safe + /// Separate array so we can search for membership quickly + /// If it's a value and not a lambda, the value is recorded as LambdaName::no_niche. + pub symbol_or_lambda: std::vec::Vec>, + storage: ExternalModuleStorage, + /// For each symbol, what types to specialize it for, points into the storage_subs + types_to_specialize: std::vec::Vec>, +} + +impl Default for ExternalSpecializations<'_> { + fn default() -> Self { + Self::new() + } +} + +impl<'a> ExternalSpecializations<'a> { + pub fn new() -> Self { + Self { + symbol_or_lambda: std::vec::Vec::new(), + storage: ExternalModuleStorage::new(Subs::default()), + types_to_specialize: std::vec::Vec::new(), + } + } + + fn insert_external( + &mut self, + symbol_or_lambda: LambdaName<'a>, + env_subs: &mut Subs, + variable: Variable, + ) { + let stored_variable = self.storage.extend_with_variable(env_subs, variable); + roc_tracing::debug!(original = ?variable, stored = ?stored_variable, "stored needed external"); + + match self + .symbol_or_lambda + .iter() + .position(|s| *s == symbol_or_lambda) + { + None => { + self.symbol_or_lambda.push(symbol_or_lambda); + self.types_to_specialize.push(vec![stored_variable]); + } + Some(index) => { + let types_to_specialize = &mut self.types_to_specialize[index]; + types_to_specialize.push(stored_variable); + } + } + } + + fn decompose( + self, + ) -> ( + StorageSubs, + impl Iterator, std::vec::Vec)>, + ) { + ( + self.storage.into_storage_subs(), + self.symbol_or_lambda + .into_iter() + .zip(self.types_to_specialize), + ) + } + + fn snapshot_cache(&mut self) -> ExternalModuleStorageSnapshot { + self.storage.snapshot_cache() + } + + fn rollback_cache(&mut self, snapshot: ExternalModuleStorageSnapshot) { + self.storage.rollback_cache(snapshot) + } + + fn invalidate_cache(&mut self, changed_variables: &[Variable]) { + self.storage.invalidate_cache(changed_variables) + } + + fn invalidate_whole_cache(&mut self) { + self.storage.invalidate_whole_cache() + } +} + +#[derive(Clone, Debug)] +pub struct Suspended<'a> { + pub store: StorageSubs, + /// LambdaName::no_niche if it's a value + pub symbol_or_lambdas: Vec<'a, LambdaName<'a>>, + pub layouts: Vec<'a, ProcLayout<'a>>, + pub variables: Vec<'a, Variable>, +} + +impl<'a> Suspended<'a> { + fn new_in(arena: &'a Bump) -> Self { + Self { + store: StorageSubs::new(Subs::new_from_varstore(Default::default())), + symbol_or_lambdas: Vec::new_in(arena), + layouts: Vec::new_in(arena), + variables: Vec::new_in(arena), + } + } + + fn is_empty(&self) -> bool { + self.symbol_or_lambdas.is_empty() + } + + fn specialization( + &mut self, + subs: &mut Subs, + symbol_or_lambda: LambdaName<'a>, + proc_layout: ProcLayout<'a>, + variable: Variable, + ) { + // de-duplicate + for (i, s) in self.symbol_or_lambdas.iter().enumerate() { + if *s == symbol_or_lambda { + let existing = &self.layouts[i]; + if &proc_layout == existing { + // symbol + layout combo exists + return; + } + } + } + + self.symbol_or_lambdas.push(symbol_or_lambda); + self.layouts.push(proc_layout); + + let variable = self.store.import_variable_from(subs, variable).variable; + + self.variables.push(variable); + } +} + +#[derive(Clone, Debug)] +enum PendingSpecializations<'a> { + /// We are finding specializations we need. This is a separate step so + /// that we can give specializations we need to modules higher up in the dependency chain, so + /// that they can start making specializations too + Finding(Suspended<'a>), + /// We are making specializations. + /// If any new one comes up while specializing a body, we can do one of two things: + /// - if the new specialization is for a symbol that is not in the current stack of symbols + /// being specialized, make it immediately + /// - if it is, we must suspend the specialization, and we'll do it once the stack is clear + /// again. + Making(Suspended<'a>), +} + +impl<'a> PendingSpecializations<'a> { + fn is_empty(&self) -> bool { + match self { + PendingSpecializations::Finding(suspended) + | PendingSpecializations::Making(suspended) => suspended.is_empty(), + } + } +} + +#[derive(Clone, Debug, Default)] +struct Specialized<'a> { + symbols: std::vec::Vec, + proc_layouts: std::vec::Vec>, + procedures: std::vec::Vec>, +} + +impl<'a> Specialized<'a> { + fn len(&self) -> usize { + self.symbols.len() + } + + #[allow(dead_code)] + fn is_empty(&self) -> bool { + self.symbols.is_empty() + } + + fn into_iter_assert_done(self) -> impl Iterator, Proc<'a>)> { + self.symbols + .into_iter() + .zip(self.proc_layouts) + .zip(self.procedures) + .filter_map(|((s, l), in_progress)| { + if let Symbol::REMOVED_SPECIALIZATION = s { + None + } else { + match in_progress { + InProgressProc::InProgress => { + panic!("Function {s:?} ({l:?}) is not done specializing") + } + InProgressProc::Done(proc) => Some((s, l, proc)), + } + } + }) + } + + fn is_specialized(&self, symbol: Symbol, layout: &ProcLayout<'a>) -> bool { + for (i, s) in self.symbols.iter().enumerate() { + if *s == symbol && &self.proc_layouts[i] == layout { + return true; + } + } + + false + } + + fn mark_in_progress(&mut self, symbol: Symbol, layout: ProcLayout<'a>) { + for (i, s) in self.symbols.iter().enumerate() { + if *s == symbol && self.proc_layouts[i] == layout { + match &self.procedures[i] { + InProgressProc::InProgress => { + return; + } + InProgressProc::Done(_) => { + panic!("marking in progress, but this proc is already done!") + } + } + } + } + + // the key/layout combo was not found; insert it + self.symbols.push(symbol); + self.proc_layouts.push(layout); + self.procedures.push(InProgressProc::InProgress); + } + + fn remove_specialized(&mut self, symbol: Symbol, layout: &ProcLayout<'a>) -> bool { + let mut index = None; + + for (i, s) in self.symbols.iter().enumerate() { + if *s == symbol && &self.proc_layouts[i] == layout { + index = Some(i); + } + } + + if let Some(index) = index { + self.symbols[index] = Symbol::REMOVED_SPECIALIZATION; + + true + } else { + false + } + } + + fn insert_specialized( + &mut self, + symbol: Symbol, + layout: ProcLayout<'a>, + proc: Proc<'a>, + ) -> SpecializedIndex { + for (i, s) in self.symbols.iter().enumerate() { + if *s == symbol && self.proc_layouts[i] == layout { + match &self.procedures[i] { + InProgressProc::InProgress => { + self.procedures[i] = InProgressProc::Done(proc); + return SpecializedIndex(i); + } + InProgressProc::Done(_) => { + // overwrite existing! this is important in practice + // TODO investigate why we generate the wrong proc in some cases and then + // correct later + self.procedures[i] = InProgressProc::Done(proc); + return SpecializedIndex(i); + } + } + } + } + + // the key/layout combo was not found; insert it + let i = self.symbols.len(); + + self.symbols.push(symbol); + self.proc_layouts.push(layout); + self.procedures.push(InProgressProc::Done(proc)); + + SpecializedIndex(i) + } +} + +struct SpecializedIndex(usize); + +/// Uniquely determines the specialization of a polymorphic (non-proc) value symbol. +/// Two specializations are equivalent if their [`SpecializationMark`]s are equal. +#[derive(PartialEq, Eq, Debug, Clone, Copy)] +struct SpecializationMark<'a> { + /// The layout of the symbol itself. + layout: InLayout<'a>, + + /// If this symbol is a closure def, we must also keep track of what function it specializes, + /// because the [`layout`] field will only keep track of its closure and lambda set - which can + /// be the same for two different function specializations. For example, + /// + /// id = if True then \x -> x else \y -> y + /// { a: id "", b: id 1u8 } + /// + /// The lambda set and captures of `id` is the same in both usages inside the record, but the + /// reified specializations of `\x -> x` and `\y -> y` must be for Str and U8. + /// + /// Note that this field is not relevant for anything that is not a function. + function_mark: Option>, +} + +/// The deepest closure in the current stack of procedures under specialization a symbol specialization +/// was used in. +/// +/// This is necessary to understand what symbol specializations are used in what capture sets. For +/// example, consider +/// +/// main = +/// x = 1 +/// +/// y = \{} -> 1u8 + x +/// z = \{} -> 1u16 + x +/// +/// Here, we have a two specializations of `x` to U8 and U16 with deepest uses of +/// (2, y) and (2, z), respectively. This tells us that both of those specializations must be +/// preserved by `main` (which is at depth 1), but that `y` and `z` respectively only need to +/// capture one particular specialization of `x` each. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +struct UseDepth { + depth: usize, + symbol: Symbol, +} + +impl UseDepth { + fn is_nested_use_in(&self, outer: &Self) -> bool { + if self.symbol == outer.symbol { + debug_assert!(self.depth == outer.depth); + return true; + } + self.depth > outer.depth + } +} + +type NumberSpecializations<'a> = VecMap, (Symbol, UseDepth)>; + +/// When walking a function body, we may encounter specialized usages of polymorphic number symbols. +/// For example +/// +/// n = 1 +/// use1 : U8 +/// use1 = 1 +/// use2 : Nat +/// use2 = 2 +/// +/// We keep track of the specializations of `myTag` and create fresh symbols when there is more +/// than one, so that a unique def can be created for each. +#[derive(Default, Debug, Clone)] +struct SymbolSpecializations<'a>( + // THEORY: + // 1. the number of symbols in a def is very small + // 2. the number of specializations of a symbol in a def is even smaller (almost always only one) + // So, a linear VecMap is preferrable. Use a two-layered one to make (1) extraction of defs easy + // and (2) reads of a certain symbol be determined by its first occurrence, not its last. + VecMap>, +); + +impl<'a> SymbolSpecializations<'a> { + /// Mark a let-generalized symbol eligible for specialization. + /// Only those bound to number literals can be compiled polymorphically. + fn mark_eligible(&mut self, symbol: Symbol) { + let _old = self.0.insert(symbol, VecMap::with_capacity(1)); + debug_assert!(_old.is_none(), "overwriting specializations for {symbol:?}"); + } + + /// Removes all specializations for a symbol, returning the type and symbol of each specialization. + fn remove(&mut self, symbol: Symbol) -> Option> { + self.0 + .remove(&symbol) + .map(|(_, specializations)| specializations) + } + + fn is_empty(&self) -> bool { + self.0.is_empty() + } + + fn maybe_get_specialized(&self, symbol: Symbol, layout: InLayout) -> Symbol { + self.0 + .get(&symbol) + .and_then(|m| m.get(&layout)) + .map(|x| x.0) + .unwrap_or(symbol) + } +} + +#[derive(Clone, Debug, Default)] +pub struct ProcsBase<'a> { + pub partial_procs: BumpMap>, + pub module_thunks: &'a [Symbol], + /// A host-exposed function must be specialized; it's a seed for subsequent specializations + pub host_specializations: HostSpecializations<'a>, + pub runtime_errors: BumpMap, + pub imported_module_thunks: &'a [Symbol], +} + +impl<'a> ProcsBase<'a> { + pub fn get_host_exposed_symbols(&self) -> impl Iterator + '_ { + self.host_specializations + .symbol_or_lambdas + .iter() + .copied() + .map(|n| n.name()) + } +} + +/// The current set of functions under specialization. They form a stack where the latest +/// specialization to be seen is at the head of the stack. +#[derive(Clone, Debug)] +struct SpecializationStack<'a>(Vec<'a, Symbol>); + +impl<'a> SpecializationStack<'a> { + fn current_use_depth(&self) -> UseDepth { + UseDepth { + depth: self.0.len(), + symbol: *self.0.last().unwrap(), + } + } +} + +pub type HostExposedLambdaSets<'a> = + std::vec::Vec<(LambdaName<'a>, Symbol, HostExposedLambdaSet<'a>)>; + +#[derive(Clone, Debug)] +pub struct Procs<'a> { + pub partial_procs: PartialProcs<'a>, + ability_member_aliases: AbilityAliases, + pending_specializations: PendingSpecializations<'a>, + specialized: Specialized<'a>, + host_exposed_lambda_sets: HostExposedLambdaSets<'a>, + pub runtime_errors: BumpMap, + pub externals_we_need: BumpMap>, + symbol_specializations: SymbolSpecializations<'a>, + specialization_stack: SpecializationStack<'a>, + + pub imported_module_thunks: &'a [Symbol], + pub module_thunks: &'a [Symbol], + pub host_exposed_symbols: &'a [Symbol], +} + +impl<'a> Procs<'a> { + pub fn new_in(arena: &'a Bump) -> Self { + Self { + partial_procs: PartialProcs::new_in(arena), + ability_member_aliases: AbilityAliases::new_in(arena), + pending_specializations: PendingSpecializations::Finding(Suspended::new_in(arena)), + specialized: Specialized::default(), + runtime_errors: BumpMap::new_in(arena), + externals_we_need: BumpMap::new_in(arena), + host_exposed_lambda_sets: std::vec::Vec::new(), + symbol_specializations: Default::default(), + specialization_stack: SpecializationStack(Vec::with_capacity_in(16, arena)), + + imported_module_thunks: &[], + module_thunks: &[], + host_exposed_symbols: &[], + } + } + + fn push_active_specialization(&mut self, specialization: Symbol) { + self.specialization_stack.0.push(specialization); + } + + fn pop_active_specialization(&mut self, specialization: Symbol) { + let popped = self + .specialization_stack + .0 + .pop() + .expect("specialization stack is empty"); + debug_assert_eq!( + popped, specialization, + "incorrect popped specialization: passed {specialization:?}, but was {popped:?}" + ); + } + + /// If we need to specialize a function that is already in the stack, we must wait to do so + /// until that function is popped off. That's because the type environment will be configured + /// for the existing specialization on the stack. + /// + /// For example, in + /// + /// foo = \val, b -> if b then "done" else bar val + /// bar = \_ -> foo {} True + /// foo "" False + /// + /// During the specialization of `foo : Str False -> Str`, we specialize `bar : Str -> Str`, + /// which in turn needs a specialization of `foo : {} False -> Str`. However, we can't + /// specialize both `foo : Str False -> Str` and `foo : {} False -> Str` at the same time, so + /// the latter specialization must be deferred. + fn symbol_needs_suspended_specialization(&self, specialization: Symbol) -> bool { + self.specialization_stack.0.contains(&specialization) + } +} + +#[derive(Clone, Debug, PartialEq)] +pub enum InProgressProc<'a> { + InProgress, + Done(Proc<'a>), +} + +impl<'a> Procs<'a> { + fn is_imported_module_thunk(&self, symbol: Symbol) -> bool { + self.imported_module_thunks.iter().any(|x| *x == symbol) + } + + fn is_module_thunk(&self, symbol: Symbol) -> bool { + self.module_thunks.iter().any(|x| *x == symbol) + } + + fn get_partial_proc<'b>(&'b self, symbol: Symbol) -> Option<&'b PartialProc<'a>> { + self.partial_procs.get_symbol(symbol) + } + + pub fn get_specialized_procs_without_rc( + self, + ) -> ( + MutMap<(Symbol, ProcLayout<'a>), Proc<'a>>, + HostExposedLambdaSets<'a>, + ProcsBase<'a>, + ) { + let mut specialized_procs = + MutMap::with_capacity_and_hasher(self.specialized.len(), default_hasher()); + + for (symbol, layout, proc) in self.specialized.into_iter_assert_done() { + let key = (symbol, layout); + specialized_procs.insert(key, proc); + } + + let restored_procs_base = ProcsBase { + partial_procs: self.partial_procs.drain().collect(), + module_thunks: self.module_thunks, + // This must now be empty + host_specializations: HostSpecializations::default(), + runtime_errors: self.runtime_errors, + imported_module_thunks: self.imported_module_thunks, + }; + + ( + specialized_procs, + self.host_exposed_lambda_sets, + restored_procs_base, + ) + } + + // TODO trim these down + #[allow(clippy::too_many_arguments)] + fn insert_anonymous( + &mut self, + env: &mut Env<'a, '_>, + name: LambdaName<'a>, + annotation: Variable, + loc_args: std::vec::Vec<(Variable, AnnotatedMark, Loc)>, + loc_body: Loc, + captured_symbols: CapturedSymbols<'a>, + ret_var: Variable, + layout_cache: &mut LayoutCache<'a>, + ) -> Result, RuntimeError> { + let raw_layout = layout_cache + .raw_from_var(env.arena, annotation, env.subs) + .unwrap_or_else(|err| panic!("TODO turn fn_var into a RuntimeError {err:?}")); + + let top_level = ProcLayout::from_raw_named(env.arena, name, raw_layout); + + // anonymous functions cannot reference themselves, therefore cannot be tail-recursive + // EXCEPT when the closure conversion makes it tail-recursive. + let is_self_recursive = match top_level + .arguments + .last() + .map(|l| layout_cache.get_repr(*l)) + { + Some(LayoutRepr::LambdaSet(lambda_set)) => lambda_set.contains(name.name()), + _ => false, + }; + + match patterns_to_when(env, loc_args, ret_var, loc_body) { + Ok((_, pattern_symbols, body)) => { + // an anonymous closure. These will always be specialized already + // by the surrounding context, so we can add pending specializations + // for them immediately. + + let already_specialized = self.specialized.is_specialized(name.name(), &top_level); + + let layout = top_level; + + // if we've already specialized this one, no further work is needed. + if !already_specialized { + if self.is_module_thunk(name.name()) { + debug_assert!(layout.arguments.is_empty(), "{name:?}"); + } + + let needs_suspended_specialization = + self.symbol_needs_suspended_specialization(name.name()); + + match ( + &mut self.pending_specializations, + needs_suspended_specialization, + ) { + (PendingSpecializations::Finding(suspended), _) + | (PendingSpecializations::Making(suspended), true) => { + // register the pending specialization, so this gets code genned later + suspended.specialization(env.subs, name, layout, annotation); + + match self.partial_procs.symbol_to_id(name.name()) { + Some(occupied) => { + let existing = self.partial_procs.get_id(occupied); + // if we're adding the same partial proc twice, they must be the actual same! + // + // NOTE we can't skip extra work! we still need to make the specialization for this + // invocation. The content of the `annotation` can be different, even if the variable + // number is the same + debug_assert_eq!(annotation, existing.annotation); + debug_assert_eq!(captured_symbols, existing.captured_symbols); + debug_assert_eq!(is_self_recursive, existing.is_self_recursive); + + // the partial proc is already in there, do nothing + } + None => { + let pattern_symbols = pattern_symbols.into_bump_slice(); + + let partial_proc = PartialProc { + annotation, + pattern_symbols, + captured_symbols, + body: body.value, + body_var: ret_var, + is_self_recursive, + }; + + self.partial_procs.insert(name.name(), partial_proc); + } + } + } + (PendingSpecializations::Making(_), false) => { + // Mark this proc as in-progress, so if we're dealing with + // mutually recursive functions, we don't loop forever. + // (We had a bug around this before this system existed!) + self.specialized.mark_in_progress(name.name(), layout); + + let partial_proc_id = if let Some(partial_proc_id) = + self.partial_procs.symbol_to_id(name.name()) + { + // NOTE we can't skip extra work! We still need to make the specialization for this + // invocation. + partial_proc_id + } else { + let pattern_symbols = pattern_symbols.into_bump_slice(); + + let partial_proc = PartialProc { + annotation, + pattern_symbols, + captured_symbols, + body: body.value, + body_var: ret_var, + is_self_recursive, + }; + + self.partial_procs.insert(name.name(), partial_proc) + }; + + match specialize_variable( + env, + self, + name, + layout_cache, + annotation, + partial_proc_id, + ) { + Ok((proc, layout)) => { + let proc_name = proc.name; + let function_layout = + ProcLayout::from_raw_named(env.arena, proc_name, layout); + self.specialized.insert_specialized( + proc_name.name(), + function_layout, + proc, + ); + } + Err(error) => { + panic!("TODO generate a RuntimeError message for {error:?}"); + } + } + } + } + } + + Ok(layout) + } + Err(loc_error) => Err(loc_error.value), + } + } + + fn insert_passed_by_name( + &mut self, + env: &mut Env<'a, '_>, + fn_var: Variable, + name: LambdaName<'a>, + layout: ProcLayout<'a>, + layout_cache: &mut LayoutCache<'a>, + ) { + // If we've already specialized this one, no further work is needed. + if self.specialized.is_specialized(name.name(), &layout) { + return; + } + + // If this is an imported symbol, let its home module make this specialization + if env.is_imported_symbol(name.name()) || env.is_unloaded_derived_symbol(name.name(), self) + { + add_needed_external(self, env, fn_var, name); + return; + } + + // register the pending specialization, so this gets code genned later + if self.module_thunks.contains(&name.name()) { + debug_assert!(layout.arguments.is_empty()); + } + + // This should only be called when pending_specializations is Some. + // Otherwise, it's being called in the wrong pass! + let needs_suspended_specialization = + self.symbol_needs_suspended_specialization(name.name()); + match ( + &mut self.pending_specializations, + needs_suspended_specialization, + ) { + (PendingSpecializations::Finding(suspended), _) + | (PendingSpecializations::Making(suspended), true) => { + suspended.specialization(env.subs, name, layout, fn_var); + } + (PendingSpecializations::Making(_), false) => { + let proc_name = name; + + let partial_proc_id = match self.partial_procs.symbol_to_id(proc_name.name()) { + Some(p) => p, + None => panic!( + "no partial_proc for {:?} in module {:?}", + proc_name, env.home + ), + }; + + // Mark this proc as in-progress, so if we're dealing with + // mutually recursive functions, we don't loop forever. + // (We had a bug around this before this system existed!) + self.specialized.mark_in_progress(proc_name.name(), layout); + + // See https://github.com/roc-lang/roc/issues/1600 + // + // The annotation variable is the generic/lifted/top-level annotation. + // It is connected to the variables of the function's body + // + // fn_var is the variable representing the type that we actually need for the + // function right here. + match specialize_variable( + env, + self, + proc_name, + layout_cache, + fn_var, + partial_proc_id, + ) { + Ok((proc, raw_layout)) => { + let proc_layout = + ProcLayout::from_raw_named(env.arena, proc_name, raw_layout); + + self.specialized + .insert_specialized(proc_name.name(), proc_layout, proc); + } + Err(error) => { + panic!("TODO generate a RuntimeError message for {error:?}"); + } + } + } + } + } + + /// Gets a specialization for a symbol, or creates a new one. + #[inline(always)] + fn get_or_insert_symbol_specialization( + &mut self, + env: &mut Env<'a, '_>, + layout_cache: &mut LayoutCache<'a>, + symbol: Symbol, + specialization_var: Variable, + ) -> Symbol { + let symbol_specializations = match self.symbol_specializations.0.get_mut(&symbol) { + Some(m) => m, + None => { + // Not eligible for multiple specializations + return symbol; + } + }; + + let arena = env.arena; + let subs: &Subs = env.subs; + + let layout = match layout_cache.from_var(arena, specialization_var, subs) { + Ok(layout) => layout, + // This can happen when the def symbol has a type error. In such cases just use the + // def symbol, which is erroring. + Err(_) => return symbol, + }; + + // For the first specialization, always reuse the current symbol. The vast majority of defs + // only have one instance type, so this preserves readability of the IR. + // TODO: turn me off and see what breaks. + let needs_fresh_symbol = !symbol_specializations.is_empty(); + + let mut make_specialized_symbol = || { + if needs_fresh_symbol { + env.unique_symbol() + } else { + symbol + } + }; + + let current_use = self.specialization_stack.current_use_depth(); + let (specialized_symbol, deepest_use) = symbol_specializations + .get_or_insert(layout, || (make_specialized_symbol(), current_use)); + + if deepest_use.is_nested_use_in(¤t_use) { + *deepest_use = current_use; + } + + *specialized_symbol + } + + /// Get the symbol specializations used in the active specialization's body. + pub fn get_symbol_specializations_used_in_body( + &self, + symbol: Symbol, + ) -> Option + '_> { + let this_use = self.specialization_stack.current_use_depth(); + self.symbol_specializations.0.get(&symbol).map(move |l| { + l.iter().filter_map(move |(_, (sym, deepest_use))| { + if deepest_use.is_nested_use_in(&this_use) { + Some(*sym) + } else { + None + } + }) + }) + } +} + +#[derive(Default)] +pub struct Specializations<'a> { + by_symbol: MutMap, Proc<'a>>>, +} + +impl<'a> Specializations<'a> { + pub fn insert(&mut self, symbol: Symbol, layout: InLayout<'a>, proc: Proc<'a>) { + let procs_by_layout = self + .by_symbol + .entry(symbol) + .or_insert_with(|| HashMap::with_capacity_and_hasher(1, default_hasher())); + + // If we already have an entry for this, it should be no different + // from what we're about to insert. + debug_assert!( + !procs_by_layout.contains_key(&layout) || procs_by_layout.get(&layout) == Some(&proc) + ); + + procs_by_layout.insert(layout, proc); + } + + pub fn len(&self) -> usize { + self.by_symbol.len() + } + + pub fn is_empty(&self) -> bool { + self.by_symbol.is_empty() + } +} + +pub struct Env<'a, 'i> { + pub arena: &'a Bump, + pub subs: &'i mut Subs, + /// [Subs] to write specialized variables of lookups in expects. + /// [None] if this module doesn't produce any expects. + pub expectation_subs: Option<&'i mut Subs>, + pub home: ModuleId, + pub ident_ids: &'i mut IdentIds, + pub target_info: TargetInfo, + pub update_mode_ids: &'i mut UpdateModeIds, + pub call_specialization_counter: u32, + // TODO: WorldAbilities and exposed_by_module share things, think about how to combine them + pub abilities: AbilitiesView<'i>, + pub exposed_by_module: &'i ExposedByModule, + pub derived_module: &'i SharedDerivedModule, + pub struct_indexing: UsageTrackingMap<(Symbol, u64), Symbol>, +} + +impl<'a, 'i> Env<'a, 'i> { + pub fn unique_symbol(&mut self) -> Symbol { + let ident_id = self.ident_ids.gen_unique(); + + Symbol::new(self.home, ident_id) + } + + pub fn named_unique_symbol(&mut self, name: &str) -> Symbol { + let ident_id = self.ident_ids.add_str(name); + Symbol::new(self.home, ident_id) + } + + pub fn next_update_mode_id(&mut self) -> UpdateModeId { + self.update_mode_ids.next_id() + } + + pub fn next_call_specialization_id(&mut self) -> CallSpecId { + let id = CallSpecId { + id: self.call_specialization_counter, + }; + + self.call_specialization_counter += 1; + + id + } + + pub fn is_imported_symbol(&self, symbol: Symbol) -> bool { + let sym_module = symbol.module_id(); + sym_module != self.home + // The Derived_gen module takes responsibility for code-generating symbols in the + // Derived_synth module. + && !(self.home == ModuleId::DERIVED_GEN && sym_module == ModuleId::DERIVED_SYNTH) + } + + /// While specializing the Derived_gen module, derived implementation symbols from the + /// Derived_synth module may be discovered. These implementations may not have yet been loaded + /// into the Derived_gen module, because we only load them before making specializations, and + /// not during mono itself (yet). + /// + /// When this procedure returns `true`, the symbol should be marked as an external specialization, + /// so that a subsequent specializations pass loads the derived implementation into Derived_gen + /// and then code-generates appropriately. + pub fn is_unloaded_derived_symbol(&self, symbol: Symbol, procs: &Procs<'a>) -> bool { + self.home == ModuleId::DERIVED_GEN + && symbol.module_id() == ModuleId::DERIVED_SYNTH + && !procs.partial_procs.contains_key(symbol) + // TODO: locking to find the answer in the `Derived_gen` module is not great, since + // Derived_gen also blocks other modules specializing. Improve this later. + && self + .derived_module + .lock() + .expect("derived module is poisoned") + .is_derived_def(symbol) + } + + /// Unifies two variables and performs lambda set compaction. + /// Use this rather than [roc_unify::unify] directly! + fn unify<'b, 'c: 'b>( + &mut self, + external_specializations: impl IntoIterator>, + layout_cache: &mut LayoutCache, + left: Variable, + right: Variable, + ) -> Result<(), UnificationFailed> { + debug_assert_ne!( + self.home, + ModuleId::DERIVED_SYNTH, + "should never be monomorphizing the derived synth module!" + ); + + let changed_variables = roc_late_solve::unify( + self.home, + self.arena, + self.subs, + &self.abilities, + self.derived_module, + self.exposed_by_module, + left, + right, + )?; + + layout_cache.invalidate(self.subs, changed_variables.iter().copied()); + external_specializations + .into_iter() + .for_each(|e| e.invalidate_cache(&changed_variables)); + + Ok(()) + } +} + +#[derive(Clone, Debug, PartialEq, Copy, Eq, Hash)] +pub struct JoinPointId(pub Symbol); + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Param<'a> { + pub symbol: Symbol, + pub layout: InLayout<'a>, +} + +impl<'a> Param<'a> { + pub const EMPTY: Self = Param { + symbol: Symbol::EMPTY_PARAM, + layout: Layout::UNIT, + }; +} + +pub fn cond<'a>( + env: &mut Env<'a, '_>, + cond_symbol: Symbol, + cond_layout: InLayout<'a>, + pass: Stmt<'a>, + fail: Stmt<'a>, + ret_layout: InLayout<'a>, +) -> Stmt<'a> { + let branches = env.arena.alloc([(1u64, BranchInfo::None, pass)]); + let default_branch = (BranchInfo::None, &*env.arena.alloc(fail)); + + Stmt::Switch { + cond_symbol, + cond_layout, + ret_layout, + branches, + default_branch, + } +} + +pub type Stores<'a> = &'a [(Symbol, Layout<'a>, Expr<'a>)]; + +/// The specialized type of a lookup. Represented as a type-variable. +pub type LookupType = Variable; + +#[derive(Clone, Debug, PartialEq)] +pub enum Stmt<'a> { + Let(Symbol, Expr<'a>, InLayout<'a>, &'a Stmt<'a>), + Switch { + /// This *must* stand for an integer, because Switch potentially compiles to a jump table. + cond_symbol: Symbol, + cond_layout: InLayout<'a>, + /// The u64 in the tuple will be compared directly to the condition Expr. + /// If they are equal, this branch will be taken. + branches: &'a [(u64, BranchInfo<'a>, Stmt<'a>)], + /// If no other branches pass, this default branch will be taken. + default_branch: (BranchInfo<'a>, &'a Stmt<'a>), + /// Each branch must return a value of this type. + ret_layout: InLayout<'a>, + }, + Ret(Symbol), + Refcounting(ModifyRc, &'a Stmt<'a>), + Expect { + condition: Symbol, + region: Region, + lookups: &'a [Symbol], + variables: &'a [LookupType], + /// what happens after the expect + remainder: &'a Stmt<'a>, + }, + ExpectFx { + condition: Symbol, + region: Region, + lookups: &'a [Symbol], + variables: &'a [LookupType], + /// what happens after the expect + remainder: &'a Stmt<'a>, + }, + Dbg { + /// The expression we're displaying + symbol: Symbol, + /// The specialized variable of the expression + variable: Variable, + /// What happens after the dbg + remainder: &'a Stmt<'a>, + }, + /// a join point `join f = in remainder` + Join { + id: JoinPointId, + parameters: &'a [Param<'a>], + /// body of the join point + /// what happens after _jumping to_ the join point + body: &'a Stmt<'a>, + /// what happens after _defining_ the join point + remainder: &'a Stmt<'a>, + }, + Jump(JoinPointId, &'a [Symbol]), + Crash(Symbol, CrashTag), +} + +/// Source of crash, and its runtime representation to roc_panic. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[repr(u32)] +pub enum CrashTag { + /// The crash is due to Roc, either via a builtin or type error. + Roc = 0, + /// The crash is user-defined. + User = 1, +} + +impl TryFrom for CrashTag { + type Error = (); + + fn try_from(value: u32) -> Result { + match value { + 0 => Ok(Self::Roc), + 1 => Ok(Self::User), + _ => Err(()), + } + } +} + +/// in the block below, symbol `scrutinee` is assumed be be of shape `tag_id` +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum BranchInfo<'a> { + None, + Constructor { + scrutinee: Symbol, + layout: InLayout<'a>, + tag_id: TagIdIntType, + }, + List { + scrutinee: Symbol, + len: u64, + }, + Unique { + scrutinee: Symbol, + unique: bool, + }, +} + +impl<'a> BranchInfo<'a> { + pub fn to_doc<'b, D, A>(&'b self, alloc: &'b D, _pretty: bool) -> DocBuilder<'b, D, A> + where + D: DocAllocator<'b, A>, + D::Doc: Clone, + A: Clone, + { + alloc.text("") + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum ModifyRc { + /// Increment a reference count + Inc(Symbol, u64), + /// Decrement a reference count + Dec(Symbol), + /// A DecRef is a non-recursive reference count decrement + /// e.g. If we Dec a list of lists, then if the reference count of the outer list is one, + /// a Dec will recursively decrement all elements, then free the memory of the outer list. + /// A DecRef would just free the outer list. + /// That is dangerous because you may not free the elements, but in our Zig builtins, + /// sometimes we know we already dealt with the elements (e.g. by copying them all over + /// to a new list) and so we can just do a DecRef, which is much cheaper in such a case. + DecRef(Symbol), + /// Unconditionally deallocate the memory. For tag union that do pointer tagging (store the tag + /// id in the pointer) the backend has to clear the tag id! + Free(Symbol), +} + +impl ModifyRc { + pub fn to_doc<'a, D, A>(self, alloc: &'a D, pretty: bool) -> DocBuilder<'a, D, A> + where + D: DocAllocator<'a, A>, + D::Doc: Clone, + A: Clone, + { + use ModifyRc::*; + + match self { + Inc(symbol, 1) => alloc + .text("inc ") + .append(symbol_to_doc(alloc, symbol, pretty)) + .append(";"), + Inc(symbol, n) => alloc + .text("inc ") + .append(text!(alloc, "{} ", n)) + .append(symbol_to_doc(alloc, symbol, pretty)) + .append(";"), + Dec(symbol) => alloc + .text("dec ") + .append(symbol_to_doc(alloc, symbol, pretty)) + .append(";"), + DecRef(symbol) => alloc + .text("decref ") + .append(symbol_to_doc(alloc, symbol, pretty)) + .append(";"), + Free(symbol) => alloc + .text("free ") + .append(symbol_to_doc(alloc, symbol, pretty)) + .append(";"), + } + } + + pub fn get_symbol(&self) -> Symbol { + use ModifyRc::*; + + match self { + Inc(symbol, _) => *symbol, + Dec(symbol) => *symbol, + DecRef(symbol) => *symbol, + Free(symbol) => *symbol, + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Call<'a> { + pub call_type: CallType<'a>, + pub arguments: &'a [Symbol], +} + +impl<'a> Call<'a> { + pub fn to_doc<'b, D, A>(&'b self, alloc: &'b D, pretty: bool) -> DocBuilder<'b, D, A> + where + D: DocAllocator<'b, A>, + D::Doc: Clone, + A: Clone, + { + use CallType::*; + + let arguments = self.arguments; + + match self.call_type { + CallType::ByName { name, .. } => { + let it = std::iter::once(name.name()) + .chain(arguments.iter().copied()) + .map(|s| symbol_to_doc(alloc, s, pretty)); + + alloc.text("CallByName ").append(alloc.intersperse(it, " ")) + } + CallType::ByPointer { pointer, .. } => { + let it = std::iter::once(pointer) + .chain(arguments.iter().copied()) + .map(|s| symbol_to_doc(alloc, s, pretty)); + + alloc.text("CallByPtr ").append(alloc.intersperse(it, " ")) + } + LowLevel { op: lowlevel, .. } => { + let it = arguments.iter().map(|s| symbol_to_doc(alloc, *s, pretty)); + + text!(alloc, "lowlevel {:?} ", lowlevel).append(alloc.intersperse(it, " ")) + } + HigherOrder(higher_order) => { + let it = arguments.iter().map(|s| symbol_to_doc(alloc, *s, pretty)); + + text!(alloc, "lowlevel {:?} ", higher_order.op).append(alloc.intersperse(it, " ")) + } + Foreign { + ref foreign_symbol, .. + } => { + let it = arguments.iter().map(|s| symbol_to_doc(alloc, *s, pretty)); + + text!(alloc, "foreign {:?} ", foreign_symbol.as_str()) + .append(alloc.intersperse(it, " ")) + } + } + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct CallSpecId { + id: u32, +} + +impl CallSpecId { + pub fn to_bytes(self) -> [u8; 4] { + self.id.to_ne_bytes() + } + + /// Dummy value for generating refcount helper procs in the backends + /// This happens *after* specialization so it's safe + pub const BACKEND_DUMMY: Self = Self { id: 0 }; +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct UpdateModeId { + id: u32, +} + +impl UpdateModeId { + pub fn to_bytes(self) -> [u8; 4] { + self.id.to_ne_bytes() + } + + /// Dummy value for generating refcount helper procs in the backends + /// This happens *after* alias analysis so it's safe + pub const BACKEND_DUMMY: Self = Self { id: 0 }; +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct UpdateModeIds { + next: u32, +} + +impl UpdateModeIds { + pub const fn new() -> Self { + Self { next: 0 } + } + + pub fn next_id(&mut self) -> UpdateModeId { + let id = UpdateModeId { id: self.next }; + self.next += 1; + id + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum CallType<'a> { + ByName { + name: LambdaName<'a>, + ret_layout: InLayout<'a>, + arg_layouts: &'a [InLayout<'a>], + specialization_id: CallSpecId, + }, + ByPointer { + pointer: Symbol, + ret_layout: InLayout<'a>, + arg_layouts: &'a [InLayout<'a>], + }, + Foreign { + foreign_symbol: ForeignSymbol, + ret_layout: InLayout<'a>, + }, + LowLevel { + op: LowLevel, + update_mode: UpdateModeId, + }, + HigherOrder(&'a HigherOrderLowLevel<'a>), +} + +impl<'a> CallType<'a> { + /** + Replace calls to wrappers of lowlevel functions with the lowlevel function itself + */ + pub fn replace_lowlevel_wrapper(self) -> Self { + match self { + CallType::ByName { name, .. } => match LowLevelWrapperType::from_symbol(name.name()) { + LowLevelWrapperType::CanBeReplacedBy(lowlevel) => CallType::LowLevel { + op: lowlevel, + update_mode: UpdateModeId::BACKEND_DUMMY, + }, + LowLevelWrapperType::NotALowLevelWrapper => self, + }, + _ => self, + } + } +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct PassedFunction<'a> { + /// name of the top-level function that is passed as an argument + /// e.g. in `List.map xs Num.abs` this would be `Num.abs` + pub name: LambdaName<'a>, + + pub argument_layouts: &'a [InLayout<'a>], + pub return_layout: InLayout<'a>, + + pub specialization_id: CallSpecId, + + /// Symbol of the environment captured by the function argument + pub captured_environment: Symbol, + + pub owns_captured_environment: bool, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct HigherOrderLowLevel<'a> { + pub op: crate::low_level::HigherOrder, + + /// TODO I _think_ we can get rid of this, perhaps only keeping track of + /// the layout of the closure argument, if any + pub closure_env_layout: Option>, + + /// update mode of the higher order lowlevel itself + pub update_mode: UpdateModeId, + + pub passed_function: PassedFunction<'a>, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct ReuseToken { + pub symbol: Symbol, + pub update_tag_id: bool, + pub update_mode: UpdateModeId, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum ErasedField { + /// Load a dereferenceable pointer to the value. + Value, + /// Load a non-dereferenceable pointer to the value. + ValuePtr, + Callee, +} + +#[derive(Clone, Debug, PartialEq)] +pub enum Expr<'a> { + Literal(Literal<'a>), + + // Functions + Call(Call<'a>), + + Tag { + tag_layout: UnionLayout<'a>, + tag_id: TagIdIntType, + arguments: &'a [Symbol], + reuse: Option, + }, + Struct(&'a [Symbol]), + NullPointer, + + StructAtIndex { + index: u64, + field_layouts: &'a [InLayout<'a>], + structure: Symbol, + }, + + GetTagId { + structure: Symbol, + union_layout: UnionLayout<'a>, + }, + + UnionAtIndex { + structure: Symbol, + tag_id: TagIdIntType, + union_layout: UnionLayout<'a>, + index: u64, + }, + GetElementPointer { + structure: Symbol, + union_layout: UnionLayout<'a>, + indices: &'a [u64], + }, + + Array { + elem_layout: InLayout<'a>, + elems: &'a [ListLiteralElement<'a>], + }, + EmptyArray, + + /// Creates a type-erased value. + ErasedMake { + /// The erased value. If this is an erased function, the value are the function captures, + /// or `None` if the function is not a closure. + value: Option, + /// The function pointer of the erased value, if it's an erased function. + callee: Symbol, + }, + + /// Loads a field from a type-erased value. + ErasedLoad { + /// The erased symbol. + symbol: Symbol, + /// The field to load. + field: ErasedField, + }, + + /// Returns a pointer to the given function. + FunctionPointer { + lambda_name: LambdaName<'a>, + }, + + Alloca { + element_layout: InLayout<'a>, + initializer: Option, + }, + + Reset { + symbol: Symbol, + update_mode: UpdateModeId, + }, + + // Just like Reset, but does not recursively decrement the children. + // Used in reuse analysis to replace a decref with a resetRef to avoid decrementing when the dec ref didn't. + ResetRef { + symbol: Symbol, + update_mode: UpdateModeId, + }, + + RuntimeErrorFunction(&'a str), +} + +impl<'a> Literal<'a> { + pub fn to_doc<'b, D, A>(&'b self, alloc: &'b D) -> DocBuilder<'b, D, A> + where + D: DocAllocator<'b, A>, + D::Doc: Clone, + A: Clone, + { + use Literal::*; + + match self { + Int(bytes) => text!(alloc, "{}i64", i128::from_ne_bytes(*bytes)), + U128(bytes) => text!(alloc, "{}u128", u128::from_ne_bytes(*bytes)), + Float(lit) => text!(alloc, "{}f64", lit), + Decimal(bytes) => text!(alloc, "{}dec", RocDec::from_ne_bytes(*bytes)), + Bool(lit) => text!(alloc, "{}", lit), + Byte(lit) => text!(alloc, "{}u8", lit), + Str(lit) => text!(alloc, "{:?}", lit), + } + } +} + +pub(crate) fn symbol_to_doc_string(symbol: Symbol, force_pretty: bool) -> String { + use roc_module::ident::ModuleName; + + if pretty_print_ir_symbols() || force_pretty { + format!("{symbol:?}") + } else { + let text = format!("{symbol}"); + + if text.starts_with(ModuleName::APP) { + let name: String = text.trim_start_matches(ModuleName::APP).into(); + format!("Test{name}") + } else { + text + } + } +} + +fn symbol_to_doc<'b, D, A>(alloc: &'b D, symbol: Symbol, force_pretty: bool) -> DocBuilder<'b, D, A> +where + D: DocAllocator<'b, A>, + D::Doc: Clone, + A: Clone, +{ + alloc.text(symbol_to_doc_string(symbol, force_pretty)) +} + +fn join_point_to_doc<'b, D, A>( + alloc: &'b D, + symbol: JoinPointId, + pretty: bool, +) -> DocBuilder<'b, D, A> +where + D: DocAllocator<'b, A>, + D::Doc: Clone, + A: Clone, +{ + symbol_to_doc(alloc, symbol.0, pretty) +} + +impl<'a> Expr<'a> { + pub fn to_doc<'b, D, A>(&'b self, alloc: &'b D, pretty: bool) -> DocBuilder<'b, D, A> + where + D: DocAllocator<'b, A>, + D::Doc: Clone, + A: Clone, + { + use Expr::*; + + match self { + Literal(lit) => lit.to_doc(alloc), + + Call(call) => call.to_doc(alloc, pretty), + + Tag { + tag_id, + arguments, + reuse: None, + .. + } => { + let doc_tag = alloc + .text("TagId(") + .append(alloc.text(tag_id.to_string())) + .append(")"); + + let it = arguments.iter().map(|s| symbol_to_doc(alloc, *s, pretty)); + + doc_tag + .append(alloc.space()) + .append(alloc.intersperse(it, " ")) + } + + Tag { + tag_id, + arguments, + reuse: Some(reuse_token), + .. + } => { + let doc_tag = alloc + .text("TagId(") + .append(alloc.text(tag_id.to_string())) + .append(")"); + + let it = arguments.iter().map(|s| symbol_to_doc(alloc, *s, pretty)); + + alloc + .text("Reuse ") + .append(symbol_to_doc(alloc, reuse_token.symbol, pretty)) + .append(alloc.space()) + .append(format!("{:?}", reuse_token.update_mode)) + .append(alloc.space()) + .append(doc_tag) + .append(alloc.space()) + .append(alloc.intersperse(it, " ")) + } + NullPointer => alloc.text("NullPointer"), + Reset { + symbol, + update_mode, + } => alloc + .text("Reset { symbol: ") + .append(symbol_to_doc(alloc, *symbol, pretty)) + .append(", id: ") + .append(format!("{update_mode:?}")) + .append(" }"), + ResetRef { + symbol, + update_mode, + } => alloc + .text("ResetRef { symbol: ") + .append(symbol_to_doc(alloc, *symbol, pretty)) + .append(", id: ") + .append(format!("{update_mode:?}")) + .append(" }"), + Struct(args) => { + let it = args.iter().map(|s| symbol_to_doc(alloc, *s, pretty)); + + alloc + .text("Struct {") + .append(alloc.intersperse(it, ", ")) + .append(alloc.text("}")) + } + Array { elems, .. } => { + let it = elems.iter().map(|e| match e { + ListLiteralElement::Literal(l) => l.to_doc(alloc), + ListLiteralElement::Symbol(s) => symbol_to_doc(alloc, *s, pretty), + }); + + alloc + .text("Array [") + .append(alloc.intersperse(it, ", ")) + .append(alloc.text("]")) + } + EmptyArray => alloc.text("Array []"), + + StructAtIndex { + index, structure, .. + } => text!(alloc, "StructAtIndex {} ", index) + .append(symbol_to_doc(alloc, *structure, pretty)), + + RuntimeErrorFunction(s) => text!(alloc, "ErrorFunction {}", s), + + GetTagId { structure, .. } => alloc + .text("GetTagId ") + .append(symbol_to_doc(alloc, *structure, pretty)), + + ErasedMake { value, callee } => { + let value = match value { + Some(v) => symbol_to_doc(alloc, *v, pretty), + None => alloc.text(""), + }; + let callee = symbol_to_doc(alloc, *callee, pretty); + alloc + .text("ErasedMake { value: ") + .append(value) + .append(", callee: ") + .append(callee) + .append(" }") + } + + ErasedLoad { symbol, field } => { + let field = match field { + ErasedField::Value => ".Value", + ErasedField::ValuePtr => ".ValuePtr", + ErasedField::Callee => ".Callee", + }; + + alloc + .text("ErasedLoad ") + .append(symbol_to_doc(alloc, *symbol, pretty)) + .append(alloc.text(" ")) + .append(field) + } + + FunctionPointer { lambda_name } => alloc + .text("FunctionPointer ") + .append(symbol_to_doc(alloc, lambda_name.name(), pretty)), + + UnionAtIndex { + tag_id, + structure, + index, + .. + } => text!(alloc, "UnionAtIndex (Id {tag_id}) (Index {index}) ") + .append(symbol_to_doc(alloc, *structure, pretty)), + + GetElementPointer { + structure, indices, .. + } => { + let it = indices.iter().map(|num| alloc.as_string(num)); + let it = alloc.intersperse(it, ", "); + text!(alloc, "GetElementPointer (Indices [",) + .append(it) + .append(alloc.text("]) ")) + .append(symbol_to_doc(alloc, *structure, pretty)) + } + // .append(alloc.intersperse(index.iter(), ", "))}, + Alloca { initializer, .. } => match initializer { + Some(initializer) => { + text!(alloc, "Alloca ").append(symbol_to_doc(alloc, *initializer, pretty)) + } + None => text!(alloc, "Alloca "), + }, + } + } + + pub fn to_pretty(&self, width: usize, pretty: bool) -> String { + let allocator = BoxAllocator; + let mut w = std::vec::Vec::new(); + self.to_doc::<_, ()>(&allocator, pretty) + .1 + .render(width, &mut w) + .unwrap(); + w.push(b'\n'); + String::from_utf8(w).unwrap() + } + + pub(crate) fn ptr_load(symbol: &'a Symbol) -> Expr<'a> { + Expr::Call(Call { + call_type: CallType::LowLevel { + op: LowLevel::PtrLoad, + update_mode: UpdateModeId::BACKEND_DUMMY, + }, + arguments: std::slice::from_ref(symbol), + }) + } + + pub(crate) fn ptr_store(arguments: &'a [Symbol]) -> Expr<'a> { + Expr::Call(Call { + call_type: CallType::LowLevel { + op: LowLevel::PtrStore, + update_mode: UpdateModeId::BACKEND_DUMMY, + }, + arguments, + }) + } +} + +impl<'a> Stmt<'a> { + pub fn new( + env: &mut Env<'a, '_>, + can_expr: roc_can::expr::Expr, + var: Variable, + procs: &mut Procs<'a>, + layout_cache: &mut LayoutCache<'a>, + ) -> Self { + from_can(env, var, can_expr, procs, layout_cache) + } + + pub fn to_doc<'b, D, A, I>( + &'b self, + alloc: &'b D, + interner: &I, + pretty: bool, + ) -> DocBuilder<'b, D, A> + where + D: DocAllocator<'b, A>, + D::Doc: Clone, + A: Clone, + I: LayoutInterner<'a>, + { + use Stmt::*; + + match self { + Let(symbol, expr, layout, cont) => alloc + .text("let ") + .append(symbol_to_doc(alloc, *symbol, pretty)) + .append(" : ") + .append(interner.to_doc_top(*layout, alloc)) + .append(" = ") + .append(expr.to_doc(alloc, pretty)) + .append(";") + .append(alloc.hardline()) + .append(cont.to_doc(alloc, interner, pretty)), + + Refcounting(modify, cont) => modify + .to_doc(alloc, pretty) + .append(alloc.hardline()) + .append(cont.to_doc(alloc, interner, pretty)), + + Dbg { + symbol, remainder, .. + } => alloc + .text("dbg ") + .append(symbol_to_doc(alloc, *symbol, pretty)) + .append(";") + .append(alloc.hardline()) + .append(remainder.to_doc(alloc, interner, pretty)), + + Expect { + condition, + remainder, + .. + } => alloc + .text("expect ") + .append(symbol_to_doc(alloc, *condition, pretty)) + .append(";") + .append(alloc.hardline()) + .append(remainder.to_doc(alloc, interner, pretty)), + + ExpectFx { + condition, + remainder, + .. + } => alloc + .text("expect-fx ") + .append(symbol_to_doc(alloc, *condition, pretty)) + .append(";") + .append(alloc.hardline()) + .append(remainder.to_doc(alloc, interner, pretty)), + + Ret(symbol) => alloc + .text("ret ") + .append(symbol_to_doc(alloc, *symbol, pretty)) + .append(";"), + + Switch { + cond_symbol, + branches, + default_branch, + .. + } => { + match branches { + [(1, info, pass)] => { + let fail = default_branch.1; + alloc + .text("if ") + .append(symbol_to_doc(alloc, *cond_symbol, pretty)) + .append(" then") + .append(info.to_doc(alloc, pretty)) + .append(alloc.hardline()) + .append(pass.to_doc(alloc, interner, pretty).indent(4)) + .append(alloc.hardline()) + .append(alloc.text("else")) + .append(default_branch.0.to_doc(alloc, pretty)) + .append(alloc.hardline()) + .append(fail.to_doc(alloc, interner, pretty).indent(4)) + } + + _ => { + let default_doc = alloc + .text("default:") + .append(alloc.hardline()) + .append(default_branch.1.to_doc(alloc, interner, pretty).indent(4)) + .indent(4); + + let branches_docs = branches + .iter() + .map(|(tag, _info, expr)| { + text!(alloc, "case {}:", tag) + .append(alloc.hardline()) + .append(expr.to_doc(alloc, interner, pretty).indent(4)) + .indent(4) + }) + .chain(std::iter::once(default_doc)); + // + alloc + .text("switch ") + .append(symbol_to_doc(alloc, *cond_symbol, pretty)) + .append(":") + .append(alloc.hardline()) + .append(alloc.intersperse( + branches_docs, + alloc.hardline().append(alloc.hardline()), + )) + .append(alloc.hardline()) + } + } + } + + Crash(s, _src) => alloc + .text("Crash ") + .append(symbol_to_doc(alloc, *s, pretty)), + + Join { + id, + parameters, + body: continuation, + remainder, + } => { + let it = parameters + .iter() + .map(|p| symbol_to_doc(alloc, p.symbol, pretty)); + + alloc.intersperse( + vec![ + alloc + .text("joinpoint ") + .append(join_point_to_doc(alloc, *id, pretty)) + .append(" ".repeat(parameters.len().min(1))) + .append(alloc.intersperse(it, alloc.space())) + .append(":"), + continuation.to_doc(alloc, interner, pretty).indent(4), + alloc.text("in"), + remainder.to_doc(alloc, interner, pretty), + ], + alloc.hardline(), + ) + } + Jump(id, arguments) => { + let it = arguments.iter().map(|s| symbol_to_doc(alloc, *s, pretty)); + + alloc + .text("jump ") + .append(join_point_to_doc(alloc, *id, pretty)) + .append(" ".repeat(arguments.len().min(1))) + .append(alloc.intersperse(it, alloc.space())) + .append(";") + } + } + } + + pub fn to_pretty(&self, interner: &I, width: usize, pretty: bool) -> String + where + I: LayoutInterner<'a>, + { + let allocator = BoxAllocator; + let mut w = std::vec::Vec::new(); + self.to_doc::<_, (), _>(&allocator, interner, pretty) + .1 + .render(width, &mut w) + .unwrap(); + w.push(b'\n'); + String::from_utf8(w).unwrap() + } + + pub fn if_then_else( + arena: &'a Bump, + condition_symbol: Symbol, + return_layout: InLayout<'a>, + then_branch_stmt: Stmt<'a>, + else_branch_stmt: &'a Stmt<'a>, + ) -> Self { + let then_branch_info = BranchInfo::Constructor { + scrutinee: condition_symbol, + layout: Layout::BOOL, + tag_id: 1, + }; + let then_branch = (1u64, then_branch_info, then_branch_stmt); + + let else_branch_info = BranchInfo::Constructor { + scrutinee: condition_symbol, + layout: Layout::BOOL, + tag_id: 0, + }; + let else_branch = (else_branch_info, else_branch_stmt); + + Stmt::Switch { + cond_symbol: condition_symbol, + cond_layout: Layout::BOOL, + branches: &*arena.alloc([then_branch]), + default_branch: else_branch, + ret_layout: return_layout, + } + } +} + +fn from_can_let<'a>( + env: &mut Env<'a, '_>, + procs: &mut Procs<'a>, + layout_cache: &mut LayoutCache<'a>, + def: Box, + cont: Box>, + variable: Variable, + opt_assigned_and_hole: Option<(Symbol, &'a Stmt<'a>)>, +) -> Stmt<'a> { + use roc_can::expr::Expr::*; + + macro_rules! lower_rest { + ($variable:expr, $expr:expr) => { + lower_rest!(env, procs, layout_cache, $variable, $expr) + }; + ($env:expr, $procs:expr, $layout_cache:expr, $variable:expr, $expr:expr) => { + match opt_assigned_and_hole { + None => from_can($env, $variable, $expr, $procs, $layout_cache), + Some((assigned, hole)) => with_hole( + $env, + $expr, + $variable, + $procs, + $layout_cache, + assigned, + hole, + ), + } + }; + } + + if let roc_can::pattern::Pattern::Identifier(symbol) = &def.loc_pattern.value { + return match def.loc_expr.value { + Closure(closure_data) => { + register_capturing_closure(env, procs, layout_cache, *symbol, closure_data); + + lower_rest!(variable, cont.value) + } + RecordAccessor(accessor_data) => { + let fresh_record_symbol = env.unique_symbol(); + let closure_data = accessor_data.to_closure_data(fresh_record_symbol); + debug_assert_eq!(*symbol, closure_data.name); + register_noncapturing_closure(env, procs, *symbol, closure_data); + + lower_rest!(variable, cont.value) + } + Var(original, _) | AbilityMember(original, _, _) + if procs.get_partial_proc(original).is_none() => + { + // a variable is aliased, e.g. + // + // foo = bar + // + // We need to generate an IR that is free of local lvalue aliasing, as this aids in + // refcounting. As such, variable aliasing usually involves renaming the LHS in the + // rest of the program with the RHS (i.e. [foo->bar]); see `handle_variable_aliasing` + // below for the exact algorithm. + // + // However, do not attempt to eliminate aliasing to procedures + // (either a function pointer or a thunk) here. Doing so is not necessary - if we + // have `var = f` where `f` is either a proc or thunk, in either case, `f` will be + // resolved to an rvalue, not an lvalue: + // + // - If `f` is a proc, we assign to `var` its closure data (even if the lambda set + // of `f` is unary with no captures, we leave behind the empty closure data) + // + // - If `f` is a thunk, we force the thunk and assign to `var` its value. + // + // With this in mind, when `f` is a thunk or proper function, we are free to follow + // the usual (non-lvalue-aliasing) branch of assignment, and end up with correct + // code. + // + // === + // + // Recording that an lvalue references a procedure or a thunk may open up + // opportunities for optimization - and indeed, recording this information may + // sometimes eliminate such unused lvalues, or inline closure data. However, in + // general, making sure this kind of aliasing works correctly is very difficult. As + // illustration, consider + // + // getNum1 = \{} -> 1u64 + // getNum2 = \{} -> 2u64 + // + // dispatch = \fun -> fun {} + // + // main = + // myFun1 = getNum1 + // myFun2 = getNum2 + // dispatch (if Bool.true then myFun1 else myFun2) + // + // Suppose we leave nothing behind for the assignments `myFun* = getNum*`, and + // instead simply associate that they reference procs. In the if-then-else + // expression, we then need to construct the closure data for both getNum1 and + // getNum2 - but we do not know what lambdas they represent, as we only have access + // to the symbols `myFun1` and `myFun2`. + // + // While associations of `myFun1 -> getNum1` could be propogated, the story gets + // more complicated when the referenced proc itself resolves a lambda set with + // indirection; for example consider the amendment + // + // getNum1 = @Dispatcher \{} -> 1u64 + // getNum2 = @Dispatcher \{} -> 2u64 + // + // Now, even the association of `myFun1 -> getNum1` is not enough, as the lambda + // set of (if Bool.true then myFun1 else myFun2) would not be { getNum1, getNum2 } + // - it would be the binary lambda set of the anonymous closures created under the + // `@Dispatcher` wrappers. + // + // Trying to keep all this information in line has been error prone, and is not + // attempted. + + // TODO: right now we need help out rustc with the closure types; + // it isn't able to infer the right lifetime bounds. See if we + // can remove the annotations in the future. + let build_rest = + |env: &mut Env<'a, '_>, + procs: &mut Procs<'a>, + layout_cache: &mut LayoutCache<'a>| { + lower_rest!(env, procs, layout_cache, variable, cont.value) + }; + + return handle_variable_aliasing( + env, + procs, + layout_cache, + def.expr_var, + *symbol, + original, + build_rest, + ); + } + LetNonRec(nested_def, nested_cont) => { + use roc_can::expr::Expr::*; + // We must transform + // + // let answer = 1337 + // in + // let unused = + // let nested = 17 + // in + // nested + // in + // answer + // + // into + // + // let answer = 1337 + // in + // let nested = 17 + // in + // let unused = nested + // in + // answer + + use roc_can::{def::Def, expr::Expr, pattern::Pattern}; + + let new_outer = match &nested_cont.value { + &Expr::Closure(ClosureData { + name: anon_name, .. + }) => { + // A wrinkle: + // + // let f = + // let n = 1 in + // \{} -[#lam]-> n + // + // must become + // + // let n = 1 in + // let #lam = \{} -[#lam]-> n in + // let f = #lam + + debug_assert_ne!(*symbol, anon_name); + + // #lam = \... + let def_anon_closure = Box::new(Def { + loc_pattern: Loc::at_zero(Pattern::Identifier(anon_name)), + loc_expr: *nested_cont, + expr_var: def.expr_var, + pattern_vars: std::iter::once((anon_name, def.expr_var)).collect(), + annotation: None, + }); + + // f = #lam + let new_def = Box::new(Def { + loc_pattern: def.loc_pattern, + loc_expr: Loc::at_zero(Expr::Var(anon_name, def.expr_var)), + expr_var: def.expr_var, + pattern_vars: def.pattern_vars, + annotation: def.annotation, + }); + + let new_inner = LetNonRec(new_def, cont); + + LetNonRec( + nested_def, + Box::new(Loc::at_zero(LetNonRec( + def_anon_closure, + Box::new(Loc::at_zero(new_inner)), + ))), + ) + } + _ => { + let new_def = Def { + loc_pattern: def.loc_pattern, + loc_expr: *nested_cont, + pattern_vars: def.pattern_vars, + annotation: def.annotation, + expr_var: def.expr_var, + }; + + let new_inner = LetNonRec(Box::new(new_def), cont); + + LetNonRec(nested_def, Box::new(Loc::at_zero(new_inner))) + } + }; + + lower_rest!(variable, new_outer) + } + LetRec(nested_defs, nested_cont, cycle_mark) => { + use roc_can::expr::Expr::*; + // We must transform + // + // let answer = 1337 + // in + // let unused = + // let nested = \{} -> nested {} + // in + // nested + // in + // answer + // + // into + // + // let answer = 1337 + // in + // let nested = \{} -> nested {} + // in + // let unused = nested + // in + // answer + + let new_def = roc_can::def::Def { + loc_pattern: def.loc_pattern, + loc_expr: *nested_cont, + pattern_vars: def.pattern_vars, + annotation: def.annotation, + expr_var: def.expr_var, + }; + + let new_inner = LetNonRec(Box::new(new_def), cont); + + let new_outer = LetRec(nested_defs, Box::new(Loc::at_zero(new_inner)), cycle_mark); + + lower_rest!(variable, new_outer) + } + e @ (Int(..) | Float(..) | Num(..)) => { + let (str, val): (Box, IntOrFloatValue) = match e { + Int(_, _, str, val, _) => (str, IntOrFloatValue::Int(val)), + Float(_, _, str, val, _) => (str, IntOrFloatValue::Float(val)), + Num(_, str, val, _) => (str, IntOrFloatValue::Int(val)), + _ => unreachable!(), + }; + procs.symbol_specializations.mark_eligible(*symbol); + + let mut stmt = lower_rest!(variable, cont.value); + + let needed_specializations = procs.symbol_specializations.remove(*symbol).unwrap(); + let zero_specialization = if needed_specializations.is_empty() { + let layout = layout_cache + .from_var(env.arena, def.expr_var, env.subs) + .unwrap(); + Some((layout, *symbol)) + } else { + None + }; + + // Layer on the specialized numbers + for (layout, sym) in needed_specializations + .into_iter() + .map(|(lay, (sym, _))| (lay, sym)) + .chain(zero_specialization) + { + let literal = make_num_literal(&layout_cache.interner, layout, &str, val); + stmt = Stmt::Let( + sym, + Expr::Literal(literal.to_expr_literal()), + layout, + env.arena.alloc(stmt), + ); + } + + stmt + } + _ => { + let rest = lower_rest!(variable, cont.value); + + with_hole( + env, + def.loc_expr.value, + def.expr_var, + procs, + layout_cache, + *symbol, + env.arena.alloc(rest), + ) + } + }; + } + + // this may be a destructure pattern + let (mono_pattern, assignments) = + match from_can_pattern(env, procs, layout_cache, &def.loc_pattern.value) { + Ok(v) => v, + Err(_) => todo!(), + }; + + // convert the continuation + let mut stmt = lower_rest!(variable, cont.value); + + // layer on any default record fields + for (symbol, variable, expr) in assignments { + let hole = env.arena.alloc(stmt); + stmt = with_hole(env, expr, variable, procs, layout_cache, symbol, hole); + } + + match def.loc_expr.value { + roc_can::expr::Expr::Var(outer_symbol, _) if !procs.is_module_thunk(outer_symbol) => { + store_pattern(env, procs, layout_cache, &mono_pattern, outer_symbol, stmt) + } + _ => { + let outer_symbol = env.unique_symbol(); + stmt = store_pattern(env, procs, layout_cache, &mono_pattern, outer_symbol, stmt); + + // convert the def body, store in outer_symbol + with_hole( + env, + def.loc_expr.value, + def.expr_var, + procs, + layout_cache, + outer_symbol, + env.arena.alloc(stmt), + ) + } + } +} + +/// turn record/tag patterns into a when expression, e.g. +/// +/// foo = \{ x } -> body +/// +/// becomes +/// +/// foo = \r -> when r is { x } -> body +/// +/// conversion of one-pattern when expressions will do the most optimal thing +#[allow(clippy::type_complexity)] +fn patterns_to_when<'a>( + env: &mut Env<'a, '_>, + patterns: std::vec::Vec<(Variable, AnnotatedMark, Loc)>, + body_var: Variable, + body: Loc, +) -> Result<(Vec<'a, Variable>, Vec<'a, Symbol>, Loc), Loc> { + let mut arg_vars = Vec::with_capacity_in(patterns.len(), env.arena); + let mut symbols = Vec::with_capacity_in(patterns.len(), env.arena); + let mut body = Ok(body); + + // patterns that are not yet in a when (e.g. in let or function arguments) must be irrefutable + // to pass type checking. So the order in which we add them to the body does not matter: there + // are only stores anyway, no branches. + // + // NOTE this fails if the pattern contains rigid variables, + // see https://github.com/roc-lang/roc/issues/786 + // this must be fixed when moving exhaustiveness checking to the new canonical AST + for (pattern_var, annotated_mark, pattern) in patterns.into_iter() { + if annotated_mark.exhaustive.is_non_exhaustive(env.subs) { + // Even if the body was Ok, replace it with this Err. + // If it was already an Err, leave it at that Err, so the first + // RuntimeError we encountered remains the first. + let value = RuntimeError::UnsupportedPattern(pattern.region); + body = body.and({ + Err(Loc { + region: pattern.region, + value, + }) + }); + } else if let Ok(unwrapped_body) = body { + let (new_symbol, new_body) = + pattern_to_when(env, pattern_var, pattern, body_var, unwrapped_body); + + symbols.push(new_symbol); + arg_vars.push(pattern_var); + + body = Ok(new_body) + } + } + + match body { + Ok(body) => Ok((arg_vars, symbols, body)), + Err(loc_error) => Err(loc_error), + } +} + +/// turn irrefutable patterns into when. For example +/// +/// foo = \{ x } -> body +/// +/// Assuming the above program typechecks, the pattern match cannot fail +/// (it is irrefutable). It becomes +/// +/// foo = \r -> +/// when r is +/// { x } -> body +/// +/// conversion of one-pattern when expressions will do the most optimal thing +fn pattern_to_when( + env: &mut Env<'_, '_>, + pattern_var: Variable, + pattern: Loc, + body_var: Variable, + body: Loc, +) -> (Symbol, Loc) { + use roc_can::expr::Expr::*; + use roc_can::expr::{WhenBranch, WhenBranchPattern}; + use roc_can::pattern::Pattern::{self, *}; + + match &pattern.value { + Identifier(symbol) => (*symbol, body), + Underscore => { + // for underscore we generate a dummy Symbol + (env.unique_symbol(), body) + } + Shadowed(region, loc_ident, new_symbol) => { + let error = roc_problem::can::RuntimeError::Shadowing { + original_region: *region, + shadow: loc_ident.clone(), + kind: ShadowKind::Variable, + }; + (*new_symbol, Loc::at_zero(RuntimeError(error))) + } + + As(_, _) => todo!("as bindings are not supported yet"), + + UnsupportedPattern(region) => { + // create the runtime error here, instead of delegating to When. + // UnsupportedPattern should then never occur in When + let error = roc_problem::can::RuntimeError::UnsupportedPattern(*region); + (env.unique_symbol(), Loc::at_zero(RuntimeError(error))) + } + + MalformedPattern(problem, region) => { + // create the runtime error here, instead of delegating to When. + let error = roc_problem::can::RuntimeError::MalformedPattern(*problem, *region); + (env.unique_symbol(), Loc::at_zero(RuntimeError(error))) + } + + OpaqueNotInScope(loc_ident) => { + // create the runtime error here, instead of delegating to When. + // TODO(opaques) should be `RuntimeError::OpaqueNotDefined` + let error = roc_problem::can::RuntimeError::UnsupportedPattern(loc_ident.region); + (env.unique_symbol(), Loc::at_zero(RuntimeError(error))) + } + + AppliedTag { .. } + | RecordDestructure { .. } + | TupleDestructure { .. } + | UnwrappedOpaque { .. } => { + let symbol = env.unique_symbol(); + + let wrapped_body = When { + cond_var: pattern_var, + expr_var: body_var, + region: Region::zero(), + loc_cond: Box::new(Loc::at_zero(Var(symbol, pattern_var))), + branches: vec![WhenBranch { + patterns: vec![WhenBranchPattern { + pattern, + degenerate: false, + }], + value: body, + guard: None, + // If this type-checked, it's non-redundant + redundant: RedundantMark::known_non_redundant(), + }], + branches_cond_var: pattern_var, + // If this type-checked, it's exhaustive + exhaustive: ExhaustiveMark::known_exhaustive(), + }; + + (symbol, Loc::at_zero(wrapped_body)) + } + + Pattern::List { .. } => todo!(), + + IntLiteral(..) + | NumLiteral(..) + | FloatLiteral(..) + | StrLiteral(..) + | roc_can::pattern::Pattern::SingleQuote(..) => { + // These patters are refutable, and thus should never occur outside a `when` expression + // They should have been replaced with `UnsupportedPattern` during canonicalization + unreachable!("refutable pattern {:?} where irrefutable pattern is expected. This should never happen!", pattern.value) + } + + AbilityMemberSpecialization { .. } => { + unreachable!( + "Ability member specialization {:?} should never appear in a when!", + pattern.value + ) + } + } +} + +fn specialize_suspended<'a>( + env: &mut Env<'a, '_>, + procs: &mut Procs<'a>, + layout_cache: &mut LayoutCache<'a>, + suspended: Suspended<'a>, +) { + let offset_variable = StorageSubs::merge_into(suspended.store, env.subs); + + for (i, (symbol_or_lambda, var)) in suspended + .symbol_or_lambdas + .iter() + .zip(suspended.variables.iter()) + .enumerate() + { + let name = *symbol_or_lambda; + let outside_layout = suspended.layouts[i]; + + let var = offset_variable(*var); + + // TODO define our own Entry for Specialized? + let partial_proc = if procs + .specialized + .is_specialized(name.name(), &outside_layout) + { + // already specialized, just continue + continue; + } else { + match procs.partial_procs.symbol_to_id(name.name()) { + Some(v) => { + // Mark this proc as in-progress, so if we're dealing with + // mutually recursive functions, we don't loop forever. + // (We had a bug around this before this system existed!) + procs + .specialized + .mark_in_progress(name.name(), outside_layout); + + v + } + None => { + // TODO this assumes the specialization is done by another module + // make sure this does not become a problem down the road! + debug_assert!(name.name().module_id() != name.name().module_id()); + continue; + } + } + }; + + match specialize_variable(env, procs, name, layout_cache, var, partial_proc) { + Ok((proc, raw_layout)) => { + let proc_layout = ProcLayout::from_raw_named(env.arena, name, raw_layout); + procs + .specialized + .insert_specialized(name.name(), proc_layout, proc); + } + Err(SpecializeFailure { + attempted_layout, .. + }) => { + let proc = generate_runtime_error_function(env, name, attempted_layout); + + let top_level = ProcLayout::from_raw_named(env.arena, name, attempted_layout); + + procs + .specialized + .insert_specialized(name.name(), top_level, proc); + } + } + } +} + +pub fn specialize_all<'a>( + env: &mut Env<'a, '_>, + mut procs: Procs<'a>, + externals_others_need: std::vec::Vec>, + specializations_for_host: HostSpecializations<'a>, + layout_cache: &mut LayoutCache<'a>, +) -> Procs<'a> { + // When calling from_can, pending_specializations should be unavailable. + // This must be a single pass, and we must not add any more entries to it! + let pending_specializations = std::mem::replace( + &mut procs.pending_specializations, + PendingSpecializations::Making(Suspended::new_in(env.arena)), + ); + + // Add all of our existing pending specializations. + match pending_specializations { + PendingSpecializations::Finding(suspended) => { + specialize_suspended(env, &mut procs, layout_cache, suspended) + } + PendingSpecializations::Making(suspended) => { + debug_assert!( + suspended.is_empty(), + "suspended specializations cannot ever start off non-empty when making" + ); + } + } + + // Specialize all the symbols everyone else needs. + for externals in externals_others_need { + specialize_external_specializations(env, &mut procs, layout_cache, externals); + } + + // Specialize any symbols the host needs. + specialize_host_specializations(env, &mut procs, layout_cache, specializations_for_host); + + // Now, we must go through and continuously complete any new suspended specializations that were + // discovered in specializing the other demanded symbols. + while !procs.pending_specializations.is_empty() { + let pending_specializations = std::mem::replace( + &mut procs.pending_specializations, + PendingSpecializations::Making(Suspended::new_in(env.arena)), + ); + match pending_specializations { + PendingSpecializations::Making(suspended) => { + specialize_suspended(env, &mut procs, layout_cache, suspended); + } + PendingSpecializations::Finding(_) => { + internal_error!("should not have this variant after making specializations") + } + } + } + + debug_assert!( + procs.symbol_specializations.is_empty(), + "{:?}", + &procs.symbol_specializations + ); + + procs +} + +fn specialize_host_specializations<'a>( + env: &mut Env<'a, '_>, + procs: &mut Procs<'a>, + layout_cache: &mut LayoutCache<'a>, + host_specializations: HostSpecializations<'a>, +) { + let (store, it) = host_specializations.decompose(); + + let offset_variable = StorageSubs::merge_into(store, env.subs); + + for (lambda_name, from_app, opt_from_platform) in it { + let from_app = offset_variable(from_app); + let index = specialize_external_help(env, procs, layout_cache, lambda_name, from_app); + + let Some(from_platform) = opt_from_platform else { continue }; + + // now run the lambda set numbering scheme + let hels = find_lambda_sets(env.arena, env.subs, from_platform); + + // now unify + let mut unify_env = roc_unify::Env::new( + env.subs, + #[cfg(debug_assertions)] + None, + ); + + let unified = roc_unify::unify::unify( + &mut unify_env, + from_platform, + from_app, + roc_solve_schema::UnificationMode::EQ, + roc_types::types::Polarity::Pos, + ); + + { + use roc_unify::unify::Unified::*; + + match unified { + Success { .. } => { /* great */ } + Failure(..) => internal_error!("unification here should never fail"), + } + } + + for (var, id) in hels { + let symbol = env.unique_symbol(); + let lambda_name = LambdaName::no_niche(symbol); + + let mut layout_env = + layout::Env::from_components(layout_cache, env.subs, env.arena, env.target_info); + let lambda_set = env.subs.get_lambda_set(var); + let raw_function_layout = + RawFunctionLayout::from_var(&mut layout_env, lambda_set.ambient_function) + .value() + .unwrap(); + + let (key, (top_level, proc)) = generate_host_exposed_function( + env, + procs, + layout_cache, + lambda_name, + raw_function_layout, + ); + + procs + .specialized + .insert_specialized(symbol, top_level, proc); + + let hels = HostExposedLambdaSet { + id, + symbol, + proc_layout: top_level, + raw_function_layout, + }; + + let in_progress = &mut procs.specialized.procedures[index.0]; + let InProgressProc::Done(proc) = in_progress else { unreachable!() }; + + procs.host_exposed_lambda_sets.push((proc.name, key, hels)); + } + } +} + +fn specialize_external_specializations<'a>( + env: &mut Env<'a, '_>, + procs: &mut Procs<'a>, + layout_cache: &mut LayoutCache<'a>, + externals_others_need: ExternalSpecializations<'a>, +) { + let (store, it) = externals_others_need.decompose(); + + let offset_variable = StorageSubs::merge_into(store, env.subs); + + for (symbol, solved_types) in it { + for store_variable in solved_types { + let imported_variable = offset_variable(store_variable); + + roc_tracing::debug!(proc_name = ?symbol, ?store_variable, ?imported_variable, "specializing needed external"); + + // historical note: we used to deduplicate with a hash here, + // but the cost of that hash is very high. So for now we make + // duplicate specializations, and the insertion into a hash map + // below will deduplicate them. + + specialize_external_help(env, procs, layout_cache, symbol, imported_variable); + } + } +} + +fn specialize_external_help<'a>( + env: &mut Env<'a, '_>, + procs: &mut Procs<'a>, + layout_cache: &mut LayoutCache<'a>, + name: LambdaName<'a>, + variable: Variable, +) -> SpecializedIndex { + let partial_proc_id = match procs.partial_procs.symbol_to_id(name.name()) { + Some(v) => v, + None => { + panic!("Cannot find a partial proc for {name:?}"); + } + }; + + let specialization_result = + specialize_variable(env, procs, name, layout_cache, variable, partial_proc_id); + + match specialization_result { + Ok((proc, layout)) => { + let top_level = ProcLayout::from_raw_named(env.arena, name, layout); + + if procs.is_module_thunk(name.name()) { + debug_assert!(top_level.arguments.is_empty()); + } + + if procs.host_exposed_symbols.contains(&proc.name.name()) { + // layouts that are (transitively) used in the type of `mainForHost`. + let mut host_exposed_layouts: Vec<_> = top_level + .arguments + .iter() + .copied() + .chain([top_level.result]) + .collect_in(env.arena); + + // it is very likely we see the same types across functions, or in multiple arguments + host_exposed_layouts.sort(); + host_exposed_layouts.dedup(); + + // Computer the getter procs for every host-exposed layout. + for in_layout in host_exposed_layouts { + let layout = layout_cache.interner.get(in_layout); + + let all_glue_procs = generate_glue_procs( + env.home, + env.ident_ids, + env.arena, + &mut layout_cache.interner, + env.arena.alloc(layout), + ); + + let GlueProcs { + getters, + legacy_layout_based_extern_names: _, + } = all_glue_procs; + + for (_layout, glue_procs) in getters { + for glue_proc in glue_procs { + procs.specialized.insert_specialized( + glue_proc.proc.name.name(), + glue_proc.proc_layout, + glue_proc.proc, + ); + } + } + } + } + + procs + .specialized + .insert_specialized(name.name(), top_level, proc) + } + Err(SpecializeFailure { attempted_layout }) => { + let proc = generate_runtime_error_function(env, name, attempted_layout); + + let top_level = ProcLayout::from_raw_named(env.arena, name, attempted_layout); + + procs + .specialized + .insert_specialized(name.name(), top_level, proc) + } + } +} + +fn generate_runtime_error_function<'a>( + env: &mut Env<'a, '_>, + lambda_name: LambdaName<'a>, + layout: RawFunctionLayout<'a>, +) -> Proc<'a> { + let mut msg = bumpalo::collections::string::String::with_capacity_in(80, env.arena); + use std::fmt::Write; + write!( + &mut msg, + "The {:?} function could not be generated, likely due to a type error.", + lambda_name.name(), + ) + .unwrap(); + + dbg_do!(ROC_PRINT_RUNTIME_ERROR_GEN, { + eprintln!( + "emitted runtime error function {:?} for layout {:?}", + &msg, layout + ); + }); + + let runtime_error = runtime_error(env, msg.into_bump_str()); + + let is_erased = layout.is_erased_function(); + let (args, ret_layout) = match layout { + RawFunctionLayout::Function(arg_layouts, lambda_set, ret_layout) => { + let real_arg_layouts = + lambda_set.extend_argument_list_for_named(env.arena, lambda_name, arg_layouts); + let mut args = Vec::with_capacity_in(real_arg_layouts.len(), env.arena); + + for arg in arg_layouts { + args.push((*arg, env.unique_symbol())); + } + if real_arg_layouts.len() != arg_layouts.len() { + let lambda_set_layout = lambda_set.full_layout; + args.push((lambda_set_layout, Symbol::ARG_CLOSURE)); + } + + (args.into_bump_slice(), ret_layout) + } + RawFunctionLayout::ErasedFunction(..) => { + todo_lambda_erasure!() + } + RawFunctionLayout::ZeroArgumentThunk(ret_layout) => (&[] as &[_], ret_layout), + }; + + Proc { + name: lambda_name, + args, + body: runtime_error, + closure_data_layout: None, + ret_layout, + is_self_recursive: SelfRecursive::NotSelfRecursive, + is_erased, + } +} + +/// A snapshot of the state of types at a moment in time. +/// Includes the exact types, but also auxiliary information like layouts. +struct TypeStateSnapshot { + subs_snapshot: roc_types::subs::SubsSnapshot, + layout_snapshot: crate::layout::CacheSnapshot, + external_storage_snapshot: VecMap, +} + +/// Takes a snapshot of the type state. Snapshots should be taken before new specializations, and +/// accordingly [rolled back][rollback_typestate] a specialization is complete, so as to not +/// interfere with other specializations. +fn snapshot_typestate( + subs: &mut Subs, + procs: &mut Procs, + layout_cache: &mut LayoutCache<'_>, +) -> TypeStateSnapshot { + TypeStateSnapshot { + subs_snapshot: subs.snapshot(), + layout_snapshot: layout_cache.snapshot(), + external_storage_snapshot: procs + .externals_we_need + .iter_mut() + .map(|(module, es)| (*module, es.snapshot_cache())) + .collect(), + } +} + +/// Rolls back the type state to the given [snapshot]. +/// Should be called after a specialization is complete to avoid interfering with other +/// specializations. +fn rollback_typestate( + subs: &mut Subs, + procs: &mut Procs, + layout_cache: &mut LayoutCache<'_>, + snapshot: TypeStateSnapshot, +) { + let TypeStateSnapshot { + subs_snapshot, + layout_snapshot, + mut external_storage_snapshot, + } = snapshot; + + subs.rollback_to(subs_snapshot); + layout_cache.rollback_to(layout_snapshot); + + for (module, es) in procs.externals_we_need.iter_mut() { + if let Some((_, snapshot)) = external_storage_snapshot.remove(module) { + es.rollback_cache(snapshot); + } else { + es.invalidate_whole_cache(); + } + } +} + +fn generate_host_exposed_function<'a>( + env: &mut Env<'a, '_>, + procs: &mut Procs<'a>, + layout_cache: &mut LayoutCache<'a>, + lambda_name: LambdaName<'a>, + layout: RawFunctionLayout<'a>, +) -> (Symbol, (ProcLayout<'a>, Proc<'a>)) { + let function_name = lambda_name.name(); + + match layout { + RawFunctionLayout::Function(_, lambda_set, _) => { + let (proc, top_level) = generate_host_exposed_lambda_set( + env, + procs, + layout_cache, + function_name, + lambda_set, + ); + + (function_name, (top_level, proc)) + } + RawFunctionLayout::ErasedFunction(..) => { + todo_lambda_erasure!() + } + RawFunctionLayout::ZeroArgumentThunk(result) => { + let assigned = env.unique_symbol(); + let hole = env.arena.alloc(Stmt::Ret(assigned)); + let forced = force_thunk(env, function_name, result, assigned, hole); + + let lambda_name = LambdaName::no_niche(function_name); + let proc = Proc { + name: lambda_name, + args: &[], + body: forced, + closure_data_layout: None, + ret_layout: result, + is_self_recursive: SelfRecursive::NotSelfRecursive, + is_erased: false, + }; + + let top_level = ProcLayout::from_raw_named(env.arena, lambda_name, layout); + + (function_name, (top_level, proc)) + } + } +} + +fn generate_host_exposed_lambda_set<'a>( + env: &mut Env<'a, '_>, + procs: &mut Procs<'a>, + layout_cache: &mut LayoutCache<'a>, + name: Symbol, + lambda_set: LambdaSet<'a>, +) -> (Proc<'a>, ProcLayout<'a>) { + let assigned = env.unique_symbol(); + + let argument_layouts = *lambda_set.args; + let return_layout = lambda_set.ret; + + let mut argument_symbols = Vec::with_capacity_in(argument_layouts.len(), env.arena); + let mut proc_arguments = Vec::with_capacity_in(argument_layouts.len() + 1, env.arena); + let mut top_level_arguments = Vec::with_capacity_in(argument_layouts.len() + 1, env.arena); + + for layout in *lambda_set.args { + let symbol = env.unique_symbol(); + + proc_arguments.push((*layout, symbol)); + + argument_symbols.push(symbol); + top_level_arguments.push(*layout); + } + + // the proc needs to take an extra closure argument + let lambda_set_layout = lambda_set.full_layout; + proc_arguments.push((lambda_set_layout, Symbol::ARG_CLOSURE)); + + // this should also be reflected in the TopLevel signature + top_level_arguments.push(lambda_set_layout); + + let hole = env.arena.alloc(Stmt::Ret(assigned)); + + let body = match_on_lambda_set( + env, + layout_cache, + procs, + lambda_set, + Symbol::ARG_CLOSURE, + argument_symbols.into_bump_slice(), + argument_layouts, + return_layout, + assigned, + hole, + ); + + let proc = Proc { + name: LambdaName::no_niche(name), + args: proc_arguments.into_bump_slice(), + body, + closure_data_layout: None, + ret_layout: return_layout, + is_self_recursive: SelfRecursive::NotSelfRecursive, + is_erased: false, + }; + + let top_level = ProcLayout::new( + env.arena, + top_level_arguments.into_bump_slice(), + Niche::NONE, + return_layout, + ); + + (proc, top_level) +} + +/// Specialize a single proc. +/// +/// The caller should snapshot and rollback the type state before and after calling this function, +/// respectively. This function will not take snapshots itself, but will modify the type state. +fn specialize_proc_help<'a>( + env: &mut Env<'a, '_>, + procs: &mut Procs<'a>, + lambda_name: LambdaName<'a>, + layout_cache: &mut LayoutCache<'a>, + fn_var: Variable, + partial_proc_id: PartialProcId, +) -> Result, LayoutProblem> { + let partial_proc = procs.partial_procs.get_id(partial_proc_id); + let captured_symbols = partial_proc.captured_symbols; + + let _unified = env.unify( + procs.externals_we_need.values_mut(), + layout_cache, + partial_proc.annotation, + fn_var, + ); + + // This will not hold for programs with type errors + // let is_valid = matches!(unified, roc_unify::unify::Unified::Success(_)); + // debug_assert!(is_valid, "unificaton failure for {:?}", proc_name); + + // if this is a closure, add the closure record argument + let pattern_symbols = match partial_proc.captured_symbols { + CapturedSymbols::None => partial_proc.pattern_symbols, + CapturedSymbols::Captured([]) => partial_proc.pattern_symbols, + CapturedSymbols::Captured(_) => { + let mut temp = + Vec::from_iter_in(partial_proc.pattern_symbols.iter().copied(), env.arena); + temp.push(Symbol::ARG_CLOSURE); + temp.into_bump_slice() + } + }; + + let specialized = + build_specialized_proc_from_var(env, layout_cache, lambda_name, pattern_symbols, fn_var)?; + + let recursivity = if partial_proc.is_self_recursive { + SelfRecursive::SelfRecursive(JoinPointId(env.unique_symbol())) + } else { + SelfRecursive::NotSelfRecursive + }; + + let body = partial_proc.body.clone(); + let body_var = partial_proc.body_var; + + let mut specialized_body = from_can(env, body_var, body, procs, layout_cache); + + let specialized_proc = match specialized { + SpecializedLayout::FunctionPointerBody { + ret_layout, + closure: opt_closure_layout, + is_erased, + } => { + // this is a function body like + // + // foo = Num.add + // + // we need to expand this to + // + // foo = \x,y -> Num.add x y + + let closure_data_layout = match opt_closure_layout { + Some(lambda_set) => lambda_set.full_layout, + None => Layout::UNIT, + }; + + // I'm not sure how to handle the closure case, does it ever occur? + debug_assert!(matches!(captured_symbols, CapturedSymbols::None)); + + Proc { + name: lambda_name, + args: &[], + body: specialized_body, + closure_data_layout: Some(closure_data_layout), + ret_layout, + is_self_recursive: recursivity, + is_erased, + } + } + SpecializedLayout::FunctionBody { + arguments: proc_args, + closure: opt_closure_layout, + ret_layout, + is_erased, + } => { + let mut proc_args = Vec::from_iter_in(proc_args.iter().copied(), env.arena); + + // unpack the closure symbols, if any + match (opt_closure_layout, captured_symbols) { + ( + Some(ClosureDataKind::LambdaSet(closure_layout)), + CapturedSymbols::Captured(captured), + ) => { + // debug_assert!(!captured.is_empty()); + + // An argument from the closure list may have taken on a specialized symbol + // name during the evaluation of the def body. If this is the case, load the + // specialized name rather than the original captured name! + let get_specialized_name = |symbol| { + let specs_used_in_body = + procs.get_symbol_specializations_used_in_body(symbol); + + match specs_used_in_body { + Some(mut specs) => { + let spec_symbol = specs.next().unwrap_or(symbol); + if specs.next().is_some() { + internal_error!( + "polymorphic symbol captures not supported yet" + ); + } + spec_symbol + } + None => symbol, + } + }; + + match closure_layout + .layout_for_member_with_lambda_name(&layout_cache.interner, lambda_name) + { + ClosureRepresentation::Union { + alphabetic_order_fields: field_layouts, + union_layout, + tag_id, + .. + } => { + debug_assert!(matches!( + union_layout, + UnionLayout::NonRecursive(_) + | UnionLayout::Recursive(_) + | UnionLayout::NullableUnwrapped { .. } + | UnionLayout::NullableWrapped { .. } + )); + debug_assert_eq!(field_layouts.len(), captured.len()); + + // captured variables are in symbol-alphabetic order, but now we want + // them ordered by their alignment requirements + let mut combined = Vec::from_iter_in( + captured.iter().map(|(x, _)| x).zip(field_layouts.iter()), + env.arena, + ); + + combined.sort_by(|(_, layout1), (_, layout2)| { + let size1 = layout_cache + .get_repr(**layout1) + .alignment_bytes(&layout_cache.interner); + let size2 = layout_cache + .get_repr(**layout2) + .alignment_bytes(&layout_cache.interner); + + size2.cmp(&size1) + }); + + for (index, (symbol, _)) in combined.iter().enumerate() { + let layout = union_layout.layout_at( + &mut layout_cache.interner, + tag_id, + index, + ); + + let expr = Expr::UnionAtIndex { + tag_id, + structure: Symbol::ARG_CLOSURE, + index: index as u64, + union_layout, + }; + + let symbol = get_specialized_name(**symbol); + + let fresh_symbol = + env.named_unique_symbol(&format!("{:?}_closure", symbol)); + + specialized_body = Stmt::Let( + fresh_symbol, + expr, + layout, + env.arena.alloc(specialized_body), + ); + + // the same symbol may be used where + // - the closure is created + // - the closure is consumed + substitute_in_exprs( + env.arena, + &mut specialized_body, + symbol, + fresh_symbol, + ); + } + } + ClosureRepresentation::AlphabeticOrderStruct(field_layouts) => { + // captured variables are in symbol-alphabetic order, but now we want + // them ordered by their alignment requirements + // + // TODO: sort only the fields and apply the found permutation to the symbols + // TODO: can we move this ordering to `layout_for_member`? + let mut combined = Vec::from_iter_in( + captured.iter().map(|(x, _)| x).zip(field_layouts.iter()), + env.arena, + ); + + combined.sort_by(|(_, layout1), (_, layout2)| { + let size1 = layout_cache + .get_repr(**layout1) + .alignment_bytes(&layout_cache.interner); + let size2 = layout_cache + .get_repr(**layout2) + .alignment_bytes(&layout_cache.interner); + + size2.cmp(&size1) + }); + + let ordered_field_layouts = Vec::from_iter_in( + combined.iter().map(|(_, layout)| **layout), + env.arena, + ); + let ordered_field_layouts = ordered_field_layouts.into_bump_slice(); + + debug_assert_eq!( + captured.len(), + ordered_field_layouts.len(), + "{:?} captures {:?} but has layout {:?}", + lambda_name, + &captured, + &ordered_field_layouts + ); + + for (index, (symbol, layout)) in combined.iter().enumerate() { + let expr = Expr::StructAtIndex { + index: index as _, + field_layouts: ordered_field_layouts, + structure: Symbol::ARG_CLOSURE, + }; + + let symbol = get_specialized_name(**symbol); + + specialized_body = Stmt::Let( + symbol, + expr, + **layout, + env.arena.alloc(specialized_body), + ); + } + } + + ClosureRepresentation::UnwrappedCapture(_layout) => { + debug_assert_eq!(captured.len(), 1); + let (captured_symbol, _captured_layout) = captured[0]; + + // The capture set is unwrapped, so simply replace the closure argument + // to the function with the unwrapped capture name. + let captured_symbol = get_specialized_name(captured_symbol); + let closure_arg = proc_args.last_mut().unwrap(); + debug_assert_eq!(closure_arg.1, Symbol::ARG_CLOSURE); + closure_arg.1 = captured_symbol; + } + + ClosureRepresentation::EnumDispatch(_) => { + // just ignore this value, since it's not a capture + // IDEA don't pass this value in the future + } + } + } + (Some(ClosureDataKind::Erased), CapturedSymbols::Captured(captured)) => { + specialized_body = erased::unpack_closure_data( + env, + layout_cache, + Symbol::ARG_CLOSURE, + captured, + specialized_body, + ); + } + (None, CapturedSymbols::None) | (None, CapturedSymbols::Captured([])) => {} + _ => unreachable!("to closure or not to closure?"), + } + + proc_args.iter_mut().for_each(|(layout, symbol)| { + // Grab the specialization symbol, if it exists. + *symbol = procs + .symbol_specializations + .maybe_get_specialized(*symbol, *layout) + }); + + let closure_data_layout = opt_closure_layout.map(|clos| clos.data_layout()); + + Proc { + name: lambda_name, + args: proc_args.into_bump_slice(), + body: specialized_body, + closure_data_layout, + ret_layout, + is_self_recursive: recursivity, + is_erased, + } + } + }; + + Ok(specialized_proc) +} + +#[derive(Debug)] +enum SpecializedLayout<'a> { + /// A body like `foo = \a,b,c -> ...` + FunctionBody { + arguments: &'a [(InLayout<'a>, Symbol)], + closure: Option>, + ret_layout: InLayout<'a>, + is_erased: bool, + }, + /// A body like `foo = Num.add` + FunctionPointerBody { + closure: Option>, + ret_layout: InLayout<'a>, + is_erased: bool, + }, +} + +#[allow(clippy::type_complexity)] +fn build_specialized_proc_from_var<'a>( + env: &mut Env<'a, '_>, + layout_cache: &mut LayoutCache<'a>, + lambda_name: LambdaName<'a>, + pattern_symbols: &[Symbol], + fn_var: Variable, +) -> Result, LayoutProblem> { + match layout_cache.raw_from_var(env.arena, fn_var, env.subs)? { + RawFunctionLayout::Function(pattern_layouts, closure_layout, ret_layout) => { + let mut pattern_layouts_vec = Vec::with_capacity_in(pattern_layouts.len(), env.arena); + pattern_layouts_vec.extend_from_slice(pattern_layouts); + + build_specialized_proc( + env.arena, + lambda_name, + pattern_symbols, + pattern_layouts_vec, + Some(ClosureDataKind::LambdaSet(closure_layout)), + ret_layout, + ) + } + RawFunctionLayout::ErasedFunction(pattern_layouts, ret_layout) => { + let mut pattern_layouts_vec = Vec::with_capacity_in(pattern_layouts.len(), env.arena); + pattern_layouts_vec.extend_from_slice(pattern_layouts); + + build_specialized_proc( + env.arena, + lambda_name, + pattern_symbols, + pattern_layouts_vec, + Some(ClosureDataKind::Erased), + ret_layout, + ) + } + RawFunctionLayout::ZeroArgumentThunk(ret_layout) => { + // a top-level constant 0-argument thunk + build_specialized_proc( + env.arena, + lambda_name, + pattern_symbols, + Vec::new_in(env.arena), + None, + ret_layout, + ) + } + } +} + +#[allow(clippy::type_complexity)] +fn build_specialized_proc<'a>( + arena: &'a Bump, + lambda_name: LambdaName<'a>, + pattern_symbols: &[Symbol], + pattern_layouts: Vec<'a, InLayout<'a>>, + closure_data: Option>, + ret_layout: InLayout<'a>, +) -> Result, LayoutProblem> { + use SpecializedLayout::*; + + let mut proc_args = Vec::with_capacity_in(pattern_layouts.len(), arena); + + let pattern_layouts_len = pattern_layouts.len(); + + for (arg_layout, arg_name) in pattern_layouts.into_iter().zip(pattern_symbols.iter()) { + proc_args.push((arg_layout, *arg_name)); + } + + let is_erased = matches!(closure_data, Some(ClosureDataKind::Erased)); + + // Given + // + // foo = + // x = 42 + // + // f = \{} -> x + // + // We desugar that into + // + // f = \{}, x -> x + // + // foo = + // x = 42 + // + // f_closure = { ptr: f, closure: x } + // + // then + + let proc_name = lambda_name.name(); + match closure_data { + Some(closure_data) if pattern_symbols.last() == Some(&Symbol::ARG_CLOSURE) => { + // here we define the lifted (now top-level) f function. Its final argument is `Symbol::ARG_CLOSURE`, + // it stores the closure structure (just an integer in this case) + let closure_data_layout = closure_data.data_layout(); + proc_args.push((closure_data_layout, Symbol::ARG_CLOSURE)); + + debug_assert_eq!( + pattern_layouts_len + 1, + pattern_symbols.len(), + "Tried to zip two vecs with different lengths in {proc_name:?}!", + ); + + let proc_args = proc_args.into_bump_slice(); + + Ok(FunctionBody { + arguments: proc_args, + closure: Some(closure_data), + ret_layout, + is_erased, + }) + } + Some(closure_data) => { + // a function that returns a function, but is not itself a closure + // e.g. f = Num.add + + // make sure there is not arg_closure argument without a closure layout + debug_assert!(pattern_symbols.last() != Some(&Symbol::ARG_CLOSURE)); + + use std::cmp::Ordering; + match pattern_layouts_len.cmp(&pattern_symbols.len()) { + Ordering::Equal => { + let proc_args = proc_args.into_bump_slice(); + + Ok(FunctionBody { + arguments: proc_args, + closure: None, + ret_layout, + is_erased, + }) + } + Ordering::Greater => { + if pattern_symbols.is_empty() { + let ret_layout = closure_data.data_layout(); + Ok(FunctionPointerBody { + closure: None, + ret_layout, + is_erased, + }) + } else { + // so far, the problem when hitting this branch was always somewhere else + // I think this branch should not be reachable in a bugfree compiler + panic!( + "more arguments (according to the layout) than argument symbols for {proc_name:?}" + ) + } + } + Ordering::Less => panic!( + "more argument symbols than arguments (according to the layout) for {proc_name:?}" + ), + } + } + None => { + // else we're making a normal function, no closure problems to worry about + // we'll just assert some things + + // make sure there is not arg_closure argument without a closure layout + debug_assert!(pattern_symbols.last() != Some(&Symbol::ARG_CLOSURE)); + + use std::cmp::Ordering; + match pattern_layouts_len.cmp(&pattern_symbols.len()) { + Ordering::Equal => { + let proc_args = proc_args.into_bump_slice(); + + Ok(FunctionBody { + arguments: proc_args, + closure: None, + ret_layout, + is_erased, + }) + } + Ordering::Greater => { + if pattern_symbols.is_empty() { + Ok(FunctionPointerBody { + closure: None, + ret_layout, + is_erased, + }) + } else { + // so far, the problem when hitting this branch was always somewhere else + // I think this branch should not be reachable in a bugfree compiler + panic!( + "more arguments (according to the layout) than argument symbols for {proc_name:?}" + ) + } + } + Ordering::Less => panic!( + "more argument symbols than arguments (according to the layout) for {proc_name:?}. Pattern symbols: {:?}\n\nPattern layouts: {:?}", pattern_symbols, pattern_layouts_len, + ), + } + } + } +} + +#[derive(Debug)] +struct SpecializeFailure<'a> { + /// The layout we attempted to create + attempted_layout: RawFunctionLayout<'a>, +} + +type SpecializeSuccess<'a> = (Proc<'a>, RawFunctionLayout<'a>); + +fn specialize_variable<'a>( + env: &mut Env<'a, '_>, + procs: &mut Procs<'a>, + proc_name: LambdaName<'a>, + layout_cache: &mut LayoutCache<'a>, + fn_var: Variable, + partial_proc_id: PartialProcId, +) -> Result, SpecializeFailure<'a>> { + let snapshot = snapshot_typestate(env.subs, procs, layout_cache); + + // for debugging only + // TODO: can we get rid of raw entirely? + let raw = layout_cache + .raw_from_var(env.arena, fn_var, env.subs) + .unwrap_or_else(|err| panic!("TODO handle invalid function {err:?}")); + + let raw = if procs.is_module_thunk(proc_name.name()) { + match raw { + RawFunctionLayout::Function(_, lambda_set, _) => { + let lambda_set_layout = lambda_set.full_layout; + RawFunctionLayout::ZeroArgumentThunk(lambda_set_layout) + } + _ => raw, + } + } else { + raw + }; + + // make sure rigid variables in the annotation are converted to flex variables + let annotation_var = procs.partial_procs.get_id(partial_proc_id).annotation; + instantiate_rigids(env.subs, annotation_var); + + procs.push_active_specialization(proc_name.name()); + roc_tracing::debug!(?proc_name, ?fn_var, fn_content = ?roc_types::subs::SubsFmtContent(env.subs.get_content_without_compacting(fn_var), env.subs), "specialization start"); + + let specialized = + specialize_proc_help(env, procs, proc_name, layout_cache, fn_var, partial_proc_id); + + roc_tracing::debug!( + ?proc_name, + succeeded = specialized.is_ok(), + "specialization end" + ); + procs.pop_active_specialization(proc_name.name()); + + let result = match specialized { + Ok(proc) => { + // when successful, the layout after unification should be the layout before unification + // debug_assert_eq!( + // attempted_layout, + // layout_cache + // .from_var(env.arena, fn_var, env.subs) + // .unwrap_or_else(|err| panic!("TODO handle invalid function {:?}", err)) + // ); + + Ok((proc, raw)) + } + Err(error) => { + // earlier we made this information available where we handle the failure + // but we didn't do anything useful with it. So it's here if we ever need it again + let _ = error; + + Err(SpecializeFailure { + attempted_layout: raw, + }) + } + }; + + rollback_typestate(env.subs, procs, layout_cache, snapshot); + + result +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub struct ProcLayout<'a> { + pub arguments: &'a [InLayout<'a>], + pub result: InLayout<'a>, + pub niche: Niche<'a>, +} + +impl<'a> ProcLayout<'a> { + pub(crate) fn new( + arena: &'a Bump, + old_arguments: &'a [InLayout<'a>], + old_niche: Niche<'a>, + result: InLayout<'a>, + ) -> Self { + let mut arguments = Vec::with_capacity_in(old_arguments.len(), arena); + + for old in old_arguments { + let other = old; + arguments.push(*other); + } + + let other = result; + let new_result = other; + + ProcLayout { + arguments: arguments.into_bump_slice(), + niche: old_niche, + result: new_result, + } + } + + fn from_raw_named( + arena: &'a Bump, + lambda_name: LambdaName<'a>, + raw: RawFunctionLayout<'a>, + ) -> Self { + match raw { + RawFunctionLayout::Function(arguments, lambda_set, result) => { + let arguments = + lambda_set.extend_argument_list_for_named(arena, lambda_name, arguments); + ProcLayout::new(arena, arguments, lambda_name.niche(), result) + } + RawFunctionLayout::ErasedFunction(arguments, result) => { + let arguments = if lambda_name.no_captures() { + arguments + } else { + let mut extended_args = Vec::with_capacity_in(arguments.len(), arena); + extended_args.extend(arguments.iter().chain(&[Layout::ERASED]).copied()); + extended_args.into_bump_slice() + }; + + ProcLayout::new(arena, arguments, lambda_name.niche(), result) + } + RawFunctionLayout::ZeroArgumentThunk(result) => { + ProcLayout::new(arena, &[], Niche::NONE, result) + } + } + } + + pub fn dbg_deep<'r, I: LayoutInterner<'a>>(&self, interner: &'r I) -> DbgProcLayout<'a, 'r, I> { + DbgProcLayout { + layout: *self, + interner, + } + } +} + +pub struct DbgProcLayout<'a, 'r, I: LayoutInterner<'a>> { + layout: ProcLayout<'a>, + interner: &'r I, +} + +impl<'a, 'r, I: LayoutInterner<'a>> std::fmt::Debug for DbgProcLayout<'a, 'r, I> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let ProcLayout { + arguments, + result, + niche, + } = self.layout; + f.debug_struct("ProcLayout") + .field("arguments", &self.interner.dbg_deep_iter(arguments)) + .field("result", &self.interner.dbg_deep(result)) + .field("niche", &niche.dbg_deep(self.interner)) + .finish() + } +} + +fn specialize_naked_symbol<'a>( + env: &mut Env<'a, '_>, + variable: Variable, + procs: &mut Procs<'a>, + layout_cache: &mut LayoutCache<'a>, + assigned: Symbol, + hole: &'a Stmt<'a>, + symbol: Symbol, +) -> Stmt<'a> { + if procs.is_module_thunk(symbol) { + let fn_var = variable; + + // This is a top-level declaration, which will code gen to a 0-arity thunk. + let result = call_by_name( + env, + procs, + fn_var, + symbol, + std::vec::Vec::new(), + layout_cache, + assigned, + hole, + ); + + return result; + } else if env.is_imported_symbol(symbol) { + match layout_cache.from_var(env.arena, variable, env.subs) { + Err(e) => panic!("invalid layout {e:?}"), + Ok(_) => { + // this is a 0-arity thunk + let result = call_by_name( + env, + procs, + variable, + symbol, + std::vec::Vec::new(), + layout_cache, + assigned, + hole, + ); + + return result; + } + } + } + + // if the symbol is a function symbol, ensure it is properly specialized! + let original = symbol; + + let opt_fn_var = Some(variable); + + // if this is a function symbol, ensure that it's properly specialized! + specialize_symbol( + env, + procs, + layout_cache, + opt_fn_var, + assigned, + hole, + original, + ) +} + +fn try_make_literal<'a>( + interner: &TLLayoutInterner<'a>, + can_expr: &roc_can::expr::Expr, + layout: InLayout<'a>, +) -> Option> { + use roc_can::expr::Expr::*; + + match can_expr { + Int(_, _, int_str, int, _bound) => Some( + make_num_literal(interner, layout, int_str, IntOrFloatValue::Int(*int)) + .to_expr_literal(), + ), + + Float(_, _, float_str, float, _bound) => Some( + make_num_literal(interner, layout, float_str, IntOrFloatValue::Float(*float)) + .to_expr_literal(), + ), + + // TODO investigate lifetime trouble + // Str(string) => Some(Literal::Str(env.arena.alloc(string))), + Num(_, num_str, num, _bound) => Some( + make_num_literal(interner, layout, num_str, IntOrFloatValue::Int(*num)) + .to_expr_literal(), + ), + _ => None, + } +} + +pub fn with_hole<'a>( + env: &mut Env<'a, '_>, + can_expr: roc_can::expr::Expr, + variable: Variable, + procs: &mut Procs<'a>, + layout_cache: &mut LayoutCache<'a>, + assigned: Symbol, + hole: &'a Stmt<'a>, +) -> Stmt<'a> { + use roc_can::expr::Expr::*; + + let arena = env.arena; + + match can_expr { + Int(_, _, int_str, int, _bound) => { + match assign_num_literal_expr( + env, + layout_cache, + assigned, + variable, + &int_str, + IntOrFloatValue::Int(int), + hole, + ) { + Ok(stmt) => stmt, + Err(_) => hole.clone(), + } + } + + Float(_, _, float_str, float, _bound) => { + match assign_num_literal_expr( + env, + layout_cache, + assigned, + variable, + &float_str, + IntOrFloatValue::Float(float), + hole, + ) { + Ok(stmt) => stmt, + Err(_) => hole.clone(), + } + } + + Num(_, num_str, num, _bound) => { + match assign_num_literal_expr( + env, + layout_cache, + assigned, + variable, + &num_str, + IntOrFloatValue::Int(num), + hole, + ) { + Ok(stmt) => stmt, + Err(_) => hole.clone(), + } + } + + Str(string) => Stmt::Let( + assigned, + Expr::Literal(Literal::Str(arena.alloc(string))), + Layout::STR, + hole, + ), + + IngestedFile(_, bytes, var) => { + let interned = layout_cache.from_var(env.arena, var, env.subs).unwrap(); + let layout = layout_cache.get_repr(interned); + + match layout { + LayoutRepr::Builtin(Builtin::List(elem_layout)) if elem_layout == Layout::U8 => { + let mut elements = Vec::with_capacity_in(bytes.len(), env.arena); + for byte in bytes.iter() { + elements.push(ListLiteralElement::Literal(Literal::Byte(*byte))); + } + let expr = Expr::Array { + elem_layout, + elems: elements.into_bump_slice(), + }; + + Stmt::Let(assigned, expr, interned, hole) + } + LayoutRepr::Builtin(Builtin::Str) => Stmt::Let( + assigned, + Expr::Literal(Literal::Str( + // This is safe because we ensure the utf8 bytes are valid earlier in the compiler pipeline. + arena.alloc( + unsafe { std::str::from_utf8_unchecked(bytes.as_ref()) }.to_owned(), + ), + )), + Layout::STR, + hole, + ), + _ => { + // This will not manifest as a real runtime error and is just returned to have a value here. + // The actual type error during solve will be fatal. + runtime_error(env, "Invalid type for ingested file") + } + } + } + SingleQuote(_, _, character, _) => { + let layout = layout_cache + .from_var(env.arena, variable, env.subs) + .unwrap(); + + Stmt::Let( + assigned, + Expr::Literal(Literal::Int((character as i128).to_ne_bytes())), + layout, + hole, + ) + } + LetNonRec(def, cont) => from_can_let( + env, + procs, + layout_cache, + def, + cont, + variable, + Some((assigned, hole)), + ), + LetRec(defs, cont, _cycle_mark) => { + // because Roc is strict, only functions can be recursive! + for def in defs.into_iter() { + if let roc_can::pattern::Pattern::Identifier(symbol) = &def.loc_pattern.value { + if let Closure(closure_data) = def.loc_expr.value { + register_noncapturing_closure(env, procs, *symbol, closure_data); + + continue; + } + } + unreachable!("recursive value does not have Identifier pattern") + } + + with_hole( + env, + cont.value, + variable, + procs, + layout_cache, + assigned, + hole, + ) + } + Var(mut symbol, _) => { + // If this symbol is a raw value, find the real name we gave to its specialized usage. + if let ReuseSymbol::Value(_symbol) = can_reuse_symbol( + env, + layout_cache, + procs, + &roc_can::expr::Expr::Var(symbol, variable), + variable, + ) { + let real_symbol = + procs.get_or_insert_symbol_specialization(env, layout_cache, symbol, variable); + symbol = real_symbol; + } + + specialize_naked_symbol(env, variable, procs, layout_cache, assigned, hole, symbol) + } + AbilityMember(member, specialization_id, specialization_var) => { + let specialization_symbol = late_resolve_ability_specialization( + env, + member, + specialization_id, + specialization_var, + ); + + specialize_naked_symbol( + env, + variable, + procs, + layout_cache, + assigned, + hole, + specialization_symbol, + ) + } + Tag { + tag_union_var: variant_var, + name: tag_name, + arguments: args, + .. + } => { + let arena = env.arena; + + debug_assert!(!matches!( + env.subs.get_content_without_compacting(variant_var), + Content::Structure(FlatType::Func(_, _, _)) + )); + convert_tag_union( + env, + variant_var, + assigned, + hole, + tag_name, + procs, + layout_cache, + args, + arena, + ) + } + + ZeroArgumentTag { + variant_var: _, + name: tag_name, + ext_var, + closure_name, + } => { + let arena = env.arena; + + let content = env.subs.get_content_without_compacting(variable); + + if let Content::Structure(FlatType::Func(arg_vars, _, ret_var)) = content { + let ret_var = *ret_var; + let arg_vars = *arg_vars; + + tag_union_to_function( + env, + arg_vars, + ret_var, + tag_name, + closure_name, + ext_var, + procs, + variable, + layout_cache, + assigned, + hole, + ) + } else { + convert_tag_union( + env, + variable, + assigned, + hole, + tag_name, + procs, + layout_cache, + std::vec::Vec::new(), + arena, + ) + } + } + + OpaqueRef { argument, .. } => { + let (arg_var, loc_arg_expr) = *argument; + + match can_reuse_symbol(env, layout_cache, procs, &loc_arg_expr.value, arg_var) { + // Opaques decay to their argument. + ReuseSymbol::Value(symbol) => { + let real_name = procs.get_or_insert_symbol_specialization( + env, + layout_cache, + symbol, + arg_var, + ); + let mut result = hole.clone(); + substitute_in_exprs(arena, &mut result, assigned, real_name); + result + } + _ => with_hole( + env, + loc_arg_expr.value, + arg_var, + procs, + layout_cache, + assigned, + hole, + ), + } + } + + Tuple { + tuple_var, elems, .. + } => { + let sorted_elems_result = { + let mut layout_env = layout::Env::from_components( + layout_cache, + env.subs, + env.arena, + env.target_info, + ); + layout::sort_tuple_elems(&mut layout_env, tuple_var) + }; + let sorted_elems = match sorted_elems_result { + Ok(elems) => elems, + Err(_) => return runtime_error(env, "Can't create tuple with improper layout"), + }; + + // Hacky way to let us remove the owned elements from the vector, possibly out-of-order. + let mut elems = Vec::from_iter_in(elems.into_iter().map(Some), env.arena); + let take_elem_expr = move |index: usize| elems[index].take(); + + compile_struct_like( + env, + procs, + layout_cache, + sorted_elems, + take_elem_expr, + tuple_var, + hole, + assigned, + ) + } + + Record { + record_var, + mut fields, + .. + } => { + let sorted_fields_result = { + let mut layout_env = layout::Env::from_components( + layout_cache, + env.subs, + env.arena, + env.target_info, + ); + layout::sort_record_fields(&mut layout_env, record_var) + }; + let sorted_fields = match sorted_fields_result { + Ok(fields) => fields, + Err(_) => return runtime_error(env, "Can't create record with improper layout"), + }; + + let take_field_expr = + move |field: Lowercase| fields.remove(&field).map(|f| (f.var, f.loc_expr)); + + compile_struct_like( + env, + procs, + layout_cache, + sorted_fields, + take_field_expr, + record_var, + hole, + assigned, + ) + } + + EmptyRecord => let_empty_struct(assigned, hole), + + Expect { .. } => unreachable!("I think this is unreachable"), + ExpectFx { .. } => unreachable!("I think this is unreachable"), + Dbg { + loc_message, + loc_continuation, + variable: cond_variable, + symbol: dbg_symbol, + } => { + let rest = with_hole( + env, + loc_continuation.value, + variable, + procs, + layout_cache, + assigned, + hole, + ); + + compile_dbg( + env, + procs, + layout_cache, + dbg_symbol, + *loc_message, + cond_variable, + rest, + ) + } + + If { + cond_var, + branch_var, + branches, + final_else, + } => { + match ( + layout_cache.from_var(env.arena, branch_var, env.subs), + layout_cache.from_var(env.arena, cond_var, env.subs), + ) { + (Ok(ret_layout), Ok(cond_layout)) => { + // if the hole is a return, then we don't need to merge the two + // branches together again, we can just immediately return + let is_terminated = matches!(hole, Stmt::Ret(_)); + + if is_terminated { + let terminator = hole; + + let mut stmt = with_hole( + env, + final_else.value, + branch_var, + procs, + layout_cache, + assigned, + terminator, + ); + + for (loc_cond, loc_then) in branches.into_iter().rev() { + let branching_symbol = env.unique_symbol(); + + let then = with_hole( + env, + loc_then.value, + branch_var, + procs, + layout_cache, + assigned, + terminator, + ); + + stmt = cond(env, branching_symbol, cond_layout, then, stmt, ret_layout); + + // add condition + stmt = with_hole( + env, + loc_cond.value, + cond_var, + procs, + layout_cache, + branching_symbol, + env.arena.alloc(stmt), + ); + } + stmt + } else { + let assigned_in_jump = env.unique_symbol(); + let id = JoinPointId(env.unique_symbol()); + + let terminator = env + .arena + .alloc(Stmt::Jump(id, env.arena.alloc([assigned_in_jump]))); + + let mut stmt = with_hole( + env, + final_else.value, + branch_var, + procs, + layout_cache, + assigned_in_jump, + terminator, + ); + + for (loc_cond, loc_then) in branches.into_iter().rev() { + let branching_symbol = possible_reuse_symbol_or_specialize( + env, + procs, + layout_cache, + &loc_cond.value, + cond_var, + ); + + let then = with_hole( + env, + loc_then.value, + branch_var, + procs, + layout_cache, + assigned_in_jump, + terminator, + ); + + stmt = cond(env, branching_symbol, cond_layout, then, stmt, ret_layout); + + // add condition + stmt = assign_to_symbol( + env, + procs, + layout_cache, + cond_var, + loc_cond, + branching_symbol, + stmt, + ); + } + + let layout = layout_cache + .from_var(env.arena, branch_var, env.subs) + .unwrap_or_else(|err| { + panic!("TODO turn fn_var into a RuntimeError {err:?}") + }); + + let param = Param { + symbol: assigned, + layout, + }; + + Stmt::Join { + id, + parameters: env.arena.alloc([param]), + remainder: env.arena.alloc(stmt), + body: hole, + } + } + } + (Err(_), _) => runtime_error(env, "invalid ret_layout"), + (_, Err(_)) => runtime_error(env, "invalid cond_layout"), + } + } + + When { + cond_var, + expr_var, + region: _, + loc_cond, + branches, + branches_cond_var: _, + exhaustive, + } => { + let cond_symbol = possible_reuse_symbol_or_specialize( + env, + procs, + layout_cache, + &loc_cond.value, + cond_var, + ); + + let id = JoinPointId(env.unique_symbol()); + + let mut stmt = from_can_when( + env, + cond_var, + expr_var, + cond_symbol, + branches, + exhaustive, + layout_cache, + procs, + Some(id), + ); + + // define the `when` condition + stmt = assign_to_symbol( + env, + procs, + layout_cache, + cond_var, + *loc_cond, + cond_symbol, + stmt, + ); + + let layout = layout_cache + .from_var(env.arena, expr_var, env.subs) + .unwrap_or_else(|err| panic!("TODO turn fn_var into a RuntimeError {err:?}")); + + let param = Param { + symbol: assigned, + layout, + }; + + Stmt::Join { + id, + parameters: env.arena.alloc([param]), + remainder: env.arena.alloc(stmt), + body: env.arena.alloc(hole), + } + } + + List { + loc_elems, + elem_var, + .. + } if loc_elems.is_empty() => { + // because an empty list has an unknown element type, it is handled differently + let opt_elem_layout = layout_cache.from_var(env.arena, elem_var, env.subs); + + match opt_elem_layout { + Ok(elem_layout) => { + let expr = Expr::EmptyArray; + let list_layout = layout_cache + .put_in_direct_no_semantic(LayoutRepr::Builtin(Builtin::List(elem_layout))); + Stmt::Let(assigned, expr, list_layout, hole) + } + Err(LayoutProblem::UnresolvedTypeVar(_)) => { + let expr = Expr::EmptyArray; + let list_layout = layout_cache.put_in_direct_no_semantic(LayoutRepr::Builtin( + Builtin::List(Layout::VOID), + )); + Stmt::Let(assigned, expr, list_layout, hole) + } + Err(LayoutProblem::Erroneous) => panic!("list element is error type"), + } + } + + List { + elem_var, + loc_elems, + } => { + let mut arg_symbols = Vec::with_capacity_in(loc_elems.len(), env.arena); + let mut elements = Vec::with_capacity_in(loc_elems.len(), env.arena); + + let mut symbol_exprs = Vec::with_capacity_in(loc_elems.len(), env.arena); + + let elem_layout = layout_cache + .from_var(env.arena, elem_var, env.subs) + .unwrap_or_else(|err| panic!("TODO turn fn_var into a RuntimeError {err:?}")); + + for arg_expr in loc_elems.into_iter() { + if let Some(literal) = + try_make_literal(&layout_cache.interner, &arg_expr.value, elem_layout) + { + elements.push(ListLiteralElement::Literal(literal)); + } else { + let symbol = possible_reuse_symbol_or_specialize( + env, + procs, + layout_cache, + &arg_expr.value, + elem_var, + ); + + elements.push(ListLiteralElement::Symbol(symbol)); + arg_symbols.push(symbol); + symbol_exprs.push(arg_expr); + } + } + let arg_symbols = arg_symbols.into_bump_slice(); + + let expr = Expr::Array { + elem_layout, + elems: elements.into_bump_slice(), + }; + + let list_layout = layout_cache + .put_in_direct_no_semantic(LayoutRepr::Builtin(Builtin::List(elem_layout))); + + let stmt = Stmt::Let(assigned, expr, list_layout, hole); + + let iter = symbol_exprs + .into_iter() + .rev() + .map(|e| (elem_var, e)) + .zip(arg_symbols.iter().rev()); + + assign_to_symbols(env, procs, layout_cache, iter, stmt) + } + + RecordAccess { + record_var, + field_var, + field, + loc_expr, + .. + } => { + let sorted_fields_result = { + let mut layout_env = layout::Env::from_components( + layout_cache, + env.subs, + env.arena, + env.target_info, + ); + layout::sort_record_fields(&mut layout_env, record_var) + }; + let sorted_fields = match sorted_fields_result { + Ok(fields) => fields, + Err(_) => return runtime_error(env, "Can't access record with improper layout"), + }; + + let mut index = None; + let mut field_layouts = Vec::with_capacity_in(sorted_fields.len(), env.arena); + + let mut current = 0; + for (label, _, opt_field_layout) in sorted_fields.into_iter() { + match opt_field_layout { + Err(_) => { + // this was an optional field, and now does not exist! + // do not increment `current`! + } + Ok(field_layout) => { + field_layouts.push(field_layout); + + if label == field { + index = Some(current); + } + + current += 1; + } + } + } + + compile_struct_like_access( + env, + procs, + layout_cache, + field_layouts, + index.expect("field not in its own type") as _, + *loc_expr, + record_var, + hole, + assigned, + field_var, + ) + } + + RecordAccessor(accessor_data) => { + let field_var = accessor_data.field_var; + let fresh_record_symbol = env.unique_symbol(); + + let ClosureData { + name, + function_type, + arguments, + loc_body, + .. + } = accessor_data.to_closure_data(fresh_record_symbol); + + match procs.insert_anonymous( + env, + LambdaName::no_niche(name), + function_type, + arguments, + *loc_body, + CapturedSymbols::None, + field_var, + layout_cache, + ) { + Ok(_) => { + let raw_layout = return_on_layout_error!( + env, + layout_cache.raw_from_var(env.arena, function_type, env.subs), + "Expr::Accessor" + ); + + match raw_layout { + RawFunctionLayout::Function(_, lambda_set, _) => { + let lambda_name = + find_lambda_name(env, layout_cache, lambda_set, name, &[]); + construct_closure_data( + env, + procs, + layout_cache, + lambda_set, + lambda_name, + &[], + assigned, + hole, + ) + } + RawFunctionLayout::ErasedFunction(_, _) => todo_lambda_erasure!(), + RawFunctionLayout::ZeroArgumentThunk(_) => unreachable!(), + } + } + + Err(_error) => runtime_error( + env, + "TODO convert anonymous function error to a RuntimeError string", + ), + } + } + + TupleAccess { + tuple_var, + elem_var, + index: accessed_index, + loc_expr, + .. + } => { + let sorted_elems_result = { + let mut layout_env = layout::Env::from_components( + layout_cache, + env.subs, + env.arena, + env.target_info, + ); + layout::sort_tuple_elems(&mut layout_env, tuple_var) + }; + let sorted_elems = match sorted_elems_result { + Ok(fields) => fields, + Err(_) => return runtime_error(env, "Can't access tuple with improper layout"), + }; + let mut field_layouts = Vec::with_capacity_in(sorted_elems.len(), env.arena); + + let mut final_index = None; + + for (current, (index, _, elem_layout)) in sorted_elems.into_iter().enumerate() { + field_layouts.push(elem_layout); + + if index == accessed_index { + final_index = Some(current); + } + } + + compile_struct_like_access( + env, + procs, + layout_cache, + field_layouts, + final_index.expect("elem not in its own type") as u64, + *loc_expr, + tuple_var, + hole, + assigned, + elem_var, + ) + } + + OpaqueWrapFunction(wrap_fn_data) => { + let opaque_var = wrap_fn_data.opaque_var; + let arg_symbol = env.unique_symbol(); + + let ClosureData { + name, + function_type, + arguments, + loc_body, + .. + } = wrap_fn_data.to_closure_data(arg_symbol); + + match procs.insert_anonymous( + env, + LambdaName::no_niche(name), + function_type, + arguments, + *loc_body, + CapturedSymbols::None, + opaque_var, + layout_cache, + ) { + Ok(_) => { + let raw_layout = return_on_layout_error!( + env, + layout_cache.raw_from_var(env.arena, function_type, env.subs), + "Expr::OpaqueWrapFunction" + ); + + match raw_layout { + RawFunctionLayout::Function(_, lambda_set, _) => { + let lambda_name = + find_lambda_name(env, layout_cache, lambda_set, name, &[]); + construct_closure_data( + env, + procs, + layout_cache, + lambda_set, + lambda_name, + &[], + assigned, + hole, + ) + } + RawFunctionLayout::ErasedFunction(..) => todo_lambda_erasure!(), + RawFunctionLayout::ZeroArgumentThunk(_) => { + internal_error!("should not be a thunk!") + } + } + } + + Err(_error) => runtime_error( + env, + "TODO convert anonymous function error to a RuntimeError string", + ), + } + } + + RecordUpdate { + record_var, + symbol: structure, + ref updates, + .. + } => { + use FieldType::*; + + enum FieldType<'a> { + CopyExisting, + UpdateExisting(&'a roc_can::expr::Field), + } + + // Strategy: turn a record update into the creation of a new record. + // This has the benefit that we don't need to do anything special for reference + // counting + let sorted_fields_result = { + let mut layout_env = layout::Env::from_components( + layout_cache, + env.subs, + env.arena, + env.target_info, + ); + layout::sort_record_fields(&mut layout_env, record_var) + }; + + let sorted_fields = match sorted_fields_result { + Ok(fields) => fields, + Err(_) => return runtime_error(env, "Can't update record with improper layout"), + }; + + let single_field_struct = sorted_fields.len() == 1; + + // The struct indexing generated by the current context + let mut current_struct_indexing = Vec::with_capacity_in(sorted_fields.len(), env.arena); + // The symbols that are used to create the new struct + let mut new_struct_symbols = Vec::with_capacity_in(sorted_fields.len(), env.arena); + // Information about the fields that are being updated + let mut fields = Vec::with_capacity_in(sorted_fields.len(), env.arena); + let mut index = 0; + for (label, _, opt_field_layout) in sorted_fields.iter() { + let record_index = (structure, index); + + match opt_field_layout { + Err(_) => { + debug_assert!(!updates.contains_key(label)); + // this was an optional field, and now does not exist! + // do not increment `index`! + } + Ok(_field_layout) => { + current_struct_indexing.push(record_index); + + // The struct with a single field is optimized in such a way that replacing later indexing will cause an incorrect IR. + // Thus, only insert these struct_indices if there is more than one field in the struct. + if !single_field_struct { + let original_struct_symbol = env.unique_symbol(); + env.struct_indexing + .insert(record_index, original_struct_symbol); + } + if let Some(field) = updates.get(label) { + let new_struct_symbol = possible_reuse_symbol_or_specialize( + env, + procs, + layout_cache, + &field.loc_expr.value, + field.var, + ); + new_struct_symbols.push(new_struct_symbol); + fields.push(UpdateExisting(field)); + } else { + new_struct_symbols + .push(*env.struct_indexing.get(record_index).unwrap()); + fields.push(CopyExisting); + } + + index += 1; + } + } + } + + let new_struct_symbols = new_struct_symbols.into_bump_slice(); + + let record_layout = layout_cache + .from_var(env.arena, record_var, env.subs) + .unwrap_or_else(|err| panic!("TODO turn fn_var into a RuntimeError {err:?}")); + + let field_layouts = match layout_cache.get_repr(record_layout) { + LayoutRepr::Struct(field_layouts) => field_layouts, + _ => arena.alloc([record_layout]), + }; + + if single_field_struct { + // TODO we can probably special-case this more, skipping the generation of + // UpdateExisting + let mut stmt = hole.clone(); + + let what_to_do = &fields[0]; + + match what_to_do { + UpdateExisting(field) => { + substitute_in_exprs(env.arena, &mut stmt, assigned, new_struct_symbols[0]); + + stmt = assign_to_symbol( + env, + procs, + layout_cache, + field.var, + *field.loc_expr.clone(), + new_struct_symbols[0], + stmt, + ); + } + CopyExisting => { + unreachable!( + r"when a record has just one field and is updated, it must update that one field" + ); + } + } + + stmt + } else { + let expr = Expr::Struct(new_struct_symbols); + let mut stmt = Stmt::Let(assigned, expr, record_layout, hole); + + for (new_struct_symbol, what_to_do) in new_struct_symbols.iter().zip(fields) { + match what_to_do { + UpdateExisting(field) => { + stmt = assign_to_symbol( + env, + procs, + layout_cache, + field.var, + *field.loc_expr.clone(), + *new_struct_symbol, + stmt, + ); + } + CopyExisting => { + // When a field is copied, the indexing symbol is already placed in new_struct_symbols + // Thus, we don't need additional logic here. + } + } + } + + let structure_needs_specialization = + procs.ability_member_aliases.get(structure).is_some() + || procs.is_module_thunk(structure) + || procs.is_imported_module_thunk(structure); + + let specialized_structure_sym = if structure_needs_specialization { + // We need to specialize the record now; create a new one for it. + env.unique_symbol() + } else { + // The record is already good. + structure + }; + + for record_index in current_struct_indexing.into_iter().rev() { + if let Some(symbol) = env.struct_indexing.get_used(&record_index) { + let layout = field_layouts[record_index.1 as usize]; + let access_expr = Expr::StructAtIndex { + structure: specialized_structure_sym, + index: record_index.1, + field_layouts, + }; + stmt = Stmt::Let(symbol, access_expr, layout, arena.alloc(stmt)); + }; + } + + if structure_needs_specialization { + stmt = specialize_symbol( + env, + procs, + layout_cache, + Some(record_var), + specialized_structure_sym, + env.arena.alloc(stmt), + structure, + ); + } + + stmt + } + } + + Closure(ClosureData { + function_type, + return_type, + name, + arguments, + captured_symbols, + loc_body: boxed_body, + .. + }) => { + let loc_body = *boxed_body; + + let raw = layout_cache.raw_from_var(env.arena, function_type, env.subs); + + match return_on_layout_error!(env, raw, "Expr::Closure") { + RawFunctionLayout::ZeroArgumentThunk(_) => { + unreachable!("a closure syntactically always must have at least one argument") + } + RawFunctionLayout::ErasedFunction(argument_layouts, ret_layout) => { + let captured_symbols = if captured_symbols.is_empty() { + CapturedSymbols::None + } else { + let captured_symbols = Vec::from_iter_in(captured_symbols, env.arena); + let captured_symbols = captured_symbols.into_bump_slice(); + CapturedSymbols::Captured(captured_symbols) + }; + let resolved_erased_lambda = ResolvedErasedLambda::new( + env, + layout_cache, + name, + captured_symbols, + argument_layouts, + ret_layout, + ); + + let inserted = procs.insert_anonymous( + env, + resolved_erased_lambda.lambda_name(), + function_type, + arguments, + loc_body, + captured_symbols, + return_type, + layout_cache, + ); + + if let Err(e) = inserted { + return runtime_error(env, env.arena.alloc(format!("RuntimeError: {e:?}"))); + } + drop(inserted); + + build_erased_function(env, layout_cache, resolved_erased_lambda, assigned, hole) + } + RawFunctionLayout::Function(_argument_layouts, lambda_set, _ret_layout) => { + let mut captured_symbols = Vec::from_iter_in(captured_symbols, env.arena); + captured_symbols.sort(); + let captured_symbols = captured_symbols.into_bump_slice(); + + let symbols = + Vec::from_iter_in(captured_symbols.iter(), env.arena).into_bump_slice(); + + let lambda_name = find_lambda_name( + env, + layout_cache, + lambda_set, + name, + symbols.iter().copied(), + ); + + let inserted = procs.insert_anonymous( + env, + lambda_name, + function_type, + arguments, + loc_body, + CapturedSymbols::Captured(captured_symbols), + return_type, + layout_cache, + ); + + if let Err(e) = inserted { + return runtime_error( + env, + env.arena.alloc(format!("RuntimeError: {e:?}",)), + ); + } + drop(inserted); + + // define the closure data + + construct_closure_data( + env, + procs, + layout_cache, + lambda_set, + lambda_name, + symbols.iter().copied(), + assigned, + hole, + ) + } + } + } + + Call(boxed, loc_args, _) => { + let (fn_var, loc_expr, _lambda_set_var, _ret_var) = *boxed; + + // even if a call looks like it's by name, it may in fact be by-pointer. + // E.g. in `(\f, x -> f x)` the call is in fact by pointer. + // So we check the function name against the list of partial procedures, + // the procedures that we have lifted to the top-level and can call by name + // if it's in there, it's a call by name, otherwise it's a call by pointer + let is_known = |key| { + // a proc in this module, or an imported symbol + procs.partial_procs.contains_key(key) + || (env.is_imported_symbol(key) && !procs.is_imported_module_thunk(key)) + }; + + match loc_expr.value { + roc_can::expr::Expr::Var(proc_name, _) if is_known(proc_name) => { + // a call by a known name + call_by_name( + env, + procs, + fn_var, + proc_name, + loc_args, + layout_cache, + assigned, + hole, + ) + } + roc_can::expr::Expr::AbilityMember(member, specialization_id, _) => { + let specialization_proc_name = + late_resolve_ability_specialization(env, member, specialization_id, fn_var); + + call_by_name( + env, + procs, + fn_var, + specialization_proc_name, + loc_args, + layout_cache, + assigned, + hole, + ) + } + _ => { + // Call by pointer - the closure was anonymous, e.g. + // + // ((\a -> a) 5) + // + // It might even be the anonymous result of a conditional: + // + // ((if x > 0 then \a -> a else \_ -> 0) 5) + // + // It could be named too: + // + // ((if x > 0 then foo else bar) 5) + // + // also this occurs for functions passed in as arguments, e.g. + // + // (\f, x -> f x) + + let arg_symbols = Vec::from_iter_in( + loc_args.iter().map(|(var, arg_expr)| { + possible_reuse_symbol_or_specialize( + env, + procs, + layout_cache, + &arg_expr.value, + *var, + ) + }), + arena, + ) + .into_bump_slice(); + + let full_layout = return_on_layout_error!( + env, + layout_cache.raw_from_var(env.arena, fn_var, env.subs), + "Expr::Call" + ); + + // if the function expression (loc_expr) is already a symbol, + // re-use that symbol, and don't define its value again + let mut result; + use ReuseSymbol::*; + match can_reuse_symbol(env, layout_cache, procs, &loc_expr.value, fn_var) { + LocalFunction(_) => { + unreachable!("if this was known to be a function, we would not be here") + } + Imported(thunk_name) => { + debug_assert!(procs.is_imported_module_thunk(thunk_name)); + + add_needed_external( + procs, + env, + fn_var, + LambdaName::no_niche(thunk_name), + ); + + let function_symbol = env.unique_symbol(); + + match full_layout { + RawFunctionLayout::Function( + arg_layouts, + lambda_set, + ret_layout, + ) => { + let closure_data_symbol = function_symbol; + + result = match_on_lambda_set( + env, + layout_cache, + procs, + lambda_set, + closure_data_symbol, + arg_symbols, + arg_layouts, + ret_layout, + assigned, + hole, + ); + + let lambda_set_layout = lambda_set.full_layout; + + result = force_thunk( + env, + thunk_name, + lambda_set_layout, + function_symbol, + env.arena.alloc(result), + ); + } + RawFunctionLayout::ErasedFunction(..) => todo_lambda_erasure!(), + RawFunctionLayout::ZeroArgumentThunk(_) => { + unreachable!("calling a non-closure layout") + } + } + } + Value(function_symbol) => { + let function_symbol = procs.get_or_insert_symbol_specialization( + env, + layout_cache, + function_symbol, + fn_var, + ); + + match full_layout { + RawFunctionLayout::Function( + arg_layouts, + lambda_set, + ret_layout, + ) => { + let closure_data_symbol = function_symbol; + + result = match_on_lambda_set( + env, + layout_cache, + procs, + lambda_set, + closure_data_symbol, + arg_symbols, + arg_layouts, + ret_layout, + assigned, + hole, + ); + } + RawFunctionLayout::ErasedFunction(..) => todo_lambda_erasure!(), + RawFunctionLayout::ZeroArgumentThunk(_) => { + unreachable!("calling a non-closure layout") + } + } + } + UnspecializedExpr(symbol) => { + match procs.ability_member_aliases.get(symbol).unwrap() { + &self::AbilityMember(member) => { + let resolved_proc = resolve_ability_specialization(env.home, env.subs, &env.abilities, member, fn_var) + .expect("Recorded as an ability member, but it doesn't have a specialization"); + + let resolved_proc = match resolved_proc { + Resolved::Specialization(symbol) => symbol, + Resolved::Derive(_) => { + todo_abilities!("Generate impls for structural types") + } + }; + + // a call by a known name + return call_by_name( + env, + procs, + fn_var, + resolved_proc, + loc_args, + layout_cache, + assigned, + hole, + ); + } + } + } + NotASymbol => { + // the expression is not a symbol. That means it's an expression + // evaluating to a function value. + + match full_layout { + RawFunctionLayout::Function( + arg_layouts, + lambda_set, + ret_layout, + ) => { + let closure_data_symbol = env.unique_symbol(); + + result = match_on_lambda_set( + env, + layout_cache, + procs, + lambda_set, + closure_data_symbol, + arg_symbols, + arg_layouts, + ret_layout, + assigned, + hole, + ); + + result = with_hole( + env, + loc_expr.value, + fn_var, + procs, + layout_cache, + closure_data_symbol, + env.arena.alloc(result), + ); + } + RawFunctionLayout::ErasedFunction(arg_layouts, ret_layout) => { + let hole_layout = + layout_cache.from_var(env.arena, fn_var, env.subs).unwrap(); + result = erased::call_erased_function( + env, + layout_cache, + procs, + loc_expr.value, + fn_var, + (arg_layouts, ret_layout), + arg_symbols, + assigned, + hole, + hole_layout, + ); + } + RawFunctionLayout::ZeroArgumentThunk(_) => { + unreachable!( + "{:?} cannot be called in the source language", + full_layout + ) + } + } + } + } + let iter = loc_args.into_iter().rev().zip(arg_symbols.iter().rev()); + assign_to_symbols(env, procs, layout_cache, iter, result) + } + } + } + + ForeignCall { + foreign_symbol, + args, + ret_var, + } => { + let mut arg_symbols = Vec::with_capacity_in(args.len(), env.arena); + + for (var, arg_expr) in args.iter() { + arg_symbols.push(possible_reuse_symbol_or_specialize( + env, + procs, + layout_cache, + arg_expr, + *var, + )); + } + let arg_symbols = arg_symbols.into_bump_slice(); + + // layout of the return type + let layout = return_on_layout_error!( + env, + layout_cache.from_var(env.arena, ret_var, env.subs), + "ForeignCall" + ); + + let call = self::Call { + call_type: CallType::Foreign { + foreign_symbol, + ret_layout: layout, + }, + arguments: arg_symbols, + }; + + let result = build_call(env, call, assigned, layout, hole); + + let iter = args + .into_iter() + .rev() + .map(|(a, b)| (a, Loc::at_zero(b))) + .zip(arg_symbols.iter().rev()); + assign_to_symbols(env, procs, layout_cache, iter, result) + } + + RunLowLevel { op, args, ret_var } => { + let mut arg_symbols = Vec::with_capacity_in(args.len(), env.arena); + + for (var, arg_expr) in args.iter() { + arg_symbols.push(possible_reuse_symbol_or_specialize( + env, + procs, + layout_cache, + arg_expr, + *var, + )); + } + let arg_symbols = arg_symbols.into_bump_slice(); + + // layout of the return type + let layout = return_on_layout_error!( + env, + layout_cache.from_var(env.arena, ret_var, env.subs), + "RunLowLevel" + ); + + macro_rules! match_on_closure_argument { + ( $ho:ident, [$($x:ident),* $(,)?]) => {{ + let closure_index = op.function_argument_position(); + let closure_data_symbol = arg_symbols[closure_index]; + let closure_data_var = args[closure_index].0; + + let closure_data_layout = return_on_layout_error!( + env, + layout_cache.raw_from_var(env.arena, closure_data_var, env.subs), + "match_on_closure_argument" + ); + + let arena = env.arena; + + match closure_data_layout { + RawFunctionLayout::Function(_, lambda_set, _) => { + lowlevel_match_on_lambda_set( + env, + layout_cache, + lambda_set, + op, + closure_data_symbol, + |(lambda_name, closure_data, closure_env_layout, specialization_id, update_mode)| { + // Build a call for a specific lambda in the set + let top_level = ProcLayout::from_raw_named(env.arena, lambda_name, closure_data_layout); + let arg_layouts = top_level.arguments; + let ret_layout = top_level.result; + + let passed_function = PassedFunction { + name: lambda_name, + captured_environment: closure_data_symbol, + owns_captured_environment: true, + specialization_id, + argument_layouts: arg_layouts, + return_layout: ret_layout, + }; + + let higher_order = HigherOrderLowLevel { + op: crate::low_level::HigherOrder::$ho { $($x,)* }, + closure_env_layout, + update_mode, + passed_function, + }; + + self::Call { + call_type: CallType::HigherOrder(arena.alloc(higher_order)), + arguments: arena.alloc([$($x,)* lambda_name.name(), closure_data]), + } + }, + layout, + assigned, + hole, + ) + } + RawFunctionLayout::ErasedFunction(..) => todo_lambda_erasure!(), + RawFunctionLayout::ZeroArgumentThunk(_) => unreachable!("match_on_closure_argument received a zero-argument thunk"), + } + }}; + } + + use LowLevel::*; + match op { + ListMap => { + debug_assert_eq!(arg_symbols.len(), 2); + let xs = arg_symbols[0]; + match_on_closure_argument!(ListMap, [xs]) + } + ListSortWith => { + debug_assert_eq!(arg_symbols.len(), 2); + let xs = arg_symbols[0]; + match_on_closure_argument!(ListSortWith, [xs]) + } + ListMap2 => { + debug_assert_eq!(arg_symbols.len(), 3); + + let xs = arg_symbols[0]; + let ys = arg_symbols[1]; + + match_on_closure_argument!(ListMap2, [xs, ys]) + } + ListMap3 => { + debug_assert_eq!(arg_symbols.len(), 4); + + let xs = arg_symbols[0]; + let ys = arg_symbols[1]; + let zs = arg_symbols[2]; + + match_on_closure_argument!(ListMap3, [xs, ys, zs]) + } + ListMap4 => { + debug_assert_eq!(arg_symbols.len(), 5); + + let xs = arg_symbols[0]; + let ys = arg_symbols[1]; + let zs = arg_symbols[2]; + let ws = arg_symbols[3]; + + match_on_closure_argument!(ListMap4, [xs, ys, zs, ws]) + } + BoxExpr => { + debug_assert_eq!(arg_symbols.len(), 1); + let x = arg_symbols[0]; + + let element_layout = match layout_cache.interner.get_repr(layout) { + LayoutRepr::Union(UnionLayout::NonNullableUnwrapped([l])) => l, + _ => unreachable!("invalid layout for a box expression"), + }; + + let expr = boxed::box_(arena.alloc(x), element_layout); + + Stmt::Let(assigned, expr, layout, hole) + } + UnboxExpr => { + debug_assert_eq!(arg_symbols.len(), 1); + let x = arg_symbols[0]; + + let expr = boxed::unbox(x, arena.alloc(layout)); + + Stmt::Let(assigned, expr, layout, hole) + } + _ => { + let call = self::Call { + call_type: CallType::LowLevel { + op, + update_mode: env.next_update_mode_id(), + }, + arguments: arg_symbols, + }; + + let result = build_call(env, call, assigned, layout, hole); + + let iter = args + .into_iter() + .rev() + .map(|(a, b)| (a, Loc::at_zero(b))) + .zip(arg_symbols.iter().rev()); + assign_to_symbols(env, procs, layout_cache, iter, result) + } + } + } + TypedHole(_) => runtime_error(env, "Hit a blank"), + RuntimeError(e) => runtime_error(env, env.arena.alloc(e.runtime_message())), + Crash { msg, ret_var: _ } => { + let msg_sym = possible_reuse_symbol_or_specialize( + env, + procs, + layout_cache, + &msg.value, + Variable::STR, + ); + let stmt = Stmt::Crash(msg_sym, CrashTag::User); + + assign_to_symbol(env, procs, layout_cache, Variable::STR, *msg, msg_sym, stmt) + } + } +} + +/// Compiles a `dbg` expression. +fn compile_dbg<'a>( + env: &mut Env<'a, '_>, + procs: &mut Procs<'a>, + layout_cache: &mut LayoutCache<'a>, + dbg_symbol: Symbol, + loc_condition: Loc, + variable: Variable, + continuation: Stmt<'a>, +) -> Stmt<'a> { + let spec_var = env + .expectation_subs + .as_mut() + .unwrap() + .fresh_unnamed_flex_var(); + + let dbg_stmt = Stmt::Dbg { + symbol: dbg_symbol, + variable: spec_var, + remainder: env.arena.alloc(continuation), + }; + + // Now that the dbg value has been specialized, export its specialized type into the + // expectations subs. + store_specialized_expectation_lookups(env, [variable], &[spec_var]); + + let symbol_is_reused = matches!( + can_reuse_symbol(env, layout_cache, procs, &loc_condition.value, variable), + ReuseSymbol::Value(_) + ); + + // skip evaluating the condition if it's just a symbol + if symbol_is_reused { + dbg_stmt + } else { + with_hole( + env, + loc_condition.value, + variable, + procs, + layout_cache, + dbg_symbol, + env.arena.alloc(dbg_stmt), + ) + } +} + +/// Compiles an access into a tuple or record. +fn compile_struct_like_access<'a>( + env: &mut Env<'a, '_>, + procs: &mut Procs<'a>, + layout_cache: &mut LayoutCache<'a>, + field_layouts: Vec<'a, InLayout<'a>>, + index: u64, + loc_expr: Loc, + struct_like_var: Variable, + hole: &'a Stmt<'a>, + assigned: Symbol, + elem_var: Variable, +) -> Stmt<'a> { + let struct_symbol = possible_reuse_symbol_or_specialize( + env, + procs, + layout_cache, + &loc_expr.value, + struct_like_var, + ); + + let mut stmt = match field_layouts.as_slice() { + [_] => { + let mut hole = hole.clone(); + substitute_in_exprs(env.arena, &mut hole, assigned, struct_symbol); + + hole + } + _ => { + let expr = Expr::StructAtIndex { + index, + field_layouts: field_layouts.into_bump_slice(), + structure: struct_symbol, + }; + + let layout = layout_cache + .from_var(env.arena, elem_var, env.subs) + .unwrap_or_else(|err| panic!("TODO turn fn_var into a RuntimeError {err:?}")); + + Stmt::Let(assigned, expr, layout, hole) + } + }; + + stmt = assign_to_symbol( + env, + procs, + layout_cache, + struct_like_var, + loc_expr, + struct_symbol, + stmt, + ); + + stmt +} + +/// Compiles a record or a tuple. +// TODO: UnusedLayout is because `sort_record_fields` currently returns a three-tuple, but is, in +// fact, unneeded for the compilation. +fn compile_struct_like<'a, L, UnusedLayout>( + env: &mut Env<'a, '_>, + procs: &mut Procs<'a>, + layout_cache: &mut LayoutCache<'a>, + sorted_elems: Vec<(L, Variable, UnusedLayout)>, + mut take_elem_expr: impl FnMut(L) -> Option<(Variable, Box>)>, + struct_like_var: Variable, + hole: &'a Stmt<'a>, + assigned: Symbol, +) -> Stmt<'a> { + let mut elem_symbols = Vec::with_capacity_in(sorted_elems.len(), env.arena); + let mut can_elems = Vec::with_capacity_in(sorted_elems.len(), env.arena); + + #[allow(clippy::enum_variant_names)] + enum Field { + // TODO: rename this since it can handle unspecialized expressions now too + FunctionOrUnspecialized(Symbol, Variable), + ValueSymbol, + Field(Variable, Loc), + } + + for (index, variable, _) in sorted_elems.into_iter() { + // TODO how should function pointers be handled here? + use ReuseSymbol::*; + match take_elem_expr(index) { + Some((var, loc_expr)) => { + match can_reuse_symbol(env, layout_cache, procs, &loc_expr.value, var) { + Imported(symbol) => { + // we cannot re-use the symbol in this case; it is used as a value, but defined as a thunk + elem_symbols.push(env.unique_symbol()); + can_elems.push(Field::FunctionOrUnspecialized(symbol, variable)); + } + LocalFunction(symbol) | UnspecializedExpr(symbol) => { + elem_symbols.push(symbol); + can_elems.push(Field::FunctionOrUnspecialized(symbol, variable)); + } + Value(symbol) => { + let reusable = procs.get_or_insert_symbol_specialization( + env, + layout_cache, + symbol, + var, + ); + elem_symbols.push(reusable); + can_elems.push(Field::ValueSymbol); + } + NotASymbol => { + elem_symbols.push(env.unique_symbol()); + can_elems.push(Field::Field(var, *loc_expr)); + } + } + } + None => { + // this field was optional, but not given + continue; + } + } + } + + // creating a record from the var will unpack it if it's just a single field. + let layout = match layout_cache.from_var(env.arena, struct_like_var, env.subs) { + Ok(layout) => layout, + Err(_) => return runtime_error(env, "Can't create record with improper layout"), + }; + + let elem_symbols = elem_symbols.into_bump_slice(); + + let mut stmt = if let [only_field] = elem_symbols { + let mut hole = hole.clone(); + substitute_in_exprs(env.arena, &mut hole, assigned, *only_field); + hole + } else { + Stmt::Let(assigned, Expr::Struct(elem_symbols), layout, hole) + }; + + for (opt_field, symbol) in can_elems.into_iter().rev().zip(elem_symbols.iter().rev()) { + match opt_field { + Field::ValueSymbol => { + // this symbol is already defined; nothing to do + } + Field::FunctionOrUnspecialized(can_symbol, variable) => { + stmt = specialize_symbol( + env, + procs, + layout_cache, + Some(variable), + *symbol, + env.arena.alloc(stmt), + can_symbol, + ); + } + Field::Field(var, loc_expr) => { + stmt = with_hole( + env, + loc_expr.value, + var, + procs, + layout_cache, + *symbol, + env.arena.alloc(stmt), + ); + } + } + } + + stmt +} + +#[inline(always)] +fn late_resolve_ability_specialization( + env: &mut Env<'_, '_>, + member: Symbol, + specialization_id: Option, + specialization_var: Variable, +) -> Symbol { + let opt_resolved = specialization_id.and_then(|id| { + env.abilities + .with_module_abilities_store(env.home, |store| store.get_resolved(id)) + }); + + if let Some(spec_symbol) = opt_resolved { + // Fast path: specialization is monomorphic, was found during solving. + spec_symbol + } else if let Content::Structure(FlatType::Func(_, lambda_set, _)) = + env.subs.get_content_without_compacting(specialization_var) + { + // Fast path: the member is a function, so the lambda set will tell us the + // specialization. + use roc_types::subs::LambdaSet; + let LambdaSet { + solved, + unspecialized, + recursion_var: _, + ambient_function, + } = env.subs.get_lambda_set(*lambda_set); + + debug_assert!(unspecialized.is_empty()); + let mut iter_lambda_set = solved.iter_all(); + debug_assert_eq!( + iter_lambda_set.len(), + 1, + "{:?}", + (env.subs.dbg(*lambda_set), env.subs.dbg(ambient_function)) + ); + let spec_symbol_index = iter_lambda_set.next().unwrap().0; + env.subs[spec_symbol_index] + } else { + // Otherwise, resolve by checking the able var. + let specialization = resolve_ability_specialization( + env.home, + env.subs, + &env.abilities, + member, + specialization_var, + ) + .expect("Ability specialization is unknown - code generation cannot proceed!"); + + match specialization { + Resolved::Specialization(symbol) => symbol, + Resolved::Derive(derive_key) => { + match derive_key { + roc_derive_key::Derived::Immediate(imm) + | roc_derive_key::Derived::SingleLambdaSetImmediate(imm) => { + // The immediate may be an ability member itself, so it must be resolved! + late_resolve_ability_specialization(env, imm, None, specialization_var) + } + roc_derive_key::Derived::Key(derive_key) => { + let mut derived_module = env + .derived_module + .lock() + .expect("derived module unavailable"); + + derived_module + .get_or_insert(env.exposed_by_module, derive_key) + .0 + } + } + } + } + } +} + +fn find_lambda_name<'a, I>( + env: &mut Env<'a, '_>, + layout_cache: &mut LayoutCache<'a>, + lambda_set: LambdaSet<'a>, + function_name: Symbol, + captures: I, +) -> LambdaName<'a> +where + I: IntoIterator, +{ + let this_function_captures_layouts = captures + .into_iter() + .map(|(_, var)| { + layout_cache + .from_var(env.arena, *var, env.subs) + .expect("layout problem for capture") + }) + .collect_in::>(env.arena); + lambda_set.find_lambda_name( + &layout_cache.interner, + function_name, + &this_function_captures_layouts, + ) +} + +#[allow(clippy::too_many_arguments)] +fn construct_closure_data<'a, I>( + env: &mut Env<'a, '_>, + procs: &mut Procs<'a>, + layout_cache: &mut LayoutCache<'a>, + lambda_set: LambdaSet<'a>, + name: LambdaName<'a>, + symbols: I, + assigned: Symbol, + hole: &'a Stmt<'a>, +) -> Stmt<'a> +where + I: IntoIterator, + I::IntoIter: ExactSizeIterator, +{ + let lambda_set_layout = lambda_set.full_layout; + let symbols = symbols.into_iter(); + + let result = match lambda_set.layout_for_member_with_lambda_name(&layout_cache.interner, name) { + ClosureRepresentation::Union { + tag_id, + alphabetic_order_fields: field_layouts, + union_layout, + closure_name: _, + } => { + // captured variables are in symbol-alphabetic order, but now we want + // them ordered by their alignment requirements + let mut combined = Vec::with_capacity_in(symbols.len(), env.arena); + for ((symbol, _variable), layout) in symbols.zip(field_layouts.iter()) { + combined.push((*symbol, layout)) + } + + combined.sort_by(|(_, layout1), (_, layout2)| { + let size1 = layout_cache + .get_repr(**layout1) + .alignment_bytes(&layout_cache.interner); + let size2 = layout_cache + .get_repr(**layout2) + .alignment_bytes(&layout_cache.interner); + + size2.cmp(&size1) + }); + + let symbols = + Vec::from_iter_in(combined.iter().map(|(a, _)| *a), env.arena).into_bump_slice(); + + let expr = Expr::Tag { + tag_id, + tag_layout: union_layout, + arguments: symbols, + reuse: None, + }; + + Stmt::Let(assigned, expr, lambda_set_layout, env.arena.alloc(hole)) + } + ClosureRepresentation::AlphabeticOrderStruct(field_layouts) => { + debug_assert_eq!(field_layouts.len(), symbols.len()); + + // captured variables are in symbol-alphabetic order, but now we want + // them ordered by their alignment requirements + let mut combined = Vec::with_capacity_in(symbols.len(), env.arena); + for ((symbol, _variable), layout) in symbols.zip(field_layouts.iter()) { + combined.push((*symbol, layout)) + } + + combined.sort_by(|(_, layout1), (_, layout2)| { + let size1 = layout_cache + .get_repr(**layout1) + .alignment_bytes(&layout_cache.interner); + let size2 = layout_cache + .get_repr(**layout2) + .alignment_bytes(&layout_cache.interner); + + size2.cmp(&size1) + }); + + let symbols = + Vec::from_iter_in(combined.iter().map(|(a, _)| *a), env.arena).into_bump_slice(); + let field_layouts = + Vec::from_iter_in(combined.iter().map(|(_, b)| **b), env.arena).into_bump_slice(); + + debug_assert_eq!( + LayoutRepr::struct_(field_layouts), + layout_cache.get_repr(lambda_set.runtime_representation()) + ); + + let expr = Expr::Struct(symbols); + + Stmt::Let(assigned, expr, lambda_set_layout, hole) + } + ClosureRepresentation::UnwrappedCapture(_layout) => { + debug_assert_eq!(symbols.len(), 1); + + let mut symbols = symbols; + let (captured_symbol, captured_var) = symbols.next().unwrap(); + + let captured_symbol = procs.get_or_insert_symbol_specialization( + env, + layout_cache, + *captured_symbol, + *captured_var, + ); + + // The capture set is unwrapped, so just replaced the assigned capture symbol with the + // only capture. + let mut hole = hole.clone(); + substitute_in_exprs(env.arena, &mut hole, assigned, captured_symbol); + hole + } + ClosureRepresentation::EnumDispatch(repr) => match repr { + EnumDispatch::Bool => { + debug_assert_eq!(symbols.len(), 0); + + debug_assert_eq!(lambda_set.len(), 2); + let tag_id = name.name() != lambda_set.iter_set().next().unwrap().name(); + let expr = Expr::Literal(Literal::Bool(tag_id)); + + Stmt::Let(assigned, expr, lambda_set_layout, hole) + } + EnumDispatch::U8 => { + debug_assert_eq!(symbols.len(), 0); + + debug_assert!(lambda_set.len() > 2); + let tag_id = lambda_set + .iter_set() + .position(|s| s.name() == name.name()) + .unwrap() as u8; + + let expr = Expr::Literal(Literal::Byte(tag_id)); + + Stmt::Let(assigned, expr, lambda_set_layout, hole) + } + }, + }; + + result +} + +#[allow(clippy::too_many_arguments)] +fn convert_tag_union<'a>( + env: &mut Env<'a, '_>, + variant_var: Variable, + assigned: Symbol, + hole: &'a Stmt<'a>, + tag_name: TagName, + procs: &mut Procs<'a>, + layout_cache: &mut LayoutCache<'a>, + args: std::vec::Vec<(Variable, Loc)>, + arena: &'a Bump, +) -> Stmt<'a> { + use crate::layout::UnionVariant::*; + let res_variant = { + let mut layout_env = + layout::Env::from_components(layout_cache, env.subs, env.arena, env.target_info); + crate::layout::union_sorted_tags(&mut layout_env, variant_var) + }; + let variant = match res_variant { + Ok(cached) => cached, + Err(LayoutProblem::UnresolvedTypeVar(_)) => { + return runtime_error( + env, + env.arena.alloc(format!( + "Unresolved type variable for tag {}", + tag_name.0.as_str() + )), + ) + } + Err(LayoutProblem::Erroneous) => { + return runtime_error( + env, + env.arena.alloc(format!( + "Tag {} was part of a type error!", + tag_name.0.as_str() + )), + ); + } + }; + + match variant { + Never => unreachable!( + "The `[]` type has no constructors, source var {:?}", + variant_var + ), + Unit => Stmt::Let(assigned, Expr::Struct(&[]), Layout::UNIT, hole), + BoolUnion { ttrue, .. } => Stmt::Let( + assigned, + Expr::Literal(Literal::Bool(&tag_name == ttrue.expect_tag_ref())), + Layout::BOOL, + hole, + ), + ByteUnion(tag_names) => { + let opt_tag_id = tag_names + .iter() + .position(|key| key.expect_tag_ref() == &tag_name); + + match opt_tag_id { + Some(tag_id) => Stmt::Let( + assigned, + Expr::Literal(Literal::Byte(tag_id as u8)), + Layout::U8, + hole, + ), + None => runtime_error(env, "tag must be in its own type"), + } + } + + Newtype { + arguments: field_layouts, + .. + } => { + let field_symbols_temp = sorted_field_symbols(env, procs, layout_cache, args); + + let mut field_symbols = Vec::with_capacity_in(field_layouts.len(), env.arena); + field_symbols.extend(field_symbols_temp.iter().map(|r| r.1)); + let field_symbols = field_symbols.into_bump_slice(); + + // Layout will unpack this unwrapped tack if it only has one (non-zero-sized) field + let layout = layout_cache + .from_var(env.arena, variant_var, env.subs) + .unwrap_or_else(|err| panic!("TODO turn fn_var into a RuntimeError {err:?}")); + + // even though this was originally a Tag, we treat it as a Struct from now on + let stmt = if let [only_field] = field_symbols { + let mut hole = hole.clone(); + substitute_in_exprs(env.arena, &mut hole, assigned, *only_field); + hole + } else { + Stmt::Let(assigned, Expr::Struct(field_symbols), layout, hole) + }; + + let iter = field_symbols_temp.into_iter().map(|(_, _, data)| data); + assign_to_symbols(env, procs, layout_cache, iter, stmt) + } + NewtypeByVoid { + data_tag_arguments: field_layouts, + data_tag_name, + .. + } => { + let dataful_tag = data_tag_name.expect_tag(); + + if dataful_tag != tag_name { + // this tag is not represented, and hence will never be reached, at runtime. + runtime_error(env, "voided tag constructor is unreachable") + } else { + let field_symbols_temp = sorted_field_symbols(env, procs, layout_cache, args); + + let mut field_symbols = Vec::with_capacity_in(field_layouts.len(), env.arena); + field_symbols.extend(field_symbols_temp.iter().map(|r| r.1)); + let field_symbols = field_symbols.into_bump_slice(); + + // Layout will unpack this unwrapped tack if it only has one (non-zero-sized) field + let layout = layout_cache + .from_var(env.arena, variant_var, env.subs) + .unwrap_or_else(|err| panic!("TODO turn fn_var into a RuntimeError {err:?}")); + + // even though this was originally a Tag, we treat it as a Struct from now on + let stmt = if let [only_field] = field_symbols { + let mut hole = hole.clone(); + substitute_in_exprs(env.arena, &mut hole, assigned, *only_field); + hole + } else { + Stmt::Let(assigned, Expr::Struct(field_symbols), layout, hole) + }; + + let iter = field_symbols_temp.into_iter().map(|(_, _, data)| data); + assign_to_symbols(env, procs, layout_cache, iter, stmt) + } + } + Wrapped(variant) => { + let (tag_id, _) = variant.tag_name_to_id(&tag_name); + + let field_symbols_temp = sorted_field_symbols(env, procs, layout_cache, args); + + let field_symbols; + + // we must derive the union layout from the whole_var, building it up + // from `layouts` would unroll recursive tag unions, and that leads to + // problems down the line because we hash layouts and an unrolled + // version is not the same as the minimal version. + let variant_layout = return_on_layout_error!( + env, + layout_cache.from_var(env.arena, variant_var, env.subs), + "Wrapped" + ); + let union_layout = match layout_cache.interner.chase_recursive(variant_layout) { + LayoutRepr::Union(ul) => ul, + other => internal_error!( + "unexpected layout {:?} for {:?}", + other, + roc_types::subs::SubsFmtContent( + env.subs.get_content_without_compacting(variant_var), + env.subs + ) + ), + }; + + use WrappedVariant::*; + let (tag, union_layout) = match variant { + Recursive { sorted_tag_layouts } => { + debug_assert!(sorted_tag_layouts.len() > 1); + + field_symbols = { + let mut temp = Vec::with_capacity_in(field_symbols_temp.len() + 1, arena); + + temp.extend(field_symbols_temp.iter().map(|r| r.1)); + + temp.into_bump_slice() + }; + + let mut layouts: Vec<&'a [InLayout<'a>]> = + Vec::with_capacity_in(sorted_tag_layouts.len(), env.arena); + + for (_, arg_layouts) in sorted_tag_layouts.into_iter() { + layouts.push(arg_layouts); + } + + let tag = Expr::Tag { + tag_layout: union_layout, + tag_id: tag_id as _, + arguments: field_symbols, + reuse: None, + }; + + (tag, union_layout) + } + NonNullableUnwrapped { + tag_name: wrapped_tag_name, + .. + } => { + debug_assert_eq!(wrapped_tag_name.expect_tag(), tag_name); + + field_symbols = { + let mut temp = Vec::with_capacity_in(field_symbols_temp.len(), arena); + + temp.extend(field_symbols_temp.iter().map(|r| r.1)); + + temp.into_bump_slice() + }; + + let tag = Expr::Tag { + tag_layout: union_layout, + tag_id: tag_id as _, + arguments: field_symbols, + reuse: None, + }; + + (tag, union_layout) + } + NonRecursive { sorted_tag_layouts } => { + field_symbols = { + let mut temp = Vec::with_capacity_in(field_symbols_temp.len(), arena); + + temp.extend(field_symbols_temp.iter().map(|r| r.1)); + + temp.into_bump_slice() + }; + + let mut layouts: Vec<&'a [InLayout<'a>]> = + Vec::with_capacity_in(sorted_tag_layouts.len(), env.arena); + + for (_, arg_layouts) in sorted_tag_layouts.into_iter() { + layouts.push(arg_layouts); + } + + let tag = Expr::Tag { + tag_layout: union_layout, + tag_id: tag_id as _, + arguments: field_symbols, + reuse: None, + }; + + (tag, union_layout) + } + NullableWrapped { + sorted_tag_layouts, .. + } => { + field_symbols = { + let mut temp = Vec::with_capacity_in(field_symbols_temp.len() + 1, arena); + + temp.extend(field_symbols_temp.iter().map(|r| r.1)); + + temp.into_bump_slice() + }; + + let mut layouts: Vec<&'a [InLayout<'a>]> = + Vec::with_capacity_in(sorted_tag_layouts.len(), env.arena); + + for (_, arg_layouts) in sorted_tag_layouts.into_iter() { + layouts.push(arg_layouts); + } + + let tag = Expr::Tag { + tag_layout: union_layout, + tag_id: tag_id as _, + arguments: field_symbols, + reuse: None, + }; + + (tag, union_layout) + } + NullableUnwrapped { .. } => { + field_symbols = { + let mut temp = Vec::with_capacity_in(field_symbols_temp.len() + 1, arena); + + temp.extend(field_symbols_temp.iter().map(|r| r.1)); + + temp.into_bump_slice() + }; + + let tag = Expr::Tag { + tag_layout: union_layout, + tag_id: tag_id as _, + arguments: field_symbols, + reuse: None, + }; + + (tag, union_layout) + } + }; + + let union_layout = + layout_cache.put_in_direct_no_semantic(LayoutRepr::Union(union_layout)); + + let stmt = Stmt::Let(assigned, tag, union_layout, hole); + let iter = field_symbols_temp + .into_iter() + .map(|x| x.2 .0) + .rev() + .zip(field_symbols.iter().rev()); + + assign_to_symbols(env, procs, layout_cache, iter, stmt) + } + } +} + +#[allow(clippy::too_many_arguments)] +fn tag_union_to_function<'a>( + env: &mut Env<'a, '_>, + argument_variables: VariableSubsSlice, + return_variable: Variable, + tag_name: TagName, + proc_symbol: Symbol, + ext_var: Variable, + procs: &mut Procs<'a>, + whole_var: Variable, + layout_cache: &mut LayoutCache<'a>, + assigned: Symbol, + hole: &'a Stmt<'a>, +) -> Stmt<'a> { + let mut loc_pattern_args = vec![]; + let mut loc_expr_args = vec![]; + + for index in argument_variables { + let arg_var = env.subs[index]; + + let arg_symbol = env.unique_symbol(); + + let loc_pattern = Loc::at_zero(roc_can::pattern::Pattern::Identifier(arg_symbol)); + + let loc_expr = Loc::at_zero(roc_can::expr::Expr::Var(arg_symbol, arg_var)); + + loc_pattern_args.push((arg_var, AnnotatedMark::known_exhaustive(), loc_pattern)); + loc_expr_args.push((arg_var, loc_expr)); + } + + let loc_body = Loc::at_zero(roc_can::expr::Expr::Tag { + tag_union_var: return_variable, + name: tag_name, + arguments: loc_expr_args, + ext_var, + }); + + // Lambda does not capture anything, can't have a captures niche + let lambda_name = LambdaName::no_niche(proc_symbol); + + let inserted = procs.insert_anonymous( + env, + lambda_name, + whole_var, + loc_pattern_args, + loc_body, + CapturedSymbols::None, + return_variable, + layout_cache, + ); + + match inserted { + Ok(_layout) => { + // only need to construct closure data + let raw_layout = return_on_layout_error!( + env, + layout_cache.raw_from_var(env.arena, whole_var, env.subs), + "tag_union_to_function" + ); + + match raw_layout { + RawFunctionLayout::Function(_, lambda_set, _) => { + let lambda_name = + find_lambda_name(env, layout_cache, lambda_set, proc_symbol, &[]); + debug_assert!(lambda_name.no_captures()); + construct_closure_data( + env, + procs, + layout_cache, + lambda_set, + lambda_name, + &[], + assigned, + hole, + ) + } + RawFunctionLayout::ErasedFunction(..) => todo_lambda_erasure!(), + RawFunctionLayout::ZeroArgumentThunk(_) => unreachable!(), + } + } + + Err(e) => runtime_error( + env, + env.arena.alloc(format!( + "Could not produce tag function due to a runtime error: {e:?}", + )), + ), + } +} + +#[allow(clippy::type_complexity)] +fn sorted_field_symbols<'a>( + env: &mut Env<'a, '_>, + procs: &mut Procs<'a>, + layout_cache: &mut LayoutCache<'a>, + mut args: std::vec::Vec<(Variable, Loc)>, +) -> Vec< + 'a, + ( + u32, + Symbol, + ((Variable, Loc), &'a Symbol), + ), +> { + let mut field_symbols_temp = Vec::with_capacity_in(args.len(), env.arena); + + for (var, mut arg) in args.drain(..) { + // Layout will unpack this unwrapped tag if it only has one (non-zero-sized) field + let layout = match layout_cache.from_var(env.arena, var, env.subs) { + Ok(cached) => cached, + Err(LayoutProblem::UnresolvedTypeVar(_)) => { + // this argument has type `forall a. a`, which is isomorphic to + // the empty type (Void, Never, the empty tag union `[]`) + // Note it does not catch the use of `[]` currently. + use roc_can::expr::Expr; + arg.value = Expr::RuntimeError(RuntimeError::VoidValue); + Layout::UNIT + } + Err(LayoutProblem::Erroneous) => { + // something went very wrong + panic!("TODO turn fn_var into a RuntimeError") + } + }; + + let alignment = layout_cache + .get_repr(layout) + .alignment_bytes(&layout_cache.interner); + + let symbol = possible_reuse_symbol_or_specialize(env, procs, layout_cache, &arg.value, var); + field_symbols_temp.push((alignment, symbol, ((var, arg), &*env.arena.alloc(symbol)))); + } + field_symbols_temp.sort_by(|a, b| b.0.cmp(&a.0)); + + field_symbols_temp +} + +/// Insert a closure that does capture symbols (because it is top-level) to the list of partial procs +fn register_noncapturing_closure<'a>( + env: &mut Env<'a, '_>, + procs: &mut Procs<'a>, + closure_name: Symbol, + closure_data: ClosureData, +) { + let ClosureData { + function_type, + return_type, + recursive, + arguments, + loc_body: boxed_body, + captured_symbols, + .. + } = closure_data; + + // Extract Procs, but discard the resulting Expr::Load. + // That Load looks up the pointer, which we won't use here! + + let loc_body = *boxed_body; + + let is_self_recursive = !matches!(recursive, roc_can::expr::Recursive::NotRecursive); + + // this should be a top-level declaration, and hence have no captured symbols + // if we ever do hit this (and it's not a bug), we should make sure to put the + // captured symbols into a CapturedSymbols and give it to PartialProc::from_named_function + debug_assert!(captured_symbols.is_empty()); + + let partial_proc = PartialProc::from_named_function( + env, + function_type, + arguments, + loc_body, + CapturedSymbols::None, + is_self_recursive, + return_type, + ); + + procs.partial_procs.insert(closure_name, partial_proc); +} + +/// Insert a closure that may capture symbols to the list of partial procs +fn register_capturing_closure<'a>( + env: &mut Env<'a, '_>, + procs: &mut Procs<'a>, + layout_cache: &mut LayoutCache<'a>, + closure_name: Symbol, + closure_data: ClosureData, +) { + // the function surrounding the closure definition may be specialized multiple times, + // hence in theory this partial proc may be added multiple times. That would be wasteful + // so we check whether this partial proc is already there. + // + // (the `gen_primitives::task_always_twice` test has this behavior) + if !procs.partial_procs.contains_key(closure_name) { + let ClosureData { + function_type, + return_type, + closure_type, + recursive, + arguments, + loc_body: boxed_body, + captured_symbols, + .. + } = closure_data; + let loc_body = *boxed_body; + + let is_self_recursive = !matches!(recursive, roc_can::expr::Recursive::NotRecursive); + + let captured_symbols = match *env.subs.get_content_without_compacting(function_type) { + Content::Structure(FlatType::Func(args, closure_var, ret)) => { + let lambda_set_layout = { + LambdaSet::from_var_pub( + layout_cache, + env.arena, + env.subs, + args, + closure_var, + ret, + env.target_info, + ) + }; + + match lambda_set_layout { + Ok(lambda_set) => { + if lambda_set.is_represented(&layout_cache.interner).is_none() { + CapturedSymbols::None + } else { + let mut temp = Vec::from_iter_in(captured_symbols, env.arena); + temp.sort(); + CapturedSymbols::Captured(temp.into_bump_slice()) + } + } + Err(_) => { + // just allow this. see https://github.com/roc-lang/roc/issues/1585 + if captured_symbols.is_empty() { + CapturedSymbols::None + } else { + let mut temp = Vec::from_iter_in(captured_symbols, env.arena); + temp.sort(); + CapturedSymbols::Captured(temp.into_bump_slice()) + } + } + } + } + _ => { + // This is a value (zero-argument thunk); it cannot capture any variables. + debug_assert!( + captured_symbols.is_empty(), + "{:?} with layout {:?} {:?} {:?}", + &captured_symbols, + layout_cache.raw_from_var(env.arena, function_type, env.subs,), + env.subs, + (function_type, closure_type), + ); + CapturedSymbols::None + } + }; + + let partial_proc = PartialProc::from_named_function( + env, + function_type, + arguments, + loc_body, + captured_symbols, + is_self_recursive, + return_type, + ); + + procs.partial_procs.insert(closure_name, partial_proc); + } +} + +pub fn from_can<'a>( + env: &mut Env<'a, '_>, + variable: Variable, + can_expr: roc_can::expr::Expr, + procs: &mut Procs<'a>, + layout_cache: &mut LayoutCache<'a>, +) -> Stmt<'a> { + use roc_can::expr::Expr::*; + + match can_expr { + When { + cond_var, + expr_var, + region: _, + loc_cond, + branches, + branches_cond_var: _, + exhaustive, + } => { + let cond_symbol = possible_reuse_symbol_or_specialize( + env, + procs, + layout_cache, + &loc_cond.value, + cond_var, + ); + + let stmt = from_can_when( + env, + cond_var, + expr_var, + cond_symbol, + branches, + exhaustive, + layout_cache, + procs, + None, + ); + + // define the `when` condition + assign_to_symbol( + env, + procs, + layout_cache, + cond_var, + *loc_cond, + cond_symbol, + stmt, + ) + } + If { + cond_var, + branch_var, + branches, + final_else, + } => { + let ret_layout = return_on_layout_error!( + env, + layout_cache.from_var(env.arena, branch_var, env.subs), + "invalid return type in if expression" + ); + let cond_layout = return_on_layout_error!( + env, + layout_cache.from_var(env.arena, cond_var, env.subs), + "invalid condition type in if expression" + ); + + let mut stmt = from_can(env, branch_var, final_else.value, procs, layout_cache); + + for (loc_cond, loc_then) in branches.into_iter().rev() { + let branching_symbol = possible_reuse_symbol_or_specialize( + env, + procs, + layout_cache, + &loc_cond.value, + cond_var, + ); + let then = from_can(env, branch_var, loc_then.value, procs, layout_cache); + + stmt = cond(env, branching_symbol, cond_layout, then, stmt, ret_layout); + + stmt = assign_to_symbol( + env, + procs, + layout_cache, + cond_var, + loc_cond, + branching_symbol, + stmt, + ); + } + + stmt + } + + Expect { + loc_condition, + loc_continuation, + lookups_in_cond, + } => { + let rest = from_can(env, variable, loc_continuation.value, procs, layout_cache); + let cond_symbol = env.unique_symbol(); + + let mut lookups = Vec::with_capacity_in(lookups_in_cond.len(), env.arena); + let mut lookup_variables = Vec::with_capacity_in(lookups_in_cond.len(), env.arena); + let mut specialized_variables = Vec::with_capacity_in(lookups_in_cond.len(), env.arena); + + for ExpectLookup { + symbol, + var, + ability_info, + } in lookups_in_cond.iter().copied() + { + let symbol = match ability_info { + Some(specialization_id) => late_resolve_ability_specialization( + env, + symbol, + Some(specialization_id), + var, + ), + None => symbol, + }; + + let expectation_subs = env + .expectation_subs + .as_deref_mut() + .expect("if expects are compiled, their subs should be available"); + let spec_var = expectation_subs.fresh_unnamed_flex_var(); + + if !env.subs.is_function(var) { + // Exclude functions from lookups + lookups.push(symbol); + lookup_variables.push(var); + specialized_variables.push(spec_var); + } + } + + let specialized_variables = specialized_variables.into_bump_slice(); + + let mut stmt = Stmt::Expect { + condition: cond_symbol, + region: loc_condition.region, + lookups: lookups.into_bump_slice(), + variables: specialized_variables, + remainder: env.arena.alloc(rest), + }; + + stmt = with_hole( + env, + loc_condition.value, + Variable::BOOL, + procs, + layout_cache, + cond_symbol, + env.arena.alloc(stmt), + ); + + // Now that the condition has been specialized, export the specialized types of our + // lookups into the expectation subs. + store_specialized_expectation_lookups(env, lookup_variables, specialized_variables); + + stmt + } + + ExpectFx { + loc_condition, + loc_continuation, + lookups_in_cond, + } => { + let rest = from_can(env, variable, loc_continuation.value, procs, layout_cache); + let cond_symbol = env.unique_symbol(); + + let mut lookups = Vec::with_capacity_in(lookups_in_cond.len(), env.arena); + let mut lookup_variables = Vec::with_capacity_in(lookups_in_cond.len(), env.arena); + let mut specialized_variables = Vec::with_capacity_in(lookups_in_cond.len(), env.arena); + + for ExpectLookup { + symbol, + var, + ability_info, + } in lookups_in_cond.iter().copied() + { + let symbol = match ability_info { + Some(specialization_id) => late_resolve_ability_specialization( + env, + symbol, + Some(specialization_id), + var, + ), + None => symbol, + }; + + let expectation_subs = env + .expectation_subs + .as_deref_mut() + .expect("if expects are compiled, their subs should be available"); + let spec_var = expectation_subs.fresh_unnamed_flex_var(); + + if !env.subs.is_function(var) { + // Exclude functions from lookups + lookups.push(symbol); + lookup_variables.push(var); + specialized_variables.push(spec_var); + } + } + + let specialized_variables = specialized_variables.into_bump_slice(); + + let mut stmt = Stmt::ExpectFx { + condition: cond_symbol, + region: loc_condition.region, + lookups: lookups.into_bump_slice(), + variables: specialized_variables, + remainder: env.arena.alloc(rest), + }; + + stmt = with_hole( + env, + loc_condition.value, + Variable::BOOL, + procs, + layout_cache, + cond_symbol, + env.arena.alloc(stmt), + ); + + store_specialized_expectation_lookups(env, lookup_variables, specialized_variables); + + stmt + } + + Dbg { + loc_message, + loc_continuation, + variable: cond_variable, + symbol: dbg_symbol, + } => { + let rest = from_can(env, variable, loc_continuation.value, procs, layout_cache); + + compile_dbg( + env, + procs, + layout_cache, + dbg_symbol, + *loc_message, + cond_variable, + rest, + ) + } + + LetRec(defs, cont, _cycle_mark) => { + // because Roc is strict, only functions can be recursive! + for def in defs.into_iter() { + if let roc_can::pattern::Pattern::Identifier(symbol) = &def.loc_pattern.value { + // Now that we know for sure it's a closure, get an owned + // version of these variant args so we can use them properly. + match def.loc_expr.value { + Closure(closure_data) => { + register_capturing_closure( + env, + procs, + layout_cache, + *symbol, + closure_data, + ); + + continue; + } + _ => unreachable!("recursive value is not a function"), + } + } + unreachable!("recursive value does not have Identifier pattern") + } + + from_can(env, variable, cont.value, procs, layout_cache) + } + LetNonRec(def, cont) => from_can_let(env, procs, layout_cache, def, cont, variable, None), + _ => { + let symbol = env.unique_symbol(); + let hole = env.arena.alloc(Stmt::Ret(symbol)); + with_hole(env, can_expr, variable, procs, layout_cache, symbol, hole) + } + } +} + +fn store_specialized_expectation_lookups( + env: &mut Env, + lookup_variables: impl IntoIterator, + specialized_variables: &[Variable], +) { + let subs = &env.subs; + let expectation_subs = env.expectation_subs.as_deref_mut().unwrap(); + for (lookup_var, stored_var) in lookup_variables.into_iter().zip(specialized_variables) { + let stored_specialized_var = + storage_copy_var_to(&mut Default::default(), subs, expectation_subs, lookup_var); + let stored_specialized_desc = expectation_subs.get(stored_specialized_var); + expectation_subs.union(*stored_var, stored_specialized_var, stored_specialized_desc); + } +} + +fn to_opt_branches<'a>( + env: &mut Env<'a, '_>, + procs: &mut Procs<'a>, + branches: std::vec::Vec, + exhaustive_mark: ExhaustiveMark, + layout_cache: &mut LayoutCache<'a>, +) -> std::vec::Vec<( + Pattern<'a>, + Option>, + roc_can::expr::Expr, +)> { + debug_assert!(!branches.is_empty()); + + let mut opt_branches = std::vec::Vec::new(); + + for when_branch in branches { + if when_branch.redundant.is_redundant(env.subs) { + // Don't codegen this branch since it's redundant. + continue; + } + + for loc_pattern in when_branch.patterns { + match from_can_pattern(env, procs, layout_cache, &loc_pattern.pattern.value) { + Ok((mono_pattern, assignments)) => { + let loc_expr = if !loc_pattern.degenerate { + let mut loc_expr = when_branch.value.clone(); + + let region = loc_pattern.pattern.region; + for (symbol, variable, expr) in assignments.into_iter().rev() { + let def = roc_can::def::Def { + annotation: None, + expr_var: variable, + loc_expr: Loc::at(region, expr), + loc_pattern: Loc::at( + region, + roc_can::pattern::Pattern::Identifier(symbol), + ), + pattern_vars: std::iter::once((symbol, variable)).collect(), + }; + let new_expr = + roc_can::expr::Expr::LetNonRec(Box::new(def), Box::new(loc_expr)); + loc_expr = Loc::at(region, new_expr); + } + + loc_expr + } else { + // This pattern is degenerate; when it's reached we must emit a runtime + // error. + Loc::at_zero(roc_can::expr::Expr::RuntimeError( + RuntimeError::DegenerateBranch(loc_pattern.pattern.region), + )) + }; + + // TODO remove clone? + opt_branches.push((mono_pattern, when_branch.guard.clone(), loc_expr.value)); + } + Err(runtime_error) => { + // TODO remove clone? + opt_branches.push(( + Pattern::Underscore, + when_branch.guard.clone(), + roc_can::expr::Expr::RuntimeError(runtime_error), + )); + } + } + } + } + + if exhaustive_mark.is_non_exhaustive(env.subs) { + // In contrast to elm (currently), we still do codegen even if a pattern is non-exhaustive. + // So we not only report exhaustiveness errors, but also correct them + opt_branches.push(( + Pattern::Underscore, + None, + roc_can::expr::Expr::RuntimeError(roc_problem::can::RuntimeError::NonExhaustivePattern), + )); + } + + opt_branches +} + +#[allow(clippy::too_many_arguments)] +fn from_can_when<'a>( + env: &mut Env<'a, '_>, + cond_var: Variable, + expr_var: Variable, + cond_symbol: Symbol, + branches: std::vec::Vec, + exhaustive_mark: ExhaustiveMark, + layout_cache: &mut LayoutCache<'a>, + procs: &mut Procs<'a>, + join_point: Option, +) -> Stmt<'a> { + if branches.is_empty() { + // A when-expression with no branches is a runtime error. + // We can't know what to return! + return runtime_error(env, "Hit a 0-branch when expression"); + } + let opt_branches = to_opt_branches(env, procs, branches, exhaustive_mark, layout_cache); + + let cond_layout = return_on_layout_error!( + env, + layout_cache.from_var(env.arena, cond_var, env.subs), + "from_can_when cond_layout" + ); + + let ret_layout = return_on_layout_error!( + env, + layout_cache.from_var(env.arena, expr_var, env.subs), + "from_can_when ret_layout" + ); + + let arena = env.arena; + let it = opt_branches + .into_iter() + .filter_map(|(pattern, opt_guard, can_expr)| { + // If the pattern has a void layout we can drop it; however, we must still perform the + // work of building the body, because that may contain specializations we must + // discover for use elsewhere. See + // `unreachable_branch_is_eliminated_but_produces_lambda_specializations` in test_mono + // for an example. + let should_eliminate_branch = pattern.is_voided(); + + // If we're going to eliminate the branch, we need to take a snapshot of the symbol + // specializations before we enter the branch, because any new specializations that + // will be added in the branch body will never need to be resolved! + let specialization_symbol_snapshot = if should_eliminate_branch { + Some(std::mem::take(&mut procs.symbol_specializations)) + } else { + None + }; + + let branch_stmt = match join_point { + None => from_can(env, expr_var, can_expr, procs, layout_cache), + Some(id) => { + let symbol = env.unique_symbol(); + let arguments = bumpalo::vec![in env.arena; symbol].into_bump_slice(); + let jump = env.arena.alloc(Stmt::Jump(id, arguments)); + + with_hole(env, can_expr, expr_var, procs, layout_cache, symbol, jump) + } + }; + + use decision_tree::Guard; + let result = if let Some(loc_expr) = opt_guard { + let guard_spec = GuardStmtSpec { + guard_expr: loc_expr.value, + identity: env.next_call_specialization_id(), + }; + + ( + pattern.clone(), + Guard::Guard { + pattern, + stmt_spec: guard_spec, + }, + branch_stmt, + ) + } else { + (pattern, Guard::NoGuard, branch_stmt) + }; + + if should_eliminate_branch { + procs.symbol_specializations = specialization_symbol_snapshot.unwrap(); + None + } else { + Some(result) + } + }); + let mono_branches = Vec::from_iter_in(it, arena); + + decision_tree::optimize_when( + env, + procs, + layout_cache, + cond_symbol, + cond_layout, + ret_layout, + mono_branches, + ) +} + +/// A functor to generate IR for a guard under a `when` branch. +/// Used in the decision tree compiler, after building a decision tree and converting into IR. +/// +/// A guard might appear more than once in various places in the compiled decision tree, so the +/// functor here may be called more than once. As such, it implements clone, which duplicates the +/// guard AST for subsequent IR-regeneration. This is a bit wasteful, but in practice, guard ASTs +/// are quite small. Moreoever, they must be generated on a per-case basis, since the guard may +/// have calls or joins, whose specialization IDs and joinpoint IDs, respectively, must be unique. +#[derive(Debug, Clone)] +pub(crate) struct GuardStmtSpec { + guard_expr: roc_can::expr::Expr, + + /// Unique id to indentity identical guard statements, even across clones. + /// Needed so that we can implement [PartialEq] on this type. Re-uses call specialization IDs, + /// since the identity is kind of irrelevant. + identity: CallSpecId, +} + +impl PartialEq for GuardStmtSpec { + fn eq(&self, other: &Self) -> bool { + self.identity == other.identity + } +} + +impl std::hash::Hash for GuardStmtSpec { + fn hash(&self, state: &mut H) { + self.identity.id.hash(state); + } +} + +impl GuardStmtSpec { + /// Generates IR for the guard, and the joinpoint that the guard will jump to with the + /// calculated guard boolean value. + /// + /// The caller should create a joinpoint with the given joinpoint ID and decide how to branch + /// after the guard has been evaluated. + /// + /// The compiled guard statement expects the pattern before the guard to be destructed before the + /// returned statement. The caller should layer on the pattern destructuring, as bound from the + /// `when` condition value. + pub(crate) fn generate_guard_and_join<'a>( + self, + env: &mut Env<'a, '_>, + procs: &mut Procs<'a>, + layout_cache: &mut LayoutCache<'a>, + ) -> CompiledGuardStmt<'a> { + let Self { + guard_expr, + identity: _, + } = self; + + let join_point_id = JoinPointId(env.unique_symbol()); + let symbol = env.unique_symbol(); + let jump = env + .arena + .alloc(Stmt::Jump(join_point_id, env.arena.alloc([symbol]))); + + let stmt = with_hole( + env, + guard_expr, + Variable::BOOL, + procs, + layout_cache, + symbol, + jump, + ); + + CompiledGuardStmt { + join_point_id, + stmt, + } + } +} + +pub(crate) struct CompiledGuardStmt<'a> { + pub join_point_id: JoinPointId, + pub stmt: Stmt<'a>, +} + +fn substitute(substitutions: &BumpMap, s: Symbol) -> Option { + match substitutions.get(&s) { + Some(new) => { + debug_assert!(!substitutions.contains_key(new)); + Some(*new) + } + None => None, + } +} + +fn substitute_in_exprs<'a>(arena: &'a Bump, stmt: &mut Stmt<'a>, from: Symbol, to: Symbol) { + let mut subs = BumpMap::with_capacity_in(1, arena); + subs.insert(from, to); + + // TODO clean this up + let ref_stmt = arena.alloc(stmt.clone()); + if let Some(new) = substitute_in_stmt_help(arena, ref_stmt, &subs) { + *stmt = new.clone(); + } +} + +pub(crate) fn substitute_in_exprs_many<'a>( + arena: &'a Bump, + stmt: &mut Stmt<'a>, + subs: BumpMap, +) { + // TODO clean this up + let ref_stmt = arena.alloc(stmt.clone()); + if let Some(new) = substitute_in_stmt_help(arena, ref_stmt, &subs) { + *stmt = new.clone(); + } +} + +fn substitute_in_stmt_help<'a>( + arena: &'a Bump, + stmt: &'a Stmt<'a>, + subs: &BumpMap, +) -> Option<&'a Stmt<'a>> { + use Stmt::*; + + match stmt { + Let(symbol, expr, layout, cont) => { + let opt_cont = substitute_in_stmt_help(arena, cont, subs); + let opt_expr = substitute_in_expr(arena, expr, subs); + + if opt_expr.is_some() || opt_cont.is_some() { + let cont = opt_cont.unwrap_or(cont); + let expr = opt_expr.unwrap_or_else(|| expr.clone()); + + Some(arena.alloc(Let(*symbol, expr, *layout, cont))) + } else { + None + } + } + Join { + id, + parameters, + remainder, + body: continuation, + } => { + let opt_remainder = substitute_in_stmt_help(arena, remainder, subs); + let opt_continuation = substitute_in_stmt_help(arena, continuation, subs); + + if opt_remainder.is_some() || opt_continuation.is_some() { + let remainder = opt_remainder.unwrap_or(remainder); + let continuation = opt_continuation.unwrap_or(*continuation); + + Some(arena.alloc(Join { + id: *id, + parameters, + remainder, + body: continuation, + })) + } else { + None + } + } + Switch { + cond_symbol, + cond_layout, + branches, + default_branch, + ret_layout, + } => { + let mut did_change = false; + + let cond_symbol = match substitute(subs, *cond_symbol) { + Some(s) => { + did_change = true; + s + } + None => *cond_symbol, + }; + + let opt_default = substitute_in_stmt_help(arena, default_branch.1, subs); + + let opt_branches = Vec::from_iter_in( + branches.iter().map(|(label, info, branch)| { + match substitute_in_stmt_help(arena, branch, subs) { + None => None, + Some(branch) => { + did_change = true; + Some((*label, info.clone(), branch.clone())) + } + } + }), + arena, + ); + + if opt_default.is_some() || did_change { + let default_branch = ( + default_branch.0.clone(), + opt_default.unwrap_or(default_branch.1), + ); + + let branches = if did_change { + let new = Vec::from_iter_in( + opt_branches.into_iter().zip(branches.iter()).map( + |(opt_branch, branch)| match opt_branch { + None => branch.clone(), + Some(new_branch) => new_branch, + }, + ), + arena, + ); + + new.into_bump_slice() + } else { + branches + }; + + Some(arena.alloc(Switch { + cond_symbol, + cond_layout: *cond_layout, + default_branch, + branches, + ret_layout: *ret_layout, + })) + } else { + None + } + } + Ret(s) => match substitute(subs, *s) { + Some(s) => Some(arena.alloc(Ret(s))), + None => None, + }, + Refcounting(modify, cont) => { + // TODO should we substitute in the ModifyRc? + match substitute_in_stmt_help(arena, cont, subs) { + Some(cont) => Some(arena.alloc(Refcounting(*modify, cont))), + None => None, + } + } + + Dbg { + symbol, + variable, + remainder, + } => { + let new_remainder = + substitute_in_stmt_help(arena, remainder, subs).unwrap_or(remainder); + + let expect = Dbg { + symbol: substitute(subs, *symbol).unwrap_or(*symbol), + variable: *variable, + remainder: new_remainder, + }; + + Some(arena.alloc(expect)) + } + + Expect { + condition, + region, + lookups, + variables, + remainder, + } => { + let new_remainder = + substitute_in_stmt_help(arena, remainder, subs).unwrap_or(remainder); + + let new_lookups = Vec::from_iter_in( + lookups.iter().map(|s| substitute(subs, *s).unwrap_or(*s)), + arena, + ); + + let expect = Expect { + condition: substitute(subs, *condition).unwrap_or(*condition), + region: *region, + lookups: new_lookups.into_bump_slice(), + variables, + remainder: new_remainder, + }; + + Some(arena.alloc(expect)) + } + + ExpectFx { + condition, + region, + lookups, + variables, + remainder, + } => { + let new_remainder = + substitute_in_stmt_help(arena, remainder, subs).unwrap_or(remainder); + + let new_lookups = Vec::from_iter_in( + lookups.iter().map(|s| substitute(subs, *s).unwrap_or(*s)), + arena, + ); + + let expect = ExpectFx { + condition: substitute(subs, *condition).unwrap_or(*condition), + region: *region, + lookups: new_lookups.into_bump_slice(), + variables, + remainder: new_remainder, + }; + + Some(arena.alloc(expect)) + } + + Jump(id, args) => { + let mut did_change = false; + let new_args = Vec::from_iter_in( + args.iter().map(|s| match substitute(subs, *s) { + None => *s, + Some(s) => { + did_change = true; + s + } + }), + arena, + ); + + if did_change { + let args = new_args.into_bump_slice(); + + Some(arena.alloc(Jump(*id, args))) + } else { + None + } + } + Crash(msg, tag) => substitute(subs, *msg).map(|new| &*arena.alloc(Crash(new, *tag))), + } +} + +fn substitute_in_call<'a>( + arena: &'a Bump, + call: &'a Call<'a>, + subs: &BumpMap, +) -> Option> { + let Call { + call_type, + arguments, + } = call; + + let opt_call_type = match call_type { + CallType::ByName { + name, + arg_layouts, + ret_layout, + specialization_id, + } => substitute(subs, name.name()).map(|new| CallType::ByName { + name: name.replace_name(new), + arg_layouts, + ret_layout: *ret_layout, + specialization_id: *specialization_id, + }), + CallType::ByPointer { + pointer, + arg_layouts, + ret_layout, + } => substitute(subs, *pointer).map(|new| CallType::ByPointer { + pointer: new, + arg_layouts, + ret_layout: *ret_layout, + }), + CallType::Foreign { .. } => None, + CallType::LowLevel { .. } => None, + CallType::HigherOrder { .. } => None, + }; + + let mut did_change = false; + let new_args = Vec::from_iter_in( + arguments.iter().map(|s| match substitute(subs, *s) { + None => *s, + Some(s) => { + did_change = true; + s + } + }), + arena, + ); + + if did_change || opt_call_type.is_some() { + let call_type = opt_call_type.unwrap_or_else(|| call_type.clone()); + + let arguments = new_args.into_bump_slice(); + + Some(self::Call { + call_type, + arguments, + }) + } else { + None + } +} + +fn substitute_in_expr<'a>( + arena: &'a Bump, + expr: &'a Expr<'a>, + subs: &BumpMap, +) -> Option> { + use Expr::*; + + match expr { + Literal(_) | EmptyArray | RuntimeErrorFunction(_) => None, + + Call(call) => substitute_in_call(arena, call, subs).map(Expr::Call), + + Tag { + tag_layout, + tag_id, + arguments: args, + reuse, + } => { + let mut did_change = false; + let new_args = Vec::from_iter_in( + args.iter().map(|s| match substitute(subs, *s) { + None => *s, + Some(s) => { + did_change = true; + s + } + }), + arena, + ); + + let reuse = match *reuse { + Some(mut ru) => match substitute(subs, ru.symbol) { + Some(s) => { + did_change = true; + ru.symbol = s; + Some(ru) + } + None => Some(ru), + }, + None => None, + }; + + if did_change { + let arguments = new_args.into_bump_slice(); + + Some(Tag { + tag_layout: *tag_layout, + tag_id: *tag_id, + arguments, + reuse, + }) + } else { + None + } + } + + NullPointer => None, + + Reset { .. } | ResetRef { .. } => { + unreachable!("reset(ref) has not been introduced yet") + } + + Struct(args) => { + let mut did_change = false; + let new_args = Vec::from_iter_in( + args.iter().map(|s| match substitute(subs, *s) { + None => *s, + Some(s) => { + did_change = true; + s + } + }), + arena, + ); + + if did_change { + let args = new_args.into_bump_slice(); + + Some(Struct(args)) + } else { + None + } + } + + Array { + elems: args, + elem_layout, + } => { + let mut did_change = false; + let new_args = Vec::from_iter_in( + args.iter().map(|e| { + if let ListLiteralElement::Symbol(s) = e { + match substitute(subs, *s) { + None => ListLiteralElement::Symbol(*s), + Some(s) => { + did_change = true; + ListLiteralElement::Symbol(s) + } + } + } else { + *e + } + }), + arena, + ); + + if did_change { + let args = new_args.into_bump_slice(); + + Some(Array { + elem_layout: *elem_layout, + elems: args, + }) + } else { + None + } + } + + ErasedMake { value, callee } => { + match ( + value.and_then(|v| substitute(subs, v)), + substitute(subs, *callee), + ) { + (None, None) => None, + (Some(value), None) => Some(ErasedMake { + value: Some(value), + callee: *callee, + }), + (None, Some(callee)) => Some(ErasedMake { + value: *value, + callee, + }), + (Some(value), Some(callee)) => Some(ErasedMake { + value: Some(value), + callee, + }), + } + } + + ErasedLoad { symbol, field } => substitute(subs, *symbol).map(|new_symbol| ErasedLoad { + symbol: new_symbol, + field: *field, + }), + + FunctionPointer { .. } => None, + + StructAtIndex { + index, + structure, + field_layouts, + } => match substitute(subs, *structure) { + Some(structure) => Some(StructAtIndex { + index: *index, + field_layouts, + structure, + }), + None => None, + }, + + GetTagId { + structure, + union_layout, + } => match substitute(subs, *structure) { + Some(structure) => Some(GetTagId { + structure, + union_layout: *union_layout, + }), + None => None, + }, + + UnionAtIndex { + structure, + tag_id, + index, + union_layout, + } => match substitute(subs, *structure) { + Some(structure) => Some(UnionAtIndex { + structure, + tag_id: *tag_id, + index: *index, + union_layout: *union_layout, + }), + None => None, + }, + + // currently only used for tail recursion modulo cons (TRMC) + GetElementPointer { + structure, + indices, + union_layout, + } => match substitute(subs, *structure) { + Some(structure) => Some(GetElementPointer { + structure, + indices, + union_layout: *union_layout, + }), + None => None, + }, + + Alloca { + element_layout, + initializer, + } => match substitute(subs, (*initializer)?) { + Some(initializer) => Some(Alloca { + element_layout: *element_layout, + initializer: Some(initializer), + }), + None => None, + }, + } +} + +/// We want to re-use symbols that are not function symbols +/// for any other expression, we create a new symbol, and will +/// later make sure it gets assigned the correct value. +#[derive(Debug)] +enum ReuseSymbol { + Imported(Symbol), + LocalFunction(Symbol), + Value(Symbol), + UnspecializedExpr(Symbol), + NotASymbol, +} + +fn can_reuse_symbol<'a>( + env: &mut Env<'a, '_>, + layout_cache: &mut LayoutCache<'a>, + procs: &mut Procs<'a>, + expr: &roc_can::expr::Expr, + expr_var: Variable, +) -> ReuseSymbol { + use roc_can::expr::Expr::*; + use ReuseSymbol::*; + + let symbol = match expr { + AbilityMember(member, specialization_id, _) => { + late_resolve_ability_specialization(env, *member, *specialization_id, expr_var) + } + Var(symbol, _) => *symbol, + RecordAccess { + record_var, + field, + loc_expr, + .. + } => { + let sorted_fields_result = { + let mut layout_env = layout::Env::from_components( + layout_cache, + env.subs, + env.arena, + env.target_info, + ); + layout::sort_record_fields(&mut layout_env, *record_var) + }; + + let sorted_fields = match sorted_fields_result { + Ok(fields) => fields, + Err(_) => unreachable!("Can't access record with improper layout"), + }; + + let index = sorted_fields + .into_iter() + .enumerate() + .find_map(|(current, (label, _, _))| (label == *field).then_some(current)); + + let struct_index = index.expect("field not in its own type"); + + let struct_symbol = possible_reuse_symbol_or_specialize( + env, + procs, + layout_cache, + &loc_expr.value, + *record_var, + ); + + match env + .struct_indexing + .get((struct_symbol, struct_index as u64)) + { + Some(symbol) => *symbol, + None => { + return NotASymbol; + } + } + } + _ => return NotASymbol, + }; + + let arguments = [ + Symbol::ARG_1, + Symbol::ARG_2, + Symbol::ARG_3, + Symbol::ARG_4, + Symbol::ARG_5, + Symbol::ARG_6, + Symbol::ARG_7, + ]; + + if arguments.contains(&symbol) { + Value(symbol) + } else if env.is_imported_symbol(symbol) || env.is_unloaded_derived_symbol(symbol, procs) { + Imported(symbol) + } else if procs.partial_procs.contains_key(symbol) { + LocalFunction(symbol) + } else if procs.ability_member_aliases.get(symbol).is_some() { + UnspecializedExpr(symbol) + } else { + Value(symbol) + } +} + +fn possible_reuse_symbol_or_specialize<'a>( + env: &mut Env<'a, '_>, + procs: &mut Procs<'a>, + layout_cache: &mut LayoutCache<'a>, + expr: &roc_can::expr::Expr, + var: Variable, +) -> Symbol { + match can_reuse_symbol(env, layout_cache, procs, expr, var) { + ReuseSymbol::Value(symbol) => { + procs.get_or_insert_symbol_specialization(env, layout_cache, symbol, var) + } + _ => env.unique_symbol(), + } +} + +fn handle_variable_aliasing<'a, BuildRest>( + env: &mut Env<'a, '_>, + procs: &mut Procs<'a>, + layout_cache: &mut LayoutCache<'a>, + variable: Variable, + left: Symbol, + right: Symbol, + build_rest: BuildRest, +) -> Stmt<'a> +where + BuildRest: FnOnce(&mut Env<'a, '_>, &mut Procs<'a>, &mut LayoutCache<'a>) -> Stmt<'a>, +{ + // 1. Handle references to ability members - we could be aliasing an ability member, or another + // alias to an ability member. + { + let is_ability_member = env + .abilities + .with_module_abilities_store(env.home, |store| store.is_ability_member_name(right)); + + if is_ability_member { + procs + .ability_member_aliases + .insert(left, AbilityMember(right)); + return build_rest(env, procs, layout_cache); + } + if let Some(&ability_member) = procs.ability_member_aliases.get(right) { + procs.ability_member_aliases.insert(left, ability_member); + return build_rest(env, procs, layout_cache); + } + } + + // We should never reference a partial proc - instead, we want to generate closure data and + // leave it there, even if the lambda set is unary. That way, we avoid having to try to resolve + // lambda set of the proc based on the symbol name, which can cause many problems! + // See my git blame for details. + debug_assert!(!procs.partial_procs.contains_key(right)); + + let result = build_rest(env, procs, layout_cache); + + if procs.is_imported_module_thunk(right) { + // if this is an imported symbol, then we must make sure it is + // specialized, and wrap the original in a function pointer. + add_needed_external(procs, env, variable, LambdaName::no_niche(right)); + + let res_layout = layout_cache.from_var(env.arena, variable, env.subs); + let layout = return_on_layout_error!(env, res_layout, "handle_variable_aliasing"); + + force_thunk(env, right, layout, left, env.arena.alloc(result)) + } else if env.is_imported_symbol(right) { + // if this is an imported symbol, then we must make sure it is + // specialized, and wrap the original in a function pointer. + add_needed_external(procs, env, variable, LambdaName::no_niche(right)); + + // then we must construct its closure; since imported symbols have no closure, we use the empty struct + let_empty_struct(left, env.arena.alloc(result)) + } else { + let mut result = result; + substitute_in_exprs(env.arena, &mut result, left, right); + result + } +} + +fn force_thunk<'a>( + env: &mut Env<'a, '_>, + thunk_name: Symbol, + layout: InLayout<'a>, + assigned: Symbol, + hole: &'a Stmt<'a>, +) -> Stmt<'a> { + let call = self::Call { + call_type: CallType::ByName { + name: LambdaName::no_niche(thunk_name), + ret_layout: layout, + arg_layouts: &[], + specialization_id: env.next_call_specialization_id(), + }, + arguments: &[], + }; + + build_call(env, call, assigned, layout, env.arena.alloc(hole)) +} + +fn let_empty_struct<'a>(assigned: Symbol, hole: &'a Stmt<'a>) -> Stmt<'a> { + Stmt::Let(assigned, Expr::Struct(&[]), Layout::UNIT, hole) +} + +/// If the symbol is a function or polymorphic value, make sure it is properly specialized +fn specialize_symbol<'a>( + env: &mut Env<'a, '_>, + procs: &mut Procs<'a>, + layout_cache: &mut LayoutCache<'a>, + arg_var: Option, + assign_to: Symbol, + result: &'a Stmt<'a>, + original: Symbol, +) -> Stmt<'a> { + match procs.get_partial_proc(original) { + None => { + match arg_var { + Some(arg_var) + if env.is_imported_symbol(original) + || env.is_unloaded_derived_symbol(original, procs) => + { + let raw = match layout_cache.raw_from_var(env.arena, arg_var, env.subs) { + Ok(v) => v, + Err(e) => return_on_layout_error_help!(env, e, "specialize_symbol"), + }; + + match raw { + RawFunctionLayout::Function(_, lambda_set, _) + if !procs.is_imported_module_thunk(original) => + { + let lambda_name = + find_lambda_name(env, layout_cache, lambda_set, original, &[]); + + debug_assert!( + lambda_name.no_captures(), + "imported functions are top-level and should never capture" + ); + + let function_ptr_layout = + ProcLayout::from_raw_named(env.arena, lambda_name, raw); + procs.insert_passed_by_name( + env, + arg_var, + lambda_name, + function_ptr_layout, + layout_cache, + ); + + construct_closure_data( + env, + procs, + layout_cache, + lambda_set, + lambda_name, + &[], + assign_to, + env.arena.alloc(result), + ) + } + _ => { + // This is an imported ZAT that returns either a value, or the closure + // data for a lambda set. + let layout = match raw { + RawFunctionLayout::ZeroArgumentThunk(layout) => layout, + RawFunctionLayout::ErasedFunction(..) => todo_lambda_erasure!(), + RawFunctionLayout::Function(_, lambda_set, _) => layout_cache + .put_in_direct_no_semantic(LayoutRepr::LambdaSet(lambda_set)), + }; + + let raw = RawFunctionLayout::ZeroArgumentThunk(layout); + let lambda_name = LambdaName::no_niche(original); + let top_level = ProcLayout::from_raw_named(env.arena, lambda_name, raw); + + procs.insert_passed_by_name( + env, + arg_var, + lambda_name, + top_level, + layout_cache, + ); + + force_thunk(env, original, layout, assign_to, env.arena.alloc(result)) + } + } + } + + _ => { + // danger: a foreign symbol may not be specialized! + debug_assert!( + !env.is_imported_symbol(original), + "symbol {:?} while processing module {:?}", + original, + (env.home, &arg_var), + ); + + // Replaces references of `assign_to` in the rest of the block with `original`, + // since we don't actually need to specialize the original symbol to a value. + // + // This usually means we are using a symbol received from a joinpoint. + let mut result = result.clone(); + substitute_in_exprs(env.arena, &mut result, assign_to, original); + result + } + } + } + + Some(partial_proc) => { + let arg_var = arg_var.unwrap_or(partial_proc.annotation); + // this symbol is a function, that is used by-name (e.g. as an argument to another + // function). Register it with the current variable, then create a function pointer + // to it in the IR. + let res_layout = return_on_layout_error!( + env, + layout_cache.raw_from_var(env.arena, arg_var, env.subs), + "specialize_symbol res_layout" + ); + + // we have three kinds of functions really. Plain functions, closures by capture, + // and closures by unification. Here we record whether this function captures + // anything. + let captures = partial_proc.captured_symbols.captures(); + let captured = partial_proc.captured_symbols; + + match res_layout { + RawFunctionLayout::Function(_, lambda_set, _) => { + if captures { + let symbols = match captured { + CapturedSymbols::Captured(captured_symbols) => { + Vec::from_iter_in(captured_symbols.iter(), env.arena) + .into_bump_slice() + } + CapturedSymbols::None => unreachable!(), + }; + + let lambda_name = find_lambda_name( + env, + layout_cache, + lambda_set, + original, + symbols.iter().copied(), + ); + + // define the function pointer + let function_ptr_layout = + ProcLayout::from_raw_named(env.arena, lambda_name, res_layout); + + // this is a closure by capture, meaning it itself captures local variables. + procs.insert_passed_by_name( + env, + arg_var, + lambda_name, + function_ptr_layout, + layout_cache, + ); + + let closure_data = assign_to; + + construct_closure_data( + env, + procs, + layout_cache, + lambda_set, + lambda_name, + symbols.iter().copied(), + closure_data, + env.arena.alloc(result), + ) + } else if procs.is_module_thunk(original) { + // this is a 0-argument thunk + + // TODO suspicious + // let layout = Layout::Closure(argument_layouts, lambda_set, ret_layout); + // panic!("suspicious"); + let layout = lambda_set.full_layout; + let top_level = ProcLayout::new(env.arena, &[], Niche::NONE, layout); + procs.insert_passed_by_name( + env, + arg_var, + LambdaName::no_niche(original), + top_level, + layout_cache, + ); + + force_thunk(env, original, layout, assign_to, env.arena.alloc(result)) + } else { + // even though this function may not itself capture, + // unification may still cause it to have an extra argument + let lambda_name = + find_lambda_name(env, layout_cache, lambda_set, original, &[]); + + debug_assert!(lambda_name.no_captures()); + + // define the function pointer + let function_ptr_layout = + ProcLayout::from_raw_named(env.arena, lambda_name, res_layout); + + procs.insert_passed_by_name( + env, + arg_var, + lambda_name, + function_ptr_layout, + layout_cache, + ); + + construct_closure_data( + env, + procs, + layout_cache, + lambda_set, + lambda_name, + &[], + assign_to, + env.arena.alloc(result), + ) + } + } + RawFunctionLayout::ErasedFunction(argument_layouts, ret_layout) => { + let erased_lambda = erased::ResolvedErasedLambda::new( + env, + layout_cache, + original, + captured, + argument_layouts, + ret_layout, + ); + let lambda_name = erased_lambda.lambda_name(); + + let proc_layout = + ProcLayout::from_raw_named(env.arena, lambda_name, res_layout); + + procs.insert_passed_by_name( + env, + arg_var, + lambda_name, + proc_layout, + layout_cache, + ); + + erased::build_erased_function( + env, + layout_cache, + erased_lambda, + assign_to, + result, + ) + } + RawFunctionLayout::ZeroArgumentThunk(ret_layout) => { + // this is a 0-argument thunk + let top_level = ProcLayout::new(env.arena, &[], Niche::NONE, ret_layout); + procs.insert_passed_by_name( + env, + arg_var, + LambdaName::no_niche(original), + top_level, + layout_cache, + ); + + force_thunk( + env, + original, + ret_layout, + assign_to, + env.arena.alloc(result), + ) + } + } + } + } +} + +fn assign_to_symbol<'a>( + env: &mut Env<'a, '_>, + procs: &mut Procs<'a>, + layout_cache: &mut LayoutCache<'a>, + arg_var: Variable, + loc_arg: Loc, + symbol: Symbol, + result: Stmt<'a>, +) -> Stmt<'a> { + use ReuseSymbol::*; + match can_reuse_symbol(env, layout_cache, procs, &loc_arg.value, arg_var) { + Imported(original) | LocalFunction(original) | UnspecializedExpr(original) => { + // for functions we must make sure they are specialized correctly + specialize_symbol( + env, + procs, + layout_cache, + Some(arg_var), + symbol, + env.arena.alloc(result), + original, + ) + } + Value(_symbol) => result, + NotASymbol => with_hole( + env, + loc_arg.value, + arg_var, + procs, + layout_cache, + symbol, + env.arena.alloc(result), + ), + } +} + +fn assign_to_symbols<'a, I>( + env: &mut Env<'a, '_>, + procs: &mut Procs<'a>, + layout_cache: &mut LayoutCache<'a>, + iter: I, + mut result: Stmt<'a>, +) -> Stmt<'a> +where + I: Iterator), &'a Symbol)>, +{ + for ((arg_var, loc_arg), symbol) in iter { + result = assign_to_symbol(env, procs, layout_cache, arg_var, loc_arg, *symbol, result); + } + + result +} + +fn add_needed_external<'a>( + procs: &mut Procs<'a>, + env: &mut Env<'a, '_>, + fn_var: Variable, + name: LambdaName<'a>, +) { + // call of a function that is not in this module + use hashbrown::hash_map::Entry::{Occupied, Vacant}; + + let existing = match procs.externals_we_need.entry(name.name().module_id()) { + Vacant(entry) => entry.insert(ExternalSpecializations::new()), + Occupied(entry) => entry.into_mut(), + }; + + roc_tracing::debug!(proc_name = ?name, ?fn_var, fn_content = ?roc_types::subs::SubsFmtContent(env.subs.get_content_without_compacting(fn_var), env.subs), "needed external"); + + existing.insert_external(name, env.subs, fn_var); +} + +fn build_call<'a>( + _env: &mut Env<'a, '_>, + call: Call<'a>, + assigned: Symbol, + return_layout: InLayout<'a>, + hole: &'a Stmt<'a>, +) -> Stmt<'a> { + Stmt::Let(assigned, Expr::Call(call), return_layout, hole) +} + +/// See https://github.com/roc-lang/roc/issues/1549 +/// +/// What happened is that a function has a type error, but the arguments are not processed. +/// That means specializations were missing. Normally that is not a problem, but because +/// of our closure strategy, internal functions can "leak". That's what happened here. +/// +/// The solution is to evaluate the arguments as normal, and only when calling the function give an error +fn evaluate_arguments_then_runtime_error<'a>( + env: &mut Env<'a, '_>, + procs: &mut Procs<'a>, + layout_cache: &mut LayoutCache<'a>, + msg: String, + loc_args: std::vec::Vec<(Variable, Loc)>, +) -> Stmt<'a> { + let arena = env.arena; + + // eventually we will throw this runtime error + let result = runtime_error(env, env.arena.alloc(msg)); + + // but, we also still evaluate and specialize the arguments to give better error messages + let arg_symbols = Vec::from_iter_in( + loc_args.iter().map(|(var, arg_expr)| { + possible_reuse_symbol_or_specialize(env, procs, layout_cache, &arg_expr.value, *var) + }), + arena, + ) + .into_bump_slice(); + + let iter = loc_args.into_iter().rev().zip(arg_symbols.iter().rev()); + assign_to_symbols(env, procs, layout_cache, iter, result) +} + +#[allow(clippy::too_many_arguments)] +fn call_by_name<'a>( + env: &mut Env<'a, '_>, + procs: &mut Procs<'a>, + fn_var: Variable, + proc_name: Symbol, + loc_args: std::vec::Vec<(Variable, Loc)>, + layout_cache: &mut LayoutCache<'a>, + assigned: Symbol, + hole: &'a Stmt<'a>, +) -> Stmt<'a> { + // Register a pending_specialization for this function + match layout_cache.raw_from_var(env.arena, fn_var, env.subs) { + Err(LayoutProblem::UnresolvedTypeVar(var)) => { + let msg = format!( + "Hit an unresolved type variable {var:?} when creating a layout for {proc_name:?} (var {fn_var:?})" + ); + + evaluate_arguments_then_runtime_error(env, procs, layout_cache, msg, loc_args) + } + Err(LayoutProblem::Erroneous) => { + let msg = format!("Hit an erroneous type when creating a layout for {proc_name:?}"); + + evaluate_arguments_then_runtime_error(env, procs, layout_cache, msg, loc_args) + } + Ok(RawFunctionLayout::Function(arg_layouts, lambda_set, ret_layout)) => { + if procs.is_module_thunk(proc_name) { + if loc_args.is_empty() { + call_by_name_module_thunk( + env, + procs, + fn_var, + proc_name, + lambda_set.full_layout, + layout_cache, + assigned, + hole, + ) + } else { + // here we turn a call to a module thunk into forcing of that thunk + // the thunk represents the closure environment for the body, so we then match + // on the closure environment to perform the call that the body represents. + // + // Example: + // + // > main = parseA "foo" "bar" + // > parseA = Str.concat + + let closure_data_symbol = env.unique_symbol(); + + let arena = env.arena; + let arg_symbols = Vec::from_iter_in( + loc_args.iter().map(|(arg_var, arg_expr)| { + possible_reuse_symbol_or_specialize( + env, + procs, + layout_cache, + &arg_expr.value, + *arg_var, + ) + }), + arena, + ) + .into_bump_slice(); + + debug_assert_eq!(arg_symbols.len(), arg_layouts.len()); + + let result = match_on_lambda_set( + env, + layout_cache, + procs, + lambda_set, + closure_data_symbol, + arg_symbols, + arg_layouts, + ret_layout, + assigned, + hole, + ); + + let result = call_by_name_module_thunk( + env, + procs, + fn_var, + proc_name, + lambda_set.full_layout, + layout_cache, + closure_data_symbol, + env.arena.alloc(result), + ); + + let iter = loc_args.into_iter().rev().zip(arg_symbols.iter().rev()); + assign_to_symbols(env, procs, layout_cache, iter, result) + } + } else { + call_by_name_help( + env, + procs, + fn_var, + proc_name, + loc_args, + lambda_set, + arg_layouts, + ret_layout, + layout_cache, + assigned, + hole, + ) + } + } + Ok(RawFunctionLayout::ErasedFunction(arg_layouts, ret_layout)) => { + // TODO(erased-lambdas) call-by-name should never apply here + let arena = env.arena; + let arg_symbols = Vec::from_iter_in( + loc_args.iter().map(|(arg_var, arg_expr)| { + possible_reuse_symbol_or_specialize( + env, + procs, + layout_cache, + &arg_expr.value, + *arg_var, + ) + }), + arena, + ) + .into_bump_slice(); + + let result = erased::call_erased_function( + env, + layout_cache, + procs, + roc_can::expr::Expr::Var(proc_name, fn_var), + fn_var, + (arg_layouts, ret_layout), + arg_symbols, + assigned, + hole, + // TODO is this right?? + ret_layout, + ); + + let iter = loc_args.into_iter().rev().zip(arg_symbols.iter().rev()); + assign_to_symbols(env, procs, layout_cache, iter, result) + } + Ok(RawFunctionLayout::ZeroArgumentThunk(ret_layout)) => { + if procs.is_module_thunk(proc_name) { + // here we turn a call to a module thunk into forcing of that thunk + call_by_name_module_thunk( + env, + procs, + fn_var, + proc_name, + ret_layout, + layout_cache, + assigned, + hole, + ) + } else if env.is_imported_symbol(proc_name) { + add_needed_external(procs, env, fn_var, LambdaName::no_niche(proc_name)); + force_thunk(env, proc_name, ret_layout, assigned, hole) + } else { + panic!("most likely we're trying to call something that is not a function"); + } + } + } +} + +#[allow(clippy::too_many_arguments)] +fn call_by_name_help<'a>( + env: &mut Env<'a, '_>, + procs: &mut Procs<'a>, + fn_var: Variable, + proc_name: Symbol, + loc_args: std::vec::Vec<(Variable, Loc)>, + lambda_set: LambdaSet<'a>, + argument_layouts: &'a [InLayout<'a>], + ret_layout: InLayout<'a>, + layout_cache: &mut LayoutCache<'a>, + assigned: Symbol, + hole: &'a Stmt<'a>, +) -> Stmt<'a> { + let original_fn_var = fn_var; + let arena = env.arena; + + // the arguments given to the function, stored in symbols + let mut field_symbols = Vec::with_capacity_in(loc_args.len(), arena); + field_symbols.extend(loc_args.iter().map(|(arg_var, arg_expr)| { + possible_reuse_symbol_or_specialize(env, procs, layout_cache, &arg_expr.value, *arg_var) + })); + + // THEORY: with a call by name, there are three options: + // - this is actually a thunk, and the lambda set is empty + // - the name references a function directly, like `main = \x -> ...`. In this case the + // lambda set includes only the function itself, and hence there is exactly one captures + // niche for the function. + // - the name references a value that yields a function, like + // `main = if b then \x -> .. else \y -> ..`. In that case the name being called never + // actually appears in the lambda set, and in fact has no capture set, and hence no + // captures niche. + // So, if this function has any captures niche, it will be the first one. + let mut iter_lambda_names = lambda_set + .iter_set() + .filter(|lam_name| lam_name.name() == proc_name); + let proc_name = match iter_lambda_names.next() { + Some(name) => { + debug_assert!( + iter_lambda_names.next().is_none(), + "Somehow, call by name for {proc_name:?} has multiple capture niches: {lambda_set:?}" + ); + name + } + None => LambdaName::no_niche(proc_name), + }; + + // If required, add an extra argument to the layout that is the captured environment + // afterwards, we MUST make sure the number of arguments in the layout matches the + // number of arguments actually passed. + let top_level_layout = { + let argument_layouts = + lambda_set.extend_argument_list_for_named(env.arena, proc_name, argument_layouts); + ProcLayout::new(env.arena, argument_layouts, proc_name.niche(), ret_layout) + }; + + // the variables of the given arguments + let mut pattern_vars = Vec::with_capacity_in(loc_args.len(), arena); + for (var, _) in &loc_args { + match layout_cache.from_var(env.arena, *var, env.subs) { + Ok(_) => { + pattern_vars.push(*var); + } + Err(_) => { + // One of this function's arguments code gens to a runtime error, + // so attempting to call it will immediately crash. + return runtime_error(env, "TODO runtime error for invalid layout"); + } + } + } + + // If we've already specialized this one, no further work is needed. + if procs + .specialized + .is_specialized(proc_name.name(), &top_level_layout) + { + debug_assert_eq!( + argument_layouts.len(), + field_symbols.len(), + "see call_by_name for background (scroll down a bit), function is {proc_name:?}", + ); + call_specialized_proc( + env, + procs, + proc_name, + lambda_set, + RawFunctionLayout::Function(argument_layouts, lambda_set, ret_layout), + top_level_layout, + field_symbols.into_bump_slice(), + loc_args, + layout_cache, + assigned, + hole, + ) + } else if env.is_imported_symbol(proc_name.name()) + || env.is_unloaded_derived_symbol(proc_name.name(), procs) + { + add_needed_external(procs, env, original_fn_var, proc_name); + + debug_assert_ne!(proc_name.name().module_id(), ModuleId::ATTR); + + if procs.is_imported_module_thunk(proc_name.name()) { + force_thunk( + env, + proc_name.name(), + lambda_set.full_layout, + assigned, + hole, + ) + } else if field_symbols.is_empty() { + // this is a case like `Str.concat`, an imported standard function, applied to zero arguments + + // imported symbols cannot capture anything + let captured = &[]; + debug_assert!(proc_name.no_captures()); + + construct_closure_data( + env, + procs, + layout_cache, + lambda_set, + proc_name, + captured, + assigned, + hole, + ) + } else { + debug_assert_eq!( + argument_layouts.len(), + field_symbols.len(), + "see call_by_name for background (scroll down a bit), function is {proc_name:?}", + ); + + let field_symbols = field_symbols.into_bump_slice(); + + let call = self::Call { + call_type: CallType::ByName { + name: proc_name, + ret_layout, + arg_layouts: argument_layouts, + specialization_id: env.next_call_specialization_id(), + }, + arguments: field_symbols, + }; + + let result = build_call(env, call, assigned, ret_layout, hole); + + let iter = loc_args.into_iter().rev().zip(field_symbols.iter().rev()); + assign_to_symbols(env, procs, layout_cache, iter, result) + } + } else { + // When requested (that is, when procs.pending_specializations is `Some`), + // store a pending specialization rather than specializing immediately. + // + // We do this so that we can do specialization in two passes: first, + // build the mono_expr with all the specialized calls in place (but + // no specializations performed yet), and then second, *after* + // de-duplicating requested specializations (since multiple modules + // which could be getting monomorphized in parallel might request + // the same specialization independently), we work through the + // queue of pending specializations to complete each specialization + // exactly once. + if procs.is_module_thunk(proc_name.name()) { + debug_assert!(top_level_layout.arguments.is_empty()); + } + + let needs_suspended_specialization = + procs.symbol_needs_suspended_specialization(proc_name.name()); + match ( + &mut procs.pending_specializations, + needs_suspended_specialization, + ) { + (PendingSpecializations::Finding(suspended), _) + | (PendingSpecializations::Making(suspended), true) => { + debug_assert!(!env.is_imported_symbol(proc_name.name())); + + // register the pending specialization, so this gets code genned later + suspended.specialization(env.subs, proc_name, top_level_layout, fn_var); + + debug_assert_eq!( + argument_layouts.len(), + field_symbols.len(), + "see call_by_name for background (scroll down a bit), function is {proc_name:?}", + ); + + let field_symbols = field_symbols.into_bump_slice(); + + call_specialized_proc( + env, + procs, + proc_name, + lambda_set, + RawFunctionLayout::Function(argument_layouts, lambda_set, ret_layout), + top_level_layout, + field_symbols, + loc_args, + layout_cache, + assigned, + hole, + ) + } + (PendingSpecializations::Making(_), false) => { + let opt_partial_proc = procs.partial_procs.symbol_to_id(proc_name.name()); + + let field_symbols = field_symbols.into_bump_slice(); + + match opt_partial_proc { + Some(partial_proc) => { + // Mark this proc as in-progress, so if we're dealing with + // mutually recursive functions, we don't loop forever. + // (We had a bug around this before this system existed!) + procs + .specialized + .mark_in_progress(proc_name.name(), top_level_layout); + + match specialize_variable( + env, + procs, + proc_name, + layout_cache, + fn_var, + partial_proc, + ) { + Ok((proc, layout)) => { + let proc_name = proc.name; + let function_layout = + ProcLayout::from_raw_named(env.arena, proc_name, layout); + procs.specialized.insert_specialized( + proc_name.name(), + function_layout, + proc, + ); + + // now we just call our freshly-specialized function + call_specialized_proc( + env, + procs, + proc_name, + lambda_set, + layout, + function_layout, + field_symbols, + loc_args, + layout_cache, + assigned, + hole, + ) + } + Err(SpecializeFailure { attempted_layout }) => { + let proc = generate_runtime_error_function( + env, + proc_name, + attempted_layout, + ); + + let proc_name = proc.name; + let function_layout = ProcLayout::from_raw_named( + env.arena, + proc_name, + attempted_layout, + ); + procs.specialized.insert_specialized( + proc_name.name(), + function_layout, + proc, + ); + + call_specialized_proc( + env, + procs, + proc_name, + lambda_set, + attempted_layout, + function_layout, + field_symbols, + loc_args, + layout_cache, + assigned, + hole, + ) + } + } + } + + None => { + unreachable!("Proc name {:?} is invalid", proc_name) + } + } + } + } + } +} + +#[allow(clippy::too_many_arguments)] +fn call_by_name_module_thunk<'a>( + env: &mut Env<'a, '_>, + procs: &mut Procs<'a>, + fn_var: Variable, + proc_name: Symbol, + ret_layout: InLayout<'a>, + layout_cache: &mut LayoutCache<'a>, + assigned: Symbol, + hole: &'a Stmt<'a>, +) -> Stmt<'a> { + let top_level_layout = ProcLayout::new(env.arena, &[], Niche::NONE, ret_layout); + + let inner_layout = ret_layout; + + // If we've already specialized this one, no further work is needed. + let already_specialized = procs + .specialized + .is_specialized(proc_name, &top_level_layout); + + if already_specialized { + force_thunk(env, proc_name, inner_layout, assigned, hole) + } else { + // When requested (that is, when procs.pending_specializations is `Some`), + // store a pending specialization rather than specializing immediately. + // + // We do this so that we can do specialization in two passes: first, + // build the mono_expr with all the specialized calls in place (but + // no specializations performed yet), and then second, *after* + // de-duplicating requested specializations (since multiple modules + // which could be getting monomorphized in parallel might request + // the same specialization independently), we work through the + // queue of pending specializations to complete each specialization + // exactly once. + if procs.is_module_thunk(proc_name) { + debug_assert!(top_level_layout.arguments.is_empty()); + } + + let needs_suspended_specialization = procs.symbol_needs_suspended_specialization(proc_name); + match ( + &mut procs.pending_specializations, + needs_suspended_specialization, + ) { + (PendingSpecializations::Finding(suspended), _) + | (PendingSpecializations::Making(suspended), true) => { + debug_assert!(!env.is_imported_symbol(proc_name)); + + // register the pending specialization, so this gets code genned later + suspended.specialization( + env.subs, + LambdaName::no_niche(proc_name), + top_level_layout, + fn_var, + ); + + force_thunk(env, proc_name, inner_layout, assigned, hole) + } + (PendingSpecializations::Making(_), false) => { + let opt_partial_proc = procs.partial_procs.symbol_to_id(proc_name); + + match opt_partial_proc { + Some(partial_proc) => { + // Mark this proc as in-progress, so if we're dealing with + // mutually recursive functions, we don't loop forever. + // (We had a bug around this before this system existed!) + procs + .specialized + .mark_in_progress(proc_name, top_level_layout); + + match specialize_variable( + env, + procs, + LambdaName::no_niche(proc_name), + layout_cache, + fn_var, + partial_proc, + ) { + Ok((proc, raw_layout)) => { + debug_assert!( + raw_layout.is_zero_argument_thunk(), + "but actually {raw_layout:?}" + ); + + let was_present = procs + .specialized + .remove_specialized(proc_name, &top_level_layout); + debug_assert!(was_present); + + procs.specialized.insert_specialized( + proc_name, + top_level_layout, + proc, + ); + + force_thunk(env, proc_name, inner_layout, assigned, hole) + } + Err(SpecializeFailure { attempted_layout }) => { + let proc = generate_runtime_error_function( + env, + LambdaName::no_niche(proc_name), + attempted_layout, + ); + + let was_present = procs + .specialized + .remove_specialized(proc_name, &top_level_layout); + debug_assert!(was_present); + + procs.specialized.insert_specialized( + proc_name, + top_level_layout, + proc, + ); + + force_thunk(env, proc_name, inner_layout, assigned, hole) + } + } + } + + None => { + unreachable!("Proc name {:?} is invalid", proc_name) + } + } + } + } + } +} + +#[allow(clippy::too_many_arguments)] +fn call_specialized_proc<'a>( + env: &mut Env<'a, '_>, + procs: &mut Procs<'a>, + proc_name: LambdaName<'a>, + lambda_set: LambdaSet<'a>, + layout: RawFunctionLayout<'a>, + function_layout: ProcLayout<'a>, + field_symbols: &'a [Symbol], + loc_args: std::vec::Vec<(Variable, Loc)>, + layout_cache: &mut LayoutCache<'a>, + assigned: Symbol, + hole: &'a Stmt<'a>, +) -> Stmt<'a> { + if field_symbols.is_empty() { + debug_assert!(loc_args.is_empty()); + + // This happens when we return a function, e.g. + // + // foo = Num.add + // + // Even though the layout (and type) are functions, + // there are no arguments. This confuses our IR, + // and we have to fix it here. + match layout { + RawFunctionLayout::Function(_, lambda_set, _) => { + // when the body is a closure, the function will return the closure environment + let call = self::Call { + call_type: CallType::ByName { + name: proc_name, + ret_layout: function_layout.result, + arg_layouts: function_layout.arguments, + specialization_id: env.next_call_specialization_id(), + }, + arguments: field_symbols, + }; + + // the closure argument is already added here (to get the right specialization) + // but now we need to remove it because the `match_on_lambda_set` will add it again + build_call(env, call, assigned, lambda_set.full_layout, hole) + } + RawFunctionLayout::ErasedFunction(..) => todo_lambda_erasure!(), + RawFunctionLayout::ZeroArgumentThunk(_) => { + unreachable!() + } + } + } else { + let iter = loc_args.into_iter().rev().zip(field_symbols.iter().rev()); + + match procs + .partial_procs + .get_symbol(proc_name.name()) + .map(|pp| &pp.captured_symbols) + { + Some(&CapturedSymbols::Captured(captured_symbols)) => { + let symbols = + Vec::from_iter_in(captured_symbols.iter(), env.arena).into_bump_slice(); + + let closure_data_symbol = env.unique_symbol(); + + // the closure argument is already added here (to get the right specialization) + // but now we need to remove it because the `match_on_lambda_set` will add it again + let mut argument_layouts = + Vec::from_iter_in(function_layout.arguments.iter().copied(), env.arena); + argument_layouts.pop().unwrap(); + + debug_assert_eq!(argument_layouts.len(), field_symbols.len(),); + + let new_hole = match_on_lambda_set( + env, + layout_cache, + procs, + lambda_set, + closure_data_symbol, + field_symbols, + argument_layouts.into_bump_slice(), + function_layout.result, + assigned, + hole, + ); + + let result = construct_closure_data( + env, + procs, + layout_cache, + lambda_set, + proc_name, + symbols.iter().copied(), + closure_data_symbol, + env.arena.alloc(new_hole), + ); + + assign_to_symbols(env, procs, layout_cache, iter, result) + } + _ => { + debug_assert_eq!( + function_layout.arguments.len(), + field_symbols.len(), + "function {:?} with layout {:#?} expects {:?} arguments, but is applied to {:?}", + proc_name, + function_layout, + function_layout.arguments.len(), + field_symbols.len(), + ); + + let call = self::Call { + call_type: CallType::ByName { + name: proc_name, + ret_layout: function_layout.result, + arg_layouts: function_layout.arguments, + specialization_id: env.next_call_specialization_id(), + }, + arguments: field_symbols, + }; + + let result = build_call(env, call, assigned, function_layout.result, hole); + + assign_to_symbols(env, procs, layout_cache, iter, result) + } + } + } +} + +fn assign_num_literal_expr<'a>( + env: &mut Env<'a, '_>, + layout_cache: &mut LayoutCache<'a>, + assigned: Symbol, + variable: Variable, + num_str: &str, + num_value: IntOrFloatValue, + hole: &'a Stmt<'a>, +) -> Result, RuntimeError> { + let layout = layout_cache.from_var(env.arena, variable, env.subs)?; + let literal = + make_num_literal(&layout_cache.interner, layout, num_str, num_value).to_expr_literal(); + + Ok(Stmt::Let(assigned, Expr::Literal(literal), layout, hole)) +} + +type ToLowLevelCallArguments<'a> = ( + LambdaName<'a>, + Symbol, + Option>, + CallSpecId, + UpdateModeId, +); + +/// Use the lambda set to figure out how to make a lowlevel call +#[allow(clippy::too_many_arguments)] +fn lowlevel_match_on_lambda_set<'a, ToLowLevelCall>( + env: &mut Env<'a, '_>, + layout_cache: &LayoutCache<'a>, + lambda_set: LambdaSet<'a>, + op: LowLevel, + closure_data_symbol: Symbol, + to_lowlevel_call: ToLowLevelCall, + return_layout: InLayout<'a>, + assigned: Symbol, + hole: &'a Stmt<'a>, +) -> Stmt<'a> +where + ToLowLevelCall: Fn(ToLowLevelCallArguments<'a>) -> Call<'a> + Copy, +{ + match lambda_set.call_by_name_options(&layout_cache.interner) { + ClosureCallOptions::Void => empty_lambda_set_error(env), + ClosureCallOptions::Union(union_layout) => { + let closure_tag_id_symbol = env.unique_symbol(); + + let result = lowlevel_union_lambda_set_to_switch( + env, + lambda_set.iter_set(), + closure_tag_id_symbol, + union_layout.tag_id_layout(), + closure_data_symbol, + lambda_set.is_represented(&layout_cache.interner), + to_lowlevel_call, + return_layout, + assigned, + hole, + ); + + // extract & assign the closure_tag_id_symbol + let expr = Expr::GetTagId { + structure: closure_data_symbol, + union_layout, + }; + + Stmt::Let( + closure_tag_id_symbol, + expr, + union_layout.tag_id_layout(), + env.arena.alloc(result), + ) + } + ClosureCallOptions::Struct { .. } => match lambda_set.iter_set().next() { + Some(lambda_name) => { + let call_spec_id = env.next_call_specialization_id(); + let update_mode = env.next_update_mode_id(); + let call = to_lowlevel_call(( + lambda_name, + closure_data_symbol, + lambda_set.is_represented(&layout_cache.interner), + call_spec_id, + update_mode, + )); + + build_call(env, call, assigned, return_layout, env.arena.alloc(hole)) + } + None => { + eprintln!( + "a function passed to `{op:?}` LowLevel call has an empty lambda set! + The most likely reason is that some symbol you use is not in scope. + " + ); + + hole.clone() + } + }, + ClosureCallOptions::UnwrappedCapture(_) => { + let lambda_name = lambda_set + .iter_set() + .next() + .expect("no function in lambda set"); + + let call_spec_id = env.next_call_specialization_id(); + let update_mode = env.next_update_mode_id(); + let call = to_lowlevel_call(( + lambda_name, + closure_data_symbol, + lambda_set.is_represented(&layout_cache.interner), + call_spec_id, + update_mode, + )); + + build_call(env, call, assigned, return_layout, env.arena.alloc(hole)) + } + ClosureCallOptions::EnumDispatch(repr) => match repr { + EnumDispatch::Bool => { + let closure_tag_id_symbol = closure_data_symbol; + + lowlevel_enum_lambda_set_to_switch( + env, + lambda_set.iter_set(), + closure_tag_id_symbol, + Layout::BOOL, + closure_data_symbol, + lambda_set.is_represented(&layout_cache.interner), + to_lowlevel_call, + return_layout, + assigned, + hole, + ) + } + EnumDispatch::U8 => { + let closure_tag_id_symbol = closure_data_symbol; + + lowlevel_enum_lambda_set_to_switch( + env, + lambda_set.iter_set(), + closure_tag_id_symbol, + Layout::U8, + closure_data_symbol, + lambda_set.is_represented(&layout_cache.interner), + to_lowlevel_call, + return_layout, + assigned, + hole, + ) + } + }, + } +} + +#[allow(clippy::too_many_arguments)] +fn lowlevel_union_lambda_set_to_switch<'a, ToLowLevelCall>( + env: &mut Env<'a, '_>, + lambda_set: impl ExactSizeIterator> + 'a, + closure_tag_id_symbol: Symbol, + closure_tag_id_layout: InLayout<'a>, + closure_data_symbol: Symbol, + closure_env_layout: Option>, + to_lowlevel_call: ToLowLevelCall, + return_layout: InLayout<'a>, + assigned: Symbol, + hole: &'a Stmt<'a>, +) -> Stmt<'a> +where + ToLowLevelCall: Fn(ToLowLevelCallArguments<'a>) -> Call<'a> + Copy, +{ + debug_assert_ne!(lambda_set.len(), 0); + + let join_point_id = JoinPointId(env.unique_symbol()); + + let mut branches = Vec::with_capacity_in(lambda_set.len(), env.arena); + + for (i, lambda_name) in lambda_set.into_iter().enumerate() { + let assigned = env.unique_symbol(); + + let hole = Stmt::Jump(join_point_id, env.arena.alloc([assigned])); + + let call_spec_id = env.next_call_specialization_id(); + let update_mode = env.next_update_mode_id(); + let call = to_lowlevel_call(( + lambda_name, + closure_data_symbol, + closure_env_layout, + call_spec_id, + update_mode, + )); + let stmt = build_call(env, call, assigned, return_layout, env.arena.alloc(hole)); + + branches.push((i as u64, BranchInfo::None, stmt)); + } + + let default_branch = { + let (_, info, stmt) = branches.pop().unwrap(); + + (info, &*env.arena.alloc(stmt)) + }; + + let switch = Stmt::Switch { + cond_symbol: closure_tag_id_symbol, + cond_layout: closure_tag_id_layout, + branches: branches.into_bump_slice(), + default_branch, + ret_layout: return_layout, + }; + + let param = Param { + symbol: assigned, + layout: return_layout, + }; + + Stmt::Join { + id: join_point_id, + parameters: &*env.arena.alloc([param]), + body: hole, + remainder: env.arena.alloc(switch), + } +} + +fn empty_lambda_set_error<'a>(env: &mut Env<'a, '_>) -> Stmt<'a> { + let msg = "a Lambda Set is empty. Most likely there is a type error in your program."; + runtime_error(env, msg) +} + +/// Use the lambda set to figure out how to make a call-by-name +#[allow(clippy::too_many_arguments)] +fn match_on_lambda_set<'a>( + env: &mut Env<'a, '_>, + layout_cache: &LayoutCache<'a>, + procs: &mut Procs<'a>, + lambda_set: LambdaSet<'a>, + closure_data_symbol: Symbol, + argument_symbols: &'a [Symbol], + argument_layouts: &'a [InLayout<'a>], + return_layout: InLayout<'a>, + assigned: Symbol, + hole: &'a Stmt<'a>, +) -> Stmt<'a> { + match lambda_set.call_by_name_options(&layout_cache.interner) { + ClosureCallOptions::Void => empty_lambda_set_error(env), + ClosureCallOptions::Union(union_layout) => { + let closure_tag_id_symbol = env.unique_symbol(); + + let result = union_lambda_set_to_switch( + env, + lambda_set, + closure_tag_id_symbol, + union_layout.tag_id_layout(), + closure_data_symbol, + argument_symbols, + argument_layouts, + return_layout, + assigned, + hole, + ); + + // extract & assign the closure_tag_id_symbol + let expr = Expr::GetTagId { + structure: closure_data_symbol, + union_layout, + }; + + Stmt::Let( + closure_tag_id_symbol, + expr, + union_layout.tag_id_layout(), + env.arena.alloc(result), + ) + } + ClosureCallOptions::Struct(field_layouts) => { + let function_symbol = match lambda_set.iter_set().next() { + Some(function_symbol) => function_symbol, + None => { + // Lambda set is empty, so this function is never called; synthesize a function + // that always yields a runtime error. + let name = env.unique_symbol(); + let lambda_name = LambdaName::no_niche(name); + let function_layout = + RawFunctionLayout::Function(argument_layouts, lambda_set, return_layout); + let proc = generate_runtime_error_function(env, lambda_name, function_layout); + let top_level = + ProcLayout::from_raw_named(env.arena, lambda_name, function_layout); + + procs.specialized.insert_specialized(name, top_level, proc); + + lambda_name + } + }; + + let closure_info = match field_layouts { + [] => ClosureInfo::DoesNotCapture, + _ => ClosureInfo::Captures { + lambda_set, + closure_data_symbol, + }, + }; + + union_lambda_set_branch_help( + env, + function_symbol, + closure_info, + argument_symbols, + argument_layouts, + return_layout, + assigned, + hole, + ) + } + ClosureCallOptions::UnwrappedCapture(_) => { + let function_symbol = lambda_set + .iter_set() + .next() + .expect("no function in lambda set"); + + let closure_info = ClosureInfo::Captures { + lambda_set, + closure_data_symbol, + }; + + union_lambda_set_branch_help( + env, + function_symbol, + closure_info, + argument_symbols, + argument_layouts, + return_layout, + assigned, + hole, + ) + } + ClosureCallOptions::EnumDispatch(repr) => match repr { + EnumDispatch::Bool => { + let closure_tag_id_symbol = closure_data_symbol; + + enum_lambda_set_to_switch( + env, + lambda_set.iter_set(), + closure_tag_id_symbol, + Layout::BOOL, + argument_symbols, + argument_layouts, + return_layout, + assigned, + hole, + ) + } + EnumDispatch::U8 => { + let closure_tag_id_symbol = closure_data_symbol; + + enum_lambda_set_to_switch( + env, + lambda_set.iter_set(), + closure_tag_id_symbol, + Layout::U8, + argument_symbols, + argument_layouts, + return_layout, + assigned, + hole, + ) + } + }, + } +} + +#[allow(clippy::too_many_arguments)] +fn union_lambda_set_to_switch<'a>( + env: &mut Env<'a, '_>, + lambda_set: LambdaSet<'a>, + closure_tag_id_symbol: Symbol, + closure_tag_id_layout: InLayout<'a>, + closure_data_symbol: Symbol, + argument_symbols: &'a [Symbol], + argument_layouts: &'a [InLayout<'a>], + return_layout: InLayout<'a>, + assigned: Symbol, + hole: &'a Stmt<'a>, +) -> Stmt<'a> { + if lambda_set.is_empty() { + // NOTE this can happen if there is a type error somewhere. Since the lambda set is empty, + // there is really nothing we can do here. We generate a runtime error here which allows + // code gen to proceed. We then assume that we hit another (more descriptive) error before + // hitting this one + return empty_lambda_set_error(env); + } + + let (opt_join, branch_assigned, branch_hole) = match hole { + Stmt::Ret(_) => { + // No need to jump to a joinpoint, inline the return in each statement as-is. + // This makes further analyses, like TCO, easier as well. + (None, assigned, hole) + } + _ => { + let join_point_id = JoinPointId(env.unique_symbol()); + let assigned = env.unique_symbol(); + let hole = Stmt::Jump(join_point_id, env.arena.alloc([assigned])); + + (Some(join_point_id), assigned, &*env.arena.alloc(hole)) + } + }; + + let mut branches = Vec::with_capacity_in(lambda_set.len(), env.arena); + + for (i, lambda_name) in lambda_set.iter_set().enumerate() { + let closure_info = if lambda_name.no_captures() { + ClosureInfo::DoesNotCapture + } else { + ClosureInfo::Captures { + lambda_set, + closure_data_symbol, + } + }; + + let stmt = union_lambda_set_branch( + env, + lambda_name, + closure_info, + argument_symbols, + argument_layouts, + return_layout, + branch_assigned, + branch_hole, + ); + branches.push((i as u64, BranchInfo::None, stmt)); + } + + let default_branch = { + let (_, info, stmt) = branches.pop().unwrap(); + + (info, &*env.arena.alloc(stmt)) + }; + + let switch = Stmt::Switch { + cond_symbol: closure_tag_id_symbol, + cond_layout: closure_tag_id_layout, + branches: branches.into_bump_slice(), + default_branch, + ret_layout: return_layout, + }; + + match opt_join { + None => switch, + Some(join_point_id) => { + let param = Param { + symbol: assigned, + layout: return_layout, + }; + + Stmt::Join { + id: join_point_id, + parameters: &*env.arena.alloc([param]), + body: hole, + remainder: env.arena.alloc(switch), + } + } + } +} + +#[allow(clippy::too_many_arguments)] +fn union_lambda_set_branch<'a>( + env: &mut Env<'a, '_>, + lambda_name: LambdaName<'a>, + closure_info: ClosureInfo<'a>, + argument_symbols_slice: &'a [Symbol], + argument_layouts_slice: &'a [InLayout<'a>], + return_layout: InLayout<'a>, + assigned: Symbol, + hole: &'a Stmt<'a>, +) -> Stmt<'a> { + union_lambda_set_branch_help( + env, + lambda_name, + closure_info, + argument_symbols_slice, + argument_layouts_slice, + return_layout, + assigned, + env.arena.alloc(hole), + ) +} + +#[derive(Clone, Copy)] +enum ClosureInfo<'a> { + Captures { + closure_data_symbol: Symbol, + /// The whole lambda set representation this closure is a variant of + lambda_set: LambdaSet<'a>, + }, + DoesNotCapture, +} + +#[allow(clippy::too_many_arguments)] +fn union_lambda_set_branch_help<'a>( + env: &mut Env<'a, '_>, + lambda_name: LambdaName<'a>, + closure_info: ClosureInfo<'a>, + argument_symbols_slice: &'a [Symbol], + argument_layouts_slice: &'a [InLayout<'a>], + return_layout: InLayout<'a>, + assigned: Symbol, + hole: &'a Stmt<'a>, +) -> Stmt<'a> { + let (argument_layouts, argument_symbols) = match closure_info { + ClosureInfo::Captures { + lambda_set, + closure_data_symbol, + } => { + let argument_layouts = lambda_set.extend_argument_list_for_named( + env.arena, + lambda_name, + argument_layouts_slice, + ); + + // Since this lambda captures, the arguments must have been extended. + debug_assert!(argument_layouts.len() > argument_layouts_slice.len()); + // Extend symbols with the symbol of the closure environment. + let mut argument_symbols = + Vec::with_capacity_in(argument_symbols_slice.len() + 1, env.arena); + argument_symbols.extend(argument_symbols_slice); + argument_symbols.push(closure_data_symbol); + + (argument_layouts, argument_symbols.into_bump_slice()) + } + ClosureInfo::DoesNotCapture => { + // sometimes unification causes a function that does not itself capture anything + // to still get a lambda set that does store information. We must not pass a closure + // argument in this case + + (argument_layouts_slice, argument_symbols_slice) + } + }; + + // build the call + let call = self::Call { + call_type: CallType::ByName { + name: lambda_name, + ret_layout: return_layout, + arg_layouts: argument_layouts, + specialization_id: env.next_call_specialization_id(), + }, + arguments: argument_symbols, + }; + + build_call(env, call, assigned, return_layout, hole) +} + +/// Switches over a enum lambda set, which may dispatch to different functions, none of which +/// capture. +#[allow(clippy::too_many_arguments)] +fn enum_lambda_set_to_switch<'a>( + env: &mut Env<'a, '_>, + lambda_set: impl ExactSizeIterator>, + closure_tag_id_symbol: Symbol, + closure_tag_id_layout: InLayout<'a>, + argument_symbols: &'a [Symbol], + argument_layouts: &'a [InLayout<'a>], + return_layout: InLayout<'a>, + assigned: Symbol, + hole: &'a Stmt<'a>, +) -> Stmt<'a> { + debug_assert_ne!(lambda_set.len(), 0); + + let (opt_join, branch_assigned, branch_hole) = match hole { + Stmt::Ret(_) => { + // No need to jump to a joinpoint, inline the return in each statement as-is. + // This makes further analyses, like TCO, easier as well. + (None, assigned, hole) + } + _ => { + let join_point_id = JoinPointId(env.unique_symbol()); + let assigned = env.unique_symbol(); + let hole = Stmt::Jump(join_point_id, env.arena.alloc([assigned])); + + (Some(join_point_id), assigned, &*env.arena.alloc(hole)) + } + }; + + let mut branches = Vec::with_capacity_in(lambda_set.len(), env.arena); + + for (i, lambda_name) in lambda_set.into_iter().enumerate() { + let stmt = enum_lambda_set_branch( + env, + lambda_name, + argument_symbols, + argument_layouts, + return_layout, + branch_assigned, + branch_hole, + ); + branches.push((i as u64, BranchInfo::None, stmt)); + } + + let default_branch = { + let (_, info, stmt) = branches.pop().unwrap(); + + (info, &*env.arena.alloc(stmt)) + }; + + let switch = Stmt::Switch { + cond_symbol: closure_tag_id_symbol, + cond_layout: closure_tag_id_layout, + branches: branches.into_bump_slice(), + default_branch, + ret_layout: return_layout, + }; + + match opt_join { + None => switch, + Some(join_point_id) => { + let param = Param { + symbol: assigned, + layout: return_layout, + }; + + Stmt::Join { + id: join_point_id, + parameters: &*env.arena.alloc([param]), + body: hole, + remainder: env.arena.alloc(switch), + } + } + } +} + +/// A branch for an enum lambda set branch dispatch, which never capture! +#[allow(clippy::too_many_arguments)] +fn enum_lambda_set_branch<'a>( + env: &mut Env<'a, '_>, + lambda_name: LambdaName<'a>, + argument_symbols: &'a [Symbol], + argument_layouts: &'a [InLayout<'a>], + return_layout: InLayout<'a>, + assigned: Symbol, + hole: &'a Stmt<'a>, +) -> Stmt<'a> { + let call = self::Call { + call_type: CallType::ByName { + name: lambda_name, + ret_layout: return_layout, + arg_layouts: argument_layouts, + specialization_id: env.next_call_specialization_id(), + }, + arguments: argument_symbols, + }; + build_call(env, call, assigned, return_layout, hole) +} + +#[allow(clippy::too_many_arguments)] +fn lowlevel_enum_lambda_set_to_switch<'a, ToLowLevelCall>( + env: &mut Env<'a, '_>, + lambda_set: impl ExactSizeIterator>, + closure_tag_id_symbol: Symbol, + closure_tag_id_layout: InLayout<'a>, + closure_data_symbol: Symbol, + closure_env_layout: Option>, + to_lowlevel_call: ToLowLevelCall, + return_layout: InLayout<'a>, + assigned: Symbol, + hole: &'a Stmt<'a>, +) -> Stmt<'a> +where + ToLowLevelCall: Fn(ToLowLevelCallArguments<'a>) -> Call<'a> + Copy, +{ + debug_assert_ne!(lambda_set.len(), 0); + + let join_point_id = JoinPointId(env.unique_symbol()); + + let mut branches = Vec::with_capacity_in(lambda_set.len(), env.arena); + + for (i, function_symbol) in lambda_set.into_iter().enumerate() { + let result_symbol = env.unique_symbol(); + + let hole = Stmt::Jump(join_point_id, env.arena.alloc([result_symbol])); + + let call_spec_id = env.next_call_specialization_id(); + let update_mode = env.next_update_mode_id(); + let call = to_lowlevel_call(( + function_symbol, + closure_data_symbol, + closure_env_layout, + call_spec_id, + update_mode, + )); + let stmt = build_call( + env, + call, + result_symbol, + return_layout, + env.arena.alloc(hole), + ); + + branches.push((i as u64, BranchInfo::None, stmt)); + } + + let default_branch = { + let (_, info, stmt) = branches.pop().unwrap(); + + (info, &*env.arena.alloc(stmt)) + }; + + let switch = Stmt::Switch { + cond_symbol: closure_tag_id_symbol, + cond_layout: closure_tag_id_layout, + branches: branches.into_bump_slice(), + default_branch, + ret_layout: return_layout, + }; + + let param = Param { + symbol: assigned, + layout: return_layout, + }; + + Stmt::Join { + id: join_point_id, + parameters: &*env.arena.alloc([param]), + body: hole, + remainder: env.arena.alloc(switch), + } +} + +#[derive(Debug, Default)] +pub struct GlueLayouts<'a> { + pub getters: std::vec::Vec<(Symbol, ProcLayout<'a>)>, +} + +type GlueProcId = u16; + +#[derive(Debug)] +pub struct GlueProc<'a> { + pub name: Symbol, + pub proc_layout: ProcLayout<'a>, + pub proc: Proc<'a>, +} + +pub struct GlueProcs<'a> { + pub getters: Vec<'a, (Layout<'a>, Vec<'a, GlueProc<'a>>)>, + /// Lambda set IDs computed from the layout of the lambda set. Should be replaced by + /// computation from type variable eventually. + pub legacy_layout_based_extern_names: Vec<'a, (LambdaSetId, RawFunctionLayout<'a>)>, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] +pub struct LambdaSetId(pub u32); + +impl LambdaSetId { + #[must_use] + pub fn next(self) -> Self { + debug_assert!(self.0 < u32::MAX); + Self(self.0 + 1) + } +} + +pub fn find_lambda_sets( + arena: &Bump, + subs: &Subs, + initial: Variable, +) -> MutMap { + let mut stack = bumpalo::collections::Vec::new_in(arena); + + // ignore the lambda set of top-level functions + match subs.get_without_compacting(initial).content { + Content::Structure(FlatType::Func(arguments, _, result)) => { + let arguments = &subs.variables[arguments.indices()]; + + stack.extend(arguments.iter().copied()); + stack.push(result); + } + _ => { + stack.push(initial); + } + } + + find_lambda_sets_help(subs, stack) +} + +fn find_lambda_sets_help( + subs: &Subs, + mut stack: Vec<'_, Variable>, +) -> MutMap { + use roc_types::subs::GetSubsSlice; + + let mut lambda_set_id = LambdaSetId::default(); + + let mut result = MutMap::default(); + + while let Some(var) = stack.pop() { + match subs.get_content_without_compacting(var) { + Content::RangedNumber(_) + | Content::Error + | Content::FlexVar(_) + | Content::RigidVar(_) + | Content::FlexAbleVar(_, _) + | Content::RigidAbleVar(_, _) + | Content::RecursionVar { .. } => {} + Content::Structure(flat_type) => match flat_type { + FlatType::Apply(_, arguments) => { + stack.extend(subs.get_subs_slice(*arguments).iter().rev()); + } + FlatType::Func(arguments, lambda_set_var, ret_var) => { + use std::collections::hash_map::Entry; + // Only insert a lambda_set_var if we didn't already have a value for this key. + if let Entry::Vacant(entry) = result.entry(*lambda_set_var) { + entry.insert(lambda_set_id); + lambda_set_id = lambda_set_id.next(); + } + + let arguments = &subs.variables[arguments.indices()]; + + stack.extend(arguments.iter().copied()); + stack.push(*lambda_set_var); + stack.push(*ret_var); + } + FlatType::Record(fields, ext) => { + stack.extend(subs.get_subs_slice(fields.variables()).iter().rev()); + stack.push(*ext); + } + FlatType::Tuple(elements, ext) => { + stack.extend(subs.get_subs_slice(elements.variables()).iter().rev()); + stack.push(*ext); + } + FlatType::FunctionOrTagUnion(_, _, ext) => { + // just the ext + match ext { + roc_types::subs::TagExt::Openness(var) => stack.push(*var), + roc_types::subs::TagExt::Any(_) => { /* ignore */ } + } + } + FlatType::TagUnion(union_tags, ext) + | FlatType::RecursiveTagUnion(_, union_tags, ext) => { + for tag in union_tags.variables() { + stack.extend( + subs.get_subs_slice(subs.variable_slices[tag.index as usize]) + .iter() + .rev(), + ); + } + + match ext { + roc_types::subs::TagExt::Openness(var) => stack.push(*var), + roc_types::subs::TagExt::Any(_) => { /* ignore */ } + } + } + FlatType::EmptyRecord => {} + FlatType::EmptyTuple => {} + FlatType::EmptyTagUnion => {} + }, + Content::Alias(_, _, actual, _) => { + stack.push(*actual); + } + Content::LambdaSet(lambda_set) => { + // the lambda set itself should already be caught by Func above, but the + // capture can itself contain more lambda sets + for index in lambda_set.solved.variables() { + let subs_slice = subs.variable_slices[index.index as usize]; + stack.extend(subs.variables[subs_slice.indices()].iter()); + } + } + Content::ErasedLambda => {} + } + } + + result +} + +pub fn generate_glue_procs<'a, 'i, I>( + home: ModuleId, + ident_ids: &mut IdentIds, + arena: &'a Bump, + layout_interner: &'i mut I, + layout: &'a Layout<'a>, +) -> GlueProcs<'a> +where + I: LayoutInterner<'a>, +{ + let mut answer = GlueProcs { + getters: Vec::new_in(arena), + legacy_layout_based_extern_names: Vec::new_in(arena), + }; + + let mut lambda_set_id = LambdaSetId(0); + + let mut stack: Vec<'a, Layout<'a>> = Vec::from_iter_in([*layout], arena); + let mut next_unique_id = 0; + + macro_rules! handle_tag_field_layouts { + ($tag_id:expr, $layout:expr, $union_layout:expr, $field_layouts: expr) => {{ + if $field_layouts.iter().any(|l| { + layout_interner + .get_repr(*l) + .has_varying_stack_size(layout_interner, arena) + }) { + let procs = generate_glue_procs_for_tag_fields( + layout_interner, + home, + &mut next_unique_id, + ident_ids, + arena, + $tag_id, + &$layout, + $union_layout, + $field_layouts, + ); + + answer.getters.push(($layout, procs)); + } + + for in_layout in $field_layouts.iter().rev() { + stack.push(layout_interner.get(*in_layout)); + } + }}; + } + + while let Some(layout) = stack.pop() { + match layout.repr(layout_interner) { + LayoutRepr::Builtin(builtin) => match builtin { + Builtin::Int(_) + | Builtin::Float(_) + | Builtin::Bool + | Builtin::Decimal + | Builtin::Str => { /* do nothing */ } + Builtin::List(element) => stack.push(layout_interner.get(element)), + }, + LayoutRepr::Struct(field_layouts) => { + if field_layouts.iter().any(|l| { + layout_interner + .get_repr(*l) + .has_varying_stack_size(layout_interner, arena) + }) { + let procs = generate_glue_procs_for_struct_fields( + layout_interner, + home, + &mut next_unique_id, + ident_ids, + arena, + &layout, + field_layouts, + ); + + answer.getters.push((layout, procs)); + } + + for in_layout in field_layouts.iter().rev() { + stack.push(layout_interner.get(*in_layout)); + } + } + LayoutRepr::Ptr(inner) => { + stack.push(layout_interner.get(inner)); + } + LayoutRepr::Union(union_layout) => match union_layout { + UnionLayout::NonRecursive(tags) => { + for in_layout in tags.iter().flat_map(|e| e.iter()) { + stack.push(layout_interner.get(*in_layout)); + } + } + UnionLayout::Recursive(tags) => { + for in_layout in tags.iter().flat_map(|e| e.iter()) { + stack.push(layout_interner.get(*in_layout)); + } + } + UnionLayout::NonNullableUnwrapped(field_layouts) => { + handle_tag_field_layouts!(0, layout, union_layout, field_layouts); + } + UnionLayout::NullableWrapped { + other_tags, + nullable_id, + } => { + let tag_ids = + (0..nullable_id).chain(nullable_id + 1..other_tags.len() as u16 + 1); + for (i, field_layouts) in tag_ids.zip(other_tags) { + handle_tag_field_layouts!(i, layout, union_layout, *field_layouts); + } + } + UnionLayout::NullableUnwrapped { other_fields, .. } => { + for in_layout in other_fields.iter().rev() { + stack.push(layout_interner.get(*in_layout)); + } + } + }, + LayoutRepr::LambdaSet(lambda_set) => { + let raw_function_layout = + RawFunctionLayout::Function(lambda_set.args, lambda_set, lambda_set.ret); + + let key = (lambda_set_id, raw_function_layout); + answer.legacy_layout_based_extern_names.push(key); + + // this id is used, increment for the next one + lambda_set_id = lambda_set_id.next(); + + stack.push(layout_interner.get(lambda_set.runtime_representation())); + + // TODO: figure out if we need to look at the other layouts + // stack.push(layout_interner.get(lambda_set.ret)); + } + LayoutRepr::RecursivePointer(_) => { + /* do nothing, we've already generated for this type through the Union(_) */ + } + LayoutRepr::FunctionPointer(_) => todo_lambda_erasure!(), + LayoutRepr::Erased(_) => todo_lambda_erasure!(), + } + } + + answer +} + +fn generate_glue_procs_for_struct_fields<'a, 'i, I>( + layout_interner: &'i mut I, + home: ModuleId, + next_unique_id: &mut GlueProcId, + ident_ids: &mut IdentIds, + arena: &'a Bump, + unboxed_struct_layout: &Layout<'a>, + field_layouts: &[InLayout<'a>], +) -> Vec<'a, GlueProc<'a>> +where + I: LayoutInterner<'a>, +{ + let interned_unboxed_struct_layout = layout_interner.insert(*unboxed_struct_layout); + let union_layout = + UnionLayout::NonNullableUnwrapped(arena.alloc([interned_unboxed_struct_layout])); + let boxed_struct_layout = Layout::no_semantic(LayoutRepr::Union(union_layout).direct()); + let boxed_struct_layout = layout_interner.insert(boxed_struct_layout); + let mut answer = bumpalo::collections::Vec::with_capacity_in(field_layouts.len(), arena); + + let field_layouts = match layout_interner.get_repr(interned_unboxed_struct_layout) { + LayoutRepr::Struct(field_layouts) => field_layouts, + other => { + unreachable!( + "{:?} {:?}", + layout_interner.dbg(interned_unboxed_struct_layout), + other + ) + } + }; + + for (index, field) in field_layouts.iter().enumerate() { + let proc_layout = ProcLayout { + arguments: arena.alloc([boxed_struct_layout]), + result: *field, + niche: Niche::NONE, + }; + + let symbol = unique_glue_symbol(arena, next_unique_id, home, ident_ids); + + let argument = Symbol::new(home, ident_ids.gen_unique()); + let unboxed = Symbol::new(home, ident_ids.gen_unique()); + let result = Symbol::new(home, ident_ids.gen_unique()); + + home.register_debug_idents(ident_ids); + + let ret_stmt = arena.alloc(Stmt::Ret(result)); + + let field_get_expr = Expr::StructAtIndex { + index: index as u64, + field_layouts, + structure: unboxed, + }; + + let field_get_stmt = Stmt::Let(result, field_get_expr, *field, ret_stmt); + + let unbox_expr = boxed::unbox(argument, arena.alloc(interned_unboxed_struct_layout)); + + let unbox_stmt = Stmt::Let( + unboxed, + unbox_expr, + interned_unboxed_struct_layout, + arena.alloc(field_get_stmt), + ); + + let proc = Proc { + name: LambdaName::no_niche(symbol), + args: arena.alloc([(boxed_struct_layout, argument)]), + body: unbox_stmt, + closure_data_layout: None, + ret_layout: *field, + is_self_recursive: SelfRecursive::NotSelfRecursive, + is_erased: false, + }; + + answer.push(GlueProc { + name: symbol, + proc_layout, + proc, + }); + } + + answer +} + +fn unique_glue_symbol( + arena: &Bump, + next_unique_id: &mut GlueProcId, + home: ModuleId, + ident_ids: &mut IdentIds, +) -> Symbol { + let unique_id = *next_unique_id; + + *next_unique_id = unique_id + 1; + + // then name of the platform `main.roc` is the empty string + let module_name = ""; + + // Turn unique_id into a Symbol without doing a heap allocation. + use std::fmt::Write; + let mut string = bumpalo::collections::String::with_capacity_in(32, arena); + + let _result = write!(&mut string, "roc__getter_{module_name}_{unique_id}"); + debug_assert_eq!(_result, Ok(())); // This should never fail, but doesn't hurt to debug-check! + + let bump_string = string.into_bump_str(); + let ident_id = ident_ids.get_or_insert(bump_string); + + Symbol::new(home, ident_id) +} + +#[allow(clippy::too_many_arguments)] +fn generate_glue_procs_for_tag_fields<'a, 'i, I>( + layout_interner: &'i mut I, + home: ModuleId, + next_unique_id: &mut GlueProcId, + ident_ids: &mut IdentIds, + arena: &'a Bump, + tag_id: TagIdIntType, + unboxed_struct_layout: &Layout<'a>, + union_layout: UnionLayout<'a>, + field_layouts: &'a [InLayout<'a>], +) -> Vec<'a, GlueProc<'a>> +where + I: LayoutInterner<'a>, +{ + let interned = layout_interner.insert(*unboxed_struct_layout); + let box_union_layout = UnionLayout::NonNullableUnwrapped(arena.alloc([interned])); + let boxed_struct_layout = Layout::no_semantic(LayoutRepr::Union(box_union_layout).direct()); + let boxed_struct_layout = layout_interner.insert(boxed_struct_layout); + let mut answer = bumpalo::collections::Vec::with_capacity_in(field_layouts.len(), arena); + + for (index, field) in field_layouts.iter().enumerate() { + let proc_layout = ProcLayout { + arguments: arena.alloc([boxed_struct_layout]), + result: *field, + niche: Niche::NONE, + }; + let symbol = unique_glue_symbol(arena, next_unique_id, home, ident_ids); + + let argument = Symbol::new(home, ident_ids.gen_unique()); + let unboxed = Symbol::new(home, ident_ids.gen_unique()); + let result = Symbol::new(home, ident_ids.gen_unique()); + + home.register_debug_idents(ident_ids); + + let ret_stmt = arena.alloc(Stmt::Ret(result)); + + let field_get_expr = Expr::UnionAtIndex { + structure: unboxed, + tag_id, + union_layout, + index: index as u64, + }; + + let field_get_stmt = Stmt::Let(result, field_get_expr, *field, ret_stmt); + + let unbox_expr = boxed::unbox(argument, arena.alloc(interned)); + let unbox_stmt = Stmt::Let(unboxed, unbox_expr, interned, arena.alloc(field_get_stmt)); + + let proc = Proc { + name: LambdaName::no_niche(symbol), + args: arena.alloc([(boxed_struct_layout, argument)]), + body: unbox_stmt, + closure_data_layout: None, + ret_layout: *field, + is_self_recursive: SelfRecursive::NotSelfRecursive, + is_erased: false, + }; + + answer.push(GlueProc { + name: symbol, + proc_layout, + proc, + }); + } + + answer +} + +enum Usage { + Used, + Unused, +} + +pub struct UsageTrackingMap { + map: MutMap, +} + +impl Default for UsageTrackingMap { + fn default() -> Self { + Self { + map: MutMap::default(), + } + } +} + +impl UsageTrackingMap +where + K: std::cmp::Eq + std::hash::Hash, +{ + pub fn insert(&mut self, key: K, value: V) { + self.map.insert(key, (value, Usage::Unused)); + } + + pub fn get(&mut self, key: K) -> Option<&V> { + let (value, usage) = self.map.get_mut(&key)?; + *usage = Usage::Used; + Some(value) + } + + fn get_used(&mut self, key: &K) -> Option { + self.map.remove(key).and_then(|(value, usage)| match usage { + Usage::Used => Some(value), + Usage::Unused => None, + }) + } +} diff --git a/crates/compiler/mono/src/ir/boxed.rs b/crates/compiler/mono/src/ir/boxed.rs new file mode 100644 index 0000000000..8add424fd9 --- /dev/null +++ b/crates/compiler/mono/src/ir/boxed.rs @@ -0,0 +1,41 @@ +use roc_module::symbol::Symbol; + +use crate::layout::{InLayout, UnionLayout}; + +use super::Expr; + +pub fn box_<'a>(symbol: &'a Symbol, element_layout: &'a InLayout<'a>) -> Expr<'a> { + Expr::Tag { + tag_layout: UnionLayout::NonNullableUnwrapped(std::slice::from_ref(element_layout)), + tag_id: 0, + arguments: std::slice::from_ref(symbol), + reuse: None, + } +} + +pub fn unbox<'a>(symbol: Symbol, element_layout: &'a InLayout<'a>) -> Expr<'a> { + Expr::UnionAtIndex { + structure: symbol, + tag_id: 0, + union_layout: UnionLayout::NonNullableUnwrapped(std::slice::from_ref(element_layout)), + index: 0, + } +} + +pub fn box_nullable<'a>(symbol: &'a Symbol, element_layout: &'a InLayout<'a>) -> Expr<'a> { + Expr::Tag { + tag_layout: UnionLayout::boxed_erased_value(element_layout), + tag_id: 0, + arguments: std::slice::from_ref(symbol), + reuse: None, + } +} + +pub fn unbox_nullable<'a>(symbol: Symbol, element_layout: &'a InLayout<'a>) -> Expr<'a> { + Expr::UnionAtIndex { + structure: symbol, + tag_id: 0, + union_layout: UnionLayout::boxed_erased_value(element_layout), + index: 0, + } +} diff --git a/crates/compiler/mono/src/ir/decision_tree.rs b/crates/compiler/mono/src/ir/decision_tree.rs new file mode 100644 index 0000000000..745d2cae12 --- /dev/null +++ b/crates/compiler/mono/src/ir/decision_tree.rs @@ -0,0 +1,2762 @@ +use super::pattern::{build_list_index_probe, store_pattern, DestructType, ListIndex, Pattern}; +use crate::ir::{ + substitute_in_exprs_many, BranchInfo, Call, CallType, CompiledGuardStmt, Env, Expr, + GuardStmtSpec, JoinPointId, Literal, Param, Procs, Stmt, +}; +use crate::layout::{ + Builtin, InLayout, Layout, LayoutCache, LayoutInterner, LayoutRepr, TLLayoutInterner, + TagIdIntType, UnionLayout, +}; +use roc_builtins::bitcode::{FloatWidth, IntWidth}; +use roc_collections::all::{MutMap, MutSet}; +use roc_collections::BumpMap; +use roc_error_macros::internal_error; +use roc_exhaustive::{Ctor, CtorName, ListArity, RenderAs, TagId, Union}; +use roc_module::ident::TagName; +use roc_module::low_level::LowLevel; +use roc_module::symbol::Symbol; + +/// COMPILE CASES + +type Label = u64; +const RECORD_TAG_NAME: &str = "#Record"; +const TUPLE_TAG_NAME: &str = "#Tuple"; + +/// Users of this module will mainly interact with this function. It takes +/// some normal branches and gives out a decision tree that has "labels" at all +/// the leafs and a dictionary that maps these "labels" to the code that should +/// run. +fn compile<'a>( + interner: &TLLayoutInterner<'a>, + raw_branches: Vec<(Guard<'a>, Pattern<'a>, u64)>, +) -> DecisionTree<'a> { + let formatted = raw_branches + .into_iter() + .map(|(guard, pattern, index)| Branch { + goal: index, + guard, + patterns: vec![(Vec::new(), pattern)], + }) + .collect(); + + to_decision_tree(interner, formatted) +} + +#[derive(Clone, Debug, PartialEq)] +pub(crate) enum Guard<'a> { + NoGuard, + Guard { + /// pattern + pattern: Pattern<'a>, + /// How to compile the guard statement. + stmt_spec: GuardStmtSpec, + }, +} + +impl<'a> Guard<'a> { + fn is_none(&self) -> bool { + self == &Guard::NoGuard + } + + fn is_some(&self) -> bool { + !self.is_none() + } +} + +type Edge<'a> = (GuardedTest<'a>, DecisionTree<'a>); + +#[derive(Clone, Debug, PartialEq)] +enum DecisionTree<'a> { + Match(Label), + Decision { + path: Vec, + edges: Vec>, + default: Option>>, + }, +} + +#[derive(Clone, Debug, PartialEq)] +enum GuardedTest<'a> { + // e.g. `x if True -> ...` + GuardedNoTest { + /// pattern + pattern: Pattern<'a>, + /// How to compile the guard body. + stmt_spec: GuardStmtSpec, + }, + // e.g. ` -> ...` + TestNotGuarded { + test: Test<'a>, + }, + // e.g. `_ -> ...` or `x -> ...` + PlaceholderWithGuard, +} + +#[derive(Clone, Copy, Debug, PartialEq, Hash)] +enum ListLenBound { + Exact, + AtLeast, +} + +#[derive(Clone, Debug, PartialEq)] +#[allow(clippy::enum_variant_names)] +enum Test<'a> { + IsCtor { + tag_id: TagIdIntType, + ctor_name: CtorName, + union: roc_exhaustive::Union, + arguments: Vec<(Pattern<'a>, InLayout<'a>)>, + }, + IsInt([u8; 16], IntWidth), + // stores the f64 bits; u64 so that this type can impl Hash + IsFloat(u64, FloatWidth), + IsDecimal([u8; 16]), + IsStr(Box), + IsBit(bool), + IsByte { + tag_id: TagIdIntType, + num_alts: usize, + }, + IsListLen { + bound: ListLenBound, + len: u64, + }, +} + +impl<'a> Test<'a> { + fn can_be_switch(&self) -> bool { + match self { + Test::IsCtor { .. } => true, + Test::IsInt(_, int_width) => { + // llvm does not like switching on 128-bit values + !matches!(int_width, IntWidth::U128 | IntWidth::I128) + } + Test::IsFloat(_, _) => false, + Test::IsDecimal(_) => false, + Test::IsStr(_) => false, + Test::IsBit(_) => true, + Test::IsByte { .. } => true, + Test::IsListLen { bound, .. } => match bound { + ListLenBound::Exact => true, + ListLenBound::AtLeast => false, + }, + } + } +} + +use std::hash::{Hash, Hasher}; +impl<'a> Hash for Test<'a> { + fn hash(&self, state: &mut H) { + use Test::*; + + match self { + IsCtor { tag_id, .. } => { + state.write_u8(0); + tag_id.hash(state); + // The point of this custom implementation is to not hash the tag arguments + } + IsInt(v, width) => { + state.write_u8(1); + v.hash(state); + width.hash(state); + } + IsFloat(v, width) => { + state.write_u8(2); + v.hash(state); + width.hash(state); + } + IsStr(v) => { + state.write_u8(3); + v.hash(state); + } + IsBit(v) => { + state.write_u8(4); + v.hash(state); + } + IsByte { tag_id, num_alts } => { + state.write_u8(5); + tag_id.hash(state); + num_alts.hash(state); + } + IsDecimal(v) => { + state.write_u8(6); + v.hash(state); + } + IsListLen { len, bound } => { + state.write_u8(7); + (len, bound).hash(state); + } + } + } +} + +impl<'a> Hash for GuardedTest<'a> { + fn hash(&self, state: &mut H) { + match self { + GuardedTest::GuardedNoTest { stmt_spec, .. } => { + state.write_u8(1); + stmt_spec.hash(state); + } + GuardedTest::TestNotGuarded { test } => { + state.write_u8(0); + test.hash(state); + } + GuardedTest::PlaceholderWithGuard => { + state.write_u8(2); + } + } + } +} + +// ACTUALLY BUILD DECISION TREES + +#[derive(Clone, Debug, PartialEq)] +struct Branch<'a> { + goal: Label, + guard: Guard<'a>, + patterns: Vec<(Vec, Pattern<'a>)>, +} + +fn to_decision_tree<'a>( + interner: &TLLayoutInterner<'a>, + raw_branches: Vec>, +) -> DecisionTree<'a> { + let branches: Vec<_> = raw_branches.into_iter().map(flatten_patterns).collect(); + + debug_assert!(!branches.is_empty()); + + match check_for_match(&branches) { + Match::Exact(goal) => DecisionTree::Match(goal), + + Match::GuardOnly => { + // the first branch has no more tests to do, but it has an if-guard + + let mut branches = branches; + let first = branches.remove(0); + + match first.guard { + Guard::NoGuard => unreachable!(), + + Guard::Guard { pattern, stmt_spec } => { + let guarded_test = GuardedTest::GuardedNoTest { pattern, stmt_spec }; + + // the guard test does not have a path + let path = vec![]; + + // we expect none of the patterns need tests, those decisions should have been made already + debug_assert!(first + .patterns + .iter() + .all(|(_, pattern)| !needs_tests(pattern))); + + let default = if branches.is_empty() { + None + } else { + Some(Box::new(to_decision_tree(interner, branches))) + }; + + DecisionTree::Decision { + path, + edges: vec![(guarded_test, DecisionTree::Match(first.goal))], + default, + } + } + } + } + + Match::None => { + // must clone here to release the borrow on `branches` + let path = pick_path(&branches).clone(); + + let bs = branches.clone(); + + let (edges, fallback) = gather_edges(interner, branches, &path); + + let mut decision_edges: Vec<_> = edges + .into_iter() + .map(|(test, branches)| { + if bs == branches { + panic!(); + } else { + (test, to_decision_tree(interner, branches)) + } + }) + .collect(); + + match (decision_edges.as_slice(), fallback.as_slice()) { + ([(_test, _decision_tree)], []) => { + // only one test with no fallback: we will always enter this branch + + // get the `_decision_tree` without cloning + decision_edges.pop().unwrap().1 + } + (_, []) => break_out_guard(path, decision_edges, None), + ([], _) => { + // should be guaranteed by the patterns + debug_assert!(!fallback.is_empty()); + to_decision_tree(interner, fallback) + } + (_, _) => break_out_guard( + path, + decision_edges, + Some(Box::new(to_decision_tree(interner, fallback))), + ), + } + } + } +} + +/// Give a guard it's own Decision +fn break_out_guard<'a>( + path: Vec, + mut edges: Vec<(GuardedTest<'a>, DecisionTree<'a>)>, + default: Option>>, +) -> DecisionTree<'a> { + match edges + .iter() + .position(|(t, _)| matches!(t, GuardedTest::PlaceholderWithGuard)) + { + None => DecisionTree::Decision { + path, + edges, + default, + }, + Some(index) => { + let (a, b) = edges.split_at_mut(index + 1); + + let new_default = break_out_guard(path.clone(), b.to_vec(), default); + + let mut left = a.to_vec(); + let guard = left.pop().unwrap(); + + let help = DecisionTree::Decision { + path: path.clone(), + edges: vec![guard], + default: Some(Box::new(new_default)), + }; + + DecisionTree::Decision { + path, + edges: left, + default: Some(Box::new(help)), + } + } + } +} + +fn guarded_tests_are_complete(tests: &[GuardedTest]) -> bool { + let length = tests.len(); + debug_assert!(length > 0); + + let no_guard = tests + .iter() + .all(|t| matches!(t, GuardedTest::TestNotGuarded { .. })); + + match tests.last().unwrap() { + GuardedTest::PlaceholderWithGuard => false, + GuardedTest::GuardedNoTest { .. } => false, + GuardedTest::TestNotGuarded { test } => no_guard && tests_are_complete_help(test, length), + } +} + +fn tests_are_complete_help(last_test: &Test, number_of_tests: usize) -> bool { + match last_test { + Test::IsCtor { union, .. } => number_of_tests == union.alternatives.len(), + Test::IsByte { num_alts, .. } => number_of_tests == *num_alts, + Test::IsBit(_) => number_of_tests == 2, + Test::IsInt(_, _) => false, + Test::IsFloat(_, _) => false, + Test::IsDecimal(_) => false, + Test::IsStr(_) => false, + Test::IsListLen { + bound: ListLenBound::AtLeast, + len: 0, + } => true, // [..] test + Test::IsListLen { .. } => false, + } +} + +fn flatten_patterns(branch: Branch) -> Branch { + let mut result = Vec::with_capacity(branch.patterns.len()); + + for path_pattern in branch.patterns { + flatten(path_pattern, &mut result); + } + + Branch { + patterns: result, + ..branch + } +} + +fn flatten<'a>( + path_pattern: (Vec, Pattern<'a>), + path_patterns: &mut Vec<(Vec, Pattern<'a>)>, +) { + match path_pattern.1 { + Pattern::AppliedTag { + union, + arguments, + tag_id, + tag_name, + layout, + } if union.alternatives.len() == 1 && !layout.is_nullable() => { + // TODO ^ do we need to check that guard.is_none() here? + + let path = path_pattern.0; + // Theory: unbox doesn't have any value for us, because one-element tag unions + // don't store the tag anyway. + if arguments.len() == 1 { + // NOTE here elm will unbox, but we don't use that + path_patterns.push(( + path, + Pattern::AppliedTag { + union, + arguments, + tag_id, + tag_name, + layout, + }, + )); + } else { + for (index, (arg_pattern, _)) in arguments.iter().enumerate() { + let mut new_path = path.clone(); + new_path.push(PathInstruction::TagIndex { + index: index as u64, + tag_id, + }); + + flatten((new_path, arg_pattern.clone()), path_patterns); + } + } + } + + _ => { + path_patterns.push(path_pattern); + } + } +} + +/// SUCCESSFULLY MATCH + +/// If the first branch has no more "decision points" we can finally take that +/// path. If that is the case we give the resulting label and a mapping from free +/// variables to "how to get their value". So a pattern like (Just (x,_)) will give +/// us something like ("x" => value.0.0) + +#[derive(Debug)] +enum Match { + Exact(Label), + GuardOnly, + None, +} + +fn check_for_match(branches: &[Branch]) -> Match { + match branches.get(0) { + Some(Branch { + goal, + patterns, + guard, + }) if patterns.iter().all(|(_, pattern)| !needs_tests(pattern)) => { + if guard.is_none() { + Match::Exact(*goal) + } else { + Match::GuardOnly + } + } + _ => Match::None, + } +} + +/// GATHER OUTGOING EDGES + +// my understanding: branches that we could jump to based on the pattern at the current path +fn gather_edges<'a>( + interner: &TLLayoutInterner<'a>, + branches: Vec>, + path: &[PathInstruction], +) -> (Vec<(GuardedTest<'a>, Vec>)>, Vec>) { + let relevant_tests = tests_at_path(path, &branches); + + let check = guarded_tests_are_complete(&relevant_tests); + + let all_edges = relevant_tests + .into_iter() + .map(|t| edges_for(interner, path, &branches, t)) + .collect(); + + let fallbacks = if check { + vec![] + } else { + branches + .into_iter() + .filter(|b| is_irrelevant_to(path, b)) + .collect() + }; + + (all_edges, fallbacks) +} + +/// FIND RELEVANT TESTS + +fn tests_at_path<'a>( + selected_path: &[PathInstruction], + branches: &[Branch<'a>], +) -> Vec> { + // NOTE the ordering of the result is important! + + let mut all_tests = Vec::new(); + + for branch in branches { + all_tests.extend(test_at_path(selected_path, branch)); + } + + // The rust HashMap also uses equality, here we really want to use the custom hash function + // defined on Test to determine whether a test is unique. So we have to do the hashing + // explicitly + + use std::collections::hash_map::DefaultHasher; + + let mut visited = MutSet::default(); + let mut unique = Vec::new(); + + for test in all_tests { + let hash = { + let mut hasher = DefaultHasher::new(); + test.hash(&mut hasher); + hasher.finish() + }; + + if !visited.contains(&hash) { + visited.insert(hash); + unique.push(test); + } + } + + unique +} + +fn test_for_pattern<'a>(pattern: &Pattern<'a>) -> Option> { + use Pattern::*; + use Test::*; + + let test = match pattern { + Identifier(_) | Underscore => { + return None; + } + + As(subpattern, _) => return test_for_pattern(subpattern), + + RecordDestructure(destructs, _) => { + // not rendered, so pick the easiest + let union = Union { + render_as: RenderAs::Tag, + alternatives: vec![Ctor { + tag_id: TagId(0), + name: CtorName::Tag(TagName(RECORD_TAG_NAME.into())), + arity: destructs.len(), + }], + }; + + let mut arguments = std::vec::Vec::new(); + + for destruct in destructs { + match &destruct.typ { + DestructType::Guard(guard) => { + arguments.push((guard.clone(), destruct.layout)); + } + DestructType::Required(_) => { + arguments.push((Pattern::Underscore, destruct.layout)); + } + } + } + + IsCtor { + tag_id: 0, + ctor_name: CtorName::Tag(TagName(RECORD_TAG_NAME.into())), + union, + arguments, + } + } + + TupleDestructure(destructs, _) => { + // not rendered, so pick the easiest + let union = Union { + render_as: RenderAs::Tag, + alternatives: vec![Ctor { + tag_id: TagId(0), + name: CtorName::Tag(TagName(TUPLE_TAG_NAME.into())), + arity: destructs.len(), + }], + }; + + let mut arguments = std::vec::Vec::new(); + + for destruct in destructs { + arguments.push((destruct.pat.clone(), destruct.layout)); + } + + IsCtor { + tag_id: 0, + ctor_name: CtorName::Tag(TagName(TUPLE_TAG_NAME.into())), + union, + arguments, + } + } + + NewtypeDestructure { + tag_name, + arguments, + } => { + let tag_id = 0; + let union = Union::newtype_wrapper(CtorName::Tag(tag_name.clone()), arguments.len()); + + IsCtor { + tag_id, + ctor_name: CtorName::Tag(tag_name.clone()), + union, + arguments: arguments.to_vec(), + } + } + + AppliedTag { + tag_name, + tag_id, + arguments, + union, + .. + } => IsCtor { + tag_id: *tag_id, + ctor_name: CtorName::Tag(tag_name.clone()), + union: union.clone(), + arguments: arguments.to_vec(), + }, + + List { + arity, + list_layout: _, + element_layout: _, + elements: _, + opt_rest: _, + } => IsListLen { + bound: match arity { + ListArity::Exact(_) => ListLenBound::Exact, + ListArity::Slice(_, _) => ListLenBound::AtLeast, + }, + len: arity.min_len() as _, + }, + + Voided { .. } => internal_error!("unreachable"), + + OpaqueUnwrap { opaque, argument } => { + let union = Union { + render_as: RenderAs::Tag, + alternatives: vec![Ctor { + tag_id: TagId(0), + name: CtorName::Opaque(*opaque), + arity: 1, + }], + }; + + IsCtor { + tag_id: 0, + ctor_name: CtorName::Opaque(*opaque), + union, + arguments: vec![(**argument).clone()], + } + } + + BitLiteral { value, .. } => IsBit(*value), + EnumLiteral { tag_id, union, .. } => IsByte { + tag_id: *tag_id as _, + num_alts: union.alternatives.len(), + }, + IntLiteral(v, precision) => IsInt(*v, *precision), + FloatLiteral(v, precision) => IsFloat(*v, *precision), + DecimalLiteral(v) => IsDecimal(*v), + StrLiteral(v) => IsStr(v.clone()), + }; + + Some(test) +} + +fn test_at_path<'a>( + selected_path: &[PathInstruction], + branch: &Branch<'a>, +) -> Option> { + let (_, pattern) = branch + .patterns + .iter() + .find(|(path, _)| path == selected_path)?; + + match test_for_pattern(pattern) { + Some(test) => Some(GuardedTest::TestNotGuarded { test }), + None => { + if let Guard::Guard { .. } = &branch.guard { + // no tests for this pattern remain, but we cannot discard it yet + // because it has a guard! + Some(GuardedTest::PlaceholderWithGuard) + } else { + None + } + } + } +} + +/// BUILD EDGES + +// understanding: if the test is successful, where could we go? +fn edges_for<'a>( + interner: &TLLayoutInterner<'a>, + path: &[PathInstruction], + branches: &[Branch<'a>], + test: GuardedTest<'a>, +) -> (GuardedTest<'a>, Vec>) { + let mut new_branches = Vec::new(); + + // if we test for a guard, skip all branches until one that has a guard + + let it = match test { + GuardedTest::GuardedNoTest { .. } => { + let index = branches + .iter() + .position(|b| b.guard.is_some()) + .expect("if testing for a guard, one branch must have a guard"); + + branches[index..].iter() + } + GuardedTest::PlaceholderWithGuard => { + // Skip all branches until we hit the one with a placeholder and a guard. + let index = branches + .iter() + .position(|b| { + if b.guard.is_none() { + return false; + } + + let (_, pattern) = b + .patterns + .iter() + .find(|(branch_path, _)| branch_path == path) + .expect( + "if testing for a placeholder with guard, must find a branch matching the path", + ); + + test_for_pattern(pattern).is_none() + }) + .expect("if testing for a guard, one branch must have a guard"); + + branches[index..].iter() + } + GuardedTest::TestNotGuarded { .. } => branches.iter(), + }; + + for branch in it { + new_branches.extend(to_relevant_branch(interner, &test, path, branch)); + } + + (test, new_branches) +} + +fn to_relevant_branch<'a>( + interner: &TLLayoutInterner<'a>, + guarded_test: &GuardedTest<'a>, + path: &[PathInstruction], + branch: &Branch<'a>, +) -> Option> { + // TODO remove clone + match extract(path, branch.patterns.clone()) { + Extract::NotFound => Some(branch.clone()), + Extract::Found { + start, + found_pattern: pattern, + end, + } => match guarded_test { + GuardedTest::PlaceholderWithGuard | GuardedTest::GuardedNoTest { .. } => { + // if there is no test, the pattern should not require any + debug_assert!( + matches!(pattern, Pattern::Identifier(_) | Pattern::Underscore,), + "{pattern:?}", + ); + + Some(branch.clone()) + } + GuardedTest::TestNotGuarded { test } => { + to_relevant_branch_help(interner, test, path, start, end, branch, pattern) + } + }, + } +} + +fn to_relevant_branch_help<'a>( + interner: &TLLayoutInterner<'a>, + test: &Test<'a>, + path: &[PathInstruction], + mut start: Vec<(Vec, Pattern<'a>)>, + end: Vec<(Vec, Pattern<'a>)>, + branch: &Branch<'a>, + pattern: Pattern<'a>, +) -> Option> { + use Pattern::*; + use Test::*; + + match pattern { + Identifier(_) | Underscore => Some(branch.clone()), + + As(subpattern, _symbol) => { + to_relevant_branch_help(interner, test, path, start, end, branch, *subpattern) + } + + RecordDestructure(destructs, _) => match test { + IsCtor { + ctor_name: test_name, + tag_id, + .. + } => { + debug_assert!(test_name == &CtorName::Tag(TagName(RECORD_TAG_NAME.into()))); + let destructs_len = destructs.len(); + let sub_positions = destructs.into_iter().enumerate().map(|(index, destruct)| { + let pattern = match destruct.typ { + DestructType::Guard(guard) => guard.clone(), + DestructType::Required(_) => Pattern::Underscore, + }; + + let mut new_path = path.to_vec(); + let next_instr = if destructs_len == 1 { + PathInstruction::NewType + } else { + PathInstruction::TagIndex { + index: index as u64, + tag_id: *tag_id, + } + }; + new_path.push(next_instr); + + (new_path, pattern) + }); + start.extend(sub_positions); + start.extend(end); + + Some(Branch { + goal: branch.goal, + guard: branch.guard.clone(), + patterns: start, + }) + } + _ => None, + }, + + TupleDestructure(destructs, _) => match test { + IsCtor { + ctor_name: test_name, + tag_id, + .. + } => { + debug_assert!(test_name == &CtorName::Tag(TagName(TUPLE_TAG_NAME.into()))); + let destructs_len = destructs.len(); + let sub_positions = destructs.into_iter().enumerate().map(|(index, destruct)| { + let pattern = destruct.pat.clone(); + + let mut new_path = path.to_vec(); + let next_instr = if destructs_len == 1 { + PathInstruction::NewType + } else { + PathInstruction::TagIndex { + index: index as u64, + tag_id: *tag_id, + } + }; + new_path.push(next_instr); + + (new_path, pattern) + }); + start.extend(sub_positions); + start.extend(end); + + Some(Branch { + goal: branch.goal, + guard: branch.guard.clone(), + patterns: start, + }) + } + _ => None, + }, + + OpaqueUnwrap { opaque, argument } => match test { + IsCtor { + ctor_name: test_opaque_tag_name, + tag_id, + .. + } => { + debug_assert_eq!(*tag_id, 0); + debug_assert_eq!(test_opaque_tag_name, &CtorName::Opaque(opaque)); + + let (argument, _) = *argument; + + let mut new_path = path.to_vec(); + new_path.push(PathInstruction::NewType); + + start.push((new_path, argument)); + start.extend(end); + Some(Branch { + goal: branch.goal, + guard: branch.guard.clone(), + patterns: start, + }) + } + _ => None, + }, + + List { + arity: my_arity, + elements, + list_layout: _, + element_layout: _, + opt_rest: _, + } => match test { + IsListLen { + bound: test_bound, + len, + } if my_arity.covers_length(*len as _) + // Spread tests [_, ..] can only match spread tests, not exact-sized bounds [_]. + // + // On the other hand, exact-sized tests [_] can match spread bounds [_, ..], + // because each spread bound generates 0 or more exact-sized tests. + // + // See exhaustiveness checking of lists for more details on the tests generated + // for spread bounds. + && !matches!( + (test_bound, my_arity), + (ListLenBound::AtLeast, ListArity::Exact(..)) + ) => + { + let sub_positions = elements.into_iter().enumerate().map(|(index, elem_pat)| { + let mut new_path = path.to_vec(); + + let probe_index = ListIndex::from_pattern_index(index, my_arity); + + let next_instr = PathInstruction::ListIndex { + index: probe_index as _, + }; + new_path.push(next_instr); + + (new_path, elem_pat) + }); + start.extend(sub_positions); + start.extend(end); + + Some(Branch { + goal: branch.goal, + guard: branch.guard.clone(), + patterns: start, + }) + } + + _ => None, + }, + + NewtypeDestructure { + tag_name, + arguments, + .. + } => match test { + IsCtor { + ctor_name: test_name, + tag_id: test_id, + .. + } if test_name.is_tag(&tag_name) => { + let tag_id = 0; + debug_assert_eq!(tag_id, *test_id); + + let num_args = arguments.len(); + let sub_positions = + arguments + .into_iter() + .enumerate() + .map(|(index, (pattern, _))| { + let mut new_path = path.to_vec(); + let next_instr = if num_args == 1 { + PathInstruction::NewType + } else { + PathInstruction::TagIndex { + index: index as u64, + tag_id, + } + }; + new_path.push(next_instr); + (new_path, pattern) + }); + start.extend(sub_positions); + start.extend(end); + + Some(Branch { + goal: branch.goal, + guard: branch.guard.clone(), + patterns: start, + }) + } + + _ => None, + }, + + AppliedTag { + tag_name, + tag_id, + arguments, + layout, + .. + } => { + match test { + IsCtor { + ctor_name: test_name, + tag_id: test_id, + .. + } if test_name.is_tag(&tag_name) => { + debug_assert_eq!(tag_id, *test_id); + + // the test matches the constructor of this pattern + match layout { + UnionLayout::NonRecursive([[arg]]) + if matches!(interner.get_repr(*arg), LayoutRepr::Struct([_],)) => + { + // a one-element record equivalent + // Theory: Unbox doesn't have any value for us + debug_assert_eq!(arguments.len(), 1); + let arg = arguments[0].clone(); + { + // NOTE here elm unboxes, but we ignore that + // Path::Unbox(Box::new(path.clone())) + start.push((path.to_vec(), arg.0)); + start.extend(end); + } + } + UnionLayout::NonRecursive([_]) | UnionLayout::NonNullableUnwrapped(_) => { + let sub_positions = + arguments + .into_iter() + .enumerate() + .map(|(index, (pattern, _))| { + let mut new_path = path.to_vec(); + new_path.push(PathInstruction::TagIndex { + index: index as u64, + tag_id, + }); + (new_path, pattern) + }); + start.extend(sub_positions); + start.extend(end); + } + UnionLayout::NonRecursive(_) + | UnionLayout::Recursive(_) + | UnionLayout::NullableWrapped { .. } + | UnionLayout::NullableUnwrapped { .. } => { + let sub_positions = + arguments + .into_iter() + .enumerate() + .map(|(index, (pattern, _))| { + let mut new_path = path.to_vec(); + new_path.push(PathInstruction::TagIndex { + index: index as u64, + tag_id, + }); + (new_path, pattern) + }); + start.extend(sub_positions); + start.extend(end); + } + } + + Some(Branch { + goal: branch.goal, + guard: branch.guard.clone(), + patterns: start, + }) + } + _ => None, + } + } + Voided { .. } => internal_error!("unreachable"), + StrLiteral(string) => match test { + IsStr(test_str) if string == *test_str => { + start.extend(end); + Some(Branch { + goal: branch.goal, + guard: branch.guard.clone(), + patterns: start, + }) + } + _ => None, + }, + + IntLiteral(int, p1) => match test { + IsInt(is_int, p2) if int == *is_int => { + debug_assert_eq!(p1, *p2); + start.extend(end); + Some(Branch { + goal: branch.goal, + guard: branch.guard.clone(), + patterns: start, + }) + } + _ => None, + }, + + FloatLiteral(float, p1) => match test { + IsFloat(test_float, p2) if float == *test_float => { + debug_assert_eq!(p1, *p2); + start.extend(end); + Some(Branch { + goal: branch.goal, + guard: branch.guard.clone(), + patterns: start, + }) + } + _ => None, + }, + + DecimalLiteral(dec) => match test { + IsDecimal(test_dec) if dec.eq(test_dec) => { + start.extend(end); + Some(Branch { + goal: branch.goal, + guard: branch.guard.clone(), + patterns: start, + }) + } + _ => None, + }, + + BitLiteral { value: bit, .. } => match test { + IsBit(test_bit) if bit == *test_bit => { + start.extend(end); + Some(Branch { + goal: branch.goal, + guard: branch.guard.clone(), + patterns: start, + }) + } + _ => None, + }, + + EnumLiteral { tag_id, .. } => match test { + IsByte { + tag_id: test_id, .. + } if tag_id == *test_id as u8 => { + start.extend(end); + Some(Branch { + goal: branch.goal, + guard: branch.guard.clone(), + patterns: start, + }) + } + + _ => None, + }, + } +} + +enum Extract<'a> { + NotFound, + Found { + start: Vec<(Vec, Pattern<'a>)>, + found_pattern: Pattern<'a>, + end: Vec<(Vec, Pattern<'a>)>, + }, +} + +fn extract<'a>( + selected_path: &[PathInstruction], + path_patterns: Vec<(Vec, Pattern<'a>)>, +) -> Extract<'a> { + let mut start = Vec::new(); + + // TODO potential ordering problem + let mut it = path_patterns.into_iter(); + while let Some(current) = it.next() { + if current.0 == selected_path { + return Extract::Found { + start, + found_pattern: current.1, + end: it.collect::>(), + }; + } else { + start.push(current); + } + } + + Extract::NotFound +} + +/// FIND IRRELEVANT BRANCHES + +fn is_irrelevant_to(selected_path: &[PathInstruction], branch: &Branch) -> bool { + match branch + .patterns + .iter() + .find(|(path, _)| path == selected_path) + { + None => true, + Some((_, pattern)) => branch.guard.is_none() && !needs_tests(pattern), + } +} + +/// Does this pattern need a branch test? +/// +/// Keep up to date with [needs_path_instruction]. +fn needs_tests(pattern: &Pattern) -> bool { + use Pattern::*; + + match pattern { + Identifier(_) | Underscore => false, + + As(subpattern, _) => needs_tests(subpattern), + + NewtypeDestructure { .. } + | RecordDestructure(..) + | TupleDestructure(..) + | AppliedTag { .. } + | OpaqueUnwrap { .. } + | BitLiteral { .. } + | EnumLiteral { .. } + | IntLiteral(_, _) + | FloatLiteral(_, _) + | DecimalLiteral(_) + | StrLiteral(_) + | List { .. } => true, + + Voided { .. } => internal_error!("unreachable"), + } +} + +/// PICK A PATH + +fn pick_path<'a>(branches: &'a [Branch]) -> &'a Vec { + let mut all_paths = Vec::with_capacity(branches.len()); + + // is choice path + for branch in branches { + for (path, pattern) in &branch.patterns { + // NOTE we no longer check for the guard here + // if !branch.guard.is_none() || needs_tests(&pattern) { + if needs_tests(pattern) { + all_paths.push(path); + } else { + // do nothing + } + } + } + + let mut by_small_defaults = bests_by_small_defaults(branches, all_paths.into_iter()); + + if by_small_defaults.len() == 1 { + by_small_defaults.remove(0) + } else { + debug_assert!(!by_small_defaults.is_empty()); + let mut result = bests_by_small_branching_factor(branches, by_small_defaults.into_iter()); + + match result.pop() { + None => unreachable!("bests_by will always return at least one value in the vec"), + Some(path) => path, + } + } +} + +fn bests_by_small_branching_factor<'a, I>( + branches: &[Branch], + mut all_paths: I, +) -> Vec<&'a Vec> +where + I: Iterator>, +{ + match all_paths.next() { + None => panic!("Cannot choose the best of zero paths. This should never happen."), + Some(first_path) => { + let mut min_weight = small_branching_factor(branches, first_path); + let mut min_paths = vec![first_path]; + + for path in all_paths { + let weight = small_branching_factor(branches, path); + + use std::cmp::Ordering; + match weight.cmp(&min_weight) { + Ordering::Equal => { + min_paths.push(path); + } + Ordering::Less => { + min_weight = weight; + min_paths.clear(); + min_paths.push(path); + } + Ordering::Greater => {} + } + } + + min_paths + } + } +} + +fn bests_by_small_defaults<'a, I>( + branches: &[Branch], + mut all_paths: I, +) -> Vec<&'a Vec> +where + I: Iterator>, +{ + match all_paths.next() { + None => panic!("Cannot choose the best of zero paths. This should never happen."), + Some(first_path) => { + let mut min_weight = small_defaults(branches, first_path); + let mut min_paths = vec![first_path]; + + for path in all_paths { + let weight = small_defaults(branches, path); + + use std::cmp::Ordering; + match weight.cmp(&min_weight) { + Ordering::Equal => { + min_paths.push(path); + } + Ordering::Less => { + min_weight = weight; + min_paths.clear(); + min_paths.push(path); + } + Ordering::Greater => {} + } + } + + min_paths + } + } +} + +/// PATH PICKING HEURISTICS + +fn small_defaults(branches: &[Branch], path: &[PathInstruction]) -> usize { + branches + .iter() + .filter(|b| is_irrelevant_to(path, b)) + .map(|_| 1) + .sum() +} + +fn small_branching_factor(branches: &[Branch], path: &[PathInstruction]) -> usize { + // a specialized version of gather_edges that just counts the number of options + + let relevant_tests = tests_at_path(path, branches); + + let check = guarded_tests_are_complete(&relevant_tests); + + let fallbacks = if check { + false + } else { + branches.iter().any(|b| is_irrelevant_to(path, b)) + }; + + relevant_tests.len() + usize::from(fallbacks) +} + +#[derive(Debug, PartialEq)] +enum Decider<'a, T> { + Leaf(T), + Guarded { + pattern: Pattern<'a>, + + /// The guard expression and how to compile it. + stmt_spec: GuardStmtSpec, + + success: Box>, + failure: Box>, + }, + Chain { + test_chain: Vec<(Vec, Test<'a>)>, + success: Box>, + failure: Box>, + }, + FanOut { + path: Vec, + tests: Vec<(Test<'a>, Decider<'a, T>)>, + fallback: Box>, + }, +} + +#[derive(Clone, Debug, PartialEq)] +enum Choice<'a> { + Inline(Stmt<'a>), + Jump(Label), +} + +type StoresVec<'a> = bumpalo::collections::Vec<'a, (Symbol, InLayout<'a>, Expr<'a>)>; + +struct JumpSpec<'a> { + target_index: u64, + id: JoinPointId, + /// Symbols, from the unpacked pattern, to add on when jumping to the target. + jump_pattern_param_symbols: &'a [Symbol], + + // Used to construct the joinpoint + join_params: &'a [Param<'a>], + join_body: Stmt<'a>, +} + +pub(crate) fn optimize_when<'a>( + env: &mut Env<'a, '_>, + procs: &mut Procs<'a>, + layout_cache: &mut LayoutCache<'a>, + cond_symbol: Symbol, + cond_layout: InLayout<'a>, + ret_layout: InLayout<'a>, + opt_branches: bumpalo::collections::Vec<'a, (Pattern<'a>, Guard<'a>, Stmt<'a>)>, +) -> Stmt<'a> { + let (patterns, indexed_branches): (_, Vec<_>) = opt_branches + .into_iter() + .enumerate() + .map(|(index, (pattern, guard, branch))| { + let has_guard = guard.is_some(); + ( + (guard, pattern.clone(), index as u64), + (index as u64, branch, pattern, has_guard), + ) + }) + .unzip(); + + let decision_tree = compile(&layout_cache.interner, patterns); + let decider = tree_to_decider(decision_tree); + + // for each target (branch body), count in how many ways it can be reached + let mut target_counts = bumpalo::vec![in env.arena; 0; indexed_branches.len()]; + count_targets(&mut target_counts, &decider); + + let mut choices = MutMap::default(); + let mut jumps = Vec::new(); + + for (target, mut branch, pattern, has_guard) in indexed_branches.into_iter() { + let should_inline = { + let target_counts = &target_counts; + match target_counts.get(target as usize) { + None => unreachable!( + "this should never happen: {:?} not in {:?}", + target, target_counts + ), + Some(count) => *count == 1, + } + }; + + let join_params: &'a [Param<'a>]; + let jump_pattern_param_symbols: &'a [Symbol]; + match (has_guard, should_inline) { + (false, _) => { + // Bind the fields referenced in the pattern. + branch = store_pattern(env, procs, layout_cache, &pattern, cond_symbol, branch); + + join_params = &[]; + jump_pattern_param_symbols = &[]; + } + (true, true) => { + // Nothing more to do - the patterns will be bound when the guard is evaluated in + // `decide_to_branching`. + join_params = &[]; + jump_pattern_param_symbols = &[]; + } + (true, false) => { + // The patterns will be bound when the guard is evaluated, and then we need to get + // them back into the joinpoint here. + // + // So, figure out what symbols the pattern binds, and update the joinpoint + // parameter to take each symbol. Then, when the joinpoint is called, the unpacked + // symbols will be filled in. + // + // Since the joinpoint's parameters will be fresh symbols, the join body also needs + // updating. + let pattern_bindings = pattern.collect_symbols(cond_layout); + + let mut parameters_buf = bumpalo::collections::Vec::with_capacity_in(1, env.arena); + let mut pattern_symbols_buf = + bumpalo::collections::Vec::with_capacity_in(1, env.arena); + let mut substitutions = BumpMap::default(); + + for (pattern_symbol, layout) in pattern_bindings { + let param_symbol = env.unique_symbol(); + parameters_buf.push(Param { + symbol: param_symbol, + layout, + }); + pattern_symbols_buf.push(pattern_symbol); + substitutions.insert(pattern_symbol, param_symbol); + } + + join_params = parameters_buf.into_bump_slice(); + jump_pattern_param_symbols = pattern_symbols_buf.into_bump_slice(); + + substitute_in_exprs_many(env.arena, &mut branch, substitutions); + } + } + + let ((branch_index, choice), opt_jump) = if should_inline { + ((target, Choice::Inline(branch)), None) + } else { + ((target, Choice::Jump(target)), Some((target, branch))) + }; + + if let Some((target_index, body)) = opt_jump { + let id = JoinPointId(env.unique_symbol()); + jumps.push(JumpSpec { + target_index, + id, + jump_pattern_param_symbols, + join_params, + join_body: body, + }); + } + + choices.insert(branch_index, choice); + } + + let choice_decider = insert_choices(&choices, decider); + + let mut stmt = decide_to_branching( + env, + procs, + layout_cache, + cond_symbol, + cond_layout, + ret_layout, + choice_decider, + &jumps, + ); + + for JumpSpec { + target_index: _, + id, + jump_pattern_param_symbols: _, + join_params, + join_body, + } in jumps.into_iter() + { + stmt = Stmt::Join { + id, + parameters: join_params, + body: env.arena.alloc(join_body), + remainder: env.arena.alloc(stmt), + }; + } + + stmt +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum PathInstruction { + NewType, + TagIndex { index: u64, tag_id: TagIdIntType }, + ListIndex { index: ListIndex }, +} + +fn path_to_expr_help<'a>( + env: &mut Env<'a, '_>, + layout_interner: &mut TLLayoutInterner<'a>, + mut symbol: Symbol, + path: &[PathInstruction], + mut layout: InLayout<'a>, +) -> (Symbol, StoresVec<'a>, InLayout<'a>) { + let mut stores = bumpalo::collections::Vec::new_in(env.arena); + + // let instructions = reverse_path(path); + let instructions = path; + let mut it = instructions.iter().peekable(); + + while let Some(path_instr) = it.next() { + match path_instr { + PathInstruction::NewType => { + // pass through + } + + PathInstruction::TagIndex { index, tag_id } => { + let index = *index; + + match layout_interner.chase_recursive(layout) { + LayoutRepr::Union(union_layout) => { + let inner_expr = Expr::UnionAtIndex { + tag_id: *tag_id, + structure: symbol, + index, + union_layout, + }; + + let inner_layout = union_layout.layout_at( + layout_interner, + *tag_id as TagIdIntType, + index as usize, + ); + + symbol = env.unique_symbol(); + stores.push((symbol, inner_layout, inner_expr)); + + layout = inner_layout; + } + + LayoutRepr::Struct(field_layouts) => { + debug_assert!(field_layouts.len() > 1); + + let inner_expr = Expr::StructAtIndex { + index, + field_layouts, + structure: symbol, + }; + + let inner_layout = field_layouts[index as usize]; + + symbol = env.unique_symbol(); + stores.push((symbol, inner_layout, inner_expr)); + + layout = inner_layout; + } + + _ => { + // this MUST be an index into a single-element (hence unwrapped) record + + debug_assert_eq!(index, 0, "{:?}", &layout); + debug_assert_eq!(*tag_id, 0); + debug_assert!(it.peek().is_none()); + + break; + } + } + } + + PathInstruction::ListIndex { index } => { + let list_sym = symbol; + + match layout_interner.get_repr(layout) { + LayoutRepr::Builtin(Builtin::List(elem_layout)) => { + let (index_sym, new_stores) = build_list_index_probe(env, list_sym, index); + + stores.extend(new_stores); + + let load_sym = env.unique_symbol(); + let load_expr = Expr::Call(Call { + call_type: CallType::LowLevel { + op: LowLevel::ListGetUnsafe, + update_mode: env.next_update_mode_id(), + }, + arguments: env.arena.alloc([list_sym, index_sym]), + }); + + stores.push((load_sym, elem_layout, load_expr)); + + layout = elem_layout; + symbol = load_sym; + } + _ => internal_error!("not a list"), + } + } + } + } + + (symbol, stores, layout) +} + +fn test_to_comparison<'a>( + env: &mut Env<'a, '_>, + layout_interner: &mut TLLayoutInterner<'a>, + cond_symbol: Symbol, + cond_layout: &InLayout<'a>, + path: &[PathInstruction], + test: Test<'a>, +) -> (StoresVec<'a>, Comparison, Option>) { + let (rhs_symbol, mut stores, test_layout) = + path_to_expr_help(env, layout_interner, cond_symbol, path, *cond_layout); + + match test { + Test::IsCtor { tag_id, union, .. } => { + let path_symbol = rhs_symbol; + // the IsCtor check should never be generated for tag unions of size 1 + // (e.g. record pattern guard matches) + debug_assert!(union.alternatives.len() > 1); + + match layout_interner.chase_recursive(test_layout) { + LayoutRepr::Union(union_layout) => { + let lhs = Expr::Literal(Literal::Int((tag_id as i128).to_ne_bytes())); + + let rhs = Expr::GetTagId { + structure: path_symbol, + union_layout, + }; + + let lhs_symbol = env.unique_symbol(); + let rhs_symbol = env.unique_symbol(); + + let tag_id_layout = union_layout.tag_id_layout(); + + stores.push((lhs_symbol, tag_id_layout, lhs)); + stores.push((rhs_symbol, tag_id_layout, rhs)); + + ( + stores, + (lhs_symbol, Comparator::Eq, rhs_symbol), + Some(ConstructorKnown::OneTag { + scrutinee: path_symbol, + layout: *cond_layout, + tag_id, + }), + ) + } + _ => unreachable!("{:#?}", (cond_layout, union, test_layout, path)), + } + } + + Test::IsInt(test_int, precision) => { + let lhs = Expr::Literal(Literal::Int(test_int)); + let lhs_symbol = env.unique_symbol(); + stores.push((lhs_symbol, Layout::int_width(precision), lhs)); + + (stores, (lhs_symbol, Comparator::Eq, rhs_symbol), None) + } + + Test::IsFloat(test_int, precision) => { + // TODO maybe we can actually use i64 comparison here? + let test_float = f64::from_bits(test_int); + let lhs = Expr::Literal(Literal::Float(test_float)); + let lhs_symbol = env.unique_symbol(); + stores.push((lhs_symbol, Layout::float_width(precision), lhs)); + + (stores, (lhs_symbol, Comparator::Eq, rhs_symbol), None) + } + + Test::IsDecimal(test_dec) => { + let lhs = Expr::Literal(Literal::Decimal(test_dec)); + let lhs_symbol = env.unique_symbol(); + stores.push((lhs_symbol, *cond_layout, lhs)); + + (stores, (lhs_symbol, Comparator::Eq, rhs_symbol), None) + } + + Test::IsByte { + tag_id: test_byte, .. + } => { + debug_assert!(test_byte <= (u8::MAX as u16)); + + let lhs = Expr::Literal(Literal::Byte(test_byte as u8)); + let lhs_symbol = env.unique_symbol(); + stores.push((lhs_symbol, Layout::U8, lhs)); + + (stores, (lhs_symbol, Comparator::Eq, rhs_symbol), None) + } + + Test::IsBit(test_bit) => { + let lhs = Expr::Literal(Literal::Bool(test_bit)); + let lhs_symbol = env.unique_symbol(); + stores.push((lhs_symbol, Layout::BOOL, lhs)); + + (stores, (lhs_symbol, Comparator::Eq, rhs_symbol), None) + } + + Test::IsStr(test_str) => { + let lhs = Expr::Literal(Literal::Str(env.arena.alloc(test_str))); + let lhs_symbol = env.unique_symbol(); + + stores.push((lhs_symbol, Layout::STR, lhs)); + + (stores, (lhs_symbol, Comparator::Eq, rhs_symbol), None) + } + + Test::IsListLen { bound, len } => { + let list_layout = test_layout; + let list_sym = rhs_symbol; + + match layout_interner.get_repr(list_layout) { + LayoutRepr::Builtin(Builtin::List(_elem_layout)) => { + let real_len_expr = Expr::Call(Call { + call_type: CallType::LowLevel { + op: LowLevel::ListLen, + update_mode: env.next_update_mode_id(), + }, + arguments: env.arena.alloc([list_sym]), + }); + let test_len_expr = Expr::Literal(Literal::Int((len as i128).to_ne_bytes())); + + let real_len = env.unique_symbol(); + let test_len = env.unique_symbol(); + + let usize_layout = Layout::usize(env.target_info); + + stores.push((real_len, usize_layout, real_len_expr)); + stores.push((test_len, usize_layout, test_len_expr)); + + let comparison = match bound { + ListLenBound::Exact => (real_len, Comparator::Eq, test_len), + ListLenBound::AtLeast => (real_len, Comparator::Geq, test_len), + }; + + (stores, comparison, None) + } + _ => internal_error!( + "test path is not a list: {:#?}", + (cond_layout, test_layout, path) + ), + } + } + } +} + +#[derive(Debug, Clone, Copy)] +enum Comparator { + Eq, + Geq, +} + +type Comparison = (Symbol, Comparator, Symbol); + +type Tests<'a> = std::vec::Vec<( + bumpalo::collections::Vec<'a, (Symbol, InLayout<'a>, Expr<'a>)>, + Comparison, + Option>, +)>; + +fn stores_and_condition<'a>( + env: &mut Env<'a, '_>, + layout_interner: &mut TLLayoutInterner<'a>, + cond_symbol: Symbol, + cond_layout: &InLayout<'a>, + test_chain: Vec<(Vec, Test<'a>)>, +) -> Tests<'a> { + let mut tests: Tests = Vec::with_capacity(test_chain.len()); + + // Assumption: there is at most 1 guard, and it is the outer layer. + for (path, test) in test_chain { + tests.push(test_to_comparison( + env, + layout_interner, + cond_symbol, + cond_layout, + &path, + test, + )) + } + + tests +} + +#[allow(clippy::too_many_arguments)] +fn compile_test<'a>( + env: &mut Env<'a, '_>, + ret_layout: InLayout<'a>, + stores: bumpalo::collections::Vec<'a, (Symbol, InLayout<'a>, Expr<'a>)>, + lhs: Symbol, + cmp: Comparator, + rhs: Symbol, + fail: &'a Stmt<'a>, + cond: Stmt<'a>, +) -> Stmt<'a> { + compile_test_help( + env, + ConstructorKnown::None, + ret_layout, + stores, + lhs, + cmp, + rhs, + fail, + cond, + ) +} + +#[allow(clippy::too_many_arguments)] +fn compile_test_help<'a>( + env: &mut Env<'a, '_>, + branch_info: ConstructorKnown<'a>, + ret_layout: InLayout<'a>, + stores: bumpalo::collections::Vec<'a, (Symbol, InLayout<'a>, Expr<'a>)>, + lhs: Symbol, + cmp: Comparator, + rhs: Symbol, + fail: &'a Stmt<'a>, + mut cond: Stmt<'a>, +) -> Stmt<'a> { + // if test_symbol then cond else fail + let test_symbol = env.unique_symbol(); + let arena = env.arena; + + let (pass_info, fail_info) = { + use ConstructorKnown::*; + match branch_info { + BothTags { + scrutinee, + layout, + pass, + fail, + } => { + let pass_info = BranchInfo::Constructor { + scrutinee, + layout, + tag_id: pass, + }; + let fail_info = BranchInfo::Constructor { + scrutinee, + layout, + tag_id: fail, + }; + + (pass_info, fail_info) + } + + OneTag { + scrutinee, + layout, + tag_id, + } => { + let pass_info = BranchInfo::Constructor { + scrutinee, + layout, + tag_id, + }; + + (pass_info, BranchInfo::None) + } + + ListLen { scrutinee, len } => { + let pass_info = BranchInfo::List { scrutinee, len }; + + (pass_info, BranchInfo::None) + } + + None => (BranchInfo::None, BranchInfo::None), + } + }; + + let branches = env.arena.alloc([(1u64, pass_info, cond)]); + let default_branch = (fail_info, &*env.arena.alloc(fail.clone())); + + cond = Stmt::Switch { + cond_symbol: test_symbol, + cond_layout: Layout::BOOL, + ret_layout, + branches, + default_branch, + }; + + let op = match cmp { + Comparator::Eq => LowLevel::Eq, + Comparator::Geq => LowLevel::NumGte, + }; + let test = Expr::Call(crate::ir::Call { + call_type: crate::ir::CallType::LowLevel { + op, + update_mode: env.next_update_mode_id(), + }, + arguments: arena.alloc([lhs, rhs]), + }); + + // write to the test symbol + cond = Stmt::Let(test_symbol, test, Layout::BOOL, arena.alloc(cond)); + + // stores are in top-to-bottom order, so we have to add them in reverse + for (symbol, layout, expr) in stores.into_iter().rev() { + cond = Stmt::Let(symbol, expr, layout, arena.alloc(cond)); + } + + cond +} + +fn compile_tests<'a>( + env: &mut Env<'a, '_>, + ret_layout: InLayout<'a>, + tests: Tests<'a>, + fail: &'a Stmt<'a>, + mut cond: Stmt<'a>, +) -> Stmt<'a> { + for (new_stores, (lhs, cmp, rhs), opt_constructor_info) in tests.into_iter() { + match opt_constructor_info { + None => { + cond = compile_test(env, ret_layout, new_stores, lhs, cmp, rhs, fail, cond); + } + Some(cinfo) => { + cond = compile_test_help( + env, cinfo, ret_layout, new_stores, lhs, cmp, rhs, fail, cond, + ); + } + } + } + cond +} + +#[derive(Debug)] +enum ConstructorKnown<'a> { + BothTags { + scrutinee: Symbol, + layout: InLayout<'a>, + pass: TagIdIntType, + fail: TagIdIntType, + }, + OneTag { + scrutinee: Symbol, + layout: InLayout<'a>, + tag_id: TagIdIntType, + }, + ListLen { + scrutinee: Symbol, + len: u64, + }, + None, +} + +impl<'a> ConstructorKnown<'a> { + fn from_test_chain( + cond_symbol: Symbol, + cond_layout: InLayout<'a>, + test_chain: &[(Vec, Test)], + ) -> Self { + match test_chain { + [(path, test)] => match test { + Test::IsCtor { tag_id, union, .. } if path.is_empty() => { + if union.alternatives.len() == 2 { + // excluded middle: we also know the tag_id in the fail branch + ConstructorKnown::BothTags { + layout: cond_layout, + scrutinee: cond_symbol, + pass: *tag_id, + fail: (*tag_id == 0) as _, + } + } else { + ConstructorKnown::OneTag { + layout: cond_layout, + scrutinee: cond_symbol, + tag_id: *tag_id, + } + } + } + Test::IsListLen { + bound: ListLenBound::Exact, + len, + } if path.is_empty() => ConstructorKnown::ListLen { + scrutinee: cond_symbol, + len: *len, + }, + _ => ConstructorKnown::None, + }, + _ => ConstructorKnown::None, + } + } +} + +// TODO procs and layout are currently unused, but potentially required +// for defining optional fields? +// if not, do remove +#[allow(clippy::too_many_arguments, clippy::needless_collect)] +fn decide_to_branching<'a>( + env: &mut Env<'a, '_>, + procs: &mut Procs<'a>, + layout_cache: &mut LayoutCache<'a>, + cond_symbol: Symbol, + cond_layout: InLayout<'a>, + ret_layout: InLayout<'a>, + decider: Decider<'a, Choice<'a>>, + jumps: &[JumpSpec<'a>], +) -> Stmt<'a> { + use Choice::*; + use Decider::*; + + let arena = env.arena; + + match decider { + Leaf(Jump(label)) => { + let index = jumps + .binary_search_by_key(&label, |r| r.target_index) + .expect("jump not in list of jumps"); + + Stmt::Jump(jumps[index].id, jumps[index].jump_pattern_param_symbols) + } + Leaf(Inline(expr)) => expr, + Guarded { + pattern, + stmt_spec, + success, + failure, + } => { + // the guard is the final thing that we check, so needs to be layered on first! + let test_symbol = env.unique_symbol(); + let arena = env.arena; + + let pass_expr = decide_to_branching( + env, + procs, + layout_cache, + cond_symbol, + cond_layout, + ret_layout, + *success, + jumps, + ); + + let fail_expr = decide_to_branching( + env, + procs, + layout_cache, + cond_symbol, + cond_layout, + ret_layout, + *failure, + jumps, + ); + + let decide = crate::ir::cond( + env, + test_symbol, + Layout::BOOL, + pass_expr, + fail_expr, + ret_layout, + ); + + // calculate the guard value + let param = Param { + symbol: test_symbol, + layout: Layout::BOOL, + }; + + let CompiledGuardStmt { + join_point_id, + stmt, + } = stmt_spec.generate_guard_and_join(env, procs, layout_cache); + + let join = Stmt::Join { + id: join_point_id, + parameters: arena.alloc([param]), + body: arena.alloc(decide), + remainder: arena.alloc(stmt), + }; + + store_pattern(env, procs, layout_cache, &pattern, cond_symbol, join) + } + Chain { + test_chain, + success, + failure, + } => { + // generate a (nested) if-then-else + + let pass_expr = decide_to_branching( + env, + procs, + layout_cache, + cond_symbol, + cond_layout, + ret_layout, + *success, + jumps, + ); + + let fail_expr = decide_to_branching( + env, + procs, + layout_cache, + cond_symbol, + cond_layout, + ret_layout, + *failure, + jumps, + ); + + let chain_branch_info = + ConstructorKnown::from_test_chain(cond_symbol, cond_layout, &test_chain); + + let tests = stores_and_condition( + env, + &mut layout_cache.interner, + cond_symbol, + &cond_layout, + test_chain, + ); + + let number_of_tests = tests.len() as i64; + + debug_assert!(number_of_tests > 0); + + let fail = env.arena.alloc(fail_expr); + if number_of_tests == 1 { + // if there is just one test, compile to a simple if-then-else + + let (new_stores, (lhs, cmp, rhs), _cinfo) = tests.into_iter().next().unwrap(); + + compile_test_help( + env, + chain_branch_info, + ret_layout, + new_stores, + lhs, + cmp, + rhs, + fail, + pass_expr, + ) + } else { + // otherwise, we use a join point so the code for the `else` case + // is only generated once. + let fail_jp_id = JoinPointId(env.unique_symbol()); + let jump = arena.alloc(Stmt::Jump(fail_jp_id, &[])); + + let test_stmt = compile_tests(env, ret_layout, tests, jump, pass_expr); + + Stmt::Join { + id: fail_jp_id, + parameters: &[], + body: fail, + remainder: arena.alloc(test_stmt), + } + } + } + FanOut { + path, + tests, + fallback, + } => { + // the cond_layout can change in the process. E.g. if the cond is a Tag, we actually + // switch on the tag discriminant (currently an i64 value) + // NOTE the tag discriminant is not actually loaded, `cond` can point to a tag + let (inner_cond_symbol, cond_stores_vec, inner_cond_layout) = path_to_expr_help( + env, + &mut layout_cache.interner, + cond_symbol, + &path, + cond_layout, + ); + + let default_branch = decide_to_branching( + env, + procs, + layout_cache, + cond_symbol, + cond_layout, + ret_layout, + *fallback, + jumps, + ); + + let mut branches = bumpalo::collections::Vec::with_capacity_in(tests.len(), env.arena); + + let mut tag_id_sum: i64 = (0..tests.len() as i64 + 1).sum(); + let mut union_size: i64 = -1; + + for (test, decider) in tests { + let branch = decide_to_branching( + env, + procs, + layout_cache, + cond_symbol, + cond_layout, + ret_layout, + decider, + jumps, + ); + + let tag = match test { + Test::IsInt(v, _) => i128::from_ne_bytes(v) as u64, + Test::IsFloat(_, _) => unreachable!("floats cannot be switched on"), + Test::IsBit(v) => v as u64, + Test::IsByte { tag_id, .. } => tag_id as u64, + Test::IsCtor { tag_id, .. } => tag_id as u64, + Test::IsListLen { len, bound } => match bound { + ListLenBound::Exact => len as _, + ListLenBound::AtLeast => { + unreachable!("at-least bounds cannot be switched on") + } + }, + Test::IsDecimal(_) => unreachable!("decimals cannot be switched on"), + Test::IsStr(_) => unreachable!("strings cannot be switched on"), + }; + + // branch info is only useful for refcounted values + let branch_info = match test { + Test::IsCtor { tag_id, union, .. } => { + tag_id_sum -= tag_id as i64; + union_size = union.alternatives.len() as i64; + + BranchInfo::Constructor { + scrutinee: inner_cond_symbol, + layout: inner_cond_layout, + tag_id, + } + } + Test::IsListLen { + bound: ListLenBound::Exact, + len, + } => { + tag_id_sum = -1; + BranchInfo::List { + scrutinee: inner_cond_symbol, + len, + } + } + _ => { + tag_id_sum = -1; + BranchInfo::None + } + }; + + branches.push((tag, branch_info, branch)); + } + + // determine if the switch is exhaustive + let default_branch_info = if tag_id_sum > 0 && union_size > 0 { + BranchInfo::Constructor { + scrutinee: inner_cond_symbol, + layout: inner_cond_layout, + tag_id: tag_id_sum as u8 as _, + } + } else { + BranchInfo::None + }; + + // We have learned more about the exact layout of the cond (based on the path) + // but tests are still relative to the original cond symbol + let inner_cond_layout_raw = layout_cache.interner.chase_recursive(inner_cond_layout); + let mut switch = if let LayoutRepr::Union(union_layout) = inner_cond_layout_raw { + let tag_id_symbol = env.unique_symbol(); + + let temp = Stmt::Switch { + cond_layout: union_layout.tag_id_layout(), + cond_symbol: tag_id_symbol, + branches: branches.into_bump_slice(), + default_branch: (default_branch_info, env.arena.alloc(default_branch)), + ret_layout, + }; + + let expr = Expr::GetTagId { + structure: inner_cond_symbol, + union_layout, + }; + + Stmt::Let( + tag_id_symbol, + expr, + union_layout.tag_id_layout(), + env.arena.alloc(temp), + ) + } else if let LayoutRepr::Builtin(Builtin::List(_)) = inner_cond_layout_raw { + let len_symbol = env.unique_symbol(); + + let switch = Stmt::Switch { + cond_layout: Layout::usize(env.target_info), + cond_symbol: len_symbol, + branches: branches.into_bump_slice(), + default_branch: (default_branch_info, env.arena.alloc(default_branch)), + ret_layout, + }; + + let len_expr = Expr::Call(Call { + call_type: CallType::LowLevel { + op: LowLevel::ListLen, + update_mode: env.next_update_mode_id(), + }, + arguments: env.arena.alloc([inner_cond_symbol]), + }); + + Stmt::Let( + len_symbol, + len_expr, + Layout::usize(env.target_info), + env.arena.alloc(switch), + ) + } else { + Stmt::Switch { + cond_layout: inner_cond_layout, + cond_symbol: inner_cond_symbol, + branches: branches.into_bump_slice(), + default_branch: (default_branch_info, env.arena.alloc(default_branch)), + ret_layout, + } + }; + + for (symbol, layout, expr) in cond_stores_vec.into_iter().rev() { + switch = Stmt::Let(symbol, expr, layout, env.arena.alloc(switch)); + } + + // make a jump table based on the tests + switch + } + } +} + +/* +fn boolean_all<'a>(arena: &'a Bump, tests: Vec<(Expr<'a>, Expr<'a>, InLayout<'a>)>) -> Expr<'a> { + let mut expr = Expr::Bool(true); + + for (lhs, rhs, layout) in tests.into_iter().rev() { + let test = Expr::RunLowLevel( + LowLevel::Eq, + bumpalo::vec![in arena; (lhs, layout.clone()), (rhs, layout.clone())].into_bump_slice(), + ); + + expr = Expr::RunLowLevel( + LowLevel::And, + arena.alloc([ + (test, LayoutRepr::Builtin(Builtin::Int1)), + (expr, LayoutRepr::Builtin(Builtin::Int1)), + ]), + ); + } + + expr +} +*/ + +/// TREE TO DECIDER +/// +/// Decision trees may have some redundancies, so we convert them to a Decider +/// which has special constructs to avoid code duplication when possible. + +/// If a test always succeeds, we don't need to branch on it +/// this saves on work and jumps +fn test_always_succeeds(test: &Test) -> bool { + match test { + Test::IsCtor { union, .. } => union.alternatives.len() == 1, + _ => false, + } +} + +fn sort_edge_tests_by_priority(edges: &mut [Edge<'_>]) { + use std::cmp::{Ordering, Ordering::*}; + use GuardedTest::*; + edges.sort_by(|(t1, _), (t2, _)| match (t1, t2) { + // Guarded takes priority + (GuardedNoTest { .. }, GuardedNoTest { .. }) => Equal, + (GuardedNoTest { .. }, TestNotGuarded { .. }) + | (GuardedNoTest { .. }, PlaceholderWithGuard) => Less, + // Interesting case: what test do we pick? + (TestNotGuarded { test: t1 }, TestNotGuarded { test: t2 }) => order_tests(t1, t2), + // Otherwise we are between guarded and fall-backs + (TestNotGuarded { .. }, GuardedNoTest { .. }) => Greater, + (TestNotGuarded { .. }, PlaceholderWithGuard) => Less, + // Placeholder is always last + (PlaceholderWithGuard, PlaceholderWithGuard) => Equal, + (PlaceholderWithGuard, GuardedNoTest { .. }) + | (PlaceholderWithGuard, TestNotGuarded { .. }) => Greater, + }); + + fn order_tests(t1: &Test, t2: &Test) -> Ordering { + match (t1, t2) { + ( + Test::IsListLen { + bound: bound_l, + len: l, + }, + Test::IsListLen { + bound: bound_m, + len: m, + }, + ) => { + // List tests can either check for + // - exact length (= l) + // - a size greater or equal to a given length (>= l) + // (>= l) tests can be superset of other tests + // - (>= m) where m > l + // - (= m) + // So, if m > l, we enforce the following order for list tests + // (>= m) then (= l) then (>= l) + match m.cmp(l) { + Less => Less, // (>= m) then (>= l) + Greater => Greater, + + Equal => { + use ListLenBound::*; + match (bound_l, bound_m) { + (Exact, AtLeast) => Less, // (= l) then (>= l) + (AtLeast, Exact) => Greater, + + (AtLeast, AtLeast) | (Exact, Exact) => Equal, + } + } + } + } + + (Test::IsListLen { .. }, t) | (t, Test::IsListLen { .. }) => internal_error!( + "list-length tests should never pair with another test {t:?} at the same level" + ), + // We don't care about anything other than list-length tests, since all other tests + // should be disjoint. + _ => Equal, + } + } +} + +fn tree_to_decider(tree: DecisionTree) -> Decider { + use Decider::*; + use DecisionTree::*; + + match tree { + Match(target) => Leaf(target), + + Decision { + path, + mut edges, + default, + } => { + // Some of the head-constructor tests we generate can be supersets of other tests. + // Edges must be ordered so that more general tests always happen after their + // specialized variants. + // + // For example, patterns + // + // [1, ..] -> ... + // [2, 1, ..] -> ... + // + // may generate the edges + // + // ListLen(>=1) -> + // ListLen(>=2) -> + // + // but evaluated in exactly this order, the second edge is never reachable. + // The necessary ordering is + // + // ListLen(>=2) -> + // ListLen(>=1) -> + sort_edge_tests_by_priority(&mut edges); + + match default { + None => match edges.len() { + 0 => panic!("compiler bug, somehow created an empty decision tree"), + 1 => { + let (_, sub_tree) = edges.remove(0); + + tree_to_decider(sub_tree) + } + 2 => { + let (_, failure_tree) = edges.remove(1); + let (guarded_test, success_tree) = edges.remove(0); + + chain_decider(path, guarded_test, failure_tree, success_tree) + } + + _ => { + let fallback = edges.remove(edges.len() - 1).1; + + fanout_decider(path, fallback, edges) + } + }, + + Some(last) => match edges.len() { + 0 => tree_to_decider(*last), + 1 => { + let failure_tree = *last; + let (guarded_test, success_tree) = edges.remove(0); + + chain_decider(path, guarded_test, failure_tree, success_tree) + } + + _ => { + let fallback = *last; + + fanout_decider(path, fallback, edges) + } + }, + } + } + } +} + +fn fanout_decider<'a>( + path: Vec, + fallback: DecisionTree<'a>, + edges: Vec<(GuardedTest<'a>, DecisionTree<'a>)>, +) -> Decider<'a, u64> { + let fallback_decider = tree_to_decider(fallback); + let necessary_tests: Vec<_> = edges + .into_iter() + .map(|(test, tree)| fanout_decider_help(tree, test)) + .collect(); + + if necessary_tests.iter().all(|(t, _)| t.can_be_switch()) { + Decider::FanOut { + path, + tests: necessary_tests, + fallback: Box::new(fallback_decider), + } + } else { + // in llvm, we cannot switch on strings so must chain + let mut decider = fallback_decider; + + for (test, branch_decider) in necessary_tests.into_iter().rev() { + decider = Decider::Chain { + test_chain: vec![(path.clone(), test)], + success: Box::new(branch_decider), + failure: Box::new(decider), + }; + } + + decider + } +} + +fn fanout_decider_help<'a>( + dectree: DecisionTree<'a>, + guarded_test: GuardedTest<'a>, +) -> (Test<'a>, Decider<'a, u64>) { + match guarded_test { + GuardedTest::PlaceholderWithGuard | GuardedTest::GuardedNoTest { .. } => { + unreachable!("this would not end up in a switch") + } + GuardedTest::TestNotGuarded { test } => { + let decider = tree_to_decider(dectree); + (test, decider) + } + } +} + +fn chain_decider<'a>( + path: Vec, + guarded_test: GuardedTest<'a>, + failure_tree: DecisionTree<'a>, + success_tree: DecisionTree<'a>, +) -> Decider<'a, u64> { + match guarded_test { + GuardedTest::GuardedNoTest { pattern, stmt_spec } => { + let failure = Box::new(tree_to_decider(failure_tree)); + let success = Box::new(tree_to_decider(success_tree)); + + Decider::Guarded { + pattern, + stmt_spec, + success, + failure, + } + } + GuardedTest::TestNotGuarded { test } => { + if test_always_succeeds(&test) { + tree_to_decider(success_tree) + } else { + to_chain(path, test, success_tree, failure_tree) + } + } + + GuardedTest::PlaceholderWithGuard => { + // ? + tree_to_decider(success_tree) + } + } +} + +fn to_chain<'a>( + path: Vec, + test: Test<'a>, + success_tree: DecisionTree<'a>, + failure_tree: DecisionTree<'a>, +) -> Decider<'a, u64> { + use Decider::*; + + let failure = tree_to_decider(failure_tree); + + match tree_to_decider(success_tree) { + Chain { + mut test_chain, + success, + failure: sub_failure, + } if failure == *sub_failure => { + test_chain.push((path, test)); + + Chain { + test_chain, + success, + failure: Box::new(failure), + } + } + + success => Chain { + test_chain: vec![(path, test)], + success: Box::new(success), + failure: Box::new(failure), + }, + } +} + +/// INSERT CHOICES +/// +/// If a target appears exactly once in a Decider, the corresponding expression +/// can be inlined. Whether things are inlined or jumps is called a "choice". + +fn count_targets(targets: &mut bumpalo::collections::Vec, initial: &Decider) { + use Decider::*; + + let mut stack = vec![initial]; + + while let Some(decision_tree) = stack.pop() { + match decision_tree { + Leaf(target) => { + targets[*target as usize] += 1; + } + + Guarded { + success, failure, .. + } => { + stack.push(success); + stack.push(failure); + } + + Chain { + success, failure, .. + } => { + stack.push(success); + stack.push(failure); + } + + FanOut { + tests, fallback, .. + } => { + stack.push(fallback); + + for (_, decider) in tests { + stack.push(decider); + } + } + } + } +} + +fn insert_choices<'a>( + choice_dict: &MutMap>, + decider: Decider<'a, u64>, +) -> Decider<'a, Choice<'a>> { + use Decider::*; + match decider { + Leaf(target) => { + // TODO remove clone + // Only targes that appear once are Inline, so it's safe to remove them from the dict. + Leaf(choice_dict[&target].clone()) + } + + Guarded { + pattern, + stmt_spec, + success, + failure, + } => Guarded { + pattern, + stmt_spec, + success: Box::new(insert_choices(choice_dict, *success)), + failure: Box::new(insert_choices(choice_dict, *failure)), + }, + + Chain { + test_chain, + success, + failure, + } => Chain { + test_chain, + success: Box::new(insert_choices(choice_dict, *success)), + failure: Box::new(insert_choices(choice_dict, *failure)), + }, + + FanOut { + path, + tests, + fallback, + } => FanOut { + path, + tests: tests + .into_iter() + .map(|(test, nested)| (test, insert_choices(choice_dict, nested))) + .collect(), + fallback: Box::new(insert_choices(choice_dict, *fallback)), + }, + } +} diff --git a/crates/compiler/mono/src/ir/erased.rs b/crates/compiler/mono/src/ir/erased.rs new file mode 100644 index 0000000000..2038413375 --- /dev/null +++ b/crates/compiler/mono/src/ir/erased.rs @@ -0,0 +1,497 @@ +use bumpalo::{collections::Vec as AVec, Bump}; +use roc_module::{low_level::LowLevel, symbol::Symbol}; +use roc_types::subs::Variable; + +use crate::layout::{FunctionPointer, InLayout, LambdaName, Layout, LayoutCache, LayoutRepr}; + +use super::{ + boxed, with_hole, BranchInfo, Call, CallType, CapturedSymbols, Env, ErasedField, Expr, + JoinPointId, Param, Procs, Stmt, UpdateModeId, +}; + +fn index_erased_function<'a>( + arena: &'a Bump, + assign_to: Symbol, + erased_function: Symbol, + field: ErasedField, + layout: InLayout<'a>, +) -> impl FnOnce(Stmt<'a>) -> Stmt<'a> { + move |rest| { + Stmt::Let( + assign_to, + Expr::ErasedLoad { + symbol: erased_function, + field, + }, + layout, + arena.alloc(rest), + ) + } +} + +fn call_callee<'a>( + arena: &'a Bump, + result_symbol: Symbol, + result: InLayout<'a>, + fn_ptr_symbol: Symbol, + fn_arg_layouts: &'a [InLayout<'a>], + fn_arguments: &'a [Symbol], +) -> impl FnOnce(Stmt<'a>) -> Stmt<'a> { + move |rest| { + Stmt::Let( + result_symbol, + Expr::Call(Call { + call_type: CallType::ByPointer { + pointer: fn_ptr_symbol, + ret_layout: result, + arg_layouts: fn_arg_layouts, + }, + arguments: fn_arguments, + }), + result, + arena.alloc(rest), + ) + } +} + +fn is_null<'a>( + env: &mut Env<'a, '_>, + arena: &'a Bump, + assign_to: Symbol, + ptr_symbol: Symbol, + layout: InLayout<'a>, +) -> impl FnOnce(Stmt<'a>) -> Stmt<'a> { + let null_symbol = env.unique_symbol(); + move |rest| { + Stmt::Let( + null_symbol, + Expr::NullPointer, + layout, + arena.alloc(Stmt::Let( + assign_to, + Expr::Call(Call { + call_type: CallType::LowLevel { + op: LowLevel::Eq, + update_mode: UpdateModeId::BACKEND_DUMMY, + }, + arguments: arena.alloc([ptr_symbol, null_symbol]), + }), + Layout::BOOL, + arena.alloc(rest), + )), + ) + } +} + +struct BuiltFunctionPointer<'a> { + function_pointer: InLayout<'a>, + reified_arguments: &'a [InLayout<'a>], +} + +fn build_function_pointer<'a>( + arena: &'a Bump, + layout_cache: &mut LayoutCache<'a>, + argument_layouts: &'a [InLayout<'a>], + return_layout: InLayout<'a>, + pass_closure: bool, +) -> BuiltFunctionPointer<'a> { + let reified_arguments = if pass_closure { + let mut args = AVec::with_capacity_in(argument_layouts.len() + 1, arena); + args.extend(argument_layouts.iter().chain(&[Layout::ERASED]).copied()); + args.into_bump_slice() + } else { + argument_layouts + }; + + let fn_ptr_layout = LayoutRepr::FunctionPointer(FunctionPointer { + args: reified_arguments, + ret: return_layout, + }); + + let function_pointer = layout_cache.put_in_direct_no_semantic(fn_ptr_layout); + + BuiltFunctionPointer { + function_pointer, + reified_arguments, + } +} + +/// Given +/// +/// ```text +/// Call(f, args) +/// ``` +/// +/// We generate +/// +/// ```text +/// f = compile(f) +/// joinpoint join result: +/// +/// f_value: Ptr<[]> = ErasedLoad(f, .value_ptr) +/// f_callee: Ptr<[]> = ErasedLoad(f, .callee) +/// if (f_value != nullptr) { +/// f_callee = Cast(f_callee, (..params, Erased) -> ret); +/// result = f_callee ..args f +/// jump join result +/// } else { +/// f_callee = cast(f_callee, (..params) -> ret); +/// result = f_callee ..args +/// jump join result +/// } +/// ``` +pub fn call_erased_function<'a>( + env: &mut Env<'a, '_>, + layout_cache: &mut LayoutCache<'a>, + procs: &mut Procs<'a>, + function_expr: roc_can::expr::Expr, + function_var: Variable, + function_signature: (&'a [InLayout<'a>], InLayout<'a>), + function_argument_symbols: &'a [Symbol], + call_result_symbol: Symbol, + hole: &'a Stmt<'a>, + hole_layout: InLayout<'a>, +) -> Stmt<'a> { + let arena = env.arena; + let (f_args, f_ret) = function_signature; + + let f = env.unique_symbol(); + + let join_point_id = JoinPointId(env.unique_symbol()); + + // f_value = ErasedLoad(f, .value) + let f_value = env.unique_symbol(); + let let_f_value = + index_erased_function(arena, f_value, f, ErasedField::ValuePtr, Layout::OPAQUE_PTR); + + let mut build_closure_data_branch = |env: &mut Env, pass_closure| { + // f_callee = Cast(f_callee, (..params) -> ret); + // result = f_callee ..args + // jump join result + + let BuiltFunctionPointer { + function_pointer, + reified_arguments: f_args, + } = build_function_pointer(arena, layout_cache, f_args, f_ret, pass_closure); + + // f_callee = ErasedLoad(f, .callee) + let f_callee = env.unique_symbol(); + let let_f_callee = + index_erased_function(arena, f_callee, f, ErasedField::Callee, function_pointer); + + let function_argument_symbols = if pass_closure { + // function_argument_symbols = ...args, f.value + let mut args = AVec::with_capacity_in(function_argument_symbols.len() + 1, arena); + args.extend(function_argument_symbols.iter().chain(&[f])); + args.into_bump_slice() + } else { + function_argument_symbols + }; + + let result = env.unique_symbol(); + let let_result = call_callee( + arena, + result, + f_ret, + f_callee, + f_args, + function_argument_symbols, + ); + + let_f_callee( + // + let_result( + // + Stmt::Jump(join_point_id, arena.alloc([result])), + ), + ) + }; + + let value_is_null = env.unique_symbol(); + let let_value_is_null = is_null(env, arena, value_is_null, f_value, Layout::OPAQUE_PTR); + + let call_and_jump_on_value = let_value_is_null( + // + Stmt::Switch { + cond_symbol: value_is_null, + cond_layout: Layout::BOOL, + // value == null + branches: arena.alloc([(1, BranchInfo::None, build_closure_data_branch(env, false))]), + // value != null + default_branch: ( + BranchInfo::None, + arena.alloc(build_closure_data_branch(env, true)), + ), + ret_layout: hole_layout, + }, + ); + + let joinpoint = { + let param = Param { + symbol: call_result_symbol, + layout: f_ret, + }; + + let remainder = let_f_value( + // f_value = ErasedLoad(f, .value) + // + call_and_jump_on_value, + ); + + Stmt::Join { + id: join_point_id, + parameters: env.arena.alloc([param]), + body: hole, + remainder: arena.alloc(remainder), + } + }; + + // Compile the function expression into f_val + with_hole( + env, + function_expr, + function_var, + procs, + layout_cache, + f, + env.arena.alloc(joinpoint), + ) +} + +/// Given +/// +/// ```text +/// f = \{} -> s +/// ``` +/// +/// We generate +/// +/// ```text +/// value = Expr::Box({s}) +/// callee = Expr::FunctionPointer(f) +/// f = Expr::ErasedMake({ value, callee }) +/// ``` +/// +/// Given +/// +/// ```text +/// f = \{} -> {} +/// ``` +/// +/// We generate +/// +/// ```text +/// callee = Expr::FunctionPointer(f) +/// f = Expr::ErasedMake({ value: nullptr, callee }) +/// ``` +pub fn build_erased_function<'a>( + env: &mut Env<'a, '_>, + layout_cache: &mut LayoutCache<'a>, + resolved_lambda: ResolvedErasedLambda<'a>, + assigned: Symbol, + hole: &'a Stmt<'a>, +) -> Stmt<'a> { + let ResolvedErasedLambda { + captures, + lambda_name, + arguments, + ret, + } = resolved_lambda; + + let value = match captures { + None => None, + Some(_) => Some(env.unique_symbol()), + }; + + let callee = env.unique_symbol(); + + // assigned = Expr::ErasedMake({ value, callee }) + // hole + let result = Stmt::Let( + assigned, + Expr::ErasedMake { value, callee }, + Layout::ERASED, + hole, + ); + + let BuiltFunctionPointer { + function_pointer, + reified_arguments: _, + } = build_function_pointer(env.arena, layout_cache, arguments, ret, captures.is_some()); + + // callee = Expr::FunctionPointer(f) + let result = Stmt::Let( + callee, + Expr::FunctionPointer { lambda_name }, + function_pointer, + env.arena.alloc(result), + ); + + // value = Expr::Box({s}) + match captures { + None => result, + Some(ResolvedErasedCaptures { layouts, symbols }) => { + // captures = {...captures} + // captures = Box(captures) + // value = Cast(captures, void*) + // + + let stack_captures = env.unique_symbol(); + let stack_captures_layout = + layout_cache.put_in_direct_no_semantic(LayoutRepr::Struct(layouts)); + let stack_captures_layout = env.arena.alloc(stack_captures_layout); + + let boxed_captures_layout = layout_cache + .put_in_direct_no_semantic(LayoutRepr::boxed_erased_value(stack_captures_layout)); + + let result = Stmt::Let( + value.unwrap(), + boxed::box_nullable(env.arena.alloc(stack_captures), stack_captures_layout), + boxed_captures_layout, + env.arena.alloc(result), + ); + + let result = Stmt::Let( + stack_captures, + Expr::Struct(symbols), + *stack_captures_layout, + env.arena.alloc(result), + ); + + result + } + } +} + +#[derive(Debug)] +struct ResolvedErasedCaptures<'a> { + layouts: &'a [InLayout<'a>], + symbols: &'a [Symbol], +} + +#[derive(Debug)] +pub struct ResolvedErasedLambda<'a> { + captures: Option>, + lambda_name: LambdaName<'a>, + arguments: &'a [InLayout<'a>], + ret: InLayout<'a>, +} + +impl<'a> ResolvedErasedLambda<'a> { + pub fn new( + env: &Env<'a, '_>, + layout_cache: &mut LayoutCache<'a>, + lambda_symbol: Symbol, + captures: CapturedSymbols<'a>, + arguments: &'a [InLayout<'a>], + ret: InLayout<'a>, + ) -> Self { + let resolved_captures; + let lambda_name; + match captures { + CapturedSymbols::None => { + resolved_captures = None; + lambda_name = LambdaName::from_captures(lambda_symbol, &[]); + } + CapturedSymbols::Captured(captures) => { + let layouts = { + let layouts = captures + .iter() + .map(|(_, var)| layout_cache.from_var(env.arena, *var, env.subs).unwrap()); + env.arena.alloc_slice_fill_iter(layouts) + }; + let symbols = { + let symbols = captures.iter().map(|(sym, _)| *sym); + env.arena.alloc_slice_fill_iter(symbols) + }; + + resolved_captures = Some(ResolvedErasedCaptures { layouts, symbols }); + lambda_name = LambdaName::from_captures(lambda_symbol, layouts); + } + }; + + Self { + captures: resolved_captures, + lambda_name, + arguments, + ret, + } + } + + pub fn lambda_name(&self) -> LambdaName<'a> { + self.lambda_name + } +} + +/// Given +/// +/// ```text +/// proc f(...args, captures_symbol: Erased): +/// # captures = { a: A, b: B } +/// ``` +/// +/// We generate +/// +/// ```text +/// loaded_captures: Ptr<[]> = ErasedLoad(captures_symbol, .value) +/// heap_captures: Box<{ A, B }> = Expr::Call(Lowlevel { Cast, captures_symbol }) +/// stack_captures = Expr::Unbox(heap_captures) +/// a = Expr::StructAtIndex(stack_captures, 0) +/// b = Expr::StructAtIndex(stack_captures, 1) +/// +/// ``` +pub fn unpack_closure_data<'a>( + env: &mut Env<'a, '_>, + layout_cache: &mut LayoutCache<'a>, + captures_symbol: Symbol, + captures: &[(Symbol, Variable)], + mut hole: Stmt<'a>, +) -> Stmt<'a> { + let heap_captures = env.unique_symbol(); + let stack_captures = env.unique_symbol(); + + let captures_layouts = { + let layouts = captures + .iter() + .map(|(_, var)| layout_cache.from_var(env.arena, *var, env.subs).unwrap()); + &*env.arena.alloc_slice_fill_iter(layouts) + }; + + let stack_captures_layout = + layout_cache.put_in_direct_no_semantic(LayoutRepr::Struct(captures_layouts)); + let stack_captures_layout = env.arena.alloc(stack_captures_layout); + let heap_captures_layout = layout_cache + .put_in_direct_no_semantic(LayoutRepr::boxed_erased_value(stack_captures_layout)); + + for (i, ((capture, _capture_var), &capture_layout)) in + captures.iter().zip(captures_layouts).enumerate().rev() + { + hole = Stmt::Let( + *capture, + Expr::StructAtIndex { + index: i as _, + field_layouts: captures_layouts, + structure: stack_captures, + }, + capture_layout, + env.arena.alloc(hole), + ); + } + + hole = Stmt::Let( + stack_captures, + boxed::unbox_nullable(heap_captures, stack_captures_layout), + *stack_captures_layout, + env.arena.alloc(hole), + ); + + let let_loaded_captures = index_erased_function( + env.arena, + heap_captures, + captures_symbol, + ErasedField::Value, + heap_captures_layout, + ); + + let_loaded_captures(hole) +} diff --git a/crates/compiler/mono/src/ir/literal.rs b/crates/compiler/mono/src/ir/literal.rs new file mode 100644 index 0000000000..c7dcabf682 --- /dev/null +++ b/crates/compiler/mono/src/ir/literal.rs @@ -0,0 +1,116 @@ +use roc_builtins::bitcode::{FloatWidth, IntWidth}; +use roc_can::expr::IntValue; +use roc_error_macros::internal_error; +use roc_module::symbol::Symbol; +use roc_std::RocDec; + +use crate::layout::{Builtin, InLayout, LayoutInterner, LayoutRepr, TLLayoutInterner}; + +use super::pattern::Pattern; + +#[derive(Debug, Clone, Copy)] +pub enum IntOrFloatValue { + Int(IntValue), + Float(f64), +} + +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum Literal<'a> { + // Literals + /// stored as raw bytes rather than a number to avoid an alignment bump + Int([u8; 16]), + /// stored as raw bytes rather than a number to avoid an alignment bump + U128([u8; 16]), + Float(f64), + /// stored as raw bytes rather than a number to avoid an alignment bump + Decimal([u8; 16]), + Str(&'a str), + /// Closed tag unions containing exactly two (0-arity) tags compile to Expr::Bool, + /// so they can (at least potentially) be emitted as 1-bit machine bools. + /// + /// So [True, False] compiles to this, and so do [A, B] and [Foo, Bar]. + /// However, a union like [True, False, Other Int] would not. + Bool(bool), + /// Closed tag unions containing between 3 and 256 tags (all of 0 arity) + /// compile to bytes, e.g. [Blue, Black, Red, Green, White] + Byte(u8), +} + +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum ListLiteralElement<'a> { + Literal(Literal<'a>), + Symbol(Symbol), +} + +impl<'a> ListLiteralElement<'a> { + pub fn to_symbol(&self) -> Option { + match self { + Self::Symbol(s) => Some(*s), + _ => None, + } + } +} + +pub enum NumLiteral { + Int([u8; 16], IntWidth), + U128([u8; 16]), + Float(f64, FloatWidth), + Decimal([u8; 16]), +} + +impl NumLiteral { + pub fn to_expr_literal(&self) -> Literal<'static> { + match *self { + NumLiteral::Int(n, _) => Literal::Int(n), + NumLiteral::U128(n) => Literal::U128(n), + NumLiteral::Float(n, _) => Literal::Float(n), + NumLiteral::Decimal(n) => Literal::Decimal(n), + } + } + pub fn to_pattern(&self) -> Pattern<'static> { + match *self { + NumLiteral::Int(n, w) => Pattern::IntLiteral(n, w), + NumLiteral::U128(n) => Pattern::IntLiteral(n, IntWidth::U128), + NumLiteral::Float(n, w) => Pattern::FloatLiteral(f64::to_bits(n), w), + NumLiteral::Decimal(n) => Pattern::DecimalLiteral(n), + } + } +} + +pub fn make_num_literal<'a>( + interner: &TLLayoutInterner<'a>, + layout: InLayout<'a>, + num_str: &str, + num_value: IntOrFloatValue, +) -> NumLiteral { + match interner.get_repr(layout) { + LayoutRepr::Builtin(Builtin::Int(width)) => match num_value { + IntOrFloatValue::Int(IntValue::I128(n)) => NumLiteral::Int(n, width), + IntOrFloatValue::Int(IntValue::U128(n)) => NumLiteral::U128(n), + IntOrFloatValue::Float(..) => { + internal_error!("Frac value where int was expected, should have been a type error") + } + }, + LayoutRepr::Builtin(Builtin::Float(width)) => match num_value { + IntOrFloatValue::Float(n) => NumLiteral::Float(n, width), + IntOrFloatValue::Int(int_value) => match int_value { + IntValue::I128(n) => NumLiteral::Float(i128::from_ne_bytes(n) as f64, width), + IntValue::U128(n) => NumLiteral::Float(u128::from_ne_bytes(n) as f64, width), + }, + }, + LayoutRepr::Builtin(Builtin::Decimal) => { + let dec = match RocDec::from_str(num_str) { + Some(d) => d, + None => internal_error!( + "Invalid decimal for float literal = {}. This should be a type error!", + num_str + ), + }; + NumLiteral::Decimal(dec.to_ne_bytes()) + } + layout => internal_error!( + "Found a non-num layout where a number was expected: {:?}", + layout + ), + } +} diff --git a/crates/compiler/mono/src/ir/pattern.rs b/crates/compiler/mono/src/ir/pattern.rs new file mode 100644 index 0000000000..b8f9fdb408 --- /dev/null +++ b/crates/compiler/mono/src/ir/pattern.rs @@ -0,0 +1,1893 @@ +use crate::ir::{substitute_in_exprs, Env, Expr, Procs, Stmt}; +use crate::layout::{ + self, Builtin, InLayout, Layout, LayoutCache, LayoutInterner, LayoutProblem, LayoutRepr, + TagIdIntType, UnionLayout, WrappedVariant, +}; +use bumpalo::collections::Vec; +use roc_builtins::bitcode::{FloatWidth, IntWidth}; +use roc_collections::all::{BumpMap, BumpMapDefault}; +use roc_error_macros::internal_error; +use roc_exhaustive::{Ctor, CtorName, ListArity, RenderAs, TagId}; +use roc_module::ident::{Lowercase, TagName}; +use roc_module::low_level::LowLevel; +use roc_module::symbol::Symbol; +use roc_problem::can::{RuntimeError, ShadowKind}; +use roc_types::subs::Variable; + +use super::literal::{make_num_literal, IntOrFloatValue}; +use super::{Call, CallType, Literal}; + +/// A pattern, including possible problems (e.g. shadowing) so that +/// codegen can generate a runtime error if this pattern is reached. +#[derive(Clone, Debug, PartialEq)] +pub enum Pattern<'a> { + Identifier(Symbol), + Underscore, + As(Box>, Symbol), + IntLiteral([u8; 16], IntWidth), + FloatLiteral(u64, FloatWidth), + DecimalLiteral([u8; 16]), + BitLiteral { + value: bool, + tag_name: TagName, + union: roc_exhaustive::Union, + }, + EnumLiteral { + tag_id: u8, + tag_name: TagName, + union: roc_exhaustive::Union, + }, + StrLiteral(Box), + + RecordDestructure(Vec<'a, RecordDestruct<'a>>, &'a [InLayout<'a>]), + TupleDestructure(Vec<'a, TupleDestruct<'a>>, &'a [InLayout<'a>]), + NewtypeDestructure { + tag_name: TagName, + arguments: Vec<'a, (Pattern<'a>, InLayout<'a>)>, + }, + AppliedTag { + tag_name: TagName, + tag_id: TagIdIntType, + arguments: Vec<'a, (Pattern<'a>, InLayout<'a>)>, + layout: UnionLayout<'a>, + union: roc_exhaustive::Union, + }, + Voided { + tag_name: TagName, + }, + OpaqueUnwrap { + opaque: Symbol, + argument: Box<(Pattern<'a>, InLayout<'a>)>, + }, + List { + arity: ListArity, + list_layout: InLayout<'a>, + element_layout: InLayout<'a>, + elements: Vec<'a, Pattern<'a>>, + opt_rest: Option<(usize, Option)>, + }, +} + +impl<'a> Pattern<'a> { + /// This pattern contains a pattern match on Void (i.e. [], the empty tag union) + /// such branches are not reachable at runtime + pub fn is_voided(&self) -> bool { + let mut stack: std::vec::Vec<&Pattern> = vec![self]; + + while let Some(pattern) = stack.pop() { + match pattern { + Pattern::Identifier(_) + | Pattern::Underscore + | Pattern::IntLiteral(_, _) + | Pattern::FloatLiteral(_, _) + | Pattern::DecimalLiteral(_) + | Pattern::BitLiteral { .. } + | Pattern::EnumLiteral { .. } + | Pattern::StrLiteral(_) => { /* terminal */ } + Pattern::As(subpattern, _) => stack.push(subpattern), + Pattern::RecordDestructure(destructs, _) => { + for destruct in destructs { + match &destruct.typ { + DestructType::Required(_) => { /* do nothing */ } + DestructType::Guard(pattern) => { + stack.push(pattern); + } + } + } + } + Pattern::TupleDestructure(destructs, _) => { + for destruct in destructs { + stack.push(&destruct.pat); + } + } + Pattern::NewtypeDestructure { arguments, .. } => { + stack.extend(arguments.iter().map(|(t, _)| t)) + } + Pattern::Voided { .. } => return true, + Pattern::AppliedTag { arguments, .. } => { + stack.extend(arguments.iter().map(|(t, _)| t)) + } + Pattern::OpaqueUnwrap { argument, .. } => stack.push(&argument.0), + Pattern::List { elements, .. } => stack.extend(elements), + } + } + + false + } + + pub fn collect_symbols( + &self, + layout: InLayout<'a>, + ) -> impl Iterator)> + '_ { + PatternBindingIter::One(self, layout) + } +} + +enum PatternBindingIter<'r, 'a> { + Done, + One(&'r Pattern<'a>, InLayout<'a>), + Stack(std::vec::Vec<(PatternBindingWork<'r, 'a>, InLayout<'a>)>), +} + +enum PatternBindingWork<'r, 'a> { + Pat(&'r Pattern<'a>), + RecordDestruct(&'r DestructType<'a>), +} + +impl<'r, 'a> Iterator for PatternBindingIter<'r, 'a> { + type Item = (Symbol, InLayout<'a>); + + fn next(&mut self) -> Option { + use Pattern::*; + use PatternBindingIter::*; + use PatternBindingWork::*; + match self { + Done => None, + One(pattern, layout) => { + let layout = *layout; + match pattern { + Identifier(symbol) => { + *self = Done; + (*symbol, layout).into() + } + Underscore => None, + As(pat, symbol) => { + *self = One(pat, layout); + (*symbol, layout).into() + } + RecordDestructure(destructs, _) => { + let stack = destructs + .iter() + .map(|destruct| (RecordDestruct(&destruct.typ), destruct.layout)) + .rev() + .collect(); + *self = Stack(stack); + self.next() + } + TupleDestructure(destructs, _) => { + let stack = destructs + .iter() + .map(|destruct| (Pat(&destruct.pat), destruct.layout)) + .rev() + .collect(); + *self = Stack(stack); + self.next() + } + NewtypeDestructure { arguments, .. } | AppliedTag { arguments, .. } => { + let stack = arguments.iter().map(|(p, l)| (Pat(p), *l)).rev().collect(); + *self = Stack(stack); + self.next() + } + OpaqueUnwrap { argument, .. } => { + *self = One(&argument.0, layout); + self.next() + } + List { + element_layout, + elements, + .. + } => { + let stack = elements + .iter() + .map(|p| (Pat(p), *element_layout)) + .rev() + .collect(); + *self = Stack(stack); + self.next() + } + IntLiteral(_, _) + | FloatLiteral(_, _) + | DecimalLiteral(_) + | BitLiteral { .. } + | EnumLiteral { .. } + | StrLiteral(_) + | Voided { .. } => None, + } + } + Stack(stack) => { + while let Some((pat, layout)) = stack.pop() { + match pat { + Pat(pattern) => match pattern { + Identifier(symbol) => return (*symbol, layout).into(), + As(pat, symbol) => { + stack.push((Pat(pat), layout)); + return (*symbol, layout).into(); + } + RecordDestructure(destructs, _) => stack.extend( + destructs + .iter() + .map(|destruct| { + (RecordDestruct(&destruct.typ), destruct.layout) + }) + .rev(), + ), + TupleDestructure(destructs, _) => stack.extend( + destructs + .iter() + .map(|destruct| (Pat(&destruct.pat), destruct.layout)) + .rev(), + ), + NewtypeDestructure { arguments, .. } | AppliedTag { arguments, .. } => { + stack.extend(arguments.iter().map(|(p, l)| (Pat(p), *l)).rev()) + } + OpaqueUnwrap { argument, .. } => { + stack.push((Pat(&argument.0), layout)); + } + List { + element_layout, + elements, + .. + } => { + stack.extend( + elements.iter().map(|p| (Pat(p), *element_layout)).rev(), + ); + } + IntLiteral(_, _) + | FloatLiteral(_, _) + | DecimalLiteral(_) + | BitLiteral { .. } + | EnumLiteral { .. } + | Underscore + | StrLiteral(_) + | Voided { .. } => {} + }, + PatternBindingWork::RecordDestruct(_) => todo!(), + } + } + + *self = Done; + None + } + } + } +} + +#[derive(Clone, Debug, PartialEq)] +pub struct RecordDestruct<'a> { + pub label: Lowercase, + pub variable: Variable, + pub layout: InLayout<'a>, + pub typ: DestructType<'a>, +} + +#[derive(Clone, Debug, PartialEq)] +pub struct TupleDestruct<'a> { + pub index: usize, + pub variable: Variable, + pub layout: InLayout<'a>, + pub pat: Pattern<'a>, +} + +#[derive(Clone, Debug, PartialEq)] +pub enum DestructType<'a> { + Required(Symbol), + Guard(Pattern<'a>), +} + +#[derive(Clone, Debug, PartialEq)] +pub struct WhenBranch<'a> { + pub patterns: Vec<'a, Pattern<'a>>, + pub value: Expr<'a>, + pub guard: Option>, +} + +#[allow(clippy::type_complexity)] +pub fn from_can_pattern<'a>( + env: &mut Env<'a, '_>, + procs: &mut Procs<'a>, + layout_cache: &mut LayoutCache<'a>, + can_pattern: &roc_can::pattern::Pattern, +) -> Result< + ( + Pattern<'a>, + Vec<'a, (Symbol, Variable, roc_can::expr::Expr)>, + ), + RuntimeError, +> { + let mut assignments = Vec::new_in(env.arena); + let pattern = from_can_pattern_help(env, procs, layout_cache, can_pattern, &mut assignments)?; + + Ok((pattern, assignments)) +} + +fn from_can_pattern_help<'a>( + env: &mut Env<'a, '_>, + procs: &mut Procs<'a>, + layout_cache: &mut LayoutCache<'a>, + can_pattern: &roc_can::pattern::Pattern, + assignments: &mut Vec<'a, (Symbol, Variable, roc_can::expr::Expr)>, +) -> Result, RuntimeError> { + use roc_can::pattern::Pattern::*; + + match can_pattern { + Underscore => Ok(Pattern::Underscore), + Identifier(symbol) => Ok(Pattern::Identifier(*symbol)), + As(subpattern, symbol) => { + let mono_subpattern = + from_can_pattern_help(env, procs, layout_cache, &subpattern.value, assignments)?; + + Ok(Pattern::As(Box::new(mono_subpattern), *symbol)) + } + AbilityMemberSpecialization { ident, .. } => Ok(Pattern::Identifier(*ident)), + IntLiteral(var, _, int_str, int, _bound) => Ok(make_num_literal_pattern( + env, + layout_cache, + *var, + int_str, + IntOrFloatValue::Int(*int), + )), + FloatLiteral(var, _, float_str, float, _bound) => Ok(make_num_literal_pattern( + env, + layout_cache, + *var, + float_str, + IntOrFloatValue::Float(*float), + )), + StrLiteral(v) => Ok(Pattern::StrLiteral(v.clone())), + SingleQuote(var, _, c, _) => { + let layout = layout_cache.from_var(env.arena, *var, env.subs); + match layout.map(|l| layout_cache.get_repr(l)) { + Ok(LayoutRepr::Builtin(Builtin::Int(width))) => { + Ok(Pattern::IntLiteral((*c as i128).to_ne_bytes(), width)) + } + o => internal_error!("an integer width was expected, but we found {:?}", o), + } + } + Shadowed(region, ident, _new_symbol) => Err(RuntimeError::Shadowing { + original_region: *region, + shadow: ident.clone(), + kind: ShadowKind::Variable, + }), + UnsupportedPattern(region) => Err(RuntimeError::UnsupportedPattern(*region)), + MalformedPattern(_problem, region) => { + // TODO preserve malformed problem information here? + Err(RuntimeError::UnsupportedPattern(*region)) + } + OpaqueNotInScope(loc_ident) => { + // TODO(opaques) should be `RuntimeError::OpaqueNotDefined` + Err(RuntimeError::UnsupportedPattern(loc_ident.region)) + } + NumLiteral(var, num_str, num, _bound) => Ok(make_num_literal_pattern( + env, + layout_cache, + *var, + num_str, + IntOrFloatValue::Int(*num), + )), + + AppliedTag { + whole_var, + tag_name, + arguments, + .. + } => { + use crate::layout::UnionVariant::*; + use roc_exhaustive::Union; + + let res_variant = { + let mut layout_env = layout::Env::from_components( + layout_cache, + env.subs, + env.arena, + env.target_info, + ); + crate::layout::union_sorted_tags(&mut layout_env, *whole_var).map_err(Into::into) + }; + + let variant = match res_variant { + Ok(cached) => cached, + Err(LayoutProblem::UnresolvedTypeVar(_)) => { + return Err(RuntimeError::UnresolvedTypeVar) + } + Err(LayoutProblem::Erroneous) => return Err(RuntimeError::ErroneousType), + }; + + let result = match variant { + Never => unreachable!( + "there is no pattern of type `[]`, union var {:?}", + *whole_var + ), + Unit => Pattern::EnumLiteral { + tag_id: 0, + tag_name: tag_name.clone(), + union: Union { + render_as: RenderAs::Tag, + alternatives: vec![Ctor { + tag_id: TagId(0), + name: CtorName::Tag(tag_name.clone()), + arity: 0, + }], + }, + }, + BoolUnion { ttrue, ffalse } => { + let (ttrue, ffalse) = (ttrue.expect_tag(), ffalse.expect_tag()); + Pattern::BitLiteral { + value: tag_name == &ttrue, + tag_name: tag_name.clone(), + union: Union { + render_as: RenderAs::Tag, + alternatives: vec![ + Ctor { + tag_id: TagId(0), + name: CtorName::Tag(ffalse), + arity: 0, + }, + Ctor { + tag_id: TagId(1), + name: CtorName::Tag(ttrue), + arity: 0, + }, + ], + }, + } + } + ByteUnion(tag_names) => { + let tag_id = tag_names + .iter() + .position(|key| tag_name == key.expect_tag_ref()) + .expect("tag must be in its own type"); + + let mut ctors = std::vec::Vec::with_capacity(tag_names.len()); + for (i, tag_name) in tag_names.into_iter().enumerate() { + ctors.push(Ctor { + tag_id: TagId(i as _), + name: CtorName::Tag(tag_name.expect_tag()), + arity: 0, + }) + } + + let union = roc_exhaustive::Union { + render_as: RenderAs::Tag, + alternatives: ctors, + }; + + Pattern::EnumLiteral { + tag_id: tag_id as u8, + tag_name: tag_name.clone(), + union, + } + } + Newtype { + arguments: field_layouts, + .. + } => { + let mut arguments = arguments.clone(); + + arguments.sort_by(|arg1, arg2| { + let size1 = layout_cache + .from_var(env.arena, arg1.0, env.subs) + .map(|x| layout_cache.interner.alignment_bytes(x)) + .unwrap_or(0); + + let size2 = layout_cache + .from_var(env.arena, arg2.0, env.subs) + .map(|x| layout_cache.interner.alignment_bytes(x)) + .unwrap_or(0); + + size2.cmp(&size1) + }); + + let mut mono_args = Vec::with_capacity_in(arguments.len(), env.arena); + for ((_, loc_pat), layout) in arguments.iter().zip(field_layouts.iter()) { + mono_args.push(( + from_can_pattern_help( + env, + procs, + layout_cache, + &loc_pat.value, + assignments, + )?, + *layout, + )); + } + + Pattern::NewtypeDestructure { + tag_name: tag_name.clone(), + arguments: mono_args, + } + } + NewtypeByVoid { + data_tag_arguments, + data_tag_name, + .. + } => { + let data_tag_name = data_tag_name.expect_tag(); + + if tag_name != &data_tag_name { + // this tag is not represented at runtime + Pattern::Voided { + tag_name: tag_name.clone(), + } + } else { + let mut arguments = arguments.clone(); + + arguments.sort_by(|arg1, arg2| { + let size1 = layout_cache + .from_var(env.arena, arg1.0, env.subs) + .map(|x| layout_cache.interner.alignment_bytes(x)) + .unwrap_or(0); + + let size2 = layout_cache + .from_var(env.arena, arg2.0, env.subs) + .map(|x| layout_cache.interner.alignment_bytes(x)) + .unwrap_or(0); + + size2.cmp(&size1) + }); + + let mut mono_args = Vec::with_capacity_in(arguments.len(), env.arena); + let it = arguments.iter().zip(data_tag_arguments.iter()); + for ((_, loc_pat), layout) in it { + mono_args.push(( + from_can_pattern_help( + env, + procs, + layout_cache, + &loc_pat.value, + assignments, + )?, + *layout, + )); + } + + Pattern::NewtypeDestructure { + tag_name: tag_name.clone(), + arguments: mono_args, + } + } + } + + Wrapped(variant) => { + let (tag_id, argument_layouts) = variant.tag_name_to_id(tag_name); + let number_of_tags = variant.number_of_tags(); + let mut ctors = std::vec::Vec::with_capacity(number_of_tags); + + let arguments = { + let mut temp = arguments.clone(); + + temp.sort_by(|arg1, arg2| { + let layout1 = + layout_cache.from_var(env.arena, arg1.0, env.subs).unwrap(); + let layout2 = + layout_cache.from_var(env.arena, arg2.0, env.subs).unwrap(); + + let size1 = layout_cache.interner.alignment_bytes(layout1); + let size2 = layout_cache.interner.alignment_bytes(layout2); + + size2.cmp(&size1) + }); + + temp + }; + + // we must derive the union layout from the whole_var, building it up + // from `layouts` would unroll recursive tag unions, and that leads to + // problems down the line because we hash layouts and an unrolled + // version is not the same as the minimal version. + let whole_var_layout = layout_cache.from_var(env.arena, *whole_var, env.subs); + let layout = + match whole_var_layout.map(|l| layout_cache.interner.chase_recursive(l)) { + Ok(LayoutRepr::Union(ul)) => ul, + _ => internal_error!(), + }; + + use WrappedVariant::*; + match variant { + NonRecursive { + sorted_tag_layouts: ref tags, + } => { + debug_assert!(tags.len() > 1); + + for (i, (tag_name, args)) in tags.iter().enumerate() { + ctors.push(Ctor { + tag_id: TagId(i as _), + name: CtorName::Tag(tag_name.expect_tag_ref().clone()), + arity: args.len(), + }) + } + + let union = roc_exhaustive::Union { + render_as: RenderAs::Tag, + alternatives: ctors, + }; + + let mut mono_args = Vec::with_capacity_in(arguments.len(), env.arena); + + debug_assert_eq!( + arguments.len(), + argument_layouts.len(), + "The {:?} tag got {} arguments, but its layout expects {}!", + tag_name, + arguments.len(), + argument_layouts.len(), + ); + let it = argument_layouts.iter(); + + for ((_, loc_pat), layout) in arguments.iter().zip(it) { + mono_args.push(( + from_can_pattern_help( + env, + procs, + layout_cache, + &loc_pat.value, + assignments, + )?, + *layout, + )); + } + + Pattern::AppliedTag { + tag_name: tag_name.clone(), + tag_id: tag_id as _, + arguments: mono_args, + union, + layout, + } + } + + Recursive { + sorted_tag_layouts: ref tags, + } => { + debug_assert!(tags.len() > 1); + + for (i, (tag_name, args)) in tags.iter().enumerate() { + ctors.push(Ctor { + tag_id: TagId(i as _), + name: CtorName::Tag(tag_name.expect_tag_ref().clone()), + arity: args.len(), + }) + } + + let union = roc_exhaustive::Union { + render_as: RenderAs::Tag, + alternatives: ctors, + }; + + let mut mono_args = Vec::with_capacity_in(arguments.len(), env.arena); + + debug_assert_eq!(arguments.len(), argument_layouts.len()); + let it = argument_layouts.iter(); + + for ((_, loc_pat), layout) in arguments.iter().zip(it) { + mono_args.push(( + from_can_pattern_help( + env, + procs, + layout_cache, + &loc_pat.value, + assignments, + )?, + *layout, + )); + } + + Pattern::AppliedTag { + tag_name: tag_name.clone(), + tag_id: tag_id as _, + arguments: mono_args, + union, + layout, + } + } + + NonNullableUnwrapped { + tag_name: w_tag_name, + fields, + } => { + debug_assert_eq!(w_tag_name.expect_tag_ref(), tag_name); + + ctors.push(Ctor { + tag_id: TagId(0), + name: CtorName::Tag(tag_name.clone()), + arity: fields.len(), + }); + + let union = roc_exhaustive::Union { + render_as: RenderAs::Tag, + alternatives: ctors, + }; + + let mut mono_args = Vec::with_capacity_in(arguments.len(), env.arena); + + debug_assert_eq!(arguments.len(), argument_layouts.len()); + let it = argument_layouts.iter(); + + for ((_, loc_pat), layout) in arguments.iter().zip(it) { + mono_args.push(( + from_can_pattern_help( + env, + procs, + layout_cache, + &loc_pat.value, + assignments, + )?, + *layout, + )); + } + + Pattern::AppliedTag { + tag_name: tag_name.clone(), + tag_id: tag_id as _, + arguments: mono_args, + union, + layout, + } + } + + NullableWrapped { + sorted_tag_layouts: ref non_nulled_tags, + nullable_id, + nullable_name, + } => { + for id in 0..(non_nulled_tags.len() + 1) { + if id == nullable_id as usize { + ctors.push(Ctor { + tag_id: TagId(id as _), + name: CtorName::Tag(nullable_name.expect_tag_ref().clone()), + arity: 0, + }); + } else { + let i = if id < nullable_id.into() { id } else { id - 1 }; + let (tag_name, args) = &non_nulled_tags[i]; + ctors.push(Ctor { + tag_id: TagId(i as _), + name: CtorName::Tag(tag_name.expect_tag_ref().clone()), + arity: args.len(), + }); + } + } + + let union = roc_exhaustive::Union { + render_as: RenderAs::Tag, + alternatives: ctors, + }; + + let mut mono_args = Vec::with_capacity_in(arguments.len(), env.arena); + + let it = if tag_name == nullable_name.expect_tag_ref() { + [].iter() + } else { + argument_layouts.iter() + }; + + for ((_, loc_pat), layout) in arguments.iter().zip(it) { + mono_args.push(( + from_can_pattern_help( + env, + procs, + layout_cache, + &loc_pat.value, + assignments, + )?, + *layout, + )); + } + + Pattern::AppliedTag { + tag_name: tag_name.clone(), + tag_id: tag_id as _, + arguments: mono_args, + union, + layout, + } + } + + NullableUnwrapped { + other_fields, + nullable_id, + nullable_name, + other_name: _, + } => { + debug_assert!(!other_fields.is_empty()); + + ctors.push(Ctor { + tag_id: TagId(nullable_id as _), + name: CtorName::Tag(nullable_name.expect_tag_ref().clone()), + arity: 0, + }); + + ctors.push(Ctor { + tag_id: TagId(!nullable_id as _), + name: CtorName::Tag(nullable_name.expect_tag_ref().clone()), + arity: other_fields.len(), + }); + + let union = roc_exhaustive::Union { + render_as: RenderAs::Tag, + alternatives: ctors, + }; + + let mut mono_args = Vec::with_capacity_in(arguments.len(), env.arena); + + let it = if tag_name == nullable_name.expect_tag_ref() { + [].iter() + } else { + argument_layouts.iter() + }; + + for ((_, loc_pat), layout) in arguments.iter().zip(it) { + mono_args.push(( + from_can_pattern_help( + env, + procs, + layout_cache, + &loc_pat.value, + assignments, + )?, + *layout, + )); + } + + Pattern::AppliedTag { + tag_name: tag_name.clone(), + tag_id: tag_id as _, + arguments: mono_args, + union, + layout, + } + } + } + } + }; + + Ok(result) + } + + UnwrappedOpaque { + opaque, argument, .. + } => { + let (arg_var, loc_arg_pattern) = &(**argument); + let arg_layout = layout_cache + .from_var(env.arena, *arg_var, env.subs) + .unwrap(); + let mono_arg_pattern = from_can_pattern_help( + env, + procs, + layout_cache, + &loc_arg_pattern.value, + assignments, + )?; + Ok(Pattern::OpaqueUnwrap { + opaque: *opaque, + argument: Box::new((mono_arg_pattern, arg_layout)), + }) + } + + TupleDestructure { + whole_var, + destructs, + .. + } => { + // sorted fields based on the type + let sorted_elems = { + let mut layout_env = layout::Env::from_components( + layout_cache, + env.subs, + env.arena, + env.target_info, + ); + crate::layout::sort_tuple_elems(&mut layout_env, *whole_var) + .map_err(RuntimeError::from)? + }; + + // sorted fields based on the destruct + let mut mono_destructs = Vec::with_capacity_in(destructs.len(), env.arena); + let mut destructs_by_index = Vec::with_capacity_in(destructs.len(), env.arena); + destructs_by_index.extend(destructs.iter().map(Some)); + + let mut elem_layouts = Vec::with_capacity_in(sorted_elems.len(), env.arena); + + for (index, variable, res_layout) in sorted_elems.into_iter() { + if index < destructs.len() { + // this elem is destructured by the pattern + mono_destructs.push(from_can_tuple_destruct( + env, + procs, + layout_cache, + &destructs[index].value, + res_layout, + assignments, + )?); + } else { + // this elem is not destructured by the pattern + // put in an underscore + mono_destructs.push(TupleDestruct { + index, + variable, + layout: res_layout, + pat: Pattern::Underscore, + }); + } + + // the layout of this field is part of the layout of the record + elem_layouts.push(res_layout); + } + + Ok(Pattern::TupleDestructure( + mono_destructs, + elem_layouts.into_bump_slice(), + )) + } + + RecordDestructure { + whole_var, + destructs, + .. + } => { + // sorted fields based on the type + let sorted_fields = { + let mut layout_env = layout::Env::from_components( + layout_cache, + env.subs, + env.arena, + env.target_info, + ); + crate::layout::sort_record_fields(&mut layout_env, *whole_var) + .map_err(RuntimeError::from)? + }; + + // sorted fields based on the destruct + let mut mono_destructs = Vec::with_capacity_in(destructs.len(), env.arena); + let mut destructs_by_label = BumpMap::with_capacity_in(destructs.len(), env.arena); + destructs_by_label.extend(destructs.iter().map(|x| (&x.value.label, x))); + + let mut field_layouts = Vec::with_capacity_in(sorted_fields.len(), env.arena); + + // next we step through both sequences of fields. The outer loop is the sequence based + // on the type, since not all fields need to actually be destructured in the source + // language. + // + // However in mono patterns, we do destruct all patterns (but use Underscore) when + // in the source the field is not matche in the source language. + // + // Optional fields somewhat complicate the matter here + + for (label, variable, res_layout) in sorted_fields.into_iter() { + match res_layout { + Ok(field_layout) => { + // the field is non-optional according to the type + + match destructs_by_label.remove(&label) { + Some(destruct) => { + // this field is destructured by the pattern + mono_destructs.push(from_can_record_destruct( + env, + procs, + layout_cache, + &destruct.value, + field_layout, + assignments, + )?); + } + None => { + // this field is not destructured by the pattern + // put in an underscore + mono_destructs.push(RecordDestruct { + label: label.clone(), + variable, + layout: field_layout, + typ: DestructType::Guard(Pattern::Underscore), + }); + } + } + + // the layout of this field is part of the layout of the record + field_layouts.push(field_layout); + } + Err(field_layout) => { + // the field is optional according to the type + match destructs_by_label.remove(&label) { + Some(destruct) => { + // this field is destructured by the pattern + match &destruct.value.typ { + roc_can::pattern::DestructType::Optional(_, loc_expr) => { + // if we reach this stage, the optional field is not present + // so we push the default assignment into the branch + assignments.push(( + destruct.value.symbol, + variable, + loc_expr.value.clone(), + )); + } + _ => unreachable!( + "only optional destructs can be optional fields" + ), + }; + } + None => { + // this field is not destructured by the pattern + // put in an underscore + mono_destructs.push(RecordDestruct { + label: label.clone(), + variable, + layout: field_layout, + typ: DestructType::Guard(Pattern::Underscore), + }); + } + } + } + } + } + + for (_, destruct) in destructs_by_label.drain() { + // this destruct is not in the type, but is in the pattern + // it must be an optional field, and we will use the default + match &destruct.value.typ { + roc_can::pattern::DestructType::Optional(field_var, loc_expr) => { + assignments.push(( + destruct.value.symbol, + // destruct.value.var, + *field_var, + loc_expr.value.clone(), + )); + } + _ => unreachable!("only optional destructs can be optional fields"), + } + } + + Ok(Pattern::RecordDestructure( + mono_destructs, + field_layouts.into_bump_slice(), + )) + } + + List { + list_var, + elem_var, + patterns, + } => { + let list_layout = match layout_cache.from_var(env.arena, *list_var, env.subs) { + Ok(lay) => lay, + Err(LayoutProblem::UnresolvedTypeVar(_)) => { + return Err(RuntimeError::UnresolvedTypeVar) + } + Err(LayoutProblem::Erroneous) => return Err(RuntimeError::ErroneousType), + }; + + let element_layout = match layout_cache.from_var(env.arena, *elem_var, env.subs) { + Ok(lay) => lay, + Err(LayoutProblem::UnresolvedTypeVar(_)) => { + return Err(RuntimeError::UnresolvedTypeVar) + } + Err(LayoutProblem::Erroneous) => return Err(RuntimeError::ErroneousType), + }; + + let arity = patterns.arity(); + + let mut mono_patterns = Vec::with_capacity_in(patterns.patterns.len(), env.arena); + for loc_pat in patterns.patterns.iter() { + let mono_pat = + from_can_pattern_help(env, procs, layout_cache, &loc_pat.value, assignments)?; + mono_patterns.push(mono_pat); + } + + Ok(Pattern::List { + arity, + list_layout, + element_layout, + elements: mono_patterns, + opt_rest: patterns.opt_rest, + }) + } + } +} + +fn make_num_literal_pattern<'a>( + env: &mut Env<'a, '_>, + layout_cache: &mut LayoutCache<'a>, + variable: Variable, + num_str: &str, + num_value: IntOrFloatValue, +) -> Pattern<'a> { + let layout = layout_cache + .from_var(env.arena, variable, env.subs) + .unwrap(); + let literal = make_num_literal(&layout_cache.interner, layout, num_str, num_value); + literal.to_pattern() +} + +fn from_can_record_destruct<'a>( + env: &mut Env<'a, '_>, + procs: &mut Procs<'a>, + layout_cache: &mut LayoutCache<'a>, + can_rd: &roc_can::pattern::RecordDestruct, + field_layout: InLayout<'a>, + assignments: &mut Vec<'a, (Symbol, Variable, roc_can::expr::Expr)>, +) -> Result, RuntimeError> { + Ok(RecordDestruct { + label: can_rd.label.clone(), + variable: can_rd.var, + layout: field_layout, + typ: match &can_rd.typ { + roc_can::pattern::DestructType::Required => DestructType::Required(can_rd.symbol), + roc_can::pattern::DestructType::Optional(_, _) => { + // if we reach this stage, the optional field is present + DestructType::Required(can_rd.symbol) + } + roc_can::pattern::DestructType::Guard(_, loc_pattern) => DestructType::Guard( + from_can_pattern_help(env, procs, layout_cache, &loc_pattern.value, assignments)?, + ), + }, + }) +} + +fn from_can_tuple_destruct<'a>( + env: &mut Env<'a, '_>, + procs: &mut Procs<'a>, + layout_cache: &mut LayoutCache<'a>, + can_rd: &roc_can::pattern::TupleDestruct, + field_layout: InLayout<'a>, + assignments: &mut Vec<'a, (Symbol, Variable, roc_can::expr::Expr)>, +) -> Result, RuntimeError> { + Ok(TupleDestruct { + index: can_rd.destruct_index, + variable: can_rd.var, + layout: field_layout, + pat: from_can_pattern_help(env, procs, layout_cache, &can_rd.typ.1.value, assignments)?, + }) +} + +#[allow(clippy::too_many_arguments)] +pub fn store_pattern<'a>( + env: &mut Env<'a, '_>, + procs: &mut Procs<'a>, + layout_cache: &mut LayoutCache<'a>, + can_pat: &Pattern<'a>, + outer_symbol: Symbol, + stmt: Stmt<'a>, +) -> Stmt<'a> { + match store_pattern_help(env, procs, layout_cache, can_pat, outer_symbol, stmt) { + StorePattern::Productive(new) => new, + StorePattern::NotProductive(new) => new, + } +} + +enum StorePattern<'a> { + /// we bound new symbols + Productive(Stmt<'a>), + /// no new symbols were bound in this pattern + NotProductive(Stmt<'a>), +} + +/// It is crucial for correct RC insertion that we don't create dead variables! +#[allow(clippy::too_many_arguments)] +fn store_pattern_help<'a>( + env: &mut Env<'a, '_>, + procs: &mut Procs<'a>, + layout_cache: &mut LayoutCache<'a>, + can_pat: &Pattern<'a>, + outer_symbol: Symbol, + mut stmt: Stmt<'a>, +) -> StorePattern<'a> { + use Pattern::*; + + match can_pat { + Identifier(symbol) => { + substitute_in_exprs(env.arena, &mut stmt, *symbol, outer_symbol); + } + Underscore => { + // do nothing + return StorePattern::NotProductive(stmt); + } + As(subpattern, symbol) => { + let stored_subpattern = + store_pattern_help(env, procs, layout_cache, subpattern, outer_symbol, stmt); + + let mut stmt = match stored_subpattern { + StorePattern::Productive(stmt) => stmt, + StorePattern::NotProductive(stmt) => stmt, + }; + + substitute_in_exprs(env.arena, &mut stmt, *symbol, outer_symbol); + + return StorePattern::Productive(stmt); + } + IntLiteral(_, _) + | FloatLiteral(_, _) + | DecimalLiteral(_) + | EnumLiteral { .. } + | BitLiteral { .. } + | StrLiteral(_) => { + return StorePattern::NotProductive(stmt); + } + NewtypeDestructure { arguments, .. } => match arguments.as_slice() { + [(pattern, _layout)] => { + return store_pattern_help(env, procs, layout_cache, pattern, outer_symbol, stmt); + } + _ => { + let mut fields = Vec::with_capacity_in(arguments.len(), env.arena); + fields.extend(arguments.iter().map(|x| x.1)); + + let layout = layout_cache + .put_in_direct_no_semantic(LayoutRepr::struct_(fields.into_bump_slice())); + + return store_newtype_pattern( + env, + procs, + layout_cache, + outer_symbol, + layout, + arguments, + stmt, + ); + } + }, + AppliedTag { + arguments, + layout, + tag_id, + .. + } => { + return store_tag_pattern( + env, + procs, + layout_cache, + outer_symbol, + *layout, + arguments, + *tag_id, + stmt, + ); + } + + List { + arity, + list_layout, + element_layout, + elements, + opt_rest, + } => { + return store_list_pattern( + env, + procs, + layout_cache, + outer_symbol, + *arity, + *list_layout, + *element_layout, + elements, + opt_rest, + stmt, + ) + } + + Voided { .. } => { + return StorePattern::NotProductive(stmt); + } + + OpaqueUnwrap { argument, .. } => { + let (pattern, _layout) = &**argument; + return store_pattern_help(env, procs, layout_cache, pattern, outer_symbol, stmt); + } + + RecordDestructure(destructs, [_single_field]) => { + for destruct in destructs { + match &destruct.typ { + DestructType::Required(symbol) => { + substitute_in_exprs(env.arena, &mut stmt, *symbol, outer_symbol); + } + DestructType::Guard(guard_pattern) => { + return store_pattern_help( + env, + procs, + layout_cache, + guard_pattern, + outer_symbol, + stmt, + ); + } + } + } + } + RecordDestructure(destructs, sorted_fields) => { + let mut is_productive = false; + for (index, destruct) in destructs.iter().enumerate().rev() { + match store_record_destruct( + env, + procs, + layout_cache, + destruct, + index as u64, + outer_symbol, + sorted_fields, + stmt, + ) { + StorePattern::Productive(new) => { + is_productive = true; + stmt = new; + } + StorePattern::NotProductive(new) => { + stmt = new; + } + } + } + + if !is_productive { + return StorePattern::NotProductive(stmt); + } + } + + TupleDestructure(destructs, [_single_field]) => { + if let Some(destruct) = destructs.first() { + return store_pattern_help( + env, + procs, + layout_cache, + &destruct.pat, + outer_symbol, + stmt, + ); + } + } + TupleDestructure(destructs, sorted_fields) => { + let mut is_productive = false; + for (index, destruct) in destructs.iter().enumerate().rev() { + match store_tuple_destruct( + env, + procs, + layout_cache, + destruct, + index as u64, + outer_symbol, + sorted_fields, + stmt, + ) { + StorePattern::Productive(new) => { + is_productive = true; + stmt = new; + } + StorePattern::NotProductive(new) => { + stmt = new; + } + } + } + + if !is_productive { + return StorePattern::NotProductive(stmt); + } + } + } + + StorePattern::Productive(stmt) +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct ListIndex( + /// Positive if we should index from the head, negative if we should index from the tail + /// 0 is lst[0] + /// -1 is lst[List.len lst - 1] + i64, +); + +impl ListIndex { + pub fn from_pattern_index(index: usize, arity: ListArity) -> Self { + match arity { + ListArity::Exact(_) => Self(index as _), + ListArity::Slice(head, tail) => { + if index < head { + Self(index as _) + } else { + // Slice(head=2, tail=5) + // + // s t ... w y z x q + // 0 1 2 3 4 5 6 index + // 0 1 2 3 4 (index - head) + // 5 4 3 2 1 (tail - (index - head)) + Self(-((tail - (index - head)) as i64)) + } + } + } + } +} + +pub(crate) type Store<'a> = (Symbol, InLayout<'a>, Expr<'a>); + +/// Builds the list index we should index into +#[must_use] +pub(crate) fn build_list_index_probe<'a>( + env: &mut Env<'a, '_>, + list_sym: Symbol, + list_index: &ListIndex, +) -> (Symbol, impl DoubleEndedIterator>) { + let usize_layout = Layout::usize(env.target_info); + + let list_index = list_index.0; + let index_sym = env.unique_symbol(); + + let (opt_len_store, opt_offset_store, index_store) = if list_index >= 0 { + let index_expr = Expr::Literal(Literal::Int((list_index as i128).to_ne_bytes())); + + let index_store = (index_sym, usize_layout, index_expr); + + (None, None, index_store) + } else { + let len_sym = env.unique_symbol(); + let len_expr = Expr::Call(Call { + call_type: CallType::LowLevel { + op: LowLevel::ListLen, + update_mode: env.next_update_mode_id(), + }, + arguments: env.arena.alloc([list_sym]), + }); + + let offset = list_index.abs(); + let offset_sym = env.unique_symbol(); + let offset_expr = Expr::Literal(Literal::Int((offset as i128).to_ne_bytes())); + + let index_expr = Expr::Call(Call { + call_type: CallType::LowLevel { + op: LowLevel::NumSub, + update_mode: env.next_update_mode_id(), + }, + arguments: env.arena.alloc([len_sym, offset_sym]), + }); + + let len_store = (len_sym, usize_layout, len_expr); + let offset_store = (offset_sym, usize_layout, offset_expr); + let index_store = (index_sym, usize_layout, index_expr); + + (Some(len_store), Some(offset_store), index_store) + }; + + let stores = (opt_len_store.into_iter()) + .chain(opt_offset_store) + .chain([index_store]); + + (index_sym, stores) +} + +#[allow(clippy::too_many_arguments)] +fn store_list_pattern<'a>( + env: &mut Env<'a, '_>, + procs: &mut Procs<'a>, + layout_cache: &mut LayoutCache<'a>, + list_sym: Symbol, + list_arity: ListArity, + list_layout: InLayout<'a>, + element_layout: InLayout<'a>, + elements: &[Pattern<'a>], + opt_rest: &Option<(usize, Option)>, + mut stmt: Stmt<'a>, +) -> StorePattern<'a> { + use Pattern::*; + + let mut is_productive = false; + + for (index, element) in elements.iter().enumerate().rev() { + let compute_element_load = |env: &mut Env<'a, '_>| { + let list_index = ListIndex::from_pattern_index(index, list_arity); + + let (index_sym, needed_stores) = build_list_index_probe(env, list_sym, &list_index); + + let load = Expr::Call(Call { + call_type: CallType::LowLevel { + op: LowLevel::ListGetUnsafe, + update_mode: env.next_update_mode_id(), + }, + arguments: env.arena.alloc([list_sym, index_sym]), + }); + + (load, needed_stores) + }; + + let (store_loaded, needed_stores) = match element { + Identifier(symbol) => { + let (load, needed_stores) = compute_element_load(env); + + // store immediately in the given symbol + ( + Stmt::Let(*symbol, load, element_layout, env.arena.alloc(stmt)), + needed_stores, + ) + } + Underscore + | IntLiteral(_, _) + | FloatLiteral(_, _) + | DecimalLiteral(_) + | EnumLiteral { .. } + | BitLiteral { .. } + | StrLiteral(_) => { + // ignore + continue; + } + _ => { + // store the field in a symbol, and continue matching on it + let symbol = env.unique_symbol(); + + // first recurse, continuing to unpack symbol + match store_pattern_help(env, procs, layout_cache, element, symbol, stmt) { + StorePattern::Productive(new) => { + stmt = new; + let (load, needed_stores) = compute_element_load(env); + + // only if we bind one of its (sub)fields to a used name should we + // extract the field + ( + Stmt::Let(symbol, load, element_layout, env.arena.alloc(stmt)), + needed_stores, + ) + } + StorePattern::NotProductive(new) => { + // do nothing + stmt = new; + continue; + } + } + } + }; + + is_productive = true; + + stmt = store_loaded; + for (sym, lay, expr) in needed_stores.rev() { + stmt = Stmt::Let(sym, expr, lay, env.arena.alloc(stmt)); + } + } + + stmt = store_list_rest(env, list_sym, list_arity, list_layout, opt_rest, stmt); + + if is_productive { + StorePattern::Productive(stmt) + } else { + StorePattern::NotProductive(stmt) + } +} + +fn store_list_rest<'a>( + env: &mut Env<'a, '_>, + list_sym: Symbol, + list_arity: ListArity, + list_layout: InLayout<'a>, + opt_rest: &Option<(usize, Option)>, + mut stmt: Stmt<'a>, +) -> Stmt<'a> { + if let Some((index, Some(rest_sym))) = opt_rest { + let usize_layout = Layout::usize(env.target_info); + + let total_dropped = list_arity.min_len(); + + let total_dropped_sym = env.unique_symbol(); + let total_dropped_expr = Expr::Literal(Literal::Int((total_dropped as u128).to_ne_bytes())); + + let list_len_sym = env.unique_symbol(); + let list_len_expr = Expr::Call(Call { + call_type: CallType::LowLevel { + op: LowLevel::ListLen, + update_mode: env.next_update_mode_id(), + }, + arguments: env.arena.alloc([list_sym]), + }); + + let rest_len_sym = env.unique_symbol(); + let rest_len_expr = Expr::Call(Call { + call_type: CallType::LowLevel { + op: LowLevel::NumSub, + update_mode: env.next_update_mode_id(), + }, + arguments: env.arena.alloc([list_len_sym, total_dropped_sym]), + }); + + let start_sym = env.unique_symbol(); + let start_expr = Expr::Literal(Literal::Int((*index as u128).to_ne_bytes())); + + let rest_expr = Expr::Call(Call { + call_type: CallType::LowLevel { + op: LowLevel::ListSublist, + update_mode: env.next_update_mode_id(), + }, + arguments: env.arena.alloc([list_sym, start_sym, rest_len_sym]), + }); + let needed_stores = [ + (total_dropped_sym, total_dropped_expr, usize_layout), + (list_len_sym, list_len_expr, usize_layout), + (rest_len_sym, rest_len_expr, usize_layout), + (start_sym, start_expr, usize_layout), + (*rest_sym, rest_expr, list_layout), + ]; + for (sym, expr, lay) in needed_stores.into_iter().rev() { + stmt = Stmt::Let(sym, expr, lay, env.arena.alloc(stmt)); + } + } + stmt +} + +#[allow(clippy::too_many_arguments)] +fn store_tag_pattern<'a>( + env: &mut Env<'a, '_>, + procs: &mut Procs<'a>, + layout_cache: &mut LayoutCache<'a>, + structure: Symbol, + union_layout: UnionLayout<'a>, + arguments: &[(Pattern<'a>, InLayout<'a>)], + tag_id: TagIdIntType, + mut stmt: Stmt<'a>, +) -> StorePattern<'a> { + use Pattern::*; + + let mut is_productive = false; + + for (index, (argument, arg_layout)) in arguments.iter().enumerate().rev() { + let mut arg_layout = *arg_layout; + + if let LayoutRepr::RecursivePointer(_) = layout_cache.get_repr(arg_layout) { + // TODO(recursive-layouts): fix after disjoint rec ptrs + arg_layout = layout_cache.put_in_direct_no_semantic(LayoutRepr::Union(union_layout)); + } + + let load = Expr::UnionAtIndex { + index: index as u64, + structure, + tag_id, + union_layout, + }; + + match argument { + Identifier(symbol) => { + // store immediately in the given symbol + stmt = Stmt::Let(*symbol, load, arg_layout, env.arena.alloc(stmt)); + is_productive = true; + } + Underscore => { + // ignore + } + IntLiteral(_, _) + | FloatLiteral(_, _) + | DecimalLiteral(_) + | EnumLiteral { .. } + | BitLiteral { .. } + | StrLiteral(_) => {} + _ => { + // store the field in a symbol, and continue matching on it + let symbol = env.unique_symbol(); + + // first recurse, continuing to unpack symbol + match store_pattern_help(env, procs, layout_cache, argument, symbol, stmt) { + StorePattern::Productive(new) => { + is_productive = true; + stmt = new; + // only if we bind one of its (sub)fields to a used name should we + // extract the field + stmt = Stmt::Let(symbol, load, arg_layout, env.arena.alloc(stmt)); + } + StorePattern::NotProductive(new) => { + // do nothing + stmt = new; + } + } + } + } + } + + if is_productive { + StorePattern::Productive(stmt) + } else { + StorePattern::NotProductive(stmt) + } +} + +#[allow(clippy::too_many_arguments)] +fn store_newtype_pattern<'a>( + env: &mut Env<'a, '_>, + procs: &mut Procs<'a>, + layout_cache: &mut LayoutCache<'a>, + structure: Symbol, + layout: InLayout<'a>, + arguments: &[(Pattern<'a>, InLayout<'a>)], + mut stmt: Stmt<'a>, +) -> StorePattern<'a> { + use Pattern::*; + + let mut arg_layouts = Vec::with_capacity_in(arguments.len(), env.arena); + let mut is_productive = false; + + for (_, layout) in arguments { + arg_layouts.push(*layout); + } + + for (index, (argument, arg_layout)) in arguments.iter().enumerate().rev() { + let mut arg_layout = *arg_layout; + + if let LayoutRepr::RecursivePointer(_) = layout_cache.get_repr(arg_layout) { + arg_layout = layout; + } + + let load = Expr::StructAtIndex { + index: index as u64, + field_layouts: arg_layouts.clone().into_bump_slice(), + structure, + }; + + match argument { + Identifier(symbol) => { + stmt = Stmt::Let(*symbol, load, arg_layout, env.arena.alloc(stmt)); + is_productive = true; + } + Underscore => { + // ignore + } + IntLiteral(_, _) + | FloatLiteral(_, _) + | DecimalLiteral(_) + | EnumLiteral { .. } + | BitLiteral { .. } + | StrLiteral(_) => {} + _ => { + // store the field in a symbol, and continue matching on it + let symbol = env.unique_symbol(); + + // first recurse, continuing to unpack symbol + match store_pattern_help(env, procs, layout_cache, argument, symbol, stmt) { + StorePattern::Productive(new) => { + is_productive = true; + stmt = new; + // only if we bind one of its (sub)fields to a used name should we + // extract the field + stmt = Stmt::Let(symbol, load, arg_layout, env.arena.alloc(stmt)); + } + StorePattern::NotProductive(new) => { + // do nothing + stmt = new; + } + } + } + } + } + + if is_productive { + StorePattern::Productive(stmt) + } else { + StorePattern::NotProductive(stmt) + } +} + +#[allow(clippy::too_many_arguments)] +fn store_tuple_destruct<'a>( + env: &mut Env<'a, '_>, + procs: &mut Procs<'a>, + layout_cache: &mut LayoutCache<'a>, + destruct: &TupleDestruct<'a>, + index: u64, + outer_symbol: Symbol, + sorted_fields: &'a [InLayout<'a>], + mut stmt: Stmt<'a>, +) -> StorePattern<'a> { + use Pattern::*; + + let load = Expr::StructAtIndex { + index, + field_layouts: sorted_fields, + structure: outer_symbol, + }; + + match &destruct.pat { + Identifier(symbol) => { + stmt = Stmt::Let(*symbol, load, destruct.layout, env.arena.alloc(stmt)); + } + Underscore => { + // important that this is special-cased to do nothing: mono record patterns will extract all the + // fields, but those not bound in the source code are guarded with the underscore + // pattern. So given some record `{ x : a, y : b }`, a match + // + // { x } -> ... + // + // is actually + // + // { x, y: _ } -> ... + // + // internally. But `y` is never used, so we must make sure it't not stored/loaded. + // + // This also happens with tuples, so when matching a tuple `(a, b, c)`, + // a pattern like `(x, y)` will be internally rewritten to `(x, y, _)`. + return StorePattern::NotProductive(stmt); + } + IntLiteral(_, _) + | FloatLiteral(_, _) + | DecimalLiteral(_) + | EnumLiteral { .. } + | BitLiteral { .. } + | StrLiteral(_) => { + return StorePattern::NotProductive(stmt); + } + + _ => { + let symbol = env.unique_symbol(); + + match store_pattern_help(env, procs, layout_cache, &destruct.pat, symbol, stmt) { + StorePattern::Productive(new) => { + stmt = new; + stmt = Stmt::Let(symbol, load, destruct.layout, env.arena.alloc(stmt)); + } + StorePattern::NotProductive(stmt) => return StorePattern::NotProductive(stmt), + } + } + } + + StorePattern::Productive(stmt) +} + +#[allow(clippy::too_many_arguments)] +fn store_record_destruct<'a>( + env: &mut Env<'a, '_>, + procs: &mut Procs<'a>, + layout_cache: &mut LayoutCache<'a>, + destruct: &RecordDestruct<'a>, + index: u64, + outer_symbol: Symbol, + sorted_fields: &'a [InLayout<'a>], + mut stmt: Stmt<'a>, +) -> StorePattern<'a> { + use Pattern::*; + + let load = Expr::StructAtIndex { + index, + field_layouts: sorted_fields, + structure: outer_symbol, + }; + + match &destruct.typ { + DestructType::Required(symbol) => { + stmt = Stmt::Let(*symbol, load, destruct.layout, env.arena.alloc(stmt)); + } + DestructType::Guard(guard_pattern) => match &guard_pattern { + Identifier(symbol) => { + stmt = Stmt::Let(*symbol, load, destruct.layout, env.arena.alloc(stmt)); + } + Underscore => { + // important that this is special-cased to do nothing: mono record patterns will extract all the + // fields, but those not bound in the source code are guarded with the underscore + // pattern. So given some record `{ x : a, y : b }`, a match + // + // { x } -> ... + // + // is actually + // + // { x, y: _ } -> ... + // + // internally. But `y` is never used, so we must make sure it't not stored/loaded. + return StorePattern::NotProductive(stmt); + } + IntLiteral(_, _) + | FloatLiteral(_, _) + | DecimalLiteral(_) + | EnumLiteral { .. } + | BitLiteral { .. } + | StrLiteral(_) => { + return StorePattern::NotProductive(stmt); + } + + _ => { + let symbol = env.unique_symbol(); + + match store_pattern_help(env, procs, layout_cache, guard_pattern, symbol, stmt) { + StorePattern::Productive(new) => { + stmt = new; + stmt = Stmt::Let(symbol, load, destruct.layout, env.arena.alloc(stmt)); + } + StorePattern::NotProductive(stmt) => return StorePattern::NotProductive(stmt), + } + } + }, + } + + StorePattern::Productive(stmt) +} diff --git a/crates/compiler/mono/src/layout.rs b/crates/compiler/mono/src/layout.rs new file mode 100644 index 0000000000..6056f3e98d --- /dev/null +++ b/crates/compiler/mono/src/layout.rs @@ -0,0 +1,4845 @@ +use crate::ir::Parens; +use crate::layout::intern::NeedsRecursionPointerFixup; +use bitvec::vec::BitVec; +use bumpalo::collections::Vec; +use bumpalo::Bump; +use roc_builtins::bitcode::{FloatWidth, IntWidth}; +use roc_collections::all::{default_hasher, FnvMap, MutMap}; +use roc_collections::{SmallVec, VecSet}; +use roc_error_macros::{internal_error, todo_abilities}; +use roc_module::ident::{Lowercase, TagName}; +use roc_module::symbol::{Interns, Symbol}; +use roc_problem::can::RuntimeError; +use roc_target::{PtrWidth, TargetInfo}; +use roc_types::num::NumericRange; +use roc_types::subs::{ + self, Content, FlatType, GetSubsSlice, OptVariable, RecordFields, Subs, TagExt, TupleElems, + UnsortedUnionLabels, Variable, VariableSubsSlice, +}; +use roc_types::types::{ + gather_fields_unsorted_iter, gather_tuple_elems_unsorted_iter, RecordField, RecordFieldsError, + TupleElemsError, +}; +use std::cmp::Ordering; +use std::collections::hash_map::Entry; +use std::collections::HashMap; +use std::hash::Hash; +use ven_pretty::{DocAllocator, DocBuilder}; + +mod erased; +mod intern; +mod semantic; + +pub use erased::Erased; +pub use intern::{ + GlobalLayoutInterner, InLayout, LayoutInterner, STLayoutInterner, TLLayoutInterner, +}; +pub use semantic::SemanticRepr; + +// if your changes cause this number to go down, great! +// please change it to the lower number. +// if it went up, maybe check that the change is really required +roc_error_macros::assert_sizeof_aarch64!(Builtin, 2 * 8); +roc_error_macros::assert_sizeof_aarch64!(Layout, 9 * 8); +roc_error_macros::assert_sizeof_aarch64!(UnionLayout, 3 * 8); +roc_error_macros::assert_sizeof_aarch64!(LambdaSet, 5 * 8); + +roc_error_macros::assert_sizeof_wasm!(Builtin, 2 * 4); +roc_error_macros::assert_sizeof_wasm!(Layout, 9 * 4); +roc_error_macros::assert_sizeof_wasm!(UnionLayout, 3 * 4); +roc_error_macros::assert_sizeof_wasm!(LambdaSet, 5 * 4); + +roc_error_macros::assert_sizeof_default!(Builtin, 2 * 8); +roc_error_macros::assert_sizeof_default!(Layout, 9 * 8); +roc_error_macros::assert_sizeof_default!(UnionLayout, 3 * 8); +roc_error_macros::assert_sizeof_default!(LambdaSet, 5 * 8); + +type LayoutResult<'a> = Result, LayoutProblem>; +type RawFunctionLayoutResult<'a> = Result, LayoutProblem>; + +#[derive(Debug, Clone)] +struct CacheMeta { + /// Does this cache entry include a recursive structure? If so, what's the recursion variable + /// of that structure? + recursive_structures: SmallVec, +} + +impl CacheMeta { + #[inline(always)] + fn into_criteria(self) -> CacheCriteria { + let CacheMeta { + recursive_structures, + } = self; + CacheCriteria { + has_naked_recursion_pointer: false, + recursive_structures, + } + } +} + +/// A single layer of the layout cache. +/// Snapshots are implemented by operating on new layers, and rollbacks by dropping the latest +/// layer. +#[derive(Debug)] +struct CacheLayer(FnvMap); + +impl Default for CacheLayer { + fn default() -> Self { + Self(Default::default()) + } +} + +#[cfg(debug_assertions)] +#[derive(Debug, Default, Clone, Copy)] +pub struct CacheStatistics { + pub hits: u64, + pub misses: u64, + /// How many times we could not cache a calculated layout + pub non_insertable: u64, + /// How many time we could not reuse a cached layout + pub non_reusable: u64, + /// How many times an entry was added to the cache + pub insertions: u64, +} + +macro_rules! inc_stat { + ($stats:expr, $field:ident) => { + #[cfg(debug_assertions)] + { + $stats.$field += 1; + } + }; +} + +/// Layout cache to avoid recomputing [Layout] from a [Variable] multiple times. +#[derive(Debug)] +pub struct LayoutCache<'a> { + pub target_info: TargetInfo, + cache: std::vec::Vec>>, + raw_function_cache: std::vec::Vec>>, + + pub interner: TLLayoutInterner<'a>, + + /// Statistics on the usage of the layout cache. + #[cfg(debug_assertions)] + stats: CacheStatistics, + #[cfg(debug_assertions)] + raw_function_stats: CacheStatistics, +} + +impl<'a> LayoutCache<'a> { + pub fn new(interner: TLLayoutInterner<'a>, target_info: TargetInfo) -> Self { + let mut cache = std::vec::Vec::with_capacity(4); + cache.push(Default::default()); + let mut raw_cache = std::vec::Vec::with_capacity(4); + raw_cache.push(Default::default()); + Self { + target_info, + cache, + raw_function_cache: raw_cache, + + interner, + + #[cfg(debug_assertions)] + stats: CacheStatistics::default(), + #[cfg(debug_assertions)] + raw_function_stats: CacheStatistics::default(), + } + } + + pub fn from_var( + &mut self, + arena: &'a Bump, + var: Variable, + subs: &Subs, + ) -> Result, LayoutProblem> { + // Store things according to the root Variable, to avoid duplicate work. + let var = subs.get_root_key_without_compacting(var); + + let mut env = Env { + arena, + subs, + seen: Vec::new_in(arena), + target_info: self.target_info, + cache: self, + }; + + // [Layout::from_var] should query the cache! + let Cacheable(value, criteria) = Layout::from_var(&mut env, var); + debug_assert!( + criteria.is_cacheable(), + "{value:?} not cacheable as top-level" + ); + value + } + + pub fn raw_from_var( + &mut self, + arena: &'a Bump, + var: Variable, + subs: &Subs, + ) -> Result, LayoutProblem> { + // Store things according to the root Variable, to avoid duplicate work. + let var = subs.get_root_key_without_compacting(var); + + let mut env = Env { + arena, + subs, + seen: Vec::new_in(arena), + target_info: self.target_info, + cache: self, + }; + + // [Layout::from_var] should query the cache! + let Cacheable(value, criteria) = RawFunctionLayout::from_var(&mut env, var); + debug_assert!( + criteria.is_cacheable(), + "{value:?} not cacheable as top-level" + ); + value + } + + #[inline(always)] + fn get_help( + cache: &[CacheLayer], + subs: &Subs, + var: Variable, + ) -> Option<(Result, CacheMeta)> { + let root = subs.get_root_key_without_compacting(var); + + for layer in cache.iter().rev() { + // TODO: it's possible that after unification, roots in earlier cache layers changed... + // how often does that happen? + if let Some(result) = layer.0.get(&root) { + return Some(result.clone()); + } + } + None + } + + #[inline(always)] + fn insert_help( + cache: &mut [CacheLayer], + subs: &Subs, + var: Variable, + result: Result, + cache_metadata: CacheMeta, + ) { + let root = subs.get_root_key_without_compacting(var); + let layer = cache + .last_mut() + .expect("cache must have at least one layer"); + let opt_old_result = layer.0.insert(root, (result, cache_metadata)); + if let Some(old_result) = opt_old_result { + // Can happen when we need to re-calculate a recursive layout + roc_tracing::debug!( + ?old_result, + new_result=?result, + ?var, + "overwritting layout cache" + ); + } + } + + #[inline(always)] + fn get(&self, subs: &Subs, var: Variable) -> Option<(LayoutResult<'a>, CacheMeta)> { + Self::get_help(&self.cache, subs, var) + } + + #[inline(always)] + fn get_raw_function( + &self, + subs: &Subs, + var: Variable, + ) -> Option<(RawFunctionLayoutResult<'a>, CacheMeta)> { + Self::get_help(&self.raw_function_cache, subs, var) + } + + #[inline(always)] + fn insert( + &mut self, + subs: &Subs, + var: Variable, + result: LayoutResult<'a>, + cache_metadata: CacheMeta, + ) { + Self::insert_help(&mut self.cache, subs, var, result, cache_metadata) + } + + #[inline(always)] + fn insert_raw_function( + &mut self, + subs: &Subs, + var: Variable, + result: RawFunctionLayoutResult<'a>, + cache_metadata: CacheMeta, + ) { + Self::insert_help( + &mut self.raw_function_cache, + subs, + var, + result, + cache_metadata, + ) + } + + #[inline(always)] + pub fn snapshot(&mut self) -> CacheSnapshot { + debug_assert_eq!(self.raw_function_cache.len(), self.cache.len()); + self.cache.push(Default::default()); + self.raw_function_cache.push(Default::default()); + CacheSnapshot { + layer: self.cache.len(), + } + } + + #[inline(always)] + pub fn rollback_to(&mut self, snapshot: CacheSnapshot) { + let CacheSnapshot { layer } = snapshot; + + debug_assert_eq!(self.cache.len(), layer); + debug_assert_eq!(self.raw_function_cache.len(), layer); + + self.cache.pop(); + self.raw_function_cache.pop(); + } + + /// Invalidates the list of given root variables. + /// Usually called after unification, when merged variables with changed contents need to be + /// invalidated. + pub fn invalidate(&mut self, subs: &Subs, vars: impl IntoIterator) { + // TODO(layout-cache): optimize me somehow + for var in vars.into_iter() { + let var = subs.get_root_key_without_compacting(var); + for layer in self.cache.iter_mut().rev() { + layer + .0 + .retain(|k, _| !subs.equivalent_without_compacting(var, *k)); + roc_tracing::debug!(?var, "invalidating cached layout"); + } + for layer in self.raw_function_cache.iter_mut().rev() { + layer + .0 + .retain(|k, _| !subs.equivalent_without_compacting(var, *k)); + roc_tracing::debug!(?var, "invalidating cached layout"); + } + } + } + + pub fn get_in(&self, interned: InLayout<'a>) -> Layout<'a> { + self.interner.get(interned) + } + pub fn get_repr(&self, interned: InLayout<'a>) -> LayoutRepr<'a> { + self.interner.get_repr(interned) + } + + pub fn put_in(&mut self, layout: Layout<'a>) -> InLayout<'a> { + self.interner.insert(layout) + } + pub(crate) fn put_in_direct_no_semantic(&mut self, repr: LayoutRepr<'a>) -> InLayout<'a> { + self.interner.insert_direct_no_semantic(repr) + } + + #[cfg(debug_assertions)] + pub fn statistics(&self) -> (CacheStatistics, CacheStatistics) { + (self.stats, self.raw_function_stats) + } +} + +pub struct CacheSnapshot { + /// Index of the pushed layer + layer: usize, +} + +#[derive(Clone, Debug)] +struct CacheCriteria { + /// Whether there is a naked recursion pointer in this layout, that doesn't pass through a + /// recursive structure. + has_naked_recursion_pointer: bool, + /// Recursive structures this layout contains, if any. + // Typically at most 1 recursive structure is contained, but there may be more. + recursive_structures: SmallVec, +} + +const CACHEABLE: CacheCriteria = CacheCriteria { + has_naked_recursion_pointer: false, + recursive_structures: SmallVec::new(), +}; + +const NAKED_RECURSION_PTR: CacheCriteria = CacheCriteria { + has_naked_recursion_pointer: true, + recursive_structures: SmallVec::new(), +}; + +impl CacheCriteria { + #[inline(always)] + fn is_cacheable(&self) -> bool { + // Can't cache if there a naked recursion pointer that isn't covered by a recursive layout. + !self.has_naked_recursion_pointer + } + + /// Makes `self` cacheable iff self and other are cacheable. + #[inline(always)] + fn and(&mut self, other: Self, subs: &Subs) { + self.has_naked_recursion_pointer = + self.has_naked_recursion_pointer || other.has_naked_recursion_pointer; + + for &other_rec in other.recursive_structures.iter() { + if self + .recursive_structures + .iter() + .any(|rec| subs.equivalent_without_compacting(*rec, other_rec)) + { + continue; + } + self.recursive_structures.push(other_rec); + } + } + + #[inline(always)] + fn pass_through_recursive_union(&mut self, recursion_var: Variable) { + self.has_naked_recursion_pointer = false; + self.recursive_structures.push(recursion_var); + } + + #[inline(always)] + fn cache_metadata(&self) -> CacheMeta { + CacheMeta { + recursive_structures: self.recursive_structures.clone(), + } + } +} + +#[derive(Debug)] +pub(crate) struct Cacheable(T, CacheCriteria); + +impl Cacheable { + #[inline(always)] + fn map(self, f: impl FnOnce(T) -> U) -> Cacheable { + Cacheable(f(self.0), self.1) + } + + #[inline(always)] + fn decompose(self, and_with: &mut CacheCriteria, subs: &Subs) -> T { + let Self(value, criteria) = self; + and_with.and(criteria, subs); + value + } + + #[inline(always)] + pub fn value(self) -> T { + self.0 + } +} + +impl Cacheable> { + #[inline(always)] + fn then(self, f: impl FnOnce(T) -> U) -> Cacheable> { + let Cacheable(result, criteria) = self; + match result { + Ok(t) => Cacheable(Ok(f(t)), criteria), + Err(e) => Cacheable(Err(e), criteria), + } + } +} + +#[inline(always)] +fn cacheable(v: T) -> Cacheable { + Cacheable(v, CACHEABLE) +} + +/// Decomposes a cached layout. +/// If the layout is an error, the problem is immediately returned with the cache policy (this is +/// like `?`). +/// If the layout is not an error, the cache policy is `and`ed with `total_criteria`, and the layout +/// is passed back. +macro_rules! cached { + ($expr:expr, $total_criteria:expr, $subs:expr) => { + match $expr { + Cacheable(Ok(v), criteria) => { + $total_criteria.and(criteria, $subs); + v + } + Cacheable(Err(v), criteria) => return Cacheable(Err(v), criteria), + } + }; +} + +pub type TagIdIntType = u16; +pub const MAX_ENUM_SIZE: usize = std::mem::size_of::() * 8; +const GENERATE_NULLABLE: bool = true; + +#[derive(Debug, Clone, Copy)] +pub enum LayoutProblem { + UnresolvedTypeVar(Variable), + Erroneous, +} + +impl From for RuntimeError { + fn from(lp: LayoutProblem) -> Self { + match lp { + LayoutProblem::UnresolvedTypeVar(_) => RuntimeError::UnresolvedTypeVar, + LayoutProblem::Erroneous => RuntimeError::ErroneousType, + } + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum RawFunctionLayout<'a> { + Function(&'a [InLayout<'a>], LambdaSet<'a>, InLayout<'a>), + ErasedFunction(&'a [InLayout<'a>], InLayout<'a>), + ZeroArgumentThunk(InLayout<'a>), +} + +impl<'a> RawFunctionLayout<'a> { + pub fn is_zero_argument_thunk(&self) -> bool { + matches!(self, RawFunctionLayout::ZeroArgumentThunk(_)) + } + + pub fn is_erased_function(&self) -> bool { + matches!(self, RawFunctionLayout::ErasedFunction(_, _)) + } + + fn new_help<'b>( + env: &mut Env<'a, 'b>, + var: Variable, + content: Content, + ) -> Cacheable> { + use roc_types::subs::Content::*; + match content { + FlexVar(_) | RigidVar(_) => cacheable(Err(LayoutProblem::UnresolvedTypeVar(var))), + FlexAbleVar(_, _) | RigidAbleVar(_, _) => todo_abilities!("Not reachable yet"), + RecursionVar { structure, .. } => { + let structure_content = env.subs.get_content_without_compacting(structure); + Self::new_help(env, structure, *structure_content) + } + LambdaSet(_) => { + internal_error!("lambda set should only appear under a function, where it's handled independently."); + } + ErasedLambda => internal_error!("erased lambda type should only appear under a function, where it's handled independently"), + Structure(flat_type) => Self::layout_from_flat_type(env, flat_type), + RangedNumber(..) => Layout::new_help(env, var, content).then(Self::ZeroArgumentThunk), + + // Ints + Alias(Symbol::NUM_I128, args, _, _) => { + debug_assert!(args.is_empty()); + cacheable(Ok(Self::ZeroArgumentThunk(Layout::I128))) + } + Alias(Symbol::NUM_I64, args, _, _) => { + debug_assert!(args.is_empty()); + cacheable(Ok(Self::ZeroArgumentThunk(Layout::I64))) + } + Alias(Symbol::NUM_I32, args, _, _) => { + debug_assert!(args.is_empty()); + cacheable(Ok(Self::ZeroArgumentThunk(Layout::I32))) + } + Alias(Symbol::NUM_I16, args, _, _) => { + debug_assert!(args.is_empty()); + cacheable(Ok(Self::ZeroArgumentThunk(Layout::I16))) + } + Alias(Symbol::NUM_I8, args, _, _) => { + debug_assert!(args.is_empty()); + cacheable(Ok(Self::ZeroArgumentThunk(Layout::I8))) + } + + // I think unsigned and signed use the same layout + Alias(Symbol::NUM_U128, args, _, _) => { + debug_assert!(args.is_empty()); + cacheable(Ok(Self::ZeroArgumentThunk(Layout::U128))) + } + Alias(Symbol::NUM_U64, args, _, _) => { + debug_assert!(args.is_empty()); + cacheable(Ok(Self::ZeroArgumentThunk(Layout::U64))) + } + Alias(Symbol::NUM_U32, args, _, _) => { + debug_assert!(args.is_empty()); + cacheable(Ok(Self::ZeroArgumentThunk(Layout::U32))) + } + Alias(Symbol::NUM_U16, args, _, _) => { + debug_assert!(args.is_empty()); + cacheable(Ok(Self::ZeroArgumentThunk(Layout::U16))) + } + Alias(Symbol::NUM_U8, args, _, _) => { + debug_assert!(args.is_empty()); + cacheable(Ok(Self::ZeroArgumentThunk(Layout::U8))) + } + + // Floats + Alias(Symbol::NUM_F64, args, _, _) => { + debug_assert!(args.is_empty()); + cacheable(Ok(Self::ZeroArgumentThunk(Layout::F64))) + } + Alias(Symbol::NUM_F32, args, _, _) => { + debug_assert!(args.is_empty()); + cacheable(Ok(Self::ZeroArgumentThunk(Layout::F32))) + } + + // Nat + Alias(Symbol::NUM_NAT, args, _, _) => { + debug_assert!(args.is_empty()); + cacheable(Ok(Self::ZeroArgumentThunk(Layout::usize(env.target_info)))) + } + + Alias(Symbol::INSPECT_ELEM_WALKER | Symbol::INSPECT_KEY_VAL_WALKER, _, var, _) => Self::from_var(env, var), + + Alias(symbol, _, var, _) if symbol.is_builtin() => { + Layout::new_help(env, var, content).then(Self::ZeroArgumentThunk) + } + + Alias(_, _, var, _) => Self::from_var(env, var), + Error => cacheable(Err(LayoutProblem::Erroneous)), + } + } + + fn layout_from_flat_type( + env: &mut Env<'a, '_>, + flat_type: FlatType, + ) -> Cacheable> { + use roc_types::subs::FlatType::*; + + let arena = env.arena; + + match flat_type { + Func(args, closure_var, ret_var) => { + let mut fn_args = Vec::with_capacity_in(args.len(), arena); + + let mut cache_criteria = CACHEABLE; + + for index in args.into_iter() { + let arg_var = env.subs[index]; + let layout = cached!(Layout::from_var(env, arg_var), cache_criteria, env.subs); + fn_args.push(layout); + } + + let ret = cached!(Layout::from_var(env, ret_var), cache_criteria, env.subs); + + let fn_args = fn_args.into_bump_slice(); + + let closure_data = build_function_closure_data(env, args, closure_var, ret_var); + let closure_data = cached!(closure_data, cache_criteria, env.subs); + + let function_layout = match closure_data { + ClosureDataKind::LambdaSet(lambda_set) => { + Self::Function(fn_args, lambda_set, ret) + } + ClosureDataKind::Erased => Self::ErasedFunction(fn_args, ret), + }; + + Cacheable(Ok(function_layout), cache_criteria) + } + TagUnion(tags, ext) if tags.is_newtype_wrapper(env.subs) => { + debug_assert!(ext_var_is_empty_tag_union(env.subs, ext)); + let slice_index = tags.variables().into_iter().next().unwrap(); + let slice = env.subs[slice_index]; + let var_index = slice.into_iter().next().unwrap(); + let var = env.subs[var_index]; + + Self::from_var(env, var) + } + Record(fields, ext) if fields.len() == 1 => { + debug_assert!(ext_var_is_empty_record(env.subs, ext)); + + let var_index = fields.iter_variables().next().unwrap(); + let var = env.subs[var_index]; + + Self::from_var(env, var) + } + _ => { + let mut criteria = CACHEABLE; + let layout = cached!(layout_from_flat_type(env, flat_type), criteria, env.subs); + Cacheable(Ok(Self::ZeroArgumentThunk(layout)), criteria) + } + } + } + + /// Returns Err(()) if given an error, or Ok(Layout) if given a non-erroneous Structure. + /// Panics if given a FlexVar or RigidVar, since those should have been + /// monomorphized away already! + pub(crate) fn from_var( + env: &mut Env<'a, '_>, + var: Variable, + ) -> Cacheable> { + env.cached_raw_function_or(var, |env| { + if env.is_seen(var) { + unreachable!("The initial variable of a signature cannot be seen already") + } else { + let content = env.subs.get_content_without_compacting(var); + Self::new_help(env, var, *content) + } + }) + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct Layout<'a> { + repr: LayoutWrapper<'a>, + semantic: SemanticRepr<'a>, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub(crate) enum LayoutWrapper<'a> { + Direct(LayoutRepr<'a>), + Newtype(InLayout<'a>), +} + +/// Types for code gen must be monomorphic. No type variables allowed! +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub enum LayoutRepr<'a> { + Builtin(Builtin<'a>), + Struct(&'a [InLayout<'a>]), + // A pointer (heap or stack) without any reference counting + // Ptr is not user-facing. The compiler author must make sure that invariants are upheld + Ptr(InLayout<'a>), + Union(UnionLayout<'a>), + LambdaSet(LambdaSet<'a>), + RecursivePointer(InLayout<'a>), + /// Only used in erased functions. + FunctionPointer(FunctionPointer<'a>), + /// The layout of an erasure. + Erased(Erased), +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct FunctionPointer<'a> { + pub args: &'a [InLayout<'a>], + pub ret: InLayout<'a>, +} + +impl<'a> FunctionPointer<'a> { + pub fn to_doc<'b, D, A, I>( + self, + alloc: &'b D, + interner: &I, + seen_rec: &mut SeenRecPtrs<'a>, + parens: Parens, + ) -> DocBuilder<'b, D, A> + where + D: DocAllocator<'b, A>, + D::Doc: Clone, + A: Clone, + I: LayoutInterner<'a>, + { + let Self { args, ret } = self; + + let args = args + .iter() + .map(|arg| interner.to_doc(*arg, alloc, seen_rec, parens)); + let args = alloc.intersperse(args, alloc.text(", ")); + let ret = interner.to_doc(ret, alloc, seen_rec, parens); + + alloc + .text("FunPtr((") + .append(args) + .append(alloc.text(") -> ")) + .append(ret) + .append(")") + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub enum UnionLayout<'a> { + /// A non-recursive tag union + /// e.g. `Result a e : [Ok a, Err e]` + NonRecursive(&'a [&'a [InLayout<'a>]]), + /// A recursive tag union (general case) + /// e.g. `Expr : [Sym Str, Add Expr Expr]` + Recursive(&'a [&'a [InLayout<'a>]]), + /// A recursive tag union with just one constructor + /// Optimization: No need to store a tag ID (the payload is "unwrapped") + /// e.g. `RoseTree a : [Tree a (List (RoseTree a))]` + NonNullableUnwrapped(&'a [InLayout<'a>]), + /// A recursive tag union that has an empty variant + /// Optimization: Represent the empty variant as null pointer => no memory usage & fast comparison + /// It has more than one other variant, so they need tag IDs (payloads are "wrapped") + /// e.g. `FingerTree a : [Empty, Single a, More (Some a) (FingerTree (Tuple a)) (Some a)]` + /// see also: https://youtu.be/ip92VMpf_-A?t=164 + /// + /// nullable_id refers to the index of the tag that is represented at runtime as NULL. + /// For example, in `FingerTree a : [Empty, Single a, More (Some a) (FingerTree (Tuple a)) (Some a)]`, + /// the ids would be Empty = 0, More = 1, Single = 2, because that's how those tags are + /// ordered alphabetically. Since the Empty tag will be represented at runtime as NULL, + /// and since Empty's tag id is 0, here nullable_id would be 0. + NullableWrapped { + nullable_id: u16, + other_tags: &'a [&'a [InLayout<'a>]], + }, + /// A recursive tag union with only two variants, where one is empty. + /// Optimizations: Use null for the empty variant AND don't store a tag ID for the other variant. + /// e.g. `ConsList a : [Nil, Cons a (ConsList a)]` + /// + /// nullable_id is a bool because it's only ever 0 or 1, but (as with the NullableWrapped + /// variant), it reprsents the index of the tag that will be represented at runtime as NULL. + /// + /// So for example, in `ConsList a : [Nil, Cons a (ConsList a)]`, Nil is tag id 1 and + /// Cons is tag id 0 because Nil comes alphabetically after Cons. Here, Nil will be + /// represented as NULL at runtime, so nullable_id is 1 - which is to say, `true`, because + /// `(1 as bool)` is `true`. + NullableUnwrapped { + nullable_id: bool, + other_fields: &'a [InLayout<'a>], + }, +} + +impl<'a> UnionLayout<'a> { + pub fn to_doc<'b, D, A, I>( + self, + alloc: &'b D, + interner: &I, + seen_rec: &mut SeenRecPtrs<'a>, + _parens: Parens, + ) -> DocBuilder<'b, D, A> + where + D: DocAllocator<'b, A>, + D::Doc: Clone, + A: Clone, + I: LayoutInterner<'a>, + { + use UnionLayout::*; + + match self { + NonRecursive(tags) => { + let tags_doc = tags.iter().map(|fields| { + alloc.text("C ").append( + alloc.intersperse( + fields + .iter() + .map(|x| interner.to_doc(*x, alloc, seen_rec, Parens::InTypeParam)), + " ", + ), + ) + }); + + alloc + .text("[") + .append(alloc.intersperse(tags_doc, ", ")) + .append(alloc.text("]")) + } + Recursive(tags) => { + let tags_doc = tags.iter().map(|fields| { + alloc.text("C ").append( + alloc.intersperse( + fields + .iter() + .map(|x| interner.to_doc(*x, alloc, seen_rec, Parens::InTypeParam)), + " ", + ), + ) + }); + alloc + .text("[") + .append(alloc.intersperse(tags_doc, ", ")) + .append(alloc.text("]")) + } + NonNullableUnwrapped(fields) => { + let fields_doc = alloc.text("C ").append( + alloc.intersperse( + fields + .iter() + .map(|x| interner.to_doc(*x, alloc, seen_rec, Parens::InTypeParam)), + " ", + ), + ); + alloc + .text("[") + .append(fields_doc) + .append(alloc.text("]")) + } + NullableUnwrapped { + nullable_id, + other_fields, + } => { + let fields_doc = alloc.text("C ").append( + alloc.intersperse( + other_fields + .iter() + .map(|x| interner.to_doc(*x, alloc, seen_rec, Parens::InTypeParam)), + " ", + ), + ); + let tags_doc = if nullable_id { + alloc.concat(vec![alloc.text(", "), fields_doc]) + } else { + alloc.concat(vec![fields_doc, alloc.text(", ")]) + }; + alloc + .text("[") + .append(tags_doc) + .append(alloc.text("]")) + } + NullableWrapped { + nullable_id, + other_tags, + } => { + let nullable_id = nullable_id as usize; + let tags_docs = + (0..(other_tags.len() + 1)).map(|i| { + if i == nullable_id { + alloc.text("") + } else { + let idx = if i > nullable_id { i - 1 } else { i }; + alloc.text("C ").append(alloc.intersperse( + other_tags[idx].iter().map(|x| { + interner.to_doc(*x, alloc, seen_rec, Parens::InTypeParam) + }), + " ", + )) + } + }); + let tags_docs = alloc.intersperse(tags_docs, alloc.text(", ")); + alloc + .text("[") + .append(tags_docs) + .append(alloc.text("]")) + } + } + } + + pub fn layout_at(self, interner: &mut I, tag_id: TagIdIntType, index: usize) -> InLayout<'a> + where + I: LayoutInterner<'a>, + { + let result = match self { + UnionLayout::NonRecursive(tag_layouts) => { + let field_layouts = tag_layouts[tag_id as usize]; + + // this cannot be recursive; return immediately + return field_layouts[index]; + } + UnionLayout::Recursive(tag_layouts) => { + let field_layouts = tag_layouts[tag_id as usize]; + + field_layouts[index] + } + UnionLayout::NonNullableUnwrapped(field_layouts) => field_layouts[index], + UnionLayout::NullableWrapped { + nullable_id, + other_tags, + } => { + debug_assert_ne!(nullable_id, tag_id); + + let tag_index = if tag_id < nullable_id { + tag_id + } else { + tag_id - 1 + }; + + let field_layouts = other_tags[tag_index as usize]; + field_layouts[index] + } + + UnionLayout::NullableUnwrapped { + nullable_id, + other_fields, + } => { + debug_assert_ne!(nullable_id, tag_id != 0); + + other_fields[index] + } + }; + + // TODO(recursive-layouts): simplify after we have disjoint recursive pointers + if let LayoutRepr::RecursivePointer(_) = interner.get_repr(result) { + interner.insert_direct_no_semantic(LayoutRepr::Union(self)) + } else { + result + } + } + + pub fn number_of_tags(&'a self) -> usize { + match self { + UnionLayout::NonRecursive(tags) | UnionLayout::Recursive(tags) => tags.len(), + + UnionLayout::NullableWrapped { other_tags, .. } => other_tags.len() + 1, + UnionLayout::NonNullableUnwrapped(_) => 1, + UnionLayout::NullableUnwrapped { .. } => 2, + } + } + + pub fn discriminant(&self) -> Discriminant { + match self { + UnionLayout::NonRecursive(tags) => Discriminant::from_number_of_tags(tags.len()), + UnionLayout::Recursive(tags) => Discriminant::from_number_of_tags(tags.len()), + + UnionLayout::NullableWrapped { other_tags, .. } => { + Discriminant::from_number_of_tags(other_tags.len() + 1) + } + UnionLayout::NonNullableUnwrapped(_) => Discriminant::from_number_of_tags(2), + UnionLayout::NullableUnwrapped { .. } => Discriminant::from_number_of_tags(1), + } + } + + pub fn tag_id_layout(&self) -> InLayout<'static> { + self.discriminant().layout() + } + + fn stores_tag_id_in_pointer_bits(tags: &[&[InLayout<'a>]], target_info: TargetInfo) -> bool { + tags.len() < target_info.ptr_width() as usize + } + + pub const POINTER_MASK_32BIT: usize = 0b0000_0111; + pub const POINTER_MASK_64BIT: usize = 0b0000_0011; + + pub fn tag_id_pointer_bits_and_mask(target_info: TargetInfo) -> (usize, usize) { + match target_info.ptr_width() { + PtrWidth::Bytes8 => (3, Self::POINTER_MASK_64BIT), + PtrWidth::Bytes4 => (2, Self::POINTER_MASK_32BIT), + } + } + + // i.e. it is not implicit and not stored in the pointer bits + pub fn stores_tag_id_as_data(&self, target_info: TargetInfo) -> bool { + match self { + UnionLayout::NonRecursive(_) => true, + UnionLayout::Recursive(tags) + | UnionLayout::NullableWrapped { + other_tags: tags, .. + } => !Self::stores_tag_id_in_pointer_bits(tags, target_info), + UnionLayout::NonNullableUnwrapped(_) | UnionLayout::NullableUnwrapped { .. } => false, + } + } + + pub fn stores_tag_id_in_pointer(&self, target_info: TargetInfo) -> bool { + match self { + UnionLayout::NonRecursive(_) => false, + UnionLayout::Recursive(tags) + | UnionLayout::NullableWrapped { + other_tags: tags, .. + } => Self::stores_tag_id_in_pointer_bits(tags, target_info), + UnionLayout::NonNullableUnwrapped(_) | UnionLayout::NullableUnwrapped { .. } => false, + } + } + + pub fn tag_is_null(&self, tag_id: TagIdIntType) -> bool { + match self { + UnionLayout::NonRecursive(_) + | UnionLayout::NonNullableUnwrapped(_) + | UnionLayout::Recursive(_) => false, + UnionLayout::NullableWrapped { nullable_id, .. } => *nullable_id == tag_id, + UnionLayout::NullableUnwrapped { nullable_id, .. } => *nullable_id == (tag_id != 0), + } + } + + pub fn is_nullable(&self) -> bool { + match self { + UnionLayout::NonRecursive(_) + | UnionLayout::Recursive(_) + | UnionLayout::NonNullableUnwrapped { .. } => false, + UnionLayout::NullableWrapped { .. } | UnionLayout::NullableUnwrapped { .. } => true, + } + } + + fn tags_alignment_bytes(interner: &I, tags: &[&'a [InLayout<'a>]]) -> u32 + where + I: LayoutInterner<'a>, + { + tags.iter() + .map(|field_layouts| LayoutRepr::struct_(field_layouts).alignment_bytes(interner)) + .max() + .unwrap_or(0) + } + + pub fn allocation_alignment_bytes(&self, interner: &I) -> u32 + where + I: LayoutInterner<'a>, + { + let allocation = match self { + UnionLayout::NonRecursive(tags) => Self::tags_alignment_bytes(interner, tags), + UnionLayout::Recursive(tags) => Self::tags_alignment_bytes(interner, tags), + UnionLayout::NonNullableUnwrapped(field_layouts) => { + LayoutRepr::struct_(field_layouts).alignment_bytes(interner) + } + UnionLayout::NullableWrapped { other_tags, .. } => { + Self::tags_alignment_bytes(interner, other_tags) + } + UnionLayout::NullableUnwrapped { other_fields, .. } => { + LayoutRepr::struct_(other_fields).alignment_bytes(interner) + } + }; + + // because we store a refcount, the alignment must be at least the size of a pointer + allocation.max(interner.target_info().ptr_width() as u32) + } + + /// Size of the data in memory, whether it's stack or heap (for non-null tag ids) + pub fn data_size_and_alignment(&self, interner: &I) -> (u32, u32) + where + I: LayoutInterner<'a>, + { + let (data_width, data_align) = self.data_size_and_alignment_help_match(interner); + + if self.stores_tag_id_as_data(interner.target_info()) { + use Discriminant::*; + match self.discriminant() { + U0 => (round_up_to_alignment(data_width, data_align), data_align), + U1 | U8 => ( + round_up_to_alignment(data_width + 1, data_align), + data_align, + ), + U16 => { + // first, round up the data so the tag id is well-aligned; + // then add the tag id width, and make sure the whole extends + // to the next alignment multiple + let tag_align = data_align.max(2); + let tag_width = + round_up_to_alignment(round_up_to_alignment(data_width, 2) + 2, tag_align); + + (tag_width, tag_align) + } + } + } else { + (data_width, data_align) + } + } + + /// Size of the data before the tag_id, if it exists. + /// Returns None if the tag_id is not stored as data in the layout. + pub fn data_size_without_tag_id(&self, interner: &I) -> Option + where + I: LayoutInterner<'a>, + { + if !self.stores_tag_id_as_data(interner.target_info()) { + return None; + }; + + Some(self.data_size_and_alignment_help_match(interner).0) + } + + fn data_size_and_alignment_help_match(&self, interner: &I) -> (u32, u32) + where + I: LayoutInterner<'a>, + { + match self { + Self::NonRecursive(tags) => Layout::stack_size_and_alignment_slices(interner, tags), + Self::Recursive(tags) => Layout::stack_size_and_alignment_slices(interner, tags), + Self::NonNullableUnwrapped(fields) => { + Layout::stack_size_and_alignment_slices(interner, &[fields]) + } + Self::NullableWrapped { other_tags, .. } => { + Layout::stack_size_and_alignment_slices(interner, other_tags) + } + Self::NullableUnwrapped { other_fields, .. } => { + Layout::stack_size_and_alignment_slices(interner, &[other_fields]) + } + } + } + + pub fn tag_id_offset(&self, interner: &I) -> Option + where + I: LayoutInterner<'a>, + { + match self { + UnionLayout::NonRecursive(tags) + | UnionLayout::Recursive(tags) + | UnionLayout::NullableWrapped { + other_tags: tags, .. + } => Some(Self::tag_id_offset_help(interner, tags)), + UnionLayout::NonNullableUnwrapped(_) | UnionLayout::NullableUnwrapped { .. } => None, + } + } + + fn tag_id_offset_help(interner: &I, layouts: &[&[InLayout<'a>]]) -> u32 + where + I: LayoutInterner<'a>, + { + let (data_width, data_align) = Layout::stack_size_and_alignment_slices(interner, layouts); + + round_up_to_alignment(data_width, data_align) + } + + /// Very important to use this when doing a memcpy! + fn stack_size_without_alignment(&self, interner: &I) -> u32 + where + I: LayoutInterner<'a>, + { + match self { + UnionLayout::NonRecursive(_) => { + let (width, align) = self.data_size_and_alignment(interner); + round_up_to_alignment(width, align) + } + UnionLayout::Recursive(_) + | UnionLayout::NonNullableUnwrapped(_) + | UnionLayout::NullableWrapped { .. } + | UnionLayout::NullableUnwrapped { .. } => interner.target_info().ptr_width() as u32, + } + } + + pub fn is_recursive(&self) -> bool { + use UnionLayout::*; + + match self { + NonRecursive(_) => false, + Recursive(_) + | NonNullableUnwrapped(_) + | NullableWrapped { .. } + | NullableUnwrapped { .. } => true, + } + } +} + +pub enum Discriminant { + U0, + U1, + U8, + U16, +} + +impl Discriminant { + pub const fn from_number_of_tags(tags: usize) -> Self { + match tags { + 0 => Discriminant::U0, + 1 => Discriminant::U0, + 2 => Discriminant::U1, + 3..=255 => Discriminant::U8, + 256..=65_535 => Discriminant::U16, + _ => panic!("discriminant too large"), + } + } + + pub const fn stack_size(&self) -> u32 { + match self { + Discriminant::U0 => 0, + Discriminant::U1 => 1, + Discriminant::U8 => 1, + Discriminant::U16 => 2, + } + } + + pub const fn alignment_bytes(&self) -> u32 { + self.stack_size() + } + + pub const fn layout(&self) -> InLayout<'static> { + // TODO is it beneficial to return a more specific layout? + // e.g. Layout::bool() and Layout::VOID + match self { + Discriminant::U0 => Layout::U8, + Discriminant::U1 => Layout::U8, + Discriminant::U8 => Layout::U8, + Discriminant::U16 => Layout::U16, + } + } +} + +/// Custom type so we can get the numeric representation of a symbol in tests (so `#UserApp.3` +/// instead of `UserApp.foo`). The pretty name is not reliable when running many tests +/// concurrently. The number does not change and will give a reliable output. +struct SetElement<'a> { + symbol: Symbol, + layout: &'a [InLayout<'a>], +} + +impl std::fmt::Debug for SetElement<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let symbol_string = crate::ir::symbol_to_doc_string(self.symbol, false); + + write!(f, "( {}, {:?})", symbol_string, self.layout) + } +} + +impl std::fmt::Debug for LambdaSet<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + struct Helper<'a> { + set: &'a [(Symbol, &'a [InLayout<'a>])], + } + + impl std::fmt::Debug for Helper<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let entries = self.set.iter().map(|x| SetElement { + symbol: x.0, + layout: x.1, + }); + + f.debug_list().entries(entries).finish() + } + } + + f.debug_struct("LambdaSet") + .field("set", &Helper { set: self.set }) + .field("args", &self.args) + .field("ret", &self.ret) + .field("representation", &self.representation) + .field("full_layout", &self.full_layout) + .finish() + } +} + +/// See [Niche]. +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +enum NichePriv<'a> { + /// Distinguishes captures this proc takes, when it is a part of a lambda set that has multiple + /// lambdas of the same name, but different captures. + Captures(&'a [InLayout<'a>]), +} + +/// Niches identify lambdas (including thunks) in ways that are not distinguishable solely by their +/// [runtime function layout][RawFunctionLayout]. +/// +/// Currently, there are two kinds of niches. +/// +/// # Captures niches +/// +/// Captures niches identify a procedure's set of captured symbols. This is relevant when a +/// procedure is part of a lambda set that has multiple lambdas of the procedure's name, but each +/// has a different set of captures. +/// +/// The capture set is identified only in the body of a procedure and not in its runtime layout. +/// Any capturing lambda takes the whole lambda set as an argument, rather than just its captures. +/// A captures niche can be attached to a [lambda name][LambdaName] to uniquely identify lambdas +/// in these scenarios. +/// +/// Procedure names with captures niches are typically produced by [find_lambda_name][LambdaSet::find_lambda_name]. +/// Captures niches are irrelevant for thunks. +/// +/// ## Example +/// +/// `fun` has lambda set `[[forcer U64, forcer U8]]` in the following program: +/// +/// ```roc +/// capture : _ -> ({} -> Str) +/// capture = \val -> +/// forcer = \{} -> Num.toStr val +/// forcer +/// +/// fun = \x -> +/// when x is +/// True -> capture 123u64 +/// False -> capture 18u8 +/// ``` +/// +/// By recording the captures layouts each `forcer` expects, we can distinguish +/// between such differences when constructing the closure capture data that is +/// return value of `fun`. +/// +/// See also https://github.com/roc-lang/roc/issues/3336. +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +#[repr(transparent)] +pub struct Niche<'a>(NichePriv<'a>); + +impl<'a> Niche<'a> { + pub const NONE: Niche<'a> = Niche(NichePriv::Captures(&[])); + + pub fn to_doc<'b, D, A, I>( + self, + alloc: &'b D, + interner: &I, + seen_rec: &mut SeenRecPtrs<'a>, + ) -> DocBuilder<'b, D, A> + where + D: DocAllocator<'b, A>, + D::Doc: Clone, + A: Clone, + I: LayoutInterner<'a>, + { + match self.0 { + NichePriv::Captures(captures) => alloc.concat([ + alloc.reflow("(niche {"), + alloc.intersperse( + captures + .iter() + .map(|c| interner.to_doc(*c, alloc, seen_rec, Parens::NotNeeded)), + alloc.reflow(", "), + ), + alloc.reflow("})"), + ]), + } + } + + pub fn dbg_deep<'r, I: LayoutInterner<'a>>( + &'r self, + interner: &'r I, + ) -> crate::layout::intern::dbg_deep::DbgFields<'a, 'r, I> { + let NichePriv::Captures(caps) = &self.0; + interner.dbg_deep_iter(caps) + } + + pub fn dbg_stable<'r, I: LayoutInterner<'a>>( + &'r self, + interner: &'r I, + ) -> crate::layout::intern::dbg_stable::DbgFields<'a, 'r, I> { + let NichePriv::Captures(caps) = &self.0; + interner.dbg_stable_iter(caps) + } +} + +#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] +pub struct LambdaName<'a> { + name: Symbol, + niche: Niche<'a>, +} + +impl<'a> LambdaName<'a> { + pub(crate) fn from_captures(symbol: Symbol, captures: &'a [InLayout<'a>]) -> Self { + Self { + name: symbol, + niche: Niche(NichePriv::Captures(captures)), + } + } + + #[inline(always)] + pub fn name(&self) -> Symbol { + self.name + } + + #[inline(always)] + pub fn niche(&self) -> Niche<'a> { + self.niche + } + + #[inline(always)] + pub(crate) fn no_captures(&self) -> bool { + match self.niche.0 { + NichePriv::Captures(captures) => captures.is_empty(), + } + } + + #[inline(always)] + pub fn no_niche(name: Symbol) -> Self { + Self { + name, + niche: Niche::NONE, + } + } + + #[inline(always)] + pub(crate) fn replace_name(&self, name: Symbol) -> Self { + Self { name, ..*self } + } +} + +/// Closure data for a function +#[derive(Debug, Clone, Copy)] +pub(crate) enum ClosureDataKind<'a> { + /// The function is compiled with lambda sets. + LambdaSet(LambdaSet<'a>), + /// The function is compiled as type-erased. + Erased, +} + +impl<'a> ClosureDataKind<'a> { + pub fn data_layout(&self) -> InLayout<'a> { + match self { + Self::LambdaSet(lambda_set) => lambda_set.full_layout, + Self::Erased => Layout::ERASED, + } + } +} + +fn build_function_closure_data<'a>( + env: &mut Env<'a, '_>, + args: VariableSubsSlice, + closure_var: Variable, + ret_var: Variable, +) -> Cacheable, LayoutProblem>> { + match env.subs.get_content_without_compacting(closure_var) { + Content::ErasedLambda => cacheable(Ok(ClosureDataKind::Erased)), + _ => LambdaSet::from_var(env, args, closure_var, ret_var) + .map(|result| result.map(ClosureDataKind::LambdaSet)), + } +} + +#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct LambdaSet<'a> { + pub(crate) args: &'a &'a [InLayout<'a>], + pub(crate) ret: InLayout<'a>, + /// collection of function names and their closure arguments + // Double reference to cut from fat slice (16 bytes) to 8 bytes + pub(crate) set: &'a &'a [(Symbol, &'a [InLayout<'a>])], + /// how the closure will be represented at runtime + pub(crate) representation: InLayout<'a>, + /// The interned [Layout] representation of the lambda set, as `Layout::LambdaSet(self)`. + pub(crate) full_layout: InLayout<'a>, +} + +#[derive(Debug)] +pub enum EnumDispatch { + Bool, + U8, +} + +/// representation of the closure *for a particular function* +#[derive(Debug)] +pub enum ClosureRepresentation<'a> { + /// The closure is represented as a union. Includes the tag ID! + /// Each variant is a different function, and its payloads are the captures. + Union { + alphabetic_order_fields: &'a [InLayout<'a>], + closure_name: Symbol, + tag_id: TagIdIntType, + union_layout: UnionLayout<'a>, + }, + /// The closure is one function, whose captures are represented as a struct. + /// The layouts are sorted alphabetically by the identifier that is captured. + /// + /// We MUST sort these according to their stack size before code gen! + AlphabeticOrderStruct(&'a [InLayout<'a>]), + /// The closure is one function that captures a single identifier, whose value is unwrapped. + UnwrappedCapture(InLayout<'a>), + /// The closure dispatches to multiple functions, but none of them capture anything, so this is + /// a boolean or integer flag. + EnumDispatch(EnumDispatch), +} + +/// How the closure should be seen when determining a call-by-name. +#[derive(Debug)] +pub enum ClosureCallOptions<'a> { + /// This is an empty lambda set, dispatching is an error + Void, + /// One of a few capturing functions can be called to + Union(UnionLayout<'a>), + /// The closure is one function, whose captures are represented as a struct. + Struct(&'a [InLayout<'a>]), + /// The closure is one function that captures a single identifier, whose value is unwrapped. + UnwrappedCapture(InLayout<'a>), + /// The closure dispatches to multiple possible functions, none of which capture. + EnumDispatch(EnumDispatch), +} + +impl<'a> LambdaSet<'a> { + pub fn runtime_representation(&self) -> InLayout<'a> { + self.representation + } + + /// Does the lambda set contain the given symbol? + pub fn contains(&self, symbol: Symbol) -> bool { + self.set.iter().any(|(s, _)| *s == symbol) + } + + pub fn is_represented(&self, interner: &I) -> Option> + where + I: LayoutInterner<'a>, + { + if self.has_unwrapped_capture_repr() { + let repr = self.representation; + Some(repr) + } else if self.has_enum_dispatch_repr() { + None + } else { + let repr = self.representation; + match interner.get_repr(repr) { + LayoutRepr::Struct(&[]) => None, + _ => Some(repr), + } + } + } + + pub fn iter_set(&self) -> impl ExactSizeIterator> { + self.set.iter().map(|(name, captures_layouts)| { + let niche = match captures_layouts { + [] => Niche::NONE, + _ => Niche(NichePriv::Captures(captures_layouts)), + }; + LambdaName { name: *name, niche } + }) + } + + #[inline(always)] + pub fn len(&self) -> usize { + self.set.len() + } + + #[inline(always)] + pub fn is_empty(&self) -> bool { + self.set.is_empty() + } + + pub fn layout_for_member_with_lambda_name( + &self, + interner: &I, + lambda_name: LambdaName, + ) -> ClosureRepresentation<'a> + where + I: LayoutInterner<'a>, + { + debug_assert!(self.contains(lambda_name.name)); + + let NichePriv::Captures(captures) = lambda_name.niche.0; + + let comparator = |other_name: Symbol, other_captures_layouts: &[InLayout]| { + other_name == lambda_name.name + // Make sure all captures are equal + && other_captures_layouts + .iter() + .eq(captures) + }; + + self.layout_for_member(interner, comparator) + } + + /// Finds an alias name for a possible-multimorphic lambda variant in the lambda set. + pub fn find_lambda_name( + &self, + interner: &I, + function_symbol: Symbol, + captures_layouts: &[InLayout<'a>], + ) -> LambdaName<'a> + where + I: LayoutInterner<'a>, + { + debug_assert!( + self.contains(function_symbol), + "function symbol {function_symbol:?} not in set {self:?}" + ); + + let comparator = |other_name: Symbol, other_captures_layouts: &[InLayout<'a>]| { + other_name == function_symbol + && other_captures_layouts.iter().zip(captures_layouts).all( + |(other_layout, layout)| { + self.capture_layouts_eq(interner, other_layout, layout) + }, + ) + }; + + let (name, layouts) = self + .set + .iter() + .find(|(name, layouts)| comparator(*name, layouts)) + .unwrap_or_else(|| { + internal_error!( + "no lambda set found for ({:?}, {:#?}): {:#?}", + function_symbol, + captures_layouts, + self + ) + }); + + LambdaName { + name: *name, + niche: Niche(NichePriv::Captures(layouts)), + } + } + + /// Checks if two captured layouts are equivalent under the current lambda set. + /// Resolves recursive pointers to the layout of the lambda set. + fn capture_layouts_eq(&self, interner: &I, left: &InLayout<'a>, right: &InLayout<'a>) -> bool + where + I: LayoutInterner<'a>, + { + interner.equiv(*left, *right) + } + + fn layout_for_member(&self, interner: &I, comparator: F) -> ClosureRepresentation<'a> + where + I: LayoutInterner<'a>, + F: Fn(Symbol, &[InLayout]) -> bool, + { + if self.has_unwrapped_capture_repr() { + // Only one function, that captures one identifier. + return ClosureRepresentation::UnwrappedCapture(self.representation); + } + + let repr_layout = interner.chase_recursive(self.representation); + + match repr_layout { + LayoutRepr::Union(union) => { + // here we rely on the fact that a union in a closure would be stored in a one-element record. + // a closure representation that is itself union must be a of the shape `Closure1 ... | Closure2 ...` + match union { + UnionLayout::NonRecursive(_) => { + // get the fields from the set, where they are sorted in alphabetic order + // (and not yet sorted by their alignment) + let (index, (name, fields)) = self + .set + .iter() + .enumerate() + .find(|(_, (s, layouts))| comparator(*s, layouts)) + .unwrap(); + + let closure_name = *name; + + ClosureRepresentation::Union { + tag_id: index as TagIdIntType, + alphabetic_order_fields: fields, + closure_name, + union_layout: union, + } + } + UnionLayout::Recursive(_) => { + let (index, (name, fields)) = self + .set + .iter() + .enumerate() + .find(|(_, (s, layouts))| comparator(*s, layouts)) + .unwrap(); + + let closure_name = *name; + + ClosureRepresentation::Union { + tag_id: index as TagIdIntType, + alphabetic_order_fields: fields, + closure_name, + union_layout: union, + } + } + UnionLayout::NullableUnwrapped { + nullable_id: _, + other_fields: _, + } => { + let (index, (name, fields)) = self + .set + .iter() + .enumerate() + .find(|(_, (s, layouts))| comparator(*s, layouts)) + .unwrap(); + + let closure_name = *name; + + ClosureRepresentation::Union { + tag_id: index as TagIdIntType, + alphabetic_order_fields: fields, + closure_name, + union_layout: union, + } + } + UnionLayout::NullableWrapped { + nullable_id: _, + other_tags: _, + } => { + let (index, (name, fields)) = self + .set + .iter() + .enumerate() + .find(|(_, (s, layouts))| comparator(*s, layouts)) + .unwrap(); + + let closure_name = *name; + + ClosureRepresentation::Union { + tag_id: index as TagIdIntType, + alphabetic_order_fields: fields, + closure_name, + union_layout: union, + } + } + UnionLayout::NonNullableUnwrapped(_) => internal_error!("I thought a non-nullable-unwrapped variant for a lambda set was impossible: how could such a lambda set be created without a base case?"), + } + } + LayoutRepr::Struct { .. } => { + debug_assert_eq!(self.set.len(), 1); + + // get the fields from the set, where they are sorted in alphabetic order + // (and not yet sorted by their alignment) + let (_, fields) = self + .set + .iter() + .find(|(s, layouts)| comparator(*s, layouts)) + .unwrap(); + + ClosureRepresentation::AlphabeticOrderStruct(fields) + } + layout => { + debug_assert!(self.has_enum_dispatch_repr()); + let enum_repr = match layout { + LayoutRepr::Builtin(Builtin::Bool) => EnumDispatch::Bool, + LayoutRepr::Builtin(Builtin::Int(IntWidth::U8)) => EnumDispatch::U8, + other => internal_error!("Invalid layout for enum dispatch: {:?}", other), + }; + ClosureRepresentation::EnumDispatch(enum_repr) + } + } + } + + fn has_unwrapped_capture_repr(&self) -> bool { + self.set.len() == 1 && self.set[0].1.len() == 1 + } + + fn has_enum_dispatch_repr(&self) -> bool { + self.set.len() > 1 && self.set.iter().all(|(_, captures)| captures.is_empty()) + } + + pub fn call_by_name_options(&self, interner: &I) -> ClosureCallOptions<'a> + where + I: LayoutInterner<'a>, + { + if self.has_unwrapped_capture_repr() { + return ClosureCallOptions::UnwrappedCapture(self.representation); + } + + let repr_layout = interner.chase_recursive(self.representation); + + match repr_layout { + LayoutRepr::Union(union_layout) => { + if repr_layout == Layout::VOID_NAKED.repr(interner) { + debug_assert!(self.set.is_empty()); + return ClosureCallOptions::Void; + } + ClosureCallOptions::Union(union_layout) + } + LayoutRepr::Struct(field_layouts) => { + debug_assert_eq!(self.set.len(), 1); + ClosureCallOptions::Struct(field_layouts) + } + layout => { + debug_assert!(self.has_enum_dispatch_repr()); + let enum_repr = match layout { + LayoutRepr::Builtin(Builtin::Bool) => EnumDispatch::Bool, + LayoutRepr::Builtin(Builtin::Int(IntWidth::U8)) => EnumDispatch::U8, + other => internal_error!("Invalid layout for enum dispatch: {:?}", other), + }; + ClosureCallOptions::EnumDispatch(enum_repr) + } + } + } + + /// If `lambda_name` captures, extend the arguments to the lambda with the lambda set, from + /// which the lambda should extract its captures from. + /// + /// If `lambda_name` doesn't capture, the arguments are unaffected. + pub(crate) fn extend_argument_list_for_named( + &self, + arena: &'a Bump, + lambda_name: LambdaName<'a>, + argument_layouts: &'a [InLayout<'a>], + ) -> &'a [InLayout<'a>] { + let Niche(NichePriv::Captures(captures)) = lambda_name.niche; + // TODO(https://github.com/roc-lang/roc/issues/4831): we should turn on this debug-assert; + // however, currently it causes false-positives, because host-exposed functions that are + // function pointers to platform-exposed functions are compiled as if they are proper + // functions, despite not appearing in the lambda set. + // We don't want to compile them as thunks, so we need to figure out a special-casing for + // them. + // To reproduce: test cli_run + // + // debug_assert!( + // self.set + // .contains(&(lambda_name.name, lambda_name.captures_niche.0)), + // "{:?}", + // (self, lambda_name) + // ); + + // If we don't capture, there is nothing to extend. + if captures.is_empty() { + argument_layouts + } else { + let mut arguments = Vec::with_capacity_in(argument_layouts.len() + 1, arena); + arguments.extend(argument_layouts); + arguments.push(self.full_layout); + + arguments.into_bump_slice() + } + } + + pub fn from_var_pub( + cache: &mut LayoutCache<'a>, + arena: &'a Bump, + subs: &Subs, + args: VariableSubsSlice, + closure_var: Variable, + ret_var: Variable, + target_info: TargetInfo, + ) -> Result { + let mut env = Env::from_components(cache, subs, arena, target_info); + Self::from_var(&mut env, args, closure_var, ret_var).value() + } + + fn from_var( + env: &mut Env<'a, '_>, + args: VariableSubsSlice, + closure_var: Variable, + ret_var: Variable, + ) -> Cacheable> { + let Cacheable(result, criteria) = env.cached_or(closure_var, |env| { + let Cacheable(result, criteria) = Self::from_var_help(env, args, closure_var, ret_var); + let result = result.map(|l| l.full_layout); + Cacheable(result, criteria) + }); + + match result.map(|l| env.cache.interner.chase_recursive(l)) { + Ok(LayoutRepr::LambdaSet(lambda_set)) => Cacheable(Ok(lambda_set), criteria), + Err(err) => Cacheable(Err(err), criteria), + Ok(layout) => internal_error!("other layout found for lambda set: {:?}", layout), + } + } + + fn from_var_help( + env: &mut Env<'a, '_>, + args: VariableSubsSlice, + closure_var: Variable, + ret_var: Variable, + ) -> Cacheable> { + roc_tracing::debug!(var = ?closure_var, size = ?lambda_set_size(env.subs, closure_var), "building lambda set layout"); + + let lambda_set = resolve_lambda_set(env.subs, closure_var); + + let mut cache_criteria = CACHEABLE; + + let mut fn_args = Vec::with_capacity_in(args.len(), env.arena); + + for index in args.into_iter() { + let arg_var = env.subs[index]; + let layout = cached!(Layout::from_var(env, arg_var), cache_criteria, env.subs); + fn_args.push(layout); + } + + let ret = cached!(Layout::from_var(env, ret_var), cache_criteria, env.subs); + + let fn_args = env.arena.alloc(fn_args.into_bump_slice()); + + match lambda_set { + ResolvedLambdaSet::Set(mut lambdas, opt_recursion_var) => { + // sort the tags; make sure ordering stays intact! + lambdas.sort_by_key(|(sym, _)| *sym); + + let mut set: Vec<(Symbol, &[InLayout])> = + Vec::with_capacity_in(lambdas.len(), env.arena); + let mut set_with_variables: std::vec::Vec<(&Symbol, &[Variable])> = + std::vec::Vec::with_capacity(lambdas.len()); + let mut set_captures_have_naked_rec_ptr = false; + + let mut last_function_symbol = None; + let mut lambdas_it = lambdas.iter().peekable(); + + let mut has_duplicate_lambda_names = false; + while let Some((function_symbol, variables)) = lambdas_it.next() { + let mut arguments = Vec::with_capacity_in(variables.len(), env.arena); + + if let Some(rec_var) = opt_recursion_var.into_variable() { + env.insert_seen(rec_var); + } + + for var in variables { + // We determine cacheability of the lambda set based on the runtime + // representation, so here the criteria doesn't matter. + let mut criteria = CACHEABLE; + let arg = cached!(Layout::from_var(env, *var), criteria, env.subs); + arguments.push(arg); + set_captures_have_naked_rec_ptr = + set_captures_have_naked_rec_ptr || criteria.has_naked_recursion_pointer; + } + + let arguments = arguments.into_bump_slice(); + + let is_multimorphic = match (last_function_symbol, lambdas_it.peek()) { + (None, None) => false, + (Some(sym), None) | (None, Some((sym, _))) => function_symbol == sym, + (Some(sym1), Some((sym2, _))) => { + function_symbol == sym1 || function_symbol == sym2 + } + }; + + has_duplicate_lambda_names = has_duplicate_lambda_names || is_multimorphic; + + set.push((*function_symbol, arguments)); + set_with_variables.push((function_symbol, variables.as_slice())); + + last_function_symbol = Some(function_symbol); + + if let Some(rec_var) = opt_recursion_var.into_variable() { + env.remove_seen(rec_var); + } + } + + let (set, set_with_variables) = if has_duplicate_lambda_names { + // If we have a lambda set with duplicate names, then we sort first by name, + // and break ties by sorting on the layout. We need to do this again since the + // first sort would not have sorted on the layout. + + // TODO: be more efficient, we can compute the permutation once and then apply + // it to both vectors. + let mut joined = set + .into_iter() + .zip(set_with_variables) + .collect::>(); + joined.sort_by(|(lam_and_captures1, _), (lam_and_captures2, _)| { + lam_and_captures1.cmp(lam_and_captures2) + }); + // Remove duplicate lambda captures layouts unification can't see as + // duplicates, for example [[Thunk {a: Str}, Thunk [A Str]]], each of which are + // newtypes over the lambda layout `Thunk Str`. + joined.dedup_by_key(|((name, captures), _)| (*name, *captures)); + + let (set, set_with_variables): (std::vec::Vec<_>, std::vec::Vec<_>) = + joined.into_iter().unzip(); + + let set = Vec::from_iter_in(set, env.arena); + + (set, set_with_variables) + } else { + (set, set_with_variables) + }; + + let Cacheable(representation, criteria) = Self::make_representation( + env, + set_with_variables, + opt_recursion_var.into_variable(), + ); + cache_criteria.and(criteria, env.subs); + + let needs_recursive_fixup = NeedsRecursionPointerFixup( + opt_recursion_var.is_some() && set_captures_have_naked_rec_ptr, + ); + + let lambda_set = env.cache.interner.insert_lambda_set( + env.arena, + fn_args, + ret, + env.arena.alloc(set.into_bump_slice()), + needs_recursive_fixup, + representation, + ); + + Cacheable(Ok(lambda_set), cache_criteria) + } + ResolvedLambdaSet::Unbound => { + // The lambda set is unbound which means it must be unused. Just give it the empty lambda set. + // See also https://github.com/roc-lang/roc/issues/3163. + let lambda_set = env.cache.interner.insert_lambda_set( + env.arena, + fn_args, + ret, + &(&[] as &[(Symbol, &[InLayout])]), + NeedsRecursionPointerFixup(false), + Layout::UNIT, + ); + Cacheable(Ok(lambda_set), cache_criteria) + } + } + } + + fn make_representation( + env: &mut Env<'a, '_>, + set: std::vec::Vec<(&Symbol, &[Variable])>, + opt_rec_var: Option, + ) -> Cacheable> { + let union_labels = UnsortedUnionLabels { tags: set }; + + // Even if a variant in the lambda set has uninhabitable captures (and is hence + // unreachable as a function), we want to keep it in the representation. Failing to do so + // risks dropping relevant specializations needed during monomorphization. + let drop_uninhabited_variants = DropUninhabitedVariants(false); + + match opt_rec_var { + Some(rec_var) => { + let Cacheable(result, criteria) = + layout_from_recursive_union(env, rec_var, &union_labels); + let result = result.expect("unable to create lambda set representation"); + Cacheable(result, criteria) + } + + None => layout_from_non_recursive_union(env, &union_labels, drop_uninhabited_variants), + } + } + + pub fn stack_size(&self, interner: &I) -> u32 + where + I: LayoutInterner<'a>, + { + interner.get_repr(self.representation).stack_size(interner) + } + pub fn contains_refcounted(&self, interner: &I) -> bool + where + I: LayoutInterner<'a>, + { + interner + .get_repr(self.representation) + .contains_refcounted(interner) + } + pub fn safe_to_memcpy(&self, interner: &I) -> bool + where + I: LayoutInterner<'a>, + { + interner + .get_repr(self.representation) + .safe_to_memcpy(interner) + } + + pub fn alignment_bytes(&self, interner: &I) -> u32 + where + I: LayoutInterner<'a>, + { + interner + .get_repr(self.representation) + .alignment_bytes(interner) + } +} + +enum ResolvedLambdaSet { + Set( + std::vec::Vec<(Symbol, std::vec::Vec)>, + OptVariable, + ), + /// The lambda set is empty, that means this function is never called. + Unbound, +} + +fn resolve_lambda_set(subs: &Subs, mut var: Variable) -> ResolvedLambdaSet { + let mut set = vec![]; + loop { + match subs.get_content_without_compacting(var) { + Content::LambdaSet(subs::LambdaSet { + solved, + recursion_var, + unspecialized, + ambient_function: _, + }) => { + debug_assert!( + unspecialized.is_empty(), + "unspecialized lambda sets left over during resolution: {:?}, {:?}", + roc_types::subs::SubsFmtContent(subs.get_content_without_compacting(var), subs), + subs.uls_of_var + ); + roc_types::pretty_print::push_union(subs, solved, &mut set); + return ResolvedLambdaSet::Set(set, *recursion_var); + } + Content::RecursionVar { structure, .. } => { + var = *structure; + } + Content::FlexVar(_) => return ResolvedLambdaSet::Unbound, + + c => internal_error!("called with a non-lambda set {:?}", c), + } + } +} + +/// Determines the "size" of a lambda set. Size roughly calculates how many nested lambda sets are +/// captured in a lambda set. +/// Size is calculated in three dimensions: +/// - the depth of the longest chain of nested lambda sets, including type constructors besides +/// lambda sets. +/// - the depth of the longest chain of nested lambda sets, excluding type constructors besides +/// lambda sets. +/// - the total number of lambda sets +/// The returned tuple consists of these statistics in order. A lambda set with no nested lambda +/// set captures, but perhaps with other captures, would have a size of (1, 1, 1). +/// +/// Follows recursion variables until they are seen twice. +/// Returns (0, 0, 0) if the provided variable is not a lambda set. +fn lambda_set_size(subs: &Subs, var: Variable) -> (usize, usize, usize) { + // NOTE: we must be very careful not to recurse on the stack. + let mut max_depth_any_ctor = 0; + let mut max_depth_only_lset = 0; + let mut total = 0; + + let mut seen_rec_vars = roc_collections::VecSet::default(); + + // Run a DFS. I think in general deeply nested lambda sets wind up looking like multi-leaf + // trees, so I think running the depth first saves space. + let mut stack = std::vec::Vec::with_capacity(4); + stack.push((var, 0, 0)); + while let Some((var, depth_any, depth_lset)) = stack.pop() { + match subs.get_content_without_compacting(var) { + // The interesting case + Content::LambdaSet(roc_types::subs::LambdaSet { + solved, + recursion_var, + unspecialized: _, + ambient_function: _, + }) => { + total += 1; + + let new_depth_any = depth_any + 1; + let new_depth_lset = depth_lset + 1; + max_depth_any_ctor = std::cmp::max(max_depth_any_ctor, new_depth_any); + max_depth_only_lset = std::cmp::max(max_depth_only_lset, new_depth_lset); + + if let Some(rec_var) = recursion_var.into_variable() { + seen_rec_vars.insert(rec_var); + } + for (_, captures) in solved.iter_from_subs(subs) { + for capture in captures { + stack.push((*capture, new_depth_any, new_depth_lset)); + } + } + } + // The boring ones + Content::RecursionVar { + structure, + opt_name: _, + } => { + if !seen_rec_vars.contains(&var) { + stack.push((*structure, depth_any + 1, depth_lset)) + } + } + Content::Alias(_, _, real_var, _) => { + // For layout purposes, only the real_var matters. + stack.push((*real_var, depth_any + 1, depth_lset)); + } + Content::Structure(flat_type) => match flat_type { + FlatType::Apply(_, args) => { + for var in subs.get_subs_slice(*args) { + stack.push((*var, depth_any + 1, depth_lset)); + } + } + FlatType::Func(args, lset, ret) => { + for var in subs.get_subs_slice(*args) { + stack.push((*var, depth_any + 1, depth_lset)); + } + stack.push((*lset, depth_any + 1, depth_lset)); + stack.push((*ret, depth_any + 1, depth_lset)); + } + FlatType::Record(fields, ext) => { + for var_index in fields.iter_variables() { + let var = subs[var_index]; + stack.push((var, depth_any + 1, depth_lset)); + } + stack.push((*ext, depth_any + 1, depth_lset)); + } + FlatType::Tuple(elems, ext) => { + for var_index in elems.iter_variables() { + let var = subs[var_index]; + stack.push((var, depth_any + 1, depth_lset)); + } + stack.push((*ext, depth_any + 1, depth_lset)); + } + FlatType::FunctionOrTagUnion(_, _, ext) => { + stack.push((ext.var(), depth_any + 1, depth_lset)); + } + FlatType::TagUnion(tags, ext) => { + for (_, payloads) in tags.iter_from_subs(subs) { + for payload in payloads { + stack.push((*payload, depth_any + 1, depth_lset)); + } + } + stack.push((ext.var(), depth_any + 1, depth_lset)); + } + FlatType::RecursiveTagUnion(rec_var, tags, ext) => { + seen_rec_vars.insert(*rec_var); + for (_, payloads) in tags.iter_from_subs(subs) { + for payload in payloads { + stack.push((*payload, depth_any + 1, depth_lset)); + } + } + stack.push((ext.var(), depth_any + 1, depth_lset)); + } + FlatType::EmptyRecord | FlatType::EmptyTuple | FlatType::EmptyTagUnion => {} + }, + Content::FlexVar(_) + | Content::RigidVar(_) + | Content::FlexAbleVar(_, _) + | Content::RigidAbleVar(_, _) + | Content::RangedNumber(_) + | Content::Error + | Content::ErasedLambda => {} + } + } + (max_depth_any_ctor, max_depth_only_lset, total) +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub enum Builtin<'a> { + Int(IntWidth), + Float(FloatWidth), + Bool, + Decimal, + Str, + List(InLayout<'a>), +} + +#[macro_export] +macro_rules! list_element_layout { + ($interner:expr, $list_layout:expr) => { + match $interner.get_repr($list_layout) { + LayoutRepr::Builtin(Builtin::List(list_layout)) => list_layout, + _ => internal_error!("invalid list layout"), + } + }; +} + +pub struct Env<'a, 'b> { + target_info: TargetInfo, + pub(crate) arena: &'a Bump, + seen: Vec<'a, Variable>, + pub(crate) subs: &'b Subs, + cache: &'b mut LayoutCache<'a>, +} + +impl<'a, 'b> Env<'a, 'b> { + pub fn from_components( + cache: &'b mut LayoutCache<'a>, + subs: &'b Subs, + arena: &'a Bump, + target_info: TargetInfo, + ) -> Self { + Self { + cache, + subs, + seen: Vec::new_in(arena), + arena, + target_info, + } + } + + fn is_seen(&self, var: Variable) -> bool { + let var = self.subs.get_root_key_without_compacting(var); + + self.seen.iter().rev().any(|x| x == &var) + } + + fn insert_seen(&mut self, var: Variable) { + let var = self.subs.get_root_key_without_compacting(var); + + self.seen.push(var); + } + + fn remove_seen(&mut self, var: Variable) -> bool { + let var = self.subs.get_root_key_without_compacting(var); + + if let Some(index) = self.seen.iter().rposition(|x| x == &var) { + self.seen.remove(index); + true + } else { + false + } + } + + #[inline(always)] + fn can_reuse_cached(&self, var: Variable, cache_metadata: &CacheMeta) -> bool { + let CacheMeta { + recursive_structures, + } = cache_metadata; + for &recursive_structure in recursive_structures.iter() { + if self.is_seen(recursive_structure) { + // If the cached entry references a recursive structure that we're in the process + // of visiting currently, we can't use the cached entry, and instead must + // recalculate the nested layout, because the nested recursive structure will + // likely turn into a recursive pointer now. + // + // For example, suppose we are constructing the layout of + // + // [A, B (List r)] as r + // + // and we have already constructed and cached the layout of `List r`, which would + // be + // + // List (Recursive [Unit, List RecursivePointer]) + // + // If we use the cached entry of `List r`, we would end up with the layout + // + // Recursive [Unit, (List (Recursive [Unit, List RecursivePointer]))] + // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ cached layout for `List r` + // + // but this is not correct; the canonical layout of `[A, B (List r)] as r` is + // + // Recursive [Unit, (List RecursivePointer)] + roc_tracing::debug!(?var, "not reusing cached recursive structure"); + return false; + } + } + true + } +} + +macro_rules! cached_or_impl { + ($self:ident, $var:ident, $compute_layout:ident, $get:ident, $insert:ident, $stats:ident) => {{ + if let Some((result, metadata)) = $self.cache.$get($self.subs, $var) { + // cache HIT + inc_stat!($self.cache.$stats, hits); + + if $self.can_reuse_cached($var, &metadata) { + // Happy path - the cached layout can be reused, return it immediately. + return Cacheable(result, metadata.into_criteria()); + } else { + // Although we have a cached layout, we cannot readily reuse it at this time. We'll + // need to recompute the layout, as done below. + inc_stat!($self.cache.$stats, non_reusable); + } + } else { + // cache MISS - compute the layout + inc_stat!($self.cache.$stats, misses); + } + + let Cacheable(result, criteria) = $compute_layout($self); + + if criteria.is_cacheable() { + // The computed layout is cacheable; insert it. + $self + .cache + .$insert($self.subs, $var, result, criteria.cache_metadata()); + inc_stat!($self.cache.$stats, insertions); + } else { + // The computed layout is not cacheable. We'll return it with the criteria that made it + // non-cacheable. + inc_stat!($self.cache.$stats, non_insertable); + roc_tracing::debug!(?result, ?$var, "not caching"); + } + + Cacheable(result, criteria) + }}; +} + +impl<'a, 'b> Env<'a, 'b> { + #[inline(always)] + fn cached_or( + &mut self, + var: Variable, + compute_layout: impl FnOnce(&mut Env<'a, 'b>) -> Cacheable>, + ) -> Cacheable> { + if self.is_seen(var) { + // Always return recursion pointers directly, NEVER cache them as naked! + // When this recursion pointer gets used in a recursive union, it will be filled to + // looop back to the correct layout. + // TODO(recursive-layouts): after the naked pointer is updated, we can cache `var` to + // point to the updated layout. + let rec_ptr = Layout::NAKED_RECURSIVE_PTR; + return Cacheable(Ok(rec_ptr), NAKED_RECURSION_PTR); + } + + cached_or_impl!(self, var, compute_layout, get, insert, stats) + } + + #[inline(always)] + fn cached_raw_function_or( + &mut self, + var: Variable, + compute_layout: impl FnOnce(&mut Env<'a, 'b>) -> Cacheable>, + ) -> Cacheable> { + cached_or_impl!( + self, + var, + compute_layout, + get_raw_function, + insert_raw_function, + raw_function_stats + ) + } +} + +pub const fn round_up_to_alignment(width: u32, alignment: u32) -> u32 { + match alignment { + 0 => width, + 1 => width, + _ => { + if width % alignment > 0 { + width + alignment - (width % alignment) + } else { + width + } + } + } +} + +#[inline(always)] +pub fn is_unresolved_var(subs: &Subs, var: Variable) -> bool { + use Content::*; + let content = subs.get_content_without_compacting(var); + matches!( + content, + FlexVar(..) | RigidVar(..) | FlexAbleVar(..) | RigidAbleVar(..), + ) +} + +#[inline(always)] +pub fn is_any_float_range(subs: &Subs, var: Variable) -> bool { + use {Content::*, NumericRange::*}; + let content = subs.get_content_without_compacting(var); + matches!( + content, + RangedNumber(NumAtLeastEitherSign(..) | NumAtLeastSigned(..)), + ) +} + +impl<'a> Layout<'a> { + pub(crate) const fn new(repr: LayoutWrapper<'a>, semantic: SemanticRepr<'a>) -> Self { + Self { repr, semantic } + } + + pub(crate) const fn no_semantic(repr: LayoutWrapper<'a>) -> Self { + Self { + repr, + semantic: SemanticRepr::NONE, + } + } + + pub(crate) fn repr(&self, interner: &I) -> LayoutRepr<'a> + where + I: LayoutInterner<'a>, + { + let mut lay = *self; + loop { + match lay.repr { + LayoutWrapper::Direct(repr) => return repr, + LayoutWrapper::Newtype(real) => { + lay = interner.get(real); + } + } + } + } + + fn new_help<'b>( + env: &mut Env<'a, 'b>, + _var: Variable, + content: Content, + ) -> Cacheable> { + use roc_types::subs::Content::*; + match content { + FlexVar(_) | RigidVar(_) => { + roc_debug_flags::dbg_do!(roc_debug_flags::ROC_NO_UNBOUND_LAYOUT, { + return cacheable(Err(LayoutProblem::UnresolvedTypeVar(_var))); + }); + + // If we encounter an unbound type var (e.g. `*` or `a`) + // then it's zero-sized; In the future we may drop this argument + // completely, but for now we represent it with the empty tag union + cacheable(Ok(Layout::VOID)) + } + FlexAbleVar(_, _) | RigidAbleVar(_, _) => { + roc_debug_flags::dbg_do!(roc_debug_flags::ROC_NO_UNBOUND_LAYOUT, { + todo_abilities!("Able var is unbound!"); + }); + + // If we encounter an unbound type var (e.g. `*` or `a`) + // then it's zero-sized; In the future we may drop this argument + // completely, but for now we represent it with the empty tag union + cacheable(Ok(Layout::VOID)) + } + RecursionVar { structure, .. } => { + let structure_content = env.subs.get_content_without_compacting(structure); + Self::new_help(env, structure, *structure_content) + } + LambdaSet(_) => { + internal_error!("lambda set should only appear under a function, where it's handled independently."); + } + ErasedLambda => { + internal_error!("erased lambda type should only appear under a function, where it's handled independently."); + } + Structure(flat_type) => layout_from_flat_type(env, flat_type), + + Alias(symbol, _args, actual_var, _) => { + if let Some(int_width) = IntWidth::try_from_symbol(symbol) { + return cacheable(Ok(Layout::int_width(int_width))); + } + + if let Some(float_width) = FloatWidth::try_from_symbol(symbol) { + return cacheable(Ok(Layout::float_width(float_width))); + } + + match symbol { + Symbol::NUM_DECIMAL => cacheable(Ok(Layout::DEC)), + + Symbol::NUM_NAT | Symbol::NUM_NATURAL => { + cacheable(Ok(Layout::usize(env.target_info))) + } + + Symbol::NUM_NUM | Symbol::NUM_INT | Symbol::NUM_INTEGER + if is_unresolved_var(env.subs, actual_var) => + { + // default to i64 + cacheable(Ok(Layout::default_integer())) + } + + Symbol::NUM_FRAC | Symbol::NUM_FLOATINGPOINT + if is_unresolved_var(env.subs, actual_var) + || is_any_float_range(env.subs, actual_var) => + { + // default to f64 + cacheable(Ok(Layout::default_float())) + } + + _ => Self::from_var(env, actual_var), + } + } + + RangedNumber(range) => Self::layout_from_ranged_number(env, range), + + Error => cacheable(Err(LayoutProblem::Erroneous)), + } + } + + pub const fn from_int_width(int_width: IntWidth) -> InLayout<'static> { + match int_width { + IntWidth::U8 => Layout::U8, + IntWidth::U16 => Layout::U16, + IntWidth::U32 => Layout::U32, + IntWidth::U64 => Layout::U64, + IntWidth::U128 => Layout::U128, + IntWidth::I8 => Layout::I8, + IntWidth::I16 => Layout::I16, + IntWidth::I32 => Layout::I32, + IntWidth::I64 => Layout::I64, + IntWidth::I128 => Layout::I128, + } + } + + fn layout_from_ranged_number( + env: &mut Env<'a, '_>, + range: NumericRange, + ) -> Cacheable> { + // We don't pass the range down because `RangedNumber`s are somewhat rare, they only + // appear due to number literals, so no need to increase parameter list sizes. + let num_layout = range.default_compilation_width(); + + cacheable(Ok(Layout::int_literal_width_to_int( + num_layout, + env.target_info, + ))) + } + + /// Returns Err(()) if given an error, or Ok(Layout) if given a non-erroneous Structure. + /// Panics if given a FlexVar or RigidVar, since those should have been + /// monomorphized away already! + fn from_var(env: &mut Env<'a, '_>, var: Variable) -> Cacheable> { + env.cached_or(var, |env| { + let content = env.subs.get_content_without_compacting(var); + Self::new_help(env, var, *content) + }) + } + + pub fn stack_size_and_alignment_slices( + interner: &I, + slices: &[&[InLayout<'a>]], + ) -> (u32, u32) + where + I: LayoutInterner<'a>, + { + let mut data_align = 1; + let mut data_width = 0; + + for tag in slices { + let mut total = 0; + for layout in tag.iter() { + let (stack_size, alignment) = interner + .get_repr(*layout) + .stack_size_and_alignment(interner); + total += stack_size; + data_align = data_align.max(alignment); + } + + data_width = data_width.max(total); + } + + data_width = round_up_to_alignment(data_width, data_align); + + (data_width, data_align) + } + + pub fn runtime_representation(&self, interner: &I) -> Self + where + I: LayoutInterner<'a>, + { + use LayoutRepr::*; + match self.repr(interner) { + LambdaSet(lambda_set) => interner.get(lambda_set.runtime_representation()), + _ => *self, + } + } + + pub fn runtime_representation_in(layout: InLayout<'a>, interner: &I) -> InLayout<'a> + where + I: LayoutInterner<'a>, + { + use LayoutRepr::*; + match interner.get_repr(layout) { + LambdaSet(lambda_set) => lambda_set.runtime_representation(), + _ => layout, + } + } +} + +impl<'a> LayoutRepr<'a> { + pub const UNIT: Self = LayoutRepr::struct_(&[]); + pub const BOOL: Self = LayoutRepr::Builtin(Builtin::Bool); + pub const U8: Self = LayoutRepr::Builtin(Builtin::Int(IntWidth::U8)); + pub const U16: Self = LayoutRepr::Builtin(Builtin::Int(IntWidth::U16)); + pub const U32: Self = LayoutRepr::Builtin(Builtin::Int(IntWidth::U32)); + pub const U64: Self = LayoutRepr::Builtin(Builtin::Int(IntWidth::U64)); + pub const U128: Self = LayoutRepr::Builtin(Builtin::Int(IntWidth::U128)); + pub const I8: Self = LayoutRepr::Builtin(Builtin::Int(IntWidth::I8)); + pub const I16: Self = LayoutRepr::Builtin(Builtin::Int(IntWidth::I16)); + pub const I32: Self = LayoutRepr::Builtin(Builtin::Int(IntWidth::I32)); + pub const I64: Self = LayoutRepr::Builtin(Builtin::Int(IntWidth::I64)); + pub const I128: Self = LayoutRepr::Builtin(Builtin::Int(IntWidth::I128)); + pub const F32: Self = LayoutRepr::Builtin(Builtin::Float(FloatWidth::F32)); + pub const F64: Self = LayoutRepr::Builtin(Builtin::Float(FloatWidth::F64)); + pub const DEC: Self = LayoutRepr::Builtin(Builtin::Decimal); + pub const STR: Self = LayoutRepr::Builtin(Builtin::Str); + pub const OPAQUE_PTR: Self = LayoutRepr::Ptr(Layout::VOID); + pub const ERASED: Self = LayoutRepr::Erased(Erased); + + pub const fn struct_(field_layouts: &'a [InLayout<'a>]) -> Self { + Self::Struct(field_layouts) + } + + pub(crate) const fn direct(self) -> LayoutWrapper<'a> { + LayoutWrapper::Direct(self) + } + + pub fn safe_to_memcpy(&self, interner: &I) -> bool + where + I: LayoutInterner<'a>, + { + use LayoutRepr::*; + + match self { + Builtin(builtin) => builtin.safe_to_memcpy(), + Struct(field_layouts) => field_layouts + .iter() + .all(|field_layout| interner.get_repr(*field_layout).safe_to_memcpy(interner)), + Union(variant) => { + use UnionLayout::*; + + match variant { + NonRecursive(tags) => tags.iter().all(|tag_layout| { + tag_layout + .iter() + .all(|field| interner.get_repr(*field).safe_to_memcpy(interner)) + }), + Recursive(_) + | NullableWrapped { .. } + | NullableUnwrapped { .. } + | NonNullableUnwrapped(_) => { + // a recursive union will always contain a pointer, and is thus not safe to memcpy + false + } + } + } + LambdaSet(lambda_set) => interner + .get_repr(lambda_set.runtime_representation()) + .safe_to_memcpy(interner), + Ptr(_) | RecursivePointer(_) => { + // We cannot memcpy pointers, because then we would have the same pointer in multiple places! + false + } + Erased(e) => e.safe_to_memcpy(), + FunctionPointer(..) => true, + } + } + + pub fn is_dropped_because_empty(&self) -> bool { + // For this calculation, we don't need an accurate + // stack size, we just need to know whether it's zero, + // so it's fine to use a pointer size of 1. + false // TODO this should use is_zero_sized once doing so doesn't break things! + } + + pub fn is_passed_by_reference(&self, interner: &I) -> bool + where + I: LayoutInterner<'a>, + { + match self { + LayoutRepr::Builtin(builtin) => { + use Builtin::*; + + match interner.target_info().ptr_width() { + PtrWidth::Bytes4 => { + // more things fit into a register + false + } + PtrWidth::Bytes8 => { + // currently, only Str is passed by-reference internally + matches!(builtin, Str) + } + } + } + LayoutRepr::Union(UnionLayout::NonRecursive(_)) => true, + LayoutRepr::Struct(_) => { + // TODO: write tests for this! + self.stack_size(interner) as usize > interner.target_info().max_by_value_size() + } + + LayoutRepr::LambdaSet(lambda_set) => interner + .get_repr(lambda_set.runtime_representation()) + .is_passed_by_reference(interner), + _ => false, + } + } + + pub fn stack_size(&self, interner: &I) -> u32 + where + I: LayoutInterner<'a>, + { + let width = self.stack_size_without_alignment(interner); + let alignment = self.alignment_bytes(interner); + + round_up_to_alignment(width, alignment) + } + + pub fn stack_size_and_alignment(&self, interner: &I) -> (u32, u32) + where + I: LayoutInterner<'a>, + { + let width = self.stack_size_without_alignment(interner); + let alignment = self.alignment_bytes(interner); + + let size = round_up_to_alignment(width, alignment); + (size, alignment) + } + + /// Very important to use this when doing a memcpy! + pub fn stack_size_without_alignment(&self, interner: &I) -> u32 + where + I: LayoutInterner<'a>, + { + use LayoutRepr::*; + + match self { + Builtin(builtin) => builtin.stack_size(interner.target_info()), + Struct(field_layouts) => { + let mut sum = 0; + + for field_layout in *field_layouts { + sum += interner.get_repr(*field_layout).stack_size(interner); + } + + sum + } + Union(variant) => variant.stack_size_without_alignment(interner), + LambdaSet(lambda_set) => interner + .get_repr(lambda_set.runtime_representation()) + .stack_size_without_alignment(interner), + RecursivePointer(_) | Ptr(_) | FunctionPointer(_) => { + interner.target_info().ptr_width() as u32 + } + Erased(e) => e.stack_size_without_alignment(interner.target_info()), + } + } + + pub fn alignment_bytes(&self, interner: &I) -> u32 + where + I: LayoutInterner<'a>, + { + use LayoutRepr::*; + match self { + Struct(field_layouts) => field_layouts + .iter() + .map(|x| interner.get_repr(*x).alignment_bytes(interner)) + .max() + .unwrap_or(0), + + Union(variant) => { + use UnionLayout::*; + + match variant { + NonRecursive(tags) => { + let max_alignment = tags + .iter() + .flat_map(|layouts| { + layouts.iter().map(|layout| { + interner.get_repr(*layout).alignment_bytes(interner) + }) + }) + .max(); + + let discriminant = variant.discriminant(); + match max_alignment { + Some(align) => round_up_to_alignment( + align.max(discriminant.alignment_bytes()), + discriminant.alignment_bytes(), + ), + None => { + // none of the tags had any payload, but the tag id still contains information + discriminant.alignment_bytes() + } + } + } + Recursive(_) + | NullableWrapped { .. } + | NullableUnwrapped { .. } + | NonNullableUnwrapped(_) => interner.target_info().ptr_width() as u32, + } + } + LambdaSet(lambda_set) => interner + .get_repr(lambda_set.runtime_representation()) + .alignment_bytes(interner), + Builtin(builtin) => builtin.alignment_bytes(interner.target_info()), + RecursivePointer(_) | Ptr(_) | FunctionPointer(_) => { + interner.target_info().ptr_width() as u32 + } + Erased(e) => e.alignment_bytes(interner.target_info()), + } + } + + pub fn allocation_alignment_bytes(&self, interner: &I) -> u32 + where + I: LayoutInterner<'a>, + { + let ptr_width = interner.target_info().ptr_width() as u32; + + use LayoutRepr::*; + match self { + Builtin(builtin) => builtin.allocation_alignment_bytes(interner), + Struct { .. } => self.alignment_bytes(interner).max(ptr_width), + Union(union_layout) => union_layout.allocation_alignment_bytes(interner), + LambdaSet(lambda_set) => interner + .get_repr(lambda_set.runtime_representation()) + .allocation_alignment_bytes(interner), + RecursivePointer(_) => { + unreachable!("should be looked up to get an actual layout") + } + Ptr(inner) => interner.get_repr(*inner).alignment_bytes(interner), + FunctionPointer(_) => ptr_width, + Erased(e) => e.allocation_alignment_bytes(interner.target_info()), + } + } + + pub fn is_refcounted(&self, interner: &I) -> bool + where + I: LayoutInterner<'a>, + { + use self::Builtin::*; + use LayoutRepr::*; + + match self { + Union(UnionLayout::NonRecursive(_)) => false, + + Union(_) => true, + + RecursivePointer(_) => true, + + Builtin(List(_)) | Builtin(Str) => true, + + Erased(_) => true, + + LambdaSet(lambda_set) => interner.is_refcounted(lambda_set.runtime_representation()), + + _ => false, + } + } + + pub fn is_nullable(&self) -> bool { + use LayoutRepr::*; + + match self { + Union(union_layout) => match union_layout { + UnionLayout::NonRecursive(_) => false, + UnionLayout::Recursive(_) => false, + UnionLayout::NonNullableUnwrapped(_) => false, + UnionLayout::NullableWrapped { .. } => true, + UnionLayout::NullableUnwrapped { .. } => true, + }, + + _ => false, + } + } + + /// Even if a value (say, a record) is not itself reference counted, + /// it may contains values/fields that are. Therefore when this record + /// goes out of scope, the refcount on those values/fields must be decremented. + pub fn contains_refcounted(&self, interner: &I) -> bool + where + I: LayoutInterner<'a>, + { + use LayoutRepr::*; + + match self { + Builtin(builtin) => builtin.is_refcounted(), + Struct(field_layouts) => field_layouts + .iter() + .any(|f| interner.get_repr(*f).contains_refcounted(interner)), + Union(variant) => { + use UnionLayout::*; + + match variant { + NonRecursive(fields) => fields + .iter() + .flat_map(|ls| ls.iter()) + .any(|f| interner.get_repr(*f).contains_refcounted(interner)), + Recursive(_) + | NullableWrapped { .. } + | NullableUnwrapped { .. } + | NonNullableUnwrapped(_) => true, + } + } + LambdaSet(lambda_set) => interner + .get_repr(lambda_set.runtime_representation()) + .contains_refcounted(interner), + RecursivePointer(_) => true, + Ptr(_) => { + // we never consider pointers for refcounting. Ptr is not user-facing. The compiler + // author must make sure that invariants are upheld + false + } + FunctionPointer(_) => false, + Erased(e) => e.is_refcounted(), + } + } + + pub fn has_varying_stack_size(self, interner: &I, arena: &bumpalo::Bump) -> bool + where + I: LayoutInterner<'a>, + { + let mut stack: Vec = bumpalo::collections::Vec::new_in(arena); + + stack.push(self); + + use LayoutRepr::*; + while let Some(layout) = stack.pop() { + match layout { + Builtin(builtin) => { + use self::Builtin::*; + match builtin { + Int(_) + | Float(_) + | Bool + | Decimal + | Str + // If there's any layer of indirection (behind a pointer), then it doesn't vary! + | List(_) => { /* do nothing */ } + } + } + // If there's any layer of indirection (behind a pointer), then it doesn't vary! + Struct(field_layouts) => stack.extend( + field_layouts + .iter() + .map(|interned| interner.get_repr(*interned)), + ), + Union(tag_union) => match tag_union { + UnionLayout::NonRecursive(tags) | UnionLayout::Recursive(tags) => { + for tag in tags { + stack.extend(tag.iter().map(|interned| interner.get_repr(*interned))); + } + } + UnionLayout::NonNullableUnwrapped(fields) => { + stack.extend(fields.iter().map(|interned| interner.get_repr(*interned))); + } + UnionLayout::NullableWrapped { other_tags, .. } => { + for tag in other_tags { + stack.extend(tag.iter().map(|interned| interner.get_repr(*interned))); + } + } + UnionLayout::NullableUnwrapped { other_fields, .. } => { + stack.extend( + other_fields + .iter() + .map(|interned| interner.get_repr(*interned)), + ); + } + }, + LambdaSet(_) => return true, + Ptr(_) => { + // If there's any layer of indirection (behind a pointer), then it doesn't vary! + } + RecursivePointer(_) => { + /* do nothing, we've already generated for this type through the Union(_) */ + } + FunctionPointer(_) => { + // drop through + } + Erased(_) => { + // erasures are just pointers, so they do not vary + } + } + } + + false + } +} + +pub type SeenRecPtrs<'a> = VecSet>; + +impl<'a> Layout<'a> { + pub fn usize(target_info: TargetInfo) -> InLayout<'a> { + match target_info.ptr_width() { + roc_target::PtrWidth::Bytes4 => Layout::U32, + roc_target::PtrWidth::Bytes8 => Layout::U64, + } + } + + pub fn isize(target_info: TargetInfo) -> InLayout<'a> { + match target_info.ptr_width() { + roc_target::PtrWidth::Bytes4 => Layout::I32, + roc_target::PtrWidth::Bytes8 => Layout::I64, + } + } + + pub fn default_integer() -> InLayout<'a> { + Layout::I64 + } + + pub fn default_float() -> InLayout<'a> { + Layout::DEC + } + + pub fn int_literal_width_to_int( + width: roc_types::num::IntLitWidth, + target_info: TargetInfo, + ) -> InLayout<'a> { + use roc_types::num::IntLitWidth::*; + match width { + U8 => Layout::U8, + U16 => Layout::U16, + U32 => Layout::U32, + U64 => Layout::U64, + U128 => Layout::U128, + I8 => Layout::I8, + I16 => Layout::I16, + I32 => Layout::I32, + I64 => Layout::I64, + I128 => Layout::I128, + Nat => Layout::usize(target_info), + // f32 int literal bounded by +/- 2^24, so fit it into an i32 + F32 => Layout::F32, + // f64 int literal bounded by +/- 2^53, so fit it into an i32 + F64 => Layout::F64, + // dec int literal bounded by i128, so fit it into an i128 + Dec => Layout::DEC, + } + } + + pub fn is_recursive_tag_union(self, interner: &I) -> bool + where + I: LayoutInterner<'a>, + { + matches!( + self.repr(interner), + LayoutRepr::Union( + UnionLayout::NullableUnwrapped { .. } + | UnionLayout::Recursive(_) + | UnionLayout::NullableWrapped { .. } + | UnionLayout::NonNullableUnwrapped { .. }, + ) + ) + } +} + +impl<'a> Builtin<'a> { + const I1_SIZE: u32 = std::mem::size_of::() as u32; + const DECIMAL_SIZE: u32 = std::mem::size_of::() as u32; + + /// Number of machine words in an empty one of these + pub const STR_WORDS: u32 = 3; + pub const LIST_WORDS: u32 = 3; + + /// Layout of collection wrapper for List, Str, Dict, and Set - a struct of (pointer, length, capacity). + pub const WRAPPER_PTR: u32 = 0; + pub const WRAPPER_LEN: u32 = 1; + pub const WRAPPER_CAPACITY: u32 = 2; + + pub fn stack_size(&self, target_info: TargetInfo) -> u32 { + use Builtin::*; + + let ptr_width = target_info.ptr_width() as u32; + + match self { + Int(int) => int.stack_size(), + Float(float) => float.stack_size(), + Bool => Builtin::I1_SIZE, + Decimal => Builtin::DECIMAL_SIZE, + Str => Builtin::STR_WORDS * ptr_width, + List(_) => Builtin::LIST_WORDS * ptr_width, + } + } + + pub fn alignment_bytes(&self, target_info: TargetInfo) -> u32 { + use std::mem::align_of; + use Builtin::*; + + let ptr_width = target_info.ptr_width() as u32; + + // for our data structures, what counts is the alignment of the `( ptr, len )` tuple, and + // since both of those are one pointer size, the alignment of that structure is a pointer + // size + match self { + Int(int_width) => int_width.alignment_bytes(target_info), + Float(float_width) => float_width.alignment_bytes(target_info), + Bool => align_of::() as u32, + Decimal => IntWidth::I128.alignment_bytes(target_info), + // we often treat these as i128 (64-bit systems) + // or i64 (32-bit systems). + // + // In webassembly, For that to be safe + // they must be aligned to allow such access + List(_) => ptr_width, + Str => ptr_width, + } + } + + pub fn safe_to_memcpy(&self) -> bool { + use Builtin::*; + + match self { + Int(_) | Float(_) | Bool | Decimal => true, + + Str | List(_) => false, + } + } + + // Question: does is_refcounted exactly correspond with the "safe to memcpy" property? + pub fn is_refcounted(&self) -> bool { + use Builtin::*; + + match self { + Int(_) | Float(_) | Bool | Decimal => false, + List(_) => true, + Str => true, + } + } + + pub fn to_doc<'b, D, A, I>( + self, + alloc: &'b D, + interner: &I, + seen_rec: &mut SeenRecPtrs<'a>, + _parens: Parens, + ) -> DocBuilder<'b, D, A> + where + D: DocAllocator<'b, A>, + D::Doc: Clone, + A: Clone, + I: LayoutInterner<'a>, + { + use Builtin::*; + + match self { + Int(int_width) => { + use IntWidth::*; + + match int_width { + I128 => alloc.text("I128"), + I64 => alloc.text("I64"), + I32 => alloc.text("I32"), + I16 => alloc.text("I16"), + I8 => alloc.text("I8"), + U128 => alloc.text("U128"), + U64 => alloc.text("U64"), + U32 => alloc.text("U32"), + U16 => alloc.text("U16"), + U8 => alloc.text("U8"), + } + } + + Float(float_width) => { + use FloatWidth::*; + + match float_width { + F64 => alloc.text("Float64"), + F32 => alloc.text("Float32"), + } + } + + Bool => alloc.text("Int1"), + Decimal => alloc.text("Decimal"), + + Str => alloc.text("Str"), + List(layout) => alloc.text("List ").append(interner.to_doc( + layout, + alloc, + seen_rec, + Parens::InTypeParam, + )), + } + } + + pub fn allocation_alignment_bytes(&self, interner: &I) -> u32 + where + I: LayoutInterner<'a>, + { + let target_info = interner.target_info(); + let ptr_width = target_info.ptr_width() as u32; + + let allocation = match self { + Builtin::Str => ptr_width, + Builtin::List(e) => { + let e = interner.get_repr(*e); + e.alignment_bytes(interner).max(ptr_width) + } + // The following are usually not heap-allocated, but they might be when inside a Box. + Builtin::Int(int_width) => int_width.alignment_bytes(target_info).max(ptr_width), + Builtin::Float(float_width) => float_width.alignment_bytes(target_info).max(ptr_width), + Builtin::Bool => (core::mem::align_of::() as u32).max(ptr_width), + Builtin::Decimal => IntWidth::I128.alignment_bytes(target_info).max(ptr_width), + }; + + allocation.max(ptr_width) + } +} + +fn layout_from_flat_type<'a>( + env: &mut Env<'a, '_>, + flat_type: FlatType, +) -> Cacheable> { + use roc_types::subs::FlatType::*; + + let arena = env.arena; + let subs = env.subs; + let target_info = env.target_info; + + match flat_type { + Apply(symbol, args) => { + let args = Vec::from_iter_in(args.into_iter().map(|index| subs[index]), arena); + + match symbol { + // Ints + Symbol::NUM_NAT => { + debug_assert_eq!(args.len(), 0); + cacheable(Ok(Layout::usize(env.target_info))) + } + + Symbol::NUM_I128 => { + debug_assert_eq!(args.len(), 0); + cacheable(Ok(Layout::I128)) + } + Symbol::NUM_I64 => { + debug_assert_eq!(args.len(), 0); + cacheable(Ok(Layout::I64)) + } + Symbol::NUM_I32 => { + debug_assert_eq!(args.len(), 0); + cacheable(Ok(Layout::I32)) + } + Symbol::NUM_I16 => { + debug_assert_eq!(args.len(), 0); + cacheable(Ok(Layout::I16)) + } + Symbol::NUM_I8 => { + debug_assert_eq!(args.len(), 0); + cacheable(Ok(Layout::I8)) + } + + Symbol::NUM_U128 => { + debug_assert_eq!(args.len(), 0); + cacheable(Ok(Layout::U128)) + } + Symbol::NUM_U64 => { + debug_assert_eq!(args.len(), 0); + cacheable(Ok(Layout::U64)) + } + Symbol::NUM_U32 => { + debug_assert_eq!(args.len(), 0); + cacheable(Ok(Layout::U32)) + } + Symbol::NUM_U16 => { + debug_assert_eq!(args.len(), 0); + cacheable(Ok(Layout::U16)) + } + Symbol::NUM_U8 => { + debug_assert_eq!(args.len(), 0); + cacheable(Ok(Layout::U8)) + } + + // Floats + Symbol::NUM_DEC => { + debug_assert_eq!(args.len(), 0); + cacheable(Ok(Layout::DEC)) + } + Symbol::NUM_F64 => { + debug_assert_eq!(args.len(), 0); + cacheable(Ok(Layout::F64)) + } + Symbol::NUM_F32 => { + debug_assert_eq!(args.len(), 0); + cacheable(Ok(Layout::F32)) + } + + Symbol::NUM_NUM => { + // Num.Num should only ever have 1 argument, e.g. Num.Num Int.Integer + debug_assert_eq!(args.len(), 1); + + let var = args[0]; + let content = subs.get_content_without_compacting(var); + + layout_from_num_content(content, target_info) + } + + Symbol::STR_STR => cacheable(Ok(Layout::STR)), + Symbol::LIST_LIST => list_layout_from_elem(env, args[0]), + Symbol::BOX_BOX_TYPE => { + // Num.Num should only ever have 1 argument, e.g. Num.Num Int.Integer + debug_assert_eq!(args.len(), 1); + + let mut criteria = CACHEABLE; + + let inner_var = args[0]; + let inner_layout = + cached!(Layout::from_var(env, inner_var), criteria, env.subs); + + let repr = LayoutRepr::Union(UnionLayout::NonNullableUnwrapped( + arena.alloc([inner_layout]), + )); + + let boxed_layout = env.cache.put_in(Layout { + repr: repr.direct(), + semantic: SemanticRepr::NONE, + }); + + Cacheable(Ok(boxed_layout), criteria) + } + _ => { + panic!("TODO layout_from_flat_type for Apply({symbol:?}, {args:?})"); + } + } + } + Func(args, closure_var, ret_var) => { + if env.is_seen(closure_var) { + // TODO(recursive-layouts): after the naked pointer is updated, we can cache `var` to + // point to the updated layout. + let rec_ptr = Layout::NAKED_RECURSIVE_PTR; + Cacheable(Ok(rec_ptr), NAKED_RECURSION_PTR) + } else { + let mut criteria = CACHEABLE; + + let closure_data = build_function_closure_data(env, args, closure_var, ret_var); + let closure_data = cached!(closure_data, criteria, env.subs); + + match closure_data { + ClosureDataKind::LambdaSet(lambda_set) => { + Cacheable(Ok(lambda_set.full_layout), criteria) + } + ClosureDataKind::Erased => Cacheable(Ok(Layout::ERASED), criteria), + } + } + } + Record(fields, ext_var) => { + let mut criteria = CACHEABLE; + + // extract any values from the ext_var + let mut sortables = Vec::with_capacity_in(fields.len(), arena); + let it = match fields.unsorted_iterator(subs, ext_var) { + Ok(it) => it, + Err(RecordFieldsError) => { + return Cacheable(Err(LayoutProblem::Erroneous), criteria) + } + }; + + for (label, field) in it { + match field { + RecordField::Required(field_var) + | RecordField::Demanded(field_var) + | RecordField::RigidRequired(field_var) => { + let field_layout = + cached!(Layout::from_var(env, field_var), criteria, env.subs); + sortables.push((label, field_layout)); + } + RecordField::Optional(_) | RecordField::RigidOptional(_) => { + // drop optional fields + } + } + } + + sortables.sort_by(|(label1, layout1), (label2, layout2)| { + cmp_fields(&env.cache.interner, label1, *layout1, label2, *layout2) + }); + + let ordered_field_names = Vec::from_iter_in( + sortables + .iter() + .map(|(label, _)| &*arena.alloc_str(label.as_str())), + arena, + ) + .into_bump_slice(); + let semantic = SemanticRepr::record(ordered_field_names); + + let repr = if sortables.len() == 1 { + // If the record has only one field that isn't zero-sized, + // unwrap it. + let inner_repr = sortables.pop().unwrap().1; + inner_repr.newtype() + } else { + let layouts = Vec::from_iter_in(sortables.into_iter().map(|t| t.1), arena); + LayoutRepr::Struct(layouts.into_bump_slice()).direct() + }; + + let result = Ok(env.cache.put_in(Layout { repr, semantic })); + + Cacheable(result, criteria) + } + Tuple(elems, ext_var) => { + let mut criteria = CACHEABLE; + + // extract any values from the ext_var + let mut sortables = Vec::with_capacity_in(elems.len(), arena); + let it = match elems.unsorted_iterator(subs, ext_var) { + Ok(it) => it, + Err(TupleElemsError) => return Cacheable(Err(LayoutProblem::Erroneous), criteria), + }; + + for (index, elem) in it { + let elem_layout = cached!(Layout::from_var(env, elem), criteria, env.subs); + sortables.push((index, elem_layout)); + } + + sortables.sort_by(|(index1, layout1), (index2, layout2)| { + cmp_fields(&env.cache.interner, index1, *layout1, index2, *layout2) + }); + + let result = if sortables.len() == 1 { + // If the tuple has only one field that isn't zero-sized, + // unwrap it. + Ok(sortables.pop().unwrap().1) + } else { + let field_layouts = + Vec::from_iter_in(sortables.into_iter().map(|t| t.1), arena).into_bump_slice(); + let struct_layout = Layout { + repr: LayoutRepr::Struct(field_layouts).direct(), + semantic: SemanticRepr::tuple(field_layouts.len()), + }; + + Ok(env.cache.put_in(struct_layout)) + }; + + Cacheable(result, criteria) + } + TagUnion(tags, ext_var) => { + let (tags, ext_var) = tags.unsorted_tags_and_ext(subs, ext_var); + + debug_assert!(ext_var_is_empty_tag_union(subs, ext_var)); + + layout_from_non_recursive_union(env, &tags, DropUninhabitedVariants(true)).map(Ok) + } + FunctionOrTagUnion(tag_names, _, ext_var) => { + debug_assert!( + ext_var_is_empty_tag_union(subs, ext_var), + "If ext_var wasn't empty, this wouldn't be a FunctionOrTagUnion!" + ); + + let tag_names = subs.get_subs_slice(tag_names); + let unsorted_tags = UnsortedUnionLabels { + tags: tag_names.iter().map(|t| (t, &[] as &[Variable])).collect(), + }; + + layout_from_non_recursive_union(env, &unsorted_tags, DropUninhabitedVariants(true)) + .map(Ok) + } + RecursiveTagUnion(rec_var, tags, ext_var) => { + let (tags, ext_var) = tags.unsorted_tags_and_ext(subs, ext_var); + + debug_assert!(ext_var_is_empty_tag_union(subs, ext_var)); + + layout_from_recursive_union(env, rec_var, &tags) + } + EmptyTagUnion => cacheable(Ok(Layout::VOID)), + EmptyRecord => cacheable(Ok(Layout::UNIT)), + EmptyTuple => cacheable(Ok(Layout::UNIT)), + } +} + +pub type SortedTupleElem<'a> = (usize, Variable, InLayout<'a>); + +pub fn sort_tuple_elems<'a>( + env: &mut Env<'a, '_>, + var: Variable, +) -> Result>, LayoutProblem> { + let (it, _) = match gather_tuple_elems_unsorted_iter(env.subs, TupleElems::empty(), var) { + Ok(it) => it, + Err(_) => return Err(LayoutProblem::Erroneous), + }; + + sort_tuple_elems_help(env, it) +} + +fn sort_tuple_elems_help<'a>( + env: &mut Env<'a, '_>, + elems_map: impl Iterator, +) -> Result>, LayoutProblem> { + let mut sorted_elems = Vec::with_capacity_in(elems_map.size_hint().0, env.arena); + + for (index, elem) in elems_map { + let Cacheable(layout, _) = Layout::from_var(env, elem); + let layout = layout?; + sorted_elems.push((index, elem, layout)); + } + + sorted_elems.sort_by(|(index1, _, res_layout1), (index2, _, res_layout2)| { + cmp_fields( + &env.cache.interner, + index1, + *res_layout1, + index2, + *res_layout2, + ) + }); + + Ok(sorted_elems) +} + +pub type SortedField<'a> = (Lowercase, Variable, Result, InLayout<'a>>); + +pub fn sort_record_fields<'a>( + env: &mut Env<'a, '_>, + var: Variable, +) -> Result>, LayoutProblem> { + let (it, _) = match gather_fields_unsorted_iter(env.subs, RecordFields::empty(), var) { + Ok(it) => it, + Err(_) => return Err(LayoutProblem::Erroneous), + }; + + let it = it + .into_iter() + .map(|(field, field_type)| (field.clone(), field_type)); + + sort_record_fields_help(env, it) +} + +fn sort_record_fields_help<'a>( + env: &mut Env<'a, '_>, + fields_map: impl Iterator)>, +) -> Result>, LayoutProblem> { + // Sort the fields by label + let mut sorted_fields = Vec::with_capacity_in(fields_map.size_hint().0, env.arena); + + for (label, field) in fields_map { + match field { + RecordField::Demanded(v) | RecordField::Required(v) | RecordField::RigidRequired(v) => { + let Cacheable(layout, _) = Layout::from_var(env, v); + let layout = layout?; + sorted_fields.push((label, v, Ok(layout))); + } + RecordField::Optional(v) | RecordField::RigidOptional(v) => { + let Cacheable(layout, _) = Layout::from_var(env, v); + let layout = layout?; + sorted_fields.push((label, v, Err(layout))); + } + }; + } + + sorted_fields.sort_by( + |(label1, _, res_layout1), (label2, _, res_layout2)| match res_layout1 { + Ok(layout1) | Err(layout1) => match res_layout2 { + Ok(layout2) | Err(layout2) => { + cmp_fields(&env.cache.interner, label1, *layout1, label2, *layout2) + } + }, + }, + ); + + Ok(sorted_fields) +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub enum TagOrClosure { + Tag(TagName), + Closure(Symbol), +} + +impl TagOrClosure { + pub fn expect_tag(self) -> TagName { + match self { + Self::Tag(t) => t, + _ => internal_error!("not a tag"), + } + } + pub fn expect_tag_ref(&self) -> &TagName { + match self { + Self::Tag(t) => t, + _ => internal_error!("not a tag"), + } + } +} + +impl From for TagOrClosure { + fn from(t: TagName) -> Self { + Self::Tag(t) + } +} + +impl From for TagOrClosure { + fn from(s: Symbol) -> Self { + Self::Closure(s) + } +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub enum UnionVariant<'a> { + Never, + Unit, + BoolUnion { + ttrue: TagOrClosure, + ffalse: TagOrClosure, + }, + ByteUnion(Vec<'a, TagOrClosure>), + Newtype { + tag_name: TagOrClosure, + arguments: Vec<'a, InLayout<'a>>, + }, + NewtypeByVoid { + data_tag_name: TagOrClosure, + data_tag_id: TagIdIntType, + data_tag_arguments: Vec<'a, InLayout<'a>>, + }, + Wrapped(WrappedVariant<'a>), +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub enum WrappedVariant<'a> { + Recursive { + sorted_tag_layouts: Vec<'a, (TagOrClosure, &'a [InLayout<'a>])>, + }, + NonRecursive { + sorted_tag_layouts: Vec<'a, (TagOrClosure, &'a [InLayout<'a>])>, + }, + NullableWrapped { + nullable_id: TagIdIntType, + nullable_name: TagOrClosure, + sorted_tag_layouts: Vec<'a, (TagOrClosure, &'a [InLayout<'a>])>, + }, + NonNullableUnwrapped { + tag_name: TagOrClosure, + fields: &'a [InLayout<'a>], + }, + NullableUnwrapped { + nullable_id: bool, + nullable_name: TagOrClosure, + other_name: TagOrClosure, + other_fields: &'a [InLayout<'a>], + }, +} + +impl<'a> WrappedVariant<'a> { + pub fn tag_name_to_id(&self, tag_name: &TagName) -> (TagIdIntType, &'a [InLayout<'a>]) { + use WrappedVariant::*; + + match self { + Recursive { sorted_tag_layouts } | NonRecursive { sorted_tag_layouts } => { + let (tag_id, (_, argument_layouts)) = sorted_tag_layouts + .iter() + .enumerate() + .find(|(_, (key, _))| key.expect_tag_ref() == tag_name) + .expect("tag name is not in its own type"); + + debug_assert!(tag_id < 256); + (tag_id as TagIdIntType, *argument_layouts) + } + NullableWrapped { + nullable_id, + nullable_name, + sorted_tag_layouts, + } => { + // assumption: the nullable_name is not included in sorted_tag_layouts + + if tag_name == nullable_name.expect_tag_ref() { + (*nullable_id as TagIdIntType, &[] as &[_]) + } else { + let (mut tag_id, (_, argument_layouts)) = sorted_tag_layouts + .iter() + .enumerate() + .find(|(_, (key, _))| key.expect_tag_ref() == tag_name) + .expect("tag name is not in its own type"); + + if tag_id >= *nullable_id as usize { + tag_id += 1; + } + + debug_assert!(tag_id < 256); + (tag_id as TagIdIntType, *argument_layouts) + } + } + NullableUnwrapped { + nullable_id, + nullable_name, + other_name, + other_fields, + } => { + if tag_name == nullable_name.expect_tag_ref() { + (*nullable_id as TagIdIntType, &[] as &[_]) + } else { + debug_assert_eq!(other_name.expect_tag_ref(), tag_name); + + (!*nullable_id as TagIdIntType, *other_fields) + } + } + NonNullableUnwrapped { fields, .. } => (0, fields), + } + } + + pub fn number_of_tags(&'a self) -> usize { + use WrappedVariant::*; + + match self { + Recursive { sorted_tag_layouts } | NonRecursive { sorted_tag_layouts } => { + sorted_tag_layouts.len() + } + NullableWrapped { + sorted_tag_layouts, .. + } => { + // assumption: the nullable_name is not included in sorted_tag_layouts + + sorted_tag_layouts.len() + 1 + } + NullableUnwrapped { .. } => 2, + NonNullableUnwrapped { .. } => 1, + } + } +} + +pub fn union_sorted_tags<'a>( + env: &mut Env<'a, '_>, + var: Variable, +) -> Result, LayoutProblem> { + use roc_types::pretty_print::ChasedExt; + use Content::*; + + let var = if let Content::RecursionVar { structure, .. } = + env.subs.get_content_without_compacting(var) + { + *structure + } else { + var + }; + + let drop_uninhabited_variants = DropUninhabitedVariants(true); + + let mut tags_vec = std::vec::Vec::new(); + let result = match roc_types::pretty_print::chase_ext_tag_union(env.subs, var, &mut tags_vec) { + ChasedExt::Empty => { + let opt_rec_var = get_recursion_var(env.subs, var); + let Cacheable(result, _) = + union_sorted_tags_help(env, tags_vec, opt_rec_var, drop_uninhabited_variants); + result + } + ChasedExt::NonEmpty { content, .. } => { + match content { + FlexVar(_) | FlexAbleVar(..) | RigidVar(_) | RigidAbleVar(..) => { + // Admit type variables in the extension for now. This may come from things that never got + // monomorphized, like in + // x : [A]* + // x = A + // x + // In such cases it's fine to drop the variable. We may be proven wrong in the future... + let opt_rec_var = get_recursion_var(env.subs, var); + let Cacheable(result, _) = union_sorted_tags_help( + env, + tags_vec, + opt_rec_var, + drop_uninhabited_variants, + ); + result + } + RecursionVar { .. } => { + let opt_rec_var = get_recursion_var(env.subs, var); + let Cacheable(result, _) = union_sorted_tags_help( + env, + tags_vec, + opt_rec_var, + drop_uninhabited_variants, + ); + result + } + + Error => return Err(LayoutProblem::Erroneous), + + other => panic!("invalid content in tag union variable: {other:?}"), + } + } + }; + + Ok(result) +} + +fn get_recursion_var(subs: &Subs, var: Variable) -> Option { + match subs.get_content_without_compacting(var) { + Content::Structure(FlatType::RecursiveTagUnion(rec_var, _, _)) => Some(*rec_var), + Content::Alias(_, _, actual, _) => get_recursion_var(subs, *actual), + _ => None, + } +} + +trait Label: subs::Label + Ord + Clone + Into { + fn semantic_repr<'a, 'r>( + arena: &'a Bump, + labels: impl ExactSizeIterator, + ) -> SemanticRepr<'a> + where + Self: 'r; +} + +impl Label for TagName { + fn semantic_repr<'a, 'r>( + arena: &'a Bump, + labels: impl ExactSizeIterator, + ) -> SemanticRepr<'a> { + SemanticRepr::tag_union( + arena.alloc_slice_fill_iter(labels.map(|x| &*arena.alloc_str(x.0.as_str()))), + ) + } +} + +impl Label for Symbol { + fn semantic_repr<'a, 'r>( + arena: &'a Bump, + labels: impl ExactSizeIterator, + ) -> SemanticRepr<'a> { + SemanticRepr::lambdas(arena.alloc_slice_fill_iter(labels.copied())) + } +} + +struct DropUninhabitedVariants(bool); + +fn union_sorted_non_recursive_tags_help<'a, L>( + env: &mut Env<'a, '_>, + tags_list: &mut Vec<'_, &'_ (&'_ L, &[Variable])>, + drop_uninhabited_variants: DropUninhabitedVariants, +) -> Cacheable> +where + L: Label + Ord + Clone + Into, +{ + let mut cache_criteria = CACHEABLE; + + // sort up front; make sure the ordering stays intact! + tags_list.sort_unstable_by(|(a, _), (b, _)| a.cmp(b)); + + match tags_list.len() { + 0 => { + // trying to instantiate a type with no values + Cacheable(UnionVariant::Never, cache_criteria) + } + 1 => { + let &&(tag_name, arguments) = &tags_list[0]; + let tag_name = tag_name.clone().into(); + + // just one tag in the union (but with arguments) can be a struct + let mut layouts = Vec::with_capacity_in(tags_list.len(), env.arena); + + for &var in arguments { + let Cacheable(result, criteria) = Layout::from_var(env, var); + cache_criteria.and(criteria, env.subs); + match result { + Ok(layout) => { + layouts.push(layout); + } + Err(LayoutProblem::UnresolvedTypeVar(_)) => { + // If we encounter an unbound type var (e.g. `Ok *`) + // then it's zero-sized; In the future we may drop this argument + // completely, but for now we represent it with the empty tag union + layouts.push(Layout::VOID) + } + Err(LayoutProblem::Erroneous) => { + // An erroneous type var will code gen to a runtime + // error, so we don't need to store any data for it. + } + } + } + + layouts.sort_by(|layout1, layout2| { + let size1 = env + .cache + .get_repr(*layout1) + .alignment_bytes(&env.cache.interner); + let size2 = env + .cache + .get_repr(*layout2) + .alignment_bytes(&env.cache.interner); + + size2.cmp(&size1) + }); + + if layouts.is_empty() { + Cacheable(UnionVariant::Unit, cache_criteria) + } else { + Cacheable( + UnionVariant::Newtype { + tag_name, + arguments: layouts, + }, + cache_criteria, + ) + } + } + num_tags => { + // default path + let mut answer: Vec<(TagOrClosure, &[InLayout])> = + Vec::with_capacity_in(tags_list.len(), env.arena); + let mut has_any_arguments = false; + + let mut inhabited_tag_ids = BitVec::::repeat(true, num_tags); + + for &&(tag_name, arguments) in tags_list.iter() { + let mut arg_layouts = Vec::with_capacity_in(arguments.len() + 1, env.arena); + + for &var in arguments { + let Cacheable(result, criteria) = Layout::from_var(env, var); + cache_criteria.and(criteria, env.subs); + match result { + Ok(layout) => { + has_any_arguments = true; + + arg_layouts.push(layout); + + if layout == Layout::VOID { + inhabited_tag_ids.set(answer.len(), false); + } + } + Err(LayoutProblem::UnresolvedTypeVar(_)) => { + // If we encounter an unbound type var (e.g. `Ok *`) + // then it's zero-sized; In the future we may drop this argument + // completely, but for now we represent it with the empty tag union + arg_layouts.push(Layout::VOID) + } + Err(LayoutProblem::Erroneous) => { + // An erroneous type var will code gen to a runtime + // error, so we don't need to store any data for it. + } + } + } + + arg_layouts.sort_by(|layout1, layout2| { + let size1 = env + .cache + .get_repr(*layout1) + .alignment_bytes(&env.cache.interner); + let size2 = env + .cache + .get_repr(*layout2) + .alignment_bytes(&env.cache.interner); + + size2.cmp(&size1) + }); + + answer.push((tag_name.clone().into(), arg_layouts.into_bump_slice())); + } + + if inhabited_tag_ids.count_ones() == 1 && drop_uninhabited_variants.0 { + let kept_tag_id = inhabited_tag_ids.first_one().unwrap(); + let kept = answer.get(kept_tag_id).unwrap(); + + let variant = UnionVariant::NewtypeByVoid { + data_tag_name: kept.0.clone(), + data_tag_id: kept_tag_id as _, + data_tag_arguments: Vec::from_iter_in(kept.1.iter().copied(), env.arena), + }; + return Cacheable(variant, cache_criteria); + } + + match num_tags { + 2 if !has_any_arguments => { + // type can be stored in a boolean + + // tags_vec is sorted, and answer is sorted the same way + let ttrue = answer.remove(1).0; + let ffalse = answer.remove(0).0; + + Cacheable(UnionVariant::BoolUnion { ffalse, ttrue }, cache_criteria) + } + 3..=MAX_ENUM_SIZE if !has_any_arguments => { + // type can be stored in a byte + // needs the sorted tag names to determine the tag_id + let mut tag_names = Vec::with_capacity_in(answer.len(), env.arena); + + for (tag_name, _) in answer { + tag_names.push(tag_name); + } + + Cacheable(UnionVariant::ByteUnion(tag_names), cache_criteria) + } + _ => { + let variant = WrappedVariant::NonRecursive { + sorted_tag_layouts: answer, + }; + + Cacheable(UnionVariant::Wrapped(variant), cache_criteria) + } + } + } + } +} + +pub fn union_sorted_tags_pub<'a, L>( + env: &mut Env<'a, '_>, + tags_vec: std::vec::Vec<(L, std::vec::Vec)>, + opt_rec_var: Option, +) -> UnionVariant<'a> +where + L: Into + Ord + Clone, +{ + union_sorted_tags_help(env, tags_vec, opt_rec_var, DropUninhabitedVariants(true)).value() +} + +fn find_nullable_tag<'a, L, I>(tags: I) -> Option<(TagIdIntType, L)> +where + I: Iterator, + L: Into + Ord + Clone + 'a, +{ + let mut length = 0; + let mut has_payload = 0; + let mut nullable = None; + + for (index, (name, variables)) in tags.enumerate() { + length += 1; + + if variables.is_empty() { + nullable = nullable.or_else(|| Some((index as TagIdIntType, name.clone()))); + } else { + has_payload += 1; + } + } + + let has_no_payload = length - has_payload; + + // in the scenario of `[ A Str, B, C, D ]`, rather than having one tag be nullable, we want + // to store the tag id in the pointer. (we want NonNullableUnwrapped, not NullableWrapped) + if (has_payload > 1 && has_no_payload > 0) || has_no_payload == 1 { + nullable + } else { + None + } +} + +fn union_sorted_tags_help<'a, L>( + env: &mut Env<'a, '_>, + mut tags_vec: std::vec::Vec<(L, std::vec::Vec)>, + opt_rec_var: Option, + drop_uninhabited_variants: DropUninhabitedVariants, +) -> Cacheable> +where + L: Into + Ord + Clone, +{ + // sort up front; make sure the ordering stays intact! + tags_vec.sort_unstable_by(|(a, _), (b, _)| a.cmp(b)); + + let mut cache_criteria = CACHEABLE; + + match tags_vec.len() { + 0 => { + // trying to instantiate a type with no values + Cacheable(UnionVariant::Never, cache_criteria) + } + 1 => { + let (tag_name, arguments) = tags_vec.remove(0); + + // just one tag in the union (but with arguments) can be a struct + let mut layouts = Vec::with_capacity_in(tags_vec.len(), env.arena); + + for var in arguments { + let Cacheable(result, criteria) = Layout::from_var(env, var); + cache_criteria.and(criteria, env.subs); + match result { + Ok(layout) => { + layouts.push(layout); + } + Err(LayoutProblem::UnresolvedTypeVar(_)) => { + // If we encounter an unbound type var (e.g. `Ok *`) + // then it's zero-sized; In the future we may drop this argument + // completely, but for now we represent it with the empty tag union + layouts.push(Layout::VOID) + } + Err(LayoutProblem::Erroneous) => { + // An erroneous type var will code gen to a runtime + // error, so we don't need to store any data for it. + } + } + } + + layouts.sort_by(|layout1, layout2| { + let size1 = env.cache.interner.alignment_bytes(*layout1); + let size2 = env.cache.interner.alignment_bytes(*layout2); + + size2.cmp(&size1) + }); + + if layouts.is_empty() { + Cacheable(UnionVariant::Unit, cache_criteria) + } else if let Some(rec_var) = opt_rec_var { + let variant = UnionVariant::Wrapped(WrappedVariant::NonNullableUnwrapped { + tag_name: tag_name.into(), + fields: layouts.into_bump_slice(), + }); + cache_criteria.pass_through_recursive_union(rec_var); + Cacheable(variant, cache_criteria) + } else { + Cacheable( + UnionVariant::Newtype { + tag_name: tag_name.into(), + arguments: layouts, + }, + cache_criteria, + ) + } + } + num_tags => { + // default path + let mut answer = Vec::with_capacity_in(tags_vec.len(), env.arena); + let mut has_any_arguments = false; + + let mut nullable = None; + let mut inhabited_tag_ids = BitVec::::repeat(true, num_tags); + + // only recursive tag unions can be nullable + let is_recursive = opt_rec_var.is_some(); + if is_recursive && GENERATE_NULLABLE { + nullable = find_nullable_tag(tags_vec.iter().map(|(a, b)| (a, b.as_slice()))); + } + + for (index, (tag_name, arguments)) in tags_vec.into_iter().enumerate() { + // reserve space for the tag discriminant + if matches!(nullable, Some((i, _)) if i as usize == index) { + debug_assert!(arguments.is_empty()); + continue; + } + + let mut arg_layouts = Vec::with_capacity_in(arguments.len() + 1, env.arena); + + for var in arguments { + let Cacheable(result, criteria) = Layout::from_var(env, var); + cache_criteria.and(criteria, env.subs); + match result { + Ok(in_layout) => { + has_any_arguments = true; + + let layout = env.cache.get_in(in_layout); + + // make sure to not unroll recursive types! + let self_recursion = opt_rec_var.is_some() + && env.subs.get_root_key_without_compacting(var) + == env + .subs + .get_root_key_without_compacting(opt_rec_var.unwrap()) + && layout.is_recursive_tag_union(&env.cache.interner); + + let arg_layout = if self_recursion { + Layout::NAKED_RECURSIVE_PTR + } else { + in_layout + }; + arg_layouts.push(arg_layout); + + if layout == Layout::VOID_NAKED { + inhabited_tag_ids.set(answer.len(), false); + } + } + Err(LayoutProblem::UnresolvedTypeVar(_)) => { + // If we encounter an unbound type var (e.g. `Ok *`) + // then it's zero-sized; In the future we may drop this argument + // completely, but for now we represent it with the empty struct tag + // union + arg_layouts.push(Layout::VOID); + } + Err(LayoutProblem::Erroneous) => { + // An erroneous type var will code gen to a runtime + // error, so we don't need to store any data for it. + } + } + } + + arg_layouts.sort_by(|layout1, layout2| { + let size1 = env + .cache + .get_repr(*layout1) + .alignment_bytes(&env.cache.interner); + let size2 = env + .cache + .get_repr(*layout2) + .alignment_bytes(&env.cache.interner); + + size2.cmp(&size1) + }); + + answer.push((tag_name.into(), arg_layouts.into_bump_slice())); + } + + if inhabited_tag_ids.count_ones() == 1 && !is_recursive && drop_uninhabited_variants.0 { + let kept_tag_id = inhabited_tag_ids.first_one().unwrap(); + let kept = answer.get(kept_tag_id).unwrap(); + + let variant = UnionVariant::NewtypeByVoid { + data_tag_name: kept.0.clone(), + data_tag_id: kept_tag_id as _, + data_tag_arguments: Vec::from_iter_in(kept.1.iter().copied(), env.arena), + }; + return Cacheable(variant, cache_criteria); + } + + match num_tags { + 2 if !has_any_arguments => { + // type can be stored in a boolean + + // tags_vec is sorted, and answer is sorted the same way + let ttrue = answer.remove(1).0; + let ffalse = answer.remove(0).0; + + Cacheable(UnionVariant::BoolUnion { ffalse, ttrue }, cache_criteria) + } + 3..=MAX_ENUM_SIZE if !has_any_arguments => { + // type can be stored in a byte + // needs the sorted tag names to determine the tag_id + let mut tag_names = Vec::with_capacity_in(answer.len(), env.arena); + + for (tag_name, _) in answer { + tag_names.push(tag_name); + } + + Cacheable(UnionVariant::ByteUnion(tag_names), cache_criteria) + } + _ => { + let variant = if let Some((nullable_id, nullable_name)) = nullable { + if answer.len() == 1 { + let (other_name, other_arguments) = answer.drain(..).next().unwrap(); + let nullable_id = nullable_id != 0; + + WrappedVariant::NullableUnwrapped { + nullable_id, + nullable_name: nullable_name.into(), + other_name, + other_fields: other_arguments, + } + } else { + WrappedVariant::NullableWrapped { + nullable_id, + nullable_name: nullable_name.into(), + sorted_tag_layouts: answer, + } + } + } else if is_recursive { + debug_assert!(answer.len() > 1); + WrappedVariant::Recursive { + sorted_tag_layouts: answer, + } + } else { + WrappedVariant::NonRecursive { + sorted_tag_layouts: answer, + } + }; + + if let Some(rec_var) = opt_rec_var { + cache_criteria.pass_through_recursive_union(rec_var); + debug_assert!(!matches!(variant, WrappedVariant::NonRecursive { .. })); + } + + Cacheable(UnionVariant::Wrapped(variant), cache_criteria) + } + } + } + } +} + +fn layout_from_newtype<'a, L: Label>( + env: &mut Env<'a, '_>, + tags: &UnsortedUnionLabels, +) -> Cacheable> { + debug_assert!(tags.is_newtype_wrapper(env.subs)); + + let (_tag_name, var) = tags.get_newtype(env.subs); + + let Cacheable(result, criteria) = Layout::from_var(env, var); + match result { + Ok(layout) => Cacheable(layout, criteria), + Err(LayoutProblem::UnresolvedTypeVar(_)) => { + // If we encounter an unbound type var (e.g. `Ok *`) + // then it's zero-sized; In the future we may drop this argument + // completely, but for now we represent it with the empty tag union + Cacheable(Layout::VOID, criteria) + } + Err(LayoutProblem::Erroneous) => { + // An erroneous type var will code gen to a runtime + // error, so we don't need to store any data for it. + todo!() + } + } +} + +fn layout_from_non_recursive_union<'a, L>( + env: &mut Env<'a, '_>, + tags: &UnsortedUnionLabels, + drop_uninhabited_variants: DropUninhabitedVariants, +) -> Cacheable> +where + L: Label + Ord + Into, +{ + use UnionVariant::*; + + if tags.is_newtype_wrapper(env.subs) { + return layout_from_newtype(env, tags); + } + + let mut tags_vec = Vec::from_iter_in(tags.tags.iter(), env.arena); + + let mut criteria = CACHEABLE; + + let variant = + union_sorted_non_recursive_tags_help(env, &mut tags_vec, drop_uninhabited_variants) + .decompose(&mut criteria, env.subs); + + let compute_semantic = || L::semantic_repr(env.arena, tags_vec.iter().map(|(l, _)| *l)); + + let result = match variant { + Never => Layout::VOID, + Unit => env + .cache + .put_in(Layout::new(LayoutRepr::UNIT.direct(), compute_semantic())), + BoolUnion { .. } => env + .cache + .put_in(Layout::new(LayoutRepr::BOOL.direct(), compute_semantic())), + ByteUnion(_) => env + .cache + .put_in(Layout::new(LayoutRepr::U8.direct(), compute_semantic())), + Newtype { + arguments: field_layouts, + .. + } => { + let answer1 = if field_layouts.len() == 1 { + field_layouts[0] + } else { + env.cache + .put_in_direct_no_semantic(LayoutRepr::struct_(field_layouts.into_bump_slice())) + }; + + answer1 + } + NewtypeByVoid { + data_tag_arguments, .. + } => { + if data_tag_arguments.len() == 1 { + data_tag_arguments[0] + } else { + env.cache.put_in_direct_no_semantic(LayoutRepr::struct_( + data_tag_arguments.into_bump_slice(), + )) + } + } + Wrapped(variant) => { + use WrappedVariant::*; + + match variant { + NonRecursive { + sorted_tag_layouts: tags, + } => { + let mut tag_layouts = Vec::with_capacity_in(tags.len(), env.arena); + tag_layouts.extend(tags.iter().map(|r| r.1)); + + let layout = Layout { + repr: LayoutRepr::Union(UnionLayout::NonRecursive( + tag_layouts.into_bump_slice(), + )) + .direct(), + semantic: SemanticRepr::NONE, + }; + env.cache.put_in(layout) + } + + Recursive { .. } + | NullableWrapped { .. } + | NullableUnwrapped { .. } + | NonNullableUnwrapped { .. } => { + internal_error!("non-recursive tag union has recursive layout") + } + } + } + }; + + Cacheable(result, criteria) +} + +fn layout_from_recursive_union<'a, L>( + env: &mut Env<'a, '_>, + rec_var: Variable, + tags: &UnsortedUnionLabels, +) -> Cacheable> +where + L: Label + Ord + Into, +{ + let arena = env.arena; + let subs = env.subs; + + let mut criteria = CACHEABLE; + + // some observations + // + // * recursive tag unions are always recursive + // * therefore at least one tag has a pointer (non-zero sized) field + // * they must (to be instantiated) have 2 or more tags + // + // That means none of the optimizations for enums or single tag tag unions apply + + let rec_var = subs.get_root_key_without_compacting(rec_var); + let tags_vec = &tags.tags; + let mut tag_layouts = Vec::with_capacity_in(tags_vec.len(), arena); + + let mut nullable = None; + + if GENERATE_NULLABLE { + nullable = find_nullable_tag(tags_vec.iter().map(|(a, b)| (*a, *b))); + } + + env.insert_seen(rec_var); + for (index, &(_name, variables)) in tags_vec.iter().enumerate() { + if matches!(nullable, Some((i, _)) if i == index as TagIdIntType) { + // don't add the nullable case + continue; + } + + let mut tag_layout = Vec::with_capacity_in(variables.len() + 1, arena); + + for &var in variables { + // TODO does this cause problems with mutually recursive unions? + if rec_var == subs.get_root_key_without_compacting(var) { + // The naked pointer will get fixed-up to loopback to the union below when we + // intern the union. + tag_layout.push(Layout::NAKED_RECURSIVE_PTR); + criteria.and(NAKED_RECURSION_PTR, env.subs); + continue; + } + + let payload = cached!(Layout::from_var(env, var), criteria, env.subs); + tag_layout.push(payload); + } + + tag_layout.sort_by(|layout1, layout2| { + // TODO(intern-layouts): provide alignment bytes on interner + let size1 = env.cache.interner.alignment_bytes(*layout1); + let size2 = env.cache.interner.alignment_bytes(*layout2); + + size2.cmp(&size1) + }); + + tag_layouts.push(tag_layout.into_bump_slice()); + } + env.remove_seen(rec_var); + + let union_layout = if let Some((tag_id, _)) = nullable { + match tag_layouts.into_bump_slice() { + [one] => { + let nullable_id = tag_id != 0; + + UnionLayout::NullableUnwrapped { + nullable_id, + other_fields: one, + } + } + many => UnionLayout::NullableWrapped { + nullable_id: tag_id, + other_tags: many, + }, + } + } else if tag_layouts.len() == 1 { + // drop the tag id + UnionLayout::NonNullableUnwrapped(tag_layouts.pop().unwrap()) + } else { + UnionLayout::Recursive(tag_layouts.into_bump_slice()) + }; + + let union_layout = if criteria.has_naked_recursion_pointer { + env.cache.interner.insert_recursive( + env.arena, + Layout { + repr: LayoutRepr::Union(union_layout).direct(), + semantic: SemanticRepr::NONE, + }, + ) + } else { + // There are no naked recursion pointers, so we can insert the layout as-is. + env.cache.interner.insert(Layout { + repr: LayoutRepr::Union(union_layout).direct(), + semantic: SemanticRepr::NONE, + }) + }; + + criteria.pass_through_recursive_union(rec_var); + + Cacheable(Ok(union_layout), criteria) +} + +#[cfg(debug_assertions)] +pub fn ext_var_is_empty_record(subs: &Subs, ext_var: Variable) -> bool { + // the ext_var is empty + let fields = match roc_types::types::gather_fields(subs, RecordFields::empty(), ext_var) { + Ok(fields) => fields, + Err(_) => return false, + }; + + fields.fields.is_empty() +} + +#[cfg(not(debug_assertions))] +pub fn ext_var_is_empty_record(_subs: &Subs, _ext_var: Variable) -> bool { + // This should only ever be used in debug_assert! macros + unreachable!(); +} + +#[cfg(debug_assertions)] +pub fn ext_var_is_empty_tag_union(subs: &Subs, tag_ext: TagExt) -> bool { + use roc_types::pretty_print::ChasedExt; + use Content::*; + + // the ext_var is empty + let mut ext_fields = std::vec::Vec::new(); + match roc_types::pretty_print::chase_ext_tag_union(subs, tag_ext.var(), &mut ext_fields) { + ChasedExt::Empty => ext_fields.is_empty(), + ChasedExt::NonEmpty { content, .. } => { + match content { + // Allow flex/rigid to decay away into nothing + FlexVar(_) | FlexAbleVar(..) | RigidVar(_) | RigidAbleVar(..) => { + ext_fields.is_empty() + } + // So that we can continue compiling in the presence of errors + Error => ext_fields.is_empty(), + _ => panic!("invalid content in ext_var: {content:?}"), + } + } + } +} + +#[cfg(not(debug_assertions))] +pub fn ext_var_is_empty_tag_union(_: &Subs, _: TagExt) -> bool { + // This should only ever be used in debug_assert! macros + unreachable!(); +} + +fn layout_from_num_content<'a>( + content: &Content, + target_info: TargetInfo, +) -> Cacheable> { + use roc_types::subs::Content::*; + use roc_types::subs::FlatType::*; + + let result = match content { + RecursionVar { .. } => panic!("recursion var in num"), + FlexVar(_) | RigidVar(_) => { + // If a Num makes it all the way through type checking with an unbound + // type variable, then assume it's a 64-bit integer. + // + // (e.g. for (5 + 5) assume both 5s are 64-bit integers.) + Ok(Layout::default_integer()) + } + FlexAbleVar(_, _) | RigidAbleVar(_, _) => todo_abilities!("Not reachable yet"), + Structure(Apply(symbol, args)) => match *symbol { + // Ints + Symbol::NUM_NAT => Ok(Layout::usize(target_info)), + + Symbol::NUM_INTEGER => Ok(Layout::I64), + Symbol::NUM_I128 => Ok(Layout::I128), + Symbol::NUM_I64 => Ok(Layout::I64), + Symbol::NUM_I32 => Ok(Layout::I32), + Symbol::NUM_I16 => Ok(Layout::I16), + Symbol::NUM_I8 => Ok(Layout::I8), + + Symbol::NUM_U128 => Ok(Layout::U128), + Symbol::NUM_U64 => Ok(Layout::U64), + Symbol::NUM_U32 => Ok(Layout::U32), + Symbol::NUM_U16 => Ok(Layout::U16), + Symbol::NUM_U8 => Ok(Layout::U8), + + // Floats + Symbol::NUM_FLOATINGPOINT => Ok(Layout::F64), + Symbol::NUM_F64 => Ok(Layout::F64), + Symbol::NUM_F32 => Ok(Layout::F32), + + // Dec + Symbol::NUM_DEC => Ok(Layout::DEC), + + _ => { + panic!("Invalid Num.Num type application: Apply({symbol:?}, {args:?})"); + } + }, + Alias(_, _, _, _) => { + todo!("TODO recursively resolve type aliases in num_from_content"); + } + Structure(_) | RangedNumber(..) | LambdaSet(_) | ErasedLambda => { + panic!("Invalid Num.Num type application: {content:?}"); + } + Error => Err(LayoutProblem::Erroneous), + }; + cacheable(result) +} + +pub(crate) fn list_layout_from_elem<'a>( + env: &mut Env<'a, '_>, + element_var: Variable, +) -> Cacheable> { + let mut criteria = CACHEABLE; + + let is_variable = |content| matches!(content, &Content::FlexVar(_) | &Content::RigidVar(_)); + + let element_content = env.subs.get_content_without_compacting(element_var); + + let element_layout = if is_variable(element_content) { + // If this was still a (List *) then it must have been an empty list + Layout::VOID + } else { + // NOTE: cannot re-use Content, because it may be recursive + // then some state is not correctly kept, we have to go through from_var + cached!(Layout::from_var(env, element_var), criteria, env.subs) + }; + + let list_layout = env.cache.put_in(Layout { + repr: LayoutRepr::Builtin(Builtin::List(element_layout)).direct(), + semantic: SemanticRepr::NONE, + }); + + Cacheable(Ok(list_layout), criteria) +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct LayoutId(u32); + +impl LayoutId { + // Returns something like "#UserApp_foo_1" when given a symbol that interns to "foo" + // and a LayoutId of 1. + pub fn to_symbol_string(self, symbol: Symbol, interns: &Interns) -> String { + let ident_string = symbol.as_str(interns); + let module_string = interns.module_ids.get_name(symbol.module_id()).unwrap(); + format!("{}_{}_{}", module_string, ident_string, self.0) + } + + // Returns something like "roc__foo_1_exposed" when given a symbol that interns to "foo" + // and a LayoutId of 1. + pub fn to_exposed_symbol_string(self, symbol: Symbol, interns: &Interns) -> String { + let ident_string = symbol.as_str(interns); + format!("roc__{}_{}_exposed", ident_string, self.0) + } + + pub fn to_exposed_generic_symbol_string(self, symbol: Symbol, interns: &Interns) -> String { + let ident_string = symbol.as_str(interns); + format!("roc__{}_{}_exposed_generic", ident_string, self.0) + } +} + +struct IdsByLayout<'a> { + by_id: MutMap, u32>, + toplevels_by_id: MutMap, u32>, + next_id: u32, +} + +impl<'a> IdsByLayout<'a> { + #[inline(always)] + fn insert_layout(&mut self, layout: LayoutRepr<'a>) -> LayoutId { + match self.by_id.entry(layout) { + Entry::Vacant(vacant) => { + let answer = self.next_id; + vacant.insert(answer); + self.next_id += 1; + + LayoutId(answer) + } + Entry::Occupied(occupied) => LayoutId(*occupied.get()), + } + } + + #[inline(always)] + fn singleton_layout(layout: LayoutRepr<'a>) -> (Self, LayoutId) { + let mut by_id = HashMap::with_capacity_and_hasher(1, default_hasher()); + by_id.insert(layout, 1); + + let ids_by_layout = IdsByLayout { + by_id, + toplevels_by_id: Default::default(), + next_id: 2, + }; + + (ids_by_layout, LayoutId(1)) + } + + #[inline(always)] + fn insert_toplevel(&mut self, layout: crate::ir::ProcLayout<'a>) -> LayoutId { + match self.toplevels_by_id.entry(layout) { + Entry::Vacant(vacant) => { + let answer = self.next_id; + vacant.insert(answer); + self.next_id += 1; + + LayoutId(answer) + } + Entry::Occupied(occupied) => LayoutId(*occupied.get()), + } + } + + #[inline(always)] + fn singleton_toplevel(layout: crate::ir::ProcLayout<'a>) -> (Self, LayoutId) { + let mut toplevels_by_id = HashMap::with_capacity_and_hasher(1, default_hasher()); + toplevels_by_id.insert(layout, 1); + + let ids_by_layout = IdsByLayout { + by_id: Default::default(), + toplevels_by_id, + next_id: 2, + }; + + (ids_by_layout, LayoutId(1)) + } +} + +#[derive(Default)] +pub struct LayoutIds<'a> { + by_symbol: MutMap>, +} + +impl<'a> LayoutIds<'a> { + /// Returns a LayoutId which is unique for the given symbol and layout. + /// If given the same symbol and same layout, returns the same LayoutId. + #[inline(always)] + pub fn get<'b>(&mut self, symbol: Symbol, layout: &'b LayoutRepr<'a>) -> LayoutId { + match self.by_symbol.entry(symbol) { + Entry::Vacant(vacant) => { + let (ids_by_layout, layout_id) = IdsByLayout::singleton_layout(*layout); + + vacant.insert(ids_by_layout); + + layout_id + } + Entry::Occupied(mut occupied_ids) => occupied_ids.get_mut().insert_layout(*layout), + } + } + + /// Returns a LayoutId which is unique for the given symbol and layout. + /// If given the same symbol and same layout, returns the same LayoutId. + #[inline(always)] + pub fn get_toplevel<'b>( + &mut self, + symbol: Symbol, + layout: &'b crate::ir::ProcLayout<'a>, + ) -> LayoutId { + match self.by_symbol.entry(symbol) { + Entry::Vacant(vacant) => { + let (ids_by_layout, layout_id) = IdsByLayout::singleton_toplevel(*layout); + + vacant.insert(ids_by_layout); + + layout_id + } + Entry::Occupied(mut occupied_ids) => occupied_ids.get_mut().insert_toplevel(*layout), + } + } +} + +/// Compare two fields when sorting them for code gen. +/// This is called by both code gen and glue, so that +/// their field orderings agree. +#[inline(always)] +pub fn cmp_fields<'a, L: Ord, I>( + interner: &I, + label1: &L, + layout1: InLayout<'a>, + label2: &L, + layout2: InLayout<'a>, +) -> Ordering +where + I: LayoutInterner<'a>, +{ + let size1 = interner.get_repr(layout1).alignment_bytes(interner); + let size2 = interner.get_repr(layout2).alignment_bytes(interner); + + size2.cmp(&size1).then(label1.cmp(label2)) +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn width_and_alignment_union_empty_struct() { + let mut interner = STLayoutInterner::with_capacity(4, TargetInfo::default_x86_64()); + + let lambda_set = LambdaSet { + args: &(&[] as &[InLayout]), + ret: Layout::VOID, + set: &(&[(Symbol::LIST_MAP, &[] as &[InLayout])] as &[(Symbol, &[InLayout])]), + representation: Layout::UNIT, + full_layout: Layout::VOID, + }; + + let a = &[Layout::UNIT] as &[_]; + let b = &[interner.insert(Layout { + repr: LayoutRepr::LambdaSet(lambda_set).direct(), + semantic: SemanticRepr::NONE, + })] as &[_]; + let tt = [a, b]; + + let repr = LayoutRepr::Union(UnionLayout::NonRecursive(&tt)); + + assert_eq!(repr.stack_size(&interner), 1); + assert_eq!(repr.alignment_bytes(&interner), 1); + } + + #[test] + fn memcpy_size_result_u32_unit() { + let mut interner = STLayoutInterner::with_capacity(4, TargetInfo::default_x86_64()); + + let ok_tag = &[interner.insert(Layout { + repr: LayoutRepr::Builtin(Builtin::Int(IntWidth::U32)).direct(), + semantic: SemanticRepr::NONE, + })]; + let err_tag = &[Layout::UNIT]; + let tags = [ok_tag as &[_], err_tag as &[_]]; + let union_layout = UnionLayout::NonRecursive(&tags as &[_]); + let repr = LayoutRepr::Union(union_layout); + + assert_eq!(repr.stack_size_without_alignment(&interner), 8); + } + + #[test] + fn void_stack_size() { + let interner = STLayoutInterner::with_capacity(4, TargetInfo::default_x86_64()); + assert_eq!(Layout::VOID_NAKED.repr(&interner).stack_size(&interner), 0); + } + + #[test] + fn align_u128_in_tag_union() { + let interner = STLayoutInterner::with_capacity(4, TargetInfo::default_x86_64()); + assert_eq!(interner.alignment_bytes(Layout::U128), 16); + } +} diff --git a/crates/compiler/mono/src/layout/erased.rs b/crates/compiler/mono/src/layout/erased.rs new file mode 100644 index 0000000000..85bcabcd31 --- /dev/null +++ b/crates/compiler/mono/src/layout/erased.rs @@ -0,0 +1,72 @@ +use roc_target::TargetInfo; + +use super::{InLayout, LayoutRepr, UnionLayout}; + +/// The layout of an erasure. +/// +/// A type-erased value consists of three fields at runtime: +/// +/// ```text +/// { +/// // the material value being erased. +/// // if the erasure is a function, this is the captured environment, or null. +/// value: void*, +/// +/// // if the erasure is a function, the function pointer, or null otherwise. +/// callee: void*, +/// +/// // the refcounter for the material value, or null if there is no material value. +/// refcounter: void*, +/// } +/// ``` +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Erased; + +impl Erased { + pub fn safe_to_memcpy(&self) -> bool { + false + } + + pub fn stack_size_without_alignment(&self, target_info: TargetInfo) -> u32 { + (target_info.ptr_width() as u32) * 3 + } + + pub fn alignment_bytes(&self, target_info: TargetInfo) -> u32 { + target_info.ptr_width() as u32 + } + + pub fn allocation_alignment_bytes(&self, target_info: TargetInfo) -> u32 { + target_info.ptr_width() as u32 + } + + pub fn is_refcounted(&self) -> bool { + // The refcounter may not be present, but we don't know that statically. + // So assume we always refcount, and the implementor of the refcount function + // can no-op if it's not needed. + true + } + + pub fn to_doc<'b, D, A>(&self, alloc: &'b D) -> ven_pretty::DocBuilder<'b, D, A> + where + D: ven_pretty::DocAllocator<'b, A>, + D::Doc: Clone, + A: Clone, + { + alloc.text("?Erased") + } +} + +impl<'a> UnionLayout<'a> { + pub(crate) fn boxed_erased_value(value: &'a InLayout<'a>) -> Self { + UnionLayout::NullableUnwrapped { + nullable_id: true, + other_fields: std::slice::from_ref(value), + } + } +} + +impl<'a> LayoutRepr<'a> { + pub(crate) fn boxed_erased_value(value: &'a InLayout<'a>) -> Self { + Self::Union(UnionLayout::boxed_erased_value(value)) + } +} diff --git a/crates/compiler/mono/src/layout/intern.rs b/crates/compiler/mono/src/layout/intern.rs new file mode 100644 index 0000000000..9891c59f2f --- /dev/null +++ b/crates/compiler/mono/src/layout/intern.rs @@ -0,0 +1,2028 @@ +use std::{ + cell::RefCell, + hash::{BuildHasher, Hasher}, + marker::PhantomData, + sync::Arc, +}; + +use bumpalo::Bump; +use parking_lot::{Mutex, RwLock}; +use roc_builtins::bitcode::{FloatWidth, IntWidth}; +use roc_collections::{default_hasher, BumpMap}; +use roc_module::symbol::Symbol; +use roc_target::TargetInfo; + +use crate::layout::LayoutRepr; + +use super::{LambdaSet, Layout, LayoutWrapper, SeenRecPtrs, SemanticRepr, UnionLayout}; + +macro_rules! cache_interned_layouts { + ($($i:literal, $name:ident, $vis:vis, $layout:expr)*; $total_constants:literal) => { + impl<'a> Layout<'a> { + $( + #[allow(unused)] // for now + $vis const $name: InLayout<'static> = unsafe { InLayout::from_index($i) }; + )* + } + + fn fill_reserved_layouts(interner: &mut STLayoutInterner<'_>) { + assert!(interner.is_empty()); + $( + interner.insert($layout); + )* + } + + const fn _are_constants_in_order_non_redundant() -> usize { + let mut total_seen = 0; + $(total_seen += ($i * 0) + 1;)* + match 0usize { + $($i => {})* + _ => {} + } + total_seen + } + + const _ASSERT_NON_REDUNDANT_CONSTANTS: () = + assert!(_are_constants_in_order_non_redundant() == $total_constants); + } +} + +macro_rules! nosema { + ($r:expr) => { + Layout { + repr: $r.direct(), + semantic: SemanticRepr::NONE, + } + }; +} + +cache_interned_layouts! { + 0, VOID, pub, Layout::VOID_NAKED + 1, UNIT, pub, Layout::UNIT_NAKED + 2, BOOL, pub, nosema!(LayoutRepr::BOOL) + 3, U8, pub, nosema!(LayoutRepr::U8) + 4, U16, pub, nosema!(LayoutRepr::U16) + 5, U32, pub, nosema!(LayoutRepr::U32) + 6, U64, pub, nosema!(LayoutRepr::U64) + 7, U128, pub, nosema!(LayoutRepr::U128) + 8, I8, pub, nosema!(LayoutRepr::I8) + 9, I16, pub, nosema!(LayoutRepr::I16) + 10, I32, pub, nosema!(LayoutRepr::I32) + 11, I64, pub, nosema!(LayoutRepr::I64) + 12, I128, pub, nosema!(LayoutRepr::I128) + 13, F32, pub, nosema!(LayoutRepr::F32) + 14, F64, pub, nosema!(LayoutRepr::F64) + 15, DEC, pub, nosema!(LayoutRepr::DEC) + 16, STR, pub, nosema!(LayoutRepr::STR) + 17, OPAQUE_PTR, pub, nosema!(LayoutRepr::OPAQUE_PTR) + 18, ERASED, pub, nosema!(LayoutRepr::ERASED) + 19, NAKED_RECURSIVE_PTR, pub(super), nosema!(LayoutRepr::RecursivePointer(Layout::VOID)) + 20, STR_PTR, pub, nosema!(LayoutRepr::Ptr(Layout::STR)) + 21, LIST_U8, pub, nosema!(LayoutRepr::Builtin(crate::layout::Builtin::List(Layout::U8))) + + ; 22 +} + +macro_rules! impl_to_from_int_width { + ($($int_width:path => $layout:path,)*) => { + impl<'a> Layout<'a> { + pub const fn int_width(w: IntWidth) -> InLayout<'static> { + match w { + $($int_width => $layout,)* + } + } + } + + impl<'a> InLayout<'a> { + /// # Panics + /// + /// Panics if the layout is not an integer + pub fn to_int_width(&self) -> IntWidth { + match self { + $(&$layout => $int_width,)* + _ => roc_error_macros::internal_error!("not an integer layout!") + } + } + } + }; +} + +impl_to_from_int_width! { + IntWidth::U8 => Layout::U8, + IntWidth::U16 => Layout::U16, + IntWidth::U32 => Layout::U32, + IntWidth::U64 => Layout::U64, + IntWidth::U128 => Layout::U128, + IntWidth::I8 => Layout::I8, + IntWidth::I16 => Layout::I16, + IntWidth::I32 => Layout::I32, + IntWidth::I64 => Layout::I64, + IntWidth::I128 => Layout::I128, +} + +impl<'a> Layout<'a> { + pub(super) const VOID_NAKED: Self = Layout { + repr: LayoutRepr::Union(UnionLayout::NonRecursive(&[])).direct(), + semantic: SemanticRepr::NONE, + }; + pub(super) const UNIT_NAKED: Self = Layout { + repr: LayoutRepr::Struct(&[]).direct(), + semantic: SemanticRepr::EMPTY_RECORD, + }; + + pub const fn float_width(w: FloatWidth) -> InLayout<'static> { + match w { + FloatWidth::F32 => Self::F32, + FloatWidth::F64 => Self::F64, + } + } +} + +/// Whether a recursive lambda set being inserted into an interner needs fixing-up of naked +/// recursion pointers in the capture set. +/// Applicable only if +/// - the lambda set is indeed recursive, and +/// - its capture set contain naked pointer references +pub struct NeedsRecursionPointerFixup(pub bool); + +pub trait LayoutInterner<'a>: Sized { + /// Interns a value, returning its interned representation. + /// If the value has been interned before, the old interned representation will be re-used. + /// + /// Note that the provided value must be allocated into an arena of your choosing, but which + /// must live at least as long as the interner lives. + // TODO: we should consider maintaining our own arena in the interner, to avoid redundant + // allocations when values already have interned representations. + fn insert(&mut self, value: Layout<'a>) -> InLayout<'a>; + + /// Interns a value with no semantic representation, returning its interned representation. + /// If the value has been interned before, the old interned representation will be re-used. + fn insert_direct_no_semantic(&mut self, repr: LayoutRepr<'a>) -> InLayout<'a> { + self.insert(Layout::no_semantic(repr.direct())) + } + + /// Creates a [LambdaSet], including caching the [LayoutRepr::LambdaSet] representation of the + /// lambda set onto itself. + fn insert_lambda_set( + &mut self, + arena: &'a Bump, + args: &'a &'a [InLayout<'a>], + ret: InLayout<'a>, + set: &'a &'a [(Symbol, &'a [InLayout<'a>])], + needs_recursive_fixup: NeedsRecursionPointerFixup, + representation: InLayout<'a>, + ) -> LambdaSet<'a>; + + /// Inserts a recursive layout into the interner. + /// Takes a normalized recursive layout with the recursion pointer set to [Layout::VOID]. + /// Will update the RecursivePointer as appropriate during insertion. + fn insert_recursive(&mut self, arena: &'a Bump, normalized_layout: Layout<'a>) -> InLayout<'a>; + + /// Retrieves a value from the interner. + fn get(&self, key: InLayout<'a>) -> Layout<'a>; + + // + // Convenience methods + + fn get_repr(&self, mut key: InLayout<'a>) -> LayoutRepr<'a> { + loop { + match self.get(key).repr { + LayoutWrapper::Direct(repr) => return repr, + LayoutWrapper::Newtype(inner) => key = inner, + } + } + } + + fn get_semantic(&self, key: InLayout<'a>) -> SemanticRepr<'a> { + self.get(key).semantic + } + + fn eq_repr(&self, a: InLayout<'a>, b: InLayout<'a>) -> bool { + self.get_repr(a) == self.get_repr(b) + } + + fn target_info(&self) -> TargetInfo; + + fn alignment_bytes(&self, layout: InLayout<'a>) -> u32 { + self.get_repr(layout).alignment_bytes(self) + } + + fn allocation_alignment_bytes(&self, layout: InLayout<'a>) -> u32 { + self.get_repr(layout).allocation_alignment_bytes(self) + } + + fn stack_size(&self, layout: InLayout<'a>) -> u32 { + self.get_repr(layout).stack_size(self) + } + + fn stack_size_and_alignment(&self, layout: InLayout<'a>) -> (u32, u32) { + self.get_repr(layout).stack_size_and_alignment(self) + } + + fn stack_size_without_alignment(&self, layout: InLayout<'a>) -> u32 { + self.get_repr(layout).stack_size_without_alignment(self) + } + + fn contains_refcounted(&self, layout: InLayout<'a>) -> bool { + self.get_repr(layout).contains_refcounted(self) + } + + fn is_refcounted(&self, layout: InLayout<'a>) -> bool { + self.get_repr(layout).is_refcounted(self) + } + + fn is_nullable(&self, layout: InLayout<'a>) -> bool { + self.get_repr(layout).is_nullable() + } + + fn is_passed_by_reference(&self, layout: InLayout<'a>) -> bool { + self.get_repr(layout).is_passed_by_reference(self) + } + + fn runtime_representation(&self, layout: InLayout<'a>) -> LayoutRepr<'a> { + self.get_repr(self.runtime_representation_in(layout)) + } + + fn runtime_representation_in(&self, layout: InLayout<'a>) -> InLayout<'a> { + Layout::runtime_representation_in(layout, self) + } + + fn has_varying_stack_size(&self, layout: InLayout<'a>, arena: &'a Bump) -> bool { + self.get_repr(layout).has_varying_stack_size(self, arena) + } + + fn chase_recursive(&self, mut layout: InLayout<'a>) -> LayoutRepr<'a> { + loop { + let lay = self.get_repr(layout); + match lay { + LayoutRepr::RecursivePointer(l) => layout = l, + _ => return lay, + } + } + } + + fn chase_recursive_in(&self, mut layout: InLayout<'a>) -> InLayout<'a> { + loop { + match self.get_repr(layout) { + LayoutRepr::RecursivePointer(l) => layout = l, + _ => return layout, + } + } + } + + fn safe_to_memcpy(&self, layout: InLayout<'a>) -> bool { + self.get_repr(layout).safe_to_memcpy(self) + } + + /// Checks if two layouts are equivalent up to isomorphism. + /// + /// This is only to be used when layouts need to be compared across statements and depths, + /// for example + /// - when looking up a layout index in a lambda set + /// - in the [checker][crate::debug::check_procs], where `x = UnionAtIndex(f, 0)` may have + /// that the recorded layout of `x` is at a different depth than that determined when we + /// index the recorded layout of `f` at 0. Hence the two layouts may have different + /// interned representations, even if they are in fact isomorphic. + fn equiv(&self, l1: InLayout<'a>, l2: InLayout<'a>) -> bool { + std::thread_local! { + static SCRATCHPAD: RefCell, InLayout<'static>)>>> = RefCell::new(Some(Vec::with_capacity(64))); + } + + SCRATCHPAD.with(|f| { + // SAFETY: the promotion to lifetime 'a only lasts during equivalence-checking; the + // scratchpad stack is cleared after every use. + let mut stack: Vec<(InLayout<'a>, InLayout<'a>)> = + unsafe { std::mem::transmute(f.take().unwrap()) }; + + let answer = equiv::equivalent(&mut stack, self, l1, l2); + stack.clear(); + + let stack: Vec<(InLayout<'static>, InLayout<'static>)> = + unsafe { std::mem::transmute(stack) }; + f.replace(Some(stack)); + answer + }) + } + + fn to_doc<'b, D, A>( + &self, + layout: InLayout<'a>, + alloc: &'b D, + seen_rec: &mut SeenRecPtrs<'a>, + parens: crate::ir::Parens, + ) -> ven_pretty::DocBuilder<'b, D, A> + where + D: ven_pretty::DocAllocator<'b, A>, + D::Doc: Clone, + A: Clone, + { + use LayoutRepr::*; + + match self.get_repr(layout) { + Builtin(builtin) => builtin.to_doc(alloc, self, seen_rec, parens), + Struct(field_layouts) => { + let fields_doc = field_layouts + .iter() + .map(|x| self.to_doc(*x, alloc, seen_rec, parens)); + + alloc + .text("{") + .append(alloc.intersperse(fields_doc, ", ")) + .append(alloc.text("}")) + } + Union(union_layout) => { + let is_recursive = !matches!(union_layout, UnionLayout::NonRecursive(..)); + if is_recursive { + seen_rec.insert(layout); + } + let doc = union_layout.to_doc(alloc, self, seen_rec, parens); + if is_recursive { + seen_rec.remove(&layout); + } + doc + } + LambdaSet(lambda_set) => { + self.to_doc(lambda_set.runtime_representation(), alloc, seen_rec, parens) + } + RecursivePointer(rec_layout) => { + if seen_rec.contains(&rec_layout) { + alloc.text("*self") + } else { + self.to_doc(rec_layout, alloc, seen_rec, parens) + } + } + Ptr(inner) => alloc + .text("Ptr(") + .append(self.to_doc(inner, alloc, seen_rec, parens)) + .append(")"), + FunctionPointer(fp) => fp.to_doc(alloc, self, seen_rec, parens), + Erased(e) => e.to_doc(alloc), + } + } + + fn to_doc_top<'b, D, A>( + &self, + layout: InLayout<'a>, + alloc: &'b D, + ) -> ven_pretty::DocBuilder<'b, D, A> + where + D: ven_pretty::DocAllocator<'b, A>, + D::Doc: Clone, + A: Clone, + { + self.to_doc( + layout, + alloc, + &mut Default::default(), + crate::ir::Parens::NotNeeded, + ) + } + + /// Pretty-print a representation of the layout. + fn dbg(&self, layout: InLayout<'a>) -> String { + let alloc: ven_pretty::Arena<()> = ven_pretty::Arena::new(); + let doc = self.to_doc_top(layout, &alloc); + doc.1.pretty(80).to_string() + } + + /// Yields a debug representation of a layout, traversing its entire nested structure and + /// debug-printing all intermediate interned layouts. + /// + /// By default, a [Layout] is composed inductively by [interned layout][InLayout]s. + /// This makes debugging a layout more than one level challenging, as you may run into further + /// opaque interned layouts that need unwrapping. + /// + /// [`dbg_deep`][LayoutInterner::dbg_deep] works around this by returning a value whose debug + /// representation chases through all nested interned layouts as you would otherwise have to do + /// manually. + /// + /// ## Example + /// + /// ```ignore(illustrative) + /// fn is_rec_ptr<'a>(interner: &impl LayoutInterner<'a>, layout: InLayout<'a>) -> bool { + /// if matches!(interner.get(layout), LayoutRepr::RecursivePointer(..)) { + /// return true; + /// } + /// + /// let deep_dbg = interner.dbg_deep(layout); + /// roc_tracing::info!("not a recursive pointer, actually a {deep_dbg:?}"); + /// return false; + /// } + /// ``` + fn dbg_deep<'r>(&'r self, layout: InLayout<'a>) -> dbg_deep::Dbg<'a, 'r, Self> { + dbg_deep::Dbg(self, layout) + } + + fn dbg_deep_iter<'r>( + &'r self, + layouts: &'a [InLayout<'a>], + ) -> dbg_deep::DbgFields<'a, 'r, Self> { + dbg_deep::DbgFields(self, layouts) + } + + /// Similar to `Self::dbg_deep`, but does not display the interned name of symbols. This keeps + /// the output consistent in a multi-threaded (test) run + fn dbg_stable<'r>(&'r self, layout: InLayout<'a>) -> dbg_stable::Dbg<'a, 'r, Self> { + dbg_stable::Dbg(self, layout) + } + + fn dbg_stable_iter<'r>( + &'r self, + layouts: &'a [InLayout<'a>], + ) -> dbg_stable::DbgFields<'a, 'r, Self> { + dbg_stable::DbgFields(self, layouts) + } +} + +/// An interned layout. +/// +/// When possible, prefer comparing/hashing on the [InLayout] representation of a value, rather +/// than the value itself. +#[derive(PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct InLayout<'a>(usize, std::marker::PhantomData<&'a ()>); +impl<'a> Clone for InLayout<'a> { + fn clone(&self) -> Self { + Self(self.0, Default::default()) + } +} + +impl<'a> Copy for InLayout<'a> {} + +impl std::fmt::Debug for InLayout<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match *self { + Layout::VOID => f.write_str("InLayout(VOID)"), + Layout::UNIT => f.write_str("InLayout(UNIT)"), + Layout::BOOL => f.write_str("InLayout(BOOL)"), + Layout::U8 => f.write_str("InLayout(U8)"), + Layout::U16 => f.write_str("InLayout(U16)"), + Layout::U32 => f.write_str("InLayout(U32)"), + Layout::U64 => f.write_str("InLayout(U64)"), + Layout::U128 => f.write_str("InLayout(U128)"), + Layout::I8 => f.write_str("InLayout(I8)"), + Layout::I16 => f.write_str("InLayout(I16)"), + Layout::I32 => f.write_str("InLayout(I32)"), + Layout::I64 => f.write_str("InLayout(I64)"), + Layout::I128 => f.write_str("InLayout(I128)"), + Layout::F32 => f.write_str("InLayout(F32)"), + Layout::F64 => f.write_str("InLayout(F64)"), + Layout::DEC => f.write_str("InLayout(DEC)"), + Layout::STR => f.write_str("InLayout(STR)"), + Layout::OPAQUE_PTR => f.write_str("InLayout(OPAQUE_PTR)"), + Layout::NAKED_RECURSIVE_PTR => f.write_str("InLayout(NAKED_RECURSIVE_PTR)"), + Layout::STR_PTR => f.write_str("InLayout(STR_PTR)"), + Layout::LIST_U8 => f.write_str("InLayout(LIST_U8)"), + _ => f.debug_tuple("InLayout").field(&self.0).finish(), + } + } +} + +impl<'a> InLayout<'a> { + /// # Safety + /// + /// The index is not guaranteed to exist. Use this only when creating an interner with constant + /// indices, with the variant that `insert` returns a monotonically increasing index. + /// + /// For example: + /// + /// ```ignore(illustrative) + /// let reserved_interned = InLayout::from_reserved_index(0); + /// let interner = GlobalLayoutInterner::with_capacity(1); + /// let inserted = interner.insert("something"); + /// assert_eq!(reserved_interned, inserted); + /// ``` + pub(crate) const unsafe fn from_index(index: usize) -> Self { + Self(index, PhantomData) + } + + pub(crate) const fn newtype(self) -> LayoutWrapper<'a> { + LayoutWrapper::Newtype(self) + } + + pub fn index(&self) -> usize { + self.0 + } + + pub fn try_int_width(self) -> Option { + match self { + Layout::U8 => Some(IntWidth::U8), + Layout::U16 => Some(IntWidth::U16), + Layout::U32 => Some(IntWidth::U32), + Layout::U64 => Some(IntWidth::U64), + Layout::U128 => Some(IntWidth::U128), + Layout::I8 => Some(IntWidth::I8), + Layout::I16 => Some(IntWidth::I16), + Layout::I32 => Some(IntWidth::I32), + Layout::I64 => Some(IntWidth::I64), + Layout::I128 => Some(IntWidth::I128), + _ => None, + } + } +} + +/// A concurrent interner, suitable for usage between threads. +/// +/// The interner does not currently maintain its own arena; you will have to supply +/// values-to-be-interned as allocated in an independent arena. +/// +/// If you need a concurrent global interner, you'll likely want each thread to take a +/// [TLLayoutInterner] via [GlobalLayoutInterner::fork], for caching purposes. +/// +/// Originally derived from https://gist.github.com/matklad/44ba1a5a6168bc0c26c995131c007907; +/// thank you, Aleksey! +#[derive(Debug)] +pub struct GlobalLayoutInterner<'a>(Arc>); + +#[derive(Debug)] +struct GlobalLayoutInternerInner<'a> { + map: Mutex, InLayout<'a>>>, + normalized_lambda_set_map: Mutex, LambdaSet<'a>>>, + vec: RwLock>>, + target_info: TargetInfo, +} + +/// A derivative of a [GlobalLayoutInterner] interner that provides caching desirable for +/// thread-local workloads. The only way to get a [TLLayoutInterner] is via +/// [GlobalLayoutInterner::fork]. +/// +/// All values interned into a [TLLayoutInterner] are made available in its parent +/// [GlobalLayoutInterner], making this suitable for global sharing of interned values. +/// +/// Originally derived from https://gist.github.com/matklad/44ba1a5a6168bc0c26c995131c007907; +/// thank you, Aleksey! +#[derive(Debug)] +pub struct TLLayoutInterner<'a> { + parent: GlobalLayoutInterner<'a>, + map: BumpMap, InLayout<'a>>, + normalized_lambda_set_map: BumpMap, LambdaSet<'a>>, + /// Cache of interned values from the parent for local access. + vec: RefCell>>>, + target_info: TargetInfo, +} + +/// A single-threaded interner, with no concurrency properties. +/// +/// The only way to construct such an interner is to collapse a shared [GlobalLayoutInterner] into +/// a [STLayoutInterner], via [GlobalLayoutInterner::unwrap]. +#[derive(Debug)] +pub struct STLayoutInterner<'a> { + map: BumpMap, InLayout<'a>>, + normalized_lambda_set_map: BumpMap, LambdaSet<'a>>, + vec: Vec>, + target_info: TargetInfo, +} + +/// Interner constructed with an exclusive lock over [GlobalLayoutInterner] +struct LockedGlobalInterner<'a, 'r> { + map: &'r mut BumpMap, InLayout<'a>>, + normalized_lambda_set_map: &'r mut BumpMap, LambdaSet<'a>>, + vec: &'r mut Vec>, + target_info: TargetInfo, +} + +/// Generic hasher for a value, to be used by all interners. +/// +/// This uses the [default_hasher], so interner maps should also rely on [default_hasher]. +fn hash(val: V) -> u64 { + let mut state = roc_collections::all::BuildHasher::default().build_hasher(); + val.hash(&mut state); + state.finish() +} + +#[inline(always)] +fn make_normalized_lamdba_set<'a>( + args: &'a &'a [InLayout<'a>], + ret: InLayout<'a>, + set: &'a &'a [(Symbol, &'a [InLayout<'a>])], + representation: InLayout<'a>, +) -> LambdaSet<'a> { + LambdaSet { + args, + ret, + set, + representation, + full_layout: Layout::VOID, + } +} + +impl<'a> GlobalLayoutInterner<'a> { + /// Creates a new global interner with the given capacity. + pub fn with_capacity(cap: usize, target_info: TargetInfo) -> Self { + STLayoutInterner::with_capacity(cap, target_info).into_global() + } + + /// Creates a derivative [TLLayoutInterner] pointing back to this global interner. + pub fn fork(&self) -> TLLayoutInterner<'a> { + TLLayoutInterner { + parent: Self(Arc::clone(&self.0)), + map: Default::default(), + normalized_lambda_set_map: Default::default(), + vec: Default::default(), + target_info: self.0.target_info, + } + } + + /// Collapses a shared [GlobalLayoutInterner] into a [STLayoutInterner]. + /// + /// Returns an [Err] with `self` if there are outstanding references to the [GlobalLayoutInterner]. + pub fn unwrap(self) -> Result, Self> { + let GlobalLayoutInternerInner { + map, + normalized_lambda_set_map, + vec, + target_info, + } = match Arc::try_unwrap(self.0) { + Ok(inner) => inner, + Err(li) => return Err(Self(li)), + }; + let map = Mutex::into_inner(map); + let normalized_lambda_set_map = Mutex::into_inner(normalized_lambda_set_map); + let vec = RwLock::into_inner(vec); + Ok(STLayoutInterner { + map, + normalized_lambda_set_map, + vec, + target_info, + }) + } + + /// Interns a value with a pre-computed hash. + /// Prefer calling this when possible, especially from [TLLayoutInterner], to avoid + /// re-computing hashes. + fn insert_hashed(&self, value: Layout<'a>, hash: u64) -> InLayout<'a> { + let mut map = self.0.map.lock(); + let (_, interned) = map + .raw_entry_mut() + .from_key_hashed_nocheck(hash, &value) + .or_insert_with(|| { + let mut vec = self.0.vec.write(); + let interned = InLayout(vec.len(), Default::default()); + vec.push(value); + (value, interned) + }); + *interned + } + + fn get_or_insert_hashed_normalized_lambda_set( + &self, + arena: &'a Bump, + normalized: LambdaSet<'a>, + needs_recursive_fixup: NeedsRecursionPointerFixup, + normalized_hash: u64, + ) -> WrittenGlobalLambdaSet<'a> { + let mut normalized_lambda_set_map = self.0.normalized_lambda_set_map.lock(); + if let Some((_, &full_lambda_set)) = normalized_lambda_set_map + .raw_entry() + .from_key_hashed_nocheck(normalized_hash, &normalized) + { + let full_layout = self.0.vec.read()[full_lambda_set.full_layout.0]; + return WrittenGlobalLambdaSet { + full_lambda_set, + full_layout, + }; + } + + // We don't already have an entry for the lambda set, which means it must be new to + // the world. Reserve a slot, insert the lambda set, and that should fill the slot + // in. + let mut map = self.0.map.lock(); + let mut vec = self.0.vec.write(); + + let slot = unsafe { InLayout::from_index(vec.len()) }; + vec.push(Layout::VOID_NAKED); + + let set = if needs_recursive_fixup.0 { + let mut interner = LockedGlobalInterner { + map: &mut map, + normalized_lambda_set_map: &mut normalized_lambda_set_map, + vec: &mut vec, + target_info: self.0.target_info, + }; + reify::reify_lambda_set_captures(arena, &mut interner, slot, normalized.set) + } else { + normalized.set + }; + + let full_lambda_set = LambdaSet { + full_layout: slot, + set, + ..normalized + }; + let lambda_set_layout = Layout { + repr: LayoutRepr::LambdaSet(full_lambda_set).direct(), + semantic: SemanticRepr::NONE, + }; + + vec[slot.0] = lambda_set_layout; + + // TODO: Is it helpful to persist the hash and give it back to the thread-local + // interner? + let _old = map.insert(lambda_set_layout, slot); + debug_assert!(_old.is_none()); + + let _old_normalized = normalized_lambda_set_map.insert(normalized, full_lambda_set); + debug_assert!(_old_normalized.is_none()); + + let full_layout = vec[full_lambda_set.full_layout.0]; + WrittenGlobalLambdaSet { + full_lambda_set, + full_layout, + } + } + + fn get_or_insert_hashed_normalized_recursive( + &self, + arena: &'a Bump, + normalized: Layout<'a>, + normalized_hash: u64, + ) -> WrittenGlobalRecursive<'a> { + let mut map = self.0.map.lock(); + if let Some((_, &interned)) = map + .raw_entry() + .from_key_hashed_nocheck(normalized_hash, &normalized) + { + let full_layout = self.0.vec.read()[interned.0]; + return WrittenGlobalRecursive { + interned_layout: interned, + full_layout, + }; + } + + let mut vec = self.0.vec.write(); + let mut normalized_lambda_set_map = self.0.normalized_lambda_set_map.lock(); + + let slot = unsafe { InLayout::from_index(vec.len()) }; + vec.push(Layout::VOID_NAKED); + + let mut interner = LockedGlobalInterner { + map: &mut map, + normalized_lambda_set_map: &mut normalized_lambda_set_map, + vec: &mut vec, + target_info: self.0.target_info, + }; + let full_layout = reify::reify_recursive_layout(arena, &mut interner, slot, normalized); + + vec[slot.0] = full_layout; + + let _old = map.insert(normalized, slot); + debug_assert!(_old.is_none()); + + let _old_full_layout = map.insert(full_layout, slot); + debug_assert!(_old_full_layout.is_none()); + + WrittenGlobalRecursive { + interned_layout: slot, + full_layout, + } + } + + fn get(&self, interned: InLayout<'a>) -> Layout<'a> { + let InLayout(index, _) = interned; + self.0.vec.read()[index] + } + + pub fn is_empty(&self) -> bool { + self.0.vec.read().is_empty() + } +} + +struct WrittenGlobalLambdaSet<'a> { + full_lambda_set: LambdaSet<'a>, + full_layout: Layout<'a>, +} + +struct WrittenGlobalRecursive<'a> { + interned_layout: InLayout<'a>, + full_layout: Layout<'a>, +} + +impl<'a> TLLayoutInterner<'a> { + /// Records an interned value in thread-specific storage, for faster access on lookups. + fn record(&self, key: Layout<'a>, interned: InLayout<'a>) { + let mut vec = self.vec.borrow_mut(); + let len = vec.len().max(interned.0 + 1); + vec.resize(len, None); + vec[interned.0] = Some(key); + } +} + +impl<'a> LayoutInterner<'a> for TLLayoutInterner<'a> { + fn insert(&mut self, value: Layout<'a>) -> InLayout<'a> { + let global = &self.parent; + let hash = hash(value); + let (&mut value, &mut interned) = self + .map + .raw_entry_mut() + .from_key_hashed_nocheck(hash, &value) + .or_insert_with(|| { + let interned = global.insert_hashed(value, hash); + (value, interned) + }); + self.record(value, interned); + interned + } + + fn insert_lambda_set( + &mut self, + arena: &'a Bump, + args: &'a &'a [InLayout<'a>], + ret: InLayout<'a>, + set: &'a &'a [(Symbol, &'a [InLayout<'a>])], + needs_recursive_fixup: NeedsRecursionPointerFixup, + representation: InLayout<'a>, + ) -> LambdaSet<'a> { + // The tricky bit of inserting a lambda set is we need to fill in the `full_layout` only + // after the lambda set is inserted, but we don't want to allocate a new interned slot if + // the same lambda set layout has already been inserted with a different `full_layout` + // slot. + // + // So, + // - check if the "normalized" lambda set (with a void full_layout slot) maps to an + // inserted lambda set in + // - in our thread-local cache, or globally + // - if so, use that one immediately + // - otherwise, allocate a new (global) slot, intern the lambda set, and then fill the slot in + let global = &self.parent; + let normalized = make_normalized_lamdba_set(args, ret, set, representation); + let normalized_hash = hash(normalized); + let mut new_interned_layout = None; + let (_, &mut full_lambda_set) = self + .normalized_lambda_set_map + .raw_entry_mut() + .from_key_hashed_nocheck(normalized_hash, &normalized) + .or_insert_with(|| { + let WrittenGlobalLambdaSet { + full_lambda_set, + full_layout, + } = global.get_or_insert_hashed_normalized_lambda_set( + arena, + normalized, + needs_recursive_fixup, + normalized_hash, + ); + + // The Layout(lambda_set) isn't present in our thread; make sure it is for future + // reference. + new_interned_layout = Some((full_layout, full_lambda_set.full_layout)); + + (normalized, full_lambda_set) + }); + + if let Some((new_layout, new_interned)) = new_interned_layout { + // Write the interned lambda set layout into our thread-local cache. + self.record(new_layout, new_interned); + } + + full_lambda_set + } + + fn insert_recursive(&mut self, arena: &'a Bump, normalized_layout: Layout<'a>) -> InLayout<'a> { + // - Check if the normalized layout already has an interned slot. If it does we're done, since no + // recursive layout would ever have have VOID as the recursion pointer. + // - If not, allocate a slot and compute the recursive layout with the recursion pointer + // resolving to the new slot. + // - Point the resolved and normalized layout to the new slot. + let global = &self.parent; + let normalized_hash = hash(normalized_layout); + let mut new_interned_full_layout = None; + let (&mut _, &mut interned) = self + .map + .raw_entry_mut() + .from_key_hashed_nocheck(normalized_hash, &normalized_layout) + .or_insert_with(|| { + let WrittenGlobalRecursive { + interned_layout, + full_layout, + } = global.get_or_insert_hashed_normalized_recursive( + arena, + normalized_layout, + normalized_hash, + ); + + // The new filled-in layout isn't present in our thread; make sure it is for future + // reference. + new_interned_full_layout = Some(full_layout); + + (normalized_layout, interned_layout) + }); + if let Some(full_layout) = new_interned_full_layout { + self.record(full_layout, interned); + } + interned + } + + fn get(&self, key: InLayout<'a>) -> Layout<'a> { + if let Some(Some(value)) = self.vec.borrow().get(key.0) { + return *value; + } + let value = self.parent.get(key); + self.record(value, key); + value + } + + fn target_info(&self) -> TargetInfo { + self.target_info + } +} + +impl<'a> STLayoutInterner<'a> { + /// Creates a new single threaded interner with the given capacity. + pub fn with_capacity(cap: usize, target_info: TargetInfo) -> Self { + let mut interner = Self { + map: BumpMap::with_capacity_and_hasher(cap, default_hasher()), + normalized_lambda_set_map: BumpMap::with_capacity_and_hasher(cap, default_hasher()), + vec: Vec::with_capacity(cap), + target_info, + }; + fill_reserved_layouts(&mut interner); + interner + } + + /// Promotes the [STLayoutInterner] back to a [GlobalLayoutInterner]. + /// + /// You should *only* use this if you need to go from a single-threaded to a concurrent context, + /// or in a case where you explicitly need access to [TLLayoutInterner]s. + pub fn into_global(self) -> GlobalLayoutInterner<'a> { + let STLayoutInterner { + map, + normalized_lambda_set_map, + vec, + target_info, + } = self; + GlobalLayoutInterner(Arc::new(GlobalLayoutInternerInner { + map: Mutex::new(map), + normalized_lambda_set_map: Mutex::new(normalized_lambda_set_map), + vec: RwLock::new(vec), + target_info, + })) + } + + pub fn is_empty(&self) -> bool { + self.vec.is_empty() + } +} + +macro_rules! st_impl { + ($($lt:lifetime)? $interner:ident) => { + impl<'a$(, $lt)?> LayoutInterner<'a> for $interner<'a$(, $lt)?> { + fn insert(&mut self, value: Layout<'a>) -> InLayout<'a> { + let hash = hash(value); + let (_, interned) = self + .map + .raw_entry_mut() + .from_key_hashed_nocheck(hash, &value) + .or_insert_with(|| { + let interned = InLayout(self.vec.len(), Default::default()); + self.vec.push(value); + (value, interned) + }); + *interned + } + + fn insert_lambda_set( + &mut self, + arena: &'a Bump, + args: &'a &'a [InLayout<'a>], + ret: InLayout<'a>, + set: &'a &'a [(Symbol, &'a [InLayout<'a>])], + needs_recursive_fixup: NeedsRecursionPointerFixup, + representation: InLayout<'a>, + ) -> LambdaSet<'a> { + // IDEA: + // - check if the "normalized" lambda set (with a void full_layout slot) maps to an + // inserted lambda set + // - if so, use that one immediately + // - otherwise, allocate a new slot, intern the lambda set, and then fill the slot in + let normalized_lambda_set = + make_normalized_lamdba_set(args, ret, set, representation); + if let Some(lambda_set) = self.normalized_lambda_set_map.get(&normalized_lambda_set) + { + return *lambda_set; + } + + // This lambda set must be new to the interner, reserve a slot and fill it in. + let slot = unsafe { InLayout::from_index(self.vec.len()) }; + self.vec.push(Layout::VOID_NAKED); + + let set = if needs_recursive_fixup.0 { + reify::reify_lambda_set_captures(arena, self, slot, set) + } else { + set + }; + + let lambda_set = LambdaSet { + args, + ret, + set, + representation, + full_layout: slot, + }; + let lay = Layout { + repr: LayoutRepr::LambdaSet(lambda_set).direct(), + semantic: SemanticRepr::NONE + }; + self.vec[slot.0] = lay; + + let _old = self.map.insert(lay, slot); + debug_assert!(_old.is_none()); + + let _old = self.normalized_lambda_set_map + .insert(normalized_lambda_set, lambda_set); + debug_assert!(_old.is_none()); + + lambda_set + } + + fn insert_recursive( + &mut self, + arena: &'a Bump, + normalized_layout: Layout<'a>, + ) -> InLayout<'a> { + // IDEA: + // - check if the normalized layout (with a void recursion pointer) maps to an + // inserted lambda set + // - if so, use that one immediately + // - otherwise, allocate a new slot, update the recursive layout, and intern + if let Some(in_layout) = self.map.get(&normalized_layout) { + return *in_layout; + } + + // This recursive layout must be new to the interner, reserve a slot and fill it in. + let slot = unsafe { InLayout::from_index(self.vec.len()) }; + self.vec.push(Layout::VOID_NAKED); + let full_layout = + reify::reify_recursive_layout(arena, self, slot, normalized_layout); + self.vec[slot.0] = full_layout; + + self.map.insert(normalized_layout, slot); + self.map.insert(full_layout, slot); + + slot + } + + fn get(&self, key: InLayout<'a>) -> Layout<'a> { + let InLayout(index, _) = key; + self.vec[index] + } + + fn target_info(&self) -> TargetInfo { + self.target_info + } + } + }; +} + +st_impl!(STLayoutInterner); +st_impl!('r LockedGlobalInterner); + +mod reify { + use bumpalo::{collections::Vec, Bump}; + use roc_module::symbol::Symbol; + + use crate::layout::{ + Builtin, FunctionPointer, LambdaSet, Layout, LayoutRepr, LayoutWrapper, UnionLayout, + }; + + use super::{InLayout, LayoutInterner, NeedsRecursionPointerFixup}; + + // TODO: if recursion becomes a problem we could make this iterative + pub fn reify_recursive_layout<'a>( + arena: &'a Bump, + interner: &mut impl LayoutInterner<'a>, + slot: InLayout<'a>, + normalized_layout: Layout<'a>, + ) -> Layout<'a> { + let Layout { repr, semantic } = normalized_layout; + let reified_repr = match repr { + LayoutWrapper::Direct(repr) => { + reify_recursive_layout_repr(arena, interner, slot, repr).direct() + } + LayoutWrapper::Newtype(inner) => reify_layout(arena, interner, slot, inner).newtype(), + }; + + Layout::new(reified_repr, semantic) + } + + fn reify_recursive_layout_repr<'a>( + arena: &'a Bump, + interner: &mut impl LayoutInterner<'a>, + slot: InLayout<'a>, + repr: LayoutRepr<'a>, + ) -> LayoutRepr<'a> { + match repr { + LayoutRepr::Builtin(builtin) => { + LayoutRepr::Builtin(reify_builtin(arena, interner, slot, builtin)) + } + LayoutRepr::Struct(field_layouts) => { + LayoutRepr::Struct(reify_layout_slice(arena, interner, slot, field_layouts)) + } + LayoutRepr::Ptr(lay) => LayoutRepr::Ptr(reify_layout(arena, interner, slot, lay)), + LayoutRepr::Union(un) => LayoutRepr::Union(reify_union(arena, interner, slot, un)), + LayoutRepr::LambdaSet(ls) => { + LayoutRepr::LambdaSet(reify_lambda_set(arena, interner, slot, ls)) + } + LayoutRepr::RecursivePointer(l) => { + // If the layout is not void at its point then it has already been solved as + // another recursive union's layout, do not change it. + LayoutRepr::RecursivePointer(if l == Layout::VOID { slot } else { l }) + } + LayoutRepr::FunctionPointer(FunctionPointer { args, ret }) => { + LayoutRepr::FunctionPointer(FunctionPointer { + args: reify_layout_slice(arena, interner, slot, args), + ret: reify_layout(arena, interner, slot, ret), + }) + } + LayoutRepr::Erased(e) => LayoutRepr::Erased(e), + } + } + + fn reify_layout<'a>( + arena: &'a Bump, + interner: &mut impl LayoutInterner<'a>, + slot: InLayout<'a>, + layout: InLayout<'a>, + ) -> InLayout<'a> { + let layout = reify_recursive_layout(arena, interner, slot, interner.get(layout)); + interner.insert(layout) + } + + fn reify_layout_slice<'a>( + arena: &'a Bump, + interner: &mut impl LayoutInterner<'a>, + slot: InLayout<'a>, + layouts: &[InLayout<'a>], + ) -> &'a [InLayout<'a>] { + let mut slice = Vec::with_capacity_in(layouts.len(), arena); + for &layout in layouts { + slice.push(reify_layout(arena, interner, slot, layout)); + } + slice.into_bump_slice() + } + + fn reify_layout_slice_slice<'a>( + arena: &'a Bump, + interner: &mut impl LayoutInterner<'a>, + slot: InLayout<'a>, + layouts: &[&[InLayout<'a>]], + ) -> &'a [&'a [InLayout<'a>]] { + let mut slice = Vec::with_capacity_in(layouts.len(), arena); + for &layouts in layouts { + slice.push(reify_layout_slice(arena, interner, slot, layouts)); + } + slice.into_bump_slice() + } + + fn reify_builtin<'a>( + arena: &'a Bump, + interner: &mut impl LayoutInterner<'a>, + slot: InLayout<'a>, + builtin: Builtin<'a>, + ) -> Builtin<'a> { + match builtin { + Builtin::Int(_) + | Builtin::Float(_) + | Builtin::Bool + | Builtin::Decimal + | Builtin::Str => builtin, + Builtin::List(elem) => Builtin::List(reify_layout(arena, interner, slot, elem)), + } + } + + fn reify_union<'a>( + arena: &'a Bump, + interner: &mut impl LayoutInterner<'a>, + slot: InLayout<'a>, + union: UnionLayout<'a>, + ) -> UnionLayout<'a> { + match union { + UnionLayout::NonRecursive(tags) => { + UnionLayout::NonRecursive(reify_layout_slice_slice(arena, interner, slot, tags)) + } + UnionLayout::Recursive(tags) => { + UnionLayout::Recursive(reify_layout_slice_slice(arena, interner, slot, tags)) + } + UnionLayout::NonNullableUnwrapped(fields) => { + UnionLayout::NonNullableUnwrapped(reify_layout_slice(arena, interner, slot, fields)) + } + UnionLayout::NullableWrapped { + nullable_id, + other_tags, + } => UnionLayout::NullableWrapped { + nullable_id, + other_tags: reify_layout_slice_slice(arena, interner, slot, other_tags), + }, + UnionLayout::NullableUnwrapped { + nullable_id, + other_fields, + } => UnionLayout::NullableUnwrapped { + nullable_id, + other_fields: reify_layout_slice(arena, interner, slot, other_fields), + }, + } + } + + fn reify_lambda_set<'a>( + arena: &'a Bump, + interner: &mut impl LayoutInterner<'a>, + slot: InLayout<'a>, + lambda_set: LambdaSet<'a>, + ) -> LambdaSet<'a> { + let LambdaSet { + args, + ret, + set, + representation, + full_layout: _, + } = lambda_set; + + let args = reify_layout_slice(arena, interner, slot, args); + let ret = reify_layout(arena, interner, slot, ret); + let set = { + let mut new_set = Vec::with_capacity_in(set.len(), arena); + for (lambda, captures) in set.iter() { + new_set.push((*lambda, reify_layout_slice(arena, interner, slot, captures))); + } + new_set.into_bump_slice() + }; + let representation = reify_layout(arena, interner, slot, representation); + + interner.insert_lambda_set( + arena, + arena.alloc(args), + ret, + arena.alloc(set), + // All nested recursive pointers should been fixed up, since we just did that above. + NeedsRecursionPointerFixup(false), + representation, + ) + } + + pub fn reify_lambda_set_captures<'a>( + arena: &'a Bump, + interner: &mut impl LayoutInterner<'a>, + slot: InLayout<'a>, + set: &[(Symbol, &'a [InLayout<'a>])], + ) -> &'a &'a [(Symbol, &'a [InLayout<'a>])] { + let mut reified_set = Vec::with_capacity_in(set.len(), arena); + for (f, captures) in set.iter() { + let reified_captures = reify_layout_slice(arena, interner, slot, captures); + reified_set.push((*f, reified_captures)); + } + arena.alloc(reified_set.into_bump_slice()) + } +} + +mod equiv { + use crate::layout::{self, LayoutRepr, UnionLayout}; + + use super::{InLayout, LayoutInterner}; + + pub fn equivalent<'a>( + stack: &mut Vec<(InLayout<'a>, InLayout<'a>)>, + interner: &impl LayoutInterner<'a>, + l1: InLayout<'a>, + l2: InLayout<'a>, + ) -> bool { + stack.push((l1, l2)); + + macro_rules! equiv_fields { + ($fields1:expr, $fields2:expr) => {{ + if $fields1.len() != $fields2.len() { + return false; + } + stack.extend($fields1.iter().copied().zip($fields2.iter().copied())); + }}; + } + + macro_rules! equiv_unions { + ($tags1:expr, $tags2:expr) => {{ + if $tags1.len() != $tags2.len() { + return false; + } + for (payloads1, payloads2) in $tags1.iter().zip($tags2) { + equiv_fields!(payloads1, payloads2) + } + }}; + } + + while let Some((l1, l2)) = stack.pop() { + if l1 == l2 { + continue; + } + use LayoutRepr::*; + match (interner.get_repr(l1), interner.get_repr(l2)) { + (RecursivePointer(rec), _) => stack.push((rec, l2)), + (_, RecursivePointer(rec)) => stack.push((l1, rec)), + (Builtin(b1), Builtin(b2)) => { + use crate::layout::Builtin::*; + match (b1, b2) { + (List(e1), List(e2)) => stack.push((e1, e2)), + (b1, b2) => { + if b1 != b2 { + return false; + } + } + } + } + (Struct(fl1), Struct(fl2)) => { + equiv_fields!(fl1, fl2) + } + (Ptr(b1), Ptr(b2)) => stack.push((b1, b2)), + (Union(u1), Union(u2)) => { + use UnionLayout::*; + match (u1, u2) { + (NonRecursive(tags1), NonRecursive(tags2)) => equiv_unions!(tags1, tags2), + (Recursive(tags1), Recursive(tags2)) => equiv_unions!(tags1, tags2), + (NonNullableUnwrapped(fields1), NonNullableUnwrapped(fields2)) => { + equiv_fields!(fields1, fields2) + } + ( + NullableWrapped { + nullable_id: null_id1, + other_tags: tags1, + }, + NullableWrapped { + nullable_id: null_id2, + other_tags: tags2, + }, + ) => { + if null_id1 != null_id2 { + return false; + } + equiv_unions!(tags1, tags2) + } + ( + NullableUnwrapped { + nullable_id: null_id1, + other_fields: fields1, + }, + NullableUnwrapped { + nullable_id: null_id2, + other_fields: fields2, + }, + ) => { + if null_id1 != null_id2 { + return false; + } + equiv_fields!(fields1, fields2) + } + _ => return false, + } + } + ( + LambdaSet(layout::LambdaSet { + args: args1, + ret: ret1, + set: set1, + representation: repr1, + full_layout: _, + }), + LambdaSet(layout::LambdaSet { + args: args2, + ret: ret2, + set: set2, + representation: repr2, + full_layout: _, + }), + ) => { + for ((fn1, captures1), (fn2, captures2)) in (**set1).iter().zip(*set2) { + if fn1 != fn2 { + return false; + } + equiv_fields!(captures1, captures2); + } + equiv_fields!(args1, args2); + stack.push((ret1, ret2)); + stack.push((repr1, repr2)); + } + _ => return false, + } + } + + true + } +} + +pub mod dbg_deep { + use roc_module::symbol::Symbol; + + use crate::layout::{Builtin, Erased, LambdaSet, LayoutRepr, UnionLayout}; + + use super::{InLayout, LayoutInterner}; + + pub struct Dbg<'a, 'r, I: LayoutInterner<'a>>(pub &'r I, pub InLayout<'a>); + + impl<'a, 'r, I: LayoutInterner<'a>> std::fmt::Debug for Dbg<'a, 'r, I> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let repr = self.0.get_repr(self.1); + let semantic = self.0.get_semantic(self.1); + + f.debug_struct("Layout") + .field("repr", &DbgRepr(self.0, &repr)) + .field("semantic", &semantic) + .finish() + } + } + + pub struct DbgFields<'a, 'r, I: LayoutInterner<'a>>(pub &'r I, pub &'a [InLayout<'a>]); + + impl<'a, 'r, I: LayoutInterner<'a>> std::fmt::Debug for DbgFields<'a, 'r, I> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_list() + .entries(self.1.iter().map(|l| Dbg(self.0, *l))) + .finish() + } + } + + struct DbgRepr<'a, 'r, I: LayoutInterner<'a>>(&'r I, &'r LayoutRepr<'a>); + + impl<'a, 'r, I: LayoutInterner<'a>> std::fmt::Debug for DbgRepr<'a, 'r, I> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self.1 { + LayoutRepr::Builtin(b) => f + .debug_tuple("Builtin") + .field(&DbgBuiltin(self.0, *b)) + .finish(), + LayoutRepr::Struct(field_layouts) => f + .debug_struct("Struct") + .field("fields", &DbgFields(self.0, field_layouts)) + .finish(), + LayoutRepr::Ptr(b) => f.debug_tuple("Ptr").field(&Dbg(self.0, *b)).finish(), + LayoutRepr::Union(un) => f + .debug_tuple("Union") + .field(&DbgUnion(self.0, *un)) + .finish(), + LayoutRepr::LambdaSet(ls) => f + .debug_tuple("LambdaSet") + .field(&DbgLambdaSet(self.0, *ls)) + .finish(), + LayoutRepr::RecursivePointer(rp) => { + f.debug_tuple("RecursivePointer").field(&rp.0).finish() + } + LayoutRepr::FunctionPointer(fp) => f + .debug_struct("FunctionPointer") + .field("args", &fp.args) + .field("ret", &fp.ret) + .finish(), + LayoutRepr::Erased(Erased) => f.debug_struct("?Erased").finish(), + } + } + } + + struct DbgTags<'a, 'r, I: LayoutInterner<'a>>(&'r I, &'a [&'a [InLayout<'a>]]); + + impl<'a, 'r, I: LayoutInterner<'a>> std::fmt::Debug for DbgTags<'a, 'r, I> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_list() + .entries(self.1.iter().map(|l| DbgFields(self.0, l))) + .finish() + } + } + + struct DbgBuiltin<'a, 'r, I: LayoutInterner<'a>>(&'r I, Builtin<'a>); + + impl<'a, 'r, I: LayoutInterner<'a>> std::fmt::Debug for DbgBuiltin<'a, 'r, I> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self.1 { + Builtin::Int(w) => f.debug_tuple("Int").field(&w).finish(), + Builtin::Float(w) => f.debug_tuple("Frac").field(&w).finish(), + Builtin::Bool => f.debug_tuple("Bool").finish(), + Builtin::Decimal => f.debug_tuple("Decimal").finish(), + Builtin::Str => f.debug_tuple("Str").finish(), + Builtin::List(e) => f.debug_tuple("List").field(&Dbg(self.0, e)).finish(), + } + } + } + + struct DbgUnion<'a, 'r, I: LayoutInterner<'a>>(&'r I, UnionLayout<'a>); + + impl<'a, 'r, I: LayoutInterner<'a>> std::fmt::Debug for DbgUnion<'a, 'r, I> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self.1 { + UnionLayout::NonRecursive(payloads) => f + .debug_tuple("NonRecursive") + .field(&DbgTags(self.0, payloads)) + .finish(), + UnionLayout::Recursive(payloads) => f + .debug_tuple("Recursive") + .field(&DbgTags(self.0, payloads)) + .finish(), + UnionLayout::NonNullableUnwrapped(fields) => f + .debug_tuple("NonNullableUnwrapped") + .field(&DbgFields(self.0, fields)) + .finish(), + UnionLayout::NullableWrapped { + nullable_id, + other_tags, + } => f + .debug_struct("NullableWrapped") + .field("nullable_id", &nullable_id) + .field("other_tags", &DbgTags(self.0, other_tags)) + .finish(), + UnionLayout::NullableUnwrapped { + nullable_id, + other_fields, + } => f + .debug_struct("NullableUnwrapped") + .field("nullable_id", &nullable_id) + .field("other_tags", &DbgFields(self.0, other_fields)) + .finish(), + } + } + } + + struct DbgLambdaSet<'a, 'r, I: LayoutInterner<'a>>(&'r I, LambdaSet<'a>); + + impl<'a, 'r, I: LayoutInterner<'a>> std::fmt::Debug for DbgLambdaSet<'a, 'r, I> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let LambdaSet { + args, + ret, + set, + representation, + full_layout, + } = self.1; + + f.debug_struct("LambdaSet") + .field("args", &DbgFields(self.0, args)) + .field("ret", &Dbg(self.0, ret)) + .field("set", &DbgCapturesSet(self.0, set)) + .field("representation", &Dbg(self.0, representation)) + .field("full_layout", &full_layout) + .finish() + } + } + + struct DbgCapturesSet<'a, 'r, I: LayoutInterner<'a>>(&'r I, &'a [(Symbol, &'a [InLayout<'a>])]); + + impl<'a, 'r, I: LayoutInterner<'a>> std::fmt::Debug for DbgCapturesSet<'a, 'r, I> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_list() + .entries( + self.1 + .iter() + .map(|(sym, captures)| (sym, DbgFields(self.0, captures))), + ) + .finish() + } + } +} + +/// Provides a stable debug output +/// +/// The debug output defined in `dbg_deep` uses the `Symbol` `std::fmt::Debug` instance, which uses +/// interned string names to make the output easier to interpret. That is useful for manual +/// debugging, but the interned strings are not stable in a multi-threaded context (e.g. when +/// running `cargo test`). The output of this module is always stable. +pub mod dbg_stable { + use roc_module::symbol::Symbol; + + use crate::layout::{Builtin, Erased, LambdaSet, LayoutRepr, SemanticRepr, UnionLayout}; + + use super::{InLayout, LayoutInterner}; + + pub struct Dbg<'a, 'r, I: LayoutInterner<'a>>(pub &'r I, pub InLayout<'a>); + + impl<'a, 'r, I: LayoutInterner<'a>> std::fmt::Debug for Dbg<'a, 'r, I> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let repr = self.0.get_repr(self.1); + let semantic = self.0.get_semantic(self.1); + + struct ConsistentSemanticRepr<'a>(SemanticRepr<'a>); + + impl std::fmt::Debug for ConsistentSemanticRepr<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt_consistent(f) + } + } + + f.debug_struct("Layout") + .field("repr", &DbgRepr(self.0, &repr)) + .field("semantic", &ConsistentSemanticRepr(semantic)) + .finish() + } + } + + pub struct DbgFields<'a, 'r, I: LayoutInterner<'a>>(pub &'r I, pub &'a [InLayout<'a>]); + + impl<'a, 'r, I: LayoutInterner<'a>> std::fmt::Debug for DbgFields<'a, 'r, I> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_list() + .entries(self.1.iter().map(|l| Dbg(self.0, *l))) + .finish() + } + } + + struct DbgRepr<'a, 'r, I: LayoutInterner<'a>>(&'r I, &'r LayoutRepr<'a>); + + impl<'a, 'r, I: LayoutInterner<'a>> std::fmt::Debug for DbgRepr<'a, 'r, I> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self.1 { + LayoutRepr::Builtin(b) => f + .debug_tuple("Builtin") + .field(&DbgBuiltin(self.0, *b)) + .finish(), + LayoutRepr::Struct(field_layouts) => f + .debug_struct("Struct") + .field("fields", &DbgFields(self.0, field_layouts)) + .finish(), + LayoutRepr::Ptr(b) => f.debug_tuple("Ptr").field(&Dbg(self.0, *b)).finish(), + LayoutRepr::Union(un) => f + .debug_tuple("Union") + .field(&DbgUnion(self.0, *un)) + .finish(), + LayoutRepr::LambdaSet(ls) => f + .debug_tuple("LambdaSet") + .field(&DbgLambdaSet(self.0, *ls)) + .finish(), + LayoutRepr::RecursivePointer(rp) => { + f.debug_tuple("RecursivePointer").field(&rp.0).finish() + } + LayoutRepr::FunctionPointer(fp) => f + .debug_struct("FunctionPointer") + .field("args", &fp.args) + .field("ret", &fp.ret) + .finish(), + LayoutRepr::Erased(Erased) => f.debug_struct("?Erased").finish(), + } + } + } + + struct DbgTags<'a, 'r, I: LayoutInterner<'a>>(&'r I, &'a [&'a [InLayout<'a>]]); + + impl<'a, 'r, I: LayoutInterner<'a>> std::fmt::Debug for DbgTags<'a, 'r, I> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_list() + .entries(self.1.iter().map(|l| DbgFields(self.0, l))) + .finish() + } + } + + struct DbgBuiltin<'a, 'r, I: LayoutInterner<'a>>(&'r I, Builtin<'a>); + + impl<'a, 'r, I: LayoutInterner<'a>> std::fmt::Debug for DbgBuiltin<'a, 'r, I> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self.1 { + Builtin::Int(w) => f.debug_tuple("Int").field(&w).finish(), + Builtin::Float(w) => f.debug_tuple("Frac").field(&w).finish(), + Builtin::Bool => f.debug_tuple("Bool").finish(), + Builtin::Decimal => f.debug_tuple("Decimal").finish(), + Builtin::Str => f.debug_tuple("Str").finish(), + Builtin::List(e) => f.debug_tuple("List").field(&Dbg(self.0, e)).finish(), + } + } + } + + struct DbgUnion<'a, 'r, I: LayoutInterner<'a>>(&'r I, UnionLayout<'a>); + + impl<'a, 'r, I: LayoutInterner<'a>> std::fmt::Debug for DbgUnion<'a, 'r, I> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self.1 { + UnionLayout::NonRecursive(payloads) => f + .debug_tuple("NonRecursive") + .field(&DbgTags(self.0, payloads)) + .finish(), + UnionLayout::Recursive(payloads) => f + .debug_tuple("Recursive") + .field(&DbgTags(self.0, payloads)) + .finish(), + UnionLayout::NonNullableUnwrapped(fields) => f + .debug_tuple("NonNullableUnwrapped") + .field(&DbgFields(self.0, fields)) + .finish(), + UnionLayout::NullableWrapped { + nullable_id, + other_tags, + } => f + .debug_struct("NullableWrapped") + .field("nullable_id", &nullable_id) + .field("other_tags", &DbgTags(self.0, other_tags)) + .finish(), + UnionLayout::NullableUnwrapped { + nullable_id, + other_fields, + } => f + .debug_struct("NullableUnwrapped") + .field("nullable_id", &nullable_id) + .field("other_tags", &DbgFields(self.0, other_fields)) + .finish(), + } + } + } + + struct DbgLambdaSet<'a, 'r, I: LayoutInterner<'a>>(&'r I, LambdaSet<'a>); + + impl<'a, 'r, I: LayoutInterner<'a>> std::fmt::Debug for DbgLambdaSet<'a, 'r, I> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let LambdaSet { + args, + ret, + set, + representation, + full_layout, + } = self.1; + + f.debug_struct("LambdaSet") + .field("args", &DbgFields(self.0, args)) + .field("ret", &Dbg(self.0, ret)) + .field("set", &DbgCapturesSet(self.0, set)) + .field("representation", &Dbg(self.0, representation)) + .field("full_layout", &full_layout) + .finish() + } + } + + struct DbgCapturesSet<'a, 'r, I: LayoutInterner<'a>>(&'r I, &'a [(Symbol, &'a [InLayout<'a>])]); + + impl<'a, 'r, I: LayoutInterner<'a>> std::fmt::Debug for DbgCapturesSet<'a, 'r, I> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_list() + .entries( + self.1 + .iter() + .map(|(sym, captures)| (sym.as_u64(), DbgFields(self.0, captures))), + ) + .finish() + } + } +} + +#[cfg(test)] +mod insert_lambda_set { + use bumpalo::Bump; + use roc_module::symbol::Symbol; + use roc_target::TargetInfo; + + use crate::layout::{LambdaSet, Layout, LayoutRepr, SemanticRepr}; + + use super::{GlobalLayoutInterner, InLayout, LayoutInterner, NeedsRecursionPointerFixup}; + + const TARGET_INFO: TargetInfo = TargetInfo::default_x86_64(); + const TEST_SET: &&[(Symbol, &[InLayout])] = + &(&[(Symbol::ATTR_ATTR, &[Layout::UNIT] as &[_])] as &[_]); + const TEST_ARGS: &&[InLayout] = &(&[Layout::UNIT] as &[_]); + const TEST_RET: InLayout = Layout::UNIT; + + const FIXUP: NeedsRecursionPointerFixup = NeedsRecursionPointerFixup(true); + + #[test] + fn two_threads_write() { + for _ in 0..100 { + let mut arenas: Vec<_> = std::iter::repeat_with(Bump::new).take(10).collect(); + let global = GlobalLayoutInterner::with_capacity(2, TARGET_INFO); + let set = TEST_SET; + let repr = Layout::UNIT; + std::thread::scope(|s| { + let mut handles = Vec::with_capacity(10); + for arena in arenas.iter_mut() { + let mut interner = global.fork(); + handles.push(s.spawn(move || { + interner.insert_lambda_set(arena, TEST_ARGS, TEST_RET, set, FIXUP, repr) + })) + } + let ins: Vec = handles.into_iter().map(|t| t.join().unwrap()).collect(); + let interned = ins[0]; + assert!(ins.iter().all(|in2| interned == *in2)); + }); + } + } + + #[test] + fn insert_then_reintern() { + let arena = &Bump::new(); + let global = GlobalLayoutInterner::with_capacity(2, TARGET_INFO); + let mut interner = global.fork(); + + let lambda_set = + interner.insert_lambda_set(arena, TEST_ARGS, TEST_RET, TEST_SET, FIXUP, Layout::UNIT); + let lambda_set_layout_in = interner.insert(Layout { + repr: LayoutRepr::LambdaSet(lambda_set).direct(), + semantic: SemanticRepr::NONE, + }); + assert_eq!(lambda_set.full_layout, lambda_set_layout_in); + } + + #[test] + fn write_global_then_single_threaded() { + let arena = &Bump::new(); + let global = GlobalLayoutInterner::with_capacity(2, TARGET_INFO); + let set = TEST_SET; + let repr = Layout::UNIT; + + let in1 = { + let mut interner = global.fork(); + interner.insert_lambda_set(arena, TEST_ARGS, TEST_RET, set, FIXUP, repr) + }; + + let in2 = { + let mut st_interner = global.unwrap().unwrap(); + st_interner.insert_lambda_set(arena, TEST_ARGS, TEST_RET, set, FIXUP, repr) + }; + + assert_eq!(in1, in2); + } + + #[test] + fn write_single_threaded_then_global() { + let arena = &Bump::new(); + let global = GlobalLayoutInterner::with_capacity(2, TARGET_INFO); + let mut st_interner = global.unwrap().unwrap(); + + let set = TEST_SET; + let repr = Layout::UNIT; + + let in1 = st_interner.insert_lambda_set(arena, TEST_ARGS, TEST_RET, set, FIXUP, repr); + + let global = st_interner.into_global(); + let mut interner = global.fork(); + + let in2 = interner.insert_lambda_set(arena, TEST_ARGS, TEST_RET, set, FIXUP, repr); + + assert_eq!(in1, in2); + } +} + +#[cfg(test)] +mod insert_recursive_layout { + use bumpalo::Bump; + use roc_target::TargetInfo; + + use crate::layout::{Builtin, InLayout, Layout, LayoutRepr, SemanticRepr, UnionLayout}; + + use super::{GlobalLayoutInterner, LayoutInterner}; + + const TARGET_INFO: TargetInfo = TargetInfo::default_x86_64(); + + fn make_layout<'a>(arena: &'a Bump, interner: &mut impl LayoutInterner<'a>) -> Layout<'a> { + let list_rec = Layout { + repr: LayoutRepr::Builtin(Builtin::List(Layout::NAKED_RECURSIVE_PTR)).direct(), + semantic: SemanticRepr::NONE, + }; + let repr = LayoutRepr::Union(UnionLayout::Recursive(&*arena.alloc([ + &*arena.alloc([interner.insert(list_rec)]), + &*arena.alloc_slice_fill_iter([interner.insert_direct_no_semantic( + LayoutRepr::struct_(&*arena.alloc([Layout::NAKED_RECURSIVE_PTR])), + )]), + ]))) + .direct(); + Layout { + repr, + semantic: SemanticRepr::NONE, + } + } + + fn get_rec_ptr_index<'a>(interner: &impl LayoutInterner<'a>, layout: InLayout<'a>) -> usize { + match interner.chase_recursive(layout) { + LayoutRepr::Union(UnionLayout::Recursive(&[&[l1], &[l2]])) => { + match (interner.get_repr(l1), interner.get_repr(l2)) { + (LayoutRepr::Builtin(Builtin::List(l1)), LayoutRepr::Struct(&[l2])) => { + match (interner.get_repr(l1), interner.get_repr(l2)) { + ( + LayoutRepr::RecursivePointer(i1), + LayoutRepr::RecursivePointer(i2), + ) => { + assert_eq!(i1, i2); + assert_ne!(i1, Layout::VOID); + i1.0 + } + _ => unreachable!(), + } + } + _ => unreachable!(), + } + } + _ => unreachable!(), + } + } + + #[test] + fn write_two_threads() { + let arena = &Bump::new(); + let global = GlobalLayoutInterner::with_capacity(2, TARGET_INFO); + let layout = { + let mut interner = global.fork(); + make_layout(arena, &mut interner) + }; + + let in1 = { + let mut interner = global.fork(); + interner.insert_recursive(arena, layout) + }; + + let in2 = { + let mut interner = global.fork(); + interner.insert_recursive(arena, layout) + }; + + assert_eq!(in1, in2); + } + + #[test] + fn write_twice_thread_local_single_thread() { + let arena = &Bump::new(); + let global = GlobalLayoutInterner::with_capacity(2, TARGET_INFO); + let mut interner = global.fork(); + let layout = make_layout(arena, &mut interner); + + let in1 = interner.insert_recursive(arena, layout); + let rec1 = get_rec_ptr_index(&interner, in1); + let in2 = interner.insert_recursive(arena, layout); + let rec2 = get_rec_ptr_index(&interner, in2); + + assert_eq!(in1, in2); + assert_eq!(rec1, rec2); + } + + #[test] + fn write_twice_single_thread() { + let arena = &Bump::new(); + let global = GlobalLayoutInterner::with_capacity(2, TARGET_INFO); + let mut interner = GlobalLayoutInterner::unwrap(global).unwrap(); + let layout = make_layout(arena, &mut interner); + + let in1 = interner.insert_recursive(arena, layout); + let rec1 = get_rec_ptr_index(&interner, in1); + let in2 = interner.insert_recursive(arena, layout); + let rec2 = get_rec_ptr_index(&interner, in2); + + assert_eq!(in1, in2); + assert_eq!(rec1, rec2); + } + + #[test] + fn many_threads_read_write() { + for _ in 0..100 { + let mut arenas: Vec<_> = std::iter::repeat_with(Bump::new).take(10).collect(); + let global = GlobalLayoutInterner::with_capacity(2, TARGET_INFO); + std::thread::scope(|s| { + let mut handles = Vec::with_capacity(10); + for arena in arenas.iter_mut() { + let mut interner = global.fork(); + let handle = s.spawn(move || { + let layout = make_layout(arena, &mut interner); + let in_layout = interner.insert_recursive(arena, layout); + (in_layout, get_rec_ptr_index(&interner, in_layout)) + }); + handles.push(handle); + } + let ins: Vec<(InLayout, usize)> = + handles.into_iter().map(|t| t.join().unwrap()).collect(); + let interned = ins[0]; + assert!(ins.iter().all(|in2| interned == *in2)); + }); + } + } + + #[test] + fn insert_then_reintern() { + let arena = &Bump::new(); + let global = GlobalLayoutInterner::with_capacity(2, TARGET_INFO); + let mut interner = global.fork(); + + let layout = make_layout(arena, &mut interner); + let interned_layout = interner.insert_recursive(arena, layout); + let full_layout = interner.get(interned_layout); + assert_ne!(layout, full_layout); + assert_eq!(interner.insert(full_layout), interned_layout); + } + + #[test] + fn write_global_then_single_threaded() { + let arena = &Bump::new(); + let global = GlobalLayoutInterner::with_capacity(2, TARGET_INFO); + let layout = { + let mut interner = global.fork(); + make_layout(arena, &mut interner) + }; + + let in1: InLayout = { + let mut interner = global.fork(); + interner.insert_recursive(arena, layout) + }; + + let in2 = { + let mut st_interner = global.unwrap().unwrap(); + st_interner.insert_recursive(arena, layout) + }; + + assert_eq!(in1, in2); + } + + #[test] + fn write_single_threaded_then_global() { + let arena = &Bump::new(); + let global = GlobalLayoutInterner::with_capacity(2, TARGET_INFO); + let mut st_interner = global.unwrap().unwrap(); + + let layout = make_layout(arena, &mut st_interner); + + let in1 = st_interner.insert_recursive(arena, layout); + + let global = st_interner.into_global(); + let mut interner = global.fork(); + + let in2 = interner.insert_recursive(arena, layout); + + assert_eq!(in1, in2); + } +} diff --git a/crates/compiler/mono/src/layout/semantic.rs b/crates/compiler/mono/src/layout/semantic.rs new file mode 100644 index 0000000000..293e719e59 --- /dev/null +++ b/crates/compiler/mono/src/layout/semantic.rs @@ -0,0 +1,85 @@ +//! Semantic representations of memory layouts for the purposes of specialization. + +use roc_module::symbol::Symbol; + +/// A semantic representation of a memory layout. +/// Semantic representations describe the shape of a type a [Layout][super::Layout] is generated +/// for. Semantic representations disambiguate types that have the same runtime memory layout, but +/// different shapes. +#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct SemanticRepr<'a>(Inner<'a>); + +impl<'a> std::fmt::Debug for SemanticRepr<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} + +impl<'a> SemanticRepr<'a> { + pub fn fmt_consistent(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt_consistent(f) + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] +enum Inner<'a> { + None, + Record(SemaRecord<'a>), + Tuple(SemaTuple), + TagUnion(SemaTagUnion<'a>), + Lambdas(SemaLambdas<'a>), +} + +impl<'a> Inner<'a> { + fn fmt_consistent(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + // don't print the interned string name of a symbol to get consistent output + if let Self::Lambdas(sema_lambdas) = self { + f.debug_struct("SemaLambdas") + .field("lambda_count", &sema_lambdas.lambdas.len()) + .finish() + } else { + std::fmt::Debug::fmt(&self, f) + } + } +} + +impl<'a> SemanticRepr<'a> { + pub(super) const NONE: Self = Self(Inner::None); + pub(super) const EMPTY_RECORD: Self = Self::record(&[]); + + pub(super) const fn record(fields: &'a [&'a str]) -> Self { + Self(Inner::Record(SemaRecord { fields })) + } + + pub(super) fn tuple(size: usize) -> Self { + Self(Inner::Tuple(SemaTuple { size })) + } + + pub(super) fn tag_union(tags: &'a [&'a str]) -> Self { + Self(Inner::TagUnion(SemaTagUnion { tags })) + } + + pub(super) fn lambdas(lambdas: &'a [Symbol]) -> Self { + Self(Inner::Lambdas(SemaLambdas { lambdas })) + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] +struct SemaRecord<'a> { + fields: &'a [&'a str], +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] +struct SemaTuple { + size: usize, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] +struct SemaTagUnion<'a> { + tags: &'a [&'a str], +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] +struct SemaLambdas<'a> { + lambdas: &'a [Symbol], +} diff --git a/crates/compiler/mono/src/lib.rs b/crates/compiler/mono/src/lib.rs new file mode 100644 index 0000000000..fffd47c13f --- /dev/null +++ b/crates/compiler/mono/src/lib.rs @@ -0,0 +1,21 @@ +//! Roc's main intermediate representation (IR), which is responsible for +//! [monomorphization](https://en.wikipedia.org/wiki/Monomorphization), +//! defunctionalization, inserting [ref-count](https://en.wikipedia.org/wiki/Reference_counting) +//! instructions, and transforming a Roc program into a form that is easy to +//! consume by a backend. +#![warn(clippy::dbg_macro)] +// See github.com/roc-lang/roc/issues/800 for discussion of the large_enum_variant check. +#![allow(clippy::large_enum_variant, clippy::upper_case_acronyms)] +// Not a useful lint for us +#![allow(clippy::too_many_arguments)] + +pub mod code_gen_help; +pub mod drop_specialization; +pub mod inc_dec; +pub mod ir; +pub mod layout; +pub mod low_level; +pub mod reset_reuse; +pub mod tail_recursion; + +pub mod debug; diff --git a/crates/compiler/mono/src/low_level.rs b/crates/compiler/mono/src/low_level.rs new file mode 100644 index 0000000000..d0fe15a8ea --- /dev/null +++ b/crates/compiler/mono/src/low_level.rs @@ -0,0 +1,137 @@ +use roc_module::symbol::Symbol; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum HigherOrder { + ListMap { + xs: Symbol, + }, + ListMap2 { + xs: Symbol, + ys: Symbol, + }, + ListMap3 { + xs: Symbol, + ys: Symbol, + zs: Symbol, + }, + ListMap4 { + xs: Symbol, + ys: Symbol, + zs: Symbol, + ws: Symbol, + }, + ListSortWith { + xs: Symbol, + }, +} + +impl HigherOrder { + pub fn function_arity(&self) -> usize { + match self { + HigherOrder::ListMap { .. } => 1, + HigherOrder::ListMap2 { .. } => 2, + HigherOrder::ListMap3 { .. } => 3, + HigherOrder::ListMap4 { .. } => 4, + HigherOrder::ListSortWith { .. } => 2, + } + } + + /// Index in the array of arguments of the symbol that is the closure data + /// (captured environment for this function) + pub const fn closure_data_index(&self) -> usize { + use HigherOrder::*; + + match self { + ListMap { .. } | ListSortWith { .. } => 2, + ListMap2 { .. } => 3, + ListMap3 { .. } => 4, + ListMap4 { .. } => 5, + } + } + + /// Index of the function symbol in the argument list + pub const fn function_index(&self) -> usize { + self.closure_data_index() - 1 + } +} + +#[allow(dead_code)] +enum FirstOrder { + StrConcat, + StrJoinWith, + StrIsEmpty, + StrStartsWith, + StrStartsWithScalar, + StrEndsWith, + StrSplit, + StrCountGraphemes, + StrFromInt, + StrFromUtf8, + StrFromUtf8Range, + StrToUtf8, + StrRepeat, + StrFromFloat, + ListLen, + ListGetUnsafe, + ListSublist, + ListDropAt, + ListConcat, + ListAppend, + ListPrepend, + ListSwap, + NumAdd, + NumAddWrap, + NumAddChecked, + NumSub, + NumSubWrap, + NumSubChecked, + NumMul, + NumMulWrap, + NumMulSaturated, + NumMulChecked, + NumGt, + NumGte, + NumLt, + NumLte, + NumCompare, + NumDivUnchecked, + NumRemUnchecked, + NumIsMultipleOf, + NumAbs, + NumNeg, + NumSin, + NumCos, + NumTan, + NumSqrtUnchecked, + NumLogUnchecked, + NumRound, + NumToFrac, + NumPow, + NumCeiling, + NumPowInt, + NumFloor, + NumIsNan, + NumIsInfinite, + NumIsFinite, + NumAtan, + NumAcos, + NumAsin, + NumBitwiseAnd, + NumBitwiseXor, + NumBitwiseOr, + NumShiftLeftBy, + NumShiftRightBy, + NumBytesToU16, + NumBytesToU32, + NumBytesToU64, + NumBytesToU128, + NumShiftRightZfBy, + NumIntCast, + NumFloatCast, + Eq, + NotEq, + And, + Or, + Not, + Hash, +} diff --git a/crates/compiler/mono/src/reset_reuse.rs b/crates/compiler/mono/src/reset_reuse.rs new file mode 100644 index 0000000000..cb893903dc --- /dev/null +++ b/crates/compiler/mono/src/reset_reuse.rs @@ -0,0 +1,1412 @@ +// This program was written by Jelle Teeuwissen within a final +// thesis project of the Computing Science master program at Utrecht +// University under supervision of Wouter Swierstra (w.s.swierstra@uu.nl). + +// Implementation based of Reference Counting with Frame Limited Reuse +// https://www.microsoft.com/en-us/research/uploads/prod/2021/11/flreuse-tr.pdf + +use std::hash::Hash; + +use crate::ir::{ + BranchInfo, Expr, JoinPointId, ModifyRc, Param, Proc, ProcLayout, ReuseToken, Stmt, + UpdateModeId, UpdateModeIds, +}; +use crate::layout::{InLayout, LayoutInterner, LayoutRepr, STLayoutInterner, UnionLayout}; + +use bumpalo::Bump; + +use bumpalo::collections::vec::Vec; +use bumpalo::collections::CollectIn; +use roc_collections::{MutMap, MutSet}; +use roc_module::low_level::LowLevel; +use roc_module::symbol::{IdentIds, ModuleId, Symbol}; +use roc_target::TargetInfo; + +/** + Insert reset and reuse operations into the IR. +To allow for the reuse of memory allocation when said memory is no longer used. + */ +pub fn insert_reset_reuse_operations<'a, 'i>( + arena: &'a Bump, + layout_interner: &'i STLayoutInterner<'a>, + home: ModuleId, + target_info: TargetInfo, + ident_ids: &'i mut IdentIds, + update_mode_ids: &'i mut UpdateModeIds, + procs: &mut MutMap<(Symbol, ProcLayout<'a>), Proc<'a>>, +) { + let mut global_layouts = SymbolLayout::default(); + for (symbol, _layout) in procs.keys() { + global_layouts.insert(*symbol, LayoutOption::GloballyDefined); + } + + for proc in procs.values_mut() { + let new_proc = insert_reset_reuse_operations_proc( + arena, + layout_interner, + target_info, + home, + ident_ids, + update_mode_ids, + global_layouts.clone(), + proc.clone(), + ); + *proc = new_proc; + } +} + +fn insert_reset_reuse_operations_proc<'a, 'i>( + arena: &'a Bump, + layout_interner: &'i STLayoutInterner<'a>, + target_info: TargetInfo, + home: ModuleId, + ident_ids: &'i mut IdentIds, + update_mode_ids: &'i mut UpdateModeIds, + mut symbol_layout: SymbolLayout<'a>, + mut proc: Proc<'a>, +) -> Proc<'a> { + for (layout, symbol) in proc.args { + symbol_layout.insert(*symbol, LayoutOption::Layout(layout)); + } + + let mut env = ReuseEnvironment { + target_info, + symbol_tags: MutMap::default(), + non_unique_symbols: MutSet::default(), + reuse_tokens: MutMap::default(), + symbol_layouts: symbol_layout, + joinpoint_reuse_tokens: MutMap::default(), + jump_reuse_tokens: MutMap::default(), + }; + + let new_body = insert_reset_reuse_operations_stmt( + arena, + layout_interner, + home, + ident_ids, + update_mode_ids, + &mut env, + arena.alloc(proc.body), + ); + + // All reuse tokens either have to be used by reuse or not be inserted at all at the reset (and removed from the environment). + debug_assert!(env.reuse_tokens.is_empty()); + + proc.body = new_body.clone(); + proc +} + +fn insert_reset_reuse_operations_stmt<'a, 'i>( + arena: &'a Bump, + layout_interner: &'i STLayoutInterner<'a>, + home: ModuleId, + ident_ids: &'i mut IdentIds, + update_mode_ids: &'i mut UpdateModeIds, + environment: &mut ReuseEnvironment<'a>, + stmt: &'a Stmt<'a>, +) -> &'a Stmt<'a> { + match stmt { + Stmt::Let(_, _, _, _) => { + // Collect all the subsequent let bindings (including the current one). + // To prevent the stack from overflowing when there are many let bindings. + let mut triples = vec![]; + let mut current_stmt = stmt; + while let Stmt::Let(binding, expr, layout, next_stmt) = current_stmt { + triples.push((binding, expr, layout)); + current_stmt = next_stmt + } + + debug_assert!( + !triples.is_empty(), + "Expected at least one let binding in the vector" + ); + debug_assert!( + !matches!(current_stmt, Stmt::Let(_, _, _, _)), + "All let bindings should be in the vector" + ); + + // Update the triplets with reuse operations. Making sure to update the environment before the next let binding. + let mut new_triplets = vec![]; + for (binding, expr, layout) in triples { + let new_expr = match expr { + Expr::Tag { + tag_layout, + tag_id, + arguments, + reuse, + } => { + debug_assert!(reuse.is_none()); + + // The value of the tag is currently only used in the case of nullable recursive unions. + // But for completeness we add every kind of union to the layout_tags. + environment.add_symbol_tag(*binding, *tag_id); + + // Check if the tag id for this layout can be reused at all. + match can_reuse_union_layout_tag(*tag_layout, Option::Some(*tag_id)) { + // The tag is reusable. + Reuse::Reusable(union_layout) => { + // See if we have a token. + match environment.pop_reuse_token(&get_reuse_layout_info( + layout_interner, + union_layout, + )) { + // We have a reuse token for this layout, use it. + Some(TokenWithInLayout { + token: mut reuse_token, + inlayout: layout_info, + }) => { + if layout_info == layout { + // The reuse token layout is the same, we can use it without casting. + ( + None, + Expr::Tag { + tag_layout: *tag_layout, + tag_id: *tag_id, + arguments, + reuse: Some(reuse_token), + }, + ) + } else { + // The reuse token has a different layout from the tag, we need to pointercast it before. + let new_symbol = + Symbol::new(home, ident_ids.gen_unique()); + + let ptr_cast = move |new_let| { + arena.alloc(Stmt::Let( + new_symbol, + create_ptr_cast(arena, reuse_token.symbol), + *layout, + new_let, + )) + }; + + // we now want to reuse the cast pointer + reuse_token.symbol = new_symbol; + + ( + Some(ptr_cast), + Expr::Tag { + tag_layout: *tag_layout, + tag_id: *tag_id, + arguments, + reuse: Some(reuse_token), + }, + ) + } + } + + // We have no reuse token available, keep the old expression with a fresh allocation. + None => (None, expr.clone()), + } + } + // We cannot reuse this tag id because it's a null pointer. + Reuse::Nonreusable => (None, expr.clone()), + } + } + _ => (None, expr.clone()), + }; + + environment.add_symbol_layout(*binding, layout); + new_triplets.push((binding, new_expr, layout)) + } + + let new_continuation = insert_reset_reuse_operations_stmt( + arena, + layout_interner, + home, + ident_ids, + update_mode_ids, + environment, + current_stmt, + ); + + new_triplets.into_iter().rev().fold( + new_continuation, + |new_continuation, (binding, (opt_ptr_cast, new_expr), layout)| { + let new_let = + arena.alloc(Stmt::Let(*binding, new_expr, *layout, new_continuation)); + + // if the layout for the reuse does not match that of the reset, use PtrCast to convert the layout. + match opt_ptr_cast { + Some(ptr_cast) => ptr_cast(new_let), + None => new_let, + } + }, + ) + } + Stmt::Switch { + cond_symbol, + cond_layout, + branches, + default_branch, + ret_layout, + } => { + macro_rules! update_env_with_constructor { + ($branch_env:expr, $info:expr) => {{ + match $info { + BranchInfo::Constructor { + scrutinee, + tag_id: tag, + .. + } => { + $branch_env.add_symbol_tag(*scrutinee, *tag); + } + BranchInfo::Unique { + scrutinee, + unique: false, + } => { + $branch_env.non_unique_symbols.insert(*scrutinee); + } + BranchInfo::None + | BranchInfo::List { .. } + | BranchInfo::Unique { unique: true, .. } => {} + } + }}; + } + + let new_branches = branches + .iter() + .map(|(tag_id, info, branch)| { + let mut branch_env = environment.clone(); + + update_env_with_constructor!(branch_env, info); + + let new_branch = insert_reset_reuse_operations_stmt( + arena, + layout_interner, + home, + ident_ids, + update_mode_ids, + &mut branch_env, + branch, + ); + + (*tag_id, info.clone(), new_branch, branch_env) + }) + .collect_in::>(arena); + + let new_default_branch = { + let (info, branch) = default_branch; + + let mut branch_env = environment.clone(); + + update_env_with_constructor!(branch_env, info); + + let new_branch = insert_reset_reuse_operations_stmt( + arena, + layout_interner, + home, + ident_ids, + update_mode_ids, + &mut branch_env, + branch, + ); + + (info.clone(), new_branch, branch_env) + }; + + // First we determine the minimum of reuse tokens available for each layout. + let layout_min_reuse_tokens = { + let branch_envs = { + let mut branch_environments = + Vec::with_capacity_in(new_branches.len() + 1, arena); + + for (_, _, _, branch_env) in new_branches.iter() { + branch_environments.push(branch_env); + } + + branch_environments.push(&new_default_branch.2); + + branch_environments + }; + + let layout_min_reuse_tokens = + environment.reuse_tokens.keys().copied().map(|layout| { + let min_reuse_tokens = branch_envs + .iter() + .map(|branch_environment| { + branch_environment + .reuse_tokens + .get(&layout) + .map_or(0, |tokens| tokens.len()) + }) + .min() + .expect("There should be at least one branch"); + (layout, min_reuse_tokens) + }); + + layout_min_reuse_tokens.collect::>() + }; + + // Then we drop any unused reuse tokens in branches where the minimum is not reached. + let msg = + "All layouts in the environment should be in the layout_min_reuse_tokens map."; + let newer_branches = Vec::from_iter_in( + new_branches + .iter() + .map(|(label, info, branch, branch_env)| { + let unused_tokens = branch_env + .reuse_tokens + .iter() + .flat_map(|(layout, reuse_tokens)| { + let min_reuse_tokens = + layout_min_reuse_tokens.get(layout).expect(msg); + &reuse_tokens[*min_reuse_tokens..] + }) + .map(|token_with_layout| token_with_layout.token); + + let newer_branch = drop_unused_reuse_tokens(arena, unused_tokens, branch); + + (*label, info.clone(), newer_branch.clone()) + }), + arena, + ) + .into_bump_slice(); + + let newer_default_branch = { + // let (info, branch, branch_env) = new_default_branch; + let unused_tokens= new_default_branch.2.reuse_tokens.iter().flat_map(|(layout, reuse_tokens) |{ + let min_reuse_tokens = layout_min_reuse_tokens.get(layout).expect("All layouts in the environment should be in the layout_min_reuse_tokens map."); + &reuse_tokens[*min_reuse_tokens..] + }).map(|token_with_layout|{token_with_layout.token}); + + let newer_branch = + drop_unused_reuse_tokens(arena, unused_tokens, new_default_branch.1); + + (new_default_branch.0, newer_branch) + }; + + // And finally we update the current environment to reflect the correct number of reuse tokens. + for (layout, reuse_tokens) in environment.reuse_tokens.iter_mut() { + let min_reuse_tokens = layout_min_reuse_tokens.get(layout).expect( + "All layouts in the environment should be in the layout_min_reuse_tokens map.", + ); + reuse_tokens.truncate(*min_reuse_tokens) + } + + // And remove any layouts that are no longer used. + environment + .reuse_tokens + .retain(|_, reuse_tokens| !reuse_tokens.is_empty()); + + // Propagate jump reuse tokens upwards. + environment.propagate_jump_reuse_tokens( + new_branches + .into_iter() + .map(|(_, _, _, branch_env)| branch_env) + .chain([new_default_branch.2]), + ); + + arena.alloc(Stmt::Switch { + cond_symbol: *cond_symbol, + cond_layout: *cond_layout, + branches: newer_branches, + default_branch: newer_default_branch, + ret_layout: *ret_layout, + }) + } + Stmt::Refcounting(rc, continuation) => { + enum SymbolIsUnique { + Never, + Always(Symbol), + MustCheck(Symbol), + } + + let can_reuse = match rc { + ModifyRc::Dec(symbol) | ModifyRc::DecRef(symbol) => { + // can only reuse if the symbol is (potentially) unique + if environment.non_unique_symbols.contains(symbol) { + SymbolIsUnique::Never + } else { + SymbolIsUnique::MustCheck(*symbol) + } + } + ModifyRc::Free(symbol) => { + // a free'd symbol is guaranteed to be unique + SymbolIsUnique::Always(*symbol) + } + ModifyRc::Inc(_, _) => { + // an incremented symbol is never unique + SymbolIsUnique::Never + } + }; + + enum ResetOperation { + Reset, + ResetRef, + ClearTagId, + Nothing, + } + + let reuse_pair = match can_reuse { + SymbolIsUnique::MustCheck(symbol) | SymbolIsUnique::Always(symbol) => { + // Get the layout of the symbol from where it is defined. + let layout_option = environment.get_symbol_layout(symbol); + + // If the symbol is defined in the current proc, we can use the layout from the environment. + match layout_option { + LayoutOption::Layout(layout) => { + match symbol_layout_reusability( + layout_interner, + environment, + &symbol, + layout, + ) { + Reuse::Reusable(union_layout) => { + let (reuse_symbol, reset_op) = match rc { + ModifyRc::Dec(_) => ( + Symbol::new(home, ident_ids.gen_unique()), + ResetOperation::Reset, + ), + ModifyRc::DecRef(_) => ( + Symbol::new(home, ident_ids.gen_unique()), + ResetOperation::ResetRef, + ), + ModifyRc::Free(_) => { + if union_layout + .stores_tag_id_in_pointer(environment.target_info) + { + ( + Symbol::new(home, ident_ids.gen_unique()), + ResetOperation::ClearTagId, + ) + } else { + (symbol, ResetOperation::Nothing) + } + } + _ => unreachable!(), + }; + + let reuse_token = ReuseToken { + symbol: reuse_symbol, + update_mode: update_mode_ids.next_id(), + // for now, always overwrite the tag ID just to be sure + update_tag_id: true, + }; + + let owned_layout = **layout; + + environment.push_reuse_token( + arena, + get_reuse_layout_info(layout_interner, union_layout), + reuse_token, + layout, + ); + + Some(( + owned_layout, + union_layout, + symbol, + reuse_token, + reset_op, + )) + } + Reuse::Nonreusable => None, + } + } + _ => None, + } + } + SymbolIsUnique::Never => { + // We don't need to do anything for an inc or symbols known to be non-unique. + None + } + }; + + let new_continuation = insert_reset_reuse_operations_stmt( + arena, + layout_interner, + home, + ident_ids, + update_mode_ids, + environment, + continuation, + ); + + // If we inserted a reuse token, we need to insert a reset reuse operation if the reuse token is consumed. + if let Some((layout, union_layout, symbol, reuse_token, reset_op)) = reuse_pair { + let stack_reuse_token = environment + .peek_reuse_token(&get_reuse_layout_info(layout_interner, union_layout)); + + match stack_reuse_token { + Some(token_with_layout) if token_with_layout.token == reuse_token => { + // The token we inserted is still on the stack, so we don't need to insert a reset operation. + // We do need to remove the token from the environment. To prevent errors higher in the tree. + let _ = environment + .pop_reuse_token(&get_reuse_layout_info(layout_interner, union_layout)); + } + _ => { + // The token we inserted is no longer on the stack, it must have been consumed. + // So we need to insert a reset operation. + match reset_op { + ResetOperation::Reset => { + // a dec will be replaced by a reset. + let reset_expr = Expr::Reset { + symbol, + update_mode: reuse_token.update_mode, + }; + + return arena.alloc(Stmt::Let( + reuse_token.symbol, + reset_expr, + layout, + new_continuation, + )); + } + ResetOperation::ResetRef => { + // a decref will be replaced by a resetref. + let reset_expr = Expr::ResetRef { + symbol, + update_mode: reuse_token.update_mode, + }; + + return arena.alloc(Stmt::Let( + reuse_token.symbol, + reset_expr, + layout, + new_continuation, + )); + } + ResetOperation::ClearTagId => { + let reset_expr = Expr::Call(crate::ir::Call { + call_type: crate::ir::CallType::LowLevel { + op: LowLevel::PtrClearTagId, + update_mode: update_mode_ids.next_id(), + }, + arguments: arena.alloc([symbol]), + }); + + return arena.alloc(Stmt::Let( + reuse_token.symbol, + reset_expr, + layout, + new_continuation, + )); + } + ResetOperation::Nothing => { + // the reuse token is already in a valid state + return new_continuation; + } + } + } + } + } + + // TODO update jump join points for the returned environment. + + arena.alloc(Stmt::Refcounting(*rc, new_continuation)) + } + Stmt::Ret(_) => { + // The return statement just doesn't consume any tokens. Dropping these tokens will be handled before. + stmt + } + Stmt::Expect { + condition, + region, + lookups, + variables, + remainder, + } => { + let new_remainder = insert_reset_reuse_operations_stmt( + arena, + layout_interner, + home, + ident_ids, + update_mode_ids, + environment, + remainder, + ); + + arena.alloc(Stmt::Expect { + condition: *condition, + region: *region, + lookups, + variables, + remainder: new_remainder, + }) + } + Stmt::ExpectFx { + condition, + region, + lookups, + variables, + remainder, + } => { + let new_remainder = insert_reset_reuse_operations_stmt( + arena, + layout_interner, + home, + ident_ids, + update_mode_ids, + environment, + remainder, + ); + + arena.alloc(Stmt::ExpectFx { + condition: *condition, + region: *region, + lookups, + variables, + remainder: new_remainder, + }) + } + Stmt::Dbg { + symbol, + variable, + remainder, + } => { + let new_remainder = insert_reset_reuse_operations_stmt( + arena, + layout_interner, + home, + ident_ids, + update_mode_ids, + environment, + remainder, + ); + + arena.alloc(Stmt::Dbg { + symbol: *symbol, + variable: *variable, + remainder: new_remainder, + }) + } + Stmt::Join { + id: joinpoint_id, + parameters, + body, + remainder, + } => { + // First we evaluate the remainder, to see what reuse tokens are available at each jump. We generate code as if no reuse tokens are used. + // Then we evaluate the body, to see what reuse tokens are consumed by the body. + // - If no reuse tokens are consumed (or when there were no available in the previous step), we stop here and return the first pass symbols. + // Then we evaluate the body and remainder again, given the consumed reuse tokens. And we update the joinpoint parameters. + + let (first_pass_remainder_environment, first_pass_remainder) = { + let mut first_pass_environment = environment.clone(); + + first_pass_environment.add_joinpoint_reuse_tokens( + *joinpoint_id, + JoinPointReuseTokens::RemainderFirst, + ); + + let first_pass_remainder = insert_reset_reuse_operations_stmt( + arena, + layout_interner, + home, + ident_ids, + update_mode_ids, + &mut first_pass_environment, + remainder, + ); + + first_pass_environment.remove_joinpoint_reuse_tokens(*joinpoint_id); + + (first_pass_environment, first_pass_remainder) + }; + + let max_reuse_tokens = + match first_pass_remainder_environment.get_jump_reuse_tokens(*joinpoint_id) { + Some(all_reuse_maps) => { + let all_token_layouts = all_reuse_maps + .iter() + .flat_map(|reuse_map| reuse_map.keys()) + // PERF: replace this collect with an unique iterator. To make sure every layout is only used once. + .collect::>() + .into_iter(); + let reuse_layouts_max_tokens = all_token_layouts.map(|token_layout| { + // We get the tokens from the jump with the most tokens for this token layout. + // So we have an inlayout for each token. And can cast when needed. + let max_token_inlayouts = all_reuse_maps + .iter() + .filter_map(|reuse_map| reuse_map.get(token_layout)) + .max_by_key(|tokens| tokens.len()) + .expect("all layouts should be in at least one of the reuse maps"); + (token_layout, max_token_inlayouts) + }); + Vec::from_iter_in(reuse_layouts_max_tokens, arena) + } + // Normally the remainder should always have jumps and this would not be None, + // But for testing this might not be the case, so default to no available reuse tokens. + None => Vec::new_in(arena), + }; + + let (first_pass_body_environment, first_pass_body, used_reuse_tokens) = { + // For each possibly available reuse token, create a reuse token to add to the join point environment. + let max_reuse_token_symbols = max_reuse_tokens + .iter() + .copied() + .map(|(token_layout, tokens)| { + ( + *token_layout, + Vec::from_iter_in( + tokens.iter().map(|token| TokenWithInLayout { + token: ReuseToken { + symbol: Symbol::new(home, ident_ids.gen_unique()), + update_mode: update_mode_ids.next_id(), + // for now, always overwrite the tag ID just to be sure + update_tag_id: true, + }, + inlayout: token.inlayout, + }), + arena, + ), + ) + }) + .collect::(); + + // Create a new environment for the body. With everything but the jump reuse tokens. As those should be given by the jump. + let mut first_pass_body_environment = ReuseEnvironment { + target_info: environment.target_info, + symbol_tags: environment.symbol_tags.clone(), + non_unique_symbols: environment.non_unique_symbols.clone(), + reuse_tokens: max_reuse_token_symbols.clone(), + symbol_layouts: environment.symbol_layouts.clone(), + joinpoint_reuse_tokens: environment.joinpoint_reuse_tokens.clone(), + jump_reuse_tokens: environment.jump_reuse_tokens.clone(), + }; + + // Add the parameters to the body environment as well. + for param in parameters.iter() { + first_pass_body_environment.add_symbol_layout(param.symbol, ¶m.layout); + } + + // Add a entry so that the body knows any jumps to this join point is recursive. + first_pass_body_environment + .add_joinpoint_reuse_tokens(*joinpoint_id, JoinPointReuseTokens::BodyFirst); + + let first_pass_body = insert_reset_reuse_operations_stmt( + arena, + layout_interner, + home, + ident_ids, + update_mode_ids, + &mut first_pass_body_environment, + body, + ); + + first_pass_body_environment.remove_joinpoint_reuse_tokens(*joinpoint_id); + + let used_reuse_tokens = { + max_reuse_token_symbols + .iter() + .filter_map(|(layout, reuse_tokens)| { + match first_pass_body_environment.reuse_tokens.get(layout) { + Some(remaining_tokens) => { + // There are tokens left, remove those from the bottom of the stack and return the consumed ones. + let mut consumed_reuse_tokens = reuse_tokens + .iter() + .skip(remaining_tokens.len()) + .copied() + .peekable(); + + #[allow(clippy::manual_map)] + match consumed_reuse_tokens.peek() { + // If there are no consumed tokens, remove the layout from the map. + None => None, + // Otherwise return the layout and the consumed tokens. + Some(_) => Some(( + *layout, + Vec::from_iter_in(consumed_reuse_tokens, arena), + )), + } + } + None => { + // All tokens were consumed. Meaning all of them should be passed from the jump. Keep tokens as is. + Some((*layout, reuse_tokens.clone())) + } + } + }) + .collect::() + }; + + ( + first_pass_body_environment, + first_pass_body, + used_reuse_tokens, + ) + }; + + // In the evaluation of the body and remainder we assumed no reuse tokens to be used. + // So if there indeed are no reuse tokens used, we can just return the body and remainder as is. + if used_reuse_tokens.is_empty() { + // We evaluated the first pass using a cloned environment to be able to do a second pass with the same environment. + // But if we don't need a second environment, we override the passed env with the first pass env. + *environment = first_pass_remainder_environment.clone(); + + // Propagate jump reuse tokens upwards. + environment + .propagate_jump_reuse_tokens(std::iter::once(first_pass_body_environment)); + + return arena.alloc(Stmt::Join { + id: *joinpoint_id, + parameters, + body: first_pass_body, + remainder: first_pass_remainder, + }); + } + + let layouts_for_reuse = Vec::from_iter_in( + used_reuse_tokens.iter().flat_map(|(layout, tokens)| { + tokens.iter().map(|token| (token.inlayout, *layout)) + }), + arena, + ); + + let second_pass_remainder = { + environment.add_joinpoint_reuse_tokens( + *joinpoint_id, + JoinPointReuseTokens::RemainderSecond(layouts_for_reuse.clone()), + ); + + let second_pass_remainder = insert_reset_reuse_operations_stmt( + arena, + layout_interner, + home, + ident_ids, + update_mode_ids, + environment, + remainder, + ); + + environment.remove_joinpoint_reuse_tokens(*joinpoint_id); + + second_pass_remainder + }; + + let extended_parameters = { + let layouts_for_reuse_with_token = Vec::from_iter_in( + used_reuse_tokens + .iter() + .flat_map(|(layout, tokens)| tokens.iter().map(|token| (*layout, *token))), + arena, + ); + + let token_params = + layouts_for_reuse_with_token + .into_iter() + .map(|(_reuse_layout, token)| Param { + symbol: token.token.symbol, + layout: *token.inlayout, + }); + + // Add the reuse tokens to the join arguments to match the expected arguments of the jump. + let extended_parameters = + Vec::from_iter_in(parameters.iter().copied().chain(token_params), arena) + .into_bump_slice(); + + extended_parameters + }; + + if first_pass_body_environment + .get_jump_reuse_tokens(*joinpoint_id) + .is_none() + { + // The body has no jumps to this join point. So we can just return the body and remainder as is. + // As there are no jumps to update. + + // Propagate jump reuse tokens upwards. + environment + .propagate_jump_reuse_tokens(std::iter::once(first_pass_body_environment)); + + return arena.alloc(Stmt::Join { + id: *joinpoint_id, + parameters: extended_parameters, + body: first_pass_body, + remainder: second_pass_remainder, + }); + } + + let (second_pass_body_environment, second_pass_body) = { + // Create a new environment for the body. With everything but the jump reuse tokens. As those should be given by the jump. + let mut body_environment = ReuseEnvironment { + target_info: environment.target_info, + symbol_tags: environment.symbol_tags.clone(), + non_unique_symbols: environment.non_unique_symbols.clone(), + reuse_tokens: used_reuse_tokens.clone(), + symbol_layouts: environment.symbol_layouts.clone(), + joinpoint_reuse_tokens: environment.joinpoint_reuse_tokens.clone(), + jump_reuse_tokens: environment.jump_reuse_tokens.clone(), + }; + + // Add the parameters to the body environment as well. + for param in parameters.iter() { + body_environment.add_symbol_layout(param.symbol, ¶m.layout); + } + + // Add a entry so that the body knows any jumps to this join point is recursive. + body_environment.add_joinpoint_reuse_tokens( + *joinpoint_id, + JoinPointReuseTokens::BodySecond(layouts_for_reuse), + ); + + let second_pass_body = insert_reset_reuse_operations_stmt( + arena, + layout_interner, + home, + ident_ids, + update_mode_ids, + &mut body_environment, + body, + ); + + body_environment.remove_joinpoint_reuse_tokens(*joinpoint_id); + + (body_environment, second_pass_body) + }; + + environment.propagate_jump_reuse_tokens(std::iter::once(second_pass_body_environment)); + + arena.alloc(Stmt::Join { + id: *joinpoint_id, + parameters: extended_parameters, + body: second_pass_body, + remainder: second_pass_remainder, + }) + } + Stmt::Jump(id, arguments) => { + // TODO make sure that the reuse tokens that are provided by most jumps are the tokens that are used in most paths. + let joinpoint_tokens = environment.get_joinpoint_reuse_tokens(*id); + + match joinpoint_tokens { + JoinPointReuseTokens::RemainderFirst | JoinPointReuseTokens::BodyFirst => { + // For both the first pass of the continuation and the body, act as if there are no tokens to reuse. + environment.add_jump_reuse_tokens(*id, environment.reuse_tokens.clone()); + arena.alloc(Stmt::Jump(*id, arguments)) + } + JoinPointReuseTokens::RemainderSecond(token_layouts) => { + // If there are no tokens to reuse, we can just jump. + if token_layouts.is_empty() { + return arena.alloc(Stmt::Jump(*id, arguments)); + } + + let token_layouts_clone = token_layouts.clone(); + + let mut reuse_tokens_to_cast = Vec::new_in(arena); + let mut void_pointer_layout_symbols = Vec::new_in(arena); + + // See what tokens we can get from the env, if none are available, use a void pointer. + // We process the tokens in reverse order, so that when we consume the tokens we last added, + // we consume the tokens that are most likely not to be null. + let tokens = token_layouts_clone + .iter() + .rev() + .map(|(param_layout, token_layout)| { + match environment.pop_reuse_token(token_layout) { + Some(reuse_token) => { + if reuse_token.inlayout != *param_layout { + let new_symbol = Symbol::new(home, ident_ids.gen_unique()); + reuse_tokens_to_cast.push(( + *param_layout, + reuse_token.token.symbol, + new_symbol, + )); + new_symbol + } else { + reuse_token.token.symbol + } + } + None => match void_pointer_layout_symbols + .iter() + .find(|(layout, _)| layout == param_layout) + { + Some(existing_symbol) => existing_symbol.1, + None => { + let new_symbol = Symbol::new(home, ident_ids.gen_unique()); + void_pointer_layout_symbols + .push((*param_layout, new_symbol)); + new_symbol + } + }, + } + }) + // Collect to prevent revs from cancelling out. + .collect_in::>(arena); + + // Add the void tokens to the jump arguments to match the expected arguments of the join point. + let extended_arguments = Vec::from_iter_in( + arguments + .iter() + .copied() + .chain(tokens.iter().copied().rev()), + arena, + ) + .into_bump_slice(); + + let casted_tokens = reuse_tokens_to_cast.into_iter().fold( + arena.alloc(Stmt::Jump(*id, extended_arguments)), + |child, (layout, old_symbol, new_symbol)| { + arena.alloc(Stmt::Let( + new_symbol, + create_ptr_cast(arena, old_symbol), + *layout, + child, + )) + }, + ); + + // Wrap the jump in a let statement for each void pointer token layout. + void_pointer_layout_symbols.into_iter().fold( + casted_tokens, + |child, (param_layout, symbol)| { + arena.alloc(Stmt::Let(symbol, Expr::NullPointer, *param_layout, child)) + }, + ) + } + JoinPointReuseTokens::BodySecond(token_layouts) => { + // If there are no tokens to reuse, we can just jump. + if token_layouts.is_empty() { + return arena.alloc(Stmt::Jump(*id, arguments)); + } + + // We currently don't pass any reuse tokens to recursive jumps. + // This is to avoid keeping reuse tokens alive for too long. But it could be changed. + let mut void_pointer_layout_symbols: std::vec::Vec<(&'a InLayout<'a>, Symbol)> = + vec![]; + + let void_tokens = token_layouts.iter().map(|(param_layout, _token_layout)| { + match void_pointer_layout_symbols + .iter() + .find(|(void_layout, _)| void_layout == param_layout) + { + Some(existing_symbol) => existing_symbol.1, + None => { + let new_symbol = Symbol::new(home, ident_ids.gen_unique()); + void_pointer_layout_symbols.push((*param_layout, new_symbol)); + new_symbol + } + } + }); + + // Add the void tokens to the jump arguments to match the expected arguments of the join point. + let extended_arguments = + Vec::from_iter_in(arguments.iter().copied().chain(void_tokens), arena) + .into_bump_slice(); + + // Wrap the jump in a let statement for each void pointer token layout. + void_pointer_layout_symbols.into_iter().fold( + arena.alloc(Stmt::Jump(*id, extended_arguments)), + |child, (layout, symbol)| { + arena.alloc(Stmt::Let(symbol, Expr::NullPointer, *layout, child)) + }, + ) + } + } + } + Stmt::Crash(_, _) => stmt, + } +} + +fn create_ptr_cast(arena: &Bump, symbol: Symbol) -> Expr { + Expr::Call(crate::ir::Call { + call_type: crate::ir::CallType::LowLevel { + op: LowLevel::PtrCast, + update_mode: UpdateModeId::BACKEND_DUMMY, + }, + arguments: Vec::from_iter_in([symbol], arena).into_bump_slice(), + }) +} + +// TODO make sure all dup/drop operations are already inserted statically. +// (e.g. not as a side effect of another operation) To make sure we can actually reuse. + +enum Reuse<'a> { + // Reuseable but the pointer *might* be null, which will cause a fresh allocation. + Reusable(UnionLayout<'a>), + Nonreusable, +} + +/** +Map containing the reuse tokens of a layout. +A vec is used as a stack as we want to use the latest reuse token available. +*/ +type ReuseTokens<'a> = MutMap>>; + +/** +Struct to to check whether two reuse layouts are interchangeable. +*/ +#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] +struct TokenLayout { + size: u32, + alignment: u32, +} + +/** +Combines a reuse token with it's possible layout info. +*/ +#[derive(Clone, Copy, PartialEq, Eq)] +struct TokenWithInLayout<'a> { + token: ReuseToken, + inlayout: &'a InLayout<'a>, +} + +type Tag = u16; + +/** +Map containing the layout of a symbol. +Used to determine whether the pointer of a symbol can be reused, if it is reference counted (heap allocated). +*/ +type SymbolLayout<'a> = MutMap>; + +#[derive(Clone)] +enum LayoutOption<'a> { + // A normal layout defined in the current function. + Layout(&'a InLayout<'a>), + + // No layout as this symbol is defined in a global scope and should not be reused. + GloballyDefined, +} + +#[derive(Clone)] +enum JoinPointReuseTokens<'a> { + // The body indicates that we currently in the body of the join point. + // This means that (for now) don't pass any reuse tokens from the jump. + // As we only use reuse tokens from jumps outside the join point. + BodyFirst, + + // Second body pass, to update any jump calls to pass void pointer parameters instead of no parameters. + BodySecond(Vec<'a, (&'a InLayout<'a>, TokenLayout)>), + + // The first pass is used to determine the amount of reuse tokens a join point can expect. + // Therefore, we don't know the amount of reuse tokens yet. + RemainderFirst, + + // In the second pass, we determined the amount of reuse tokens a join point can expect. + // Therefore, we know the amount of reuse tokens and can use. + RemainderSecond(Vec<'a, (&'a InLayout<'a>, TokenLayout)>), +} + +#[derive(Clone)] +struct ReuseEnvironment<'a> { + target_info: TargetInfo, + symbol_tags: MutMap, + non_unique_symbols: MutSet, + reuse_tokens: ReuseTokens<'a>, + symbol_layouts: SymbolLayout<'a>, + // A map containing the amount of reuse tokens a join point expects for each layout. + joinpoint_reuse_tokens: MutMap>, + // A map containing the reuse tokens for each jump. + jump_reuse_tokens: MutMap>>, +} + +impl<'a> ReuseEnvironment<'a> { + /** + Add the known tag for a layout. + Used to optimize reuse of unions that are know to have a null pointer. + */ + fn add_symbol_tag(&mut self, symbol: Symbol, tag: Tag) { + self.symbol_tags.insert(symbol, tag); + } + + /** + Retrieve the known tag for a layout. + */ + fn get_symbol_tag(&self, symbol: &Symbol) -> Option { + self.symbol_tags.get(symbol).copied() + } + + /** + Retrieve a reuse token for a layout from the stack for said layout. + */ + fn pop_reuse_token(&mut self, token_layout: &TokenLayout) -> Option { + let reuse_tokens = self.reuse_tokens.get_mut(token_layout)?; + // If the layout is in the map, pop the token from the stack. + let reuse_token = reuse_tokens.pop(); + // If the stack is empty, remove the layout from the map. + if reuse_tokens.is_empty() { + self.reuse_tokens.remove(token_layout); + } + reuse_token + } + + /** + Retrieve a reuse token for a layout from the stack for said layout. + Without consuming the token. + */ + fn peek_reuse_token(&mut self, token_layout: &TokenLayout) -> Option { + let reuse_tokens = self.reuse_tokens.get(token_layout)?; + // If the layout is in the map, peek at the last element. + let reuse_token = reuse_tokens.last(); + reuse_token.copied() + } + + /** + Push a reuse token for a layout on the stack for said layout. + */ + fn push_reuse_token( + &mut self, + arena: &'a Bump, + token_layout: TokenLayout, + reuse_token: ReuseToken, + layout: &'a InLayout<'a>, + ) { + let with_info = TokenWithInLayout { + token: reuse_token, + inlayout: layout, + }; + self.reuse_tokens + .entry(token_layout) + .and_modify(|reuse_tokens| reuse_tokens.push(with_info)) + .or_insert_with(|| Vec::from_iter_in([with_info], arena)); + } + + /** + Add the layout of a symbol. + */ + fn add_symbol_layout(&mut self, symbol: Symbol, layout: &'a InLayout<'a>) { + self.symbol_layouts + .insert(symbol, LayoutOption::Layout(layout)); + } + + /** + Retrieve the layout of a symbol. + */ + fn get_symbol_layout(&self, symbol: Symbol) -> &LayoutOption<'a> { + self.symbol_layouts.get(&symbol).expect("Expected symbol to have a layout. It should have been inserted in the environment already.") + } + + /** + Add the reuse tokens of a jump to be used by a join point. + */ + fn add_jump_reuse_tokens(&mut self, joinpoint_id: JoinPointId, reuse_tokens: ReuseTokens<'a>) { + match self.jump_reuse_tokens.get_mut(&joinpoint_id) { + Some(jump_reuse_tokens) => { + jump_reuse_tokens.push(reuse_tokens); + } + None => { + self.jump_reuse_tokens + .insert(joinpoint_id, vec![reuse_tokens]); + } + }; + } + + /** + Propagate the reuse tokens of jumps from multiple environments to the current environment. + */ + fn propagate_jump_reuse_tokens(&mut self, envs: impl Iterator>) { + for (joinpoint_id, layout_reuse_tokens) in + envs.flat_map(|env| env.jump_reuse_tokens.into_iter()) + { + for layout_reuse_token in layout_reuse_tokens.iter() { + self.add_jump_reuse_tokens(joinpoint_id, layout_reuse_token.clone()); + } + } + } + + /** + Get the all available reuse tokens from all jumps to a join point. + */ + fn get_jump_reuse_tokens( + &self, + joinpoint_id: JoinPointId, + ) -> Option<&std::vec::Vec>> { + self.jump_reuse_tokens.get(&joinpoint_id) + } + + /** + Insert join_point_reuse_tokens for a join point. + */ + fn add_joinpoint_reuse_tokens( + &mut self, + joinpoint_id: JoinPointId, + join_point_reuse_tokens: JoinPointReuseTokens<'a>, + ) { + self.joinpoint_reuse_tokens + .insert(joinpoint_id, join_point_reuse_tokens); + } + + /** + Retrieve the reuse tokens amount of a join point. + */ + fn get_joinpoint_reuse_tokens(&self, joinpoint_id: JoinPointId) -> &JoinPointReuseTokens<'a> { + self.joinpoint_reuse_tokens + .get(&joinpoint_id) + .expect("Expected join point to have reuse tokens.") + } + + /** + Remove the reuse tokens of a joinpoint for cleanup + */ + fn remove_joinpoint_reuse_tokens(&mut self, joinpoint_id: JoinPointId) { + self.joinpoint_reuse_tokens.remove(&joinpoint_id); + } +} + +/** +Check if a layout can be reused. by verifying if the layout is a union and if the tag is not nullable. +*/ +fn symbol_layout_reusability<'a>( + layout_interner: &STLayoutInterner<'a>, + environment: &ReuseEnvironment<'a>, + symbol: &Symbol, + layout: &InLayout<'a>, +) -> Reuse<'a> { + match layout_interner.get_repr(*layout) { + LayoutRepr::Union(union_layout) => { + can_reuse_union_layout_tag(union_layout, environment.get_symbol_tag(symbol)) + } + // Strings literals are constants. + // Arrays are probably given to functions and reused there. Little use to reuse them here. + _ => Reuse::Nonreusable, + } +} + +/** + Check if a union layout can be reused. by verifying if the tag is not nullable. +*/ +fn can_reuse_union_layout_tag(union_layout: UnionLayout, tag_id_option: Option) -> Reuse { + match union_layout { + UnionLayout::NonRecursive(_) => Reuse::Nonreusable, + // Non nullable union layouts + UnionLayout::Recursive(_) | UnionLayout::NonNullableUnwrapped(_) => { + // Non nullable union layouts can always be reused. + Reuse::Reusable(union_layout) + } + // Nullable union layouts + UnionLayout::NullableWrapped { .. } | UnionLayout::NullableUnwrapped { .. } => { + match tag_id_option { + Some(tag_id) => { + if union_layout.tag_is_null(tag_id) { + // Symbol of layout is always null, so it can't ever be reused. + Reuse::Nonreusable + } else { + // Symbol of layout is not null, so it can be reused. + Reuse::Reusable(union_layout) + } + } + None => { + // Symbol of layout might be null, so it might be reused. + // If null will cause null pointer and fresh allocation. + Reuse::Reusable(union_layout) + } + } + } + } +} + +/** +Drop the reuse tokens that are not used anymore. +Useful when reuse tokens are used in a branch, and thus should be created. +But not in all branches, and thus should be dropped in those branches. +*/ +fn drop_unused_reuse_tokens<'a>( + arena: &'a Bump, + unused_tokens: impl Iterator, + continuation: &'a Stmt<'a>, +) -> &'a Stmt<'a> { + unused_tokens.fold(continuation, |continuation, reuse_token| { + arena.alloc(Stmt::Refcounting( + ModifyRc::DecRef(reuse_token.symbol), + continuation, + )) + }) +} + +fn get_reuse_layout_info<'a>( + layout_interner: &STLayoutInterner<'a>, + union_layout: UnionLayout<'a>, +) -> TokenLayout { + let (size, alignment) = union_layout.data_size_and_alignment(layout_interner); + + TokenLayout { size, alignment } +} diff --git a/crates/compiler/mono/src/tail_recursion.rs b/crates/compiler/mono/src/tail_recursion.rs new file mode 100644 index 0000000000..5b6bcd29f4 --- /dev/null +++ b/crates/compiler/mono/src/tail_recursion.rs @@ -0,0 +1,1132 @@ +#![allow(clippy::manual_map)] + +use crate::ir::{ + Call, CallType, Expr, JoinPointId, Param, Proc, ProcLayout, SelfRecursive, Stmt, UpdateModeId, +}; +use crate::layout::{ + InLayout, LambdaName, Layout, LayoutInterner, LayoutRepr, STLayoutInterner, TagIdIntType, + UnionLayout, +}; +use bumpalo::collections::Vec; +use bumpalo::Bump; +use roc_collections::{MutMap, VecMap}; +use roc_module::low_level::LowLevel; +use roc_module::symbol::{IdentIds, ModuleId, Symbol}; + +pub struct Env<'a, 'i> { + arena: &'a Bump, + home: ModuleId, + interner: &'i mut STLayoutInterner<'a>, + ident_ids: &'i mut IdentIds, +} + +impl<'a, 'i> Env<'a, 'i> { + fn unique_symbol(&mut self) -> Symbol { + let ident_id = self.ident_ids.gen_unique(); + + Symbol::new(self.home, ident_id) + } + + fn named_unique_symbol(&mut self, name: &str) -> Symbol { + let ident_id = self.ident_ids.add_str(name); + Symbol::new(self.home, ident_id) + } +} + +pub fn apply_trmc<'a, 'i>( + arena: &'a Bump, + interner: &'i mut STLayoutInterner<'a>, + home: ModuleId, + ident_ids: &'i mut IdentIds, + procs: &mut MutMap<(Symbol, ProcLayout<'a>), Proc<'a>>, +) { + let mut env = Env { + arena, + interner, + home, + ident_ids, + }; + + let env = &mut env; + + for proc in procs.values_mut() { + use self::SelfRecursive::*; + if let SelfRecursive(id) = proc.is_self_recursive { + let trmc_candidate_symbols = trmc_candidates(env.interner, proc); + + if !trmc_candidate_symbols.is_empty() { + let new_proc = + crate::tail_recursion::TrmcEnv::init(env, proc, trmc_candidate_symbols); + *proc = new_proc; + } else { + let mut args = Vec::with_capacity_in(proc.args.len(), arena); + let mut proc_args = Vec::with_capacity_in(proc.args.len(), arena); + + for (layout, symbol) in proc.args { + let new = env.unique_symbol(); + args.push((*layout, *symbol, new)); + proc_args.push((*layout, new)); + } + + let transformed = crate::tail_recursion::make_tail_recursive( + arena, + id, + proc.name, + proc.body.clone(), + args.into_bump_slice(), + proc.ret_layout, + ); + + if let Some(with_tco) = transformed { + proc.body = with_tco; + proc.args = proc_args.into_bump_slice(); + } + } + } + } +} + +/// Make tail calls into loops (using join points) +/// +/// e.g. +/// +/// > factorial n accum = if n == 1 then accum else factorial (n - 1) (n * accum) +/// +/// becomes +/// +/// ```elm +/// factorial n1 accum1 = +/// let joinpoint j n accum = +/// if n == 1 then +/// accum +/// else +/// jump j (n - 1) (n * accum) +/// +/// in +/// jump j n1 accum1 +/// ``` +/// +/// This will effectively compile into a loop in llvm, and +/// won't grow the call stack for each iteration + +fn make_tail_recursive<'a>( + arena: &'a Bump, + id: JoinPointId, + needle: LambdaName, + stmt: Stmt<'a>, + args: &'a [(InLayout<'a>, Symbol, Symbol)], + ret_layout: InLayout<'a>, +) -> Option> { + let allocated = arena.alloc(stmt); + + let new_stmt = insert_jumps(arena, allocated, id, needle, args, ret_layout)?; + + // if we did not early-return, jumps were inserted, we must now add a join point + + let params = Vec::from_iter_in( + args.iter().map(|(layout, symbol, _)| Param { + symbol: *symbol, + layout: *layout, + }), + arena, + ) + .into_bump_slice(); + + // TODO could this be &[]? + let args = Vec::from_iter_in(args.iter().map(|t| t.2), arena).into_bump_slice(); + + let jump = arena.alloc(Stmt::Jump(id, args)); + + let join = Stmt::Join { + id, + remainder: jump, + parameters: params, + body: new_stmt, + }; + + Some(join) +} + +fn insert_jumps<'a>( + arena: &'a Bump, + stmt: &'a Stmt<'a>, + goal_id: JoinPointId, + needle: LambdaName, + needle_arguments: &'a [(InLayout<'a>, Symbol, Symbol)], + needle_result: InLayout<'a>, +) -> Option<&'a Stmt<'a>> { + use Stmt::*; + + // to insert a tail-call, it must not just be a call to the function itself, but it must also + // have the same layout. In particular when lambda sets get involved, a self-recursive call may + // have a different type and should not be converted to a jump! + let is_equal_function = |function_name: LambdaName, arguments: &[_], result| { + let it = needle_arguments.iter().map(|t| &t.0); + needle == function_name && it.eq(arguments.iter()) && needle_result == result + }; + + match stmt { + Let( + symbol, + Expr::Call(crate::ir::Call { + call_type: + CallType::ByName { + name: fsym, + ret_layout, + arg_layouts, + .. + }, + arguments, + }), + _, + Stmt::Ret(rsym), + ) if symbol == rsym && is_equal_function(*fsym, arg_layouts, *ret_layout) => { + // replace the call and return with a jump + + let jump = Stmt::Jump(goal_id, arguments); + + Some(arena.alloc(jump)) + } + + Let(symbol, expr, layout, cont) => { + let opt_cont = insert_jumps( + arena, + cont, + goal_id, + needle, + needle_arguments, + needle_result, + ); + + if opt_cont.is_some() { + let cont = opt_cont.unwrap_or(cont); + + Some(arena.alloc(Let(*symbol, expr.clone(), *layout, cont))) + } else { + None + } + } + + Join { + id, + parameters, + remainder, + body: continuation, + } => { + let opt_remainder = insert_jumps( + arena, + remainder, + goal_id, + needle, + needle_arguments, + needle_result, + ); + let opt_continuation = insert_jumps( + arena, + continuation, + goal_id, + needle, + needle_arguments, + needle_result, + ); + + if opt_remainder.is_some() || opt_continuation.is_some() { + let remainder = opt_remainder.unwrap_or(remainder); + let continuation = opt_continuation.unwrap_or(*continuation); + + Some(arena.alloc(Join { + id: *id, + parameters, + remainder, + body: continuation, + })) + } else { + None + } + } + Switch { + cond_symbol, + cond_layout, + branches, + default_branch, + ret_layout, + } => { + let opt_default = insert_jumps( + arena, + default_branch.1, + goal_id, + needle, + needle_arguments, + needle_result, + ); + + let mut did_change = false; + + let opt_branches = Vec::from_iter_in( + branches.iter().map(|(label, info, branch)| { + match insert_jumps( + arena, + branch, + goal_id, + needle, + needle_arguments, + needle_result, + ) { + None => None, + Some(branch) => { + did_change = true; + Some((*label, info.clone(), branch.clone())) + } + } + }), + arena, + ); + + if opt_default.is_some() || did_change { + let default_branch = ( + default_branch.0.clone(), + opt_default.unwrap_or(default_branch.1), + ); + + let branches = if did_change { + let new = Vec::from_iter_in( + opt_branches.into_iter().zip(branches.iter()).map( + |(opt_branch, branch)| match opt_branch { + None => branch.clone(), + Some(new_branch) => new_branch, + }, + ), + arena, + ); + + new.into_bump_slice() + } else { + branches + }; + + Some(arena.alloc(Switch { + cond_symbol: *cond_symbol, + cond_layout: *cond_layout, + default_branch, + branches, + ret_layout: *ret_layout, + })) + } else { + None + } + } + Refcounting(modify, cont) => { + match insert_jumps( + arena, + cont, + goal_id, + needle, + needle_arguments, + needle_result, + ) { + Some(cont) => Some(arena.alloc(Refcounting(*modify, cont))), + None => None, + } + } + + Dbg { + symbol, + variable, + remainder, + } => match insert_jumps( + arena, + remainder, + goal_id, + needle, + needle_arguments, + needle_result, + ) { + Some(cont) => Some(arena.alloc(Dbg { + symbol: *symbol, + variable: *variable, + remainder: cont, + })), + None => None, + }, + + Expect { + condition, + region, + lookups, + variables, + remainder, + } => match insert_jumps( + arena, + remainder, + goal_id, + needle, + needle_arguments, + needle_result, + ) { + Some(cont) => Some(arena.alloc(Expect { + condition: *condition, + region: *region, + lookups, + variables, + remainder: cont, + })), + None => None, + }, + + ExpectFx { + condition, + region, + lookups, + variables, + remainder, + } => match insert_jumps( + arena, + remainder, + goal_id, + needle, + needle_arguments, + needle_result, + ) { + Some(cont) => Some(arena.alloc(ExpectFx { + condition: *condition, + region: *region, + lookups, + variables, + remainder: cont, + })), + None => None, + }, + + Ret(_) => None, + Jump(_, _) => None, + Crash(..) => None, + } +} + +#[derive(Debug, Default)] +struct TrmcCandidateSet { + interner: arrayvec::ArrayVec, + confirmed: u64, + active: u64, + invalid: u64, +} + +impl TrmcCandidateSet { + fn confirmed(&self) -> impl Iterator + '_ { + self.interner + .iter() + .enumerate() + .filter_map(|(i, s)| (self.confirmed & (1 << i) != 0).then_some(*s)) + } + + fn active(&self) -> impl Iterator + '_ { + self.interner + .iter() + .enumerate() + .filter_map(|(i, s)| (self.active & (1 << i) != 0).then_some(*s)) + } + + fn position(&self, symbol: Symbol) -> Option { + self.interner.iter().position(|s| *s == symbol) + } + + fn insert(&mut self, symbol: Symbol) { + // there really is no way it could have been inserted already + debug_assert!(self.position(symbol).is_none()); + + let index = self.interner.len(); + self.interner.push(symbol); + + self.active |= 1 << index; + } + + fn retain(&mut self, keep: F) + where + F: Fn(&Symbol) -> bool, + { + for (i, s) in self.interner.iter().enumerate() { + if !keep(s) { + let mask = 1 << i; + + self.active &= !mask; + self.confirmed &= !mask; + + self.invalid |= mask; + } + } + } + + fn confirm(&mut self, symbol: Symbol) { + match self.position(symbol) { + None => debug_assert_eq!(0, 1, "confirm of invalid symbol"), + Some(index) => { + let mask = 1 << index; + + debug_assert_eq!(self.invalid & mask, 0); + debug_assert_ne!(self.active & mask, 0); + + self.active &= !mask; + self.confirmed |= mask; + } + } + } + + fn is_empty(&self) -> bool { + self.confirmed == 0 + } +} + +fn trmc_candidates<'a, I>(interner: &'_ I, proc: &'_ Proc<'a>) -> TrmcCandidateSet +where + I: LayoutInterner<'a>, +{ + // it must be a self-recursive function + if !matches!( + proc.is_self_recursive, + crate::ir::SelfRecursive::SelfRecursive(_) + ) { + return TrmcCandidateSet::default(); + } + + // and return a recursive tag union + if !matches!(interner.get_repr(proc.ret_layout), LayoutRepr::Union(union_layout) if union_layout.is_recursive()) + { + return TrmcCandidateSet::default(); + } + + let mut candidate_set = TrmcCandidateSet::default(); + trmc_candidates_help(proc.name, &proc.body, &mut candidate_set); + candidate_set +} + +fn trmc_candidates_help( + function_name: LambdaName, + stmt: &'_ Stmt<'_>, + candidates: &mut TrmcCandidateSet, +) { + // if this stmt is the literal tail tag application and return, then this is a TRMC opportunity + if let Some(cons_info) = TrmcEnv::is_terminal_constructor(stmt) { + // the tag application must directly use the result of the recursive call + let recursive_call = candidates + .active() + .find(|call| cons_info.arguments.contains(call)); + + // if we find a usage, this is a confirmed TRMC call + if let Some(recursive_call) = recursive_call { + candidates.confirm(recursive_call); + + return; + } + } + + // if the stmt uses the active recursive call, that invalidates the recursive call for this branch + candidates.retain(|recursive_call| !stmt_contains_symbol_nonrec(stmt, *recursive_call)); + + match stmt { + Stmt::Let(symbol, expr, _, next) => { + // find a new recursive call if we currently have none + // that means we generally pick the first recursive call we find + if TrmcEnv::is_recursive_expr(expr, function_name).is_some() { + candidates.insert(*symbol); + } + + trmc_candidates_help(function_name, next, candidates) + } + Stmt::Switch { + branches, + default_branch, + .. + } => { + let it = branches + .iter() + .map(|(_, _, stmt)| stmt) + .chain([default_branch.1]); + + for next in it { + trmc_candidates_help(function_name, next, candidates); + } + } + Stmt::Refcounting(_, next) => trmc_candidates_help(function_name, next, candidates), + Stmt::Expect { remainder, .. } + | Stmt::ExpectFx { remainder, .. } + | Stmt::Dbg { remainder, .. } => trmc_candidates_help(function_name, remainder, candidates), + Stmt::Join { + body, remainder, .. + } => { + trmc_candidates_help(function_name, body, candidates); + trmc_candidates_help(function_name, remainder, candidates); + } + Stmt::Ret(_) | Stmt::Jump(_, _) | Stmt::Crash(_, _) => { /* terminal */ } + } +} + +// TRMC (tail recursion modulo constructor) is an optimization for some recursive functions that return a recursive data type. The most basic example is a repeat function on linked lists: +// +// ```roc +// LinkedList a : [ Nil, Cons a (LinkedList a) ] +// +// repeat : a, Nat -> LinkedList a +// repeat = \element, n -> +// when n is +// 0 -> Nil +// _ -> Cons element (repeat element (n - 1)) +// ``` +// +// This function is recursive, but cannot use standard tail-call elimintation, because the recursive call is not in tail position (i.e. the last thing happening before a return). Rather the recursive call is an argument to a constructor of the recursive output type. This means that `repeat n` will creat `n` stack frames. For big inputs, a stack overflow is inevitable. +// +// But there is a trick: TRMC. Using TRMC and join points, we are able to convert this function into a loop, which uses only one stack frame for the whole process. +// +// ```pseudo-roc +// repeat : a, Nat -> LinkedList a +// repeat = \initialElement, initialN -> +// joinpoint trmc = \element, n, hole, head -> +// when n is +// 0 -> +// # write the value `Nil` into the hole +// *hole = Nil +// # dereference (load from) the pointer to the first element +// *head +// +// _ -> +// *hole = Cons element NULL +// newHole = &hole.Cons.1 +// jump trmc element (n - 1) newHole head +// in +// # creates a stack allocation, gives a pointer to that stack allocation +// initial : Ptr (LinkedList a) = #alloca NULL +// jump trmc initialElement initialN initial initial +// ``` +// +// The functionality here figures out whether this transformation can be applied in valid way, and then performs the transformation. + +#[derive(Clone)] +pub(crate) struct TrmcEnv<'a> { + lambda_name: LambdaName<'a>, + /// Current hole to fill + hole_symbol: Symbol, + /// Pointer to the first constructor ("the head of the list") + head_symbol: Symbol, + joinpoint_id: JoinPointId, + return_layout: InLayout<'a>, + ptr_return_layout: InLayout<'a>, + + trmc_calls: VecMap>>, +} + +#[derive(Debug)] +struct ConstructorInfo<'a> { + tag_layout: UnionLayout<'a>, + tag_id: TagIdIntType, + arguments: &'a [Symbol], +} + +impl<'a> TrmcEnv<'a> { + #[inline(always)] + fn is_terminal_constructor(stmt: &Stmt<'a>) -> Option> { + match stmt { + Stmt::Let(s1, expr, _layout, Stmt::Ret(s2)) if s1 == s2 => { + Self::get_contructor_info(expr) + } + + _ => None, + } + } + + fn get_contructor_info(expr: &Expr<'a>) -> Option> { + if let Expr::Tag { + tag_layout, + tag_id, + arguments, + reuse, + } = expr + { + debug_assert!(reuse.is_none()); + + let info = ConstructorInfo { + tag_layout: *tag_layout, + tag_id: *tag_id, + arguments, + }; + + Some(info) + } else { + None + } + } + + fn is_recursive_expr(expr: &Expr<'a>, lambda_name: LambdaName<'_>) -> Option> { + if let Expr::Call(call) = expr { + Self::is_recursive_call(call, lambda_name).then_some(call.clone()) + } else { + None + } + } + + fn is_recursive_call(call: &Call<'a>, lambda_name: LambdaName<'_>) -> bool { + match call.call_type { + CallType::ByName { name, .. } => { + // because we do not allow polymorphic recursion, this is the only constraint + name == lambda_name + } + CallType::ByPointer { .. } => false, + CallType::Foreign { .. } | CallType::LowLevel { .. } | CallType::HigherOrder(_) => { + false + } + } + } + + fn is_tail_recursive_call( + lambda_name: LambdaName, + symbol: Symbol, + expr: &Expr<'a>, + next: &Stmt<'a>, + ) -> Option> { + match next { + Stmt::Ret(s) if *s == symbol => Self::is_recursive_expr(expr, lambda_name), + _ => None, + } + } + + fn ptr_write( + env: &mut Env<'a, '_>, + ptr: Symbol, + value: Symbol, + next: &'a Stmt<'a>, + ) -> Stmt<'a> { + let ptr_write = Call { + call_type: crate::ir::CallType::LowLevel { + op: LowLevel::PtrStore, + update_mode: UpdateModeId::BACKEND_DUMMY, + }, + arguments: env.arena.alloc([ptr, value]), + }; + + Stmt::Let( + env.named_unique_symbol("_ptr_write_unit"), + Expr::Call(ptr_write), + Layout::UNIT, + next, + ) + } + + fn init<'i>(env: &mut Env<'a, 'i>, proc: &Proc<'a>, trmc_calls: TrmcCandidateSet) -> Proc<'a> { + let arena = env.arena; + let return_layout = proc.ret_layout; + + let mut joinpoint_parameters = Vec::with_capacity_in(proc.args.len() + 2, env.arena); + let mut new_proc_arguments = Vec::with_capacity_in(proc.args.len(), env.arena); + let mut jump_arguments = Vec::with_capacity_in(proc.args.len() + 2, env.arena); + + for (i, (layout, old_symbol)) in proc.args.iter().enumerate() { + let symbol = env.named_unique_symbol(&format!("arg_{i}")); + new_proc_arguments.push((*layout, symbol)); + jump_arguments.push(symbol); + + let param = Param { + symbol: *old_symbol, + layout: *layout, + }; + joinpoint_parameters.push(param); + } + + // the root of the recursive structure that we'll be building + let initial_ptr_symbol = env.named_unique_symbol("initial"); + jump_arguments.push(initial_ptr_symbol); + jump_arguments.push(initial_ptr_symbol); + + let null_symbol = env.named_unique_symbol("null"); + let let_null = |next| Stmt::Let(null_symbol, Expr::NullPointer, return_layout, next); + + let ptr_return_layout = env + .interner + .insert_direct_no_semantic(LayoutRepr::Ptr(return_layout)); + + let ptr_null = Expr::Alloca { + initializer: Some(null_symbol), + element_layout: return_layout, + }; + let let_ptr = |next| Stmt::Let(initial_ptr_symbol, ptr_null, ptr_return_layout, next); + + let joinpoint_id = JoinPointId(env.named_unique_symbol("trmc")); + let hole_symbol = env.named_unique_symbol("hole"); + let head_symbol = env.named_unique_symbol("head"); + + let jump_stmt = Stmt::Jump(joinpoint_id, jump_arguments.into_bump_slice()); + + let trmc_calls = trmc_calls.confirmed().map(|s| (s, None)).collect(); + + let mut this = Self { + lambda_name: proc.name, + hole_symbol, + head_symbol, + joinpoint_id, + return_layout, + ptr_return_layout, + trmc_calls, + }; + + let param = Param { + symbol: hole_symbol, + layout: ptr_return_layout, + }; + joinpoint_parameters.push(param); + + let param = Param { + symbol: head_symbol, + layout: ptr_return_layout, + }; + joinpoint_parameters.push(param); + + let joinpoint = Stmt::Join { + id: joinpoint_id, + parameters: joinpoint_parameters.into_bump_slice(), + body: arena.alloc(this.walk_stmt(env, &proc.body)), + remainder: arena.alloc(jump_stmt), + }; + + let body = let_null(arena.alloc( + // + let_ptr(arena.alloc( + // + joinpoint, + )), + )); + + #[cfg(debug_assertions)] + env.home.register_debug_idents(env.ident_ids); + + Proc { + name: proc.name, + args: new_proc_arguments.into_bump_slice(), + body, + closure_data_layout: proc.closure_data_layout, + ret_layout: proc.ret_layout, + is_self_recursive: SelfRecursive::NotSelfRecursive, + is_erased: proc.is_erased, + } + } + + fn walk_stmt(&mut self, env: &mut Env<'a, '_>, stmt: &Stmt<'a>) -> Stmt<'a> { + let arena = env.arena; + + match stmt { + Stmt::Let(symbol, expr, layout, next) => { + // if this is a TRMC call, remember what the call looks like, so we can turn it + // into a jump later. The call is then removed from the Stmt + if let Some(opt_call) = self.trmc_calls.get_mut(symbol) { + debug_assert!( + opt_call.is_none(), + "didn't expect to visit call again since symbols are unique" + ); + + let call = match expr { + Expr::Call(call) => call, + _ => unreachable!(), + }; + + *opt_call = Some(call.clone()); + + return self.walk_stmt(env, next); + } + + if let Some(call) = + Self::is_tail_recursive_call(self.lambda_name, *symbol, expr, next) + { + // turn the call into a jump. Just re-use the existing hole + let mut arguments = Vec::new_in(arena); + arguments.extend(call.arguments); + arguments.push(self.hole_symbol); + arguments.push(self.head_symbol); + + let jump = Stmt::Jump(self.joinpoint_id, arguments.into_bump_slice()); + + return jump; + } + + if let Some(cons_info) = Self::is_terminal_constructor(stmt) { + // figure out which TRMC call to use here. We pick the first one that works + let opt_recursive_call = cons_info.arguments.iter().find_map(|arg| { + self.trmc_calls + .get(arg) + .and_then(|x| x.as_ref()) + .map(|x| (arg, x)) + }); + + match opt_recursive_call { + None => { + // this control flow path did not encounter a recursive call. Just + // write the end result into the hole and we're done. + + let define_tag = |next| Stmt::Let(*symbol, expr.clone(), *layout, next); + + let output = define_tag(arena.alloc( + // + self.non_trmc_return(env, *symbol), + )); + + return output; + } + Some((call_symbol, call)) => { + // we did encounter a recursive call, and can perform TRMC in this + // branch. + + let opt_recursive_field_index = + cons_info.arguments.iter().position(|s| *s == *call_symbol); + + let recursive_field_index = match opt_recursive_field_index { + None => { + let next = self.walk_stmt(env, next); + return Stmt::Let( + *symbol, + expr.clone(), + *layout, + arena.alloc(next), + ); + } + Some(v) => v, + }; + + let tag_arg_null_symbol = env.named_unique_symbol("tag_arg_null"); + let let_tag_arg_null = |next| { + Stmt::Let( + tag_arg_null_symbol, + Expr::NullPointer, + self.return_layout, + next, + ) + }; + + let mut arguments = + Vec::from_iter_in(cons_info.arguments.iter().copied(), env.arena); + arguments[recursive_field_index] = tag_arg_null_symbol; + + let tag_expr = Expr::Tag { + tag_layout: cons_info.tag_layout, + tag_id: cons_info.tag_id, + arguments: arguments.into_bump_slice(), + reuse: None, + }; + + let indices = arena + .alloc([cons_info.tag_id as u64, recursive_field_index as u64]); + + let let_tag = |next| Stmt::Let(*symbol, tag_expr, *layout, next); + let get_reference_expr = Expr::GetElementPointer { + structure: *symbol, + union_layout: cons_info.tag_layout, + indices, + }; + + let new_hole_symbol = env.named_unique_symbol("newHole"); + let let_new_hole = |next| { + Stmt::Let( + new_hole_symbol, + get_reference_expr, + self.ptr_return_layout, + next, + ) + }; + + let mut jump_arguments = + Vec::from_iter_in(call.arguments.iter().copied(), env.arena); + jump_arguments.push(new_hole_symbol); + jump_arguments.push(self.head_symbol); + + let jump = + Stmt::Jump(self.joinpoint_id, jump_arguments.into_bump_slice()); + + let output = let_tag_arg_null(arena.alloc( + // + let_tag(arena.alloc( + // + let_new_hole(arena.alloc( + // + Self::ptr_write( + env, + self.hole_symbol, + *symbol, + arena.alloc(jump), + ), + )), + )), + )); + + return output; + } + } + } + + let next = self.walk_stmt(env, next); + Stmt::Let(*symbol, expr.clone(), *layout, arena.alloc(next)) + } + Stmt::Switch { + cond_symbol, + cond_layout, + branches, + default_branch, + ret_layout, + } => { + let mut new_branches = Vec::with_capacity_in(branches.len(), arena); + + for (id, info, stmt) in branches.iter() { + let new_stmt = self.walk_stmt(env, stmt); + + new_branches.push((*id, info.clone(), new_stmt)); + } + + let new_default_branch = &*arena.alloc(self.walk_stmt(env, default_branch.1)); + + Stmt::Switch { + cond_symbol: *cond_symbol, + cond_layout: *cond_layout, + branches: arena.alloc(new_branches.into_bump_slice()), + default_branch: (default_branch.0.clone(), new_default_branch), + ret_layout: *ret_layout, + } + } + Stmt::Ret(symbol) => { + // write the symbol we're supposed to return into the hole + // then read initial_symbol and return its contents + self.non_trmc_return(env, *symbol) + } + Stmt::Refcounting(op, next) => { + let new_next = self.walk_stmt(env, next); + Stmt::Refcounting(*op, arena.alloc(new_next)) + } + Stmt::Expect { + condition, + region, + lookups, + variables, + remainder, + } => Stmt::Expect { + condition: *condition, + region: *region, + lookups, + variables, + remainder: arena.alloc(self.walk_stmt(env, remainder)), + }, + Stmt::ExpectFx { + condition, + region, + lookups, + variables, + remainder, + } => Stmt::Expect { + condition: *condition, + region: *region, + lookups, + variables, + remainder: arena.alloc(self.walk_stmt(env, remainder)), + }, + Stmt::Dbg { + symbol, + variable, + remainder, + } => Stmt::Dbg { + symbol: *symbol, + variable: *variable, + remainder: arena.alloc(self.walk_stmt(env, remainder)), + }, + Stmt::Join { + id, + parameters, + body, + remainder, + } => { + let new_body = self.walk_stmt(env, body); + let new_remainder = self.walk_stmt(env, remainder); + + Stmt::Join { + id: *id, + parameters, + body: arena.alloc(new_body), + remainder: arena.alloc(new_remainder), + } + } + Stmt::Jump(id, arguments) => Stmt::Jump(*id, arguments), + Stmt::Crash(symbol, crash_tag) => Stmt::Crash(*symbol, *crash_tag), + } + } + + fn non_trmc_return(&mut self, env: &mut Env<'a, '_>, value_symbol: Symbol) -> Stmt<'a> { + let arena = env.arena; + let layout = self.return_layout; + + let final_symbol = env.named_unique_symbol("final"); + + let call = Call { + call_type: CallType::LowLevel { + op: LowLevel::PtrLoad, + update_mode: UpdateModeId::BACKEND_DUMMY, + }, + arguments: &*arena.alloc([self.head_symbol]), + }; + + let ptr_load = |next| Stmt::Let(final_symbol, Expr::Call(call), layout, next); + + Self::ptr_write( + env, + self.hole_symbol, + value_symbol, + arena.alloc( + // + ptr_load(arena.alloc(Stmt::Ret(final_symbol))), + ), + ) + } +} + +fn expr_contains_symbol(expr: &Expr, needle: Symbol) -> bool { + match expr { + Expr::Literal(_) => false, + Expr::Call(call) => call.arguments.contains(&needle), + Expr::Tag { + arguments, reuse, .. + } => match reuse { + None => arguments.contains(&needle), + Some(ru) => ru.symbol == needle || arguments.contains(&needle), + }, + Expr::Struct(fields) => fields.contains(&needle), + Expr::NullPointer | Expr::FunctionPointer { .. } => false, + Expr::StructAtIndex { structure, .. } + | Expr::GetTagId { structure, .. } + | Expr::UnionAtIndex { structure, .. } + | Expr::GetElementPointer { structure, .. } => needle == *structure, + Expr::Array { elems, .. } => elems.iter().any(|element| match element { + crate::ir::ListLiteralElement::Literal(_) => false, + crate::ir::ListLiteralElement::Symbol(symbol) => needle == *symbol, + }), + Expr::EmptyArray => false, + Expr::Reset { symbol, .. } | Expr::ResetRef { symbol, .. } => needle == *symbol, + Expr::RuntimeErrorFunction(_) => false, + Expr::ErasedMake { value, callee } => { + value.map(|v| v == needle).unwrap_or(false) || needle == *callee + } + Expr::ErasedLoad { symbol, field: _ } => needle == *symbol, + Expr::Alloca { initializer, .. } => &Some(needle) == initializer, + } +} + +fn stmt_contains_symbol_nonrec(stmt: &Stmt, needle: Symbol) -> bool { + use crate::ir::ModifyRc::*; + + match stmt { + Stmt::Let(_, expr, _, _) => expr_contains_symbol(expr, needle), + Stmt::Switch { cond_symbol, .. } => needle == *cond_symbol, + Stmt::Ret(symbol) => needle == *symbol, + Stmt::Refcounting(modify, _) => { + matches!( modify, Inc(symbol, _) | Dec(symbol) | DecRef(symbol) if needle == *symbol ) + } + Stmt::Expect { + condition, lookups, .. + } + | Stmt::ExpectFx { + condition, lookups, .. + } => needle == *condition || lookups.contains(&needle), + Stmt::Dbg { symbol, .. } => needle == *symbol, + Stmt::Join { .. } => false, + Stmt::Jump(_, arguments) => arguments.contains(&needle), + Stmt::Crash(symbol, _) => needle == *symbol, + } +} diff --git a/crates/compiler/parse/Cargo.toml b/crates/compiler/parse/Cargo.toml new file mode 100644 index 0000000000..5beb273a4c --- /dev/null +++ b/crates/compiler/parse/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "roc_parse" +description = "Implements the Roc parser, which transforms a textual representation of a Roc program to an AST." + +authors.workspace = true +edition.workspace = true +license.workspace = true +version.workspace = true + +[features] +"parse_debug_trace" = [] + +[dependencies] +roc_collections = { path = "../collections" } +roc_module = { path = "../module" } +roc_region = { path = "../region" } +roc_error_macros = { path = "../../error_macros" } + +bumpalo.workspace = true +encode_unicode.workspace = true + +[dev-dependencies] +criterion.workspace = true +indoc.workspace = true +pretty_assertions.workspace = true +proptest.workspace = true +quickcheck.workspace = true +quickcheck_macros.workspace = true +tempfile.workspace = true + +[[bench]] +harness = false +name = "bench_parse" diff --git a/crates/compiler/parse/benches/bench_parse.rs b/crates/compiler/parse/benches/bench_parse.rs new file mode 100644 index 0000000000..77cca882ce --- /dev/null +++ b/crates/compiler/parse/benches/bench_parse.rs @@ -0,0 +1,58 @@ +use bumpalo::Bump; +use criterion::{black_box, criterion_group, criterion_main, Criterion}; +use roc_parse::{module, module::module_defs, parser::Parser, state::State}; +use std::path::PathBuf; + +pub fn parse_benchmark(c: &mut Criterion) { + c.bench_function("parse false-interpreter", |b| { + let mut path = PathBuf::from(std::env!("ROC_WORKSPACE_DIR")); + path.push("examples"); + path.push("cli"); + path.push("false-interpreter"); + path.push("False.roc"); + let src = std::fs::read_to_string(&path).unwrap(); + + b.iter(|| { + let arena = Bump::new(); + + let (_actual, state) = + module::parse_header(&arena, State::new(src.as_bytes())).unwrap(); + + let min_indent = 0; + let res = module_defs() + .parse(&arena, state, min_indent) + .map(|tuple| tuple.1) + .unwrap(); + + black_box(res.len()); + }) + }); + + c.bench_function("parse Num builtin", |b| { + let mut path = PathBuf::from(std::env!("ROC_WORKSPACE_DIR")); + path.push("crates"); + path.push("compiler"); + path.push("builtins"); + path.push("roc"); + path.push("Num.roc"); + let src = std::fs::read_to_string(&path).unwrap(); + + b.iter(|| { + let arena = Bump::new(); + + let (_actual, state) = + module::parse_header(&arena, State::new(src.as_bytes())).unwrap(); + + let min_indent = 0; + let res = module_defs() + .parse(&arena, state, min_indent) + .map(|tuple| tuple.1) + .unwrap(); + + black_box(res.len()); + }) + }); +} + +criterion_group!(benches, parse_benchmark); +criterion_main!(benches); diff --git a/crates/compiler/parse/src/ast.rs b/crates/compiler/parse/src/ast.rs new file mode 100644 index 0000000000..949d88b443 --- /dev/null +++ b/crates/compiler/parse/src/ast.rs @@ -0,0 +1,1855 @@ +use std::fmt::Debug; +use std::path::Path; + +use crate::header::{AppHeader, HostedHeader, InterfaceHeader, PackageHeader, PlatformHeader}; +use crate::ident::Accessor; +use crate::parser::ESingleQuote; +use bumpalo::collections::{String, Vec}; +use bumpalo::Bump; +use roc_collections::soa::{EitherIndex, Index, Slice}; +use roc_module::called_via::{BinOp, CalledVia, UnaryOp}; +use roc_region::all::{Loc, Position, Region}; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Spaces<'a, T> { + pub before: &'a [CommentOrNewline<'a>], + pub item: T, + pub after: &'a [CommentOrNewline<'a>], +} + +#[derive(Copy, Clone, PartialEq)] +pub enum Spaced<'a, T> { + Item(T), + + // Spaces + SpaceBefore(&'a Spaced<'a, T>, &'a [CommentOrNewline<'a>]), + SpaceAfter(&'a Spaced<'a, T>, &'a [CommentOrNewline<'a>]), +} + +impl<'a, T> Spaced<'a, T> { + /// A `Spaced` is multiline if it has newlines or comments before or after the item, since + /// comments induce newlines! + pub fn is_multiline(&self) -> bool { + match self { + Spaced::Item(_) => false, + Spaced::SpaceBefore(_, spaces) | Spaced::SpaceAfter(_, spaces) => { + debug_assert!(!spaces.is_empty()); + true + } + } + } + + pub fn item(&self) -> &T { + match self { + Spaced::Item(answer) => answer, + Spaced::SpaceBefore(next, _spaces) | Spaced::SpaceAfter(next, _spaces) => next.item(), + } + } +} + +impl<'a, T: Debug> Debug for Spaced<'a, T> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Item(item) => item.fmt(f), + Self::SpaceBefore(item, space) => f + .debug_tuple("SpaceBefore") + .field(item) + .field(space) + .finish(), + Self::SpaceAfter(item, space) => f + .debug_tuple("SpaceAfter") + .field(item) + .field(space) + .finish(), + } + } +} + +pub trait ExtractSpaces<'a>: Sized + Copy { + type Item; + fn extract_spaces(&self) -> Spaces<'a, Self::Item>; +} + +impl<'a, T: ExtractSpaces<'a>> ExtractSpaces<'a> for &'a T { + type Item = T::Item; + fn extract_spaces(&self) -> Spaces<'a, Self::Item> { + (*self).extract_spaces() + } +} + +impl<'a, T: ExtractSpaces<'a>> ExtractSpaces<'a> for Loc { + type Item = T::Item; + fn extract_spaces(&self) -> Spaces<'a, Self::Item> { + let spaces = self.value.extract_spaces(); + Spaces { + before: spaces.before, + item: spaces.item, + after: spaces.after, + } + } +} + +#[derive(Clone, Debug, PartialEq)] +pub struct Module<'a> { + pub comments: &'a [CommentOrNewline<'a>], + pub header: Header<'a>, +} + +#[derive(Clone, Debug, PartialEq)] +pub enum Header<'a> { + Interface(InterfaceHeader<'a>), + App(AppHeader<'a>), + Package(PackageHeader<'a>), + Platform(PlatformHeader<'a>), + Hosted(HostedHeader<'a>), +} + +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct WhenBranch<'a> { + pub patterns: &'a [Loc>], + pub value: Loc>, + pub guard: Option>>, +} + +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct WhenPattern<'a> { + pub pattern: Loc>, + pub guard: Option>>, +} + +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum StrSegment<'a> { + Plaintext(&'a str), // e.g. "foo" + Unicode(Loc<&'a str>), // e.g. "00A0" in "\u(00A0)" + EscapedChar(EscapedChar), // e.g. '\n' in "Hello!\n" + Interpolated(Loc<&'a Expr<'a>>), // e.g. (name) in "Hi, \(name)!" +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum SingleQuoteSegment<'a> { + Plaintext(&'a str), // e.g. 'f' + Unicode(Loc<&'a str>), // e.g. '00A0' in '\u(00A0)' + EscapedChar(EscapedChar), // e.g. '\n' + // No interpolated expressions in single-quoted strings +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum EscapedChar { + Newline, // \n + Tab, // \t + DoubleQuote, // \" + SingleQuote, // \' + Backslash, // \\ + CarriageReturn, // \r +} + +impl EscapedChar { + /// Returns the char that would have been originally parsed to + pub fn to_parsed_char(self) -> char { + use EscapedChar::*; + + match self { + Backslash => '\\', + SingleQuote => '\'', + DoubleQuote => '"', + CarriageReturn => 'r', + Tab => 't', + Newline => 'n', + } + } + + pub fn unescape(self) -> char { + use EscapedChar::*; + + match self { + Backslash => '\\', + SingleQuote => '\'', + DoubleQuote => '"', + CarriageReturn => '\r', + Tab => '\t', + Newline => '\n', + } + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum SingleQuoteLiteral<'a> { + /// The most common case: a plain character with no escapes + PlainLine(&'a str), + Line(&'a [SingleQuoteSegment<'a>]), +} + +impl<'a> SingleQuoteLiteral<'a> { + pub fn to_str_in(&self, arena: &'a Bump) -> &'a str { + match self { + SingleQuoteLiteral::PlainLine(s) => s, + SingleQuoteLiteral::Line(segments) => { + let mut s = String::new_in(arena); + for segment in *segments { + match segment { + SingleQuoteSegment::Plaintext(s2) => s.push_str(s2), + SingleQuoteSegment::Unicode(loc) => { + let s2 = loc.value; + let c = u32::from_str_radix(s2, 16).expect("Invalid unicode escape"); + s.push(char::from_u32(c).expect("Invalid unicode codepoint")); + } + SingleQuoteSegment::EscapedChar(c) => { + s.push(c.unescape()); + } + } + } + s.into_bump_str() + } + } + } +} + +impl<'a> TryFrom> for SingleQuoteSegment<'a> { + type Error = ESingleQuote; + + fn try_from(value: StrSegment<'a>) -> Result { + match value { + StrSegment::Plaintext(s) => Ok(SingleQuoteSegment::Plaintext(s)), + StrSegment::Unicode(s) => Ok(SingleQuoteSegment::Unicode(s)), + StrSegment::EscapedChar(s) => Ok(SingleQuoteSegment::EscapedChar(s)), + StrSegment::Interpolated(_) => Err(ESingleQuote::InterpolationNotAllowed), + } + } +} + +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum StrLiteral<'a> { + /// The most common case: a plain string with no escapes or interpolations + PlainLine(&'a str), + Line(&'a [StrSegment<'a>]), + Block(&'a [&'a [StrSegment<'a>]]), +} + +/// A parsed expression. This uses lifetimes extensively for two reasons: +/// +/// 1. It uses Bump::alloc for all allocations, which returns a reference. +/// 2. It often stores references into the input string instead of allocating. +/// +/// This dramatically reduces allocations during parsing. Once parsing is done, +/// we move on to canonicalization, which often needs to allocate more because +/// it's doing things like turning local variables into fully qualified symbols. +/// Once canonicalization is done, the arena and the input string get dropped. +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum Expr<'a> { + // Number Literals + Float(&'a str), + Num(&'a str), + NonBase10Int { + string: &'a str, + base: Base, + is_negative: bool, + }, + + // String Literals + Str(StrLiteral<'a>), // string without escapes in it + /// eg 'b' + SingleQuote(&'a str), + + /// Look up exactly one field on a record, e.g. `x.foo`. + RecordAccess(&'a Expr<'a>, &'a str), + + /// e.g. `.foo` or `.0` + AccessorFunction(Accessor<'a>), + + /// Look up exactly one field on a tuple, e.g. `(x, y).1`. + TupleAccess(&'a Expr<'a>, &'a str), + + // Collection Literals + List(Collection<'a, &'a Loc>>), + + RecordUpdate { + update: &'a Loc>, + fields: Collection<'a, Loc>>>, + }, + + Record(Collection<'a, Loc>>>), + + Tuple(Collection<'a, &'a Loc>>), + + // Record Builders + RecordBuilder(Collection<'a, Loc>>), + + // The name of a file to be ingested directly into a variable. + IngestedFile(&'a Path, &'a Loc>), + + // Lookups + Var { + module_name: &'a str, // module_name will only be filled if the original Roc code stated something like `5 + SomeModule.myVar`, module_name will be blank if it was `5 + myVar` + ident: &'a str, + }, + + Underscore(&'a str), + + // The "crash" keyword + Crash, + + // Tags + Tag(&'a str), + + // Reference to an opaque type, e.g. @Opaq + OpaqueRef(&'a str), + + // Pattern Matching + Closure(&'a [Loc>], &'a Loc>), + /// Multiple defs in a row + Defs(&'a Defs<'a>, &'a Loc>), + Backpassing(&'a [Loc>], &'a Loc>, &'a Loc>), + Expect(&'a Loc>, &'a Loc>), + Dbg(&'a Loc>, &'a Loc>), + // This form of debug is a desugared call to roc_dbg + LowLevelDbg(&'a Loc>, &'a Loc>), + + // Application + /// To apply by name, do Apply(Var(...), ...) + /// To apply a tag by name, do Apply(Tag(...), ...) + Apply(&'a Loc>, &'a [&'a Loc>], CalledVia), + BinOps(&'a [(Loc>, Loc)], &'a Loc>), + UnaryOp(&'a Loc>, Loc), + + // Conditionals + If(&'a [(Loc>, Loc>)], &'a Loc>), + When( + /// The condition + &'a Loc>, + /// A | B if bool -> expression + /// | if -> + /// Vec, because there may be many patterns, and the guard + /// is Option because each branch may be preceded by + /// a guard (".. if .."). + &'a [&'a WhenBranch<'a>], + ), + + // Blank Space (e.g. comments, spaces, newlines) before or after an expression. + // We preserve this for the formatter; canonicalization ignores it. + SpaceBefore(&'a Expr<'a>, &'a [CommentOrNewline<'a>]), + SpaceAfter(&'a Expr<'a>, &'a [CommentOrNewline<'a>]), + ParensAround(&'a Expr<'a>), + + // Problems + MalformedIdent(&'a str, crate::ident::BadIdent), + MalformedClosure, + // Both operators were non-associative, e.g. (True == False == False). + // We should tell the author to disambiguate by grouping them with parens. + PrecedenceConflict(&'a PrecedenceConflict<'a>), + MultipleRecordBuilders(&'a Loc>), + UnappliedRecordBuilder(&'a Loc>), +} + +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct PrecedenceConflict<'a> { + pub whole_region: Region, + pub binop1_position: Position, + pub binop2_position: Position, + pub binop1: BinOp, + pub binop2: BinOp, + pub expr: &'a Loc>, +} + +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct TypeHeader<'a> { + pub name: Loc<&'a str>, + pub vars: &'a [Loc>], +} + +impl<'a> TypeHeader<'a> { + pub fn region(&self) -> Region { + Region::across_all( + [self.name.region] + .iter() + .chain(self.vars.iter().map(|v| &v.region)), + ) + } +} + +/// The `implements` keyword associated with ability definitions. +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum Implements<'a> { + Implements, + SpaceBefore(&'a Implements<'a>, &'a [CommentOrNewline<'a>]), + SpaceAfter(&'a Implements<'a>, &'a [CommentOrNewline<'a>]), +} + +/// An ability demand is a value defining the ability; for example `hash : a -> U64 where a implements Hash` +/// for a `Hash` ability. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct AbilityMember<'a> { + pub name: Loc>, + pub typ: Loc>, +} + +impl AbilityMember<'_> { + pub fn region(&self) -> Region { + Region::across_all([self.name.region, self.typ.region].iter()) + } +} + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum TypeDef<'a> { + /// A type alias. This is like a standalone annotation, except the pattern + /// must be a capitalized Identifier, e.g. + /// + /// Foo : Bar Baz + Alias { + header: TypeHeader<'a>, + ann: Loc>, + }, + + /// An opaque type, wrapping its inner type. E.g. Age := U64. + Opaque { + header: TypeHeader<'a>, + typ: Loc>, + derived: Option>>, + }, + + /// An ability definition. E.g. + /// Hash implements + /// hash : a -> U64 where a implements Hash + Ability { + header: TypeHeader<'a>, + loc_implements: Loc>, + members: &'a [AbilityMember<'a>], + }, +} + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum ValueDef<'a> { + // TODO in canonicalization, validate the pattern; only certain patterns + // are allowed in annotations. + Annotation(Loc>, Loc>), + + // TODO in canonicalization, check to see if there are any newlines after the + // annotation; if not, and if it's followed by a Body, then the annotation + // applies to that expr! (TODO: verify that the pattern for both annotation and body match.) + // No need to track that relationship in any data structure. + Body(&'a Loc>, &'a Loc>), + + AnnotatedBody { + ann_pattern: &'a Loc>, + ann_type: &'a Loc>, + comment: Option<&'a str>, + body_pattern: &'a Loc>, + body_expr: &'a Loc>, + }, + + Dbg { + condition: &'a Loc>, + preceding_comment: Region, + }, + + Expect { + condition: &'a Loc>, + preceding_comment: Region, + }, + + ExpectFx { + condition: &'a Loc>, + preceding_comment: Region, + }, +} + +#[derive(Debug, Clone, PartialEq, Default)] +pub struct Defs<'a> { + pub tags: std::vec::Vec, ValueDef<'a>>>, + pub regions: std::vec::Vec, + pub space_before: std::vec::Vec>>, + pub space_after: std::vec::Vec>>, + pub spaces: std::vec::Vec>, + pub type_defs: std::vec::Vec>, + pub value_defs: std::vec::Vec>, +} + +impl<'a> Defs<'a> { + pub fn is_empty(&self) -> bool { + self.tags.is_empty() + } + + pub fn len(&self) -> usize { + self.tags.len() + } + + pub fn defs(&self) -> impl Iterator, &ValueDef<'a>>> { + self.tags.iter().map(|tag| match tag.split() { + Ok(type_index) => Ok(&self.type_defs[type_index.index()]), + Err(value_index) => Err(&self.value_defs[value_index.index()]), + }) + } + + pub fn last(&self) -> Option, &ValueDef<'a>>> { + self.tags.last().map(|tag| match tag.split() { + Ok(type_index) => Ok(&self.type_defs[type_index.index()]), + Err(value_index) => Err(&self.value_defs[value_index.index()]), + }) + } + + /// NOTE assumes the def itself is pushed already! + fn push_def_help( + &mut self, + tag: EitherIndex, ValueDef<'a>>, + region: Region, + spaces_before: &[CommentOrNewline<'a>], + spaces_after: &[CommentOrNewline<'a>], + ) { + self.tags.push(tag); + + self.regions.push(region); + + let before = Slice::extend_new(&mut self.spaces, spaces_before.iter().copied()); + self.space_before.push(before); + + let after = Slice::extend_new(&mut self.spaces, spaces_after.iter().copied()); + self.space_after.push(after); + } + + pub fn push_value_def( + &mut self, + value_def: ValueDef<'a>, + region: Region, + spaces_before: &[CommentOrNewline<'a>], + spaces_after: &[CommentOrNewline<'a>], + ) { + let value_def_index = Index::push_new(&mut self.value_defs, value_def); + let tag = EitherIndex::from_right(value_def_index); + self.push_def_help(tag, region, spaces_before, spaces_after) + } + + pub fn replace_with_value_def( + &mut self, + index: usize, + value_def: ValueDef<'a>, + region: Region, + ) { + let value_def_index = Index::push_new(&mut self.value_defs, value_def); + let tag = EitherIndex::from_right(value_def_index); + + self.tags[index] = tag; + self.regions[index] = region; + } + + pub fn push_type_def( + &mut self, + type_def: TypeDef<'a>, + region: Region, + spaces_before: &[CommentOrNewline<'a>], + spaces_after: &[CommentOrNewline<'a>], + ) { + let type_def_index = Index::push_new(&mut self.type_defs, type_def); + let tag = EitherIndex::from_left(type_def_index); + self.push_def_help(tag, region, spaces_before, spaces_after) + } +} + +/// Should always be a zero-argument `Apply`; we'll check this in canonicalization +pub type AbilityName<'a> = Loc>; + +#[derive(Debug, Copy, Clone, PartialEq)] +pub struct ImplementsClause<'a> { + pub var: Loc>, + pub abilities: &'a [AbilityName<'a>], +} + +#[derive(Debug, Copy, Clone, PartialEq)] +pub enum AbilityImpls<'a> { + // `{ eq: myEq }` + AbilityImpls(Collection<'a, Loc>>>), + + // We preserve this for the formatter; canonicalization ignores it. + SpaceBefore(&'a AbilityImpls<'a>, &'a [CommentOrNewline<'a>]), + SpaceAfter(&'a AbilityImpls<'a>, &'a [CommentOrNewline<'a>]), +} + +/// `Eq` or `Eq { eq: myEq }` +#[derive(Debug, Copy, Clone, PartialEq)] +pub enum ImplementsAbility<'a> { + ImplementsAbility { + /// Should be a zero-argument `Apply` or an error; we'll check this in canonicalization + ability: Loc>, + impls: Option>>, + }, + + // We preserve this for the formatter; canonicalization ignores it. + SpaceBefore(&'a ImplementsAbility<'a>, &'a [CommentOrNewline<'a>]), + SpaceAfter(&'a ImplementsAbility<'a>, &'a [CommentOrNewline<'a>]), +} + +#[derive(Debug, Copy, Clone, PartialEq)] +pub enum ImplementsAbilities<'a> { + /// `implements [Eq { eq: myEq }, Hash]` + Implements(Collection<'a, Loc>>), + + // We preserve this for the formatter; canonicalization ignores it. + SpaceBefore(&'a ImplementsAbilities<'a>, &'a [CommentOrNewline<'a>]), + SpaceAfter(&'a ImplementsAbilities<'a>, &'a [CommentOrNewline<'a>]), +} + +impl ImplementsAbilities<'_> { + pub fn collection(&self) -> &Collection> { + let mut it = self; + loop { + match it { + Self::SpaceBefore(inner, _) | Self::SpaceAfter(inner, _) => { + it = inner; + } + Self::Implements(collection) => return collection, + } + } + } + + pub fn is_empty(&self) -> bool { + self.collection().is_empty() + } +} + +#[derive(Debug, Copy, Clone, PartialEq)] +pub enum TypeAnnotation<'a> { + /// A function. The types of its arguments, then the type of its return value. + Function(&'a [Loc>], &'a Loc>), + + /// Applying a type to some arguments (e.g. Map.Map String Int) + Apply(&'a str, &'a str, &'a [Loc>]), + + /// A bound type variable, e.g. `a` in `(a -> a)` + BoundVariable(&'a str), + + /// Inline type alias, e.g. `as List a` in `[Cons a (List a), Nil] as List a` + As( + &'a Loc>, + &'a [CommentOrNewline<'a>], + TypeHeader<'a>, + ), + + Record { + fields: Collection<'a, Loc>>>, + /// The row type variable in an open record, e.g. the `r` in `{ name: Str }r`. + /// This is None if it's a closed record annotation like `{ name: Str }`. + ext: Option<&'a Loc>>, + }, + + Tuple { + elems: Collection<'a, Loc>>, + /// The row type variable in an open tuple, e.g. the `r` in `( Str, Str )r`. + /// This is None if it's a closed tuple annotation like `( Str, Str )`. + ext: Option<&'a Loc>>, + }, + + /// A tag union, e.g. `[ + TagUnion { + /// The row type variable in an open tag union, e.g. the `a` in `[Foo, Bar]a`. + /// This is None if it's a closed tag union like `[Foo, Bar]`. + ext: Option<&'a Loc>>, + tags: Collection<'a, Loc>>, + }, + + /// '_', indicating the compiler should infer the type + Inferred, + + /// The `*` type variable, e.g. in (List *) + Wildcard, + + /// A "where" clause demanding abilities designated by a `where`, e.g. `a -> U64 where a implements Hash` + Where(&'a Loc>, &'a [Loc>]), + + // We preserve this for the formatter; canonicalization ignores it. + SpaceBefore(&'a TypeAnnotation<'a>, &'a [CommentOrNewline<'a>]), + SpaceAfter(&'a TypeAnnotation<'a>, &'a [CommentOrNewline<'a>]), + + /// A malformed type annotation, which will code gen to a runtime error + Malformed(&'a str), +} + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum Tag<'a> { + Apply { + name: Loc<&'a str>, + args: &'a [Loc>], + }, + + // We preserve this for the formatter; canonicalization ignores it. + SpaceBefore(&'a Tag<'a>, &'a [CommentOrNewline<'a>]), + SpaceAfter(&'a Tag<'a>, &'a [CommentOrNewline<'a>]), + + /// A malformed tag, which will code gen to a runtime error + Malformed(&'a str), +} + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum AssignedField<'a, Val> { + // A required field with a label, e.g. `{ name: "blah" }` or `{ name : Str }` + RequiredValue(Loc<&'a str>, &'a [CommentOrNewline<'a>], &'a Loc), + + // An optional field with a label, e.g. `{ name ? "blah" }` + // + // NOTE: This only comes up in type annotations (e.g. `name ? Str`) + // and in destructuring patterns (e.g. `{ name ? "blah" }`) + OptionalValue(Loc<&'a str>, &'a [CommentOrNewline<'a>], &'a Loc), + + // A label with no value, e.g. `{ name }` (this is sugar for { name: name }) + LabelOnly(Loc<&'a str>), + + // We preserve this for the formatter; canonicalization ignores it. + SpaceBefore(&'a AssignedField<'a, Val>, &'a [CommentOrNewline<'a>]), + SpaceAfter(&'a AssignedField<'a, Val>, &'a [CommentOrNewline<'a>]), + + /// A malformed assigned field, which will code gen to a runtime error + Malformed(&'a str), +} + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum RecordBuilderField<'a> { + // A field with a value, e.g. `{ name: "blah" }` + Value(Loc<&'a str>, &'a [CommentOrNewline<'a>], &'a Loc>), + + // A field with a function we can apply to build part of the record, e.g. `{ name: <- apply getName }` + ApplyValue( + Loc<&'a str>, + &'a [CommentOrNewline<'a>], + &'a [CommentOrNewline<'a>], + &'a Loc>, + ), + + // A label with no value, e.g. `{ name }` (this is sugar for { name: name }) + LabelOnly(Loc<&'a str>), + + // We preserve this for the formatter; canonicalization ignores it. + SpaceBefore(&'a RecordBuilderField<'a>, &'a [CommentOrNewline<'a>]), + SpaceAfter(&'a RecordBuilderField<'a>, &'a [CommentOrNewline<'a>]), + + /// A malformed assigned field, which will code gen to a runtime error + Malformed(&'a str), +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum CommentOrNewline<'a> { + Newline, + LineComment(&'a str), + DocComment(&'a str), +} + +impl<'a> CommentOrNewline<'a> { + pub fn is_comment(&self) -> bool { + use CommentOrNewline::*; + match self { + Newline => false, + LineComment(_) => true, + DocComment(_) => true, + } + } + + pub fn is_newline(&self) -> bool { + use CommentOrNewline::*; + match self { + Newline => true, + LineComment(_) => false, + DocComment(_) => false, + } + } + + pub fn to_string_repr(&self) -> std::string::String { + use CommentOrNewline::*; + match self { + Newline => "\n".to_owned(), + LineComment(comment_str) => format!("#{comment_str}"), + DocComment(comment_str) => format!("##{comment_str}"), + } + } + + pub fn comment_str(&'a self) -> Option<&'a str> { + match self { + CommentOrNewline::LineComment(s) => Some(*s), + CommentOrNewline::DocComment(s) => Some(*s), + _ => None, + } + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct PatternAs<'a> { + pub spaces_before: &'a [CommentOrNewline<'a>], + pub identifier: Loc<&'a str>, +} + +impl<'a> PatternAs<'a> { + pub fn equivalent(&self, other: &Self) -> bool { + self.identifier.value == other.identifier.value + } +} + +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum Pattern<'a> { + // Identifier + Identifier(&'a str), + + Tag(&'a str), + + OpaqueRef(&'a str), + + Apply(&'a Loc>, &'a [Loc>]), + + /// This is Located rather than Located so we can record comments + /// around the destructured names, e.g. { x ### x does stuff ###, y } + /// In practice, these patterns will always be Identifier + RecordDestructure(Collection<'a, Loc>>), + + /// A required field pattern, e.g. { x: Just 0 } -> ... + /// Can only occur inside of a RecordDestructure + RequiredField(&'a str, &'a Loc>), + + /// An optional field pattern, e.g. { x ? Just 0 } -> ... + /// Can only occur inside of a RecordDestructure + OptionalField(&'a str, &'a Loc>), + + // Literal + NumLiteral(&'a str), + NonBase10Literal { + string: &'a str, + base: Base, + is_negative: bool, + }, + FloatLiteral(&'a str), + StrLiteral(StrLiteral<'a>), + Underscore(&'a str), + SingleQuote(&'a str), + + /// A tuple pattern, e.g. (Just x, 1) + Tuple(Collection<'a, Loc>>), + + /// A list pattern like [_, x, ..] + List(Collection<'a, Loc>>), + + /// A list-rest pattern ".." + /// Can only occur inside of a [Pattern::List] + ListRest(Option<(&'a [CommentOrNewline<'a>], PatternAs<'a>)>), + + As(&'a Loc>, PatternAs<'a>), + + // Space + SpaceBefore(&'a Pattern<'a>, &'a [CommentOrNewline<'a>]), + SpaceAfter(&'a Pattern<'a>, &'a [CommentOrNewline<'a>]), + + // Malformed + Malformed(&'a str), + MalformedIdent(&'a str, crate::ident::BadIdent), + QualifiedIdentifier { + module_name: &'a str, + ident: &'a str, + }, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub enum Base { + Octal, + Binary, + Hex, + Decimal, +} + +impl<'a> Pattern<'a> { + /// Check that patterns are equivalent, meaning they have the same shape, but may have + /// different locations/whitespace + pub fn equivalent(&self, other: &Self) -> bool { + use Pattern::*; + + match other { + SpaceBefore(y, _) | SpaceAfter(y, _) => { + return self.equivalent(y); + } + _ => {} + } + + match self { + Tag(x) => { + if let Tag(y) = other { + x == y + } else { + false + } + } + Apply(constructor_x, args_x) => { + if let Apply(constructor_y, args_y) = other { + let equivalent_args = args_x + .iter() + .zip(args_y.iter()) + .all(|(p, q)| p.value.equivalent(&q.value)); + + constructor_x.value.equivalent(&constructor_y.value) && equivalent_args + } else { + false + } + } + RecordDestructure(fields_x) => { + if let RecordDestructure(fields_y) = other { + fields_x + .iter() + .zip(fields_y.iter()) + .all(|(p, q)| p.value.equivalent(&q.value)) + } else { + false + } + } + RequiredField(x, inner_x) => { + if let RequiredField(y, inner_y) = other { + x == y && inner_x.value.equivalent(&inner_y.value) + } else { + false + } + } + + // optional record fields can be annotated as: + // { x, y } : { x : Int, y ? Bool } + // { x, y ? False } = rec + OptionalField(x, _) => match other { + Identifier(y) | OptionalField(y, _) => x == y, + _ => false, + }, + Identifier(x) => match other { + Identifier(y) | OptionalField(y, _) => x == y, + _ => false, + }, + NumLiteral(x) => { + if let NumLiteral(y) = other { + x == y + } else { + false + } + } + NonBase10Literal { + string: string_x, + base: base_x, + is_negative: is_negative_x, + } => { + if let NonBase10Literal { + string: string_y, + base: base_y, + is_negative: is_negative_y, + } = other + { + string_x == string_y && base_x == base_y && is_negative_x == is_negative_y + } else { + false + } + } + FloatLiteral(x) => { + if let FloatLiteral(y) = other { + x == y + } else { + false + } + } + StrLiteral(x) => { + if let StrLiteral(y) = other { + x == y + } else { + false + } + } + Underscore(x) => { + if let Underscore(y) = other { + x == y + } else { + false + } + } + SpaceBefore(x, _) | SpaceAfter(x, _) => match other { + SpaceBefore(y, _) | SpaceAfter(y, _) => x.equivalent(y), + y => x.equivalent(y), + }, + Malformed(x) => { + if let Malformed(y) = other { + x == y + } else { + false + } + } + QualifiedIdentifier { + module_name: a, + ident: x, + } => { + if let QualifiedIdentifier { + module_name: b, + ident: y, + } = other + { + a == b && x == y + } else { + false + } + } + OpaqueRef(a) => { + if let OpaqueRef(b) = other { + a == b + } else { + false + } + } + SingleQuote(a) => { + if let SingleQuote(b) = other { + a == b + } else { + false + } + } + Tuple(items_x) => { + if let Tuple(items_y) = other { + items_x + .iter() + .zip(items_y.iter()) + .all(|(p, q)| p.value.equivalent(&q.value)) + } else { + false + } + } + List(items_x) => { + if let List(items_y) = other { + items_x + .iter() + .zip(items_y.iter()) + .all(|(p, q)| p.value.equivalent(&q.value)) + } else { + false + } + } + + ListRest(pattern_as) => match other { + ListRest(other_pattern_as) => match (pattern_as, other_pattern_as) { + (Some((_, a)), Some((_, b))) => a.equivalent(b), + _ => false, + }, + _ => false, + }, + + As(pattern, pattern_as) => match other { + As(other_pattern, other_pattern_as) => { + pattern_as.equivalent(other_pattern_as) + && pattern.value.equivalent(&other_pattern.value) + } + _ => false, + }, + + MalformedIdent(str_x, _) => { + if let MalformedIdent(str_y, _) = other { + str_x == str_y + } else { + false + } + } + } + } +} +#[derive(Copy, Clone)] +pub struct Collection<'a, T> { + pub items: &'a [T], + // Use a pointer to a slice (rather than just a slice), in order to avoid bloating + // Ast variants. The final_comments field is rarely accessed in the hot path, so + // this shouldn't matter much for perf. + // Use an Option, so it's possible to initialize without allocating. + final_comments: Option<&'a &'a [CommentOrNewline<'a>]>, +} + +impl<'a, T> Collection<'a, T> { + pub fn empty() -> Collection<'a, T> { + Collection { + items: &[], + final_comments: None, + } + } + + pub const fn with_items(items: &'a [T]) -> Collection<'a, T> { + Collection { + items, + final_comments: None, + } + } + + pub fn with_items_and_comments( + arena: &'a Bump, + items: &'a [T], + comments: &'a [CommentOrNewline<'a>], + ) -> Collection<'a, T> { + Collection { + items, + final_comments: if comments.is_empty() { + None + } else { + Some(arena.alloc(comments)) + }, + } + } + + pub fn replace_items(&self, new_items: &'a [V]) -> Collection<'a, V> { + Collection { + items: new_items, + final_comments: self.final_comments, + } + } + + pub fn ptrify_items(&self, arena: &'a Bump) -> Collection<'a, &'a T> { + let mut allocated = Vec::with_capacity_in(self.len(), arena); + + for parsed_elem in self.items { + allocated.push(parsed_elem); + } + + self.replace_items(allocated.into_bump_slice()) + } + + pub fn map_items(&self, arena: &'a Bump, f: impl Fn(&'a T) -> V) -> Collection<'a, V> { + let mut allocated = Vec::with_capacity_in(self.len(), arena); + + for parsed_elem in self.items { + allocated.push(f(parsed_elem)); + } + + self.replace_items(allocated.into_bump_slice()) + } + + pub fn map_items_result( + &self, + arena: &'a Bump, + f: impl Fn(&T) -> Result, + ) -> Result, E> { + let mut allocated = Vec::with_capacity_in(self.len(), arena); + + for parsed_elem in self.items { + allocated.push(f(parsed_elem)?); + } + + Ok(self.replace_items(allocated.into_bump_slice())) + } + + pub fn final_comments(&self) -> &'a [CommentOrNewline<'a>] { + if let Some(final_comments) = self.final_comments { + final_comments + } else { + &[] + } + } + + pub fn iter(&self) -> impl Iterator { + self.items.iter() + } + + pub fn len(&self) -> usize { + self.items.len() + } + + pub fn is_empty(&self) -> bool { + self.items.is_empty() + } +} + +impl<'a, T: PartialEq> PartialEq for Collection<'a, T> { + fn eq(&self, other: &Self) -> bool { + self.items == other.items && self.final_comments() == other.final_comments() + } +} + +impl<'a, T: Debug> Debug for Collection<'a, T> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if self.final_comments().is_empty() { + f.debug_list().entries(self.items.iter()).finish() + } else { + f.debug_struct("Collection") + .field("items", &self.items) + .field("final_comments", &self.final_comments()) + .finish() + } + } +} + +impl<'a, T> Default for Collection<'a, T> { + fn default() -> Self { + Self::empty() + } +} + +pub trait Spaceable<'a> { + fn before(&'a self, _: &'a [CommentOrNewline<'a>]) -> Self; + fn after(&'a self, _: &'a [CommentOrNewline<'a>]) -> Self; + + fn with_spaces_before(&'a self, spaces: &'a [CommentOrNewline<'a>], region: Region) -> Loc + where + Self: Sized, + { + Loc { + region, + value: self.before(spaces), + } + } + + fn with_spaces_after(&'a self, spaces: &'a [CommentOrNewline<'a>], region: Region) -> Loc + where + Self: Sized, + { + Loc { + region, + value: self.after(spaces), + } + } +} + +impl<'a, T> Spaceable<'a> for Spaced<'a, T> { + fn before(&'a self, spaces: &'a [CommentOrNewline<'a>]) -> Self { + Spaced::SpaceBefore(self, spaces) + } + fn after(&'a self, spaces: &'a [CommentOrNewline<'a>]) -> Self { + Spaced::SpaceAfter(self, spaces) + } +} + +impl<'a> Spaceable<'a> for Expr<'a> { + fn before(&'a self, spaces: &'a [CommentOrNewline<'a>]) -> Self { + Expr::SpaceBefore(self, spaces) + } + fn after(&'a self, spaces: &'a [CommentOrNewline<'a>]) -> Self { + Expr::SpaceAfter(self, spaces) + } +} + +impl<'a> Spaceable<'a> for Pattern<'a> { + fn before(&'a self, spaces: &'a [CommentOrNewline<'a>]) -> Self { + Pattern::SpaceBefore(self, spaces) + } + fn after(&'a self, spaces: &'a [CommentOrNewline<'a>]) -> Self { + Pattern::SpaceAfter(self, spaces) + } +} + +impl<'a> Spaceable<'a> for TypeAnnotation<'a> { + fn before(&'a self, spaces: &'a [CommentOrNewline<'a>]) -> Self { + TypeAnnotation::SpaceBefore(self, spaces) + } + fn after(&'a self, spaces: &'a [CommentOrNewline<'a>]) -> Self { + TypeAnnotation::SpaceAfter(self, spaces) + } +} + +impl<'a, Val> Spaceable<'a> for AssignedField<'a, Val> { + fn before(&'a self, spaces: &'a [CommentOrNewline<'a>]) -> Self { + AssignedField::SpaceBefore(self, spaces) + } + fn after(&'a self, spaces: &'a [CommentOrNewline<'a>]) -> Self { + AssignedField::SpaceAfter(self, spaces) + } +} + +impl<'a> Spaceable<'a> for RecordBuilderField<'a> { + fn before(&'a self, spaces: &'a [CommentOrNewline<'a>]) -> Self { + RecordBuilderField::SpaceBefore(self, spaces) + } + fn after(&'a self, spaces: &'a [CommentOrNewline<'a>]) -> Self { + RecordBuilderField::SpaceAfter(self, spaces) + } +} + +impl<'a> Spaceable<'a> for Tag<'a> { + fn before(&'a self, spaces: &'a [CommentOrNewline<'a>]) -> Self { + Tag::SpaceBefore(self, spaces) + } + fn after(&'a self, spaces: &'a [CommentOrNewline<'a>]) -> Self { + Tag::SpaceAfter(self, spaces) + } +} + +impl<'a> Spaceable<'a> for Implements<'a> { + fn before(&'a self, spaces: &'a [CommentOrNewline<'a>]) -> Self { + Implements::SpaceBefore(self, spaces) + } + fn after(&'a self, spaces: &'a [CommentOrNewline<'a>]) -> Self { + Implements::SpaceAfter(self, spaces) + } +} + +impl<'a> Spaceable<'a> for AbilityImpls<'a> { + fn before(&'a self, spaces: &'a [CommentOrNewline<'a>]) -> Self { + AbilityImpls::SpaceBefore(self, spaces) + } + fn after(&'a self, spaces: &'a [CommentOrNewline<'a>]) -> Self { + AbilityImpls::SpaceAfter(self, spaces) + } +} + +impl<'a> Spaceable<'a> for ImplementsAbility<'a> { + fn before(&'a self, spaces: &'a [CommentOrNewline<'a>]) -> Self { + ImplementsAbility::SpaceBefore(self, spaces) + } + fn after(&'a self, spaces: &'a [CommentOrNewline<'a>]) -> Self { + ImplementsAbility::SpaceAfter(self, spaces) + } +} + +impl<'a> Spaceable<'a> for ImplementsAbilities<'a> { + fn before(&'a self, spaces: &'a [CommentOrNewline<'a>]) -> Self { + ImplementsAbilities::SpaceBefore(self, spaces) + } + fn after(&'a self, spaces: &'a [CommentOrNewline<'a>]) -> Self { + ImplementsAbilities::SpaceAfter(self, spaces) + } +} + +impl<'a> Expr<'a> { + pub fn loc_ref(&'a self, region: Region) -> Loc<&'a Self> { + Loc { + region, + value: self, + } + } + + pub fn loc(self, region: Region) -> Loc { + Loc { + region, + value: self, + } + } + + pub fn is_tag(&self) -> bool { + matches!(self, Expr::Tag(_)) + } + + pub fn is_opaque(&self) -> bool { + matches!(self, Expr::OpaqueRef(_)) + } +} + +macro_rules! impl_extract_spaces { + ($t:ident $(< $($generic_args:ident),* >)?) => { + + impl<'a, $($($generic_args: Copy),*)?> ExtractSpaces<'a> for $t<'a, $($($generic_args),*)?> { + type Item = Self; + fn extract_spaces(&self) -> Spaces<'a, Self::Item> { + match self { + $t::SpaceBefore(item, before) => { + match item { + $t::SpaceBefore(_, _) => todo!(), + $t::SpaceAfter(item, after) => { + Spaces { + before, + item: **item, + after, + } + } + _ => { + Spaces { + before, + item: **item, + after: &[], + } + } + } + }, + $t::SpaceAfter(item, after) => { + match item { + $t::SpaceBefore(item, before) => { + Spaces { + before, + item: **item, + after, + } + } + $t::SpaceAfter(_, _) => todo!(), + _ => { + Spaces { + before: &[], + item: **item, + after, + } + } + } + }, + _ => { + Spaces { + before: &[], + item: *self, + after: &[], + } + } + } + } + } + }; +} + +impl_extract_spaces!(Expr); +impl_extract_spaces!(Pattern); +impl_extract_spaces!(Tag); +impl_extract_spaces!(AssignedField); +impl_extract_spaces!(TypeAnnotation); +impl_extract_spaces!(ImplementsAbility); + +impl<'a, T: Copy> ExtractSpaces<'a> for Spaced<'a, T> { + type Item = T; + + fn extract_spaces(&self) -> Spaces<'a, T> { + match self { + Spaced::SpaceBefore(item, before) => match item { + Spaced::SpaceBefore(_, _) => todo!(), + Spaced::SpaceAfter(item, after) => { + if let Spaced::Item(item) = item { + Spaces { + before, + item: *item, + after, + } + } else { + todo!(); + } + } + Spaced::Item(item) => Spaces { + before, + item: *item, + after: &[], + }, + }, + Spaced::SpaceAfter(item, after) => match item { + Spaced::SpaceBefore(item, before) => { + if let Spaced::Item(item) = item { + Spaces { + before, + item: *item, + after, + } + } else { + todo!(); + } + } + Spaced::SpaceAfter(_, _) => todo!(), + Spaced::Item(item) => Spaces { + before: &[], + item: *item, + after, + }, + }, + Spaced::Item(item) => Spaces { + before: &[], + item: *item, + after: &[], + }, + } + } +} + +impl<'a> ExtractSpaces<'a> for AbilityImpls<'a> { + type Item = Collection<'a, Loc>>>; + + fn extract_spaces(&self) -> Spaces<'a, Self::Item> { + match self { + AbilityImpls::AbilityImpls(inner) => Spaces { + before: &[], + item: *inner, + after: &[], + }, + AbilityImpls::SpaceBefore(item, before) => match item { + AbilityImpls::AbilityImpls(inner) => Spaces { + before, + item: *inner, + after: &[], + }, + AbilityImpls::SpaceBefore(_, _) => todo!(), + AbilityImpls::SpaceAfter(AbilityImpls::AbilityImpls(inner), after) => Spaces { + before, + item: *inner, + after, + }, + AbilityImpls::SpaceAfter(_, _) => todo!(), + }, + AbilityImpls::SpaceAfter(item, after) => match item { + AbilityImpls::AbilityImpls(inner) => Spaces { + before: &[], + item: *inner, + after, + }, + AbilityImpls::SpaceBefore(AbilityImpls::AbilityImpls(inner), before) => Spaces { + before, + item: *inner, + after, + }, + AbilityImpls::SpaceBefore(_, _) => todo!(), + AbilityImpls::SpaceAfter(_, _) => todo!(), + }, + } + } +} + +pub trait Malformed { + /// Returns whether this node is malformed, or contains a malformed node (recursively). + fn is_malformed(&self) -> bool; +} + +impl<'a> Malformed for Module<'a> { + fn is_malformed(&self) -> bool { + self.header.is_malformed() + } +} + +impl<'a> Malformed for Header<'a> { + fn is_malformed(&self) -> bool { + match self { + Header::Interface(header) => header.is_malformed(), + Header::App(header) => header.is_malformed(), + Header::Package(header) => header.is_malformed(), + Header::Platform(header) => header.is_malformed(), + Header::Hosted(header) => header.is_malformed(), + } + } +} + +impl<'a, T: Malformed> Malformed for Spaces<'a, T> { + fn is_malformed(&self) -> bool { + self.item.is_malformed() + } +} + +impl<'a> Malformed for Expr<'a> { + fn is_malformed(&self) -> bool { + use Expr::*; + + match self { + Float(_) | + Num(_) | + NonBase10Int { .. } | + AccessorFunction(_) | + Var { .. } | + Underscore(_) | + Tag(_) | + OpaqueRef(_) | + SingleQuote(_) | // This is just a &str - not a bunch of segments + IngestedFile(_, _) | + Crash => false, + + Str(inner) => inner.is_malformed(), + + RecordAccess(inner, _) | + TupleAccess(inner, _) => inner.is_malformed(), + + List(items) => items.is_malformed(), + + RecordUpdate { update, fields } => update.is_malformed() || fields.is_malformed(), + Record(items) => items.is_malformed(), + Tuple(items) => items.is_malformed(), + + RecordBuilder(items) => items.is_malformed(), + + Closure(args, body) => args.iter().any(|arg| arg.is_malformed()) || body.is_malformed(), + Defs(defs, body) => defs.is_malformed() || body.is_malformed(), + Backpassing(args, call, body) => args.iter().any(|arg| arg.is_malformed()) || call.is_malformed() || body.is_malformed(), + Expect(condition, continuation) | + Dbg(condition, continuation) => condition.is_malformed() || continuation.is_malformed(), + LowLevelDbg(condition, continuation) => condition.is_malformed() || continuation.is_malformed(), + Apply(func, args, _) => func.is_malformed() || args.iter().any(|arg| arg.is_malformed()), + BinOps(firsts, last) => firsts.iter().any(|(expr, _)| expr.is_malformed()) || last.is_malformed(), + UnaryOp(expr, _) => expr.is_malformed(), + If(chain, els) => chain.iter().any(|(cond, body)| cond.is_malformed() || body.is_malformed()) || els.is_malformed(), + When(cond, branches) => cond.is_malformed() || branches.iter().any(|branch| branch.is_malformed()), + + SpaceBefore(expr, _) | + SpaceAfter(expr, _) | + ParensAround(expr) => expr.is_malformed(), + + MalformedIdent(_, _) | + MalformedClosure | + PrecedenceConflict(_) | + MultipleRecordBuilders(_) | + UnappliedRecordBuilder(_) => true, + } + } +} + +impl<'a> Malformed for WhenBranch<'a> { + fn is_malformed(&self) -> bool { + self.patterns.iter().any(|pat| pat.is_malformed()) + || self.value.is_malformed() + || self.guard.map(|g| g.is_malformed()).unwrap_or_default() + } +} + +impl<'a, T: Malformed> Malformed for Collection<'a, T> { + fn is_malformed(&self) -> bool { + self.iter().any(|item| item.is_malformed()) + } +} + +impl<'a> Malformed for StrLiteral<'a> { + fn is_malformed(&self) -> bool { + match self { + StrLiteral::PlainLine(_) => false, + StrLiteral::Line(segs) => segs.iter().any(|seg| seg.is_malformed()), + StrLiteral::Block(lines) => lines + .iter() + .any(|segs| segs.iter().any(|seg| seg.is_malformed())), + } + } +} + +impl<'a> Malformed for StrSegment<'a> { + fn is_malformed(&self) -> bool { + match self { + StrSegment::Plaintext(_) | StrSegment::Unicode(_) | StrSegment::EscapedChar(_) => false, + StrSegment::Interpolated(expr) => expr.is_malformed(), + } + } +} + +impl<'a, T: Malformed> Malformed for &'a T { + fn is_malformed(&self) -> bool { + (*self).is_malformed() + } +} + +impl Malformed for Loc { + fn is_malformed(&self) -> bool { + self.value.is_malformed() + } +} + +impl Malformed for Option { + fn is_malformed(&self) -> bool { + self.as_ref() + .map(|value| value.is_malformed()) + .unwrap_or_default() + } +} + +impl<'a, T: Malformed> Malformed for AssignedField<'a, T> { + fn is_malformed(&self) -> bool { + match self { + AssignedField::RequiredValue(_, _, val) | AssignedField::OptionalValue(_, _, val) => { + val.is_malformed() + } + AssignedField::LabelOnly(_) => false, + AssignedField::SpaceBefore(field, _) | AssignedField::SpaceAfter(field, _) => { + field.is_malformed() + } + AssignedField::Malformed(_) => true, + } + } +} + +impl<'a> Malformed for RecordBuilderField<'a> { + fn is_malformed(&self) -> bool { + match self { + RecordBuilderField::Value(_, _, expr) + | RecordBuilderField::ApplyValue(_, _, _, expr) => expr.is_malformed(), + RecordBuilderField::LabelOnly(_) => false, + RecordBuilderField::SpaceBefore(field, _) + | RecordBuilderField::SpaceAfter(field, _) => field.is_malformed(), + RecordBuilderField::Malformed(_) => true, + } + } +} + +impl<'a> Malformed for Pattern<'a> { + fn is_malformed(&self) -> bool { + use Pattern::*; + + match self { + Identifier(_) | + Tag(_) | + OpaqueRef(_) => false, + Apply(func, args) => func.is_malformed() || args.iter().any(|arg| arg.is_malformed()), + RecordDestructure(items) => items.iter().any(|item| item.is_malformed()), + RequiredField(_, pat) => pat.is_malformed(), + OptionalField(_, expr) => expr.is_malformed(), + + NumLiteral(_) | + NonBase10Literal { .. } | + Underscore(_) | + SingleQuote(_) | // This is just a &str - not a bunch of segments + FloatLiteral(_) => false, + + StrLiteral(lit) => lit.is_malformed(), + Tuple(items) => items.iter().any(|item| item.is_malformed()), + List(items) => items.iter().any(|item| item.is_malformed()), + ListRest(_) =>false, + As(pat, _) => pat.is_malformed(), + SpaceBefore(pat, _) | + SpaceAfter(pat, _) => pat.is_malformed(), + + Malformed(_) | + MalformedIdent(_, _) | + QualifiedIdentifier { .. } => true, + } + } +} +impl<'a> Malformed for Defs<'a> { + fn is_malformed(&self) -> bool { + self.type_defs.iter().any(|def| def.is_malformed()) + || self.value_defs.iter().any(|def| def.is_malformed()) + } +} + +impl<'a> Malformed for TypeDef<'a> { + fn is_malformed(&self) -> bool { + match self { + TypeDef::Alias { header, ann } => header.is_malformed() || ann.is_malformed(), + TypeDef::Opaque { + header, + typ, + derived, + } => header.is_malformed() || typ.is_malformed() || derived.is_malformed(), + TypeDef::Ability { + header, + loc_implements, + members, + } => { + header.is_malformed() + || loc_implements.is_malformed() + || members.iter().any(|member| member.is_malformed()) + } + } + } +} + +impl<'a> Malformed for AbilityMember<'a> { + fn is_malformed(&self) -> bool { + self.typ.is_malformed() + } +} + +impl<'a> Malformed for Implements<'a> { + fn is_malformed(&self) -> bool { + match self { + Implements::Implements => false, + Implements::SpaceBefore(has, _) | Implements::SpaceAfter(has, _) => has.is_malformed(), + } + } +} + +impl<'a> Malformed for ImplementsAbility<'a> { + fn is_malformed(&self) -> bool { + match self { + ImplementsAbility::ImplementsAbility { ability, impls } => { + ability.is_malformed() || impls.iter().any(|impl_| impl_.is_malformed()) + } + ImplementsAbility::SpaceBefore(has, _) | ImplementsAbility::SpaceAfter(has, _) => { + has.is_malformed() + } + } + } +} + +impl<'a> Malformed for ImplementsAbilities<'a> { + fn is_malformed(&self) -> bool { + match self { + ImplementsAbilities::Implements(abilities) => { + abilities.iter().any(|ability| ability.is_malformed()) + } + ImplementsAbilities::SpaceBefore(has, _) | ImplementsAbilities::SpaceAfter(has, _) => { + has.is_malformed() + } + } + } +} + +impl<'a> Malformed for AbilityImpls<'a> { + fn is_malformed(&self) -> bool { + match self { + AbilityImpls::AbilityImpls(impls) => impls.iter().any(|ability| ability.is_malformed()), + AbilityImpls::SpaceBefore(has, _) | AbilityImpls::SpaceAfter(has, _) => { + has.is_malformed() + } + } + } +} + +impl<'a> Malformed for ValueDef<'a> { + fn is_malformed(&self) -> bool { + match self { + ValueDef::Annotation(pat, annotation) => { + pat.is_malformed() || annotation.is_malformed() + } + ValueDef::Body(pat, expr) => pat.is_malformed() || expr.is_malformed(), + ValueDef::AnnotatedBody { + ann_pattern, + ann_type, + comment: _, + body_pattern, + body_expr, + } => { + ann_pattern.is_malformed() + || ann_type.is_malformed() + || body_pattern.is_malformed() + || body_expr.is_malformed() + } + ValueDef::Dbg { + condition, + preceding_comment: _, + } + | ValueDef::Expect { + condition, + preceding_comment: _, + } + | ValueDef::ExpectFx { + condition, + preceding_comment: _, + } => condition.is_malformed(), + } + } +} + +impl<'a> Malformed for TypeAnnotation<'a> { + fn is_malformed(&self) -> bool { + match self { + TypeAnnotation::Function(args, ret) => { + args.iter().any(|arg| arg.is_malformed()) || ret.is_malformed() + } + TypeAnnotation::Apply(_, _, args) => args.iter().any(|arg| arg.is_malformed()), + TypeAnnotation::BoundVariable(_) + | TypeAnnotation::Inferred + | TypeAnnotation::Wildcard => false, + TypeAnnotation::As(ty, _, head) => ty.is_malformed() || head.is_malformed(), + TypeAnnotation::Record { fields, ext } => { + fields.iter().any(|field| field.is_malformed()) + || ext.map(|ext| ext.is_malformed()).unwrap_or_default() + } + TypeAnnotation::Tuple { elems: fields, ext } => { + fields.iter().any(|field| field.is_malformed()) + || ext.map(|ext| ext.is_malformed()).unwrap_or_default() + } + TypeAnnotation::TagUnion { ext, tags } => { + tags.iter().any(|field| field.is_malformed()) + || ext.map(|ext| ext.is_malformed()).unwrap_or_default() + } + TypeAnnotation::Where(ann, clauses) => { + ann.is_malformed() || clauses.iter().any(|clause| clause.is_malformed()) + } + TypeAnnotation::SpaceBefore(ty, _) | TypeAnnotation::SpaceAfter(ty, _) => { + ty.is_malformed() + } + TypeAnnotation::Malformed(_) => true, + } + } +} + +impl<'a> Malformed for TypeHeader<'a> { + fn is_malformed(&self) -> bool { + self.vars.iter().any(|var| var.is_malformed()) + } +} + +impl<'a> Malformed for Tag<'a> { + fn is_malformed(&self) -> bool { + match self { + Tag::Apply { name: _, args } => args.iter().any(|arg| arg.is_malformed()), + Tag::SpaceBefore(tag, _) | Tag::SpaceAfter(tag, _) => tag.is_malformed(), + Tag::Malformed(_) => true, + } + } +} + +impl<'a> Malformed for ImplementsClause<'a> { + fn is_malformed(&self) -> bool { + self.abilities.iter().any(|ability| ability.is_malformed()) + } +} + +impl<'a, T: Malformed> Malformed for Spaced<'a, T> { + fn is_malformed(&self) -> bool { + match self { + Spaced::Item(t) => t.is_malformed(), + Spaced::SpaceBefore(t, _) | Spaced::SpaceAfter(t, _) => t.is_malformed(), + } + } +} diff --git a/crates/compiler/parse/src/blankspace.rs b/crates/compiler/parse/src/blankspace.rs new file mode 100644 index 0000000000..b0dbb7bb35 --- /dev/null +++ b/crates/compiler/parse/src/blankspace.rs @@ -0,0 +1,519 @@ +use crate::ast::CommentOrNewline; +use crate::ast::Spaceable; +use crate::parser::Progress; +use crate::parser::SpaceProblem; +use crate::parser::{self, and, backtrackable, BadInputError, Parser, Progress::*}; +use crate::state::State; +use bumpalo::collections::vec::Vec; +use bumpalo::Bump; +use roc_region::all::Loc; +use roc_region::all::Position; +use roc_region::all::Region; + +pub fn space0_around_ee<'a, P, S, E>( + parser: P, + indent_before_problem: fn(Position) -> E, + indent_after_problem: fn(Position) -> E, +) -> impl Parser<'a, Loc, E> +where + S: 'a + Spaceable<'a>, + P: 'a + Parser<'a, Loc, E>, + E: 'a + SpaceProblem, +{ + parser::map_with_arena( + and( + space0_e(indent_before_problem), + and(parser, space0_e(indent_after_problem)), + ), + spaces_around_help, + ) +} + +pub fn spaces_around<'a, P, S, E>(parser: P) -> impl Parser<'a, Loc, E> +where + S: 'a + Spaceable<'a>, + P: 'a + Parser<'a, Loc, E>, + E: 'a + SpaceProblem, +{ + parser::map_with_arena(and(spaces(), and(parser, spaces())), spaces_around_help) +} + +pub fn space0_around_e_no_after_indent_check<'a, P, S, E>( + parser: P, + indent_before_problem: fn(Position) -> E, +) -> impl Parser<'a, Loc, E> +where + S: 'a + Spaceable<'a>, + P: 'a + Parser<'a, Loc, E>, + E: 'a + SpaceProblem, +{ + parser::map_with_arena( + and(space0_e(indent_before_problem), and(parser, spaces())), + spaces_around_help, + ) +} + +pub fn space0_before_optional_after<'a, P, S, E>( + parser: P, + indent_before_problem: fn(Position) -> E, + indent_after_problem: fn(Position) -> E, +) -> impl Parser<'a, Loc, E> +where + S: 'a + Spaceable<'a>, + P: 'a + Parser<'a, Loc, E>, + E: 'a + SpaceProblem, +{ + parser::map_with_arena( + and( + space0_e(indent_before_problem), + and( + parser, + one_of![ + backtrackable(space0_e(indent_after_problem)), + succeed!(&[] as &[_]), + ], + ), + ), + spaces_around_help, + ) +} + +pub fn spaces_before_optional_after<'a, P, S, E>(parser: P) -> impl Parser<'a, Loc, E> +where + S: 'a + Spaceable<'a>, + P: 'a + Parser<'a, Loc, E>, + E: 'a + SpaceProblem, +{ + parser::map_with_arena( + and( + spaces(), + and( + parser, + one_of![backtrackable(spaces()), succeed!(&[] as &[_]),], + ), + ), + spaces_around_help, + ) +} + +fn spaces_around_help<'a, S>( + arena: &'a Bump, + tuples: ( + &'a [CommentOrNewline<'a>], + (Loc, &'a [CommentOrNewline<'a>]), + ), +) -> Loc +where + S: 'a + Spaceable<'a>, +{ + let (spaces_before, (loc_val, spaces_after)) = tuples; + + if spaces_before.is_empty() { + if spaces_after.is_empty() { + loc_val + } else { + arena + .alloc(loc_val.value) + .with_spaces_after(spaces_after, loc_val.region) + } + } else if spaces_after.is_empty() { + arena + .alloc(loc_val.value) + .with_spaces_before(spaces_before, loc_val.region) + } else { + let wrapped_expr = arena + .alloc(loc_val.value) + .with_spaces_after(spaces_after, loc_val.region); + + arena + .alloc(wrapped_expr.value) + .with_spaces_before(spaces_before, wrapped_expr.region) + } +} + +pub fn spaces_before<'a, P, S, E>(parser: P) -> impl Parser<'a, Loc, E> +where + S: 'a + Spaceable<'a>, + P: 'a + Parser<'a, Loc, E>, + E: 'a + SpaceProblem, +{ + parser::map_with_arena( + and!(spaces(), parser), + |arena: &'a Bump, (space_list, loc_expr): (&'a [CommentOrNewline<'a>], Loc)| { + if space_list.is_empty() { + loc_expr + } else { + arena + .alloc(loc_expr.value) + .with_spaces_before(space_list, loc_expr.region) + } + }, + ) +} + +pub fn space0_before_e<'a, P, S, E>( + parser: P, + indent_problem: fn(Position) -> E, +) -> impl Parser<'a, Loc, E> +where + S: 'a + Spaceable<'a>, + P: 'a + Parser<'a, Loc, E>, + E: 'a + SpaceProblem, +{ + parser::map_with_arena( + and!(space0_e(indent_problem), parser), + |arena: &'a Bump, (space_list, loc_expr): (&'a [CommentOrNewline<'a>], Loc)| { + if space_list.is_empty() { + loc_expr + } else { + arena + .alloc(loc_expr.value) + .with_spaces_before(space_list, loc_expr.region) + } + }, + ) +} + +pub fn space0_after_e<'a, P, S, E>( + parser: P, + indent_problem: fn(Position) -> E, +) -> impl Parser<'a, Loc, E> +where + S: 'a + Spaceable<'a>, + P: 'a + Parser<'a, Loc, E>, + E: 'a + SpaceProblem, +{ + parser::map_with_arena( + and!(parser, space0_e(indent_problem)), + |arena: &'a Bump, (loc_expr, space_list): (Loc, &'a [CommentOrNewline<'a>])| { + if space_list.is_empty() { + loc_expr + } else { + arena + .alloc(loc_expr.value) + .with_spaces_after(space_list, loc_expr.region) + } + }, + ) +} + +pub fn check_indent<'a, E>(indent_problem: fn(Position) -> E) -> impl Parser<'a, (), E> +where + E: 'a, +{ + move |_, state: State<'a>, min_indent: u32| { + if state.column() >= min_indent { + Ok((NoProgress, (), state)) + } else { + Err((NoProgress, indent_problem(state.pos()))) + } + } +} + +pub fn simple_eat_whitespace(bytes: &[u8]) -> usize { + let mut i = 0; + while i < bytes.len() { + match bytes[i] { + b' ' => i += 1, + _ => break, + } + } + i +} + +pub fn fast_eat_whitespace(bytes: &[u8]) -> usize { + // Load 8 bytes at a time, keeping in mind that the initial offset may not be aligned + let mut i = 0; + while i + 8 <= bytes.len() { + let chunk = unsafe { + // Safe because we know the pointer is in bounds + (bytes.as_ptr().add(i) as *const u64) + .read_unaligned() + .to_le() + }; + + // Space character is 0x20, which has a single bit set + // We can check for any space character by checking if any other bit is set + let spaces = 0x2020_2020_2020_2020; + + // First, generate a mask where each byte is 0xff if the byte is a space, + // and some other bit sequence otherwise + let mask = !(chunk ^ spaces); + + // Now mask off the high bit, so there's some place to carry into without + // overflowing into the next byte. + let mask = mask & !0x8080_8080_8080_8080; + + // Now add 0x0101_0101_0101_0101 to each byte, which will carry into the high bit + // if and only if the byte is a space. + let mask = mask + 0x0101_0101_0101_0101; + + // Now mask off areas where the original bytes had the high bit set, so that + // 0x80|0x20 = 0xa0 will not be considered a space. + let mask = mask & !(chunk & 0x8080_8080_8080_8080); + + // Make sure all the _other_ bits aside from the high bit are set, + // and count the number of trailing one bits, dividing by 8 to get the number of + // bytes that are spaces. + let count = ((mask | !0x8080_8080_8080_8080).trailing_ones() as usize) / 8; + + if count == 8 { + i += 8; + } else { + return i + count; + } + } + + // Check the remaining bytes + simple_eat_whitespace(&bytes[i..]) + i +} + +pub fn simple_eat_until_control_character(bytes: &[u8]) -> usize { + let mut i = 0; + while i < bytes.len() { + if bytes[i] < b' ' { + break; + } else { + i += 1; + } + } + i +} + +pub fn fast_eat_until_control_character(bytes: &[u8]) -> usize { + // Load 8 bytes at a time, keeping in mind that the initial offset may not be aligned + let mut i = 0; + while i + 8 <= bytes.len() { + let chunk = unsafe { + // Safe because we know the pointer is in bounds + (bytes.as_ptr().add(i) as *const u64) + .read_unaligned() + .to_le() + }; + + // Control characters are 0x00-0x1F, and don't have any high bits set. + // They only have bits set that fall under the 0x1F mask. + let control = 0x1F1F_1F1F_1F1F_1F1F; + + // First we set up a value where, if a given byte is a control character, + // it'll have a all the non-control bits set to 1. All control bits are set to zero. + let mask = !(chunk & !control) & !control; + + // Now, down shift by one bit. This will leave room for the following add to + // carry, without impacting the next byte. + let mask = mask >> 1; + + // Add one (shifted by the right amount), causing all the one bits in the control + // characters to cascade, and put a one in the high bit. + let mask = mask.wrapping_add(0x1010_1010_1010_1010); + + // Now, we can count the number of trailing zero bits, dividing by 8 to get the + // number of bytes before the first control character. + let count = (mask & 0x8080_8080_8080_8080).trailing_zeros() as usize / 8; + + if count == 8 { + i += 8; + } else { + return i + count; + } + } + + // Check the remaining bytes + simple_eat_until_control_character(&bytes[i..]) + i +} + +#[cfg(test)] +mod tests { + use super::*; + use proptest::prelude::*; + + #[test] + fn test_eat_whitespace_simple() { + let bytes = &[0, 0, 0, 0, 0, 0, 0, 0]; + assert_eq!(simple_eat_whitespace(bytes), fast_eat_whitespace(bytes)); + } + + proptest! { + #[test] + fn test_eat_whitespace(bytes in proptest::collection::vec(any::(), 0..100)) { + prop_assert_eq!(simple_eat_whitespace(&bytes), fast_eat_whitespace(&bytes)); + } + } + + #[test] + fn test_eat_until_control_character_simple() { + let bytes = &[32, 0, 0, 0, 0, 0, 0, 0]; + assert_eq!( + simple_eat_until_control_character(bytes), + fast_eat_until_control_character(bytes) + ); + } + + proptest! { + #[test] + fn test_eat_until_control_character(bytes in proptest::collection::vec(any::(), 0..100)) { + prop_assert_eq!( + simple_eat_until_control_character(&bytes), + fast_eat_until_control_character(&bytes)); + } + } +} + +pub fn space0_e<'a, E>( + indent_problem: fn(Position) -> E, +) -> impl Parser<'a, &'a [CommentOrNewline<'a>], E> +where + E: 'a + SpaceProblem, +{ + move |arena, state: State<'a>, min_indent: u32| { + let start = state.pos(); + match spaces().parse(arena, state, min_indent) { + Ok((progress, spaces, state)) => { + if progress == NoProgress || state.column() >= min_indent { + Ok((progress, spaces, state)) + } else { + Err((progress, indent_problem(start))) + } + } + Err((progress, err)) => Err((progress, err)), + } + } +} + +fn begins_with_crlf(bytes: &[u8]) -> bool { + bytes.len() >= 2 && bytes[0] == b'\r' && bytes[1] == b'\n' +} + +pub fn spaces<'a, E>() -> impl Parser<'a, &'a [CommentOrNewline<'a>], E> +where + E: 'a + SpaceProblem, +{ + move |arena, state: State<'a>, _min_indent: u32| { + let mut newlines = Vec::new_in(arena); + + match consume_spaces(state, |_, space, _| newlines.push(space)) { + Ok((progress, state)) => Ok((progress, newlines.into_bump_slice(), state)), + Err((progress, err)) => Err((progress, err)), + } + } +} + +pub fn loc_spaces<'a, E>() -> impl Parser<'a, &'a [Loc>], E> +where + E: 'a + SpaceProblem, +{ + move |arena, state: State<'a>, _min_indent: u32| { + let mut newlines = Vec::new_in(arena); + + match consume_spaces(state, |start, space, end| { + newlines.push(Loc::at(Region::between(start, end), space)) + }) { + Ok((progress, state)) => Ok((progress, newlines.into_bump_slice(), state)), + Err((progress, err)) => Err((progress, err)), + } + } +} + +fn consume_spaces<'a, E, F>( + mut state: State<'a>, + mut on_space: F, +) -> Result<(Progress, State<'a>), (Progress, E)> +where + E: 'a + SpaceProblem, + F: FnMut(Position, CommentOrNewline<'a>, Position), +{ + let mut progress = NoProgress; + let mut found_newline = false; + loop { + let whitespace = fast_eat_whitespace(state.bytes()); + if whitespace > 0 { + state.advance_mut(whitespace); + progress = MadeProgress; + } + + let start = state.pos(); + + match state.bytes().first() { + Some(b'#') => { + state.advance_mut(1); + + let is_doc_comment = state.bytes().first() == Some(&b'#') + && (state.bytes().get(1) == Some(&b' ') + || state.bytes().get(1) == Some(&b'\n') + || begins_with_crlf(&state.bytes()[1..]) + || Option::is_none(&state.bytes().get(1))); + + if is_doc_comment { + state.advance_mut(1); + if state.bytes().first() == Some(&b' ') { + state.advance_mut(1); + } + } + + let len = fast_eat_until_control_character(state.bytes()); + + // We already checked that the string is valid UTF-8 + debug_assert!(std::str::from_utf8(&state.bytes()[..len]).is_ok()); + let text = unsafe { std::str::from_utf8_unchecked(&state.bytes()[..len]) }; + + let comment = if is_doc_comment { + CommentOrNewline::DocComment(text) + } else { + CommentOrNewline::LineComment(text) + }; + state.advance_mut(len); + on_space(start, comment, state.pos()); + found_newline = true; + + if begins_with_crlf(state.bytes()) { + state.advance_mut(1); + state = state.advance_newline(); + } else if state.bytes().first() == Some(&b'\n') { + state = state.advance_newline(); + } + + progress = MadeProgress; + } + Some(b'\r') => { + if state.bytes().get(1) == Some(&b'\n') { + state.advance_mut(1); + state = state.advance_newline(); + on_space(start, CommentOrNewline::Newline, state.pos()); + found_newline = true; + progress = MadeProgress; + } else { + return Err(( + progress, + E::space_problem(BadInputError::HasMisplacedCarriageReturn, state.pos()), + )); + } + } + Some(b'\n') => { + state = state.advance_newline(); + on_space(start, CommentOrNewline::Newline, state.pos()); + found_newline = true; + progress = MadeProgress; + } + Some(b'\t') => { + return Err(( + progress, + E::space_problem(BadInputError::HasTab, state.pos()), + )); + } + Some(x) if *x < b' ' => { + return Err(( + progress, + E::space_problem(BadInputError::HasAsciiControl, state.pos()), + )); + } + _ => { + if found_newline { + state = state.mark_current_indent(); + } + break; + } + } + } + + Ok((progress, state)) +} diff --git a/crates/compiler/parse/src/expr.rs b/crates/compiler/parse/src/expr.rs new file mode 100644 index 0000000000..f51602a48d --- /dev/null +++ b/crates/compiler/parse/src/expr.rs @@ -0,0 +1,3019 @@ +use crate::ast::{ + AssignedField, Collection, CommentOrNewline, Defs, Expr, ExtractSpaces, Implements, + ImplementsAbilities, Pattern, RecordBuilderField, Spaceable, Spaces, TypeAnnotation, TypeDef, + TypeHeader, ValueDef, +}; +use crate::blankspace::{ + space0_after_e, space0_around_e_no_after_indent_check, space0_around_ee, space0_before_e, + space0_before_optional_after, space0_e, spaces, spaces_around, spaces_before, +}; +use crate::ident::{integer_ident, lowercase_ident, parse_ident, Accessor, Ident}; +use crate::keyword; +use crate::parser::{ + self, backtrackable, increment_min_indent, line_min_indent, optional, reset_min_indent, + sep_by1, sep_by1_e, set_min_indent, specialize, specialize_ref, then, word1, word1_indent, + word2, EClosure, EExpect, EExpr, EIf, EInParens, EList, ENumber, EPattern, ERecord, EString, + EType, EWhen, Either, ParseResult, Parser, +}; +use crate::pattern::{closure_param, loc_implements_parser}; +use crate::state::State; +use crate::string_literal::StrLikeLiteral; +use crate::type_annotation; +use bumpalo::collections::Vec; +use bumpalo::Bump; +use roc_collections::soa::Slice; +use roc_error_macros::internal_error; +use roc_module::called_via::{BinOp, CalledVia, UnaryOp}; +use roc_region::all::{Loc, Position, Region}; + +use crate::parser::Progress::{self, *}; + +fn expr_end<'a>() -> impl Parser<'a, (), EExpr<'a>> { + |_arena, state: State<'a>, _min_indent: u32| { + if state.has_reached_end() { + Ok((NoProgress, (), state)) + } else { + Err((NoProgress, EExpr::BadExprEnd(state.pos()))) + } + } +} + +pub fn test_parse_expr<'a>( + min_indent: u32, + arena: &'a bumpalo::Bump, + state: State<'a>, +) -> Result>, EExpr<'a>> { + let parser = skip_second!( + space0_before_optional_after(loc_expr(true), EExpr::IndentStart, EExpr::IndentEnd), + expr_end() + ); + + match parser.parse(arena, state, min_indent) { + Ok((_, expression, _)) => Ok(expression), + Err((_, fail)) => Err(fail), + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ExprParseOptions { + /// Check for and accept multi-backpassing syntax + /// This is usually true, but false within list/record literals + /// because the comma separating backpassing arguments conflicts + /// with the comma separating literal elements + pub accept_multi_backpassing: bool, + + /// Check for the `->` token, and raise an error if found + /// This is usually true, but false in if-guards + /// + /// > Just foo if foo == 2 -> ... + pub check_for_arrow: bool, +} + +pub fn expr_help<'a>() -> impl Parser<'a, Expr<'a>, EExpr<'a>> { + move |arena, state: State<'a>, min_indent: u32| { + loc_expr(true) + .parse(arena, state, min_indent) + .map(|(a, b, c)| (a, b.value, c)) + } +} + +fn loc_expr_in_parens_help<'a>() -> impl Parser<'a, Loc>, EInParens<'a>> { + then( + loc!(collection_trailing_sep_e!( + word1(b'(', EInParens::Open), + specialize_ref(EInParens::Expr, loc_expr(false)), + word1(b',', EInParens::End), + word1(b')', EInParens::End), + Expr::SpaceBefore + )), + move |arena, state, _, loc_elements| { + let elements = loc_elements.value; + let region = loc_elements.region; + + if elements.len() > 1 { + Ok(( + MadeProgress, + Loc::at(region, Expr::Tuple(elements.ptrify_items(arena))), + state, + )) + } else if elements.is_empty() { + Err((NoProgress, EInParens::Empty(state.pos()))) + } else { + // TODO: don't discard comments before/after + // (stored in the Collection) + Ok(( + MadeProgress, + Loc::at( + elements.items[0].region, + Expr::ParensAround(&elements.items[0].value), + ), + state, + )) + } + }, + ) + .trace("in_parens") +} + +fn loc_expr_in_parens_etc_help<'a>() -> impl Parser<'a, Loc>, EExpr<'a>> { + map_with_arena!( + loc!(and!( + specialize(EExpr::InParens, loc_expr_in_parens_help()), + record_field_access_chain() + )), + move |arena: &'a Bump, value: Loc<(Loc>, Vec<'a, Accessor<'a>>)>| { + let Loc { + mut region, + value: (loc_expr, field_accesses), + } = value; + + let mut value = loc_expr.value; + + // if there are field accesses, include the parentheses in the region + // otherwise, don't include the parentheses + if field_accesses.is_empty() { + region = loc_expr.region; + } else { + value = apply_expr_access_chain(arena, value, field_accesses); + } + + Loc::at(region, value) + } + ) +} + +fn record_field_access_chain<'a>() -> impl Parser<'a, Vec<'a, Accessor<'a>>, EExpr<'a>> { + zero_or_more!(skip_first!( + word1(b'.', EExpr::Access), + specialize( + |_, pos| EExpr::Access(pos), + one_of!( + map!(lowercase_ident(), Accessor::RecordField), + map!(integer_ident(), Accessor::TupleIndex), + ) + ) + )) +} + +/// In some contexts we want to parse the `_` as an expression, so it can then be turned into a +/// pattern later +fn loc_term_or_underscore_or_conditional<'a>( + options: ExprParseOptions, +) -> impl Parser<'a, Loc>, EExpr<'a>> { + one_of!( + loc_expr_in_parens_etc_help(), + loc!(specialize(EExpr::If, if_expr_help(options))), + loc!(specialize(EExpr::When, when::expr_help(options))), + loc!(specialize(EExpr::Str, string_like_literal_help())), + loc!(specialize(EExpr::Number, positive_number_literal_help())), + loc!(specialize(EExpr::Closure, closure_help(options))), + loc!(crash_kw()), + loc!(underscore_expression()), + loc!(record_literal_help()), + loc!(specialize(EExpr::List, list_literal_help())), + loc!(map_with_arena!( + assign_or_destructure_identifier(), + ident_to_expr + )), + ) +} + +/// In some contexts we want to parse the `_` as an expression, so it can then be turned into a +/// pattern later +fn loc_term_or_underscore<'a>( + options: ExprParseOptions, +) -> impl Parser<'a, Loc>, EExpr<'a>> { + one_of!( + loc_expr_in_parens_etc_help(), + loc!(specialize(EExpr::Str, string_like_literal_help())), + loc!(specialize(EExpr::Number, positive_number_literal_help())), + loc!(specialize(EExpr::Closure, closure_help(options))), + loc!(underscore_expression()), + loc!(record_literal_help()), + loc!(specialize(EExpr::List, list_literal_help())), + loc!(map_with_arena!( + assign_or_destructure_identifier(), + ident_to_expr + )), + ) +} + +fn loc_term<'a>(options: ExprParseOptions) -> impl Parser<'a, Loc>, EExpr<'a>> { + one_of!( + loc_expr_in_parens_etc_help(), + loc!(specialize(EExpr::Str, string_like_literal_help())), + loc!(specialize(EExpr::Number, positive_number_literal_help())), + loc!(specialize(EExpr::Closure, closure_help(options))), + loc!(record_literal_help()), + loc!(specialize(EExpr::List, list_literal_help())), + loc!(map_with_arena!( + assign_or_destructure_identifier(), + ident_to_expr + )), + ) +} + +fn underscore_expression<'a>() -> impl Parser<'a, Expr<'a>, EExpr<'a>> { + move |arena: &'a Bump, state: State<'a>, min_indent: u32| { + let start = state.pos(); + + let (_, _, next_state) = word1(b'_', EExpr::Underscore).parse(arena, state, min_indent)?; + + let lowercase_ident_expr = { specialize(move |_, _| EExpr::End(start), lowercase_ident()) }; + + let (_, output, final_state) = + optional(lowercase_ident_expr).parse(arena, next_state, min_indent)?; + + match output { + Some(name) => Ok((MadeProgress, Expr::Underscore(name), final_state)), + None => Ok((MadeProgress, Expr::Underscore(""), final_state)), + } + } +} + +fn crash_kw<'a>() -> impl Parser<'a, Expr<'a>, EExpr<'a>> { + move |arena: &'a Bump, state: State<'a>, min_indent: u32| { + let (_, _, next_state) = crate::parser::keyword_e(crate::keyword::CRASH, EExpr::Crash) + .parse(arena, state, min_indent)?; + + Ok((MadeProgress, Expr::Crash, next_state)) + } +} + +fn loc_possibly_negative_or_negated_term<'a>( + options: ExprParseOptions, +) -> impl Parser<'a, Loc>, EExpr<'a>> { + one_of![ + |arena, state: State<'a>, min_indent: u32| { + let initial = state.clone(); + + let (_, (loc_op, loc_expr), state) = + and!(loc!(unary_negate()), loc_term(options)).parse(arena, state, min_indent)?; + + let loc_expr = numeric_negate_expression(arena, initial, loc_op, loc_expr, &[]); + + Ok((MadeProgress, loc_expr, state)) + }, + // this will parse negative numbers, which the unary negate thing up top doesn't (for now) + loc!(specialize(EExpr::Number, number_literal_help())), + loc!(map_with_arena!( + and!( + loc!(word1(b'!', EExpr::Start)), + space0_before_e(loc_term(options), EExpr::IndentStart) + ), + |arena: &'a Bump, (loc_op, loc_expr): (Loc<_>, _)| { + Expr::UnaryOp(arena.alloc(loc_expr), Loc::at(loc_op.region, UnaryOp::Not)) + } + )), + loc_term_or_underscore_or_conditional(options) + ] +} + +fn fail_expr_start_e<'a, T: 'a>() -> impl Parser<'a, T, EExpr<'a>> { + |_arena, state: State<'a>, _min_indent: u32| Err((NoProgress, EExpr::Start(state.pos()))) +} + +fn unary_negate<'a>() -> impl Parser<'a, (), EExpr<'a>> { + move |_arena: &'a Bump, state: State<'a>, _min_indent: u32| { + // a minus is unary iff + // + // - it is preceded by whitespace (spaces, newlines, comments) + // - it is not followed by whitespace + let followed_by_whitespace = state + .bytes() + .get(1) + .map(|c| c.is_ascii_whitespace() || *c == b'#') + .unwrap_or(false); + + if state.bytes().starts_with(b"-") && !followed_by_whitespace { + // the negate is only unary if it is not followed by whitespace + let state = state.advance(1); + Ok((MadeProgress, (), state)) + } else { + // this is not a negated expression + Err((NoProgress, EExpr::UnaryNot(state.pos()))) + } + } +} + +fn expr_start<'a>(options: ExprParseOptions) -> impl Parser<'a, Loc>, EExpr<'a>> { + one_of![ + loc!(specialize(EExpr::If, if_expr_help(options))), + loc!(specialize(EExpr::When, when::expr_help(options))), + loc!(specialize(EExpr::Expect, expect_help(options))), + loc!(specialize(EExpr::Dbg, dbg_help(options))), + loc!(specialize(EExpr::Closure, closure_help(options))), + loc!(expr_operator_chain(options)), + fail_expr_start_e() + ] + .trace("expr_start") +} + +fn expr_operator_chain<'a>(options: ExprParseOptions) -> impl Parser<'a, Expr<'a>, EExpr<'a>> { + line_min_indent(move |arena, state: State<'a>, min_indent: u32| { + let (_, expr, state) = + loc_possibly_negative_or_negated_term(options).parse(arena, state, min_indent)?; + + let initial_state = state.clone(); + let end = state.pos(); + + match space0_e(EExpr::IndentEnd).parse(arena, state.clone(), min_indent) { + Err((_, _)) => Ok((MadeProgress, expr.value, state)), + Ok((_, spaces_before_op, state)) => { + let expr_state = ExprState { + operators: Vec::new_in(arena), + arguments: Vec::new_in(arena), + expr, + spaces_after: spaces_before_op, + end, + }; + + parse_expr_end(min_indent, options, expr_state, arena, state, initial_state) + } + } + }) +} + +#[derive(Debug)] +struct ExprState<'a> { + operators: Vec<'a, (Loc>, Loc)>, + arguments: Vec<'a, &'a Loc>>, + expr: Loc>, + spaces_after: &'a [CommentOrNewline<'a>], + end: Position, +} + +impl<'a> ExprState<'a> { + fn consume_spaces(&mut self, arena: &'a Bump) { + if !self.spaces_after.is_empty() { + if let Some(last) = self.arguments.pop() { + let new = last.value.with_spaces_after(self.spaces_after, last.region); + + self.arguments.push(arena.alloc(new)); + } else { + let region = self.expr.region; + + let mut value = Expr::Num(""); + std::mem::swap(&mut self.expr.value, &mut value); + + self.expr = arena + .alloc(value) + .with_spaces_after(self.spaces_after, region); + }; + + self.spaces_after = &[]; + } + } + + fn validate_assignment_or_backpassing( + mut self, + arena: &'a Bump, + loc_op: Loc, + argument_error: F, + ) -> Result>, EExpr<'a>> + where + F: Fn(Region, Position) -> EExpr<'a>, + { + if !self.operators.is_empty() { + // this `=` or `<-` likely occurred inline; treat it as an invalid operator + let opchar = match loc_op.value { + BinOp::Assignment => "=", + BinOp::Backpassing => "<-", + _ => unreachable!(), + }; + + let fail = EExpr::BadOperator(opchar, loc_op.region.start()); + + Err(fail) + } else if !self.expr.value.is_tag() + && !self.expr.value.is_opaque() + && !self.arguments.is_empty() + { + let region = Region::across_all(self.arguments.iter().map(|v| &v.region)); + + Err(argument_error(region, loc_op.region.start())) + } else { + self.consume_spaces(arena); + Ok(to_call(arena, self.arguments, self.expr)) + } + } + + fn validate_is_type_def( + mut self, + arena: &'a Bump, + loc_op: Loc, + kind: AliasOrOpaque, + ) -> Result<(Loc>, Vec<'a, &'a Loc>>), EExpr<'a>> { + debug_assert_eq!( + loc_op.value, + match kind { + AliasOrOpaque::Alias => BinOp::IsAliasType, + AliasOrOpaque::Opaque => BinOp::IsOpaqueType, + } + ); + + if !self.operators.is_empty() { + // this `:`/`:=` likely occurred inline; treat it as an invalid operator + let op = match kind { + AliasOrOpaque::Alias => ":", + AliasOrOpaque::Opaque => ":=", + }; + let fail = EExpr::BadOperator(op, loc_op.region.start()); + + Err(fail) + } else { + self.consume_spaces(arena); + Ok((self.expr, self.arguments)) + } + } +} + +#[allow(clippy::unnecessary_wraps)] +fn parse_expr_final<'a>(expr_state: ExprState<'a>, arena: &'a Bump) -> Expr<'a> { + let right_arg = to_call(arena, expr_state.arguments, expr_state.expr); + + if expr_state.operators.is_empty() { + right_arg.value + } else { + Expr::BinOps( + expr_state.operators.into_bump_slice(), + arena.alloc(right_arg), + ) + } +} + +fn to_call<'a>( + arena: &'a Bump, + mut arguments: Vec<'a, &'a Loc>>, + loc_expr1: Loc>, +) -> Loc> { + if arguments.is_empty() { + loc_expr1 + } else { + let last = arguments.last().map(|x| x.region).unwrap_or_default(); + let region = Region::span_across(&loc_expr1.region, &last); + + let spaces = if let Some(last) = arguments.last_mut() { + let spaces = last.value.extract_spaces(); + + if spaces.after.is_empty() { + &[] + } else { + let inner = if !spaces.before.is_empty() { + arena.alloc(spaces.item).before(spaces.before) + } else { + spaces.item + }; + *last = arena.alloc(Loc::at(last.region, inner)); + + spaces.after + } + } else { + &[] + }; + + let mut apply = Expr::Apply( + arena.alloc(loc_expr1), + arguments.into_bump_slice(), + CalledVia::Space, + ); + + if !spaces.is_empty() { + apply = arena.alloc(apply).after(spaces) + } + + Loc::at(region, apply) + } +} + +fn numeric_negate_expression<'a, T>( + arena: &'a Bump, + state: State<'a>, + loc_op: Loc, + expr: Loc>, + spaces: &'a [CommentOrNewline<'a>], +) -> Loc> { + debug_assert_eq!(state.bytes().first(), Some(&b'-')); + // for overflow reasons, we must make the unary minus part of the number literal. + let start = state.pos(); + let region = Region::new(start, expr.region.end()); + + let new_expr = match expr.value { + Expr::Num(string) => { + let new_string = + unsafe { std::str::from_utf8_unchecked(&state.bytes()[..string.len() + 1]) }; + + Expr::Num(new_string) + } + Expr::Float(string) => { + let new_string = + unsafe { std::str::from_utf8_unchecked(&state.bytes()[..string.len() + 1]) }; + + Expr::Float(new_string) + } + Expr::NonBase10Int { + string, + base, + is_negative, + } => { + // don't include the minus sign here; it will not be parsed right + Expr::NonBase10Int { + is_negative: !is_negative, + string, + base, + } + } + _ => Expr::UnaryOp(arena.alloc(expr), Loc::at(loc_op.region, UnaryOp::Negate)), + }; + + let new_loc_expr = Loc::at(region, new_expr); + + if spaces.is_empty() { + new_loc_expr + } else { + arena + .alloc(new_loc_expr.value) + .with_spaces_before(spaces, new_loc_expr.region) + } +} + +pub fn parse_single_def<'a>( + options: ExprParseOptions, + min_indent: u32, + arena: &'a Bump, + state: State<'a>, +) -> ParseResult<'a, Option>, EExpr<'a>> { + let initial = state.clone(); + + let mut spaces_before_current = &[] as &[_]; + let spaces_before_current_start = state.pos(); + + let state = match space0_e(EExpr::IndentStart).parse(arena, state, min_indent) { + Err((MadeProgress, bad_input @ EExpr::Space(_, _))) => { + return Err((MadeProgress, bad_input)); + } + Err((MadeProgress, _)) => { + return Err((MadeProgress, EExpr::DefMissingFinalExpr(initial.pos()))); + } + Ok((_, spaces, state)) => { + spaces_before_current = spaces; + state + } + Err((NoProgress, _)) => initial.clone(), + }; + + let start = state.pos(); + + let parse_expect_vanilla = crate::parser::keyword_e(crate::keyword::EXPECT, EExpect::Expect); + let parse_expect_fx = crate::parser::keyword_e(crate::keyword::EXPECT_FX, EExpect::Expect); + let parse_expect = either!(parse_expect_fx, parse_expect_vanilla); + + match space0_after_e(crate::pattern::loc_pattern_help(), EPattern::IndentEnd).parse( + arena, + state.clone(), + min_indent, + ) { + Err((NoProgress, _)) => { + match parse_expect.parse(arena, state.clone(), min_indent) { + Err((_, _)) => { + // a hacky way to get expression-based error messages. TODO fix this + Ok((NoProgress, None, initial)) + } + Ok((_, expect_flavor, state)) => parse_statement_inside_def( + arena, + state, + min_indent, + options, + start, + spaces_before_current_start, + spaces_before_current, + |preceding_comment, loc_def_expr| match expect_flavor { + Either::Second(_) => ValueDef::Expect { + condition: arena.alloc(loc_def_expr), + preceding_comment, + }, + Either::First(_) => ValueDef::ExpectFx { + condition: arena.alloc(loc_def_expr), + preceding_comment, + }, + }, + ), + } + } + Err((MadeProgress, _)) => { + // a hacky way to get expression-based error messages. TODO fix this + Ok((NoProgress, None, initial)) + } + Ok((_, loc_pattern, state)) => { + // First let's check whether this is an ability definition. + let opt_tag_and_args: Option<(&str, Region, &[Loc])> = match loc_pattern.value + { + Pattern::Apply( + Loc { + value: Pattern::Tag(name), + region, + }, + args, + ) => Some((name, *region, args)), + Pattern::Tag(name) => Some((name, loc_pattern.region, &[])), + _ => None, + }; + + if let Some((name, name_region, args)) = opt_tag_and_args { + if let Ok((_, loc_implements, state)) = + loc_implements_parser().parse(arena, state.clone(), min_indent) + { + let (_, (type_def, def_region), state) = finish_parsing_ability_def_help( + min_indent, + Loc::at(name_region, name), + args, + loc_implements, + arena, + state, + )?; + + return Ok(( + MadeProgress, + Some(SingleDef { + type_or_value: Either::First(type_def), + region: def_region, + spaces_before: spaces_before_current, + }), + state, + )); + } + } + + // Otherwise, this is a def or alias. + match operator().parse(arena, state, min_indent) { + Ok((_, BinOp::Assignment, state)) => { + let parse_def_expr = space0_before_e( + increment_min_indent(expr_start(options)), + EExpr::IndentEnd, + ); + + let (_, loc_def_expr, state) = + parse_def_expr.parse(arena, state, min_indent)?; + let value_def = + ValueDef::Body(arena.alloc(loc_pattern), &*arena.alloc(loc_def_expr)); + let region = Region::span_across(&loc_pattern.region, &loc_def_expr.region); + + Ok(( + MadeProgress, + Some(SingleDef { + type_or_value: Either::Second(value_def), + region, + spaces_before: spaces_before_current, + }), + state, + )) + } + Ok((_, BinOp::IsAliasType, state)) => { + // the increment_min_indent here is probably _wrong_, since alias_signature_with_space_before does + // that internally. + // TODO: re-evaluate this + let parser = increment_min_indent(alias_signature_with_space_before()); + let (_, ann_type, state) = parser.parse(arena, state, min_indent)?; + let region = Region::span_across(&loc_pattern.region, &ann_type.region); + + match &loc_pattern.value.extract_spaces().item { + Pattern::Apply( + Loc { + value: Pattern::Tag(name), + .. + }, + alias_arguments, + ) => { + let name = Loc::at(loc_pattern.region, *name); + let header = TypeHeader { + name, + vars: alias_arguments, + }; + + let type_def = TypeDef::Alias { + header, + ann: ann_type, + }; + + Ok(( + MadeProgress, + Some(SingleDef { + type_or_value: Either::First(type_def), + region, + spaces_before: spaces_before_current, + }), + state, + )) + } + Pattern::Tag(name) => { + let name = Loc::at(loc_pattern.region, *name); + let pattern_arguments: &'a [Loc>] = &[]; + let header = TypeHeader { + name, + vars: pattern_arguments, + }; + + let type_def = TypeDef::Alias { + header, + ann: ann_type, + }; + + Ok(( + MadeProgress, + Some(SingleDef { + type_or_value: Either::First(type_def), + region, + spaces_before: spaces_before_current, + }), + state, + )) + } + _ => { + let value_def = ValueDef::Annotation(loc_pattern, ann_type); + + Ok(( + MadeProgress, + Some(SingleDef { + type_or_value: Either::Second(value_def), + region, + spaces_before: spaces_before_current, + }), + state, + )) + } + } + } + Ok((_, BinOp::IsOpaqueType, state)) => { + let (_, (signature, derived), state) = + opaque_signature_with_space_before().parse(arena, state, min_indent + 1)?; + let region = Region::span_across(&loc_pattern.region, &signature.region); + + match &loc_pattern.value.extract_spaces().item { + Pattern::Apply( + Loc { + value: Pattern::Tag(name), + .. + }, + alias_arguments, + ) => { + let name = Loc::at(loc_pattern.region, *name); + let header = TypeHeader { + name, + vars: alias_arguments, + }; + + let type_def = TypeDef::Opaque { + header, + typ: signature, + derived, + }; + + Ok(( + MadeProgress, + Some(SingleDef { + type_or_value: Either::First(type_def), + region, + spaces_before: spaces_before_current, + }), + state, + )) + } + Pattern::Tag(name) => { + let name = Loc::at(loc_pattern.region, *name); + let pattern_arguments: &'a [Loc>] = &[]; + let header = TypeHeader { + name, + vars: pattern_arguments, + }; + + let type_def = TypeDef::Opaque { + header, + typ: signature, + derived, + }; + + Ok(( + MadeProgress, + Some(SingleDef { + type_or_value: Either::First(type_def), + region, + spaces_before: spaces_before_current, + }), + state, + )) + } + _ => { + let value_def = ValueDef::Annotation(loc_pattern, signature); + + Ok(( + MadeProgress, + Some(SingleDef { + type_or_value: Either::Second(value_def), + region, + spaces_before: spaces_before_current, + }), + state, + )) + } + } + } + _ => Ok((MadeProgress, None, initial)), + } + } + } +} + +/// e.g. Things that can be on their own line in a def, e.g. `expect`, `expect-fx`, or `dbg` +#[allow(clippy::too_many_arguments)] +fn parse_statement_inside_def<'a>( + arena: &'a Bump, + state: State<'a>, + min_indent: u32, + options: ExprParseOptions, + start: Position, + spaces_before_current_start: Position, + spaces_before_current: &'a [CommentOrNewline<'a>], + get_value_def: impl Fn(Region, Loc>) -> ValueDef<'a>, +) -> Result<(Progress, Option>, State<'a>), (Progress, EExpr<'a>)> { + let parse_def_expr = + space0_before_e(increment_min_indent(expr_start(options)), EExpr::IndentEnd); + let (_, loc_def_expr, state) = parse_def_expr.parse(arena, state, min_indent)?; + let end = loc_def_expr.region.end(); + let region = Region::new(start, end); + + // drop newlines before the preceding comment + let spaces_before_start = spaces_before_current_start.offset as usize; + let spaces_before_end = start.offset as usize; + let mut spaces_before_current_start = spaces_before_current_start; + + for byte in &state.original_bytes()[spaces_before_start..spaces_before_end] { + match byte { + b' ' | b'\n' => { + spaces_before_current_start.offset += 1; + } + _ => break, + } + } + + let preceding_comment = Region::new(spaces_before_current_start, start); + let value_def = get_value_def(preceding_comment, loc_def_expr); + + Ok(( + MadeProgress, + Some(SingleDef { + type_or_value: Either::Second(value_def), + region, + spaces_before: spaces_before_current, + }), + state, + )) +} + +// This is a macro only because trying to make it be a function caused lifetime issues. +#[macro_export] +macro_rules! join_ann_to_body { + ($arena:expr, $loc_pattern:expr, $loc_def_expr:expr, $ann_pattern:expr, $ann_type:expr, $spaces_before_current:expr, $region:expr) => {{ + // join this body with the preceding annotation + + let value_def = ValueDef::AnnotatedBody { + ann_pattern: $arena.alloc(*$ann_pattern), + ann_type: $arena.alloc(*$ann_type), + comment: $spaces_before_current + .first() + .and_then($crate::ast::CommentOrNewline::comment_str), + body_pattern: $arena.alloc($loc_pattern), + body_expr: *$arena.alloc($loc_def_expr), + }; + + ( + value_def, + roc_region::all::Region::span_across(&$ann_pattern.region, &$region), + ) + }}; +} + +// This is a macro only because trying to make it be a function caused lifetime issues. +#[macro_export] +macro_rules! join_alias_to_body { + ($arena:expr, $loc_pattern:expr, $loc_def_expr:expr, $header:expr, $ann_type:expr, $spaces_before_current:expr, $region:expr) => {{ + use roc_region::all::Region; + + // This is a case like + // UserId x : [UserId Int] + // UserId x = UserId 42 + // We optimistically parsed the first line as an alias; we now turn it + // into an annotation. + + let loc_name = $arena.alloc($header.name.map(|x| Pattern::Tag(x))); + let ann_pattern = Pattern::Apply(loc_name, $header.vars); + + let vars_region = Region::across_all($header.vars.iter().map(|v| &v.region)); + let region_ann_pattern = Region::span_across(&loc_name.region, &vars_region); + let loc_ann_pattern = Loc::at(region_ann_pattern, ann_pattern); + + let value_def = ValueDef::AnnotatedBody { + ann_pattern: $arena.alloc(loc_ann_pattern), + ann_type: $arena.alloc(*$ann_type), + comment: $spaces_before_current + .first() + .and_then($crate::ast::CommentOrNewline::comment_str), + body_pattern: $arena.alloc($loc_pattern), + body_expr: *$arena.alloc($loc_def_expr), + }; + + ( + value_def, + Region::span_across(&$header.name.region, &$region), + ) + }}; +} + +fn parse_defs_end<'a>( + options: ExprParseOptions, + min_indent: u32, + mut defs: Defs<'a>, + arena: &'a Bump, + state: State<'a>, +) -> ParseResult<'a, Defs<'a>, EExpr<'a>> { + let mut global_state = state; + + loop { + let state = global_state; + + global_state = match parse_single_def(options, min_indent, arena, state) { + Ok((_, Some(single_def), next_state)) => { + let region = single_def.region; + let spaces_before_current = single_def.spaces_before; + + match single_def.type_or_value { + Either::First(type_def) => { + defs.push_type_def(type_def, region, spaces_before_current, &[]); + } + Either::Second(value_def) => { + // If we got a ValueDef::Body, check if a type annotation preceded it. + // If so, we may need to combine them into an AnnotatedBody. + let joined = match value_def { + ValueDef::Body(loc_pattern, loc_def_expr) + if spaces_before_current.len() <= 1 => + { + let region = + Region::span_across(&loc_pattern.region, &loc_def_expr.region); + + match defs.last() { + Some(Err(ValueDef::Annotation(ann_pattern, ann_type))) => { + let (value_def, region) = join_ann_to_body!( + arena, + loc_pattern, + loc_def_expr, + ann_pattern, + ann_type, + spaces_before_current, + region + ); + + defs.replace_with_value_def( + defs.tags.len() - 1, + value_def, + region, + ); + + true + } + Some(Ok(TypeDef::Alias { + header, + ann: ann_type, + })) => { + let (value_def, region) = join_alias_to_body!( + arena, + loc_pattern, + loc_def_expr, + header, + ann_type, + spaces_before_current, + region + ); + + defs.replace_with_value_def( + defs.tags.len() - 1, + value_def, + region, + ); + + true + } + _ => false, + } + } + _ => false, + }; + + if !joined { + // the previous and current def can't be joined up + defs.push_value_def(value_def, region, spaces_before_current, &[]); + } + } + } + + next_state + } + Ok((progress, None, s)) => return Ok((progress, defs, s)), + Err((progress, err)) => return Err((progress, err)), + }; + } +} + +pub struct SingleDef<'a> { + pub type_or_value: Either, ValueDef<'a>>, + pub region: Region, + pub spaces_before: &'a [CommentOrNewline<'a>], +} + +fn parse_defs_expr<'a>( + options: ExprParseOptions, + min_indent: u32, + defs: Defs<'a>, + arena: &'a Bump, + state: State<'a>, +) -> ParseResult<'a, Expr<'a>, EExpr<'a>> { + match parse_defs_end(options, min_indent, defs, arena, state) { + Err(bad) => Err(bad), + Ok((_, def_state, state)) => { + // this is no def, because there is no `=` or `:`; parse as an expr + let parse_final_expr = space0_before_e(expr_start(options), EExpr::IndentEnd); + + match parse_final_expr.parse(arena, state.clone(), min_indent) { + Err((_, fail)) => { + return Err(( + MadeProgress, + EExpr::DefMissingFinalExpr2(arena.alloc(fail), state.pos()), + )); + } + Ok((_, loc_ret, state)) => { + return Ok(( + MadeProgress, + Expr::Defs(arena.alloc(def_state), arena.alloc(loc_ret)), + state, + )); + } + } + } + } +} + +fn alias_signature_with_space_before<'a>() -> impl Parser<'a, Loc>, EExpr<'a>> { + increment_min_indent(specialize( + EExpr::Type, + space0_before_e(type_annotation::located(false), EType::TIndentStart), + )) +} + +fn opaque_signature_with_space_before<'a>() -> impl Parser< + 'a, + ( + Loc>, + Option>>, + ), + EExpr<'a>, +> { + and!( + specialize( + EExpr::Type, + space0_before_e( + type_annotation::located_opaque_signature(true), + EType::TIndentStart, + ), + ), + optional(backtrackable(specialize( + EExpr::Type, + space0_before_e(type_annotation::implements_abilities(), EType::TIndentStart,), + ))) + ) +} + +#[derive(Copy, Clone, PartialEq, Eq)] +enum AliasOrOpaque { + Alias, + Opaque, +} + +fn extract_tag_and_spaces<'a>(arena: &'a Bump, expr: Expr<'a>) -> Option> { + let mut expr = expr.extract_spaces(); + + loop { + match &expr.item { + Expr::ParensAround(inner_expr) => { + let inner_expr = inner_expr.extract_spaces(); + expr.item = inner_expr.item; + expr.before = merge_spaces(arena, expr.before, inner_expr.before); + expr.after = merge_spaces(arena, inner_expr.after, expr.after); + } + Expr::Tag(tag) => { + return Some(Spaces { + before: expr.before, + item: tag, + after: expr.after, + }); + } + _ => return None, + } + } +} + +#[allow(clippy::too_many_arguments)] +fn finish_parsing_alias_or_opaque<'a>( + min_indent: u32, + options: ExprParseOptions, + expr_state: ExprState<'a>, + loc_op: Loc, + arena: &'a Bump, + state: State<'a>, + spaces_after_operator: &'a [CommentOrNewline<'a>], + kind: AliasOrOpaque, +) -> ParseResult<'a, Expr<'a>, EExpr<'a>> { + let expr_region = expr_state.expr.region; + let indented_more = min_indent + 1; + + let (expr, arguments) = expr_state + .validate_is_type_def(arena, loc_op, kind) + .map_err(|fail| (MadeProgress, fail))?; + + let mut defs = Defs::default(); + + let state = if let Some(tag) = extract_tag_and_spaces(arena, expr.value) { + let name = tag.item; + let mut type_arguments = Vec::with_capacity_in(arguments.len(), arena); + + for argument in arguments { + match expr_to_pattern_help(arena, &argument.value) { + Ok(good) => { + type_arguments.push(Loc::at(argument.region, good)); + } + Err(()) => { + return Err(( + MadeProgress, + EExpr::Pattern( + arena.alloc(EPattern::NotAPattern(state.pos())), + state.pos(), + ), + )); + } + } + } + + match kind { + AliasOrOpaque::Alias => { + let (_, signature, state) = + alias_signature_with_space_before().parse(arena, state, min_indent)?; + + let def_region = Region::span_across(&expr.region, &signature.region); + + let header = TypeHeader { + name: Loc::at(expr.region, name), + vars: type_arguments.into_bump_slice(), + }; + + let def = TypeDef::Alias { + header, + ann: signature, + }; + + defs.push_type_def(def, def_region, &[], &[]); + + state + } + + AliasOrOpaque::Opaque => { + let (_, (signature, derived), state) = + opaque_signature_with_space_before().parse(arena, state, indented_more)?; + + let def_region = Region::span_across(&expr.region, &signature.region); + + let header = TypeHeader { + name: Loc::at(expr.region, name), + vars: type_arguments.into_bump_slice(), + }; + + let def = TypeDef::Opaque { + header, + typ: signature, + derived, + }; + + defs.push_type_def(def, def_region, &[], &[]); + + state + } + } + } else { + let call = to_call(arena, arguments, expr); + + match expr_to_pattern_help(arena, &call.value) { + Ok(good) => { + let parser = specialize( + EExpr::Type, + space0_before_e( + set_min_indent(indented_more, type_annotation::located(false)), + EType::TIndentStart, + ), + ); + + match parser.parse(arena, state.clone(), min_indent) { + Err((_, fail)) => return Err((MadeProgress, fail)), + Ok((_, mut ann_type, state)) => { + // put the spaces from after the operator in front of the call + if !spaces_after_operator.is_empty() { + ann_type = arena + .alloc(ann_type.value) + .with_spaces_before(spaces_after_operator, ann_type.region); + } + + let def_region = Region::span_across(&call.region, &ann_type.region); + + let value_def = ValueDef::Annotation(Loc::at(expr_region, good), ann_type); + + defs.push_value_def(value_def, def_region, &[], &[]); + + state + } + } + } + Err(_) => { + // this `:`/`:=` likely occurred inline; treat it as an invalid operator + let op = match kind { + AliasOrOpaque::Alias => ":", + AliasOrOpaque::Opaque => ":=", + }; + let fail = EExpr::BadOperator(op, loc_op.region.start()); + + return Err((MadeProgress, fail)); + } + } + }; + + parse_defs_expr(options, min_indent, defs, arena, state) +} + +mod ability { + use super::*; + use crate::{ + ast::{AbilityMember, Spaceable, Spaced}, + parser::EAbility, + }; + + /// Parses a single ability demand line; see `parse_demand`. + fn parse_demand_help<'a>() -> impl Parser<'a, AbilityMember<'a>, EAbility<'a>> { + map!( + // Require the type to be more indented than the name + absolute_indented_seq!( + specialize(|_, pos| EAbility::DemandName(pos), loc!(lowercase_ident())), + skip_first!( + and!( + // TODO: do we get anything from picking up spaces here? + space0_e(EAbility::DemandName), + word1(b':', EAbility::DemandColon) + ), + specialize(EAbility::Type, type_annotation::located(true)) + ) + ), + |(name, typ): (Loc<&'a str>, Loc>)| { + AbilityMember { + name: name.map_owned(Spaced::Item), + typ, + } + } + ) + } + + pub enum IndentLevel { + PendingMin(u32), + Exact(u32), + } + + /// Parses an ability demand like `hash : a -> U64 where a implements Hash`, in the context of a larger + /// ability definition. + /// This is basically the same as parsing a free-floating annotation, but with stricter rules. + pub fn parse_demand<'a>( + indent: IndentLevel, + ) -> impl Parser<'a, (u32, AbilityMember<'a>), EAbility<'a>> { + move |arena, state: State<'a>, min_indent: u32| { + // Put no restrictions on the indent after the spaces; we'll check it manually. + match space0_e(EAbility::DemandName).parse(arena, state, 0) { + Err((MadeProgress, fail)) => Err((NoProgress, fail)), + Err((NoProgress, fail)) => Err((NoProgress, fail)), + + Ok((_progress, spaces, state)) => { + match indent { + IndentLevel::PendingMin(min_indent) if state.column() < min_indent => { + let indent_difference = state.column() as i32 - min_indent as i32; + Err(( + MadeProgress, + EAbility::DemandAlignment(indent_difference, state.pos()), + )) + } + IndentLevel::Exact(wanted) if state.column() < wanted => { + // This demand is not indented correctly + let indent_difference = state.column() as i32 - wanted as i32; + Err(( + // Rollback because the deindent may be because there is a next + // expression + NoProgress, + EAbility::DemandAlignment(indent_difference, state.pos()), + )) + } + IndentLevel::Exact(wanted) if state.column() > wanted => { + // This demand is not indented correctly + let indent_difference = state.column() as i32 - wanted as i32; + + // We might be trying to parse at EOF, at which case the indent level + // will be off, but there is actually nothing left. + let progress = if state.has_reached_end() { + NoProgress + } else { + MadeProgress + }; + + Err(( + progress, + EAbility::DemandAlignment(indent_difference, state.pos()), + )) + } + _ => { + let indent_column = state.column(); + + let parser = parse_demand_help(); + + match parser.parse(arena, state.clone(), min_indent) { + Err((MadeProgress, fail)) => Err((MadeProgress, fail)), + Err((NoProgress, fail)) => { + // We made progress relative to the entire ability definition, + // so this is an error. + Err((MadeProgress, fail)) + } + + Ok((_, mut demand, state)) => { + // Tag spaces onto the parsed demand name + if !spaces.is_empty() { + demand.name = arena + .alloc(demand.name.value) + .with_spaces_before(spaces, demand.name.region); + } + + Ok((MadeProgress, (indent_column, demand), state)) + } + } + } + } + } + } + } + } +} + +fn finish_parsing_ability_def_help<'a>( + start_column: u32, + name: Loc<&'a str>, + args: &'a [Loc>], + loc_implements: Loc>, + arena: &'a Bump, + state: State<'a>, +) -> ParseResult<'a, (TypeDef<'a>, Region), EExpr<'a>> { + let mut demands = Vec::with_capacity_in(2, arena); + + let min_indent_for_demand = start_column + 1; + + // Parse the first demand. This will determine the indentation level all the + // other demands must observe. + let start = state.pos(); + let (_, (demand_indent_level, first_demand), mut state) = + ability::parse_demand(ability::IndentLevel::PendingMin(min_indent_for_demand)) + .parse(arena, state, min_indent_for_demand) + .map_err(|(progress, err)| (progress, EExpr::Ability(err, start)))?; + demands.push(first_demand); + + let demand_indent = ability::IndentLevel::Exact(demand_indent_level); + let demand_parser = ability::parse_demand(demand_indent); + + loop { + match demand_parser.parse(arena, state.clone(), min_indent_for_demand) { + Ok((_, (_indent, demand), next_state)) => { + state = next_state; + demands.push(demand); + } + Err((MadeProgress, problem)) => { + return Err((MadeProgress, EExpr::Ability(problem, state.pos()))); + } + Err((NoProgress, _)) => { + break; + } + } + } + + let def_region = Region::span_across(&name.region, &demands.last().unwrap().typ.region); + let type_def = TypeDef::Ability { + header: TypeHeader { name, vars: args }, + loc_implements, + members: demands.into_bump_slice(), + }; + + Ok((MadeProgress, (type_def, def_region), state)) +} + +fn parse_expr_operator<'a>( + min_indent: u32, + options: ExprParseOptions, + mut expr_state: ExprState<'a>, + loc_op: Loc, + arena: &'a Bump, + state: State<'a>, + initial_state: State<'a>, +) -> ParseResult<'a, Expr<'a>, EExpr<'a>> { + let (_, spaces_after_operator, state) = + space0_e(EExpr::IndentEnd).parse(arena, state, min_indent)?; + + // a `-` is unary if it is preceded by a space and not followed by a space + + let op = loc_op.value; + let op_start = loc_op.region.start(); + let op_end = loc_op.region.end(); + let new_start = state.pos(); + match op { + BinOp::Minus if expr_state.end != op_start && op_end == new_start => { + // negative terms + + let (_, negated_expr, state) = loc_term(options).parse(arena, state, min_indent)?; + let new_end = state.pos(); + + let arg = numeric_negate_expression( + arena, + initial_state, + loc_op, + negated_expr, + expr_state.spaces_after, + ); + + let initial_state = state.clone(); + + let (spaces, state) = + match space0_e(EExpr::IndentEnd).parse(arena, state.clone(), min_indent) { + Err((_, _)) => (&[] as &[_], state), + Ok((_, spaces, state)) => (spaces, state), + }; + + expr_state.arguments.push(arena.alloc(arg)); + expr_state.spaces_after = spaces; + expr_state.end = new_end; + + parse_expr_end(min_indent, options, expr_state, arena, state, initial_state) + } + BinOp::Assignment => { + let expr_region = expr_state.expr.region; + let indented_more = min_indent + 1; + + let call = expr_state + .validate_assignment_or_backpassing(arena, loc_op, EExpr::ElmStyleFunction) + .map_err(|fail| (MadeProgress, fail))?; + + let (value_def, def_region, state) = { + match expr_to_pattern_help(arena, &call.value) { + Ok(good) => { + let (_, mut body, state) = + expr_start(options).parse(arena, state, indented_more)?; + + // put the spaces from after the operator in front of the call + if !spaces_after_operator.is_empty() { + body = arena + .alloc(body.value) + .with_spaces_before(spaces_after_operator, body.region); + } + + let body_region = Region::span_across(&call.region, &body.region); + + let alias = ValueDef::Body( + arena.alloc(Loc::at(expr_region, good)), + arena.alloc(body), + ); + + (alias, body_region, state) + } + Err(_) => { + // this `=` likely occurred inline; treat it as an invalid operator + let fail = EExpr::BadOperator(arena.alloc("="), loc_op.region.start()); + + return Err((MadeProgress, fail)); + } + } + }; + + let mut defs = Defs::default(); + defs.push_value_def(value_def, def_region, &[], &[]); + + parse_defs_expr(options, min_indent, defs, arena, state) + } + BinOp::Backpassing => { + let expr_region = expr_state.expr.region; + let indented_more = min_indent + 1; + + let call = expr_state + .validate_assignment_or_backpassing(arena, loc_op, |_, pos| { + EExpr::BadOperator("<-", pos) + }) + .map_err(|fail| (MadeProgress, fail))?; + + let (loc_pattern, loc_body, state) = { + match expr_to_pattern_help(arena, &call.value) { + Ok(good) => { + let (_, mut ann_type, state) = + expr_start(options).parse(arena, state, indented_more)?; + + // put the spaces from after the operator in front of the call + if !spaces_after_operator.is_empty() { + ann_type = arena + .alloc(ann_type.value) + .with_spaces_before(spaces_after_operator, ann_type.region); + } + + (Loc::at(expr_region, good), ann_type, state) + } + Err(_) => { + // this `=` likely occurred inline; treat it as an invalid operator + let fail = EExpr::BadOperator("=", loc_op.region.start()); + + return Err((MadeProgress, fail)); + } + } + }; + + let parse_cont = space0_before_e(expr_start(options), EExpr::IndentEnd); + + let (_, loc_cont, state) = parse_cont.parse(arena, state, min_indent)?; + + let ret = Expr::Backpassing( + arena.alloc([loc_pattern]), + arena.alloc(loc_body), + arena.alloc(loc_cont), + ); + + Ok((MadeProgress, ret, state)) + } + BinOp::IsAliasType | BinOp::IsOpaqueType => finish_parsing_alias_or_opaque( + min_indent, + options, + expr_state, + loc_op, + arena, + state, + spaces_after_operator, + match op { + BinOp::IsAliasType => AliasOrOpaque::Alias, + BinOp::IsOpaqueType => AliasOrOpaque::Opaque, + _ => unreachable!(), + }, + ), + _ => match loc_possibly_negative_or_negated_term(options).parse( + arena, + state.clone(), + min_indent, + ) { + Err((MadeProgress, f)) => Err((MadeProgress, f)), + Ok((_, mut new_expr, state)) => { + let new_end = state.pos(); + + let initial_state = state.clone(); + + // put the spaces from after the operator in front of the new_expr + if !spaces_after_operator.is_empty() { + new_expr = arena + .alloc(new_expr.value) + .with_spaces_before(spaces_after_operator, new_expr.region); + } + + match space0_e(EExpr::IndentEnd).parse(arena, state.clone(), min_indent) { + Err((_, _)) => { + let args = std::mem::replace(&mut expr_state.arguments, Vec::new_in(arena)); + + let call = to_call(arena, args, expr_state.expr); + + expr_state.operators.push((call, loc_op)); + expr_state.expr = new_expr; + expr_state.end = new_end; + expr_state.spaces_after = &[]; + + let expr = parse_expr_final(expr_state, arena); + Ok((MadeProgress, expr, state)) + } + Ok((_, spaces, state)) => { + let args = std::mem::replace(&mut expr_state.arguments, Vec::new_in(arena)); + + let call = to_call(arena, args, expr_state.expr); + + expr_state.operators.push((call, loc_op)); + expr_state.expr = new_expr; + expr_state.end = new_end; + expr_state.spaces_after = spaces; + + // TODO new start? + parse_expr_end(min_indent, options, expr_state, arena, state, initial_state) + } + } + } + Err((NoProgress, _e)) => { + return Err((MadeProgress, EExpr::TrailingOperator(state.pos()))); + } + }, + } +} + +fn parse_expr_end<'a>( + min_indent: u32, + options: ExprParseOptions, + mut expr_state: ExprState<'a>, + arena: &'a Bump, + state: State<'a>, + initial_state: State<'a>, +) -> ParseResult<'a, Expr<'a>, EExpr<'a>> { + let parser = skip_first!( + crate::blankspace::check_indent(EExpr::IndentEnd), + loc_term_or_underscore(options) + ); + + match parser.parse(arena, state.clone(), min_indent) { + Err((MadeProgress, f)) => Err((MadeProgress, f)), + Ok(( + _, + has @ Loc { + value: + Expr::Var { + module_name: "", + ident: crate::keyword::IMPLEMENTS, + }, + .. + }, + state, + )) if matches!(expr_state.expr.value, Expr::Tag(..)) => { + // This is an ability definition, `Ability arg1 ... implements ...`. + + let name = expr_state.expr.map_owned(|e| match e { + Expr::Tag(name) => name, + _ => unreachable!(), + }); + + let mut arguments = Vec::with_capacity_in(expr_state.arguments.len(), arena); + for argument in expr_state.arguments { + match expr_to_pattern_help(arena, &argument.value) { + Ok(good) => { + arguments.push(Loc::at(argument.region, good)); + } + Err(_) => { + let start = argument.region.start(); + let err = &*arena.alloc(EPattern::Start(start)); + return Err((MadeProgress, EExpr::Pattern(err, argument.region.start()))); + } + } + } + + // Attach any spaces to the `implements` keyword + let has = if !expr_state.spaces_after.is_empty() { + arena + .alloc(Implements::Implements) + .with_spaces_before(expr_state.spaces_after, has.region) + } else { + Loc::at(has.region, Implements::Implements) + }; + + let args = arguments.into_bump_slice(); + let (_, (type_def, def_region), state) = + finish_parsing_ability_def_help(min_indent, name, args, has, arena, state)?; + + let mut defs = Defs::default(); + + defs.push_type_def(type_def, def_region, &[], &[]); + + parse_defs_expr(options, min_indent, defs, arena, state) + } + Ok((_, mut arg, state)) => { + let new_end = state.pos(); + + // now that we have `function arg1 ... argn`, attach the spaces to the `argn` + if !expr_state.spaces_after.is_empty() { + arg = arena + .alloc(arg.value) + .with_spaces_before(expr_state.spaces_after, arg.region); + + expr_state.spaces_after = &[]; + } + let initial_state = state.clone(); + + match space0_e(EExpr::IndentEnd).parse(arena, state.clone(), min_indent) { + Err((_, _)) => { + expr_state.arguments.push(arena.alloc(arg)); + expr_state.end = new_end; + expr_state.spaces_after = &[]; + + let expr = parse_expr_final(expr_state, arena); + Ok((MadeProgress, expr, state)) + } + Ok((_, new_spaces, state)) => { + expr_state.arguments.push(arena.alloc(arg)); + expr_state.end = new_end; + expr_state.spaces_after = new_spaces; + + parse_expr_end(min_indent, options, expr_state, arena, state, initial_state) + } + } + } + Err((NoProgress, _)) => { + let before_op = state.clone(); + // try an operator + match loc!(operator()).parse(arena, state.clone(), min_indent) { + Err((MadeProgress, f)) => Err((MadeProgress, f)), + Ok((_, loc_op, state)) => { + expr_state.consume_spaces(arena); + let initial_state = before_op; + parse_expr_operator( + min_indent, + options, + expr_state, + loc_op, + arena, + state, + initial_state, + ) + } + Err((NoProgress, _)) => { + let mut state = state; + // try multi-backpassing + if options.accept_multi_backpassing && state.bytes().starts_with(b",") { + state = state.advance(1); + + let (_, mut patterns, state) = specialize_ref( + EExpr::Pattern, + crate::parser::sep_by0( + word1(b',', EPattern::Start), + space0_around_ee( + crate::pattern::loc_pattern_help(), + EPattern::Start, + EPattern::IndentEnd, + ), + ), + ) + .parse(arena, state, min_indent)?; + + expr_state.consume_spaces(arena); + let call = to_call(arena, expr_state.arguments, expr_state.expr); + + let pattern = expr_to_pattern_help(arena, &call.value).map_err(|()| { + ( + MadeProgress, + EExpr::Pattern( + arena.alloc(EPattern::NotAPattern(state.pos())), + state.pos(), + ), + ) + })?; + + let loc_pattern = Loc::at(call.region, pattern); + + patterns.insert(0, loc_pattern); + + match word2(b'<', b'-', EExpr::BackpassArrow).parse( + arena, + state.clone(), + min_indent, + ) { + Err((_, fail)) => Err((MadeProgress, fail)), + Ok((_, _, state)) => { + let parse_body = space0_before_e( + increment_min_indent(expr_start(options)), + EExpr::IndentEnd, + ); + + let (_, loc_body, state) = + parse_body.parse(arena, state, min_indent)?; + + let parse_cont = + space0_before_e(expr_start(options), EExpr::IndentEnd); + + let (_, loc_cont, state) = + parse_cont.parse(arena, state, min_indent)?; + + let ret = Expr::Backpassing( + patterns.into_bump_slice(), + arena.alloc(loc_body), + arena.alloc(loc_cont), + ); + + Ok((MadeProgress, ret, state)) + } + } + } else if options.check_for_arrow && state.bytes().starts_with(b"->") { + Err((MadeProgress, EExpr::BadOperator("->", state.pos()))) + } else { + let expr = parse_expr_final(expr_state, arena); + + // roll back space parsing + Ok((MadeProgress, expr, initial_state)) + } + } + } + } + } +} + +pub fn loc_expr<'a>(accept_multi_backpassing: bool) -> impl Parser<'a, Loc>, EExpr<'a>> { + expr_start(ExprParseOptions { + accept_multi_backpassing, + check_for_arrow: true, + }) +} + +pub fn merge_spaces<'a>( + arena: &'a Bump, + a: &'a [CommentOrNewline<'a>], + b: &'a [CommentOrNewline<'a>], +) -> &'a [CommentOrNewline<'a>] { + if a.is_empty() { + b + } else if b.is_empty() { + a + } else { + let mut merged = Vec::with_capacity_in(a.len() + b.len(), arena); + merged.extend_from_slice(a); + merged.extend_from_slice(b); + merged.into_bump_slice() + } +} + +/// If the given Expr would parse the same way as a valid Pattern, convert it. +/// Example: (foo) could be either an Expr::Var("foo") or Pattern::Identifier("foo") +fn expr_to_pattern_help<'a>(arena: &'a Bump, expr: &Expr<'a>) -> Result, ()> { + let mut expr = expr.extract_spaces(); + + if let Expr::ParensAround(loc_expr) = &expr.item { + let expr_inner = loc_expr.extract_spaces(); + + expr.before = merge_spaces(arena, expr.before, expr_inner.before); + expr.after = merge_spaces(arena, expr_inner.after, expr.after); + expr.item = expr_inner.item; + } + + let mut pat = match expr.item { + Expr::Var { module_name, ident } => { + if module_name.is_empty() { + Pattern::Identifier(ident) + } else { + Pattern::QualifiedIdentifier { module_name, ident } + } + } + Expr::Underscore(opt_name) => Pattern::Underscore(opt_name), + Expr::Tag(value) => Pattern::Tag(value), + Expr::OpaqueRef(value) => Pattern::OpaqueRef(value), + Expr::Apply(loc_val, loc_args, _) => { + let region = loc_val.region; + let value = expr_to_pattern_help(arena, &loc_val.value)?; + let val_pattern = arena.alloc(Loc { region, value }); + + let mut arg_patterns = Vec::with_capacity_in(loc_args.len(), arena); + + for loc_arg in loc_args.iter() { + let region = loc_arg.region; + let value = expr_to_pattern_help(arena, &loc_arg.value)?; + + arg_patterns.push(Loc { region, value }); + } + + let pattern = Pattern::Apply(val_pattern, arg_patterns.into_bump_slice()); + + pattern + } + + Expr::SpaceBefore(..) + | Expr::SpaceAfter(..) + | Expr::ParensAround(..) + | Expr::RecordBuilder(..) => unreachable!(), + + Expr::Record(fields) => { + let patterns = fields.map_items_result(arena, |loc_assigned_field| { + let region = loc_assigned_field.region; + let value = assigned_expr_field_to_pattern_help(arena, &loc_assigned_field.value)?; + Ok(Loc { region, value }) + })?; + + Pattern::RecordDestructure(patterns) + } + + Expr::Tuple(fields) => Pattern::Tuple(fields.map_items_result(arena, |loc_expr| { + Ok(Loc { + region: loc_expr.region, + value: expr_to_pattern_help(arena, &loc_expr.value)?, + }) + })?), + + Expr::Float(string) => Pattern::FloatLiteral(string), + Expr::Num(string) => Pattern::NumLiteral(string), + Expr::NonBase10Int { + string, + base, + is_negative, + } => Pattern::NonBase10Literal { + string, + base, + is_negative, + }, + // These would not have parsed as patterns + Expr::IngestedFile(_, _) + | Expr::AccessorFunction(_) + | Expr::RecordAccess(_, _) + | Expr::TupleAccess(_, _) + | Expr::List { .. } + | Expr::Closure(_, _) + | Expr::Backpassing(_, _, _) + | Expr::BinOps { .. } + | Expr::Defs(_, _) + | Expr::If(_, _) + | Expr::When(_, _) + | Expr::Expect(_, _) + | Expr::Dbg(_, _) + | Expr::LowLevelDbg(_, _) + | Expr::MalformedClosure + | Expr::PrecedenceConflict { .. } + | Expr::MultipleRecordBuilders { .. } + | Expr::UnappliedRecordBuilder { .. } + | Expr::RecordUpdate { .. } + | Expr::UnaryOp(_, _) + | Expr::Crash => return Err(()), + + Expr::Str(string) => Pattern::StrLiteral(string), + Expr::SingleQuote(string) => Pattern::SingleQuote(string), + Expr::MalformedIdent(string, problem) => Pattern::MalformedIdent(string, problem), + }; + + // Now we re-add the spaces + + if !expr.before.is_empty() { + pat = Pattern::SpaceBefore(arena.alloc(pat), expr.before); + } + if !expr.after.is_empty() { + pat = Pattern::SpaceAfter(arena.alloc(pat), expr.after); + } + + Ok(pat) +} + +fn assigned_expr_field_to_pattern_help<'a>( + arena: &'a Bump, + assigned_field: &AssignedField<'a, Expr<'a>>, +) -> Result, ()> { + // the assigned fields always store spaces, but this slice is often empty + Ok(match assigned_field { + AssignedField::RequiredValue(name, spaces, value) => { + let pattern = expr_to_pattern_help(arena, &value.value)?; + let result = arena.alloc(Loc { + region: value.region, + value: pattern, + }); + if spaces.is_empty() { + Pattern::RequiredField(name.value, result) + } else { + Pattern::SpaceAfter( + arena.alloc(Pattern::RequiredField(name.value, result)), + spaces, + ) + } + } + AssignedField::OptionalValue(name, spaces, value) => { + let result = arena.alloc(Loc { + region: value.region, + value: value.value, + }); + if spaces.is_empty() { + Pattern::OptionalField(name.value, result) + } else { + Pattern::SpaceAfter( + arena.alloc(Pattern::OptionalField(name.value, result)), + spaces, + ) + } + } + AssignedField::LabelOnly(name) => Pattern::Identifier(name.value), + AssignedField::SpaceBefore(nested, spaces) => Pattern::SpaceBefore( + arena.alloc(assigned_expr_field_to_pattern_help(arena, nested)?), + spaces, + ), + AssignedField::SpaceAfter(nested, spaces) => Pattern::SpaceAfter( + arena.alloc(assigned_expr_field_to_pattern_help(arena, nested)?), + spaces, + ), + AssignedField::Malformed(string) => Pattern::Malformed(string), + }) +} + +pub fn toplevel_defs<'a>() -> impl Parser<'a, Defs<'a>, EExpr<'a>> { + move |arena, state: State<'a>, min_indent: u32| { + let (_, initial_space, state) = + space0_e(EExpr::IndentEnd).parse(arena, state, min_indent)?; + + let start_column = state.column(); + + let options = ExprParseOptions { + accept_multi_backpassing: true, + check_for_arrow: true, + }; + + let mut output = Defs::default(); + let before = Slice::extend_new(&mut output.spaces, initial_space.iter().copied()); + + let (_, mut output, state) = parse_defs_end(options, start_column, output, arena, state)?; + + let (_, final_space, state) = + space0_e(EExpr::IndentEnd).parse(arena, state, start_column)?; + + if !output.tags.is_empty() { + // add surrounding whitespace + let after = Slice::extend_new(&mut output.spaces, final_space.iter().copied()); + + debug_assert!(output.space_before[0].is_empty()); + output.space_before[0] = before; + + let last = output.tags.len() - 1; + debug_assert!(output.space_after[last].is_empty() || after.is_empty()); + output.space_after[last] = after; + } + + Ok((MadeProgress, output, state)) + } +} + +// PARSER HELPERS + +fn closure_help<'a>(options: ExprParseOptions) -> impl Parser<'a, Expr<'a>, EClosure<'a>> { + // closure_help_help(options) + map_with_arena!( + // After the first token, all other tokens must be indented past the start of the line + indented_seq!( + // All closures start with a '\' - e.g. (\x -> x + 1) + word1_indent(b'\\', EClosure::Start), + // Once we see the '\', we're committed to parsing this as a closure. + // It may turn out to be malformed, but it is definitely a closure. + and!( + // Parse the params + // Params are comma-separated + sep_by1_e( + word1(b',', EClosure::Comma), + space0_around_ee( + specialize(EClosure::Pattern, closure_param()), + EClosure::IndentArg, + EClosure::IndentArrow, + ), + EClosure::Arg, + ), + skip_first!( + // Parse the -> which separates params from body + word2(b'-', b'>', EClosure::Arrow), + // Parse the body + space0_before_e( + specialize_ref(EClosure::Body, expr_start(options)), + EClosure::IndentBody + ) + ) + ) + ), + |arena: &'a Bump, (params, body)| { + let params: Vec<'a, Loc>> = params; + let params: &'a [Loc>] = params.into_bump_slice(); + Expr::Closure(params, arena.alloc(body)) + } + ) +} + +mod when { + use super::*; + use crate::ast::WhenBranch; + + /// Parser for when expressions. + pub fn expr_help<'a>(options: ExprParseOptions) -> impl Parser<'a, Expr<'a>, EWhen<'a>> { + map_with_arena!( + and!( + indented_seq!( + parser::keyword_e(keyword::WHEN, EWhen::When), + space0_around_e_no_after_indent_check( + specialize_ref(EWhen::Condition, expr_start(options)), + EWhen::IndentCondition, + ) + ), + // Note that we allow the `is` to be at any indent level, since this doesn't introduce any + // ambiguity. The formatter will fix it up. + // + // We require that branches are indented relative to the line containing the `is`. + indented_seq!( + parser::keyword_e(keyword::IS, EWhen::Is), + branches(options) + ) + ), + move |arena: &'a Bump, (loc_condition, branches): (Loc>, Vec<'a, &'a WhenBranch<'a>>)| { + Expr::When(arena.alloc(loc_condition), branches.into_bump_slice()) + } + ) + } + + fn branches<'a>( + options: ExprParseOptions, + ) -> impl Parser<'a, Vec<'a, &'a WhenBranch<'a>>, EWhen<'a>> { + move |arena, state: State<'a>, min_indent: u32| { + let mut branches: Vec<'a, &'a WhenBranch<'a>> = Vec::with_capacity_in(2, arena); + + // 1. Parse the first branch and get its indentation level. (It must be >= min_indent.) + // 2. Parse the other branches. Their indentation levels must be == the first branch's. + + let (_, ((pattern_indent_level, loc_first_patterns), loc_first_guard), state): ( + _, + ((_, _), _), + State<'a>, + ) = branch_alternatives(options, None).parse(arena, state, min_indent)?; + + let original_indent = pattern_indent_level; + + // Parse the first "->" and the expression after it. + let (_, loc_first_expr, mut state) = + branch_result(original_indent + 1).parse(arena, state, original_indent + 1)?; + + // Record this as the first branch, then optionally parse additional branches. + branches.push(arena.alloc(WhenBranch { + patterns: loc_first_patterns.into_bump_slice(), + value: loc_first_expr, + guard: loc_first_guard, + })); + + let branch_parser = map!( + and!( + then( + branch_alternatives(options, Some(pattern_indent_level)), + move |_arena, state, _, ((indent_column, loc_patterns), loc_guard)| { + if pattern_indent_level == indent_column { + Ok((MadeProgress, (loc_patterns, loc_guard), state)) + } else { + let indent = pattern_indent_level - indent_column; + Err((MadeProgress, EWhen::PatternAlignment(indent, state.pos()))) + } + }, + ), + branch_result(original_indent + 1) + ), + |((patterns, guard), expr)| { + let patterns: Vec<'a, _> = patterns; + WhenBranch { + patterns: patterns.into_bump_slice(), + value: expr, + guard, + } + } + ); + + while !state.bytes().is_empty() { + match branch_parser.parse(arena, state.clone(), min_indent) { + Ok((_, next_output, next_state)) => { + state = next_state; + + branches.push(arena.alloc(next_output)); + } + Err((MadeProgress, problem)) => { + return Err((MadeProgress, problem)); + } + Err((NoProgress, _)) => { + break; + } + } + } + + Ok((MadeProgress, branches, state)) + } + } + + /// Parsing alternative patterns in `when` branches. + fn branch_alternatives<'a>( + options: ExprParseOptions, + pattern_indent_level: Option, + ) -> impl Parser<'a, ((u32, Vec<'a, Loc>>), Option>>), EWhen<'a>> { + let options = ExprParseOptions { + check_for_arrow: false, + ..options + }; + and!( + branch_alternatives_help(pattern_indent_level), + one_of![ + map!( + skip_first!( + parser::keyword_e(keyword::IF, EWhen::IfToken), + // TODO we should require space before the expression but not after + space0_around_ee( + specialize_ref( + EWhen::IfGuard, + increment_min_indent(expr_start(options)) + ), + EWhen::IndentIfGuard, + EWhen::IndentArrow, + ) + ), + Some + ), + |_, s, _| Ok((NoProgress, None, s)) + ] + ) + } + + fn branch_single_alternative<'a>() -> impl Parser<'a, Loc>, EWhen<'a>> { + move |arena, state, min_indent| { + let (_, spaces, state) = + backtrackable(space0_e(EWhen::IndentPattern)).parse(arena, state, min_indent)?; + + let (_, loc_pattern, state) = space0_after_e( + specialize(EWhen::Pattern, crate::pattern::loc_pattern_help()), + EWhen::IndentPattern, + ) + .parse(arena, state, min_indent)?; + + Ok(( + MadeProgress, + if spaces.is_empty() { + loc_pattern + } else { + arena + .alloc(loc_pattern.value) + .with_spaces_before(spaces, loc_pattern.region) + }, + state, + )) + } + } + + fn branch_alternatives_help<'a>( + pattern_indent_level: Option, + ) -> impl Parser<'a, (u32, Vec<'a, Loc>>), EWhen<'a>> { + move |arena, state: State<'a>, min_indent: u32| { + // put no restrictions on the indent after the spaces; we'll check it manually + match space0_e(EWhen::IndentPattern).parse(arena, state, 0) { + Err((MadeProgress, fail)) => Err((NoProgress, fail)), + Err((NoProgress, fail)) => Err((NoProgress, fail)), + Ok((_progress, spaces, state)) => { + match pattern_indent_level { + Some(wanted) if state.column() > wanted => { + // this branch is indented too much + Err((NoProgress, EWhen::IndentPattern(state.pos()))) + } + Some(wanted) if state.column() < wanted => { + let indent = wanted - state.column(); + Err((NoProgress, EWhen::PatternAlignment(indent, state.pos()))) + } + _ => { + let pattern_indent = + min_indent.max(pattern_indent_level.unwrap_or(min_indent)); + // the region is not reliable for the indent column in the case of + // parentheses around patterns + let pattern_indent_column = state.column(); + + let parser = + sep_by1(word1(b'|', EWhen::Bar), branch_single_alternative()); + + match parser.parse(arena, state.clone(), pattern_indent) { + Err((MadeProgress, fail)) => Err((MadeProgress, fail)), + Err((NoProgress, fail)) => { + // roll back space parsing if the pattern made no progress + Err((NoProgress, fail)) + } + + Ok((_, mut loc_patterns, state)) => { + // tag spaces onto the first parsed pattern + if !spaces.is_empty() { + if let Some(first) = loc_patterns.get_mut(0) { + *first = arena + .alloc(first.value) + .with_spaces_before(spaces, first.region); + } + } + + Ok((MadeProgress, (pattern_indent_column, loc_patterns), state)) + } + } + } + } + } + } + } + } + + /// Parsing the righthandside of a branch in a when conditional. + fn branch_result<'a>(indent: u32) -> impl Parser<'a, Loc>, EWhen<'a>> { + move |arena, state, _min_indent| { + skip_first!( + word2(b'-', b'>', EWhen::Arrow), + space0_before_e( + specialize_ref(EWhen::Branch, loc_expr(true)), + EWhen::IndentBranch, + ) + ) + .parse(arena, state, indent) + } + } +} + +fn if_branch<'a>() -> impl Parser<'a, (Loc>, Loc>), EIf<'a>> { + skip_second!( + and!( + skip_second!( + space0_around_ee( + specialize_ref(EIf::Condition, loc_expr(true)), + EIf::IndentCondition, + EIf::IndentThenToken, + ), + parser::keyword_e(keyword::THEN, EIf::Then) + ), + space0_around_ee( + specialize_ref(EIf::ThenBranch, loc_expr(true)), + EIf::IndentThenBranch, + EIf::IndentElseToken, + ) + ), + parser::keyword_e(keyword::ELSE, EIf::Else) + ) +} + +fn expect_help<'a>(options: ExprParseOptions) -> impl Parser<'a, Expr<'a>, EExpect<'a>> { + move |arena: &'a Bump, state: State<'a>, min_indent| { + let start_column = state.column(); + + let (_, _, state) = + parser::keyword_e(keyword::EXPECT, EExpect::Expect).parse(arena, state, min_indent)?; + + let (_, condition, state) = space0_before_e( + specialize_ref( + EExpect::Condition, + set_min_indent(start_column + 1, expr_start(options)), + ), + EExpect::IndentCondition, + ) + .parse(arena, state, start_column + 1) + .map_err(|(_, f)| (MadeProgress, f))?; + + let parse_cont = specialize_ref( + EExpect::Continuation, + space0_before_e(expr_start(options), EExpr::IndentEnd), + ); + + let (_, loc_cont, state) = parse_cont.parse(arena, state, min_indent)?; + + let expr = Expr::Expect(arena.alloc(condition), arena.alloc(loc_cont)); + + Ok((MadeProgress, expr, state)) + } +} + +fn dbg_help<'a>(options: ExprParseOptions) -> impl Parser<'a, Expr<'a>, EExpect<'a>> { + move |arena: &'a Bump, state: State<'a>, min_indent| { + let start_column = state.column(); + + let (_, _, state) = + parser::keyword_e(keyword::DBG, EExpect::Dbg).parse(arena, state, min_indent)?; + + let (_, condition, state) = space0_before_e( + specialize_ref( + EExpect::Condition, + set_min_indent(start_column + 1, expr_start(options)), + ), + EExpect::IndentCondition, + ) + .parse(arena, state, start_column + 1) + .map_err(|(_, f)| (MadeProgress, f))?; + + let parse_cont = specialize_ref( + EExpect::Continuation, + space0_before_e(expr_start(options), EExpr::IndentEnd), + ); + + let (_, loc_cont, state) = parse_cont.parse(arena, state, min_indent)?; + + let expr = Expr::Dbg(arena.alloc(condition), arena.alloc(loc_cont)); + + Ok((MadeProgress, expr, state)) + } +} + +fn if_expr_help<'a>(options: ExprParseOptions) -> impl Parser<'a, Expr<'a>, EIf<'a>> { + move |arena: &'a Bump, state, min_indent| { + let (_, _, state) = + parser::keyword_e(keyword::IF, EIf::If).parse(arena, state, min_indent)?; + + let mut branches = Vec::with_capacity_in(1, arena); + + let mut loop_state = state; + + let state_final_else = loop { + let (_, (cond, then_branch), state) = + if_branch().parse(arena, loop_state, min_indent)?; + + branches.push((cond, then_branch)); + + // try to parse another `if` + // NOTE this drops spaces between the `else` and the `if` + let optional_if = and!( + backtrackable(space0_e(EIf::IndentIf)), + parser::keyword_e(keyword::IF, EIf::If) + ); + + match optional_if.parse(arena, state.clone(), min_indent) { + Err((_, _)) => break state, + Ok((_, _, state)) => { + loop_state = state; + continue; + } + } + }; + + let (_, else_branch, state) = space0_before_e( + specialize_ref(EIf::ElseBranch, expr_start(options)), + EIf::IndentElseBranch, + ) + .parse(arena, state_final_else, min_indent) + .map_err(|(_, f)| (MadeProgress, f))?; + + let expr = Expr::If(branches.into_bump_slice(), arena.alloc(else_branch)); + + Ok((MadeProgress, expr, state)) + } +} + +/// This is a helper function for parsing function args. +/// The rules for (-) are special-cased, and they come up in function args. +/// +/// They work like this: +/// +/// x - y # "x minus y" +/// x-y # "x minus y" +/// x- y # "x minus y" (probably written in a rush) +/// x -y # "call x, passing (-y)" +/// +/// Since operators have higher precedence than function application, +/// any time we encounter a '-' it is unary iff it is both preceded by spaces +/// and is *not* followed by a whitespace character. + +/// When we parse an ident like `foo ` it could be any of these: +/// +/// 1. A standalone variable with trailing whitespace (e.g. because an operator is next) +/// 2. The beginning of a function call (e.g. `foo bar baz`) +/// 3. The beginning of a definition (e.g. `foo =`) +/// 4. The beginning of a type annotation (e.g. `foo :`) +/// 5. A reserved keyword (e.g. `if ` or `case `), meaning we should do something else. + +fn assign_or_destructure_identifier<'a>() -> impl Parser<'a, Ident<'a>, EExpr<'a>> { + crate::ident::parse_ident +} + +#[allow(dead_code)] +fn with_indent<'a, E, T, P>(parser: P) -> impl Parser<'a, u32, E> +where + P: Parser<'a, T, E>, + E: 'a, +{ + move |arena, state: State<'a>, min_indent: u32| { + let indent_column = state.column(); + + let (progress, _, state) = parser.parse(arena, state, min_indent)?; + + Ok((progress, indent_column, state)) + } +} + +fn ident_to_expr<'a>(arena: &'a Bump, src: Ident<'a>) -> Expr<'a> { + match src { + Ident::Tag(string) => Expr::Tag(string), + Ident::OpaqueRef(string) => Expr::OpaqueRef(string), + Ident::Access { module_name, parts } => { + let mut iter = parts.iter(); + + // The first value in the iterator is the variable name, + // e.g. `foo` in `foo.bar.baz` + let mut answer = match iter.next() { + Some(Accessor::RecordField(ident)) => Expr::Var { module_name, ident }, + Some(Accessor::TupleIndex(_)) => { + // TODO: make this state impossible to represent in Ident::Access, + // by splitting out parts[0] into a separate field with a type of `&'a str`, + // rather than a `&'a [Accessor<'a>]`. + internal_error!("Parsed an Ident::Access with a first part of a tuple index"); + } + None => { + internal_error!("Parsed an Ident::Access with no parts"); + } + }; + + // The remaining items in the iterator are record field accesses, + // e.g. `bar` in `foo.bar.baz`, followed by `baz` + for field in iter { + // Wrap the previous answer in the new one, so we end up + // with a nested Expr. That way, `foo.bar.baz` gets represented + // in the AST as if it had been written (foo.bar).baz all along. + match field { + Accessor::RecordField(field) => { + answer = Expr::RecordAccess(arena.alloc(answer), field); + } + Accessor::TupleIndex(index) => { + answer = Expr::TupleAccess(arena.alloc(answer), index); + } + } + } + + answer + } + Ident::AccessorFunction(string) => Expr::AccessorFunction(string), + Ident::Malformed(string, problem) => Expr::MalformedIdent(string, problem), + } +} + +fn list_literal_help<'a>() -> impl Parser<'a, Expr<'a>, EList<'a>> { + map_with_arena!( + collection_trailing_sep_e!( + word1(b'[', EList::Open), + specialize_ref(EList::Expr, loc_expr(false)), + word1(b',', EList::End), + word1(b']', EList::End), + Expr::SpaceBefore + ), + |arena, elements: Collection<'a, _>| { + let elements = elements.ptrify_items(arena); + Expr::List(elements) + } + ) + .trace("list_literal") +} + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum RecordField<'a> { + RequiredValue(Loc<&'a str>, &'a [CommentOrNewline<'a>], &'a Loc>), + OptionalValue(Loc<&'a str>, &'a [CommentOrNewline<'a>], &'a Loc>), + LabelOnly(Loc<&'a str>), + SpaceBefore(&'a RecordField<'a>, &'a [CommentOrNewline<'a>]), + SpaceAfter(&'a RecordField<'a>, &'a [CommentOrNewline<'a>]), + ApplyValue( + Loc<&'a str>, + &'a [CommentOrNewline<'a>], + &'a [CommentOrNewline<'a>], + &'a Loc>, + ), +} + +#[derive(Debug)] +pub struct FoundApplyValue; + +#[derive(Debug)] +struct FoundOptionalValue; + +impl<'a> RecordField<'a> { + fn is_apply_value(&self) -> bool { + let mut current = self; + + loop { + match current { + RecordField::ApplyValue(_, _, _, _) => break true, + RecordField::SpaceBefore(field, _) | RecordField::SpaceAfter(field, _) => { + current = *field; + } + _ => break false, + } + } + } + + pub fn to_assigned_field( + self, + arena: &'a Bump, + ) -> Result>, FoundApplyValue> { + use AssignedField::*; + + match self { + RecordField::RequiredValue(loc_label, spaces, loc_expr) => { + Ok(RequiredValue(loc_label, spaces, loc_expr)) + } + + RecordField::OptionalValue(loc_label, spaces, loc_expr) => { + Ok(OptionalValue(loc_label, spaces, loc_expr)) + } + + RecordField::LabelOnly(loc_label) => Ok(LabelOnly(loc_label)), + + RecordField::ApplyValue(_, _, _, _) => Err(FoundApplyValue), + + RecordField::SpaceBefore(field, spaces) => { + let assigned_field = field.to_assigned_field(arena)?; + + Ok(SpaceBefore(arena.alloc(assigned_field), spaces)) + } + + RecordField::SpaceAfter(field, spaces) => { + let assigned_field = field.to_assigned_field(arena)?; + + Ok(SpaceAfter(arena.alloc(assigned_field), spaces)) + } + } + } + + fn to_builder_field( + self, + arena: &'a Bump, + ) -> Result, FoundOptionalValue> { + use RecordBuilderField::*; + + match self { + RecordField::RequiredValue(loc_label, spaces, loc_expr) => { + Ok(Value(loc_label, spaces, loc_expr)) + } + + RecordField::OptionalValue(_, _, _) => Err(FoundOptionalValue), + + RecordField::LabelOnly(loc_label) => Ok(LabelOnly(loc_label)), + + RecordField::ApplyValue(loc_label, colon_spaces, arrow_spaces, loc_expr) => { + Ok(ApplyValue(loc_label, colon_spaces, arrow_spaces, loc_expr)) + } + + RecordField::SpaceBefore(field, spaces) => { + let builder_field = field.to_builder_field(arena)?; + + Ok(SpaceBefore(arena.alloc(builder_field), spaces)) + } + + RecordField::SpaceAfter(field, spaces) => { + let builder_field = field.to_builder_field(arena)?; + + Ok(SpaceAfter(arena.alloc(builder_field), spaces)) + } + } + } +} + +impl<'a> Spaceable<'a> for RecordField<'a> { + fn before(&'a self, spaces: &'a [CommentOrNewline<'a>]) -> Self { + RecordField::SpaceBefore(self, spaces) + } + fn after(&'a self, spaces: &'a [CommentOrNewline<'a>]) -> Self { + RecordField::SpaceAfter(self, spaces) + } +} + +pub fn record_field<'a>() -> impl Parser<'a, RecordField<'a>, ERecord<'a>> { + use RecordField::*; + + map_with_arena!( + and!( + specialize(|_, pos| ERecord::Field(pos), loc!(lowercase_ident())), + and!( + spaces(), + optional(either!( + and!(word1(b':', ERecord::Colon), record_field_expr()), + and!( + word1(b'?', ERecord::QuestionMark), + spaces_before(specialize_ref(ERecord::Expr, loc_expr(false))) + ) + )) + ) + ), + |arena: &'a bumpalo::Bump, (loc_label, (spaces, opt_loc_val))| { + match opt_loc_val { + Some(Either::First((_, RecordFieldExpr::Value(loc_val)))) => { + RequiredValue(loc_label, spaces, arena.alloc(loc_val)) + } + + Some(Either::First((_, RecordFieldExpr::Apply(arrow_spaces, loc_val)))) => { + ApplyValue(loc_label, spaces, arrow_spaces, arena.alloc(loc_val)) + } + + Some(Either::Second((_, loc_val))) => { + OptionalValue(loc_label, spaces, arena.alloc(loc_val)) + } + + // If no value was provided, record it as a Var. + // Canonicalize will know what to do with a Var later. + None => { + if !spaces.is_empty() { + SpaceAfter(arena.alloc(LabelOnly(loc_label)), spaces) + } else { + LabelOnly(loc_label) + } + } + } + } + ) +} + +enum RecordFieldExpr<'a> { + Apply(&'a [CommentOrNewline<'a>], Loc>), + Value(Loc>), +} + +fn record_field_expr<'a>() -> impl Parser<'a, RecordFieldExpr<'a>, ERecord<'a>> { + map_with_arena!( + and!( + spaces(), + either!( + and!( + word2(b'<', b'-', ERecord::Arrow), + spaces_before(specialize_ref(ERecord::Expr, loc_expr(false))) + ), + specialize_ref(ERecord::Expr, loc_expr(false)) + ) + ), + |arena: &'a bumpalo::Bump, (spaces, either)| { + match either { + Either::First((_, loc_expr)) => RecordFieldExpr::Apply(spaces, loc_expr), + Either::Second(loc_expr) => RecordFieldExpr::Value({ + if spaces.is_empty() { + loc_expr + } else { + arena + .alloc(loc_expr.value) + .with_spaces_before(spaces, loc_expr.region) + } + }), + } + } + ) +} + +fn record_updateable_identifier<'a>() -> impl Parser<'a, Expr<'a>, ERecord<'a>> { + specialize( + |_, pos| ERecord::Updateable(pos), + map_with_arena!(parse_ident, ident_to_expr), + ) +} + +struct RecordHelp<'a> { + update: Option>>, + fields: Collection<'a, Loc>>, +} + +fn record_help<'a>() -> impl Parser<'a, RecordHelp<'a>, ERecord<'a>> { + between!( + word1(b'{', ERecord::Open), + reset_min_indent(record!(RecordHelp { + // You can optionally have an identifier followed by an '&' to + // make this a record update, e.g. { Foo.user & username: "blah" }. + update: optional(backtrackable(skip_second!( + spaces_around( + // We wrap the ident in an Expr here, + // so that we have a Spaceable value to work with, + // and then in canonicalization verify that it's an Expr::Var + // (and not e.g. an `Expr::Access`) and extract its string. + loc!(record_updateable_identifier()), + ), + word1(b'&', ERecord::Ampersand) + ))), + fields: collection_inner!( + loc!(record_field()), + word1(b',', ERecord::End), + RecordField::SpaceBefore + ), + })), + word1(b'}', ERecord::End) + ) +} + +fn record_literal_help<'a>() -> impl Parser<'a, Expr<'a>, EExpr<'a>> { + then( + and!( + specialize(EExpr::Record, record_help()), + // there can be field access, e.g. `{ x : 4 }.x` + record_field_access_chain() + ), + move |arena, state, _, (record, accessors)| { + let expr_result = match record.update { + Some(update) => record_update_help(arena, update, record.fields), + None => { + let is_record_builder = record + .fields + .iter() + .any(|field| field.value.is_apply_value()); + + if is_record_builder { + record_builder_help(arena, record.fields) + } else { + let fields = record.fields.map_items(arena, |loc_field| { + loc_field.map(|field| field.to_assigned_field(arena).unwrap()) + }); + + Ok(Expr::Record(fields)) + } + } + }; + + match expr_result { + Ok(expr) => { + let value = apply_expr_access_chain(arena, expr, accessors); + + Ok((MadeProgress, value, state)) + } + Err(err) => Err((MadeProgress, err)), + } + }, + ) +} + +fn record_update_help<'a>( + arena: &'a Bump, + update: Loc>, + fields: Collection<'a, Loc>>, +) -> Result, EExpr<'a>> { + let result = fields.map_items_result(arena, |loc_field| { + match loc_field.value.to_assigned_field(arena) { + Ok(builder_field) => Ok(Loc { + region: loc_field.region, + value: builder_field, + }), + Err(FoundApplyValue) => Err(EExpr::RecordUpdateBuilder(loc_field.region)), + } + }); + + result.map(|fields| Expr::RecordUpdate { + update: &*arena.alloc(update), + fields, + }) +} + +fn record_builder_help<'a>( + arena: &'a Bump, + fields: Collection<'a, Loc>>, +) -> Result, EExpr<'a>> { + let result = fields.map_items_result(arena, |loc_field| { + match loc_field.value.to_builder_field(arena) { + Ok(builder_field) => Ok(Loc { + region: loc_field.region, + value: builder_field, + }), + Err(FoundOptionalValue) => Err(EExpr::OptionalValueInRecordBuilder(loc_field.region)), + } + }); + + result.map(Expr::RecordBuilder) +} + +fn apply_expr_access_chain<'a>( + arena: &'a Bump, + value: Expr<'a>, + accessors: Vec<'a, Accessor<'a>>, +) -> Expr<'a> { + accessors + .into_iter() + .fold(value, |value, accessor| match accessor { + Accessor::RecordField(field) => Expr::RecordAccess(arena.alloc(value), field), + Accessor::TupleIndex(field) => Expr::TupleAccess(arena.alloc(value), field), + }) +} + +fn string_like_literal_help<'a>() -> impl Parser<'a, Expr<'a>, EString<'a>> { + map_with_arena!( + crate::string_literal::parse_str_like_literal(), + |arena, lit| match lit { + StrLikeLiteral::Str(s) => Expr::Str(s), + StrLikeLiteral::SingleQuote(s) => { + // TODO: preserve the original escaping + Expr::SingleQuote(s.to_str_in(arena)) + } + } + ) +} + +fn positive_number_literal_help<'a>() -> impl Parser<'a, Expr<'a>, ENumber> { + map!( + crate::number_literal::positive_number_literal(), + |literal| { + use crate::number_literal::NumLiteral::*; + + match literal { + Num(s) => Expr::Num(s), + Float(s) => Expr::Float(s), + NonBase10Int { + string, + base, + is_negative, + } => Expr::NonBase10Int { + string, + base, + is_negative, + }, + } + } + ) +} + +fn number_literal_help<'a>() -> impl Parser<'a, Expr<'a>, ENumber> { + map!(crate::number_literal::number_literal(), |literal| { + use crate::number_literal::NumLiteral::*; + + match literal { + Num(s) => Expr::Num(s), + Float(s) => Expr::Float(s), + NonBase10Int { + string, + base, + is_negative, + } => Expr::NonBase10Int { + string, + base, + is_negative, + }, + } + }) +} + +const BINOP_CHAR_SET: &[u8] = b"+-/*=.<>:&|^?%!"; + +const BINOP_CHAR_MASK: [bool; 125] = { + let mut result = [false; 125]; + + let mut i = 0; + while i < BINOP_CHAR_SET.len() { + let index = BINOP_CHAR_SET[i] as usize; + + result[index] = true; + + i += 1; + } + + result +}; + +fn operator<'a>() -> impl Parser<'a, BinOp, EExpr<'a>> { + |_, state, _m| operator_help(EExpr::Start, EExpr::BadOperator, state) +} + +#[inline(always)] +fn operator_help<'a, F, G, E>( + to_expectation: F, + to_error: G, + mut state: State<'a>, +) -> ParseResult<'a, BinOp, E> +where + F: Fn(Position) -> E, + G: Fn(&'a str, Position) -> E, + E: 'a, +{ + let chomped = chomp_ops(state.bytes()); + + macro_rules! good { + ($op:expr, $width:expr) => {{ + state = state.advance($width); + + Ok((MadeProgress, $op, state)) + }}; + } + + macro_rules! bad_made_progress { + ($op:expr) => {{ + Err((MadeProgress, to_error($op, state.pos()))) + }}; + } + + match chomped { + "" => Err((NoProgress, to_expectation(state.pos()))), + "+" => good!(BinOp::Plus, 1), + "-" => good!(BinOp::Minus, 1), + "*" => good!(BinOp::Star, 1), + "/" => good!(BinOp::Slash, 1), + "%" => good!(BinOp::Percent, 1), + "^" => good!(BinOp::Caret, 1), + ">" => good!(BinOp::GreaterThan, 1), + "<" => good!(BinOp::LessThan, 1), + "." => { + // a `.` makes no progress, so it does not interfere with `.foo` access(or) + Err((NoProgress, to_error(".", state.pos()))) + } + "=" => good!(BinOp::Assignment, 1), + ":=" => good!(BinOp::IsOpaqueType, 2), + ":" => good!(BinOp::IsAliasType, 1), + "|>" => good!(BinOp::Pizza, 2), + "==" => good!(BinOp::Equals, 2), + "!=" => good!(BinOp::NotEquals, 2), + ">=" => good!(BinOp::GreaterThanOrEq, 2), + "<=" => good!(BinOp::LessThanOrEq, 2), + "&&" => good!(BinOp::And, 2), + "||" => good!(BinOp::Or, 2), + "//" => good!(BinOp::DoubleSlash, 2), + "->" => { + // makes no progress, so it does not interfere with `_ if isGood -> ...` + Err((NoProgress, to_error("->", state.pos()))) + } + "<-" => good!(BinOp::Backpassing, 2), + _ => bad_made_progress!(chomped), + } +} + +fn chomp_ops(bytes: &[u8]) -> &str { + let mut chomped = 0; + + for c in bytes.iter() { + if let Some(true) = BINOP_CHAR_MASK.get(*c as usize) { + chomped += 1; + } else { + break; + } + } + + unsafe { + // Safe because BINOP_CHAR_SET only contains ascii chars + std::str::from_utf8_unchecked(&bytes[..chomped]) + } +} diff --git a/crates/compiler/parse/src/header.rs b/crates/compiler/parse/src/header.rs new file mode 100644 index 0000000000..3425f46573 --- /dev/null +++ b/crates/compiler/parse/src/header.rs @@ -0,0 +1,409 @@ +use crate::ast::{ + Collection, CommentOrNewline, Malformed, Spaced, Spaces, StrLiteral, TypeAnnotation, +}; +use crate::blankspace::space0_e; +use crate::expr::merge_spaces; +use crate::ident::{lowercase_ident, UppercaseIdent}; +use crate::parser::{optional, then}; +use crate::parser::{specialize, word1, EPackageEntry, EPackageName, Parser}; +use crate::string_literal; +use roc_module::symbol::{ModuleId, Symbol}; +use roc_region::all::Loc; +use std::fmt::Debug; + +impl<'a> HeaderType<'a> { + pub fn exposed_or_provided_values(&'a self) -> &'a [Loc>] { + match self { + HeaderType::App { + provides: exposes, .. + } + | HeaderType::Hosted { exposes, .. } + | HeaderType::Builtin { exposes, .. } + | HeaderType::Interface { exposes, .. } => exposes, + HeaderType::Platform { .. } | HeaderType::Package { .. } => &[], + } + } +} + +#[derive(Debug)] +pub enum HeaderType<'a> { + App { + output_name: StrLiteral<'a>, + provides: &'a [Loc>], + to_platform: To<'a>, + }, + Hosted { + name: ModuleName<'a>, + exposes: &'a [Loc>], + generates: UppercaseIdent<'a>, + generates_with: &'a [Loc>], + }, + /// Only created during canonicalization, never actually parsed from source + Builtin { + name: ModuleName<'a>, + exposes: &'a [Loc>], + generates_with: &'a [Symbol], + }, + Package { + /// usually something other than `pf` + config_shorthand: &'a str, + exposes: &'a [Loc>], + exposes_ids: &'a [ModuleId], + }, + Platform { + opt_app_module_id: Option, + /// the name and type scheme of the main function (required by the platform) + /// (type scheme is currently unused) + provides: &'a [(Loc>, Loc>)], + requires: &'a [Loc>], + requires_types: &'a [Loc>], + exposes: &'a [Loc>], + exposes_ids: &'a [ModuleId], + + /// usually `pf` + config_shorthand: &'a str, + }, + Interface { + name: ModuleName<'a>, + exposes: &'a [Loc>], + }, +} + +#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash)] +pub enum Version<'a> { + Exact(&'a str), + Range { + min: &'a str, + min_comparison: VersionComparison, + max: &'a str, + max_comparison: VersionComparison, + }, +} + +#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash)] +pub enum VersionComparison { + AllowsEqual, + DisallowsEqual, +} + +#[derive(Copy, Clone, PartialEq, Eq, Debug)] +pub struct PackageName<'a>(&'a str); + +impl<'a> PackageName<'a> { + pub fn to_str(self) -> &'a str { + self.0 + } + + pub fn as_str(&self) -> &'a str { + self.0 + } +} + +impl<'a> From> for &'a str { + fn from(name: PackageName<'a>) -> &'a str { + name.0 + } +} + +impl<'a> From<&'a str> for PackageName<'a> { + fn from(string: &'a str) -> Self { + Self(string) + } +} + +#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash)] +pub struct ModuleName<'a>(&'a str); + +impl<'a> From> for &'a str { + fn from(name: ModuleName<'a>) -> Self { + name.0 + } +} + +impl<'a> ModuleName<'a> { + pub const fn new(name: &'a str) -> Self { + ModuleName(name) + } + + pub const fn as_str(&'a self) -> &'a str { + self.0 + } +} + +#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash)] +pub struct ExposedName<'a>(&'a str); + +impl<'a> From> for &'a str { + fn from(name: ExposedName<'a>) -> Self { + name.0 + } +} + +impl<'a> ExposedName<'a> { + pub const fn new(name: &'a str) -> Self { + ExposedName(name) + } + + pub fn as_str(&'a self) -> &'a str { + self.0 + } +} + +pub trait Keyword: Copy + Clone + Debug { + const KEYWORD: &'static str; +} + +macro_rules! keywords { + ($($name:ident => $string:expr),* $(,)?) => { + $( + #[derive(Copy, Clone, PartialEq, Eq, Debug)] + pub struct $name; + + impl Keyword for $name { + const KEYWORD: &'static str = $string; + } + )* + } +} + +keywords! { + ExposesKeyword => "exposes", + ImportsKeyword => "imports", + WithKeyword => "with", + GeneratesKeyword => "generates", + PackageKeyword => "package", + PackagesKeyword => "packages", + RequiresKeyword => "requires", + ProvidesKeyword => "provides", + ToKeyword => "to", +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct KeywordItem<'a, K, V> { + pub keyword: Spaces<'a, K>, + pub item: V, +} + +#[derive(Clone, Debug, PartialEq)] +pub struct InterfaceHeader<'a> { + pub before_name: &'a [CommentOrNewline<'a>], + pub name: Loc>, + + pub exposes: KeywordItem<'a, ExposesKeyword, Collection<'a, Loc>>>>, + pub imports: KeywordItem<'a, ImportsKeyword, Collection<'a, Loc>>>>, +} + +#[derive(Clone, Debug, PartialEq)] +pub struct HostedHeader<'a> { + pub before_name: &'a [CommentOrNewline<'a>], + pub name: Loc>, + pub exposes: KeywordItem<'a, ExposesKeyword, Collection<'a, Loc>>>>, + + pub imports: KeywordItem<'a, ImportsKeyword, Collection<'a, Loc>>>>, + + pub generates: KeywordItem<'a, GeneratesKeyword, UppercaseIdent<'a>>, + pub generates_with: + KeywordItem<'a, WithKeyword, Collection<'a, Loc>>>>, +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum To<'a> { + ExistingPackage(&'a str), + NewPackage(PackageName<'a>), +} + +#[derive(Clone, Debug, PartialEq)] +pub struct AppHeader<'a> { + pub before_name: &'a [CommentOrNewline<'a>], + pub name: Loc>, + + pub packages: + Option>>>>>, + pub imports: + Option>>>>>, + pub provides: ProvidesTo<'a>, +} + +#[derive(Clone, Debug, PartialEq)] +pub struct ProvidesTo<'a> { + pub provides_keyword: Spaces<'a, ProvidesKeyword>, + pub entries: Collection<'a, Loc>>>, + pub types: Option>>>>, + + pub to_keyword: Spaces<'a, ToKeyword>, + pub to: Loc>, +} + +#[derive(Clone, Debug, PartialEq)] +pub struct PackageHeader<'a> { + pub before_name: &'a [CommentOrNewline<'a>], + pub name: Loc>, + + pub exposes: KeywordItem<'a, ExposesKeyword, Collection<'a, Loc>>>>, + pub packages: + KeywordItem<'a, PackagesKeyword, Collection<'a, Loc>>>>, +} + +#[derive(Clone, Debug, PartialEq)] +pub struct PlatformRequires<'a> { + pub rigids: Collection<'a, Loc>>>, + pub signature: Loc>>, +} + +#[derive(Clone, Debug, PartialEq)] +pub struct PlatformHeader<'a> { + pub before_name: &'a [CommentOrNewline<'a>], + pub name: Loc>, + + pub requires: KeywordItem<'a, RequiresKeyword, PlatformRequires<'a>>, + pub exposes: KeywordItem<'a, ExposesKeyword, Collection<'a, Loc>>>>, + pub packages: + KeywordItem<'a, PackagesKeyword, Collection<'a, Loc>>>>, + pub imports: KeywordItem<'a, ImportsKeyword, Collection<'a, Loc>>>>, + pub provides: + KeywordItem<'a, ProvidesKeyword, Collection<'a, Loc>>>>, +} + +#[derive(Copy, Clone, Debug, PartialEq)] +pub enum ImportsEntry<'a> { + /// e.g. `Task` or `Task.{ Task, after }` + Module( + ModuleName<'a>, + Collection<'a, Loc>>>, + ), + + /// e.g. `pf.Task` or `pf.Task.{ after }` or `pf.{ Task.{ Task, after } }` + Package( + &'a str, + ModuleName<'a>, + Collection<'a, Loc>>>, + ), + + /// e.g "path/to/my/file.txt" as myFile : Str + IngestedFile(StrLiteral<'a>, Spaced<'a, TypedIdent<'a>>), +} + +/// e.g. +/// +/// printLine : Str -> Effect {} +#[derive(Copy, Clone, Debug, PartialEq)] +pub struct TypedIdent<'a> { + pub ident: Loc<&'a str>, + pub spaces_before_colon: &'a [CommentOrNewline<'a>], + pub ann: Loc>, +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct PackageEntry<'a> { + pub shorthand: &'a str, + pub spaces_after_shorthand: &'a [CommentOrNewline<'a>], + pub package_name: Loc>, +} + +pub fn package_entry<'a>() -> impl Parser<'a, Spaced<'a, PackageEntry<'a>>, EPackageEntry<'a>> { + map_with_arena!( + // You may optionally have a package shorthand, + // e.g. "uc" in `uc: roc/unicode 1.0.0` + // + // (Indirect dependencies don't have a shorthand.) + and!( + optional(and!( + skip_second!( + and!( + specialize(|_, pos| EPackageEntry::Shorthand(pos), lowercase_ident()), + space0_e(EPackageEntry::IndentPackage) + ), + word1(b':', EPackageEntry::Colon) + ), + space0_e(EPackageEntry::IndentPackage) + )), + loc!(specialize(EPackageEntry::BadPackage, package_name())) + ), + move |arena, (opt_shorthand, package_or_path)| { + let entry = match opt_shorthand { + Some(((shorthand, spaces_before_colon), spaces_after_colon)) => PackageEntry { + shorthand, + spaces_after_shorthand: merge_spaces( + arena, + spaces_before_colon, + spaces_after_colon, + ), + package_name: package_or_path, + }, + None => PackageEntry { + shorthand: "", + spaces_after_shorthand: &[], + package_name: package_or_path, + }, + }; + + Spaced::Item(entry) + } + ) +} + +pub fn package_name<'a>() -> impl Parser<'a, PackageName<'a>, EPackageName<'a>> { + then( + loc!(specialize( + EPackageName::BadPath, + string_literal::parse_str_literal() + )), + move |_arena, state, progress, text| match text.value { + StrLiteral::PlainLine(text) => Ok((progress, PackageName(text), state)), + StrLiteral::Line(_) => Err((progress, EPackageName::Escapes(text.region.start()))), + StrLiteral::Block(_) => Err((progress, EPackageName::Multiline(text.region.start()))), + }, + ) +} + +impl<'a, K, V> Malformed for KeywordItem<'a, K, V> +where + K: Malformed, + V: Malformed, +{ + fn is_malformed(&self) -> bool { + self.keyword.is_malformed() || self.item.is_malformed() + } +} + +impl<'a> Malformed for InterfaceHeader<'a> { + fn is_malformed(&self) -> bool { + false + } +} + +impl<'a> Malformed for HostedHeader<'a> { + fn is_malformed(&self) -> bool { + false + } +} + +impl<'a> Malformed for AppHeader<'a> { + fn is_malformed(&self) -> bool { + self.name.is_malformed() + } +} + +impl<'a> Malformed for PackageHeader<'a> { + fn is_malformed(&self) -> bool { + false + } +} + +impl<'a> Malformed for PlatformRequires<'a> { + fn is_malformed(&self) -> bool { + self.signature.is_malformed() + } +} + +impl<'a> Malformed for PlatformHeader<'a> { + fn is_malformed(&self) -> bool { + false + } +} + +impl<'a> Malformed for TypedIdent<'a> { + fn is_malformed(&self) -> bool { + self.ann.is_malformed() + } +} diff --git a/crates/compiler/parse/src/highlight.rs b/crates/compiler/parse/src/highlight.rs new file mode 100644 index 0000000000..2b561d1446 --- /dev/null +++ b/crates/compiler/parse/src/highlight.rs @@ -0,0 +1,661 @@ +use encode_unicode::CharExt; +use std::collections::HashSet; + +use bumpalo::Bump; +use roc_region::all::{Loc, Region}; + +use crate::{ + ast::CommentOrNewline, + blankspace::loc_spaces, + keyword::KEYWORDS, + number_literal::positive_number_literal, + parser::{EExpr, ParseResult, Parser}, + state::State, + string_literal::{parse_str_like_literal, StrLikeLiteral}, +}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum Token { + LineComment, + DocComment, + Error, + SingleQuote, + String, + UnicodeEscape, + EscapedChar, + Interpolated, + Keyword, + UpperIdent, + LowerIdent, + Number, + QuestionMark, + Percent, + Caret, + Other, + Minus, + Bang, + BangEquals, + Plus, + Colon, + ColonEquals, + Bar, + DoubleBar, + And, + DoubleAnd, + Equals, + DoubleEquals, + GreaterThan, + GreaterThanEquals, + LessThan, + LessThanEquals, + Comma, + Backslash, + Slash, + DoubleSlash, + Pizza, + Brace, + Bracket, + AtSign, + Paren, + Arrow, + Pipe, + Backpass, + Decimal, + Multiply, + Underscore, +} + +pub fn highlight(text: &str) -> Vec> { + let mut tokens = Vec::new(); + let state = State::new(text.as_bytes()); + + let arena = Bump::new(); + + let header_keywords = HEADER_KEYWORDS.iter().copied().collect::>(); + let body_keywords = KEYWORDS.iter().copied().collect::>(); + + if let Ok((_prog, _, new_state)) = crate::module::header().parse(&arena, state.clone(), 0) { + let inner_state = + State::new(text[..state.bytes().len() - new_state.bytes().len()].as_bytes()); + highlight_inner(&arena, inner_state, &mut tokens, &header_keywords); + highlight_inner(&arena, new_state, &mut tokens, &body_keywords); + } else { + highlight_inner(&arena, state, &mut tokens, &body_keywords); + } + + tokens +} + +fn highlight_inner<'a>( + arena: &'a Bump, + mut state: State<'a>, + tokens: &mut Vec>, + keywords: &HashSet<&str>, +) { + loop { + let start = state.pos(); + if let Ok((b, _width)) = char::from_utf8_slice_start(state.bytes()) { + match b { + ' ' | '\n' | '\t' | '\r' | '#' => { + let res: ParseResult<'a, _, EExpr<'a>> = + loc_spaces().parse(arena, state.clone(), 0); + if let Ok((_, spaces, new_state)) = res { + state = new_state; + for space in spaces { + let token = match space.value { + CommentOrNewline::Newline => { + continue; + } + CommentOrNewline::LineComment(_) => Token::LineComment, + CommentOrNewline::DocComment(_) => Token::DocComment, + }; + tokens.push(Loc::at(space.region, token)); + } + } else { + fast_forward_to(&mut state, tokens, start, |c| c == b'\n'); + } + } + '"' | '\'' => { + if let Ok((_, item, new_state)) = + parse_str_like_literal().parse(arena, state.clone(), 0) + { + state = new_state; + match item { + StrLikeLiteral::SingleQuote(_) => { + tokens.push(Loc::at( + Region::between(start, state.pos()), + Token::SingleQuote, + )); + } + StrLikeLiteral::Str(_) => { + tokens.push(Loc::at( + Region::between(start, state.pos()), + Token::String, + )); + } + } + } else { + fast_forward_to(&mut state, tokens, start, |c| c == b'\n'); + } + } + c if c.is_alphabetic() => { + let buffer = state.bytes(); + let mut chomped = 0; + + let is_upper = c.is_uppercase(); + + while let Ok((ch, width)) = char::from_utf8_slice_start(&buffer[chomped..]) { + if ch.is_alphabetic() || ch.is_ascii_digit() { + chomped += width; + } else { + // we're done + break; + } + } + + let ident = std::str::from_utf8(&buffer[..chomped]).unwrap(); + state.advance_mut(chomped); + + if keywords.contains(ident) { + tokens.push(Loc::at(Region::between(start, state.pos()), Token::Keyword)); + } else { + tokens.push(Loc::at( + Region::between(start, state.pos()), + if is_upper { + Token::UpperIdent + } else { + Token::LowerIdent + }, + )); + } + } + '.' => { + state.advance_mut(1); + tokens.push(Loc::at(Region::between(start, state.pos()), Token::Decimal)); + } + '0'..='9' => { + if let Ok((_, _item, new_state)) = + positive_number_literal().parse(arena, state.clone(), 0) + { + state = new_state; + tokens.push(Loc::at(Region::between(start, state.pos()), Token::Number)); + } else { + fast_forward_to(&mut state, tokens, start, |b| !b.is_ascii_digit()); + } + } + ':' => { + state.advance_mut(1); + let tok = if state.bytes().first() == Some(&b'=') { + state.advance_mut(1); + Token::ColonEquals + } else { + Token::Colon + }; + tokens.push(Loc::at(Region::between(start, state.pos()), tok)); + } + '|' => { + state.advance_mut(1); + let tok = if state.bytes().first() == Some(&b'>') { + state.advance_mut(1); + Token::Pizza + } else if state.bytes().first() == Some(&b'|') { + state.advance_mut(1); + Token::DoubleBar + } else { + Token::Bar + }; + tokens.push(Loc::at(Region::between(start, state.pos()), tok)); + } + '&' => { + state.advance_mut(1); + let tok = if state.bytes().first() == Some(&b'&') { + state.advance_mut(1); + Token::DoubleAnd + } else { + Token::And + }; + tokens.push(Loc::at(Region::between(start, state.pos()), tok)); + } + '-' => { + state.advance_mut(1); + let tok = if state.bytes().first() == Some(&b'>') { + state.advance_mut(1); + Token::Arrow + } else { + Token::Minus + }; + tokens.push(Loc::at(Region::between(start, state.pos()), tok)); + } + '+' => { + state.advance_mut(1); + tokens.push(Loc::at(Region::between(start, state.pos()), Token::Plus)); + } + '=' => { + state.advance_mut(1); + let tok = if state.bytes().first() == Some(&b'=') { + state.advance_mut(1); + Token::DoubleEquals + } else { + Token::Equals + }; + tokens.push(Loc::at(Region::between(start, state.pos()), tok)); + } + '>' => { + state.advance_mut(1); + let tok = if state.bytes().first() == Some(&b'=') { + state.advance_mut(1); + Token::GreaterThanEquals + } else { + Token::GreaterThan + }; + tokens.push(Loc::at(Region::between(start, state.pos()), tok)); + } + '<' => { + state.advance_mut(1); + let tok = if state.bytes().first() == Some(&b'=') { + state.advance_mut(1); + Token::LessThanEquals + } else if state.bytes().first() == Some(&b'-') { + state.advance_mut(1); + Token::Backpass + } else { + Token::LessThan + }; + tokens.push(Loc::at(Region::between(start, state.pos()), tok)); + } + '!' => { + state.advance_mut(1); + let tok = if state.bytes().first() == Some(&b'=') { + state.advance_mut(1); + Token::BangEquals + } else { + Token::Bang + }; + tokens.push(Loc::at(Region::between(start, state.pos()), tok)); + } + ',' => { + state.advance_mut(1); + tokens.push(Loc::at(Region::between(start, state.pos()), Token::Comma)); + } + '_' => { + state.advance_mut(1); + tokens.push(Loc::at( + Region::between(start, state.pos()), + Token::Underscore, + )); + } + '?' => { + state.advance_mut(1); + tokens.push(Loc::at( + Region::between(start, state.pos()), + Token::QuestionMark, + )); + } + '%' => { + state.advance_mut(1); + tokens.push(Loc::at(Region::between(start, state.pos()), Token::Percent)); + } + '*' => { + state.advance_mut(1); + tokens.push(Loc::at( + Region::between(start, state.pos()), + Token::Multiply, + )); + } + '^' => { + state.advance_mut(1); + tokens.push(Loc::at(Region::between(start, state.pos()), Token::Caret)); + } + '\\' => { + state.advance_mut(1); + tokens.push(Loc::at( + Region::between(start, state.pos()), + Token::Backslash, + )); + } + '/' => { + state.advance_mut(1); + let tok = if state.bytes().first() == Some(&b'/') { + state.advance_mut(1); + Token::DoubleSlash + } else { + Token::Slash + }; + tokens.push(Loc::at(Region::between(start, state.pos()), tok)); + } + '@' => { + state.advance_mut(1); + tokens.push(Loc::at(Region::between(start, state.pos()), Token::AtSign)); + } + '{' | '}' => { + state.advance_mut(1); + tokens.push(Loc::at(Region::between(start, state.pos()), Token::Brace)); + } + '[' | ']' => { + state.advance_mut(1); + tokens.push(Loc::at(Region::between(start, state.pos()), Token::Bracket)); + } + '(' | ')' => { + state.advance_mut(1); + tokens.push(Loc::at(Region::between(start, state.pos()), Token::Paren)); + } + _ => { + state.advance_mut(1); + tokens.push(Loc::at(Region::between(start, state.pos()), Token::Other)); + } + } + } else { + break; + } + } +} + +fn fast_forward_to( + state: &mut State, + tokens: &mut Vec>, + start: roc_region::all::Position, + end: impl Fn(u8) -> bool, +) { + while let Some(b) = state.bytes().first() { + if end(*b) { + break; + } + state.advance_mut(1); + } + tokens.push(Loc::at(Region::between(start, state.pos()), Token::Error)); +} + +pub const HEADER_KEYWORDS: [&str; 14] = [ + "interface", + "app", + "package", + "platform", + "hosted", + "exposes", + "imports", + "with", + "generates", + "package", + "packages", + "requires", + "provides", + "to", +]; + +#[cfg(test)] +mod tests { + use roc_region::all::Position; + + use super::*; + + #[test] + fn test_highlight_comments() { + let text = "# a\n#b\n#c"; + let tokens = highlight(text); + assert_eq!( + tokens, + vec![ + Loc::at( + Region::between(Position::new(0), Position::new(3)), + Token::LineComment + ), + Loc::at( + Region::between(Position::new(4), Position::new(6)), + Token::LineComment + ), + Loc::at( + Region::between(Position::new(7), Position::new(9)), + Token::LineComment + ), + ] + ); + } + + #[test] + fn test_highlight_doc_comments() { + let text = "## a\n##b\n##c"; + let tokens = highlight(text); + assert_eq!( + tokens, + vec![ + Loc::at( + Region::between(Position::new(0), Position::new(4)), + Token::DocComment + ), + // the next two are line comments because there's not a space at the beginning + Loc::at( + Region::between(Position::new(5), Position::new(8)), + Token::LineComment + ), + Loc::at( + Region::between(Position::new(9), Position::new(12)), + Token::LineComment + ), + ] + ); + } + + #[test] + fn test_highlight_strings() { + let text = r#""a""#; + let tokens = highlight(text); + assert_eq!( + tokens, + vec![Loc::at( + Region::between(Position::new(0), Position::new(3)), + Token::String + )] + ); + } + + #[test] + fn test_highlight_single_quotes() { + let text = r#"'a'"#; + let tokens = highlight(text); + assert_eq!( + tokens, + vec![Loc::at( + Region::between(Position::new(0), Position::new(3)), + Token::SingleQuote + )] + ); + } + + #[test] + fn test_highlight_header() { + let text = r#"app "test-app" provides [] to "./blah""#; + let tokens = highlight(text); + assert_eq!( + tokens, + vec![ + Loc::at( + Region::between(Position::new(0), Position::new(3)), + Token::Keyword + ), + Loc::at( + Region::between(Position::new(4), Position::new(14)), + Token::String + ), + Loc::at( + Region::between(Position::new(15), Position::new(23)), + Token::Keyword + ), + Loc::at( + Region::between(Position::new(24), Position::new(25)), + Token::Bracket + ), + Loc::at( + Region::between(Position::new(25), Position::new(26)), + Token::Bracket + ), + Loc::at( + Region::between(Position::new(27), Position::new(29)), + Token::Keyword + ), + Loc::at( + Region::between(Position::new(30), Position::new(38)), + Token::String + ), + ] + ); + } + + #[test] + fn test_highlight_numbers() { + let text = "123.0 123 123. 123.0e10 123e10 123e-10 0x123"; + let tokens = highlight(text); + assert_eq!( + tokens, + vec![ + Loc::at( + Region::between(Position::new(0), Position::new(5)), + Token::Number + ), + Loc::at( + Region::between(Position::new(6), Position::new(9)), + Token::Number + ), + Loc::at( + Region::between(Position::new(10), Position::new(14)), + Token::Number + ), + Loc::at( + Region::between(Position::new(15), Position::new(23)), + Token::Number + ), + Loc::at( + Region::between(Position::new(24), Position::new(30)), + Token::Number + ), + Loc::at( + Region::between(Position::new(31), Position::new(38)), + Token::Number + ), + Loc::at( + Region::between(Position::new(39), Position::new(44)), + Token::Number + ), + ] + ); + } + + #[test] + fn test_combine_tokens() { + let text = "-> := <- |> || >= <= =="; + let actual = highlight(text); + + let expected = vec![ + Loc::at( + Region::between(Position::new(0), Position::new(2)), + Token::Arrow, + ), + Loc::at( + Region::between(Position::new(3), Position::new(5)), + Token::ColonEquals, + ), + Loc::at( + Region::between(Position::new(6), Position::new(8)), + Token::Backpass, + ), + Loc::at( + Region::between(Position::new(9), Position::new(11)), + Token::Pizza, + ), + Loc::at( + Region::between(Position::new(12), Position::new(14)), + Token::DoubleBar, + ), + Loc::at( + Region::between(Position::new(15), Position::new(17)), + Token::GreaterThanEquals, + ), + Loc::at( + Region::between(Position::new(18), Position::new(20)), + Token::LessThanEquals, + ), + Loc::at( + Region::between(Position::new(21), Position::new(23)), + Token::DoubleEquals, + ), + ]; + + assert_eq!(actual, expected); + } + + #[test] + fn test_highlight_pattern_matching() { + let text = "Green | Yellow -> \"not red\""; + let tokens = highlight(text); + assert_eq!( + tokens, + vec![ + Loc::at( + Region::between(Position::new(0), Position::new(5)), + Token::UpperIdent + ), + Loc::at( + Region::between(Position::new(6), Position::new(7)), + Token::Bar + ), + Loc::at( + Region::between(Position::new(8), Position::new(14)), + Token::UpperIdent + ), + Loc::at( + Region::between(Position::new(15), Position::new(17)), + Token::Arrow + ), + Loc::at( + Region::between(Position::new(18), Position::new(27)), + Token::String + ), + ] + ) + } + + #[test] + fn test_highlight_question_mark() { + let text = "title? Str"; + let tokens = highlight(text); + assert_eq!( + tokens, + vec![ + Loc::at( + Region::between(Position::new(0), Position::new(5)), + Token::LowerIdent + ), + Loc::at( + Region::between(Position::new(5), Position::new(6)), + Token::QuestionMark + ), + Loc::at( + Region::between(Position::new(7), Position::new(10)), + Token::UpperIdent + ), + ] + ) + } + + #[test] + fn test_highlight_slash() { + let text = "first / second"; + let tokens = highlight(text); + assert_eq!( + tokens, + vec![ + Loc::at( + Region::between(Position::new(0), Position::new(5)), + Token::LowerIdent + ), + Loc::at( + Region::between(Position::new(6), Position::new(7)), + Token::Slash + ), + Loc::at( + Region::between(Position::new(8), Position::new(14)), + Token::LowerIdent + ), + ] + ) + } +} diff --git a/crates/compiler/parse/src/ident.rs b/crates/compiler/parse/src/ident.rs new file mode 100644 index 0000000000..efd4758e26 --- /dev/null +++ b/crates/compiler/parse/src/ident.rs @@ -0,0 +1,662 @@ +use crate::parser::Progress::{self, *}; +use crate::parser::{BadInputError, EExpr, ParseResult, Parser}; +use crate::state::State; +use bumpalo::collections::vec::Vec; +use bumpalo::Bump; +use roc_region::all::{Position, Region}; + +/// A tag, for example. Must start with an uppercase letter +/// and then contain only letters and numbers afterwards - no dots allowed! +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub struct UppercaseIdent<'a>(&'a str); + +impl<'a> From<&'a str> for UppercaseIdent<'a> { + fn from(string: &'a str) -> Self { + UppercaseIdent(string) + } +} + +impl<'a> From> for &'a str { + fn from(ident: UppercaseIdent<'a>) -> Self { + ident.0 + } +} + +impl<'a> From<&'a UppercaseIdent<'a>> for &'a str { + fn from(ident: &'a UppercaseIdent<'a>) -> Self { + ident.0 + } +} + +/// The parser accepts all of these in any position where any one of them could +/// appear. This way, canonicalization can give more helpful error messages like +/// "you can't redefine this tag!" if you wrote `Foo = ...` or +/// "you can only define unqualified constants" if you wrote `Foo.bar = ...` +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Ident<'a> { + /// Foo or Bar + Tag(&'a str), + /// @Foo or @Bar + OpaqueRef(&'a str), + /// foo or foo.bar or Foo.Bar.baz.qux + Access { + module_name: &'a str, + parts: &'a [Accessor<'a>], + }, + /// `.foo { foo: 42 }` or `.1 (1, 2, 3)` + AccessorFunction(Accessor<'a>), + /// .Foo or foo. or something like foo.Bar + Malformed(&'a str, BadIdent), +} + +impl<'a> Ident<'a> { + pub fn len(&self) -> usize { + use self::Ident::*; + + match self { + Tag(string) | OpaqueRef(string) => string.len(), + Access { module_name, parts } => { + let mut len = if module_name.is_empty() { + 0 + } else { + module_name.len() + 1 + // +1 for the dot + }; + + for part in parts.iter() { + len += part.len() + 1 // +1 for the dot + } + + len - 1 + } + AccessorFunction(string) => string.len(), + Malformed(string, _) => string.len(), + } + } + + pub fn is_empty(&self) -> bool { + self.len() == 0 + } +} + +/// This could be: +/// +/// * A record field, e.g. "email" in `.email` or in `email:` +/// * A named pattern match, e.g. "foo" in `foo =` or `foo ->` or `\foo ->` +pub fn lowercase_ident<'a>() -> impl Parser<'a, &'a str, ()> { + move |_, state: State<'a>, _min_indent: u32| match chomp_lowercase_part(state.bytes()) { + Err(progress) => Err((progress, ())), + Ok(ident) => { + if crate::keyword::KEYWORDS.iter().any(|kw| &ident == kw) { + Err((NoProgress, ())) + } else { + let width = ident.len(); + Ok((MadeProgress, ident, state.advance(width))) + } + } + } +} + +/// This is a tuple accessor, e.g. "1" in `.1` +pub fn integer_ident<'a>() -> impl Parser<'a, &'a str, ()> { + move |_, state: State<'a>, _min_indent: u32| match chomp_integer_part(state.bytes()) { + Err(progress) => Err((progress, ())), + Ok(ident) => { + let width = ident.len(); + Ok((MadeProgress, ident, state.advance(width))) + } + } +} + +/// Like `lowercase_ident`, but returns an error with MadeProgress if the +/// identifier is a keyword. +pub fn lowercase_ident_keyword_e<'a>() -> impl Parser<'a, &'a str, ()> { + move |_, state: State<'a>, _min_indent: u32| match chomp_lowercase_part(state.bytes()) { + Err(progress) => Err((progress, ())), + Ok(ident) => { + if crate::keyword::KEYWORDS.iter().any(|kw| &ident == kw) { + Err((MadeProgress, ())) + } else { + let width = ident.len(); + Ok((MadeProgress, ident, state.advance(width))) + } + } + } +} + +pub fn tag_name<'a>() -> impl Parser<'a, &'a str, ()> { + move |arena, state: State<'a>, min_indent: u32| { + uppercase_ident().parse(arena, state, min_indent) + } +} + +/// This could be: +/// +/// * A module name +/// * A type name +/// * A tag +pub fn uppercase<'a>() -> impl Parser<'a, UppercaseIdent<'a>, ()> { + move |_, state: State<'a>, _min_indent: u32| match chomp_uppercase_part(state.bytes()) { + Err(progress) => Err((progress, ())), + Ok(ident) => { + let width = ident.len(); + Ok((MadeProgress, ident.into(), state.advance(width))) + } + } +} + +/// This could be: +/// +/// * A module name +/// * A type name +/// * A tag +pub fn uppercase_ident<'a>() -> impl Parser<'a, &'a str, ()> { + move |_, state: State<'a>, _min_indent: u32| match chomp_uppercase_part(state.bytes()) { + Err(progress) => Err((progress, ())), + Ok(ident) => { + let width = ident.len(); + Ok((MadeProgress, ident, state.advance(width))) + } + } +} + +pub fn unqualified_ident<'a>() -> impl Parser<'a, &'a str, ()> { + move |_, state: State<'a>, _min_indent: u32| match chomp_anycase_part(state.bytes()) { + Err(progress) => Err((progress, ())), + Ok(ident) => { + if crate::keyword::KEYWORDS.iter().any(|kw| &ident == kw) { + Err((MadeProgress, ())) + } else { + let width = ident.len(); + Ok((MadeProgress, ident, state.advance(width))) + } + } + } +} + +macro_rules! advance_state { + ($state:expr, $n:expr) => { + Ok($state.advance($n)) + }; +} + +pub fn parse_ident<'a>( + arena: &'a Bump, + state: State<'a>, + _min_indent: u32, +) -> ParseResult<'a, Ident<'a>, EExpr<'a>> { + let initial = state.clone(); + + match chomp_identifier_chain(arena, state.bytes(), state.pos()) { + Ok((width, ident)) => { + let state = advance_state!(state, width as usize)?; + if let Ident::Access { module_name, parts } = ident { + if module_name.is_empty() { + if let Some(first) = parts.first() { + for keyword in crate::keyword::KEYWORDS.iter() { + if first == &Accessor::RecordField(keyword) { + return Err((NoProgress, EExpr::Start(initial.pos()))); + } + } + } + } + } + + Ok((MadeProgress, ident, state)) + } + Err((0, _)) => Err((NoProgress, EExpr::Start(state.pos()))), + Err((width, fail)) => match fail { + BadIdent::Start(pos) => Err((NoProgress, EExpr::Start(pos))), + BadIdent::Space(e, pos) => Err((NoProgress, EExpr::Space(e, pos))), + _ => malformed_identifier( + initial.bytes(), + fail, + advance_state!(state, width as usize)?, + ), + }, + } +} + +fn malformed_identifier<'a>( + initial_bytes: &'a [u8], + problem: BadIdent, + mut state: State<'a>, +) -> ParseResult<'a, Ident<'a>, EExpr<'a>> { + let chomped = chomp_malformed(state.bytes()); + let delta = initial_bytes.len() - state.bytes().len(); + let parsed_str = unsafe { std::str::from_utf8_unchecked(&initial_bytes[..chomped + delta]) }; + + state = state.advance(chomped); + + Ok((MadeProgress, Ident::Malformed(parsed_str, problem), state)) +} + +/// skip forward to the next non-identifier character +pub fn chomp_malformed(bytes: &[u8]) -> usize { + use encode_unicode::CharExt; + let mut chomped = 0; + while let Ok((ch, width)) = char::from_utf8_slice_start(&bytes[chomped..]) { + // We can't use ch.is_alphanumeric() here because that passes for + // things that are "numeric" but not ASCII digits, like `¾` + if ch == '.' || ch == '_' || ch.is_alphabetic() || ch.is_ascii_digit() { + chomped += width; + continue; + } else { + break; + } + } + + chomped +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum BadIdent { + Start(Position), + Space(BadInputError, Position), + + UnderscoreAlone(Position), + UnderscoreInMiddle(Position), + UnderscoreAtStart { + position: Position, + /// If this variable was already declared in a pattern (e.g. \_x -> _x), + /// then this is where it was declared. + declaration_region: Option, + }, + QualifiedTag(Position), + WeirdAccessor(Position), + WeirdDotAccess(Position), + WeirdDotQualified(Position), + StrayDot(Position), + BadOpaqueRef(Position), + QualifiedTupleAccessor(Position), +} + +fn is_alnum(ch: char) -> bool { + ch.is_alphabetic() || ch.is_ascii_digit() +} + +fn chomp_lowercase_part(buffer: &[u8]) -> Result<&str, Progress> { + chomp_part(char::is_lowercase, is_alnum, buffer) +} + +fn chomp_uppercase_part(buffer: &[u8]) -> Result<&str, Progress> { + chomp_part(char::is_uppercase, is_alnum, buffer) +} + +fn chomp_anycase_part(buffer: &[u8]) -> Result<&str, Progress> { + chomp_part(char::is_alphabetic, is_alnum, buffer) +} + +fn chomp_integer_part(buffer: &[u8]) -> Result<&str, Progress> { + chomp_part( + |ch| char::is_ascii_digit(&ch), + |ch| char::is_ascii_digit(&ch), + buffer, + ) +} + +fn is_plausible_ident_continue(ch: char) -> bool { + ch == '_' || is_alnum(ch) +} + +#[inline(always)] +fn chomp_part(leading_is_good: F, rest_is_good: G, buffer: &[u8]) -> Result<&str, Progress> +where + F: Fn(char) -> bool, + G: Fn(char) -> bool, +{ + use encode_unicode::CharExt; + + let mut chomped = 0; + + if let Ok((ch, width)) = char::from_utf8_slice_start(&buffer[chomped..]) { + if leading_is_good(ch) { + chomped += width; + } else { + return Err(NoProgress); + } + } + + while let Ok((ch, width)) = char::from_utf8_slice_start(&buffer[chomped..]) { + if rest_is_good(ch) { + chomped += width; + } else { + // we're done + break; + } + } + + if let Ok((next, _width)) = char::from_utf8_slice_start(&buffer[chomped..]) { + // This would mean we have e.g.: + // * identifier followed by a _ + // * an integer followed by an alphabetic char + if is_plausible_ident_continue(next) { + return Err(NoProgress); + } + } + + if chomped == 0 { + Err(NoProgress) + } else { + let name = unsafe { std::str::from_utf8_unchecked(&buffer[..chomped]) }; + + Ok(name) + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum Accessor<'a> { + RecordField(&'a str), + TupleIndex(&'a str), +} + +impl<'a> Accessor<'a> { + pub fn len(&self) -> usize { + match self { + Accessor::RecordField(name) => name.len(), + Accessor::TupleIndex(name) => name.len(), + } + } + + pub fn is_empty(&self) -> bool { + self.len() > 0 + } + + pub fn as_inner(&self) -> &'a str { + match self { + Accessor::RecordField(name) => name, + Accessor::TupleIndex(name) => name, + } + } +} + +/// a `.foo` or `.1` accessor function +fn chomp_accessor(buffer: &[u8], pos: Position) -> Result { + // assumes the leading `.` has been chomped already + use encode_unicode::CharExt; + + match chomp_lowercase_part(buffer) { + Ok(name) => { + let chomped = name.len(); + + if let Ok(('.', _)) = char::from_utf8_slice_start(&buffer[chomped..]) { + Err(BadIdent::WeirdAccessor(pos)) + } else { + Ok(Accessor::RecordField(name)) + } + } + Err(_) => { + match chomp_integer_part(buffer) { + Ok(name) => { + let chomped = name.len(); + + if let Ok(('.', _)) = char::from_utf8_slice_start(&buffer[chomped..]) { + Err(BadIdent::WeirdAccessor(pos)) + } else { + Ok(Accessor::TupleIndex(name)) + } + } + Err(_) => { + // we've already made progress with the initial `.` + Err(BadIdent::StrayDot(pos.bump_column(1))) + } + } + } + } +} + +/// a `@Token` opaque +fn chomp_opaque_ref(buffer: &[u8], pos: Position) -> Result<&str, BadIdent> { + // assumes the leading `@` has NOT been chomped already + debug_assert_eq!(buffer.first(), Some(&b'@')); + use encode_unicode::CharExt; + + let bad_ident = BadIdent::BadOpaqueRef; + + match chomp_uppercase_part(&buffer[1..]) { + Ok(name) => { + let width = 1 + name.len(); + + if let Ok(('.', _)) = char::from_utf8_slice_start(&buffer[width..]) { + Err(bad_ident(pos.bump_column(width as u32))) + } else { + let value = unsafe { std::str::from_utf8_unchecked(&buffer[..width]) }; + Ok(value) + } + } + Err(_) => Err(bad_ident(pos.bump_column(1))), + } +} + +fn chomp_identifier_chain<'a>( + arena: &'a Bump, + buffer: &'a [u8], + pos: Position, +) -> Result<(u32, Ident<'a>), (u32, BadIdent)> { + use encode_unicode::CharExt; + + let first_is_uppercase; + let mut chomped = 0; + + match char::from_utf8_slice_start(&buffer[chomped..]) { + Ok((ch, width)) => match ch { + '.' => match chomp_accessor(&buffer[1..], pos) { + Ok(accessor) => { + let bytes_parsed = 1 + accessor.len(); + return Ok((bytes_parsed as u32, Ident::AccessorFunction(accessor))); + } + Err(fail) => return Err((1, fail)), + }, + '@' => match chomp_opaque_ref(buffer, pos) { + Ok(tagname) => { + let bytes_parsed = tagname.len(); + + let ident = Ident::OpaqueRef; + + return Ok((bytes_parsed as u32, ident(tagname))); + } + Err(fail) => return Err((1, fail)), + }, + c if c.is_alphabetic() => { + // fall through + chomped += width; + first_is_uppercase = c.is_uppercase(); + } + _ => { + return Err((0, BadIdent::Start(pos))); + } + }, + Err(_) => return Err((0, BadIdent::Start(pos))), + } + + while let Ok((ch, width)) = char::from_utf8_slice_start(&buffer[chomped..]) { + if ch.is_alphabetic() || ch.is_ascii_digit() { + chomped += width; + } else { + // we're done + break; + } + } + + if let Ok(('.', _)) = char::from_utf8_slice_start(&buffer[chomped..]) { + let module_name = if first_is_uppercase { + match chomp_module_chain(&buffer[chomped..]) { + Ok(width) => { + chomped += width as usize; + unsafe { std::str::from_utf8_unchecked(&buffer[..chomped]) } + } + Err(MadeProgress) => todo!(), + Err(NoProgress) => unsafe { std::str::from_utf8_unchecked(&buffer[..chomped]) }, + } + } else { + "" + }; + + let mut parts = Vec::with_capacity_in(4, arena); + + if !first_is_uppercase { + let first_part = unsafe { std::str::from_utf8_unchecked(&buffer[..chomped]) }; + parts.push(Accessor::RecordField(first_part)); + } + + match chomp_access_chain(&buffer[chomped..], &mut parts) { + Ok(width) => { + if matches!(parts[0], Accessor::TupleIndex(_)) && first_is_uppercase { + return Err(( + chomped as u32, + BadIdent::QualifiedTupleAccessor(pos.bump_column(chomped as u32)), + )); + } + + chomped += width as usize; + + let ident = Ident::Access { + module_name, + parts: parts.into_bump_slice(), + }; + + Ok((chomped as u32, ident)) + } + Err(0) if !module_name.is_empty() => Err(( + chomped as u32, + BadIdent::QualifiedTag(pos.bump_column(chomped as u32)), + )), + Err(1) if parts.is_empty() => Err(( + chomped as u32 + 1, + BadIdent::WeirdDotQualified(pos.bump_column(chomped as u32 + 1)), + )), + Err(width) => Err(( + chomped as u32 + width, + BadIdent::WeirdDotAccess(pos.bump_column(chomped as u32 + width)), + )), + } + } else if let Ok(('_', _)) = char::from_utf8_slice_start(&buffer[chomped..]) { + // we don't allow underscores in the middle of an identifier + // but still parse them (and generate a malformed identifier) + // to give good error messages for this case + Err(( + chomped as u32 + 1, + BadIdent::UnderscoreInMiddle(pos.bump_column(chomped as u32 + 1)), + )) + } else if first_is_uppercase { + // just one segment, starting with an uppercase letter; that's a tag + let value = unsafe { std::str::from_utf8_unchecked(&buffer[..chomped]) }; + Ok((chomped as u32, Ident::Tag(value))) + } else { + // just one segment, starting with a lowercase letter; that's a normal identifier + let value = unsafe { std::str::from_utf8_unchecked(&buffer[..chomped]) }; + let ident = Ident::Access { + module_name: "", + parts: arena.alloc([Accessor::RecordField(value)]), + }; + Ok((chomped as u32, ident)) + } +} + +fn chomp_module_chain(buffer: &[u8]) -> Result { + let mut chomped = 0; + + while let Some(b'.') = buffer.get(chomped) { + match &buffer.get(chomped + 1..) { + Some(slice) => match chomp_uppercase_part(slice) { + Ok(name) => { + chomped += name.len() + 1; + } + Err(MadeProgress) => return Err(MadeProgress), + Err(NoProgress) => break, + }, + None => return Err(MadeProgress), + } + } + + if chomped == 0 { + Err(NoProgress) + } else { + Ok(chomped as u32) + } +} + +pub fn concrete_type<'a>() -> impl Parser<'a, (&'a str, &'a str), ()> { + move |_, state: State<'a>, _min_indent: u32| match chomp_concrete_type(state.bytes()) { + Err(progress) => Err((progress, ())), + Ok((module_name, type_name, width)) => { + Ok((MadeProgress, (module_name, type_name), state.advance(width))) + } + } +} + +// parse a type name like `Result` or `Result.Result` +fn chomp_concrete_type(buffer: &[u8]) -> Result<(&str, &str, usize), Progress> { + let first = crate::ident::chomp_uppercase_part(buffer)?; + + if let Some(b'.') = buffer.get(first.len()) { + match crate::ident::chomp_module_chain(&buffer[first.len()..]) { + Err(_) => Err(MadeProgress), + Ok(rest) => { + let width = first.len() + rest as usize; + + // we must explicitly check here for a trailing `.` + if let Some(b'.') = buffer.get(width) { + return Err(MadeProgress); + } + + let slice = &buffer[..width]; + + match slice.iter().rev().position(|c| *c == b'.') { + None => Ok(("", first, first.len())), + Some(rev_index) => { + let index = slice.len() - rev_index; + let module_name = + unsafe { std::str::from_utf8_unchecked(&slice[..index - 1]) }; + let type_name = unsafe { std::str::from_utf8_unchecked(&slice[index..]) }; + + Ok((module_name, type_name, width)) + } + } + } + } + } else { + Ok(("", first, first.len())) + } +} + +fn chomp_access_chain<'a>(buffer: &'a [u8], parts: &mut Vec<'a, Accessor<'a>>) -> Result { + let mut chomped = 0; + + while let Some(b'.') = buffer.get(chomped) { + match &buffer.get(chomped + 1..) { + Some(slice) => match chomp_lowercase_part(slice) { + Ok(name) => { + let value = unsafe { + std::str::from_utf8_unchecked( + &buffer[chomped + 1..chomped + 1 + name.len()], + ) + }; + parts.push(Accessor::RecordField(value)); + + chomped += name.len() + 1; + } + Err(_) => match chomp_integer_part(slice) { + Ok(name) => { + let value = unsafe { + std::str::from_utf8_unchecked( + &buffer[chomped + 1..chomped + 1 + name.len()], + ) + }; + parts.push(Accessor::TupleIndex(value)); + + chomped += name.len() + 1; + } + Err(_) => return Err(chomped as u32 + 1), + }, + }, + None => return Err(chomped as u32 + 1), + } + } + + if chomped == 0 { + Err(0) + } else { + Ok(chomped as u32) + } +} diff --git a/crates/compiler/parse/src/keyword.rs b/crates/compiler/parse/src/keyword.rs new file mode 100644 index 0000000000..e37f0c5e5c --- /dev/null +++ b/crates/compiler/parse/src/keyword.rs @@ -0,0 +1,17 @@ +// These keywords are valid in expressions +pub const IF: &str = "if"; +pub const THEN: &str = "then"; +pub const ELSE: &str = "else"; +pub const WHEN: &str = "when"; +pub const AS: &str = "as"; +pub const IS: &str = "is"; +pub const DBG: &str = "dbg"; +pub const EXPECT: &str = "expect"; +pub const EXPECT_FX: &str = "expect-fx"; +pub const CRASH: &str = "crash"; + +// These keywords are valid in types +pub const IMPLEMENTS: &str = "implements"; +pub const WHERE: &str = "where"; + +pub const KEYWORDS: [&str; 10] = [IF, THEN, ELSE, WHEN, AS, IS, DBG, EXPECT, EXPECT_FX, CRASH]; diff --git a/crates/compiler/parse/src/lib.rs b/crates/compiler/parse/src/lib.rs new file mode 100644 index 0000000000..007f933eff --- /dev/null +++ b/crates/compiler/parse/src/lib.rs @@ -0,0 +1,24 @@ +//! Implements the Roc parser, which transforms a textual representation of a +//! Roc program to an [abstract syntax tree](https://en.wikipedia.org/wiki/Abstract_syntax_tree). +#![warn(clippy::dbg_macro)] +// See github.com/roc-lang/roc/issues/800 for discussion of the large_enum_variant check. +#![allow(clippy::large_enum_variant)] + +#[macro_use] +pub mod parser; +pub mod ast; +pub mod blankspace; +pub mod expr; +pub mod header; +pub mod highlight; +pub mod ident; +pub mod keyword; +pub mod module; +pub mod number_literal; +pub mod pattern; +pub mod problems; +pub mod src64; +pub mod state; +pub mod string_literal; +pub mod test_helpers; +pub mod type_annotation; diff --git a/crates/compiler/parse/src/module.rs b/crates/compiler/parse/src/module.rs new file mode 100644 index 0000000000..aea466ae9f --- /dev/null +++ b/crates/compiler/parse/src/module.rs @@ -0,0 +1,674 @@ +use crate::ast::{Collection, Defs, Header, Module, Spaced, Spaces}; +use crate::blankspace::{space0_around_ee, space0_before_e, space0_e}; +use crate::header::{ + package_entry, package_name, AppHeader, ExposedName, ExposesKeyword, GeneratesKeyword, + HostedHeader, ImportsEntry, ImportsKeyword, InterfaceHeader, Keyword, KeywordItem, ModuleName, + PackageEntry, PackageHeader, PackagesKeyword, PlatformHeader, PlatformRequires, + ProvidesKeyword, ProvidesTo, RequiresKeyword, To, ToKeyword, TypedIdent, WithKeyword, +}; +use crate::ident::{self, lowercase_ident, unqualified_ident, uppercase, UppercaseIdent}; +use crate::parser::Progress::{self, *}; +use crate::parser::{ + backtrackable, increment_min_indent, optional, reset_min_indent, specialize, word1, word2, + EExposes, EGenerates, EGeneratesWith, EHeader, EImports, EPackages, EProvides, ERequires, + ETypedIdent, Parser, SourceError, SpaceProblem, SyntaxError, +}; +use crate::state::State; +use crate::string_literal::{self, parse_str_literal}; +use crate::type_annotation; +use roc_region::all::{Loc, Position}; + +fn end_of_file<'a>() -> impl Parser<'a, (), SyntaxError<'a>> { + |_arena, state: State<'a>, _min_indent: u32| { + if state.has_reached_end() { + Ok((NoProgress, (), state)) + } else { + Err((NoProgress, SyntaxError::NotEndOfFile(state.pos()))) + } + } +} + +#[inline(always)] +pub fn module_defs<'a>() -> impl Parser<'a, Defs<'a>, SyntaxError<'a>> { + skip_second!( + specialize(SyntaxError::Expr, crate::expr::toplevel_defs(),), + end_of_file() + ) +} + +pub fn parse_header<'a>( + arena: &'a bumpalo::Bump, + state: State<'a>, +) -> Result<(Module<'a>, State<'a>), SourceError<'a, EHeader<'a>>> { + let min_indent = 0; + match header().parse(arena, state.clone(), min_indent) { + Ok((_, module, state)) => Ok((module, state)), + Err((_, fail)) => Err(SourceError::new(fail, &state)), + } +} + +pub fn header<'a>() -> impl Parser<'a, Module<'a>, EHeader<'a>> { + use crate::parser::keyword_e; + + record!(Module { + comments: space0_e(EHeader::IndentStart), + header: one_of![ + map!( + skip_first!( + keyword_e("interface", EHeader::Start), + increment_min_indent(interface_header()) + ), + Header::Interface + ), + map!( + skip_first!( + keyword_e("app", EHeader::Start), + increment_min_indent(app_header()) + ), + Header::App + ), + map!( + skip_first!( + keyword_e("package", EHeader::Start), + increment_min_indent(package_header()) + ), + Header::Package + ), + map!( + skip_first!( + keyword_e("platform", EHeader::Start), + increment_min_indent(platform_header()) + ), + Header::Platform + ), + map!( + skip_first!( + keyword_e("hosted", EHeader::Start), + increment_min_indent(hosted_header()) + ), + Header::Hosted + ), + ] + }) +} + +#[inline(always)] +fn interface_header<'a>() -> impl Parser<'a, InterfaceHeader<'a>, EHeader<'a>> { + record!(InterfaceHeader { + before_name: space0_e(EHeader::IndentStart), + name: loc!(module_name_help(EHeader::ModuleName)), + exposes: specialize(EHeader::Exposes, exposes_values()), + imports: specialize(EHeader::Imports, imports()), + }) + .trace("interface_header") +} + +#[inline(always)] +fn hosted_header<'a>() -> impl Parser<'a, HostedHeader<'a>, EHeader<'a>> { + record!(HostedHeader { + before_name: space0_e(EHeader::IndentStart), + name: loc!(module_name_help(EHeader::ModuleName)), + exposes: specialize(EHeader::Exposes, exposes_values()), + imports: specialize(EHeader::Imports, imports()), + generates: specialize(EHeader::Generates, generates()), + generates_with: specialize(EHeader::GeneratesWith, generates_with()), + }) + .trace("hosted_header") +} + +fn chomp_module_name(buffer: &[u8]) -> Result<&str, Progress> { + use encode_unicode::CharExt; + + let mut chomped = 0; + + if let Ok((first_letter, width)) = char::from_utf8_slice_start(&buffer[chomped..]) { + if first_letter.is_uppercase() { + chomped += width; + } else { + return Err(Progress::NoProgress); + } + } + + while let Ok((ch, width)) = char::from_utf8_slice_start(&buffer[chomped..]) { + // After the first character, only these are allowed: + // + // * Unicode alphabetic chars - you might include `鹏` if that's clear to your readers + // * ASCII digits - e.g. `1` but not `¾`, both of which pass .is_numeric() + // * A '.' separating module parts + if ch.is_alphabetic() || ch.is_ascii_digit() { + chomped += width; + } else if ch == '.' { + chomped += width; + + if let Ok((first_letter, width)) = char::from_utf8_slice_start(&buffer[chomped..]) { + if first_letter.is_uppercase() { + chomped += width; + } else if first_letter == '{' { + // the .{ starting a `Foo.{ bar, baz }` importing clauses + chomped -= width; + break; + } else { + return Err(Progress::MadeProgress); + } + } + } else { + // we're done + break; + } + } + + let name = unsafe { std::str::from_utf8_unchecked(&buffer[..chomped]) }; + + Ok(name) +} + +#[inline(always)] +fn module_name<'a>() -> impl Parser<'a, ModuleName<'a>, ()> { + |_, mut state: State<'a>, _min_indent: u32| match chomp_module_name(state.bytes()) { + Ok(name) => { + let width = name.len(); + state = state.advance(width); + + Ok((MadeProgress, ModuleName::new(name), state)) + } + Err(progress) => Err((progress, ())), + } +} + +#[inline(always)] +fn app_header<'a>() -> impl Parser<'a, AppHeader<'a>, EHeader<'a>> { + record!(AppHeader { + before_name: space0_e(EHeader::IndentStart), + name: loc!(crate::parser::specialize( + EHeader::AppName, + string_literal::parse_str_literal() + )), + packages: optional(specialize(EHeader::Packages, packages())), + imports: optional(specialize(EHeader::Imports, imports())), + provides: specialize(EHeader::Provides, provides_to()), + }) + .trace("app_header") +} + +#[inline(always)] +fn package_header<'a>() -> impl Parser<'a, PackageHeader<'a>, EHeader<'a>> { + record!(PackageHeader { + before_name: space0_e(EHeader::IndentStart), + name: loc!(specialize(EHeader::PackageName, package_name())), + exposes: specialize(EHeader::Exposes, exposes_modules()), + packages: specialize(EHeader::Packages, packages()), + }) + .trace("package_header") +} + +#[inline(always)] +fn platform_header<'a>() -> impl Parser<'a, PlatformHeader<'a>, EHeader<'a>> { + record!(PlatformHeader { + before_name: space0_e(EHeader::IndentStart), + name: loc!(specialize(EHeader::PlatformName, package_name())), + requires: specialize(EHeader::Requires, requires()), + exposes: specialize(EHeader::Exposes, exposes_modules()), + packages: specialize(EHeader::Packages, packages()), + imports: specialize(EHeader::Imports, imports()), + provides: specialize(EHeader::Provides, provides_exposed()), + }) + .trace("platform_header") +} + +fn provides_to_package<'a>() -> impl Parser<'a, To<'a>, EProvides<'a>> { + one_of![ + specialize( + |_, pos| EProvides::Identifier(pos), + map!(lowercase_ident(), To::ExistingPackage) + ), + specialize(EProvides::Package, map!(package_name(), To::NewPackage)) + ] +} + +#[inline(always)] +fn provides_to<'a>() -> impl Parser<'a, ProvidesTo<'a>, EProvides<'a>> { + record!(ProvidesTo { + provides_keyword: spaces_around_keyword( + ProvidesKeyword, + EProvides::Provides, + EProvides::IndentProvides, + EProvides::IndentListStart + ), + entries: collection_trailing_sep_e!( + word1(b'[', EProvides::ListStart), + exposes_entry(EProvides::Identifier), + word1(b',', EProvides::ListEnd), + word1(b']', EProvides::ListEnd), + Spaced::SpaceBefore + ), + types: optional(backtrackable(provides_types())), + to_keyword: spaces_around_keyword( + ToKeyword, + EProvides::To, + EProvides::IndentTo, + EProvides::IndentListStart + ), + to: loc!(provides_to_package()), + }) + .trace("provides_to") +} + +fn provides_exposed<'a>() -> impl Parser< + 'a, + KeywordItem<'a, ProvidesKeyword, Collection<'a, Loc>>>>, + EProvides<'a>, +> { + record!(KeywordItem { + keyword: spaces_around_keyword( + ProvidesKeyword, + EProvides::Provides, + EProvides::IndentProvides, + EProvides::IndentListStart + ), + item: collection_trailing_sep_e!( + word1(b'[', EProvides::ListStart), + exposes_entry(EProvides::Identifier), + word1(b',', EProvides::ListEnd), + word1(b']', EProvides::ListEnd), + Spaced::SpaceBefore + ), + }) +} + +#[inline(always)] +fn provides_types<'a>( +) -> impl Parser<'a, Collection<'a, Loc>>>, EProvides<'a>> { + skip_first!( + // We only support spaces here, not newlines, because this is not intended + // to be the design forever. Someday it will hopefully work like Elm, + // where platform authors can provide functions like Browser.sandbox which + // present an API based on ordinary-looking type variables. + zero_or_more!(word1( + b' ', + // HACK: If this errors, EProvides::Provides is not an accurate reflection + // of what went wrong. However, this is both skipped and zero_or_more, + // so this error should never be visible to anyone in practice! + EProvides::Provides + )), + collection_trailing_sep_e!( + word1(b'{', EProvides::ListStart), + provides_type_entry(EProvides::Identifier), + word1(b',', EProvides::ListEnd), + word1(b'}', EProvides::ListEnd), + Spaced::SpaceBefore + ) + ) +} + +fn provides_type_entry<'a, F, E>( + to_expectation: F, +) -> impl Parser<'a, Loc>>, E> +where + F: Fn(Position) -> E, + F: Copy, + E: 'a, +{ + loc!(map!( + specialize(|_, pos| to_expectation(pos), ident::uppercase()), + Spaced::Item + )) +} + +fn exposes_entry<'a, F, E>( + to_expectation: F, +) -> impl Parser<'a, Loc>>, E> +where + F: Fn(Position) -> E, + F: Copy, + E: 'a, +{ + loc!(map!( + specialize(|_, pos| to_expectation(pos), unqualified_ident()), + |n| Spaced::Item(ExposedName::new(n)) + )) +} + +#[inline(always)] +fn requires<'a>( +) -> impl Parser<'a, KeywordItem<'a, RequiresKeyword, PlatformRequires<'a>>, ERequires<'a>> { + record!(KeywordItem { + keyword: spaces_around_keyword( + RequiresKeyword, + ERequires::Requires, + ERequires::IndentRequires, + ERequires::IndentListStart + ), + item: platform_requires(), + }) +} + +#[inline(always)] +fn platform_requires<'a>() -> impl Parser<'a, PlatformRequires<'a>, ERequires<'a>> { + record!(PlatformRequires { + rigids: skip_second!(requires_rigids(), space0_e(ERequires::ListStart)), + signature: requires_typed_ident() + }) +} + +#[inline(always)] +fn requires_rigids<'a>( +) -> impl Parser<'a, Collection<'a, Loc>>>, ERequires<'a>> { + collection_trailing_sep_e!( + word1(b'{', ERequires::ListStart), + specialize( + |_, pos| ERequires::Rigid(pos), + loc!(map!(ident::uppercase(), Spaced::Item)) + ), + word1(b',', ERequires::ListEnd), + word1(b'}', ERequires::ListEnd), + Spaced::SpaceBefore + ) +} + +#[inline(always)] +fn requires_typed_ident<'a>() -> impl Parser<'a, Loc>>, ERequires<'a>> { + skip_first!( + word1(b'{', ERequires::ListStart), + skip_second!( + reset_min_indent(space0_around_ee( + specialize(ERequires::TypedIdent, loc!(typed_ident()),), + ERequires::ListStart, + ERequires::ListEnd + )), + word1(b'}', ERequires::ListStart) + ) + ) +} + +#[inline(always)] +fn exposes_values<'a>() -> impl Parser< + 'a, + KeywordItem<'a, ExposesKeyword, Collection<'a, Loc>>>>, + EExposes, +> { + record!(KeywordItem { + keyword: spaces_around_keyword( + ExposesKeyword, + EExposes::Exposes, + EExposes::IndentExposes, + EExposes::IndentListStart + ), + item: collection_trailing_sep_e!( + word1(b'[', EExposes::ListStart), + exposes_entry(EExposes::Identifier), + word1(b',', EExposes::ListEnd), + word1(b']', EExposes::ListEnd), + Spaced::SpaceBefore + ) + }) +} + +fn spaces_around_keyword<'a, K: Keyword, E>( + keyword_item: K, + expectation: fn(Position) -> E, + indent_problem1: fn(Position) -> E, + indent_problem2: fn(Position) -> E, +) -> impl Parser<'a, Spaces<'a, K>, E> +where + E: 'a + SpaceProblem, +{ + map!( + and!( + skip_second!( + backtrackable(space0_e(indent_problem1)), + crate::parser::keyword_e(K::KEYWORD, expectation) + ), + space0_e(indent_problem2) + ), + |(before, after)| { + Spaces { + before, + item: keyword_item, + after, + } + } + ) +} + +#[inline(always)] +fn exposes_modules<'a>() -> impl Parser< + 'a, + KeywordItem<'a, ExposesKeyword, Collection<'a, Loc>>>>, + EExposes, +> { + record!(KeywordItem { + keyword: spaces_around_keyword( + ExposesKeyword, + EExposes::Exposes, + EExposes::IndentExposes, + EExposes::IndentListStart + ), + item: collection_trailing_sep_e!( + word1(b'[', EExposes::ListStart), + exposes_module(EExposes::Identifier), + word1(b',', EExposes::ListEnd), + word1(b']', EExposes::ListEnd), + Spaced::SpaceBefore + ), + }) +} + +fn exposes_module<'a, F, E>( + to_expectation: F, +) -> impl Parser<'a, Loc>>, E> +where + F: Fn(Position) -> E, + F: Copy, + E: 'a, +{ + loc!(map!( + specialize(|_, pos| to_expectation(pos), module_name()), + Spaced::Item + )) +} + +#[inline(always)] +fn packages<'a>() -> impl Parser< + 'a, + KeywordItem<'a, PackagesKeyword, Collection<'a, Loc>>>>, + EPackages<'a>, +> { + record!(KeywordItem { + keyword: spaces_around_keyword( + PackagesKeyword, + EPackages::Packages, + EPackages::IndentPackages, + EPackages::IndentListStart + ), + item: collection_trailing_sep_e!( + word1(b'{', EPackages::ListStart), + specialize(EPackages::PackageEntry, loc!(package_entry())), + word1(b',', EPackages::ListEnd), + word1(b'}', EPackages::ListEnd), + Spaced::SpaceBefore + ) + }) +} + +#[inline(always)] +fn generates<'a>( +) -> impl Parser<'a, KeywordItem<'a, GeneratesKeyword, UppercaseIdent<'a>>, EGenerates> { + record!(KeywordItem { + keyword: spaces_around_keyword( + GeneratesKeyword, + EGenerates::Generates, + EGenerates::IndentGenerates, + EGenerates::IndentTypeStart + ), + item: specialize(|(), pos| EGenerates::Identifier(pos), uppercase()) + }) +} + +#[inline(always)] +fn generates_with<'a>() -> impl Parser< + 'a, + KeywordItem<'a, WithKeyword, Collection<'a, Loc>>>>, + EGeneratesWith, +> { + record!(KeywordItem { + keyword: spaces_around_keyword( + WithKeyword, + EGeneratesWith::With, + EGeneratesWith::IndentWith, + EGeneratesWith::IndentListStart + ), + item: collection_trailing_sep_e!( + word1(b'[', EGeneratesWith::ListStart), + exposes_entry(EGeneratesWith::Identifier), + word1(b',', EGeneratesWith::ListEnd), + word1(b']', EGeneratesWith::ListEnd), + Spaced::SpaceBefore + ) + }) +} + +#[inline(always)] +fn imports<'a>() -> impl Parser< + 'a, + KeywordItem<'a, ImportsKeyword, Collection<'a, Loc>>>>, + EImports, +> { + record!(KeywordItem { + keyword: spaces_around_keyword( + ImportsKeyword, + EImports::Imports, + EImports::IndentImports, + EImports::IndentListStart + ), + item: collection_trailing_sep_e!( + word1(b'[', EImports::ListStart), + loc!(imports_entry()), + word1(b',', EImports::ListEnd), + word1(b']', EImports::ListEnd), + Spaced::SpaceBefore + ) + }) + .trace("imports") +} + +#[inline(always)] +fn typed_ident<'a>() -> impl Parser<'a, Spaced<'a, TypedIdent<'a>>, ETypedIdent<'a>> { + // e.g. + // + // printLine : Str -> Effect {} + map!( + and!( + and!( + loc!(specialize( + |_, pos| ETypedIdent::Identifier(pos), + lowercase_ident() + )), + space0_e(ETypedIdent::IndentHasType) + ), + skip_first!( + word1(b':', ETypedIdent::HasType), + space0_before_e( + specialize( + ETypedIdent::Type, + reset_min_indent(type_annotation::located(true)) + ), + ETypedIdent::IndentType, + ) + ) + ), + |((ident, spaces_before_colon), ann)| { + Spaced::Item(TypedIdent { + ident, + spaces_before_colon, + ann, + }) + } + ) +} + +fn shortname<'a>() -> impl Parser<'a, &'a str, EImports> { + specialize(|_, pos| EImports::Shorthand(pos), lowercase_ident()) +} + +fn module_name_help<'a, F, E>(to_expectation: F) -> impl Parser<'a, ModuleName<'a>, E> +where + F: Fn(Position) -> E, + E: 'a, + F: 'a, +{ + specialize(move |_, pos| to_expectation(pos), module_name()) +} + +#[inline(always)] +fn imports_entry<'a>() -> impl Parser<'a, Spaced<'a, ImportsEntry<'a>>, EImports> { + type Temp<'a> = ( + (Option<&'a str>, ModuleName<'a>), + Option>>>>, + ); + + one_of!( + map!( + and!( + and!( + // e.g. `pf.` + optional(backtrackable(skip_second!( + shortname(), + word1(b'.', EImports::ShorthandDot) + ))), + // e.g. `Task` + module_name_help(EImports::ModuleName) + ), + // e.g. `.{ Task, after}` + optional(skip_first!( + word1(b'.', EImports::ExposingDot), + collection_trailing_sep_e!( + word1(b'{', EImports::SetStart), + exposes_entry(EImports::Identifier), + word1(b',', EImports::SetEnd), + word1(b'}', EImports::SetEnd), + Spaced::SpaceBefore + ) + )) + ), + |((opt_shortname, module_name), opt_values): Temp<'a>| { + let exposed_values = opt_values.unwrap_or_else(Collection::empty); + + let entry = match opt_shortname { + Some(shortname) => { + ImportsEntry::Package(shortname, module_name, exposed_values) + } + + None => ImportsEntry::Module(module_name, exposed_values), + }; + + Spaced::Item(entry) + } + ) + .trace("normal_import"), + map!( + and!( + and!( + // e.g. "filename" + // TODO: str literal allows for multiline strings. We probably don't want that for file names. + specialize(|_, pos| EImports::StrLiteral(pos), parse_str_literal()), + // e.g. as + and!( + and!( + space0_e(EImports::AsKeyword), + word2(b'a', b's', EImports::AsKeyword) + ), + space0_e(EImports::AsKeyword) + ) + ), + // e.g. file : Str + specialize(|_, pos| EImports::TypedIdent(pos), typed_ident()) + ), + |((file_name, _), typed_ident)| { + // TODO: look at blacking block strings during parsing. + Spaced::Item(ImportsEntry::IngestedFile(file_name, typed_ident)) + } + ) + .trace("ingest_file_import") + ) + .trace("imports_entry") +} diff --git a/compiler/parse/src/number_literal.rs b/crates/compiler/parse/src/number_literal.rs similarity index 89% rename from compiler/parse/src/number_literal.rs rename to crates/compiler/parse/src/number_literal.rs index a4e8624684..71d27ba4e2 100644 --- a/compiler/parse/src/number_literal.rs +++ b/crates/compiler/parse/src/number_literal.rs @@ -13,22 +13,22 @@ pub enum NumLiteral<'a> { } pub fn positive_number_literal<'a>() -> impl Parser<'a, NumLiteral<'a>, ENumber> { - move |_arena, state: State<'a>| { - match state.bytes().get(0) { + move |_arena, state: State<'a>, _min_indent: u32| { + match state.bytes().first() { Some(first_byte) if (*first_byte as char).is_ascii_digit() => { parse_number_base(false, state.bytes(), state) } _ => { // this is not a number at all - Err((Progress::NoProgress, ENumber::End, state)) + Err((Progress::NoProgress, ENumber::End)) } } } } pub fn number_literal<'a>() -> impl Parser<'a, NumLiteral<'a>, ENumber> { - move |_arena, state: State<'a>| { - match state.bytes().get(0) { + move |_arena, state: State<'a>, _min_indent: u32| { + match state.bytes().first() { Some(first_byte) if *first_byte == b'-' => { // drop the minus parse_number_base(true, &state.bytes()[1..], state) @@ -38,7 +38,7 @@ pub fn number_literal<'a>() -> impl Parser<'a, NumLiteral<'a>, ENumber> { } _ => { // this is not a number at all - Err((Progress::NoProgress, ENumber::End, state)) + Err((Progress::NoProgress, ENumber::End)) } } } @@ -89,12 +89,12 @@ fn chomp_number_dec<'a>( if is_negative && chomped == 0 { // we're probably actually looking at unary negation here - return Err((Progress::NoProgress, ENumber::End, state)); + return Err((Progress::NoProgress, ENumber::End)); } - if !bytes.get(0).copied().unwrap_or_default().is_ascii_digit() { + if !bytes.first().copied().unwrap_or_default().is_ascii_digit() { // we're probably actually looking at unary negation here - return Err((Progress::NoProgress, ENumber::End, state)); + return Err((Progress::NoProgress, ENumber::End)); } let string = @@ -117,7 +117,7 @@ fn chomp_number(mut bytes: &[u8]) -> (bool, usize) { let start_bytes_len = bytes.len(); let mut is_float = false; - while let Some(byte) = bytes.get(0) { + while let Some(byte) = bytes.first() { match byte { b'.' => { // skip, fix multiple `.`s in canonicalization diff --git a/crates/compiler/parse/src/parser.rs b/crates/compiler/parse/src/parser.rs new file mode 100644 index 0000000000..1cc05a4e32 --- /dev/null +++ b/crates/compiler/parse/src/parser.rs @@ -0,0 +1,1848 @@ +use crate::state::State; +use bumpalo::collections::vec::Vec; +use bumpalo::Bump; +use roc_region::all::{Loc, Position, Region}; +use Progress::*; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Either { + First(First), + Second(Second), +} + +impl Copy for Either {} + +pub type ParseResult<'a, Output, Error> = Result<(Progress, Output, State<'a>), (Progress, Error)>; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Progress { + MadeProgress, + NoProgress, +} + +impl Progress { + pub fn from_lengths(before: usize, after: usize) -> Self { + Self::from_consumed(before - after) + } + pub fn from_consumed(chars_consumed: usize) -> Self { + Self::progress_when(chars_consumed != 0) + } + + pub fn progress_when(made_progress: bool) -> Self { + if made_progress { + Progress::MadeProgress + } else { + Progress::NoProgress + } + } + + pub fn or(&self, other: Self) -> Self { + if (*self == MadeProgress) || (other == MadeProgress) { + MadeProgress + } else { + NoProgress + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum SyntaxError<'a> { + Unexpected(Region), + OutdentedTooFar, + Eof(Region), + InvalidPattern, + BadUtf8, + ReservedKeyword(Region), + ArgumentsBeforeEquals(Region), + NotYetImplemented(String), + Todo, + Type(EType<'a>), + Pattern(EPattern<'a>), + Expr(EExpr<'a>, Position), + Header(EHeader<'a>), + Space(BadInputError), + NotEndOfFile(Position), +} +pub trait SpaceProblem: std::fmt::Debug { + fn space_problem(e: BadInputError, pos: Position) -> Self; +} + +macro_rules! impl_space_problem { + ($($name:ident $(< $lt:tt >)?),*) => { + $( + impl $(< $lt >)? SpaceProblem for $name $(< $lt >)? { + fn space_problem(e: BadInputError, pos: Position) -> Self { + Self::Space(e, pos) + } + } + )* + }; +} + +impl_space_problem! { + EExpect<'a>, + EExposes, + EExpr<'a>, + EGenerates, + EGeneratesWith, + EHeader<'a>, + EIf<'a>, + EImports, + EInParens<'a>, + EClosure<'a>, + EList<'a>, + EPackageEntry<'a>, + EPackages<'a>, + EPattern<'a>, + EProvides<'a>, + ERecord<'a>, + ERequires<'a>, + EString<'a>, + EType<'a>, + ETypeInParens<'a>, + ETypeRecord<'a>, + ETypeTagUnion<'a>, + ETypedIdent<'a>, + ETypeAbilityImpl<'a>, + EWhen<'a>, + EAbility<'a>, + PInParens<'a>, + PRecord<'a>, + PList<'a> +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum EHeader<'a> { + Provides(EProvides<'a>, Position), + Exposes(EExposes, Position), + Imports(EImports, Position), + Requires(ERequires<'a>, Position), + Packages(EPackages<'a>, Position), + Generates(EGenerates, Position), + GeneratesWith(EGeneratesWith, Position), + + Space(BadInputError, Position), + Start(Position), + ModuleName(Position), + AppName(EString<'a>, Position), + PackageName(EPackageName<'a>, Position), + PlatformName(EPackageName<'a>, Position), + IndentStart(Position), + + InconsistentModuleName(Region), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum EProvides<'a> { + Provides(Position), + Open(Position), + To(Position), + IndentProvides(Position), + IndentTo(Position), + IndentListStart(Position), + IndentPackage(Position), + ListStart(Position), + ListEnd(Position), + Identifier(Position), + Package(EPackageName<'a>, Position), + Space(BadInputError, Position), +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum EExposes { + Exposes(Position), + Open(Position), + IndentExposes(Position), + IndentListStart(Position), + ListStart(Position), + ListEnd(Position), + Identifier(Position), + Space(BadInputError, Position), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ERequires<'a> { + Requires(Position), + Open(Position), + IndentRequires(Position), + IndentListStart(Position), + ListStart(Position), + ListEnd(Position), + TypedIdent(ETypedIdent<'a>, Position), + Rigid(Position), + Space(BadInputError, Position), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ETypedIdent<'a> { + Space(BadInputError, Position), + HasType(Position), + IndentHasType(Position), + Name(Position), + Type(EType<'a>, Position), + IndentType(Position), + Identifier(Position), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum EPackages<'a> { + Open(Position), + Space(BadInputError, Position), + Packages(Position), + IndentPackages(Position), + ListStart(Position), + ListEnd(Position), + IndentListStart(Position), + IndentListEnd(Position), + PackageEntry(EPackageEntry<'a>, Position), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum EPackageName<'a> { + BadPath(EString<'a>, Position), + Escapes(Position), + Multiline(Position), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum EPackageEntry<'a> { + BadPackage(EPackageName<'a>, Position), + Shorthand(Position), + Colon(Position), + IndentPackage(Position), + Space(BadInputError, Position), +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum EImports { + Open(Position), + Imports(Position), + IndentImports(Position), + IndentListStart(Position), + IndentListEnd(Position), + ListStart(Position), + ListEnd(Position), + Identifier(Position), + ExposingDot(Position), + ShorthandDot(Position), + Shorthand(Position), + ModuleName(Position), + Space(BadInputError, Position), + IndentSetStart(Position), + SetStart(Position), + SetEnd(Position), + TypedIdent(Position), + AsKeyword(Position), + StrLiteral(Position), +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum EGenerates { + Open(Position), + Generates(Position), + IndentGenerates(Position), + Identifier(Position), + Space(BadInputError, Position), + IndentTypeStart(Position), + IndentTypeEnd(Position), +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum EGeneratesWith { + Open(Position), + With(Position), + IndentWith(Position), + IndentListStart(Position), + IndentListEnd(Position), + ListStart(Position), + ListEnd(Position), + Identifier(Position), + Space(BadInputError, Position), +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum BadInputError { + HasTab, + HasMisplacedCarriageReturn, + HasAsciiControl, + BadUtf8, +} + +impl<'a, T> SourceError<'a, T> { + pub fn new(problem: T, state: &State<'a>) -> Self { + Self { + problem, + bytes: state.original_bytes(), + } + } + + pub fn map_problem(self, f: impl FnOnce(T) -> E) -> SourceError<'a, E> { + SourceError { + problem: f(self.problem), + bytes: self.bytes, + } + } + + pub fn into_file_error(self, filename: std::path::PathBuf) -> FileError<'a, T> { + FileError { + problem: self, + filename, + } + } +} + +impl<'a> SyntaxError<'a> { + pub fn into_source_error(self, state: &State<'a>) -> SourceError<'a, SyntaxError<'a>> { + SourceError { + problem: self, + bytes: state.original_bytes(), + } + } + + pub fn into_file_error( + self, + filename: std::path::PathBuf, + state: &State<'a>, + ) -> FileError<'a, SyntaxError<'a>> { + self.into_source_error(state).into_file_error(filename) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum EExpr<'a> { + TrailingOperator(Position), + + Start(Position), + End(Position), + BadExprEnd(Position), + Space(BadInputError, Position), + + Dot(Position), + Access(Position), + UnaryNot(Position), + UnaryNegate(Position), + BadOperator(&'a str, Position), + + DefMissingFinalExpr(Position), + DefMissingFinalExpr2(&'a EExpr<'a>, Position), + Type(EType<'a>, Position), + Pattern(&'a EPattern<'a>, Position), + Ability(EAbility<'a>, Position), + IndentDefBody(Position), + IndentEquals(Position), + IndentAnnotation(Position), + Equals(Position), + Colon(Position), + DoubleColon(Position), + Ident(Position), + ElmStyleFunction(Region, Position), + MalformedPattern(Position), + QualifiedTag(Position), + BackpassComma(Position), + BackpassArrow(Position), + + When(EWhen<'a>, Position), + If(EIf<'a>, Position), + + Expect(EExpect<'a>, Position), + Dbg(EExpect<'a>, Position), + + Closure(EClosure<'a>, Position), + Underscore(Position), + Crash(Position), + + InParens(EInParens<'a>, Position), + Record(ERecord<'a>, Position), + OptionalValueInRecordBuilder(Region), + RecordUpdateBuilder(Region), + + // SingleQuote errors are folded into the EString + Str(EString<'a>, Position), + + Number(ENumber, Position), + List(EList<'a>, Position), + + IndentStart(Position), + IndentEnd(Position), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ENumber { + End, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum EString<'a> { + Open(Position), + + CodePtOpen(Position), + CodePtEnd(Position), + + InvalidSingleQuote(ESingleQuote, Position), + + Space(BadInputError, Position), + EndlessSingleLine(Position), + EndlessMultiLine(Position), + EndlessSingleQuote(Position), + UnknownEscape(Position), + Format(&'a EExpr<'a>, Position), + FormatEnd(Position), + MultilineInsufficientIndent(Position), + ExpectedDoubleQuoteGotSingleQuote(Position), +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ESingleQuote { + Empty, + TooLong, + InterpolationNotAllowed, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ERecord<'a> { + End(Position), + Open(Position), + + Updateable(Position), + Field(Position), + Colon(Position), + QuestionMark(Position), + Arrow(Position), + Ampersand(Position), + + // TODO remove + Expr(&'a EExpr<'a>, Position), + + Space(BadInputError, Position), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum EInParens<'a> { + End(Position), + Open(Position), + + /// Empty parens, e.g. () is not allowed + Empty(Position), + + /// + Expr(&'a EExpr<'a>, Position), + + /// + Space(BadInputError, Position), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum EClosure<'a> { + Space(BadInputError, Position), + Start(Position), + Arrow(Position), + Comma(Position), + Arg(Position), + // TODO make EEXpr + Pattern(EPattern<'a>, Position), + Body(&'a EExpr<'a>, Position), + IndentArrow(Position), + IndentBody(Position), + IndentArg(Position), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum EList<'a> { + Open(Position), + End(Position), + Space(BadInputError, Position), + + Expr(&'a EExpr<'a>, Position), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum EWhen<'a> { + Space(BadInputError, Position), + When(Position), + Is(Position), + Pattern(EPattern<'a>, Position), + Arrow(Position), + Bar(Position), + + IfToken(Position), + IfGuard(&'a EExpr<'a>, Position), + + Condition(&'a EExpr<'a>, Position), + Branch(&'a EExpr<'a>, Position), + + IndentCondition(Position), + IndentPattern(Position), + IndentArrow(Position), + IndentBranch(Position), + IndentIfGuard(Position), + PatternAlignment(u32, Position), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum EAbility<'a> { + Space(BadInputError, Position), + Type(EType<'a>, Position), + + DemandAlignment(i32, Position), + DemandName(Position), + DemandColon(Position), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum EIf<'a> { + Space(BadInputError, Position), + If(Position), + Then(Position), + Else(Position), + // TODO make EEXpr + Condition(&'a EExpr<'a>, Position), + ThenBranch(&'a EExpr<'a>, Position), + ElseBranch(&'a EExpr<'a>, Position), + + IndentCondition(Position), + IndentIf(Position), + IndentThenToken(Position), + IndentElseToken(Position), + IndentThenBranch(Position), + IndentElseBranch(Position), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum EExpect<'a> { + Space(BadInputError, Position), + Dbg(Position), + Expect(Position), + Condition(&'a EExpr<'a>, Position), + Continuation(&'a EExpr<'a>, Position), + IndentCondition(Position), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum EPattern<'a> { + Record(PRecord<'a>, Position), + List(PList<'a>, Position), + AsKeyword(Position), + AsIdentifier(Position), + Underscore(Position), + NotAPattern(Position), + + Start(Position), + End(Position), + Space(BadInputError, Position), + + PInParens(PInParens<'a>, Position), + NumLiteral(ENumber, Position), + + IndentStart(Position), + IndentEnd(Position), + AsIndentStart(Position), + + AccessorFunction(Position), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum PRecord<'a> { + End(Position), + Open(Position), + + Field(Position), + Colon(Position), + Optional(Position), + + Pattern(&'a EPattern<'a>, Position), + Expr(&'a EExpr<'a>, Position), + + Space(BadInputError, Position), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum PList<'a> { + End(Position), + Open(Position), + + Rest(Position), + Pattern(&'a EPattern<'a>, Position), + + Space(BadInputError, Position), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum PInParens<'a> { + Empty(Position), + End(Position), + Open(Position), + Pattern(&'a EPattern<'a>, Position), + + Space(BadInputError, Position), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum EType<'a> { + Space(BadInputError, Position), + + UnderscoreSpacing(Position), + TRecord(ETypeRecord<'a>, Position), + TTagUnion(ETypeTagUnion<'a>, Position), + TInParens(ETypeInParens<'a>, Position), + TApply(ETypeApply, Position), + TInlineAlias(ETypeInlineAlias, Position), + TBadTypeVariable(Position), + TWildcard(Position), + TInferred(Position), + /// + TStart(Position), + TEnd(Position), + TFunctionArgument(Position), + TWhereBar(Position), + TImplementsClause(Position), + TAbilityImpl(ETypeAbilityImpl<'a>, Position), + /// + TIndentStart(Position), + TIndentEnd(Position), + TAsIndentStart(Position), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ETypeRecord<'a> { + End(Position), + Open(Position), + + Field(Position), + Colon(Position), + Optional(Position), + Type(&'a EType<'a>, Position), + + Space(BadInputError, Position), + + IndentOpen(Position), + IndentColon(Position), + IndentOptional(Position), + IndentEnd(Position), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ETypeTagUnion<'a> { + End(Position), + Open(Position), + + Type(&'a EType<'a>, Position), + + Space(BadInputError, Position), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ETypeInParens<'a> { + /// e.g. (), which isn't a valid type + Empty(Position), + + End(Position), + Open(Position), + /// + Type(&'a EType<'a>, Position), + + /// + Space(BadInputError, Position), + /// + IndentOpen(Position), + IndentEnd(Position), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ETypeApply { + /// + StartNotUppercase(Position), + End(Position), + Space(BadInputError, Position), + /// + DoubleDot(Position), + TrailingDot(Position), + StartIsNumber(Position), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ETypeInlineAlias { + NotAnAlias(Position), + Qualified(Position), + ArgumentNotLowercase(Position), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ETypeAbilityImpl<'a> { + End(Position), + Open(Position), + + Field(Position), + Colon(Position), + Arrow(Position), + Optional(Position), + Type(&'a EType<'a>, Position), + + Space(BadInputError, Position), + + Updateable(Position), + QuestionMark(Position), + Ampersand(Position), + Expr(&'a EExpr<'a>, Position), + IndentBar(Position), + IndentAmpersand(Position), +} + +impl<'a> From> for ETypeAbilityImpl<'a> { + fn from(e: ERecord<'a>) -> Self { + match e { + ERecord::End(p) => ETypeAbilityImpl::End(p), + ERecord::Open(p) => ETypeAbilityImpl::Open(p), + ERecord::Field(p) => ETypeAbilityImpl::Field(p), + ERecord::Colon(p) => ETypeAbilityImpl::Colon(p), + ERecord::Arrow(p) => ETypeAbilityImpl::Arrow(p), + ERecord::Space(s, p) => ETypeAbilityImpl::Space(s, p), + ERecord::Updateable(p) => ETypeAbilityImpl::Updateable(p), + ERecord::QuestionMark(p) => ETypeAbilityImpl::QuestionMark(p), + ERecord::Ampersand(p) => ETypeAbilityImpl::Ampersand(p), + ERecord::Expr(e, p) => ETypeAbilityImpl::Expr(e, p), + } + } +} + +#[derive(Debug)] +pub struct SourceError<'a, T> { + pub problem: T, + pub bytes: &'a [u8], +} + +#[derive(Debug)] +pub struct FileError<'a, T> { + pub problem: SourceError<'a, T>, + pub filename: std::path::PathBuf, +} + +pub trait Parser<'a, Output, Error> { + fn parse( + &self, + arena: &'a Bump, + state: State<'a>, + min_indent: u32, + ) -> ParseResult<'a, Output, Error>; + + #[cfg(not(feature = "parse_debug_trace"))] + #[inline(always)] + fn trace(self, _message: &'static str) -> Self + where + Self: Sized, + Output: std::fmt::Debug, + Error: std::fmt::Debug, + { + self + } + + #[cfg(feature = "parse_debug_trace")] + fn trace(self, message: &'static str) -> Traced<'a, Output, Error, Self> + where + Self: Sized, + Output: std::fmt::Debug, + Error: std::fmt::Debug, + { + Traced { + parser: self, + message, + _phantom: Default::default(), + } + } +} + +impl<'a, F, Output, Error> Parser<'a, Output, Error> for F +where + Error: 'a, + F: Fn(&'a Bump, State<'a>, u32) -> ParseResult<'a, Output, Error>, +{ + fn parse( + &self, + arena: &'a Bump, + state: State<'a>, + min_indent: u32, + ) -> ParseResult<'a, Output, Error> { + self(arena, state, min_indent) + } +} + +#[cfg(feature = "parse_debug_trace")] +pub struct Traced<'a, O, E, P: Parser<'a, O, E>> { + parser: P, + message: &'static str, + _phantom: std::marker::PhantomData<&'a (O, E)>, +} + +#[cfg(feature = "parse_debug_trace")] +impl<'a, O: std::fmt::Debug, E: std::fmt::Debug, P: Parser<'a, O, E>> Parser<'a, O, E> + for Traced<'a, O, E, P> +where + E: 'a, +{ + fn parse(&self, arena: &'a Bump, state: State<'a>, min_indent: u32) -> ParseResult<'a, O, E> { + use std::cell::RefCell; + + thread_local! { + pub static INDENT: RefCell = RefCell::new(0); + } + + // This should be enough for anyone. Right? RIGHT? + let indent_text = + "| ; : ! | ; : ! | ; : ! | ; : ! | ; : ! | ; : ! | ; : ! | ; : ! | ; : ! "; + + let cur_indent = INDENT.with(|i| *i.borrow()); + + println!( + "{:<5?}: {}{:<50}", + state.pos(), + &indent_text[..cur_indent * 2], + self.message + ); + + let previous_state = state.clone(); + INDENT.with(|i| *i.borrow_mut() += 1); + let res = self.parser.parse(arena, state, min_indent); + INDENT.with(|i| *i.borrow_mut() = cur_indent); + + let (progress, value, state) = match &res { + Ok((progress, result, state)) => (progress, Ok(result), state), + Err((progress, error)) => (progress, Err(error), &previous_state), + }; + + println!( + "{:<5?}: {}{:<50} {:<15} {:?}", + state.pos(), + &indent_text[..cur_indent * 2], + self.message, + format!("{:?}", progress), + value + ); + + res + } +} + +pub fn allocated<'a, P, Val, Error>(parser: P) -> impl Parser<'a, &'a Val, Error> +where + Error: 'a, + P: Parser<'a, Val, Error>, + Val: 'a, +{ + move |arena, state: State<'a>, min_indent: u32| { + let (progress, answer, state) = parser.parse(arena, state, min_indent)?; + + Ok((progress, &*arena.alloc(answer), state)) + } +} + +pub fn and_then<'a, P1, P2, F, Before, After, Error>( + parser: P1, + transform: F, +) -> impl Parser<'a, After, Error> +where + P1: Parser<'a, Before, Error>, + P2: Parser<'a, After, Error>, + F: Fn(Progress, Before) -> P2, + Error: 'a, +{ + move |arena, state, min_indent| { + parser + .parse(arena, state, min_indent) + .and_then(|(progress, output, next_state)| { + transform(progress, output).parse(arena, next_state, min_indent) + }) + } +} + +pub fn then<'a, P1, F, Before, After, E>(parser: P1, transform: F) -> impl Parser<'a, After, E> +where + P1: Parser<'a, Before, E>, + After: 'a, + E: 'a, + F: Fn(&'a Bump, State<'a>, Progress, Before) -> ParseResult<'a, After, E>, +{ + move |arena, state, min_indent| { + parser + .parse(arena, state, min_indent) + .and_then(|(progress, output, next_state)| { + transform(arena, next_state, progress, output) + }) + } +} + +pub fn keyword_e<'a, ToError, E>(keyword: &'static str, if_error: ToError) -> impl Parser<'a, (), E> +where + ToError: Fn(Position) -> E, + E: 'a, +{ + move |_, mut state: State<'a>, _min_indent| { + let width = keyword.len(); + + if !state.bytes().starts_with(keyword.as_bytes()) { + return Err((NoProgress, if_error(state.pos()))); + } + + // the next character should not be an identifier character + // to prevent treating `whence` or `iffy` as keywords + match state.bytes().get(width) { + Some(next) if *next == b' ' || *next == b'#' || *next == b'\n' || *next == b'\r' => { + state = state.advance(width); + Ok((MadeProgress, (), state)) + } + None => { + state = state.advance(width); + Ok((MadeProgress, (), state)) + } + Some(_) => Err((NoProgress, if_error(state.pos()))), + } + } +} + +/// Parse zero or more values separated by a delimiter (e.g. a comma) whose +/// values are discarded +pub fn sep_by0<'a, P, D, Val, Error>( + delimiter: D, + parser: P, +) -> impl Parser<'a, Vec<'a, Val>, Error> +where + D: Parser<'a, (), Error>, + P: Parser<'a, Val, Error>, + Error: 'a, +{ + move |arena, state: State<'a>, min_indent: u32| { + let original_state = state.clone(); + + let start_bytes_len = state.bytes().len(); + + match parser.parse(arena, state, min_indent) { + Ok((elem_progress, first_output, next_state)) => { + // in practice, we want elements to make progress + debug_assert_eq!(elem_progress, MadeProgress); + + let mut state = next_state; + let mut buf = Vec::with_capacity_in(1, arena); + + buf.push(first_output); + + loop { + match delimiter.parse(arena, state.clone(), min_indent) { + Ok((_, (), next_state)) => { + // If the delimiter passed, check the element parser. + match parser.parse(arena, next_state.clone(), min_indent) { + Ok((element_progress, next_output, next_state)) => { + // in practice, we want elements to make progress + debug_assert_eq!(element_progress, MadeProgress); + + state = next_state; + buf.push(next_output); + } + Err((_, fail)) => { + // If the delimiter parsed, but the following + // element did not, that's a fatal error. + let progress = Progress::from_lengths( + start_bytes_len, + next_state.bytes().len(), + ); + + return Err((progress, fail)); + } + } + } + Err((delim_progress, fail)) => match delim_progress { + MadeProgress => return Err((MadeProgress, fail)), + NoProgress => return Ok((NoProgress, buf, state)), + }, + } + } + } + Err((element_progress, fail)) => match element_progress { + MadeProgress => Err((MadeProgress, fail)), + NoProgress => Ok((NoProgress, Vec::new_in(arena), original_state)), + }, + } + } +} + +/// Parse zero or more values separated by a delimiter (e.g. a comma) +/// with an optional trailing delimiter whose values are discarded +pub fn trailing_sep_by0<'a, P, D, Val, Error>( + delimiter: D, + parser: P, +) -> impl Parser<'a, Vec<'a, Val>, Error> +where + D: Parser<'a, (), Error>, + P: Parser<'a, Val, Error>, + Error: 'a, +{ + move |arena, state: State<'a>, min_indent: u32| { + let original_state = state.clone(); + let start_bytes_len = state.bytes().len(); + + match parser.parse(arena, state, min_indent) { + Ok((progress, first_output, next_state)) => { + // in practice, we want elements to make progress + debug_assert_eq!(progress, MadeProgress); + let mut state = next_state; + let mut buf = Vec::with_capacity_in(1, arena); + + buf.push(first_output); + + loop { + match delimiter.parse(arena, state.clone(), min_indent) { + Ok((_, (), next_state)) => { + // If the delimiter passed, check the element parser. + match parser.parse(arena, next_state.clone(), min_indent) { + Ok((element_progress, next_output, next_state)) => { + // in practice, we want elements to make progress + debug_assert_eq!(element_progress, MadeProgress); + + state = next_state; + buf.push(next_output); + } + Err((_, _fail)) => { + // If the delimiter parsed, but the following + // element did not, that means we saw a trailing comma + let progress = Progress::from_lengths( + start_bytes_len, + next_state.bytes().len(), + ); + return Ok((progress, buf, next_state)); + } + } + } + Err((delim_progress, fail)) => match delim_progress { + MadeProgress => return Err((MadeProgress, fail)), + NoProgress => return Ok((NoProgress, buf, state)), + }, + } + } + } + Err((element_progress, fail)) => match element_progress { + MadeProgress => Err((MadeProgress, fail)), + NoProgress => Ok((NoProgress, Vec::new_in(arena), original_state)), + }, + } + } +} + +/// Parse one or more values separated by a delimiter (e.g. a comma) whose +/// values are discarded +pub fn sep_by1<'a, P, D, Val, Error>( + delimiter: D, + parser: P, +) -> impl Parser<'a, Vec<'a, Val>, Error> +where + D: Parser<'a, (), Error>, + P: Parser<'a, Val, Error>, + Error: 'a, +{ + move |arena, state: State<'a>, min_indent: u32| { + let start_bytes_len = state.bytes().len(); + + match parser.parse(arena, state, min_indent) { + Ok((progress, first_output, next_state)) => { + debug_assert_eq!(progress, MadeProgress); + let mut state = next_state; + let mut buf = Vec::with_capacity_in(1, arena); + + buf.push(first_output); + + loop { + let old_state = state.clone(); + match delimiter.parse(arena, state, min_indent) { + Ok((_, (), next_state)) => { + // If the delimiter passed, check the element parser. + match parser.parse(arena, next_state, min_indent) { + Ok((_, next_output, next_state)) => { + state = next_state; + buf.push(next_output); + } + Err((_, fail)) => { + return Err((MadeProgress, fail)); + } + } + } + Err((delim_progress, fail)) => { + match delim_progress { + MadeProgress => { + // fail if the delimiter made progress + return Err((MadeProgress, fail)); + } + NoProgress => { + let progress = Progress::from_lengths( + start_bytes_len, + old_state.bytes().len(), + ); + return Ok((progress, buf, old_state)); + } + } + } + } + } + } + Err((fail_progress, fail)) => Err((fail_progress, fail)), + } + } +} + +/// Parse one or more values separated by a delimiter (e.g. a comma) whose +/// values are discarded +pub fn sep_by1_e<'a, P, V, D, Val, Error>( + delimiter: D, + parser: P, + to_element_error: V, +) -> impl Parser<'a, Vec<'a, Val>, Error> +where + D: Parser<'a, (), Error>, + P: Parser<'a, Val, Error>, + V: Fn(Position) -> Error, + Error: 'a, +{ + move |arena, state: State<'a>, min_indent: u32| { + let original_state = state.clone(); + let start_bytes_len = state.bytes().len(); + + match parser.parse(arena, state, min_indent) { + Ok((progress, first_output, next_state)) => { + debug_assert_eq!(progress, MadeProgress); + let mut state = next_state; + let mut buf = Vec::with_capacity_in(1, arena); + + buf.push(first_output); + + loop { + let old_state = state.clone(); + match delimiter.parse(arena, state, min_indent) { + Ok((_, (), next_state)) => { + // If the delimiter passed, check the element parser. + match parser.parse(arena, next_state.clone(), min_indent) { + Ok((_, next_output, next_state)) => { + state = next_state; + buf.push(next_output); + } + Err((MadeProgress, fail)) => { + return Err((MadeProgress, fail)); + } + Err((NoProgress, _fail)) => { + return Err((NoProgress, to_element_error(next_state.pos()))); + } + } + } + Err((delim_progress, fail)) => { + match delim_progress { + MadeProgress => { + // fail if the delimiter made progress + return Err((MadeProgress, fail)); + } + NoProgress => { + let progress = Progress::from_lengths( + start_bytes_len, + old_state.bytes().len(), + ); + return Ok((progress, buf, old_state)); + } + } + } + } + } + } + + Err((MadeProgress, fail)) => Err((MadeProgress, fail)), + Err((NoProgress, _fail)) => Err((NoProgress, to_element_error(original_state.pos()))), + } + } +} + +pub fn optional<'a, P, T, E>(parser: P) -> impl Parser<'a, Option, E> +where + P: Parser<'a, T, E>, + E: 'a, +{ + move |arena: &'a Bump, state: State<'a>, min_indent: u32| { + // We have to clone this because if the optional parser fails, + // we need to revert back to the original state. + let original_state = state.clone(); + + match parser.parse(arena, state, min_indent) { + Ok((progress, out1, state)) => Ok((progress, Some(out1), state)), + Err((MadeProgress, e)) => Err((MadeProgress, e)), + Err((NoProgress, _)) => Ok((NoProgress, None, original_state)), + } + } +} + +// MACRO COMBINATORS +// +// Using some combinators together results in combinatorial type explosion +// which makes things take forever to compile. Using macros instead avoids this! + +#[macro_export] +macro_rules! loc { + ($parser:expr) => { + move |arena, state: $crate::state::State<'a>, min_indent: u32| { + use roc_region::all::{Loc, Region}; + + let start = state.pos(); + + match $parser.parse(arena, state, min_indent) { + Ok((progress, value, state)) => { + let end = state.pos(); + let region = Region::new(start, end); + + Ok((progress, Loc { region, value }, state)) + } + Err(err) => Err(err), + } + } + }; +} + +/// If the first one parses, ignore its output and move on to parse with the second one. +#[macro_export] +macro_rules! skip_first { + ($p1:expr, $p2:expr) => { + move |arena, state: $crate::state::State<'a>, min_indent: u32| match $p1 + .parse(arena, state, min_indent) + { + Ok((p1, _, state)) => match $p2.parse(arena, state, min_indent) { + Ok((p2, out2, state)) => Ok((p1.or(p2), out2, state)), + Err((p2, fail)) => Err((p1.or(p2), fail)), + }, + Err((progress, fail)) => Err((progress, fail)), + } + }; +} + +/// If the first one parses, parse the second one; if it also parses, use the +/// output from the first one. +#[macro_export] +macro_rules! skip_second { + ($p1:expr, $p2:expr) => { + move |arena, state: $crate::state::State<'a>, min_indent: u32| match $p1 + .parse(arena, state, min_indent) + { + Ok((p1, out1, state)) => match $p2.parse(arena, state, min_indent) { + Ok((p2, _, state)) => Ok((p1.or(p2), out1, state)), + Err((p2, fail)) => Err((p1.or(p2), fail)), + }, + Err((progress, fail)) => Err((progress, fail)), + } + }; +} + +#[macro_export] +macro_rules! collection_inner { + ($elem:expr, $delimiter:expr, $space_before:expr) => { + map_with_arena!( + and!( + and!( + $crate::blankspace::spaces(), + $crate::parser::trailing_sep_by0( + $delimiter, + $crate::blankspace::spaces_before_optional_after($elem,) + ) + ), + $crate::blankspace::spaces() + ), + |arena: &'a bumpalo::Bump, + ((spaces, mut parsed_elems), mut final_comments): ( + ( + &'a [$crate::ast::CommentOrNewline<'a>], + bumpalo::collections::vec::Vec<'a, Loc<_>> + ), + &'a [$crate::ast::CommentOrNewline<'a>] + )| { + if !spaces.is_empty() { + if let Some(first) = parsed_elems.first_mut() { + first.value = $space_before(arena.alloc(first.value), spaces) + } else { + debug_assert!(final_comments.is_empty()); + final_comments = spaces; + } + } + + $crate::ast::Collection::with_items_and_comments( + arena, + parsed_elems.into_bump_slice(), + final_comments, + ) + } + ) + }; +} + +#[macro_export] +macro_rules! collection_trailing_sep_e { + ($opening_brace:expr, $elem:expr, $delimiter:expr, $closing_brace:expr, $space_before:expr) => { + between!( + $opening_brace, + $crate::parser::reset_min_indent($crate::collection_inner!( + $elem, + $delimiter, + $space_before + )), + $closing_brace + ) + }; +} + +#[macro_export] +macro_rules! succeed { + ($value:expr) => { + move |_arena: &'a bumpalo::Bump, state: $crate::state::State<'a>, _min_indent: u32| { + Ok((NoProgress, $value, state)) + } + }; +} + +pub fn fail_when<'a, T, T2, E, F, P>(f: F, p: P) -> impl Parser<'a, T, E> +where + T: 'a, + T2: 'a, + E: 'a, + F: Fn(Position) -> E, + P: Parser<'a, T2, E>, +{ + move |arena: &'a bumpalo::Bump, state: State<'a>, min_indent: u32| { + let original_state = state.clone(); + match p.parse(arena, state, min_indent) { + Ok((_, _, _)) => Err((MadeProgress, f(original_state.pos()))), + Err((progress, err)) => Err((progress, err)), + } + } +} + +pub fn fail<'a, T, E, F>(f: F) -> impl Parser<'a, T, E> +where + T: 'a, + E: 'a, + F: Fn(Position) -> E, +{ + move |_arena: &'a bumpalo::Bump, state: State<'a>, _min_indent: u32| { + Err((NoProgress, f(state.pos()))) + } +} + +#[macro_export] +macro_rules! and { + ($p1:expr, $p2:expr) => { + move |arena: &'a bumpalo::Bump, state: $crate::state::State<'a>, min_indent: u32| match $p1 + .parse(arena, state, min_indent) + { + Ok((p1, out1, state)) => match $p2.parse(arena, state, min_indent) { + Ok((p2, out2, state)) => Ok((p1.or(p2), (out1, out2), state)), + Err((p2, fail)) => Err((p1.or(p2), fail)), + }, + Err((progress, fail)) => Err((progress, fail)), + } + }; +} + +/// Take as input something that looks like a struct literal where values are parsers +/// and return a parser that runs each parser and returns a struct literal with the +/// results. +#[macro_export] +macro_rules! record { + ($name:ident $(:: $name_ext:ident)* { $($field:ident: $parser:expr),* $(,)? }) => { + move |arena: &'a bumpalo::Bump, state: $crate::state::State<'a>, min_indent: u32| { + let mut state = state; + let mut progress = NoProgress; + $( + let (new_progress, $field, new_state) = $parser.parse(arena, state, min_indent)?; + state = new_state; + progress = progress.or(new_progress); + )* + Ok((progress, $name $(:: $name_ext)* { $($field),* }, state)) + } + }; +} + +/// Similar to `and`, but we modify the min_indent of the second parser to be +/// 1 greater than the line_indent() at the start of the first parser. +#[macro_export] +macro_rules! indented_seq { + ($p1:expr, $p2:expr) => { + move |arena: &'a bumpalo::Bump, state: $crate::state::State<'a>, _min_indent: u32| { + let start_indent = state.line_indent(); + + // TODO: we should account for min_indent here, but this doesn't currently work + // because min_indent is sometimes larger than it really should be, which is in turn + // due to uses of `increment_indent`. + // + // let p1_indent = std::cmp::max(start_indent, min_indent); + + let p1_indent = start_indent; + let p2_indent = p1_indent + 1; + + match $p1.parse(arena, state, p1_indent) { + Ok((p1, (), state)) => match $p2.parse(arena, state, p2_indent) { + Ok((p2, out2, state)) => Ok((p1.or(p2), out2, state)), + Err((p2, fail)) => Err((p1.or(p2), fail)), + }, + Err((progress, fail)) => Err((progress, fail)), + } + } + }; +} + +/// Similar to `and`, but we modify the min_indent of the second parser to be +/// 1 greater than the column() at the start of the first parser. +#[macro_export] +macro_rules! absolute_indented_seq { + ($p1:expr, $p2:expr) => { + move |arena: &'a bumpalo::Bump, state: $crate::state::State<'a>, _min_indent: u32| { + let start_indent = state.column(); + + let p1_indent = start_indent; + let p2_indent = p1_indent + 1; + + match $p1.parse(arena, state, p1_indent) { + Ok((p1, out1, state)) => match $p2.parse(arena, state, p2_indent) { + Ok((p2, out2, state)) => Ok((p1.or(p2), (out1, out2), state)), + Err((p2, fail)) => Err((p1.or(p2), fail)), + }, + Err((progress, fail)) => Err((progress, fail)), + } + } + }; +} + +#[macro_export] +macro_rules! one_of { + ($p1:expr, $p2:expr) => { + move |arena: &'a bumpalo::Bump, state: $crate::state::State<'a>, min_indent: u32| { + let original_state = state.clone(); + + match $p1.parse(arena, state, min_indent) { + valid @ Ok(_) => valid, + Err((MadeProgress, fail)) => Err((MadeProgress, fail)), + Err((NoProgress, _)) => $p2.parse(arena, original_state, min_indent), + } + } + }; + + ($p1:expr, $($others:expr),+) => { + one_of!($p1, one_of!($($others),+)) + }; + ($p1:expr, $($others:expr),+ $(,)?) => { + one_of!($p1, $($others),+) + }; +} + +#[macro_export] +macro_rules! one_of_with_error { + ($toerror:expr; $p1:expr) => { + move |arena: &'a bumpalo::Bump, state: $crate::state::State<'a>, min_indent: u32| { + let original_state = state.clone(); + + match $p1.parse(arena, state, min_indent) { + valid @ Ok(_) => valid, + Err((MadeProgress, fail)) => Err((MadeProgress, fail)), + Err((NoProgress, _)) => Err((MadeProgress, $toerror(original_state.pos()))), + } + } + }; + + ($toerror:expr; $p1:expr, $($others:expr),+) => { + one_of_with_error!($toerror, $p1, one_of_with_error!($($others),+)) + }; +} + +pub fn reset_min_indent<'a, P, T, X: 'a>(parser: P) -> impl Parser<'a, T, X> +where + P: Parser<'a, T, X>, +{ + move |arena, state, _min_indent| parser.parse(arena, state, 0) +} + +pub fn set_min_indent<'a, P, T, X: 'a>(min_indent: u32, parser: P) -> impl Parser<'a, T, X> +where + P: Parser<'a, T, X>, +{ + move |arena, state, _m| parser.parse(arena, state, min_indent) +} + +pub fn increment_min_indent<'a, P, T, X: 'a>(parser: P) -> impl Parser<'a, T, X> +where + P: Parser<'a, T, X>, +{ + move |arena, state, min_indent| parser.parse(arena, state, min_indent + 1) +} + +pub fn line_min_indent<'a, P, T, X: 'a>(parser: P) -> impl Parser<'a, T, X> +where + P: Parser<'a, T, X>, +{ + move |arena, state: State<'a>, min_indent| { + let min_indent = std::cmp::max(state.line_indent(), min_indent); + parser.parse(arena, state, min_indent) + } +} + +pub fn absolute_column_min_indent<'a, P, T, X: 'a>(parser: P) -> impl Parser<'a, T, X> +where + P: Parser<'a, T, X>, +{ + move |arena, state: State<'a>, _min_indent| { + let min_indent = state.column() + 1; + parser.parse(arena, state, min_indent) + } +} + +pub fn specialize<'a, F, P, T, X, Y>(map_error: F, parser: P) -> impl Parser<'a, T, Y> +where + F: Fn(X, Position) -> Y, + P: Parser<'a, T, X>, + Y: 'a, +{ + move |a, state: State<'a>, min_indent| { + let original_state = state.clone(); + match parser.parse(a, state, min_indent) { + Ok(t) => Ok(t), + Err((p, error)) => Err((p, map_error(error, original_state.pos()))), + } + } +} + +pub fn specialize_ref<'a, F, P, T, X, Y>(map_error: F, parser: P) -> impl Parser<'a, T, Y> +where + F: Fn(&'a X, Position) -> Y, + P: Parser<'a, T, X>, + Y: 'a, + X: 'a, +{ + move |a, state: State<'a>, min_indent| { + let original_state = state.clone(); + match parser.parse(a, state, min_indent) { + Ok(t) => Ok(t), + Err((p, error)) => Err((p, map_error(a.alloc(error), original_state.pos()))), + } + } +} + +pub fn word<'a, ToError, E>(word: &'static str, to_error: ToError) -> impl Parser<'a, (), E> +where + ToError: Fn(Position) -> E, + E: 'a, +{ + debug_assert!(!word.contains('\n')); + + move |_arena: &'a Bump, state: State<'a>, _min_indent: u32| { + if state.bytes().starts_with(word.as_bytes()) { + let state = state.advance(word.len()); + Ok((MadeProgress, (), state)) + } else { + Err((NoProgress, to_error(state.pos()))) + } + } +} + +pub fn word1<'a, ToError, E>(word: u8, to_error: ToError) -> impl Parser<'a, (), E> +where + ToError: Fn(Position) -> E, + E: 'a, +{ + debug_assert_ne!(word, b'\n'); + + move |_arena: &'a Bump, state: State<'a>, _min_indent: u32| match state.bytes().first() { + Some(x) if *x == word => { + let state = state.advance(1); + Ok((MadeProgress, (), state)) + } + _ => Err((NoProgress, to_error(state.pos()))), + } +} + +pub fn word1_indent<'a, ToError, E>(word: u8, to_error: ToError) -> impl Parser<'a, (), E> +where + ToError: Fn(Position) -> E, + E: 'a, +{ + debug_assert_ne!(word, b'\n'); + + move |_arena: &'a Bump, state: State<'a>, min_indent: u32| { + if min_indent > state.column() { + return Err((NoProgress, to_error(state.pos()))); + } + + match state.bytes().first() { + Some(x) if *x == word => { + let state = state.advance(1); + Ok((MadeProgress, (), state)) + } + _ => Err((NoProgress, to_error(state.pos()))), + } + } +} + +pub fn word2<'a, ToError, E>(word_1: u8, word_2: u8, to_error: ToError) -> impl Parser<'a, (), E> +where + ToError: Fn(Position) -> E, + E: 'a, +{ + debug_assert_ne!(word_1, b'\n'); + debug_assert_ne!(word_2, b'\n'); + + let needle = [word_1, word_2]; + + move |_arena: &'a Bump, state: State<'a>, _min_indent: u32| { + if state.bytes().starts_with(&needle) { + let state = state.advance(2); + Ok((MadeProgress, (), state)) + } else { + Err((NoProgress, to_error(state.pos()))) + } + } +} + +pub fn word3<'a, ToError, E>( + word_1: u8, + word_2: u8, + word_3: u8, + to_error: ToError, +) -> impl Parser<'a, (), E> +where + ToError: Fn(Position) -> E, + E: 'a, +{ + debug_assert_ne!(word_1, b'\n'); + debug_assert_ne!(word_2, b'\n'); + debug_assert_ne!(word_3, b'\n'); + + let needle = [word_1, word_2, word_3]; + + move |_arena: &'a Bump, state: State<'a>, _min_indent: u32| { + if state.bytes().starts_with(&needle) { + let state = state.advance(3); + Ok((MadeProgress, (), state)) + } else { + Err((NoProgress, to_error(state.pos()))) + } + } +} + +#[macro_export] +macro_rules! word1_check_indent { + ($word:expr, $word_problem:expr, $min_indent:expr, $indent_problem:expr) => { + and!( + word1($word, $word_problem), + $crate::parser::check_indent($min_indent, $indent_problem) + ) + }; +} + +#[macro_export] +macro_rules! map { + ($parser:expr, $transform:expr) => { + move |arena, state, min_indent| { + #[allow(clippy::redundant_closure_call)] + $parser + .parse(arena, state, min_indent) + .map(|(progress, output, next_state)| (progress, $transform(output), next_state)) + } + }; +} + +#[macro_export] +macro_rules! map_with_arena { + ($parser:expr, $transform:expr) => { + move |arena, state, min_indent| { + #[allow(clippy::redundant_closure_call)] + $parser + .parse(arena, state, min_indent) + .map(|(progress, output, next_state)| { + (progress, $transform(arena, output), next_state) + }) + } + }; +} + +#[macro_export] +macro_rules! zero_or_more { + ($parser:expr) => { + move |arena, state: State<'a>, min_indent: u32| { + use bumpalo::collections::Vec; + + let original_state = state.clone(); + + let start_bytes_len = state.bytes().len(); + + match $parser.parse(arena, state, min_indent) { + Ok((_, first_output, next_state)) => { + let mut state = next_state; + let mut buf = Vec::with_capacity_in(1, arena); + + buf.push(first_output); + + loop { + let old_state = state.clone(); + match $parser.parse(arena, state, min_indent) { + Ok((_, next_output, next_state)) => { + state = next_state; + buf.push(next_output); + } + Err((fail_progress, fail)) => { + match fail_progress { + MadeProgress => { + // made progress on an element and then failed; that's an error + return Err((MadeProgress, fail)); + } + NoProgress => { + // the next element failed with no progress + // report whether we made progress before + let progress = Progress::from_lengths(start_bytes_len, old_state.bytes().len()); + return Ok((progress, buf, old_state)); + } + } + } + } + } + } + Err((fail_progress, fail)) => { + match fail_progress { + MadeProgress => { + // made progress on an element and then failed; that's an error + Err((MadeProgress, fail)) + } + NoProgress => { + // the first element failed (with no progress), but that's OK + // because we only need to parse 0 elements + Ok((NoProgress, Vec::new_in(arena), original_state)) + } + } + } + } + } + }; +} + +#[macro_export] +macro_rules! one_or_more { + ($parser:expr, $to_error:expr) => { + move |arena, state: State<'a>, min_indent: u32| { + use bumpalo::collections::Vec; + + match $parser.parse(arena, state.clone(), min_indent) { + Ok((_, first_output, next_state)) => { + let mut state = next_state; + let mut buf = Vec::with_capacity_in(1, arena); + + buf.push(first_output); + + loop { + let old_state = state.clone(); + match $parser.parse(arena, state, min_indent) { + Ok((_, next_output, next_state)) => { + state = next_state; + buf.push(next_output); + } + Err((NoProgress, _)) => { + return Ok((MadeProgress, buf, old_state)); + } + Err((MadeProgress, fail)) => { + return Err((MadeProgress, fail)); + } + } + } + } + Err((progress, _)) => Err((progress, $to_error(state.pos()))), + } + } + }; +} + +#[macro_export] +macro_rules! debug { + ($parser:expr) => { + move |arena, state: $crate::state::State<'a>, min_indent: u32| { + dbg!($parser.parse(arena, state, min_indent)) + } + }; +} + +#[macro_export] +macro_rules! either { + ($p1:expr, $p2:expr) => { + move |arena: &'a bumpalo::Bump, state: $crate::state::State<'a>, min_indent: u32| { + let original_state = state.clone(); + match $p1.parse(arena, state, min_indent) { + Ok((progress, output, state)) => { + Ok((progress, $crate::parser::Either::First(output), state)) + } + Err((NoProgress, _)) => { + match $p2.parse(arena, original_state.clone(), min_indent) { + Ok((progress, output, state)) => { + Ok((progress, $crate::parser::Either::Second(output), state)) + } + Err((progress, fail)) => Err((progress, fail)), + } + } + Err((MadeProgress, fail)) => Err((MadeProgress, fail)), + } + } + }; +} + +/// Parse everything between two braces (e.g. parentheses), skipping both braces +/// and keeping only whatever was parsed in between them. +#[macro_export] +macro_rules! between { + ($opening_brace:expr, $parser:expr, $closing_brace:expr) => { + skip_first!($opening_brace, skip_second!($parser, $closing_brace)) + }; +} + +/// For some reason, some usages won't compile unless they use this instead of the macro version +#[inline(always)] +pub fn and<'a, P1, P2, A, B, E>(p1: P1, p2: P2) -> impl Parser<'a, (A, B), E> +where + P1: Parser<'a, A, E>, + P2: Parser<'a, B, E>, + P1: 'a, + P2: 'a, + A: 'a, + B: 'a, + E: 'a, +{ + and!(p1, p2) +} + +/// For some reason, some usages won't compile unless they use this instead of the macro version +#[inline(always)] +pub fn loc<'a, P, Val, Error>(parser: P) -> impl Parser<'a, Loc, Error> +where + P: Parser<'a, Val, Error>, + Error: 'a, +{ + loc!(parser) +} + +/// For some reason, some usages won't compile unless they use this instead of the macro version +#[inline(always)] +pub fn map_with_arena<'a, P, F, Before, After, E>( + parser: P, + transform: F, +) -> impl Parser<'a, After, E> +where + P: Parser<'a, Before, E>, + P: 'a, + F: Fn(&'a Bump, Before) -> After, + F: 'a, + Before: 'a, + After: 'a, + E: 'a, +{ + map_with_arena!(parser, transform) +} + +pub fn backtrackable<'a, P, Val, Error>(parser: P) -> impl Parser<'a, Val, Error> +where + P: Parser<'a, Val, Error>, + Error: 'a, +{ + move |arena: &'a Bump, state: State<'a>, min_indent: u32| match parser + .parse(arena, state, min_indent) + { + Ok((_, a, s1)) => Ok((NoProgress, a, s1)), + Err((_, f)) => Err((NoProgress, f)), + } +} diff --git a/crates/compiler/parse/src/pattern.rs b/crates/compiler/parse/src/pattern.rs new file mode 100644 index 0000000000..19afb719b9 --- /dev/null +++ b/crates/compiler/parse/src/pattern.rs @@ -0,0 +1,570 @@ +use crate::ast::{Implements, Pattern, PatternAs, Spaceable}; +use crate::blankspace::{space0_e, spaces, spaces_before}; +use crate::ident::{lowercase_ident, parse_ident, Accessor, Ident}; +use crate::keyword; +use crate::parser::Progress::{self, *}; +use crate::parser::{ + self, backtrackable, fail_when, optional, specialize, specialize_ref, then, word1, word2, + word3, EPattern, PInParens, PList, PRecord, Parser, +}; +use crate::state::State; +use crate::string_literal::StrLikeLiteral; +use bumpalo::collections::string::String; +use bumpalo::collections::Vec; +use bumpalo::Bump; +use roc_region::all::{Loc, Region}; + +/// Different patterns are supported in different circumstances. +/// For example, when branches can pattern match on number literals, but +/// assignments and function args can't. Underscore is supported in function +/// arg patterns and in when branch patterns, but not in assignments. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum PatternType { + TopLevelDef, + DefExpr, + FunctionArg, + WhenBranch, +} + +pub fn closure_param<'a>() -> impl Parser<'a, Loc>, EPattern<'a>> { + one_of!( + // An ident is the most common param, e.g. \foo -> ... + loc_ident_pattern_help(true), + // Underscore is also common, e.g. \_ -> ... + loc!(underscore_pattern_help()), + // You can destructure records in params, e.g. \{ x, y } -> ... + loc!(specialize( + EPattern::Record, + crate::pattern::record_pattern_help() + )), + // If you wrap it in parens, you can match any arbitrary pattern at all. + // e.g. \User.UserId userId -> ... + specialize(EPattern::PInParens, loc_pattern_in_parens_help()) + ) +} + +pub fn loc_pattern_help<'a>() -> impl Parser<'a, Loc>, EPattern<'a>> { + move |arena, state: State<'a>, min_indent| { + let (_, pattern, state) = loc_pattern_help_help().parse(arena, state, min_indent)?; + + let pattern_state = state.clone(); + + let (pattern_spaces, state) = + match space0_e(EPattern::AsKeyword).parse(arena, state, min_indent) { + Err(_) => return Ok((MadeProgress, pattern, pattern_state)), + Ok((_, pattern_spaces, state)) => (pattern_spaces, state), + }; + + match pattern_as().parse(arena, state, min_indent) { + Err((progress, e)) => match progress { + MadeProgress => Err((MadeProgress, e)), + NoProgress => Ok((MadeProgress, pattern, pattern_state)), + }, + Ok((_, pattern_as, state)) => { + let region = Region::span_across(&pattern.region, &pattern_as.identifier.region); + let pattern = arena + .alloc(pattern.value) + .with_spaces_after(pattern_spaces, pattern.region); + let as_pattern = Pattern::As(arena.alloc(pattern), pattern_as); + + Ok((MadeProgress, Loc::at(region, as_pattern), state)) + } + } + } +} + +fn loc_pattern_help_help<'a>() -> impl Parser<'a, Loc>, EPattern<'a>> { + one_of!( + specialize(EPattern::PInParens, loc_pattern_in_parens_help()), + loc!(underscore_pattern_help()), + loc_ident_pattern_help(true), + loc!(specialize( + EPattern::Record, + crate::pattern::record_pattern_help() + )), + loc!(specialize(EPattern::List, list_pattern_help())), + loc!(number_pattern_help()), + loc!(string_like_pattern_help()), + ) +} + +fn pattern_as<'a>() -> impl Parser<'a, PatternAs<'a>, EPattern<'a>> { + move |arena, state: State<'a>, min_indent| { + let (_, _, state) = + parser::keyword_e(keyword::AS, EPattern::AsKeyword).parse(arena, state, min_indent)?; + + let (_, spaces, state) = + space0_e(EPattern::AsIdentifier).parse(arena, state, min_indent)?; + + let position = state.pos(); + + match loc!(lowercase_ident()).parse(arena, state, min_indent) { + Ok((_, identifier, state)) => Ok(( + MadeProgress, + PatternAs { + spaces_before: spaces, + identifier, + }, + state, + )), + Err((_, ())) => Err((MadeProgress, EPattern::AsIdentifier(position))), + } + } +} + +fn loc_tag_pattern_args_help<'a>() -> impl Parser<'a, Vec<'a, Loc>>, EPattern<'a>> { + zero_or_more!(loc_tag_pattern_arg(false)) +} + +/// Like `loc_tag_pattern_args_help`, but stops if a "implements" keyword is seen (indicating an ability). +fn loc_type_def_tag_pattern_args_help<'a>( +) -> impl Parser<'a, Vec<'a, Loc>>, EPattern<'a>> { + zero_or_more!(loc_tag_pattern_arg(true)) +} + +fn loc_tag_pattern_arg<'a>( + stop_on_has_kw: bool, +) -> impl Parser<'a, Loc>, EPattern<'a>> { + // Don't parse operators, because they have a higher precedence than function application. + // If we encounter one, we're done parsing function args! + move |arena, original_state: State<'a>, min_indent| { + let (_, spaces, state) = backtrackable(space0_e(EPattern::IndentStart)).parse( + arena, + original_state.clone(), + min_indent, + )?; + + let (_, loc_pat, state) = loc_parse_tag_pattern_arg().parse(arena, state, min_indent)?; + + let Loc { region, value } = loc_pat; + + if stop_on_has_kw && matches!(value, Pattern::Identifier(crate::keyword::IMPLEMENTS)) { + Err((NoProgress, EPattern::End(original_state.pos()))) + } else { + Ok(( + MadeProgress, + if spaces.is_empty() { + Loc::at(region, value) + } else { + Loc::at(region, Pattern::SpaceBefore(arena.alloc(value), spaces)) + }, + state, + )) + } + } +} + +pub fn loc_implements_parser<'a>() -> impl Parser<'a, Loc>, EPattern<'a>> { + then( + loc_tag_pattern_arg(false), + |_arena, state, progress, pattern| { + if matches!( + pattern.value, + Pattern::Identifier(crate::keyword::IMPLEMENTS) + ) { + Ok(( + progress, + Loc::at(pattern.region, Implements::Implements), + state, + )) + } else { + Err((progress, EPattern::End(state.pos()))) + } + }, + ) +} + +fn loc_parse_tag_pattern_arg<'a>() -> impl Parser<'a, Loc>, EPattern<'a>> { + one_of!( + specialize(EPattern::PInParens, loc_pattern_in_parens_help()), + loc!(underscore_pattern_help()), + // Make sure `Foo Bar 1` is parsed as `Foo (Bar) 1`, and not `Foo (Bar 1)` + loc_ident_pattern_help(false), + loc!(specialize( + EPattern::Record, + crate::pattern::record_pattern_help() + )), + loc!(string_like_pattern_help()), + loc!(number_pattern_help()) + ) +} + +fn loc_pattern_in_parens_help<'a>() -> impl Parser<'a, Loc>, PInParens<'a>> { + then( + loc!(collection_trailing_sep_e!( + word1(b'(', PInParens::Open), + specialize_ref(PInParens::Pattern, loc_pattern_help()), + word1(b',', PInParens::End), + word1(b')', PInParens::End), + Pattern::SpaceBefore + )), + move |_arena, state, _, loc_elements| { + let elements = loc_elements.value; + let region = loc_elements.region; + + if elements.len() > 1 { + Ok(( + MadeProgress, + Loc::at(region, Pattern::Tuple(elements)), + state, + )) + } else if elements.is_empty() { + Err((NoProgress, PInParens::Empty(state.pos()))) + } else { + // TODO: don't discard comments before/after + // (stored in the Collection) + // TODO: add Pattern::ParensAround to faithfully represent the input + Ok((MadeProgress, elements.items[0], state)) + } + }, + ) + .trace("pat_in_parens") +} + +fn number_pattern_help<'a>() -> impl Parser<'a, Pattern<'a>, EPattern<'a>> { + specialize( + EPattern::NumLiteral, + map!(crate::number_literal::number_literal(), |literal| { + use crate::number_literal::NumLiteral::*; + + match literal { + Num(s) => Pattern::NumLiteral(s), + Float(s) => Pattern::FloatLiteral(s), + NonBase10Int { + string, + base, + is_negative, + } => Pattern::NonBase10Literal { + string, + base, + is_negative, + }, + } + }), + ) +} + +fn string_like_pattern_help<'a>() -> impl Parser<'a, Pattern<'a>, EPattern<'a>> { + specialize( + |_, pos| EPattern::Start(pos), + map_with_arena!( + crate::string_literal::parse_str_like_literal(), + |arena, lit| match lit { + StrLikeLiteral::Str(s) => Pattern::StrLiteral(s), + StrLikeLiteral::SingleQuote(s) => { + // TODO: preserve the original escaping + Pattern::SingleQuote(s.to_str_in(arena)) + } + } + ), + ) +} + +fn list_pattern_help<'a>() -> impl Parser<'a, Pattern<'a>, PList<'a>> { + map!( + collection_trailing_sep_e!( + word1(b'[', PList::Open), + list_element_pattern(), + word1(b',', PList::End), + word1(b']', PList::End), + Pattern::SpaceBefore + ), + Pattern::List + ) +} + +fn list_element_pattern<'a>() -> impl Parser<'a, Loc>, PList<'a>> { + one_of!( + three_list_rest_pattern_error(), + list_rest_pattern(), + specialize_ref(PList::Pattern, loc_pattern_help()), + ) +} + +fn three_list_rest_pattern_error<'a>() -> impl Parser<'a, Loc>, PList<'a>> { + fail_when(PList::Rest, loc!(word3(b'.', b'.', b'.', PList::Rest))) +} + +fn list_rest_pattern<'a>() -> impl Parser<'a, Loc>, PList<'a>> { + move |arena: &'a Bump, state: State<'a>, min_indent: u32| { + let (_, loc_word, state) = + loc!(word2(b'.', b'.', PList::Open)).parse(arena, state, min_indent)?; + + let no_as = Loc::at(loc_word.region, Pattern::ListRest(None)); + + let pattern_state = state.clone(); + + let (pattern_spaces, state) = + match space0_e(EPattern::AsKeyword).parse(arena, state, min_indent) { + Err(_) => return Ok((MadeProgress, no_as, pattern_state)), + Ok((_, pattern_spaces, state)) => (pattern_spaces, state), + }; + + let position = state.pos(); + match pattern_as().parse(arena, state, min_indent) { + Err((progress, e)) => match progress { + MadeProgress => Err((MadeProgress, PList::Pattern(arena.alloc(e), position))), + NoProgress => Ok((MadeProgress, no_as, pattern_state)), + }, + Ok((_, pattern_as, state)) => { + let region = Region::span_across(&loc_word.region, &pattern_as.identifier.region); + + let as_pattern = Pattern::ListRest(Some((pattern_spaces, pattern_as))); + + Ok((MadeProgress, Loc::at(region, as_pattern), state)) + } + } + } +} + +fn loc_ident_pattern_help<'a>( + can_have_arguments: bool, +) -> impl Parser<'a, Loc>, EPattern<'a>> { + move |arena: &'a Bump, state: State<'a>, min_indent: u32| { + let original_state = state.clone(); + + let (_, loc_ident, state) = specialize(|_, pos| EPattern::Start(pos), loc!(parse_ident)) + .parse(arena, state, min_indent)?; + + match loc_ident.value { + Ident::Tag(tag) => { + let loc_tag = Loc { + region: loc_ident.region, + value: Pattern::Tag(tag), + }; + + // Make sure `Foo Bar 1` is parsed as `Foo (Bar) 1`, and not `Foo (Bar 1)` + if can_have_arguments { + let (_, loc_args, state) = + loc_type_def_tag_pattern_args_help().parse(arena, state, min_indent)?; + + if loc_args.is_empty() { + Ok((MadeProgress, loc_tag, state)) + } else { + let region = Region::across_all( + std::iter::once(&loc_ident.region) + .chain(loc_args.iter().map(|loc_arg| &loc_arg.region)), + ); + let value = + Pattern::Apply(&*arena.alloc(loc_tag), loc_args.into_bump_slice()); + + Ok((MadeProgress, Loc { region, value }, state)) + } + } else { + Ok((MadeProgress, loc_tag, state)) + } + } + Ident::OpaqueRef(name) => { + let loc_pat = Loc { + region: loc_ident.region, + value: Pattern::OpaqueRef(name), + }; + + // Make sure `@Foo Bar 1` is parsed as `@Foo (Bar) 1`, and not `@Foo (Bar 1)` + if can_have_arguments { + let (_, loc_args, state) = + loc_tag_pattern_args_help().parse(arena, state, min_indent)?; + + if loc_args.is_empty() { + Ok((MadeProgress, loc_pat, state)) + } else { + let region = Region::across_all( + std::iter::once(&loc_ident.region) + .chain(loc_args.iter().map(|loc_arg| &loc_arg.region)), + ); + let value = + Pattern::Apply(&*arena.alloc(loc_pat), loc_args.into_bump_slice()); + + Ok((MadeProgress, Loc { region, value }, state)) + } + } else { + Ok((MadeProgress, loc_pat, state)) + } + } + Ident::Access { module_name, parts } => { + // Plain identifiers (e.g. `foo`) are allowed in patterns, but + // more complex ones (e.g. `Foo.bar` or `foo.bar.baz`) are not. + + for keyword in crate::keyword::KEYWORDS.iter() { + if parts[0] == Accessor::RecordField(keyword) { + return Err((NoProgress, EPattern::End(original_state.pos()))); + } + } + + if module_name.is_empty() && parts.len() == 1 { + if let Accessor::RecordField(var) = &parts[0] { + return Ok(( + MadeProgress, + Loc { + region: loc_ident.region, + value: Pattern::Identifier(var), + }, + state, + )); + } + } + let mut malformed_str = String::new_in(arena); + + if !module_name.is_empty() { + malformed_str.push_str(module_name); + }; + for part in parts { + if !malformed_str.is_empty() { + malformed_str.push('.'); + } + malformed_str.push_str(part.as_inner()); + } + + Ok(( + MadeProgress, + Loc { + region: loc_ident.region, + value: Pattern::Malformed(malformed_str.into_bump_str()), + }, + state, + )) + } + Ident::AccessorFunction(_string) => Err(( + MadeProgress, + EPattern::AccessorFunction(loc_ident.region.start()), + )), + Ident::Malformed(malformed, problem) => { + debug_assert!(!malformed.is_empty()); + + Ok(( + MadeProgress, + Loc { + region: loc_ident.region, + value: Pattern::MalformedIdent(malformed, problem), + }, + state, + )) + } + } + } +} + +fn underscore_pattern_help<'a>() -> impl Parser<'a, Pattern<'a>, EPattern<'a>> { + map!( + skip_first!( + word1(b'_', EPattern::Underscore), + optional(lowercase_ident_pattern()) + ), + |output| match output { + Some(name) => Pattern::Underscore(name), + None => Pattern::Underscore(""), + } + ) +} + +fn lowercase_ident_pattern<'a>() -> impl Parser<'a, &'a str, EPattern<'a>> { + specialize(move |_, pos| EPattern::End(pos), lowercase_ident()) +} + +#[inline(always)] +fn record_pattern_help<'a>() -> impl Parser<'a, Pattern<'a>, PRecord<'a>> { + map!( + collection_trailing_sep_e!( + word1(b'{', PRecord::Open), + record_pattern_field(), + word1(b',', PRecord::End), + word1(b'}', PRecord::End), + Pattern::SpaceBefore + ), + Pattern::RecordDestructure + ) +} + +fn record_pattern_field<'a>() -> impl Parser<'a, Loc>, PRecord<'a>> { + use crate::parser::Either::*; + + move |arena, state: State<'a>, min_indent: u32| { + // You must have a field name, e.g. "email" + // using the initial pos is important for error reporting + let pos = state.pos(); + let (progress, loc_label, state) = loc!(specialize( + move |_, _| PRecord::Field(pos), + lowercase_ident() + )) + .parse(arena, state, min_indent)?; + debug_assert_eq!(progress, MadeProgress); + + let (_, spaces, state) = spaces().parse(arena, state, min_indent)?; + + // Having a value is optional; both `{ email }` and `{ email: blah }` work. + // (This is true in both literals and types.) + let (_, opt_loc_val, state) = optional(either!( + word1(b':', PRecord::Colon), + word1(b'?', PRecord::Optional) + )) + .parse(arena, state, min_indent)?; + + match opt_loc_val { + Some(First(_)) => { + let val_parser = specialize_ref(PRecord::Pattern, loc_pattern_help()); + let (_, loc_val, state) = + spaces_before(val_parser).parse(arena, state, min_indent)?; + + let Loc { + value: label, + region, + } = loc_label; + + let region = Region::span_across(®ion, &loc_val.region); + + Ok(( + MadeProgress, + Loc::at( + region, + Pattern::RequiredField( + label, + // TODO spaces are dropped here + // arena.alloc(arena.alloc(value).with_spaces_before(spaces, region)), + arena.alloc(loc_val), + ), + ), + state, + )) + } + Some(Second(_)) => { + let val_parser = specialize_ref(PRecord::Expr, crate::expr::loc_expr(false)); + + let (_, loc_val, state) = + spaces_before(val_parser).parse(arena, state, min_indent)?; + + let Loc { + value: label, + region, + } = loc_label; + + let region = Region::span_across(®ion, &loc_val.region); + + Ok(( + MadeProgress, + Loc::at( + region, + Pattern::OptionalField( + label, + // TODO spaces are dropped + // arena.alloc(arena.alloc(value).with_spaces_before(spaces, region)), + arena.alloc(loc_val), + ), + ), + state, + )) + } + // If no value was provided, record it as a Var. + // Canonicalize will know what to do with a Var later. + None => { + let Loc { value, region } = loc_label; + let value = if !spaces.is_empty() { + Pattern::SpaceAfter(arena.alloc(Pattern::Identifier(value)), spaces) + } else { + Pattern::Identifier(value) + }; + + Ok((MadeProgress, Loc::at(region, value), state)) + } + } + } +} diff --git a/compiler/parse/src/problems.rs b/crates/compiler/parse/src/problems.rs similarity index 100% rename from compiler/parse/src/problems.rs rename to crates/compiler/parse/src/problems.rs diff --git a/crates/compiler/parse/src/src64.rs b/crates/compiler/parse/src/src64.rs new file mode 100644 index 0000000000..8aef931e9c --- /dev/null +++ b/crates/compiler/parse/src/src64.rs @@ -0,0 +1,561 @@ +// Loads Roc source files (from strings or from files) into a structure which is +// guaranteed to have the following properties, all of which the SIMD parser requires: +// - 16B alignment +// - byte length is a multiple of 64 +// - if the source bytes were not a multiple of 64, the extra space is filled with trailing newlines +// +// (Trailing newlines are the filler of choice because they are irrelevant to the parser.) +// +// It does this as efficiently as possible by using branchless SIMD to fill padding bytes, +// and reading the contents of the file directly into an arena in as few syscalls as possible. + +use bumpalo::{self, Bump}; +use core::{ + alloc::Layout, + mem::{align_of, MaybeUninit}, + ptr::{self, NonNull}, +}; + +#[cfg(not(test))] +/// We store both line and column numbers as u16s, so the largest possible file you could open +/// would be every line having the longest possible column length, or u16::MAX * u16::MAX. +const MAX_ROC_SOURCE_FILE_SIZE: usize = u16::MAX as usize * u16::MAX as usize; // 4GB + +#[cfg(test)] +const MAX_ROC_SOURCE_FILE_SIZE: usize = 1024; // small enough that we can create a tempfile to exercise this scenario + +pub struct Src64<'a> { + /// These bytes are guaranteed to have a 16B-aligned address (so the parser can do 128-bit SIMD on it). + /// This slice is guaranteed to have a length that's a multiple of 64B, because the parser iterates in + /// chunks of 64B. (If extra bytes are needed to make it a multiple of 64B, we add trailing newlines + /// because the parser ignores those.) + bytes: &'a [u8], +} + +#[derive(Debug, Copy, Clone, PartialEq)] +pub enum FileErr { + FileWasEmpty, + ReadErr, + FileWasTooBig(usize), + ErrReadingFileSize, + FileOpenFailed, +} + +impl<'a> Src64<'a> { + const BYTES_ALIGNMENT: usize = 64; + + /// The underlying source bytes that originally came from a file or from a string. + /// + /// These bytes are guaranteed to have a 16B-aligned address (so the parser can do 128-bit SIMD on it). + /// This slice is guaranteed to have a length that's a multiple of 64B, because the parser iterates in + /// chunks of 64B. (If extra bytes are needed to make it a multiple of 64B, we add trailing newlines + /// because the parser ignores those.) + pub fn bytes(&self) -> &[u8] { + self.bytes + } + + pub fn len(&self) -> usize { + self.bytes.len() + } + + pub fn is_empty(&self) -> bool { + self.bytes.is_empty() + } + + /// Returns None if the given string exceeds the maximum size of a Roc source file. + pub fn from_str(arena: &'a Bump, src: &'a str) -> Option> { + let src_len = src.len(); + + if src_len == 0 { + return None; + } + + let capacity = round_up_to_nearest_64(src_len); + + debug_assert_eq!(capacity % 64, 0); + + if capacity == src_len && src.as_ptr().align_offset(Self::BYTES_ALIGNMENT) == 0 { + // If the string already happens to meet our capacity and alignment requirements, just return it. + return Some(Self { + bytes: src.as_bytes(), + }); + } + + // Safety: we got capacity by rounding up to the nearest 64B + let dest = unsafe { allocate_chunks(arena, capacity)? }.as_ptr() as *mut u8; + + // Safety: `dest` has a length of `capacity`, which has been rounded up to a multiple of 64. + unsafe { + let trailing_newlines_needed = capacity - src_len; + + // Start writing newlines right after the last of the bytes we got from the file. + write_newlines(dest.add(src_len), trailing_newlines_needed); + }; + + // Safety: we just allocated `dest` to have len >= src.len(), and they're both u8 arrays. + unsafe { + ptr::copy_nonoverlapping(src.as_bytes().as_ptr(), dest, src_len); + } + + Some(Self { + // Safety: all the bytes should now be initialized + bytes: unsafe { core::slice::from_raw_parts_mut(dest, capacity) }, + }) + } + + #[cfg(any(unix, windows))] // This is not available on wasm32. We could make it work with WASI if desired. + pub fn from_file(arena: &'a Bump, path: &std::path::Path) -> Result { + use core::ffi::c_void; + + let file = match std::fs::File::open(path) { + Ok(file) => file, + Err(_) => { + return Err(FileErr::FileOpenFailed); + } + }; + + let file_size = match file.metadata() { + Ok(metadata) => { + #[cfg(unix)] + { + use std::os::unix::prelude::MetadataExt; + + metadata.size() as usize + } + + #[cfg(windows)] + { + use std::os::windows::prelude::MetadataExt; + + metadata.file_size() as usize + } + } + Err(_io_err) => { + return Err(FileErr::ErrReadingFileSize); + } + }; + + if file_size == 0 { + return Err(FileErr::FileWasEmpty); + } + + let capacity = round_up_to_nearest_64(file_size); + + // Safety: round_up_to_nearest_u64 will give us a capacity that is + // at least 64, and also a multiple of 64. + match unsafe { allocate_chunks(arena, capacity) } { + Some(buf) => { + // Read bytes equal to file_size into the arena allocation. + // + // We use the native OS read() operation here to avoid UB; file.read_exact() + // only reads into a slice, and constructing a slice with uninitialized + // data is UB (per the slice::from_raw_parts docs). The allocation is uninitialized here, + // and initializing it would be a waste of CPU cycles because we're about to overwrite + // those bytes with bytes from the file anyway. + let bytes_read = { + #[cfg(unix)] + unsafe { + use std::os::fd::AsRawFd; + + // This extern lets us avoid an entire libc crate dependency. + extern "C" { + // https://linux.die.net/man/2/read + pub fn read( + fd: core::ffi::c_int, + buf: *mut c_void, + count: usize, + ) -> isize; + } + + read(file.as_raw_fd(), buf.as_ptr() as *mut c_void, file_size) as usize + } + + #[cfg(windows)] + unsafe { + use std::os::windows::io::AsRawHandle; + + // This extern lets us avoid an entire winapi crate dependency. + extern "system" { + // https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-readfile + pub fn ReadFile( + hFile: *mut c_void, + lpBuffer: *mut c_void, + nNumberOfBytesToRead: u32, + lpNumberOfBytesRead: *mut u32, + lpOverlapped: *mut c_void, // this should be a pointer to a struct, but we always pass null. + ) -> i32; + } + + let mut bytes_read = core::mem::MaybeUninit::uninit(); + + // We should have already errored out if file_size exceeded u32::MAX, + // due to our maximum source file size. This debug_assert! is here to + // make sure casting file_size to u32 is safe in the ReadFile call. + debug_assert!(MAX_ROC_SOURCE_FILE_SIZE <= u32::MAX as usize); + + ReadFile( + file.as_raw_handle() as *mut c_void, + buf.as_ptr() as *mut c_void, + file_size as u32, + bytes_read.as_mut_ptr(), + core::ptr::null_mut(), + ); + + bytes_read.assume_init() as usize + } + }; + + // We can close the file now; we're done with it. + drop(file); + + // It's crucial that we successfully read the entire file; otherwise, it would be unsafe + // to make a slice out of it because we might not have overwritten the uninitialized + // memory leading up to the newlines at the end! + // + // Note that on UNIX, bytes_read might be -1 if this was a file read error. This + // condition will catch that too, since we know file_size won't be (-1isize as usize) + // beacuse if it was, then this match would have taken the None branch due to + // (-1isize as usize) exceeding our maximum file size. + if bytes_read != file_size { + return Err(FileErr::ReadErr); + } + + // Before we write newlines to the last chunk, branchlessly prefetch the first four 64-byte chunks. + // We're about to have a cache miss due to loading the last chunk from main memory (DMA will have + // written it there without having gone through the CPU), and if we don't prefetch here, then we'll + // immediately get a second cache miss when we start traversing the loaded file. The prefetch means + // by the time we finish resolving the first cache miss on the last chunk, continuing with the first + // chunk(s) won't be a cache miss anymore because they'll already be in cache. + // + // We can do further prefetches in the actual tokenization loop. + { + // We know capacity >= 64, so this will never wrap. + let last_chunk_offset = capacity - 64; + + // Prefetch the first 64-byte chunk. + prefetch_read(buf, 0); + + // Prefetch the second 64-byte chunk, using min() to branchlessly avoid prefetching an address we might not own. + prefetch_read(buf, 64.min(last_chunk_offset)); + + // Prefetch the third 64-byte chunk, using min() to branchlessly avoid prefetching an address we might not own. + prefetch_read(buf, 128.min(last_chunk_offset)); + + // Prefetch the fourth 64-byte chunk, using min() to branchlessly avoid prefetching an address we might not own. + prefetch_read(buf, 192.min(last_chunk_offset)); + + // Further prefetching can happen in the tokenization loop. Now that we've prefetched the first pages, + // we should be able to prefetch the others in the tokenization loop before it needs to read them. + } + + // We may have coincidentally had a file size that was a multiple of 64, but if not, + // we'll need to fill the allocation with trailing newlines so we aren't tokenizing + // uninitialized memory. + if capacity > file_size { + debug_assert!(capacity - file_size < 64); + let trailing_newlines_needed = capacity - file_size; + + // Safety: `buf_ptr` has a length of `capacity`, which has been rounded up to a multiple of 64. + unsafe { + // Start writing newlines right after the last of the bytes we got from the file. + write_newlines(buf.as_ptr().add(file_size), trailing_newlines_needed); + }; + } + + // Safety: bytes_ptr came from an allocation of `capacity` bytes, it's had + // newlines filled at the end, and `file_size` bytes written over the rest. + let bytes = + unsafe { core::slice::from_raw_parts_mut(buf.as_ptr() as *mut u8, capacity) }; + + Ok(Self { bytes }) + } + None => Err(FileErr::FileWasTooBig(file_size)), + } + } +} + +fn round_up_to_nearest_64(num: usize) -> usize { + // Round up to the nearest 64. (Writing this out as 64 - 1 so it's clearer where the numbers came from.) + // We can do saturating addition here rather than overflow checking, because if we overflow usize::MAX, + // we will most definitely be over the max source file size and return None anyway. + (num.saturating_add(64 - 1)) & !(64 - 1) +} + +/// Safety: capacity must be a multiple of 64, and must be at least 64. +unsafe fn allocate_chunks(arena: &Bump, capacity: usize) -> Option> { + // Compare capacity here instead of size because this file limit is based on what we can record row and line + // numbers for, and those can theoretically oveflow on the trailing newlines we may have added. + // This distinction will most likely come up in practice zero times ever, but it could come up in fuzzing. + if capacity > MAX_ROC_SOURCE_FILE_SIZE { + return None; + } + + debug_assert!(capacity >= 64); + debug_assert!(capacity % 64 == 0); + + // Safety: the rules we follow are https://doc.rust-lang.org/core/alloc/struct.Layout.html#method.from_size_align_unchecked + // `align` is valid because it's hardcoded, and we already rounded `capacity` up to something even bigger. + // We align it to 64B so that it's on cache line boundaries on many CPUs, which makes prefetching simpler. + let layout = unsafe { Layout::from_size_align_unchecked(capacity, Src64::BYTES_ALIGNMENT) }; + + // We have to use alloc_layout here because we have stricter alignment requirements than normal slices. + Some(arena.alloc_layout(layout)) +} + +/// This is branchless so there can't be mispredictions. +/// +/// Safety: this pointer must have an alignment of at least 64, +/// and the length must be both at least 64 and also a multiple of 64. +unsafe fn write_newlines(dest: *mut u8, len: usize) { + debug_assert!(len <= 64); + + #[cfg(target_feature = "sse2")] + { + use core::arch::x86_64::{__m128i, _mm_set1_epi8, _mm_storeu_si128}; + + let mut buf: MaybeUninit<[__m128i; 4]> = MaybeUninit::uninit(); + let newline = _mm_set1_epi8(b'\n' as i8); + let ptr = buf.as_mut_ptr() as *mut __m128i; + + debug_assert_eq!(ptr.align_offset(align_of::<__m128i>()), 0); + + _mm_storeu_si128(ptr.add(0), newline); + _mm_storeu_si128(ptr.add(1), newline); + _mm_storeu_si128(ptr.add(2), newline); + _mm_storeu_si128(ptr.add(3), newline); + + core::ptr::copy_nonoverlapping(ptr as *const u8, dest, len); + } + + #[cfg(target_feature = "neon")] + { + use core::arch::aarch64::{int8x16_t, vdupq_n_s8, vst1q_s8}; + + let mut buf: MaybeUninit<[int8x16_t; 4]> = MaybeUninit::uninit(); + let newline = vdupq_n_s8(b'\n' as i8); + let ptr = buf.as_mut_ptr() as *mut i8; + + debug_assert_eq!(ptr.align_offset(align_of::()), 0); + + vst1q_s8(ptr.add(0), newline); + vst1q_s8(ptr.add(16), newline); + vst1q_s8(ptr.add(32), newline); + vst1q_s8(ptr.add(48), newline); + + core::ptr::copy_nonoverlapping(ptr as *const u8, dest, len); + } + + #[cfg(not(any(target_feature = "sse2", target_feature = "neon")))] + { + // We don't have access to SIMD, so do eight 64-bit writes instead of four 128-bit writes. + let mut buf: MaybeUninit<[u64; 8]> = MaybeUninit::uninit(); + let newline_repeated = (b'\n' as u64) * 0x0101010101010101; + let ptr = buf.as_mut_ptr() as *mut u64; + + debug_assert_eq!(ptr.align_offset(align_of::()), 0); + + *ptr.add(0) = newline_repeated; + *ptr.add(1) = newline_repeated; + *ptr.add(2) = newline_repeated; + *ptr.add(3) = newline_repeated; + *ptr.add(4) = newline_repeated; + *ptr.add(5) = newline_repeated; + *ptr.add(6) = newline_repeated; + *ptr.add(7) = newline_repeated; + + core::ptr::copy_nonoverlapping(ptr as *const u8, dest, len); + } +} + +#[inline(always)] +fn prefetch_read(non_null_ptr: NonNull, offset: usize) { + // Use inline asm until this is stabilized: + // https://doc.rust-lang.org/std/intrinsics/fn.prefetch_read_data.html + + #[cfg(target_arch = "x86_64")] + unsafe { + core::arch::asm!( + "prefetcht0 [{}]", + in(reg) non_null_ptr.as_ptr().add(offset) + ); + } + + #[cfg(target_arch = "aarch64")] + unsafe { + core::arch::asm!( + "prfm PLDL1KEEP, [{}]", + in(reg) non_null_ptr.as_ptr().add(offset) + ); + } + + // If we're not on x64 or aarch64, just do nothing! +} + +#[cfg(test)] +mod src64_tests { + use super::{FileErr, Src64, MAX_ROC_SOURCE_FILE_SIZE}; + use bumpalo::Bump; + use quickcheck::{quickcheck, Arbitrary, Gen}; + use std::fs::File; + use std::io::Write; + use tempfile::tempdir; + + fn expect_from_str(arena: &Bump, contents: &str, expected: &Result, FileErr>) { + match Src64::from_str(arena, contents) { + Some(actual) => { + assert_eq!(actual.len() % 64, 0); + assert_eq!( + expected.as_ref().ok(), + Some(&actual.bytes().into()), + "Src64::from_str had unexpected output" + ) + } + None => { + assert_eq!( + expected.as_ref().ok(), + None, + "Src64::from_str had unexpected output" + ) + } + } + } + + fn expect_from_file(arena: &Bump, contents: &str, expected: &Result, FileErr>) { + let dir = tempdir().expect("Failed to create temp dir"); + let file_path = dir.path().join("temp_file"); + + // Write contents to the temp file + { + let mut file = File::create(&file_path).expect("Failed to create temp file"); + file.write_all(contents.as_bytes()) + .expect("Failed to write to temp file"); + } + + match Src64::from_file(arena, &file_path) { + Ok(actual) => { + assert_eq!(actual.len() % 64, 0); + assert_eq!( + expected, + &Ok(actual.bytes().into()), + "Src64::from_file had unexpected output" + ) + } + Err(err) => assert_eq!( + expected, + &Err(err), + "Src64::from_file had unexpected output" + ), + } + } + + /// Runs both Src64::from_str and Src64::from_file on the given str, then + /// asserts the output of both of those functions is equal to `expected`. + /// (Since from_str returns an Option, we call .ok() on `expected` before comparing it.) + fn expect_from(contents: &str, expected: Result, FileErr>) { + let arena = Bump::new(); + + expect_from_str(&arena, contents, &expected); + expect_from_file(&arena, contents, &expected); + } + + #[test] + fn empty() { + expect_from("", Err(FileErr::FileWasEmpty)); + } + + #[test] + fn one_newline() { + expect_from("\n", Ok([b'\n'; 64].into())); + } + + #[test] + fn one_byte() { + expect_from( + "x", + Ok({ + let mut vec: Vec = [b'\n'; 64].as_mut_slice().into(); + + vec[0] = b'x'; + + vec + }), + ); + } + + #[test] + fn two_bytes() { + expect_from( + "xy", + Ok({ + let mut vec: Vec = [b'\n'; 64].as_mut_slice().into(); + + vec[0] = b'x'; + vec[1] = b'y'; + + vec + }), + ); + } + + #[test] + fn max_file_size() { + let bytes = [b'z'; MAX_ROC_SOURCE_FILE_SIZE]; + + expect_from( + core::str::from_utf8(bytes.as_slice()).unwrap(), + Ok(bytes.into()), + ); + } + + #[test] + fn too_big() { + let bytes = [b'z'; MAX_ROC_SOURCE_FILE_SIZE + 1]; + + expect_from( + core::str::from_utf8(bytes.as_slice()).unwrap(), + Err(FileErr::FileWasTooBig(bytes.len())), + ); + } + + #[derive(Debug, Clone)] + struct FileBytes(Vec); + + impl Arbitrary for FileBytes { + fn arbitrary(g: &mut Gen) -> Self { + let len = g.size() % (MAX_ROC_SOURCE_FILE_SIZE + 1); // Wrap around to avoid clustering + // + FileBytes((0..len).map(|_| u8::arbitrary(g)).collect()) + } + } + + quickcheck! { + /// Creates a tempfile containing arbitrary bytes, then reads it with Str::from_file. Asserts that: + /// - the returned Result is Ok + /// - its length is a multiple of 64 + /// - it's at least as long as the input bytes were + /// - it starts_with the input bytes + fn from_arb_file(bytes: FileBytes) -> bool { + let FileBytes(bytes) = bytes; + + let dir = tempdir().expect("Failed to create temp dir"); + let file_path = dir.path().join("temp_file"); + + // Write random bytes to the temp file + { + let mut file = File::create(&file_path).expect("Failed to create temp file"); + file.write_all(&bytes).expect("Failed to write to temp file"); + } + + let arena = Bump::new(); + + match Src64::from_file(&arena, &file_path) { + Ok(src64) => { + let len = src64.len(); + + len % 64 == 0 && len >= bytes.len() && src64.bytes().starts_with(&bytes) + } + Err(_) => false + } + } + } +} diff --git a/crates/compiler/parse/src/state.rs b/crates/compiler/parse/src/state.rs new file mode 100644 index 0000000000..7e4207584f --- /dev/null +++ b/crates/compiler/parse/src/state.rs @@ -0,0 +1,155 @@ +use roc_region::all::{Position, Region}; +use std::fmt; + +use crate::parser::Progress; + +/// A position in a source file. +// NB: [Copy] is explicitly NOT derived to reduce the chance of bugs due to accidentally re-using +// parser state. +#[derive(Clone)] +pub struct State<'a> { + /// The raw input bytes from the file. + /// Beware: original_bytes[0] always points at the start of the file. + /// Use bytes()[0] to access the current byte the parser is inspecting + original_bytes: &'a [u8], + + /// Offset in original_bytes that the parser is currently inspecting + offset: usize, + + /// Position of the start of the current line + pub(crate) line_start: Position, + + /// Position of the first non-whitespace character on the current line + pub(crate) line_start_after_whitespace: Position, +} + +impl<'a> State<'a> { + pub fn new(bytes: &'a [u8]) -> State<'a> { + State { + original_bytes: bytes, + offset: 0, + line_start: Position::zero(), + + // Technically not correct. + // We don't know the position of the first non-whitespace character yet. + line_start_after_whitespace: Position::zero(), + } + } + + pub fn original_bytes(&self) -> &'a [u8] { + self.original_bytes + } + + pub(crate) fn bytes(&self) -> &'a [u8] { + &self.original_bytes[self.offset..] + } + + pub fn column(&self) -> u32 { + self.pos().offset - self.line_start.offset + } + + pub fn line_indent(&self) -> u32 { + self.line_start_after_whitespace.offset - self.line_start.offset + } + + /// Check that the indent is at least `indent` spaces. + /// Return a new indent if the current indent is greater than `indent`. + pub fn check_indent( + &self, + indent: u32, + e: impl Fn(Position) -> E, + ) -> Result { + if self.column() < indent { + Err((Progress::NoProgress, e(self.pos()))) + } else { + Ok(std::cmp::max(indent, self.line_indent())) + } + } + + /// Mutably advance the state by a given offset + #[inline(always)] + pub(crate) fn advance_mut(&mut self, offset: usize) { + self.offset += offset; + } + + /// If the next `text.len()` bytes of the input match the provided `text`, + /// mutably advance the state by that much. + #[inline(always)] + pub(crate) fn consume_mut(&mut self, text: &str) -> bool { + let found = self.bytes().starts_with(text.as_bytes()); + + if found { + self.advance_mut(text.len()); + } + + found + } + + #[must_use] + #[inline(always)] + pub(crate) const fn advance(mut self, offset: usize) -> State<'a> { + self.offset += offset; + self + } + + #[must_use] + #[inline(always)] + pub(crate) const fn advance_newline(mut self) -> State<'a> { + self.offset += 1; + self.line_start = self.pos(); + + // WARNING! COULD CAUSE BUGS IF WE FORGET TO CALL mark_current_indent LATER! + // We really need to be stricter about this. + self.line_start_after_whitespace = self.line_start; + + self + } + + #[must_use] + #[inline(always)] + pub(crate) const fn mark_current_indent(mut self) -> State<'a> { + self.line_start_after_whitespace = self.pos(); + self + } + + /// Returns the current position + pub const fn pos(&self) -> Position { + Position::new(self.offset as u32) + } + + /// Returns whether the parser has reached the end of the input + pub const fn has_reached_end(&self) -> bool { + self.offset == self.original_bytes.len() + } + + /// Returns a Region corresponding to the current state, but + /// with the the end column advanced by the given amount. This is + /// useful when parsing something "manually" (using input.chars()) + /// and thus wanting a Region while not having access to loc(). + pub fn len_region(&self, length: u32) -> Region { + Region::new(self.pos(), self.pos().bump_column(length)) + } +} + +impl<'a> fmt::Debug for State<'a> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "State {{")?; + + match std::str::from_utf8(self.bytes()) { + Ok(string) => write!(f, "\n\tbytes: [utf8] {string:?}")?, + Err(_) => write!(f, "\n\tbytes: [invalid utf8] {:?}", self.bytes())?, + } + + write!(f, "\n\t(offset): {:?},", self.pos())?; + write!(f, "\n}}") + } +} + +#[test] +fn state_size() { + // State should always be under 8 machine words, so it fits in a typical + // cache line. + let state_size = std::mem::size_of::(); + let maximum = std::mem::size_of::() * 8; + assert!(state_size <= maximum, "{state_size:?} <= {maximum:?}"); +} diff --git a/crates/compiler/parse/src/string_literal.rs b/crates/compiler/parse/src/string_literal.rs new file mode 100644 index 0000000000..325674f782 --- /dev/null +++ b/crates/compiler/parse/src/string_literal.rs @@ -0,0 +1,466 @@ +use crate::ast::{EscapedChar, SingleQuoteLiteral, StrLiteral, StrSegment}; +use crate::expr; +use crate::parser::Progress::{self, *}; +use crate::parser::{ + allocated, loc, reset_min_indent, specialize_ref, then, word1, BadInputError, ESingleQuote, + EString, Parser, +}; +use crate::state::State; +use bumpalo::collections::vec::Vec; +use bumpalo::Bump; + +/// One or more ASCII hex digits. (Useful when parsing unicode escape codes, +/// which must consist entirely of ASCII hex digits.) +fn ascii_hex_digits<'a>() -> impl Parser<'a, &'a str, EString<'a>> { + move |arena, mut state: State<'a>, _min_indent: u32| { + let mut buf = bumpalo::collections::String::new_in(arena); + + for &byte in state.bytes().iter() { + if (byte as char).is_ascii_hexdigit() { + buf.push(byte as char); + } else if buf.is_empty() { + // We didn't find any hex digits! + return Err((NoProgress, EString::CodePtEnd(state.pos()))); + } else { + state.advance_mut(buf.len()); + + return Ok((MadeProgress, buf.into_bump_str(), state)); + } + } + + Err((NoProgress, EString::CodePtEnd(state.pos()))) + } +} + +fn consume_indent(mut state: State, mut indent: u32) -> Result { + while indent > 0 { + match state.bytes().first() { + Some(b' ') => { + state.advance_mut(1); + indent -= 1; + } + None | Some(b'\n') => { + break; + } + Some(_) => { + return Err(( + MadeProgress, + EString::MultilineInsufficientIndent(state.pos()), + )); + } + } + } + + Ok(state) +} + +fn utf8<'a>(state: State<'a>, string_bytes: &'a [u8]) -> Result<&'a str, (Progress, EString<'a>)> { + std::str::from_utf8(string_bytes).map_err(|_| { + // Note Based on where this `utf8` function is used, the fact that we know the whole string + // in the parser is valid utf8, and barring bugs in the parser itself + // (e.g. where we accidentally split a multibyte utf8 char), this error _should_ actually be unreachable. + ( + MadeProgress, + EString::Space(BadInputError::BadUtf8, state.pos()), + ) + }) +} + +pub enum StrLikeLiteral<'a> { + SingleQuote(SingleQuoteLiteral<'a>), + Str(StrLiteral<'a>), +} + +pub fn parse_str_literal<'a>() -> impl Parser<'a, StrLiteral<'a>, EString<'a>> { + then( + loc!(parse_str_like_literal()), + |_arena, state, progress, str_like| match str_like.value { + StrLikeLiteral::SingleQuote(_) => Err(( + progress, + EString::ExpectedDoubleQuoteGotSingleQuote(str_like.region.start()), + )), + StrLikeLiteral::Str(str_literal) => Ok((progress, str_literal, state)), + }, + ) +} + +pub fn parse_str_like_literal<'a>() -> impl Parser<'a, StrLikeLiteral<'a>, EString<'a>> { + move |arena: &'a Bump, mut state: State<'a>, min_indent: u32| { + let is_multiline; + let is_single_quote; + + let indent = state.column(); + + let start_state; + + if state.consume_mut("\"\"\"") { + start_state = state.clone(); + + // we will be parsing a multi-line string + is_multiline = true; + is_single_quote = false; + + if state.consume_mut("\n") { + state = consume_indent(state, indent)?; + } + } else if state.consume_mut("\"") { + start_state = state.clone(); + + // we will be parsing a single-line string + is_multiline = false; + is_single_quote = false; + } else if state.consume_mut("'") { + start_state = state.clone(); + + is_multiline = false; + is_single_quote = true; + } else { + return Err((NoProgress, EString::Open(state.pos()))); + } + + let mut bytes = state.bytes().iter(); + + let mut segment_parsed_bytes = 0; + let mut segments = Vec::new_in(arena); + + macro_rules! escaped_char { + ($ch:expr) => { + // Record the escaped char. + segments.push(StrSegment::EscapedChar($ch)); + + // Advance past the segment we just added + state.advance_mut(segment_parsed_bytes); + + // Reset the segment + segment_parsed_bytes = 0; + }; + } + + macro_rules! end_segment { + ($transform:expr) => { + // Don't push anything if the string would be empty. + if segment_parsed_bytes > 1 { + // This function is always called after we just parsed + // something which signalled that we should end the + // current segment - so use segment_parsed_bytes - 1 here, + // to exclude that char we just parsed. + let string_bytes = &state.bytes()[0..(segment_parsed_bytes - 1)]; + + match std::str::from_utf8(string_bytes) { + Ok(string) => { + state.advance_mut(string.len()); + + segments.push($transform(string)); + } + Err(_) => { + return Err(( + MadeProgress, + EString::Space(BadInputError::BadUtf8, state.pos()), + )); + } + } + } + + // Depending on where this macro is used, in some + // places this is unused. + #[allow(unused_assignments)] + { + // This function is always called after we just parsed + // something which signalled that we should end the + // current segment. + segment_parsed_bytes = 1; + } + }; + } + + while let Some(&byte) = bytes.next() { + // This is for the byte we just grabbed from the iterator. + segment_parsed_bytes += 1; + + match byte { + b'"' if !is_single_quote => { + if segment_parsed_bytes == 1 && segments.is_empty() { + // special case of the empty string + if is_multiline { + if bytes.as_slice().starts_with(b"\"\"") { + return Ok(( + MadeProgress, + StrLikeLiteral::Str(StrLiteral::Block(&[])), + state.advance(3), + )); + } else { + // this quote is in a block string + continue; + } + } else { + // This is the end of the string! + // Advance 1 for the close quote + return Ok(( + MadeProgress, + StrLikeLiteral::Str(StrLiteral::PlainLine("")), + state.advance(1), + )); + } + } else { + // the string is non-empty, which means we need to convert any previous segments + // and the current segment into a string literal + if is_multiline { + if bytes.as_slice().starts_with(b"\"\"") { + end_segment!(StrSegment::Plaintext); + + let expr = if segments.len() == 1 { + // We had exactly one segment, so this is a candidate + // to be StrLiteral::Plaintext + match segments.pop().unwrap() { + StrSegment::Plaintext(string) => { + StrLiteral::PlainLine(string) + } + other => StrLiteral::Line(arena.alloc([other])), + } + } else { + StrLiteral::Block(arena.alloc([segments.into_bump_slice()])) + }; + + return Ok(( + MadeProgress, + StrLikeLiteral::Str(expr), + state.advance(3), + )); + } else { + // this quote is in a block string + continue; + } + } else { + end_segment!(StrSegment::Plaintext); + + let expr = if segments.len() == 1 { + // We had exactly one segment, so this is a candidate + // to be StrLiteral::Plaintext + match segments.pop().unwrap() { + StrSegment::Plaintext(string) => StrLiteral::PlainLine(string), + other => StrLiteral::Line(arena.alloc([other])), + } + } else { + StrLiteral::Line(segments.into_bump_slice()) + }; + + // Advance the state 1 to account for the closing `"` + return Ok((MadeProgress, StrLikeLiteral::Str(expr), state.advance(1))); + } + }; + } + b'\'' if is_single_quote => { + end_segment!(StrSegment::Plaintext); + + let expr = if segments.len() == 1 { + // We had exactly one segment, so this is a candidate + // to be SingleQuoteLiteral::Plaintext + match segments.pop().unwrap() { + StrSegment::Plaintext(string) => SingleQuoteLiteral::PlainLine(string), + other => { + let o = other.try_into().map_err(|e| { + ( + MadeProgress, + EString::InvalidSingleQuote(e, start_state.pos()), + ) + })?; + SingleQuoteLiteral::Line(arena.alloc([o])) + } + } + } else { + let mut new_segments = Vec::with_capacity_in(segments.len(), arena); + for segment in segments { + let segment = segment.try_into().map_err(|e| { + ( + MadeProgress, + EString::InvalidSingleQuote(e, start_state.pos()), + ) + })?; + new_segments.push(segment); + } + + SingleQuoteLiteral::Line(new_segments.into_bump_slice()) + }; + + // Validate that the string is a valid char literal. + // Note that currently, we accept anything that: + // * Is between 1 and 5 bytes long + // -> utf-8 encoding is trivial to extend to 5 bytes, even tho 4 is the technical max + // -> TODO: do we want to change this? + // * Decodes as valid UTF-8 + // -> Might be a single code point, or multiple code points + // -> TODO: do we want to change this? + + // Simply by decoding this, it's guaranteed to be valid utf-8 + let text = expr.to_str_in(arena); + + if text.len() > 5 { + return Err(( + MadeProgress, + EString::InvalidSingleQuote(ESingleQuote::TooLong, start_state.pos()), + )); + } + + if text.is_empty() { + return Err(( + MadeProgress, + EString::InvalidSingleQuote(ESingleQuote::Empty, start_state.pos()), + )); + } + + // Advance the state 1 to account for the closing `'` + return Ok(( + MadeProgress, + StrLikeLiteral::SingleQuote(expr), + state.advance(1), + )); + } + b'\n' => { + if is_multiline { + let without_newline = &state.bytes()[0..(segment_parsed_bytes - 1)]; + let with_newline = &state.bytes()[0..segment_parsed_bytes]; + + state.advance_mut(segment_parsed_bytes); + state = consume_indent(state, indent)?; + bytes = state.bytes().iter(); + + if state.bytes().starts_with(b"\"\"\"") { + // ending the string; don't use the last newline + if !without_newline.is_empty() { + segments.push(StrSegment::Plaintext(utf8( + state.clone(), + without_newline, + )?)); + } + } else { + segments + .push(StrSegment::Plaintext(utf8(state.clone(), with_newline)?)); + } + + segment_parsed_bytes = 0; + + continue; + } else { + // This is a single-line string, which cannot have newlines! + // Treat this as an unclosed string literal, and consume + // all remaining chars. This will mask all other errors, but + // it should make it easiest to debug; the file will be a giant + // error starting from where the open quote appeared. + return Err((MadeProgress, EString::EndlessSingleLine(start_state.pos()))); + } + } + b'\\' => { + // We're about to begin an escaped segment of some sort! + // + // Record the current segment so we can begin a new one. + // End it right before the `\` char we just parsed. + end_segment!(StrSegment::Plaintext); + + // This is for the byte we're about to parse. + segment_parsed_bytes += 1; + + // This is the start of a new escape. Look at the next byte + // to figure out what type of escape it is. + match bytes.next() { + Some(b'(') => { + // Advance past the `\(` before using the expr parser + state.advance_mut(2); + + let original_byte_count = state.bytes().len(); + + // This is an interpolated variable. + // Parse an arbitrary expression, then give a + // canonicalization error if that expression variant + // is not allowed inside a string interpolation. + let (_progress, loc_expr, new_state) = skip_second!( + specialize_ref( + EString::Format, + loc(allocated(reset_min_indent(expr::expr_help()))) + ), + word1(b')', EString::FormatEnd) + ) + .parse(arena, state, min_indent)?; + + // Advance the iterator past the expr we just parsed. + for _ in 0..(original_byte_count - new_state.bytes().len()) { + bytes.next(); + } + + segments.push(StrSegment::Interpolated(loc_expr)); + + // Reset the segment + segment_parsed_bytes = 0; + state = new_state; + } + Some(b'u') => { + // Advance past the `\u` before using the expr parser + state.advance_mut(2); + + let original_byte_count = state.bytes().len(); + + // Parse the hex digits, surrounded by parens, then + // give a canonicalization error if the digits form + // an invalid unicode code point. + let (_progress, loc_digits, new_state) = between!( + word1(b'(', EString::CodePtOpen), + loc(ascii_hex_digits()), + word1(b')', EString::CodePtEnd) + ) + .parse(arena, state, min_indent)?; + + // Advance the iterator past the expr we just parsed. + for _ in 0..(original_byte_count - new_state.bytes().len()) { + bytes.next(); + } + + segments.push(StrSegment::Unicode(loc_digits)); + + // Reset the segment + segment_parsed_bytes = 0; + state = new_state; + } + Some(b'\\') => { + escaped_char!(EscapedChar::Backslash); + } + Some(b'"') => { + escaped_char!(EscapedChar::DoubleQuote); + } + Some(b'\'') => { + escaped_char!(EscapedChar::SingleQuote); + } + Some(b'r') => { + escaped_char!(EscapedChar::CarriageReturn); + } + Some(b't') => { + escaped_char!(EscapedChar::Tab); + } + Some(b'n') => { + escaped_char!(EscapedChar::Newline); + } + _ => { + // Invalid escape! A backslash must be followed + // by either an open paren or else one of the + // escapable characters (\n, \t, \", \\, etc) + return Err((MadeProgress, EString::UnknownEscape(state.pos()))); + } + } + } + _ => { + // All other characters need no special handling. + } + } + } + + // We ran out of characters before finding a closed quote + Err(( + MadeProgress, + if is_single_quote { + EString::EndlessSingleQuote(start_state.pos()) + } else if is_multiline { + EString::EndlessMultiLine(start_state.pos()) + } else { + EString::EndlessSingleLine(start_state.pos()) + }, + )) + } +} diff --git a/crates/compiler/parse/src/test_helpers.rs b/crates/compiler/parse/src/test_helpers.rs new file mode 100644 index 0000000000..0f11f09dee --- /dev/null +++ b/crates/compiler/parse/src/test_helpers.rs @@ -0,0 +1,55 @@ +use crate::ast; +use crate::ast::Defs; +use crate::module::module_defs; +use crate::parser::Parser; +use crate::parser::SourceError; +use crate::parser::SyntaxError; +use crate::state::State; +use bumpalo::Bump; +use roc_region::all::Loc; +use roc_region::all::Position; + +pub fn parse_expr_with<'a>( + arena: &'a Bump, + input: &'a str, +) -> Result, SyntaxError<'a>> { + parse_loc_with(arena, input) + .map(|loc_expr| loc_expr.value) + .map_err(|e| e.problem) +} + +#[allow(dead_code)] +pub fn parse_loc_with<'a>( + arena: &'a Bump, + input: &'a str, +) -> Result>, SourceError<'a, SyntaxError<'a>>> { + let state = State::new(input.trim().as_bytes()); + + match crate::expr::test_parse_expr(0, arena, state.clone()) { + Ok(loc_expr) => Ok(loc_expr), + Err(fail) => Err(SyntaxError::Expr(fail, Position::default()).into_source_error(&state)), + } +} + +pub fn parse_defs_with<'a>(arena: &'a Bump, input: &'a str) -> Result, SyntaxError<'a>> { + let state = State::new(input.trim().as_bytes()); + + let min_indent = 0; + + match module_defs().parse(arena, state, min_indent) { + Ok(tuple) => Ok(tuple.1), + Err(tuple) => Err(tuple.1), + } +} + +pub fn parse_header_with<'a>( + arena: &'a Bump, + input: &'a str, +) -> Result, SyntaxError<'a>> { + let state = State::new(input.trim().as_bytes()); + + match crate::module::parse_header(arena, state.clone()) { + Ok((header, _)) => Ok(header), + Err(fail) => Err(SyntaxError::Header(fail.problem)), + } +} diff --git a/crates/compiler/parse/src/type_annotation.rs b/crates/compiler/parse/src/type_annotation.rs new file mode 100644 index 0000000000..d9a8eabee0 --- /dev/null +++ b/crates/compiler/parse/src/type_annotation.rs @@ -0,0 +1,744 @@ +use crate::ast::{ + AbilityImpls, AssignedField, CommentOrNewline, Expr, ImplementsAbilities, ImplementsAbility, + ImplementsClause, Pattern, Spaceable, Spaced, Tag, TypeAnnotation, TypeHeader, +}; +use crate::blankspace::{ + space0_around_ee, space0_before_e, space0_before_optional_after, space0_e, +}; +use crate::expr::{record_field, FoundApplyValue}; +use crate::ident::{lowercase_ident, lowercase_ident_keyword_e}; +use crate::keyword; +use crate::parser::{ + absolute_column_min_indent, increment_min_indent, then, ERecord, ETypeAbilityImpl, +}; +use crate::parser::{ + allocated, backtrackable, fail, optional, specialize, specialize_ref, word, word1, word2, + EType, ETypeApply, ETypeInParens, ETypeInlineAlias, ETypeRecord, ETypeTagUnion, Parser, + Progress::{self, *}, +}; +use crate::state::State; +use bumpalo::collections::vec::Vec; +use bumpalo::Bump; +use roc_region::all::{Loc, Position, Region}; + +pub fn located<'a>( + is_trailing_comma_valid: bool, +) -> impl Parser<'a, Loc>, EType<'a>> { + expression(is_trailing_comma_valid, false) +} + +pub fn located_opaque_signature<'a>( + is_trailing_comma_valid: bool, +) -> impl Parser<'a, Loc>, EType<'a>> { + expression(is_trailing_comma_valid, true) +} + +#[inline(always)] +fn tag_union_type<'a>( + stop_at_surface_has: bool, +) -> impl Parser<'a, TypeAnnotation<'a>, ETypeTagUnion<'a>> { + move |arena, state, min_indent| { + let (_, tags, state) = collection_trailing_sep_e!( + word1(b'[', ETypeTagUnion::Open), + loc!(tag_type(false)), + word1(b',', ETypeTagUnion::End), + word1(b']', ETypeTagUnion::End), + Tag::SpaceBefore + ) + .parse(arena, state, min_indent)?; + + // This could be an open tag union, e.g. `[Foo, Bar]a` + let (_, ext, state) = optional(allocated(specialize_ref( + ETypeTagUnion::Type, + term(stop_at_surface_has), + ))) + .parse(arena, state, min_indent)?; + + let result = TypeAnnotation::TagUnion { tags, ext }; + + Ok((MadeProgress, result, state)) + } +} + +fn check_type_alias<'a>( + arena: &'a Bump, + annot: Loc>, +) -> Result, ETypeInlineAlias> { + match annot.value { + TypeAnnotation::Apply("", tag_name, vars) => { + let mut var_names = Vec::new_in(arena); + var_names.reserve(vars.len()); + for var in vars { + if let TypeAnnotation::BoundVariable(v) = var.value { + var_names.push(Loc::at(var.region, Pattern::Identifier(v))); + } else { + return Err(ETypeInlineAlias::ArgumentNotLowercase(var.region.start())); + } + } + + let name_start = annot.region.start(); + let name_region = + Region::between(name_start, name_start.bump_column(tag_name.len() as u32)); + + let header = TypeHeader { + name: Loc::at(name_region, tag_name), + vars: var_names.into_bump_slice(), + }; + + Ok(header) + } + TypeAnnotation::Apply(_, _, _) => Err(ETypeInlineAlias::Qualified(annot.region.start())), + _ => Err(ETypeInlineAlias::NotAnAlias(annot.region.start())), + } +} + +fn parse_type_alias_after_as<'a>() -> impl Parser<'a, TypeHeader<'a>, EType<'a>> { + then( + space0_before_e(term(false), EType::TAsIndentStart), + // TODO: introduce a better combinator for this. + // `check_type_alias` doesn't need to modify the state or progress, but it needs to access `state.pos()` + |arena, state, progress, output| { + let res = check_type_alias(arena, output); + + match res { + Ok(header) => Ok((progress, header, state)), + Err(err) => Err((progress, EType::TInlineAlias(err, state.pos()))), + } + }, + ) +} + +fn term<'a>(stop_at_surface_has: bool) -> impl Parser<'a, Loc>, EType<'a>> { + map_with_arena!( + and!( + one_of!( + loc_wildcard(), + loc_inferred(), + specialize(EType::TInParens, loc_type_in_parens(stop_at_surface_has)), + loc!(specialize(EType::TRecord, record_type(stop_at_surface_has))), + loc!(specialize( + EType::TTagUnion, + tag_union_type(stop_at_surface_has) + )), + loc!(applied_type(stop_at_surface_has)), + loc!(parse_type_variable(stop_at_surface_has)), + fail(EType::TStart), + ), + // Inline alias notation, e.g. [Nil, Cons a (List a)] as List a + one_of![ + map!( + and!( + skip_second!( + backtrackable(space0_e(EType::TIndentEnd)), + crate::parser::keyword_e(keyword::AS, EType::TEnd) + ), + parse_type_alias_after_as() + ), + Some + ), + succeed!(None) + ] + ), + |arena: &'a Bump, + (loc_ann, opt_as): (Loc>, Option<(&'a [_], TypeHeader<'a>)>)| { + match opt_as { + Some((spaces, alias)) => { + let alias_vars_region = + Region::across_all(alias.vars.iter().map(|v| &v.region)); + let region = Region::span_across(&loc_ann.region, &alias_vars_region); + let value = TypeAnnotation::As(arena.alloc(loc_ann), spaces, alias); + + Loc { region, value } + } + + None => loc_ann, + } + } + ) + .trace("type_annotation:term") +} + +/// The `*` type variable, e.g. in (List *) Wildcard, +fn loc_wildcard<'a>() -> impl Parser<'a, Loc>, EType<'a>> { + map!(loc!(word1(b'*', EType::TWildcard)), |loc_val: Loc<()>| { + loc_val.map(|_| TypeAnnotation::Wildcard) + }) +} + +/// The `_` indicating an inferred type, e.g. in (List _) +fn loc_inferred<'a>() -> impl Parser<'a, Loc>, EType<'a>> { + // TODO: make this more combinator based, or perhaps make the underlying + // representation token-based + move |_arena, mut state: State<'a>, _min_indent: u32| { + if !state.bytes().starts_with(b"_") { + return Err((NoProgress, EType::TInferred(state.pos()))); + } + + // the next character should not be an identifier character + // to prevent treating `_a` as an inferred type + match state.bytes().get(1) { + Some(b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' | b'_') => { + Err((NoProgress, EType::TInferred(state.pos()))) + } + _ => { + let start = state.pos(); + state.advance_mut(1); + let end = state.pos(); + let region = Region::new(start, end); + Ok(( + MadeProgress, + Loc::at(region, TypeAnnotation::Inferred), + state, + )) + } + } + } +} + +fn loc_applied_arg<'a>( + stop_at_surface_has: bool, +) -> impl Parser<'a, Loc>, EType<'a>> { + map_with_arena!( + and!( + backtrackable(space0_e(EType::TIndentStart)), + one_of!( + loc_wildcard(), + loc_inferred(), + specialize(EType::TInParens, loc_type_in_parens(stop_at_surface_has)), + loc!(specialize(EType::TRecord, record_type(stop_at_surface_has))), + loc!(specialize( + EType::TTagUnion, + tag_union_type(stop_at_surface_has) + )), + loc!(specialize(EType::TApply, concrete_type())), + loc!(parse_type_variable(stop_at_surface_has)) + ) + ), + |arena: &'a Bump, (spaces, argument): (&'a [_], Loc>)| { + if spaces.is_empty() { + argument + } else { + let Loc { region, value } = argument; + arena.alloc(value).with_spaces_before(spaces, region) + } + } + ) +} + +fn loc_type_in_parens<'a>( + stop_at_surface_has: bool, +) -> impl Parser<'a, Loc>, ETypeInParens<'a>> { + then( + loc!(and!( + collection_trailing_sep_e!( + word1(b'(', ETypeInParens::Open), + specialize_ref(ETypeInParens::Type, expression(true, false)), + word1(b',', ETypeInParens::End), + word1(b')', ETypeInParens::End), + TypeAnnotation::SpaceBefore + ), + optional(allocated(specialize_ref( + ETypeInParens::Type, + term(stop_at_surface_has) + ))) + )), + |_arena, state, progress, item| { + let Loc { + region, + value: (fields, ext), + } = item; + if fields.len() > 1 || ext.is_some() { + Ok(( + MadeProgress, + Loc::at(region, TypeAnnotation::Tuple { elems: fields, ext }), + state, + )) + } else if fields.len() == 1 { + Ok((MadeProgress, fields.items[0], state)) + } else { + debug_assert!(fields.is_empty()); + Err((progress, ETypeInParens::Empty(state.pos()))) + } + }, + ) + .trace("type_annotation:type_in_parens") +} + +#[inline(always)] +fn tag_type<'a>(stop_at_surface_has: bool) -> impl Parser<'a, Tag<'a>, ETypeTagUnion<'a>> { + move |arena, state: State<'a>, min_indent: u32| { + let (_, name, state) = + loc!(parse_tag_name(ETypeTagUnion::End)).parse(arena, state, min_indent)?; + + let (_, args, state) = + specialize_ref(ETypeTagUnion::Type, loc_applied_args_e(stop_at_surface_has)) + .parse(arena, state, min_indent)?; + + let result = Tag::Apply { + name, + args: args.into_bump_slice(), + }; + + Ok((MadeProgress, result, state)) + } +} + +fn parse_tag_name<'a, F, E>(to_problem: F) -> impl Parser<'a, &'a str, E> +where + F: Fn(Position) -> E, + E: 'a, +{ + move |arena, state: State<'a>, min_indent: u32| match crate::ident::tag_name().parse( + arena, + state.clone(), + min_indent, + ) { + Ok(good) => Ok(good), + Err((progress, _)) => Err((progress, to_problem(state.pos()))), + } +} + +fn record_type_field<'a>() -> impl Parser<'a, AssignedField<'a, TypeAnnotation<'a>>, ETypeRecord<'a>> +{ + use crate::parser::Either::*; + use AssignedField::*; + + (move |arena, state: State<'a>, min_indent: u32| { + // You must have a field name, e.g. "email" + // using the initial pos is important for error reporting + let pos = state.pos(); + let (progress, loc_label, state) = loc!(specialize( + move |_, _| ETypeRecord::Field(pos), + lowercase_ident_keyword_e() + )) + .parse(arena, state, min_indent)?; + debug_assert_eq!(progress, MadeProgress); + + let (_, spaces, state) = + space0_e(ETypeRecord::IndentEnd).parse(arena, state, min_indent)?; + + // Having a value is optional; both `{ email }` and `{ email: blah }` work. + // (This is true in both literals and types.) + let (_, opt_loc_val, state) = optional(either!( + word1(b':', ETypeRecord::Colon), + word1(b'?', ETypeRecord::Optional) + )) + .parse(arena, state, min_indent)?; + + let val_parser = specialize_ref(ETypeRecord::Type, expression(true, false)); + + match opt_loc_val { + Some(First(_)) => { + let (_, loc_val, state) = space0_before_e(val_parser, ETypeRecord::IndentColon) + .parse(arena, state, min_indent)?; + + Ok(( + MadeProgress, + RequiredValue(loc_label, spaces, arena.alloc(loc_val)), + state, + )) + } + Some(Second(_)) => { + let (_, loc_val, state) = space0_before_e(val_parser, ETypeRecord::IndentOptional) + .parse(arena, state, min_indent)?; + + Ok(( + MadeProgress, + OptionalValue(loc_label, spaces, arena.alloc(loc_val)), + state, + )) + } + // If no value was provided, record it as a Var. + // Canonicalize will know what to do with a Var later. + None => { + let value = if !spaces.is_empty() { + SpaceAfter(arena.alloc(LabelOnly(loc_label)), spaces) + } else { + LabelOnly(loc_label) + }; + + Ok((MadeProgress, value, state)) + } + } + }) + .trace("type_annotation:record_type_field") +} + +#[inline(always)] +fn record_type<'a>( + stop_at_surface_has: bool, +) -> impl Parser<'a, TypeAnnotation<'a>, ETypeRecord<'a>> { + record!(TypeAnnotation::Record { + fields: collection_trailing_sep_e!( + word1(b'{', ETypeRecord::Open), + loc!(record_type_field()), + word1(b',', ETypeRecord::End), + word1(b'}', ETypeRecord::End), + AssignedField::SpaceBefore + ), + ext: optional(allocated(specialize_ref( + ETypeRecord::Type, + term(stop_at_surface_has) + ))) + }) + .trace("type_annotation:record_type") +} + +fn applied_type<'a>(stop_at_surface_has: bool) -> impl Parser<'a, TypeAnnotation<'a>, EType<'a>> { + map!( + and!( + specialize(EType::TApply, concrete_type()), + // Optionally parse space-separated arguments for the constructor, + // e.g. `Str Float` in `Map Str Float` + loc_applied_args_e(stop_at_surface_has) + ), + |(ctor, args): (TypeAnnotation<'a>, Vec<'a, Loc>>)| { + match &ctor { + TypeAnnotation::Apply(module_name, name, _) => { + if args.is_empty() { + // ctor is already an Apply with no args, so return it directly. + ctor + } else { + TypeAnnotation::Apply(module_name, name, args.into_bump_slice()) + } + } + TypeAnnotation::Malformed(_) => ctor, + _ => unreachable!(), + } + } + ) + .trace("type_annotation:applied_type") +} + +fn loc_applied_args_e<'a>( + stop_at_surface_has: bool, +) -> impl Parser<'a, Vec<'a, Loc>>, EType<'a>> { + zero_or_more!(loc_applied_arg(stop_at_surface_has)) +} + +// Hash & Eq & ... +fn ability_chain<'a>() -> impl Parser<'a, Vec<'a, Loc>>, EType<'a>> { + map!( + and!( + space0_before_optional_after( + specialize(EType::TApply, loc!(concrete_type())), + EType::TIndentStart, + EType::TIndentEnd, + ), + zero_or_more!(skip_first!( + word1(b'&', EType::TImplementsClause), + space0_before_optional_after( + specialize(EType::TApply, loc!(concrete_type())), + EType::TIndentStart, + EType::TIndentEnd, + ) + )) + ), + |(first_ability, mut other_abilities): ( + Loc>, + Vec<'a, Loc>> + )| { + other_abilities.insert(0, first_ability); + other_abilities + } + ) +} + +fn implements_clause<'a>() -> impl Parser<'a, Loc>, EType<'a>> { + map!( + // Suppose we are trying to parse "a implements Hash" + and!( + space0_around_ee( + // Parse "a", with appropriate spaces + specialize( + |_, pos| EType::TBadTypeVariable(pos), + loc!(map!(lowercase_ident(), Spaced::Item)), + ), + EType::TIndentStart, + EType::TIndentEnd + ), + skip_first!( + // Parse "implements"; we don't care about this keyword + word(crate::keyword::IMPLEMENTS, EType::TImplementsClause), + // Parse "Hash & ..."; this may be qualified from another module like "Hash.Hash" + absolute_column_min_indent(ability_chain()) + ) + ), + |(var, abilities): (Loc>, Vec<'a, Loc>>)| { + let abilities_region = Region::span_across( + &abilities.first().unwrap().region, + &abilities.last().unwrap().region, + ); + let region = Region::span_across(&var.region, &abilities_region); + let implements_clause = ImplementsClause { + var, + abilities: abilities.into_bump_slice(), + }; + Loc::at(region, implements_clause) + } + ) +} + +/// Parse a chain of `implements` clauses, e.g. " where a implements Hash, b implements Eq". +/// Returns the clauses and spaces before the starting "where", if there were any. +fn implements_clause_chain<'a>( +) -> impl Parser<'a, (&'a [CommentOrNewline<'a>], &'a [Loc>]), EType<'a>> { + move |arena, state: State<'a>, min_indent: u32| { + let (_, (spaces_before, ()), state) = and!( + space0_e(EType::TIndentStart), + word(crate::keyword::WHERE, EType::TWhereBar) + ) + .parse(arena, state, min_indent)?; + + // Parse the first clause (there must be one), then the rest + let (_, first_clause, state) = implements_clause().parse(arena, state, min_indent)?; + + let (_, mut clauses, state) = zero_or_more!(skip_first!( + word1(b',', EType::TImplementsClause), + implements_clause() + )) + .parse(arena, state, min_indent)?; + + // Usually the number of clauses shouldn't be too large, so this is okay + clauses.insert(0, first_clause); + + Ok(( + MadeProgress, + (spaces_before, clauses.into_bump_slice()), + state, + )) + } +} + +/// Parse a implements-abilities clause, e.g. `implements [Eq, Hash]`. +pub fn implements_abilities<'a>() -> impl Parser<'a, Loc>, EType<'a>> { + increment_min_indent(skip_first!( + // Parse "implements"; we don't care about this keyword + word(crate::keyword::IMPLEMENTS, EType::TImplementsClause), + // Parse "Hash"; this may be qualified from another module like "Hash.Hash" + space0_before_e( + loc!(map!( + collection_trailing_sep_e!( + word1(b'[', EType::TStart), + loc!(parse_implements_ability()), + word1(b',', EType::TEnd), + word1(b']', EType::TEnd), + ImplementsAbility::SpaceBefore + ), + ImplementsAbilities::Implements + )), + EType::TIndentEnd, + ) + )) +} + +fn parse_implements_ability<'a>() -> impl Parser<'a, ImplementsAbility<'a>, EType<'a>> { + increment_min_indent(record!(ImplementsAbility::ImplementsAbility { + ability: loc!(specialize(EType::TApply, concrete_type())), + impls: optional(backtrackable(space0_before_e( + loc!(map!( + specialize( + EType::TAbilityImpl, + collection_trailing_sep_e!( + word1(b'{', ETypeAbilityImpl::Open), + specialize(|e: ERecord<'_>, _| e.into(), loc!(ability_impl_field())), + word1(b',', ETypeAbilityImpl::End), + word1(b'}', ETypeAbilityImpl::End), + AssignedField::SpaceBefore + ) + ), + AbilityImpls::AbilityImpls + )), + EType::TIndentEnd + ))) + })) +} + +fn ability_impl_field<'a>() -> impl Parser<'a, AssignedField<'a, Expr<'a>>, ERecord<'a>> { + then(record_field(), move |arena, state, _, field| { + match field.to_assigned_field(arena) { + Ok(assigned_field) => Ok((MadeProgress, assigned_field, state)), + Err(FoundApplyValue) => Err((MadeProgress, ERecord::Field(state.pos()))), + } + }) +} + +fn expression<'a>( + is_trailing_comma_valid: bool, + stop_at_surface_has: bool, +) -> impl Parser<'a, Loc>, EType<'a>> { + (move |arena, state: State<'a>, min_indent: u32| { + let (p1, first, state) = space0_before_e(term(stop_at_surface_has), EType::TIndentStart) + .parse(arena, state, min_indent)?; + + let result = and![ + zero_or_more!(skip_first!( + word1(b',', EType::TFunctionArgument), + one_of![ + space0_around_ee( + term(stop_at_surface_has), + EType::TIndentStart, + EType::TIndentEnd + ), + fail(EType::TFunctionArgument) + ] + )) + .trace("type_annotation:expression:rest_args"), + skip_second!( + space0_e(EType::TIndentStart), + word2(b'-', b'>', EType::TStart) + ) + .trace("type_annotation:expression:arrow") + ] + .parse(arena, state.clone(), min_indent); + + let (progress, annot, state) = match result { + Ok((p2, (rest, space_before_arrow), state)) => { + let (p3, return_type, state) = + space0_before_e(term(stop_at_surface_has), EType::TIndentStart) + .parse(arena, state, min_indent)?; + + let region = Region::span_across(&first.region, &return_type.region); + + // prepare arguments + let mut arguments = Vec::with_capacity_in(rest.len() + 1, arena); + arguments.push(first); + arguments.extend(rest); + + if !space_before_arrow.is_empty() { + if let Some(last) = arguments.last_mut() { + let new_value = arena.alloc(last.value).after(space_before_arrow); + last.value = new_value; + } + } + + let output = arena.alloc(arguments); + + let result = Loc { + region, + value: TypeAnnotation::Function(output, arena.alloc(return_type)), + }; + let progress = p1.or(p2).or(p3); + (progress, result, state) + } + Err(err) => { + if !is_trailing_comma_valid { + let (_, comma, _) = optional(backtrackable(skip_first!( + space0_e(EType::TIndentStart), + word1(b',', EType::TStart) + ))) + .trace("check trailing comma") + .parse(arena, state.clone(), min_indent)?; + + if comma.is_some() { + // If the surrounding scope has declared that a trailing comma is not a valid state + // for a type annotation - and we found one anyway - return an error so that we can + // produce a more useful error message, knowing that the user was probably writing a + // function type and messed up the syntax somehow. + return Err(err); + } + } + + // We ran into trouble parsing the function bits; just return the single term + (p1, first, state) + } + }; + + // Finally, try to parse a where clause if there is one. + // The where clause must be at least as deep as where the type annotation started. + match implements_clause_chain().parse(arena, state.clone(), min_indent) { + Ok((where_progress, (spaces_before, implements_chain), state)) => { + let region = + Region::span_across(&annot.region, &implements_chain.last().unwrap().region); + let type_annot = if !spaces_before.is_empty() { + // We're transforming the spaces_before the 'where' + // into spaces_after the thing before the 'where' + let spaced = arena + .alloc(annot.value) + .with_spaces_after(spaces_before, annot.region); + &*arena.alloc(spaced) + } else { + &*arena.alloc(annot) + }; + let where_annot = TypeAnnotation::Where(type_annot, implements_chain); + Ok(( + where_progress.or(progress), + Loc::at(region, where_annot), + state, + )) + } + Err(_) => { + // Ran into a problem parsing a where clause; don't suppose there is one. + Ok((progress, annot, state)) + } + } + }) + .trace("type_annotation:expression") +} + +/// Parse a basic type annotation that's a combination of variables +/// (which are lowercase and unqualified, e.g. `a` in `List a`), +/// type applications (which are uppercase and optionally qualified, e.g. +/// `Int`, or the `List` in `List a` or the qualified application `Set.Set Float`), +/// and function types like `(a -> b)`. +/// +/// Type annotations can also contain records, parentheses, and the `*` character, +/// but this function is not responsible for parsing those. +// Function(&'a [TypeAnnotation<'a>], &'a TypeAnnotation<'a>), + +// /// Applying a type to some arguments (e.g. Map.Map String Int) +// Apply(&'a [&'a str], &'a str, &'a [&'a TypeAnnotation<'a>]), + +// /// A bound type variable, e.g. `a` in `(a -> a)` +// BoundVariable(&'a str), + +fn concrete_type<'a>() -> impl Parser<'a, TypeAnnotation<'a>, ETypeApply> { + move |arena: &'a Bump, state: State<'a>, min_indent: u32| { + let initial_bytes = state.bytes(); + + match crate::ident::concrete_type().parse(arena, state.clone(), min_indent) { + Ok((_, (module_name, type_name), state)) => { + let answer = TypeAnnotation::Apply(module_name, type_name, &[]); + + Ok((MadeProgress, answer, state)) + } + Err((NoProgress, _)) => Err((NoProgress, ETypeApply::End(state.pos()))), + Err((MadeProgress, _)) => { + let mut state = state.clone(); + // we made some progress, but ultimately failed. + // that means a malformed type name + let chomped = crate::ident::chomp_malformed(state.bytes()); + let delta = initial_bytes.len() - state.bytes().len(); + let parsed_str = + unsafe { std::str::from_utf8_unchecked(&initial_bytes[..chomped + delta]) }; + + state = state.advance(chomped); + + Ok((MadeProgress, TypeAnnotation::Malformed(parsed_str), state)) + } + } + } +} + +fn parse_type_variable<'a>( + stop_at_surface_has: bool, +) -> impl Parser<'a, TypeAnnotation<'a>, EType<'a>> { + move |arena, state: State<'a>, min_indent: u32| match crate::ident::lowercase_ident().parse( + arena, + state.clone(), + min_indent, + ) { + Ok((_, name, state)) => { + if name == crate::keyword::WHERE + || (name == crate::keyword::IMPLEMENTS && stop_at_surface_has) + { + Err((NoProgress, EType::TEnd(state.pos()))) + } else { + let answer = TypeAnnotation::BoundVariable(name); + + Ok((MadeProgress, answer, state)) + } + } + Err((progress, _)) => Err((progress, EType::TBadTypeVariable(state.pos()))), + } +} diff --git a/crates/compiler/parse/tests/test_parse.rs b/crates/compiler/parse/tests/test_parse.rs new file mode 100644 index 0000000000..b55e1ad269 --- /dev/null +++ b/crates/compiler/parse/tests/test_parse.rs @@ -0,0 +1,366 @@ +/// Simple tests for parsing expressions, module headers, module defs, etc. +/// Note, much more extensive tests are in the `test_syntax` crate (in test_snapshots). + +#[macro_use] +extern crate pretty_assertions; +#[macro_use] +extern crate indoc; +extern crate bumpalo; +extern crate quickcheck; + +#[macro_use(quickcheck)] +extern crate quickcheck_macros; + +extern crate roc_module; +extern crate roc_parse; + +#[cfg(test)] +mod test_parse { + use bumpalo::collections::vec::Vec; + use bumpalo::{self, Bump}; + use roc_parse::ast::Expr::{self, *}; + use roc_parse::ast::StrSegment::*; + use roc_parse::ast::{self, EscapedChar}; + use roc_parse::ast::{CommentOrNewline, StrLiteral::*}; + use roc_parse::module::module_defs; + use roc_parse::parser::{Parser, SyntaxError}; + use roc_parse::state::State; + use roc_parse::test_helpers::parse_expr_with; + use roc_region::all::{Loc, Region}; + use std::{f64, i64}; + + fn assert_parses_to<'a>(input: &'a str, expected_expr: Expr<'a>) { + let arena = Bump::new(); + let actual = parse_expr_with(&arena, input.trim()); + assert_eq!(Ok(expected_expr), actual); + } + + fn assert_parsing_fails(input: &str, _reason: SyntaxError) { + let arena = Bump::new(); + let actual = parse_expr_with(&arena, input); + + assert!(actual.is_err()); + } + + fn assert_segments Vec<'_, ast::StrSegment<'_>>>(input: &str, to_expected: E) { + let arena = Bump::new(); + let actual = parse_expr_with(&arena, arena.alloc(input)); + let expected_slice = to_expected(&arena); + let expected_expr = Expr::Str(Line(&expected_slice)); + + assert_eq!(Ok(expected_expr), actual); + } + + fn parses_with_escaped_char< + I: Fn(&str) -> String, + E: Fn(EscapedChar, &Bump) -> Vec<'_, ast::StrSegment<'_>>, + >( + to_input: I, + to_expected: E, + ) { + let arena = Bump::new(); + + // Try parsing with each of the escaped chars Roc supports + for (string, escaped) in &[ + ("\\\\", EscapedChar::Backslash), + ("\\n", EscapedChar::Newline), + ("\\r", EscapedChar::CarriageReturn), + ("\\t", EscapedChar::Tab), + ("\\\"", EscapedChar::DoubleQuote), + ] { + let actual = parse_expr_with(&arena, arena.alloc(to_input(string))); + let expected_slice = to_expected(*escaped, &arena); + let expected_expr = Expr::Str(Line(&expected_slice)); + + assert_eq!(Ok(expected_expr), actual); + } + } + + // BACKSLASH ESCAPES + + #[test] + fn string_with_escaped_char_at_end() { + parses_with_escaped_char( + |esc| format!(r#""abcd{esc}""#), + |esc, arena| bumpalo::vec![in arena; Plaintext("abcd"), EscapedChar(esc)], + ); + } + + #[test] + fn string_with_escaped_char_in_front() { + parses_with_escaped_char( + |esc| format!(r#""{esc}abcd""#), + |esc, arena| bumpalo::vec![in arena; EscapedChar(esc), Plaintext("abcd")], + ); + } + + #[test] + fn string_with_escaped_char_in_middle() { + parses_with_escaped_char( + |esc| format!(r#""ab{esc}cd""#), + |esc, arena| bumpalo::vec![in arena; Plaintext("ab"), EscapedChar(esc), Plaintext("cd")], + ); + } + + #[test] + fn string_with_multiple_escaped_chars() { + parses_with_escaped_char( + |esc| format!(r#""{esc}abc{esc}de{esc}fghi{esc}""#), + |esc, arena| bumpalo::vec![in arena; EscapedChar(esc), Plaintext("abc"), EscapedChar(esc), Plaintext("de"), EscapedChar(esc), Plaintext("fghi"), EscapedChar(esc)], + ); + } + + // UNICODE ESCAPES + + #[test] + fn unicode_escape_in_middle() { + assert_segments(r#""Hi, \u(123)!""#, |arena| { + bumpalo::vec![in arena; + Plaintext("Hi, "), + Unicode(Loc::new(8, 11, "123")), + Plaintext("!") + ] + }); + } + + #[test] + fn unicode_escape_in_front() { + assert_segments(r#""\u(1234) is a unicode char""#, |arena| { + bumpalo::vec![in arena; + Unicode(Loc::new(4, 8, "1234")), + Plaintext(" is a unicode char") + ] + }); + } + + #[test] + fn unicode_escape_in_back() { + assert_segments(r#""this is unicode: \u(1)""#, |arena| { + bumpalo::vec![in arena; + Plaintext("this is unicode: "), + Unicode(Loc::new(21, 22, "1")) + ] + }); + } + + #[test] + fn unicode_escape_multiple() { + assert_segments(r#""\u(a1) this is \u(2Bcd) unicode \u(ef97)""#, |arena| { + bumpalo::vec![in arena; + Unicode(Loc::new(4, 6, "a1")), + Plaintext(" this is "), + Unicode(Loc::new(19, 23, "2Bcd")), + Plaintext(" unicode "), + Unicode(Loc::new(36, 40, "ef97")) + ] + }); + } + + // INTERPOLATION + + #[test] + fn string_with_interpolation_in_middle() { + assert_segments(r#""Hi, \(name)!""#, |arena| { + let expr = arena.alloc(Var { + module_name: "", + ident: "name", + }); + + bumpalo::vec![in arena; + Plaintext("Hi, "), + Interpolated(Loc::new(7, 11, expr)), + Plaintext("!") + ] + }); + } + + #[test] + fn string_with_interpolation_in_front() { + assert_segments(r#""\(name), hi!""#, |arena| { + let expr = arena.alloc(Var { + module_name: "", + ident: "name", + }); + + bumpalo::vec![in arena; + Interpolated(Loc::new(3, 7, expr)), + Plaintext(", hi!") + ] + }); + } + + #[test] + fn string_with_interpolation_in_back() { + assert_segments(r#""Hello \(name)""#, |arena| { + let expr = arena.alloc(Var { + module_name: "", + ident: "name", + }); + + bumpalo::vec![in arena; + Plaintext("Hello "), + Interpolated(Loc::new(9, 13, expr)) + ] + }); + } + + #[test] + fn string_with_multiple_interpolations() { + assert_segments(r#""Hi, \(name)! How is \(project) going?""#, |arena| { + let expr1 = arena.alloc(Var { + module_name: "", + ident: "name", + }); + + let expr2 = arena.alloc(Var { + module_name: "", + ident: "project", + }); + + bumpalo::vec![in arena; + Plaintext("Hi, "), + Interpolated(Loc::new(7, 11, expr1)), + Plaintext("! How is "), + Interpolated(Loc::new(23, 30, expr2)), + Plaintext(" going?") + ] + }); + } + + #[test] + fn empty_source_file() { + assert_parsing_fails("", SyntaxError::Eof(Region::zero())); + } + + #[quickcheck] + fn all_i64_values_parse(num: i64) { + assert_parses_to(num.to_string().as_str(), Num(num.to_string().as_str())); + } + + #[quickcheck] + fn all_f64_values_parse(mut num: f64) { + // NaN, Infinity, -Infinity (these would all parse as tags in Roc) + if !num.is_finite() { + num = 0.0; + } + + // These can potentially be whole numbers. `Display` omits the decimal point for those, + // causing them to no longer be parsed as fractional numbers by Roc. + // Using `Debug` instead of `Display` ensures they always have a decimal point. + let float_string = format!("{num:?}"); + + assert_parses_to(float_string.as_str(), Float(float_string.as_str())); + } + + // SINGLE QUOTE LITERAL + #[test] + fn single_quote() { + assert_parses_to("'b'", Expr::SingleQuote("b")); + } + + #[test] + fn repro_keyword_bug() { + // Reproducing this bug requires a bizarre set of things to all be true: + // + // * Must be parsing a *module* def (nested expr defs don't repro this) + // * That top-level module def contains a def inside it + // * That inner def is defining a function + // * The name of the inner def begins with a keyword (`if`, `then`, `else`, `when`, `is`) + // + // If all of these are true, then lookups on that def get skipped over by the parser. + // If any one of the above is false, then everything works. + + let arena = Bump::new(); + let src = indoc!( + r#" + foo = \list -> + isTest = \_ -> 5 + List.map list isTest + "# + ); + let actual = module_defs() + .parse(&arena, State::new(src.as_bytes()), 0) + .map(|tuple| tuple.1); + + // It should occur twice in the debug output - once for the pattern, + // and then again for the lookup. + let occurrences = format!("{actual:?}").split("isTest").count() - 1; + + assert_eq!(occurrences, 2); + } + + #[test] + fn outdenting_newline_after_else() { + let arena = &Bump::new(); + + // highlights a problem with the else branch demanding a newline after its expression + let src = indoc!( + r#" + main = + v = \y -> if x then y else z + + 1 + "# + ); + + let state = State::new(src.as_bytes()); + let parser = module_defs(); + let parsed = parser.parse(arena, state, 0); + match parsed { + Ok((_, _, _state)) => { + // dbg!(_state); + } + Err((_, _fail)) => { + // dbg!(_fail, _state); + panic!("Failed to parse!"); + } + } + } + + #[test] + fn parse_expr_size() { + assert_eq!(std::mem::size_of::(), 40); + } + + #[test] + fn parse_two_line_comment_with_crlf() { + let src = "# foo\r\n# bar\r\n42"; + assert_parses_to( + src, + Expr::SpaceBefore( + &Expr::Num("42"), + &[ + CommentOrNewline::LineComment(" foo"), + // We used to have a bug where there was an extra CommentOrNewline::Newline between these. + CommentOrNewline::LineComment(" bar"), + ], + ), + ); + } + + // PARSE ERROR + + // TODO this should be parse error, but isn't! + // #[test] + // fn trailing_paren() { + // assert_parses_to( + // indoc!( + // r#" + // r = "foo" + // s = { left : "foo" } + + // when 0 is + // 1 -> { x: s.left, y: s.left } + // 0 -> { x: s.left, y: r } + // ) + // "# + // ), + // Str(PlainLine("")), + // ); + // } + + // TODO test for non-ASCII variables + // + // TODO verify that when a string literal contains a newline before the + // closing " it correctly updates both the line *and* column in the State. +} diff --git a/crates/compiler/problem/Cargo.toml b/crates/compiler/problem/Cargo.toml new file mode 100644 index 0000000000..d6b0d7cf4e --- /dev/null +++ b/crates/compiler/problem/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "roc_problem" +description = "Provides types to describe problems that can occur when compiling Roc code." + +authors.workspace = true +edition.workspace = true +license.workspace = true +version.workspace = true + +[dependencies] +roc_collections = { path = "../collections" } +roc_module = { path = "../module" } +roc_parse = { path = "../parse" } +roc_region = { path = "../region" } +roc_types = { path = "../types" } diff --git a/crates/compiler/problem/src/can.rs b/crates/compiler/problem/src/can.rs new file mode 100644 index 0000000000..8c7395e49d --- /dev/null +++ b/crates/compiler/problem/src/can.rs @@ -0,0 +1,684 @@ +use std::io; +use std::path::PathBuf; + +use roc_collections::all::MutSet; +use roc_module::called_via::BinOp; +use roc_module::ident::{Ident, Lowercase, ModuleName, TagName}; +use roc_module::symbol::{ModuleId, Symbol}; +use roc_parse::ast::Base; +use roc_parse::pattern::PatternType; +use roc_region::all::{Loc, Region}; +use roc_types::types::AliasKind; + +use crate::Severity; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct CycleEntry { + pub symbol: Symbol, + pub symbol_region: Region, + pub expr_region: Region, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum BadPattern { + Unsupported(PatternType), +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum ShadowKind { + Variable, + Alias(Symbol), + Opaque(Symbol), + Ability(Symbol), +} + +/// Problems that can occur in the course of canonicalization. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum Problem { + UnusedDef(Symbol, Region), + UnusedImport(Symbol, Region), + UnusedModuleImport(ModuleId, Region), + ExposedButNotDefined(Symbol), + UnknownGeneratesWith(Loc), + /// First symbol is the name of the closure with that argument + /// Bool is whether the closure is anonymous + /// Second symbol is the name of the argument that is unused + UnusedArgument(Symbol, bool, Symbol, Region), + UnusedBranchDef(Symbol, Region), + DefsOnlyUsedInRecursion(usize, Region), + PrecedenceProblem(PrecedenceProblem), + // Example: (5 = 1 + 2) is an unsupported pattern in an assignment; Int patterns aren't allowed in assignments! + UnsupportedPattern(BadPattern, Region), + Shadowing { + original_region: Region, + shadow: Loc, + kind: ShadowKind, + }, + CyclicAlias(Symbol, Region, Vec, AliasKind), + BadRecursion(Vec), + PhantomTypeArgument { + typ: Symbol, + variable_region: Region, + variable_name: Lowercase, + alias_kind: AliasKind, + }, + UnboundTypeVariable { + typ: Symbol, + num_unbound: usize, + one_occurrence: Region, + kind: AliasKind, + }, + DuplicateRecordFieldValue { + field_name: Lowercase, + record_region: Region, + field_region: Region, + replaced_region: Region, + }, + DuplicateRecordFieldType { + field_name: Lowercase, + record_region: Region, + field_region: Region, + replaced_region: Region, + }, + InvalidOptionalValue { + field_name: Lowercase, + record_region: Region, + field_region: Region, + }, + + DuplicateTag { + tag_name: TagName, + tag_union_region: Region, + tag_region: Region, + replaced_region: Region, + }, + RuntimeError(RuntimeError), + SignatureDefMismatch { + annotation_pattern: Region, + def_pattern: Region, + }, + InvalidAliasRigid { + alias_name: Symbol, + region: Region, + }, + InvalidInterpolation(Region), + InvalidHexadecimal(Region), + InvalidUnicodeCodePt(Region), + NestedDatatype { + alias: Symbol, + def_region: Region, + differing_recursion_region: Region, + }, + InvalidExtensionType { + region: Region, + kind: ExtensionTypeKind, + }, + AbilityHasTypeVariables { + name: Symbol, + variables_region: Region, + }, + ImplementsClauseIsNotAbility { + region: Region, + }, + IllegalImplementsClause { + region: Region, + }, + DuplicateImplementsAbility { + ability: Symbol, + region: Region, + }, + AbilityMemberMissingImplementsClause { + member: Symbol, + ability: Symbol, + region: Region, + }, + AbilityMemberMultipleBoundVars { + member: Symbol, + ability: Symbol, + span_implements_clauses: Region, + bound_var_names: Vec, + }, + AbilityNotOnToplevel { + region: Region, + }, + AbilityUsedAsType(Lowercase, Symbol, Region), + NestedSpecialization(Symbol, Region), + IllegalDerivedAbility(Region), + ImplementationNotFound { + member: Symbol, + region: Region, + }, + NotAnAbilityMember { + ability: Symbol, + name: String, + region: Region, + }, + OptionalAbilityImpl { + ability: Symbol, + region: Region, + }, + QualifiedAbilityImpl { + region: Region, + }, + AbilityImplNotIdent { + region: Region, + }, + DuplicateImpl { + original: Region, + duplicate: Region, + }, + NotAnAbility(Region), + ImplementsNonRequired { + region: Region, + ability: Symbol, + not_required: Vec, + }, + DoesNotImplementAbility { + region: Region, + ability: Symbol, + not_implemented: Vec, + }, + NotBoundInAllPatterns { + unbound_symbol: Symbol, + region: Region, + }, + NoIdentifiersIntroduced(Region), + OverloadedSpecialization { + overload: Region, + original_opaque: Symbol, + ability_member: Symbol, + }, + UnnecessaryOutputWildcard { + region: Region, + }, + MultipleListRestPattern { + region: Region, + }, + BadTypeArguments { + symbol: Symbol, + region: Region, + alias_needs: u8, + type_got: u8, + alias_kind: AliasKind, + }, + UnappliedCrash { + region: Region, + }, + OverAppliedCrash { + region: Region, + }, + FileProblem { + filename: PathBuf, + error: io::ErrorKind, + }, +} + +impl Problem { + pub fn severity(&self) -> Severity { + use Severity::{Fatal, RuntimeError, Warning}; + + match self { + Problem::UnusedDef(_, _) => Warning, + Problem::UnusedImport(_, _) => Warning, + Problem::UnusedModuleImport(_, _) => Warning, + Problem::ExposedButNotDefined(_) => RuntimeError, + Problem::UnknownGeneratesWith(_) => RuntimeError, + Problem::UnusedArgument(_, _, _, _) => Warning, + Problem::UnusedBranchDef(_, _) => Warning, + Problem::PrecedenceProblem(_) => RuntimeError, + Problem::UnsupportedPattern(_, _) => RuntimeError, + Problem::Shadowing { .. } => RuntimeError, + Problem::CyclicAlias(..) => RuntimeError, + Problem::BadRecursion(_) => RuntimeError, + Problem::PhantomTypeArgument { .. } => Warning, + Problem::UnboundTypeVariable { .. } => RuntimeError, + Problem::DuplicateRecordFieldValue { .. } => Warning, + Problem::DuplicateRecordFieldType { .. } => RuntimeError, + Problem::InvalidOptionalValue { .. } => RuntimeError, + Problem::DuplicateTag { .. } => RuntimeError, + Problem::RuntimeError(_) => RuntimeError, + Problem::SignatureDefMismatch { .. } => RuntimeError, + Problem::InvalidAliasRigid { .. } => RuntimeError, + Problem::InvalidInterpolation(_) => RuntimeError, + Problem::InvalidHexadecimal(_) => RuntimeError, + Problem::InvalidUnicodeCodePt(_) => RuntimeError, + Problem::NestedDatatype { .. } => RuntimeError, + Problem::InvalidExtensionType { .. } => RuntimeError, + Problem::AbilityHasTypeVariables { .. } => RuntimeError, + Problem::ImplementsClauseIsNotAbility { .. } => RuntimeError, + Problem::IllegalImplementsClause { .. } => RuntimeError, + Problem::DuplicateImplementsAbility { .. } => Warning, + Problem::AbilityMemberMissingImplementsClause { .. } => RuntimeError, + Problem::AbilityMemberMultipleBoundVars { .. } => RuntimeError, + Problem::AbilityNotOnToplevel { .. } => RuntimeError, // Ideally, could be compiled + Problem::AbilityUsedAsType(_, _, _) => RuntimeError, + Problem::NestedSpecialization(_, _) => RuntimeError, // Ideally, could be compiled + Problem::IllegalDerivedAbility(_) => RuntimeError, + Problem::ImplementationNotFound { .. } => RuntimeError, + Problem::NotAnAbilityMember { .. } => RuntimeError, + Problem::OptionalAbilityImpl { .. } => RuntimeError, + Problem::QualifiedAbilityImpl { .. } => RuntimeError, + Problem::AbilityImplNotIdent { .. } => RuntimeError, + Problem::DuplicateImpl { .. } => Warning, // First impl is used at runtime + Problem::NotAnAbility(_) => Warning, + Problem::ImplementsNonRequired { .. } => Warning, + Problem::DoesNotImplementAbility { .. } => RuntimeError, + Problem::NotBoundInAllPatterns { .. } => RuntimeError, + Problem::NoIdentifiersIntroduced(_) => Warning, + Problem::OverloadedSpecialization { .. } => Warning, // Ideally, will compile + Problem::UnnecessaryOutputWildcard { .. } => Warning, + // TODO: sometimes this can just be a warning, e.g. if you have [1, .., .., 2] but we + // don't catch that yet. + Problem::MultipleListRestPattern { .. } => RuntimeError, + Problem::BadTypeArguments { .. } => RuntimeError, + // TODO: this can be a warning instead if we recover the program by + // injecting a crash message + Problem::UnappliedCrash { .. } => RuntimeError, + Problem::OverAppliedCrash { .. } => RuntimeError, + Problem::DefsOnlyUsedInRecursion(_, _) => Warning, + Problem::FileProblem { .. } => Fatal, + } + } + + /// Returns a Region value from the Problem, if possible. + /// Some problems have more than one region; in those cases, + /// this tries to pick the one that's closest to the original + /// definition site, since that's what the REPL uses this for: + /// filtering out errors and warnings from wrapped defs based + /// on their Region being outside the expression currently being evaluated. + pub fn region(&self) -> Option { + match self { + Problem::UnusedDef(_, region) + | Problem::Shadowing { + original_region: region, + .. + } + | Problem::UnusedImport(_, region) + | Problem::UnusedModuleImport(_, region) + | Problem::UnknownGeneratesWith(Loc { region, .. }) + | Problem::UnusedArgument(_, _, _, region) + | Problem::UnusedBranchDef(_, region) + | Problem::PrecedenceProblem(PrecedenceProblem::BothNonAssociative(region, _, _)) + | Problem::UnsupportedPattern(_, region) + | Problem::CyclicAlias(_, region, _, _) + | Problem::PhantomTypeArgument { + variable_region: region, + .. + } + | Problem::UnboundTypeVariable { + one_occurrence: region, + .. + } + | Problem::DuplicateRecordFieldValue { + record_region: region, + .. + } + | Problem::DuplicateRecordFieldType { + record_region: region, + .. + } + | Problem::InvalidOptionalValue { + record_region: region, + .. + } + | Problem::DuplicateTag { + tag_union_region: region, + .. + } + | Problem::RuntimeError(RuntimeError::Shadowing { + original_region: region, + .. + }) + | Problem::RuntimeError(RuntimeError::InvalidOptionalValue { + record_region: region, + .. + }) + | Problem::RuntimeError(RuntimeError::UnsupportedPattern(region)) + | Problem::RuntimeError(RuntimeError::MalformedPattern(_, region)) + | Problem::RuntimeError(RuntimeError::LookupNotInScope { + loc_name: Loc { region, .. }, + suggestion_options: _, + underscored_suggestion_region: _, + }) + | Problem::RuntimeError(RuntimeError::OpaqueNotDefined { + usage: Loc { region, .. }, + .. + }) + | Problem::RuntimeError(RuntimeError::OpaqueOutsideScope { + referenced_region: region, + .. + }) + | Problem::RuntimeError(RuntimeError::OpaqueNotApplied(Loc { region, .. })) + | Problem::RuntimeError(RuntimeError::OpaqueAppliedToMultipleArgs(region)) + | Problem::RuntimeError(RuntimeError::ValueNotExposed { region, .. }) + | Problem::RuntimeError(RuntimeError::ModuleNotImported { region, .. }) + | Problem::RuntimeError(RuntimeError::InvalidPrecedence(_, region)) + | Problem::RuntimeError(RuntimeError::MalformedIdentifier(_, _, region)) + | Problem::RuntimeError(RuntimeError::MalformedTypeName(_, region)) + | Problem::RuntimeError(RuntimeError::MalformedClosure(region)) + | Problem::RuntimeError(RuntimeError::InvalidRecordUpdate { region }) + | Problem::RuntimeError(RuntimeError::InvalidFloat(_, region, _)) + | Problem::RuntimeError(RuntimeError::InvalidInt(_, _, region, _)) + | Problem::RuntimeError(RuntimeError::InvalidInterpolation(region)) + | Problem::RuntimeError(RuntimeError::InvalidHexadecimal(region)) + | Problem::RuntimeError(RuntimeError::InvalidUnicodeCodePt(region)) + | Problem::RuntimeError(RuntimeError::EmptySingleQuote(region)) + | Problem::RuntimeError(RuntimeError::MultipleCharsInSingleQuote(region)) + | Problem::RuntimeError(RuntimeError::DegenerateBranch(region)) + | Problem::RuntimeError(RuntimeError::MultipleRecordBuilders(region)) + | Problem::RuntimeError(RuntimeError::UnappliedRecordBuilder(region)) + | Problem::InvalidAliasRigid { region, .. } + | Problem::InvalidInterpolation(region) + | Problem::InvalidHexadecimal(region) + | Problem::InvalidUnicodeCodePt(region) + | Problem::NestedDatatype { + def_region: region, .. + } + | Problem::InvalidExtensionType { region, .. } + | Problem::AbilityHasTypeVariables { + variables_region: region, + .. + } + | Problem::ImplementsClauseIsNotAbility { region } + | Problem::IllegalImplementsClause { region } + | Problem::DuplicateImplementsAbility { region, .. } + | Problem::AbilityMemberMissingImplementsClause { region, .. } + | Problem::AbilityMemberMultipleBoundVars { + span_implements_clauses: region, + .. + } + | Problem::AbilityNotOnToplevel { region } + | Problem::AbilityUsedAsType(_, _, region) + | Problem::NestedSpecialization(_, region) + | Problem::IllegalDerivedAbility(region) + | Problem::ImplementationNotFound { region, .. } + | Problem::NotAnAbilityMember { region, .. } + | Problem::OptionalAbilityImpl { region, .. } + | Problem::QualifiedAbilityImpl { region } + | Problem::AbilityImplNotIdent { region } + | Problem::DuplicateImpl { + original: region, .. + } + | Problem::NotAnAbility(region) + | Problem::ImplementsNonRequired { region, .. } + | Problem::DoesNotImplementAbility { region, .. } + | Problem::NoIdentifiersIntroduced(region) + | Problem::OverloadedSpecialization { + overload: region, .. + } + | Problem::NotBoundInAllPatterns { region, .. } + | Problem::SignatureDefMismatch { + def_pattern: region, + .. + } + | Problem::MultipleListRestPattern { region } + | Problem::BadTypeArguments { region, .. } + | Problem::UnnecessaryOutputWildcard { region } + | Problem::OverAppliedCrash { region } + | Problem::UnappliedCrash { region } + | Problem::DefsOnlyUsedInRecursion(_, region) => Some(*region), + Problem::RuntimeError(RuntimeError::CircularDef(cycle_entries)) + | Problem::BadRecursion(cycle_entries) => { + cycle_entries.first().map(|entry| entry.expr_region) + } + Problem::RuntimeError(RuntimeError::UnresolvedTypeVar) + | Problem::RuntimeError(RuntimeError::ErroneousType) + | Problem::RuntimeError(RuntimeError::NonExhaustivePattern) + | Problem::RuntimeError(RuntimeError::NoImplementation) + | Problem::RuntimeError(RuntimeError::VoidValue) + | Problem::RuntimeError(RuntimeError::ExposedButNotDefined(_)) + | Problem::RuntimeError(RuntimeError::NoImplementationNamed { .. }) + | Problem::FileProblem { .. } + | Problem::ExposedButNotDefined(_) => None, + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum ExtensionTypeKind { + Record, + TagUnion, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum PrecedenceProblem { + BothNonAssociative(Region, Loc, Loc), +} + +impl PrecedenceProblem { + pub fn region(&self) -> Region { + match self { + PrecedenceProblem::BothNonAssociative(region, _, _) => *region, + } + } +} + +/// Enum to store the various types of errors that can cause parsing an integer to fail. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum IntErrorKind { + /// Value being parsed is empty. + /// + /// Among other causes, this variant will be constructed when parsing an empty string. + /// In roc, this can happen with non-base-10 literals, e.g. `0x` or `0b` without any digits + Empty, + /// Contains an invalid digit. + /// + /// Among other causes, this variant will be constructed when parsing a string that + /// contains a letter. + InvalidDigit, + /// Integer is too large to store in target integer type. + Overflow, + /// Integer is too small to store in target integer type. + Underflow, + /// This is an integer, but it has a float numeric suffix. + FloatSuffix, + /// The integer literal overflows the width of the suffix associated with it. + OverflowsSuffix { + suffix_type: &'static str, + max_value: u128, + }, + /// The integer literal underflows the width of the suffix associated with it. + UnderflowsSuffix { + suffix_type: &'static str, + min_value: i128, + }, +} + +/// Enum to store the various types of errors that can cause parsing a float to fail. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum FloatErrorKind { + /// Probably an invalid digit + Error, + /// the literal is too small for f64 + NegativeInfinity, + /// the literal is too large for f64 + PositiveInfinity, + /// This is a float, but it has an integer numeric suffix. + IntSuffix, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum RuntimeError { + Shadowing { + original_region: Region, + shadow: Loc, + kind: ShadowKind, + }, + InvalidOptionalValue { + field_name: Lowercase, + record_region: Region, + field_region: Region, + }, + // Example: (5 = 1 + 2) is an unsupported pattern in an assignment; Int patterns aren't allowed in assignments! + UnsupportedPattern(Region), + // Example: when 1 is 1.X -> 32 + MalformedPattern(MalformedPatternProblem, Region), + + UnresolvedTypeVar, + ErroneousType, + + LookupNotInScope { + loc_name: Loc, + /// All of the names in scope (for the error message) + suggestion_options: MutSet>, + /// If the unfound variable is `name` and there's an ignored variable called `_name`, + /// this is the region where `_name` is defined (for the error message) + underscored_suggestion_region: Option, + }, + OpaqueNotDefined { + usage: Loc, + opaques_in_scope: MutSet>, + opt_defined_alias: Option, + }, + OpaqueOutsideScope { + opaque: Ident, + referenced_region: Region, + imported_region: Region, + }, + OpaqueNotApplied(Loc), + OpaqueAppliedToMultipleArgs(Region), + ValueNotExposed { + module_name: ModuleName, + ident: Ident, + region: Region, + exposed_values: Vec, + }, + /// A module was referenced, but hasn't been imported anywhere in the program + /// + /// An example would be: + /// ```roc + /// app "hello" + /// packages { pf: "platform/main.roc" } + /// imports [pf.Stdout] + /// provides [main] to pf + /// + /// main : Task.Task {} [] // Task isn't imported! + /// main = Stdout.line "I'm a Roc application!" + /// ``` + ModuleNotImported { + /// The name of the module that was referenced + module_name: ModuleName, + /// A list of modules which *have* been imported + imported_modules: MutSet>, + /// Where the problem occurred + region: Region, + /// Whether or not the module exists at all + /// + /// This is used to suggest that the user import the module, as opposed to fix a + /// typo in the spelling. For example, if the user typed `Task`, and the platform + /// exposes a `Task` module that hasn't been imported, we can sugguest that they + /// add the import statement. + /// + /// On the other hand, if the user typed `Tesk`, they might want to check their + /// spelling. + /// + /// If unsure, this should be set to `false` + module_exists: bool, + }, + InvalidPrecedence(PrecedenceProblem, Region), + MalformedIdentifier(Box, roc_parse::ident::BadIdent, Region), + MalformedTypeName(Box, Region), + MalformedClosure(Region), + InvalidRecordUpdate { + region: Region, + }, + InvalidFloat(FloatErrorKind, Region, Box), + InvalidInt(IntErrorKind, Base, Region, Box), + CircularDef(Vec), + + NonExhaustivePattern, + + InvalidInterpolation(Region), + InvalidHexadecimal(Region), + InvalidUnicodeCodePt(Region), + + /// When the author specifies a type annotation but no implementation + NoImplementationNamed { + def_symbol: Symbol, + }, + NoImplementation, + + /// cases where the `[]` value (or equivalently, `forall a. a`) pops up + VoidValue, + + ExposedButNotDefined(Symbol), + + /// where '' + EmptySingleQuote(Region), + /// where 'aa' + MultipleCharsInSingleQuote(Region), + + DegenerateBranch(Region), + + MultipleRecordBuilders(Region), + UnappliedRecordBuilder(Region), +} + +impl RuntimeError { + pub fn runtime_message(self) -> String { + use RuntimeError::*; + + match self { + DegenerateBranch(region) => { + format!( + "Hit a branch pattern that does not bind all symbols its body needs, at {region:?}" + ) + } + err => format!("{err:?}"), + } + } + + pub fn region(&self) -> Region { + match self { + RuntimeError::Shadowing { shadow, .. } => shadow.region, + RuntimeError::InvalidOptionalValue { field_region, .. } => *field_region, + RuntimeError::UnsupportedPattern(region) + | RuntimeError::MalformedPattern(_, region) + | RuntimeError::OpaqueOutsideScope { + referenced_region: region, + .. + } + | RuntimeError::OpaqueAppliedToMultipleArgs(region) + | RuntimeError::ValueNotExposed { region, .. } + | RuntimeError::ModuleNotImported { region, .. } + | RuntimeError::InvalidPrecedence(_, region) + | RuntimeError::MalformedIdentifier(_, _, region) + | RuntimeError::MalformedTypeName(_, region) + | RuntimeError::MalformedClosure(region) + | RuntimeError::InvalidRecordUpdate { region } + | RuntimeError::InvalidFloat(_, region, _) + | RuntimeError::InvalidInt(_, _, region, _) + | RuntimeError::EmptySingleQuote(region) + | RuntimeError::MultipleCharsInSingleQuote(region) + | RuntimeError::DegenerateBranch(region) + | RuntimeError::InvalidInterpolation(region) + | RuntimeError::InvalidHexadecimal(region) + | RuntimeError::MultipleRecordBuilders(region) + | RuntimeError::UnappliedRecordBuilder(region) + | RuntimeError::InvalidUnicodeCodePt(region) => *region, + RuntimeError::UnresolvedTypeVar | RuntimeError::ErroneousType => Region::zero(), + RuntimeError::LookupNotInScope { loc_name, .. } => loc_name.region, + RuntimeError::OpaqueNotDefined { usage, .. } => usage.region, + RuntimeError::OpaqueNotApplied(ident) => ident.region, + RuntimeError::CircularDef(cycle) => cycle[0].symbol_region, + RuntimeError::NonExhaustivePattern => Region::zero(), + RuntimeError::NoImplementationNamed { .. } + | RuntimeError::NoImplementation + | RuntimeError::VoidValue + | RuntimeError::ExposedButNotDefined(_) => Region::zero(), + } + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum MalformedPatternProblem { + MalformedInt, + MalformedFloat, + MalformedBase(Base), + Unknown, + QualifiedIdentifier, + BadIdent(roc_parse::ident::BadIdent), + EmptySingleQuote, + MultipleCharsInSingleQuote, + DuplicateListRestPattern, +} diff --git a/crates/compiler/problem/src/lib.rs b/crates/compiler/problem/src/lib.rs new file mode 100644 index 0000000000..ce5ca0d9d4 --- /dev/null +++ b/crates/compiler/problem/src/lib.rs @@ -0,0 +1,21 @@ +//! Provides types to describe problems that can occur when compiling Roc code. +#![warn(clippy::dbg_macro)] +// See github.com/roc-lang/roc/issues/800 for discussion of the large_enum_variant check. +#![allow(clippy::large_enum_variant)] +pub mod can; + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum Severity { + /// This should stop compilation in all cases. + /// Due to delayed loading of ingested files, this is wanted behaviour over a runtime error. + Fatal, + + /// This will cause a runtime error if some code get srun + /// (e.g. type mismatch, naming error) + RuntimeError, + + /// This will never cause the code to misbehave, + /// but should be cleaned up + /// (e.g. unused def, unused import) + Warning, +} diff --git a/crates/compiler/region/Cargo.toml b/crates/compiler/region/Cargo.toml new file mode 100644 index 0000000000..bc82774e74 --- /dev/null +++ b/crates/compiler/region/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "roc_region" +description = "Data structures for storing source-code-location information, used heavily for contextual error messages." + +authors.workspace = true +edition.workspace = true +license.workspace = true +version.workspace = true + +[dependencies] +static_assertions.workspace = true diff --git a/crates/compiler/region/src/all.rs b/crates/compiler/region/src/all.rs new file mode 100644 index 0000000000..53ee9c6e2e --- /dev/null +++ b/crates/compiler/region/src/all.rs @@ -0,0 +1,478 @@ +use std::fmt::{self, Debug}; + +#[derive(Copy, Clone, Eq, PartialEq, PartialOrd, Ord, Hash, Default)] +pub struct Region { + start: Position, + end: Position, +} + +impl Region { + pub const fn zero() -> Self { + Region { + start: Position::zero(), + end: Position::zero(), + } + } + + pub const fn new(start: Position, end: Position) -> Self { + Self { start, end } + } + + pub fn contains(&self, other: &Self) -> bool { + self.start <= other.start && self.end >= other.end + } + + pub fn contains_pos(&self, pos: Position) -> bool { + self.start <= pos && self.end >= pos + } + + pub fn is_empty(&self) -> bool { + self.start == self.end + } + + pub const fn len(&self) -> u32 { + self.end.offset - self.start.offset + } + + pub fn span_across(start: &Region, end: &Region) -> Self { + Region { + start: start.start, + end: end.end, + } + } + + pub fn across_all<'a, I>(regions: I) -> Self + where + I: IntoIterator, + { + let mut it = regions.into_iter(); + + if let Some(first) = it.next() { + let mut result = *first; + + for r in it { + result = Self::span_across(&result, r); + } + + result + } else { + Self::zero() + } + } + + pub const fn from_pos(pos: Position) -> Self { + Region { + start: pos, + end: pos.bump_column(1), + } + } + + pub const fn start(&self) -> Position { + self.start + } + + pub const fn end(&self) -> Position { + self.end + } + + pub const fn between(start: Position, end: Position) -> Self { + Self::new(start, end) + } +} + +// Region is used all over the place. Avoid increasing its size! +static_assertions::assert_eq_size!([u8; 8], Region); + +impl fmt::Debug for Region { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + if self.start == Position::zero() && self.end == Position::zero() { + // In tests, it's super common to set all Located values to 0. + // Also in tests, we don't want to bother printing the locations + // because it makes failed assertions much harder to read. + write!(f, "…") + } else { + write!(f, "@{}-{}", self.start.offset, self.end.offset,) + } + } +} + +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] +pub struct Position { + pub offset: u32, +} + +impl Position { + pub const fn zero() -> Position { + Position { offset: 0 } + } + + pub const fn new(offset: u32) -> Position { + Position { offset } + } + + #[must_use] + pub const fn bump_column(self, count: u32) -> Self { + Self { + offset: self.offset + count, + } + } + + #[must_use] + pub fn bump_invisible(self, count: u32) -> Self { + Self { + offset: self.offset + count, + } + } + + #[must_use] + pub fn bump_newline(self) -> Self { + Self { + offset: self.offset + 1, + } + } + + #[must_use] + pub const fn sub(self, count: u32) -> Self { + Self { + offset: self.offset - count, + } + } + + pub fn byte_offset(&self) -> usize { + self.offset as usize + } +} + +impl Debug for Position { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "@")?; + self.offset.fmt(f) + } +} + +#[derive(Debug, Copy, Clone, Eq, PartialEq, PartialOrd, Ord, Hash, Default)] +pub struct LineColumn { + pub line: u32, + pub column: u32, +} + +impl LineColumn { + pub const fn zero() -> Self { + LineColumn { line: 0, column: 0 } + } + + #[must_use] + pub const fn bump_column(self, count: u32) -> Self { + Self { + line: self.line, + column: self.column + count, + } + } +} + +#[derive(Copy, Clone, Eq, PartialEq, PartialOrd, Ord, Hash, Default)] +pub struct LineColumnRegion { + pub start: LineColumn, + pub end: LineColumn, +} + +impl LineColumnRegion { + pub const fn new(start: LineColumn, end: LineColumn) -> Self { + LineColumnRegion { start, end } + } + + pub const fn zero() -> Self { + LineColumnRegion { + start: LineColumn::zero(), + end: LineColumn::zero(), + } + } + + pub fn contains(&self, other: &Self) -> bool { + use std::cmp::Ordering::*; + match self.start.line.cmp(&other.start.line) { + Greater => false, + Equal => match self.end.line.cmp(&other.end.line) { + Less => false, + Equal => { + self.start.column <= other.start.column && self.end.column >= other.end.column + } + Greater => self.start.column >= other.start.column, + }, + Less => match self.end.line.cmp(&other.end.line) { + Less => false, + Equal => self.end.column >= other.end.column, + Greater => true, + }, + } + } + + pub fn includes(&self, lc: LineColumn) -> bool { + self.contains(&Self::from_pos(lc)) + } + + pub const fn from_pos(pos: LineColumn) -> Self { + Self { + start: pos, + end: pos.bump_column(1), + } + } + + pub fn is_empty(&self) -> bool { + self.end.line == self.start.line && self.start.column == self.end.column + } + + pub fn span_across(start: &LineColumnRegion, end: &LineColumnRegion) -> Self { + LineColumnRegion { + start: start.start, + end: end.end, + } + } + + pub fn across_all<'a, I>(regions: I) -> Self + where + I: IntoIterator, + { + let mut it = regions.into_iter(); + + if let Some(first) = it.next() { + let mut result = *first; + + for r in it { + result = Self::span_across(&result, r); + } + + result + } else { + Self::zero() + } + } + + pub fn lines_between(&self, other: &LineColumnRegion) -> u32 { + if self.end.line <= other.start.line { + other.start.line - self.end.line + } else if self.start.line >= other.end.line { + self.start.line - other.end.line + } else { + // intersection + 0 + } + } + + pub const fn start(&self) -> LineColumn { + self.start + } + + pub const fn end(&self) -> LineColumn { + self.end + } +} + +impl fmt::Debug for LineColumnRegion { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + if self.start.line == 0 + && self.start.column == 0 + && self.end.line == 0 + && self.end.column == 0 + { + // In tests, it's super common to set all Located values to 0. + // Also in tests, we don't want to bother printing the locations + // because it makes failed assertions much harder to read. + write!(f, "…") + } else { + write!( + f, + "|L {}-{}, C {}-{}|", + self.start.line, self.end.line, self.start.column, self.end.column, + ) + } + } +} + +#[derive(Clone, Eq, Copy, PartialEq, PartialOrd, Ord, Hash)] +pub struct Loc { + pub region: Region, + pub value: T, +} + +impl Loc { + pub const fn new(start: u32, end: u32, value: T) -> Loc { + let region = Region::new(Position::new(start), Position::new(end)); + Loc { region, value } + } + + pub const fn at(region: Region, value: T) -> Loc { + Loc { region, value } + } + + pub const fn at_zero(value: T) -> Loc { + let region = Region::zero(); + Loc { region, value } + } +} + +impl Loc { + pub fn with_value(&self, value: U) -> Loc { + Loc { + region: self.region, + value, + } + } + + pub fn map(&self, transform: F) -> Loc + where + F: (FnOnce(&T) -> U), + { + Loc { + region: self.region, + value: transform(&self.value), + } + } + + pub fn map_owned(self, transform: F) -> Loc + where + F: (FnOnce(T) -> U), + { + Loc { + region: self.region, + value: transform(self.value), + } + } + + pub fn byte_range(&self) -> std::ops::Range { + self.region.start.byte_offset()..self.region.end.byte_offset() + } +} + +impl fmt::Debug for Loc +where + T: fmt::Debug, +{ + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let region = self.region; + + if region.start == Position::zero() && region.end == Position::zero() { + // In tests, it's super common to set all Located values to 0. + // Also in tests, we don't want to bother printing the locations + // because it makes failed assertions much harder to read. + self.value.fmt(f) + } else if f.alternate() { + write!(f, "{:?} {:#?}", region, self.value) + } else { + write!(f, "{:?} {:?}", region, self.value) + } + } +} + +#[derive(Debug)] +pub struct LineInfo { + line_offsets: Vec, +} + +impl LineInfo { + pub fn new(src: &str) -> LineInfo { + let mut line_offsets = vec![0]; + line_offsets.extend(src.match_indices('\n').map(|(offset, _)| offset as u32 + 1)); + LineInfo { line_offsets } + } + + pub fn convert_offset(&self, offset: u32) -> LineColumn { + let search = self.line_offsets.binary_search(&offset); + let line = match search { + Ok(i) => i, + Err(i) => i - 1, + }; + let column = offset - self.line_offsets[line]; + LineColumn { + line: line as u32, + column, + } + } + + pub fn convert_pos(&self, pos: Position) -> LineColumn { + self.convert_offset(pos.offset) + } + + pub fn convert_region(&self, region: Region) -> LineColumnRegion { + LineColumnRegion { + start: self.convert_pos(region.start()), + end: self.convert_pos(region.end()), + } + } + + pub fn convert_line_column(&self, lc: LineColumn) -> Position { + let offset = self.line_offsets[lc.line as usize] + lc.column; + Position::new(offset) + } + + pub fn convert_line_column_region(&self, lc_region: LineColumnRegion) -> Region { + let start = self.convert_line_column(lc_region.start); + let end = self.convert_line_column(lc_region.end); + Region::new(start, end) + } + + pub fn num_lines(&self) -> u32 { + self.line_offsets.len() as u32 + } +} + +#[test] +fn test_line_info() { + fn char_at_line<'a>(lines: &[&'a str], line_column: LineColumn) -> &'a str { + let line = line_column.line as usize; + let line_text = if line < lines.len() { lines[line] } else { "" }; + let column = line_column.column as usize; + if column == line_text.len() { + "\n" + } else { + &line_text[column..column + 1] + } + } + + fn check_correctness(lines: &[&str]) { + let mut input = String::new(); + for (i, line) in lines.iter().enumerate() { + if i > 0 { + input.push('\n'); + } + input.push_str(line); + } + let info = LineInfo::new(&input); + + let mut last: Option = None; + + for offset in 0..=input.len() { + let expected = if offset < input.len() { + &input[offset..offset + 1] + } else { + "\n" // HACK! pretend there's an extra newline on the end, strictly so we can do the comparison + }; + println!("checking {input:?} {offset:?}, expecting {expected:?}"); + let line_column = info.convert_offset(offset as u32); + assert!( + Some(line_column) > last, + "{:?} > {:?}", + Some(line_column), + last + ); + assert_eq!(expected, char_at_line(lines, line_column)); + last = Some(line_column); + } + + assert_eq!( + info.convert_offset(input.len() as u32), + LineColumn { + line: lines.len().saturating_sub(1) as u32, + column: lines.last().map(|l| l.len()).unwrap_or(0) as u32, + } + ) + } + + check_correctness(&["", "abc", "def", "", "gi"]); + + check_correctness(&[]); + + check_correctness(&["a"]); + + check_correctness(&["", ""]); +} diff --git a/crates/compiler/region/src/lib.rs b/crates/compiler/region/src/lib.rs new file mode 100644 index 0000000000..cb7f5ba923 --- /dev/null +++ b/crates/compiler/region/src/lib.rs @@ -0,0 +1,7 @@ +//! Data structures for storing source-code-location information, used heavily +//! for contextual error messages. +#![warn(clippy::dbg_macro)] +// See github.com/roc-lang/roc/issues/800 for discussion of the large_enum_variant check. +#![allow(clippy::large_enum_variant)] + +pub mod all; diff --git a/crates/compiler/roc_target/Cargo.toml b/crates/compiler/roc_target/Cargo.toml new file mode 100644 index 0000000000..e75e309667 --- /dev/null +++ b/crates/compiler/roc_target/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "roc_target" +description = "Provides types and helpers for compiler targets such as default_x86_64." + +authors.workspace = true +edition.workspace = true +license.workspace = true +version.workspace = true + +[dependencies] +strum.workspace = true +strum_macros.workspace = true +target-lexicon.workspace = true diff --git a/crates/compiler/roc_target/src/lib.rs b/crates/compiler/roc_target/src/lib.rs new file mode 100644 index 0000000000..334041b994 --- /dev/null +++ b/crates/compiler/roc_target/src/lib.rs @@ -0,0 +1,330 @@ +//! Provides types and helpers for compiler targets such as `default_x86_64`. +#![warn(clippy::dbg_macro)] +// See github.com/roc-lang/roc/issues/800 for discussion of the large_enum_variant check. +#![allow(clippy::large_enum_variant)] + +use strum_macros::{EnumCount, EnumIter, EnumString, IntoStaticStr}; +use target_lexicon::Triple; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum OperatingSystem { + Windows, + Unix, + Wasi, +} + +impl OperatingSystem { + pub const fn new(target: target_lexicon::OperatingSystem) -> Option { + match target { + target_lexicon::OperatingSystem::Windows => Some(OperatingSystem::Windows), + target_lexicon::OperatingSystem::Wasi => Some(OperatingSystem::Wasi), + target_lexicon::OperatingSystem::Linux => Some(OperatingSystem::Unix), + target_lexicon::OperatingSystem::MacOSX { .. } => Some(OperatingSystem::Unix), + target_lexicon::OperatingSystem::Darwin => Some(OperatingSystem::Unix), + target_lexicon::OperatingSystem::Unknown => Some(OperatingSystem::Unix), + _ => None, + } + } + + pub const fn object_file_ext(&self) -> &str { + match self { + OperatingSystem::Windows => "obj", + OperatingSystem::Unix => "o", + OperatingSystem::Wasi => "wasm", + } + } + + pub const fn static_library_file_ext(&self) -> &str { + match self { + OperatingSystem::Windows => "lib", + OperatingSystem::Unix => "a", + OperatingSystem::Wasi => "wasm", + } + } + + pub const fn executable_file_ext(&self) -> Option<&str> { + match self { + OperatingSystem::Windows => Some("exe"), + OperatingSystem::Unix => None, + OperatingSystem::Wasi => Some("wasm"), + } + } +} + +impl From for OperatingSystem { + fn from(target: target_lexicon::OperatingSystem) -> Self { + Self::new(target) + .unwrap_or_else(|| unreachable!("unsupported operating system {:?}", target)) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct TargetInfo { + pub architecture: Architecture, + pub operating_system: OperatingSystem, +} + +impl TargetInfo { + pub const fn ptr_width(&self) -> PtrWidth { + self.architecture.ptr_width() + } + + pub const fn ptr_size(&self) -> usize { + match self.ptr_width() { + PtrWidth::Bytes4 => 4, + PtrWidth::Bytes8 => 8, + } + } + + pub const fn max_by_value_size(&self) -> usize { + // Pass values larger than 4 machine words by reference. + // This is a reasonable default for most architectures. We want to pass large values by + // reference because it's more efficient than copying them around on the stack, and puts + // less pressure on CPU registers. + self.ptr_size() * 4 + } + + pub const fn ptr_alignment_bytes(&self) -> usize { + self.architecture.ptr_alignment_bytes() + } + + pub const fn default_aarch64() -> Self { + TargetInfo { + architecture: Architecture::Aarch64, + operating_system: OperatingSystem::Unix, + } + } + + pub const fn default_x86_64() -> Self { + TargetInfo { + architecture: Architecture::X86_64, + operating_system: OperatingSystem::Unix, + } + } + + pub const fn default_wasm32() -> Self { + TargetInfo { + architecture: Architecture::Wasm32, + operating_system: OperatingSystem::Wasi, + } + } +} + +impl From<&target_lexicon::Triple> for TargetInfo { + fn from(triple: &target_lexicon::Triple) -> Self { + let architecture = Architecture::from(triple.architecture); + let operating_system = OperatingSystem::from(triple.operating_system); + + Self { + architecture, + operating_system, + } + } +} + +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum PtrWidth { + Bytes4 = 4, + Bytes8 = 8, +} + +/// These should be sorted alphabetically! +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, EnumIter, EnumCount)] +#[repr(u8)] +pub enum Architecture { + Aarch32, + Aarch64, + Wasm32, + X86_32, + X86_64, +} + +impl Architecture { + pub const fn ptr_width(&self) -> PtrWidth { + use Architecture::*; + + match self { + X86_64 | Aarch64 => PtrWidth::Bytes8, + X86_32 | Aarch32 | Wasm32 => PtrWidth::Bytes4, + } + } + + pub const fn ptr_alignment_bytes(&self) -> usize { + self.ptr_width() as usize + } +} + +impl From for Architecture { + fn from(target: target_lexicon::Architecture) -> Self { + match target { + target_lexicon::Architecture::X86_64 => Architecture::X86_64, + target_lexicon::Architecture::X86_32(_) => Architecture::X86_32, + target_lexicon::Architecture::Aarch64(_) => Architecture::Aarch64, + target_lexicon::Architecture::Arm(_) => Architecture::Aarch32, + target_lexicon::Architecture::Wasm32 => Architecture::Wasm32, + _ => unreachable!("unsupported architecture"), + } + } +} + +#[derive(Debug, Copy, Clone, EnumIter, EnumString, IntoStaticStr, PartialEq, Eq, Default)] +pub enum Target { + #[strum(serialize = "system")] + #[default] + System, + #[strum(serialize = "linux-x32")] + LinuxX32, + #[strum(serialize = "linux-x64")] + LinuxX64, + #[strum(serialize = "linux-arm64")] + LinuxArm64, + #[strum(serialize = "macos-x64")] + MacX64, + #[strum(serialize = "macos-arm64")] + MacArm64, + #[strum(serialize = "windows-x32")] + WinX32, + #[strum(serialize = "windows-x64")] + WinX64, + #[strum(serialize = "windows-arm64")] + WinArm64, + #[strum(serialize = "wasm32")] + Wasm32, +} + +const MACOS: target_lexicon::OperatingSystem = target_lexicon::OperatingSystem::MacOSX { + major: 12, + minor: 0, + patch: 0, +}; + +impl Target { + pub fn to_triple(self) -> Triple { + use target_lexicon::*; + + match self { + Target::System => Triple::host(), + Target::LinuxX32 => Triple { + architecture: Architecture::X86_32(X86_32Architecture::I386), + vendor: Vendor::Unknown, + operating_system: OperatingSystem::Linux, + environment: Environment::Unknown, + binary_format: BinaryFormat::Elf, + }, + Target::LinuxX64 => Triple { + architecture: Architecture::X86_64, + vendor: Vendor::Unknown, + operating_system: OperatingSystem::Linux, + environment: Environment::Unknown, + binary_format: BinaryFormat::Elf, + }, + Target::LinuxArm64 => Triple { + architecture: Architecture::Aarch64(Aarch64Architecture::Aarch64), + vendor: Vendor::Unknown, + operating_system: OperatingSystem::Linux, + environment: Environment::Unknown, + binary_format: BinaryFormat::Elf, + }, + Target::WinX32 => Triple { + architecture: Architecture::X86_32(X86_32Architecture::I386), + vendor: Vendor::Unknown, + operating_system: OperatingSystem::Windows, + environment: Environment::Gnu, + binary_format: BinaryFormat::Coff, + }, + Target::WinX64 => Triple { + architecture: Architecture::X86_64, + vendor: Vendor::Unknown, + operating_system: OperatingSystem::Windows, + environment: Environment::Gnu, + binary_format: BinaryFormat::Coff, + }, + Target::WinArm64 => Triple { + architecture: Architecture::Aarch64(Aarch64Architecture::Aarch64), + vendor: Vendor::Unknown, + operating_system: OperatingSystem::Windows, + environment: Environment::Gnu, + binary_format: BinaryFormat::Coff, + }, + Target::MacX64 => Triple { + architecture: Architecture::X86_64, + vendor: Vendor::Apple, + operating_system: MACOS, + environment: Environment::Unknown, + binary_format: BinaryFormat::Macho, + }, + Target::MacArm64 => Triple { + architecture: Architecture::Aarch64(Aarch64Architecture::Aarch64), + vendor: Vendor::Apple, + operating_system: MACOS, + environment: Environment::Unknown, + binary_format: BinaryFormat::Macho, + }, + Target::Wasm32 => Triple { + architecture: Architecture::Wasm32, + vendor: Vendor::Unknown, + operating_system: OperatingSystem::Wasi, + environment: Environment::Unknown, + binary_format: BinaryFormat::Wasm, + }, + } + } +} + +impl From<&Target> for Triple { + fn from(target: &Target) -> Self { + target.to_triple() + } +} + +impl std::fmt::Display for Target { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "{}", Into::<&'static str>::into(self)) + } +} + +pub fn get_target_triple_str(target: &target_lexicon::Triple) -> Option<&'static str> { + match target { + target_lexicon::Triple { + architecture: target_lexicon::Architecture::Wasm32, + .. + } => Some(Target::Wasm32.into()), + target_lexicon::Triple { + operating_system: target_lexicon::OperatingSystem::Linux, + architecture: target_lexicon::Architecture::X86_64, + .. + } => Some(Target::LinuxX64.into()), + target_lexicon::Triple { + operating_system: target_lexicon::OperatingSystem::Linux, + architecture: target_lexicon::Architecture::Aarch64(_), + .. + } => Some(Target::LinuxArm64.into()), + target_lexicon::Triple { + operating_system: target_lexicon::OperatingSystem::Darwin, + architecture: target_lexicon::Architecture::Aarch64(_), + .. + } => Some(Target::MacArm64.into()), + target_lexicon::Triple { + operating_system: target_lexicon::OperatingSystem::Darwin, + architecture: target_lexicon::Architecture::X86_64, + .. + } => Some(Target::MacX64.into()), + target_lexicon::Triple { + operating_system: target_lexicon::OperatingSystem::Windows, + architecture: target_lexicon::Architecture::X86_64, + .. + } => Some(Target::WinX64.into()), + target_lexicon::Triple { + operating_system: target_lexicon::OperatingSystem::Windows, + architecture: target_lexicon::Architecture::X86_32(_), + .. + } => Some(Target::WinX32.into()), + target_lexicon::Triple { + operating_system: target_lexicon::OperatingSystem::Windows, + architecture: target_lexicon::Architecture::Aarch64(_), + .. + } => Some(Target::WinArm64.into()), + _ => None, + } +} diff --git a/crates/compiler/serialize/Cargo.toml b/crates/compiler/serialize/Cargo.toml new file mode 100644 index 0000000000..9431277b53 --- /dev/null +++ b/crates/compiler/serialize/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "roc_serialize" +description = "Provides helpers for serializing and deserializing to/from bytes." + +authors.workspace = true +edition.workspace = true +license.workspace = true +version.workspace = true + +[dependencies] +roc_collections = { path = "../collections" } diff --git a/crates/compiler/serialize/src/bytes.rs b/crates/compiler/serialize/src/bytes.rs new file mode 100644 index 0000000000..3f9ae6f489 --- /dev/null +++ b/crates/compiler/serialize/src/bytes.rs @@ -0,0 +1,385 @@ +use std::{ + borrow::Borrow, + io::{self, Write}, +}; + +use roc_collections::{MutMap, VecMap}; + +pub fn serialize_slice( + slice: &[T], + writer: &mut impl Write, + written: usize, +) -> io::Result { + let alignment = std::mem::align_of::(); + let padding_bytes = next_multiple_of(written, alignment) - written; + + for _ in 0..padding_bytes { + writer.write_all(&[0])?; + } + + let bytes_slice = unsafe { slice_as_bytes(slice) }; + writer.write_all(bytes_slice)?; + + Ok(written + padding_bytes + bytes_slice.len()) +} + +pub fn deserialize_slice(bytes: &[u8], length: usize, mut offset: usize) -> (&[T], usize) { + let alignment = std::mem::align_of::(); + let size = std::mem::size_of::(); + + offset = next_multiple_of(offset, alignment); + + let byte_length = length * size; + let byte_slice = &bytes[offset..][..byte_length]; + + let slice = unsafe { std::slice::from_raw_parts(byte_slice.as_ptr() as *const T, length) }; + + (slice, offset + byte_length) +} + +pub fn deserialize_vec( + bytes: &[u8], + length: usize, + offset: usize, +) -> (Vec, usize) { + let (slice, offset) = deserialize_slice(bytes, length, offset); + (slice.to_vec(), offset) +} + +#[derive(Copy, Clone)] +struct VecSlice { + pub start: u32, + pub length: u16, + _marker: std::marker::PhantomData, +} + +impl VecSlice { + const fn len(&self) -> usize { + self.length as usize + } + + fn indices(&self) -> std::ops::Range { + self.start as usize..(self.start as usize + self.length as usize) + } + + fn extend_new(vec: &mut Vec, it: impl IntoIterator) -> Self { + let start = vec.len(); + + vec.extend(it); + + let end = vec.len(); + + Self { + start: start as u32, + length: (end - start) as u16, + _marker: Default::default(), + } + } +} + +pub fn serialize_slice_of_slices<'a, T, U>( + slice_of_slices: &[U], + writer: &mut impl Write, + written: usize, +) -> io::Result +where + T: 'a + Copy, + U: 'a + Borrow<[T]> + Sized, +{ + let mut item_buf: Vec = Vec::new(); + let mut serialized_slices: Vec> = Vec::new(); + + for slice in slice_of_slices { + let slice = VecSlice::extend_new(&mut item_buf, slice.borrow().iter().copied()); + serialized_slices.push(slice); + } + + let written = serialize_slice(&serialized_slices, writer, written)?; + serialize_slice(&item_buf, writer, written) +} + +pub fn deserialize_slice_of_slices( + bytes: &[u8], + length: usize, + offset: usize, +) -> (Vec, usize) +where + T: Copy, + Container: From>, +{ + let (serialized_slices, offset) = deserialize_slice::>(bytes, length, offset); + + let (vars_slice, offset) = { + let total_items = serialized_slices.iter().map(|s| s.len()).sum(); + deserialize_slice::(bytes, total_items, offset) + }; + + let mut slice_of_slices = Vec::with_capacity(length); + for slice in serialized_slices { + let deserialized_slice = &vars_slice[slice.indices()]; + slice_of_slices.push(deserialized_slice.to_vec().into()) + } + + (slice_of_slices, offset) +} + +pub fn serialize_map( + map: &MutMap, + ser_keys: fn(&[K], &mut W, usize) -> io::Result, + ser_values: fn(&[V], &mut W, usize) -> io::Result, + writer: &mut W, + written: usize, +) -> io::Result { + let keys = map.keys().cloned().collect::>(); + let values = map.values().cloned().collect::>(); + + let written = ser_keys(keys.as_slice(), writer, written)?; + let written = ser_values(values.as_slice(), writer, written)?; + + Ok(written) +} + +#[allow(clippy::type_complexity)] +pub fn deserialize_map( + bytes: &[u8], + deser_keys: fn(&[u8], usize, usize) -> (Vec, usize), + deser_values: fn(&[u8], usize, usize) -> (Vec, usize), + length: usize, + offset: usize, +) -> (MutMap, usize) +where + K: Clone + std::hash::Hash + Eq, + V: Clone, +{ + let (keys, offset) = deser_keys(bytes, length, offset); + let (values, offset) = deser_values(bytes, length, offset); + + ( + MutMap::from_iter((keys.iter().cloned()).zip(values.iter().cloned())), + offset, + ) +} + +pub fn serialize_vec_map( + map: &VecMap, + ser_keys: fn(&[K], &mut W, usize) -> io::Result, + ser_values: fn(&[V], &mut W, usize) -> io::Result, + writer: &mut W, + written: usize, +) -> io::Result { + let (keys, values) = map.unzip_slices(); + + let written = ser_keys(keys, writer, written)?; + let written = ser_values(values, writer, written)?; + + Ok(written) +} + +#[allow(clippy::type_complexity)] +pub fn deserialize_vec_map( + bytes: &[u8], + deser_keys: fn(&[u8], usize, usize) -> (Vec, usize), + deser_values: fn(&[u8], usize, usize) -> (Vec, usize), + length: usize, + offset: usize, +) -> (VecMap, usize) +where + K: PartialEq, +{ + let (keys, offset) = deser_keys(bytes, length, offset); + let (values, offset) = deser_values(bytes, length, offset); + + (unsafe { VecMap::zip(keys, values) }, offset) +} + +unsafe fn slice_as_bytes(slice: &[T]) -> &[u8] { + let ptr = slice.as_ptr(); + let byte_length = std::mem::size_of_val(slice); + + std::slice::from_raw_parts(ptr as *const u8, byte_length) +} + +// TODO check on https://github.com/rust-lang/rust/issues/88581 some time in the future +pub const fn next_multiple_of(lhs: usize, rhs: usize) -> usize { + match lhs % rhs { + 0 => lhs, + r => lhs + (rhs - r), + } +} + +#[cfg(test)] +mod test { + use roc_collections::{MutMap, VecMap, VecSet}; + + use super::{ + deserialize_map, deserialize_slice, deserialize_slice_of_slices, deserialize_vec, + deserialize_vec_map, serialize_map, serialize_slice, serialize_slice_of_slices, + serialize_vec_map, + }; + + #[test] + fn serde_empty_slice() { + let mut buf = vec![]; + serialize_slice(&[] as &[u8], &mut buf, 0).unwrap(); + assert!(buf.is_empty()); + + let (out, size) = deserialize_slice::(&buf, 0, 0); + assert!(out.is_empty()); + assert_eq!(size, 0); + } + + #[test] + fn serde_slice() { + let input: &[u64] = &[15u64, 23, 37, 89]; + + let mut buf = vec![]; + serialize_slice(input, &mut buf, 0).unwrap(); + assert!(!buf.is_empty()); + + let (out, size) = deserialize_slice::(&buf, 4, 0); + assert_eq!(out, input); + assert_eq!(size, 4 * 8); + } + + #[test] + fn serde_vec() { + let input: &[u64] = &[15u64, 23, 37, 89]; + + let mut buf = vec![]; + serialize_slice(input, &mut buf, 0).unwrap(); + assert!(!buf.is_empty()); + + let (out, size) = deserialize_vec::(&buf, 4, 0); + assert_eq!(out, input); + assert_eq!(size, buf.len()); + } + + #[test] + fn serde_empty_slice_of_slices() { + let input: &[&[u64]] = &[]; + + let mut buf = vec![]; + serialize_slice_of_slices(input, &mut buf, 0).unwrap(); + assert!(buf.is_empty()); + + let (out, size) = deserialize_slice_of_slices::>(&buf, 0, 0); + assert!(out.is_empty()); + assert_eq!(size, 0); + } + + #[test] + fn serde_slice_of_slices() { + let input: &[&[u64]] = &[&[15, 23, 47], &[61, 72], &[85, 91]]; + + let mut buf = vec![]; + serialize_slice_of_slices(input, &mut buf, 0).unwrap(); + assert!(!buf.is_empty()); + + let (out, size) = deserialize_slice_of_slices::>(&buf, 3, 0); + assert_eq!(out, input); + assert_eq!(size, buf.len()); + } + + #[test] + fn serde_slice_of_slices_into_vec_set() { + let input: &[&[u64]] = &[&[15, 23, 47], &[61, 72], &[85, 91]]; + + let mut buf = vec![]; + serialize_slice_of_slices(input, &mut buf, 0).unwrap(); + assert!(!buf.is_empty()); + + let (out, size) = deserialize_slice_of_slices::>(&buf, 3, 0); + assert_eq!(size, buf.len()); + + let mut out = out.into_iter(); + assert_eq!(out.next().unwrap().into_vec(), &[15, 23, 47]); + assert_eq!(out.next().unwrap().into_vec(), &[61, 72]); + assert_eq!(out.next().unwrap().into_vec(), &[85, 91]); + assert!(out.next().is_none()); + } + + #[test] + fn serde_empty_map() { + let input: MutMap = Default::default(); + + let mut buf = vec![]; + serialize_map(&input, serialize_slice, serialize_slice, &mut buf, 0).unwrap(); + assert!(buf.is_empty()); + + let (out, size) = deserialize_map::(&buf, deserialize_vec, deserialize_vec, 0, 0); + assert!(out.is_empty()); + assert_eq!(size, 0); + } + + #[test] + fn serde_map() { + let mut input: MutMap> = Default::default(); + input.insert(51, vec![15, 23, 37]); + input.insert(39, vec![17, 91, 43]); + input.insert(82, vec![90, 35, 76]); + + let mut buf = vec![]; + serialize_map( + &input, + serialize_slice, + serialize_slice_of_slices, + &mut buf, + 0, + ) + .unwrap(); + assert!(!buf.is_empty()); + + let (out, size) = deserialize_map::>( + &buf, + deserialize_vec, + deserialize_slice_of_slices, + 3, + 0, + ); + assert_eq!(out, input); + assert_eq!(size, buf.len()); + } + + #[test] + fn serde_empty_vec_map() { + let input: VecMap = Default::default(); + + let mut buf = vec![]; + serialize_vec_map(&input, serialize_slice, serialize_slice, &mut buf, 0).unwrap(); + assert!(buf.is_empty()); + + let (out, size) = + deserialize_vec_map::(&buf, deserialize_vec, deserialize_vec, 0, 0); + assert!(out.is_empty()); + assert_eq!(size, 0); + } + + #[test] + fn serde_vec_map() { + let mut input: VecMap> = Default::default(); + input.insert(51, vec![15, 23, 37]); + input.insert(39, vec![17, 91, 43]); + input.insert(82, vec![90, 35, 76]); + + let mut buf = vec![]; + serialize_vec_map( + &input, + serialize_slice, + serialize_slice_of_slices, + &mut buf, + 0, + ) + .unwrap(); + assert!(!buf.is_empty()); + + let (out, size) = deserialize_vec_map::>( + &buf, + deserialize_vec, + deserialize_slice_of_slices, + 3, + 0, + ); + assert_eq!(out.unzip_slices(), input.unzip_slices()); + assert_eq!(size, buf.len()); + } +} diff --git a/crates/compiler/serialize/src/lib.rs b/crates/compiler/serialize/src/lib.rs new file mode 100644 index 0000000000..bfbf5c6bf3 --- /dev/null +++ b/crates/compiler/serialize/src/lib.rs @@ -0,0 +1,2 @@ +//! Provides helpers for serializing and deserializing to/from bytes. +pub mod bytes; diff --git a/crates/compiler/solve/Cargo.toml b/crates/compiler/solve/Cargo.toml new file mode 100644 index 0000000000..3549682c54 --- /dev/null +++ b/crates/compiler/solve/Cargo.toml @@ -0,0 +1,48 @@ +[package] +name = "roc_solve" +description = "The entry point of Roc's type inference system. Implements type inference and specialization of abilities." + +authors.workspace = true +edition.workspace = true +license.workspace = true +version.workspace = true + +[dependencies] +roc_can = { path = "../can" } +roc_checkmate = { path = "../checkmate" } +roc_collections = { path = "../collections" } +roc_debug_flags = { path = "../debug_flags" } +roc_derive = { path = "../derive" } +roc_derive_key = { path = "../derive_key" } +roc_error_macros = { path = "../../error_macros" } +roc_exhaustive = { path = "../exhaustive" } +roc_module = { path = "../module" } +roc_packaging = { path = "../../packaging" } +roc_problem = { path = "../problem" } +roc_region = { path = "../region" } +roc_solve_problem = { path = "../solve_problem" } +roc_solve_schema = { path = "../solve_schema" } +roc_types = { path = "../types" } +roc_unify = { path = "../unify" } + +arrayvec.workspace = true +bumpalo.workspace = true + +[dev-dependencies] +roc_builtins = { path = "../builtins" } +roc_derive = { path = "../derive", features = ["debug-derived-symbols"] } +roc_load = { path = "../load" } +roc_parse = { path = "../parse" } +roc_problem = { path = "../problem" } +roc_reporting = { path = "../../reporting" } +roc_target = { path = "../roc_target" } +test_solve_helpers = { path = "../test_solve_helpers" } + +bumpalo.workspace = true +indoc.workspace = true +insta.workspace = true +lazy_static.workspace = true +pretty_assertions.workspace = true +regex.workspace = true +tempfile.workspace = true +libtest-mimic.workspace = true diff --git a/crates/compiler/solve/docs/ambient_lambda_set_specialization.md b/crates/compiler/solve/docs/ambient_lambda_set_specialization.md new file mode 100644 index 0000000000..dde36b63cc --- /dev/null +++ b/crates/compiler/solve/docs/ambient_lambda_set_specialization.md @@ -0,0 +1,699 @@ +# Ambient Lambda Set Specialization + +Ayaz Hafiz + +## Summary + +This document describes how polymorphic lambda sets are specialized and resolved +in the compiler's type solver. It's derived from the original document at . + +TL;DR: lambda sets are resolved by unifying their ambient arrow types in a “bottom-up” fashion. + +## Background + +In this section I’ll explain how lambda sets and specialization lambda sets work today, mostly from the ground-up. I’ll gloss over a few details and assume an understanding of type unification. The background will leave us with a direct presentation of the current specialization lambda set unification algorithm, and its limitation. + +Lambda sets are a technique Roc uses for static dispatch of closures. For example, + +```jsx +id1 = \x -> x +id2 = \x -> x +f = if True then id1 else id2 +``` + +has the elaboration (solved-type annotations) + +```jsx +id1 = \x -> x +^^^ id1 : a -[[id1]] -> a +id2 = \x -> x +^^^ id2 : a -[[id2]] -> a +f = if True then id1 else id2 +^ f : a -[[id1, id2]] -> a +``` + +The syntax `-[[id1]]->` can be read as “a function that dispatches to `id1`". Then the arrow `-[[id1, id2]]->` then is “a function that dispatches to `id1`, or `id2`". The tag union `[id1, id2]` can contain payloads to represent the captures of `id1` and `id2`; however, the implications of that are out of scope for this discussion, see [Folkert’s great explanation](https://github.com/roc-lang/roc/pull/2307#discussion_r777042512) for more information. During compile-time, Roc would attach a run-time examinable tag to the value in each branch of the `f` expression body, representing whether to dispatch to `id1` or `id2`. Whenever `f` is dispatched, that tag is examined to determine exactly which function should be dispatched to. This is “**defunctionalization**”. + +In the presence of [abilities](https://docs.google.com/document/d/1kUh53p1Du3fWP_jZp-sdqwb5C9DuS43YJwXHg1NzETY/edit), lambda sets get more complicated. Now, I can write something like + +```jsx +Hash has hash : a -> U64 | a has Hash + +zeroHash = \_ -> 0 + +f = if True then hash else zeroHash +``` + +The elaboration of `f` as `f : a -[[hash, zeroHash]]-> U64` is incorrect, in the sense that it is incomplete - `hash` is not an actual definition, and we don’t know exactly what specialization of `hash` to dispatch to until the type variable `a` is resolved. This elaboration does not communicate to the code generator that the value of `hash` is actually polymorphic over `a`. + +To support polymorphic values in lambda sets, we use something we call “**specialization lambda sets**”. In this technique, the lambda under the only arrow in `hash` is parameterized on (1) the type variable the `hash` specialization depends on, and (2) the “region” in the type signature of the specialization that the actual type should be recovered from. + +That was a lot of words, so let me give you an example. To better illustrate how the mechanism works, let’s suppose `Hash` is actually defined as `Hash has hashThunk : a -> ({} -> U64) | a has Hash`. Now let’s consider the following program elaboration: + +```jsx +Hash has + hashThunk : a -> ({} -> U64) | a has Hash +# ^^^^^^^^^ a -[[] + a:hashThunk:1]-> ({} -[[] + a:hashThunk:2]-> U64) + +zeroHash = \_ -> \{} -> 0 +#^^^^^^^ a -[[zeroHash]]-> \{} -[[lam1]]-> U64 + +f = if True then hash else zeroHash +#^ a -[[zeroHash] + a:hashThunk:1]-> ({} -[[lam1] + a:hashThunk:2]-> U64) +``` + +The grammar of a lambda set is now + +```jsx +lambda_set: [[(concrete_lambda)*] (+ specialization_lambda)*] + +concrete_lambda: lambda_name ( capture_type)* +specialization_lambda: :ability_member_name:region +region: +``` + +Since `hashThunk` is a specification for an ability member and not a concrete implementation, it contains only specialization lambdas, in this case parameterized over `a`, which is the type parameter that implementors of `Hash` must specialize. Since `hashThunk` has two function types, we need to distinguish how they should be resolved. For this reason we record a “region” noting where the specialization lambda occurs in an ability member signature. When `a` is resolved to a concrete type `C`, we would resolve `C:hashThunk:2` by looking up the lambda set of `C`'s specialization of `hashThunk`, at region 2. + +`zeroHash` is a concrete implementation, and uses only concrete lambdas, so its two lambda sets are fully resolved with concrete types. I’ve named the anonymous lambda `\{} -> 0` `lam1` for readability. + +At `f`, we unify the function types in both branches. Unification of lambda sets is basically a union of both sides’ concrete lambdas and specialization lambdas (this is not quite true, but that doesn’t matter here). This way, we preserve the fact that how `f` should be dispatched is parameterized over `a`. + +Now, let’s say we apply `f` to a concrete type, like + +```jsx +Foo := {} +hashThunk = \@Foo {} -> \{} -> 1 +#^^^^^^^^ Foo -[[Foo#hashThunk]]-> \{} -[[lam2]]-> U64 + +f (@Foo {}) +``` + +The unification trace for the call `f (@Foo {})` proceeds as follows. I use `'tN`, where `N` is a number, to represent fresh unbound type variables. Since `f` is a generalized type, `a'` is the fresh type “based on `a`" created for a particular usage of `f`. + +```text + typeof f +~ Foo -'t1-> 't2 +=> + a' -[[zeroHash] + a':hashThunk:1]-> ({} -[[lam1] + a':hashThunk:2]-> U64) +~ Foo -'t1-> 't2 +=> + Foo -[[zeroHash] + Foo:hashThunk:1]-> ({} -[[lam1] + Foo:hashThunk:2]-> U64) +``` + +Now that the specialization lambdas’ type variables point to concrete types, we can resolve the concrete lambdas of `Foo:hashThunk:1` and `Foo:hashThunk:2`. Cool! Let’s do that. We know that + +```text +hashThunk = \@Foo {} -> \{} -> 1 +#^^^^^^^^ Foo -[[Foo#hashThunk]]-> \{} -[[lam2]]-> U64 +``` + +So `Foo:hashThunk:1` is `[[Foo#hashThunk]]` and `Foo:hashThunk:2` is `[[lam2]]`. Applying that to the type of `f` we get the trace + +```text + Foo -[[zeroHash] + Foo:hashThunk:1]-> ({} -[[lam1] + Foo:hashThunk:2]-> U64) + + Foo:hashThunk:1 -> [[Foo#hashThunk]] + Foo:hashThunk:2 -> [[lam2]] +=> + Foo -[[zeroHash, Foo#hashThunk]]-> ({} -[[lam1, lam2]] -> U64) +``` + +Great, so now we know our options to dispatch `f` in the call `f (@Foo {})`, and the code-generator will insert tags appropriately for the specialization definition of `f` where `a = Foo` knowing the concrete lambda symbols. + +## The Problem + +This technique for lambda set resolution is all well and good when the specialized lambda sets are monomorphic, that is, they contain only concrete lambdas. So far in our development of the end-to-end compilation model that’s been the case, and when it wasn’t, there’s been enough ambient information to coerce the specializations to be monomorphic. + +Unfortunately we cannot assume that the specializations will be monomorphic in general, and we must now think about how to deal with that. I didn’t think there was any good, efficient solution, but now we have no option other than to come up with something, so this document is a description of my attempt. But before we get there, let’s whet our appetite for what the problem even is. I’ve been waving my hands too long. + +Let’s consider the following program: + +```python +F has f : a -> (b -> {}) | a has F, b has G +# ^ a -[[] + a:f:1]-> (b -[[] + a:f:2]-> {}) | a has F, b has G + +G has g : b -> {} | b has G +# ^ b -[[] + b:g:1]-> {} + +Fo := {} +f = \@Fo {} -> g +#^ Fo -[[Fo#f]]-> (b -[[] + b:g:1]-> {}) | b has G +# instantiation with a=Fo of +# a -[[] + a:f:1]-> (b -[[] + a:f:2]-> {}) | a has F, b has G + +Go := {} +g = \@Go {} -> {} +#^ Go -[[Go#g]]-> {} +# instantiation with b=Go of +# b -[[] + b:g:1]-> {} +``` + +Apologies for the complicated types, I know this can be a bit confusing. It helps to look at the specialized types of `f` and `g` relative to the ability member signatures. + +The key thing to notice here is that `Fo#f` must continue to vary over `b | b has G`, since it can only specialize the type parameter `a` (in this case, it specialized it to `Fo`). Its return value is the unspecialized ability member `g`, which has type `b -> {}`, as we wanted. But its lambda set **also** varies over `b`, being `b -[[] + b:g:1]-> {}`. + +Suppose we have the call + +```python +(f (@Fo {})) (@Go {}) +``` + +With the present specialization technique, unification proceeds as follows: + +```text +== solve (f (@Fo {})) == + typeof f +~ Fo -'t1-> 't2 + + a' -[[] + a':f:1]-> (b' -[[] + a':f:2]-> {}) +~ Fo -'t1-> 't2 +=> Fo -[[] + Fo:f:1]-> (b' -[[] + Fo:f:2]-> {}) + + Fo:f:1 -> [[Fo#f]] + Fo:f:2 -> [[] + b'':g:1] | This is key bit 1! +=> Fo -[[Fo#f]]-> (b' -[[] + b'':g:1] -> {}) + +== solve (f (@Fo {})) (@Go {}) == + return_typeof f +~ Go -'t3-> 't4 + - + b' -[[] + b'':g:1] -> {} | This is key bit 2! +~ Go -'t3-> 't4 | +=> Go -[[] + b'':g:1] -> {} | + - + +== final type of f == +f : Fo -[[Fo#f]]-> (Go -[[] + b'':g:1]-> {}) +``` + +Let's go over what happened. The important pieces are the unification traces I’ve annotated as “key bits”. + +In resolving `Fo:f:2`, we pulled down the let-generalized lambda set `[[] + b:g:2]` at that region in `Fo`, which means we have to generate a fresh type variable for `b` for that particular instantiation of the lambda set. That makes sense, that’s how let-generalization works. So, we get the lambda set `[[] + b'':g:1]` for our particular instance. + +But in key bit 2, we see that we know what we want `b''` to be! We want it to be this `b'`, which gets instantiated to `Go`. But `b'` and `b''` are independent type variables, and so unifying `b' ~ Go` doesn’t solve `b'' = Go`. Instead, `b''` is now totally unbound, and in the end, we get a type for `f` that has an unspecialized lambda set, even though you or I, staring at this program, know exactly what `[[] + b'':g:1]` should really be - `[[Go#g]]`. + +So where did we go wrong? Well, our problem is that we never saw that `b'` and `b''` should really be the same type variable. If only we knew that in this specialization `b'` and `b''` are the same instantiation, we’d be all good. + +## A Solution + +I’ll now explain the best way I’ve thought of for us to solve this problem. If you see a better way, please let me know! I’m not sure I love this solution, but I do like it a lot more than some other naive approaches. + +Okay, so first we’ll enumerate some terminology, and the exact algorithm. Then we’ll illustrate the algorithm with some examples; my hope is this will help explain why it must proceed in the way it does. We’ll see that the algorithm depends on a few key invariants; I’ll discuss them and their consequences along the way. Finally, we’ll discuss a couple details regarding the algorithm not directly related to its operation, but important to recognize. I hope then, you will tell me where I have gone wrong, or where you see a better opportunity to do things. + +### The algorithm + +#### Some definitions + +- **The region invariant.** Previously we discussed the “region” of a lambda set in a specialization function definition. The way regions are assigned in the compiler follows a very specific ordering and holds a invariant we’ll call the “region invariant”. First, let’s define a procedure for creating function types and assigning regions: + + ```text + Type = \region -> + (Type_atom, region) + | Type_function region + + Type_function = \region -> + let left_type, new_region = Type (region + 1) + let right_type, new_region = Type (new_region) + let func_type = left_type -[Lambda region]-> right_type + (func_type, new_region) + ``` + + This procedure would create functions that look like the trees(abbreviating `L=Lambda`, `a=atom` below) + + ```text + -[L 1]-> + a a + + === + + -[L 1]-> + -[L 2]-> -[L 3]-> + a a a a + + === + -[L 1]-> + -[L 2]-> -[L 5]-> + -[L 3]-> -[L 4]-> -[L 6]-> -[L 7]-> + a a a a a a a a + ``` + + The invariant is this: for a region `r`, the only functions enclosing `r` have a region number that is less than `r`. Moreover, every region `r' < r`, either the function at `r'` encloses `r`, or is disjoint from `r`. + +- **Ambient functions.** For a given lambda set at region `r`, any function that encloses `r` is called an **ambient function** of `r`. The function directly at region `r` is called the **directly ambient function**. + + For example, the functions identified by `L 4`, `L 2`, and `L 1` in the last example tree above are all ambient functions of the function identified by `L 4`. + + The region invariant means that the only functions that are ambient of a region `r` are those identified by regions `< r`. + +- `uls_of_var`. A look aside table of the unspecialized lambda sets (uls) depending on a variable. For example, in `a -[[] + a:f:1]-> (b -[[] + a:f:2]-> {})`, there would be a mapping of `a => { [[] + a:f:1]; [[] + a:f:2] }`. When `a` gets instantiated with a concrete type, we know that these lambda sets are ready to be resolved. + +#### Explicit Description + +The algorithm concerns what happens during the lambda-set-specialization-time. You may want to read it now, but it’s also helpful to first look at the intuition below, then the examples, then revisit the explicit algorithm description. + +Suppose a type variable `a` with `uls_of_var` mapping `uls_a = {l1, ... ln}` has been instantiated to a concrete type `C`. Then, + +1. Let each `l` in `uls_a` be of form `[concrete_lambdas + ... + C:f:r + ...]`. It has to be in this form because of how `uls_of_var` is constructed. + 1. Note that there may be multiple unspecialized lambdas of form `C:f:r, C:f1:r1, ..., C:fn:rn` in `l`. In this case, let `t1, ... tm` be the other unspecialized lambdas not of form `C:_:_`, that is, none of which are now specialized to the type `C`. Then, deconstruct `l` such that `l' = [concrete_lambdas + t1 + ... + tm + C:f:r` and `l1 = [[] + C:f1:r1], ..., ln = [[] + C:fn:rn]`. Replace `l` with `l', l1, ..., ln` in `uls_a`, flattened. +2. Now, each `l` in `uls_a` has a unique unspecialized lambda of form `C:f:r`. Sort `uls_a` primarily by `f` (arbitrary order), and secondarily by `r` in descending order. This sorted list is called `uls_a'`. + 1. That is, we are sorting `uls_a` so that it is partitioned by ability member name of the unspecialized lambda sets, and each partition is in descending order of region. + 2. An example of the sort would be `[[] + C:foo:2], [[] + C:bar:3], [[] + C:bar:1]`. +3. For each `l` in `uls_a'` with unique unspecialized lambda `C:f:r`: + 1. Let `t_f1` be the directly ambient function of the lambda set containing `C:f:r`. Remove `C:f:r` from `t_f1`'s lambda set. + 1. For example, `(b' -[[] + Fo:f:2]-> {})` if `C:f:r=Fo:f:2`. Removing `Fo:f:2`, we get `(b' -[[]]-> {})`. + 2. Let `t_f2` be the directly ambient function of the specialization lambda set resolved by `C:f:r`. + 1. For example, `(b -[[] + b:g:1]-> {})` if `C:f:r=Fo:f:2`, running on example from above. + 3. Unify `t_f1 ~ t_f2`. + +#### Intuition + +The intuition is that we walk up the function type being specialized, starting from the leaves. Along the way we pick up bound type variables from both the function type being specialized, and the specialization type. The region invariant makes sure we thread bound variables through an increasingly larger scope. + +### Some Examples + +#### The motivating example + +Recall the program from our problem statement + +```python +F has f : a -> (b -> {}) | a has F, b has G +# ^ a -[[] + a:f:1]-> (b -[[] + a:f:2]-> {}) | a has F, b has G + +G has g : b -> {} | b has G +# ^ b -[[] + b:g:1]-> {} + +Fo := {} +f = \@Fo {} -> g +#^ Fo -[[Fo#f]]-> (b -[[] + b:g:1]-> {}) | b has G +# instantiation with a=Fo of +# a -[[] + a:f:1]-> (b -[[] + a:f:2]-> {}) | a has F, b has G + +Go := {} +g = \@Go {} -> {} +#^ Go -[[Go#g]]-> {} +# instantiation with b=Go of +# b -[[] + b:g:1]-> {} +``` + +With our algorithm, the call + +```python +(f (@Fo {})) (@Go {}) +``` + +has unification proceed as follows: + +```text +== solve (f (@Fo {})) == + typeof f +~ Fo -'t1-> 't2 + + a' -[[] + a':f:1]-> (b' -[[] + a':f:2]-> {}) +~ Fo -'t1-> 't2 +=> Fo -[[] + Fo:f:1]-> (b' -[[] + Fo:f:2]-> {}) + + step 1: + uls_Fo = { [[] + Fo:f:1], [[] + Fo:f:2] } + step 2 (sort): + uls_Fo' = { [[] + Fo:f:2], [[] + Fo:f:1] } + step 3: + 1. iteration: [[] + Fo:f:2] + b' -[[]]-> {} (t_f1 after removing Fo:f:2) + ~ b'' -[[] + b'':g:1]-> {} + = b'' -[[] + b'':g:1]-> {} + => typeof f now Fo -[[] + Fo:f:1]-> (b'' -[[] + b'':g:1]-> {}) + + 2. iteration: [[] + Fo:f:1] + Fo -[[]]-> (b'' -[[] + b'':g:1]-> {}) (t_f1 after removing Fo:f:1) + ~ Fo -[[Fo#f]]-> (b''' -[[] + b''':g:1]-> {}) + = Fo -[[Fo#f]]-> (b''' -[[] + b''':g:1]-> {}) + + => typeof f = Fo -[[Fo#f]]-> (b''' -[[] + b''':g:1]-> {}) + +== solve (f (@Fo {})) (@Go {}) == + return_typeof f +~ Go -'t3-> 't4 + + b''' -[[] + b''':g:1]-> {} +~ Go -'t3-> 't4 +=> Go -[[] + Go:g:1] -> {} + + step 1: + uls_Go = { [[] + Go:g:1] } + step 2 (sort): + uls_Go' = { [[] + Go:g:1] } + step 3: + 1. iteration: [[] + Go:g:1] + Go -[[]]-> {} (t_f1 after removing Go:g:1) + ~ Go -[[Go#g]]-> {} + = Go -[[Go#g]]-> {} + + => typeof f = Fo -[[Fo#f]]-> (Go -[[Go#g]]-> {}) + +== final type of f == +f : Fo -[[Fo#f]]-> (Go -[[Go#g]]-> {}) +``` + +There we go. We’ve recovered the specialization type of the second lambda set to `Go#g`, as we wanted. + +#### The motivating example, in the presence of let-generalization + +Suppose instead we let-generalized the motivating example, so it was a program like + +```coffee +h = f (@Fo {}) +h (@Go {}) +``` + +`h` still gets resolved correctly in this case. It’s basically the same unification trace as above, except that after we find out that + +```text +typeof f = Fo -[[Fo#f]]-> (b''' -[[] + b''':g:1]-> {}) +``` + +we see that `h` has type + +```text +b''' -[[] + b''':g:1]-> {} +``` + +We generalize this to + +```text +h : c -[[] + c:g:1]-> {} +``` + +Then, the call `h (@Go {})` has the trace + +```text +=== solve h (@Go {}) === + typeof h +~ Go -'t1-> 't2 + + c' -[[] + c':g:1]-> {} +~ Go -'t1-> 't2 +=> Go -[[] + Go:g:1]-> {} + + step 1: + uls_Go = { [[] + Go:g:1] } + step 2 (sort): + uls_Go' = { [[] + Go:g:1] } + step 3: + 1. iteration: [[] + Go:g:1] + Go -[[]]-> {} (t_f1 after removing Go:g:1) + ~ Go -[[Go#g]]-> {} + = Go -[[Go#g]]-> {} + => Go -[[Go#g]]-> {} +``` + +#### Bindings on the right side of an arrow + +This continues to work if instead of a type variable being bound on the left side of an arrow, it is bound on the right side. Let’s see what that looks like. Consider + +```python +F has f : a -> ({} -> b) | a has F, b has G +G has g : {} -> b | b has G + +Fo := {} +f = \@Fo {} -> g +#^ Fo -[[Fo#f]]-> ({} -[[] + b:g:1]-> b) | b has G +# instantiation with a=Fo of +# a -[[] + a:f:1]-> ({} -[[] + a:f:2]-> b) | a has F, b has G + +Go := {} +g = \{} -> @Go {} +#^ {} -[[Go#g]]-> Go +# instantiation with b=Go of +# {} -[[] + b:g:1]-> b +``` + +This is symmetrical to the first example we ran through. I can include a trace if you all would like, though it could be helpful to go through yourself and see that it would work. + +#### Deep specializations and captures + +Alright, bear with me, this is a long and contrived one, but it demonstrates how this works in the presence of polymorphic captures (it’s “nothing special”), and more importantly, why the bottom-up unification is important. + +Here’s the source program: + +```python +F has f : a, b -> ({} -> ({} -> {})) | a has F, b has G +# ^ a, b -[[] + a:f:1]-> ({} -[[] + a:f:2]-> ({} -[[] + a:f:3]-> {})) | a has F, b has G +G has g : b -> ({} -> {}) | b has G +# ^ b -[[] + b:g:1]-> ({} -[[] + b:g:2]-> {}) | b has G + +Fo := {} +f = \@Fo {}, b -> \{} -> g b +#^ Fo, b -[[Fo#f]]-> ({} -[[lamF b]]-> ({} -[[] + b:g:2]]-> {})) | b has G +# instantiation with a=Fo of +# a, b -[[] + a:f:1]-> ({} -[[] + a:f:2]-> ({} -[[] + a:f:3]-> {})) | a has F, b has G + +Go := {} +g = \@Go {} -> \{} -> {} +#^ {} -[[Go#g]]-> ({} -[[lamG]]-> {}) +# instantiation with b=Go of +# b -[[] + b:g:1]-> ({} -[[] + b:g:2]-> {}) | b has G +``` + +Here is the call we’re going to trace: + +```python +(f (@Fo {}) (@Go {})) {} +``` + +Let’s get to it. + +```text +=== solve (f (@Fo {}) (@Go {})) === + typeof f +~ Fo, Go -'t1-> 't2 + + a, b -[[] + a:f:1]-> ({} -[[] + a:f:2]-> ({} -[[] + a:f:3]-> {})) +~ Fo, Go -'t1-> 't2 +=> Fo, Go -[[] + Fo:f:1]-> ({} -[[] + Fo:f:2]-> ({} -[[] + Fo:f:3]-> {})) + + step 1: + uls_Fo = { [[] + Fo:f:1], [[] + Fo:f:2], [[] + Fo:f:3] } + step 2: + uls_Fo = { [[] + Fo:f:3], [[] + Fo:f:2], [[] + Fo:f:1] } (sorted) + step_3: + 1. iteration: [[] + Fo:f:3] + {} -[[]]-> {} (t_f1 after removing Fo:f:3) + ~ {} -[[] + b':g:2]]-> {} + = {} -[[] + b':g:2]-> {} + => Fo, Go -[[] + Fo:f:1]-> ({} -[[] + Fo:f:2]-> ({} -[[] + b':g:2]-> {})) + + 2. iteration: [[] + Fo:f:2] + {} -[[]]-> ({} -[[] + b':g:2]-> {}) (t_f1 after removing Fo:f:2) + ~ {} -[[lamF b'']]-> ({} -[[] + b'':g:2]]-> {}) + = {} -[[lamF b'']]-> ({} -[[] + b'':g:2]]-> {}) + => Fo, Go -[[] + Fo:f:1]-> ({} -[[lamF b'']]-> ({} -[[] + b'':g:2]]-> {})) + + 3. iteration: [[] + Fo:f:1] + Fo, Go -[[]]-> ({} -[[lamF b'']]-> ({} -[[] + b'':g:2]]-> {})) (t_f1 after removing Fo:f:2) + ~ Fo, b''' -[[Fo#f]]-> ({} -[[lamF b''']]-> ({} -[[] + b''':g:2]]-> {})) + = Fo, Go -[[Fo#f]]-> ({} -[[lamF Go]]-> ({} -[[] + Go:g:2]-> {})) + + step 1: + uls_Go = { [[] + Go:g:2] } + step 2: + uls_Go = { [[] + Go:g:2] } (sorted) + step_3: + 1. iteration: [[] + Go:g:2] + {} -[[]]-> {} (t_f1 after removing Go:g:2) + ~ {} -[[lamG]]-> {} + = {} -[[lamG]]-> {} + => Fo, Go -[[Fo#f]]-> ({} -[[lamF Go]]-> ({} -[[lamG]]-> {})) + +== final type of f == +f : Fo, Go -[[Fo#f]]-> ({} -[[lamF Go]]-> ({} -[[lamG]]-> {})) +``` + +Look at that! Resolved the capture, and all the lambdas. + +Notice that in the first `` trace, had we not sorted the `Fo:f:_` specialization lambdas in descending order of region, we would have resolved `Fo:f:3` last, and not bound the specialized `[[] + b':g:2]` to any `b'` variable. Intuitively, that’s because the variable we need to bind it to occurs in the most ambient function type of all those specialization lambdas: the one at `[[] + Fo:f:1]` + +### An important requirement + +There is one invariant I have left implicit in this construction, that may not hold in general. (Maybe I left others that you noticed that don’t hold - let me know!). That invariant is that any type variable in a signature is bound in either the left or right hand side of an arrow. + +I know what you’re thinking, “of course, how else can you get a type variable?” Well, they have played us for fools. Evil lies in the midst. No sanctity passes unscathed through ad-hoc polymorphism. + +```python +Evil has + getEvil : {} -> a | a has Evil + eatEvil : a -> ({} -> {}) | a has Evil + +f = eatEvil (getEvil {}) +``` + +The type of `f` here is `{} -> [[] + a:eatEvil:2]-> {} | a has Evil`. “Blasphemy!” you cry. Well, you’re totally right, this program is total nonsense. Somehow it’s well-typed, but the code generator can’t just synthesize an `a | a has Evil` out of nowhere. + +Well, okay, the solution is actually pretty simple - make this a type error. It’s actually a more general problem with abilities, for example we can type the following program: + +```python +Evil has + getEvil : {} -> a | a has Evil + eatEvil : a -> {} | a has Evil + +f = eatEvil (getEvil {}) +``` + +Now the type variable `a | a has Evil` isn’t even visible on the surface: `f` has type `f : {}`. But it lies in the middle, snuggly between `getEvil` and `eatEvil` where it can’t be seen. + +In fact, to us, detecting these cases is straightforward - such nonsense programs are identified when they have type variables that don’t escape to either the front or the back of an exposed type. That’s the only way to do monomorphization - otherwise, we could have values that are pathologically polymorphic, which means they are either unused, or this kind of non-codegen-able case. + +How do we make this a type error? A couple options have been considered, but we haven’t settled on anything. + +1. One approach, suggested by Richard, is to sort abilities into strongly-connected components and see if there is any zig-zag chain of member signatures in a SCC where an ability-bound type variable doesn’t escape through the front or back. We can observe two things: (1) such SCCs can only exist within a single module because Roc doesn’t have (source-level) circular dependencies and (2) we only need to examine pairs of functions have at least one type variable only appearing on one side of an arrow. That means the worst case performance of this analysis is quadratic in the number of ability members in a module. The downside of this approach is that it would reject some uses of abilities that can be resolved and code-generated by the compiler. +2. Another approach is to check whether generalized variables in a let-bound definition’s body escaped out the front or back of the let-generalized definition’s type (and **not** in a lambda set, for the reasons described above). This admits some programs that would be illegal with the other analysis but can’t be performed until typechecking. As for performance, note that new unbound type variables in a body can only be introduced by using a let-generalized symbol that is polymorphic. Those variables would need to be checked, so the performance of this approach on a per-module basis is linear in the number of let-generalized symbols used in the module (assuming the number of generalized variables returned is a constant factor). + +### A Property that’s lost, and how we can hold on to it + +One question I asked myself was, does this still ensure lambda sets can vary over multiple able type parameters? At first, I believed the answer was yes — however, this may not hold and be sound. For example, consider + +```python +J has j : j -> (k -> {}) | j has J, k has K +K has k : k -> {} | k has K + +C := {} +j = \@C _ -> k + +D := {} +j = \@D _ -> k + +E := {} +k = \@E _ -> {} + +f = \flag, a, b, c -> + it = when flag is + A -> j a + B -> j b + it c +``` + +The first branch has type (`a` has generalized type `a'`) + +```text +c'' -[[] + a':j:2]-> {} +``` + +The second branch has type (`b` has generalized type `b'`) + +```text +c''' -[[] + b':j:2]-> {} +``` + +So now, how do we unify this? Well, following the construction above, we must unify `a'` and `b'` - but this demands that they are actually the same type variable. Is there another option? + +Well, one idea is that during normal type unification, we simply take the union of unspecialized lambda sets with **disjoint** variables. In the case above, we would get `c' -[[] + a':j:2 + b':j:2]` (supposing `c` has type `c'`). During lambda set compaction, when we unify ambient types, choose one non-concrete type to unify with. Since we’re maintaining the invariant that each generalized type variable appears at least once on one side of an arrow, eventually you will have picked up all type variables in unspecialized lambda sets. + +```text +=== monomorphize (f A (@C {}) (@D {}) (@E {})) === +(inside f, solving `it`:) + +it ~ E -[[] + C:j:2 + D:j:2]-> {} + + step 1: + uls_C = { [[] + C:j:2 + D:j:2] } + step 2: + uls_C = { [[] + C:j:2 + D:j:2] } (sorted) + step_3: + 1. iteration: [[] + C:j:2 + D:j:2] + E -[[] + D:j:2]-> {} (t_f1 after removing C:j:2) + ~ k' -[[] + k':k:2]-> {} + = E -[[] + E:k:2 + D:j:2]-> {} (no non-concrete type to unify with) + => E -[[] + E:k:2 + D:j:2]-> {} + + step 1: + uls_D = { [[] + E:k:2 + D:j:2] } + step 2: + uls_D = { [[] + E:k:2 + D:j:2] } (sorted) + step_3: + 1. iteration: [[] + E:k:2 + D:j:2] + E -[[] + E:k:2]-> {} (t_f1 after removing D:j:2) + ~ k'' -[[] + k'':k:2]-> {} + = E -[[] + E:k:2 + E:k:2]-> {} (no non-concrete type to unify with) + => E -[[] + E:k:2 + E:k:2]-> {} + + step 1: + uls_E = { [[] + E:k:2], [[] + E:k:2] } + step 2: + uls_E = { [[] + E:k:2], [[] + E:k:2] } (sorted) + step_3: + 1. iteration: [[] + E:k:2] + E -[[]]-> {} (t_f1 after removing E:k:2) + ~ E -[[lamE]]-> {} + = E -[[lamE]]-> {} + => E -[[lamE]]-> {} + => E -[[lamE]]-> {} + +== final type of it == +it : E -[[lamE]]-> {} +``` + +The disjointedness is important - we want to unify unspecialized lambdas whose type variables are equivalent. For example, + +```coffee +f = \flag, a, c -> + it = when flag is + A -> j a + B -> j a + it c +``` + +Should produce `it` having generalized type + +```text +c' -[[] + a':j:2]-> {} +``` + +and not + +```text +c' -[[] + a':j:2 + a':j:2]-> {} +``` + +For now, we will not try to preserve this property, and instead unify all type variables with the same member/region in a lambda set. We can improve the status of this over time. + +## Conclusion + +Will this work? I think so, but I don’t know. In the sense that, I am sure it will work for some of the problems we are dealing with today, but there may be even more interactions that aren’t clear to us until further down the road. + +Obviously, this is not a rigorous study of this problem. We are making several assumptions, and I have not proved any of the properties I claim. However, the intuition makes sense to me, predicated on the “type variables escape either the front or back of a type” invariant, and this is the only approach that really makes sense to me while only being a little bit complicated. Let me know what you think. + +## Appendix + +### Optimization: only the lowest-region ambient function type is needed + +You may have observed that step 1 and step 2 of the algorithm are somewhat overkill, really, it seems you only need the lowest-number region’s directly ambient function type to unify the specialization with. That’s because by the region invariant, the lowest-region’s ambient function would contain every other region’s ambient function. + +This optimization is correct with a change to the region numbering scheme: + +```python +Type = \region -> + (Type_atom, region) +| Type_function region + +Type_function = \region -> + let left_type = Type (region * 2) + let right_type = Type (region * 2 + 1) + let func_type = left_type -[Lambda region]-> right_type + func_type +``` + +Which produces a tree like + +```text + -[L 1]-> + -[L 2]-> -[L 3]-> + -[L 4]-> -[L 5]-> -[L 6]-> -[L 7]-> +a a a a a a a a +``` + +Now, given a set of `uls` sorted in increasing order of region, you can remove all `uls` that have region `r` such that a floored 2-divisor of `r` is another region `r'` of a unspecialized lambda in `uls`. For example, given `[a:f:2, a:f:5, a:f3, a:f:7]`, you only need to keep `[a:f:2, a:f:3]`. + +Then, when running the algorithm, you must remove unspecialized lambdas of form `C:f:_` from **all** nested lambda sets in the directly ambient function, not just in the directly ambient function. This will still be cheaper than unifying deeper lambda sets, but may be an inconvenience. + +### Testing Strategies + +- Quickcheck - the shape of functions we care about is quite clearly defined. Basically just create a bunch of let-bound functions, polymorphic over able variables, use them in an expression that evaluates monomorphically, and check that everything in the monomorphic expression is resolved. diff --git a/crates/compiler/solve/src/ability.rs b/crates/compiler/solve/src/ability.rs new file mode 100644 index 0000000000..47e33d62e3 --- /dev/null +++ b/crates/compiler/solve/src/ability.rs @@ -0,0 +1,1591 @@ +use roc_can::abilities::AbilitiesStore; +use roc_can::expr::PendingDerives; +use roc_checkmate::with_checkmate; +use roc_collections::{VecMap, VecSet}; +use roc_debug_flags::dbg_do; +#[cfg(debug_assertions)] +use roc_debug_flags::ROC_PRINT_UNDERIVABLE; +use roc_derive_key::{DeriveError, Derived}; +use roc_error_macros::internal_error; +use roc_module::symbol::{ModuleId, Symbol}; +use roc_region::all::{Loc, Region}; +use roc_solve_problem::{ + NotDerivableContext, NotDerivableDecode, NotDerivableEncode, NotDerivableEq, TypeError, + UnderivableReason, Unfulfilled, +}; +use roc_solve_schema::UnificationMode; +use roc_types::num::NumericRange; +use roc_types::subs::{ + instantiate_rigids, Content, FlatType, GetSubsSlice, Rank, RecordFields, Subs, SubsSlice, + TupleElems, Variable, +}; +use roc_types::types::{AliasKind, Category, MemberImpl, PatternCategory, Polarity, Types}; +use roc_unify::unify::MustImplementConstraints; +use roc_unify::unify::{MustImplementAbility, Obligated}; +use roc_unify::Env as UEnv; + +use crate::env::InferenceEnv; +use crate::{aliases::Aliases, to_var::type_to_var}; + +#[derive(Debug, Clone)] +pub enum AbilityImplError { + /// Promote this to a generic error that a type doesn't implement an ability + DoesNotImplement, + /// Promote this error to a `TypeError::BadExpr` from elsewhere + BadExpr(Region, Category, Variable), + /// Promote this error to a `TypeError::BadPattern` from elsewhere + BadPattern(Region, PatternCategory, Variable), +} + +/// Indexes a requested deriving of an ability for an opaque type. +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub struct RequestedDeriveKey { + pub opaque: Symbol, + pub ability: Symbol, +} + +/// Indexes a custom implementation of an ability for an opaque type. +#[derive(Debug, PartialEq, Clone, Copy)] +struct ImplKey { + opaque: Symbol, + ability: Symbol, +} + +#[derive(Debug)] +pub struct PendingDerivesTable( + /// derive key -> (opaque type var to use for checking, derive region) + VecMap, +); + +impl PendingDerivesTable { + pub fn new( + env: &mut InferenceEnv, + types: &mut Types, + aliases: &mut Aliases, + pending_derives: PendingDerives, + problems: &mut Vec, + abilities_store: &mut AbilitiesStore, + obligation_cache: &mut ObligationCache, + ) -> Self { + let mut table = VecMap::with_capacity(pending_derives.len()); + + for (opaque, (typ, derives)) in pending_derives.into_iter() { + for Loc { + value: ability, + region, + } in derives + { + debug_assert!( + ability.is_derivable_ability(), + "Not a builtin - should have been caught during can" + ); + let derive_key = RequestedDeriveKey { opaque, ability }; + + // Neither rank nor pools should matter here. + let typ = types.from_old_type(&typ); + let opaque_var = type_to_var( + env, + Rank::toplevel(), + problems, + abilities_store, + obligation_cache, + types, + aliases, + typ, + ); + let real_var = match env.subs.get_content_without_compacting(opaque_var) { + Content::Alias(_, _, real_var, AliasKind::Opaque) => real_var, + _ => internal_error!("Non-opaque in derives table"), + }; + + let old = table.insert(derive_key, (*real_var, region)); + debug_assert!(old.is_none()); + } + } + + Self(table) + } +} + +type ObligationResult = Result<(), Unfulfilled>; + +#[derive(Default)] +pub struct ObligationCache { + impl_cache: VecMap, + derive_cache: VecMap, +} + +enum ReadCache { + Impl, +} + +pub struct CheckedDerives { + pub legal_derives: Vec, + pub problems: Vec, +} + +impl ObligationCache { + #[must_use] + pub fn check_derives( + &mut self, + subs: &mut Subs, + abilities_store: &AbilitiesStore, + pending_derives: PendingDerivesTable, + ) -> CheckedDerives { + let mut legal_derives = Vec::with_capacity(pending_derives.0.len()); + let mut problems = vec![]; + + // First, check all derives. + for (&derive_key, &(opaque_real_var, derive_region)) in pending_derives.0.iter() { + self.check_derive( + subs, + abilities_store, + derive_key, + opaque_real_var, + derive_region, + ); + let result = self.derive_cache.get(&derive_key).unwrap(); + match result { + Ok(()) => legal_derives.push(derive_key), + Err(problem) => problems.push(TypeError::UnfulfilledAbility(problem.clone())), + } + } + + CheckedDerives { + legal_derives, + problems, + } + } + + #[must_use] + pub fn check_obligations( + &mut self, + subs: &mut Subs, + abilities_store: &AbilitiesStore, + must_implement: MustImplementConstraints, + on_error: AbilityImplError, + ) -> Vec { + let must_implement = must_implement.get_unique(); + + let mut get_unfulfilled = |must_implement: &[MustImplementAbility]| { + must_implement + .iter() + .filter_map(|mia| { + self.check_one(subs, abilities_store, *mia) + .as_ref() + .err() + .cloned() + }) + .collect::>() + }; + + let mut reported_in_context = VecSet::default(); + let mut incomplete_not_in_context = VecSet::default(); + let mut problems = vec![]; + + use AbilityImplError::*; + match on_error { + DoesNotImplement => { + // These aren't attached to another type error, so if these must_implement + // constraints aren't met, we'll emit a generic "this type doesn't implement an + // ability" error message at the end. We only want to do this if it turns out + // the "must implement" constraint indeed wasn't part of a more specific type + // error. + incomplete_not_in_context.extend(must_implement); + } + BadExpr(region, category, var) => { + let unfulfilled = get_unfulfilled(&must_implement); + + if !unfulfilled.is_empty() { + // Demote the bad variable that exposed this problem to an error, both so + // that we have an ErrorType to report and so that codegen knows to deal + // with the error later. + let error_type = subs.var_to_error_type(var, Polarity::OF_VALUE); + problems.push(TypeError::BadExprMissingAbility( + region, + category, + error_type, + unfulfilled, + )); + reported_in_context.extend(must_implement); + } + } + BadPattern(region, category, var) => { + let unfulfilled = get_unfulfilled(&must_implement); + + if !unfulfilled.is_empty() { + // Demote the bad variable that exposed this problem to an error, both so + // that we have an ErrorType to report and so that codegen knows to deal + // with the error later. + let error_type = subs.var_to_error_type(var, Polarity::OF_PATTERN); + problems.push(TypeError::BadPatternMissingAbility( + region, + category, + error_type, + unfulfilled, + )); + reported_in_context.extend(must_implement); + } + } + } + + // Go through and attach generic "type does not implement ability" errors, if they were not + // part of a larger context. + for mia in incomplete_not_in_context.into_iter() { + // If the obligation is already cached, we must have already reported it in another + // context. + if !self.has_cached(mia) && !reported_in_context.contains(&mia) { + if let Err(unfulfilled) = self.check_one(subs, abilities_store, mia) { + problems.push(TypeError::UnfulfilledAbility(unfulfilled.clone())); + } + } + } + + problems + } + + fn check_one( + &mut self, + subs: &mut Subs, + abilities_store: &AbilitiesStore, + mia: MustImplementAbility, + ) -> ObligationResult { + let MustImplementAbility { typ, ability } = mia; + + match typ { + Obligated::Adhoc(var) => self.check_adhoc(subs, abilities_store, var, ability), + Obligated::Opaque(opaque) => self + .check_opaque_and_read(abilities_store, opaque, ability) + .clone(), + } + } + + fn has_cached(&self, mia: MustImplementAbility) -> bool { + match mia.typ { + Obligated::Opaque(opaque) => self.impl_cache.contains_key(&ImplKey { + opaque, + ability: mia.ability, + }), + Obligated::Adhoc(_) => { + // ad-hoc obligations are never cached + false + } + } + } + + fn check_adhoc( + &mut self, + subs: &mut Subs, + abilities_store: &AbilitiesStore, + var: Variable, + ability: Symbol, + ) -> ObligationResult { + // Not worth caching ad-hoc checks because variables are unlikely to be the same between + // independent queries. + + let opt_can_derive_builtin = match ability { + Symbol::ENCODE_ENCODING => Some(DeriveEncoding::is_derivable( + self, + abilities_store, + subs, + var, + )), + + Symbol::DECODE_DECODING => Some(DeriveDecoding::is_derivable( + self, + abilities_store, + subs, + var, + )), + + Symbol::HASH_HASH_ABILITY => { + Some(DeriveHash::is_derivable(self, abilities_store, subs, var)) + } + + Symbol::BOOL_EQ => Some(DeriveEq::is_derivable(self, abilities_store, subs, var)), + + Symbol::INSPECT_INSPECT_ABILITY => Some(DeriveInspect::is_derivable( + self, + abilities_store, + subs, + var, + )), + + _ => None, + }; + + let opt_underivable = match opt_can_derive_builtin { + Some(Ok(())) => { + // can derive! + None + } + Some(Err(NotDerivable { + var: failure_var, + context, + })) => { + dbg_do!(ROC_PRINT_UNDERIVABLE, { + eprintln!("❌ derived {:?} of {:?}", ability, subs.dbg(failure_var)); + }); + + Some(if failure_var == var { + UnderivableReason::SurfaceNotDerivable(context) + } else { + let error_type = subs.var_to_error_type(failure_var, Polarity::OF_VALUE); + UnderivableReason::NestedNotDerivable(error_type, context) + }) + } + None => Some(UnderivableReason::NotABuiltin), + }; + + if let Some(underivable_reason) = opt_underivable { + let error_type = subs.var_to_error_type(var, Polarity::OF_VALUE); + + Err(Unfulfilled::AdhocUnderivable { + typ: error_type, + ability, + reason: underivable_reason, + }) + } else { + Ok(()) + } + } + + fn check_opaque( + &mut self, + abilities_store: &AbilitiesStore, + opaque: Symbol, + ability: Symbol, + ) -> ReadCache { + let impl_key = ImplKey { opaque, ability }; + + self.check_impl(abilities_store, impl_key); + ReadCache::Impl + } + + fn check_opaque_and_read( + &mut self, + abilities_store: &AbilitiesStore, + opaque: Symbol, + ability: Symbol, + ) -> &ObligationResult { + match self.check_opaque(abilities_store, opaque, ability) { + ReadCache::Impl => self.impl_cache.get(&ImplKey { opaque, ability }).unwrap(), + } + } + + fn check_impl(&mut self, abilities_store: &AbilitiesStore, impl_key: ImplKey) { + if self.impl_cache.get(&impl_key).is_some() { + return; + } + + let ImplKey { opaque, ability } = impl_key; + + // Every type has the Inspect ability automatically, even opaques with no `implements` declaration. + let is_inspect = ability == Symbol::INSPECT_INSPECT_ABILITY; + let has_known_impl = + is_inspect || abilities_store.has_declared_implementation(opaque, ability); + + // Some builtins, like Float32 and Bool, would have a cyclic dependency on Encode/Decode/etc. + // if their Roc implementations explicitly defined some abilities they support. + let builtin_opaque_impl_ok = || match ability { + DeriveEncoding::ABILITY => DeriveEncoding::is_derivable_builtin_opaque(opaque), + DeriveDecoding::ABILITY => DeriveDecoding::is_derivable_builtin_opaque(opaque), + DeriveEq::ABILITY => DeriveEq::is_derivable_builtin_opaque(opaque), + DeriveHash::ABILITY => DeriveHash::is_derivable_builtin_opaque(opaque), + DeriveInspect::ABILITY => DeriveInspect::is_derivable_builtin_opaque(opaque), + _ => false, + }; + + let obligation_result = if has_known_impl || builtin_opaque_impl_ok() { + Ok(()) + } else { + Err(Unfulfilled::OpaqueDoesNotImplement { + typ: opaque, + ability, + }) + }; + + self.impl_cache.insert(impl_key, obligation_result); + } + + fn check_derive( + &mut self, + subs: &mut Subs, + abilities_store: &AbilitiesStore, + derive_key: RequestedDeriveKey, + opaque_real_var: Variable, + derive_region: Region, + ) { + if self.derive_cache.get(&derive_key).is_some() { + return; + } + + // The opaque may be recursive, so make sure we stop if we see it again. + // We need to enforce that on both the impl and derive cache. + let fake_fulfilled = Ok(()); + // If this opaque both derives and specializes, we may already know whether the + // specialization fulfills or not. Since specializations take priority over derives, we + // want to keep that result around. + let impl_key = ImplKey { + opaque: derive_key.opaque, + ability: derive_key.ability, + }; + let opt_specialization_result = self.impl_cache.insert(impl_key, fake_fulfilled.clone()); + + let old_deriving = self.derive_cache.insert(derive_key, fake_fulfilled.clone()); + debug_assert!( + old_deriving.is_none(), + "Already knew deriving result, but here anyway" + ); + + // Now we check whether the structural type behind the opaque is derivable, since that's + // what we'll need to generate an implementation for during codegen. + let real_var_result = + self.check_adhoc(subs, abilities_store, opaque_real_var, derive_key.ability); + + let root_result = real_var_result.map_err(|err| match err { + // Promote the failure, which should be related to a structural type not being + // derivable for the ability, to a failure regarding the opaque in particular. + Unfulfilled::AdhocUnderivable { + typ, + ability, + reason, + } => Unfulfilled::OpaqueUnderivable { + typ, + ability, + reason, + opaque: derive_key.opaque, + derive_region, + }, + _ => internal_error!("unexpected underivable result"), + }); + + // Remove the derive result because the specialization check should take priority. + let check_has_fake = self.impl_cache.remove(&impl_key); + debug_assert_eq!(check_has_fake.map(|(_, b)| b), Some(fake_fulfilled.clone())); + + if let Some(specialization_result) = opt_specialization_result { + self.impl_cache.insert(impl_key, specialization_result); + } + + // Make sure we fix-up with the correct result of the check. + let check_has_fake = self.derive_cache.insert(derive_key, root_result); + debug_assert_eq!(check_has_fake, Some(fake_fulfilled)); + } +} + +#[inline(always)] +#[rustfmt::skip] +fn is_builtin_fixed_int_alias(symbol: Symbol) -> bool { + matches!(symbol, + | Symbol::NUM_U8 | Symbol::NUM_UNSIGNED8 + | Symbol::NUM_U16 | Symbol::NUM_UNSIGNED16 + | Symbol::NUM_U32 | Symbol::NUM_UNSIGNED32 + | Symbol::NUM_U64 | Symbol::NUM_UNSIGNED64 + | Symbol::NUM_U128 | Symbol::NUM_UNSIGNED128 + | Symbol::NUM_I8 | Symbol::NUM_SIGNED8 + | Symbol::NUM_I16 | Symbol::NUM_SIGNED16 + | Symbol::NUM_I32 | Symbol::NUM_SIGNED32 + | Symbol::NUM_I64 | Symbol::NUM_SIGNED64 + | Symbol::NUM_I128 | Symbol::NUM_SIGNED128 + ) +} + +#[inline(always)] +fn is_builtin_nat_alias(symbol: Symbol) -> bool { + matches!(symbol, Symbol::NUM_NAT | Symbol::NUM_NATURAL) +} + +#[inline(always)] +#[rustfmt::skip] +fn is_builtin_float_alias(symbol: Symbol) -> bool { + matches!(symbol, + | Symbol::NUM_F32 | Symbol::NUM_BINARY32 + | Symbol::NUM_F64 | Symbol::NUM_BINARY64 + ) +} + +#[inline(always)] +fn is_builtin_dec_alias(symbol: Symbol) -> bool { + matches!(symbol, Symbol::NUM_DEC | Symbol::NUM_DECIMAL,) +} + +#[inline(always)] +fn is_builtin_number_alias(symbol: Symbol) -> bool { + is_builtin_fixed_int_alias(symbol) + || is_builtin_nat_alias(symbol) + || is_builtin_float_alias(symbol) + || is_builtin_dec_alias(symbol) +} + +#[inline(always)] +fn is_builtin_bool_alias(symbol: Symbol) -> bool { + matches!(symbol, Symbol::BOOL_BOOL) +} + +struct NotDerivable { + var: Variable, + context: NotDerivableContext, +} + +struct Descend(bool); + +trait DerivableVisitor { + const ABILITY: Symbol; + const ABILITY_SLICE: SubsSlice; + + #[inline(always)] + fn is_derivable_builtin_opaque(_symbol: Symbol) -> bool { + false + } + + #[inline(always)] + fn visit_recursion(var: Variable) -> Result { + Err(NotDerivable { + var, + context: NotDerivableContext::NoContext, + }) + } + + #[inline(always)] + fn visit_apply(var: Variable, _symbol: Symbol) -> Result { + Err(NotDerivable { + var, + context: NotDerivableContext::NoContext, + }) + } + + #[inline(always)] + fn visit_func(var: Variable) -> Result { + Err(NotDerivable { + var, + context: NotDerivableContext::Function, + }) + } + + #[inline(always)] + fn visit_record( + _subs: &Subs, + var: Variable, + _fields: RecordFields, + ) -> Result { + Err(NotDerivable { + var, + context: NotDerivableContext::NoContext, + }) + } + + #[inline(always)] + fn visit_tuple( + _subs: &Subs, + var: Variable, + _elems: TupleElems, + ) -> Result { + Err(NotDerivable { + var, + context: NotDerivableContext::NoContext, + }) + } + + #[inline(always)] + fn visit_tag_union(var: Variable) -> Result { + Err(NotDerivable { + var, + context: NotDerivableContext::NoContext, + }) + } + + #[inline(always)] + fn visit_recursive_tag_union(var: Variable) -> Result { + Err(NotDerivable { + var, + context: NotDerivableContext::NoContext, + }) + } + + #[inline(always)] + fn visit_function_or_tag_union(var: Variable) -> Result { + Err(NotDerivable { + var, + context: NotDerivableContext::NoContext, + }) + } + + #[inline(always)] + fn visit_empty_record(var: Variable) -> Result<(), NotDerivable> { + Err(NotDerivable { + var, + context: NotDerivableContext::NoContext, + }) + } + + #[inline(always)] + fn visit_empty_tuple(var: Variable) -> Result<(), NotDerivable> { + Err(NotDerivable { + var, + context: NotDerivableContext::NoContext, + }) + } + + #[inline(always)] + fn visit_empty_tag_union(var: Variable) -> Result<(), NotDerivable> { + Err(NotDerivable { + var, + context: NotDerivableContext::NoContext, + }) + } + + #[inline(always)] + fn visit_alias(var: Variable, _symbol: Symbol) -> Result { + Err(NotDerivable { + var, + context: NotDerivableContext::NoContext, + }) + } + + #[inline(always)] + fn visit_floating_point_content( + var: Variable, + _subs: &mut Subs, + _content_var: Variable, + ) -> Result { + Err(NotDerivable { + var, + context: NotDerivableContext::NoContext, + }) + } + + #[inline(always)] + fn visit_ranged_number(var: Variable, _range: NumericRange) -> Result<(), NotDerivable> { + Err(NotDerivable { + var, + context: NotDerivableContext::NoContext, + }) + } + + #[inline(always)] + fn is_derivable( + obligation_cache: &mut ObligationCache, + abilities_store: &AbilitiesStore, + subs: &mut Subs, + var: Variable, + ) -> Result<(), NotDerivable> { + let mut stack = vec![var]; + let mut seen_recursion_vars = vec![]; + + macro_rules! push_var_slice { + ($slice:expr) => { + stack.extend(subs.get_subs_slice($slice)) + }; + } + + while let Some(var) = stack.pop() { + if seen_recursion_vars.contains(&var) { + continue; + } + + let content = subs.get_content_without_compacting(var); + + use Content::*; + use FlatType::*; + match *content { + FlexVar(opt_name) => { + // Promote the flex var to be bound to the ability. + subs.set_content(var, Content::FlexAbleVar(opt_name, Self::ABILITY_SLICE)); + } + RigidVar(_) => { + return Err(NotDerivable { + var, + context: NotDerivableContext::UnboundVar, + }) + } + FlexAbleVar(opt_name, abilities) => { + // This flex var inherits the ability. + let merged_abilites = roc_unify::unify::merged_ability_slices( + subs, + abilities, + Self::ABILITY_SLICE, + ); + subs.set_content(var, Content::FlexAbleVar(opt_name, merged_abilites)); + } + RigidAbleVar(_, abilities) => { + if !subs.get_subs_slice(abilities).contains(&Self::ABILITY) { + return Err(NotDerivable { + var, + context: NotDerivableContext::NoContext, + }); + } + } + RecursionVar { + structure, + opt_name: _, + } => { + let descend = Self::visit_recursion(var)?; + if descend.0 { + seen_recursion_vars.push(var); + stack.push(structure); + } + } + Structure(flat_type) => match flat_type { + Apply(symbol, vars) => { + let descend = Self::visit_apply(var, symbol)?; + if descend.0 { + push_var_slice!(vars) + } + } + Func(args, _clos, ret) => { + let descend = Self::visit_func(var)?; + if descend.0 { + push_var_slice!(args); + stack.push(ret); + } + } + Record(fields, ext) => { + let descend = Self::visit_record(subs, var, fields)?; + if descend.0 { + push_var_slice!(fields.variables()); + if !matches!( + subs.get_content_without_compacting(ext), + Content::FlexVar(_) | Content::RigidVar(_) + ) { + // TODO: currently, just we suppose the presence of a flex var may + // include more or less things which we can derive. But, we should + // instead recurse here, and add a `t ~ u where u implements Decode` constraint as needed. + stack.push(ext); + } + } + } + Tuple(elems, ext) => { + let descend = Self::visit_tuple(subs, var, elems)?; + if descend.0 { + push_var_slice!(elems.variables()); + if !matches!( + subs.get_content_without_compacting(ext), + Content::FlexVar(_) | Content::RigidVar(_) + ) { + stack.push(ext); + } + } + } + TagUnion(tags, ext) => { + let descend = Self::visit_tag_union(var)?; + if descend.0 { + for i in tags.variables() { + push_var_slice!(subs[i]); + } + stack.push(ext.var()); + } + } + FunctionOrTagUnion(_tag_name, _fn_name, ext) => { + let descend = Self::visit_function_or_tag_union(var)?; + if descend.0 { + stack.push(ext.var()); + } + } + RecursiveTagUnion(rec, tags, ext) => { + let descend = Self::visit_recursive_tag_union(var)?; + if descend.0 { + seen_recursion_vars.push(rec); + for i in tags.variables() { + push_var_slice!(subs[i]); + } + stack.push(ext.var()); + } + } + EmptyRecord => Self::visit_empty_record(var)?, + EmptyTuple => Self::visit_empty_tuple(var)?, + EmptyTagUnion => Self::visit_empty_tag_union(var)?, + }, + Alias( + Symbol::NUM_NUM | Symbol::NUM_INTEGER, + _alias_variables, + real_var, + AliasKind::Opaque, + ) => { + // Unbound numbers and integers: always decay until a ground is hit, + // since all of our builtin abilities currently support integers. + stack.push(real_var); + } + Alias(Symbol::NUM_FLOATINGPOINT, _alias_variables, real_var, AliasKind::Opaque) => { + let descend = Self::visit_floating_point_content(var, subs, real_var)?; + if descend.0 { + // Decay to a ground + stack.push(real_var) + } + } + Alias(opaque, _alias_variables, _real_var, AliasKind::Opaque) => { + if obligation_cache + .check_opaque_and_read(abilities_store, opaque, Self::ABILITY) + .is_err() + && !Self::is_derivable_builtin_opaque(opaque) + { + return Err(NotDerivable { + var, + context: NotDerivableContext::Opaque(opaque), + }); + } + } + Alias(symbol, _alias_variables, real_var, AliasKind::Structural) => { + let descend = Self::visit_alias(var, symbol)?; + if descend.0 { + stack.push(real_var); + } + } + RangedNumber(range) => Self::visit_ranged_number(var, range)?, + + LambdaSet(..) => { + return Err(NotDerivable { + var, + context: NotDerivableContext::NoContext, + }) + } + ErasedLambda => { + return Err(NotDerivable { + var, + context: NotDerivableContext::NoContext, + }) + } + Error => { + return Err(NotDerivable { + var, + context: NotDerivableContext::NoContext, + }); + } + } + } + + Ok(()) + } +} + +struct DeriveInspect; +impl DerivableVisitor for DeriveInspect { + const ABILITY: Symbol = Symbol::INSPECT_INSPECT_ABILITY; + const ABILITY_SLICE: SubsSlice = Subs::AB_INSPECT; + + #[inline(always)] + fn is_derivable_builtin_opaque(_: Symbol) -> bool { + true + } + + #[inline(always)] + fn visit_recursion(_var: Variable) -> Result { + Ok(Descend(true)) + } + + #[inline(always)] + fn visit_apply(_: Variable, _: Symbol) -> Result { + Ok(Descend(true)) + } + + #[inline(always)] + fn visit_record( + _subs: &Subs, + _var: Variable, + _fields: RecordFields, + ) -> Result { + Ok(Descend(true)) + } + + #[inline(always)] + fn visit_tuple( + _subs: &Subs, + _var: Variable, + _elems: TupleElems, + ) -> Result { + Ok(Descend(true)) + } + + #[inline(always)] + fn visit_tag_union(_var: Variable) -> Result { + Ok(Descend(true)) + } + + #[inline(always)] + fn visit_recursive_tag_union(_var: Variable) -> Result { + Ok(Descend(true)) + } + + #[inline(always)] + fn visit_function_or_tag_union(_var: Variable) -> Result { + Ok(Descend(true)) + } + + #[inline(always)] + fn visit_empty_record(_var: Variable) -> Result<(), NotDerivable> { + Ok(()) + } + + #[inline(always)] + fn visit_empty_tag_union(_var: Variable) -> Result<(), NotDerivable> { + Ok(()) + } + + #[inline(always)] + fn visit_alias(_var: Variable, symbol: Symbol) -> Result { + if is_builtin_number_alias(symbol) { + Ok(Descend(false)) + } else { + Ok(Descend(true)) + } + } + + #[inline(always)] + fn visit_ranged_number(_var: Variable, _range: NumericRange) -> Result<(), NotDerivable> { + Ok(()) + } + + #[inline(always)] + fn visit_floating_point_content( + _var: Variable, + _subs: &mut Subs, + _content_var: Variable, + ) -> Result { + Ok(Descend(false)) + } +} + +struct DeriveEncoding; +impl DerivableVisitor for DeriveEncoding { + const ABILITY: Symbol = Symbol::ENCODE_ENCODING; + const ABILITY_SLICE: SubsSlice = Subs::AB_ENCODING; + + #[inline(always)] + fn is_derivable_builtin_opaque(symbol: Symbol) -> bool { + (is_builtin_number_alias(symbol) && !is_builtin_nat_alias(symbol)) + || is_builtin_bool_alias(symbol) + } + + #[inline(always)] + fn visit_recursion(_var: Variable) -> Result { + Ok(Descend(true)) + } + + #[inline(always)] + fn visit_apply(var: Variable, symbol: Symbol) -> Result { + if matches!( + symbol, + Symbol::LIST_LIST | Symbol::SET_SET | Symbol::DICT_DICT | Symbol::STR_STR, + ) { + Ok(Descend(true)) + } else { + Err(NotDerivable { + var, + context: NotDerivableContext::NoContext, + }) + } + } + + #[inline(always)] + fn visit_record( + _subs: &Subs, + _var: Variable, + _fields: RecordFields, + ) -> Result { + Ok(Descend(true)) + } + + #[inline(always)] + fn visit_tuple( + _subs: &Subs, + _var: Variable, + _elems: TupleElems, + ) -> Result { + Ok(Descend(true)) + } + + #[inline(always)] + fn visit_tag_union(_var: Variable) -> Result { + Ok(Descend(true)) + } + + #[inline(always)] + fn visit_recursive_tag_union(_var: Variable) -> Result { + Ok(Descend(true)) + } + + #[inline(always)] + fn visit_function_or_tag_union(_var: Variable) -> Result { + Ok(Descend(true)) + } + + #[inline(always)] + fn visit_empty_record(_var: Variable) -> Result<(), NotDerivable> { + Ok(()) + } + + #[inline(always)] + fn visit_empty_tag_union(_var: Variable) -> Result<(), NotDerivable> { + Ok(()) + } + + #[inline(always)] + fn visit_alias(var: Variable, symbol: Symbol) -> Result { + if is_builtin_number_alias(symbol) { + if is_builtin_nat_alias(symbol) { + Err(NotDerivable { + var, + context: NotDerivableContext::Encode(NotDerivableEncode::Nat), + }) + } else { + Ok(Descend(false)) + } + } else { + Ok(Descend(true)) + } + } + + #[inline(always)] + fn visit_ranged_number(_var: Variable, _range: NumericRange) -> Result<(), NotDerivable> { + Ok(()) + } + + #[inline(always)] + fn visit_floating_point_content( + _var: Variable, + _subs: &mut Subs, + _content_var: Variable, + ) -> Result { + Ok(Descend(false)) + } +} + +struct DeriveDecoding; +impl DerivableVisitor for DeriveDecoding { + const ABILITY: Symbol = Symbol::DECODE_DECODING; + const ABILITY_SLICE: SubsSlice = Subs::AB_DECODING; + + #[inline(always)] + fn is_derivable_builtin_opaque(symbol: Symbol) -> bool { + (is_builtin_number_alias(symbol) && !is_builtin_nat_alias(symbol)) + || is_builtin_bool_alias(symbol) + } + + #[inline(always)] + fn visit_recursion(_var: Variable) -> Result { + Ok(Descend(true)) + } + + #[inline(always)] + fn visit_apply(var: Variable, symbol: Symbol) -> Result { + if matches!( + symbol, + Symbol::LIST_LIST | Symbol::SET_SET | Symbol::DICT_DICT | Symbol::STR_STR, + ) { + Ok(Descend(true)) + } else { + Err(NotDerivable { + var, + context: NotDerivableContext::NoContext, + }) + } + } + + #[inline(always)] + fn visit_record( + subs: &Subs, + var: Variable, + fields: RecordFields, + ) -> Result { + for (field_name, _, field) in fields.iter_all() { + if subs[field].is_optional() { + return Err(NotDerivable { + var, + context: NotDerivableContext::Decode(NotDerivableDecode::OptionalRecordField( + subs[field_name].clone(), + )), + }); + } + } + + Ok(Descend(true)) + } + + #[inline(always)] + fn visit_tuple( + _subs: &Subs, + _var: Variable, + _elems: TupleElems, + ) -> Result { + Ok(Descend(true)) + } + + #[inline(always)] + fn visit_tag_union(_var: Variable) -> Result { + Ok(Descend(true)) + } + + #[inline(always)] + fn visit_recursive_tag_union(_var: Variable) -> Result { + Ok(Descend(true)) + } + + #[inline(always)] + fn visit_function_or_tag_union(_var: Variable) -> Result { + Ok(Descend(true)) + } + + #[inline(always)] + fn visit_empty_record(_var: Variable) -> Result<(), NotDerivable> { + Ok(()) + } + + #[inline(always)] + fn visit_empty_tag_union(_var: Variable) -> Result<(), NotDerivable> { + Ok(()) + } + + #[inline(always)] + fn visit_alias(var: Variable, symbol: Symbol) -> Result { + if is_builtin_number_alias(symbol) { + if is_builtin_nat_alias(symbol) { + Err(NotDerivable { + var, + context: NotDerivableContext::Decode(NotDerivableDecode::Nat), + }) + } else { + Ok(Descend(false)) + } + } else { + Ok(Descend(true)) + } + } + + #[inline(always)] + fn visit_ranged_number(_var: Variable, _range: NumericRange) -> Result<(), NotDerivable> { + Ok(()) + } + + #[inline(always)] + fn visit_floating_point_content( + _var: Variable, + _subs: &mut Subs, + _content_var: Variable, + ) -> Result { + Ok(Descend(false)) + } +} + +struct DeriveHash; +impl DerivableVisitor for DeriveHash { + const ABILITY: Symbol = Symbol::HASH_HASH_ABILITY; + const ABILITY_SLICE: SubsSlice = Subs::AB_HASH; + + #[inline(always)] + fn is_derivable_builtin_opaque(symbol: Symbol) -> bool { + is_builtin_number_alias(symbol) || is_builtin_bool_alias(symbol) + } + + #[inline(always)] + fn visit_recursion(_var: Variable) -> Result { + Ok(Descend(true)) + } + + #[inline(always)] + fn visit_apply(var: Variable, symbol: Symbol) -> Result { + if matches!( + symbol, + Symbol::LIST_LIST | Symbol::SET_SET | Symbol::DICT_DICT | Symbol::STR_STR, + ) { + Ok(Descend(true)) + } else { + Err(NotDerivable { + var, + context: NotDerivableContext::NoContext, + }) + } + } + + #[inline(always)] + fn visit_record( + subs: &Subs, + var: Variable, + fields: RecordFields, + ) -> Result { + for (field_name, _, field) in fields.iter_all() { + if subs[field].is_optional() { + return Err(NotDerivable { + var, + context: NotDerivableContext::Decode(NotDerivableDecode::OptionalRecordField( + subs[field_name].clone(), + )), + }); + } + } + + Ok(Descend(true)) + } + + #[inline(always)] + fn visit_tuple( + _subs: &Subs, + _var: Variable, + _elems: TupleElems, + ) -> Result { + Ok(Descend(true)) + } + + #[inline(always)] + fn visit_tag_union(_var: Variable) -> Result { + Ok(Descend(true)) + } + + #[inline(always)] + fn visit_recursive_tag_union(_var: Variable) -> Result { + Ok(Descend(true)) + } + + #[inline(always)] + fn visit_function_or_tag_union(_var: Variable) -> Result { + Ok(Descend(true)) + } + + #[inline(always)] + fn visit_empty_record(_var: Variable) -> Result<(), NotDerivable> { + Ok(()) + } + + #[inline(always)] + fn visit_empty_tag_union(_var: Variable) -> Result<(), NotDerivable> { + Ok(()) + } + + #[inline(always)] + fn visit_alias(_var: Variable, symbol: Symbol) -> Result { + if is_builtin_number_alias(symbol) { + Ok(Descend(false)) + } else { + Ok(Descend(true)) + } + } + + #[inline(always)] + fn visit_ranged_number(_var: Variable, _range: NumericRange) -> Result<(), NotDerivable> { + Ok(()) + } + + #[inline(always)] + fn visit_floating_point_content( + _var: Variable, + _subs: &mut Subs, + _content_var: Variable, + ) -> Result { + Ok(Descend(false)) + } +} + +struct DeriveEq; +impl DerivableVisitor for DeriveEq { + const ABILITY: Symbol = Symbol::BOOL_EQ; + const ABILITY_SLICE: SubsSlice = Subs::AB_EQ; + + #[inline(always)] + fn is_derivable_builtin_opaque(symbol: Symbol) -> bool { + is_builtin_fixed_int_alias(symbol) + || is_builtin_nat_alias(symbol) + || is_builtin_dec_alias(symbol) + || is_builtin_bool_alias(symbol) + } + + #[inline(always)] + fn visit_recursion(_var: Variable) -> Result { + Ok(Descend(true)) + } + + #[inline(always)] + fn visit_apply(var: Variable, symbol: Symbol) -> Result { + if matches!( + symbol, + Symbol::LIST_LIST + | Symbol::SET_SET + | Symbol::DICT_DICT + | Symbol::STR_STR + | Symbol::BOX_BOX_TYPE, + ) { + Ok(Descend(true)) + } else { + Err(NotDerivable { + var, + context: NotDerivableContext::NoContext, + }) + } + } + + #[inline(always)] + fn visit_record( + subs: &Subs, + var: Variable, + fields: RecordFields, + ) -> Result { + for (field_name, _, field) in fields.iter_all() { + if subs[field].is_optional() { + return Err(NotDerivable { + var, + context: NotDerivableContext::Decode(NotDerivableDecode::OptionalRecordField( + subs[field_name].clone(), + )), + }); + } + } + + Ok(Descend(true)) + } + + #[inline(always)] + fn visit_tuple( + _subs: &Subs, + _var: Variable, + _elems: TupleElems, + ) -> Result { + Ok(Descend(true)) + } + + #[inline(always)] + fn visit_tag_union(_var: Variable) -> Result { + Ok(Descend(true)) + } + + #[inline(always)] + fn visit_recursive_tag_union(_var: Variable) -> Result { + Ok(Descend(true)) + } + + #[inline(always)] + fn visit_function_or_tag_union(_var: Variable) -> Result { + Ok(Descend(true)) + } + + #[inline(always)] + fn visit_empty_record(_var: Variable) -> Result<(), NotDerivable> { + Ok(()) + } + + #[inline(always)] + fn visit_empty_tag_union(_var: Variable) -> Result<(), NotDerivable> { + Ok(()) + } + + #[inline(always)] + fn visit_alias(var: Variable, symbol: Symbol) -> Result { + if is_builtin_float_alias(symbol) { + Err(NotDerivable { + var, + context: NotDerivableContext::Eq(NotDerivableEq::FloatingPoint), + }) + } else if is_builtin_number_alias(symbol) { + Ok(Descend(false)) + } else { + Ok(Descend(true)) + } + } + + fn visit_floating_point_content( + var: Variable, + subs: &mut Subs, + content_var: Variable, + ) -> Result { + use roc_unify::unify::unify; + + // Of the floating-point types, + // only Dec implements Eq. + // TODO(checkmate): pass checkmate through + let unified = unify( + &mut with_checkmate!({ + on => UEnv::new(subs, None), + off => UEnv::new(subs), + }), + content_var, + Variable::DECIMAL, + UnificationMode::EQ, + Polarity::Pos, + ); + match unified { + roc_unify::unify::Unified::Success { .. } => Ok(Descend(false)), + roc_unify::unify::Unified::Failure(..) => Err(NotDerivable { + var, + context: NotDerivableContext::Eq(NotDerivableEq::FloatingPoint), + }), + } + } + + #[inline(always)] + fn visit_ranged_number(_var: Variable, _range: NumericRange) -> Result<(), NotDerivable> { + // Ranged numbers are allowed, because they are always possibly ints - floats can not have + // `isEq` derived, but if something were to be a float, we'd see it exactly as a float. + Ok(()) + } +} + +/// Determines what type implements an ability member of a specialized signature, given the +/// [MustImplementAbility] constraints of the signature. +pub fn type_implementing_specialization( + specialization_must_implement_constraints: &MustImplementConstraints, + ability: Symbol, +) -> Option { + debug_assert!({ + specialization_must_implement_constraints + .clone() + .get_unique() + .into_iter() + .filter(|mia| mia.ability == ability) + .count() + } < 2, + "Multiple variables bound to an ability - this is ambiguous and should have been caught in canonicalization: {specialization_must_implement_constraints:?}" + ); + + specialization_must_implement_constraints + .iter_for_ability(ability) + .next() + .map(|mia| mia.typ) +} + +/// Result of trying to resolve an ability specialization. +#[derive(Clone, Debug)] +pub enum Resolved { + /// A user-defined specialization should be used. + Specialization(Symbol), + /// A specialization must be generated with the given derive key. + Derive(Derived), +} + +/// An [`AbilityResolver`] is a shell of an abilities store that answers questions needed for +/// [resolving ability specializations][`resolve_ability_specialization`]. +/// +/// The trait is provided so you can implement your own resolver at other points in the compilation +/// process, for example during monomorphization we have module-re-entrant ability stores that are +/// not available during solving. +pub trait AbilityResolver { + /// Gets the parent ability and type of an ability member. + /// + /// If needed, the type of the ability member will be imported into a local `subs` buffer; as + /// such, subs must be provided. + fn member_parent_and_signature_var( + &self, + ability_member: Symbol, + home_subs: &mut Subs, + ) -> Option<(Symbol, Variable)>; + + /// Finds the declared implementation of an [`ImplKey`][roc_can::abilities::ImplKey]. + fn get_implementation(&self, impl_key: roc_can::abilities::ImplKey) -> Option; +} + +/// Trivial implementation of a resolver for a module-local abilities store, that defers all +/// queries to the module store. +impl AbilityResolver for AbilitiesStore { + #[inline(always)] + fn member_parent_and_signature_var( + &self, + ability_member: Symbol, + _home_subs: &mut Subs, // only have access to one abilities store, do nothing with subs + ) -> Option<(Symbol, Variable)> { + self.member_def(ability_member) + .map(|def| (def.parent_ability, def.signature_var())) + } + + #[inline(always)] + fn get_implementation(&self, impl_key: roc_can::abilities::ImplKey) -> Option { + self.get_implementation(impl_key).copied() + } +} + +/// Whether this is a module whose types' ability implementations should be checked via derive_key, +/// because they do not explicitly list ability implementations due to circular dependencies. +#[inline] +pub(crate) fn builtin_module_with_unlisted_ability_impl(module_id: ModuleId) -> bool { + matches!(module_id, ModuleId::NUM | ModuleId::BOOL) +} + +#[derive(Debug)] +pub enum ResolveError { + NonDerivableAbility(Symbol), + DeriveError(DeriveError), + NoTypeImplementingSpecialization, +} + +impl From for ResolveError { + fn from(e: DeriveError) -> Self { + Self::DeriveError(e) + } +} + +pub fn resolve_ability_specialization( + subs: &mut Subs, + resolver: &R, + ability_member: Symbol, + specialization_var: Variable, +) -> Result { + use roc_unify::unify::unify; + + let (parent_ability, signature_var) = resolver + .member_parent_and_signature_var(ability_member, subs) + .expect("Not an ability member symbol"); + + // Figure out the ability we're resolving in a temporary subs snapshot. + let snapshot = subs.snapshot(); + + instantiate_rigids(subs, signature_var); + let (_vars, must_implement_ability, _lambda_sets_to_specialize, _meta) = unify( + // TODO(checkmate): pass checkmate through + &mut with_checkmate!({ + on => UEnv::new(subs, None), + off => UEnv::new(subs), + }), + specialization_var, + signature_var, + UnificationMode::EQ, + Polarity::Pos, + ) + .expect_success( + "If resolving a specialization, the specialization must be known to typecheck.", + ); + + subs.rollback_to(snapshot); + + use ResolveError::*; + + let obligated = type_implementing_specialization(&must_implement_ability, parent_ability) + .ok_or(NoTypeImplementingSpecialization)?; + + let resolved = match obligated { + Obligated::Opaque(symbol) => { + if builtin_module_with_unlisted_ability_impl(symbol.module_id()) { + let derive_key = roc_derive_key::Derived::builtin_with_builtin_symbol( + ability_member.try_into().map_err(NonDerivableAbility)?, + symbol, + )?; + + Resolved::Derive(derive_key) + } else { + let impl_key = roc_can::abilities::ImplKey { + opaque: symbol, + ability_member, + }; + + match resolver + .get_implementation(impl_key) + .ok_or(NoTypeImplementingSpecialization)? + { + roc_types::types::MemberImpl::Impl(spec_symbol) => { + Resolved::Specialization(spec_symbol) + } + // TODO this is not correct. We can replace `Resolved` with `MemberImpl` entirely, + // which will make this simpler. + roc_types::types::MemberImpl::Error => { + Resolved::Specialization(Symbol::UNDERSCORE) + } + } + } + } + Obligated::Adhoc(variable) => { + let derive_key = roc_derive_key::Derived::builtin( + ability_member.try_into().map_err(NonDerivableAbility)?, + subs, + variable, + )?; + + Resolved::Derive(derive_key) + } + }; + + Ok(resolved) +} diff --git a/crates/compiler/solve/src/aliases.rs b/crates/compiler/solve/src/aliases.rs new file mode 100644 index 0000000000..94896e5a5e --- /dev/null +++ b/crates/compiler/solve/src/aliases.rs @@ -0,0 +1,329 @@ +use roc_can::abilities::AbilitiesStore; +use roc_collections::{soa::Index, MutMap}; +use roc_error_macros::internal_error; +use roc_module::symbol::Symbol; +use roc_solve_problem::TypeError; +use roc_types::{ + subs::{AliasVariables, Content, FlatType, Rank, Subs, SubsSlice, TagExt, UnionTags, Variable}, + types::{Alias, AliasKind, OptAbleVar, Type, TypeTag, Types}, +}; + +use crate::to_var::type_to_var_help; +use crate::{ability::ObligationCache, env::InferenceEnv}; + +#[derive(Debug, Clone, Copy)] +struct DelayedAliasVariables { + start: u32, + type_variables_len: u8, + lambda_set_variables_len: u8, + recursion_variables_len: u8, + infer_ext_in_output_variables_len: u8, +} + +impl DelayedAliasVariables { + fn recursion_variables(self, variables: &mut [OptAbleVar]) -> &mut [OptAbleVar] { + let start = self.start as usize + + (self.type_variables_len + self.lambda_set_variables_len) as usize; + let length = self.recursion_variables_len as usize; + + &mut variables[start..][..length] + } + + fn lambda_set_variables(self, variables: &mut [OptAbleVar]) -> &mut [OptAbleVar] { + let start = self.start as usize + self.type_variables_len as usize; + let length = self.lambda_set_variables_len as usize; + + &mut variables[start..][..length] + } + + fn type_variables(self, variables: &mut [OptAbleVar]) -> &mut [OptAbleVar] { + let start = self.start as usize; + let length = self.type_variables_len as usize; + + &mut variables[start..][..length] + } + + fn infer_ext_in_output_variables(self, variables: &mut [OptAbleVar]) -> &mut [OptAbleVar] { + let start = self.start as usize + + (self.type_variables_len + + self.lambda_set_variables_len + + self.recursion_variables_len) as usize; + let length = self.infer_ext_in_output_variables_len as usize; + + &mut variables[start..][..length] + } +} + +#[derive(Debug, Default)] +pub struct Aliases { + aliases: Vec<(Symbol, Index, DelayedAliasVariables, AliasKind)>, + variables: Vec, +} + +impl Aliases { + pub fn with_capacity(cap: usize) -> Self { + Self { + aliases: Vec::with_capacity(cap), + variables: Vec::with_capacity(cap * 2), + } + } + + pub fn insert(&mut self, types: &mut Types, symbol: Symbol, alias: Alias) { + let alias_variables = + { + let start = self.variables.len() as _; + + self.variables.extend( + alias + .type_variables + .iter() + .map(|x| OptAbleVar::from(&x.value)), + ); + + self.variables.extend(alias.lambda_set_variables.iter().map( + |x| match x.as_inner() { + Type::Variable(v) => OptAbleVar::unbound(*v), + _ => unreachable!("lambda set type is not a variable"), + }, + )); + + let recursion_variables_len = alias.recursion_variables.len() as _; + self.variables.extend( + alias + .recursion_variables + .iter() + .copied() + .map(OptAbleVar::unbound), + ); + + self.variables.extend( + alias + .infer_ext_in_output_variables + .iter() + .map(|v| OptAbleVar::unbound(*v)), + ); + + DelayedAliasVariables { + start, + type_variables_len: alias.type_variables.len() as _, + lambda_set_variables_len: alias.lambda_set_variables.len() as _, + recursion_variables_len, + infer_ext_in_output_variables_len: alias.infer_ext_in_output_variables.len() + as _, + } + }; + + // TODO: can we construct Aliases from TypeTag directly? + let alias_typ = types.from_old_type(&alias.typ); + + self.aliases + .push((symbol, alias_typ, alias_variables, alias.kind)); + } + + fn instantiate_result_result( + env: &mut InferenceEnv, + rank: Rank, + alias_variables: AliasVariables, + ) -> Variable { + let tag_names_slice = Subs::RESULT_TAG_NAMES; + + let err_slice = SubsSlice::new(alias_variables.variables_start + 1, 1); + let ok_slice = SubsSlice::new(alias_variables.variables_start, 1); + + let variable_slices = + SubsSlice::extend_new(&mut env.subs.variable_slices, [err_slice, ok_slice]); + + let union_tags = UnionTags::from_slices(tag_names_slice, variable_slices); + let ext_var = TagExt::Any(Variable::EMPTY_TAG_UNION); + let flat_type = FlatType::TagUnion(union_tags, ext_var); + let content = Content::Structure(flat_type); + + env.register(rank, content) + } + + /// Build an alias of the form `Num range := range` + fn build_num_opaque( + env: &mut InferenceEnv, + rank: Rank, + symbol: Symbol, + range_var: Variable, + ) -> Variable { + let content = Content::Alias( + symbol, + AliasVariables::insert_into_subs(env.subs, [range_var], [], []), + range_var, + AliasKind::Opaque, + ); + + env.register(rank, content) + } + + fn instantiate_builtin_aliases_real_var( + &mut self, + env: &mut InferenceEnv, + rank: Rank, + symbol: Symbol, + alias_variables: AliasVariables, + ) -> Option<(Variable, AliasKind)> { + match symbol { + Symbol::RESULT_RESULT => { + let var = Self::instantiate_result_result(env, rank, alias_variables); + + Some((var, AliasKind::Structural)) + } + Symbol::NUM_NUM | Symbol::NUM_INTEGER | Symbol::NUM_FLOATINGPOINT => { + // Num range := range | Integer range := range | FloatingPoint range := range + let range_var = env.subs.variables[alias_variables.variables_start as usize]; + Some((range_var, AliasKind::Opaque)) + } + Symbol::NUM_INT => { + // Int range : Num (Integer range) + // + // build `Integer range := range` + let integer_content_var = Self::build_num_opaque( + env, + rank, + Symbol::NUM_INTEGER, + env.subs.variables[alias_variables.variables_start as usize], + ); + + // build `Num (Integer range) := Integer range` + let num_content_var = + Self::build_num_opaque(env, rank, Symbol::NUM_NUM, integer_content_var); + + Some((num_content_var, AliasKind::Structural)) + } + Symbol::NUM_FRAC => { + // Frac range : Num (FloatingPoint range) + // + // build `FloatingPoint range := range` + let fpoint_content_var = Self::build_num_opaque( + env, + rank, + Symbol::NUM_FLOATINGPOINT, + env.subs.variables[alias_variables.variables_start as usize], + ); + + // build `Num (FloatingPoint range) := FloatingPoint range` + let num_content_var = + Self::build_num_opaque(env, rank, Symbol::NUM_NUM, fpoint_content_var); + + Some((num_content_var, AliasKind::Structural)) + } + Symbol::NUM_SIGNED8 => Some((Variable::SIGNED8, AliasKind::Opaque)), + Symbol::NUM_SIGNED16 => Some((Variable::SIGNED16, AliasKind::Opaque)), + Symbol::NUM_SIGNED32 => Some((Variable::SIGNED32, AliasKind::Opaque)), + Symbol::NUM_SIGNED64 => Some((Variable::SIGNED64, AliasKind::Opaque)), + Symbol::NUM_SIGNED128 => Some((Variable::SIGNED128, AliasKind::Opaque)), + Symbol::NUM_UNSIGNED8 => Some((Variable::UNSIGNED8, AliasKind::Opaque)), + Symbol::NUM_UNSIGNED16 => Some((Variable::UNSIGNED16, AliasKind::Opaque)), + Symbol::NUM_UNSIGNED32 => Some((Variable::UNSIGNED32, AliasKind::Opaque)), + Symbol::NUM_UNSIGNED64 => Some((Variable::UNSIGNED64, AliasKind::Opaque)), + Symbol::NUM_UNSIGNED128 => Some((Variable::UNSIGNED128, AliasKind::Opaque)), + Symbol::NUM_BINARY32 => Some((Variable::BINARY32, AliasKind::Opaque)), + Symbol::NUM_BINARY64 => Some((Variable::BINARY64, AliasKind::Opaque)), + _ => None, + } + } + + pub fn instantiate_real_var( + &mut self, + env: &mut InferenceEnv, + rank: Rank, + problems: &mut Vec, + abilities_store: &AbilitiesStore, + obligation_cache: &mut ObligationCache, + arena: &bumpalo::Bump, + types: &mut Types, + symbol: Symbol, + alias_variables: AliasVariables, + ) -> (Variable, AliasKind) { + // hardcoded instantiations for builtin aliases + if let Some((var, kind)) = + self.instantiate_builtin_aliases_real_var(env, rank, symbol, alias_variables) + { + return (var, kind); + } + + let (typ, delayed_variables, kind) = + match self.aliases.iter().find(|(s, _, _, _)| *s == symbol) { + None => internal_error!( + "Alias {:?} not registered in delayed aliases! {:?}", + symbol, + &self.aliases + ), + Some(&(_, typ, delayed_variables, kind)) => (typ, delayed_variables, kind), + }; + + let mut substitutions: MutMap<_, _> = Default::default(); + + let old_type_variables = delayed_variables.type_variables(&mut self.variables); + let new_type_variables = &env.subs.variables[alias_variables.type_variables().indices()]; + + for (old, new) in old_type_variables.iter_mut().zip(new_type_variables) { + // if constraint gen duplicated a type these variables could be the same + // (happens very often in practice) + if old.var != *new { + substitutions.insert(old.var, *new); + } + } + + for OptAbleVar { + var: rec_var, + opt_abilities, + } in delayed_variables + .recursion_variables(&mut self.variables) + .iter_mut() + { + debug_assert!(opt_abilities.is_none()); + let new_var = env.subs.fresh_unnamed_flex_var(); + substitutions.insert(*rec_var, new_var); + } + + let old_lambda_set_variables = delayed_variables.lambda_set_variables(&mut self.variables); + let new_lambda_set_variables = + &env.subs.variables[alias_variables.lambda_set_variables().indices()]; + + for (old, new) in old_lambda_set_variables + .iter_mut() + .zip(new_lambda_set_variables) + { + debug_assert!(old.opt_abilities.is_none()); + if old.var != *new { + substitutions.insert(old.var, *new); + } + } + + let old_infer_ext_vars = + delayed_variables.infer_ext_in_output_variables(&mut self.variables); + let new_infer_ext_vars = + &env.subs.variables[alias_variables.infer_ext_in_output_variables().indices()]; + + for (old, new) in old_infer_ext_vars.iter_mut().zip(new_infer_ext_vars) { + debug_assert!(old.opt_abilities.is_none()); + if old.var != *new { + substitutions.insert(old.var, *new); + } + } + + let typ = if !substitutions.is_empty() { + types.clone_with_variable_substitutions(typ, &substitutions) + } else { + typ + }; + + let alias_variable = type_to_var_help( + env, + rank, + problems, + abilities_store, + obligation_cache, + arena, + self, + types, + typ, + false, + ); + (alias_variable, kind) + } +} diff --git a/crates/compiler/solve/src/deep_copy.rs b/crates/compiler/solve/src/deep_copy.rs new file mode 100644 index 0000000000..9c9e54b7b7 --- /dev/null +++ b/crates/compiler/solve/src/deep_copy.rs @@ -0,0 +1,375 @@ +use std::ops::ControlFlow; + +use bumpalo::Bump; +use roc_error_macros::internal_error; +use roc_types::{ + subs::{ + self, AliasVariables, Content, Descriptor, FlatType, GetSubsSlice, Mark, OptVariable, Rank, + RecordFields, Subs, SubsSlice, TagExt, TupleElems, UnionLabels, Variable, + }, + types::{RecordField, Uls}, +}; + +use crate::env::SolveEnv; + +// TODO: eventually, we could possibly use the arena in Env instead. +pub(crate) fn deep_copy_var_in( + env: &mut SolveEnv, + rank: Rank, + var: Variable, + arena: &Bump, +) -> Variable { + let mut visited = bumpalo::collections::Vec::with_capacity_in(256, arena); + + let pool = env.pools.get_mut(rank); + + let var = env.subs.get_root_key(var); + match deep_copy_var_decision(env.subs, rank, var) { + ControlFlow::Break(copy) => copy, + ControlFlow::Continue(copy) => { + deep_copy_var_help(env.subs, rank, pool, &mut visited, var, copy); + + // we have tracked all visited variables, and can now traverse them + // in one go (without looking at the UnificationTable) and clear the copy field + for var in visited { + env.subs.set_copy_unchecked(var, OptVariable::NONE); + } + + copy + } + } +} + +#[inline] +fn has_trivial_copy(subs: &Subs, root_var: Variable) -> Option { + let existing_copy = subs.get_copy_unchecked(root_var); + + if let Some(copy) = existing_copy.into_variable() { + Some(copy) + } else if subs.get_rank_unchecked(root_var) != Rank::GENERALIZED { + Some(root_var) + } else { + None + } +} + +#[inline] +fn deep_copy_var_decision( + subs: &mut Subs, + max_rank: Rank, + var: Variable, +) -> ControlFlow { + let var = subs.get_root_key(var); + if let Some(copy) = has_trivial_copy(subs, var) { + ControlFlow::Break(copy) + } else { + let copy_descriptor = Descriptor { + content: Content::Structure(FlatType::EmptyTagUnion), + rank: max_rank, + mark: Mark::NONE, + copy: OptVariable::NONE, + }; + + let copy = subs.fresh(copy_descriptor); + + // Link the original variable to the new variable. This lets us + // avoid making multiple copies of the variable we are instantiating. + // + // Need to do this before recursively copying to avoid looping. + subs.set_mark_unchecked(var, Mark::NONE); + subs.set_copy_unchecked(var, copy.into()); + + ControlFlow::Continue(copy) + } +} + +fn deep_copy_var_help( + subs: &mut Subs, + max_rank: Rank, + pool: &mut Vec, + visited: &mut bumpalo::collections::Vec<'_, Variable>, + initial_source: Variable, + initial_copy: Variable, +) -> Variable { + use roc_types::subs::Content::*; + use roc_types::subs::FlatType::*; + + struct DeepCopyVarWork { + source: Variable, + copy: Variable, + } + + let initial = DeepCopyVarWork { + source: initial_source, + copy: initial_copy, + }; + let mut stack = vec![initial]; + + macro_rules! work { + ($variable:expr) => {{ + let var = subs.get_root_key($variable); + match deep_copy_var_decision(subs, max_rank, var) { + ControlFlow::Break(copy) => copy, + ControlFlow::Continue(copy) => { + stack.push(DeepCopyVarWork { source: var, copy }); + + copy + } + } + }}; + } + + macro_rules! copy_sequence { + ($length:expr, $variables:expr) => {{ + let new_variables = SubsSlice::reserve_into_subs(subs, $length as _); + for (target_index, var_index) in (new_variables.indices()).zip($variables) { + let var = subs[var_index]; + let copy_var = work!(var); + subs.variables[target_index] = copy_var; + } + + new_variables + }}; + } + + macro_rules! copy_union { + ($tags:expr) => {{ + let new_variable_slices = SubsSlice::reserve_variable_slices(subs, $tags.len()); + + let it = (new_variable_slices.indices()).zip($tags.variables()); + for (target_index, index) in it { + let slice = subs[index]; + + let new_variables = copy_sequence!(slice.len(), slice); + subs.variable_slices[target_index] = new_variables; + } + + UnionLabels::from_slices($tags.labels(), new_variable_slices) + }}; + } + + // When generalizing annotations with `Openness` extensions + // we want to promote them to `Any`, so that usages at + // specialized sites can grow unboundedly and are not bound to + // openness-polymorphism. + macro_rules! copy_tag_ext { + ($ext:expr) => { + TagExt::Any(work!($ext.var())) + }; + } + + while let Some(DeepCopyVarWork { source: var, copy }) = stack.pop() { + visited.push(var); + pool.push(copy); + + let content = *subs.get_content_unchecked(var); + + // Now we recursively copy the content of the variable. + // We have already marked the variable as copied, so we + // will not repeat this work or crawl this variable again. + match content { + Structure(flat_type) => { + let new_flat_type = match flat_type { + Apply(symbol, arguments) => { + let new_arguments = copy_sequence!(arguments.len(), arguments); + + Apply(symbol, new_arguments) + } + + Func(arguments, closure_var, ret_var) => { + let new_ret_var = work!(ret_var); + let new_closure_var = work!(closure_var); + + let new_arguments = copy_sequence!(arguments.len(), arguments); + + Func(new_arguments, new_closure_var, new_ret_var) + } + + same @ EmptyRecord | same @ EmptyTuple | same @ EmptyTagUnion => same, + + Record(fields, ext_var) => { + let record_fields = { + let new_variables = + copy_sequence!(fields.len(), fields.iter_variables()); + + // When copying a let-generalized record to a specialized region, rigid + // optionals just become optionals. + let field_types = subs.get_subs_slice(fields.record_fields()); + let has_rigid_optional_field = field_types + .iter() + .any(|f| matches!(f, RecordField::RigidOptional(..))); + + let new_field_types_start = if has_rigid_optional_field { + let field_types = field_types.to_vec(); + let slice = SubsSlice::extend_new( + &mut subs.record_fields, + field_types.into_iter().map(|f| match f { + RecordField::RigidOptional(()) + | RecordField::RigidRequired(()) => internal_error!("Rigid optional/required should be generalized to non-rigid by this point"), + + RecordField::Demanded(_) + | RecordField::Required(_) + | RecordField::Optional(_) => f, + }), + ); + slice.start + } else { + fields.field_types_start + }; + + RecordFields { + length: fields.length, + field_names_start: fields.field_names_start, + variables_start: new_variables.start, + field_types_start: new_field_types_start, + } + }; + + Record(record_fields, work!(ext_var)) + } + + Tuple(elems, ext_var) => { + let tuple_elems = { + let new_variables = copy_sequence!(elems.len(), elems.iter_variables()); + + TupleElems { + length: elems.length, + variables_start: new_variables.start, + elem_index_start: elems.elem_index_start, + } + }; + + Tuple(tuple_elems, work!(ext_var)) + } + + TagUnion(tags, ext_var) => { + let union_tags = copy_union!(tags); + + TagUnion(union_tags, copy_tag_ext!(ext_var)) + } + + FunctionOrTagUnion(tag_name, symbol, ext_var) => { + FunctionOrTagUnion(tag_name, symbol, copy_tag_ext!(ext_var)) + } + + RecursiveTagUnion(rec_var, tags, ext_var) => { + let union_tags = copy_union!(tags); + + RecursiveTagUnion(work!(rec_var), union_tags, copy_tag_ext!(ext_var)) + } + }; + + subs.set_content_unchecked(copy, Structure(new_flat_type)); + } + + FlexVar(_) | FlexAbleVar(_, _) | Error | ErasedLambda => { + subs.set_content_unchecked(copy, content); + } + + RecursionVar { + opt_name, + structure, + } => { + let content = RecursionVar { + opt_name, + structure: work!(structure), + }; + + subs.set_content_unchecked(copy, content); + } + + RigidVar(name) => { + subs.set_content_unchecked(copy, FlexVar(Some(name))); + } + + RigidAbleVar(name, ability) => { + subs.set_content_unchecked(copy, FlexAbleVar(Some(name), ability)); + } + + Alias(symbol, arguments, real_type_var, kind) => { + let new_variables = + copy_sequence!(arguments.all_variables_len, arguments.all_variables()); + + let new_arguments = AliasVariables { + variables_start: new_variables.start, + ..arguments + }; + + let new_real_type_var = work!(real_type_var); + let new_content = Alias(symbol, new_arguments, new_real_type_var, kind); + + subs.set_content_unchecked(copy, new_content); + } + + LambdaSet(subs::LambdaSet { + solved, + recursion_var, + unspecialized, + ambient_function: ambient_function_var, + }) => { + let lambda_set_var = copy; + + let new_solved = copy_union!(solved); + let new_rec_var = recursion_var.map(|v| work!(v)); + let new_unspecialized = SubsSlice::reserve_uls_slice(subs, unspecialized.len()); + + for (new_uls_index, uls_index) in + (new_unspecialized.into_iter()).zip(unspecialized.into_iter()) + { + let Uls(var, sym, region) = subs[uls_index]; + let new_var = work!(var); + + deep_copy_uls_precondition(subs, var, new_var); + + subs[new_uls_index] = Uls(new_var, sym, region); + + subs.uls_of_var.add(new_var, lambda_set_var); + } + + let new_ambient_function_var = work!(ambient_function_var); + debug_assert_ne!( + ambient_function_var, new_ambient_function_var, + "lambda set cloned but its ambient function wasn't?" + ); + + subs.set_content_unchecked( + lambda_set_var, + LambdaSet(subs::LambdaSet { + solved: new_solved, + recursion_var: new_rec_var, + unspecialized: new_unspecialized, + ambient_function: new_ambient_function_var, + }), + ); + } + + RangedNumber(range) => { + let new_content = RangedNumber(range); + + subs.set_content_unchecked(copy, new_content); + } + } + } + + initial_copy +} + +#[inline(always)] +fn deep_copy_uls_precondition(subs: &Subs, original_var: Variable, new_var: Variable) { + if cfg!(debug_assertions) { + let content = subs.get_content_without_compacting(original_var); + + debug_assert!( + matches!( + content, + Content::FlexAbleVar(..) | Content::RigidAbleVar(..) + ), + "var in unspecialized lamba set is not bound to an ability, it is {:?}", + roc_types::subs::SubsFmtContent(content, subs) + ); + debug_assert!( + original_var != new_var, + "unspecialized lamba set var was not instantiated" + ); + } +} diff --git a/crates/compiler/solve/src/env.rs b/crates/compiler/solve/src/env.rs new file mode 100644 index 0000000000..eaedd2ce05 --- /dev/null +++ b/crates/compiler/solve/src/env.rs @@ -0,0 +1,132 @@ +use bumpalo::Bump; +use roc_can::{constraint::Constraints, module::ExposedByModule}; +use roc_checkmate::with_checkmate; +use roc_derive::SharedDerivedModule; +use roc_types::subs::{Content, Descriptor, Mark, OptVariable, Rank, Subs, Variable}; +use roc_unify::Env as UEnv; + +use crate::{FunctionKind, Pools}; + +pub struct DerivedEnv<'a> { + pub derived_module: &'a SharedDerivedModule, + /// Exposed types needed by the derived module. + pub exposed_types: &'a ExposedByModule, +} + +/// Environment necessary for solving and specialization. +pub struct SolveEnv<'a> { + pub arena: &'a Bump, + pub derived_env: &'a DerivedEnv<'a>, + pub subs: &'a mut Subs, + pub pools: &'a mut Pools, + #[cfg(debug_assertions)] + pub checkmate: &'a mut Option, +} + +/// Environment necessary for inference. +pub struct InferenceEnv<'a> { + pub constraints: &'a Constraints, + pub function_kind: FunctionKind, + pub arena: &'a Bump, + pub derived_env: &'a DerivedEnv<'a>, + pub subs: &'a mut Subs, + pub pools: &'a mut Pools, + #[cfg(debug_assertions)] + pub checkmate: Option, +} + +impl<'a> SolveEnv<'a> { + /// Introduce some variables to Pools at the given rank. + /// Also, set each of their ranks in Subs to be the given rank. + pub fn introduce(&mut self, rank: Rank, vars: &[Variable]) { + introduce(self.subs, self.pools, rank, vars); + } + + /// Retrieves an environment for unification. + pub fn uenv(&mut self) -> UEnv { + with_checkmate!({ + on => UEnv::new(self.subs, self.checkmate.as_mut()), + off => UEnv::new(self.subs), + }) + } +} + +impl<'a> InferenceEnv<'a> { + #[inline(always)] + pub fn register(&mut self, rank: Rank, content: Content) -> Variable { + let descriptor = Descriptor { + content, + rank, + mark: Mark::NONE, + copy: OptVariable::NONE, + }; + + let var = self.subs.fresh(descriptor); + + self.pools.get_mut(rank).push(var); + + var + } + + /// Introduce some variables to Pools at the given rank. + /// Also, set each of their ranks in Subs to be the given rank. + pub fn introduce(&mut self, rank: Rank, vars: &[Variable]) { + introduce(self.subs, self.pools, rank, vars); + } + + #[inline(always)] + pub fn register_existing_var(&mut self, var: Variable) { + self.pools.get_mut(self.subs.get_rank(var)).push(var); + } + + pub fn register_with_known_var( + &mut self, + var: Variable, + rank: Rank, + content: Content, + ) -> Variable { + let descriptor = Descriptor { + content, + rank, + mark: Mark::NONE, + copy: OptVariable::NONE, + }; + + self.subs.set(var, descriptor); + + self.pools.get_mut(rank).push(var); + + var + } + + /// Retrieves an environment for unification. + pub fn uenv(&mut self) -> UEnv { + with_checkmate!({ + on => UEnv::new(self.subs, self.checkmate.as_mut()), + off => UEnv::new(self.subs), + }) + } + + pub fn as_solve_env(&mut self) -> SolveEnv { + SolveEnv { + arena: self.arena, + derived_env: self.derived_env, + subs: self.subs, + pools: self.pools, + #[cfg(debug_assertions)] + checkmate: &mut self.checkmate, + } + } +} + +/// Introduce some variables to Pools at the given rank. +/// Also, set each of their ranks in Subs to be the given rank. +fn introduce(subs: &mut Subs, pools: &mut Pools, rank: Rank, vars: &[Variable]) { + let pool: &mut Vec = pools.get_mut(rank); + + for &var in vars.iter() { + subs.set_rank(var, rank); + } + + pool.extend(vars); +} diff --git a/crates/compiler/solve/src/kinds.rs b/crates/compiler/solve/src/kinds.rs new file mode 100644 index 0000000000..6ad1a7a34c --- /dev/null +++ b/crates/compiler/solve/src/kinds.rs @@ -0,0 +1,8 @@ +/// How function kinds should be represented in the type system. +#[derive(Debug, Clone, Copy)] +pub enum FunctionKind { + /// Function values are solved to lambda sets; lambda sets are the kind. + LambdaSet, + /// Function values are erased, no kind is introduced. + Erased, +} diff --git a/crates/compiler/solve/src/lib.rs b/crates/compiler/solve/src/lib.rs new file mode 100644 index 0000000000..b763fdf51c --- /dev/null +++ b/crates/compiler/solve/src/lib.rs @@ -0,0 +1,25 @@ +//! The entry point of Roc's [type inference](https://en.wikipedia.org/wiki/Type_inference) +//! system. Implements type inference and specialization of abilities. +#![warn(clippy::dbg_macro)] +// See github.com/roc-lang/roc/issues/800 for discussion of the large_enum_variant check. +#![allow(clippy::large_enum_variant)] +// TODO to be removed +#![allow(clippy::too_many_arguments)] + +pub mod ability; +pub mod module; +pub mod solve; +pub mod specialize; + +mod aliases; +mod deep_copy; +mod env; +mod kinds; +mod pools; +mod to_var; + +pub use aliases::Aliases; +pub use env::{DerivedEnv, InferenceEnv, SolveEnv}; +pub use kinds::FunctionKind; +pub use pools::Pools; +pub use to_var::type_to_var; diff --git a/crates/compiler/solve/src/module.rs b/crates/compiler/solve/src/module.rs new file mode 100644 index 0000000000..77b4bf2aa6 --- /dev/null +++ b/crates/compiler/solve/src/module.rs @@ -0,0 +1,248 @@ +use crate::solve::RunSolveOutput; +use crate::FunctionKind; +use crate::{aliases::Aliases, solve}; +use roc_can::abilities::{AbilitiesStore, ResolvedImpl}; +use roc_can::constraint::{Constraint, Constraints}; +use roc_can::expr::PendingDerives; +use roc_can::module::{ExposedByModule, ResolvedImplementations, RigidVariables}; +use roc_collections::all::MutMap; +use roc_collections::VecMap; +use roc_derive::SharedDerivedModule; +use roc_error_macros::internal_error; +use roc_module::symbol::{ModuleId, Symbol}; +use roc_solve_problem::TypeError; +use roc_types::subs::{Content, ExposedTypesStorageSubs, FlatType, StorageSubs, Subs, Variable}; +use roc_types::types::{Alias, MemberImpl, Types}; + +/// A marker that a given Subs has been solved. +/// The only way to obtain a Solved is by running the solver on it. +#[derive(Clone, Debug)] +pub struct Solved(pub T); + +impl Solved { + pub fn inner(&self) -> &'_ T { + &self.0 + } + + pub fn inner_mut(&mut self) -> &'_ mut T { + &mut self.0 + } + + pub fn into_inner(self) -> T { + self.0 + } +} + +#[derive(Debug)] +pub struct SolvedModule { + pub problems: Vec, + + /// all aliases and their definitions. this has to include non-exposed aliases + /// because exposed aliases can depend on non-exposed ones) + pub aliases: MutMap, + + /// Used when the goal phase is TypeChecking, and + /// to create the types for HostExposed. This + /// has some overlap with the StorageSubs fields, + /// so maybe we can get rid of this at some point + /// + /// Contains both variables of symbols that are explicitly exposed by the header, + /// and the variables of any solved ability specializations we have. + pub exposed_vars_by_symbol: Vec<(Symbol, Variable)>, + + /// Used when importing this module into another module + pub solved_implementations: ResolvedImplementations, + pub exposed_types: ExposedTypesStorageSubs, +} + +pub struct SolveConfig<'a> { + /// The module we are solving. + pub home: ModuleId, + pub constraints: &'a Constraints, + pub root_constraint: Constraint, + /// All types introduced in the module. Canonicalized, but not necessarily yet associated with + /// a variable substitution. + pub types: Types, + /// How functions should be kinded. + pub function_kind: FunctionKind, + /// Table of types introduced in this module that claim to derive an ability implementation. + /// Due for checking and instantiation after the solver runs over the module. + pub pending_derives: PendingDerives, + /// Types exposed by other modules. + /// Available for late instantiation of imports, lambda sets, or ability types. + pub exposed_by_module: &'a ExposedByModule, + /// The unique `#Derived` module, used to generate and retrieve derived ability + /// implementations. + /// Needed during solving to resolve lambda sets from derived implementations that escape into + /// the user module. + pub derived_module: SharedDerivedModule, + + #[cfg(debug_assertions)] + /// The checkmate collector for this module. + pub checkmate: Option, +} + +pub struct SolveOutput { + pub subs: Solved, + pub scope: solve::Scope, + pub errors: Vec, + pub resolved_abilities_store: AbilitiesStore, + + #[cfg(debug_assertions)] + pub checkmate: Option, +} + +pub fn run_solve( + config: SolveConfig<'_>, + rigid_variables: RigidVariables, + mut subs: Subs, + mut aliases: Aliases, + mut abilities_store: AbilitiesStore, +) -> SolveOutput { + for (var, name) in rigid_variables.named { + subs.rigid_var(var, name); + } + + for (var, (name, abilities)) in rigid_variables.able { + subs.rigid_able_var(var, name, abilities); + } + + for var in rigid_variables.wildcards { + subs.rigid_var(var, "*".into()); + } + + // Now that the module is parsed, canonicalized, and constrained, + // we need to type check it. + let mut problems = Vec::new(); + + // Run the solver to populate Subs. + let RunSolveOutput { + solved, + scope, + #[cfg(debug_assertions)] + checkmate, + } = solve::run( + config, + &mut problems, + subs, + &mut aliases, + &mut abilities_store, + ); + + SolveOutput { + subs: solved, + scope, + errors: problems, + resolved_abilities_store: abilities_store, + #[cfg(debug_assertions)] + checkmate, + } +} + +/// Copies exposed types and all ability specializations, which may be implicitly exposed. +pub fn exposed_types_storage_subs( + home: ModuleId, + solved_subs: &mut Solved, + exposed_vars_by_symbol: &[(Symbol, Variable)], + solved_implementations: &ResolvedImplementations, + abilities_store: &AbilitiesStore, +) -> ExposedTypesStorageSubs { + let subs = solved_subs.inner_mut(); + let mut storage_subs = StorageSubs::new(Subs::new()); + let mut stored_vars_by_symbol = VecMap::with_capacity(exposed_vars_by_symbol.len()); + + for (symbol, var) in exposed_vars_by_symbol.iter() { + let new_var = storage_subs.import_variable_from(subs, *var).variable; + stored_vars_by_symbol.insert(*symbol, new_var); + } + + let mut stored_specialization_lambda_set_vars = + VecMap::with_capacity(solved_implementations.len()); + + for (_, member_impl) in solved_implementations.iter() { + match member_impl { + ResolvedImpl::Impl(member_specialization) => { + // Export all the lambda sets and their ambient functions. + for (_, &lset_var) in member_specialization.specialization_lambda_sets.iter() { + let specialization_lset_ambient_function_var = + subs.get_lambda_set(lset_var).ambient_function; + + // Import the ambient function of this specialization lambda set; that will import the + // lambda set as well. The ambient function is needed for the lambda set compaction + // algorithm. + let imported_lset_ambient_function_var = storage_subs + .import_variable_from(subs, specialization_lset_ambient_function_var) + .variable; + + let imported_lset_var = match storage_subs + .as_inner() + .get_content_without_compacting(imported_lset_ambient_function_var) + { + Content::Structure(FlatType::Func(_, lambda_set_var, _)) => *lambda_set_var, + content => internal_error!( + "ambient lambda set function import is not a function, found: {:?}", + roc_types::subs::SubsFmtContent(content, storage_subs.as_inner()) + ), + }; + stored_specialization_lambda_set_vars.insert(lset_var, imported_lset_var); + } + } + ResolvedImpl::Error => { + // nothing to do + } + } + } + + // Store the regioned lambda sets of the ability members defined in this module. + let stored_ability_member_vars = abilities_store + .root_ability_members() + .iter() + .filter_map(|(member, data)| { + if member.module_id() == home { + let var = data.signature_var(); + let imported_var = storage_subs.import_variable_from(subs, var).variable; + Some((var, imported_var)) + } else { + None + } + }) + .collect(); + + ExposedTypesStorageSubs { + storage_subs, + stored_vars_by_symbol, + stored_specialization_lambda_set_vars, + stored_ability_member_vars, + } +} + +/// Extracts the ability member implementations owned by a solved module. +pub fn extract_module_owned_implementations( + module_id: ModuleId, + abilities_store: &AbilitiesStore, +) -> ResolvedImplementations { + abilities_store + .iter_declared_implementations() + .filter_map(|(impl_key, member_impl)| { + // This module solved this specialization if either the member or the type comes from the + // module. + if impl_key.ability_member.module_id() != module_id + && impl_key.opaque.module_id() != module_id + { + return None; + } + + let resolved_impl = match member_impl { + MemberImpl::Impl(impl_symbol) => { + let specialization = abilities_store.specialization_info(*impl_symbol).expect( + "declared implementations should be resolved conclusively after solving", + ); + ResolvedImpl::Impl(specialization.clone()) + } + MemberImpl::Error => ResolvedImpl::Error, + }; + + Some((impl_key, resolved_impl)) + }) + .collect() +} diff --git a/crates/compiler/solve/src/pools.rs b/crates/compiler/solve/src/pools.rs new file mode 100644 index 0000000000..877ebbec79 --- /dev/null +++ b/crates/compiler/solve/src/pools.rs @@ -0,0 +1,59 @@ +use roc_types::subs::{Rank, Variable}; + +const DEFAULT_POOLS: usize = 8; + +#[derive(Clone, Debug)] +pub struct Pools(Vec>); + +impl Default for Pools { + fn default() -> Self { + Pools::new(DEFAULT_POOLS) + } +} + +impl Pools { + pub fn new(num_pools: usize) -> Self { + Pools(vec![Vec::new(); num_pools]) + } + + pub fn len(&self) -> usize { + self.0.len() + } + + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + pub fn get_mut(&mut self, rank: Rank) -> &mut Vec { + match self.0.get_mut(rank.into_usize()) { + Some(reference) => reference, + None => panic!("Compiler bug: could not find pool at rank {rank}"), + } + } + + pub fn get(&self, rank: Rank) -> &Vec { + match self.0.get(rank.into_usize()) { + Some(reference) => reference, + None => panic!("Compiler bug: could not find pool at rank {rank}"), + } + } + + pub fn iter(&self) -> std::slice::Iter<'_, Vec> { + self.0.iter() + } + + pub fn split_last(mut self) -> (Vec, Vec>) { + let last = self + .0 + .pop() + .unwrap_or_else(|| panic!("Attempted to split_last() on non-empty Pools")); + + (last, self.0) + } + + pub fn extend_to(&mut self, n: usize) { + for _ in self.len()..n { + self.0.push(Vec::new()); + } + } +} diff --git a/crates/compiler/solve/src/solve.rs b/crates/compiler/solve/src/solve.rs new file mode 100644 index 0000000000..95dafa4851 --- /dev/null +++ b/crates/compiler/solve/src/solve.rs @@ -0,0 +1,2337 @@ +use crate::ability::{ + resolve_ability_specialization, type_implementing_specialization, AbilityImplError, + CheckedDerives, ObligationCache, PendingDerivesTable, Resolved, +}; +use crate::deep_copy::deep_copy_var_in; +use crate::env::{DerivedEnv, InferenceEnv}; +use crate::module::{SolveConfig, Solved}; +use crate::pools::Pools; +use crate::specialize::{ + compact_lambda_sets_of_vars, AwaitingSpecializations, CompactionResult, SolvePhase, +}; +use crate::to_var::{either_type_index_to_var, type_to_var}; +use crate::Aliases; +use bumpalo::Bump; +use roc_can::abilities::{AbilitiesStore, MemberSpecializationInfo}; +use roc_can::constraint::Constraint::{self, *}; +use roc_can::constraint::{Cycle, LetConstraint, OpportunisticResolve}; +use roc_can::expected::{Expected, PExpected}; +use roc_debug_flags::dbg_do; +#[cfg(debug_assertions)] +use roc_debug_flags::ROC_VERIFY_RIGID_LET_GENERALIZED; +use roc_error_macros::internal_error; +use roc_module::symbol::Symbol; +use roc_problem::can::CycleEntry; +use roc_region::all::Loc; +use roc_solve_problem::TypeError; +use roc_solve_schema::UnificationMode; +use roc_types::subs::{ + self, Content, FlatType, GetSubsSlice, Mark, OptVariable, Rank, Subs, TagExt, UlsOfVar, + Variable, +}; +use roc_types::types::{Category, Polarity, Reason, RecordField, Type, TypeExtension, Types, Uls}; +use roc_unify::unify::{ + unify, unify_introduced_ability_specialization, Obligated, SpecializationLsetCollector, + Unified::*, +}; + +mod scope; +pub use scope::Scope; + +// Type checking system adapted from Elm by Evan Czaplicki, BSD-3-Clause Licensed +// https://github.com/elm/compiler +// Thank you, Evan! + +// A lot of energy was put into making type inference fast. That means it's pretty intimidating. +// +// Fundamentally, type inference assigns very general types based on syntax, and then tries to +// make all the pieces fit together. For instance when writing +// +// > f x +// +// We know that `f` is a function, and thus must have some type `a -> b`. +// `x` is just a variable, that gets the type `c` +// +// Next comes constraint generation. For `f x` to be well-typed, +// it must be the case that `c = a`, So a constraint `Eq(c, a)` is generated. +// But `Eq` is a bit special: `c` does not need to equal `a` exactly, but they need to be equivalent. +// This allows for instance the use of aliases. `c` could be an alias, and so looks different from +// `a`, but they still represent the same type. +// +// Then we get to solving, which happens in this file. +// +// When we hit an `Eq` constraint, then we check whether the two involved types are in fact +// equivalent using unification, and when they are, we can substitute one for the other. +// +// When all constraints are processed, and no unification errors have occurred, then the program +// is type-correct. Otherwise the errors are reported. +// +// Now, coming back to efficiency, this type checker uses *ranks* to optimize +// The rank tracks the number of let-bindings a variable is "under". Top-level definitions +// have rank 1. A let in a top-level definition gets rank 2, and so on. +// +// This has to do with generalization of type variables. This is described here +// +// http://okmij.org/ftp/ML/generalization.html#levels +// +// The problem is that when doing inference naively, this program would fail to typecheck +// +// f = +// id = \x -> x +// +// { a: id 1, b: id "foo" } +// +// Because `id` is applied to an integer, the type `Int -> Int` is inferred, which then gives a +// type error for `id "foo"`. +// +// Thus instead the inferred type for `id` is generalized (see the `generalize` function) to `a -> a`. +// Ranks are used to limit the number of type variables considered for generalization. Only those inside +// of the let (so those used in inferring the type of `\x -> x`) are considered. + +#[derive(Clone)] +struct State { + scope: Scope, + mark: Mark, +} + +pub struct RunSolveOutput { + pub solved: Solved, + pub scope: Scope, + + #[cfg(debug_assertions)] + pub checkmate: Option, +} + +pub fn run( + config: SolveConfig, + problems: &mut Vec, + subs: Subs, + aliases: &mut Aliases, + abilities_store: &mut AbilitiesStore, +) -> RunSolveOutput { + run_help(config, problems, subs, aliases, abilities_store) +} + +fn run_help( + config: SolveConfig, + problems: &mut Vec, + mut owned_subs: Subs, + aliases: &mut Aliases, + abilities_store: &mut AbilitiesStore, +) -> RunSolveOutput { + let subs = &mut owned_subs; + let SolveConfig { + home: _, + constraints, + root_constraint, + mut types, + pending_derives, + exposed_by_module, + derived_module, + function_kind, + .. + } = config; + + let mut pools = Pools::default(); + + let state = State { + scope: Scope::default(), + mark: Mark::NONE.next(), + }; + let rank = Rank::toplevel(); + let arena = Bump::new(); + + let mut obligation_cache = ObligationCache::default(); + let mut awaiting_specializations = AwaitingSpecializations::default(); + + let derived_env = DerivedEnv { + derived_module: &derived_module, + exposed_types: exposed_by_module, + }; + + let mut env = InferenceEnv { + arena: &arena, + constraints, + function_kind, + derived_env: &derived_env, + subs, + pools: &mut pools, + #[cfg(debug_assertions)] + checkmate: config.checkmate, + }; + + let pending_derives = PendingDerivesTable::new( + &mut env, + &mut types, + aliases, + pending_derives, + problems, + abilities_store, + &mut obligation_cache, + ); + let CheckedDerives { + legal_derives: _, + problems: derives_problems, + } = obligation_cache.check_derives(env.subs, abilities_store, pending_derives); + problems.extend(derives_problems); + + let state = solve( + &mut env, + types, + state, + rank, + problems, + aliases, + &root_constraint, + abilities_store, + &mut obligation_cache, + &mut awaiting_specializations, + ); + + RunSolveOutput { + scope: state.scope, + #[cfg(debug_assertions)] + checkmate: env.checkmate, + solved: Solved(owned_subs), + } +} + +#[derive(Debug)] +enum Work<'a> { + Constraint { + scope: &'a Scope, + rank: Rank, + constraint: &'a Constraint, + }, + CheckForInfiniteTypes(LocalDefVarsVec<(Symbol, Loc)>), + /// The ret_con part of a let constraint that does NOT introduces rigid and/or flex variables + LetConNoVariables { + scope: &'a Scope, + rank: Rank, + let_con: &'a LetConstraint, + + /// The variables used to store imported types in the Subs. + /// The `Contents` are copied from the source module, but to + /// mimic `type_to_var`, we must add these variables to `Pools` + /// at the correct rank + pool_variables: &'a [Variable], + }, + /// The ret_con part of a let constraint that introduces rigid and/or flex variables + /// + /// These introduced variables must be generalized, hence this variant + /// is more complex than `LetConNoVariables`. + LetConIntroducesVariables { + scope: &'a Scope, + rank: Rank, + let_con: &'a LetConstraint, + + /// The variables used to store imported types in the Subs. + /// The `Contents` are copied from the source module, but to + /// mimic `type_to_var`, we must add these variables to `Pools` + /// at the correct rank + pool_variables: &'a [Variable], + }, +} + +fn solve( + env: &mut InferenceEnv, + mut can_types: Types, + mut state: State, + rank: Rank, + problems: &mut Vec, + aliases: &mut Aliases, + constraint: &Constraint, + abilities_store: &mut AbilitiesStore, + obligation_cache: &mut ObligationCache, + awaiting_specializations: &mut AwaitingSpecializations, +) -> State { + let initial = Work::Constraint { + scope: &Scope::default(), + rank, + constraint, + }; + + let mut stack = vec![initial]; + + while let Some(work_item) = stack.pop() { + let (scope, rank, constraint) = match work_item { + Work::Constraint { + scope, + rank, + constraint, + } => { + // the default case; actually solve this constraint + (scope, rank, constraint) + } + Work::CheckForInfiniteTypes(def_vars) => { + // after a LetCon, we must check if any of the variables that we introduced + // loop back to themselves after solving the ret_constraint + for (symbol, loc_var) in def_vars.iter() { + check_for_infinite_type(env, problems, *symbol, *loc_var); + } + + continue; + } + Work::LetConNoVariables { + scope, + rank, + let_con, + pool_variables, + } => { + // NOTE be extremely careful with shadowing here + let offset = let_con.defs_and_ret_constraint.index(); + let ret_constraint = &env.constraints.constraints[offset + 1]; + + // Add a variable for each def to new_vars_by_env. + let local_def_vars = LocalDefVarsVec::from_def_types( + env, + rank, + problems, + abilities_store, + obligation_cache, + &mut can_types, + aliases, + let_con.def_types, + ); + + env.pools.get_mut(rank).extend(pool_variables); + + let mut new_scope = scope.clone(); + for (symbol, loc_var) in local_def_vars.iter() { + check_ability_specialization( + env, + rank, + abilities_store, + obligation_cache, + awaiting_specializations, + problems, + *symbol, + *loc_var, + ); + + new_scope.insert_symbol_var_if_vacant(*symbol, loc_var.value); + } + + stack.push(Work::Constraint { + scope: env.arena.alloc(new_scope), + rank, + constraint: ret_constraint, + }); + // Check for infinite types first + stack.push(Work::CheckForInfiniteTypes(local_def_vars)); + + continue; + } + Work::LetConIntroducesVariables { + scope, + rank, + let_con, + pool_variables, + } => { + // NOTE be extremely careful with shadowing here + let offset = let_con.defs_and_ret_constraint.index(); + let ret_constraint = &env.constraints.constraints[offset + 1]; + + let mark = state.mark; + let saved_scope = state.scope; + + let young_mark = mark; + let visit_mark = young_mark.next(); + let final_mark = visit_mark.next(); + + let intro_rank = if let_con.generalizable.0 { + rank.next() + } else { + rank + }; + + // Add a variable for each def to local_def_vars. + let local_def_vars = LocalDefVarsVec::from_def_types( + env, + intro_rank, + problems, + abilities_store, + obligation_cache, + &mut can_types, + aliases, + let_con.def_types, + ); + + // If the let-binding can be generalized, introduce all variables at the next rank; + // those that persist at the next rank after rank-adjustment will be generalized. + // + // Otherwise, introduce all variables at the current rank; since none of them will + // end up at the next rank, none will be generalized. + if let_con.generalizable.0 { + env.pools.get_mut(rank.next()).extend(pool_variables); + } else { + env.pools.get_mut(rank).extend(pool_variables); + } + + debug_assert_eq!( + // Check that no variable ended up in a higher rank than the next rank.. that + // would mean we generalized one level more than we need to! + { + let offenders = env + .pools + .get(rank.next()) + .iter() + .filter(|var| { + env.subs.get_rank(**var).into_usize() > rank.next().into_usize() + }) + .collect::>(); + + let result = offenders.len(); + + if result > 0 { + eprintln!("subs = {:?}", &env.subs); + eprintln!("offenders = {:?}", &offenders); + eprintln!("let_con.def_types = {:?}", &let_con.def_types); + } + + result + }, + 0 + ); + + // If the let-binding is eligible for generalization, it was solved at the + // next rank. The variables introduced in the let-binding that are still at + // that rank (intuitively, they did not "escape" into the lower level + // before or after the let-binding) now get to be generalized. + generalize(env, young_mark, visit_mark, rank.next()); + debug_assert!(env.pools.get(rank.next()).is_empty(), "variables left over in let-binding scope, but they should all be in a lower scope or generalized now"); + + // check that things went well + dbg_do!(ROC_VERIFY_RIGID_LET_GENERALIZED, { + let rigid_vars = &env.constraints.variables[let_con.rigid_vars.indices()]; + + // NOTE the `subs.redundant` check does not come from elm. + // It's unclear whether this is a bug with our implementation + // (something is redundant that shouldn't be) + // or that it just never came up in elm. + let mut it = rigid_vars + .iter() + .filter(|&var| { + !env.subs.redundant(*var) + && env.subs.get_rank(*var) != Rank::GENERALIZED + }) + .peekable(); + + if it.peek().is_some() { + let failing: Vec<_> = it.collect(); + println!("Rigids {:?}", &rigid_vars); + println!("Failing {failing:?}"); + debug_assert!(false); + } + }); + + let mut new_scope = scope.clone(); + for (symbol, loc_var) in local_def_vars.iter() { + check_ability_specialization( + env, + rank, + abilities_store, + obligation_cache, + awaiting_specializations, + problems, + *symbol, + *loc_var, + ); + + new_scope.insert_symbol_var_if_vacant(*symbol, loc_var.value); + } + + // Note that this vars_by_symbol is the one returned by the + // previous call to solve() + let state_for_ret_con = State { + scope: saved_scope, + mark: final_mark, + }; + + // Now solve the body, using the new vars_by_symbol which includes + // the assignments' name-to-variable mappings. + stack.push(Work::Constraint { + scope: env.arena.alloc(new_scope), + rank, + constraint: ret_constraint, + }); + // Check for infinite types first + stack.push(Work::CheckForInfiniteTypes(local_def_vars)); + + state = state_for_ret_con; + + continue; + } + }; + + state = match constraint { + True => state, + SaveTheEnvironment => { + let mut copy = state; + + copy.scope = scope.clone(); + + copy + } + Eq(roc_can::constraint::Eq(type_index, expectation_index, category_index, region)) => { + let category = &env.constraints.categories[category_index.index()]; + + let actual = either_type_index_to_var( + env, + rank, + problems, + abilities_store, + obligation_cache, + &mut can_types, + aliases, + *type_index, + ); + + let expectation = &env.constraints.expectations[expectation_index.index()]; + let expected = either_type_index_to_var( + env, + rank, + problems, + abilities_store, + obligation_cache, + &mut can_types, + aliases, + *expectation.get_type_ref(), + ); + + match unify( + &mut env.uenv(), + actual, + expected, + UnificationMode::EQ, + Polarity::OF_VALUE, + ) { + Success { + vars, + must_implement_ability, + lambda_sets_to_specialize, + extra_metadata: _, + } => { + env.introduce(rank, &vars); + + if !must_implement_ability.is_empty() { + let new_problems = obligation_cache.check_obligations( + env.subs, + abilities_store, + must_implement_ability, + AbilityImplError::BadExpr(*region, category.clone(), actual), + ); + problems.extend(new_problems); + } + compact_lambdas_and_check_obligations( + env, + problems, + abilities_store, + obligation_cache, + awaiting_specializations, + lambda_sets_to_specialize, + ); + + state + } + Failure(vars, actual_type, expected_type, _bad_impls) => { + env.introduce(rank, &vars); + + let problem = TypeError::BadExpr( + *region, + category.clone(), + actual_type, + expectation.replace_ref(expected_type), + ); + + problems.push(problem); + + state + } + } + } + Store(source_index, target, _filename, _linenr) => { + // a special version of Eq that is used to store types in the AST. + // IT DOES NOT REPORT ERRORS! + let actual = either_type_index_to_var( + env, + rank, + &mut vec![], // don't report any extra errors + abilities_store, + obligation_cache, + &mut can_types, + aliases, + *source_index, + ); + + let actual_desc = env.subs.get(actual); + env.subs.union(*target, actual, actual_desc); + state + } + Lookup(symbol, expectation_index, region) => { + match scope.get_var_by_symbol(symbol) { + Some(var) => { + // Deep copy the vars associated with this symbol before unifying them. + // Otherwise, suppose we have this: + // + // identity = \a -> a + // + // x = identity 5 + // + // When we call (identity 5), it's important that we not unify + // on identity's original vars. If we do, the type of `identity` will be + // mutated to be `Int -> Int` instead of `a -> `, which would be incorrect; + // the type of `identity` is more general than that! + // + // Instead, we want to unify on a *copy* of its vars. If the copy unifies + // successfully (in this case, to `Int -> Int`), we can use that to + // infer the type of this lookup (in this case, `Int`) without ever + // having mutated the original. + // + // If this Lookup is targeting a value in another module, + // then we copy from that module's Subs into our own. If the value + // is being looked up in this module, then we use our Subs as both + // the source and destination. + let actual = { + let mut solve_env = env.as_solve_env(); + let solve_env = &mut solve_env; + deep_copy_var_in(solve_env, rank, var, solve_env.arena) + }; + let expectation = &env.constraints.expectations[expectation_index.index()]; + + let expected = either_type_index_to_var( + env, + rank, + problems, + abilities_store, + obligation_cache, + &mut can_types, + aliases, + *expectation.get_type_ref(), + ); + + match unify( + &mut env.uenv(), + actual, + expected, + UnificationMode::EQ, + Polarity::OF_VALUE, + ) { + Success { + vars, + must_implement_ability, + lambda_sets_to_specialize, + extra_metadata: _, + } => { + env.introduce(rank, &vars); + + if !must_implement_ability.is_empty() { + let new_problems = obligation_cache.check_obligations( + env.subs, + abilities_store, + must_implement_ability, + AbilityImplError::BadExpr( + *region, + Category::Lookup(*symbol), + actual, + ), + ); + problems.extend(new_problems); + } + compact_lambdas_and_check_obligations( + env, + problems, + abilities_store, + obligation_cache, + awaiting_specializations, + lambda_sets_to_specialize, + ); + + state + } + + Failure(vars, actual_type, expected_type, _bad_impls) => { + env.introduce(rank, &vars); + + let problem = TypeError::BadExpr( + *region, + Category::Lookup(*symbol), + actual_type, + expectation.replace_ref(expected_type), + ); + + problems.push(problem); + + state + } + } + } + None => { + problems.push(TypeError::UnexposedLookup(*region, *symbol)); + + state + } + } + } + And(slice) => { + let it = env.constraints.constraints[slice.indices()].iter().rev(); + for sub_constraint in it { + stack.push(Work::Constraint { + scope, + rank, + constraint: sub_constraint, + }) + } + + state + } + Pattern(type_index, expectation_index, category_index, region) + | PatternPresence(type_index, expectation_index, category_index, region) => { + let category = &env.constraints.pattern_categories[category_index.index()]; + + let actual = either_type_index_to_var( + env, + rank, + problems, + abilities_store, + obligation_cache, + &mut can_types, + aliases, + *type_index, + ); + + let expectation = &env.constraints.pattern_expectations[expectation_index.index()]; + let expected = either_type_index_to_var( + env, + rank, + problems, + abilities_store, + obligation_cache, + &mut can_types, + aliases, + *expectation.get_type_ref(), + ); + + let mode = match constraint { + PatternPresence(..) => UnificationMode::PRESENT, + _ => UnificationMode::EQ, + }; + + match unify( + &mut env.uenv(), + actual, + expected, + mode, + Polarity::OF_PATTERN, + ) { + Success { + vars, + must_implement_ability, + lambda_sets_to_specialize, + extra_metadata: _, + } => { + env.introduce(rank, &vars); + + if !must_implement_ability.is_empty() { + let new_problems = obligation_cache.check_obligations( + env.subs, + abilities_store, + must_implement_ability, + AbilityImplError::BadPattern(*region, category.clone(), actual), + ); + problems.extend(new_problems); + } + compact_lambdas_and_check_obligations( + env, + problems, + abilities_store, + obligation_cache, + awaiting_specializations, + lambda_sets_to_specialize, + ); + + state + } + Failure(vars, actual_type, expected_type, _bad_impls) => { + env.introduce(rank, &vars); + + let problem = TypeError::BadPattern( + *region, + category.clone(), + actual_type, + expectation.replace_ref(expected_type), + ); + + problems.push(problem); + + state + } + } + } + Let(index, pool_slice) => { + let let_con = &env.constraints.let_constraints[index.index()]; + + let offset = let_con.defs_and_ret_constraint.index(); + let defs_constraint = &env.constraints.constraints[offset]; + let ret_constraint = &env.constraints.constraints[offset + 1]; + + let flex_vars = &env.constraints.variables[let_con.flex_vars.indices()]; + let rigid_vars = &env.constraints.variables[let_con.rigid_vars.indices()]; + + let pool_variables = &env.constraints.variables[pool_slice.indices()]; + + if matches!(&ret_constraint, True) && let_con.rigid_vars.is_empty() { + debug_assert!(pool_variables.is_empty()); + + env.introduce(rank, flex_vars); + + // If the return expression is guaranteed to solve, + // solve the assignments themselves and move on. + stack.push(Work::Constraint { + scope, + rank, + constraint: defs_constraint, + }); + + state + } else if let_con.rigid_vars.is_empty() && let_con.flex_vars.is_empty() { + // items are popped from the stack in reverse order. That means that we'll + // first solve then defs_constraint, and then (eventually) the ret_constraint. + // + // Note that the LetConSimple gets the current env and rank, + // and not the env/rank from after solving the defs_constraint + stack.push(Work::LetConNoVariables { + scope, + rank, + let_con, + pool_variables, + }); + stack.push(Work::Constraint { + scope, + rank, + constraint: defs_constraint, + }); + + state + } else { + // If the let-binding is generalizable, work at the next rank (which will be + // the rank at which introduced variables will become generalized, if they end up + // staying there); otherwise, stay at the current level. + let binding_rank = if let_con.generalizable.0 { + rank.next() + } else { + rank + }; + + // determine the next pool + if binding_rank.into_usize() < env.pools.len() { + // Nothing to do, we already accounted for the next rank, no need to + // adjust the pools + } else { + // we should be off by one at this point + debug_assert_eq!(binding_rank.into_usize(), 1 + env.pools.len()); + env.pools.extend_to(binding_rank.into_usize()); + } + + let pool: &mut Vec = env.pools.get_mut(binding_rank); + + // Introduce the variables of this binding, and extend the pool at our binding + // rank. + for &var in rigid_vars.iter().chain(flex_vars.iter()) { + env.subs.set_rank(var, binding_rank); + } + pool.reserve(rigid_vars.len() + flex_vars.len()); + pool.extend(rigid_vars.iter()); + pool.extend(flex_vars.iter()); + + // Now, run our binding constraint, generalize, then solve the rest of the + // program. + // + // Items are popped from the stack in reverse order. That means that we'll + // first solve the defs_constraint, and then (eventually) the ret_constraint. + // + // NB: LetCon gets the current scope's env and rank, not the env/rank from after solving the defs_constraint. + // That's because the defs constraints will be solved in next_rank if it is eligible for generalization. + // The LetCon will then generalize variables that are at a higher rank than the rank of the current scope. + stack.push(Work::LetConIntroducesVariables { + scope, + rank, + let_con, + pool_variables, + }); + stack.push(Work::Constraint { + scope, + rank: binding_rank, + constraint: defs_constraint, + }); + + state + } + } + IsOpenType(type_index) => { + let actual = either_type_index_to_var( + env, + rank, + problems, + abilities_store, + obligation_cache, + &mut can_types, + aliases, + *type_index, + ); + + open_tag_union(env, actual); + + state + } + IncludesTag(index) => { + let includes_tag = &env.constraints.includes_tags[index.index()]; + + let roc_can::constraint::IncludesTag { + type_index, + tag_name, + types, + pattern_category, + region, + } = includes_tag; + + let pattern_category = + &env.constraints.pattern_categories[pattern_category.index()]; + + let actual = either_type_index_to_var( + env, + rank, + problems, + abilities_store, + obligation_cache, + &mut can_types, + aliases, + *type_index, + ); + + let payload_types = env.constraints.variables[types.indices()] + .iter() + .map(|v| Type::Variable(*v)) + .collect(); + + let tag_ty = can_types.from_old_type(&Type::TagUnion( + vec![(tag_name.clone(), payload_types)], + TypeExtension::Closed, + )); + let includes = type_to_var( + env, + rank, + problems, + abilities_store, + obligation_cache, + &mut can_types, + aliases, + tag_ty, + ); + + match unify( + &mut env.uenv(), + actual, + includes, + UnificationMode::PRESENT, + Polarity::OF_PATTERN, + ) { + Success { + vars, + must_implement_ability, + lambda_sets_to_specialize, + extra_metadata: _, + } => { + env.introduce(rank, &vars); + + if !must_implement_ability.is_empty() { + let new_problems = obligation_cache.check_obligations( + env.subs, + abilities_store, + must_implement_ability, + AbilityImplError::BadPattern( + *region, + pattern_category.clone(), + actual, + ), + ); + problems.extend(new_problems); + } + compact_lambdas_and_check_obligations( + env, + problems, + abilities_store, + obligation_cache, + awaiting_specializations, + lambda_sets_to_specialize, + ); + + state + } + Failure(vars, actual_type, expected_to_include_type, _bad_impls) => { + env.introduce(rank, &vars); + + let problem = TypeError::BadPattern( + *region, + pattern_category.clone(), + expected_to_include_type, + PExpected::NoExpectation(actual_type), + ); + problems.push(problem); + + state + } + } + } + &Exhaustive(eq, sketched_rows, context, exhaustive_mark) => { + // A few cases: + // 1. Either condition or branch types already have a type error. In this case just + // propagate it. + // 2. Types are correct, but there are redundancies. In this case we want + // exhaustiveness checking to pull those out. + // 3. Condition and branch types are "almost equal", that is one or the other is + // only missing a few more tags. In this case we want to run + // exhaustiveness checking both ways, to see which one is missing tags. + // 4. Condition and branch types aren't "almost equal", this is just a normal type + // error. + + let (real_var, real_region, branches_var, category_and_expected) = match eq { + Ok(eq) => { + let roc_can::constraint::Eq(real_var, expected, category, real_region) = + env.constraints.eq[eq.index()]; + let expected = &env.constraints.expectations[expected.index()]; + + ( + real_var, + real_region, + *expected.get_type_ref(), + Ok((category, expected)), + ) + } + Err(peq) => { + let roc_can::constraint::PatternEq( + real_var, + expected, + category, + real_region, + ) = env.constraints.pattern_eq[peq.index()]; + let expected = &env.constraints.pattern_expectations[expected.index()]; + + ( + real_var, + real_region, + *expected.get_type_ref(), + Err((category, expected)), + ) + } + }; + + let real_var = either_type_index_to_var( + env, + rank, + problems, + abilities_store, + obligation_cache, + &mut can_types, + aliases, + real_var, + ); + + let branches_var = either_type_index_to_var( + env, + rank, + problems, + abilities_store, + obligation_cache, + &mut can_types, + aliases, + branches_var, + ); + + let cond_source_is_likely_positive_value = category_and_expected.is_ok(); + let cond_polarity = if cond_source_is_likely_positive_value { + Polarity::OF_VALUE + } else { + Polarity::OF_PATTERN + }; + + let real_content = env.subs.get_content_without_compacting(real_var); + let branches_content = env.subs.get_content_without_compacting(branches_var); + let already_have_error = matches!( + (real_content, branches_content), + (Content::Error, _) | (_, Content::Error) + ); + + let snapshot = env.subs.snapshot(); + let unify_cond_and_patterns_outcome = unify( + &mut env.uenv(), + branches_var, + real_var, + UnificationMode::EQ, + cond_polarity, + ); + + let should_check_exhaustiveness; + let has_unification_error = + !matches!(unify_cond_and_patterns_outcome, Success { .. }); + match unify_cond_and_patterns_outcome { + Success { + vars, + must_implement_ability, + lambda_sets_to_specialize, + extra_metadata: _, + } => { + env.subs.commit_snapshot(snapshot); + + env.introduce(rank, &vars); + + problems.extend(obligation_cache.check_obligations( + env.subs, + abilities_store, + must_implement_ability, + AbilityImplError::DoesNotImplement, + )); + compact_lambdas_and_check_obligations( + env, + problems, + abilities_store, + obligation_cache, + awaiting_specializations, + lambda_sets_to_specialize, + ); + + // Case 1: unify error types, but don't check exhaustiveness. + // Case 2: run exhaustiveness to check for redundant branches. + should_check_exhaustiveness = !already_have_error; + } + Failure(..) => { + // Rollback and check for almost-equality. + env.subs.rollback_to(snapshot); + + let almost_eq_snapshot = env.subs.snapshot(); + // TODO: turn this on for bidirectional exhaustiveness checking + // open_tag_union(subs, real_var); + open_tag_union(env, branches_var); + let almost_eq = matches!( + unify( + &mut env.uenv(), + real_var, + branches_var, + UnificationMode::EQ, + cond_polarity, + ), + Success { .. } + ); + + env.subs.rollback_to(almost_eq_snapshot); + + if almost_eq { + // Case 3: almost equal, check exhaustiveness. + should_check_exhaustiveness = true; + } else { + // Case 4: incompatible types, report type error. + // Re-run first failed unification to get the type diff. + match unify( + &mut env.uenv(), + real_var, + branches_var, + UnificationMode::EQ, + cond_polarity, + ) { + Failure(vars, actual_type, expected_type, _bad_impls) => { + env.introduce(rank, &vars); + + // Figure out the problem - it might be pattern or value + // related. + let problem = match category_and_expected { + Ok((category, expected)) => { + let real_category = env.constraints.categories + [category.index()] + .clone(); + TypeError::BadExpr( + real_region, + real_category, + actual_type, + expected.replace_ref(expected_type), + ) + } + + Err((category, expected)) => { + let real_category = env.constraints.pattern_categories + [category.index()] + .clone(); + TypeError::BadPattern( + real_region, + real_category, + expected_type, + expected.replace_ref(actual_type), + ) + } + }; + + problems.push(problem); + should_check_exhaustiveness = false; + } + _ => internal_error!("Must be failure"), + } + } + } + } + + let sketched_rows = env.constraints.sketched_rows[sketched_rows.index()].clone(); + + if should_check_exhaustiveness { + use roc_can::exhaustive::{check, ExhaustiveSummary}; + + // If the condition type likely comes from an positive-position value (e.g. a + // literal or a return type), rather than an input position, we employ the + // heuristic that the positive-position value would only need to be open if the + // branches of the `when` constrained them as open. To avoid suggesting + // catch-all branches, now mark the condition type as closed, so that we only + // show the variants that explicitly not matched. + // + // We avoid this heuristic if the condition type likely comes from a negative + // position, e.g. a function parameter, since in that case if the condition + // type is open, we definitely want to show the catch-all branch as necessary. + // + // For example: + // + // x : [A, B, C] + // + // when x is + // A -> .. + // B -> .. + // + // This is checked as "almost equal" and hence exhaustiveness-checked with + // [A, B] compared to [A, B, C]*. However, we really want to compare against + // [A, B, C] (notice the closed union), so we optimistically close the + // condition type here. + // + // On the other hand, in a case like + // + // f : [A, B, C]* -> .. + // f = \x -> when x is + // A -> .. + // B -> .. + // + // we want to show `C` and/or `_` as necessary branches, so this heuristic is + // not applied. + // + // In the above case, notice it would not be safe to apply this heuristic if + // `C` was matched as well. Since the positive/negative value determination is + // only an estimate, we also only apply this heursitic in the "almost equal" + // case, when there was in fact a unification error. + // + // TODO: this can likely be removed after remodelling tag extension types + // (#4440). + if cond_source_is_likely_positive_value && has_unification_error { + close_pattern_matched_tag_unions(env.subs, real_var); + } + + if let Ok(ExhaustiveSummary { + errors, + exhaustive, + redundancies, + }) = check(env.subs, real_var, sketched_rows, context) + { + // Store information about whether the "when" is exhaustive, and + // which (if any) of its branches are redundant. Codegen may use + // this for branch-fixing and redundant elimination. + if !exhaustive { + exhaustive_mark.set_non_exhaustive(env.subs); + } + for redundant_mark in redundancies { + redundant_mark.set_redundant(env.subs); + } + + // Store the errors. + problems.extend(errors.into_iter().map(TypeError::Exhaustive)); + } else { + // Otherwise there were type errors deeper in the pattern; we will have + // already reported them. + } + } + + state + } + &Resolve(OpportunisticResolve { + specialization_variable, + member, + specialization_id, + }) => { + if let Ok(Resolved::Specialization(specialization)) = resolve_ability_specialization( + env.subs, + abilities_store, + member, + specialization_variable, + ) { + abilities_store.insert_resolved(specialization_id, specialization); + } + + state + } + CheckCycle(cycle, cycle_mark) => { + let Cycle { + def_names, + expr_regions, + } = &env.constraints.cycles[cycle.index()]; + let symbols = &env.constraints.loc_symbols[def_names.indices()]; + + // If the type of a symbol is not a function, that's an error. + // Roc is strict, so only functions can be mutually recursive. + let any_is_bad = { + use Content::*; + + symbols.iter().any(|(s, _)| { + let var = scope.get_var_by_symbol(s).expect("Symbol not solved!"); + let (_, underlying_content) = chase_alias_content(env.subs, var); + + !matches!(underlying_content, Error | Structure(FlatType::Func(..))) + }) + }; + + if any_is_bad { + // expr regions are stored in loc_symbols (that turned out to be convenient). + // The symbol is just a dummy, and should not be used + let expr_regions = &env.constraints.loc_symbols[expr_regions.indices()]; + + let cycle = symbols + .iter() + .zip(expr_regions.iter()) + .map(|(&(symbol, symbol_region), &(_, expr_region))| CycleEntry { + symbol, + symbol_region, + expr_region, + }) + .collect(); + + problems.push(TypeError::CircularDef(cycle)); + + cycle_mark.set_illegal(env.subs); + } + + state + } + IngestedFile(type_index, file_path, bytes) => { + let actual = either_type_index_to_var( + env, + rank, + problems, + abilities_store, + obligation_cache, + &mut can_types, + aliases, + *type_index, + ); + + let snapshot = env.subs.snapshot(); + if let Success { + vars, + must_implement_ability, + lambda_sets_to_specialize, + extra_metadata: _, + } = unify( + &mut env.uenv(), + actual, + Variable::LIST_U8, + UnificationMode::EQ, + Polarity::OF_VALUE, + ) { + // List U8 always valid. + env.introduce(rank, &vars); + + debug_assert!( + must_implement_ability.is_empty() && lambda_sets_to_specialize.is_empty(), + "List U8 will never need to implement abilities or specialize lambda sets" + ); + + state + } else { + env.subs.rollback_to(snapshot); + + // We explicitly match on the last unify to get the type in the case it errors. + match unify( + &mut env.uenv(), + actual, + Variable::STR, + UnificationMode::EQ, + Polarity::OF_VALUE, + ) { + Success { + vars, + must_implement_ability, + lambda_sets_to_specialize, + extra_metadata: _, + } => { + env.introduce(rank, &vars); + + debug_assert!( + must_implement_ability.is_empty() && lambda_sets_to_specialize.is_empty(), + "Str will never need to implement abilities or specialize lambda sets" + ); + + // Str only valid if valid utf8. + if let Err(err) = std::str::from_utf8(bytes) { + let problem = + TypeError::IngestedFileBadUtf8(file_path.clone(), err); + problems.push(problem); + } + + state + } + Failure(vars, actual_type, _, _) => { + env.introduce(rank, &vars); + + let problem = TypeError::IngestedFileUnsupportedType( + file_path.clone(), + actual_type, + ); + problems.push(problem); + state + } + } + } + } + }; + } + + state +} + +fn chase_alias_content(subs: &Subs, mut var: Variable) -> (Variable, &Content) { + loop { + match subs.get_content_without_compacting(var) { + Content::Alias(_, _, real_var, _) => { + var = *real_var; + } + content => return (var, content), + } + } +} + +fn compact_lambdas_and_check_obligations( + env: &mut InferenceEnv, + problems: &mut Vec, + abilities_store: &mut AbilitiesStore, + obligation_cache: &mut ObligationCache, + awaiting_specialization: &mut AwaitingSpecializations, + lambda_sets_to_specialize: UlsOfVar, +) { + let CompactionResult { + obligations, + awaiting_specialization: new_awaiting, + } = compact_lambda_sets_of_vars( + &mut env.as_solve_env(), + lambda_sets_to_specialize, + &SolvePhase { abilities_store }, + ); + problems.extend(obligation_cache.check_obligations( + env.subs, + abilities_store, + obligations, + AbilityImplError::DoesNotImplement, + )); + awaiting_specialization.union(new_awaiting); +} + +fn open_tag_union(env: &mut InferenceEnv, var: Variable) { + let mut stack = vec![var]; + while let Some(var) = stack.pop() { + use {Content::*, FlatType::*}; + + let desc = env.subs.get(var); + match desc.content { + Structure(TagUnion(tags, ext)) => { + if let Structure(EmptyTagUnion) = env.subs.get_content_without_compacting(ext.var()) + { + let new_ext_var = env.register(desc.rank, Content::FlexVar(None)); + + let new_union = Structure(TagUnion(tags, TagExt::Any(new_ext_var))); + env.subs.set_content(var, new_union); + } + + // Also open up all nested tag unions. + let all_vars = tags.variables().into_iter(); + stack.extend( + all_vars + .flat_map(|slice| env.subs[slice]) + .map(|var| env.subs[var]), + ); + } + + Structure(Record(fields, _)) => { + // Open up all nested tag unions. + stack.extend(env.subs.get_subs_slice(fields.variables())); + } + + Structure(Tuple(elems, _)) => { + // Open up all nested tag unions. + stack.extend(env.subs.get_subs_slice(elems.variables())); + } + + Structure(Apply(Symbol::LIST_LIST, args)) => { + // Open up nested tag unions. + stack.extend(env.subs.get_subs_slice(args)); + } + + _ => { + // Everything else is not a structural type that can be opened + // (i.e. cannot be matched in a pattern-match) + } + } + + // Today, an "open" constraint doesn't affect any types + // other than tag unions. Recursive tag unions are constructed + // at a later time (during occurs checks after tag unions are + // resolved), so that's not handled here either. + } +} + +/// Optimistically closes the positive type of a value matched in a `when` statement, to produce +/// better exhaustiveness error messages. +/// +/// This should only be applied if it's already known that a `when` expression is not exhaustive. +/// +/// See [Constraint::Exhaustive]. +fn close_pattern_matched_tag_unions(subs: &mut Subs, var: Variable) { + let mut stack = vec![var]; + while let Some(var) = stack.pop() { + use {Content::*, FlatType::*}; + + let desc = subs.get(var); + match desc.content { + Structure(TagUnion(tags, mut ext)) => { + // Close the extension, chasing it as far as it goes. + loop { + match subs.get_content_without_compacting(ext.var()) { + Structure(FlatType::EmptyTagUnion) => { + break; + } + FlexVar(..) | FlexAbleVar(..) => { + subs.set_content_unchecked( + ext.var(), + Structure(FlatType::EmptyTagUnion), + ); + break; + } + RigidVar(..) | RigidAbleVar(..) => { + // Don't touch rigids, they tell us more information than the heuristic + // of closing tag unions does for better exhaustiveness checking does. + break; + } + Structure(FlatType::TagUnion(_, deep_ext)) + | Structure(FlatType::RecursiveTagUnion(_, _, deep_ext)) + | Structure(FlatType::FunctionOrTagUnion(_, _, deep_ext)) => { + ext = *deep_ext; + } + other => internal_error!( + "not a tag union: {:?}", + roc_types::subs::SubsFmtContent(other, subs) + ), + } + } + + // Also open up all nested tag unions. + let all_vars = tags.variables().into_iter(); + stack.extend(all_vars.flat_map(|slice| subs[slice]).map(|var| subs[var])); + } + + Structure(Record(fields, _)) => { + // Close up all nested tag unions. + stack.extend(subs.get_subs_slice(fields.variables())); + } + + Structure(Apply(Symbol::LIST_LIST, args)) => { + // Close up nested tag unions. + stack.extend(subs.get_subs_slice(args)); + } + + Alias(_, _, real_var, _) => { + stack.push(real_var); + } + + _ => { + // Everything else is not a type that can be opened/matched in a pattern match. + } + } + + // Recursive tag unions are constructed at a later time + // (during occurs checks after tag unions are resolved), + // so that's not handled here. + } +} + +/// If a symbol claims to specialize an ability member, check that its solved type in fact +/// does specialize the ability, and record the specialization. +// Aggressive but necessary - there aren't many usages. +#[inline(always)] +fn check_ability_specialization( + env: &mut InferenceEnv, + rank: Rank, + abilities_store: &mut AbilitiesStore, + obligation_cache: &mut ObligationCache, + awaiting_specializations: &mut AwaitingSpecializations, + problems: &mut Vec, + symbol: Symbol, + symbol_loc_var: Loc, +) { + // If the symbol specializes an ability member, we need to make sure that the + // inferred type for the specialization actually aligns with the expected + // implementation. + if let Some((impl_key, root_data)) = abilities_store.impl_key_and_def(symbol) { + let ability_member = impl_key.ability_member; + let root_signature_var = root_data.signature_var(); + let parent_ability = root_data.parent_ability; + + // Check if they unify - if they don't, then the claimed specialization isn't really one, + // and that's a type error! + // This also fixes any latent type variables that need to be specialized to exactly what + // the ability signature expects. + + // We need to freshly instantiate the root signature so that all unifications are reflected + // in the specialization type, but not the original signature type. + let root_signature_var = { + let mut solve_env = env.as_solve_env(); + let solve_env = &mut solve_env; + deep_copy_var_in( + solve_env, + Rank::toplevel(), + root_signature_var, + solve_env.arena, + ) + }; + let snapshot = env.subs.snapshot(); + let unified = unify_introduced_ability_specialization( + &mut env.uenv(), + root_signature_var, + symbol_loc_var.value, + UnificationMode::EQ, + ); + + let resolved_mark = match unified { + Success { + vars, + must_implement_ability, + lambda_sets_to_specialize, + extra_metadata: SpecializationLsetCollector(specialization_lambda_sets), + } => { + let specialization_type = + type_implementing_specialization(&must_implement_ability, parent_ability); + + match specialization_type { + Some(Obligated::Opaque(opaque)) => { + // This is a specialization for an opaque - but is it the opaque the + // specialization was claimed to be for? + if opaque == impl_key.opaque { + // It was! All is good. + + env.subs.commit_snapshot(snapshot); + env.introduce(rank, &vars); + + let specialization_lambda_sets = specialization_lambda_sets + .into_iter() + .map(|((symbol, region), var)| { + debug_assert_eq!(symbol, ability_member); + (region, var) + }) + .collect(); + + compact_lambdas_and_check_obligations( + env, + problems, + abilities_store, + obligation_cache, + awaiting_specializations, + lambda_sets_to_specialize, + ); + + let specialization = + MemberSpecializationInfo::new(symbol, specialization_lambda_sets); + + Ok(specialization) + } else { + // This def is not specialized for the claimed opaque type, that's an + // error. + + // Commit so that the bad signature and its error persists in subs. + env.subs.commit_snapshot(snapshot); + + let _typ = env + .subs + .var_to_error_type(symbol_loc_var.value, Polarity::OF_VALUE); + + let problem = TypeError::WrongSpecialization { + region: symbol_loc_var.region, + ability_member: impl_key.ability_member, + expected_opaque: impl_key.opaque, + found_opaque: opaque, + }; + + problems.push(problem); + + Err(()) + } + } + Some(Obligated::Adhoc(var)) => { + // This is a specialization of a structural type - never allowed. + + // Commit so that `var` persists in subs. + env.subs.commit_snapshot(snapshot); + + let typ = env.subs.var_to_error_type(var, Polarity::OF_VALUE); + + let problem = TypeError::StructuralSpecialization { + region: symbol_loc_var.region, + typ, + ability: parent_ability, + member: ability_member, + }; + + problems.push(problem); + + Err(()) + } + None => { + // This can happen when every ability constriant on a type variable went + // through only another type variable. That means this def is not specialized + // for one concrete type, and especially not our opaque - we won't admit this currently. + + // Rollback the snapshot so we unlink the root signature with the specialization, + // so we can have two separate error types. + env.subs.rollback_to(snapshot); + + let expected_type = env + .subs + .var_to_error_type(root_signature_var, Polarity::OF_VALUE); + let actual_type = env + .subs + .var_to_error_type(symbol_loc_var.value, Polarity::OF_VALUE); + + let reason = Reason::GeneralizedAbilityMemberSpecialization { + member_name: ability_member, + def_region: root_data.region, + }; + + let problem = TypeError::BadExpr( + symbol_loc_var.region, + Category::AbilityMemberSpecialization(ability_member), + actual_type, + Expected::ForReason(reason, expected_type, symbol_loc_var.region), + ); + + problems.push(problem); + + Err(()) + } + } + } + + Failure(vars, expected_type, actual_type, unimplemented_abilities) => { + env.subs.commit_snapshot(snapshot); + env.introduce(rank, &vars); + + let reason = Reason::InvalidAbilityMemberSpecialization { + member_name: ability_member, + def_region: root_data.region, + unimplemented_abilities, + }; + + let problem = TypeError::BadExpr( + symbol_loc_var.region, + Category::AbilityMemberSpecialization(ability_member), + actual_type, + Expected::ForReason(reason, expected_type, symbol_loc_var.region), + ); + + problems.push(problem); + + Err(()) + } + }; + + abilities_store + .mark_implementation(impl_key, resolved_mark) + .expect("marked as a custom implementation, but not recorded as such"); + + // Get the lambda sets that are ready for specialization because this ability member + // specialization was resolved, and compact them. + let new_lambda_sets_to_specialize = + awaiting_specializations.remove_for_specialized(env.subs, impl_key); + compact_lambdas_and_check_obligations( + env, + problems, + abilities_store, + obligation_cache, + awaiting_specializations, + new_lambda_sets_to_specialize, + ); + debug_assert!( + !awaiting_specializations.waiting_for(impl_key), + "still have lambda sets waiting for {impl_key:?}, but it was just resolved" + ); + } +} + +#[derive(Debug)] +enum LocalDefVarsVec { + Stack(arrayvec::ArrayVec), + Heap(Vec), +} + +impl LocalDefVarsVec { + #[inline(always)] + fn with_length(length: usize) -> Self { + if length <= 32 { + Self::Stack(Default::default()) + } else { + Self::Heap(Default::default()) + } + } + + fn push(&mut self, element: T) { + match self { + LocalDefVarsVec::Stack(vec) => vec.push(element), + LocalDefVarsVec::Heap(vec) => vec.push(element), + } + } + + fn iter(&self) -> impl Iterator { + match self { + LocalDefVarsVec::Stack(vec) => vec.iter(), + LocalDefVarsVec::Heap(vec) => vec.iter(), + } + } +} + +impl LocalDefVarsVec<(Symbol, Loc)> { + fn from_def_types( + env: &mut InferenceEnv, + rank: Rank, + problems: &mut Vec, + abilities_store: &mut AbilitiesStore, + obligation_cache: &mut ObligationCache, + types: &mut Types, + aliases: &mut Aliases, + def_types_slice: roc_can::constraint::DefTypes, + ) -> Self { + let type_indices_slice = &env.constraints.type_slices[def_types_slice.types.indices()]; + let loc_symbols_slice = &env.constraints.loc_symbols[def_types_slice.loc_symbols.indices()]; + + let mut local_def_vars = Self::with_length(type_indices_slice.len()); + + for (&(symbol, region), typ_index) in (loc_symbols_slice.iter()).zip(type_indices_slice) { + let var = either_type_index_to_var( + env, + rank, + problems, + abilities_store, + obligation_cache, + types, + aliases, + *typ_index, + ); + + local_def_vars.push((symbol, Loc { value: var, region })); + } + + local_def_vars + } +} + +fn check_for_infinite_type( + env: &mut InferenceEnv, + problems: &mut Vec, + symbol: Symbol, + loc_var: Loc, +) { + let var = loc_var.value; + + 'next_occurs_check: while let Err((_, chain)) = env.subs.occurs(var) { + // walk the chain till we find a tag union or lambda set, starting from the variable that + // occurred recursively, which is always at the end of the chain. + for &var in chain.iter().rev() { + match *env.subs.get_content_without_compacting(var) { + Content::Structure(FlatType::TagUnion(tags, ext_var)) => { + let rec_var = env.subs.mark_tag_union_recursive(var, tags, ext_var); + env.register_existing_var(rec_var); + + continue 'next_occurs_check; + } + Content::LambdaSet(subs::LambdaSet { + solved, + recursion_var: OptVariable::NONE, + unspecialized, + ambient_function: ambient_function_var, + }) => { + let rec_var = env.subs.mark_lambda_set_recursive( + var, + solved, + unspecialized, + ambient_function_var, + ); + env.register_existing_var(rec_var); + + continue 'next_occurs_check; + } + _ => { /* fall through */ } + } + } + + circular_error(env.subs, problems, symbol, &loc_var); + } +} + +fn circular_error( + subs: &mut Subs, + problems: &mut Vec, + symbol: Symbol, + loc_var: &Loc, +) { + let var = loc_var.value; + let error_type = subs.var_to_error_type(var, Polarity::OF_VALUE); + let problem = TypeError::CircularType(loc_var.region, symbol, error_type); + + subs.set_content(var, Content::Error); + + problems.push(problem); +} + +/// Generalizes variables at the `young_rank`, which did not escape a let-binding +/// into a lower scope. +/// +/// Ensures that variables introduced at the `young_rank`, but that should be +/// stuck at a lower level, are marked at that level and not generalized at the +/// present `young_rank`. See [adjust_rank]. +fn generalize(env: &mut InferenceEnv, young_mark: Mark, visit_mark: Mark, young_rank: Rank) { + let subs = &mut env.subs; + let pools = &mut env.pools; + + let young_vars = std::mem::take(pools.get_mut(young_rank)); + let rank_table = pool_to_rank_table(subs, young_mark, young_rank, young_vars); + + // Get the ranks right for each entry. + // Start at low ranks so we only have to pass over the information once. + for (index, table) in rank_table.iter().enumerate() { + for &var in table.iter() { + adjust_rank(subs, young_mark, visit_mark, Rank::from(index), var); + } + } + + let (mut last_pool, all_but_last_pool) = rank_table.split_last(); + + // For variables that have rank lowerer than young_rank, register them in + // the appropriate old pool if they are not redundant. + for vars in all_but_last_pool { + for var in vars { + let rank = subs.get_rank(var); + + pools.get_mut(rank).push(var); + } + } + + // For variables with rank young_rank, if rank < young_rank: register in old pool, + // otherwise generalize + for var in last_pool.drain(..) { + let desc_rank = subs.get_rank(var); + + if desc_rank < young_rank { + pools.get_mut(desc_rank).push(var); + } else { + subs.set_rank(var, Rank::GENERALIZED); + } + } + + // re-use the last_vector (which likely has a good capacity for future runs) + debug_assert!(last_pool.is_empty()); + *pools.get_mut(young_rank) = last_pool; +} + +/// Sort the variables into buckets by rank. +#[inline] +fn pool_to_rank_table( + subs: &mut Subs, + young_mark: Mark, + young_rank: Rank, + mut young_vars: Vec, +) -> Pools { + let mut pools = Pools::new(young_rank.into_usize() + 1); + + // the vast majority of young variables have young_rank + let mut i = 0; + while i < young_vars.len() { + let var = subs.get_root_key(young_vars[i]); + + subs.set_mark_unchecked(var, young_mark); + let rank = subs.get_rank_unchecked(var); + + if rank != young_rank { + debug_assert!(rank.into_usize() < young_rank.into_usize() + 1); + + pools.get_mut(rank).push(var); + + // swap an element in; don't increment i + young_vars.swap_remove(i); + } else { + i += 1; + } + } + + std::mem::swap(pools.get_mut(young_rank), &mut young_vars); + + pools +} + +/// Adjust variable ranks such that ranks never increase as you move deeper. +/// This way the outermost rank is representative of the entire structure. +/// +/// This procedure also catches type variables at a given rank that contain types at a higher rank. +/// In such cases, the contained types must be lowered to the rank of the outer type. This is +/// critical for soundness of the type inference; for example consider +/// +/// ```ignore(illustrative) +/// \f -> # rank=1 +/// g = \x -> f x # rank=2 +/// g +/// ``` +/// +/// say that during the solving of the outer body at rank 1 we conditionally give `f` the type +/// `a -> b (rank=1)`. Without rank-adjustment, the type of `g` would be solved as `c -> d (rank=2)` for +/// some `c ~ a`, `d ~ b`, and hence would be generalized to the function `c -> d`, even though `c` +/// and `d` are individually at rank 1 after unfication with `a` and `b` respectively. +/// This is incorrect; the whole of `c -> d` must lie at rank 1, and only be generalized at the +/// level that `f` is introduced. +fn adjust_rank( + subs: &mut Subs, + young_mark: Mark, + visit_mark: Mark, + group_rank: Rank, + var: Variable, +) -> Rank { + let var = subs.get_root_key(var); + + let desc_rank = subs.get_rank_unchecked(var); + let desc_mark = subs.get_mark_unchecked(var); + + if desc_mark == young_mark { + let content = *subs.get_content_unchecked(var); + + // Mark the variable as visited before adjusting content, as it may be cyclic. + subs.set_mark_unchecked(var, visit_mark); + + // Adjust the nested types' ranks, making sure that no nested unbound type variable is at a + // higher rank than the group rank this `var` is at + let max_rank = adjust_rank_content(subs, young_mark, visit_mark, group_rank, &content); + + subs.set_rank_unchecked(var, max_rank); + subs.set_mark_unchecked(var, visit_mark); + + max_rank + } else if desc_mark == visit_mark { + // we have already visited this variable + // (probably two variables had the same root) + desc_rank + } else { + let min_rank = group_rank.min(desc_rank); + + // TODO from elm-compiler: how can min_rank ever be group_rank? + subs.set_rank_unchecked(var, min_rank); + subs.set_mark_unchecked(var, visit_mark); + + min_rank + } +} + +fn adjust_rank_content( + subs: &mut Subs, + young_mark: Mark, + visit_mark: Mark, + group_rank: Rank, + content: &Content, +) -> Rank { + use roc_types::subs::Content::*; + use roc_types::subs::FlatType::*; + + match content { + FlexVar(_) | RigidVar(_) | FlexAbleVar(_, _) | RigidAbleVar(_, _) | Error => group_rank, + + RecursionVar { .. } => group_rank, + + Structure(flat_type) => { + match flat_type { + Apply(_, args) => { + let mut rank = Rank::toplevel(); + + for var_index in args.into_iter() { + let var = subs[var_index]; + rank = rank.max(adjust_rank(subs, young_mark, visit_mark, group_rank, var)); + } + + rank + } + + Func(arg_vars, closure_var, ret_var) => { + let mut rank = adjust_rank(subs, young_mark, visit_mark, group_rank, *ret_var); + + // TODO investigate further. + // + // My theory is that because the closure_var contains variables already + // contained in the signature only, it does not need to be part of the rank + // calculuation + if true { + rank = rank.max(adjust_rank( + subs, + young_mark, + visit_mark, + group_rank, + *closure_var, + )); + } + + for index in arg_vars.into_iter() { + let var = subs[index]; + rank = rank.max(adjust_rank(subs, young_mark, visit_mark, group_rank, var)); + } + + rank + } + + EmptyRecord | EmptyTuple => { + // from elm-compiler: THEORY: an empty record never needs to get generalized + // + // But for us, that theory does not hold, because there might be type variables hidden + // inside a lambda set but not on the left or right of an arrow, and records should not + // force de-generalization in such cases. + // + // See https://github.com/roc-lang/roc/issues/3641 for a longer discussion and + // example. + group_rank + } + + // THEORY: an empty tag never needs to get generalized + EmptyTagUnion => Rank::toplevel(), + + Record(fields, ext_var) => { + let mut rank = adjust_rank(subs, young_mark, visit_mark, group_rank, *ext_var); + + for (_, var_index, field_index) in fields.iter_all() { + let var = subs[var_index]; + rank = rank.max(adjust_rank(subs, young_mark, visit_mark, group_rank, var)); + + // When generalizing annotations with rigid optional/required fields, + // we want to promote them to non-rigid, so that usages at + // specialized sites don't have to exactly include the optional/required field. + match subs[field_index] { + RecordField::RigidOptional(()) => { + subs[field_index] = RecordField::Optional(()); + } + RecordField::RigidRequired(()) => { + subs[field_index] = RecordField::Required(()); + } + _ => {} + } + } + + rank + } + + Tuple(elems, ext_var) => { + let mut rank = adjust_rank(subs, young_mark, visit_mark, group_rank, *ext_var); + + for (_, var_index) in elems.iter_all() { + let var = subs[var_index]; + rank = rank.max(adjust_rank(subs, young_mark, visit_mark, group_rank, var)); + } + + rank + } + + TagUnion(tags, ext_var) => { + let mut rank = + adjust_rank(subs, young_mark, visit_mark, group_rank, ext_var.var()); + // For performance reasons, we only keep one representation of empty tag unions + // in subs. That representation exists at rank 0, which we don't always want to + // reflect the whole tag union as, because doing so may over-generalize free + // type variables. + // Normally this is not a problem because of the loop below that maximizes the + // rank from nested types in the union. But suppose we have the simple tag + // union + // [Z]{} + // there are no nested types in the tags, and the empty tag union is at rank 0, + // so we promote the tag union to rank 0. Now if we introduce the presence + // constraint + // [Z]{} += [S a] + // we'll wind up with [Z, S a]{}, but it will be at rank 0, and "a" will get + // over-generalized. Really, the empty tag union should be introduced at + // whatever current group rank we're at, and so that's how we encode it here. + if ext_var.var() == Variable::EMPTY_TAG_UNION && rank.is_generalized() { + rank = group_rank; + } + + for (_, index) in tags.iter_all() { + let slice = subs[index]; + for var_index in slice { + let var = subs[var_index]; + rank = rank + .max(adjust_rank(subs, young_mark, visit_mark, group_rank, var)); + } + } + + rank + } + + FunctionOrTagUnion(_, _, ext_var) => { + adjust_rank(subs, young_mark, visit_mark, group_rank, ext_var.var()) + } + + RecursiveTagUnion(rec_var, tags, ext_var) => { + let mut rank = + adjust_rank(subs, young_mark, visit_mark, group_rank, ext_var.var()); + + for (_, index) in tags.iter_all() { + let slice = subs[index]; + for var_index in slice { + let var = subs[var_index]; + rank = rank + .max(adjust_rank(subs, young_mark, visit_mark, group_rank, var)); + } + } + + // The recursion var may have a higher rank than the tag union itself, if it is + // erroneous and escapes into a region where it is let-generalized before it is + // constrained back down to the rank it originated from. + // + // For example, see the `recursion_var_specialization_error` reporting test - + // there, we have + // + // Job a : [Job (List (Job a)) a] + // + // job : Job Str + // + // when job is + // Job lst _ -> lst == "" + // + // In this case, `lst` is generalized and has a higher rank for the type + // `(List (Job a)) as a` - notice that only the recursion var `a` is active + // here, not the entire recursive tag union. In the body of this branch, `lst` + // becomes a type error, but the nested recursion var `a` is left untouched, + // because it is nested under the of `lst`, not the surface type that becomes + // an error. + // + // Had this not become a type error, `lst` would then be constrained against + // `job`, and its rank would get pulled back down. So, this can only happen in + // the presence of type errors. + // + // In all other cases, the recursion var has the same rank as the tag union itself + // all types it uses are also in the tags already, so it cannot influence the + // rank. + if cfg!(debug_assertions) + && !matches!( + subs.get_content_without_compacting(*rec_var), + Content::Error | Content::FlexVar(..) + ) + { + let rec_var_rank = + adjust_rank(subs, young_mark, visit_mark, group_rank, *rec_var); + + debug_assert!( + rank >= rec_var_rank, + "rank was {:?} but recursion var <{:?}>{:?} has higher rank {:?}", + rank, + rec_var, + subs.get_content_without_compacting(*rec_var), + rec_var_rank + ); + } + + rank + } + } + } + + Alias(_, args, real_var, _) => { + let mut rank = Rank::toplevel(); + + // Avoid visiting lambda set variables stored in the type variables of the alias + // independently. + // + // Why? Lambda set variables on the alias are not truly type arguments to the alias, + // and instead are links to the lambda sets that appear in functions under the real + // type of the alias. If their ranks are adjusted independently, we end up looking at + // function types "inside-out" - when the whole point of rank-adjustment is to look + // from the outside-in to determine at what rank a type lies! + // + // So, just wait to adjust their ranks until we visit the function types that contain + // them. If they should be generalized (or pulled to a lower rank) that will happen + // then; otherwise, we risk generalizing a lambda set too early, when its enclosing + // function type should not be. + let adjustable_variables = + (args.type_variables().into_iter()).chain(args.infer_ext_in_output_variables()); + + for var_index in adjustable_variables { + let var = subs[var_index]; + rank = rank.max(adjust_rank(subs, young_mark, visit_mark, group_rank, var)); + } + + // from elm-compiler: THEORY: anything in the real_var would be Rank::toplevel() + // this theory is not true in Roc! aliases of function types capture the closure var + rank = rank.max(adjust_rank( + subs, young_mark, visit_mark, group_rank, *real_var, + )); + + rank + } + + LambdaSet(subs::LambdaSet { + solved, + recursion_var, + unspecialized, + ambient_function: ambient_function_var, + }) => { + let mut rank = group_rank; + + for (_, index) in solved.iter_all() { + let slice = subs[index]; + for var_index in slice { + let var = subs[var_index]; + rank = rank.max(adjust_rank(subs, young_mark, visit_mark, group_rank, var)); + } + } + + for uls_index in *unspecialized { + let Uls(var, _, _) = subs[uls_index]; + rank = rank.max(adjust_rank(subs, young_mark, visit_mark, group_rank, var)); + } + + if let (true, Some(rec_var)) = (cfg!(debug_assertions), recursion_var.into_variable()) { + // THEORY: unlike the situation for recursion vars under recursive tag unions, + // recursive vars inside lambda sets can't escape into higher let-generalized regions + // because lambda sets aren't user-facing. + // + // So the recursion var should be fully accounted by everything else in the lambda set + // (since it appears in the lambda set), and if the rank is higher, it's either a + // bug or our theory is wrong and indeed they can escape into higher regions. + let rec_var_rank = adjust_rank(subs, young_mark, visit_mark, group_rank, rec_var); + + debug_assert!( + rank >= rec_var_rank, + "rank was {:?} but recursion var <{:?}>{:?} has higher rank {:?}", + rank, + rec_var, + subs.get_content_without_compacting(rec_var), + rec_var_rank + ); + } + + // NEVER TOUCH the ambient function var, it would already have been passed through. + { + let _ = ambient_function_var; + } + + rank + } + + ErasedLambda => group_rank, + + RangedNumber(_) => group_rank, + } +} diff --git a/crates/compiler/solve/src/solve/scope.rs b/crates/compiler/solve/src/solve/scope.rs new file mode 100644 index 0000000000..5acd041c96 --- /dev/null +++ b/crates/compiler/solve/src/solve/scope.rs @@ -0,0 +1,40 @@ +use roc_module::symbol::Symbol; +use roc_types::subs::Variable; + +/// The scope of the solver, as symbols are introduced. +#[derive(Clone, Debug, Default)] +pub struct Scope { + symbols: Vec, + variables: Vec, +} + +impl Scope { + pub fn vars_by_symbol(&self) -> impl Iterator + '_ { + let it1 = self.symbols.iter().copied(); + let it2 = self.variables.iter().copied(); + + it1.zip(it2) + } + + #[inline(always)] + pub fn get_var_by_symbol(&self, symbol: &Symbol) -> Option { + self.symbols + .iter() + .position(|s| s == symbol) + .map(|index| self.variables[index]) + } + + #[inline(always)] + pub fn insert_symbol_var_if_vacant(&mut self, symbol: Symbol, var: Variable) { + match self.symbols.iter().position(|s| *s == symbol) { + None => { + // symbol is not in vars_by_symbol yet; insert it + self.symbols.push(symbol); + self.variables.push(var); + } + Some(_) => { + // do nothing + } + } + } +} diff --git a/crates/compiler/solve/src/specialize.rs b/crates/compiler/solve/src/specialize.rs new file mode 100644 index 0000000000..cfde2e45e3 --- /dev/null +++ b/crates/compiler/solve/src/specialize.rs @@ -0,0 +1,877 @@ +//! Module [specialize] is resolves specialization lambda sets. + +use std::collections::VecDeque; + +use roc_can::abilities::{AbilitiesStore, ImplKey}; +use roc_collections::{VecMap, VecSet}; +use roc_debug_flags::dbg_do; +#[cfg(debug_assertions)] +use roc_debug_flags::ROC_TRACE_COMPACTION; +use roc_derive_key::{DeriveError, DeriveKey}; +use roc_error_macros::{internal_error, todo_abilities}; +use roc_module::symbol::{ModuleId, Symbol}; +use roc_solve_schema::UnificationMode; +use roc_types::{ + subs::{ + get_member_lambda_sets_at_region, Content, Descriptor, GetSubsSlice, LambdaSet, Mark, + OptVariable, Rank, Subs, SubsSlice, UlsOfVar, Variable, + }, + types::{AliasKind, MemberImpl, Polarity, Uls}, +}; +use roc_unify::unify::{unify, MustImplementConstraints}; + +use crate::{ + ability::builtin_module_with_unlisted_ability_impl, + deep_copy::deep_copy_var_in, + env::{DerivedEnv, SolveEnv}, +}; + +/// What phase in the compiler is reaching out to specialize lambda sets? +/// This is important to distinguish subtle differences in the behavior of the solving algorithm. +// +// TODO the APIs of this trait suck, this needs a nice cleanup. +pub trait Phase { + /// The regular type-solving phase, or during some later phase of compilation. + /// During the solving phase we must anticipate that some information is still unknown and react to + /// that; during late phases, we expect that all information is resolved. + const IS_LATE: bool; + + fn with_module_abilities_store(&self, module: ModuleId, f: F) -> T + where + F: FnMut(&AbilitiesStore) -> T; + + /// Given a known lambda set's ambient function in an external module, copy that ambient + /// function into the given subs. + fn copy_lambda_set_ambient_function_to_home_subs( + &self, + external_lambda_set_var: Variable, + external_module_id: ModuleId, + home_subs: &mut Subs, + ) -> Variable; + + /// Find the ambient function var at a given region for an ability member definition (not a + /// specialization!), and copy that into the given subs. + fn get_and_copy_ability_member_ambient_function( + &self, + ability_member: Symbol, + region: u8, + home_subs: &mut Subs, + ) -> Variable; +} + +pub(crate) struct SolvePhase<'a> { + pub abilities_store: &'a AbilitiesStore, +} +impl Phase for SolvePhase<'_> { + const IS_LATE: bool = false; + + fn with_module_abilities_store(&self, _module: ModuleId, mut f: F) -> T + where + F: FnMut(&AbilitiesStore) -> T, + { + // During solving we're only aware of our module's abilities store. + f(self.abilities_store) + } + + fn copy_lambda_set_ambient_function_to_home_subs( + &self, + external_lambda_set_var: Variable, + _external_module_id: ModuleId, + home_subs: &mut Subs, + ) -> Variable { + // During solving we're only aware of our module's abilities store, the var must + // be in our module store. Even if the specialization lambda set comes from another + // module, we should have taken care to import it before starting solving in this module. + let LambdaSet { + ambient_function, .. + } = home_subs.get_lambda_set(external_lambda_set_var); + ambient_function + } + + fn get_and_copy_ability_member_ambient_function( + &self, + ability_member: Symbol, + region: u8, + home_subs: &mut Subs, + ) -> Variable { + // During solving we're only aware of our module's abilities store, the var must + // be in our module store. Even if the specialization lambda set comes from another + // module, we should have taken care to import it before starting solving in this module. + let member_def = self + .abilities_store + .member_def(ability_member) + .unwrap_or_else(|| { + internal_error!( + "{:?} is not resolved, or not an ability member!", + ability_member + ) + }); + let member_var = member_def.signature_var(); + + let region_lset = get_member_lambda_sets_at_region(home_subs, member_var, region); + + let LambdaSet { + ambient_function, .. + } = home_subs.get_lambda_set(region_lset); + + ambient_function + } +} + +#[derive(Default)] +pub struct AwaitingSpecializations { + // What variables' specialized lambda sets in `uls_of_var` will be unlocked for specialization + // when an implementation key's specialization is resolved? + waiting: VecMap>, + uls_of_var: UlsOfVar, +} + +impl AwaitingSpecializations { + pub fn remove_for_specialized(&mut self, subs: &Subs, impl_key: ImplKey) -> UlsOfVar { + let spec_variables = self + .waiting + .remove(&impl_key) + .map(|(_, set)| set) + .unwrap_or_default(); + + let mut result = UlsOfVar::default(); + for var in spec_variables { + let target_lambda_sets = self + .uls_of_var + .remove_dependent_unspecialized_lambda_sets(subs, var); + + result.extend(var, target_lambda_sets); + } + result + } + + pub fn add( + &mut self, + impl_key: ImplKey, + var: Variable, + lambda_sets: impl IntoIterator, + ) { + self.uls_of_var.extend(var, lambda_sets); + let waiting = self.waiting.get_or_insert(impl_key, Default::default); + waiting.insert(var); + } + + pub fn union(&mut self, other: Self) { + for (impl_key, waiting_vars) in other.waiting { + let waiting = self.waiting.get_or_insert(impl_key, Default::default); + waiting.extend(waiting_vars); + } + self.uls_of_var.union(other.uls_of_var); + } + + pub fn waiting_for(&self, impl_key: ImplKey) -> bool { + self.waiting.contains_key(&impl_key) + } +} + +pub struct CompactionResult { + pub obligations: MustImplementConstraints, + pub awaiting_specialization: AwaitingSpecializations, +} + +#[cfg(debug_assertions)] +fn trace_compaction_step_1(subs: &Subs, c_a: Variable, uls_a: &[Variable]) { + let c_a = roc_types::subs::SubsFmtContent(subs.get_content_without_compacting(c_a), subs); + let uls_a = uls_a + .iter() + .map(|v| { + format!( + "{:?}", + roc_types::subs::SubsFmtContent(subs.get_content_without_compacting(*v), subs) + ) + }) + .collect::>() + .join(","); + eprintln!("===lambda set compaction==="); + eprintln!(" concrete type: {c_a:?}"); + eprintln!(" step 1:"); + eprintln!(" uls_a = {{ {uls_a} }}"); +} + +#[cfg(debug_assertions)] +fn trace_compaction_step_2(subs: &Subs, uls_a: &[Variable]) { + let uls_a = uls_a + .iter() + .map(|v| { + format!( + "{:?}", + roc_types::subs::SubsFmtContent(subs.get_content_without_compacting(*v), subs) + ) + }) + .collect::>() + .join(","); + eprintln!(" step 2:"); + eprintln!(" uls_a' = {{ {uls_a} }}"); +} + +#[cfg(debug_assertions)] +fn trace_compaction_step_3start() { + eprintln!(" step 3:"); +} + +#[cfg(debug_assertions)] +fn trace_compaction_step_3iter_start( + subs: &Subs, + iteration_lambda_set: Variable, + t_f1: Variable, + t_f2: Variable, +) { + let iteration_lambda_set = roc_types::subs::SubsFmtContent( + subs.get_content_without_compacting(iteration_lambda_set), + subs, + ); + let t_f1 = roc_types::subs::SubsFmtContent(subs.get_content_without_compacting(t_f1), subs); + let t_f2 = roc_types::subs::SubsFmtContent(subs.get_content_without_compacting(t_f2), subs); + eprintln!(" - iteration: {iteration_lambda_set:?}"); + eprintln!(" {t_f1:?}"); + eprintln!(" ~ {t_f2:?}"); +} + +#[cfg(debug_assertions)] +#[rustfmt::skip] +fn trace_compaction_step_3iter_end(subs: &Subs, t_f_result: Variable, skipped: bool) { + let t_f_result = + roc_types::subs::SubsFmtContent(subs.get_content_without_compacting(t_f_result), subs); + if skipped { + eprintln!(" SKIP"); + } + eprintln!(" = {t_f_result:?}\n"); +} + +macro_rules! trace_compact { + (1. $subs:expr, $c_a:expr, $uls_a:expr) => {{ + dbg_do!(ROC_TRACE_COMPACTION, { + trace_compaction_step_1($subs, $c_a, $uls_a) + }) + }}; + (2. $subs:expr, $uls_a:expr) => {{ + dbg_do!(ROC_TRACE_COMPACTION, { + trace_compaction_step_2($subs, $uls_a) + }) + }}; + (3start.) => {{ + dbg_do!(ROC_TRACE_COMPACTION, { trace_compaction_step_3start() }) + }}; + (3iter_start. $subs:expr, $iteration_lset:expr, $t_f1:expr, $t_f2:expr) => {{ + dbg_do!(ROC_TRACE_COMPACTION, { + trace_compaction_step_3iter_start($subs, $iteration_lset, $t_f1, $t_f2) + }) + }}; + (3iter_end. $subs:expr, $t_f_result:expr) => {{ + dbg_do!(ROC_TRACE_COMPACTION, { + trace_compaction_step_3iter_end($subs, $t_f_result, false) + }) + }}; + (3iter_end_skipped. $subs:expr, $t_f_result:expr) => {{ + dbg_do!(ROC_TRACE_COMPACTION, { + trace_compaction_step_3iter_end($subs, $t_f_result, true) + }) + }}; +} + +#[inline(always)] +fn iter_concrete_of_unspecialized<'a>( + subs: &'a Subs, + c_a: Variable, + uls: &'a [Uls], +) -> impl Iterator { + uls.iter() + .filter(move |Uls(var, _, _)| subs.equivalent_without_compacting(*var, c_a)) +} + +/// Gets the unique unspecialized lambda resolving to concrete type `c_a` in a list of +/// unspecialized lambda sets. +#[inline(always)] +fn unique_unspecialized_lambda(subs: &Subs, c_a: Variable, uls: &[Uls]) -> Option { + let mut iter_concrete = iter_concrete_of_unspecialized(subs, c_a, uls); + let uls = iter_concrete.next()?; + debug_assert!(iter_concrete.next().is_none(), "multiple concrete"); + Some(*uls) +} + +#[must_use] +pub fn compact_lambda_sets_of_vars( + env: &mut SolveEnv, + uls_of_var: UlsOfVar, + phase: &P, +) -> CompactionResult { + let mut must_implement = MustImplementConstraints::default(); + let mut awaiting_specialization = AwaitingSpecializations::default(); + + let mut uls_of_var_queue = VecDeque::with_capacity(uls_of_var.len()); + uls_of_var_queue.extend(uls_of_var.drain()); + + // Suppose a type variable `a` with `uls_of_var` mapping `uls_a = {l1, ... ln}` has been instantiated to a concrete type `C_a`. + while let Some((c_a, uls_a)) = uls_of_var_queue.pop_front() { + let c_a = env.subs.get_root_key_without_compacting(c_a); + // 1. Let each `l` in `uls_a` be of form `[solved_lambdas + ... + C:f:r + ...]`. + // NB: There may be multiple unspecialized lambdas of form `C:f:r, C:f1:r1, ..., C:fn:rn` in `l`. + // In this case, let `t1, ... tm` be the other unspecialized lambdas not of form `C:_:_`, + // that is, none of which are now specialized to the type `C`. Then, deconstruct + // `l` such that `l' = [solved_lambdas + t1 + ... + tm + C:f:r]` and `l1 = [[] + C:f1:r1], ..., ln = [[] + C:fn:rn]`. + // Replace `l` with `l', l1, ..., ln` in `uls_a`, flattened. + // TODO: the flattening step described above + let uls_a = { + let mut uls = uls_a.into_vec(); + + // De-duplicate lambdas by root key. + uls.iter_mut().for_each(|v| *v = env.subs.get_root_key(*v)); + uls.sort(); + uls.dedup(); + uls + }; + + trace_compact!(1. env.subs, c_a, &uls_a); + + // The flattening step - remove lambda sets that don't reference the concrete var, and for + // flatten lambda sets that reference it more than once. + let mut uls_a: Vec<_> = uls_a + .into_iter() + .flat_map(|lambda_set| { + let LambdaSet { + solved, + recursion_var, + unspecialized, + ambient_function, + } = env.subs.get_lambda_set(lambda_set); + let lambda_set_rank = env.subs.get_rank(lambda_set); + let unspecialized = env.subs.get_subs_slice(unspecialized); + // TODO: is it faster to traverse once, see if we only have one concrete lambda, and + // bail in that happy-path, rather than always splitting? + let (concrete, mut not_concrete): (Vec<_>, Vec<_>) = unspecialized + .iter() + .copied() + .partition(|Uls(var, _, _)| env.subs.equivalent_without_compacting(*var, c_a)); + if concrete.len() == 1 { + // No flattening needs to be done, just return the lambda set as-is + return vec![lambda_set]; + } + // Must flatten + concrete + .into_iter() + .enumerate() + .map(|(i, concrete_lambda)| { + let (var, unspecialized) = if i == 0 { + // The first lambda set contains one concrete lambda, plus all solved + // lambdas, plus all other unspecialized lambdas. + // l' = [solved_lambdas + t1 + ... + tm + C:f:r] + let unspecialized = SubsSlice::extend_new( + &mut env.subs.unspecialized_lambda_sets, + not_concrete + .drain(..) + .chain(std::iter::once(concrete_lambda)), + ); + (lambda_set, unspecialized) + } else { + // All the other lambda sets consists only of their respective concrete + // lambdas. + // ln = [[] + C:fn:rn] + let unspecialized = SubsSlice::extend_new( + &mut env.subs.unspecialized_lambda_sets, + [concrete_lambda], + ); + let var = env.subs.fresh(Descriptor { + content: Content::Error, + rank: lambda_set_rank, + mark: Mark::NONE, + copy: OptVariable::NONE, + }); + (var, unspecialized) + }; + + env.subs.set_content( + var, + Content::LambdaSet(LambdaSet { + solved, + recursion_var, + unspecialized, + ambient_function, + }), + ); + var + }) + .collect() + }) + .collect(); + + // 2. Now, each `l` in `uls_a` has a unique unspecialized lambda of form `C:f:r`. + // Sort `uls_a` primarily by `f` (arbitrary order), and secondarily by `r` in descending order. + uls_a.sort_by(|v1, v2| { + let unspec_1 = env + .subs + .get_subs_slice(env.subs.get_lambda_set(*v1).unspecialized); + let unspec_2 = env + .subs + .get_subs_slice(env.subs.get_lambda_set(*v2).unspecialized); + + let Uls(_, f1, r1) = unique_unspecialized_lambda(env.subs, c_a, unspec_1).unwrap(); + let Uls(_, f2, r2) = unique_unspecialized_lambda(env.subs, c_a, unspec_2).unwrap(); + + match f1.cmp(&f2) { + std::cmp::Ordering::Equal => { + // Order by descending order of region. + r2.cmp(&r1) + } + ord => ord, + } + }); + + trace_compact!(2. env.subs, &uls_a); + + // 3. For each `l` in `uls_a` with unique unspecialized lambda `C:f:r`: + // 1. Let `t_f1` be the directly ambient function of the lambda set containing `C:f:r`. Remove `C:f:r` from `t_f1`'s lambda set. + // - For example, `(b' -[[] + Fo:f:2]-> {})` if `C:f:r=Fo:f:2`. Removing `Fo:f:2`, we get `(b' -[[]]-> {})`. + // 2. Let `t_f2` be the directly ambient function of the specialization lambda set resolved by `C:f:r`. + // - For example, `(b -[[] + b:g:1]-> {})` if `C:f:r=Fo:f:2`, running on example from above. + // 3. Unify `t_f1 ~ t_f2`. + trace_compact!(3start.); + for l in uls_a { + let compaction_result = compact_lambda_set(env, c_a, l, phase); + + match compaction_result { + OneCompactionResult::Compacted { + new_obligations, + new_lambda_sets_to_specialize, + } => { + must_implement.extend(new_obligations); + uls_of_var_queue.extend(new_lambda_sets_to_specialize.drain()); + } + OneCompactionResult::MustWaitForSpecialization(impl_key) => { + awaiting_specialization.add(impl_key, c_a, [l]) + } + } + } + } + + CompactionResult { + obligations: must_implement, + awaiting_specialization, + } +} + +enum OneCompactionResult { + Compacted { + new_obligations: MustImplementConstraints, + new_lambda_sets_to_specialize: UlsOfVar, + }, + MustWaitForSpecialization(ImplKey), +} + +#[must_use] +#[allow(clippy::too_many_arguments)] +fn compact_lambda_set( + env: &mut SolveEnv, + resolved_concrete: Variable, + this_lambda_set: Variable, + phase: &P, +) -> OneCompactionResult { + // 3. For each `l` in `uls_a` with unique unspecialized lambda `C:f:r`: + // 1. Let `t_f1` be the directly ambient function of the lambda set containing `C:f:r`. Remove `C:f:r` from `t_f1`'s lambda set. + // - For example, `(b' -[[] + Fo:f:2]-> {})` if `C:f:r=Fo:f:2`. Removing `Fo:f:2`, we get `(b' -[[]]-> {})`. + // 2. Let `t_f2` be the directly ambient function of the specialization lambda set resolved by `C:f:r`. + // - For example, `(b -[[] + b:g:1]-> {})` if `C:f:r=Fo:f:2`, from the algorithm's running example. + // 3. Unify `t_f1 ~ t_f2`. + let LambdaSet { + solved, + recursion_var, + unspecialized, + ambient_function: t_f1, + } = env.subs.get_lambda_set(this_lambda_set); + let target_rank = env.subs.get_rank(this_lambda_set); + + debug_assert!(!unspecialized.is_empty()); + + let unspecialized = env.subs.get_subs_slice(unspecialized); + + // 1. Let `t_f1` be the directly ambient function of the lambda set containing `C:f:r`. + let Uls(c, f, r) = + unique_unspecialized_lambda(env.subs, resolved_concrete, unspecialized).unwrap(); + + debug_assert!(env.subs.equivalent_without_compacting(c, resolved_concrete)); + + // Now decide: do we + // - proceed with specialization + // - simply drop the specialization lambda set (due to an error) + // - or do we need to wait, because we don't know enough information for the specialization yet? + let specialization_decision = make_specialization_decision(env.subs, phase, c, f); + let specialization_key_or_drop = match specialization_decision { + SpecializeDecision::Specialize(key) => Ok(key), + SpecializeDecision::Drop => Err(()), + SpecializeDecision::PendingSpecialization(impl_key) => { + // Bail, we need to wait for the specialization to be known. + return OneCompactionResult::MustWaitForSpecialization(impl_key); + } + }; + + // 1b. Remove `C:f:r` from `t_f1`'s lambda set. + let new_unspecialized: Vec<_> = unspecialized + .iter() + .filter(|Uls(v, _, _)| { + !env.subs + .equivalent_without_compacting(*v, resolved_concrete) + }) + .copied() + .collect(); + debug_assert_eq!(new_unspecialized.len(), unspecialized.len() - 1); + let t_f1_lambda_set_without_concrete = LambdaSet { + solved, + recursion_var, + unspecialized: SubsSlice::extend_new( + &mut env.subs.unspecialized_lambda_sets, + new_unspecialized, + ), + ambient_function: t_f1, + }; + env.subs.set_content( + this_lambda_set, + Content::LambdaSet(t_f1_lambda_set_without_concrete), + ); + + let specialization_key = match specialization_key_or_drop { + Ok(specialization_key) => specialization_key, + Err(()) => { + // Do nothing other than to remove the concrete lambda to drop from the lambda set, + // which we already did in 1b above. + trace_compact!(3iter_end_skipped.env.subs, t_f1); + return OneCompactionResult::Compacted { + new_obligations: Default::default(), + new_lambda_sets_to_specialize: Default::default(), + }; + } + }; + + let specialization_ambient_function_var = get_specialization_lambda_set_ambient_function( + env.subs, + env.derived_env, + phase, + f, + r, + specialization_key, + target_rank, + ); + + let t_f2 = match specialization_ambient_function_var { + Ok(lset) => lset, + Err(()) => { + // Do nothing other than to remove the concrete lambda to drop from the lambda set, + // which we already did in 1b above. + trace_compact!(3iter_end_skipped.env.subs, t_f1); + return OneCompactionResult::Compacted { + new_obligations: Default::default(), + new_lambda_sets_to_specialize: Default::default(), + }; + } + }; + + // Ensure the specialized ambient function we'll unify with is not a generalized one, but one + // at the rank of the lambda set being compacted. + let t_f2 = deep_copy_var_in(env, target_rank, t_f2, env.arena); + + // 3. Unify `t_f1 ~ t_f2`. + trace_compact!(3iter_start.env.subs, this_lambda_set, t_f1, t_f2); + let (vars, new_obligations, new_lambda_sets_to_specialize, _meta) = unify( + &mut env.uenv(), + t_f1, + t_f2, + UnificationMode::LAMBDA_SET_SPECIALIZATION, + Polarity::Pos, + ) + .expect_success("ambient functions don't unify"); + trace_compact!(3iter_end.env.subs, t_f1); + + env.introduce(target_rank, &vars); + + OneCompactionResult::Compacted { + new_obligations, + new_lambda_sets_to_specialize, + } +} + +#[derive(Debug)] +enum SpecializationTypeKey { + Opaque(Symbol), + Derived(DeriveKey), + Immediate(Symbol), + SingleLambdaSetImmediate(Symbol), +} + +#[derive(Debug)] +enum SpecializeDecision { + Specialize(SpecializationTypeKey), + Drop, + + /// Only relevant during module solving of recursive defs - we don't yet know the + /// specialization type for a declared ability implementation, so we must hold off on + /// specialization. + PendingSpecialization(ImplKey), +} + +fn make_specialization_decision( + subs: &Subs, + phase: &P, + var: Variable, + ability_member: Symbol, +) -> SpecializeDecision { + use Content::*; + use SpecializationTypeKey::*; + match subs.get_content_without_compacting(var) { + Alias(opaque, _, _, AliasKind::Opaque) + if !builtin_module_with_unlisted_ability_impl(opaque.module_id()) => + { + if P::IS_LATE { + SpecializeDecision::Specialize(Opaque(*opaque)) + } else { + // Solving within a module. + phase.with_module_abilities_store(opaque.module_id(), |abilities_store| { + make_ability_specialization_decision(*opaque, ability_member, abilities_store) + }) + } + } + Structure(_) | Alias(_, _, _, _) | RecursionVar { .. } => { + let builtin = match ability_member.try_into() { + Ok(builtin) => builtin, + Err(_) => return SpecializeDecision::Drop, + }; + + // This is a structural type, find the derived ability function it should use. + match roc_derive_key::Derived::builtin(builtin, subs, var) { + Ok(derived) => match derived { + roc_derive_key::Derived::Immediate(imm) => { + SpecializeDecision::Specialize(Immediate(imm)) + } + roc_derive_key::Derived::SingleLambdaSetImmediate(imm) => { + SpecializeDecision::Specialize(SingleLambdaSetImmediate(imm)) + } + roc_derive_key::Derived::Key(derive_key) => { + SpecializeDecision::Specialize(Derived(derive_key)) + } + }, + Err(DeriveError::UnboundVar) => { + // not specialized yet, but that also means that it can't possibly be derivable + // at this point? + // TODO: is this right? Revisit if it causes us problems in the future. + SpecializeDecision::Drop + } + Err(DeriveError::Underivable) => { + // we should have reported an error for this; drop the lambda set. + SpecializeDecision::Drop + } + } + } + Error => SpecializeDecision::Drop, + FlexAbleVar(_, _) + | RigidAbleVar(..) + | FlexVar(..) + | RigidVar(..) + | LambdaSet(..) + | ErasedLambda + | RangedNumber(..) => { + internal_error!("unexpected") + } + } +} + +fn make_ability_specialization_decision( + opaque: Symbol, + ability_member: Symbol, + abilities_store: &AbilitiesStore, +) -> SpecializeDecision { + use SpecializationTypeKey::*; + let impl_key = ImplKey { + opaque, + ability_member, + }; + match abilities_store.get_implementation(impl_key) { + None => { + match ability_member { + // Inspect is special - if there is no implementation for the + // opaque type, we always emit a default implementation. + Symbol::INSPECT_TO_INSPECTOR => { + SpecializeDecision::Specialize(Immediate(Symbol::INSPECT_OPAQUE)) + } + _ => { + // Doesn't specialize; an error will already be reported for this. + SpecializeDecision::Drop + } + } + } + Some(MemberImpl::Error) => { + // TODO: probably not right, we may want to choose a derive decision! + SpecializeDecision::Specialize(Opaque(opaque)) + } + Some(MemberImpl::Impl(specialization_symbol)) => { + match abilities_store.specialization_info(*specialization_symbol) { + Some(_) => SpecializeDecision::Specialize(Opaque(opaque)), + + // If we expect a specialization impl but don't yet know it, we must hold off + // compacting the lambda set until the specialization is well-known. + None => SpecializeDecision::PendingSpecialization(impl_key), + } + } + } +} + +#[allow(clippy::too_many_arguments)] +fn get_specialization_lambda_set_ambient_function( + subs: &mut Subs, + derived_env: &DerivedEnv, + phase: &P, + ability_member: Symbol, + lset_region: u8, + mut specialization_key: SpecializationTypeKey, + target_rank: Rank, +) -> Result { + loop { + match specialization_key { + SpecializationTypeKey::Opaque(opaque) => { + let opaque_home = opaque.module_id(); + let found = phase.with_module_abilities_store(opaque_home, |abilities_store| { + find_opaque_specialization_ambient_function( + abilities_store, + opaque, + ability_member, + lset_region, + ) + }); + + let external_specialized_lset = match found { + FoundOpaqueSpecialization::UpdatedSpecializationKey(key) => { + specialization_key = key; + continue; + } + FoundOpaqueSpecialization::AmbientFunction(lset) => lset, + FoundOpaqueSpecialization::NotFound => { + if P::IS_LATE { + internal_error!( + "expected to know a specialization for {:?}#{:?}, but it wasn't found", + opaque, + ability_member + ); + } else { + // We'll have reported an error for this. + return Err(()); + } + } + }; + + let specialized_ambient = phase.copy_lambda_set_ambient_function_to_home_subs( + external_specialized_lset, + opaque_home, + subs, + ); + + return Ok(specialized_ambient); + } + + SpecializationTypeKey::Derived(derive_key) => { + let mut derived_module = derived_env.derived_module.lock().unwrap(); + + let (_, _, specialization_lambda_sets) = + derived_module.get_or_insert(derived_env.exposed_types, derive_key); + + let specialized_lambda_set = *specialization_lambda_sets + .get(&lset_region) + .expect("lambda set region not resolved"); + + let specialized_ambient = derived_module.copy_lambda_set_ambient_function_to_subs( + specialized_lambda_set, + subs, + target_rank, + ); + + return Ok(specialized_ambient); + } + + SpecializationTypeKey::Immediate(imm) => { + // Immediates are like opaques in that we can simply look up their type definition in + // the ability store, there is nothing new to synthesize. + // + // THEORY: if something can become an immediate, it will always be available in the + // local ability store, because the transformation is local (?) + // + // TODO: I actually think we can get what we need here by examining `derived_env.exposed_types`, + // since immediates can only refer to builtins - and in userspace, all builtin types + // are available in `exposed_types`. + let immediate_lambda_set_at_region = + phase.get_and_copy_ability_member_ambient_function(imm, lset_region, subs); + + return Ok(immediate_lambda_set_at_region); + } + + SpecializationTypeKey::SingleLambdaSetImmediate(imm) => { + let module_id = imm.module_id(); + debug_assert!(module_id.is_builtin()); + + let module_types = &derived_env + .exposed_types + .get(&module_id) + .unwrap() + .exposed_types_storage_subs; + + // Since this immediate has only one lambda set, the region must be pointing to 1, and + // moreover the imported function type is the ambient function of the single lset. + debug_assert_eq!(lset_region, 1); + let storage_var = module_types.stored_vars_by_symbol.get(&imm).unwrap(); + let imported = module_types + .storage_subs + .export_variable_to(subs, *storage_var); + + roc_types::subs::instantiate_rigids(subs, imported.variable); + + return Ok(imported.variable); + } + } + } +} + +enum FoundOpaqueSpecialization { + UpdatedSpecializationKey(SpecializationTypeKey), + AmbientFunction(Variable), + NotFound, +} + +fn find_opaque_specialization_ambient_function( + abilities_store: &AbilitiesStore, + opaque: Symbol, + ability_member: Symbol, + lset_region: u8, +) -> FoundOpaqueSpecialization { + let impl_key = roc_can::abilities::ImplKey { + opaque, + ability_member, + }; + + let opt_specialization = abilities_store.get_implementation(impl_key); + match opt_specialization { + None => match ability_member { + Symbol::INSPECT_TO_INSPECTOR => FoundOpaqueSpecialization::UpdatedSpecializationKey( + SpecializationTypeKey::Immediate(Symbol::INSPECT_OPAQUE), + ), + _ => FoundOpaqueSpecialization::NotFound, + }, + Some(member_impl) => match member_impl { + MemberImpl::Impl(spec_symbol) => { + let specialization = + abilities_store.specialization_info(*spec_symbol).expect("expected custom implementations to always have complete specialization info by this point"); + + let specialized_lambda_set = *specialization + .specialization_lambda_sets + .get(&lset_region) + .unwrap_or_else(|| { + panic!( + "lambda set region not resolved: {:?}", + (spec_symbol, specialization) + ) + }); + + FoundOpaqueSpecialization::AmbientFunction(specialized_lambda_set) + } + MemberImpl::Error => todo_abilities!(), + }, + } +} diff --git a/crates/compiler/solve/src/to_var.rs b/crates/compiler/solve/src/to_var.rs new file mode 100644 index 0000000000..86e4dc9cfa --- /dev/null +++ b/crates/compiler/solve/src/to_var.rs @@ -0,0 +1,1216 @@ +use std::cell::RefCell; + +use roc_can::{abilities::AbilitiesStore, constraint::TypeOrVar, expected::Expected}; +use roc_collections::soa::{Index, Slice}; +use roc_error_macros::internal_error; +use roc_module::{ident::TagName, symbol::Symbol}; +use roc_region::all::Loc; +use roc_solve_problem::TypeError; +use roc_solve_schema::UnificationMode; +use roc_types::{ + subs::{ + self, AliasVariables, Content, FlatType, GetSubsSlice, LambdaSet, OptVariable, Rank, + RecordFields, Subs, SubsSlice, TagExt, TupleElems, UnionLabels, UnionLambdas, UnionTags, + Variable, VariableSubsSlice, + }, + types::{ + gather_fields_unsorted_iter, gather_tuple_elems_unsorted_iter, AliasKind, AliasShared, + Category, ExtImplicitOpenness, Polarity, TypeTag, Types, + }, +}; +use roc_unify::unify::{unify, Unified}; + +use crate::{ + ability::{AbilityImplError, ObligationCache}, + deep_copy::deep_copy_var_in, + env::InferenceEnv, + Aliases, FunctionKind, +}; + +std::thread_local! { + /// Scratchpad arena so we don't need to allocate a new one all the time + static SCRATCHPAD: RefCell> = RefCell::new(Some(bumpalo::Bump::with_capacity(4 * 1024))); +} + +fn take_scratchpad() -> bumpalo::Bump { + SCRATCHPAD.with(|f| f.take().unwrap()) +} + +fn put_scratchpad(scratchpad: bumpalo::Bump) { + SCRATCHPAD.with(|f| { + f.replace(Some(scratchpad)); + }); +} + +pub(crate) fn either_type_index_to_var( + env: &mut InferenceEnv, + rank: Rank, + problems: &mut Vec, + abilities_store: &mut AbilitiesStore, + obligation_cache: &mut ObligationCache, + types: &mut Types, + aliases: &mut Aliases, + either_type_index: TypeOrVar, +) -> Variable { + match either_type_index.split() { + Ok(type_index) => { + // Converts the celled type to a variable, emplacing the new variable for re-use. + let var = type_to_var( + env, + rank, + problems, + abilities_store, + obligation_cache, + types, + aliases, + type_index, + ); + + debug_assert!( + matches!(types[type_index], TypeTag::Variable(v) if v == var) + || matches!( + types[type_index], + TypeTag::EmptyRecord | TypeTag::EmptyTagUnion + ), + "different variable was returned for type index variable cell!" + ); + var + } + Err(var_index) => { + // we cheat, and store the variable directly in the index + unsafe { Variable::from_index(var_index.index() as _) } + } + } +} + +pub fn type_to_var( + env: &mut InferenceEnv, + rank: Rank, + problems: &mut Vec, + abilities_store: &mut AbilitiesStore, + obligation_cache: &mut ObligationCache, + types: &mut Types, + aliases: &mut Aliases, + typ: Index, +) -> Variable { + if let TypeTag::Variable(var) = types[typ] { + var + } else { + let mut arena = take_scratchpad(); + + let var = type_to_var_help( + env, + rank, + problems, + abilities_store, + obligation_cache, + &arena, + aliases, + types, + typ, + false, + ); + + arena.reset(); + put_scratchpad(arena); + + var + } +} + +enum RegisterVariable { + /// Based on the Type, we already know what variable this will be + Direct(Variable), + /// This Type needs more complicated Content. We reserve a Variable + /// for it, but put a placeholder Content in subs + Deferred, +} + +impl RegisterVariable { + fn from_type( + env: &mut InferenceEnv, + rank: Rank, + arena: &'_ bumpalo::Bump, + types: &mut Types, + typ: Index, + ) -> Self { + use RegisterVariable::*; + + match types[typ] { + TypeTag::Variable(var) => Direct(var), + TypeTag::EmptyRecord => Direct(Variable::EMPTY_RECORD), + TypeTag::EmptyTagUnion => Direct(Variable::EMPTY_TAG_UNION), + TypeTag::DelayedAlias { shared } + | TypeTag::StructuralAlias { shared, .. } + | TypeTag::OpaqueAlias { shared, .. } => { + let AliasShared { symbol, .. } = types[shared]; + if let Some(reserved) = Variable::get_reserved(symbol) { + let direct_var = if rank.is_generalized() { + // reserved variables are stored with rank NONE + reserved + } else { + // for any other rank, we need to copy; it takes care of adjusting the rank + deep_copy_var_in(&mut env.as_solve_env(), rank, reserved, arena) + }; + // Safety: the `destination` will become the source-of-truth for the type index, since it + // was not already transformed before (if it was, we'd be in the Variable branch!) + let _old_typ = unsafe { types.emplace_variable(typ, direct_var) }; + return Direct(direct_var); + } + + Deferred + } + _ => Deferred, + } + } + + #[inline(always)] + fn with_stack( + env: &mut InferenceEnv, + rank: Rank, + arena: &'_ bumpalo::Bump, + types: &mut Types, + typ_index: Index, + stack: &mut bumpalo::collections::Vec<'_, TypeToVar>, + ) -> Variable { + match Self::from_type(env, rank, arena, types, typ_index) { + Self::Direct(var) => var, + Self::Deferred => { + let var = env.subs.fresh_unnamed_flex_var(); + // Safety: the `destination` will become the source-of-truth for the type index, since it + // was not already transformed before (if it was, it wouldn't be deferred!) + let typ = unsafe { types.emplace_variable(typ_index, var) }; + stack.push(TypeToVar::Defer { + typ, + typ_index, + destination: var, + ambient_function: AmbientFunctionPolicy::NoFunction, + }); + var + } + } + } +} + +/// Instantiation of ambient functions in unspecialized lambda sets is somewhat tricky due to other +/// optimizations we have in place. This struct tells us how they should be instantiated. +#[derive(Debug)] +enum AmbientFunctionPolicy { + /// We're not in a function. This variant may never hold for unspecialized lambda sets. + NoFunction, + /// We're in a known function. + Function(Variable), +} + +impl AmbientFunctionPolicy { + fn link_to_alias_lambda_set_var(&self, subs: &mut Subs, var: Variable) { + let ambient_function = match self { + AmbientFunctionPolicy::Function(var) => *var, + _ => { + // Might be linked at a deeper point in time, ignore for now + return; + } + }; + let content = subs.get_content_without_compacting(var); + let new_content = match content { + Content::LambdaSet(LambdaSet { + solved, + recursion_var, + unspecialized, + ambient_function: _, + }) => Content::LambdaSet(LambdaSet { + solved: *solved, + recursion_var: *recursion_var, + unspecialized: *unspecialized, + ambient_function, + }), + Content::FlexVar(_) => { + // Something like + // Encoder fmt : List U8, fmt -a-> List U8 | fmt has EncoderFormatting + // THEORY: Replace these with empty lambda sets. They will unify the same as a flex + // var does, but allows us to record the ambient function properly. + Content::LambdaSet(LambdaSet { + solved: UnionLabels::default(), + recursion_var: OptVariable::NONE, + unspecialized: SubsSlice::default(), + ambient_function, + }) + } + content => internal_error!("{:?}({:?}) not a lambda set", content, var), + }; + subs.set_content_unchecked(var, new_content); + } +} + +#[derive(Debug)] +enum TypeToVar { + Defer { + typ: TypeTag, + typ_index: Index, + destination: Variable, + ambient_function: AmbientFunctionPolicy, + }, +} + +#[allow(clippy::too_many_arguments)] +pub(crate) fn type_to_var_help( + env: &mut InferenceEnv, + rank: Rank, + problems: &mut Vec, + abilities_store: &AbilitiesStore, + obligation_cache: &mut ObligationCache, + arena: &bumpalo::Bump, + aliases: &mut Aliases, + types: &mut Types, + typ: Index, + // Helpers for instantiating ambient functions of lambda set variables from type aliases. + is_alias_lambda_set_arg: bool, +) -> Variable { + use bumpalo::collections::Vec; + + let mut stack = Vec::with_capacity_in(8, arena); + let mut bind_to_abilities = Vec::new_in(arena); + + macro_rules! helper { + ($typ:expr, $ambient_function_policy:expr) => {{ + match RegisterVariable::from_type(env, rank, arena, types, $typ) { + RegisterVariable::Direct(var) => { + // If the variable is just a type variable but we know we're in a lambda set + // context, try to link to the ambient function. + $ambient_function_policy.link_to_alias_lambda_set_var(env.subs, var); + + var + } + RegisterVariable::Deferred => { + let var = env.subs.fresh_unnamed_flex_var(); + + // Safety: the `destination` will become the source-of-truth for the type index, since it + // was not already transformed before (if it was, it wouldn't be deferred!) + let typ = unsafe { types.emplace_variable($typ, var) }; + + stack.push(TypeToVar::Defer { + typ, + typ_index: $typ, + destination: var, + ambient_function: $ambient_function_policy, + }); + + var + } + } + }}; + ($typ:expr) => {{ + helper!($typ, AmbientFunctionPolicy::NoFunction) + }}; + } + + let result = helper!(typ); + + while let Some(TypeToVar::Defer { + typ_index, + typ, + destination, + ambient_function, + }) = stack.pop() + { + use TypeTag::*; + match typ { + Variable(_) | EmptyRecord | EmptyTagUnion => { + unreachable!("This variant should never be deferred!",) + } + RangedNumber(range) => { + let content = Content::RangedNumber(range); + + env.register_with_known_var(destination, rank, content) + } + Apply { + symbol, + type_argument_regions: _, + region: _, + } => { + let arguments = types.get_type_arguments(typ_index); + let new_arguments = VariableSubsSlice::reserve_into_subs(env.subs, arguments.len()); + for (target_index, var_index) in + (new_arguments.indices()).zip(arguments.into_iter()) + { + let var = helper!(var_index); + env.subs.variables[target_index] = var; + } + + let flat_type = FlatType::Apply(symbol, new_arguments); + let content = Content::Structure(flat_type); + + env.register_with_known_var(destination, rank, content) + } + + ClosureTag { + name, + ambient_function, + } => { + match env.function_kind { + FunctionKind::LambdaSet => { + let captures = types.get_type_arguments(typ_index); + let union_lambdas = create_union_lambda( + env, rank, arena, types, name, captures, &mut stack, + ); + + let content = Content::LambdaSet(subs::LambdaSet { + solved: union_lambdas, + // We may figure out the lambda set is recursive during solving, but it never + // is to begin with. + recursion_var: OptVariable::NONE, + unspecialized: SubsSlice::default(), + ambient_function, + }); + + env.register_with_known_var(destination, rank, content) + } + FunctionKind::Erased => { + // TODO(erased-lambda): can we merge in with Variable::ERASED_LAMBDA instead? + env.register_with_known_var(destination, rank, Content::ErasedLambda) + } + } + } + UnspecializedLambdaSet { unspecialized } => { + let unspecialized_slice = SubsSlice::extend_new( + &mut env.subs.unspecialized_lambda_sets, + std::iter::once(unspecialized), + ); + + // `ClosureTag` ambient functions are resolved during constraint generation. + // But `UnspecializedLambdaSet`s can only ever live in a type signature, and don't + // correspond to a expression, so they are never constrained. + // Instead, we resolve their ambient functions during type translation, observing + // the invariant that a lambda set can only ever appear under a function type. + let ambient_function = match ambient_function { + AmbientFunctionPolicy::NoFunction => { + debug_assert!(is_alias_lambda_set_arg); + // To be filled in during delayed type alias instantiation + roc_types::subs::Variable::NULL + } + AmbientFunctionPolicy::Function(var) => var, + }; + + let content = Content::LambdaSet(subs::LambdaSet { + unspecialized: unspecialized_slice, + solved: UnionLabels::default(), + recursion_var: OptVariable::NONE, + ambient_function, + }); + + env.register_with_known_var(destination, rank, content) + } + // This case is important for the rank of boolean variables + Function(closure_type, ret_type) => { + let arguments = types.get_type_arguments(typ_index); + let new_arguments = VariableSubsSlice::reserve_into_subs(env.subs, arguments.len()); + for (target_index, var_index) in + (new_arguments.indices()).zip(arguments.into_iter()) + { + let var = helper!(var_index); + env.subs.variables[target_index] = var; + } + + let ret_var = helper!(ret_type); + let closure_var = + helper!(closure_type, AmbientFunctionPolicy::Function(destination)); + let content = + Content::Structure(FlatType::Func(new_arguments, closure_var, ret_var)); + + env.register_with_known_var(destination, rank, content) + } + Record(fields) => { + let ext_slice = types.get_type_arguments(typ_index); + + // An empty fields is inefficient (but would be correct) + // If hit, try to turn the value into an EmptyRecord in canonicalization + debug_assert!(!fields.is_empty() || !ext_slice.is_empty()); + + let mut field_vars = Vec::with_capacity_in(fields.len(), arena); + + let (fields_names, field_kinds, field_tys) = types.record_fields_slices(fields); + + for ((field, field_kind), field_type) in (fields_names.into_iter()) + .zip(field_kinds.into_iter()) + .zip(field_tys.into_iter()) + { + let field_var = { + let t = helper!(field_type); + types[field_kind].replace(t) + }; + + field_vars.push((types[field].clone(), field_var)); + } + + debug_assert!(ext_slice.len() <= 1); + let temp_ext_var = match ext_slice.into_iter().next() { + None => roc_types::subs::Variable::EMPTY_RECORD, + Some(ext) => helper!(ext), + }; + + let (it, new_ext_var) = + gather_fields_unsorted_iter(env.subs, RecordFields::empty(), temp_ext_var) + .expect("Something ended up weird in this record type"); + + let it = it + .into_iter() + .map(|(field, field_type)| (field.clone(), field_type)); + + field_vars.extend(it); + insertion_sort_by(&mut field_vars, RecordFields::compare); + + let record_fields = RecordFields::insert_into_subs(env.subs, field_vars); + + let content = Content::Structure(FlatType::Record(record_fields, new_ext_var)); + + env.register_with_known_var(destination, rank, content) + } + + Tuple(elems) => { + let ext_slice = types.get_type_arguments(typ_index); + + // Elems should never be empty; we don't support empty tuples + debug_assert!(!elems.is_empty() || !ext_slice.is_empty()); + + let mut elem_vars = Vec::with_capacity_in(elems.len(), arena); + + let (indices, elem_tys) = types.tuple_elems_slices(elems); + + for (index, elem_type) in indices.into_iter().zip(elem_tys.into_iter()) { + let elem_var = helper!(elem_type); + elem_vars.push((types[index], elem_var)); + } + + debug_assert!(ext_slice.len() <= 1); + let temp_ext_var = match ext_slice.into_iter().next() { + None => roc_types::subs::Variable::EMPTY_TUPLE, + Some(ext) => helper!(ext), + }; + + let (it, new_ext_var) = + gather_tuple_elems_unsorted_iter(env.subs, TupleElems::empty(), temp_ext_var) + .expect("Something ended up weird in this tuple type"); + + elem_vars.extend(it); + let tuple_elems = TupleElems::insert_into_subs(env.subs, elem_vars); + + let content = Content::Structure(FlatType::Tuple(tuple_elems, new_ext_var)); + + env.register_with_known_var(destination, rank, content) + } + + TagUnion(tags, ext_openness) => { + let ext_slice = types.get_type_arguments(typ_index); + + // An empty tags is inefficient (but would be correct) + // If hit, try to turn the value into an EmptyTagUnion in canonicalization + debug_assert!(!tags.is_empty() || !ext_slice.is_empty()); + + let (union_tags, ext) = type_to_union_tags( + env, + rank, + arena, + types, + tags, + ext_slice, + ext_openness, + &mut stack, + ); + let content = Content::Structure(FlatType::TagUnion(union_tags, ext)); + + env.register_with_known_var(destination, rank, content) + } + FunctionOrTagUnion(symbol, ext_openness) => { + let ext_slice = types.get_type_arguments(typ_index); + let tag_name = types.get_tag_name(&typ_index).clone(); + + debug_assert!(ext_slice.len() <= 1); + let temp_ext = match ext_slice.into_iter().next() { + Some(ext) => { + let var = helper!(ext); + TagExt::from_can(var, ext_openness) + } + None => TagExt::Any(roc_types::subs::Variable::EMPTY_TAG_UNION), + }; + + let (it, ext) = roc_types::types::gather_tags_unsorted_iter( + env.subs, + UnionTags::default(), + temp_ext, + ) + .expect("extension var could not be seen as a tag union"); + + for _ in it { + unreachable!("we assert that the ext var is empty; otherwise we'd already know it was a tag union!"); + } + + let tag_names = SubsSlice::extend_new(&mut env.subs.tag_names, [tag_name]); + let symbols = SubsSlice::extend_new(&mut env.subs.symbol_names, [symbol]); + + let content = + Content::Structure(FlatType::FunctionOrTagUnion(tag_names, symbols, ext)); + + env.register_with_known_var(destination, rank, content) + } + RecursiveTagUnion(rec_var, tags, ext_openness) => { + let ext_slice = types.get_type_arguments(typ_index); + + // An empty tags is inefficient (but would be correct) + // If hit, try to turn the value into an EmptyTagUnion in canonicalization + debug_assert!(!tags.is_empty() || !ext_slice.is_empty()); + + let (union_tags, ext) = type_to_union_tags( + env, + rank, + arena, + types, + tags, + ext_slice, + ext_openness, + &mut stack, + ); + let content = + Content::Structure(FlatType::RecursiveTagUnion(rec_var, union_tags, ext)); + + let tag_union_var = destination; + env.register_with_known_var(tag_union_var, rank, content); + + env.register_with_known_var( + rec_var, + rank, + Content::RecursionVar { + opt_name: None, + structure: tag_union_var, + }, + ); + + tag_union_var + } + + DelayedAlias { shared } => { + let AliasShared { + symbol, + type_argument_abilities, + type_argument_regions, + lambda_set_variables, + infer_ext_in_output_variables, + } = types[shared]; + + let type_arguments = types.get_type_arguments(typ_index); + + let alias_variables = { + let all_vars_length = type_arguments.len() + + lambda_set_variables.len() + + infer_ext_in_output_variables.len(); + let new_variables = + VariableSubsSlice::reserve_into_subs(env.subs, all_vars_length); + + let type_arguments_offset = 0; + let lambda_set_vars_offset = type_arguments_offset + type_arguments.len(); + let infer_ext_vars_offset = lambda_set_vars_offset + lambda_set_variables.len(); + + for (((target_index, arg_type), arg_region), abilities) in + (new_variables.indices().skip(type_arguments_offset)) + .zip(type_arguments.into_iter()) + .zip(type_argument_regions.into_iter()) + .zip(type_argument_abilities.into_iter()) + { + let copy_var = helper!(arg_type); + env.subs.variables[target_index] = copy_var; + if !types[abilities].is_empty() { + let arg_region = types[arg_region]; + bind_to_abilities.push((Loc::at(arg_region, copy_var), abilities)); + } + } + + let it = (new_variables.indices().skip(lambda_set_vars_offset)) + .zip(lambda_set_variables.into_iter()); + for (target_index, ls) in it { + // We MUST do this now, otherwise when linking the ambient function during + // instantiation of the real var, there will be nothing to link against. + let copy_var = type_to_var_help( + env, + rank, + problems, + abilities_store, + obligation_cache, + arena, + aliases, + types, + ls, + true, + ); + env.subs.variables[target_index] = copy_var; + } + + let it = (new_variables.indices().skip(infer_ext_vars_offset)) + .zip(infer_ext_in_output_variables.into_iter()); + for (target_index, ext_typ) in it { + let copy_var = helper!(ext_typ); + env.subs.variables[target_index] = copy_var; + } + + AliasVariables { + variables_start: new_variables.start, + type_variables_len: type_arguments.len() as _, + lambda_set_variables_len: lambda_set_variables.len() as _, + all_variables_len: all_vars_length as _, + } + }; + + let (alias_variable, kind) = aliases.instantiate_real_var( + env, + rank, + problems, + abilities_store, + obligation_cache, + arena, + types, + symbol, + alias_variables, + ); + + let content = Content::Alias(symbol, alias_variables, alias_variable, kind); + + env.register_with_known_var(destination, rank, content) + } + + StructuralAlias { shared, actual } | OpaqueAlias { shared, actual } => { + let kind = match typ { + StructuralAlias { .. } => AliasKind::Structural, + OpaqueAlias { .. } => AliasKind::Opaque, + _ => internal_error!(), + }; + + let AliasShared { + symbol, + type_argument_abilities, + type_argument_regions, + lambda_set_variables, + infer_ext_in_output_variables, + } = types[shared]; + + debug_assert!(roc_types::subs::Variable::get_reserved(symbol).is_none()); + + let type_arguments = types.get_type_arguments(typ_index); + + let alias_variables = { + let all_vars_length = type_arguments.len() + + lambda_set_variables.len() + + infer_ext_in_output_variables.len(); + + let type_arguments_offset = 0; + let lambda_set_vars_offset = type_arguments_offset + type_arguments.len(); + let infer_ext_vars_offset = lambda_set_vars_offset + lambda_set_variables.len(); + + let new_variables = + VariableSubsSlice::reserve_into_subs(env.subs, all_vars_length); + + for (((target_index, typ), region), abilities) in + (new_variables.indices().skip(type_arguments_offset)) + .zip(type_arguments.into_iter()) + .zip(type_argument_regions.into_iter()) + .zip(type_argument_abilities.into_iter()) + { + let copy_var = helper!(typ); + env.subs.variables[target_index] = copy_var; + if !types[abilities].is_empty() { + let region = types[region]; + bind_to_abilities.push((Loc::at(region, copy_var), abilities)); + } + } + + let it = (new_variables.indices().skip(lambda_set_vars_offset)) + .zip(lambda_set_variables.into_iter()); + for (target_index, ls) in it { + let copy_var = helper!(ls); + env.subs.variables[target_index] = copy_var; + } + + let it = (new_variables.indices().skip(infer_ext_vars_offset)) + .zip(infer_ext_in_output_variables.into_iter()); + for (target_index, ext_typ) in it { + let copy_var = helper!(ext_typ); + env.subs.variables[target_index] = copy_var; + } + + AliasVariables { + variables_start: new_variables.start, + type_variables_len: type_arguments.len() as _, + lambda_set_variables_len: lambda_set_variables.len() as _, + all_variables_len: all_vars_length as _, + } + }; + + let alias_variable = if let Symbol::RESULT_RESULT = symbol { + roc_result_to_var(env, rank, arena, types, actual, &mut stack) + } else { + helper!(actual) + }; + let content = Content::Alias(symbol, alias_variables, alias_variable, kind); + + env.register_with_known_var(destination, rank, content) + } + Error => { + let content = Content::Error; + + env.register_with_known_var(destination, rank, content) + } + }; + } + + for (Loc { value: var, region }, abilities) in bind_to_abilities { + let abilities = &types[abilities]; + match *env.subs.get_content_unchecked(var) { + Content::RigidVar(a) => { + // TODO(multi-abilities): check run cache + let abilities_slice = SubsSlice::extend_new( + &mut env.subs.symbol_names, + abilities.sorted_iter().copied(), + ); + env.subs + .set_content(var, Content::RigidAbleVar(a, abilities_slice)); + } + Content::RigidAbleVar(_, abs) + if (env.subs.get_subs_slice(abs).iter()).eq(abilities.sorted_iter()) => + { + // pass, already bound + } + _ => { + let abilities_slice = SubsSlice::extend_new( + &mut env.subs.symbol_names, + abilities.sorted_iter().copied(), + ); + + let flex_ability = env.register(rank, Content::FlexAbleVar(None, abilities_slice)); + + let category = Category::OpaqueArg; + match unify( + &mut env.uenv(), + var, + flex_ability, + UnificationMode::EQ, + Polarity::OF_VALUE, + ) { + Unified::Success { + vars: _, + must_implement_ability, + lambda_sets_to_specialize, + extra_metadata: _, + } => { + // No introduction needed + + if !must_implement_ability.is_empty() { + let new_problems = obligation_cache.check_obligations( + env.subs, + abilities_store, + must_implement_ability, + AbilityImplError::BadExpr(region, category, flex_ability), + ); + problems.extend(new_problems); + } + debug_assert!(lambda_sets_to_specialize + .drain() + .all(|(_, vals)| vals.is_empty())); + } + Unified::Failure(_vars, actual_type, expected_type, _bad_impls) => { + // No introduction needed + + let problem = TypeError::BadExpr( + region, + category, + actual_type, + Expected::NoExpectation(expected_type), + ); + + problems.push(problem); + } + } + } + } + } + + result +} + +#[inline(always)] +fn roc_result_to_var( + env: &mut InferenceEnv, + rank: Rank, + arena: &'_ bumpalo::Bump, + types: &mut Types, + result_type: Index, + stack: &mut bumpalo::collections::Vec<'_, TypeToVar>, +) -> Variable { + match types[result_type] { + TypeTag::TagUnion(tags, _ext_openness) => { + let ext_slice = types.get_type_arguments(result_type); + + debug_assert!(ext_slice.is_empty()); + debug_assert!(tags.len() == 2); + + let (tags_slice, payload_slices_slice) = types.union_tag_slices(tags); + + if let ([err, ok], [err_args, ok_args]) = + (&types[tags_slice], &types[payload_slices_slice]) + { + debug_assert_eq!(err, &env.subs.tag_names[0]); + debug_assert_eq!(ok, &env.subs.tag_names[1]); + + debug_assert_eq!(err_args.len(), 1); + debug_assert_eq!(ok_args.len(), 1); + + if let (Some(err_type), Some(ok_type)) = + (err_args.into_iter().next(), ok_args.into_iter().next()) + { + let err_var = + RegisterVariable::with_stack(env, rank, arena, types, err_type, stack); + let ok_var = + RegisterVariable::with_stack(env, rank, arena, types, ok_type, stack); + + let start = env.subs.variables.len() as u32; + let err_slice = SubsSlice::new(start, 1); + let ok_slice = SubsSlice::new(start + 1, 1); + + env.subs.variables.push(err_var); + env.subs.variables.push(ok_var); + + let variables = SubsSlice::new(env.subs.variable_slices.len() as _, 2); + env.subs.variable_slices.push(err_slice); + env.subs.variable_slices.push(ok_slice); + + let union_tags = UnionTags::from_slices(Subs::RESULT_TAG_NAMES, variables); + let ext = TagExt::Any(Variable::EMPTY_TAG_UNION); + + let content = Content::Structure(FlatType::TagUnion(union_tags, ext)); + + return env.register(rank, content); + } + } + + unreachable!("invalid arguments to Result.Result; canonicalization should catch this!") + } + _ => unreachable!("not a valid type inside a Result.Result alias"), + } +} + +fn insertion_sort_by(arr: &mut [T], mut compare: F) +where + F: FnMut(&T, &T) -> std::cmp::Ordering, +{ + for i in 1..arr.len() { + let val = &arr[i]; + let mut j = i; + let pos = arr[..i] + .binary_search_by(|x| compare(x, val)) + .unwrap_or_else(|pos| pos); + // Swap all elements until specific position. + while j > pos { + arr.swap(j - 1, j); + j -= 1; + } + } +} + +fn sorted_no_duplicate_tags(tag_slices: &[TagName]) -> bool { + match tag_slices.split_first() { + None => true, + Some((first, rest)) => { + let mut current = first; + + for next in rest { + if current >= next { + return false; + } else { + current = next; + } + } + + true + } + } +} + +fn sort_and_deduplicate(tag_vars: &mut bumpalo::collections::Vec<(TagName, T)>) { + insertion_sort_by(tag_vars, |(a, _), (b, _)| a.cmp(b)); + + // deduplicate, keeping the right-most occurrence of a tag name + let mut i = 0; + + while i < tag_vars.len() { + match (tag_vars.get(i), tag_vars.get(i + 1)) { + (Some((t1, _)), Some((t2, _))) => { + if t1 == t2 { + tag_vars.remove(i); + } else { + i += 1; + } + } + _ => break, + } + } +} + +/// Find whether the current run of tag names is in the subs.tag_names array already. If so, +/// we take a SubsSlice to the existing tag names, so we don't have to add/clone those tag names +/// and keep subs memory consumption low +fn find_tag_name_run(slice: &[TagName], subs: &mut Subs) -> Option> { + use std::cmp::Ordering; + + let tag_name = slice.get(0)?; + + let mut result = None; + + // the `SubsSlice` that inserting `slice` into subs would give + let bigger_slice = SubsSlice::new(subs.tag_names.len() as _, slice.len() as _); + + match subs.tag_name_cache.get_mut(tag_name) { + Some(occupied) => { + let subs_slice = *occupied; + + let prefix_slice = SubsSlice::new(subs_slice.start, slice.len() as _); + + if slice.len() == 1 { + return Some(prefix_slice); + } + + match slice.len().cmp(&subs_slice.len()) { + Ordering::Less => { + // we might have a prefix + let tag_names = &subs.tag_names[subs_slice.start as usize..]; + + for (from_subs, from_slice) in tag_names.iter().zip(slice.iter()) { + if from_subs != from_slice { + return None; + } + } + + result = Some(prefix_slice); + } + Ordering::Equal => { + let tag_names = &subs.tag_names[subs_slice.indices()]; + + for (from_subs, from_slice) in tag_names.iter().zip(slice.iter()) { + if from_subs != from_slice { + return None; + } + } + + result = Some(subs_slice); + } + Ordering::Greater => { + // switch to the bigger slice that is not inserted yet, but will be soon + *occupied = bigger_slice; + } + } + } + None => { + subs.tag_name_cache.push(tag_name, bigger_slice); + } + } + + result +} + +#[inline(always)] +fn register_tag_arguments( + env: &mut InferenceEnv, + rank: Rank, + arena: &'_ bumpalo::Bump, + types: &mut Types, + stack: &mut bumpalo::collections::Vec<'_, TypeToVar>, + arguments: Slice, +) -> VariableSubsSlice { + if arguments.is_empty() { + VariableSubsSlice::default() + } else { + let new_variables = VariableSubsSlice::reserve_into_subs(env.subs, arguments.len()); + let it = new_variables.indices().zip(arguments.into_iter()); + + for (target_index, argument) in it { + let var = RegisterVariable::with_stack(env, rank, arena, types, argument, stack); + env.subs.variables[target_index] = var; + } + + new_variables + } +} + +/// Assumes that the tags are sorted and there are no duplicates! +fn insert_tags_fast_path( + env: &mut InferenceEnv, + rank: Rank, + arena: &'_ bumpalo::Bump, + types: &mut Types, + union_tags: UnionTags, + stack: &mut bumpalo::collections::Vec<'_, TypeToVar>, +) -> UnionTags { + let (tags, payload_slices) = types.union_tag_slices(union_tags); + + debug_assert_eq!(tags.len(), payload_slices.len()); + + if let [arguments_slice] = &types[payload_slices] { + let arguments_slice = *arguments_slice; + + let variable_slice = + register_tag_arguments(env, rank, arena, types, stack, arguments_slice); + + let new_variable_slices = + SubsSlice::extend_new(&mut env.subs.variable_slices, [variable_slice]); + + macro_rules! subs_tag_name { + ($tag_name_slice:expr) => { + return UnionTags::from_slices($tag_name_slice, new_variable_slices) + }; + } + + match types[tags][0].0.as_str() { + "Ok" => subs_tag_name!(Subs::TAG_NAME_OK.as_slice()), + "Err" => subs_tag_name!(Subs::TAG_NAME_ERR.as_slice()), + "InvalidNumStr" => subs_tag_name!(Subs::TAG_NAME_INVALID_NUM_STR.as_slice()), + "BadUtf8" => subs_tag_name!(Subs::TAG_NAME_BAD_UTF_8.as_slice()), + "OutOfBounds" => subs_tag_name!(Subs::TAG_NAME_OUT_OF_BOUNDS.as_slice()), + _other => {} + } + } + + let new_variable_slices = SubsSlice::reserve_variable_slices(env.subs, tags.len()); + match find_tag_name_run(&types[tags], env.subs) { + Some(new_tag_names) => { + let it = (new_variable_slices.indices()).zip(payload_slices.into_iter()); + + for (variable_slice_index, arguments_index) in it { + let arguments = types[arguments_index]; + env.subs.variable_slices[variable_slice_index] = + register_tag_arguments(env, rank, arena, types, stack, arguments); + } + + UnionTags::from_slices(new_tag_names, new_variable_slices) + } + None => { + let new_tag_names = SubsSlice::reserve_tag_names(env.subs, tags.len()); + + let it = (new_variable_slices.indices()) + .zip(new_tag_names.indices()) + .zip(tags.into_iter()) + .zip(payload_slices.into_iter()); + + for (((variable_slice_index, tag_name_index), tag_name), arguments_index) in it { + let arguments = types[arguments_index]; + env.subs.variable_slices[variable_slice_index] = + register_tag_arguments(env, rank, arena, types, stack, arguments); + + env.subs.tag_names[tag_name_index] = types[tag_name].clone(); + } + + UnionTags::from_slices(new_tag_names, new_variable_slices) + } + } +} + +fn insert_tags_slow_path( + env: &mut InferenceEnv, + rank: Rank, + arena: &'_ bumpalo::Bump, + types: &mut Types, + union_tags: UnionTags, + mut tag_vars: bumpalo::collections::Vec<(TagName, VariableSubsSlice)>, + stack: &mut bumpalo::collections::Vec<'_, TypeToVar>, +) -> UnionTags { + let (tags, payload_slices) = types.union_tag_slices(union_tags); + + for (tag_index, tag_argument_types_index) in (tags.into_iter()).zip(payload_slices.into_iter()) + { + let tag_argument_types = &types[tag_argument_types_index]; + + let new_slice = VariableSubsSlice::reserve_into_subs(env.subs, tag_argument_types.len()); + + for (i, arg) in (new_slice.indices()).zip(tag_argument_types.into_iter()) { + let var = RegisterVariable::with_stack(env, rank, arena, types, arg, stack); + env.subs.variables[i] = var; + } + + tag_vars.push((types[tag_index].clone(), new_slice)); + } + + sort_and_deduplicate(&mut tag_vars); + + UnionTags::insert_slices_into_subs(env.subs, tag_vars) +} + +fn type_to_union_tags( + env: &mut InferenceEnv, + rank: Rank, + arena: &'_ bumpalo::Bump, + types: &mut Types, + union_tags: UnionTags, + opt_ext_slice: Slice, + ext_openness: ExtImplicitOpenness, + stack: &mut bumpalo::collections::Vec<'_, TypeToVar>, +) -> (UnionTags, TagExt) { + use bumpalo::collections::Vec; + + let (tags, _) = types.union_tag_slices(union_tags); + + let sorted = tags.len() == 1 || sorted_no_duplicate_tags(&types[tags]); + + debug_assert!(opt_ext_slice.len() <= 1); + + match opt_ext_slice.into_iter().next() { + None => { + let ext = Variable::EMPTY_TAG_UNION; + + let union_tags = if sorted { + insert_tags_fast_path(env, rank, arena, types, union_tags, stack) + } else { + let tag_vars = Vec::with_capacity_in(tags.len(), arena); + insert_tags_slow_path(env, rank, arena, types, union_tags, tag_vars, stack) + }; + + (union_tags, TagExt::Any(ext)) + } + Some(ext) => { + let mut tag_vars = Vec::with_capacity_in(tags.len(), arena); + + let temp_ext = { + let temp_ext_var = + RegisterVariable::with_stack(env, rank, arena, types, ext, stack); + TagExt::from_can(temp_ext_var, ext_openness) + }; + let (it, ext) = roc_types::types::gather_tags_unsorted_iter( + env.subs, + UnionTags::default(), + temp_ext, + ) + .expect("extension var could not be seen as tag union"); + + tag_vars.extend(it.map(|(n, v)| (n.clone(), v))); + + let union_tags = if tag_vars.is_empty() && sorted { + insert_tags_fast_path(env, rank, arena, types, union_tags, stack) + } else { + insert_tags_slow_path(env, rank, arena, types, union_tags, tag_vars, stack) + }; + + (union_tags, ext) + } + } +} + +fn create_union_lambda( + env: &mut InferenceEnv, + rank: Rank, + arena: &'_ bumpalo::Bump, + types: &mut Types, + closure: Symbol, + capture_types: Slice, + stack: &mut bumpalo::collections::Vec<'_, TypeToVar>, +) -> UnionLambdas { + let variable_slice = register_tag_arguments(env, rank, arena, types, stack, capture_types); + let new_variable_slices = + SubsSlice::extend_new(&mut env.subs.variable_slices, [variable_slice]); + + let lambda_name_slice = SubsSlice::extend_new(&mut env.subs.symbol_names, [closure]); + + UnionLambdas::from_slices(lambda_name_slice, new_variable_slices) +} diff --git a/crates/compiler/solve/tests/solve_expr.rs b/crates/compiler/solve/tests/solve_expr.rs new file mode 100644 index 0000000000..df1b4f06c7 --- /dev/null +++ b/crates/compiler/solve/tests/solve_expr.rs @@ -0,0 +1,5137 @@ +#[macro_use] +extern crate pretty_assertions; +#[macro_use] +extern crate indoc; + +extern crate bumpalo; + +#[cfg(test)] +mod solve_expr { + use roc_load::LoadedModule; + use roc_solve::FunctionKind; + use test_solve_helpers::{format_problems, run_load_and_infer}; + + use roc_types::pretty_print::{name_and_print_var, DebugPrint}; + + // HELPERS + + fn infer_eq_help(src: &str) -> Result<(String, String, String), std::io::Error> { + let ( + LoadedModule { + module_id: home, + mut can_problems, + mut type_problems, + interns, + mut solved, + mut exposed_to_host, + abilities_store, + .. + }, + src, + ) = run_load_and_infer(src, [], false, FunctionKind::LambdaSet)?; + + let mut can_problems = can_problems.remove(&home).unwrap_or_default(); + let type_problems = type_problems.remove(&home).unwrap_or_default(); + + // Disregard UnusedDef problems, because those are unavoidable when + // returning a function from the test expression. + can_problems.retain(|prob| { + !matches!( + prob, + roc_problem::can::Problem::UnusedDef(_, _) + | roc_problem::can::Problem::UnusedBranchDef(..) + ) + }); + + let (can_problems, type_problems) = + format_problems(&src, home, &interns, can_problems, type_problems); + + let subs = solved.inner_mut(); + + exposed_to_host.retain(|s, _| !abilities_store.is_specialization_name(*s)); + + debug_assert!(exposed_to_host.len() == 1, "{exposed_to_host:?}"); + let (_symbol, variable) = exposed_to_host.into_iter().next().unwrap(); + let actual_str = name_and_print_var(variable, subs, home, &interns, DebugPrint::NOTHING); + + Ok((type_problems, can_problems, actual_str)) + } + + fn infer_eq(src: &str, expected: &str) { + let (_, can_problems, actual) = infer_eq_help(src).unwrap(); + + assert!( + can_problems.is_empty(), + "Canonicalization problems: {can_problems}" + ); + + assert_eq!(actual, expected.to_string()); + } + + fn infer_eq_without_problem(src: &str, expected: &str) { + let (type_problems, can_problems, actual) = infer_eq_help(src).unwrap(); + + assert!( + can_problems.is_empty(), + "Canonicalization problems: {can_problems}" + ); + + if !type_problems.is_empty() { + // fail with an assert, but print the problems normally so rust doesn't try to diff + // an empty vec with the problems. + panic!("expected:\n{expected:?}\ninferred:\n{actual:?}\nproblems:\n{type_problems}",); + } + assert_eq!(actual, expected.to_string()); + } + + #[test] + fn int_literal() { + infer_eq("5", "Num *"); + } + + #[test] + fn float_literal() { + infer_eq("0.5", "Frac *"); + } + + #[test] + fn dec_literal() { + infer_eq( + indoc!( + r#" + val : Dec + val = 1.2 + + val + "# + ), + "Dec", + ); + } + + #[test] + fn string_literal() { + infer_eq( + indoc!( + r#" + "type inference!" + "# + ), + "Str", + ); + } + + #[test] + fn empty_string() { + infer_eq( + indoc!( + r#" + "" + "# + ), + "Str", + ); + } + + #[test] + fn string_starts_with() { + infer_eq_without_problem( + indoc!( + r#" + Str.startsWith + "# + ), + "Str, Str -> Bool", + ); + } + + #[test] + fn string_from_int() { + infer_eq_without_problem( + indoc!( + r#" + Num.toStr + "# + ), + "Num * -> Str", + ); + } + + #[test] + fn string_from_utf8() { + infer_eq_without_problem( + indoc!( + r#" + Str.fromUtf8 + "# + ), + "List U8 -> Result Str [BadUtf8 Utf8ByteProblem Nat]", + ); + } + + // LIST + + #[test] + fn empty_list() { + infer_eq( + indoc!( + r#" + [] + "# + ), + "List *", + ); + } + + #[test] + fn list_of_lists() { + infer_eq( + indoc!( + r#" + [[]] + "# + ), + "List (List *)", + ); + } + + #[test] + fn triple_nested_list() { + infer_eq( + indoc!( + r#" + [[[]]] + "# + ), + "List (List (List *))", + ); + } + + #[test] + fn nested_empty_list() { + infer_eq( + indoc!( + r#" + [[], [[]]] + "# + ), + "List (List (List *))", + ); + } + + #[test] + fn list_of_one_int() { + infer_eq( + indoc!( + r#" + [42] + "# + ), + "List (Num *)", + ); + } + + #[test] + fn triple_nested_int_list() { + infer_eq( + indoc!( + r#" + [[[5]]] + "# + ), + "List (List (List (Num *)))", + ); + } + + #[test] + fn list_of_ints() { + infer_eq( + indoc!( + r#" + [1, 2, 3] + "# + ), + "List (Num *)", + ); + } + + #[test] + fn nested_list_of_ints() { + infer_eq( + indoc!( + r#" + [[1], [2, 3]] + "# + ), + "List (List (Num *))", + ); + } + + #[test] + fn list_of_one_string() { + infer_eq( + indoc!( + r#" + ["cowabunga"] + "# + ), + "List Str", + ); + } + + #[test] + fn triple_nested_string_list() { + infer_eq( + indoc!( + r#" + [[["foo"]]] + "# + ), + "List (List (List Str))", + ); + } + + #[test] + fn list_of_strings() { + infer_eq( + indoc!( + r#" + ["foo", "bar"] + "# + ), + "List Str", + ); + } + + // INTERPOLATED STRING + + #[test] + fn infer_interpolated_string() { + infer_eq( + indoc!( + r#" + whatItIs = "great" + + "type inference is \(whatItIs)!" + "# + ), + "Str", + ); + } + + #[test] + fn infer_interpolated_var() { + infer_eq( + indoc!( + r#" + whatItIs = "great" + + str = "type inference is \(whatItIs)!" + + whatItIs + "# + ), + "Str", + ); + } + + #[test] + fn infer_interpolated_field() { + infer_eq( + indoc!( + r#" + rec = { whatItIs: "great" } + + str = "type inference is \(rec.whatItIs)!" + + rec + "# + ), + "{ whatItIs : Str }", + ); + } + + // LIST MISMATCH + + #[test] + fn mismatch_heterogeneous_list() { + infer_eq( + indoc!( + r#" + ["foo", 5] + "# + ), + "List ", + ); + } + + #[test] + fn mismatch_heterogeneous_nested_list() { + infer_eq( + indoc!( + r#" + [["foo", 5]] + "# + ), + "List (List )", + ); + } + + #[test] + fn mismatch_heterogeneous_nested_empty_list() { + infer_eq( + indoc!( + r#" + [[1], [[]]] + "# + ), + "List ", + ); + } + + // CLOSURE + + #[test] + fn always_return_empty_record() { + infer_eq( + indoc!( + r#" + \_ -> {} + "# + ), + "* -> {}", + ); + } + + #[test] + fn two_arg_return_int() { + infer_eq( + indoc!( + r#" + \_, _ -> 42 + "# + ), + "*, * -> Num *", + ); + } + + #[test] + fn three_arg_return_string() { + infer_eq( + indoc!( + r#" + \_, _, _ -> "test!" + "# + ), + "*, *, * -> Str", + ); + } + + // DEF + + #[test] + fn def_empty_record() { + infer_eq( + indoc!( + r#" + foo = {} + + foo + "# + ), + "{}", + ); + } + + #[test] + fn def_string() { + infer_eq( + indoc!( + r#" + str = "thing" + + str + "# + ), + "Str", + ); + } + + #[test] + fn def_1_arg_closure() { + infer_eq( + indoc!( + r#" + fn = \_ -> {} + + fn + "# + ), + "* -> {}", + ); + } + + #[test] + fn applied_tag() { + infer_eq_without_problem( + indoc!( + r#" + List.map ["a", "b"] \elem -> Foo elem + "# + ), + "List [Foo Str]", + ) + } + + // Tests (TagUnion, Func) + #[test] + fn applied_tag_function() { + infer_eq_without_problem( + indoc!( + r#" + foo = Foo + + foo "hi" + "# + ), + "[Foo Str]", + ) + } + + // Tests (TagUnion, Func) + #[test] + fn applied_tag_function_list_map() { + infer_eq_without_problem( + indoc!( + r#" + List.map ["a", "b"] Foo + "# + ), + "List [Foo Str]", + ) + } + + // Tests (TagUnion, Func) + #[test] + fn applied_tag_function_list() { + infer_eq_without_problem( + indoc!( + r#" + [\x -> Bar x, Foo] + "# + ), + "List (a -> [Bar a, Foo a])", + ) + } + + // Tests (Func, TagUnion) + #[test] + fn applied_tag_function_list_other_way() { + infer_eq_without_problem( + indoc!( + r#" + [Foo, \x -> Bar x] + "# + ), + "List (a -> [Bar a, Foo a])", + ) + } + + // Tests (Func, TagUnion) + #[test] + fn applied_tag_function_record() { + infer_eq_without_problem( + indoc!( + r#" + foo0 = Foo + foo1 = Foo + foo2 = Foo + + { + x: [foo0, Foo], + y: [foo1, \x -> Foo x], + z: [foo2, \x,y -> Foo x y] + } + "# + ), + "{ x : List [Foo], y : List (a -> [Foo a]), z : List (b, c -> [Foo b c]) }", + ) + } + + // Tests (TagUnion, Func) + #[test] + fn applied_tag_function_with_annotation() { + infer_eq_without_problem( + indoc!( + r#" + x : List [Foo I64] + x = List.map [1, 2] Foo + + x + "# + ), + "List [Foo I64]", + ) + } + + #[test] + fn def_2_arg_closure() { + infer_eq( + indoc!( + r#" + func = \_, _ -> 42 + + func + "# + ), + "*, * -> Num *", + ); + } + + #[test] + fn def_3_arg_closure() { + infer_eq( + indoc!( + r#" + f = \_, _, _ -> "test!" + + f + "# + ), + "*, *, * -> Str", + ); + } + + #[test] + fn def_multiple_functions() { + infer_eq( + indoc!( + r#" + a = \_, _, _ -> "test!" + + b = a + + b + "# + ), + "*, *, * -> Str", + ); + } + + #[test] + fn def_multiple_strings() { + infer_eq( + indoc!( + r#" + a = "test!" + + b = a + + b + "# + ), + "Str", + ); + } + + #[test] + fn def_multiple_ints() { + infer_eq( + indoc!( + r#" + c = b + + b = a + + a = 42 + + c + "# + ), + "Num *", + ); + } + + #[test] + fn def_returning_closure() { + infer_eq( + indoc!( + r#" + f = \z -> z + g = \z -> z + + (\x -> + a = f x + b = g x + x + ) + "# + ), + "a -> a", + ); + } + + // CALLING FUNCTIONS + + #[test] + fn call_returns_int() { + infer_eq( + indoc!( + r#" + alwaysFive = \_ -> 5 + + alwaysFive "stuff" + "# + ), + "Num *", + ); + } + + #[test] + fn identity_returns_given_type() { + infer_eq( + indoc!( + r#" + identity = \a -> a + + identity "hi" + "# + ), + "Str", + ); + } + + #[test] + fn identity_infers_principal_type() { + infer_eq( + indoc!( + r#" + identity = \x -> x + + y = identity 5 + + identity + "# + ), + "a -> a", + ); + } + + #[test] + fn identity_works_on_incompatible_types() { + infer_eq( + indoc!( + r#" + identity = \a -> a + + x = identity 5 + y = identity "hi" + + x + "# + ), + "Num *", + ); + } + + #[test] + fn call_returns_list() { + infer_eq( + indoc!( + r#" + enlist = \val -> [val] + + enlist 5 + "# + ), + "List (Num *)", + ); + } + + #[test] + fn indirect_always() { + infer_eq( + indoc!( + r#" + always = \val -> (\_ -> val) + alwaysFoo = always "foo" + + alwaysFoo 42 + "# + ), + "Str", + ); + } + + #[test] + fn pizza_desugar() { + infer_eq( + indoc!( + r#" + 1 |> (\a -> a) + "# + ), + "Num *", + ); + } + + #[test] + fn pizza_desugar_two_arguments() { + infer_eq( + indoc!( + r#" + always2 = \a, _ -> a + + 1 |> always2 "foo" + "# + ), + "Num *", + ); + } + + #[test] + fn anonymous_identity() { + infer_eq( + indoc!( + r#" + (\a -> a) 3.14 + "# + ), + "Frac *", + ); + } + + #[test] + fn identity_of_identity() { + infer_eq( + indoc!( + r#" + (\val -> val) (\val -> val) + "# + ), + "a -> a", + ); + } + + #[test] + fn recursive_identity() { + infer_eq( + indoc!( + r#" + identity = \val -> val + + identity identity + "# + ), + "a -> a", + ); + } + + #[test] + fn identity_function() { + infer_eq( + indoc!( + r#" + \val -> val + "# + ), + "a -> a", + ); + } + + #[test] + fn use_apply() { + infer_eq( + indoc!( + r#" + identity = \a -> a + apply = \f, x -> f x + + apply identity 5 + "# + ), + "Num *", + ); + } + + #[test] + fn apply_function() { + infer_eq( + indoc!( + r#" + \f, x -> f x + "# + ), + "(a -> b), a -> b", + ); + } + + // #[test] + // TODO FIXME this should pass, but instead fails to canonicalize + // fn use_flip() { + // infer_eq( + // indoc!( + // r#" + // flip = \f -> (\a b -> f b a) + // neverendingInt = \f int -> f int + // x = neverendingInt (\a -> a) 5 + + // flip neverendingInt + // "# + // ), + // "(Num *, (a -> a)) -> Num *", + // ); + // } + + #[test] + fn flip_function() { + infer_eq( + indoc!( + r#" + \f -> (\a, b -> f b a) + "# + ), + "(a, b -> c) -> (b, a -> c)", + ); + } + + #[test] + fn always_function() { + infer_eq( + indoc!( + r#" + \val -> \_ -> val + "# + ), + "a -> (* -> a)", + ); + } + + #[test] + fn pass_a_function() { + infer_eq( + indoc!( + r#" + \f -> f {} + "# + ), + "({} -> a) -> a", + ); + } + + // OPERATORS + + // #[test] + // fn div_operator() { + // infer_eq( + // indoc!( + // r#" + // \l r -> l / r + // "# + // ), + // "F64, F64 -> F64", + // ); + // } + + // #[test] + // fn basic_float_division() { + // infer_eq( + // indoc!( + // r#" + // 1 / 2 + // "# + // ), + // "F64", + // ); + // } + + // #[test] + // fn basic_int_division() { + // infer_eq( + // indoc!( + // r#" + // 1 // 2 + // "# + // ), + // "Num *", + // ); + // } + + // #[test] + // fn basic_addition() { + // infer_eq( + // indoc!( + // r#" + // 1 + 2 + // "# + // ), + // "Num *", + // ); + // } + + // #[test] + // fn basic_circular_type() { + // infer_eq( + // indoc!( + // r#" + // \x -> x x + // "# + // ), + // "", + // ); + // } + + // #[test] + // fn y_combinator_has_circular_type() { + // assert_eq!( + // infer(indoc!(r#" + // \f -> (\x -> f x x) (\x -> f x x) + // "#)), + // Erroneous(Problem::CircularType) + // ); + // } + + // #[test] + // fn no_higher_ranked_types() { + // // This should error because it can't type of alwaysFive + // infer_eq( + // indoc!( + // r#" + // \always -> [always [], always ""] + // "# + // ), + // "", + // ); + // } + + #[test] + fn always_with_list() { + infer_eq( + indoc!( + r#" + alwaysFive = \_ -> 5 + + [alwaysFive "foo", alwaysFive []] + "# + ), + "List (Num *)", + ); + } + + #[test] + fn if_with_int_literals() { + infer_eq( + indoc!( + r#" + if Bool.true then + 42 + else + 24 + "# + ), + "Num *", + ); + } + + #[test] + fn when_with_int_literals() { + infer_eq( + indoc!( + r#" + when 1 is + 1 -> 2 + 3 -> 4 + "# + ), + "Num *", + ); + } + + // RECORDS + + #[test] + fn empty_record() { + infer_eq("{}", "{}"); + } + + #[test] + fn one_field_record() { + infer_eq("{ x: 5 }", "{ x : Num * }"); + } + + #[test] + fn two_field_record() { + infer_eq("{ x: 5, y : 3.14 }", "{ x : Num *, y : Frac * }"); + } + + #[test] + fn record_literal_accessor() { + infer_eq("{ x: 5, y : 3.14 }.x", "Num *"); + } + + #[test] + fn record_literal_accessor_function() { + infer_eq(".x { x: 5, y : 3.14 }", "Num *"); + } + + #[test] + fn tuple_literal_accessor() { + infer_eq("(5, 3.14 ).0", "Num *"); + } + + #[test] + fn tuple_literal_accessor_function() { + infer_eq(".0 (5, 3.14 )", "Num *"); + } + + #[test] + fn tuple_literal_ty() { + infer_eq("(5, 3.14 )", "( Num *, Frac * )*"); + } + + #[test] + fn tuple_literal_accessor_ty() { + infer_eq(".0", "( a )* -> a"); + infer_eq(".4", "( _, _, _, _, a )* -> a"); + infer_eq(".5", "( ... 5 omitted, a )* -> a"); + infer_eq(".200", "( ... 200 omitted, a )* -> a"); + } + + #[test] + fn tuple_accessor_generalization() { + infer_eq( + indoc!( + r#" + get0 = .0 + + { a: get0 (1, 2), b: get0 ("a", "b", "c") } + "# + ), + "{ a : Num *, b : Str }", + ); + } + + #[test] + fn record_arg() { + infer_eq("\\rec -> rec.x", "{ x : a }* -> a"); + } + + #[test] + fn record_with_bound_var() { + infer_eq( + indoc!( + r#" + fn = \rec -> + x = rec.x + + rec + + fn + "# + ), + "{ x : a }b -> { x : a }b", + ); + } + + #[test] + fn using_type_signature() { + infer_eq( + indoc!( + r#" + bar : custom -> custom + bar = \x -> x + + bar + "# + ), + "custom -> custom", + ); + } + + #[test] + fn type_signature_without_body() { + infer_eq( + indoc!( + r#" + foo: Str -> {} + + foo "hi" + "# + ), + "{}", + ); + } + + #[test] + fn type_signature_without_body_rigid() { + infer_eq( + indoc!( + r#" + foo : Num * -> custom + + foo 2 + "# + ), + "custom", + ); + } + + #[test] + fn accessor_function() { + infer_eq(".foo", "{ foo : a }* -> a"); + } + + #[test] + fn type_signature_without_body_record() { + infer_eq( + indoc!( + r#" + { x, y } : { x : ({} -> custom), y : {} } + + x + "# + ), + "{} -> custom", + ); + } + + #[test] + fn empty_record_pattern() { + infer_eq( + indoc!( + r#" + # technically, an empty record can be destructured + thunk = \{} -> 42 + + xEmpty = if thunk {} == 42 then { x: {} } else { x: {} } + + when xEmpty is + { x: {} } -> {} + "# + ), + "{}", + ); + } + + #[test] + fn record_type_annotation() { + // check that a closed record remains closed + infer_eq( + indoc!( + r#" + foo : { x : custom } -> custom + foo = \{ x } -> x + + foo + "# + ), + "{ x : custom } -> custom", + ); + } + + #[test] + fn record_update() { + infer_eq( + indoc!( + r#" + user = { year: "foo", name: "Sam" } + + { user & year: "foo" } + "# + ), + "{ name : Str, year : Str }", + ); + } + + #[test] + fn bare_tag() { + infer_eq( + indoc!( + r#" + Foo + "# + ), + "[Foo]", + ); + } + + #[test] + fn single_tag_pattern() { + infer_eq( + indoc!( + r#" + \Foo -> 42 + "# + ), + "[Foo] -> Num *", + ); + } + + #[test] + fn two_tag_pattern() { + infer_eq( + indoc!( + r#" + \x -> + when x is + True -> 1 + False -> 0 + "# + ), + "[False, True] -> Num *", + ); + } + + #[test] + fn tag_application() { + infer_eq( + indoc!( + r#" + Foo "happy" 12 + "# + ), + "[Foo Str (Num *)]", + ); + } + + #[test] + fn record_extraction() { + infer_eq( + indoc!( + r#" + f = \x -> + when x is + { a, b: _ } -> a + + f + "# + ), + "{ a : a, b : * }* -> a", + ); + } + + #[test] + fn record_field_pattern_match_with_guard() { + infer_eq( + indoc!( + r#" + when { x: 5 } is + { x: 4 } -> 4 + "# + ), + "Num *", + ); + } + + #[test] + fn tag_union_pattern_match() { + infer_eq( + indoc!( + r#" + \Foo x -> Foo x + "# + ), + "[Foo a] -> [Foo a]", + ); + } + + #[test] + fn tag_union_pattern_match_ignored_field() { + infer_eq( + indoc!( + r#" + \Foo x _ -> Foo x "y" + "# + ), + "[Foo a *] -> [Foo a Str]", + ); + } + + #[test] + fn tag_with_field() { + infer_eq( + indoc!( + r#" + when Foo "blah" is + Foo x -> x + "# + ), + "Str", + ); + } + + #[test] + fn qualified_annotation_num_integer() { + infer_eq( + indoc!( + r#" + int : Num.Num (Num.Integer Num.Signed64) + + int + "# + ), + "I64", + ); + } + #[test] + fn qualified_annotated_num_integer() { + infer_eq( + indoc!( + r#" + int : Num.Num (Num.Integer Num.Signed64) + int = 5 + + int + "# + ), + "I64", + ); + } + #[test] + fn annotation_num_integer() { + infer_eq( + indoc!( + r#" + int : Num (Integer Signed64) + + int + "# + ), + "I64", + ); + } + #[test] + fn annotated_num_integer() { + infer_eq( + indoc!( + r#" + int : Num (Integer Signed64) + int = 5 + + int + "# + ), + "I64", + ); + } + + #[test] + fn qualified_annotation_using_i128() { + infer_eq( + indoc!( + r#" + int : Num.I128 + + int + "# + ), + "I128", + ); + } + #[test] + fn qualified_annotated_using_i128() { + infer_eq( + indoc!( + r#" + int : Num.I128 + int = 5 + + int + "# + ), + "I128", + ); + } + #[test] + fn annotation_using_i128() { + infer_eq( + indoc!( + r#" + int : I128 + + int + "# + ), + "I128", + ); + } + #[test] + fn annotated_using_i128() { + infer_eq( + indoc!( + r#" + int : I128 + int = 5 + + int + "# + ), + "I128", + ); + } + + #[test] + fn qualified_annotation_using_u128() { + infer_eq( + indoc!( + r#" + int : Num.U128 + + int + "# + ), + "U128", + ); + } + #[test] + fn qualified_annotated_using_u128() { + infer_eq( + indoc!( + r#" + int : Num.U128 + int = 5 + + int + "# + ), + "U128", + ); + } + #[test] + fn annotation_using_u128() { + infer_eq( + indoc!( + r#" + int : U128 + + int + "# + ), + "U128", + ); + } + #[test] + fn annotated_using_u128() { + infer_eq( + indoc!( + r#" + int : U128 + int = 5 + + int + "# + ), + "U128", + ); + } + + #[test] + fn qualified_annotation_using_i64() { + infer_eq( + indoc!( + r#" + int : Num.I64 + + int + "# + ), + "I64", + ); + } + #[test] + fn qualified_annotated_using_i64() { + infer_eq( + indoc!( + r#" + int : Num.I64 + int = 5 + + int + "# + ), + "I64", + ); + } + #[test] + fn annotation_using_i64() { + infer_eq( + indoc!( + r#" + int : I64 + + int + "# + ), + "I64", + ); + } + #[test] + fn annotated_using_i64() { + infer_eq( + indoc!( + r#" + int : I64 + int = 5 + + int + "# + ), + "I64", + ); + } + + #[test] + fn qualified_annotation_using_u64() { + infer_eq( + indoc!( + r#" + int : Num.U64 + + int + "# + ), + "U64", + ); + } + #[test] + fn qualified_annotated_using_u64() { + infer_eq( + indoc!( + r#" + int : Num.U64 + int = 5 + + int + "# + ), + "U64", + ); + } + #[test] + fn annotation_using_u64() { + infer_eq( + indoc!( + r#" + int : U64 + + int + "# + ), + "U64", + ); + } + #[test] + fn annotated_using_u64() { + infer_eq( + indoc!( + r#" + int : U64 + int = 5 + + int + "# + ), + "U64", + ); + } + + #[test] + fn qualified_annotation_using_i32() { + infer_eq( + indoc!( + r#" + int : Num.I32 + + int + "# + ), + "I32", + ); + } + #[test] + fn qualified_annotated_using_i32() { + infer_eq( + indoc!( + r#" + int : Num.I32 + int = 5 + + int + "# + ), + "I32", + ); + } + #[test] + fn annotation_using_i32() { + infer_eq( + indoc!( + r#" + int : I32 + + int + "# + ), + "I32", + ); + } + #[test] + fn annotated_using_i32() { + infer_eq( + indoc!( + r#" + int : I32 + int = 5 + + int + "# + ), + "I32", + ); + } + + #[test] + fn qualified_annotation_using_u32() { + infer_eq( + indoc!( + r#" + int : Num.U32 + + int + "# + ), + "U32", + ); + } + #[test] + fn qualified_annotated_using_u32() { + infer_eq( + indoc!( + r#" + int : Num.U32 + int = 5 + + int + "# + ), + "U32", + ); + } + #[test] + fn annotation_using_u32() { + infer_eq( + indoc!( + r#" + int : U32 + + int + "# + ), + "U32", + ); + } + #[test] + fn annotated_using_u32() { + infer_eq( + indoc!( + r#" + int : U32 + int = 5 + + int + "# + ), + "U32", + ); + } + + #[test] + fn qualified_annotation_using_i16() { + infer_eq( + indoc!( + r#" + int : Num.I16 + + int + "# + ), + "I16", + ); + } + #[test] + fn qualified_annotated_using_i16() { + infer_eq( + indoc!( + r#" + int : Num.I16 + int = 5 + + int + "# + ), + "I16", + ); + } + #[test] + fn annotation_using_i16() { + infer_eq( + indoc!( + r#" + int : I16 + + int + "# + ), + "I16", + ); + } + #[test] + fn annotated_using_i16() { + infer_eq( + indoc!( + r#" + int : I16 + int = 5 + + int + "# + ), + "I16", + ); + } + + #[test] + fn qualified_annotation_using_u16() { + infer_eq( + indoc!( + r#" + int : Num.U16 + + int + "# + ), + "U16", + ); + } + #[test] + fn qualified_annotated_using_u16() { + infer_eq( + indoc!( + r#" + int : Num.U16 + int = 5 + + int + "# + ), + "U16", + ); + } + #[test] + fn annotation_using_u16() { + infer_eq( + indoc!( + r#" + int : U16 + + int + "# + ), + "U16", + ); + } + #[test] + fn annotated_using_u16() { + infer_eq( + indoc!( + r#" + int : U16 + int = 5 + + int + "# + ), + "U16", + ); + } + + #[test] + fn qualified_annotation_using_i8() { + infer_eq( + indoc!( + r#" + int : Num.I8 + + int + "# + ), + "I8", + ); + } + #[test] + fn qualified_annotated_using_i8() { + infer_eq( + indoc!( + r#" + int : Num.I8 + int = 5 + + int + "# + ), + "I8", + ); + } + #[test] + fn annotation_using_i8() { + infer_eq( + indoc!( + r#" + int : I8 + + int + "# + ), + "I8", + ); + } + #[test] + fn annotated_using_i8() { + infer_eq( + indoc!( + r#" + int : I8 + int = 5 + + int + "# + ), + "I8", + ); + } + + #[test] + fn qualified_annotation_using_u8() { + infer_eq( + indoc!( + r#" + int : Num.U8 + + int + "# + ), + "U8", + ); + } + #[test] + fn qualified_annotated_using_u8() { + infer_eq( + indoc!( + r#" + int : Num.U8 + int = 5 + + int + "# + ), + "U8", + ); + } + #[test] + fn annotation_using_u8() { + infer_eq( + indoc!( + r#" + int : U8 + + int + "# + ), + "U8", + ); + } + #[test] + fn annotated_using_u8() { + infer_eq( + indoc!( + r#" + int : U8 + int = 5 + + int + "# + ), + "U8", + ); + } + + #[test] + fn qualified_annotation_num_floatingpoint() { + infer_eq( + indoc!( + r#" + float : Num.Num (Num.FloatingPoint Num.Binary64) + + float + "# + ), + "F64", + ); + } + #[test] + fn qualified_annotated_num_floatingpoint() { + infer_eq( + indoc!( + r#" + float : Num.Num (Num.FloatingPoint Num.Binary64) + float = 5.5 + + float + "# + ), + "F64", + ); + } + #[test] + fn annotation_num_floatingpoint() { + infer_eq( + indoc!( + r#" + float : Num (FloatingPoint Binary64) + + float + "# + ), + "F64", + ); + } + #[test] + fn annotated_num_floatingpoint() { + infer_eq( + indoc!( + r#" + float : Num (FloatingPoint Binary64) + float = 5.5 + + float + "# + ), + "F64", + ); + } + + #[test] + fn qualified_annotation_f64() { + infer_eq( + indoc!( + r#" + float : Num.F64 + + float + "# + ), + "F64", + ); + } + #[test] + fn qualified_annotated_f64() { + infer_eq( + indoc!( + r#" + float : Num.F64 + float = 5.5 + + float + "# + ), + "F64", + ); + } + #[test] + fn annotation_f64() { + infer_eq( + indoc!( + r#" + float : F64 + + float + "# + ), + "F64", + ); + } + #[test] + fn annotated_f64() { + infer_eq( + indoc!( + r#" + float : F64 + float = 5.5 + + float + "# + ), + "F64", + ); + } + + #[test] + fn qualified_annotation_f32() { + infer_eq( + indoc!( + r#" + float : Num.F32 + + float + "# + ), + "F32", + ); + } + #[test] + fn qualified_annotated_f32() { + infer_eq( + indoc!( + r#" + float : Num.F32 + float = 5.5 + + float + "# + ), + "F32", + ); + } + #[test] + fn annotation_f32() { + infer_eq( + indoc!( + r#" + float : F32 + + float + "# + ), + "F32", + ); + } + #[test] + fn annotated_f32() { + infer_eq( + indoc!( + r#" + float : F32 + float = 5.5 + + float + "# + ), + "F32", + ); + } + + #[test] + fn fake_result_ok() { + infer_eq( + indoc!( + r#" + Res a e : [Okay a, Error e] + + ok : Res I64 * + ok = Okay 5 + + ok + "# + ), + "Res I64 *", + ); + } + + #[test] + fn fake_result_err() { + infer_eq( + indoc!( + r#" + Res a e : [Okay a, Error e] + + err : Res * Str + err = Error "blah" + + err + "# + ), + "Res * Str", + ); + } + + #[test] + fn basic_result_ok() { + infer_eq( + indoc!( + r#" + ok : Result I64 * + ok = Ok 5 + + ok + "# + ), + "Result I64 *", + ); + } + + #[test] + fn basic_result_err() { + infer_eq( + indoc!( + r#" + err : Result * Str + err = Err "blah" + + err + "# + ), + "Result * Str", + ); + } + + #[test] + fn basic_result_conditional() { + infer_eq( + indoc!( + r#" + ok : Result I64 _ + ok = Ok 5 + + err : Result _ Str + err = Err "blah" + + if 1 > 0 then + ok + else + err + "# + ), + "Result I64 Str", + ); + } + + // #[test] + // fn annotation_using_num_used() { + // // There was a problem where `I64`, because it is only an annotation + // // wasn't added to the vars_by_symbol. + // infer_eq_without_problem( + // indoc!( + // r#" + // int : I64 + + // p = (\x -> x) int + + // p + // "# + // ), + // "I64", + // ); + // } + + #[test] + fn num_identity() { + infer_eq_without_problem( + indoc!( + r#" + numIdentity : Num.Num a -> Num.Num a + numIdentity = \x -> x + + y = numIdentity 3.14 + + { numIdentity, x : numIdentity 42, y } + "# + ), + "{ numIdentity : Num a -> Num a, x : Num *, y : Frac * }", + ); + } + + #[test] + fn when_with_annotation() { + infer_eq_without_problem( + indoc!( + r#" + x : Num.Num (Num.Integer Num.Signed64) + x = + when 2 is + 3 -> 4 + _ -> 5 + + x + "# + ), + "I64", + ); + } + + // TODO add more realistic function when able + #[test] + fn integer_sum() { + infer_eq_without_problem( + indoc!( + r#" + f = \n -> + when n is + 0 -> 0 + _ -> f n + + f + "# + ), + "Num * -> Num *", + ); + } + + #[test] + fn identity_map() { + infer_eq_without_problem( + indoc!( + r#" + map : (a -> b), [Identity a] -> [Identity b] + map = \f, identity -> + when identity is + Identity v -> Identity (f v) + map + "# + ), + "(a -> b), [Identity a] -> [Identity b]", + ); + } + + #[test] + fn to_bit() { + infer_eq_without_problem( + indoc!( + r#" + toBit = \bool -> + when bool is + True -> 1 + False -> 0 + + toBit + "# + ), + "[False, True] -> Num *", + ); + } + + // this test is related to a bug where ext_var would have an incorrect rank. + // This match has duplicate cases, but we ignore that. + #[test] + fn to_bit_record() { + infer_eq( + indoc!( + r#" + foo = \rec -> + when rec is + { x: _ } -> "1" + { y: _ } -> "2" + + foo + "# + ), + "{ x : *, y : * }* -> Str", + ); + } + + #[test] + fn from_bit() { + infer_eq_without_problem( + indoc!( + r#" + fromBit = \int -> + when int is + 0 -> False + _ -> True + + fromBit + "# + ), + "Num * -> [False, True]", + ); + } + + #[test] + fn result_map_explicit() { + infer_eq_without_problem( + indoc!( + r#" + map : (a -> b), [Err e, Ok a] -> [Err e, Ok b] + map = \f, result -> + when result is + Ok v -> Ok (f v) + Err e -> Err e + + map + "# + ), + "(a -> b), [Err e, Ok a] -> [Err e, Ok b]", + ); + } + + #[test] + fn result_map_alias() { + infer_eq_without_problem( + indoc!( + r#" + Res e a : [Ok a, Err e] + + map : (a -> b), Res e a -> Res e b + map = \f, result -> + when result is + Ok v -> Ok (f v) + Err e -> Err e + + map + "# + ), + "(a -> b), Res e a -> Res e b", + ); + } + + #[test] + fn record_from_load() { + infer_eq_without_problem( + indoc!( + r#" + foo = \{ x } -> x + + foo { x: 5 } + "# + ), + "Num *", + ); + } + + #[test] + fn defs_from_load() { + infer_eq_without_problem( + indoc!( + r#" + alwaysThreePointZero = \_ -> 3.0 + + answer = 42 + + identity = \a -> a + + threePointZero = identity (alwaysThreePointZero {}) + + threePointZero + "# + ), + "Frac *", + ); + } + + #[test] + fn use_as_in_signature() { + infer_eq_without_problem( + indoc!( + r#" + foo : Str.Str as Foo -> Foo + foo = \_ -> "foo" + + foo + "# + ), + "Foo -> Foo", + ); + } + + #[test] + fn use_alias_in_let() { + infer_eq_without_problem( + indoc!( + r#" + Foo : Str.Str + + foo : Foo -> Foo + foo = \_ -> "foo" + + foo + "# + ), + "Foo -> Foo", + ); + } + + #[test] + fn use_alias_with_argument_in_let() { + infer_eq_without_problem( + indoc!( + r#" + Foo a : { foo : a } + + v : Foo (Num.Num (Num.Integer Num.Signed64)) + v = { foo: 42 } + + v + "# + ), + "Foo I64", + ); + } + + #[test] + fn identity_alias() { + infer_eq_without_problem( + indoc!( + r#" + Foo a : { foo : a } + + id : Foo a -> Foo a + id = \x -> x + + id + "# + ), + "Foo a -> Foo a", + ); + } + + #[test] + fn linked_list_empty() { + infer_eq_without_problem( + indoc!( + r#" + empty : [Cons a (ConsList a), Nil] as ConsList a + empty = Nil + + empty + "# + ), + "ConsList a", + ); + } + + #[test] + fn linked_list_singleton() { + infer_eq_without_problem( + indoc!( + r#" + singleton : a -> [Cons a (ConsList a), Nil] as ConsList a + singleton = \x -> Cons x Nil + + singleton + "# + ), + "a -> ConsList a", + ); + } + + #[test] + fn peano_length() { + infer_eq_without_problem( + indoc!( + r#" + Peano : [S Peano, Z] + + length : Peano -> Num.Num (Num.Integer Num.Signed64) + length = \peano -> + when peano is + Z -> 0 + S v -> length v + + length + "# + ), + "Peano -> I64", + ); + } + + #[test] + fn peano_map() { + infer_eq_without_problem( + indoc!( + r#" + map : [S Peano, Z] as Peano -> Peano + map = \peano -> + when peano is + Z -> Z + S v -> S (map v) + + map + "# + ), + "Peano -> Peano", + ); + } + + #[test] + fn infer_linked_list_map() { + infer_eq_without_problem( + indoc!( + r#" + map = \f, list -> + when list is + Nil -> Nil + Cons x xs -> + a = f x + b = map f xs + + Cons a b + + map + "# + ), + "(a -> b), [Cons a c, Nil] as c -> [Cons b d, Nil] as d", + ); + } + + #[test] + fn typecheck_linked_list_map() { + infer_eq_without_problem( + indoc!( + r#" + ConsList a : [Cons a (ConsList a), Nil] + + map : (a -> b), ConsList a -> ConsList b + map = \f, list -> + when list is + Nil -> Nil + Cons x xs -> + Cons (f x) (map f xs) + + map + "# + ), + "(a -> b), ConsList a -> ConsList b", + ); + } + + #[test] + fn mismatch_in_alias_args_gets_reported() { + infer_eq( + indoc!( + r#" + Foo a : a + + r : Foo {} + r = {} + + s : Foo Str.Str + s = "bar" + + when {} is + _ -> s + _ -> r + "# + ), + "", + ); + } + + #[test] + fn mismatch_in_apply_gets_reported() { + infer_eq( + indoc!( + r#" + r : { x : (Num.Num (Num.Integer Signed64)) } + r = { x : 1 } + + s : { left : { x : Num.Num (Num.FloatingPoint Num.Binary64) } } + s = { left: { x : 3.14 } } + + when 0 is + 1 -> s.left + 0 -> r + "# + ), + "", + ); + } + + #[test] + fn mismatch_in_tag_gets_reported() { + infer_eq( + indoc!( + r#" + r : [Ok Str.Str] + r = Ok 1 + + s : { left: [Ok {}] } + s = { left: Ok 3.14 } + + when 0 is + 1 -> s.left + 0 -> r + "# + ), + "", + ); + } + + // TODO As intended, this fails, but it fails with the wrong error! + // + // #[test] + // fn nums() { + // infer_eq_without_problem( + // indoc!( + // r#" + // s : Num * + // s = 3.1 + + // s + // "# + // ), + // "", + // ); + // } + + #[test] + fn peano_map_alias() { + infer_eq( + indoc!( + r#" + app "test" provides [main] to "./platform" + + Peano : [S Peano, Z] + + map : Peano -> Peano + map = \peano -> + when peano is + Z -> Z + S rest -> S (map rest) + + main = + map + "# + ), + "Peano -> Peano", + ); + } + + #[test] + fn unit_alias() { + infer_eq( + indoc!( + r#" + Unit : [Unit] + + unit : Unit + unit = Unit + + unit + "# + ), + "Unit", + ); + } + + #[test] + fn rigid_in_letnonrec() { + infer_eq_without_problem( + indoc!( + r#" + ConsList a : [Cons a (ConsList a), Nil] + + toEmpty : ConsList a -> ConsList a + toEmpty = \_ -> + result : ConsList a + result = Nil + + result + + toEmpty + "# + ), + "ConsList a -> ConsList a", + ); + } + + #[test] + fn rigid_in_letrec_ignored() { + infer_eq_without_problem( + indoc!( + r#" + ConsList a : [Cons a (ConsList a), Nil] + + toEmpty : ConsList a -> ConsList a + toEmpty = \_ -> + result : ConsList _ # TODO to enable using `a` we need scoped variables + result = Nil + + toEmpty result + + toEmpty + "# + ), + "ConsList a -> ConsList a", + ); + } + + #[test] + fn rigid_in_letrec() { + infer_eq_without_problem( + indoc!( + r#" + app "test" provides [main] to "./platform" + + ConsList a : [Cons a (ConsList a), Nil] + + toEmpty : ConsList a -> ConsList a + toEmpty = \_ -> + result : ConsList _ # TODO to enable using `a` we need scoped variables + result = Nil + + toEmpty result + + main = + toEmpty + "# + ), + "ConsList a -> ConsList a", + ); + } + + #[test] + fn let_record_pattern_with_annotation() { + infer_eq_without_problem( + indoc!( + r#" + { x, y } : { x : Str.Str, y : Num.Num (Num.FloatingPoint Num.Binary64) } + { x, y } = { x : "foo", y : 3.14 } + + x + "# + ), + "Str", + ); + } + + #[test] + fn let_record_pattern_with_annotation_alias() { + infer_eq( + indoc!( + r#" + Foo : { x : Str.Str, y : Num.Num (Num.FloatingPoint Num.Binary64) } + + { x, y } : Foo + { x, y } = { x : "foo", y : 3.14 } + + x + "# + ), + "Str", + ); + } + + #[test] + fn peano_map_infer() { + infer_eq( + indoc!( + r#" + app "test" provides [main] to "./platform" + + map = + \peano -> + when peano is + Z -> Z + S rest -> map rest |> S + + + main = + map + "# + ), + "[S a, Z] as a -> [S b, Z] as b", + ); + } + + #[test] + fn peano_map_infer_nested() { + infer_eq( + indoc!( + r#" + map = \peano -> + when peano is + Z -> Z + S rest -> + map rest |> S + + + map + "# + ), + "[S a, Z] as a -> [S b, Z] as b", + ); + } + + #[test] + fn let_record_pattern_with_alias_annotation() { + infer_eq_without_problem( + indoc!( + r#" + Foo : { x : Str.Str, y : Num.Num (Num.FloatingPoint Num.Binary64) } + + { x, y } : Foo + { x, y } = { x : "foo", y : 3.14 } + + x + "# + ), + "Str", + ); + } + + #[test] + fn let_tag_pattern_with_annotation() { + infer_eq_without_problem( + indoc!( + r#" + UserId x : [UserId I64] + UserId x = UserId 42 + + x + "# + ), + "I64", + ); + } + + #[test] + fn typecheck_record_linked_list_map() { + infer_eq_without_problem( + indoc!( + r#" + ConsList q : [Cons { x: q, xs: ConsList q }, Nil] + + map : (a -> b), ConsList a -> ConsList b + map = \f, list -> + when list is + Nil -> Nil + Cons { x, xs } -> + Cons { x: f x, xs : map f xs } + + map + "# + ), + "(a -> b), ConsList a -> ConsList b", + ); + } + + #[test] + fn infer_record_linked_list_map() { + infer_eq_without_problem( + indoc!( + r#" + map = \f, list -> + when list is + Nil -> Nil + Cons { x, xs } -> + Cons { x: f x, xs : map f xs } + + map + "# + ), + "(a -> b), [Cons { x : a, xs : c }*, Nil] as c -> [Cons { x : b, xs : d }, Nil] as d", + ); + } + + #[test] + fn typecheck_mutually_recursive_tag_union_2() { + infer_eq_without_problem( + indoc!( + r#" + ListA a b : [Cons a (ListB b a), Nil] + ListB a b : [Cons a (ListA b a), Nil] + + ConsList q : [Cons q (ConsList q), Nil] + + toAs : (b -> a), ListA a b -> ConsList a + toAs = \f, lista -> + when lista is + Nil -> Nil + Cons a listb -> + when listb is + Nil -> Nil + Cons b newLista -> + Cons a (Cons (f b) (toAs f newLista)) + + toAs + "# + ), + "(b -> a), ListA a b -> ConsList a", + ); + } + + #[test] + fn typecheck_mutually_recursive_tag_union_listabc() { + infer_eq_without_problem( + indoc!( + r#" + ListA a : [Cons a (ListB a)] + ListB a : [Cons a (ListC a)] + ListC a : [Cons a (ListA a), Nil] + + val : ListC Num.I64 + val = Cons 1 (Cons 2 (Cons 3 Nil)) + + val + "# + ), + "ListC I64", + ); + } + + #[test] + fn infer_mutually_recursive_tag_union() { + infer_eq_without_problem( + indoc!( + r#" + toAs = \f, lista -> + when lista is + Nil -> Nil + Cons a listb -> + when listb is + Nil -> Nil + Cons b newLista -> + Cons a (Cons (f b) (toAs f newLista)) + + toAs + "# + ), + "(a -> b), [Cons c [Cons a d, Nil], Nil] as d -> [Cons c [Cons b e], Nil] as e", + ); + } + + #[test] + fn solve_list_get() { + infer_eq_without_problem( + indoc!( + r#" + List.get ["a"] 0 + "# + ), + "Result Str [OutOfBounds]", + ); + } + + #[test] + fn type_more_general_than_signature() { + infer_eq_without_problem( + indoc!( + r#" + partition : Nat, Nat, List (Int a) -> [Pair Nat (List (Int a))] + partition = \low, high, initialList -> + when List.get initialList high is + Ok _ -> + Pair 0 [] + + Err _ -> + Pair (low - 1) initialList + + partition + "# + ), + "Nat, Nat, List (Int a) -> [Pair Nat (List (Int a))]", + ); + } + + #[test] + fn quicksort_partition() { + infer_eq_without_problem( + indoc!( + r#" + swap : Nat, Nat, List a -> List a + swap = \i, j, list -> + when Pair (List.get list i) (List.get list j) is + Pair (Ok atI) (Ok atJ) -> + list + |> List.set i atJ + |> List.set j atI + + _ -> + list + + partition : Nat, Nat, List (Int a) -> [Pair Nat (List (Int a))] + partition = \low, high, initialList -> + when List.get initialList high is + Ok pivot -> + go = \i, j, list -> + if j < high then + when List.get list j is + Ok value -> + if value <= pivot then + go (i + 1) (j + 1) (swap (i + 1) j list) + else + go i (j + 1) list + + Err _ -> + Pair i list + else + Pair i list + + when go (low - 1) low initialList is + Pair newI newList -> + Pair (newI + 1) (swap (newI + 1) high newList) + + Err _ -> + Pair (low - 1) initialList + + partition + "# + ), + "Nat, Nat, List (Int a) -> [Pair Nat (List (Int a))]", + ); + } + + #[test] + fn identity_list() { + infer_eq_without_problem( + indoc!( + r#" + idList : List a -> List a + idList = \list -> list + + foo : List I64 -> List I64 + foo = \initialList -> idList initialList + + + foo + "# + ), + "List I64 -> List I64", + ); + } + + #[test] + fn list_get() { + infer_eq_without_problem( + indoc!( + r#" + List.get [10, 9, 8, 7] 1 + "# + ), + "Result (Num *) [OutOfBounds]", + ); + + infer_eq_without_problem( + indoc!( + r#" + List.get + "# + ), + "List a, Nat -> Result a [OutOfBounds]", + ); + } + + #[test] + fn use_rigid_twice() { + infer_eq_without_problem( + indoc!( + r#" + id1 : q -> q + id1 = \x -> x + + id2 : q -> q + id2 = \x -> x + + { id1, id2 } + "# + ), + "{ id1 : q -> q, id2 : q1 -> q1 }", + ); + } + + #[test] + fn map_insert() { + infer_eq_without_problem( + indoc!( + r#" + Dict.insert + "# + ), + "Dict k v, k, v -> Dict k v where k implements Hash & Eq", + ); + } + + #[test] + fn num_to_frac() { + infer_eq_without_problem( + indoc!( + r#" + Num.toFrac + "# + ), + "Num * -> Frac a", + ); + } + + #[test] + fn pow() { + infer_eq_without_problem( + indoc!( + r#" + Num.pow + "# + ), + "Frac a, Frac a -> Frac a", + ); + } + + #[test] + fn ceiling() { + infer_eq_without_problem( + indoc!( + r#" + Num.ceiling + "# + ), + "Frac * -> Int a", + ); + } + + #[test] + fn floor() { + infer_eq_without_problem( + indoc!( + r#" + Num.floor + "# + ), + "Frac * -> Int a", + ); + } + + #[test] + fn div() { + infer_eq_without_problem( + indoc!( + r#" + Num.div + "# + ), + "Frac a, Frac a -> Frac a", + ) + } + + #[test] + fn div_checked() { + infer_eq_without_problem( + indoc!( + r#" + Num.divChecked + "# + ), + "Frac a, Frac a -> Result (Frac a) [DivByZero]", + ) + } + + #[test] + fn div_ceil() { + infer_eq_without_problem( + indoc!( + r#" + Num.divCeil + "# + ), + "Int a, Int a -> Int a", + ); + } + + #[test] + fn div_ceil_checked() { + infer_eq_without_problem( + indoc!( + r#" + Num.divCeilChecked + "# + ), + "Int a, Int a -> Result (Int a) [DivByZero]", + ); + } + + #[test] + fn div_trunc() { + infer_eq_without_problem( + indoc!( + r#" + Num.divTrunc + "# + ), + "Int a, Int a -> Int a", + ); + } + + #[test] + fn div_trunc_checked() { + infer_eq_without_problem( + indoc!( + r#" + Num.divTruncChecked + "# + ), + "Int a, Int a -> Result (Int a) [DivByZero]", + ); + } + + #[test] + fn atan() { + infer_eq_without_problem( + indoc!( + r#" + Num.atan + "# + ), + "Frac a -> Frac a", + ); + } + + #[test] + fn min_i128() { + infer_eq_without_problem( + indoc!( + r#" + Num.minI128 + "# + ), + "I128", + ); + } + + #[test] + fn max_i128() { + infer_eq_without_problem( + indoc!( + r#" + Num.maxI128 + "# + ), + "I128", + ); + } + + #[test] + fn min_i64() { + infer_eq_without_problem( + indoc!( + r#" + Num.minI64 + "# + ), + "I64", + ); + } + + #[test] + fn max_i64() { + infer_eq_without_problem( + indoc!( + r#" + Num.maxI64 + "# + ), + "I64", + ); + } + + #[test] + fn min_u64() { + infer_eq_without_problem( + indoc!( + r#" + Num.minU64 + "# + ), + "U64", + ); + } + + #[test] + fn max_u64() { + infer_eq_without_problem( + indoc!( + r#" + Num.maxU64 + "# + ), + "U64", + ); + } + + #[test] + fn min_i32() { + infer_eq_without_problem( + indoc!( + r#" + Num.minI32 + "# + ), + "I32", + ); + } + + #[test] + fn max_i32() { + infer_eq_without_problem( + indoc!( + r#" + Num.maxI32 + "# + ), + "I32", + ); + } + + #[test] + fn min_u32() { + infer_eq_without_problem( + indoc!( + r#" + Num.minU32 + "# + ), + "U32", + ); + } + + #[test] + fn max_u32() { + infer_eq_without_problem( + indoc!( + r#" + Num.maxU32 + "# + ), + "U32", + ); + } + + #[test] + fn reconstruct_path() { + infer_eq_without_problem( + indoc!( + r#" + reconstructPath : Dict position position, position -> List position where position implements Hash & Eq + reconstructPath = \cameFrom, goal -> + when Dict.get cameFrom goal is + Err KeyNotFound -> + [] + + Ok next -> + List.append (reconstructPath cameFrom next) goal + + reconstructPath + "# + ), + "Dict position position, position -> List position where position implements Hash & Eq", + ); + } + + #[test] + fn use_correct_ext_record() { + // Related to a bug solved in 81fbab0b3fe4765bc6948727e603fc2d49590b1c + infer_eq_without_problem( + indoc!( + r#" + f = \r -> + g = r.q + h = r.p + + 42 + + f + "# + ), + "{ p : *, q : * }* -> Num *", + ); + } + + #[test] + fn use_correct_ext_tag_union() { + // related to a bug solved in 08c82bf151a85e62bce02beeed1e14444381069f + infer_eq_without_problem( + indoc!( + r#" + app "test" imports [Result.{ Result }] provides [main] to "./platform" + + boom = \_ -> boom {} + + Model position : { openSet : Set position } + + cheapestOpen : Model position -> Result position [KeyNotFound] where position implements Hash & Eq + cheapestOpen = \model -> + + folder = \resSmallestSoFar, position -> + when resSmallestSoFar is + Err _ -> resSmallestSoFar + Ok smallestSoFar -> + if position == smallestSoFar.position then resSmallestSoFar + + else + Ok { position, cost: 0.0 } + + Set.walk model.openSet (Ok { position: boom {}, cost: 0.0 }) folder + |> Result.map (\x -> x.position) + + astar : Model position -> Result position [KeyNotFound] where position implements Hash & Eq + astar = \model -> cheapestOpen model + + main = + astar + "# + ), + "Model position -> Result position [KeyNotFound] where position implements Hash & Eq", + ); + } + + #[test] + fn when_with_or_pattern_and_guard() { + infer_eq_without_problem( + indoc!( + r#" + \x -> + when x is + 2 | 3 -> 0 + a if a < 20 -> 1 + 3 | 4 if Bool.false -> 2 + _ -> 3 + "# + ), + "Num * -> Num *", + ); + } + + #[test] + fn sorting() { + // based on https://github.com/elm/compiler/issues/2057 + // Roc seems to do this correctly, tracking to make sure it stays that way + infer_eq_without_problem( + indoc!( + r#" + sort : ConsList cm -> ConsList cm + sort = + \xs -> + f : cm, cm -> Order + f = \_, _ -> LT + + sortWith f xs + + sortBy : (x -> cmpl), ConsList x -> ConsList x + sortBy = + \_, list -> + cmp : x, x -> Order + cmp = \_, _ -> LT + + sortWith cmp list + + always = \x, _ -> x + + sortWith : (foobar, foobar -> Order), ConsList foobar -> ConsList foobar + sortWith = + \_, list -> + f = \arg -> + g arg + + g = \bs -> + when bs is + bx -> f bx + + always Nil (f list) + + Order : [LT, GT, EQ] + ConsList a : [Nil, Cons a (ConsList a)] + + { x: sortWith, y: sort, z: sortBy } + "# + ), + "{ x : (foobar, foobar -> Order), ConsList foobar -> ConsList foobar, y : ConsList cm -> ConsList cm, z : (x -> cmpl), ConsList x -> ConsList x }" + ); + } + + // Like in elm, this test now fails. Polymorphic recursion (even with an explicit signature) + // yields a type error. + // + // We should at some point investigate why that is. Elm did support polymorphic recursion in + // earlier versions. + // + // #[test] + // fn wrapper() { + // // based on https://github.com/elm/compiler/issues/1964 + // // Roc seems to do this correctly, tracking to make sure it stays that way + // infer_eq_without_problem( + // indoc!( + // r#" + // Type a : [TypeCtor (Type (Wrapper a))] + // + // Wrapper a : [Wrapper a] + // + // Opaque : [Opaque] + // + // encodeType1 : Type a -> Opaque + // encodeType1 = \thing -> + // when thing is + // TypeCtor v0 -> + // encodeType1 v0 + // + // encodeType1 + // "# + // ), + // "Type a -> Opaque", + // ); + // } + + #[test] + fn rigids() { + infer_eq_without_problem( + indoc!( + r#" + f : List a -> List a + f = \input -> + # let-polymorphism at work + x : {} -> List b + x = \{} -> [] + + when List.get input 0 is + Ok val -> List.append (x {}) val + Err _ -> input + f + "# + ), + "List a -> List a", + ); + } + + #[cfg(debug_assertions)] + #[test] + #[should_panic] + fn rigid_record_quantification() { + // the ext here is qualified on the outside (because we have rank 1 types, not rank 2). + // That means e.g. `f : { bar : String, foo : I64 } -> Bool }` is a valid argument, but + // that function could not be applied to the `{ foo : I64 }` list. Therefore, this function + // is not allowed. + // + // should hit a debug_assert! in debug mode, and produce a type error in release mode + infer_eq_without_problem( + indoc!( + r#" + test : ({ foo : I64 }ext -> Bool), { foo : I64 } -> Bool + test = \fn, a -> fn a + + test + "# + ), + "should fail", + ); + } + + // OPTIONAL RECORD FIELDS + + #[test] + fn optional_field_unifies_with_missing() { + infer_eq_without_problem( + indoc!( + r#" + negatePoint : { x : I64, y : I64, z ? Num c } -> { x : I64, y : I64, z : Num c } + + negatePoint { x: 1, y: 2 } + "# + ), + "{ x : I64, y : I64, z : Num c }", + ); + } + + #[test] + fn open_optional_field_unifies_with_missing() { + infer_eq_without_problem( + indoc!( + r#" + negatePoint : { x : I64, y : I64, z ? Num c }r -> { x : I64, y : I64, z : Num c }r + + a = negatePoint { x: 1, y: 2 } + b = negatePoint { x: 1, y: 2, blah : "hi" } + + { a, b } + "# + ), + "{ a : { x : I64, y : I64, z : Num c }, b : { blah : Str, x : I64, y : I64, z : Num c1 } }", + ); + } + + #[test] + fn optional_field_unifies_with_present() { + infer_eq_without_problem( + indoc!( + r#" + negatePoint : { x : Num a, y : Num b, z ? c } -> { x : Num a, y : Num b, z : c } + + negatePoint { x: 1, y: 2.1, z: 0x3 } + "# + ), + "{ x : Num *, y : Frac *, z : Int * }", + ); + } + + #[test] + fn open_optional_field_unifies_with_present() { + infer_eq_without_problem( + indoc!( + r#" + negatePoint : { x : Num a, y : Num b, z ? c }r -> { x : Num a, y : Num b, z : c }r + + a = negatePoint { x: 1, y: 2.1 } + b = negatePoint { x: 1, y: 2.1, blah : "hi" } + + { a, b } + "# + ), + "{ a : { x : Num *, y : Frac *, z : c }, b : { blah : Str, x : Num *, y : Frac *, z : c1 } }", + ); + } + + #[test] + fn optional_field_function() { + infer_eq_without_problem( + indoc!( + r#" + \{ x, y ? 0 } -> x + y + "# + ), + "{ x : Num a, y ? Num a }* -> Num a", + ); + } + + #[test] + fn optional_field_let() { + infer_eq_without_problem( + indoc!( + r#" + { x, y ? 0 } = { x: 32 } + + x + y + "# + ), + "Num *", + ); + } + + #[test] + fn optional_field_when() { + infer_eq_without_problem( + indoc!( + r#" + \r -> + when r is + { x, y ? 0 } -> x + y + "# + ), + "{ x : Num a, y ? Num a }* -> Num a", + ); + } + + #[test] + fn optional_field_let_with_signature() { + infer_eq_without_problem( + indoc!( + r#" + \rec -> + { x, y } : { x : I64, y ? Bool }* + { x, y ? Bool.false } = rec + + { x, y } + "# + ), + "{ x : I64, y ? Bool }* -> { x : I64, y : Bool }", + ); + } + + #[test] + fn list_walk_backwards() { + infer_eq_without_problem( + indoc!( + r#" + List.walkBackwards + "# + ), + "List elem, state, (state, elem -> state) -> state", + ); + } + + #[test] + fn list_walk_backwards_example() { + infer_eq_without_problem( + indoc!( + r#" + empty : List I64 + empty = + [] + + List.walkBackwards empty 0 (\a, b -> a + b) + "# + ), + "I64", + ); + } + + #[test] + fn list_drop_at() { + infer_eq_without_problem( + indoc!( + r#" + List.dropAt + "# + ), + "List elem, Nat -> List elem", + ); + } + + #[test] + fn str_trim() { + infer_eq_without_problem( + indoc!( + r#" + Str.trim + "# + ), + "Str -> Str", + ); + } + + #[test] + fn str_trim_start() { + infer_eq_without_problem( + indoc!( + r#" + Str.trimStart + "# + ), + "Str -> Str", + ); + } + + #[test] + fn list_take_first() { + infer_eq_without_problem( + indoc!( + r#" + List.takeFirst + "# + ), + "List elem, Nat -> List elem", + ); + } + + #[test] + fn list_take_last() { + infer_eq_without_problem( + indoc!( + r#" + List.takeLast + "# + ), + "List elem, Nat -> List elem", + ); + } + + #[test] + fn list_sublist() { + infer_eq_without_problem( + indoc!( + r#" + List.sublist + "# + ), + "List elem, { len : Nat, start : Nat } -> List elem", + ); + } + + #[test] + fn list_split() { + infer_eq_without_problem( + indoc!("List.split"), + "List elem, Nat -> { before : List elem, others : List elem }", + ); + } + + #[test] + fn list_drop_last() { + infer_eq_without_problem( + indoc!( + r#" + List.dropLast + "# + ), + "List elem, Nat -> List elem", + ); + } + + #[test] + fn list_intersperse() { + infer_eq_without_problem( + indoc!( + r#" + List.intersperse + "# + ), + "List elem, elem -> List elem", + ); + } + #[test] + fn function_that_captures_nothing_is_not_captured() { + // we should make sure that a function that doesn't capture anything it not itself captured + // such functions will be lifted to the top-level, and are thus globally available! + infer_eq_without_problem( + indoc!( + r#" + f = \x -> x + 1 + + g = \y -> f y + + g + "# + ), + "Num a -> Num a", + ); + } + + #[test] + fn double_named_rigids() { + infer_eq_without_problem( + indoc!( + r#" + app "test" provides [main] to "./platform" + + + main : List x + main = + empty : List x + empty = [] + + empty + "# + ), + "List x", + ); + } + + #[test] + fn double_tag_application() { + infer_eq_without_problem( + indoc!( + r#" + app "test" provides [main] to "./platform" + + + main = + if 1 == 1 then + Foo (Bar) 1 + else + Foo Bar 1 + "# + ), + "[Foo [Bar] (Num *)]", + ); + + infer_eq_without_problem("Foo Bar 1", "[Foo [Bar] (Num *)]"); + } + + #[test] + fn double_tag_application_pattern() { + infer_eq_without_problem( + indoc!( + r#" + app "test" provides [main] to "./platform" + + Bar : [Bar] + Foo : [Foo Bar I64, Empty] + + foo : Foo + foo = Foo Bar 1 + + main = + when foo is + Foo Bar 1 -> + Foo Bar 2 + + x -> + x + "# + ), + "[Empty, Foo [Bar] I64]", + ); + } + + #[test] + fn recursive_function_with_rigid() { + infer_eq_without_problem( + indoc!( + r#" + app "test" provides [main] to "./platform" + + State a : { count : I64, x : a } + + foo : State a -> I64 + foo = \state -> + if state.count == 0 then + 0 + else + 1 + foo { count: state.count - 1, x: state.x } + + main : I64 + main = + foo { count: 3, x: {} } + "# + ), + "I64", + ); + } + + #[test] + fn rbtree_empty() { + infer_eq_without_problem( + indoc!( + r#" + app "test" provides [main] to "./platform" + + # The color of a node. Leaves are considered Black. + NodeColor : [Red, Black] + + RBTree k v : [Node NodeColor k v (RBTree k v) (RBTree k v), Empty] + + # Create an empty dictionary. + empty : {} -> RBTree k v + empty = \{} -> + Empty + + foo : RBTree I64 I64 + foo = empty {} + + main : RBTree I64 I64 + main = + foo + "# + ), + "RBTree I64 I64", + ); + } + + #[test] + fn rbtree_insert() { + // exposed an issue where pattern variables were not introduced + // at the correct level in the constraint + // + // see 22592eff805511fbe1da63849771ee5f367a6a16 + infer_eq_without_problem( + indoc!( + r#" + app "test" provides [main] to "./platform" + + RBTree k : [Node k (RBTree k), Empty] + + balance : RBTree k -> RBTree k + balance = \left -> + when left is + Node _ Empty -> Empty + + _ -> Empty + + main : RBTree {} + main = + balance Empty + "# + ), + "RBTree {}", + ); + } + + #[test] + fn rbtree_full_remove_min() { + infer_eq_without_problem( + indoc!( + r#" + app "test" provides [main] to "./platform" + + NodeColor : [Red, Black] + + RBTree k v : [Node NodeColor k v (RBTree k v) (RBTree k v), Empty] + + moveRedLeft : RBTree k v -> RBTree k v + moveRedLeft = \dict -> + when dict is + # Node clr k v (Node lClr lK lV lLeft lRight) (Node rClr rK rV ((Node Red rlK rlV rlL rlR) as rLeft) rRight) -> + # Node clr k v (Node lClr lK lV lLeft lRight) (Node rClr rK rV rLeft rRight) -> + Node clr k v (Node _ lK lV lLeft lRight) (Node _ rK rV rLeft rRight) -> + when rLeft is + Node Red rlK rlV rlL rlR -> + Node + Red + rlK + rlV + (Node Black k v (Node Red lK lV lLeft lRight) rlL) + (Node Black rK rV rlR rRight) + + _ -> + when clr is + Black -> + Node + Black + k + v + (Node Red lK lV lLeft lRight) + (Node Red rK rV rLeft rRight) + + Red -> + Node + Black + k + v + (Node Red lK lV lLeft lRight) + (Node Red rK rV rLeft rRight) + + _ -> + dict + + balance : NodeColor, k, v, RBTree k v, RBTree k v -> RBTree k v + balance = \color, key, value, left, right -> + when right is + Node Red rK rV rLeft rRight -> + when left is + Node Red lK lV lLeft lRight -> + Node + Red + key + value + (Node Black lK lV lLeft lRight) + (Node Black rK rV rLeft rRight) + + _ -> + Node color rK rV (Node Red key value left rLeft) rRight + + _ -> + when left is + Node Red lK lV (Node Red llK llV llLeft llRight) lRight -> + Node + Red + lK + lV + (Node Black llK llV llLeft llRight) + (Node Black key value lRight right) + + _ -> + Node color key value left right + + + Key k : Num k + + removeHelpEQGT : Key k, RBTree (Key k) v -> RBTree (Key k) v where k implements Hash & Eq + removeHelpEQGT = \targetKey, dict -> + when dict is + Node color key value left right -> + if targetKey == key then + when getMin right is + Node _ minKey minValue _ _ -> + balance color minKey minValue left (removeMin right) + + Empty -> + Empty + else + balance color key value left (removeHelp targetKey right) + + Empty -> + Empty + + getMin : RBTree k v -> RBTree k v + getMin = \dict -> + when dict is + # Node _ _ _ ((Node _ _ _ _ _) as left) _ -> + Node _ _ _ left _ -> + when left is + Node _ _ _ _ _ -> getMin left + _ -> dict + + _ -> + dict + + + moveRedRight : RBTree k v -> RBTree k v + moveRedRight = \dict -> + when dict is + Node clr k v (Node lClr lK lV (Node Red llK llV llLeft llRight) lRight) (Node rClr rK rV rLeft rRight) -> + Node + Red + lK + lV + (Node Black llK llV llLeft llRight) + (Node Black k v lRight (Node Red rK rV rLeft rRight)) + + Node clr k v (Node lClr lK lV lLeft lRight) (Node rClr rK rV rLeft rRight) -> + when clr is + Black -> + Node + Black + k + v + (Node Red lK lV lLeft lRight) + (Node Red rK rV rLeft rRight) + + Red -> + Node + Black + k + v + (Node Red lK lV lLeft lRight) + (Node Red rK rV rLeft rRight) + + _ -> + dict + + + removeHelpPrepEQGT : Key k, RBTree (Key k) v, NodeColor, (Key k), v, RBTree (Key k) v, RBTree (Key k) v -> RBTree (Key k) v + removeHelpPrepEQGT = \_, dict, color, key, value, left, right -> + when left is + Node Red lK lV lLeft lRight -> + Node + color + lK + lV + lLeft + (Node Red key value lRight right) + + _ -> + when right is + Node Black _ _ (Node Black _ _ _ _) _ -> + moveRedRight dict + + Node Black _ _ Empty _ -> + moveRedRight dict + + _ -> + dict + + + removeMin : RBTree k v -> RBTree k v + removeMin = \dict -> + when dict is + Node color key value left right -> + when left is + Node lColor _ _ lLeft _ -> + when lColor is + Black -> + when lLeft is + Node Red _ _ _ _ -> + Node color key value (removeMin left) right + + _ -> + when moveRedLeft dict is # here 1 + Node nColor nKey nValue nLeft nRight -> + balance nColor nKey nValue (removeMin nLeft) nRight + + Empty -> + Empty + + _ -> + Node color key value (removeMin left) right + + _ -> + Empty + _ -> + Empty + + removeHelp : Key k, RBTree (Key k) v -> RBTree (Key k) v where k implements Hash & Eq + removeHelp = \targetKey, dict -> + when dict is + Empty -> + Empty + + Node color key value left right -> + if targetKey < key then + when left is + Node Black _ _ lLeft _ -> + when lLeft is + Node Red _ _ _ _ -> + Node color key value (removeHelp targetKey left) right + + _ -> + when moveRedLeft dict is # here 2 + Node nColor nKey nValue nLeft nRight -> + balance nColor nKey nValue (removeHelp targetKey nLeft) nRight + + Empty -> + Empty + + _ -> + Node color key value (removeHelp targetKey left) right + else + removeHelpEQGT targetKey (removeHelpPrepEQGT targetKey dict color key value left right) + + + main : RBTree I64 I64 + main = + removeHelp 1i64 Empty + "# + ), + "RBTree (Key (Integer Signed64)) I64", + ); + } + + #[test] + fn rbtree_remove_min_1() { + infer_eq_without_problem( + indoc!( + r#" + app "test" provides [main] to "./platform" + + RBTree k : [Node k (RBTree k) (RBTree k), Empty] + + removeHelp : Num k, RBTree (Num k) -> RBTree (Num k) + removeHelp = \targetKey, dict -> + when dict is + Empty -> + Empty + + Node key left right -> + if targetKey < key then + when left is + Node _ lLeft _ -> + when lLeft is + Node _ _ _ -> + Empty + + _ -> Empty + + _ -> + Node key (removeHelp targetKey left) right + else + Empty + + + main : RBTree I64 + main = + removeHelp 1 Empty + "# + ), + "RBTree I64", + ); + } + + #[test] + fn rbtree_foobar() { + infer_eq_without_problem( + indoc!( + r#" + app "test" provides [main] to "./platform" + + NodeColor : [Red, Black] + + RBTree k v : [Node NodeColor k v (RBTree k v) (RBTree k v), Empty] + + removeHelp : Num k, RBTree (Num k) v -> RBTree (Num k) v where k implements Hash & Eq + removeHelp = \targetKey, dict -> + when dict is + Empty -> + Empty + + Node color key value left right -> + if targetKey < key then + when left is + Node Black _ _ lLeft _ -> + when lLeft is + Node Red _ _ _ _ -> + Node color key value (removeHelp targetKey left) right + + _ -> + when moveRedLeft dict is # here 2 + Node nColor nKey nValue nLeft nRight -> + balance nColor nKey nValue (removeHelp targetKey nLeft) nRight + + Empty -> + Empty + + _ -> + Node color key value (removeHelp targetKey left) right + else + removeHelpEQGT targetKey (removeHelpPrepEQGT targetKey dict color key value left right) + + Key k : Num k + + balance : NodeColor, k, v, RBTree k v, RBTree k v -> RBTree k v + + moveRedLeft : RBTree k v -> RBTree k v + + removeHelpPrepEQGT : Key k, RBTree (Key k) v, NodeColor, (Key k), v, RBTree (Key k) v, RBTree (Key k) v -> RBTree (Key k) v + + removeHelpEQGT : Key k, RBTree (Key k) v -> RBTree (Key k) v where k implements Hash & Eq + removeHelpEQGT = \targetKey, dict -> + when dict is + Node color key value left right -> + if targetKey == key then + when getMin right is + Node _ minKey minValue _ _ -> + balance color minKey minValue left (removeMin right) + + Empty -> + Empty + else + balance color key value left (removeHelp targetKey right) + + Empty -> + Empty + + getMin : RBTree k v -> RBTree k v + + removeMin : RBTree k v -> RBTree k v + + main : RBTree I64 I64 + main = + removeHelp 1i64 Empty + "# + ), + "RBTree I64 I64", + ); + } + + #[test] + fn quicksort_partition_help() { + infer_eq_without_problem( + indoc!( + r#" + app "test" provides [partitionHelp] to "./platform" + + swap : Nat, Nat, List a -> List a + swap = \i, j, list -> + when Pair (List.get list i) (List.get list j) is + Pair (Ok atI) (Ok atJ) -> + list + |> List.set i atJ + |> List.set j atI + + _ -> + [] + + partitionHelp : Nat, Nat, List (Num a), Nat, (Num a) -> [Pair Nat (List (Num a))] + partitionHelp = \i, j, list, high, pivot -> + if j < high then + when List.get list j is + Ok value -> + if value <= pivot then + partitionHelp (i + 1) (j + 1) (swap (i + 1) j list) high pivot + else + partitionHelp i (j + 1) list high pivot + + Err _ -> + Pair i list + else + Pair i list + "# + ), + "Nat, Nat, List (Num a), Nat, Num a -> [Pair Nat (List (Num a))]", + ); + } + + #[test] + fn rbtree_old_balance_simplified() { + infer_eq_without_problem( + indoc!( + r#" + app "test" provides [main] to "./platform" + + RBTree k : [Node k (RBTree k) (RBTree k), Empty] + + balance : k, RBTree k -> RBTree k + balance = \key, left -> + Node key left Empty + + main : RBTree I64 + main = + balance 0 Empty + "# + ), + "RBTree I64", + ); + } + + #[test] + fn rbtree_balance_simplified() { + infer_eq_without_problem( + indoc!( + r#" + app "test" provides [main] to "./platform" + + RBTree k : [Node k (RBTree k) (RBTree k), Empty] + + node = \x,y,z -> Node x y z + + balance : k, RBTree k -> RBTree k + balance = \key, left -> + node key left Empty + + main : RBTree I64 + main = + balance 0 Empty + "# + ), + "RBTree I64", + ); + } + + #[test] + fn rbtree_balance() { + infer_eq_without_problem( + indoc!( + r#" + app "test" provides [main] to "./platform" + + NodeColor : [Red, Black] + + RBTree k v : [Node NodeColor k v (RBTree k v) (RBTree k v), Empty] + + balance : NodeColor, k, v, RBTree k v, RBTree k v -> RBTree k v + balance = \color, key, value, left, right -> + when right is + Node Red rK rV rLeft rRight -> + when left is + Node Red lK lV lLeft lRight -> + Node + Red + key + value + (Node Black lK lV lLeft lRight) + (Node Black rK rV rLeft rRight) + + _ -> + Node color rK rV (Node Red key value left rLeft) rRight + + _ -> + when left is + Node Red lK lV (Node Red llK llV llLeft llRight) lRight -> + Node + Red + lK + lV + (Node Black llK llV llLeft llRight) + (Node Black key value lRight right) + + _ -> + Node color key value left right + + main : RBTree I64 I64 + main = + balance Red 0 0 Empty Empty + "# + ), + "RBTree I64 I64", + ); + } + + #[test] + fn pattern_rigid_problem() { + infer_eq_without_problem( + indoc!( + r#" + app "test" provides [main] to "./platform" + + RBTree k : [Node k (RBTree k) (RBTree k), Empty] + + balance : k, RBTree k -> RBTree k + balance = \key, left -> + when left is + Node _ _ lRight -> + Node key lRight Empty + + _ -> + Empty + + + main : RBTree I64 + main = + balance 0 Empty + "# + ), + "RBTree I64", + ); + } + + #[test] + fn expr_to_str() { + infer_eq_without_problem( + indoc!( + r#" + app "test" provides [main] to "./platform" + + Expr : [Add Expr Expr, Val I64, Var I64] + + printExpr : Expr -> Str + printExpr = \e -> + when e is + Add a b -> + "Add (" + |> Str.concat (printExpr a) + |> Str.concat ") (" + |> Str.concat (printExpr b) + |> Str.concat ")" + Val v -> Num.toStr v + Var v -> "Var " |> Str.concat (Num.toStr v) + + main : Str + main = printExpr (Var 3) + "# + ), + "Str", + ); + } + + #[test] + fn int_type_let_polymorphism() { + infer_eq_without_problem( + indoc!( + r#" + app "test" provides [main] to "./platform" + + x = 4 + + f : U8 -> U32 + f = \z -> Num.intCast z + + y = f x + + main = + x + "# + ), + "Num *", + ); + } + + #[test] + fn rigid_type_variable_problem() { + // see https://github.com/roc-lang/roc/issues/1162 + infer_eq_without_problem( + indoc!( + r#" + app "test" provides [main] to "./platform" + + RBTree k : [Node k (RBTree k) (RBTree k), Empty] + + balance : a, RBTree a -> RBTree a + balance = \key, left -> + when left is + Node _ _ lRight -> + Node key lRight Empty + + _ -> + Empty + + + main : RBTree {} + main = + balance {} Empty + "# + ), + "RBTree {}", + ); + } + + #[test] + fn inference_var_inside_arrow() { + infer_eq_without_problem( + indoc!( + r#" + id : _ -> _ + id = \x -> x + id + "# + ), + "a -> a", + ) + } + + #[test] + fn inference_var_inside_ctor() { + infer_eq_without_problem( + indoc!( + r#" + canIGo : _ -> Result.Result _ _ + canIGo = \color -> + when color is + "green" -> Ok "go!" + "yellow" -> Err (SlowIt "whoa, let's slow down!") + "red" -> Err (StopIt "absolutely not") + _ -> Err (UnknownColor "this is a weird stoplight") + canIGo + "# + ), + "Str -> Result Str [SlowIt Str, StopIt Str, UnknownColor Str]", + ) + } + + #[test] + fn inference_var_inside_ctor_linked() { + infer_eq_without_problem( + indoc!( + r#" + swapRcd: {x: _, y: _} -> {x: _, y: _} + swapRcd = \{x, y} -> {x: y, y: x} + swapRcd + "# + ), + "{ x : a, y : b } -> { x : b, y : a }", + ) + } + + #[test] + fn inference_var_link_with_rigid() { + infer_eq_without_problem( + indoc!( + r#" + swapRcd: {x: tx, y: ty} -> {x: _, y: _} + swapRcd = \{x, y} -> {x: y, y: x} + swapRcd + "# + ), + "{ x : tx, y : ty } -> { x : ty, y : tx }", + ) + } + + #[test] + fn inference_var_inside_tag_ctor() { + infer_eq_without_problem( + indoc!( + r#" + badComics: [True, False] -> [CowTools _, Thagomizer _] + badComics = \c -> + when c is + True -> CowTools "The Far Side" + False -> Thagomizer "The Far Side" + badComics + "# + ), + "[False, True] -> [CowTools Str, Thagomizer Str]", + ) + } + + #[test] + fn inference_var_tag_union_ext() { + // TODO: we should really be inferring [Blue, Orange]a -> [Lavender, Peach]a here. + // See https://github.com/roc-lang/roc/issues/2053 + infer_eq_without_problem( + indoc!( + r#" + pastelize: _ -> [Lavender, Peach]_ + pastelize = \color -> + when color is + Blue -> Lavender + Orange -> Peach + col -> col + pastelize + "# + ), + "[Blue, Lavender, Orange, Peach]a -> [Blue, Lavender, Orange, Peach]a", + ) + } + + #[test] + fn inference_var_rcd_union_ext() { + infer_eq_without_problem( + indoc!( + r#" + setRocEmail : _ -> { name: Str, email: Str }_ + setRocEmail = \person -> + { person & email: "\(person.name)@roclang.com" } + setRocEmail + "# + ), + "{ email : Str, name : Str }a -> { email : Str, name : Str }a", + ) + } + + #[test] + fn issue_2217() { + infer_eq_without_problem( + indoc!( + r#" + LinkedList elem : [Empty, Prepend (LinkedList elem) elem] + + fromList : List elem -> LinkedList elem + fromList = \elems -> List.walk elems Empty Prepend + + fromList + "# + ), + "List elem -> LinkedList elem", + ) + } + + #[test] + fn issue_2217_inlined() { + infer_eq_without_problem( + indoc!( + r#" + fromList : List elem -> [Empty, Prepend (LinkedList elem) elem] as LinkedList elem + fromList = \elems -> List.walk elems Empty Prepend + + fromList + "# + ), + "List elem -> LinkedList elem", + ) + } + + #[test] + fn infer_union_input_position1() { + infer_eq_without_problem( + indoc!( + r#" + \tag -> + when tag is + A -> X + B -> Y + "# + ), + "[A, B] -> [X, Y]", + ) + } + + #[test] + fn infer_union_input_position2() { + infer_eq_without_problem( + indoc!( + r#" + \tag -> + when tag is + A -> X + B -> Y + _ -> Z + "# + ), + "[A, B]* -> [X, Y, Z]", + ) + } + + #[test] + fn infer_union_input_position3() { + infer_eq_without_problem( + indoc!( + r#" + \tag -> + when tag is + A M -> X + A N -> Y + "# + ), + "[A [M, N]] -> [X, Y]", + ) + } + + #[test] + fn infer_union_input_position4() { + infer_eq_without_problem( + indoc!( + r#" + \tag -> + when tag is + A M -> X + A N -> Y + A _ -> Z + "# + ), + "[A [M, N]*] -> [X, Y, Z]", + ) + } + + #[test] + fn infer_union_input_position5() { + infer_eq_without_problem( + indoc!( + r#" + \tag -> + when tag is + A (M J) -> X + A (N K) -> X + "# + ), + "[A [M [J], N [K]]] -> [X]", + ) + } + + #[test] + fn infer_union_input_position6() { + infer_eq_without_problem( + indoc!( + r#" + \tag -> + when tag is + A M -> X + B -> X + A N -> X + "# + ), + "[A [M, N], B] -> [X]", + ) + } + + #[test] + fn infer_union_input_position7() { + infer_eq_without_problem( + indoc!( + r#" + \tag -> + when tag is + A -> X + t -> t + "# + ), + "[A, X]a -> [A, X]a", + ) + } + + #[test] + fn infer_union_input_position8() { + infer_eq_without_problem( + indoc!( + r#" + \opt -> + when opt is + Some ({tag: A}) -> 1 + Some ({tag: B}) -> 1 + None -> 0 + "# + ), + "[None, Some { tag : [A, B] }*] -> Num *", + ) + } + + #[test] + fn infer_union_input_position9() { + infer_eq_without_problem( + indoc!( + r#" + opt : [Some Str, None] + opt = Some "" + rcd = { opt } + + when rcd is + { opt: Some s } -> s + { opt: None } -> "?" + "# + ), + "Str", + ) + } + + #[test] + fn infer_union_input_position10() { + infer_eq_without_problem( + indoc!( + r#" + \r -> + when r is + { x: Blue, y ? 3 } -> y + { x: Red, y ? 5 } -> y + "# + ), + "{ x : [Blue, Red], y ? Num a }* -> Num a", + ) + } + + #[test] + // Issue #2299 + fn infer_union_argument_position() { + infer_eq_without_problem( + indoc!( + r#" + \UserId id -> id + 1 + "# + ), + "[UserId (Num a)] -> Num a", + ) + } + + #[test] + fn infer_union_def_position() { + infer_eq_without_problem( + indoc!( + r#" + \email -> + Email str = email + Str.isEmpty str + "# + ), + "[Email Str] -> Bool", + ) + } + + #[test] + fn numeric_literal_suffixes() { + infer_eq_without_problem( + indoc!( + r#" + { + u8: 123u8, + u16: 123u16, + u32: 123u32, + u64: 123u64, + u128: 123u128, + + i8: 123i8, + i16: 123i16, + i32: 123i32, + i64: 123i64, + i128: 123i128, + + nat: 123nat, + + bu8: 0b11u8, + bu16: 0b11u16, + bu32: 0b11u32, + bu64: 0b11u64, + bu128: 0b11u128, + + bi8: 0b11i8, + bi16: 0b11i16, + bi32: 0b11i32, + bi64: 0b11i64, + bi128: 0b11i128, + + bnat: 0b11nat, + + dec: 123.0dec, + f32: 123.0f32, + f64: 123.0f64, + + fdec: 123dec, + ff32: 123f32, + ff64: 123f64, + } + "# + ), + r#"{ bi128 : I128, bi16 : I16, bi32 : I32, bi64 : I64, bi8 : I8, bnat : Nat, bu128 : U128, bu16 : U16, bu32 : U32, bu64 : U64, bu8 : U8, dec : Dec, f32 : F32, f64 : F64, fdec : Dec, ff32 : F32, ff64 : F64, i128 : I128, i16 : I16, i32 : I32, i64 : I64, i8 : I8, nat : Nat, u128 : U128, u16 : U16, u32 : U32, u64 : U64, u8 : U8 }"#, + ) + } + + #[test] + fn numeric_literal_suffixes_in_pattern() { + infer_eq_without_problem( + indoc!( + r#" + { + u8: (\n -> + when n is + 123u8 -> n + _ -> n), + u16: (\n -> + when n is + 123u16 -> n + _ -> n), + u32: (\n -> + when n is + 123u32 -> n + _ -> n), + u64: (\n -> + when n is + 123u64 -> n + _ -> n), + u128: (\n -> + when n is + 123u128 -> n + _ -> n), + + i8: (\n -> + when n is + 123i8 -> n + _ -> n), + i16: (\n -> + when n is + 123i16 -> n + _ -> n), + i32: (\n -> + when n is + 123i32 -> n + _ -> n), + i64: (\n -> + when n is + 123i64 -> n + _ -> n), + i128: (\n -> + when n is + 123i128 -> n + _ -> n), + + nat: (\n -> + when n is + 123nat -> n + _ -> n), + + bu8: (\n -> + when n is + 0b11u8 -> n + _ -> n), + bu16: (\n -> + when n is + 0b11u16 -> n + _ -> n), + bu32: (\n -> + when n is + 0b11u32 -> n + _ -> n), + bu64: (\n -> + when n is + 0b11u64 -> n + _ -> n), + bu128: (\n -> + when n is + 0b11u128 -> n + _ -> n), + + bi8: (\n -> + when n is + 0b11i8 -> n + _ -> n), + bi16: (\n -> + when n is + 0b11i16 -> n + _ -> n), + bi32: (\n -> + when n is + 0b11i32 -> n + _ -> n), + bi64: (\n -> + when n is + 0b11i64 -> n + _ -> n), + bi128: (\n -> + when n is + 0b11i128 -> n + _ -> n), + + bnat: (\n -> + when n is + 0b11nat -> n + _ -> n), + + dec: (\n -> + when n is + 123.0dec -> n + _ -> n), + f32: (\n -> + when n is + 123.0f32 -> n + _ -> n), + f64: (\n -> + when n is + 123.0f64 -> n + _ -> n), + + fdec: (\n -> + when n is + 123dec -> n + _ -> n), + ff32: (\n -> + when n is + 123f32 -> n + _ -> n), + ff64: (\n -> + when n is + 123f64 -> n + _ -> n), + } + "# + ), + r#"{ bi128 : I128 -> I128, bi16 : I16 -> I16, bi32 : I32 -> I32, bi64 : I64 -> I64, bi8 : I8 -> I8, bnat : Nat -> Nat, bu128 : U128 -> U128, bu16 : U16 -> U16, bu32 : U32 -> U32, bu64 : U64 -> U64, bu8 : U8 -> U8, dec : Dec -> Dec, f32 : F32 -> F32, f64 : F64 -> F64, fdec : Dec -> Dec, ff32 : F32 -> F32, ff64 : F64 -> F64, i128 : I128 -> I128, i16 : I16 -> I16, i32 : I32 -> I32, i64 : I64 -> I64, i8 : I8 -> I8, nat : Nat -> Nat, u128 : U128 -> U128, u16 : U16 -> U16, u32 : U32 -> U32, u64 : U64 -> U64, u8 : U8 -> U8 }"#, + ) + } +} diff --git a/crates/compiler/solve_problem/Cargo.toml b/crates/compiler/solve_problem/Cargo.toml new file mode 100644 index 0000000000..0c33244b60 --- /dev/null +++ b/crates/compiler/solve_problem/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "roc_solve_problem" +description = "Provides types to describe problems that can occur during solving." + +authors.workspace = true +edition.workspace = true +license.workspace = true +version.workspace = true + +[dependencies] +roc_can = { path = "../can" } +roc_collections = { path = "../collections" } +roc_exhaustive = { path = "../exhaustive" } +roc_module = { path = "../module" } +roc_problem = { path = "../problem" } +roc_region = { path = "../region" } +roc_types = { path = "../types" } diff --git a/crates/compiler/solve_problem/src/lib.rs b/crates/compiler/solve_problem/src/lib.rs new file mode 100644 index 0000000000..f8118a4495 --- /dev/null +++ b/crates/compiler/solve_problem/src/lib.rs @@ -0,0 +1,143 @@ +//! Provides types to describe problems that can occur during solving. +use std::{path::PathBuf, str::Utf8Error}; + +use roc_can::expected::{Expected, PExpected}; +use roc_module::{ident::Lowercase, symbol::Symbol}; +use roc_problem::{can::CycleEntry, Severity}; +use roc_region::all::Region; + +use roc_types::types::{Category, ErrorType, PatternCategory}; + +#[derive(Debug, Clone)] +pub enum TypeError { + BadExpr(Region, Category, ErrorType, Expected), + BadPattern(Region, PatternCategory, ErrorType, PExpected), + CircularType(Region, Symbol, ErrorType), + CircularDef(Vec), + UnexposedLookup(Region, Symbol), + UnfulfilledAbility(Unfulfilled), + BadExprMissingAbility(Region, Category, ErrorType, Vec), + BadPatternMissingAbility(Region, PatternCategory, ErrorType, Vec), + Exhaustive(roc_exhaustive::Error), + StructuralSpecialization { + region: Region, + typ: ErrorType, + ability: Symbol, + member: Symbol, + }, + WrongSpecialization { + region: Region, + ability_member: Symbol, + expected_opaque: Symbol, + found_opaque: Symbol, + }, + IngestedFileBadUtf8(Box, Utf8Error), + IngestedFileUnsupportedType(Box, ErrorType), +} + +impl TypeError { + pub fn severity(&self) -> Severity { + use Severity::*; + match self { + TypeError::BadExpr(..) => RuntimeError, + TypeError::BadPattern(..) => RuntimeError, + TypeError::CircularType(..) => RuntimeError, + TypeError::CircularDef(_) => RuntimeError, + TypeError::UnexposedLookup(..) => RuntimeError, + TypeError::UnfulfilledAbility(_) => RuntimeError, + TypeError::BadExprMissingAbility(_, _, _, _) => RuntimeError, + TypeError::BadPatternMissingAbility(_, _, _, _) => RuntimeError, + // NB: if bidirectional exhaustiveness checking is implemented, the other direction + // is also not a runtime error. + TypeError::Exhaustive(exhtv) => exhtv.severity(), + TypeError::StructuralSpecialization { .. } => RuntimeError, + TypeError::WrongSpecialization { .. } => RuntimeError, + TypeError::IngestedFileBadUtf8(..) => Fatal, + TypeError::IngestedFileUnsupportedType(..) => Fatal, + } + } + + pub fn region(&self) -> Option { + match self { + TypeError::BadExpr(region, ..) + | TypeError::BadPattern(region, ..) + | TypeError::CircularType(region, ..) + | TypeError::UnexposedLookup(region, ..) + | TypeError::BadExprMissingAbility(region, ..) + | TypeError::StructuralSpecialization { region, .. } + | TypeError::WrongSpecialization { region, .. } + | TypeError::BadPatternMissingAbility(region, ..) => Some(*region), + TypeError::UnfulfilledAbility(ab, ..) => ab.region(), + TypeError::Exhaustive(e) => Some(e.region()), + TypeError::CircularDef(c) => c.first().map(|ce| ce.symbol_region), + TypeError::IngestedFileBadUtf8(_, _) => None, + TypeError::IngestedFileUnsupportedType(_, _) => None, + } + } +} + +#[derive(PartialEq, Eq, Debug, Clone)] +pub enum Unfulfilled { + /// No claimed implementation of an ability for an opaque type. + OpaqueDoesNotImplement { typ: Symbol, ability: Symbol }, + /// Cannot derive implementation of an ability for a structural type. + AdhocUnderivable { + typ: ErrorType, + ability: Symbol, + reason: UnderivableReason, + }, + /// Cannot derive implementation of an ability for an opaque type. + OpaqueUnderivable { + typ: ErrorType, + ability: Symbol, + opaque: Symbol, + derive_region: Region, + reason: UnderivableReason, + }, +} + +impl Unfulfilled { + fn region(&self) -> Option { + match self { + Unfulfilled::OpaqueDoesNotImplement { .. } => None, + Unfulfilled::AdhocUnderivable { .. } => None, + Unfulfilled::OpaqueUnderivable { derive_region, .. } => Some(*derive_region), + } + } +} + +#[derive(PartialEq, Eq, Debug, Clone)] +pub enum UnderivableReason { + NotABuiltin, + /// The surface type is not derivable + SurfaceNotDerivable(NotDerivableContext), + /// A nested type is not derivable + NestedNotDerivable(ErrorType, NotDerivableContext), +} + +#[derive(PartialEq, Eq, Debug, Clone)] +pub enum NotDerivableContext { + NoContext, + Function, + UnboundVar, + Opaque(Symbol), + Encode(NotDerivableEncode), + Decode(NotDerivableDecode), + Eq(NotDerivableEq), +} + +#[derive(PartialEq, Eq, Debug, Clone)] +pub enum NotDerivableEncode { + Nat, +} + +#[derive(PartialEq, Eq, Debug, Clone)] +pub enum NotDerivableDecode { + Nat, + OptionalRecordField(Lowercase), +} + +#[derive(PartialEq, Eq, Debug, Clone)] +pub enum NotDerivableEq { + FloatingPoint, +} diff --git a/crates/compiler/solve_schema/Cargo.toml b/crates/compiler/solve_schema/Cargo.toml new file mode 100644 index 0000000000..5a73baa60d --- /dev/null +++ b/crates/compiler/solve_schema/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "roc_solve_schema" +description = "Types used in the solver." + +authors.workspace = true +edition.workspace = true +license.workspace = true +version.workspace = true + +[dependencies] +bitflags.workspace = true diff --git a/crates/compiler/solve_schema/src/lib.rs b/crates/compiler/solve_schema/src/lib.rs new file mode 100644 index 0000000000..1cd34f5e4e --- /dev/null +++ b/crates/compiler/solve_schema/src/lib.rs @@ -0,0 +1,3 @@ +mod unify; + +pub use unify::UnificationMode; diff --git a/crates/compiler/solve_schema/src/unify.rs b/crates/compiler/solve_schema/src/unify.rs new file mode 100644 index 0000000000..22b36f2e8c --- /dev/null +++ b/crates/compiler/solve_schema/src/unify.rs @@ -0,0 +1,50 @@ +use bitflags::bitflags; + +bitflags! { + pub struct UnificationMode : u8 { + /// Instructs the unifier to solve two types for equality. + /// + /// For example, { n : Str }a ~ { n: Str, m : Str } will solve "a" to "{ m : Str }". + const EQ = 1 << 0; + /// Instructs the unifier to treat the right-hand-side of a constraint as + /// present in the left-hand-side, rather than strictly equal. + /// + /// For example, t1 += [A Str] says we should "add" the tag "A Str" to the type of "t1". + const PRESENT = 1 << 1; + /// Like [`UnificationMode::EQ`], but also instructs the unifier that the ambient lambda set + /// specialization algorithm is running. This has implications for the unification of + /// unspecialized lambda sets; see [`unify_unspecialized_lambdas`]. + const LAMBDA_SET_SPECIALIZATION = UnificationMode::EQ.bits | (1 << 2); + } +} + +impl UnificationMode { + pub fn is_eq(&self) -> bool { + debug_assert!(!self.contains(UnificationMode::EQ | UnificationMode::PRESENT)); + self.contains(UnificationMode::EQ) + } + + pub fn is_present(&self) -> bool { + debug_assert!(!self.contains(UnificationMode::EQ | UnificationMode::PRESENT)); + self.contains(UnificationMode::PRESENT) + } + + pub fn is_lambda_set_specialization(&self) -> bool { + debug_assert!(!self.contains(UnificationMode::EQ | UnificationMode::PRESENT)); + self.contains(UnificationMode::LAMBDA_SET_SPECIALIZATION) + } + + pub fn as_eq(self) -> Self { + (self - UnificationMode::PRESENT) | UnificationMode::EQ + } + + pub fn pretty_print(&self) -> &str { + if self.contains(UnificationMode::EQ) { + "~" + } else if self.contains(UnificationMode::PRESENT) { + "+=" + } else { + unreachable!("Bad mode!") + } + } +} diff --git a/crates/compiler/test_derive/Cargo.toml b/crates/compiler/test_derive/Cargo.toml new file mode 100644 index 0000000000..2c5f5e14a3 --- /dev/null +++ b/crates/compiler/test_derive/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "test_derive" +description = "Tests Roc's auto-derivers." + +authors.workspace = true +edition.workspace = true +license.workspace = true +version.workspace = true + +[[test]] +name = "test_derive" +path = "src/tests.rs" + +[dev-dependencies] +roc_builtins = { path = "../builtins" } +roc_can = { path = "../can" } +roc_collections = { path = "../collections" } +roc_constrain = { path = "../constrain" } +roc_debug_flags = { path = "../debug_flags" } +roc_derive = { path = "../derive", features = ["debug-derived-symbols", "open-extension-vars"] } +roc_derive_key = { path = "../derive_key" } +roc_load_internal = { path = "../load_internal" } +roc_module = { path = "../module" } +roc_packaging = { path = "../../packaging" } +roc_region = { path = "../region" } +roc_reporting = { path = "../../reporting" } +roc_solve = { path = "../solve" } +roc_target = { path = "../roc_target" } +roc_types = { path = "../types" } + +ven_pretty = { path = "../../vendor/pretty" } + +bumpalo.workspace = true +insta.workspace = true diff --git a/crates/compiler/test_derive/src/decoding.rs b/crates/compiler/test_derive/src/decoding.rs new file mode 100644 index 0000000000..edba290303 --- /dev/null +++ b/crates/compiler/test_derive/src/decoding.rs @@ -0,0 +1,233 @@ +#![cfg(test)] +// Even with #[allow(non_snake_case)] on individual idents, rust-analyzer issues diagnostics. +// See https://github.com/rust-lang/rust-analyzer/issues/6541. +// For the `v!` macro we use uppercase variables when constructing tag unions. +#![allow(non_snake_case)] + +use crate::{ + test_key_eq, test_key_neq, + util::{check_derivable, check_immediate, check_underivable, derive_test}, + v, +}; +use insta::assert_snapshot; +use roc_module::symbol::Symbol; +use roc_types::subs::Variable; + +use roc_derive_key::{decoding::FlatDecodableKey, DeriveBuiltin::Decoder, DeriveError, DeriveKey}; + +test_key_eq! { + Decoder, + + same_record: + v!({ a: v!(U8), }), v!({ a: v!(U8), }) + same_record_fields_diff_types: + v!({ a: v!(U8), }), v!({ a: v!(STR), }) + same_record_fields_any_order: + v!({ a: v!(U8), b: v!(U8), c: v!(U8), }), + v!({ c: v!(U8), a: v!(U8), b: v!(U8), }) + explicit_empty_record_and_implicit_empty_record: + v!(EMPTY_RECORD), v!({}) + + same_tuple: + v!((v!(U8), v!(U16),)), v!((v!(U8), v!(U16),)) + same_tuple_fields_diff_types: + v!((v!(U8), v!(U16),)), v!((v!(U32), v!(U64),)) + + list_list_diff_types: + v!(Symbol::LIST_LIST v!(STR)), v!(Symbol::LIST_LIST v!(U8)) + str_str: + v!(Symbol::STR_STR), v!(Symbol::STR_STR) +} + +test_key_neq! { + Decoder, + + different_record_fields: + v!({ a: v!(U8), }), v!({ b: v!(U8), }) + record_empty_vs_nonempty: + v!(EMPTY_RECORD), v!({ a: v!(U8), }) + + different_tuple_arities: + v!((v!(U8), v!(U16),)), v!((v!(U8), v!(U16), v!(U32),)) +} + +#[test] +fn immediates() { + check_immediate(Decoder, v!(U8), Symbol::DECODE_U8); + check_immediate(Decoder, v!(U16), Symbol::DECODE_U16); + check_immediate(Decoder, v!(U32), Symbol::DECODE_U32); + check_immediate(Decoder, v!(U64), Symbol::DECODE_U64); + check_immediate(Decoder, v!(U128), Symbol::DECODE_U128); + check_immediate(Decoder, v!(I8), Symbol::DECODE_I8); + check_immediate(Decoder, v!(I16), Symbol::DECODE_I16); + check_immediate(Decoder, v!(I32), Symbol::DECODE_I32); + check_immediate(Decoder, v!(I64), Symbol::DECODE_I64); + check_immediate(Decoder, v!(I128), Symbol::DECODE_I128); + check_immediate(Decoder, v!(DEC), Symbol::DECODE_DEC); + check_immediate(Decoder, v!(F32), Symbol::DECODE_F32); + check_immediate(Decoder, v!(F64), Symbol::DECODE_F64); + check_immediate(Decoder, v!(STR), Symbol::DECODE_STRING); +} + +#[test] +fn optional_record_field_derive_error() { + check_underivable(Decoder, v!({ ?a: v!(U8), }), DeriveError::Underivable); +} + +#[test] +fn derivable_record_ext_flex_var() { + check_derivable( + Decoder, + v!({ a: v!(STR), }* ), + DeriveKey::Decoder(FlatDecodableKey::Record(vec!["a".into()])), + ); +} + +#[test] +fn derivable_record_ext_flex_able_var() { + check_derivable( + Decoder, + v!({ a: v!(STR), }a implements Symbol::DECODE_DECODER ), + DeriveKey::Decoder(FlatDecodableKey::Record(vec!["a".into()])), + ); +} + +#[test] +fn derivable_record_with_record_ext() { + check_derivable( + Decoder, + v!({ b: v!(STR), }{ a: v!(STR), } ), + DeriveKey::Decoder(FlatDecodableKey::Record(vec!["a".into(), "b".into()])), + ); +} + +#[test] +fn list() { + derive_test(Decoder, v!(Symbol::LIST_LIST v!(STR)), |golden| { + assert_snapshot!(golden, @r###" + # derived for List Str + # Decoder (List val) fmt where fmt implements DecoderFormatting, val implements Decoding + # List U8, fmt -[[custom(3)]]-> { rest : List U8, result : [Err [TooShort], Ok (List val)] } where fmt implements DecoderFormatting, val implements Decoding + # Specialization lambda sets: + # @<1>: [[custom(3)]] + #Derived.decoder_list = + custom + \#Derived.bytes, #Derived.fmt -> + decodeWith #Derived.bytes (list decoder) #Derived.fmt + "### + ) + }) +} + +#[test] +fn record_2_fields() { + derive_test(Decoder, v!({first: v!(STR), second: v!(STR),}), |golden| { + assert_snapshot!(golden, @r###" + # derived for { first : Str, second : Str } + # Decoder { first : val, second : val1 } fmt where fmt implements DecoderFormatting, val implements Decoding, val1 implements Decoding + # List U8, fmt -[[custom(22)]]-> { rest : List U8, result : [Err [TooShort], Ok { first : val, second : val1 }] } where fmt implements DecoderFormatting, val implements Decoding, val1 implements Decoding + # Specialization lambda sets: + # @<1>: [[custom(22)]] + #Derived.decoder_{first,second} = + custom + \#Derived.bytes3, #Derived.fmt3 -> + decodeWith + #Derived.bytes3 + (record + { second: Err NoField, first: Err NoField } + \#Derived.stateRecord2, #Derived.field -> + when #Derived.field is + "first" -> + Keep (custom + \#Derived.bytes, #Derived.fmt -> + when decodeWith #Derived.bytes decoder #Derived.fmt is + #Derived.rec -> + { + result: when #Derived.rec.result is + Ok #Derived.val -> + Ok { stateRecord2 & first: Ok #Derived.val } + Err #Derived.err -> Err #Derived.err, + rest: #Derived.rec.rest + }) + "second" -> + Keep (custom + \#Derived.bytes2, #Derived.fmt2 -> + when decodeWith #Derived.bytes2 decoder #Derived.fmt2 is + #Derived.rec2 -> + { + result: when #Derived.rec2.result is + Ok #Derived.val2 -> + Ok { stateRecord2 & second: Ok #Derived.val2 } + Err #Derived.err2 -> Err #Derived.err2, + rest: #Derived.rec2.rest + }) + _ -> Skip + \#Derived.stateRecord -> + when #Derived.stateRecord.first is + Ok #Derived.first -> + when #Derived.stateRecord.second is + Ok #Derived.second -> + Ok { second: #Derived.second, first: #Derived.first } + _ -> Err TooShort + _ -> Err TooShort) + #Derived.fmt3 + "### + ) + }) +} + +#[test] +fn tuple_2_fields() { + derive_test(Decoder, v!((v!(STR), v!(U8),)), |golden| { + assert_snapshot!(golden, @r###" + # derived for ( Str, U8 )* + # Decoder ( val, val1 )* fmt where fmt implements DecoderFormatting, val implements Decoding, val1 implements Decoding + # List U8, fmt -[[custom(22)]]-> { rest : List U8, result : [Err [TooShort], Ok ( val, val1 )a] } where fmt implements DecoderFormatting, val implements Decoding, val1 implements Decoding + # Specialization lambda sets: + # @<1>: [[custom(22)]] + #Derived.decoder_(arity:2) = + custom + \#Derived.bytes3, #Derived.fmt3 -> + decodeWith + #Derived.bytes3 + (tuple + { e1: Err NoElem, e0: Err NoElem } + \#Derived.stateRecord2, #Derived.index -> + when #Derived.index is + 0 -> + Next (custom + \#Derived.bytes, #Derived.fmt -> + when decodeWith #Derived.bytes decoder #Derived.fmt is + #Derived.rec -> + { + result: when #Derived.rec.result is + Ok #Derived.val -> + Ok { stateRecord2 & e0: Ok #Derived.val } + Err #Derived.err -> Err #Derived.err, + rest: #Derived.rec.rest + }) + 1 -> + Next (custom + \#Derived.bytes2, #Derived.fmt2 -> + when decodeWith #Derived.bytes2 decoder #Derived.fmt2 is + #Derived.rec2 -> + { + result: when #Derived.rec2.result is + Ok #Derived.val2 -> + Ok { stateRecord2 & e1: Ok #Derived.val2 } + Err #Derived.err2 -> Err #Derived.err2, + rest: #Derived.rec2.rest + }) + _ -> TooLong + \#Derived.stateRecord -> + when #Derived.stateRecord.e0 is + Ok #Derived.0 -> + when #Derived.stateRecord.e1 is + Ok #Derived.1 -> Ok ( #Derived.0, #Derived.1 ) + _ -> Err TooShort + _ -> Err TooShort) + #Derived.fmt3 + "### + ) + }) +} diff --git a/crates/compiler/test_derive/src/encoding.rs b/crates/compiler/test_derive/src/encoding.rs new file mode 100644 index 0000000000..8274062059 --- /dev/null +++ b/crates/compiler/test_derive/src/encoding.rs @@ -0,0 +1,450 @@ +#![cfg(test)] +// Even with #[allow(non_snake_case)] on individual idents, rust-analyzer issues diagnostics. +// See https://github.com/rust-lang/rust-analyzer/issues/6541. +// For the `v!` macro we use uppercase variables when constructing tag unions. +#![allow(non_snake_case)] + +use insta::assert_snapshot; + +use crate::{ + test_key_eq, test_key_neq, + util::{check_derivable, check_immediate, derive_test}, + v, +}; +use roc_derive_key::{encoding::FlatEncodableKey, DeriveBuiltin::ToEncoder, DeriveKey}; +use roc_module::symbol::Symbol; +use roc_types::subs::Variable; + +// {{{ hash tests + +test_key_eq! { + ToEncoder, + + same_record: + v!({ a: v!(U8), }), v!({ a: v!(U8), }) + same_record_fields_diff_types: + v!({ a: v!(U8), }), v!({ a: v!(STR), }) + same_record_fields_any_order: + v!({ a: v!(U8), b: v!(U8), c: v!(U8), }), + v!({ c: v!(U8), a: v!(U8), b: v!(U8), }) + explicit_empty_record_and_implicit_empty_record: + v!(EMPTY_RECORD), v!({}) + same_record_fields_required_vs_optional: + v!({ a: v!(U8), b: v!(U8), }), + v!({ ?a: v!(U8), ?b: v!(U8), }) + + same_tuple: + v!((v!(U8), v!(U16),)), v!((v!(U8), v!(U16),)) + same_tuple_fields_diff_types: + v!((v!(U8), v!(U16),)), v!((v!(U32), v!(U64),)) + + same_tag_union: + v!([ A v!(U8) v!(STR), B v!(STR) ]), v!([ A v!(U8) v!(STR), B v!(STR) ]) + same_tag_union_tags_diff_types: + v!([ A v!(U8) v!(U8), B v!(U8) ]), v!([ A v!(STR) v!(STR), B v!(STR) ]) + same_tag_union_tags_any_order: + v!([ A v!(U8) v!(U8), B v!(U8), C ]), v!([ C, B v!(STR), A v!(STR) v!(STR) ]) + explicit_empty_tag_union_and_implicit_empty_tag_union: + v!(EMPTY_TAG_UNION), v!([]) + + same_recursive_tag_union: + v!([ Nil, Cons v!(^lst)] as lst), v!([ Nil, Cons v!(^lst)] as lst) + same_tag_union_and_recursive_tag_union_fields: + v!([ Nil, Cons v!(STR)]), v!([ Nil, Cons v!(^lst)] as lst) + + list_list_diff_types: + v!(Symbol::LIST_LIST v!(STR)), v!(Symbol::LIST_LIST v!(U8)) + set_set_diff_types: + v!(Symbol::SET_SET v!(STR)), v!(Symbol::SET_SET v!(U8)) + dict_dict_diff_types: + v!(Symbol::DICT_DICT v!(STR) v!(STR)), v!(Symbol::DICT_DICT v!(U8) v!(U8)) + str_str: + v!(Symbol::STR_STR), v!(Symbol::STR_STR) + + alias_eq_real_type: + v!(Symbol::ATTR_ATTR => v!([ True, False ])), v!([False, True]) + diff_alias_same_real_type: + v!(Symbol::ATTR_ATTR => v!([ True, False ])), v!(Symbol::UNDERSCORE => v!([False, True])) + + opaque_eq_real_type: + v!(@Symbol::ATTR_ATTR => v!([ True, False ])), v!([False, True]) + diff_opaque_same_real_type: + v!(@Symbol::ATTR_ATTR => v!([ True, False ])), v!(@Symbol::UNDERSCORE => v!([False, True])) + + opaque_real_type_eq_alias_real_type: + v!(@Symbol::ATTR_ATTR => v!([ True, False ])), v!(Symbol::UNDERSCORE => v!([False, True])) +} + +test_key_neq! { + ToEncoder, + + different_record_fields: + v!({ a: v!(U8), }), v!({ b: v!(U8), }) + record_empty_vs_nonempty: + v!(EMPTY_RECORD), v!({ a: v!(U8), }) + + different_tuple_arities: + v!((v!(U8), v!(U16),)), v!((v!(U8), v!(U16), v!(U32),)) + + different_tag_union_tags: + v!([ A v!(U8) ]), v!([ B v!(U8) ]) + tag_union_empty_vs_nonempty: + v!(EMPTY_TAG_UNION), v!([ B v!(U8) ]) + different_recursive_tag_union_tags: + v!([ Nil, Cons v!(^lst) ] as lst), v!([ Nil, Next v!(^lst) ] as lst) + + same_alias_diff_real_type: + v!(Symbol::ATTR_ATTR => v!([ True, False ])), v!(Symbol::ATTR_ATTR => v!([ False, True, Maybe ])) + diff_alias_diff_real_type: + v!(Symbol::ATTR_ATTR => v!([ True, False ])), v!(Symbol::UNDERSCORE => v!([ False, True, Maybe ])) + + same_opaque_diff_real_type: + v!(@Symbol::ATTR_ATTR => v!([ True, False ])), v!(@Symbol::ATTR_ATTR => v!([ False, True, Maybe ])) + diff_opaque_diff_real_type: + v!(@Symbol::ATTR_ATTR => v!([ True, False ])), v!(@Symbol::UNDERSCORE => v!([ False, True, Maybe ])) +} + +// }}} hash tests + +// {{{ deriver tests + +#[test] +fn immediates() { + check_immediate(ToEncoder, v!(U8), Symbol::ENCODE_U8); + check_immediate(ToEncoder, v!(U16), Symbol::ENCODE_U16); + check_immediate(ToEncoder, v!(U32), Symbol::ENCODE_U32); + check_immediate(ToEncoder, v!(U64), Symbol::ENCODE_U64); + check_immediate(ToEncoder, v!(U128), Symbol::ENCODE_U128); + check_immediate(ToEncoder, v!(I8), Symbol::ENCODE_I8); + check_immediate(ToEncoder, v!(I16), Symbol::ENCODE_I16); + check_immediate(ToEncoder, v!(I32), Symbol::ENCODE_I32); + check_immediate(ToEncoder, v!(I64), Symbol::ENCODE_I64); + check_immediate(ToEncoder, v!(I128), Symbol::ENCODE_I128); + check_immediate(ToEncoder, v!(DEC), Symbol::ENCODE_DEC); + check_immediate(ToEncoder, v!(F32), Symbol::ENCODE_F32); + check_immediate(ToEncoder, v!(F64), Symbol::ENCODE_F64); + check_immediate(ToEncoder, v!(STR), Symbol::ENCODE_STRING); +} + +#[test] +fn derivable_record_ext_flex_var() { + check_derivable( + ToEncoder, + v!({ a: v!(STR), }* ), + DeriveKey::ToEncoder(FlatEncodableKey::Record(vec!["a".into()])), + ); +} + +#[test] +fn derivable_record_ext_flex_able_var() { + check_derivable( + ToEncoder, + v!({ a: v!(STR), }a implements Symbol::ENCODE_TO_ENCODER), + DeriveKey::ToEncoder(FlatEncodableKey::Record(vec!["a".into()])), + ); +} + +#[test] +fn derivable_record_with_record_ext() { + check_derivable( + ToEncoder, + v!({ b: v!(STR), }{ a: v!(STR), } ), + DeriveKey::ToEncoder(FlatEncodableKey::Record(vec!["a".into(), "b".into()])), + ); +} + +#[test] +fn derivable_tag_ext_flex_var() { + check_derivable( + ToEncoder, + v!([ A v!(STR) ]* ), + DeriveKey::ToEncoder(FlatEncodableKey::TagUnion(vec![("A".into(), 1)])), + ); +} + +#[test] +fn derivable_tag_ext_flex_able_var() { + check_derivable( + ToEncoder, + v!([ A v!(STR) ]a implements Symbol::ENCODE_TO_ENCODER), + DeriveKey::ToEncoder(FlatEncodableKey::TagUnion(vec![("A".into(), 1)])), + ); +} + +#[test] +fn derivable_tag_with_tag_ext() { + check_derivable( + ToEncoder, + v!([ B v!(STR) v!(U8) ][ A v!(STR) ]), + DeriveKey::ToEncoder(FlatEncodableKey::TagUnion(vec![ + ("A".into(), 1), + ("B".into(), 2), + ])), + ); +} + +#[test] +fn empty_record() { + derive_test(ToEncoder, v!(EMPTY_RECORD), |golden| { + assert_snapshot!(golden, @r###" + # derived for {} + # {} -[[toEncoder_{}(0)]]-> Encoder fmt where fmt implements EncoderFormatting + # {} -[[toEncoder_{}(0)]]-> (List U8, fmt -[[custom(2) {}]]-> List U8) where fmt implements EncoderFormatting + # Specialization lambda sets: + # @<1>: [[toEncoder_{}(0)]] + # @<2>: [[custom(2) {}]] + #Derived.toEncoder_{} = + \#Derived.rcd -> + custom + \#Derived.bytes, #Derived.fmt -> + appendWith #Derived.bytes (record []) #Derived.fmt + "### + ) + }) +} + +#[test] +fn zero_field_record() { + derive_test(ToEncoder, v!({}), |golden| { + assert_snapshot!(golden, @r###" + # derived for {} + # {} -[[toEncoder_{}(0)]]-> Encoder fmt where fmt implements EncoderFormatting + # {} -[[toEncoder_{}(0)]]-> (List U8, fmt -[[custom(2) {}]]-> List U8) where fmt implements EncoderFormatting + # Specialization lambda sets: + # @<1>: [[toEncoder_{}(0)]] + # @<2>: [[custom(2) {}]] + #Derived.toEncoder_{} = + \#Derived.rcd -> + custom + \#Derived.bytes, #Derived.fmt -> + appendWith #Derived.bytes (record []) #Derived.fmt + "### + ) + }) +} + +#[test] +fn one_field_record() { + derive_test(ToEncoder, v!({ a: v!(U8), }), |golden| { + assert_snapshot!(golden, @r###" + # derived for { a : U8 } + # { a : val } -[[toEncoder_{a}(0)]]-> Encoder fmt where fmt implements EncoderFormatting, val implements Encoding + # { a : val } -[[toEncoder_{a}(0)]]-> (List U8, fmt -[[custom(2) { a : val }]]-> List U8) where fmt implements EncoderFormatting, val implements Encoding + # Specialization lambda sets: + # @<1>: [[toEncoder_{a}(0)]] + # @<2>: [[custom(2) { a : val }]] where val implements Encoding + #Derived.toEncoder_{a} = + \#Derived.rcd -> + custom + \#Derived.bytes, #Derived.fmt -> + appendWith + #Derived.bytes + (record [{ value: toEncoder #Derived.rcd.a, key: "a" }]) + #Derived.fmt + "### + ) + }) +} + +#[test] +fn two_field_record() { + derive_test(ToEncoder, v!({ a: v!(U8), b: v!(STR), }), |golden| { + assert_snapshot!(golden, @r###" + # derived for { a : U8, b : Str } + # { a : val, b : val1 } -[[toEncoder_{a,b}(0)]]-> Encoder fmt where fmt implements EncoderFormatting, val implements Encoding, val1 implements Encoding + # { a : val, b : val1 } -[[toEncoder_{a,b}(0)]]-> (List U8, fmt -[[custom(2) { a : val, b : val1 }]]-> List U8) where fmt implements EncoderFormatting, val implements Encoding, val1 implements Encoding + # Specialization lambda sets: + # @<1>: [[toEncoder_{a,b}(0)]] + # @<2>: [[custom(2) { a : val, b : val1 }]] where val implements Encoding, val1 implements Encoding + #Derived.toEncoder_{a,b} = + \#Derived.rcd -> + custom + \#Derived.bytes, #Derived.fmt -> + appendWith + #Derived.bytes + (record + [ + { value: toEncoder #Derived.rcd.a, key: "a" }, + { value: toEncoder #Derived.rcd.b, key: "b" }, + ]) + #Derived.fmt + "### + ) + }) +} + +#[test] +fn two_field_tuple() { + derive_test(ToEncoder, v!((v!(U8), v!(STR),)), |golden| { + assert_snapshot!(golden, @r###" + # derived for ( U8, Str )* + # ( val, val1 )* -[[toEncoder_(arity:2)(0)]]-> Encoder fmt where fmt implements EncoderFormatting, val implements Encoding, val1 implements Encoding + # ( val, val1 )a -[[toEncoder_(arity:2)(0)]]-> (List U8, fmt -[[custom(2) ( val, val1 )a]]-> List U8) where fmt implements EncoderFormatting, val implements Encoding, val1 implements Encoding + # Specialization lambda sets: + # @<1>: [[toEncoder_(arity:2)(0)]] + # @<2>: [[custom(2) ( val, val1 )*]] where val implements Encoding, val1 implements Encoding + #Derived.toEncoder_(arity:2) = + \#Derived.tup -> + custom + \#Derived.bytes, #Derived.fmt -> + appendWith + #Derived.bytes + (tuple [toEncoder #Derived.tup.0, toEncoder #Derived.tup.1]) + #Derived.fmt + "### + ) + }) +} + +#[test] +#[ignore = "NOTE: this would never actually happen, because [] is uninhabited, and hence toEncoder can never be called with a value of []! +Rightfully it induces broken assertions in other parts of the compiler, so we ignore it."] +fn empty_tag_union() { + derive_test(ToEncoder, v!(EMPTY_TAG_UNION), |golden| { + assert_snapshot!( + golden, + @r#" + "# + ) + }) +} + +#[test] +fn tag_one_label_zero_args() { + derive_test(ToEncoder, v!([A]), |golden| { + assert_snapshot!(golden, @r###" + # derived for [A] + # [A] -[[toEncoder_[A 0](0)]]-> Encoder fmt where fmt implements EncoderFormatting + # [A] -[[toEncoder_[A 0](0)]]-> (List U8, fmt -[[custom(2) [A]]]-> List U8) where fmt implements EncoderFormatting + # Specialization lambda sets: + # @<1>: [[toEncoder_[A 0](0)]] + # @<2>: [[custom(2) [A]]] + #Derived.toEncoder_[A 0] = + \#Derived.tag -> + custom + \#Derived.bytes, #Derived.fmt -> + appendWith + #Derived.bytes + (when #Derived.tag is + A -> tag "A" []) + #Derived.fmt + "### + ) + }) +} + +#[test] +fn tag_one_label_two_args() { + derive_test(ToEncoder, v!([A v!(U8) v!(STR)]), |golden| { + assert_snapshot!(golden, @r###" + # derived for [A U8 Str] + # [A val val1] -[[toEncoder_[A 2](0)]]-> Encoder fmt where fmt implements EncoderFormatting, val implements Encoding, val1 implements Encoding + # [A val val1] -[[toEncoder_[A 2](0)]]-> (List U8, fmt -[[custom(4) [A val val1]]]-> List U8) where fmt implements EncoderFormatting, val implements Encoding, val1 implements Encoding + # Specialization lambda sets: + # @<1>: [[toEncoder_[A 2](0)]] + # @<2>: [[custom(4) [A val val1]]] where val implements Encoding, val1 implements Encoding + #Derived.toEncoder_[A 2] = + \#Derived.tag -> + custom + \#Derived.bytes, #Derived.fmt -> + appendWith + #Derived.bytes + (when #Derived.tag is + A #Derived.2 #Derived.3 -> + tag "A" [toEncoder #Derived.2, toEncoder #Derived.3]) + #Derived.fmt + "### + ) + }) +} + +#[test] +fn tag_two_labels() { + derive_test( + ToEncoder, + v!([A v!(U8) v!(STR) v!(U16), B v!(STR)]), + |golden| { + assert_snapshot!(golden, @r###" + # derived for [A U8 Str U16, B Str] + # [A val val1 val1, B val1] -[[toEncoder_[A 3,B 1](0)]]-> Encoder fmt where fmt implements EncoderFormatting, val implements Encoding, val1 implements Encoding + # [A val val1 val1, B val1] -[[toEncoder_[A 3,B 1](0)]]-> (List U8, fmt -[[custom(6) [A val val1 val1, B val1]]]-> List U8) where fmt implements EncoderFormatting, val implements Encoding, val1 implements Encoding + # Specialization lambda sets: + # @<1>: [[toEncoder_[A 3,B 1](0)]] + # @<2>: [[custom(6) [A val val1 val1, B val1]]] where val implements Encoding, val1 implements Encoding + #Derived.toEncoder_[A 3,B 1] = + \#Derived.tag -> + custom + \#Derived.bytes, #Derived.fmt -> + appendWith + #Derived.bytes + (when #Derived.tag is + A #Derived.2 #Derived.3 #Derived.4 -> + tag + "A" + [ + toEncoder #Derived.2, + toEncoder #Derived.3, + toEncoder #Derived.4, + ] + B #Derived.5 -> tag "B" [toEncoder #Derived.5]) + #Derived.fmt + "### + ) + }, + ) +} + +#[test] +fn recursive_tag_union() { + derive_test( + ToEncoder, + v!([Nil, Cons v!(U8) v!(^lst) ] as lst), + |golden| { + assert_snapshot!(golden, @r###" + # derived for [Cons U8 $rec, Nil] as $rec + # [Cons val val1, Nil] -[[toEncoder_[Cons 2,Nil 0](0)]]-> Encoder fmt where fmt implements EncoderFormatting, val implements Encoding, val1 implements Encoding + # [Cons val val1, Nil] -[[toEncoder_[Cons 2,Nil 0](0)]]-> (List U8, fmt -[[custom(4) [Cons val val1, Nil]]]-> List U8) where fmt implements EncoderFormatting, val implements Encoding, val1 implements Encoding + # Specialization lambda sets: + # @<1>: [[toEncoder_[Cons 2,Nil 0](0)]] + # @<2>: [[custom(4) [Cons val val1, Nil]]] where val implements Encoding, val1 implements Encoding + #Derived.toEncoder_[Cons 2,Nil 0] = + \#Derived.tag -> + custom + \#Derived.bytes, #Derived.fmt -> + appendWith + #Derived.bytes + (when #Derived.tag is + Cons #Derived.2 #Derived.3 -> + tag "Cons" [toEncoder #Derived.2, toEncoder #Derived.3] + Nil -> tag "Nil" []) + #Derived.fmt + "### + ) + }, + ) +} + +#[test] +fn list() { + derive_test(ToEncoder, v!(Symbol::LIST_LIST v!(STR)), |golden| { + assert_snapshot!(golden, @r###" + # derived for List Str + # List val -[[toEncoder_list(0)]]-> Encoder fmt where fmt implements EncoderFormatting, val implements Encoding + # List val -[[toEncoder_list(0)]]-> (List U8, fmt -[[custom(4) (List val)]]-> List U8) where fmt implements EncoderFormatting, val implements Encoding + # Specialization lambda sets: + # @<1>: [[toEncoder_list(0)]] + # @<2>: [[custom(4) (List val)]] where val implements Encoding + #Derived.toEncoder_list = + \#Derived.lst -> + custom + \#Derived.bytes, #Derived.fmt -> + appendWith + #Derived.bytes + (list #Derived.lst \#Derived.elem -> toEncoder #Derived.elem) + #Derived.fmt + "### + ) + }) +} + +// }}} deriver tests diff --git a/crates/compiler/test_derive/src/eq.rs b/crates/compiler/test_derive/src/eq.rs new file mode 100644 index 0000000000..32ba64a1c0 --- /dev/null +++ b/crates/compiler/test_derive/src/eq.rs @@ -0,0 +1,58 @@ +#![cfg(test)] +// Even with #[allow(non_snake_case)] on individual idents, rust-analyzer issues diagnostics. +// See https://github.com/rust-lang/rust-analyzer/issues/6541. +// For the `v!` macro we use uppercase variables when constructing tag unions. +#![allow(non_snake_case)] + +use crate::{util::check_single_lset_immediate, v}; +use roc_module::symbol::Symbol; +use roc_types::subs::Variable; + +use roc_derive_key::DeriveBuiltin::IsEq; + +#[test] +fn immediates() { + // Everything is an immediate for `Eq`. + check_single_lset_immediate(IsEq, v!(U8), Symbol::BOOL_STRUCTURAL_EQ); + check_single_lset_immediate(IsEq, v!(U16), Symbol::BOOL_STRUCTURAL_EQ); + check_single_lset_immediate(IsEq, v!(U32), Symbol::BOOL_STRUCTURAL_EQ); + check_single_lset_immediate(IsEq, v!(U64), Symbol::BOOL_STRUCTURAL_EQ); + check_single_lset_immediate(IsEq, v!(U128), Symbol::BOOL_STRUCTURAL_EQ); + check_single_lset_immediate(IsEq, v!(I8), Symbol::BOOL_STRUCTURAL_EQ); + check_single_lset_immediate(IsEq, v!(I16), Symbol::BOOL_STRUCTURAL_EQ); + check_single_lset_immediate(IsEq, v!(I32), Symbol::BOOL_STRUCTURAL_EQ); + check_single_lset_immediate(IsEq, v!(I64), Symbol::BOOL_STRUCTURAL_EQ); + check_single_lset_immediate(IsEq, v!(I128), Symbol::BOOL_STRUCTURAL_EQ); + check_single_lset_immediate(IsEq, v!(STR), Symbol::BOOL_STRUCTURAL_EQ); + check_single_lset_immediate( + IsEq, + v!(Symbol::LIST_LIST v!(U8)), + Symbol::BOOL_STRUCTURAL_EQ, + ); + check_single_lset_immediate( + IsEq, + v!(Symbol::LIST_LIST v!(STR)), + Symbol::BOOL_STRUCTURAL_EQ, + ); + check_single_lset_immediate(IsEq, v!({ a: v!(U8), }), Symbol::BOOL_STRUCTURAL_EQ); + check_single_lset_immediate(IsEq, v!(EMPTY_RECORD), Symbol::BOOL_STRUCTURAL_EQ); + check_single_lset_immediate( + IsEq, + v!([ A v!(U8) v!(STR), B v!(STR) ]), + Symbol::BOOL_STRUCTURAL_EQ, + ); + check_single_lset_immediate( + IsEq, + v!([ A v!(U8) v!(STR), B v!(STR) ]), + Symbol::BOOL_STRUCTURAL_EQ, + ); + check_single_lset_immediate( + IsEq, + v!([ Nil, Cons v!(^lst)] as lst), + Symbol::BOOL_STRUCTURAL_EQ, + ); + + // NOTE: despite this reaching an immediate, `F64`s will never actually be allowed to be + // compared, because obligation checking will rule them out from `isEq`! + check_single_lset_immediate(IsEq, v!(F64), Symbol::BOOL_STRUCTURAL_EQ); +} diff --git a/crates/compiler/test_derive/src/hash.rs b/crates/compiler/test_derive/src/hash.rs new file mode 100644 index 0000000000..96a119199e --- /dev/null +++ b/crates/compiler/test_derive/src/hash.rs @@ -0,0 +1,320 @@ +#![cfg(test)] +// Even with #[allow(non_snake_case)] on individual idents, rust-analyzer issues diagnostics. +// See https://github.com/rust-lang/rust-analyzer/issues/6541. +// For the `v!` macro we use uppercase variables when constructing tag unions. +#![allow(non_snake_case)] + +use crate::{ + test_key_eq, test_key_neq, + util::{check_derivable, check_single_lset_immediate, check_underivable, derive_test}, + v, +}; +use insta::assert_snapshot; +use roc_module::symbol::Symbol; +use roc_types::subs::Variable; + +use roc_derive_key::{hash::FlatHashKey, DeriveBuiltin::Hash, DeriveError, DeriveKey}; + +test_key_eq! { + Hash, + + same_record: + v!({ a: v!(U8), }), v!({ a: v!(U8), }) + same_record_fields_diff_types: + v!({ a: v!(U8), }), v!({ a: v!(STR), }) + same_record_fields_any_order: + v!({ a: v!(U8), b: v!(U8), c: v!(U8), }), + v!({ c: v!(U8), a: v!(U8), b: v!(U8), }) + explicit_empty_record_and_implicit_empty_record: + v!(EMPTY_RECORD), v!({}) + + same_tuple: + v!((v!(U8), v!(U16),)), v!((v!(U8), v!(U16),)) + same_tuple_fields_diff_types: + v!((v!(U8), v!(U16),)), v!((v!(U32), v!(U64),)) + + same_tag_union: + v!([ A v!(U8) v!(STR), B v!(STR) ]), v!([ A v!(U8) v!(STR), B v!(STR) ]) + same_tag_union_tags_diff_types: + v!([ A v!(U8) v!(U8), B v!(U8) ]), v!([ A v!(STR) v!(STR), B v!(STR) ]) + same_tag_union_tags_any_order: + v!([ A v!(U8) v!(U8), B v!(U8), C ]), v!([ C, B v!(STR), A v!(STR) v!(STR) ]) + explicit_empty_tag_union_and_implicit_empty_tag_union: + v!(EMPTY_TAG_UNION), v!([]) + + same_recursive_tag_union: + v!([ Nil, Cons v!(^lst)] as lst), v!([ Nil, Cons v!(^lst)] as lst) + same_tag_union_and_recursive_tag_union_fields: + v!([ Nil, Cons v!(STR)]), v!([ Nil, Cons v!(^lst)] as lst) +} + +test_key_neq! { + Hash, + + different_record_fields: + v!({ a: v!(U8), }), v!({ b: v!(U8), }) + record_empty_vs_nonempty: + v!(EMPTY_RECORD), v!({ a: v!(U8), }) + + different_tuple_arities: + v!((v!(U8), v!(U16),)), v!((v!(U8), v!(U16), v!(U32),)) + + different_tag_union_tags: + v!([ A v!(U8) ]), v!([ B v!(U8) ]) + tag_union_empty_vs_nonempty: + v!(EMPTY_TAG_UNION), v!([ B v!(U8) ]) + different_recursive_tag_union_tags: + v!([ Nil, Cons v!(^lst) ] as lst), v!([ Nil, Next v!(^lst) ] as lst) +} + +#[test] +fn immediates() { + check_single_lset_immediate(Hash, v!(U8), Symbol::HASH_ADD_U8); + check_single_lset_immediate(Hash, v!(U16), Symbol::HASH_ADD_U16); + check_single_lset_immediate(Hash, v!(U32), Symbol::HASH_ADD_U32); + check_single_lset_immediate(Hash, v!(U64), Symbol::HASH_ADD_U64); + check_single_lset_immediate(Hash, v!(U128), Symbol::HASH_ADD_U128); + check_single_lset_immediate(Hash, v!(I8), Symbol::HASH_HASH_I8); + check_single_lset_immediate(Hash, v!(I16), Symbol::HASH_HASH_I16); + check_single_lset_immediate(Hash, v!(I32), Symbol::HASH_HASH_I32); + check_single_lset_immediate(Hash, v!(I64), Symbol::HASH_HASH_I64); + check_single_lset_immediate(Hash, v!(I128), Symbol::HASH_HASH_I128); + check_single_lset_immediate(Hash, v!(STR), Symbol::HASH_HASH_STR_BYTES); + check_single_lset_immediate(Hash, v!(Symbol::LIST_LIST v!(U8)), Symbol::HASH_HASH_LIST); + check_single_lset_immediate(Hash, v!(Symbol::LIST_LIST v!(STR)), Symbol::HASH_HASH_LIST); +} + +#[test] +fn optional_record_field_derive_error() { + check_underivable(Hash, v!({ ?a: v!(U8), }), DeriveError::Underivable); +} + +#[test] +fn derivable_record_ext_flex_var() { + check_derivable( + Hash, + v!({ a: v!(STR), }* ), + DeriveKey::Hash(FlatHashKey::Record(vec!["a".into()])), + ); +} + +#[test] +fn derivable_record_ext_flex_able_var() { + check_derivable( + Hash, + v!({ a: v!(STR), }a implements Symbol::DECODE_DECODER ), + DeriveKey::Hash(FlatHashKey::Record(vec!["a".into()])), + ); +} + +#[test] +fn derivable_record_with_record_ext() { + check_derivable( + Hash, + v!({ b: v!(STR), }{ a: v!(STR), } ), + DeriveKey::Hash(FlatHashKey::Record(vec!["a".into(), "b".into()])), + ); +} + +#[test] +fn derivable_tag_ext_flex_var() { + check_derivable( + Hash, + v!([ A v!(STR) ]* ), + DeriveKey::Hash(FlatHashKey::TagUnion(vec![("A".into(), 1)])), + ); +} + +#[test] +fn derivable_tag_ext_flex_able_var() { + check_derivable( + Hash, + v!([ A v!(STR) ]a implements Symbol::ENCODE_TO_ENCODER), + DeriveKey::Hash(FlatHashKey::TagUnion(vec![("A".into(), 1)])), + ); +} + +#[test] +fn derivable_tag_with_tag_ext() { + check_derivable( + Hash, + v!([ B v!(STR) v!(U8) ][ A v!(STR) ]), + DeriveKey::Hash(FlatHashKey::TagUnion(vec![ + ("A".into(), 1), + ("B".into(), 2), + ])), + ); +} + +#[test] +fn empty_record() { + derive_test(Hash, v!(EMPTY_RECORD), |golden| { + assert_snapshot!(golden, @r###" + # derived for {} + # hasher, {} -[[hash_{}(0)]]-> hasher where hasher implements Hasher + # hasher, {} -[[hash_{}(0)]]-> hasher where hasher implements Hasher + # Specialization lambda sets: + # @<1>: [[hash_{}(0)]] + #Derived.hash_{} = \#Derived.hasher, #Derived.rcd -> #Derived.hasher + "### + ) + }) +} + +#[test] +fn zero_field_record() { + derive_test(Hash, v!({}), |golden| { + assert_snapshot!(golden, @r###" + # derived for {} + # hasher, {} -[[hash_{}(0)]]-> hasher where hasher implements Hasher + # hasher, {} -[[hash_{}(0)]]-> hasher where hasher implements Hasher + # Specialization lambda sets: + # @<1>: [[hash_{}(0)]] + #Derived.hash_{} = \#Derived.hasher, #Derived.rcd -> #Derived.hasher + "### + ) + }) +} + +#[test] +fn one_field_record() { + derive_test(Hash, v!({ a: v!(U8), }), |golden| { + assert_snapshot!(golden, @r###" + # derived for { a : U8 } + # hasher, { a : a } -[[hash_{a}(0)]]-> hasher where a implements Hash, hasher implements Hasher + # hasher, { a : a } -[[hash_{a}(0)]]-> hasher where a implements Hash, hasher implements Hasher + # Specialization lambda sets: + # @<1>: [[hash_{a}(0)]] + #Derived.hash_{a} = + \#Derived.hasher, #Derived.rcd -> hash #Derived.hasher #Derived.rcd.a + "### + ) + }) +} + +#[test] +fn two_field_record() { + derive_test(Hash, v!({ a: v!(U8), b: v!(STR), }), |golden| { + assert_snapshot!(golden, @r###" + # derived for { a : U8, b : Str } + # hasher, { a : a, b : a1 } -[[hash_{a,b}(0)]]-> hasher where a implements Hash, a1 implements Hash, hasher implements Hasher + # hasher, { a : a, b : a1 } -[[hash_{a,b}(0)]]-> hasher where a implements Hash, a1 implements Hash, hasher implements Hasher + # Specialization lambda sets: + # @<1>: [[hash_{a,b}(0)]] + #Derived.hash_{a,b} = + \#Derived.hasher, #Derived.rcd -> + hash (hash #Derived.hasher #Derived.rcd.a) #Derived.rcd.b + "### + ) + }) +} + +#[test] +fn two_element_tuple() { + derive_test(Hash, v!((v!(U8), v!(STR),)), |golden| { + assert_snapshot!(golden, @r###" + # derived for ( U8, Str )* + # hasher, ( a, a1 )* -[[hash_(arity:2)(0)]]-> hasher where a implements Hash, a1 implements Hash, hasher implements Hasher + # hasher, ( a, a1 )* -[[hash_(arity:2)(0)]]-> hasher where a implements Hash, a1 implements Hash, hasher implements Hasher + # Specialization lambda sets: + # @<1>: [[hash_(arity:2)(0)]] + #Derived.hash_(arity:2) = + \#Derived.hasher, #Derived.tup -> + hash (hash #Derived.hasher #Derived.tup.0) #Derived.tup.1 + "### + ) + }) +} + +#[test] +fn tag_one_label_no_payloads() { + derive_test(Hash, v!([A]), |golden| { + assert_snapshot!(golden, @r###" + # derived for [A] + # hasher, [A] -[[hash_[A 0](0)]]-> hasher where hasher implements Hasher + # hasher, [A] -[[hash_[A 0](0)]]-> hasher where hasher implements Hasher + # Specialization lambda sets: + # @<1>: [[hash_[A 0](0)]] + #Derived.hash_[A 0] = \#Derived.hasher, A -> #Derived.hasher + "### + ) + }) +} + +#[test] +fn tag_one_label_newtype() { + derive_test(Hash, v!([A v!(U8) v!(STR)]), |golden| { + assert_snapshot!(golden, @r###" + # derived for [A U8 Str] + # hasher, [A a a1] -[[hash_[A 2](0)]]-> hasher where a implements Hash, a1 implements Hash, hasher implements Hasher + # hasher, [A a a1] -[[hash_[A 2](0)]]-> hasher where a implements Hash, a1 implements Hash, hasher implements Hasher + # Specialization lambda sets: + # @<1>: [[hash_[A 2](0)]] + #Derived.hash_[A 2] = + \#Derived.hasher, A #Derived.2 #Derived.3 -> + hash (hash #Derived.hasher #Derived.2) #Derived.3 + "### + ) + }) +} + +#[test] +fn tag_two_labels() { + derive_test(Hash, v!([A v!(U8) v!(STR) v!(U16), B v!(STR)]), |golden| { + assert_snapshot!(golden, @r###" + # derived for [A U8 Str U16, B Str] + # a, [A a1 a2 a3, B a3] -[[hash_[A 3,B 1](0)]]-> a where a implements Hasher, a1 implements Hash, a2 implements Hash, a3 implements Hash + # a, [A a1 a2 a3, B a3] -[[hash_[A 3,B 1](0)]]-> a where a implements Hasher, a1 implements Hash, a2 implements Hash, a3 implements Hash + # Specialization lambda sets: + # @<1>: [[hash_[A 3,B 1](0)]] + #Derived.hash_[A 3,B 1] = + \#Derived.hasher, #Derived.union -> + when #Derived.union is + A #Derived.3 #Derived.4 #Derived.5 -> + hash + (hash (hash (addU8 #Derived.hasher 0) #Derived.3) #Derived.4) + #Derived.5 + B #Derived.6 -> hash (addU8 #Derived.hasher 1) #Derived.6 + "### + ) + }) +} + +#[test] +fn tag_two_labels_no_payloads() { + derive_test(Hash, v!([A, B]), |golden| { + assert_snapshot!(golden, @r###" + # derived for [A, B] + # a, [A, B] -[[hash_[A 0,B 0](0)]]-> a where a implements Hasher + # a, [A, B] -[[hash_[A 0,B 0](0)]]-> a where a implements Hasher + # Specialization lambda sets: + # @<1>: [[hash_[A 0,B 0](0)]] + #Derived.hash_[A 0,B 0] = + \#Derived.hasher, #Derived.union -> + when #Derived.union is + A -> addU8 #Derived.hasher 0 + B -> addU8 #Derived.hasher 1 + "### + ) + }) +} + +#[test] +fn recursive_tag_union() { + derive_test(Hash, v!([Nil, Cons v!(U8) v!(^lst) ] as lst), |golden| { + assert_snapshot!(golden, @r###" + # derived for [Cons U8 $rec, Nil] as $rec + # a, [Cons a1 a2, Nil] -[[hash_[Cons 2,Nil 0](0)]]-> a where a implements Hasher, a1 implements Hash, a2 implements Hash + # a, [Cons a1 a2, Nil] -[[hash_[Cons 2,Nil 0](0)]]-> a where a implements Hasher, a1 implements Hash, a2 implements Hash + # Specialization lambda sets: + # @<1>: [[hash_[Cons 2,Nil 0](0)]] + #Derived.hash_[Cons 2,Nil 0] = + \#Derived.hasher, #Derived.union -> + when #Derived.union is + Cons #Derived.3 #Derived.4 -> + hash (hash (addU8 #Derived.hasher 0) #Derived.3) #Derived.4 + Nil -> addU8 #Derived.hasher 1 + "### + ) + }) +} diff --git a/crates/compiler/test_derive/src/tests.rs b/crates/compiler/test_derive/src/tests.rs new file mode 100644 index 0000000000..f41484e842 --- /dev/null +++ b/crates/compiler/test_derive/src/tests.rs @@ -0,0 +1,8 @@ +#![cfg(test)] + +mod decoding; +mod encoding; +mod eq; +mod hash; + +mod util; diff --git a/crates/compiler/test_derive/src/util.rs b/crates/compiler/test_derive/src/util.rs new file mode 100644 index 0000000000..0f5cacd2e7 --- /dev/null +++ b/crates/compiler/test_derive/src/util.rs @@ -0,0 +1,591 @@ +use std::fmt::Write as _; // import without risk of name clashing +use std::path::PathBuf; + +use bumpalo::Bump; +use roc_packaging::cache::RocCacheDir; +use roc_solve::{ + module::{SolveConfig, SolveOutput}, + FunctionKind, +}; +use ven_pretty::DocAllocator; + +use roc_can::{ + abilities::{AbilitiesStore, SpecializationLambdaSets}, + constraint::Constraints, + debug::{pretty_print_def, PPCtx}, + def::Def, + expr::Declarations, + module::{ + ExposedByModule, ExposedForModule, ExposedModuleTypes, ResolvedImplementations, + RigidVariables, + }, +}; +use roc_collections::VecSet; +use roc_constrain::expr::constrain_decls; +use roc_debug_flags::dbg_do; +use roc_derive::DerivedModule; +use roc_derive_key::{DeriveBuiltin, DeriveError, DeriveKey, Derived}; +use roc_load_internal::file::{add_imports, Threading}; +use roc_load_internal::module::LoadedModule; +use roc_module::symbol::{IdentIds, Interns, ModuleId, Symbol}; +use roc_region::all::LineInfo; +use roc_reporting::report::{type_problem, RocDocAllocator}; +use roc_types::{ + pretty_print::{name_and_print_var, DebugPrint}, + subs::{ExposedTypesStorageSubs, Subs, Variable}, + types::Types, +}; + +const DERIVED_MODULE: ModuleId = ModuleId::DERIVED_SYNTH; + +fn module_source_and_path(builtin: DeriveBuiltin) -> (ModuleId, &'static str, PathBuf) { + use roc_builtins::roc::module_source; + + let repo_root = std::env::var("ROC_WORKSPACE_DIR").expect("are you running with `cargo test`?"); + let builtins_path = PathBuf::from(repo_root) + .join("compiler") + .join("builtins") + .join("roc"); + + match builtin { + DeriveBuiltin::ToEncoder => ( + ModuleId::ENCODE, + module_source(ModuleId::ENCODE), + builtins_path.join("Encode.roc"), + ), + DeriveBuiltin::Decoder => ( + ModuleId::DECODE, + module_source(ModuleId::DECODE), + builtins_path.join("Decode.roc"), + ), + DeriveBuiltin::Hash => ( + ModuleId::HASH, + module_source(ModuleId::HASH), + builtins_path.join("Hash.roc"), + ), + DeriveBuiltin::IsEq => ( + ModuleId::BOOL, + module_source(ModuleId::BOOL), + builtins_path.join("Bool.roc"), + ), + DeriveBuiltin::ToInspector => ( + ModuleId::INSPECT, + module_source(ModuleId::INSPECT), + builtins_path.join("Inspect.roc"), + ), + } +} + +/// DSL for creating [`Content`][roc_types::subs::Content]. +#[macro_export] +macro_rules! v { + ({ $($field:ident: $make_v:expr,)* $(?$opt_field:ident : $make_opt_v:expr,)* }$( $($ext:tt)+ )?) => {{ + #[allow(unused)] + use roc_types::types::RecordField; + use roc_types::subs::{Subs, RecordFields, Content, FlatType, Variable}; + |subs: &mut Subs| { + $(let $field = $make_v(subs);)* + $(let $opt_field = $make_opt_v(subs);)* + let fields = vec![ + $( (stringify!($field).into(), RecordField::Required($field)) ,)* + $( (stringify!($opt_field).into(), RecordField::Optional($opt_field)) ,)* + ]; + let fields = RecordFields::insert_into_subs(subs, fields); + + #[allow(unused_mut, unused)] + let mut ext = Variable::EMPTY_RECORD; + $( ext = $crate::v!($($ext)+)(subs); )? + + roc_derive::synth_var(subs, Content::Structure(FlatType::Record(fields, ext))) + } + }}; + (( $($make_v:expr,)* )$( $($ext:tt)+ )?) => {{ + #[allow(unused)] + use roc_types::subs::{Subs, RecordFields, Content, FlatType, Variable, TupleElems}; + |subs: &mut Subs| { + let elems = [ + $($make_v(subs),)* + ].into_iter().enumerate(); + let elems = TupleElems::insert_into_subs(subs, elems); + + #[allow(unused_mut, unused)] + let mut ext = Variable::EMPTY_TUPLE; + $( ext = $crate::v!($($ext)+)(subs); )? + + roc_derive::synth_var(subs, Content::Structure(FlatType::Tuple(elems, ext))) + } + }}; + ([ $($tag:ident $($payload:expr)*),* ] as $rec_var:ident) => {{ + use roc_types::subs::{Subs, SubsIndex, Variable, Content, FlatType, TagExt, UnionTags}; + use roc_module::ident::TagName; + |subs: &mut Subs| { + let $rec_var = subs.fresh_unnamed_flex_var(); + let rec_name_index = + SubsIndex::push_new(&mut subs.field_names, stringify!($rec).into()); + + $( + let $tag = vec![ $( $payload(subs), )* ]; + )* + let tags = UnionTags::insert_into_subs::<_, Vec>(subs, vec![ $( (TagName(stringify!($tag).into()), $tag) ,)* ]); + let tag_union_var = roc_derive::synth_var(subs, Content::Structure(FlatType::RecursiveTagUnion($rec_var, tags, TagExt::Any(Variable::EMPTY_TAG_UNION)))); + + subs.set_content( + $rec_var, + Content::RecursionVar { + structure: tag_union_var, + opt_name: Some(rec_name_index), + }, + ); + tag_union_var + } + }}; + ([ $($tag:ident $($payload:expr)*),* ]$( $($ext:tt)+ )?) => {{ + #[allow(unused)] + use roc_types::subs::{Subs, UnionTags, Content, FlatType, TagExt, Variable}; + #[allow(unused)] + use roc_module::ident::TagName; + |subs: &mut Subs| { + $( + let $tag = vec![ $( $payload(subs), )* ]; + )* + let tags = UnionTags::insert_into_subs::<_, Vec>(subs, vec![ $( (TagName(stringify!($tag).into()), $tag) ,)* ]); + + #[allow(unused_mut, unused)] + let mut ext = Variable::EMPTY_TAG_UNION; + $( ext = $crate::v!($($ext)+)(subs); )? + + roc_derive::synth_var(subs, Content::Structure(FlatType::TagUnion(tags, TagExt::Any(ext)))) + } + }}; + (Symbol::$sym:ident $($arg:expr)*) => {{ + use roc_types::subs::{Subs, SubsSlice, Content, FlatType}; + use roc_module::symbol::Symbol; + |subs: &mut Subs| { + let $sym = vec![ $( $arg(subs) ,)* ]; + let var_slice = SubsSlice::insert_into_subs(subs, $sym); + roc_derive::synth_var(subs, Content::Structure(FlatType::Apply(Symbol::$sym, var_slice))) + } + }}; + (Symbol::$alias:ident $($arg:expr)* => $real_var:expr) => {{ + use roc_types::subs::{Subs, AliasVariables, Content}; + use roc_types::types::AliasKind; + use roc_module::symbol::Symbol; + |subs: &mut Subs| { + let args = vec![$( $arg(subs) )*]; + let alias_variables = AliasVariables::insert_into_subs::, Vec<_>, _>(subs, args, vec![], vec![]); + let real_var = $real_var(subs); + roc_derive::synth_var(subs, Content::Alias(Symbol::$alias, alias_variables, real_var, AliasKind::Structural)) + } + }}; + (@Symbol::$alias:ident $($arg:expr)* => $real_var:expr) => {{ + use roc_types::subs::{Subs, AliasVariables, Content}; + use roc_types::types::AliasKind; + use roc_module::symbol::Symbol; + |subs: &mut Subs| { + let args = vec![$( $arg(subs) )*]; + let alias_variables = AliasVariables::insert_into_subs::, Vec<_>, _>(subs, args, vec![], vec![]); + let real_var = $real_var(subs); + roc_derive::synth_var(subs, Content::Alias(Symbol::$alias, alias_variables, real_var, AliasKind::Opaque)) + } + }}; + (*) => {{ + use roc_types::subs::{Subs, Content}; + |subs: &mut Subs| { roc_derive::synth_var(subs, Content::FlexVar(None)) } + }}; + ($name:ident implements $ability:path) => {{ + use roc_types::subs::{Subs, SubsIndex, SubsSlice, Content}; + |subs: &mut Subs| { + let name_index = + SubsIndex::push_new(&mut subs.field_names, stringify!($name).into()); + + let abilities_slice = SubsSlice::extend_new(&mut subs.symbol_names, [$ability]); + + roc_derive::synth_var(subs, Content::FlexAbleVar(Some(name_index), abilities_slice)) + } + }}; + (^$rec_var:ident) => {{ + use roc_types::subs::{Subs}; + |_: &mut Subs| { $rec_var } + }}; + ($var:ident) => {{ + use roc_types::subs::{Subs}; + |_: &mut Subs| { Variable::$var } + }}; + } + +pub(crate) fn check_key(builtin: DeriveBuiltin, eq: bool, synth1: S1, synth2: S2) +where + S1: FnOnce(&mut Subs) -> Variable, + S2: FnOnce(&mut Subs) -> Variable, +{ + let mut subs = Subs::new(); + let var1 = synth1(&mut subs); + let var2 = synth2(&mut subs); + + let key1 = Derived::builtin(builtin, &subs, var1); + let key2 = Derived::builtin(builtin, &subs, var2); + + if eq { + assert_eq!(key1, key2); + } else { + assert_ne!(key1, key2); + } +} + +#[macro_export] +macro_rules! test_key_eq { + ($builtin:expr, $($name:ident: $synth1:expr, $synth2:expr)*) => {$( + #[test] + fn $name() { + $crate::util::check_key($builtin,true, $synth1, $synth2) + } + )*}; +} + +#[macro_export] +macro_rules! test_key_neq { + ($builtin:expr, $($name:ident: $synth1:expr, $synth2:expr)*) => {$( + #[test] + fn $name() { + $crate::util::check_key($builtin, false, $synth1, $synth2) + } + )*}; +} + +pub(crate) fn check_derivable(builtin: DeriveBuiltin, synth: Sy, key: DeriveKey) +where + Sy: FnOnce(&mut Subs) -> Variable, +{ + let mut subs = Subs::new(); + let var = synth(&mut subs); + + let derived = Derived::builtin(builtin, &subs, var); + + assert_eq!(derived, Ok(Derived::Key(key))); +} + +pub(crate) fn check_underivable(builtin: DeriveBuiltin, synth: Sy, err: DeriveError) +where + Sy: FnOnce(&mut Subs) -> Variable, +{ + let mut subs = Subs::new(); + let var = synth(&mut subs); + + let key = Derived::builtin(builtin, &subs, var); + + assert_eq!(key, Err(err)); +} + +pub(crate) fn check_immediate(builtin: DeriveBuiltin, synth: S, immediate: Symbol) +where + S: FnOnce(&mut Subs) -> Variable, +{ + let mut subs = Subs::new(); + let var = synth(&mut subs); + + let key = Derived::builtin(builtin, &subs, var); + + assert_eq!(key, Ok(Derived::Immediate(immediate))); +} + +pub(crate) fn check_single_lset_immediate(builtin: DeriveBuiltin, synth: S, immediate: Symbol) +where + S: FnOnce(&mut Subs) -> Variable, +{ + let mut subs = Subs::new(); + let var = synth(&mut subs); + + let key = Derived::builtin(builtin, &subs, var); + + assert_eq!(key, Ok(Derived::SingleLambdaSetImmediate(immediate))); +} + +#[allow(clippy::too_many_arguments)] +fn assemble_derived_golden( + subs: &mut Subs, + test_module: ModuleId, + interns: &Interns, + source_var: Variable, + derived_source: &str, + typ: Variable, + specialization_lsets: SpecializationLambdaSets, +) -> String { + let mut print_var = |var: Variable, print_only_under_alias| { + let snapshot = subs.snapshot(); + let pretty_type = name_and_print_var( + var, + subs, + test_module, + interns, + DebugPrint { + print_lambda_sets: true, + print_only_under_alias, + ..DebugPrint::NOTHING + }, + ); + subs.rollback_to(snapshot); + pretty_type + }; + + let mut pretty_buf = String::new(); + + // ignore returned result, writeln can not fail as it is used here + let _ = writeln!(pretty_buf, "# derived for {}", print_var(source_var, false)); + + let pretty_type = print_var(typ, false); + let _ = writeln!(pretty_buf, "# {}", &pretty_type); + + let pretty_type_under_aliases = print_var(typ, true); + let _ = writeln!(pretty_buf, "# {}", &pretty_type_under_aliases); + + pretty_buf.push_str("# Specialization lambda sets:\n"); + let mut specialization_lsets = specialization_lsets.into_iter().collect::>(); + specialization_lsets.sort_by_key(|(region, _)| *region); + for (region, var) in specialization_lsets { + let pretty_lset = print_var(var, false); + let _ = writeln!(pretty_buf, "# @<{region}>: {pretty_lset}"); + } + + pretty_buf.push_str(derived_source); + + pretty_buf +} + +/// The environment of the module containing the builtin ability we're deriving for a type. +struct DeriveBuiltinEnv { + module_id: ModuleId, + exposed_types: ExposedTypesStorageSubs, + abilities_store: AbilitiesStore, +} + +#[allow(clippy::too_many_arguments)] +fn check_derived_typechecks_and_golden( + derived_def: Def, + test_module: ModuleId, + mut test_subs: Subs, + interns: &Interns, + derive_builtin_env: DeriveBuiltinEnv, + source_var: Variable, + derived_program: &str, + specialization_lsets: SpecializationLambdaSets, + check_golden: impl Fn(&str), +) { + // constrain the derived + let mut types = Types::new(); + let mut constraints = Constraints::new(); + let def_var = derived_def.expr_var; + let mut decls = Declarations::new(); + decls.push_def(derived_def); + let constr = constrain_decls(&mut types, &mut constraints, test_module, &decls); + + // the derived implementation on stuff from the builtin module, so + // - we need to add those dependencies as imported on the constraint + // - we need to add the builtin ability info to a local abilities store + let values_to_import_from_builtin_module = derive_builtin_env + .exposed_types + .stored_vars_by_symbol + .keys() + .copied() + .collect::>(); + let pending_abilities = derive_builtin_env + .abilities_store + .closure_from_imported(&values_to_import_from_builtin_module); + let mut exposed_by_module = ExposedByModule::default(); + exposed_by_module.insert( + derive_builtin_env.module_id, + ExposedModuleTypes { + exposed_types_storage_subs: derive_builtin_env.exposed_types, + resolved_implementations: ResolvedImplementations::default(), + }, + ); + let exposed_for_module = ExposedForModule::new( + values_to_import_from_builtin_module.iter(), + exposed_by_module, + ); + let mut def_types = Default::default(); + let mut rigid_vars = Default::default(); + let mut flex_vars = Default::default(); + let (import_variables, abilities_store) = add_imports( + test_module, + &mut constraints, + &mut test_subs, + pending_abilities, + &exposed_for_module, + &mut def_types, + &mut rigid_vars, + &mut flex_vars, + ); + let constr = constraints.let_import_constraint( + rigid_vars, + flex_vars, + def_types, + constr, + &import_variables, + ); + + // run the solver, print and fail if we have errors + dbg_do!( + roc_debug_flags::ROC_PRINT_UNIFICATIONS_DERIVED, + std::env::set_var(roc_debug_flags::ROC_PRINT_UNIFICATIONS, "1") + ); + + let solve_config = SolveConfig { + home: test_module, + constraints: &constraints, + root_constraint: constr, + types, + function_kind: FunctionKind::LambdaSet, + pending_derives: Default::default(), + exposed_by_module: &exposed_for_module.exposed_by_module, + derived_module: Default::default(), + + #[cfg(debug_assertions)] + checkmate: None, + }; + + let SolveOutput { + subs: mut solved_subs, + errors: problems, + .. + } = roc_solve::module::run_solve( + solve_config, + RigidVariables::default(), + test_subs, + Default::default(), + abilities_store, + ); + dbg_do!( + roc_debug_flags::ROC_PRINT_UNIFICATIONS_DERIVED, + std::env::set_var(roc_debug_flags::ROC_PRINT_UNIFICATIONS, "0") + ); + let subs = solved_subs.inner_mut(); + + if !problems.is_empty() { + let filename = PathBuf::from("Test.roc"); + let lines = LineInfo::new(" "); + let src_lines = vec![" "]; + let mut reports = Vec::new(); + let alloc = RocDocAllocator::new(&src_lines, test_module, interns); + + for problem in problems.into_iter() { + if let Some(report) = type_problem(&alloc, &lines, filename.clone(), problem.clone()) { + reports.push(report); + } + } + + let has_reports = !reports.is_empty(); + + let doc = alloc + .stack(reports.into_iter().map(|v| v.pretty(&alloc))) + .append(if has_reports { + alloc.line() + } else { + alloc.nil() + }); + + let mut buf = String::new(); + doc.1 + .render_raw(80, &mut roc_reporting::report::CiWrite::new(&mut buf)) + .unwrap(); + + panic!("Derived does not typecheck:\n{buf}\nDerived def:\n{derived_program}"); + } + + let golden = assemble_derived_golden( + subs, + test_module, + interns, + source_var, + derived_program, + def_var, + specialization_lsets, + ); + + check_golden(&golden) +} + +fn get_key(builtin: DeriveBuiltin, subs: &Subs, var: Variable) -> DeriveKey { + match Derived::builtin(builtin, subs, var) { + Ok(Derived::Key(key)) => key, + _ => unreachable!(), + } +} + +pub(crate) fn derive_test(builtin: DeriveBuiltin, synth_input: S, check_golden: impl Fn(&str)) +where + S: FnOnce(&mut Subs) -> Variable, +{ + let arena = Bump::new(); + let (builtin_module, source, path) = module_source_and_path(builtin); + let target_info = roc_target::TargetInfo::default_x86_64(); + + let LoadedModule { + mut interns, + exposed_types_storage, + abilities_store, + resolved_implementations, + .. + } = roc_load_internal::file::load_and_typecheck_str( + &arena, + path.file_name().unwrap().into(), + source, + path.parent().unwrap().to_path_buf(), + Default::default(), + target_info, + FunctionKind::LambdaSet, + roc_reporting::report::RenderTarget::ColorTerminal, + roc_reporting::report::DEFAULT_PALETTE, + RocCacheDir::Disallowed, + Threading::AllAvailable, + ) + .unwrap(); + + let mut subs = Subs::new(); + let ident_ids = IdentIds::default(); + let source_var = synth_input(&mut subs); + let key = get_key(builtin, &subs, source_var); + + let mut derived_module = unsafe { DerivedModule::from_components(subs, ident_ids) }; + + let mut exposed_by_module = ExposedByModule::default(); + exposed_by_module.insert( + builtin_module, + ExposedModuleTypes { + exposed_types_storage_subs: exposed_types_storage.clone(), + resolved_implementations, + }, + ); + + let (_derived_symbol, derived_def, specialization_lsets) = + derived_module.get_or_insert(&exposed_by_module, key); + let specialization_lsets = specialization_lsets.clone(); + let derived_def = derived_def.clone(); + + let (subs, ident_ids) = derived_module.decompose(); + + interns.all_ident_ids.insert(DERIVED_MODULE, ident_ids); + DERIVED_MODULE.register_debug_idents(interns.all_ident_ids.get(&DERIVED_MODULE).unwrap()); + + let pp_ctx = PPCtx { + interns: &interns, + print_lambda_names: false, + home: builtin_module, + }; + let derived_program = pretty_print_def(&pp_ctx, &derived_def); + + check_derived_typechecks_and_golden( + derived_def, + DERIVED_MODULE, + subs, + &interns, + DeriveBuiltinEnv { + module_id: builtin_module, + exposed_types: exposed_types_storage, + abilities_store, + }, + source_var, + &derived_program, + specialization_lsets, + check_golden, + ); +} diff --git a/crates/compiler/test_gen/.gitignore b/crates/compiler/test_gen/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/crates/compiler/test_gen/.gitignore @@ -0,0 +1 @@ +/build diff --git a/crates/compiler/test_gen/Cargo.toml b/crates/compiler/test_gen/Cargo.toml new file mode 100644 index 0000000000..cc323c814b --- /dev/null +++ b/crates/compiler/test_gen/Cargo.toml @@ -0,0 +1,78 @@ +[package] +name = "test_gen" +description = "Contains all of Roc's code generation tests." + +authors.workspace = true +edition.workspace = true +license.workspace = true +version.workspace = true + +[[test]] +name = "test_gen" +path = "src/tests.rs" + +[build-dependencies] +roc_bitcode = { path = "../builtins/bitcode" } +roc_command_utils = { path = "../../utils/command" } +wasi_libc_sys = { path = "../../wasi-libc-sys" } + +tempfile.workspace = true + +[dependencies] +roc_gen_llvm = { path = "../gen_llvm", optional = true } +inkwell = { workspace = true, optional = true } + +[dev-dependencies] +roc_gen_dev = { path = "../gen_dev" } +roc_gen_wasm = { path = "../gen_wasm" } +roc_bitcode = { path = "../builtins/bitcode" } +roc_build = { path = "../build", features = ["target-aarch64", "target-x86_64", "target-wasm32"] } +roc_builtins = { path = "../builtins" } +roc_can = { path = "../can" } +roc_collections = { path = "../collections" } +roc_command_utils = { path = "../../utils/command" } +roc_constrain = { path = "../constrain" } +roc_debug_flags = { path = "../debug_flags" } +roc_error_macros = { path = "../../error_macros" } +roc_load = { path = "../load" } +roc_module = { path = "../module" } +roc_mono = { path = "../mono" } +roc_packaging = { path = "../../packaging" } +roc_parse = { path = "../parse" } +roc_problem = { path = "../problem" } +roc_region = { path = "../region" } +roc_reporting = { path = "../../reporting" } +roc_solve = { path = "../solve" } +roc_std = { path = "../../roc_std" } +roc_target = { path = "../roc_target" } +roc_types = { path = "../types" } +roc_unify = { path = "../unify" } +roc_wasm_interp = { path = "../../wasm_interp" } +roc_wasm_module = { path = "../../wasm_module" } + +bumpalo.workspace = true +criterion.workspace = true +indoc.workspace = true +libc.workspace = true +libloading.workspace = true +target-lexicon.workspace = true +tempfile.workspace = true + + +[features] +default = ["gen-llvm"] +gen-dev = [] +gen-llvm = ["roc_gen_llvm", "inkwell"] +gen-llvm-wasm = ["gen-llvm"] +gen-wasm = [] + +[[bench]] +name = "list_map" +harness = false + +[[bench]] +name = "quicksort" +harness = false + +[package.metadata.cargo-udeps.ignore] +development = ["roc_wasm_interp"] diff --git a/crates/compiler/test_gen/README.md b/crates/compiler/test_gen/README.md new file mode 100644 index 0000000000..6a66db10b3 --- /dev/null +++ b/crates/compiler/test_gen/README.md @@ -0,0 +1,28 @@ +# Running our CodeGen tests + +Our code generation tests are all in this crate. Feature flags are used to run the tests with a specific backend. For convenience, some aliases are added in `.cargo/config`: + +```toml +[alias] +test-gen-llvm = "test -p test_gen" +test-gen-dev = "test -p test_gen --no-default-features --features gen-dev" +test-gen-wasm = "test -p test_gen --no-default-features --features gen-wasm" +``` + +So we can run: + +```sh +cargo test-gen-llvm +``` + +To run the gen tests with the LLVM backend. To filter tests, append a filter like so: + +```sh +> cargo test-gen-wasm wasm_str::small + Finished test [unoptimized + debuginfo] target(s) in 0.13s + Running src/tests.rs (target/debug/deps/test_gen-b4ad63a9dd50f050) + +running 2 tests +test wasm_str::small_str_literal ... ok +test wasm_str::small_str_zeroed_literal ... ok +``` diff --git a/crates/compiler/test_gen/benches/list_map.rs b/crates/compiler/test_gen/benches/list_map.rs new file mode 100644 index 0000000000..2aa8243bfd --- /dev/null +++ b/crates/compiler/test_gen/benches/list_map.rs @@ -0,0 +1,116 @@ +#[path = "../src/helpers/mod.rs"] +mod helpers; + +// defines roc_alloc and friends +pub use helpers::platform_functions::*; + +use bumpalo::Bump; +use criterion::{black_box, criterion_group, criterion_main, Criterion}; +use roc_gen_llvm::{llvm::build::LlvmBackendMode, run_roc::RocCallResult, run_roc_dylib}; +use roc_mono::ir::OptLevel; +use roc_std::RocList; + +// results July 6, 2022 +// +// roc sum map time: [612.73 ns 614.24 ns 615.98 ns] +// roc sum map_with_index time: [5.3177 us 5.3218 us 5.3255 us] +// rust (debug) time: [24.081 us 24.163 us 24.268 us] +// +// results April 9, 2023 +// +// roc sum map time: [510.77 ns 517.47 ns 524.47 ns] +// roc sum map_with_index time: [573.49 ns 578.17 ns 583.76 ns] + +type Input = RocList; +type Output = i64; + +type Main = unsafe extern "C" fn(I, *mut RocCallResult); + +const ROC_LIST_MAP: &str = indoc::indoc!( + r#" + app "bench" provides [main] to "./platform" + + main : List I64 -> Nat + main = \list -> + list + |> List.map (\x -> x + 2) + |> List.len + "# +); + +const ROC_LIST_MAP_WITH_INDEX: &str = indoc::indoc!( + r#" + app "bench" provides [main] to "./platform" + + main : List I64 -> Nat + main = \list -> + list + |> List.mapWithIndex (\x, _ -> x + 2) + |> List.len + "# +); + +fn roc_function<'a, 'b>( + arena: &'a Bump, + source: &str, +) -> libloading::Symbol<'a, Main<&'b Input, Output>> { + let config = helpers::llvm::HelperConfig { + mode: LlvmBackendMode::GenTest, + ignore_problems: false, + add_debug_info: true, + opt_level: OptLevel::Optimize, + }; + + let context = inkwell::context::Context::create(); + let (main_fn_name, errors, lib) = helpers::llvm::helper( + arena, + config, + source, + arena.alloc(context), + roc_load::FunctionKind::LambdaSet, + ); + + assert!(errors.is_empty(), "Encountered errors:\n{errors}"); + + run_roc_dylib!(arena.alloc(lib), main_fn_name, &Input, Output) +} + +fn create_input_list() -> RocList { + let numbers = Vec::from_iter(0..1_000); + + RocList::from_slice(&numbers) +} + +pub fn criterion_benchmark(c: &mut Criterion) { + let arena = Bump::new(); + + let list_map_main = roc_function(&arena, ROC_LIST_MAP); + let list_map_with_index_main = roc_function(&arena, ROC_LIST_MAP_WITH_INDEX); + + let input = &*arena.alloc(create_input_list()); + + c.bench_function("roc sum map", |b| { + b.iter(|| unsafe { + let mut main_result = RocCallResult::default(); + + // the roc code will dec this list, so inc it first so it is not free'd + std::mem::forget(input.clone()); + + list_map_main(black_box(input), &mut main_result); + }) + }); + + c.bench_function("roc sum map_with_index", |b| { + b.iter(|| unsafe { + let mut main_result = RocCallResult::default(); + + // the roc code will dec this list, so inc it first so it is not free'd + std::mem::forget(input.clone()); + + list_map_with_index_main(black_box(input), &mut main_result); + }) + }); +} + +criterion_group!(benches, criterion_benchmark); +criterion_main!(benches); diff --git a/crates/compiler/test_gen/benches/quicksort.rs b/crates/compiler/test_gen/benches/quicksort.rs new file mode 100644 index 0000000000..2b786ac8dd --- /dev/null +++ b/crates/compiler/test_gen/benches/quicksort.rs @@ -0,0 +1,163 @@ +#[path = "../src/helpers/mod.rs"] +mod helpers; + +// defines roc_alloc and friends +pub use helpers::platform_functions::*; + +use bumpalo::Bump; +use criterion::{black_box, criterion_group, criterion_main, Criterion}; +use roc_gen_llvm::{llvm::build::LlvmBackendMode, run_roc::RocCallResult, run_roc_dylib}; +use roc_mono::ir::OptLevel; +use roc_std::RocList; + +// results April 9, 2023 +// +// > pure roc quicksort time: [106.97 us 107.27 us 107.63 us] +// > roc zig quicksort time: [34.765 us 35.301 us 35.865 us] +// > rust std sort time: [20.413 us 20.623 us 20.838 us] + +type Input = RocList; +type Output = RocList; + +type Main = unsafe extern "C" fn(I, *mut RocCallResult); + +const ZIG_ROC_QUICKSORT: &str = indoc::indoc!( + r#" + app "bench" provides [main] to "./platform" + + main : List I64 -> List I64 + main = \originalList -> List.sortAsc originalList + "# +); + +const PURE_ROC_QUICKSORT: &str = indoc::indoc!( + r#" + app "bench" provides [main] to "./platform" + + main : List I64 -> List I64 + main = \originalList -> + n = List.len originalList + + quicksortHelp originalList 0 (n - 1) + + quicksortHelp : List (Num a), Nat, Nat -> List (Num a) + quicksortHelp = \list, low, high -> + if low < high then + when partition low high list is + Pair partitionIndex partitioned -> + partitioned + |> quicksortHelp low (partitionIndex - 1) + |> quicksortHelp (partitionIndex + 1) high + else + list + + partition : Nat, Nat, List (Num a) -> [Pair Nat (List (Num a))] + partition = \low, high, initialList -> + when List.get initialList high is + Ok pivot -> + when partitionHelp low low initialList high pivot is + Pair newI newList -> + Pair newI (List.swap newList newI high) + + Err _ -> + Pair low initialList + + partitionHelp : Nat, Nat, List (Num c), Nat, Num c -> [Pair Nat (List (Num c))] + partitionHelp = \i, j, list, high, pivot -> + if j < high then + when List.get list j is + Ok value -> + if value <= pivot then + partitionHelp (i + 1) (j + 1) (List.swap list i j) high pivot + else + partitionHelp i (j + 1) list high pivot + + Err _ -> + Pair i list + else + Pair i list + "# +); + +fn roc_function<'a>( + arena: &'a Bump, + source: &str, +) -> libloading::Symbol<'a, Main<*mut Input, Output>> { + let config = helpers::llvm::HelperConfig { + mode: LlvmBackendMode::GenTest, + ignore_problems: false, + add_debug_info: true, + opt_level: OptLevel::Optimize, + }; + + let context = inkwell::context::Context::create(); + let (main_fn_name, errors, lib) = helpers::llvm::helper( + arena, + config, + source, + arena.alloc(context), + roc_load::FunctionKind::LambdaSet, + ); + + assert!(errors.is_empty(), "Encountered errors:\n{errors}"); + + run_roc_dylib!(arena.alloc(lib), main_fn_name, *mut Input, Output) +} + +pub fn criterion_benchmark(c: &mut Criterion) { + let arena = Bump::new(); + + let pure_roc_quicksort_main = roc_function(&arena, PURE_ROC_QUICKSORT); + let roc_zig_quicksort_main = roc_function(&arena, ZIG_ROC_QUICKSORT); + + let input_numbers: Vec<_> = std::iter::repeat([1, 2, 3, 4, 5, 6, 7, 8]) + .flatten() + .take(1000) + .collect(); + + let input = arena.alloc(RocList::from_slice(&input_numbers)); + + c.bench_function("pure roc quicksort", |b| { + b.iter(|| unsafe { + let mut main_result = RocCallResult::default(); + + assert!(input.is_unique()); + + // reset_input + input.copy_from_slice(&input_numbers); + + pure_roc_quicksort_main(black_box(input), &mut main_result); + }) + }); + + c.bench_function("roc zig quicksort", |b| { + b.iter(|| unsafe { + let mut main_result = RocCallResult::default(); + + assert!(input.is_unique()); + + // reset_input + input.copy_from_slice(&input_numbers); + + roc_zig_quicksort_main(black_box(input), &mut main_result); + }) + }); + + c.bench_function("rust std sort", |b| { + b.iter(|| unsafe { + assert!(input.is_unique()); + + // reset_input + input.copy_from_slice(&input_numbers); + + // an attempt to block optimizing based on the input list + let ptr = black_box(input.as_mut_ptr()); + let input = std::slice::from_raw_parts_mut(ptr, 1000); + + input.sort() + }) + }); +} + +criterion_group!(quicksort_benches, criterion_benchmark); +criterion_main!(quicksort_benches); diff --git a/crates/compiler/test_gen/build.rs b/crates/compiler/test_gen/build.rs new file mode 100644 index 0000000000..40bb4d8e12 --- /dev/null +++ b/crates/compiler/test_gen/build.rs @@ -0,0 +1,169 @@ +use roc_command_utils::zig; +use std::env; +use std::fs; +use std::path::Path; +use std::path::PathBuf; + +use wasi_libc_sys::{WASI_COMPILER_RT_PATH, WASI_LIBC_PATH}; + +const PLATFORM_FILENAME: &str = "wasm_test_platform"; + +fn main() { + println!("cargo:rerun-if-changed=build.rs"); + if feature_is_enabled("gen-wasm") || feature_is_enabled("gen-llvm-wasm") { + build_wasm_test_host(); + build_wasm_linking_test_host(); + } +} + +const fn object_file_extension() -> &'static str { + if cfg!(windows) { + "obj" + } else { + "o" + } +} + +fn build_wasm_linking_test_host() { + let host_source_path = PathBuf::from("src") + .join("helpers") + .join("wasm_linking_test_host.zig"); + + let import_source_path = PathBuf::from("src") + .join("helpers") + .join("wasm_linking_host_imports.zig"); + + let host_wasm_path = PathBuf::from("build").join("wasm_linking_test_host.wasm"); + let host_native_path = PathBuf::from("build").join("wasm_linking_test_host"); + + let host_source: &str = host_source_path.to_str().unwrap(); + let import_source: &str = import_source_path.to_str().unwrap(); + let host_wasm: &str = host_wasm_path.to_str().unwrap(); + let host_native: &str = host_native_path.to_str().unwrap(); + + println!("cargo:rerun-if-changed={host_source}"); + println!("cargo:rerun-if-changed={import_source}"); + + if !Path::new("build").exists() { + fs::create_dir("build").unwrap(); + } + + if Path::new(host_wasm).exists() { + fs::remove_file(host_wasm).unwrap(); + } + + run_zig(&[ + "build-obj", + "-target", + "wasm32-freestanding-musl", + host_source, + &format!("-femit-bin={host_wasm}"), + ]); + + let mut import_obj_path = PathBuf::from("build").join("wasm_linking_host_imports"); + import_obj_path.set_extension(object_file_extension()); + let import_obj = import_obj_path.to_str().unwrap(); + run_zig(&[ + "build-obj", + import_source, + &format!("-femit-bin={}", &import_obj), + ]); + + run_zig(&[ + "build-exe", + host_source, + import_obj, + &format!("-femit-bin={host_native}"), + #[cfg(windows)] + "--subsystem", + #[cfg(windows)] + "console", + #[cfg(windows)] + "-lc", + ]); +} + +fn build_wasm_test_host() { + let mut source_path = PathBuf::new() + .join("src") + .join("helpers") + .join(PLATFORM_FILENAME); + source_path.set_extension("c"); + println!("cargo:rerun-if-changed={}", source_path.to_str().unwrap()); + + let out_dir = env::var("OUT_DIR").unwrap(); + + // Create an object file with relocations + let platform_path = build_wasm_platform(&out_dir, source_path.to_str().unwrap()); + + let mut outfile = PathBuf::from(&out_dir).join(PLATFORM_FILENAME); + outfile.set_extension("wasm"); + + let builtins_host_tempfile = roc_bitcode::host_wasm_tempfile() + .expect("failed to write host builtins object to tempfile"); + + run_zig(&[ + "wasm-ld", + builtins_host_tempfile.path().to_str().unwrap(), + platform_path.to_str().unwrap(), + WASI_COMPILER_RT_PATH, + WASI_LIBC_PATH, + "-o", + outfile.to_str().unwrap(), + "--no-entry", + "--relocatable", + ]); + + // Extend the lifetime of the tempfile so it doesn't get dropped + // (and thus deleted) before the Zig process is done using it! + let _ = builtins_host_tempfile; +} + +fn build_wasm_platform(out_dir: &str, source_path: &str) -> PathBuf { + let mut outfile = PathBuf::from(out_dir).join(PLATFORM_FILENAME); + outfile.set_extension("wasm"); + + run_zig(&[ + "build-lib", + "-target", + "wasm32-wasi-musl", + "-lc", + source_path, + &format!("-femit-bin={}", outfile.to_str().unwrap()), + ]); + + outfile +} + +fn feature_is_enabled(feature_name: &str) -> bool { + let cargo_env_var = format!( + "CARGO_FEATURE_{}", + feature_name.replace('-', "_").to_uppercase() + ); + env::var(cargo_env_var).is_ok() +} + +// Run cargo with -vv to see commands printed out +fn run_zig(args: &[&str]) { + let mut zig_cmd = zig(); + + let full_zig_cmd = zig_cmd.args(args); + println!("{full_zig_cmd:?}"); + + let zig_cmd_output = full_zig_cmd.output().unwrap(); + + if !zig_cmd_output.status.success() { + eprintln!( + "stdout:\n{}", + String::from_utf8_lossy(&zig_cmd_output.stdout) + ); + eprintln!( + "stderr:\n{}", + String::from_utf8_lossy(&zig_cmd_output.stderr) + ); + panic!("zig call failed with status {:?}", zig_cmd_output.status); + } + + assert!(zig_cmd_output.stdout.is_empty(), "{zig_cmd_output:#?}"); + assert!(zig_cmd_output.stderr.is_empty(), "{zig_cmd_output:#?}"); +} diff --git a/crates/compiler/test_gen/src/gen_abilities.rs b/crates/compiler/test_gen/src/gen_abilities.rs new file mode 100644 index 0000000000..7c96408390 --- /dev/null +++ b/crates/compiler/test_gen/src/gen_abilities.rs @@ -0,0 +1,2329 @@ +#[cfg(feature = "gen-llvm")] +use crate::helpers::llvm::assert_evals_to; + +#[cfg(feature = "gen-wasm")] +use crate::helpers::wasm::assert_evals_to; + +#[cfg(all(test, any(feature = "gen-llvm", feature = "gen-wasm")))] +use indoc::indoc; + +#[cfg(all(test, any(feature = "gen-llvm", feature = "gen-wasm")))] +use roc_std::RocList; +#[cfg(all(test, any(feature = "gen-llvm", feature = "gen-wasm")))] +use roc_std::RocStr; + +use crate::helpers::with_larger_debug_stack; + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn hash_specialization() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + MHash implements + hash : a -> U64 where a implements MHash + + Id := U64 implements [MHash {hash}] + + hash = \@Id n -> n + + main = hash (@Id 1234) + "# + ), + 1234, + u64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn hash_specialization_multiple_add() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + MHash implements + hash : a -> U64 where a implements MHash + + Id := U64 implements [ MHash {hash: hashId} ] + + hashId = \@Id n -> n + + One := {} implements [ MHash {hash: hashOne} ] + + hashOne = \@One _ -> 1 + + main = hash (@Id 1234) + hash (@One {}) + "# + ), + 1235, + u64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn alias_member_specialization() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + MHash implements + hash : a -> U64 where a implements MHash + + Id := U64 implements [MHash {hash}] + + hash = \@Id n -> n + + main = + aliasedMHash = hash + aliasedMHash (@Id 1234) + "# + ), + 1234, + u64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn ability_constrained_in_non_member_usage() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [result] to "./platform" + + MHash implements + hash : a -> U64 where a implements MHash + + mulMHashes : a, a -> U64 where a implements MHash + mulMHashes = \x, y -> hash x * hash y + + Id := U64 implements [MHash {hash}] + hash = \@Id n -> n + + result = mulMHashes (@Id 5) (@Id 7) + "# + ), + 35, + u64 + ) +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn ability_constrained_in_non_member_usage_inferred() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [result] to "./platform" + + MHash implements + hash : a -> U64 where a implements MHash + + mulMHashes = \x, y -> hash x * hash y + + Id := U64 implements [MHash {hash}] + hash = \@Id n -> n + + result = mulMHashes (@Id 5) (@Id 7) + "# + ), + 35, + u64 + ) +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn ability_constrained_in_non_member_multiple_specializations() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [result] to "./platform" + + MHash implements + hash : a -> U64 where a implements MHash + + mulMHashes : a, b -> U64 where a implements MHash, b implements MHash + mulMHashes = \x, y -> hash x * hash y + + Id := U64 implements [MHash { hash: hashId }] + hashId = \@Id n -> n + + Three := {} implements [MHash { hash: hashThree }] + hashThree = \@Three _ -> 3 + + result = mulMHashes (@Id 100) (@Three {}) + "# + ), + 300, + u64 + ) +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn ability_constrained_in_non_member_multiple_specializations_inferred() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [result] to "./platform" + + MHash implements + hash : a -> U64 where a implements MHash + + mulMHashes = \x, y -> hash x * hash y + + Id := U64 implements [MHash { hash: hashId }] + hashId = \@Id n -> n + + Three := {} implements [MHash { hash: hashThree }] + hashThree = \@Three _ -> 3 + + result = mulMHashes (@Id 100) (@Three {}) + "# + ), + 300, + u64 + ) +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn ability_used_as_type_still_compiles() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [result] to "./platform" + + MHash implements + hash : a -> U64 where a implements MHash + + mulMHashes : MHash, MHash -> U64 + mulMHashes = \x, y -> hash x * hash y + + Id := U64 implements [MHash { hash: hashId }] + hashId = \@Id n -> n + + Three := {} implements [MHash { hash: hashThree }] + hashThree = \@Three _ -> 3 + + result = mulMHashes (@Id 100) (@Three {}) + "# + ), + 300, + u64 + ) +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn bounds_to_multiple_abilities() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + Idempot implements idempot : a -> a where a implements Idempot + Consume implements consume : a -> Str where a implements Consume + + Hello := Str implements [Idempot { idempot: idempotHello }, Consume { consume: consumeHello }] + + idempotHello = \@Hello msg -> @Hello msg + consumeHello = \@Hello msg -> msg + + lifecycle : a -> Str where a implements Idempot & Consume + lifecycle = \x -> idempot x |> consume + + main = lifecycle (@Hello "hello world") + "# + ), + RocStr::from("hello world"), + RocStr + ) +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn encode() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [myU8Bytes] to "./platform" + + MEncoder fmt := List U8, fmt -> List U8 where fmt implements Format + + MEncoding implements + toEncoder : val -> MEncoder fmt where val implements MEncoding, fmt implements Format + + Format implements + u8 : U8 -> MEncoder fmt where fmt implements Format + + appendWith : List U8, MEncoder fmt, fmt -> List U8 where fmt implements Format + appendWith = \lst, (@MEncoder doFormat), fmt -> doFormat lst fmt + + toBytes : val, fmt -> List U8 where val implements MEncoding, fmt implements Format + toBytes = \val, fmt -> appendWith [] (toEncoder val) fmt + + + Linear := {} implements [Format {u8}] + + u8 = \n -> @MEncoder (\lst, @Linear {} -> List.append lst n) + + Rgba := { r : U8, g : U8, b : U8, a : U8 } implements [MEncoding {toEncoder}] + + toEncoder = \@Rgba {r, g, b, a} -> + @MEncoder \lst, fmt -> lst + |> appendWith (u8 r) fmt + |> appendWith (u8 g) fmt + |> appendWith (u8 b) fmt + |> appendWith (u8 a) fmt + + myU8Bytes = toBytes (@Rgba { r: 106, g: 90, b: 205, a: 255 }) (@Linear {}) + "# + ), + RocList::from_slice(&[106, 90, 205, 255]), + RocList + ) +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +#[ignore = "running into weird let-generalization issue when a variable is only in output position, see #3660"] +fn decode() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [myU8] to "./platform" + + MDecodeError : [TooShort, Leftover (List U8)] + + MDecoder val fmt := List U8, fmt -> { result: Result val MDecodeError, rest: List U8 } where fmt implements MDecoderFormatting + + MDecoding implements + decoder : MDecoder val fmt where val implements MDecoding, fmt implements MDecoderFormatting + + MDecoderFormatting implements + u8 : MDecoder U8 fmt where fmt implements MDecoderFormatting + + decodeWith : List U8, MDecoder val fmt, fmt -> { result: Result val MDecodeError, rest: List U8 } where fmt implements MDecoderFormatting + decodeWith = \lst, (@MDecoder doDecode), fmt -> doDecode lst fmt + + fromBytes : List U8, fmt -> Result val MDecodeError + where fmt implements MDecoderFormatting, val implements MDecoding + fromBytes = \lst, fmt -> + when decodeWith lst decoder fmt is + { result, rest } -> + Result.try result \val -> + if List.isEmpty rest + then Ok val + else Err (Leftover rest) + + + Linear := {} implements [MDecoderFormatting {u8}] + + u8 = @MDecoder \lst, @Linear {} -> + when List.first lst is + Ok n -> { result: Ok n, rest: List.dropFirst lst 1 } + Err _ -> { result: Err TooShort, rest: [] } + + MyU8 := U8 implements [MDecoding {decoder}] + + # impl MDecoding for MyU8 + decoder = @MDecoder \lst, fmt -> + { result, rest } = decodeWith lst u8 fmt + { result: Result.map result (\n -> @MyU8 n), rest } + + myU8 = + when fromBytes [15] (@Linear {}) is + Ok (@MyU8 n) -> n + _ -> 27u8 + "# + ), + 15, + u8 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn encode_use_stdlib() { + assert_evals_to!( + indoc!( + r#" + app "test" + imports [Encode, TotallyNotJson] + provides [main] to "./platform" + + HelloWorld := {} implements [Encoding {toEncoder}] + toEncoder = \@HelloWorld {} -> + Encode.custom \bytes, fmt -> + bytes + |> Encode.appendWith (Encode.string "Hello, World!\n") fmt + + main = + result = Str.fromUtf8 (Encode.toBytes (@HelloWorld {}) TotallyNotJson.json) + when result is + Ok s -> s + _ -> "" + "# + ), + RocStr::from("\"Hello, World!\\n\""), + RocStr + ) +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn encode_use_stdlib_without_wrapping_custom() { + assert_evals_to!( + indoc!( + r#" + app "test" + imports [Encode, TotallyNotJson] + provides [main] to "./platform" + + HelloWorld := {} implements [Encoding {toEncoder}] + toEncoder = \@HelloWorld {} -> Encode.string "Hello, World!\n" + + main = + result = Str.fromUtf8 (Encode.toBytes (@HelloWorld {}) TotallyNotJson.json) + when result is + Ok s -> s + _ -> "" + "# + ), + RocStr::from("\"Hello, World!\\n\""), + RocStr + ) +} + +#[test] +#[cfg(not(debug_assertions))] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn encode_derive_to_encoder_for_opaque() { + assert_evals_to!( + indoc!( + r#" + app "test" + imports [TotallyNotJson] + provides [main] to "./platform" + + HelloWorld := { a: Str } implements [Encoding] + + main = + result = Str.fromUtf8 (Encode.toBytes (@HelloWorld { a: "Hello, World!" }) TotallyNotJson.json) + when result is + Ok s -> s + _ -> "" + "# + ), + RocStr::from(r#"{"a":"Hello, World!"}"#), + RocStr + ) +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn to_encoder_encode_custom_has_capture() { + assert_evals_to!( + indoc!( + r#" + app "test" + imports [Encode, TotallyNotJson] + provides [main] to "./platform" + + HelloWorld := Str implements [Encoding {toEncoder}] + toEncoder = \@HelloWorld s1 -> + Encode.custom \bytes, fmt -> + bytes + |> Encode.appendWith (Encode.string s1) fmt + + main = + result = Str.fromUtf8 (Encode.toBytes (@HelloWorld "Hello, World!\n") TotallyNotJson.json) + when result is + Ok s -> s + _ -> "" + "# + ), + RocStr::from("\"Hello, World!\\n\""), + RocStr + ) +} + +mod encode_immediate { + #[cfg(feature = "gen-llvm")] + use crate::helpers::llvm::assert_evals_to; + + #[cfg(feature = "gen-wasm")] + use crate::helpers::wasm::assert_evals_to; + + #[cfg(all(test, any(feature = "gen-llvm", feature = "gen-wasm")))] + use indoc::indoc; + + #[cfg(all(test, any(feature = "gen-llvm", feature = "gen-wasm")))] + use roc_std::RocStr; + + #[test] + #[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] + fn string() { + assert_evals_to!( + indoc!( + r#" + app "test" imports [Encode, TotallyNotJson] provides [main] to "./platform" + + main = + when Str.fromUtf8 (Encode.toBytes "foo" TotallyNotJson.json) is + Ok s -> s + _ -> "" + "# + ), + RocStr::from("\"foo\""), + RocStr + ) + } + + #[test] + #[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] + fn ranged_number() { + assert_evals_to!( + indoc!( + r#" + app "test" imports [Encode, TotallyNotJson] provides [main] to "./platform" + + main = + when Str.fromUtf8 (Encode.toBytes [1, 2, 3] TotallyNotJson.json) is + Ok s -> s + _ -> "" + "# + ), + RocStr::from(r"[1,2,3]"), + RocStr + ) + } + + #[test] + #[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] + fn bool() { + assert_evals_to!( + indoc!( + r#" + app "test" imports [Encode, TotallyNotJson] provides [main] to "./platform" + + main = + when Str.fromUtf8 (Encode.toBytes Bool.false TotallyNotJson.json) is + Ok s -> s + _ -> "" + "# + ), + RocStr::from(r"false"), + RocStr + ) + } + + macro_rules! num_immediate { + ($($num:expr, $typ:ident)*) => {$( + #[test] + #[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] + fn $typ() { + assert_evals_to!( + &format!(indoc!( + r#" + app "test" imports [Encode, TotallyNotJson] provides [main] to "./platform" + + main = + when Str.fromUtf8 (Encode.toBytes {}{} TotallyNotJson.json) is + Ok s -> s + _ -> "" + "# + ), $num, stringify!($typ)), + RocStr::from(format!(r#"{}"#, $num).as_str()), + RocStr + ) + } + )*} + } + + num_immediate! { + 17, i8 + 17, i16 + 17, i32 + 17, i64 + 17, i128 + 17, u8 + 17, u16 + 17, u32 + 17, u64 + 17, u128 + 17.25, f32 + 17.23, f64 + 17.23, dec + } +} + +#[test] +#[cfg(not(debug_assertions))] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn encode_derived_record_one_field_string() { + assert_evals_to!( + indoc!( + r#" + app "test" + imports [Encode, TotallyNotJson] + provides [main] to "./platform" + + main = + result = Str.fromUtf8 (Encode.toBytes {a: "foo"} TotallyNotJson.json) + when result is + Ok s -> s + _ -> "" + "# + ), + RocStr::from(r#"{"a":"foo"}"#), + RocStr + ) +} + +#[test] +#[cfg(not(debug_assertions))] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn encode_derived_record_two_fields_strings() { + assert_evals_to!( + indoc!( + r#" + app "test" + imports [Encode, TotallyNotJson] + provides [main] to "./platform" + + main = + rcd = {a: "foo", b: "bar"} + result = Str.fromUtf8 (Encode.toBytes rcd TotallyNotJson.json) + when result is + Ok s -> s + _ -> "" + "# + ), + RocStr::from(r#"{"a":"foo","b":"bar"}"#), + RocStr + ) +} + +#[test] +#[cfg(not(debug_assertions))] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn encode_derived_nested_record_string() { + assert_evals_to!( + indoc!( + r#" + app "test" + imports [Encode, TotallyNotJson] + provides [main] to "./platform" + + main = + rcd = {a: {b: "bar"}} + encoded = Encode.toBytes rcd TotallyNotJson.json + result = Str.fromUtf8 encoded + when result is + Ok s -> s + _ -> "" + "# + ), + RocStr::from(r#"{"a":{"b":"bar"}}"#), + RocStr + ) +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn encode_derived_tag_one_payload_string() { + assert_evals_to!( + indoc!( + r#" + app "test" + imports [Encode, TotallyNotJson] + provides [main] to "./platform" + + main = + x = A "foo" + result = Str.fromUtf8 (Encode.toBytes x TotallyNotJson.json) + when result is + Ok s -> s + _ -> "" + "# + ), + RocStr::from(r#"{"A":["foo"]}"#), + RocStr + ) +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn encode_derived_tag_two_payloads_string() { + assert_evals_to!( + indoc!( + r#" + app "test" + imports [Encode, TotallyNotJson] + provides [main] to "./platform" + + main = + x = A "foo" "bar" + result = Str.fromUtf8 (Encode.toBytes x TotallyNotJson.json) + when result is + Ok s -> s + _ -> "" + "# + ), + RocStr::from(r#"{"A":["foo","bar"]}"#), + RocStr + ) +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn encode_derived_nested_tag_string() { + assert_evals_to!( + indoc!( + r#" + app "test" + imports [Encode, TotallyNotJson] + provides [main] to "./platform" + + main = + x = A (B "foo" "bar") + encoded = Encode.toBytes x TotallyNotJson.json + result = Str.fromUtf8 encoded + when result is + Ok s -> s + _ -> "" + "# + ), + RocStr::from(r#"{"A":[{"B":["foo","bar"]}]}"#), + RocStr + ) +} + +#[test] +#[cfg(not(debug_assertions))] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn encode_derived_nested_record_tag_record() { + assert_evals_to!( + indoc!( + r#" + app "test" + imports [Encode, TotallyNotJson] + provides [main] to "./platform" + + main = + x = {a: (B ({c: "foo"}))} + encoded = Encode.toBytes x TotallyNotJson.json + result = Str.fromUtf8 encoded + when result is + Ok s -> s + _ -> "" + "# + ), + RocStr::from(r#"{"a":{"B":[{"c":"foo"}]}}"#), + RocStr + ) +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn encode_derived_list_string() { + assert_evals_to!( + indoc!( + r#" + app "test" + imports [Encode, TotallyNotJson] + provides [main] to "./platform" + + main = + lst = ["foo", "bar", "baz"] + encoded = Encode.toBytes lst TotallyNotJson.json + result = Str.fromUtf8 encoded + when result is + Ok s -> s + _ -> "" + "# + ), + RocStr::from(r#"["foo","bar","baz"]"#), + RocStr + ) +} + +#[test] +#[cfg(not(debug_assertions))] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn encode_derived_list_of_records() { + assert_evals_to!( + indoc!( + r#" + app "test" + imports [Encode, TotallyNotJson] + provides [main] to "./platform" + + main = + lst = [{a: "foo"}, {a: "bar"}, {a: "baz"}] + encoded = Encode.toBytes lst TotallyNotJson.json + result = Str.fromUtf8 encoded + when result is + Ok s -> s + _ -> "" + "# + ), + RocStr::from(r#"[{"a":"foo"},{"a":"bar"},{"a":"baz"}]"#), + RocStr + ) +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn encode_derived_list_of_lists_of_strings() { + assert_evals_to!( + indoc!( + r#" + app "test" + imports [Encode, TotallyNotJson] + provides [main] to "./platform" + + main = + lst = [["a", "b"], ["c", "d", "e"], ["f"]] + encoded = Encode.toBytes lst TotallyNotJson.json + result = Str.fromUtf8 encoded + when result is + Ok s -> s + _ -> "" + "# + ), + RocStr::from(r#"[["a","b"],["c","d","e"],["f"]]"#), + RocStr + ) +} + +#[test] +#[cfg(not(debug_assertions))] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn encode_derived_record_with_many_types() { + assert_evals_to!( + indoc!( + r#" + app "test" + imports [Encode, TotallyNotJson] + provides [main] to "./platform" + + main = + fresh : [Fresh Str, Rotten Str] + fresh = Fresh "tomatoes" + rcd = {actors: ["Idris Elba", "Mila Kunis"], year: 2004u16, rating: {average: 7u8, min: 1u8, max: 10u8, sentiment: fresh}} + result = Str.fromUtf8 (Encode.toBytes rcd TotallyNotJson.json) + when result is + Ok s -> s + _ -> "" + "# + ), + RocStr::from( + r#"{"actors":["Idris Elba","Mila Kunis"],"rating":{"average":7,"max":10,"min":1,"sentiment":{"Fresh":["tomatoes"]}},"year":2004}"# + ), + RocStr + ) +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn encode_derived_tuple_two_fields() { + assert_evals_to!( + indoc!( + r#" + app "test" + imports [Encode, TotallyNotJson] + provides [main] to "./platform" + + main = + tup = ("foo", 10u8) + result = Str.fromUtf8 (Encode.toBytes tup TotallyNotJson.json) + when result is + Ok s -> s + _ -> "" + "# + ), + RocStr::from(r#"["foo",10]"#), + RocStr + ) +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn encode_derived_tuple_of_tuples() { + assert_evals_to!( + indoc!( + r#" + app "test" + imports [Encode, TotallyNotJson] + provides [main] to "./platform" + + main = + tup = ( ("foo", 10u8), (23u8, "bar", 15u8) ) + result = Str.fromUtf8 (Encode.toBytes tup TotallyNotJson.json) + when result is + Ok s -> s + _ -> "" + "# + ), + RocStr::from(r#"[["foo",10],[23,"bar",15]]"#), + RocStr + ) +} + +#[test] +#[cfg(not(debug_assertions))] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn encode_derived_generic_record_with_different_field_types() { + assert_evals_to!( + indoc!( + r#" + app "test" + imports [Encode, TotallyNotJson] + provides [main] to "./platform" + + Q a b := {a: a, b: b} implements [Encoding] + + q = @Q {a: 10u32, b: "fieldb"} + + main = + result = Str.fromUtf8 (Encode.toBytes q TotallyNotJson.json) + when result is + Ok s -> s + _ -> "" + "# + ), + RocStr::from(r#"{"a":10,"b":"fieldb"}"#), + RocStr + ) +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn encode_derived_generic_tag_with_different_field_types() { + assert_evals_to!( + indoc!( + r#" + app "test" + imports [Encode, TotallyNotJson] + provides [main] to "./platform" + + Q a b := [A a, B b] implements [Encoding] + + q : Q Str U32 + q = @Q (B 67) + + main = + result = Str.fromUtf8 (Encode.toBytes q TotallyNotJson.json) + when result is + Ok s -> s + _ -> "" + "# + ), + RocStr::from(r#"{"B":[67]}"#), + RocStr + ) +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn specialize_unique_newtype_records() { + with_larger_debug_stack(|| { + assert_evals_to!( + indoc!( + r#" + app "test" + imports [Encode, TotallyNotJson] + provides [main] to "./platform" + + main = + when Str.fromUtf8 (Encode.toBytes {a: Bool.true} TotallyNotJson.json) is + Ok s -> when Str.fromUtf8 (Encode.toBytes {b: Bool.true} TotallyNotJson.json) is + Ok t -> "\(s)\(t)" + _ -> "" + _ -> "" + "# + ), + RocStr::from(r#"{"a":true}{"b":true}"#), + RocStr + ) + }); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn decode_use_stdlib() { + assert_evals_to!( + indoc!( + r#" + app "test" + imports [TotallyNotJson] + provides [main] to "./platform" + + MyNum := U8 implements [Decoding {decoder: myDecoder}] + + myDecoder = + Decode.custom \bytes, fmt -> + when Decode.decodeWith bytes Decode.u8 fmt is + {result, rest} -> + when result is + Ok n -> {result: Ok (@MyNum n), rest} + Err e -> {result: Err e, rest} + + main = + when Decode.fromBytes [49, 53] TotallyNotJson.json is + Ok (@MyNum n) -> n + _ -> 101 + "# + ), + 15, + u8 + ) +} + +#[test] +#[cfg(all( + any(feature = "gen-llvm", feature = "gen-wasm"), + not(debug_assertions) // https://github.com/roc-lang/roc/issues/3898 +))] +fn decode_derive_decoder_for_opaque() { + assert_evals_to!( + indoc!( + r#" + app "test" + imports [TotallyNotJson] + provides [main] to "./platform" + + HelloWorld := { a: Str } implements [Decoding] + + main = + when Str.toUtf8 """{"a":"Hello, World!"}""" |> Decode.fromBytes TotallyNotJson.json is + Ok (@HelloWorld {a}) -> a + _ -> "FAIL" + "# + ), + RocStr::from(r#"Hello, World!"#), + RocStr + ) +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn decode_use_stdlib_json_list() { + assert_evals_to!( + indoc!( + r#" + app "test" + imports [TotallyNotJson] + provides [main] to "./platform" + + MyNumList := List U8 implements [Decoding {decoder: myDecoder}] + + myDecoder = + Decode.custom \bytes, fmt -> + when Decode.decodeWith bytes (Decode.list Decode.u8) fmt is + {result, rest} -> + when result is + Ok lst -> {result: Ok (@MyNumList lst), rest} + Err e -> {result: Err e, rest} + + main = + when Str.toUtf8 "[1,2,3]" |> Decode.fromBytes TotallyNotJson.json is + Ok (@MyNumList lst) -> lst + _ -> [] + "# + ), + RocList::from_slice(&[1u8, 2u8, 3u8]), + RocList + ) +} + +mod decode_immediate { + #[cfg(feature = "gen-llvm")] + use crate::helpers::llvm::assert_evals_to; + + #[cfg(feature = "gen-wasm")] + use crate::helpers::wasm::assert_evals_to; + + #[cfg(all(test, any(feature = "gen-llvm", feature = "gen-wasm")))] + use indoc::indoc; + + #[cfg(all(test, feature = "gen-llvm"))] + use roc_std::RocStr; + + use crate::helpers::with_larger_debug_stack; + + #[test] + #[cfg(feature = "gen-llvm")] + fn string() { + with_larger_debug_stack(|| { + assert_evals_to!( + indoc!( + r#" + app "test" imports [TotallyNotJson] provides [main] to "./platform" + + main = + when Str.toUtf8 "\"foo\"" |> Decode.fromBytes TotallyNotJson.json is + Ok s -> s + _ -> "" + "# + ), + RocStr::from("foo"), + RocStr + ) + }); + } + + #[test] + #[cfg(feature = "gen-llvm")] + fn ranged_number() { + assert_evals_to!( + indoc!( + r#" + app "test" imports [TotallyNotJson] provides [main] to "./platform" + + main = + input = Str.toUtf8 "[1,2,3]" + expected = [1,2,3] + + actual = Decode.fromBytes input TotallyNotJson.json |> Result.withDefault [] + + actual == expected + "# + ), + true, + bool + ) + } + + #[test] + #[cfg(feature = "gen-llvm")] + fn bool() { + assert_evals_to!( + indoc!( + r#" + app "test" imports [TotallyNotJson] provides [main] to "./platform" + + main = + when Str.toUtf8 "false" |> Decode.fromBytes TotallyNotJson.json is + Ok s -> s + _ -> Bool.true + "# + ), + false, + bool + ) + } + + macro_rules! num_immediate { + ($($num:expr, $typ:ident)*) => {$( + #[test] + #[cfg(feature = "gen-llvm")] + fn $typ() { + assert_evals_to!( + &format!(indoc!( + r#" + app "test" imports [TotallyNotJson] provides [main] to "./platform" + + main = + when Num.toStr {}{} |> Str.toUtf8 |> Decode.fromBytes TotallyNotJson.json is + Ok n -> n + _ -> 101{} + "# + ), $num, stringify!($typ), stringify!($typ)), + $num, + $typ + ) + } + )*} + } + + num_immediate! { + 17, i8 + 17, i16 + 17, i32 + 17, i64 + 17, i128 + 17, u8 + 17, u16 + 17, u32 + 17, u64 + 17, u128 + 17.23, f32 + 17.23, f64 + } + + #[test] + #[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] + fn dec() { + use roc_std::RocDec; + + assert_evals_to!( + indoc!( + r#" + app "test" imports [TotallyNotJson] provides [main] to "./platform" + + main = + when Num.toStr 17.23dec |> Str.toUtf8 |> Decode.fromBytes TotallyNotJson.json is + Ok n -> n + _ -> 101dec + "# + ), + RocDec::from_str("17.23").unwrap(), + RocDec + ) + } +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn decode_list_of_strings() { + with_larger_debug_stack(|| { + assert_evals_to!( + indoc!( + r#" + app "test" imports [TotallyNotJson] provides [main] to "./platform" + + main = + when Str.toUtf8 "[\"a\",\"b\",\"c\"]" |> Decode.fromBytes TotallyNotJson.json is + Ok l -> Str.joinWith l "," + _ -> "" + "# + ), + RocStr::from("a,b,c"), + RocStr + ) + }); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn encode_then_decode_list_of_strings() { + with_larger_debug_stack(|| { + assert_evals_to!( + indoc!( + r#" + app "test" imports [TotallyNotJson] provides [main] to "./platform" + + main = + when Encode.toBytes ["a", "b", "c"] TotallyNotJson.json |> Decode.fromBytes TotallyNotJson.json is + Ok l -> Str.joinWith l "," + _ -> "something went wrong" + "# + ), + RocStr::from("a,b,c"), + RocStr + ) + }); +} + +#[test] +#[cfg(feature = "gen-llvm")] +#[ignore = "#3696: Currently hits some weird panic in borrow checking, not sure if it's directly related to abilities."] +fn encode_then_decode_list_of_lists_of_strings() { + with_larger_debug_stack(|| { + assert_evals_to!( + indoc!( + r#" + app "test" imports [TotallyNotJson] provides [main] to "./platform" + + main = + when Encode.toBytes [["a", "b"], ["c", "d", "e"], ["f"]] TotallyNotJson.json |> Decode.fromBytes TotallyNotJson.json is + Ok list -> (List.map list \inner -> Str.joinWith inner ",") |> Str.joinWith l ";" + _ -> "something went wrong" + "# + ), + RocStr::from("a,b;c,d,e;f"), + RocStr + ) + }) +} + +#[test] +#[cfg(all( + any(feature = "gen-llvm", feature = "gen-wasm"), + not(debug_assertions) // https://github.com/roc-lang/roc/issues/3898 +))] +fn decode_record_two_fields() { + assert_evals_to!( + indoc!( + r#" + app "test" imports [TotallyNotJson] provides [main] to "./platform" + + main = + when Str.toUtf8 "{\"first\":\"ab\",\"second\":\"cd\"}" |> Decode.fromBytes TotallyNotJson.json is + Ok {first: "ab", second: "cd"} -> "abcd" + _ -> "something went wrong" + "# + ), + RocStr::from("abcd"), + RocStr + ) +} + +#[test] +#[cfg(all( + any(feature = "gen-llvm", feature = "gen-wasm"), + not(debug_assertions) // https://github.com/roc-lang/roc/issues/3898 +))] +fn decode_record_two_fields_string_and_int() { + assert_evals_to!( + indoc!( + r#" + app "test" imports [TotallyNotJson] provides [main] to "./platform" + + main = + when Str.toUtf8 "{\"first\":\"ab\",\"second\":10}" |> Decode.fromBytes TotallyNotJson.json is + Ok {first: "ab", second: 10u8} -> "ab10" + _ -> "something went wrong" + "# + ), + RocStr::from("ab10"), + RocStr + ) +} + +#[test] +#[cfg(all( + any(feature = "gen-llvm", feature = "gen-wasm"), + not(debug_assertions) // https://github.com/roc-lang/roc/issues/3898 +))] +fn decode_record_two_fields_string_and_string_infer() { + assert_evals_to!( + indoc!( + r#" + app "test" imports [TotallyNotJson] provides [main] to "./platform" + + main = + when Str.toUtf8 "{\"first\":\"ab\",\"second\":\"cd\"}" |> Decode.fromBytes TotallyNotJson.json is + Ok {first, second} -> Str.concat first second + _ -> "something went wrong" + "# + ), + RocStr::from("abcd"), + RocStr + ) +} + +#[test] +#[cfg(all( + any(feature = "gen-llvm", feature = "gen-wasm"), + not(debug_assertions) // https://github.com/roc-lang/roc/issues/3898 +))] +fn decode_record_two_fields_string_and_string_infer_local_var() { + assert_evals_to!( + indoc!( + r#" + app "test" imports [TotallyNotJson] provides [main] to "./platform" + + main = + decoded = Str.toUtf8 "{\"first\":\"ab\",\"second\":\"cd\"}" |> Decode.fromBytes TotallyNotJson.json + when decoded is + Ok rcd -> Str.concat rcd.first rcd.second + _ -> "something went wrong" + "# + ), + RocStr::from("abcd"), + RocStr + ) +} + +#[test] +#[cfg(all( + any(feature = "gen-llvm", feature = "gen-wasm"), + not(debug_assertions) // https://github.com/roc-lang/roc/issues/3898 +))] +fn decode_record_two_fields_string_and_string_infer_local_var_destructured() { + assert_evals_to!( + indoc!( + r#" + app "test" imports [TotallyNotJson] provides [main] to "./platform" + + main = + decoded = Str.toUtf8 "{\"first\":\"ab\",\"second\":\"cd\"}" |> Decode.fromBytes TotallyNotJson.json + when decoded is + Ok {first, second} -> Str.concat first second + _ -> "something went wrong" + "# + ), + RocStr::from("abcd"), + RocStr + ) +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +#[ignore = "json parsing impl must be fixed first"] +fn decode_empty_record() { + assert_evals_to!( + indoc!( + r#" + app "test" imports [TotallyNotJson] provides [main] to "./platform" + + main = + when Str.toUtf8 "{}" |> Decode.fromBytes TotallyNotJson.json is + Ok {} -> "empty" + _ -> "something went wrong" + "# + ), + RocStr::from("empty"), + RocStr + ) +} + +#[test] +#[cfg(all( + any(feature = "gen-llvm", feature = "gen-wasm"), + not(feature = "gen-llvm-wasm"), // hits a wasm3 stack overflow + not(debug_assertions) // https://github.com/roc-lang/roc/issues/3898 +))] +fn decode_record_of_record() { + assert_evals_to!( + indoc!( + r#" + app "test" imports [TotallyNotJson] provides [main] to "./platform" + + main = + when Str.toUtf8 "{\"outer\":{\"inner\":\"a\"},\"other\":{\"one\":\"b\",\"two\":10}}" |> Decode.fromBytes TotallyNotJson.json is + Ok {outer: {inner: "a"}, other: {one: "b", two: 10u8}} -> "ab10" + _ -> "something went wrong" + "# + ), + RocStr::from("ab10"), + RocStr + ) +} + +#[test] +#[cfg(all( + any(feature = "gen-llvm", feature = "gen-wasm"), + not(debug_assertions) // https://github.com/roc-lang/roc/issues/3898 +))] +fn decode_tuple_two_elements() { + assert_evals_to!( + indoc!( + r#" + app "test" imports [TotallyNotJson] provides [main] to "./platform" + + main = + when Str.toUtf8 "[\"ab\",10]" |> Decode.fromBytes TotallyNotJson.json is + Ok ("ab", 10u8) -> "abcd" + _ -> "something went wrong" + "# + ), + RocStr::from("abcd"), + RocStr + ) +} + +#[test] +#[cfg(all( + any(feature = "gen-llvm", feature = "gen-wasm"), + not(debug_assertions) // https://github.com/roc-lang/roc/issues/3898 +))] +fn decode_tuple_of_tuples() { + assert_evals_to!( + indoc!( + r#" + app "test" imports [TotallyNotJson] provides [main] to "./platform" + + main = + when Str.toUtf8 "[[\"ab\",10],[\"cd\",25]]" |> Decode.fromBytes TotallyNotJson.json is + Ok ( ("ab", 10u8), ("cd", 25u8) ) -> "abcd" + _ -> "something went wrong" + "# + ), + RocStr::from("abcd"), + RocStr + ) +} + +#[cfg(all(test, any(feature = "gen-llvm", feature = "gen-wasm")))] +mod hash { + #[cfg(feature = "gen-llvm")] + use crate::helpers::llvm::assert_evals_to; + + #[cfg(feature = "gen-wasm")] + use crate::helpers::wasm::assert_evals_to; + + use indoc::indoc; + + const TEST_HASHER: &str = indoc!( + r#" + THasher := List U8 implements [Hasher { + addBytes: tAddBytes, + addU8: tAddU8, + addU16: tAddU16, + addU32: tAddU32, + addU64: tAddU64, + addU128: tAddU128, + complete: tComplete, + }] + + # ignores endian-ness + byteAt = \n, shift -> + Num.bitwiseAnd (Num.shiftRightBy n (shift * 8)) 0xFF + |> Num.toU8 + + do8 = \total, n -> + total + |> List.append (byteAt n 0) + + do16 = \total, n -> + total + |> do8 (n |> Num.toU8) + |> do8 (Num.shiftRightBy n 8 |> Num.toU8) + + do32 = \total, n -> + total + |> do16 (n |> Num.toU16) + |> do16 (Num.shiftRightBy n 16 |> Num.toU16) + + do64 = \total, n -> + total + |> do32 (n |> Num.toU32) + |> do32 (Num.shiftRightBy n 32 |> Num.toU32) + + do128 = \total, n -> + total + |> do64 (n |> Num.toU64) + |> do64 (Num.shiftRightBy n 64 |> Num.toU64) + + tAddBytes = \@THasher total, bytes -> @THasher (List.concat total bytes) + tAddU8 = \@THasher total, n -> @THasher (do8 total n) + tAddU16 = \@THasher total, n -> @THasher (do16 total n) + tAddU32 = \@THasher total, n -> @THasher (do32 total n) + tAddU64 = \@THasher total, n -> @THasher (do64 total n) + tAddU128 = \@THasher total, n -> @THasher (do128 total n) + tComplete = \@THasher _ -> Num.maxU64 + + tRead = \@THasher bytes -> bytes + "# + ); + + fn build_test(input: &str) -> String { + format!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + {} + + main = + @THasher [] + |> Hash.hash ({}) + |> tRead + "# + ), + TEST_HASHER, input, + ) + } + + mod immediate { + use super::{assert_evals_to, build_test}; + use roc_std::RocList; + + #[test] + fn bool_false() { + assert_evals_to!( + &build_test("Bool.false"), + RocList::from_slice(&[0]), + RocList + ) + } + + #[test] + fn bool_true() { + assert_evals_to!( + &build_test("Bool.true"), + RocList::from_slice(&[1]), + RocList + ) + } + + #[test] + fn i8() { + assert_evals_to!( + &build_test("-2i8"), + RocList::from_slice(&[254]), + RocList + ) + } + + #[test] + fn u8() { + assert_evals_to!( + &build_test("254u8"), + RocList::from_slice(&[254]), + RocList + ) + } + + #[test] + fn i16() { + assert_evals_to!( + &build_test("-2i16"), + RocList::from_slice(&[254, 255]), + RocList + ) + } + + #[test] + fn u16() { + assert_evals_to!( + &build_test("Num.maxU16 - 1"), + RocList::from_slice(&[254, 255]), + RocList + ) + } + + #[test] + fn i32() { + assert_evals_to!( + &build_test("-2i32"), + RocList::from_slice(&[254, 255, 255, 255]), + RocList + ) + } + + #[test] + fn u32() { + assert_evals_to!( + &build_test("Num.maxU32 - 1"), + RocList::from_slice(&[254, 255, 255, 255]), + RocList + ) + } + + #[test] + fn i64() { + assert_evals_to!( + &build_test("-2i64"), + RocList::from_slice(&[254, 255, 255, 255, 255, 255, 255, 255]), + RocList + ) + } + + #[test] + fn u64() { + assert_evals_to!( + &build_test("Num.maxU64 - 1"), + RocList::from_slice(&[254, 255, 255, 255, 255, 255, 255, 255]), + RocList + ) + } + + #[test] + #[cfg(not(feature = "gen-wasm"))] // shr not implemented for U128 + fn i128() { + assert_evals_to!( + &build_test("-2i128"), + RocList::from_slice(&[ + 254, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + ]), + RocList + ) + } + + #[test] + #[cfg(not(feature = "gen-wasm"))] // shr not implemented for U128 + fn u128() { + assert_evals_to!( + &build_test("Num.maxU128 - 1"), + RocList::from_slice(&[ + 254, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255 + ]), + RocList + ) + } + + #[test] + #[cfg(not(feature = "gen-wasm"))] // shr not implemented for U128 + fn dec() { + assert_evals_to!( + &build_test("1.1dec"), + RocList::from_slice(&[0, 0, 238, 4, 44, 252, 67, 15, 0, 0, 0, 0, 0, 0, 0, 0]), + RocList + ) + } + + #[test] + fn string() { + assert_evals_to!( + &build_test(r#""ab☃AB""#), + RocList::from_slice(&[97, 98, 226, 152, 131, 65, 66]), + RocList + ) + } + + #[test] + fn list_u8() { + assert_evals_to!( + &build_test(r#"[15u8, 23u8, 37u8]"#), + RocList::from_slice(&[15, 23, 37]), + RocList + ) + } + + #[test] + fn list_string() { + assert_evals_to!( + &build_test(r#"["ab", "cd", "ef"]"#), + RocList::from_slice(&[97, 98, 99, 100, 101, 102]), + RocList + ) + } + + #[test] + fn list_list_string() { + assert_evals_to!( + &build_test(r#"[[ "ab", "cd" ], [ "ef" ]]"#), + RocList::from_slice(&[97, 98, 99, 100, 101, 102]), + RocList + ) + } + } + + mod derived { + use super::{assert_evals_to, build_test, indoc, TEST_HASHER}; + use roc_std::RocList; + + #[test] + fn empty_record() { + assert_evals_to!( + &build_test(r#"{}"#), + RocList::from_slice(&[] as &[u8]), + RocList + ) + } + + #[test] + fn record_of_u8_and_str() { + assert_evals_to!( + &build_test(r#"{ a: 15u8, b: "bc" }"#), + RocList::from_slice(&[15, 98, 99]), + RocList + ) + } + + #[test] + fn record_of_records() { + assert_evals_to!( + &build_test(r#"{ a: { b: 15u8, c: "bc" }, d: { b: 23u8, e: "ef" } }"#), + RocList::from_slice(&[15, 98, 99, 23, 101, 102]), + RocList + ) + } + + #[test] + fn record_of_list_of_records() { + assert_evals_to!( + &build_test( + r#"{ a: [ { b: 15u8 }, { b: 23u8 } ], b: [ { c: 45u8 }, { c: 73u8 } ] }"# + ), + RocList::from_slice(&[15, 23, 45, 73]), + RocList + ) + } + + #[test] + fn tuple_of_u8_and_str() { + assert_evals_to!( + &build_test(r#"(15u8, "bc")"#), + RocList::from_slice(&[15, 98, 99]), + RocList + ) + } + + #[test] + fn tuple_of_tuples() { + assert_evals_to!( + &build_test(r#"( (15u8, "bc"), (23u8, "ef") )"#), + RocList::from_slice(&[15, 98, 99, 23, 101, 102]), + RocList + ) + } + + #[test] + fn tuple_of_list_of_tuples() { + assert_evals_to!( + &build_test( + r#"( [ ( 15u8, 32u8 ), ( 23u8, 41u8 ) ], [ (45u8, 63u8), (58u8, 73u8) ] )"# + ), + RocList::from_slice(&[15, 32, 23, 41, 45, 63, 58, 73]), + RocList + ) + } + + #[test] + fn hash_singleton_union() { + assert_evals_to!( + &format!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + {} + + a : [A] + a = A + + main = + @THasher [] + |> Hash.hash a + |> tRead + "# + ), + TEST_HASHER, + ), + RocList::from_slice(&[ + // hash nothing because this is a newtype of a unit layout. + ] as &[u8]), + RocList + ) + } + + #[test] + fn hash_bool_tag_union() { + assert_evals_to!( + &format!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + {} + + a : [A, B] + a = A + + b : [A, B] + b = B + + main = + @THasher [] + |> Hash.hash a + |> Hash.hash b + |> tRead + "# + ), + TEST_HASHER, + ), + RocList::from_slice(&[ + 0, // A + 1, // B + ]), + RocList + ) + } + + #[test] + fn hash_byte_tag_union() { + assert_evals_to!( + &format!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + {} + + l : List [A, B, C, D, E, F, G, H] + l = [A, B, C, D, E, F, G, H] + + main = + @THasher [] + |> Hash.hash l + |> tRead + "# + ), + TEST_HASHER, + ), + RocList::from_slice(&[ + 0, // A + 1, // B + 2, // C + 3, // D + 4, // E + 5, // F + 6, // G + 7, // H + ]), + RocList + ) + } + + #[test] + fn hash_newtype_tag_union() { + assert_evals_to!( + &format!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + {} + + a : [A U8 U8 U8] + a = A 15 23 47 + + main = + @THasher [] + |> Hash.hash a + |> tRead + "# + ), + TEST_HASHER, + ), + RocList::from_slice(&[ + // discriminant is skipped because it's a newtype + 15, 23, 47 + ]), + RocList + ) + } + + #[test] + fn hash_newtype_by_void_tag_union() { + assert_evals_to!( + &format!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + {} + + a : Result [A U8 U8 U8] [] + a = Ok (A 15 23 47) + + main = + @THasher [] + |> Hash.hash a + |> tRead + "# + ), + TEST_HASHER, + ), + RocList::from_slice(&[ + 1, // Ok + // A is skipped because it is a newtype + 15, 23, 47 + ]), + RocList + ) + } + + #[test] + fn hash_heterogenous_tags() { + assert_evals_to!( + &format!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + {} + + a : [A U8 U8, B {{ a: U8 }}, C Str] + a = A 15 23 + + b : [A U8 U8, B {{ a: U8 }}, C Str] + b = B {{ a: 37 }} + + c : [A U8 U8, B {{ a: U8 }}, C Str] + c = C "abc" + + main = + @THasher [] + |> Hash.hash a + |> Hash.hash b + |> Hash.hash c + |> tRead + "# + ), + TEST_HASHER, + ), + RocList::from_slice(&[ + 0, // dicsr A + 15, 23, // payloads A + 1, // discr B + 37, // payloads B + 2, // discr C + 97, 98, 99 // payloads C + ]), + RocList + ) + } + + #[test] + fn hash_recursive_tag_union() { + assert_evals_to!( + &format!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + {} + + ConsList : [Cons U8 ConsList, Nil] + + c : ConsList + c = Cons 1 (Cons 2 Nil) + + main = + @THasher [] + |> Hash.hash c + |> tRead + "# + ), + TEST_HASHER, + ), + RocList::from_slice(&[ + 0, 1, // Cons 1 + 0, 2, // Cons 2 + 1, // Nil + ]), + RocList + ) + } + + #[test] + fn derived_hash_for_opaque_record() { + assert_evals_to!( + &format!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + {} + + Q := {{ a: U8, b: U8, c: U8 }} implements [Hash] + + q = @Q {{ a: 15, b: 27, c: 31 }} + + main = + @THasher [] + |> Hash.hash q + |> tRead + "# + ), + TEST_HASHER, + ), + RocList::from_slice(&[15, 27, 31]), + RocList + ) + } + } +} + +#[cfg(all(test, any(feature = "gen-llvm", feature = "gen-wasm")))] +mod eq { + #[cfg(feature = "gen-llvm")] + use crate::helpers::llvm::assert_evals_to; + + #[cfg(feature = "gen-wasm")] + use crate::helpers::wasm::assert_evals_to; + + use indoc::indoc; + use roc_std::RocStr; + + #[test] + fn eq_tuple() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + main = + ("a", "b") == ("a", "b") + "# + ), + true, + bool + ) + } + + #[test] + fn custom_eq_impl() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + LyingEq := U8 implements [Eq {isEq}] + + isEq = \@LyingEq m, @LyingEq n -> m != n + + main = + a = @LyingEq 10 + b = @LyingEq 5 + c = @LyingEq 5 + if Bool.isEq a b && !(Bool.isEq b c) then + "okay" + else + "fail" + "# + ), + RocStr::from("okay"), + RocStr + ) + } + + #[test] + fn custom_eq_impl_for_fn_opaque() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + Q := ({} -> Str) implements [Eq {isEq: isEqQ}] + + isEqQ = \@Q _, @Q _ -> Bool.true + + main = isEqQ (@Q \{} -> "a") (@Q \{} -> "a") + "# + ), + true, + bool + ) + } + + #[test] + #[ignore = "needs https://github.com/roc-lang/roc/issues/4557 first"] + fn custom_eq_impl_for_fn_opaque_material() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + Q := ({} -> Str) implements [Eq {isEq: isEqQ}] + + isEqQ = \@Q f1, @Q f2 -> (f1 {} == f2 {}) + + main = isEqQ (@Q \{} -> "a") (@Q \{} -> "a") + "# + ), + true, + bool + ) + } + + #[test] + fn derive_structural_eq() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + main = Bool.isEq 10u8 10u8 + "# + ), + true, + bool + ) + } + + #[test] + fn derive_structural_eq_for_opaque() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + Q := U8 implements [Eq] + + main = (@Q 15) == (@Q 15) + "# + ), + true, + bool + ) + } +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn issue_4772_weakened_monomorphic_destructure() { + with_larger_debug_stack(|| { + assert_evals_to!( + indoc!( + r###" + app "test" + imports [TotallyNotJson] + provides [main] to "./platform" + + getNumber = + { result, rest } = Decode.fromBytesPartial (Str.toUtf8 "\"1234\"") TotallyNotJson.json + + when result is + Ok val -> + when Str.toI64 val is + Ok number -> + Ok {val : number, input : rest} + Err InvalidNumStr -> + Err (ParsingFailure "not a number") + + Err _ -> + Err (ParsingFailure "not a number") + + main = + getNumber |> Result.map .val |> Result.withDefault 0 + "### + ), + 1234i64, + i64 + ) + }) +} + +mod inspect { + #[cfg(feature = "gen-llvm")] + use crate::helpers::llvm::assert_evals_to; + + #[cfg(feature = "gen-wasm")] + use crate::helpers::wasm::assert_evals_to; + + #[cfg(all(test, any(feature = "gen-llvm", feature = "gen-wasm")))] + use indoc::indoc; + + #[cfg(all(test, any(feature = "gen-llvm", feature = "gen-wasm")))] + use roc_std::RocStr; + + #[test] + #[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] + fn bool() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + main = [ + Inspect.inspect Bool.true, + Inspect.inspect Bool.false, + ] |> List.map Inspect.toDbgStr |> Str.joinWith ", " + "# + ), + RocStr::from("Bool.true, Bool.false"), + RocStr + ); + } + + #[test] + #[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] + fn num() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + main = [ + Inspect.inspect 0, # Num a + Inspect.inspect 1u8, # U8 + Inspect.inspect 2i8, # I8 + Inspect.inspect 3u16, # U16 + Inspect.inspect 4i16, # I16 + Inspect.inspect 5u32, # U32 + Inspect.inspect 6i32, # I32 + Inspect.inspect 7u64, # U64 + Inspect.inspect 8i64, # I64 + Inspect.inspect 9u128, # U128 + Inspect.inspect 10i128, # I128 + Inspect.inspect 0.5, # Frac a + Inspect.inspect 1.5f32, # F32 + Inspect.inspect 2.2f64, # F64 + Inspect.inspect (1.1dec + 2.2), # Dec + ] |> List.map Inspect.toDbgStr |> Str.joinWith ", " + "# + ), + RocStr::from("0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 0.5, 1.5, 2.2, 3.3"), + RocStr + ); + } + + #[test] + #[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] + fn list() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + main = [ + Inspect.inspect [0, 1, 2], # List (Num *) + Inspect.inspect [1, 0x2, 3], # List (Int *) + Inspect.inspect [0.1 + 0.2, 0.4], # List (Frac *) + Inspect.inspect [1u8, 2u8], # List U8 + Inspect.inspect ["foo"], # List Str + ] |> List.map Inspect.toDbgStr |> Str.joinWith ", " + "# + ), + RocStr::from("[0, 1, 2], [1, 2, 3], [0.3, 0.4], [1, 2], [\"foo\"]"), + RocStr + ); + } + + #[test] + #[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] + fn str() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + main = [ + Inspect.inspect "", + Inspect.inspect "a small string", + Inspect.inspect "an extraordinarily long string - so long it's on the heap!", + ] |> List.map Inspect.toDbgStr |> Str.joinWith ", " + "# + ), + RocStr::from( + r#""", "a small string", "an extraordinarily long string - so long it's on the heap!""# + ), + RocStr + ); + } + + #[test] + #[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] + fn opaque_automatic() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + Op := {} + + main = Inspect.toDbgStr (Inspect.inspect (@Op {})) + "# + ), + RocStr::from(r#""#), + RocStr + ); + } + + #[test] + #[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] + fn opaque_automatic_with_polymorphic_call() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + Op := {} + + late = \a -> Inspect.toDbgStr (Inspect.inspect a) + + main = late (@Op {}) + "# + ), + RocStr::from(r#""#), + RocStr + ); + } +} diff --git a/compiler/test_gen/src/gen_compare.rs b/crates/compiler/test_gen/src/gen_compare.rs similarity index 84% rename from compiler/test_gen/src/gen_compare.rs rename to crates/compiler/test_gen/src/gen_compare.rs index efc062eb5c..7e2f31dc93 100644 --- a/compiler/test_gen/src/gen_compare.rs +++ b/crates/compiler/test_gen/src/gen_compare.rs @@ -27,7 +27,7 @@ fn eq_i64() { } #[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] fn neq_i64() { assert_evals_to!( indoc!( @@ -61,7 +61,7 @@ fn eq_u64() { } #[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] fn neq_u64() { assert_evals_to!( indoc!( @@ -78,49 +78,15 @@ fn neq_u64() { } #[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn eq_f64() { - assert_evals_to!( - indoc!( - r#" - i : F64 - i = 1 - - i == i - "# - ), - true, - bool - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -fn neq_f64() { - assert_evals_to!( - indoc!( - r#" - i : F64 - i = 1 - - i != i - "# - ), - false, - bool - ); -} - -#[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] fn eq_bool_tag() { assert_evals_to!( indoc!( r#" true : Bool - true = True + true = Bool.true - true == True + true == Bool.true "# ), true, @@ -129,15 +95,15 @@ fn eq_bool_tag() { } #[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] fn neq_bool_tag() { assert_evals_to!( indoc!( r#" true : Bool - true = True + true = Bool.true - true == False + true == Bool.false "# ), false, @@ -145,6 +111,52 @@ fn neq_bool_tag() { ); } +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn bool_logic() { + assert_evals_to!( + indoc!( + r#" + bool1 = Bool.true + bool2 = Bool.false + bool3 = !bool1 + + (bool1 && bool2) || bool2 && bool3 + "# + ), + false, + bool + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn and_bool() { + assert_evals_to!("Bool.true && Bool.true", true, bool); + assert_evals_to!("Bool.true && Bool.false", false, bool); + assert_evals_to!("Bool.false && Bool.true", false, bool); + assert_evals_to!("Bool.false && Bool.false", false, bool); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn or_bool() { + assert_evals_to!("Bool.true || Bool.true", true, bool); + assert_evals_to!("Bool.true || Bool.false", true, bool); + assert_evals_to!("Bool.false || Bool.true", true, bool); + assert_evals_to!("Bool.false || Bool.false", false, bool); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn not_bool() { + assert_evals_to!("!Bool.true", false, bool); + assert_evals_to!("!Bool.false", true, bool); + + assert_evals_to!("!(!Bool.true)", true, bool); + assert_evals_to!("!(!Bool.false)", false, bool); +} + #[test] #[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] fn empty_record() { @@ -186,7 +198,7 @@ fn unit() { } #[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] fn newtype() { assert_evals_to!("Identity 42 == Identity 42", true, bool); assert_evals_to!("Identity 42 != Identity 42", false, bool); @@ -373,7 +385,7 @@ fn eq_linked_list_false() { } #[test] -#[cfg(any(feature = "gen-wasm"))] +#[ignore] // breaks for LLVM (no tail recursion), takes a long time for Wasm fn eq_linked_list_long() { assert_evals_to!( indoc!( @@ -389,7 +401,7 @@ fn eq_linked_list_long() { prependOnes (n-1) (Cons 1 tail) main = - n = 100_000 + n = 100_000 # be careful, can make a noticeble difference to test_gen total time! x : LinkedList I64 x = prependOnes n (Cons 999 Nil) @@ -499,7 +511,7 @@ fn eq_rosetree() { } #[test] -#[cfg(any(feature = "gen-wasm"))] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] fn eq_different_rosetrees() { // Requires two different equality procedures for `List (Rose I64)` and `List (Rose Str)` // even though both appear in the mono Layout as `List(RecursivePointer)` @@ -542,10 +554,7 @@ fn eq_different_rosetrees() { #[test] #[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -#[ignore] fn rosetree_with_tag() { - // currently stack overflows in type checking - assert_evals_to!( indoc!( r#" @@ -672,3 +681,25 @@ fn compare_nullable_recursive_union_same_content() { bool ); } + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn boxed_eq_int() { + assert_evals_to!("Box.box 1i64 == Box.box 1", true, bool); + assert_evals_to!("Box.box 2i64 == Box.box 1", false, bool); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn boxed_eq_str() { + assert_evals_to!( + "Box.box \"Hello, world\" == Box.box \"Hello, world\"", + true, + bool + ); + assert_evals_to!( + "Box.box \"Hello, world\" == Box.box \"Hello, stranger\"", + false, + bool + ); +} diff --git a/crates/compiler/test_gen/src/gen_definitions.rs b/crates/compiler/test_gen/src/gen_definitions.rs new file mode 100644 index 0000000000..ef575efc48 --- /dev/null +++ b/crates/compiler/test_gen/src/gen_definitions.rs @@ -0,0 +1,47 @@ +#[cfg(feature = "gen-llvm")] +use crate::helpers::llvm::assert_evals_to; + +#[cfg(feature = "gen-dev")] +use crate::helpers::dev::assert_evals_to; + +#[cfg(feature = "gen-wasm")] +use crate::helpers::wasm::assert_evals_to; + +// use crate::helpers::with_larger_debug_stack; +//use crate::assert_wasm_evals_to as assert_evals_to; +#[allow(unused_imports)] +use indoc::indoc; +#[allow(unused_imports)] +use roc_std::{RocList, RocResult, RocStr}; + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))] +fn def_closure_in_parens() { + assert_evals_to!( + indoc!( + r#" + id = (\x -> x) + + id 42u32 + "# + ), + 42, + u32 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))] +fn def_closure_in_multiple_parens() { + assert_evals_to!( + indoc!( + r#" + id = (((\x -> x))) + + id 42u32 + "# + ), + 42, + u32 + ); +} diff --git a/crates/compiler/test_gen/src/gen_dict.rs b/crates/compiler/test_gen/src/gen_dict.rs new file mode 100644 index 0000000000..6e0181223c --- /dev/null +++ b/crates/compiler/test_gen/src/gen_dict.rs @@ -0,0 +1,552 @@ +#![cfg(all( + any(feature = "gen-llvm", feature = "gen-wasm"), + not(debug_assertions) // https://github.com/roc-lang/roc/issues/3898 +))] + +#[cfg(feature = "gen-llvm")] +use crate::helpers::llvm::assert_evals_to; + +// #[cfg(feature = "gen-dev")] +// use crate::helpers::dev::assert_evals_to; + +#[cfg(feature = "gen-wasm")] +use crate::helpers::wasm::assert_evals_to; + +use indoc::indoc; +use roc_std::{RocList, RocStr}; + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn dict_empty_len() { + assert_evals_to!( + indoc!( + r#" + Dict.len (Dict.empty {}) + "# + ), + 0, + usize + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn dict_insert_empty() { + assert_evals_to!( + indoc!( + r#" + Dict.empty {} + |> Dict.insert 42 32 + |> Dict.len + "# + ), + 1, + usize + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn dict_empty_contains() { + assert_evals_to!( + indoc!( + r#" + empty : Dict.Dict I64 F64 + empty = Dict.empty {} + + Dict.contains empty 42 + "# + ), + false, + bool + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn dict_nonempty_contains() { + assert_evals_to!( + indoc!( + r#" + empty : Dict.Dict I64 F64 + empty = Dict.insert (Dict.empty {}) 42 1.23 + + Dict.contains empty 42 + "# + ), + true, + bool + ); +} + +#[test] +#[ignore = "TODO figure out why this is broken with llvm wasm tests"] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn dict_empty_remove() { + assert_evals_to!( + indoc!( + r#" + empty : Dict.Dict I64 F64 + empty = Dict.empty {} + + empty + |> Dict.remove 42 + |> Dict.len + "# + ), + 0, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn dict_nonempty_remove() { + assert_evals_to!( + indoc!( + r#" + empty : Dict.Dict I64 F64 + empty = Dict.insert (Dict.empty {}) 42 1.23 + + empty + |> Dict.remove 42 + |> Dict.len + |> Num.toI64 + "# + ), + 0, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn dict_nonempty_get() { + assert_evals_to!( + indoc!( + r#" + empty : Dict.Dict I64 F64 + empty = Dict.insert (Dict.empty {}) 42 1.23 + + withDefault = \x, def -> + when x is + Ok v -> v + Err _ -> def + + empty + |> Dict.insert 42 1.23f64 + |> Dict.get 42 + |> withDefault 0 + "# + ), + 1.23, + f64 + ); + + assert_evals_to!( + indoc!( + r#" + withDefault = \x, def -> + when x is + Ok v -> v + Err _ -> def + + Dict.empty {} + |> Dict.insert 42 1.23f64 + |> Dict.get 43 + |> withDefault 0 + "# + ), + 0.0, + f64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn keys() { + assert_evals_to!( + indoc!( + r#" + myDict : Dict.Dict I64 I64 + myDict = + Dict.empty {} + |> Dict.insert 0 100 + |> Dict.insert 1 100 + |> Dict.insert 2 100 + + + Dict.keys myDict + "# + ), + RocList::from_slice(&[0, 1, 2]), + RocList + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn values() { + assert_evals_to!( + indoc!( + r#" + myDict : Dict.Dict I64 I64 + myDict = + Dict.empty {} + |> Dict.insert 0 100 + |> Dict.insert 1 200 + |> Dict.insert 2 300 + + + Dict.values myDict + "# + ), + RocList::from_slice(&[100, 200, 300]), + RocList + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn from_list_with_fold_simple() { + assert_evals_to!( + indoc!( + r#" + myDict : Dict.Dict I64 I64 + myDict = + [1,2,3] + |> List.walk (Dict.empty {}) (\accum, value -> Dict.insert accum value value) + + Dict.values myDict + "# + ), + RocList::from_slice(&[1, 2, 3]), + RocList + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn from_list_with_fold_reallocates() { + assert_evals_to!( + indoc!( + r#" + range : I64, I64, List I64-> List I64 + range = \low, high, accum -> + if low < high then + range (low + 1) high (List.append accum low) + else + accum + + myDict : Dict.Dict I64 I64 + myDict = + # 25 elements (8 + 16 + 1) is guaranteed to overflow/reallocate at least twice + range 0 25 [] + |> List.walk (Dict.empty {}) (\accum, value -> Dict.insert accum value value) + + Dict.values myDict + "# + ), + RocList::from_slice(&[ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, + 24 + ]), + RocList + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn small_str_keys() { + assert_evals_to!( + indoc!( + r#" + myDict : Dict.Dict Str I64 + myDict = + Dict.empty {} + |> Dict.insert "a" 100 + + Dict.keys myDict + "# + ), + RocList::from_slice(&["a".into()]), + RocList + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn big_str_keys() { + assert_evals_to!( + indoc!( + r#" + myDict : Dict.Dict Str I64 + myDict = + Dict.empty {} + |> Dict.insert "Leverage agile frameworks to provide a robust" 100 + |> Dict.insert "synopsis for high level overviews. Iterative approaches" 200 + |> Dict.insert "to corporate strategy foster collaborative thinking to" 300 + + Dict.keys myDict + "# + ), + RocList::from_slice(&[ + RocStr::from("Leverage agile frameworks to provide a robust"), + RocStr::from("synopsis for high level overviews. Iterative approaches"), + RocStr::from("to corporate strategy foster collaborative thinking to"), + ]), + RocList + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn big_str_values() { + assert_evals_to!( + indoc!( + r#" + myDict : Dict.Dict I64 Str + myDict = + Dict.empty {} + |> Dict.insert 100 "Leverage agile frameworks to provide a robust" + |> Dict.insert 200 "synopsis for high level overviews. Iterative approaches" + |> Dict.insert 300 "to corporate strategy foster collaborative thinking to" + + Dict.values myDict + "# + ), + RocList::from_slice(&[ + RocStr::from("Leverage agile frameworks to provide a robust"), + RocStr::from("synopsis for high level overviews. Iterative approaches"), + RocStr::from("to corporate strategy foster collaborative thinking to"), + ]), + RocList + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn unit_values() { + assert_evals_to!( + indoc!( + r#" + myDict : Dict.Dict I64 {} + myDict = + Dict.empty {} + |> Dict.insert 0 {} + |> Dict.insert 1 {} + |> Dict.insert 2 {} + |> Dict.insert 3 {} + + Num.toI64 (Dict.len myDict) + "# + ), + 4, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn single() { + assert_evals_to!( + indoc!( + r#" + myDict : Dict.Dict I64 {} + myDict = + Dict.single 12345 {} + + Num.toI64 (Dict.len myDict) + "# + ), + 1, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn insert_all() { + assert_evals_to!( + indoc!( + r#" + myDict : Dict I64 {} + myDict = + Dict.insertAll (Dict.single 0 {}) (Dict.single 1 {}) + + Dict.len myDict + |> Num.toI64 + "# + ), + 2, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn insert_all_prefer_second() { + assert_evals_to!( + indoc!( + r#" + myDict : Dict.Dict I64 I64 + myDict = + (Dict.single 0 100) + |> Dict.insertAll (Dict.single 0 200) + + Dict.values myDict + "# + ), + RocList::from_slice(&[200]), + RocList + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn keep_shared() { + assert_evals_to!( + indoc!( + r#" + dict1 : Dict.Dict I64 {} + dict1 = + Dict.empty {} + |> Dict.insert 1 {} + |> Dict.insert 2 {} + |> Dict.insert 3 {} + |> Dict.insert 4 {} + |> Dict.insert 5 {} + + dict2 : Dict.Dict I64 {} + dict2 = + Dict.empty {} + |> Dict.insert 0 {} + |> Dict.insert 2 {} + |> Dict.insert 4 {} + + Dict.keepShared dict1 dict2 + |> Dict.len + |> Num.toI64 + "# + ), + 2, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn keep_shared_prefer_first() { + assert_evals_to!( + indoc!( + r#" + dict1 : Dict.Dict I64 I64 + dict1 = + Dict.empty {} + |> Dict.insert 1 1 + |> Dict.insert 2 2 + |> Dict.insert 3 3 + |> Dict.insert 4 4 + |> Dict.insert 5 5 + + dict2 : Dict.Dict I64 I64 + dict2 = + Dict.empty {} + |> Dict.insert 0 100 + |> Dict.insert 2 200 + |> Dict.insert 4 300 + + Dict.keepShared dict1 dict2 + |> Dict.values + "# + ), + RocList::from_slice(&[2, 4]), + RocList + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn remove_all() { + assert_evals_to!( + indoc!( + r#" + dict1 : Dict.Dict I64 {} + dict1 = + Dict.empty {} + |> Dict.insert 1 {} + |> Dict.insert 2 {} + |> Dict.insert 3 {} + |> Dict.insert 4 {} + |> Dict.insert 5 {} + + dict2 : Dict.Dict I64 {} + dict2 = + Dict.empty {} + |> Dict.insert 0 {} + |> Dict.insert 2 {} + |> Dict.insert 4 {} + + Dict.removeAll dict1 dict2 + |> Dict.len + |> Num.toI64 + "# + ), + 3, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn remove_all_prefer_first() { + assert_evals_to!( + indoc!( + r#" + dict1 : Dict.Dict I64 I64 + dict1 = + Dict.empty {} + |> Dict.insert 1 1 + |> Dict.insert 2 2 + |> Dict.insert 3 3 + |> Dict.insert 4 4 + |> Dict.insert 5 5 + + dict2 : Dict.Dict I64 I64 + dict2 = + Dict.empty {} + |> Dict.insert 0 100 + |> Dict.insert 2 200 + |> Dict.insert 4 300 + + Dict.removeAll dict1 dict2 + |> Dict.values + "# + ), + RocList::from_slice(&[1, 5, 3]), + RocList + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn walk_sum_keys() { + assert_evals_to!( + indoc!( + r#" + dict1 : Dict.Dict I64 I64 + dict1 = + Dict.empty {} + |> Dict.insert 1 1 + |> Dict.insert 2 2 + |> Dict.insert 3 3 + |> Dict.insert 4 4 + |> Dict.insert 5 5 + + Dict.walk dict1 0 \k, _, a -> k + a + "# + ), + 15, + i64 + ); +} diff --git a/crates/compiler/test_gen/src/gen_erased.rs b/crates/compiler/test_gen/src/gen_erased.rs new file mode 100644 index 0000000000..df4b87ff69 --- /dev/null +++ b/crates/compiler/test_gen/src/gen_erased.rs @@ -0,0 +1,44 @@ +#[cfg(feature = "gen-llvm")] +use crate::helpers::llvm::assert_evals_to_erased; + +use indoc::indoc; + +#[test] +#[cfg(feature = "gen-llvm")] +fn capture_multiple() { + assert_evals_to_erased!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + f = \n, m -> + \{} -> n + m + 15u8 + + main = (f 10u8 20u8) {} + "# + ), + 45, + u8 + ); +} + +#[test] +#[cfg(feature = "gen-llvm")] +fn multi_branch_capturing() { + assert_evals_to_erased!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + f = \t, s -> + if t + then \{} -> 15nat + else \{} -> Str.countGraphemes s + + main = ((f Bool.true "abc") {}, (f Bool.false "abc") {}) + "# + ), + (15, 3), + (usize, usize) + ); +} diff --git a/crates/compiler/test_gen/src/gen_list.rs b/crates/compiler/test_gen/src/gen_list.rs new file mode 100644 index 0000000000..db6863b8da --- /dev/null +++ b/crates/compiler/test_gen/src/gen_list.rs @@ -0,0 +1,4065 @@ +#[cfg(feature = "gen-llvm")] +use crate::helpers::llvm::assert_evals_to; + +#[cfg(feature = "gen-dev")] +use crate::helpers::dev::assert_evals_to; + +#[cfg(feature = "gen-wasm")] +use crate::helpers::wasm::assert_evals_to; + +use crate::helpers::with_larger_debug_stack; +//use crate::assert_wasm_evals_to as assert_evals_to; +#[allow(unused_imports)] +use indoc::indoc; +#[allow(unused_imports)] +use roc_std::{RocDec, RocList, RocResult, RocStr}; + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn roc_list_construction() { + let list = RocList::from_slice(&[1i64; 23]); + assert_eq!(&list, &list); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn empty_list_literal() { + assert_evals_to!("[]", RocList::::from_slice(&[]), RocList); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_literal_empty_record() { + assert_evals_to!("[{}]", RocList::from_slice(&[()]), RocList<()>); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn int_singleton_list_literal() { + assert_evals_to!("[1, 2]", RocList::from_slice(&[1, 2]), RocList); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn int_list_literal() { + assert_evals_to!("[12, 9]", RocList::from_slice(&[12, 9]), RocList); + assert_evals_to!( + "[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1]", + RocList::from_slice(&[1i64; 23]), + RocList + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn bool_list_literal() { + assert_evals_to!( + indoc!( + r#" + false = Bool.false + + [false] + "# + ), + RocList::from_slice(&[false; 1]), + RocList + ); + + assert_evals_to!( + "[Bool.true, Bool.false, Bool.true]", + RocList::from_slice(&[true, false, true]), + RocList + ); + + assert_evals_to!( + indoc!( + r#" + false : Bool + false = Bool.false + + [false] + "# + ), + RocList::from_slice(&[false; 1]), + RocList + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn dec_list_literal() { + assert_evals_to!( + "[1.0dec, 2.0dec]", + RocList::from_slice(&[RocDec::from(1), RocDec::from(2)]), + RocList + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn dec_list_join() { + assert_evals_to!( + "List.concat [1.0dec, 2.0] [3.0, 4.0, 5.0]", + RocList::from_slice(&[ + RocDec::from(1), + RocDec::from(2), + RocDec::from(3), + RocDec::from(4), + RocDec::from(5), + ]), + RocList + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn bool_list_concat() { + assert_evals_to!( + indoc!( + r#" + List.concat [Bool.true, Bool.false] [Bool.false, Bool.true] + "# + ), + RocList::from_slice(&[true, false, false, true]), + RocList + ); + + assert_evals_to!( + indoc!( + r#" + List.concat [] [Bool.false, Bool.true] + "# + ), + RocList::from_slice(&[false, true]), + RocList + ); + + assert_evals_to!( + indoc!( + r#" + List.concat [Bool.true, Bool.false] [] + "# + ), + RocList::from_slice(&[true, false]), + RocList + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn bool_list_literal_repeat() { + assert_evals_to!( + indoc!( + r#" + true : Bool + true = Bool.true + + List.repeat true 23 + "# + ), + RocList::from_slice(&[true; 23]), + RocList + ); + + assert_evals_to!( + indoc!( + r#" + true : Bool + true = Bool.true + + List.repeat { x: true, y: true } 23 + "# + ), + RocList::from_slice(&[[true, true]; 23]), + RocList<[bool; 2]> + ); + + assert_evals_to!( + indoc!( + r#" + true : Bool + true = Bool.true + + List.repeat { x: true, y: true, a: true, b: true, c: true, d : true, e: true, f: true } 23 + "# + ), + RocList::from_slice(&[[true, true, true, true, true, true, true, true]; 23]), + RocList<[bool; 8]> + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn variously_sized_list_literals() { + assert_evals_to!("[]", RocList::::from_slice(&[]), RocList); + assert_evals_to!("[1]", RocList::from_slice(&[1]), RocList); + assert_evals_to!("[1, 2]", RocList::from_slice(&[1, 2]), RocList); + assert_evals_to!("[1, 2, 3]", RocList::from_slice(&[1, 2, 3]), RocList); + assert_evals_to!( + "[1, 2, 3, 4]", + RocList::from_slice(&[1, 2, 3, 4]), + RocList + ); + assert_evals_to!( + "[1, 2, 3, 4, 5]", + RocList::from_slice(&[1, 2, 3, 4, 5]), + RocList + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_append_basic() { + assert_evals_to!( + "List.append [1] 2", + RocList::from_slice(&[1, 2]), + RocList + ); + assert_evals_to!( + "List.append [1, 1] 2", + RocList::from_slice(&[1, 1, 2]), + RocList + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_take_first() { + assert_evals_to!( + "List.takeFirst [1, 2, 3] 2", + RocList::from_slice(&[1, 2]), + RocList + ); + assert_evals_to!( + "List.takeFirst [1, 2, 3] 0", + RocList::::from_slice(&[]), + RocList + ); + assert_evals_to!( + "List.takeFirst [] 1", + RocList::::from_slice(&[]), + RocList + ); + assert_evals_to!( + "List.takeFirst [1,2] 5", + RocList::from_slice(&[1, 2]), + RocList + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_take_last() { + assert_evals_to!( + "List.takeLast [1, 2, 3] 2", + RocList::from_slice(&[2, 3]), + RocList + ); + assert_evals_to!( + "List.takeLast [1, 2, 3] 0", + RocList::::from_slice(&[]), + RocList + ); + assert_evals_to!( + "List.takeLast [] 1", + RocList::::from_slice(&[]), + RocList + ); + assert_evals_to!( + "List.takeLast [1,2] 5", + RocList::from_slice(&[1, 2]), + RocList + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_sublist() { + assert_evals_to!( + "List.sublist [1, 2, 3] { start: 0 , len: 2 } ", + RocList::from_slice(&[1, 2]), + RocList + ); + assert_evals_to!( + "List.sublist [1, 2, 3] { start: 1 , len: 2 } ", + RocList::from_slice(&[2, 3]), + RocList + ); + assert_evals_to!( + "List.sublist [1, 2, 3] { start: 2 , len: 2 } ", + RocList::from_slice(&[3]), + RocList + ); + assert_evals_to!( + "List.sublist [1, 2, 3] { start: 3 , len: 2 } ", + RocList::::from_slice(&[]), + RocList + ); + assert_evals_to!( + "List.sublist [] { start: 1 , len: 1 } ", + RocList::::from_slice(&[]), + RocList + ); + assert_evals_to!( + "List.sublist [1, 2, 3] { start: 1 , len: 0 } ", + RocList::::from_slice(&[]), + RocList + ); + assert_evals_to!( + "List.sublist [1, 2, 3] { start: 0 , len: 5 } ", + RocList::from_slice(&[1, 2, 3]), + RocList + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_map_try_ok() { + assert_evals_to!( + // No transformation + r#" + List.mapTry [1, 2, 3] \elem -> Ok elem + "#, + // Result I64 [] is unwrapped to just I64 + RocList::::from_slice(&[1, 2, 3]), + RocList + ); + assert_evals_to!( + // Transformation + r#" + List.mapTry [1, 2, 3] \num -> + str = Num.toStr (num * 2) + + Ok "\(str)!" + "#, + // Result Str [] is unwrapped to just Str + RocList::::from_slice(&[ + RocStr::from("2!"), + RocStr::from("4!"), + RocStr::from("6!"), + ]), + RocList + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_map_try_err() { + use core::convert::Infallible; + + assert_evals_to!( + r#" + List.mapTry [1, 2, 3] \_ -> Err -1 + "#, + RocResult::err(-1), + RocResult, i64> + ); + + assert_evals_to!( + // If any element returns Err, the whole thing returns Err + r#" + List.mapTry [1, 2, 3] \num -> + if num > 2 then + Err -1 + else + Ok num + "#, + RocResult::err(-1), + RocResult, i64> + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_split() { + assert_evals_to!( + r#" + list = List.split [1, 2, 3] 0 + list.before + "#, + RocList::::from_slice(&[]), + RocList + ); + assert_evals_to!( + r#" + list = List.split [1, 2, 3] 0 + list.others + "#, + RocList::from_slice(&[1, 2, 3]), + RocList + ); + assert_evals_to!( + r#" + List.split [1, 2, 3] 1 + "#, + (RocList::from_slice(&[1]), RocList::from_slice(&[2, 3])), + (RocList, RocList,) + ); + assert_evals_to!( + "List.split [1, 2, 3] 3", + ( + RocList::from_slice(&[1, 2, 3]), + RocList::::from_slice(&[]), + ), + (RocList, RocList,) + ); + assert_evals_to!( + "List.split [1, 2, 3] 4", + ( + RocList::from_slice(&[1, 2, 3]), + RocList::::from_slice(&[]), + ), + (RocList, RocList,) + ); + assert_evals_to!( + "List.split [] 1", + ( + RocList::::from_slice(&[]), + RocList::::from_slice(&[]), + ), + (RocList, RocList,) + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_split_first() { + assert_evals_to!( + r#" + List.splitFirst [2, 3, 0, 4, 0, 6, 0, 8, 9] 0 + |> Result.map .before + "#, + RocResult::ok(RocList::::from_slice(&[2, 3])), + RocResult, ()> + ); + assert_evals_to!( + r#" + List.splitFirst [2, 3, 0, 4, 0, 6, 0, 8, 9] 0 + |> Result.map .after + "#, + RocResult::ok(RocList::::from_slice(&[4, 0, 6, 0, 8, 9])), + RocResult, ()> + ); + + assert_evals_to!( + "List.splitFirst [1, 2, 3] 0", + RocResult::err(()), + RocResult<(RocList, RocList), ()> + ); + + assert_evals_to!( + "List.splitFirst [] 1", + RocResult::err(()), + RocResult<(RocList, RocList), ()> + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_split_last() { + assert_evals_to!( + r#" + List.splitLast [2, 3, 0, 4, 0, 6, 0, 8, 9] 0 + |> Result.map .before + "#, + RocResult::ok(RocList::::from_slice(&[2, 3, 0, 4, 0, 6])), + RocResult, ()> + ); + assert_evals_to!( + r#" + List.splitLast [2, 3, 0, 4, 0, 6, 0, 8, 9] 0 + |> Result.map .after + "#, + RocResult::ok(RocList::::from_slice(&[8, 9])), + RocResult, ()> + ); + + assert_evals_to!( + "List.splitLast [1, 2, 3] 0", + RocResult::err(()), + RocResult<(RocList, RocList), ()> + ); + + assert_evals_to!( + "List.splitLast [] 1", + RocResult::err(()), + RocResult<(RocList, RocList), ()> + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn list_chunks_of() { + assert_evals_to!( + "List.chunksOf [1, 2, 3, 4, 5, 6, 7, 8] 3", + RocList::>::from_slice(&[ + RocList::from_slice(&[1, 2, 3]), + RocList::from_slice(&[4, 5, 6]), + RocList::from_slice(&[7, 8]), + ]), + RocList> + ); + + assert_evals_to!( + "List.chunksOf [1, 2, 3, 4] 5", + RocList::>::from_slice(&[RocList::from_slice(&[1, 2, 3, 4]),]), + RocList> + ); + + assert_evals_to!( + "List.chunksOf [1, 2, 3] 0", + RocList::>::from_slice(&[]), + RocList> + ); + + assert_evals_to!( + "List.chunksOf [] 5", + RocList::>::from_slice(&[]), + RocList> + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_drop_first() { + assert_evals_to!( + "List.dropFirst [1,2,3] 2", + RocList::from_slice(&[3]), + RocList + ); + assert_evals_to!( + "List.dropFirst [] 1", + RocList::::from_slice(&[]), + RocList + ); + assert_evals_to!( + "List.dropFirst [1,2] 5", + RocList::::from_slice(&[]), + RocList + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_drop_at() { + assert_evals_to!( + "List.dropAt [1, 2, 3] 0", + RocList::from_slice(&[2, 3]), + RocList + ); + assert_evals_to!( + "List.dropAt [0, 0, 0] 3", + RocList::from_slice(&[0, 0, 0]), + RocList + ); + assert_evals_to!( + "List.dropAt [] 1", + RocList::::from_slice(&[]), + RocList + ); + assert_evals_to!( + "List.dropAt [0] 0", + RocList::::from_slice(&[]), + RocList + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_intersperse() { + assert_evals_to!( + indoc!( + r#" + List.intersperse [0, 0, 0] 1 + "# + ), + RocList::from_slice(&[0, 1, 0, 1, 0]), + RocList + ); + assert_evals_to!( + indoc!( + r#" + List.intersperse [] 1 + "# + ), + RocList::::from_slice(&[]), + RocList + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_drop_at_shared() { + assert_evals_to!( + indoc!( + r#" + list : List I64 + list = [if Bool.true then 4 else 4, 5, 6] + + { newList: List.dropAt list 0, original: list } + "# + ), + ( + // new_list + RocList::from_slice(&[5, 6]), + // original + RocList::from_slice(&[4, 5, 6]), + ), + (RocList, RocList,) + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_drop_if_empty_list_of_int() { + assert_evals_to!( + indoc!( + r#" + empty : List I64 + empty = [] + + List.dropIf empty \_ -> Bool.true + "# + ), + RocList::::from_slice(&[]), + RocList + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_drop_if_empty_list() { + assert_evals_to!( + indoc!( + r#" + alwaysTrue : I64 -> Bool + alwaysTrue = \_ -> Bool.true + + List.dropIf [] alwaysTrue + "# + ), + RocList::::from_slice(&[]), + RocList + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_drop_if_always_false_for_non_empty_list() { + assert_evals_to!( + indoc!( + r#" + List.dropIf [1,2,3,4,5,6,7,8] (\_ -> Bool.false) + "# + ), + RocList::from_slice(&[1, 2, 3, 4, 5, 6, 7, 8]), + RocList + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_drop_if_always_true_for_non_empty_list() { + assert_evals_to!( + indoc!( + r#" + List.dropIf [1,2,3,4,5,6,7,8] (\_ -> Bool.true) + "# + ), + RocList::::from_slice(&[]), + RocList + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_drop_if_geq3() { + assert_evals_to!( + indoc!( + r#" + List.dropIf [1,2,3,4,5,6,7,8] (\n -> n >= 3) + "# + ), + RocList::from_slice(&[1, 2]), + RocList + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_drop_if_string_eq() { + assert_evals_to!( + indoc!( + r#" + List.dropIf ["x", "y", "x"] (\s -> s == "y") + "# + ), + RocList::from_slice(&[RocStr::from("x"), RocStr::from("x")]), + RocList + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_drop_last() { + assert_evals_to!( + "List.dropLast [1, 2, 3] 1", + RocList::from_slice(&[1, 2]), + RocList + ); + assert_evals_to!( + "List.dropLast [] 5", + RocList::::from_slice(&[]), + RocList + ); + assert_evals_to!( + "List.dropLast [0] 0", + RocList::::from_slice(&[0]), + RocList + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_drop_last_mutable() { + assert_evals_to!( + indoc!( + r#" + list : List I64 + list = [if Bool.true then 4 else 4, 5, 6] + + { newList: List.dropLast list 1, original: list } + "# + ), + ( + // new_list + RocList::from_slice(&[4, 5]), + // original + RocList::from_slice(&[4, 5, 6]), + ), + (RocList, RocList,) + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_swap() { + assert_evals_to!( + "List.swap [] 0 1", + RocList::::from_slice(&[]), + RocList + ); + assert_evals_to!("List.swap [0] 1 2", RocList::from_slice(&[0]), RocList); + assert_evals_to!( + "List.swap [1, 2] 0 1", + RocList::from_slice(&[2, 1]), + RocList + ); + assert_evals_to!( + "List.swap [1, 2] 1 0", + RocList::from_slice(&[2, 1]), + RocList + ); + assert_evals_to!( + "List.swap [0, 1, 2, 3, 4, 5] 2 4", + RocList::from_slice(&[0, 1, 4, 3, 2, 5]), + RocList + ); + assert_evals_to!( + "List.swap [0, 1, 2] 1 3", + RocList::from_slice(&[0, 1, 2]), + RocList + ); + assert_evals_to!( + "List.swap [1, 2, 3] 1 1", + RocList::from_slice(&[1, 2, 3]), + RocList + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_append_to_empty_list() { + assert_evals_to!("List.append [] 3", RocList::from_slice(&[3]), RocList); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_append_to_empty_list_of_int() { + assert_evals_to!( + indoc!( + r#" + initThrees : List I64 + initThrees = + [] + + List.append (List.append initThrees 3) 3 + "# + ), + RocList::from_slice(&[3, 3]), + RocList + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_append_bools() { + assert_evals_to!( + "List.append [Bool.true, Bool.false] Bool.true", + RocList::from_slice(&[true, false, true]), + RocList + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_append_longer_list() { + assert_evals_to!( + "List.append [11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22] 23", + RocList::from_slice(&[11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23]), + RocList + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_prepend() { + assert_evals_to!("List.prepend [] 1", RocList::from_slice(&[1]), RocList); + assert_evals_to!( + "List.prepend [2] 1", + RocList::from_slice(&[1, 2]), + RocList + ); + + assert_evals_to!( + indoc!( + r#" + init : List I64 + init = + [] + + List.prepend (List.prepend init 4) 6 + "# + ), + RocList::from_slice(&[6, 4]), + RocList + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_prepend_str() { + assert_evals_to!( + indoc!( + r#" + init : List Str + init = + ["foo"] + + List.prepend init "bar" + "# + ), + RocList::from_slice(&[RocStr::from("bar"), RocStr::from("foo"),]), + RocList + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_prepend_bools() { + assert_evals_to!( + "List.prepend [Bool.true, Bool.false] Bool.true", + RocList::from_slice(&[true, true, false]), + RocList + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_prepend_big_list() { + assert_evals_to!( + "List.prepend [10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 100, 100, 100, 100] 9", + RocList::from_slice(&[ + 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 100, 100, 100, 100 + ]), + RocList + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_walk_backwards_empty_all_inline() { + assert_evals_to!( + indoc!( + r#" + List.walkBackwards [0x1] 0 \state, elem -> state + elem + "# + ), + 1, + i64 + ); + + assert_evals_to!( + indoc!( + r#" + empty : List I64 + empty = + [] + + List.walkBackwards empty 0 \state, elem -> state + elem + "# + ), + 0, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_walk_backwards_with_str() { + assert_evals_to!( + r#"List.walkBackwards ["x", "y", "z"] "<" Str.concat"#, + RocStr::from(" + when b is + Zero -> { r & zeroes: r.zeroes + 1 } + One -> { r & ones: r.ones + 1 } + + finalCounts = List.walkBackwards byte initialCounts acc + + finalCounts.ones * 10 + finalCounts.zeroes + "# + ), + 35, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_walk_with_str() { + assert_evals_to!( + r#"List.walk ["x", "y", "z"] "<" Str.concat"#, + RocStr::from(" Continue (a + b)"#, + 3, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_walk_implements_position() { + assert_evals_to!( + r#" + Option a : [Some a, None] + + find : List a, a -> Option Nat where a implements Eq + find = \list, needle -> + findHelp list needle + |> .v + + findHelp = \list, needle -> + List.walkUntil list { n: 0, v: None } \{ n, v }, element -> + if element == needle then + Break { n, v: Some n } + else + Continue { n: n + 1, v } + + when find [1, 2, 3] 3 is + None -> 0 + Some v -> v + "#, + 2, + usize + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_walk_until_even_prefix_sum() { + assert_evals_to!( + r#" + helper = \a, b -> + if Num.isEven b then + Continue (a + b) + + else + Break a + + List.walkUntil [2, 4, 8, 9] 0 helper"#, + 2 + 4 + 8, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_walk_from_sum() { + assert_evals_to!(r#"List.walkFrom [1, 2, 3] 1 0 Num.add"#, 5, i64); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_keep_if_empty_list_of_int() { + assert_evals_to!( + indoc!( + r#" + empty : List I64 + empty = + [] + + List.keepIf empty \_ -> Bool.true + "# + ), + RocList::::from_slice(&[]), + RocList + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_keep_if_empty_list() { + assert_evals_to!( + indoc!( + r#" + alwaysTrue : I64 -> Bool + alwaysTrue = \_ -> + Bool.true + + + List.keepIf [] alwaysTrue + "# + ), + RocList::::from_slice(&[]), + RocList + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_keep_if_always_true_for_non_empty_list() { + assert_evals_to!( + indoc!( + r#" + alwaysTrue : I64 -> Bool + alwaysTrue = \_ -> + Bool.true + + oneThroughEight : List I64 + oneThroughEight = + [1,2,3,4,5,6,7,8] + + List.keepIf oneThroughEight alwaysTrue + "# + ), + RocList::from_slice(&[1, 2, 3, 4, 5, 6, 7, 8]), + RocList + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_keep_if_always_false_for_non_empty_list() { + assert_evals_to!( + indoc!( + r#" + alwaysFalse : I64 -> Bool + alwaysFalse = \_ -> + Bool.false + + List.keepIf [1,2,3,4,5,6,7,8] alwaysFalse + "# + ), + RocList::::from_slice(&[]), + RocList + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_keep_if_one() { + assert_evals_to!( + indoc!( + r#" + intIsLessThanThree : I64 -> Bool + intIsLessThanThree = \i -> + i < 3 + + List.keepIf [1,2,3,4,5,6,7,8] intIsLessThanThree + "# + ), + RocList::from_slice(&[1, 2]), + RocList + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_keep_if_str_is_hello() { + assert_evals_to!( + indoc!( + r#" + List.keepIf ["x", "y", "x"] (\x -> x == "x") + "# + ), + RocList::from_slice(&[RocStr::from("x"), RocStr::from("x")]), + RocList + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_count_if_empty_list() { + assert_evals_to!( + indoc!( + r#" + List.countIf [] \_ -> Bool.true + "# + ), + 0, + usize + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_count_if_always_true_for_non_empty_list() { + assert_evals_to!( + indoc!( + r#" + alwaysTrue : I64 -> Bool + alwaysTrue = \_ -> + Bool.true + + oneThroughEight : List I64 + oneThroughEight = + [1,2,3,4,5,6,7,8] + + List.countIf oneThroughEight alwaysTrue + "# + ), + 8, + usize + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_count_if_always_false_for_non_empty_list() { + assert_evals_to!( + indoc!( + r#" + alwaysFalse : I64 -> Bool + alwaysFalse = \_ -> + Bool.false + + List.countIf [1,2,3,4,5,6,7,8] alwaysFalse + "# + ), + 0, + usize + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_count_if_condition() { + assert_evals_to!( + indoc!( + r#" + intIsLessThanThree : I64 -> Bool + intIsLessThanThree = \i -> + i < 3 + + List.countIf [1,2,3,4,5,6,7,8] intIsLessThanThree + "# + ), + 2, + usize + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_count_if_str() { + assert_evals_to!( + indoc!( + r#" + List.countIf ["x", "y", "x"] (\x -> x == "x") + "# + ), + 2, + usize + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_map_on_empty_list_with_int_layout() { + assert_evals_to!( + indoc!( + r#" + empty : List I64 + empty = + [] + + List.map empty (\x -> x) + "# + ), + RocList::::from_slice(&[]), + RocList + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_map_on_non_empty_list() { + assert_evals_to!( + indoc!( + r#" + nonEmpty : List I64 + nonEmpty = + [1] + + List.map nonEmpty (\x -> x) + "# + ), + RocList::from_slice(&[1]), + RocList + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_map_changes_input() { + assert_evals_to!( + indoc!( + r#" + nonEmpty : List I64 + nonEmpty = + [1] + + List.map nonEmpty (\x -> x + 1) + "# + ), + RocList::from_slice(&[2]), + RocList + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_map_on_big_list() { + assert_evals_to!( + indoc!( + r#" + nonEmpty : List I64 + nonEmpty = + [1, 2, 3, 4, 5, 1, 2, 3, 4, 5, 1, 2, 3, 4, 5, 1, 2, 3, 4, 5, 1, 2, 3, 4, 5] + + List.map nonEmpty (\x -> x * 2) + "# + ), + RocList::from_slice(&[ + 2, 4, 6, 8, 10, 2, 4, 6, 8, 10, 2, 4, 6, 8, 10, 2, 4, 6, 8, 10, 2, 4, 6, 8, 10 + ]), + RocList + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_map_with_type_change() { + assert_evals_to!( + indoc!( + r#" + nonEmpty : List I64 + nonEmpty = + [1, 1, -4, 1, 2] + + + List.map nonEmpty (\x -> x > 0) + "# + ), + RocList::from_slice(&[true, true, false, true, true]), + RocList + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_map_using_defined_function() { + assert_evals_to!( + indoc!( + r#" + nonEmpty : List I64 + nonEmpty = + [2, 2, -4, 2, 3] + + greaterThanOne : I64 -> Bool + greaterThanOne = \i -> + i > 1 + + List.map nonEmpty greaterThanOne + "# + ), + RocList::from_slice(&[true, true, false, true, true]), + RocList + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_map_all_inline() { + assert_evals_to!( + indoc!( + r#" + List.map [] (\x -> x > 0) + "# + ), + RocList::::from_slice(&[]), + RocList + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_map_closure_int() { + assert_evals_to!( + indoc!( + r#" + int : I64 + int = 123 + + single : List I64 + single = + [0] + + List.map single (\x -> x + int) + "# + ), + RocList::from_slice(&[123]), + RocList + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_map_closure_float() { + assert_evals_to!( + indoc!( + r#" + float : F64 + float = 1.23 + + single : List F64 + single = + [0] + + List.map single (\x -> x + float) + "# + ), + RocList::from_slice(&[1.23]), + RocList + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_map_closure_string() { + assert_evals_to!( + indoc!( + r#" + one : Str + one = "one " + + List.map ["pear", "apple"] (\x -> Str.concat one x) + "# + ), + RocList::from_slice(&[RocStr::from("one pear"), RocStr::from("one apple")]), + RocList + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_map4_group() { + assert_evals_to!( + indoc!( + r#" + List.map4 [1,2,3] [3,2,1] [2,1,3] [3,1,2] (\a, b, c, d -> Group a b c d) + "# + ), + RocList::from_slice(&[[1, 3, 2, 3], [2, 2, 1, 1], [3, 1, 3, 2]]), + RocList<[i64; 4]> + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_map4_different_length() { + assert_evals_to!( + indoc!( + r#" + List.map4 + ["h", "i", "j", "k"] + ["o", "p", "q"] + ["l", "m"] + ["a"] + (\a, b, c, d -> Str.concat a (Str.concat b (Str.concat c d))) + "# + ), + RocList::from_slice(&[RocStr::from("hola"),]), + RocList + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_map3_group() { + assert_evals_to!( + indoc!( + r#" + List.map3 [1,2,3] [3,2,1] [2,1,3] (\a, b, c -> Group a b c) + "# + ), + RocList::from_slice(&[(1, 3, 2), (2, 2, 1), (3, 1, 3)]), + RocList<(i64, i64, i64)> + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_map3_different_length() { + assert_evals_to!( + indoc!( + r#" + List.map3 + ["a", "b", "d"] + ["b", "x"] + ["c"] + (\a, b, c -> Str.concat a (Str.concat b c)) + "# + ), + RocList::from_slice(&[RocStr::from("abc"),]), + RocList + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_map2_pair() { + assert_evals_to!( + indoc!( + r#" + f = (\a,b -> Pair a b) + List.map2 [1,2,3] [3,2,1] f + "# + ), + RocList::from_slice(&[(1, 3), (2, 2), (3, 1)]), + RocList<(i64, i64)> + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_map2_different_lengths() { + assert_evals_to!( + indoc!( + r#" + List.map2 + ["a", "b", "lllllllllllllooooooooongnggg"] + ["b"] + (\a, b -> Str.concat a b) + "# + ), + RocList::from_slice(&[RocStr::from("ab"),]), + RocList + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_join_empty_list() { + assert_evals_to!( + "List.join []", + RocList::::from_slice(&[]), + RocList + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_join_one_list() { + assert_evals_to!( + indoc!("List.walk [[1, 2, 3]] [] List.concat",), + RocList::from_slice(&[1, 2, 3]), + RocList + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_join_two_non_empty_lists() { + assert_evals_to!( + "List.join [[1, 2, 3] , [4 ,5, 6]]", + RocList::from_slice(&[1, 2, 3, 4, 5, 6]), + RocList + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_join_two_non_empty_lists_of_float() { + assert_evals_to!( + "List.join [[1.2f64, 1.1], [2.1, 2.2]]", + RocList::from_slice(&[1.2, 1.1, 2.1, 2.2]), + RocList + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_join_to_big_list() { + assert_evals_to!( + indoc!( + r#" + List.join + [ + [1.2f64, 1.1], + [2.1, 2.2], + [3.0, 4.0, 5.0, 6.1, 9.0], + [3.0, 4.0, 5.0, 6.1, 9.0], + [3.0, 4.0, 5.0, 6.1, 9.0], + [3.0, 4.0, 5.0, 6.1, 9.0], + [3.0, 4.0, 5.0, 6.1, 9.0] + ] + "# + ), + RocList::from_slice(&[ + 1.2, 1.1, 2.1, 2.2, 3.0, 4.0, 5.0, 6.1, 9.0, 3.0, 4.0, 5.0, 6.1, 9.0, 3.0, 4.0, 5.0, + 6.1, 9.0, 3.0, 4.0, 5.0, 6.1, 9.0, 3.0, 4.0, 5.0, 6.1, 9.0 + ]), + RocList + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_join_defined_empty_list() { + assert_evals_to!( + indoc!( + r#" + empty : List F64 + empty = + [] + + List.join [[0.2, 11.11], empty] + "# + ), + RocList::from_slice(&[0.2, 11.11]), + RocList + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_join_all_empty_lists() { + assert_evals_to!( + "List.join [[], [], []]", + RocList::::from_slice(&[]), + RocList + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_join_one_empty_list() { + assert_evals_to!( + "List.join [[1.2f64, 1.1], []]", + RocList::from_slice(&[1.2, 1.1]), + RocList + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_single() { + assert_evals_to!("List.single 1", RocList::from_slice(&[1]), RocList); + assert_evals_to!( + "List.single 5.6f32", + RocList::from_slice(&[5.6]), + RocList + ); + assert_evals_to!( + "List.single 5.6f64", + RocList::from_slice(&[5.6]), + RocList + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_repeat() { + assert_evals_to!( + "List.repeat 1 5", + RocList::from_slice(&[1, 1, 1, 1, 1]), + RocList + ); + assert_evals_to!( + "List.repeat 2 4", + RocList::from_slice(&[2, 2, 2, 2]), + RocList + ); + + assert_evals_to!( + "List.repeat [] 2", + RocList::from_slice(&[RocList::::default(), RocList::default()]), + RocList> + ); + + assert_evals_to!( + indoc!( + r#" + noStrs : List Str + noStrs = + [] + + List.repeat noStrs 2 + "# + ), + RocList::from_slice(&[RocList::::default(), RocList::default()]), + RocList> + ); + + assert_evals_to!( + "List.repeat 4 15", + RocList::from_slice(&[4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4]), + RocList + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_reverse() { + assert_evals_to!( + "List.reverse [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]", + RocList::from_slice(&[12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1]), + RocList + ); + assert_evals_to!( + "List.reverse [1, 2, 3]", + RocList::from_slice(&[3, 2, 1]), + RocList + ); + assert_evals_to!("List.reverse [4]", RocList::from_slice(&[4]), RocList); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_reverse_empty_list_of_int() { + assert_evals_to!( + indoc!( + r#" + emptyList : List I64 + emptyList = + [] + + List.reverse emptyList + "# + ), + RocList::::from_slice(&[]), + RocList + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_reverse_empty_list() { + assert_evals_to!( + "List.reverse []", + RocList::::from_slice(&[]), + RocList + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_concat_two_empty_lists() { + assert_evals_to!( + "List.concat [] []", + RocList::::from_slice(&[]), + RocList + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_concat_two_empty_lists_of_int() { + assert_evals_to!( + indoc!( + r#" + firstList : List I64 + firstList = + [] + + secondList : List I64 + secondList = + [] + + List.concat firstList secondList + "# + ), + RocList::::from_slice(&[]), + RocList + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_concat_second_list_is_empty() { + assert_evals_to!( + "List.concat [12, 13] []", + RocList::from_slice(&[12, 13]), + RocList + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_concat_first_list_is_empty() { + assert_evals_to!( + "List.concat [] [23, 24]", + RocList::from_slice(&[23, 24]), + RocList + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_concat_two_non_empty_lists() { + assert_evals_to!( + "List.concat [1, 2] [3, 4]", + RocList::from_slice(&[1, 2, 3, 4]), + RocList + ); + + assert_evals_to!( + "List.concat [34, 43] [64, 55, 66]", + RocList::from_slice(&[34, 43, 64, 55, 66]), + RocList + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_concat_two_bigger_non_empty_lists() { + assert_evals_to!( + "List.concat [1.1f64, 2.2] [3.3, 4.4, 5.5]", + RocList::from_slice(&[1.1f64, 2.2, 3.3, 4.4, 5.5]), + RocList + ); +} + +#[allow(dead_code)] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn assert_concat_worked(num_elems1: i64, num_elems2: i64) { + let vec1: Vec = (0..num_elems1) + .map(|i| 12345 % (i + num_elems1 + num_elems2 + 1)) + .collect(); + let vec2: Vec = (0..num_elems2) + .map(|i| 54321 % (i + num_elems1 + num_elems2 + 1)) + .collect(); + let slice_str1 = format!("{vec1:?}"); + let slice_str2 = format!("{vec2:?}"); + let mut expected = vec1; + + expected.extend(vec2); + + assert_evals_to!( + &format!("List.concat {slice_str1} {slice_str2}"), + RocList::from_slice(&expected), + RocList + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_concat_empty_list() { + assert_concat_worked(0, 0); + assert_concat_worked(1, 0); + assert_concat_worked(2, 0); + assert_concat_worked(3, 0); + assert_concat_worked(4, 0); + assert_concat_worked(7, 0); + assert_concat_worked(8, 0); + assert_concat_worked(9, 0); + assert_concat_worked(25, 0); + assert_concat_worked(150, 0); + assert_concat_worked(0, 1); + assert_concat_worked(0, 2); + assert_concat_worked(0, 3); + assert_concat_worked(0, 4); + assert_concat_worked(0, 7); + assert_concat_worked(0, 8); + assert_concat_worked(0, 9); + assert_concat_worked(0, 25); + assert_concat_worked(0, 150); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_concat_nonempty_lists() { + assert_concat_worked(1, 1); + assert_concat_worked(1, 2); + assert_concat_worked(1, 3); + assert_concat_worked(2, 3); + assert_concat_worked(2, 1); + assert_concat_worked(2, 2); + assert_concat_worked(3, 1); + assert_concat_worked(3, 2); + assert_concat_worked(2, 3); + assert_concat_worked(3, 3); + assert_concat_worked(4, 4); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_concat_large() { + with_larger_debug_stack(|| { + // these values produce mono ASTs so large that + // it can cause a stack overflow. This has been solved + // for current code, but may become a problem again in the future. + assert_concat_worked(150, 150); + assert_concat_worked(129, 350); + assert_concat_worked(350, 129); + }) +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn empty_list_len() { + assert_evals_to!("List.len []", 0, usize); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn basic_int_list_len() { + assert_evals_to!("List.len [12, 9, 6, 3]", 4, usize); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn loaded_int_list_len() { + assert_evals_to!( + indoc!( + r#" + nums = [2, 4, 6] + + List.len nums + "# + ), + 3, + usize + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn fn_int_list_len() { + assert_evals_to!( + indoc!( + r#" + getLen = \list -> List.len list + + nums = [2, 4, 6, 8] + + getLen nums + "# + ), + 4, + usize + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn int_list_is_empty() { + assert_evals_to!("List.isEmpty [12, 9, 6, 3]", false, bool); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn empty_list_is_empty() { + assert_evals_to!("List.isEmpty []", true, bool); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn first_int_list() { + assert_evals_to!( + indoc!( + r#" + List.first [12, 9, 6, 3] + "# + ), + RocResult::ok(12), + RocResult + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn first_str_list() { + assert_evals_to!( + indoc!( + r#" + List.first ["short", "bar"] + "# + ), + RocResult::ok(RocStr::from("short")), + RocResult + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn first_wildcard_empty_list() { + assert_evals_to!( + indoc!( + r#" + List.last [] |> Result.map (\_ -> 0i64) + "# + ), + RocResult::err(()), + RocResult + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn first_empty_list() { + assert_evals_to!( + indoc!( + r#" + list : List I64 + list = [] + + List.first list + "# + ), + RocResult::err(()), + RocResult + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn last_int_list() { + assert_evals_to!( + indoc!( + r#" + List.last [12, 9, 6, 3] + "# + ), + RocResult::ok(3), + RocResult + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn last_wildcard_empty_list() { + assert_evals_to!( + indoc!( + r#" + List.last [] |> Result.map (\_ -> 0i64) + "# + ), + RocResult::err(()), + RocResult + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn last_empty_list() { + assert_evals_to!( + indoc!( + r#" + list : List I64 + list = [] + + List.last list + "# + ), + RocResult::err(()), + RocResult + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn get_empty_list() { + assert_evals_to!( + indoc!( + r#" + list : List I64 + list = [] + + List.get list 0 + "# + ), + RocResult::err(()), + RocResult + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn get_wildcard_empty_list() { + // NOTE: by default, the return type is `Result [] [NotFound]`, which is actually represented + // as just `[NotFound]`. Casting that to `RocResult<(), ()>` is invalid! But accepting any `()` + // would make the test pointless. Therefore, we must explicitly change the type on the roc side + assert_evals_to!( + indoc!( + r#" + List.get [] 0 + |> Result.map (\_ -> {}) + "# + ), + RocResult::err(()), + RocResult<(), ()> + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn get_str_list_ok() { + assert_evals_to!( + indoc!( + r#" + List.get ["foo", "bar"] 1 + "# + ), + RocResult::ok(RocStr::from("bar")), + RocResult + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn get_int_list_ok() { + assert_evals_to!( + indoc!( + r#" + List.get [12, 9, 6] 1 + "# + ), + RocResult::ok(9), + RocResult + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn get_int_list_oob() { + assert_evals_to!( + indoc!( + r#" + List.get [12, 9, 6] 1000 + "# + ), + RocResult::err(()), + RocResult + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn replace_unique_int_list() { + assert_evals_to!( + indoc!( + r#" + record = List.replace [12, 9, 7, 1, 5] 2 33 + record.list + "# + ), + RocList::from_slice(&[12, 9, 33, 1, 5]), + RocList + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn replace_unique_int_list_out_of_bounds() { + assert_evals_to!( + indoc!( + r#" + record = List.replace [12, 9, 7, 1, 5] 5 33 + record.value + "# + ), + 33, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn replace_unique_int_list_get_old_value() { + assert_evals_to!( + indoc!( + r#" + record = List.replace [12, 9, 7, 1, 5] 2 33 + record.value + "# + ), + 7, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn replace_unique_get_large_value() { + assert_evals_to!( + indoc!( + r#" + list : List { a : U64, b: U64, c: U64, d: U64 } + list = [{ a: 1, b: 2, c: 3, d: 4 }, { a: 5, b: 6, c: 7, d: 8 }, { a: 9, b: 10, c: 11, d: 12 }] + record = List.replace list 1 { a: 13, b: 14, c: 15, d: 16 } + record.value + "# + ), + [5, 6, 7, 8], + [u64; 4] + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn replace_shared_int_list() { + assert_evals_to!( + indoc!( + r#" + wrapper = \shared -> + # This should not mutate the original + replaced = (List.replace shared 1 7.7).list + x = + when List.get replaced 1 is + Ok num -> num + Err _ -> 0 + + y = + when List.get shared 1 is + Ok num -> num + Err _ -> 0 + + { x, y } + + wrapper [2.1f64, 4.3] + "# + ), + (7.7, 4.3), + (f64, f64) + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn get_set_unique_int_list_i64() { + assert_evals_to!( + indoc!( + r#" + List.get (List.set [12, 9, 7, 3] 1 42) 1 + "# + ), + RocResult::ok(42), + RocResult + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn get_set_unique_int_list_i8() { + assert_evals_to!( + indoc!( + r#" + List.get (List.set [12, 9, 7, 3] 1 42i8) 1 + "# + ), + RocResult::ok(42), + RocResult + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn set_unique_int_list() { + assert_evals_to!( + "List.set [12, 9, 7, 1, 5] 2 33", + RocList::from_slice(&[12, 9, 33, 1, 5]), + RocList + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn set_unique_list_oob() { + assert_evals_to!( + "List.set [3f64, 17, 4.1] 1337 9.25", + RocList::from_slice(&[3.0, 17.0, 4.1]), + RocList + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn set_shared_int_list() { + assert_evals_to!( + indoc!( + r#" + wrapper = \shared -> + # This should not mutate the original + x = + when List.get (List.set shared 1 7.7) 1 is + Ok num -> num + Err _ -> 0 + + y = + when List.get shared 1 is + Ok num -> num + Err _ -> 0 + + { x, y } + + wrapper [2.1f64, 4.3] + "# + ), + (7.7, 4.3), + (f64, f64) + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn set_shared_list_oob() { + assert_evals_to!( + indoc!( + r#" + shared = [2, 4] + + # This List.set is out of bounds, and should have no effect + x = + when List.get (List.set shared 422 0) 1 is + Ok num -> num + Err _ -> 0 + + y = + when List.get shared 1 is + Ok num -> num + Err _ -> 0 + + { x, y } + "# + ), + (4, 4), + (i64, i64) + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn get_unique_int_list() { + assert_evals_to!( + indoc!( + r#" + unique = [2, 4] + + List.get unique 1 + "# + ), + RocResult::ok(4), + RocResult + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn gen_wrap_len() { + assert_evals_to!( + indoc!( + r#" + wrapLen = \list -> + [List.len list] + + wrapLen [1, 7, 9] + "# + ), + RocList::from_slice(&[3]), + RocList + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn gen_wrap_first() { + assert_evals_to!( + indoc!( + r#" + wrapFirst = \list -> + [List.first list] + + wrapFirst [1, 2] + "# + ), + RocList::from_slice(&[1]), + RocList + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn gen_duplicate() { + assert_evals_to!( + indoc!( + r#" + # Duplicate the first element into the second index + dupe = \list -> + when List.first list is + Ok elem -> + List.set list 1 elem + + _ -> + [] + + dupe [1, 2] + "# + ), + RocList::from_slice(&[1, 1]), + RocList + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn gen_swap() { + assert_evals_to!( + indoc!( + r#" + app "quicksort" provides [main] to "./platform" + + + swap : Nat, Nat, List a -> List a + swap = \i, j, list -> + when Pair (List.get list i) (List.get list j) is + Pair (Ok atI) (Ok atJ) -> + list + |> List.set i atJ + |> List.set j atI + + _ -> + [] + + main = + swap 0 1 [1, 2] + "# + ), + RocList::from_slice(&[2, 1]), + RocList + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn gen_quicksort() { + with_larger_debug_stack(|| { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + quicksort : List (Num a) -> List (Num a) + quicksort = \list -> + n = List.len list + quicksortHelp list 0 (n - 1) + + + quicksortHelp : List (Num a), Nat, Nat -> List (Num a) + quicksortHelp = \list, low, high -> + if low < high then + when partition low high list is + Pair partitionIndex partitioned -> + partitioned + |> quicksortHelp low (Num.subSaturated partitionIndex 1) + |> quicksortHelp (partitionIndex + 1) high + else + list + + + swap : Nat, Nat, List a -> List a + swap = \i, j, list -> + when Pair (List.get list i) (List.get list j) is + Pair (Ok atI) (Ok atJ) -> + list + |> List.set i atJ + |> List.set j atI + + _ -> + [] + + partition : Nat, Nat, List (Num a) -> [Pair Nat (List (Num a))] + partition = \low, high, initialList -> + when List.get initialList high is + Ok pivot -> + when partitionHelp low low initialList high pivot is + Pair newI newList -> + Pair newI (swap newI high newList) + + Err _ -> + Pair low initialList + + + partitionHelp : Nat, Nat, List (Num a), Nat, (Num a) -> [Pair Nat (List (Num a))] + partitionHelp = \i, j, list, high, pivot -> + if j < high then + when List.get list j is + Ok value -> + if value <= pivot then + partitionHelp (i + 1) (j + 1) (swap i j list) high pivot + else + partitionHelp i (j + 1) list high pivot + + Err _ -> + Pair i list + else + Pair i list + + main = quicksort [7, 4, 21, 19] + "# + ), + RocList::from_slice(&[4, 7, 19, 21]), + RocList + ); + }) +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn quicksort() { + with_larger_debug_stack(|| { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + quicksort : List (Num a) -> List (Num a) + quicksort = \list -> + quicksortHelp list 0 (List.len list - 1) + + + quicksortHelp : List (Num a), Nat, Nat -> List (Num a) + quicksortHelp = \list, low, high -> + if low < high then + when partition low high list is + Pair partitionIndex partitioned -> + partitioned + |> quicksortHelp low (Num.subSaturated partitionIndex 1) + |> quicksortHelp (partitionIndex + 1) high + else + list + + + swap : Nat, Nat, List a -> List a + swap = \i, j, list -> + when Pair (List.get list i) (List.get list j) is + Pair (Ok atI) (Ok atJ) -> + list + |> List.set i atJ + |> List.set j atI + + _ -> + [] + + partition : Nat, Nat, List (Num a) -> [Pair Nat (List (Num a))] + partition = \low, high, initialList -> + when List.get initialList high is + Ok pivot -> + when partitionHelp low low initialList high pivot is + Pair newI newList -> + Pair newI (swap newI high newList) + + Err _ -> + Pair low initialList + + + partitionHelp : Nat, Nat, List (Num a), Nat, Num a -> [Pair Nat (List (Num a))] + partitionHelp = \i, j, list, high, pivot -> + # if j < high then + if Bool.false then + when List.get list j is + Ok value -> + if value <= pivot then + partitionHelp (i + 1) (j + 1) (swap i j list) high pivot + else + partitionHelp i (j + 1) list high pivot + + Err _ -> + Pair i list + else + Pair i list + + + + main = quicksort [7, 4, 21, 19] + "# + ), + RocList::from_slice(&[19, 7, 4, 21]), + RocList + ); + }) +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn quicksort_singleton() { + with_larger_debug_stack(|| { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + quicksort : List (Num a) -> List (Num a) + quicksort = \list -> + quicksortHelp list 0 (List.len list - 1) + + + quicksortHelp : List (Num a), Nat, Nat -> List (Num a) + quicksortHelp = \list, low, high -> + if low < high then + when partition low high list is + Pair partitionIndex partitioned -> + partitioned + |> quicksortHelp low (Num.subSaturated partitionIndex 1) + |> quicksortHelp (partitionIndex + 1) high + else + list + + + swap : Nat, Nat, List a -> List a + swap = \i, j, list -> + when Pair (List.get list i) (List.get list j) is + Pair (Ok atI) (Ok atJ) -> + list + |> List.set i atJ + |> List.set j atI + + _ -> + [] + + partition : Nat, Nat, List (Num a) -> [Pair Nat (List (Num a))] + partition = \low, high, initialList -> + when List.get initialList high is + Ok pivot -> + when partitionHelp low low initialList high pivot is + Pair newI newList -> + Pair newI (swap newI high newList) + + Err _ -> + Pair low initialList + + + partitionHelp : Nat, Nat, List (Num a), Nat, Num a -> [Pair Nat (List (Num a))] + partitionHelp = \i, j, list, high, pivot -> + if j < high then + when List.get list j is + Ok value -> + if value <= pivot then + partitionHelp (i + 1) (j + 1) (swap i j list) high pivot + else + partitionHelp i (j + 1) list high pivot + + Err _ -> + Pair i list + else + Pair i list + + + + main = + when List.first (quicksort [0x1]) is + _ -> 4 + "# + ), + 4, + i64 + ); + }) +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn empty_list_increment_decrement() { + assert_evals_to!( + indoc!( + r#" + x : List I64 + x = [] + + List.len x + List.len x + "# + ), + 0, + usize + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_literal_increment_decrement() { + assert_evals_to!( + indoc!( + r#" + x : List I64 + x = [1,2,3] + + List.len x + List.len x + "# + ), + 6, + usize + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_pass_to_function() { + assert_evals_to!( + indoc!( + r#" + x : List I64 + x = [1,2,3] + + id : List I64 -> List I64 + id = \y -> y + + id x + "# + ), + RocList::from_slice(&[1, 2, 3]), + RocList + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_pass_to_set() { + assert_evals_to!( + indoc!( + r#" + x : List I64 + x = [1,2,3] + + id : List I64 -> List I64 + id = \y -> List.set y 0 0 + + id x + "# + ), + RocList::from_slice(&[0, 2, 3]), + RocList + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_wrap_in_tag() { + assert_evals_to!( + indoc!( + r#" + id : List I64 -> [Pair (List I64) I64] + id = \y -> Pair y 4 + + when id [1,2,3] is + Pair v _ -> v + "# + ), + RocList::from_slice(&[1, 2, 3]), + RocList + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_contains_int() { + assert_evals_to!(indoc!("List.contains [1,2,3] 1"), true, bool); + + assert_evals_to!(indoc!("List.contains [1,2,3] 4"), false, bool); + + assert_evals_to!(indoc!("List.contains [] 4"), false, bool); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_contains_str() { + assert_evals_to!(indoc!(r#"List.contains ["foo", "bar"] "bar""#), true, bool); + + assert_evals_to!( + indoc!(r#"List.contains ["foo", "bar"] "spam""#), + false, + bool + ); + + assert_evals_to!(indoc!(r#"List.contains [] "spam""#), false, bool); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_manual_range() { + assert_evals_to!( + indoc!( + r#" + range : I64, I64, List I64-> List I64 + range = \low, high, accum -> + if low < high then + range (low + 1) high (List.append accum low) + else + accum + + range 0 5 [42] + "# + ), + RocList::from_slice(&[42, 0, 1, 2, 3, 4]), + RocList + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_min() { + assert_evals_to!( + indoc!( + r#" + List.min [] + |> Result.map (\_ -> {}) + "# + ), + RocResult::err(()), + RocResult<(), ()> + ); + + assert_evals_to!( + indoc!( + r#" + List.min [3, 1, 2] + "# + ), + RocResult::ok(1), + RocResult + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_max() { + assert_evals_to!( + indoc!( + r#" + List.max [] + |> Result.map (\_ -> {}) + "# + ), + RocResult::err(()), + RocResult<(), ()> + ); + + assert_evals_to!( + indoc!( + r#" + List.max [3, 1, 2] + "# + ), + RocResult::ok(3), + RocResult + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_sum() { + assert_evals_to!("List.sum []", 0, i64); + assert_evals_to!("List.sum [1, 2, 3]", 6, i64); + assert_evals_to!("List.sum [1.1f64, 2.2, 3.3]", 6.6, f64); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_sum_dec() { + assert_evals_to!("List.sum [1.0dec, 2.0]", RocDec::from(3), RocDec); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_product() { + assert_evals_to!("List.product []", 1, i64); + assert_evals_to!("List.product [1, 2, 3]", 6, i64); + assert_evals_to!("List.product [1.1f64, 2.2, 3.3]", 1.1 * 2.2 * 3.3, f64); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_keep_void() { + assert_evals_to!( + "List.keepOks [] (\\x -> x)", + RocList::from_slice(&[]), + RocList<()> + ); + + assert_evals_to!( + "List.keepErrs [] (\\x -> x)", + RocList::from_slice(&[]), + RocList<()> + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_keep_oks() { + assert_evals_to!( + "List.keepOks [Ok {}, Ok {}] (\\x -> x)", + RocList::from_slice(&[(), ()]), + RocList<()> + ); + assert_evals_to!( + "List.keepOks [1,2] (\\x -> Ok x)", + RocList::from_slice(&[1, 2]), + RocList + ); + assert_evals_to!( + "List.keepOks [1,2] (\\x -> Num.remChecked x 2)", + RocList::from_slice(&[1, 0]), + RocList + ); + assert_evals_to!( + "List.keepOks [Ok 1, Err 2] (\\x -> x)", + RocList::from_slice(&[1]), + RocList + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_keep_errs() { + assert_evals_to!( + "List.keepErrs [Err {}, Err {}] (\\x -> x)", + RocList::from_slice(&[(), ()]), + RocList<()> + ); + assert_evals_to!( + "List.keepErrs [1,2] (\\x -> Err x)", + RocList::from_slice(&[1, 2]), + RocList + ); + assert_evals_to!( + indoc!( + r#" + List.keepErrs [0,1,2] (\x -> Num.remChecked x 0 |> Result.mapErr (\_ -> 32)) + "# + ), + RocList::from_slice(&[32, 32, 32]), + RocList + ); + + assert_evals_to!( + "List.keepErrs [Ok 1, Err 2] (\\x -> x)", + RocList::from_slice(&[2]), + RocList + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_map_with_index() { + assert_evals_to!( + "List.mapWithIndex [0,0,0] (\\x, index -> Num.intCast index + x)", + RocList::from_slice(&[0, 1, 2]), + RocList + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +#[should_panic(expected = r#"Roc failed with message: "integer addition overflowed!"#)] +fn cleanup_because_exception() { + assert_evals_to!( + indoc!( + r#" + x = [1,2] + + five : I64 + five = 5 + + five + Num.maxI64 + 3 + (Num.intCast (List.len x)) + "# + ), + 9, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_sort_with() { + assert_evals_to!( + "List.sortWith [] Num.compare", + RocList::::from_slice(&[]), + RocList + ); + assert_evals_to!( + "List.sortWith [4,3,2,1] Num.compare", + RocList::from_slice(&[1, 2, 3, 4]), + RocList + ); + assert_evals_to!( + "List.sortWith [1,2,3,4] (\\a,b -> Num.compare b a)", + RocList::from_slice(&[4, 3, 2, 1]), + RocList + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_sort_asc() { + assert_evals_to!( + "List.sortAsc []", + RocList::::from_slice(&[]), + RocList + ); + assert_evals_to!( + "List.sortAsc [4,3,2,1]", + RocList::from_slice(&[1, 2, 3, 4]), + RocList + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_sort_desc() { + assert_evals_to!( + "List.sortDesc []", + RocList::::from_slice(&[]), + RocList + ); + assert_evals_to!( + "List.sortDesc [1,2,3,4]", + RocList::from_slice(&[4, 3, 2, 1]), + RocList + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_any() { + assert_evals_to!("List.any [] (\\e -> e > 3)", false, bool); + assert_evals_to!("List.any [1, 2, 3] (\\e -> e > 3)", false, bool); + assert_evals_to!("List.any [1, 2, 4] (\\e -> e > 3)", true, bool); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_any_empty_with_unknown_element_type() { + assert_evals_to!("List.any [] (\\_ -> Bool.true)", false, bool); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_all() { + assert_evals_to!("List.all [] (\\e -> e > 3)", true, bool); + assert_evals_to!("List.all [1, 2, 3] (\\e -> e > 3)", false, bool); + assert_evals_to!("List.all [1, 2, 4] (\\e -> e > 3)", false, bool); + assert_evals_to!("List.all [1, 2, 3] (\\e -> e >= 1)", true, bool); +} + +#[test] +#[cfg(feature = "gen-llvm")] +fn list_all_empty_with_unknown_element_type() { + assert_evals_to!("List.all [] (\\_ -> Bool.true)", true, bool); +} + +#[test] +// This doesn't work on Windows. If you make it return a `bool`, e.g. with `|> Str.isEmpty` at the end, +// then it works. We don't know what the problem is here! +#[cfg(all( + not(target_family = "windows"), + any(feature = "gen-llvm", feature = "gen-wasm") +))] +#[should_panic(expected = r#"Roc failed with message: "invalid ret_layout""#)] +fn lists_with_incompatible_type_param_in_if() { + assert_evals_to!( + indoc!( + r#" + list1 = [{}] + + list2 = [""] + + x = if Bool.true then list1 else list2 + + "" + "# + ), + RocStr::default(), + RocStr + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn map_with_index_multi_record() { + // see https://github.com/roc-lang/roc/issues/1700 + assert_evals_to!( + indoc!( + r#" + List.mapWithIndex [{ x: {}, y: {} }] \_, _ -> {} + "# + ), + RocList::from_slice(&[((), ())]), + RocList<((), ())> + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn empty_list_of_function_type() { + // see https://github.com/roc-lang/roc/issues/1732 + assert_evals_to!( + indoc!( + r#" + myList : List (Str -> Str) + myList = [] + + myClosure : Str -> Str + myClosure = \_ -> "bar" + + choose = + if Bool.false then + myList + else + [myClosure] + + when List.get choose 0 is + Ok f -> f "foo" + Err _ -> "bad!" + "# + ), + RocStr::from("bar"), + RocStr + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_join_map() { + assert_evals_to!( + indoc!( + r#" + List.joinMap ["guava,apple,pear", "bailey,cyrus"] (\s -> Str.split s ",") + "# + ), + RocList::from_slice(&[ + RocStr::from("guava"), + RocStr::from("apple"), + RocStr::from("pear"), + RocStr::from("bailey"), + RocStr::from("cyrus"), + ]), + RocList + ) +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_join_map_empty() { + assert_evals_to!( + indoc!( + r#" + List.joinMap [] (\s -> Str.split s ",") + "# + ), + RocList::from_slice(&[]), + RocList + ) +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_find() { + assert_evals_to!( + indoc!( + r#" + when List.findFirst ["a", "bc", "def", "g"] (\s -> Str.countGraphemes s > 1) is + Ok v -> v + Err _ -> "not found" + "# + ), + RocStr::from("bc"), + RocStr + ); + + assert_evals_to!( + indoc!( + r#" + when List.findLast ["a", "bc", "def", "g"] (\s -> Str.countGraphemes s > 1) is + Ok v -> v + Err _ -> "not found" + "# + ), + RocStr::from("def"), + RocStr + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_find_not_found() { + assert_evals_to!( + indoc!( + r#" + when List.findFirst ["a", "bc", "def", "g"] (\s -> Str.countGraphemes s > 5) is + Ok v -> v + Err _ -> "not found" + "# + ), + RocStr::from("not found"), + RocStr + ); + + assert_evals_to!( + indoc!( + r#" + when List.findLast ["a", "bc", "def", "g"] (\s -> Str.countGraphemes s > 5) is + Ok v -> v + Err _ -> "not found" + "# + ), + RocStr::from("not found"), + RocStr + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_find_empty_typed_list() { + assert_evals_to!( + indoc!( + r#" + when List.findFirst [] (\s -> Str.countGraphemes s > 5) is + Ok v -> v + Err _ -> "not found" + "# + ), + RocStr::from("not found"), + RocStr + ); + + assert_evals_to!( + indoc!( + r#" + when List.findLast [] (\s -> Str.countGraphemes s > 5) is + Ok v -> v + Err _ -> "not found" + "# + ), + RocStr::from("not found"), + RocStr + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_find_empty_layout() { + assert_evals_to!( + indoc!( + r#" + List.findFirst [] \_ -> Bool.true + "# + ), + // [Ok [], Err [NotFound]] gets unwrapped all the way to just [NotFound], + // which is the unit! + (), + () + ); + + assert_evals_to!( + indoc!( + r#" + List.findLast [] \_ -> Bool.true + "# + ), + // [Ok [], Err [NotFound]] gets unwrapped all the way to just [NotFound], + // which is the unit! + (), + () + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn list_find_index() { + assert_evals_to!( + indoc!( + r#" + when List.findFirstIndex ["a", "bc", "def", "g"] (\s -> Str.countGraphemes s > 1) is + Ok v -> v + Err _ -> 999 + "# + ), + 1, + usize + ); + + assert_evals_to!( + indoc!( + r#" + when List.findLastIndex ["a", "bc", "def", "g"] (\s -> Str.countGraphemes s > 1) is + Ok v -> v + Err _ -> 999 + "# + ), + 2, + usize + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_find_index_not_found() { + assert_evals_to!( + indoc!( + r#" + when List.findFirstIndex ["a", "bc", "def", "g"] (\s -> Str.countGraphemes s > 5) is + Ok v -> v + Err _ -> 999 + "# + ), + 999, + usize + ); + + assert_evals_to!( + indoc!( + r#" + when List.findLastIndex ["a", "bc", "def"] (\s -> Str.countGraphemes s > 5) is + Ok v -> v + Err _ -> 999 + "# + ), + 999, + usize + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_find_index_empty_typed_list() { + assert_evals_to!( + indoc!( + r#" + when List.findFirstIndex [] (\s -> Str.countGraphemes s > 5) is + Ok v -> v + Err _ -> 999 + "# + ), + 999, + usize + ); + + assert_evals_to!( + indoc!( + r#" + when List.findLastIndex [] (\s -> Str.countGraphemes s > 5) is + Ok v -> v + Err _ -> 999 + "# + ), + 999, + usize + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_ends_with_empty() { + assert_evals_to!( + indoc!( + r#" + List.endsWith [] [] + "# + ), + true, + bool + ); + + assert_evals_to!( + indoc!( + r#" + List.endsWith ["a"] [] + "# + ), + true, + bool + ); + + assert_evals_to!( + indoc!( + r#" + List.endsWith [] ["a"] + "# + ), + false, + bool + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_ends_with_nonempty() { + assert_evals_to!( + indoc!( + r#" + List.endsWith ["a", "bc", "def"] ["def"] + "# + ), + true, + bool + ); + + assert_evals_to!( + indoc!( + r#" + List.endsWith ["a", "bc", "def"] ["bc", "def"] + "# + ), + true, + bool + ); + + assert_evals_to!( + indoc!( + r#" + List.endsWith ["a", "bc", "def"] ["a"] + "# + ), + false, + bool + ); + + assert_evals_to!( + indoc!( + r#" + List.endsWith ["a", "bc", "def"] [""] + "# + ), + false, + bool + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_starts_with_empty() { + assert_evals_to!( + indoc!( + r#" + List.startsWith [] [] + "# + ), + true, + bool + ); + + assert_evals_to!( + indoc!( + r#" + List.startsWith ["a"] [] + "# + ), + true, + bool + ); + + assert_evals_to!( + indoc!( + r#" + List.startsWith [] ["a"] + "# + ), + false, + bool + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_starts_with_nonempty() { + assert_evals_to!( + indoc!( + r#" + List.startsWith ["a", "bc", "def"] ["a"] + "# + ), + true, + bool + ); + + assert_evals_to!( + indoc!( + r#" + List.startsWith ["a", "bc", "def"] ["a", "bc"] + "# + ), + true, + bool + ); + + assert_evals_to!( + indoc!( + r#" + List.startsWith ["a", "bc", "def"] ["def"] + "# + ), + false, + bool + ); + + assert_evals_to!( + indoc!( + r#" + List.startsWith ["a", "bc", "def"] [""] + "# + ), + false, + bool + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn monomorphized_lists() { + assert_evals_to!( + indoc!( + r#" + l = \{} -> [1, 2, 3] + + f : List U8, List U16 -> Nat + f = \_, _ -> 18 + + f (l {}) (l {}) + "# + ), + 18, + usize + ) +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn with_capacity() { + assert_evals_to!( + indoc!( + r#" + l : List U64 + l = List.withCapacity 10 + + l + "# + ), + // Equality check for RocList does not account for capacity + (10, RocList::with_capacity(10)), + RocList, + |value: RocList| (value.capacity(), value) + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn with_capacity_append() { + // see https://github.com/roc-lang/roc/issues/1732 + assert_evals_to!( + indoc!( + r#" + List.withCapacity 10 + |> List.append 0u64 + |> List.append 1u64 + |> List.append 2u64 + |> List.append 3u64 + "# + ), + (10, RocList::from_slice(&[0, 1, 2, 3])), + RocList, + |value: RocList| (value.capacity(), value) + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn reserve() { + assert_evals_to!( + indoc!( + r#" + List.reserve [] 15 + "# + ), + (15, RocList::empty()), + RocList, + |value: RocList| (value.capacity(), value) + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn reserve_unchanged() { + assert_evals_to!( + indoc!( + r#" + a = [] + b = List.reserve a 15 + {a, b} + "# + ), + // a's capacity is unchanged when we reserve 15 more capcity + // both lists are empty. + (0, RocList::empty(), 15, RocList::empty()), + (RocList, RocList), + |(value_a, value_b): (RocList, RocList)| ( + value_a.capacity(), + value_a, + value_b.capacity(), + value_b + ) + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn release_excess_capacity() { + assert_evals_to!( + indoc!( + r#" + List.reserve [] 15 + |> List.releaseExcessCapacity + "# + ), + (0, RocList::empty()), + RocList, + |value: RocList| (value.capacity(), value) + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn release_excess_capacity_with_len() { + assert_evals_to!( + indoc!( + r#" + List.reserve [1] 50 + |> List.releaseExcessCapacity + "# + ), + (1, RocList::from_slice(&[1])), + RocList, + |value: RocList| (value.capacity(), value) + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn release_excess_capacity_empty() { + assert_evals_to!( + indoc!( + r#" + List.releaseExcessCapacity [] + "# + ), + (0, RocList::empty()), + RocList, + |value: RocList| (value.capacity(), value) + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn call_function_in_empty_list() { + assert_evals_to!( + indoc!( + r#" + lst : List ({} -> {}) + lst = [] + List.map lst \f -> f {} + "# + ), + RocList::from_slice(&[]), + RocList<()> + ) +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn call_function_in_empty_list_unbound() { + assert_evals_to!( + indoc!( + r#" + lst = [] + List.map lst \f -> f {} + "# + ), + RocList::from_slice(&[]), + RocList<()> + ) +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn issue_3571_lowlevel_call_function_with_bool_lambda_set() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + apply : List (a -> b), List a -> List b + apply = \funs, vals -> + initial = List.withCapacity ((List.len funs) * (List.len vals)) + List.walk funs initial \state, fun -> + mappedVals = List.map vals fun + List.concat state mappedVals + + add2 : Str -> Str + add2 = \x -> "added \(x)" + + mul2 : Str -> Str + mul2 = \x -> "multiplied \(x)" + + foo = [add2, mul2] + bar = ["1", "2", "3", "4"] + + main = foo |> apply bar |> Str.joinWith ", " + "# + ), + RocStr::from("added 1, added 2, added 3, added 4, multiplied 1, multiplied 2, multiplied 3, multiplied 4"), + RocStr + ) +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn issue_3530_uninitialized_capacity_in_list_literal() { + assert_evals_to!( + indoc!( + r#" + [11,22,33] + "# + ), + 3, + (usize, usize, usize), + |(_, _, cap)| cap + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_infer_usage() { + assert_evals_to!( + indoc!( + r#" + empty : List _ + empty = [] + + xs : List Str + xs = List.append empty "foo" + + List.len xs + "# + ), + 1, + usize + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_walk_backwards_implements_position() { + assert_evals_to!( + r#" + Option a : [Some a, None] + + find : List a, a -> Option Nat where a implements Eq + find = \list, needle -> + findHelp list needle + |> .v + + findHelp = \list, needle -> + List.walkBackwardsUntil list { n: 0, v: None } \{ n, v }, element -> + if element == needle then + Break { n, v: Some n } + else + Continue { n: n + 1, v } + + when find [1, 2, 3] 3 is + None -> 0 + Some v -> v + "#, + 0, + usize + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_walk_backwards_until_sum() { + assert_evals_to!( + r#"List.walkBackwardsUntil [1, 2] 0 \a,b -> Continue (a + b)"#, + 3, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_walk_backwards_until_even_prefix_sum() { + assert_evals_to!( + r#" + helper = \a, b -> + if Num.isEven b then + Continue (a + b) + + else + Break a + + List.walkBackwardsUntil [9, 8, 4, 2] 0 helper"#, + 2 + 4 + 8, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_walk_from_until_sum() { + assert_evals_to!( + r#"List.walkFromUntil [1, 2, 3, 4] 2 0 \a,b -> Continue (a + b)"#, + 7, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn concat_unique_to_nonunique_overlapping_issue_4697() { + assert_evals_to!( + r#" + # originalList is shared, but others is unique. + # When we concat originalList with others, others should be re-used. + + originalList = [1u8] + others = [2u8, 3u8, 4u8] + new = List.concat originalList others + {a: originalList, b: new} + "#, + ( + RocList::from_slice(&[1u8]), + RocList::from_slice(&[1u8, 2, 3, 4]), + ), + (RocList, RocList) + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_walk_from_even_prefix_sum() { + assert_evals_to!( + r#" + helper = \a, b -> + if Num.isEven b then + Continue (a + b) + + else + Break a + + List.walkFromUntil [2, 4, 8, 9] 1 0 helper"#, + 4 + 8, + i64 + ); +} + +#[test] +#[cfg(feature = "gen-llvm")] +// TODO: update how roc decides whether or not to print `User crashed` or `Roc failed` such that this prints `Roc failed ...`` +#[should_panic( + expected = r#"User crash with message: "List.range: failed to generate enough elements to fill the range before overflowing the numeric type"# +)] +fn list_range_length_overflow() { + assert_evals_to!( + indoc!( + r#" + List.range {start: At 255u8, end: Length 2} + "# + ), + RocList::::default(), + RocList:: + ); +} + +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +mod pattern_match { + #[allow(unused_imports)] + use crate::helpers::with_larger_debug_stack; + + #[cfg(feature = "gen-llvm")] + use crate::helpers::llvm::assert_evals_to; + + #[cfg(feature = "gen-wasm")] + use crate::helpers::wasm::assert_evals_to; + + #[cfg(feature = "gen-dev")] + use crate::helpers::dev::assert_evals_to; + + use super::RocList; + + #[test] + fn unary_exact_size_match() { + assert_evals_to!( + r#" + helper = \l -> when l is + [] -> 1u8 + _ -> 2u8 + + [ helper [], helper [{}] ] + "#, + RocList::from_slice(&[1, 2]), + RocList + ) + } + + #[test] + fn many_exact_size_match() { + assert_evals_to!( + r#" + helper = \l -> when l is + [] -> 1u8 + [_] -> 2u8 + [_, _] -> 3u8 + [_, _, _] -> 4u8 + _ -> 5u8 + + [ helper [], helper [{}], helper [{}, {}], helper [{}, {}, {}], helper [{}, {}, {}, {}] ] + "#, + RocList::from_slice(&[1, 2, 3, 4, 5]), + RocList + ) + } + + #[test] + fn ranged_matches_head() { + with_larger_debug_stack(|| { + assert_evals_to!( + r#" + helper = \l -> when l is + [] -> 1u8 + [A] -> 2u8 + [A, A, ..] -> 3u8 + [A, B, ..] -> 4u8 + [B, ..] -> 5u8 + + [ + helper [], + helper [A], + helper [A, A], helper [A, A, A], helper [A, A, B], helper [A, A, B, A], + helper [A, B], helper [A, B, A], helper [A, B, B], helper [A, B, A, B], + helper [B], helper [B, A], helper [B, B], helper [B, A, B, B], + ] + "#, + RocList::from_slice(&[ + 1, // + 2, // + 3, 3, 3, 3, // + 4, 4, 4, 4, // + 5, 5, 5, 5, // + ]), + RocList + ) + }); + } + + #[test] + fn ranged_matches_tail() { + with_larger_debug_stack(|| { + assert_evals_to!( + r#" + helper = \l -> when l is + [] -> 1u8 + [A] -> 2u8 + [.., A, A] -> 3u8 + [.., B, A] -> 4u8 + [.., B] -> 5u8 + + [ + helper [], + helper [A], + helper [A, A], helper [A, A, A], helper [B, A, A], helper [A, B, A, A], + helper [B, A], helper [A, B, A], helper [B, B, A], helper [B, A, B, A], + helper [B], helper [A, B], helper [B, B], helper [B, A, B, B], + ] + "#, + RocList::from_slice(&[ + 1, // + 2, // + 3, 3, 3, 3, // + 4, 4, 4, 4, // + 5, 5, 5, 5, // + ]), + RocList + ) + }) + } + + #[test] + fn bind_variables() { + assert_evals_to!( + r#" + helper : List U16 -> U16 + helper = \l -> when l is + [] -> 1 + [x] -> x + [.., w, x, y, z] -> w * x * y * z + [x, y, ..] -> x * y + + [ + helper [], + helper [5], + helper [3, 5], helper [3, 5, 7], + helper [2, 3, 5, 7], helper [11, 2, 3, 5, 7], helper [13, 11, 2, 3, 5, 7], + ] + "#, + RocList::from_slice(&[ + 1, // + 5, // + 15, 15, // + 210, 210, 210, // + ]), + RocList + ) + } + + #[test] + fn order_list_size_tests_issue_4732() { + assert_evals_to!( + r#" + helper : List U8 -> U8 + helper = \l -> when l is + [1, ..] -> 1 + [2, 1, ..] -> 2 + [3, 2, 1, ..] -> 3 + [4, 3, 2, 1, ..] -> 4 + [4, 3, 2, ..] -> 5 + [4, 3, ..] -> 6 + [4, ..] -> 7 + _ -> 8 + + [ + helper [1], helper [1, 2], + + helper [2, 1], helper [2, 1, 3], + + helper [3, 2, 1], helper [3, 2, 1, 4], + + helper [4, 3, 2, 1], helper [4, 3, 2, 1, 5], + + helper [4, 3, 2], helper [4, 3, 2, 5], + + helper [4, 3], helper [4, 3, 5], + + helper [4], helper [4, 5], + + helper [], helper [7], + ] + "#, + RocList::from_slice(&[ + 1, 1, // + 2, 2, // + 3, 3, // + 4, 4, // + 5, 5, // + 6, 6, // + 7, 7, // + 8, 8, // + ]), + RocList + ) + } + + #[test] + fn rest_as() { + assert_evals_to!( + r#" + helper : List U8 -> U8 + helper = \l -> when l is + [1, .. as rest, 1] -> helper rest + [1, .. as rest] -> helper rest + [.. as rest, 1] -> helper rest + [first, .., last] | [first as last] -> first + last + [] -> 0 + [ + helper [1, 1, 1], + helper [2, 1], + helper [1, 1, 2, 4, 1], + helper [1, 1, 8, 7, 3, 1, 1, 1], + ] + "#, + RocList::from_slice(&[0, 4, 6, 11]), + RocList + ) + } +} diff --git a/crates/compiler/test_gen/src/gen_num.rs b/crates/compiler/test_gen/src/gen_num.rs new file mode 100644 index 0000000000..c6651b9416 --- /dev/null +++ b/crates/compiler/test_gen/src/gen_num.rs @@ -0,0 +1,4074 @@ +#[cfg(feature = "gen-llvm")] +use crate::helpers::llvm::assert_evals_to; + +#[cfg(feature = "gen-dev")] +use crate::helpers::dev::assert_evals_to; + +#[cfg(feature = "gen-wasm")] +use crate::helpers::wasm::assert_evals_to; + +// use crate::assert_wasm_evals_to as assert_evals_to; +#[allow(unused_imports)] +use indoc::indoc; +#[allow(unused_imports)] +use roc_std::{RocDec, RocOrder, RocResult}; + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))] +fn nat_alias() { + assert_evals_to!( + indoc!( + r#" + i : Num.Nat + i = 1 + + i + "# + ), + 1, + usize + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn i128_signed_int_alias() { + assert_evals_to!( + indoc!( + r#" + i : I128 + i = 128 + + i + "# + ), + 128, + i128 + ); +} +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))] +fn i64_signed_int_alias() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + main = + i : I64 + i = 64 + + i + "# + ), + 64, + i64 + ); +} +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))] +fn i32_signed_int_alias() { + assert_evals_to!( + indoc!( + r#" + i : I32 + i = 32 + + i + "# + ), + 32, + i32 + ); +} +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))] +fn i16_signed_int_alias() { + assert_evals_to!( + indoc!( + r#" + i : I16 + i = 16 + + i + "# + ), + 16, + i16 + ); +} +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))] +fn i8_signed_int_alias() { + assert_evals_to!( + indoc!( + r#" + i : I8 + i = 8 + + i + "# + ), + 8, + i8 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))] +fn i128_hex_int_alias() { + assert_evals_to!( + indoc!( + r#" + f : I128 + f = 0x123 + + f + "# + ), + 0x123, + i128 + ); +} +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))] +fn i64_hex_int_alias() { + assert_evals_to!( + indoc!( + r#" + f : I64 + f = 0x123 + + f + "# + ), + 0x123, + i64 + ); +} +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))] +fn i32_hex_int_alias() { + assert_evals_to!( + indoc!( + r#" + f : I32 + f = 0x123 + + f + "# + ), + 0x123, + i32 + ); +} +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))] +fn i16_hex_int_alias() { + assert_evals_to!( + indoc!( + r#" + f : I16 + f = 0x123 + + f + "# + ), + 0x123, + i16 + ); +} +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))] +fn i8_hex_int_alias() { + assert_evals_to!( + indoc!( + r#" + f : I8 + f = 0xA + + f + "# + ), + 0xA, + i8 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))] +fn u128_signed_int_alias() { + assert_evals_to!( + indoc!( + r#" + i : U128 + i = 128 + + i + "# + ), + 128, + u128 + ); +} +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))] +fn u64_signed_int_alias() { + assert_evals_to!( + indoc!( + r#" + i : U64 + i = 64 + + i + "# + ), + 64, + u64 + ); +} +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))] +fn u32_signed_int_alias() { + assert_evals_to!( + indoc!( + r#" + i : U32 + i = 32 + + i + "# + ), + 32, + u32 + ); +} +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))] +fn u16_signed_int_alias() { + assert_evals_to!( + indoc!( + r#" + i : U16 + i = 16 + + i + "# + ), + 16, + u16 + ); +} +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))] +fn u8_signed_int_alias() { + assert_evals_to!( + indoc!( + r#" + i : U8 + i = 8 + + i + "# + ), + 8, + u8 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))] +fn u128_hex_int_alias() { + assert_evals_to!( + indoc!( + r#" + f : U128 + f = 0x123 + + f + "# + ), + 0x123, + i128 + ); +} +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))] +fn u64_hex_int_alias() { + assert_evals_to!( + indoc!( + r#" + f : U64 + f = 0x123 + + f + "# + ), + 0x123, + u64 + ); +} +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))] +fn u32_hex_int_alias() { + assert_evals_to!( + indoc!( + r#" + f : U32 + f = 0x123 + + f + "# + ), + 0x123, + u32 + ); +} +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))] +fn u16_hex_int_alias() { + assert_evals_to!( + indoc!( + r#" + f : U16 + f = 0x123 + + f + "# + ), + 0x123, + u16 + ); +} +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))] +fn u8_hex_int_alias() { + assert_evals_to!( + indoc!( + r#" + f : U8 + f = 0xA + + f + "# + ), + 0xA, + u8 + ); +} + +#[test] +fn character_literal() { + assert_evals_to!( + indoc!( + r#" + x = 'A' + + x + "# + ), + 65, + i64 + ); +} + +#[test] +fn character_literal_back_slash() { + assert_evals_to!( + indoc!( + r#" + x = '\\' + + x + "# + ), + 92, + i64 + ); +} + +#[test] +fn character_literal_single_quote() { + assert_evals_to!( + indoc!( + r#" + x = '\'' + + x + "# + ), + 39, + i64 + ); +} + +#[test] +fn character_literal_new_line() { + assert_evals_to!( + indoc!( + r#" + x = '\n' + + x + "# + ), + 10, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))] +fn dec_float_alias() { + assert_evals_to!( + indoc!( + r#" + x : Dec + x = 2.1 + + x + "# + ), + RocDec::from_str_to_i128_unsafe("2.1"), + i128 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))] +fn f64_float_alias() { + assert_evals_to!( + indoc!( + r#" + f : F64 + f = 3.6 + + f + "# + ), + 3.6, + f64 + ); +} +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))] +fn f32_float_alias() { + assert_evals_to!( + indoc!( + r#" + f : F32 + f = 3.6 + + f + "# + ), + 3.6, + f32 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))] +fn f64_sqrt_100() { + assert_evals_to!("Num.sqrt 100f64", 10.0, f64); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))] +fn f64_sqrt_checked_0() { + assert_evals_to!("Num.sqrt 0f64", 0.0, f64); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn f64_sqrt_checked_positive() { + assert_evals_to!("Num.sqrtChecked 100f64", RocResult::ok(10.0), RocResult); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn f64_sqrt_checked_negative() { + assert_evals_to!("Num.sqrtChecked -1f64", RocResult::err(()), RocResult); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))] +fn f64_log() { + assert_evals_to!("Num.log 7.38905609893f64", 1.999999999999912, f64); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn f64_log_checked_one() { + assert_evals_to!("Num.logChecked 1f64", RocResult::ok(0.0), RocResult); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn f64_log_checked_zero() { + assert_evals_to!("Num.logChecked 0f64", RocResult::err(()), RocResult); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn f64_log_negative() { + assert_evals_to!("Num.log -1f64", true, f64, |f: f64| f.is_nan()); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))] +fn f64_round() { + assert_evals_to!("Num.round 3.6f64", 4, i64); + assert_evals_to!("Num.round 3.4f64", 3, i64); + assert_evals_to!("Num.round 2.5f64", 3, i64); + assert_evals_to!("Num.round -2.3f64", -2, i64); + assert_evals_to!("Num.round -2.5f64", -3, i64); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))] +fn f64_abs() { + assert_evals_to!("Num.abs -4.7f64", 4.7, f64); + assert_evals_to!("Num.abs 5.8f64", 5.8, f64); + + #[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] + { + assert_evals_to!("Num.abs Num.maxF64", f64::MAX, f64); + assert_evals_to!("Num.abs Num.minF64", f64::MAX, f64); + } +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn i64_abs() { + assert_evals_to!("Num.abs -6", 6, i64); + assert_evals_to!("Num.abs 7", 7, i64); + assert_evals_to!("Num.abs 0", 0, i64); + assert_evals_to!("Num.abs -0", 0, i64); + assert_evals_to!("Num.abs -1", 1, i64); + assert_evals_to!("Num.abs 1", 1, i64); + assert_evals_to!("Num.abs 9_000_000_000_000", 9_000_000_000_000, i64); + assert_evals_to!("Num.abs -9_000_000_000_000", 9_000_000_000_000, i64); + assert_evals_to!("Num.abs Num.maxI64", i64::MAX, i64); + assert_evals_to!("Num.abs (Num.minI64 + 1)", -(i64::MIN + 1), i64); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn various_sized_abs() { + assert_evals_to!("Num.abs -6i8", 6, i8); + assert_evals_to!("Num.abs -6i16", 6, i16); + assert_evals_to!("Num.abs -6i32", 6, i32); + assert_evals_to!("Num.abs -6i64", 6, i64); + if !cfg!(feature = "gen-wasm") { + assert_evals_to!("Num.abs -6i128", 6, i128); + } + assert_evals_to!("Num.abs 6u8", 6, u8); + assert_evals_to!("Num.abs 6u16", 6, u16); + assert_evals_to!("Num.abs 6u32", 6, u32); + assert_evals_to!("Num.abs 6u64", 6, u64); + if !cfg!(feature = "gen-wasm") { + assert_evals_to!("Num.abs 6u128", 6, u128); + } +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +#[should_panic( + expected = r#"Roc failed with message: "integer absolute overflowed because its argument is the minimum value"# +)] +fn abs_min_int_overflow() { + assert_evals_to!( + indoc!( + r#" + Num.abs Num.minI64 + "# + ), + 0, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))] +fn gen_if_fn() { + assert_evals_to!( + indoc!( + r#" + limitedNegate = \num -> + x = + if num == 1 then + -1 + else if num == -1 then + 1 + else + num + x + + limitedNegate 1 + "# + ), + -1, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn gen_float_eq() { + assert_evals_to!( + indoc!( + r#" + 1.0 == 1.0 + "# + ), + true, + bool + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn gen_add_dec() { + assert_evals_to!( + indoc!( + r#" + x : Dec + x = 2.1 + + y : Dec + y = 3.1 + + z : Dec + z = x + y + + z + "# + ), + RocDec::from_str_to_i128_unsafe("5.2"), + i128 + ); +} +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))] +fn gen_add_f32() { + assert_evals_to!( + indoc!( + r#" + 1.1f32 + 2.4f32 + 3 + "# + ), + 6.5, + f32 + ); +} +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))] +fn gen_add_f64() { + assert_evals_to!( + indoc!( + r#" + 1.1f64 + 2.4 + 3 + "# + ), + 6.5, + f64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))] +fn gen_wrap_add_nums() { + assert_evals_to!( + indoc!( + r#" + add2 = \num1, num2 -> num1 + num2 + + add2 4 5 + "# + ), + 9, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn gen_div_f64() { + assert_evals_to!("48f64 / 2", 24.0, f64); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn gen_div_f32() { + assert_evals_to!("48f32 / 2", 24.0, f32); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn gen_div_checked_f64() { + assert_evals_to!( + indoc!( + r#" + when Num.divChecked 48 2f64 is + Ok val -> val + Err _ -> -1 + "# + ), + 24.0, + f64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn gen_div_checked_by_zero_f64() { + assert_evals_to!( + indoc!( + r#" + when Num.divChecked 47 0f64 is + Ok val -> val + Err _ -> -1 + "# + ), + -1.0, + f64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn gen_div_dec() { + assert_evals_to!( + indoc!( + r#" + x : Dec + x = 10 + + y : Dec + y = 3 + + x / y + "# + ), + RocDec::from_str_to_i128_unsafe("3.333333333333333333"), + i128 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn gen_div_checked_dec() { + assert_evals_to!( + indoc!( + r#" + x : Dec + x = 10 + + y : Dec + y = 3 + + when Num.divChecked x y is + Ok val -> val + Err _ -> -1 + "# + ), + RocDec::from_str_to_i128_unsafe("3.333333333333333333"), + i128 + ); +} +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn gen_div_checked_by_zero_dec() { + assert_evals_to!( + indoc!( + r#" + x : Dec + x = 10 + + y : Dec + y = 0 + + when Num.divChecked x y is + Ok val -> val + Err _ -> -1 + "# + ), + RocDec::from_str_to_i128_unsafe("-1"), + i128 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))] +fn gen_int_eq() { + assert_evals_to!( + indoc!( + r#" + 4 == 4 + "# + ), + true, + bool + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn gen_int_neq() { + assert_evals_to!( + indoc!( + r#" + 4 != 5 + "# + ), + true, + bool + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn int_less_than() { + assert_evals_to!("4 < 5", true, bool); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn float_less_than() { + assert_evals_to!("4.0 < 5.0", true, bool); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn float_greater_than() { + assert_evals_to!("5.0 > 4.0", true, bool); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn gen_dec_eq() { + assert_evals_to!( + indoc!( + r#" + x : Dec + x = 4 + + y : Dec + y = 4 + + x == y + "# + ), + true, + bool + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn gen_dec_neq() { + assert_evals_to!( + indoc!( + r#" + x : Dec + x = 4 + + y : Dec + y = 5 + + x != y + "# + ), + true, + bool + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn gen_wrap_int_neq() { + assert_evals_to!( + indoc!( + r#" + wrappedNotEq : a, a -> Bool where a implements Eq + wrappedNotEq = \num1, num2 -> + num1 != num2 + + wrappedNotEq 2 3 + "# + ), + true, + bool + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))] +fn gen_add_i8() { + assert_evals_to!( + indoc!( + r#" + 1i8 + 2i8 + 3i8 + "# + ), + 6, + i8 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))] +fn gen_add_u8() { + assert_evals_to!( + indoc!( + r#" + 1u8 + 2u8 + 3u8 + "# + ), + 6, + u8 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))] +fn gen_add_i16() { + assert_evals_to!( + indoc!( + r#" + 1i16 + 2i16 + 3i16 + "# + ), + 6, + i16 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))] +fn gen_add_u16() { + assert_evals_to!( + indoc!( + r#" + 1u16 + 2u16 + 3u16 + "# + ), + 6, + u16 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))] +fn gen_add_i32() { + assert_evals_to!( + indoc!( + r#" + 1i32 + 2i32 + 3i32 + "# + ), + 6, + i32 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))] +fn gen_add_u32() { + assert_evals_to!( + indoc!( + r#" + 1u32 + 2u32 + 3u32 + "# + ), + 6, + u32 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))] +fn gen_add_i64() { + assert_evals_to!( + indoc!( + r#" + 1 + 2 + 3 + "# + ), + 6, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn gen_sub_dec() { + assert_evals_to!( + indoc!( + r#" + x : Dec + x = 1.5 + + y : Dec + y = 2.4 + + z : Dec + z = 3 + + (x - y) - z + "# + ), + RocDec::from_str_to_i128_unsafe("-3.9"), + i128 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn gen_mul_dec() { + assert_evals_to!( + indoc!( + r#" + x : Dec + x = 2 + + y : Dec + y = 4 + + z : Dec + z = 6 + + x * y * z + "# + ), + RocDec::from_str_to_i128_unsafe("48.0"), + i128 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn gen_sub_f64() { + assert_evals_to!("1.5f64 - 2.4 - 3", -3.9, f64); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn gen_sub_f32() { + assert_evals_to!("1.5f32 - 2.4 - 3", -3.9, f32); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))] +fn gen_sub_i8() { + assert_evals_to!("1i8 - 2i8 - 3i8", -4, i8); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))] +fn gen_sub_u8() { + assert_evals_to!("8u8 - 2u8 - 3u8", 3, u8); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))] +fn gen_sub_i16() { + assert_evals_to!("1i16 - 2i16 - 3i16", -4, i16); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))] +fn gen_sub_u16() { + assert_evals_to!("8u16 - 2u16 - 3u16", 3, u16); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))] +fn gen_sub_i32() { + assert_evals_to!("1i32 - 2i32 - 3i32", -4, i32); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))] +fn gen_sub_u32() { + assert_evals_to!("8u32 - 2u32 - 3u32", 3, u32); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))] +fn gen_sub_i64() { + assert_evals_to!("1 - 2 - 3", -4, i64); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))] +fn gen_signed_mul_quadword_and_lower() { + assert_evals_to!("2i64 * 4 * 6", 48, i64); + assert_evals_to!("2i32 * 4 * 6", 48, i32); + assert_evals_to!("2i16 * 4 * 6", 48, i16); + assert_evals_to!("2i8 * 4 * 6", 48, i8); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))] +fn gen_unsigned_mul_quadword_and_lower() { + assert_evals_to!("2u64 * 4 * 6", 48, u64); + assert_evals_to!("2u32 * 4 * 6", 48, u32); + assert_evals_to!("2u16 * 4 * 6", 48, u16); + assert_evals_to!("2u8 * 4 * 6", 48, u8); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))] +fn gen_mul_f64() { + assert_evals_to!("2f64 * 4 * 6", 48.0, f64); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))] +fn gen_mul_f32() { + assert_evals_to!("2f32 * 4 * 6", 48.0, f32); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn gen_div_i64() { + assert_evals_to!("1000i64 // 10", 100, i64); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn gen_div_u64() { + assert_evals_to!("1000u64 // 10", 100, u64); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn gen_div_checked_i64() { + assert_evals_to!( + indoc!( + r#" + when Num.divTruncChecked 1000 10 is + Ok val -> val + Err _ -> -1 + "# + ), + 100, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn gen_div_checked_by_zero_i64() { + assert_evals_to!( + indoc!( + r#" + when Num.divTruncChecked 1000 0 is + Err DivByZero -> 99 + _ -> -24 + "# + ), + 99, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn gen_rem_i64() { + assert_evals_to!("Num.rem 8 3", 2, i64); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn gen_rem_checked_div_by_zero_i64() { + assert_evals_to!( + indoc!( + r#" + when Num.remChecked 8 0 is + Err DivByZero -> 4 + Ok _ -> -23 + "# + ), + 4, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn gen_is_positive_i64() { + assert_evals_to!("Num.isPositive 0", false, bool); + assert_evals_to!("Num.isPositive 1", true, bool); + assert_evals_to!("Num.isPositive -5", false, bool); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn gen_is_negative_i64() { + assert_evals_to!("Num.isNegative 0", false, bool); + assert_evals_to!("Num.isNegative 3", false, bool); + assert_evals_to!("Num.isNegative -2", true, bool); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn gen_is_positive_f64() { + assert_evals_to!("Num.isPositive 0.0", false, bool); + assert_evals_to!("Num.isPositive 4.7", true, bool); + assert_evals_to!("Num.isPositive -8.5", false, bool); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn gen_is_negative_f64() { + assert_evals_to!("Num.isNegative 0.0", false, bool); + assert_evals_to!("Num.isNegative 9.9", false, bool); + assert_evals_to!("Num.isNegative -4.4", true, bool); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn gen_is_zero_i64() { + assert_evals_to!("Num.isZero 0", true, bool); + assert_evals_to!("Num.isZero 0_0", true, bool); + assert_evals_to!("Num.isZero 1", false, bool); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn gen_is_zero_f64() { + assert_evals_to!("Num.isZero 0.0", true, bool); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn gen_is_odd() { + assert_evals_to!("Num.isOdd 4", false, bool); + assert_evals_to!("Num.isOdd 5", true, bool); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn gen_is_even() { + assert_evals_to!("Num.isEven 6", true, bool); + assert_evals_to!("Num.isEven 7", false, bool); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn sin() { + assert_evals_to!("Num.sin 0f64", 0.0, f64); + assert_evals_to!("Num.sin 1.41421356237f64", 0.9877659459922529, f64); + assert_evals_to!("Num.sin 0dec", RocDec::from_str("0.0").unwrap(), RocDec); + assert_evals_to!( + "Num.sin 1.414213562373095049dec", + RocDec::from_str("0.987765945992735616").unwrap(), + RocDec + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn cos() { + assert_evals_to!("Num.cos 0f64", 1.0, f64); + assert_evals_to!("Num.cos 3.14159265359f64", -1.0, f64); + assert_evals_to!("Num.cos 0dec", RocDec::from_str("1.0").unwrap(), RocDec); + assert_evals_to!( + "Num.cos 3.141592653589793238dec", + RocDec::from_str("-1.0").unwrap(), + RocDec + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn tan() { + assert_evals_to!("Num.tan 0f64", 0.0f64, f64); + assert_evals_to!("Num.tan 0dec", RocDec::from_str("0.0").unwrap(), RocDec); + // TODO: deal with answers rounding differently on different cpus. + // These leads to results being off by a bit or 2. + // assert_evals_to!("Num.tan 1f64", 1.5574077246549023f64, f64); + // assert_evals_to!( + // "Num.tan 1dec", + // RocDec::from_str("1.557407724654902272").unwrap(), + // RocDec + // ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn bitwise_and() { + assert_evals_to!("Num.bitwiseAnd 20 20", 20, i64); + assert_evals_to!("Num.bitwiseAnd 25 10", 8, i64); + assert_evals_to!("Num.bitwiseAnd 200 0", 0, i64); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn bitwise_xor() { + assert_evals_to!("Num.bitwiseXor 20 20", 0, i64); + assert_evals_to!("Num.bitwiseXor 15 14", 1, i64); + assert_evals_to!("Num.bitwiseXor 7 15", 8, i64); + assert_evals_to!("Num.bitwiseXor 200 0", 200, i64); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn bitwise_or() { + assert_evals_to!("Num.bitwiseOr 1 1", 1, i64); + assert_evals_to!("Num.bitwiseOr 1 2", 3, i64); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn lt_u8() { + assert_evals_to!("1u8 < 2u8", true, bool); + assert_evals_to!("1u8 < 1u8", false, bool); + assert_evals_to!("2u8 < 1u8", false, bool); + assert_evals_to!("0u8 < 0u8", false, bool); + assert_evals_to!("128u8 < 0u8", false, bool); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn lte_u8() { + assert_evals_to!("1u8 <= 1u8", true, bool); + assert_evals_to!("2u8 <= 1u8", false, bool); + assert_evals_to!("1u8 <= 2u8", true, bool); + assert_evals_to!("0u8 <= 0u8", true, bool); + assert_evals_to!("128u8 <= 0u8", false, bool); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn gt_u8() { + assert_evals_to!("2u8 > 1u8", true, bool); + assert_evals_to!("2u8 > 2u8", false, bool); + assert_evals_to!("1u8 > 1u8", false, bool); + assert_evals_to!("0u8 > 0u8", false, bool); + assert_evals_to!("0u8 > 128u8", false, bool); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn gte_u8() { + assert_evals_to!("1u8 >= 1u8", true, bool); + assert_evals_to!("1u8 >= 2u8", false, bool); + assert_evals_to!("2u8 >= 1u8", true, bool); + assert_evals_to!("0u8 >= 0u8", true, bool); + assert_evals_to!("0u8 >= 128u8", false, bool); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn lt_u64() { + assert_evals_to!("1u64 < 2u64", true, bool); + assert_evals_to!("1u64 < 1u64", false, bool); + assert_evals_to!("2u64 < 1u64", false, bool); + assert_evals_to!("0u64 < 0u64", false, bool); + assert_evals_to!("9223372036854775808u64 < 0u64", false, bool); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn lte_u64() { + assert_evals_to!("1u64 <= 1u64", true, bool); + assert_evals_to!("2u64 <= 1u64", false, bool); + assert_evals_to!("1u64 <= 2u64", true, bool); + assert_evals_to!("0u64 <= 0u64", true, bool); + assert_evals_to!("9223372036854775808u64 <= 0u64", false, bool); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn gt_u64() { + assert_evals_to!("2u64 > 1u64", true, bool); + assert_evals_to!("2u64 > 2u64", false, bool); + assert_evals_to!("1u64 > 1u64", false, bool); + assert_evals_to!("0u64 > 0u64", false, bool); + assert_evals_to!("0u64 > 9223372036854775808u64", false, bool); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn gte_u64() { + assert_evals_to!("1u64 >= 1u64", true, bool); + assert_evals_to!("1u64 >= 2u64", false, bool); + assert_evals_to!("2u64 >= 1u64", true, bool); + assert_evals_to!("0u64 >= 0u64", true, bool); + assert_evals_to!("0u64 >= 9223372036854775808u64", false, bool); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn lt_i64() { + assert_evals_to!("1 < 2", true, bool); + assert_evals_to!("1 < 1", false, bool); + assert_evals_to!("2 < 1", false, bool); + assert_evals_to!("0 < 0", false, bool); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn lte_i64() { + assert_evals_to!("1 <= 1", true, bool); + assert_evals_to!("2 <= 1", false, bool); + assert_evals_to!("1 <= 2", true, bool); + assert_evals_to!("0 <= 0", true, bool); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn gt_i64() { + assert_evals_to!("2 > 1", true, bool); + assert_evals_to!("2 > 2", false, bool); + assert_evals_to!("1 > 1", false, bool); + assert_evals_to!("0 > 0", false, bool); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn gte_i64() { + assert_evals_to!("1 >= 1", true, bool); + assert_evals_to!("1 >= 2", false, bool); + assert_evals_to!("2 >= 1", true, bool); + assert_evals_to!("0 >= 0", true, bool); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn lt_f64() { + assert_evals_to!("1.1f64 < 1.2", true, bool); + assert_evals_to!("1.1f64 < 1.1", false, bool); + assert_evals_to!("1.2f64 < 1.1", false, bool); + assert_evals_to!("0.0f64 < 0.0", false, bool); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn lte_f64() { + assert_evals_to!("1.1f64 <= 1.1", true, bool); + assert_evals_to!("1.2f64 <= 1.1", false, bool); + assert_evals_to!("1.1f64 <= 1.2", true, bool); + assert_evals_to!("0.0f64 <= 0.0", true, bool); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn gt_f64() { + assert_evals_to!("2.2f64 > 1.1", true, bool); + assert_evals_to!("2.2f64 > 2.2", false, bool); + assert_evals_to!("1.1f64 > 2.2", false, bool); + assert_evals_to!("0.0f64 > 0.0", false, bool); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn gte_f64() { + assert_evals_to!("1.1f64 >= 1.1", true, bool); + assert_evals_to!("1.1f64 >= 1.2", false, bool); + assert_evals_to!("1.2f64 >= 1.1", true, bool); + assert_evals_to!("0.0f64 >= 0.0", true, bool); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))] +fn gen_order_of_arithmetic_ops() { + assert_evals_to!( + indoc!( + r#" + 1 + 3 * 7 - 2 + "# + ), + 20, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn gen_order_of_arithmetic_ops_complex_float() { + assert_evals_to!( + indoc!( + r#" + 3 - 48 * 2.0f64 + "# + ), + -93.0, + f64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))] +fn if_guard_bind_variable_false() { + assert_evals_to!( + indoc!( + r#" + wrapper = \{} -> + when 10 is + x if x == 5 -> 0 + _ -> 42 + + wrapper {} + "# + ), + 42, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))] +fn if_guard_bind_variable_true() { + assert_evals_to!( + indoc!( + r#" + wrapper = \{} -> + when 10 is + x if x == 10 -> 42 + _ -> 0 + + wrapper {} + "# + ), + 42, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))] +fn tail_call_elimination() { + assert_evals_to!( + indoc!( + r#" + sum = \n, accum -> + when n is + 0 -> accum + _ -> sum (n - 1) (n + accum) + + sum 1_000_000 0 + "# + ), + 500000500000, + i64 + ); +} + +#[test] +#[cfg(feature = "gen-dev")] +fn int_negate_dev() { + // TODO + // dev backend yet to have `Num.maxI64` or `Num.minI64`. + // add the "gen-dev" feature to the test below after implementing them both. + assert_evals_to!("Num.neg 123", -123, i64); + assert_evals_to!("Num.neg -123", 123, i64); + assert_evals_to!("Num.neg 0", 0, i64); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn int_negate() { + assert_evals_to!("Num.neg 123", -123, i64); + assert_evals_to!("Num.neg Num.maxI64", -i64::MAX, i64); + assert_evals_to!("Num.neg (Num.minI64 + 1)", i64::MAX, i64); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +#[should_panic( + expected = r#"Roc failed with message: "integer negation overflowed because its argument is the minimum value"# +)] +fn neg_min_int_overflow() { + assert_evals_to!( + indoc!( + r#" + Num.neg Num.minI64 + "# + ), + 0, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn gen_wrap_int_neg() { + assert_evals_to!( + indoc!( + r#" + wrappedNeg = \num -> -num + + wrappedNeg 3 + "# + ), + -3, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))] +fn gen_basic_fn() { + assert_evals_to!( + indoc!( + r#" + always42 : Num.Num (Num.Integer Num.Signed64) -> Num.Num (Num.Integer Num.Signed64) + always42 = \_ -> 42 + + always42 5 + "# + ), + 42, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn int_to_float() { + assert_evals_to!("Num.toFrac 0x9", RocDec::from(9i32), RocDec); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn num_to_frac() { + assert_evals_to!("Num.toFrac 9", RocDec::from(9i32), RocDec); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev"))] +fn num_to_frac_f64_to_f32() { + assert_evals_to!( + indoc!( + r#" + f64 : F64 + f64 = 9.0 + + f32 : F32 + f32 = Num.toFrac f64 + f32 + "# + ), + 9.0, + f32 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev"))] +fn num_to_frac_f32_to_f32() { + assert_evals_to!( + indoc!( + r#" + + arg : F32 + arg = 9.0 + + ret : F32 + ret = Num.toFrac arg + ret + "# + ), + 9.0, + f32 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn num_to_frac_f64_to_f64() { + assert_evals_to!( + indoc!( + r#" + + arg : F64 + arg = 9.0 + + ret : F64 + ret = Num.toFrac arg + ret + "# + ), + 9.0, + f64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn num_to_frac_f32_to_f64() { + assert_evals_to!( + indoc!( + r#" + + f32 : F32 + f32 = 9.0 + + f64 : F64 + f64 = Num.toFrac f32 + f64 + "# + ), + 9.0, + f64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn float_to_float() { + assert_evals_to!( + indoc!( + r#" + x : F64 + x = Num.toFrac 0.5f64 + + x + "# + ), + 0.5, + f64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn frac_is_nan() { + assert_evals_to!("Num.isNaN (0 / 0f64)", true, bool); + assert_evals_to!("Num.isNaN (1 / 0f64)", false, bool); + assert_evals_to!("Num.isNaN 42f64", false, bool); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn frac_is_infinite() { + assert_evals_to!("Num.isInfinite (1 / 0f64)", true, bool); + assert_evals_to!("Num.isInfinite (-1 / 0f64)", true, bool); + assert_evals_to!("Num.isInfinite (0 / 0f64)", false, bool); + assert_evals_to!("Num.isInfinite 42f64", false, bool); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn frac_is_finite() { + assert_evals_to!("Num.isFinite 42f64", true, bool); + assert_evals_to!("Num.isFinite (1 / 0f64)", false, bool); + assert_evals_to!("Num.isFinite (0 / 0f64)", false, bool); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn int_compare() { + assert_evals_to!("Num.compare 0 1", RocOrder::Lt, RocOrder); + assert_evals_to!("Num.compare 1 1", RocOrder::Eq, RocOrder); + assert_evals_to!("Num.compare 1 0", RocOrder::Gt, RocOrder); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn float_compare() { + assert_evals_to!("Num.compare 0.01 3.14", RocOrder::Lt, RocOrder); + assert_evals_to!("Num.compare 3.14 3.14", RocOrder::Eq, RocOrder); + assert_evals_to!("Num.compare 3.14 0.01", RocOrder::Gt, RocOrder); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn pow() { + assert_evals_to!("Num.pow 2.0f64 2.0f64", 4.0, f64); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn ceiling() { + assert_evals_to!("Num.ceiling 1.1f64", 2, i64); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn floor() { + assert_evals_to!("Num.floor 1.9f64", 1, i64); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))] +fn pow_int() { + assert_evals_to!("Num.powInt 2 3", 8, i64); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))] +fn atan() { + assert_evals_to!("Num.atan 10f64", 1.4711276743037347, f64); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +#[should_panic(expected = r#"Roc failed with message: "integer addition overflowed!"#)] +fn int_add_overflow() { + assert_evals_to!( + indoc!( + r#" + 9_223_372_036_854_775_807 + 1 + "# + ), + 0, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn int_add_checked_ok() { + assert_evals_to!( + "Num.addChecked 1 2", + RocResult::ok(3), + RocResult + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn int_add_checked_err() { + assert_evals_to!( + "Num.addChecked 9_223_372_036_854_775_807 1", + RocResult::err(()), + RocResult + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn int_add_wrap() { + assert_evals_to!( + "Num.addWrap 9_223_372_036_854_775_807 1", + std::i64::MIN, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn float_add_checked_pass() { + assert_evals_to!( + "Num.addChecked 1.0 0.0f64", + RocResult::ok(1.0), + RocResult + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn float_add_checked_fail() { + assert_evals_to!( + "Num.addChecked 1.7976931348623157e308f64 1.7976931348623157e308", + RocResult::err(()), + RocResult + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn float_add_overflow() { + assert_evals_to!( + "1.7976931348623157e308f64 + 1.7976931348623157e308", + f64::INFINITY, + f64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +#[should_panic(expected = r#"Roc failed with message: "integer subtraction overflowed!"#)] +fn int_sub_overflow() { + assert_evals_to!("-9_223_372_036_854_775_808 - 1", 0, i64); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))] +fn int_sub_wrap() { + assert_evals_to!( + "Num.subWrap -9_223_372_036_854_775_808 1", + std::i64::MAX, + i64 + ); + + assert_evals_to!("Num.subWrap -128i8 1", std::i8::MAX, i8); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn float_sub_overflow() { + assert_evals_to!( + "-1.7976931348623157e308f64 - 1.7976931348623157e308", + -f64::INFINITY, + f64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn int_sub_checked() { + assert_evals_to!( + indoc!( + r#" + when Num.subChecked 5 2 is + Ok v -> v + _ -> -1 + "# + ), + 3, + i64 + ); + + assert_evals_to!( + indoc!( + r#" + when Num.subChecked Num.minI64 1 is + Err Overflow -> -1 + Ok v -> v + "# + ), + -1, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn float_sub_checked() { + assert_evals_to!( + indoc!( + r#" + when Num.subChecked 1.0 0.0f64 is + Ok v -> v + Err Overflow -> -1.0 + "# + ), + 1.0, + f64 + ); + + assert_evals_to!( + indoc!( + r#" + when Num.subChecked -1.7976931348623157e308f64 1.7976931348623157e308 is + Err Overflow -> -1 + Ok v -> v + "# + ), + -1.0, + f64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +#[should_panic(expected = r#"Roc failed with message: "integer multiplication overflowed!"#)] +fn int_positive_mul_overflow() { + assert_evals_to!( + indoc!( + r#" + 9_223_372_036_854_775_807 * 2 + "# + ), + 0, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +#[should_panic(expected = r#"Roc failed with message: "integer multiplication overflowed!"#)] +fn int_negative_mul_overflow() { + assert_evals_to!( + indoc!( + r#" + (-9_223_372_036_854_775_808) * 2 + "# + ), + 0, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn float_positive_mul_overflow() { + assert_evals_to!( + indoc!( + r#" + 1.7976931348623157e308f64 * 2 + "# + ), + f64::INFINITY, + f64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn float_negative_mul_overflow() { + assert_evals_to!( + indoc!( + r#" + -1.7976931348623157e308f64 * 2 + "# + ), + -f64::INFINITY, + f64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn int_mul_wrap_i64() { + assert_evals_to!("Num.mulWrap Num.maxI64 2", -2, i64); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn int_mul_wrap_i128() { + assert_evals_to!("Num.mulWrap Num.maxI128 2", -2, i128); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn int_mul_checked() { + assert_evals_to!( + indoc!( + r#" + when Num.mulChecked 20 2 is + Ok v -> v + _ -> -1 + "# + ), + 40, + i64 + ); + + assert_evals_to!( + indoc!( + r#" + when Num.mulChecked Num.maxI64 2 is + Err Overflow -> -1 + Ok v -> v + "# + ), + -1, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn float_mul_checked() { + assert_evals_to!( + indoc!( + r#" + when Num.mulChecked 20.0 2.0f64 is + Ok v -> v + Err Overflow -> -1.0 + "# + ), + 40.0, + f64 + ); + + assert_evals_to!( + indoc!( + r#" + when Num.mulChecked 1.7976931348623157e308f64 2 is + Err Overflow -> -1 + Ok v -> v + "# + ), + -1.0, + f64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn shift_left_by() { + assert_evals_to!("Num.shiftLeftBy 0b0000_0001 0", 0b0000_0001, i64); + assert_evals_to!("Num.shiftLeftBy 0b0000_0001 1", 0b0000_0010, i64); + assert_evals_to!("Num.shiftLeftBy 0b0000_0011 2", 0b0000_1100, i64); + assert_evals_to!("Num.shiftLeftBy 2u16 2", 8, u16); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn shift_right_by() { + // Sign Extended Right Shift + + let is_llvm_release_mode = cfg!(feature = "gen-llvm") && !cfg!(debug_assertions); + + assert_evals_to!("Num.shiftRightBy 0b0100_0000i8 2", 0b0001_0000i8, i8); + assert_evals_to!("Num.shiftRightBy 0b1110_0000u8 1", 0b1111_0000u8, u8); + assert_evals_to!("Num.shiftRightBy 0b1100_0000u8 2", 0b1111_0000u8, u8); + assert_evals_to!("Num.shiftRightBy 0b0100_0000u8 12", 0b0000_0000u8, u8); + + // LLVM in release mode returns 0 instead of -1 for some reason + if !is_llvm_release_mode { + assert_evals_to!("Num.shiftRightBy 0b1000_0000u8 12", 0b1111_1111u8, u8); + } + assert_evals_to!("Num.shiftRightBy 12 0", 12, i64); + assert_evals_to!("Num.shiftRightBy 12 1", 6, i64); + assert_evals_to!("Num.shiftRightBy -12 1", -6, i64); + assert_evals_to!("Num.shiftRightBy 12 8", 0, i64); + assert_evals_to!("Num.shiftRightBy -12 8", -1, i64); + assert_evals_to!("Num.shiftRightBy 0 0", 0, i64); + assert_evals_to!("Num.shiftRightBy 0 1", 0, i64); + + assert_evals_to!("Num.shiftRightBy 12i32 0", 12, i32); + assert_evals_to!("Num.shiftRightBy 12i32 1", 6, i32); + assert_evals_to!("Num.shiftRightBy -12i32 1", -6, i32); + assert_evals_to!("Num.shiftRightBy 12i32 8", 0, i32); + assert_evals_to!("Num.shiftRightBy -12i32 8", -1, i32); + + assert_evals_to!("Num.shiftRightBy 12i8 0", 12, i8); + assert_evals_to!("Num.shiftRightBy 12i8 1", 6, i8); + assert_evals_to!("Num.shiftRightBy -12i8 1", -6, i8); + assert_evals_to!("Num.shiftRightBy 12i8 8", 0, i8); + + if !is_llvm_release_mode { + assert_evals_to!("Num.shiftRightBy -12i8 8", -1, i8); + } +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn shift_right_zf_by() { + // Logical Right Shift + assert_evals_to!("Num.shiftRightZfBy 0b1100_0000u8 2", 0b0011_0000u8, u8); + assert_evals_to!("Num.shiftRightZfBy 0b0000_0010u8 1", 0b0000_0001u8, u8); + assert_evals_to!("Num.shiftRightZfBy 0b0000_1100u8 2", 0b0000_0011u8, u8); + assert_evals_to!("Num.shiftRightZfBy 0b1000_0000u8 12", 0b0000_0000u8, u8); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn shift_right_cast_i8() { + // FIXME (Brian) Something funny happening with 8-bit binary literals in tests + + // arithmetic + assert_evals_to!( + "Num.shiftRightBy (Num.toI8 0b1100_0000u8) 2", + 0b1111_0000u8 as i8, + i8 + ); + + // logical + assert_evals_to!( + "Num.shiftRightZfBy (Num.toI8 0b1100_0000u8) 2", + 0b0011_0000i8, + i8 + ); + assert_evals_to!("Num.shiftRightZfBy 0b1100_0000u8 2", 0b0011_0000u8, u8); + assert_evals_to!("Num.shiftRightZfBy 0b0000_0010u8 1", 0b0000_0001u8, u8); + assert_evals_to!("Num.shiftRightZfBy 0b0000_1100u8 2", 0b0000_0011u8, u8); + assert_evals_to!("Num.shiftRightZfBy 0b1000_0000u8 12", 0b0000_0000u8, u8); + assert_evals_to!( + "Num.shiftRightZfBy 0xffff_0000_0000_0000_0000_0000_0000_ffffu128 4", + 0x0fff_f000_0000_0000_0000_0000_0000_0fffu128, + u128 + ); + assert_evals_to!( + "Num.shiftRightZfBy 0xaaaa_0000_0000_bbbb_ffff_ffff_ffff_ffffu128 68", + 0x0000_0000_0000_0000_0aaa_a000_0000_0bbbu128, + u128 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn min_i128() { + assert_evals_to!("Num.minI128", i128::MIN, i128); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn max_i128() { + assert_evals_to!("Num.maxI128", i128::MAX, i128); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn min_i64() { + assert_evals_to!("Num.minI64", i64::MIN, i64); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn max_i64() { + assert_evals_to!("Num.maxI64", i64::MAX, i64); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn min_u64() { + assert_evals_to!("Num.minU64", u64::MIN, u64); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn max_u64() { + assert_evals_to!("Num.maxU64", u64::MAX, u64); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn min_i32() { + assert_evals_to!("Num.minI32", i32::MIN, i32); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn max_i32() { + assert_evals_to!("Num.maxI32", i32::MAX, i32); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn min_u32() { + assert_evals_to!("Num.minU32", u32::MIN, u32); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn max_u32() { + assert_evals_to!("Num.maxU32", u32::MAX, u32); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn min_i16() { + assert_evals_to!("Num.minI16", i16::MIN, i16); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn max_i16() { + assert_evals_to!("Num.maxI16", i16::MAX, i16); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn min_u16() { + assert_evals_to!("Num.minU16", u16::MIN, u16); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn max_u16() { + assert_evals_to!("Num.maxU16", u16::MAX, u16); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn min_i8() { + assert_evals_to!("Num.minI8", i8::MIN, i8); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn max_i8() { + assert_evals_to!("Num.maxI8", i8::MAX, i8); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn min_u8() { + assert_evals_to!("Num.minU8", u8::MIN, u8); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn max_u8() { + assert_evals_to!("Num.maxU8", u8::MAX, u8); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn max_f64() { + assert_evals_to!("Num.maxF64", f64::MAX, f64); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn min_f64() { + assert_evals_to!("Num.minF64", f64::MIN, f64); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn max_f32() { + assert_evals_to!("Num.maxF32", f32::MAX, f32); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn min_f32() { + assert_evals_to!("Num.minF32", f32::MIN, f32); +} + +#[test] +#[cfg(all(feature = "gen-llvm", not(feature = "gen-llvm-wasm")))] +fn to_nat_truncate_wraps() { + let input = "Num.toNat 10_000_000_000_000_000_000_000i128"; + assert_evals_to!(input, 1864712049423024128, u64) +} + +macro_rules! num_conversion_tests { + ($($fn:expr, $typ:ty, ($($test_name:ident, $input:expr, $output:expr $(, [$($support_gen:literal),*])? )*))*) => {$($( + #[test] + #[cfg(any(feature = "gen-llvm", $($(feature = $support_gen,)*)?))] + fn $test_name() { + let input = format!("{} {}", $fn, $input); + assert_evals_to!(&input, $output, $typ) + } + )*)*} +} + +num_conversion_tests! { + "Num.toI8", i8, ( + to_i8_same_width, "15u8", 15, ["gen-wasm", "gen-dev"] + to_i8_truncate, "115i32", 115, ["gen-wasm", "gen-dev"] + to_i8_truncate_wraps, "500i32", -12, ["gen-wasm", "gen-dev"] + ) + "Num.toI16", i16, ( + to_i16_same_width, "15u16", 15, ["gen-wasm", "gen-dev"] + to_i16_extend, "15i8", 15, ["gen-wasm", "gen-dev"] + to_i16_sign_extend_i8, "-15i8", -15, ["gen-wasm", "gen-dev"] + to_i16_truncate, "115i32", 115, ["gen-wasm", "gen-dev"] + to_i16_truncate_wraps, "60000i32", -5536, ["gen-wasm", "gen-dev"] + ) + "Num.toI32", i32, ( + to_i32_same_width, "15u32", 15, ["gen-wasm", "gen-dev"] + to_i32_extend, "15i8", 15, ["gen-wasm", "gen-dev"] + to_i32_sign_extend_i8, "-15i8", -15, ["gen-wasm", "gen-dev"] + to_i32_sign_extend_i16, "-15i16", -15, ["gen-wasm", "gen-dev"] + to_i32_truncate, "115i64", 115, ["gen-wasm", "gen-dev"] + to_i32_truncate_wraps, "5000000000i64", 705032704, ["gen-wasm", "gen-dev"] + ) + "Num.toI64", i64, ( + to_i64_same_width, "15u64", 15, ["gen-wasm", "gen-dev"] + to_i64_extend, "15i8", 15, ["gen-wasm", "gen-dev"] + to_i64_sign_extend_i8, "-15i8", -15, ["gen-wasm", "gen-dev"] + to_i64_sign_extend_i16, "-15i16", -15, ["gen-wasm", "gen-dev"] + to_i64_sign_extend_i32, "-15i32", -15, ["gen-wasm", "gen-dev"] + to_i64_truncate, "115i128", 115 + to_i64_truncate_wraps, "10_000_000_000_000_000_000i128", -8446744073709551616 + ) + "Num.toI128", i128, ( + to_i128_same_width, "15u128", 15 + to_i128_extend, "15i8", 15 + ) + "Num.toU8", u8, ( + to_u8_same_width, "15i8", 15, ["gen-wasm", "gen-dev"] + to_u8_truncate, "115i32", 115, ["gen-wasm", "gen-dev"] + to_u8_truncate_wraps, "500i32", 244, ["gen-wasm", "gen-dev"] + ) + "Num.toU16", u16, ( + to_u16_same_width, "15i16", 15, ["gen-wasm", "gen-dev"] + to_u16_extend, "15i8", 15, ["gen-wasm", "gen-dev"] + to_u16_truncate, "115i32", 115, ["gen-wasm", "gen-dev"] + to_u16_truncate_wraps, "600000000i32", 17920, ["gen-wasm", "gen-dev"] + ) + "Num.toU32", u32, ( + to_u32_same_width, "15i32", 15, ["gen-wasm", "gen-dev"] + to_u32_extend, "15i8", 15, ["gen-wasm", "gen-dev"] + to_u32_truncate, "115i64", 115, ["gen-wasm", "gen-dev"] + to_u32_truncate_wraps, "5000000000000000000i64", 1156841472, ["gen-wasm", "gen-dev"] + ) + "Num.toU64", u64, ( + to_u64_same_width, "15i64", 15, ["gen-wasm", "gen-dev"] + to_u64_extend, "15i8", 15, ["gen-wasm", "gen-dev"] + to_u64_truncate, "115i128", 115 + to_u64_truncate_wraps, "10_000_000_000_000_000_000_000i128", 1864712049423024128 + ) + "Num.toU128", u128, ( + to_u128_same_width, "15i128", 15 + to_u128_extend, "15i8", 15 + ) + "Num.toNat", usize, ( + to_nat_same_width, "15i64", 15, ["gen-wasm", "gen-dev"] + to_nat_extend, "15i8", 15, ["gen-wasm", "gen-dev"] + to_nat_truncate, "115i128", 115 + ) + "Num.toF32", f32, ( + to_f32_from_i8, "15i8", 15.0 + to_f32_from_i16, "15i16", 15.0 + to_f32_from_i32, "15i32", 15.0 + to_f32_from_i64, "15i64", 15.0 + to_f32_from_i128, "15i128", 15.0 + to_f32_from_u8, "15u8", 15.0 + to_f32_from_u16, "15u16", 15.0 + to_f32_from_u32, "15u32", 15.0 + to_f32_from_u64, "15u64", 15.0 + to_f32_from_u128, "15u128", 15.0 + to_f32_from_nat, "15nat", 15.0 + to_f32_from_f32, "1.5f32", 1.5 + to_f32_from_f64, "1.5f64", 1.5 + ) + "Num.toF64", f64, ( + to_f64_from_i8, "15i8", 15.0 + to_f64_from_i16, "15i16", 15.0 + to_f64_from_i32, "15i32", 15.0 + to_f64_from_i64, "15i64", 15.0 + to_f64_from_i128, "15i128", 15.0 + to_f64_from_u8, "15u8", 15.0 + to_f64_from_u16, "15u16", 15.0 + to_f64_from_u32, "15u32", 15.0 + to_f64_from_u64, "15u64", 15.0 + to_f64_from_u128, "15u128", 15.0 + to_f64_from_nat, "15nat", 15.0 + to_f64_from_f32, "1.5f32", 1.5 + to_f64_from_f64, "1.5f64", 1.5 + ) +} + +macro_rules! to_int_checked_tests { + ($($fn:expr, $typ:ty, ($($test_name:ident, $input:expr, $output:expr)*))*) => {$($( + #[test] + #[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] + fn $test_name() { + let sentinel = 23; + // Some n = Ok n, None = OutOfBounds + let expected = match $output.into() { + None => sentinel, + Some(n) => { + assert_ne!(n, sentinel); + n + } + }; + let input = format!("Result.withDefault ({} {}) {}", $fn, $input, sentinel); + assert_evals_to!(&input, expected, $typ) + } + )*)*} +} + +to_int_checked_tests! { + "Num.toI8Checked", i8, ( + to_i8_checked_same, "15i8", 15 + to_i8_checked_same_width_unsigned_fits, "15u8", 15 + to_i8_checked_same_width_unsigned_oob, "128u8", None + to_i8_checked_larger_width_signed_fits_pos, "15i16", 15 + to_i8_checked_larger_width_signed_oob_pos, "128i16", None + to_i8_checked_larger_width_signed_fits_neg, "-15i16", -15 + to_i8_checked_larger_width_signed_oob_neg, "-129i16", None + to_i8_checked_larger_width_unsigned_fits_pos, "15u16", 15 + to_i8_checked_larger_width_unsigned_oob_pos, "128u16", None + ) + "Num.toI16Checked", i16, ( + to_i16_checked_smaller_width_pos, "15i8", 15 + to_i16_checked_smaller_width_neg, "-15i8", -15 + to_i16_checked_same, "15i16", 15 + to_i16_checked_same_width_unsigned_fits, "15u16", 15 + to_i16_checked_same_width_unsigned_oob, "32768u16", None + to_i16_checked_larger_width_signed_fits_pos, "15i32", 15 + to_i16_checked_larger_width_signed_oob_pos, "32768i32", None + to_i16_checked_larger_width_signed_fits_neg, "-15i32", -15 + to_i16_checked_larger_width_signed_oob_neg, "-32769i32", None + to_i16_checked_larger_width_unsigned_fits_pos, "15u32", 15 + to_i16_checked_larger_width_unsigned_oob_pos, "32768u32", None + ) + "Num.toI32Checked", i32, ( + to_i32_checked_smaller_width_pos, "15i8", 15 + to_i32_checked_smaller_width_neg, "-15i8", -15 + to_i32_checked_same, "15i32", 15 + to_i32_checked_same_width_unsigned_fits, "15u32", 15 + to_i32_checked_same_width_unsigned_oob, "2147483648u32", None + to_i32_checked_larger_width_signed_fits_pos, "15i64", 15 + to_i32_checked_larger_width_signed_oob_pos, "2147483648i64", None + to_i32_checked_larger_width_signed_fits_neg, "-15i64", -15 + to_i32_checked_larger_width_signed_oob_neg, "-2147483649i64", None + to_i32_checked_larger_width_unsigned_fits_pos, "15u64", 15 + to_i32_checked_larger_width_unsigned_oob_pos, "2147483648u64", None + ) + "Num.toI64Checked", i64, ( + to_i64_checked_smaller_width_pos, "15i8", 15 + to_i64_checked_smaller_width_neg, "-15i8", -15 + to_i64_checked_same, "15i64", 15 + to_i64_checked_same_width_unsigned_fits, "15u64", 15 + to_i64_checked_same_width_unsigned_oob, "9223372036854775808u64", None + to_i64_checked_larger_width_signed_fits_pos, "15i128", 15 + to_i64_checked_larger_width_signed_oob_pos, "9223372036854775808i128", None + to_i64_checked_larger_width_signed_fits_neg, "-15i128", -15 + to_i64_checked_larger_width_signed_oob_neg, "-9223372036854775809i128", None + to_i64_checked_larger_width_unsigned_fits_pos, "15u128", 15 + to_i64_checked_larger_width_unsigned_oob_pos, "9223372036854775808u128", None + ) + "Num.toI128Checked", i128, ( + to_i128_checked_smaller_width_pos, "15i8", 15 + to_i128_checked_smaller_width_neg, "-15i8", -15 + to_i128_checked_same, "15i128", 15 + to_i128_checked_same_width_unsigned_fits, "15u128", 15 + to_i128_checked_same_width_unsigned_oob, "170141183460469231731687303715884105728u128", None + ) + "Num.toU8Checked", u8, ( + to_u8_checked_same, "15u8", 15 + to_u8_checked_same_width_signed_fits, "15i8", 15 + to_u8_checked_same_width_signed_oob, "-1i8", None + to_u8_checked_larger_width_signed_fits_pos, "15i16", 15 + to_u8_checked_larger_width_signed_oob_pos, "256i16", None + to_u8_checked_larger_width_signed_oob_neg, "-1i16", None + to_u8_checked_larger_width_unsigned_fits_pos, "15u16", 15 + to_u8_checked_larger_width_unsigned_oob_pos, "256u16", None + ) + "Num.toU16Checked", u16, ( + to_u16_checked_smaller_width_pos, "15i8", 15 + to_u16_checked_smaller_width_neg_oob, "-15i8", None + to_u16_checked_same, "15u16", 15 + to_u16_checked_same_width_signed_fits, "15i16", 15 + to_u16_checked_same_width_signed_oob, "-1i16", None + to_u16_checked_larger_width_signed_fits_pos, "15i32", 15 + to_u16_checked_larger_width_signed_oob_pos, "65536i32", None + to_u16_checked_larger_width_signed_oob_neg, "-1i32", None + to_u16_checked_larger_width_unsigned_fits_pos, "15u32", 15 + to_u16_checked_larger_width_unsigned_oob_pos, "65536u32", None + ) + "Num.toU32Checked", u32, ( + to_u32_checked_smaller_width_pos, "15i8", 15 + to_u32_checked_smaller_width_neg_oob, "-15i8", None + to_u32_checked_same, "15u32", 15 + to_u32_checked_same_width_signed_fits, "15i32", 15 + to_u32_checked_same_width_signed_oob, "-1i32", None + to_u32_checked_larger_width_signed_fits_pos, "15i64", 15 + to_u32_checked_larger_width_signed_oob_pos, "4294967296i64", None + to_u32_checked_larger_width_signed_oob_neg, "-1i64", None + to_u32_checked_larger_width_unsigned_fits_pos, "15u64", 15 + to_u32_checked_larger_width_unsigned_oob_pos, "4294967296u64", None + ) + "Num.toU64Checked", u64, ( + to_u64_checked_smaller_width_pos, "15i8", 15 + to_u64_checked_smaller_width_neg_oob, "-15i8", None + to_u64_checked_same, "15u64", 15 + to_u64_checked_same_width_signed_fits, "15i64", 15 + to_u64_checked_same_width_signed_oob, "-1i64", None + to_u64_checked_larger_width_signed_fits_pos, "15i128", 15 + to_u64_checked_larger_width_signed_oob_pos, "18446744073709551616i128", None + to_u64_checked_larger_width_signed_oob_neg, "-1i128", None + to_u64_checked_larger_width_unsigned_fits_pos, "15u128", 15 + to_u64_checked_larger_width_unsigned_oob_pos, "18446744073709551616u128", None + ) + "Num.toU128Checked", u128, ( + to_u128_checked_smaller_width_pos, "15i8", 15 + to_u128_checked_smaller_width_neg_oob, "-15i8", None + to_u128_checked_same, "15u128", 15 + to_u128_checked_same_width_signed_fits, "15i128", 15 + to_u128_checked_same_width_signed_oob, "-1i128", None + ) + "Num.toNatChecked", usize, ( + to_nat_checked_smaller_width_pos, "15i8", 15 + to_nat_checked_smaller_width_neg_oob, "-15i8", None + to_nat_checked_same, "15u64", 15 + to_nat_checked_same_width_signed_fits, "15i64", 15 + to_nat_checked_same_width_signed_oob, "-1i64", None + to_nat_checked_larger_width_signed_fits_pos, "15i128", 15 + to_nat_checked_larger_width_signed_oob_pos, "18446744073709551616i128", None + to_nat_checked_larger_width_signed_oob_neg, "-1i128", None + to_nat_checked_larger_width_unsigned_fits_pos, "15u128", 15 + to_nat_checked_larger_width_unsigned_oob_pos, "18446744073709551616u128", None + ) +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn is_multiple_of_signed() { + // true + assert_evals_to!("Num.isMultipleOf 5 1", true, bool); + assert_evals_to!("Num.isMultipleOf 5 -1", true, bool); + assert_evals_to!("Num.isMultipleOf 0 0", true, bool); + assert_evals_to!("Num.isMultipleOf 0 1", true, bool); + assert_evals_to!("Num.isMultipleOf 0 -1", true, bool); + // false + assert_evals_to!("Num.isMultipleOf 5 2", false, bool); + assert_evals_to!("Num.isMultipleOf 5 0", false, bool); + + // overflow + assert_evals_to!("Num.isMultipleOf -9223372036854775808 -1", true, bool); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn is_multiple_of_unsigned() { + // true + assert_evals_to!("Num.isMultipleOf 5u8 1", true, bool); + assert_evals_to!("Num.isMultipleOf 0u8 0", true, bool); + assert_evals_to!("Num.isMultipleOf 0u8 1", true, bool); + assert_evals_to!("Num.isMultipleOf 0u8 0xFF", true, bool); + + // false + assert_evals_to!("Num.isMultipleOf 5u8 2", false, bool); + assert_evals_to!("Num.isMultipleOf 5u8 0", false, bool); + + // unsigned result is different from signed + assert_evals_to!("Num.isMultipleOf 5u8 0xFF", false, bool); + assert_evals_to!("Num.isMultipleOf 0xFCu8 0xFE", false, bool); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn bytes_to_u16_clearly_out_of_bounds() { + assert_evals_to!( + indoc!( + r#" + bytes = Str.toUtf8 "hello" + when Num.bytesToU16 bytes 234 is + Ok v -> v + Err OutOfBounds -> 1 + "# + ), + 1, + u16 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn bytes_to_u16_subtly_out_of_bounds() { + assert_evals_to!( + indoc!( + r#" + bytes = Str.toUtf8 "hello" + when Num.bytesToU16 bytes 4 is + Ok v -> v + Err OutOfBounds -> 1 + "# + ), + 1, + u16 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn bytes_to_u32_clearly_out_of_bounds() { + assert_evals_to!( + indoc!( + r#" + bytes = Str.toUtf8 "hello" + when Num.bytesToU32 bytes 234 is + Ok v -> v + Err OutOfBounds -> 1 + "# + ), + 1, + u32 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn bytes_to_u32_subtly_out_of_bounds() { + assert_evals_to!( + indoc!( + r#" + bytes = Str.toUtf8 "hello" + when Num.bytesToU32 bytes 2 is + Ok v -> v + Err OutOfBounds -> 1 + "# + ), + 1, + u32 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn bytes_to_u64_clearly_out_of_bounds() { + assert_evals_to!( + indoc!( + r#" + bytes = Str.toUtf8 "hello" + when Num.bytesToU64 bytes 234 is + Ok v -> v + Err OutOfBounds -> 1 + "# + ), + 1, + u64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn bytes_to_u64_subtly_out_of_bounds() { + assert_evals_to!( + indoc!( + r#" + bytes = Str.toUtf8 "hello world" + when Num.bytesToU64 bytes 4 is + Ok v -> v + Err OutOfBounds -> 1 + "# + ), + 1, + u64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn bytes_to_u128_clearly_out_of_bounds() { + assert_evals_to!( + indoc!( + r#" + bytes = Str.toUtf8 "hello" + when Num.bytesToU128 bytes 234 is + Ok v -> v + Err OutOfBounds -> 1 + "# + ), + 1, + u128 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn bytes_to_u128_subtly_out_of_bounds() { + assert_evals_to!( + indoc!( + r#" + bytes = Str.toUtf8 "hello world!!!!!!" + when Num.bytesToU128 bytes 2 is + Ok v -> v + Err OutOfBounds -> 1 + "# + ), + 1, + u128 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn bytes_to_u16_max_u8s() { + assert_evals_to!( + indoc!( + r#" + when Num.bytesToU16 [255, 255] 0 is + Ok v -> v + Err OutOfBounds -> 1 + "# + ), + 65535, + u16 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn bytes_to_u16_min_u8s() { + assert_evals_to!( + indoc!( + r#" + when Num.bytesToU16 [0, 0] 0 is + Ok v -> v + Err OutOfBounds -> 1 + "# + ), + 0, + u16 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn bytes_to_u16_random_u8s() { + assert_evals_to!( + indoc!( + r#" + when Num.bytesToU16 [164, 215] 0 is + Ok v -> v + Err OutOfBounds -> 1 + "# + ), + 55_204, + u16 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn bytes_to_u32_min_u8s() { + assert_evals_to!( + indoc!( + r#" + when Num.bytesToU32 [0, 0, 0, 0] 0 is + Ok v -> v + Err OutOfBounds -> 1 + "# + ), + 0, + u32 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn bytes_to_u32_max_u8s() { + assert_evals_to!( + indoc!( + r#" + when Num.bytesToU32 [255, 255, 255, 255] 0 is + Ok v -> v + Err OutOfBounds -> 1 + "# + ), + 4_294_967_295, + u32 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn bytes_to_u32_random_u8s() { + assert_evals_to!( + indoc!( + r#" + when Num.bytesToU32 [252, 124, 128, 121] 0 is + Ok v -> v + Err OutOfBounds -> 1 + "# + ), + 2_038_463_740, + u32 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn bytes_to_u64_min_u8s() { + assert_evals_to!( + indoc!( + r#" + when Num.bytesToU64 [0, 0, 0, 0, 0, 0, 0, 0] 0 is + Ok v -> v + Err OutOfBounds -> 1 + "# + ), + 0, + u64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn bytes_to_u64_max_u8s() { + assert_evals_to!( + indoc!( + r#" + when Num.bytesToU64 [255, 255, 255, 255, 255, 255, 255, 255] 0 is + Ok v -> v + Err OutOfBounds -> 1 + "# + ), + 18_446_744_073_709_551_615, + u64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn bytes_to_u64_random_u8s() { + assert_evals_to!( + indoc!( + r#" + when Num.bytesToU64 [252, 124, 128, 121, 1, 32, 177, 211] 0 is + Ok v -> v + Err OutOfBounds -> 1 + "# + ), + 15_254_008_603_586_100_476, + u64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn bytes_to_u128_min_u8s() { + assert_evals_to!( + indoc!( + r#" + when Num.bytesToU128 [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] 0 is + Ok v -> v + Err OutOfBounds -> 1 + "# + ), + 0, + u128 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn bytes_to_u128_max_u8s() { + assert_evals_to!( + indoc!( + r#" + when Num.bytesToU128 [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255] 0 is + Ok v -> v + Err OutOfBounds -> 1 + "# + ), + 340_282_366_920_938_463_463_374_607_431_768_211_455, + u128 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn bytes_to_u128_random_u8s() { + assert_evals_to!( + indoc!( + r#" + when Num.bytesToU128 [252, 124, 128, 121, 1, 32, 177, 211, 3, 57, 203, 122, 95, 164, 23, 145] 0 is + Ok v -> v + Err OutOfBounds -> 1 + "# + ), + 192_860_816_096_412_392_720_639_456_393_488_792_828, + u128 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn when_on_i32() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + x : I32 + x = 0 + + main : I32 + main = + when x is + 0 -> 42 + _ -> -1 + "# + ), + 42, + i32 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn when_on_i16() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + x : I16 + x = 0 + + main : I16 + main = + when x is + 0 -> 42 + _ -> -1 + "# + ), + 42, + i16 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn num_to_str() { + use roc_std::RocStr; + + assert_evals_to!(r#"Num.toStr 1234"#, RocStr::from("1234"), RocStr); + assert_evals_to!(r#"Num.toStr 0"#, RocStr::from("0"), RocStr); + assert_evals_to!(r#"Num.toStr -1"#, RocStr::from("-1"), RocStr); + + let max = format!("{}", i64::MAX); + assert_evals_to!( + r#"Num.toStr Num.maxI64"#, + RocStr::from(max.as_str()), + RocStr + ); + + let min = format!("{}", i64::MIN); + assert_evals_to!( + r#"Num.toStr Num.minI64"#, + RocStr::from(min.as_str()), + RocStr + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn num_to_str_u8() { + use roc_std::RocStr; + + assert_evals_to!(r#"Num.toStr 0u8"#, RocStr::from("0"), RocStr); + assert_evals_to!(r#"Num.toStr 1u8"#, RocStr::from("1"), RocStr); + assert_evals_to!(r#"Num.toStr 10u8"#, RocStr::from("10"), RocStr); + + let max = format!("{}", u8::MAX); + assert_evals_to!(r#"Num.toStr Num.maxU8"#, RocStr::from(max.as_str()), RocStr); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn num_to_str_u16() { + use roc_std::RocStr; + + assert_evals_to!(r#"Num.toStr 0u16"#, RocStr::from("0"), RocStr); + assert_evals_to!(r#"Num.toStr 1u16"#, RocStr::from("1"), RocStr); + assert_evals_to!(r#"Num.toStr 10u16"#, RocStr::from("10"), RocStr); + + let max = format!("{}", u16::MAX); + assert_evals_to!( + r#"Num.toStr Num.maxU16"#, + RocStr::from(max.as_str()), + RocStr + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn num_to_str_u32() { + use roc_std::RocStr; + + assert_evals_to!(r#"Num.toStr 0u32"#, RocStr::from("0"), RocStr); + assert_evals_to!(r#"Num.toStr 1u32"#, RocStr::from("1"), RocStr); + assert_evals_to!(r#"Num.toStr 10u32"#, RocStr::from("10"), RocStr); + + let max = format!("{}", u32::MAX); + assert_evals_to!( + r#"Num.toStr Num.maxU32"#, + RocStr::from(max.as_str()), + RocStr + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn num_to_str_u64() { + use roc_std::RocStr; + + assert_evals_to!(r#"Num.toStr 0u64"#, RocStr::from("0"), RocStr); + assert_evals_to!(r#"Num.toStr 1u64"#, RocStr::from("1"), RocStr); + assert_evals_to!(r#"Num.toStr 10u64"#, RocStr::from("10"), RocStr); + + let max = format!("{}", u64::MAX); + assert_evals_to!( + r#"Num.toStr Num.maxU64"#, + RocStr::from(max.as_str()), + RocStr + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn num_to_str_i8() { + use roc_std::RocStr; + + assert_evals_to!(r#"Num.toStr -10i8"#, RocStr::from("-10"), RocStr); + assert_evals_to!(r#"Num.toStr -1i8"#, RocStr::from("-1"), RocStr); + assert_evals_to!(r#"Num.toStr 0i8"#, RocStr::from("0"), RocStr); + assert_evals_to!(r#"Num.toStr 1i8"#, RocStr::from("1"), RocStr); + assert_evals_to!(r#"Num.toStr 10i8"#, RocStr::from("10"), RocStr); + + let max = format!("{}", i8::MAX); + assert_evals_to!(r#"Num.toStr Num.maxI8"#, RocStr::from(max.as_str()), RocStr); + + let max = format!("{}", i8::MIN); + assert_evals_to!(r#"Num.toStr Num.minI8"#, RocStr::from(max.as_str()), RocStr); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn num_to_str_i16() { + use roc_std::RocStr; + + assert_evals_to!(r#"Num.toStr -10i16"#, RocStr::from("-10"), RocStr); + assert_evals_to!(r#"Num.toStr -1i16"#, RocStr::from("-1"), RocStr); + assert_evals_to!(r#"Num.toStr 0i16"#, RocStr::from("0"), RocStr); + assert_evals_to!(r#"Num.toStr 1i16"#, RocStr::from("1"), RocStr); + assert_evals_to!(r#"Num.toStr 10i16"#, RocStr::from("10"), RocStr); + + let max = format!("{}", i16::MAX); + assert_evals_to!( + r#"Num.toStr Num.maxI16"#, + RocStr::from(max.as_str()), + RocStr + ); + + let max = format!("{}", i16::MIN); + assert_evals_to!( + r#"Num.toStr Num.minI16"#, + RocStr::from(max.as_str()), + RocStr + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn num_to_str_i32() { + use roc_std::RocStr; + + assert_evals_to!(r#"Num.toStr -10i32"#, RocStr::from("-10"), RocStr); + assert_evals_to!(r#"Num.toStr -1i32"#, RocStr::from("-1"), RocStr); + assert_evals_to!(r#"Num.toStr 0i32"#, RocStr::from("0"), RocStr); + assert_evals_to!(r#"Num.toStr 1i32"#, RocStr::from("1"), RocStr); + assert_evals_to!(r#"Num.toStr 10i32"#, RocStr::from("10"), RocStr); + + let max = format!("{}", i32::MAX); + assert_evals_to!( + r#"Num.toStr Num.maxI32"#, + RocStr::from(max.as_str()), + RocStr + ); + + let max = format!("{}", i32::MIN); + assert_evals_to!( + r#"Num.toStr Num.minI32"#, + RocStr::from(max.as_str()), + RocStr + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn num_to_str_i64() { + use roc_std::RocStr; + + assert_evals_to!(r#"Num.toStr -10i64"#, RocStr::from("-10"), RocStr); + assert_evals_to!(r#"Num.toStr -1i64"#, RocStr::from("-1"), RocStr); + assert_evals_to!(r#"Num.toStr 0i64"#, RocStr::from("0"), RocStr); + assert_evals_to!(r#"Num.toStr 1i64"#, RocStr::from("1"), RocStr); + assert_evals_to!(r#"Num.toStr 10i64"#, RocStr::from("10"), RocStr); + + let max = format!("{}", i64::MAX); + assert_evals_to!( + r#"Num.toStr Num.maxI64"#, + RocStr::from(max.as_str()), + RocStr + ); + + let max = format!("{}", i64::MIN); + assert_evals_to!( + r#"Num.toStr Num.minI64"#, + RocStr::from(max.as_str()), + RocStr + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn num_to_str_f32() { + use roc_std::RocStr; + + assert_evals_to!(r#"Num.toStr -10.75f32"#, RocStr::from("-10.75"), RocStr); + assert_evals_to!(r#"Num.toStr -1.75f32"#, RocStr::from("-1.75"), RocStr); + assert_evals_to!(r#"Num.toStr 0f32"#, RocStr::from("0"), RocStr); + assert_evals_to!(r#"Num.toStr 1.75f32"#, RocStr::from("1.75"), RocStr); + assert_evals_to!(r#"Num.toStr 10.75f32"#, RocStr::from("10.75"), RocStr); + + assert_evals_to!( + r#"Num.toStr Num.maxF32"#, + RocStr::from("340282346638528860000000000000000000000"), + RocStr + ); + + assert_evals_to!( + r#"Num.toStr Num.minF32"#, + RocStr::from("-340282346638528860000000000000000000000"), + RocStr + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn num_to_str_f64() { + use roc_std::RocStr; + + assert_evals_to!(r#"Num.toStr -10.75f64"#, RocStr::from("-10.75"), RocStr); + assert_evals_to!(r#"Num.toStr -1.75f64"#, RocStr::from("-1.75"), RocStr); + assert_evals_to!(r#"Num.toStr 0f64"#, RocStr::from("0"), RocStr); + assert_evals_to!(r#"Num.toStr 1.75f64"#, RocStr::from("1.75"), RocStr); + assert_evals_to!(r#"Num.toStr 10.75f64"#, RocStr::from("10.75"), RocStr); + + assert_evals_to!( + r#"Num.toStr Num.maxF64"#, + RocStr::from(f64::MAX.to_string().as_str()), + RocStr + ); + + assert_evals_to!( + r#"Num.toStr Num.minF64"#, + RocStr::from(f64::MIN.to_string().as_str()), + RocStr + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn num_to_str_dec() { + use roc_std::RocStr; + + assert_evals_to!(r#"Num.toStr -10.75dec"#, RocStr::from("-10.75"), RocStr); + assert_evals_to!(r#"Num.toStr -1.75dec"#, RocStr::from("-1.75"), RocStr); + assert_evals_to!(r#"Num.toStr 0dec"#, RocStr::from("0.0"), RocStr); + assert_evals_to!(r#"Num.toStr 1.75dec"#, RocStr::from("1.75"), RocStr); + assert_evals_to!(r#"Num.toStr 10.75dec"#, RocStr::from("10.75"), RocStr); + + assert_evals_to!( + r#"Num.toStr 170141183460469.105727dec"#, + RocStr::from("170141183460469.105727"), + RocStr + ); + + assert_evals_to!( + r#"Num.toStr -170141183460469.105727dec"#, + RocStr::from("-170141183460469.105727"), + RocStr + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn u8_addition_greater_than_i8() { + assert_evals_to!( + indoc!( + r#" + x : U8 + x = 100 + y : U8 + y = 100 + x + y + "# + ), + 200, + u8 + ) +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn u8_sub_greater_than_i8() { + assert_evals_to!( + indoc!( + r#" + x : U8 + x = 255 + y : U8 + y = 55 + x - y + "# + ), + 200, + u8 + ) +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn u8_mul_greater_than_i8() { + assert_evals_to!( + indoc!( + r#" + x : U8 + x = 40 + y : U8 + y = 5 + x * y + "# + ), + 200, + u8 + ) +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn add_saturated() { + assert_evals_to!( + indoc!( + r#" + x : U8 + x = 200 + y : U8 + y = 200 + Num.addSaturated x y + "# + ), + 255, + u8 + ); + + assert_evals_to!( + indoc!( + r#" + x : I8 + x = 100 + y : I8 + y = 100 + Num.addSaturated x y + "# + ), + 127, + i8 + ); + + assert_evals_to!( + indoc!( + r#" + x : I8 + x = -100 + y : I8 + y = -100 + Num.addSaturated x y + "# + ), + -128, + i8 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn sub_saturated() { + assert_evals_to!( + indoc!( + r#" + x : U8 + x = 10 + y : U8 + y = 20 + Num.subSaturated x y + "# + ), + 0, + u8 + ); + assert_evals_to!( + indoc!( + r#" + x : I8 + x = -100 + y : I8 + y = 100 + Num.subSaturated x y + "# + ), + -128, + i8 + ); + assert_evals_to!( + indoc!( + r#" + x : I8 + x = 100 + y : I8 + y = -100 + Num.subSaturated x y + "# + ), + 127, + i8 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn mul_saturated() { + assert_evals_to!( + indoc!( + r#" + x : U8 + x = 20 + y : U8 + y = 20 + Num.mulSaturated x y + "# + ), + 255, + u8 + ); + assert_evals_to!( + indoc!( + r#" + x : I8 + x = -20 + y : I8 + y = -20 + Num.mulSaturated x y + "# + ), + 127, + i8 + ); + assert_evals_to!( + indoc!( + r#" + x : I8 + x = 20 + y : I8 + y = -20 + Num.mulSaturated x y + "# + ), + -128, + i8 + ); + assert_evals_to!( + indoc!( + r#" + x : I8 + x = -20 + y : I8 + y = 20 + Num.mulSaturated x y + "# + ), + -128, + i8 + ); + assert_evals_to!( + indoc!( + r#" + x : I8 + x = 20 + y : I8 + y = 20 + Num.mulSaturated x y + "# + ), + 127, + i8 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn monomorphized_ints() { + assert_evals_to!( + indoc!( + r#" + x = 100 + + f : U8, U32 -> Nat + f = \_, _ -> 18 + + f x x + "# + ), + 18, + usize + ) +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn monomorphized_floats() { + assert_evals_to!( + indoc!( + r#" + x = 100.0 + + f : F32, F64 -> Nat + f = \_, _ -> 18 + + f x x + "# + ), + 18, + usize + ) +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn monomorphized_ints_names_dont_conflict() { + assert_evals_to!( + indoc!( + r#" + f : U8 -> Nat + f = \_ -> 9 + x = + n = 100 + f n + + y = + n = 100 + f n + + x + y + "# + ), + 18, + usize + ) +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn monomorphized_ints_aliased() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + main = + y = \{} -> 100 + w1 = \{} -> y {} + w2 = \{} -> y {} + + f1 : U8, U32 -> U8 + f1 = \_, _ -> 1 + + f2 : U32, U8 -> U8 + f2 = \_, _ -> 2 + + f1 (w1 {}) (w2 {}) + f2 (w1 {}) (w2 {}) + "# + ), + 3, + u8 + ) +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn to_float_f32() { + assert_evals_to!( + indoc!( + r#" + n : U8 + n = 100 + + f : F32 + f = Num.toFrac n + f + "# + ), + 100., + f32 + ) +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn to_float_f64() { + assert_evals_to!( + indoc!( + r#" + n : U8 + n = 100 + + f : F64 + f = Num.toFrac n + f + "# + ), + 100., + f64 + ) +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +// https://github.com/roc-lang/roc/issues/2696 +fn upcast_of_int_is_zext() { + assert_evals_to!( + indoc!( + r#" + Num.toU16 0b1000_0000u8 + "# + ), + 128, + u16 + ) +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +// https://github.com/roc-lang/roc/issues/2696 +fn upcast_of_int_checked_is_zext() { + assert_evals_to!( + indoc!( + r#" + when Num.toU16Checked 0b1000_0000u8 is + Ok 128u16 -> 1u8 + _ -> 0u8 + "# + ), + 1, + u8 + ) +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn modulo_of_unsigned() { + assert_evals_to!( + indoc!( + r#" + 0b1111_1111u8 % 64 + "# + ), + 63, + u8 + ) +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn div_of_unsigned() { + assert_evals_to!( + indoc!( + r#" + 0b1111_1111u8 // 2 + "# + ), + 127, + u8 + ) +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn dec_float_suffix() { + assert_evals_to!( + indoc!( + r#" + 123.0dec + "# + ), + RocDec::from_str_to_i128_unsafe("123.0"), + i128 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn dec_no_decimal() { + assert_evals_to!( + indoc!( + r#" + 3dec + "# + ), + RocDec::from_str_to_i128_unsafe("3.0"), + i128 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn ceiling_to_u32() { + assert_evals_to!( + indoc!( + r#" + n : U32 + n = Num.ceiling 124.5f64 + n + "# + ), + 125, + u32 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn floor_to_u32() { + assert_evals_to!( + indoc!( + r#" + n : U32 + n = Num.floor 124.5f64 + n + "# + ), + 124, + u32 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn round_to_u32() { + assert_evals_to!( + indoc!( + r#" + n : U32 + n = Num.round 124.49f64 + n + "# + ), + 124, + u32 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn promote_u64_number_layout() { + assert_evals_to!( + indoc!( + r#" + 9999999999999999999 + 1 + "# + ), + 10000000000000000000, + u64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn promote_i128_number_layout() { + assert_evals_to!( + indoc!( + r#" + { + a: 18446744073709551616 + 1, + b: -9223372036854775809 + 1, + } + "# + ), + (18446744073709551617, -9223372036854775808), + (i128, i128) + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn promote_u128_number_layout() { + assert_evals_to!( + indoc!( + r#" + 170141183460469231731687303715884105728 + 1 + "# + ), + 170141183460469231731687303715884105729, + u128 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn when_on_decimals() { + assert_evals_to!( + indoc!( + r#" + when 42.42dec is + 42.42 -> 42 + 0.05 -> 1 + 3.14 -> 2 + _ -> 4 + "# + ), + 42, + i64 + ); + + assert_evals_to!( + indoc!( + r#" + when 42.42dec is + 0.05 -> 1 + 3.14 -> 2 + _ -> 4 + "# + ), + 4, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn when_on_i128() { + assert_evals_to!( + indoc!( + r#" + when 1701411834604692317316873037158841057i128 is + 1701411834604692317316873037158841057 -> 42 + 32 -> 1 + 64 -> 2 + _ -> 4 + "# + ), + 42, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn when_on_u128() { + assert_evals_to!( + indoc!( + r#" + when 170141183460469231731687303715884105728u128 is + 170141183460469231731687303715884105728u128 -> 42 + 32 -> 1 + 64 -> 2 + _ -> 4 + "# + ), + 42, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn condition_polymorphic_num_becomes_float() { + assert_evals_to!( + indoc!( + r#" + x = if Bool.true then 2 else 3 + x * 5f32 + "# + ), + 10., + f32 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn num_count_leading_zero_bits() { + assert_evals_to!(r#"Num.countLeadingZeroBits 0b0010_1000u8"#, 2, usize); + assert_evals_to!(r#"Num.countLeadingZeroBits 0b0010_1000u16"#, 10, usize); + assert_evals_to!(r#"Num.countLeadingZeroBits 0b0010_1000u32"#, 26, usize); + assert_evals_to!(r#"Num.countLeadingZeroBits 0b0010_1000u64"#, 58, usize); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn num_count_trailing_zero_bits() { + assert_evals_to!(r#"Num.countTrailingZeroBits 0b0010_1000u8"#, 3, usize); + assert_evals_to!(r#"Num.countTrailingZeroBits 0b0010_0000u16"#, 5, usize); + assert_evals_to!(r#"Num.countTrailingZeroBits 0u32"#, 32, usize); + assert_evals_to!(r#"Num.countTrailingZeroBits 0b0010_1111u64"#, 0, usize); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn num_count_one_bits() { + assert_evals_to!(r#"Num.countOneBits 0b0010_1000u8"#, 2, usize); + assert_evals_to!(r#"Num.countOneBits 0b0010_0000u16"#, 1, usize); + assert_evals_to!(r#"Num.countOneBits 0u32"#, 0, usize); + assert_evals_to!(r#"Num.countOneBits 0b0010_1111u64"#, 5, usize); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn num_abs_diff_int() { + assert_evals_to!(r#"Num.absDiff 0u8 0u8"#, 0, u8); + assert_evals_to!(r#"Num.absDiff 1u8 2u8"#, 1, u8); + assert_evals_to!(r#"Num.absDiff 2u8 1u8"#, 1, u8); + assert_evals_to!(r#"Num.absDiff -1 1"#, 2, i64); + assert_evals_to!(r#"Num.absDiff 1 -1"#, 2, i64); + assert_evals_to!(r#"Num.absDiff Num.minI64 -1"#, i64::MAX, i64); +} + +#[test] +#[cfg(feature = "gen-llvm")] +fn num_abs_diff_large_bits() { + assert_evals_to!(r#"Num.absDiff 0u128 0u128"#, 0, u128); + assert_evals_to!(r#"Num.absDiff 1u128 2u128"#, 1, u128); + assert_evals_to!(r#"Num.absDiff -1i128 1i128"#, 2, i128); + assert_evals_to!(r#"Num.absDiff Num.minI128 -1i128"#, i128::MAX, i128); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn num_abs_diff_float() { + assert_evals_to!(r#"Num.absDiff 0.0f64 0.0"#, 0.0, f64); + assert_evals_to!(r#"Num.absDiff 1.0f64 2.0"#, 1.0, f64); + assert_evals_to!(r#"Num.absDiff 2.0f64 1.0"#, 1.0, f64); + assert_evals_to!(r#"Num.absDiff -1.0f64 1.0"#, 2.0, f64); + assert_evals_to!(r#"Num.absDiff 1.0f64 -1.0"#, 2.0, f64); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +#[should_panic(expected = r#"Roc failed with message: "integer subtraction overflowed!"#)] +fn num_abs_max_overflow() { + assert_evals_to!(r#"Num.absDiff Num.maxI64 -1"#, 0, i64); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +#[should_panic(expected = r#"Roc failed with message: "integer subtraction overflowed!"#)] +fn num_abs_int_min_overflow() { + assert_evals_to!(r#"Num.absDiff Num.minI64 0"#, 0, i64); +} + +#[test] +#[cfg(feature = "gen-llvm")] +#[should_panic(expected = r#"Roc failed with message: "integer subtraction overflowed!"#)] +fn num_abs_large_bits_min_overflow() { + assert_evals_to!(r#"Num.absDiff Num.minI128 0"#, 0, i128); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn num_abs_float_overflow() { + assert_evals_to!("Num.absDiff Num.maxF64 Num.minF64", f64::INFINITY, f64); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))] +fn bool_in_switch() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + loop : [ Continue {}, Break {} ] + loop = Continue {} + + all = \{} -> + when loop is + Continue {} -> Bool.true + Break {} -> Bool.false + + main = all {} + "# + ), + true, + bool + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn add_checked_dec() { + assert_evals_to!( + indoc!( + r#" + Num.addChecked 2.0dec 4.0dec + "# + ), + RocResult::ok(RocDec::from(6)), + RocResult + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn sub_checked_dec() { + assert_evals_to!( + indoc!( + r#" + Num.subChecked 5.0dec 2.0dec + "# + ), + RocResult::ok(RocDec::from(3)), + RocResult + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn mul_checked_dec() { + assert_evals_to!( + indoc!( + r#" + Num.mulChecked 5.0dec 2.0dec + "# + ), + RocResult::ok(RocDec::from_str("10.0").unwrap()), + RocResult + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn mul_checked_u128() { + assert_evals_to!( + indoc!( + r#" + x : Result U128 [ Overflow ] + x = Num.mulChecked 5u128 2u128 + + x + "# + ), + RocResult::ok(5u128 * 2u128), + RocResult + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn sub_checked_u128() { + assert_evals_to!( + indoc!( + r#" + x : Result U128 [ Overflow ] + x = Num.subChecked 5u128 2u128 + + x + "# + ), + RocResult::ok(5u128 - 2u128), + RocResult + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn add_checked_u128() { + assert_evals_to!( + indoc!( + r#" + x : Result U128 [ Overflow ] + x = Num.addChecked 5u128 2u128 + + x + "# + ), + RocResult::ok(5u128 + 2u128), + RocResult + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))] +fn num_min() { + assert_evals_to!(r#"Num.min 0 0"#, 0, i64); + assert_evals_to!(r#"Num.min 1 2"#, 1, i64); + assert_evals_to!(r#"Num.min 2 1"#, 1, i64); + assert_evals_to!(r#"Num.min 2 -2"#, -2, i64); + assert_evals_to!(r#"Num.min -2 2"#, -2, i64); + assert_evals_to!(r#"Num.min Num.minI64 Num.maxI64"#, i64::MIN, i64); + assert_evals_to!(r#"Num.min Num.maxI64 Num.minI64"#, i64::MIN, i64); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))] +fn num_max() { + assert_evals_to!(r#"Num.max 0 0"#, 0, i64); + assert_evals_to!(r#"Num.max 1 2"#, 2, i64); + assert_evals_to!(r#"Num.max 2 1"#, 2, i64); + assert_evals_to!(r#"Num.max 2 -2"#, 2, i64); + assert_evals_to!(r#"Num.max -2 2"#, 2, i64); + assert_evals_to!(r#"Num.max Num.minI64 Num.maxI64"#, i64::MAX, i64); + assert_evals_to!(r#"Num.max Num.maxI64 Num.minI64"#, i64::MAX, i64); +} diff --git a/crates/compiler/test_gen/src/gen_panic.rs b/crates/compiler/test_gen/src/gen_panic.rs new file mode 100644 index 0000000000..b39c945b76 --- /dev/null +++ b/crates/compiler/test_gen/src/gen_panic.rs @@ -0,0 +1,88 @@ +use indoc::indoc; +use roc_std::RocList; + +#[cfg(feature = "gen-llvm")] +use crate::helpers::llvm::assert_evals_to; + +#[cfg(feature = "gen-wasm")] +use crate::helpers::wasm::assert_evals_to; + +#[cfg(feature = "gen-dev")] +use crate::helpers::dev::assert_evals_to; + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +#[should_panic = r#"User crash with message: "hello crash""#] +fn crash_literal() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + main = if Bool.true then crash "hello crash" else 1u8 + "# + ), + 1u8, + u8 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +#[should_panic = r#"User crash with message: "hello crash""#] +fn crash_variable() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + main = + msg = "hello crash" + if Bool.true then crash msg else 1u8 + "# + ), + 1u8, + u8 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +#[should_panic = r#"User crash with message: "turns out this was fallible""#] +fn crash_in_call() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + getInfallible = \result -> when result is + Ok x -> x + _ -> crash "turns out this was fallible" + + main = + x : [Ok U64, Err Str] + x = Err "" + getInfallible x + "# + ), + 1u64, + u64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +#[should_panic = r#"User crash with message: "no new even primes""#] +fn crash_in_passed_closure() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + main = List.map [1, 2, 3] \n -> if n == 2 then crash "no new even primes" else "" + "# + ), + RocList::from_slice(&[1u8]), + RocList + ); +} diff --git a/crates/compiler/test_gen/src/gen_primitives.rs b/crates/compiler/test_gen/src/gen_primitives.rs new file mode 100644 index 0000000000..4509a2a60c --- /dev/null +++ b/crates/compiler/test_gen/src/gen_primitives.rs @@ -0,0 +1,4665 @@ +#[cfg(feature = "gen-llvm")] +use crate::helpers::llvm::assert_evals_to; + +#[cfg(feature = "gen-dev")] +use crate::helpers::dev::assert_evals_to; + +#[cfg(feature = "gen-wasm")] +use crate::helpers::wasm::assert_evals_to; + +use indoc::indoc; +#[allow(unused_imports)] +use roc_std::{RocBox, RocDec, RocList, RocStr}; + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn basic_int() { + assert_evals_to!("123", 123, i64); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn basic_float() { + assert_evals_to!("1234.0f64", 1234.0, f64); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn branch_first_float() { + assert_evals_to!( + indoc!( + r#" + when 1.23f64 is + 1.23 -> 12 + _ -> 34 + "# + ), + 12, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn branch_second_float() { + assert_evals_to!( + indoc!( + r#" + when 2.34 is + 1.23 -> 63 + _ -> 48 + "# + ), + 48, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn branch_third_float() { + assert_evals_to!( + indoc!( + r#" + when 10.0 is + 1.0 -> 63 + 2.0 -> 48 + _ -> 112 + "# + ), + 112, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn branch_first_int() { + assert_evals_to!( + indoc!( + r#" + when 1 is + 1 -> 12 + _ -> 34 + "# + ), + 12, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn branch_second_int() { + assert_evals_to!( + indoc!( + r#" + when 2 is + 1 -> 63 + _ -> 48 + "# + ), + 48, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev"))] +fn branch_third_int() { + assert_evals_to!( + indoc!( + r#" + when 10 is + 1 -> 63 + 2 -> 48 + _ -> 112 + "# + ), + 112, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn branch_store_variable() { + assert_evals_to!( + indoc!( + r#" + when 0 is + 1 -> 12 + a -> a + "# + ), + 0, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn when_one_element_tag() { + assert_evals_to!( + indoc!( + r#" + x : [Pair (Int a) (Int a)] + x = Pair 0x2 0x3 + + when x is + Pair l r -> l + r + "# + ), + 5, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn when_two_element_tag_first() { + assert_evals_to!( + indoc!( + r#" + x : [A (Int a), B (Int a)] + x = A 0x2 + + when x is + A v -> v + B v -> v + "# + ), + 2, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn when_two_element_tag_second() { + assert_evals_to!( + indoc!( + r#" + x : [A (Int a), B (Int a)] + x = B 0x3 + + when x is + A v -> v + B v -> v + "# + ), + 3, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn gen_when_one_branch() { + assert_evals_to!( + indoc!( + r#" + when 1.23 is + _ -> 23 + "# + ), + 23, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn gen_large_when_int() { + assert_evals_to!( + indoc!( + r#" + foo = \num -> + when num is + 0 -> 200 + -3 -> 111 # TODO adding more negative numbers reproduces parsing bugs here + 3 -> 789 + 1 -> 123 + 2 -> 456 + _ -> 1000 + + foo -3 + "# + ), + 111, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn gen_large_when_float() { + assert_evals_to!( + indoc!( + r#" + foo = \num -> + when num is + 0.5f64 -> 200.1 + -3.6 -> 111.2 # TODO adding more negative numbers reproduces parsing bugs here + 3.6 -> 789.5 + 1.7 -> 123.3 + 2.8 -> 456.4 + _ -> 1000.6f64 + + foo -3.6 + "# + ), + 111.2, + f64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn or_pattern() { + assert_evals_to!( + indoc!( + r#" + when 2 is + 1 | 2 -> 42 + _ -> 1 + "# + ), + 42, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn apply_identity() { + assert_evals_to!( + indoc!( + r#" + identity = \a -> a + + identity 5 + "# + ), + 5, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn apply_unnamed_identity() { + assert_evals_to!( + indoc!( + r#" + wrapper = \{} -> + (\a -> a) 5 + + wrapper {} + "# + ), + 5, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn return_unnamed_fn() { + assert_evals_to!( + indoc!( + r#" + wrapper = \{} -> + alwaysFloatIdentity : Int * -> (Frac a -> Frac a) + alwaysFloatIdentity = \_ -> + (\a -> a) + + (alwaysFloatIdentity 2) 1.23f64 + + wrapper {} + "# + ), + 1.23, + f64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn gen_when_fn() { + assert_evals_to!( + indoc!( + r#" + limitedNegate = \num -> + when num is + 1 -> -1 + -1 -> 1 + _ -> num + + limitedNegate 1 + "# + ), + -1, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn gen_basic_def() { + assert_evals_to!( + indoc!( + r#" + answer = 42 + + answer + "# + ), + 42, + i64 + ); + + assert_evals_to!( + indoc!( + r#" + float = 1.23f64 + + float + "# + ), + 1.23, + f64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn gen_multiple_defs() { + assert_evals_to!( + indoc!( + r#" + answer = 42 + + float = 1.23f64 + + if float > 3 then answer else answer + "# + ), + 42, + i64 + ); + + assert_evals_to!( + indoc!( + r#" + answer = 42 + + float = 1.23f64 + + if answer > 3 then float else float + "# + ), + 1.23, + f64 + ); +} + +// These tests caught a bug in how Defs are converted to the mono IR +// but they have UnusedDef or UnusedArgument problems, and don't run any more +// #[test] +// #[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +// fn gen_chained_defs() { +// assert_evals_to!( +// indoc!( +// r#" +// x = i1 +// i3 = i2 +// i1 = 1337 +// i2 = i1 +// y = 12.4 +// +// i3 +// "# +// ), +// 1337, +// i64 +// ); +// } +// +// #[test] +// #[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +// fn gen_nested_defs_old() { +// assert_evals_to!( +// indoc!( +// r#" +// x = 5 +// +// answer = +// i3 = i2 +// +// nested = +// a = 1.0 +// b = 5 +// +// i1 +// +// i1 = 1337 +// i2 = i1 +// +// +// nested +// +// # None of this should affect anything, even though names +// # overlap with the previous nested defs +// unused = +// nested = 17 +// +// i1 = 84.2 +// +// nested +// +// y = 12.4 +// +// answer +// "# +// ), +// 1337, +// i64 +// ); +// } +// +// #[test] +// #[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +// fn let_x_in_x() { +// assert_evals_to!( +// indoc!( +// r#" +// x = 5 +// +// answer = +// 1337 +// +// unused = +// nested = 17 +// nested +// +// answer +// "# +// ), +// 1337, +// i64 +// ); +// } + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn factorial() { + assert_evals_to!( + indoc!( + r#" + factorial = \n, accum -> + when n is + 0 -> + accum + + _ -> + factorial (n - 1) (n * accum) + + factorial 10 1 + "# + ), + 3628800, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn peano1() { + assert_evals_to!( + indoc!( + r#" + Peano : [S Peano, Z] + + three : Peano + three = S (S (S Z)) + + when three is + Z -> 2 + S _ -> 1 + "# + ), + 1, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn peano2() { + assert_evals_to!( + indoc!( + r#" + Peano : [S Peano, Z] + + three : Peano + three = S (S (S Z)) + + when three is + S (S _) -> 1 + S (_) -> 0 + Z -> 0 + "# + ), + 1, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn top_level_constant() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + float = 1.2315f64 + + main = + float + float + "# + ), + 1.2315 + 1.2315, + f64 + ); +} + +#[test] +#[ignore] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn top_level_destructure() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + {a, b} = { a: 1, b: 2 } + + main = + + a + b + "# + ), + 3, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn linked_list_len_0() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + LinkedList a : [Nil, Cons a (LinkedList a)] + + len : LinkedList a -> Int * + len = \list -> + when list is + Nil -> 0 + Cons _ rest -> 1 + len rest + + main = + nil : LinkedList F64 + nil = Nil + + len nil + "# + ), + 0, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn linked_list_len_twice_0() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + LinkedList a : [Nil, Cons a (LinkedList a)] + + nil : LinkedList I64 + nil = Nil + + length : LinkedList a -> Int * + length = \list -> + when list is + Nil -> 0 + Cons _ rest -> 1 + length rest + + main = + length nil + length nil + "# + ), + 0, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn linked_list_len_1() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + LinkedList a : [Nil, Cons a (LinkedList a)] + + one : LinkedList (Int *) + one = Cons 1 Nil + + length : LinkedList a -> Int * + length = \list -> + when list is + Nil -> 0 + Cons _ rest -> 1 + length rest + + main = + length one + "# + ), + 1, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn linked_list_len_twice_1() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + LinkedList a : [Nil, Cons a (LinkedList a)] + + one : LinkedList (Int *) + one = Cons 1 Nil + + length : LinkedList a -> Int * + length = \list -> + when list is + Nil -> 0 + Cons _ rest -> 1 + length rest + + main = + length one + length one + "# + ), + 2, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn linked_list_len_3() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + LinkedList a : [Nil, Cons a (LinkedList a)] + + three : LinkedList (Int *) + three = Cons 3 (Cons 2 (Cons 1 Nil)) + + length : LinkedList a -> Int * + length = \list -> + when list is + Nil -> 0 + Cons _ rest -> 1 + length rest + + + main = + length three + "# + ), + 3, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn linked_list_sum_num_a() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + LinkedList a : [Nil, Cons a (LinkedList a)] + + three : LinkedList (Int *) + three = Cons 3 (Cons 2 (Cons 1 Nil)) + + + sum : LinkedList (Num a) -> Num a + sum = \list -> + when list is + Nil -> 0 + Cons x rest -> x + sum rest + + main = + sum three + "# + ), + 3 + 2 + 1, + i64 + ) +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn linked_list_sum_int() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + LinkedList a : [Nil, Cons a (LinkedList a)] + + zero : LinkedList (Int *) + zero = Nil + + sum : LinkedList (Int a) -> Int a + sum = \list -> + when list is + Nil -> 0 + Cons x rest -> x + sum rest + + main = + sum zero + "# + ), + 0, + i64 + ) +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn linked_list_map() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + LinkedList a : [Nil, Cons a (LinkedList a)] + + three : LinkedList (Int *) + three = Cons 3 (Cons 2 (Cons 1 Nil)) + + sum : LinkedList (Num a) -> Num a + sum = \list -> + when list is + Nil -> 0 + Cons x rest -> x + sum rest + + map : (a -> b), LinkedList a -> LinkedList b + map = \f, list -> + when list is + Nil -> Nil + Cons x rest -> Cons (f x) (map f rest) + + main = + sum (map (\_ -> 1) three) + "# + ), + 3, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn when_nested_maybe() { + assert_evals_to!( + indoc!( + r#" + Maybe a : [Nothing, Just a] + + x : Maybe (Maybe (Int a)) + x = Just (Just 41) + + when x is + Just (Just v) -> v + 0x1 + _ -> 0x1 + "# + ), + 42, + i64 + ); + + assert_evals_to!( + indoc!( + r#" + Maybe a : [Nothing, Just a] + + x : Maybe (Maybe (Int *)) + x = Just Nothing + + when x is + Just (Just v) -> v + 0x1 + Just Nothing -> 0x2 + Nothing -> 0x1 + "# + ), + 2, + i64 + ); + + assert_evals_to!( + indoc!( + r#" + Maybe a : [Nothing, Just a] + + x : Maybe (Maybe (Int *)) + x = Nothing + + when x is + Just (Just v) -> v + 0x1 + Just Nothing -> 0x2 + Nothing -> 0x1 + "# + ), + 1, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn when_peano() { + assert_evals_to!( + indoc!( + r#" + Peano : [S Peano, Z] + + three : Peano + three = S (S (S Z)) + + when three is + S (S _) -> 1 + S (_) -> 2 + Z -> 3 + "# + ), + 1, + i64 + ); + + assert_evals_to!( + indoc!( + r#" + Peano : [S Peano, Z] + + three : Peano + three = S Z + + when three is + S (S _) -> 1 + S (_) -> 2 + Z -> 3 + "# + ), + 2, + i64 + ); + + assert_evals_to!( + indoc!( + r#" + Peano : [S Peano, Z] + + three : Peano + three = Z + + when three is + S (S _) -> 1 + S (_) -> 2 + Z -> 3 + "# + ), + 3, + i64 + ); +} + +#[test] +// This doesn't work on Windows. If you make it return a Result.withDefault 0 (so it's returning +// an integer instead of a Result), then it works on Windows, suggesting this is a C ABI issue. +// We should try this out on Windows again after making adjustments to the Result C ABI! +#[cfg(all( + not(target_family = "windows"), + any(feature = "gen-llvm", feature = "gen-wasm") +))] +#[should_panic(expected = "Roc failed with message: ")] +fn overflow_frees_list() { + assert_evals_to!( + indoc!( + r#" + myList = [1,2,3] + + # integer overflow; must use the list so it is defined before the overflow + # the list will then be freed in a cleanup block + n : I64 + n = 9_223_372_036_854_775_807 + (Num.intCast (List.len myList)) + + index : Nat + index = Num.intCast n + + List.get myList index + "# + ), + (3, 0), + (i64, i8) + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +#[should_panic(expected = "Roc failed with message: ")] +fn undefined_variable() { + assert_evals_to!( + indoc!( + r#" + if Bool.true then + x + z + else + y + z + "# + ), + 3, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +#[should_panic(expected = "User crash with message: \"a crash\"")] +fn a_crash() { + assert_evals_to!( + indoc!( + r#" + if Bool.true then + crash "a crash" + else + 0u64 + "# + ), + 3, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +#[should_panic(expected = "Roc failed with message: ")] +fn annotation_without_body() { + assert_evals_to!( + indoc!( + r#" + foo : Int * + + + foo + "# + ), + 3, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn simple_closure() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + x = 42 + + f = \{} -> x + + + main = + f {} + "# + ), + 42, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn nested_closure() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + foo = \{} -> + x = 41 + y = 1 + f = \{} -> x + y + f + + main = + g = foo {} + g {} + "# + ), + 42, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn closure_in_list() { + use roc_std::RocList; + + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + foo = \{} -> + x = 41 + + f = \{} -> x + + [f] + + main = + items = foo {} + + items + "# + ), + RocList::from_slice(&[41]), + RocList + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn specialize_closure() { + use roc_std::RocList; + + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + foo = \{} -> + x = 41 + y = [1] + + f = \{} -> x + g = \{} -> x + Num.intCast (List.len y) + + [f, g] + + apply = \f -> f {} + + main = + items = foo {} + + List.map items apply + "# + ), + RocList::from_slice(&[41, 42]), + RocList + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn io_poc_effect() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + Effect a := {} -> a + + succeed : a -> Effect a + succeed = \x -> @Effect \{} -> x + + runEffect : Effect a -> a + runEffect = \@Effect thunk -> thunk {} + + foo : Effect F64 + foo = + succeed 1.23 + + main : F64 + main = + runEffect foo + + "# + ), + 1.23, + f64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn io_poc_desugared() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + # succeed : a -> ({} -> a) + succeed = \x -> \_ -> x + + foo : Str -> F64 + foo = + succeed 1.23 + + # runEffect : ({} -> a) -> a + runEffect = \thunk -> thunk "" + + main : F64 + main = + runEffect foo + "# + ), + 1.23, + f64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn return_wrapped_function_a() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + Effect a := {} -> a + + foo : Effect {} + foo = @Effect \{} -> {} + + main : Effect {} + main = foo + "# + ), + (), + () + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn return_wrapped_function_b() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + + foo : { x: (I64 -> Str) } + foo = { x: (\_ -> "foobar") } + + main : { x: (I64 -> Str) } + main = foo + "# + ), + (), + () + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn return_wrapped_closure() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + Effect a := {} -> a + + foo : Effect {} + foo = + x = 5 + + @Effect (\{} -> if x > 3 then {} else {}) + + main : Effect {} + main = foo + "# + ), + 5, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn linked_list_is_singleton() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + ConsList a : [Cons a (ConsList a), Nil] + + empty : {} -> ConsList a + empty = \{} -> Nil + + isSingleton : ConsList a -> Bool + isSingleton = \list -> + when list is + Cons _ Nil -> + Bool.true + + _ -> + Bool.false + + main : Bool + main = + myList : ConsList I64 + myList = empty {} + + isSingleton myList + "# + ), + false, + bool + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn linked_list_is_empty_1() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + ConsList a : [Cons a (ConsList a), Nil] + + empty : {} -> ConsList a + empty = \{} -> Nil + + isEmpty : ConsList a -> Bool + isEmpty = \list -> + when list is + Cons _ _ -> + Bool.false + + Nil -> + Bool.true + + main : Bool + main = + myList : ConsList U8 + myList = empty {} + + isEmpty myList + "# + ), + true, + bool + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn linked_list_is_empty_2() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + ConsList a : [Cons a (ConsList a), Nil] + + isEmpty : ConsList a -> Bool + isEmpty = \list -> + when list is + Cons _ _ -> + Bool.false + + Nil -> + Bool.true + + main : Bool + main = + myList : ConsList I64 + myList = Cons 0x1 Nil + + isEmpty myList + "# + ), + false, + bool + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn linked_list_singleton() { + // verifies only that valid llvm is produced + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + ConsList a : [Cons a (ConsList a), Nil] + + main : ConsList I64 + main = Cons 0x1 Nil + "# + ), + 0, + usize, + |_| 0 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn recursive_function_with_rigid() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + State a : { count : I64, x : a } + + foo : State a -> Int * + foo = \state -> + if state.count == 0 then + 0 + else + 1 + foo { count: state.count - 1, x: state.x } + + main : Int * + main = + foo { count: 3, x: {} } + "# + ), + 3, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn rbtree_insert() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + NodeColor : [Red, Black] + + RedBlackTree k v : [Node NodeColor k v (RedBlackTree k v) (RedBlackTree k v), Empty] + + Key k : Num k + + insert : Key k, v, RedBlackTree (Key k) v -> RedBlackTree (Key k) v + insert = \key, value, dict -> + when insertHelp key value dict is + Node Red k v l r -> + Node Black k v l r + + x -> + x + + insertHelp : (Key k), v, RedBlackTree (Key k) v -> RedBlackTree (Key k) v + insertHelp = \key, value, dict -> + when dict is + Empty -> + # New nodes are always red. If it violates the rules, it will be fixed + # when balancing. + Node Red key value Empty Empty + + Node nColor nKey nValue nLeft nRight -> + when Num.compare key nKey is + LT -> + balance nColor nKey nValue (insertHelp key value nLeft) nRight + + EQ -> + Node nColor nKey value nLeft nRight + + GT -> + balance nColor nKey nValue nLeft (insertHelp key value nRight) + + balance : NodeColor, k, v, RedBlackTree k v, RedBlackTree k v -> RedBlackTree k v + balance = \color, key, value, left, right -> + when right is + Node Red rK rV rLeft rRight -> + when left is + Node Red lK lV lLeft lRight -> + Node + Red + key + value + (Node Black lK lV lLeft lRight) + (Node Black rK rV rLeft rRight) + + _ -> + Node color rK rV (Node Red key value left rLeft) rRight + + _ -> + when left is + Node Red lK lV (Node Red llK llV llLeft llRight) lRight -> + Node + Red + lK + lV + (Node Black llK llV llLeft llRight) + (Node Black key value lRight right) + + _ -> + Node color key value left right + + show : RedBlackTree I64 {} -> Str + show = \tree -> + when tree is + Empty -> "Empty" + Node _ _ _ _ _ -> "Node" + + + main : Str + main = + show (insert 0 {} Empty) + "# + ), + RocStr::from("Node"), + RocStr + ); +} + +#[test] +#[cfg(all( + any(feature = "gen-llvm", feature = "gen-wasm"), + not(feature = "gen-llvm-wasm") +))] +fn rbtree_balance_3() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + RedBlackTree k : [Node k (RedBlackTree k) (RedBlackTree k), Empty] + + balance : k, RedBlackTree k -> RedBlackTree k + balance = \key, left -> + Node key left Empty + + main : RedBlackTree (Int *) + main = + balance 0 Empty + "# + ), + false, + usize, + |x: usize| x == 0 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +#[ignore] +fn rbtree_layout_issue() { + // there is a flex var in here somewhere that blows up layout creation + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + NodeColor : [Red, Black] + + RedBlackTree k v : [Node NodeColor k v (RedBlackTree k v) (RedBlackTree k v), Empty] + + # balance : NodeColor, k, v, RedBlackTree k v -> RedBlackTree k v + balance = \color, key, value, right -> + when right is + Node Red _ _ rLeft rRight -> + Node color key value rLeft rRight + + + _ -> + Empty + + show : RedBlackTree * * -> Str + show = \tree -> + when tree is + Empty -> "Empty" + Node _ _ _ _ _ -> "Node" + + zero : I64 + zero = 0 + + main : Str + main = show (balance Red zero zero Empty) + "# + ), + RocStr::from("Empty"), + RocStr + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +#[ignore] +fn rbtree_balance_mono_problem() { + // because of how the function is written, only `Red` is used and so in the function's + // type, the first argument is a unit and dropped. Apparently something is weird with + // constraint generation where the specialization required by `main` does not fix the + // problem. As a result, the first argument is dropped and we run into issues down the line + // + // concretely, the `rRight` symbol will not be defined + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + NodeColor : [Red, Black] + + RedBlackTree k v : [Node NodeColor k v (RedBlackTree k v) (RedBlackTree k v), Empty] + + # balance : NodeColor, k, v, RedBlackTree k v, RedBlackTree k v -> RedBlackTree k v + balance = \color, key, value, left, right -> + when right is + Node Red rK rV rLeft rRight -> + when left is + Node Red lK lV lLeft lRight -> + Node + Red + key + value + (Node Black lK lV lLeft lRight) + (Node Black rK rV rLeft rRight) + + _ -> + Node color rK rV (Node Red key value left rLeft) rRight + + _ -> + Empty + + show : RedBlackTree * * -> Str + show = \tree -> + when tree is + Empty -> "Empty" + Node _ _ _ _ _ -> "Node" + + + main : Str + main = show (balance Red 0 0 Empty Empty) + "# + ), + RocStr::from("Empty"), + RocStr + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn rbtree_balance_full() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + NodeColor : [Red, Black] + + RedBlackTree k v : [Node NodeColor k v (RedBlackTree k v) (RedBlackTree k v), Empty] + + balance : NodeColor, k, v, RedBlackTree k v, RedBlackTree k v -> RedBlackTree k v + balance = \color, key, value, left, right -> + when right is + Node Red rK rV rLeft rRight -> + when left is + Node Red lK lV lLeft lRight -> + Node + Red + key + value + (Node Black lK lV lLeft lRight) + (Node Black rK rV rLeft rRight) + + _ -> + Node color rK rV (Node Red key value left rLeft) rRight + + _ -> + when left is + Node Red lK lV (Node Red llK llV llLeft llRight) lRight -> + Node + Red + lK + lV + (Node Black llK llV llLeft llRight) + (Node Black key value lRight right) + + _ -> + Node color key value left right + + main : RedBlackTree F64 F64 + main = + balance Red 0 0 Empty Empty + "# + ), + true, + usize, + |x| x != 0 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn nested_pattern_match_two_ways() { + // exposed an issue in the ordering of pattern match checks when ran with `--release` mode + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + ConsList a : [Cons a (ConsList a), Nil] + + balance : ConsList (Int *) -> Int * + balance = \right -> + when right is + Cons 1 foo -> + when foo is + Cons 1 _ -> 3 + _ -> 3 + _ -> 3 + + main : Int * + main = + when balance Nil is + _ -> 3 + "# + ), + 3, + i64 + ); + + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + ConsList a : [Cons a (ConsList a), Nil] + + balance : ConsList (Int *) -> Int * + balance = \right -> + when right is + Cons 1 (Cons 1 _) -> 3 + _ -> 3 + + main : Int * + main = + when balance Nil is + _ -> 3 + "# + ), + 3, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn linked_list_guarded_double_pattern_match() { + // the important part here is that the first case (with the nested Cons) does not match + // TODO this also has undefined behavior + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + ConsList a : [Cons a (ConsList a), Nil] + + balance : ConsList (Int *) -> Int * + balance = \right -> + when right is + Cons 1 foo -> + when foo is + Cons 1 _ -> 3 + _ -> 3 + _ -> 3 + + main : Int * + main = + when balance Nil is + _ -> 3 + "# + ), + 3, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn linked_list_double_pattern_match() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + ConsList a : [Cons a (ConsList a), Nil] + + foo : ConsList (Int a) -> Int a + foo = \list -> + when list is + Cons _ (Cons x _) -> x + _ -> 0 + + main : Int * + main = + foo (Cons 1 (Cons 32 Nil)) + "# + ), + 32, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +// dev backend: this test somehow corrupts the errors vector ?! +fn binary_tree_double_pattern_match() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + BTree : [Node BTree BTree, Leaf I64] + + foo : BTree -> I64 + foo = \btree -> + when btree is + Node (Node (Leaf x) _) _ -> x + _ -> 1 + + main : I64 + main = + foo (Node (Node (Leaf 32) (Leaf 2)) (Leaf 3)) + "# + ), + 32, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn unified_empty_closure_bool() { + // none of the Closure tags will have a payload + // this was not handled correctly in the past + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + foo = \{} -> + when A is + A -> (\_ -> 1.23f64) + B -> (\_ -> 1.23f64) + + main : F64 + main = + (foo {}) 0 + "# + ), + 1.23, + f64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn unified_empty_closure_byte() { + // none of the Closure tags will have a payload + // this was not handled correctly in the past + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + foo = \{} -> + when A is + A -> (\_ -> 1.23f64) + B -> (\_ -> 1.23f64) + C -> (\_ -> 1.23) + + main : F64 + main = + (foo {}) 0 + "# + ), + 1.23, + f64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn task_always_twice() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + Effect a := {} -> a + + effectAlways : a -> Effect a + effectAlways = \x -> + inner = \{} -> x + + @Effect inner + + effectAfter : Effect a, (a -> Effect b) -> Effect b + effectAfter = \(@Effect thunk), transform -> transform (thunk {}) + + Task a err : Effect (Result a err) + + always : a -> Task a * + always = \x -> effectAlways (Ok x) + + fail : err -> Task * err + fail = \x -> effectAlways (Err x) + + after : Task a err, (a -> Task b err) -> Task b err + after = \task, transform -> + effectAfter task \res -> + when res is + Ok x -> transform x + Err e -> fail e + + main : Task {} F64 + main = after (always "foo") (\_ -> always {}) + + "# + ), + (), + (f64, u8), + |_| () + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn wildcard_rigid() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + Effect a := {} -> a + + Task a err : Effect (Result a err) + + # this failed because of the `*`, but worked with `err` + always : a -> Task a * + always = \x -> + inner = \{} -> (Ok x) + + @Effect inner + + + main : Task {} (Frac *) + main = always {} + "# + ), + (), + () + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn alias_of_alias_with_type_arguments() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + Effect a := a + + Task a err : Effect (Result a err) + + always : a -> Task a * + always = \x -> + inner = (Ok x) + + @Effect inner + + + main : Task {} F64 + main = always {} + "# + ), + (), + (f64, u8), + |_| () + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +#[ignore] +fn todo_bad_error_message() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + Effect a := {} -> a + + effectAlways : a -> Effect a + effectAlways = \x -> + inner = \{} -> x + + @Effect inner + + effectAfter : Effect a, (a -> Effect b) -> Effect b + effectAfter = \(@Effect thunk), transform -> transform (thunk {}) + + Task a err : Effect (Result a err) + + always : a -> Task a (Frac *) + always = \x -> effectAlways (Ok x) + + # the problem is that this restricts to `Task {} *` + fail : err -> Task {} err + fail = \x -> effectAlways (Err x) + + after : Task a err, (a -> Task b err) -> Task b err + after = \task, transform -> + effectAfter task \res -> + when res is + Ok x -> transform x + # but here it must be `forall b. Task b {}` + Err e -> fail e + + main : Task {} (Frac *) + main = + after (always "foo") (\_ -> always {}) + "# + ), + 0, + i64, + |_| 0 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn hof_conditional() { + // exposed issue with the if condition being just a symbol + assert_evals_to!( + indoc!( + r#" + passTrue = \f -> f Bool.true + + passTrue (\trueVal -> if trueVal then Bool.false else Bool.true) + "# + ), + 0, + u8 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +#[should_panic( + expected = "Roc failed with message: \"Shadowing { original_region: @55-56, shadow: @88-89 Ident" +)] +fn pattern_shadowing() { + assert_evals_to!( + indoc!( + r#" + x = 4 + + when 4 is + x -> x + "# + ), + 0, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +#[ignore] +#[should_panic(expected = "")] +fn unsupported_pattern_str_interp() { + assert_evals_to!( + indoc!( + r#" + { x: 4 } = { x : 4 } + + x + "# + ), + 0, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +#[ignore] +fn fingertree_basic() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + Some a : [One a, Two a a, Three a a a] + + Tuple a : [Pair a a, Triple a a a] + + # a FingerTree implementation + Seq a : [Nil, Unit a, More (Some a) (Seq (Tuple a)) (Some a)] + + # cons : a, Seq a -> Seq a + cons = \x, s -> + when s is + Nil -> Unit x + Unit y -> More (One x) Nil (One y) + More some q u -> + when some is + One y -> More (Two x y) q u + Two y z -> More (Three x y z) q u + Three y z w -> More (Two x y) (consTuple (Pair z w) q) u + + consTuple : Tuple a, Seq (Tuple a) -> Seq (Tuple a) + consTuple = \a, b -> cons a b + + main : Bool + main = + when cons 0x1 Nil is + Unit 1 -> Bool.true + _ -> Bool.false + "# + ), + true, + bool + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn case_or_pattern() { + // the `0` branch body should only be generated once in the future + // it is currently duplicated + assert_evals_to!( + indoc!( + r#" + x : [Red, Green, Blue] + x = Red + + when x is + Red | Green -> 0 + Blue -> 1 + "# + ), + 0, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +#[ignore] +fn rosetree_basic() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + Tree a : [Tree a (List (Tree a))] + + singleton : a -> Tree a + singleton = \x -> Tree x [] + + main : Bool + main = + x : Tree F64 + x = singleton 3 + when x is + Tree 3.0 _ -> Bool.true + _ -> Bool.false + "# + ), + true, + bool + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn case_jump() { + // the decision tree will generate a jump to the `1` branch here + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + ConsList a : [Cons a (ConsList a), Nil] + + x : ConsList I64 + x = Nil + + main = + when Pair x x is + Pair Nil _ -> 1 + Pair _ Nil -> 2 + Pair (Cons a _) (Cons b _) -> a + b + 3 + Pair _ _ -> 4 + "# + ), + 1, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn nullable_eval_cfold() { + // the decision tree will generate a jump to the `1` branch here + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + Expr : [Var, Val I64, Add Expr Expr, Mul Expr Expr] + + mkExpr : I64, I64 -> Expr + mkExpr = \n , v -> + when n is + 0 -> if v == 0 then Var else Val v + _ -> Add (mkExpr (n-1) (v+1)) (mkExpr (n-1) (max (v-1) 0)) + + max : I64, I64 -> I64 + max = \a, b -> if a > b then a else b + + eval : Expr -> I64 + eval = \e -> + when e is + Var -> 0 + Val v -> v + Add l r -> eval l + eval r + Mul l r -> eval l * eval r + + main : I64 + main = eval (mkExpr 3 1) + "# + ), + 11, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn nested_switch() { + crate::helpers::with_larger_debug_stack(|| + // exposed bug with passing the right symbol/layout down into switch branch generation + // This is also the only test_gen test that exercises Reset/Reuse (as of Aug 2022) + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + Expr : [ZAdd Expr Expr, Val I64, Var I64] + + eval : Expr -> I64 + eval = \e -> + when e is + Var _ -> 0 + Val v -> v + ZAdd l r -> eval l + eval r + + constFolding : Expr -> Expr + constFolding = \e -> + when e is + ZAdd e1 e2 -> + when Pair e1 e2 is + Pair (Val a) (Val b) -> Val (a+b) + Pair (Val a) (ZAdd x (Val b)) -> ZAdd (Val (a+b)) x + Pair _ _ -> ZAdd e1 e2 + + + _ -> e + + expr : Expr + expr = ZAdd (Val 3) (ZAdd (Val 4) (Val 5)) + + main : I64 + main = eval (constFolding expr) + "# + ), + 12, + i64 + )); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn count_deriv_x() { + // exposed bug with basing the block_of_memory on a specific (smaller) tag layout + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + Expr : [Ln Expr, Pow Expr Expr, Var Str] + + count : Expr -> I64 + count = \expr -> + when expr is + (Var _) -> 1 + (Pow f g) -> count f + count g + (Ln f) -> count f + + main : I64 + main = count (Var "x") + "# + ), + 1, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn deriv_pow() { + // exposed bug with ordering of variable declarations before switch + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + Expr : [Ln Expr, Pow Expr Expr, Var Str, Val I64] + + count : Expr -> I64 + count = \expr -> + when expr is + (Var _) -> 1 + (Val n) -> n + (Pow f g) -> count f + count g + (Ln f) -> count f + + pow : Expr, Expr -> Expr + pow = \a,b -> + when Pair a b is + Pair (Val _) (Val _) -> Val -1 + Pair _ (Val 0) -> Val 1 + Pair f (Val 1) -> f + Pair (Val 0) _ -> Val 0 + Pair f g -> Pow f g + + main : I64 + main = count (pow (Var "x") (Var "x")) + "# + ), + 2, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn multiple_increment() { + // the `leaf` value will be incremented multiple times at once + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + + Color : [Red, Black] + + Tree a b : [Leaf, Node Color (Tree a b) a b (Tree a b)] + + Map : Tree I64 Bool + + main : I64 + main = + leaf : Map + leaf = Leaf + + m : Map + m = Node Black (Node Black leaf 10 Bool.false leaf) 11 Bool.false (Node Black leaf 12 Bool.false (Node Red leaf 13 Bool.false leaf)) + + when m is + Leaf -> 0 + Node _ _ _ _ _ -> 1 + "# + ), + 1, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn switch_fuse_rc_non_exhaustive() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + Foo : [A I64 Foo, B I64 Foo, C I64 Foo, Empty] + + sum : Foo, I64 -> I64 + sum = \foo, accum -> + when foo is + A x resta -> sum resta (x + accum) + B x restb -> sum restb (x + accum) + # Empty -> accum + # C x restc -> sum restc (x + accum) + _ -> accum + + main : I64 + main = + A 1 (B 2 (C 3 Empty)) + |> sum 0 + "# + ), + 3, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn switch_fuse_rc_exhaustive() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + Foo : [A I64 Foo, B I64 Foo, C I64 Foo, Empty] + + sum : Foo, I64 -> I64 + sum = \foo, accum -> + when foo is + A x resta -> sum resta (x + accum) + B x restb -> sum restb (x + accum) + C x restc -> sum restc (x + accum) + Empty -> accum + + main : I64 + main = + A 1 (B 2 (C 3 Empty)) + |> sum 0 + "# + ), + 6, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn build_then_apply_closure() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + main : Str + main = + x = "long string that is malloced" + + (\_ -> x) {} + "# + ), + RocStr::from("long string that is malloced"), + RocStr + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn expanded_result() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + a : Result I64 Str + a = Ok 4 + + after = \x, f -> + when x is + Ok v -> f v + Err e -> Err e + + main : I64 + main = + helper = after a (\x -> Ok x) + + when helper is + Ok v -> v + Err _ -> 0 + + "# + ), + 4, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn backpassing_result() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + a : Result I64 Str + a = Ok 1 + + f = \x -> Ok (x + 1) + g = \y -> Ok (y * 2) + + main : I64 + main = + helper = + x <- Result.try a + y <- Result.try (f x) + z <- Result.try (g y) + + Ok z + + helper + |> Result.withDefault 0 + + "# + ), + 4, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +#[should_panic(expected = "Shadowing { original_region: @55-56, shadow: @72-73 Ident")] +fn function_malformed_pattern() { + assert_evals_to!( + indoc!( + r#" + x = 3 + + (\x -> x) 42 + "# + ), + 3, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +#[ignore = "causes alias analysis panics, should roc_panic"] +fn call_invalid_layout() { + assert_evals_to!( + indoc!( + r#" + f : I64 -> I64 + f = \x -> x + + f {} + "# + ), + 3, + i64, + |x| x, + true + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn increment_or_double_closure() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + + apply : (a -> a), a -> a + apply = \f, x -> f x + + main = + one : I64 + one = 1 + + two : I64 + two = 2 + + b : Bool + b = Bool.true + + increment : I64 -> I64 + increment = \x -> x + one + + double : I64 -> I64 + double = \x -> if b then x * two else x + + f = (if Bool.true then increment else double) + + apply f 42 + "# + ), + 43, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn module_thunk_is_function() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + main = helper "foo" "bar" + helper = Str.concat + "# + ), + RocStr::from("foobar"), + RocStr + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn pass_through_unresolved_type_variable() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + main : Str + main = + (accept \x -> x) "B" + + + accept : * -> (b -> b) + accept = \_ -> + \input -> input + "# + ), + RocStr::from("B"), + RocStr + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn pattern_match_empty_record() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + main : I64 + main = + when {} is + {} -> 0 + + "# + ), + 0, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn pattern_match_unit_tag() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + unit : [Unit] + unit = Unit + + main : I64 + main = + when unit is + Unit -> 0 + + "# + ), + 0, + i64 + ); +} + +// see for why this is disabled on wasm32 https://github.com/roc-lang/roc/issues/1687 +#[cfg(not(feature = "gen-llvm-wasm"))] +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn mirror_llvm_alignment_padding() { + // see https://github.com/roc-lang/roc/issues/1569 + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + main : Str + main = + p1 = {name : "test1", test: 1 == 1 } + + List.map [p1, p1] (\{ test } -> if test then "pass" else "fail") + |> Str.joinWith "\n" + + "# + ), + RocStr::from("pass\npass"), + RocStr + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn lambda_set_bool() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + p1 = (\u -> u == 97) + p2 = (\u -> u == 98) + + main : I64 + main = + oneOfResult = List.map [p1, p2] (\p -> p 42) + + when oneOfResult is + _ -> 32 + + "# + ), + 32, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn lambda_set_byte() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + p1 = (\u -> u == 97) + p2 = (\u -> u == 98) + p3 = (\u -> u == 99) + + main : I64 + main = + oneOfResult = List.map [p1, p2, p3] (\p -> p 42) + + when oneOfResult is + _ -> 32 + + "# + ), + 32, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn lambda_set_struct_byte() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + + main : I64 + main = + r : [Red, Green, Blue] + r = Red + + p1 = (\u -> r == u) + foobarbaz = (\p -> p Green) + oneOfResult = List.map [p1, p1] foobarbaz + + when oneOfResult is + _ -> 32 + + "# + ), + 32, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn lambda_set_enum_byte_byte() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + + main : I64 + main = + r : [Red, Green, Blue] + r = Red + + g : [Red, Green, Blue] + g = Green + + p1 = (\u -> r == u) + p2 = (\u -> g == u) + oneOfResult = List.map [p1, p2] (\p -> p Green) + + when oneOfResult is + _ -> 32 + + "# + ), + 32, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn list_walk_until() { + // see https://github.com/roc-lang/roc/issues/1576 + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + + satisfyA : {} -> List {} + satisfyA = \_ -> [] + + oneOfResult = + List.walkUntil [satisfyA] [] \_, _ -> Break [] + + main = + when oneOfResult is + _ -> 32 + "# + ), + 32, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn int_literal_not_specialized_with_annotation() { + // see https://github.com/roc-lang/roc/issues/1600 + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + main = + satisfy : (U8 -> Str) -> Str + satisfy = \_ -> "foo" + + myEq : a, a -> Str + myEq = \_, _ -> "bar" + + p1 : Num * -> Str + p1 = (\u -> myEq u 64) + + when satisfy p1 is + _ -> 32 + "# + ), + 32, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn int_literal_not_specialized_no_annotation() { + // see https://github.com/roc-lang/roc/issues/1600 + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + main = + satisfy : (U8 -> Str) -> Str + satisfy = \_ -> "foo" + + myEq : a, a -> Str + myEq = \_, _ -> "bar" + + p1 = (\u -> myEq u 64) + + when satisfy p1 is + _ -> 32 + "# + ), + 32, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn unresolved_tvar_when_capture_is_unused() { + // see https://github.com/roc-lang/roc/issues/1585 + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + main : I64 + main = + r : U8 + r = 1 + + p1 = (\_ -> r == 1) + oneOfResult = List.map [p1] (\p -> p Green) + + when oneOfResult is + _ -> 32 + + "# + ), + 32, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +#[should_panic(expected = "Roc failed with message: ")] +fn value_not_exposed_hits_panic() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + main : I64 + main = + Str.toInt 32 + "# + ), + 32, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn mix_function_and_closure() { + // see https://github.com/roc-lang/roc/pull/1706 + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + # foo does not capture any variables + # but through unification will get a lambda set that does store information + # we must handle that correctly + foo = \x -> x + + bar = \y -> \_ -> y + + main : Str + main = + (if 1 == 1 then foo else (bar "nope nope nope")) "hello world" + "# + ), + RocStr::from("hello world"), + RocStr + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn mix_function_and_closure_level_of_indirection() { + // see https://github.com/roc-lang/roc/pull/1706 + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + foo = \x -> x + + bar = \y -> \_ -> y + + f = (if 1 == 1 then foo else (bar "nope nope nope")) + + main : Str + main = + f "hello world" + "# + ), + RocStr::from("hello world"), + RocStr + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +#[cfg_attr(debug_assertions, ignore)] // this test stack-overflows the compiler in debug mode +fn do_pass_bool_byte_closure_layout() { + // see https://github.com/roc-lang/roc/pull/1706 + // the distinction is actually important, dropping that info means some functions just get + // skipped + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + ## PARSER + + Parser a : List U8 -> List [Pair a (List U8)] + + + ## ANY + + # If successful, the any parser consumes one character + + any: Parser U8 + any = \inp -> + when List.first inp is + Ok u -> [Pair u (List.dropFirst inp 1)] + _ -> [] + + + + ## SATISFY + + satisfy : (U8 -> Bool) -> Parser U8 + satisfy = \predicate -> + \input -> + walker = \accum, (Pair u rest) -> + if predicate u then + Break [Pair u rest] + + else + Break accum + + List.walkUntil (any input) [] walker + + + + oneOf : List (Parser a) -> Parser a + oneOf = \parserList -> + \input -> + walker = \accum, p -> + output = p input + if List.len output == 1 then + Break output + + else + Continue accum + + List.walkUntil parserList [] walker + + + satisfyA = satisfy (\u -> u == 97) # recognize 97 + satisfyB = satisfy (\u -> u == 98) # recognize 98 + + test1 = if List.len ((oneOf [satisfyA, satisfyB]) [97, 98, 99, 100] ) == 1 then "PASS" else "FAIL" + test2 = if List.len ((oneOf [satisfyA, satisfyB]) [98, 99, 100, 97] ) == 1 then "PASS" else "FAIL" + test3 = if List.len ((oneOf [satisfyB , satisfyA]) [98, 99, 100, 97] ) == 1 then "PASS" else "FAIL" + test4 = if List.len ((oneOf [satisfyA, satisfyB]) [99, 100, 101] ) == 0 then "PASS" else "FAIL" + + + main : Str + main = [test1, test2, test3, test4] |> Str.joinWith ", " + "# + ), + RocStr::from("PASS, PASS, PASS, PASS"), + RocStr + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn nested_rigid_list() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + foo : List a -> List a + foo = \list -> + p2 : List a + p2 = list + + p2 + + main = + when foo [] is + _ -> "hello world" + "# + ), + RocStr::from("hello world"), + RocStr + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev"))] +fn nested_rigid_alias() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + Identity a := a + + foo : Identity a -> Identity a + foo = \list -> + p2 : Identity a + p2 = list + + p2 + + main = + when foo (@Identity "foo") is + _ -> "hello world" + "# + ), + RocStr::from("hello world"), + RocStr + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev"))] +fn nested_rigid_tag_union() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + foo : [Identity a] -> [Identity a] + foo = \list -> + p2 : [Identity a] + p2 = list + + p2 + + main = + when foo (Identity "foo") is + _ -> "hello world" + "# + ), + RocStr::from("hello world"), + RocStr + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn call_that_needs_closure_parameter() { + // here both p2 is lifted to the top-level, which means that `list` must be + // passed to it from `manyAux`. + assert_evals_to!( + indoc!( + r#" + Step state a : [Loop state, Done a] + + manyAux : List a -> [Pair (Step (List a) (List a))] + manyAux = \list -> + p2 = \_ -> Pair (Done list) + + p2 "foo" + + manyAuxTest = (manyAux []) == Pair (Loop [97]) + + runTest = \t -> if t then "PASS" else "FAIL" + + runTest manyAuxTest + "# + ), + RocStr::from("FAIL"), + RocStr + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn alias_defined_out_of_order() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + main : Foo + main = "foo" + + Foo : Bar + Bar : Str + + "# + ), + RocStr::from("foo"), + RocStr + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn recursively_build_effect() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + greeting = + hi = "Hello" + name = "World" + + "\(hi), \(name)!" + + main = + when nestHelp 4 is + _ -> greeting + + nestHelp : I64 -> XEffect {} + nestHelp = \m -> + when m is + 0 -> + always {} + + _ -> + always {} |> after \_ -> nestHelp (m - 1) + + + XEffect a := {} -> a + + always : a -> XEffect a + always = \x -> @XEffect (\{} -> x) + + after : XEffect a, (a -> XEffect b) -> XEffect b + after = \(@XEffect e), toB -> + @XEffect \{} -> + when toB (e {}) is + @XEffect e2 -> + e2 {} + "# + ), + RocStr::from("Hello, World!"), + RocStr + ); +} + +#[test] +#[ignore = "TODO; currently generates bad code because `a` isn't specialized inside the closure."] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn polymophic_expression_captured_inside_closure() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + asU8 : U8 -> U8 + asU8 = \_ -> 30 + + main = + a = 15 + f = \{} -> + asU8 a + + f {} + "# + ), + 30, + u8 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn issue_2322() { + assert_evals_to!( + indoc!( + r#" + double = \x -> x * 2 + doubleBind = \x -> (\_ -> double x) + doubleThree = doubleBind 3 + doubleThree {} + "# + ), + 6, + i64 + ) +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn box_and_unbox_small_string() { + assert_evals_to!( + indoc!( + r#" + "short" + |> Box.box + |> Box.unbox + "# + ), + RocStr::from("short"), + RocStr + ) +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn box_and_unbox_big_string() { + assert_evals_to!( + indoc!( + r#" + Str.concat "Leverage " "agile frameworks to provide a robust synopsis for high level overviews" + |> Box.box + |> Box.unbox + "# + ), + RocStr::from( + "Leverage agile frameworks to provide a robust synopsis for high level overviews" + ), + RocStr + ) +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn box_and_unbox_nonrecursive_tag() { + assert_evals_to!( + indoc!( + r#" + result : Result U64 U64 + result = Ok 42 + + result + |> Box.box + |> Box.unbox + "# + ), + roc_std::RocResult::ok(42), + roc_std::RocResult + ) +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn box_num() { + assert_evals_to!("Box.box 123u64", RocBox::new(123), RocBox) +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn box_record_2_u64() { + assert_evals_to!( + "Box.box { x: 1u64, y: 2u64 }", + RocBox::new((1u64, 2u64)), + RocBox<(u64, u64)> + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn box_record_3_u64() { + assert_evals_to!( + "Box.box { x: 1u64, y: 2u64, z: 3u64 }", + RocBox::new((1u64, 2u64, 3u64)), + RocBox<(u64, u64, u64)> + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn box_str() { + assert_evals_to!( + "Box.box \"short\"", + RocBox::new(RocStr::from("short")), + RocBox + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn box_and_unbox_u64() { + assert_evals_to!("Box.unbox (Box.box (123u64))", 123, u64) +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn box_and_unbox_u32() { + assert_evals_to!("Box.unbox (Box.box (123u32))", 123, u32) +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn box_and_unbox_u16() { + assert_evals_to!("Box.unbox (Box.box (123u16))", 123, u16) +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn box_and_unbox_u8() { + assert_evals_to!("Box.unbox (Box.box (123u8))", 123, u8) +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn box_and_unbox_bool() { + assert_evals_to!("Box.unbox (Box.box (Bool.true))", true, bool) +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn box_and_unbox_f64() { + assert_evals_to!("Box.unbox (Box.box (123.0f64))", 123.0, f64) +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn box_and_unbox_f32() { + assert_evals_to!("Box.unbox (Box.box (123.0f32))", 123.0, f32) +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn box_and_unbox_record_2_u64() { + assert_evals_to!( + indoc!( + r#" + Box.unbox (Box.box { a: 15u64, b: 27u64 }) + "# + ), + (15, 27), + (u64, u64) + ) +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn box_and_unbox_record_3_u64() { + assert_evals_to!( + indoc!( + r#" + Box.unbox (Box.box { a: 15u64, b: 27u64, c: 34u64 }) + "# + ), + (15, 27, 34), + (u64, u64, u64) + ) +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn box_and_unbox_record_2_u8() { + assert_evals_to!( + indoc!( + r#" + Box.unbox (Box.box { a: 15u8, b: 27u8 }) + "# + ), + (15, 27), + (u8, u8) + ) +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn box_and_unbox_record_3_u8() { + assert_evals_to!( + indoc!( + r#" + Box.unbox (Box.box { a: 15u8, b: 27u8, c: 34u8 }) + "# + ), + (15, 27, 34), + (u8, u8, u8) + ) +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn box_and_unbox_tag_union() { + assert_evals_to!( + indoc!( + r#" + v : [A U8, B U8] # usually stack allocated + v = B 27u8 + + Box.unbox (Box.box v) + "# + ), + (27, 1), + (u8, u8) + ) +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn closure_called_in_its_defining_scope() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + main : Str + main = + g : Str + g = "hello world" + + getG : {} -> Str + getG = \{} -> g + + getG {} + "# + ), + RocStr::from("hello world"), + RocStr + ) +} + +#[test] +#[ignore] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn issue_2894() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + main : U32 + main = + g : { x : U32 } + g = { x: 1u32 } + + getG : {} -> { x : U32 } + getG = \{} -> g + + h : {} -> U32 + h = \{} -> (getG {}).x + + h {} + "# + ), + 1u32, + u32 + ) +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn polymorphic_def_used_in_closure() { + assert_evals_to!( + indoc!( + r#" + a : I64 -> _ + a = \g -> + f = { r: g, h: 32 } + + h1 : U64 + h1 = (\{} -> f.h) {} + h1 + a 1 + "# + ), + 32, + u64 + ) +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn polymorphic_lambda_set_usage() { + assert_evals_to!( + indoc!( + r#" + id1 = \x -> x + id2 = \y -> y + id = if Bool.true then id1 else id2 + + id 9u8 + "# + ), + 9, + u8 + ) +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn polymorphic_lambda_set_multiple_specializations() { + assert_evals_to!( + indoc!( + r#" + id1 = \x -> x + id2 = \y -> y + id = \z -> + f = if Bool.true then id1 else id2 + f z + + (id 9u8) + Num.toU8 (id 16u16) + "# + ), + 25, + u8 + ) +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn list_map2_conslist() { + // this had an RC problem, https://github.com/roc-lang/roc/issues/2968 + assert_evals_to!( + indoc!( + r#" + ConsList a : [Nil, Cons a (ConsList a)] + + x : List (ConsList Str) + x = List.map2 [] [Nil] Cons + + when List.first x is + _ -> "" + "# + ), + RocStr::default(), + RocStr + ) +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn mutual_recursion_top_level_defs() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [ main ] to "./platform" + + isEven = \n -> + when n is + 0 -> Bool.true + 1 -> Bool.false + _ -> isOdd (n - 1) + + isOdd = \n -> + when n is + 0 -> Bool.false + 1 -> Bool.true + _ -> isEven (n - 1) + + main = isOdd 11 + "# + ), + true, + bool + ) +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn polymorphic_lambda_captures_polymorphic_value() { + assert_evals_to!( + indoc!( + r#" + x = 2 + + f1 = \_ -> x + + f = if Bool.true then f1 else f1 + f {} + "# + ), + 2, + u64 + ) +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn lambda_capture_niche_u64_vs_u8_capture() { + assert_evals_to!( + indoc!( + r#" + capture : _ -> ({} -> Str) + capture = \val -> + \{} -> + Num.toStr val + + x : Bool + x = Bool.true + + fun = + if x then + capture 123u64 + else + capture 18u8 + + fun {} + "# + ), + RocStr::from("123"), + RocStr + ) +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn lambda_capture_niches_with_other_lambda_capture() { + assert_evals_to!( + indoc!( + r#" + capture : _ -> ({} -> Str) + capture = \val -> + \{} -> + Num.toStr val + + capture2 = \val -> \{} -> val + + f = \x -> + g = + when x is + A -> capture 11u8 + B -> capture2 "lisa" + C -> capture 187128u64 + g {} + + {a: f A, b: f B, c: f C} + "# + ), + ( + RocStr::from("11"), + RocStr::from("lisa"), + RocStr::from("187128") + ), + (RocStr, RocStr, RocStr) + ) +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn lambda_capture_niches_with_non_capturing_function() { + assert_evals_to!( + indoc!( + r#" + capture : _ -> ({} -> Str) + capture = \val -> + \{} -> + Num.toStr val + + triv = \{} -> "triv" + + f = \x -> + g = + when x is + A -> capture 11u8 + B -> triv + C -> capture 187128u64 + g {} + + {a: f A, b: f B, c: f C} + "# + ), + ( + RocStr::from("11"), + RocStr::from("triv"), + RocStr::from("187128") + ), + (RocStr, RocStr, RocStr) + ) +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn lambda_capture_niches_have_captured_function_in_closure() { + assert_evals_to!( + indoc!( + r#" + Lazy a : {} -> a + + after : Lazy a, (a -> Lazy b) -> Lazy b + after = \effect, map -> + thunk = \{} -> + when map (effect {}) is + b -> b {} + thunk + + f = \_ -> \_ -> "fun f" + g = \{ s1 } -> \_ -> s1 + + fun = \x -> + h = + if x then + after (\{} -> "") f + else + after (\{} -> {s1: "s1"}) g + h {} + + {a: fun Bool.false, b: fun Bool.true} + "# + ), + (RocStr::from("s1"), RocStr::from("fun f")), + (RocStr, RocStr) + ) +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn recursive_call_capturing_function() { + assert_evals_to!( + indoc!( + r#" + a = \b -> + c = \d -> + if d == 7 then d else c (d + b) + c 1 + + a 6 + "# + ), + 7, + i64 + ) +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn shared_pattern_variable_in_when_branches() { + assert_evals_to!( + indoc!( + r#" + f = \t -> + when t is + A x | B x -> x + + {a: f (A 15u8), b: (B 31u8)} + "# + ), + (15u8, 31u8), + (u8, u8) + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn symbol_not_bound_in_all_patterns_runs_when_no_bound_symbol_used() { + assert_evals_to!( + indoc!( + r#" + f = \t -> when t is + A x | B y -> 31u8 + + {a: f (A 15u8), b: f (B 15u8)} + "# + ), + (31u8, 31u8), + (u8, u8), + |x| x, + true // allow errors + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn symbol_not_bound_in_all_patterns_runs_when_bound_pattern_reached() { + assert_evals_to!( + indoc!( + r#" + when A 15u8 is + A x | B y -> x + "# + ), + 15u8, + u8, + |x| x, + true // allow errors + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +#[should_panic( + expected = r#"Roc failed with message: "Hit a branch pattern that does not bind all symbols its body needs"# +)] +fn runtime_error_when_degenerate_pattern_reached() { + assert_evals_to!( + indoc!( + r#" + when B 15u8 is + A x | B y -> x + 5u8 + "# + ), + 15u8, + u8, + |x| x, + true // allow errors + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn recursive_lambda_set_issue_3444() { + assert_evals_to!( + indoc!( + r#" + combine = \f, g -> \x -> g (f x) + const = \x -> (\_y -> x) + + list = [const "a", const "b", const "c"] + + res : Str -> Str + res = List.walk list (const "z") (\c1, c2 -> combine c1 c2) + res "hello" + "# + ), + RocStr::from("c"), + RocStr + ) +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn recursive_lambda_set_toplevel_issue_3444() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + combine = \f, g -> \x -> g (f x) + const = \x -> (\_y -> x) + + list = [const "a", const "b", const "c"] + + res : Str -> Str + res = List.walk list (const "z") (\c1, c2 -> combine c1 c2) + + main = res "hello" + "# + ), + RocStr::from("c"), + RocStr + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn recursive_lambda_set_issue_3444_inferred() { + assert_evals_to!( + indoc!( + r#" + combine = \f, g -> \x -> g (f x) + const = \x -> (\_y -> x) + + list = [const "a", const "b", const "c"] + + res = List.walk list (const "z") (\c1, c2 -> combine c1 c2) + res "hello" + "# + ), + RocStr::from("c"), + RocStr + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn compose_recursive_lambda_set_productive_toplevel() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + compose = \f, g -> \x -> g (f x) + + identity = \x -> x + exclaim = \s -> "\(s)!" + whisper = \s -> "(\(s))" + + main = + res: Str -> Str + res = List.walk [ exclaim, whisper ] identity compose + res "hello" + "# + ), + RocStr::from("(hello!)"), + RocStr + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn compose_recursive_lambda_set_productive_nested() { + assert_evals_to!( + indoc!( + r#" + compose = \f, g -> \x -> g (f x) + + identity = \x -> x + exclaim = \s -> "\(s)!" + whisper = \s -> "(\(s))" + + res: Str -> Str + res = List.walk [ exclaim, whisper ] identity compose + res "hello" + "# + ), + RocStr::from("(hello!)"), + RocStr + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn compose_recursive_lambda_set_productive_inferred() { + assert_evals_to!( + indoc!( + r#" + compose = \f, g -> \x -> g (f x) + + identity = \x -> x + exclaim = \s -> "\(s)!" + whisper = \s -> "(\(s))" + + res = List.walk [ exclaim, whisper ] identity compose + res "hello" + "# + ), + RocStr::from("(hello!)"), + RocStr + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn compose_recursive_lambda_set_productive_nullable_wrapped() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + compose = \forward -> \f, g -> + if forward + then \x -> g (f x) + else \x -> f (g x) + + identity = \x -> x + exclame = \s -> "\(s)!" + whisper = \s -> "(\(s))" + + main = + res: Str -> Str + res = List.walk [ exclame, whisper ] identity (compose Bool.false) + res "hello" + "# + ), + RocStr::from("(hello)!"), + RocStr + ) +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn local_binding_aliases_function() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [ main ] to "./platform" + + f : {} -> List a + f = \_ -> [] + + main : List U8 + main = + g = f + + g {} + "# + ), + RocList::::from_slice(&[]), + RocList + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn local_binding_aliases_function_inferred() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [ main ] to "./platform" + + f = \_ -> [] + + main = + g = f + + g {} + "# + ), + RocList::from_slice(&[]), + RocList + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn transient_captures() { + assert_evals_to!( + indoc!( + r#" + x = "abc" + + getX = \{} -> x + + h = \{} -> getX {} + + h {} + "# + ), + RocStr::from("abc"), + RocStr + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn transient_captures_after_def_ordering() { + assert_evals_to!( + indoc!( + r#" + h = \{} -> getX {} + + getX = \{} -> x + + x = "abc" + + h {} + "# + ), + RocStr::from("abc"), + RocStr + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn deep_transient_capture_chain() { + assert_evals_to!( + indoc!( + r#" + z = "abc" + + getX = \{} -> getY {} + getY = \{} -> getZ {} + getZ = \{} -> z + + h = \{} -> getX {} + + h {} + "# + ), + RocStr::from("abc"), + RocStr + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn deep_transient_capture_chain_with_multiple_captures() { + assert_evals_to!( + indoc!( + r#" + h = "h" + x = "x" + y = "y" + z = "z" + + getX = \{} -> Str.concat x (getY {}) + getY = \{} -> Str.concat y (getZ {}) + getZ = \{} -> z + + getH = \{} -> Str.concat h (getX {}) + + getH {} + "# + ), + RocStr::from("hxyz"), + RocStr + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn transient_captures_from_outer_scope() { + assert_evals_to!( + indoc!( + r#" + x = "abc" + + getX = \{} -> x + + innerScope = + h = \{} -> getX {} + h {} + + innerScope + "# + ), + RocStr::from("abc"), + RocStr + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn mutually_recursive_captures() { + assert_evals_to!( + indoc!( + r#" + x : Bool + x = Bool.true + + y : Bool + y = Bool.false + + a = "foo" + b = "bar" + + foo = \{} -> if x then a else bar {} + bar = \{} -> if y then b else foo {} + + bar {} + "# + ), + RocStr::from("foo"), + RocStr + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn int_let_generalization() { + assert_evals_to!( + indoc!( + r#" + manyAux : {} -> I32 + manyAux = \_ -> + output = \_ -> 42 + + output {} + + when manyAux {} is + _ -> "done" + "# + ), + RocStr::from("done"), + RocStr + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn pattern_match_char() { + assert_evals_to!( + indoc!( + r#" + c = 'A' + + when c is + 'A' -> "okay" + _ -> "FAIL" + "# + ), + RocStr::from("okay"), + RocStr + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn issue_4348() { + assert_evals_to!( + indoc!( + r#" + str = "z" + (\_ -> + when str is + "z" -> "okay" + _ -> "") "FAIL" + "# + ), + RocStr::from("okay"), + RocStr + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn issue_4349() { + assert_evals_to!( + indoc!( + r#" + ir = Ok "" + res = + Result.try ir \_ -> + when ir is + Ok "" -> Ok "" + _ -> Err Bad + when res is + Ok _ -> "okay" + _ -> "FAIL" + "# + ), + RocStr::from("okay"), + RocStr + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn issue_4712() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + Parser a : {} -> a + + v1 : {} + v1 = {} + + v2 : Str + v2 = "cd" + + apply : Parser (a -> Str), a -> Parser Str + apply = \fnParser, valParser -> + \{} -> + (fnParser {}) (valParser) + + map : a, (a -> Str) -> Parser Str + map = \simpleParser, transform -> + apply (\{} -> transform) simpleParser + + gen = \{} -> + [ map v1 (\{} -> "ab"), map v2 (\s -> s) ] + |> List.map (\f -> f {}) + |> Str.joinWith "," + + main = gen {} + "# + ), + RocStr::from("ab,cd"), + RocStr + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn pattern_as_toplevel() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + record = { a: 42i64, b: "foo" } + + main = + when record is + { a: 42i64 } as r -> record == r + _ -> Bool.false + "# + ), + true, + bool + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn pattern_as_nested() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + record = { a: 42i64, b: "foo" } + + main = + when Pair {} record is + Pair {} ({ a: 42i64 } as r) -> record == r + _ -> Bool.false + "# + ), + true, + bool + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn pattern_as_of_symbol() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + main = + when "foo" is + a as b -> a == b + "# + ), + true, + bool + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn function_specialization_information_in_lambda_set_thunk() { + assert_evals_to!( + indoc!( + r###" + app "test" provides [main] to "./platform" + + andThen = \{} -> + x = 10u8 + \newFn -> Num.add (newFn {}) x + + between = andThen {} + + main = between \{} -> between \{} -> 10u8 + "### + ), + 30, + u8 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn function_specialization_information_in_lambda_set_thunk_independent_defs() { + assert_evals_to!( + indoc!( + r###" + app "test" provides [main] to "./platform" + + andThen = \{} -> + x = 10u8 + \newFn -> Num.add (newFn {}) x + + between1 = andThen {} + + between2 = andThen {} + + main = between1 \{} -> between2 \{} -> 10u8 + "### + ), + 30, + u8 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn when_guard_appears_multiple_times_in_compiled_decision_tree_issue_5176() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + go : U8 -> U8 + go = \byte -> + when byte is + 15 if Bool.true -> 1 + b if Bool.true -> b + 2 + _ -> 3 + + main = go '.' + "# + ), + b'.' + 2, + u8 + ) +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn recursive_lambda_set_resolved_only_upon_specialization() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + factCPS = \n, cont -> + if n == 0 then + cont 1 + else + factCPS (n - 1) \value -> cont (n * value) + + main = + factCPS 5u64 \x -> x + "# + ), + 120, + u64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn layout_cache_structure_with_multiple_recursive_structures() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + Chain : [ + End, + Link Chain, + ] + + LinkedList : [Nil, Cons { first : Chain, rest : LinkedList }] + + main = + base : LinkedList + base = Nil + + walker : LinkedList, Chain -> LinkedList + walker = \rest, first -> Cons { first, rest } + + list : List Chain + list = [] + + r = List.walk list base walker + + if r == base then 11u8 else 22u8 + "# + ), + 11, + u8 + ); +} + +#[test] +#[cfg(feature = "gen-llvm")] +fn reset_recursive_type_wraps_in_named_type() { + assert_evals_to!( + indoc!( + r###" + app "test" provides [main] to "./platform" + + main : Str + main = + newList = mapLinkedList (Cons 1 (Cons 2 (Cons 3 Nil))) (\x -> x + 1) + printLinkedList newList Num.toStr + + LinkedList a : [Cons a (LinkedList a), Nil] + + mapLinkedList : LinkedList a, (a -> b) -> LinkedList b + mapLinkedList = \linkedList, f -> when linkedList is + Nil -> Nil + Cons x xs -> + s = if Bool.true then "true" else "false" + expect s == "true" + + Cons (f x) (mapLinkedList xs f) + + printLinkedList : LinkedList a, (a -> Str) -> Str + printLinkedList = \linkedList, f -> + when linkedList is + Nil -> "Nil" + Cons x xs -> + strX = f x + strXs = printLinkedList xs f + "Cons \(strX) (\(strXs))" + "### + ), + RocStr::from("Cons 2 (Cons 3 (Cons 4 (Nil)))"), + RocStr + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn pass_lambda_set_to_function() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + instr = if Bool.true then Num.mul else Num.add + + fn = \a -> instr a a + + main = fn 3 + "# + ), + 3 * 3, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn linked_list_trmc() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + LinkedList a : [Nil, Cons a (LinkedList a)] + + repeat : a, Nat -> LinkedList a + repeat = \value, n -> + when n is + 0 -> Nil + _ -> Cons value (repeat value (n - 1)) + + length : LinkedList a -> I64 + length = \list -> + when list is + Nil -> 0 + Cons _ rest -> 1 + length rest + + main : I64 + main = + repeat "foo" 5 + |> length + "# + ), + 5, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))] +fn many_arguments() { + // exhausts all argument registers on x86 and aarch + assert_evals_to!( + indoc!( + r#" + fun = \a,b,c,d, e,f,g,h, i -> + (a + b + c + d) + (e + f + g + h) + i + + fun 0i64 1 2 3 4 5 6 7 8 + "# + ), + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))] +fn multiple_uses_of_bool_true_record() { + assert_evals_to!( + indoc!( + r#" + (Bool.true, Bool.true).0 + "# + ), + true, + bool + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))] +fn multiple_uses_of_bool_true_tag_union() { + assert_evals_to!( + indoc!( + r#" + x : [ One Bool Bool, Empty ] + x = One Bool.true Bool.true + + when x is + One a _ -> a + Empty -> Bool.false + "# + ), + true, + bool + ); +} diff --git a/compiler/test_gen/src/gen_records.rs b/crates/compiler/test_gen/src/gen_records.rs similarity index 81% rename from compiler/test_gen/src/gen_records.rs rename to crates/compiler/test_gen/src/gen_records.rs index 5b4fcf31c2..0851f493b3 100644 --- a/compiler/test_gen/src/gen_records.rs +++ b/crates/compiler/test_gen/src/gen_records.rs @@ -1,16 +1,16 @@ #[cfg(feature = "gen-llvm")] -use crate::helpers::llvm::{assert_evals_to, expect_runtime_error_panic}; +use crate::helpers::llvm::assert_evals_to; #[cfg(feature = "gen-dev")] use crate::helpers::dev::assert_evals_to; #[cfg(feature = "gen-wasm")] -use crate::helpers::wasm::{assert_evals_to, expect_runtime_error_panic}; +use crate::helpers::wasm::assert_evals_to; // use crate::assert_wasm_evals_to as assert_evals_to; use indoc::indoc; -#[cfg(test)] +#[cfg(all(test, any(feature = "gen-llvm", feature = "gen-wasm")))] use roc_std::{RocList, RocStr}; #[test] @@ -53,7 +53,7 @@ fn f64_record() { assert_evals_to!( indoc!( r#" - rec = { y: 17.2, x: 15.1, z: 19.3 } + rec = { y: 17.2f64, x: 15.1f64, z: 19.3f64 } rec.x "# @@ -65,7 +65,7 @@ fn f64_record() { assert_evals_to!( indoc!( r#" - rec = { y: 17.2, x: 15.1, z: 19.3 } + rec = { y: 17.2f64, x: 15.1f64, z: 19.3f64 } rec.y "# @@ -77,7 +77,7 @@ fn f64_record() { assert_evals_to!( indoc!( r#" - rec = { y: 17.2, x: 15.1, z: 19.3 } + rec = { y: 17.2f64, x: 15.1f64, z: 19.3f64 } rec.z "# @@ -88,7 +88,27 @@ fn f64_record() { } #[test] -#[cfg(any(feature = "gen-llvm"))] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn pass_bool_record() { + // found a bug there the register to use was not incremented correctly + assert_evals_to!( + indoc!( + r#" + true : Bool + true = Bool.true + + f = \_, x -> x + + f { x: true, y: true } 23 + "# + ), + 23, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] fn fn_record() { assert_evals_to!( indoc!( @@ -102,30 +122,6 @@ fn fn_record() { i64 ); - assert_evals_to!( - indoc!( - r#" - rec = { x: 15, y: 17, z: 19 } - - rec.y - "# - ), - 17, - i64 - ); - - assert_evals_to!( - indoc!( - r#" - rec = { x: 15, y: 17, z: 19 } - - rec.z - "# - ), - 19, - i64 - ); - assert_evals_to!( indoc!( r#" @@ -286,7 +282,7 @@ fn i64_record2_literal() { } // #[test] -// #[cfg(any(feature = "gen-llvm"))] +// #[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] // fn i64_record3_literal() { // assert_evals_to!( // indoc!( @@ -304,7 +300,7 @@ fn f64_record2_literal() { assert_evals_to!( indoc!( r#" - { x: 3.1, y: 5.1 } + { x: 3.1f64, y: 5.1f64 } "# ), (3.1, 5.1), @@ -313,7 +309,7 @@ fn f64_record2_literal() { } // #[test] -// #[cfg(any(feature = "gen-llvm"))] +// #[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] // fn f64_record3_literal() { // assert_evals_to!( // indoc!( @@ -327,13 +323,13 @@ fn f64_record2_literal() { // } // #[test] -// #[cfg(any(feature = "gen-llvm"))] +// #[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] // fn bool_record4_literal() { // assert_evals_to!( // indoc!( // r#" // record : { a : Bool, b : Bool, c : Bool, d : Bool } -// record = { a: True, b: True, c : True, d : Bool } +// record = { a: Bool.true, b: Bool.true, c : Bool.true, d : Bool } // record // "# @@ -357,7 +353,7 @@ fn i64_record1_literal() { } // #[test] -// #[cfg(any(feature = "gen-llvm"))] +// #[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] // fn i64_record9_literal() { // assert_evals_to!( // indoc!( @@ -371,7 +367,7 @@ fn i64_record1_literal() { // } // #[test] -// #[cfg(any(feature = "gen-llvm"))] +// #[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] // fn f64_record3_literal() { // assert_evals_to!( // indoc!( @@ -384,13 +380,13 @@ fn i64_record1_literal() { // ); // } #[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] fn bool_literal() { assert_evals_to!( indoc!( r#" x : Bool - x = True + x = Bool.true x "# @@ -418,7 +414,7 @@ fn return_record() { } #[test] -#[cfg(any(feature = "gen-llvm"))] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] fn optional_field_when_use_default() { assert_evals_to!( indoc!( @@ -446,7 +442,7 @@ fn optional_field_when_use_default() { } #[test] -#[cfg(any(feature = "gen-llvm"))] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] fn optional_field_when_use_default_nested() { assert_evals_to!( indoc!( @@ -470,8 +466,8 @@ fn optional_field_when_use_default_nested() { } #[test] -#[cfg(any(feature = "gen-llvm"))] -fn optional_field_when_no_use_default() { +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn optional_field_destructure_module() { assert_evals_to!( indoc!( r#" @@ -491,16 +487,16 @@ fn optional_field_when_no_use_default() { } #[test] -#[cfg(any(feature = "gen-llvm"))] -fn optional_field_when_no_use_default_nested() { +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn optional_field_destructure_expr() { assert_evals_to!( indoc!( r#" - f = \r -> + fn = \r -> { x ? 10, y } = r x + y - f { x: 4, y: 9 } + fn { x: 4, y: 9 } "# ), 13, @@ -530,7 +526,7 @@ fn optional_field_let_use_default() { } #[test] -#[cfg(any(feature = "gen-llvm"))] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] fn optional_field_let_no_use_default() { assert_evals_to!( indoc!( @@ -551,7 +547,7 @@ fn optional_field_let_no_use_default() { } #[test] -#[cfg(any(feature = "gen-llvm"))] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] fn optional_field_let_no_use_default_nested() { assert_evals_to!( indoc!( @@ -560,7 +556,7 @@ fn optional_field_let_no_use_default_nested() { { x ? 10, y } = r x + y - f { x: 4, y: 9 } + f { y: 9, x: 4 } "# ), 13, @@ -586,7 +582,7 @@ fn optional_field_function_use_default() { } #[test] -#[cfg(any(feature = "gen-llvm"))] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] fn optional_field_function_no_use_default() { assert_evals_to!( indoc!( @@ -605,7 +601,7 @@ fn optional_field_function_no_use_default() { } #[test] -#[cfg(any(feature = "gen-llvm"))] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] fn optional_field_function_no_use_default_nested() { assert_evals_to!( indoc!( @@ -741,7 +737,7 @@ fn return_record_float_int() { assert_evals_to!( indoc!( r#" - { a: 1.23, b: 0x1 } + { a: 1.23f64, b: 0x1 } "# ), (1.23, 0x1), @@ -755,7 +751,7 @@ fn return_record_int_float() { assert_evals_to!( indoc!( r#" - { a: 0x1, b: 1.23 } + { a: 0x1, b: 1.23f64 } "# ), (0x1, 1.23), @@ -769,7 +765,7 @@ fn return_record_float_float() { assert_evals_to!( indoc!( r#" - { a: 2.46, b: 1.23 } + { a: 2.46f64, b: 1.23f64 } "# ), (2.46, 1.23), @@ -783,7 +779,7 @@ fn return_record_float_float_float() { assert_evals_to!( indoc!( r#" - { a: 2.46, b: 1.23, c: 0.1 } + { a: 2.46f64, b: 1.23f64, c: 0.1f64 } "# ), (2.46, 1.23, 0.1), @@ -797,7 +793,7 @@ fn return_nested_record() { assert_evals_to!( indoc!( r#" - { flag: 0x0, payload: { a: 2.46, b: 1.23, c: 0.1 } } + { flag: 0x0, payload: { a: 2.46f64, b: 1.23f64, c: 0.1f64 } } "# ), (0x0, (2.46, 1.23, 0.1)), @@ -824,13 +820,13 @@ fn nested_record_load() { } #[test] -#[cfg(any(feature = "gen-llvm"))] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] fn accessor_twice() { - assert_evals_to!(".foo { foo: 4 } + .foo { bar: 2.46, foo: 3 } ", 7, i64); + assert_evals_to!(".foo { foo: 4 } + .foo { bar: 2.46f64, foo: 3 } ", 7, i64); } #[test] -#[cfg(any(feature = "gen-llvm"))] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] fn accessor_multi_element_record() { assert_evals_to!( indoc!( @@ -844,7 +840,7 @@ fn accessor_multi_element_record() { } #[test] -#[cfg(any(feature = "gen-llvm"))] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] fn accessor_single_element_record() { assert_evals_to!( indoc!( @@ -858,12 +854,12 @@ fn accessor_single_element_record() { } #[test] -#[cfg(any(feature = "gen-llvm"))] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] fn update_record() { assert_evals_to!( indoc!( r#" - rec = { foo: 42, bar: 2.46 } + rec = { foo: 42, bar: 2.46f64 } { rec & foo: rec.foo + 1 } "# @@ -915,10 +911,12 @@ fn booleans_in_record() { } #[test] -#[cfg(any(feature = "gen-llvm"))] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] fn alignment_in_record() { assert_evals_to!( - indoc!("{ c: 32, b: if True then Red else if True then Green else Blue, a: 1 == 1 }"), + indoc!( + "{ c: 32, b: if Bool.true then Red else if Bool.true then Green else Blue, a: 1 == 1 }" + ), (32i64, true, 2u8), (i64, bool, u8) ); @@ -988,7 +986,7 @@ fn update_the_only_field() { #[test] #[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -// https://github.com/rtfeldman/roc/issues/1513 +// https://github.com/roc-lang/roc/issues/1513 fn both_have_unique_fields() { assert_evals_to!( indoc!( @@ -1009,7 +1007,7 @@ fn both_have_unique_fields() { #[test] #[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] -// https://github.com/rtfeldman/roc/issues/2535 +// https://github.com/roc-lang/roc/issues/2535 fn different_proc_types_specialized_to_same_layout() { assert_evals_to!( indoc!( @@ -1038,15 +1036,12 @@ fn different_proc_types_specialized_to_same_layout() { } #[test] -#[cfg(any(feature = "gen-llvm"))] -#[should_panic( - // TODO: something upstream is escaping the ' - // NOTE: Are we sure it's upstream? It's not escaped in gen-wasm version below! - expected = r#"Roc failed with message: "Can\'t create record with improper layout""# -)] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +#[should_panic(expected = r#"Roc failed with message: "Can't create record with improper layout""#)] fn call_with_bad_record_runtime_error() { - expect_runtime_error_panic!(indoc!( - r#" + assert_evals_to!( + indoc!( + r#" app "test" provides [main] to "./platform" main = @@ -1054,23 +1049,12 @@ fn call_with_bad_record_runtime_error() { get = \{a} -> a get {b: ""} "# - )) -} - -#[test] -#[cfg(any(feature = "gen-wasm"))] -#[should_panic(expected = r#"Can't create record with improper layout"#)] -fn call_with_bad_record_runtime_error() { - expect_runtime_error_panic!(indoc!( - r#" - app "test" provides [main] to "./platform" - - main = - get : {a: Bool} -> Bool - get = \{a} -> a - get {b: ""} - "# - )) + ), + true, + bool, + |x| x, + true // ignore type errors + ) } #[test] @@ -1088,3 +1072,81 @@ fn generalized_accessor() { RocStr ); } + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn update_record_that_is_a_thunk() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + main = Num.toStr fromOriginal.birds + + original = { birds: 5, iguanas: 7, zebras: 2, goats: 1 } + + fromOriginal = { original & birds: 4, iguanas: 3 } + "# + ), + RocStr::from("4"), + RocStr + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn update_record_that_is_a_thunk_single_field() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + main = Num.toStr fromOriginal.birds + + original = { birds: 5 } + + fromOriginal = { original & birds: 4 } + "# + ), + RocStr::from("4"), + RocStr + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn toplevel_accessor_fn_thunk() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + ra = .field + + main = + ra { field : 15u8 } + "# + ), + 15u8, + u8 + ) +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn pass_record_of_u8s() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + ra = \_ -> 1u8 + + main = + ra { a: 1u8, b: 0u8 } + "# + ), + true, + bool + ) +} diff --git a/crates/compiler/test_gen/src/gen_refcount.rs b/crates/compiler/test_gen/src/gen_refcount.rs new file mode 100644 index 0000000000..de2c846380 --- /dev/null +++ b/crates/compiler/test_gen/src/gen_refcount.rs @@ -0,0 +1,573 @@ +#[cfg(feature = "gen-wasm")] +use crate::helpers::{wasm::assert_refcounts, RefCount::*}; + +#[allow(unused_imports)] +use indoc::indoc; + +#[allow(unused_imports)] +use roc_std::{RocList, RocStr}; + +// A "good enough" representation of a pointer for these tests, because +// we ignore the return value. As long as it's the right stack size, it's fine. +#[allow(dead_code)] +type Pointer = usize; + +#[test] +#[cfg(feature = "gen-wasm")] +fn str_inc() { + assert_refcounts!( + indoc!( + r#" + s = Str.concat "A long enough string " "to be heap-allocated" + + [s, s, s] + "# + ), + RocList, + &[ + Live(3), // s + Live(1) // result + ] + ); +} + +#[test] +#[cfg(feature = "gen-wasm")] +fn str_dealloc() { + assert_refcounts!( + indoc!( + r#" + s = Str.concat "A long enough string " "to be heap-allocated" + + Str.isEmpty s + "# + ), + bool, + &[Deallocated] + ); +} + +#[test] +#[cfg(feature = "gen-wasm")] +fn list_int_inc() { + assert_refcounts!( + indoc!( + r#" + list = [0x111, 0x222, 0x333] + [list, list, list] + "# + ), + RocList>, + &[ + Live(3), // list + Live(1) // result + ] + ); +} + +#[test] +#[cfg(feature = "gen-wasm")] +fn list_int_dealloc() { + assert_refcounts!( + indoc!( + r#" + list = [0x111, 0x222, 0x333] + List.len [list, list, list] + "# + ), + usize, + &[ + Deallocated, // list + Deallocated // result + ] + ); +} + +#[test] +#[cfg(feature = "gen-wasm")] +fn list_str_inc() { + assert_refcounts!( + indoc!( + r#" + s = Str.concat "A long enough string " "to be heap-allocated" + list = [s, s, s] + [list, list] + "# + ), + RocList>, + &[ + Live(6), // s + Live(2), // list + Live(1) // result + ] + ); +} + +#[test] +#[cfg(feature = "gen-wasm")] +fn list_str_dealloc() { + assert_refcounts!( + indoc!( + r#" + s = Str.concat "A long enough string " "to be heap-allocated" + list = [s, s, s] + List.len [list, list] + "# + ), + usize, + &[ + Deallocated, // s + Deallocated, // list + Deallocated // result + ] + ); +} + +#[test] +#[cfg(feature = "gen-wasm")] +fn struct_inc() { + assert_refcounts!( + indoc!( + r#" + s = Str.concat "A long enough string " "to be heap-allocated" + r1 : { a: I64, b: Str, c: Str } + r1 = { a: 123, b: s, c: s } + { y: r1, z: r1 } + "# + ), + [(i64, RocStr, RocStr); 2], + &[Live(4)] // s + ); +} + +#[test] +#[cfg(feature = "gen-wasm")] +fn struct_dealloc() { + assert_refcounts!( + indoc!( + r#" + s = Str.concat "A long enough string " "to be heap-allocated" + r1 : { a: I64, b: Str, c: Str } + r1 = { a: 123, b: s, c: s } + r2 = { x: 456, y: r1, z: r1 } + r2.x + "# + ), + i64, + &[Deallocated] // s + ); +} + +#[test] +#[cfg(feature = "gen-wasm")] +fn union_nonrecursive_inc() { + type TwoStr = (RocStr, RocStr, i64); + + assert_refcounts!( + indoc!( + r#" + TwoOrNone a: [Two a a, None] + + s = Str.concat "A long enough string " "to be heap-allocated" + + two : TwoOrNone Str + two = Two s s + + four : TwoOrNone (TwoOrNone Str) + four = Two two two + + four + "# + ), + (TwoStr, TwoStr, i64), + &[Live(4)] + ); +} + +#[test] +#[cfg(feature = "gen-wasm")] +fn union_nonrecursive_dec() { + assert_refcounts!( + indoc!( + r#" + TwoOrNone a: [Two a a, None] + + s = Str.concat "A long enough string " "to be heap-allocated" + + two : TwoOrNone Str + two = Two s s + + when two is + Two x _ -> x + None -> "" + "# + ), + RocStr, + &[Live(1)] // s + ); +} + +#[test] +#[cfg(feature = "gen-wasm")] +fn union_recursive_inc() { + assert_refcounts!( + indoc!( + r#" + Expr : [Sym Str, Add Expr Expr] + + s = Str.concat "heap_allocated" "_symbol_name" + + x : Expr + x = Sym s + + e : Expr + e = Add x x + + Pair e e + "# + ), + (Pointer, Pointer), + &[ + Live(1), // s + Live(2), // x + Live(2), // e + ] + ); +} + +#[test] +#[cfg(feature = "gen-wasm")] +fn union_recursive_dec() { + assert_refcounts!( + indoc!( + r#" + Expr : [Sym Str, Add Expr Expr] + + s = Str.concat "heap_allocated" "_symbol_name" + + x : Expr + x = Sym s + + e : Expr + e = Add x x + + when e is + Add y _ -> y + Sym _ -> e + "# + ), + Pointer, + &[ + Live(1), // s + Live(1), // sym + Deallocated // e + ] + ); +} + +#[test] +#[cfg(feature = "gen-wasm")] +fn refcount_different_rosetrees_inc() { + // Requires two different Inc procedures for `List (Rose I64)` and `List (Rose Str)` + // even though both appear in the mono Layout as `List(RecursivePointer)` + assert_refcounts!( + indoc!( + r#" + Rose a : [Rose a (List (Rose a))] + + s = Str.concat "A long enough string " "to be heap-allocated" + + i1 : Rose I64 + i1 = Rose 999 [] + + s1 : Rose Str + s1 = Rose s [] + + i2 : Rose I64 + i2 = Rose 0 [i1, i1, i1] + + s2 : Rose Str + s2 = Rose "" [s1, s1] + + Tuple i2 s2 + "# + ), + (Pointer, Pointer), + &[ + Live(1), // s + Live(3), // i1 + Live(2), // s1 + Live(1), // [i1, i1] + Live(1), // i2 + Live(1), // [s1, s1] + Live(1) // s2 + ] + ); +} + +#[test] +#[cfg(feature = "gen-wasm")] +fn refcount_different_rosetrees_dec() { + // Requires two different Dec procedures for `List (Rose I64)` and `List (Rose Str)` + // even though both appear in the mono Layout as `List(RecursivePointer)` + assert_refcounts!( + indoc!( + r#" + Rose a : [Rose a (List (Rose a))] + + s = Str.concat "A long enough string " "to be heap-allocated" + + i1 : Rose I64 + i1 = Rose 999 [] + + s1 : Rose Str + s1 = Rose s [] + + i2 : Rose I64 + i2 = Rose 0 [i1, i1] + + s2 : Rose Str + s2 = Rose "" [s1, s1] + + when (Tuple i2 s2) is + Tuple (Rose x _) _ -> x + "# + ), + i64, + &[ + Deallocated, // s + Deallocated, // i1 + Deallocated, // s1 + Deallocated, // [i1, i1] + Deallocated, // i2 + Deallocated, // [s1, s1] + Deallocated, // s2 + ] + ); +} + +#[test] +#[cfg(feature = "gen-wasm")] +fn union_linked_list_inc() { + assert_refcounts!( + indoc!( + r#" + LinkedList a : [Nil, Cons a (LinkedList a)] + + s = Str.concat "A long enough string " "to be heap-allocated" + + linked : LinkedList Str + linked = Cons s (Cons s (Cons s Nil)) + + Tuple linked linked + "# + ), + (Pointer, Pointer), + &[ + Live(3), // s + Live(1), // inner-most Cons + Live(1), // middle Cons + Live(2), // linked + ] + ); +} + +#[test] +#[cfg(feature = "gen-wasm")] +fn union_linked_list_dec() { + assert_refcounts!( + indoc!( + r#" + LinkedList a : [Nil, Cons a (LinkedList a)] + + s = Str.concat "A long enough string " "to be heap-allocated" + + linked : LinkedList Str + linked = Cons s (Cons s (Cons s Nil)) + + when linked is + Cons x _ -> x + Nil -> "" + "# + ), + RocStr, + &[ + Live(1), // s + Deallocated, // Cons + Deallocated, // Cons + Deallocated, // Cons + ] + ); +} + +#[test] +#[cfg(feature = "gen-wasm")] +fn union_linked_list_nil_dec() { + let no_refcounts: &[crate::helpers::RefCount] = &[]; + assert_refcounts!( + indoc!( + r#" + LinkedList a : [Nil, Cons a (LinkedList a)] + + linked : LinkedList Str + linked = Nil + + when linked is + Cons x _ -> x + Nil -> "" + "# + ), + RocStr, + no_refcounts + ); +} + +#[test] +#[cfg(feature = "gen-wasm")] +fn union_linked_list_long_dec() { + assert_refcounts!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + LinkedList a : [Nil, Cons a (LinkedList a)] + + prependOnes = \n, tail -> + if n == 0 then + tail + else + prependOnes (n-1) (Cons 1 tail) + + main = + n = 1_000 + + linked : LinkedList I64 + linked = prependOnes n Nil + + when linked is + Cons x _ -> x + Nil -> -1 + "# + ), + i64, + &[Deallocated; 1_000] + ); +} + +#[test] +#[cfg(feature = "gen-wasm")] +fn boxed_str_inc() { + assert_refcounts!( + indoc!( + r#" + s = Str.concat "A long enough string " "to be heap-allocated" + b = Box.box s + + Tuple b b + "# + ), + (Pointer, Pointer), + &[ + Live(1), // s + Live(2), // b + ] + ); +} + +#[test] +#[cfg(feature = "gen-wasm")] +fn boxed_str_dec() { + assert_refcounts!( + indoc!( + r#" + s = Str.concat "A long enough string " "to be heap-allocated" + b = Box.box s + + if Bool.false then + ReturnTheBox b + else + DeallocateEverything + "# + ), + (i32, i32), + &[ + Deallocated, // s + Deallocated, // b + ] + ); +} + +#[test] +#[cfg(feature = "gen-wasm")] +fn non_nullable_unwrapped_alignment_8() { + assert_refcounts!( + indoc!( + r#" + Expr : [ZAdd Expr Expr, Val I64, Var I64] + + eval : Expr -> I64 + eval = \e -> + when e is + Var _ -> 0 + Val v -> v + ZAdd l r -> eval l + eval r + + expr : Expr + expr = (ZAdd (Val 4) (Val 5)) + + eval expr + "# + ), + i64, + &[ + Deallocated, // Val 4 + Deallocated, // Val 5 + Deallocated, // ZAdd _ _ + ] + ); +} + +#[test] +#[cfg(feature = "gen-wasm")] +fn reset_reuse_alignment_8() { + assert_refcounts!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + Expr : [ZAdd Expr Expr, Val I64, Var I64] + + eval : Expr -> I64 + eval = \e -> + when e is + Var _ -> 0 + Val v -> v + ZAdd l r -> eval l + eval r + + constFolding : Expr -> Expr + constFolding = \e -> + when e is + ZAdd e1 e2 -> + when Pair e1 e2 is + Pair (Val a) (Val b) -> Val (a+b) + Pair _ _ -> ZAdd e1 e2 + + + _ -> e + + + expr : Expr + expr = ZAdd (Val 4) (Val 5) + + main : I64 + main = eval (constFolding expr) + "# + ), + i64, + &[ + Deallocated, // Val 4 + Deallocated, // Val 5 + Deallocated, // ZAdd _ _ + ] + ); +} diff --git a/crates/compiler/test_gen/src/gen_result.rs b/crates/compiler/test_gen/src/gen_result.rs new file mode 100644 index 0000000000..99f4f3ff09 --- /dev/null +++ b/crates/compiler/test_gen/src/gen_result.rs @@ -0,0 +1,356 @@ +#[cfg(feature = "gen-llvm")] +use crate::helpers::llvm::assert_evals_to; + +#[cfg(feature = "gen-dev")] +use crate::helpers::dev::assert_evals_to; + +#[cfg(feature = "gen-wasm")] +use crate::helpers::wasm::assert_evals_to; + +use indoc::indoc; + +#[allow(unused_imports)] +use roc_std::{RocResult, RocStr}; + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn with_default_ok() { + assert_evals_to!( + indoc!( + r#" + result : Result I64 {} + result = Ok 12345 + + Result.withDefault result 0 + "# + ), + 12345, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn with_default_err() { + assert_evals_to!( + indoc!( + r#" + result : Result I64 {} + result = Err {} + + Result.withDefault result 0 + "# + ), + 0, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn result_map() { + assert_evals_to!( + indoc!( + r#" + result : Result I64 {} + result = Ok 2 + + result + |> Result.map (\x -> x + 1) + |> Result.withDefault 0 + "# + ), + 3, + i64 + ); + + assert_evals_to!( + indoc!( + r#" + result : Result I64 {} + result = Err {} + + result + |> Result.map (\x -> x + 1) + |> Result.withDefault 0 + "# + ), + 0, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn result_map_err() { + assert_evals_to!( + indoc!( + r#" + result : Result {} I64 + result = Err 2 + + when Result.mapErr result (\x -> x + 1) is + Err n -> n + Ok _ -> 0 + "# + ), + 3, + i64 + ); + + assert_evals_to!( + indoc!( + r#" + result : Result {} I64 + result = Ok {} + + when Result.mapErr result (\x -> x + 1) is + Err n -> n + Ok _ -> 0 + "# + ), + 0, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn err_type_var() { + assert_evals_to!( + indoc!( + r#" + Result.map (Ok 3) (\x -> x + 1) + |> Result.withDefault -1 + "# + ), + 4, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn err_type_var_annotation() { + assert_evals_to!( + indoc!( + r#" + ok : Result I64 * + ok = Ok 3 + + Result.map ok (\x -> x + 1) + |> Result.withDefault -1 + "# + ), + 4, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn err_empty_tag_union() { + assert_evals_to!( + indoc!( + r#" + ok : Result I64 [] + ok = Ok 3 + + Result.map ok (\x -> x + 1) + |> Result.withDefault -1 + "# + ), + 4, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn is_ok() { + assert_evals_to!( + indoc!( + r#" + result : Result I64 {} + result = Ok 2 + + Result.isOk result + "# + ), + true, + bool + ); + + assert_evals_to!( + indoc!( + r#" + result : Result I64 {} + result = Err {} + + Result.isOk result + "# + ), + false, + bool + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn is_err() { + assert_evals_to!( + indoc!( + r#" + result : Result I64 {} + result = Ok 2 + + Result.isErr result + "# + ), + false, + bool + ); + + assert_evals_to!( + indoc!( + r#" + result : Result I64 {} + result = Err {} + + Result.isErr result + "# + ), + true, + bool + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn roc_result_ok_i64() { + assert_evals_to!( + indoc!( + r#" + result : Result I64 {} + result = Ok 42 + + result + "# + ), + RocResult::ok(42), + RocResult + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn roc_result_ok_f64() { + // NOTE: the dev backend does not currently use float registers when returning a more + // complex type, but the rust side does expect it to. Hence this test fails with gen-dev + + assert_evals_to!( + indoc!( + r#" + result : Result F64 {} + result = Ok 42.0 + + result + "# + ), + RocResult::ok(42.0), + RocResult + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn roc_result_err() { + assert_evals_to!( + indoc!( + r#" + result : Result I64 Str + result = Err "foo" + + result + "# + ), + RocResult::err(RocStr::from("foo")), + RocResult + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn issue_2583_specialize_errors_behind_unified_branches() { + assert_evals_to!( + r#" + if Bool.true then List.first [15] else Str.toI64 "" + "#, + RocResult::ok(15i64), + RocResult + ) +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn roc_result_after_on_ok() { + assert_evals_to!(indoc!( + r#" + input : Result I64 Str + input = Ok 1 + + Result.try input \num -> + if num < 0 then Err "negative!" else Ok -num + "#), + RocResult::ok(-1), + RocResult + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn roc_result_after_on_err() { + assert_evals_to!(indoc!( + r#" + input : Result I64 Str + input = (Err "already a string") + + Result.try input \num -> + if num < 0 then Err "negative!" else Ok -num + "#), + RocResult::err(RocStr::from("already a string")), + RocResult + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn roc_result_after_err() { + assert_evals_to!( + indoc!( + r#" + result : Result Str I64 + result = + Result.onErr (Ok "already a string") \num -> + if num < 0 then Ok "negative!" else Err -num + + result + "# + ), + RocResult::ok(RocStr::from("already a string")), + RocResult + ); + + assert_evals_to!(indoc!( + r#" + result : Result Str I64 + result = + Result.onErr (Err 100) \num -> + if num < 0 then Ok "negative!" else Err -num + + result + "#), + RocResult::err(-100), + RocResult + ); +} diff --git a/crates/compiler/test_gen/src/gen_set.rs b/crates/compiler/test_gen/src/gen_set.rs new file mode 100644 index 0000000000..e046899e0f --- /dev/null +++ b/crates/compiler/test_gen/src/gen_set.rs @@ -0,0 +1,312 @@ +#![cfg(all( + feature = "gen-llvm", + not(debug_assertions) // https://github.com/roc-lang/roc/issues/3898 +))] + +#[cfg(feature = "gen-llvm")] +use crate::helpers::llvm::assert_evals_to; + +// #[cfg(feature = "gen-dev")] +// use crate::helpers::dev::assert_evals_to; + +// #[cfg(feature = "gen-wasm")] +// use crate::helpers::wasm::assert_evals_to; + +use indoc::indoc; +use roc_std::RocList; + +#[test] +#[cfg(feature = "gen-llvm")] +fn empty_len() { + assert_evals_to!( + indoc!( + r#" + Set.len (Set.empty {}) + "# + ), + 0, + usize + ); +} + +#[test] +#[cfg(feature = "gen-llvm")] +fn single_len() { + assert_evals_to!( + indoc!( + r#" + Set.len (Set.single 42) + "# + ), + 1, + usize + ); +} + +#[test] +#[cfg(feature = "gen-llvm")] +fn single_to_list() { + assert_evals_to!( + indoc!( + r#" + Set.toList (Set.single 42) + "# + ), + RocList::from_slice(&[42]), + RocList + ); + + assert_evals_to!( + indoc!( + r#" + Set.toList (Set.single 1) + "# + ), + RocList::from_slice(&[1]), + RocList + ); +} + +#[test] +#[cfg(feature = "gen-llvm")] +fn insert() { + assert_evals_to!( + indoc!( + r#" + Set.empty {} + |> Set.insert 0 + |> Set.insert 1 + |> Set.insert 2 + |> Set.toList + "# + ), + RocList::from_slice(&[0, 1, 2]), + RocList + ); +} + +#[test] +#[cfg(feature = "gen-llvm")] +fn remove() { + assert_evals_to!( + indoc!( + r#" + Set.empty {} + |> Set.insert 0 + |> Set.insert 1 + |> Set.remove 1 + |> Set.remove 2 + |> Set.toList + "# + ), + RocList::from_slice(&[0]), + RocList + ); +} + +#[test] +#[cfg(feature = "gen-llvm")] +fn union() { + assert_evals_to!( + indoc!( + r#" + set1 : Set.Set I64 + set1 = Set.fromList [1,2] + + set2 : Set.Set I64 + set2 = Set.fromList [1,3,4] + + Set.union set1 set2 + |> Set.toList + "# + ), + RocList::from_slice(&[1, 2, 3, 4]), + RocList + ); +} + +#[test] +#[cfg(feature = "gen-llvm")] +fn difference() { + assert_evals_to!( + indoc!( + r#" + set1 : Set.Set I64 + set1 = Set.fromList [1,2] + + set2 : Set.Set I64 + set2 = Set.fromList [1,3,4] + + Set.difference set1 set2 + |> Set.toList + "# + ), + RocList::from_slice(&[2]), + RocList + ); +} + +#[test] +#[cfg(feature = "gen-llvm")] +fn intersection() { + assert_evals_to!( + indoc!( + r#" + set1 : Set.Set I64 + set1 = Set.fromList [1,2] + + set2 : Set.Set I64 + set2 = Set.fromList [1,3,4] + + Set.intersection set1 set2 + |> Set.toList + "# + ), + RocList::from_slice(&[1]), + RocList + ); +} + +#[test] +#[cfg(feature = "gen-llvm")] +fn walk_sum() { + assert_evals_to!( + indoc!( + r#" + Set.walk (Set.fromList [1,2,3]) 0 (\x, y -> x + y) + "# + ), + 6, + i64 + ); +} + +#[test] +#[cfg(feature = "gen-llvm")] +fn contains() { + assert_evals_to!( + indoc!( + r#" + Set.contains (Set.fromList [1,3,4]) 4 + "# + ), + true, + bool + ); + + assert_evals_to!( + indoc!( + r#" + Set.contains (Set.fromList [1,3,4]) 2 + "# + ), + false, + bool + ); +} + +#[test] +#[cfg(feature = "gen-llvm")] +fn from_list() { + assert_evals_to!( + indoc!( + r#" + [1,2,2,3,1,4] + |> Set.fromList + |> Set.toList + "# + ), + RocList::from_slice(&[1, 2, 3, 4]), + RocList + ); + + assert_evals_to!( + indoc!( + r#" + empty : List I64 + empty = [] + + empty + |> Set.fromList + |> Set.toList + "# + ), + RocList::::default(), + RocList + ); +} + +#[test] +#[ignore] +#[cfg(feature = "gen-llvm")] +fn from_list_void() { + assert_evals_to!( + indoc!( + r#" + [] + |> Set.fromList + |> Set.toList + "# + ), + RocList::::default(), + RocList + ); +} + +#[test] +#[cfg(feature = "gen-llvm")] +fn to_list_empty() { + assert_evals_to!( + indoc!( + r#" + Set.toList (Set.empty {}) + "# + ), + RocList::::default(), + RocList + ); +} + +#[test] +#[cfg(feature = "gen-llvm")] +fn from_list_result() { + assert_evals_to!( + indoc!( + r#" + x : Result Str {} + x = Ok "foo" + + [x] + |> Set.fromList + |> Set.toList + |> List.len + |> Num.toI64 + "# + ), + 1, + i64 + ); +} + +#[test] +#[cfg(feature = "gen-llvm")] +fn resolve_set_eq_issue_4671() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + main = + s1 : Set U8 + s1 = Set.fromList [1, 2, 3] + + s2 : Set U8 + s2 = Set.fromList [3, 2, 1] + + s1 == s2 + "# + ), + true, + bool + ); +} diff --git a/crates/compiler/test_gen/src/gen_str.rs b/crates/compiler/test_gen/src/gen_str.rs new file mode 100644 index 0000000000..77ce175adb --- /dev/null +++ b/crates/compiler/test_gen/src/gen_str.rs @@ -0,0 +1,2206 @@ +#![cfg(not(feature = "gen-wasm"))] + +#[cfg(feature = "gen-llvm")] +use crate::helpers::llvm::assert_evals_to; +#[cfg(feature = "gen-llvm")] +use crate::helpers::llvm::assert_llvm_evals_to; + +#[cfg(feature = "gen-dev")] +use crate::helpers::dev::assert_evals_to; +#[cfg(feature = "gen-dev")] +use crate::helpers::dev::assert_evals_to as assert_llvm_evals_to; + +#[allow(unused_imports)] +use indoc::indoc; +#[allow(unused_imports)] +use roc_std::{RocList, RocResult, RocStr}; + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn string_eq() { + // context: the dev backend did not correctly mask the boolean that zig returns here + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + main : I64 + main = if "*" == "*" then 123 else 456 + "# + ), + 123, + u64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn string_neq() { + // context: the dev backend did not correctly mask the boolean that zig returns here + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + main : I64 + main = if "*" != "*" then 123 else 456 + "# + ), + 456, + u64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev"))] +fn str_split_empty_delimiter() { + assert_evals_to!( + indoc!( + r#" + List.len (Str.split "hello" "") + "# + ), + 1, + usize + ); + + assert_evals_to!( + indoc!( + r#" + when List.first (Str.split "JJJ" "") is + Ok str -> + Str.countGraphemes str + + _ -> + 1729 + + "# + ), + 3, + usize + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev"))] +fn str_split_bigger_delimiter_small_str() { + assert_evals_to!( + indoc!( + r#" + List.len (Str.split "hello" "JJJJ there") + "# + ), + 1, + usize + ); + + assert_evals_to!( + indoc!( + r#" + when List.first (Str.split "JJJ" "JJJJ there") is + Ok str -> + Str.countGraphemes str + + _ -> + 1729 + + "# + ), + 3, + usize + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev"))] +fn str_split_str_concat_repeated() { + assert_evals_to!( + indoc!( + r#" + when List.first (Str.split "JJJJJ" "JJJJ there") is + Ok str -> + str + |> Str.concat str + |> Str.concat str + |> Str.concat str + |> Str.concat str + + _ -> + "Not Str!" + + "# + ), + RocStr::from("JJJJJJJJJJJJJJJJJJJJJJJJJ"), + RocStr + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev"))] +fn str_split_small_str_bigger_delimiter() { + assert_evals_to!( + indoc!(r#"Str.split "JJJ" "0123456789abcdefghi""#), + RocList::from_slice(&[RocStr::from("JJJ")]), + RocList + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev"))] +fn str_split_big_str_small_delimiter() { + assert_evals_to!( + indoc!( + r#" + Str.split "01234567789abcdefghi?01234567789abcdefghi" "?" + "# + ), + RocList::from_slice(&[ + RocStr::from("01234567789abcdefghi"), + RocStr::from("01234567789abcdefghi") + ]), + RocList + ); + + assert_evals_to!( + indoc!( + r#" + Str.split "01234567789abcdefghi 3ch 01234567789abcdefghi" "3ch" + "# + ), + RocList::from_slice(&[ + RocStr::from("01234567789abcdefghi "), + RocStr::from(" 01234567789abcdefghi") + ]), + RocList + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev"))] +fn str_split_small_str_small_delimiter() { + assert_evals_to!( + indoc!( + r#" + Str.split "J!J!J" "!" + "# + ), + RocList::from_slice(&[RocStr::from("J"), RocStr::from("J"), RocStr::from("J")]), + RocList + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev"))] +fn str_split_bigger_delimiter_big_strs() { + assert_evals_to!( + indoc!( + r#" + Str.split + "string to split is shorter" + "than the delimiter which happens to be very very long" + "# + ), + RocList::from_slice(&[RocStr::from("string to split is shorter")]), + RocList + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev"))] +fn str_split_empty_strs() { + assert_evals_to!( + indoc!( + r#" + Str.split "" "" + "# + ), + RocList::from_slice(&[RocStr::from("")]), + RocList + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev"))] +fn str_split_minimal_example() { + assert_evals_to!( + indoc!( + r#" + Str.split "a," "," + "# + ), + RocList::from_slice(&[RocStr::from("a"), RocStr::from("")]), + RocList + ) +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev"))] +fn str_split_small_str_big_delimiter() { + assert_evals_to!( + indoc!( + r#" + Str.split + "1---- ---- ---- ---- ----2---- ---- ---- ---- ----" + "---- ---- ---- ---- ----" + |> List.len + "# + ), + 3, + usize + ); + + assert_evals_to!( + indoc!( + r#" + Str.split + "1---- ---- ---- ---- ----2---- ---- ---- ---- ----" + "---- ---- ---- ---- ----" + "# + ), + RocList::from_slice(&[RocStr::from("1"), RocStr::from("2"), RocStr::from("")]), + RocList + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev"))] +fn str_split_small_str_20_char_delimiter() { + assert_evals_to!( + indoc!( + r#" + Str.split + "3|-- -- -- -- -- -- |4|-- -- -- -- -- -- |" + "|-- -- -- -- -- -- |" + "# + ), + RocList::from_slice(&[RocStr::from("3"), RocStr::from("4"), RocStr::from("")]), + RocList + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev"))] +fn str_concat_big_to_big() { + assert_evals_to!( + indoc!( + r#" + Str.concat + "First string that is fairly long. Longer strings make for different errors. " + "Second string that is also fairly long. Two long strings test things that might not appear with short strings." + "# + ), + RocStr::from("First string that is fairly long. Longer strings make for different errors. Second string that is also fairly long. Two long strings test things that might not appear with short strings."), + RocStr + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev"))] +fn small_str_literal() { + assert_llvm_evals_to!( + "\"JJJJJJJJJJJJJJJJJJJJJJJ\"", + [ + b'J', + b'J', + b'J', + b'J', + b'J', + b'J', + b'J', + b'J', + b'J', + b'J', + b'J', + b'J', + b'J', + b'J', + b'J', + b'J', + b'J', + b'J', + b'J', + b'J', + b'J', + b'J', + b'J', + 0b1000_0000 | 23 + ], + [u8; 24] + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev"))] +fn small_str_zeroed_literal() { + // Verifies that we zero out unused bytes in the string. + // This is important so that string equality tests don't randomly + // fail due to unused memory being there! + assert_llvm_evals_to!( + "\"J\"", + [ + 0x4a, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0b1000_0001 + ], + [u8; 24] + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev"))] +fn small_str_concat_empty_first_arg() { + assert_llvm_evals_to!( + r#"Str.concat "" "JJJJJJJJJJJJJJJ""#, + [ + b'J', + b'J', + b'J', + b'J', + b'J', + b'J', + b'J', + b'J', + b'J', + b'J', + b'J', + b'J', + b'J', + b'J', + b'J', + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0b1000_0000 | 15 + ], + [u8; 24] + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev"))] +fn small_str_concat_empty_second_arg() { + assert_llvm_evals_to!( + r#"Str.concat "JJJJJJJJJJJJJJJ" """#, + [ + b'J', + b'J', + b'J', + b'J', + b'J', + b'J', + b'J', + b'J', + b'J', + b'J', + b'J', + b'J', + b'J', + b'J', + b'J', + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0b1000_0000 | 15 + ], + [u8; 24] + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev"))] +fn small_str_concat_small_to_big() { + assert_evals_to!( + r#"Str.concat "abc" " this is longer than 15 chars""#, + RocStr::from("abc this is longer than 15 chars"), + RocStr + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev"))] +fn small_str_concat_small_to_small_staying_small() { + assert_llvm_evals_to!( + r#"Str.concat "J" "JJJJJJJJJJJJJJ""#, + [ + b'J', + b'J', + b'J', + b'J', + b'J', + b'J', + b'J', + b'J', + b'J', + b'J', + b'J', + b'J', + b'J', + b'J', + b'J', + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0b1000_0000 | 15 + ], + [u8; 24] + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev"))] +fn small_str_concat_small_to_small_overflow_to_big() { + assert_evals_to!( + r#"Str.concat "abcdefghijklm" "nopqrstuvwxyz""#, + RocStr::from("abcdefghijklmnopqrstuvwxyz"), + RocStr + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev"))] +fn str_concat_empty() { + assert_evals_to!(r#"Str.concat "" """#, RocStr::default(), RocStr); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev"))] +fn small_str_is_empty() { + assert_evals_to!(r#"Str.isEmpty "abc""#, false, bool); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev"))] +fn big_str_is_empty() { + assert_evals_to!( + r#"Str.isEmpty "this is more than 23 chars long""#, + false, + bool + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev"))] +fn empty_str_is_empty() { + assert_evals_to!(r#"Str.isEmpty """#, true, bool); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev"))] +fn str_starts_with() { + assert_evals_to!(r#"Str.startsWith "hello world" "hell""#, true, bool); + assert_evals_to!(r#"Str.startsWith "hello world" """#, true, bool); + assert_evals_to!(r#"Str.startsWith "nope" "hello world""#, false, bool); + assert_evals_to!(r#"Str.startsWith "hell" "hello world""#, false, bool); + assert_evals_to!(r#"Str.startsWith "" "hello world""#, false, bool); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev"))] +fn str_starts_with_scalar() { + assert_evals_to!( + &format!(r#"Str.startsWithScalar "foobar" {}"#, 'f' as u32), + true, + bool + ); + assert_evals_to!( + &format!(r#"Str.startsWithScalar "zoobar" {}"#, 'f' as u32), + false, + bool + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev"))] +fn str_ends_with() { + assert_evals_to!(r#"Str.endsWith "hello world" "world""#, true, bool); + assert_evals_to!(r#"Str.endsWith "nope" "hello world""#, false, bool); + assert_evals_to!(r#"Str.endsWith "" "hello world""#, false, bool); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev"))] +fn str_count_graphemes_small_str() { + assert_evals_to!(r#"Str.countGraphemes "å🤔""#, 2, usize); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev"))] +fn str_count_graphemes_three_js() { + assert_evals_to!(r#"Str.countGraphemes "JJJ""#, 3, usize); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev"))] +fn str_count_graphemes_big_str() { + assert_evals_to!( + r#"Str.countGraphemes "6🤔å🤔e¥🤔çppkd🙃1jdal🦯asdfa∆ltråø˚waia8918.,🏅jjc""#, + 45, + usize + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev"))] +fn str_starts_with_same_big_str() { + assert_evals_to!( + r#"Str.startsWith "123456789123456789" "123456789123456789""#, + true, + bool + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev"))] +fn str_starts_with_different_big_str() { + assert_evals_to!( + r#"Str.startsWith "12345678912345678910" "123456789123456789""#, + true, + bool + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev"))] +fn str_starts_with_same_small_str() { + assert_evals_to!(r#"Str.startsWith "1234" "1234""#, true, bool); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev"))] +fn str_starts_with_different_small_str() { + assert_evals_to!(r#"Str.startsWith "1234" "12""#, true, bool); +} +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev"))] +fn str_starts_with_false_small_str() { + assert_evals_to!(r#"Str.startsWith "1234" "23""#, false, bool); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev"))] +fn str_from_utf8_pass_single_ascii() { + assert_evals_to!( + indoc!( + r#" + when Str.fromUtf8 [97] is + Ok val -> val + Err _ -> "" + "# + ), + roc_std::RocStr::from("a"), + roc_std::RocStr + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev"))] +fn str_from_utf8_pass_many_ascii() { + assert_evals_to!( + indoc!( + r#" + when Str.fromUtf8 [97, 98, 99, 0x7E] is + Ok val -> val + Err _ -> "" + "# + ), + roc_std::RocStr::from("abc~"), + roc_std::RocStr + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev"))] +fn str_from_utf8_pass_single_unicode() { + assert_evals_to!( + indoc!( + r#" + when Str.fromUtf8 [0xE2, 0x88, 0x86] is + Ok val -> val + Err _ -> "" + "# + ), + roc_std::RocStr::from("∆"), + roc_std::RocStr + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev"))] +fn str_from_utf8_pass_many_unicode() { + assert_evals_to!( + indoc!( + r#" + when Str.fromUtf8 [0xE2, 0x88, 0x86, 0xC5, 0x93, 0xC2, 0xAC] is + Ok val -> val + Err _ -> "" + "# + ), + roc_std::RocStr::from("∆œ¬"), + roc_std::RocStr + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev"))] +fn str_from_utf8_pass_single_grapheme() { + assert_evals_to!( + indoc!( + r#" + when Str.fromUtf8 [0xF0, 0x9F, 0x92, 0x96] is + Ok val -> val + Err _ -> "" + "# + ), + roc_std::RocStr::from("💖"), + roc_std::RocStr + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev"))] +fn str_from_utf8_pass_many_grapheme() { + assert_evals_to!( + indoc!( + r#" + when Str.fromUtf8 [0xF0, 0x9F, 0x92, 0x96, 0xF0, 0x9F, 0xA4, 0xA0, 0xF0, 0x9F, 0x9A, 0x80] is + Ok val -> val + Err _ -> "" + "# + ), + roc_std::RocStr::from("💖🤠🚀"), + roc_std::RocStr + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev"))] +fn str_from_utf8_pass_all() { + assert_evals_to!( + indoc!( + r#" + when Str.fromUtf8 [0xF0, 0x9F, 0x92, 0x96, 98, 0xE2, 0x88, 0x86] is + Ok val -> val + Err _ -> "" + "# + ), + roc_std::RocStr::from("💖b∆"), + roc_std::RocStr + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev"))] +fn str_from_utf8_fail_invalid_start_byte() { + assert_evals_to!( + indoc!( + r#" + when Str.fromUtf8 [97, 98, 0x80, 99] is + Err (BadUtf8 InvalidStartByte byteIndex) -> + if byteIndex == 2 then + "a" + else + "b" + _ -> "" + "# + ), + roc_std::RocStr::from("a"), + roc_std::RocStr + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev"))] +fn str_from_utf8_fail_unexpected_end_of_sequence() { + assert_evals_to!( + indoc!( + r#" + when Str.fromUtf8 [97, 98, 99, 0xC2] is + Err (BadUtf8 UnexpectedEndOfSequence byteIndex) -> + if byteIndex == 3 then + "a" + else + "b" + _ -> "" + "# + ), + roc_std::RocStr::from("a"), + roc_std::RocStr + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev"))] +fn str_from_utf8_fail_expected_continuation() { + assert_evals_to!( + indoc!( + r#" + when Str.fromUtf8 [97, 98, 99, 0xC2, 0x00] is + Err (BadUtf8 ExpectedContinuation byteIndex) -> + if byteIndex == 3 then + "a" + else + "b" + _ -> "" + "# + ), + roc_std::RocStr::from("a"), + roc_std::RocStr + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev"))] +fn str_from_utf8_fail_overlong_encoding() { + assert_evals_to!( + indoc!( + r#" + when Str.fromUtf8 [97, 0xF0, 0x80, 0x80, 0x80] is + Err (BadUtf8 OverlongEncoding byteIndex) -> + if byteIndex == 1 then + "a" + else + "b" + _ -> "" + "# + ), + roc_std::RocStr::from("a"), + roc_std::RocStr + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev"))] +fn str_from_utf8_fail_codepoint_too_large() { + assert_evals_to!( + indoc!( + r#" + when Str.fromUtf8 [97, 0xF4, 0x90, 0x80, 0x80] is + Err (BadUtf8 CodepointTooLarge byteIndex) -> + if byteIndex == 1 then + "a" + else + "b" + _ -> "" + "# + ), + roc_std::RocStr::from("a"), + roc_std::RocStr + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev"))] +fn str_from_utf8_fail_surrogate_half() { + assert_evals_to!( + indoc!( + r#" + when Str.fromUtf8 [97, 98, 0xED, 0xA0, 0x80] is + Err (BadUtf8 EncodesSurrogateHalf byteIndex) -> + if byteIndex == 2 then + "a" + else + "b" + _ -> "" + "# + ), + roc_std::RocStr::from("a"), + roc_std::RocStr + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev"))] +fn str_equality() { + assert_evals_to!(r#""a" == "a""#, true, bool); + assert_evals_to!( + r#""loremipsumdolarsitamet" == "loremipsumdolarsitamet""#, + true, + bool + ); + assert_evals_to!(r#""a" != "b""#, true, bool); + assert_evals_to!(r#""a" == "b""#, false, bool); +} + +#[test] +fn str_clone() { + use roc_std::RocStr; + let long = RocStr::from("loremipsumdolarsitamet"); + let short = RocStr::from("x"); + let empty = RocStr::from(""); + + debug_assert_eq!(long.clone(), long); + debug_assert_eq!(short.clone(), short); + debug_assert_eq!(empty.clone(), empty); +} + +#[test] +#[cfg(feature = "gen-llvm")] +fn nested_recursive_literal() { + assert_evals_to!( + indoc!( + r#" + Expr : [Add Expr Expr, Val I64, Var I64] + + expr : Expr + expr = Add (Add (Val 3) (Val 1)) (Add (Val 1) (Var 1)) + + printExpr : Expr -> Str + printExpr = \e -> + when e is + Add a b -> + "Add (" + |> Str.concat (printExpr a) + |> Str.concat ") (" + |> Str.concat (printExpr b) + |> Str.concat ")" + Val v -> "Val " |> Str.concat (Num.toStr v) + Var v -> "Var " |> Str.concat (Num.toStr v) + + printExpr expr + "# + ), + RocStr::from("Add (Add (Val 3) (Val 1)) (Add (Val 1) (Var 1))"), + RocStr + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev"))] +fn str_join_comma_small() { + assert_evals_to!( + r#"Str.joinWith ["1", "2"] ", " "#, + RocStr::from("1, 2"), + RocStr + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev"))] +fn str_join_comma_big() { + assert_evals_to!( + r#"Str.joinWith ["10000000", "2000000", "30000000"] ", " "#, + RocStr::from("10000000, 2000000, 30000000"), + RocStr + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev"))] +fn str_join_comma_single() { + assert_evals_to!(r#"Str.joinWith ["1"] ", " "#, RocStr::from("1"), RocStr); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev"))] +fn str_to_utf8() { + assert_evals_to!( + r#"Str.toUtf8 "hello""#, + RocList::from_slice(&[104, 101, 108, 108, 111]), + RocList + ); + assert_evals_to!( + r#"Str.toUtf8 "this is a long string""#, + RocList::from_slice(&[ + 116, 104, 105, 115, 32, 105, 115, 32, 97, 32, 108, 111, 110, 103, 32, 115, 116, 114, + 105, 110, 103 + ]), + RocList + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev"))] +fn str_from_utf8_range() { + assert_evals_to!( + indoc!( + r#" + bytes = Str.toUtf8 "hello" + when Str.fromUtf8Range bytes { count: 5, start: 0 } is + Ok utf8String -> utf8String + _ -> "" + "# + ), + RocStr::from("hello"), + RocStr + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev"))] +fn str_from_utf8_range_slice() { + assert_evals_to!( + indoc!( + r#" + bytes = Str.toUtf8 "hello" + when Str.fromUtf8Range bytes { count: 4, start: 1 } is + Ok utf8String -> utf8String + _ -> "" + "# + ), + RocStr::from("ello"), + RocStr + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev"))] +fn str_from_utf8_range_slice_not_end() { + assert_evals_to!( + indoc!( + r#" + bytes = Str.toUtf8 "hello" + when Str.fromUtf8Range bytes { count: 3, start: 1 } is + Ok utf8String -> utf8String + _ -> "" + "# + ), + RocStr::from("ell"), + RocStr + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev"))] +fn str_from_utf8_range_order_does_not_matter() { + assert_evals_to!( + indoc!( + r#" + bytes = Str.toUtf8 "hello" + when Str.fromUtf8Range bytes { start: 1, count: 3 } is + Ok utf8String -> utf8String + _ -> "" + "# + ), + RocStr::from("ell"), + RocStr + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev"))] +fn str_from_utf8_range_out_of_bounds_start_value() { + assert_evals_to!( + indoc!( + r#" + bytes = Str.toUtf8 "hello" + when Str.fromUtf8Range bytes { start: 7, count: 3 } is + Ok _ -> "" + Err (BadUtf8 _ _) -> "" + Err OutOfBounds -> "out of bounds" + "# + ), + RocStr::from("out of bounds"), + RocStr + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev"))] +fn str_from_utf8_range_count_too_high() { + assert_evals_to!( + indoc!( + r#" + bytes = Str.toUtf8 "hello" + when Str.fromUtf8Range bytes { start: 0, count: 6 } is + Ok _ -> "" + Err (BadUtf8 _ _) -> "" + Err OutOfBounds -> "out of bounds" + "# + ), + RocStr::from("out of bounds"), + RocStr + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev"))] +fn str_from_utf8_range_count_too_high_for_start() { + assert_evals_to!( + indoc!( + r#" + bytes = Str.toUtf8 "hello" + when Str.fromUtf8Range bytes { start: 4, count: 3 } is + Ok _ -> "" + Err (BadUtf8 _ _) -> "" + Err OutOfBounds -> "out of bounds" + "# + ), + RocStr::from("out of bounds"), + RocStr + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev"))] +fn str_repeat_small_stays_small() { + assert_evals_to!( + indoc!(r#"Str.repeat "Roc" 3"#), + RocStr::from("RocRocRoc"), + RocStr + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev"))] +fn str_repeat_small_becomes_big() { + assert_evals_to!( + indoc!(r#"Str.repeat "less than 23 characters" 2"#), + RocStr::from("less than 23 charactersless than 23 characters"), + RocStr + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev"))] +fn str_repeat_big() { + assert_evals_to!( + indoc!(r#"Str.repeat "more than 23 characters now" 2"#), + RocStr::from("more than 23 characters nowmore than 23 characters now"), + RocStr + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev"))] +fn str_repeat_empty_string() { + let a = indoc!(r#"Str.repeat "" 3"#); + assert_evals_to!(a, RocStr::from(""), RocStr); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev"))] +fn str_repeat_zero_times() { + assert_evals_to!(indoc!(r#"Str.repeat "Roc" 0"#), RocStr::from(""), RocStr); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev"))] +fn str_trim_empty_string() { + assert_evals_to!(indoc!(r#"Str.trim """#), RocStr::from(""), RocStr); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev"))] +fn str_trim_null_byte() { + assert_evals_to!( + indoc!(r#"Str.trim (Str.reserve "\u(0000)" 40)"#), + RocStr::from("\0"), + RocStr + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev"))] +fn str_trim_small_blank_string() { + assert_evals_to!(indoc!(r#"Str.trim " ""#), RocStr::from(""), RocStr); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev"))] +fn str_trim_small_to_small() { + assert_evals_to!( + indoc!(r#"Str.trim " hello world ""#), + RocStr::from("hello world"), + RocStr + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev"))] +fn str_trim_large_to_large_unique() { + assert_evals_to!( + indoc!(r#"Str.trim (Str.concat " " "hello world from a large string ")"#), + RocStr::from("hello world from a large string"), + RocStr + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev"))] +fn str_trim_large_to_small_unique() { + assert_evals_to!( + indoc!(r#"Str.trim (Str.concat " " "hello world ")"#), + RocStr::from("hello world"), + RocStr + ); +} + +#[test] +#[cfg(feature = "gen-llvm")] +fn str_trim_large_to_large_shared() { + assert_evals_to!( + indoc!( + r#" + original : Str + original = " hello world world " + + { trimmed: Str.trim original, original: original } + "# + ), + ( + RocStr::from(" hello world world "), + RocStr::from("hello world world"), + ), + (RocStr, RocStr) + ); +} + +#[test] +#[cfg(feature = "gen-llvm")] +fn str_trim_large_to_small_shared() { + assert_evals_to!( + indoc!( + r#" + original : Str + original = " hello world " + + { trimmed: Str.trim original, original: original } + "# + ), + ( + RocStr::from(" hello world "), + RocStr::from("hello world"), + ), + (RocStr, RocStr) + ); +} + +#[test] +#[cfg(feature = "gen-llvm")] +fn str_trim_small_to_small_shared() { + assert_evals_to!( + indoc!( + r#" + original : Str + original = " hello world " + + { trimmed: Str.trim original, original: original } + "# + ), + (RocStr::from(" hello world "), RocStr::from("hello world"),), + (RocStr, RocStr) + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev"))] +fn str_trim_start_small_blank_string() { + assert_evals_to!(indoc!(r#"Str.trimStart " ""#), RocStr::from(""), RocStr); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev"))] +fn str_trim_start_small_to_small() { + assert_evals_to!( + indoc!(r#"Str.trimStart " hello world ""#), + RocStr::from("hello world "), + RocStr + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev"))] +fn str_trim_start_large_to_large_unique() { + assert_evals_to!( + indoc!(r#"Str.trimStart (Str.concat " " "hello world from a large string ")"#), + RocStr::from("hello world from a large string "), + RocStr + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev"))] +fn str_trim_start_large_to_small_unique() { + assert_evals_to!( + indoc!(r#"Str.trimStart (Str.concat " " "hello world ")"#), + RocStr::from("hello world "), + RocStr + ); +} + +#[test] +#[cfg(feature = "gen-llvm")] +fn str_trim_start_large_to_large_shared() { + assert_evals_to!( + indoc!( + r#" + original : Str + original = " hello world world " + + { trimmed: Str.trimStart original, original: original } + "# + ), + ( + RocStr::from(" hello world world "), + RocStr::from("hello world world "), + ), + (RocStr, RocStr) + ); +} + +#[test] +#[cfg(feature = "gen-llvm")] +fn str_trim_start_large_to_small_shared() { + assert_evals_to!( + indoc!( + r#" + original : Str + original = " hello world " + + { trimmed: Str.trimStart original, original: original } + "# + ), + ( + RocStr::from(" hello world "), + RocStr::from("hello world "), + ), + (RocStr, RocStr) + ); +} + +#[test] +#[cfg(feature = "gen-llvm")] +fn str_trim_start_small_to_small_shared() { + assert_evals_to!( + indoc!( + r#" + original : Str + original = " hello world " + + { trimmed: Str.trimStart original, original: original } + "# + ), + (RocStr::from(" hello world "), RocStr::from("hello world "),), + (RocStr, RocStr) + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev"))] +fn str_trim_end_small_blank_string() { + assert_evals_to!(indoc!(r#"Str.trimEnd " ""#), RocStr::from(""), RocStr); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev"))] +fn str_trim_end_small_to_small() { + assert_evals_to!( + indoc!(r#"Str.trimEnd " hello world ""#), + RocStr::from(" hello world"), + RocStr + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev"))] +fn str_trim_end_large_to_large_unique() { + assert_evals_to!( + indoc!(r#"Str.trimEnd (Str.concat " hello world from a large string" " ")"#), + RocStr::from(" hello world from a large string"), + RocStr + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev"))] +fn str_trim_end_large_to_small_unique() { + assert_evals_to!( + indoc!(r#"Str.trimEnd (Str.concat " hello world" " ")"#), + RocStr::from(" hello world"), + RocStr + ); +} + +#[test] +#[cfg(feature = "gen-llvm")] +fn str_trim_end_large_to_large_shared() { + assert_evals_to!( + indoc!( + r#" + original : Str + original = " hello world world " + + { trimmed: Str.trimEnd original, original: original } + "# + ), + ( + RocStr::from(" hello world world "), + RocStr::from(" hello world world"), + ), + (RocStr, RocStr) + ); +} + +#[test] +#[cfg(feature = "gen-llvm")] +fn str_trim_end_large_to_small_shared() { + assert_evals_to!( + indoc!( + r#" + original : Str + original = " hello world " + + { trimmed: Str.trimEnd original, original: original } + "# + ), + ( + RocStr::from(" hello world "), + RocStr::from(" hello world"), + ), + (RocStr, RocStr) + ); +} + +#[test] +#[cfg(feature = "gen-llvm")] +fn str_trim_end_small_to_small_shared() { + assert_evals_to!( + indoc!( + r#" + original : Str + original = " hello world " + + { trimmed: Str.trimEnd original, original: original } + "# + ), + (RocStr::from(" hello world "), RocStr::from(" hello world"),), + (RocStr, RocStr) + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev"))] +fn str_to_nat() { + assert_evals_to!( + indoc!( + r#" + Str.toNat "1" + "# + ), + RocResult::ok(1), + RocResult + ); +} + +#[test] +#[cfg(feature = "gen-llvm")] +fn str_to_i128() { + assert_evals_to!( + indoc!( + r#" + Str.toI128 "1" + "# + ), + RocResult::ok(1), + RocResult + ); +} + +#[test] +#[cfg(feature = "gen-llvm")] +fn str_to_u128() { + assert_evals_to!( + indoc!( + r#" + Str.toU128 "1" + "# + ), + RocResult::ok(1), + RocResult + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev"))] +fn str_to_i64() { + assert_evals_to!( + indoc!( + r#" + Str.toI64 "1" + "# + ), + RocResult::ok(1), + RocResult + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev"))] +fn str_to_u64() { + assert_evals_to!( + indoc!( + r#" + Str.toU64 "1" + "# + ), + RocResult::ok(1), + RocResult + ); +} + +#[test] +#[cfg(feature = "gen-llvm")] +fn str_to_i32() { + assert_evals_to!( + indoc!( + r#" + Str.toI32 "1" + "# + ), + RocResult::ok(1), + RocResult + ); +} + +#[test] +#[cfg(feature = "gen-llvm")] +fn str_to_u32() { + assert_evals_to!( + indoc!( + r#" + Str.toU32 "1" + "# + ), + RocResult::ok(1), + RocResult + ); +} + +#[test] +#[cfg(feature = "gen-llvm")] +fn str_to_i16() { + assert_evals_to!( + indoc!( + r#" + Str.toI16 "1" + "# + ), + RocResult::ok(1), + RocResult + ); +} + +#[test] +#[cfg(feature = "gen-llvm")] +fn str_to_u16() { + assert_evals_to!( + indoc!( + r#" + Str.toU16 "1" + "# + ), + RocResult::ok(1), + RocResult + ); +} + +#[test] +#[cfg(feature = "gen-llvm")] +fn str_to_i8() { + assert_evals_to!( + indoc!( + r#" + Str.toI8 "1" + "# + ), + RocResult::ok(1), + RocResult + ); +} + +#[test] +#[cfg(feature = "gen-llvm")] +fn str_to_u8() { + assert_evals_to!( + indoc!( + r#" + Str.toU8 "1" + "# + ), + RocResult::ok(1), + RocResult + ); +} + +#[test] +#[cfg(feature = "gen-llvm")] +fn str_to_f64() { + assert_evals_to!( + indoc!( + r#" + when Str.toF64 "1.0" is + Ok n -> n + Err _ -> 0 + + "# + ), + 1.0, + f64 + ); +} + +#[test] +#[cfg(feature = "gen-llvm")] +fn str_to_f32() { + assert_evals_to!( + indoc!( + r#" + when Str.toF32 "1.0" is + Ok n -> n + Err _ -> 0 + + "# + ), + 1.0, + f32 + ); +} + +#[test] +#[cfg(feature = "gen-llvm")] +fn str_to_dec() { + use roc_std::RocDec; + + assert_evals_to!( + indoc!( + r#" + when Str.toDec "1.0" is + Ok n -> n + Err _ -> 0 + + "# + ), + RocDec::from_str("1.0").unwrap(), + RocDec + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev"))] +fn issue_2811() { + assert_evals_to!( + indoc!( + r#" + x = Command { tool: "bash" } + Command c = x + c.tool + "# + ), + RocStr::from("bash"), + RocStr + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev"))] +fn to_scalar_1_byte() { + assert_evals_to!( + indoc!( + r#" + Str.toScalars "R" + "# + ), + RocList::from_slice(&[82u32]), + RocList + ); + + assert_evals_to!( + indoc!( + r#" + Str.toScalars "Roc!" + "# + ), + RocList::from_slice(&[82u32, 111, 99, 33]), + RocList + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev"))] +fn to_scalar_2_byte() { + assert_evals_to!( + indoc!( + r#" + Str.toScalars "é" + "# + ), + RocList::from_slice(&[233u32]), + RocList + ); + + assert_evals_to!( + indoc!( + r#" + Str.toScalars "Cäfés" + "# + ), + RocList::from_slice(&[67u32, 228, 102, 233, 115]), + RocList + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev"))] +fn to_scalar_3_byte() { + assert_evals_to!( + indoc!( + r#" + Str.toScalars "鹏" + "# + ), + RocList::from_slice(&[40527u32]), + RocList + ); + + assert_evals_to!( + indoc!( + r#" + Str.toScalars "鹏很有趣" + "# + ), + RocList::from_slice(&[40527u32, 24456, 26377, 36259]), + RocList + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev"))] +fn to_scalar_4_byte() { + // from https://design215.com/toolbox/utf8-4byte-characters.php + assert_evals_to!( + indoc!( + r#" + Str.toScalars "𒀀" + "# + ), + RocList::from_slice(&[73728u32]), + RocList + ); + + assert_evals_to!( + indoc!( + r#" + Str.toScalars "𒀀𒀁" + "# + ), + RocList::from_slice(&[73728u32, 73729u32]), + RocList + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev"))] +fn str_split_first_one_char() { + assert_evals_to!( + indoc!( + r#" + Str.splitFirst "foo/bar/baz" "/" + "# + ), + // the result is a { before, after } record, and because of + // alphabetic ordering the fields here are flipped + RocResult::ok((RocStr::from("bar/baz"), RocStr::from("foo"))), + RocResult<(RocStr, RocStr), ()> + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev"))] +fn str_split_first_multiple_chars() { + assert_evals_to!( + indoc!( + r#" + Str.splitFirst "foo//bar//baz" "//" + "# + ), + RocResult::ok((RocStr::from("bar//baz"), RocStr::from("foo"))), + RocResult<(RocStr, RocStr), ()> + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev"))] +fn str_split_first_entire_input() { + assert_evals_to!( + indoc!( + r#" + Str.splitFirst "foo" "foo" + "# + ), + RocResult::ok((RocStr::from(""), RocStr::from(""))), + RocResult<(RocStr, RocStr), ()> + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev"))] +fn str_split_first_not_found() { + assert_evals_to!( + indoc!( + r#" + Str.splitFirst "foo" "bar" + "# + ), + RocResult::err(()), + RocResult<(RocStr, RocStr), ()> + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev"))] +fn str_split_last_one_char() { + assert_evals_to!( + indoc!( + r#" + Str.splitLast"foo/bar/baz" "/" + "# + ), + RocResult::ok((RocStr::from("baz"), RocStr::from("foo/bar"))), + RocResult<(RocStr, RocStr), ()> + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev"))] +fn str_split_last_multiple_chars() { + assert_evals_to!( + indoc!( + r#" + Str.splitLast "foo//bar//baz" "//" + "# + ), + RocResult::ok((RocStr::from("baz"), RocStr::from("foo//bar"))), + RocResult<(RocStr, RocStr), ()> + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev"))] +fn str_split_last_entire_input() { + assert_evals_to!( + indoc!( + r#" + Str.splitLast "foo" "foo" + "# + ), + RocResult::ok((RocStr::from(""), RocStr::from(""))), + RocResult<(RocStr, RocStr), ()> + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev"))] +fn str_split_last_not_found() { + assert_evals_to!( + indoc!( + r#" + Str.splitLast "foo" "bar" + "# + ), + RocResult::err(()), + RocResult<(RocStr, RocStr), ()> + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev"))] +fn str_split_overlapping_substring_1() { + assert_evals_to!( + r#"Str.split "aaa" "aa""#, + RocList::from_slice(&[RocStr::from(""), RocStr::from("a")]), + RocList + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev"))] +fn str_split_overlapping_substring_2() { + assert_evals_to!( + r#"Str.split "aaaa" "aa""#, + RocList::from_slice(&[RocStr::from(""), RocStr::from(""), RocStr::from("")]), + RocList + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev"))] +fn str_walk_utf8() { + #[cfg(not(feature = "gen-llvm-wasm"))] + assert_evals_to!( + // Reverse the bytes + indoc!( + r#" + Str.walkUtf8 "abcd" [] (\list, byte -> List.prepend list byte) + "# + ), + RocList::from_slice(&[b'd', b'c', b'b', b'a']), + RocList + ); + + #[cfg(feature = "gen-llvm-wasm")] + assert_evals_to!( + indoc!( + r#" + Str.walkUtf8WithIndex "abcd" [] (\list, byte, index -> List.append list (Pair index byte)) + "# + ), + RocList::from_slice(&[(0, 'a'), (1, 'b'), (2, 'c'), (3, 'd')]), + RocList<(u32, char)> + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev"))] +fn str_walk_utf8_with_index() { + #[cfg(not(feature = "gen-llvm-wasm"))] + assert_evals_to!( + indoc!( + r#" + Str.walkUtf8WithIndex "abcd" [] (\list, byte, index -> List.append list (Pair index byte)) + "# + ), + RocList::from_slice(&[(0, b'a'), (1, b'b'), (2, b'c'), (3, b'd')]), + RocList<(u64, u8)> + ); + + #[cfg(feature = "gen-llvm-wasm")] + assert_evals_to!( + indoc!( + r#" + Str.walkUtf8WithIndex "abcd" [] (\list, byte, index -> List.append list (Pair index byte)) + "# + ), + RocList::from_slice(&[(0, 'a'), (1, 'b'), (2, 'c'), (3, 'd')]), + RocList<(u32, char)> + ); +} + +#[test] +#[cfg(feature = "gen-llvm")] +fn str_append_scalar() { + assert_evals_to!( + indoc!( + r#" + Str.appendScalar "abcd" 'A' + "# + ), + RocStr::from("abcdA"), + RocStr + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev"))] +fn str_walk_scalars() { + assert_evals_to!( + indoc!( + r#" + Str.walkScalars "abcd" [] List.append + "# + ), + RocList::from_slice(&['a', 'b', 'c', 'd']), + RocList + ); +} + +#[test] +#[cfg(feature = "gen-llvm-wasm")] +fn llvm_wasm_str_layout() { + assert_evals_to!( + indoc!( + r#" + "hello" + |> Str.reserve 42 + "# + ), + [0, 5, 1], + [u32; 3], + |[_ptr, len, cap]: [u32; 3]| [0, len, if cap >= 42 { 1 } else { 0 }] + ) +} + +#[test] +#[cfg(feature = "gen-llvm-wasm")] +fn llvm_wasm_str_layout_small() { + // exposed an error in using bitcast instead of zextend + assert_evals_to!( + indoc!( + r#" + "𒀀𒀁" + |> Str.trim + "# + ), + [-2139057424, -2122280208, -2013265920], + [i32; 3] + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn when_on_strings() { + assert_evals_to!( + indoc!( + r#" + when "Deyr fé, deyja frændr" is + "Deyr fé, deyja frændr" -> 42 + "deyr sjalfr it sama" -> 1 + "en orðstírr deyr aldregi" -> 2 + "hveim er sér góðan getr" -> 3 + _ -> 4 + "# + ), + 42, + i64 + ); + + assert_evals_to!( + indoc!( + r#" + when "Deyr fé, deyja frændr" is + "deyr sjalfr it sama" -> 1 + "en orðstírr deyr aldregi" -> 2 + "hveim er sér góðan getr" -> 3 + _ -> 4 + "# + ), + 4, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn with_capacity() { + assert_evals_to!( + indoc!( + r#" + Str.withCapacity 10 + "# + ), + RocStr::from(""), + RocStr + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn with_capacity_concat() { + assert_evals_to!( + indoc!( + r#" + Str.withCapacity 10 |> Str.concat "Forty-two" + "# + ), + RocStr::from("Forty-two"), + RocStr + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn str_with_prefix() { + assert_evals_to!( + indoc!( + r#" + Str.withPrefix "world!" "Hello " + "# + ), + RocStr::from("Hello world!"), + RocStr + ); + + assert_evals_to!( + indoc!( + r#" + "two" |> Str.withPrefix "Forty " + "# + ), + RocStr::from("Forty two"), + RocStr + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn destructure_pattern_assigned_from_thunk_opaque() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + MyCustomType := Str + myMsg = @MyCustomType "Hello" + + main = + @MyCustomType msg = myMsg + + msg + "# + ), + RocStr::from("Hello"), + RocStr + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn destructure_pattern_assigned_from_thunk_tag() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + myMsg = A "hello " "world" + + main = + A m1 m2 = myMsg + + Str.concat m1 m2 + "# + ), + RocStr::from("hello world"), + RocStr + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn release_excess_capacity() { + assert_evals_to!( + indoc!( + r#" + Str.reserve "" 50 + |> Str.releaseExcessCapacity + "# + ), + (RocStr::empty().capacity(), RocStr::empty()), + RocStr, + |value: RocStr| (value.capacity(), value) + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn release_excess_capacity_with_len() { + assert_evals_to!( + indoc!( + r#" + "123456789012345678901234567890" + |> Str.reserve 50 + |> Str.releaseExcessCapacity + "# + ), + (30, "123456789012345678901234567890".into()), + RocStr, + |value: RocStr| (value.capacity(), value) + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn release_excess_capacity_empty() { + assert_evals_to!( + indoc!( + r#" + Str.releaseExcessCapacity "" + "# + ), + (RocStr::empty().capacity(), RocStr::empty()), + RocStr, + |value: RocStr| (value.capacity(), value) + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev"))] +fn str_contains_positive() { + assert_evals_to!( + r#" + Str.contains "foobarbaz" "bar" + "#, + true, + bool + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev"))] +fn str_contains_negative() { + assert_evals_to!( + r#" + Str.contains "apple" "orange" + "#, + false, + bool + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev"))] +fn str_contains_empty_positive() { + assert_evals_to!( + r#" + Str.contains "anything" "" + "#, + true, + bool + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev"))] +fn str_contains_empty_negative() { + assert_evals_to!( + r#" + Str.contains "" "anything" + "#, + false, + bool + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev"))] +fn str_contains_self() { + assert_evals_to!( + r#" + Str.contains "self" "self" + "#, + true, + bool + ); +} diff --git a/crates/compiler/test_gen/src/gen_tags.rs b/crates/compiler/test_gen/src/gen_tags.rs new file mode 100644 index 0000000000..3a67b9db44 --- /dev/null +++ b/crates/compiler/test_gen/src/gen_tags.rs @@ -0,0 +1,2312 @@ +#[allow(unused_imports)] +use crate::helpers::with_larger_debug_stack; + +#[cfg(feature = "gen-llvm")] +use crate::helpers::llvm::assert_evals_to; + +#[cfg(feature = "gen-dev")] +use crate::helpers::dev::assert_evals_to; + +#[cfg(feature = "gen-wasm")] +use crate::helpers::wasm::assert_evals_to; + +#[cfg(test)] +use indoc::indoc; + +use roc_mono::layout::{LayoutRepr, STLayoutInterner}; +#[cfg(test)] +use roc_std::{RocList, RocStr, U128}; + +#[test] +fn width_and_alignment_u8_u8() { + use roc_mono::layout::Layout; + use roc_mono::layout::UnionLayout; + + let target_info = roc_target::TargetInfo::default_x86_64(); + let interner = STLayoutInterner::with_capacity(4, target_info); + + let t = &[Layout::U8] as &[_]; + let tt = [t, t]; + + let layout = LayoutRepr::Union(UnionLayout::NonRecursive(&tt)); + + assert_eq!(layout.alignment_bytes(&interner), 1); + assert_eq!(layout.stack_size(&interner), 2); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn applied_tag_nothing() { + assert_evals_to!( + indoc!( + r#" + Maybe a : [Just a, Nothing] + + x : Maybe I64 + x = Nothing + + x + "# + ), + 1, + (i64, u8), + |(_, tag)| tag + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn applied_tag_just() { + assert_evals_to!( + indoc!( + r#" + Maybe a : [Just a, Nothing] + + y : Maybe I64 + y = Just 0x4 + + y + "# + ), + (0x4, 0), + (i64, u8) + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn applied_tag_just_enum() { + assert_evals_to!( + indoc!( + r#" + Fruit : [Orange, Apple, Banana] + Maybe a : [Just a, Nothing] + + orange : Fruit + orange = Orange + + y : Maybe Fruit + y = Just orange + + y + "# + ), + (2, 0), + (u8, u8) + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn true_is_true() { + assert_evals_to!( + indoc!( + r#" + bool : Bool + bool = Bool.true + + bool + "# + ), + true, + bool + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn false_is_false() { + assert_evals_to!( + indoc!( + r#" + bool : Bool + bool = Bool.false + + bool + "# + ), + false, + bool + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn basic_enum() { + assert_evals_to!( + indoc!( + r#" + Fruit : [Apple, Orange, Banana] + + apple : Fruit + apple = Apple + + orange : Fruit + orange = Orange + + apple == orange + "# + ), + false, + bool + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn even_odd() { + assert_evals_to!( + indoc!( + r#" + even = \n -> + when n is + 0 -> Bool.true + 1 -> Bool.false + _ -> odd (n - 1) + + odd = \n -> + when n is + 0 -> Bool.false + 1 -> Bool.true + _ -> even (n - 1) + + odd 5 && even 42 + "# + ), + true, + bool + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn gen_literal_true() { + assert_evals_to!( + indoc!( + r#" + if Bool.true then -1 else 1 + "# + ), + -1, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn gen_if_float() { + assert_evals_to!( + indoc!( + r#" + if Bool.true then -1.0 else 1.0f64 + "# + ), + -1.0, + f64 + ); +} +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn when_on_nothing() { + assert_evals_to!( + indoc!( + r#" + x : [Nothing, Just I64] + x = Nothing + + when x is + Nothing -> 0x2 + Just _ -> 0x1 + "# + ), + 2, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn when_on_just() { + assert_evals_to!( + indoc!( + r#" + x : [Nothing, Just I64] + x = Just 41 + + when x is + Just v -> v + 0x1 + Nothing -> 0x1 + "# + ), + 42, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn when_on_result() { + assert_evals_to!( + indoc!( + r#" + x : Result I64 I64 + x = Err 41 + + when x is + Err v -> v + 1 + Ok _ -> 1 + "# + ), + 42, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn when_on_these() { + assert_evals_to!( + indoc!( + r#" + These a b : [This a, That b, These a b] + + x : These I64 I64 + x = These 0x3 0x2 + + when x is + These a b -> a + b + That v -> v + This v -> v + "# + ), + 5, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn match_on_two_values() { + // this will produce a Chain internally + assert_evals_to!( + indoc!( + r#" + when Pair 2 3 is + Pair 4 3 -> 9 + Pair a b -> a + b + "# + ), + 5, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn pair_with_underscore() { + assert_evals_to!( + indoc!( + r#" + when Pair 2 3 is + Pair 4 _ -> 1 + Pair 3 _ -> 2 + Pair a b -> a + b + "# + ), + 5, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn result_with_underscore() { + // This test revealed an issue with hashing Test values + assert_evals_to!( + indoc!( + r#" + x : Result I64 I64 + x = Ok 2 + + when x is + Ok 3 -> 1 + Ok _ -> 2 + Err _ -> 3 + "# + ), + 2, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn maybe_is_just_not_nested() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + Maybe a : [Just a, Nothing] + + isJust : Maybe a -> Bool + isJust = \list -> + when list is + Nothing -> Bool.false + Just _ -> Bool.true + + main = + isJust (Just 42) + "# + ), + true, + bool + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn maybe_is_just_nested() { + assert_evals_to!( + indoc!( + r#" + Maybe a : [Just a, Nothing] + + isJust : Maybe a -> Bool + isJust = \list -> + when list is + Nothing -> Bool.false + Just _ -> Bool.true + + isJust (Just 42) + "# + ), + true, + bool + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn nested_pattern_match() { + assert_evals_to!( + indoc!( + r#" + Maybe a : [Nothing, Just a] + + x : Maybe (Maybe I64) + x = Just (Just 41) + + when x is + Just (Just v) -> v + 0x1 + _ -> 0x1 + "# + ), + 42, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn if_guard_vanilla() { + assert_evals_to!( + indoc!( + r#" + when "fooz" is + s if s == "foo" -> [] + s -> Str.toUtf8 s + "# + ), + RocList::from_slice(b"fooz"), + RocList + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn when_on_single_value_tag() { + assert_evals_to!( + indoc!( + r#" + when Identity 0 is + Identity 0 -> 6 + Identity s -> s + "# + ), + 6, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn if_guard_multiple() { + assert_evals_to!( + indoc!( + r#" + f = \n -> + when Identity n 0 is + Identity x _ if x == 0 -> x + 0 + Identity x _ if x == 1 -> x + 0 + Identity x _ if x == 2 -> x + 0 + Identity x _ -> x - x + + { a: f 0, b: f 1, c: f 2, d: f 4 } + "# + ), + [0, 1, 2, 0], + [i64; 4] + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn if_guard_constructor_switch() { + assert_evals_to!( + indoc!( + r#" + when Identity 32 0 is + Identity 41 _ -> 0 + Identity s 0 if s == 32 -> 3 + # Identity s 0 -> s + Identity z _ -> z + "# + ), + 3, + i64 + ); + + assert_evals_to!( + indoc!( + r#" + when Identity 42 "" is + Identity 41 _ -> 0 + Identity 42 _ if 3 == 3 -> 1 + Identity z _ -> z + "# + ), + 1, + i64 + ); + + assert_evals_to!( + indoc!( + r#" + when Identity 42 "" is + Identity 41 _ -> 0 + Identity 42 _ if 3 != 3 -> 1 + Identity z _ -> z + "# + ), + 42, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn if_guard_constructor_chain() { + assert_evals_to!( + indoc!( + r#" + when Identity 43 0 is + Identity 42 _ if 3 == 3 -> 43 + # Identity 42 _ -> 1 + Identity z _ -> z + "# + ), + 43, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn if_guard_pattern_false() { + assert_evals_to!( + indoc!( + r#" + wrapper = \{} -> + when 2 is + 2 if Bool.false -> 0 + _ -> 42 + + wrapper {} + "# + ), + 42, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn if_guard_switch() { + assert_evals_to!( + indoc!( + r#" + wrapper = \{} -> + when 2 is + 2 | 3 if Bool.false -> 0 + _ -> 42 + + wrapper {} + "# + ), + 42, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn if_guard_pattern_true() { + assert_evals_to!( + indoc!( + r#" + wrapper = \{} -> + when 2 is + 2 if Bool.true -> 42 + _ -> 0 + + wrapper {} + "# + ), + 42, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn if_guard_exhaustiveness() { + assert_evals_to!( + indoc!( + r#" + wrapper = \{} -> + when 2 is + _ if Bool.false -> 0 + _ -> 42 + + wrapper {} + "# + ), + 42, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn when_on_enum() { + assert_evals_to!( + indoc!( + r#" + Fruit : [Apple, Orange, Banana] + + apple : Fruit + apple = Apple + + when apple is + Apple -> 1 + Banana -> 2 + Orange -> 3 + "# + ), + 1, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn pattern_matching_unit() { + assert_evals_to!( + indoc!( + r#" + Unit : [Unit] + + f : Unit -> I64 + f = \Unit -> 42 + + f Unit + "# + ), + 42, + i64 + ); + + assert_evals_to!( + indoc!( + r#" + Unit : [Unit] + + x : Unit + x = Unit + + when x is + Unit -> 42 + "# + ), + 42, + i64 + ); + + assert_evals_to!( + indoc!( + r#" + f : {} -> I64 + f = \{} -> 42 + + f {} + "# + ), + 42, + i64 + ); + + assert_evals_to!( + indoc!( + r#" + when {} is + {} -> 42 + "# + ), + 42, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn one_element_tag() { + assert_evals_to!( + indoc!( + r#" + x : [Pair I64] + x = Pair 2 + + x + "# + ), + 2, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn nested_tag_union() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + Maybe a : [Nothing, Just a] + + x : Maybe (Maybe I64) + x = Just (Just 41) + + main = + x + "# + ), + ((41, 0), 0), + ((i64, u8), u8) + ); +} +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn unit_type() { + assert_evals_to!( + indoc!( + r#" + Unit : [Unit] + + v : Unit + v = Unit + + v + "# + ), + (), + () + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn join_point_if() { + assert_evals_to!( + indoc!( + r#" + x = + if Bool.true then 1 else 2 + + x + "# + ), + 1, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn join_point_when() { + assert_evals_to!( + indoc!( + r#" + wrapper = \{} -> + x : [Red, White, Blue] + x = Blue + + y = + when x is + Red -> 1 + White -> 2 + Blue -> 3.1f64 + + y + + wrapper {} + "# + ), + 3.1, + f64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn join_point_with_cond_expr() { + assert_evals_to!( + indoc!( + r#" + wrapper = \{} -> + y = + when 1 + 2 is + 3 -> 3 + 1 -> 1 + _ -> 0 + + y + + wrapper {} + "# + ), + 3, + i64 + ); + + assert_evals_to!( + indoc!( + r#" + y = + if 1 + 2 > 0 then + 3 + else + 0 + + y + "# + ), + 3, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn alignment_in_single_tag_construction() { + assert_evals_to!(indoc!("Three (1 == 1) 32"), (32i64, true), (i64, bool)); + + assert_evals_to!( + indoc!("Three (1 == 1) (if Bool.true then Red else if Bool.true then Green else Blue) 32"), + (32i64, true, 2u8), + (i64, bool, u8) + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn alignment_in_single_tag_pattern_match() { + assert_evals_to!( + indoc!( + r"# + x = Three (1 == 1) 32 + + when x is + Three bool int -> + { bool, int } + #" + ), + (32i64, true), + (i64, bool) + ); + + assert_evals_to!( + indoc!( + r"# + x = Three (1 == 1) (if Bool.true then Red else if Bool.true then Green else Blue) 32 + + when x is + Three bool color int -> + { bool, color, int } + #" + ), + (32i64, true, 2u8), + (i64, bool, u8) + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn alignment_in_multi_tag_construction_two() { + assert_evals_to!( + indoc!( + r"# + x : [Three Bool I64, Empty] + x = Three (1 == 1) 32 + + x + + #" + ), + ((32i64, true), 1), + ((i64, bool), u8) + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn alignment_in_multi_tag_construction_three() { + assert_evals_to!( + indoc!( + r"# + x : [Three Bool [Red, Green, Blue] I64, Empty] + x = Three (1 == 1) (if Bool.true then Red else if Bool.true then Green else Blue) 32 + + x + #" + ), + ((32i64, true, 2u8), 1), + ((i64, bool, u8), u8) + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn alignment_in_multi_tag_pattern_match() { + assert_evals_to!( + indoc!( + r"# + x : [Three Bool I64, Empty] + x = Three (1 == 1) 32 + + when x is + Three bool int -> + { bool, int } + + Empty -> + # dev backend codegen bug means we cannot use this inline + false = Bool.false + { bool: false, int: 0 } + #" + ), + (32i64, true), + (i64, bool) + ); + + assert_evals_to!( + indoc!( + r"# + x : [Three Bool [Red, Green, Blue] I64, Empty] + x = Three (1 == 1) (if Bool.true then Red else if Bool.true then Green else Blue) 32 + + when x is + Three bool color int -> + { bool, color, int } + Empty -> + false = Bool.false + { bool: false, color: Red, int: 0 } + #" + ), + (32i64, true, 2u8), + (i64, bool, u8) + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn phantom_polymorphic() { + assert_evals_to!( + indoc!( + r"# + Point coordinate : [Point coordinate I64 I64] + + World := {} + + zero : Point World + zero = Point (@World {}) 0 0 + + add : Point a -> Point a + add = \(Point c x y) -> (Point c x y) + + add zero + #" + ), + (0, 0), + (i64, i64) + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn phantom_polymorphic_record() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + Point coordinate : { coordinate : coordinate, x : I64, y : I64 } + + zero : Point I64 + zero = { coordinate : 0, x : 0, y : 0 } + + add : Point a -> Point a + add = \{ coordinate } -> { coordinate, x: 0 + 0, y: 0 } + + main = add zero + "# + ), + (0, 0, 0), + (i64, i64, i64) + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn result_never() { + assert_evals_to!( + indoc!( + r"# + res : Result I64 [] + res = Ok 4 + + when res is + Ok v -> v + #" + ), + 4, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn nested_recursive_literal() { + assert_evals_to!( + indoc!( + r"# + Expr : [Add Expr Expr, Val I64, Var I64] + + e : Expr + e = Add (Add (Val 3) (Val 1)) (Add (Val 1) (Var 1)) + + e + #" + ), + 0, + usize, + |_| 0 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn newtype_wrapper() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + ConsList a : [Nil, Cons a (ConsList a)] + + foo : ConsList I64 -> ConsList I64 + foo = \t -> + when Delmin (Del t 0.0) is + Delmin (Del ry _) -> Cons 42 ry + + main = foo Nil + "# + ), + 42, + roc_std::RocBox, + |x: roc_std::RocBox| { + let value = *x; + std::mem::forget(x); + value + } + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn applied_tag_function() { + assert_evals_to!( + indoc!( + r#" + x : List [Foo Str] + x = List.map ["a", "b"] Foo + + x + "# + ), + RocList::from_slice(&[RocStr::from("a"), RocStr::from("b")]), + RocList + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn applied_tag_function_result() { + assert_evals_to!( + indoc!( + r#" + x : List (Result Str *) + x = List.map ["a", "b"] Ok + + List.keepOks x (\y -> y) + "# + ), + RocList::from_slice(&[(RocStr::from("a")), (RocStr::from("b"))]), + RocList + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +#[ignore = "This test has incorrect refcounts: https://github.com/roc-lang/roc/issues/2968"] +fn applied_tag_function_linked_list() { + assert_evals_to!( + indoc!( + r#" + ConsList a : [Nil, Cons a (ConsList a)] + + x : List (ConsList Str) + x = List.map2 ["a", "b"] [Nil, Cons "c" Nil] Cons + + when List.first x is + Ok (Cons "a" Nil) -> 1 + _ -> 0 + "# + ), + 1, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn applied_tag_function_pair() { + assert_evals_to!( + indoc!( + r#" + Pair a : [Pair a a] + + x : List (Pair Str) + x = List.map2 ["a", "b"] ["c", "d"] Pair + + when List.first x is + Ok (Pair "a" "c") -> 1 + _ -> 0 + "# + ), + 1, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +#[should_panic(expected = "")] // TODO: this only panics because it returns 0 instead of 1! +fn tag_must_be_its_own_type() { + assert_evals_to!( + indoc!( + r#" + z : [A, B, C] + z = Z + + z + "# + ), + 1, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn recursive_tag_union_into_flat_tag_union() { + // Comprehensive test for correctness in cli/tests/repl_eval + assert_evals_to!( + indoc!( + r#" + Item : [Shallow [L Str, R Str], Deep Item] + i : Item + i = Deep (Shallow (R "woo")) + i + "# + ), + 0, + usize, + |_| 0 + ) +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn monomorphized_tag() { + assert_evals_to!( + indoc!( + r#" + b = \{} -> Bar + f : [Foo, Bar], [Bar, Baz] -> U8 + f = \_, _ -> 18 + f (b {}) (b {}) + "# + ), + 18, + u8 + ) +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn monomorphized_applied_tag() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + main = + a = A "abc" + f = \x -> + when x is + A y -> y + B y -> y + f a + "# + ), + RocStr::from("abc"), + RocStr + ) +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn monomorphized_tag_with_polymorphic_arg() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + main = + a = \{} -> A + wrap = \{} -> Wrapped (a {}) + + useWrap1 : [Wrapped [A], Other] -> U8 + useWrap1 = + \w -> when w is + Wrapped A -> 2 + Other -> 3 + + useWrap2 : [Wrapped [A, B]] -> U8 + useWrap2 = + \w -> when w is + Wrapped A -> 5 + Wrapped B -> 7 + + if Bool.true then useWrap1 (wrap {}) else useWrap2 (wrap {}) + "# + ), + 2, + u8 + ) +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn monomorphized_tag_with_polymorphic_and_monomorphic_arg() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + main = + mono : U8 + mono = 15 + poly = \{} -> A + wrap = \{} -> Wrapped (poly {}) mono + + useWrap1 : [Wrapped [A] U8, Other] -> U8 + useWrap1 = + \w -> when w is + Wrapped A n -> n + Other -> 0 + + useWrap2 : [Wrapped [A, B] U8] -> U8 + useWrap2 = + \w -> when w is + Wrapped A n -> n + Wrapped B _ -> 0 + + useWrap1 (wrap {}) * useWrap2 (wrap {}) + "# + ), + 225, + u8 + ) +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn issue_2365_monomorphize_tag_with_non_empty_ext_var() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + Single a : [A, B, C]a + Compound a : Single [D, E, F]a + + single : {} -> Single * + single = \{} -> C + + compound : {} -> Compound * + compound = \{} -> single {} + + main = compound {} + "# + ), + 2, // C + u8 + ) +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn issue_2365_monomorphize_tag_with_non_empty_ext_var_wrapped() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + Single a : [A, B, C]a + Compound a : Single [D, E, F]a + Padding : { a: U64, b: U64, c: U64 } + + single : {} -> Result Padding (Single *) + single = \{} -> Err C + + compound : {} -> Result Padding (Compound *) + compound = \{} -> + when single {} is + Ok s -> Ok s + Err e -> Err e + + main = compound {} + "# + ), + (0, 2), // Err, C + ([u8; std::mem::size_of::<(u64, u64, u64)>()], u8), + |(err_tag, wrap_tag): ([u8; std::mem::size_of::<(u64, u64, u64)>()], u8)| ( + wrap_tag, err_tag[0] + ) + ) +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn issue_2365_monomorphize_tag_with_non_empty_ext_var_wrapped_nested() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + Single a : [A, B, C]a + Compound a : Single [D, E, F]a + Padding : { a: U64, b: U64, c: U64 } + + main = + single : {} -> Result Padding (Single *) + single = \{} -> Err C + + compound : {} -> Result Padding (Compound *) + compound = \{} -> + when single {} is + Ok s -> Ok s + Err e -> Err e + + compound {} + "# + ), + (0, 2), // Err, C + ([u8; std::mem::size_of::<(u64, u64, u64)>()], u8), + |(err_tag, wrap_tag): ([u8; std::mem::size_of::<(u64, u64, u64)>()], u8)| ( + wrap_tag, err_tag[0] + ) + ) +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn issue_2445() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + none : [None, Update _] + none = None + + press : [None, Update U8] + press = none + + main = + when press is + None -> 15 + Update _ -> 25 + "# + ), + 15, + i64 + ); +} + +#[test] +#[cfg(feature = "gen-llvm")] +fn issue_2458() { + assert_evals_to!( + indoc!( + r#" + Foo a : [Blah (Bar a), Nothing {}] + Bar a : Foo a + + v : Bar {} + v = Blah (Blah (Nothing {})) + + when v is + Blah (Blah (Nothing {})) -> 15 + _ -> 25 + "# + ), + 15, + u8 + ) +} + +#[test] +#[ignore = "See https://github.com/roc-lang/roc/issues/2466"] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn issue_2458_deep_recursion_var() { + assert_evals_to!( + indoc!( + r#" + Foo a : [Blah (Result (Bar a) {})] + Bar a : Foo a + + v : Bar {} + + when v is + Blah (Ok (Blah (Err {}))) -> "1" + _ -> "2" + "# + ), + 15, + u8 + ) +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn issue_1162() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + RBTree k : [Node k (RBTree k) (RBTree k), Empty] + + balance : a, RBTree a -> RBTree a + balance = \key, left -> + when left is + Node _ _ lRight -> + Node key lRight Empty + + _ -> + Empty + + + tree : RBTree {} + tree = + balance {} Empty + + main : U8 + main = + when tree is + Empty -> 15 + _ -> 25 + "# + ), + 15, + u8 + ) +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn polymorphic_tag() { + assert_evals_to!( + indoc!( + r#" + x : [Y U8] + x = Y 3 + x + "# + ), + 3, // Y is a newtype, it gets unwrapped + u8 + ) +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn issue_2725_alias_polymorphic_lambda() { + assert_evals_to!( + indoc!( + r#" + wrap = \value -> Tag value + wrapIt = wrap + wrapIt 42 + "# + ), + 42, // Tag is a newtype, it gets unwrapped + i64 + ) +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn opaque_assign_to_symbol() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [out] to "./platform" + + Variable := U8 + + fromUtf8 : U8 -> Result Variable [InvalidVariableUtf8] + fromUtf8 = \char -> + Ok (@Variable char) + + out = + when fromUtf8 98 is + Ok (@Variable n) -> n + _ -> 1 + "# + ), + 98, + u8 + ) +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn issue_2777_default_branch_codegen() { + assert_evals_to!( + indoc!( + r#" + f1 = \color -> + when color is + Red -> "red" + Yellow -> "yellow" + _ -> "unknown" + + r1 = Red |> f1 |> Str.concat (f1 Orange) + + f2 = \color -> + when color is + Red -> "red" + Yellow -> "yellow" + Green -> "green" + _ -> "unknown" + + r2 = Red |> f2 |> Str.concat (f2 Orange) + + f3 = \color -> + when color is + Red -> "red" + Yellow -> "yellow" + Green -> "green" + _ -> "unknown" + + r3 = Orange |> f3 |> Str.concat (f3 Red) + + f4 = \color -> + when color is + Red -> "red" + Yellow | Gold -> "yellow" + _ -> "unknown" + + r4 = Red |> f4 |> Str.concat (f4 Orange) + + [r1, r2, r3, r4] + "# + ), + RocList::from_slice(&[ + RocStr::from("redunknown"), + RocStr::from("redunknown"), + RocStr::from("unknownred"), + RocStr::from("redunknown"), + ]), + RocList + ) +} + +#[test] +// This doesn't work on Windows. If you make it return a `bool`, e.g. with `|> Str.isEmpty` at the end, +// then it works. We don't know what the problem is here! +#[cfg(all( + not(target_family = "windows"), + any(feature = "gen-llvm", feature = "gen-wasm") +))] +#[should_panic(expected = r#"Roc failed with message: "Tag Foo was part of a type error!""#)] +fn issue_2900_unreachable_pattern() { + assert_evals_to!( + indoc!( + r#" + foo : [Foo, Bar, Baz, Blah] -> Str + foo = \arg -> + when arg is + Foo -> "foo" + AnUnreachableTag -> "blah" + _ -> "other" + + foo Foo + "# + ), + RocStr::from("foo"), + RocStr, + |x| x, + true // ignore type errors + ) +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn issue_3261_non_nullable_unwrapped_recursive_union_at_index() { + assert_evals_to!( + indoc!( + r#" + Named : [Named Str (List Named)] + + foo : Named + foo = Named "outer" [Named "inner" []] + + Named name outerList = foo + + {name, outerList}.name + "# + ), + RocStr::from("outer"), + RocStr + ) +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn instantiate_annotated_as_recursive_alias_toplevel() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + Value : [Nil, Array (List Value)] + + foo : [Nil]_ + foo = Nil + + it : Value + it = foo + + main = + when it is + Nil -> 123i64 + _ -> -1i64 + "# + ), + 123, + i64 + ) +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn instantiate_annotated_as_recursive_alias_polymorphic_expr() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + main = + Value : [Nil, Array (List Value)] + + foo : [Nil]_ + foo = Nil + + it : Value + it = foo + + when it is + Nil -> 123i64 + _ -> -1i64 + "# + ), + 123, + i64 + ) +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn instantiate_annotated_as_recursive_alias_multiple_polymorphic_expr() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + main = + Value : [Nil, Array (List Value)] + + foo : {} -> [Nil] + foo = \{} -> Nil + + v1 : Value + v1 = foo {} + + Value2 : [Nil, B U16, Array (List Value)] + + v2 : Value2 + v2 = foo {} + + when {v1, v2} is + {v1: Nil, v2: Nil} -> 123i64 + {v1: _, v2: _} -> -1i64 + "# + ), + 123, + i64 + ) +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn issue_3560_nested_tag_constructor_is_newtype() { + assert_evals_to!( + indoc!( + r#" + f : _ -> u8 + f = \t -> + when t is + Wrapper (Payload it) -> it + Wrapper (AlternatePayload it) -> it + + {a: f (Wrapper (Payload 15u8)), b: f(Wrapper (AlternatePayload 31u8))} + "# + ), + (15, 31), + (u8, u8) + ) +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn issue_3560_nested_tag_constructor_is_record_newtype() { + assert_evals_to!( + indoc!( + r#" + f : _ -> u8 + f = \t -> + when t is + {wrapper: (Payload it)} -> it + {wrapper: (AlternatePayload it)} -> it + + {a: f {wrapper: (Payload 15u8)}, b: f {wrapper: (AlternatePayload 31u8)}} + "# + ), + (15, 31), + (u8, u8) + ) +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn issue_3560_newtype_tag_constructor_has_nested_constructor_with_no_payload() { + assert_evals_to!( + indoc!( + r#" + when Wrapper (Payload "err") is + Wrapper (Payload str) -> str + Wrapper NoPayload -> "nothing" + "# + ), + RocStr::from("err"), + RocStr + ) +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn alignment_i128() { + assert_evals_to!( + indoc!( + r"# + x : [One I128 Bool, Empty] + x = One 42 (1 == 1) + x + #" + ), + // NOTE: roc_std::U128 is always aligned to 16, unlike rust's u128 + ((U128::from(42), true), 1), + ((U128, bool), u8) + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +#[ignore = "causes alias analysis panics, should roc_panic"] +fn error_type_in_tag_union_payload() { + assert_evals_to!( + indoc!( + r#" + f : ([] -> Bool) -> Bool + f = \fun -> + if Bool.true then + fun 42 + else + Bool.false + + f (\x -> x) + "# + ), + 0, + u8, + |x| x, + true // ignore type errors + ) +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn issue_3653_recursion_pointer_in_naked_opaque() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + Peano := [ Zero, Succ Peano ] + + recurse = \@Peano peano -> + when peano is + Succ inner -> recurse inner + _ -> {} + + main = + when recurse (@Peano Zero) is + _ -> "we're back" + "# + ), + RocStr::from("we're back"), + RocStr + ) +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn issue_3653_recursion_pointer_in_naked_opaque_localized() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + Peano := [ Zero, Succ Peano ] + + recurse = \peano -> + when peano is + @Peano (Succ inner) -> recurse inner + @Peano Zero -> {} + + main = + when recurse (@Peano Zero) is + _ -> "we're back" + "# + ), + RocStr::from("we're back"), + RocStr + ) +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn issue_2165_recursive_tag_destructure() { + assert_evals_to!( + indoc!( + r#" + SomeTag : [ Ctor { rec : List SomeTag } ] + + x : SomeTag + x = Ctor { rec: [] } + + when x is + Ctor { rec } -> Num.toStr (List.len rec) + "# + ), + RocStr::from("0"), + RocStr + ) +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn tag_union_let_generalization() { + assert_evals_to!( + indoc!( + r#" + manyAux : {} -> [ Loop, Done ] + manyAux = \_ -> + output = Done + + output + + when manyAux {} is + Loop -> "loop" + Done -> "done" + "# + ), + RocStr::from("done"), + RocStr + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn fit_recursive_union_in_struct_into_recursive_pointer() { + assert_evals_to!( + indoc!( + r#" + NonEmpty := [ + First Str, + Next { item: Str, rest: NonEmpty }, + ] + + nonEmpty = + a = "abcdefgh" + b = @NonEmpty (First "ijkl") + c = Next { item: a, rest: b } + @NonEmpty c + + when nonEmpty is + @NonEmpty (Next r) -> r.item + _ -> "" + "# + ), + RocStr::from("abcdefgh"), + RocStr + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn match_on_result_with_uninhabited_error_branch() { + assert_evals_to!( + indoc!( + r#" + x : Result Str [] + x = Ok "abc" + + when x is + Ok s -> s + "# + ), + RocStr::from("abc"), + RocStr + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn dispatch_tag_union_function_inferred() { + assert_evals_to!( + indoc!( + r#" + g = \b -> if b then H else J + + when P ((g Bool.true) "") ((g Bool.false) "") is + P (H _) (J _) -> "okay" + _ -> "FAIL" + "# + ), + RocStr::from("okay"), + RocStr + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn issue_4077_fixed_fixpoint() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + Input : [FromProjectSource, FromJob Job] + + Job : [Job { inputs : List Input }] + + job : { inputs : List Input } -> Job + job = \config -> Job config + + main = + when job { inputs: [] } is + _ -> "OKAY" + "# + ), + RocStr::from("OKAY"), + RocStr + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn unify_types_with_fixed_fixpoints_outside_fixing_region() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + Input := [ + FromJob Job (List Str), + ] + + Job := [ + Job (List Input) + ] + + job : List Input -> Job + job = \inputs -> + @Job (Job inputs) + + helloWorld : Job + helloWorld = + @Job ( Job [ @Input (FromJob greeting []) ] ) + + greeting : Job + greeting = + job [] + + main = (\_ -> "OKAY") helloWorld + "# + ), + RocStr::from("OKAY"), + RocStr + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn lambda_set_with_imported_toplevels_issue_4733() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + fn = \s -> + instr = if s == "*" then (Op Num.mul) else (Op Num.add) + + Op op = instr + + \a -> op a a + + main = ((fn "*") 3) * ((fn "+") 5) + "# + ), + 90, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn non_unary_union_with_lambda_set_with_imported_toplevels_issue_4733() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + fn = \s -> + instr = + if s == "*" then (Op Num.mul) + else if s == "+" then (Op Num.add) + else Noop + + when instr is + Op op -> (\a -> op a a) + _ -> (\a -> a) + + + main = ((fn "*") 3i64) * ((fn "+") 5) + "# + ), + 90, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn nullable_wrapped_with_non_nullable_singleton_tags() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + F : [ + A F, + B, + C, + ] + + g : F -> Str + g = \f -> when f is + A _ -> "A" + B -> "B" + C -> "C" + + main = + g (A (B)) + |> Str.concat (g B) + |> Str.concat (g C) + "# + ), + RocStr::from("ABC"), + RocStr + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn nullable_wrapped_with_nullable_not_last_index() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + Parser : [ + CharLiteral, + Keyword Str, + OneOrMore Parser, + ] + + toIdParser : Parser -> Str + toIdParser = \parser -> + when parser is + OneOrMore _ -> "a" + Keyword _ -> "b" + CharLiteral -> "c" + + main = + toIdParser (OneOrMore CharLiteral) + |> Str.concat (toIdParser (Keyword "try")) + |> Str.concat (toIdParser CharLiteral) + "# + ), + RocStr::from("abc"), + RocStr + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn refcount_nullable_unwrapped_needing_no_refcount_issue_5027() { + assert_evals_to!( + indoc!( + r#" + app "test" provides [main] to "./platform" + + Effect : {} -> Str + + after = \effect, buildNext -> + \{} -> + when buildNext (effect {}) is + thunk -> thunk {} + + line : Effect + line = \{} -> "done" + + await : Effect, (Str -> Effect) -> Effect + await = \fx, cont -> + after + fx + cont + + succeed : {} -> Effect + succeed = \{} -> (\{} -> "success") + + test = + await line \s -> + if s == "done" then succeed {} else test + + main = test {} + "# + ), + RocStr::from("success"), + RocStr + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn issue_5162_recast_nested_nullable_unwrapped_layout() { + with_larger_debug_stack(|| { + assert_evals_to!( + indoc!( + r###" + app "test" provides [main] to "./platform" + + Concept : [ + AtomicConcept, + ExistentialRestriction { role : Str, concept : Concept } + ] + + bottom : Concept + bottom = AtomicConcept + + main = + when Dict.single bottom 0 is + _ -> Bool.true + "### + ), + true, + bool + ); + }); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn nullable_wrapped_eq_issue_5434() { + assert_evals_to!( + indoc!( + r###" + app "test" provides [main] to "./platform" + + Value : [ + A, + B I64, + C, + D (List [T Str Value]), + ] + + main = + x : Value + x = B 32 + y : Value + y = B 0 + if x == y then + Bool.true + else + Bool.false + "### + ), + false, + bool + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn recursive_tag_id_in_allocation_basic() { + assert_evals_to!( + indoc!( + r###" + app "test" provides [main] to "./platform" + + Value : [ + A Value, + B I64, + C I64, + D I64, + E I64, + F I64, + G I64, + H I64, + I I64, + ] + + x : Value + x = H 42 + + main = + when x is + A _ -> "A" + B _ -> "B" + C _ -> "C" + D _ -> "D" + E _ -> "E" + F _ -> "F" + G _ -> "G" + H _ -> "H" + I _ -> "I" + "### + ), + RocStr::from("H"), + RocStr + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn recursive_tag_id_in_allocation_eq() { + assert_evals_to!( + indoc!( + r###" + app "test" provides [main] to "./platform" + + Value : [ + A Value, + B I64, + C I64, + D I64, + E I64, + F I64, + G I64, + H I64, + I I64, + ] + + x : Value + x = G 42 + + y : Value + y = H 42 + + main = (x == x) && (x != y) && (y == y) + "### + ), + true, + bool + ); +} diff --git a/crates/compiler/test_gen/src/gen_tuples.rs b/crates/compiler/test_gen/src/gen_tuples.rs new file mode 100644 index 0000000000..5d62885b96 --- /dev/null +++ b/crates/compiler/test_gen/src/gen_tuples.rs @@ -0,0 +1,620 @@ +#[cfg(feature = "gen-llvm")] +use crate::helpers::llvm::assert_evals_to; + +#[cfg(feature = "gen-dev")] +use crate::helpers::dev::assert_evals_to; + +#[cfg(feature = "gen-wasm")] +use crate::helpers::wasm::assert_evals_to; + +// use crate::assert_wasm_evals_to as assert_evals_to; +use indoc::indoc; + +#[cfg(all(test, any(feature = "gen-llvm", feature = "gen-wasm")))] +use roc_std::RocStr; + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))] +fn basic_tuple() { + assert_evals_to!( + indoc!( + r#" + ( 15, 17, 19 ).0 + "# + ), + 15, + i64 + ); + + assert_evals_to!( + indoc!( + r#" + ( 15, 17, 19 ).1 + "# + ), + 17, + i64 + ); + + assert_evals_to!( + indoc!( + r#" + ( 15, 17, 19 ).2 + "# + ), + 19, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))] +fn f64_tuple() { + assert_evals_to!( + indoc!( + r#" + tup = (17.2f64, 15.1f64, 19.3f64) + + tup.0 + "# + ), + 17.2, + f64 + ); + + assert_evals_to!( + indoc!( + r#" + tup = (17.2f64, 15.1f64, 19.3f64) + + tup.1 + "# + ), + 15.1, + f64 + ); + + assert_evals_to!( + indoc!( + r#" + tup = (17.2f64, 15.1f64, 19.3f64) + + tup.2 + "# + ), + 19.3, + f64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn fn_tuple() { + assert_evals_to!( + indoc!( + r#" + getRec = \x -> ("foo", x, 19) + + (getRec 15).1 + "# + ), + 15, + i64 + ); + + assert_evals_to!( + indoc!( + r#" + rec = (15, 17, 19) + + rec.2 + rec.0 + "# + ), + 34, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))] +fn int_tuple() { + assert_evals_to!( + indoc!( + r#" + rec = (15, 17, 19) + + rec.0 + "# + ), + 15, + i64 + ); + + assert_evals_to!( + indoc!( + r#" + rec = (15, 17, 19) + + rec.1 + "# + ), + 17, + i64 + ); + + assert_evals_to!( + indoc!( + r#" + rec = (15, 17, 19) + + rec.2 + "# + ), + 19, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))] +fn when_on_tuple() { + assert_evals_to!( + indoc!( + r#" + when (0x2, 0x3) is + (x, y) -> x + y + "# + ), + 5, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))] +fn when_tuple_with_guard_pattern() { + assert_evals_to!( + indoc!( + r#" + when (0x2, 1.23) is + (var, _) -> var + 3 + "# + ), + 5, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))] +fn let_with_tuple_pattern() { + assert_evals_to!( + indoc!( + r#" + (x, _ ) = (0x2, 1.23) + + x + "# + ), + 2, + i64 + ); + + assert_evals_to!( + indoc!( + r#" + (_, y) = (0x2, 0x3) + + y + "# + ), + 3, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))] +fn tuple_guard_pattern() { + assert_evals_to!( + indoc!( + r#" + when (0x2, 1.23) is + (0x4, _) -> 5 + (x, _) -> x + 4 + "# + ), + 6, + i64 + ); + + assert_evals_to!( + indoc!( + r#" + when (0x2, 0x3) is + (_, 0x4) -> 5 + (_, x) -> x + 4 + "# + ), + 7, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))] +fn twice_tuple_access() { + assert_evals_to!( + indoc!( + r#" + x = (0x2, 0x3) + + x.0 + x.1 + "# + ), + 5, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn i64_tuple2_literal() { + assert_evals_to!( + indoc!( + r#" + (3, 5) + "# + ), + (3, 5), + (i64, i64) + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn i64_tuple3_literal() { + assert_evals_to!( + indoc!( + r#" + (3, 5, 17) + "# + ), + (3, 5, 17), + (i64, i64, i64) + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn f64_tuple2_literal() { + assert_evals_to!( + indoc!( + r#" + (3.1f64, 5.1f64) + "# + ), + (3.1, 5.1), + (f64, f64) + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn bool_tuple4_literal() { + assert_evals_to!( + indoc!( + r#" + tuple : (Bool, Bool, Bool, Bool) + tuple = (Bool.true, Bool.false, Bool.false, Bool.true) + + tuple + "# + ), + (true, false, false, true), + (bool, bool, bool, bool) + ); +} + +// Not supported by wasm because of the size of the tuple: +// FromWasm32Memory is only implemented for tuples of up to 4 elements +#[test] +#[cfg(feature = "gen-llvm")] +fn i64_tuple9_literal() { + assert_evals_to!( + indoc!( + r#" + ( 3, 5, 17, 1, 9, 12, 13, 14, 15 ) + "# + ), + [3, 5, 17, 1, 9, 12, 13, 14, 15], + [i64; 9] + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn return_tuple() { + assert_evals_to!( + indoc!( + r#" + x = 4 + y = 3 + + (x, y) + "# + ), + (4, 3), + (i64, i64) + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn return_tuple_2() { + assert_evals_to!( + indoc!( + r#" + (3, 5) + "# + ), + [3, 5], + [i64; 2] + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn return_tuple_3() { + assert_evals_to!( + indoc!( + r#" + ( 3, 5, 4 ) + "# + ), + (3, 5, 4), + (i64, i64, i64) + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn return_tuple_4() { + assert_evals_to!( + indoc!( + r#" + ( 3, 5, 4, 2 ) + "# + ), + [3, 5, 4, 2], + [i64; 4] + ); +} + +// Not supported by wasm because of the size of the tuple: +// FromWasm32Memory is only implemented for tuples of up to 4 elements +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev"))] +fn return_tuple_5() { + assert_evals_to!( + indoc!( + r#" + ( 3, 5, 4, 2, 1 ) + "# + ), + [3, 5, 4, 2, 1], + [i64; 5] + ); +} + +// Not supported by wasm because of the size of the tuple: +// FromWasm32Memory is only implemented for tuples of up to 4 elements +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev"))] +fn return_tuple_6() { + assert_evals_to!( + indoc!( + r#" + ( 3, 5, 4, 2, 1, 7 ) + "# + ), + [3, 5, 4, 2, 1, 7], + [i64; 6] + ); +} + +// Not supported by wasm because of the size of the tuple: +// FromWasm32Memory is only implemented for tuples of up to 4 elements +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev"))] +fn return_tuple_7() { + assert_evals_to!( + indoc!( + r#" + ( 3, 5, 4, 2, 1, 7, 8 ) + "# + ), + [3, 5, 4, 2, 1, 7, 8], + [i64; 7] + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn return_tuple_float_int() { + assert_evals_to!( + indoc!( + r#" + (1.23f64, 0x1) + "# + ), + (1.23, 0x1), + (f64, i64) + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn return_tuple_int_float() { + assert_evals_to!( + indoc!( + r#" + ( 0x1, 1.23f64 ) + "# + ), + (0x1, 1.23), + (i64, f64) + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn return_tuple_float_float() { + assert_evals_to!( + indoc!( + r#" + ( 2.46f64, 1.23f64 ) + "# + ), + (2.46, 1.23), + (f64, f64) + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn return_tuple_float_float_float() { + assert_evals_to!( + indoc!( + r#" + ( 2.46f64, 1.23f64, 0.1f64 ) + "# + ), + (2.46, 1.23, 0.1), + (f64, f64, f64) + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))] +fn return_nested_tuple() { + assert_evals_to!( + indoc!( + r#" + (0x0, (2.46f64, 1.23f64, 0.1f64)) + "# + ), + (0x0, (2.46, 1.23, 0.1)), + (i64, (f64, f64, f64)) + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn nested_tuple_load() { + assert_evals_to!( + indoc!( + r#" + x = (0, (0x2, 0x5, 0x6)) + + y = x.1 + + y.2 + "# + ), + 6, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn tuple_accessor_twice() { + assert_evals_to!(".0 (4, 5) + .1 ( 2.46, 3 ) ", 7, i64); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn tuple_accessor_multi_element_tuple() { + assert_evals_to!( + indoc!( + r#" + .0 (4, "foo") + "# + ), + 4, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn booleans_in_tuple() { + assert_evals_to!(indoc!("(1 == 1, 1 == 1)"), (true, true), (bool, bool)); + assert_evals_to!(indoc!("(1 != 1, 1 == 1)"), (false, true), (bool, bool)); + assert_evals_to!(indoc!("(1 == 1, 1 != 1)"), (true, false), (bool, bool)); + assert_evals_to!(indoc!("(1 != 1, 1 != 1)"), (false, false), (bool, bool)); +} + +// TODO: this test fails for mysterious reasons +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn alignment_in_tuple() { + assert_evals_to!( + indoc!("(32, 1 == 1, 78u16)"), + (32i64, 78u16, true), + (i64, u16, bool) + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn tuple_length_polymorphism() { + assert_evals_to!( + indoc!( + r#" + a = (42, 43) + b = (1, 2, 44) + + f : (I64, I64)a, (I64, I64)b -> I64 + f = \(x1, x2), (x3, x4) -> x1 + x2 + x3 + x4 + + f a b + "# + ), + 88, + i64 + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn generalized_tuple_accessor() { + assert_evals_to!( + indoc!( + r#" + return0 = .0 + + return0 ("foo", 1) + "# + ), + RocStr::from("foo"), + RocStr + ); +} + +#[test] +#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] +fn generalized_explicit_tuple_accessor() { + assert_evals_to!( + indoc!( + r#" + return0 = \x -> x.0 + + return0 ("foo", 1) + "# + ), + RocStr::from("foo"), + RocStr + ); +} diff --git a/compiler/test_gen/src/helpers/debug-wasm-test.html b/crates/compiler/test_gen/src/helpers/debug-wasm-test.html similarity index 88% rename from compiler/test_gen/src/helpers/debug-wasm-test.html rename to crates/compiler/test_gen/src/helpers/debug-wasm-test.html index 38fd81c0ac..47b25a0a1b 100644 --- a/compiler/test_gen/src/helpers/debug-wasm-test.html +++ b/crates/compiler/test_gen/src/helpers/debug-wasm-test.html @@ -23,7 +23,6 @@ background-color: #000; padding: 1px 4px; font-size: 16px; - border-radius: 6px; } input, button { @@ -31,6 +30,10 @@ font-weight: bold; padding: 4px 12px; } + select { + font-size: 18px; + padding: 4px 12px; + } small { font-style: italic; } @@ -66,10 +69,12 @@

Steps

  • - In gen_wasm/src/lib.rs, set - DEBUG_LOG_SETTINGS.keep_test_binary = true + Run + ROC_WRITE_FINAL_WASM=1 cargo test-gen-wasm + my_test_function_name
  • -
  • Run cargo test-gen-wasm -- my_test --nocapture
  • Look for the path written to the console for final.wasm and select it in the file picker below @@ -78,46 +83,60 @@ Open the browser DevTools
    Control+Shift+I or Command+Option+I or F12
  • -
  • - Click one of the buttons below, depending on what kind of test it is. -
    - - Only one of them will work. The other will probably crash or - something. - -
  • The debugger should pause just before entering the first Wasm call. Step into a couple of Wasm calls until you reach your test code in - $#UserApp_main_1 + $main
  • - Chrome DevTools now has a Memory Inspector panel! In the debugger, + Chrome DevTools has a handy Memory Inspector panel. In the debugger, find Module -> memories -> $memory. Right click and select "Reveal in Memory Inspector"
+
+ +
- - +
+ + + + + + + + + +
+ +
+
+
+ +
+
+

Made by people who like to make nice things.

+
+ + + diff --git a/crates/docs/src/static/link.svg b/crates/docs/src/static/link.svg new file mode 100644 index 0000000000..50eaa23920 --- /dev/null +++ b/crates/docs/src/static/link.svg @@ -0,0 +1,7 @@ + + + \ No newline at end of file diff --git a/crates/docs/src/static/search.js b/crates/docs/src/static/search.js new file mode 100644 index 0000000000..3beea47a51 --- /dev/null +++ b/crates/docs/src/static/search.js @@ -0,0 +1,147 @@ +(() => { + let sidebar = document.getElementById("sidebar-nav"); + let searchBox = document.getElementById("module-search"); + + if (searchBox != null) { + function search() { + let text = searchBox.value.toLowerCase(); // Search is case-insensitive. + + if (text === "") { + // Un-hide everything + sidebar + .querySelectorAll(".sidebar-entry a") + .forEach((entry) => entry.classList.remove("hidden")); + + // Re-hide all the sub-entries except for those of the current module + let currentModuleName = + document.querySelector(".module-name").textContent; + + sidebar.querySelectorAll(".sidebar-entry").forEach((entry) => { + let entryName = entry.querySelector( + ".sidebar-module-link" + ).textContent; + if (currentModuleName === entryName) { + entry.firstChild.classList.add("active"); + return; + } + entry + .querySelectorAll(".sidebar-sub-entries a") + .forEach((subEntry) => + subEntry.classList.add("hidden") + ); + }); + } else { + // First, show/hide all the sub-entries within each module (top-level functions etc.) + sidebar + .querySelectorAll(".sidebar-sub-entries a") + .forEach((entry) => { + if (entry.textContent.toLowerCase().includes(text)) { + entry.classList.remove("hidden"); + } else { + entry.classList.add("hidden"); + } + }); + + // Then, show/hide modules based on whether they match, or any of their sub-entries matched + sidebar + .querySelectorAll(".sidebar-module-link") + .forEach((entry) => { + if ( + entry.textContent.toLowerCase().includes(text) || + entry.parentNode.querySelectorAll( + ".sidebar-sub-entries a:not(.hidden)" + ).length > 0 + ) { + entry.classList.remove("hidden"); + } else { + entry.classList.add("hidden"); + } + }); + } + } + + searchBox.addEventListener("input", search); + + search(); + + // Capture '/' keypress for quick search + window.addEventListener("keyup", (e) => { + if (e.key === "s" && document.activeElement !== searchBox) { + e.preventDefault; + searchBox.focus(); + searchBox.value = ""; + } + + if (e.key === "Escape" && document.activeElement === searchBox) { + e.preventDefault; + + // De-focus input box + searchBox.blur(); + + // Reset sidebar state + search(); + } + }); + } + + const isTouchSupported = () => { + try { + document.createEvent("TouchEvent"); + return true; + } catch (e) { + return false; + } + }; + + // Select all elements that are children of
 elements
+    const codeBlocks = document.querySelectorAll("pre > samp");
+
+    // Iterate over each code block
+    codeBlocks.forEach((codeBlock) => {
+        // Create a "Copy" button
+        const copyButton = document.createElement("button");
+        copyButton.classList.add("copy-button");
+        copyButton.textContent = "Copy";
+
+        // Add event listener to copy button
+        copyButton.addEventListener("click", () => {
+            const codeText = codeBlock.innerText;
+            navigator.clipboard.writeText(codeText);
+            copyButton.textContent = "Copied!";
+            copyButton.classList.add("copy-button-copied");
+            copyButton.addEventListener("mouseleave", () => {
+                copyButton.textContent = "Copy";
+                copyButton.classList.remove("copy-button-copied");
+            });
+        });
+
+        // Create a container for the copy button and append it to the document
+        const buttonContainer = document.createElement("div");
+        buttonContainer.classList.add("button-container");
+        buttonContainer.appendChild(copyButton);
+        codeBlock.parentNode.insertBefore(buttonContainer, codeBlock);
+
+        // Hide the button container by default
+        buttonContainer.style.display = "none";
+
+        if (isTouchSupported()) {
+            // Show the button container on click for touch support (e.g. mobile)
+            document.addEventListener("click", (event) => {
+                if (event.target.closest("pre > samp") !== codeBlock) {
+                    buttonContainer.style.display = "none";
+                } else {
+                    buttonContainer.style.display = "block";
+                }
+            });
+        } else {
+            // Show the button container on hover for non-touch support (e.g. desktop)
+            codeBlock.parentNode.addEventListener("mouseenter", () => {
+                buttonContainer.style.display = "block";
+            });
+
+            codeBlock.parentNode.addEventListener("mouseleave", () => {
+                buttonContainer.style.display = "none";
+            });
+        }
+    });
+})();
diff --git a/crates/docs/src/static/styles.css b/crates/docs/src/static/styles.css
new file mode 100644
index 0000000000..46fef3872e
--- /dev/null
+++ b/crates/docs/src/static/styles.css
@@ -0,0 +1,753 @@
+:root {
+  /* WCAG AAA Compliant colors */
+  --code-bg: #f4f8f9;
+  --gray: #717171;
+  --orange: #bf5000;
+  --green: #0b8400;
+  --cyan: #067c94;
+  --blue: #05006d;
+  --violet: #7c38f5;
+  --violet-bg: #ece2fd;
+  --magenta: #a20031;
+  --link-hover-color: #333;
+
+  --link-color: var(--violet);
+  --code-link-color: var(--violet);
+  --text-color: #000;
+  --text-hover-color: var(--violet);
+  --body-bg-color: #ffffff;
+  --border-color: #717171;
+  --faded-color: #4c4c4c;
+  --font-sans: -apple-system, BlinkMacSystemFont, Roboto, Helvetica, Arial,
+      sans-serif;
+  --font-mono: SFMono-Regular, Consolas, "Liberation Mono", Menlo, Courier,
+      monospace;
+  --top-header-height: 67px;
+  --sidebar-width: 280px;
+}
+
+a {
+  color: var(--violet);
+}
+
+table tr th {
+  border: 1px solid var(--gray);
+}
+
+table tr th,
+table tr td {
+  padding: 6px 13px;
+}
+
+.logo {
+  padding: 2px 8px;
+}
+
+.logo svg {
+  height: 48px;
+  width: 48px;
+  fill: var(--violet);
+}
+
+.logo:hover {
+  text-decoration: none;
+}
+
+.logo svg:hover {
+  fill: var(--link-hover-color);
+}
+
+.pkg-full-name {
+  display: flex;
+  align-items: center;
+  font-size: 32px;
+  margin: 0 8px;
+  font-weight: normal;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  height: 100%;
+}
+
+.entry-name {
+  white-space: pre-wrap;
+  font-family: var(--font-mono);
+  font-size: 18px;
+  font-weight: normal;
+  color: var(--violet);
+  width: auto;
+  margin-top: 0;
+  margin-bottom: 24px;
+  padding: 8px 16px;
+  border-left: 2px solid var(--violet);
+}
+
+.entry-name a {
+  visibility: hidden;
+  display: inline-block;
+  width: 18px;
+  height: 14px;
+  margin-left: -8px;
+  margin-right: 4px;
+  user-select: none;
+  color: var(--violet);
+}
+
+.entry-name:hover a {
+  visibility: visible;
+  text-decoration: none;
+}
+
+.entry-name:not(:hover) a {
+  visibility: hidden;
+  transition: visibility 2s;
+}
+
+.pkg-full-name a {
+  padding-top: 12px;
+  padding-bottom: 16px;
+}
+
+a {
+  text-decoration: none;
+}
+
+a:hover,
+a:hover code {
+  text-decoration: underline;
+}
+
+.pkg-and-logo {
+  min-width: 0; /* necessary for text-overflow: ellipsis to work in descendants */
+  display: flex;
+  align-items: center;
+  height: 100%;
+  background-color: var(--violet-bg);
+}
+
+.pkg-and-logo a,
+.pkg-and-logo a:visited {
+  color: var(--violet);
+}
+
+.pkg-and-logo a:hover {
+  color: var(--link-hover-color);
+  text-decoration: none;
+}
+
+.search-button {
+  flex-shrink: 0; /* always shrink the package name before these; they have a relatively constrained length */
+  padding: 12px 18px;
+  margin-right: 42px;
+  display: none; /* only show this in the mobile view */
+}
+
+.version {
+  padding: 18px 10px;
+  min-width: 48px;
+  margin-right: 8px;
+}
+
+body {
+  display: grid;
+  grid-template-columns:
+      [before-sidebar] 1fr [sidebar] var(--sidebar-width)
+      [main-content] fit-content(calc(1280px - var(--sidebar-width)))
+      [end] 1fr;
+  grid-template-rows: [top-header] var(--top-header-height) [above-footer] auto [footer] auto;
+  box-sizing: border-box;
+  margin: 0;
+  padding: 0;
+  font-family: var(--font-sans);
+  color: var(--text-color);
+  background-color: var(--body-bg-color);
+}
+
+main {
+  grid-column-start: main-content;
+  grid-column-end: main-content;
+  grid-row-start: above-footer;
+  grid-row-end: above-footer;
+  box-sizing: border-box;
+  position: relative;
+  font-size: 18px;
+  line-height: 1.85em;
+  margin-top: 2px;
+  padding: 48px;
+  max-width: 740px;
+  /* necessary for text-overflow: ellipsis to work in descendants */
+  min-width: 0;
+}
+
+/* Module links on the package index page (/index.html) */
+.index-module-links {
+    margin: 0;
+    padding: 0;
+    list-style-type: none;
+}
+
+section {
+  padding: 0px 0px 16px 0px;
+  margin: 72px 0px;
+}
+
+section blockquote {
+  font-style: italic;
+  position: relative;
+  margin-left: 0;
+  margin-right: 0;
+}
+
+section blockquote:before {
+  content: "";
+  position: absolute;
+  top: 0;
+  right: 0;
+  width: 2px;
+  height: 100%;
+  background-color: var(--gray);
+}
+
+
+section > *:last-child {
+  margin-bottom: 0;
+}
+
+section h1,
+section h2,
+section h3,
+section h4,
+section p {
+padding: 0px 16px;
+}
+
+#sidebar-nav {
+  grid-column-start: sidebar;
+  grid-column-end: sidebar;
+  grid-row-start: above-footer;
+  grid-row-end: above-footer;
+  position: relative;
+  display: flex;
+  flex-direction: column;
+  box-sizing: border-box;
+  padding-left: 56px;
+  padding-top: 6px;
+  width: 100%;
+}
+
+.top-header-extension {
+  grid-column-start: before-sidebar;
+  grid-column-end: sidebar;
+  grid-row-start: top-header;
+  grid-row-end: top-header;
+  background-color: var(--violet-bg);
+}
+
+.top-header {
+  grid-column-start: sidebar;
+  grid-column-end: end;
+  grid-row-start: top-header;
+  grid-row-end: top-header;
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  flex-wrap: nowrap;
+  box-sizing: border-box;
+  font-family: var(--font-sans);
+  font-size: 24px;
+  height: 100%;
+  /* min-width must be set to something (even 0) for text-overflow: ellipsis to work in descendants, but we want this anyway. */
+  min-width: 1024px;
+}
+
+.top-header-triangle {
+  /* This used to be a clip-path, but Firefox on Android (at least as of early 2020)
+ * rendered the page extremely slowly in that version. With this approach it's super fast.
+ */
+  width: 0;
+  height: 0;
+  border-style: solid;
+  border-width: var(--top-header-height) 0 0 48px;
+  border-color: transparent transparent transparent var(--violet-bg);
+}
+
+p {
+  overflow-wrap: break-word;
+  margin: 24px 0;
+}
+
+footer {
+  grid-column-start: main-content;
+  grid-column-end: main-content;
+  grid-row-start: footer;
+  grid-row-end: footer;
+  max-width: var(--main-content-max-width);
+  font-size: 14px;
+  box-sizing: border-box;
+  padding: 16px;
+}
+
+footer p {
+  display: inline-block;
+  margin-top: 0;
+  margin-bottom: 8px;
+}
+
+.content {
+  box-sizing: border-box;
+  display: flex;
+  flex-direction: row;
+  justify-content: space-between;
+}
+
+.sidebar-entry ul {
+  list-style-type: none;
+  margin: 0;
+}
+
+.sidebar-entry a {
+  box-sizing: border-box;
+  min-height: 48px;
+  min-width: 48px;
+  padding: 12px 16px;
+  font-family: var(--font-mono);
+}
+
+.sidebar-entry a,
+.sidebar-entry a:visited {
+  color: var(--text-color);
+}
+
+.sidebar-sub-entries a {
+  display: block;
+  line-height: 24px;
+  width: 100%;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  margin-left: 14px;
+  padding-left: 26px;
+  border-left: 2px solid var(--violet);
+}
+
+.module-name {
+  font-size: 56px;
+  line-height: 1em;
+  font-family: var(--font-mono);
+  font-weight: bold;
+  margin-top: 18px;
+  margin-bottom: 48px;
+  color: var(--violet);
+}
+
+.module-name a,
+.module-name a:visited {
+color: inherit;
+}
+
+.module-name a:hover {
+  color: var(--link-hover-color);
+}
+
+.sidebar-module-link {
+  box-sizing: border-box;
+  font-size: 18px;
+  line-height: 24px;
+  font-family: var(--font-mono);
+  display: block;
+  width: 100%;
+  padding: 8px 0;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
+
+.sidebar-module-link.active {
+  font-weight: bold;
+}
+
+a,
+a:visited {
+  color: var(--link-color);
+}
+
+h3 {
+  font-size: 32px;
+  margin: 48px 0 24px 0;
+}
+
+h4 {
+  font-size: 24px;
+}
+
+.type-def {
+  font-size: 24px;
+  color: var(--link-color);
+}
+
+.code-snippet {
+  padding: 8px 16px;
+  display: block;
+  box-sizing: border-box;
+  font-family: var(--font-mono);
+  background-color: var(--code-bg);
+}
+
+code {
+  font-family: var(--font-mono);
+  color: var(--code-color);
+  background-color: var(--code-bg);
+  display: inline-block;
+}
+
+p code {
+  padding: 0 8px;
+}
+
+code a,
+a code {
+  text-decoration: none;
+  color: var(--code-link-color);
+  background: none;
+  padding: 0;
+}
+
+code a:visited,
+a:visited code {
+  color: var(--code-link-color);
+}
+
+pre {
+  margin: 36px 0;
+  padding: 8px 16px;
+  box-sizing: border-box;
+  background-color: var(--code-bg);
+  overflow-x: hidden;
+  position: relative;
+  word-wrap: normal;
+}
+
+pre>samp {
+  overflow-x: auto;
+  display: block;
+}
+
+.hidden {
+  /* Use !important to win all specificity fights. */
+  display: none !important;
+}
+
+#module-search:placeholder-shown {
+  padding: 0;
+  opacity: 0;
+  height: 0;
+}
+
+#module-search,
+#module-search:focus {
+  opacity: 1;
+  padding: 12px 16px;
+  height: 48px;
+}
+
+/* Show the "Search" label link when the text input has a placeholder */
+#module-search:placeholder-shown + #search-link {
+  display: flex;
+}
+
+/* Hide the "Search" label link when the text input has focus */
+#module-search:focus + #search-link {
+  display: none;
+}
+
+#module-search {
+  display: block;
+  box-sizing: border-box;
+  width: 100%;
+  box-sizing: border-box;
+  font-size: 18px;
+  line-height: 18px;
+  margin-top: 6px;
+  border: none;
+  color: var(--faded-color);
+  background-color: var(--code-bg);
+  font-family: var(--font-serif);
+}
+
+#module-search::placeholder {
+  color: var(--faded-color);
+  opacity: 1;
+}
+
+#search-link {
+  box-sizing: border-box;
+  display: none;
+  align-items: center;
+  font-size: 18px;
+  line-height: 18px;
+  padding: 12px 16px;
+  height: 48px;
+  cursor: pointer;
+}
+
+#search-link:hover #search-link-text {
+  text-decoration: underline;
+}
+
+#search-link-hint {
+  margin-left: 1em;
+  opacity: 0.6;
+}
+
+#search-shortcut-key {
+  font-family: monospace;
+  border: 1px solid #666;
+  padding: 1px 3px 3px;
+  font-style: normal;
+  line-height: 15px;
+}
+
+.builtins-tip {
+  padding: 1em;
+  font-style: italic;
+  line-height: 1.3em;
+}
+
+@media (prefers-color-scheme: dark) {
+  :root {
+      /* WCAG AAA Compliant colors */
+      --code-bg: #202746;
+      --gray: #b6b6b6;
+      --orange: #fd6e08;
+      --green: #8ecc88;
+      --cyan: #12c9be;
+      --blue: #b1afdf;
+      --violet: #CAADFB;
+      --violet-bg: #332944;
+      --magenta: #f39bac;
+      --link-hover-color: #fff;
+
+      --link-color: var(--violet);
+      --code-link-color: var(--violet);
+      --text-color: #eaeaea;
+      --body-bg-color: #0e0e0f;
+      --border-color: var(--gray);
+      --code-color: #eeeeee;
+      --logo-solid: #8f8f8f;
+      --faded-color: #bbbbbb;
+      --gray: #6e6e6e;
+  }
+
+  html {
+      scrollbar-color: #8f8f8f #2f2f2f;
+  }
+}
+
+@media only screen and (max-device-width: 480px) and (orientation: portrait) {
+  #search-link-hint {
+      display: none;
+  }
+
+  .search-button {
+      display: block; /* This is only visible in mobile. */
+  }
+
+  .top-header {
+      justify-content: space-between;
+      width: auto;
+      /* min-width must be set to something (even 0) for text-overflow: ellipsis to work in descendants. */
+      min-width: 0;
+  }
+
+  .pkg-full-name {
+      margin-left: 8px;
+      margin-right: 12px;
+      font-size: 24px;
+      padding-bottom: 14px;
+  }
+
+  .pkg-full-name a {
+      vertical-align: middle;
+      padding: 18px 0;
+  }
+
+  .logo {
+      padding-left: 2px;
+      width: 50px;
+      height: 54px;
+  }
+
+  .version {
+      margin: 0;
+      font-weight: normal;
+      font-size: 18px;
+      padding-bottom: 16px;
+  }
+
+  .module-name {
+      font-size: 36px;
+      margin-top: 8px;
+      margin-bottom: 8px;
+      max-width: calc(100% - 18px);
+      overflow: hidden;
+      text-overflow: ellipsis;
+  }
+
+  main {
+      grid-column-start: none;
+      grid-column-end: none;
+      grid-row-start: above-footer;
+      grid-row-end: above-footer;
+      padding: 18px;
+      font-size: 16px;
+      max-width: none;
+  }
+
+  #sidebar-nav {
+      grid-column-start: none;
+      grid-column-end: none;
+      grid-row-start: sidebar;
+      grid-row-end: sidebar;
+      margin-top: 0;
+      padding-left: 0;
+      width: auto;
+  }
+
+  #sidebar-heading {
+      font-size: 24px;
+      margin: 16px;
+  }
+
+  h3 {
+      font-size: 18px;
+      margin: 0;
+      padding: 0;
+  }
+
+  h4 {
+      font-size: 16px;
+  }
+
+  body {
+      grid-template-columns: auto;
+      grid-template-rows: [top-header] var(--top-header-height) [before-sidebar] auto [sidebar] auto [above-footer] auto [footer] auto;
+      /* [before-sidebar] 1fr [sidebar] var(--sidebar-width) [main-content] fit-content(calc(1280px - var(--sidebar-width))) [end] 1fr; */
+
+      margin: 0;
+      min-width: 320px;
+      max-width: 100%;
+  }
+
+  .top-header-triangle {
+      display: none;
+  }
+
+  .pkg-and-logo {
+      width: 100%;
+  }
+
+  .pkg-full-name {
+      flex-grow: 1;
+  }
+
+  .pkg-full-name a {
+      padding-top: 24px;
+      padding-bottom: 12px;
+  }
+}
+
+/* Comments `#` and Documentation comments `##` */
+samp .comment,
+code .comment {
+  color: var(--green);
+}
+
+/* Number, String, Tag literals */
+samp .storage.type,
+code .storage.type,
+samp .string,
+code .string,
+samp .string.begin,
+code .string.begin,
+samp .string.end,
+code .string.end,
+samp .constant,
+code .constant,
+samp .literal,
+code .literal {
+  color: var(--cyan);
+}
+
+/* Keywords and punctuation */
+samp .keyword,
+code .keyword,
+samp .punctuation.section,
+code .punctuation.section,
+samp .punctuation.separator,
+code .punctuation.separator,
+samp .punctuation.terminator,
+code .punctuation.terminator,
+samp .kw,
+code .kw {
+    color: var(--magenta);
+}
+
+/* Operators */
+samp .op,
+code .op,
+samp .keyword.operator,
+code .keyword.operator {
+  color: var(--orange);
+}
+
+/* Delimieters */
+samp .delimeter,
+code .delimeter {
+  color: var(--gray);
+}
+
+/* Variables modules and field names */
+samp .function,
+code .function,
+samp .meta.group,
+code .meta.group,
+samp .meta.block,
+code .meta.block,
+samp .lowerident,
+code .lowerident {
+  color: var(--blue);
+}
+
+/* Types, Tags, and Modules */
+samp .type,
+code .type,
+samp .meta.path,
+code .meta.path,
+samp .upperident,
+code .upperident {
+  color: var(--green);
+}
+
+samp .dim,
+code .dim {
+  opacity: 0.55;
+}
+
+.button-container {
+  position: absolute;
+  top: 0;
+  right: 0;
+}
+
+.copy-button {
+  background: var(--code-bg);
+  border: 1px solid var(--magenta);
+  color: var(--magenta);
+  display: inline-block;
+  cursor: pointer;
+  margin: 8px;
+}
+
+.copy-button:hover {
+  border-color: var(--link-hover-color);
+  color: var(--link-hover-color);
+}
diff --git a/crates/docs_cli/Cargo.toml b/crates/docs_cli/Cargo.toml
new file mode 100644
index 0000000000..8495e48179
--- /dev/null
+++ b/crates/docs_cli/Cargo.toml
@@ -0,0 +1,27 @@
+[package]
+name = "roc_docs_cli"
+description = "Provides a binary that is only used for static build servers."
+
+authors.workspace = true
+edition.workspace = true
+license.workspace = true
+version.workspace = true
+
+# This binary is only used on static build servers, e.g. Netlify.
+# Having its own (extremely minimal) CLI means docs can be generated
+# on a build server after building this crate from source, without
+# having to install non-Rust dependencies (LLVM, Zig, wasm things, etc.)
+# It gets called in www/build.sh via `cargo run --bin roc-docs`
+[[bin]]
+name = "roc-docs"
+path = "src/main.rs"
+test = false
+bench = false
+
+[dependencies]
+roc_docs = { path = "../docs" }
+
+clap.workspace = true
+
+[target.'cfg(windows)'.dependencies]
+libc.workspace = true
diff --git a/crates/docs_cli/src/main.rs b/crates/docs_cli/src/main.rs
new file mode 100644
index 0000000000..d7f8a931d7
--- /dev/null
+++ b/crates/docs_cli/src/main.rs
@@ -0,0 +1,66 @@
+//! Provides a binary that is only used for static build servers.
+use clap::{value_parser, Arg, Command};
+use roc_docs::generate_docs_html;
+use std::io;
+use std::path::PathBuf;
+
+pub const ROC_FILE: &str = "ROC_FILE";
+const DEFAULT_ROC_FILENAME: &str = "main.roc";
+
+fn main() -> io::Result<()> {
+    let matches = Command::new("roc-docs")
+        .about("Generate documentation for a Roc package")
+        .arg(
+            Arg::new(ROC_FILE)
+                .help("The package's main .roc file")
+                .num_args(0..)
+                .value_parser(value_parser!(PathBuf))
+                .default_value(DEFAULT_ROC_FILENAME),
+        )
+        .get_matches();
+
+    // Populate roc_files
+    generate_docs_html(
+        matches.get_one::(ROC_FILE).unwrap().to_owned(),
+        &PathBuf::from("./generated-docs"),
+    );
+
+    Ok(())
+}
+
+// These functions don't end up in the final Roc binary but Windows linker needs a definition inside the crate.
+// On Windows, there seems to be less dead-code-elimination than on Linux or MacOS, or maybe it's done later.
+#[cfg(windows)]
+#[allow(unused_imports)]
+use windows_roc_platform_functions::*;
+
+#[cfg(windows)]
+mod windows_roc_platform_functions {
+    use core::ffi::c_void;
+
+    /// # Safety
+    /// The Roc application needs this.
+    #[no_mangle]
+    pub unsafe fn roc_alloc(size: usize, _alignment: u32) -> *mut c_void {
+        libc::malloc(size)
+    }
+
+    /// # Safety
+    /// The Roc application needs this.
+    #[no_mangle]
+    pub unsafe 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
+    /// The Roc application needs this.
+    #[no_mangle]
+    pub unsafe fn roc_dealloc(c_ptr: *mut c_void, _alignment: u32) {
+        libc::free(c_ptr)
+    }
+}
diff --git a/crates/error_macros/Cargo.toml b/crates/error_macros/Cargo.toml
new file mode 100644
index 0000000000..812b2805e4
--- /dev/null
+++ b/crates/error_macros/Cargo.toml
@@ -0,0 +1,10 @@
+[package]
+name = "roc_error_macros"
+description = "Provides macros for consistent reporting of errors in Roc's rust code."
+
+authors.workspace = true
+edition.workspace = true
+license.workspace = true
+version.workspace = true
+
+[dependencies]
diff --git a/crates/error_macros/src/lib.rs b/crates/error_macros/src/lib.rs
new file mode 100644
index 0000000000..c36f880991
--- /dev/null
+++ b/crates/error_macros/src/lib.rs
@@ -0,0 +1,129 @@
+//! Provides macros for consistent reporting of errors in Roc's rust code.
+/// `internal_error!` should be used whenever a compiler invariant is broken.
+/// It is a wrapper around panic that tells the user to file a bug.
+/// This should only be used in cases where there would be a compiler bug and the user can't fix it.
+/// If there is simply an unimplemented feature, please use `unimplemented!`
+/// If there is a user error, please use roc_reporting to print a nice error message.
+#[macro_export]
+macro_rules! internal_error {
+    ($($arg:tt)*) => ({
+        eprintln!("An internal compiler expectation was broken.");
+        eprintln!("This is definitely a compiler bug.");
+        // TODO: update this to the new bug template.
+        eprintln!("Please file an issue here: https://github.com/roc-lang/roc/issues/new/choose");
+        #[allow(clippy::panic)] {
+            panic!($($arg)*);
+        }
+    })
+}
+
+/// `user_error!` should only ever be used temporarily.
+/// It is a way to document locations where we do not yet have nice error reporting.
+/// All cases of `user_error!` should eventually be replaced with pretty error printing using roc_reporting.
+#[macro_export]
+macro_rules! user_error {
+    ($($arg:tt)*) => ({
+        eprintln!("We ran into an issue while compiling your code.");
+        eprintln!("Sadly, we don't have a pretty error message for this case yet.");
+        eprintln!("If you can't figure out the problem from the context below, please reach out at: https://roc.zulipchat.com/");
+        eprintln!($($arg)*);
+        std::process::exit(1);
+    })
+}
+
+/// Assert that a type has the expected size on ARM
+#[macro_export]
+macro_rules! assert_sizeof_aarch64 {
+    ($t: ty, $expected_size: expr) => {
+        #[cfg(target_arch = "aarch64")]
+        static_assertions::assert_eq_size!($t, [u8; $expected_size]);
+    };
+}
+
+/// Assert that a type has the expected size in Wasm
+#[macro_export]
+macro_rules! assert_sizeof_wasm {
+    ($t: ty, $expected_size: expr) => {
+        #[cfg(target_family = "wasm")]
+        static_assertions::assert_eq_size!($t, [u8; $expected_size]);
+    };
+}
+
+/// Assert that a type has the expected size on any target not covered above
+/// In practice we use this for x86_64, and add specific macros for other targets
+#[macro_export]
+macro_rules! assert_sizeof_default {
+    ($t: ty, $expected_size: expr) => {
+        #[cfg(not(any(target_family = "wasm", target_arch = "aarch64")))]
+        static_assertions::assert_eq_size!($t, [u8; $expected_size]);
+    };
+}
+
+/// Assert that a type has the expected size on all targets
+#[macro_export]
+macro_rules! assert_sizeof_all {
+    ($t: ty, $expected_size: expr) => {
+        static_assertions::assert_eq_size!($t, [u8; $expected_size]);
+    };
+}
+
+/// Assert that a type has the expected size on all targets except wasm
+#[macro_export]
+macro_rules! assert_sizeof_non_wasm {
+    ($t: ty, $expected_size: expr) => {
+        #[cfg(not(target_family = "wasm"))]
+        static_assertions::assert_eq_size!($t, [u8; $expected_size]);
+    };
+}
+
+/// Assert that a type has `Copy`
+#[macro_export]
+macro_rules! assert_copyable {
+    ($t: ty) => {
+        static_assertions::assert_impl_all!($t: Copy);
+    };
+}
+
+// LARGE SCALE PROJECTS
+//
+// This section is for "todo!"-style macros enabled in sections where large-scale changes to the
+// language are in progress.
+
+#[macro_export]
+macro_rules! _incomplete_project {
+    ($project_name:literal, $tracking_issue_no:literal) => {
+        panic!(
+            "[{}] not yet implemented. Tracking issue: https://github.com/roc-lang/roc/issues/{}",
+            $project_name, $tracking_issue_no,
+        )
+    };
+    ($project_name:literal, $tracking_issue_no:literal, $($arg:tt)+) => {
+        panic!(
+            "[{}] not yet implemented. Tracking issue: https://github.com/roc-lang/roc/issues/{}.\nAdditional information: {}",
+            $project_name, $tracking_issue_no,
+            format_args!($($arg)+),
+        )
+    };
+}
+
+#[macro_export]
+macro_rules! todo_abilities {
+    () => {
+        $crate::_incomplete_project!("Abilities", 2463)
+    };
+    ($($arg:tt)+) => {
+        $crate::_incomplete_project!("Abilities", 2463, $($arg)+)
+    };
+}
+
+#[macro_export]
+macro_rules! todo_lambda_erasure {
+    () => {
+        $crate::_incomplete_project!("Lambda Erasure", 5575)
+    };
+    ($($arg:tt)+) => {
+        $crate::_incomplete_project!("Lambda Erasure", 5575, $($arg)+)
+    };
+}
+
+// END LARGE SCALE PROJECTS
diff --git a/crates/glue/.gitignore b/crates/glue/.gitignore
new file mode 100644
index 0000000000..4bdc65207f
--- /dev/null
+++ b/crates/glue/.gitignore
@@ -0,0 +1,3 @@
+*.so
+*.dylib
+*.dll
diff --git a/crates/glue/Cargo.toml b/crates/glue/Cargo.toml
new file mode 100644
index 0000000000..c01b7d94cb
--- /dev/null
+++ b/crates/glue/Cargo.toml
@@ -0,0 +1,44 @@
+[package]
+name = "roc_glue"
+description = "Generates code needed for platform hosts to communicate with Roc apps. This tool is not necessary for writing a platform in another language, however, it's a great convenience! Currently supports Rust platforms, and the plan is to support any language via a plugin model."
+
+authors.workspace = true
+edition.workspace = true
+license.workspace = true
+version.workspace = true
+
+[dependencies]
+roc_build = { path = "../compiler/build" }
+roc_builtins = { path = "../compiler/builtins" }
+roc_can = { path = "../compiler/can" }
+roc_collections = { path = "../compiler/collections" }
+roc_error_macros = { path = "../error_macros" }
+roc_gen_llvm = { path = "../compiler/gen_llvm" }
+roc_linker = { path = "../linker" }
+roc_load = { path = "../compiler/load" }
+roc_module = { path = "../compiler/module" }
+roc_mono = { path = "../compiler/mono" }
+roc_packaging = { path = "../packaging" }
+roc_reporting = { path = "../reporting" }
+roc_std = { path = "../roc_std" }
+roc_target = { path = "../compiler/roc_target" }
+roc_tracing = { path = "../tracing" }
+roc_types = { path = "../compiler/types" }
+
+backtrace.workspace = true
+bumpalo.workspace = true
+fnv.workspace = true
+indexmap.workspace = true
+libc.workspace = true
+libloading.workspace = true
+strum.workspace = true
+strum_macros.workspace = true
+target-lexicon.workspace = true
+tempfile.workspace = true
+
+[dev-dependencies]
+cli_utils = { path = "../cli_utils" }
+dircpy.workspace = true
+
+indoc.workspace = true
+pretty_assertions.workspace = true
diff --git a/crates/glue/platform/File.roc b/crates/glue/platform/File.roc
new file mode 100644
index 0000000000..da7a99e517
--- /dev/null
+++ b/crates/glue/platform/File.roc
@@ -0,0 +1,5 @@
+interface File
+    exposes [File]
+    imports []
+
+File : { name : Str, content : Str }
diff --git a/crates/glue/platform/InternalTypeId.roc b/crates/glue/platform/InternalTypeId.roc
new file mode 100644
index 0000000000..ebdce6d173
--- /dev/null
+++ b/crates/glue/platform/InternalTypeId.roc
@@ -0,0 +1,11 @@
+interface InternalTypeId
+    exposes [InternalTypeId, fromNat, toNat]
+    imports []
+
+InternalTypeId : Nat
+
+toNat : InternalTypeId -> Nat
+toNat = \x -> x
+
+fromNat : Nat -> InternalTypeId
+fromNat = \x -> x
diff --git a/crates/glue/platform/Shape.roc b/crates/glue/platform/Shape.roc
new file mode 100644
index 0000000000..14d805fed4
--- /dev/null
+++ b/crates/glue/platform/Shape.roc
@@ -0,0 +1,136 @@
+interface Shape
+    exposes [Shape, RocNum, RocTagUnion, RocStructFields, RocFn, RocSingleTagPayload]
+    imports [TypeId.{ TypeId }]
+
+Shape : [
+    RocStr,
+    Bool,
+    RocResult TypeId TypeId,
+    Num RocNum,
+    RocList TypeId,
+    RocDict TypeId TypeId,
+    RocSet TypeId,
+    RocBox TypeId,
+    TagUnion RocTagUnion,
+    EmptyTagUnion,
+    Struct
+        {
+            name : Str,
+            fields : RocStructFields,
+        },
+    TagUnionPayload
+        {
+            name : Str,
+            fields : RocStructFields,
+        },
+    ## A recursive pointer, e.g. in StrConsList : [Nil, Cons Str StrConsList],
+    ## this would be the field of Cons containing the (recursive) StrConsList type,
+    ## and the TypeId is the TypeId of StrConsList itself.
+    RecursivePointer TypeId,
+    Function RocFn,
+    # A zero-sized type, such as an empty record or a single-tag union with no payload
+    Unit,
+    Unsized,
+]
+
+RocNum : [
+    I8,
+    U8,
+    I16,
+    U16,
+    I32,
+    U32,
+    I64,
+    U64,
+    I128,
+    U128,
+    F32,
+    F64,
+    Dec,
+]
+
+RocTagUnion : [
+    Enumeration
+        {
+            name : Str,
+            tags : List Str,
+            size : U32,
+        },
+    ## A non-recursive tag union
+    ## e.g. `Result a e : [Ok a, Err e]`
+    NonRecursive
+        {
+            name : Str,
+            tags : List { name : Str, payload : [Some TypeId, None] },
+            discriminantSize : U32,
+            discriminantOffset : U32,
+        },
+    ## A recursive tag union (general case)
+    ## e.g. `Expr : [Sym Str, Add Expr Expr]`
+    Recursive
+        {
+            name : Str,
+            tags : List { name : Str, payload : [Some TypeId, None] },
+            discriminantSize : U32,
+            discriminantOffset : U32,
+        },
+    ## A recursive tag union that has an empty variant
+    ## Optimization: Represent the empty variant as null pointer => no memory usage & fast comparison
+    ## It has more than one other variant, so they need tag IDs (payloads are "wrapped")
+    ## e.g. `FingerTree a : [Empty, Single a, More (Some a) (FingerTree (Tuple a)) (Some a)]`
+    ## see also: https://youtu.be/ip92VMpf_-A?t=164
+    NullableWrapped
+        {
+            name : Str,
+            indexOfNullTag : U16,
+            tags : List { name : Str, payload : [Some TypeId, None] },
+            discriminantSize : U32,
+            discriminantOffset : U32,
+        },
+    ## Optimization: No need to store a tag ID (the payload is "unwrapped")
+    ## e.g. `RoseTree a : [Tree a (List (RoseTree a))]`
+    NonNullableUnwrapped
+        {
+            name : Str,
+            tagName : Str,
+            payload : TypeId, # These always have a payload.
+        },
+    ## Optimization: No need to store a tag ID (the payload is "unwrapped")
+    ## e.g. `[Foo Str Bool]`
+    SingleTagStruct
+        {
+            name : Str,
+            tagName : Str,
+            payload : RocSingleTagPayload,
+        },
+    ## A recursive tag union with only two variants, where one is empty.
+    ## Optimizations: Use null for the empty variant AND don't store a tag ID for the other variant.
+    ## e.g. `ConsList a : [Nil, Cons a (ConsList a)]`
+    NullableUnwrapped
+        {
+            name : Str,
+            nullTag : Str,
+            nonNullTag : Str,
+            nonNullPayload : TypeId,
+            whichTagIsNull : [FirstTagIsNull, SecondTagIsNull],
+        },
+]
+
+RocStructFields : [
+    HasNoClosure (List { name : Str, id : TypeId }),
+    HasClosure (List { name : Str, id : TypeId, accessors : { getter : Str } }),
+]
+
+RocSingleTagPayload : [
+    HasClosure (List { name : Str, id : TypeId }),
+    HasNoClosure (List { id : TypeId }),
+]
+
+RocFn : {
+    functionName : Str,
+    externName : Str,
+    args : List TypeId,
+    lambdaSet : TypeId,
+    ret : TypeId,
+    isToplevel : Bool,
+}
diff --git a/crates/glue/platform/Target.roc b/crates/glue/platform/Target.roc
new file mode 100644
index 0000000000..3103486974
--- /dev/null
+++ b/crates/glue/platform/Target.roc
@@ -0,0 +1,22 @@
+interface Target
+    exposes [Target, Architecture, OperatingSystem]
+    imports []
+
+Target : {
+    architecture : Architecture,
+    operatingSystem : OperatingSystem,
+}
+
+Architecture : [
+    Aarch32,
+    Aarch64,
+    Wasm32,
+    X86x32,
+    X86x64,
+]
+
+OperatingSystem : [
+    Windows,
+    Unix,
+    Wasi,
+]
diff --git a/crates/glue/platform/TypeId.roc b/crates/glue/platform/TypeId.roc
new file mode 100644
index 0000000000..18e1b88776
--- /dev/null
+++ b/crates/glue/platform/TypeId.roc
@@ -0,0 +1,5 @@
+interface TypeId
+    exposes [TypeId]
+    imports [InternalTypeId.{ InternalTypeId }]
+
+TypeId : InternalTypeId
diff --git a/crates/glue/platform/Types.roc b/crates/glue/platform/Types.roc
new file mode 100644
index 0000000000..eca34dc9fc
--- /dev/null
+++ b/crates/glue/platform/Types.roc
@@ -0,0 +1,66 @@
+interface Types
+    exposes [Types, shape, size, alignment, target, walkShapes, entryPoints]
+    imports [Shape.{ Shape }, TypeId.{ TypeId }, Target.{ Target }, InternalTypeId]
+
+# TODO: switch AssocList uses to Dict once roc_std is updated.
+Tuple1 : [T Str TypeId]
+Tuple2 : [T TypeId (List TypeId)]
+
+Types := {
+    # These are all indexed by TypeId
+    types : List Shape,
+    sizes : List U32,
+    aligns : List U32,
+
+    # Needed to check for duplicates
+    typesByName : List Tuple1,
+
+    ## Dependencies - that is, which type depends on which other type.
+    ## This is important for declaration order in C; we need to output a
+    ## type declaration earlier in the file than where it gets referenced by another type.
+    deps : List Tuple2,
+
+    ## Names and types of the entry points of the program (e.g. mainForHost)
+    entrypoints : List Tuple1,
+    target : Target,
+}
+
+target : Types -> Target
+target = \@Types types -> types.target
+
+entryPoints : Types -> List Tuple1
+entryPoints = \@Types { entrypoints } -> entrypoints
+
+walkShapes : Types, state, (state, Shape, TypeId -> state) -> state
+walkShapes = \@Types { types: shapes }, originalState, update ->
+    List.walkWithIndex shapes originalState \state, elem, index ->
+        id = InternalTypeId.fromNat index
+
+        update state elem id
+
+shape : Types, TypeId -> Shape
+shape = \@Types types, id ->
+    when List.get types.types (InternalTypeId.toNat id) is
+        Ok answer -> answer
+        Err OutOfBounds ->
+            idStr = Num.toStr (InternalTypeId.toNat id)
+
+            crash "TypeId #\(idStr) was not found in Types. This should never happen, and means there was a bug in `roc glue`. If you have time, please open an issue at "
+
+alignment : Types, TypeId -> U32
+alignment = \@Types types, id ->
+    when List.get types.aligns (InternalTypeId.toNat id) is
+        Ok answer -> answer
+        Err OutOfBounds ->
+            idStr = Num.toStr (InternalTypeId.toNat id)
+
+            crash "TypeId #\(idStr) was not found in Types. This should never happen, and means there was a bug in `roc glue`. If you have time, please open an issue at "
+
+size : Types, TypeId -> U32
+size = \@Types types, id ->
+    when List.get types.sizes (InternalTypeId.toNat id) is
+        Ok answer -> answer
+        Err OutOfBounds ->
+            idStr = Num.toStr (InternalTypeId.toNat id)
+
+            crash "TypeId #\(idStr) was not found in Types. This should never happen, and means there was a bug in `roc glue`. If you have time, please open an issue at "
diff --git a/crates/glue/platform/main.roc b/crates/glue/platform/main.roc
new file mode 100644
index 0000000000..8abd83a981
--- /dev/null
+++ b/crates/glue/platform/main.roc
@@ -0,0 +1,9 @@
+platform "roc-lang/glue"
+    requires {} { makeGlue : List Types -> Result (List File) Str }
+    exposes [Shape, File, Types, TypeId, Target]
+    packages {}
+    imports [Types.{ Types }, File.{ File }]
+    provides [makeGlueForHost]
+
+makeGlueForHost : List Types -> Result (List File) Str
+makeGlueForHost = \types -> makeGlue types
diff --git a/crates/glue/src/RustGlue.roc b/crates/glue/src/RustGlue.roc
new file mode 100644
index 0000000000..284315f918
--- /dev/null
+++ b/crates/glue/src/RustGlue.roc
@@ -0,0 +1,2161 @@
+app "rust-glue"
+    packages { pf: "../platform/main.roc" }
+    imports [
+        pf.Types.{ Types },
+        pf.Shape.{ Shape, RocFn },
+        pf.File.{ File },
+        pf.TypeId.{ TypeId },
+        "../static/Cargo.toml" as rocAppCargoToml : Str,
+        "../../roc_std/Cargo.toml" as rocStdCargoToml : Str,
+        "../../roc_std/src/lib.rs" as rocStdLib : Str,
+        "../../roc_std/src/roc_box.rs" as rocStdBox : Str,
+        "../../roc_std/src/roc_list.rs" as rocStdList : Str,
+        "../../roc_std/src/roc_dict.rs" as rocStdDict : Str,
+        "../../roc_std/src/roc_set.rs" as rocStdSet : Str,
+        "../../roc_std/src/roc_str.rs" as rocStdStr : Str,
+        "../../roc_std/src/storage.rs" as rocStdStorage : Str,
+    ]
+    provides [makeGlue] to pf
+
+makeGlue : List Types -> Result (List File) Str
+makeGlue = \typesByArch ->
+    modFileContent =
+        List.walk typesByArch fileHeader \content, types ->
+            arch = (Types.target types).architecture
+            archStr = archName arch
+
+            Str.concat
+                content
+                """
+                #[cfg(target_arch = "\(archStr)")]
+                mod \(archStr);
+                #[cfg(target_arch = "\(archStr)")]
+                pub use \(archStr)::*;
+
+                """
+
+    typesByArch
+    |> List.map convertTypesToFile
+    |> List.append { name: "roc_app/src/lib.rs", content: modFileContent }
+    |> List.concat staticFiles
+    |> Ok
+
+## These are always included, and don't depend on the specifics of the app.
+staticFiles : List File
+staticFiles = [
+    { name: "roc_app/Cargo.toml", content: rocAppCargoToml },
+    { name: "roc_std/Cargo.toml", content: rocStdCargoToml },
+    { name: "roc_std/src/lib.rs", content: rocStdLib },
+    { name: "roc_std/src/roc_box.rs", content: rocStdBox },
+    { name: "roc_std/src/roc_list.rs", content: rocStdList },
+    { name: "roc_std/src/roc_dict.rs", content: rocStdDict },
+    { name: "roc_std/src/roc_set.rs", content: rocStdSet },
+    { name: "roc_std/src/roc_str.rs", content: rocStdStr },
+    { name: "roc_std/src/storage.rs", content: rocStdStorage },
+]
+
+convertTypesToFile : Types -> File
+convertTypesToFile = \types ->
+    content =
+        Types.walkShapes types fileHeader \buf, type, id ->
+            when type is
+                Struct { name, fields } ->
+                    generateStruct buf types id name fields Public
+
+                TagUnionPayload { name, fields } ->
+                    generateStruct buf types id name (nameTagUnionPayloadFields fields) Public
+
+                TagUnion (Enumeration { name, tags, size }) ->
+                    generateEnumeration buf types type name tags size
+
+                TagUnion (NonRecursive { name, tags, discriminantSize, discriminantOffset }) ->
+                    if !(List.isEmpty tags) then
+                        generateNonRecursiveTagUnion buf types id name tags discriminantSize discriminantOffset
+                    else
+                        buf
+
+                TagUnion (Recursive { name, tags, discriminantSize, discriminantOffset }) ->
+                    if !(List.isEmpty tags) then
+                        generateRecursiveTagUnion buf types id name tags discriminantSize discriminantOffset None
+                    else
+                        buf
+
+                TagUnion (NullableWrapped { name, indexOfNullTag, tags, discriminantSize, discriminantOffset }) ->
+                    # TODO: generate this as `TypeName(*mut u8)` if the payload contains functions / unsized types
+                    generateRecursiveTagUnion buf types id name tags discriminantSize discriminantOffset (Some indexOfNullTag)
+
+                TagUnion (NullableUnwrapped { name, nullTag, nonNullTag, nonNullPayload, whichTagIsNull }) ->
+                    generateNullableUnwrapped buf types id name nullTag nonNullTag nonNullPayload whichTagIsNull
+
+                TagUnion (SingleTagStruct { name, tagName, payload }) ->
+                    generateSingleTagStruct buf types name tagName payload
+
+                TagUnion (NonNullableUnwrapped { name, tagName, payload }) ->
+                    generateNonNullableUnwrapped buf types name tagName payload 0 0 None
+
+                Function rocFn ->
+                    if rocFn.isToplevel then
+                        buf
+                    else
+                        generateFunction buf types rocFn
+
+                RecursivePointer _ ->
+                    # This is recursively pointing to a type that should already have been added,
+                    # so no extra work needs to happen.
+                    buf
+
+                Unit
+                | Unsized
+                | EmptyTagUnion
+                | Num _
+                | Bool
+                | RocResult _ _
+                | RocStr
+                | RocDict _ _
+                | RocSet _
+                | RocList _
+                | RocBox _ ->
+                    # These types don't need to be declared in Rust.
+                    # TODO: Eventually we want to generate roc_std. So these types will need to be emitted.
+                    buf
+
+    arch = (Types.target types).architecture
+    archStr = archName arch
+
+    {
+        name: "roc_app/src/\(archStr).rs",
+        content: content |> generateEntryPoints types,
+    }
+
+generateEntryPoints : Str, Types -> Str
+generateEntryPoints = \buf, types ->
+    List.walk (Types.entryPoints types) buf \accum, T name id -> generateEntryPoint accum types name id
+
+generateEntryPoint : Str, Types, Str, TypeId -> Str
+generateEntryPoint = \buf, types, name, id ->
+    publicSignature =
+        when Types.shape types id is
+            Function rocFn ->
+                arguments =
+                    toArgStr rocFn.args types \argId, _shape, index ->
+                        type = typeName types argId
+                        indexStr = Num.toStr index
+
+                        "arg\(indexStr): \(type)"
+
+                ret = typeName types rocFn.ret
+
+                "(\(arguments)) -> \(ret)"
+
+            _ ->
+                ret = typeName types id
+                "() -> \(ret)"
+
+    (externSignature, returnTypeName, returnsFn) =
+        when Types.shape types id is
+            Function rocFn ->
+                arguments =
+                    toArgStr rocFn.args types \argId, shape, _index ->
+                        type = typeName types argId
+
+                        if canDeriveCopy types shape then
+                            "_: \(type)"
+                        else
+                            "_: &mut core::mem::ManuallyDrop<\(type)>"
+
+                ret = typeName types rocFn.ret
+                when Types.shape types rocFn.ret is
+                    Function _ ->
+                        ("(_: *mut u8, \(arguments))", ret, Bool.true)
+                    _ -> 
+                        ("(_: *mut \(ret), \(arguments))", ret, Bool.false)
+
+            _ ->
+                ret = typeName types id
+                ("(_: *mut \(ret))", ret, Bool.false)
+
+    externArguments =
+        when Types.shape types id is
+            Function rocFn ->
+                toArgStr rocFn.args types \_argId, shape, index ->
+                    indexStr = Num.toStr index
+
+                    if canDeriveCopy types shape then
+                        "arg\(indexStr)"
+                    else
+                        "&mut core::mem::ManuallyDrop::new(arg\(indexStr))"
+
+            _ ->
+                ""
+
+    if returnsFn then
+        """
+        \(buf)
+
+        pub fn \(name)\(publicSignature) {
+            extern "C" {
+                fn roc__\(name)_1_exposed_generic\(externSignature);
+                fn roc__\(name)_1_exposed_size() -> i64;
+            }
+
+            unsafe {
+                let capacity = roc__\(name)_1_exposed_size() as usize;
+
+                let mut ret = \(returnTypeName) {
+                    closure_data: Vec::with_capacity(capacity),
+                };
+                ret.closure_data.resize(capacity, 0);
+
+                roc__\(name)_1_exposed_generic(ret.closure_data.as_mut_ptr(), \(externArguments));
+
+                ret
+            }
+        }
+        """
+    else
+        """
+        \(buf)
+
+        pub fn \(name)\(publicSignature) {
+            extern "C" {
+                fn roc__\(name)_1_exposed_generic\(externSignature);
+            }
+
+            let mut ret = core::mem::MaybeUninit::uninit();
+
+            unsafe {
+                roc__\(name)_1_exposed_generic(ret.as_mut_ptr(), \(externArguments));
+
+                ret.assume_init()
+            }
+        }
+        """
+
+generateFunction : Str, Types, RocFn -> Str
+generateFunction = \buf, types, rocFn ->
+    name = rocFn.functionName
+    externName = rocFn.externName
+
+    publicArguments =
+        toArgStr rocFn.args types \argId, _shape, index ->
+            type = typeName types argId
+            indexStr = Num.toStr index
+
+            "arg\(indexStr): \(type)"
+
+    externDefArguments =
+        withoutUnit =
+            toArgStr rocFn.args types \argId, _shape, index ->
+                type = typeName types argId
+                indexStr = Num.toStr index
+
+                "arg\(indexStr): *const \(type)"
+
+        if Str.isEmpty withoutUnit then
+            # These always have a first argument that's a pointer, even if it's to nothing.
+            "arg0: *const ()"
+        else
+            withoutUnit
+
+    externCallArguments =
+        withoutUnit =
+            toArgStr rocFn.args types \_argId, _shape, index ->
+                indexStr = Num.toStr index
+
+                "&arg\(indexStr)"
+
+        if Str.isEmpty withoutUnit then
+            # These always have a first argument that's a pointer, even if it's to nothing.
+            "&()"
+        else
+            withoutUnit
+
+    publicComma = if Str.isEmpty publicArguments then "" else ", "
+
+    ret = typeName types rocFn.ret
+
+    """
+    \(buf)
+
+    #[repr(C)]
+    #[derive(Debug)]
+    pub struct \(name) {
+        closure_data: Vec,
+    }
+
+    impl \(name) {
+        pub fn force_thunk(mut self\(publicComma)\(publicArguments)) -> \(ret) {
+            extern "C" {
+                fn \(externName)(\(externDefArguments), closure_data: *mut u8, output: *mut \(ret));
+            }
+
+            let mut output = core::mem::MaybeUninit::uninit();
+
+            unsafe {
+                \(externName)(\(externCallArguments), self.closure_data.as_mut_ptr(), output.as_mut_ptr());
+
+                output.assume_init()
+            }
+        }
+    }
+    """
+
+generateStruct : Str, Types, TypeId, _, _, _ -> Str
+generateStruct = \buf, types, id, name, structFields, visibility ->
+    escapedName = escapeKW name
+    repr =
+        length =
+            when structFields is
+                HasClosure fields -> List.len fields
+                HasNoClosure fields -> List.len fields
+        if length <= 1 then
+            "transparent"
+        else
+            "C"
+
+    pub =
+        when visibility is
+            Public -> "pub "
+            Private -> ""
+
+    structType = Types.shape types id
+
+    buf
+    |> generateDeriveStr types structType IncludeDebug
+    |> Str.concat "#[repr(\(repr))]\n\(pub)struct \(escapedName) {\n"
+    |> generateStructFields types Public structFields
+    |> Str.concat "}\n\n"
+
+generateStructFields = \buf, types, visibility, structFields ->
+    when structFields is
+        HasNoClosure fields ->
+            List.walk fields buf (generateStructFieldWithoutClosure types visibility)
+
+        HasClosure fields ->
+            List.walk fields buf (generateStructFieldWithoutClosure types visibility)
+
+generateStructFieldWithoutClosure = \types, visibility ->
+    \accum, { name: fieldName, id } ->
+        typeStr = typeName types id
+        escapedFieldName = escapeKW fieldName
+
+        pub =
+            when visibility is
+                Public -> "pub"
+                Private -> ""
+
+        Str.concat accum "\(indent)\(pub) \(escapedFieldName): \(typeStr),\n"
+
+nameTagUnionPayloadFields = \payloadFields ->
+    # Tag union payloads have numbered fields, so we prefix them
+    # with an "f" because Rust doesn't allow struct fields to be numbers.
+    when payloadFields is
+        HasNoClosure fields ->
+            renamedFields = List.map fields \{ name, id } -> { name: "f\(name)", id }
+            HasNoClosure renamedFields
+
+        HasClosure fields ->
+            renamedFields = List.map fields \{ name, id, accessors } -> { name: "f\(name)", id, accessors }
+            HasClosure renamedFields
+
+generateEnumeration = \buf, types, enumType, name, tags, tagBytes ->
+    escapedName = escapeKW name
+
+    reprBits = tagBytes * 8 |> Num.toStr
+
+    buf
+    |> generateDeriveStr types enumType ExcludeDebug
+    |> Str.concat "#[repr(u\(reprBits))]\npub enum \(escapedName) {\n"
+    |> \b -> List.walkWithIndex tags b generateEnumTags
+    |>
+    # Enums require a custom debug impl to ensure naming is identical on all platforms.
+    Str.concat
+        """
+        }
+
+        impl core::fmt::Debug for \(escapedName) {
+            fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
+                match self {
+
+        """
+    |> \b -> List.walk tags b (generateEnumTagsDebug name)
+    |> Str.concat "\(indent)\(indent)}\n\(indent)}\n}\n\n"
+
+generateEnumTags = \accum, name, index ->
+    indexStr = Num.toStr index
+
+    Str.concat accum "\(indent)\(name) = \(indexStr),\n"
+
+generateEnumTagsDebug = \name ->
+    \accum, tagName ->
+        Str.concat accum "\(indent)\(indent)\(indent)Self::\(tagName) => f.write_str(\"\(name)::\(tagName)\"),\n"
+
+deriveCloneTagUnion : Str, Str, List { name : Str, payload : [Some TypeId, None] } -> Str
+deriveCloneTagUnion = \buf, tagUnionType, tags ->
+    clones =
+        List.walk tags "" \accum, { name: tagName } ->
+            """
+            \(accum)
+                            \(tagName) => union_\(tagUnionType) {
+                                \(tagName): self.payload.\(tagName).clone(),
+                            },
+            """
+
+    """
+    \(buf)
+
+    impl Clone for \(tagUnionType) {
+        fn clone(&self) -> Self {
+            use discriminant_\(tagUnionType)::*;
+
+            let payload = unsafe {
+                match self.discriminant {\(clones)
+                }
+            };
+
+            Self {
+                discriminant: self.discriminant,
+                payload,
+            }
+        }
+    }
+    """
+
+deriveDebugTagUnion : Str, Types, Str, List { name : Str, payload : [Some TypeId, None] } -> Str
+deriveDebugTagUnion = \buf, types, tagUnionType, tags ->
+    checks =
+        List.walk tags "" \accum, { name: tagName, payload } ->
+            type =
+                when payload is
+                    Some id -> typeName types id
+                    None -> "()"
+
+            """
+            \(accum)
+                            \(tagName) => {
+                                let field: &\(type) = &self.payload.\(tagName);
+                                f.debug_tuple("\(tagUnionType)::\(tagName)").field(field).finish()
+                            },
+            """
+
+    """
+    \(buf)
+
+    impl core::fmt::Debug for \(tagUnionType) {
+        fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
+            use discriminant_\(tagUnionType)::*;
+
+            unsafe {
+                match self.discriminant {\(checks)
+                }
+            }
+        }
+    }
+    """
+
+deriveEqTagUnion : Str, Types, Shape, Str -> Str
+deriveEqTagUnion = \buf, types, shape, tagUnionType ->
+    if canSupportEqHashOrd types shape then
+        """
+        \(buf)
+
+        impl Eq for \(tagUnionType) {}
+        """
+    else
+        buf
+
+derivePartialEqTagUnion : Str, Types, Shape, Str, List { name : Str, payload : [Some TypeId, None] } -> Str
+derivePartialEqTagUnion = \buf, types, shape, tagUnionType, tags ->
+    if canSupportPartialEqOrd types shape then
+        checks =
+            List.walk tags "" \accum, { name: tagName } ->
+                """
+                \(accum)
+                                \(tagName) => self.payload.\(tagName) == other.payload.\(tagName),
+                """
+
+        """
+        \(buf)
+
+        impl PartialEq for \(tagUnionType) {
+            fn eq(&self, other: &Self) -> bool {
+                use discriminant_\(tagUnionType)::*;
+
+                if self.discriminant != other.discriminant {
+                    return false;
+                }
+
+                unsafe {
+                    match self.discriminant {\(checks)
+                    }
+                }
+            }
+        }
+        """
+    else
+        buf
+
+deriveOrdTagUnion : Str, Types, Shape, Str -> Str
+deriveOrdTagUnion = \buf, types, shape, tagUnionType ->
+    if canSupportEqHashOrd types shape then
+        """
+        \(buf)
+
+        impl Ord for \(tagUnionType) {
+            fn cmp(&self, other: &Self) -> std::cmp::Ordering {
+                self.partial_cmp(other).unwrap()
+            }
+        }
+        """
+    else
+        buf
+
+derivePartialOrdTagUnion : Str, Types, Shape, Str, List { name : Str, payload : [Some TypeId, None] } -> Str
+derivePartialOrdTagUnion = \buf, types, shape, tagUnionType, tags ->
+    if canSupportPartialEqOrd types shape then
+        checks =
+            List.walk tags "" \accum, { name: tagName } ->
+                """
+                \(accum)
+                                    \(tagName) => self.payload.\(tagName).partial_cmp(&other.payload.\(tagName)),
+                """
+
+        """
+        \(buf)
+
+        impl PartialOrd for \(tagUnionType) {
+            fn partial_cmp(&self, other: &Self) -> Option {
+                use discriminant_\(tagUnionType)::*;
+
+                use std::cmp::Ordering::*;
+
+                match self.discriminant.cmp(&other.discriminant) {
+                    Less => Option::Some(Less),
+                    Greater => Option::Some(Greater),
+                    Equal => unsafe {
+                        match self.discriminant {\(checks)
+                        }
+                    },
+                }
+            }
+        }
+        """
+    else
+        buf
+
+deriveHashTagUnion : Str, Types, Shape, Str, List { name : Str, payload : [Some TypeId, None] } -> Str
+deriveHashTagUnion = \buf, types, shape, tagUnionType, tags ->
+    if canSupportEqHashOrd types shape then
+        checks =
+            List.walk tags "" \accum, { name: tagName } ->
+                """
+                \(accum)
+                                \(tagName) => self.payload.\(tagName).hash(state),
+                """
+
+        """
+        \(buf)
+
+        impl core::hash::Hash for \(tagUnionType) {
+            fn hash(&self, state: &mut H) {
+                use discriminant_\(tagUnionType)::*;
+
+                unsafe {
+                    match self.discriminant {\(checks)
+                    }
+                }
+            }
+        }
+        """
+    else
+        buf
+
+generateConstructorFunctions : Str, Types, Str, List { name : Str, payload : [Some TypeId, None] } -> Str
+generateConstructorFunctions = \buf, types, tagUnionType, tags ->
+    buf
+    |> Str.concat "\n\nimpl \(tagUnionType) {"
+    |> \b -> List.walk tags b \accum, r -> generateConstructorFunction accum types tagUnionType r.name r.payload
+    |> Str.concat "\n}\n\n"
+
+generateConstructorFunction : Str, Types, Str, Str, [Some TypeId, None] -> Str
+generateConstructorFunction = \buf, types, tagUnionType, name, optPayload ->
+    when optPayload is
+        None ->
+            """
+            \(buf)
+
+                pub fn \(name)() -> Self {
+                    Self {
+                        discriminant: discriminant_\(tagUnionType)::\(name),
+                        payload: union_\(tagUnionType) {
+                            \(name): (),
+                        }
+                    }
+                }
+            """
+
+        Some payloadId ->
+            payloadType = typeName types payloadId
+            shape = Types.shape types payloadId
+
+            new =
+                if canDeriveCopy types shape then
+                    "payload"
+                else
+                    "core::mem::ManuallyDrop::new(payload)"
+
+            """
+            \(buf)
+
+                pub fn \(name)(payload: \(payloadType)) -> Self {
+                    Self {
+                        discriminant: discriminant_\(tagUnionType)::\(name),
+                        payload: union_\(tagUnionType) {
+                            \(name): \(new),
+                        }
+                    }
+                }
+            """
+
+generateDestructorFunctions : Str, Types, Str, List { name : Str, payload : [Some TypeId, None] } -> Str
+generateDestructorFunctions = \buf, types, tagUnionType, tags ->
+    buf
+    |> Str.concat "\n\nimpl \(tagUnionType) {"
+    |> \b -> List.walk tags b \accum, r -> generateDestructorFunction accum types tagUnionType r.name r.payload
+    |> Str.concat "\n}\n\n"
+
+generateDestructorFunction : Str, Types, Str, Str, [Some TypeId, None] -> Str
+generateDestructorFunction = \buf, types, tagUnionType, name, optPayload ->
+    when optPayload is
+        None ->
+            """
+            \(buf)
+
+                pub fn is_\(name)(&self) -> bool {
+                    matches!(self.discriminant, discriminant_\(tagUnionType)::\(name))
+                }
+            """
+
+        Some payloadId ->
+            payloadType = typeName types payloadId
+            shape = Types.shape types payloadId
+
+            take =
+                if canDeriveCopy types shape then
+                    "unsafe { self.payload.\(name) }"
+                else
+                    "unsafe { core::mem::ManuallyDrop::take(&mut self.payload.\(name)) }"
+
+            """
+            \(buf)
+
+                pub fn unwrap_\(name)(mut self) -> \(payloadType) {
+                    debug_assert_eq!(self.discriminant, discriminant_\(tagUnionType)::\(name));
+                    \(take)
+                }
+
+                pub fn is_\(name)(&self) -> bool {
+                    matches!(self.discriminant, discriminant_\(tagUnionType)::\(name))
+                }
+            """
+
+generateNonRecursiveTagUnion : Str, Types, TypeId, Str, List { name : Str, payload : [Some TypeId, None] }, U32, U32 -> Str
+generateNonRecursiveTagUnion = \buf, types, id, name, tags, discriminantSize, discriminantOffset ->
+    escapedName = escapeKW name
+    discriminantName = "discriminant_\(escapedName)"
+    unionName = "union_\(escapedName)"
+    discriminantOffsetStr = Num.toStr discriminantOffset
+    tagNames = List.map tags \{ name: n } -> n
+    selfMut = "self"
+
+    max = \a, b -> if a >= b then a else b
+
+    alignOfUnion =
+        List.walk tags 1 \accum, { payload } ->
+            when payload is
+                Some payloadId -> max accum (Types.alignment types payloadId)
+                None -> accum
+
+    alignOfUnionStr = Num.toStr alignOfUnion
+
+    sizeOfUnionStr =
+        List.walk tags 1 \accum, { payload } ->
+            when payload is
+                Some payloadId -> max accum (Types.size types payloadId)
+                None -> accum
+        |> nextMultipleOf alignOfUnion
+        |> Num.toStr
+
+    sizeOfSelf = Num.toStr (Types.size types id)
+    alignOfSelf = Num.toStr (Types.alignment types id)
+    shape = Types.shape types id
+
+    # TODO: this value can be different than the alignment of `id`
+    align =
+        List.walk tags 1 \accum, { payload } ->
+            when payload is
+                Some payloadId -> max accum (Types.alignment types payloadId)
+                None -> accum
+        |> Num.toStr
+
+    buf
+    |> generateDiscriminant types discriminantName tagNames discriminantSize
+    |> Str.concat "#[repr(C, align(\(align)))]\npub union \(unionName) {\n"
+    |> \b -> List.walk tags b (generateUnionField types)
+    |> Str.concat
+        """
+        }
+
+        const _SIZE_CHECK_\(unionName): () = assert!(core::mem::size_of::<\(unionName)>() == \(sizeOfUnionStr));
+        const _ALIGN_CHECK_\(unionName): () = assert!(core::mem::align_of::<\(unionName)>() == \(alignOfUnionStr));
+
+        const _SIZE_CHECK_\(escapedName): () = assert!(core::mem::size_of::<\(escapedName)>() == \(sizeOfSelf));
+        const _ALIGN_CHECK_\(escapedName): () = assert!(core::mem::align_of::<\(escapedName)>() == \(alignOfSelf));
+
+        impl \(escapedName) {
+            \(discriminantDocComment)
+            pub fn discriminant(&self) -> \(discriminantName) {
+                unsafe {
+                    let bytes = core::mem::transmute::<&Self, &[u8; core::mem::size_of::()]>(self);
+
+                    core::mem::transmute::(*bytes.as_ptr().add(\(discriminantOffsetStr)))
+                }
+            }
+
+            /// Internal helper
+            fn set_discriminant(&mut self, discriminant: \(discriminantName)) {
+                let discriminant_ptr: *mut \(discriminantName) = (self as *mut \(escapedName)).cast();
+
+                unsafe {
+                    *(discriminant_ptr.add(\(discriminantOffsetStr))) = discriminant;
+                }
+            }
+        }
+
+
+        """
+    |> Str.concat
+        """
+        #[repr(C)]
+        pub struct \(escapedName) {
+            payload: union_\(escapedName),
+            discriminant: discriminant_\(escapedName),
+        }
+        """
+    |> deriveCloneTagUnion escapedName tags
+    |> deriveDebugTagUnion types escapedName tags
+    |> deriveEqTagUnion types shape escapedName
+    |> derivePartialEqTagUnion types shape escapedName tags
+    |> deriveOrdTagUnion types shape escapedName
+    |> derivePartialOrdTagUnion types shape escapedName tags
+    |> deriveHashTagUnion types shape escapedName tags
+    |> generateDestructorFunctions types escapedName tags
+    |> generateConstructorFunctions types escapedName tags
+    |> \b ->
+        type = Types.shape types id
+        if cannotSupportCopy types type then
+            # A custom drop impl is only needed when we can't derive copy.
+            b
+            |> Str.concat
+                """
+                impl Drop for \(escapedName) {
+                    fn drop(&mut self) {
+                        // Drop the payloads
+
+                """
+            |> generateTagUnionDropPayload types selfMut tags discriminantName discriminantSize 2
+            |> Str.concat
+                """
+                    }
+                }
+
+
+                """
+        else
+            b
+
+generateNonNullableUnwrapped = \buf, types, name, tagName, payload, discriminantSize, _discriminantOffset, _nullTagIndex ->
+    escapedName = escapeKW name
+    discriminantName = "discriminant_\(escapedName)"
+
+    payloadFields =
+        when Types.shape types payload is
+            TagUnionPayload { fields } ->
+                when fields is
+                    HasNoClosure xs -> List.map xs .id
+                    HasClosure xs -> List.map xs .id
+
+            _ ->
+                []
+
+    payloadFieldNames =
+        commaSeparated "" payloadFields \_, i ->
+            n = Num.toStr i
+            "f\(n)"
+
+    constructorArguments =
+        commaSeparated "" payloadFields \id, i ->
+            n = Num.toStr i
+            type = typeName types id
+            "f\(n): \(type)"
+
+    debugFields =
+        payloadFields
+        |> List.mapWithIndex \_, i ->
+            n = Num.toStr i
+            ".field(&node.f\(n))"
+        |> Str.joinWith ""
+
+    buf1 = buf |> generateDiscriminant types discriminantName [tagName] discriminantSize
+
+    """
+    \(buf1)
+
+    #[repr(transparent)]
+    #[derive(Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
+    pub struct \(escapedName)(roc_std::RocBox<\(name)_\(tagName)>);
+
+    impl \(escapedName) {
+        pub fn \(tagName)(\(constructorArguments)) -> Self {
+            let payload = \(name)_\(tagName) { \(payloadFieldNames) };
+
+            Self(roc_std::RocBox::new(payload))
+        }
+    }
+
+    impl core::fmt::Debug for \(escapedName) {
+        fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
+            let node = &self.0;
+            f.debug_tuple("\(escapedName)::\(tagName)")\(debugFields).finish()
+        }
+    }
+    """
+
+generateRecursiveTagUnion = \buf, types, id, tagUnionName, tags, discriminantSize, _discriminantOffset, nullTagIndex ->
+    escapedName = escapeKW tagUnionName
+    discriminantName = "discriminant_\(escapedName)"
+    tagNames = List.map tags \{ name: n } -> n
+    # self = "(&*self.union_pointer())"
+    # selfMut = "(&mut *self.union_pointer())"
+    # other = "(&*other.union_pointer())"
+    unionName = "union_\(escapedName)"
+
+    discriminants =
+        tagNames
+        |> Str.joinWith ", "
+        |> \b -> "[ \(b) ]"
+
+    nullTagId =
+        when nullTagIndex is
+            Some index ->
+                n = Num.toStr index
+                "discriminants[\(n)]"
+
+            None ->
+                """
+                unreachable!("this pointer cannot be NULL")
+                """
+
+    isFunction = \{ name: tagName, payload: optPayload }, index ->
+        payloadFields =
+            when optPayload is
+                Some payload ->
+                    when Types.shape types payload is
+                        TagUnionPayload { fields } ->
+                            when fields is
+                                HasNoClosure xs -> List.map xs .id
+                                HasClosure xs -> List.map xs .id
+
+                        _ ->
+                            []
+
+                None ->
+                    []
+
+        fieldGetters =
+            List.walk payloadFields { i: 0, accum: "" } \{ i, accum }, fieldTypeId ->
+                fieldTypeName = typeName types fieldTypeId
+                fieldIndex = Num.toStr i
+
+                {
+                    i: i + 1,
+                    accum:
+                    """
+                    \(accum)
+                        pub fn get_\(tagName)_f\(fieldIndex)(&self) -> &\(fieldTypeName) {
+                            debug_assert!(self.is_\(tagName)());
+
+                            // extern "C" {
+                            //     fn foobar(tag_id: u16, field_index: usize) -> usize;
+                            // }
+
+                            // let offset = unsafe { foobar(\(fieldIndex)) };
+                            let offset = 0;
+                            unsafe { &*self.unmasked_pointer().add(offset).cast() }
+                        }
+
+                    """,
+                }
+            |> .accum
+
+        payloadFieldNames =
+            commaSeparated "" payloadFields \_, i ->
+                n = Num.toStr i
+                "f\(n)"
+
+        constructorArguments =
+            commaSeparated "" payloadFields \payloadId, i ->
+                n = Num.toStr i
+                type = typeName types payloadId
+                "f\(n): \(type)"
+
+        fixManuallyDrop =
+            when optPayload is
+                Some payload ->
+                    shape = Types.shape types payload
+
+                    if canDeriveCopy types shape then
+                        "payload"
+                    else
+                        "core::mem::ManuallyDrop::new(payload)"
+
+                None ->
+                    "payload"
+
+        if Some (Num.intCast index) == nullTagIndex then
+            """
+                pub fn is_\(tagName)(&self) -> bool {
+                    matches!(self.discriminant(), discriminant_\(escapedName)::\(tagName))
+                }
+
+                pub fn \(tagName)(\(constructorArguments)) -> Self {
+                    Self(std::ptr::null_mut())
+                }
+            """
+        else
+            """
+                pub fn is_\(tagName)(&self) -> bool {
+                    matches!(self.discriminant(), discriminant_\(escapedName)::\(tagName))
+                }
+
+                pub fn \(tagName)(\(constructorArguments)) -> Self {
+                    let tag_id = discriminant_\(escapedName)::\(tagName);
+
+                    let payload = \(escapedName)_\(tagName) { \(payloadFieldNames) } ;
+
+                    let union_payload = union_\(escapedName) { \(tagName): \(fixManuallyDrop) };
+
+                    let ptr = unsafe { roc_std::RocBox::leak(roc_std::RocBox::new(union_payload)) };
+
+                    Self((ptr as usize | tag_id as usize) as *mut _)
+                }
+            \(fieldGetters)
+
+                pub fn get_\(tagName)(mut self) -> \(escapedName)_\(tagName) {
+                    debug_assert!(self.is_\(tagName)());
+
+                    unsafe { core::mem::ManuallyDrop::take(&mut self.ptr_read_union().\(tagName)) }
+                }
+            """
+
+    constructors =
+        tags
+        |> List.mapWithIndex isFunction
+        |> Str.joinWith "\n\n"
+
+    cloneCase = \{ name: tagName }, index ->
+        if Some (Num.intCast index) == nullTagIndex then
+            """
+                        \(tagName) => Self::\(tagName)(),
+            """
+        else
+            """
+                        \(tagName) => {
+                            let tag_id = discriminant_\(escapedName)::\(tagName);
+
+                            let payload_union = unsafe { self.ptr_read_union() };
+                            let payload = union_\(escapedName) {
+                                \(tagName): unsafe { payload_union.\(tagName).clone() },
+                            };
+
+                            let ptr = unsafe { roc_std::RocBox::leak(roc_std::RocBox::new(payload)) };
+
+                            Self((ptr as usize | tag_id as usize) as *mut _)
+                        },
+            """
+
+    cloneCases =
+        tags
+        |> List.mapWithIndex cloneCase
+        |> Str.joinWith "\n"
+
+    partialEqCase = \{ name: tagName }, index ->
+        if Some (Num.intCast index) == nullTagIndex then
+            """
+                        \(tagName) => true,
+            """
+        else
+            """
+                        \(tagName) => {
+                            let payload_union1 = unsafe { self.ptr_read_union() };
+                            let payload_union2 = unsafe { other.ptr_read_union() };
+
+                            unsafe {
+                                payload_union1.\(tagName) == payload_union2.\(tagName)
+                            }
+                        },
+            """
+
+    partialEqCases =
+        tags
+        |> List.mapWithIndex partialEqCase
+        |> Str.joinWith "\n"
+
+    partialEqImpl =
+        if canSupportPartialEqOrd types (Types.shape types id) then
+            """
+            impl PartialEq for \(escapedName) {
+                fn eq(&self, other: &Self) -> bool {
+                    use discriminant_\(escapedName)::*;
+
+                    if self.discriminant() != other.discriminant() {
+                        return false;
+                    }
+
+                    match self.discriminant() {
+                        \(partialEqCases)
+                    }
+                }
+            }
+
+            impl Eq for \(escapedName) {}
+            """
+        else
+            ""
+
+    debugCase = \{ name: tagName, payload: optPayload }, index ->
+        if Some (Num.intCast index) == nullTagIndex then
+            """
+                        \(tagName) => f.debug_tuple("\(escapedName)::\(tagName)").finish(),
+            """
+        else
+            payloadFields =
+                when optPayload is
+                    Some payload ->
+                        when Types.shape types payload is
+                            TagUnionPayload { fields } ->
+                                when fields is
+                                    HasNoClosure xs -> List.map xs .id
+                                    HasClosure xs -> List.map xs .id
+
+                            _ ->
+                                []
+
+                    None ->
+                        []
+
+            debugFields =
+                payloadFields
+                |> List.mapWithIndex \_, i ->
+                    n = Num.toStr i
+                    ".field(&payload_union.\(tagName).f\(n))"
+                |> Str.joinWith ""
+
+            """
+                        \(tagName) => {
+                            let payload_union = unsafe { self.ptr_read_union() };
+
+                            unsafe {
+                                f.debug_tuple("\(escapedName)::\(tagName)")\(debugFields).finish()
+                            }
+                        },
+            """
+
+    debugCases =
+        tags
+        |> List.mapWithIndex debugCase
+        |> Str.joinWith "\n"
+
+    hashCase = \{ name: tagName }, index ->
+        if Some (Num.intCast index) == nullTagIndex then
+            """
+                        \(tagName) => {}
+            """
+        else
+            """
+                        \(tagName) => {
+                            let payload_union = unsafe { self.ptr_read_union() };
+                            unsafe { payload_union.\(tagName).hash(state) };
+                        },
+            """
+
+    hashCases =
+        tags
+        |> List.mapWithIndex hashCase
+        |> Str.joinWith "\n"
+
+    hashImpl =
+        if canSupportPartialEqOrd types (Types.shape types id) then
+            """
+            impl core::hash::Hash for \(escapedName) {
+                fn hash(&self, state: &mut H) {
+                    use discriminant_\(escapedName)::*;
+
+                    self.discriminant().hash(state);
+
+                    match self.discriminant() {
+                        \(hashCases)
+                    }
+                }
+            }
+            """
+        else
+            ""
+
+    partialOrdCase = \{ name: tagName }, index ->
+        if Some (Num.intCast index) == nullTagIndex then
+            """
+                        \(tagName) => std::cmp::Ordering::Equal,
+            """
+        else
+            """
+                        \(tagName) => {
+                            let payload_union1 = unsafe { self.ptr_read_union() };
+                            let payload_union2 = unsafe { other.ptr_read_union() };
+
+                            unsafe {
+                                payload_union1.\(tagName).cmp(&payload_union2.\(tagName))
+                            }
+                        },
+            """
+
+    partialOrdCases =
+        tags
+        |> List.mapWithIndex partialOrdCase
+        |> Str.joinWith "\n"
+
+    partialOrdImpl =
+        if canSupportPartialEqOrd types (Types.shape types id) then
+            """
+            impl PartialOrd for \(escapedName) {
+                fn partial_cmp(&self, other: &Self) -> Option {
+                    Some(::cmp(self, other))
+                }
+            }
+
+            impl Ord for \(escapedName) {
+                fn cmp(&self, other: &Self) -> std::cmp::Ordering {
+                    use discriminant_\(escapedName)::*;
+
+                    use std::cmp::Ordering::*;
+
+                    match self.discriminant().cmp(&other.discriminant()) {
+                        Less => Less,
+                        Greater => Greater,
+                        Equal => unsafe {
+                            match self.discriminant() {
+                                \(partialOrdCases)
+                            }
+                        },
+                    }
+                }
+            }
+            """
+        else
+            ""
+
+    sizeOfSelf = Num.toStr (Types.size types id)
+    alignOfSelf = Num.toStr (Types.alignment types id)
+
+    buf
+    |> generateDiscriminant types discriminantName tagNames discriminantSize
+    |> Str.concat
+        """
+        #[repr(transparent)]
+        pub struct \(escapedName)(*mut \(unionName));
+
+        const _SIZE_CHECK_\(escapedName): () = assert!(core::mem::size_of::<\(escapedName)>() == \(sizeOfSelf));
+        const _ALIGN_CHECK_\(escapedName): () = assert!(core::mem::align_of::<\(escapedName)>() == \(alignOfSelf));
+
+        impl \(escapedName) {
+            pub fn discriminant(&self) -> discriminant_\(escapedName) {
+                let discriminants = {
+                    use \(discriminantName)::*;
+
+                    \(discriminants)
+                };
+
+                if self.0.is_null() {
+                    \(nullTagId)
+                } else  {
+                    match std::mem::size_of::() {
+                        4 => discriminants[self.0 as usize & 0b011],
+                        8 => discriminants[self.0 as usize & 0b111],
+                        _ => unreachable!(),
+                    }
+                }
+            }
+
+            fn unmasked_pointer(&self) -> *mut union_\(escapedName) {
+                debug_assert!(!self.0.is_null());
+
+                let mask = match std::mem::size_of::() {
+                    4 => !0b011usize,
+                    8 => !0b111usize,
+                    _ => unreachable!(),
+                };
+
+                ((self.0 as usize) & mask) as *mut union_\(escapedName)
+            }
+
+            unsafe fn ptr_read_union(&self) -> core::mem::ManuallyDrop {
+                let ptr = self.unmasked_pointer();
+
+                core::mem::ManuallyDrop::new(unsafe { std::ptr::read(ptr) })
+            }
+
+            \(constructors)
+        }
+
+        impl Clone for \(escapedName) {
+            fn clone(&self) -> Self {
+                use discriminant_\(escapedName)::*;
+
+                let discriminant = self.discriminant();
+
+                match discriminant {
+                \(cloneCases)
+                }
+            }
+        }
+
+        \(partialEqImpl)
+
+        \(hashImpl)
+
+        \(partialOrdImpl)
+
+
+        impl core::fmt::Debug for \(escapedName) {
+            fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
+                use discriminant_\(escapedName)::*;
+
+                match self.discriminant() {
+                    \(debugCases)
+                }
+            }
+        }
+
+
+        #[repr(C)]
+        union \(unionName) {
+        """
+    |> \b -> List.walk tags b (generateUnionField types)
+    |> generateTagUnionSizer types id tags
+    |> Str.concat "}\n\n"
+
+generateTagUnionDropPayload = \buf, types, selfMut, tags, discriminantName, discriminantSize, indents ->
+    if discriminantSize == 0 then
+        when List.first tags is
+            Ok { name } ->
+                # There's only one tag, so there's no discriminant and no need to match;
+                # just drop the pointer.
+                buf
+                |> writeIndents indents
+                |> Str.concat "unsafe { core::mem::ManuallyDrop::drop(&mut core::ptr::read(self.pointer).\(name)); }"
+
+            Err ListWasEmpty ->
+                crash "unreachable"
+    else
+        buf
+        |> writeTagImpls tags discriminantName indents \name, payload ->
+            when payload is
+                Some id if cannotSupportCopy types (Types.shape types id) ->
+                    "unsafe { core::mem::ManuallyDrop::drop(&mut \(selfMut).payload.\(name)) },"
+
+                _ ->
+                    # If it had no payload, or if the payload had no pointers,
+                    # there's nothing to clean up, so do `=> {}` for the branch.
+                    "{}"
+
+writeIndents = \buf, indents ->
+    if indents <= 0 then
+        buf
+    else
+        buf
+        |> Str.concat indent
+        |> writeIndents (indents - 1)
+
+writeTagImpls = \buf, tags, discriminantName, indents, f ->
+    buf
+    |> writeIndents indents
+    |> Str.concat "match self.discriminant() {\n"
+    |> \b -> List.walk tags b \accum, { name, payload } ->
+            branchStr = f name payload
+            accum
+            |> writeIndents (indents + 1)
+            |> Str.concat "\(discriminantName)::\(name) => \(branchStr)\n"
+    |> writeIndents indents
+    |> Str.concat "}\n"
+
+generateTagUnionSizer : Str, Types, TypeId, _ -> Str
+generateTagUnionSizer = \buf, types, id, tags ->
+    if List.len tags > 1 then
+        # When there's a discriminant (so, multiple tags) and there is
+        # no alignment padding after the largest variant,
+        # the compiler will make extra room for the discriminant.
+        # We need that to be reflected in the overall size of the enum,
+        # so add an extra variant with the appropriate size.
+        #
+        # (Do this even if theoretically shouldn't be necessary, since
+        # there's no runtime cost and it more explicitly syncs the
+        # union's size with what we think it should be.)
+        size = getSizeRoundedToAlignment types id
+        sizeStr = Num.toStr size
+
+        Str.concat buf "\(indent)_sizer: [u8; \(sizeStr)],\n"
+    else
+        buf
+
+generateDiscriminant = \buf, types, name, tags, size ->
+    if size > 0 then
+        enumType =
+            TagUnion
+                (
+                    Enumeration {
+                        name,
+                        tags,
+                        size,
+                    }
+                )
+
+        buf
+        |> generateEnumeration types enumType name tags size
+    else
+        buf
+
+generateUnionField = \types ->
+    \accum, { name: fieldName, payload } ->
+        escapedFieldName = escapeKW fieldName
+
+        when payload is
+            Some id ->
+                typeStr = typeName types id
+
+                type = Types.shape types id
+                fullTypeStr =
+                    if cannotSupportCopy types type then
+                        # types with pointers need ManuallyDrop
+                        # because rust unions don't (and can't)
+                        # know how to drop them automatically!
+                        "core::mem::ManuallyDrop<\(typeStr)>"
+                    else
+                        typeStr
+
+                Str.concat accum "\(indent)\(escapedFieldName): \(fullTypeStr),\n"
+
+            None ->
+                # use unit as the payload
+                Str.concat accum "\(indent)\(escapedFieldName): (),\n"
+
+commaSeparated : Str, List a, (a, Nat -> Str) -> Str
+commaSeparated = \buf, items, step ->
+    length = List.len items
+    List.walk items { buf, count: 0 } \accum, item ->
+        if accum.count + 1 == length then
+            { buf: Str.concat accum.buf (step item accum.count), count: length }
+        else
+            { buf: Str.concat accum.buf (step item accum.count) |> Str.concat ", ", count: accum.count + 1 }
+    |> .buf
+
+generateNullableUnwrapped : Str, Types, TypeId, Str, Str, Str, TypeId, [FirstTagIsNull, SecondTagIsNull] -> Str
+generateNullableUnwrapped = \buf, types, tagUnionid, name, nullTag, nonNullTag, nonNullPayload, whichTagIsNull ->
+    payloadFields =
+        when Types.shape types nonNullPayload is
+            TagUnionPayload { fields } ->
+                when fields is
+                    HasNoClosure xs -> List.map xs .id
+                    HasClosure xs -> List.map xs .id
+
+            _ ->
+                []
+
+    payloadFieldNames =
+        commaSeparated "" payloadFields \_, i ->
+            n = Num.toStr i
+            "f\(n)"
+
+    constructorArguments =
+        commaSeparated "" payloadFields \id, i ->
+            n = Num.toStr i
+            type = typeName types id
+            "f\(n): \(type)"
+
+    debugFields =
+        payloadFields
+        |> List.mapWithIndex \_, i ->
+            n = Num.toStr i
+            ".field(&node.f\(n))"
+        |> Str.joinWith ""
+
+    discriminant =
+        when whichTagIsNull is
+            FirstTagIsNull ->
+                """
+                #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
+                pub enum discriminant_\(name) {
+                    \(nullTag) = 0,
+                    \(nonNullTag) = 1,
+                }
+                """
+
+            SecondTagIsNull ->
+                """
+                #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
+                pub enum discriminant_\(name) {
+                    \(nonNullTag) = 0,
+                    \(nullTag) = 1,
+                }
+                """
+
+    sizeOfSelf = Num.toStr (Types.size types tagUnionid)
+    alignOfSelf = Num.toStr (Types.alignment types tagUnionid)
+
+    """
+    \(buf)
+
+    #[derive(PartialOrd, Ord)]
+    #[repr(C)]
+    pub struct \(name)(*mut \(name)_\(nonNullTag));
+
+    \(discriminant)
+
+    const _SIZE_CHECK_\(name): () = assert!(core::mem::size_of::<\(name)>() == \(sizeOfSelf));
+    const _ALIGN_CHECK_\(name): () = assert!(core::mem::align_of::<\(name)>() == \(alignOfSelf));
+
+    impl \(name) {
+        pub fn \(nullTag)() -> Self {
+            Self(core::ptr::null_mut())
+        }
+
+        pub fn \(nonNullTag)(\(constructorArguments)) -> Self {
+            let payload = \(name)_\(nonNullTag) { \(payloadFieldNames) };
+
+            let ptr = unsafe { roc_std::RocBox::leak(roc_std::RocBox::new(payload)) };
+
+            Self(ptr)
+        }
+
+        pub fn discriminant(&self) -> discriminant_\(name) {
+            if self.is_\(nullTag)() {
+                discriminant_\(name)::\(nullTag)
+            } else {
+                discriminant_\(name)::\(nonNullTag)
+            }
+        }
+
+        pub fn is_\(nullTag)(&self) -> bool {
+            self.0.is_null()
+        }
+
+        pub fn is_\(nonNullTag)(&self) -> bool {
+            !self.0.is_null()
+        }
+    }
+
+    impl core::fmt::Debug for \(name) {
+        fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
+            if self.is_\(nullTag)() {
+                f.debug_tuple("\(name)::\(nullTag)").finish()
+            } else {
+                let node = core::mem::ManuallyDrop::new(unsafe { std::ptr::read(self.0) });
+                f.debug_tuple("\(name)::\(nonNullTag)")\(debugFields).finish()
+            }
+        }
+    }
+
+    impl Clone for \(name) {
+        fn clone(&self) -> Self {
+            if self.is_\(nullTag)() {
+                Self::\(nullTag)()
+            } else {
+                use std::ops::Deref;
+
+                let node_ref = core::mem::ManuallyDrop::new(unsafe { std::ptr::read(self.0) });
+                let payload : \(name)_\(nonNullTag) = (node_ref.deref()).clone();
+
+                let ptr = unsafe { roc_std::RocBox::leak(roc_std::RocBox::new(payload)) };
+
+                Self(ptr)
+            }
+        }
+    }
+
+    impl PartialEq for \(name) {
+        fn eq(&self, other: &Self) -> bool {
+            if self.discriminant() != other.discriminant() {
+                return false;
+            }
+
+            if self.is_\(nullTag)() {
+                return true;
+            }
+
+            let payload1 = core::mem::ManuallyDrop::new(unsafe { std::ptr::read(self.0) });
+            let payload2 = core::mem::ManuallyDrop::new(unsafe { std::ptr::read(other.0) });
+
+            payload1 == payload2
+        }
+    }
+
+    impl Eq for \(name) {}
+
+    impl core::hash::Hash for \(name) {
+        fn hash(&self, state: &mut H) {
+            self.discriminant().hash(state);
+
+            if self.is_\(nonNullTag)() {
+                let payload = core::mem::ManuallyDrop::new(unsafe { std::ptr::read(self.0) });
+                payload.hash(state);
+            }
+        }
+    }
+    """
+
+generateSingleTagStruct = \buf, types, name, tagName, payload ->
+    # Store single-tag unions as structs rather than enums,
+    # because they have only one alternative. However, still
+    # offer the usual tag union APIs.
+    escapedName = escapeKW name
+    repr =
+        length =
+            when payload is
+                HasClosure fields -> List.len fields
+                HasNoClosure fields -> List.len fields
+        if length <= 1 then
+            "transparent"
+        else
+            "C"
+
+    when payload is
+        HasNoClosure fields ->
+            asStructFields =
+                List.mapWithIndex fields \{ id }, index ->
+                    indexStr = Num.toStr index
+
+                    { name: "f\(indexStr)", id }
+                |> HasNoClosure
+            asStructType =
+                Struct {
+                    name,
+                    fields: asStructFields,
+                }
+
+            buf
+            |> generateDeriveStr types asStructType ExcludeDebug
+            |> Str.concat "#[repr(\(repr))]\npub struct \(escapedName) "
+            |> \b ->
+                if List.isEmpty fields then
+                    generateZeroElementSingleTagStruct b escapedName tagName
+                else
+                    generateMultiElementSingleTagStruct b types escapedName tagName fields asStructFields
+
+        HasClosure _ ->
+            Str.concat buf "\\TODO: SingleTagStruct with closures"
+
+generateMultiElementSingleTagStruct = \buf, types, name, tagName, payloadFields, asStructFields ->
+    buf
+    |> Str.concat "{\n"
+    |> generateStructFields types Private asStructFields
+    |> Str.concat "}\n\n"
+    |> Str.concat
+        """
+        impl \(name) {
+
+        """
+    |> \b ->
+        fieldTypes =
+            payloadFields
+            |> List.map \{ id } ->
+                typeName types id
+        args =
+            fieldTypes
+            |> List.mapWithIndex \fieldTypeName, index ->
+                indexStr = Num.toStr index
+
+                "f\(indexStr): \(fieldTypeName)"
+        fields =
+            payloadFields
+            |> List.mapWithIndex \_, index ->
+                indexStr = Num.toStr index
+
+                "f\(indexStr)"
+
+        fieldAccesses =
+            fields
+            |> List.map \field ->
+                "self.\(field)"
+
+        {
+            b,
+            args,
+            fields,
+            fieldTypes,
+            fieldAccesses,
+        }
+    |> \{ b, args, fields, fieldTypes, fieldAccesses } ->
+        argsStr = Str.joinWith args ", "
+        fieldsStr = Str.joinWith fields "\n\(indent)\(indent)\(indent)"
+
+        {
+            b: Str.concat
+                b
+                """
+                \(indent)/// A tag named ``\(tagName)``, with the given payload.
+                \(indent)pub fn \(tagName)(\(argsStr)) -> Self {
+                \(indent)    Self {
+                \(indent)        \(fieldsStr)
+                \(indent)    }
+                \(indent)}
+
+
+                """,
+            fieldTypes,
+            fieldAccesses,
+        }
+    |> \{ b, fieldTypes, fieldAccesses } ->
+        retType = asRustTuple fieldTypes
+        retExpr = asRustTuple fieldAccesses
+
+        {
+            b: Str.concat
+                b
+                """
+                \(indent)/// Since `\(name)` only has one tag (namely, `\(tagName)`),
+                \(indent)/// convert it to `\(tagName)`'s payload.
+                \(indent)pub fn into_\(tagName)(self) -> \(retType) {
+                \(indent)    \(retExpr)
+                \(indent)}
+
+
+                """,
+            fieldTypes,
+            fieldAccesses,
+        }
+    |> \{ b, fieldTypes, fieldAccesses } ->
+        retType =
+            fieldTypes
+            |> List.map \ft -> "&\(ft)"
+            |> asRustTuple
+        retExpr =
+            fieldAccesses
+            |> List.map \fa -> "&\(fa)"
+            |> asRustTuple
+
+        Str.concat
+            b
+            """
+            \(indent)/// Since `\(name)` only has one tag (namely, `\(tagName)`),
+            \(indent)/// convert it to `\(tagName)`'s payload.
+            \(indent)pub fn as_\(tagName)(&self) -> \(retType) {
+            \(indent)    \(retExpr)
+            \(indent)}
+
+            """
+    |> Str.concat
+        """
+        }
+
+
+        impl core::fmt::Debug for \(name) {
+            fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
+                f.debug_tuple("\(name)::\(tagName)")
+
+        """
+    |> \b ->
+        payloadFields
+        |> List.mapWithIndex \_, index ->
+            indexStr = Num.toStr index
+
+            "\(indent)\(indent)\(indent)\(indent).field(&self.f\(indexStr))\n"
+        |> List.walk b Str.concat
+    |> Str.concat
+        """
+                        .finish()
+            }
+        }
+
+
+        """
+
+asRustTuple = \list ->
+    # If there is 1 element in the list we just return it
+    # Otherwise, we make a proper tuple string.
+    joined = Str.joinWith list ", "
+
+    if List.len list == 1 then
+        joined
+    else
+        "(\(joined))"
+
+generateZeroElementSingleTagStruct = \buf, name, tagName ->
+    # A single tag with no payload is a zero-sized unit type, so
+    # represent it as a zero-sized struct (e.g. "struct Foo()").
+    buf
+    |> Str.concat "();\n\n"
+    |> Str.concat
+        """
+        impl \(name) {
+            /// A tag named \(tagName), which has no payload.
+            pub const \(tagName): Self = Self();
+
+            /// Other `into_` methods return a payload, but since \(tagName) tag
+            /// has no payload, this does nothing and is only here for completeness.
+            pub fn into_\(tagName)(self) {
+                ()
+            }
+
+            /// Other `as_` methods return a payload, but since \(tagName) tag
+            /// has no payload, this does nothing and is only here for completeness.
+            pub fn as_\(tagName)(&self) {
+                ()
+            }
+        }
+
+        impl core::fmt::Debug for \(name) {
+            fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
+                f.write_str("\(name)::\(tagName)")
+            }
+        }
+
+
+        """
+
+generateDeriveStr = \buf, types, type, includeDebug ->
+    condWrite = \b, cond, str ->
+        if cond then
+            Str.concat b str
+        else
+            b
+
+    deriveDebug =
+        when includeDebug is
+            IncludeDebug -> Bool.true
+            ExcludeDebug -> Bool.false
+
+    buf
+    |> Str.concat "#[derive(Clone, "
+    |> condWrite (!(cannotSupportCopy types type)) "Copy, "
+    |> condWrite (!(cannotSupportDefault types type)) "Default, "
+    |> condWrite deriveDebug "Debug, "
+    |> condWrite (canSupportPartialEqOrd types type) "PartialEq, PartialOrd, "
+    |> condWrite (canSupportEqHashOrd types type) "Eq, Ord, Hash, "
+    |> Str.concat ")]\n"
+
+canSupportEqHashOrd : Types, Shape -> Bool
+canSupportEqHashOrd = \types, type ->
+    !(hasFloat types type) && (canSupportPartialEqOrd types type)
+
+canSupportPartialEqOrd : Types, Shape -> Bool
+canSupportPartialEqOrd = \types, type ->
+    when type is
+        Function rocFn ->
+            runtimeRepresentation = Types.shape types rocFn.lambdaSet
+            canSupportPartialEqOrd types runtimeRepresentation
+
+        Unsized -> Bool.false
+        Unit | EmptyTagUnion | Bool | Num _ | TagUnion (Enumeration _) -> Bool.true
+        RocStr -> Bool.true
+        RocList inner | RocSet inner | RocBox inner ->
+            innerType = Types.shape types inner
+            canSupportPartialEqOrd types innerType
+
+        RocDict k v ->
+            kType = Types.shape types k
+            vType = Types.shape types v
+
+            canSupportPartialEqOrd types kType && canSupportPartialEqOrd types vType
+
+        TagUnion (Recursive { tags }) ->
+            List.all tags \{ payload } ->
+                when payload is
+                    None -> Bool.true
+                    Some id -> canSupportPartialEqOrd types (Types.shape types id)
+
+        TagUnion (NullableWrapped { tags }) ->
+            List.all tags \{ payload } ->
+                when payload is
+                    None -> Bool.true
+                    Some id -> canSupportPartialEqOrd types (Types.shape types id)
+
+        TagUnion (NonNullableUnwrapped { payload }) ->
+            canSupportPartialEqOrd types (Types.shape types payload)
+
+        TagUnion (NullableUnwrapped { nonNullPayload }) ->
+            canSupportPartialEqOrd types (Types.shape types nonNullPayload)
+
+        RecursivePointer _ -> Bool.true
+        TagUnion (SingleTagStruct { payload: HasNoClosure fields }) ->
+            List.all fields \{ id } -> canSupportPartialEqOrd types (Types.shape types id)
+
+        TagUnion (SingleTagStruct { payload: HasClosure _ }) ->
+            Bool.false
+
+        TagUnion (NonRecursive { tags }) ->
+            List.all tags \{ payload } ->
+                when payload is
+                    Some id -> canSupportPartialEqOrd types (Types.shape types id)
+                    None -> Bool.true
+
+        RocResult okId errId ->
+            okShape = Types.shape types okId
+            errShape = Types.shape types errId
+
+            canSupportPartialEqOrd types okShape && canSupportPartialEqOrd types errShape
+
+        Struct { fields: HasNoClosure fields } | TagUnionPayload { fields: HasNoClosure fields } ->
+            List.all fields \{ id } -> canSupportPartialEqOrd types (Types.shape types id)
+
+        Struct { fields: HasClosure fields } | TagUnionPayload { fields: HasClosure fields } ->
+            List.all fields \{ id } -> canSupportPartialEqOrd types (Types.shape types id)
+
+cannotSupportCopy : Types, Shape -> Bool
+cannotSupportCopy = \types, type ->
+    !(canDeriveCopy types type)
+
+canDeriveCopy : Types, Shape -> Bool
+canDeriveCopy = \types, type ->
+    when type is
+        Function rocFn ->
+            runtimeRepresentation = Types.shape types rocFn.lambdaSet
+            canDeriveCopy types runtimeRepresentation
+
+        # unsized values are heap-allocated
+        Unsized -> Bool.false
+        Unit | EmptyTagUnion | Bool | Num _ | TagUnion (Enumeration _) -> Bool.true
+        RocStr | RocList _ | RocDict _ _ | RocSet _ | RocBox _ | TagUnion (NullableUnwrapped _) | TagUnion (NullableWrapped _) | TagUnion (Recursive _) | TagUnion (NonNullableUnwrapped _) | RecursivePointer _ -> Bool.false
+        TagUnion (SingleTagStruct { payload: HasNoClosure fields }) ->
+            List.all fields \{ id } -> canDeriveCopy types (Types.shape types id)
+
+        TagUnion (SingleTagStruct { payload: HasClosure fields }) ->
+            List.all fields \{ id } -> canDeriveCopy types (Types.shape types id)
+
+        TagUnion (NonRecursive { tags }) ->
+            List.all tags \{ payload } ->
+                when payload is
+                    Some id -> canDeriveCopy types (Types.shape types id)
+                    None -> Bool.true
+
+        RocResult okId errId ->
+            canDeriveCopy types (Types.shape types okId)
+            && canDeriveCopy types (Types.shape types errId)
+
+        Struct { fields: HasNoClosure fields } | TagUnionPayload { fields: HasNoClosure fields } ->
+            List.all fields \{ id } -> canDeriveCopy types (Types.shape types id)
+
+        Struct { fields: HasClosure fields } | TagUnionPayload { fields: HasClosure fields } ->
+            List.all fields \{ id } -> canDeriveCopy types (Types.shape types id)
+
+cannotSupportDefault = \types, type ->
+    when type is
+        Unit | Unsized | EmptyTagUnion | TagUnion _ | RocResult _ _ | RecursivePointer _ | Function _ -> Bool.true
+        RocStr | Bool | Num _ -> Bool.false
+        RocList id | RocSet id | RocBox id ->
+            cannotSupportDefault types (Types.shape types id)
+
+        TagUnionPayload { fields: HasClosure _ } -> Bool.true
+        RocDict keyId valId ->
+            cannotSupportCopy types (Types.shape types keyId)
+            || cannotSupportCopy types (Types.shape types valId)
+
+        Struct { fields: HasClosure _ } -> Bool.true
+        Struct { fields: HasNoClosure fields } | TagUnionPayload { fields: HasNoClosure fields } ->
+            List.any fields \{ id } -> cannotSupportDefault types (Types.shape types id)
+
+hasFloat = \types, type ->
+    hasFloatHelp types type (Set.empty {})
+
+hasFloatHelp = \types, type, doNotRecurse ->
+    # TODO: is doNotRecurse problematic? Do we need an updated doNotRecurse for calls up the tree?
+    # I think there is a change it really only matters for RecursivePointer, so it may be fine.
+    # Otherwise we need to deal with threading through updates to doNotRecurse
+    when type is
+        Num kind ->
+            when kind is
+                F32 | F64 -> Bool.true
+                _ -> Bool.false
+
+        Unit | Unsized | EmptyTagUnion | RocStr | Bool | TagUnion (Enumeration _) | Function _ -> Bool.false
+        RocList id | RocSet id | RocBox id ->
+            hasFloatHelp types (Types.shape types id) doNotRecurse
+
+        RocDict id0 id1 | RocResult id0 id1 ->
+            hasFloatHelp types (Types.shape types id0) doNotRecurse
+            || hasFloatHelp types (Types.shape types id1) doNotRecurse
+
+        Struct { fields: HasNoClosure fields } | TagUnionPayload { fields: HasNoClosure fields } ->
+            List.any fields \{ id } -> hasFloatHelp types (Types.shape types id) doNotRecurse
+
+        Struct { fields: HasClosure fields } | TagUnionPayload { fields: HasClosure fields } ->
+            List.any fields \{ id } -> hasFloatHelp types (Types.shape types id) doNotRecurse
+
+        TagUnion (SingleTagStruct { payload: HasNoClosure fields }) ->
+            List.any fields \{ id } -> hasFloatHelp types (Types.shape types id) doNotRecurse
+
+        TagUnion (SingleTagStruct { payload: HasClosure fields }) ->
+            List.any fields \{ id } -> hasFloatHelp types (Types.shape types id) doNotRecurse
+
+        TagUnion (Recursive { tags }) ->
+            List.any tags \{ payload } ->
+                when payload is
+                    Some id -> hasFloatHelp types (Types.shape types id) doNotRecurse
+                    None -> Bool.false
+
+        TagUnion (NonRecursive { tags }) ->
+            List.any tags \{ payload } ->
+                when payload is
+                    Some id -> hasFloatHelp types (Types.shape types id) doNotRecurse
+                    None -> Bool.false
+
+        TagUnion (NullableWrapped { tags }) ->
+            List.any tags \{ payload } ->
+                when payload is
+                    Some id -> hasFloatHelp types (Types.shape types id) doNotRecurse
+                    None -> Bool.false
+
+        TagUnion (NonNullableUnwrapped { payload }) ->
+            if Set.contains doNotRecurse payload then
+                Bool.false
+            else
+                nextDoNotRecurse = Set.insert doNotRecurse payload
+
+                hasFloatHelp types (Types.shape types payload) nextDoNotRecurse
+
+        TagUnion (NullableUnwrapped { nonNullPayload }) ->
+            if Set.contains doNotRecurse nonNullPayload then
+                Bool.false
+            else
+                nextDoNotRecurse = Set.insert doNotRecurse nonNullPayload
+
+                hasFloatHelp types (Types.shape types nonNullPayload) nextDoNotRecurse
+
+        RecursivePointer payload ->
+            if Set.contains doNotRecurse payload then
+                Bool.false
+            else
+                nextDoNotRecurse = Set.insert doNotRecurse payload
+
+                hasFloatHelp types (Types.shape types payload) nextDoNotRecurse
+
+typeName = \types, id ->
+    when Types.shape types id is
+        Unit -> "()"
+        Unsized -> "roc_std::RocList"
+        EmptyTagUnion -> "std::convert::Infallible"
+        RocStr -> "roc_std::RocStr"
+        Bool -> "bool"
+        Num U8 -> "u8"
+        Num U16 -> "u16"
+        Num U32 -> "u32"
+        Num U64 -> "u64"
+        Num U128 -> "u128"
+        Num I8 -> "i8"
+        Num I16 -> "i16"
+        Num I32 -> "i32"
+        Num I64 -> "i64"
+        Num I128 -> "i128"
+        Num F32 -> "f32"
+        Num F64 -> "f64"
+        Num Dec -> "roc_std:RocDec"
+        RocDict key value ->
+            keyName = typeName types key
+            valueName = typeName types value
+
+            "roc_std::RocDict<\(keyName), \(valueName)>"
+
+        RocSet elem ->
+            elemName = typeName types elem
+
+            "roc_std::RocSet<\(elemName)>"
+
+        RocList elem ->
+            elemName = typeName types elem
+
+            "roc_std::RocList<\(elemName)>"
+
+        RocBox elem ->
+            elemName = typeName types elem
+
+            "roc_std::RocBox<\(elemName)>"
+
+        RocResult ok err ->
+            okName = typeName types ok
+            errName = typeName types err
+
+            "roc_std::RocResult<\(okName), \(errName)>"
+
+        RecursivePointer content ->
+            typeName types content
+
+        Struct { name } -> escapeKW name
+        TagUnionPayload { name } -> escapeKW name
+        TagUnion (NonRecursive { name }) -> escapeKW name
+        TagUnion (Recursive { name }) -> escapeKW name
+        TagUnion (Enumeration { name }) -> escapeKW name
+        TagUnion (NullableWrapped { name }) -> escapeKW name
+        TagUnion (NullableUnwrapped { name }) -> escapeKW name
+        TagUnion (NonNullableUnwrapped { name }) -> escapeKW name
+        TagUnion (SingleTagStruct { name }) -> escapeKW name
+        Function { functionName } -> escapeKW functionName
+
+getSizeRoundedToAlignment = \types, id ->
+    alignment = Types.alignment types id
+
+    Types.size types id
+    |> roundUpToAlignment alignment
+
+roundUpToAlignment = \width, alignment ->
+    when alignment is
+        0 -> width
+        1 -> width
+        _ ->
+            if width % alignment > 0 then
+                width + alignment - (width % alignment)
+            else
+                width
+
+archName = \arch ->
+    when arch is
+        Aarch32 ->
+            "arm"
+
+        Aarch64 ->
+            "aarch64"
+
+        Wasm32 ->
+            "wasm32"
+
+        X86x32 ->
+            "x86"
+
+        X86x64 ->
+            "x86_64"
+
+fileHeader =
+    """
+    // ⚠️ GENERATED CODE ⚠️ - this entire file was generated by the `roc glue` CLI command
+
+    #![allow(unused_unsafe)]
+    #![allow(dead_code)]
+    #![allow(unused_mut)]
+    #![allow(non_snake_case)]
+    #![allow(non_camel_case_types)]
+    #![allow(non_upper_case_globals)]
+    #![allow(clippy::undocumented_unsafe_blocks)]
+    #![allow(clippy::redundant_static_lifetimes)]
+    #![allow(clippy::unused_unit)]
+    #![allow(clippy::missing_safety_doc)]
+    #![allow(clippy::let_and_return)]
+    #![allow(clippy::missing_safety_doc)]
+    #![allow(clippy::redundant_static_lifetimes)]
+    #![allow(clippy::needless_borrow)]
+    #![allow(clippy::clone_on_copy)]
+
+
+
+    """
+
+indent = "    "
+discriminantDocComment = "/// Returns which variant this tag union holds. Note that this never includes a payload!"
+
+reservedKeywords = Set.fromList [
+    "try",
+    "abstract",
+    "become",
+    "box",
+    "do",
+    "final",
+    "macro",
+    "override",
+    "priv",
+    "typeof",
+    "unsized",
+    "virtual",
+    "yield",
+    "async",
+    "await",
+    "dyn",
+    "as",
+    "break",
+    "const",
+    "continue",
+    "crate",
+    "else",
+    "enum",
+    "extern",
+    "false",
+    "fn",
+    "for",
+    "if",
+    "impl",
+    "in",
+    "let",
+    "loop",
+    "match",
+    "mod",
+    "move",
+    "mut",
+    "pub",
+    "ref",
+    "return",
+    "self",
+    "Self",
+    "static",
+    "struct",
+    "super",
+    "trait",
+    "true",
+    "type",
+    "unsafe",
+    "use",
+    "where",
+    "while",
+]
+
+escapeKW = \input ->
+    # use a raw identifier for this, to prevent a syntax error due to using a reserved keyword.
+    # https://doc.rust-lang.org/rust-by-example/compatibility/raw_identifiers.html
+    # another design would be to add an underscore after it; this is an experiment!
+    if Set.contains reservedKeywords input then
+        "r#\(input)"
+    else
+        input
+
+nextMultipleOf = \lhs, rhs ->
+    when lhs % rhs is
+        0 -> lhs
+        r -> lhs + (rhs - r)
+
+isUnit : Shape -> Bool
+isUnit = \shape ->
+    when shape is
+        Unit -> Bool.true
+        _ -> Bool.false
+
+toArgStr : List TypeId, Types, (TypeId, Shape, Nat -> Str) -> Str
+toArgStr = \args, types, fmt ->
+    List.walkWithIndex args "" \state, argId, index ->
+        shape = Types.shape types argId
+
+        # Drop `()` args; they aren't FFI-safe, and nothing will get passed anyway.
+        if isUnit shape then
+            state
+        else
+            argStr = fmt argId shape index
+
+            if Str.isEmpty state then
+                argStr # Don't prepend a comma if this is the first one
+            else
+                state
+                |> Str.concat ", "
+                |> Str.concat argStr
diff --git a/bindgen/src/enums.rs b/crates/glue/src/enums.rs
similarity index 100%
rename from bindgen/src/enums.rs
rename to crates/glue/src/enums.rs
diff --git a/crates/glue/src/glue.rs b/crates/glue/src/glue.rs
new file mode 100644
index 0000000000..325ea22370
--- /dev/null
+++ b/crates/glue/src/glue.rs
@@ -0,0 +1,4088 @@
+// ⚠️ GENERATED CODE ⚠️ - this entire file was generated by the `roc glue` CLI command
+
+#![allow(unused_unsafe)]
+#![allow(dead_code)]
+#![allow(unused_mut)]
+#![allow(non_snake_case)]
+#![allow(non_camel_case_types)]
+#![allow(non_upper_case_globals)]
+#![allow(clippy::undocumented_unsafe_blocks)]
+#![allow(clippy::redundant_static_lifetimes)]
+#![allow(clippy::unused_unit)]
+#![allow(clippy::missing_safety_doc)]
+#![allow(clippy::let_and_return)]
+#![allow(clippy::missing_safety_doc)]
+#![allow(clippy::redundant_static_lifetimes)]
+#![allow(clippy::needless_borrow)]
+#![allow(clippy::clone_on_copy)]
+
+#[cfg(any(
+    target_arch = "arm",
+    target_arch = "wasm32",
+    target_arch = "x86"
+))]
+#[derive(Clone, Debug, Eq, Ord, Hash, PartialEq, PartialOrd)]
+#[repr(C)]
+pub struct Types {
+    pub aligns: roc_std::RocList,
+    pub deps: roc_std::RocDict>,
+    pub sizes: roc_std::RocList,
+    pub types: roc_std::RocList,
+    pub typesByName: roc_std::RocDict,
+    pub target: Target,
+}
+
+#[cfg(any(
+    target_arch = "arm",
+    target_arch = "aarch64",
+    target_arch = "wasm32",
+    target_arch = "x86",
+    target_arch = "x86_64"
+))]
+#[derive(Clone, Copy, Eq, Ord, Hash, PartialEq, PartialOrd)]
+#[repr(u8)]
+pub enum discriminant_RocType {
+    Bool = 0,
+    EmptyTagUnion = 1,
+    Function = 2,
+    Num = 3,
+    RecursivePointer = 4,
+    RocBox = 5,
+    RocDict = 6,
+    RocList = 7,
+    RocResult = 8,
+    RocSet = 9,
+    RocStr = 10,
+    Struct = 11,
+    TagUnion = 12,
+    TagUnionPayload = 13,
+    Unit = 14,
+}
+
+impl core::fmt::Debug for discriminant_RocType {
+    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
+        match self {
+            Self::Bool => f.write_str("discriminant_RocType::Bool"),
+            Self::EmptyTagUnion => f.write_str("discriminant_RocType::EmptyTagUnion"),
+            Self::Function => f.write_str("discriminant_RocType::Function"),
+            Self::Num => f.write_str("discriminant_RocType::Num"),
+            Self::RecursivePointer => f.write_str("discriminant_RocType::RecursivePointer"),
+            Self::RocBox => f.write_str("discriminant_RocType::RocBox"),
+            Self::RocDict => f.write_str("discriminant_RocType::RocDict"),
+            Self::RocList => f.write_str("discriminant_RocType::RocList"),
+            Self::RocResult => f.write_str("discriminant_RocType::RocResult"),
+            Self::RocSet => f.write_str("discriminant_RocType::RocSet"),
+            Self::RocStr => f.write_str("discriminant_RocType::RocStr"),
+            Self::Struct => f.write_str("discriminant_RocType::Struct"),
+            Self::TagUnion => f.write_str("discriminant_RocType::TagUnion"),
+            Self::TagUnionPayload => f.write_str("discriminant_RocType::TagUnionPayload"),
+            Self::Unit => f.write_str("discriminant_RocType::Unit"),
+        }
+    }
+}
+
+#[cfg(any(
+    target_arch = "arm",
+    target_arch = "wasm32",
+    target_arch = "x86"
+))]
+#[repr(C)]
+pub union RocType {
+    Function: core::mem::ManuallyDrop,
+    Num: RocNum,
+    RecursivePointer: u32,
+    RocBox: u32,
+    RocDict: RocType_RocDict,
+    RocList: u32,
+    RocResult: RocType_RocResult,
+    RocSet: u32,
+    Struct: core::mem::ManuallyDrop,
+    TagUnion: core::mem::ManuallyDrop,
+    TagUnionPayload: core::mem::ManuallyDrop,
+    _sizer: [u8; 52],
+}
+
+#[cfg(any(
+    target_arch = "arm",
+    target_arch = "wasm32",
+    target_arch = "x86"
+))]
+#[derive(Clone, Copy, Debug, Default, Eq, Ord, Hash, PartialEq, PartialOrd)]
+#[repr(C)]
+pub struct R15 {
+    pub discriminant: u32,
+    pub r#type: u32,
+}
+
+#[cfg(any(
+    target_arch = "arm",
+    target_arch = "aarch64",
+    target_arch = "wasm32",
+    target_arch = "x86",
+    target_arch = "x86_64"
+))]
+#[derive(Clone, Debug, Eq, Ord, Hash, PartialEq, PartialOrd)]
+#[repr(C)]
+pub struct R12 {
+    pub name: roc_std::RocStr,
+    pub payload: U4,
+}
+
+#[cfg(any(
+    target_arch = "arm",
+    target_arch = "aarch64",
+    target_arch = "wasm32",
+    target_arch = "x86",
+    target_arch = "x86_64"
+))]
+#[derive(Clone, Debug, Eq, Ord, Hash, PartialEq, PartialOrd)]
+#[repr(C)]
+pub struct R10 {
+    pub name: roc_std::RocStr,
+    pub payload: U3,
+}
+
+#[cfg(any(
+    target_arch = "arm",
+    target_arch = "aarch64",
+    target_arch = "wasm32",
+    target_arch = "x86",
+    target_arch = "x86_64"
+))]
+#[derive(Clone, Debug, Eq, Ord, Hash, PartialEq, PartialOrd)]
+#[repr(C)]
+pub struct R7 {
+    pub name: roc_std::RocStr,
+    pub payload: U1,
+}
+
+#[cfg(any(
+    target_arch = "arm",
+    target_arch = "wasm32",
+    target_arch = "x86"
+))]
+#[derive(Clone, Debug, Default, Eq, Ord, Hash, PartialEq, PartialOrd)]
+#[repr(C)]
+pub struct R3 {
+    pub name: roc_std::RocStr,
+    pub r#type: u32,
+}
+
+#[cfg(any(
+    target_arch = "arm",
+    target_arch = "aarch64",
+    target_arch = "wasm32",
+    target_arch = "x86",
+    target_arch = "x86_64"
+))]
+#[derive(Clone, Copy, Debug, Eq, Ord, Hash, PartialEq, PartialOrd)]
+#[repr(C)]
+pub struct Target {
+    pub architecture: Architecture,
+    pub operatingSystem: OperatingSystem,
+}
+
+#[cfg(any(
+    target_arch = "arm",
+    target_arch = "aarch64",
+    target_arch = "wasm32",
+    target_arch = "x86",
+    target_arch = "x86_64"
+))]
+#[derive(Clone, Copy, Eq, Ord, Hash, PartialEq, PartialOrd)]
+#[repr(u8)]
+pub enum OperatingSystem {
+    Unix = 0,
+    Wasi = 1,
+    Windows = 2,
+}
+
+impl core::fmt::Debug for OperatingSystem {
+    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
+        match self {
+            Self::Unix => f.write_str("OperatingSystem::Unix"),
+            Self::Wasi => f.write_str("OperatingSystem::Wasi"),
+            Self::Windows => f.write_str("OperatingSystem::Windows"),
+        }
+    }
+}
+
+#[cfg(any(
+    target_arch = "arm",
+    target_arch = "aarch64",
+    target_arch = "wasm32",
+    target_arch = "x86",
+    target_arch = "x86_64"
+))]
+#[derive(Clone, Copy, Eq, Ord, Hash, PartialEq, PartialOrd)]
+#[repr(u8)]
+pub enum Architecture {
+    Aarch32 = 0,
+    Aarch64 = 1,
+    Wasm32 = 2,
+    X86x32 = 3,
+    X86x64 = 4,
+}
+
+impl core::fmt::Debug for Architecture {
+    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
+        match self {
+            Self::Aarch32 => f.write_str("Architecture::Aarch32"),
+            Self::Aarch64 => f.write_str("Architecture::Aarch64"),
+            Self::Wasm32 => f.write_str("Architecture::Wasm32"),
+            Self::X86x32 => f.write_str("Architecture::X86x32"),
+            Self::X86x64 => f.write_str("Architecture::X86x64"),
+        }
+    }
+}
+
+#[cfg(any(
+    target_arch = "arm",
+    target_arch = "aarch64",
+    target_arch = "wasm32",
+    target_arch = "x86",
+    target_arch = "x86_64"
+))]
+#[derive(Clone, Debug, Default, Eq, Ord, Hash, PartialEq, PartialOrd)]
+#[repr(C)]
+pub struct R14 {
+    pub fields: roc_std::RocList,
+    pub name: roc_std::RocStr,
+}
+
+#[cfg(any(
+    target_arch = "arm",
+    target_arch = "aarch64",
+    target_arch = "wasm32",
+    target_arch = "x86",
+    target_arch = "x86_64"
+))]
+#[derive(Clone, Copy, Eq, Ord, Hash, PartialEq, PartialOrd)]
+#[repr(u8)]
+pub enum discriminant_RocTagUnion {
+    Enumeration = 0,
+    NonNullableUnwrapped = 1,
+    NonRecursive = 2,
+    NullableUnwrapped = 3,
+    NullableWrapped = 4,
+    Recursive = 5,
+    SingleTagStruct = 6,
+}
+
+impl core::fmt::Debug for discriminant_RocTagUnion {
+    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
+        match self {
+            Self::Enumeration => f.write_str("discriminant_RocTagUnion::Enumeration"),
+            Self::NonNullableUnwrapped => f.write_str("discriminant_RocTagUnion::NonNullableUnwrapped"),
+            Self::NonRecursive => f.write_str("discriminant_RocTagUnion::NonRecursive"),
+            Self::NullableUnwrapped => f.write_str("discriminant_RocTagUnion::NullableUnwrapped"),
+            Self::NullableWrapped => f.write_str("discriminant_RocTagUnion::NullableWrapped"),
+            Self::Recursive => f.write_str("discriminant_RocTagUnion::Recursive"),
+            Self::SingleTagStruct => f.write_str("discriminant_RocTagUnion::SingleTagStruct"),
+        }
+    }
+}
+
+#[cfg(any(
+    target_arch = "arm",
+    target_arch = "wasm32",
+    target_arch = "x86"
+))]
+#[repr(C)]
+pub union RocTagUnion {
+    Enumeration: core::mem::ManuallyDrop,
+    NonNullableUnwrapped: core::mem::ManuallyDrop,
+    NonRecursive: core::mem::ManuallyDrop,
+    NullableUnwrapped: core::mem::ManuallyDrop,
+    NullableWrapped: core::mem::ManuallyDrop,
+    Recursive: core::mem::ManuallyDrop,
+    SingleTagStruct: core::mem::ManuallyDrop,
+    _sizer: [u8; 48],
+}
+
+#[cfg(any(
+    target_arch = "arm",
+    target_arch = "wasm32",
+    target_arch = "x86"
+))]
+#[derive(Clone, Debug, Default, Eq, Ord, Hash, PartialEq, PartialOrd)]
+#[repr(C)]
+pub struct R13 {
+    pub name: roc_std::RocStr,
+    pub payloadFields: roc_std::RocList,
+    pub tagName: roc_std::RocStr,
+}
+
+#[cfg(any(
+    target_arch = "arm",
+    target_arch = "wasm32",
+    target_arch = "x86"
+))]
+#[derive(Clone, Debug, Eq, Ord, Hash, PartialEq, PartialOrd)]
+#[repr(C)]
+pub struct R11 {
+    pub discriminantOffset: u32,
+    pub discriminantSize: u32,
+    pub name: roc_std::RocStr,
+    pub tags: roc_std::RocList,
+}
+
+#[cfg(any(
+    target_arch = "arm",
+    target_arch = "aarch64",
+    target_arch = "wasm32",
+    target_arch = "x86",
+    target_arch = "x86_64"
+))]
+#[derive(Clone, Copy, Eq, Ord, Hash, PartialEq, PartialOrd)]
+#[repr(u8)]
+pub enum discriminant_U4 {
+    None = 0,
+    Some = 1,
+}
+
+impl core::fmt::Debug for discriminant_U4 {
+    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
+        match self {
+            Self::None => f.write_str("discriminant_U4::None"),
+            Self::Some => f.write_str("discriminant_U4::Some"),
+        }
+    }
+}
+
+#[cfg(any(
+    target_arch = "arm",
+    target_arch = "wasm32",
+    target_arch = "x86"
+))]
+#[repr(C)]
+pub union U4 {
+    Some: u32,
+    _sizer: [u8; 8],
+}
+
+#[cfg(any(
+    target_arch = "arm",
+    target_arch = "wasm32",
+    target_arch = "x86"
+))]
+#[derive(Clone, Debug, Eq, Ord, Hash, PartialEq, PartialOrd)]
+#[repr(C)]
+pub struct R9 {
+    pub discriminantOffset: u32,
+    pub discriminantSize: u32,
+    pub name: roc_std::RocStr,
+    pub tags: roc_std::RocList,
+    pub indexOfNullTag: u16,
+}
+
+#[cfg(any(
+    target_arch = "arm",
+    target_arch = "aarch64",
+    target_arch = "wasm32",
+    target_arch = "x86",
+    target_arch = "x86_64"
+))]
+#[derive(Clone, Copy, Eq, Ord, Hash, PartialEq, PartialOrd)]
+#[repr(u8)]
+pub enum discriminant_U3 {
+    None = 0,
+    Some = 1,
+}
+
+impl core::fmt::Debug for discriminant_U3 {
+    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
+        match self {
+            Self::None => f.write_str("discriminant_U3::None"),
+            Self::Some => f.write_str("discriminant_U3::Some"),
+        }
+    }
+}
+
+#[cfg(any(
+    target_arch = "arm",
+    target_arch = "wasm32",
+    target_arch = "x86"
+))]
+#[repr(C)]
+pub union U3 {
+    Some: u32,
+    _sizer: [u8; 8],
+}
+
+#[cfg(any(
+    target_arch = "arm",
+    target_arch = "wasm32",
+    target_arch = "x86"
+))]
+#[derive(Clone, Debug, Eq, Ord, Hash, PartialEq, PartialOrd)]
+#[repr(C)]
+pub struct R8 {
+    pub name: roc_std::RocStr,
+    pub nonNullPayload: u32,
+    pub nonNullTag: roc_std::RocStr,
+    pub nullTag: roc_std::RocStr,
+    pub whichTagIsNull: U2,
+}
+
+#[cfg(any(
+    target_arch = "arm",
+    target_arch = "aarch64",
+    target_arch = "wasm32",
+    target_arch = "x86",
+    target_arch = "x86_64"
+))]
+#[derive(Clone, Copy, Eq, Ord, Hash, PartialEq, PartialOrd)]
+#[repr(u8)]
+pub enum U2 {
+    FirstTagIsNull = 0,
+    SecondTagIsNull = 1,
+}
+
+impl core::fmt::Debug for U2 {
+    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
+        match self {
+            Self::FirstTagIsNull => f.write_str("U2::FirstTagIsNull"),
+            Self::SecondTagIsNull => f.write_str("U2::SecondTagIsNull"),
+        }
+    }
+}
+
+#[cfg(any(
+    target_arch = "arm",
+    target_arch = "wasm32",
+    target_arch = "x86"
+))]
+#[derive(Clone, Debug, Eq, Ord, Hash, PartialEq, PartialOrd)]
+#[repr(C)]
+pub struct R6 {
+    pub discriminantOffset: u32,
+    pub discriminantSize: u32,
+    pub name: roc_std::RocStr,
+    pub tags: roc_std::RocList,
+}
+
+#[cfg(any(
+    target_arch = "arm",
+    target_arch = "aarch64",
+    target_arch = "wasm32",
+    target_arch = "x86",
+    target_arch = "x86_64"
+))]
+#[derive(Clone, Copy, Eq, Ord, Hash, PartialEq, PartialOrd)]
+#[repr(u8)]
+pub enum discriminant_U1 {
+    None = 0,
+    Some = 1,
+}
+
+impl core::fmt::Debug for discriminant_U1 {
+    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
+        match self {
+            Self::None => f.write_str("discriminant_U1::None"),
+            Self::Some => f.write_str("discriminant_U1::Some"),
+        }
+    }
+}
+
+#[cfg(any(
+    target_arch = "arm",
+    target_arch = "wasm32",
+    target_arch = "x86"
+))]
+#[repr(C)]
+pub union U1 {
+    Some: u32,
+    _sizer: [u8; 8],
+}
+
+#[cfg(any(
+    target_arch = "arm",
+    target_arch = "wasm32",
+    target_arch = "x86"
+))]
+#[derive(Clone, Debug, Default, Eq, Ord, Hash, PartialEq, PartialOrd)]
+#[repr(C)]
+pub struct R5 {
+    pub name: roc_std::RocStr,
+    pub payload: u32,
+    pub tagName: roc_std::RocStr,
+}
+
+#[cfg(any(
+    target_arch = "arm",
+    target_arch = "wasm32",
+    target_arch = "x86"
+))]
+#[derive(Clone, Debug, Default, Eq, Ord, Hash, PartialEq, PartialOrd)]
+#[repr(C)]
+pub struct R4 {
+    pub name: roc_std::RocStr,
+    pub size: u32,
+    pub tags: roc_std::RocList,
+}
+
+#[cfg(any(
+    target_arch = "arm",
+    target_arch = "aarch64",
+    target_arch = "wasm32",
+    target_arch = "x86",
+    target_arch = "x86_64"
+))]
+#[derive(Clone, Debug, Default, Eq, Ord, Hash, PartialEq, PartialOrd)]
+#[repr(C)]
+pub struct R2 {
+    pub fields: roc_std::RocList,
+    pub name: roc_std::RocStr,
+}
+
+#[cfg(any(
+    target_arch = "arm",
+    target_arch = "wasm32",
+    target_arch = "x86"
+))]
+#[derive(Clone, Copy, Debug, Default, Eq, Ord, Hash, PartialEq, PartialOrd)]
+#[repr(C)]
+struct RocType_RocResult {
+    pub f0: u32,
+    pub f1: u32,
+}
+
+#[cfg(any(
+    target_arch = "arm",
+    target_arch = "wasm32",
+    target_arch = "x86"
+))]
+#[derive(Clone, Copy, Debug, Default, Eq, Ord, Hash, PartialEq, PartialOrd)]
+#[repr(C)]
+struct RocType_RocDict {
+    pub f0: u32,
+    pub f1: u32,
+}
+
+#[cfg(any(
+    target_arch = "arm",
+    target_arch = "aarch64",
+    target_arch = "wasm32",
+    target_arch = "x86",
+    target_arch = "x86_64"
+))]
+#[derive(Clone, Copy, Eq, Ord, Hash, PartialEq, PartialOrd)]
+#[repr(u8)]
+pub enum RocNum {
+    Dec = 0,
+    F32 = 2,
+    F64 = 3,
+    I128 = 4,
+    I16 = 5,
+    I32 = 6,
+    I64 = 7,
+    I8 = 8,
+    U128 = 9,
+    U16 = 10,
+    U32 = 11,
+    U64 = 12,
+    U8 = 13,
+}
+
+impl core::fmt::Debug for RocNum {
+    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
+        match self {
+            Self::Dec => f.write_str("RocNum::Dec"),
+            Self::F32 => f.write_str("RocNum::F32"),
+            Self::F64 => f.write_str("RocNum::F64"),
+            Self::I128 => f.write_str("RocNum::I128"),
+            Self::I16 => f.write_str("RocNum::I16"),
+            Self::I32 => f.write_str("RocNum::I32"),
+            Self::I64 => f.write_str("RocNum::I64"),
+            Self::I8 => f.write_str("RocNum::I8"),
+            Self::U128 => f.write_str("RocNum::U128"),
+            Self::U16 => f.write_str("RocNum::U16"),
+            Self::U32 => f.write_str("RocNum::U32"),
+            Self::U64 => f.write_str("RocNum::U64"),
+            Self::U8 => f.write_str("RocNum::U8"),
+        }
+    }
+}
+
+#[cfg(any(
+    target_arch = "arm",
+    target_arch = "wasm32",
+    target_arch = "x86"
+))]
+#[derive(Clone, Debug, Default, Eq, Ord, Hash, PartialEq, PartialOrd)]
+#[repr(C)]
+pub struct R1 {
+    pub args: roc_std::RocList,
+    pub name: roc_std::RocStr,
+    pub ret: u32,
+}
+
+#[cfg(any(
+    target_arch = "aarch64",
+    target_arch = "x86_64"
+))]
+#[derive(Clone, Debug, Eq, Ord, Hash, PartialEq, PartialOrd)]
+#[repr(C)]
+pub struct Types {
+    pub aligns: roc_std::RocList,
+    pub deps: roc_std::RocDict>,
+    pub sizes: roc_std::RocList,
+    pub types: roc_std::RocList,
+    pub typesByName: roc_std::RocDict,
+    pub target: Target,
+}
+
+#[cfg(any(
+    target_arch = "aarch64",
+    target_arch = "x86_64"
+))]
+#[repr(C)]
+pub union RocType {
+    Function: core::mem::ManuallyDrop,
+    Num: RocNum,
+    RecursivePointer: u64,
+    RocBox: u64,
+    RocDict: RocType_RocDict,
+    RocList: u64,
+    RocResult: RocType_RocResult,
+    RocSet: u64,
+    Struct: core::mem::ManuallyDrop,
+    TagUnion: core::mem::ManuallyDrop,
+    TagUnionPayload: core::mem::ManuallyDrop,
+    _sizer: [u8; 104],
+}
+
+#[cfg(any(
+    target_arch = "aarch64",
+    target_arch = "x86_64"
+))]
+#[derive(Clone, Copy, Debug, Default, Eq, Ord, Hash, PartialEq, PartialOrd)]
+#[repr(C)]
+pub struct R15 {
+    pub discriminant: u64,
+    pub r#type: u64,
+}
+
+#[cfg(any(
+    target_arch = "aarch64",
+    target_arch = "x86_64"
+))]
+#[derive(Clone, Debug, Default, Eq, Ord, Hash, PartialEq, PartialOrd)]
+#[repr(C)]
+pub struct R3 {
+    pub name: roc_std::RocStr,
+    pub r#type: u64,
+}
+
+#[cfg(any(
+    target_arch = "aarch64",
+    target_arch = "x86_64"
+))]
+#[repr(C)]
+pub union RocTagUnion {
+    Enumeration: core::mem::ManuallyDrop,
+    NonNullableUnwrapped: core::mem::ManuallyDrop,
+    NonRecursive: core::mem::ManuallyDrop,
+    NullableUnwrapped: core::mem::ManuallyDrop,
+    NullableWrapped: core::mem::ManuallyDrop,
+    Recursive: core::mem::ManuallyDrop,
+    SingleTagStruct: core::mem::ManuallyDrop,
+    _sizer: [u8; 96],
+}
+
+#[cfg(any(
+    target_arch = "aarch64",
+    target_arch = "x86_64"
+))]
+#[derive(Clone, Debug, Default, Eq, Ord, Hash, PartialEq, PartialOrd)]
+#[repr(C)]
+pub struct R13 {
+    pub name: roc_std::RocStr,
+    pub payloadFields: roc_std::RocList,
+    pub tagName: roc_std::RocStr,
+}
+
+#[cfg(any(
+    target_arch = "aarch64",
+    target_arch = "x86_64"
+))]
+#[derive(Clone, Debug, Eq, Ord, Hash, PartialEq, PartialOrd)]
+#[repr(C)]
+pub struct R11 {
+    pub name: roc_std::RocStr,
+    pub tags: roc_std::RocList,
+    pub discriminantOffset: u32,
+    pub discriminantSize: u32,
+}
+
+#[cfg(any(
+    target_arch = "aarch64",
+    target_arch = "x86_64"
+))]
+#[repr(C)]
+pub union U4 {
+    Some: u64,
+    _sizer: [u8; 16],
+}
+
+#[cfg(any(
+    target_arch = "aarch64",
+    target_arch = "x86_64"
+))]
+#[derive(Clone, Debug, Eq, Ord, Hash, PartialEq, PartialOrd)]
+#[repr(C)]
+pub struct R9 {
+    pub name: roc_std::RocStr,
+    pub tags: roc_std::RocList,
+    pub discriminantOffset: u32,
+    pub discriminantSize: u32,
+    pub indexOfNullTag: u16,
+}
+
+#[cfg(any(
+    target_arch = "aarch64",
+    target_arch = "x86_64"
+))]
+#[repr(C)]
+pub union U3 {
+    Some: u64,
+    _sizer: [u8; 16],
+}
+
+#[cfg(any(
+    target_arch = "aarch64",
+    target_arch = "x86_64"
+))]
+#[derive(Clone, Debug, Eq, Ord, Hash, PartialEq, PartialOrd)]
+#[repr(C)]
+pub struct R8 {
+    pub name: roc_std::RocStr,
+    pub nonNullPayload: u64,
+    pub nonNullTag: roc_std::RocStr,
+    pub nullTag: roc_std::RocStr,
+    pub whichTagIsNull: U2,
+}
+
+#[cfg(any(
+    target_arch = "aarch64",
+    target_arch = "x86_64"
+))]
+#[derive(Clone, Debug, Eq, Ord, Hash, PartialEq, PartialOrd)]
+#[repr(C)]
+pub struct R6 {
+    pub name: roc_std::RocStr,
+    pub tags: roc_std::RocList,
+    pub discriminantOffset: u32,
+    pub discriminantSize: u32,
+}
+
+#[cfg(any(
+    target_arch = "aarch64",
+    target_arch = "x86_64"
+))]
+#[repr(C)]
+pub union U1 {
+    Some: u64,
+    _sizer: [u8; 16],
+}
+
+#[cfg(any(
+    target_arch = "aarch64",
+    target_arch = "x86_64"
+))]
+#[derive(Clone, Debug, Default, Eq, Ord, Hash, PartialEq, PartialOrd)]
+#[repr(C)]
+pub struct R5 {
+    pub name: roc_std::RocStr,
+    pub payload: u64,
+    pub tagName: roc_std::RocStr,
+}
+
+#[cfg(any(
+    target_arch = "aarch64",
+    target_arch = "x86_64"
+))]
+#[derive(Clone, Debug, Default, Eq, Ord, Hash, PartialEq, PartialOrd)]
+#[repr(C)]
+pub struct R4 {
+    pub name: roc_std::RocStr,
+    pub tags: roc_std::RocList,
+    pub size: u32,
+}
+
+#[cfg(any(
+    target_arch = "aarch64",
+    target_arch = "x86_64"
+))]
+#[derive(Clone, Copy, Debug, Default, Eq, Ord, Hash, PartialEq, PartialOrd)]
+#[repr(C)]
+struct RocType_RocResult {
+    pub f0: u64,
+    pub f1: u64,
+}
+
+#[cfg(any(
+    target_arch = "aarch64",
+    target_arch = "x86_64"
+))]
+#[derive(Clone, Copy, Debug, Default, Eq, Ord, Hash, PartialEq, PartialOrd)]
+#[repr(C)]
+struct RocType_RocDict {
+    pub f0: u64,
+    pub f1: u64,
+}
+
+#[cfg(any(
+    target_arch = "aarch64",
+    target_arch = "x86_64"
+))]
+#[derive(Clone, Debug, Default, Eq, Ord, Hash, PartialEq, PartialOrd)]
+#[repr(C)]
+pub struct R1 {
+    pub args: roc_std::RocList,
+    pub name: roc_std::RocStr,
+    pub ret: u64,
+}
+
+impl RocType {
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "wasm32",
+        target_arch = "x86"
+    ))]
+    /// Returns which variant this tag union holds. Note that this never includes a payload!
+    pub fn discriminant(&self) -> discriminant_RocType {
+        unsafe {
+            let bytes = core::mem::transmute::<&Self, &[u8; core::mem::size_of::()]>(self);
+
+            core::mem::transmute::(*bytes.as_ptr().add(48))
+        }
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "wasm32",
+        target_arch = "x86"
+    ))]
+    /// Internal helper
+    fn set_discriminant(&mut self, discriminant: discriminant_RocType) {
+        let discriminant_ptr: *mut discriminant_RocType = (self as *mut RocType).cast();
+
+        unsafe {
+            *(discriminant_ptr.add(48)) = discriminant;
+        }
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "wasm32",
+        target_arch = "x86"
+    ))]
+    /// A tag named Bool, which has no payload.
+    pub const Bool: Self = unsafe {
+        let mut bytes = [0; core::mem::size_of::()];
+
+        bytes[48] = discriminant_RocType::Bool as u8;
+
+        core::mem::transmute::<[u8; core::mem::size_of::()], RocType>(bytes)
+    };
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    /// Other `into_` methods return a payload, but since the Bool tag
+    /// has no payload, this does nothing and is only here for completeness.
+    pub fn into_Bool(self) {
+        ()
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    /// Other `as` methods return a payload, but since the Bool tag
+    /// has no payload, this does nothing and is only here for completeness.
+    pub fn as_Bool(&self) {
+        ()
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "wasm32",
+        target_arch = "x86"
+    ))]
+    /// A tag named EmptyTagUnion, which has no payload.
+    pub const EmptyTagUnion: Self = unsafe {
+        let mut bytes = [0; core::mem::size_of::()];
+
+        bytes[48] = discriminant_RocType::EmptyTagUnion as u8;
+
+        core::mem::transmute::<[u8; core::mem::size_of::()], RocType>(bytes)
+    };
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    /// Other `into_` methods return a payload, but since the EmptyTagUnion tag
+    /// has no payload, this does nothing and is only here for completeness.
+    pub fn into_EmptyTagUnion(self) {
+        ()
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    /// Other `as` methods return a payload, but since the EmptyTagUnion tag
+    /// has no payload, this does nothing and is only here for completeness.
+    pub fn as_EmptyTagUnion(&self) {
+        ()
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    /// Construct a tag named `Function`, with the appropriate payload
+    pub fn Function(arg0: R1) -> Self {
+            let mut answer = Self {
+                Function: core::mem::ManuallyDrop::new(arg0)
+            };
+
+            answer.set_discriminant(discriminant_RocType::Function);
+
+            answer
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    /// Unsafely assume the given `RocType` has a `.discriminant()` of `Function` and convert it to `Function`'s payload.
+            /// (Always examine `.discriminant()` first to make sure this is the correct variant!)
+            /// Panics in debug builds if the `.discriminant()` doesn't return `Function`.
+            pub unsafe fn into_Function(mut self) -> R1 {
+                debug_assert_eq!(self.discriminant(), discriminant_RocType::Function);
+        let payload = {
+            let mut uninitialized = core::mem::MaybeUninit::uninit();
+            let swapped = unsafe {
+                core::mem::replace(
+                    &mut self.Function,
+                    core::mem::ManuallyDrop::new(uninitialized.assume_init()),
+                )
+            };
+
+            core::mem::forget(self);
+
+            core::mem::ManuallyDrop::into_inner(swapped)
+        };
+
+        
+        payload
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    /// Unsafely assume the given `RocType` has a `.discriminant()` of `Function` and return its payload.
+            /// (Always examine `.discriminant()` first to make sure this is the correct variant!)
+            /// Panics in debug builds if the `.discriminant()` doesn't return `Function`.
+            pub unsafe fn as_Function(&self) -> &R1 {
+                debug_assert_eq!(self.discriminant(), discriminant_RocType::Function);
+        let payload = &self.Function;
+
+        
+        payload
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    /// Construct a tag named `Num`, with the appropriate payload
+    pub fn Num(arg: RocNum) -> Self {
+            let mut answer = Self {
+                Num: arg
+            };
+
+            answer.set_discriminant(discriminant_RocType::Num);
+
+            answer
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    /// Unsafely assume the given `RocType` has a `.discriminant()` of `Num` and convert it to `Num`'s payload.
+            /// (Always examine `.discriminant()` first to make sure this is the correct variant!)
+            /// Panics in debug builds if the `.discriminant()` doesn't return `Num`.
+            pub unsafe fn into_Num(self) -> RocNum {
+                debug_assert_eq!(self.discriminant(), discriminant_RocType::Num);
+        let payload = self.Num;
+
+        payload
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    /// Unsafely assume the given `RocType` has a `.discriminant()` of `Num` and return its payload.
+            /// (Always examine `.discriminant()` first to make sure this is the correct variant!)
+            /// Panics in debug builds if the `.discriminant()` doesn't return `Num`.
+            pub unsafe fn as_Num(&self) -> &RocNum {
+                debug_assert_eq!(self.discriminant(), discriminant_RocType::Num);
+        let payload = &self.Num;
+
+        &payload
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "wasm32",
+        target_arch = "x86"
+    ))]
+    /// Construct a tag named `RecursivePointer`, with the appropriate payload
+    pub fn RecursivePointer(arg: u32) -> Self {
+            let mut answer = Self {
+                RecursivePointer: arg
+            };
+
+            answer.set_discriminant(discriminant_RocType::RecursivePointer);
+
+            answer
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "wasm32",
+        target_arch = "x86"
+    ))]
+    /// Unsafely assume the given `RocType` has a `.discriminant()` of `RecursivePointer` and convert it to `RecursivePointer`'s payload.
+            /// (Always examine `.discriminant()` first to make sure this is the correct variant!)
+            /// Panics in debug builds if the `.discriminant()` doesn't return `RecursivePointer`.
+            pub unsafe fn into_RecursivePointer(self) -> u32 {
+                debug_assert_eq!(self.discriminant(), discriminant_RocType::RecursivePointer);
+        let payload = self.RecursivePointer;
+
+        payload
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "wasm32",
+        target_arch = "x86"
+    ))]
+    /// Unsafely assume the given `RocType` has a `.discriminant()` of `RecursivePointer` and return its payload.
+            /// (Always examine `.discriminant()` first to make sure this is the correct variant!)
+            /// Panics in debug builds if the `.discriminant()` doesn't return `RecursivePointer`.
+            pub unsafe fn as_RecursivePointer(&self) -> &u32 {
+                debug_assert_eq!(self.discriminant(), discriminant_RocType::RecursivePointer);
+        let payload = &self.RecursivePointer;
+
+        &payload
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "wasm32",
+        target_arch = "x86"
+    ))]
+    /// Construct a tag named `RocBox`, with the appropriate payload
+    pub fn RocBox(arg: u32) -> Self {
+            let mut answer = Self {
+                RocBox: arg
+            };
+
+            answer.set_discriminant(discriminant_RocType::RocBox);
+
+            answer
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "wasm32",
+        target_arch = "x86"
+    ))]
+    /// Unsafely assume the given `RocType` has a `.discriminant()` of `RocBox` and convert it to `RocBox`'s payload.
+            /// (Always examine `.discriminant()` first to make sure this is the correct variant!)
+            /// Panics in debug builds if the `.discriminant()` doesn't return `RocBox`.
+            pub unsafe fn into_RocBox(self) -> u32 {
+                debug_assert_eq!(self.discriminant(), discriminant_RocType::RocBox);
+        let payload = self.RocBox;
+
+        payload
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "wasm32",
+        target_arch = "x86"
+    ))]
+    /// Unsafely assume the given `RocType` has a `.discriminant()` of `RocBox` and return its payload.
+            /// (Always examine `.discriminant()` first to make sure this is the correct variant!)
+            /// Panics in debug builds if the `.discriminant()` doesn't return `RocBox`.
+            pub unsafe fn as_RocBox(&self) -> &u32 {
+                debug_assert_eq!(self.discriminant(), discriminant_RocType::RocBox);
+        let payload = &self.RocBox;
+
+        &payload
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "wasm32",
+        target_arch = "x86"
+    ))]
+    /// Construct a tag named `RocDict`, with the appropriate payload
+    pub fn RocDict(arg0: u32, arg1: u32) -> Self {
+            let mut answer = Self {
+                RocDict: RocType_RocDict {
+                    f0: arg0,
+                    f1: arg1,
+                }
+            };
+
+            answer.set_discriminant(discriminant_RocType::RocDict);
+
+            answer
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "wasm32",
+        target_arch = "x86"
+    ))]
+    /// Unsafely assume the given `RocType` has a `.discriminant()` of `RocDict` and convert it to `RocDict`'s payload.
+            /// (Always examine `.discriminant()` first to make sure this is the correct variant!)
+            /// Panics in debug builds if the `.discriminant()` doesn't return `RocDict`.
+            pub unsafe fn into_RocDict(self) -> (u32, u32) {
+                debug_assert_eq!(self.discriminant(), discriminant_RocType::RocDict);
+        let payload = self.RocDict;
+
+        (
+            payload.f0, 
+            payload.f1
+        )
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "wasm32",
+        target_arch = "x86"
+    ))]
+    /// Unsafely assume the given `RocType` has a `.discriminant()` of `RocDict` and return its payload.
+            /// (Always examine `.discriminant()` first to make sure this is the correct variant!)
+            /// Panics in debug builds if the `.discriminant()` doesn't return `RocDict`.
+            pub unsafe fn as_RocDict(&self) -> (&u32, &u32) {
+                debug_assert_eq!(self.discriminant(), discriminant_RocType::RocDict);
+        let payload = &self.RocDict;
+
+        (
+            &payload.f0, 
+            &payload.f1
+        )
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "wasm32",
+        target_arch = "x86"
+    ))]
+    /// Construct a tag named `RocList`, with the appropriate payload
+    pub fn RocList(arg: u32) -> Self {
+            let mut answer = Self {
+                RocList: arg
+            };
+
+            answer.set_discriminant(discriminant_RocType::RocList);
+
+            answer
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "wasm32",
+        target_arch = "x86"
+    ))]
+    /// Unsafely assume the given `RocType` has a `.discriminant()` of `RocList` and convert it to `RocList`'s payload.
+            /// (Always examine `.discriminant()` first to make sure this is the correct variant!)
+            /// Panics in debug builds if the `.discriminant()` doesn't return `RocList`.
+            pub unsafe fn into_RocList(self) -> u32 {
+                debug_assert_eq!(self.discriminant(), discriminant_RocType::RocList);
+        let payload = self.RocList;
+
+        payload
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "wasm32",
+        target_arch = "x86"
+    ))]
+    /// Unsafely assume the given `RocType` has a `.discriminant()` of `RocList` and return its payload.
+            /// (Always examine `.discriminant()` first to make sure this is the correct variant!)
+            /// Panics in debug builds if the `.discriminant()` doesn't return `RocList`.
+            pub unsafe fn as_RocList(&self) -> &u32 {
+                debug_assert_eq!(self.discriminant(), discriminant_RocType::RocList);
+        let payload = &self.RocList;
+
+        &payload
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "wasm32",
+        target_arch = "x86"
+    ))]
+    /// Construct a tag named `RocResult`, with the appropriate payload
+    pub fn RocResult(arg0: u32, arg1: u32) -> Self {
+            let mut answer = Self {
+                RocResult: RocType_RocResult {
+                    f0: arg0,
+                    f1: arg1,
+                }
+            };
+
+            answer.set_discriminant(discriminant_RocType::RocResult);
+
+            answer
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "wasm32",
+        target_arch = "x86"
+    ))]
+    /// Unsafely assume the given `RocType` has a `.discriminant()` of `RocResult` and convert it to `RocResult`'s payload.
+            /// (Always examine `.discriminant()` first to make sure this is the correct variant!)
+            /// Panics in debug builds if the `.discriminant()` doesn't return `RocResult`.
+            pub unsafe fn into_RocResult(self) -> (u32, u32) {
+                debug_assert_eq!(self.discriminant(), discriminant_RocType::RocResult);
+        let payload = self.RocResult;
+
+        (
+            payload.f0, 
+            payload.f1
+        )
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "wasm32",
+        target_arch = "x86"
+    ))]
+    /// Unsafely assume the given `RocType` has a `.discriminant()` of `RocResult` and return its payload.
+            /// (Always examine `.discriminant()` first to make sure this is the correct variant!)
+            /// Panics in debug builds if the `.discriminant()` doesn't return `RocResult`.
+            pub unsafe fn as_RocResult(&self) -> (&u32, &u32) {
+                debug_assert_eq!(self.discriminant(), discriminant_RocType::RocResult);
+        let payload = &self.RocResult;
+
+        (
+            &payload.f0, 
+            &payload.f1
+        )
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "wasm32",
+        target_arch = "x86"
+    ))]
+    /// Construct a tag named `RocSet`, with the appropriate payload
+    pub fn RocSet(arg: u32) -> Self {
+            let mut answer = Self {
+                RocSet: arg
+            };
+
+            answer.set_discriminant(discriminant_RocType::RocSet);
+
+            answer
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "wasm32",
+        target_arch = "x86"
+    ))]
+    /// Unsafely assume the given `RocType` has a `.discriminant()` of `RocSet` and convert it to `RocSet`'s payload.
+            /// (Always examine `.discriminant()` first to make sure this is the correct variant!)
+            /// Panics in debug builds if the `.discriminant()` doesn't return `RocSet`.
+            pub unsafe fn into_RocSet(self) -> u32 {
+                debug_assert_eq!(self.discriminant(), discriminant_RocType::RocSet);
+        let payload = self.RocSet;
+
+        payload
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "wasm32",
+        target_arch = "x86"
+    ))]
+    /// Unsafely assume the given `RocType` has a `.discriminant()` of `RocSet` and return its payload.
+            /// (Always examine `.discriminant()` first to make sure this is the correct variant!)
+            /// Panics in debug builds if the `.discriminant()` doesn't return `RocSet`.
+            pub unsafe fn as_RocSet(&self) -> &u32 {
+                debug_assert_eq!(self.discriminant(), discriminant_RocType::RocSet);
+        let payload = &self.RocSet;
+
+        &payload
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "wasm32",
+        target_arch = "x86"
+    ))]
+    /// A tag named RocStr, which has no payload.
+    pub const RocStr: Self = unsafe {
+        let mut bytes = [0; core::mem::size_of::()];
+
+        bytes[48] = discriminant_RocType::RocStr as u8;
+
+        core::mem::transmute::<[u8; core::mem::size_of::()], RocType>(bytes)
+    };
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    /// Other `into_` methods return a payload, but since the RocStr tag
+    /// has no payload, this does nothing and is only here for completeness.
+    pub fn into_RocStr(self) {
+        ()
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    /// Other `as` methods return a payload, but since the RocStr tag
+    /// has no payload, this does nothing and is only here for completeness.
+    pub fn as_RocStr(&self) {
+        ()
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    /// Construct a tag named `Struct`, with the appropriate payload
+    pub fn Struct(arg0: R2) -> Self {
+            let mut answer = Self {
+                Struct: core::mem::ManuallyDrop::new(arg0)
+            };
+
+            answer.set_discriminant(discriminant_RocType::Struct);
+
+            answer
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    /// Unsafely assume the given `RocType` has a `.discriminant()` of `Struct` and convert it to `Struct`'s payload.
+            /// (Always examine `.discriminant()` first to make sure this is the correct variant!)
+            /// Panics in debug builds if the `.discriminant()` doesn't return `Struct`.
+            pub unsafe fn into_Struct(mut self) -> R2 {
+                debug_assert_eq!(self.discriminant(), discriminant_RocType::Struct);
+        let payload = {
+            let mut uninitialized = core::mem::MaybeUninit::uninit();
+            let swapped = unsafe {
+                core::mem::replace(
+                    &mut self.Struct,
+                    core::mem::ManuallyDrop::new(uninitialized.assume_init()),
+                )
+            };
+
+            core::mem::forget(self);
+
+            core::mem::ManuallyDrop::into_inner(swapped)
+        };
+
+        
+        payload
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    /// Unsafely assume the given `RocType` has a `.discriminant()` of `Struct` and return its payload.
+            /// (Always examine `.discriminant()` first to make sure this is the correct variant!)
+            /// Panics in debug builds if the `.discriminant()` doesn't return `Struct`.
+            pub unsafe fn as_Struct(&self) -> &R2 {
+                debug_assert_eq!(self.discriminant(), discriminant_RocType::Struct);
+        let payload = &self.Struct;
+
+        
+        payload
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    /// Construct a tag named `TagUnion`, with the appropriate payload
+    pub fn TagUnion(arg: RocTagUnion) -> Self {
+            let mut answer = Self {
+                TagUnion: core::mem::ManuallyDrop::new(arg)
+            };
+
+            answer.set_discriminant(discriminant_RocType::TagUnion);
+
+            answer
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    /// Unsafely assume the given `RocType` has a `.discriminant()` of `TagUnion` and convert it to `TagUnion`'s payload.
+            /// (Always examine `.discriminant()` first to make sure this is the correct variant!)
+            /// Panics in debug builds if the `.discriminant()` doesn't return `TagUnion`.
+            pub unsafe fn into_TagUnion(mut self) -> RocTagUnion {
+                debug_assert_eq!(self.discriminant(), discriminant_RocType::TagUnion);
+        let payload = {
+            let mut uninitialized = core::mem::MaybeUninit::uninit();
+            let swapped = unsafe {
+                core::mem::replace(
+                    &mut self.TagUnion,
+                    core::mem::ManuallyDrop::new(uninitialized.assume_init()),
+                )
+            };
+
+            core::mem::forget(self);
+
+            core::mem::ManuallyDrop::into_inner(swapped)
+        };
+
+        payload
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    /// Unsafely assume the given `RocType` has a `.discriminant()` of `TagUnion` and return its payload.
+            /// (Always examine `.discriminant()` first to make sure this is the correct variant!)
+            /// Panics in debug builds if the `.discriminant()` doesn't return `TagUnion`.
+            pub unsafe fn as_TagUnion(&self) -> &RocTagUnion {
+                debug_assert_eq!(self.discriminant(), discriminant_RocType::TagUnion);
+        let payload = &self.TagUnion;
+
+        &payload
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    /// Construct a tag named `TagUnionPayload`, with the appropriate payload
+    pub fn TagUnionPayload(arg0: R14) -> Self {
+            let mut answer = Self {
+                TagUnionPayload: core::mem::ManuallyDrop::new(arg0)
+            };
+
+            answer.set_discriminant(discriminant_RocType::TagUnionPayload);
+
+            answer
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    /// Unsafely assume the given `RocType` has a `.discriminant()` of `TagUnionPayload` and convert it to `TagUnionPayload`'s payload.
+            /// (Always examine `.discriminant()` first to make sure this is the correct variant!)
+            /// Panics in debug builds if the `.discriminant()` doesn't return `TagUnionPayload`.
+            pub unsafe fn into_TagUnionPayload(mut self) -> R14 {
+                debug_assert_eq!(self.discriminant(), discriminant_RocType::TagUnionPayload);
+        let payload = {
+            let mut uninitialized = core::mem::MaybeUninit::uninit();
+            let swapped = unsafe {
+                core::mem::replace(
+                    &mut self.TagUnionPayload,
+                    core::mem::ManuallyDrop::new(uninitialized.assume_init()),
+                )
+            };
+
+            core::mem::forget(self);
+
+            core::mem::ManuallyDrop::into_inner(swapped)
+        };
+
+        
+        payload
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    /// Unsafely assume the given `RocType` has a `.discriminant()` of `TagUnionPayload` and return its payload.
+            /// (Always examine `.discriminant()` first to make sure this is the correct variant!)
+            /// Panics in debug builds if the `.discriminant()` doesn't return `TagUnionPayload`.
+            pub unsafe fn as_TagUnionPayload(&self) -> &R14 {
+                debug_assert_eq!(self.discriminant(), discriminant_RocType::TagUnionPayload);
+        let payload = &self.TagUnionPayload;
+
+        
+        payload
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "wasm32",
+        target_arch = "x86"
+    ))]
+    /// A tag named Unit, which has no payload.
+    pub const Unit: Self = unsafe {
+        let mut bytes = [0; core::mem::size_of::()];
+
+        bytes[48] = discriminant_RocType::Unit as u8;
+
+        core::mem::transmute::<[u8; core::mem::size_of::()], RocType>(bytes)
+    };
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    /// Other `into_` methods return a payload, but since the Unit tag
+    /// has no payload, this does nothing and is only here for completeness.
+    pub fn into_Unit(self) {
+        ()
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    /// Other `as` methods return a payload, but since the Unit tag
+    /// has no payload, this does nothing and is only here for completeness.
+    pub fn as_Unit(&self) {
+        ()
+    }
+
+    #[cfg(any(
+        target_arch = "aarch64",
+        target_arch = "x86_64"
+    ))]
+    /// Returns which variant this tag union holds. Note that this never includes a payload!
+    pub fn discriminant(&self) -> discriminant_RocType {
+        unsafe {
+            let bytes = core::mem::transmute::<&Self, &[u8; core::mem::size_of::()]>(self);
+
+            core::mem::transmute::(*bytes.as_ptr().add(96))
+        }
+    }
+
+    #[cfg(any(
+        target_arch = "aarch64",
+        target_arch = "x86_64"
+    ))]
+    /// Internal helper
+    fn set_discriminant(&mut self, discriminant: discriminant_RocType) {
+        let discriminant_ptr: *mut discriminant_RocType = (self as *mut RocType).cast();
+
+        unsafe {
+            *(discriminant_ptr.add(96)) = discriminant;
+        }
+    }
+
+    #[cfg(any(
+        target_arch = "aarch64",
+        target_arch = "x86_64"
+    ))]
+    /// A tag named Bool, which has no payload.
+    pub const Bool: Self = unsafe {
+        let mut bytes = [0; core::mem::size_of::()];
+
+        bytes[96] = discriminant_RocType::Bool as u8;
+
+        core::mem::transmute::<[u8; core::mem::size_of::()], RocType>(bytes)
+    };
+
+    #[cfg(any(
+        target_arch = "aarch64",
+        target_arch = "x86_64"
+    ))]
+    /// A tag named EmptyTagUnion, which has no payload.
+    pub const EmptyTagUnion: Self = unsafe {
+        let mut bytes = [0; core::mem::size_of::()];
+
+        bytes[96] = discriminant_RocType::EmptyTagUnion as u8;
+
+        core::mem::transmute::<[u8; core::mem::size_of::()], RocType>(bytes)
+    };
+
+    #[cfg(any(
+        target_arch = "aarch64",
+        target_arch = "x86_64"
+    ))]
+    /// Construct a tag named `RecursivePointer`, with the appropriate payload
+    pub fn RecursivePointer(arg: u64) -> Self {
+            let mut answer = Self {
+                RecursivePointer: arg
+            };
+
+            answer.set_discriminant(discriminant_RocType::RecursivePointer);
+
+            answer
+    }
+
+    #[cfg(any(
+        target_arch = "aarch64",
+        target_arch = "x86_64"
+    ))]
+    /// Unsafely assume the given `RocType` has a `.discriminant()` of `RecursivePointer` and convert it to `RecursivePointer`'s payload.
+            /// (Always examine `.discriminant()` first to make sure this is the correct variant!)
+            /// Panics in debug builds if the `.discriminant()` doesn't return `RecursivePointer`.
+            pub unsafe fn into_RecursivePointer(self) -> u64 {
+                debug_assert_eq!(self.discriminant(), discriminant_RocType::RecursivePointer);
+        let payload = self.RecursivePointer;
+
+        payload
+    }
+
+    #[cfg(any(
+        target_arch = "aarch64",
+        target_arch = "x86_64"
+    ))]
+    /// Unsafely assume the given `RocType` has a `.discriminant()` of `RecursivePointer` and return its payload.
+            /// (Always examine `.discriminant()` first to make sure this is the correct variant!)
+            /// Panics in debug builds if the `.discriminant()` doesn't return `RecursivePointer`.
+            pub unsafe fn as_RecursivePointer(&self) -> &u64 {
+                debug_assert_eq!(self.discriminant(), discriminant_RocType::RecursivePointer);
+        let payload = &self.RecursivePointer;
+
+        &payload
+    }
+
+    #[cfg(any(
+        target_arch = "aarch64",
+        target_arch = "x86_64"
+    ))]
+    /// Construct a tag named `RocBox`, with the appropriate payload
+    pub fn RocBox(arg: u64) -> Self {
+            let mut answer = Self {
+                RocBox: arg
+            };
+
+            answer.set_discriminant(discriminant_RocType::RocBox);
+
+            answer
+    }
+
+    #[cfg(any(
+        target_arch = "aarch64",
+        target_arch = "x86_64"
+    ))]
+    /// Unsafely assume the given `RocType` has a `.discriminant()` of `RocBox` and convert it to `RocBox`'s payload.
+            /// (Always examine `.discriminant()` first to make sure this is the correct variant!)
+            /// Panics in debug builds if the `.discriminant()` doesn't return `RocBox`.
+            pub unsafe fn into_RocBox(self) -> u64 {
+                debug_assert_eq!(self.discriminant(), discriminant_RocType::RocBox);
+        let payload = self.RocBox;
+
+        payload
+    }
+
+    #[cfg(any(
+        target_arch = "aarch64",
+        target_arch = "x86_64"
+    ))]
+    /// Unsafely assume the given `RocType` has a `.discriminant()` of `RocBox` and return its payload.
+            /// (Always examine `.discriminant()` first to make sure this is the correct variant!)
+            /// Panics in debug builds if the `.discriminant()` doesn't return `RocBox`.
+            pub unsafe fn as_RocBox(&self) -> &u64 {
+                debug_assert_eq!(self.discriminant(), discriminant_RocType::RocBox);
+        let payload = &self.RocBox;
+
+        &payload
+    }
+
+    #[cfg(any(
+        target_arch = "aarch64",
+        target_arch = "x86_64"
+    ))]
+    /// Construct a tag named `RocDict`, with the appropriate payload
+    pub fn RocDict(arg0: u64, arg1: u64) -> Self {
+            let mut answer = Self {
+                RocDict: RocType_RocDict {
+                    f0: arg0,
+                    f1: arg1,
+                }
+            };
+
+            answer.set_discriminant(discriminant_RocType::RocDict);
+
+            answer
+    }
+
+    #[cfg(any(
+        target_arch = "aarch64",
+        target_arch = "x86_64"
+    ))]
+    /// Unsafely assume the given `RocType` has a `.discriminant()` of `RocDict` and convert it to `RocDict`'s payload.
+            /// (Always examine `.discriminant()` first to make sure this is the correct variant!)
+            /// Panics in debug builds if the `.discriminant()` doesn't return `RocDict`.
+            pub unsafe fn into_RocDict(self) -> (u64, u64) {
+                debug_assert_eq!(self.discriminant(), discriminant_RocType::RocDict);
+        let payload = self.RocDict;
+
+        (
+            payload.f0, 
+            payload.f1
+        )
+    }
+
+    #[cfg(any(
+        target_arch = "aarch64",
+        target_arch = "x86_64"
+    ))]
+    /// Unsafely assume the given `RocType` has a `.discriminant()` of `RocDict` and return its payload.
+            /// (Always examine `.discriminant()` first to make sure this is the correct variant!)
+            /// Panics in debug builds if the `.discriminant()` doesn't return `RocDict`.
+            pub unsafe fn as_RocDict(&self) -> (&u64, &u64) {
+                debug_assert_eq!(self.discriminant(), discriminant_RocType::RocDict);
+        let payload = &self.RocDict;
+
+        (
+            &payload.f0, 
+            &payload.f1
+        )
+    }
+
+    #[cfg(any(
+        target_arch = "aarch64",
+        target_arch = "x86_64"
+    ))]
+    /// Construct a tag named `RocList`, with the appropriate payload
+    pub fn RocList(arg: u64) -> Self {
+            let mut answer = Self {
+                RocList: arg
+            };
+
+            answer.set_discriminant(discriminant_RocType::RocList);
+
+            answer
+    }
+
+    #[cfg(any(
+        target_arch = "aarch64",
+        target_arch = "x86_64"
+    ))]
+    /// Unsafely assume the given `RocType` has a `.discriminant()` of `RocList` and convert it to `RocList`'s payload.
+            /// (Always examine `.discriminant()` first to make sure this is the correct variant!)
+            /// Panics in debug builds if the `.discriminant()` doesn't return `RocList`.
+            pub unsafe fn into_RocList(self) -> u64 {
+                debug_assert_eq!(self.discriminant(), discriminant_RocType::RocList);
+        let payload = self.RocList;
+
+        payload
+    }
+
+    #[cfg(any(
+        target_arch = "aarch64",
+        target_arch = "x86_64"
+    ))]
+    /// Unsafely assume the given `RocType` has a `.discriminant()` of `RocList` and return its payload.
+            /// (Always examine `.discriminant()` first to make sure this is the correct variant!)
+            /// Panics in debug builds if the `.discriminant()` doesn't return `RocList`.
+            pub unsafe fn as_RocList(&self) -> &u64 {
+                debug_assert_eq!(self.discriminant(), discriminant_RocType::RocList);
+        let payload = &self.RocList;
+
+        &payload
+    }
+
+    #[cfg(any(
+        target_arch = "aarch64",
+        target_arch = "x86_64"
+    ))]
+    /// Construct a tag named `RocResult`, with the appropriate payload
+    pub fn RocResult(arg0: u64, arg1: u64) -> Self {
+            let mut answer = Self {
+                RocResult: RocType_RocResult {
+                    f0: arg0,
+                    f1: arg1,
+                }
+            };
+
+            answer.set_discriminant(discriminant_RocType::RocResult);
+
+            answer
+    }
+
+    #[cfg(any(
+        target_arch = "aarch64",
+        target_arch = "x86_64"
+    ))]
+    /// Unsafely assume the given `RocType` has a `.discriminant()` of `RocResult` and convert it to `RocResult`'s payload.
+            /// (Always examine `.discriminant()` first to make sure this is the correct variant!)
+            /// Panics in debug builds if the `.discriminant()` doesn't return `RocResult`.
+            pub unsafe fn into_RocResult(self) -> (u64, u64) {
+                debug_assert_eq!(self.discriminant(), discriminant_RocType::RocResult);
+        let payload = self.RocResult;
+
+        (
+            payload.f0, 
+            payload.f1
+        )
+    }
+
+    #[cfg(any(
+        target_arch = "aarch64",
+        target_arch = "x86_64"
+    ))]
+    /// Unsafely assume the given `RocType` has a `.discriminant()` of `RocResult` and return its payload.
+            /// (Always examine `.discriminant()` first to make sure this is the correct variant!)
+            /// Panics in debug builds if the `.discriminant()` doesn't return `RocResult`.
+            pub unsafe fn as_RocResult(&self) -> (&u64, &u64) {
+                debug_assert_eq!(self.discriminant(), discriminant_RocType::RocResult);
+        let payload = &self.RocResult;
+
+        (
+            &payload.f0, 
+            &payload.f1
+        )
+    }
+
+    #[cfg(any(
+        target_arch = "aarch64",
+        target_arch = "x86_64"
+    ))]
+    /// Construct a tag named `RocSet`, with the appropriate payload
+    pub fn RocSet(arg: u64) -> Self {
+            let mut answer = Self {
+                RocSet: arg
+            };
+
+            answer.set_discriminant(discriminant_RocType::RocSet);
+
+            answer
+    }
+
+    #[cfg(any(
+        target_arch = "aarch64",
+        target_arch = "x86_64"
+    ))]
+    /// Unsafely assume the given `RocType` has a `.discriminant()` of `RocSet` and convert it to `RocSet`'s payload.
+            /// (Always examine `.discriminant()` first to make sure this is the correct variant!)
+            /// Panics in debug builds if the `.discriminant()` doesn't return `RocSet`.
+            pub unsafe fn into_RocSet(self) -> u64 {
+                debug_assert_eq!(self.discriminant(), discriminant_RocType::RocSet);
+        let payload = self.RocSet;
+
+        payload
+    }
+
+    #[cfg(any(
+        target_arch = "aarch64",
+        target_arch = "x86_64"
+    ))]
+    /// Unsafely assume the given `RocType` has a `.discriminant()` of `RocSet` and return its payload.
+            /// (Always examine `.discriminant()` first to make sure this is the correct variant!)
+            /// Panics in debug builds if the `.discriminant()` doesn't return `RocSet`.
+            pub unsafe fn as_RocSet(&self) -> &u64 {
+                debug_assert_eq!(self.discriminant(), discriminant_RocType::RocSet);
+        let payload = &self.RocSet;
+
+        &payload
+    }
+
+    #[cfg(any(
+        target_arch = "aarch64",
+        target_arch = "x86_64"
+    ))]
+    /// A tag named RocStr, which has no payload.
+    pub const RocStr: Self = unsafe {
+        let mut bytes = [0; core::mem::size_of::()];
+
+        bytes[96] = discriminant_RocType::RocStr as u8;
+
+        core::mem::transmute::<[u8; core::mem::size_of::()], RocType>(bytes)
+    };
+
+    #[cfg(any(
+        target_arch = "aarch64",
+        target_arch = "x86_64"
+    ))]
+    /// A tag named Unit, which has no payload.
+    pub const Unit: Self = unsafe {
+        let mut bytes = [0; core::mem::size_of::()];
+
+        bytes[96] = discriminant_RocType::Unit as u8;
+
+        core::mem::transmute::<[u8; core::mem::size_of::()], RocType>(bytes)
+    };
+}
+
+impl Drop for RocType {
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    fn drop(&mut self) {
+        // Drop the payloads
+                    match self.discriminant() {
+                discriminant_RocType::Bool => {}
+                discriminant_RocType::EmptyTagUnion => {}
+                discriminant_RocType::Function => unsafe { core::mem::ManuallyDrop::drop(&mut self.Function) },
+                discriminant_RocType::Num => {}
+                discriminant_RocType::RecursivePointer => {}
+                discriminant_RocType::RocBox => {}
+                discriminant_RocType::RocDict => {}
+                discriminant_RocType::RocList => {}
+                discriminant_RocType::RocResult => {}
+                discriminant_RocType::RocSet => {}
+                discriminant_RocType::RocStr => {}
+                discriminant_RocType::Struct => unsafe { core::mem::ManuallyDrop::drop(&mut self.Struct) },
+                discriminant_RocType::TagUnion => unsafe { core::mem::ManuallyDrop::drop(&mut self.TagUnion) },
+                discriminant_RocType::TagUnionPayload => unsafe { core::mem::ManuallyDrop::drop(&mut self.TagUnionPayload) },
+                discriminant_RocType::Unit => {}
+            }
+
+    }
+}
+
+impl Eq for RocType {}
+
+impl PartialEq for RocType {
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    fn eq(&self, other: &Self) -> bool {
+            if self.discriminant() != other.discriminant() {
+                return false;
+            }
+
+            unsafe {
+            match self.discriminant() {
+                discriminant_RocType::Bool => true,
+                discriminant_RocType::EmptyTagUnion => true,
+                discriminant_RocType::Function => self.Function == other.Function,
+                discriminant_RocType::Num => self.Num == other.Num,
+                discriminant_RocType::RecursivePointer => self.RecursivePointer == other.RecursivePointer,
+                discriminant_RocType::RocBox => self.RocBox == other.RocBox,
+                discriminant_RocType::RocDict => self.RocDict == other.RocDict,
+                discriminant_RocType::RocList => self.RocList == other.RocList,
+                discriminant_RocType::RocResult => self.RocResult == other.RocResult,
+                discriminant_RocType::RocSet => self.RocSet == other.RocSet,
+                discriminant_RocType::RocStr => true,
+                discriminant_RocType::Struct => self.Struct == other.Struct,
+                discriminant_RocType::TagUnion => self.TagUnion == other.TagUnion,
+                discriminant_RocType::TagUnionPayload => self.TagUnionPayload == other.TagUnionPayload,
+                discriminant_RocType::Unit => true,
+            }
+        }
+    }
+}
+
+impl PartialOrd for RocType {
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    fn partial_cmp(&self, other: &Self) -> Option {
+        match self.discriminant().partial_cmp(&other.discriminant()) {
+            Some(core::cmp::Ordering::Equal) => {}
+            not_eq => return not_eq,
+        }
+
+        unsafe {
+            match self.discriminant() {
+                discriminant_RocType::Bool => Some(core::cmp::Ordering::Equal),
+                discriminant_RocType::EmptyTagUnion => Some(core::cmp::Ordering::Equal),
+                discriminant_RocType::Function => self.Function.partial_cmp(&other.Function),
+                discriminant_RocType::Num => self.Num.partial_cmp(&other.Num),
+                discriminant_RocType::RecursivePointer => self.RecursivePointer.partial_cmp(&other.RecursivePointer),
+                discriminant_RocType::RocBox => self.RocBox.partial_cmp(&other.RocBox),
+                discriminant_RocType::RocDict => self.RocDict.partial_cmp(&other.RocDict),
+                discriminant_RocType::RocList => self.RocList.partial_cmp(&other.RocList),
+                discriminant_RocType::RocResult => self.RocResult.partial_cmp(&other.RocResult),
+                discriminant_RocType::RocSet => self.RocSet.partial_cmp(&other.RocSet),
+                discriminant_RocType::RocStr => Some(core::cmp::Ordering::Equal),
+                discriminant_RocType::Struct => self.Struct.partial_cmp(&other.Struct),
+                discriminant_RocType::TagUnion => self.TagUnion.partial_cmp(&other.TagUnion),
+                discriminant_RocType::TagUnionPayload => self.TagUnionPayload.partial_cmp(&other.TagUnionPayload),
+                discriminant_RocType::Unit => Some(core::cmp::Ordering::Equal),
+            }
+        }
+    }
+}
+
+impl Ord for RocType {
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
+            match self.discriminant().cmp(&other.discriminant()) {
+                core::cmp::Ordering::Equal => {}
+                not_eq => return not_eq,
+            }
+
+            unsafe {
+            match self.discriminant() {
+                discriminant_RocType::Bool => core::cmp::Ordering::Equal,
+                discriminant_RocType::EmptyTagUnion => core::cmp::Ordering::Equal,
+                discriminant_RocType::Function => self.Function.cmp(&other.Function),
+                discriminant_RocType::Num => self.Num.cmp(&other.Num),
+                discriminant_RocType::RecursivePointer => self.RecursivePointer.cmp(&other.RecursivePointer),
+                discriminant_RocType::RocBox => self.RocBox.cmp(&other.RocBox),
+                discriminant_RocType::RocDict => self.RocDict.cmp(&other.RocDict),
+                discriminant_RocType::RocList => self.RocList.cmp(&other.RocList),
+                discriminant_RocType::RocResult => self.RocResult.cmp(&other.RocResult),
+                discriminant_RocType::RocSet => self.RocSet.cmp(&other.RocSet),
+                discriminant_RocType::RocStr => core::cmp::Ordering::Equal,
+                discriminant_RocType::Struct => self.Struct.cmp(&other.Struct),
+                discriminant_RocType::TagUnion => self.TagUnion.cmp(&other.TagUnion),
+                discriminant_RocType::TagUnionPayload => self.TagUnionPayload.cmp(&other.TagUnionPayload),
+                discriminant_RocType::Unit => core::cmp::Ordering::Equal,
+            }
+        }
+    }
+}
+
+impl Clone for RocType {
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    fn clone(&self) -> Self {
+        let mut answer = unsafe {
+            match self.discriminant() {
+                discriminant_RocType::Bool => core::mem::transmute::<
+                    core::mem::MaybeUninit,
+                    RocType,
+                >(core::mem::MaybeUninit::uninit()),
+                discriminant_RocType::EmptyTagUnion => core::mem::transmute::<
+                    core::mem::MaybeUninit,
+                    RocType,
+                >(core::mem::MaybeUninit::uninit()),
+                discriminant_RocType::Function => Self {
+                    Function: self.Function.clone(),
+                },
+                discriminant_RocType::Num => Self {
+                    Num: self.Num.clone(),
+                },
+                discriminant_RocType::RecursivePointer => Self {
+                    RecursivePointer: self.RecursivePointer.clone(),
+                },
+                discriminant_RocType::RocBox => Self {
+                    RocBox: self.RocBox.clone(),
+                },
+                discriminant_RocType::RocDict => Self {
+                    RocDict: self.RocDict.clone(),
+                },
+                discriminant_RocType::RocList => Self {
+                    RocList: self.RocList.clone(),
+                },
+                discriminant_RocType::RocResult => Self {
+                    RocResult: self.RocResult.clone(),
+                },
+                discriminant_RocType::RocSet => Self {
+                    RocSet: self.RocSet.clone(),
+                },
+                discriminant_RocType::RocStr => core::mem::transmute::<
+                    core::mem::MaybeUninit,
+                    RocType,
+                >(core::mem::MaybeUninit::uninit()),
+                discriminant_RocType::Struct => Self {
+                    Struct: self.Struct.clone(),
+                },
+                discriminant_RocType::TagUnion => Self {
+                    TagUnion: self.TagUnion.clone(),
+                },
+                discriminant_RocType::TagUnionPayload => Self {
+                    TagUnionPayload: self.TagUnionPayload.clone(),
+                },
+                discriminant_RocType::Unit => core::mem::transmute::<
+                    core::mem::MaybeUninit,
+                    RocType,
+                >(core::mem::MaybeUninit::uninit()),
+            }
+
+        };
+
+        answer.set_discriminant(self.discriminant());
+
+        answer
+    }
+}
+
+impl core::hash::Hash for RocType {
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    fn hash(&self, state: &mut H) {        match self.discriminant() {
+            discriminant_RocType::Bool => discriminant_RocType::Bool.hash(state),
+            discriminant_RocType::EmptyTagUnion => discriminant_RocType::EmptyTagUnion.hash(state),
+            discriminant_RocType::Function => unsafe {
+                    discriminant_RocType::Function.hash(state);
+                    self.Function.hash(state);
+                },
+            discriminant_RocType::Num => unsafe {
+                    discriminant_RocType::Num.hash(state);
+                    self.Num.hash(state);
+                },
+            discriminant_RocType::RecursivePointer => unsafe {
+                    discriminant_RocType::RecursivePointer.hash(state);
+                    self.RecursivePointer.hash(state);
+                },
+            discriminant_RocType::RocBox => unsafe {
+                    discriminant_RocType::RocBox.hash(state);
+                    self.RocBox.hash(state);
+                },
+            discriminant_RocType::RocDict => unsafe {
+                    discriminant_RocType::RocDict.hash(state);
+                    self.RocDict.hash(state);
+                },
+            discriminant_RocType::RocList => unsafe {
+                    discriminant_RocType::RocList.hash(state);
+                    self.RocList.hash(state);
+                },
+            discriminant_RocType::RocResult => unsafe {
+                    discriminant_RocType::RocResult.hash(state);
+                    self.RocResult.hash(state);
+                },
+            discriminant_RocType::RocSet => unsafe {
+                    discriminant_RocType::RocSet.hash(state);
+                    self.RocSet.hash(state);
+                },
+            discriminant_RocType::RocStr => discriminant_RocType::RocStr.hash(state),
+            discriminant_RocType::Struct => unsafe {
+                    discriminant_RocType::Struct.hash(state);
+                    self.Struct.hash(state);
+                },
+            discriminant_RocType::TagUnion => unsafe {
+                    discriminant_RocType::TagUnion.hash(state);
+                    self.TagUnion.hash(state);
+                },
+            discriminant_RocType::TagUnionPayload => unsafe {
+                    discriminant_RocType::TagUnionPayload.hash(state);
+                    self.TagUnionPayload.hash(state);
+                },
+            discriminant_RocType::Unit => discriminant_RocType::Unit.hash(state),
+        }
+    }
+}
+
+impl core::fmt::Debug for RocType {
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
+        f.write_str("RocType::")?;
+
+        unsafe {
+            match self.discriminant() {
+                discriminant_RocType::Bool => f.write_str("Bool"),
+                discriminant_RocType::EmptyTagUnion => f.write_str("EmptyTagUnion"),
+                discriminant_RocType::Function => f.debug_tuple("Function")
+        .field(&*self.Function)
+        .finish(),
+                discriminant_RocType::Num => f.debug_tuple("Num")
+        .field(&self.Num)
+        .finish(),
+                discriminant_RocType::RecursivePointer => f.debug_tuple("RecursivePointer")
+        .field(&self.RecursivePointer)
+        .finish(),
+                discriminant_RocType::RocBox => f.debug_tuple("RocBox")
+        .field(&self.RocBox)
+        .finish(),
+                discriminant_RocType::RocDict => f.debug_tuple("RocDict")
+        .field(&(&self.RocDict).f0)
+.field(&(&self.RocDict).f1)
+        .finish(),
+                discriminant_RocType::RocList => f.debug_tuple("RocList")
+        .field(&self.RocList)
+        .finish(),
+                discriminant_RocType::RocResult => f.debug_tuple("RocResult")
+        .field(&(&self.RocResult).f0)
+.field(&(&self.RocResult).f1)
+        .finish(),
+                discriminant_RocType::RocSet => f.debug_tuple("RocSet")
+        .field(&self.RocSet)
+        .finish(),
+                discriminant_RocType::RocStr => f.write_str("RocStr"),
+                discriminant_RocType::Struct => f.debug_tuple("Struct")
+        .field(&*self.Struct)
+        .finish(),
+                discriminant_RocType::TagUnion => f.debug_tuple("TagUnion")
+        .field(&*self.TagUnion)
+        .finish(),
+                discriminant_RocType::TagUnionPayload => f.debug_tuple("TagUnionPayload")
+        .field(&*self.TagUnionPayload)
+        .finish(),
+                discriminant_RocType::Unit => f.write_str("Unit"),
+            }
+        }
+    }
+}
+
+impl RocTagUnion {
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "wasm32",
+        target_arch = "x86"
+    ))]
+    /// Returns which variant this tag union holds. Note that this never includes a payload!
+    pub fn discriminant(&self) -> discriminant_RocTagUnion {
+        unsafe {
+            let bytes = core::mem::transmute::<&Self, &[u8; core::mem::size_of::()]>(self);
+
+            core::mem::transmute::(*bytes.as_ptr().add(44))
+        }
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "wasm32",
+        target_arch = "x86"
+    ))]
+    /// Internal helper
+    fn set_discriminant(&mut self, discriminant: discriminant_RocTagUnion) {
+        let discriminant_ptr: *mut discriminant_RocTagUnion = (self as *mut RocTagUnion).cast();
+
+        unsafe {
+            *(discriminant_ptr.add(44)) = discriminant;
+        }
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    /// Construct a tag named `Enumeration`, with the appropriate payload
+    pub fn Enumeration(arg0: R4) -> Self {
+            let mut answer = Self {
+                Enumeration: core::mem::ManuallyDrop::new(arg0)
+            };
+
+            answer.set_discriminant(discriminant_RocTagUnion::Enumeration);
+
+            answer
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    /// Unsafely assume the given `RocTagUnion` has a `.discriminant()` of `Enumeration` and convert it to `Enumeration`'s payload.
+            /// (Always examine `.discriminant()` first to make sure this is the correct variant!)
+            /// Panics in debug builds if the `.discriminant()` doesn't return `Enumeration`.
+            pub unsafe fn into_Enumeration(mut self) -> R4 {
+                debug_assert_eq!(self.discriminant(), discriminant_RocTagUnion::Enumeration);
+        let payload = {
+            let mut uninitialized = core::mem::MaybeUninit::uninit();
+            let swapped = unsafe {
+                core::mem::replace(
+                    &mut self.Enumeration,
+                    core::mem::ManuallyDrop::new(uninitialized.assume_init()),
+                )
+            };
+
+            core::mem::forget(self);
+
+            core::mem::ManuallyDrop::into_inner(swapped)
+        };
+
+        
+        payload
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    /// Unsafely assume the given `RocTagUnion` has a `.discriminant()` of `Enumeration` and return its payload.
+            /// (Always examine `.discriminant()` first to make sure this is the correct variant!)
+            /// Panics in debug builds if the `.discriminant()` doesn't return `Enumeration`.
+            pub unsafe fn as_Enumeration(&self) -> &R4 {
+                debug_assert_eq!(self.discriminant(), discriminant_RocTagUnion::Enumeration);
+        let payload = &self.Enumeration;
+
+        
+        payload
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    /// Construct a tag named `NonNullableUnwrapped`, with the appropriate payload
+    pub fn NonNullableUnwrapped(arg0: R5) -> Self {
+            let mut answer = Self {
+                NonNullableUnwrapped: core::mem::ManuallyDrop::new(arg0)
+            };
+
+            answer.set_discriminant(discriminant_RocTagUnion::NonNullableUnwrapped);
+
+            answer
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    /// Unsafely assume the given `RocTagUnion` has a `.discriminant()` of `NonNullableUnwrapped` and convert it to `NonNullableUnwrapped`'s payload.
+            /// (Always examine `.discriminant()` first to make sure this is the correct variant!)
+            /// Panics in debug builds if the `.discriminant()` doesn't return `NonNullableUnwrapped`.
+            pub unsafe fn into_NonNullableUnwrapped(mut self) -> R5 {
+                debug_assert_eq!(self.discriminant(), discriminant_RocTagUnion::NonNullableUnwrapped);
+        let payload = {
+            let mut uninitialized = core::mem::MaybeUninit::uninit();
+            let swapped = unsafe {
+                core::mem::replace(
+                    &mut self.NonNullableUnwrapped,
+                    core::mem::ManuallyDrop::new(uninitialized.assume_init()),
+                )
+            };
+
+            core::mem::forget(self);
+
+            core::mem::ManuallyDrop::into_inner(swapped)
+        };
+
+        
+        payload
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    /// Unsafely assume the given `RocTagUnion` has a `.discriminant()` of `NonNullableUnwrapped` and return its payload.
+            /// (Always examine `.discriminant()` first to make sure this is the correct variant!)
+            /// Panics in debug builds if the `.discriminant()` doesn't return `NonNullableUnwrapped`.
+            pub unsafe fn as_NonNullableUnwrapped(&self) -> &R5 {
+                debug_assert_eq!(self.discriminant(), discriminant_RocTagUnion::NonNullableUnwrapped);
+        let payload = &self.NonNullableUnwrapped;
+
+        
+        payload
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    /// Construct a tag named `NonRecursive`, with the appropriate payload
+    pub fn NonRecursive(arg0: R6) -> Self {
+            let mut answer = Self {
+                NonRecursive: core::mem::ManuallyDrop::new(arg0)
+            };
+
+            answer.set_discriminant(discriminant_RocTagUnion::NonRecursive);
+
+            answer
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    /// Unsafely assume the given `RocTagUnion` has a `.discriminant()` of `NonRecursive` and convert it to `NonRecursive`'s payload.
+            /// (Always examine `.discriminant()` first to make sure this is the correct variant!)
+            /// Panics in debug builds if the `.discriminant()` doesn't return `NonRecursive`.
+            pub unsafe fn into_NonRecursive(mut self) -> R6 {
+                debug_assert_eq!(self.discriminant(), discriminant_RocTagUnion::NonRecursive);
+        let payload = {
+            let mut uninitialized = core::mem::MaybeUninit::uninit();
+            let swapped = unsafe {
+                core::mem::replace(
+                    &mut self.NonRecursive,
+                    core::mem::ManuallyDrop::new(uninitialized.assume_init()),
+                )
+            };
+
+            core::mem::forget(self);
+
+            core::mem::ManuallyDrop::into_inner(swapped)
+        };
+
+        
+        payload
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    /// Unsafely assume the given `RocTagUnion` has a `.discriminant()` of `NonRecursive` and return its payload.
+            /// (Always examine `.discriminant()` first to make sure this is the correct variant!)
+            /// Panics in debug builds if the `.discriminant()` doesn't return `NonRecursive`.
+            pub unsafe fn as_NonRecursive(&self) -> &R6 {
+                debug_assert_eq!(self.discriminant(), discriminant_RocTagUnion::NonRecursive);
+        let payload = &self.NonRecursive;
+
+        
+        payload
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    /// Construct a tag named `NullableUnwrapped`, with the appropriate payload
+    pub fn NullableUnwrapped(arg0: R8) -> Self {
+            let mut answer = Self {
+                NullableUnwrapped: core::mem::ManuallyDrop::new(arg0)
+            };
+
+            answer.set_discriminant(discriminant_RocTagUnion::NullableUnwrapped);
+
+            answer
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    /// Unsafely assume the given `RocTagUnion` has a `.discriminant()` of `NullableUnwrapped` and convert it to `NullableUnwrapped`'s payload.
+            /// (Always examine `.discriminant()` first to make sure this is the correct variant!)
+            /// Panics in debug builds if the `.discriminant()` doesn't return `NullableUnwrapped`.
+            pub unsafe fn into_NullableUnwrapped(mut self) -> R8 {
+                debug_assert_eq!(self.discriminant(), discriminant_RocTagUnion::NullableUnwrapped);
+        let payload = {
+            let mut uninitialized = core::mem::MaybeUninit::uninit();
+            let swapped = unsafe {
+                core::mem::replace(
+                    &mut self.NullableUnwrapped,
+                    core::mem::ManuallyDrop::new(uninitialized.assume_init()),
+                )
+            };
+
+            core::mem::forget(self);
+
+            core::mem::ManuallyDrop::into_inner(swapped)
+        };
+
+        
+        payload
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    /// Unsafely assume the given `RocTagUnion` has a `.discriminant()` of `NullableUnwrapped` and return its payload.
+            /// (Always examine `.discriminant()` first to make sure this is the correct variant!)
+            /// Panics in debug builds if the `.discriminant()` doesn't return `NullableUnwrapped`.
+            pub unsafe fn as_NullableUnwrapped(&self) -> &R8 {
+                debug_assert_eq!(self.discriminant(), discriminant_RocTagUnion::NullableUnwrapped);
+        let payload = &self.NullableUnwrapped;
+
+        
+        payload
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    /// Construct a tag named `NullableWrapped`, with the appropriate payload
+    pub fn NullableWrapped(arg0: R9) -> Self {
+            let mut answer = Self {
+                NullableWrapped: core::mem::ManuallyDrop::new(arg0)
+            };
+
+            answer.set_discriminant(discriminant_RocTagUnion::NullableWrapped);
+
+            answer
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    /// Unsafely assume the given `RocTagUnion` has a `.discriminant()` of `NullableWrapped` and convert it to `NullableWrapped`'s payload.
+            /// (Always examine `.discriminant()` first to make sure this is the correct variant!)
+            /// Panics in debug builds if the `.discriminant()` doesn't return `NullableWrapped`.
+            pub unsafe fn into_NullableWrapped(mut self) -> R9 {
+                debug_assert_eq!(self.discriminant(), discriminant_RocTagUnion::NullableWrapped);
+        let payload = {
+            let mut uninitialized = core::mem::MaybeUninit::uninit();
+            let swapped = unsafe {
+                core::mem::replace(
+                    &mut self.NullableWrapped,
+                    core::mem::ManuallyDrop::new(uninitialized.assume_init()),
+                )
+            };
+
+            core::mem::forget(self);
+
+            core::mem::ManuallyDrop::into_inner(swapped)
+        };
+
+        
+        payload
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    /// Unsafely assume the given `RocTagUnion` has a `.discriminant()` of `NullableWrapped` and return its payload.
+            /// (Always examine `.discriminant()` first to make sure this is the correct variant!)
+            /// Panics in debug builds if the `.discriminant()` doesn't return `NullableWrapped`.
+            pub unsafe fn as_NullableWrapped(&self) -> &R9 {
+                debug_assert_eq!(self.discriminant(), discriminant_RocTagUnion::NullableWrapped);
+        let payload = &self.NullableWrapped;
+
+        
+        payload
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    /// Construct a tag named `Recursive`, with the appropriate payload
+    pub fn Recursive(arg0: R11) -> Self {
+            let mut answer = Self {
+                Recursive: core::mem::ManuallyDrop::new(arg0)
+            };
+
+            answer.set_discriminant(discriminant_RocTagUnion::Recursive);
+
+            answer
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    /// Unsafely assume the given `RocTagUnion` has a `.discriminant()` of `Recursive` and convert it to `Recursive`'s payload.
+            /// (Always examine `.discriminant()` first to make sure this is the correct variant!)
+            /// Panics in debug builds if the `.discriminant()` doesn't return `Recursive`.
+            pub unsafe fn into_Recursive(mut self) -> R11 {
+                debug_assert_eq!(self.discriminant(), discriminant_RocTagUnion::Recursive);
+        let payload = {
+            let mut uninitialized = core::mem::MaybeUninit::uninit();
+            let swapped = unsafe {
+                core::mem::replace(
+                    &mut self.Recursive,
+                    core::mem::ManuallyDrop::new(uninitialized.assume_init()),
+                )
+            };
+
+            core::mem::forget(self);
+
+            core::mem::ManuallyDrop::into_inner(swapped)
+        };
+
+        
+        payload
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    /// Unsafely assume the given `RocTagUnion` has a `.discriminant()` of `Recursive` and return its payload.
+            /// (Always examine `.discriminant()` first to make sure this is the correct variant!)
+            /// Panics in debug builds if the `.discriminant()` doesn't return `Recursive`.
+            pub unsafe fn as_Recursive(&self) -> &R11 {
+                debug_assert_eq!(self.discriminant(), discriminant_RocTagUnion::Recursive);
+        let payload = &self.Recursive;
+
+        
+        payload
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    /// Construct a tag named `SingleTagStruct`, with the appropriate payload
+    pub fn SingleTagStruct(arg0: R13) -> Self {
+            let mut answer = Self {
+                SingleTagStruct: core::mem::ManuallyDrop::new(arg0)
+            };
+
+            answer.set_discriminant(discriminant_RocTagUnion::SingleTagStruct);
+
+            answer
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    /// Unsafely assume the given `RocTagUnion` has a `.discriminant()` of `SingleTagStruct` and convert it to `SingleTagStruct`'s payload.
+            /// (Always examine `.discriminant()` first to make sure this is the correct variant!)
+            /// Panics in debug builds if the `.discriminant()` doesn't return `SingleTagStruct`.
+            pub unsafe fn into_SingleTagStruct(mut self) -> R13 {
+                debug_assert_eq!(self.discriminant(), discriminant_RocTagUnion::SingleTagStruct);
+        let payload = {
+            let mut uninitialized = core::mem::MaybeUninit::uninit();
+            let swapped = unsafe {
+                core::mem::replace(
+                    &mut self.SingleTagStruct,
+                    core::mem::ManuallyDrop::new(uninitialized.assume_init()),
+                )
+            };
+
+            core::mem::forget(self);
+
+            core::mem::ManuallyDrop::into_inner(swapped)
+        };
+
+        
+        payload
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    /// Unsafely assume the given `RocTagUnion` has a `.discriminant()` of `SingleTagStruct` and return its payload.
+            /// (Always examine `.discriminant()` first to make sure this is the correct variant!)
+            /// Panics in debug builds if the `.discriminant()` doesn't return `SingleTagStruct`.
+            pub unsafe fn as_SingleTagStruct(&self) -> &R13 {
+                debug_assert_eq!(self.discriminant(), discriminant_RocTagUnion::SingleTagStruct);
+        let payload = &self.SingleTagStruct;
+
+        
+        payload
+    }
+
+    #[cfg(any(
+        target_arch = "aarch64",
+        target_arch = "x86_64"
+    ))]
+    /// Returns which variant this tag union holds. Note that this never includes a payload!
+    pub fn discriminant(&self) -> discriminant_RocTagUnion {
+        unsafe {
+            let bytes = core::mem::transmute::<&Self, &[u8; core::mem::size_of::()]>(self);
+
+            core::mem::transmute::(*bytes.as_ptr().add(88))
+        }
+    }
+
+    #[cfg(any(
+        target_arch = "aarch64",
+        target_arch = "x86_64"
+    ))]
+    /// Internal helper
+    fn set_discriminant(&mut self, discriminant: discriminant_RocTagUnion) {
+        let discriminant_ptr: *mut discriminant_RocTagUnion = (self as *mut RocTagUnion).cast();
+
+        unsafe {
+            *(discriminant_ptr.add(88)) = discriminant;
+        }
+    }
+}
+
+impl Drop for RocTagUnion {
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    fn drop(&mut self) {
+        // Drop the payloads
+                    match self.discriminant() {
+                discriminant_RocTagUnion::Enumeration => unsafe { core::mem::ManuallyDrop::drop(&mut self.Enumeration) },
+                discriminant_RocTagUnion::NonNullableUnwrapped => unsafe { core::mem::ManuallyDrop::drop(&mut self.NonNullableUnwrapped) },
+                discriminant_RocTagUnion::NonRecursive => unsafe { core::mem::ManuallyDrop::drop(&mut self.NonRecursive) },
+                discriminant_RocTagUnion::NullableUnwrapped => unsafe { core::mem::ManuallyDrop::drop(&mut self.NullableUnwrapped) },
+                discriminant_RocTagUnion::NullableWrapped => unsafe { core::mem::ManuallyDrop::drop(&mut self.NullableWrapped) },
+                discriminant_RocTagUnion::Recursive => unsafe { core::mem::ManuallyDrop::drop(&mut self.Recursive) },
+                discriminant_RocTagUnion::SingleTagStruct => unsafe { core::mem::ManuallyDrop::drop(&mut self.SingleTagStruct) },
+            }
+
+    }
+}
+
+impl Eq for RocTagUnion {}
+
+impl PartialEq for RocTagUnion {
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    fn eq(&self, other: &Self) -> bool {
+            if self.discriminant() != other.discriminant() {
+                return false;
+            }
+
+            unsafe {
+            match self.discriminant() {
+                discriminant_RocTagUnion::Enumeration => self.Enumeration == other.Enumeration,
+                discriminant_RocTagUnion::NonNullableUnwrapped => self.NonNullableUnwrapped == other.NonNullableUnwrapped,
+                discriminant_RocTagUnion::NonRecursive => self.NonRecursive == other.NonRecursive,
+                discriminant_RocTagUnion::NullableUnwrapped => self.NullableUnwrapped == other.NullableUnwrapped,
+                discriminant_RocTagUnion::NullableWrapped => self.NullableWrapped == other.NullableWrapped,
+                discriminant_RocTagUnion::Recursive => self.Recursive == other.Recursive,
+                discriminant_RocTagUnion::SingleTagStruct => self.SingleTagStruct == other.SingleTagStruct,
+            }
+        }
+    }
+}
+
+impl PartialOrd for RocTagUnion {
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    fn partial_cmp(&self, other: &Self) -> Option {
+        match self.discriminant().partial_cmp(&other.discriminant()) {
+            Some(core::cmp::Ordering::Equal) => {}
+            not_eq => return not_eq,
+        }
+
+        unsafe {
+            match self.discriminant() {
+                discriminant_RocTagUnion::Enumeration => self.Enumeration.partial_cmp(&other.Enumeration),
+                discriminant_RocTagUnion::NonNullableUnwrapped => self.NonNullableUnwrapped.partial_cmp(&other.NonNullableUnwrapped),
+                discriminant_RocTagUnion::NonRecursive => self.NonRecursive.partial_cmp(&other.NonRecursive),
+                discriminant_RocTagUnion::NullableUnwrapped => self.NullableUnwrapped.partial_cmp(&other.NullableUnwrapped),
+                discriminant_RocTagUnion::NullableWrapped => self.NullableWrapped.partial_cmp(&other.NullableWrapped),
+                discriminant_RocTagUnion::Recursive => self.Recursive.partial_cmp(&other.Recursive),
+                discriminant_RocTagUnion::SingleTagStruct => self.SingleTagStruct.partial_cmp(&other.SingleTagStruct),
+            }
+        }
+    }
+}
+
+impl Ord for RocTagUnion {
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
+            match self.discriminant().cmp(&other.discriminant()) {
+                core::cmp::Ordering::Equal => {}
+                not_eq => return not_eq,
+            }
+
+            unsafe {
+            match self.discriminant() {
+                discriminant_RocTagUnion::Enumeration => self.Enumeration.cmp(&other.Enumeration),
+                discriminant_RocTagUnion::NonNullableUnwrapped => self.NonNullableUnwrapped.cmp(&other.NonNullableUnwrapped),
+                discriminant_RocTagUnion::NonRecursive => self.NonRecursive.cmp(&other.NonRecursive),
+                discriminant_RocTagUnion::NullableUnwrapped => self.NullableUnwrapped.cmp(&other.NullableUnwrapped),
+                discriminant_RocTagUnion::NullableWrapped => self.NullableWrapped.cmp(&other.NullableWrapped),
+                discriminant_RocTagUnion::Recursive => self.Recursive.cmp(&other.Recursive),
+                discriminant_RocTagUnion::SingleTagStruct => self.SingleTagStruct.cmp(&other.SingleTagStruct),
+            }
+        }
+    }
+}
+
+impl Clone for RocTagUnion {
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    fn clone(&self) -> Self {
+        let mut answer = unsafe {
+            match self.discriminant() {
+                discriminant_RocTagUnion::Enumeration => Self {
+                    Enumeration: self.Enumeration.clone(),
+                },
+                discriminant_RocTagUnion::NonNullableUnwrapped => Self {
+                    NonNullableUnwrapped: self.NonNullableUnwrapped.clone(),
+                },
+                discriminant_RocTagUnion::NonRecursive => Self {
+                    NonRecursive: self.NonRecursive.clone(),
+                },
+                discriminant_RocTagUnion::NullableUnwrapped => Self {
+                    NullableUnwrapped: self.NullableUnwrapped.clone(),
+                },
+                discriminant_RocTagUnion::NullableWrapped => Self {
+                    NullableWrapped: self.NullableWrapped.clone(),
+                },
+                discriminant_RocTagUnion::Recursive => Self {
+                    Recursive: self.Recursive.clone(),
+                },
+                discriminant_RocTagUnion::SingleTagStruct => Self {
+                    SingleTagStruct: self.SingleTagStruct.clone(),
+                },
+            }
+
+        };
+
+        answer.set_discriminant(self.discriminant());
+
+        answer
+    }
+}
+
+impl core::hash::Hash for RocTagUnion {
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    fn hash(&self, state: &mut H) {        match self.discriminant() {
+            discriminant_RocTagUnion::Enumeration => unsafe {
+                    discriminant_RocTagUnion::Enumeration.hash(state);
+                    self.Enumeration.hash(state);
+                },
+            discriminant_RocTagUnion::NonNullableUnwrapped => unsafe {
+                    discriminant_RocTagUnion::NonNullableUnwrapped.hash(state);
+                    self.NonNullableUnwrapped.hash(state);
+                },
+            discriminant_RocTagUnion::NonRecursive => unsafe {
+                    discriminant_RocTagUnion::NonRecursive.hash(state);
+                    self.NonRecursive.hash(state);
+                },
+            discriminant_RocTagUnion::NullableUnwrapped => unsafe {
+                    discriminant_RocTagUnion::NullableUnwrapped.hash(state);
+                    self.NullableUnwrapped.hash(state);
+                },
+            discriminant_RocTagUnion::NullableWrapped => unsafe {
+                    discriminant_RocTagUnion::NullableWrapped.hash(state);
+                    self.NullableWrapped.hash(state);
+                },
+            discriminant_RocTagUnion::Recursive => unsafe {
+                    discriminant_RocTagUnion::Recursive.hash(state);
+                    self.Recursive.hash(state);
+                },
+            discriminant_RocTagUnion::SingleTagStruct => unsafe {
+                    discriminant_RocTagUnion::SingleTagStruct.hash(state);
+                    self.SingleTagStruct.hash(state);
+                },
+        }
+    }
+}
+
+impl core::fmt::Debug for RocTagUnion {
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
+        f.write_str("RocTagUnion::")?;
+
+        unsafe {
+            match self.discriminant() {
+                discriminant_RocTagUnion::Enumeration => f.debug_tuple("Enumeration")
+        .field(&*self.Enumeration)
+        .finish(),
+                discriminant_RocTagUnion::NonNullableUnwrapped => f.debug_tuple("NonNullableUnwrapped")
+        .field(&*self.NonNullableUnwrapped)
+        .finish(),
+                discriminant_RocTagUnion::NonRecursive => f.debug_tuple("NonRecursive")
+        .field(&*self.NonRecursive)
+        .finish(),
+                discriminant_RocTagUnion::NullableUnwrapped => f.debug_tuple("NullableUnwrapped")
+        .field(&*self.NullableUnwrapped)
+        .finish(),
+                discriminant_RocTagUnion::NullableWrapped => f.debug_tuple("NullableWrapped")
+        .field(&*self.NullableWrapped)
+        .finish(),
+                discriminant_RocTagUnion::Recursive => f.debug_tuple("Recursive")
+        .field(&*self.Recursive)
+        .finish(),
+                discriminant_RocTagUnion::SingleTagStruct => f.debug_tuple("SingleTagStruct")
+        .field(&*self.SingleTagStruct)
+        .finish(),
+            }
+        }
+    }
+}
+
+impl U4 {
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "wasm32",
+        target_arch = "x86"
+    ))]
+    /// Returns which variant this tag union holds. Note that this never includes a payload!
+    pub fn discriminant(&self) -> discriminant_U4 {
+        unsafe {
+            let bytes = core::mem::transmute::<&Self, &[u8; core::mem::size_of::()]>(self);
+
+            core::mem::transmute::(*bytes.as_ptr().add(4))
+        }
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "wasm32",
+        target_arch = "x86"
+    ))]
+    /// Internal helper
+    fn set_discriminant(&mut self, discriminant: discriminant_U4) {
+        let discriminant_ptr: *mut discriminant_U4 = (self as *mut U4).cast();
+
+        unsafe {
+            *(discriminant_ptr.add(4)) = discriminant;
+        }
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "wasm32",
+        target_arch = "x86"
+    ))]
+    /// A tag named None, which has no payload.
+    pub const None: Self = unsafe {
+        let mut bytes = [0; core::mem::size_of::()];
+
+        bytes[4] = discriminant_U4::None as u8;
+
+        core::mem::transmute::<[u8; core::mem::size_of::()], U4>(bytes)
+    };
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    /// Other `into_` methods return a payload, but since the None tag
+    /// has no payload, this does nothing and is only here for completeness.
+    pub fn into_None(self) {
+        ()
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    /// Other `as` methods return a payload, but since the None tag
+    /// has no payload, this does nothing and is only here for completeness.
+    pub fn as_None(&self) {
+        ()
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "wasm32",
+        target_arch = "x86"
+    ))]
+    /// Construct a tag named `Some`, with the appropriate payload
+    pub fn Some(arg: u32) -> Self {
+            let mut answer = Self {
+                Some: arg
+            };
+
+            answer.set_discriminant(discriminant_U4::Some);
+
+            answer
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "wasm32",
+        target_arch = "x86"
+    ))]
+    /// Unsafely assume the given `U4` has a `.discriminant()` of `Some` and convert it to `Some`'s payload.
+            /// (Always examine `.discriminant()` first to make sure this is the correct variant!)
+            /// Panics in debug builds if the `.discriminant()` doesn't return `Some`.
+            pub unsafe fn into_Some(self) -> u32 {
+                debug_assert_eq!(self.discriminant(), discriminant_U4::Some);
+        let payload = self.Some;
+
+        payload
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "wasm32",
+        target_arch = "x86"
+    ))]
+    /// Unsafely assume the given `U4` has a `.discriminant()` of `Some` and return its payload.
+            /// (Always examine `.discriminant()` first to make sure this is the correct variant!)
+            /// Panics in debug builds if the `.discriminant()` doesn't return `Some`.
+            pub unsafe fn as_Some(&self) -> &u32 {
+                debug_assert_eq!(self.discriminant(), discriminant_U4::Some);
+        let payload = &self.Some;
+
+        &payload
+    }
+
+    #[cfg(any(
+        target_arch = "aarch64",
+        target_arch = "x86_64"
+    ))]
+    /// Returns which variant this tag union holds. Note that this never includes a payload!
+    pub fn discriminant(&self) -> discriminant_U4 {
+        unsafe {
+            let bytes = core::mem::transmute::<&Self, &[u8; core::mem::size_of::()]>(self);
+
+            core::mem::transmute::(*bytes.as_ptr().add(8))
+        }
+    }
+
+    #[cfg(any(
+        target_arch = "aarch64",
+        target_arch = "x86_64"
+    ))]
+    /// Internal helper
+    fn set_discriminant(&mut self, discriminant: discriminant_U4) {
+        let discriminant_ptr: *mut discriminant_U4 = (self as *mut U4).cast();
+
+        unsafe {
+            *(discriminant_ptr.add(8)) = discriminant;
+        }
+    }
+
+    #[cfg(any(
+        target_arch = "aarch64",
+        target_arch = "x86_64"
+    ))]
+    /// A tag named None, which has no payload.
+    pub const None: Self = unsafe {
+        let mut bytes = [0; core::mem::size_of::()];
+
+        bytes[8] = discriminant_U4::None as u8;
+
+        core::mem::transmute::<[u8; core::mem::size_of::()], U4>(bytes)
+    };
+
+    #[cfg(any(
+        target_arch = "aarch64",
+        target_arch = "x86_64"
+    ))]
+    /// Construct a tag named `Some`, with the appropriate payload
+    pub fn Some(arg: u64) -> Self {
+            let mut answer = Self {
+                Some: arg
+            };
+
+            answer.set_discriminant(discriminant_U4::Some);
+
+            answer
+    }
+
+    #[cfg(any(
+        target_arch = "aarch64",
+        target_arch = "x86_64"
+    ))]
+    /// Unsafely assume the given `U4` has a `.discriminant()` of `Some` and convert it to `Some`'s payload.
+            /// (Always examine `.discriminant()` first to make sure this is the correct variant!)
+            /// Panics in debug builds if the `.discriminant()` doesn't return `Some`.
+            pub unsafe fn into_Some(self) -> u64 {
+                debug_assert_eq!(self.discriminant(), discriminant_U4::Some);
+        let payload = self.Some;
+
+        payload
+    }
+
+    #[cfg(any(
+        target_arch = "aarch64",
+        target_arch = "x86_64"
+    ))]
+    /// Unsafely assume the given `U4` has a `.discriminant()` of `Some` and return its payload.
+            /// (Always examine `.discriminant()` first to make sure this is the correct variant!)
+            /// Panics in debug builds if the `.discriminant()` doesn't return `Some`.
+            pub unsafe fn as_Some(&self) -> &u64 {
+                debug_assert_eq!(self.discriminant(), discriminant_U4::Some);
+        let payload = &self.Some;
+
+        &payload
+    }
+}
+
+impl Eq for U4 {}
+
+impl PartialEq for U4 {
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    fn eq(&self, other: &Self) -> bool {
+            if self.discriminant() != other.discriminant() {
+                return false;
+            }
+
+            unsafe {
+            match self.discriminant() {
+                discriminant_U4::None => true,
+                discriminant_U4::Some => self.Some == other.Some,
+            }
+        }
+    }
+}
+
+impl PartialOrd for U4 {
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    fn partial_cmp(&self, other: &Self) -> Option {
+        match self.discriminant().partial_cmp(&other.discriminant()) {
+            Some(core::cmp::Ordering::Equal) => {}
+            not_eq => return not_eq,
+        }
+
+        unsafe {
+            match self.discriminant() {
+                discriminant_U4::None => Some(core::cmp::Ordering::Equal),
+                discriminant_U4::Some => self.Some.partial_cmp(&other.Some),
+            }
+        }
+    }
+}
+
+impl Ord for U4 {
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
+            match self.discriminant().cmp(&other.discriminant()) {
+                core::cmp::Ordering::Equal => {}
+                not_eq => return not_eq,
+            }
+
+            unsafe {
+            match self.discriminant() {
+                discriminant_U4::None => core::cmp::Ordering::Equal,
+                discriminant_U4::Some => self.Some.cmp(&other.Some),
+            }
+        }
+    }
+}
+
+impl Copy for U4 {}
+
+impl Clone for U4 {
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    fn clone(&self) -> Self {
+        let mut answer = unsafe {
+            match self.discriminant() {
+                discriminant_U4::None => core::mem::transmute::<
+                    core::mem::MaybeUninit,
+                    U4,
+                >(core::mem::MaybeUninit::uninit()),
+                discriminant_U4::Some => Self {
+                    Some: self.Some.clone(),
+                },
+            }
+
+        };
+
+        answer.set_discriminant(self.discriminant());
+
+        answer
+    }
+}
+
+impl core::hash::Hash for U4 {
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    fn hash(&self, state: &mut H) {        match self.discriminant() {
+            discriminant_U4::None => discriminant_U4::None.hash(state),
+            discriminant_U4::Some => unsafe {
+                    discriminant_U4::Some.hash(state);
+                    self.Some.hash(state);
+                },
+        }
+    }
+}
+
+impl core::fmt::Debug for U4 {
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
+        f.write_str("U4::")?;
+
+        unsafe {
+            match self.discriminant() {
+                discriminant_U4::None => f.write_str("None"),
+                discriminant_U4::Some => f.debug_tuple("Some")
+        .field(&self.Some)
+        .finish(),
+            }
+        }
+    }
+}
+
+impl U3 {
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "wasm32",
+        target_arch = "x86"
+    ))]
+    /// Returns which variant this tag union holds. Note that this never includes a payload!
+    pub fn discriminant(&self) -> discriminant_U3 {
+        unsafe {
+            let bytes = core::mem::transmute::<&Self, &[u8; core::mem::size_of::()]>(self);
+
+            core::mem::transmute::(*bytes.as_ptr().add(4))
+        }
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "wasm32",
+        target_arch = "x86"
+    ))]
+    /// Internal helper
+    fn set_discriminant(&mut self, discriminant: discriminant_U3) {
+        let discriminant_ptr: *mut discriminant_U3 = (self as *mut U3).cast();
+
+        unsafe {
+            *(discriminant_ptr.add(4)) = discriminant;
+        }
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "wasm32",
+        target_arch = "x86"
+    ))]
+    /// A tag named None, which has no payload.
+    pub const None: Self = unsafe {
+        let mut bytes = [0; core::mem::size_of::()];
+
+        bytes[4] = discriminant_U3::None as u8;
+
+        core::mem::transmute::<[u8; core::mem::size_of::()], U3>(bytes)
+    };
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    /// Other `into_` methods return a payload, but since the None tag
+    /// has no payload, this does nothing and is only here for completeness.
+    pub fn into_None(self) {
+        ()
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    /// Other `as` methods return a payload, but since the None tag
+    /// has no payload, this does nothing and is only here for completeness.
+    pub fn as_None(&self) {
+        ()
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "wasm32",
+        target_arch = "x86"
+    ))]
+    /// Construct a tag named `Some`, with the appropriate payload
+    pub fn Some(arg: u32) -> Self {
+            let mut answer = Self {
+                Some: arg
+            };
+
+            answer.set_discriminant(discriminant_U3::Some);
+
+            answer
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "wasm32",
+        target_arch = "x86"
+    ))]
+    /// Unsafely assume the given `U3` has a `.discriminant()` of `Some` and convert it to `Some`'s payload.
+            /// (Always examine `.discriminant()` first to make sure this is the correct variant!)
+            /// Panics in debug builds if the `.discriminant()` doesn't return `Some`.
+            pub unsafe fn into_Some(self) -> u32 {
+                debug_assert_eq!(self.discriminant(), discriminant_U3::Some);
+        let payload = self.Some;
+
+        payload
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "wasm32",
+        target_arch = "x86"
+    ))]
+    /// Unsafely assume the given `U3` has a `.discriminant()` of `Some` and return its payload.
+            /// (Always examine `.discriminant()` first to make sure this is the correct variant!)
+            /// Panics in debug builds if the `.discriminant()` doesn't return `Some`.
+            pub unsafe fn as_Some(&self) -> &u32 {
+                debug_assert_eq!(self.discriminant(), discriminant_U3::Some);
+        let payload = &self.Some;
+
+        &payload
+    }
+
+    #[cfg(any(
+        target_arch = "aarch64",
+        target_arch = "x86_64"
+    ))]
+    /// Returns which variant this tag union holds. Note that this never includes a payload!
+    pub fn discriminant(&self) -> discriminant_U3 {
+        unsafe {
+            let bytes = core::mem::transmute::<&Self, &[u8; core::mem::size_of::()]>(self);
+
+            core::mem::transmute::(*bytes.as_ptr().add(8))
+        }
+    }
+
+    #[cfg(any(
+        target_arch = "aarch64",
+        target_arch = "x86_64"
+    ))]
+    /// Internal helper
+    fn set_discriminant(&mut self, discriminant: discriminant_U3) {
+        let discriminant_ptr: *mut discriminant_U3 = (self as *mut U3).cast();
+
+        unsafe {
+            *(discriminant_ptr.add(8)) = discriminant;
+        }
+    }
+
+    #[cfg(any(
+        target_arch = "aarch64",
+        target_arch = "x86_64"
+    ))]
+    /// A tag named None, which has no payload.
+    pub const None: Self = unsafe {
+        let mut bytes = [0; core::mem::size_of::()];
+
+        bytes[8] = discriminant_U3::None as u8;
+
+        core::mem::transmute::<[u8; core::mem::size_of::()], U3>(bytes)
+    };
+
+    #[cfg(any(
+        target_arch = "aarch64",
+        target_arch = "x86_64"
+    ))]
+    /// Construct a tag named `Some`, with the appropriate payload
+    pub fn Some(arg: u64) -> Self {
+            let mut answer = Self {
+                Some: arg
+            };
+
+            answer.set_discriminant(discriminant_U3::Some);
+
+            answer
+    }
+
+    #[cfg(any(
+        target_arch = "aarch64",
+        target_arch = "x86_64"
+    ))]
+    /// Unsafely assume the given `U3` has a `.discriminant()` of `Some` and convert it to `Some`'s payload.
+            /// (Always examine `.discriminant()` first to make sure this is the correct variant!)
+            /// Panics in debug builds if the `.discriminant()` doesn't return `Some`.
+            pub unsafe fn into_Some(self) -> u64 {
+                debug_assert_eq!(self.discriminant(), discriminant_U3::Some);
+        let payload = self.Some;
+
+        payload
+    }
+
+    #[cfg(any(
+        target_arch = "aarch64",
+        target_arch = "x86_64"
+    ))]
+    /// Unsafely assume the given `U3` has a `.discriminant()` of `Some` and return its payload.
+            /// (Always examine `.discriminant()` first to make sure this is the correct variant!)
+            /// Panics in debug builds if the `.discriminant()` doesn't return `Some`.
+            pub unsafe fn as_Some(&self) -> &u64 {
+                debug_assert_eq!(self.discriminant(), discriminant_U3::Some);
+        let payload = &self.Some;
+
+        &payload
+    }
+}
+
+impl Eq for U3 {}
+
+impl PartialEq for U3 {
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    fn eq(&self, other: &Self) -> bool {
+            if self.discriminant() != other.discriminant() {
+                return false;
+            }
+
+            unsafe {
+            match self.discriminant() {
+                discriminant_U3::None => true,
+                discriminant_U3::Some => self.Some == other.Some,
+            }
+        }
+    }
+}
+
+impl PartialOrd for U3 {
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    fn partial_cmp(&self, other: &Self) -> Option {
+        match self.discriminant().partial_cmp(&other.discriminant()) {
+            Some(core::cmp::Ordering::Equal) => {}
+            not_eq => return not_eq,
+        }
+
+        unsafe {
+            match self.discriminant() {
+                discriminant_U3::None => Some(core::cmp::Ordering::Equal),
+                discriminant_U3::Some => self.Some.partial_cmp(&other.Some),
+            }
+        }
+    }
+}
+
+impl Ord for U3 {
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
+            match self.discriminant().cmp(&other.discriminant()) {
+                core::cmp::Ordering::Equal => {}
+                not_eq => return not_eq,
+            }
+
+            unsafe {
+            match self.discriminant() {
+                discriminant_U3::None => core::cmp::Ordering::Equal,
+                discriminant_U3::Some => self.Some.cmp(&other.Some),
+            }
+        }
+    }
+}
+
+impl Copy for U3 {}
+
+impl Clone for U3 {
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    fn clone(&self) -> Self {
+        let mut answer = unsafe {
+            match self.discriminant() {
+                discriminant_U3::None => core::mem::transmute::<
+                    core::mem::MaybeUninit,
+                    U3,
+                >(core::mem::MaybeUninit::uninit()),
+                discriminant_U3::Some => Self {
+                    Some: self.Some.clone(),
+                },
+            }
+
+        };
+
+        answer.set_discriminant(self.discriminant());
+
+        answer
+    }
+}
+
+impl core::hash::Hash for U3 {
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    fn hash(&self, state: &mut H) {        match self.discriminant() {
+            discriminant_U3::None => discriminant_U3::None.hash(state),
+            discriminant_U3::Some => unsafe {
+                    discriminant_U3::Some.hash(state);
+                    self.Some.hash(state);
+                },
+        }
+    }
+}
+
+impl core::fmt::Debug for U3 {
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
+        f.write_str("U3::")?;
+
+        unsafe {
+            match self.discriminant() {
+                discriminant_U3::None => f.write_str("None"),
+                discriminant_U3::Some => f.debug_tuple("Some")
+        .field(&self.Some)
+        .finish(),
+            }
+        }
+    }
+}
+
+impl U1 {
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "wasm32",
+        target_arch = "x86"
+    ))]
+    /// Returns which variant this tag union holds. Note that this never includes a payload!
+    pub fn discriminant(&self) -> discriminant_U1 {
+        unsafe {
+            let bytes = core::mem::transmute::<&Self, &[u8; core::mem::size_of::()]>(self);
+
+            core::mem::transmute::(*bytes.as_ptr().add(4))
+        }
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "wasm32",
+        target_arch = "x86"
+    ))]
+    /// Internal helper
+    fn set_discriminant(&mut self, discriminant: discriminant_U1) {
+        let discriminant_ptr: *mut discriminant_U1 = (self as *mut U1).cast();
+
+        unsafe {
+            *(discriminant_ptr.add(4)) = discriminant;
+        }
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "wasm32",
+        target_arch = "x86"
+    ))]
+    /// A tag named None, which has no payload.
+    pub const None: Self = unsafe {
+        let mut bytes = [0; core::mem::size_of::()];
+
+        bytes[4] = discriminant_U1::None as u8;
+
+        core::mem::transmute::<[u8; core::mem::size_of::()], U1>(bytes)
+    };
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    /// Other `into_` methods return a payload, but since the None tag
+    /// has no payload, this does nothing and is only here for completeness.
+    pub fn into_None(self) {
+        ()
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    /// Other `as` methods return a payload, but since the None tag
+    /// has no payload, this does nothing and is only here for completeness.
+    pub fn as_None(&self) {
+        ()
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "wasm32",
+        target_arch = "x86"
+    ))]
+    /// Construct a tag named `Some`, with the appropriate payload
+    pub fn Some(arg: u32) -> Self {
+            let mut answer = Self {
+                Some: arg
+            };
+
+            answer.set_discriminant(discriminant_U1::Some);
+
+            answer
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "wasm32",
+        target_arch = "x86"
+    ))]
+    /// Unsafely assume the given `U1` has a `.discriminant()` of `Some` and convert it to `Some`'s payload.
+            /// (Always examine `.discriminant()` first to make sure this is the correct variant!)
+            /// Panics in debug builds if the `.discriminant()` doesn't return `Some`.
+            pub unsafe fn into_Some(self) -> u32 {
+                debug_assert_eq!(self.discriminant(), discriminant_U1::Some);
+        let payload = self.Some;
+
+        payload
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "wasm32",
+        target_arch = "x86"
+    ))]
+    /// Unsafely assume the given `U1` has a `.discriminant()` of `Some` and return its payload.
+            /// (Always examine `.discriminant()` first to make sure this is the correct variant!)
+            /// Panics in debug builds if the `.discriminant()` doesn't return `Some`.
+            pub unsafe fn as_Some(&self) -> &u32 {
+                debug_assert_eq!(self.discriminant(), discriminant_U1::Some);
+        let payload = &self.Some;
+
+        &payload
+    }
+
+    #[cfg(any(
+        target_arch = "aarch64",
+        target_arch = "x86_64"
+    ))]
+    /// Returns which variant this tag union holds. Note that this never includes a payload!
+    pub fn discriminant(&self) -> discriminant_U1 {
+        unsafe {
+            let bytes = core::mem::transmute::<&Self, &[u8; core::mem::size_of::()]>(self);
+
+            core::mem::transmute::(*bytes.as_ptr().add(8))
+        }
+    }
+
+    #[cfg(any(
+        target_arch = "aarch64",
+        target_arch = "x86_64"
+    ))]
+    /// Internal helper
+    fn set_discriminant(&mut self, discriminant: discriminant_U1) {
+        let discriminant_ptr: *mut discriminant_U1 = (self as *mut U1).cast();
+
+        unsafe {
+            *(discriminant_ptr.add(8)) = discriminant;
+        }
+    }
+
+    #[cfg(any(
+        target_arch = "aarch64",
+        target_arch = "x86_64"
+    ))]
+    /// A tag named None, which has no payload.
+    pub const None: Self = unsafe {
+        let mut bytes = [0; core::mem::size_of::()];
+
+        bytes[8] = discriminant_U1::None as u8;
+
+        core::mem::transmute::<[u8; core::mem::size_of::()], U1>(bytes)
+    };
+
+    #[cfg(any(
+        target_arch = "aarch64",
+        target_arch = "x86_64"
+    ))]
+    /// Construct a tag named `Some`, with the appropriate payload
+    pub fn Some(arg: u64) -> Self {
+            let mut answer = Self {
+                Some: arg
+            };
+
+            answer.set_discriminant(discriminant_U1::Some);
+
+            answer
+    }
+
+    #[cfg(any(
+        target_arch = "aarch64",
+        target_arch = "x86_64"
+    ))]
+    /// Unsafely assume the given `U1` has a `.discriminant()` of `Some` and convert it to `Some`'s payload.
+            /// (Always examine `.discriminant()` first to make sure this is the correct variant!)
+            /// Panics in debug builds if the `.discriminant()` doesn't return `Some`.
+            pub unsafe fn into_Some(self) -> u64 {
+                debug_assert_eq!(self.discriminant(), discriminant_U1::Some);
+        let payload = self.Some;
+
+        payload
+    }
+
+    #[cfg(any(
+        target_arch = "aarch64",
+        target_arch = "x86_64"
+    ))]
+    /// Unsafely assume the given `U1` has a `.discriminant()` of `Some` and return its payload.
+            /// (Always examine `.discriminant()` first to make sure this is the correct variant!)
+            /// Panics in debug builds if the `.discriminant()` doesn't return `Some`.
+            pub unsafe fn as_Some(&self) -> &u64 {
+                debug_assert_eq!(self.discriminant(), discriminant_U1::Some);
+        let payload = &self.Some;
+
+        &payload
+    }
+}
+
+impl Eq for U1 {}
+
+impl PartialEq for U1 {
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    fn eq(&self, other: &Self) -> bool {
+            if self.discriminant() != other.discriminant() {
+                return false;
+            }
+
+            unsafe {
+            match self.discriminant() {
+                discriminant_U1::None => true,
+                discriminant_U1::Some => self.Some == other.Some,
+            }
+        }
+    }
+}
+
+impl PartialOrd for U1 {
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    fn partial_cmp(&self, other: &Self) -> Option {
+        match self.discriminant().partial_cmp(&other.discriminant()) {
+            Some(core::cmp::Ordering::Equal) => {}
+            not_eq => return not_eq,
+        }
+
+        unsafe {
+            match self.discriminant() {
+                discriminant_U1::None => Some(core::cmp::Ordering::Equal),
+                discriminant_U1::Some => self.Some.partial_cmp(&other.Some),
+            }
+        }
+    }
+}
+
+impl Ord for U1 {
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
+            match self.discriminant().cmp(&other.discriminant()) {
+                core::cmp::Ordering::Equal => {}
+                not_eq => return not_eq,
+            }
+
+            unsafe {
+            match self.discriminant() {
+                discriminant_U1::None => core::cmp::Ordering::Equal,
+                discriminant_U1::Some => self.Some.cmp(&other.Some),
+            }
+        }
+    }
+}
+
+impl Copy for U1 {}
+
+impl Clone for U1 {
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    fn clone(&self) -> Self {
+        let mut answer = unsafe {
+            match self.discriminant() {
+                discriminant_U1::None => core::mem::transmute::<
+                    core::mem::MaybeUninit,
+                    U1,
+                >(core::mem::MaybeUninit::uninit()),
+                discriminant_U1::Some => Self {
+                    Some: self.Some.clone(),
+                },
+            }
+
+        };
+
+        answer.set_discriminant(self.discriminant());
+
+        answer
+    }
+}
+
+impl core::hash::Hash for U1 {
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    fn hash(&self, state: &mut H) {        match self.discriminant() {
+            discriminant_U1::None => discriminant_U1::None.hash(state),
+            discriminant_U1::Some => unsafe {
+                    discriminant_U1::Some.hash(state);
+                    self.Some.hash(state);
+                },
+        }
+    }
+}
+
+impl core::fmt::Debug for U1 {
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
+        f.write_str("U1::")?;
+
+        unsafe {
+            match self.discriminant() {
+                discriminant_U1::None => f.write_str("None"),
+                discriminant_U1::Some => f.debug_tuple("Some")
+        .field(&self.Some)
+        .finish(),
+            }
+        }
+    }
+}
diff --git a/crates/glue/src/lib.rs b/crates/glue/src/lib.rs
new file mode 100644
index 0000000000..0167f35186
--- /dev/null
+++ b/crates/glue/src/lib.rs
@@ -0,0 +1,45 @@
+//! Generates code needed for platform hosts to communicate with Roc apps.
+//! This tool is not necessary for writing a platform in another language,
+//! however, it's a great convenience! Currently supports Rust platforms, and
+//! the plan is to support any language via a plugin model.
+pub mod enums;
+pub mod load;
+pub mod roc_type;
+pub mod structs;
+pub mod types;
+
+#[rustfmt::skip]
+pub mod glue;
+
+pub use load::generate;
+
+// required because we use roc_std here
+mod roc_externs {
+    use core::ffi::c_void;
+
+    /// # Safety
+    /// This just delegates to libc::malloc, so it's equally safe.
+    #[no_mangle]
+    pub unsafe extern "C" fn roc_alloc(size: usize, _alignment: u32) -> *mut c_void {
+        libc::malloc(size)
+    }
+
+    /// # Safety
+    /// This just delegates to libc::realloc, so it's equally safe.
+    #[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 just delegates to libc::free, so it's equally safe.
+    #[no_mangle]
+    pub unsafe extern "C" fn roc_dealloc(c_ptr: *mut c_void, _alignment: u32) {
+        libc::free(c_ptr)
+    }
+}
diff --git a/crates/glue/src/load.rs b/crates/glue/src/load.rs
new file mode 100644
index 0000000000..93d594aab5
--- /dev/null
+++ b/crates/glue/src/load.rs
@@ -0,0 +1,490 @@
+use crate::roc_type;
+use crate::types::Types;
+use bumpalo::Bump;
+use libloading::Library;
+use roc_build::{
+    link::{LinkType, LinkingStrategy},
+    program::{
+        build_file, handle_error_module, handle_loading_problem, standard_load_config,
+        BuildFileError, BuildOrdering, BuiltFile, CodeGenBackend, CodeGenOptions,
+    },
+};
+use roc_collections::MutMap;
+use roc_error_macros::todo_lambda_erasure;
+use roc_load::{ExecutionMode, FunctionKind, LoadConfig, LoadedModule, LoadingProblem, Threading};
+use roc_mono::ir::{generate_glue_procs, GlueProc, OptLevel};
+use roc_mono::layout::{GlobalLayoutInterner, LayoutCache, LayoutInterner};
+use roc_packaging::cache::{self, RocCacheDir};
+use roc_reporting::report::{RenderTarget, DEFAULT_PALETTE};
+use roc_target::{Architecture, TargetInfo};
+use roc_types::subs::{Subs, Variable};
+use std::fs::File;
+use std::io::{self, ErrorKind, Write};
+use std::mem::ManuallyDrop;
+use std::path::{Component, Path, PathBuf};
+use std::process;
+use strum::IntoEnumIterator;
+use target_lexicon::Triple;
+
+pub struct IgnoreErrors {
+    pub can: bool,
+}
+
+impl IgnoreErrors {
+    const NONE: Self = IgnoreErrors { can: false };
+}
+
+pub fn generate(
+    input_path: &Path,
+    output_path: &Path,
+    spec_path: &Path,
+    backend: CodeGenBackend,
+) -> io::Result {
+    // TODO: Add verification around the paths. Make sure they heav the correct file extension and what not.
+    match load_types(
+        input_path.to_path_buf(),
+        Threading::AllAvailable,
+        IgnoreErrors::NONE,
+    ) {
+        Ok(types) => {
+            // TODO: we should to modify the app file first before loading it.
+            // Somehow it has to point to the correct platform file which may not exist on the target machine.
+            let triple = Triple::host();
+
+            let code_gen_options = CodeGenOptions {
+                backend,
+                opt_level: OptLevel::Development,
+                emit_debug_info: false,
+            };
+
+            let load_config = standard_load_config(
+                &triple,
+                BuildOrdering::BuildIfChecks,
+                Threading::AllAvailable,
+            );
+
+            let arena = ManuallyDrop::new(Bump::new());
+            let link_type = LinkType::Dylib;
+            let linking_strategy = if roc_linker::supported(link_type, &triple) {
+                LinkingStrategy::Surgical
+            } else {
+                LinkingStrategy::Legacy
+            };
+
+            let tempdir_res = tempfile::tempdir();
+
+            let res_binary_path = match tempdir_res {
+                Ok(dylib_dir) => build_file(
+                    &arena,
+                    &triple,
+                    spec_path.to_path_buf(),
+                    code_gen_options,
+                    false,
+                    link_type,
+                    linking_strategy,
+                    true,
+                    None,
+                    RocCacheDir::Persistent(cache::roc_cache_dir().as_path()),
+                    load_config,
+                    Some(dylib_dir.path()),
+                ),
+                Err(_) => {
+                    eprintln!("`roc glue` was unable to create a tempdir.");
+                    std::process::exit(1);
+                }
+            };
+
+            let answer = match res_binary_path {
+                Ok(BuiltFile {
+                    binary_path,
+                    problems,
+                    total_time,
+                    expect_metadata: _,
+                }) => {
+                    // TODO: Should binary_path be update to deal with extensions?
+                    use target_lexicon::OperatingSystem;
+                    let lib_path = match triple.operating_system {
+                        OperatingSystem::Windows => binary_path.with_extension("dll"),
+                        OperatingSystem::Darwin | OperatingSystem::MacOSX { .. } => {
+                            binary_path.with_extension("dylib")
+                        }
+
+                        _ => binary_path.with_extension("so.1.0"),
+                    };
+
+                    // TODO: Should glue try and run with errors, especially type errors.
+                    // My gut feeling is no or that we should add a flag for it.
+                    // Given glue will generally be run by random end users, I think it should default to full correctness.
+                    debug_assert_eq!(
+                        problems.errors, 0,
+                        "if there are errors, they should have been returned as an error variant"
+                    );
+                    if problems.warnings > 0 {
+                        problems.print_to_stdout(total_time);
+                        println!(
+                            ".\n\nRunning program…\n\n\x1B[36m{}\x1B[39m",
+                            "─".repeat(80)
+                        );
+                    }
+
+                    let lib = unsafe { Library::new(lib_path) }.unwrap();
+                    type MakeGlue = unsafe extern "C" fn(
+                        *mut roc_std::RocResult, roc_std::RocStr>,
+                        &roc_std::RocList,
+                    );
+
+                    let make_glue: libloading::Symbol = unsafe {
+                        lib.get("roc__makeGlueForHost_1_exposed_generic".as_bytes())
+                            .unwrap_or_else(|_| panic!("Unable to load glue function"))
+                    };
+                    let roc_types: roc_std::RocList =
+                        types.iter().map(|x| x.into()).collect();
+                    let mut files = roc_std::RocResult::err(roc_std::RocStr::empty());
+                    unsafe { make_glue(&mut files, &roc_types) };
+
+                    // Roc will free data passed into it. So forget that data.
+                    std::mem::forget(roc_types);
+
+                    let files: Result, roc_std::RocStr> =
+                        files.into();
+                    let files = files.unwrap_or_else(|err| {
+                        eprintln!("Glue generation failed: {err}");
+
+                        process::exit(1);
+                    });
+                    for roc_type::File { name, content } in &files {
+                        let valid_name = PathBuf::from(name.as_str())
+                            .components()
+                            .all(|comp| matches!(comp, Component::CurDir | Component::Normal(_)));
+                        if !valid_name {
+                            eprintln!("File name was invalid: {}", &name);
+
+                            process::exit(1);
+                        }
+                        let full_path = output_path.join(name.as_str());
+                        if let Some(dir_path) = full_path.parent() {
+                            std::fs::create_dir_all(dir_path).unwrap_or_else(|err| {
+                                eprintln!(
+                                    "Unable to create output directory {} - {:?}",
+                                    dir_path.display(),
+                                    err
+                                );
+
+                                process::exit(1);
+                            });
+                        }
+                        let mut file = File::create(&full_path).unwrap_or_else(|err| {
+                            eprintln!(
+                                "Unable to create output file {} - {:?}",
+                                full_path.display(),
+                                err
+                            );
+
+                            process::exit(1);
+                        });
+
+                        file.write_all(content.as_bytes()).unwrap_or_else(|err| {
+                            eprintln!(
+                                "Unable to write bindings to output file {} - {:?}",
+                                full_path.display(),
+                                err
+                            );
+
+                            process::exit(1);
+                        });
+                    }
+
+                    println!(
+                        "🎉 Generated type declarations in:\n\n\t{}",
+                        output_path.display()
+                    );
+
+                    Ok(0)
+                }
+                Err(BuildFileError::ErrorModule { module, total_time }) => {
+                    handle_error_module(module, total_time, spec_path.as_os_str(), true)
+                }
+                Err(BuildFileError::LoadingProblem(problem)) => handle_loading_problem(problem),
+            };
+
+            // Extend the lifetime of the tempdir to after we're done with everything,
+            // so it doesn't get dropped before we're done reading from it!
+            let _ = tempdir_res;
+
+            answer
+        }
+        Err(err) => match err.kind() {
+            ErrorKind::NotFound => {
+                eprintln!("Platform module file not found: {}", input_path.display());
+                process::exit(1);
+            }
+            error => {
+                eprintln!(
+                    "Error loading platform module file {} - {:?}",
+                    input_path.display(),
+                    error
+                );
+                process::exit(1);
+            }
+        },
+    }
+}
+
+fn number_lambda_sets(subs: &Subs, initial: Variable) -> Vec {
+    let mut lambda_sets = vec![];
+    let mut stack = vec![initial];
+
+    macro_rules! var_slice {
+        ($variable_subs_slice:expr) => {{
+            let slice = $variable_subs_slice;
+            subs.variables[slice.indices()].iter().rev()
+        }};
+    }
+
+    while let Some(var) = stack.pop() {
+        use roc_types::subs::Content::*;
+        use roc_types::subs::FlatType::*;
+
+        use roc_types::subs::GetSubsSlice;
+        use roc_types::types::Uls;
+
+        match subs.get_content_without_compacting(var) {
+            RigidVar(_) | RigidAbleVar(_, _) | FlexVar(_) | FlexAbleVar(_, _) | Error => (),
+
+            RecursionVar { .. } => {
+                // we got here, so we've treated this type already
+            }
+
+            Structure(flat_type) => match flat_type {
+                Apply(_, args) => {
+                    stack.extend(var_slice!(*args));
+                }
+
+                Func(arg_vars, closure_var, ret_var) => {
+                    lambda_sets.push(subs.get_root_key_without_compacting(*closure_var));
+
+                    stack.push(*ret_var);
+                    stack.push(*closure_var);
+                    stack.extend(var_slice!(arg_vars));
+                }
+
+                EmptyRecord => (),
+                EmptyTagUnion => (),
+                EmptyTuple => (),
+
+                Record(fields, ext) => {
+                    let fields = *fields;
+                    let ext = *ext;
+
+                    stack.push(ext);
+                    stack.extend(var_slice!(fields.variables()));
+                }
+                Tuple(_, _) => todo!(),
+                TagUnion(tags, ext) => {
+                    let tags = *tags;
+                    let ext = *ext;
+
+                    stack.push(ext.var());
+
+                    for slice_index in tags.variables() {
+                        let slice = subs.variable_slices[slice_index.index as usize];
+                        stack.extend(var_slice!(slice));
+                    }
+                }
+                FunctionOrTagUnion(_, _, ext) => {
+                    stack.push(ext.var());
+                }
+
+                RecursiveTagUnion(rec_var, tags, ext) => {
+                    let tags = *tags;
+                    let ext = *ext;
+                    let rec_var = *rec_var;
+
+                    stack.push(ext.var());
+
+                    for slice_index in tags.variables() {
+                        let slice = subs.variable_slices[slice_index.index as usize];
+                        stack.extend(var_slice!(slice));
+                    }
+
+                    stack.push(rec_var);
+                }
+            },
+            Alias(_, args, var, _) => {
+                let var = *var;
+                let args = *args;
+
+                stack.extend(var_slice!(args.all_variables()));
+
+                stack.push(var);
+            }
+            LambdaSet(roc_types::subs::LambdaSet {
+                solved,
+                recursion_var,
+                unspecialized,
+                ambient_function: _,
+            }) => {
+                for slice_index in solved.variables() {
+                    let slice = subs.variable_slices[slice_index.index as usize];
+                    stack.extend(var_slice!(slice));
+                }
+
+                if let Some(rec_var) = recursion_var.into_variable() {
+                    stack.push(rec_var);
+                }
+
+                for Uls(var, _, _) in subs.get_subs_slice(*unspecialized) {
+                    stack.push(*var);
+                }
+            }
+            ErasedLambda => todo_lambda_erasure!(),
+            &RangedNumber(_) => {}
+        }
+    }
+
+    lambda_sets
+}
+
+pub fn load_types(
+    full_file_path: PathBuf,
+    threading: Threading,
+    ignore_errors: IgnoreErrors,
+) -> Result, io::Error> {
+    let target_info = (&Triple::host()).into();
+    // TODO the function kind may need to be parameterizable.
+    let function_kind = FunctionKind::LambdaSet;
+    let arena = &Bump::new();
+    let LoadedModule {
+        module_id: home,
+        mut can_problems,
+        mut type_problems,
+        mut declarations_by_id,
+        mut solved,
+        interns,
+        exposed_to_host,
+        ..
+    } = roc_load::load_and_typecheck(
+        arena,
+        full_file_path,
+        RocCacheDir::Persistent(cache::roc_cache_dir().as_path()),
+        LoadConfig {
+            target_info,
+            function_kind,
+            render: RenderTarget::Generic,
+            palette: DEFAULT_PALETTE,
+            threading,
+            exec_mode: ExecutionMode::Check,
+        },
+    )
+    .unwrap_or_else(|problem| match problem {
+        LoadingProblem::FormattedReport(report) => {
+            eprintln!("{report}");
+
+            process::exit(1);
+        }
+        problem => {
+            todo!("{:?}", problem);
+        }
+    });
+
+    let decls = declarations_by_id.remove(&home).unwrap();
+    let subs = solved.inner_mut();
+
+    let can_problems = can_problems.remove(&home).unwrap_or_default();
+    let type_problems = type_problems.remove(&home).unwrap_or_default();
+
+    if (!ignore_errors.can && !can_problems.is_empty()) || !type_problems.is_empty() {
+        todo!(
+            "Gracefully report compilation problems during glue generation: {:?}, {:?}",
+            can_problems,
+            type_problems
+        );
+    }
+
+    // Get the variables for all the exposed_to_host symbols
+    let variables = (0..decls.len()).filter_map(|index| {
+        let symbol = decls.symbols[index].value;
+        exposed_to_host.get(&symbol).copied()
+    });
+
+    let operating_system = target_info.operating_system;
+    let architectures = Architecture::iter();
+    let mut arch_types = Vec::with_capacity(architectures.len());
+
+    for architecture in architectures {
+        let mut interns = interns.clone(); // TODO there may be a way to avoid this.
+        let target_info = TargetInfo {
+            architecture,
+            operating_system,
+        };
+        let layout_interner = GlobalLayoutInterner::with_capacity(128, target_info);
+        let mut layout_cache = LayoutCache::new(layout_interner.fork(), target_info);
+        let mut glue_procs_by_layout = MutMap::default();
+
+        let mut extern_names = MutMap::default();
+
+        // Populate glue getters/setters for all relevant variables
+        for var in variables.clone() {
+            for (i, v) in number_lambda_sets(subs, var).iter().enumerate() {
+                extern_names.insert(*v, i.to_string());
+            }
+
+            let in_layout = layout_cache
+                .from_var(arena, var, subs)
+                .expect("Something weird ended up in the content");
+
+            let layout = layout_cache.interner.get(in_layout);
+
+            if layout_cache
+                .interner
+                .has_varying_stack_size(in_layout, arena)
+            {
+                let ident_ids = interns.all_ident_ids.get_mut(&home).unwrap();
+                let answer = generate_glue_procs(
+                    home,
+                    ident_ids,
+                    arena,
+                    &mut layout_interner.fork(),
+                    arena.alloc(layout),
+                );
+
+                // Even though generate_glue_procs does more work than we need it to,
+                // it's important that we use it in order to make sure we get exactly
+                // the same names that mono::ir did for code gen!
+                for (layout, glue_procs) in answer.getters {
+                    let mut names =
+                        bumpalo::collections::Vec::with_capacity_in(glue_procs.len(), arena);
+
+                    // Record all the getter/setter names associated with this layout
+                    for GlueProc { name, .. } in glue_procs {
+                        // Given a struct layout (including lambda sets!) the offsets - and therefore
+                        // getters/setters - are deterministic, so we can use layout as the hash key
+                        // for these getters/setters. We also only need to store the name because
+                        // since they are getters and setters, we can know their types (from a
+                        // TypeId perspective) deterministically based on knowing the types of
+                        // the structs and fields.
+                        //
+                        // Store them as strings, because symbols won't be useful to glue generators!
+                        names.push(name.as_str(&interns).to_string());
+                    }
+
+                    glue_procs_by_layout.insert(layout, names.into_bump_slice());
+                }
+            }
+        }
+
+        let types = Types::new_with_entry_points(
+            arena,
+            subs,
+            arena.alloc(interns),
+            glue_procs_by_layout,
+            layout_cache,
+            target_info,
+            exposed_to_host.clone(),
+        );
+
+        arch_types.push(types);
+    }
+
+    Ok(arch_types)
+}
diff --git a/crates/glue/src/roc_type/mod.rs b/crates/glue/src/roc_type/mod.rs
new file mode 100644
index 0000000000..e053f8b0cc
--- /dev/null
+++ b/crates/glue/src/roc_type/mod.rs
@@ -0,0 +1,3967 @@
+// ⚠️ GENERATED CODE ⚠️ - this entire file was generated by the `roc glue` CLI command
+
+#![allow(unused_unsafe)]
+#![allow(dead_code)]
+#![allow(unused_mut)]
+#![allow(non_snake_case)]
+#![allow(non_camel_case_types)]
+#![allow(non_upper_case_globals)]
+#![allow(clippy::undocumented_unsafe_blocks)]
+#![allow(clippy::redundant_static_lifetimes)]
+#![allow(clippy::unused_unit)]
+#![allow(clippy::missing_safety_doc)]
+#![allow(clippy::let_and_return)]
+#![allow(clippy::missing_safety_doc)]
+#![allow(clippy::redundant_static_lifetimes)]
+#![allow(clippy::needless_borrow)]
+#![allow(clippy::clone_on_copy)]
+
+#[cfg(any(
+    target_arch = "arm",
+    target_arch = "aarch64",
+    target_arch = "wasm32",
+    target_arch = "x86",
+    target_arch = "x86_64"
+))]
+#[derive(Clone, Debug, Default, Eq, Ord, Hash, PartialEq, PartialOrd)]
+#[repr(C)]
+pub struct File {
+    pub content: roc_std::RocStr,
+    pub name: roc_std::RocStr,
+}
+
+#[cfg(any(
+    target_arch = "arm",
+    target_arch = "aarch64",
+    target_arch = "wasm32",
+    target_arch = "x86",
+    target_arch = "x86_64"
+))]
+#[derive(Clone, Debug, Eq, Ord, Hash, PartialEq, PartialOrd)]
+#[repr(C)]
+pub struct Types {
+    pub aligns: roc_std::RocList,
+    pub deps: roc_std::RocList,
+    pub entrypoints: roc_std::RocList,
+    pub sizes: roc_std::RocList,
+    pub types: roc_std::RocList,
+    pub typesByName: roc_std::RocList,
+    pub target: Target,
+}
+
+#[cfg(any(target_arch = "arm", target_arch = "wasm32", target_arch = "x86"))]
+#[repr(C)]
+#[derive(Clone, Default, Eq, Ord, Hash, PartialEq, PartialOrd)]
+pub struct Tuple1 {
+    f0: roc_std::RocStr,
+    f1: u32,
+}
+
+#[cfg(any(
+    target_arch = "arm",
+    target_arch = "aarch64",
+    target_arch = "wasm32",
+    target_arch = "x86",
+    target_arch = "x86_64"
+))]
+#[derive(Clone, Copy, Eq, Ord, Hash, PartialEq, PartialOrd)]
+#[repr(u8)]
+pub enum discriminant_RocType {
+    Bool = 0,
+    EmptyTagUnion = 1,
+    Function = 2,
+    Num = 3,
+    RecursivePointer = 4,
+    RocBox = 5,
+    RocDict = 6,
+    RocList = 7,
+    RocResult = 8,
+    RocSet = 9,
+    RocStr = 10,
+    Struct = 11,
+    TagUnion = 12,
+    TagUnionPayload = 13,
+    Unit = 14,
+    Unsized = 15,
+}
+
+impl core::fmt::Debug for discriminant_RocType {
+    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
+        match self {
+            Self::Bool => f.write_str("discriminant_RocType::Bool"),
+            Self::EmptyTagUnion => f.write_str("discriminant_RocType::EmptyTagUnion"),
+            Self::Function => f.write_str("discriminant_RocType::Function"),
+            Self::Num => f.write_str("discriminant_RocType::Num"),
+            Self::RecursivePointer => f.write_str("discriminant_RocType::RecursivePointer"),
+            Self::RocBox => f.write_str("discriminant_RocType::RocBox"),
+            Self::RocDict => f.write_str("discriminant_RocType::RocDict"),
+            Self::RocList => f.write_str("discriminant_RocType::RocList"),
+            Self::RocResult => f.write_str("discriminant_RocType::RocResult"),
+            Self::RocSet => f.write_str("discriminant_RocType::RocSet"),
+            Self::RocStr => f.write_str("discriminant_RocType::RocStr"),
+            Self::Struct => f.write_str("discriminant_RocType::Struct"),
+            Self::TagUnion => f.write_str("discriminant_RocType::TagUnion"),
+            Self::TagUnionPayload => f.write_str("discriminant_RocType::TagUnionPayload"),
+            Self::Unit => f.write_str("discriminant_RocType::Unit"),
+            Self::Unsized => f.write_str("discriminant_RocType::Unsized"),
+        }
+    }
+}
+
+#[cfg(any(target_arch = "arm", target_arch = "wasm32", target_arch = "x86"))]
+#[repr(C)]
+pub union RocType {
+    Function: core::mem::ManuallyDrop,
+    Num: RocNum,
+    RecursivePointer: u32,
+    RocBox: u32,
+    RocDict: RocType_RocDict,
+    RocList: u32,
+    RocResult: RocType_RocDict,
+    RocSet: u32,
+    Struct: core::mem::ManuallyDrop,
+    TagUnion: core::mem::ManuallyDrop,
+    TagUnionPayload: core::mem::ManuallyDrop,
+    _sizer: [u8; 96],
+}
+
+#[cfg(any(target_arch = "arm", target_arch = "wasm32", target_arch = "x86"))]
+#[derive(Clone, Copy, Debug, Default, Eq, Ord, Hash, PartialEq, PartialOrd)]
+#[repr(transparent)]
+pub struct R16 {
+    pub id: u32,
+}
+
+#[cfg(any(
+    target_arch = "arm",
+    target_arch = "aarch64",
+    target_arch = "wasm32",
+    target_arch = "x86",
+    target_arch = "x86_64"
+))]
+#[derive(Clone, Debug, Eq, Ord, Hash, PartialEq, PartialOrd)]
+#[repr(C)]
+pub struct R8 {
+    pub name: roc_std::RocStr,
+    pub payload: U1,
+}
+
+#[cfg(any(target_arch = "arm", target_arch = "wasm32", target_arch = "x86"))]
+#[derive(Clone, Debug, Default, Eq, Ord, Hash, PartialEq, PartialOrd)]
+#[repr(C)]
+pub struct R4 {
+    pub id: u32,
+    pub name: roc_std::RocStr,
+}
+
+#[cfg(any(target_arch = "arm", target_arch = "wasm32", target_arch = "x86"))]
+#[derive(Clone, Debug, Default, Eq, Ord, Hash, PartialEq, PartialOrd)]
+#[repr(C)]
+pub struct R2 {
+    pub accessors: R3,
+    pub id: u32,
+    pub name: roc_std::RocStr,
+}
+
+#[cfg(any(target_arch = "arm", target_arch = "wasm32", target_arch = "x86"))]
+#[repr(C)]
+#[derive(Clone, Default, Eq, Ord, Hash, PartialEq, PartialOrd)]
+pub struct Tuple2 {
+    f0: u32,
+    f1: roc_std::RocList,
+}
+
+#[cfg(any(
+    target_arch = "arm",
+    target_arch = "aarch64",
+    target_arch = "wasm32",
+    target_arch = "x86",
+    target_arch = "x86_64"
+))]
+#[derive(Clone, Copy, Debug, Eq, Ord, Hash, PartialEq, PartialOrd)]
+#[repr(C)]
+pub struct Target {
+    pub architecture: Architecture,
+    pub operatingSystem: OperatingSystem,
+}
+
+#[cfg(any(
+    target_arch = "arm",
+    target_arch = "aarch64",
+    target_arch = "wasm32",
+    target_arch = "x86",
+    target_arch = "x86_64"
+))]
+#[derive(Clone, Copy, Eq, Ord, Hash, PartialEq, PartialOrd)]
+#[repr(u8)]
+pub enum OperatingSystem {
+    Unix = 0,
+    Wasi = 1,
+    Windows = 2,
+}
+
+impl core::fmt::Debug for OperatingSystem {
+    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
+        match self {
+            Self::Unix => f.write_str("OperatingSystem::Unix"),
+            Self::Wasi => f.write_str("OperatingSystem::Wasi"),
+            Self::Windows => f.write_str("OperatingSystem::Windows"),
+        }
+    }
+}
+
+#[cfg(any(
+    target_arch = "arm",
+    target_arch = "aarch64",
+    target_arch = "wasm32",
+    target_arch = "x86",
+    target_arch = "x86_64"
+))]
+#[derive(Clone, Copy, Eq, Ord, Hash, PartialEq, PartialOrd)]
+#[repr(u8)]
+pub enum Architecture {
+    Aarch32 = 0,
+    Aarch64 = 1,
+    Wasm32 = 2,
+    X86x32 = 3,
+    X86x64 = 4,
+}
+
+impl core::fmt::Debug for Architecture {
+    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
+        match self {
+            Self::Aarch32 => f.write_str("Architecture::Aarch32"),
+            Self::Aarch64 => f.write_str("Architecture::Aarch64"),
+            Self::Wasm32 => f.write_str("Architecture::Wasm32"),
+            Self::X86x32 => f.write_str("Architecture::X86x32"),
+            Self::X86x64 => f.write_str("Architecture::X86x64"),
+        }
+    }
+}
+
+#[cfg(any(
+    target_arch = "arm",
+    target_arch = "aarch64",
+    target_arch = "wasm32",
+    target_arch = "x86",
+    target_arch = "x86_64"
+))]
+#[derive(Clone, Copy, Eq, Ord, Hash, PartialEq, PartialOrd)]
+#[repr(u8)]
+pub enum discriminant_RocTagUnion {
+    Enumeration = 0,
+    NonNullableUnwrapped = 1,
+    NonRecursive = 2,
+    NullableUnwrapped = 3,
+    NullableWrapped = 4,
+    Recursive = 5,
+    SingleTagStruct = 6,
+}
+
+impl core::fmt::Debug for discriminant_RocTagUnion {
+    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
+        match self {
+            Self::Enumeration => f.write_str("discriminant_RocTagUnion::Enumeration"),
+            Self::NonNullableUnwrapped => {
+                f.write_str("discriminant_RocTagUnion::NonNullableUnwrapped")
+            }
+            Self::NonRecursive => f.write_str("discriminant_RocTagUnion::NonRecursive"),
+            Self::NullableUnwrapped => f.write_str("discriminant_RocTagUnion::NullableUnwrapped"),
+            Self::NullableWrapped => f.write_str("discriminant_RocTagUnion::NullableWrapped"),
+            Self::Recursive => f.write_str("discriminant_RocTagUnion::Recursive"),
+            Self::SingleTagStruct => f.write_str("discriminant_RocTagUnion::SingleTagStruct"),
+        }
+    }
+}
+
+#[cfg(any(target_arch = "arm", target_arch = "wasm32", target_arch = "x86"))]
+#[repr(C)]
+pub union RocTagUnion {
+    Enumeration: core::mem::ManuallyDrop,
+    NonNullableUnwrapped: core::mem::ManuallyDrop,
+    NonRecursive: core::mem::ManuallyDrop,
+    NullableUnwrapped: core::mem::ManuallyDrop,
+    NullableWrapped: core::mem::ManuallyDrop,
+    Recursive: core::mem::ManuallyDrop,
+    SingleTagStruct: core::mem::ManuallyDrop,
+    _sizer: [u8; 88],
+}
+
+#[cfg(any(
+    target_arch = "arm",
+    target_arch = "aarch64",
+    target_arch = "wasm32",
+    target_arch = "x86",
+    target_arch = "x86_64"
+))]
+#[derive(Clone, Debug, Eq, Ord, Hash, PartialEq, PartialOrd)]
+#[repr(C)]
+pub struct R14 {
+    pub name: roc_std::RocStr,
+    pub payload: RocSingleTagPayload,
+    pub tagName: roc_std::RocStr,
+}
+
+#[cfg(any(
+    target_arch = "arm",
+    target_arch = "aarch64",
+    target_arch = "wasm32",
+    target_arch = "x86",
+    target_arch = "x86_64"
+))]
+#[derive(Clone, Copy, Eq, Ord, Hash, PartialEq, PartialOrd)]
+#[repr(u8)]
+pub enum discriminant_RocSingleTagPayload {
+    HasClosure = 0,
+    HasNoClosure = 1,
+}
+
+impl core::fmt::Debug for discriminant_RocSingleTagPayload {
+    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
+        match self {
+            Self::HasClosure => f.write_str("discriminant_RocSingleTagPayload::HasClosure"),
+            Self::HasNoClosure => f.write_str("discriminant_RocSingleTagPayload::HasNoClosure"),
+        }
+    }
+}
+
+#[cfg(any(
+    target_arch = "arm",
+    target_arch = "aarch64",
+    target_arch = "wasm32",
+    target_arch = "x86",
+    target_arch = "x86_64"
+))]
+#[repr(C)]
+pub union RocSingleTagPayload {
+    HasClosure: core::mem::ManuallyDrop>,
+    HasNoClosure: core::mem::ManuallyDrop>,
+    _sizer: [u8; 32],
+}
+
+#[cfg(any(target_arch = "arm", target_arch = "wasm32", target_arch = "x86"))]
+#[derive(Clone, Debug, Eq, Ord, Hash, PartialEq, PartialOrd)]
+#[repr(C)]
+pub struct R10 {
+    pub discriminantOffset: u32,
+    pub discriminantSize: u32,
+    pub name: roc_std::RocStr,
+    pub tags: roc_std::RocList,
+    pub indexOfNullTag: u16,
+}
+
+#[cfg(any(target_arch = "arm", target_arch = "wasm32", target_arch = "x86"))]
+#[derive(Clone, Debug, Eq, Ord, Hash, PartialEq, PartialOrd)]
+#[repr(C)]
+pub struct R9 {
+    pub name: roc_std::RocStr,
+    pub nonNullPayload: u32,
+    pub nonNullTag: roc_std::RocStr,
+    pub nullTag: roc_std::RocStr,
+    pub whichTagIsNull: U2,
+}
+
+#[cfg(any(
+    target_arch = "arm",
+    target_arch = "aarch64",
+    target_arch = "wasm32",
+    target_arch = "x86",
+    target_arch = "x86_64"
+))]
+#[derive(Clone, Copy, Eq, Ord, Hash, PartialEq, PartialOrd)]
+#[repr(u8)]
+pub enum U2 {
+    FirstTagIsNull = 0,
+    SecondTagIsNull = 1,
+}
+
+impl core::fmt::Debug for U2 {
+    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
+        match self {
+            Self::FirstTagIsNull => f.write_str("U2::FirstTagIsNull"),
+            Self::SecondTagIsNull => f.write_str("U2::SecondTagIsNull"),
+        }
+    }
+}
+
+#[cfg(any(target_arch = "arm", target_arch = "wasm32", target_arch = "x86"))]
+#[derive(Clone, Debug, Eq, Ord, Hash, PartialEq, PartialOrd)]
+#[repr(C)]
+pub struct R7 {
+    pub discriminantOffset: u32,
+    pub discriminantSize: u32,
+    pub name: roc_std::RocStr,
+    pub tags: roc_std::RocList,
+}
+
+#[cfg(any(
+    target_arch = "arm",
+    target_arch = "aarch64",
+    target_arch = "wasm32",
+    target_arch = "x86",
+    target_arch = "x86_64"
+))]
+#[derive(Clone, Copy, Eq, Ord, Hash, PartialEq, PartialOrd)]
+#[repr(u8)]
+pub enum discriminant_U1 {
+    None = 0,
+    Some = 1,
+}
+
+impl core::fmt::Debug for discriminant_U1 {
+    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
+        match self {
+            Self::None => f.write_str("discriminant_U1::None"),
+            Self::Some => f.write_str("discriminant_U1::Some"),
+        }
+    }
+}
+
+#[cfg(any(target_arch = "arm", target_arch = "wasm32", target_arch = "x86"))]
+#[repr(C)]
+pub union U1 {
+    Some: u32,
+    _sizer: [u8; 8],
+}
+
+#[cfg(any(target_arch = "arm", target_arch = "wasm32", target_arch = "x86"))]
+#[derive(Clone, Debug, Default, Eq, Ord, Hash, PartialEq, PartialOrd)]
+#[repr(C)]
+pub struct R6 {
+    pub name: roc_std::RocStr,
+    pub payload: u32,
+    pub tagName: roc_std::RocStr,
+}
+
+#[cfg(any(target_arch = "arm", target_arch = "wasm32", target_arch = "x86"))]
+#[derive(Clone, Debug, Default, Eq, Ord, Hash, PartialEq, PartialOrd)]
+#[repr(C)]
+pub struct R5 {
+    pub name: roc_std::RocStr,
+    pub size: u32,
+    pub tags: roc_std::RocList,
+}
+
+#[cfg(any(
+    target_arch = "arm",
+    target_arch = "aarch64",
+    target_arch = "wasm32",
+    target_arch = "x86",
+    target_arch = "x86_64"
+))]
+#[derive(Clone, Debug, Eq, Ord, Hash, PartialEq, PartialOrd)]
+#[repr(C)]
+pub struct R1 {
+    pub fields: RocStructFields,
+    pub name: roc_std::RocStr,
+}
+
+#[cfg(any(
+    target_arch = "arm",
+    target_arch = "aarch64",
+    target_arch = "wasm32",
+    target_arch = "x86",
+    target_arch = "x86_64"
+))]
+#[derive(Clone, Copy, Eq, Ord, Hash, PartialEq, PartialOrd)]
+#[repr(u8)]
+pub enum discriminant_RocStructFields {
+    HasClosure = 0,
+    HasNoClosure = 1,
+}
+
+impl core::fmt::Debug for discriminant_RocStructFields {
+    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
+        match self {
+            Self::HasClosure => f.write_str("discriminant_RocStructFields::HasClosure"),
+            Self::HasNoClosure => f.write_str("discriminant_RocStructFields::HasNoClosure"),
+        }
+    }
+}
+
+#[cfg(any(
+    target_arch = "arm",
+    target_arch = "aarch64",
+    target_arch = "wasm32",
+    target_arch = "x86",
+    target_arch = "x86_64"
+))]
+#[repr(C)]
+pub union RocStructFields {
+    HasClosure: core::mem::ManuallyDrop>,
+    HasNoClosure: core::mem::ManuallyDrop>,
+    _sizer: [u8; 32],
+}
+
+#[cfg(any(
+    target_arch = "arm",
+    target_arch = "aarch64",
+    target_arch = "wasm32",
+    target_arch = "x86",
+    target_arch = "x86_64"
+))]
+#[derive(Clone, Debug, Default, Eq, Ord, Hash, PartialEq, PartialOrd)]
+#[repr(transparent)]
+pub struct R3 {
+    pub getter: roc_std::RocStr,
+}
+
+#[cfg(any(target_arch = "arm", target_arch = "wasm32", target_arch = "x86"))]
+#[derive(Clone, Copy, Debug, Default, Eq, Ord, Hash, PartialEq, PartialOrd)]
+#[repr(C)]
+struct RocType_RocDict {
+    pub f0: u32,
+    pub f1: u32,
+}
+
+#[cfg(any(
+    target_arch = "arm",
+    target_arch = "aarch64",
+    target_arch = "wasm32",
+    target_arch = "x86",
+    target_arch = "x86_64"
+))]
+#[derive(Clone, Copy, Eq, Ord, Hash, PartialEq, PartialOrd)]
+#[repr(u8)]
+pub enum RocNum {
+    Dec = 0,
+    F32 = 1,
+    F64 = 2,
+    I128 = 3,
+    I16 = 4,
+    I32 = 5,
+    I64 = 6,
+    I8 = 7,
+    U128 = 8,
+    U16 = 9,
+    U32 = 10,
+    U64 = 11,
+    U8 = 12,
+}
+
+impl core::fmt::Debug for RocNum {
+    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
+        match self {
+            Self::Dec => f.write_str("RocNum::Dec"),
+            Self::F32 => f.write_str("RocNum::F32"),
+            Self::F64 => f.write_str("RocNum::F64"),
+            Self::I128 => f.write_str("RocNum::I128"),
+            Self::I16 => f.write_str("RocNum::I16"),
+            Self::I32 => f.write_str("RocNum::I32"),
+            Self::I64 => f.write_str("RocNum::I64"),
+            Self::I8 => f.write_str("RocNum::I8"),
+            Self::U128 => f.write_str("RocNum::U128"),
+            Self::U16 => f.write_str("RocNum::U16"),
+            Self::U32 => f.write_str("RocNum::U32"),
+            Self::U64 => f.write_str("RocNum::U64"),
+            Self::U8 => f.write_str("RocNum::U8"),
+        }
+    }
+}
+
+#[cfg(any(target_arch = "arm", target_arch = "wasm32", target_arch = "x86"))]
+#[derive(Clone, Debug, Default, Eq, Ord, Hash, PartialEq, PartialOrd)]
+#[repr(C)]
+pub struct RocFn {
+    pub args: roc_std::RocList,
+    pub externName: roc_std::RocStr,
+    pub functionName: roc_std::RocStr,
+    pub lambdaSet: u32,
+    pub ret: u32,
+    pub is_toplevel: bool,
+}
+
+#[cfg(any(target_arch = "aarch64", target_arch = "x86_64"))]
+#[repr(C)]
+#[derive(Clone, Default, Eq, Ord, Hash, PartialEq, PartialOrd)]
+pub struct Tuple1 {
+    f0: roc_std::RocStr,
+    f1: u64,
+}
+
+#[cfg(any(target_arch = "aarch64", target_arch = "x86_64"))]
+#[repr(C)]
+pub union RocType {
+    Function: core::mem::ManuallyDrop,
+    Num: RocNum,
+    RecursivePointer: u64,
+    RocBox: u64,
+    RocDict: RocType_RocDict,
+    RocList: u64,
+    RocResult: RocType_RocDict,
+    RocSet: u64,
+    Struct: core::mem::ManuallyDrop,
+    TagUnion: core::mem::ManuallyDrop,
+    TagUnionPayload: core::mem::ManuallyDrop,
+    _sizer: [u8; 104],
+}
+
+#[cfg(any(target_arch = "aarch64", target_arch = "x86_64"))]
+#[derive(Clone, Copy, Debug, Default, Eq, Ord, Hash, PartialEq, PartialOrd)]
+#[repr(transparent)]
+pub struct R16 {
+    pub id: u64,
+}
+
+#[cfg(any(target_arch = "aarch64", target_arch = "x86_64"))]
+#[derive(Clone, Debug, Default, Eq, Ord, Hash, PartialEq, PartialOrd)]
+#[repr(C)]
+pub struct R4 {
+    pub id: u64,
+    pub name: roc_std::RocStr,
+}
+
+#[cfg(any(target_arch = "aarch64", target_arch = "x86_64"))]
+#[derive(Clone, Debug, Default, Eq, Ord, Hash, PartialEq, PartialOrd)]
+#[repr(C)]
+pub struct R2 {
+    pub accessors: R3,
+    pub id: u64,
+    pub name: roc_std::RocStr,
+}
+
+#[cfg(any(target_arch = "aarch64", target_arch = "x86_64"))]
+#[repr(C)]
+#[derive(Clone, Default, Eq, Ord, Hash, PartialEq, PartialOrd)]
+pub struct Tuple2 {
+    f0: u64,
+    f1: roc_std::RocList,
+}
+
+#[cfg(any(target_arch = "aarch64", target_arch = "x86_64"))]
+#[repr(C)]
+pub union RocTagUnion {
+    Enumeration: core::mem::ManuallyDrop,
+    NonNullableUnwrapped: core::mem::ManuallyDrop,
+    NonRecursive: core::mem::ManuallyDrop,
+    NullableUnwrapped: core::mem::ManuallyDrop,
+    NullableWrapped: core::mem::ManuallyDrop,
+    Recursive: core::mem::ManuallyDrop,
+    SingleTagStruct: core::mem::ManuallyDrop,
+    _sizer: [u8; 96],
+}
+
+#[cfg(any(target_arch = "aarch64", target_arch = "x86_64"))]
+#[derive(Clone, Debug, Eq, Ord, Hash, PartialEq, PartialOrd)]
+#[repr(C)]
+pub struct R10 {
+    pub name: roc_std::RocStr,
+    pub tags: roc_std::RocList,
+    pub discriminantOffset: u32,
+    pub discriminantSize: u32,
+    pub indexOfNullTag: u16,
+}
+
+#[cfg(any(target_arch = "aarch64", target_arch = "x86_64"))]
+#[derive(Clone, Debug, Eq, Ord, Hash, PartialEq, PartialOrd)]
+#[repr(C)]
+pub struct R9 {
+    pub name: roc_std::RocStr,
+    pub nonNullPayload: u64,
+    pub nonNullTag: roc_std::RocStr,
+    pub nullTag: roc_std::RocStr,
+    pub whichTagIsNull: U2,
+}
+
+#[cfg(any(target_arch = "aarch64", target_arch = "x86_64"))]
+#[derive(Clone, Debug, Eq, Ord, Hash, PartialEq, PartialOrd)]
+#[repr(C)]
+pub struct R7 {
+    pub name: roc_std::RocStr,
+    pub tags: roc_std::RocList,
+    pub discriminantOffset: u32,
+    pub discriminantSize: u32,
+}
+
+#[cfg(any(target_arch = "aarch64", target_arch = "x86_64"))]
+#[repr(C)]
+pub union U1 {
+    Some: u64,
+    _sizer: [u8; 16],
+}
+
+#[cfg(any(target_arch = "aarch64", target_arch = "x86_64"))]
+#[derive(Clone, Debug, Default, Eq, Ord, Hash, PartialEq, PartialOrd)]
+#[repr(C)]
+pub struct R6 {
+    pub name: roc_std::RocStr,
+    pub payload: u64,
+    pub tagName: roc_std::RocStr,
+}
+
+#[cfg(any(target_arch = "aarch64", target_arch = "x86_64"))]
+#[derive(Clone, Debug, Default, Eq, Ord, Hash, PartialEq, PartialOrd)]
+#[repr(C)]
+pub struct R5 {
+    pub name: roc_std::RocStr,
+    pub tags: roc_std::RocList,
+    pub size: u32,
+}
+
+#[cfg(any(target_arch = "aarch64", target_arch = "x86_64"))]
+#[derive(Clone, Copy, Debug, Default, Eq, Ord, Hash, PartialEq, PartialOrd)]
+#[repr(C)]
+struct RocType_RocDict {
+    pub f0: u64,
+    pub f1: u64,
+}
+
+#[cfg(any(target_arch = "aarch64", target_arch = "x86_64"))]
+#[derive(Clone, Debug, Default, Eq, Ord, Hash, PartialEq, PartialOrd)]
+#[repr(C)]
+pub struct RocFn {
+    pub args: roc_std::RocList,
+    pub externName: roc_std::RocStr,
+    pub functionName: roc_std::RocStr,
+    pub lambdaSet: u64,
+    pub ret: u64,
+    pub isToplevel: bool,
+}
+
+impl Tuple1 {
+    #[cfg(any(target_arch = "arm", target_arch = "wasm32", target_arch = "x86"))]
+    /// A tag named T, with the given payload.
+    pub fn T(f0: roc_std::RocStr, f1: u32) -> Self {
+        Self { f0, f1 }
+    }
+
+    #[cfg(any(target_arch = "arm", target_arch = "wasm32", target_arch = "x86"))]
+    /// Since `T` only has one tag (namely, `T`),
+    /// convert it to `T`'s payload.
+    pub fn into_T(self) -> (roc_std::RocStr, u32) {
+        (self.f0, self.f1)
+    }
+
+    #[cfg(any(target_arch = "arm", target_arch = "wasm32", target_arch = "x86"))]
+    /// Since `T` only has one tag (namely, `T`),
+    /// convert it to `T`'s payload.
+    pub fn as_T(&self) -> (&roc_std::RocStr, &u32) {
+        (&self.f0, &self.f1)
+    }
+
+    #[cfg(any(target_arch = "aarch64", target_arch = "x86_64"))]
+    /// A tag named T, with the given payload.
+    pub fn T(f0: roc_std::RocStr, f1: u64) -> Self {
+        Self { f0, f1 }
+    }
+
+    #[cfg(any(target_arch = "aarch64", target_arch = "x86_64"))]
+    /// Since `T` only has one tag (namely, `T`),
+    /// convert it to `T`'s payload.
+    pub fn into_T(self) -> (roc_std::RocStr, u64) {
+        (self.f0, self.f1)
+    }
+
+    #[cfg(any(target_arch = "aarch64", target_arch = "x86_64"))]
+    /// Since `T` only has one tag (namely, `T`),
+    /// convert it to `T`'s payload.
+    pub fn as_T(&self) -> (&roc_std::RocStr, &u64) {
+        (&self.f0, &self.f1)
+    }
+}
+
+impl core::fmt::Debug for Tuple1 {
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
+        f.debug_tuple("Tuple1::T")
+            .field(&self.f0)
+            .field(&self.f1)
+            .finish()
+    }
+}
+
+impl RocType {
+    #[cfg(any(target_arch = "arm", target_arch = "wasm32", target_arch = "x86"))]
+    /// Returns which variant this tag union holds. Note that this never includes a payload!
+    pub fn discriminant(&self) -> discriminant_RocType {
+        unsafe {
+            let bytes = core::mem::transmute::<&Self, &[u8; core::mem::size_of::()]>(self);
+
+            core::mem::transmute::(*bytes.as_ptr().add(48))
+        }
+    }
+
+    #[cfg(any(target_arch = "arm", target_arch = "wasm32", target_arch = "x86"))]
+    /// Internal helper
+    fn set_discriminant(&mut self, discriminant: discriminant_RocType) {
+        let discriminant_ptr: *mut discriminant_RocType = (self as *mut RocType).cast();
+
+        unsafe {
+            *(discriminant_ptr.add(48)) = discriminant;
+        }
+    }
+
+    #[cfg(any(target_arch = "arm", target_arch = "wasm32", target_arch = "x86"))]
+    /// A tag named Bool, which has no payload.
+    pub const Bool: Self = unsafe {
+        let mut bytes = [0; core::mem::size_of::()];
+
+        bytes[48] = discriminant_RocType::Bool as u8;
+
+        core::mem::transmute::<[u8; core::mem::size_of::()], RocType>(bytes)
+    };
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    /// Other `into_` methods return a payload, but since the Bool tag
+    /// has no payload, this does nothing and is only here for completeness.
+    pub fn into_Bool(self) {
+        ()
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    /// Other `as` methods return a payload, but since the Bool tag
+    /// has no payload, this does nothing and is only here for completeness.
+    pub fn as_Bool(&self) {
+        ()
+    }
+
+    #[cfg(any(target_arch = "arm", target_arch = "wasm32", target_arch = "x86"))]
+    /// A tag named EmptyTagUnion, which has no payload.
+    pub const EmptyTagUnion: Self = unsafe {
+        let mut bytes = [0; core::mem::size_of::()];
+
+        bytes[48] = discriminant_RocType::EmptyTagUnion as u8;
+
+        core::mem::transmute::<[u8; core::mem::size_of::()], RocType>(bytes)
+    };
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    /// Other `into_` methods return a payload, but since the EmptyTagUnion tag
+    /// has no payload, this does nothing and is only here for completeness.
+    pub fn into_EmptyTagUnion(self) {
+        ()
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    /// Other `as` methods return a payload, but since the EmptyTagUnion tag
+    /// has no payload, this does nothing and is only here for completeness.
+    pub fn as_EmptyTagUnion(&self) {
+        ()
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    /// Construct a tag named `Function`, with the appropriate payload
+    pub fn Function(arg0: RocFn) -> Self {
+        let mut answer = Self {
+            Function: core::mem::ManuallyDrop::new(arg0),
+        };
+
+        answer.set_discriminant(discriminant_RocType::Function);
+
+        answer
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    /// Unsafely assume the given `RocType` has a `.discriminant()` of `Function` and convert it to `Function`'s payload.
+    /// (Always examine `.discriminant()` first to make sure this is the correct variant!)
+    /// Panics in debug builds if the `.discriminant()` doesn't return `Function`.
+    pub unsafe fn into_Function(mut self) -> RocFn {
+        debug_assert_eq!(self.discriminant(), discriminant_RocType::Function);
+        let payload = {
+            let mut uninitialized = core::mem::MaybeUninit::uninit();
+            let swapped = unsafe {
+                core::mem::replace(
+                    &mut self.Function,
+                    core::mem::ManuallyDrop::new(uninitialized.assume_init()),
+                )
+            };
+
+            core::mem::forget(self);
+
+            core::mem::ManuallyDrop::into_inner(swapped)
+        };
+
+        payload
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    /// Unsafely assume the given `RocType` has a `.discriminant()` of `Function` and return its payload.
+    /// (Always examine `.discriminant()` first to make sure this is the correct variant!)
+    /// Panics in debug builds if the `.discriminant()` doesn't return `Function`.
+    pub unsafe fn as_Function(&self) -> &RocFn {
+        debug_assert_eq!(self.discriminant(), discriminant_RocType::Function);
+        let payload = &self.Function;
+
+        payload
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    /// Construct a tag named `Num`, with the appropriate payload
+    pub fn Num(arg: RocNum) -> Self {
+        let mut answer = Self { Num: arg };
+
+        answer.set_discriminant(discriminant_RocType::Num);
+
+        answer
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    /// Unsafely assume the given `RocType` has a `.discriminant()` of `Num` and convert it to `Num`'s payload.
+    /// (Always examine `.discriminant()` first to make sure this is the correct variant!)
+    /// Panics in debug builds if the `.discriminant()` doesn't return `Num`.
+    pub unsafe fn into_Num(self) -> RocNum {
+        debug_assert_eq!(self.discriminant(), discriminant_RocType::Num);
+        let payload = self.Num;
+
+        payload
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    /// Unsafely assume the given `RocType` has a `.discriminant()` of `Num` and return its payload.
+    /// (Always examine `.discriminant()` first to make sure this is the correct variant!)
+    /// Panics in debug builds if the `.discriminant()` doesn't return `Num`.
+    pub unsafe fn as_Num(&self) -> &RocNum {
+        debug_assert_eq!(self.discriminant(), discriminant_RocType::Num);
+        let payload = &self.Num;
+
+        &payload
+    }
+
+    #[cfg(any(target_arch = "arm", target_arch = "wasm32", target_arch = "x86"))]
+    /// Construct a tag named `RecursivePointer`, with the appropriate payload
+    pub fn RecursivePointer(arg: u32) -> Self {
+        let mut answer = Self {
+            RecursivePointer: arg,
+        };
+
+        answer.set_discriminant(discriminant_RocType::RecursivePointer);
+
+        answer
+    }
+
+    #[cfg(any(target_arch = "arm", target_arch = "wasm32", target_arch = "x86"))]
+    /// Unsafely assume the given `RocType` has a `.discriminant()` of `RecursivePointer` and convert it to `RecursivePointer`'s payload.
+    /// (Always examine `.discriminant()` first to make sure this is the correct variant!)
+    /// Panics in debug builds if the `.discriminant()` doesn't return `RecursivePointer`.
+    pub unsafe fn into_RecursivePointer(self) -> u32 {
+        debug_assert_eq!(self.discriminant(), discriminant_RocType::RecursivePointer);
+        let payload = self.RecursivePointer;
+
+        payload
+    }
+
+    #[cfg(any(target_arch = "arm", target_arch = "wasm32", target_arch = "x86"))]
+    /// Unsafely assume the given `RocType` has a `.discriminant()` of `RecursivePointer` and return its payload.
+    /// (Always examine `.discriminant()` first to make sure this is the correct variant!)
+    /// Panics in debug builds if the `.discriminant()` doesn't return `RecursivePointer`.
+    pub unsafe fn as_RecursivePointer(&self) -> &u32 {
+        debug_assert_eq!(self.discriminant(), discriminant_RocType::RecursivePointer);
+        let payload = &self.RecursivePointer;
+
+        &payload
+    }
+
+    #[cfg(any(target_arch = "arm", target_arch = "wasm32", target_arch = "x86"))]
+    /// Construct a tag named `RocBox`, with the appropriate payload
+    pub fn RocBox(arg: u32) -> Self {
+        let mut answer = Self { RocBox: arg };
+
+        answer.set_discriminant(discriminant_RocType::RocBox);
+
+        answer
+    }
+
+    #[cfg(any(target_arch = "arm", target_arch = "wasm32", target_arch = "x86"))]
+    /// Unsafely assume the given `RocType` has a `.discriminant()` of `RocBox` and convert it to `RocBox`'s payload.
+    /// (Always examine `.discriminant()` first to make sure this is the correct variant!)
+    /// Panics in debug builds if the `.discriminant()` doesn't return `RocBox`.
+    pub unsafe fn into_RocBox(self) -> u32 {
+        debug_assert_eq!(self.discriminant(), discriminant_RocType::RocBox);
+        let payload = self.RocBox;
+
+        payload
+    }
+
+    #[cfg(any(target_arch = "arm", target_arch = "wasm32", target_arch = "x86"))]
+    /// Unsafely assume the given `RocType` has a `.discriminant()` of `RocBox` and return its payload.
+    /// (Always examine `.discriminant()` first to make sure this is the correct variant!)
+    /// Panics in debug builds if the `.discriminant()` doesn't return `RocBox`.
+    pub unsafe fn as_RocBox(&self) -> &u32 {
+        debug_assert_eq!(self.discriminant(), discriminant_RocType::RocBox);
+        let payload = &self.RocBox;
+
+        &payload
+    }
+
+    #[cfg(any(target_arch = "arm", target_arch = "wasm32", target_arch = "x86"))]
+    /// Construct a tag named `RocDict`, with the appropriate payload
+    pub fn RocDict(arg0: u32, arg1: u32) -> Self {
+        let mut answer = Self {
+            RocDict: RocType_RocDict { f0: arg0, f1: arg1 },
+        };
+
+        answer.set_discriminant(discriminant_RocType::RocDict);
+
+        answer
+    }
+
+    #[cfg(any(target_arch = "arm", target_arch = "wasm32", target_arch = "x86"))]
+    /// Unsafely assume the given `RocType` has a `.discriminant()` of `RocDict` and convert it to `RocDict`'s payload.
+    /// (Always examine `.discriminant()` first to make sure this is the correct variant!)
+    /// Panics in debug builds if the `.discriminant()` doesn't return `RocDict`.
+    pub unsafe fn into_RocDict(self) -> (u32, u32) {
+        debug_assert_eq!(self.discriminant(), discriminant_RocType::RocDict);
+        let payload = self.RocDict;
+
+        (payload.f0, payload.f1)
+    }
+
+    #[cfg(any(target_arch = "arm", target_arch = "wasm32", target_arch = "x86"))]
+    /// Unsafely assume the given `RocType` has a `.discriminant()` of `RocDict` and return its payload.
+    /// (Always examine `.discriminant()` first to make sure this is the correct variant!)
+    /// Panics in debug builds if the `.discriminant()` doesn't return `RocDict`.
+    pub unsafe fn as_RocDict(&self) -> (&u32, &u32) {
+        debug_assert_eq!(self.discriminant(), discriminant_RocType::RocDict);
+        let payload = &self.RocDict;
+
+        (&payload.f0, &payload.f1)
+    }
+
+    #[cfg(any(target_arch = "arm", target_arch = "wasm32", target_arch = "x86"))]
+    /// Construct a tag named `RocList`, with the appropriate payload
+    pub fn RocList(arg: u32) -> Self {
+        let mut answer = Self { RocList: arg };
+
+        answer.set_discriminant(discriminant_RocType::RocList);
+
+        answer
+    }
+
+    #[cfg(any(target_arch = "arm", target_arch = "wasm32", target_arch = "x86"))]
+    /// Unsafely assume the given `RocType` has a `.discriminant()` of `RocList` and convert it to `RocList`'s payload.
+    /// (Always examine `.discriminant()` first to make sure this is the correct variant!)
+    /// Panics in debug builds if the `.discriminant()` doesn't return `RocList`.
+    pub unsafe fn into_RocList(self) -> u32 {
+        debug_assert_eq!(self.discriminant(), discriminant_RocType::RocList);
+        let payload = self.RocList;
+
+        payload
+    }
+
+    #[cfg(any(target_arch = "arm", target_arch = "wasm32", target_arch = "x86"))]
+    /// Unsafely assume the given `RocType` has a `.discriminant()` of `RocList` and return its payload.
+    /// (Always examine `.discriminant()` first to make sure this is the correct variant!)
+    /// Panics in debug builds if the `.discriminant()` doesn't return `RocList`.
+    pub unsafe fn as_RocList(&self) -> &u32 {
+        debug_assert_eq!(self.discriminant(), discriminant_RocType::RocList);
+        let payload = &self.RocList;
+
+        &payload
+    }
+
+    #[cfg(any(target_arch = "arm", target_arch = "wasm32", target_arch = "x86"))]
+    /// Construct a tag named `RocResult`, with the appropriate payload
+    pub fn RocResult(arg0: u32, arg1: u32) -> Self {
+        let mut answer = Self {
+            RocResult: RocType_RocDict { f0: arg0, f1: arg1 },
+        };
+
+        answer.set_discriminant(discriminant_RocType::RocResult);
+
+        answer
+    }
+
+    #[cfg(any(target_arch = "arm", target_arch = "wasm32", target_arch = "x86"))]
+    /// Unsafely assume the given `RocType` has a `.discriminant()` of `RocResult` and convert it to `RocResult`'s payload.
+    /// (Always examine `.discriminant()` first to make sure this is the correct variant!)
+    /// Panics in debug builds if the `.discriminant()` doesn't return `RocResult`.
+    pub unsafe fn into_RocResult(self) -> (u32, u32) {
+        debug_assert_eq!(self.discriminant(), discriminant_RocType::RocResult);
+        let payload = self.RocResult;
+
+        (payload.f0, payload.f1)
+    }
+
+    #[cfg(any(target_arch = "arm", target_arch = "wasm32", target_arch = "x86"))]
+    /// Unsafely assume the given `RocType` has a `.discriminant()` of `RocResult` and return its payload.
+    /// (Always examine `.discriminant()` first to make sure this is the correct variant!)
+    /// Panics in debug builds if the `.discriminant()` doesn't return `RocResult`.
+    pub unsafe fn as_RocResult(&self) -> (&u32, &u32) {
+        debug_assert_eq!(self.discriminant(), discriminant_RocType::RocResult);
+        let payload = &self.RocResult;
+
+        (&payload.f0, &payload.f1)
+    }
+
+    #[cfg(any(target_arch = "arm", target_arch = "wasm32", target_arch = "x86"))]
+    /// Construct a tag named `RocSet`, with the appropriate payload
+    pub fn RocSet(arg: u32) -> Self {
+        let mut answer = Self { RocSet: arg };
+
+        answer.set_discriminant(discriminant_RocType::RocSet);
+
+        answer
+    }
+
+    #[cfg(any(target_arch = "arm", target_arch = "wasm32", target_arch = "x86"))]
+    /// Unsafely assume the given `RocType` has a `.discriminant()` of `RocSet` and convert it to `RocSet`'s payload.
+    /// (Always examine `.discriminant()` first to make sure this is the correct variant!)
+    /// Panics in debug builds if the `.discriminant()` doesn't return `RocSet`.
+    pub unsafe fn into_RocSet(self) -> u32 {
+        debug_assert_eq!(self.discriminant(), discriminant_RocType::RocSet);
+        let payload = self.RocSet;
+
+        payload
+    }
+
+    #[cfg(any(target_arch = "arm", target_arch = "wasm32", target_arch = "x86"))]
+    /// Unsafely assume the given `RocType` has a `.discriminant()` of `RocSet` and return its payload.
+    /// (Always examine `.discriminant()` first to make sure this is the correct variant!)
+    /// Panics in debug builds if the `.discriminant()` doesn't return `RocSet`.
+    pub unsafe fn as_RocSet(&self) -> &u32 {
+        debug_assert_eq!(self.discriminant(), discriminant_RocType::RocSet);
+        let payload = &self.RocSet;
+
+        &payload
+    }
+
+    #[cfg(any(target_arch = "arm", target_arch = "wasm32", target_arch = "x86"))]
+    /// A tag named RocStr, which has no payload.
+    pub const RocStr: Self = unsafe {
+        let mut bytes = [0; core::mem::size_of::()];
+
+        bytes[48] = discriminant_RocType::RocStr as u8;
+
+        core::mem::transmute::<[u8; core::mem::size_of::()], RocType>(bytes)
+    };
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    /// Other `into_` methods return a payload, but since the RocStr tag
+    /// has no payload, this does nothing and is only here for completeness.
+    pub fn into_RocStr(self) {
+        ()
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    /// Other `as` methods return a payload, but since the RocStr tag
+    /// has no payload, this does nothing and is only here for completeness.
+    pub fn as_RocStr(&self) {
+        ()
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    /// Construct a tag named `Struct`, with the appropriate payload
+    pub fn Struct(arg0: R1) -> Self {
+        let mut answer = Self {
+            Struct: core::mem::ManuallyDrop::new(arg0),
+        };
+
+        answer.set_discriminant(discriminant_RocType::Struct);
+
+        answer
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    /// Unsafely assume the given `RocType` has a `.discriminant()` of `Struct` and convert it to `Struct`'s payload.
+    /// (Always examine `.discriminant()` first to make sure this is the correct variant!)
+    /// Panics in debug builds if the `.discriminant()` doesn't return `Struct`.
+    pub unsafe fn into_Struct(mut self) -> R1 {
+        debug_assert_eq!(self.discriminant(), discriminant_RocType::Struct);
+        let payload = {
+            let mut uninitialized = core::mem::MaybeUninit::uninit();
+            let swapped = unsafe {
+                core::mem::replace(
+                    &mut self.Struct,
+                    core::mem::ManuallyDrop::new(uninitialized.assume_init()),
+                )
+            };
+
+            core::mem::forget(self);
+
+            core::mem::ManuallyDrop::into_inner(swapped)
+        };
+
+        payload
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    /// Unsafely assume the given `RocType` has a `.discriminant()` of `Struct` and return its payload.
+    /// (Always examine `.discriminant()` first to make sure this is the correct variant!)
+    /// Panics in debug builds if the `.discriminant()` doesn't return `Struct`.
+    pub unsafe fn as_Struct(&self) -> &R1 {
+        debug_assert_eq!(self.discriminant(), discriminant_RocType::Struct);
+        let payload = &self.Struct;
+
+        payload
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    /// Construct a tag named `TagUnion`, with the appropriate payload
+    pub fn TagUnion(arg: RocTagUnion) -> Self {
+        let mut answer = Self {
+            TagUnion: core::mem::ManuallyDrop::new(arg),
+        };
+
+        answer.set_discriminant(discriminant_RocType::TagUnion);
+
+        answer
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    /// Unsafely assume the given `RocType` has a `.discriminant()` of `TagUnion` and convert it to `TagUnion`'s payload.
+    /// (Always examine `.discriminant()` first to make sure this is the correct variant!)
+    /// Panics in debug builds if the `.discriminant()` doesn't return `TagUnion`.
+    pub unsafe fn into_TagUnion(mut self) -> RocTagUnion {
+        debug_assert_eq!(self.discriminant(), discriminant_RocType::TagUnion);
+        let payload = {
+            let mut uninitialized = core::mem::MaybeUninit::uninit();
+            let swapped = unsafe {
+                core::mem::replace(
+                    &mut self.TagUnion,
+                    core::mem::ManuallyDrop::new(uninitialized.assume_init()),
+                )
+            };
+
+            core::mem::forget(self);
+
+            core::mem::ManuallyDrop::into_inner(swapped)
+        };
+
+        payload
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    /// Unsafely assume the given `RocType` has a `.discriminant()` of `TagUnion` and return its payload.
+    /// (Always examine `.discriminant()` first to make sure this is the correct variant!)
+    /// Panics in debug builds if the `.discriminant()` doesn't return `TagUnion`.
+    pub unsafe fn as_TagUnion(&self) -> &RocTagUnion {
+        debug_assert_eq!(self.discriminant(), discriminant_RocType::TagUnion);
+        let payload = &self.TagUnion;
+
+        &payload
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    /// Construct a tag named `TagUnionPayload`, with the appropriate payload
+    pub fn TagUnionPayload(arg0: R1) -> Self {
+        let mut answer = Self {
+            TagUnionPayload: core::mem::ManuallyDrop::new(arg0),
+        };
+
+        answer.set_discriminant(discriminant_RocType::TagUnionPayload);
+
+        answer
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    /// Unsafely assume the given `RocType` has a `.discriminant()` of `TagUnionPayload` and convert it to `TagUnionPayload`'s payload.
+    /// (Always examine `.discriminant()` first to make sure this is the correct variant!)
+    /// Panics in debug builds if the `.discriminant()` doesn't return `TagUnionPayload`.
+    pub unsafe fn into_TagUnionPayload(mut self) -> R1 {
+        debug_assert_eq!(self.discriminant(), discriminant_RocType::TagUnionPayload);
+        let payload = {
+            let mut uninitialized = core::mem::MaybeUninit::uninit();
+            let swapped = unsafe {
+                core::mem::replace(
+                    &mut self.TagUnionPayload,
+                    core::mem::ManuallyDrop::new(uninitialized.assume_init()),
+                )
+            };
+
+            core::mem::forget(self);
+
+            core::mem::ManuallyDrop::into_inner(swapped)
+        };
+
+        payload
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    /// Unsafely assume the given `RocType` has a `.discriminant()` of `TagUnionPayload` and return its payload.
+    /// (Always examine `.discriminant()` first to make sure this is the correct variant!)
+    /// Panics in debug builds if the `.discriminant()` doesn't return `TagUnionPayload`.
+    pub unsafe fn as_TagUnionPayload(&self) -> &R1 {
+        debug_assert_eq!(self.discriminant(), discriminant_RocType::TagUnionPayload);
+        let payload = &self.TagUnionPayload;
+
+        payload
+    }
+
+    #[cfg(any(target_arch = "arm", target_arch = "wasm32", target_arch = "x86"))]
+    /// A tag named Unit, which has no payload.
+    pub const Unit: Self = unsafe {
+        let mut bytes = [0; core::mem::size_of::()];
+
+        bytes[48] = discriminant_RocType::Unit as u8;
+
+        core::mem::transmute::<[u8; core::mem::size_of::()], RocType>(bytes)
+    };
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    /// Other `into_` methods return a payload, but since the Unit tag
+    /// has no payload, this does nothing and is only here for completeness.
+    pub fn into_Unit(self) {
+        ()
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    /// Other `as` methods return a payload, but since the Unit tag
+    /// has no payload, this does nothing and is only here for completeness.
+    pub fn as_Unit(&self) {
+        ()
+    }
+
+    #[cfg(any(target_arch = "arm", target_arch = "wasm32", target_arch = "x86"))]
+    /// A tag named Unsized, which has no payload.
+    pub const Unsized: Self = unsafe {
+        let mut bytes = [0; core::mem::size_of::()];
+
+        bytes[48] = discriminant_RocType::Unsized as u8;
+
+        core::mem::transmute::<[u8; core::mem::size_of::()], RocType>(bytes)
+    };
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    /// Other `into_` methods return a payload, but since the Unsized tag
+    /// has no payload, this does nothing and is only here for completeness.
+    pub fn into_Unsized(self) {
+        ()
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    /// Other `as` methods return a payload, but since the Unsized tag
+    /// has no payload, this does nothing and is only here for completeness.
+    pub fn as_Unsized(&self) {
+        ()
+    }
+
+    #[cfg(any(target_arch = "aarch64", target_arch = "x86_64"))]
+    /// Returns which variant this tag union holds. Note that this never includes a payload!
+    pub fn discriminant(&self) -> discriminant_RocType {
+        unsafe {
+            let bytes = core::mem::transmute::<&Self, &[u8; core::mem::size_of::()]>(self);
+
+            core::mem::transmute::(*bytes.as_ptr().add(96))
+        }
+    }
+
+    #[cfg(any(target_arch = "aarch64", target_arch = "x86_64"))]
+    /// Internal helper
+    fn set_discriminant(&mut self, discriminant: discriminant_RocType) {
+        let discriminant_ptr: *mut discriminant_RocType = (self as *mut RocType).cast();
+
+        unsafe {
+            *(discriminant_ptr.add(96)) = discriminant;
+        }
+    }
+
+    #[cfg(any(target_arch = "aarch64", target_arch = "x86_64"))]
+    /// A tag named Bool, which has no payload.
+    pub const Bool: Self = unsafe {
+        let mut bytes = [0; core::mem::size_of::()];
+
+        bytes[96] = discriminant_RocType::Bool as u8;
+
+        core::mem::transmute::<[u8; core::mem::size_of::()], RocType>(bytes)
+    };
+
+    #[cfg(any(target_arch = "aarch64", target_arch = "x86_64"))]
+    /// A tag named EmptyTagUnion, which has no payload.
+    pub const EmptyTagUnion: Self = unsafe {
+        let mut bytes = [0; core::mem::size_of::()];
+
+        bytes[96] = discriminant_RocType::EmptyTagUnion as u8;
+
+        core::mem::transmute::<[u8; core::mem::size_of::()], RocType>(bytes)
+    };
+
+    #[cfg(any(target_arch = "aarch64", target_arch = "x86_64"))]
+    /// Construct a tag named `RecursivePointer`, with the appropriate payload
+    pub fn RecursivePointer(arg: u64) -> Self {
+        let mut answer = Self {
+            RecursivePointer: arg,
+        };
+
+        answer.set_discriminant(discriminant_RocType::RecursivePointer);
+
+        answer
+    }
+
+    #[cfg(any(target_arch = "aarch64", target_arch = "x86_64"))]
+    /// Unsafely assume the given `RocType` has a `.discriminant()` of `RecursivePointer` and convert it to `RecursivePointer`'s payload.
+    /// (Always examine `.discriminant()` first to make sure this is the correct variant!)
+    /// Panics in debug builds if the `.discriminant()` doesn't return `RecursivePointer`.
+    pub unsafe fn into_RecursivePointer(self) -> u64 {
+        debug_assert_eq!(self.discriminant(), discriminant_RocType::RecursivePointer);
+        let payload = self.RecursivePointer;
+
+        payload
+    }
+
+    #[cfg(any(target_arch = "aarch64", target_arch = "x86_64"))]
+    /// Unsafely assume the given `RocType` has a `.discriminant()` of `RecursivePointer` and return its payload.
+    /// (Always examine `.discriminant()` first to make sure this is the correct variant!)
+    /// Panics in debug builds if the `.discriminant()` doesn't return `RecursivePointer`.
+    pub unsafe fn as_RecursivePointer(&self) -> &u64 {
+        debug_assert_eq!(self.discriminant(), discriminant_RocType::RecursivePointer);
+        let payload = &self.RecursivePointer;
+
+        &payload
+    }
+
+    #[cfg(any(target_arch = "aarch64", target_arch = "x86_64"))]
+    /// Construct a tag named `RocBox`, with the appropriate payload
+    pub fn RocBox(arg: u64) -> Self {
+        let mut answer = Self { RocBox: arg };
+
+        answer.set_discriminant(discriminant_RocType::RocBox);
+
+        answer
+    }
+
+    #[cfg(any(target_arch = "aarch64", target_arch = "x86_64"))]
+    /// Unsafely assume the given `RocType` has a `.discriminant()` of `RocBox` and convert it to `RocBox`'s payload.
+    /// (Always examine `.discriminant()` first to make sure this is the correct variant!)
+    /// Panics in debug builds if the `.discriminant()` doesn't return `RocBox`.
+    pub unsafe fn into_RocBox(self) -> u64 {
+        debug_assert_eq!(self.discriminant(), discriminant_RocType::RocBox);
+        let payload = self.RocBox;
+
+        payload
+    }
+
+    #[cfg(any(target_arch = "aarch64", target_arch = "x86_64"))]
+    /// Unsafely assume the given `RocType` has a `.discriminant()` of `RocBox` and return its payload.
+    /// (Always examine `.discriminant()` first to make sure this is the correct variant!)
+    /// Panics in debug builds if the `.discriminant()` doesn't return `RocBox`.
+    pub unsafe fn as_RocBox(&self) -> &u64 {
+        debug_assert_eq!(self.discriminant(), discriminant_RocType::RocBox);
+        let payload = &self.RocBox;
+
+        &payload
+    }
+
+    #[cfg(any(target_arch = "aarch64", target_arch = "x86_64"))]
+    /// Construct a tag named `RocDict`, with the appropriate payload
+    pub fn RocDict(arg0: u64, arg1: u64) -> Self {
+        let mut answer = Self {
+            RocDict: RocType_RocDict { f0: arg0, f1: arg1 },
+        };
+
+        answer.set_discriminant(discriminant_RocType::RocDict);
+
+        answer
+    }
+
+    #[cfg(any(target_arch = "aarch64", target_arch = "x86_64"))]
+    /// Unsafely assume the given `RocType` has a `.discriminant()` of `RocDict` and convert it to `RocDict`'s payload.
+    /// (Always examine `.discriminant()` first to make sure this is the correct variant!)
+    /// Panics in debug builds if the `.discriminant()` doesn't return `RocDict`.
+    pub unsafe fn into_RocDict(self) -> (u64, u64) {
+        debug_assert_eq!(self.discriminant(), discriminant_RocType::RocDict);
+        let payload = self.RocDict;
+
+        (payload.f0, payload.f1)
+    }
+
+    #[cfg(any(target_arch = "aarch64", target_arch = "x86_64"))]
+    /// Unsafely assume the given `RocType` has a `.discriminant()` of `RocDict` and return its payload.
+    /// (Always examine `.discriminant()` first to make sure this is the correct variant!)
+    /// Panics in debug builds if the `.discriminant()` doesn't return `RocDict`.
+    pub unsafe fn as_RocDict(&self) -> (&u64, &u64) {
+        debug_assert_eq!(self.discriminant(), discriminant_RocType::RocDict);
+        let payload = &self.RocDict;
+
+        (&payload.f0, &payload.f1)
+    }
+
+    #[cfg(any(target_arch = "aarch64", target_arch = "x86_64"))]
+    /// Construct a tag named `RocList`, with the appropriate payload
+    pub fn RocList(arg: u64) -> Self {
+        let mut answer = Self { RocList: arg };
+
+        answer.set_discriminant(discriminant_RocType::RocList);
+
+        answer
+    }
+
+    #[cfg(any(target_arch = "aarch64", target_arch = "x86_64"))]
+    /// Unsafely assume the given `RocType` has a `.discriminant()` of `RocList` and convert it to `RocList`'s payload.
+    /// (Always examine `.discriminant()` first to make sure this is the correct variant!)
+    /// Panics in debug builds if the `.discriminant()` doesn't return `RocList`.
+    pub unsafe fn into_RocList(self) -> u64 {
+        debug_assert_eq!(self.discriminant(), discriminant_RocType::RocList);
+        let payload = self.RocList;
+
+        payload
+    }
+
+    #[cfg(any(target_arch = "aarch64", target_arch = "x86_64"))]
+    /// Unsafely assume the given `RocType` has a `.discriminant()` of `RocList` and return its payload.
+    /// (Always examine `.discriminant()` first to make sure this is the correct variant!)
+    /// Panics in debug builds if the `.discriminant()` doesn't return `RocList`.
+    pub unsafe fn as_RocList(&self) -> &u64 {
+        debug_assert_eq!(self.discriminant(), discriminant_RocType::RocList);
+        let payload = &self.RocList;
+
+        &payload
+    }
+
+    #[cfg(any(target_arch = "aarch64", target_arch = "x86_64"))]
+    /// Construct a tag named `RocResult`, with the appropriate payload
+    pub fn RocResult(arg0: u64, arg1: u64) -> Self {
+        let mut answer = Self {
+            RocResult: RocType_RocDict { f0: arg0, f1: arg1 },
+        };
+
+        answer.set_discriminant(discriminant_RocType::RocResult);
+
+        answer
+    }
+
+    #[cfg(any(target_arch = "aarch64", target_arch = "x86_64"))]
+    /// Unsafely assume the given `RocType` has a `.discriminant()` of `RocResult` and convert it to `RocResult`'s payload.
+    /// (Always examine `.discriminant()` first to make sure this is the correct variant!)
+    /// Panics in debug builds if the `.discriminant()` doesn't return `RocResult`.
+    pub unsafe fn into_RocResult(self) -> (u64, u64) {
+        debug_assert_eq!(self.discriminant(), discriminant_RocType::RocResult);
+        let payload = self.RocResult;
+
+        (payload.f0, payload.f1)
+    }
+
+    #[cfg(any(target_arch = "aarch64", target_arch = "x86_64"))]
+    /// Unsafely assume the given `RocType` has a `.discriminant()` of `RocResult` and return its payload.
+    /// (Always examine `.discriminant()` first to make sure this is the correct variant!)
+    /// Panics in debug builds if the `.discriminant()` doesn't return `RocResult`.
+    pub unsafe fn as_RocResult(&self) -> (&u64, &u64) {
+        debug_assert_eq!(self.discriminant(), discriminant_RocType::RocResult);
+        let payload = &self.RocResult;
+
+        (&payload.f0, &payload.f1)
+    }
+
+    #[cfg(any(target_arch = "aarch64", target_arch = "x86_64"))]
+    /// Construct a tag named `RocSet`, with the appropriate payload
+    pub fn RocSet(arg: u64) -> Self {
+        let mut answer = Self { RocSet: arg };
+
+        answer.set_discriminant(discriminant_RocType::RocSet);
+
+        answer
+    }
+
+    #[cfg(any(target_arch = "aarch64", target_arch = "x86_64"))]
+    /// Unsafely assume the given `RocType` has a `.discriminant()` of `RocSet` and convert it to `RocSet`'s payload.
+    /// (Always examine `.discriminant()` first to make sure this is the correct variant!)
+    /// Panics in debug builds if the `.discriminant()` doesn't return `RocSet`.
+    pub unsafe fn into_RocSet(self) -> u64 {
+        debug_assert_eq!(self.discriminant(), discriminant_RocType::RocSet);
+        let payload = self.RocSet;
+
+        payload
+    }
+
+    #[cfg(any(target_arch = "aarch64", target_arch = "x86_64"))]
+    /// Unsafely assume the given `RocType` has a `.discriminant()` of `RocSet` and return its payload.
+    /// (Always examine `.discriminant()` first to make sure this is the correct variant!)
+    /// Panics in debug builds if the `.discriminant()` doesn't return `RocSet`.
+    pub unsafe fn as_RocSet(&self) -> &u64 {
+        debug_assert_eq!(self.discriminant(), discriminant_RocType::RocSet);
+        let payload = &self.RocSet;
+
+        &payload
+    }
+
+    #[cfg(any(target_arch = "aarch64", target_arch = "x86_64"))]
+    /// A tag named RocStr, which has no payload.
+    pub const RocStr: Self = unsafe {
+        let mut bytes = [0; core::mem::size_of::()];
+
+        bytes[96] = discriminant_RocType::RocStr as u8;
+
+        core::mem::transmute::<[u8; core::mem::size_of::()], RocType>(bytes)
+    };
+
+    #[cfg(any(target_arch = "aarch64", target_arch = "x86_64"))]
+    /// A tag named Unit, which has no payload.
+    pub const Unit: Self = unsafe {
+        let mut bytes = [0; core::mem::size_of::()];
+
+        bytes[96] = discriminant_RocType::Unit as u8;
+
+        core::mem::transmute::<[u8; core::mem::size_of::()], RocType>(bytes)
+    };
+
+    #[cfg(any(target_arch = "aarch64", target_arch = "x86_64"))]
+    /// A tag named Unsized, which has no payload.
+    pub const Unsized: Self = unsafe {
+        let mut bytes = [0; core::mem::size_of::()];
+
+        bytes[96] = discriminant_RocType::Unsized as u8;
+
+        core::mem::transmute::<[u8; core::mem::size_of::()], RocType>(bytes)
+    };
+}
+
+impl Drop for RocType {
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    fn drop(&mut self) {
+        // Drop the payloads
+        match self.discriminant() {
+            discriminant_RocType::Bool => {}
+            discriminant_RocType::EmptyTagUnion => {}
+            discriminant_RocType::Function => unsafe {
+                core::mem::ManuallyDrop::drop(&mut self.Function)
+            },
+            discriminant_RocType::Num => {}
+            discriminant_RocType::RecursivePointer => {}
+            discriminant_RocType::RocBox => {}
+            discriminant_RocType::RocDict => {}
+            discriminant_RocType::RocList => {}
+            discriminant_RocType::RocResult => {}
+            discriminant_RocType::RocSet => {}
+            discriminant_RocType::RocStr => {}
+            discriminant_RocType::Struct => unsafe {
+                core::mem::ManuallyDrop::drop(&mut self.Struct)
+            },
+            discriminant_RocType::TagUnion => unsafe {
+                core::mem::ManuallyDrop::drop(&mut self.TagUnion)
+            },
+            discriminant_RocType::TagUnionPayload => unsafe {
+                core::mem::ManuallyDrop::drop(&mut self.TagUnionPayload)
+            },
+            discriminant_RocType::Unit => {}
+            discriminant_RocType::Unsized => {}
+        }
+    }
+}
+
+impl Eq for RocType {}
+
+impl PartialEq for RocType {
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    fn eq(&self, other: &Self) -> bool {
+        if self.discriminant() != other.discriminant() {
+            return false;
+        }
+
+        unsafe {
+            match self.discriminant() {
+                discriminant_RocType::Bool => true,
+                discriminant_RocType::EmptyTagUnion => true,
+                discriminant_RocType::Function => self.Function == other.Function,
+                discriminant_RocType::Num => self.Num == other.Num,
+                discriminant_RocType::RecursivePointer => {
+                    self.RecursivePointer == other.RecursivePointer
+                }
+                discriminant_RocType::RocBox => self.RocBox == other.RocBox,
+                discriminant_RocType::RocDict => self.RocDict == other.RocDict,
+                discriminant_RocType::RocList => self.RocList == other.RocList,
+                discriminant_RocType::RocResult => self.RocResult == other.RocResult,
+                discriminant_RocType::RocSet => self.RocSet == other.RocSet,
+                discriminant_RocType::RocStr => true,
+                discriminant_RocType::Struct => self.Struct == other.Struct,
+                discriminant_RocType::TagUnion => self.TagUnion == other.TagUnion,
+                discriminant_RocType::TagUnionPayload => {
+                    self.TagUnionPayload == other.TagUnionPayload
+                }
+                discriminant_RocType::Unit => true,
+                discriminant_RocType::Unsized => true,
+            }
+        }
+    }
+}
+
+impl PartialOrd for RocType {
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    fn partial_cmp(&self, other: &Self) -> Option {
+        match self.discriminant().partial_cmp(&other.discriminant()) {
+            Some(core::cmp::Ordering::Equal) => {}
+            not_eq => return not_eq,
+        }
+
+        unsafe {
+            match self.discriminant() {
+                discriminant_RocType::Bool => Some(core::cmp::Ordering::Equal),
+                discriminant_RocType::EmptyTagUnion => Some(core::cmp::Ordering::Equal),
+                discriminant_RocType::Function => self.Function.partial_cmp(&other.Function),
+                discriminant_RocType::Num => self.Num.partial_cmp(&other.Num),
+                discriminant_RocType::RecursivePointer => {
+                    self.RecursivePointer.partial_cmp(&other.RecursivePointer)
+                }
+                discriminant_RocType::RocBox => self.RocBox.partial_cmp(&other.RocBox),
+                discriminant_RocType::RocDict => self.RocDict.partial_cmp(&other.RocDict),
+                discriminant_RocType::RocList => self.RocList.partial_cmp(&other.RocList),
+                discriminant_RocType::RocResult => self.RocResult.partial_cmp(&other.RocResult),
+                discriminant_RocType::RocSet => self.RocSet.partial_cmp(&other.RocSet),
+                discriminant_RocType::RocStr => Some(core::cmp::Ordering::Equal),
+                discriminant_RocType::Struct => self.Struct.partial_cmp(&other.Struct),
+                discriminant_RocType::TagUnion => self.TagUnion.partial_cmp(&other.TagUnion),
+                discriminant_RocType::TagUnionPayload => {
+                    self.TagUnionPayload.partial_cmp(&other.TagUnionPayload)
+                }
+                discriminant_RocType::Unit => Some(core::cmp::Ordering::Equal),
+                discriminant_RocType::Unsized => Some(core::cmp::Ordering::Equal),
+            }
+        }
+    }
+}
+
+impl Ord for RocType {
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
+        match self.discriminant().cmp(&other.discriminant()) {
+            core::cmp::Ordering::Equal => {}
+            not_eq => return not_eq,
+        }
+
+        unsafe {
+            match self.discriminant() {
+                discriminant_RocType::Bool => core::cmp::Ordering::Equal,
+                discriminant_RocType::EmptyTagUnion => core::cmp::Ordering::Equal,
+                discriminant_RocType::Function => self.Function.cmp(&other.Function),
+                discriminant_RocType::Num => self.Num.cmp(&other.Num),
+                discriminant_RocType::RecursivePointer => {
+                    self.RecursivePointer.cmp(&other.RecursivePointer)
+                }
+                discriminant_RocType::RocBox => self.RocBox.cmp(&other.RocBox),
+                discriminant_RocType::RocDict => self.RocDict.cmp(&other.RocDict),
+                discriminant_RocType::RocList => self.RocList.cmp(&other.RocList),
+                discriminant_RocType::RocResult => self.RocResult.cmp(&other.RocResult),
+                discriminant_RocType::RocSet => self.RocSet.cmp(&other.RocSet),
+                discriminant_RocType::RocStr => core::cmp::Ordering::Equal,
+                discriminant_RocType::Struct => self.Struct.cmp(&other.Struct),
+                discriminant_RocType::TagUnion => self.TagUnion.cmp(&other.TagUnion),
+                discriminant_RocType::TagUnionPayload => {
+                    self.TagUnionPayload.cmp(&other.TagUnionPayload)
+                }
+                discriminant_RocType::Unit => core::cmp::Ordering::Equal,
+                discriminant_RocType::Unsized => core::cmp::Ordering::Equal,
+            }
+        }
+    }
+}
+
+impl Clone for RocType {
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    fn clone(&self) -> Self {
+        let mut answer = unsafe {
+            match self.discriminant() {
+                discriminant_RocType::Bool => core::mem::transmute::<
+                    core::mem::MaybeUninit,
+                    RocType,
+                >(core::mem::MaybeUninit::uninit()),
+                discriminant_RocType::EmptyTagUnion => {
+                    core::mem::transmute::, RocType>(
+                        core::mem::MaybeUninit::uninit(),
+                    )
+                }
+                discriminant_RocType::Function => Self {
+                    Function: self.Function.clone(),
+                },
+                discriminant_RocType::Num => Self {
+                    Num: self.Num.clone(),
+                },
+                discriminant_RocType::RecursivePointer => Self {
+                    RecursivePointer: self.RecursivePointer.clone(),
+                },
+                discriminant_RocType::RocBox => Self {
+                    RocBox: self.RocBox.clone(),
+                },
+                discriminant_RocType::RocDict => Self {
+                    RocDict: self.RocDict.clone(),
+                },
+                discriminant_RocType::RocList => Self {
+                    RocList: self.RocList.clone(),
+                },
+                discriminant_RocType::RocResult => Self {
+                    RocResult: self.RocResult.clone(),
+                },
+                discriminant_RocType::RocSet => Self {
+                    RocSet: self.RocSet.clone(),
+                },
+                discriminant_RocType::RocStr => core::mem::transmute::<
+                    core::mem::MaybeUninit,
+                    RocType,
+                >(core::mem::MaybeUninit::uninit()),
+                discriminant_RocType::Struct => Self {
+                    Struct: self.Struct.clone(),
+                },
+                discriminant_RocType::TagUnion => Self {
+                    TagUnion: self.TagUnion.clone(),
+                },
+                discriminant_RocType::TagUnionPayload => Self {
+                    TagUnionPayload: self.TagUnionPayload.clone(),
+                },
+                discriminant_RocType::Unit => core::mem::transmute::<
+                    core::mem::MaybeUninit,
+                    RocType,
+                >(core::mem::MaybeUninit::uninit()),
+                discriminant_RocType::Unsized => core::mem::transmute::<
+                    core::mem::MaybeUninit,
+                    RocType,
+                >(core::mem::MaybeUninit::uninit()),
+            }
+        };
+
+        answer.set_discriminant(self.discriminant());
+
+        answer
+    }
+}
+
+impl core::hash::Hash for RocType {
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    fn hash(&self, state: &mut H) {
+        match self.discriminant() {
+            discriminant_RocType::Bool => discriminant_RocType::Bool.hash(state),
+            discriminant_RocType::EmptyTagUnion => discriminant_RocType::EmptyTagUnion.hash(state),
+            discriminant_RocType::Function => unsafe {
+                discriminant_RocType::Function.hash(state);
+                self.Function.hash(state);
+            },
+            discriminant_RocType::Num => unsafe {
+                discriminant_RocType::Num.hash(state);
+                self.Num.hash(state);
+            },
+            discriminant_RocType::RecursivePointer => unsafe {
+                discriminant_RocType::RecursivePointer.hash(state);
+                self.RecursivePointer.hash(state);
+            },
+            discriminant_RocType::RocBox => unsafe {
+                discriminant_RocType::RocBox.hash(state);
+                self.RocBox.hash(state);
+            },
+            discriminant_RocType::RocDict => unsafe {
+                discriminant_RocType::RocDict.hash(state);
+                self.RocDict.hash(state);
+            },
+            discriminant_RocType::RocList => unsafe {
+                discriminant_RocType::RocList.hash(state);
+                self.RocList.hash(state);
+            },
+            discriminant_RocType::RocResult => unsafe {
+                discriminant_RocType::RocResult.hash(state);
+                self.RocResult.hash(state);
+            },
+            discriminant_RocType::RocSet => unsafe {
+                discriminant_RocType::RocSet.hash(state);
+                self.RocSet.hash(state);
+            },
+            discriminant_RocType::RocStr => discriminant_RocType::RocStr.hash(state),
+            discriminant_RocType::Struct => unsafe {
+                discriminant_RocType::Struct.hash(state);
+                self.Struct.hash(state);
+            },
+            discriminant_RocType::TagUnion => unsafe {
+                discriminant_RocType::TagUnion.hash(state);
+                self.TagUnion.hash(state);
+            },
+            discriminant_RocType::TagUnionPayload => unsafe {
+                discriminant_RocType::TagUnionPayload.hash(state);
+                self.TagUnionPayload.hash(state);
+            },
+            discriminant_RocType::Unit => discriminant_RocType::Unit.hash(state),
+            discriminant_RocType::Unsized => discriminant_RocType::Unsized.hash(state),
+        }
+    }
+}
+
+impl core::fmt::Debug for RocType {
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
+        f.write_str("RocType::")?;
+
+        unsafe {
+            match self.discriminant() {
+                discriminant_RocType::Bool => f.write_str("Bool"),
+                discriminant_RocType::EmptyTagUnion => f.write_str("EmptyTagUnion"),
+                discriminant_RocType::Function => {
+                    f.debug_tuple("Function").field(&*self.Function).finish()
+                }
+                discriminant_RocType::Num => f.debug_tuple("Num").field(&self.Num).finish(),
+                discriminant_RocType::RecursivePointer => f
+                    .debug_tuple("RecursivePointer")
+                    .field(&self.RecursivePointer)
+                    .finish(),
+                discriminant_RocType::RocBox => {
+                    f.debug_tuple("RocBox").field(&self.RocBox).finish()
+                }
+                discriminant_RocType::RocDict => f
+                    .debug_tuple("RocDict")
+                    .field(&(&self.RocDict).f0)
+                    .field(&(&self.RocDict).f1)
+                    .finish(),
+                discriminant_RocType::RocList => {
+                    f.debug_tuple("RocList").field(&self.RocList).finish()
+                }
+                discriminant_RocType::RocResult => f
+                    .debug_tuple("RocResult")
+                    .field(&(&self.RocResult).f0)
+                    .field(&(&self.RocResult).f1)
+                    .finish(),
+                discriminant_RocType::RocSet => {
+                    f.debug_tuple("RocSet").field(&self.RocSet).finish()
+                }
+                discriminant_RocType::RocStr => f.write_str("RocStr"),
+                discriminant_RocType::Struct => {
+                    f.debug_tuple("Struct").field(&*self.Struct).finish()
+                }
+                discriminant_RocType::TagUnion => {
+                    f.debug_tuple("TagUnion").field(&*self.TagUnion).finish()
+                }
+                discriminant_RocType::TagUnionPayload => f
+                    .debug_tuple("TagUnionPayload")
+                    .field(&*self.TagUnionPayload)
+                    .finish(),
+                discriminant_RocType::Unit => f.write_str("Unit"),
+                discriminant_RocType::Unsized => f.write_str("Unsized"),
+            }
+        }
+    }
+}
+
+impl Tuple2 {
+    #[cfg(any(target_arch = "arm", target_arch = "wasm32", target_arch = "x86"))]
+    /// A tag named T, with the given payload.
+    pub fn T(f0: u32, f1: roc_std::RocList) -> Self {
+        Self { f0, f1 }
+    }
+
+    #[cfg(any(target_arch = "arm", target_arch = "wasm32", target_arch = "x86"))]
+    /// Since `T` only has one tag (namely, `T`),
+    /// convert it to `T`'s payload.
+    pub fn into_T(self) -> (u32, roc_std::RocList) {
+        (self.f0, self.f1)
+    }
+
+    #[cfg(any(target_arch = "arm", target_arch = "wasm32", target_arch = "x86"))]
+    /// Since `T` only has one tag (namely, `T`),
+    /// convert it to `T`'s payload.
+    pub fn as_T(&self) -> (&u32, &roc_std::RocList) {
+        (&self.f0, &self.f1)
+    }
+
+    #[cfg(any(target_arch = "aarch64", target_arch = "x86_64"))]
+    /// A tag named T, with the given payload.
+    pub fn T(f0: u64, f1: roc_std::RocList) -> Self {
+        Self { f0, f1 }
+    }
+
+    #[cfg(any(target_arch = "aarch64", target_arch = "x86_64"))]
+    /// Since `T` only has one tag (namely, `T`),
+    /// convert it to `T`'s payload.
+    pub fn into_T(self) -> (u64, roc_std::RocList) {
+        (self.f0, self.f1)
+    }
+
+    #[cfg(any(target_arch = "aarch64", target_arch = "x86_64"))]
+    /// Since `T` only has one tag (namely, `T`),
+    /// convert it to `T`'s payload.
+    pub fn as_T(&self) -> (&u64, &roc_std::RocList) {
+        (&self.f0, &self.f1)
+    }
+}
+
+impl core::fmt::Debug for Tuple2 {
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
+        f.debug_tuple("Tuple2::T")
+            .field(&self.f0)
+            .field(&self.f1)
+            .finish()
+    }
+}
+
+impl RocTagUnion {
+    #[cfg(any(target_arch = "arm", target_arch = "wasm32", target_arch = "x86"))]
+    /// Returns which variant this tag union holds. Note that this never includes a payload!
+    pub fn discriminant(&self) -> discriminant_RocTagUnion {
+        unsafe {
+            let bytes = core::mem::transmute::<&Self, &[u8; core::mem::size_of::()]>(self);
+
+            core::mem::transmute::(*bytes.as_ptr().add(44))
+        }
+    }
+
+    #[cfg(any(target_arch = "arm", target_arch = "wasm32", target_arch = "x86"))]
+    /// Internal helper
+    fn set_discriminant(&mut self, discriminant: discriminant_RocTagUnion) {
+        let discriminant_ptr: *mut discriminant_RocTagUnion = (self as *mut RocTagUnion).cast();
+
+        unsafe {
+            *(discriminant_ptr.add(44)) = discriminant;
+        }
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    /// Construct a tag named `Enumeration`, with the appropriate payload
+    pub fn Enumeration(arg0: R5) -> Self {
+        let mut answer = Self {
+            Enumeration: core::mem::ManuallyDrop::new(arg0),
+        };
+
+        answer.set_discriminant(discriminant_RocTagUnion::Enumeration);
+
+        answer
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    /// Unsafely assume the given `RocTagUnion` has a `.discriminant()` of `Enumeration` and convert it to `Enumeration`'s payload.
+    /// (Always examine `.discriminant()` first to make sure this is the correct variant!)
+    /// Panics in debug builds if the `.discriminant()` doesn't return `Enumeration`.
+    pub unsafe fn into_Enumeration(mut self) -> R5 {
+        debug_assert_eq!(self.discriminant(), discriminant_RocTagUnion::Enumeration);
+        let payload = {
+            let mut uninitialized = core::mem::MaybeUninit::uninit();
+            let swapped = unsafe {
+                core::mem::replace(
+                    &mut self.Enumeration,
+                    core::mem::ManuallyDrop::new(uninitialized.assume_init()),
+                )
+            };
+
+            core::mem::forget(self);
+
+            core::mem::ManuallyDrop::into_inner(swapped)
+        };
+
+        payload
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    /// Unsafely assume the given `RocTagUnion` has a `.discriminant()` of `Enumeration` and return its payload.
+    /// (Always examine `.discriminant()` first to make sure this is the correct variant!)
+    /// Panics in debug builds if the `.discriminant()` doesn't return `Enumeration`.
+    pub unsafe fn as_Enumeration(&self) -> &R5 {
+        debug_assert_eq!(self.discriminant(), discriminant_RocTagUnion::Enumeration);
+        let payload = &self.Enumeration;
+
+        payload
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    /// Construct a tag named `NonNullableUnwrapped`, with the appropriate payload
+    pub fn NonNullableUnwrapped(arg0: R6) -> Self {
+        let mut answer = Self {
+            NonNullableUnwrapped: core::mem::ManuallyDrop::new(arg0),
+        };
+
+        answer.set_discriminant(discriminant_RocTagUnion::NonNullableUnwrapped);
+
+        answer
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    /// Unsafely assume the given `RocTagUnion` has a `.discriminant()` of `NonNullableUnwrapped` and convert it to `NonNullableUnwrapped`'s payload.
+    /// (Always examine `.discriminant()` first to make sure this is the correct variant!)
+    /// Panics in debug builds if the `.discriminant()` doesn't return `NonNullableUnwrapped`.
+    pub unsafe fn into_NonNullableUnwrapped(mut self) -> R6 {
+        debug_assert_eq!(
+            self.discriminant(),
+            discriminant_RocTagUnion::NonNullableUnwrapped
+        );
+        let payload = {
+            let mut uninitialized = core::mem::MaybeUninit::uninit();
+            let swapped = unsafe {
+                core::mem::replace(
+                    &mut self.NonNullableUnwrapped,
+                    core::mem::ManuallyDrop::new(uninitialized.assume_init()),
+                )
+            };
+
+            core::mem::forget(self);
+
+            core::mem::ManuallyDrop::into_inner(swapped)
+        };
+
+        payload
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    /// Unsafely assume the given `RocTagUnion` has a `.discriminant()` of `NonNullableUnwrapped` and return its payload.
+    /// (Always examine `.discriminant()` first to make sure this is the correct variant!)
+    /// Panics in debug builds if the `.discriminant()` doesn't return `NonNullableUnwrapped`.
+    pub unsafe fn as_NonNullableUnwrapped(&self) -> &R6 {
+        debug_assert_eq!(
+            self.discriminant(),
+            discriminant_RocTagUnion::NonNullableUnwrapped
+        );
+        let payload = &self.NonNullableUnwrapped;
+
+        payload
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    /// Construct a tag named `NonRecursive`, with the appropriate payload
+    pub fn NonRecursive(arg0: R7) -> Self {
+        let mut answer = Self {
+            NonRecursive: core::mem::ManuallyDrop::new(arg0),
+        };
+
+        answer.set_discriminant(discriminant_RocTagUnion::NonRecursive);
+
+        answer
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    /// Unsafely assume the given `RocTagUnion` has a `.discriminant()` of `NonRecursive` and convert it to `NonRecursive`'s payload.
+    /// (Always examine `.discriminant()` first to make sure this is the correct variant!)
+    /// Panics in debug builds if the `.discriminant()` doesn't return `NonRecursive`.
+    pub unsafe fn into_NonRecursive(mut self) -> R7 {
+        debug_assert_eq!(self.discriminant(), discriminant_RocTagUnion::NonRecursive);
+        let payload = {
+            let mut uninitialized = core::mem::MaybeUninit::uninit();
+            let swapped = unsafe {
+                core::mem::replace(
+                    &mut self.NonRecursive,
+                    core::mem::ManuallyDrop::new(uninitialized.assume_init()),
+                )
+            };
+
+            core::mem::forget(self);
+
+            core::mem::ManuallyDrop::into_inner(swapped)
+        };
+
+        payload
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    /// Unsafely assume the given `RocTagUnion` has a `.discriminant()` of `NonRecursive` and return its payload.
+    /// (Always examine `.discriminant()` first to make sure this is the correct variant!)
+    /// Panics in debug builds if the `.discriminant()` doesn't return `NonRecursive`.
+    pub unsafe fn as_NonRecursive(&self) -> &R7 {
+        debug_assert_eq!(self.discriminant(), discriminant_RocTagUnion::NonRecursive);
+        let payload = &self.NonRecursive;
+
+        payload
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    /// Construct a tag named `NullableUnwrapped`, with the appropriate payload
+    pub fn NullableUnwrapped(arg0: R9) -> Self {
+        let mut answer = Self {
+            NullableUnwrapped: core::mem::ManuallyDrop::new(arg0),
+        };
+
+        answer.set_discriminant(discriminant_RocTagUnion::NullableUnwrapped);
+
+        answer
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    /// Unsafely assume the given `RocTagUnion` has a `.discriminant()` of `NullableUnwrapped` and convert it to `NullableUnwrapped`'s payload.
+    /// (Always examine `.discriminant()` first to make sure this is the correct variant!)
+    /// Panics in debug builds if the `.discriminant()` doesn't return `NullableUnwrapped`.
+    pub unsafe fn into_NullableUnwrapped(mut self) -> R9 {
+        debug_assert_eq!(
+            self.discriminant(),
+            discriminant_RocTagUnion::NullableUnwrapped
+        );
+        let payload = {
+            let mut uninitialized = core::mem::MaybeUninit::uninit();
+            let swapped = unsafe {
+                core::mem::replace(
+                    &mut self.NullableUnwrapped,
+                    core::mem::ManuallyDrop::new(uninitialized.assume_init()),
+                )
+            };
+
+            core::mem::forget(self);
+
+            core::mem::ManuallyDrop::into_inner(swapped)
+        };
+
+        payload
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    /// Unsafely assume the given `RocTagUnion` has a `.discriminant()` of `NullableUnwrapped` and return its payload.
+    /// (Always examine `.discriminant()` first to make sure this is the correct variant!)
+    /// Panics in debug builds if the `.discriminant()` doesn't return `NullableUnwrapped`.
+    pub unsafe fn as_NullableUnwrapped(&self) -> &R9 {
+        debug_assert_eq!(
+            self.discriminant(),
+            discriminant_RocTagUnion::NullableUnwrapped
+        );
+        let payload = &self.NullableUnwrapped;
+
+        payload
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    /// Construct a tag named `NullableWrapped`, with the appropriate payload
+    pub fn NullableWrapped(arg0: R10) -> Self {
+        let mut answer = Self {
+            NullableWrapped: core::mem::ManuallyDrop::new(arg0),
+        };
+
+        answer.set_discriminant(discriminant_RocTagUnion::NullableWrapped);
+
+        answer
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    /// Unsafely assume the given `RocTagUnion` has a `.discriminant()` of `NullableWrapped` and convert it to `NullableWrapped`'s payload.
+    /// (Always examine `.discriminant()` first to make sure this is the correct variant!)
+    /// Panics in debug builds if the `.discriminant()` doesn't return `NullableWrapped`.
+    pub unsafe fn into_NullableWrapped(mut self) -> R10 {
+        debug_assert_eq!(
+            self.discriminant(),
+            discriminant_RocTagUnion::NullableWrapped
+        );
+        let payload = {
+            let mut uninitialized = core::mem::MaybeUninit::uninit();
+            let swapped = unsafe {
+                core::mem::replace(
+                    &mut self.NullableWrapped,
+                    core::mem::ManuallyDrop::new(uninitialized.assume_init()),
+                )
+            };
+
+            core::mem::forget(self);
+
+            core::mem::ManuallyDrop::into_inner(swapped)
+        };
+
+        payload
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    /// Unsafely assume the given `RocTagUnion` has a `.discriminant()` of `NullableWrapped` and return its payload.
+    /// (Always examine `.discriminant()` first to make sure this is the correct variant!)
+    /// Panics in debug builds if the `.discriminant()` doesn't return `NullableWrapped`.
+    pub unsafe fn as_NullableWrapped(&self) -> &R10 {
+        debug_assert_eq!(
+            self.discriminant(),
+            discriminant_RocTagUnion::NullableWrapped
+        );
+        let payload = &self.NullableWrapped;
+
+        payload
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    /// Construct a tag named `Recursive`, with the appropriate payload
+    pub fn Recursive(arg0: R7) -> Self {
+        let mut answer = Self {
+            Recursive: core::mem::ManuallyDrop::new(arg0),
+        };
+
+        answer.set_discriminant(discriminant_RocTagUnion::Recursive);
+
+        answer
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    /// Unsafely assume the given `RocTagUnion` has a `.discriminant()` of `Recursive` and convert it to `Recursive`'s payload.
+    /// (Always examine `.discriminant()` first to make sure this is the correct variant!)
+    /// Panics in debug builds if the `.discriminant()` doesn't return `Recursive`.
+    pub unsafe fn into_Recursive(mut self) -> R7 {
+        debug_assert_eq!(self.discriminant(), discriminant_RocTagUnion::Recursive);
+        let payload = {
+            let mut uninitialized = core::mem::MaybeUninit::uninit();
+            let swapped = unsafe {
+                core::mem::replace(
+                    &mut self.Recursive,
+                    core::mem::ManuallyDrop::new(uninitialized.assume_init()),
+                )
+            };
+
+            core::mem::forget(self);
+
+            core::mem::ManuallyDrop::into_inner(swapped)
+        };
+
+        payload
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    /// Unsafely assume the given `RocTagUnion` has a `.discriminant()` of `Recursive` and return its payload.
+    /// (Always examine `.discriminant()` first to make sure this is the correct variant!)
+    /// Panics in debug builds if the `.discriminant()` doesn't return `Recursive`.
+    pub unsafe fn as_Recursive(&self) -> &R7 {
+        debug_assert_eq!(self.discriminant(), discriminant_RocTagUnion::Recursive);
+        let payload = &self.Recursive;
+
+        payload
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    /// Construct a tag named `SingleTagStruct`, with the appropriate payload
+    pub fn SingleTagStruct(arg0: R14) -> Self {
+        let mut answer = Self {
+            SingleTagStruct: core::mem::ManuallyDrop::new(arg0),
+        };
+
+        answer.set_discriminant(discriminant_RocTagUnion::SingleTagStruct);
+
+        answer
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    /// Unsafely assume the given `RocTagUnion` has a `.discriminant()` of `SingleTagStruct` and convert it to `SingleTagStruct`'s payload.
+    /// (Always examine `.discriminant()` first to make sure this is the correct variant!)
+    /// Panics in debug builds if the `.discriminant()` doesn't return `SingleTagStruct`.
+    pub unsafe fn into_SingleTagStruct(mut self) -> R14 {
+        debug_assert_eq!(
+            self.discriminant(),
+            discriminant_RocTagUnion::SingleTagStruct
+        );
+        let payload = {
+            let mut uninitialized = core::mem::MaybeUninit::uninit();
+            let swapped = unsafe {
+                core::mem::replace(
+                    &mut self.SingleTagStruct,
+                    core::mem::ManuallyDrop::new(uninitialized.assume_init()),
+                )
+            };
+
+            core::mem::forget(self);
+
+            core::mem::ManuallyDrop::into_inner(swapped)
+        };
+
+        payload
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    /// Unsafely assume the given `RocTagUnion` has a `.discriminant()` of `SingleTagStruct` and return its payload.
+    /// (Always examine `.discriminant()` first to make sure this is the correct variant!)
+    /// Panics in debug builds if the `.discriminant()` doesn't return `SingleTagStruct`.
+    pub unsafe fn as_SingleTagStruct(&self) -> &R14 {
+        debug_assert_eq!(
+            self.discriminant(),
+            discriminant_RocTagUnion::SingleTagStruct
+        );
+        let payload = &self.SingleTagStruct;
+
+        payload
+    }
+
+    #[cfg(any(target_arch = "aarch64", target_arch = "x86_64"))]
+    /// Returns which variant this tag union holds. Note that this never includes a payload!
+    pub fn discriminant(&self) -> discriminant_RocTagUnion {
+        unsafe {
+            let bytes = core::mem::transmute::<&Self, &[u8; core::mem::size_of::()]>(self);
+
+            core::mem::transmute::(*bytes.as_ptr().add(88))
+        }
+    }
+
+    #[cfg(any(target_arch = "aarch64", target_arch = "x86_64"))]
+    /// Internal helper
+    fn set_discriminant(&mut self, discriminant: discriminant_RocTagUnion) {
+        let discriminant_ptr: *mut discriminant_RocTagUnion = (self as *mut RocTagUnion).cast();
+
+        unsafe {
+            *(discriminant_ptr.add(88)) = discriminant;
+        }
+    }
+}
+
+impl Drop for RocTagUnion {
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    fn drop(&mut self) {
+        // Drop the payloads
+        match self.discriminant() {
+            discriminant_RocTagUnion::Enumeration => unsafe {
+                core::mem::ManuallyDrop::drop(&mut self.Enumeration)
+            },
+            discriminant_RocTagUnion::NonNullableUnwrapped => unsafe {
+                core::mem::ManuallyDrop::drop(&mut self.NonNullableUnwrapped)
+            },
+            discriminant_RocTagUnion::NonRecursive => unsafe {
+                core::mem::ManuallyDrop::drop(&mut self.NonRecursive)
+            },
+            discriminant_RocTagUnion::NullableUnwrapped => unsafe {
+                core::mem::ManuallyDrop::drop(&mut self.NullableUnwrapped)
+            },
+            discriminant_RocTagUnion::NullableWrapped => unsafe {
+                core::mem::ManuallyDrop::drop(&mut self.NullableWrapped)
+            },
+            discriminant_RocTagUnion::Recursive => unsafe {
+                core::mem::ManuallyDrop::drop(&mut self.Recursive)
+            },
+            discriminant_RocTagUnion::SingleTagStruct => unsafe {
+                core::mem::ManuallyDrop::drop(&mut self.SingleTagStruct)
+            },
+        }
+    }
+}
+
+impl Eq for RocTagUnion {}
+
+impl PartialEq for RocTagUnion {
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    fn eq(&self, other: &Self) -> bool {
+        if self.discriminant() != other.discriminant() {
+            return false;
+        }
+
+        unsafe {
+            match self.discriminant() {
+                discriminant_RocTagUnion::Enumeration => self.Enumeration == other.Enumeration,
+                discriminant_RocTagUnion::NonNullableUnwrapped => {
+                    self.NonNullableUnwrapped == other.NonNullableUnwrapped
+                }
+                discriminant_RocTagUnion::NonRecursive => self.NonRecursive == other.NonRecursive,
+                discriminant_RocTagUnion::NullableUnwrapped => {
+                    self.NullableUnwrapped == other.NullableUnwrapped
+                }
+                discriminant_RocTagUnion::NullableWrapped => {
+                    self.NullableWrapped == other.NullableWrapped
+                }
+                discriminant_RocTagUnion::Recursive => self.Recursive == other.Recursive,
+                discriminant_RocTagUnion::SingleTagStruct => {
+                    self.SingleTagStruct == other.SingleTagStruct
+                }
+            }
+        }
+    }
+}
+
+impl PartialOrd for RocTagUnion {
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    fn partial_cmp(&self, other: &Self) -> Option {
+        match self.discriminant().partial_cmp(&other.discriminant()) {
+            Some(core::cmp::Ordering::Equal) => {}
+            not_eq => return not_eq,
+        }
+
+        unsafe {
+            match self.discriminant() {
+                discriminant_RocTagUnion::Enumeration => {
+                    self.Enumeration.partial_cmp(&other.Enumeration)
+                }
+                discriminant_RocTagUnion::NonNullableUnwrapped => self
+                    .NonNullableUnwrapped
+                    .partial_cmp(&other.NonNullableUnwrapped),
+                discriminant_RocTagUnion::NonRecursive => {
+                    self.NonRecursive.partial_cmp(&other.NonRecursive)
+                }
+                discriminant_RocTagUnion::NullableUnwrapped => {
+                    self.NullableUnwrapped.partial_cmp(&other.NullableUnwrapped)
+                }
+                discriminant_RocTagUnion::NullableWrapped => {
+                    self.NullableWrapped.partial_cmp(&other.NullableWrapped)
+                }
+                discriminant_RocTagUnion::Recursive => self.Recursive.partial_cmp(&other.Recursive),
+                discriminant_RocTagUnion::SingleTagStruct => {
+                    self.SingleTagStruct.partial_cmp(&other.SingleTagStruct)
+                }
+            }
+        }
+    }
+}
+
+impl Ord for RocTagUnion {
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
+        match self.discriminant().cmp(&other.discriminant()) {
+            core::cmp::Ordering::Equal => {}
+            not_eq => return not_eq,
+        }
+
+        unsafe {
+            match self.discriminant() {
+                discriminant_RocTagUnion::Enumeration => self.Enumeration.cmp(&other.Enumeration),
+                discriminant_RocTagUnion::NonNullableUnwrapped => {
+                    self.NonNullableUnwrapped.cmp(&other.NonNullableUnwrapped)
+                }
+                discriminant_RocTagUnion::NonRecursive => {
+                    self.NonRecursive.cmp(&other.NonRecursive)
+                }
+                discriminant_RocTagUnion::NullableUnwrapped => {
+                    self.NullableUnwrapped.cmp(&other.NullableUnwrapped)
+                }
+                discriminant_RocTagUnion::NullableWrapped => {
+                    self.NullableWrapped.cmp(&other.NullableWrapped)
+                }
+                discriminant_RocTagUnion::Recursive => self.Recursive.cmp(&other.Recursive),
+                discriminant_RocTagUnion::SingleTagStruct => {
+                    self.SingleTagStruct.cmp(&other.SingleTagStruct)
+                }
+            }
+        }
+    }
+}
+
+impl Clone for RocTagUnion {
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    fn clone(&self) -> Self {
+        let mut answer = unsafe {
+            match self.discriminant() {
+                discriminant_RocTagUnion::Enumeration => Self {
+                    Enumeration: self.Enumeration.clone(),
+                },
+                discriminant_RocTagUnion::NonNullableUnwrapped => Self {
+                    NonNullableUnwrapped: self.NonNullableUnwrapped.clone(),
+                },
+                discriminant_RocTagUnion::NonRecursive => Self {
+                    NonRecursive: self.NonRecursive.clone(),
+                },
+                discriminant_RocTagUnion::NullableUnwrapped => Self {
+                    NullableUnwrapped: self.NullableUnwrapped.clone(),
+                },
+                discriminant_RocTagUnion::NullableWrapped => Self {
+                    NullableWrapped: self.NullableWrapped.clone(),
+                },
+                discriminant_RocTagUnion::Recursive => Self {
+                    Recursive: self.Recursive.clone(),
+                },
+                discriminant_RocTagUnion::SingleTagStruct => Self {
+                    SingleTagStruct: self.SingleTagStruct.clone(),
+                },
+            }
+        };
+
+        answer.set_discriminant(self.discriminant());
+
+        answer
+    }
+}
+
+impl core::hash::Hash for RocTagUnion {
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    fn hash(&self, state: &mut H) {
+        match self.discriminant() {
+            discriminant_RocTagUnion::Enumeration => unsafe {
+                discriminant_RocTagUnion::Enumeration.hash(state);
+                self.Enumeration.hash(state);
+            },
+            discriminant_RocTagUnion::NonNullableUnwrapped => unsafe {
+                discriminant_RocTagUnion::NonNullableUnwrapped.hash(state);
+                self.NonNullableUnwrapped.hash(state);
+            },
+            discriminant_RocTagUnion::NonRecursive => unsafe {
+                discriminant_RocTagUnion::NonRecursive.hash(state);
+                self.NonRecursive.hash(state);
+            },
+            discriminant_RocTagUnion::NullableUnwrapped => unsafe {
+                discriminant_RocTagUnion::NullableUnwrapped.hash(state);
+                self.NullableUnwrapped.hash(state);
+            },
+            discriminant_RocTagUnion::NullableWrapped => unsafe {
+                discriminant_RocTagUnion::NullableWrapped.hash(state);
+                self.NullableWrapped.hash(state);
+            },
+            discriminant_RocTagUnion::Recursive => unsafe {
+                discriminant_RocTagUnion::Recursive.hash(state);
+                self.Recursive.hash(state);
+            },
+            discriminant_RocTagUnion::SingleTagStruct => unsafe {
+                discriminant_RocTagUnion::SingleTagStruct.hash(state);
+                self.SingleTagStruct.hash(state);
+            },
+        }
+    }
+}
+
+impl core::fmt::Debug for RocTagUnion {
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
+        f.write_str("RocTagUnion::")?;
+
+        unsafe {
+            match self.discriminant() {
+                discriminant_RocTagUnion::Enumeration => f
+                    .debug_tuple("Enumeration")
+                    .field(&*self.Enumeration)
+                    .finish(),
+                discriminant_RocTagUnion::NonNullableUnwrapped => f
+                    .debug_tuple("NonNullableUnwrapped")
+                    .field(&*self.NonNullableUnwrapped)
+                    .finish(),
+                discriminant_RocTagUnion::NonRecursive => f
+                    .debug_tuple("NonRecursive")
+                    .field(&*self.NonRecursive)
+                    .finish(),
+                discriminant_RocTagUnion::NullableUnwrapped => f
+                    .debug_tuple("NullableUnwrapped")
+                    .field(&*self.NullableUnwrapped)
+                    .finish(),
+                discriminant_RocTagUnion::NullableWrapped => f
+                    .debug_tuple("NullableWrapped")
+                    .field(&*self.NullableWrapped)
+                    .finish(),
+                discriminant_RocTagUnion::Recursive => {
+                    f.debug_tuple("Recursive").field(&*self.Recursive).finish()
+                }
+                discriminant_RocTagUnion::SingleTagStruct => f
+                    .debug_tuple("SingleTagStruct")
+                    .field(&*self.SingleTagStruct)
+                    .finish(),
+            }
+        }
+    }
+}
+
+impl RocSingleTagPayload {
+    #[cfg(any(target_arch = "arm", target_arch = "wasm32", target_arch = "x86"))]
+    /// Returns which variant this tag union holds. Note that this never includes a payload!
+    pub fn discriminant(&self) -> discriminant_RocSingleTagPayload {
+        unsafe {
+            let bytes = core::mem::transmute::<&Self, &[u8; core::mem::size_of::()]>(self);
+
+            core::mem::transmute::(*bytes.as_ptr().add(12))
+        }
+    }
+
+    #[cfg(any(target_arch = "arm", target_arch = "wasm32", target_arch = "x86"))]
+    /// Internal helper
+    fn set_discriminant(&mut self, discriminant: discriminant_RocSingleTagPayload) {
+        let discriminant_ptr: *mut discriminant_RocSingleTagPayload =
+            (self as *mut RocSingleTagPayload).cast();
+
+        unsafe {
+            *(discriminant_ptr.add(12)) = discriminant;
+        }
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    /// Construct a tag named `HasClosure`, with the appropriate payload
+    pub fn HasClosure(arg: roc_std::RocList) -> Self {
+        let mut answer = Self {
+            HasClosure: core::mem::ManuallyDrop::new(arg),
+        };
+
+        answer.set_discriminant(discriminant_RocSingleTagPayload::HasClosure);
+
+        answer
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    /// Unsafely assume the given `RocSingleTagPayload` has a `.discriminant()` of `HasClosure` and convert it to `HasClosure`'s payload.
+    /// (Always examine `.discriminant()` first to make sure this is the correct variant!)
+    /// Panics in debug builds if the `.discriminant()` doesn't return `HasClosure`.
+    pub unsafe fn into_HasClosure(mut self) -> roc_std::RocList {
+        debug_assert_eq!(
+            self.discriminant(),
+            discriminant_RocSingleTagPayload::HasClosure
+        );
+        let payload = {
+            let mut uninitialized = core::mem::MaybeUninit::uninit();
+            let swapped = unsafe {
+                core::mem::replace(
+                    &mut self.HasClosure,
+                    core::mem::ManuallyDrop::new(uninitialized.assume_init()),
+                )
+            };
+
+            core::mem::forget(self);
+
+            core::mem::ManuallyDrop::into_inner(swapped)
+        };
+
+        payload
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    /// Unsafely assume the given `RocSingleTagPayload` has a `.discriminant()` of `HasClosure` and return its payload.
+    /// (Always examine `.discriminant()` first to make sure this is the correct variant!)
+    /// Panics in debug builds if the `.discriminant()` doesn't return `HasClosure`.
+    pub unsafe fn as_HasClosure(&self) -> &roc_std::RocList {
+        debug_assert_eq!(
+            self.discriminant(),
+            discriminant_RocSingleTagPayload::HasClosure
+        );
+        let payload = &self.HasClosure;
+
+        &payload
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    /// Construct a tag named `HasNoClosure`, with the appropriate payload
+    pub fn HasNoClosure(arg: roc_std::RocList) -> Self {
+        let mut answer = Self {
+            HasNoClosure: core::mem::ManuallyDrop::new(arg),
+        };
+
+        answer.set_discriminant(discriminant_RocSingleTagPayload::HasNoClosure);
+
+        answer
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    /// Unsafely assume the given `RocSingleTagPayload` has a `.discriminant()` of `HasNoClosure` and convert it to `HasNoClosure`'s payload.
+    /// (Always examine `.discriminant()` first to make sure this is the correct variant!)
+    /// Panics in debug builds if the `.discriminant()` doesn't return `HasNoClosure`.
+    pub unsafe fn into_HasNoClosure(mut self) -> roc_std::RocList {
+        debug_assert_eq!(
+            self.discriminant(),
+            discriminant_RocSingleTagPayload::HasNoClosure
+        );
+        let payload = {
+            let mut uninitialized = core::mem::MaybeUninit::uninit();
+            let swapped = unsafe {
+                core::mem::replace(
+                    &mut self.HasNoClosure,
+                    core::mem::ManuallyDrop::new(uninitialized.assume_init()),
+                )
+            };
+
+            core::mem::forget(self);
+
+            core::mem::ManuallyDrop::into_inner(swapped)
+        };
+
+        payload
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    /// Unsafely assume the given `RocSingleTagPayload` has a `.discriminant()` of `HasNoClosure` and return its payload.
+    /// (Always examine `.discriminant()` first to make sure this is the correct variant!)
+    /// Panics in debug builds if the `.discriminant()` doesn't return `HasNoClosure`.
+    pub unsafe fn as_HasNoClosure(&self) -> &roc_std::RocList {
+        debug_assert_eq!(
+            self.discriminant(),
+            discriminant_RocSingleTagPayload::HasNoClosure
+        );
+        let payload = &self.HasNoClosure;
+
+        &payload
+    }
+
+    #[cfg(any(target_arch = "aarch64", target_arch = "x86_64"))]
+    /// Returns which variant this tag union holds. Note that this never includes a payload!
+    pub fn discriminant(&self) -> discriminant_RocSingleTagPayload {
+        unsafe {
+            let bytes = core::mem::transmute::<&Self, &[u8; core::mem::size_of::()]>(self);
+
+            core::mem::transmute::(*bytes.as_ptr().add(24))
+        }
+    }
+
+    #[cfg(any(target_arch = "aarch64", target_arch = "x86_64"))]
+    /// Internal helper
+    fn set_discriminant(&mut self, discriminant: discriminant_RocSingleTagPayload) {
+        let discriminant_ptr: *mut discriminant_RocSingleTagPayload =
+            (self as *mut RocSingleTagPayload).cast();
+
+        unsafe {
+            *(discriminant_ptr.add(24)) = discriminant;
+        }
+    }
+}
+
+impl Drop for RocSingleTagPayload {
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    fn drop(&mut self) {
+        // Drop the payloads
+        match self.discriminant() {
+            discriminant_RocSingleTagPayload::HasClosure => unsafe {
+                core::mem::ManuallyDrop::drop(&mut self.HasClosure)
+            },
+            discriminant_RocSingleTagPayload::HasNoClosure => unsafe {
+                core::mem::ManuallyDrop::drop(&mut self.HasNoClosure)
+            },
+        }
+    }
+}
+
+impl Eq for RocSingleTagPayload {}
+
+impl PartialEq for RocSingleTagPayload {
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    fn eq(&self, other: &Self) -> bool {
+        if self.discriminant() != other.discriminant() {
+            return false;
+        }
+
+        unsafe {
+            match self.discriminant() {
+                discriminant_RocSingleTagPayload::HasClosure => self.HasClosure == other.HasClosure,
+                discriminant_RocSingleTagPayload::HasNoClosure => {
+                    self.HasNoClosure == other.HasNoClosure
+                }
+            }
+        }
+    }
+}
+
+impl PartialOrd for RocSingleTagPayload {
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    fn partial_cmp(&self, other: &Self) -> Option {
+        match self.discriminant().partial_cmp(&other.discriminant()) {
+            Some(core::cmp::Ordering::Equal) => {}
+            not_eq => return not_eq,
+        }
+
+        unsafe {
+            match self.discriminant() {
+                discriminant_RocSingleTagPayload::HasClosure => {
+                    self.HasClosure.partial_cmp(&other.HasClosure)
+                }
+                discriminant_RocSingleTagPayload::HasNoClosure => {
+                    self.HasNoClosure.partial_cmp(&other.HasNoClosure)
+                }
+            }
+        }
+    }
+}
+
+impl Ord for RocSingleTagPayload {
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
+        match self.discriminant().cmp(&other.discriminant()) {
+            core::cmp::Ordering::Equal => {}
+            not_eq => return not_eq,
+        }
+
+        unsafe {
+            match self.discriminant() {
+                discriminant_RocSingleTagPayload::HasClosure => {
+                    self.HasClosure.cmp(&other.HasClosure)
+                }
+                discriminant_RocSingleTagPayload::HasNoClosure => {
+                    self.HasNoClosure.cmp(&other.HasNoClosure)
+                }
+            }
+        }
+    }
+}
+
+impl Clone for RocSingleTagPayload {
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    fn clone(&self) -> Self {
+        let mut answer = unsafe {
+            match self.discriminant() {
+                discriminant_RocSingleTagPayload::HasClosure => Self {
+                    HasClosure: self.HasClosure.clone(),
+                },
+                discriminant_RocSingleTagPayload::HasNoClosure => Self {
+                    HasNoClosure: self.HasNoClosure.clone(),
+                },
+            }
+        };
+
+        answer.set_discriminant(self.discriminant());
+
+        answer
+    }
+}
+
+impl core::hash::Hash for RocSingleTagPayload {
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    fn hash(&self, state: &mut H) {
+        match self.discriminant() {
+            discriminant_RocSingleTagPayload::HasClosure => unsafe {
+                discriminant_RocSingleTagPayload::HasClosure.hash(state);
+                self.HasClosure.hash(state);
+            },
+            discriminant_RocSingleTagPayload::HasNoClosure => unsafe {
+                discriminant_RocSingleTagPayload::HasNoClosure.hash(state);
+                self.HasNoClosure.hash(state);
+            },
+        }
+    }
+}
+
+impl core::fmt::Debug for RocSingleTagPayload {
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
+        f.write_str("RocSingleTagPayload::")?;
+
+        unsafe {
+            match self.discriminant() {
+                discriminant_RocSingleTagPayload::HasClosure => f
+                    .debug_tuple("HasClosure")
+                    .field(&*self.HasClosure)
+                    .finish(),
+                discriminant_RocSingleTagPayload::HasNoClosure => f
+                    .debug_tuple("HasNoClosure")
+                    .field(&*self.HasNoClosure)
+                    .finish(),
+            }
+        }
+    }
+}
+
+impl U1 {
+    #[cfg(any(target_arch = "arm", target_arch = "wasm32", target_arch = "x86"))]
+    /// Returns which variant this tag union holds. Note that this never includes a payload!
+    pub fn discriminant(&self) -> discriminant_U1 {
+        unsafe {
+            let bytes = core::mem::transmute::<&Self, &[u8; core::mem::size_of::()]>(self);
+
+            core::mem::transmute::(*bytes.as_ptr().add(4))
+        }
+    }
+
+    #[cfg(any(target_arch = "arm", target_arch = "wasm32", target_arch = "x86"))]
+    /// Internal helper
+    fn set_discriminant(&mut self, discriminant: discriminant_U1) {
+        let discriminant_ptr: *mut discriminant_U1 = (self as *mut U1).cast();
+
+        unsafe {
+            *(discriminant_ptr.add(4)) = discriminant;
+        }
+    }
+
+    #[cfg(any(target_arch = "arm", target_arch = "wasm32", target_arch = "x86"))]
+    /// A tag named None, which has no payload.
+    pub const None: Self = unsafe {
+        let mut bytes = [0; core::mem::size_of::()];
+
+        bytes[4] = discriminant_U1::None as u8;
+
+        core::mem::transmute::<[u8; core::mem::size_of::()], U1>(bytes)
+    };
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    /// Other `into_` methods return a payload, but since the None tag
+    /// has no payload, this does nothing and is only here for completeness.
+    pub fn into_None(self) {
+        ()
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    /// Other `as` methods return a payload, but since the None tag
+    /// has no payload, this does nothing and is only here for completeness.
+    pub fn as_None(&self) {
+        ()
+    }
+
+    #[cfg(any(target_arch = "arm", target_arch = "wasm32", target_arch = "x86"))]
+    /// Construct a tag named `Some`, with the appropriate payload
+    pub fn Some(arg: u32) -> Self {
+        let mut answer = Self { Some: arg };
+
+        answer.set_discriminant(discriminant_U1::Some);
+
+        answer
+    }
+
+    #[cfg(any(target_arch = "arm", target_arch = "wasm32", target_arch = "x86"))]
+    /// Unsafely assume the given `U1` has a `.discriminant()` of `Some` and convert it to `Some`'s payload.
+    /// (Always examine `.discriminant()` first to make sure this is the correct variant!)
+    /// Panics in debug builds if the `.discriminant()` doesn't return `Some`.
+    pub unsafe fn into_Some(self) -> u32 {
+        debug_assert_eq!(self.discriminant(), discriminant_U1::Some);
+        let payload = self.Some;
+
+        payload
+    }
+
+    #[cfg(any(target_arch = "arm", target_arch = "wasm32", target_arch = "x86"))]
+    /// Unsafely assume the given `U1` has a `.discriminant()` of `Some` and return its payload.
+    /// (Always examine `.discriminant()` first to make sure this is the correct variant!)
+    /// Panics in debug builds if the `.discriminant()` doesn't return `Some`.
+    pub unsafe fn as_Some(&self) -> &u32 {
+        debug_assert_eq!(self.discriminant(), discriminant_U1::Some);
+        let payload = &self.Some;
+
+        &payload
+    }
+
+    #[cfg(any(target_arch = "aarch64", target_arch = "x86_64"))]
+    /// Returns which variant this tag union holds. Note that this never includes a payload!
+    pub fn discriminant(&self) -> discriminant_U1 {
+        unsafe {
+            let bytes = core::mem::transmute::<&Self, &[u8; core::mem::size_of::()]>(self);
+
+            core::mem::transmute::(*bytes.as_ptr().add(8))
+        }
+    }
+
+    #[cfg(any(target_arch = "aarch64", target_arch = "x86_64"))]
+    /// Internal helper
+    fn set_discriminant(&mut self, discriminant: discriminant_U1) {
+        let discriminant_ptr: *mut discriminant_U1 = (self as *mut U1).cast();
+
+        unsafe {
+            *(discriminant_ptr.add(8)) = discriminant;
+        }
+    }
+
+    #[cfg(any(target_arch = "aarch64", target_arch = "x86_64"))]
+    /// A tag named None, which has no payload.
+    pub const None: Self = unsafe {
+        let mut bytes = [0; core::mem::size_of::()];
+
+        bytes[8] = discriminant_U1::None as u8;
+
+        core::mem::transmute::<[u8; core::mem::size_of::()], U1>(bytes)
+    };
+
+    #[cfg(any(target_arch = "aarch64", target_arch = "x86_64"))]
+    /// Construct a tag named `Some`, with the appropriate payload
+    pub fn Some(arg: u64) -> Self {
+        let mut answer = Self { Some: arg };
+
+        answer.set_discriminant(discriminant_U1::Some);
+
+        answer
+    }
+
+    #[cfg(any(target_arch = "aarch64", target_arch = "x86_64"))]
+    /// Unsafely assume the given `U1` has a `.discriminant()` of `Some` and convert it to `Some`'s payload.
+    /// (Always examine `.discriminant()` first to make sure this is the correct variant!)
+    /// Panics in debug builds if the `.discriminant()` doesn't return `Some`.
+    pub unsafe fn into_Some(self) -> u64 {
+        debug_assert_eq!(self.discriminant(), discriminant_U1::Some);
+        let payload = self.Some;
+
+        payload
+    }
+
+    #[cfg(any(target_arch = "aarch64", target_arch = "x86_64"))]
+    /// Unsafely assume the given `U1` has a `.discriminant()` of `Some` and return its payload.
+    /// (Always examine `.discriminant()` first to make sure this is the correct variant!)
+    /// Panics in debug builds if the `.discriminant()` doesn't return `Some`.
+    pub unsafe fn as_Some(&self) -> &u64 {
+        debug_assert_eq!(self.discriminant(), discriminant_U1::Some);
+        let payload = &self.Some;
+
+        &payload
+    }
+}
+
+impl Eq for U1 {}
+
+impl PartialEq for U1 {
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    fn eq(&self, other: &Self) -> bool {
+        if self.discriminant() != other.discriminant() {
+            return false;
+        }
+
+        unsafe {
+            match self.discriminant() {
+                discriminant_U1::None => true,
+                discriminant_U1::Some => self.Some == other.Some,
+            }
+        }
+    }
+}
+
+impl PartialOrd for U1 {
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    fn partial_cmp(&self, other: &Self) -> Option {
+        match self.discriminant().partial_cmp(&other.discriminant()) {
+            Some(core::cmp::Ordering::Equal) => {}
+            not_eq => return not_eq,
+        }
+
+        unsafe {
+            match self.discriminant() {
+                discriminant_U1::None => Some(core::cmp::Ordering::Equal),
+                discriminant_U1::Some => self.Some.partial_cmp(&other.Some),
+            }
+        }
+    }
+}
+
+impl Ord for U1 {
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
+        match self.discriminant().cmp(&other.discriminant()) {
+            core::cmp::Ordering::Equal => {}
+            not_eq => return not_eq,
+        }
+
+        unsafe {
+            match self.discriminant() {
+                discriminant_U1::None => core::cmp::Ordering::Equal,
+                discriminant_U1::Some => self.Some.cmp(&other.Some),
+            }
+        }
+    }
+}
+
+impl Copy for U1 {}
+
+impl Clone for U1 {
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    fn clone(&self) -> Self {
+        let mut answer = unsafe {
+            match self.discriminant() {
+                discriminant_U1::None => core::mem::transmute::, U1>(
+                    core::mem::MaybeUninit::uninit(),
+                ),
+                discriminant_U1::Some => Self {
+                    Some: self.Some.clone(),
+                },
+            }
+        };
+
+        answer.set_discriminant(self.discriminant());
+
+        answer
+    }
+}
+
+impl core::hash::Hash for U1 {
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    fn hash(&self, state: &mut H) {
+        match self.discriminant() {
+            discriminant_U1::None => discriminant_U1::None.hash(state),
+            discriminant_U1::Some => unsafe {
+                discriminant_U1::Some.hash(state);
+                self.Some.hash(state);
+            },
+        }
+    }
+}
+
+impl core::fmt::Debug for U1 {
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
+        f.write_str("U1::")?;
+
+        unsafe {
+            match self.discriminant() {
+                discriminant_U1::None => f.write_str("None"),
+                discriminant_U1::Some => f.debug_tuple("Some").field(&self.Some).finish(),
+            }
+        }
+    }
+}
+
+impl RocStructFields {
+    #[cfg(any(target_arch = "arm", target_arch = "wasm32", target_arch = "x86"))]
+    /// Returns which variant this tag union holds. Note that this never includes a payload!
+    pub fn discriminant(&self) -> discriminant_RocStructFields {
+        unsafe {
+            let bytes = core::mem::transmute::<&Self, &[u8; core::mem::size_of::()]>(self);
+
+            core::mem::transmute::(*bytes.as_ptr().add(12))
+        }
+    }
+
+    #[cfg(any(target_arch = "arm", target_arch = "wasm32", target_arch = "x86"))]
+    /// Internal helper
+    fn set_discriminant(&mut self, discriminant: discriminant_RocStructFields) {
+        let discriminant_ptr: *mut discriminant_RocStructFields =
+            (self as *mut RocStructFields).cast();
+
+        unsafe {
+            *(discriminant_ptr.add(12)) = discriminant;
+        }
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    /// Construct a tag named `HasClosure`, with the appropriate payload
+    pub fn HasClosure(arg: roc_std::RocList) -> Self {
+        let mut answer = Self {
+            HasClosure: core::mem::ManuallyDrop::new(arg),
+        };
+
+        answer.set_discriminant(discriminant_RocStructFields::HasClosure);
+
+        answer
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    /// Unsafely assume the given `RocStructFields` has a `.discriminant()` of `HasClosure` and convert it to `HasClosure`'s payload.
+    /// (Always examine `.discriminant()` first to make sure this is the correct variant!)
+    /// Panics in debug builds if the `.discriminant()` doesn't return `HasClosure`.
+    pub unsafe fn into_HasClosure(mut self) -> roc_std::RocList {
+        debug_assert_eq!(
+            self.discriminant(),
+            discriminant_RocStructFields::HasClosure
+        );
+        let payload = {
+            let mut uninitialized = core::mem::MaybeUninit::uninit();
+            let swapped = unsafe {
+                core::mem::replace(
+                    &mut self.HasClosure,
+                    core::mem::ManuallyDrop::new(uninitialized.assume_init()),
+                )
+            };
+
+            core::mem::forget(self);
+
+            core::mem::ManuallyDrop::into_inner(swapped)
+        };
+
+        payload
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    /// Unsafely assume the given `RocStructFields` has a `.discriminant()` of `HasClosure` and return its payload.
+    /// (Always examine `.discriminant()` first to make sure this is the correct variant!)
+    /// Panics in debug builds if the `.discriminant()` doesn't return `HasClosure`.
+    pub unsafe fn as_HasClosure(&self) -> &roc_std::RocList {
+        debug_assert_eq!(
+            self.discriminant(),
+            discriminant_RocStructFields::HasClosure
+        );
+        let payload = &self.HasClosure;
+
+        &payload
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    /// Construct a tag named `HasNoClosure`, with the appropriate payload
+    pub fn HasNoClosure(arg: roc_std::RocList) -> Self {
+        let mut answer = Self {
+            HasNoClosure: core::mem::ManuallyDrop::new(arg),
+        };
+
+        answer.set_discriminant(discriminant_RocStructFields::HasNoClosure);
+
+        answer
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    /// Unsafely assume the given `RocStructFields` has a `.discriminant()` of `HasNoClosure` and convert it to `HasNoClosure`'s payload.
+    /// (Always examine `.discriminant()` first to make sure this is the correct variant!)
+    /// Panics in debug builds if the `.discriminant()` doesn't return `HasNoClosure`.
+    pub unsafe fn into_HasNoClosure(mut self) -> roc_std::RocList {
+        debug_assert_eq!(
+            self.discriminant(),
+            discriminant_RocStructFields::HasNoClosure
+        );
+        let payload = {
+            let mut uninitialized = core::mem::MaybeUninit::uninit();
+            let swapped = unsafe {
+                core::mem::replace(
+                    &mut self.HasNoClosure,
+                    core::mem::ManuallyDrop::new(uninitialized.assume_init()),
+                )
+            };
+
+            core::mem::forget(self);
+
+            core::mem::ManuallyDrop::into_inner(swapped)
+        };
+
+        payload
+    }
+
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    /// Unsafely assume the given `RocStructFields` has a `.discriminant()` of `HasNoClosure` and return its payload.
+    /// (Always examine `.discriminant()` first to make sure this is the correct variant!)
+    /// Panics in debug builds if the `.discriminant()` doesn't return `HasNoClosure`.
+    pub unsafe fn as_HasNoClosure(&self) -> &roc_std::RocList {
+        debug_assert_eq!(
+            self.discriminant(),
+            discriminant_RocStructFields::HasNoClosure
+        );
+        let payload = &self.HasNoClosure;
+
+        &payload
+    }
+
+    #[cfg(any(target_arch = "aarch64", target_arch = "x86_64"))]
+    /// Returns which variant this tag union holds. Note that this never includes a payload!
+    pub fn discriminant(&self) -> discriminant_RocStructFields {
+        unsafe {
+            let bytes = core::mem::transmute::<&Self, &[u8; core::mem::size_of::()]>(self);
+
+            core::mem::transmute::(*bytes.as_ptr().add(24))
+        }
+    }
+
+    #[cfg(any(target_arch = "aarch64", target_arch = "x86_64"))]
+    /// Internal helper
+    fn set_discriminant(&mut self, discriminant: discriminant_RocStructFields) {
+        let discriminant_ptr: *mut discriminant_RocStructFields =
+            (self as *mut RocStructFields).cast();
+
+        unsafe {
+            *(discriminant_ptr.add(24)) = discriminant;
+        }
+    }
+}
+
+impl Drop for RocStructFields {
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    fn drop(&mut self) {
+        // Drop the payloads
+        match self.discriminant() {
+            discriminant_RocStructFields::HasClosure => unsafe {
+                core::mem::ManuallyDrop::drop(&mut self.HasClosure)
+            },
+            discriminant_RocStructFields::HasNoClosure => unsafe {
+                core::mem::ManuallyDrop::drop(&mut self.HasNoClosure)
+            },
+        }
+    }
+}
+
+impl Eq for RocStructFields {}
+
+impl PartialEq for RocStructFields {
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    fn eq(&self, other: &Self) -> bool {
+        if self.discriminant() != other.discriminant() {
+            return false;
+        }
+
+        unsafe {
+            match self.discriminant() {
+                discriminant_RocStructFields::HasClosure => self.HasClosure == other.HasClosure,
+                discriminant_RocStructFields::HasNoClosure => {
+                    self.HasNoClosure == other.HasNoClosure
+                }
+            }
+        }
+    }
+}
+
+impl PartialOrd for RocStructFields {
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    fn partial_cmp(&self, other: &Self) -> Option {
+        match self.discriminant().partial_cmp(&other.discriminant()) {
+            Some(core::cmp::Ordering::Equal) => {}
+            not_eq => return not_eq,
+        }
+
+        unsafe {
+            match self.discriminant() {
+                discriminant_RocStructFields::HasClosure => {
+                    self.HasClosure.partial_cmp(&other.HasClosure)
+                }
+                discriminant_RocStructFields::HasNoClosure => {
+                    self.HasNoClosure.partial_cmp(&other.HasNoClosure)
+                }
+            }
+        }
+    }
+}
+
+impl Ord for RocStructFields {
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
+        match self.discriminant().cmp(&other.discriminant()) {
+            core::cmp::Ordering::Equal => {}
+            not_eq => return not_eq,
+        }
+
+        unsafe {
+            match self.discriminant() {
+                discriminant_RocStructFields::HasClosure => self.HasClosure.cmp(&other.HasClosure),
+                discriminant_RocStructFields::HasNoClosure => {
+                    self.HasNoClosure.cmp(&other.HasNoClosure)
+                }
+            }
+        }
+    }
+}
+
+impl Clone for RocStructFields {
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    fn clone(&self) -> Self {
+        let mut answer = unsafe {
+            match self.discriminant() {
+                discriminant_RocStructFields::HasClosure => Self {
+                    HasClosure: self.HasClosure.clone(),
+                },
+                discriminant_RocStructFields::HasNoClosure => Self {
+                    HasNoClosure: self.HasNoClosure.clone(),
+                },
+            }
+        };
+
+        answer.set_discriminant(self.discriminant());
+
+        answer
+    }
+}
+
+impl core::hash::Hash for RocStructFields {
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    fn hash(&self, state: &mut H) {
+        match self.discriminant() {
+            discriminant_RocStructFields::HasClosure => unsafe {
+                discriminant_RocStructFields::HasClosure.hash(state);
+                self.HasClosure.hash(state);
+            },
+            discriminant_RocStructFields::HasNoClosure => unsafe {
+                discriminant_RocStructFields::HasNoClosure.hash(state);
+                self.HasNoClosure.hash(state);
+            },
+        }
+    }
+}
+
+impl core::fmt::Debug for RocStructFields {
+    #[cfg(any(
+        target_arch = "arm",
+        target_arch = "aarch64",
+        target_arch = "wasm32",
+        target_arch = "x86",
+        target_arch = "x86_64"
+    ))]
+    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
+        f.write_str("RocStructFields::")?;
+
+        unsafe {
+            match self.discriminant() {
+                discriminant_RocStructFields::HasClosure => f
+                    .debug_tuple("HasClosure")
+                    .field(&*self.HasClosure)
+                    .finish(),
+                discriminant_RocStructFields::HasNoClosure => f
+                    .debug_tuple("HasNoClosure")
+                    .field(&*self.HasNoClosure)
+                    .finish(),
+            }
+        }
+    }
+}
diff --git a/crates/glue/src/rust_glue.rs b/crates/glue/src/rust_glue.rs
new file mode 100644
index 0000000000..2259953ca0
--- /dev/null
+++ b/crates/glue/src/rust_glue.rs
@@ -0,0 +1,2973 @@
+use crate::types::{
+    Accessors, File, RocFn, RocNum, RocSingleTagPayload, RocStructFields, RocTagUnion, RocType,
+    TypeId, Types,
+};
+use indexmap::IndexMap;
+use roc_target::{Architecture, TargetInfo};
+use std::fmt::{Display, Write};
+
+const INDENT: &str = "    ";
+const DISCRIMINANT_DOC_COMMENT: &str =
+    "/// Returns which variant this tag union holds. Note that this never includes a payload!";
+
+type Impls = IndexMap>>;
+type Impl = Option;
+
+/// Recursive tag unions need a custom Clone which bumps refcount.
+const RECURSIVE_TAG_UNION_CLONE: &str = r#"fn clone(&self) -> Self {
+        if let Some(storage) = self.storage() {
+            let mut new_storage = storage.get();
+            if !new_storage.is_readonly() {
+                new_storage.increment_reference_count();
+                storage.set(new_storage);
+            }
+        }
+
+        Self {
+            pointer: self.pointer
+        }
+    }"#;
+
+const RECURSIVE_TAG_UNION_STORAGE: &str = r#"#[inline(always)]
+    fn storage(&self) -> Option<&core::cell::Cell> {
+        let mask = match std::mem::size_of::() {
+            4 => 0b11,
+            8 => 0b111,
+            _ => unreachable!(),
+        };
+
+        // NOTE: pointer provenance is probably lost here
+        let unmasked_address = (self.pointer as usize) & !mask;
+        let untagged = unmasked_address as *const core::cell::Cell;
+
+        if untagged.is_null() {
+            None
+        } else {
+            unsafe {
+                Some(&*untagged.sub(1))
+            }
+        }
+    }"#;
+
+/// Add the given declaration body, along with the architecture, to the Impls.
+/// This can optionally be within an `impl`, or if no `impl` is specified,
+/// then it's added at the top level.
+fn add_decl(impls: &mut Impls, opt_impl: Impl, target_info: TargetInfo, body: String) {
+    let decls = impls.entry(opt_impl).or_default();
+    let targets = decls.entry(body).or_default();
+
+    targets.push(target_info);
+}
+
+pub fn emit(types: &[Types]) -> Vec {
+    let mut buf = std::str::from_utf8(HEADER).unwrap().to_string();
+    let mut impls: Impls = IndexMap::default();
+
+    for types in types {
+        for id in types.sorted_ids() {
+            add_type(types.target(), id, types, &mut impls);
+        }
+    }
+
+    for (opt_impl, decls) in impls {
+        let has_impl;
+
+        if let Some(impl_str) = opt_impl {
+            has_impl = true;
+
+            buf.push('\n');
+            buf.push_str(&impl_str);
+            buf.push_str(" {");
+        } else {
+            has_impl = false;
+        }
+
+        for (decl, targets) in decls {
+            // If we're inside an `impl` block, indent the cfg annotation
+            let indent = if has_impl { INDENT } else { "" };
+
+            // Push a newline and potentially an indent before the #[cfg(...)] line
+            buf.push('\n');
+            buf.push_str(indent);
+
+            match targets.len() {
+                1 => {
+                    let arch = arch_to_str(targets.get(0).unwrap().architecture);
+
+                    write!(buf, "#[cfg(target_arch = \"{arch}\")]").unwrap();
+                }
+                _ => {
+                    // We should never have a decl recorded with 0 targets!
+                    debug_assert_ne!(targets.len(), 0);
+
+                    let mut it = targets.iter().peekable();
+
+                    writeln!(buf, "#[cfg(any(").unwrap();
+
+                    while let Some(target_info) = it.next() {
+                        write!(
+                            buf,
+                            "{indent}{INDENT}target_arch = \"{}\"",
+                            arch_to_str(target_info.architecture)
+                        )
+                        .unwrap();
+
+                        if it.peek().is_some() {
+                            buf.push_str(",\n");
+                        } else {
+                            buf.push('\n');
+                        }
+                    }
+
+                    write!(buf, "{indent}))]").unwrap();
+                }
+            }
+
+            buf.push('\n'); // newline after the #[cfg(...)] line
+
+            // indent and print the decl (e.g. a `fn`), with a newline at the end
+            buf.push_str(indent);
+            buf.push_str(&decl);
+            buf.push('\n');
+        }
+
+        // If this was an impl, it needs a closing brace at the end.
+        if has_impl {
+            buf.push_str("}\n");
+        }
+    }
+
+    vec![crate::types::File {
+        name: "mod.rs".to_string(),
+        content: buf,
+    }]
+}
+
+fn add_type(target_info: TargetInfo, id: TypeId, types: &Types, impls: &mut Impls) {
+    match types.get_type(id) {
+        RocType::Struct { name, fields } => {
+            add_struct(name, target_info, fields, id, types, impls, false)
+        }
+        RocType::TagUnionPayload { name, fields } => {
+            add_struct(name, target_info, fields, id, types, impls, true)
+        }
+        RocType::TagUnion(tag_union) => {
+            match tag_union {
+                RocTagUnion::Enumeration { tags, name, size } => add_enumeration(
+                    name,
+                    target_info,
+                    types.get_type(id),
+                    tags.iter(),
+                    *size,
+                    types,
+                    impls,
+                ),
+                RocTagUnion::NonRecursive {
+                    tags,
+                    name,
+                    discriminant_size,
+                    discriminant_offset,
+                } => {
+                    // Empty tag unions can never come up at runtime,
+                    // and so don't need declared types.
+                    if !tags.is_empty() {
+                        add_tag_union(
+                            Recursiveness::NonRecursive,
+                            name,
+                            target_info,
+                            id,
+                            tags,
+                            None,
+                            *discriminant_size,
+                            *discriminant_offset,
+                            types,
+                            impls,
+                        );
+                    }
+                }
+                RocTagUnion::Recursive {
+                    tags,
+                    name,
+                    discriminant_size,
+                    discriminant_offset,
+                } => {
+                    // Empty tag unions can never come up at runtime,
+                    // and so don't need declared types.
+                    if !tags.is_empty() {
+                        add_tag_union(
+                            Recursiveness::Recursive,
+                            name,
+                            target_info,
+                            id,
+                            tags,
+                            None,
+                            *discriminant_size,
+                            *discriminant_offset,
+                            types,
+                            impls,
+                        );
+                    }
+                }
+                RocTagUnion::NullableWrapped {
+                    name,
+                    index_of_null_tag,
+                    tags,
+                    discriminant_size,
+                    discriminant_offset,
+                } => {
+                    // index_of_null_tag refers to the index of the tag that is represented at runtime as NULL.
+                    // For example, in `FingerTree a : [Empty, Single a, More (Some a) (FingerTree (Tuple a)) (Some a)]`,
+                    // the ids would be Empty = 0, More = 1, Single = 2, because that's how those tags are
+                    // ordered alphabetically. Since the Empty tag will be represented at runtime as NULL,
+                    // and since Empty's tag id is 0, here nullable_id would be 0.
+                    add_tag_union(
+                        Recursiveness::Recursive,
+                        name,
+                        target_info,
+                        id,
+                        tags,
+                        Some(*index_of_null_tag as usize),
+                        *discriminant_size,
+                        *discriminant_offset,
+                        types,
+                        impls,
+                    )
+                }
+                RocTagUnion::NullableUnwrapped {
+                    name,
+                    null_tag,
+                    non_null_tag,
+                    non_null_payload,
+                    null_represents_first_tag,
+                } => add_nullable_unwrapped(
+                    name,
+                    target_info,
+                    id,
+                    null_tag,
+                    non_null_tag,
+                    *non_null_payload,
+                    *null_represents_first_tag,
+                    types,
+                    impls,
+                ),
+                RocTagUnion::SingleTagStruct {
+                    name,
+                    tag_name,
+                    payload,
+                } => {
+                    add_single_tag_struct(name, tag_name, payload, types, impls, target_info);
+                }
+                RocTagUnion::NonNullableUnwrapped {
+                    name,
+                    tag_name,
+                    payload,
+                } => {
+                    add_tag_union(
+                        Recursiveness::Recursive,
+                        name,
+                        target_info,
+                        id,
+                        &[(tag_name.clone(), Some(*payload))],
+                        None,
+                        0,
+                        0,
+                        types,
+                        impls,
+                    );
+                }
+            }
+        }
+        // These types don't need to be declared in Rust.
+        RocType::Unit
+        | RocType::EmptyTagUnion
+        | RocType::Num(_)
+        | RocType::Bool
+        | RocType::RocResult(_, _)
+        | RocType::RocStr
+        | RocType::RocDict(_, _)
+        | RocType::RocSet(_)
+        | RocType::RocList(_)
+        | RocType::RocBox(_)
+        | RocType::Unsized => {}
+        RocType::RecursivePointer { .. } => {
+            // This is recursively pointing to a type that should already have been added,
+            // so no extra work needs to happen.
+        }
+        RocType::Function(roc_fn) => add_function(target_info, roc_fn, types, impls),
+    }
+}
+
+fn add_single_tag_struct(
+    name: &str,
+    tag_name: &str,
+    payload: &RocSingleTagPayload,
+    types: &Types,
+    impls: &mut IndexMap, IndexMap>>,
+    target_info: TargetInfo,
+) {
+    let name = escape_kw(name.to_string());
+
+    // Store single-tag unions as structs rather than enums,
+    // because they have only one alternative. However, still
+    // offer the usual tag union APIs.
+    {
+        let mut buf = String::new();
+
+        // Make a dummy RocType::Struct so that we can pass it to deriving
+        // and have that work out as normal.
+        let struct_type = match payload {
+            RocSingleTagPayload::HasClosure { payload_getters } => {
+                {
+                    if payload_getters.is_empty() {
+                        // A single tag with no payload is a zero-sized unit type, so
+                        // represent it as a zero-sized struct (e.g. "struct Foo()").
+                        buf.push_str("();\n");
+                    } else {
+                        buf.push_str("{\n");
+
+                        for (_index, _getter_fn) in payload_getters.iter().enumerate() {
+                            // TODO these should be added as separate functions in the impl!
+                            todo!("TODO generate payload getters");
+                        }
+
+                        buf.push_str("}\n");
+                    }
+
+                    let fields = payload_getters
+                        .iter()
+                        .map(|(type_id, getter)| {
+                            (
+                                String::new(),
+                                *type_id,
+                                Accessors {
+                                    getter: getter.clone(),
+                                },
+                            )
+                        })
+                        .collect();
+
+                    RocType::Struct {
+                        // Deriving doesn't depend on the struct's name,
+                        // so no need to clone name here.
+                        name: String::new(),
+                        fields: RocStructFields::HasClosure { fields },
+                    }
+                }
+            }
+            RocSingleTagPayload::HasNoClosure {
+                payload_fields: payloads,
+            } => {
+                if payloads.is_empty() {
+                    // A single tag with no payload is a zero-sized unit type, so
+                    // represent it as a zero-sized struct (e.g. "struct Foo()").
+                    buf.push_str("();\n");
+                } else {
+                    buf.push_str("{\n");
+
+                    for (index, field_id) in payloads.iter().enumerate() {
+                        let field_type = type_name(*field_id, types);
+
+                        // These are all private fields, since this is a tag union.
+                        // ignore returned result, writeln can not fail as it is used here
+                        let _ = writeln!(buf, "{INDENT}f{index}: {field_type},");
+                    }
+
+                    buf.push_str("}\n");
+                }
+
+                let fields = payloads
+                    .iter()
+                    .map(|type_id| (String::new(), *type_id))
+                    .collect();
+
+                RocType::Struct {
+                    // Deriving doesn't depend on the struct's name,
+                    // so no need to clone name here.
+                    name: String::new(),
+                    fields: RocStructFields::HasNoClosure { fields },
+                }
+            }
+        };
+        let derive = derive_str(&struct_type, types, false);
+
+        let body = format!("#[repr(transparent)]\n{derive}\npub struct {name} {buf}");
+
+        add_decl(impls, None, target_info, body);
+    }
+
+    // the impl for the single-tag union itself
+    match payload {
+        RocSingleTagPayload::HasNoClosure { payload_fields } => {
+            let opt_impl = Some(format!("impl {name}"));
+
+            if payload_fields.is_empty() {
+                add_decl(
+                    impls,
+                    opt_impl.clone(),
+                    target_info,
+                    format!(
+                        r#"/// A tag named {tag_name}, which has no payload.
+        pub const {tag_name}: Self = Self();"#,
+                    ),
+                );
+
+                add_decl(
+                    impls,
+                    opt_impl.clone(),
+                    target_info,
+                    format!(
+                        r#"/// Other `into_` methods return a payload, but since the {tag_name} tag
+        /// has no payload, this does nothing and is only here for completeness.
+        pub fn into_{tag_name}(self) {{
+            ()
+        }}"#,
+                    ),
+                );
+
+                add_decl(
+                    impls,
+                    opt_impl,
+                    target_info,
+                    format!(
+                        r#"/// Other `as` methods return a payload, but since the {tag_name} tag
+        /// has no payload, this does nothing and is only here for completeness.
+        pub fn as_{tag_name}(&self) {{
+            ()
+        }}"#,
+                    ),
+                );
+            } else {
+                let mut args: Vec = Vec::with_capacity(payload_fields.len());
+                let mut fields: Vec = Vec::with_capacity(payload_fields.len());
+                let mut field_types: Vec = Vec::with_capacity(payload_fields.len());
+                let mut field_access: Vec = Vec::with_capacity(payload_fields.len());
+
+                for (index, field_id) in payload_fields.iter().enumerate() {
+                    let field_type = type_name(*field_id, types);
+
+                    field_access.push(format!("self.f{index}"));
+                    args.push(format!("f{index}: {field_type}"));
+                    fields.push(format!("{INDENT}{INDENT}{INDENT}f{index},"));
+                    field_types.push(field_type);
+                }
+
+                let args = args.join(", ");
+                let fields = fields.join("\n");
+
+                add_decl(
+                    impls,
+                    opt_impl.clone(),
+                    target_info,
+                    format!(
+                        r#"/// A tag named {tag_name}, with the given payload.
+    pub fn {tag_name}({args}) -> Self {{
+        Self {{
+{fields}
+        }}
+    }}"#,
+                    ),
+                );
+
+                {
+                    // Return a tuple
+                    let ret_type = {
+                        let joined = field_types.join(", ");
+
+                        if field_types.len() == 1 {
+                            joined
+                        } else {
+                            format!("({joined})")
+                        }
+                    };
+                    let ret_expr = {
+                        let joined = field_access.join(", ");
+
+                        if field_access.len() == 1 {
+                            joined
+                        } else {
+                            format!("({joined})")
+                        }
+                    };
+
+                    add_decl(
+                        impls,
+                        opt_impl.clone(),
+                        target_info,
+                        format!(
+                            r#"/// Since `{tag_name}` only has one tag (namely, `{tag_name}`),
+    /// convert it to `{tag_name}`'s payload.
+    pub fn into_{tag_name}(self) -> {ret_type} {{
+        {ret_expr}
+    }}"#,
+                        ),
+                    );
+                }
+
+                {
+                    // Return a tuple
+                    let ret_type = {
+                        let joined = field_types
+                            .iter()
+                            .map(|field_type| format!("&{field_type}"))
+                            .collect::>()
+                            .join(", ");
+
+                        if field_types.len() == 1 {
+                            joined
+                        } else {
+                            format!("({joined})")
+                        }
+                    };
+                    let ret_expr = {
+                        let joined = field_access
+                            .iter()
+                            .map(|field| format!("&{field}"))
+                            .collect::>()
+                            .join(", ");
+
+                        if field_access.len() == 1 {
+                            joined
+                        } else {
+                            format!("({joined})")
+                        }
+                    };
+
+                    add_decl(
+                        impls,
+                        opt_impl,
+                        target_info,
+                        format!(
+                            r#"/// Since `{tag_name}` only has one tag (namely, `{tag_name}`),
+    /// convert it to `{tag_name}`'s payload.
+    pub fn as_{tag_name}(&self) -> {ret_type} {{
+        {ret_expr}
+    }}"#,
+                        ),
+                    );
+                }
+            }
+        }
+        RocSingleTagPayload::HasClosure { payload_getters: _ } => todo!(),
+    }
+
+    // The Debug impl for the single-tag union
+    match payload {
+        RocSingleTagPayload::HasNoClosure { payload_fields } => {
+            let opt_impl = Some(format!("impl core::fmt::Debug for {name}"));
+
+            let mut buf = "fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {"
+                .to_string();
+
+            if payload_fields.is_empty() {
+                // ignore returned result, write can not fail as it is used here
+                let _ = write!(buf, "f.write_str(\"{name}::{tag_name}\")");
+            } else {
+                let _ = write!(
+                    buf,
+                    "\n{INDENT}{INDENT}{INDENT}f.debug_tuple(\"{name}::{tag_name}\")"
+                );
+
+                for (index, _) in payload_fields.iter().enumerate() {
+                    let _ = write!(
+                        buf,
+                        "{INDENT}{INDENT}{INDENT}{INDENT}.field(&self.f{index})"
+                    );
+                }
+
+                let _ = write!(buf, "{INDENT}{INDENT}{INDENT}{INDENT}.finish()");
+            }
+
+            buf.push_str("    }\n");
+
+            add_decl(impls, opt_impl, target_info, buf);
+        }
+        RocSingleTagPayload::HasClosure { payload_getters: _ } => todo!(),
+    }
+}
+
+fn add_discriminant(
+    name: &str,
+    target_info: TargetInfo,
+    tag_names: Vec,
+    size: u32,
+    types: &Types,
+    impls: &mut Impls,
+) -> String {
+    // The tag union's discriminant, e.g.
+    //
+    // #[repr(u8)]
+    // pub enum tag_MyTagUnion {
+    //     Bar,
+    //     Foo,
+    // }
+    let discriminant_name = format!("discriminant_{name}");
+    let discriminant_type = RocType::TagUnion(RocTagUnion::Enumeration {
+        name: discriminant_name.clone(),
+        tags: tag_names.clone(),
+        size,
+    });
+
+    add_enumeration(
+        &discriminant_name,
+        target_info,
+        &discriminant_type,
+        tag_names.into_iter(),
+        size,
+        types,
+        impls,
+    );
+
+    discriminant_name
+}
+
+#[derive(Copy, Clone, PartialEq, Debug)]
+enum Recursiveness {
+    Recursive,
+    NonRecursive,
+}
+
+#[allow(clippy::too_many_arguments)]
+fn add_tag_union(
+    recursiveness: Recursiveness,
+    name: &str,
+    target_info: TargetInfo,
+    type_id: TypeId,
+    tags: &[(String, Option)],
+    null_tag_index: Option, // used only in the nullable-wrapped case
+    discriminant_size: u32,
+    discriminant_offset: u32,
+    types: &Types,
+    impls: &mut Impls,
+) {
+    let name = escape_kw(name.to_string());
+
+    // We should never be attempting to generate glue for empty tag unions;
+    // RocType should not have let this happen.
+    debug_assert_ne!(tags.len(), 0);
+
+    let tag_names = tags.iter().map(|(name, _)| name).cloned().collect();
+    let discriminant_name = if discriminant_size > 0 {
+        add_discriminant(
+            name.as_str(),
+            target_info,
+            tag_names,
+            discriminant_size,
+            types,
+            impls,
+        )
+    } else {
+        // If there's no discriminant, use an empty string for the name.
+        String::new()
+    };
+    let typ = types.get_type(type_id);
+    let size_rounded_to_alignment = types.size_rounded_to_alignment(type_id);
+    let (actual_self, actual_self_mut, actual_other, union_name) = match recursiveness {
+        Recursiveness::Recursive => (
+            "(&*self.union_pointer())",
+            "(&mut *self.union_pointer())",
+            "(&*other.union_pointer())",
+            format!("union_{name}"),
+        ),
+        Recursiveness::NonRecursive => ("self", "self", "other", name.to_string()),
+    };
+
+    {
+        // Recursive tag unions have a public struct, not a public union
+        let pub_str;
+        let decl_union_name: &str;
+
+        match recursiveness {
+            Recursiveness::Recursive => {
+                add_decl(
+                    impls,
+                    None,
+                    target_info,
+                    format!(
+                        r#"#[repr(transparent)]
+pub struct {name} {{
+    pointer: *mut {union_name},
+}}
+"#
+                    ),
+                );
+
+                pub_str = "";
+                decl_union_name = &union_name;
+            }
+            Recursiveness::NonRecursive => {
+                pub_str = "pub ";
+                decl_union_name = &name;
+            }
+        };
+
+        // No #[derive(...)] for unions; we have to generate each impl ourselves!
+        let mut buf = format!("#[repr(C)]\n{pub_str}union {decl_union_name} {{\n");
+
+        for (tag_name, opt_payload_id) in tags {
+            // If there's no payload, we don't need a discriminant for it.
+            if let Some(payload_id) = opt_payload_id {
+                let payload_type = types.get_type(*payload_id);
+
+                write!(buf, "{INDENT}{tag_name}: ").unwrap();
+
+                if cannot_derive_copy(payload_type, types) {
+                    // types with pointers need ManuallyDrop
+                    // because rust unions don't (and can't)
+                    // know how to drop them automatically!
+                    writeln!(
+                        buf,
+                        "core::mem::ManuallyDrop<{}>,",
+                        type_name(*payload_id, types)
+                    )
+                    .unwrap();
+                } else {
+                    buf.push_str(&type_name(*payload_id, types));
+                    buf.push_str(",\n");
+                }
+            }
+        }
+
+        if tags.len() > 1 {
+            // When there's a discriminant (so, multiple tags) and there is
+            // no alignment padding after the largest variant,
+            // the compiler will make extra room for the discriminant.
+            // We need that to be reflected in the overall size of the enum,
+            // so add an extra variant with the appropriate size.
+            //
+            // (Do this even if theoretically shouldn't be necessary, since
+            // there's no runtime cost and it more explicitly syncs the
+            // union's size with what we think it should be.)
+            writeln!(buf, "{INDENT}_sizer: [u8; {size_rounded_to_alignment}],").unwrap();
+        }
+
+        buf.push('}');
+
+        add_decl(impls, None, target_info, buf);
+    }
+
+    // The impl for the tag union
+    {
+        let opt_impl = Some(format!("impl {name}"));
+        let bitmask;
+
+        match recursiveness {
+            Recursiveness::Recursive => {
+                let storage_fn = if discriminant_size == 0 {
+                    r#"fn storage(&self) -> Option<&core::cell::Cell> {
+        let untagged = self.pointer as *const core::cell::Cell;
+
+        unsafe {
+            Some(&*untagged.sub(1))
+        }
+    }"#
+                    .to_string()
+                } else {
+                    RECURSIVE_TAG_UNION_STORAGE.to_string()
+                };
+
+                add_decl(impls, opt_impl.clone(), target_info, storage_fn);
+
+                if tags.len() <= max_pointer_tagged_variants(target_info.architecture) {
+                    bitmask = format!("{:#b}", tagged_pointer_bitmask(target_info.architecture));
+
+                    if discriminant_size == 0 {
+                        add_decl(
+                            impls,
+                            opt_impl.clone(),
+                            target_info,
+                            r#"/// This is a single-tag union, so it has no alternatives
+    /// to discriminate between. This method is only included for completeness.
+    pub fn discriminant(&self) -> () {
+        ()
+    }"#
+                            .to_string(),
+                        );
+                    } else {
+                        add_decl(
+                            impls,
+                            opt_impl.clone(),
+                            target_info,
+                            format!(
+                                r#"{DISCRIMINANT_DOC_COMMENT}
+    pub fn discriminant(&self) -> {discriminant_name} {{
+        // The discriminant is stored in the unused bytes at the end of the recursive pointer
+        unsafe {{ core::mem::transmute::((self.pointer as u8) & {bitmask}) }}
+    }}"#
+                            ),
+                        );
+
+                        add_decl(
+                            impls,
+                            opt_impl.clone(),
+                            target_info,
+                            format!(
+                                r#"/// Internal helper
+    fn tag_discriminant(pointer: *mut {union_name}, discriminant: {discriminant_name}) -> *mut {union_name} {{
+        // The discriminant is stored in the unused bytes at the end of the union pointer
+        let untagged = (pointer as usize) & (!{bitmask} as usize);
+        let tagged = untagged | (discriminant as usize);
+
+        tagged as *mut {union_name}
+    }}"#
+                            ),
+                        );
+
+                        add_decl(
+                            impls,
+                            opt_impl.clone(),
+                            target_info,
+                            format!(
+                                r#"/// Internal helper
+    fn union_pointer(&self) -> *mut {union_name} {{
+        // The discriminant is stored in the unused bytes at the end of the union pointer
+        ((self.pointer as usize) & (!{bitmask} as usize)) as *mut {union_name}
+    }}"#
+                            ),
+                        );
+                    }
+                } else {
+                    todo!(
+                        "Support {} tags in a recursive tag union on target_info {:?}. (This is too many tags for pointer tagging to work, so we need to generate different glue.)",
+                        tags.len(),
+                        target_info
+                    );
+                }
+            }
+            Recursiveness::NonRecursive => {
+                // The bitmask doesn't come up in a nonrecursive tag union.
+                bitmask = String::new();
+
+                // An old design, which ended up not working out, was that the tag union
+                // was a struct containing two fields: one for the `union`, and another
+                // for the discriminant.
+                //
+                // The problem with this was alignment; e.g. if you have one variant with a
+                // RocStr in it and another with an I128, then the `union` has a size of 32B
+                // and the discriminant is right after it - making the size of the whole struct
+                // round up to 48B total, since it has an alignment of 16 from the I128.
+                //
+                // However, Roc will generate the more efficient thing here: the whole thing will
+                // be 32B, and the discriminant will appear at offset 24 - right after the end of
+                // the RocStr. The current design recognizes this and works with it, by representing
+                // the entire structure as a union and manually setting the tag at the appropriate offset.
+                add_decl(
+                    impls,
+                    opt_impl.clone(),
+                    target_info,
+                    format!(
+                        r#"{DISCRIMINANT_DOC_COMMENT}
+    pub fn discriminant(&self) -> {discriminant_name} {{
+        unsafe {{
+            let bytes = core::mem::transmute::<&Self, &[u8; core::mem::size_of::()]>(self);
+
+            core::mem::transmute::(*bytes.as_ptr().add({discriminant_offset}))
+        }}
+    }}"#
+                    ),
+                );
+
+                add_decl(
+                    impls,
+                    opt_impl.clone(),
+                    target_info,
+                    format!(
+                        r#"/// Internal helper
+    fn set_discriminant(&mut self, discriminant: {discriminant_name}) {{
+        let discriminant_ptr: *mut {discriminant_name} = (self as *mut {name}).cast();
+
+        unsafe {{
+            *(discriminant_ptr.add({discriminant_offset})) = discriminant;
+        }}
+    }}"#
+                    ),
+                );
+            }
+        }
+
+        for (tag_index, (tag_name, opt_payload_id)) in tags.iter().enumerate() {
+            // Add a convenience constructor function to the impl, e.g.
+            //
+            // /// Construct a tag named Foo, with the appropriate payload
+            // pub fn Foo(payload: roc_std::RocStr) -> Self {
+            //     Self {
+            //         tag: tag_MyTagUnion::Foo,
+            //         discriminant: discriminant_MyTagUnion {
+            //             Foo: core::mem::ManuallyDrop::new(payload),
+            //         },
+            //     }
+            // }
+            if let Some(payload_id) = opt_payload_id {
+                let payload_type = types.get_type(*payload_id);
+                let self_for_into;
+                let payload_args;
+                let args_to_payload;
+                let owned_ret_type;
+                let borrowed_ret_type;
+                let owned_get_payload;
+                let borrowed_get_payload;
+                let owned_ret;
+                let borrowed_ret;
+
+                match recursiveness {
+                    Recursiveness::Recursive => {
+                        if cannot_derive_copy(payload_type, types) {
+                            owned_get_payload = format!(
+                                r#"{{
+            let ptr = (self.pointer as usize & !{bitmask}) as *mut {union_name};
+            let mut uninitialized = core::mem::MaybeUninit::uninit();
+            let swapped = unsafe {{
+                core::mem::replace(
+                    &mut (*ptr).{tag_name},
+                    core::mem::ManuallyDrop::new(uninitialized.assume_init()),
+                )
+            }};
+
+            core::mem::forget(self);
+
+            core::mem::ManuallyDrop::into_inner(swapped)
+        }}"#
+                            );
+                            borrowed_get_payload = format!(
+                                r#"{{
+            let ptr = (self.pointer as usize & !{bitmask}) as *mut {union_name};
+
+            unsafe {{ &(*ptr).{tag_name} }}
+        }}"#
+                            );
+                            // we need `mut self` for the argument because of ManuallyDrop
+                            self_for_into = "mut self";
+                        } else {
+                            owned_get_payload = format!(
+                                r#"{{
+            let ptr = (self.pointer as usize & !{bitmask}) as *mut {union_name};
+
+            core::ptr::read(ptr).{tag_name}
+        }}"#
+                            );
+                            borrowed_get_payload = format!(
+                                r#"{{
+            let ptr = (self.pointer as usize & !{bitmask}) as *mut {union_name};
+
+            (&ptr).{tag_name}
+        }}"#
+                            );
+                            // we don't need `mut self` unless we need ManuallyDrop
+                            self_for_into = "self";
+                        };
+                    }
+                    Recursiveness::NonRecursive => {
+                        if cannot_derive_copy(payload_type, types) {
+                            owned_get_payload = format!(
+                                r#"{{
+            let mut uninitialized = core::mem::MaybeUninit::uninit();
+            let swapped = unsafe {{
+                core::mem::replace(
+                    &mut self.{tag_name},
+                    core::mem::ManuallyDrop::new(uninitialized.assume_init()),
+                )
+            }};
+
+            core::mem::forget(self);
+
+            core::mem::ManuallyDrop::into_inner(swapped)
+        }}"#
+                            );
+                            borrowed_get_payload = format!("&self.{tag_name}");
+                            // we need `mut self` for the argument because of ManuallyDrop
+                            self_for_into = "mut self";
+                        } else {
+                            owned_get_payload = format!("self.{tag_name}");
+                            borrowed_get_payload = format!("&self.{tag_name}");
+                            // we don't need `mut self` unless we need ManuallyDrop
+                            self_for_into = "self";
+                        };
+                    }
+                }
+
+                match payload_type {
+                    RocType::Unit
+                    | RocType::EmptyTagUnion
+                    | RocType::RocStr
+                    | RocType::Bool
+                    | RocType::Num(_)
+                    | RocType::RocList(_)
+                    | RocType::RocDict(_, _)
+                    | RocType::RocSet(_)
+                    | RocType::RocBox(_)
+                    | RocType::TagUnion(_)
+                    | RocType::RocResult(_, _)
+                    | RocType::RecursivePointer { .. } => {
+                        owned_ret_type = type_name(*payload_id, types);
+                        borrowed_ret_type = format!("&{owned_ret_type}");
+                        owned_ret = "payload".to_string();
+                        borrowed_ret = format!("&{owned_ret}");
+                        payload_args = format!("arg: {owned_ret_type}");
+                        args_to_payload = if cannot_derive_copy(payload_type, types) {
+                            "core::mem::ManuallyDrop::new(arg)".to_string()
+                        } else {
+                            "arg".to_string()
+                        };
+                    }
+                    RocType::Struct {
+                        fields: RocStructFields::HasNoClosure { fields },
+                        name,
+                    } => {
+                        let answer = tag_union_struct_help(name, fields, *payload_id, types, false);
+
+                        owned_ret = answer.owned_ret;
+                        borrowed_ret = answer.borrowed_ret;
+                        owned_ret_type = answer.owned_ret_type;
+                        borrowed_ret_type = answer.borrowed_ret_type;
+                        payload_args = answer.payload_args;
+                        args_to_payload = answer.args_to_payload;
+                    }
+                    RocType::TagUnionPayload {
+                        fields: RocStructFields::HasNoClosure { fields },
+                        name,
+                    } => {
+                        let answer = tag_union_struct_help(name, fields, *payload_id, types, true);
+
+                        owned_ret = answer.owned_ret;
+                        borrowed_ret = answer.borrowed_ret;
+                        owned_ret_type = answer.owned_ret_type;
+                        borrowed_ret_type = answer.borrowed_ret_type;
+                        payload_args = answer.payload_args;
+                        args_to_payload = answer.args_to_payload;
+                    }
+                    RocType::Struct {
+                        fields: RocStructFields::HasClosure { fields: _ },
+                        name: _,
+                    } => {
+                        todo!("struct");
+                    }
+                    RocType::TagUnionPayload {
+                        fields: RocStructFields::HasClosure { fields },
+                        name: _, // TODO call this payload_struct_name and use it to define the struct...or don't define it at all, maybe, since there are only getters and setters?
+                    } => {
+                        // TODO don't generate op.into_StdoutWrite() - only getters/setters instead!
+                        for (field_name, field, accessor) in fields {
+                            let getter_name = &accessor.getter;
+                            let ret = type_name(*field, types);
+                            let returns_via_pointer = true;
+
+                            let body = if let RocType::Function(_) = types.get_type(*field) {
+                                format!(
+                                    r#"
+                                    extern "C" {{
+                                        #[link_name = "{getter_name}_size"]
+                                        fn size() -> usize;
+
+                                        #[link_name = "{getter_name}_generic"]
+                                        fn getter(_: *mut u8, _: *const {name});
+                                    }}
+
+                                    // allocate memory to store this variably-sized value
+                                    // allocates with roc_alloc, but that likely still uses the heap
+                                    let it = std::iter::repeat(0xAAu8).take(size());
+                                    let mut bytes = roc_std::RocList::from_iter(it);
+
+                                    getter(bytes.as_mut_ptr(), self);
+
+                                    {ret} {{
+                                        closure_data: bytes,
+                                    }}
+                                    "#
+                                )
+                            } else if returns_via_pointer {
+                                format!(
+                                    r#"
+                                    extern "C" {{
+                                        #[link_name = "{getter_name}_generic"]
+                                        fn getter(_: *mut {ret}, _: *const {name});
+                                    }}
+
+                                    let mut ret = core::mem::MaybeUninit::uninit();
+                                    getter(ret.as_mut_ptr(), self);
+                                    ret.assume_init()
+                                    "#
+                                )
+                            } else {
+                                format!(
+                                    r#"
+                                    extern "C" {{
+                                        #[link_name = "{getter_name}"]
+                                        fn getter(_: *const {name}) -> {ret};
+                                    }}
+
+                                    getter(self)
+                                    "#
+                                )
+                            };
+
+                            add_decl(
+                                impls,
+                                opt_impl.clone(),
+                                target_info,
+                                format!(
+                                    r#"/// Unsafely assume this `{name}` has a `.discriminant()` of `{tag_name}` and return its payload at index {field_name}.
+    /// (Always examine `.discriminant()` first to make sure this is the correct variant!)
+    /// Panics in debug builds if the `.discriminant()` doesn't return `{tag_name}`.
+    pub unsafe fn get_{tag_name}_{field_name}(&self) -> {ret} {{
+        debug_assert_eq!(self.discriminant(), {discriminant_name}::{tag_name});
+
+        {body}
+    }}"#,
+                                ),
+                            );
+                        }
+
+                        // TODO revise these - they're all copy/pasted from somewhere else
+                        owned_ret_type = type_name(*payload_id, types);
+                        borrowed_ret_type = format!("&{owned_ret_type}");
+                        owned_ret = "payload".to_string();
+                        borrowed_ret = format!("&{owned_ret}");
+                        payload_args = format!("arg: {owned_ret_type}");
+                        args_to_payload = if cannot_derive_copy(payload_type, types) {
+                            "core::mem::ManuallyDrop::new(arg)".to_string()
+                        } else {
+                            "arg".to_string()
+                        };
+                    }
+                    RocType::Unsized => todo!(),
+                    RocType::Function(RocFn { .. }) => todo!(),
+                };
+
+                {
+                    let body = match recursiveness {
+                        Recursiveness::Recursive => {
+                            let pointer_val = if discriminant_size == 0 {
+                                "ptr".to_string()
+                            } else {
+                                format!(
+                                    "Self::tag_discriminant(ptr, {discriminant_name}::{tag_name})"
+                                )
+                            };
+
+                            format!(
+                                r#"
+            let size = core::mem::size_of::<{union_name}>();
+            let align = core::mem::align_of::<{union_name}>() as u32;
+
+            unsafe {{
+                let ptr = roc_std::roc_alloc_refcounted::<{union_name}>();
+
+                *ptr = {union_name} {{
+                    {tag_name}: {args_to_payload}
+                }};
+
+                Self {{
+                    pointer: {pointer_val},
+                }}
+            }}"#
+                            )
+                        }
+                        Recursiveness::NonRecursive => {
+                            format!(
+                                r#"
+            let mut answer = Self {{
+                {tag_name}: {args_to_payload}
+            }};
+
+            answer.set_discriminant({discriminant_name}::{tag_name});
+
+            answer"#
+                            )
+                        }
+                    };
+
+                    add_decl(
+                        impls,
+                        opt_impl.clone(),
+                        target_info,
+                        format!(
+                            r#"/// Construct a tag named `{tag_name}`, with the appropriate payload
+    pub fn {tag_name}({payload_args}) -> Self {{{body}
+    }}"#
+                        ),
+                    );
+                }
+
+                {
+                    let fn_heading = if discriminant_size == 0 {
+                        format!(
+                            r#"/// Since `{name}` only has one tag (namely, `{tag_name}`),
+    /// convert it to `{tag_name}`'s payload.
+    pub fn into_{tag_name}({self_for_into}) -> {owned_ret_type} {{"#
+                        )
+                    } else {
+                        format!(
+                            r#"/// Unsafely assume this `{name}` has a `.discriminant()` of `{tag_name}` and convert it to `{tag_name}`'s payload.
+            /// (Always examine `.discriminant()` first to make sure this is the correct variant!)
+            /// Panics in debug builds if the `.discriminant()` doesn't return `{tag_name}`.
+            pub unsafe fn into_{tag_name}({self_for_into}) -> {owned_ret_type} {{
+                debug_assert_eq!(self.discriminant(), {discriminant_name}::{tag_name});"#
+                        )
+                    };
+
+                    add_decl(
+                        impls,
+                        opt_impl.clone(),
+                        target_info,
+                        format!(
+                            r#"{fn_heading}
+        let payload = {owned_get_payload};
+
+        {owned_ret}
+    }}"#,
+                        ),
+                    );
+                }
+
+                {
+                    let fn_heading = if discriminant_size == 0 {
+                        format!(
+                            r#"/// Since `{name}` only has one tag (namely, `{tag_name}`),
+    /// convert it to `{tag_name}`'s payload.
+    pub fn as_{tag_name}(&self) -> {borrowed_ret_type} {{"#
+                        )
+                    } else {
+                        format!(
+                            r#"/// Unsafely assume this `{name}` has a `.discriminant()` of `{tag_name}` and return its payload.
+            /// (Always examine `.discriminant()` first to make sure this is the correct variant!)
+            /// Panics in debug builds if the `.discriminant()` doesn't return `{tag_name}`.
+            pub unsafe fn as_{tag_name}(&self) -> {borrowed_ret_type} {{
+                debug_assert_eq!(self.discriminant(), {discriminant_name}::{tag_name});"#
+                        )
+                    };
+
+                    add_decl(
+                        impls,
+                        opt_impl.clone(),
+                        target_info,
+                        format!(
+                            r#"{fn_heading}
+        let payload = {borrowed_get_payload};
+
+        {borrowed_ret}
+    }}"#,
+                        ),
+                    );
+                }
+            } else if Some(tag_index) == null_tag_index {
+                // The null tag index only occurs for nullable-wrapped tag unions,
+                // and it always has no payload. This is the one scenario where
+                // that constructor could come up!
+                add_decl(
+                    impls,
+                    opt_impl.clone(),
+                    target_info,
+                    format!(
+                        r#"/// A tag named {tag_name}, which has no payload.
+    pub const {tag_name}: Self = Self {{
+        pointer: core::ptr::null_mut(),
+    }};"#,
+                    ),
+                );
+            } else {
+                add_decl(
+                    impls,
+                    opt_impl.clone(),
+                    target_info,
+                    format!(
+                        r#"/// A tag named {tag_name}, which has no payload.
+    pub const {tag_name}: Self = unsafe {{
+        let mut bytes = [0; core::mem::size_of::<{name}>()];
+
+        bytes[{discriminant_offset}] = {discriminant_name}::{tag_name} as u8;
+
+        core::mem::transmute::<[u8; core::mem::size_of::<{name}>()], {name}>(bytes)
+    }};"#,
+                    ),
+                );
+
+                add_decl(
+                    impls,
+                    opt_impl.clone(),
+                    target_info,
+                    format!(
+                        r#"/// Other `into_` methods return a payload, but since the {tag_name} tag
+    /// has no payload, this does nothing and is only here for completeness.
+    pub fn into_{tag_name}(self) {{
+        ()
+    }}"#,
+                    ),
+                );
+
+                add_decl(
+                    impls,
+                    opt_impl.clone(),
+                    target_info,
+                    format!(
+                        r#"/// Other `as` methods return a payload, but since the {tag_name} tag
+    /// has no payload, this does nothing and is only here for completeness.
+    pub fn as_{tag_name}(&self) {{
+        ()
+    }}"#,
+                    ),
+                );
+            }
+        }
+    }
+
+    // The Drop impl for the tag union
+    if cannot_derive_copy(typ, types) {
+        let opt_impl = Some(format!("impl Drop for {name}"));
+        let mut drop_payload = String::new();
+
+        if discriminant_size == 0 {
+            let (tag_name, _) = tags.first().unwrap();
+
+            // There's only one tag, so there's no discriminant and no need to match;
+            // just drop the pointer.
+            let _ = write!(
+                drop_payload,
+                r#"unsafe {{ core::mem::ManuallyDrop::drop(&mut core::ptr::read(self.pointer).{tag_name}); }}"#
+            );
+        } else {
+            write_impl_tags(
+                3,
+                tags.iter(),
+                &discriminant_name,
+                &mut drop_payload,
+                |tag_name, opt_payload_id| {
+                    match opt_payload_id {
+                        Some(payload_id)
+                            if cannot_derive_copy(types.get_type(payload_id), types) =>
+                        {
+                            format!("unsafe {{ core::mem::ManuallyDrop::drop(&mut {actual_self_mut}.{tag_name}) }},",)
+                        }
+                        _ => {
+                            // If it had no payload, or if the payload had no pointers,
+                            // there's nothing to clean up, so do `=> {}` for the branch.
+                            "{}".to_string()
+                        }
+                    }
+                },
+            );
+        }
+
+        // Drop works differently for recursive vs non-recursive tag unions.
+        let drop_fn = match recursiveness {
+            Recursiveness::Recursive => {
+                format!(
+                    r#"fn drop(&mut self) {{
+        // We only need to do any work if there's actually a heap-allocated payload.
+        if let Some(storage) = self.storage() {{
+            let mut new_storage = storage.get();
+
+            // Decrement the refcount
+            let needs_dealloc = !new_storage.is_readonly() && new_storage.decrease();
+
+            if needs_dealloc {{
+                // Drop the payload first.
+                {drop_payload}
+
+                // Dealloc the pointer
+                let alignment = core::mem::align_of::().max(core::mem::align_of::());
+
+                unsafe {{ crate::roc_dealloc(storage.as_ptr().cast(), alignment as u32); }}
+            }} else {{
+                // Write the storage back.
+                storage.set(new_storage);
+            }}
+        }}
+    }}"#
+                )
+            }
+            Recursiveness::NonRecursive => {
+                format!(
+                    r#"fn drop(&mut self) {{
+        // Drop the payloads
+        {drop_payload}
+    }}"#
+                )
+            }
+        };
+
+        add_decl(impls, opt_impl, target_info, drop_fn);
+    }
+
+    // The PartialEq impl for the tag union
+    {
+        let opt_impl_prefix = if has_float(typ, types) {
+            String::new()
+        } else {
+            format!("impl Eq for {name} {{}}\n\n")
+        };
+        let opt_impl = Some(format!("{opt_impl_prefix}impl PartialEq for {name}"));
+        let mut buf = r#"fn eq(&self, other: &Self) -> bool {
+            if self.discriminant() != other.discriminant() {
+                return false;
+            }
+
+            unsafe {
+"#
+        .to_string();
+
+        if discriminant_size == 0 {
+            let (tag_name, _) = tags.first().unwrap();
+
+            // There's only one tag, so there's no discriminant and no need to match;
+            // just return whether my payload equals the other one.
+            let _ = write!(
+                buf,
+                r#"{INDENT}{INDENT}{INDENT}{INDENT}(*self.pointer).{tag_name} == (*other.pointer).{tag_name}
+    "#
+            );
+        } else {
+            write_impl_tags(
+                3,
+                tags.iter(),
+                &discriminant_name,
+                &mut buf,
+                |tag_name, opt_payload_id| {
+                    if opt_payload_id.is_some() {
+                        format!("{actual_self}.{tag_name} == {actual_other}.{tag_name},")
+                    } else {
+                        // if the tags themselves had been unequal, we already would have
+                        // early-returned with false, so this means the tags were equal
+                        // and there's no payload; return true!
+                        "true,".to_string()
+                    }
+                },
+            );
+        }
+
+        buf.push_str(INDENT);
+        buf.push_str(INDENT);
+        buf.push_str("}\n");
+        buf.push_str(INDENT);
+        buf.push('}');
+
+        add_decl(impls, opt_impl, target_info, buf);
+    }
+
+    // The PartialOrd impl for the tag union
+    {
+        let opt_impl = Some(format!("impl PartialOrd for {name}"));
+
+        let body = if discriminant_size == 0 {
+            let (tag_name, _) = tags.first().unwrap();
+
+            format!(
+                r#"fn partial_cmp(&self, other: &Self) -> Option {{
+        unsafe {{
+            (&*self.pointer).{tag_name}.partial_cmp(&(*other.pointer).{tag_name})
+        }}
+    }}
+"#
+            )
+        } else {
+            let mut buf = r#"fn partial_cmp(&self, other: &Self) -> Option {
+        match self.discriminant().partial_cmp(&other.discriminant()) {
+            Some(core::cmp::Ordering::Equal) => {}
+            not_eq => return not_eq,
+        }
+
+        unsafe {
+"#
+            .to_string();
+
+            write_impl_tags(
+                3,
+                tags.iter(),
+                &discriminant_name,
+                &mut buf,
+                |tag_name, opt_payload_id| {
+                    if opt_payload_id.is_some() {
+                        format!("{actual_self}.{tag_name}.partial_cmp(&{actual_other}.{tag_name}),",)
+                    } else {
+                        // if the tags themselves had been unequal, we already would have
+                        // early-returned, so this means the tags were equal and there's
+                        // no payload; return Equal!
+                        "Some(core::cmp::Ordering::Equal),".to_string()
+                    }
+                },
+            );
+
+            buf.push_str(INDENT);
+            buf.push_str(INDENT);
+            buf.push_str("}\n");
+            buf.push_str(INDENT);
+            buf.push('}');
+
+            buf
+        };
+
+        add_decl(impls, opt_impl, target_info, body);
+    }
+
+    // The Ord impl for the tag union
+    if !has_float(typ, types) {
+        let opt_impl = Some(format!("impl Ord for {name}"));
+
+        let body = if discriminant_size == 0 {
+            let (tag_name, _) = tags.first().unwrap();
+
+            format!(
+                r#"fn cmp(&self, other: &Self) -> core::cmp::Ordering {{
+        unsafe {{
+            (&*self.pointer).{tag_name}.cmp(&(*other.pointer).{tag_name})
+        }}
+    }}
+"#
+            )
+        } else {
+            let mut buf = r#"fn cmp(&self, other: &Self) -> core::cmp::Ordering {
+            match self.discriminant().cmp(&other.discriminant()) {
+                core::cmp::Ordering::Equal => {}
+                not_eq => return not_eq,
+            }
+
+            unsafe {
+"#
+            .to_string();
+
+            write_impl_tags(
+                3,
+                tags.iter(),
+                &discriminant_name,
+                &mut buf,
+                |tag_name, opt_payload_id| {
+                    if opt_payload_id.is_some() {
+                        format!("{actual_self}.{tag_name}.cmp(&{actual_other}.{tag_name}),",)
+                    } else {
+                        // if the tags themselves had been unequal, we already would have
+                        // early-returned, so this means the tags were equal and there's
+                        // no payload; return Equal!
+                        "core::cmp::Ordering::Equal,".to_string()
+                    }
+                },
+            );
+
+            buf.push_str(INDENT);
+            buf.push_str(INDENT);
+            buf.push_str("}\n");
+            buf.push_str(INDENT);
+            buf.push('}');
+
+            buf
+        };
+
+        add_decl(impls, opt_impl, target_info, body);
+    }
+
+    // The Clone impl for the tag union
+    {
+        let opt_impl_prefix = if cannot_derive_copy(typ, types) {
+            String::new()
+        } else {
+            format!("impl Copy for {name} {{}}\n\n")
+        };
+
+        let opt_impl = Some(format!("{opt_impl_prefix}impl Clone for {name}"));
+        let body = match recursiveness {
+            Recursiveness::Recursive => RECURSIVE_TAG_UNION_CLONE.to_string(),
+            Recursiveness::NonRecursive => {
+                let mut buf = r#"fn clone(&self) -> Self {
+        let mut answer = unsafe {
+"#
+                .to_string();
+
+                write_impl_tags(
+                    3,
+                    tags.iter(),
+                    &discriminant_name,
+                    &mut buf,
+                    |tag_name, opt_payload_id| {
+                        if opt_payload_id.is_some() {
+                            match recursiveness {
+                                Recursiveness::Recursive => {
+                                    format!(
+                                        r#"Self {{
+                    {union_name} {{
+                        {tag_name}: self.pointer
+                    }}
+                }},"#,
+                                    )
+                                }
+                                Recursiveness::NonRecursive => {
+                                    format!(
+                                        r#"Self {{
+                    {tag_name}: {actual_self}.{tag_name}.clone(),
+                }},"#,
+                                    )
+                                }
+                            }
+                        } else {
+                            // when there's no payload, initialize to garbage memory.
+                            format!(
+                                r#"core::mem::transmute::<
+                    core::mem::MaybeUninit<{name}>,
+                    {name},
+                >(core::mem::MaybeUninit::uninit()),"#,
+                            )
+                        }
+                    },
+                );
+
+                buf.push_str(
+                    r#"
+        };
+
+        answer.set_discriminant(self.discriminant());
+
+        answer
+    }"#,
+                );
+
+                buf
+            }
+        };
+
+        add_decl(impls, opt_impl, target_info, body);
+    }
+
+    // The Hash impl for the tag union
+    if !has_float(typ, types) {
+        let opt_impl = Some(format!("impl core::hash::Hash for {name}"));
+        let mut buf = r#"fn hash(&self, state: &mut H) {"#.to_string();
+
+        if discriminant_size == 0 {
+            let (tag_name, _) = tags.first().unwrap();
+
+            // There's only one tag, so there's no discriminant and no need to match;
+            // just return whether my payload equals the other one.
+            let _ = write!(
+                buf,
+                r#"
+        unsafe {{
+            (*self.pointer).{tag_name}.hash(state)
+        }}"#
+            );
+        } else {
+            write_impl_tags(
+                2,
+                tags.iter(),
+                &discriminant_name,
+                &mut buf,
+                |tag_name, opt_payload_id| {
+                    let hash_tag = format!("{discriminant_name}::{tag_name}.hash(state)");
+
+                    if opt_payload_id.is_some() {
+                        format!(
+                            r#"unsafe {{
+                    {hash_tag};
+                    {actual_self}.{tag_name}.hash(state);
+                }},"#
+                        )
+                    } else {
+                        format!("{hash_tag},")
+                    }
+                },
+            );
+        };
+
+        buf.push_str(INDENT);
+        buf.push('}');
+
+        add_decl(impls, opt_impl, target_info, buf);
+    }
+
+    // The Debug impl for the tag union
+    {
+        let opt_impl = Some(format!("impl core::fmt::Debug for {name}"));
+        let mut buf = format!(
+            r#"fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {{
+        f.write_str("{name}::")?;
+
+        unsafe {{
+"#
+        );
+
+        if discriminant_size == 0 {
+            let (tag_name, _) = tags.first().unwrap();
+
+            // There's only one tag, so there's no discriminant and no need to match;
+            // just return whether my payload equals the other one.
+            let _ = write!(
+                buf,
+                r#"f.debug_tuple("{tag_name}")
+        .field(&(*self.pointer).{tag_name})
+        .finish()"#,
+            );
+        } else {
+            write_impl_tags(
+                3,
+                tags.iter(),
+                &discriminant_name,
+                &mut buf,
+                |tag_name, opt_payload_id| match opt_payload_id {
+                    Some(payload_id) => {
+                        // If it's a ManuallyDrop, we need a `*` prefix to dereference it
+                        // (because otherwise we're using ManuallyDrop's Debug instance
+                        // rather than the Debug instance of the value it wraps).
+                        let payload_type = types.get_type(payload_id);
+                        let deref_str = if cannot_derive_copy(payload_type, types) {
+                            "&*"
+                        } else {
+                            "&"
+                        };
+
+                        let fields_str = match payload_type {
+                            RocType::Unit
+                            | RocType::EmptyTagUnion
+                            | RocType::RocStr
+                            | RocType::Bool
+                            | RocType::Num(_)
+                            | RocType::RocList(_)
+                            | RocType::RocDict(_, _)
+                            | RocType::RocSet(_)
+                            | RocType::RocBox(_)
+                            | RocType::TagUnion(_)
+                            | RocType::RocResult(_, _)
+                            | RocType::Struct { .. }
+                            | RocType::RecursivePointer { .. } => {
+                                format!(".field({deref_str}{actual_self}.{tag_name})")
+                            }
+                            RocType::TagUnionPayload { fields, .. } => {
+                                let mut buf = Vec::new();
+
+                                match fields {
+                                    RocStructFields::HasNoClosure { fields } => {
+                                        for (label, _) in fields {
+                                            // Needs an "f" prefix
+                                            buf.push(format!(
+                                        ".field(&({deref_str}{actual_self}.{tag_name}).f{label})"
+                                    ));
+                                        }
+                                    }
+                                    RocStructFields::HasClosure { fields: _ } => {
+                                        buf.push("// TODO HAS CLOSURE".to_string());
+                                    }
+                                }
+
+                                buf.join("\n")
+                            }
+                            RocType::Unsized => todo!(),
+                            RocType::Function(RocFn { .. }) => todo!(),
+                        };
+
+                        format!(
+                            r#"f.debug_tuple("{tag_name}")
+        {fields_str}
+        .finish(),"#,
+                        )
+                    }
+                    None => format!(r#"f.write_str("{tag_name}"),"#),
+                },
+            );
+        }
+
+        buf.push_str(INDENT);
+        buf.push_str(INDENT);
+        buf.push_str("}\n");
+        buf.push_str(INDENT);
+        buf.push('}');
+
+        add_decl(impls, opt_impl, target_info, buf);
+    }
+}
+
+fn write_impl_tags<
+    'a,
+    I: IntoIterator)>,
+    F: Fn(&str, Option) -> String,
+>(
+    indentations: usize,
+    tags: I,
+    discriminant_name: &str,
+    buf: &mut String,
+    to_branch_str: F,
+) {
+    write_indents(indentations, buf);
+
+    buf.push_str("match self.discriminant() {\n");
+
+    for (tag_name, opt_payload_id) in tags {
+        let branch_str = to_branch_str(tag_name, *opt_payload_id);
+
+        write_indents(indentations + 1, buf);
+
+        writeln!(buf, "{discriminant_name}::{tag_name} => {branch_str}").unwrap();
+    }
+
+    write_indents(indentations, buf);
+
+    buf.push_str("}\n");
+}
+
+fn add_enumeration, S: AsRef + Display>(
+    name: &str,
+    target_info: TargetInfo,
+    typ: &RocType,
+    tags: I,
+    tag_bytes: u32,
+    types: &Types,
+    impls: &mut Impls,
+) {
+    let name = escape_kw(name.to_string());
+    let derive = derive_str(typ, types, false);
+    let repr_bits = tag_bytes * 8;
+
+    // e.g. "#[repr(u8)]\npub enum Foo {\n"
+    let mut buf = format!("{derive}\n#[repr(u{repr_bits})]\npub enum {name} {{\n");
+
+    // Debug impls should never vary by target_info.
+    let mut debug_buf = format!(
+        r#"impl core::fmt::Debug for {name} {{
+    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {{
+        match self {{
+"#
+    );
+
+    for (index, tag_name) in tags.enumerate() {
+        writeln!(buf, "{INDENT}{tag_name} = {index},").unwrap();
+
+        write_indents(3, &mut debug_buf);
+
+        writeln!(
+            debug_buf,
+            "Self::{tag_name} => f.write_str(\"{name}::{tag_name}\"),"
+        )
+        .unwrap();
+    }
+
+    write!(buf, "}}\n\n{debug_buf}{INDENT}{INDENT}}}\n{INDENT}}}\n}}").unwrap();
+
+    add_decl(impls, None, target_info, buf);
+}
+
+fn add_function(
+    // name: &str,
+    target_info: TargetInfo,
+    roc_fn: &RocFn,
+    types: &Types,
+    impls: &mut Impls,
+) {
+    let name = escape_kw(roc_fn.function_name.to_string());
+    // let derive = derive_str(types.get_type(struct_id), types, true);
+    let derive = "";
+    let pub_str = "pub ";
+    let mut buf;
+
+    let repr = "C";
+    buf = format!("{derive}\n#[repr({repr})]\n{pub_str}struct {name} {{\n");
+
+    let fields = [("closure_data", &roc_fn.lambda_set)];
+
+    for (label, type_id) in fields {
+        let type_str = type_name(*type_id, types);
+
+        // Tag union payloads have numbered fields, so we prefix them
+        // with an "f" because Rust doesn't allow struct fields to be numbers.
+        let label = escape_kw(label.to_string());
+
+        writeln!(buf, "{INDENT}pub {label}: {type_str},",).unwrap();
+    }
+
+    buf.push('}');
+
+    buf.push('\n');
+    buf.push('\n');
+
+    let extern_name = &roc_fn.extern_name;
+
+    let return_type_str = type_name(roc_fn.ret, types);
+
+    writeln!(buf, "impl {name} {{").unwrap();
+
+    write!(buf, "{INDENT}pub fn force_thunk(mut self").unwrap();
+    for (i, argument_type) in roc_fn.args.iter().enumerate() {
+        write!(buf, ", arg_{i}: {}", type_name(*argument_type, types)).unwrap();
+    }
+    writeln!(buf, ") -> {return_type_str} {{").unwrap();
+
+    writeln!(buf, "{INDENT}{INDENT}extern \"C\" {{").unwrap();
+
+    // fn extern_name(output: *mut return_type, arg1: arg1_type, ..., closure_data: *mut u8);
+    write!(buf, "{INDENT}{INDENT}{INDENT} fn {extern_name}(").unwrap();
+
+    for (i, argument_type) in roc_fn.args.iter().enumerate() {
+        write!(buf, "arg_{i}: &{}, ", type_name(*argument_type, types)).unwrap();
+    }
+
+    writeln!(
+        buf,
+        "closure_data: *mut u8, output: *mut {return_type_str});"
+    )
+    .unwrap();
+
+    // {argument_types} "
+
+    writeln!(buf, "{INDENT}{INDENT}}}").unwrap();
+
+    writeln!(buf).unwrap();
+
+    writeln!(
+        buf,
+        "{INDENT}{INDENT}let mut output = std::mem::MaybeUninit::uninit();"
+    )
+    .unwrap();
+
+    writeln!(
+        buf,
+        "{INDENT}{INDENT}let ptr = self.closure_data.as_mut_ptr();"
+    )
+    .unwrap();
+
+    write!(buf, "{INDENT}{INDENT}unsafe {{ {extern_name}(").unwrap();
+
+    for (i, _) in roc_fn.args.iter().enumerate() {
+        write!(buf, "&arg_{i}, ").unwrap();
+    }
+
+    writeln!(buf, "ptr, output.as_mut_ptr()) }};").unwrap();
+
+    writeln!(buf, "{INDENT}{INDENT}unsafe {{ output.assume_init() }}").unwrap();
+
+    writeln!(buf, "{INDENT}}}").unwrap();
+    buf.push('}');
+
+    add_decl(impls, None, target_info, buf);
+}
+
+fn add_struct(
+    name: &str,
+    target_info: TargetInfo,
+    fields: &RocStructFields,
+    struct_id: TypeId,
+    types: &Types,
+    impls: &mut Impls,
+    is_tag_union_payload: bool,
+) {
+    let name = escape_kw(name.to_string());
+    let derive = derive_str(types.get_type(struct_id), types, true);
+    let pub_str = if is_tag_union_payload { "" } else { "pub " };
+    let mut buf;
+
+    match fields {
+        RocStructFields::HasNoClosure { fields } => {
+            let repr = if fields.len() == 1 {
+                "transparent"
+            } else {
+                "C"
+            };
+
+            buf = format!("{derive}\n#[repr({repr})]\n{pub_str}struct {name} {{\n");
+
+            for (label, type_id) in fields {
+                let type_str = type_name(*type_id, types);
+
+                // Tag union payloads have numbered fields, so we prefix them
+                // with an "f" because Rust doesn't allow struct fields to be numbers.
+                let label = if is_tag_union_payload {
+                    format!("f{label}")
+                } else {
+                    escape_kw(label.to_string())
+                };
+
+                writeln!(buf, "{INDENT}pub {label}: {type_str},",).unwrap();
+            }
+
+            buf.push('}');
+        }
+        RocStructFields::HasClosure { fields: _ } => {
+            buf = "//TODO HAS CLOSURE 2".to_string();
+        }
+    }
+
+    add_decl(impls, None, target_info, buf);
+}
+
+fn type_name(id: TypeId, types: &Types) -> String {
+    match types.get_type(id) {
+        RocType::Unit => "()".to_string(),
+        RocType::EmptyTagUnion => "std::convert::Infallible".to_string(),
+        RocType::RocStr => "roc_std::RocStr".to_string(),
+        RocType::Bool => "bool".to_string(),
+        RocType::Num(RocNum::U8) => "u8".to_string(),
+        RocType::Num(RocNum::U16) => "u16".to_string(),
+        RocType::Num(RocNum::U32) => "u32".to_string(),
+        RocType::Num(RocNum::U64) => "u64".to_string(),
+        RocType::Num(RocNum::U128) => "roc_std::U128".to_string(),
+        RocType::Num(RocNum::I8) => "i8".to_string(),
+        RocType::Num(RocNum::I16) => "i16".to_string(),
+        RocType::Num(RocNum::I32) => "i32".to_string(),
+        RocType::Num(RocNum::I64) => "i64".to_string(),
+        RocType::Num(RocNum::I128) => "roc_std::I128".to_string(),
+        RocType::Num(RocNum::F32) => "f32".to_string(),
+        RocType::Num(RocNum::F64) => "f64".to_string(),
+        RocType::Num(RocNum::Dec) => "roc_std::RocDec".to_string(),
+        RocType::RocDict(key_id, val_id) => format!(
+            "roc_std::RocDict<{}, {}>",
+            type_name(*key_id, types),
+            type_name(*val_id, types)
+        ),
+        RocType::RocSet(elem_id) => format!("roc_std::RocSet<{}>", type_name(*elem_id, types)),
+        RocType::RocList(elem_id) => format!("roc_std::RocList<{}>", type_name(*elem_id, types)),
+        RocType::RocBox(elem_id) => format!("roc_std::RocBox<{}>", type_name(*elem_id, types)),
+        RocType::Unsized => "roc_std::RocList".to_string(),
+        RocType::RocResult(ok_id, err_id) => {
+            format!(
+                "roc_std::RocResult<{}, {}>",
+                type_name(*ok_id, types),
+                type_name(*err_id, types)
+            )
+        }
+        RocType::Struct { name, .. }
+        | RocType::TagUnionPayload { name, .. }
+        | RocType::TagUnion(RocTagUnion::NonRecursive { name, .. })
+        | RocType::TagUnion(RocTagUnion::Recursive { name, .. })
+        | RocType::TagUnion(RocTagUnion::Enumeration { name, .. })
+        | RocType::TagUnion(RocTagUnion::NullableWrapped { name, .. })
+        | RocType::TagUnion(RocTagUnion::NullableUnwrapped { name, .. })
+        | RocType::TagUnion(RocTagUnion::NonNullableUnwrapped { name, .. })
+        | RocType::TagUnion(RocTagUnion::SingleTagStruct { name, .. }) => escape_kw(name.clone()),
+        RocType::RecursivePointer(content) => type_name(*content, types),
+        RocType::Function(RocFn { function_name, .. }) => escape_kw(function_name.clone()),
+    }
+}
+
+/// This explicitly asks for whether to include Debug because in the very specific
+/// case of a struct that's a payload for a recursive tag union, typ.has_enumeration()
+/// will return true, but actually we want to derive Debug here anyway.
+fn derive_str(typ: &RocType, types: &Types, include_debug: bool) -> String {
+    let mut derives = vec!["Clone"];
+
+    if !cannot_derive_copy(typ, types) {
+        derives.push("Copy");
+    }
+
+    if include_debug {
+        derives.push("Debug");
+    }
+
+    if !has_functions(typ, types) {
+        derives.push("PartialEq");
+        derives.push("PartialOrd");
+
+        if !has_float(typ, types) {
+            derives.push("Eq");
+            derives.push("Ord");
+            derives.push("Hash");
+        }
+
+        if !cannot_derive_default(typ, types) {
+            derives.push("Default");
+        }
+    }
+
+    derives.sort();
+
+    format!("#[derive({})]", derives.join(", "))
+}
+
+fn has_functions(start: &RocType, types: &Types) -> bool {
+    let mut seen: Vec = vec![];
+    let mut stack = vec![start];
+
+    macro_rules! push {
+        ($id:expr) => {{
+            if !seen.contains($id) {
+                seen.push(*$id);
+                stack.push(types.get_type(*$id));
+            }
+        }};
+    }
+
+    while let Some(typ) = stack.pop() {
+        match typ {
+            RocType::RocStr
+            | RocType::Bool
+            | RocType::Unit
+            | RocType::Num(_)
+            | RocType::EmptyTagUnion
+            | RocType::Unsized
+            | RocType::TagUnion(RocTagUnion::Enumeration { .. }) => { /* terminal */ }
+
+            RocType::TagUnion(RocTagUnion::NonRecursive { tags, .. })
+            | RocType::TagUnion(RocTagUnion::Recursive { tags, .. })
+            | RocType::TagUnion(RocTagUnion::NullableWrapped { tags, .. }) => {
+                for (_, opt_payload_id) in tags.iter() {
+                    if let Some(payload_id) = opt_payload_id {
+                        push!(payload_id);
+                    }
+                }
+            }
+            RocType::RocBox(type_id)
+            | RocType::RocList(type_id)
+            | RocType::RocSet(type_id)
+            | RocType::TagUnion(RocTagUnion::NullableUnwrapped {
+                non_null_payload: type_id,
+                ..
+            })
+            | RocType::TagUnion(RocTagUnion::NonNullableUnwrapped {
+                payload: type_id, ..
+            }) => {
+                push!(type_id);
+            }
+
+            RocType::TagUnion(RocTagUnion::SingleTagStruct {
+                payload: RocSingleTagPayload::HasNoClosure { payload_fields },
+                ..
+            }) => {
+                for payload_id in payload_fields {
+                    push!(payload_id);
+                }
+            }
+
+            RocType::TagUnion(RocTagUnion::SingleTagStruct {
+                payload: RocSingleTagPayload::HasClosure { payload_getters },
+                ..
+            }) => {
+                for (payload_id, _) in payload_getters {
+                    push!(payload_id);
+                }
+            }
+
+            RocType::TagUnionPayload {
+                fields: RocStructFields::HasNoClosure { fields },
+                ..
+            }
+            | RocType::Struct {
+                fields: RocStructFields::HasNoClosure { fields },
+                ..
+            } => {
+                for (_, type_id) in fields {
+                    push!(type_id);
+                }
+            }
+
+            RocType::TagUnionPayload {
+                fields: RocStructFields::HasClosure { fields },
+                ..
+            }
+            | RocType::Struct {
+                fields: RocStructFields::HasClosure { fields },
+                ..
+            } => {
+                for (_, type_id, _) in fields {
+                    push!(type_id);
+                }
+            }
+            RocType::RecursivePointer(type_id) => {
+                push!(type_id);
+            }
+            RocType::RocResult(id1, id2) | RocType::RocDict(id1, id2) => {
+                push!(id1);
+                push!(id2);
+            }
+            RocType::Function(_) => return true,
+        }
+    }
+
+    false
+}
+
+#[allow(clippy::too_many_arguments)]
+fn add_nullable_unwrapped(
+    name: &str,
+    target_info: TargetInfo,
+    id: TypeId,
+    null_tag: &str,
+    non_null_tag: &str,
+    non_null_payload: TypeId,
+    _null_represents_first_tag: bool, // TODO use this!
+    types: &Types,
+    impls: &mut Impls,
+) {
+    let mut tag_names = vec![null_tag.to_string(), non_null_tag.to_string()];
+
+    tag_names.sort();
+
+    let discriminant_name = add_discriminant(name, target_info, tag_names, 1, types, impls);
+    let payload_type = types.get_type(non_null_payload);
+    let payload_type_name = type_name(non_null_payload, types);
+    let cannot_derive_copy = cannot_derive_copy(payload_type, types);
+
+    // The opaque struct for the tag union
+    {
+        // This struct needs its own Clone impl because it has
+        // a refcount to bump
+        let derive_extras = if has_float(types.get_type(id), types) {
+            ""
+        } else {
+            ", Eq, Ord, Hash"
+        };
+        let body = format!(
+            r#"#[repr(transparent)]
+#[derive(PartialEq, PartialOrd{derive_extras})]
+pub struct {name} {{
+    pointer: *mut core::mem::ManuallyDrop<{payload_type_name}>,
+}}"#
+        );
+
+        add_decl(impls, None, target_info, body);
+    }
+
+    // The impl for the tag union
+    {
+        let opt_impl = Some(format!("impl {name}"));
+
+        add_decl(
+            impls,
+            opt_impl.clone(),
+            target_info,
+            RECURSIVE_TAG_UNION_STORAGE.to_string(),
+        );
+
+        add_decl(
+            impls,
+            opt_impl.clone(),
+            target_info,
+            format!(
+                r#"{DISCRIMINANT_DOC_COMMENT}
+    pub fn discriminant(&self) -> {discriminant_name} {{
+        if self.pointer.is_null() {{
+            {discriminant_name}::{null_tag}
+        }} else {{
+            {discriminant_name}::{non_null_tag}
+        }}
+    }}"#
+            ),
+        );
+
+        let owned_ret_type;
+        let borrowed_ret_type;
+        let payload_args;
+        let args_to_payload;
+        let owned_ret;
+        let borrowed_ret;
+
+        match payload_type {
+            RocType::Unit
+            | RocType::EmptyTagUnion
+            | RocType::RocStr
+            | RocType::Bool
+            | RocType::Num(_)
+            | RocType::RocList(_)
+            | RocType::RocDict(_, _)
+            | RocType::RocSet(_)
+            | RocType::RocBox(_)
+            | RocType::RocResult(_, _)
+            | RocType::TagUnion(_)
+            | RocType::RecursivePointer { .. } => {
+                owned_ret_type = type_name(non_null_payload, types);
+                borrowed_ret_type = format!("&{owned_ret_type}");
+                payload_args = format!("arg: {owned_ret_type}");
+                args_to_payload = "arg".to_string();
+                owned_ret = "payload".to_string();
+                borrowed_ret = format!("&{owned_ret}");
+            }
+            RocType::Struct { fields, name } => match fields {
+                RocStructFields::HasNoClosure { fields } => {
+                    let answer =
+                        tag_union_struct_help(name, fields, non_null_payload, types, false);
+
+                    payload_args = answer.payload_args;
+                    args_to_payload = answer.args_to_payload;
+                    owned_ret = answer.owned_ret;
+                    borrowed_ret = answer.borrowed_ret;
+                    owned_ret_type = answer.owned_ret_type;
+                    borrowed_ret_type = answer.borrowed_ret_type;
+                }
+
+                RocStructFields::HasClosure { .. } => todo!(),
+            },
+            RocType::TagUnionPayload { fields, name } => match fields {
+                RocStructFields::HasNoClosure { fields } => {
+                    let answer = tag_union_struct_help(name, fields, non_null_payload, types, true);
+
+                    payload_args = answer.payload_args;
+                    args_to_payload = answer.args_to_payload;
+                    owned_ret = answer.owned_ret;
+                    borrowed_ret = answer.borrowed_ret;
+                    owned_ret_type = answer.owned_ret_type;
+                    borrowed_ret_type = answer.borrowed_ret_type;
+                }
+                RocStructFields::HasClosure { .. } => todo!(),
+            },
+            RocType::Function { .. } => todo!(),
+            RocType::Unsized => todo!(),
+        };
+
+        // Add a convenience constructor function for the tag with the payload, e.g.
+        //
+        // /// Construct a tag named Cons, with the appropriate payload
+        // pub fn Cons(payload: roc_std::RocStr) -> Self {
+        //     let size = core::mem::size_of::();
+        //     let align = core::mem::align_of::();
+        //
+        //     unsafe {
+        //         let pointer =
+        //             roc_alloc(size, align as u32) as *mut core::mem::ManuallyDrop;
+        //
+        //         *pointer = core::mem::ManuallyDrop::new(payload);
+        //
+        //         Self { pointer }
+        //     }
+        // }
+        add_decl(
+            impls,
+            opt_impl.clone(),
+            target_info,
+            format!(
+                r#"/// Construct a tag named `{non_null_tag}`, with the appropriate payload
+    pub fn {non_null_tag}({payload_args}) -> Self {{
+        let payload_align = core::mem::align_of::<{payload_type_name}>();
+        let self_align = core::mem::align_of::();
+        let size = self_align + core::mem::size_of::<{payload_type_name}>();
+        let payload = {args_to_payload};
+
+        unsafe {{
+            // Store the payload at `self_align` bytes after the allocation,
+            // to leave room for the refcount.
+            let alloc_ptr = crate::roc_alloc(size, payload_align as u32);
+            let payload_ptr = alloc_ptr.cast::().add(self_align).cast::>();
+
+            *payload_ptr = payload;
+
+            // The reference count is stored immediately before the payload,
+            // which isn't necessarily the same as alloc_ptr - e.g. when alloc_ptr
+            // needs an alignment of 16.
+            let storage_ptr = payload_ptr.cast::().sub(1);
+            storage_ptr.write(roc_std::Storage::new_reference_counted());
+
+            Self {{ pointer: payload_ptr }}
+        }}
+    }}"#,
+            ),
+        );
+
+        {
+            let assign_payload = if cannot_derive_copy {
+                r#"{{
+            let mut uninitialized = core::mem::MaybeUninit::uninit();
+            let swapped = unsafe {{
+                core::mem::replace(
+                    &mut *self.pointer,
+                    core::mem::ManuallyDrop::new(uninitialized.assume_init()),
+                )
+            }};
+
+            core::mem::forget(self);
+
+            core::mem::ManuallyDrop::into_inner(swapped)
+        }}"#
+            } else {
+                "*self.pointer"
+            };
+
+            add_decl(
+                impls,
+                opt_impl.clone(),
+                target_info,
+                format!(
+                    r#"/// Unsafely assume this `{name}` has a `.discriminant()` of `{non_null_tag}` and convert it to `{non_null_tag}`'s payload.
+    /// (Always examine `.discriminant()` first to make sure this is the correct variant!)
+    /// Panics in debug builds if the `.discriminant()` doesn't return {non_null_tag}.
+    pub unsafe fn into_{non_null_tag}(self) -> {owned_ret_type} {{
+        debug_assert_eq!(self.discriminant(), {discriminant_name}::{non_null_tag});
+
+        let payload = {assign_payload};
+
+        {owned_ret}
+    }}"#,
+                ),
+            );
+        }
+
+        add_decl(
+            impls,
+            opt_impl.clone(),
+            target_info,
+            format!(
+                r#"/// Unsafely assume this `{name}` has a `.discriminant()` of `{non_null_tag}` and return its payload.
+    /// (Always examine `.discriminant()` first to make sure this is the correct variant!)
+    /// Panics in debug builds if the `.discriminant()` doesn't return `{non_null_tag}`.
+    pub unsafe fn as_{non_null_tag}(&self) -> {borrowed_ret_type} {{
+        debug_assert_eq!(self.discriminant(), {discriminant_name}::{non_null_tag});
+
+        let payload = &*self.pointer;
+
+        {borrowed_ret}
+    }}"#,
+            ),
+        );
+
+        // Add a convenience constructor function for the nullable tag, e.g.
+        //
+        // /// A tag named Nil, which has no payload.
+        // pub const Nil: Self = Self {
+        //     pointer: core::ptr::null_mut(),
+        // };
+        add_decl(
+            impls,
+            opt_impl.clone(),
+            target_info,
+            format!(
+                r#"/// A tag named {null_tag}, which has no payload.
+    pub const {null_tag}: Self = Self {{
+        pointer: core::ptr::null_mut(),
+    }};"#,
+            ),
+        );
+
+        add_decl(
+            impls,
+            opt_impl.clone(),
+            target_info,
+            format!(
+                r#"/// Other `into_` methods return a payload, but since the {null_tag} tag
+    /// has no payload, this does nothing and is only here for completeness.
+    pub fn into_{null_tag}(self) {{
+        ()
+    }}"#,
+            ),
+        );
+
+        add_decl(
+            impls,
+            opt_impl,
+            target_info,
+            format!(
+                r#"/// Other `as` methods return a payload, but since the {null_tag} tag
+    /// has no payload, this does nothing and is only here for completeness.
+    pub fn as_{null_tag}(&self) {{
+        ()
+    }}"#,
+            ),
+        );
+    }
+
+    // The Clone impl for the tag union
+    {
+        // Note that these never have Copy because they always contain a pointer.
+        let opt_impl = Some(format!("impl Clone for {name}"));
+
+        add_decl(
+            impls,
+            opt_impl,
+            target_info,
+            RECURSIVE_TAG_UNION_CLONE.to_string(),
+        );
+    }
+
+    // The Drop impl for the tag union
+    {
+        let opt_impl = Some(format!("impl Drop for {name}"));
+
+        add_decl(
+            impls,
+            opt_impl,
+            target_info,
+            r#"fn drop(&mut self) {{
+        // We only need to do any work if there's actually a heap-allocated payload.
+        if let Some(storage) = self.storage() {{
+            let mut new_storage = storage.get();
+
+            // Decrement the refcount
+            let needs_dealloc = !new_storage.is_readonly() && new_storage.decrease();
+
+            if needs_dealloc {{
+                // Drop the payload first.
+                unsafe {{
+                    core::mem::ManuallyDrop::drop(&mut core::ptr::read(self.pointer));
+                }}
+
+                // Dealloc the pointer
+                let alignment = core::mem::align_of::().max(core::mem::align_of::());
+
+                unsafe {{
+                    crate::roc_dealloc(storage.as_ptr().cast(), alignment as u32);
+                }}
+            }} else {{
+                // Write the storage back.
+                storage.set(new_storage);
+            }}
+        }}
+    }}"#.to_string(),
+        );
+    }
+
+    // The Debug impl for the tag union
+    {
+        let opt_impl = Some(format!("impl core::fmt::Debug for {name}"));
+        let extra_deref = if cannot_derive_copy { "*" } else { "" };
+
+        let fields_str = match payload_type {
+            RocType::Unit
+            | RocType::EmptyTagUnion
+            | RocType::RocStr
+            | RocType::Bool
+            | RocType::Num(_)
+            | RocType::RocList(_)
+            | RocType::RocDict(_, _)
+            | RocType::RocSet(_)
+            | RocType::RocBox(_)
+            | RocType::RocResult(_, _)
+            | RocType::TagUnion(_)
+            | RocType::RecursivePointer { .. } => {
+                format!(
+                    r#"f.debug_tuple("{non_null_tag}").field(&*{extra_deref}self.pointer).finish()"#
+                )
+            }
+            RocType::Struct { fields, .. } => {
+                let mut buf = Vec::new();
+
+                match fields {
+                    RocStructFields::HasNoClosure { fields } => {
+                        for (label, _) in fields {
+                            buf.push(format!(".field(&(&*{extra_deref}self.pointer).{label})"));
+                        }
+                    }
+                    RocStructFields::HasClosure { fields: _ } => todo!(),
+                }
+
+                buf.join(&format!("\n{INDENT}{INDENT}{INDENT}{INDENT}{INDENT}"))
+            }
+            RocType::TagUnionPayload { fields, .. } => {
+                let mut buf = Vec::new();
+
+                match fields {
+                    RocStructFields::HasNoClosure { fields } => {
+                        for (label, _) in fields {
+                            // Needs an "f" prefix
+                            buf.push(format!(".field(&(&*{extra_deref}self.pointer).f{label})"));
+                        }
+                    }
+                    RocStructFields::HasClosure { fields: _ } => todo!(),
+                }
+
+                buf.join(&format!("\n{INDENT}{INDENT}{INDENT}{INDENT}{INDENT}"))
+            }
+            RocType::Unsized => todo!(),
+            RocType::Function { .. } => todo!(),
+        };
+
+        let body = format!(
+            r#"fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {{
+        if self.pointer.is_null() {{
+            f.write_str("{name}::{null_tag}")
+        }} else {{
+            f.write_str("{name}::")?;
+
+            unsafe {{
+                f.debug_tuple("{non_null_tag}")
+                    {fields_str}
+                    .finish()
+            }}
+        }}
+    }}"#
+        );
+
+        add_decl(impls, opt_impl, target_info, body);
+    }
+}
+
+fn arch_to_str(architecture: Architecture) -> &'static str {
+    match architecture {
+        Architecture::X86_64 => "x86_64",
+        Architecture::X86_32 => "x86",
+        Architecture::Aarch64 => "aarch64",
+        Architecture::Aarch32 => "arm",
+        Architecture::Wasm32 => "wasm32",
+    }
+}
+
+fn write_indents(indentations: usize, buf: &mut String) {
+    for _ in 0..indentations {
+        buf.push_str(INDENT);
+    }
+}
+
+fn max_pointer_tagged_variants(architecture: Architecture) -> usize {
+    match architecture {
+        // On a 64-bit system, pointers have 3 bits that are unused, so return 2^3 = 8
+        Architecture::X86_64 | Architecture::Aarch64 => 8,
+        // On a 32-bit system, pointers have 2 bits that are unused, so return 2^4 = 4
+        Architecture::X86_32 | Architecture::Aarch32 | Architecture::Wasm32 => 4,
+    }
+}
+
+#[inline(always)]
+fn tagged_pointer_bitmask(architecture: Architecture) -> u8 {
+    match architecture {
+        // On a 64-bit system, pointers have 3 bits that are unused
+        Architecture::X86_64 | Architecture::Aarch64 => 0b0000_0111,
+        // On a 32-bit system, pointers have 2 bits that are unused
+        Architecture::X86_32 | Architecture::Aarch32 | Architecture::Wasm32 => 0b0000_0011,
+    }
+}
+
+struct StructIngredients {
+    payload_args: String,
+    args_to_payload: String,
+    owned_ret: String,
+    borrowed_ret: String,
+    owned_ret_type: String,
+    borrowed_ret_type: String,
+}
+
+fn tag_union_struct_help(
+    name: &str,
+    fields: &[(String, TypeId)],
+    payload_id: TypeId,
+    types: &Types,
+    is_tag_union_payload: bool,
+) -> StructIngredients {
+    let mut ret_types = if is_tag_union_payload {
+        // This will be a tuple we create when iterating over the fields.
+        Vec::new()
+    } else {
+        vec![name.to_string()]
+    };
+
+    let mut ret_values = Vec::new();
+
+    for (label, type_id) in fields.iter() {
+        let label = if is_tag_union_payload {
+            // Tag union payload fields need "f" prefix
+            // because they're numbers
+            format!("f{label}")
+        } else {
+            escape_kw(label.to_string())
+        };
+
+        ret_values.push(format!("payload.{label}"));
+
+        if is_tag_union_payload {
+            // Build up the tuple we'll return.
+            ret_types.push(type_name(*type_id, types));
+        }
+    }
+
+    let payload_type_name = type_name(payload_id, types);
+    let payload_args = ret_types
+        .iter()
+        .enumerate()
+        .map(|(index, typ)| format!("arg{index}: {typ}"))
+        .collect::>()
+        .join(", ");
+    let args_to_payload = if is_tag_union_payload {
+        let prefixed_fields = fields
+            .iter()
+            .enumerate()
+            .map(|(index, (label, _))| {
+                let mut indents = String::new();
+
+                for _ in 0..5 {
+                    indents.push_str(INDENT);
+                }
+
+                // Tag union payload fields need "f" prefix
+                // because they're numbers
+                format!("{indents}f{label}: arg{index},")
+            })
+            .collect::>()
+            .join("\n");
+
+        if cannot_derive_copy(types.get_type(payload_id), types) {
+            format!(
+        "core::mem::ManuallyDrop::new({payload_type_name} {{\n{prefixed_fields}\n{INDENT}{INDENT}{INDENT}{INDENT}}})")
+        } else {
+            format!("{payload_type_name} {{\n{prefixed_fields}\n{INDENT}{INDENT}{INDENT}{INDENT}}}")
+        }
+    } else {
+        "core::mem::ManuallyDrop::new(arg0)".to_string()
+    };
+    let owned_ret;
+    let borrowed_ret;
+    let owned_ret_type;
+    let borrowed_ret_type;
+
+    if ret_types.len() == 1 {
+        owned_ret_type = ret_types.join("");
+        borrowed_ret_type = format!("&{owned_ret_type}");
+
+        if is_tag_union_payload {
+            let ret_val = ret_values.first().unwrap();
+
+            owned_ret = format!("\n{INDENT}{INDENT}{ret_val}");
+            borrowed_ret = format!("\n{INDENT}{INDENT}&{ret_val}");
+        } else {
+            owned_ret = format!("\n{INDENT}{INDENT}payload");
+            // We already define `let payload = &...` so no need for an extra `&` here.
+            borrowed_ret = owned_ret.clone();
+        };
+    } else {
+        owned_ret_type = format!("({})", ret_types.join(", "));
+        borrowed_ret_type = format!(
+            "({})",
+            ret_types
+                .iter()
+                .map(|ret_type| { format!("&{ret_type}") })
+                .collect::>()
+                .join(", ")
+        );
+        owned_ret = {
+            let lines = ret_values
+                .iter()
+                .map(|line| format!("\n{INDENT}{INDENT}{INDENT}{line}"))
+                .collect::>()
+                .join(", ");
+
+            format!("({lines}\n{INDENT}{INDENT})")
+        };
+        borrowed_ret = {
+            let lines = ret_values
+                .iter()
+                .map(|line| format!("\n{INDENT}{INDENT}{INDENT}&{line}"))
+                .collect::>()
+                .join(", ");
+
+            format!("({lines}\n{INDENT}{INDENT})")
+        };
+    }
+
+    StructIngredients {
+        payload_args,
+        args_to_payload,
+        owned_ret,
+        borrowed_ret,
+        owned_ret_type,
+        borrowed_ret_type,
+    }
+}
+
+fn cannot_derive_default(roc_type: &RocType, types: &Types) -> bool {
+    match roc_type {
+        RocType::Unit
+        | RocType::EmptyTagUnion
+        | RocType::TagUnion { .. }
+        | RocType::RocResult(_, _)
+        | RocType::RecursivePointer { .. }
+        | RocType::Struct {
+            fields: RocStructFields::HasClosure { .. },
+            ..
+        }
+        | RocType::TagUnionPayload {
+            fields: RocStructFields::HasClosure { .. },
+            ..
+        }
+        | RocType::Unsized => true,
+        RocType::Function { .. } => true,
+        RocType::RocStr | RocType::Bool | RocType::Num(_) => false,
+        RocType::RocList(id) | RocType::RocSet(id) | RocType::RocBox(id) => {
+            cannot_derive_default(types.get_type(*id), types)
+        }
+        RocType::RocDict(key_id, val_id) => {
+            cannot_derive_default(types.get_type(*key_id), types)
+                || cannot_derive_default(types.get_type(*val_id), types)
+        }
+        RocType::Struct {
+            fields: RocStructFields::HasNoClosure { fields },
+            ..
+        } => fields
+            .iter()
+            .any(|(_, type_id)| cannot_derive_default(types.get_type(*type_id), types)),
+        RocType::TagUnionPayload {
+            fields: RocStructFields::HasNoClosure { fields },
+            ..
+        } => fields
+            .iter()
+            .any(|(_, type_id)| cannot_derive_default(types.get_type(*type_id), types)),
+    }
+}
+
+/// Useful when determining whether to derive Copy in a Rust type.
+fn cannot_derive_copy(roc_type: &RocType, types: &Types) -> bool {
+    match roc_type {
+        RocType::Unit
+        | RocType::EmptyTagUnion
+        | RocType::Bool
+        | RocType::Num(_)
+        | RocType::TagUnion(RocTagUnion::Enumeration { .. })
+        | RocType::Unsized
+        | RocType::Function { .. } => false,
+        RocType::RocStr
+        | RocType::RocList(_)
+        | RocType::RocDict(_, _)
+        | RocType::RocSet(_)
+        | RocType::RocBox(_)
+        | RocType::TagUnion(RocTagUnion::NullableUnwrapped { .. })
+        | RocType::TagUnion(RocTagUnion::NullableWrapped { .. })
+        | RocType::TagUnion(RocTagUnion::Recursive { .. })
+        | RocType::RecursivePointer { .. }
+        | RocType::TagUnion(RocTagUnion::NonNullableUnwrapped { .. }) => true,
+        RocType::TagUnion(RocTagUnion::SingleTagStruct {
+            payload: RocSingleTagPayload::HasNoClosure { payload_fields },
+            ..
+        }) => payload_fields
+            .iter()
+            .any(|type_id| cannot_derive_copy(types.get_type(*type_id), types)),
+        RocType::TagUnion(RocTagUnion::SingleTagStruct {
+            payload: RocSingleTagPayload::HasClosure { payload_getters },
+            ..
+        }) => payload_getters
+            .iter()
+            .any(|(type_id, _)| cannot_derive_copy(types.get_type(*type_id), types)),
+        RocType::TagUnion(RocTagUnion::NonRecursive { tags, .. }) => {
+            tags.iter().any(|(_, payloads)| {
+                payloads
+                    .iter()
+                    .any(|id| cannot_derive_copy(types.get_type(*id), types))
+            })
+        }
+        RocType::RocResult(ok_id, err_id) => {
+            cannot_derive_copy(types.get_type(*ok_id), types)
+                || cannot_derive_copy(types.get_type(*err_id), types)
+        }
+        RocType::Struct {
+            fields: RocStructFields::HasNoClosure { fields },
+            ..
+        }
+        | RocType::TagUnionPayload {
+            fields: RocStructFields::HasNoClosure { fields },
+            ..
+        } => fields
+            .iter()
+            .any(|(_, type_id)| cannot_derive_copy(types.get_type(*type_id), types)),
+        RocType::Struct {
+            fields: RocStructFields::HasClosure { fields },
+            ..
+        }
+        | RocType::TagUnionPayload {
+            fields: RocStructFields::HasClosure { fields },
+            ..
+        } => fields
+            .iter()
+            .any(|(_, type_id, _)| cannot_derive_copy(types.get_type(*type_id), types)),
+    }
+}
+
+/// Useful when determining whether to derive Eq, Ord, and Hash in a Rust type.
+fn has_float(roc_type: &RocType, types: &Types) -> bool {
+    has_float_help(roc_type, types, &[])
+}
+
+fn has_float_help(roc_type: &RocType, types: &Types, do_not_recurse: &[TypeId]) -> bool {
+    match roc_type {
+        RocType::Num(num) => {
+            use RocNum::*;
+
+            match num {
+                F32 | F64 => true,
+                I8 | U8 | I16 | U16 | I32 | U32 | I64 | U64 | I128 | U128 | Dec => false,
+            }
+        }
+        RocType::Unit
+        | RocType::EmptyTagUnion
+        | RocType::RocStr
+        | RocType::Bool
+        | RocType::TagUnion(RocTagUnion::Enumeration { .. })
+        | RocType::Function { .. }
+        | RocType::Unsized => false,
+        RocType::RocList(id) | RocType::RocSet(id) | RocType::RocBox(id) => {
+            has_float_help(types.get_type(*id), types, do_not_recurse)
+        }
+        RocType::RocResult(ok_id, err_id) => {
+            has_float_help(types.get_type(*ok_id), types, do_not_recurse)
+                || has_float_help(types.get_type(*err_id), types, do_not_recurse)
+        }
+        RocType::RocDict(key_id, val_id) => {
+            has_float_help(types.get_type(*key_id), types, do_not_recurse)
+                || has_float_help(types.get_type(*val_id), types, do_not_recurse)
+        }
+        RocType::TagUnionPayload {
+            fields: RocStructFields::HasNoClosure { fields },
+            name: _,
+        }
+        | RocType::Struct {
+            fields: RocStructFields::HasNoClosure { fields },
+            name: _,
+        } => fields
+            .iter()
+            .any(|(_, type_id)| has_float_help(types.get_type(*type_id), types, do_not_recurse)),
+        RocType::TagUnionPayload {
+            fields: RocStructFields::HasClosure { fields },
+            name: _,
+        }
+        | RocType::Struct {
+            fields: RocStructFields::HasClosure { fields },
+            name: _,
+        } => fields
+            .iter()
+            .any(|(_, type_id, _)| has_float_help(types.get_type(*type_id), types, do_not_recurse)),
+        RocType::TagUnion(RocTagUnion::SingleTagStruct {
+            payload: RocSingleTagPayload::HasNoClosure { payload_fields },
+            ..
+        }) => payload_fields
+            .iter()
+            .any(|type_id| has_float_help(types.get_type(*type_id), types, do_not_recurse)),
+        RocType::TagUnion(RocTagUnion::SingleTagStruct {
+            payload: RocSingleTagPayload::HasClosure { payload_getters },
+            ..
+        }) => payload_getters
+            .iter()
+            .any(|(type_id, _)| has_float_help(types.get_type(*type_id), types, do_not_recurse)),
+        RocType::TagUnion(RocTagUnion::Recursive {
+            tags,
+            name: _,
+            discriminant_offset: _,
+            discriminant_size: _,
+        })
+        | RocType::TagUnion(RocTagUnion::NonRecursive {
+            tags,
+            name: _,
+            discriminant_offset: _,
+            discriminant_size: _,
+        })
+        | RocType::TagUnion(RocTagUnion::NullableWrapped {
+            tags,
+            name: _,
+            index_of_null_tag: _,
+            discriminant_size: _,
+            discriminant_offset: _,
+        }) => tags.iter().any(|(_, payloads)| {
+            payloads
+                .iter()
+                .any(|id| has_float_help(types.get_type(*id), types, do_not_recurse))
+        }),
+        RocType::TagUnion(RocTagUnion::NonNullableUnwrapped { payload, .. })
+        | RocType::TagUnion(RocTagUnion::NullableUnwrapped {
+            non_null_payload: payload,
+            ..
+        })
+        | RocType::RecursivePointer(payload) => {
+            if do_not_recurse.contains(payload) {
+                false
+            } else {
+                let mut do_not_recurse: Vec = do_not_recurse.into();
+
+                do_not_recurse.push(*payload);
+
+                has_float_help(types.get_type(*payload), types, &do_not_recurse)
+            }
+        }
+    }
+}
+
+// Based on https://doc.rust-lang.org/reference/keywords.html
+const RESERVED_KEYWORDS: &[&str] = &[
+    "try", "abstract", "become", "box", "do", "final", "macro", "override", "priv", "typeof",
+    "unsized", "virtual", "yield", "async", "await", "dyn", "as", "break", "const", "continue",
+    "crate", "else", "enum", "extern", "false", "fn", "for", "if", "impl", "in", "let", "loop",
+    "match", "mod", "move", "mut", "pub", "ref", "return", "self", "Self", "static", "struct",
+    "super", "trait", "true", "type", "unsafe", "use", "where", "while",
+];
+
+/// Escape a Rust reserved keyword, if necessary.
+fn escape_kw(input: String) -> String {
+    let is_reserved_keyword = RESERVED_KEYWORDS.contains(&input.as_str());
+
+    if is_reserved_keyword {
+        // Use a raw identifier for this, to prevent a syntax error due to using a reserved keyword.
+        // https://doc.rust-lang.org/rust-by-example/compatibility/raw_identifiers.html
+        // Another design would be to add an underscore after it; this is an experiment!
+        format!("r#{input}")
+    } else {
+        input
+    }
+}
diff --git a/bindgen/src/structs.rs b/crates/glue/src/structs.rs
similarity index 100%
rename from bindgen/src/structs.rs
rename to crates/glue/src/structs.rs
diff --git a/crates/glue/src/types.rs b/crates/glue/src/types.rs
new file mode 100644
index 0000000000..2a030f4203
--- /dev/null
+++ b/crates/glue/src/types.rs
@@ -0,0 +1,2320 @@
+use crate::enums::Enums;
+use crate::roc_type;
+use crate::structs::Structs;
+use bumpalo::Bump;
+use fnv::FnvHashMap;
+use roc_builtins::bitcode::{
+    FloatWidth::*,
+    IntWidth::{self, *},
+};
+use roc_collections::{MutMap, VecMap};
+use roc_error_macros::todo_lambda_erasure;
+use roc_module::{
+    ident::TagName,
+    symbol::{Interns, Symbol},
+};
+use roc_mono::{
+    ir::LambdaSetId,
+    layout::{
+        cmp_fields, ext_var_is_empty_tag_union, round_up_to_alignment, Builtin, Discriminant,
+        InLayout, Layout, LayoutCache, LayoutInterner, LayoutRepr, TLLayoutInterner, UnionLayout,
+    },
+};
+use roc_target::{Architecture, OperatingSystem, TargetInfo};
+use roc_types::{
+    subs::{Content, FlatType, GetSubsSlice, Label, Subs, SubsSlice, UnionLabels, Variable},
+    types::{AliasKind, RecordField},
+};
+use std::convert::From;
+use std::fmt::Display;
+
+#[derive(Debug, PartialEq, Eq)]
+pub struct File {
+    pub name: String,
+    pub content: String,
+}
+
+#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord)]
+pub struct TypeId(usize);
+
+impl TypeId {
+    /// Used when making recursive pointers, which need to temporarily
+    /// have *some* TypeId value until we later in the process determine
+    /// their real TypeId and can go back and fix them up.
+    pub(crate) const PENDING: Self = Self(usize::MAX);
+
+    /// When adding, we check for overflow based on whether we've exceeded this.
+    const MAX: Self = Self(Self::PENDING.0 - 1);
+}
+
+// TODO: remove this and instead generate directly into roc_type::Types
+// Probably want to fix roc_std::RocDict and update roc_type::Types to use it first.
+#[derive(Debug, Clone)]
+pub struct Types {
+    // These are all indexed by TypeId
+    types: Vec,
+    sizes: Vec,
+    aligns: Vec,
+
+    entry_points: Vec<(String, TypeId)>,
+
+    // Needed to check for duplicates
+    types_by_name: FnvHashMap,
+
+    /// Dependencies - that is, which type depends on which other type.
+    /// This is important for declaration order in C; we need to output a
+    /// type declaration earlier in the file than where it gets referenced by another type.
+    deps: VecMap>,
+    target: TargetInfo,
+}
+
+impl Types {
+    const UNIT: TypeId = TypeId(0);
+
+    pub fn with_capacity(cap: usize, target_info: TargetInfo) -> Self {
+        let mut types = Vec::with_capacity(cap);
+        let mut sizes = Vec::with_capacity(cap);
+        let mut aligns = Vec::with_capacity(cap);
+
+        types.push(RocType::Unit);
+        sizes.push(1);
+        aligns.push(1);
+
+        Self {
+            target: target_info,
+            types,
+            sizes,
+            aligns,
+            types_by_name: FnvHashMap::with_capacity_and_hasher(10, Default::default()),
+            entry_points: Vec::new(),
+            deps: VecMap::with_capacity(cap),
+        }
+    }
+
+    #[allow(clippy::too_many_arguments)]
+    pub(crate) fn new_with_entry_points<'a>(
+        arena: &'a Bump,
+        subs: &'a Subs,
+        interns: &'a Interns,
+        glue_procs_by_layout: MutMap, &'a [String]>,
+        layout_cache: LayoutCache<'a>,
+        target: TargetInfo,
+        mut entry_points: MutMap,
+    ) -> Self {
+        let mut types = Self::with_capacity(entry_points.len(), target);
+        let mut env = Env::new(
+            arena,
+            subs,
+            interns,
+            layout_cache.interner,
+            glue_procs_by_layout,
+            target,
+        );
+
+        for (_symbol, var) in entry_points.clone() {
+            env.lambda_set_ids = env.find_lambda_sets(var);
+            let id = env.add_toplevel_type(var, &mut types);
+
+            let key = entry_points
+                .iter()
+                .find_map(|(k, v)| (*v == var).then_some((*k, id)));
+
+            if let Some((k, id)) = key {
+                let name = k.as_str(env.interns).to_string();
+                types.entry_points.push((name, id));
+                entry_points.remove(&k);
+            }
+        }
+
+        debug_assert!(entry_points.is_empty());
+
+        env.resolve_pending_recursive_types(&mut types);
+
+        types
+    }
+
+    pub fn entry_points(&self) -> &[(String, TypeId)] {
+        self.entry_points.as_slice()
+    }
+
+    pub fn is_equivalent(&self, a: &RocType, b: &RocType) -> bool {
+        self.is_equivalent_help(RocTypeOrPending::Type(a), RocTypeOrPending::Type(b))
+    }
+
+    fn is_equivalent_help(&self, a: RocTypeOrPending, b: RocTypeOrPending) -> bool {
+        use RocType::*;
+
+        let (a, b) = match (a, b) {
+            (RocTypeOrPending::Type(a), RocTypeOrPending::Type(b)) => (a, b),
+            (RocTypeOrPending::Pending, RocTypeOrPending::Pending) => return true,
+            _ => return false,
+        };
+
+        match (a, b) {
+            (Unsized, Unsized) => true,
+            (RocStr, RocStr) | (Bool, Bool) | (EmptyTagUnion, EmptyTagUnion) | (Unit, Unit) => true,
+            (RocResult(ok_a, err_a), RocResult(ok_b, err_b)) => {
+                self.is_equivalent_help(
+                    self.get_type_or_pending(*ok_a),
+                    self.get_type_or_pending(*ok_b),
+                ) && self.is_equivalent_help(
+                    self.get_type_or_pending(*err_a),
+                    self.get_type_or_pending(*err_b),
+                )
+            }
+            (Num(num_a), Num(num_b)) => num_a == num_b,
+            (RocList(elem_a), RocList(elem_b))
+            | (RocSet(elem_a), RocSet(elem_b))
+            | (RocBox(elem_a), RocBox(elem_b))
+            | (RecursivePointer(elem_a), RecursivePointer(elem_b)) => self.is_equivalent_help(
+                self.get_type_or_pending(*elem_a),
+                self.get_type_or_pending(*elem_b),
+            ),
+            (RocDict(key_a, val_a), RocDict(key_b, val_b)) => {
+                self.is_equivalent_help(
+                    self.get_type_or_pending(*key_a),
+                    self.get_type_or_pending(*key_b),
+                ) && self.is_equivalent_help(
+                    self.get_type_or_pending(*val_a),
+                    self.get_type_or_pending(*val_b),
+                )
+            }
+            (TagUnion(union_a), TagUnion(union_b)) => {
+                use RocTagUnion::*;
+
+                match (union_a, union_b) {
+                    (
+                        SingleTagStruct {
+                            name: _,
+                            tag_name: tag_name_a,
+                            payload: payload_a,
+                        },
+                        SingleTagStruct {
+                            name: _,
+                            tag_name: tag_name_b,
+                            payload: payload_b,
+                        },
+                    ) => tag_name_a == tag_name_b && payload_a == payload_b,
+                    (
+                        NonNullableUnwrapped {
+                            name: _,
+                            tag_name: tag_name_a,
+                            payload: payload_a,
+                        },
+                        NonNullableUnwrapped {
+                            name: _,
+                            tag_name: tag_name_b,
+                            payload: payload_b,
+                        },
+                    ) => {
+                        tag_name_a == tag_name_b
+                            && self.is_equivalent_help(
+                                self.get_type_or_pending(*payload_a),
+                                self.get_type_or_pending(*payload_b),
+                            )
+                    }
+                    (Enumeration { tags: tags_a, .. }, Enumeration { tags: tags_b, .. }) => {
+                        tags_a == tags_b
+                    }
+                    (
+                        NonRecursive {
+                            tags: tags_a,
+                            discriminant_size: disc_size_a,
+                            ..
+                        },
+                        NonRecursive {
+                            tags: tags_b,
+                            discriminant_size: disc_size_b,
+                            ..
+                        },
+                    )
+                    | (
+                        Recursive {
+                            tags: tags_a,
+                            discriminant_size: disc_size_a,
+                            ..
+                        },
+                        Recursive {
+                            tags: tags_b,
+                            discriminant_size: disc_size_b,
+                            ..
+                        },
+                    ) => {
+                        if disc_size_a == disc_size_b && tags_a.len() == tags_b.len() {
+                            // discriminant offset doesn't matter for equality,
+                            // since it's determined 100% by other fields
+                            tags_a.iter().zip(tags_b.iter()).all(
+                                |((name_a, opt_id_a), (name_b, opt_id_b))| {
+                                    name_a == name_b
+                                        && match (opt_id_a, opt_id_b) {
+                                            (Some(id_a), Some(id_b)) => self.is_equivalent_help(
+                                                self.get_type_or_pending(*id_a),
+                                                self.get_type_or_pending(*id_b),
+                                            ),
+                                            (None, None) => true,
+                                            (None, Some(_)) | (Some(_), None) => false,
+                                        }
+                                },
+                            )
+                        } else {
+                            false
+                        }
+                    }
+                    (
+                        NullableWrapped { tags: tags_a, .. },
+                        NullableWrapped { tags: tags_b, .. },
+                    ) => {
+                        if tags_a.len() != tags_b.len() {
+                            tags_a.iter().zip(tags_b.iter()).all(
+                                |((name_a, opt_id_a), (name_b, opt_id_b))| {
+                                    name_a == name_b
+                                        && match (opt_id_a, opt_id_b) {
+                                            (Some(id_a), Some(id_b)) => self.is_equivalent_help(
+                                                self.get_type_or_pending(*id_a),
+                                                self.get_type_or_pending(*id_b),
+                                            ),
+                                            (None, None) => true,
+                                            (None, Some(_)) | (Some(_), None) => false,
+                                        }
+                                },
+                            )
+                        } else {
+                            false
+                        }
+                    }
+                    (
+                        NullableUnwrapped {
+                            null_tag: null_tag_a,
+                            non_null_tag: non_null_tag_a,
+                            non_null_payload: non_null_payload_a,
+                            null_represents_first_tag: null_represents_first_tag_a,
+                            ..
+                        },
+                        NullableUnwrapped {
+                            null_tag: null_tag_b,
+                            non_null_tag: non_null_tag_b,
+                            non_null_payload: non_null_payload_b,
+                            null_represents_first_tag: null_represents_first_tag_b,
+                            ..
+                        },
+                    ) => {
+                        null_tag_a == null_tag_b
+                            && non_null_tag_a == non_null_tag_b
+                            && non_null_payload_a == non_null_payload_b
+                            && null_represents_first_tag_a == null_represents_first_tag_b
+                    }
+                    // These are all listed explicitly so that if we ever add a new variant,
+                    // we'll get an exhaustiveness error here.
+                    (NonNullableUnwrapped { .. }, _)
+                    | (_, NonNullableUnwrapped { .. })
+                    | (Enumeration { .. }, _)
+                    | (_, Enumeration { .. })
+                    | (NonRecursive { .. }, _)
+                    | (_, NonRecursive { .. })
+                    | (Recursive { .. }, _)
+                    | (_, Recursive { .. })
+                    | (SingleTagStruct { .. }, NullableWrapped { .. })
+                    | (NullableWrapped { .. }, SingleTagStruct { .. })
+                    | (NullableUnwrapped { .. }, _)
+                    | (_, NullableUnwrapped { .. }) => false,
+                }
+            }
+            (
+                TagUnionPayload {
+                    fields: RocStructFields::HasClosure { fields: fields_a },
+                    name: _,
+                },
+                TagUnionPayload {
+                    fields: RocStructFields::HasClosure { fields: fields_b },
+                    name: _,
+                },
+            )
+            | (
+                Struct {
+                    fields: RocStructFields::HasClosure { fields: fields_a },
+                    name: _,
+                },
+                Struct {
+                    fields: RocStructFields::HasClosure { fields: fields_b },
+                    name: _,
+                },
+            ) => {
+                if fields_a.len() == fields_b.len() {
+                    fields_a.iter().zip(fields_b.iter()).all(
+                        |((name_a, id_a, _), (name_b, id_b, _))| {
+                            name_a == name_b
+                                && self.is_equivalent_help(
+                                    self.get_type_or_pending(*id_a),
+                                    self.get_type_or_pending(*id_b),
+                                )
+                        },
+                    )
+                } else {
+                    false
+                }
+            }
+            (
+                TagUnionPayload {
+                    fields: RocStructFields::HasNoClosure { fields: fields_a },
+                    name: _,
+                },
+                TagUnionPayload {
+                    fields: RocStructFields::HasNoClosure { fields: fields_b },
+                    name: _,
+                },
+            )
+            | (
+                Struct {
+                    fields: RocStructFields::HasNoClosure { fields: fields_a },
+                    name: _,
+                },
+                Struct {
+                    fields: RocStructFields::HasNoClosure { fields: fields_b },
+                    name: _,
+                },
+            ) => {
+                if fields_a.len() == fields_b.len() {
+                    fields_a
+                        .iter()
+                        .zip(fields_b.iter())
+                        .all(|((name_a, id_a), (name_b, id_b))| {
+                            name_a == name_b
+                                && self.is_equivalent_help(
+                                    self.get_type_or_pending(*id_a),
+                                    self.get_type_or_pending(*id_b),
+                                )
+                        })
+                } else {
+                    false
+                }
+            }
+            (
+                TagUnionPayload {
+                    fields: RocStructFields::HasClosure { .. },
+                    name: _,
+                },
+                TagUnionPayload {
+                    fields: RocStructFields::HasNoClosure { .. },
+                    name: _,
+                },
+            )
+            | (
+                TagUnionPayload {
+                    fields: RocStructFields::HasNoClosure { .. },
+                    name: _,
+                },
+                TagUnionPayload {
+                    fields: RocStructFields::HasClosure { .. },
+                    name: _,
+                },
+            )
+            | (
+                Struct {
+                    fields: RocStructFields::HasNoClosure { .. },
+                    name: _,
+                },
+                Struct {
+                    fields: RocStructFields::HasClosure { .. },
+                    name: _,
+                },
+            )
+            | (
+                Struct {
+                    fields: RocStructFields::HasClosure { .. },
+                    name: _,
+                },
+                Struct {
+                    fields: RocStructFields::HasNoClosure { .. },
+                    name: _,
+                },
+            ) => false,
+            (
+                Function(RocFn {
+                    function_name: name_a,
+                    extern_name: extern_a,
+                    args: args_a,
+                    lambda_set: lambda_a,
+                    ret: ret_a,
+                    is_toplevel: is_toplevel_a,
+                }),
+                Function(RocFn {
+                    function_name: name_b,
+                    extern_name: extern_b,
+                    args: args_b,
+                    lambda_set: lambda_b,
+                    ret: ret_b,
+                    is_toplevel: is_toplevel_b,
+                }),
+            ) => {
+                // for functions, the name is actually important because two functions
+                // with the same type could have completely different implementations!
+                if name_a == name_b
+                    && extern_a == extern_b
+                    && is_toplevel_a == is_toplevel_b
+                    && args_a.len() == args_b.len()
+                    && self.is_equivalent_help(
+                        self.get_type_or_pending(*lambda_a),
+                        self.get_type_or_pending(*lambda_b),
+                    )
+                    && self.is_equivalent_help(
+                        self.get_type_or_pending(*ret_a),
+                        self.get_type_or_pending(*ret_b),
+                    )
+                {
+                    args_a.iter().zip(args_b.iter()).all(|(id_a, id_b)| {
+                        self.is_equivalent_help(
+                            self.get_type_or_pending(*id_a),
+                            self.get_type_or_pending(*id_b),
+                        )
+                    })
+                } else {
+                    false
+                }
+            }
+            // These are all listed explicitly so that if we ever add a new variant,
+            // we'll get an exhaustiveness error here.
+            (RocStr, _)
+            | (_, RocStr)
+            | (Bool, _)
+            | (_, Bool)
+            | (RocResult(_, _), _)
+            | (_, RocResult(_, _))
+            | (Num(_), _)
+            | (_, Num(_))
+            | (RocList(_), _)
+            | (_, RocList(_))
+            | (RocDict(_, _), _)
+            | (_, RocDict(_, _))
+            | (RocSet(_), _)
+            | (_, RocSet(_))
+            | (RocBox(_), _)
+            | (_, RocBox(_))
+            | (TagUnion(_), _)
+            | (_, TagUnion(_))
+            | (EmptyTagUnion, _)
+            | (_, EmptyTagUnion)
+            | (Struct { .. }, _)
+            | (_, Struct { .. })
+            | (TagUnionPayload { .. }, _)
+            | (_, TagUnionPayload { .. })
+            | (RecursivePointer(_), _)
+            | (_, RecursivePointer(_))
+            | (Function { .. }, _)
+            | (_, Function { .. })
+            | (Unsized, _)
+            | (_, Unsized) => false,
+        }
+    }
+
+    pub fn add_named<'a>(
+        &mut self,
+        interner: &TLLayoutInterner<'a>,
+        name: String,
+        typ: RocType,
+        layout: InLayout<'a>,
+    ) -> TypeId {
+        if let Some(existing_type_id) = self.types_by_name.get(&name) {
+            let existing_type = self.get_type(*existing_type_id);
+
+            if self.is_equivalent(existing_type, &typ) {
+                *existing_type_id
+            } else {
+                // TODO report this gracefully!
+                panic!(
+                    "Duplicate name detected - {name:?} could refer to either {existing_type:?} or {typ:?}"
+                );
+            }
+        } else {
+            let id = self.add_anonymous(interner, typ, layout);
+
+            self.types_by_name.insert(name, id);
+
+            id
+        }
+    }
+
+    pub fn add_anonymous<'a>(
+        &mut self,
+        interner: &TLLayoutInterner<'a>,
+        typ: RocType,
+        layout: InLayout<'a>,
+    ) -> TypeId {
+        for (id, existing_type) in self.types.iter().enumerate() {
+            if self.is_equivalent(&typ, existing_type) {
+                return TypeId(id);
+            }
+        }
+
+        debug_assert_eq!(self.types.len(), self.sizes.len());
+        debug_assert_eq!(self.types.len(), self.aligns.len());
+
+        let id = TypeId(self.types.len());
+
+        assert!(id.0 <= TypeId::MAX.0);
+
+        let size = interner.stack_size(layout);
+        let align = interner.alignment_bytes(layout);
+
+        self.types.push(typ);
+        self.sizes.push(size);
+        self.aligns.push(align);
+
+        id
+    }
+
+    pub fn depends(&mut self, id: TypeId, depends_on: TypeId) {
+        self.deps.get_or_insert(id, Vec::new).push(depends_on);
+    }
+
+    pub fn get_type(&self, id: TypeId) -> &RocType {
+        match self.types.get(id.0) {
+            Some(typ) => typ,
+            None => unreachable!("{:?}", id),
+        }
+    }
+
+    fn get_type_or_pending(&self, id: TypeId) -> RocTypeOrPending {
+        match self.types.get(id.0) {
+            Some(typ) => RocTypeOrPending::Type(typ),
+            None if id == TypeId::PENDING => RocTypeOrPending::Pending,
+            None => unreachable!("{:?}", id),
+        }
+    }
+
+    /// Contrast this with the size_ignoring_alignment method
+    pub fn size_rounded_to_alignment(&self, id: TypeId) -> u32 {
+        let size_ignoring_alignment = self.size_ignoring_alignment(id);
+        let alignment = self.align(id);
+
+        round_up_to_alignment(size_ignoring_alignment, alignment)
+    }
+
+    /// Contrast this with the size_rounded_to_alignment method
+    pub fn size_ignoring_alignment(&self, id: TypeId) -> u32 {
+        match self.sizes.get(id.0) {
+            Some(size) => *size,
+            None => unreachable!(),
+        }
+    }
+
+    pub fn align(&self, id: TypeId) -> u32 {
+        match self.aligns.get(id.0) {
+            Some(align) => *align,
+            None => unreachable!(),
+        }
+    }
+
+    pub fn replace(&mut self, id: TypeId, typ: RocType) {
+        debug_assert!(self.types.get(id.0).is_some());
+
+        self.types[id.0] = typ;
+    }
+
+    pub fn ids(&self) -> impl ExactSizeIterator {
+        (0..self.types.len()).map(TypeId)
+    }
+
+    pub fn sorted_ids(&self) -> Vec {
+        use roc_collections::{ReferenceMatrix, TopologicalSort};
+
+        let mut matrix = ReferenceMatrix::new(self.types.len());
+
+        for type_id in self.ids() {
+            for dep in self.deps.get(&type_id).iter().flat_map(|x| x.iter()) {
+                matrix.set_row_col(type_id.0, dep.0, true);
+            }
+        }
+
+        match matrix.topological_sort_into_groups() {
+            TopologicalSort::Groups { groups } => groups
+                .into_iter()
+                .flatten()
+                .rev()
+                .map(|n| TypeId(n as usize))
+                .collect(),
+            TopologicalSort::HasCycles {
+                groups: _,
+                nodes_in_cycle,
+            } => unreachable!("Cyclic type definitions: {:?}", nodes_in_cycle),
+        }
+    }
+
+    pub fn target(&self) -> TargetInfo {
+        self.target
+    }
+}
+
+impl From<&Types> for roc_type::Types {
+    fn from(types: &Types) -> Self {
+        let deps = types
+            .deps
+            .iter()
+            .map(|(k, v)| roc_type::Tuple2::T(k.0 as _, v.iter().map(|x| x.0 as _).collect()))
+            .collect();
+        let types_by_name = types
+            .types_by_name
+            .iter()
+            .map(|(k, v)| roc_type::Tuple1::T(k.as_str().into(), v.0 as _))
+            .collect();
+
+        let entrypoints = types
+            .entry_points()
+            .iter()
+            .map(|(k, v)| roc_type::Tuple1::T(k.as_str().into(), v.0 as _))
+            .collect();
+
+        roc_type::Types {
+            aligns: types.aligns.as_slice().into(),
+            deps,
+            entrypoints,
+            sizes: types.sizes.as_slice().into(),
+            types: types.types.iter().map(roc_type::RocType::from).collect(),
+            typesByName: types_by_name,
+            target: types.target.into(),
+        }
+    }
+}
+
+impl From<&RocType> for roc_type::RocType {
+    fn from(rc: &RocType) -> Self {
+        match rc {
+            RocType::RocStr => roc_type::RocType::RocStr,
+            RocType::Bool => roc_type::RocType::Bool,
+            RocType::RocResult(ok, err) => roc_type::RocType::RocResult(ok.0 as _, err.0 as _),
+            RocType::Num(num_type) => roc_type::RocType::Num(num_type.into()),
+            RocType::RocList(elem) => roc_type::RocType::RocList(elem.0 as _),
+            RocType::RocDict(k, v) => roc_type::RocType::RocDict(k.0 as _, v.0 as _),
+            RocType::RocSet(elem) => roc_type::RocType::RocSet(elem.0 as _),
+            RocType::RocBox(elem) => roc_type::RocType::RocBox(elem.0 as _),
+            RocType::TagUnion(union) => roc_type::RocType::TagUnion(union.into()),
+            RocType::EmptyTagUnion => roc_type::RocType::EmptyTagUnion,
+            RocType::Struct { name, fields } => roc_type::RocType::Struct(roc_type::R1 {
+                fields: fields.into(),
+                name: name.as_str().into(),
+            }),
+            RocType::TagUnionPayload { name, fields } => {
+                roc_type::RocType::TagUnionPayload(roc_type::R1 {
+                    fields: fields.into(),
+                    name: name.as_str().into(),
+                })
+            }
+            RocType::RecursivePointer(elem) => roc_type::RocType::RecursivePointer(elem.0 as _),
+            RocType::Function(RocFn {
+                function_name,
+                extern_name,
+                args,
+                lambda_set,
+                ret,
+                is_toplevel,
+            }) => roc_type::RocType::Function(roc_type::RocFn {
+                args: args.iter().map(|arg| arg.0 as _).collect(),
+                functionName: function_name.as_str().into(),
+                externName: extern_name.as_str().into(),
+                ret: ret.0 as _,
+                lambdaSet: lambda_set.0 as _,
+                isToplevel: *is_toplevel,
+            }),
+            RocType::Unit => roc_type::RocType::Unit,
+            RocType::Unsized => roc_type::RocType::Unsized,
+        }
+    }
+}
+
+impl From<&RocNum> for roc_type::RocNum {
+    fn from(rn: &RocNum) -> Self {
+        match rn {
+            RocNum::I8 => roc_type::RocNum::I8,
+            RocNum::U8 => roc_type::RocNum::U8,
+            RocNum::I16 => roc_type::RocNum::I16,
+            RocNum::U16 => roc_type::RocNum::U16,
+            RocNum::I32 => roc_type::RocNum::I32,
+            RocNum::U32 => roc_type::RocNum::U32,
+            RocNum::I64 => roc_type::RocNum::I64,
+            RocNum::U64 => roc_type::RocNum::U64,
+            RocNum::I128 => roc_type::RocNum::I128,
+            RocNum::U128 => roc_type::RocNum::U128,
+            RocNum::F32 => roc_type::RocNum::F32,
+            RocNum::F64 => roc_type::RocNum::F64,
+            RocNum::Dec => roc_type::RocNum::Dec,
+        }
+    }
+}
+
+impl From<&RocTagUnion> for roc_type::RocTagUnion {
+    fn from(rtu: &RocTagUnion) -> Self {
+        match rtu {
+            RocTagUnion::Enumeration { name, tags, size } => {
+                roc_type::RocTagUnion::Enumeration(roc_type::R5 {
+                    name: name.as_str().into(),
+                    tags: tags.iter().map(|name| name.as_str().into()).collect(),
+                    size: *size,
+                })
+            }
+            RocTagUnion::NonRecursive {
+                name,
+                tags,
+                discriminant_size,
+                discriminant_offset,
+            } => roc_type::RocTagUnion::NonRecursive(roc_type::R7 {
+                name: name.as_str().into(),
+                tags: tags
+                    .iter()
+                    .map(|(name, payload)| roc_type::R8 {
+                        name: name.as_str().into(),
+                        payload: payload.into(),
+                    })
+                    .collect(),
+                discriminantSize: *discriminant_size,
+                discriminantOffset: *discriminant_offset,
+            }),
+            RocTagUnion::Recursive {
+                name,
+                tags,
+                discriminant_size,
+                discriminant_offset,
+            } => roc_type::RocTagUnion::Recursive(roc_type::R7 {
+                name: name.as_str().into(),
+                tags: tags
+                    .iter()
+                    .map(|(name, payload)| roc_type::R8 {
+                        name: name.as_str().into(),
+                        payload: payload.into(),
+                    })
+                    .collect(),
+                discriminantSize: *discriminant_size,
+                discriminantOffset: *discriminant_offset,
+            }),
+            RocTagUnion::NonNullableUnwrapped {
+                name,
+                tag_name,
+                payload,
+            } => roc_type::RocTagUnion::NonNullableUnwrapped(roc_type::R6 {
+                name: name.as_str().into(),
+                tagName: tag_name.as_str().into(),
+                payload: payload.0 as _,
+            }),
+            RocTagUnion::SingleTagStruct {
+                name,
+                tag_name,
+                payload,
+            } => roc_type::RocTagUnion::SingleTagStruct(roc_type::R14 {
+                name: name.as_str().into(),
+                tagName: tag_name.as_str().into(),
+                payload: payload.into(),
+            }),
+            RocTagUnion::NullableWrapped {
+                name,
+                index_of_null_tag,
+                tags,
+                discriminant_size,
+                discriminant_offset,
+            } => roc_type::RocTagUnion::NullableWrapped(roc_type::R10 {
+                name: name.as_str().into(),
+                indexOfNullTag: *index_of_null_tag,
+                tags: tags
+                    .iter()
+                    .map(|(name, payload)| roc_type::R8 {
+                        name: name.as_str().into(),
+                        payload: payload.into(),
+                    })
+                    .collect(),
+                discriminantSize: *discriminant_size,
+                discriminantOffset: *discriminant_offset,
+            }),
+            RocTagUnion::NullableUnwrapped {
+                name,
+                null_tag,
+                non_null_tag,
+                non_null_payload,
+                null_represents_first_tag,
+            } => roc_type::RocTagUnion::NullableUnwrapped(roc_type::R9 {
+                name: name.as_str().into(),
+                nonNullPayload: non_null_payload.0 as _,
+                nonNullTag: non_null_tag.as_str().into(),
+                nullTag: null_tag.as_str().into(),
+                whichTagIsNull: if *null_represents_first_tag {
+                    roc_type::U2::FirstTagIsNull
+                } else {
+                    roc_type::U2::SecondTagIsNull
+                },
+            }),
+        }
+    }
+}
+
+impl From<&RocStructFields> for roc_type::RocStructFields {
+    fn from(struct_fields: &RocStructFields) -> Self {
+        match struct_fields {
+            RocStructFields::HasNoClosure { fields } => roc_type::RocStructFields::HasNoClosure(
+                fields
+                    .iter()
+                    .map(|(name, id)| roc_type::R4 {
+                        name: name.as_str().into(),
+                        id: id.0 as _,
+                    })
+                    .collect(),
+            ),
+            RocStructFields::HasClosure { fields } => roc_type::RocStructFields::HasClosure(
+                fields
+                    .iter()
+                    .map(|(name, id, accessors)| roc_type::R2 {
+                        name: name.as_str().into(),
+                        id: id.0 as _,
+                        accessors: roc_type::R3 {
+                            getter: accessors.getter.as_str().into(),
+                        },
+                    })
+                    .collect(),
+            ),
+        }
+    }
+}
+
+impl From<&RocSingleTagPayload> for roc_type::RocSingleTagPayload {
+    fn from(struct_fields: &RocSingleTagPayload) -> Self {
+        match struct_fields {
+            RocSingleTagPayload::HasNoClosure { payload_fields } => {
+                roc_type::RocSingleTagPayload::HasNoClosure(
+                    payload_fields
+                        .iter()
+                        .map(|id| roc_type::R16 { id: id.0 as _ })
+                        .collect(),
+                )
+            }
+            RocSingleTagPayload::HasClosure { payload_getters } => {
+                roc_type::RocSingleTagPayload::HasClosure(
+                    payload_getters
+                        .iter()
+                        .map(|(id, name)| roc_type::R4 {
+                            id: id.0 as _,
+                            name: name.as_str().into(),
+                        })
+                        .collect(),
+                )
+            }
+        }
+    }
+}
+
+impl From<&Option> for roc_type::U1 {
+    fn from(opt: &Option) -> Self {
+        match opt {
+            Some(x) => roc_type::U1::Some(x.0 as _),
+            None => roc_type::U1::None,
+        }
+    }
+}
+
+impl From for roc_type::Target {
+    fn from(target: TargetInfo) -> Self {
+        roc_type::Target {
+            architecture: target.architecture.into(),
+            operatingSystem: target.operating_system.into(),
+        }
+    }
+}
+
+impl From for roc_type::Architecture {
+    fn from(arch: Architecture) -> Self {
+        match arch {
+            Architecture::Aarch32 => roc_type::Architecture::Aarch32,
+            Architecture::Aarch64 => roc_type::Architecture::Aarch64,
+            Architecture::Wasm32 => roc_type::Architecture::Wasm32,
+            Architecture::X86_32 => roc_type::Architecture::X86x32,
+            Architecture::X86_64 => roc_type::Architecture::X86x64,
+        }
+    }
+}
+
+impl From for roc_type::OperatingSystem {
+    fn from(os: OperatingSystem) -> Self {
+        match os {
+            OperatingSystem::Windows => roc_type::OperatingSystem::Windows,
+            OperatingSystem::Unix => roc_type::OperatingSystem::Unix,
+            OperatingSystem::Wasi => roc_type::OperatingSystem::Wasi,
+        }
+    }
+}
+
+enum RocTypeOrPending<'a> {
+    Type(&'a RocType),
+    /// A pending recursive pointer
+    Pending,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+pub struct Accessors {
+    // The name of the extern
+    pub getter: String,
+    // TODO setter
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+pub enum RocStructFields {
+    HasNoClosure {
+        fields: Vec<(String, TypeId)>,
+    },
+    HasClosure {
+        fields: Vec<(String, TypeId, Accessors)>,
+        // no struct_size because it's not knowable if there's a closure; must call a size getter!
+    },
+}
+
+impl RocStructFields {
+    pub fn is_empty(&self) -> bool {
+        self.len() == 0
+    }
+
+    pub fn len(&self) -> usize {
+        match self {
+            RocStructFields::HasNoClosure { fields } => fields.len(),
+            RocStructFields::HasClosure { fields } => fields.len(),
+        }
+    }
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+pub struct RocFn {
+    pub function_name: String,
+    pub extern_name: String,
+    pub is_toplevel: bool,
+    pub args: Vec,
+    pub lambda_set: TypeId,
+    pub ret: TypeId,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+pub enum RocType {
+    RocStr,
+    Bool,
+    RocResult(TypeId, TypeId),
+    Num(RocNum),
+    RocList(TypeId),
+    RocDict(TypeId, TypeId),
+    RocSet(TypeId),
+    RocBox(TypeId),
+    TagUnion(RocTagUnion),
+    EmptyTagUnion,
+    Struct {
+        name: String,
+        fields: RocStructFields,
+    },
+    TagUnionPayload {
+        name: String,
+        fields: RocStructFields,
+    },
+    /// A recursive pointer, e.g. in StrConsList : [Nil, Cons Str StrConsList],
+    /// this would be the field of Cons containing the (recursive) StrConsList type,
+    /// and the TypeId is the TypeId of StrConsList itself.
+    RecursivePointer(TypeId),
+    Function(RocFn),
+    /// A zero-sized type, such as an empty record or a single-tag union with no payload
+    Unit,
+    /// A type that has a size that is not statically known
+    Unsized,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Hash, Copy)]
+pub enum RocNum {
+    I8,
+    U8,
+    I16,
+    U16,
+    I32,
+    U32,
+    I64,
+    U64,
+    I128,
+    U128,
+    F32,
+    F64,
+    Dec,
+}
+
+impl RocNum {
+    /// These sizes don't vary by target.
+    pub fn size(&self) -> u32 {
+        use core::mem::size_of;
+
+        let answer = match self {
+            RocNum::I8 => size_of::(),
+            RocNum::U8 => size_of::(),
+            RocNum::I16 => size_of::(),
+            RocNum::U16 => size_of::(),
+            RocNum::I32 => size_of::(),
+            RocNum::U32 => size_of::(),
+            RocNum::I64 => size_of::(),
+            RocNum::U64 => size_of::(),
+            RocNum::I128 => size_of::(),
+            RocNum::U128 => size_of::(),
+            RocNum::F32 => size_of::(),
+            RocNum::F64 => size_of::(),
+            RocNum::Dec => size_of::(),
+        };
+
+        answer as u32
+    }
+}
+
+impl From for RocNum {
+    fn from(width: IntWidth) -> Self {
+        match width {
+            IntWidth::U8 => RocNum::U8,
+            IntWidth::U16 => RocNum::U16,
+            IntWidth::U32 => RocNum::U32,
+            IntWidth::U64 => RocNum::U64,
+            IntWidth::U128 => RocNum::U128,
+            IntWidth::I8 => RocNum::I8,
+            IntWidth::I16 => RocNum::I16,
+            IntWidth::I32 => RocNum::I32,
+            IntWidth::I64 => RocNum::I64,
+            IntWidth::I128 => RocNum::I128,
+        }
+    }
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+pub enum RocSingleTagPayload {
+    /// If at least one payload field contains a closure, we have to provide
+    /// field getters and setters because the size and order of those fields can vary based on the
+    /// application's implementation, so those sizes and order are not knowable at host build time.
+    HasClosure {
+        payload_getters: Vec<(TypeId, String)>,
+    },
+    HasNoClosure {
+        payload_fields: Vec,
+    },
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+pub enum RocTagUnion {
+    Enumeration {
+        name: String,
+        tags: Vec,
+        size: u32,
+    },
+    /// A non-recursive tag union
+    /// e.g. `Result a e : [Ok a, Err e]`
+    NonRecursive {
+        name: String,
+        tags: Vec<(String, Option)>,
+        discriminant_offset: u32,
+        discriminant_size: u32,
+    },
+    /// A recursive tag union (general case)
+    /// e.g. `Expr : [Sym Str, Add Expr Expr]`
+    Recursive {
+        name: String,
+        tags: Vec<(String, Option)>,
+        discriminant_offset: u32,
+        discriminant_size: u32,
+    },
+    /// Optimization: No need to store a tag ID (the payload is "unwrapped")
+    /// e.g. `RoseTree a : [Tree a (List (RoseTree a))]`
+    NonNullableUnwrapped {
+        name: String,
+        tag_name: String,
+        payload: TypeId, // These always have a payload.
+    },
+    /// Optimization: No need to store a tag ID (the payload is "unwrapped")
+    /// e.g. `[Foo Str Bool]`
+    /// Just like a normal struct, if one of the payload fields is a closure, we have to provide
+    /// field getters and setters because the size and order of those fields can vary based on the
+    /// application's implementation, so those sizes and order are not knowable at host build time.
+    SingleTagStruct {
+        name: String,
+        tag_name: String,
+        payload: RocSingleTagPayload,
+    },
+    /// A recursive tag union that has an empty variant
+    /// Optimization: Represent the empty variant as null pointer => no memory usage & fast comparison
+    /// It has more than one other variant, so they need tag IDs (payloads are "wrapped")
+    /// e.g. `FingerTree a : [Empty, Single a, More (Some a) (FingerTree (Tuple a)) (Some a)]`
+    /// see also: https://youtu.be/ip92VMpf_-A?t=164
+    NullableWrapped {
+        name: String,
+        /// Which of the tags in .tags is the null pointer.
+        /// Note that this index is *not necessarily* the same as the offset of that tag
+        /// at runtime, which can move around if any of the payloads contain closures!
+        index_of_null_tag: u16,
+        tags: Vec<(String, Option)>,
+        discriminant_size: u32,
+        discriminant_offset: u32,
+    },
+    /// A recursive tag union with only two variants, where one is empty.
+    /// Optimizations: Use null for the empty variant AND don't store a tag ID for the other variant.
+    /// e.g. `ConsList a : [Nil, Cons a (ConsList a)]`
+    NullableUnwrapped {
+        name: String,
+        /// e.g. Nil in `StrConsList : [Nil, Cons Str (ConsList Str)]`
+        null_tag: String,
+        /// e.g. Cons in `StrConsList : [Nil, Cons Str (ConsList Str)]`
+        non_null_tag: String,
+        /// There must be a payload associated with the non-null tag.
+        /// Otherwise, this would have been an Enumeration!
+        non_null_payload: TypeId,
+        /// True iff the first tag (alphabetically) is represented by null.
+        /// If this is false, it means the second tag is represented by null instead.
+        null_represents_first_tag: bool,
+    },
+}
+
+struct Env<'a> {
+    arena: &'a Bump,
+    subs: &'a Subs,
+    layout_cache: LayoutCache<'a>,
+    glue_procs_by_layout: MutMap, &'a [String]>,
+    lambda_set_ids: MutMap,
+    interns: &'a Interns,
+    struct_names: Structs,
+    enum_names: Enums,
+    pending_recursive_types: VecMap,
+    known_recursive_types: VecMap,
+}
+
+impl<'a> Env<'a> {
+    fn new(
+        arena: &'a Bump,
+        subs: &'a Subs,
+        interns: &'a Interns,
+        layout_interner: TLLayoutInterner<'a>,
+        glue_procs_by_layout: MutMap, &'a [String]>,
+        target: TargetInfo,
+    ) -> Self {
+        Env {
+            arena,
+            subs,
+            interns,
+            struct_names: Default::default(),
+            enum_names: Default::default(),
+            pending_recursive_types: Default::default(),
+            known_recursive_types: Default::default(),
+            glue_procs_by_layout,
+            lambda_set_ids: Default::default(),
+            layout_cache: LayoutCache::new(layout_interner, target),
+        }
+    }
+
+    fn resolve_pending_recursive_types(&mut self, types: &mut Types) {
+        // TODO if VecMap gets a drain() method, use that instead of doing take() and into_iter
+        let pending = core::mem::take(&mut self.pending_recursive_types);
+
+        for (type_id, root_var) in pending.into_iter() {
+            let actual_type_id = *self
+                .known_recursive_types
+                .get(&root_var)
+                .unwrap_or_else(|| {
+                    unreachable!(
+                        "There was no known recursive TypeId for the pending recursive type {:?}",
+                        root_var
+                    );
+                });
+
+            debug_assert!(
+                matches!(types.get_type(type_id), RocType::RecursivePointer(TypeId::PENDING)),
+                "The TypeId {type_id:?} was registered as a pending recursive pointer, but was not stored in Types as one."
+            );
+
+            // size and alignment shouldn't change; this is still
+            // a RecursivePointer, it's just pointing to something else.
+            types.replace(type_id, RocType::RecursivePointer(actual_type_id));
+        }
+    }
+
+    fn find_lambda_sets(&self, root: Variable) -> MutMap {
+        roc_mono::ir::find_lambda_sets(self.arena, self.subs, root)
+    }
+
+    fn add_toplevel_type(&mut self, var: Variable, types: &mut Types) -> TypeId {
+        roc_tracing::debug!(content=?roc_types::subs::SubsFmtContent(self.subs.get_content_without_compacting(var), self.subs), "adding toplevel type");
+
+        let layout = self
+            .layout_cache
+            .from_var(self.arena, var, self.subs)
+            .expect("Something weird ended up in the content");
+
+        match self.subs.get_content_without_compacting(var) {
+            Content::Structure(FlatType::Func(args, closure_var, ret_var)) => {
+                // this is a toplevel type, so the closure must be empty
+                let is_toplevel = true;
+                add_function_type(
+                    self,
+                    layout,
+                    types,
+                    args,
+                    *closure_var,
+                    *ret_var,
+                    is_toplevel,
+                )
+            }
+            _ => add_type_help(self, layout, var, None, types),
+        }
+    }
+}
+
+fn add_function_type<'a>(
+    env: &mut Env<'a>,
+    layout: InLayout<'a>,
+    types: &mut Types,
+    args: &SubsSlice,
+    closure_var: Variable,
+    ret_var: Variable,
+    is_toplevel: bool,
+) -> TypeId {
+    let args = env.subs.get_subs_slice(*args);
+    let mut arg_type_ids = Vec::with_capacity(args.len());
+
+    let name = format!("RocFunction_{closure_var:?}");
+
+    let extern_name = match env.lambda_set_ids.get(&closure_var) {
+        Some(id) => format!("roc__mainForHost_{}_caller", id.0),
+        None => {
+            debug_assert!(is_toplevel);
+            String::from("this_extern_should_not_be_used_this_is_a_bug")
+        }
+    };
+
+    for arg_var in args {
+        let arg_layout = env
+            .layout_cache
+            .from_var(env.arena, *arg_var, env.subs)
+            .expect("Something weird ended up in the content");
+
+        arg_type_ids.push(add_type_help(env, arg_layout, *arg_var, None, types));
+    }
+
+    let lambda_set_type_id = if is_toplevel {
+        Types::UNIT
+    } else {
+        let lambda_set_layout = env
+            .layout_cache
+            .from_var(env.arena, closure_var, env.subs)
+            .expect("Something weird ended up in the content");
+
+        // TODO this treats any lambda set as unsized. We should be able to figure out whether a
+        // lambda set is unsized in practice, and use the runtime representation otherwise.
+        add_type_help(env, lambda_set_layout, closure_var, None, types)
+    };
+
+    let ret_type_id = {
+        let ret_layout = env
+            .layout_cache
+            .from_var(env.arena, ret_var, env.subs)
+            .expect("Something weird ended up in the content");
+
+        add_type_help(env, ret_layout, ret_var, None, types)
+    };
+
+    let fn_type_id = add_function(env, name, types, layout, |name| {
+        RocType::Function(RocFn {
+            function_name: name,
+            extern_name,
+            args: arg_type_ids.clone(),
+            lambda_set: lambda_set_type_id,
+            ret: ret_type_id,
+            is_toplevel,
+        })
+    });
+
+    types.depends(fn_type_id, ret_type_id);
+
+    for arg_type_id in arg_type_ids {
+        types.depends(fn_type_id, arg_type_id);
+    }
+
+    fn_type_id
+}
+
+fn add_type_help<'a>(
+    env: &mut Env<'a>,
+    layout: InLayout<'a>,
+    var: Variable,
+    opt_name: Option,
+    types: &mut Types,
+) -> TypeId {
+    let subs = env.subs;
+
+    match subs.get_content_without_compacting(var) {
+        Content::FlexVar(_)
+        | Content::RigidVar(_)
+        | Content::FlexAbleVar(_, _)
+        | Content::RigidAbleVar(_, _) => {
+            todo!("TODO give a nice error message for a non-concrete type being passed to the host")
+        }
+        Content::Structure(FlatType::Tuple(..)) => {
+            todo!();
+        }
+        Content::Structure(FlatType::Record(fields, ext)) => {
+            let it = fields
+                .unsorted_iterator(subs, *ext)
+                .expect("something weird in content")
+                .flat_map(|(label, field)| {
+                    match field {
+                        RecordField::Required(field_var)
+                        | RecordField::Demanded(field_var)
+                        | RecordField::RigidRequired(field_var) => {
+                            Some((label.to_string(), field_var))
+                        }
+                        RecordField::Optional(_) | RecordField::RigidOptional(_) => {
+                            // drop optional fields
+                            None
+                        }
+                    }
+                });
+
+            let name = match opt_name {
+                Some(sym) => sym.as_str(env.interns).to_string(),
+                None => env.struct_names.get_name(var),
+            };
+
+            add_struct(env, name, it, types, layout, |name, fields| {
+                RocType::Struct { name, fields }
+            })
+        }
+        Content::Structure(FlatType::TagUnion(tags, ext_var)) => {
+            debug_assert!(ext_var_is_empty_tag_union(subs, *ext_var));
+
+            add_tag_union(env, opt_name, tags, var, types, layout, None)
+        }
+        Content::Structure(FlatType::RecursiveTagUnion(rec_var, tags, ext_var)) => {
+            debug_assert!(ext_var_is_empty_tag_union(subs, *ext_var));
+
+            let rec_root = subs.get_root_key_without_compacting(*rec_var);
+
+            add_tag_union(env, opt_name, tags, var, types, layout, Some(rec_root))
+        }
+        Content::Structure(FlatType::Apply(symbol, _)) => match env.layout_cache.get_repr(layout) {
+            LayoutRepr::Builtin(builtin) => {
+                add_builtin_type(env, builtin, var, opt_name, types, layout)
+            }
+            _ => {
+                if symbol.is_builtin() {
+                    todo!(
+                        "Handle Apply for builtin symbol {:?} and layout {:?}",
+                        symbol,
+                        layout
+                    )
+                } else {
+                    todo!(
+                        "Handle non-builtin Apply for symbol {:?} and layout {:?}",
+                        symbol,
+                        layout
+                    )
+                }
+            }
+        },
+        Content::Structure(FlatType::Func(args, closure_var, ret_var)) => {
+            let is_toplevel = false; // or in any case, we cannot assume that we are
+
+            add_function_type(
+                env,
+                layout,
+                types,
+                args,
+                *closure_var,
+                *ret_var,
+                is_toplevel,
+            )
+        }
+        Content::Structure(FlatType::FunctionOrTagUnion(_, _, _)) => {
+            todo!()
+        }
+        Content::Structure(FlatType::EmptyRecord) => {
+            types.add_anonymous(&env.layout_cache.interner, RocType::Unit, layout)
+        }
+        Content::Structure(FlatType::EmptyTuple) => {
+            types.add_anonymous(&env.layout_cache.interner, RocType::Unit, layout)
+        }
+        Content::Structure(FlatType::EmptyTagUnion) => {
+            types.add_anonymous(&env.layout_cache.interner, RocType::EmptyTagUnion, layout)
+        }
+        Content::Alias(name, alias_vars, real_var, _) => {
+            if name.is_builtin() {
+                match env.layout_cache.get_repr(layout) {
+                    LayoutRepr::Builtin(builtin) => {
+                        add_builtin_type(env, builtin, var, opt_name, types, layout)
+                    }
+                    LayoutRepr::Union(union_layout) if *name == Symbol::BOOL_BOOL => {
+                        if cfg!(debug_assertions) {
+                            match union_layout {
+                                UnionLayout::NonRecursive(tag_layouts) => {
+                                    // Bool should always have exactly two tags: True and False
+                                    debug_assert_eq!(tag_layouts.len(), 2);
+
+                                    // Both tags should have no payload
+                                    debug_assert_eq!(tag_layouts[0].len(), 0);
+                                    debug_assert_eq!(tag_layouts[1].len(), 0);
+                                }
+                                _ => debug_assert!(false),
+                            }
+                        }
+
+                        types.add_anonymous(&env.layout_cache.interner, RocType::Bool, layout)
+                    }
+                    LayoutRepr::Union(union_layout) if *name == Symbol::RESULT_RESULT => {
+                        match union_layout {
+                            UnionLayout::NonRecursive(tags) => {
+                                // Result should always have exactly two tags: Ok and Err
+                                debug_assert_eq!(tags.len(), 2);
+
+                                let type_vars =
+                                    env.subs.get_subs_slice(alias_vars.type_variables());
+
+                                let ok_var = type_vars[0];
+                                let ok_layout =
+                                    env.layout_cache.from_var(env.arena, ok_var, subs).unwrap();
+                                let ok_id = add_type_help(env, ok_layout, ok_var, None, types);
+
+                                let err_var = type_vars[1];
+                                let err_layout =
+                                    env.layout_cache.from_var(env.arena, err_var, subs).unwrap();
+                                let err_id = add_type_help(env, err_layout, err_var, None, types);
+
+                                let type_id = types.add_anonymous(
+                                    &env.layout_cache.interner,
+                                    RocType::RocResult(ok_id, err_id),
+                                    layout,
+                                );
+
+                                types.depends(type_id, ok_id);
+                                types.depends(type_id, err_id);
+
+                                type_id
+                            }
+                            UnionLayout::Recursive(_)
+                            | UnionLayout::NonNullableUnwrapped(_)
+                            | UnionLayout::NullableWrapped { .. }
+                            | UnionLayout::NullableUnwrapped { .. } => {
+                                unreachable!();
+                            }
+                        }
+                    }
+                    LayoutRepr::Struct { .. } if *name == Symbol::RESULT_RESULT => {
+                        // can happen if one or both of a and b in `Result.Result a b` are the
+                        // empty tag union `[]`
+                        add_type_help(env, layout, *real_var, opt_name, types)
+                    }
+                    LayoutRepr::Struct { .. } if *name == Symbol::DICT_DICT => {
+                        let type_vars = env.subs.get_subs_slice(alias_vars.type_variables());
+
+                        let key_var = type_vars[0];
+                        let key_layout =
+                            env.layout_cache.from_var(env.arena, key_var, subs).unwrap();
+                        let key_id = add_type_help(env, key_layout, key_var, None, types);
+
+                        let value_var = type_vars[1];
+                        let value_layout = env
+                            .layout_cache
+                            .from_var(env.arena, value_var, subs)
+                            .unwrap();
+                        let value_id = add_type_help(env, value_layout, value_var, None, types);
+
+                        let type_id = types.add_anonymous(
+                            &env.layout_cache.interner,
+                            RocType::RocDict(key_id, value_id),
+                            layout,
+                        );
+
+                        types.depends(type_id, key_id);
+                        types.depends(type_id, value_id);
+
+                        type_id
+                    }
+                    LayoutRepr::Struct { .. } if *name == Symbol::SET_SET => {
+                        let type_vars = env.subs.get_subs_slice(alias_vars.type_variables());
+
+                        let key_var = type_vars[0];
+                        let key_layout =
+                            env.layout_cache.from_var(env.arena, key_var, subs).unwrap();
+                        let key_id = add_type_help(env, key_layout, key_var, None, types);
+
+                        let type_id = types.add_anonymous(
+                            &env.layout_cache.interner,
+                            RocType::RocSet(key_id),
+                            layout,
+                        );
+
+                        types.depends(type_id, key_id);
+
+                        type_id
+                    }
+                    _ => {
+                        unreachable!()
+                    }
+                }
+            } else {
+                // If this was a non-builtin type alias, we can use that alias name
+                // in the generated bindings.
+                add_type_help(env, layout, *real_var, Some(*name), types)
+            }
+        }
+        Content::RangedNumber(_) => todo!(),
+        Content::Error => todo!(),
+        Content::RecursionVar { structure, .. } => {
+            let type_id = types.add_anonymous(
+                &env.layout_cache.interner,
+                RocType::RecursivePointer(TypeId::PENDING),
+                layout,
+            );
+
+            // These should be different Variables, but the same layout!
+            debug_assert_eq!(
+                layout,
+                env.layout_cache
+                    .from_var(env.arena, *structure, subs)
+                    .unwrap()
+            );
+
+            let root_var = subs.get_root_key_without_compacting(var);
+
+            env.pending_recursive_types.insert(type_id, root_var);
+
+            type_id
+        }
+        Content::ErasedLambda => todo_lambda_erasure!(),
+        Content::LambdaSet(lambda_set) => {
+            let tags = lambda_set.solved;
+
+            if tags.is_empty() {
+                // this function does not capture anything. Represent that at runtime as a unit value
+                types.add_anonymous(&env.layout_cache.interner, RocType::Unsized, layout)
+            } else {
+                add_tag_union(env, opt_name, &tags, var, types, layout, None)
+            }
+        }
+    }
+}
+
+fn add_builtin_type<'a>(
+    env: &mut Env<'a>,
+    builtin: Builtin<'a>,
+    var: Variable,
+    opt_name: Option,
+    types: &mut Types,
+    layout: InLayout<'a>,
+) -> TypeId {
+    use Content::*;
+    use FlatType::*;
+
+    let builtin_type = env.subs.get_content_without_compacting(var);
+
+    match (builtin, builtin_type) {
+        (Builtin::Int(width), _) => match width {
+            U8 => types.add_anonymous(&env.layout_cache.interner, RocType::Num(RocNum::U8), layout),
+            U16 => types.add_anonymous(
+                &env.layout_cache.interner,
+                RocType::Num(RocNum::U16),
+                layout,
+            ),
+            U32 => types.add_anonymous(
+                &env.layout_cache.interner,
+                RocType::Num(RocNum::U32),
+                layout,
+            ),
+            U64 => types.add_anonymous(
+                &env.layout_cache.interner,
+                RocType::Num(RocNum::U64),
+                layout,
+            ),
+            U128 => types.add_anonymous(
+                &env.layout_cache.interner,
+                RocType::Num(RocNum::U128),
+                layout,
+            ),
+            I8 => types.add_anonymous(&env.layout_cache.interner, RocType::Num(RocNum::I8), layout),
+            I16 => types.add_anonymous(
+                &env.layout_cache.interner,
+                RocType::Num(RocNum::I16),
+                layout,
+            ),
+            I32 => types.add_anonymous(
+                &env.layout_cache.interner,
+                RocType::Num(RocNum::I32),
+                layout,
+            ),
+            I64 => types.add_anonymous(
+                &env.layout_cache.interner,
+                RocType::Num(RocNum::I64),
+                layout,
+            ),
+            I128 => types.add_anonymous(
+                &env.layout_cache.interner,
+                RocType::Num(RocNum::I128),
+                layout,
+            ),
+        },
+        (Builtin::Float(width), _) => match width {
+            F32 => types.add_anonymous(
+                &env.layout_cache.interner,
+                RocType::Num(RocNum::F32),
+                layout,
+            ),
+            F64 => types.add_anonymous(
+                &env.layout_cache.interner,
+                RocType::Num(RocNum::F64),
+                layout,
+            ),
+        },
+        (Builtin::Decimal, _) => types.add_anonymous(
+            &env.layout_cache.interner,
+            RocType::Num(RocNum::Dec),
+            layout,
+        ),
+        (Builtin::Bool, _) => {
+            types.add_anonymous(&env.layout_cache.interner, RocType::Bool, layout)
+        }
+        (Builtin::Str, _) => {
+            types.add_anonymous(&env.layout_cache.interner, RocType::RocStr, layout)
+        }
+        (Builtin::List(elem_layout), Structure(Apply(Symbol::LIST_LIST, args))) => {
+            let args = env.subs.get_subs_slice(*args);
+            debug_assert_eq!(args.len(), 1);
+
+            let elem_id = add_type_help(env, elem_layout, args[0], opt_name, types);
+            let list_id = types.add_anonymous(
+                &env.layout_cache.interner,
+                RocType::RocList(elem_id),
+                layout,
+            );
+
+            types.depends(list_id, elem_id);
+
+            list_id
+        }
+        (
+            Builtin::List(elem_layout),
+            Alias(Symbol::DICT_DICT, _alias_variables, alias_var, AliasKind::Opaque),
+        ) => {
+            match (
+                env.layout_cache.get_repr(elem_layout),
+                env.subs.get_content_without_compacting(*alias_var),
+            ) {
+                (
+                    LayoutRepr::Struct(field_layouts),
+                    Content::Structure(FlatType::Apply(Symbol::LIST_LIST, args_subs_slice)),
+                ) => {
+                    let (key_var, val_var) = {
+                        let args_tuple = env.subs.get_subs_slice(*args_subs_slice);
+
+                        debug_assert_eq!(args_tuple.len(), 1);
+
+                        match env.subs.get_content_without_compacting(args_tuple[0]) {
+                            Content::Structure(FlatType::TagUnion(union_tags, ext_var)) => {
+                                let (mut iter, _) = union_tags.sorted_iterator_and_ext(env.subs, *ext_var);
+                                let payloads = iter.next().unwrap().1;
+
+                                debug_assert_eq!(iter.next(), None);
+
+                                (payloads[0], payloads[1])
+                            }
+                            _ => {
+                                unreachable!()
+                            }
+                        }
+                    };
+
+                    debug_assert_eq!(field_layouts.len(), 2);
+
+                    let key_id = add_type_help(env, field_layouts[0], key_var, opt_name, types);
+                    let val_id = add_type_help(env, field_layouts[1], val_var, opt_name, types);
+                    let dict_id = types.add_anonymous(&env.layout_cache.interner,RocType::RocDict(key_id, val_id), layout);
+
+                    types.depends(dict_id, key_id);
+                    types.depends(dict_id, val_id);
+
+                    dict_id
+                }
+                (elem_layout, alias_content) => unreachable!(
+                    "Unrecognized List element for Dict. Layout was: {:?} and alias_content was: {:?}",
+                    elem_layout,
+                    alias_content
+                ),
+            }
+        }
+        (
+            Builtin::List(elem_layout),
+            Alias(Symbol::SET_SET, _alias_vars, alias_var, AliasKind::Opaque),
+        ) => {
+            match (
+                env.layout_cache.get_repr(elem_layout),
+                env.subs.get_content_without_compacting(*alias_var),
+            ) {
+                (
+                    LayoutRepr::Struct(field_layouts),
+                    Alias(Symbol::DICT_DICT, alias_args, _alias_var, AliasKind::Opaque),
+                ) => {
+                    let dict_type_vars = env.subs.get_subs_slice(alias_args.type_variables());
+
+                    debug_assert_eq!(dict_type_vars.len(), 2);
+
+                    // Sets only use the key of the Dict they wrap, not the value
+                    let elem_var = dict_type_vars[0];
+
+                    debug_assert_eq!(field_layouts.len(), 2);
+
+                    let elem_id = add_type_help(env, field_layouts[0], elem_var, opt_name, types);
+                    let set_id = types.add_anonymous(&env.layout_cache.interner,RocType::RocSet(elem_id), layout);
+
+                    types.depends(set_id, elem_id);
+
+                    set_id
+                }
+                (elem_layout, alias_content) => unreachable!(
+                    "Unrecognized List element for Set. Layout was: {:?} and alias_content was: {:?}",
+                    elem_layout,
+                    alias_content
+                ),
+            }
+        }
+        (Builtin::List(elem_layout), alias) => {
+            unreachable!(
+                "The type alias {:?} was not an Apply(Symbol::LIST_LIST) as expected, given that its builtin was Builtin::List({:?})",
+                alias, elem_layout
+            );
+        }
+    }
+}
+
+fn add_function<'a, F>(
+    env: &mut Env<'a>,
+    name: String,
+    types: &mut Types,
+    layout: InLayout<'a>,
+    to_type: F,
+) -> TypeId
+where
+    F: FnOnce(String) -> RocType,
+{
+    // let subs = env.subs;
+    // let arena = env.arena;
+
+    types.add_named(
+        &env.layout_cache.interner,
+        name.clone(),
+        to_type(name),
+        layout,
+    )
+}
+
+fn add_struct<'a, I, L, F>(
+    env: &mut Env<'a>,
+    name: String,
+    fields: I,
+    types: &mut Types,
+    in_layout: InLayout<'a>,
+    to_type: F,
+) -> TypeId
+where
+    I: IntoIterator,
+    L: Display + Ord,
+    F: FnOnce(String, RocStructFields) -> RocType,
+{
+    let subs = env.subs;
+    let arena = env.arena;
+    let fields_iter = &mut fields.into_iter();
+    let mut sortables =
+        bumpalo::collections::Vec::with_capacity_in(fields_iter.size_hint().0, arena);
+
+    for (label, field_var) in fields_iter {
+        sortables.push((
+            label,
+            field_var,
+            env.layout_cache
+                .from_var(env.arena, field_var, subs)
+                .unwrap(),
+        ));
+    }
+
+    sortables.sort_by(|(label1, _, layout1), (label2, _, layout2)| {
+        cmp_fields(
+            &env.layout_cache.interner,
+            label1,
+            *layout1,
+            label2,
+            *layout2,
+        )
+    });
+
+    // This layout should have an entry in glue_procs_by_layout iff it
+    // contains closures, but we'll double-check that with a debug_assert.
+    let layout = env.layout_cache.interner.get(in_layout);
+    let struct_fields = match env.glue_procs_by_layout.get(&layout) {
+        Some(&glue_procs) => {
+            debug_assert!(env
+                .layout_cache
+                .interner
+                .has_varying_stack_size(in_layout, arena));
+
+            let fields: Vec<(String, TypeId, Accessors)> = sortables
+                .into_iter()
+                .zip(glue_procs.iter())
+                .map(|((label, field_var, field_layout), getter)| {
+                    let type_id = add_type_help(env, field_layout, field_var, None, types);
+                    let accessors = Accessors {
+                        getter: getter.clone(),
+                    };
+
+                    (format!("{label}"), type_id, accessors)
+                })
+                .collect();
+
+            RocStructFields::HasClosure { fields }
+        }
+        None => {
+            // debug_assert!(!layout.has_varying_stack_size(&env.layout_cache.interner, arena));
+
+            let fields: Vec<(String, TypeId)> = sortables
+                .into_iter()
+                .map(|(label, field_var, field_layout)| {
+                    let type_id = add_type_help(env, field_layout, field_var, None, types);
+
+                    (format!("{label}"), type_id)
+                })
+                .collect();
+
+            RocStructFields::HasNoClosure { fields }
+        }
+    };
+
+    types.add_named(
+        &env.layout_cache.interner,
+        name.clone(),
+        to_type(name, struct_fields),
+        in_layout,
+    )
+}
+
+trait UnionTag: Label + std::fmt::Debug {
+    fn union_tag_name(&self) -> String;
+}
+
+impl UnionTag for TagName {
+    fn union_tag_name(&self) -> String {
+        self.0.as_str().to_string()
+    }
+}
+
+impl UnionTag for Symbol {
+    fn union_tag_name(&self) -> String {
+        format!("C{:?}_{}", self.module_id(), self.ident_id().index())
+    }
+}
+
+fn tag_union_type_from_layout<'a>(
+    env: &mut Env<'a>,
+    opt_name: Option,
+    name: String,
+    union_tags: &UnionLabels,
+    var: Variable,
+    types: &mut Types,
+    layout: InLayout<'a>,
+) -> RocTagUnion {
+    let subs = env.subs;
+
+    match env.layout_cache.get_repr(layout) {
+        _ if union_tags.is_newtype_wrapper(subs)
+            && matches!(
+                subs.get_content_without_compacting(var),
+                // Make sure this is a tag union, *not* a recursive tag union!
+                // Otherwise, we could end up with a recursive tag union
+                // getting unwrapped incorrectly.
+                Content::Structure(FlatType::TagUnion(_, _))
+            ) =>
+        {
+            // A newtype wrapper should always have the same layout as its payload.
+            let payload_layout = layout;
+            let (tag_name, payload) =
+                single_tag_payload_fields(env, union_tags, subs, layout, &[payload_layout], types);
+
+            RocTagUnion::SingleTagStruct {
+                name: name.clone(),
+                tag_name,
+                payload,
+            }
+        }
+        LayoutRepr::Union(union_layout) => {
+            use UnionLayout::*;
+
+            match union_layout {
+                // A non-recursive tag union
+                // e.g. `Result ok err : [Ok ok, Err err]`
+                NonRecursive(_) => {
+                    let tags =
+                        union_tags_to_types(&name, union_tags, subs, env, types, layout, false);
+                    // TODO deal with empty tag union
+                    let discriminant_size = Discriminant::from_number_of_tags(tags.len())
+                        .stack_size()
+                        .max(1);
+                    let discriminant_offset = union_layout
+                        .tag_id_offset(&env.layout_cache.interner)
+                        .unwrap();
+
+                    RocTagUnion::NonRecursive {
+                        name: name.clone(),
+                        tags,
+                        discriminant_size,
+                        discriminant_offset,
+                    }
+                }
+                // A recursive tag union (general case)
+                // e.g. `Expr : [Sym Str, Add Expr Expr]`
+                Recursive(_) => {
+                    let tags =
+                        union_tags_to_types(&name, union_tags, subs, env, types, layout, true);
+                    let discriminant_size =
+                        Discriminant::from_number_of_tags(tags.len()).stack_size();
+                    let discriminant_offset = union_layout
+                        .tag_id_offset(&env.layout_cache.interner)
+                        .unwrap();
+
+                    RocTagUnion::Recursive {
+                        name: name.clone(),
+                        tags,
+                        discriminant_size,
+                        discriminant_offset,
+                    }
+                }
+                NonNullableUnwrapped(_) => {
+                    let (tag_name, payload_vars) = single_tag_payload(union_tags, subs);
+                    let (tag_name, opt_payload) =
+                        tag_to_type(&name, env, tag_name, payload_vars, types, layout, true);
+
+                    // A recursive tag union with just one constructor
+                    // Optimization: No need to store a tag ID (the payload is "unwrapped")
+                    // e.g. `RoseTree a : [Tree a (List (RoseTree a))]`
+                    RocTagUnion::NonNullableUnwrapped {
+                        name: name.clone(),
+                        tag_name,
+                        payload: opt_payload.unwrap(),
+                    }
+                }
+                // A recursive tag union that has an empty variant
+                // Optimization: Represent the empty variant as null pointer => no memory usage & fast comparison
+                // It has more than one other variant, so they need tag IDs (payloads are "wrapped")
+                // e.g. `FingerTree a : [Empty, Single a, More (Some a) (FingerTree (Tuple a)) (Some a)]`
+                // see also: https://youtu.be/ip92VMpf_-A?t=164
+                NullableWrapped {
+                    nullable_id,
+                    other_tags,
+                } => {
+                    let tags =
+                        union_tags_to_types(&name, union_tags, subs, env, types, layout, true);
+                    let discriminant_size =
+                        Discriminant::from_number_of_tags(other_tags.len()).stack_size();
+                    let discriminant_offset = union_layout
+                        .tag_id_offset(&env.layout_cache.interner)
+                        .unwrap();
+
+                    // nullable_id refers to the index of the tag that is represented at runtime as NULL.
+                    // For example, in `FingerTree a : [Empty, Single a, More (Some a) (FingerTree (Tuple a)) (Some a)]`,
+                    // the ids would be Empty = 0, More = 1, Single = 2, because that's how those tags are
+                    // ordered alphabetically. Since the Empty tag will be represented at runtime as NULL,
+                    // and since Empty's tag id is 0, here nullable_id would be 0.
+                    RocTagUnion::NullableWrapped {
+                        name: name.clone(),
+                        index_of_null_tag: nullable_id,
+                        tags,
+                        discriminant_size,
+                        discriminant_offset,
+                    }
+                }
+                // A recursive tag union with only two variants, where one is empty.
+                // Optimizations: Use null for the empty variant AND don't store a tag ID for the other variant.
+                // e.g. `ConsList a : [Nil, Cons a (ConsList a)]`
+                NullableUnwrapped {
+                    nullable_id: null_represents_first_tag,
+                    other_fields: _, // TODO use this!
+                } => {
+                    let mut tags =
+                        union_tags_to_types(&name, union_tags, subs, env, types, layout, true);
+                    // NullableUnwrapped tag unions should always have exactly 2 tags.
+                    debug_assert_eq!(tags.len(), 2);
+
+                    let null_tag;
+                    let non_null;
+
+                    if null_represents_first_tag {
+                        // If nullable_id is true, then the null tag is second, which means
+                        // pop() will return it because it's at the end of the vec.
+                        null_tag = tags.pop().unwrap().0;
+                        non_null = tags.pop().unwrap();
+                    } else {
+                        // The null tag is first, which means the tag with the payload is second.
+                        non_null = tags.pop().unwrap();
+                        null_tag = tags.pop().unwrap().0;
+                    }
+
+                    let (non_null_tag, non_null_payload) = non_null;
+
+                    RocTagUnion::NullableUnwrapped {
+                        name: name.clone(),
+                        null_tag,
+                        non_null_tag,
+                        non_null_payload: non_null_payload.unwrap(),
+                        null_represents_first_tag,
+                    }
+                }
+            }
+        }
+        LayoutRepr::Builtin(Builtin::Int(int_width)) => {
+            add_int_enumeration(union_tags, subs, &name, int_width)
+        }
+        LayoutRepr::Struct(field_layouts) => {
+            let (tag_name, payload) =
+                single_tag_payload_fields(env, union_tags, subs, layout, field_layouts, types);
+
+            RocTagUnion::SingleTagStruct {
+                name: name.clone(),
+                tag_name,
+                payload,
+            }
+        }
+        LayoutRepr::Builtin(Builtin::Bool) => {
+            // This isn't actually a Bool, but rather a 2-tag union with no payloads
+            // (so it has the same layout as a Bool, but actually isn't one; if it were
+            // a real Bool, it would have been handled elsewhere already!)
+            add_int_enumeration(union_tags, subs, &name, IntWidth::U8)
+        }
+        LayoutRepr::Builtin(builtin) => {
+            let type_id = add_builtin_type(env, builtin, var, opt_name, types, layout);
+            let (tag_name, _) = single_tag_payload(union_tags, subs);
+
+            RocTagUnion::SingleTagStruct {
+                name: name.clone(),
+                tag_name,
+                payload: RocSingleTagPayload::HasNoClosure {
+                    // Builtins have no closures
+                    payload_fields: vec![type_id],
+                },
+            }
+        }
+        LayoutRepr::Ptr(_) => unreachable!("Ptr values are never publicly exposed"),
+        LayoutRepr::LambdaSet(lambda_set) => tag_union_type_from_layout(
+            env,
+            opt_name,
+            name,
+            union_tags,
+            var,
+            types,
+            lambda_set.runtime_representation(),
+        ),
+        LayoutRepr::RecursivePointer(_) => {
+            // A single-tag union which only wraps itself is erroneous and should have
+            // been turned into an error earlier in the process.
+            unreachable!();
+        }
+        LayoutRepr::FunctionPointer(_) => todo_lambda_erasure!(),
+        LayoutRepr::Erased(_) => todo_lambda_erasure!(),
+    }
+}
+
+fn add_tag_union<'a>(
+    env: &mut Env<'a>,
+    opt_name: Option,
+    union_tags: &UnionLabels,
+    var: Variable,
+    types: &mut Types,
+    layout: InLayout<'a>,
+    rec_root: Option,
+) -> TypeId {
+    let name = match opt_name {
+        Some(sym) => sym.as_str(env.interns).to_string(),
+        None => env.enum_names.get_name(var),
+    };
+
+    let tag_union_type = tag_union_type_from_layout(
+        env,
+        opt_name,
+        name.to_string(),
+        union_tags,
+        var,
+        types,
+        layout,
+    );
+
+    let typ = RocType::TagUnion(tag_union_type);
+    let type_id = types.add_named(&env.layout_cache.interner, name, typ, layout);
+
+    if let Some(rec_var) = rec_root {
+        env.known_recursive_types.insert(rec_var, type_id);
+    }
+
+    type_id
+}
+
+fn add_int_enumeration(
+    union_tags: &UnionLabels,
+    subs: &Subs,
+    name: &str,
+    int_width: IntWidth,
+) -> RocTagUnion {
+    let tags: Vec = union_tags
+        .iter_from_subs(subs)
+        .map(|(tag_name, _)| tag_name.union_tag_name())
+        .collect();
+    RocTagUnion::Enumeration {
+        name: name.to_string(),
+        tags,
+        size: int_width.stack_size(),
+    }
+}
+
+fn union_tags_to_types<'a>(
+    name: &str,
+    union_tags: &UnionLabels,
+    subs: &Subs,
+    env: &mut Env<'a>,
+    types: &mut Types,
+    layout: InLayout<'a>,
+    is_recursive: bool,
+) -> Vec<(String, Option)> {
+    let mut tags: Vec<(String, Vec)> = union_tags
+        .iter_from_subs(subs)
+        .map(|(tag_name, payload_vars)| {
+            let name_str = tag_name.union_tag_name();
+
+            (name_str, payload_vars.to_vec())
+        })
+        .collect();
+
+    // Sort tags alphabetically by tag name
+    tags.sort_by(|(name1, _), (name2, _)| name1.cmp(name2));
+
+    tags.into_iter()
+        .map(|(tag_name, payload_vars)| {
+            tag_to_type(
+                name,
+                env,
+                tag_name,
+                &payload_vars,
+                types,
+                layout,
+                is_recursive,
+            )
+        })
+        .collect()
+}
+
+fn single_tag_payload<'a>(
+    union_tags: &'a UnionLabels,
+    subs: &'a Subs,
+) -> (String, &'a [Variable]) {
+    let mut iter = union_tags.iter_from_subs(subs);
+    let (tag_name, payload_vars) = iter.next().unwrap();
+
+    // This should be a single-tag union, but it could be the remnant of a `Result.Result a []`,
+    // where the `Err` branch is inconsequential, but still part of the type
+
+    (tag_name.union_tag_name(), payload_vars)
+}
+
+fn single_tag_payload_fields<'a, 'b>(
+    env: &mut Env<'a>,
+    union_tags: &'b UnionLabels,
+    subs: &'b Subs,
+    in_layout: InLayout<'a>,
+    field_layouts: &[InLayout<'a>],
+    types: &mut Types,
+) -> (String, RocSingleTagPayload) {
+    let layout = env.layout_cache.interner.get(in_layout);
+    // There should be a glue_procs_by_layout entry iff this layout has a closure in it,
+    // so we shouldn't need to separately check that. Howeevr, we still do a debug_assert
+    // anyway just so we have some warning in case that relationship somehow didn't hold!
+    debug_assert_eq!(
+        env.glue_procs_by_layout.get(&layout).is_some(),
+        env.layout_cache
+            .interner
+            .has_varying_stack_size(in_layout, env.arena),
+        "glue_procs_by_layout for {:?} was {:?}, but the layout cache said its has_varying_stack_size was {}",
+            &layout,
+            env.glue_procs_by_layout.get(&layout),
+            env.layout_cache
+                .interner
+                .has_varying_stack_size(in_layout, env.arena)
+    );
+
+    let (tag_name, payload_vars) = single_tag_payload(union_tags, subs);
+
+    let payload = match env.glue_procs_by_layout.get(&layout) {
+        Some(glue_procs) => {
+            let payload_getters = payload_vars
+                .iter()
+                .zip(field_layouts.iter())
+                .zip(glue_procs.iter())
+                .map(|((field_var, field_layout), getter_name)| {
+                    let type_id = add_type_help(env, *field_layout, *field_var, None, types);
+
+                    (type_id, getter_name.to_string())
+                })
+                .collect();
+
+            RocSingleTagPayload::HasClosure { payload_getters }
+        }
+        None => RocSingleTagPayload::HasNoClosure {
+            payload_fields: payload_vars
+                .iter()
+                .zip(field_layouts.iter())
+                .map(|(field_var, field_layout)| {
+                    add_type_help(env, *field_layout, *field_var, None, types)
+                })
+                .collect(),
+        },
+    };
+
+    (tag_name, payload)
+}
+
+fn tag_to_type<'a, D: Display>(
+    name: &str,
+    env: &mut Env<'a>,
+    tag_name: D,
+    payload_vars: &[Variable],
+    types: &mut Types,
+    layout: InLayout<'a>,
+    is_recursive: bool,
+) -> (D, Option) {
+    match struct_fields_needed(env, payload_vars.iter().copied()) {
+        0 => {
+            // no payload
+            (tag_name, None)
+        }
+        1 if !is_recursive => {
+            // this isn't recursive and there's 1 payload item, so it doesn't
+            // need its own struct - e.g. for `[Foo Str, Bar Str]` both of them
+            // can have payloads of plain old Str, no struct wrapper needed.
+            let payload_var = payload_vars.get(0).unwrap();
+            let payload_layout = env
+                .layout_cache
+                .from_var(env.arena, *payload_var, env.subs)
+                .expect("Something weird ended up in the content");
+            let payload_id = add_type_help(env, payload_layout, *payload_var, None, types);
+
+            (tag_name, Some(payload_id))
+        }
+        _ => {
+            // create a RocType for the payload and save it
+            let struct_name = format!("{}_{}", &name, tag_name); // e.g. "MyUnion_MyVariant"
+            let fields = payload_vars.iter().copied().enumerate();
+            let struct_id = add_struct(env, struct_name, fields, types, layout, |name, fields| {
+                RocType::TagUnionPayload { name, fields }
+            });
+
+            (tag_name, Some(struct_id))
+        }
+    }
+}
+
+fn struct_fields_needed>(env: &mut Env<'_>, vars: I) -> usize {
+    let subs = env.subs;
+    let arena = env.arena;
+
+    vars.into_iter().fold(0, |count, var| {
+        let layout = env.layout_cache.from_var(arena, var, subs).unwrap();
+
+        if env.layout_cache.get_repr(layout).is_dropped_because_empty() {
+            count
+        } else {
+            count + 1
+        }
+    })
+}
diff --git a/crates/glue/static/Cargo.toml b/crates/glue/static/Cargo.toml
new file mode 100644
index 0000000000..ff90095969
--- /dev/null
+++ b/crates/glue/static/Cargo.toml
@@ -0,0 +1,8 @@
+[package]
+name = "roc_app"
+description = "This was generated by `roc glue`. It provides glue between a specific Roc platform and a Rust host."
+version = "1.0.0"
+edition = "2021"
+
+[dependencies]
+roc_std = { path = "../roc_std" }
diff --git a/crates/glue/templates/header.rs b/crates/glue/templates/header.rs
new file mode 100644
index 0000000000..22a435144b
--- /dev/null
+++ b/crates/glue/templates/header.rs
@@ -0,0 +1,23 @@
+// ⚠️ GENERATED CODE ⚠️ - this entire file was generated by the `roc glue` CLI command
+
+#![allow(unused_unsafe)]
+#![allow(unused_variables)]
+#![allow(dead_code)]
+#![allow(unused_mut)]
+#![allow(non_snake_case)]
+#![allow(non_camel_case_types)]
+#![allow(non_upper_case_globals)]
+#![allow(clippy::undocumented_unsafe_blocks)]
+#![allow(clippy::redundant_static_lifetimes)]
+#![allow(clippy::unused_unit)]
+#![allow(clippy::missing_safety_doc)]
+#![allow(clippy::let_and_return)]
+#![allow(clippy::missing_safety_doc)]
+#![allow(clippy::redundant_static_lifetimes)]
+#![allow(clippy::needless_borrow)]
+#![allow(clippy::clone_on_copy)]
+
+type Op_StderrWrite = roc_std::RocStr;
+type Op_StdoutWrite = roc_std::RocStr;
+type TODO_roc_function_69 = roc_std::RocStr;
+type TODO_roc_function_70 = roc_std::RocStr;
diff --git a/crates/glue/tests/fixture-templates/rust/Cargo.toml b/crates/glue/tests/fixture-templates/rust/Cargo.toml
new file mode 100644
index 0000000000..56f73f42ff
--- /dev/null
+++ b/crates/glue/tests/fixture-templates/rust/Cargo.toml
@@ -0,0 +1,35 @@
+# ⚠️ READ THIS BEFORE MODIFYING THIS FILE! ⚠️
+#
+# This file is a fixture template. If the file you're looking at is
+# in the fixture-templates/ directory, then you're all set - go ahead
+# and modify it, and it will modify all the fixture tests.
+#
+# If this file is in the fixtures/ directory, on the other hand, then
+# it is gitignored and will be overwritten the next time tests run.
+# So you probably don't want to modify it by hand! Instead, modify the
+# file with the same name in the fixture-templates/ directory.
+
+[package]
+name = "host"
+version = "0.0.1"
+authors = ["The Roc Contributors"]
+license = "UPL-1.0"
+edition = "2018"
+links = "app"
+
+[lib]
+name = "host"
+path = "src/lib.rs"
+crate-type = ["staticlib", "rlib"]
+
+[[bin]]
+name = "host"
+path = "src/main.rs"
+
+[dependencies]
+roc_std = { path = "test_glue/roc_std" }
+roc_app = { path = "test_glue/roc_app" }
+libc = "0.2"
+indoc = "1.0.6"
+
+[workspace]
diff --git a/crates/glue/tests/fixture-templates/rust/build.rs b/crates/glue/tests/fixture-templates/rust/build.rs
new file mode 100644
index 0000000000..6c70d142f0
--- /dev/null
+++ b/crates/glue/tests/fixture-templates/rust/build.rs
@@ -0,0 +1,20 @@
+// ⚠️ READ THIS BEFORE MODIFYING THIS FILE! ⚠️
+//
+// This file is a fixture template. If the file you're looking at is
+// in the fixture-templates/ directory, then you're all set - go ahead
+// and modify it, and it will modify all the fixture tests.
+//
+// If this file is in the fixtures/ directory, on the other hand, then
+// it is gitignored and will be overwritten the next time tests run.
+// So you probably don't want to modify it by hand! Instead, modify the
+// file with the same name in the fixture-templates/ directory.
+
+fn main() {
+    #[cfg(not(windows))]
+    println!("cargo:rustc-link-lib=dylib=app");
+
+    #[cfg(windows)]
+    println!("cargo:rustc-link-lib=dylib=libapp");
+
+    println!("cargo:rustc-link-search=.");
+}
diff --git a/crates/glue/tests/fixture-templates/rust/host.c b/crates/glue/tests/fixture-templates/rust/host.c
new file mode 100644
index 0000000000..7d0fec6812
--- /dev/null
+++ b/crates/glue/tests/fixture-templates/rust/host.c
@@ -0,0 +1,17 @@
+// ⚠️ READ THIS BEFORE MODIFYING THIS FILE! ⚠️
+//
+// This file is a fixture template. If the file you're looking at is
+// in the fixture-templates/ directory, then you're all set - go ahead
+// and modify it, and it will modify all the fixture tests.
+//
+// If this file is in the fixtures/ directory, on the other hand, then
+// it is gitignored and will be overwritten the next time tests run.
+// So you probably don't want to modify it by hand! Instead, modify the
+// file with the same name in the fixture-templates/ directory.
+
+extern void rust_main();
+
+int main() {
+  rust_main();
+  return 0;
+}
diff --git a/crates/glue/tests/fixture-templates/rust/src/main.rs b/crates/glue/tests/fixture-templates/rust/src/main.rs
new file mode 100644
index 0000000000..57692d3619
--- /dev/null
+++ b/crates/glue/tests/fixture-templates/rust/src/main.rs
@@ -0,0 +1,3 @@
+fn main() {
+    host::rust_main();
+}
diff --git a/crates/glue/tests/fixtures/.gitignore b/crates/glue/tests/fixtures/.gitignore
new file mode 100644
index 0000000000..5d524b2751
--- /dev/null
+++ b/crates/glue/tests/fixtures/.gitignore
@@ -0,0 +1,12 @@
+Cargo.lock
+Cargo.toml
+build.rs
+host.c
+test_glue
+roc_externs.rs
+main.rs
+app
+dynhost
+libapp.so
+metadata
+preprocessedhost
diff --git a/crates/glue/tests/fixtures/advanced-recursive-union/app.roc b/crates/glue/tests/fixtures/advanced-recursive-union/app.roc
new file mode 100644
index 0000000000..0c24291ff9
--- /dev/null
+++ b/crates/glue/tests/fixtures/advanced-recursive-union/app.roc
@@ -0,0 +1,14 @@
+app "app"
+    packages { pf: "platform.roc" }
+    imports []
+    provides [main] to pf
+
+main = {
+    default: Job {
+        command: Command {
+            tool: SystemTool { name: "test", num: 42 },
+        },
+        inputFiles: ["foo"],
+    },
+}
+
diff --git a/crates/glue/tests/fixtures/advanced-recursive-union/platform.roc b/crates/glue/tests/fixtures/advanced-recursive-union/platform.roc
new file mode 100644
index 0000000000..06b43a6dc9
--- /dev/null
+++ b/crates/glue/tests/fixtures/advanced-recursive-union/platform.roc
@@ -0,0 +1,26 @@
+platform "test-platform"
+    requires {} { main : _ }
+    exposes []
+    packages {}
+    imports []
+    provides [mainForHost]
+
+Tool : [
+    SystemTool { name : Str, num : U32 },
+    FromJob { job : Job, num : U32 },
+]
+
+Command : [Command { tool : Tool }]
+
+Job : [
+    Job { command : Command, inputFiles : List Str },
+    Foo Str,
+    # TODO make a recursive tag union test that doesn't try to do mutual recursion,
+    # just so I can get a PR up.
+    # WithTool Tool # Mutual recursion; Tool also references Job
+]
+
+Rbt : { default : Job }
+
+mainForHost : {} -> Rbt
+mainForHost = \{} -> main
diff --git a/crates/glue/tests/fixtures/advanced-recursive-union/src/lib.rs b/crates/glue/tests/fixtures/advanced-recursive-union/src/lib.rs
new file mode 100644
index 0000000000..9fa24c4665
--- /dev/null
+++ b/crates/glue/tests/fixtures/advanced-recursive-union/src/lib.rs
@@ -0,0 +1,87 @@
+use roc_app;
+
+use indoc::indoc;
+use roc_std::RocStr;
+
+#[no_mangle]
+pub extern "C" fn rust_main() {
+    use std::cmp::Ordering;
+    use std::collections::hash_set::HashSet;
+
+    let tag_union = roc_app::mainForHost();
+
+    // Verify that it has all the expected traits.
+
+    assert!(tag_union == tag_union); // PartialEq
+
+    assert!(tag_union.clone() == tag_union.clone()); // Clone
+
+    assert!(tag_union.partial_cmp(&tag_union) == Some(Ordering::Equal)); // PartialOrd
+    assert!(tag_union.cmp(&tag_union) == Ordering::Equal); // Ord
+
+    print!(
+        indoc!(
+            r#"
+                rbt was: {:?}
+            "#
+        ),
+        tag_union,
+    ); // Debug
+
+    let mut set = HashSet::new();
+
+    set.insert(tag_union.clone()); // Eq, Hash
+    set.insert(tag_union);
+
+    assert_eq!(set.len(), 1);
+}
+
+// Externs required by roc_std and by the Roc app
+
+use core::ffi::c_void;
+use std::ffi::CStr;
+use std::os::raw::c_char;
+
+#[no_mangle]
+pub unsafe extern "C" fn roc_alloc(size: usize, _alignment: u32) -> *mut c_void {
+    libc::malloc(size)
+}
+
+#[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 {
+    return libc::realloc(c_ptr, new_size);
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn roc_dealloc(c_ptr: *mut c_void, _alignment: u32) {
+    return libc::free(c_ptr);
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn roc_panic(msg: *mut RocStr, tag_id: u32) {
+    match tag_id {
+        0 => {
+            eprintln!("Roc standard library hit a panic: {}", &*msg);
+        }
+        1 => {
+            eprintln!("Application hit a panic: {}", &*msg);
+        }
+        _ => unreachable!(),
+    }
+    std::process::exit(1);
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn roc_dbg(loc: *mut RocStr, msg: *mut RocStr) {
+    eprintln!("[{}] {}", &*loc, &*msg);
+}
+
+#[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)
+}
diff --git a/crates/glue/tests/fixtures/arguments/app.roc b/crates/glue/tests/fixtures/arguments/app.roc
new file mode 100644
index 0000000000..a6fb727c6c
--- /dev/null
+++ b/crates/glue/tests/fixtures/arguments/app.roc
@@ -0,0 +1,7 @@
+app "app"
+    packages { pf: "platform.roc" }
+    imports []
+    provides [main] to pf
+
+main : I64 -> I64
+main = \x -> 2 * x
diff --git a/crates/glue/tests/fixtures/arguments/platform.roc b/crates/glue/tests/fixtures/arguments/platform.roc
new file mode 100644
index 0000000000..b4cfc05570
--- /dev/null
+++ b/crates/glue/tests/fixtures/arguments/platform.roc
@@ -0,0 +1,9 @@
+platform "test-platform"
+    requires {} { main : I64 -> I64 }
+    exposes []
+    packages {}
+    imports []
+    provides [mainForHost]
+
+mainForHost : I64 -> I64
+mainForHost = \x -> main x
diff --git a/crates/glue/tests/fixtures/arguments/src/lib.rs b/crates/glue/tests/fixtures/arguments/src/lib.rs
new file mode 100644
index 0000000000..4f0db9af41
--- /dev/null
+++ b/crates/glue/tests/fixtures/arguments/src/lib.rs
@@ -0,0 +1,59 @@
+use roc_app;
+use roc_std::RocStr;
+
+#[no_mangle]
+pub extern "C" fn rust_main() {
+    let answer = roc_app::mainForHost(42i64);
+
+    println!("Answer was: {:?}", answer); // Debug
+}
+
+// Externs required by roc_std and by the Roc app
+
+use core::ffi::c_void;
+use std::ffi::CStr;
+use std::os::raw::c_char;
+
+#[no_mangle]
+pub unsafe extern "C" fn roc_alloc(size: usize, _alignment: u32) -> *mut c_void {
+    return libc::malloc(size);
+}
+
+#[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 {
+    return libc::realloc(c_ptr, new_size);
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn roc_dealloc(c_ptr: *mut c_void, _alignment: u32) {
+    return libc::free(c_ptr);
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn roc_panic(msg: *mut RocStr, tag_id: u32) {
+    match tag_id {
+        0 => {
+            eprintln!("Roc standard library hit a panic: {}", &*msg);
+        }
+        1 => {
+            eprintln!("Application hit a panic: {}", &*msg);
+        }
+        _ => unreachable!(),
+    }
+    std::process::exit(1);
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn roc_dbg(loc: *mut RocStr, msg: *mut RocStr) {
+    eprintln!("[{}] {}", &*loc, &*msg);
+}
+
+#[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)
+}
diff --git a/crates/glue/tests/fixtures/basic-record/app.roc b/crates/glue/tests/fixtures/basic-record/app.roc
new file mode 100644
index 0000000000..72bacc9abc
--- /dev/null
+++ b/crates/glue/tests/fixtures/basic-record/app.roc
@@ -0,0 +1,6 @@
+app "app"
+    packages { pf: "platform.roc" }
+    imports []
+    provides [main] to pf
+
+main = { a: 1995, b: 42 }
diff --git a/bindgen/tests/fixtures/basic-record/Package-Config.roc b/crates/glue/tests/fixtures/basic-record/platform.roc
similarity index 100%
rename from bindgen/tests/fixtures/basic-record/Package-Config.roc
rename to crates/glue/tests/fixtures/basic-record/platform.roc
diff --git a/crates/glue/tests/fixtures/basic-record/src/lib.rs b/crates/glue/tests/fixtures/basic-record/src/lib.rs
new file mode 100644
index 0000000000..cd50e9b76b
--- /dev/null
+++ b/crates/glue/tests/fixtures/basic-record/src/lib.rs
@@ -0,0 +1,81 @@
+use roc_app;
+use roc_std::RocStr;
+
+#[no_mangle]
+pub extern "C" fn rust_main() {
+    use std::cmp::Ordering;
+    use std::collections::hash_set::HashSet;
+
+    let record = roc_app::mainForHost();
+
+    // Verify that the record has all the expected traits.
+
+    assert!(record == record); // PartialEq
+    assert!(record.clone() == record.clone()); // Clone
+
+    // Since this is a move, later uses of `record` will fail unless `record` has Copy
+    let rec2 = record; // Copy
+
+    assert!(rec2 != Default::default()); // Default
+    assert!(record.partial_cmp(&record) == Some(Ordering::Equal)); // PartialOrd
+    assert!(record.cmp(&record) == Ordering::Equal); // Ord
+
+    let mut set = HashSet::new();
+
+    set.insert(record); // Eq, Hash
+    set.insert(rec2);
+
+    assert_eq!(set.len(), 1);
+
+    println!("Record was: {:?}", record); // Debug
+}
+
+// Externs required by roc_std and by the Roc app
+
+use core::ffi::c_void;
+use std::ffi::CStr;
+use std::os::raw::c_char;
+
+#[no_mangle]
+pub unsafe extern "C" fn roc_alloc(size: usize, _alignment: u32) -> *mut c_void {
+    return libc::malloc(size);
+}
+
+#[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 {
+    return libc::realloc(c_ptr, new_size);
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn roc_dealloc(c_ptr: *mut c_void, _alignment: u32) {
+    return libc::free(c_ptr);
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn roc_panic(msg: *mut RocStr, tag_id: u32) {
+    match tag_id {
+        0 => {
+            eprintln!("Roc standard library hit a panic: {}", &*msg);
+        }
+        1 => {
+            eprintln!("Application hit a panic: {}", &*msg);
+        }
+        _ => unreachable!(),
+    }
+    std::process::exit(1);
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn roc_dbg(loc: *mut RocStr, msg: *mut RocStr) {
+    eprintln!("[{}] {}", &*loc, &*msg);
+}
+
+#[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)
+}
diff --git a/crates/glue/tests/fixtures/basic-recursive-union/app.roc b/crates/glue/tests/fixtures/basic-recursive-union/app.roc
new file mode 100644
index 0000000000..9533544fa0
--- /dev/null
+++ b/crates/glue/tests/fixtures/basic-recursive-union/app.roc
@@ -0,0 +1,6 @@
+app "app"
+    packages { pf: "platform.roc" }
+    imports []
+    provides [main] to pf
+
+main = Concat (String "Hello, ") (String "World!")
diff --git a/crates/glue/tests/fixtures/basic-recursive-union/platform.roc b/crates/glue/tests/fixtures/basic-recursive-union/platform.roc
new file mode 100644
index 0000000000..2816c4ba72
--- /dev/null
+++ b/crates/glue/tests/fixtures/basic-recursive-union/platform.roc
@@ -0,0 +1,11 @@
+platform "test-platform"
+    requires {} { main : _ }
+    exposes []
+    packages {}
+    imports []
+    provides [mainForHost]
+
+Expr : [String Str, Concat Expr Expr]
+
+mainForHost : {} -> Expr
+mainForHost = \{} -> main
diff --git a/crates/glue/tests/fixtures/basic-recursive-union/src/lib.rs b/crates/glue/tests/fixtures/basic-recursive-union/src/lib.rs
new file mode 100644
index 0000000000..b104cb5a29
--- /dev/null
+++ b/crates/glue/tests/fixtures/basic-recursive-union/src/lib.rs
@@ -0,0 +1,92 @@
+use indoc::indoc;
+use roc_app::{self, Expr};
+use roc_std::RocStr;
+
+#[no_mangle]
+pub extern "C" fn rust_main() {
+    use std::cmp::Ordering;
+    use std::collections::hash_set::HashSet;
+
+    let tag_union = roc_app::mainForHost();
+
+    // Verify that it has all the expected traits.
+
+    assert!(tag_union == tag_union); // PartialEq
+    assert!(tag_union.clone() == tag_union.clone()); // Clone
+
+    assert!(tag_union.partial_cmp(&tag_union) == Some(Ordering::Equal)); // PartialOrd
+    assert!(tag_union.cmp(&tag_union) == Ordering::Equal); // Ord
+
+    print!(
+        indoc!(
+            r#"
+                tag_union was: {:?}
+                `Concat (String "Hello, ") (String "World!")` is: {:?}
+                `String "this is a test"` is: {:?}
+            "#
+        ),
+        tag_union,
+        Expr::Concat(
+            Expr::String("Hello, ".into()),
+            Expr::String("World!".into()),
+        ),
+        Expr::String("this is a test".into()),
+    ); // Debug
+
+    let mut set = HashSet::new();
+
+    set.insert(tag_union.clone()); // Eq, Hash
+    set.insert(tag_union);
+
+    assert_eq!(set.len(), 1);
+}
+
+// Externs required by roc_std and by the Roc app
+
+use core::ffi::c_void;
+use std::ffi::CStr;
+use std::os::raw::c_char;
+
+#[no_mangle]
+pub unsafe extern "C" fn roc_alloc(size: usize, _alignment: u32) -> *mut c_void {
+    return libc::malloc(size);
+}
+
+#[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 {
+    return libc::realloc(c_ptr, new_size);
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn roc_dealloc(c_ptr: *mut c_void, _alignment: u32) {
+    return libc::free(c_ptr);
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn roc_panic(msg: *mut RocStr, tag_id: u32) {
+    match tag_id {
+        0 => {
+            eprintln!("Roc standard library hit a panic: {}", &*msg);
+        }
+        1 => {
+            eprintln!("Application hit a panic: {}", &*msg);
+        }
+        _ => unreachable!(),
+    }
+    std::process::exit(1);
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn roc_dbg(loc: *mut RocStr, msg: *mut RocStr) {
+    eprintln!("[{}] {}", &*loc, &*msg);
+}
+
+#[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)
+}
diff --git a/crates/glue/tests/fixtures/closures/app.roc b/crates/glue/tests/fixtures/closures/app.roc
new file mode 100644
index 0000000000..89d1df619d
--- /dev/null
+++ b/crates/glue/tests/fixtures/closures/app.roc
@@ -0,0 +1,10 @@
+app "app"
+    packages { pf: "platform.roc" }
+    imports []
+    provides [main] to pf
+
+main : I64 -> ({} -> I64)
+main = \x ->
+    capture1 = 2
+    capture2 = 8
+    \{} -> capture1 * capture2 * x
diff --git a/crates/glue/tests/fixtures/closures/platform.roc b/crates/glue/tests/fixtures/closures/platform.roc
new file mode 100644
index 0000000000..d00c64de20
--- /dev/null
+++ b/crates/glue/tests/fixtures/closures/platform.roc
@@ -0,0 +1,9 @@
+platform "test-platform"
+    requires {} { main : I64 -> ({} -> I64) }
+    exposes []
+    packages {}
+    imports []
+    provides [mainForHost]
+
+mainForHost : I64 -> ({} -> I64)
+mainForHost = \x -> main x
diff --git a/crates/glue/tests/fixtures/closures/src/lib.rs b/crates/glue/tests/fixtures/closures/src/lib.rs
new file mode 100644
index 0000000000..9179f6d02c
--- /dev/null
+++ b/crates/glue/tests/fixtures/closures/src/lib.rs
@@ -0,0 +1,59 @@
+use roc_app;
+use roc_std::RocStr;
+
+#[no_mangle]
+pub extern "C" fn rust_main() {
+    let closure = roc_app::mainForHost(42i64);
+
+    println!("Answer was: {:?}", closure.force_thunk()); // Debug
+}
+
+// Externs required by roc_std and by the Roc app
+
+use core::ffi::c_void;
+use std::ffi::CStr;
+use std::os::raw::c_char;
+
+#[no_mangle]
+pub unsafe extern "C" fn roc_alloc(size: usize, _alignment: u32) -> *mut c_void {
+    return libc::malloc(size);
+}
+
+#[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 {
+    return libc::realloc(c_ptr, new_size);
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn roc_dealloc(c_ptr: *mut c_void, _alignment: u32) {
+    return libc::free(c_ptr);
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn roc_panic(msg: *mut RocStr, tag_id: u32) {
+    match tag_id {
+        0 => {
+            eprintln!("Roc standard library hit a panic: {}", &*msg);
+        }
+        1 => {
+            eprintln!("Application hit a panic: {}", &*msg);
+        }
+        _ => unreachable!(),
+    }
+    std::process::exit(1);
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn roc_dbg(loc: *mut RocStr, msg: *mut RocStr) {
+    eprintln!("[{}] {}", &*loc, &*msg);
+}
+
+#[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)
+}
diff --git a/crates/glue/tests/fixtures/enumeration/app.roc b/crates/glue/tests/fixtures/enumeration/app.roc
new file mode 100644
index 0000000000..d6a78fc409
--- /dev/null
+++ b/crates/glue/tests/fixtures/enumeration/app.roc
@@ -0,0 +1,6 @@
+app "app"
+    packages { pf: "platform.roc" }
+    imports []
+    provides [main] to pf
+
+main = Foo
diff --git a/bindgen/tests/fixtures/enumeration/Package-Config.roc b/crates/glue/tests/fixtures/enumeration/platform.roc
similarity index 100%
rename from bindgen/tests/fixtures/enumeration/Package-Config.roc
rename to crates/glue/tests/fixtures/enumeration/platform.roc
diff --git a/crates/glue/tests/fixtures/enumeration/src/lib.rs b/crates/glue/tests/fixtures/enumeration/src/lib.rs
new file mode 100644
index 0000000000..ebe1a45e41
--- /dev/null
+++ b/crates/glue/tests/fixtures/enumeration/src/lib.rs
@@ -0,0 +1,85 @@
+use roc_app;
+use roc_std::RocStr;
+
+#[no_mangle]
+pub extern "C" fn rust_main() {
+    use std::cmp::Ordering;
+    use std::collections::hash_set::HashSet;
+
+    let tag_union = roc_app::mainForHost();
+
+    // Verify that it has all the expected traits.
+
+    assert!(tag_union == tag_union); // PartialEq
+    assert!(tag_union.clone() == tag_union.clone()); // Clone
+
+    // Since this is a move, later uses of `tag_union` will fail unless `tag_union` has Copy
+    let union2 = tag_union; // Copy
+
+    assert!(tag_union.partial_cmp(&tag_union) == Some(Ordering::Equal)); // PartialOrd
+    assert!(tag_union.cmp(&tag_union) == Ordering::Equal); // Ord
+
+    let mut set = HashSet::new();
+
+    set.insert(tag_union); // Eq, Hash
+    set.insert(union2);
+
+    assert_eq!(set.len(), 1);
+
+    println!(
+        "tag_union was: {:?}, Bar is: {:?}, Baz is: {:?}",
+        tag_union,
+        roc_app::MyEnum::Bar,
+        roc_app::MyEnum::Baz,
+    ); // Debug
+}
+
+// Externs required by roc_std and by the Roc app
+
+use core::ffi::c_void;
+use std::ffi::CStr;
+use std::os::raw::c_char;
+
+#[no_mangle]
+pub unsafe extern "C" fn roc_alloc(size: usize, _alignment: u32) -> *mut c_void {
+    return libc::malloc(size);
+}
+
+#[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 {
+    return libc::realloc(c_ptr, new_size);
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn roc_dealloc(c_ptr: *mut c_void, _alignment: u32) {
+    return libc::free(c_ptr);
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn roc_panic(msg: *mut RocStr, tag_id: u32) {
+    match tag_id {
+        0 => {
+            eprintln!("Roc standard library hit a panic: {}", &*msg);
+        }
+        1 => {
+            eprintln!("Application hit a panic: {}", &*msg);
+        }
+        _ => unreachable!(),
+    }
+    std::process::exit(1);
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn roc_dbg(loc: *mut RocStr, msg: *mut RocStr) {
+    eprintln!("[{}] {}", &*loc, &*msg);
+}
+
+#[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)
+}
diff --git a/crates/glue/tests/fixtures/list-recursive-union/app.roc b/crates/glue/tests/fixtures/list-recursive-union/app.roc
new file mode 100644
index 0000000000..eb1b704e37
--- /dev/null
+++ b/crates/glue/tests/fixtures/list-recursive-union/app.roc
@@ -0,0 +1,16 @@
+app "app"
+    packages { pf: "platform.roc" }
+    imports []
+    provides [main] to pf
+
+main = {
+    default: Job {
+        command: Command {
+            tool: SystemTool { name: "test" },
+            args: [],
+        },
+        job: [],
+        inputFiles: ["foo"],
+    },
+}
+
diff --git a/crates/glue/tests/fixtures/list-recursive-union/platform.roc b/crates/glue/tests/fixtures/list-recursive-union/platform.roc
new file mode 100644
index 0000000000..b248ccc6f1
--- /dev/null
+++ b/crates/glue/tests/fixtures/list-recursive-union/platform.roc
@@ -0,0 +1,17 @@
+platform "test-platform"
+    requires {} { main : _ }
+    exposes []
+    packages {}
+    imports []
+    provides [mainForHost]
+
+Tool : [SystemTool { name : Str }]
+
+Command : [Command { tool : Tool, args : List Str }]
+
+Job : [Job { command : Command, job : List Job, inputFiles : List Str }, Blah Str]
+
+Rbt : { default : Job }
+
+mainForHost : Rbt
+mainForHost = main
diff --git a/crates/glue/tests/fixtures/list-recursive-union/src/lib.rs b/crates/glue/tests/fixtures/list-recursive-union/src/lib.rs
new file mode 100644
index 0000000000..7dd435fa73
--- /dev/null
+++ b/crates/glue/tests/fixtures/list-recursive-union/src/lib.rs
@@ -0,0 +1,87 @@
+use roc_app;
+
+use indoc::indoc;
+use roc_app::Rbt;
+use roc_std::RocStr;
+
+#[no_mangle]
+pub extern "C" fn rust_main() {
+    use std::cmp::Ordering;
+    use std::collections::hash_set::HashSet;
+
+    let tag_union = roc_app::mainForHost();
+
+    // Verify that it has all the expected traits.
+
+    assert!(tag_union == tag_union); // PartialEq
+    assert!(tag_union.clone() == tag_union.clone()); // Clone
+
+    assert!(tag_union.partial_cmp(&tag_union) == Some(Ordering::Equal)); // PartialOrd
+    assert!(tag_union.cmp(&tag_union) == Ordering::Equal); // Ord
+
+    print!(
+        indoc!(
+            r#"
+                rbt was: {:?}
+            "#
+        ),
+        tag_union,
+    ); // Debug
+
+    let mut set = HashSet::new();
+
+    set.insert(tag_union.clone()); // Eq, Hash
+    set.insert(tag_union);
+
+    assert_eq!(set.len(), 1);
+}
+
+// Externs required by roc_std and by the Roc app
+
+use core::ffi::c_void;
+use std::ffi::CStr;
+use std::os::raw::c_char;
+
+#[no_mangle]
+pub unsafe extern "C" fn roc_alloc(size: usize, _alignment: u32) -> *mut c_void {
+    return libc::malloc(size);
+}
+
+#[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 {
+    return libc::realloc(c_ptr, new_size);
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn roc_dealloc(c_ptr: *mut c_void, _alignment: u32) {
+    return libc::free(c_ptr);
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn roc_panic(msg: *mut RocStr, tag_id: u32) {
+    match tag_id {
+        0 => {
+            eprintln!("Roc standard library hit a panic: {}", &*msg);
+        }
+        1 => {
+            eprintln!("Application hit a panic: {}", &*msg);
+        }
+        _ => unreachable!(),
+    }
+    std::process::exit(1);
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn roc_dbg(loc: *mut RocStr, msg: *mut RocStr) {
+    eprintln!("[{}] {}", &*loc, &*msg);
+}
+
+#[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)
+}
diff --git a/crates/glue/tests/fixtures/multiple-modules/Dep1.roc b/crates/glue/tests/fixtures/multiple-modules/Dep1.roc
new file mode 100644
index 0000000000..291dfd66ac
--- /dev/null
+++ b/crates/glue/tests/fixtures/multiple-modules/Dep1.roc
@@ -0,0 +1,5 @@
+interface Dep1 exposes [DepStr1, string] imports []
+
+DepStr1 := [S Str]
+
+string = \s -> @DepStr1 (S s)
diff --git a/crates/glue/tests/fixtures/multiple-modules/Dep2.roc b/crates/glue/tests/fixtures/multiple-modules/Dep2.roc
new file mode 100644
index 0000000000..99f00e08c1
--- /dev/null
+++ b/crates/glue/tests/fixtures/multiple-modules/Dep2.roc
@@ -0,0 +1,5 @@
+interface Dep2 exposes [DepStr2, string] imports []
+
+DepStr2 := [R Str]
+
+string = \s -> @DepStr2 (R s)
diff --git a/crates/glue/tests/fixtures/multiple-modules/app.roc b/crates/glue/tests/fixtures/multiple-modules/app.roc
new file mode 100644
index 0000000000..da8a540581
--- /dev/null
+++ b/crates/glue/tests/fixtures/multiple-modules/app.roc
@@ -0,0 +1,6 @@
+app "app"
+    packages { pf: "platform.roc" }
+    imports [pf.Dep1, pf.Dep2]
+    provides [main] to pf
+
+main = { s1: Dep1.string "hello", s2: Dep2.string "world" }
diff --git a/crates/glue/tests/fixtures/multiple-modules/platform.roc b/crates/glue/tests/fixtures/multiple-modules/platform.roc
new file mode 100644
index 0000000000..016bb3fc13
--- /dev/null
+++ b/crates/glue/tests/fixtures/multiple-modules/platform.roc
@@ -0,0 +1,11 @@
+platform "test-platform"
+    requires {} { main : _ }
+    exposes []
+    packages {}
+    imports [Dep1, Dep2]
+    provides [mainForHost]
+
+Combined : { s1 : Dep1.DepStr1, s2 : Dep2.DepStr2 }
+
+mainForHost : Combined
+mainForHost = main
diff --git a/crates/glue/tests/fixtures/multiple-modules/src/lib.rs b/crates/glue/tests/fixtures/multiple-modules/src/lib.rs
new file mode 100644
index 0000000000..8f0194fa0d
--- /dev/null
+++ b/crates/glue/tests/fixtures/multiple-modules/src/lib.rs
@@ -0,0 +1,85 @@
+use indoc::indoc;
+use roc_app;
+use roc_std::RocStr;
+
+#[no_mangle]
+pub extern "C" fn rust_main() {
+    use std::cmp::Ordering;
+    use std::collections::hash_set::HashSet;
+
+    let tag_union = roc_app::mainForHost();
+
+    // Verify that it has all the expected traits.
+
+    assert!(tag_union == tag_union); // PartialEq
+    assert!(tag_union.clone() == tag_union.clone()); // Clone
+
+    assert!(tag_union.partial_cmp(&tag_union) == Some(Ordering::Equal)); // PartialOrd
+    assert!(tag_union.cmp(&tag_union) == Ordering::Equal); // Ord
+
+    print!(
+        indoc!(
+            r#"
+                combined was: {:?}
+            "#
+        ),
+        tag_union,
+    ); // Debug
+
+    let mut set = HashSet::new();
+
+    set.insert(tag_union.clone()); // Eq, Hash
+    set.insert(tag_union);
+
+    assert_eq!(set.len(), 1);
+}
+
+// Externs required by roc_std and by the Roc app
+
+use core::ffi::c_void;
+use std::ffi::CStr;
+use std::os::raw::c_char;
+
+#[no_mangle]
+pub unsafe extern "C" fn roc_alloc(size: usize, _alignment: u32) -> *mut c_void {
+    return libc::malloc(size);
+}
+
+#[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 {
+    return libc::realloc(c_ptr, new_size);
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn roc_dealloc(c_ptr: *mut c_void, _alignment: u32) {
+    return libc::free(c_ptr);
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn roc_panic(msg: *mut RocStr, tag_id: u32) {
+    match tag_id {
+        0 => {
+            eprintln!("Roc standard library hit a panic: {}", &*msg);
+        }
+        1 => {
+            eprintln!("Application hit a panic: {}", &*msg);
+        }
+        _ => unreachable!(),
+    }
+    std::process::exit(1);
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn roc_dbg(loc: *mut RocStr, msg: *mut RocStr) {
+    eprintln!("[{}] {}", &*loc, &*msg);
+}
+
+#[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)
+}
diff --git a/crates/glue/tests/fixtures/nested-record/app.roc b/crates/glue/tests/fixtures/nested-record/app.roc
new file mode 100644
index 0000000000..3d90d82a22
--- /dev/null
+++ b/crates/glue/tests/fixtures/nested-record/app.roc
@@ -0,0 +1,6 @@
+app "app"
+    packages { pf: "platform.roc" }
+    imports []
+    provides [main] to pf
+
+main = { x: { a: 5, b: 24 }, y: "foo", z: [1, 2] }
diff --git a/bindgen/tests/fixtures/nested-record/Package-Config.roc b/crates/glue/tests/fixtures/nested-record/platform.roc
similarity index 100%
rename from bindgen/tests/fixtures/nested-record/Package-Config.roc
rename to crates/glue/tests/fixtures/nested-record/platform.roc
diff --git a/crates/glue/tests/fixtures/nested-record/src/lib.rs b/crates/glue/tests/fixtures/nested-record/src/lib.rs
new file mode 100644
index 0000000000..1263d2341b
--- /dev/null
+++ b/crates/glue/tests/fixtures/nested-record/src/lib.rs
@@ -0,0 +1,85 @@
+use roc_app;
+use roc_std::RocStr;
+
+#[no_mangle]
+pub extern "C" fn rust_main() {
+    use std::cmp::Ordering;
+
+    let outer = roc_app::mainForHost();
+
+    // Verify that `inner` has all the expected traits.
+    {
+        let inner = outer.x;
+
+        assert!(inner == inner); // PartialEq
+        assert!(inner.clone() == inner.clone()); // Clone
+
+        // Since this is a move, later uses of `inner` will fail unless `inner` has Copy
+        let inner2 = inner; // Copy
+
+        assert!(inner2 != Default::default()); // Default
+        assert!(inner.partial_cmp(&inner) == Some(Ordering::Equal)); // PartialOrd
+    }
+
+    // Verify that `outer` has all the expected traits.
+    {
+        assert!(outer == outer); // PartialEq
+        assert!(outer.clone() == outer.clone()); // Clone
+        assert!(outer != Default::default()); // Default
+        assert!(outer.partial_cmp(&outer) == Some(Ordering::Equal)); // PartialOrd
+    }
+
+    println!("Record was: {:?}", outer);
+
+    std::process::exit(0);
+}
+
+// Externs required by roc_std and by the Roc app
+
+use core::ffi::c_void;
+use std::ffi::CStr;
+use std::os::raw::c_char;
+
+#[no_mangle]
+pub unsafe extern "C" fn roc_alloc(size: usize, _alignment: u32) -> *mut c_void {
+    return libc::malloc(size);
+}
+
+#[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 {
+    return libc::realloc(c_ptr, new_size);
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn roc_dealloc(c_ptr: *mut c_void, _alignment: u32) {
+    return libc::free(c_ptr);
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn roc_panic(msg: *mut RocStr, tag_id: u32) {
+    match tag_id {
+        0 => {
+            eprintln!("Roc standard library hit a panic: {}", &*msg);
+        }
+        1 => {
+            eprintln!("Application hit a panic: {}", &*msg);
+        }
+        _ => unreachable!(),
+    }
+    std::process::exit(1);
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn roc_dbg(loc: *mut RocStr, msg: *mut RocStr) {
+    eprintln!("[{}] {}", &*loc, &*msg);
+}
+
+#[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)
+}
diff --git a/crates/glue/tests/fixtures/nonnullable-unwrapped/app.roc b/crates/glue/tests/fixtures/nonnullable-unwrapped/app.roc
new file mode 100644
index 0000000000..ebdf4581f3
--- /dev/null
+++ b/crates/glue/tests/fixtures/nonnullable-unwrapped/app.roc
@@ -0,0 +1,6 @@
+app "app"
+    packages { pf: "platform.roc" }
+    imports []
+    provides [main] to pf
+
+main = Tree "root" [Tree "leaf1" [], Tree "leaf2" []]
diff --git a/crates/glue/tests/fixtures/nonnullable-unwrapped/platform.roc b/crates/glue/tests/fixtures/nonnullable-unwrapped/platform.roc
new file mode 100644
index 0000000000..6765fbf525
--- /dev/null
+++ b/crates/glue/tests/fixtures/nonnullable-unwrapped/platform.roc
@@ -0,0 +1,11 @@
+platform "test-platform"
+    requires {} { main : _ }
+    exposes []
+    packages {}
+    imports []
+    provides [mainForHost]
+
+StrRoseTree : [Tree Str (List StrRoseTree)]
+
+mainForHost : StrRoseTree
+mainForHost = main
diff --git a/crates/glue/tests/fixtures/nonnullable-unwrapped/src/lib.rs b/crates/glue/tests/fixtures/nonnullable-unwrapped/src/lib.rs
new file mode 100644
index 0000000000..ca96c8d6fe
--- /dev/null
+++ b/crates/glue/tests/fixtures/nonnullable-unwrapped/src/lib.rs
@@ -0,0 +1,100 @@
+use roc_app;
+
+use indoc::indoc;
+use roc_app::StrRoseTree;
+use roc_std::{RocList, RocStr};
+
+extern "C" {
+    #[link_name = "roc__mainForHost_1_exposed_generic"]
+    fn roc_main(_: *mut StrRoseTree);
+}
+
+#[no_mangle]
+pub extern "C" fn rust_main() {
+    use std::cmp::Ordering;
+    use std::collections::hash_set::HashSet;
+
+    let tag_union = unsafe {
+        let mut ret: core::mem::MaybeUninit = core::mem::MaybeUninit::uninit();
+
+        roc_main(ret.as_mut_ptr());
+
+        ret.assume_init()
+    };
+
+    // Verify that it has all the expected traits.
+
+    assert!(tag_union == tag_union); // PartialEq
+    assert!(tag_union.clone() == tag_union.clone()); // Clone
+
+    assert!(tag_union.partial_cmp(&tag_union) == Some(Ordering::Equal)); // PartialOrd
+    assert!(tag_union.cmp(&tag_union) == Ordering::Equal); // Ord
+
+    print!(
+        indoc!(
+            r#"
+                tag_union was: {:?}
+                Tree "foo" [] is: {:?}
+            "#
+        ),
+        tag_union,
+        StrRoseTree::Tree("foo".into(), RocList::empty())
+    ); // Debug
+
+    let mut set = HashSet::new();
+
+    set.insert(tag_union.clone()); // Eq, Hash
+    set.insert(tag_union);
+
+    assert_eq!(set.len(), 1);
+}
+
+// Externs required by roc_std and by the Roc app
+
+use core::ffi::c_void;
+use std::ffi::CStr;
+use std::os::raw::c_char;
+
+#[no_mangle]
+pub unsafe extern "C" fn roc_alloc(size: usize, _alignment: u32) -> *mut c_void {
+    return libc::malloc(size);
+}
+
+#[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 {
+    return libc::realloc(c_ptr, new_size);
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn roc_dealloc(c_ptr: *mut c_void, _alignment: u32) {
+    return libc::free(c_ptr);
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn roc_panic(msg: *mut RocStr, tag_id: u32) {
+    match tag_id {
+        0 => {
+            eprintln!("Roc standard library hit a panic: {}", &*msg);
+        }
+        1 => {
+            eprintln!("Application hit a panic: {}", &*msg);
+        }
+        _ => unreachable!(),
+    }
+    std::process::exit(1);
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn roc_dbg(loc: *mut RocStr, msg: *mut RocStr) {
+    eprintln!("[{}] {}", &*loc, &*msg);
+}
+
+#[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)
+}
diff --git a/crates/glue/tests/fixtures/nullable-unwrapped/app.roc b/crates/glue/tests/fixtures/nullable-unwrapped/app.roc
new file mode 100644
index 0000000000..b2a570c398
--- /dev/null
+++ b/crates/glue/tests/fixtures/nullable-unwrapped/app.roc
@@ -0,0 +1,6 @@
+app "app"
+    packages { pf: "platform.roc" }
+    imports []
+    provides [main] to pf
+
+main = Cons "World!" (Cons "Hello " Nil)
diff --git a/bindgen/tests/fixtures/nullable-unwrapped/Package-Config.roc b/crates/glue/tests/fixtures/nullable-unwrapped/platform.roc
similarity index 100%
rename from bindgen/tests/fixtures/nullable-unwrapped/Package-Config.roc
rename to crates/glue/tests/fixtures/nullable-unwrapped/platform.roc
diff --git a/crates/glue/tests/fixtures/nullable-unwrapped/src/lib.rs b/crates/glue/tests/fixtures/nullable-unwrapped/src/lib.rs
new file mode 100644
index 0000000000..dd2f6a92fd
--- /dev/null
+++ b/crates/glue/tests/fixtures/nullable-unwrapped/src/lib.rs
@@ -0,0 +1,92 @@
+use roc_app;
+
+use indoc::indoc;
+use roc_app::StrConsList;
+use roc_std::RocStr;
+
+#[no_mangle]
+pub extern "C" fn rust_main() {
+    use std::cmp::Ordering;
+    use std::collections::hash_set::HashSet;
+
+    let tag_union = roc_app::mainForHost();
+
+    // Verify that it has all the expected traits.
+
+    assert!(tag_union == tag_union); // PartialEq
+
+    assert!(tag_union.clone() == tag_union.clone()); // Clone
+
+    assert!(tag_union.partial_cmp(&tag_union) == Some(Ordering::Equal)); // PartialOrd
+    assert!(tag_union.cmp(&tag_union) == Ordering::Equal); // Ord
+
+    print!(
+        indoc!(
+            r#"
+                tag_union was: {:?}
+                `Cons "small str" Nil` is: {:?}
+                `Nil` is: {:?}
+            "#
+        ),
+        tag_union,
+        StrConsList::Cons("small str".into(), StrConsList::Nil()),
+        StrConsList::Nil(),
+    ); // Debug
+
+    let mut set = HashSet::new();
+
+    set.insert(tag_union.clone()); // Eq, Hash
+    set.insert(tag_union);
+
+    assert_eq!(set.len(), 1);
+}
+
+// Externs required by roc_std and by the Roc app
+
+use core::ffi::c_void;
+use std::ffi::CStr;
+use std::os::raw::c_char;
+
+#[no_mangle]
+pub unsafe extern "C" fn roc_alloc(size: usize, _alignment: u32) -> *mut c_void {
+    return libc::malloc(size);
+}
+
+#[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 {
+    return libc::realloc(c_ptr, new_size);
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn roc_dealloc(c_ptr: *mut c_void, _alignment: u32) {
+    return libc::free(c_ptr);
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn roc_panic(msg: *mut RocStr, tag_id: u32) {
+    match tag_id {
+        0 => {
+            eprintln!("Roc standard library hit a panic: {}", &*msg);
+        }
+        1 => {
+            eprintln!("Application hit a panic: {}", &*msg);
+        }
+        _ => unreachable!(),
+    }
+    std::process::exit(1);
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn roc_dbg(loc: *mut RocStr, msg: *mut RocStr) {
+    eprintln!("[{}] {}", &*loc, &*msg);
+}
+
+#[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)
+}
diff --git a/crates/glue/tests/fixtures/nullable-wrapped/app.roc b/crates/glue/tests/fixtures/nullable-wrapped/app.roc
new file mode 100644
index 0000000000..ca62d400b5
--- /dev/null
+++ b/crates/glue/tests/fixtures/nullable-wrapped/app.roc
@@ -0,0 +1,6 @@
+app "app"
+    packages { pf: "platform.roc" }
+    imports []
+    provides [main] to pf
+
+main = More "foo" (More "bar" Empty)
diff --git a/crates/glue/tests/fixtures/nullable-wrapped/platform.roc b/crates/glue/tests/fixtures/nullable-wrapped/platform.roc
new file mode 100644
index 0000000000..dc3d2283c3
--- /dev/null
+++ b/crates/glue/tests/fixtures/nullable-wrapped/platform.roc
@@ -0,0 +1,11 @@
+platform "test-platform"
+    requires {} { main : _ }
+    exposes []
+    packages {}
+    imports []
+    provides [mainForHost]
+
+StrFingerTree : [Empty, Single Str, More Str StrFingerTree]
+
+mainForHost : {} -> StrFingerTree
+mainForHost = \{} -> main
diff --git a/crates/glue/tests/fixtures/nullable-wrapped/src/lib.rs b/crates/glue/tests/fixtures/nullable-wrapped/src/lib.rs
new file mode 100644
index 0000000000..0ef35c17c1
--- /dev/null
+++ b/crates/glue/tests/fixtures/nullable-wrapped/src/lib.rs
@@ -0,0 +1,111 @@
+use roc_app;
+
+use indoc::indoc;
+use roc_app::StrFingerTree;
+use roc_std::RocStr;
+
+extern "C" {
+    #[link_name = "roc__mainForHost_1_exposed_generic"]
+    fn roc_main(_: *mut StrFingerTree);
+}
+
+#[no_mangle]
+pub extern "C" fn rust_main() {
+    use std::cmp::Ordering;
+    use std::collections::hash_set::HashSet;
+
+    let tag_union = roc_app::mainForHost();
+
+    // Eq
+    assert!(StrFingerTree::Empty() == StrFingerTree::Empty());
+    assert!(StrFingerTree::Empty() != tag_union);
+    assert!(
+        StrFingerTree::Single(RocStr::from("foo")) == StrFingerTree::Single(RocStr::from("foo"))
+    );
+    assert!(StrFingerTree::Single(RocStr::from("foo")) != StrFingerTree::Empty());
+
+    // Verify that it has all the expected traits.
+    assert!(tag_union == tag_union); // PartialEq
+    assert!(tag_union.clone() == tag_union.clone()); // Clone
+    assert!(StrFingerTree::Empty().clone() == StrFingerTree::Empty()); // Clone
+
+    assert!(tag_union.partial_cmp(&tag_union) == Some(Ordering::Equal)); // PartialOrd
+    assert!(tag_union.cmp(&tag_union) == Ordering::Equal); // Ord
+
+    print!(
+        indoc!(
+            r#"
+                tag_union was: {:?}
+                `More "small str" (Single "other str")` is: {:?}
+                `More "small str" Empty` is: {:?}
+                `Single "small str"` is: {:?}
+                `Empty` is: {:?}
+            "#
+        ),
+        tag_union,
+        StrFingerTree::More(
+            "small str".into(),
+            StrFingerTree::Single("other str".into()),
+        ),
+        StrFingerTree::More("small str".into(), StrFingerTree::Empty()),
+        StrFingerTree::Single("small str".into()),
+        StrFingerTree::Empty(),
+    ); // Debug
+
+    let mut set = HashSet::new();
+
+    set.insert(tag_union.clone()); // Eq, Hash
+    set.insert(tag_union);
+
+    assert_eq!(set.len(), 1);
+}
+
+// Externs required by roc_std and by the Roc app
+
+use core::ffi::c_void;
+use std::ffi::CStr;
+use std::os::raw::c_char;
+
+#[no_mangle]
+pub unsafe extern "C" fn roc_alloc(size: usize, _alignment: u32) -> *mut c_void {
+    return libc::malloc(size);
+}
+
+#[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 {
+    return libc::realloc(c_ptr, new_size);
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn roc_dealloc(c_ptr: *mut c_void, _alignment: u32) {
+    return libc::free(c_ptr);
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn roc_panic(msg: *mut RocStr, tag_id: u32) {
+    match tag_id {
+        0 => {
+            eprintln!("Roc standard library hit a panic: {}", &*msg);
+        }
+        1 => {
+            eprintln!("Application hit a panic: {}", &*msg);
+        }
+        _ => unreachable!(),
+    }
+    std::process::exit(1);
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn roc_dbg(loc: *mut RocStr, msg: *mut RocStr) {
+    eprintln!("[{}] {}", &*loc, &*msg);
+}
+
+#[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)
+}
diff --git a/crates/glue/tests/fixtures/option/app.roc b/crates/glue/tests/fixtures/option/app.roc
new file mode 100644
index 0000000000..db5d87f793
--- /dev/null
+++ b/crates/glue/tests/fixtures/option/app.roc
@@ -0,0 +1,11 @@
+app "app"
+    packages { pf: "platform.roc" }
+    imports []
+    provides [main] to pf
+
+main : Bool -> [ Some Str, None ] 
+main = \returnStr ->
+    if returnStr then
+        Some "Hello World!"
+    else
+        None
diff --git a/crates/glue/tests/fixtures/option/platform.roc b/crates/glue/tests/fixtures/option/platform.roc
new file mode 100644
index 0000000000..bfa9a5f9f8
--- /dev/null
+++ b/crates/glue/tests/fixtures/option/platform.roc
@@ -0,0 +1,9 @@
+platform "test-platform"
+    requires {} { main : Bool -> [ Some Str, None ] }
+    exposes []
+    packages {}
+    imports []
+    provides [mainForHost]
+
+mainForHost : Bool -> [ Some Str, None ]
+mainForHost = \u -> main u
diff --git a/crates/glue/tests/fixtures/option/src/lib.rs b/crates/glue/tests/fixtures/option/src/lib.rs
new file mode 100644
index 0000000000..c5e565c75f
--- /dev/null
+++ b/crates/glue/tests/fixtures/option/src/lib.rs
@@ -0,0 +1,61 @@
+use roc_app;
+use roc_std::RocStr;
+
+#[no_mangle]
+pub extern "C" fn rust_main() {
+    let string = roc_app::mainForHost(true);
+    println!("Answer was: {:?}", string.unwrap_Some()); // Debug
+                                                        //
+    let integer = roc_app::mainForHost(false);
+    println!("Answer was: {:?}", integer.discriminant()); // Debug
+}
+
+// Externs required by roc_std and by the Roc app
+
+use core::ffi::c_void;
+use std::ffi::CStr;
+use std::os::raw::c_char;
+
+#[no_mangle]
+pub unsafe extern "C" fn roc_alloc(size: usize, _alignment: u32) -> *mut c_void {
+    return libc::malloc(size);
+}
+
+#[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 {
+    return libc::realloc(c_ptr, new_size);
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn roc_dealloc(c_ptr: *mut c_void, _alignment: u32) {
+    return libc::free(c_ptr);
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn roc_panic(msg: *mut RocStr, tag_id: u32) {
+    match tag_id {
+        0 => {
+            eprintln!("Roc standard library hit a panic: {}", &*msg);
+        }
+        1 => {
+            eprintln!("Application hit a panic: {}", &*msg);
+        }
+        _ => unreachable!(),
+    }
+    std::process::exit(1);
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn roc_dbg(loc: *mut RocStr, msg: *mut RocStr) {
+    eprintln!("[{}] {}", &*loc, &*msg);
+}
+
+#[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)
+}
diff --git a/crates/glue/tests/fixtures/rocresult/app.roc b/crates/glue/tests/fixtures/rocresult/app.roc
new file mode 100644
index 0000000000..9e6788708f
--- /dev/null
+++ b/crates/glue/tests/fixtures/rocresult/app.roc
@@ -0,0 +1,11 @@
+app "app"
+    packages { pf: "platform.roc" }
+    imports []
+    provides [main] to pf
+
+main : Bool -> Result Str I32
+main = \returnStr ->
+    if returnStr then
+        Ok "Hello World!"
+    else
+        Err 42
diff --git a/crates/glue/tests/fixtures/rocresult/platform.roc b/crates/glue/tests/fixtures/rocresult/platform.roc
new file mode 100644
index 0000000000..bdb6635c38
--- /dev/null
+++ b/crates/glue/tests/fixtures/rocresult/platform.roc
@@ -0,0 +1,9 @@
+platform "test-platform"
+    requires {} { main : Bool -> Result Str I32 }
+    exposes []
+    packages {}
+    imports []
+    provides [mainForHost]
+
+mainForHost : Bool -> Result Str I32
+mainForHost = \u -> main u
diff --git a/crates/glue/tests/fixtures/rocresult/src/lib.rs b/crates/glue/tests/fixtures/rocresult/src/lib.rs
new file mode 100644
index 0000000000..27092d4d68
--- /dev/null
+++ b/crates/glue/tests/fixtures/rocresult/src/lib.rs
@@ -0,0 +1,61 @@
+use roc_app;
+use roc_std::RocStr;
+
+#[no_mangle]
+pub extern "C" fn rust_main() {
+    let string = roc_app::mainForHost(true);
+    println!("Answer was: {:?}", string); // Debug
+                                          //
+    let integer = roc_app::mainForHost(false);
+    println!("Answer was: {:?}", integer); // Debug
+}
+
+// Externs required by roc_std and by the Roc app
+
+use core::ffi::c_void;
+use std::ffi::CStr;
+use std::os::raw::c_char;
+
+#[no_mangle]
+pub unsafe extern "C" fn roc_alloc(size: usize, _alignment: u32) -> *mut c_void {
+    return libc::malloc(size);
+}
+
+#[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 {
+    return libc::realloc(c_ptr, new_size);
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn roc_dealloc(c_ptr: *mut c_void, _alignment: u32) {
+    return libc::free(c_ptr);
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn roc_panic(msg: *mut RocStr, tag_id: u32) {
+    match tag_id {
+        0 => {
+            eprintln!("Roc standard library hit a panic: {}", &*msg);
+        }
+        1 => {
+            eprintln!("Application hit a panic: {}", &*msg);
+        }
+        _ => unreachable!(),
+    }
+    std::process::exit(1);
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn roc_dbg(loc: *mut RocStr, msg: *mut RocStr) {
+    eprintln!("[{}] {}", &*loc, &*msg);
+}
+
+#[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)
+}
diff --git a/crates/glue/tests/fixtures/single-tag-union/app.roc b/crates/glue/tests/fixtures/single-tag-union/app.roc
new file mode 100644
index 0000000000..be494c5fad
--- /dev/null
+++ b/crates/glue/tests/fixtures/single-tag-union/app.roc
@@ -0,0 +1,6 @@
+app "app"
+    packages { pf: "platform.roc" }
+    imports []
+    provides [main] to pf
+
+main = OneTag
diff --git a/crates/glue/tests/fixtures/single-tag-union/platform.roc b/crates/glue/tests/fixtures/single-tag-union/platform.roc
new file mode 100644
index 0000000000..c6c13c5e58
--- /dev/null
+++ b/crates/glue/tests/fixtures/single-tag-union/platform.roc
@@ -0,0 +1,11 @@
+platform "test-platform"
+    requires {} { main : _ }
+    exposes []
+    packages {}
+    imports []
+    provides [mainForHost]
+
+SingleTagUnion : [OneTag]
+
+mainForHost : SingleTagUnion
+mainForHost = main
diff --git a/crates/glue/tests/fixtures/single-tag-union/src/lib.rs b/crates/glue/tests/fixtures/single-tag-union/src/lib.rs
new file mode 100644
index 0000000000..a59c631a9f
--- /dev/null
+++ b/crates/glue/tests/fixtures/single-tag-union/src/lib.rs
@@ -0,0 +1,87 @@
+use roc_app;
+
+use indoc::indoc;
+use roc_app::SingleTagUnion;
+use roc_std::RocStr;
+
+#[no_mangle]
+pub extern "C" fn rust_main() {
+    use std::cmp::Ordering;
+    use std::collections::hash_set::HashSet;
+
+    let tag_union = roc_app::mainForHost();
+
+    // Verify that it has all the expected traits.
+
+    assert!(tag_union == SingleTagUnion::OneTag); // PartialEq
+    assert!(tag_union.clone() == tag_union.clone()); // Clone
+
+    assert!(tag_union.partial_cmp(&tag_union) == Some(Ordering::Equal)); // PartialOrd
+    assert!(tag_union.cmp(&tag_union) == Ordering::Equal); // Ord
+
+    print!(
+        indoc!(
+            r#"
+                tag_union was: {:?}
+            "#
+        ),
+        tag_union,
+    ); // Debug
+
+    let mut set = HashSet::new();
+
+    set.insert(tag_union.clone()); // Eq, Hash
+    set.insert(tag_union);
+
+    assert_eq!(set.len(), 1);
+}
+
+// Externs required by roc_std and by the Roc app
+
+use core::ffi::c_void;
+use std::ffi::CStr;
+use std::os::raw::c_char;
+
+#[no_mangle]
+pub unsafe extern "C" fn roc_alloc(size: usize, _alignment: u32) -> *mut c_void {
+    return libc::malloc(size);
+}
+
+#[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 {
+    return libc::realloc(c_ptr, new_size);
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn roc_dealloc(c_ptr: *mut c_void, _alignment: u32) {
+    return libc::free(c_ptr);
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn roc_panic(msg: *mut RocStr, tag_id: u32) {
+    match tag_id {
+        0 => {
+            eprintln!("Roc standard library hit a panic: {}", &*msg);
+        }
+        1 => {
+            eprintln!("Application hit a panic: {}", &*msg);
+        }
+        _ => unreachable!(),
+    }
+    std::process::exit(1);
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn roc_dbg(loc: *mut RocStr, msg: *mut RocStr) {
+    eprintln!("[{}] {}", &*loc, &*msg);
+}
+
+#[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)
+}
diff --git a/crates/glue/tests/fixtures/union-with-padding/app.roc b/crates/glue/tests/fixtures/union-with-padding/app.roc
new file mode 100644
index 0000000000..f0e650c59b
--- /dev/null
+++ b/crates/glue/tests/fixtures/union-with-padding/app.roc
@@ -0,0 +1,6 @@
+app "app"
+    packages { pf: "platform.roc" }
+    imports []
+    provides [main] to pf
+
+main = Foo "This is a test"
diff --git a/crates/glue/tests/fixtures/union-with-padding/platform.roc b/crates/glue/tests/fixtures/union-with-padding/platform.roc
new file mode 100644
index 0000000000..ba923679cb
--- /dev/null
+++ b/crates/glue/tests/fixtures/union-with-padding/platform.roc
@@ -0,0 +1,17 @@
+platform "test-platform"
+    requires {} { main : _ }
+    exposes []
+    packages {}
+    imports []
+    provides [mainForHost]
+
+# This case is important to test because the U128
+# gives the whole struct an alignment of 16, but the
+# Str is the largest variant, so the whole union has
+# a size of 32 (due to alignment, rounded up from Str's 24),
+# and the discriminant is stored in the 8+ bytes of padding
+# that all variants have.
+NonRecursive : [Foo Str, Bar U128, Blah I32, Baz]
+
+mainForHost : {} -> NonRecursive
+mainForHost = \{} -> main
diff --git a/crates/glue/tests/fixtures/union-with-padding/src/lib.rs b/crates/glue/tests/fixtures/union-with-padding/src/lib.rs
new file mode 100644
index 0000000000..42e32770d3
--- /dev/null
+++ b/crates/glue/tests/fixtures/union-with-padding/src/lib.rs
@@ -0,0 +1,92 @@
+use roc_app;
+
+use roc_app::NonRecursive;
+use roc_std::RocStr;
+
+extern "C" {
+    #[link_name = "roc__mainForHost_1_exposed_generic"]
+    fn roc_main(_: *mut NonRecursive);
+}
+
+#[no_mangle]
+pub extern "C" fn rust_main() {
+    use std::cmp::Ordering;
+    use std::collections::hash_set::HashSet;
+
+    let tag_union = roc_app::mainForHost();
+
+    // Verify that it has all the expected traits.
+
+    assert!(tag_union == tag_union); // PartialEq
+    assert!(tag_union.clone() == tag_union.clone()); // Clone
+
+    assert!(tag_union.partial_cmp(&tag_union) == Some(Ordering::Equal)); // PartialOrd
+    assert!(tag_union.cmp(&tag_union) == Ordering::Equal); // Ord
+
+    println!(
+        "tag_union was: {:?}\n`Foo \"small str\"` is: {:?}\n`Foo \"A long enough string to not be small\"` is: {:?}\n`Bar 123` is: {:?}\n`Baz` is: {:?}\n`Blah 456` is: {:?}",
+        tag_union,
+        NonRecursive::Foo("small str".into()),
+        NonRecursive::Foo("A long enough string to not be small".into()),
+        NonRecursive::Bar(123),
+        NonRecursive::Baz(),
+        NonRecursive::Blah(456),
+    ); // Debug
+
+    let mut set = HashSet::new();
+
+    set.insert(tag_union.clone()); // Eq, Hash
+    set.insert(tag_union);
+
+    assert_eq!(set.len(), 1);
+}
+
+// Externs required by roc_std and by the Roc app
+
+use core::ffi::c_void;
+use std::ffi::CStr;
+use std::os::raw::c_char;
+
+#[no_mangle]
+pub unsafe extern "C" fn roc_alloc(size: usize, _alignment: u32) -> *mut c_void {
+    return libc::malloc(size);
+}
+
+#[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 {
+    return libc::realloc(c_ptr, new_size);
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn roc_dealloc(c_ptr: *mut c_void, _alignment: u32) {
+    return libc::free(c_ptr);
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn roc_panic(msg: *mut RocStr, tag_id: u32) {
+    match tag_id {
+        0 => {
+            eprintln!("Roc standard library hit a panic: {}", &*msg);
+        }
+        1 => {
+            eprintln!("Application hit a panic: {}", &*msg);
+        }
+        _ => unreachable!(),
+    }
+    std::process::exit(1);
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn roc_dbg(loc: *mut RocStr, msg: *mut RocStr) {
+    eprintln!("[{}] {}", &*loc, &*msg);
+}
+
+#[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)
+}
diff --git a/crates/glue/tests/fixtures/union-without-padding/app.roc b/crates/glue/tests/fixtures/union-without-padding/app.roc
new file mode 100644
index 0000000000..f0e650c59b
--- /dev/null
+++ b/crates/glue/tests/fixtures/union-without-padding/app.roc
@@ -0,0 +1,6 @@
+app "app"
+    packages { pf: "platform.roc" }
+    imports []
+    provides [main] to pf
+
+main = Foo "This is a test"
diff --git a/crates/glue/tests/fixtures/union-without-padding/platform.roc b/crates/glue/tests/fixtures/union-without-padding/platform.roc
new file mode 100644
index 0000000000..35ff29794e
--- /dev/null
+++ b/crates/glue/tests/fixtures/union-without-padding/platform.roc
@@ -0,0 +1,15 @@
+platform "test-platform"
+    requires {} { main : _ }
+    exposes []
+    packages {}
+    imports []
+    provides [mainForHost]
+
+# This case is important to test because there's no padding
+# after the largest variant, so the compiler adds an extra u8
+# (rounded up to alignment, so an an extra 8 bytes) in which
+# to store the discriminant. We have to generate glue code accordingly!
+NonRecursive : [Foo Str, Bar I64, Blah I32, Baz]
+
+mainForHost : {} -> NonRecursive
+mainForHost = \{} -> main
diff --git a/crates/glue/tests/fixtures/union-without-padding/src/lib.rs b/crates/glue/tests/fixtures/union-without-padding/src/lib.rs
new file mode 100644
index 0000000000..f424da0a9f
--- /dev/null
+++ b/crates/glue/tests/fixtures/union-without-padding/src/lib.rs
@@ -0,0 +1,89 @@
+use roc_app;
+use roc_std::RocStr;
+
+extern "C" {
+    #[link_name = "roc__mainForHost_1_exposed_generic"]
+    fn roc_main(_: *mut roc_app::NonRecursive);
+}
+
+#[no_mangle]
+pub extern "C" fn rust_main() {
+    use std::cmp::Ordering;
+    use std::collections::hash_set::HashSet;
+
+    let tag_union = roc_app::mainForHost();
+
+    // Verify that it has all the expected traits.
+
+    assert!(tag_union == tag_union); // PartialEq
+    assert!(tag_union.clone() == tag_union.clone()); // Clone
+
+    assert!(tag_union.partial_cmp(&tag_union) == Some(Ordering::Equal)); // PartialOrd
+    assert!(tag_union.cmp(&tag_union) == Ordering::Equal); // Ord
+
+    println!(
+        "tag_union was: {:?}\n`Foo \"small str\"` is: {:?}\n`Bar 123` is: {:?}\n`Baz` is: {:?}\n`Blah 456` is: {:?}",
+        tag_union,
+        roc_app::NonRecursive::Foo("small str".into()),
+        roc_app::NonRecursive::Bar(123),
+        roc_app::NonRecursive::Baz(),
+        roc_app::NonRecursive::Blah(456),
+    ); // Debug
+
+    let mut set = HashSet::new();
+
+    set.insert(tag_union.clone()); // Eq, Hash
+    set.insert(tag_union);
+
+    assert_eq!(set.len(), 1);
+}
+
+// Externs required by roc_std and by the Roc app
+
+use core::ffi::c_void;
+use std::ffi::CStr;
+use std::os::raw::c_char;
+
+#[no_mangle]
+pub unsafe extern "C" fn roc_alloc(size: usize, _alignment: u32) -> *mut c_void {
+    return libc::malloc(size);
+}
+
+#[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 {
+    return libc::realloc(c_ptr, new_size);
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn roc_dealloc(c_ptr: *mut c_void, _alignment: u32) {
+    return libc::free(c_ptr);
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn roc_panic(msg: *mut RocStr, tag_id: u32) {
+    match tag_id {
+        0 => {
+            eprintln!("Roc standard library hit a panic: {}", &*msg);
+        }
+        1 => {
+            eprintln!("Application hit a panic: {}", &*msg);
+        }
+        _ => unreachable!(),
+    }
+    std::process::exit(1);
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn roc_dbg(loc: *mut RocStr, msg: *mut RocStr) {
+    eprintln!("[{}] {}", &*loc, &*msg);
+}
+
+#[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)
+}
diff --git a/crates/glue/tests/helpers/mod.rs b/crates/glue/tests/helpers/mod.rs
new file mode 100644
index 0000000000..e75969625c
--- /dev/null
+++ b/crates/glue/tests/helpers/mod.rs
@@ -0,0 +1,35 @@
+use std::env;
+use std::path::PathBuf;
+
+#[allow(dead_code)]
+pub fn fixtures_dir(dir_name: &str) -> PathBuf {
+    let mut path = root_dir();
+
+    // Descend into cli/tests/fixtures/{dir_name}
+    path.push("crates");
+    path.push("glue");
+    path.push("tests");
+    path.push("fixtures");
+    path.push(dir_name);
+
+    path
+}
+
+#[allow(dead_code)]
+pub fn root_dir() -> PathBuf {
+    let mut path = env::current_exe().ok().unwrap();
+
+    // Get rid of the filename in target/debug/deps/cli_run-99c65e4e9a1fbd06
+    path.pop();
+
+    // If we're in deps/ get rid of deps/ in target/debug/deps/
+    if path.ends_with("deps") {
+        path.pop();
+    }
+
+    // Get rid of target/debug/ so we're back at the project root
+    path.pop();
+    path.pop();
+
+    path
+}
diff --git a/crates/glue/tests/test_glue_cli.rs b/crates/glue/tests/test_glue_cli.rs
new file mode 100644
index 0000000000..86e824b2e9
--- /dev/null
+++ b/crates/glue/tests/test_glue_cli.rs
@@ -0,0 +1,257 @@
+#[macro_use]
+extern crate pretty_assertions;
+
+#[macro_use]
+extern crate indoc;
+
+extern crate dircpy;
+extern crate roc_collections;
+
+mod helpers;
+
+#[cfg(test)]
+mod glue_cli_run {
+    use crate::helpers::fixtures_dir;
+    use cli_utils::helpers::{has_error, run_glue, run_roc, Out};
+    use std::fs;
+    use std::path::Path;
+
+    /// This macro does two things.
+    ///
+    /// First, it generates and runs a separate test for each of the given
+    /// expected stdout endings. Each of these should test a particular .roc file
+    /// in the fixtures/ directory. The fixtures themselves run assertions too, but
+    /// the stdout check verifies that we're actually running the code we think we are;
+    /// without it, it would be possible that the fixtures are just exiting without running
+    /// any assertions, and we would have no way to find out!
+    ///
+    /// Second, this generates an extra test which (non-recursively) traverses the
+    /// fixtures/ directory and verifies that each of the .roc files in there
+    /// has had a corresponding test generated in the previous step. This test
+    /// will fail if we ever add a new .roc file to fixtures/ and forget to
+    /// add a test for it here!
+    macro_rules! fixtures {
+        ($($test_name:ident:$fixture_dir:expr => $ends_with:expr,)+) => {
+            $(
+                #[test]
+                #[allow(non_snake_case)]
+                fn $test_name() {
+                    let dir = fixtures_dir($fixture_dir);
+
+                    generate_glue_for(&dir, std::iter::empty());
+
+                    let test_name_str = stringify!($test_name);
+
+                    // TODO after #5924 is fixed; remove this if
+                    if !(cfg!(target_os = "linux") && (test_name_str == "nullable_unwrapped" || test_name_str == "nullable_wrapped")) {
+                        let out = run_app(&dir.join("app.roc"), std::iter::empty());
+
+                        assert!(out.status.success());
+                        let ignorable = "🔨 Rebuilding platform...\n";
+                        let stderr = out.stderr.replacen(ignorable, "", 1);
+                        assert_eq!(stderr, "");
+                        assert!(
+                            out.stdout.ends_with($ends_with),
+                            "Unexpected stdout ending\n\n  expected:\n\n    {}\n\n  but stdout was:\n\n    {}",
+                            $ends_with,
+                            out.stdout
+                        );
+                    }
+                }
+            )*
+
+            #[test]
+            #[ignore]
+            fn all_fixtures_have_tests() {
+                use roc_collections::VecSet;
+
+                let mut all_fixtures: VecSet = VecSet::default();
+
+                $(
+                    all_fixtures.insert($fixture_dir.to_string());
+                )*
+
+                check_for_tests(&mut all_fixtures);
+            }
+        }
+    }
+
+    fixtures! {
+        basic_record:"basic-record" => "Record was: MyRcd { b: 42, a: 1995 }\n",
+        // TODO: re-enable this test. Currently it is flaking on macos x86-64 with a bad exit code.
+        // nested_record:"nested-record" => "Record was: Outer { y: \"foo\", z: [1, 2], x: Inner { b: 24.0, a: 5 } }\n",
+        // enumeration:"enumeration" => "tag_union was: MyEnum::Foo, Bar is: MyEnum::Bar, Baz is: MyEnum::Baz\n",
+        single_tag_union:"single-tag-union" => indoc!(r#"
+            tag_union was: SingleTagUnion::OneTag
+        "#),
+        union_with_padding:"union-with-padding" => indoc!(r#"
+            tag_union was: NonRecursive::Foo("This is a test")
+            `Foo "small str"` is: NonRecursive::Foo("small str")
+            `Foo "A long enough string to not be small"` is: NonRecursive::Foo("A long enough string to not be small")
+            `Bar 123` is: NonRecursive::Bar(123)
+            `Baz` is: NonRecursive::Baz(())
+            `Blah 456` is: NonRecursive::Blah(456)
+        "#),
+        union_without_padding:"union-without-padding" => indoc!(r#"
+            tag_union was: NonRecursive::Foo("This is a test")
+            `Foo "small str"` is: NonRecursive::Foo("small str")
+            `Bar 123` is: NonRecursive::Bar(123)
+            `Baz` is: NonRecursive::Baz(())
+            `Blah 456` is: NonRecursive::Blah(456)
+        "#), 
+        nullable_wrapped:"nullable-wrapped" => indoc!(r#"
+            tag_union was: StrFingerTree::More("foo", StrFingerTree::More("bar", StrFingerTree::Empty))
+            `More "small str" (Single "other str")` is: StrFingerTree::More("small str", StrFingerTree::Single("other str"))
+            `More "small str" Empty` is: StrFingerTree::More("small str", StrFingerTree::Empty)
+            `Single "small str"` is: StrFingerTree::Single("small str")
+            `Empty` is: StrFingerTree::Empty
+        "#),
+        nullable_unwrapped:"nullable-unwrapped" => indoc!(r#"
+            tag_union was: StrConsList::Cons("World!", StrConsList::Cons("Hello ", StrConsList::Nil))
+            `Cons "small str" Nil` is: StrConsList::Cons("small str", StrConsList::Nil)
+            `Nil` is: StrConsList::Nil
+        "#),
+        nonnullable_unwrapped:"nonnullable-unwrapped" => indoc!(r#"
+            tag_union was: StrRoseTree::Tree("root", [StrRoseTree::Tree("leaf1", []), StrRoseTree::Tree("leaf2", [])])
+            Tree "foo" [] is: StrRoseTree::Tree("foo", [])
+        "#),
+        basic_recursive_union:"basic-recursive-union" => indoc!(r#"
+            tag_union was: Expr::Concat(Expr::String("Hello, "), Expr::String("World!"))
+            `Concat (String "Hello, ") (String "World!")` is: Expr::Concat(Expr::String("Hello, "), Expr::String("World!"))
+            `String "this is a test"` is: Expr::String("this is a test")
+        "#),
+        advanced_recursive_union:"advanced-recursive-union" => indoc!(r#"
+            rbt was: Rbt { default: Job::Job(R1 { command: Command::Command(R2 { tool: Tool::SystemTool(R4 { name: "test", num: 42 }) }), inputFiles: ["foo"] }) }
+        "#),
+        list_recursive_union:"list-recursive-union" => indoc!(r#"
+            rbt was: Rbt { default: Job::Job(R1 { command: Command::Command(R2 { args: [], tool: Tool::SystemTool(R3 { name: "test" }) }), inputFiles: ["foo"], job: [] }) }
+        "#),
+        multiple_modules:"multiple-modules" => indoc!(r#"
+            combined was: Combined { s1: DepStr1::S("hello"), s2: DepStr2::R("world") }
+        "#),
+        arguments:"arguments" => indoc!(r#"
+            Answer was: 84
+        "#),
+        closures:"closures" => indoc!(r#"
+            Answer was: 672
+        "#),
+        rocresult:"rocresult" => indoc!(r#"
+            Answer was: RocOk(ManuallyDrop { value: "Hello World!" })
+            Answer was: RocErr(ManuallyDrop { value: 42 })
+        "#),
+        option:"option" => indoc!(r#"
+            Answer was: "Hello World!"
+            Answer was: discriminant_U1::None
+        "#),
+    }
+
+    fn check_for_tests(all_fixtures: &mut roc_collections::VecSet) {
+        use roc_collections::VecSet;
+
+        let fixtures = fixtures_dir("");
+        let entries = std::fs::read_dir(fixtures.as_path()).unwrap_or_else(|err| {
+            panic!(
+                "Error trying to read {} as a fixtures directory: {}",
+                fixtures.to_string_lossy(),
+                err
+            );
+        });
+
+        for entry in entries {
+            let entry = entry.unwrap();
+
+            if entry.file_type().unwrap().is_dir() {
+                let fixture_dir_name = entry.file_name().into_string().unwrap();
+
+                if !all_fixtures.remove(&fixture_dir_name) {
+                    panic!(
+                        "The glue fixture directory {} does not have any corresponding tests in test_glue_cli. Please add one, so if it ever stops working, we'll know about it right away!",
+                        entry.path().to_string_lossy()
+                    );
+                }
+            }
+        }
+
+        assert_eq!(all_fixtures, &mut VecSet::default());
+    }
+
+    fn generate_glue_for<'a, I: IntoIterator>(
+        platform_dir: &'a Path,
+        args: I,
+    ) -> Out {
+        let platform_module_path = platform_dir.join("platform.roc");
+        let glue_dir = platform_dir.join("test_glue");
+        let fixture_templates_dir = platform_dir
+            .parent()
+            .unwrap()
+            .parent()
+            .unwrap()
+            .join("fixture-templates");
+
+        // Copy the rust template from the templates directory into the fixture dir.
+        dircpy::CopyBuilder::new(fixture_templates_dir.join("rust"), platform_dir)
+            .overwrite(true) // overwrite any files that were already present
+            .run()
+            .unwrap();
+
+        // Delete the glue file to make sure we're actually regenerating it!
+        if glue_dir.exists() {
+            fs::remove_dir_all(&glue_dir)
+                .expect("Unable to remove test_glue dir in order to regenerate it in the test");
+        }
+
+        let rust_glue_spec = fixture_templates_dir
+            .parent()
+            .unwrap()
+            .parent()
+            .unwrap()
+            .join("src")
+            .join("RustGlue.roc");
+
+        // Generate a fresh test_glue for this platform
+        let glue_out = run_glue(
+            // converting these all to String avoids lifetime issues
+            std::iter::once("glue".to_string()).chain(
+                args.into_iter().map(|arg| arg.to_string()).chain([
+                    rust_glue_spec.to_str().unwrap().to_string(),
+                    glue_dir.to_str().unwrap().to_string(),
+                    platform_module_path.to_str().unwrap().to_string(),
+                ]),
+            ),
+        );
+
+        if has_error(&glue_out.stderr) {
+            panic!(
+                "`roc glue` command had unexpected stderr: {}",
+                glue_out.stderr
+            );
+        }
+
+        assert!(glue_out.status.success(), "bad status {glue_out:?}");
+
+        glue_out
+    }
+
+    fn run_app<'a, I: IntoIterator>(app_file: &'a Path, args: I) -> Out {
+        // Generate test_glue for this platform
+        let compile_out = run_roc(
+            // converting these all to String avoids lifetime issues
+            args.into_iter()
+                .map(|arg| arg.to_string())
+                .chain([app_file.to_str().unwrap().to_string()]),
+            &[],
+            &[],
+        );
+
+        if has_error(&compile_out.stderr) {
+            panic!(
+                "`roc` command had unexpected stderr: {}",
+                compile_out.stderr
+            );
+        }
+
+        assert!(compile_out.status.success(), "bad status {compile_out:?}");
+
+        compile_out
+    }
+}
diff --git a/crates/highlight/Cargo.toml b/crates/highlight/Cargo.toml
new file mode 100644
index 0000000000..b5b47385c8
--- /dev/null
+++ b/crates/highlight/Cargo.toml
@@ -0,0 +1,14 @@
+[package]
+name = "roc_highlight"
+description = "For syntax highlighting, starts with a string and returns HTML."
+
+authors.workspace = true
+edition.workspace = true
+license.workspace = true
+version.workspace = true
+
+[dependencies]
+html-escape = "0.2"
+roc_parse = { path = "../compiler/parse" }
+roc_region = { path = "../compiler/region" }
+
diff --git a/crates/highlight/src/lib.rs b/crates/highlight/src/lib.rs
new file mode 100644
index 0000000000..2f30e2b293
--- /dev/null
+++ b/crates/highlight/src/lib.rs
@@ -0,0 +1,116 @@
+use roc_parse::highlight::Token;
+use roc_region::all::Loc;
+
+pub fn highlight_roc_code(code: &str) -> String {
+    let buf = highlight(code);
+
+    format!("
{}
", buf.join("")) +} + +pub fn highlight_roc_code_inline(code: &str) -> String { + let buf = highlight(code); + + format!("{}", buf.join("")) +} + +pub fn highlight(code: &str) -> Vec { + let locations: Vec> = roc_parse::highlight::highlight(code); + let mut buf: Vec = Vec::new(); + let mut offset = 0; + + for location in locations { + let current_text = &code[offset..location.byte_range().end]; + + match location.value { + // Comments `#` and Documentation comments `##` + Token::LineComment | Token::DocComment => { + buf = push_html_span(buf, current_text, "comment"); + } + // Number, String, Tag, Type literals + Token::SingleQuote + | Token::String + | Token::UnicodeEscape + | Token::EscapedChar + | Token::Interpolated + | Token::Number => { + buf = push_html_span(buf, current_text, "literal"); + } + // Keywords and punctuation + Token::Keyword + | Token::Equals + | Token::Backslash + | Token::Pizza + | Token::Arrow + | Token::Backpass + | Token::ColonEquals + | Token::Colon + | Token::And + | Token::QuestionMark => { + buf = push_html_span(buf, current_text, "kw"); + } + // Operators + Token::Percent + | Token::Caret + | Token::Bang + | Token::BangEquals + | Token::Slash + | Token::DoubleSlash + | Token::Pipe + | Token::GreaterThan + | Token::GreaterThanEquals + | Token::Minus + | Token::LessThan + | Token::LessThanEquals + | Token::DoubleEquals + | Token::DoubleBar + | Token::Multiply + | Token::Plus + | Token::DoubleAnd => { + buf = push_html_span(buf, current_text, "op"); + } + // Delimieters + Token::Paren + | Token::Bracket + | Token::Brace + | Token::Comma + | Token::Bar + | Token::Decimal => { + buf = push_html_span(buf, current_text, "delimeter"); + } + // Types, Tags, and Modules + Token::UpperIdent | Token::AtSign => { + buf = push_html_span(buf, current_text, "upperident"); + } + // Variables modules and field names + Token::LowerIdent | Token::Underscore => { + buf = push_html_span(buf, current_text, "lowerident"); + } + // Anyting else that wasn't tokenised + Token::Error | Token::Other => { + buf = push_html(buf, current_text); + } + } + + offset = location.byte_range().end; + } + + buf +} + +fn push_html_span(mut buf: Vec, curr: &str, class: &str) -> Vec { + // html escape strings from source code + let escaped = html_escape::encode_text(curr); + + buf.push(format!("{escaped}")); + + buf +} + +fn push_html(mut buf: Vec, curr: &str) -> Vec { + // html escape strings from source code + let escaped = html_escape::encode_text(curr); + + buf.push(format!("{escaped}")); + + buf +} diff --git a/crates/lang_srv/Cargo.toml b/crates/lang_srv/Cargo.toml new file mode 100644 index 0000000000..93a034f081 --- /dev/null +++ b/crates/lang_srv/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "roc_lang_srv" +version = "0.0.1" +edition = "2021" + +[[bin]] +name = "roc_ls" +path = "src/server.rs" + +[dependencies] +roc_can = { path = "../compiler/can" } +roc_collections = { path = "../compiler/collections" } +roc_fmt = { path = "../compiler/fmt" } +roc_load = { path = "../compiler/load" } +roc_module = { path = "../compiler/module" } +roc_parse = { path = "../compiler/parse" } +roc_problem = { path = "../compiler/problem" } +roc_region = { path = "../compiler/region" } +roc_reporting = { path = "../reporting" } +roc_solve_problem = { path = "../compiler/solve_problem" } +roc_target = { path = "../compiler/roc_target" } +roc_types = { path = "../compiler/types" } +roc_packaging = {path = "../packaging"} + +bumpalo.workspace = true +parking_lot.workspace = true + +tower-lsp = "0.17.0" +tokio = { version = "1.20.1", features = [ "rt", "rt-multi-thread", "macros", "io-std" ] } diff --git a/crates/lang_srv/README.md b/crates/lang_srv/README.md new file mode 100644 index 0000000000..136aa77bc2 --- /dev/null +++ b/crates/lang_srv/README.md @@ -0,0 +1,81 @@ +# roc_ls + +This is a rudimentary language server for supporting basic editor usage in Roc. + +Support for the following LSP features are provided: + + + + +- Inline diagnostics +- Hover support to get types of values +- Go-to-definition + -
Example + + https://github.com/ayazhafiz/roc/assets/20735482/23a57d06-5b70-46f2-b0c4-5836eaec669b + +
+ - Note that go-to-definition for the builtins does not yet work. + - Go-to-definition for abilities resolves to their specialization, if one exists. + -
Example + + https://github.com/ayazhafiz/roc/assets/20735482/1ba98bf9-518b-4c47-b606-a6ce6767566f + +
+- Formatting Roc files on save + -
Example + + https://github.com/ayazhafiz/roc/assets/20735482/fbbe4bc1-64af-4c7d-b633-d7761906df11 + +
+ +Semantic highlighting will also be added soon. Additional features require +changes to the compiler infrastructure that are not yet available. + +Note that the language server is a bit naive: +- If you make a change in a dependency, you'll need to also make a change in + the dependents' files for the changes to be picked up +- The language server will only operate on changes that are also reflected on + disk (so save often) + +## Installing + +At this time, only from-source installations of the binary are supported. + +Follow the [installation from source](https://github.com/roc-lang/roc/tree/main/getting_started#installation) instructions. Then run + +``` +cargo build -p roc_lang_srv --release +``` + +which will give you a language server binary at + +``` +target/release/roc_ls +``` + +### Configuring in your editor + +Please follow your editor's language server implementation's documentation to see how custom language servers should be configured. + +#### [coc.nvim](https://github.com/neoclide/coc.nvim) + +Add the following to your coc JSON configuration file: + +``` +{ + "languageserver": { + "roc": { + "command": "/roc_ls", + "filetypes": ["roc"] + } + } +} +``` + +If you're using coc.nvim and want to use the configuration above, be sure to also instruct your vim that `*.roc` files have roc filetype. + +## Debug + +If you want to debug the server, use [debug_server.sh](./debug_server.sh) +instead of the direct binary. diff --git a/crates/lang_srv/debug_server.sh b/crates/lang_srv/debug_server.sh new file mode 100755 index 0000000000..5510f3d8d7 --- /dev/null +++ b/crates/lang_srv/debug_server.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +SCRIPT_DIR=$(cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd) +${SCRIPT_DIR}/../../target/debug/roc_ls "$@" 2> /tmp/roc_ls.err diff --git a/crates/lang_srv/src/analysis.rs b/crates/lang_srv/src/analysis.rs new file mode 100644 index 0000000000..6725db5940 --- /dev/null +++ b/crates/lang_srv/src/analysis.rs @@ -0,0 +1,399 @@ +use std::{ + collections::HashMap, + path::{Path, PathBuf}, +}; + +use bumpalo::Bump; +use roc_can::{abilities::AbilitiesStore, expr::Declarations}; +use roc_collections::MutMap; +use roc_load::{CheckedModule, LoadedModule}; +use roc_module::symbol::{Interns, ModuleId, Symbol}; +use roc_packaging::cache::{self, RocCacheDir}; +use roc_region::all::LineInfo; +use roc_reporting::report::RocDocAllocator; +use roc_solve_problem::TypeError; +use roc_types::subs::Subs; +use tower_lsp::lsp_types::{ + Diagnostic, GotoDefinitionResponse, Hover, HoverContents, Location, MarkedString, Position, + Range, SemanticTokenType, SemanticTokens, SemanticTokensResult, TextEdit, Url, +}; + +use crate::convert::{ + diag::{IntoLspDiagnostic, ProblemFmt}, + ToRange, ToRocPosition, +}; + +mod parse_ast; +mod semantic_tokens; +mod tokens; + +use self::{parse_ast::Ast, semantic_tokens::arrange_semantic_tokens, tokens::Token}; +pub const HIGHLIGHT_TOKENS_LEGEND: &[SemanticTokenType] = Token::LEGEND; + +pub(crate) struct GlobalAnalysis { + pub documents: Vec, +} + +impl GlobalAnalysis { + pub fn new(source_url: Url, source: String) -> GlobalAnalysis { + let arena = Bump::new(); + + let fi = source_url.to_file_path().unwrap(); + let src_dir = find_src_dir(&fi).to_path_buf(); + let line_info = LineInfo::new(&source); + + let loaded = roc_load::load_and_typecheck_str( + &arena, + fi, + &source, + src_dir, + roc_target::TargetInfo::default_x86_64(), + roc_load::FunctionKind::LambdaSet, + roc_reporting::report::RenderTarget::Generic, + RocCacheDir::Persistent(cache::roc_cache_dir().as_path()), + roc_reporting::report::DEFAULT_PALETTE, + ); + + let module = match loaded { + Ok(module) => module, + Err(problem) => { + let all_problems = problem + .into_lsp_diagnostic(&()) + .into_iter() + .collect::>(); + + let analyzed_document = AnalyzedDocument { + url: source_url, + line_info, + source, + module: None, + diagnostics: all_problems, + }; + + return GlobalAnalysis { + documents: vec![analyzed_document], + }; + } + }; + + let mut documents = vec![]; + + let LoadedModule { + interns, + mut can_problems, + mut type_problems, + mut declarations_by_id, + sources, + mut typechecked, + solved, + abilities_store, + .. + } = module; + + let mut root_module = Some(RootModule { + subs: solved.into_inner(), + abilities_store, + }); + + let mut builder = AnalyzedDocumentBuilder { + interns: &interns, + module_id_to_url: module_id_to_url_from_sources(&sources), + can_problems: &mut can_problems, + type_problems: &mut type_problems, + declarations_by_id: &mut declarations_by_id, + typechecked: &mut typechecked, + root_module: &mut root_module, + }; + + for (module_id, (path, source)) in sources { + documents.push(builder.build_document(path, source, module_id)); + } + + GlobalAnalysis { documents } + } +} + +fn find_src_dir(path: &Path) -> &Path { + path.parent().unwrap_or(path) +} + +fn _find_parent_git_repo(path: &Path) -> Option<&Path> { + let mut path = path; + loop { + if path.join(".git").exists() { + return Some(path); + } + + path = path.parent()?; + } +} + +fn module_id_to_url_from_sources(sources: &MutMap)>) -> ModuleIdToUrl { + sources + .iter() + .map(|(module_id, (path, _))| { + let url = path_to_url(path); + (*module_id, url) + }) + .collect() +} + +fn path_to_url(path: &Path) -> Url { + if path.is_relative() { + // Make it /path + let tmpdir = std::env::temp_dir(); + Url::from_file_path(tmpdir.join(path)).unwrap() + } else { + Url::from_file_path(path).unwrap() + } +} + +struct RootModule { + subs: Subs, + abilities_store: AbilitiesStore, +} + +struct AnalyzedDocumentBuilder<'a> { + interns: &'a Interns, + module_id_to_url: ModuleIdToUrl, + can_problems: &'a mut MutMap>, + type_problems: &'a mut MutMap>, + declarations_by_id: &'a mut MutMap, + typechecked: &'a mut MutMap, + root_module: &'a mut Option, +} + +impl<'a> AnalyzedDocumentBuilder<'a> { + fn build_document( + &mut self, + path: PathBuf, + source: Box, + module_id: ModuleId, + ) -> AnalyzedDocument { + let subs; + let abilities; + let declarations; + + if let Some(m) = self.typechecked.remove(&module_id) { + subs = m.solved_subs.into_inner(); + abilities = m.abilities_store; + declarations = m.decls; + } else { + let rm = self.root_module.take().unwrap(); + subs = rm.subs; + abilities = rm.abilities_store; + declarations = self.declarations_by_id.remove(&module_id).unwrap(); + } + + let analyzed_module = AnalyzedModule { + subs, + abilities, + declarations, + module_id, + interns: self.interns.clone(), + module_id_to_url: self.module_id_to_url.clone(), + }; + + let line_info = LineInfo::new(&source); + let diagnostics = self.build_diagnostics(&path, &source, &line_info, module_id); + + AnalyzedDocument { + url: path_to_url(&path), + line_info, + source: source.into(), + module: Some(analyzed_module), + diagnostics, + } + } + + fn build_diagnostics( + &mut self, + source_path: &Path, + source: &str, + line_info: &LineInfo, + module_id: ModuleId, + ) -> Vec { + let lines: Vec<_> = source.lines().collect(); + + let alloc = RocDocAllocator::new(&lines, module_id, self.interns); + + let mut all_problems = Vec::new(); + let fmt = ProblemFmt { + alloc: &alloc, + line_info, + path: source_path, + }; + + let can_problems = self.can_problems.remove(&module_id).unwrap_or_default(); + + let type_problems = self.type_problems.remove(&module_id).unwrap_or_default(); + + for can_problem in can_problems { + if let Some(diag) = can_problem.into_lsp_diagnostic(&fmt) { + all_problems.push(diag); + } + } + + for type_problem in type_problems { + if let Some(diag) = type_problem.into_lsp_diagnostic(&fmt) { + all_problems.push(diag); + } + } + + all_problems + } +} + +type ModuleIdToUrl = HashMap; + +#[derive(Debug)] +struct AnalyzedModule { + module_id: ModuleId, + interns: Interns, + subs: Subs, + abilities: AbilitiesStore, + declarations: Declarations, + // We need this because ModuleIds are not stable between compilations, so a ModuleId visible to + // one module may not be true global to the language server. + module_id_to_url: ModuleIdToUrl, +} + +#[derive(Debug)] +pub(crate) struct AnalyzedDocument { + url: Url, + line_info: LineInfo, + source: String, + module: Option, + diagnostics: Vec, +} + +impl AnalyzedDocument { + pub fn url(&self) -> &Url { + &self.url + } + + fn line_info(&self) -> &LineInfo { + &self.line_info + } + + fn module_mut(&mut self) -> Option<&mut AnalyzedModule> { + self.module.as_mut() + } + + fn module(&self) -> Option<&AnalyzedModule> { + self.module.as_ref() + } + + fn location(&self, range: Range) -> Location { + Location { + uri: self.url.clone(), + range, + } + } + + fn whole_document_range(&self) -> Range { + let line_info = self.line_info(); + let start = Position::new(0, 0); + let end = Position::new(line_info.num_lines(), 0); + Range::new(start, end) + } + + pub fn diagnostics(&mut self) -> Vec { + self.diagnostics.clone() + } + + pub fn symbol_at(&self, position: Position) -> Option { + let line_info = self.line_info(); + + let position = position.to_roc_position(line_info); + + let AnalyzedModule { + declarations, + abilities, + .. + } = self.module()?; + + let found_symbol = + roc_can::traverse::find_closest_symbol_at(position, declarations, abilities)?; + + Some(found_symbol.implementation_symbol()) + } + + pub fn hover(&mut self, position: Position) -> Option { + let line_info = self.line_info(); + + let pos = position.to_roc_position(line_info); + + let AnalyzedModule { + subs, + declarations, + module_id, + interns, + .. + } = self.module_mut()?; + + let (region, var) = roc_can::traverse::find_closest_type_at(pos, declarations)?; + + let snapshot = subs.snapshot(); + let type_str = roc_types::pretty_print::name_and_print_var( + var, + subs, + *module_id, + interns, + roc_types::pretty_print::DebugPrint::NOTHING, + ); + subs.rollback_to(snapshot); + + let range = region.to_range(self.line_info()); + + Some(Hover { + contents: HoverContents::Scalar(MarkedString::String(type_str)), + range: Some(range), + }) + } + + pub fn definition(&self, symbol: Symbol) -> Option { + let AnalyzedModule { declarations, .. } = self.module()?; + + let found_declaration = roc_can::traverse::find_declaration(symbol, declarations)?; + + let range = found_declaration.region().to_range(self.line_info()); + + Some(GotoDefinitionResponse::Scalar(self.location(range))) + } + + pub fn format(&self) -> Option> { + let source = &self.source; + let arena = &Bump::new(); + + let ast = Ast::parse(arena, source).ok()?; + let fmt = ast.fmt(); + + if source == fmt.as_str() { + None + } else { + let range = self.whole_document_range(); + let text_edit = TextEdit::new(range, fmt.to_string().to_string()); + Some(vec![text_edit]) + } + } + + pub fn semantic_tokens(&self) -> Option { + let source = &self.source; + let arena = &Bump::new(); + + let ast = Ast::parse(arena, source).ok()?; + let tokens = ast.semantic_tokens(); + + let data = arrange_semantic_tokens(tokens, &self.line_info); + + Some(SemanticTokensResult::Tokens(SemanticTokens { + result_id: None, + data, + })) + } + + pub(crate) fn module_url(&self, module_id: ModuleId) -> Option { + self.module()?.module_id_to_url.get(&module_id).cloned() + } +} diff --git a/crates/lang_srv/src/analysis/parse_ast.rs b/crates/lang_srv/src/analysis/parse_ast.rs new file mode 100644 index 0000000000..64c1a283ea --- /dev/null +++ b/crates/lang_srv/src/analysis/parse_ast.rs @@ -0,0 +1,59 @@ +use bumpalo::Bump; +use roc_fmt::Buf; +use roc_parse::{ + ast::{Defs, Module}, + parser::SyntaxError, +}; +use roc_region::all::Loc; + +use self::format::FormattedAst; + +use super::tokens::{IterTokens, Token}; + +mod format; + +pub struct Ast<'a> { + arena: &'a Bump, + module: Module<'a>, + defs: Defs<'a>, +} + +impl<'a> Ast<'a> { + pub fn parse(arena: &'a Bump, src: &'a str) -> Result, SyntaxError<'a>> { + use roc_parse::{ + module::{module_defs, parse_header}, + parser::Parser, + state::State, + }; + + let (module, state) = parse_header(arena, State::new(src.as_bytes())) + .map_err(|e| SyntaxError::Header(e.problem))?; + + let (_, defs, _) = module_defs().parse(arena, state, 0).map_err(|(_, e)| e)?; + + Ok(Ast { + module, + defs, + arena, + }) + } + + pub fn fmt(&self) -> FormattedAst<'a> { + let mut buf = Buf::new_in(self.arena); + + roc_fmt::module::fmt_module(&mut buf, &self.module); + + roc_fmt::def::fmt_defs(&mut buf, &self.defs, 0); + + buf.fmt_end_of_file(); + + FormattedAst::new(buf) + } + + pub fn semantic_tokens(&self) -> impl IntoIterator> + '_ { + let header_tokens = self.module.iter_tokens(self.arena); + let body_tokens = self.defs.iter_tokens(self.arena); + + header_tokens.into_iter().chain(body_tokens) + } +} diff --git a/crates/lang_srv/src/analysis/parse_ast/format.rs b/crates/lang_srv/src/analysis/parse_ast/format.rs new file mode 100644 index 0000000000..fde4a117db --- /dev/null +++ b/crates/lang_srv/src/analysis/parse_ast/format.rs @@ -0,0 +1,21 @@ +use roc_fmt::Buf; + +pub struct FormattedAst<'a> { + buf: Buf<'a>, +} + +impl<'a> FormattedAst<'a> { + pub(crate) fn new(buf: Buf<'a>) -> Self { + Self { buf } + } + + pub fn as_str(&self) -> &str { + self.buf.as_str() + } +} + +impl ToString for FormattedAst<'_> { + fn to_string(&self) -> String { + self.buf.as_str().to_owned() + } +} diff --git a/crates/lang_srv/src/analysis/semantic_tokens.rs b/crates/lang_srv/src/analysis/semantic_tokens.rs new file mode 100644 index 0000000000..a8c5d54f0b --- /dev/null +++ b/crates/lang_srv/src/analysis/semantic_tokens.rs @@ -0,0 +1,49 @@ +use roc_region::all::{LineColumn, LineInfo, Loc}; +use tower_lsp::lsp_types::SemanticToken; + +use super::tokens::Token; + +/// Encodes semantic tokens as described in the LSP specification. +/// See [the sample documentation](https://github.com/microsoft/vscode-extension-samples/blob/5ae1f7787122812dcc84e37427ca90af5ee09f14/semantic-tokens-sample/vscode.proposed.d.ts#L71-L128). +pub fn arrange_semantic_tokens( + tokens: impl IntoIterator>, + line_info: &LineInfo, +) -> Vec { + let tokens = tokens.into_iter(); + let (min, max) = tokens.size_hint(); + let size_hint = max.unwrap_or(min); + let mut result = Vec::with_capacity(size_hint); + + let mut last_line = 0; + let mut last_start = 0; + + for Loc { + region, + value: token, + } in tokens + { + let length = region.len(); + + let LineColumn { line, column } = line_info.convert_pos(region.start()); + + let delta_line = line - last_line; + let delta_start = if delta_line == 0 { + column - last_start + } else { + column + }; + + result.push(SemanticToken { + delta_line, + delta_start, + length, + token_type: token as u32, + token_modifiers_bitset: 0, + }); + + last_line = line; + last_start = column; + } + + result +} diff --git a/crates/lang_srv/src/analysis/tokens.rs b/crates/lang_srv/src/analysis/tokens.rs new file mode 100644 index 0000000000..759b941c57 --- /dev/null +++ b/crates/lang_srv/src/analysis/tokens.rs @@ -0,0 +1,792 @@ +use bumpalo::{ + collections::{CollectIn, Vec as BumpVec}, + vec as bumpvec, Bump, +}; +use roc_module::called_via::{BinOp, UnaryOp}; +use roc_parse::{ + ast::{ + AbilityImpls, AbilityMember, AssignedField, Collection, Defs, Expr, Header, Implements, + ImplementsAbilities, ImplementsAbility, ImplementsClause, Module, Pattern, PatternAs, + RecordBuilderField, Spaced, StrLiteral, Tag, TypeAnnotation, TypeDef, TypeHeader, ValueDef, + WhenBranch, + }, + header::{ + AppHeader, ExposedName, HostedHeader, ImportsEntry, InterfaceHeader, ModuleName, + PackageEntry, PackageHeader, PackageName, PlatformHeader, PlatformRequires, ProvidesTo, To, + TypedIdent, + }, + ident::{Accessor, UppercaseIdent}, +}; +use roc_region::all::{Loc, Region}; +use tower_lsp::lsp_types::SemanticTokenType; + +macro_rules! tokens { + ($($(#[$meta:meta])* $token:ident => $lsp_token:literal),* $(,)?) => { + pub enum Token { + $( + $(#[$meta])* + $token + ),* + } + + fn _non_redundant_lsp_tokens() { + match "" { + $($lsp_token => (),)* + _ => (), + } + } + + impl Token { + pub const LEGEND: &[SemanticTokenType] = &[ + $(SemanticTokenType::new($lsp_token)),* + ]; + } + } +} + +// Try to use predefined values at +// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_semanticTokens +tokens! { + Module => "namespace", + Type => "type", + Ability => "interface", + #[allow(unused)] + TypeVariable => "typeParameter", + #[allow(unused)] + Parameter => "parameter", + Variable => "variable", + Field => "property", + Tag => "enumMember", + Function => "function", + Keyword => "keyword", + String => "string", + Number => "number", + Operator => "operator", + Comment => "comment", +} + +fn onetoken(token: Token, region: Region, arena: &Bump) -> BumpVec> { + bumpvec![in arena; Loc::at(region, token)] +} + +fn field_token(region: Region, arena: &Bump) -> BumpVec> { + onetoken(Token::Field, region, arena) +} + +trait HasToken { + fn token(&self) -> Token; +} + +impl HasToken for Spaced<'_, T> { + fn token(&self) -> Token { + self.item().token() + } +} + +impl HasToken for ModuleName<'_> { + fn token(&self) -> Token { + Token::Module + } +} + +impl HasToken for &str { + fn token(&self) -> Token { + if self.chars().next().unwrap().is_uppercase() { + Token::Type + } else { + Token::Variable + } + } +} + +impl HasToken for ExposedName<'_> { + fn token(&self) -> Token { + self.as_str().token() + } +} + +impl HasToken for PackageName<'_> { + fn token(&self) -> Token { + Token::Module + } +} + +impl HasToken for StrLiteral<'_> { + fn token(&self) -> Token { + Token::String + } +} + +impl HasToken for UppercaseIdent<'_> { + fn token(&self) -> Token { + Token::Type + } +} + +impl HasToken for To<'_> { + fn token(&self) -> Token { + match self { + To::ExistingPackage(_) => Token::Module, + To::NewPackage(_) => Token::Module, + } + } +} + +impl HasToken for BinOp { + fn token(&self) -> Token { + Token::Operator + } +} + +impl HasToken for UnaryOp { + fn token(&self) -> Token { + Token::Operator + } +} + +pub trait IterTokens { + // Use a vec until "impl trait in trait" is stabilized + fn iter_tokens<'a>(&self, arena: &'a Bump) -> BumpVec<'a, Loc>; +} + +impl IterTokens for Loc { + fn iter_tokens<'a>(&self, arena: &'a Bump) -> BumpVec<'a, Loc> { + onetoken(self.value.token(), self.region, arena) + } +} + +impl IterTokens for Spaced<'_, T> { + fn iter_tokens<'a>(&self, arena: &'a Bump) -> BumpVec<'a, Loc> { + self.item().iter_tokens(arena) + } +} + +impl IterTokens for Collection<'_, T> { + fn iter_tokens<'a>(&self, arena: &'a Bump) -> BumpVec<'a, Loc> { + self.items + .iter() + .flat_map(|item| item.iter_tokens(arena)) + .collect_in(arena) + } +} + +impl IterTokens for &[T] { + fn iter_tokens<'a>(&self, arena: &'a Bump) -> BumpVec<'a, Loc> { + self.iter() + .flat_map(|item| item.iter_tokens(arena)) + .collect_in(arena) + } +} + +impl IterTokens for (T, U) { + fn iter_tokens<'a>(&self, arena: &'a Bump) -> BumpVec<'a, Loc> { + let (a, b) = self; + a.iter_tokens(arena) + .into_iter() + .chain(b.iter_tokens(arena)) + .collect_in(arena) + } +} + +impl IterTokens for Module<'_> { + fn iter_tokens<'a>(&self, arena: &'a Bump) -> BumpVec<'a, Loc> { + let Self { + comments: _, + header, + } = self; + header.iter_tokens(arena) + } +} + +impl IterTokens for Header<'_> { + fn iter_tokens<'a>(&self, arena: &'a Bump) -> BumpVec<'a, Loc> { + match self { + Header::Interface(ih) => ih.iter_tokens(arena), + Header::App(app) => app.iter_tokens(arena), + Header::Package(pkg) => pkg.iter_tokens(arena), + Header::Platform(pf) => pf.iter_tokens(arena), + Header::Hosted(h) => h.iter_tokens(arena), + } + } +} + +impl IterTokens for InterfaceHeader<'_> { + fn iter_tokens<'a>(&self, arena: &'a Bump) -> BumpVec<'a, Loc> { + let Self { + before_name: _, + name, + exposes, + imports, + } = self; + + (name.iter_tokens(arena).into_iter()) + .chain(exposes.item.iter_tokens(arena)) + .chain(imports.item.iter_tokens(arena)) + .collect_in(arena) + } +} + +impl IterTokens for AppHeader<'_> { + fn iter_tokens<'a>(&self, arena: &'a Bump) -> BumpVec<'a, Loc> { + let Self { + before_name: _, + name, + packages, + imports, + provides, + } = self; + + (name.iter_tokens(arena).into_iter()) + .chain(packages.iter().flat_map(|p| p.item.iter_tokens(arena))) + .chain(imports.iter().flat_map(|i| i.item.iter_tokens(arena))) + .chain(provides.iter_tokens(arena)) + .collect_in(arena) + } +} + +impl IterTokens for PackageHeader<'_> { + fn iter_tokens<'a>(&self, arena: &'a Bump) -> BumpVec<'a, Loc> { + let Self { + before_name: _, + name, + exposes, + packages, + } = self; + + (name.iter_tokens(arena).into_iter()) + .chain(exposes.item.iter_tokens(arena)) + .chain(packages.item.iter_tokens(arena)) + .collect_in(arena) + } +} + +impl IterTokens for PlatformHeader<'_> { + fn iter_tokens<'a>(&self, arena: &'a Bump) -> BumpVec<'a, Loc> { + let Self { + before_name: _, + name, + requires, + exposes, + packages, + imports, + provides, + } = self; + + (name.iter_tokens(arena).into_iter()) + .chain(requires.item.iter_tokens(arena)) + .chain(exposes.item.iter_tokens(arena)) + .chain(packages.item.iter_tokens(arena)) + .chain(imports.item.iter_tokens(arena)) + .chain(provides.item.iter_tokens(arena)) + .collect_in(arena) + } +} + +impl IterTokens for HostedHeader<'_> { + fn iter_tokens<'a>(&self, arena: &'a Bump) -> BumpVec<'a, Loc> { + let Self { + before_name: _, + name, + exposes, + imports, + generates: _, + generates_with, + } = self; + + (name.iter_tokens(arena).into_iter()) + .chain(exposes.item.iter_tokens(arena)) + .chain(imports.item.iter_tokens(arena)) + .chain(generates_with.item.iter_tokens(arena)) + .collect_in(arena) + } +} + +impl IterTokens for Loc>> { + fn iter_tokens<'a>(&self, arena: &'a Bump) -> BumpVec<'a, Loc> { + match self.value.item() { + ImportsEntry::Module(_module_name, names) => names.iter_tokens(arena), + ImportsEntry::Package(_pkg, _module_name, names) => names.iter_tokens(arena), + ImportsEntry::IngestedFile(_str, idents) => idents.iter_tokens(arena), + } + } +} + +impl IterTokens for Loc>> { + fn iter_tokens<'a>(&self, arena: &'a Bump) -> BumpVec<'a, Loc> { + let PackageEntry { + shorthand: _, + spaces_after_shorthand: _, + package_name, + } = self.value.item(); + + package_name.iter_tokens(arena) + } +} + +impl IterTokens for Loc>> { + fn iter_tokens<'a>(&self, arena: &'a Bump) -> BumpVec<'a, Loc> { + self.value.item().iter_tokens(arena) + } +} + +impl IterTokens for TypedIdent<'_> { + fn iter_tokens<'a>(&self, arena: &'a Bump) -> BumpVec<'a, Loc> { + let Self { + ident, + spaces_before_colon: _, + ann, + } = self; + + (ident.iter_tokens(arena).into_iter()) + .chain(ann.iter_tokens(arena)) + .collect_in(arena) + } +} + +impl IterTokens for ProvidesTo<'_> { + fn iter_tokens<'a>(&self, arena: &'a Bump) -> BumpVec<'a, Loc> { + let Self { + provides_keyword: _, + entries, + types, + to_keyword: _, + to, + } = self; + + (entries.iter_tokens(arena).into_iter()) + .chain(types.iter().flat_map(|t| t.iter_tokens(arena))) + .chain(to.iter_tokens(arena)) + .collect_in(arena) + } +} + +impl IterTokens for PlatformRequires<'_> { + fn iter_tokens<'a>(&self, arena: &'a Bump) -> BumpVec<'a, Loc> { + let Self { rigids, signature } = self; + + (rigids.iter_tokens(arena).into_iter()) + .chain(signature.iter_tokens(arena)) + .collect_in(arena) + } +} + +impl IterTokens for Loc> { + fn iter_tokens<'a>(&self, arena: &'a Bump) -> BumpVec<'a, Loc> { + match self.value { + TypeAnnotation::Function(params, ret) => (params.iter_tokens(arena).into_iter()) + .chain(ret.iter_tokens(arena)) + .collect_in(arena), + TypeAnnotation::Apply(_mod, _type, args) => args.iter_tokens(arena), + TypeAnnotation::BoundVariable(_) => onetoken(Token::Type, self.region, arena), + TypeAnnotation::As(ty, _, as_ty) => (ty.iter_tokens(arena).into_iter()) + .chain(as_ty.iter_tokens(arena)) + .collect_in(arena), + TypeAnnotation::Record { fields, ext } => (fields.iter_tokens(arena).into_iter()) + .chain(ext.iter().flat_map(|t| t.iter_tokens(arena))) + .collect_in(arena), + TypeAnnotation::Tuple { elems, ext } => (elems.iter_tokens(arena).into_iter()) + .chain(ext.iter().flat_map(|t| t.iter_tokens(arena))) + .collect_in(arena), + TypeAnnotation::TagUnion { tags, ext } => (tags.iter_tokens(arena).into_iter()) + .chain(ext.iter().flat_map(|t| t.iter_tokens(arena))) + .collect_in(arena), + TypeAnnotation::Inferred => onetoken(Token::Type, self.region, arena), + TypeAnnotation::Wildcard => onetoken(Token::Type, self.region, arena), + TypeAnnotation::Where(ty, implements) => (ty.iter_tokens(arena).into_iter()) + .chain(implements.iter_tokens(arena)) + .collect_in(arena), + TypeAnnotation::SpaceBefore(ty, _) | TypeAnnotation::SpaceAfter(ty, _) => { + Loc::at(self.region, *ty).iter_tokens(arena) + } + TypeAnnotation::Malformed(_) => bumpvec![in arena;], + } + } +} + +impl IterTokens for Loc> +where + Loc: IterTokens, +{ + fn iter_tokens<'a>(&self, arena: &'a Bump) -> BumpVec<'a, Loc> { + self.value.iter_tokens(arena) + } +} + +impl IterTokens for AssignedField<'_, T> +where + Loc: IterTokens, +{ + fn iter_tokens<'a>(&self, arena: &'a Bump) -> BumpVec<'a, Loc> { + match self { + AssignedField::RequiredValue(field, _, ty) + | AssignedField::OptionalValue(field, _, ty) => (field_token(field.region, arena) + .into_iter()) + .chain(ty.iter_tokens(arena)) + .collect_in(arena), + AssignedField::LabelOnly(s) => s.iter_tokens(arena), + AssignedField::SpaceBefore(af, _) | AssignedField::SpaceAfter(af, _) => { + af.iter_tokens(arena) + } + AssignedField::Malformed(_) => bumpvec![in arena;], + } + } +} + +impl IterTokens for Loc> { + fn iter_tokens<'a>(&self, arena: &'a Bump) -> BumpVec<'a, Loc> { + self.value.iter_tokens(arena) + } +} + +impl IterTokens for Tag<'_> { + fn iter_tokens<'a>(&self, arena: &'a Bump) -> BumpVec<'a, Loc> { + match self { + Tag::Apply { name, args } => (onetoken(Token::Tag, name.region, arena).into_iter()) + .chain(args.iter_tokens(arena)) + .collect_in(arena), + Tag::SpaceBefore(t, _) | Tag::SpaceAfter(t, _) => t.iter_tokens(arena), + Tag::Malformed(_) => bumpvec![in arena;], + } + } +} + +impl IterTokens for TypeHeader<'_> { + fn iter_tokens<'a>(&self, arena: &'a Bump) -> BumpVec<'a, Loc> { + let Self { name, vars } = self; + + (name.iter_tokens(arena).into_iter()) + .chain(vars.iter().map(|v| v.with_value(Token::Type))) + .collect_in(arena) + } +} + +impl IterTokens for Loc> { + fn iter_tokens<'a>(&self, arena: &'a Bump) -> BumpVec<'a, Loc> { + self.value.iter_tokens(arena) + } +} + +impl IterTokens for ImplementsClause<'_> { + fn iter_tokens<'a>(&self, arena: &'a Bump) -> BumpVec<'a, Loc> { + let Self { var, abilities } = self; + (var.iter_tokens(arena).into_iter()) + .chain(abilities.iter_tokens(arena)) + .collect_in(arena) + } +} + +impl IterTokens for Defs<'_> { + fn iter_tokens<'a>(&self, arena: &'a Bump) -> BumpVec<'a, Loc> { + self.defs() + .flat_map(|item| match item { + Ok(type_def) => type_def.iter_tokens(arena), + Err(value_def) => value_def.iter_tokens(arena), + }) + .collect_in(arena) + } +} + +impl IterTokens for TypeDef<'_> { + fn iter_tokens<'a>(&self, arena: &'a Bump) -> BumpVec<'a, Loc> { + match self { + TypeDef::Alias { header, ann } => (header.iter_tokens(arena).into_iter()) + .chain(ann.iter_tokens(arena)) + .collect_in(arena), + TypeDef::Opaque { + header, + typ, + derived, + } => (header.iter_tokens(arena).into_iter()) + .chain(typ.iter_tokens(arena)) + .chain(derived.iter().flat_map(|t| t.iter_tokens(arena))) + .collect_in(arena), + TypeDef::Ability { + header: TypeHeader { name, vars }, + loc_implements, + members, + } => (onetoken(Token::Ability, name.region, arena).into_iter()) + .chain(vars.iter().map(|v| v.with_value(Token::Type))) + .chain(loc_implements.iter_tokens(arena)) + .chain(members.iter_tokens(arena)) + .collect_in(arena), + } + } +} + +impl IterTokens for Loc> { + fn iter_tokens<'a>(&self, arena: &'a Bump) -> BumpVec<'a, Loc> { + self.value.iter_tokens(arena) + } +} + +impl IterTokens for ImplementsAbilities<'_> { + fn iter_tokens<'a>(&self, arena: &'a Bump) -> BumpVec<'a, Loc> { + match self { + ImplementsAbilities::Implements(impls) => impls.iter_tokens(arena), + ImplementsAbilities::SpaceBefore(i, _) | ImplementsAbilities::SpaceAfter(i, _) => { + i.iter_tokens(arena) + } + } + } +} + +impl IterTokens for Loc> { + fn iter_tokens<'a>(&self, arena: &'a Bump) -> BumpVec<'a, Loc> { + self.value.iter_tokens(arena) + } +} + +impl IterTokens for ImplementsAbility<'_> { + fn iter_tokens<'a>(&self, arena: &'a Bump) -> BumpVec<'a, Loc> { + match self { + ImplementsAbility::ImplementsAbility { ability, impls } => { + (ability.iter_tokens(arena).into_iter()) + .chain(impls.iter().flat_map(|i| i.iter_tokens(arena))) + .collect_in(arena) + } + ImplementsAbility::SpaceBefore(ia, _) | ImplementsAbility::SpaceAfter(ia, _) => { + ia.iter_tokens(arena) + } + } + } +} + +impl IterTokens for Loc> { + fn iter_tokens<'a>(&self, arena: &'a Bump) -> BumpVec<'a, Loc> { + self.value.iter_tokens(arena) + } +} + +impl IterTokens for AbilityImpls<'_> { + fn iter_tokens<'a>(&self, arena: &'a Bump) -> BumpVec<'a, Loc> { + match self { + AbilityImpls::AbilityImpls(fields) => fields.iter_tokens(arena), + AbilityImpls::SpaceBefore(ai, _) | AbilityImpls::SpaceAfter(ai, _) => { + ai.iter_tokens(arena) + } + } + } +} + +impl IterTokens for Loc> { + fn iter_tokens<'a>(&self, arena: &'a Bump) -> BumpVec<'a, Loc> { + match self.value { + Implements::Implements => onetoken(Token::Keyword, self.region, arena), + Implements::SpaceBefore(i, _) | Implements::SpaceAfter(i, _) => { + Loc::at(self.region, *i).iter_tokens(arena) + } + } + } +} + +impl IterTokens for AbilityMember<'_> { + fn iter_tokens<'a>(&self, arena: &'a Bump) -> BumpVec<'a, Loc> { + let Self { name, typ } = self; + (onetoken(Token::Function, name.region, arena).into_iter()) + .chain(typ.iter_tokens(arena)) + .collect_in(arena) + } +} + +impl IterTokens for ValueDef<'_> { + fn iter_tokens<'a>(&self, arena: &'a Bump) -> BumpVec<'a, Loc> { + match self { + ValueDef::Annotation(pattern, annotation) => (pattern.iter_tokens(arena).into_iter()) + .chain(annotation.iter_tokens(arena)) + .collect_in(arena), + ValueDef::Body(pattern, body) => (pattern.iter_tokens(arena).into_iter()) + .chain(body.iter_tokens(arena)) + .collect_in(arena), + ValueDef::AnnotatedBody { + ann_pattern, + ann_type, + comment: _, + body_pattern, + body_expr, + } => (ann_pattern.iter_tokens(arena).into_iter()) + .chain(ann_type.iter_tokens(arena)) + .chain(body_pattern.iter_tokens(arena)) + .chain(body_expr.iter_tokens(arena)) + .collect_in(arena), + ValueDef::Dbg { + preceding_comment, + condition, + } + | ValueDef::Expect { + preceding_comment, + condition, + } + | ValueDef::ExpectFx { + preceding_comment, + condition, + } => (onetoken(Token::Comment, *preceding_comment, arena).into_iter()) + .chain(condition.iter_tokens(arena)) + .collect_in(arena), + } + } +} + +impl IterTokens for &Loc> { + fn iter_tokens<'a>(&self, arena: &'a Bump) -> BumpVec<'a, Loc> { + (**self).iter_tokens(arena) + } +} + +impl IterTokens for Loc> { + fn iter_tokens<'a>(&self, arena: &'a Bump) -> BumpVec<'a, Loc> { + let region = self.region; + match self.value { + Expr::Float(_) => onetoken(Token::Number, region, arena), + Expr::Num(_) => onetoken(Token::Number, region, arena), + Expr::NonBase10Int { .. } => onetoken(Token::Number, region, arena), + Expr::Str(_) => onetoken(Token::String, region, arena), + Expr::SingleQuote(_) => onetoken(Token::String, region, arena), + Expr::RecordAccess(rcd, _field) => Loc::at(region, *rcd).iter_tokens(arena), + Expr::AccessorFunction(accessor) => Loc::at(region, accessor).iter_tokens(arena), + Expr::TupleAccess(tup, _field) => Loc::at(region, *tup).iter_tokens(arena), + Expr::List(lst) => lst.iter_tokens(arena), + Expr::RecordUpdate { update, fields } => (update.iter_tokens(arena).into_iter()) + .chain(fields.iter().flat_map(|f| f.iter_tokens(arena))) + .collect_in(arena), + Expr::Record(rcd) => rcd.iter_tokens(arena), + Expr::Tuple(tup) => tup.iter_tokens(arena), + Expr::RecordBuilder(rb) => rb.iter_tokens(arena), + Expr::IngestedFile(_path, ty) => ty.iter_tokens(arena), + Expr::Var { .. } => onetoken(Token::Variable, region, arena), + Expr::Underscore(_) => onetoken(Token::Variable, region, arena), + Expr::Crash => onetoken(Token::Keyword, region, arena), + Expr::Tag(_) => onetoken(Token::Tag, region, arena), + Expr::OpaqueRef(_) => onetoken(Token::Type, region, arena), + Expr::Closure(patterns, body) => (patterns.iter_tokens(arena).into_iter()) + .chain(body.iter_tokens(arena)) + .collect_in(arena), + Expr::Defs(defs, exprs) => (defs.iter_tokens(arena).into_iter()) + .chain(exprs.iter_tokens(arena)) + .collect_in(arena), + Expr::Backpassing(patterns, e1, e2) => (patterns.iter_tokens(arena).into_iter()) + .chain(e1.iter_tokens(arena)) + .chain(e2.iter_tokens(arena)) + .collect_in(arena), + Expr::Expect(e1, e2) => (e1.iter_tokens(arena).into_iter()) + .chain(e2.iter_tokens(arena)) + .collect_in(arena), + Expr::Dbg(e1, e2) => (e1.iter_tokens(arena).into_iter()) + .chain(e2.iter_tokens(arena)) + .collect_in(arena), + Expr::LowLevelDbg(e1, e2) => (e1.iter_tokens(arena).into_iter()) + .chain(e2.iter_tokens(arena)) + .collect_in(arena), + Expr::Apply(e1, e2, _called_via) => (e1.iter_tokens(arena).into_iter()) + .chain(e2.iter_tokens(arena)) + .collect_in(arena), + Expr::BinOps(e1, e2) => (e1.iter_tokens(arena).into_iter()) + .chain(e2.iter_tokens(arena)) + .collect_in(arena), + Expr::UnaryOp(e1, op) => (op.iter_tokens(arena).into_iter()) + .chain(e1.iter_tokens(arena)) + .collect_in(arena), + Expr::If(e1, e2) => (e1.iter_tokens(arena).into_iter()) + .chain(e2.iter_tokens(arena)) + .collect_in(arena), + Expr::When(e, branches) => (e.iter_tokens(arena).into_iter()) + .chain(branches.iter_tokens(arena)) + .collect_in(arena), + Expr::SpaceBefore(e, _) | Expr::SpaceAfter(e, _) => { + Loc::at(region, *e).iter_tokens(arena) + } + Expr::ParensAround(e) => Loc::at(region, *e).iter_tokens(arena), + Expr::MultipleRecordBuilders(e) => e.iter_tokens(arena), + Expr::UnappliedRecordBuilder(e) => e.iter_tokens(arena), + Expr::MalformedIdent(_, _) | Expr::MalformedClosure | Expr::PrecedenceConflict(_) => { + bumpvec![in arena;] + } + } + } +} + +impl IterTokens for Loc> { + fn iter_tokens<'a>(&self, arena: &'a Bump) -> BumpVec<'a, Loc> { + match self.value { + Accessor::RecordField(_) => onetoken(Token::Function, self.region, arena), + Accessor::TupleIndex(_) => onetoken(Token::Function, self.region, arena), + } + } +} + +impl IterTokens for Loc> { + fn iter_tokens<'a>(&self, arena: &'a Bump) -> BumpVec<'a, Loc> { + match self.value { + RecordBuilderField::Value(field, _, e) + | RecordBuilderField::ApplyValue(field, _, _, e) => field_token(field.region, arena) + .into_iter() + .chain(e.iter_tokens(arena)) + .collect_in(arena), + RecordBuilderField::LabelOnly(field) => field_token(field.region, arena), + RecordBuilderField::SpaceBefore(rbf, _) | RecordBuilderField::SpaceAfter(rbf, _) => { + Loc::at(self.region, *rbf).iter_tokens(arena) + } + RecordBuilderField::Malformed(_) => bumpvec![in arena;], + } + } +} + +impl IterTokens for &WhenBranch<'_> { + fn iter_tokens<'a>(&self, arena: &'a Bump) -> BumpVec<'a, Loc> { + let WhenBranch { + patterns, + value, + guard, + } = self; + + (patterns.iter_tokens(arena).into_iter()) + .chain(value.iter_tokens(arena)) + .chain(guard.iter().flat_map(|g| g.iter_tokens(arena))) + .collect_in(arena) + } +} + +impl IterTokens for Loc> { + fn iter_tokens<'a>(&self, arena: &'a Bump) -> BumpVec<'a, Loc> { + let region = self.region; + match self.value { + Pattern::Identifier(_) => onetoken(Token::Variable, region, arena), + Pattern::Tag(_) => onetoken(Token::Tag, region, arena), + Pattern::OpaqueRef(_) => onetoken(Token::Type, region, arena), + Pattern::Apply(p1, p2) => (p1.iter_tokens(arena).into_iter()) + .chain(p2.iter_tokens(arena)) + .collect_in(arena), + Pattern::RecordDestructure(ps) => ps.iter_tokens(arena), + Pattern::RequiredField(_field, p) => p.iter_tokens(arena), + Pattern::OptionalField(_field, p) => p.iter_tokens(arena), + Pattern::NumLiteral(_) => onetoken(Token::Number, region, arena), + Pattern::NonBase10Literal { .. } => onetoken(Token::Number, region, arena), + Pattern::FloatLiteral(_) => onetoken(Token::Number, region, arena), + Pattern::StrLiteral(_) => onetoken(Token::String, region, arena), + Pattern::Underscore(_) => onetoken(Token::Variable, region, arena), + Pattern::SingleQuote(_) => onetoken(Token::String, region, arena), + Pattern::Tuple(ps) => ps.iter_tokens(arena), + Pattern::List(ps) => ps.iter_tokens(arena), + Pattern::ListRest(None) => bumpvec![in arena;], + Pattern::ListRest(Some((_, pas))) => pas.iter_tokens(arena), + Pattern::As(p1, pas) => (p1.iter_tokens(arena).into_iter()) + .chain(pas.iter_tokens(arena)) + .collect_in(arena), + Pattern::SpaceBefore(p, _) | Pattern::SpaceAfter(p, _) => { + Loc::at(region, *p).iter_tokens(arena) + } + Pattern::QualifiedIdentifier { .. } => onetoken(Token::Variable, region, arena), + Pattern::Malformed(_) | Pattern::MalformedIdent(_, _) => bumpvec![in arena;], + } + } +} + +impl IterTokens for PatternAs<'_> { + fn iter_tokens<'a>(&self, arena: &'a Bump) -> BumpVec<'a, Loc> { + let Self { + spaces_before: _, + identifier, + } = self; + + onetoken(Token::Variable, identifier.region, arena) + } +} diff --git a/crates/lang_srv/src/convert.rs b/crates/lang_srv/src/convert.rs new file mode 100644 index 0000000000..d2a1d8114f --- /dev/null +++ b/crates/lang_srv/src/convert.rs @@ -0,0 +1,245 @@ +use roc_region::all::{LineColumn, LineColumnRegion, LineInfo, Region}; +use tower_lsp::lsp_types::{Position, Range}; + +pub(crate) trait ToRange { + type Feed; + + fn to_range(&self, feed: &Self::Feed) -> Range; +} + +impl ToRange for Region { + type Feed = LineInfo; + + fn to_range(&self, line_info: &LineInfo) -> Range { + let LineColumnRegion { start, end } = line_info.convert_region(*self); + Range { + start: Position { + line: start.line, + character: start.column, + }, + end: Position { + line: end.line, + character: end.column, + }, + } + } +} + +pub(crate) trait ToRegion { + type Feed; + + fn to_region(&self, feed: &Self::Feed) -> Region; +} + +impl ToRegion for Range { + type Feed = LineInfo; + + fn to_region(&self, line_info: &LineInfo) -> Region { + let lc_region = LineColumnRegion { + start: LineColumn { + line: self.start.line, + column: self.start.character, + }, + end: LineColumn { + line: self.end.line, + column: self.end.line, + }, + }; + + line_info.convert_line_column_region(lc_region) + } +} + +pub(crate) trait ToRocPosition { + type Feed; + + fn to_roc_position(&self, feed: &Self::Feed) -> roc_region::all::Position; +} + +impl ToRocPosition for tower_lsp::lsp_types::Position { + type Feed = LineInfo; + + fn to_roc_position(&self, line_info: &LineInfo) -> roc_region::all::Position { + let lc = LineColumn { + line: self.line, + column: self.character, + }; + line_info.convert_line_column(lc) + } +} + +pub(crate) mod diag { + use std::path::Path; + + use roc_load::LoadingProblem; + use roc_region::all::{LineInfo, Region}; + use roc_solve_problem::TypeError; + + use roc_problem::Severity; + use roc_reporting::report::RocDocAllocator; + use tower_lsp::lsp_types::{Diagnostic, DiagnosticSeverity, Position, Range}; + + use super::ToRange; + + pub trait IntoLspSeverity { + fn into_lsp_severity(self) -> DiagnosticSeverity; + } + + impl IntoLspSeverity for Severity { + fn into_lsp_severity(self) -> DiagnosticSeverity { + match self { + Severity::RuntimeError => DiagnosticSeverity::ERROR, + Severity::Warning => DiagnosticSeverity::WARNING, + Severity::Fatal => DiagnosticSeverity::ERROR, + } + } + } + + pub trait IntoLspDiagnostic<'a> { + type Feed; + + fn into_lsp_diagnostic(self, feed: &'a Self::Feed) -> Option; + } + + impl IntoLspDiagnostic<'_> for &LoadingProblem<'_> { + type Feed = (); + + fn into_lsp_diagnostic(self, _feed: &()) -> Option { + let range = Range { + start: Position { + line: 0, + character: 0, + }, + end: Position { + line: 0, + character: 1, + }, + }; + + let msg = match self { + LoadingProblem::FileProblem { filename, error } => { + format!( + "Failed to load {} due to an I/O error: {}", + filename.display(), + error + ) + } + LoadingProblem::ParsingFailed(fe) => { + let problem = &fe.problem.problem; + format!("Failed to parse Roc source file: {problem:?}") + } + LoadingProblem::UnexpectedHeader(header) => { + format!("Unexpected header: {}", header) + } + LoadingProblem::ChannelProblem(_) => { + "Internal error: message channel died".to_string() + } + LoadingProblem::ErrJoiningWorkerThreads => { + "Internal error: analysis worker threads died".to_string() + } + LoadingProblem::TriedToImportAppModule => { + "Attempted to import app module".to_string() + } + LoadingProblem::FormattedReport(report) => report.clone(), + LoadingProblem::ImportCycle(_, _) => { + "Circular dependency between modules".to_string() + } + LoadingProblem::IncorrectModuleName(_) => "Incorrect module name".to_string(), + LoadingProblem::CouldNotFindCacheDir => { + format!( + "Could not find Roc cache directory {}", + roc_packaging::cache::roc_cache_dir().display() + ) + } + }; + + Some(Diagnostic { + range, + severity: Some(DiagnosticSeverity::ERROR), + code: None, + code_description: None, + source: Some("load".to_owned()), + message: msg, + related_information: None, + tags: None, + data: None, + }) + } + } + + pub struct ProblemFmt<'a> { + pub alloc: &'a RocDocAllocator<'a>, + pub line_info: &'a LineInfo, + pub path: &'a Path, + } + + impl<'a> IntoLspDiagnostic<'a> for roc_problem::can::Problem { + type Feed = ProblemFmt<'a>; + + fn into_lsp_diagnostic(self, fmt: &'a ProblemFmt<'a>) -> Option { + let range = self + .region() + .unwrap_or_else(Region::zero) + .to_range(fmt.line_info); + + let report = roc_reporting::report::can_problem( + fmt.alloc, + fmt.line_info, + fmt.path.to_path_buf(), + self, + ); + + let severity = report.severity.into_lsp_severity(); + + let mut msg = String::new(); + report.render_ci(&mut msg, fmt.alloc); + + Some(Diagnostic { + range, + severity: Some(severity), + code: None, + code_description: None, + source: None, + message: msg, + related_information: None, + tags: None, + data: None, + }) + } + } + + impl<'a> IntoLspDiagnostic<'a> for TypeError { + type Feed = ProblemFmt<'a>; + + fn into_lsp_diagnostic(self, fmt: &'a ProblemFmt<'a>) -> Option { + let range = self + .region() + .unwrap_or_else(Region::zero) + .to_range(fmt.line_info); + + let report = roc_reporting::report::type_problem( + fmt.alloc, + fmt.line_info, + fmt.path.to_path_buf(), + self, + )?; + + let severity = report.severity.into_lsp_severity(); + + let mut msg = String::new(); + report.render_ci(&mut msg, fmt.alloc); + + Some(Diagnostic { + range, + severity: Some(severity), + code: None, + code_description: None, + source: None, + message: msg, + related_information: None, + tags: None, + data: None, + }) + } + } +} diff --git a/crates/lang_srv/src/registry.rs b/crates/lang_srv/src/registry.rs new file mode 100644 index 0000000000..3b5a60c2ae --- /dev/null +++ b/crates/lang_srv/src/registry.rs @@ -0,0 +1,75 @@ +use std::collections::HashMap; + +use tower_lsp::lsp_types::{ + Diagnostic, GotoDefinitionResponse, Hover, Position, SemanticTokensResult, TextEdit, Url, +}; + +use crate::analysis::{AnalyzedDocument, GlobalAnalysis}; + +pub(crate) enum DocumentChange { + Modified(Url, String), + Closed(Url), +} + +#[derive(Debug, Default)] +pub(crate) struct Registry { + documents: HashMap, +} + +impl Registry { + pub fn apply_change(&mut self, change: DocumentChange) { + match change { + DocumentChange::Modified(url, source) => { + let GlobalAnalysis { documents } = GlobalAnalysis::new(url, source); + + // Only replace the set of documents and all dependencies that were re-analyzed. + // Note that this is actually the opposite of what we want - in truth we want to + // re-evaluate all dependents! + for document in documents { + let url = document.url().clone(); + self.documents.insert(url.clone(), document); + } + } + DocumentChange::Closed(_url) => { + // Do nothing. + } + } + } + + fn document_by_url(&mut self, url: &Url) -> Option<&mut AnalyzedDocument> { + self.documents.get_mut(url) + } + + pub fn diagnostics(&mut self, url: &Url) -> Vec { + let Some(document) = self.document_by_url(url) else { + return vec![]; + }; + document.diagnostics() + } + + pub fn hover(&mut self, url: &Url, position: Position) -> Option { + self.document_by_url(url)?.hover(position) + } + + pub fn goto_definition( + &mut self, + url: &Url, + position: Position, + ) -> Option { + let document = self.document_by_url(url)?; + let symbol = document.symbol_at(position)?; + let def_document_url = document.module_url(symbol.module_id())?; + let def_document = self.document_by_url(&def_document_url)?; + def_document.definition(symbol) + } + + pub fn formatting(&mut self, url: &Url) -> Option> { + let document = self.document_by_url(url)?; + document.format() + } + + pub fn semantic_tokens(&mut self, url: &Url) -> Option { + let document = self.document_by_url(url)?; + document.semantic_tokens() + } +} diff --git a/crates/lang_srv/src/server.rs b/crates/lang_srv/src/server.rs new file mode 100644 index 0000000000..cef37b3b66 --- /dev/null +++ b/crates/lang_srv/src/server.rs @@ -0,0 +1,208 @@ +use analysis::HIGHLIGHT_TOKENS_LEGEND; +use parking_lot::{Mutex, MutexGuard}; +use registry::{DocumentChange, Registry}; +use tower_lsp::jsonrpc::Result; +use tower_lsp::lsp_types::*; +use tower_lsp::{Client, LanguageServer, LspService, Server}; + +mod analysis; +mod convert; +mod registry; + +#[derive(Debug)] +struct RocLs { + client: Client, + registry: Mutex, +} + +impl std::panic::RefUnwindSafe for RocLs {} + +impl RocLs { + pub fn new(client: Client) -> Self { + Self { + client, + registry: Mutex::new(Registry::default()), + } + } + + fn registry(&self) -> MutexGuard { + self.registry.lock() + } + + pub fn capabilities() -> ServerCapabilities { + let text_document_sync = TextDocumentSyncCapability::Options( + // TODO: later on make this incremental + TextDocumentSyncOptions { + open_close: Some(true), + change: Some(TextDocumentSyncKind::FULL), + ..TextDocumentSyncOptions::default() + }, + ); + let hover_provider = HoverProviderCapability::Simple(true); + let definition_provider = DefinitionOptions { + work_done_progress_options: WorkDoneProgressOptions { + work_done_progress: None, + }, + }; + let document_formatting_provider = DocumentFormattingOptions { + work_done_progress_options: WorkDoneProgressOptions { + work_done_progress: None, + }, + }; + let semantic_tokens_provider = + SemanticTokensServerCapabilities::SemanticTokensOptions(SemanticTokensOptions { + work_done_progress_options: WorkDoneProgressOptions { + work_done_progress: None, + }, + legend: SemanticTokensLegend { + token_types: HIGHLIGHT_TOKENS_LEGEND.into(), + token_modifiers: vec![], + }, + range: None, + full: Some(SemanticTokensFullOptions::Bool(true)), + }); + + ServerCapabilities { + text_document_sync: Some(text_document_sync), + hover_provider: Some(hover_provider), + definition_provider: Some(OneOf::Right(definition_provider)), + document_formatting_provider: Some(OneOf::Right(document_formatting_provider)), + semantic_tokens_provider: Some(semantic_tokens_provider), + ..ServerCapabilities::default() + } + } + + /// Records a document content change. + async fn change(&self, fi: Url, text: String, version: i32) { + self.registry() + .apply_change(DocumentChange::Modified(fi.clone(), text)); + + let diagnostics = match std::panic::catch_unwind(|| self.registry().diagnostics(&fi)) { + Ok(ds) => ds, + Err(_) => return, + }; + + self.client + .publish_diagnostics(fi, diagnostics, Some(version)) + .await; + } + + async fn close(&self, fi: Url) { + self.registry().apply_change(DocumentChange::Closed(fi)); + } +} + +#[tower_lsp::async_trait] +impl LanguageServer for RocLs { + async fn initialize(&self, _: InitializeParams) -> Result { + Ok(InitializeResult { + capabilities: Self::capabilities(), + ..InitializeResult::default() + }) + } + + async fn initialized(&self, _: InitializedParams) { + self.client + .log_message(MessageType::INFO, "Roc language server initialized.") + .await; + } + + async fn did_open(&self, params: DidOpenTextDocumentParams) { + let TextDocumentItem { + uri, text, version, .. + } = params.text_document; + self.change(uri, text, version).await; + } + + async fn did_change(&self, params: DidChangeTextDocumentParams) { + let VersionedTextDocumentIdentifier { uri, version, .. } = params.text_document; + + // NOTE: We specify that we expect full-content syncs in the server capabilities, + // so here we assume the only change passed is a change of the entire document's content. + let TextDocumentContentChangeEvent { text, .. } = + params.content_changes.into_iter().next().unwrap(); + + self.change(uri, text, version).await; + } + + async fn did_close(&self, params: DidCloseTextDocumentParams) { + let TextDocumentIdentifier { uri } = params.text_document; + self.close(uri).await; + } + + async fn shutdown(&self) -> Result<()> { + Ok(()) + } + + async fn hover(&self, params: HoverParams) -> Result> { + let HoverParams { + text_document_position_params: + TextDocumentPositionParams { + text_document, + position, + }, + work_done_progress_params: _, + } = params; + + panic_wrapper(|| self.registry().hover(&text_document.uri, position)) + } + + async fn goto_definition( + &self, + params: GotoDefinitionParams, + ) -> Result> { + let GotoDefinitionParams { + text_document_position_params: + TextDocumentPositionParams { + text_document, + position, + }, + work_done_progress_params: _, + partial_result_params: _, + } = params; + + panic_wrapper(|| { + self.registry() + .goto_definition(&text_document.uri, position) + }) + } + + async fn formatting(&self, params: DocumentFormattingParams) -> Result>> { + let DocumentFormattingParams { + text_document, + options: _, + work_done_progress_params: _, + } = params; + + panic_wrapper(|| self.registry().formatting(&text_document.uri)) + } + + async fn semantic_tokens_full( + &self, + params: SemanticTokensParams, + ) -> Result> { + let SemanticTokensParams { + text_document, + work_done_progress_params: _, + partial_result_params: _, + } = params; + + panic_wrapper(|| self.registry().semantic_tokens(&text_document.uri)) + } +} + +fn panic_wrapper(f: impl FnOnce() -> Option + std::panic::UnwindSafe) -> Result> { + match std::panic::catch_unwind(f) { + Ok(r) => Ok(r), + Err(_) => Err(tower_lsp::jsonrpc::Error::internal_error()), + } +} + +#[tokio::main] +async fn main() { + let stdin = tokio::io::stdin(); + let stdout = tokio::io::stdout(); + + let (service, socket) = LspService::new(RocLs::new); + Server::new(stdin, stdout, socket).serve(service).await; +} diff --git a/crates/linker/Cargo.toml b/crates/linker/Cargo.toml new file mode 100644 index 0000000000..dac662a678 --- /dev/null +++ b/crates/linker/Cargo.toml @@ -0,0 +1,40 @@ +[package] +name = "roc_linker" +description = "A surgical linker for Roc" + +authors.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true +version.workspace = true + +[lib] +name = "roc_linker" +path = "src/lib.rs" + +[dependencies] +roc_collections = { path = "../compiler/collections" } +roc_error_macros = { path = "../error_macros" } +roc_module = { path = "../compiler/module" } +roc_load = { path = "../compiler/load" } +roc_mono = { path = "../compiler/mono" } +roc_packaging = { path = "../packaging" } +roc_reporting = { path = "../reporting" } +roc_solve = { path = "../compiler/solve" } +roc_target = { path = "../compiler/roc_target" } + +bincode.workspace = true +bumpalo.workspace = true +iced-x86.workspace = true +mach_object.workspace = true +memmap2.workspace = true +object.workspace = true +serde.workspace = true +target-lexicon.workspace = true +tempfile.workspace = true + + +[dev-dependencies] +indoc.workspace = true +libc.workspace = true +serial_test.workspace = true diff --git a/crates/linker/README.md b/crates/linker/README.md new file mode 100644 index 0000000000..d43c521f85 --- /dev/null +++ b/crates/linker/README.md @@ -0,0 +1,45 @@ +# The Roc Surgical Linker + +This linker has the goal of being extremely slim lined and fast. +It is focused on the scope of only linking platforms to Roc applications. +This restriction enables ignoring most of linking. + +## General Overview + +This linker is run in 2 phases: preprocessing and surgical linking. + +### Platform Preprocessor + +1. Dynamically link the platform to a dummy Roc application dynamic library +1. Create metadata related to Roc dynamically linked functions + - Symbols that need to be redefined + - Call locations that need to be modified for each symbol + - Locations of special roc functions (roc_alloc, roc_dealloc, builtins, etc) +1. Modify the main executable to no longer be dynamically link + - Delete dependency on dynamic library + - Remove symbols from the dynamic table (maybe add them to the regular table?) + - Delete GOT and PLT entries + - Remove relocations from the dynamic table + - Add extra header information about new text and data section at end of file + +### Surgical Linker + +1. Build off of preprocessed platform +1. Append text and data of application, dealing with app relocations +1. Surgically update all call locations in the platform +1. Surgically update call information in the application (also dealing with other relocations for builtins) + +## TODO (In a lightly prioritized order) + +- Add Macho support + - Honestly should be almost exactly the same code. + This means we likely need to do a lot of refactoring to minimize the duplicate code. + The fun of almost but not quite the same. +- Add PE support + - As a prereq, we need roc building on Windows (I'm not sure it does currently). + - Definitely a solid bit different than elf, but hopefully after refactoring for Macho, won't be that crazy to add. +- Look at enabling completely in memory linking that could be used with `roc run` and/or `roc repl` +- Look more into rust hosts and keeping certain functions. Currently I just disabled linker garbage collection. + This works but adds 1.2MB (40%) to even a tiny app. It may be a size issue for large rust hosts. + Roc, for reference, adds 13MB (20%) when linked without garbage collection. +- Add a feature to the compiler to make this linker optional. diff --git a/crates/linker/dynhost_benchmarks_elf64 b/crates/linker/dynhost_benchmarks_elf64 new file mode 100755 index 0000000000..b51fc7f932 Binary files /dev/null and b/crates/linker/dynhost_benchmarks_elf64 differ diff --git a/crates/linker/dynhost_benchmarks_windows.exe b/crates/linker/dynhost_benchmarks_windows.exe new file mode 100755 index 0000000000..4f8c9e8ed5 Binary files /dev/null and b/crates/linker/dynhost_benchmarks_windows.exe differ diff --git a/crates/linker/src/elf.rs b/crates/linker/src/elf.rs new file mode 100644 index 0000000000..37458bdc4d --- /dev/null +++ b/crates/linker/src/elf.rs @@ -0,0 +1,1939 @@ +use bincode::{deserialize_from, serialize_into}; +use iced_x86::{Decoder, DecoderOptions, Instruction, OpCodeOperandKind, OpKind}; +use memmap2::MmapMut; +use object::{elf, endian}; +use object::{ + CompressedFileRange, CompressionFormat, LittleEndian as LE, Object, ObjectSection, + ObjectSymbol, RelocationKind, RelocationTarget, Section, SectionIndex, SectionKind, Symbol, + SymbolIndex, SymbolSection, +}; +use roc_collections::all::MutMap; +use roc_error_macros::{internal_error, user_error}; +use serde::{Deserialize, Serialize}; +use std::{ + ffi::{c_char, CStr}, + io::{BufReader, BufWriter}, + mem, + path::Path, + time::{Duration, Instant}, +}; + +use crate::{ + align_by_constraint, align_to_offset_by_constraint, load_struct_inplace, + load_struct_inplace_mut, load_structs_inplace_mut, open_mmap, open_mmap_mut, +}; + +const MIN_SECTION_ALIGNMENT: usize = 0x40; + +// TODO: Analyze if this offset is always correct. +const PLT_ADDRESS_OFFSET: u64 = 0x10; + +struct ElfDynamicDeps { + got_app_syms: Vec<(String, usize)>, + got_sections: Vec<(usize, usize)>, + app_sym_indices: Vec, + dynamic_lib_count: usize, + shared_lib_index: usize, +} + +#[derive(Serialize, Deserialize, PartialEq, Eq, Debug)] +enum VirtualOffset { + Absolute, + Relative(u64), +} + +#[derive(Serialize, Deserialize, PartialEq, Eq, Debug)] +struct SurgeryEntry { + file_offset: u64, + virtual_offset: VirtualOffset, + size: u8, +} + +// TODO: Reanalyze each piece of data in this struct. +// I think a number of them can be combined to reduce string duplication. +// Also I think a few of them aren't need. +// For example, I think preprocessing can deal with all shifting and remove the need for added_byte_count. +// TODO: we probably should be storing numbers in an endian neutral way. +#[derive(Default, Serialize, Deserialize, PartialEq, Eq, Debug)] +struct Metadata { + app_functions: Vec, + // offset followed by address. + plt_addresses: MutMap, + surgeries: MutMap>, + dynamic_symbol_indices: MutMap, + static_symbol_indices: MutMap, + roc_symbol_vaddresses: MutMap, + exec_len: u64, + load_align_constraint: u64, + added_byte_count: u64, + last_vaddr: u64, + dynamic_section_offset: u64, + dynamic_symbol_table_section_offset: u64, + symbol_table_section_offset: u64, + symbol_table_size: u64, + _macho_cmd_loc: u64, +} + +impl Metadata { + fn write_to_file(&self, metadata_filename: &Path) { + let metadata_file = + std::fs::File::create(metadata_filename).unwrap_or_else(|e| internal_error!("{}", e)); + + serialize_into(BufWriter::new(metadata_file), self) + .unwrap_or_else(|err| internal_error!("Failed to serialize metadata: {err}")); + } + + fn read_from_file(metadata_filename: &Path) -> Self { + let input = std::fs::File::open(metadata_filename).unwrap_or_else(|e| { + internal_error!( + r#" + + Error: + {}\n"#, + e + ) + }); + + match deserialize_from(BufReader::new(input)) { + Ok(data) => data, + Err(err) => { + internal_error!("Failed to deserialize metadata: {}", err); + } + } + } +} + +fn report_timing(label: &str, duration: Duration) { + println!("\t{:9.3} ms {}", duration.as_secs_f64() * 1000.0, label,); +} + +fn is_roc_symbol(sym: &object::Symbol) -> bool { + if let Ok(name) = sym.name() { + name.trim_start_matches('_').starts_with("roc_") + } else { + false + } +} + +fn is_roc_definition(sym: &object::Symbol) -> bool { + sym.is_definition() && is_roc_symbol(sym) +} + +fn is_roc_undefined(sym: &object::Symbol) -> bool { + sym.is_undefined() && is_roc_symbol(sym) +} + +fn collect_roc_definitions<'a>(object: &object::File<'a, &'a [u8]>) -> MutMap { + let mut vaddresses = MutMap::default(); + + for sym in object.symbols().filter(is_roc_definition) { + // remove potentially trailing "@version". + let name = sym + .name() + .unwrap() + .trim_start_matches('_') + .split('@') + .next() + .unwrap(); + + let address = sym.address(); + + // special exceptions for roc_ functions that map to libc symbols + let direct_mapping = match name { + "roc_memset" => Some("memset"), + "roc_memmove" => Some("memmove"), + + // for expects + "roc_mmap" => Some("mmap"), + "roc_getppid" => Some("getppid"), + "roc_shm_open" => Some("shm_open"), + + _ => None, + }; + + if let Some(libc_symbol) = direct_mapping { + vaddresses.insert(libc_symbol.to_string(), address); + } + + vaddresses.insert(name.to_string(), address); + } + + vaddresses +} + +struct Surgeries<'a> { + surgeries: MutMap>, + app_func_addresses: MutMap, + indirect_warning_given: bool, +} + +impl<'a> Surgeries<'a> { + fn new(application_symbols: &[Symbol], app_func_addresses: MutMap) -> Self { + let mut surgeries = MutMap::default(); + + // for each symbol that the host expects from the application + // we start with an empty set of places to perform surgery + for symbol in application_symbols { + let name = symbol.name().unwrap().to_string(); + surgeries.insert(name, vec![]); + } + + Self { + surgeries, + app_func_addresses, + indirect_warning_given: false, + } + } + + fn append_text_sections( + &mut self, + object_bytes: &[u8], + object: &object::File<'a, &'a [u8]>, + verbose: bool, + ) { + let text_sections: Vec
= object + .sections() + .filter(|sec| sec.kind() == SectionKind::Text) + .collect(); + if text_sections.is_empty() { + internal_error!("No text sections found. This application has no code."); + } + if verbose { + println!(); + println!("Text Sections"); + for sec in text_sections.iter() { + println!("{sec:+x?}"); + } + } + + if verbose { + println!(); + println!("Analyzing instuctions for branches"); + } + + for text_section in text_sections { + self.append_text_section(object_bytes, &text_section, verbose) + } + } + + fn append_text_section(&mut self, object_bytes: &[u8], sec: &Section, verbose: bool) { + let (file_offset, compressed) = match sec.compressed_file_range() { + Ok(CompressedFileRange { + format: CompressionFormat::None, + offset, + .. + }) => (offset, false), + Ok(range) => (range.offset, true), + Err(err) => { + internal_error!( + "Issues dealing with section compression for {:+x?}: {}", + sec, + err + ); + } + }; + + let data = match sec.uncompressed_data() { + Ok(data) => data, + Err(err) => { + internal_error!("Failed to load text section, {:+x?}: {}", sec, err); + } + }; + let mut decoder = Decoder::with_ip(64, &data, sec.address(), DecoderOptions::NONE); + let mut inst = Instruction::default(); + + while decoder.can_decode() { + decoder.decode_out(&mut inst); + + // Note: This gets really complex fast if we want to support more than basic calls/jumps. + // A lot of them have to load addresses into registers/memory so we would have to discover that value. + // Would probably require some static code analysis and would be impossible in some cases. + // As an alternative we can leave in the calls to the plt, but change the plt to jmp to the static function. + // That way any indirect call will just have the overhead of an extra jump. + match inst.try_op_kind(0) { + // Relative Offsets. + Ok(OpKind::NearBranch16 | OpKind::NearBranch32 | OpKind::NearBranch64) => { + let target = inst.near_branch_target(); + if let Some(func_name) = self.app_func_addresses.get(&target) { + if compressed { + internal_error!("Surgical linking does not work with compressed text sections: {:+x?}", sec); + } + + if verbose { + println!( + "Found branch from {:+x} to {:+x}({})", + inst.ip(), + target, + func_name + ); + } + + // TODO: Double check these offsets are always correct. + // We may need to do a custom offset based on opcode instead. + let op_kind = inst.op_code().try_op_kind(0).unwrap(); + let op_size: u8 = match op_kind { + OpCodeOperandKind::br16_1 | OpCodeOperandKind::br32_1 => 1, + OpCodeOperandKind::br16_2 => 2, + OpCodeOperandKind::br32_4 | OpCodeOperandKind::br64_4 => 4, + _ => { + internal_error!( + "Ran into an unknown operand kind when analyzing branches: {:?}", + op_kind + ); + } + }; + let offset = inst.next_ip() - op_size as u64 - sec.address() + file_offset; + if verbose { + println!( + "\tNeed to surgically replace {op_size} bytes at file offset {offset:+x}", + ); + println!( + "\tIts current value is {:+x?}", + &object_bytes[offset as usize..(offset + op_size as u64) as usize] + ) + } + self.surgeries + .get_mut(*func_name) + .unwrap() + .push(SurgeryEntry { + file_offset: offset, + virtual_offset: VirtualOffset::Relative(inst.next_ip()), + size: op_size, + }); + } + } + Ok(OpKind::FarBranch16 | OpKind::FarBranch32) => { + internal_error!( + "Found branch type instruction that is not yet support: {:+x?}", + inst + ); + } + Ok(_) => { + if (inst.is_call_far_indirect() + || inst.is_call_near_indirect() + || inst.is_jmp_far_indirect() + || inst.is_jmp_near_indirect()) + && !self.indirect_warning_given + && verbose + { + self.indirect_warning_given = true; + println!(); + println!("Cannot analyze through indirect jmp type instructions"); + println!("Most likely this is not a problem, but it could mean a loss in optimizations"); + println!(); + } + } + Err(err) => { + internal_error!("Failed to decode assembly: {}", err); + } + } + } + } +} + +/// Constructs a `Metadata` from a host executable binary, and writes it to disk +pub(crate) fn preprocess_elf( + endianness: target_lexicon::Endianness, + host_exe_path: &Path, + metadata_path: &Path, + preprocessed_path: &Path, + shared_lib: &Path, + verbose: bool, + time: bool, +) { + let total_start = Instant::now(); + let exec_parsing_start = total_start; + let exec_data = &*open_mmap(host_exe_path); + let exec_obj = match object::File::parse(exec_data) { + Ok(obj) => obj, + Err(err) => { + internal_error!("Failed to parse executable file: {}", err); + } + }; + + let mut md = Metadata { + roc_symbol_vaddresses: collect_roc_definitions(&exec_obj), + ..Default::default() + }; + + if verbose { + println!( + "Found {} roc symbol definitions:", + md.roc_symbol_vaddresses.len() + ); + + let (mut builtins, mut other): (Vec<_>, Vec<_>) = md + .roc_symbol_vaddresses + .iter() + .partition(|(n, _)| n.starts_with("roc_builtins")); + + // sort by address + builtins.sort_by_key(|t| t.1); + other.sort_by_key(|t| t.1); + + for (name, vaddr) in other.iter() { + println!("\t{vaddr:#08x}: {name}"); + } + + println!("Of which {} are builtins", builtins.len(),); + + for (name, vaddr) in builtins.iter() { + println!("\t{vaddr:#08x}: {name}"); + } + } + + let exec_parsing_duration = exec_parsing_start.elapsed(); + + // PLT stands for Procedure Linkage Table which is, put simply, used to call external + // procedures/functions whose address isn't known in the time of linking, and is left + // to be resolved by the dynamic linker at run time. + let symbol_and_plt_processing_start = Instant::now(); + let plt_section_name = ".plt"; + let (plt_address, plt_offset) = match exec_obj.section_by_name(plt_section_name) { + Some(section) => { + let file_offset = match section.compressed_file_range() { + Ok( + range @ CompressedFileRange { + format: CompressionFormat::None, + .. + }, + ) => range.offset, + _ => { + internal_error!("Surgical linking does not work with compressed plt section"); + } + }; + (section.address(), file_offset) + } + None => { + internal_error!("Failed to find PLT section. Probably an malformed executable."); + } + }; + if verbose { + println!("PLT Address: {plt_address:+x}"); + println!("PLT File Offset: {plt_offset:+x}"); + } + + let app_syms: Vec<_> = exec_obj + .dynamic_symbols() + .filter(is_roc_undefined) + .collect(); + + let mut app_func_addresses: MutMap = MutMap::default(); + + let plt_relocs = (match exec_obj.dynamic_relocations() { + Some(relocs) => relocs, + None => { + internal_error!("Executable does not have any dynamic relocations. No work to do. Probably an invalid input."); + } + }) + .filter_map(|(_, reloc)| { + if let RelocationKind::Elf(7) = reloc.kind() { + Some(reloc) + } else { + None + } + }); + for (i, reloc) in plt_relocs.enumerate() { + for symbol in app_syms.iter() { + if reloc.target() == RelocationTarget::Symbol(symbol.index()) { + let func_address = (i as u64 + 1) * PLT_ADDRESS_OFFSET + plt_address; + let func_offset = (i as u64 + 1) * PLT_ADDRESS_OFFSET + plt_offset; + app_func_addresses.insert(func_address, symbol.name().unwrap()); + md.plt_addresses.insert( + symbol.name().unwrap().to_string(), + (func_offset, func_address), + ); + break; + } + } + } + + for sym in app_syms.iter() { + let name = sym.name().unwrap().to_string(); + md.app_functions.push(name.clone()); + md.dynamic_symbol_indices.insert(name, sym.index().0 as u64); + } + for sym in exec_obj.symbols().filter(is_roc_undefined) { + let name = sym.name().unwrap().to_string(); + md.static_symbol_indices.insert(name, sym.index().0 as u64); + } + + if verbose { + println!(); + println!("PLT Symbols for App Functions"); + for symbol in app_syms.iter() { + println!("{}: {:+x?}", symbol.index().0, symbol); + } + + println!(); + println!("App Function Address Map: {app_func_addresses:+x?}"); + } + let symbol_and_plt_processing_duration = symbol_and_plt_processing_start.elapsed(); + + // look at the text (i.e. code) sections and see collect work needs to be done + let text_disassembly_start = Instant::now(); + + let mut surgeries = Surgeries::new(&app_syms, app_func_addresses); + surgeries.append_text_sections(exec_data, &exec_obj, verbose); + md.surgeries = surgeries.surgeries; + + let text_disassembly_duration = text_disassembly_start.elapsed(); + + let scanning_dynamic_deps_duration; + let platform_gen_start; + + let out_mmap = match endianness { + target_lexicon::Endianness::Little => { + let scanning_dynamic_deps_start = Instant::now(); + + let ElfDynamicDeps { + got_app_syms, + got_sections, + app_sym_indices, + dynamic_lib_count, + shared_lib_index, + } = scan_elf_dynamic_deps( + &exec_obj, &mut md, &app_syms, shared_lib, exec_data, verbose, + ); + + scanning_dynamic_deps_duration = scanning_dynamic_deps_start.elapsed(); + + platform_gen_start = Instant::now(); + + // TODO little endian + gen_elf_le( + exec_data, + &mut md, + preprocessed_path, + &got_app_syms, + &got_sections, + &app_sym_indices, + dynamic_lib_count, + shared_lib_index, + verbose, + ) + } + target_lexicon::Endianness::Big => { + // TODO probably need to make gen_elf a macro to get this + // to work, which is annoying. A parameterized function + // does *not* work. + todo!("Roc does not yet support big-endian ELF hosts!"); + } + }; + + let platform_gen_duration = platform_gen_start.elapsed(); + + if verbose { + println!(); + println!("{md:+x?}"); + } + + let saving_metadata_start = Instant::now(); + md.write_to_file(metadata_path); + let saving_metadata_duration = saving_metadata_start.elapsed(); + + let flushing_data_start = Instant::now(); + out_mmap + .flush() + .unwrap_or_else(|e| internal_error!("{}", e)); + // Also drop files to to ensure data is fully written here. + drop(out_mmap); + let flushing_data_duration = flushing_data_start.elapsed(); + + let total_duration = total_start.elapsed(); + + if verbose || time { + println!(); + println!("Timings"); + report_timing("Executable Parsing", exec_parsing_duration); + report_timing( + "Symbol and PLT Processing", + symbol_and_plt_processing_duration, + ); + report_timing("Text Disassembly", text_disassembly_duration); + report_timing("Scanning Dynamic Deps", scanning_dynamic_deps_duration); + report_timing("Generate Modified Platform", platform_gen_duration); + report_timing("Saving Metadata", saving_metadata_duration); + report_timing("Flushing Data to Disk", flushing_data_duration); + report_timing( + "Other", + total_duration + - exec_parsing_duration + - symbol_and_plt_processing_duration + - text_disassembly_duration + - scanning_dynamic_deps_duration + - platform_gen_duration + - saving_metadata_duration + - flushing_data_duration, + ); + report_timing("Total", total_duration); + } +} + +#[allow(clippy::too_many_arguments)] +fn gen_elf_le( + exec_data: &[u8], + md: &mut Metadata, + preprocessed_path: &Path, + got_app_syms: &[(String, usize)], + got_sections: &[(usize, usize)], + app_sym_indices: &[usize], + dynamic_lib_count: usize, + shared_lib_index: usize, + verbose: bool, +) -> MmapMut { + let exec_header = load_struct_inplace::>(exec_data, 0); + let ph_offset = exec_header.e_phoff.get(LE); + let ph_ent_size = exec_header.e_phentsize.get(LE); + let ph_num = exec_header.e_phnum.get(LE); + let sh_offset = exec_header.e_shoff.get(LE); + let sh_ent_size = exec_header.e_shentsize.get(LE); + let sh_num = exec_header.e_shnum.get(LE); + + if verbose { + println!(); + println!("PH Offset: {ph_offset:+x}"); + println!("PH Entry Size: {ph_ent_size}"); + println!("PH Entry Count: {ph_num}"); + println!("SH Offset: {sh_offset:+x}"); + println!("SH Entry Size: {sh_ent_size}"); + println!("SH Entry Count: {sh_num}"); + } + + // Copy header and shift everything to enable more program sections. + let added_header_count = 3; + md.added_byte_count = ph_ent_size as u64 * added_header_count; + md.added_byte_count = md.added_byte_count + + (MIN_SECTION_ALIGNMENT as u64 - md.added_byte_count % MIN_SECTION_ALIGNMENT as u64); + let ph_end = ph_offset as usize + ph_num as usize * ph_ent_size as usize; + let physical_shift_start = ph_end as u64; + + md.exec_len = exec_data.len() as u64 + md.added_byte_count; + let mut out_mmap = open_mmap_mut(preprocessed_path, md.exec_len as usize); + + out_mmap[..ph_end].copy_from_slice(&exec_data[..ph_end]); + + let program_headers = load_structs_inplace_mut::>( + &mut out_mmap, + ph_offset as usize, + ph_num as usize, + ); + let mut first_load_found = false; + let mut virtual_shift_start = 0; + for ph in program_headers.iter() { + let p_type = ph.p_type.get(LE); + if p_type == elf::PT_LOAD && ph.p_offset.get(LE) == 0 { + first_load_found = true; + md.load_align_constraint = ph.p_align.get(LE); + virtual_shift_start = physical_shift_start + ph.p_vaddr.get(LE); + } + } + if !first_load_found { + user_error!("Executable does not load any data at 0x00000000\nProbably input the wrong file as the executable"); + } + if verbose { + println!("Shifting all data after: {physical_shift_start:+x}({virtual_shift_start:+x})"); + } + + // Shift all of the program headers. + for ph in program_headers.iter_mut() { + let p_type = ph.p_type.get(LE); + let p_offset = ph.p_offset.get(LE); + if (p_type == elf::PT_LOAD && p_offset == 0) || p_type == elf::PT_PHDR { + // Extend length for the first segment and the program header. + ph.p_filesz + .set(LE, ph.p_filesz.get(LE) + md.added_byte_count); + ph.p_memsz.set(LE, ph.p_memsz.get(LE) + md.added_byte_count); + } else { + // Shift if needed. + if physical_shift_start <= p_offset { + ph.p_offset.set(LE, p_offset + md.added_byte_count); + } + let p_vaddr = ph.p_vaddr.get(LE); + if virtual_shift_start <= p_vaddr { + ph.p_vaddr.set(LE, p_vaddr + md.added_byte_count); + ph.p_paddr.set(LE, p_vaddr + md.added_byte_count); + } + } + } + + // Get last segment virtual address. + let last_segment_vaddr = program_headers + .iter() + .filter_map(|ph| { + if ph.p_type.get(LE) != elf::PT_GNU_STACK { + Some(ph.p_vaddr.get(LE) + ph.p_memsz.get(LE)) + } else { + None + } + }) + .max() + .unwrap(); + + // Copy the rest of the file shifted as needed. + out_mmap[physical_shift_start as usize + md.added_byte_count as usize..] + .copy_from_slice(&exec_data[physical_shift_start as usize..]); + + // Update all sections for shift for extra program headers. + let section_headers = load_structs_inplace_mut::>( + &mut out_mmap, + sh_offset as usize + md.added_byte_count as usize, + sh_num as usize, + ); + + let mut rel_sections: Vec<(u64, u64)> = vec![]; + let mut rela_sections: Vec<(usize, u64, u64)> = vec![]; + for (i, sh) in section_headers.iter_mut().enumerate() { + let sh_offset = sh.sh_offset.get(LE); + let sh_addr = sh.sh_addr.get(LE); + if physical_shift_start <= sh_offset { + sh.sh_offset.set(LE, sh_offset + md.added_byte_count); + } + if virtual_shift_start <= sh_addr { + sh.sh_addr.set(LE, sh_addr + md.added_byte_count); + } + + // Record every relocation section. + let sh_type = sh.sh_type.get(LE); + if sh_type == elf::SHT_REL { + rel_sections.push((sh_offset, sh.sh_size.get(LE))); + } else if sh_type == elf::SHT_RELA { + rela_sections.push((i, sh_offset, sh.sh_size.get(LE))); + } + } + + // Get last section virtual address. + let last_section_vaddr = section_headers + .iter() + .map(|sh| sh.sh_addr.get(LE) + sh.sh_size.get(LE)) + .max() + .unwrap(); + + // Calculate end virtual address for new segment. + // TODO: potentially remove md.load_align_constraint here. I think we should be able to cram things together. + md.last_vaddr = + std::cmp::max(last_section_vaddr, last_segment_vaddr) + md.load_align_constraint; + + // Update all relocations for shift for extra program headers. + for (sec_offset, sec_size) in rel_sections { + let relocations = load_structs_inplace_mut::>( + &mut out_mmap, + sec_offset as usize + md.added_byte_count as usize, + sec_size as usize / mem::size_of::>(), + ); + for rel in relocations.iter_mut() { + let r_offset = rel.r_offset.get(LE); + if virtual_shift_start <= r_offset { + rel.r_offset.set(LE, r_offset + md.added_byte_count); + } + } + } + + let dyn_offset = md.dynamic_section_offset + md.added_byte_count; + for (sec_index, sec_offset, sec_size) in rela_sections { + let relocations = load_structs_inplace_mut::>( + &mut out_mmap, + sec_offset as usize + md.added_byte_count as usize, + sec_size as usize / mem::size_of::>(), + ); + for (i, rel) in relocations.iter_mut().enumerate() { + let r_offset = rel.r_offset.get(LE); + if virtual_shift_start <= r_offset { + rel.r_offset.set(LE, r_offset + md.added_byte_count); + // Deal with potential adjusts to absolute jumps. + // TODO: Verify other relocation types. + if rel.r_type(LE, false) == elf::R_X86_64_RELATIVE { + let r_addend = rel.r_addend.get(LE); + rel.r_addend.set(LE, r_addend + md.added_byte_count as i64); + } + } + // If the relocation goes to a roc function, we need to surgically link it and change it to relative. + let r_type = rel.r_type(LE, false); + if r_type == elf::R_X86_64_GLOB_DAT { + let r_sym = rel.r_sym(LE, false); + for (name, index) in got_app_syms.iter() { + if *index as u32 == r_sym { + rel.set_r_info(LE, false, 0, elf::R_X86_64_RELATIVE); + let addend_addr = sec_offset as usize + + i * mem::size_of::>() + // This 16 skips the first 2 fields and gets to the addend field. + + 16; + md.surgeries.get_mut(name).unwrap().push(SurgeryEntry { + file_offset: addend_addr as u64, + virtual_offset: VirtualOffset::Absolute, + size: 8, + }); + } + } + } + } + // To correctly remove the JUMP_SLOT relocations for Roc functions we: + // 1. collect the indicies of all of them. + // 2. move them all to the end of the relocation sections. + // 3. shrink the relocation section to ignore them. + // 4. update the dynamic section to reflect the shrink as well. + let mut to_remove = relocations + .iter() + .enumerate() + .filter_map(|(i, rel)| { + let r_type = rel.r_type(LE, false); + let r_sym = rel.r_sym(LE, false); + if r_type == elf::R_X86_64_JUMP_SLOT && app_sym_indices.contains(&(r_sym as usize)) + { + Some(i) + } else { + None + } + }) + .collect::>(); + + // We must remove in descending order to avoid swapping an element more than once and messing up the removal. + to_remove.sort(); + to_remove.reverse(); + + let mut j = relocations.len() - 1; + for i in to_remove.iter() { + relocations.swap(*i, j); + let r_sym = relocations[j].r_sym(LE, false); + relocations[j].set_r_info(LE, false, r_sym, elf::R_X86_64_NONE); + j -= 1; + } + + let section_headers = load_structs_inplace_mut::>( + &mut out_mmap, + sh_offset as usize + md.added_byte_count as usize, + sh_num as usize, + ); + + let old_size = section_headers[sec_index].sh_size.get(LE); + let removed_count = to_remove.len(); + let removed_size = removed_count * std::mem::size_of::>(); + section_headers[sec_index] + .sh_size + .set(LE, old_size - removed_size as u64); + + let dyns = load_structs_inplace_mut::>( + &mut out_mmap, + dyn_offset as usize, + dynamic_lib_count, + ); + let is_rela_dyn = dyns + .iter() + .filter(|d| { + let tag = d.d_tag.get(LE) as u32; + tag == elf::DT_RELA + }) + .any(|d| d.d_val.get(LE) == sec_offset); + let is_rela_plt = dyns + .iter() + .filter(|d| { + let tag = d.d_tag.get(LE) as u32; + tag == elf::DT_JMPREL + }) + .any(|d| d.d_val.get(LE) == sec_offset); + + for d in dyns.iter_mut() { + match d.d_tag.get(LE) as u32 { + elf::DT_RELACOUNT if is_rela_dyn => { + let old_count = d.d_val.get(LE); + d.d_val.set(LE, old_count - removed_count as u64); + } + elf::DT_RELASZ if is_rela_dyn => { + let old_size = d.d_val.get(LE); + d.d_val.set(LE, old_size - removed_size as u64); + } + elf::DT_PLTRELSZ if is_rela_plt => { + let old_size = d.d_val.get(LE); + d.d_val.set(LE, old_size - removed_size as u64); + } + _ => {} + } + } + } + + // Update dynamic table entries for shift for extra program headers. + let dyns = load_structs_inplace_mut::>( + &mut out_mmap, + dyn_offset as usize, + dynamic_lib_count, + ); + for d in dyns { + match d.d_tag.get(LE) as u32 { + // I believe this is the list of symbols that need to be update if addresses change. + // I am less sure about the symbols from GNU_HASH down. + elf::DT_INIT + | elf::DT_FINI + | elf::DT_PLTGOT + | elf::DT_HASH + | elf::DT_STRTAB + | elf::DT_SYMTAB + | elf::DT_RELA + | elf::DT_REL + | elf::DT_DEBUG + | elf::DT_JMPREL + | elf::DT_INIT_ARRAY + | elf::DT_FINI_ARRAY + | elf::DT_PREINIT_ARRAY + | elf::DT_SYMTAB_SHNDX + | elf::DT_GNU_HASH + | elf::DT_TLSDESC_PLT + | elf::DT_TLSDESC_GOT + | elf::DT_GNU_CONFLICT + | elf::DT_GNU_LIBLIST + | elf::DT_CONFIG + | elf::DT_DEPAUDIT + | elf::DT_AUDIT + | elf::DT_PLTPAD + | elf::DT_MOVETAB + | elf::DT_SYMINFO + | elf::DT_VERSYM + | elf::DT_VERDEF + | elf::DT_VERNEED => { + let d_addr = d.d_val.get(LE); + if virtual_shift_start <= d_addr { + d.d_val.set(LE, d_addr + md.added_byte_count); + } + } + _ => {} + } + } + + // Update symbol table entries for shift for extra program headers. + let symtab_offset = md.symbol_table_section_offset + md.added_byte_count; + let symtab_size = md.symbol_table_size as usize; + + let symbols = load_structs_inplace_mut::>( + &mut out_mmap, + symtab_offset as usize, + symtab_size / mem::size_of::>(), + ); + + for sym in symbols { + let addr = sym.st_value.get(LE); + if virtual_shift_start <= addr { + sym.st_value.set(LE, addr + md.added_byte_count); + } + } + + // Update all data in the global offset table. + for (offset, size) in got_sections { + let global_offsets = load_structs_inplace_mut::>( + &mut out_mmap, + *offset + md.added_byte_count as usize, + size / mem::size_of::>(), + ); + for go in global_offsets.iter_mut() { + let go_addr = go.get(LE); + if physical_shift_start <= go_addr { + go.set(LE, go_addr + md.added_byte_count); + } + } + } + + // TODO: look into shifting all of the debug info and eh_frames. + + // Delete shared library from the dynamic table. + let out_ptr = out_mmap.as_mut_ptr(); + unsafe { + std::ptr::copy( + out_ptr.add(dyn_offset as usize + 16 * (shared_lib_index + 1)), + out_ptr.add(dyn_offset as usize + 16 * shared_lib_index), + 16 * (dynamic_lib_count - shared_lib_index), + ); + } + + // Update main elf header for extra data. + let file_header = load_struct_inplace_mut::>(&mut out_mmap, 0); + file_header + .e_shoff + .set(LE, file_header.e_shoff.get(LE) + md.added_byte_count); + let e_entry = file_header.e_entry.get(LE); + if virtual_shift_start <= e_entry { + file_header.e_entry.set(LE, e_entry + md.added_byte_count); + } + file_header + .e_phnum + .set(LE, ph_num + added_header_count as u16); + + out_mmap +} + +fn scan_elf_dynamic_deps( + exec_obj: &object::File, + md: &mut Metadata, + app_syms: &[Symbol], + shared_lib: &Path, + exec_data: &[u8], + verbose: bool, +) -> ElfDynamicDeps { + let dyn_sec = match exec_obj.section_by_name(".dynamic") { + Some(sec) => sec, + None => { + panic!("There must be a dynamic section in the executable"); + } + }; + let dyn_offset = match dyn_sec.compressed_file_range() { + Ok( + range @ CompressedFileRange { + format: CompressionFormat::None, + .. + }, + ) => range.offset as usize, + _ => { + panic!("Surgical linking does not work with compressed dynamic section"); + } + }; + md.dynamic_section_offset = dyn_offset as u64; + + let dynstr_sec = match exec_obj.section_by_name(".dynstr") { + Some(sec) => sec, + None => { + panic!("There must be a dynstr section in the executable"); + } + }; + let dynstr_data = match dynstr_sec.uncompressed_data() { + Ok(data) => data, + Err(err) => { + panic!("Failed to load dynstr section: {err}"); + } + }; + + let shared_lib_filename = shared_lib.file_name(); + + let mut dyn_lib_index = 0; + let mut shared_lib_index = None; + loop { + let dyn_tag = u64::from_le_bytes( + <[u8; 8]>::try_from( + &exec_data[dyn_offset + dyn_lib_index * 16..dyn_offset + dyn_lib_index * 16 + 8], + ) + .unwrap(), + ); + if dyn_tag == 0 { + break; + } else if dyn_tag == 1 { + let dynstr_off = u64::from_le_bytes( + <[u8; 8]>::try_from( + &exec_data + [dyn_offset + dyn_lib_index * 16 + 8..dyn_offset + dyn_lib_index * 16 + 16], + ) + .unwrap(), + ) as usize; + let c_buf = dynstr_data[dynstr_off..].as_ptr() as *const c_char; + let c_str = unsafe { CStr::from_ptr(c_buf) }.to_str().unwrap(); + if Path::new(c_str).file_name() == shared_lib_filename { + shared_lib_index = Some(dyn_lib_index); + if verbose { + println!("Found shared lib in dynamic table at index: {dyn_lib_index}"); + } + } + } + + dyn_lib_index += 1; + } + let dynamic_lib_count = dyn_lib_index; + + if shared_lib_index.is_none() { + panic!("Shared lib not found as a dependency of the executable"); + } + let shared_lib_index = shared_lib_index.unwrap(); + + let symtab_sec = match exec_obj.section_by_name(".symtab") { + Some(sec) => sec, + None => { + panic!("There must be a symtab section in the executable"); + } + }; + let symtab_offset = match symtab_sec.compressed_file_range() { + Ok( + range @ CompressedFileRange { + format: CompressionFormat::None, + .. + }, + ) => range.offset as usize, + _ => { + panic!("Surgical linking does not work with compressed symtab section"); + } + }; + md.symbol_table_section_offset = symtab_offset as u64; + md.symbol_table_size = symtab_sec.size(); + + let dynsym_sec = match exec_obj.section_by_name(".dynsym") { + Some(sec) => sec, + None => { + panic!("There must be a dynsym section in the executable"); + } + }; + let dynsym_offset = match dynsym_sec.compressed_file_range() { + Ok( + range @ CompressedFileRange { + format: CompressionFormat::None, + .. + }, + ) => range.offset as usize, + _ => { + panic!("Surgical linking does not work with compressed dynsym section"); + } + }; + md.dynamic_symbol_table_section_offset = dynsym_offset as u64; + + let mut got_sections: Vec<(usize, usize)> = vec![]; + for sec in exec_obj + .sections() + .filter(|sec| sec.name().is_ok() && sec.name().unwrap().starts_with(".got")) + { + match sec.compressed_file_range() { + Ok( + range @ CompressedFileRange { + format: CompressionFormat::None, + .. + }, + ) => got_sections.push((range.offset as usize, range.uncompressed_size as usize)), + _ => { + panic!("Surgical linking does not work with compressed got sections"); + } + } + } + + let got_app_syms: Vec<(String, usize)> = (match exec_obj.dynamic_relocations() { + Some(relocs) => relocs, + None => { + eprintln!("Executable never calls any application functions."); + panic!("No work to do. Probably an invalid input."); + } + }) + .filter_map(|(_, reloc)| { + if let RelocationKind::Elf(elf::R_X86_64_GLOB_DAT) = reloc.kind() { + for symbol in app_syms.iter() { + if reloc.target() == RelocationTarget::Symbol(symbol.index()) { + return Some((symbol.name().unwrap().to_string(), symbol.index().0)); + } + } + } + None + }) + .collect(); + + let app_sym_indices: Vec = (match exec_obj.dynamic_relocations() { + Some(relocs) => relocs, + None => { + eprintln!("Executable never calls any application functions."); + panic!("No work to do. Probably an invalid input."); + } + }) + .filter_map(|(_, reloc)| { + if let RelocationKind::Elf(elf::R_X86_64_JUMP_SLOT) = reloc.kind() { + for symbol in app_syms.iter() { + if reloc.target() == RelocationTarget::Symbol(symbol.index()) { + return Some(symbol.index().0); + } + } + } + None + }) + .collect(); + + ElfDynamicDeps { + got_app_syms, + got_sections, + app_sym_indices, + dynamic_lib_count, + shared_lib_index, + } +} + +pub(crate) fn surgery_elf( + roc_app_bytes: &[u8], + metadata_path: &Path, + executable_path: &Path, + verbose: bool, + time: bool, +) { + let app_obj = match object::File::parse(roc_app_bytes) { + Ok(obj) => obj, + Err(err) => { + internal_error!("Failed to parse application file: {}", err); + } + }; + + if app_obj + .sections() + .filter(|sec| { + let name = sec.name().unwrap_or_default(); + !name.starts_with(".debug") && !name.starts_with(".eh") + }) + .flat_map(|sec| sec.relocations()) + .any(|(_, reloc)| reloc.kind() == RelocationKind::Absolute) + { + eprintln!("The surgical linker currently has issue #3609 and would fail linking your app."); + eprintln!("Please use `--linker=legacy` to avoid the issue for now."); + std::process::exit(1); + } + + let total_start = Instant::now(); + + let loading_metadata_start = total_start; + let md = Metadata::read_from_file(metadata_path); + let loading_metadata_duration = loading_metadata_start.elapsed(); + + let load_and_mmap_start = Instant::now(); + let max_out_len = md.exec_len + roc_app_bytes.len() as u64 + md.load_align_constraint; + let mut exec_mmap = open_mmap_mut(executable_path, max_out_len as usize); + let load_and_mmap_duration = load_and_mmap_start.elapsed(); + + let out_gen_start = Instant::now(); + let mut offset = 0; + + surgery_elf_help(verbose, &md, &mut exec_mmap, &mut offset, app_obj); + + let out_gen_duration = out_gen_start.elapsed(); + let flushing_data_start = Instant::now(); + + // TODO investigate using the async version of flush - might be faster due to not having to block on that + exec_mmap + .flush() + .unwrap_or_else(|e| internal_error!("{}", e)); + // Also drop files to to ensure data is fully written here. + drop(exec_mmap); + + let flushing_data_duration = flushing_data_start.elapsed(); + + // Make sure the final executable has permision to execute. + #[cfg(target_family = "unix")] + { + use std::fs; + use std::os::unix::fs::PermissionsExt; + + let mut perms = fs::metadata(executable_path) + .unwrap_or_else(|e| internal_error!("{}", e)) + .permissions(); + perms.set_mode(perms.mode() | 0o111); + fs::set_permissions(executable_path, perms).unwrap_or_else(|e| internal_error!("{}", e)); + } + + let total_duration = total_start.elapsed(); + + if verbose || time { + println!("\nTimings"); + report_timing("Loading Metadata", loading_metadata_duration); + report_timing("Loading and mmap-ing", load_and_mmap_duration); + report_timing("Output Generation", out_gen_duration); + report_timing("Flushing Data to Disk", flushing_data_duration); + + let sum = loading_metadata_duration + + load_and_mmap_duration + + out_gen_duration + + flushing_data_duration; + + report_timing("Other", total_duration.saturating_sub(sum)); + report_timing("Total", total_duration); + } +} + +fn surgery_elf_help( + verbose: bool, + md: &Metadata, + exec_mmap: &mut MmapMut, + offset_ref: &mut usize, // TODO return this instead of taking a mutable reference to it + app_obj: object::File, +) { + let elf64 = exec_mmap[4] == 2; + let litte_endian = exec_mmap[5] == 1; + if !elf64 || !litte_endian { + internal_error!("Only 64bit little endian elf currently supported for surgery"); + } + let exec_header = load_struct_inplace::>(exec_mmap, 0); + + let ph_offset = exec_header.e_phoff.get(LE); + let ph_ent_size = exec_header.e_phentsize.get(LE); + let ph_num = exec_header.e_phnum.get(LE); + let sh_offset = exec_header.e_shoff.get(LE); + let sh_ent_size = exec_header.e_shentsize.get(LE); + let sh_num = exec_header.e_shnum.get(LE); + + if verbose { + println!(); + println!("Is Elf64: {elf64}"); + println!("Is Little Endian: {litte_endian}"); + println!("PH Offset: {ph_offset:+x}"); + println!("PH Entry Size: {ph_ent_size}"); + println!("PH Entry Count: {ph_num}"); + println!("SH Offset: {sh_offset:+x}"); + println!("SH Entry Size: {sh_ent_size}"); + println!("SH Entry Count: {sh_num}"); + } + + // Backup section header table. + let sh_size = sh_ent_size as usize * sh_num as usize; + let sh_tab = exec_mmap[sh_offset as usize..][..sh_size].to_vec(); + + let mut offset = sh_offset as usize; + offset = align_by_constraint(offset, MIN_SECTION_ALIGNMENT); + + // Align physical and virtual address of new segment. + let mut virt_offset = align_to_offset_by_constraint( + md.last_vaddr as usize, + offset, + md.load_align_constraint as usize, + ); + + // First decide on sections locations and then recode every exact symbol locations. + + // TODO: In the future Roc may use a data section to store memoized toplevel thunks + // in development builds for caching the results of top-level constants + let rodata_sections: Vec
= app_obj + .sections() + .filter(|sec| sec.name().unwrap_or_default().starts_with(".rodata")) + .collect(); + + // bss section is like rodata section, but it has zero file size and non-zero virtual size. + let bss_sections: Vec
= app_obj + .sections() + .filter(|sec| sec.name().unwrap_or_default().starts_with(".bss")) + .collect(); + + let text_sections: Vec
= app_obj + .sections() + .filter(|sec| sec.name().unwrap_or_default().starts_with(".text")) + .collect(); + if text_sections.is_empty() { + internal_error!("No text sections found. This application has no code."); + } + + // Copy sections and resolve their symbols/relocations. + let symbols = app_obj.symbols().collect::>(); + let mut section_offset_map: MutMap = MutMap::default(); + let mut symbol_vaddr_map: MutMap = MutMap::default(); + let mut app_func_vaddr_map: MutMap = MutMap::default(); + let mut app_func_size_map: MutMap = MutMap::default(); + + // Calculate addresses and load symbols. + // Note, it is important the bss sections come after the rodata sections. + for sec in rodata_sections + .iter() + .chain(bss_sections.iter()) + .chain(text_sections.iter()) + { + offset = align_by_constraint(offset, MIN_SECTION_ALIGNMENT); + virt_offset = + align_to_offset_by_constraint(virt_offset, offset, md.load_align_constraint as usize); + if verbose { + println!( + "Section, {}, is being put at offset: {:+x}(virt: {:+x})", + sec.name().unwrap(), + offset, + virt_offset + ) + } + section_offset_map.insert(sec.index(), (offset, virt_offset)); + for sym in symbols.iter() { + if sym.section() == SymbolSection::Section(sec.index()) { + let name = sym.name().unwrap_or_default().to_string(); + if !md.roc_symbol_vaddresses.contains_key(&name) { + symbol_vaddr_map.insert(sym.index(), virt_offset + sym.address() as usize); + } + if md.app_functions.contains(&name) { + app_func_vaddr_map.insert(name.clone(), virt_offset + sym.address() as usize); + app_func_size_map.insert(name, sym.size()); + } + } + } + let section_size = match sec.file_range() { + Some((_, size)) => size, + None => 0, + }; + if sec.name().unwrap_or_default().starts_with(".bss") { + // bss sections only modify the virtual size. + virt_offset += sec.size() as usize; + } else if section_size != sec.size() { + internal_error!( "We do not deal with non bss sections that have different on disk and in memory sizes"); + } else { + offset += section_size as usize; + virt_offset += sec.size() as usize; + } + } + if verbose { + println!("Data Relocation Offsets: {symbol_vaddr_map:+x?}"); + println!("Found App Function Symbols: {app_func_vaddr_map:+x?}"); + } + + let (new_text_section_offset, new_text_section_vaddr) = text_sections + .iter() + .map(|sec| section_offset_map.get(&sec.index()).unwrap()) + .min() + .unwrap(); + let (new_text_section_offset, new_text_section_vaddr) = ( + *new_text_section_offset as u64, + *new_text_section_vaddr as u64, + ); + // BSS section is not guaranteed to exist. + // If it doesn't exist, just use the text section offset. + // This will make a bss section of size 0. + let bss_default = ( + new_text_section_offset as usize, + new_text_section_vaddr as usize, + ); + let (new_bss_section_offset, new_bss_section_vaddr) = bss_sections + .iter() + .map(|sec| section_offset_map.get(&sec.index()).unwrap()) + .min() + .unwrap_or(&bss_default); + let (new_bss_section_offset, new_bss_section_vaddr) = ( + *new_bss_section_offset as u64, + *new_bss_section_vaddr as u64, + ); + + // rodata section is not guaranteed to exist. + // If it doesn't exist, just use the bss section offset. + // This will make a rodata section of size 0. + let rodata_default = ( + new_bss_section_offset as usize, + new_bss_section_vaddr as usize, + ); + let (new_rodata_section_offset, new_rodata_section_vaddr) = rodata_sections + .iter() + .map(|sec| section_offset_map.get(&sec.index()).unwrap()) + .min() + .unwrap_or(&rodata_default); + let (new_rodata_section_offset, new_rodata_section_vaddr) = ( + *new_rodata_section_offset as u64, + *new_rodata_section_vaddr as u64, + ); + + // Move data and deal with relocations. + for sec in rodata_sections + .iter() + .chain(bss_sections.iter()) + .chain(text_sections.iter()) + { + let data = sec.data().unwrap_or_else(|err| { + internal_error!( + "Failed to load data for section, {:+x?}: {err}", + sec.name().unwrap(), + ) + }); + let (section_offset, section_virtual_offset) = + section_offset_map.get(&sec.index()).unwrap(); + let (section_offset, section_virtual_offset) = (*section_offset, *section_virtual_offset); + exec_mmap[section_offset..][..data.len()].copy_from_slice(data); + // Deal with definitions and relocations for this section. + if verbose { + println!(); + println!( + "Processing Relocations for Section: 0x{sec:+x?} @ {section_offset:+x} (virt: {section_virtual_offset:+x})" + ); + } + for rel in sec.relocations() { + if verbose { + println!("\tFound Relocation: {rel:+x?}"); + } + match rel.1.target() { + RelocationTarget::Symbol(index) => { + let target_offset = if let Some(target_offset) = symbol_vaddr_map.get(&index) { + if verbose { + println!("\t\tRelocation targets symbol in app at: {target_offset:+x}"); + } + Some(*target_offset as i64) + } else { + app_obj + .symbol_by_index(index) + .and_then(|sym| sym.name()) + .ok() + .and_then(|name| { + md.roc_symbol_vaddresses.get(name).map(|address| { + let vaddr = (*address + md.added_byte_count) as i64; + if verbose { + println!( + "\t\tRelocation targets symbol in host: {name} @ {vaddr:+x}" + ); + } + vaddr + }) + }) + }; + + if let Some(target_offset) = target_offset { + let virt_base = section_virtual_offset + rel.0 as usize; + let base = section_offset + rel.0 as usize; + let target: i64 = match rel.1.kind() { + RelocationKind::Relative | RelocationKind::PltRelative => { + target_offset - virt_base as i64 + rel.1.addend() + } + x => { + internal_error!("Relocation Kind not yet support: {:?}", x); + } + }; + if verbose { + println!( + "\t\tRelocation base location: {base:+x} (virt: {virt_base:+x})", + ); + println!("\t\tFinal relocation target offset: {target:+x}"); + } + match rel.1.size() { + 32 => { + let data = (target as i32).to_le_bytes(); + exec_mmap[base..][..4].copy_from_slice(&data); + } + 64 => { + let data = target.to_le_bytes(); + exec_mmap[base..][..8].copy_from_slice(&data); + } + other => { + internal_error!("Relocation size not yet supported: {other}"); + } + } + } else { + internal_error!( + "Undefined Symbol in relocation, {:+x?}: {:+x?}", + rel, + app_obj.symbol_by_index(index) + ); + } + } + + _ => { + internal_error!("Relocation target not yet support: {:+x?}", rel); + } + } + } + } + + offset = align_by_constraint(offset, MIN_SECTION_ALIGNMENT); + let new_sh_offset = offset; + exec_mmap[offset..][..sh_size].copy_from_slice(&sh_tab); + offset += sh_size; + + // Flush app only data to speed up write to disk. + exec_mmap + .flush_async_range( + new_rodata_section_offset as usize, + offset - new_rodata_section_offset as usize, + ) + .unwrap_or_else(|e| internal_error!("{}", e)); + + // TODO: look into merging symbol tables, debug info, and eh frames to enable better debugger experience. + + // Add 3 new sections and segments. + let new_section_count = 3; + offset += new_section_count * sh_ent_size as usize; + let section_headers = load_structs_inplace_mut::>( + exec_mmap, + new_sh_offset, + sh_num as usize + new_section_count, + ); + + let new_rodata_section_size = new_text_section_offset - new_rodata_section_offset; + let new_bss_section_virtual_size = new_text_section_vaddr - new_bss_section_vaddr; + let new_text_section_size = new_sh_offset as u64 - new_text_section_offset; + + // set the new rodata section header + section_headers[section_headers.len() - 3] = elf::SectionHeader64 { + sh_name: endian::U32::new(LE, 0), + sh_type: endian::U32::new(LE, elf::SHT_PROGBITS), + sh_flags: endian::U64::new(LE, elf::SHF_ALLOC as u64), + sh_addr: endian::U64::new(LE, new_rodata_section_vaddr), + sh_offset: endian::U64::new(LE, new_rodata_section_offset), + sh_size: endian::U64::new(LE, new_rodata_section_size), + sh_link: endian::U32::new(LE, 0), + sh_info: endian::U32::new(LE, 0), + sh_addralign: endian::U64::new(LE, 16), + sh_entsize: endian::U64::new(LE, 0), + }; + + // set the new bss section header + section_headers[section_headers.len() - 2] = elf::SectionHeader64 { + sh_name: endian::U32::new(LE, 0), + sh_type: endian::U32::new(LE, elf::SHT_NOBITS), + sh_flags: endian::U64::new(LE, (elf::SHF_ALLOC) as u64), + sh_addr: endian::U64::new(LE, new_bss_section_vaddr), + sh_offset: endian::U64::new(LE, new_bss_section_offset), + sh_size: endian::U64::new(LE, new_bss_section_virtual_size), + sh_link: endian::U32::new(LE, 0), + sh_info: endian::U32::new(LE, 0), + sh_addralign: endian::U64::new(LE, 16), + sh_entsize: endian::U64::new(LE, 0), + }; + + // set the new text section header + section_headers[section_headers.len() - 1] = elf::SectionHeader64 { + sh_name: endian::U32::new(LE, 0), + sh_type: endian::U32::new(LE, elf::SHT_PROGBITS), + sh_flags: endian::U64::new(LE, (elf::SHF_ALLOC | elf::SHF_EXECINSTR) as u64), + sh_addr: endian::U64::new(LE, new_text_section_vaddr), + sh_offset: endian::U64::new(LE, new_text_section_offset), + sh_size: endian::U64::new(LE, new_text_section_size), + sh_link: endian::U32::new(LE, 0), + sh_info: endian::U32::new(LE, 0), + sh_addralign: endian::U64::new(LE, 16), + sh_entsize: endian::U64::new(LE, 0), + }; + + // Reload and update file header and size. + let file_header = load_struct_inplace_mut::>(exec_mmap, 0); + file_header.e_shoff.set(LE, new_sh_offset as u64); + file_header + .e_shnum + .set(LE, sh_num + new_section_count as u16); + + // Add 2 new segments that match the new sections. + let program_headers = load_structs_inplace_mut::>( + exec_mmap, + ph_offset as usize, + ph_num as usize, + ); + + // set the new rodata section program header + program_headers[program_headers.len() - 3] = elf::ProgramHeader64 { + p_type: endian::U32::new(LE, elf::PT_LOAD), + p_flags: endian::U32::new(LE, elf::PF_R), + p_offset: endian::U64::new(LE, new_rodata_section_offset), + p_vaddr: endian::U64::new(LE, new_rodata_section_vaddr), + p_paddr: endian::U64::new(LE, new_rodata_section_vaddr), + p_filesz: endian::U64::new(LE, new_rodata_section_size), + p_memsz: endian::U64::new(LE, new_rodata_section_size), + p_align: endian::U64::new(LE, md.load_align_constraint), + }; + + // set the new bss section program header + program_headers[program_headers.len() - 2] = elf::ProgramHeader64 { + p_type: endian::U32::new(LE, elf::PT_LOAD), + p_flags: endian::U32::new(LE, elf::PF_R | elf::PF_W), + p_offset: endian::U64::new(LE, new_bss_section_offset), + p_vaddr: endian::U64::new(LE, new_bss_section_vaddr), + p_paddr: endian::U64::new(LE, new_bss_section_vaddr), + p_filesz: endian::U64::new(LE, 0), + p_memsz: endian::U64::new(LE, new_bss_section_virtual_size), + p_align: endian::U64::new(LE, md.load_align_constraint), + }; + + // set the new text section program header + let new_text_section_index = program_headers.len() - 1; + program_headers[new_text_section_index] = elf::ProgramHeader64 { + p_type: endian::U32::new(LE, elf::PT_LOAD), + p_flags: endian::U32::new(LE, elf::PF_R | elf::PF_X), + p_offset: endian::U64::new(LE, new_text_section_offset), + p_vaddr: endian::U64::new(LE, new_text_section_vaddr), + p_paddr: endian::U64::new(LE, new_text_section_vaddr), + p_filesz: endian::U64::new(LE, new_text_section_size), + p_memsz: endian::U64::new(LE, new_text_section_size), + p_align: endian::U64::new(LE, md.load_align_constraint), + }; + + // Update calls from platform and dynamic symbols. + let dynsym_offset = md.dynamic_symbol_table_section_offset + md.added_byte_count; + let symtab_offset = md.symbol_table_section_offset + md.added_byte_count; + + for func_name in md.app_functions.iter() { + let func_virt_offset = match app_func_vaddr_map.get(func_name) { + Some(offset) => *offset as u64, + None => { + eprintln!("Error:"); + eprintln!("\n\tFunction, {}, was not defined by the app.", &func_name); + eprintln!("\nPotential causes:"); + eprintln!("\n\t- because the platform was built with a non-compatible version of roc compared to the one you are running."); + eprintln!("\n\t\tsolutions:"); + eprintln!("\t\t\t+ Downgrade your roc version to the one that was used to build the platform."); + eprintln!("\t\t\t+ Or ask the platform author to release a new version of the platform using a current roc release."); + eprintln!("\n\t- This can also occur due to a bug in the compiler. In that case, file an issue here: https://github.com/roc-lang/roc/issues/new/choose"); + + std::process::exit(1); + } + }; + if verbose { + println!( + "Updating calls to {} to the address: {:+x}", + &func_name, func_virt_offset + ); + } + + for s in md.surgeries.get(func_name).unwrap_or(&vec![]) { + if verbose { + println!("\tPerforming surgery: {s:+x?}"); + } + let surgery_virt_offset = match s.virtual_offset { + VirtualOffset::Relative(vs) => (vs + md.added_byte_count) as i64, + VirtualOffset::Absolute => 0, + }; + match s.size { + 4 => { + let target = (func_virt_offset as i64 - surgery_virt_offset) as i32; + if verbose { + println!("\tTarget Jump: {target:+x}"); + } + let data = target.to_le_bytes(); + exec_mmap[(s.file_offset + md.added_byte_count) as usize..][..4] + .copy_from_slice(&data); + } + 8 => { + let target = func_virt_offset as i64 - surgery_virt_offset; + if verbose { + println!("\tTarget Jump: {target:+x}"); + } + let data = target.to_le_bytes(); + exec_mmap[(s.file_offset + md.added_byte_count) as usize..][..8] + .copy_from_slice(&data); + } + x => { + internal_error!("Surgery size not yet supported: {}", x); + } + } + } + + // Replace plt call code with just a jump. + // This is a backup incase we missed a call to the plt. + if let Some((plt_off, plt_vaddr)) = md.plt_addresses.get(func_name) { + let plt_off = (*plt_off + md.added_byte_count) as usize; + let plt_vaddr = *plt_vaddr + md.added_byte_count; + let jmp_inst_len = 5; + let target = + (func_virt_offset as i64 - (plt_vaddr as i64 + jmp_inst_len as i64)) as i32; + if verbose { + println!("\tPLT: {plt_off:+x}, {plt_vaddr:+x}"); + println!("\tTarget Jump: {target:+x}"); + } + let data = target.to_le_bytes(); + exec_mmap[plt_off] = 0xE9; + exec_mmap[plt_off + 1..plt_off + jmp_inst_len].copy_from_slice(&data); + for i in jmp_inst_len..PLT_ADDRESS_OFFSET as usize { + exec_mmap[plt_off + i] = 0x90; + } + } + + if let Some(i) = md.dynamic_symbol_indices.get(func_name) { + let sym = load_struct_inplace_mut::>( + exec_mmap, + dynsym_offset as usize + *i as usize * mem::size_of::>(), + ); + sym.st_shndx.set(LE, new_text_section_index as u16); + sym.st_value.set(LE, func_virt_offset); + sym.st_size.set( + LE, + match app_func_size_map.get(func_name) { + Some(size) => *size, + None => internal_error!("Size missing for: {func_name}"), + }, + ); + } + + // Also update symbols in the regular symbol table as well. + if let Some(i) = md.static_symbol_indices.get(func_name) { + let sym = load_struct_inplace_mut::>( + exec_mmap, + symtab_offset as usize + *i as usize * mem::size_of::>(), + ); + sym.st_shndx.set(LE, new_text_section_index as u16); + sym.st_value.set(LE, func_virt_offset); + sym.st_size.set( + LE, + match app_func_size_map.get(func_name) { + Some(size) => *size, + None => internal_error!("Size missing for: {func_name}"), + }, + ); + } + } + + // TODO return this instead of accepting a mutable ref! + *offset_ref = offset; +} + +#[cfg(test)] +mod tests { + use super::*; + + use crate::preprocessed_host_filename; + use indoc::indoc; + use target_lexicon::Triple; + + const ELF64_DYNHOST: &[u8] = include_bytes!("../dynhost_benchmarks_elf64") as &[_]; + + #[test] + fn collect_definitions() { + let object = object::File::parse(ELF64_DYNHOST).unwrap(); + + let symbols = collect_roc_definitions(&object); + + let mut keys = symbols.keys().collect::>(); + keys.sort_unstable(); + + assert_eq!( + [ + "memset", + "roc_alloc", + "roc_dealloc", + "roc_fx_getInt", + "roc_fx_getInt_help", + "roc_fx_putInt", + "roc_fx_putLine", + "roc_memcpy", + "roc_memset", + "roc_panic", + "roc_realloc" + ], + keys.as_slice(), + ) + } + + #[test] + fn collect_undefined_symbols_elf() { + let object = object::File::parse(ELF64_DYNHOST).unwrap(); + + let mut triple = Triple::host(); + triple.binary_format = target_lexicon::BinaryFormat::Elf; + + let mut keys: Vec<_> = object + .dynamic_symbols() + .filter(is_roc_undefined) + .filter_map(|s| s.name().ok()) + .collect(); + keys.sort_unstable(); + + assert_eq!( + [ + "roc__mainForHost_1__Fx_caller", + "roc__mainForHost_1__Fx_result_size", + "roc__mainForHost_1_exposed_generic", + "roc__mainForHost_size" + ], + keys.as_slice() + ) + } + + #[allow(dead_code)] + fn zig_host_app_help(dir: &Path, target: &Triple) { + let host_zig = indoc!( + r#" + const std = @import("std"); + + extern fn roc_magic1(usize) callconv(.C) [*]const u8; + + pub fn main() !void { + const stdout = std.io.getStdOut().writer(); + try stdout.print("Hello {s}\n", .{roc_magic1(0)[0..3]}); + } + "# + ); + + let app_zig = indoc!( + r#" + const X = [_][]const u8 { "foo" }; + + export fn roc_magic1(index: usize) [*]const u8 { + return X[index].ptr; + } + "# + ); + + let zig = std::env::var("ROC_ZIG").unwrap_or_else(|_| "zig".into()); + + std::fs::write(dir.join("host.zig"), host_zig.as_bytes()).unwrap(); + std::fs::write(dir.join("app.zig"), app_zig.as_bytes()).unwrap(); + + // we need to compile the app first + let output = std::process::Command::new(&zig) + .current_dir(dir) + .args(["build-obj", "app.zig", "-fPIC", "-OReleaseFast"]) + .output() + .unwrap(); + + if !output.status.success() { + use std::io::Write; + + std::io::stdout().write_all(&output.stdout).unwrap(); + std::io::stderr().write_all(&output.stderr).unwrap(); + + panic!("zig build-obj failed"); + } + + // open our app object; we'll copy sections from it later + let file = std::fs::File::open(dir.join("app.o")).unwrap(); + let roc_app = unsafe { memmap2::Mmap::map(&file) }.unwrap(); + + let names: Vec = { + let object = object::File::parse(&*roc_app).unwrap(); + + object + .symbols() + .filter(|s| !s.is_local()) + .map(|e| e.name().unwrap().to_string()) + .collect() + }; + + let dylib_bytes = crate::generate_dylib::create_dylib_elf64(&names).unwrap(); + std::fs::write(dir.join("libapp.so"), dylib_bytes).unwrap(); + + // now we can compile the host (it uses libapp.so, hence the order here) + let output = std::process::Command::new(&zig) + .current_dir(dir) + .args([ + "build-exe", + "libapp.so", + "host.zig", + "-fPIE", + "-lc", + "-OReleaseFast", + ]) + .output() + .unwrap(); + + if !output.status.success() { + use std::io::Write; + + std::io::stdout().write_all(&output.stdout).unwrap(); + std::io::stderr().write_all(&output.stderr).unwrap(); + + panic!("zig build-exe failed"); + } + + let preprocessed_host_filename = dir.join(preprocessed_host_filename(target).unwrap()); + + preprocess_elf( + target_lexicon::Endianness::Little, + &dir.join("host"), + &dir.join("metadata"), + &preprocessed_host_filename, + &dir.join("libapp.so"), + false, + false, + ); + + std::fs::copy(&preprocessed_host_filename, dir.join("final")).unwrap(); + + surgery_elf( + &roc_app, + &dir.join("metadata"), + &dir.join("final"), + false, + false, + ); + } + + #[cfg(target_os = "linux")] + #[test] + fn zig_host_app() { + use std::str::FromStr; + + let dir = tempfile::tempdir().unwrap(); + let dir = dir.path(); + + zig_host_app_help(dir, &Triple::from_str("x86_64-unknown-linux-musl").unwrap()); + + let output = std::process::Command::new(dir.join("final")) + .current_dir(dir) + .output() + .unwrap(); + + if !output.status.success() { + use std::io::Write; + + std::io::stdout().write_all(&output.stdout).unwrap(); + std::io::stderr().write_all(&output.stderr).unwrap(); + + panic!("app.exe failed"); + } + + let output = String::from_utf8_lossy(&output.stdout); + + assert_eq!("Hello foo\n", output); + } +} diff --git a/crates/linker/src/generate_dylib/elf64.rs b/crates/linker/src/generate_dylib/elf64.rs new file mode 100644 index 0000000000..1c6913b4aa --- /dev/null +++ b/crates/linker/src/generate_dylib/elf64.rs @@ -0,0 +1,211 @@ +use object::{elf, Endianness}; + +use crate::pe::next_multiple_of; + +pub fn create_dylib_elf64(custom_names: &[String]) -> object::read::Result> { + let endian = Endianness::Little; + + let mut out_data = Vec::new(); + let mut writer = object::write::elf::Writer::new(endian, true, &mut out_data); + + const DYNAMIC_SECTION_INDEX: usize = 2; + + let out_sections_index = [ + writer.reserve_dynsym_section_index(), + writer.reserve_dynstr_section_index(), + writer.reserve_dynamic_section_index(), + writer.reserve_strtab_section_index(), + writer.reserve_shstrtab_section_index(), + ]; + + // we need this later, but must allocate it here + let soname = writer.add_dynamic_string(b"libapp.so"); + + // Assign dynamic symbol indices. + let out_dynsyms: Vec<_> = custom_names + .iter() + .map(|name| { + ( + writer.reserve_dynamic_symbol_index(), + writer.add_dynamic_string(name.as_bytes()), + ) + }) + .collect(); + + writer.reserve_file_header(); + + const PLACEHOLDER: u64 = 0xAAAAAAAAAAAAAAAA; + let mut program_headers = [ + object::write::elf::ProgramHeader { + p_type: object::elf::PT_PHDR, + p_flags: object::elf::PF_R, + p_offset: 0x40, + p_vaddr: 0x40, + p_paddr: 0x40, + p_filesz: 0x188, + p_memsz: 0x188, + p_align: 1 << 3, + }, + object::write::elf::ProgramHeader { + p_type: object::elf::PT_LOAD, + p_flags: object::elf::PF_R | object::elf::PF_W, + p_offset: PLACEHOLDER, + p_vaddr: PLACEHOLDER, + p_paddr: PLACEHOLDER, + p_filesz: PLACEHOLDER, + p_memsz: PLACEHOLDER, + p_align: 1 << 12, + }, + object::write::elf::ProgramHeader { + p_type: object::elf::PT_DYNAMIC, + p_flags: object::elf::PF_R | object::elf::PF_W, + p_offset: PLACEHOLDER, + p_vaddr: PLACEHOLDER, + p_paddr: PLACEHOLDER, + p_filesz: PLACEHOLDER, + p_memsz: PLACEHOLDER, + p_align: 1 << 3, + }, + object::write::elf::ProgramHeader { + p_type: object::elf::PT_GNU_RELRO, + p_flags: object::elf::PF_R, + p_offset: PLACEHOLDER, + p_vaddr: PLACEHOLDER, + p_paddr: PLACEHOLDER, + p_filesz: PLACEHOLDER, + p_memsz: PLACEHOLDER, + p_align: 1 << 1, + }, + object::write::elf::ProgramHeader { + p_type: object::elf::PT_GNU_STACK, + p_flags: object::elf::PF_R, + p_offset: 0, + p_vaddr: 0, + p_paddr: 0, + p_filesz: 0, + p_memsz: 0, + p_align: 0, + }, + ]; + + writer.reserve_program_headers(program_headers.len() as u32); + + let dynsym_address = writer.reserved_len(); + writer.reserve_dynsym(); + + let dynstr_address = writer.reserved_len(); + writer.reserve_dynstr(); + + let out_dynamic = [ + (elf::DT_SONAME, 1, Some(soname)), + (elf::DT_SYMTAB, dynsym_address as u64, None), + (elf::DT_STRTAB, dynstr_address as u64, None), + (elf::DT_NULL, 0, None), + ]; + + let dyn_size = std::mem::size_of::>() * out_dynamic.len(); + + // aligned to the next multiple of 8 + let dynamic_address = next_multiple_of(writer.reserved_len(), 8); + writer.reserve_dynamic(out_dynamic.len()); + + writer.reserve_strtab(); + writer.reserve_shstrtab(); + + writer.reserve_section_headers(); + + // just enough program header info to satisfy the dynamic loader + for program_header in program_headers.iter_mut() { + match program_header.p_type { + elf::PT_LOAD | elf::PT_DYNAMIC | elf::PT_GNU_RELRO => { + program_header.p_offset = dynamic_address as u64; + program_header.p_vaddr = dynamic_address as u64 + 0x1000; + program_header.p_paddr = dynamic_address as u64 + 0x1000; + + // this puts the dynamic section inside of the segments, so + // + // Section to Segment mapping: + // Segment Sections... + // 00 + // 01 .dynamic + // 02 .dynamic + // 03 .dynamic + // 04 + program_header.p_filesz = dyn_size as u64; + program_header.p_memsz = dyn_size as u64; + } + _ => {} + } + } + + // WRITING SECTION CONTENT + + writer + .write_file_header(&object::write::elf::FileHeader { + os_abi: 0, + abi_version: 0, + e_type: 3, + e_machine: 62, + e_entry: 0x1000, + e_flags: 0, + }) + .unwrap(); + + { + writer.write_align_program_headers(); + + for header in program_headers { + writer.write_program_header(&header); + } + } + + // dynsym content + { + writer.write_null_dynamic_symbol(); + for (_index, name) in out_dynsyms { + writer.write_dynamic_symbol(&object::write::elf::Sym { + name: Some(name), + section: Some(out_sections_index[DYNAMIC_SECTION_INDEX]), + st_info: (elf::STB_GLOBAL << 4) | elf::STT_FUNC, + st_other: 0, + st_shndx: 0, + st_value: 0x1000, + st_size: 0, + }); + } + } + + // dynstr content + writer.write_dynstr(); + + // dynamic content + writer.write_align_dynamic(); + for (tag, val, opt_string) in out_dynamic { + if let Some(string) = opt_string { + writer.write_dynamic_string(tag, string); + } else { + writer.write_dynamic(tag, val); + } + } + + // strtab content + writer.write_strtab(); + writer.write_shstrtab(); + + // SECTION HEADERS + + writer.write_null_section_header(); + + writer.write_dynsym_section_header(dynsym_address as _, 1); + + writer.write_dynstr_section_header(dynstr_address as _); + + writer.write_dynamic_section_header(dynamic_address as u64 + 0x1000); + + writer.write_strtab_section_header(); + writer.write_shstrtab_section_header(); + + debug_assert_eq!(writer.reserved_len(), writer.len()); + + Ok(out_data) +} diff --git a/crates/linker/src/generate_dylib/macho.rs b/crates/linker/src/generate_dylib/macho.rs new file mode 100644 index 0000000000..b772e0818c --- /dev/null +++ b/crates/linker/src/generate_dylib/macho.rs @@ -0,0 +1,100 @@ +use object::write; +use object::{Architecture, BinaryFormat, Endianness, SymbolFlags, SymbolKind, SymbolScope}; +use roc_error_macros::internal_error; +use std::path::Path; +use std::process::Command; +use target_lexicon::Triple; + +// TODO: Eventually do this from scratch and in memory instead of with ld. +pub fn create_dylib_macho( + custom_names: &[String], + triple: &Triple, +) -> object::read::Result> { + let dummy_obj_file = tempfile::Builder::new() + .prefix("roc_lib") + .suffix(".o") + .tempfile() + .unwrap_or_else(|e| internal_error!("{}", e)); + let tmp = tempfile::tempdir().unwrap_or_else(|e| internal_error!("{}", e)); + let dummy_lib_file = tmp.path().to_path_buf().with_file_name("libapp.so"); + + let obj_target = BinaryFormat::MachO; + let obj_arch = match triple.architecture { + target_lexicon::Architecture::X86_64 => Architecture::X86_64, + target_lexicon::Architecture::Aarch64(_) => Architecture::Aarch64, + _ => { + // We should have verified this via supported() before calling this function + unreachable!() + } + }; + let mut out_object = write::Object::new(obj_target, obj_arch, Endianness::Little); + + let text_section = out_object.section_id(write::StandardSection::Text); + + for name in custom_names { + out_object.add_symbol(write::Symbol { + name: name.as_bytes().to_vec(), + value: 0, + size: 0, + kind: SymbolKind::Text, + scope: SymbolScope::Dynamic, + weak: false, + section: write::SymbolSection::Section(text_section), + flags: SymbolFlags::None, + }); + } + + std::fs::write( + dummy_obj_file.path(), + out_object.write().expect("failed to build output object"), + ) + .expect("failed to write object to file"); + + // This path only exists on macOS Big Sur, and it causes ld errors + // on Catalina if it's specified with -L, so we replace it with a + // redundant -lSystem if the directory isn't there. + let big_sur_path = "/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/lib"; + let big_sur_fix = if Path::new(big_sur_path).exists() { + "-L/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/lib" + } else { + "-lSystem" // We say -lSystem twice in the case of non-Big-Sur OSes, but it's fine. + }; + + let ld_flag_soname = "-install_name"; + let ld_prefix_args = [big_sur_fix, "-lSystem", "-dylib"]; + + let output = Command::new("ld") + .args(ld_prefix_args) + .args([ + ld_flag_soname, + dummy_lib_file.file_name().unwrap().to_str().unwrap(), + dummy_obj_file.path().to_str().unwrap(), + "-o", + dummy_lib_file.to_str().unwrap(), + // Suppress warnings, because otherwise it prints: + // + // ld: warning: -undefined dynamic_lookup may not work with chained fixups + // + // We can't disable that option without breaking either x64 mac or ARM mac + "-w", + ]) + .output() + .unwrap(); + + // Extend the lifetime of the tempfile so it doesn't get dropped + // (and thus deleted) before the linker process is done using it! + let _ = dummy_obj_file; + + if !output.status.success() { + match std::str::from_utf8(&output.stderr) { + Ok(stderr) => panic!( + "Failed to link dummy shared library - stderr of the `ld` command was:\n{stderr}" + ), + Err(utf8_err) => panic!( + "Failed to link dummy shared library - stderr of the `ld` command was invalid utf8 ({utf8_err:?})" + ), + } + } + + Ok(std::fs::read(dummy_lib_file).expect("Failed to load dummy library")) +} diff --git a/crates/linker/src/generate_dylib/mod.rs b/crates/linker/src/generate_dylib/mod.rs new file mode 100644 index 0000000000..7374f28b0a --- /dev/null +++ b/crates/linker/src/generate_dylib/mod.rs @@ -0,0 +1,111 @@ +use target_lexicon::Triple; + +mod elf64; +mod macho; +mod pe; + +#[cfg(test)] +pub(crate) use pe::synthetic_dll; + +#[cfg(test)] +pub(crate) use elf64::create_dylib_elf64; + +pub(crate) use pe::APP_DLL; + +pub fn generate(target: &Triple, custom_names: &[String]) -> object::read::Result> { + match target.binary_format { + target_lexicon::BinaryFormat::Elf => elf64::create_dylib_elf64(custom_names), + target_lexicon::BinaryFormat::Macho => macho::create_dylib_macho(custom_names, target), + target_lexicon::BinaryFormat::Coff => Ok(pe::synthetic_dll(custom_names)), + other => unimplemented!("dylib creation for {:?}", other), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + use object::Object; + + fn check_exports(target: &Triple) { + let custom_names = ["foo".to_string(), "bar".to_string()]; + + let bytes = generate(target, &custom_names).unwrap(); + let object = object::File::parse(bytes.as_slice()).unwrap(); + + let exports = object.exports().unwrap(); + for custom in custom_names { + assert!( + exports.iter().any(|e| e.name() == custom.as_bytes()), + "missing {}", + &custom + ); + } + } + + #[test] + fn check_exports_elf64() { + let target = target_lexicon::Triple { + architecture: target_lexicon::Architecture::X86_64, + operating_system: target_lexicon::OperatingSystem::Linux, + binary_format: target_lexicon::BinaryFormat::Elf, + ..target_lexicon::Triple::host() + }; + + check_exports(&target); + } + + #[test] + fn check_exports_coff() { + // NOTE: this does not work + // + // let target = target_lexicon::Triple { + // architecture: target_lexicon::Architecture::X86_64, + // operating_system: target_lexicon::OperatingSystem::Windows, + // binary_format: target_lexicon::BinaryFormat::Coff, + // ..target_lexicon::Triple::host() + // }; + // + // check_exports(&target); + // + // Because our exports point back into the .edata section, they are considered "forward" + // exports: they aren't actually defined in this .dll and hence not considered exports by + // the `object` crate. + } + + #[test] + fn check_exports_coff_manual() { + let target = target_lexicon::Triple { + architecture: target_lexicon::Architecture::X86_64, + operating_system: target_lexicon::OperatingSystem::Windows, + binary_format: target_lexicon::BinaryFormat::Coff, + ..target_lexicon::Triple::host() + }; + + let custom_names = ["foo".to_string(), "bar".to_string()]; + + let bytes = generate(&target, &custom_names).unwrap(); + let object = object::read::pe::PeFile64::parse(bytes.as_slice()).unwrap(); + + let exports = { + let mut exports = Vec::new(); + if let Some(export_table) = object.export_table().unwrap() { + for (name_pointer, _address_index) in export_table.name_iter() { + let name = export_table.name_from_pointer(name_pointer).unwrap(); + + // here the standard `.exports()` checks for `if !export_table.is_forward(address)` + exports.push(name) + } + } + exports + }; + + for custom in custom_names { + assert!( + exports.iter().any(|name| *name == custom.as_bytes()), + "missing {}", + &custom + ); + } + } +} diff --git a/crates/linker/src/generate_dylib/pe.rs b/crates/linker/src/generate_dylib/pe.rs new file mode 100644 index 0000000000..791d584388 --- /dev/null +++ b/crates/linker/src/generate_dylib/pe.rs @@ -0,0 +1,203 @@ +use object::pe; +use object::LittleEndian as LE; + +pub(crate) const APP_DLL: &str = "libapp.dll"; + +fn synthetic_image_export_directory( + name: &str, + virtual_address: u32, + custom_names: &[String], +) -> pe::ImageExportDirectory { + use object::{U16, U32}; + + let directory_size = std::mem::size_of::() as u32; + + // actual data is after the directory and the name of the dll file (null-terminated) + let start = virtual_address + directory_size + name.len() as u32 + 1; + + let address_of_functions = start; + + // by convention, names start at index 1, so we add one "padding" name + let number_of_functions = custom_names.len() as u32 + 1; + + let address_of_names = address_of_functions + 4 * number_of_functions; + let address_of_name_ordinals = address_of_names + 4 * custom_names.len() as u32; + + pe::ImageExportDirectory { + characteristics: U32::new(LE, 0), + time_date_stamp: U32::new(LE, 0), + major_version: U16::new(LE, 0), + minor_version: U16::new(LE, 0), + name: U32::new(LE, virtual_address + directory_size), + base: U32::new(LE, 0), + number_of_functions: U32::new(LE, number_of_functions), + number_of_names: U32::new(LE, custom_names.len() as u32), + address_of_functions: U32::new(LE, address_of_functions), + address_of_names: U32::new(LE, address_of_names), + address_of_name_ordinals: U32::new(LE, address_of_name_ordinals), + } +} + +fn synthetic_export_dir(virtual_address: u32, custom_names: &[String]) -> Vec { + let mut vec = vec![0; std::mem::size_of::()]; + + let ptr = vec.as_mut_ptr(); + + let name = APP_DLL; + let directory = synthetic_image_export_directory(name, virtual_address, custom_names); + + unsafe { + std::ptr::write_unaligned(ptr as *mut pe::ImageExportDirectory, directory); + } + + // write the .dll name, null-terminated + vec.extend(name.as_bytes()); + vec.push(0); + + let n = custom_names.len(); + + debug_assert_eq!( + directory.address_of_functions.get(LE) - virtual_address, + vec.len() as u32 + ); + + // Unsure what this one does; it does not seem important for our purposes + // + // Export Address Table -- Ordinal Base 0 + // [ 1] +base[ 1] 1020 Export RVA + // [ 2] +base[ 2] d030 Export RVA + + // pad with 4 bytes to have the first actual index start at 1. + // Having this table be 1-indexed seems to be convention + vec.extend(0u32.to_le_bytes()); + + // here we pre-calculate the virtual address where the name bytes will be stored + // The address hence points to a place in the .edata section. Such exports are + // called "forwarders". Because there is no actual definition, these exports don't + // show up when calling the `object` crate's .exports() method. + // + // It may turn out that we should actually not make our symbols forwarders. We can do this by + // adding a .text section with some NULL bytes before the .rdata section, and having the + // symbols point into this meaningless .text section. + let mut next_name_start = + directory.address_of_name_ordinals.get(LE) + custom_names.len() as u32 * 2; + + for name in custom_names.iter() { + vec.extend(next_name_start.to_le_bytes()); + next_name_start += name.len() as u32 + 1; // null-terminated + } + + debug_assert_eq!( + directory.address_of_names.get(LE) - virtual_address, + vec.len() as u32 + ); + + // Maps the index to a name + // + // [Ordinal/Name Pointer] Table + // [ 1] _CRT_INIT + // [ 2] _CRT_MT + let mut acc = directory.address_of_name_ordinals.get(LE) + n as u32 * 2; + for name in custom_names { + vec.extend(acc.to_le_bytes()); + acc += name.len() as u32 + 1; + } + + debug_assert_eq!( + directory.address_of_name_ordinals.get(LE) - virtual_address, + vec.len() as u32 + ); + + // the ordinals, which map to the index plus 1 to the ordinals start at 1 + for i in 0..custom_names.len() { + vec.extend((i as u16 + 1).to_le_bytes()); + } + + // write out the names of the symbols, as null-terminated strings + for name in custom_names { + vec.extend(name.as_bytes()); + vec.push(0); + } + + vec +} + +pub fn synthetic_dll(custom_names: &[String]) -> Vec { + let mut out_data = Vec::new(); + let mut writer = object::write::pe::Writer::new(true, 8, 8, &mut out_data); + + // Reserve file ranges and virtual addresses. + writer.reserve_dos_header_and_stub(); + + // we will have one header: the export directory + writer.reserve_nt_headers(1); + + writer.reserve_section_headers(1); + + let virtual_address = writer.reserved_len(); + + let exports = synthetic_export_dir(virtual_address, custom_names); + + writer.set_data_directory( + pe::IMAGE_DIRECTORY_ENTRY_EXPORT, + virtual_address, + exports.len() as _, + ); + + // it's fine if this changes, this is just here to catch any accidental changes + debug_assert_eq!(virtual_address, 0x138); + + // we store the export directory in a .rdata section + let rdata_section: (_, Vec) = { + let characteristics = object::pe::IMAGE_SCN_MEM_READ | pe::IMAGE_SCN_CNT_INITIALIZED_DATA; + let range = writer.reserve_section( + *b".rdata\0\0", + characteristics, + // virtual size + exports.len() as u32, + // size_of_raw_data + exports.len() as u32, + ); + + debug_assert_eq!(virtual_address, range.virtual_address); + + (range.file_offset, exports) + }; + + // Start writing. + writer.write_dos_header_and_stub().unwrap(); + + // the header on my machine + let headers = object::write::pe::NtHeaders { + machine: 34404, + time_date_stamp: 0, + characteristics: 8226, + major_linker_version: 14, + minor_linker_version: 0, + address_of_entry_point: 4560, + image_base: 6442450944, + major_operating_system_version: 6, + minor_operating_system_version: 0, + major_image_version: 0, + minor_image_version: 0, + major_subsystem_version: 6, + minor_subsystem_version: 0, + subsystem: 2, + dll_characteristics: 352, + size_of_stack_reserve: 1048576, + size_of_stack_commit: 4096, + size_of_heap_reserve: 1048576, + size_of_heap_commit: 4096, + }; + + writer.write_nt_headers(headers); + + writer.write_section_headers(); + + let (offset, data) = rdata_section; + writer.write_section(offset, &data); + + debug_assert_eq!(writer.reserved_len() as usize, writer.len()); + + out_data +} diff --git a/crates/linker/src/lib.rs b/crates/linker/src/lib.rs new file mode 100644 index 0000000000..1f1408bd0b --- /dev/null +++ b/crates/linker/src/lib.rs @@ -0,0 +1,673 @@ +//! Surgical linker that links platforms to Roc applications. We created our own +//! linker for performance, since regular linkers add complexity that is not +//! needed for linking Roc apps. Because we want `roc` to manage the build +//! system and final linking of the executable, it is significantly less +//! practical to use a regular linker. +use memmap2::{Mmap, MmapMut}; +use object::Object; +use roc_error_macros::internal_error; +use roc_load::{EntryPoint, ExecutionMode, ExposedToHost, LoadConfig, Threading}; +use roc_module::symbol::Interns; +use roc_packaging::cache::RocCacheDir; +use roc_reporting::report::{RenderTarget, DEFAULT_PALETTE}; +use roc_solve::FunctionKind; +use roc_target::get_target_triple_str; +use std::cmp::Ordering; +use std::mem; +use std::path::{Path, PathBuf}; +use target_lexicon::Triple; + +mod elf; +mod macho; +mod pe; + +mod generate_dylib; + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum LinkType { + // These numbers correspond to the --lib and --no-link flags + Executable = 0, + Dylib = 1, + None = 2, +} + +pub fn supported(link_type: LinkType, target: &Triple) -> bool { + if let LinkType::Executable = link_type { + match target { + Triple { + architecture: target_lexicon::Architecture::X86_64, + operating_system: target_lexicon::OperatingSystem::Linux, + binary_format: target_lexicon::BinaryFormat::Elf, + .. + } => true, + + // macho support is incomplete + Triple { + operating_system: target_lexicon::OperatingSystem::Darwin, + binary_format: target_lexicon::BinaryFormat::Macho, + .. + } => false, + + Triple { + architecture: target_lexicon::Architecture::X86_64, + operating_system: target_lexicon::OperatingSystem::Windows, + binary_format: target_lexicon::BinaryFormat::Coff, + .. + } => true, + + _ => false, + } + } else { + false + } +} + +pub const PRECOMPILED_HOST_EXT: &str = "rh"; // Short for "roc host" + +pub fn preprocessed_host_filename(target: &Triple) -> Option { + roc_target::get_target_triple_str(target).map(|x| format!("{x}.{PRECOMPILED_HOST_EXT}")) +} + +fn metadata_file_name(target: &Triple) -> String { + let target_triple_str = get_target_triple_str(target); + + format!("metadata_{}.rm", target_triple_str.unwrap_or("unknown")) +} + +pub fn link_preprocessed_host( + target: &Triple, + platform_path: &Path, + roc_app_bytes: &[u8], + binary_path: &Path, +) { + let metadata = platform_path.with_file_name(metadata_file_name(target)); + surgery(roc_app_bytes, &metadata, binary_path, false, false, target) +} + +// Exposed function to load a platform file and generate a stub lib for it. +pub fn generate_stub_lib( + input_path: &Path, + roc_cache_dir: RocCacheDir<'_>, + triple: &Triple, + function_kind: FunctionKind, +) -> (PathBuf, PathBuf, Vec) { + // Note: this should theoretically just be able to load the host, I think. + // Instead, I am loading an entire app because that was simpler and had example code. + // If this was expected to stay around for the the long term, we should change it. + // But hopefully it will be removable once we have surgical linking on all platforms. + let target_info = triple.into(); + let arena = &bumpalo::Bump::new(); + let loaded = roc_load::load_and_monomorphize( + arena, + input_path.to_path_buf(), + roc_cache_dir, + LoadConfig { + target_info, + function_kind, + render: RenderTarget::Generic, + palette: DEFAULT_PALETTE, + threading: Threading::AllAvailable, + exec_mode: ExecutionMode::Executable, + }, + ) + .unwrap_or_else(|problem| todo!("{:?}", problem)); + + let exposed_to_host = loaded + .exposed_to_host + .top_level_values + .keys() + .map(|x| x.as_str(&loaded.interns).to_string()) + .collect(); + + let exported_closure_types = loaded + .exposed_to_host + .closure_types + .iter() + .map(|x| { + format!( + "{}_{}", + x.module_string(&loaded.interns), + x.as_str(&loaded.interns) + ) + }) + .collect(); + + let exposed_symbols = ExposedSymbols { + top_level_values: exposed_to_host, + exported_closure_types, + }; + + if let EntryPoint::Executable { platform_path, .. } = &loaded.entry_point { + let stub_lib = if let target_lexicon::OperatingSystem::Windows = triple.operating_system { + platform_path.with_file_name("libapp.obj") + } else { + platform_path.with_file_name("libapp.so") + }; + + let stub_dll_symbols = exposed_symbols.stub_dll_symbols(); + generate_dynamic_lib(triple, &stub_dll_symbols, &stub_lib); + (platform_path.into(), stub_lib, stub_dll_symbols) + } else { + unreachable!(); + } +} + +pub fn generate_stub_lib_from_loaded( + target: &Triple, + platform_main_roc: &Path, + stub_dll_symbols: &[String], +) -> PathBuf { + let stub_lib_path = if let target_lexicon::OperatingSystem::Windows = target.operating_system { + platform_main_roc.with_file_name("libapp.dll") + } else { + platform_main_roc.with_file_name("libapp.so") + }; + + generate_dynamic_lib(target, stub_dll_symbols, &stub_lib_path); + + stub_lib_path +} + +pub struct ExposedSymbols { + // usually just `mainForhost` + pub top_level_values: Vec, + + // old type exposing mechanism + pub exported_closure_types: Vec, +} + +impl ExposedSymbols { + pub fn from_exposed_to_host(interns: &Interns, exposed_to_host: &ExposedToHost) -> Vec { + let mut custom_names = Vec::new(); + + for x in exposed_to_host.top_level_values.keys() { + let sym = x.as_str(interns); + + custom_names.extend([ + format!("roc__{sym}_1_exposed"), + format!("roc__{sym}_1_exposed_generic"), + format!("roc__{sym}_1_exposed_size"), + ]); + + let exported_closure_types = exposed_to_host + .closure_types + .iter() + .map(|x| format!("{}_{}", x.module_string(interns), x.as_str(interns))); + + for (i, _) in exported_closure_types.enumerate() { + custom_names.extend([ + format!("roc__{sym}_{i}_caller"), + format!("roc__{sym}_{i}_size"), + format!("roc__{sym}_{i}_result_size"), + ]); + } + } + + for x in &exposed_to_host.getters { + let sym = x.as_str(interns); + custom_names.extend([ + sym.to_string(), + format!("{sym}_generic"), + format!("{sym}_size"), + ]); + } + + for (top_level_value, lambda_set_id) in &exposed_to_host.lambda_sets { + let sym = top_level_value.as_str(interns); + let id = lambda_set_id.0; + custom_names.extend([format!("roc__{sym}_{id}_caller")]); + } + + // on windows (PE) binary search is used on the symbols, + // so they must be in alphabetical order + custom_names.sort_unstable(); + + custom_names + } + + pub fn stub_dll_symbols(&self) -> Vec { + let mut custom_names = Vec::new(); + + for sym in &self.top_level_values { + custom_names.extend([ + format!("roc__{sym}_1_exposed"), + format!("roc__{sym}_1_exposed_generic"), + format!("roc__{sym}_size"), + ]); + + for closure_type in &self.exported_closure_types { + custom_names.extend([ + format!("roc__{sym}_1_{closure_type}_caller"), + format!("roc__{sym}_1_{closure_type}_size"), + format!("roc__{sym}_1_{closure_type}_result_size"), + ]); + } + } + + // on windows (PE) binary search is used on the symbols, + // so they must be in alphabetical order + custom_names.sort_unstable(); + + custom_names + } +} + +fn generate_dynamic_lib(target: &Triple, stub_dll_symbols: &[String], stub_lib_path: &Path) { + if !stub_lib_is_up_to_date(target, stub_lib_path, stub_dll_symbols) { + let bytes = crate::generate_dylib::generate(target, stub_dll_symbols) + .unwrap_or_else(|e| internal_error!("{e}")); + + if let Err(e) = std::fs::write(stub_lib_path, bytes) { + internal_error!("failed to write stub lib to {:?}: {e}", stub_lib_path) + } + + if let target_lexicon::OperatingSystem::Windows = target.operating_system { + generate_import_library(stub_lib_path, stub_dll_symbols); + } + } +} + +fn generate_import_library(stub_lib_path: &Path, custom_names: &[String]) { + let def_file_content = generate_def_file(custom_names).expect("write to string never fails"); + + let mut def_path = stub_lib_path.to_owned(); + def_path.set_extension("def"); + + if let Err(e) = std::fs::write(&def_path, def_file_content.as_bytes()) { + internal_error!("failed to write import library to {:?}: {e}", def_path) + } + + let mut def_filename = PathBuf::from(generate_dylib::APP_DLL); + def_filename.set_extension("def"); + + let mut lib_filename = PathBuf::from(generate_dylib::APP_DLL); + lib_filename.set_extension("lib"); + + let zig = std::env::var("ROC_ZIG").unwrap_or_else(|_| "zig".into()); + + // use zig to generate the .lib file. Here is a good description of what is in an import library + // + // > https://www.codeproject.com/Articles/1253835/The-Structure-of-import-Library-File-lib + // + // For when we want to do this in-memory in the future. We can also consider using + // + // > https://github.com/messense/implib-rs + let output = std::process::Command::new(zig) + .current_dir(stub_lib_path.parent().unwrap()) + .args([ + "dlltool", + "-d", + def_filename.to_str().unwrap(), + "-m", + "i386:x86-64", + "-D", + generate_dylib::APP_DLL, + "-l", + lib_filename.to_str().unwrap(), + ]) + .output() + .unwrap(); + + if !output.status.success() { + use std::io::Write; + + std::io::stdout().write_all(&output.stdout).unwrap(); + std::io::stderr().write_all(&output.stderr).unwrap(); + + panic!("zig dlltool failed"); + } +} + +fn generate_def_file(custom_names: &[String]) -> Result { + use std::fmt::Write; + + let mut def_file = String::new(); + + writeln!(def_file, "LIBRARY libapp")?; + writeln!(def_file, "EXPORTS")?; + + for (i, name) in custom_names.iter().enumerate() { + // 1-indexed of course... + let index = i + 1; + + writeln!(def_file, " {name} @{index}")?; + } + + Ok(def_file) +} + +fn object_matches_target<'a>(target: &Triple, object: &object::File<'a, &'a [u8]>) -> bool { + use target_lexicon::{Architecture as TLA, OperatingSystem as TLO}; + + match target.architecture { + TLA::X86_64 => { + if object.architecture() != object::Architecture::X86_64 { + return false; + } + + let target_format = match target.operating_system { + TLO::Linux => object::BinaryFormat::Elf, + TLO::Windows => object::BinaryFormat::Pe, + _ => todo!("surgical linker does not support target {:?}", target), + }; + + object.format() == target_format + } + TLA::Aarch64(_) => object.architecture() == object::Architecture::Aarch64, + _ => todo!("surgical linker does not support target {:?}", target), + } +} + +/// Checks whether the stub `.dll/.so` is up to date, in other words that it exports exactly the +/// symbols that it is supposed to export, and is built for the right target. If this is the case, +/// we can skip rebuildingthe stub lib. +fn stub_lib_is_up_to_date(target: &Triple, stub_lib_path: &Path, custom_names: &[String]) -> bool { + if !std::path::Path::exists(stub_lib_path) { + return false; + } + + let stub_lib = open_mmap(stub_lib_path); + let object = object::File::parse(&*stub_lib).unwrap(); + + // the user may have been cross-compiling. + // The dynhost on disk must match our current target + if !object_matches_target(target, &object) { + return false; + } + + // we made this dynhost file. For the file to be the same as what we'd generate, + // we need all symbols to be there and in the correct order + let dynamic_symbols: Vec<_> = object.exports().unwrap(); + + let it1 = dynamic_symbols.iter().map(|e| e.name()); + let it2 = custom_names.iter().map(|s| s.as_bytes()); + + it1.eq(it2) +} + +pub fn preprocess_host( + target: &Triple, + platform_main_roc: &Path, + preprocessed_path: &Path, + shared_lib: &Path, + stub_dll_symbols: &[String], +) { + let metadata_path = platform_main_roc.with_file_name(metadata_file_name(target)); + let host_exe_path = if let target_lexicon::OperatingSystem::Windows = target.operating_system { + platform_main_roc.with_file_name("dynhost.exe") + } else { + platform_main_roc.with_file_name("dynhost") + }; + + preprocess( + target, + &host_exe_path, + &metadata_path, + preprocessed_path, + shared_lib, + stub_dll_symbols, + false, + false, + ) +} + +/// Constructs a `Metadata` from a host executable binary, and writes it to disk +#[allow(clippy::too_many_arguments)] +fn preprocess( + target: &Triple, + host_exe_path: &Path, + metadata_path: &Path, + preprocessed_path: &Path, + shared_lib: &Path, + stub_dll_symbols: &[String], + verbose: bool, + time: bool, +) { + if verbose { + println!("Targeting: {target}"); + } + + let endianness = target + .endianness() + .unwrap_or(target_lexicon::Endianness::Little); + + match target.binary_format { + target_lexicon::BinaryFormat::Elf => { + crate::elf::preprocess_elf( + endianness, + host_exe_path, + metadata_path, + preprocessed_path, + shared_lib, + verbose, + time, + ); + } + + target_lexicon::BinaryFormat::Macho => { + crate::macho::preprocess_macho( + target, + host_exe_path, + metadata_path, + preprocessed_path, + shared_lib, + verbose, + time, + ); + } + + target_lexicon::BinaryFormat::Coff => { + crate::pe::preprocess_windows( + host_exe_path, + metadata_path, + preprocessed_path, + stub_dll_symbols, + verbose, + time, + ) + .unwrap_or_else(|e| internal_error!("{}", e)); + } + + target_lexicon::BinaryFormat::Wasm => { + todo!("Roc does not yet support web assembly hosts!"); + } + target_lexicon::BinaryFormat::Unknown => { + internal_error!("Roc does not support unknown host binary formats!"); + } + other => { + internal_error!( + concat!( + r"Roc does not yet support the {:?} binary format. ", + r"Please file a bug report for this, describing what operating system you were targeting!" + ), + other, + ) + } + } +} + +fn surgery( + roc_app_bytes: &[u8], + metadata_path: &Path, + executable_path: &Path, + verbose: bool, + time: bool, + target: &Triple, +) { + match target.binary_format { + target_lexicon::BinaryFormat::Elf => { + crate::elf::surgery_elf(roc_app_bytes, metadata_path, executable_path, verbose, time); + } + + target_lexicon::BinaryFormat::Macho => { + crate::macho::surgery_macho( + roc_app_bytes, + metadata_path, + executable_path, + verbose, + time, + ); + } + + target_lexicon::BinaryFormat::Coff => { + crate::pe::surgery_pe(executable_path, metadata_path, roc_app_bytes); + } + + target_lexicon::BinaryFormat::Wasm => { + todo!("Roc does not yet support web assembly hosts!"); + } + target_lexicon::BinaryFormat::Unknown => { + internal_error!("Roc does not support unknown host binary formats!"); + } + other => { + internal_error!( + concat!( + r"Roc does not yet support the {:?} binary format. ", + r"Please file a bug report for this, describing what operating system you were targeting!" + ), + other, + ) + } + } +} + +pub(crate) fn align_by_constraint(offset: usize, constraint: usize) -> usize { + if offset % constraint == 0 { + offset + } else { + offset + constraint - (offset % constraint) + } +} + +pub(crate) fn align_to_offset_by_constraint( + current_offset: usize, + target_offset: usize, + constraint: usize, +) -> usize { + let target_remainder = target_offset % constraint; + let current_remainder = current_offset % constraint; + match target_remainder.cmp(¤t_remainder) { + Ordering::Greater => current_offset + (target_remainder - current_remainder), + Ordering::Less => current_offset + ((target_remainder + constraint) - current_remainder), + Ordering::Equal => current_offset, + } +} + +pub(crate) fn load_struct_inplace(bytes: &[u8], offset: usize) -> &T { + &load_structs_inplace(bytes, offset, 1)[0] +} + +pub(crate) fn load_struct_inplace_mut(bytes: &mut [u8], offset: usize) -> &mut T { + &mut load_structs_inplace_mut(bytes, offset, 1)[0] +} + +pub(crate) fn load_structs_inplace(bytes: &[u8], offset: usize, count: usize) -> &[T] { + let (head, body, tail) = + unsafe { bytes[offset..offset + count * mem::size_of::()].align_to::() }; + assert!(head.is_empty(), "Data was not aligned"); + assert_eq!(count, body.len(), "Failed to load all structs"); + assert!(tail.is_empty(), "End of data was not aligned"); + body +} + +pub(crate) fn load_structs_inplace_mut( + bytes: &mut [u8], + offset: usize, + count: usize, +) -> &mut [T] { + let (head, body, tail) = + unsafe { bytes[offset..offset + count * mem::size_of::()].align_to_mut::() }; + assert!(head.is_empty(), "Data was not aligned"); + assert_eq!(count, body.len(), "Failed to load all structs"); + assert!(tail.is_empty(), "End of data was not aligned"); + body +} + +pub(crate) fn open_mmap(path: &Path) -> Mmap { + let in_file = std::fs::OpenOptions::new() + .read(true) + .open(path) + .unwrap_or_else(|e| internal_error!("failed to open file {path:?}: {e}")); + + unsafe { Mmap::map(&in_file).unwrap_or_else(|e| internal_error!("{e}")) } +} + +pub(crate) fn open_mmap_mut(path: &Path, length: usize) -> MmapMut { + let out_file = std::fs::OpenOptions::new() + .read(true) + .write(true) + .create(true) + .open(path) + .unwrap_or_else(|e| internal_error!("failed to create or open file {path:?}: {e}")); + out_file + .set_len(length as u64) + .unwrap_or_else(|e| internal_error!("{e}")); + + unsafe { MmapMut::map_mut(&out_file).unwrap_or_else(|e| internal_error!("{e}")) } +} + +/// # dbg_hex +/// display dbg result in hexadecimal `{:#x?}` format. +#[macro_export] +macro_rules! dbg_hex { + // NOTE: We cannot use `concat!` to make a static string as a format argument + // of `eprintln!` because `file!` could contain a `{` or + // `$val` expression could be a block (`{ .. }`), in which case the `eprintln!` + // will be malformed. + () => { + eprintln!("[{}:{}]", file!(), line!()); + }; + ($val:expr $(,)?) => { + // Use of `match` here is intentional because it affects the lifetimes + // of temporaries - https://stackoverflow.com/a/48732525/1063961 + match $val { + tmp => { + eprintln!("[{}:{}] {} = {:#x?}", + file!(), line!(), stringify!($val), &tmp); + tmp + } + } + }; + ($($val:expr),+ $(,)?) => { + ($($crate::dbg_hex!($val)),+,) + }; +} + +// These functions don't end up in the final Roc binary but Windows linker needs a definition inside the crate. +// On Windows, there seems to be less dead-code-elimination than on Linux or MacOS, or maybe it's done later. +#[cfg(test)] +#[cfg(windows)] +#[allow(unused_imports)] +use windows_roc_platform_functions::*; + +#[cfg(test)] +#[cfg(windows)] +mod windows_roc_platform_functions { + use core::ffi::c_void; + + /// # Safety + /// The Roc application needs this. + #[no_mangle] + pub unsafe fn roc_alloc(size: usize, _alignment: u32) -> *mut c_void { + libc::malloc(size) + } + + /// # Safety + /// The Roc application needs this. + #[no_mangle] + pub unsafe 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 + /// The Roc application needs this. + #[no_mangle] + pub unsafe fn roc_dealloc(c_ptr: *mut c_void, _alignment: u32) { + libc::free(c_ptr) + } +} diff --git a/crates/linker/src/macho.rs b/crates/linker/src/macho.rs new file mode 100644 index 0000000000..ce4e8a24d1 --- /dev/null +++ b/crates/linker/src/macho.rs @@ -0,0 +1,1625 @@ +use bincode::{deserialize_from, serialize_into}; +use iced_x86::{Decoder, DecoderOptions, Instruction, OpCodeOperandKind, OpKind}; +use memmap2::MmapMut; +use object::macho; +use object::{ + CompressedFileRange, CompressionFormat, LittleEndian as LE, Object, ObjectSection, + ObjectSymbol, RelocationKind, RelocationTarget, Section, SectionIndex, SectionKind, Symbol, + SymbolIndex, SymbolSection, +}; +use roc_collections::all::MutMap; +use roc_error_macros::internal_error; +use serde::{Deserialize, Serialize}; +use std::{ + ffi::{c_char, CStr}, + io::{BufReader, BufWriter}, + mem, + path::Path, + time::{Duration, Instant}, +}; +use target_lexicon::Triple; + +use crate::{ + align_by_constraint, align_to_offset_by_constraint, load_struct_inplace, + load_struct_inplace_mut, load_structs_inplace, load_structs_inplace_mut, open_mmap, + open_mmap_mut, +}; + +const MIN_SECTION_ALIGNMENT: usize = 0x40; + +// TODO: Analyze if this offset is always correct. +const PLT_ADDRESS_OFFSET: u64 = 0x10; +const STUB_ADDRESS_OFFSET: u64 = 0x06; + +// struct MachoDynamicDeps { +// got_app_syms: Vec<(String, usize)>, +// got_sections: Vec<(usize, usize)>, +// dynamic_lib_count: usize, +// shared_lib_index: usize, +// } + +#[derive(Serialize, Deserialize, PartialEq, Eq, Debug)] +enum VirtualOffset { + Absolute, + Relative(u64), +} + +#[derive(Serialize, Deserialize, PartialEq, Eq, Debug)] +struct SurgeryEntry { + file_offset: u64, + virtual_offset: VirtualOffset, + size: u8, +} + +// TODO: Reanalyze each piece of data in this struct. +// I think a number of them can be combined to reduce string duplication. +// Also I think a few of them aren't need. +// For example, I think preprocessing can deal with all shifting and remove the need for added_byte_count. +// TODO: we probably should be storing numbers in an endian neutral way. +#[derive(Default, Serialize, Deserialize, PartialEq, Eq, Debug)] +struct Metadata { + app_functions: Vec, + // offset followed by address. + plt_addresses: MutMap, + surgeries: MutMap>, + dynamic_symbol_indices: MutMap, + _static_symbol_indices: MutMap, + roc_symbol_vaddresses: MutMap, + exec_len: u64, + load_align_constraint: u64, + added_byte_count: u64, + last_vaddr: u64, + _dynamic_section_offset: u64, + _dynamic_symbol_table_section_offset: u64, + _symbol_table_section_offset: u64, + _symbol_table_size: u64, + macho_cmd_loc: u64, +} + +impl Metadata { + fn write_to_file(&self, metadata_filename: &Path) { + let metadata_file = + std::fs::File::create(metadata_filename).unwrap_or_else(|e| internal_error!("{}", e)); + + serialize_into(BufWriter::new(metadata_file), self) + .unwrap_or_else(|err| internal_error!("Failed to serialize metadata: {err}")); + } + + fn read_from_file(metadata_filename: &Path) -> Self { + let input = std::fs::File::open(metadata_filename).unwrap_or_else(|e| { + internal_error!( + r#" + + Error: + {}\n"#, + e + ) + }); + + match deserialize_from(BufReader::new(input)) { + Ok(data) => data, + Err(err) => { + internal_error!("Failed to deserialize metadata: {}", err); + } + } + } +} + +fn report_timing(label: &str, duration: Duration) { + println!("\t{:9.3} ms {}", duration.as_secs_f64() * 1000.0, label,); +} + +fn is_roc_symbol(sym: &object::Symbol) -> bool { + if let Ok(name) = sym.name() { + name.trim_start_matches('_').starts_with("roc_") + } else { + false + } +} + +fn is_roc_definition(sym: &object::Symbol) -> bool { + sym.is_definition() && is_roc_symbol(sym) +} + +fn is_roc_undefined(sym: &object::Symbol) -> bool { + sym.is_undefined() && is_roc_symbol(sym) +} + +fn collect_roc_definitions<'a>(object: &object::File<'a, &'a [u8]>) -> MutMap { + let mut vaddresses = MutMap::default(); + + for sym in object.symbols().filter(is_roc_definition) { + // remove potentially trailing "@version". + let name = sym + .name() + .unwrap() + .trim_start_matches('_') + .split('@') + .next() + .unwrap(); + + let address = sym.address(); + + // special exceptions for memcpy and memset. + if name == "roc_memset" { + vaddresses.insert("memset".to_string(), address); + } + + vaddresses.insert(name.to_string(), address); + } + + vaddresses +} + +struct Surgeries<'a> { + surgeries: MutMap>, + app_func_addresses: MutMap, + indirect_warning_given: bool, +} + +impl<'a> Surgeries<'a> { + fn new(application_symbols: &[Symbol], app_func_addresses: MutMap) -> Self { + let mut surgeries = MutMap::default(); + + // for each symbol that the host expects from the application + // we start with an empty set of places to perform surgery + for symbol in application_symbols { + let name = symbol.name().unwrap().to_string(); + surgeries.insert(name, vec![]); + } + + Self { + surgeries, + app_func_addresses, + indirect_warning_given: false, + } + } + + fn append_text_sections( + &mut self, + object_bytes: &[u8], + object: &object::File<'a, &'a [u8]>, + verbose: bool, + ) { + let text_sections: Vec
= object + .sections() + .filter(|sec| sec.kind() == SectionKind::Text) + .collect(); + if text_sections.is_empty() { + internal_error!("No text sections found. This application has no code."); + } + if verbose { + println!(); + println!("Text Sections"); + for sec in text_sections.iter() { + println!("{sec:+x?}"); + } + } + + if verbose { + println!(); + println!("Analyzing instuctions for branches"); + } + + for text_section in text_sections { + self.append_text_section(object_bytes, &text_section, verbose) + } + } + + fn append_text_section(&mut self, object_bytes: &[u8], sec: &Section, verbose: bool) { + let (file_offset, compressed) = match sec.compressed_file_range() { + Ok(CompressedFileRange { + format: CompressionFormat::None, + offset, + .. + }) => (offset, false), + Ok(range) => (range.offset, true), + Err(err) => { + internal_error!( + "Issues dealing with section compression for {:+x?}: {}", + sec, + err + ); + } + }; + + let data = match sec.uncompressed_data() { + Ok(data) => data, + Err(err) => { + internal_error!("Failed to load text section, {:+x?}: {}", sec, err); + } + }; + let mut decoder = Decoder::with_ip(64, &data, sec.address(), DecoderOptions::NONE); + let mut inst = Instruction::default(); + + while decoder.can_decode() { + decoder.decode_out(&mut inst); + + // Note: This gets really complex fast if we want to support more than basic calls/jumps. + // A lot of them have to load addresses into registers/memory so we would have to discover that value. + // Would probably require some static code analysis and would be impossible in some cases. + // As an alternative we can leave in the calls to the plt, but change the plt to jmp to the static function. + // That way any indirect call will just have the overhead of an extra jump. + match inst.try_op_kind(0) { + // Relative Offsets. + Ok(OpKind::NearBranch16 | OpKind::NearBranch32 | OpKind::NearBranch64) => { + let target = inst.near_branch_target(); + if let Some(func_name) = self.app_func_addresses.get(&target) { + if compressed { + internal_error!("Surgical linking does not work with compressed text sections: {:+x?}", sec); + } + + if verbose { + println!( + "Found branch from {:+x} to {:+x}({})", + inst.ip(), + target, + func_name + ); + } + + // TODO: Double check these offsets are always correct. + // We may need to do a custom offset based on opcode instead. + let op_kind = inst.op_code().try_op_kind(0).unwrap(); + let op_size: u8 = match op_kind { + OpCodeOperandKind::br16_1 | OpCodeOperandKind::br32_1 => 1, + OpCodeOperandKind::br16_2 => 2, + OpCodeOperandKind::br32_4 | OpCodeOperandKind::br64_4 => 4, + _ => { + internal_error!( + "Ran into an unknown operand kind when analyzing branches: {:?}", + op_kind + ); + } + }; + let offset = inst.next_ip() - op_size as u64 - sec.address() + file_offset; + if verbose { + println!( + "\tNeed to surgically replace {op_size} bytes at file offset {offset:+x}", + ); + println!( + "\tIts current value is {:+x?}", + &object_bytes[offset as usize..(offset + op_size as u64) as usize] + ) + } + self.surgeries + .get_mut(*func_name) + .unwrap() + .push(SurgeryEntry { + file_offset: offset, + virtual_offset: VirtualOffset::Relative(inst.next_ip()), + size: op_size, + }); + } + } + Ok(OpKind::FarBranch16 | OpKind::FarBranch32) => { + internal_error!( + "Found branch type instruction that is not yet support: {:+x?}", + inst + ); + } + Ok(_) => { + if (inst.is_call_far_indirect() + || inst.is_call_near_indirect() + || inst.is_jmp_far_indirect() + || inst.is_jmp_near_indirect()) + && !self.indirect_warning_given + && verbose + { + self.indirect_warning_given = true; + println!(); + println!("Cannot analyze through indirect jmp type instructions"); + println!("Most likely this is not a problem, but it could mean a loss in optimizations"); + println!(); + } + } + Err(err) => { + internal_error!("Failed to decode assembly: {}", err); + } + } + } + } +} + +/// Constructs a `Metadata` from a host executable binary, and writes it to disk +pub(crate) fn preprocess_macho( + target: &Triple, + host_exe_path: &Path, + metadata_path: &Path, + preprocessed_path: &Path, + shared_lib: &Path, + verbose: bool, + time: bool, +) { + let total_start = Instant::now(); + let exec_parsing_start = total_start; + let exec_data = &*open_mmap(host_exe_path); + let exec_obj = match object::File::parse(exec_data) { + Ok(obj) => obj, + Err(err) => { + internal_error!("Failed to parse executable file: {}", err); + } + }; + + let mut md = Metadata { + roc_symbol_vaddresses: collect_roc_definitions(&exec_obj), + ..Default::default() + }; + + if verbose { + println!( + "Found roc symbol definitions: {:+x?}", + md.roc_symbol_vaddresses + ); + } + + let exec_parsing_duration = exec_parsing_start.elapsed(); + + // PLT stands for Procedure Linkage Table which is, put simply, used to call external + // procedures/functions whose address isn't known in the time of linking, and is left + // to be resolved by the dynamic linker at run time. + let symbol_and_plt_processing_start = Instant::now(); + let plt_section_name = "__stubs"; + + let (plt_address, plt_offset) = match exec_obj.section_by_name(plt_section_name) { + Some(section) => { + let file_offset = match section.compressed_file_range() { + Ok( + range @ CompressedFileRange { + format: CompressionFormat::None, + .. + }, + ) => range.offset, + _ => { + internal_error!("Surgical linking does not work with compressed plt section"); + } + }; + (section.address(), file_offset) + } + None => { + internal_error!("Failed to find PLT section. Probably an malformed executable."); + } + }; + if verbose { + println!("PLT Address: {plt_address:+x}"); + println!("PLT File Offset: {plt_offset:+x}"); + } + + let app_syms: Vec<_> = exec_obj.symbols().filter(is_roc_undefined).collect(); + + let mut app_func_addresses: MutMap = MutMap::default(); + let mut macho_load_so_offset = None; + + { + use macho::{DyldInfoCommand, DylibCommand, Section64, SegmentCommand64}; + + let exec_header = load_struct_inplace::>(exec_data, 0); + let num_load_cmds = exec_header.ncmds.get(LE); + + let mut offset = mem::size_of_val(exec_header); + + let mut stubs_symbol_index = None; + let mut stubs_symbol_count = None; + + 'cmds: for _ in 0..num_load_cmds { + let info = load_struct_inplace::>(exec_data, offset); + let cmd = info.cmd.get(LE); + let cmdsize = info.cmdsize.get(LE); + + if cmd == macho::LC_SEGMENT_64 { + let info = load_struct_inplace::>(exec_data, offset); + + if &info.segname[0..6] == b"__TEXT" { + let sections = info.nsects.get(LE); + + let sections_info = load_structs_inplace::>( + exec_data, + offset + mem::size_of_val(info), + sections as usize, + ); + + for section_info in sections_info { + if §ion_info.sectname[0..7] == b"__stubs" { + stubs_symbol_index = Some(section_info.reserved1.get(LE)); + stubs_symbol_count = + Some(section_info.size.get(LE) / STUB_ADDRESS_OFFSET); + + break 'cmds; + } + } + } + } + + offset += cmdsize as usize; + } + + let stubs_symbol_index = stubs_symbol_index.unwrap_or_else(|| { + panic!("Could not find stubs symbol index."); + }); + let stubs_symbol_count = stubs_symbol_count.unwrap_or_else(|| { + panic!("Could not find stubs symbol count."); + }); + + // Reset offset before looping through load commands again + offset = mem::size_of_val(exec_header); + + let shared_lib_filename = shared_lib.file_name(); + + for _ in 0..num_load_cmds { + let info = load_struct_inplace::>(exec_data, offset); + let cmd = info.cmd.get(LE); + let cmdsize = info.cmdsize.get(LE); + + if cmd == macho::LC_DYLD_INFO_ONLY { + let info = load_struct_inplace::>(exec_data, offset); + + let lazy_bind_offset = info.lazy_bind_off.get(LE) as usize; + + let lazy_bind_symbols = mach_object::LazyBind::parse( + &exec_data[lazy_bind_offset..], + mem::size_of::(), + ); + + // Find all the lazily-bound roc symbols + // (e.g. "_roc__mainForHost_1_exposed") + // For Macho, we may need to deal with some GOT stuff here as well. + for (i, symbol) in lazy_bind_symbols + .skip(stubs_symbol_index as usize) + .take(stubs_symbol_count as usize) + .enumerate() + { + if let Some(sym) = app_syms + .iter() + .find(|app_sym| app_sym.name() == Ok(&symbol.name)) + { + let func_address = (i as u64 + 1) * STUB_ADDRESS_OFFSET + plt_address; + let func_offset = (i as u64 + 1) * STUB_ADDRESS_OFFSET + plt_offset; + app_func_addresses.insert(func_address, sym.name().unwrap()); + md.plt_addresses + .insert(sym.name().unwrap().to_string(), (func_offset, func_address)); + } + } + } else if cmd == macho::LC_LOAD_DYLIB { + let info = load_struct_inplace::>(exec_data, offset); + let name_offset = info.dylib.name.offset.get(LE) as usize; + let str_start_index = offset + name_offset; + let str_end_index = offset + cmdsize as usize; + let str_bytes = &exec_data[str_start_index..str_end_index]; + let path = { + if str_bytes[str_bytes.len() - 1] == 0 { + // If it's nul-terminated, it's a C String. + // Use the unchecked version because these are + // padded with 0s at the end, so since we don't + // know the exact length, using the checked version + // of this can fail due to the interior nul bytes. + // + // Also, we have to use from_ptr instead of + // from_bytes_with_nul_unchecked because currently + // std::ffi::CStr is actually not a char* under + // the hood (!) but rather an array, so to strip + // the trailing null bytes we have to use from_ptr. + let c_str = unsafe { CStr::from_ptr(str_bytes.as_ptr() as *const c_char) }; + + Path::new(c_str.to_str().unwrap()) + } else { + // It wasn't nul-terminated, so treat all the bytes + // as the string + + Path::new(std::str::from_utf8(str_bytes).unwrap()) + } + }; + + if path.file_name() == shared_lib_filename { + macho_load_so_offset = Some(offset); + } + } + + offset += cmdsize as usize; + } + } + + for sym in app_syms.iter() { + let name = sym.name().unwrap().to_string(); + md.app_functions.push(name.clone()); + md.dynamic_symbol_indices.insert(name, sym.index().0 as u64); + } + if verbose { + println!(); + println!("PLT Symbols for App Functions"); + for symbol in app_syms.iter() { + println!("{}: {:+x?}", symbol.index().0, symbol); + } + + println!(); + println!("App Function Address Map: {app_func_addresses:+x?}"); + } + let symbol_and_plt_processing_duration = symbol_and_plt_processing_start.elapsed(); + + // look at the text (i.e. code) sections and see collect work needs to be done + let text_disassembly_start = Instant::now(); + + let mut surgeries = Surgeries::new(&app_syms, app_func_addresses); + surgeries.append_text_sections(exec_data, &exec_obj, verbose); + md.surgeries = surgeries.surgeries; + + let text_disassembly_duration = text_disassembly_start.elapsed(); + + let scanning_dynamic_deps_duration; + let platform_gen_start; + + let out_mmap = { + match target + .endianness() + .unwrap_or(target_lexicon::Endianness::Little) + { + target_lexicon::Endianness::Little => { + let scanning_dynamic_deps_start = Instant::now(); + + // let ElfDynamicDeps { + // got_app_syms, + // got_sections, + // dynamic_lib_count, + // shared_lib_index, + // } = scan_elf_dynamic_deps( + // &exec_obj, &mut md, &app_syms, shared_lib, exec_data, verbose, + // ); + + scanning_dynamic_deps_duration = scanning_dynamic_deps_start.elapsed(); + + platform_gen_start = Instant::now(); + + // TODO little endian + let macho_load_so_offset = match macho_load_so_offset { + Some(offset) => offset, + None => { + internal_error!("Host does not link library `{}`!", shared_lib.display()); + } + }; + + // TODO this is correct on modern Macs (they align to the page size) + // but maybe someone can override the alignment somehow? Maybe in the + // future this could change? Is there some way to make this more future-proof? + md.load_align_constraint = 4096; + + gen_macho_le( + exec_data, + &mut md, + preprocessed_path, + macho_load_so_offset, + target, + verbose, + ) + } + target_lexicon::Endianness::Big => { + // TODO Is big-endian macOS even a thing that exists anymore? + // Just ancient PowerPC machines maybe? + todo!("Roc does not yet support big-endian macOS hosts!"); + } + } + }; + + let platform_gen_duration = platform_gen_start.elapsed(); + + if verbose { + println!(); + println!("{md:+x?}"); + } + + let saving_metadata_start = Instant::now(); + md.write_to_file(metadata_path); + let saving_metadata_duration = saving_metadata_start.elapsed(); + + let flushing_data_start = Instant::now(); + out_mmap + .flush() + .unwrap_or_else(|e| internal_error!("{}", e)); + // Also drop files to to ensure data is fully written here. + drop(out_mmap); + let flushing_data_duration = flushing_data_start.elapsed(); + + let total_duration = total_start.elapsed(); + + if verbose || time { + println!(); + println!("Timings"); + report_timing("Executable Parsing", exec_parsing_duration); + report_timing( + "Symbol and PLT Processing", + symbol_and_plt_processing_duration, + ); + report_timing("Text Disassembly", text_disassembly_duration); + report_timing("Scanning Dynamic Deps", scanning_dynamic_deps_duration); + report_timing("Generate Modified Platform", platform_gen_duration); + report_timing("Saving Metadata", saving_metadata_duration); + report_timing("Flushing Data to Disk", flushing_data_duration); + report_timing( + "Other", + total_duration + - exec_parsing_duration + - symbol_and_plt_processing_duration + - text_disassembly_duration + - scanning_dynamic_deps_duration + - platform_gen_duration + - saving_metadata_duration + - flushing_data_duration, + ); + report_timing("Total", total_duration); + } +} + +fn gen_macho_le( + exec_data: &[u8], + md: &mut Metadata, + out_filename: &Path, + macho_load_so_offset: usize, + _target: &Triple, + _verbose: bool, +) -> MmapMut { + // Just adding some extra context/useful info here. + // I was talking to Jakub from the Zig team about macho linking and here are some useful comments: + // 1) Macho WILL run fine with multiple text segments (and theoretically data segments). + // 2) Theoretically the headers just need to be in a loadable segment, + // but otherwise don't need to relate to the starting text segment (releated to some added byte shifting information below). + // 2) Apple tooling dislikes whenever you do something non-standard, + // and there is a chance it won't work right (e.g. codesigning might fail) + // 3) Jakub wants to make apple tooling absolute is working on zignature for code signing and zig-deploy for ios apps + // https://github.com/kubkon/zignature + // https://github.com/kubkon/zig-deploy + + use macho::{Section64, SegmentCommand64}; + + let exec_header = load_struct_inplace::>(exec_data, 0); + let num_load_cmds = exec_header.ncmds.get(LE); + let size_of_cmds = exec_header.sizeofcmds.get(LE) as usize; + + // Add a new text segment and data segment + let segment_cmd_size = mem::size_of::>(); + let section_size = mem::size_of::>(); + + // We need the full command size, including the dynamic-length string at the end. + // To get that, we need to load the command. + let info = load_struct_inplace::>(exec_data, macho_load_so_offset); + let total_cmd_size = info.cmdsize.get(LE) as usize; + + // ======================== Important TODO ========================== + // TODO: we accidentally instroduced a big change here. + // We use a mix of added_bytes and md.added_byte_count. + // Theses should proabably be the same variable. + // Also, I just realized that I have been shifting a lot of virtual offsets. + // This should not be necessary. If we add a fully 4k of buffer to the file, all of the virtual offsets will still stay aligned. + // So a lot of the following work can probably be commented out if we fix that. + // Of coruse, it will cost about 4k bytes to a final executable. + // This is what the elf version currently does. + // Theoretically it is not needed (if we update all virtual addresses in the binary), + // but I definitely ran into problems with elf when not adding this extra buffering. + + // Copy header and shift everything to enable more program sections. + let added_byte_count = ((2 * segment_cmd_size) + (2 * section_size) - total_cmd_size) as u64; + md.added_byte_count = added_byte_count + // add some alignment padding + + (MIN_SECTION_ALIGNMENT as u64 - added_byte_count % MIN_SECTION_ALIGNMENT as u64); + + md.exec_len = exec_data.len() as u64 + md.added_byte_count; + + let mut out_mmap = open_mmap_mut(out_filename, md.exec_len as usize); + let end_of_cmds = size_of_cmds + mem::size_of_val(exec_header); + + // "Delete" the dylib load command - by copying all the bytes before it + // and all the bytes after it, while skipping over its bytes. + // It has a dynamic-length string at the end that we also need to delete, + // in addition to the header. + out_mmap[..macho_load_so_offset].copy_from_slice(&exec_data[..macho_load_so_offset]); + + out_mmap[macho_load_so_offset..end_of_cmds - total_cmd_size] + .copy_from_slice(&exec_data[macho_load_so_offset + total_cmd_size..end_of_cmds]); + + // Copy the rest of the file, leaving a gap for the surgical linking to add our 2 commands + // (which happens after preprocessing), and some zero padding at the end for alignemnt. + // (It seems to cause bugs if that padding isn't there!) + let rest_of_data = &exec_data[end_of_cmds..]; + let start_index = end_of_cmds + md.added_byte_count as usize; + + out_mmap[start_index..start_index + rest_of_data.len()].copy_from_slice(rest_of_data); + + let out_header = load_struct_inplace_mut::>(&mut out_mmap, 0); + + // TODO: this needs to change to adding the 2 new commands when we are ready. + // -1 because we're deleting 1 load command and then NOT adding 2 new ones. + { + let added_bytes = -(total_cmd_size as isize); // TODO: Change when add the new sections. + out_header.ncmds.set(LE, num_load_cmds - 1); + out_header + .sizeofcmds + .set(LE, (size_of_cmds as isize + added_bytes) as u32); + } + + // Go through every command and shift it by added_bytes if it's absolute, unless it's inside the command header + let mut offset = mem::size_of_val(exec_header); + + // TODO: Add this back in the future when needed. + // let cpu_type = match target.architecture { + // target_lexicon::Architecture::X86_64 => macho::CPU_TYPE_X86_64, + // target_lexicon::Architecture::Aarch64(_) => macho::CPU_TYPE_ARM64, + // _ => { + // // We should have verified this via supported() before calling this function + // unreachable!() + // } + // }; + + // minus one because we "deleted" a load command + for _ in 0..(num_load_cmds - 1) { + let info = load_struct_inplace::>(&out_mmap, offset); + let cmd_size = info.cmdsize.get(LE) as usize; + + match info.cmd.get(LE) { + macho::LC_SEGMENT_64 => { + let cmd = + load_struct_inplace_mut::>(&mut out_mmap, offset); + + // Ignore page zero, it never moves. + if cmd.segname == "__PAGEZERO\0\0\0\0\0\0".as_bytes() || cmd.vmaddr.get(LE) == 0 { + offset += cmd_size; + continue; + } + + let old_file_offest = cmd.fileoff.get(LE); + // The segment with file offset zero also includes the header. + // As such, its file offset does not change. + // Instead, its file size should be increased. + if old_file_offest > 0 { + cmd.fileoff.set(LE, old_file_offest + md.added_byte_count); + cmd.vmaddr.set(LE, cmd.vmaddr.get(LE) + md.added_byte_count); + } else { + cmd.filesize + .set(LE, cmd.filesize.get(LE) + md.added_byte_count); + cmd.vmsize.set(LE, cmd.vmsize.get(LE) + md.added_byte_count); + } + + // let num_sections = cmd.nsects.get(LE); + // let sections = load_structs_inplace_mut::>( + // &mut out_mmap, + // offset + mem::size_of::>(), + // num_sections as usize, + // ); + // struct Relocation { + // offset: u32, + // num_relocations: u32, + // } + + // let mut relocation_offsets = Vec::with_capacity(sections.len()); + + // for section in sections { + // section.addr.set( + // LE , + // section.addr.get(LE) + md.added_byte_count as u64, + // ); + + // // If offset is zero, don't update it. + // // Zero is used for things like BSS that don't exist in the file. + // let old_offset = section.offset.get(LE); + // if old_offset > 0 { + // section + // .offset + // .set(LE , old_offset + md.added_byte_count as u32); + // } + + // // dbg!(§ion.reloff.get(LE)); + // // dbg!(section.reloff.get(LE) as i32); + // // dbg!(§ion); + // // dbg!(&md.added_byte_count); + // // dbg!(String::from_utf8_lossy(§ion.sectname)); + // if section.nreloc.get(LE) > 0 { + // section.reloff.set( + // LE , + // section.reloff.get(LE) + md.added_byte_count as u32, + // ); + // } + + // relocation_offsets.push(Relocation { + // offset: section.reloff.get(LE), + // num_relocations: section.nreloc.get(LE), + // }); + // } + + // TODO FIXME this is necessary for ARM, but seems to be broken. Skipping for now since we're just targeting x86 + // for Relocation { + // offset, + // num_relocations, + // } in relocation_offsets + // { + // let relos = load_structs_inplace_mut::>( + // &mut out_mmap, + // offset as usize, + // num_relocations as usize, + // ); + + // // TODO this has never been tested, because scattered relocations only come up on ARM! + // for relo in relos.iter_mut() { + // if relo.r_scattered(LE , cpu_type) { + // let mut scattered_info = relo.scattered_info(LE); + + // if !scattered_info.r_pcrel { + // scattered_info.r_value += md.added_byte_count as u32; + + // let new_info = scattered_info.relocation(LE ); + + // relo.r_word0 = new_info.r_word0; + // relo.r_word1 = new_info.r_word1; + // } + // } + // } + // } + + // TODO this seems to be wrong and unnecessary, and should probably be deleted. + // offset += num_sections as usize * mem::size_of::>(); + } + macho::LC_SYMTAB => { + let cmd = + load_struct_inplace_mut::>(&mut out_mmap, offset); + + let sym_offset = cmd.symoff.get(LE); + let num_syms = cmd.nsyms.get(LE); + + if num_syms > 0 { + cmd.symoff.set(LE, sym_offset + md.added_byte_count as u32); + } + + if cmd.strsize.get(LE) > 0 { + cmd.stroff + .set(LE, cmd.stroff.get(LE) + md.added_byte_count as u32); + } + + let table = load_structs_inplace_mut::>( + &mut out_mmap, + sym_offset as usize + md.added_byte_count as usize, + num_syms as usize, + ); + + for entry in table { + let entry_type = entry.n_type & macho::N_TYPE; + if entry_type == macho::N_ABS || entry_type == macho::N_SECT { + entry + .n_value + .set(LE, entry.n_value.get(LE) + md.added_byte_count); + } + } + } + macho::LC_DYSYMTAB => { + let cmd = + load_struct_inplace_mut::>(&mut out_mmap, offset); + + if cmd.ntoc.get(LE) > 0 { + cmd.tocoff + .set(LE, cmd.tocoff.get(LE) + md.added_byte_count as u32); + } + + if cmd.nmodtab.get(LE) > 0 { + cmd.modtaboff + .set(LE, cmd.modtaboff.get(LE) + md.added_byte_count as u32); + } + + if cmd.nextrefsyms.get(LE) > 0 { + cmd.extrefsymoff + .set(LE, cmd.extrefsymoff.get(LE) + md.added_byte_count as u32); + } + + if cmd.nindirectsyms.get(LE) > 0 { + cmd.indirectsymoff + .set(LE, cmd.indirectsymoff.get(LE) + md.added_byte_count as u32); + } + + if cmd.nextrel.get(LE) > 0 { + cmd.extreloff + .set(LE, cmd.extreloff.get(LE) + md.added_byte_count as u32); + } + + if cmd.nlocrel.get(LE) > 0 { + cmd.locreloff + .set(LE, cmd.locreloff.get(LE) + md.added_byte_count as u32); + } + + // TODO maybe we need to update something else too - relocations maybe? + // I think this also has symbols that need to get moved around. + // Look at otool -I at least for the indirect symbols. + } + macho::LC_TWOLEVEL_HINTS => { + let cmd = load_struct_inplace_mut::>( + &mut out_mmap, + offset, + ); + + if cmd.nhints.get(LE) > 0 { + cmd.offset + .set(LE, cmd.offset.get(LE) + md.added_byte_count as u32); + } + } + macho::LC_FUNCTION_STARTS => { + let cmd = load_struct_inplace_mut::>( + &mut out_mmap, + offset, + ); + + if cmd.datasize.get(LE) > 0 { + cmd.dataoff + .set(LE, cmd.dataoff.get(LE) + md.added_byte_count as u32); + // TODO: This lists the start of every function. Which, of course, have moved. + // That being said, to my understanding this section is optional and may just be debug information. + // As such, updating it should not be required. + // It will be more work to update due to being in "DWARF-style ULEB128" values. + } + } + macho::LC_DATA_IN_CODE => { + let (offset, size) = { + let cmd = load_struct_inplace_mut::>( + &mut out_mmap, + offset, + ); + + if cmd.datasize.get(LE) > 0 { + cmd.dataoff + .set(LE, cmd.dataoff.get(LE) + md.added_byte_count as u32); + } + (cmd.dataoff.get(LE), cmd.datasize.get(LE)) + }; + + // Update every data in code entry. + if size > 0 { + let entry_size = mem::size_of::>(); + let entries = load_structs_inplace_mut::>( + &mut out_mmap, + offset as usize, + size as usize / entry_size, + ); + for entry in entries.iter_mut() { + entry + .offset + .set(LE, entry.offset.get(LE) + md.added_byte_count as u32) + } + } + } + macho::LC_CODE_SIGNATURE + | macho::LC_SEGMENT_SPLIT_INFO + | macho::LC_DYLIB_CODE_SIGN_DRS + | macho::LC_LINKER_OPTIMIZATION_HINT + | macho::LC_DYLD_EXPORTS_TRIE + | macho::LC_DYLD_CHAINED_FIXUPS => { + let cmd = load_struct_inplace_mut::>( + &mut out_mmap, + offset, + ); + + if cmd.datasize.get(LE) > 0 { + cmd.dataoff + .set(LE, cmd.dataoff.get(LE) + md.added_byte_count as u32); + } + } + macho::LC_ENCRYPTION_INFO_64 => { + let cmd = load_struct_inplace_mut::>( + &mut out_mmap, + offset, + ); + + if cmd.cryptsize.get(LE) > 0 { + cmd.cryptoff + .set(LE, cmd.cryptoff.get(LE) + md.added_byte_count as u32); + } + } + macho::LC_DYLD_INFO | macho::LC_DYLD_INFO_ONLY => { + let cmd = + load_struct_inplace_mut::>(&mut out_mmap, offset); + + if cmd.rebase_size.get(LE) > 0 { + cmd.rebase_off + .set(LE, cmd.rebase_off.get(LE) + md.added_byte_count as u32); + } + + if cmd.bind_size.get(LE) > 0 { + cmd.bind_off + .set(LE, cmd.bind_off.get(LE) + md.added_byte_count as u32); + } + + if cmd.weak_bind_size.get(LE) > 0 { + cmd.weak_bind_off + .set(LE, cmd.weak_bind_off.get(LE) + md.added_byte_count as u32); + } + + if cmd.lazy_bind_size.get(LE) > 0 { + cmd.lazy_bind_off + .set(LE, cmd.lazy_bind_off.get(LE) + md.added_byte_count as u32); + } + + // TODO: Parse and update the related tables here. + // It is possible we may just need to delete things that point to stuff that will be in the roc app. + // We also may just be able to ignore it (lazy bindings should never run). + // This definitely has a list of virtual address that need to be updated. + // Some of them definitely will point to the roc app and should probably be removed. + // Also `xcrun dyldinfo` is useful for debugging this. + } + macho::LC_SYMSEG => { + let cmd = + load_struct_inplace_mut::>(&mut out_mmap, offset); + + if cmd.size.get(LE) > 0 { + cmd.offset + .set(LE, cmd.offset.get(LE) + md.added_byte_count as u32); + } + } + macho::LC_MAIN => { + let cmd = + load_struct_inplace_mut::>(&mut out_mmap, offset); + + cmd.entryoff + .set(LE, cmd.entryoff.get(LE) + md.added_byte_count); + } + macho::LC_NOTE => { + let cmd = load_struct_inplace_mut::>(&mut out_mmap, offset); + + if cmd.size.get(LE) > 0 { + cmd.offset.set(LE, cmd.offset.get(LE) + md.added_byte_count); + } + } + macho::LC_ID_DYLIB + | macho::LC_LOAD_WEAK_DYLIB + | macho::LC_LOAD_DYLIB + | macho::LC_REEXPORT_DYLIB + | macho::LC_SOURCE_VERSION + | macho::LC_IDENT + | macho::LC_LINKER_OPTION + | macho::LC_BUILD_VERSION + | macho::LC_VERSION_MIN_MACOSX + | macho::LC_VERSION_MIN_IPHONEOS + | macho::LC_VERSION_MIN_WATCHOS + | macho::LC_VERSION_MIN_TVOS + | macho::LC_UUID + | macho::LC_RPATH + | macho::LC_ID_DYLINKER + | macho::LC_LOAD_DYLINKER + | macho::LC_DYLD_ENVIRONMENT + | macho::LC_ROUTINES_64 + | macho::LC_THREAD + | macho::LC_UNIXTHREAD + | macho::LC_PREBOUND_DYLIB + | macho::LC_SUB_FRAMEWORK + | macho::LC_SUB_CLIENT + | macho::LC_SUB_UMBRELLA + | macho::LC_SUB_LIBRARY => { + // These don't involve file offsets, so no change is needed for these. + // Just continue the loop! + } + macho::LC_PREBIND_CKSUM => { + // We don't *think* we need to change this, but + // maybe what we're doing will break checksums? + } + macho::LC_SEGMENT | macho::LC_ROUTINES | macho::LC_ENCRYPTION_INFO => { + // These are 32-bit and unsuppoted + unreachable!(); + } + macho::LC_FVMFILE | macho::LC_IDFVMLIB | macho::LC_LOADFVMLIB => { + // These are obsolete and unsupported + unreachable!() + } + cmd => { + eprintln!( + "- - - Unrecognized Mach-O command during linker preprocessing: 0x{cmd:x?}" + ); + // panic!( + // "Unrecognized Mach-O command during linker preprocessing: 0x{:x?}", + // cmd + // ); + } + } + + offset += dbg!(cmd_size); + } + + // cmd_loc should be where the last offset ended + md.macho_cmd_loc = offset as u64; + + out_mmap +} + +// fn scan_macho_dynamic_deps( +// _exec_obj: &object::File, +// _md: &mut Metadata, +// _app_syms: &[Symbol], +// _shared_lib: &str, +// _exec_data: &[u8], +// _verbose: bool, +// ) { +// // TODO +// } + +pub(crate) fn surgery_macho( + roc_app_bytes: &[u8], + metadata_path: &Path, + executable_path: &Path, + verbose: bool, + time: bool, +) { + let app_obj = match object::File::parse(roc_app_bytes) { + Ok(obj) => obj, + Err(err) => { + internal_error!("Failed to parse application file: {}", err); + } + }; + + let total_start = Instant::now(); + + let loading_metadata_start = total_start; + let md = Metadata::read_from_file(metadata_path); + let loading_metadata_duration = loading_metadata_start.elapsed(); + + let load_and_mmap_start = Instant::now(); + let max_out_len = md.exec_len + roc_app_bytes.len() as u64 + md.load_align_constraint; + let mut exec_mmap = open_mmap_mut(executable_path, max_out_len as usize); + let load_and_mmap_duration = load_and_mmap_start.elapsed(); + + let out_gen_start = Instant::now(); + let mut offset = 0; + + surgery_macho_help( + metadata_path, + executable_path, + verbose, + time, + &md, + &mut exec_mmap, + &mut offset, + app_obj, + ); + + let out_gen_duration = out_gen_start.elapsed(); + let flushing_data_start = Instant::now(); + + // TODO investigate using the async version of flush - might be faster due to not having to block on that + exec_mmap + .flush() + .unwrap_or_else(|e| internal_error!("{}", e)); + // Also drop files to to ensure data is fully written here. + drop(exec_mmap); + + let flushing_data_duration = flushing_data_start.elapsed(); + + // Make sure the final executable has permision to execute. + #[cfg(target_family = "unix")] + { + use std::fs; + use std::os::unix::fs::PermissionsExt; + + let mut perms = fs::metadata(executable_path) + .unwrap_or_else(|e| internal_error!("{}", e)) + .permissions(); + perms.set_mode(perms.mode() | 0o111); + fs::set_permissions(executable_path, perms).unwrap_or_else(|e| internal_error!("{}", e)); + } + + let total_duration = total_start.elapsed(); + + if verbose || time { + println!("\nTimings"); + report_timing("Loading Metadata", loading_metadata_duration); + report_timing("Loading and mmap-ing", load_and_mmap_duration); + report_timing("Output Generation", out_gen_duration); + report_timing("Flushing Data to Disk", flushing_data_duration); + + let sum = loading_metadata_duration + + load_and_mmap_duration + + out_gen_duration + + flushing_data_duration; + + report_timing("Other", total_duration.saturating_sub(sum)); + report_timing("Total", total_duration); + } +} + +#[allow(clippy::too_many_arguments)] +fn surgery_macho_help( + _metadata_filename: &Path, + _out_filename: &Path, + verbose: bool, + _time: bool, + md: &Metadata, + exec_mmap: &mut MmapMut, + offset_ref: &mut usize, // TODO return this instead of taking a mutable reference to it + app_obj: object::File, +) { + let mut offset = align_by_constraint(md.exec_len as usize, MIN_SECTION_ALIGNMENT); + // let new_rodata_section_offset = offset; + + // Align physical and virtual address of new segment. + let mut virt_offset = align_to_offset_by_constraint( + md.last_vaddr as usize, + offset, + md.load_align_constraint as usize, + ); + let new_rodata_section_vaddr = virt_offset; + if verbose { + println!(); + println!("New Virtual Rodata Section Address: {new_rodata_section_vaddr:+x?}"); + } + + // First decide on sections locations and then recode every exact symbol locations. + + // Copy sections and resolve their symbols/relocations. + let symbols = app_obj.symbols().collect::>(); + let mut section_offset_map: MutMap = MutMap::default(); + let mut symbol_vaddr_map: MutMap = MutMap::default(); + let mut app_func_vaddr_map: MutMap = MutMap::default(); + let mut app_func_size_map: MutMap = MutMap::default(); + + // TODO: In the future Roc may use a data section to store memoized toplevel thunks + // in development builds for caching the results of top-level constants + + let rodata_sections: Vec
= app_obj + .sections() + .filter(|sec| sec.kind() == SectionKind::ReadOnlyData) + .collect(); + + // bss section is like rodata section, but it has zero file size and non-zero virtual size. + let bss_sections: Vec
= app_obj + .sections() + .filter(|sec| sec.kind() == SectionKind::UninitializedData) + .collect(); + + let text_sections: Vec
= app_obj + .sections() + .filter(|sec| sec.kind() == SectionKind::Text) + .collect(); + if text_sections.is_empty() { + internal_error!("No text sections found. This application has no code."); + } + + // Calculate addresses and load symbols. + // Note, it is important the bss sections come after the rodata sections. + for sec in rodata_sections + .iter() + .chain(bss_sections.iter()) + .chain(text_sections.iter()) + { + offset = align_by_constraint(offset, MIN_SECTION_ALIGNMENT); + virt_offset = + align_to_offset_by_constraint(virt_offset, offset, md.load_align_constraint as usize); + if verbose { + println!( + "Section, {}, is being put at offset: {:+x}(virt: {:+x})", + sec.name().unwrap(), + offset, + virt_offset + ) + } + section_offset_map.insert(sec.index(), (offset, virt_offset)); + for sym in symbols.iter() { + if sym.section() == SymbolSection::Section(sec.index()) { + let name = sym.name().unwrap_or_default().to_string(); + if !md.roc_symbol_vaddresses.contains_key(&name) { + symbol_vaddr_map.insert(sym.index(), virt_offset + sym.address() as usize); + } + if md.app_functions.contains(&name) { + app_func_vaddr_map.insert(name.clone(), virt_offset + sym.address() as usize); + app_func_size_map.insert(name, sym.size()); + } + } + } + let section_size = match sec.file_range() { + Some((_, size)) => size, + None => 0, + }; + if sec.name().unwrap_or_default().starts_with("__BSS") { + // bss sections only modify the virtual size. + virt_offset += sec.size() as usize; + } else if section_size != sec.size() { + internal_error!( "We do not deal with non bss sections that have different on disk and in memory sizes"); + } else { + offset += section_size as usize; + virt_offset += sec.size() as usize; + } + } + if verbose { + println!("Data Relocation Offsets: {symbol_vaddr_map:+x?}"); + println!("Found App Function Symbols: {app_func_vaddr_map:+x?}"); + } + + // let (new_text_section_offset, new_text_section_vaddr) = text_sections + // .iter() + // .map(|sec| section_offset_map.get(&sec.index()).unwrap()) + // .min() + // .unwrap(); + // let (new_text_section_offset, new_text_section_vaddr) = + // (*new_text_section_offset, *new_text_section_vaddr); + + // Move data and deal with relocations. + for sec in rodata_sections + .iter() + .chain(bss_sections.iter()) + .chain(text_sections.iter()) + { + let data = match sec.data() { + Ok(data) => data, + Err(err) => { + internal_error!( + "Failed to load data for section, {:+x?}: {}", + sec.name().unwrap(), + err + ); + } + }; + let (section_offset, section_virtual_offset) = + section_offset_map.get(&sec.index()).unwrap(); + let (section_offset, section_virtual_offset) = (*section_offset, *section_virtual_offset); + exec_mmap[section_offset..section_offset + data.len()].copy_from_slice(data); + // Deal with definitions and relocations for this section. + if verbose { + println!(); + println!( + "Processing Relocations for Section: 0x{sec:+x?} @ {section_offset:+x} (virt: {section_virtual_offset:+x})" + ); + } + for rel in sec.relocations() { + if verbose { + println!("\tFound Relocation: {rel:+x?}"); + } + match rel.1.target() { + RelocationTarget::Symbol(index) => { + let target_offset = if let Some(target_offset) = symbol_vaddr_map.get(&index) { + if verbose { + println!("\t\tRelocation targets symbol in app at: {target_offset:+x}"); + } + Some(*target_offset as i64) + } else { + app_obj + .symbol_by_index(index) + .and_then(|sym| sym.name()) + .ok() + .and_then(|name| { + md.roc_symbol_vaddresses.get(name).map(|address| { + let vaddr = (*address + md.added_byte_count) as i64; + if verbose { + println!( + "\t\tRelocation targets symbol in host: {name} @ {vaddr:+x}" + ); + } + vaddr + }) + }) + }; + + if let Some(target_offset) = target_offset { + let virt_base = section_virtual_offset + rel.0 as usize; + let base = section_offset + rel.0 as usize; + let target: i64 = match rel.1.kind() { + RelocationKind::Relative | RelocationKind::PltRelative => { + target_offset - virt_base as i64 + rel.1.addend() + } + x => { + internal_error!("Relocation Kind not yet support: {:?}", x); + } + }; + if verbose { + println!( + "\t\tRelocation base location: {base:+x} (virt: {virt_base:+x})" + ); + println!("\t\tFinal relocation target offset: {target:+x}"); + } + match rel.1.size() { + 32 => { + let data = (target as i32).to_le_bytes(); + exec_mmap[base..base + 4].copy_from_slice(&data); + } + 64 => { + let data = target.to_le_bytes(); + exec_mmap[base..base + 8].copy_from_slice(&data); + } + x => { + internal_error!("Relocation size not yet supported: {}", x); + } + } + } else if matches!(app_obj.symbol_by_index(index), Ok(sym) if ["__divti3", "__udivti3", "___divti3", "___udivti3"].contains(&sym.name().unwrap_or_default())) + { + // Explicitly ignore some symbols that are currently always linked. + continue; + } else { + internal_error!( + "Undefined Symbol in relocation, {:+x?}: {:+x?}", + rel, + app_obj.symbol_by_index(index) + ); + } + } + + _ => { + internal_error!("Relocation target not yet support: {:+x?}", rel); + } + } + } + } + + // Flush app only data to speed up write to disk. + exec_mmap + .flush_async_range(md.exec_len as usize, offset - md.exec_len as usize) + .unwrap_or_else(|e| internal_error!("{}", e)); + + // TODO: look into merging symbol tables, debug info, and eh frames to enable better debugger experience. + + // let mut cmd_offset = md.macho_cmd_loc as usize; + + // // Load this section (we made room for it earlier) and then mutate all its data to make it the desired command + // { + // let cmd = + // load_struct_inplace_mut::>(exec_mmap, cmd_offset); + // let size_of_section = mem::size_of::>() as u32; + // let size_of_cmd = mem::size_of_val(cmd); + + // cmd_offset += size_of_cmd; + + // cmd.cmd.set(LE , macho::LC_SEGMENT_64); + // cmd.cmdsize + // .set(LE , size_of_section + size_of_cmd as u32); + // cmd.segname = *b"__DATA_CONST\0\0\0\0"; + // cmd.vmaddr + // .set(LE , new_rodata_section_vaddr as u64); + // cmd.vmsize.set( + // LE , + // (new_text_section_vaddr - new_rodata_section_vaddr) as u64, + // ); + // cmd.fileoff + // .set(LE , new_rodata_section_offset as u64); + // cmd.filesize.set( + // LE , + // (new_text_section_offset - new_rodata_section_offset) as u64, + // ); + // cmd.nsects.set(LE , 1); + // cmd.maxprot.set(LE , 0x00000003); + // cmd.initprot.set(LE , 0x00000003); + + // // TODO set protection + // } + + // { + // let cmd = load_struct_inplace_mut::>(exec_mmap, cmd_offset); + // let size_of_cmd = mem::size_of_val(cmd); + + // cmd_offset += size_of_cmd; + + // cmd.sectname = *b"__const\0\0\0\0\0\0\0\0\0"; + // cmd.segname = *b"__DATA_CONST\0\0\0\0"; + // cmd.addr.set(LE , new_rodata_section_vaddr as u64); + // cmd.size.set( + // LE , + // (new_text_section_offset - new_rodata_section_offset) as u64, + // ); + // cmd.offset.set(LE , 0); // TODO is this offset since the start of the file, or segment offset? + // cmd.align.set(LE , 12); // TODO should this be 4096? + // cmd.reloff.set(LE , 264); // TODO this should NOT be hardcoded! Should get it from somewhere. + // } + + // { + // let cmd = + // load_struct_inplace_mut::>(exec_mmap, cmd_offset); + // let size_of_section = mem::size_of::>() as u32; + // let size_of_cmd = mem::size_of_val(cmd); + + // cmd_offset += size_of_cmd; + + // cmd.cmd.set(LE , macho::LC_SEGMENT_64); + // cmd.cmdsize + // .set(LE , size_of_section + size_of_cmd as u32); + // cmd.segname = *b"__TEXT\0\0\0\0\0\0\0\0\0\0"; + // cmd.vmaddr.set(LE , new_text_section_vaddr as u64); + // cmd.vmsize + // .set(LE , (offset - new_text_section_offset) as u64); + // cmd.fileoff + // .set(LE , new_text_section_offset as u64); + // cmd.filesize + // .set(LE , (offset - new_text_section_offset) as u64); + // cmd.nsects.set(LE , 1); + // cmd.maxprot.set(LE , 0x00000005); // this is what a zig-generated host had + // cmd.initprot.set(LE , 0x00000005); // this is what a zig-generated host had + // } + + // { + // let cmd = load_struct_inplace_mut::>(exec_mmap, cmd_offset); + + // cmd.segname = *b"__TEXT\0\0\0\0\0\0\0\0\0\0"; + // cmd.sectname = *b"__text\0\0\0\0\0\0\0\0\0\0"; + // cmd.addr.set(LE , new_text_section_vaddr as u64); + // cmd.size + // .set(LE , (offset - new_text_section_offset) as u64); + // cmd.offset.set(LE , 0); // TODO is this offset since the start of the file, or segment offset? + // cmd.align.set(LE , 12); // TODO this is 4096 (2^12) - which load_align_constraint does, above - but should it? + // cmd.flags.set(LE , 0x80000400); // TODO this is what a zig-generated host had + // cmd.reloff.set(LE , 264); // TODO this should NOT be hardcoded! Should get it from somewhere. + // } + + // Update calls from platform and dynamic symbols. + // let dynsym_offset = md.dynamic_symbol_table_section_offset + md.added_byte_count; + + for func_name in md.app_functions.iter() { + let func_virt_offset = match app_func_vaddr_map.get(func_name) { + Some(offset) => *offset as u64, + None => { + internal_error!("Function, {}, was not defined by the app", &func_name); + } + }; + if verbose { + println!( + "Updating calls to {} to the address: {:+x}", + &func_name, func_virt_offset + ); + } + + for s in md.surgeries.get(func_name).unwrap_or(&vec![]) { + if verbose { + println!("\tPerforming surgery: {s:+x?}"); + } + let surgery_virt_offset = match s.virtual_offset { + VirtualOffset::Relative(vs) => (vs + md.added_byte_count) as i64, + VirtualOffset::Absolute => 0, + }; + match s.size { + 4 => { + let target = (func_virt_offset as i64 - surgery_virt_offset) as i32; + if verbose { + println!("\tTarget Jump: {target:+x}"); + } + let data = target.to_le_bytes(); + exec_mmap[(s.file_offset + md.added_byte_count) as usize + ..(s.file_offset + md.added_byte_count) as usize + 4] + .copy_from_slice(&data); + } + 8 => { + let target = func_virt_offset as i64 - surgery_virt_offset; + if verbose { + println!("\tTarget Jump: {target:+x}"); + } + let data = target.to_le_bytes(); + exec_mmap[(s.file_offset + md.added_byte_count) as usize + ..(s.file_offset + md.added_byte_count) as usize + 8] + .copy_from_slice(&data); + } + x => { + internal_error!("Surgery size not yet supported: {}", x); + } + } + } + + // Replace plt call code with just a jump. + // This is a backup incase we missed a call to the plt. + if let Some((plt_off, plt_vaddr)) = md.plt_addresses.get(func_name) { + let plt_off = (*plt_off + md.added_byte_count) as usize; + let plt_vaddr = *plt_vaddr + md.added_byte_count; + let jmp_inst_len = 5; + let target = + (func_virt_offset as i64 - (plt_vaddr as i64 + jmp_inst_len as i64)) as i32; + if verbose { + println!("\tPLT: {plt_off:+x}, {plt_vaddr:+x}"); + println!("\tTarget Jump: {target:+x}"); + } + let data = target.to_le_bytes(); + exec_mmap[plt_off] = 0xE9; + exec_mmap[plt_off + 1..plt_off + jmp_inst_len].copy_from_slice(&data); + for i in jmp_inst_len..PLT_ADDRESS_OFFSET as usize { + exec_mmap[plt_off + i] = 0x90; + } + } + + // Commented out because it doesn't apply to mach-o + // if let Some(i) = md.dynamic_symbol_indices.get(func_name) { + // let sym = load_struct_inplace_mut::>( + // exec_mmap, + // dynsym_offset as usize + *i as usize * mem::size_of::>(), + // ); + // sym.st_value.set(LE , func_virt_offset as u64); + // sym.st_size.set( + // LE , + // match app_func_size_map.get(func_name) { + // Some(size) => *size, + // None => { + // internal_error!("Size missing for: {}", func_name); + // } + // }, + // ); + // } + } + + *offset_ref = offset; +} diff --git a/crates/linker/src/pe.rs b/crates/linker/src/pe.rs new file mode 100644 index 0000000000..e7649642e5 --- /dev/null +++ b/crates/linker/src/pe.rs @@ -0,0 +1,1972 @@ +use std::{ + io::{BufReader, BufWriter}, + path::Path, +}; + +use bincode::{deserialize_from, serialize_into}; +use memmap2::MmapMut; +use object::{ + pe::{ + self, ImageBaseRelocation, ImageFileHeader, ImageImportDescriptor, ImageNtHeaders64, + ImageSectionHeader, ImageThunkData64, + }, + read::pe::ImportTable, + LittleEndian as LE, Object, RelocationTarget, SectionIndex, +}; +use serde::{Deserialize, Serialize}; + +use roc_collections::{MutMap, VecMap}; +use roc_error_macros::internal_error; + +use crate::{ + generate_dylib::APP_DLL, load_struct_inplace, load_struct_inplace_mut, + load_structs_inplace_mut, open_mmap, open_mmap_mut, +}; + +/// The metadata stores information about/from the host .exe because +/// +/// - it is faster to retrieve than parsing the host .exe on every link +/// - the information is erased from the host .exe to make linking faster +/// +/// For instance, we remove our dummy .dll from the import table, but its dynamic relocations are +/// still somewhere in the host .exe. We use the metadata to store this offset, so at link time we +/// can make modifications at that location. +/// +/// PeMetadata is created during preprocessing and stored to disk. +#[derive(Debug, Serialize, Deserialize)] +struct PeMetadata { + dynhost_file_size: usize, + + last_host_section_size: u64, + last_host_section_address: u64, + + /// Number of sections in the unmodified host .exe + host_section_count: usize, + + optional_header_offset: usize, + + dynamic_relocations: DynamicRelocationsPe, + + /// File offset for the thunks of our dummy .dll + thunks_start_offset_in_file: usize, + + /// Offset for the thunks of our dummy .dll within the .rdata section + thunks_start_offset_in_section: usize, + + /// Virtual address of the .rdata section + dummy_dll_thunk_section_virtual_address: u32, + + /// The offset into the file of the .reloc section + reloc_offset_in_file: usize, + + reloc_section_index: usize, + + /// Constants from the host .exe header + image_base: u64, + file_alignment: u32, + section_alignment: u32, + + /// Symbols that the host imports, like roc__mainForHost_1_exposed_generic + imports: Vec, + + /// Symbols that the host exports, like roc_alloc + exports: MutMap, +} + +impl PeMetadata { + fn write_to_file(&self, metadata_filename: &Path) { + let metadata_file = + std::fs::File::create(metadata_filename).unwrap_or_else(|e| internal_error!("{}", e)); + + serialize_into(BufWriter::new(metadata_file), self) + .unwrap_or_else(|err| internal_error!("Failed to serialize metadata: {err}")); + } + + fn read_from_file(metadata_filename: &Path) -> Self { + let input = + std::fs::File::open(metadata_filename).unwrap_or_else(|e| internal_error!("{}", e)); + + match deserialize_from(BufReader::new(input)) { + Ok(data) => data, + Err(err) => { + internal_error!("Failed to deserialize metadata: {}", err); + } + } + } + + fn from_preprocessed_host(preprocessed_data: &[u8], new_sections: &[[u8; 8]]) -> Self { + use object::ObjectSection; + + let dynhost_obj = object::read::pe::PeFile64::parse(preprocessed_data) + .unwrap_or_else(|err| internal_error!("Failed to parse executable file: {}", err)); + + let host_section_count = dynhost_obj.sections().count() - new_sections.len(); + let last_host_section = dynhost_obj + .sections() + .nth(host_section_count - 1) // -1 because we have a count and want an index + .unwrap(); + + let dynamic_relocations = DynamicRelocationsPe::new(preprocessed_data); + + let dummy_dll_thunks = find_thunks_start_offset(preprocessed_data, &dynamic_relocations); + let thunks_start_offset_in_file = dummy_dll_thunks.offset_in_file as usize; + + let dummy_dll_thunk_section_virtual_address = + dummy_dll_thunks.section.virtual_address.get(LE); + + let thunks_start_offset_in_section = (dummy_dll_thunks.offset_in_file + - dummy_dll_thunks.section.pointer_to_raw_data.get(LE) as u64) + as usize; + + let (reloc_section_index, reloc_section) = dynhost_obj + .sections() + .enumerate() + .find(|(_, s)| s.name() == Ok(".reloc")) + .unwrap(); + + let reloc_offset_in_file = reloc_section.file_range().unwrap().0 as usize; + + let optional_header = dynhost_obj.nt_headers().optional_header; + let optional_header_offset = dynhost_obj.dos_header().nt_headers_offset() as usize + + std::mem::size_of::() + + std::mem::size_of::(); + + let exports: MutMap = dynhost_obj + .exports() + .unwrap() + .into_iter() + .map(|e| { + ( + String::from_utf8(e.name().to_vec()).unwrap(), + (e.address() - optional_header.image_base.get(LE)) as i64, + ) + }) + .collect(); + + let imports: Vec<_> = dynhost_obj + .imports() + .unwrap() + .iter() + .filter(|import| import.library() == APP_DLL.as_bytes()) + .map(|import| { + std::str::from_utf8(import.name()) + .unwrap_or_default() + .to_owned() + }) + .collect(); + + let last_host_section_size = last_host_section.size(); + let last_host_section_address = last_host_section.address(); + + PeMetadata { + dynhost_file_size: preprocessed_data.len(), + image_base: optional_header.image_base.get(LE), + file_alignment: optional_header.file_alignment.get(LE), + section_alignment: optional_header.section_alignment.get(LE), + last_host_section_size, + last_host_section_address, + host_section_count, + optional_header_offset, + imports, + exports, + dynamic_relocations, + thunks_start_offset_in_file, + thunks_start_offset_in_section, + dummy_dll_thunk_section_virtual_address, + reloc_offset_in_file, + reloc_section_index, + } + } +} + +pub(crate) fn preprocess_windows( + host_exe_filename: &Path, + metadata_filename: &Path, + preprocessed_filename: &Path, + dummy_dll_symbols: &[String], + _verbose: bool, + _time: bool, +) -> object::read::Result<()> { + let data = open_mmap(host_exe_filename); + + let new_sections = [*b".text\0\0\0", *b".rdata\0\0"]; + let mut preprocessed = Preprocessor::preprocess( + preprocessed_filename, + &data, + dummy_dll_symbols.len(), + &new_sections, + ); + + // get the metadata from the preprocessed executable before the destructive operations below + let md = PeMetadata::from_preprocessed_host(&preprocessed, &new_sections); + + // in the data directories, update the length of the imports (there is one fewer now) + { + let start = md.dynamic_relocations.data_directories_offset_in_file as usize + + object::pe::IMAGE_DIRECTORY_ENTRY_IMPORT + * std::mem::size_of::(); + + let dir = load_struct_inplace_mut::(&mut preprocessed, start); + + let new = dir.size.get(LE) - std::mem::size_of::() as u32; + dir.size.set(LE, new); + } + + remove_dummy_dll_import_table_entry(&mut preprocessed, &md); + + md.write_to_file(metadata_filename); + + Ok(()) +} + +fn remove_dummy_dll_import_table_entry(executable: &mut [u8], md: &PeMetadata) { + const W: usize = std::mem::size_of::(); + + let dr = &md.dynamic_relocations; + + // there is one zeroed-out descriptor at the back + let count = dr.import_directory_size as usize / W - 1; + + let descriptors = load_structs_inplace_mut::( + executable, + dr.import_directory_offset_in_file as usize, + count, + ); + + // move the dummy to the final position + descriptors.swap(dr.dummy_import_index as usize, count - 1); + + // make this the new zeroed-out descriptor + if let Some(d) = descriptors.last_mut() { + *d = ImageImportDescriptor { + original_first_thunk: Default::default(), + time_date_stamp: Default::default(), + forwarder_chain: Default::default(), + name: Default::default(), + first_thunk: Default::default(), + } + } +} + +fn relocate_to( + executable: &mut [u8], + file_offset: usize, + destination_in_file: i64, + relocation: &object::Relocation, +) { + match relocation.size() { + 32 => { + let slice = &mut executable[file_offset..][..4]; + let implicit = if relocation.has_implicit_addend() { + i32::from_le_bytes(slice.try_into().unwrap()) + } else { + 0 + }; + + let delta = destination_in_file + relocation.addend() + implicit as i64; + + slice.copy_from_slice(&(delta as i32).to_le_bytes()); + } + + 64 => { + let slice = &mut executable[file_offset..][..8]; + let implicit = if relocation.has_implicit_addend() { + i64::from_le_bytes(slice.try_into().unwrap()) + } else { + 0 + }; + + let delta = destination_in_file + relocation.addend() + implicit; + + slice.copy_from_slice(&delta.to_le_bytes()); + } + + other => unimplemented!("relocations of {other} bits are not supported"), + } +} + +pub(crate) fn surgery_pe(executable_path: &Path, metadata_path: &Path, roc_app_bytes: &[u8]) { + let md = PeMetadata::read_from_file(metadata_path); + + let app_obj_sections = AppSections::from_data(roc_app_bytes); + + let mut symbols = app_obj_sections.roc_symbols; + + let image_base: u64 = md.image_base; + let file_alignment = md.file_alignment as usize; + let section_alignment = md.section_alignment as usize; + + let app_sections_size: usize = app_obj_sections + .sections + .iter() + .map(|s| next_multiple_of(s.bytes.len(), file_alignment)) + .sum(); + + let executable = &mut open_mmap_mut(executable_path, md.dynhost_file_size + app_sections_size); + + let app_code_section_va = md.last_host_section_address + + next_multiple_of(md.last_host_section_size as usize, section_alignment) as u64; + + let mut section_file_offset = md.dynhost_file_size; + let mut section_virtual_address = (app_code_section_va - image_base) as u32; + + // find the location to write the section headers for our new sections + let mut section_header_start = md.dynamic_relocations.section_headers_offset_in_file as usize + + md.host_section_count * std::mem::size_of::(); + + relocate_dummy_dll_entries(executable, &md); + + let mut code_bytes_added = 0; + let mut data_bytes_added = 0; + let mut file_bytes_added = 0; + + // relocations between the sections of the roc application + // (as opposed to relocations for symbols the app imports from the host) + let inter_app_relocations = process_internal_relocations( + &app_obj_sections.sections, + &app_obj_sections.other_symbols, + (app_code_section_va - image_base) as u32, + section_alignment, + ); + + for kind in [SectionKind::Text, SectionKind::ReadOnlyData] { + let length: usize = app_obj_sections + .sections + .iter() + .filter(|s| s.kind == kind) + .map(|s| s.bytes.len()) + .sum(); + + // offset_in_section now becomes a proper virtual address + for symbol in symbols.iter_mut() { + if symbol.section_kind == kind { + symbol.offset_in_section += image_base as usize + section_virtual_address as usize; + } + } + + // NOTE: sections cannot be zero in size! + let virtual_size = u32::max(1, length as u32); + let size_of_raw_data = next_multiple_of(length, file_alignment) as u32; + + match kind { + SectionKind::Text => { + code_bytes_added += size_of_raw_data; + + write_section_header( + executable, + *b".text1\0\0", + pe::IMAGE_SCN_MEM_READ | pe::IMAGE_SCN_CNT_CODE | pe::IMAGE_SCN_MEM_EXECUTE, + section_header_start, + section_file_offset, + virtual_size, + section_virtual_address, + size_of_raw_data, + ); + } + SectionKind::ReadOnlyData => { + data_bytes_added += size_of_raw_data; + + write_section_header( + executable, + *b".rdata1\0", + pe::IMAGE_SCN_MEM_READ | pe::IMAGE_SCN_CNT_INITIALIZED_DATA, + section_header_start, + section_file_offset, + virtual_size, + section_virtual_address, + size_of_raw_data, + ); + } + } + + let mut offset = section_file_offset; + let it = app_obj_sections.sections.iter().filter(|s| s.kind == kind); + for section in it { + let slice = section.bytes; + executable[offset..][..slice.len()].copy_from_slice(slice); + + let it = section + .relocations + .iter() + .flat_map(|(name, rs)| rs.iter().map(move |r| (name, r))); + + for (name, app_relocation) in it { + let AppRelocation { + offset_in_section, + relocation, + address, + } = app_relocation; + + if let Some(destination) = md.exports.get(name) { + match relocation.kind() { + object::RelocationKind::Relative => { + relocate_to( + executable, + offset + *offset_in_section as usize, + destination + - section_virtual_address as i64 + - *offset_in_section as i64, + relocation, + ); + } + _ => todo!(), + } + } else if let Some(destination) = inter_app_relocations.get(name) { + relocate_to( + executable, + offset + *offset_in_section as usize, + destination - section_virtual_address as i64 - *offset_in_section as i64, + relocation, + ); + } else if name == "___chkstk_ms" { + // this is a stack probe that is inserted when a function uses more than 2 + // pages of stack space. The source of this function is not linked in, so we + // have to do it ourselves. We patch in the bytes as a separate section, and + // here just need to jump to those bytes + + // This relies on the ___CHKSTK_MS section being the last text section in the list of sections + let destination = length - ___CHKSTK_MS.len(); + + relocate_to( + executable, + offset + *offset_in_section as usize, + destination as i64 - *offset_in_section as i64, + relocation, + ); + } else { + let is_ingested_compiler_rt = [ + "__muloti4", + "__divti3", + "__udivti3", + "__modti3", + "__umodti3", + "__fixdfti", + "__fixsfti", + "__fixunsdfti", + "__fixunssfti", + "__lshrti3", + "memcpy_decision", + ] + .contains(&name.as_str()); + if *address == 0 && !name.starts_with("roc") && !is_ingested_compiler_rt { + eprintln!( + "I don't know the address of the {name} function! this may cause segfaults" + ); + } + + match relocation.kind() { + object::RelocationKind::Relative => { + relocate_to( + executable, + offset + *offset_in_section as usize, + *address as i64 - *offset_in_section as i64, + relocation, + ); + } + _ => todo!(), + } + } + } + + offset += slice.len(); + } + + section_header_start += std::mem::size_of::(); + section_file_offset += size_of_raw_data as usize; + section_virtual_address += next_multiple_of(length, section_alignment) as u32; + file_bytes_added += next_multiple_of(length, section_alignment) as u32; + } + + update_optional_header( + executable, + md.optional_header_offset, + code_bytes_added, + file_bytes_added, + data_bytes_added, + ); + + let symbols: Vec<_> = symbols + .into_iter() + .map(|s| (s.name, s.offset_in_section as u64)) + .collect(); + + redirect_dummy_dll_functions( + executable, + &symbols, + &md.imports, + md.thunks_start_offset_in_file, + ); +} + +#[derive(Debug, Serialize, Deserialize)] +struct DynamicRelocationsPe { + name_by_virtual_address: MutMap, + + /// Map symbol name to the virtual address and file offset + /// where the actual address of this symbol is stored + address_and_offset: MutMap, + + /// Virtual offset of the first thunk + section_virtual_address: u32, + + /// Offset in the file of the first thunk + section_offset_in_file: u32, + + /// Offset in the file of the imports directory + import_directory_offset_in_file: u32, + + /// Size in the file of the imports directory + import_directory_size: u32, + + /// Offset in the file of the data directories + data_directories_offset_in_file: u32, + + /// The dummy .dll is the `dummy_import_index`th import of the host .exe + dummy_import_index: u32, + + /// Start of the first ImageSectionHeader of the file + section_headers_offset_in_file: u64, +} + +impl DynamicRelocationsPe { + fn new(data: &[u8]) -> Self { + Self::new_help(data).unwrap() + } + + fn find_roc_dummy_dll( + import_table: &ImportTable, + ) -> object::read::Result> { + let mut index = 0; + let mut it = import_table.descriptors()?; + while let Some(descriptor) = it.next()? { + let name = import_table.name(descriptor.name.get(LE))?; + // dbg!(String::from_utf8_lossy(name)); + + if name == APP_DLL.as_bytes() { + return Ok(Some((*descriptor, index))); + } + + index += 1; + } + + Ok(None) + } + + /// Append metadata for the functions (e.g. mainForHost) that the host needs from the app + fn append_roc_imports( + &mut self, + import_table: &ImportTable, + roc_dll_descriptor: &ImageImportDescriptor, + ) -> object::read::Result<()> { + // offset of first thunk from the start of the section + let thunk_start_offset = + roc_dll_descriptor.original_first_thunk.get(LE) - self.section_virtual_address; + let mut thunk_offset = 0; + + let mut thunks = import_table.thunks(roc_dll_descriptor.original_first_thunk.get(LE))?; + while let Some(thunk_data) = thunks.next::()? { + use object::read::pe::ImageThunkData; + + let temporary_address = thunk_data.address(); + let (_, name) = import_table.hint_name(temporary_address as _)?; + let name = String::from_utf8_lossy(name).to_string(); + + let offset_in_file = self.section_offset_in_file + thunk_start_offset + thunk_offset; + let virtual_address = roc_dll_descriptor.original_first_thunk.get(LE) + thunk_offset; + + self.name_by_virtual_address + .insert(virtual_address, name.clone()); + self.address_and_offset + .insert(name, (virtual_address, offset_in_file)); + + thunk_offset += std::mem::size_of::() as u32; + } + + Ok(()) + } + + fn new_help(data: &[u8]) -> object::read::Result { + use object::read::pe::ImageNtHeaders; + + let dos_header = pe::ImageDosHeader::parse(data)?; + let mut offset = dos_header.nt_headers_offset().into(); + let data_directories_offset_in_file = + offset as u32 + std::mem::size_of::() as u32; + + let (nt_headers, data_directories) = ImageNtHeaders64::parse(data, &mut offset)?; + let sections = nt_headers.sections(data, offset)?; + let section_headers_offset_in_file = offset; + + let data_dir = match data_directories.get(pe::IMAGE_DIRECTORY_ENTRY_IMPORT) { + Some(data_dir) => data_dir, + None => internal_error!("No dynamic imports, probably a bug"), + }; + + let import_va = data_dir.virtual_address.get(LE); + + let (section_data, section_va, offset_in_file) = sections + .iter() + .find_map(|section| { + section + .pe_data_containing(data, import_va) + .map(|(section_data, section_va)| { + ( + section_data, + section_va, + section.pointer_to_raw_data.get(LE), + ) + }) + }) + .expect("Invalid import data dir virtual address"); + + let import_table = ImportTable::new(section_data, section_va, import_va); + + let (import_directory_offset_in_file, import_directory_size) = data_dir + .file_range(§ions) + .expect("import directory exists"); + + let (descriptor, dummy_import_index) = Self::find_roc_dummy_dll(&import_table)?.unwrap(); + + let mut this = Self { + name_by_virtual_address: Default::default(), + address_and_offset: Default::default(), + section_virtual_address: section_va, + section_offset_in_file: offset_in_file, + import_directory_offset_in_file, + data_directories_offset_in_file, + dummy_import_index, + section_headers_offset_in_file, + import_directory_size, + }; + + this.append_roc_imports(&import_table, &descriptor)?; + + Ok(this) + } +} + +/// Preprocess the host's .exe to make space for extra sections +/// +/// We will later take code and data sections from the app and concatenate them with this +/// preprocessed host. That means we need to do some bookkeeping: add extra entries to the +/// section table, update the header with the new section count, and (because we added data) +/// update existing section headers to point to a different (shifted) location in the file +struct Preprocessor { + header_offset: u64, + additional_header_space: usize, + additional_reloc_space: usize, + extra_sections_start: usize, + extra_sections_width: usize, + section_count_offset: u64, + section_table_offset: u64, + new_section_count: usize, + old_headers_size: usize, + new_headers_size: usize, + section_alignment: usize, +} + +impl Preprocessor { + const SECTION_HEADER_WIDTH: usize = std::mem::size_of::(); + + fn preprocess( + output_path: &Path, + data: &[u8], + dummy_dll_symbols: usize, + extra_sections: &[[u8; 8]], + ) -> MmapMut { + let this = Self::new(data, dummy_dll_symbols, extra_sections); + let mut result = open_mmap_mut( + output_path, + data.len() + this.additional_header_space + this.additional_reloc_space, + ); + + this.copy(&mut result, data); + this.fix(&mut result, extra_sections); + + result + } + + fn new(data: &[u8], dummy_dll_symbols: usize, extra_sections: &[[u8; 8]]) -> Self { + use object::read::pe::ImageNtHeaders; + + let dos_header = pe::ImageDosHeader::parse(data).unwrap_or_else(|e| internal_error!("{e}")); + let mut offset = dos_header.nt_headers_offset().into(); + let header_offset = offset; + let (nt_headers, _data_directories) = + ImageNtHeaders64::parse(data, &mut offset).unwrap_or_else(|e| internal_error!("{e}")); + let section_table_offset = offset; + let sections = nt_headers + .sections(data, offset) + .unwrap_or_else(|e| internal_error!("{e}")); + + // recalculate the size of the headers. The size of the headers must be rounded up to the + // next multiple of `file_alignment`. + let old_headers_size = nt_headers.optional_header.size_of_headers.get(LE) as usize; + let file_alignment = nt_headers.optional_header.file_alignment.get(LE) as usize; + let section_alignment = nt_headers.optional_header.section_alignment.get(LE) as usize; + let extra_sections_width = extra_sections.len() * Self::SECTION_HEADER_WIDTH; + + // in a better world `extra_sections_width.div_ceil(file_alignment)` would be stable + // check on https://github.com/rust-lang/rust/issues/88581 some time in the future + let extra_alignments = div_ceil(extra_sections_width, file_alignment); + let new_headers_size = old_headers_size + extra_alignments * file_alignment; + + let additional_header_space = new_headers_size - old_headers_size; + let additional_reloc_space = + Self::additional_reloc_space(data, dummy_dll_symbols, file_alignment); + + Self { + extra_sections_start: section_table_offset as usize + + sections.len() * Self::SECTION_HEADER_WIDTH, + extra_sections_width, + additional_header_space, + additional_reloc_space, + header_offset, + // the section count is stored 6 bytes into the header + section_count_offset: header_offset + 4 + 2, + section_table_offset, + new_section_count: sections.len() + extra_sections.len(), + old_headers_size, + new_headers_size, + section_alignment, + } + } + + fn additional_reloc_space(data: &[u8], extra_symbols: usize, file_alignment: usize) -> usize { + let file = object::read::pe::PeFile64::parse(data).unwrap(); + + let reloc_section = file + .section_table() + .iter() + .find(|h| h.name == *b".reloc\0\0") + .unwrap_or_else(|| internal_error!("host binary does not have a .reloc section")); + + // we use the virtual size here because it is more granular; the `size_of_raw_data` is + // rounded up to the file alignment + let available_space = + reloc_section.size_of_raw_data.get(LE) - reloc_section.virtual_size.get(LE); + + // worst case, each relocation needs its own header + let worst_case = std::mem::size_of::() + std::mem::size_of::(); + + if available_space < (extra_symbols * worst_case) as u32 { + // resize that section + let new_size = next_multiple_of( + reloc_section.virtual_size.get(LE) as usize + (extra_symbols * worst_case), + file_alignment, + ); + + let delta = new_size - reloc_section.size_of_raw_data.get(LE) as usize; + debug_assert_eq!(delta % file_alignment, 0); + + delta + } else { + 0 + } + } + + fn copy(&self, result: &mut MmapMut, data: &[u8]) { + let extra_sections_start = self.extra_sections_start; + + // copy the headers up to and including the current section table entries + // (but omitting the terminating NULL section table entry) + result[..extra_sections_start].copy_from_slice(&data[..extra_sections_start]); + + // update the header with the new number of sections. From what I can gather, this number of + // sections is usually ignored, and section table entry of all NULL fields is used to know when + // the section table ends. But the rust `object` crate does use this number, and it seems like + // a good idea to keep the header data accurate. + result[self.section_count_offset as usize..][..2] + .copy_from_slice(&(self.new_section_count as u16).to_le_bytes()); + + // copy the rest of the headers + let rest_of_headers = self.old_headers_size - extra_sections_start; + result[extra_sections_start + self.extra_sections_width..][..rest_of_headers] + .copy_from_slice(&data[extra_sections_start..][..rest_of_headers]); + + // copy all of the actual (post-header) data + let source = &data[self.old_headers_size..]; + result[self.new_headers_size..][..source.len()].copy_from_slice(source); + } + + fn write_dummy_sections(&self, result: &mut MmapMut, extra_section_names: &[[u8; 8]]) { + const W: usize = std::mem::size_of::(); + + // only correct for the first section, but that is OK because it's overwritten later + // anyway. But, this value may be used to check whether a previous section overruns into + // the app sections. + let pointer_to_raw_data = result.len() - self.additional_header_space; + + let previous_section_header = + load_struct_inplace::(result, self.extra_sections_start - W); + + let previous_section_header_end = previous_section_header.virtual_address.get(LE) + + previous_section_header.virtual_size.get(LE); + + let mut next_virtual_address = + next_multiple_of(previous_section_header_end as usize, self.section_alignment); + + let extra_section_headers = load_structs_inplace_mut::( + result, + self.extra_sections_start, + extra_section_names.len(), + ); + + for (header, name) in extra_section_headers.iter_mut().zip(extra_section_names) { + *header = ImageSectionHeader { + name: *name, + // NOTE: the virtual_size CANNOT BE ZERO! the binary is invalid if a section has + // zero virtual size. Setting it to 1 works, (because this one byte is not backed + // up by space on disk, the loader will zero the memory if you run the executable) + virtual_size: object::U32::new(LE, 1), + // NOTE: this must be a valid virtual address, using 0 is invalid! + virtual_address: object::U32::new(LE, next_virtual_address as u32), + size_of_raw_data: Default::default(), + pointer_to_raw_data: object::U32::new(LE, pointer_to_raw_data as u32), + pointer_to_relocations: Default::default(), + pointer_to_linenumbers: Default::default(), + number_of_relocations: Default::default(), + number_of_linenumbers: Default::default(), + characteristics: Default::default(), + }; + + next_virtual_address += self.section_alignment; + } + } + + fn fix(&self, result: &mut MmapMut, extra_sections: &[[u8; 8]]) { + self.write_dummy_sections(result, extra_sections); + + // update the size of the headers + // + // Time/Date Wed Sep 14 16:04:36 2022 + // Magic 020b (PE32+) + // MajorLinkerVersion 14 + // MinorLinkerVersion 0 + // ... + // SizeOfImage 00067000 + // SizeOfHeaders 00000400 + // ... + // + // we added extra bytes to the headers, so this value must be updated + let nt_headers = + load_struct_inplace_mut::(result, self.header_offset as _); + nt_headers + .optional_header + .size_of_headers + .set(LE, self.new_headers_size as u32); + + // adding new sections increased the size of the image. We update this value so the + // preprocessedhost is, in theory, runnable. In practice for roc programs it will crash + // because there are missing symbols (those that the app should provide), but for testing + // being able to run the preprocessedhost is nice. + nt_headers.optional_header.size_of_image.set( + LE, + nt_headers.optional_header.size_of_image.get(LE) + + (self.section_alignment * extra_sections.len()) as u32, + ); + + // update the section file offsets + // + // Sections: + // Idx Name Size VMA LMA File off Algn + // 0 .text 00054d96 0000000140001000 0000000140001000 00000400 2**4 + // CONTENTS, ALLOC, LOAD, READONLY, CODE + // 1 .rdata 00008630 0000000140056000 0000000140056000 00055200 2**4 + // CONTENTS, ALLOC, LOAD, READONLY, DATA + // 2 .data 00000200 000000014005f000 000000014005f000 0005da00 2**4 + // CONTENTS, ALLOC, LOAD, DATA + // 3 .pdata 0000228c 0000000140061000 0000000140061000 0005dc00 2**2 + // + // The file offset of the sections has changed (we inserted some bytes) and this value must + // now be updated to point to the correct place in the file + let shift = self.new_headers_size - self.old_headers_size; + let mut offset = self.section_table_offset as usize; + + let headers = load_structs_inplace_mut::( + result, + offset, + self.new_section_count, + ); + + let mut it = headers.iter_mut().peekable(); + while let Some(header) = it.next() { + let old = header.pointer_to_raw_data.get(LE); + header.pointer_to_raw_data.set(LE, old + shift as u32); + + if header.name == *b".reloc\0\0" { + // assumes `.reloc` is the final section + debug_assert_eq!(it.peek().map(|s| s.name).as_ref(), extra_sections.first()); + + let old = header.size_of_raw_data.get(LE); + let new = old + self.additional_reloc_space as u32; + header.size_of_raw_data.set(LE, new); + } + + offset += std::mem::size_of::(); + } + } +} +struct DummyDllThunks<'a> { + section: &'a object::pe::ImageSectionHeader, + offset_in_file: u64, +} + +/// Find the place in the executable where the thunks for our dummy .dll are stored +fn find_thunks_start_offset<'a>( + executable: &'a [u8], + dynamic_relocations: &DynamicRelocationsPe, +) -> DummyDllThunks<'a> { + // The host executable contains indirect calls to functions that the host should provide + // + // 14000105d: e8 8e 27 00 00 call 0x1400037f0 + // 140001062: 89 c3 mov ebx,eax + // 140001064: b1 21 mov cl,0x21 + // + // This `call` is an indirect call (see https://www.felixcloutier.com/x86/call, our call starts + // with 0xe8, so it is relative) In other words, at runtime the program will go to 0x1400037f0, + // read a pointer-sized value there, and jump to that location. + // + // The 0x1400037f0 virtual address is located in the .rdata section + // + // Sections: + // Idx Name Size VMA LMA File off Algn + // 0 .text 0000169a 0000000140001000 0000000140001000 00000600 2**4 + // CONTENTS, ALLOC, LOAD, READONLY, CODE + // 1 .rdata 00000ba8 0000000140003000 0000000140003000 00001e00 2**4 + // CONTENTS, ALLOC, LOAD, READONLY, DATA + // + // Our task is then to put a valid function pointer at 0x1400037f0. The value should be a + // virtual address pointing at the start of a function (which must be located in an executable + // section) + // + // sources: + // + // - https://learn.microsoft.com/en-us/archive/msdn-magazine/2002/february/inside-windows-win32-portable-executable-file-format-in-detail + const W: usize = std::mem::size_of::(); + + let dummy_import_desc_start = dynamic_relocations.import_directory_offset_in_file as usize + + W * dynamic_relocations.dummy_import_index as usize; + + let dummy_import_desc = + load_struct_inplace::(executable, dummy_import_desc_start); + + // per https://en.wikibooks.org/wiki/X86_Disassembly/Windows_Executable_Files#The_Import_directory, + // + // > Once an imported value has been resolved, the pointer to it is stored in the FirstThunk array. + // > It can then be used at runtime to address imported values. + // + // There is also an OriginalFirstThunk array, which we don't use. + // + // The `dummy_thunks_address` is the 0x1400037f0 of our example. In and of itself that is no good though, + // this is a virtual address, we need a file offset + let dummy_thunks_address_va = dummy_import_desc.first_thunk; + let dummy_thunks_address = dummy_thunks_address_va.get(LE); + + let dos_header = object::pe::ImageDosHeader::parse(executable).unwrap(); + let mut offset = dos_header.nt_headers_offset().into(); + + use object::read::pe::ImageNtHeaders; + let (nt_headers, _data_directories) = ImageNtHeaders64::parse(executable, &mut offset).unwrap(); + let sections = nt_headers.sections(executable, offset).unwrap(); + + // find the section the virtual address is in + let (section_virtual_address, section) = sections + .iter() + .find_map(|section| { + section + .pe_data_containing(executable, dummy_thunks_address) + .map(|(_section_data, section_va)| (section_va, section)) + }) + .expect("Invalid thunk virtual address"); + + DummyDllThunks { + section, + // and get the offset in the file of 0x1400037f0 + offset_in_file: dummy_thunks_address as u64 - section_virtual_address as u64 + + section.pointer_to_raw_data.get(LE) as u64, + } +} + +/// Make the thunks point to our actual roc application functions +fn redirect_dummy_dll_functions( + executable: &mut [u8], + function_definition_vas: &[(String, u64)], + imports: &[String], + thunks_start_offset: usize, +) { + // it could be that a symbol exposed by the app is not used by the host. We must skip unused symbols + // this is an O(n^2) loop, hopefully that does not become a problem. If it does we can sort + // both vectors to get linear complexity in the loop. + 'outer: for (i, host_name) in imports.iter().enumerate() { + for (roc_app_target_name, roc_app_target_va) in function_definition_vas { + if host_name == roc_app_target_name { + // addresses are 64-bit values + let address_bytes = &mut executable[thunks_start_offset + i * 8..][..8]; + + // the array of addresses is null-terminated; make sure we haven't reached the end + let current = u64::from_le_bytes(address_bytes.try_into().unwrap()); + if current == 0 { + internal_error!("invalid state: fewer thunks than function addresses"); + } + + // update the address to a function VA + address_bytes.copy_from_slice(&roc_app_target_va.to_le_bytes()); + + continue 'outer; + } + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq)] +#[repr(u8)] +enum SectionKind { + Text, + // Data, + ReadOnlyData, +} + +#[derive(Debug)] +struct AppRelocation { + offset_in_section: u64, + address: u64, + relocation: object::Relocation, +} + +#[derive(Debug)] +struct Section<'a> { + bytes: &'a [u8], + kind: SectionKind, + relocations: MutMap>, + app_section_index: SectionIndex, +} + +#[derive(Debug)] +struct AppSymbol { + name: String, + section_kind: SectionKind, + offset_in_section: usize, +} + +#[derive(Debug, Default)] +struct AppSections<'a> { + sections: Vec>, + roc_symbols: Vec, + other_symbols: Vec<(SectionIndex, AppSymbol)>, +} + +/// Process relocations between two places within the app. This a bit different from doing a +/// relocation of a symbol that will be "imported" from the host +fn process_internal_relocations( + sections: &[Section], + other_symbols: &[(SectionIndex, AppSymbol)], + first_host_section_virtual_address: u32, + section_alignment: usize, +) -> VecMap { + let mut result = VecMap::default(); + let mut host_section_virtual_address = first_host_section_virtual_address; + + for kind in [SectionKind::Text, SectionKind::ReadOnlyData] { + let it = sections.iter().filter(|s| s.kind == kind); + for section in it { + for (s_index, app_symbol) in other_symbols.iter() { + if *s_index == section.app_section_index { + result.insert( + app_symbol.name.clone(), + app_symbol.offset_in_section as i64 + host_section_virtual_address as i64, + ); + } + } + } + + let length: usize = sections + .iter() + .filter(|s| s.kind == kind) + .map(|s| s.bytes.len()) + .sum(); + + host_section_virtual_address += next_multiple_of(length, section_alignment) as u32; + } + + result +} + +impl<'a> AppSections<'a> { + fn from_data(data: &'a [u8]) -> Self { + use object::ObjectSection; + + let file = object::File::parse(data).unwrap(); + + let mut sections = Vec::new(); + + let mut section_starts = MutMap::default(); + + let mut text_bytes = 0; + let mut rdata_bytes = 0; + + for (i, section) in file.sections().enumerate() { + let kind = match section.name() { + Ok(name) => { + match name { + _ if name.starts_with(".text") => SectionKind::Text, + // _ if name.starts_with(".data") => SectionKind::Data, + _ if name.starts_with(".rdata") => SectionKind::ReadOnlyData, + _ => continue, + } + } + Err(_) => continue, + }; + + let mut relocations: MutMap> = MutMap::default(); + + for (offset_in_section, relocation) in section.relocations() { + match relocation.target() { + RelocationTarget::Symbol(symbol_index) => { + use object::ObjectSymbol; + + let symbol = file.symbol_by_index(symbol_index); + + let address = symbol.as_ref().map(|s| s.address()).unwrap_or_default(); + let name = symbol.and_then(|s| s.name()).unwrap_or_default(); + let name = redirect_libc_functions(name).unwrap_or(name).to_string(); + + relocations.entry(name).or_default().push(AppRelocation { + offset_in_section, + address, + relocation, + }); + } + _ => todo!(), + } + } + + let (start, length) = section.file_range().unwrap(); + let file_range = &data[start as usize..][..length as usize]; + + // sections are one-indexed... + let index = SectionIndex(i + 1); + + match kind { + SectionKind::Text => { + section_starts.insert(index, (kind, text_bytes)); + text_bytes += length; + } + SectionKind::ReadOnlyData => { + section_starts.insert(index, (kind, rdata_bytes)); + rdata_bytes += length; + } + } + + let section = Section { + app_section_index: index, + bytes: file_range, + kind, + relocations, + }; + + sections.push(section); + } + + // add a fake section that contains code for a stack probe that some app functions need + let stack_check_section = Section { + bytes: &___CHKSTK_MS, + kind: SectionKind::Text, + relocations: Default::default(), + app_section_index: object::SectionIndex(0), + }; + + sections.push(stack_check_section); + + let mut roc_symbols = Vec::new(); + let mut other_symbols = Vec::new(); + + for symbol in file.symbols() { + use object::ObjectSymbol; + + if symbol.name_bytes().unwrap_or_default().starts_with(b"roc") { + if let object::SymbolSection::Section(index) = symbol.section() { + let (kind, offset_in_host_section) = section_starts[&index]; + + let symbol = AppSymbol { + name: symbol.name().unwrap_or_default().to_string(), + section_kind: kind, + offset_in_section: (offset_in_host_section + symbol.address()) as usize, + }; + + roc_symbols.push(symbol); + } + } else if let object::SymbolSection::Section(index) = symbol.section() { + if let Some((kind, offset_in_host_section)) = section_starts.get(&index) { + let symbol = AppSymbol { + name: symbol.name().unwrap_or_default().to_string(), + section_kind: *kind, + offset_in_section: (offset_in_host_section + symbol.address()) as usize, + }; + + other_symbols.push((index, symbol)); + } + } + } + + AppSections { + sections, + roc_symbols, + other_symbols, + } + } +} + +// check on https://github.com/rust-lang/rust/issues/88581 some time in the future +pub const fn next_multiple_of(lhs: usize, rhs: usize) -> usize { + match lhs % rhs { + 0 => lhs, + r => lhs + (rhs - r), + } +} + +pub const fn div_ceil(lhs: usize, rhs: usize) -> usize { + let d = lhs / rhs; + let r = lhs % rhs; + if r > 0 && rhs > 0 { + d + 1 + } else { + d + } +} + +#[derive(Debug)] +#[repr(C)] +struct HeaderMetadata { + image_base: u64, + file_alignment: usize, + section_alignment: usize, +} + +/// NOTE: the caller must make sure the `*_added` values are rounded +/// up to the section and file alignment respectively +fn update_optional_header( + data: &mut [u8], + optional_header_offset: usize, + code_bytes_added: u32, + file_bytes_added: u32, + data_bytes_added: u32, +) { + use object::pe::ImageOptionalHeader64; + + let optional_header = + load_struct_inplace_mut::(data, optional_header_offset); + + optional_header + .size_of_code + .set(LE, optional_header.size_of_code.get(LE) + code_bytes_added); + + optional_header + .size_of_image + .set(LE, optional_header.size_of_image.get(LE) + file_bytes_added); + + optional_header.size_of_initialized_data.set( + LE, + optional_header.size_of_initialized_data.get(LE) + data_bytes_added, + ); +} + +#[allow(clippy::too_many_arguments)] +fn write_section_header( + data: &mut [u8], + name: [u8; 8], + characteristics: u32, + section_header_start: usize, + section_file_offset: usize, + virtual_size: u32, + virtual_address: u32, + size_of_raw_data: u32, +) { + use object::U32; + + let header = ImageSectionHeader { + name, + virtual_size: U32::new(LE, virtual_size), + virtual_address: U32::new(LE, virtual_address), + size_of_raw_data: U32::new(LE, size_of_raw_data), + pointer_to_raw_data: U32::new(LE, section_file_offset as u32), + pointer_to_relocations: Default::default(), + pointer_to_linenumbers: Default::default(), + number_of_relocations: Default::default(), + number_of_linenumbers: Default::default(), + characteristics: U32::new(LE, characteristics), + }; + + let header_array: [u8; std::mem::size_of::()] = + unsafe { std::mem::transmute(header) }; + + data[section_header_start..][..header_array.len()].copy_from_slice(&header_array); +} + +fn relocate_dummy_dll_entries(executable: &mut [u8], md: &PeMetadata) { + // in the data directories, update the length of the base relocations + let dir = load_struct_inplace_mut::( + executable, + md.dynamic_relocations.data_directories_offset_in_file as usize + + object::pe::IMAGE_DIRECTORY_ENTRY_BASERELOC + * std::mem::size_of::(), + ); + + // for unclear reasons, we must bump the image directory size here. + // we also need some zeroed-out memory at the end, so if the directory + // ends at a multiple of `file_alignment`, pick the next one. + let new_reloc_directory_size = + next_multiple_of(dir.size.get(LE) as usize + 4, md.file_alignment as usize); + dir.size.set(LE, new_reloc_directory_size as u32); +} + +/// Redirect `memcpy` and similar libc functions to their roc equivalents +pub(crate) fn redirect_libc_functions(name: &str) -> Option<&str> { + match name { + "memset" => Some("roc_memset"), + "memmove" => Some("roc_memmove"), + _ => None, + } +} + +// 0000000000000000 <.text>: +// 0: 51 push rcx +// 1: 50 push rax +// 2: 48 3d 00 10 00 00 cmp rax,0x1000 +// 8: 48 8d 4c 24 18 lea rcx,[rsp+0x18] +// d: 72 18 jb 27 <.text+0x27> +// f: 48 81 e9 00 10 00 00 sub rcx,0x1000 +// 16: 48 85 09 test QWORD PTR [rcx],rcx +// 19: 48 2d 00 10 00 00 sub rax,0x1000 +// 1f: 48 3d 00 10 00 00 cmp rax,0x1000 +// 25: 77 e8 ja f <.text+0xf> +// 27: 48 29 c1 sub rcx,rax +// 2a: 48 85 09 test QWORD PTR [rcx],rcx +// 2d: 58 pop rax +// 2e: 59 pop rcx +// 2f: c3 ret +const ___CHKSTK_MS: [u8; 48] = [ + 0x51, // push rcx + 0x50, // push rax + 0x48, 0x3d, 0x00, 0x10, 0x00, 0x00, // cmp rax,0x0x1000 + 0x48, 0x8d, 0x4c, 0x24, 0x18, // lea rcx,0x[rsp+0x18] + 0x72, 0x18, // jb 0x27 + 0x48, 0x81, 0xe9, 0x00, 0x10, 0x00, 0x00, // sub rcx,0x0x1000 + 0x48, 0x85, 0x09, // test QWORD PTR [rcx],0xrcx + 0x48, 0x2d, 0x00, 0x10, 0x00, 0x00, // sub rax,0x0x1000 + 0x48, 0x3d, 0x00, 0x10, 0x00, 0x00, // cmp rax,0x0x1000 + 0x77, 0xe8, // ja 0xf + 0x48, 0x29, 0xc1, // sub rcx,0xrax + 0x48, 0x85, 0x09, // test QWORD PTR [rcx],rcx + 0x58, // pop rax + 0x59, // pop rcx + 0xc3, // ret +]; + +#[cfg(test)] +mod test { + const PE_DYNHOST: &[u8] = include_bytes!("../dynhost_benchmarks_windows.exe") as &[_]; + + use object::read::pe::PeFile64; + use object::{pe, LittleEndian as LE, Object}; + + use crate::preprocessed_host_filename; + use indoc::indoc; + use serial_test::serial; + use target_lexicon::Triple; + + use super::*; + + #[test] + fn dynamic_relocations() { + use object::LittleEndian as LE; + + // find the location in the virtual address space and the on-disk file where dynamic + // relocations are stored. For the example dynhost, objdump shows + + // Sections: + // Idx Name Size VMA LMA File off Algn + // 0 .text 0007bb26 0000000140001000 0000000140001000 00000400 2**4 + // CONTENTS, ALLOC, LOAD, READONLY, CODE + // 1 .rdata 000369d4 000000014007d000 000000014007d000 0007c000 2**4 + // + + // we parse that into this structure. Note how the VMA and File off correspond to the + // values that we find. Also note how the delta between virtual address and offset is the + // same for values of `address_and_offset` and the section values that we find + // + // DynamicRelocationsPe { + // name_by_virtual_address: { + // 0xaf4e0: "roc__mainForHost_size", + // 0xaf4c8: "roc__mainForHost_1__Fx_caller", + // 0xaf4d0: "roc__mainForHost_1__Fx_result_size", + // 0xaf4d8: "roc__mainForHost_1_exposed_generic", + // }, + // address_and_offset: { + // "roc__mainForHost_1__Fx_result_size": ( + // 0xaf4d0, + // 0xae4d0, + // ), + // "roc__mainForHost_1__Fx_caller": ( + // 0xaf4c8, + // 0xae4c8, + // ), + // "roc__mainForHost_1_exposed_generic": ( + // 0xaf4d8, + // 0xae4d8, + // ), + // "roc__mainForHost_size": ( + // 0xaf4e0, + // 0xae4e0, + // ), + // }, + // section_virtual_address: 0x7d000, + // section_offset_in_file: 0x7c000, + // } + + let dynamic_relocations = DynamicRelocationsPe::new(PE_DYNHOST); + // println!("{:#x?}", dynamic_relocations); + + let file = PeFile64::parse(PE_DYNHOST).unwrap(); + let import_table = file.import_table().unwrap().unwrap(); + + // let mut name_by_virtual_address: Vec<_> = dynamic_relocations + // .name_by_virtual_address + // .into_iter() + // .collect(); + // + // + // name_by_virtual_address.sort_unstable(); + + let mut address_and_offset: Vec<_> = + dynamic_relocations.address_and_offset.into_iter().collect(); + + address_and_offset.sort_unstable(); + + // get the relocations through the API + let addresses_api = { + let (descriptor, _) = DynamicRelocationsPe::find_roc_dummy_dll(&import_table) + .unwrap() + .unwrap(); + + let mut addresses = Vec::new(); + + let mut thunks = import_table + .thunks(descriptor.original_first_thunk.get(LE)) + .unwrap(); + while let Some(thunk_data) = thunks.next::().unwrap() { + use object::read::pe::ImageThunkData; + + let temporary_address = thunk_data.address(); + + let (_, name) = import_table.hint_name(temporary_address as _).unwrap(); + let name = String::from_utf8_lossy(name).to_string(); + + addresses.push((name, temporary_address)); + } + + addresses.sort_unstable(); + + addresses + }; + + // get the relocations through using our offsets into the file + let addresses_file: Vec<_> = address_and_offset + .iter() + .map(|(name, (_, offset))| { + ( + name.to_string(), + u32::from_le_bytes(PE_DYNHOST[*offset as usize..][..4].try_into().unwrap()), + ) + }) + .collect(); + + // we want our file offset approach to equal the API + assert_eq!(addresses_api, addresses_file); + } + + #[test] + fn collect_undefined_symbols_pe() { + let object = object::File::parse(PE_DYNHOST).unwrap(); + + let imports: Vec<_> = object + .imports() + .unwrap() + .iter() + .filter(|import| import.library() == APP_DLL.as_bytes()) + .map(|import| String::from_utf8_lossy(import.name())) + .collect(); + + assert_eq!( + [ + "roc__mainForHost_1__Fx_caller", + "roc__mainForHost_1__Fx_result_size", + "roc__mainForHost_1_exposed_generic", + "roc__mainForHost_size" + ], + imports.as_slice(), + ) + } + + fn remove_dummy_dll_import_table_test( + data: &mut [u8], + data_directories_offset_in_file: u32, + imports_offset_in_file: u32, + dummy_import_index: u32, + ) { + const W: usize = std::mem::size_of::(); + + // clear out the import table entry. we do implicitly assume that our dummy .dll is the last + let start = imports_offset_in_file as usize + W * dummy_import_index as usize; + for b in data[start..][..W].iter_mut() { + *b = 0; + } + + // in the data directories, update the length of the imports (there is one fewer now) + let dir = load_struct_inplace_mut::( + data, + data_directories_offset_in_file as usize + + object::pe::IMAGE_DIRECTORY_ENTRY_IMPORT + * std::mem::size_of::(), + ); + + let current = dir.size.get(LE); + dir.size.set( + LE, + current - std::mem::size_of::() as u32, + ); + } + + #[test] + fn remove_dummy_dll_import() { + let object = PeFile64::parse(PE_DYNHOST).unwrap(); + let before = object + .data_directories() + .get(pe::IMAGE_DIRECTORY_ENTRY_IMPORT) + .unwrap() + .size + .get(LE); + + let dynamic_relocations = DynamicRelocationsPe::new(PE_DYNHOST); + let mut data = PE_DYNHOST.to_vec(); + + remove_dummy_dll_import_table_test( + &mut data, + dynamic_relocations.data_directories_offset_in_file, + dynamic_relocations.import_directory_offset_in_file, + dynamic_relocations.dummy_import_index, + ); + + // parse and check that it's really gone + let object = PeFile64::parse(data.as_slice()).unwrap(); + + let after = object + .data_directories() + .get(pe::IMAGE_DIRECTORY_ENTRY_IMPORT) + .unwrap() + .size + .get(LE); + + assert_eq!( + before - after, + std::mem::size_of::() as u32 + ); + + assert_eq!( + object + .imports() + .unwrap() + .iter() + .filter(|import| import.library() == APP_DLL.as_bytes()) + .count(), + 0 + ); + } + + #[test] + fn find_number_of_sections() { + use object::pe; + + let dos_header = pe::ImageDosHeader::parse(PE_DYNHOST).unwrap(); + let offset = dos_header.nt_headers_offset(); + + let number_of_sections_offset = offset + 4 + 2; + + let actual = u16::from_le_bytes( + PE_DYNHOST[number_of_sections_offset as usize..][..2] + .try_into() + .unwrap(), + ); + + assert_eq!(actual, 7) + } + + fn increase_number_of_sections_help( + input_data: &[u8], + new_sections: &[[u8; 8]], + output_file: &Path, + ) -> MmapMut { + use object::read::pe::ImageNtHeaders; + + let dos_header = object::pe::ImageDosHeader::parse(input_data).unwrap(); + let mut offset = dos_header.nt_headers_offset().into(); + + let image_headers_old = load_struct_inplace::( + input_data, + dos_header.nt_headers_offset() as usize, + ); + + let sections_len_old = image_headers_old.file_header().number_of_sections.get(LE); + + let mmap = Preprocessor::preprocess(output_file, input_data, 0, new_sections); + + let image_headers_new = + load_struct_inplace::(&mmap, dos_header.nt_headers_offset() as usize); + + let sections_len_new = image_headers_new.file_header().number_of_sections.get(LE); + + assert_eq!( + sections_len_old + new_sections.len() as u16, + sections_len_new + ); + + let (nt_headers, _data_directories) = ImageNtHeaders64::parse(&*mmap, &mut offset).unwrap(); + let sections = nt_headers.sections(&*mmap, offset).unwrap(); + + let names: Vec<_> = sections + .iter() + .map(|h| h.name) + .skip(sections_len_old as usize) + .take(new_sections.len()) + .collect(); + + assert_eq!(new_sections, names.as_slice()); + + mmap + } + + #[test] + fn increase_number_of_sections() { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("test.exe"); + + let new_sections = [*b"placehol", *b"aaaabbbb"]; + + increase_number_of_sections_help(PE_DYNHOST, &new_sections, &path); + } + + fn zig_host_app(dir: &Path, host_zig: &str, app_zig: &str) { + let zig = std::env::var("ROC_ZIG").unwrap_or_else(|_| "zig".into()); + + std::fs::write(dir.join("host.zig"), host_zig.as_bytes()).unwrap(); + std::fs::write(dir.join("app.zig"), app_zig.as_bytes()).unwrap(); + + // we need to compile the app first + let output = std::process::Command::new(&zig) + .current_dir(dir) + .args([ + "build-obj", + "app.zig", + "-target", + "x86_64-windows-gnu", + "-fstrip", + "-rdynamic", + "-OReleaseFast", + ]) + .output() + .unwrap(); + + if !output.status.success() { + use std::io::Write; + + std::io::stdout().write_all(&output.stdout).unwrap(); + std::io::stderr().write_all(&output.stderr).unwrap(); + + panic!("zig build-obj failed"); + } + + // open our app object; we'll copy sections from it later + let file = std::fs::File::open(dir.join("app.obj")).unwrap(); + let roc_app = unsafe { memmap2::Mmap::map(&file) }.unwrap(); + + let roc_app_sections = AppSections::from_data(&roc_app); + let symbols = roc_app_sections.roc_symbols; + + // make the dummy dylib based on the app object + let names: Vec<_> = symbols.iter().map(|s| s.name.clone()).collect(); + let dylib_bytes = crate::generate_dylib::synthetic_dll(&names); + std::fs::write(dir.join("libapp.dll"), dylib_bytes).unwrap(); + + // now we can compile the host (it uses libapp.dll, hence the order here) + let mut command = std::process::Command::new(&zig); + command.current_dir(dir).args([ + "build-exe", + "libapp.dll", + "host.zig", + "-lc", + "-target", + "x86_64-windows-gnu", + "-rdynamic", + "-fstrip", + "-rdynamic", + "-OReleaseFast", + ]); + + let command_str = format!("{:?}", &command); + let output = command.output().unwrap(); + + if !output.status.success() { + use std::io::Write; + + std::io::stdout().write_all(&output.stdout).unwrap(); + std::io::stderr().write_all(&output.stderr).unwrap(); + + panic!("zig build-exe failed: {command_str}"); + } + + let preprocessed_host_filename = + dir.join(preprocessed_host_filename(&Triple::host()).unwrap()); + + preprocess_windows( + &dir.join("host.exe"), + &dir.join("metadata"), + &preprocessed_host_filename, + &names, + false, + false, + ) + .unwrap(); + + std::fs::copy(&preprocessed_host_filename, dir.join("app.exe")).unwrap(); + + surgery_pe(&dir.join("app.exe"), &dir.join("metadata"), &roc_app); + } + + #[allow(dead_code)] + fn windows_test(runner: F) -> String + where + F: Fn(&Path), + { + let dir = tempfile::tempdir().unwrap(); + let dir = dir.path(); + + runner(dir); + + let output = std::process::Command::new(dir.join("app.exe")) + .current_dir(dir) + .output() + .unwrap(); + + if !output.status.success() { + use std::io::Write; + + std::io::stdout().write_all(&output.stdout).unwrap(); + std::io::stderr().write_all(&output.stderr).unwrap(); + + panic!("app.exe failed"); + } + + String::from_utf8(output.stdout.to_vec()).unwrap() + } + + #[allow(dead_code)] + fn wine_test(runner: F) -> String + where + F: Fn(&Path), + { + let dir = tempfile::tempdir().unwrap(); + let dir = dir.path(); + + runner(dir); + + let output = std::process::Command::new("wine") + .current_dir(dir) + .args(["app.exe"]) + .output() + .unwrap(); + + if !output.status.success() { + use std::io::Write; + + std::io::stdout().write_all(&output.stdout).unwrap(); + std::io::stderr().write_all(&output.stderr).unwrap(); + + panic!("wine failed"); + } + + String::from_utf8(output.stdout.to_vec()).unwrap() + } + + /// Basics of linking: symbols imported and exported by the host and app, both values and + /// functions + #[allow(dead_code)] + fn test_basics(dir: &Path) { + zig_host_app( + dir, + indoc!( + r#" + const std = @import("std"); + + extern const roc_one: u64; + extern const roc_three: u64; + + extern fn roc_magic1() callconv(.C) u64; + extern fn roc_magic2() callconv(.C) u8; + + pub export fn roc_alloc() u64 { + return 123456; + } + + pub export fn roc_realloc() u64 { + return 111111; + } + + pub fn main() !void { + const stdout = std.io.getStdOut().writer(); + try stdout.print("Hello, {} {} {} {}!\n", .{roc_magic1(), roc_magic2(), roc_one, roc_three}); + } + "# + ), + indoc!( + r#" + export const roc_one: u64 = 1; + export const roc_three: u64 = 3; + + extern fn roc_alloc() u64; + extern fn roc_realloc() u64; + + export fn roc_magic1() u64 { + return roc_alloc() + roc_realloc(); + } + + export fn roc_magic2() u8 { + return 32; + } + "# + ), + ); + } + + #[cfg(windows)] + #[serial(zig_build)] + #[test] + fn basics_windows() { + assert_eq!("Hello, 234567 32 1 3!\n", windows_test(test_basics)) + } + + #[test] + #[serial(zig_build)] + #[ignore] + fn basics_wine() { + assert_eq!("Hello, 234567 32 1 3!\n", wine_test(test_basics)) + } + + /// This zig code sample has a relocation in the text section that points into the rodata + /// section. That means we need to correctly track where each app section ends up in the host. + #[allow(dead_code)] + fn test_internal_relocations(dir: &Path) { + zig_host_app( + dir, + indoc!( + r#" + const std = @import("std"); + + extern fn roc_magic1(usize) callconv(.C) [*]const u8; + + pub fn main() !void { + const stdout = std.io.getStdOut().writer(); + try stdout.print("Hello {s}\n", .{roc_magic1(0)[0..3]}); + } + "# + ), + indoc!( + r#" + const X = [_][]const u8 { "foo" }; + + export fn roc_magic1(index: usize) [*]const u8 { + return X[index].ptr; + } + "# + ), + ); + } + + #[cfg(windows)] + #[serial(zig_build)] + #[test] + fn app_internal_relocations_windows() { + assert_eq!("Hello foo\n", windows_test(test_internal_relocations)) + } + + #[test] + #[serial(zig_build)] + #[ignore] + fn app_internal_relocations_wine() { + assert_eq!("Hello foo\n", wine_test(test_internal_relocations)) + } + + /// Run our preprocessing on an all-zig host. There is no app here to simplify things. + fn preprocessing_help(dir: &Path) { + let zig = std::env::var("ROC_ZIG").unwrap_or_else(|_| "zig".into()); + + let host_zig = indoc!( + r#" + const std = @import("std"); + pub fn main() !void { + const stdout = std.io.getStdOut().writer(); + try stdout.print("Hello there\n", .{}); + } + "# + ); + + std::fs::write(dir.join("host.zig"), host_zig.as_bytes()).unwrap(); + + let mut command = std::process::Command::new(zig); + command.current_dir(dir).args([ + "build-exe", + "host.zig", + "-lc", + "-target", + "x86_64-windows-gnu", + "-rdynamic", + "-fstrip", + "-OReleaseFast", + ]); + + let command_str = format!("{:?}", &command); + let output = command.output().unwrap(); + + if !output.status.success() { + use std::io::Write; + + std::io::stdout().write_all(&output.stdout).unwrap(); + std::io::stderr().write_all(&output.stderr).unwrap(); + + panic!("zig build-exe failed: {command_str}"); + } + + let host_bytes = std::fs::read(dir.join("host.exe")).unwrap(); + let host_bytes = host_bytes.as_slice(); + + let extra_sections = [*b"\0\0\0\0\0\0\0\0", *b"\0\0\0\0\0\0\0\0"]; + + Preprocessor::preprocess( + &dir.join("app.exe"), + host_bytes, + 0, + extra_sections.as_slice(), + ); + } + + #[cfg(windows)] + #[test] + fn preprocessing_windows() { + assert_eq!("Hello there\n", windows_test(preprocessing_help)) + } + + #[test] + #[ignore] + fn preprocessing_wine() { + assert_eq!("Hello there\n", wine_test(preprocessing_help)) + } +} diff --git a/crates/packaging/Cargo.toml b/crates/packaging/Cargo.toml new file mode 100644 index 0000000000..f0a894dd9a --- /dev/null +++ b/crates/packaging/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "roc_packaging" +description = "Functionality for packaging Roc source code - e.g. for distribution over the network" + +authors.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true +version.workspace = true + +[dependencies] +roc_error_macros = { path = "../error_macros" } +roc_parse = { path = "../compiler/parse" } + +base64-url.workspace = true +blake3.workspace = true +brotli.workspace = true # used for decompressing tarballs over HTTPS, if the server supports brotli +bumpalo.workspace = true +flate2.workspace = true +fs_extra.workspace = true +tar.workspace = true # used for `roc build --tar` +tempfile.workspace = true +walkdir.workspace = true + +[target.'cfg(not(target_family = "wasm"))'.dependencies] +reqwest.workspace = true + +[dev-dependencies] +tempfile.workspace = true diff --git a/crates/packaging/src/cache.rs b/crates/packaging/src/cache.rs new file mode 100644 index 0000000000..90af81c9ba --- /dev/null +++ b/crates/packaging/src/cache.rs @@ -0,0 +1,262 @@ +#[cfg(not(target_family = "wasm"))] +use { + crate::https::{self, PackageMetadata, Problem}, + roc_error_macros::internal_error, + std::fs, +}; +#[cfg(not(target_family = "wasm"))] +const MAX_DOWNLOAD_BYTES: u64 = 32 * 1_000_000_000; // GB + +use std::path::{Path, PathBuf}; + +#[derive(Copy, Clone, Debug)] +pub enum RocCacheDir<'a> { + /// Normal scenario: reading from the user's cache dir on disk + Persistent(&'a Path), + /// For build.rs and tests where we never want to be downloading anything - yell loudly if we try! + Disallowed, + /// For tests only; we don't want to write to the real cache during a test! + #[cfg(test)] + Temp(&'a tempfile::TempDir), +} + +// Errors in case NixOS users try to use a dynamically linked platform +#[cfg(target_os = "linux")] +fn nixos_error_if_dynamic(url: &str, dest_dir: &Path) { + let output = std::process::Command::new("uname") + .arg("-a") + .output() + .expect("uname command failed to start"); + let running_nixos = String::from_utf8_lossy(&output.stdout).contains("NixOS"); + + if running_nixos { + // bash -c is used instead of plain ldd because process::Command escapes its arguments + let ldd_output = std::process::Command::new("bash") + .arg("-c") + .arg(format!("ldd {}/linux-x86_64.rh*", dest_dir.display())) + .output() + .expect("ldd command failed to start"); + let is_dynamic = String::from_utf8_lossy(&ldd_output.stdout).contains("=>"); + + if is_dynamic { + eprintln!("The platform downloaded from the URL {url} is dynamically linked.\n\ + Dynamically linked platforms can't be used on NixOS.\n\n\ + You can:\n\n\t\ + - Download the source of the platform and build it locally, like in this example:\n\t \ + https://github.com/roc-lang/roc/blob/main/examples/platform-switching/rocLovesRust.roc.\n\t \ + When building your roc application, you can use the flag `--prebuilt-platform` to prevent the platform from being rebuilt every time.\n\t \ + For some graphical platforms you may need to use https://github.com/guibou/nixGL.\n\n\t\ + - Contact the author of the platform to ask them to statically link their platform.\n\t \ + musl can be used to prevent a dynamic dependency on the systems' libc.\n\t \ + If the platform is dynamically linked to GPU drivers, it can not be statically linked practically. Use the previous suggestion to build locally in this case.\n" + ); + std::process::exit(1); + } + } +} + +/// Accepts either a path to the Roc cache dir, or else a TempDir. If a TempDir, always download +/// into that dir. If the cache dir on the filesystem, then look into it to see if we already +/// have an entry for the given URL. If we do, return its info. If we don't already have it, then: +/// +/// - Download and decompress the compressed tarball from the given URL +/// - Verify its bytes against the hash in the URL +/// - Extract the tarball's contents into the appropriate cache directory +/// +/// Returns the path to the installed package (which will be in the cache dir somewhere), as well +/// as the requested root module filename (optionally specified via the URL fragment). +#[cfg(not(target_family = "wasm"))] +pub fn install_package<'a>( + roc_cache_dir: RocCacheDir<'_>, + url: &'a str, +) -> Result<(PathBuf, Option<&'a str>), Problem> { + use std::io::ErrorKind; + + let PackageMetadata { + cache_subdir, + content_hash, + root_module_filename, + } = PackageMetadata::try_from(url).map_err(Problem::InvalidUrl)?; + + match roc_cache_dir { + RocCacheDir::Persistent(cache_dir) => { + // e.g. ~/.cache/roc/example.com/roc-packages/ + let parent_dir = cache_dir.join(cache_subdir); + // e.g. ~/.cache/roc/example.com/roc-packages/jDRlAFAA3738vu3-vMpLUoyxtA86Z7CaZneoOKrihbE + let dest_dir = parent_dir.join(content_hash); + + if dest_dir.exists() { + // If the cache dir exists already, we assume it has the correct contents + // (it's a cache, after all!) and return without downloading anything. + // + #[cfg(target_os = "linux")] + { + nixos_error_if_dynamic(url, &dest_dir); + } + + Ok((dest_dir, root_module_filename)) + } else { + // Download into a tempdir; only move it to dest_dir if hash verification passes. + println!( + "Downloading \u{001b}[36m{url}\u{001b}[0m\n into {}\n", + cache_dir.display() + ); + let tempdir = tempfile::tempdir().map_err(Problem::IoErr)?; + let tempdir_path = tempdir.path(); + let downloaded_hash = + https::download_and_hash(url, tempdir_path, MAX_DOWNLOAD_BYTES)?; + + // Download the tarball into memory and verify it. + // The tarball name is the hash of its contents. + if downloaded_hash == content_hash { + // Now that we've verified the hash, rename the tempdir to the real dir. + + // Create the destination dir's parent dir, since it may not exist yet. + fs::create_dir_all(parent_dir).or_else(|err| match err.kind() { + // It's fine if the destination dir's parent already exists + ErrorKind::AlreadyExists => Ok(()), + _ => Err(Problem::IoErr(err)), + })?; + + // This rename should be super cheap if it succeeds - just an inode change. + let rename_err_kind = fs::rename(tempdir_path, &dest_dir) + .err() + .map(|err| err.kind()); + + // It's okay if the rename failed because the destination already existed. + // This could be a race condition between multiple downloads happening concurrently. + // (This has happened in our test suite, for example!) Both downloads should have + // the same content, so the rename failing for that reason should be no problem. + if rename_err_kind.is_some() + && rename_err_kind != Some(ErrorKind::AlreadyExists) + { + // If the rename failed, try a recursive copy - + // it could have failed due to std::io::ErrorKind::CrossesDevices + // (e.g. if the source an destination directories are on different disks) + // which as of this implementation is nightly-only + // https://doc.rust-lang.org/std/io/enum.ErrorKind.html#variant.CrossesDevices match io_err.kind() { + // but if that's what happened, this should work! + + // fs_extra::dir::copy needs the destination directory to exist already. + fs::create_dir(&dest_dir).or_else(|err| match err.kind() { + // It's fine if the destination dir already exists + ErrorKind::AlreadyExists => Ok(()), + _ => Err(Problem::IoErr(err)), + })?; + + fs_extra::dir::copy( + tempdir_path, + &dest_dir, + &fs_extra::dir::CopyOptions { + content_only: true, + ..Default::default() + }, + ) + .or_else(|err| match err.kind { + // It's fine if the destination file already exists; this could be the same + // as the rename race condition mentioned above. + fs_extra::error::ErrorKind::AlreadyExists => Ok(0), + _ => Err(Problem::FsExtraErr(err)), + })?; + } + + #[cfg(target_os = "linux")] + { + nixos_error_if_dynamic(url, &dest_dir); + } + + // The package's files are now in the cache. We're done! + Ok((dest_dir, root_module_filename)) + } else { + Err(Problem::InvalidContentHash { + expected: content_hash.to_string(), + actual: downloaded_hash, + }) + } + } + } + RocCacheDir::Disallowed => { + internal_error!( + "Tried to download a package ({:?}) via RocCacheDir::Disallowed - which was explicitly used in order to disallow downloading packages in the current context!", + url + ) + } + #[cfg(test)] + RocCacheDir::Temp(temp_dir) => Ok((temp_dir.path().to_path_buf(), None)), + } +} + +#[cfg(windows)] +// e.g. the "Roc" in %APPDATA%\\Roc +const ROC_CACHE_DIR_NAME: &str = "Roc"; + +#[cfg(not(windows))] +// e.g. the "roc" in ~/.cache/roc +const ROC_CACHE_DIR_NAME: &str = "roc"; + +/// This looks up environment variables, so it should ideally be called once and then cached! +/// +/// Returns a path of the form cache_dir_path.join(ROC_CACHE_DIR_NAME).join("packages") +/// where cache_dir_path is: +/// - The XDG_CACHE_HOME environment varaible, if it's set. +/// - Otherwise, ~/.cache on UNIX and %APPDATA% on Windows. +/// +/// ROC_CACHE_DIR_NAME is "roc" on UNIX and "Roc" on Windows. +/// +/// So ~/.cache/roc will be typical on UNIX, and %APPDATA%\\Roc will be typical on Windows. +/// +/// Returns None if XDG_CACHE_HOME is not set, and also we can't determine the home directory +/// (or if %APPDATA% is missing on Windows) on this system. +#[cfg(not(target_family = "wasm"))] +pub fn roc_cache_dir() -> PathBuf { + use std::{env, process}; + + const PACKAGES_DIR_NAME: &str = "packages"; + + // Respect XDG, if the system appears to be using it. + // https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html + match env::var_os("XDG_CACHE_HOME") { + Some(xdg_cache_home) => Path::new(&xdg_cache_home) + .join(ROC_CACHE_DIR_NAME) + .join(PACKAGES_DIR_NAME), + None => { + #[cfg(windows)] + { + // e.g. %APPDATA%\\Roc + if let Some(appdata) = + // CSIDL_APPDATA is the same as APPDATA, according to: + // https://learn.microsoft.com/en-us/windows/deployment/usmt/usmt-recognized-environment-variables + env::var_os("APPDATA").or_else(|| env::var_os("CSIDL_APPDATA")) + { + Path::new(&appdata) + .join(ROC_CACHE_DIR_NAME) + .join(PACKAGES_DIR_NAME) + } else { + eprintln!("roc needs either the %APPDATA% or else the %XDG_CACHE_HOME% environment variables set. Please set one of these environment variables and re-run roc!"); + process::exit(1); + } + } + + #[cfg(unix)] + { + // e.g. $HOME/.cache/roc + if let Some(home) = env::var_os("HOME") { + Path::new(&home) + .join(".cache") + .join(ROC_CACHE_DIR_NAME) + .join(PACKAGES_DIR_NAME) + } else { + eprintln!("roc needs either the $HOME or else the $XDG_CACHE_HOME environment variables set. Please set one of these environment variables and re-run roc!"); + process::exit(1); + } + } + } + } +} + +/// WASI doesn't have a home directory, so just make the cache dir in the current directory +/// https://github.com/WebAssembly/wasi-filesystem/issues/59 +#[cfg(target_family = "wasm")] +pub fn roc_cache_dir() -> PathBuf { + PathBuf::from(".cache").join(ROC_CACHE_DIR_NAME) +} diff --git a/crates/packaging/src/https.rs b/crates/packaging/src/https.rs new file mode 100644 index 0000000000..5928b23dad --- /dev/null +++ b/crates/packaging/src/https.rs @@ -0,0 +1,455 @@ +use std::{ + io::{self, Read, Write}, + path::Path, +}; + +use crate::tarball::Compression; + +// gzip should be the most widely supported, and brotli offers the highest compression. +// flate2 gets us both gzip and deflate, so there's no harm in offering deflate too. +// +// Here are all the officially supported options: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Encoding +// We can consider supporting more, but that would bloat the `roc` binary more, so +// let's try to avoid doing that. +const BROTLI_BUFFER_BYTES: usize = 8 * 1_000_000; // MB + +#[derive(Debug, PartialEq, Eq)] +pub struct PackageMetadata<'a> { + /// The BLAKE3 hash of the tarball's contents. Also the .tar filename on disk. + pub content_hash: &'a str, + /// On disk, this will be the subfolder inside the cache dir where the package lives + pub cache_subdir: &'a str, + /// Other code will default this to main.roc, but this module isn't concerned with that default. + pub root_module_filename: Option<&'a str>, +} + +/// Valid URLs must end in one of these: +/// +/// - .tar +/// - .tar.gz +/// - .tar.br +const VALID_EXTENSION_SUFFIXES: [&str; 2] = [".gz", ".br"]; + +/// Since the TLD (top level domain) `.zip` is now available, there is a new attack +/// vector where malicous URLs can be used to confuse the reader. +/// Example of a URL which would take you to example.zip: +/// https://github.com∕kubernetes∕kubernetes∕archive∕refs∕tags∕@example.zip +/// roc employs a checksum mechanism to prevent tampering with packages. +/// Nevertheless we should avoid such issues earlier. +/// You can read more here: https://medium.com/@bobbyrsec/the-dangers-of-googles-zip-tld-5e1e675e59a5 +const MISLEADING_CHARACTERS_IN_URL: [char; 5] = [ + '@', // @ - For now we avoid usage of the @, to avoid the "tld zip" attack vector + '\u{2044}', // U+2044 == ⁄ Fraction Slash + '\u{2215}', // U+2215 == ∕ Division Slash + '\u{FF0F}', // U+2215 == / Fullwidth Solidus + '\u{29F8}', // U+29F8 == ⧸ Big Solidus +]; + +#[derive(Debug, PartialEq, Eq)] +pub enum UrlProblem { + InvalidExtensionSuffix(String), + MissingTarExt, + InvalidFragment(String), + MissingHash, + MissingHttps, + MisleadingCharacter, +} + +impl<'a> TryFrom<&'a str> for PackageMetadata<'a> { + type Error = UrlProblem; + + fn try_from(url: &'a str) -> Result { + PackageMetadata::new(url) + } +} + +impl<'a> PackageMetadata<'a> { + fn new(url: &'a str) -> Result { + // First, verify that the URL starts with https:// + let without_protocol = match url.split_once("https://") { + Some((_, without_protocol)) => without_protocol, + None => { + return Err(UrlProblem::MissingHttps); + } + }; + + // Next, check if there are misleading characters in the URL + if url + .chars() + .any(|ch| MISLEADING_CHARACTERS_IN_URL.contains(&ch)) + { + return Err(UrlProblem::MisleadingCharacter); + } + + // Next, get the (optional) URL fragment, which must be a .roc filename + let (without_fragment, fragment) = match without_protocol.rsplit_once('#') { + Some((before_fragment, fragment)) => { + const EXT: &str = ".roc"; + + // The fragment must be a .roc file, and the part before ".roc" can't be empty + if fragment.ends_with(EXT) && fragment.len() > EXT.len() { + (before_fragment, Some(fragment)) + } else { + return Err(UrlProblem::InvalidFragment(fragment.to_string())); + } + } + None => (without_protocol, None), + }; + + // The tarball name is everything after the "/" (without the .tar extension) + // The URL must end in .tar followed optionally by ".gz", ".br", etc. (excluding the fragment) + let without_ext = match without_fragment.rsplit_once(".tar") { + Some((before_ext, after_ext)) => { + if after_ext.is_empty() || VALID_EXTENSION_SUFFIXES.contains(&after_ext) { + before_ext + } else { + return Err(UrlProblem::InvalidExtensionSuffix(after_ext.to_string())); + } + } + None => { + // The URL didn't end in .tar at all + return Err(UrlProblem::MissingTarExt); + } + }; + + let (path, tarball_name) = match without_ext.rsplit_once('/') { + Some((path, hash)) if !hash.is_empty() => (path, hash), + _ => { + return Err(UrlProblem::MissingHash); + } + }; + + Ok(PackageMetadata { + cache_subdir: path, + content_hash: tarball_name, + root_module_filename: fragment, + }) + } +} + +#[test] +fn url_problem_missing_https() { + let expected = Err(UrlProblem::MissingHttps); + assert_eq!(PackageMetadata::try_from("http://example.com"), expected); +} + +#[test] +fn url_problem_misleading_characters() { + let expected = Err(UrlProblem::MisleadingCharacter); + + for misleading_character_example in [ + "https://user:password@example.com/", + //"https://example.com⁄path", + "https://example.com\u{2044}path", + //"https://example.com∕path", + "https://example.com\u{2215}path", + //"https://example.com/path", + "https://example.com\u{ff0f}path", + //"https://example.com⧸path", + "https://example.com\u{29f8}path", + ] { + assert_eq!( + PackageMetadata::try_from(misleading_character_example), + expected + ); + } +} + +#[test] +fn url_problem_invalid_fragment_not_a_roc_file() { + let expected = Err(UrlProblem::InvalidFragment("filename.sh".to_string())); + assert_eq!( + PackageMetadata::try_from("https://example.com/#filename.sh"), + expected + ); +} + +#[test] +fn url_problem_invalid_fragment_empty_roc_filename() { + let expected = Err(UrlProblem::InvalidFragment(".roc".to_string())); + assert_eq!( + PackageMetadata::try_from("https://example.com/#.roc"), + expected + ); +} + +#[test] +fn url_problem_not_a_tar_url() { + let expected = Err(UrlProblem::MissingTarExt); + assert_eq!( + PackageMetadata::try_from("https://example.com/filename.zip"), + expected + ); +} + +#[test] +fn url_problem_invalid_tar_suffix() { + let expected = Err(UrlProblem::InvalidExtensionSuffix(".zip".to_string())); + assert_eq!( + PackageMetadata::try_from("https://example.com/filename.tar.zip"), + expected + ); +} + +#[test] +fn url_problem_missing_hash() { + let expected = Err(UrlProblem::MissingHash); + assert_eq!( + PackageMetadata::try_from("https://example.com/.tar.gz"), + expected + ); +} + +#[test] +fn url_without_fragment() { + let expected = Ok(PackageMetadata { + cache_subdir: "example.com/path", + content_hash: "hash", + root_module_filename: None, + }); + assert_eq!( + PackageMetadata::try_from("https://example.com/path/hash.tar.gz"), + expected + ); +} + +#[test] +fn url_with_fragment() { + let expected = Ok(PackageMetadata { + cache_subdir: "example.com/path", + content_hash: "hash", + root_module_filename: Some("filename.roc"), + }); + assert_eq!( + PackageMetadata::try_from("https://example.com/path/hash.tar.gz#filename.roc"), + expected + ); +} + +#[derive(Debug)] +pub enum Problem { + UnsupportedEncoding(String), + MultipleEncodings(String), + InvalidContentHash { + expected: String, + actual: String, + }, + IoErr(io::Error), + FsExtraErr(fs_extra::error::Error), + HttpErr(reqwest::Error), + InvalidUrl(UrlProblem), + /// The Content-Length header of the response exceeded max_download_bytes + DownloadTooBig(u64), +} + +pub fn download_and_hash( + url: &str, + dest_dir: &Path, + max_download_bytes: u64, +) -> Result { + // TODO apparently it really improves performance to construct a Client once and then reuse it, + // instead of making a new Client for every request. + // Per https://github.com/seanmonstar/reqwest/issues/1454#issuecomment-1026076701 + let resp = reqwest::blocking::Client::new() + .get(url) + .send() + .map_err(Problem::HttpErr)?; + + // Some servers don't return Content-Length - e.g. Netlify seems to only sometimes return it. + // If they do, and if it says the file is going to be too big, don't bother downloading it! + if let Some(content_len) = resp.content_length() { + if content_len > max_download_bytes { + return Err(Problem::DownloadTooBig(content_len)); + } + } + + // The server can respond with multiple encodings, per + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Encoding + // ...but we don't support that. + let encoding = { + let content_encoding = match resp.headers().get("content-encoding") { + Some(header) => header.to_str().unwrap_or_default(), + None => "", + }; + + Encoding::new(content_encoding, url)? + }; + + let content_length = resp.content_length().map(|n| n as usize); + + // Use .take to prevent a malicious server from sending back bytes + // until system resources are exhausted! + let resp = ProgressReporter::new(resp.take(max_download_bytes), content_length); + decompress_into(dest_dir, encoding, resp) +} + +/// The content encodings we support +#[derive(Debug, Clone, Copy, PartialEq)] +enum Encoding { + Gzip, + Brotli, + Deflate, + Uncompressed, +} + +impl Encoding { + pub fn new(content_encoding: &str, url: &str) -> Result { + use Encoding::*; + + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Encoding#directives + match content_encoding { + "br" => Ok(Brotli), + "gzip" => Ok(Gzip), + "deflate" => Ok(Deflate), + "" => { + // There was no Content-Encoding header, but we can infer the encoding + // from the file extension in the URL. + let end_of_ext = url.rfind('#').unwrap_or(url.len()); + + // Drop the URL fragment when determining file extension + match url[0..end_of_ext].rsplit_once('.') { + Some((_, after_dot)) => match Compression::from_file_ext(after_dot) { + Some(Compression::Brotli) => Ok(Self::Brotli), + Some(Compression::Gzip) => Ok(Self::Gzip), + Some(Compression::Uncompressed) | None => Ok(Self::Uncompressed), + }, + None => Ok(Uncompressed), + } + } + other => { + if other.contains(',') { + // We don't support multiple encodings (although the spec for the HTTP header + // permits a comma-separated list) + Err(Problem::MultipleEncodings(other.to_string())) + } else { + // We don't support other encodings + Err(Problem::UnsupportedEncoding(other.to_string())) + } + } + } + } +} + +#[test] +fn encoding_from_tar_br() { + let actual = Encoding::new( + "", + "https://example.com/jDRlAFAA3738vu3-vMpLUoyxtA86Z7CaZneoOKrihbE.tar.br", + ) + .unwrap(); + + assert_eq!(Encoding::Brotli, actual); +} + +fn hash_and_unpack(dest_dir: &Path, reader: impl Read) -> Result { + let mut hash_reader = HashReader::new(reader); + + tar::Archive::new(&mut hash_reader) + .unpack(dest_dir) + .map_err(Problem::IoErr)?; + + let mut buf = Vec::with_capacity(1024); + + // Archive::new() doesn't always read all the bytes, but we need to read them all + // in order to get the correct hash! + hash_reader.read_to_end(&mut buf).map_err(Problem::IoErr)?; + + Ok(base64_url::encode(hash_reader.finalize().as_bytes())) +} + +/// Read from the given reader, decompress the bytes using the given Content-Encoding string, +/// write them to the given writer, and return the base64url-encoded BLAKE3 hash of what was written. +/// This both writes and hashes incrementally as it reads, so the only extra work that's done +/// at the end is base64url-encoding the final hash. +fn decompress_into( + dest_dir: &Path, + encoding: Encoding, + reader: impl Read, +) -> Result { + match encoding { + Encoding::Brotli => hash_and_unpack( + dest_dir, + brotli::Decompressor::new(reader, BROTLI_BUFFER_BYTES), + ), + Encoding::Gzip => { + // Note: GzDecoder::new immediately parses the gzip header (so, calls read()) + hash_and_unpack(dest_dir, flate2::read::GzDecoder::new(reader)) + } + Encoding::Deflate => hash_and_unpack(dest_dir, flate2::read::DeflateDecoder::new(reader)), + Encoding::Uncompressed => hash_and_unpack(dest_dir, reader), + } +} + +/// Read something while calculating its BLAKE3 hash +struct HashReader { + reader: R, + hasher: blake3::Hasher, +} + +impl HashReader { + pub fn new(reader: R) -> Self { + Self { + reader, + hasher: blake3::Hasher::new(), + } + } + + pub fn finalize(&self) -> blake3::Hash { + self.hasher.finalize() + } +} + +impl Read for HashReader { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + let bytes_read = self.reader.read(buf)?; + + self.hasher.update(&buf[0..bytes_read]); + + Ok(bytes_read) + } +} + +/// Prints download progress to stdout +struct ProgressReporter { + read: usize, + total: Option, + reader: R, +} + +impl ProgressReporter { + fn new(reader: R, total: Option) -> Self { + ProgressReporter { + read: 0, + total, + reader, + } + } +} + +impl Read for ProgressReporter { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + let size = self.reader.read(buf)?; + + self.read += size; + + if let Some(total) = self.total { + eprint!( + "\u{001b}[2K\u{001b}[G[{:.1} / {:.1} MB]", + self.read as f32 / 1_000_000.0, + total as f32 / 1_000_000.0, + ); + } else { + eprint!( + "\u{001b}[2K\u{001b}[G[{:.1} MB]", + self.read as f32 / 1_000_000.0, + ); + } + std::io::stderr().flush()?; + + if self.total.is_some_and(|total| self.read >= total) { + eprintln!(); + } + + Ok(size) + } +} diff --git a/crates/packaging/src/lib.rs b/crates/packaging/src/lib.rs new file mode 100644 index 0000000000..3cea6c06e2 --- /dev/null +++ b/crates/packaging/src/lib.rs @@ -0,0 +1,4 @@ +pub mod cache; +#[cfg(not(target_family = "wasm"))] +pub mod https; +pub mod tarball; diff --git a/crates/packaging/src/tarball.rs b/crates/packaging/src/tarball.rs new file mode 100644 index 0000000000..460378f8fa --- /dev/null +++ b/crates/packaging/src/tarball.rs @@ -0,0 +1,276 @@ +use brotli::enc::BrotliEncoderParams; +use bumpalo::Bump; +use flate2::write::GzEncoder; +use roc_parse::ast::{Header, Module}; +use roc_parse::header::PlatformHeader; +use roc_parse::module::parse_header; +use roc_parse::state::State; +use std::ffi::OsStr; +use std::fs::File; +use std::io::{self, Read, Write}; +use std::path::Path; +use tar; +use walkdir::WalkDir; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Compression { + Brotli, + Gzip, + Uncompressed, +} + +impl Compression { + const fn file_ext(&self) -> &'static str { + match self { + Compression::Brotli => ".tar.br", + Compression::Gzip => ".tar.gz", + Compression::Uncompressed => ".tar", + } + } + + pub fn from_file_ext(ext: &str) -> Option { + match ext { + "tar" => Some(Self::Uncompressed), + "gz" => Some(Self::Gzip), + "br" => Some(Self::Brotli), + _ => None, + } + } +} + +impl<'a> TryFrom<&'a str> for Compression { + type Error = (); + + fn try_from(extension: &'a str) -> Result { + if extension.ends_with(".br") { + Ok(Compression::Brotli) + } else if extension.ends_with(".gz") { + Ok(Compression::Gzip) + } else if extension.ends_with(".tar") { + Ok(Compression::Uncompressed) + } else { + Err(()) + } + } +} + +/// Given a path to a .roc file, write a .tar file to disk. +/// +/// The .tar file will be in the same directory, and its filename +/// will be the hash of its contents. This function returns +/// the name of that filename (including the .tar extension), +/// so the caller can obtain the path to the file by calling +/// Path::with_file_name(returned_string) on the Path argument it provided. +pub fn build(path_to_main: &Path, compression: Compression) -> io::Result { + let mut archive_bytes = Vec::new(); + + write_archive(path_to_main, &mut archive_bytes)?; + + // Now that we have our compressed archive, get its BLAKE3 hash + // and base64url encode it. Use base64url encoding because: + // - It's more concise than hex encoding, so the URL can be shorter + // - Unlike base64 encoding, it's URL-frienly (e.g. won't include slashes) + let hash = base64_url::encode(blake3::hash(&archive_bytes).as_bytes()); + let mut filename = hash; + + filename.push_str(compression.file_ext()); + + // Write the bytes to disk. + { + let dest_path = path_to_main.with_file_name(&filename); + let mut file = File::create(&dest_path).unwrap_or_else(|err| { + panic!( + "Unable to open {} for writing - error was: {:?}", + dest_path.to_string_lossy(), + err + ); + }); + + match compression { + Compression::Brotli => { + brotli::BrotliCompress( + &mut archive_bytes.as_slice(), + &mut file, + &BrotliEncoderParams { + quality: 11, + use_dictionary: true, + ..Default::default() + }, + )?; + } + Compression::Gzip => { + let mut encoder = GzEncoder::new(&mut file, flate2::Compression::fast()); + encoder.write_all(&archive_bytes)?; + encoder.finish()?; + } + Compression::Uncompressed => file.write_all(&archive_bytes)?, + }; + } + + Ok(filename) +} + +/// Write an uncompressed tar archive to the given writer. +fn write_archive(path: &Path, writer: W) -> io::Result<()> { + let root_dir = if let Some(parent) = path.parent() { + parent + } else { + eprintln!( + "{} is a directory, not a .roc file. Please specify a .roc file!", + path.to_string_lossy() + ); + std::process::exit(1); + }; + let mut builder = tar::Builder::new(writer); + let arena = Bump::new(); + let mut buf = Vec::new(); + + // TODO use this when finding .roc files by discovering them from the root module. + // let other_modules: &[Module<'_>] = + match read_header(&arena, &mut buf, path)?.header { + Header::Interface(_) => { + todo!(); + // TODO report error + } + Header::App(_) => { + todo!(); + // TODO report error + } + Header::Hosted(_) => { + todo!(); + // TODO report error + } + Header::Package(_) => { + add_dot_roc_files(root_dir, &mut builder)?; + } + Header::Platform(PlatformHeader { imports: _, .. }) => { + // Add all the prebuilt host files to the archive. + // These should all be in the same directory as the platform module. + for entry in std::fs::read_dir(root_dir)? { + let path = entry?.path(); + + if [ + // surgical linker format + Some("rh"), + // metadata file + Some("rm"), + // legacy linker formats + Some("o"), + Some("obj"), + Some("wasm"), + // optimized wasm builds compile to .zig for now, + // because zig can't emit .bc for wasm yet. + Some("zig"), + ] + .contains(&path.extension().and_then(OsStr::to_str)) + { + builder.append_path_with_name( + &path, + // Store it without the root path, so that (for example) we don't store + // `examples/cli/main.roc` and therefore end up with the root of the tarball + // being an `examples/cli/` dir instead of having `main.roc` in the root. + path.strip_prefix(root_dir).unwrap(), + )?; + } + } + + add_dot_roc_files(root_dir, &mut builder)?; + } + }; + + // TODO: This will be necessary when bundling packages (not platforms, since platforms just + // slurp up the whole directory at the moment) and also platforms in a future where they + // have precompiled hosts, and we only need to grab the .roc files and the precompiled hostfiles! + // { + // // Repeat this process on each of the root module's imports. + // let mut stack = Vec::from_iter_in(other_modules, &arena); + // let mut visited_paths = HashSet::from_iter([path]); + + // // We could do this all in parallel, but a simple stack seems fast enough for this use case. + // while let Some(path) = stack.pop() { + // let other_modules = match read_header(&arena, &mut buf, path) { + // Module::Interface { .. } => { + // // TODO use header.imports + // builder.append_path(path)?; + // } + // Module::App { .. } => { + // // TODO report error + // } + // Module::Hosted { header } => { + // // TODO report error + // } + // Module::Platform { header } => { + // // TODO report error + // } + // }; + // let other_paths = todo!("infer from other_modules"); + + // // Recurse on the other paths in the header + // for other_path in other_paths { + // if !visited_paths.contains(other_path) { + // stack.push(other_path); + // } + // } + + // paths_visited.insert(path); + // } + // } + + builder.finish() +} + +fn add_dot_roc_files( + root_dir: &Path, + builder: &mut tar::Builder, +) -> Result<(), io::Error> { + for entry in WalkDir::new(root_dir).into_iter().filter_entry(|entry| { + let path = entry.path(); + + // Ignore everything except directories and .roc files + path.is_dir() || path.extension().and_then(OsStr::to_str) == Some("roc") + }) { + let entry = entry?; + let path = entry.path(); + + // Only include files, not directories or symlinks. + // Symlinks may not work on Windows, and directories will get automatically + // added based on the paths of the files inside anyway. (In fact, if we don't + // filter out directories in this step, then empty ones can sometimes be added!) + if path.is_file() { + builder.append_path_with_name( + path, + // Store it without the root path, so that (for example) we don't store + // `examples/cli/main.roc` and therefore end up with the root of the tarball + // being an `examples/cli/` dir instead of having `main.roc` in the root. + path.strip_prefix(root_dir).unwrap(), + )?; + } + } + + Ok(()) +} + +fn read_header<'a>( + arena: &'a Bump, + buf: &'a mut Vec, + path: &'a Path, +) -> io::Result> { + // Read all the bytes into the buffer. + { + let mut file = File::open(path)?; + buf.clear(); + file.read_to_end(buf)?; + } + + // TODO avoid copying the contents of the file into a Bumpalo arena by doing multiple + // https://doc.rust-lang.org/std/io/trait.Read.html#tymethod.read calls instead of + // using the much more convenient file.read_to_end - which requires a std::vec::Vec. + // (We can't use that for the parser state and still return Module<'a> unfortunately.) + let arena_buf = bumpalo::collections::Vec::from_iter_in(buf.iter().copied(), arena); + let parse_state = State::new(arena_buf.into_bump_slice()); + let (module, _) = parse_header(arena, parse_state).unwrap_or_else(|_err| { + todo!(); // TODO report a nice error and exit 1 - or maybe just return Err, for better testability? + }); + + Ok(module) +} diff --git a/crates/repl_cli/Cargo.toml b/crates/repl_cli/Cargo.toml new file mode 100644 index 0000000000..c69ba39883 --- /dev/null +++ b/crates/repl_cli/Cargo.toml @@ -0,0 +1,48 @@ +[package] +name = "roc_repl_cli" +description = "Command Line Interface(CLI) functionality for the Read-Evaluate-Print-Loop (REPL)." + +authors.workspace = true +edition.workspace = true +license.workspace = true +version.workspace = true + +[features] +# pipe target to roc_build +target-aarch64 = ["roc_build/target-aarch64"] +target-arm = ["roc_build/target-arm"] +target-x86 = ["roc_build/target-x86"] +target-x86_64 = ["roc_build/target-x86_64"] + +[dependencies] +roc_build = { path = "../compiler/build" } +roc_builtins = { path = "../compiler/builtins" } +roc_bitcode = { path = "../compiler/builtins/bitcode" } +roc_collections = { path = "../compiler/collections" } +roc_gen_llvm = { path = "../compiler/gen_llvm" } +roc_gen_dev = { path = "../compiler/gen_dev" } +roc_load = { path = "../compiler/load" } +roc_mono = { path = "../compiler/mono" } +roc_parse = { path = "../compiler/parse" } +roc_region = { path = "../compiler/region" } +roc_repl_eval = { path = "../repl_eval" } +roc_reporting = { path = "../reporting" } +roc_std = { path = "../roc_std" } +roc_target = { path = "../compiler/roc_target" } +roc_types = { path = "../compiler/types" } +roc_error_macros = { path = "../error_macros" } +roc_repl_ui = { path = "../repl_ui" } + +tempfile.workspace = true +bumpalo.workspace = true +const_format.workspace = true +inkwell.workspace = true +libloading.workspace = true +rustyline-derive.workspace = true +rustyline.workspace = true +target-lexicon.workspace = true +unicode-segmentation.workspace = true + +[lib] +name = "roc_repl_cli" +path = "src/lib.rs" diff --git a/crates/repl_cli/src/cli_gen.rs b/crates/repl_cli/src/cli_gen.rs new file mode 100644 index 0000000000..b6e906e155 --- /dev/null +++ b/crates/repl_cli/src/cli_gen.rs @@ -0,0 +1,369 @@ +use bumpalo::Bump; +use inkwell::context::Context; +use libloading::Library; +use roc_build::link::llvm_module_to_dylib; +use roc_collections::all::MutSet; +use roc_error_macros::internal_error; +use roc_gen_llvm::llvm::build::LlvmBackendMode; +use roc_gen_llvm::llvm::externs::add_default_roc_externs; +use roc_gen_llvm::{run_jit_function, run_jit_function_dynamic_type}; +use roc_load::{EntryPoint, MonomorphizedModule}; +use roc_mono::ir::OptLevel; +use roc_mono::layout::STLayoutInterner; +use roc_parse::ast::Expr; +use roc_repl_eval::eval::jit_to_ast; +use roc_repl_eval::gen::{format_answer, ReplOutput}; +use roc_repl_eval::{ReplApp, ReplAppMemory}; +use roc_std::RocStr; +use roc_target::TargetInfo; +use roc_types::pretty_print::{name_and_print_var, DebugPrint}; +use roc_types::subs::Subs; +use target_lexicon::Triple; + +pub fn eval_llvm( + mut loaded: MonomorphizedModule<'_>, + target: &Triple, + opt_level: OptLevel, +) -> Option { + let arena = Bump::new(); + let target_info = TargetInfo::from(target); + + debug_assert_eq!(loaded.exposed_to_host.top_level_values.len(), 1); + let (main_fn_symbol, main_fn_var) = loaded + .exposed_to_host + .top_level_values + .iter() + .next() + .unwrap(); + let main_fn_symbol = *main_fn_symbol; + let main_fn_var = *main_fn_var; + + // pretty-print the expr type string for later. + let expr_type_str = name_and_print_var( + main_fn_var, + &mut loaded.subs, + loaded.module_id, + &loaded.interns, + DebugPrint::NOTHING, + ); + + let (_, main_fn_layout) = *loaded + .procedures + .keys() + .find(|(s, _)| *s == main_fn_symbol)?; + + let interns = loaded.interns.clone(); + + #[cfg(not(all( + any(target_os = "linux", target_os = "macos"), + any(target_arch = "x86_64", target_arch = "aarch64") + )))] + let (lib, main_fn_name, subs, layout_interner) = + mono_module_to_dylib_llvm(&arena, target, loaded, opt_level) + .expect("we produce a valid Dylib"); + + #[cfg(all( + any(target_os = "linux", target_os = "macos"), + any(target_arch = "x86_64", target_arch = "aarch64") + ))] + let (lib, main_fn_name, subs, layout_interner) = + mono_module_to_dylib_asm(&arena, target, loaded, opt_level) + .expect("we produce a valid Dylib"); + + let mut app = CliApp { lib }; + + let expr = jit_to_ast( + &arena, + &mut app, + main_fn_name, + main_fn_layout, + main_fn_var, + &subs, + &interns, + layout_interner.into_global().fork(), + target_info, + ); + + let expr_str = format_answer(&arena, expr).to_string(); + + Some(ReplOutput { + expr: expr_str, + expr_type: expr_type_str, + }) +} + +struct CliApp { + lib: Library, +} + +struct CliMemory; + +impl<'a> ReplApp<'a> for CliApp { + type Memory = CliMemory; + + /// Run user code that returns a type with a `Builtin` layout + /// Size of the return value is statically determined from its Rust type + fn call_function(&mut self, main_fn_name: &str, mut transform: F) -> Expr<'a> + where + F: FnMut(&'a Self::Memory, Return) -> Expr<'a>, + Self::Memory: 'a, + { + run_jit_function!(self.lib, main_fn_name, Return, |v| transform(&CliMemory, v)) + } + + /// Run user code that returns a struct or union, whose size is provided as an argument + fn call_function_dynamic_size( + &mut self, + main_fn_name: &str, + ret_bytes: usize, + mut transform: F, + ) -> T + where + F: FnMut(&'a Self::Memory, usize) -> T, + Self::Memory: 'a, + { + run_jit_function_dynamic_type!(self.lib, main_fn_name, ret_bytes, |v| transform( + &CliMemory, v + )) + } +} + +macro_rules! deref_number { + ($name: ident, $t: ty) => { + fn $name(&self, addr: usize) -> $t { + let ptr = addr as *const _; + unsafe { *ptr } + } + }; +} + +impl ReplAppMemory for CliMemory { + deref_number!(deref_bool, bool); + + deref_number!(deref_u8, u8); + deref_number!(deref_u16, u16); + deref_number!(deref_u32, u32); + deref_number!(deref_u64, u64); + deref_number!(deref_u128, u128); + deref_number!(deref_usize, usize); + + deref_number!(deref_i8, i8); + deref_number!(deref_i16, i16); + deref_number!(deref_i32, i32); + deref_number!(deref_i64, i64); + deref_number!(deref_i128, i128); + deref_number!(deref_isize, isize); + + deref_number!(deref_f32, f32); + deref_number!(deref_f64, f64); + + fn deref_str(&self, addr: usize) -> &str { + let reference: &RocStr = unsafe { std::mem::transmute(addr) }; + reference.as_str() + } + + fn deref_pointer_with_tag_id(&self, addr: usize) -> (u16, u64) { + let addr_with_id = self.deref_usize(addr); + let tag_id_mask = 0b111; + + let tag_id = addr_with_id & tag_id_mask; + let data_addr = addr_with_id & !tag_id_mask; + (tag_id as _, data_addr as _) + } +} + +#[cfg_attr( + all( + any(target_os = "linux", target_os = "macos"), + any(target_arch = "x86_64", target_arch = "aarch64") + ), + allow(unused) +)] +fn mono_module_to_dylib_llvm<'a>( + arena: &'a Bump, + target: &Triple, + loaded: MonomorphizedModule<'a>, + opt_level: OptLevel, +) -> Result<(libloading::Library, &'a str, Subs, STLayoutInterner<'a>), libloading::Error> { + let target_info = TargetInfo::from(target); + + let MonomorphizedModule { + procedures, + host_exposed_lambda_sets, + entry_point, + interns, + subs, + layout_interner, + .. + } = loaded; + + let context = Context::create(); + let builder = context.create_builder(); + let module = arena.alloc(roc_gen_llvm::llvm::build::module_from_builtins( + target, &context, "", + )); + + let module = arena.alloc(module); + let (module_pass, function_pass) = + roc_gen_llvm::llvm::build::construct_optimization_passes(module, opt_level); + + let (dibuilder, compile_unit) = roc_gen_llvm::llvm::build::Env::new_debug_info(module); + + // Compile and add all the Procs before adding main + let env = roc_gen_llvm::llvm::build::Env { + arena, + builder: &builder, + dibuilder: &dibuilder, + compile_unit: &compile_unit, + context: &context, + interns, + module, + target_info, + mode: LlvmBackendMode::GenTest, // so roc_panic is generated + // important! we don't want any procedures to get the C calling convention + exposed_to_host: MutSet::default(), + }; + + // Add roc_alloc, roc_realloc, and roc_dealloc, since the repl has no + // platform to provide them. + add_default_roc_externs(&env); + + let entry_point = match entry_point { + EntryPoint::Executable { + exposed_to_host, + platform_path: _, + } => { + // TODO support multiple of these! + debug_assert_eq!(exposed_to_host.len(), 1); + let (symbol, layout) = exposed_to_host[0]; + + roc_mono::ir::SingleEntryPoint { symbol, layout } + } + EntryPoint::Test => { + unreachable!() + } + }; + + let (main_fn_name, main_fn) = roc_gen_llvm::llvm::build::build_procedures_return_main( + &env, + &layout_interner, + opt_level, + procedures, + host_exposed_lambda_sets, + entry_point, + ); + + env.dibuilder.finalize(); + + // we don't use the debug info, and it causes weird errors. + module.strip_debug_info(); + + // Uncomment this to see the module's un-optimized LLVM instruction output: + // env.module.print_to_stderr(); + + if main_fn.verify(true) { + function_pass.run_on(&main_fn); + } else { + internal_error!("Main function {main_fn_name} failed LLVM verification in build. Uncomment things nearby to see more details.", ); + } + + module_pass.run_on(env.module); + + // Uncomment this to see the module's optimized LLVM instruction output: + // env.module.print_to_stderr(); + + // Verify the module + if let Err(errors) = env.module.verify() { + internal_error!("Errors defining module:\n{}", errors.to_string()); + } + + llvm_module_to_dylib(env.module, target, opt_level) + .map(|lib| (lib, main_fn_name, subs, layout_interner)) +} + +#[cfg_attr( + not(all( + any(target_os = "linux", target_os = "macos"), + any(target_arch = "x86_64", target_arch = "aarch64") + )), + allow(unused) +)] +fn mono_module_to_dylib_asm<'a>( + arena: &'a Bump, + target: &Triple, + loaded: MonomorphizedModule<'a>, + _opt_level: OptLevel, +) -> Result<(libloading::Library, &'a str, Subs, STLayoutInterner<'a>), libloading::Error> { + // let dir = std::env::temp_dir().join("roc_repl"); + let dir = tempfile::tempdir().unwrap(); + + let app_o_file = dir.path().join("app.o"); + + let _target_info = TargetInfo::from(target); + + let MonomorphizedModule { + module_id, + procedures, + host_exposed_lambda_sets: _, + exposed_to_host, + mut interns, + subs, + mut layout_interner, + .. + } = loaded; + + let lazy_literals = true; + let env = roc_gen_dev::Env { + arena, + module_id, + exposed_to_host: exposed_to_host.top_level_values.keys().copied().collect(), + lazy_literals, + mode: roc_gen_dev::AssemblyBackendMode::Repl, + }; + + let target = target_lexicon::Triple::host(); + let module_object = roc_gen_dev::build_module( + &env, + &mut interns, + &mut layout_interner, + &target, + procedures, + ); + + let module_out = module_object + .write() + .expect("failed to build output object"); + std::fs::write(&app_o_file, module_out).expect("failed to write object to file"); + + // TODO make this an environment variable + if false { + let file_path = std::env::temp_dir().join("app.o"); + println!("gen-test object file written to {}", file_path.display()); + std::fs::copy(&app_o_file, file_path).unwrap(); + } + + let builtins_host_tempfile = + roc_bitcode::host_tempfile().expect("failed to write host builtins object to tempfile"); + + let (mut child, dylib_path) = roc_build::link::link( + &target, + app_o_file.clone(), + // Long term we probably want a smarter way to link in zig builtins. + // With the current method all methods are kept and it adds about 100k to all outputs. + &[ + app_o_file.to_str().unwrap(), + builtins_host_tempfile.path().to_str().unwrap(), + ], + roc_build::link::LinkType::Dylib, + ) + .expect("failed to link dynamic library"); + + child.wait().unwrap(); + + // Load the dylib + let path = dylib_path.as_path().to_str().unwrap(); + + let lib = unsafe { Library::new(path) }?; + + Ok((lib, "test_main", subs, layout_interner)) +} diff --git a/crates/repl_cli/src/lib.rs b/crates/repl_cli/src/lib.rs new file mode 100644 index 0000000000..965ce87906 --- /dev/null +++ b/crates/repl_cli/src/lib.rs @@ -0,0 +1,158 @@ +//! Command Line Interface (CLI) functionality for the Read-Evaluate-Print-Loop (REPL). +mod cli_gen; + +use bumpalo::Bump; +use const_format::concatcp; +use roc_load::MonomorphizedModule; +use roc_mono::ir::OptLevel; +use roc_repl_eval::gen::Problems; +use roc_repl_ui::colors::{BLUE, END_COL, PINK}; +use roc_repl_ui::repl_state::{ReplAction, ReplState}; +use roc_repl_ui::{format_output, is_incomplete, CONT_PROMPT, PROMPT, SHORT_INSTRUCTIONS, TIPS}; +use roc_reporting::report::{ANSI_STYLE_CODES, DEFAULT_PALETTE}; +use roc_target::TargetInfo; +use rustyline::highlight::{Highlighter, PromptInfo}; +use rustyline::validate::{self, ValidationContext, ValidationResult, Validator}; +use rustyline_derive::{Completer, Helper, Hinter}; +use std::borrow::Cow; +use target_lexicon::Triple; + +use crate::cli_gen::eval_llvm; + +pub const WELCOME_MESSAGE: &str = concatcp!( + "\n The rockin' ", + BLUE, + "roc repl", + END_COL, + "\n", + PINK, + "────────────────────────", + END_COL, + "\n\n" +); + +#[derive(Completer, Helper, Hinter, Default)] +pub struct ReplHelper { + validator: InputValidator, + state: ReplState, +} + +pub fn main() -> i32 { + use rustyline::error::ReadlineError; + use rustyline::Editor; + + // To debug rustyline: + // env_logger::init(); + // RUST_LOG=rustyline=debug cargo run repl 2> debug.log + print!("{WELCOME_MESSAGE}{SHORT_INSTRUCTIONS}"); + + let mut editor = Editor::::new(); + let repl_helper = ReplHelper::default(); + editor.set_helper(Some(repl_helper)); + let target = Triple::host(); + let target_info = TargetInfo::from(&target); + let mut arena = Bump::new(); + + loop { + match editor.readline(PROMPT) { + Ok(line) => { + let line = line.trim(); + + editor.add_history_entry(line); + + let repl_state = &mut editor + .helper_mut() + .expect("Editor helper was not set") + .state; + + arena.reset(); + match repl_state.step(&arena, line, target_info, DEFAULT_PALETTE) { + ReplAction::Eval { opt_mono, problems } => { + let output = evaluate(opt_mono, problems, &target); + // If there was no output, don't print a blank line! + // (This happens for something like a type annotation.) + if !output.is_empty() { + println!("{output}"); + } + } + ReplAction::Exit => { + return 0; + } + ReplAction::Help => { + println!("{TIPS}"); + } + ReplAction::Nothing => {} + } + } + #[cfg(windows)] + Err(ReadlineError::WindowResize) => { + // This is fine; just ignore it. + } + Err(ReadlineError::Eof) => { + // End of input; we're done! + return 0; + } + Err(ReadlineError::Interrupted) => { + eprintln!("CTRL-C"); + return 1; + } + Err(err) => { + eprintln!("REPL error: {err:?}"); + return 1; + } + } + } +} + +pub fn evaluate( + opt_mono: Option>, + problems: Problems, + target: &Triple, +) -> String { + let opt_output = opt_mono.and_then(|mono| eval_llvm(mono, target, OptLevel::Normal)); + format_output(ANSI_STYLE_CODES, opt_output, problems) +} + +#[derive(Default)] +struct InputValidator {} + +impl Validator for InputValidator { + fn validate(&self, ctx: &mut ValidationContext) -> rustyline::Result { + if is_incomplete(ctx.input()) { + Ok(ValidationResult::Incomplete) + } else { + Ok(ValidationResult::Valid(None)) + } + } +} + +impl Highlighter for ReplHelper { + fn has_continuation_prompt(&self) -> bool { + true + } + + fn highlight_prompt<'b, 's: 'b, 'p: 'b>( + &'s self, + prompt: &'p str, + info: PromptInfo<'_>, + ) -> Cow<'b, str> { + if info.line_no() > 0 { + CONT_PROMPT.into() + } else { + prompt.into() + } + } +} + +impl Validator for ReplHelper { + fn validate( + &self, + ctx: &mut validate::ValidationContext, + ) -> rustyline::Result { + self.validator.validate(ctx) + } + + fn validate_while_typing(&self) -> bool { + self.validator.validate_while_typing() + } +} diff --git a/crates/repl_eval/Cargo.toml b/crates/repl_eval/Cargo.toml new file mode 100644 index 0000000000..9a41356a07 --- /dev/null +++ b/crates/repl_eval/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "roc_repl_eval" +description = "Provides the functionality for the REPL to evaluate Roc expressions." + +authors.workspace = true +edition.workspace = true +license.workspace = true +version.workspace = true + +[dependencies] +roc_builtins = { path = "../compiler/builtins" } +roc_can = { path = "../compiler/can" } +roc_collections = { path = "../compiler/collections" } +roc_fmt = { path = "../compiler/fmt" } +roc_load = { path = "../compiler/load" } +roc_module = { path = "../compiler/module" } +roc_mono = { path = "../compiler/mono" } +roc_packaging = { path = "../packaging" } +roc_parse = { path = "../compiler/parse" } +roc_problem = { path = "../compiler/problem" } +roc_region = { path = "../compiler/region" } +roc_reporting = { path = "../reporting" } +roc_solve = { path = "../compiler/solve" } +roc_std = { path = "../roc_std" } +roc_target = { path = "../compiler/roc_target" } +roc_types = { path = "../compiler/types" } + +bumpalo.workspace = true diff --git a/crates/repl_eval/src/eval.rs b/crates/repl_eval/src/eval.rs new file mode 100644 index 0000000000..2f987ba09d --- /dev/null +++ b/crates/repl_eval/src/eval.rs @@ -0,0 +1,1488 @@ +use bumpalo::collections::{CollectIn, Vec}; +use bumpalo::Bump; +use roc_types::types::AliasKind; +use std::cmp::{max_by_key, min_by_key}; + +use roc_builtins::bitcode::{FloatWidth, IntWidth}; +use roc_collections::all::MutMap; +use roc_module::called_via::CalledVia; +use roc_module::ident::TagName; +use roc_module::symbol::{Interns, ModuleId, Symbol}; +use roc_mono::ir::ProcLayout; +use roc_mono::layout::{ + self, cmp_fields, union_sorted_tags_pub, Builtin, InLayout, Layout, LayoutCache, + LayoutInterner, LayoutRepr, TLLayoutInterner, UnionLayout, UnionVariant, WrappedVariant, +}; +use roc_parse::ast::{AssignedField, Collection, Expr, Pattern, StrLiteral}; +use roc_region::all::{Loc, Region}; +use roc_std::RocDec; +use roc_target::TargetInfo; +use roc_types::subs::{ + Content, FlatType, GetSubsSlice, RecordFields, Subs, TagExt, TupleElems, UnionTags, Variable, +}; + +use crate::{ReplApp, ReplAppMemory}; + +struct Env<'a, 'env> { + arena: &'a Bump, + subs: &'env Subs, + target_info: TargetInfo, + interns: &'a Interns, + layout_cache: LayoutCache<'a>, +} + +/// JIT execute the given main function, and then wrap its results in an Expr +/// so we can display them to the user using the formatter. +/// +/// We need the original types in order to properly render records and tag unions, +/// because at runtime those are structs - that is, unlabeled memory offsets. +/// By traversing the type signature while we're traversing the layout, once +/// we get to a struct or tag, we know what the labels are and can turn them +/// back into the appropriate user-facing literals. +#[allow(clippy::too_many_arguments)] +pub fn jit_to_ast<'a, A: ReplApp<'a>>( + arena: &'a Bump, + app: &mut A, + main_fn_name: &str, + layout: ProcLayout<'a>, + var: Variable, + subs: &Subs, + interns: &'a Interns, + layout_interner: TLLayoutInterner<'a>, + target_info: TargetInfo, +) -> Expr<'a> { + let mut env = Env { + arena, + subs, + target_info, + interns, + layout_cache: LayoutCache::new(layout_interner, target_info), + }; + + match layout { + ProcLayout { + arguments: [], + result, + niche: _, + } => { + // This is a thunk, which cannot be defined in userspace, so we know + // it's `main` and can be executed. + jit_to_ast_help(&mut env, app, main_fn_name, result, var) + } + ProcLayout { arguments, .. } => { + // This is a user-supplied function; create a fake Expr for it. + let mut arg_patterns = + bumpalo::collections::Vec::with_capacity_in(arguments.len(), arena); + + // Put in an underscore for each of the args, just to get the arity right. + for _ in 0..arguments.len() { + arg_patterns.push(Loc::at_zero(Pattern::Underscore("_"))); + } + + let body_expr = Loc::at_zero(Expr::Record(Collection::empty())); + + Expr::Closure(arg_patterns.into_bump_slice(), arena.alloc(body_expr)) + } + } +} + +#[derive(Debug)] +enum NewtypeKind { + Tag(TagName), + RecordField(String), + Opaque(Symbol), +} + +fn get_newtype_tag_and_var( + env: &mut Env, + var: Variable, + tags: UnionTags, +) -> Option<(TagName, Variable)> { + let union_variant = { + let mut layout_env = roc_mono::layout::Env::from_components( + &mut env.layout_cache, + env.subs, + env.arena, + env.target_info, + ); + roc_mono::layout::union_sorted_tags(&mut layout_env, var).unwrap() + }; + + let tag_name = match union_variant { + UnionVariant::Newtype { tag_name, .. } + | UnionVariant::NewtypeByVoid { + data_tag_name: tag_name, + .. + } => tag_name.expect_tag(), + _ => return None, + }; + + let vars = tags + .unsorted_iterator(env.subs, TagExt::Any(Variable::EMPTY_TAG_UNION)) + .find(|(tag, _)| **tag == tag_name) + .unwrap() + .1; + + match vars { + [var] => Some((tag_name, *var)), + _ => { + // Multiple variables; we should not display this as a newtype. + None + } + } +} + +/// Unrolls types that are newtypes. These include +/// - Singleton tags with one type argument (e.g. `Container Str`) +/// - Records with exactly one field (e.g. `{ number: Nat }`) +/// +/// This is important in synchronizing `Content`s with `Layout`s, since `Layout`s will +/// always unwrap newtypes and use the content of the underlying type. +/// +/// The returned list of newtype containers is ordered by increasing depth. As an example, +/// `A ({b : C 123})` will have the unrolled list `[Tag(A), RecordField(b), Tag(C)]`. +/// +/// If we pass through aliases, the top-level alias that should be displayed to the user is passed +/// back as an option. +/// +/// Returns (new type containers, optional alias content, real content). +fn unroll_newtypes_and_aliases<'a, 'env>( + env: &mut Env<'a, 'env>, + var: Variable, +) -> (Vec<'a, NewtypeKind>, Option<&'env Content>, Variable) { + let mut var = var; + let mut newtype_containers = Vec::with_capacity_in(1, env.arena); + let mut alias_content = None; + loop { + let content = env.subs.get_content_without_compacting(var); + match content { + Content::Structure(FlatType::TagUnion(tags, _)) => { + match get_newtype_tag_and_var(env, var, *tags) { + Some((tag_name, inner_var)) => { + newtype_containers.push(NewtypeKind::Tag(tag_name)); + var = inner_var; + } + None => return (newtype_containers, alias_content, var), + } + } + Content::Structure(FlatType::Record(fields, _)) if fields.len() == 1 => { + let (label, field) = fields + .sorted_iterator(env.subs, Variable::EMPTY_RECORD) + .next() + .unwrap(); + newtype_containers.push(NewtypeKind::RecordField(label.to_string())); + var = field.into_inner(); + } + Content::Alias(name, _, real_var, kind) => { + if *name == Symbol::BOOL_BOOL || name.module_id() == ModuleId::NUM { + return (newtype_containers, alias_content, var); + } + // We need to pass through aliases too, because their underlying types may have + // unrolled newtypes. For example, + // T : { a : Str } + // v : T + // v = { a : "value" } + // v + // Here we need the newtype container to be `[RecordField(a)]`. + // + // At the end of the day what we should show to the user is the alias content, not + // what's inside, so keep that around too. + if *kind == AliasKind::Opaque { + newtype_containers.push(NewtypeKind::Opaque(*name)); + } + alias_content = Some(content); + var = *real_var; + } + _ => return (newtype_containers, alias_content, var), + } + } +} + +fn apply_newtypes<'a>( + env: &Env<'a, '_>, + newtype_containers: &'a [NewtypeKind], + mut expr: Expr<'a>, +) -> Expr<'a> { + let arena = env.arena; + // Reverse order of what we receieve from `unroll_newtypes_and_aliases` since + // we want the deepest container applied first. + for container in newtype_containers.iter().rev() { + match container { + NewtypeKind::Tag(tag_name) => { + let tag_expr = tag_name_to_expr(env, tag_name); + let loc_tag_expr = &*arena.alloc(Loc::at_zero(tag_expr)); + let loc_arg_expr = &*arena.alloc(Loc::at_zero(expr)); + let loc_arg_exprs = arena.alloc_slice_copy(&[loc_arg_expr]); + expr = Expr::Apply(loc_tag_expr, loc_arg_exprs, CalledVia::Space); + } + NewtypeKind::RecordField(field_name) => { + let label = Loc::at_zero(field_name.as_str()); + let field_val = arena.alloc(Loc::at_zero(expr)); + let field = Loc::at_zero(AssignedField::RequiredValue(label, &[], field_val)); + expr = Expr::Record(Collection::with_items(&*arena.alloc([field]))) + } + NewtypeKind::Opaque(name) => { + let opaque_name = arena.alloc(format!("@{}", name.as_str(env.interns))); + let opaque_ref = &*arena.alloc(Loc::at_zero(Expr::OpaqueRef(opaque_name))); + let loc_arg_expr = &*arena.alloc(Loc::at_zero(expr)); + let loc_arg_exprs = arena.alloc_slice_copy(&[loc_arg_expr]); + expr = Expr::Apply(opaque_ref, loc_arg_exprs, CalledVia::Space); + } + } + } + expr +} + +fn unroll_recursion_var<'env>(env: &Env<'_, 'env>, mut content: &'env Content) -> &'env Content { + while let Content::RecursionVar { structure, .. } = content { + content = env.subs.get_content_without_compacting(*structure); + } + content +} + +fn get_tags_vars_and_variant<'a>( + env: &mut Env<'a, '_>, + tags: &UnionTags, + opt_rec_var: Option, +) -> (MutMap>, UnionVariant<'a>) { + let tags_vec: std::vec::Vec<(TagName, std::vec::Vec)> = tags + .unsorted_iterator(env.subs, TagExt::Any(Variable::EMPTY_TAG_UNION)) + .map(|(a, b)| (a.clone(), b.to_vec())) + .collect(); + + let vars_of_tag: MutMap<_, _> = tags_vec.iter().cloned().collect(); + + let union_variant = { + let mut layout_env = layout::Env::from_components( + &mut env.layout_cache, + env.subs, + env.arena, + env.target_info, + ); + union_sorted_tags_pub(&mut layout_env, tags_vec, opt_rec_var) + }; + + (vars_of_tag, union_variant) +} + +const FAKE_EXPR: &Loc = &Loc::at_zero(Expr::Crash); + +fn expr_of_tag<'a, M: ReplAppMemory>( + env: &mut Env<'a, '_>, + mem: &'a M, + data_addr: usize, + tag_name: &TagName, + arg_layouts: &'a [InLayout<'a>], + arg_vars: &[Variable], + when_recursive: WhenRecursive<'a>, +) -> Expr<'a> { + let tag_expr = tag_name_to_expr(env, tag_name); + let loc_tag_expr = &*env.arena.alloc(Loc::at_zero(tag_expr)); + + debug_assert_eq!(arg_layouts.len(), arg_vars.len()); + + // The type checker stores payloads in definition order, but the memory representation sorts + // first by size (and tie-breaks by definition order). + let mut layouts: Vec<_> = arg_vars + .iter() + .enumerate() + .map(|(i, v)| { + let layout = env.layout_cache.from_var(env.arena, *v, env.subs).unwrap(); + (i, *v, layout) + }) + .collect_in(env.arena); + + layouts.sort_by(|(i1, _, lay1), (i2, _, lay2)| { + cmp_fields(&env.layout_cache.interner, i1, *lay1, i2, *lay2) + }); + + let mut output: Vec<&Loc> = + Vec::from_iter_in(std::iter::repeat(FAKE_EXPR).take(layouts.len()), env.arena); + let mut field_addr = data_addr; + for (i, var, lay) in layouts { + let repr = env.layout_cache.interner.get_repr(lay); + let expr = addr_to_ast(env, mem, field_addr, repr, when_recursive, var); + let loc_expr = Loc::at_zero(expr); + + output[i] = &*env.arena.alloc(loc_expr); + + // Advance the field pointer to the next field. + field_addr += env.layout_cache.interner.stack_size(lay) as usize; + } + + let output = output.into_bump_slice(); + + Expr::Apply(loc_tag_expr, output, CalledVia::Space) +} + +/// Gets the tag ID of a union variant, assuming that the tag ID is stored alongside (after) the +/// tag data. The caller is expected to check that the tag ID is indeed stored this way. +fn tag_id_from_data<'a, M: ReplAppMemory>( + env: &Env<'a, '_>, + mem: &M, + union_layout: UnionLayout<'a>, + data_addr: usize, +) -> i64 { + let offset = union_layout + .data_size_without_tag_id(&env.layout_cache.interner) + .unwrap(); + let tag_id_addr = data_addr + offset as usize; + + use roc_mono::layout::Discriminant::*; + match union_layout.discriminant() { + U0 => 0, + U1 => mem.deref_bool(tag_id_addr) as i64, + U8 => mem.deref_u8(tag_id_addr) as i64, + U16 => mem.deref_u16(tag_id_addr) as i64, + } +} + +/// Gets the tag ID of a union variant from its recursive pointer (that is, the pointer to the +/// pointer to the data of the union variant). Returns +/// - the tag ID +/// - the address of the data of the union variant, unmasked if the pointer held the tag ID +fn tag_id_from_recursive_ptr<'a, M: ReplAppMemory>( + env: &Env<'a, '_>, + mem: &M, + union_layout: UnionLayout<'a>, + rec_addr: usize, +) -> (i64, usize) { + let tag_in_ptr = union_layout.stores_tag_id_in_pointer(env.target_info); + + if tag_in_ptr { + let (tag_id, data_addr) = mem.deref_pointer_with_tag_id(rec_addr); + (tag_id as _, data_addr as _) + } else { + let addr_with_id = mem.deref_usize(rec_addr); + let tag_id = tag_id_from_data(env, mem, union_layout, addr_with_id); + (tag_id, addr_with_id) + } +} + +const OPAQUE_FUNCTION: Expr = Expr::Var { + module_name: "", + ident: "", +}; + +fn jit_to_ast_help<'a, A: ReplApp<'a>>( + env: &mut Env<'a, '_>, + app: &mut A, + main_fn_name: &str, + layout: InLayout<'a>, + var: Variable, +) -> Expr<'a> { + let (newtype_containers, _alias_content, raw_var) = unroll_newtypes_and_aliases(env, var); + + macro_rules! num_helper { + ($ty:ty) => { + app.call_function(main_fn_name, |_, num: $ty| { + number_literal_to_ast(env.arena, num) + }) + }; + } + + let expr = match env.layout_cache.get_repr(layout) { + LayoutRepr::Builtin(Builtin::Bool) => { + app.call_function(main_fn_name, |_mem: &A::Memory, num: bool| { + bool_to_ast(env, num, env.subs.get_content_without_compacting(raw_var)) + }) + } + LayoutRepr::Builtin(Builtin::Int(int_width)) => { + use IntWidth::*; + + match int_width { + U8 => { + let raw_content = env.subs.get_content_without_compacting(raw_var); + if matches!(raw_content, Content::Alias(name, ..) if name.module_id() == ModuleId::NUM) + { + num_helper!(u8) + } else { + // This is not a number, it's a tag union or something else + app.call_function(main_fn_name, |_mem: &A::Memory, num: u8| { + byte_to_ast(env, num, env.subs.get_content_without_compacting(raw_var)) + }) + } + } + U16 => num_helper!(u16), + U32 => num_helper!(u32), + U64 => num_helper!(u64), + U128 => num_helper!(u128), + I8 => num_helper!(i8), + I16 => num_helper!(i16), + I32 => num_helper!(i32), + I64 => num_helper!(i64), + I128 => num_helper!(i128), + } + } + LayoutRepr::Builtin(Builtin::Float(float_width)) => { + use FloatWidth::*; + + match float_width { + F32 => num_helper!(f32), + F64 => num_helper!(f64), + } + } + LayoutRepr::Builtin(Builtin::Decimal) => num_helper!(RocDec), + LayoutRepr::Builtin(Builtin::Str) => { + let body = |mem: &A::Memory, addr| { + let string = mem.deref_str(addr); + let arena_str = env.arena.alloc_str(string); + Expr::Str(StrLiteral::PlainLine(arena_str)) + }; + + app.call_function_returns_roc_str(env.target_info, main_fn_name, body) + } + LayoutRepr::Builtin(Builtin::List(elem_layout)) => app.call_function_returns_roc_list( + main_fn_name, + |mem: &A::Memory, (addr, len, _cap)| { + list_to_ast( + env, + mem, + addr, + len, + elem_layout, + env.subs.get_content_without_compacting(raw_var), + ) + }, + ), + LayoutRepr::Struct(field_layouts) => { + let fields = [Layout::U64, layout]; + + let result_stack_size = + LayoutRepr::struct_(env.arena.alloc(fields)).stack_size(&env.layout_cache.interner); + + let struct_addr_to_ast = |mem: &'a A::Memory, addr: usize| match env + .subs + .get_content_without_compacting(raw_var) + { + Content::Structure(FlatType::Record(fields, _)) => { + struct_to_ast(env, mem, addr, *fields) + } + Content::Structure(FlatType::EmptyRecord) => { + struct_to_ast(env, mem, addr, RecordFields::empty()) + } + Content::Structure(FlatType::Tuple(elems, _)) => { + struct_to_ast_tuple(env, mem, addr, *elems) + } + Content::Structure(FlatType::TagUnion(tags, _)) => { + let (tag_name, payload_vars) = unpack_single_element_tag_union(env.subs, *tags); + + single_tag_union_to_ast(env, mem, addr, field_layouts, tag_name, payload_vars) + } + Content::Structure(FlatType::FunctionOrTagUnion(tag_names, _, _)) => { + let tag_name = &env.subs.get_subs_slice(*tag_names)[0]; + + single_tag_union_to_ast(env, mem, addr, field_layouts, tag_name, &[]) + } + Content::Structure(FlatType::Func(_, _, _)) => { + // a function with a struct as the closure environment + OPAQUE_FUNCTION + } + other => { + unreachable!( + "Something had a Struct layout, but instead of a Record or TagUnion type, it had: {:?}", + other + ); + } + }; + + app.call_function_dynamic_size( + main_fn_name, + result_stack_size as usize, + struct_addr_to_ast, + ) + } + LayoutRepr::Union(UnionLayout::NonRecursive(_)) => { + let size = env.layout_cache.interner.stack_size(layout); + + app.call_function_dynamic_size( + main_fn_name, + size as usize, + |mem: &'a A::Memory, addr: usize| { + addr_to_ast( + env, + mem, + addr, + env.layout_cache.get_repr(layout), + WhenRecursive::Unreachable, + env.subs.get_root_key_without_compacting(raw_var), + ) + }, + ) + } + LayoutRepr::Union(UnionLayout::Recursive(_)) + | LayoutRepr::Union(UnionLayout::NonNullableUnwrapped(_)) + | LayoutRepr::Union(UnionLayout::NullableUnwrapped { .. }) + | LayoutRepr::Union(UnionLayout::NullableWrapped { .. }) => { + let size = env.layout_cache.interner.stack_size(layout); + + app.call_function_dynamic_size( + main_fn_name, + size as usize, + |mem: &'a A::Memory, addr: usize| { + addr_to_ast( + env, + mem, + addr, + env.layout_cache.get_repr(layout), + WhenRecursive::Loop(layout), + env.subs.get_root_key_without_compacting(raw_var), + ) + }, + ) + } + LayoutRepr::RecursivePointer(_) => { + unreachable!("RecursivePointers can only be inside structures") + } + LayoutRepr::Ptr(_) => { + unreachable!("Ptr will never be visible to users") + } + LayoutRepr::LambdaSet(_) | LayoutRepr::FunctionPointer(_) | LayoutRepr::Erased(_) => { + OPAQUE_FUNCTION + } + }; + + apply_newtypes(env, newtype_containers.into_bump_slice(), expr) +} + +fn tag_name_to_expr<'a>(env: &Env<'a, '_>, tag_name: &TagName) -> Expr<'a> { + Expr::Tag(env.arena.alloc_str(&tag_name.as_ident_str())) +} + +/// Represents the layout of `RecursivePointer`s in a tag union, when recursive +/// tag unions are relevant. +#[derive(Clone, Copy, Debug, PartialEq)] +enum WhenRecursive<'a> { + Unreachable, + Loop(InLayout<'a>), +} + +fn addr_to_ast<'a, M: ReplAppMemory>( + env: &mut Env<'a, '_>, + mem: &'a M, + addr: usize, + layout: LayoutRepr<'a>, + when_recursive: WhenRecursive<'a>, + var: Variable, +) -> Expr<'a> { + macro_rules! helper { + ($method: ident, $ty: ty) => {{ + let num: $ty = mem.$method(addr); + + number_literal_to_ast(env.arena, num) + }}; + } + + let (newtype_containers, _alias_content, raw_var) = unroll_newtypes_and_aliases(env, var); + let raw_content = env.subs.get_content_without_compacting(raw_var); + + let expr = match (raw_content, layout) { + (Content::Structure(FlatType::Func(_, _, _)), _) | (_, LayoutRepr::LambdaSet(_) | LayoutRepr::FunctionPointer(_) | LayoutRepr::Erased(_)) => { + OPAQUE_FUNCTION + } + (_, LayoutRepr::Builtin(Builtin::Bool)) => { + // TODO: bits are not as expected here. + // num is always false at the moment. + let num: u8 = mem.deref_u8(addr); + + debug_assert!(num == 0 || num == 1); + + bool_to_ast(env, num != 0, raw_content) + } + (_, LayoutRepr::Builtin(Builtin::Int(int_width))) => { + use IntWidth::*; + + match int_width { + U8 => { + if matches!(raw_content, Content::Alias(name, ..) if name.module_id() == ModuleId::NUM) { + helper!(deref_u8, u8) + } else { + byte_to_ast(env, mem.deref_u8(addr), raw_content) + } + }, + U16 => helper!(deref_u16, u16), + U32 => helper!(deref_u32, u32), + U64 => helper!(deref_u64, u64), + U128 => helper!(deref_u128, u128), + I8 => helper!(deref_i8, i8), + I16 => helper!(deref_i16, i16), + I32 => helper!(deref_i32, i32), + I64 => helper!(deref_i64, i64), + I128 => helper!(deref_i128, i128), + } + } + (_, LayoutRepr::Builtin(Builtin::Float(float_width))) => { + use FloatWidth::*; + + match float_width { + F32 => helper!(deref_f32, f32), + F64 => helper!(deref_f64, f64), + } + } + (_, LayoutRepr::Builtin(Builtin::Decimal)) => { + helper!(deref_dec, RocDec) + } + (_, LayoutRepr::Builtin(Builtin::List(elem_layout))) => { + let elem_addr = mem.deref_usize(addr); + let len = mem.deref_usize(addr + env.target_info.ptr_width() as usize); + let _cap = mem.deref_usize(addr + 2 * env.target_info.ptr_width() as usize); + + list_to_ast(env, mem, elem_addr, len, elem_layout, raw_content) + } + (_, LayoutRepr::Builtin(Builtin::Str)) => { + let string = mem.deref_str(addr); + let arena_str = env.arena.alloc_str(string); + Expr::Str(StrLiteral::PlainLine(arena_str)) + } + (_, LayoutRepr::Struct (field_layouts)) => match raw_content { + Content::Structure(FlatType::Record(fields, _)) => { + struct_to_ast(env, mem, addr, *fields) + } + Content::Structure(FlatType::Tuple(elems,_)) => { + struct_to_ast_tuple(env, mem, addr, *elems) + } + Content::Structure(FlatType::TagUnion(tags, _)) => { + debug_assert_eq!(tags.len(), 1); + + let (tag_name, payload_vars) = unpack_single_element_tag_union(env.subs, *tags); + single_tag_union_to_ast(env, mem, addr, field_layouts, tag_name, payload_vars) + } + Content::Structure(FlatType::FunctionOrTagUnion(tag_names, _, _)) => { + let tag_name = &env.subs.get_subs_slice(*tag_names)[0]; + single_tag_union_to_ast(env, mem, addr, field_layouts, tag_name, &[]) + } + Content::Structure(FlatType::EmptyRecord) => { + struct_to_ast(env, mem, addr, RecordFields::empty()) + } + other => { + unreachable!( + "Something had a Struct layout, but instead of a Record type, it had: {:?}", + other + ); + } + }, + (_, LayoutRepr::RecursivePointer(_)) => match (raw_content, when_recursive) { + ( + Content::RecursionVar { + structure, + opt_name: _, + }, + WhenRecursive::Loop(union_layout), + ) => { + addr_to_ast(env, mem, addr, env.layout_cache.get_repr(union_layout), when_recursive, *structure) + } + + ( + Content::RecursionVar { + structure, + opt_name: _, + }, + WhenRecursive::Unreachable, + ) => { + // It's possible to hit a recursive pointer before the full type layout; just + // figure out the actual recursive structure layout at this point. + let union_layout = env.layout_cache + .from_var(env.arena, *structure, env.subs) + .expect("no layout for structure"); + debug_assert!(matches!(env.layout_cache.get_repr(union_layout), LayoutRepr::Union(..))); + let when_recursive = WhenRecursive::Loop(union_layout); + addr_to_ast(env, mem, addr, env.layout_cache.get_repr(union_layout), when_recursive, *structure) + } + other => unreachable!("Something had a RecursivePointer layout, but instead of being a RecursionVar and having a known recursive layout, I found {:?}", other), + }, + (_, LayoutRepr::Union(UnionLayout::NonRecursive(union_layouts))) => { + let union_layout = UnionLayout::NonRecursive(union_layouts); + + let tags = match raw_content { + Content::Structure(FlatType::TagUnion(tags, _)) => tags, + other => unreachable!("Weird content for nonrecursive Union layout: {:?}", other), + }; + + debug_assert_eq!(union_layouts.len(), tags.len()); + + let (vars_of_tag, union_variant) = get_tags_vars_and_variant(env, tags, None); + + let tags_and_layouts = match union_variant { + UnionVariant::Wrapped(WrappedVariant::NonRecursive { + sorted_tag_layouts + }) => sorted_tag_layouts, + other => unreachable!("This layout tag union layout is nonrecursive but the variant isn't; found variant {:?}", other), + }; + + // Because this is a `NonRecursive`, the tag ID is definitely after the data. + let tag_id = tag_id_from_data(env, mem, union_layout, addr); + + // use the tag ID as an index, to get its name and layout of any arguments + let (tag_name, arg_layouts) = &tags_and_layouts[tag_id as usize]; + + expr_of_tag( + env, + mem, + addr, + tag_name.expect_tag_ref(), + arg_layouts, + &vars_of_tag[tag_name.expect_tag_ref()], + WhenRecursive::Unreachable, + ) + } + (_, LayoutRepr::Union(union_layout @ UnionLayout::Recursive(union_layouts))) => { + let (rec_var, tags) = match raw_content { + Content::Structure(FlatType::RecursiveTagUnion(rec_var, tags, _)) => { + (rec_var, tags) + } + Content::RecursionVar { structure, ..} => { + match env.subs.get_content_without_compacting(*structure) { + Content::Structure(FlatType::RecursiveTagUnion(rec_var, tags, _)) => { + (rec_var, tags) + } + content => unreachable!("any other content should have a different layout, but we saw {:#?}", roc_types::subs::SubsFmtContent(content, env.subs)), + } + } + _ => unreachable!("any other content should have a different layout, but we saw {:#?}", roc_types::subs::SubsFmtContent(raw_content, env.subs)), + }; + debug_assert_eq!(union_layouts.len(), tags.len()); + + let (vars_of_tag, union_variant) = get_tags_vars_and_variant(env, tags, Some(*rec_var)); + + let tags_and_layouts = match union_variant { + UnionVariant::Wrapped(WrappedVariant::Recursive { sorted_tag_layouts }) => { + sorted_tag_layouts + } + _ => unreachable!("any other variant would have a different layout"), + }; + + let (tag_id, ptr_to_data) = tag_id_from_recursive_ptr(env, mem, union_layout, addr); + + + let (tag_name, arg_layouts) = &tags_and_layouts[tag_id as usize]; + expr_of_tag( + env, + mem, + ptr_to_data, + tag_name.expect_tag_ref(), + arg_layouts, + &vars_of_tag[tag_name.expect_tag_ref()], + when_recursive, + ) + } + ( + Content::Structure(FlatType::Apply(Symbol::BOX_BOX_TYPE, args)), + LayoutRepr::Union(UnionLayout::NonNullableUnwrapped([inner_layout])), + ) => { + debug_assert_eq!(args.len(), 1); + + let inner_var_index = args.into_iter().next().unwrap(); + let inner_var = env.subs[inner_var_index]; + + let addr_of_inner = mem.deref_usize(addr); + let inner_expr = addr_to_ast( + env, + mem, + addr_of_inner, + env.layout_cache.get_repr(*inner_layout), + WhenRecursive::Unreachable, + inner_var, + ); + + let box_box = env.arena.alloc(Loc::at_zero(Expr::Var { + module_name: "Box", + ident: "box", + })); + let box_box_arg = &*env.arena.alloc(Loc::at_zero(inner_expr)); + let box_box_args = env.arena.alloc([box_box_arg]); + + Expr::Apply(box_box, box_box_args, CalledVia::Space) + } + (_, LayoutRepr::Union(UnionLayout::NonNullableUnwrapped(_))) => { + let (rec_var, tags) = match unroll_recursion_var(env, raw_content) { + Content::Structure(FlatType::RecursiveTagUnion(rec_var, tags, _)) => { + (rec_var, tags) + } + other => unreachable!("Unexpected content for NonNullableUnwrapped: {:?}", other), + }; + debug_assert_eq!(tags.len(), 1); + + let (vars_of_tag, union_variant) = get_tags_vars_and_variant(env, tags, Some(*rec_var)); + + let (tag_name, arg_layouts) = match union_variant { + UnionVariant::Wrapped(WrappedVariant::NonNullableUnwrapped { + tag_name, + fields, + }) => (tag_name.expect_tag(), fields), + _ => unreachable!("any other variant would have a different layout"), + }; + + let data_addr = mem.deref_usize(addr); + + expr_of_tag( + env, + mem, + data_addr, + &tag_name, + arg_layouts, + &vars_of_tag[&tag_name], + when_recursive, + ) + } + (_, LayoutRepr::Union(UnionLayout::NullableUnwrapped { .. })) => { + let (rec_var, tags) = match unroll_recursion_var(env, raw_content) { + Content::Structure(FlatType::RecursiveTagUnion(rec_var, tags, _)) => { + (rec_var, tags) + } + other => unreachable!("Unexpected content for NonNullableUnwrapped: {:?}", other), + }; + debug_assert!(tags.len() <= 2); + + let (vars_of_tag, union_variant) = get_tags_vars_and_variant(env, tags, Some(*rec_var)); + + let (nullable_name, other_name, other_arg_layouts) = match union_variant { + UnionVariant::Wrapped(WrappedVariant::NullableUnwrapped { + nullable_id: _, + nullable_name, + other_name, + other_fields, + }) => ( + nullable_name.expect_tag(), + other_name.expect_tag(), + other_fields, + ), + _ => unreachable!("any other variant would have a different layout"), + }; + + let data_addr = mem.deref_usize(addr); + if data_addr == 0 { + tag_name_to_expr(env, &nullable_name) + } else { + expr_of_tag( + env, + mem, + data_addr, + &other_name, + other_arg_layouts, + &vars_of_tag[&other_name], + when_recursive, + ) + } + } + (_, LayoutRepr::Union(union_layout @ UnionLayout::NullableWrapped { .. })) => { + let (rec_var, tags) = match unroll_recursion_var(env, raw_content) { + Content::Structure(FlatType::RecursiveTagUnion(rec_var, tags, _)) => { + (rec_var, tags) + } + other => unreachable!("Unexpected content for NonNullableUnwrapped: {:?}", other), + }; + + let (vars_of_tag, union_variant) = get_tags_vars_and_variant(env, tags, Some(*rec_var)); + + let (nullable_id, nullable_name, tags_and_layouts) = match union_variant { + UnionVariant::Wrapped(WrappedVariant::NullableWrapped { + nullable_id, + nullable_name, + sorted_tag_layouts, + }) => (nullable_id, nullable_name.expect_tag(), sorted_tag_layouts), + _ => unreachable!("any other variant would have a different layout"), + }; + + let data_addr = mem.deref_usize(addr); + if data_addr == 0 { + tag_name_to_expr(env, &nullable_name) + } else { + let (tag_id, data_addr) = tag_id_from_recursive_ptr(env, mem, union_layout, addr); + + let tag_id = if tag_id > nullable_id.into() { + tag_id - 1 + } else { + tag_id + }; + + let (tag_name, arg_layouts) = &tags_and_layouts[tag_id as usize]; + expr_of_tag( + env, + mem, + data_addr, + tag_name.expect_tag_ref(), + arg_layouts, + &vars_of_tag[tag_name.expect_tag_ref()], + when_recursive, + ) + } + } + (_, LayoutRepr::Ptr(_)) => { + unreachable!("Ptr layouts are never available in user code") + } + }; + apply_newtypes(env, newtype_containers.into_bump_slice(), expr) +} + +fn list_to_ast<'a, M: ReplAppMemory>( + env: &mut Env<'a, '_>, + mem: &'a M, + addr: usize, + len: usize, + elem_layout: InLayout<'a>, + content: &Content, +) -> Expr<'a> { + let elem_var = match content { + Content::Structure(FlatType::Apply(Symbol::LIST_LIST, vars)) => { + debug_assert_eq!(vars.len(), 1); + + let elem_var_index = vars.into_iter().next().unwrap(); + env.subs[elem_var_index] + } + other => { + unreachable!( + "Something had a Struct layout, but instead of a Record type, it had: {:?}", + other + ); + } + }; + + let arena = env.arena; + let mut output = Vec::with_capacity_in(len, arena); + let elem_size = env.layout_cache.interner.stack_size(elem_layout) as usize; + + for index in 0..len { + let offset_bytes = index * elem_size; + let elem_addr = addr + offset_bytes; + let (newtype_containers, _alias_content, elem_content) = + unroll_newtypes_and_aliases(env, elem_var); + let expr = addr_to_ast( + env, + mem, + elem_addr, + env.layout_cache.get_repr(elem_layout), + WhenRecursive::Unreachable, + elem_content, + ); + let expr = Loc::at_zero(apply_newtypes( + env, + newtype_containers.into_bump_slice(), + expr, + )); + + output.push(&*arena.alloc(expr)); + } + + let output = output.into_bump_slice(); + + Expr::List(Collection::with_items(output)) +} + +fn single_tag_union_to_ast<'a, M: ReplAppMemory>( + env: &mut Env<'a, '_>, + mem: &'a M, + addr: usize, + field_layouts: &'a [InLayout<'a>], + tag_name: &TagName, + payload_vars: &[Variable], +) -> Expr<'a> { + let arena = env.arena; + let tag_expr = tag_name_to_expr(env, tag_name); + + let loc_tag_expr = &*arena.alloc(Loc::at_zero(tag_expr)); + + // logic to sort the fields back into user/syntax order + let mut layouts: Vec<_> = payload_vars + .iter() + .map(|v| env.layout_cache.from_var(env.arena, *v, env.subs).unwrap()) + .enumerate() + .collect_in(env.arena); + + layouts.sort_by(|(_, a), (_, b)| { + Ord::cmp( + &env.layout_cache.interner.alignment_bytes(*b), + &env.layout_cache.interner.alignment_bytes(*a), + ) + }); + + let output = if field_layouts.len() == payload_vars.len() { + let it = payload_vars + .iter() + .copied() + .zip(field_layouts.iter().copied()); + sequence_of_expr(env, mem, addr, it, WhenRecursive::Unreachable) + } else if field_layouts.is_empty() && !payload_vars.is_empty() { + // happens for e.g. `Foo Bar` where unit structures are nested and the inner one is dropped + let it = payload_vars.iter().copied().zip([Layout::UNIT]); + sequence_of_expr(env, mem, addr, it, WhenRecursive::Unreachable) + } else { + unreachable!() + }; + + const DEFAULT: &Loc = &Loc::at_zero(Expr::Crash); + let mut vec: Vec<_> = std::iter::repeat(DEFAULT) + .take(output.len()) + .collect_in(env.arena); + + for (i, o) in output.into_iter().enumerate() { + vec[layouts[i].0] = o; + } + + Expr::Apply(loc_tag_expr, vec.into_bump_slice(), CalledVia::Space) +} + +fn sequence_of_expr<'a, 'env, I, M: ReplAppMemory>( + env: &mut Env<'a, 'env>, + mem: &'a M, + addr: usize, + sequence: I, + when_recursive: WhenRecursive<'a>, +) -> Vec<'a, &'a Loc>> +where + I: ExactSizeIterator)>, +{ + let arena = env.arena; + let mut output = Vec::with_capacity_in(sequence.len(), arena); + + // We'll advance this as we iterate through the fields + let mut field_addr = addr; + + for (var, layout) in sequence { + let expr = addr_to_ast( + env, + mem, + field_addr, + env.layout_cache.get_repr(layout), + when_recursive, + var, + ); + let loc_expr = Loc::at_zero(expr); + + output.push(&*arena.alloc(loc_expr)); + + // Advance the field pointer to the next field. + field_addr += env.layout_cache.interner.stack_size(layout) as usize; + } + + output +} + +fn struct_to_ast<'a, M: ReplAppMemory>( + env: &mut Env<'a, '_>, + mem: &'a M, + addr: usize, + record_fields: RecordFields, +) -> Expr<'a> { + let arena = env.arena; + let subs = env.subs; + let mut output = Vec::with_capacity_in(record_fields.len(), arena); + + if record_fields.len() == 1 { + // this is a 1-field wrapper record around another record or 1-tag tag union + let (label, field) = record_fields + .sorted_iterator(subs, Variable::EMPTY_RECORD) + .next() + .unwrap(); + + let inner_var = field.into_inner(); + let field_layout = env + .layout_cache + .from_var(arena, field.into_inner(), env.subs) + .unwrap(); + let inner_layouts = arena.alloc([field_layout]); + + let struct_layout = LayoutRepr::struct_(inner_layouts); + let loc_expr = &*arena.alloc(Loc { + value: addr_to_ast( + env, + mem, + addr, + struct_layout, + WhenRecursive::Unreachable, + inner_var, + ), + region: Region::zero(), + }); + + let field_name = Loc { + value: &*arena.alloc_str(label.as_str()), + region: Region::zero(), + }; + let loc_field = Loc { + value: AssignedField::RequiredValue(field_name, &[], loc_expr), + region: Region::zero(), + }; + + let output = arena.alloc([loc_field]); + + Expr::Record(Collection::with_items(output)) + } else { + // We'll advance this as we iterate through the fields + let mut field_addr = addr; + + // the type checker stores record fields in alphabetical order + let alphabetical_fields: Vec<_> = record_fields + .sorted_iterator(subs, Variable::EMPTY_RECORD) + .map(|(l, field)| { + let layout = env + .layout_cache + .from_var(arena, field.into_inner(), env.subs) + .unwrap(); + + (l, field, layout) + }) + .collect_in(arena); + + // but the memory representation sorts first by size (and uses field name as a tie breaker) + let mut in_memory_fields = alphabetical_fields; + in_memory_fields.sort_by(|(label1, _, layout1), (label2, _, layout2)| { + cmp_fields( + &env.layout_cache.interner, + label1, + *layout1, + label2, + *layout2, + ) + }); + + for (label, field, field_layout) in in_memory_fields { + let field_var = field.into_inner(); + + let loc_expr = &*arena.alloc(Loc { + value: addr_to_ast( + env, + mem, + field_addr, + env.layout_cache.get_repr(field_layout), + WhenRecursive::Unreachable, + field_var, + ), + region: Region::zero(), + }); + + let field_name = Loc { + value: &*arena.alloc_str(label.as_str()), + region: Region::zero(), + }; + let loc_field = Loc { + value: AssignedField::RequiredValue(field_name, &[], loc_expr), + region: Region::zero(), + }; + + output.push(loc_field); + + // Advance the field pointer to the next field. + field_addr += env.layout_cache.interner.stack_size(field_layout) as usize; + } + + // to the user we want to present the fields in alphabetical order again, so re-sort + fn sort_key<'a, T>(loc_field: &'a Loc>) -> &'a str { + match &loc_field.value { + AssignedField::RequiredValue(field_name, _, _) => field_name.value, + _ => unreachable!("was not added to output"), + } + } + + output.sort_by(|a, b| sort_key(a).cmp(sort_key(b))); + let output = output.into_bump_slice(); + + Expr::Record(Collection::with_items(output)) + } +} + +fn struct_to_ast_tuple<'a, M: ReplAppMemory>( + env: &mut Env<'a, '_>, + mem: &'a M, + addr: usize, + tuple_elems: TupleElems, +) -> Expr<'a> { + let arena = env.arena; + let subs = env.subs; + let mut output = Vec::with_capacity_in(tuple_elems.len(), arena); + + debug_assert!(tuple_elems.len() > 1); + + // We'll advance this as we iterate through the fields + let mut field_addr = addr; + + // the type checker stores tuple elements in alphabetical order + let alphabetical_fields: Vec<_> = tuple_elems + .sorted_iterator(subs, Variable::EMPTY_TUPLE) + .map(|(l, elem)| { + let layout = env.layout_cache.from_var(arena, elem, env.subs).unwrap(); + + (l, elem, layout) + }) + .collect_in(arena); + + // but the memory representation sorts first by size (and uses field name as a tie breaker) + let mut in_memory_fields = alphabetical_fields; + in_memory_fields.sort_by(|(label1, _, layout1), (label2, _, layout2)| { + cmp_fields( + &env.layout_cache.interner, + label1, + *layout1, + label2, + *layout2, + ) + }); + + for (label, elem_var, elem_layout) in in_memory_fields { + let loc_expr = &*arena.alloc(Loc { + value: addr_to_ast( + env, + mem, + field_addr, + env.layout_cache.get_repr(elem_layout), + WhenRecursive::Unreachable, + elem_var, + ), + region: Region::zero(), + }); + + output.push((label, loc_expr)); + + // Advance the field pointer to the next field. + field_addr += env.layout_cache.interner.stack_size(elem_layout) as usize; + } + + // to the user we want to present the fields in alphabetical order again, so re-sort + output.sort_by(|a, b| (a.0).cmp(&b.0)); + let output = env + .arena + .alloc_slice_fill_iter(output.into_iter().map(|(_, expr)| expr)); + + Expr::Tuple(Collection::with_items(output)) +} + +fn unpack_single_element_tag_union(subs: &Subs, tags: UnionTags) -> (&TagName, &[Variable]) { + let (tag_name_index, payload_vars_index) = tags.iter_all().next().unwrap(); + + let tag_name = &subs[tag_name_index]; + let subs_slice = subs[payload_vars_index]; + let payload_vars = subs.get_subs_slice(subs_slice); + + (tag_name, payload_vars) +} + +fn unpack_two_element_tag_union( + subs: &Subs, + tags: UnionTags, +) -> (&TagName, &[Variable], &TagName, &[Variable]) { + let mut it = tags.iter_all(); + let (tag_name_index, payload_vars_index) = it.next().unwrap(); + + let tag_name1 = &subs[tag_name_index]; + let subs_slice = subs[payload_vars_index]; + let payload_vars1 = subs.get_subs_slice(subs_slice); + + let (tag_name_index, payload_vars_index) = it.next().unwrap(); + + let tag_name2 = &subs[tag_name_index]; + let subs_slice = subs[payload_vars_index]; + let payload_vars2 = subs.get_subs_slice(subs_slice); + + (tag_name1, payload_vars1, tag_name2, payload_vars2) +} + +fn bool_to_ast<'a>(env: &Env<'a, '_>, value: bool, content: &Content) -> Expr<'a> { + use Content::*; + + let arena = env.arena; + + match content { + Structure(flat_type) => { + match flat_type { + FlatType::TagUnion(tags, _) if tags.len() == 1 => { + let (tag_name, payload_vars) = unpack_single_element_tag_union(env.subs, *tags); + + let loc_tag_expr = { + let tag_name = &tag_name.as_ident_str(); + let tag_expr = Expr::Tag(arena.alloc_str(tag_name)); + + &*arena.alloc(Loc { + value: tag_expr, + region: Region::zero(), + }) + }; + + let payload = { + // Since this has the layout of a number, there should be + // exactly one payload in this tag. + debug_assert_eq!(payload_vars.len(), 1); + + let var = *payload_vars.iter().next().unwrap(); + let content = env.subs.get_content_without_compacting(var); + + let loc_payload = &*arena.alloc(Loc { + value: bool_to_ast(env, value, content), + region: Region::zero(), + }); + + arena.alloc([loc_payload]) + }; + + Expr::Apply(loc_tag_expr, payload, CalledVia::Space) + } + FlatType::TagUnion(tags, _) if tags.len() == 2 => { + let (tag_name_1, payload_vars_1, tag_name_2, payload_vars_2) = + unpack_two_element_tag_union(env.subs, *tags); + + debug_assert!(payload_vars_1.is_empty()); + debug_assert!(payload_vars_2.is_empty()); + + let tag_name = if value { + max_by_key(tag_name_1, tag_name_2, |n| n.as_ident_str()) + } else { + min_by_key(tag_name_1, tag_name_2, |n| n.as_ident_str()) + }; + + tag_name_to_expr(env, tag_name) + } + FlatType::FunctionOrTagUnion(tags, _, _) if tags.len() == 2 => { + let tags = env.subs.get_subs_slice(*tags); + let tag_name_1 = &tags[0]; + let tag_name_2 = &tags[1]; + + let tag_name = if value { + max_by_key(tag_name_1, tag_name_2, |n| n.as_ident_str()) + } else { + min_by_key(tag_name_1, tag_name_2, |n| n.as_ident_str()) + }; + + tag_name_to_expr(env, tag_name) + } + other => { + unreachable!("Unexpected FlatType {:?} in bool_to_ast", other); + } + } + } + Alias(Symbol::BOOL_BOOL, _, _, _) => Expr::Var { + module_name: "Bool", + ident: if value { "true" } else { "false" }, + }, + Alias(_, _, var, _) => { + let content = env.subs.get_content_without_compacting(*var); + + bool_to_ast(env, value, content) + } + other => { + unreachable!("Unexpected FlatType {:?} in bool_to_ast", other); + } + } +} + +fn byte_to_ast<'a>(env: &mut Env<'a, '_>, value: u8, content: &Content) -> Expr<'a> { + use Content::*; + + let arena = env.arena; + + match content { + Structure(flat_type) => { + match flat_type { + FlatType::TagUnion(tags, _) if tags.len() == 1 => { + let (tag_name, payload_vars) = unpack_single_element_tag_union(env.subs, *tags); + + let loc_tag_expr = { + let tag_name = &tag_name.as_ident_str(); + let tag_expr = Expr::Tag(arena.alloc_str(tag_name)); + + &*arena.alloc(Loc { + value: tag_expr, + region: Region::zero(), + }) + }; + + let payload = { + // Since this has the layout of a number, there should be + // exactly one payload in this tag. + debug_assert_eq!(payload_vars.len(), 1); + + let var = *payload_vars.iter().next().unwrap(); + let content = env.subs.get_content_without_compacting(var); + + let loc_payload = &*arena.alloc(Loc { + value: byte_to_ast(env, value, content), + region: Region::zero(), + }); + + arena.alloc([loc_payload]) + }; + + Expr::Apply(loc_tag_expr, payload, CalledVia::Space) + } + FlatType::TagUnion(tags, _) => { + // anything with fewer tags is not a byte + debug_assert!(tags.len() > 2); + + let tags_vec: std::vec::Vec<(TagName, std::vec::Vec)> = tags + .unsorted_iterator(env.subs, TagExt::Any(Variable::EMPTY_TAG_UNION)) + .map(|(a, b)| (a.clone(), b.to_vec())) + .collect(); + + let union_variant = { + let mut layout_env = layout::Env::from_components( + &mut env.layout_cache, + env.subs, + env.arena, + env.target_info, + ); + union_sorted_tags_pub(&mut layout_env, tags_vec, None) + }; + + match union_variant { + UnionVariant::ByteUnion(tagnames) => { + let tag_name = &tagnames[value as usize].expect_tag_ref(); + let tag_expr = tag_name_to_expr(env, tag_name); + let loc_tag_expr = Loc::at_zero(tag_expr); + Expr::Apply(env.arena.alloc(loc_tag_expr), &[], CalledVia::Space) + } + _ => unreachable!("invalid union variant for a Byte!"), + } + } + FlatType::FunctionOrTagUnion(tags, _, _) => { + // anything with fewer tags is not a byte + debug_assert!(tags.len() > 2); + + let tags_vec: std::vec::Vec<(TagName, std::vec::Vec)> = env + .subs + .get_subs_slice(*tags) + .iter() + .map(|t| (t.clone(), vec![])) + .collect(); + + let union_variant = { + let mut layout_env = layout::Env::from_components( + &mut env.layout_cache, + env.subs, + env.arena, + env.target_info, + ); + union_sorted_tags_pub(&mut layout_env, tags_vec, None) + }; + + match union_variant { + UnionVariant::ByteUnion(tagnames) => { + let tag_name = &tagnames[value as usize].expect_tag_ref(); + let tag_expr = tag_name_to_expr(env, tag_name); + let loc_tag_expr = Loc::at_zero(tag_expr); + Expr::Apply(env.arena.alloc(loc_tag_expr), &[], CalledVia::Space) + } + _ => unreachable!("invalid union variant for a Byte!"), + } + } + other => { + unreachable!("Unexpected FlatType {:?} in byte_to_ast", other); + } + } + } + Alias(_, _, var, _) => { + let content = env.subs.get_content_without_compacting(*var); + + byte_to_ast(env, value, content) + } + other => { + unreachable!("Unexpected FlatType {:?} in byte_to_ast", other); + } + } +} + +/// This is centralized in case we want to format it differently later, +/// e.g. adding underscores for large numbers +fn number_literal_to_ast(arena: &Bump, num: T) -> Expr<'_> { + use std::fmt::Write; + + let mut string = bumpalo::collections::String::with_capacity_in(64, arena); + write!(string, "{num}").unwrap(); + Expr::Num(string.into_bump_str()) +} diff --git a/crates/repl_eval/src/gen.rs b/crates/repl_eval/src/gen.rs new file mode 100644 index 0000000000..658db41dd5 --- /dev/null +++ b/crates/repl_eval/src/gen.rs @@ -0,0 +1,199 @@ +use bumpalo::Bump; +use roc_load::{ExecutionMode, LoadConfig, LoadMonomorphizedError, Threading}; +use roc_packaging::cache::{self, RocCacheDir}; +use roc_problem::Severity; +use roc_reporting::report::Palette; +use std::path::PathBuf; + +use roc_fmt::annotation::Formattable; +use roc_fmt::annotation::{Newlines, Parens}; +use roc_load::{LoadingProblem, MonomorphizedModule}; +use roc_parse::ast::Expr; +use roc_region::all::LineInfo; +use roc_reporting::report::{can_problem, type_problem, RocDocAllocator}; +use roc_solve::FunctionKind; +use roc_target::TargetInfo; + +#[derive(Debug)] +pub struct ReplOutput { + pub expr: String, + pub expr_type: String, +} + +pub fn format_answer<'a>(arena: &'a Bump, answer: Expr<'_>) -> &'a str { + match answer { + Expr::Closure(_, _) | Expr::MalformedClosure => "", + _ => { + let mut expr = roc_fmt::Buf::new_in(arena); + + answer.format_with_options(&mut expr, Parens::NotNeeded, Newlines::Yes, 0); + + expr.into_bump_str() + } + } +} + +#[derive(Default, Debug)] +pub struct Problems { + pub errors: Vec, + pub warnings: Vec, +} + +impl Problems { + pub fn is_empty(&self) -> bool { + self.errors.is_empty() && self.warnings.is_empty() + } +} + +pub fn compile_to_mono<'a, 'i, I: Iterator>( + arena: &'a Bump, + defs: I, + expr: &str, + target_info: TargetInfo, + palette: Palette, +) -> (Option>, Problems) { + let filename = PathBuf::from(""); + let src_dir = PathBuf::from("fake/test/path"); + let (bytes_before_expr, module_src) = promote_expr_to_module(arena, defs, expr); + let loaded = roc_load::load_and_monomorphize_from_str( + arena, + filename, + module_src, + src_dir, + RocCacheDir::Persistent(cache::roc_cache_dir().as_path()), + LoadConfig { + target_info, + function_kind: FunctionKind::LambdaSet, + render: roc_reporting::report::RenderTarget::ColorTerminal, + palette, + threading: Threading::Single, + exec_mode: ExecutionMode::Executable, + }, + ); + + let mut loaded = match loaded { + Ok(v) => v, + Err(LoadMonomorphizedError::ErrorModule(m)) => { + todo!( + "error while loading module: {:?}", + (m.can_problems, m.type_problems) + ); + } + Err(LoadMonomorphizedError::LoadingProblem(LoadingProblem::FormattedReport(report))) => { + return ( + None, + Problems { + errors: vec![report], + warnings: Vec::new(), + }, + ); + } + Err(e) => { + todo!("error while loading module: {:?}", e) + } + }; + + let MonomorphizedModule { + interns, + sources, + can_problems, + type_problems, + .. + } = &mut loaded; + + let mut problems = Problems::default(); + + let errors = &mut problems.errors; + let warnings = &mut problems.warnings; + + for (home, (module_path, src)) in sources.iter() { + let can_probs = can_problems.remove(home).unwrap_or_default(); + let type_probs = type_problems.remove(home).unwrap_or_default(); + + let error_count = can_probs.len() + type_probs.len(); + + if error_count == 0 { + continue; + } + + let line_info = LineInfo::new(module_src); + let src_lines: Vec<&str> = src.split('\n').collect(); + + // Report parsing and canonicalization problems + let alloc = RocDocAllocator::new(&src_lines, *home, interns); + + for problem in can_probs.into_iter() { + // Filter out all warnings and errors whose regions end before this, + // because they must be part of the defs (excluding the most renently added def, + // if that's the one being evaluated) and therefore not things we should show. + // This filters out things like shadowing warnings and unused def warnings. + if problem.region().unwrap_or_default().end().offset as usize >= bytes_before_expr { + let report = can_problem(&alloc, &line_info, module_path.clone(), problem); + let severity = report.severity; + let mut buf = String::new(); + + report.render_color_terminal(&mut buf, &alloc, &palette); + + match severity { + Severity::Warning => { + warnings.push(buf); + } + Severity::Fatal | Severity::RuntimeError => { + errors.push(buf); + } + } + } + } + + for problem in type_probs { + if let Some(report) = type_problem(&alloc, &line_info, module_path.clone(), problem) { + let severity = report.severity; + let mut buf = String::new(); + + report.render_color_terminal(&mut buf, &alloc, &palette); + + match severity { + Severity::Warning => { + warnings.push(buf); + } + Severity::Fatal | Severity::RuntimeError => { + errors.push(buf); + } + } + } + } + } + + (Some(loaded), problems) +} + +fn promote_expr_to_module<'a, 'i, I: Iterator>( + arena: &'a Bump, + defs: I, + expr: &str, +) -> (usize, &'a str) { + const REPL_MODULE_HEADER: &str = "app \"app\" provides [replOutput] to \"./platform\"\n\n"; + const REPL_MODULE_MAIN_DEF: &str = "replOutput =\n"; + const INDENT: &str = " "; + + let mut buffer = bumpalo::collections::string::String::from_str_in(REPL_MODULE_HEADER, arena); + + for line in defs { + // don't indent the defs + buffer.push_str(line); + buffer.push_str("\n\n"); + } + + buffer.push_str(REPL_MODULE_MAIN_DEF); + + let bytes_before_expr = buffer.len(); + + for line in expr.lines() { + // indent the expr! + buffer.push_str(INDENT); + buffer.push_str(line); + buffer.push('\n'); + } + + (bytes_before_expr, buffer.into_bump_str()) +} diff --git a/crates/repl_eval/src/lib.rs b/crates/repl_eval/src/lib.rs new file mode 100644 index 0000000000..5f5cbc5873 --- /dev/null +++ b/crates/repl_eval/src/lib.rs @@ -0,0 +1,87 @@ +//! Provides the functionality for the REPL to evaluate Roc expressions. +use roc_parse::ast::Expr; +use roc_std::RocDec; +use roc_target::TargetInfo; + +pub mod eval; +pub mod gen; + +pub trait ReplApp<'a> { + type Memory: 'a + ReplAppMemory; + + /// Run user code that returns a type with a `Builtin` layout + /// Size of the return value is statically determined from its Rust type + /// The `transform` callback takes the app's memory and the returned value + fn call_function(&mut self, main_fn_name: &str, transform: F) -> Expr<'a> + where + F: FnMut(&'a Self::Memory, Return) -> Expr<'a>, + Self::Memory: 'a; + + fn call_function_returns_roc_list(&mut self, main_fn_name: &str, transform: F) -> Expr<'a> + where + F: FnMut(&'a Self::Memory, (usize, usize, usize)) -> Expr<'a>, + Self::Memory: 'a, + { + self.call_function(main_fn_name, transform) + } + + fn call_function_returns_roc_str( + &mut self, + target_info: TargetInfo, + main_fn_name: &str, + transform: F, + ) -> T + where + F: Fn(&'a Self::Memory, usize) -> T, + Self::Memory: 'a, + { + let roc_str_width = match target_info.ptr_width() { + roc_target::PtrWidth::Bytes4 => 12, + roc_target::PtrWidth::Bytes8 => 24, + }; + + self.call_function_dynamic_size(main_fn_name, roc_str_width, transform) + } + + /// Run user code that returns a struct or union, whose size is provided as an argument + /// The `transform` callback takes the app's memory and the address of the returned value + fn call_function_dynamic_size( + &mut self, + main_fn_name: &str, + ret_bytes: usize, + transform: F, + ) -> T + where + F: FnMut(&'a Self::Memory, usize) -> T, + Self::Memory: 'a; +} + +pub trait ReplAppMemory { + fn deref_bool(&self, addr: usize) -> bool; + + fn deref_u8(&self, addr: usize) -> u8; + fn deref_u16(&self, addr: usize) -> u16; + fn deref_u32(&self, addr: usize) -> u32; + fn deref_u64(&self, addr: usize) -> u64; + fn deref_u128(&self, addr: usize) -> u128; + fn deref_usize(&self, addr: usize) -> usize; + + fn deref_i8(&self, addr: usize) -> i8; + fn deref_i16(&self, addr: usize) -> i16; + fn deref_i32(&self, addr: usize) -> i32; + fn deref_i64(&self, addr: usize) -> i64; + fn deref_i128(&self, addr: usize) -> i128; + fn deref_isize(&self, addr: usize) -> isize; + + fn deref_f32(&self, addr: usize) -> f32; + fn deref_f64(&self, addr: usize) -> f64; + + fn deref_dec(&self, addr: usize) -> RocDec { + let bits = self.deref_i128(addr); + RocDec::new(bits) + } + + fn deref_str(&self, addr: usize) -> &str; + + fn deref_pointer_with_tag_id(&self, addr: usize) -> (u16, u64); +} diff --git a/crates/repl_expect/Cargo.toml b/crates/repl_expect/Cargo.toml new file mode 100644 index 0000000000..305077f466 --- /dev/null +++ b/crates/repl_expect/Cargo.toml @@ -0,0 +1,47 @@ +[package] +name = "roc_repl_expect" +description = "Supports evaluating expect and printing contextual information when they fail." + +authors.workspace = true +edition.workspace = true +license.workspace = true +version.workspace = true + +[dependencies] +roc_build = { path = "../compiler/build" } +roc_builtins = { path = "../compiler/builtins" } +roc_can = { path = "../compiler/can" } +roc_collections = { path = "../compiler/collections" } +roc_error_macros = { path = "../error_macros" } +roc_gen_llvm = { path = "../compiler/gen_llvm" } +roc_load = { path = "../compiler/load" } +roc_module = { path = "../compiler/module" } +roc_mono = { path = "../compiler/mono" } +roc_packaging = { path = "../packaging" } +roc_parse = { path = "../compiler/parse" } +roc_region = { path = "../compiler/region" } +roc_repl_eval = { path = "../repl_eval" } +roc_reporting = { path = "../reporting" } +roc_std = { path = "../roc_std" } +roc_target = { path = "../compiler/roc_target" } +roc_types = { path = "../compiler/types" } + +bumpalo.workspace = true +inkwell.workspace = true +libc.workspace = true +libloading.workspace = true +signal-hook.workspace = true +target-lexicon.workspace = true + +[dev-dependencies] +roc_build = { path = "../compiler/build", features = ["target-aarch64", "target-x86_64"] } + +indoc.workspace = true +pretty_assertions.workspace = true +strip-ansi-escapes.workspace = true +tempfile.workspace = true + + +[lib] +name = "roc_repl_expect" +path = "src/lib.rs" diff --git a/crates/repl_expect/src/app.rs b/crates/repl_expect/src/app.rs new file mode 100644 index 0000000000..ab8b64c559 --- /dev/null +++ b/crates/repl_expect/src/app.rs @@ -0,0 +1,148 @@ +use roc_parse::ast::Expr; +use roc_repl_eval::{ReplApp, ReplAppMemory}; +use roc_std::RocStr; +use roc_target::TargetInfo; + +pub(crate) struct ExpectMemory { + pub(crate) start: *const u8, +} + +macro_rules! deref_number { + ($name: ident, $t: ty) => { + fn $name(&self, addr: usize) -> $t { + let ptr = unsafe { self.start.add(addr) } as *const _; + unsafe { std::ptr::read_unaligned(ptr) } + } + }; +} + +impl ReplAppMemory for ExpectMemory { + fn deref_bool(&self, addr: usize) -> bool { + let ptr = unsafe { self.start.add(addr) } as *const u8; + let value = unsafe { std::ptr::read_unaligned(ptr) }; + + // bool values should only ever be 0 or 1 + debug_assert!(value == 0 || value == 1); + + value != 0 + } + + deref_number!(deref_u8, u8); + deref_number!(deref_u16, u16); + deref_number!(deref_u32, u32); + deref_number!(deref_u64, u64); + deref_number!(deref_u128, u128); + deref_number!(deref_usize, usize); + + deref_number!(deref_i8, i8); + deref_number!(deref_i16, i16); + deref_number!(deref_i32, i32); + deref_number!(deref_i64, i64); + deref_number!(deref_i128, i128); + deref_number!(deref_isize, isize); + + deref_number!(deref_f32, f32); + deref_number!(deref_f64, f64); + + fn deref_str(&self, addr: usize) -> &str { + const WIDTH: usize = 3 * std::mem::size_of::(); + + let last_byte_addr = addr + WIDTH - 1; + let last_byte = self.deref_i8(last_byte_addr); + + let is_small = last_byte < 0; + + if is_small { + let ptr = unsafe { self.start.add(addr) }; + let roc_str: &RocStr = unsafe { &*ptr.cast() }; + + roc_str.as_str() + } else { + let offset = self.deref_usize(addr); + let seamless_slice_mask = usize::MAX >> 1; + let length = + self.deref_usize(addr + std::mem::size_of::()) & seamless_slice_mask; + let _capacity = self.deref_usize(addr + 2 * std::mem::size_of::()); + + unsafe { + let ptr = self.start.add(offset); + let slice = std::slice::from_raw_parts(ptr, length); + + std::str::from_utf8_unchecked(slice) + } + } + } + + fn deref_pointer_with_tag_id(&self, addr: usize) -> (u16, u64) { + // because addr is an index/offset, we cannot use the low bits + let tag_id = self.deref_u32(addr); + let offset = self.deref_u32(addr + 4); + + (tag_id as _, offset as _) + } +} + +pub(crate) struct ExpectReplApp<'a> { + pub(crate) memory: &'a ExpectMemory, + pub(crate) offset: usize, +} + +impl<'a> ReplApp<'a> for ExpectReplApp<'a> { + type Memory = ExpectMemory; + + /// Run user code that returns a type with a `Builtin` layout + /// Size of the return value is statically determined from its Rust type + /// The `transform` callback takes the app's memory and the returned value + /// _main_fn_name is always the same and we don't use it here + fn call_function(&mut self, _main_fn_name: &str, mut transform: F) -> Expr<'a> + where + F: FnMut(&'a Self::Memory, Return) -> Expr<'a>, + Self::Memory: 'a, + { + let result: Return = unsafe { + let ptr = self.memory.start.add(self.offset); + let ptr: *const Return = std::mem::transmute(ptr); + ptr.read() + }; + + transform(self.memory, result) + } + + fn call_function_returns_roc_list(&mut self, main_fn_name: &str, transform: F) -> Expr<'a> + where + F: FnMut(&'a Self::Memory, (usize, usize, usize)) -> Expr<'a>, + Self::Memory: 'a, + { + self.call_function(main_fn_name, transform) + } + + fn call_function_returns_roc_str( + &mut self, + _target_info: TargetInfo, + main_fn_name: &str, + transform: F, + ) -> T + where + F: Fn(&'a Self::Memory, usize) -> T, + Self::Memory: 'a, + { + self.call_function_dynamic_size(main_fn_name, 24, transform) + } + + /// Run user code that returns a struct or union, whose size is provided as an argument + /// The `transform` callback takes the app's memory and the address of the returned value + /// _main_fn_name and _ret_bytes are only used for the CLI REPL. For Wasm they are compiled-in + /// to the test_wrapper function of the app itself + fn call_function_dynamic_size( + &mut self, + _main_fn_name: &str, + _ret_bytes: usize, + mut transform: F, + ) -> T + where + F: FnMut(&'a Self::Memory, usize) -> T, + Self::Memory: 'a, + { + transform(self.memory, self.offset) + } +} diff --git a/crates/repl_expect/src/lib.rs b/crates/repl_expect/src/lib.rs new file mode 100644 index 0000000000..752d0d8c0c --- /dev/null +++ b/crates/repl_expect/src/lib.rs @@ -0,0 +1,1256 @@ +//! Supports evaluating `expect` and printing contextual information when they fail. +#[cfg(not(windows))] +use { + roc_module::symbol::Interns, + roc_mono::{ + ir::ProcLayout, + layout::{GlobalLayoutInterner, LayoutCache, Niche}, + }, + roc_parse::ast::Expr, + roc_repl_eval::{eval::jit_to_ast, ReplAppMemory}, + roc_target::TargetInfo, + roc_types::subs::{Subs, Variable}, +}; + +#[cfg(not(windows))] +mod app; +#[cfg(not(windows))] +pub mod run; + +#[cfg(not(windows))] +use app::{ExpectMemory, ExpectReplApp}; + +#[cfg(not(windows))] +#[allow(clippy::too_many_arguments)] +pub fn get_values<'a>( + target_info: TargetInfo, + arena: &'a bumpalo::Bump, + subs: &Subs, + interns: &'a Interns, + layout_interner: &GlobalLayoutInterner<'a>, + start: *const u8, + start_offset: usize, + number_of_lookups: usize, +) -> (usize, Vec>, Vec) { + let mut result = Vec::with_capacity(number_of_lookups); + let mut result_vars = Vec::with_capacity(number_of_lookups); + + let memory = ExpectMemory { start }; + + let app = ExpectReplApp { + memory: arena.alloc(memory), + offset: start_offset, + }; + + let app = arena.alloc(app); + + for i in 0..number_of_lookups { + let size_of_lookup_header = 8 /* pointer to value */ + 4 /* type variable */; + + let start = app + .memory + .deref_usize(start_offset + i * size_of_lookup_header); + let variable = app.memory.deref_u32( + start_offset + i * size_of_lookup_header + 8, /* skip the pointer */ + ); + let variable = unsafe { Variable::from_index(variable) }; + + app.offset = start; + + let expr = { + // TODO: pass layout_cache to jit_to_ast directly + let mut layout_cache = LayoutCache::new(layout_interner.fork(), target_info); + let layout = layout_cache.from_var(arena, variable, subs).unwrap(); + + let proc_layout = ProcLayout { + arguments: &[], + result: layout, + niche: Niche::NONE, + }; + + jit_to_ast( + arena, + app, + "expect_repl_main_fn", + proc_layout, + variable, + subs, + interns, + layout_interner.fork(), + target_info, + ) + }; + + result.push(expr); + result_vars.push(variable); + } + + (app.offset, result, result_vars) +} + +#[cfg(not(windows))] +#[cfg(test)] +mod test { + use indoc::indoc; + use pretty_assertions::assert_eq; + use roc_error_macros::internal_error; + use roc_gen_llvm::{llvm::build::LlvmBackendMode, run_roc::RocCallResult, run_roc_dylib}; + use roc_load::{ExecutionMode, FunctionKind, LoadConfig, LoadMonomorphizedError, Threading}; + use roc_packaging::cache::RocCacheDir; + use roc_reporting::report::{RenderTarget, DEFAULT_PALETTE}; + use target_lexicon::Triple; + + use crate::run::expect_mono_module_to_dylib; + + use super::*; + + fn run_expect_test(source: &str, expected: &str) { + let arena = bumpalo::Bump::new(); + let arena = &arena; + + let triple = Triple::host(); + let target = &triple; + + let opt_level = roc_mono::ir::OptLevel::Normal; + let target_info = TargetInfo::from(target); + let function_kind = FunctionKind::LambdaSet; + + // Step 1: compile the app and generate the .o file + let src_dir = tempfile::tempdir().unwrap(); + let filename = src_dir.path().join("Test.roc"); + + std::fs::write(&filename, source).unwrap(); + + let load_config = LoadConfig { + target_info, + function_kind, + render: RenderTarget::ColorTerminal, + palette: DEFAULT_PALETTE, + threading: Threading::Single, + exec_mode: ExecutionMode::Test, + }; + let loaded = match roc_load::load_and_monomorphize_from_str( + arena, + filename, + source, + src_dir.path().to_path_buf(), + RocCacheDir::Disallowed, + load_config, + ) { + Ok(m) => m, + Err(LoadMonomorphizedError::ErrorModule(m)) => { + internal_error!("{:?}", (m.can_problems, m.type_problems)) + } + Err(e) => internal_error!("{e:?}"), + }; + + let mut loaded = loaded; + let mut expectations = std::mem::take(&mut loaded.expectations); + let loaded = loaded; + + let interns = loaded.interns.clone(); + + let (lib, expects, layout_interner) = expect_mono_module_to_dylib( + arena, + target.clone(), + loaded, + opt_level, + LlvmBackendMode::CliTest, + ) + .unwrap(); + + let arena = &bumpalo::Bump::new(); + let interns = arena.alloc(interns); + + const BUFFER_SIZE: usize = 1024; + + let mut shared_buffer = [0u8; BUFFER_SIZE]; + let mut memory = crate::run::ExpectMemory::from_slice(&mut shared_buffer); + + // communicate the mmapped name to zig/roc + let set_shared_buffer = run_roc_dylib!(lib, "set_shared_buffer", (*mut u8, usize), ()); + let mut result = RocCallResult::default(); + unsafe { set_shared_buffer((shared_buffer.as_mut_ptr(), BUFFER_SIZE), &mut result) }; + + let mut writer = Vec::with_capacity(1024); + let (_failed, _passed) = crate::run::run_expects_with_memory( + &mut writer, + RenderTarget::ColorTerminal, + arena, + interns, + &layout_interner.into_global(), + &lib, + &mut expectations, + expects, + &mut memory, + ) + .unwrap(); + + // Remove ANSI escape codes from the answer - for example: + // + // Before: "42 \u{1b}[35m:\u{1b}[0m Num *" + // After: "42 : Num *" + let bytes = strip_ansi_escapes::strip(writer).unwrap(); + let actual = String::from_utf8(bytes).unwrap(); + + if !actual.is_empty() { + // trim off the first line; it contains a path in a tempdir that + // changes between test runs + let p = actual.bytes().position(|c| c == b'\n').unwrap(); + let (_, x) = actual.split_at(p); + let x = x.trim(); + let expected = expected.trim_end(); + + if x != expected { + println!("{x}"); + } + + assert_eq!(expected, x); + } else { + assert_eq!(expected, actual); + } + } + + #[test] + fn equals_pass() { + run_expect_test( + r#" + app "test" provides [main] to "./platform" + + main = 0 + + expect 1 == 1 + "#, + "", + ); + } + + #[test] + fn equals_fail() { + run_expect_test( + indoc!( + r#" + app "test" provides [main] to "./platform" + + main = 0 + + expect 1 == 2 + "# + ), + indoc!( + r#" + This expectation failed: + + 5│ expect 1 == 2 + ^^^^^^^^^^^^^ + "# + ), + ); + } + + #[test] + fn lookup_integer() { + run_expect_test( + indoc!( + r#" + app "test" provides [main] to "./platform" + + main = 0 + + expect + a = 1 + b = 2 + + a == b + "# + ), + indoc!( + r#" + This expectation failed: + + 5│> expect + 6│> a = 1 + 7│> b = 2 + 8│> + 9│> a == b + + When it failed, these variables had these values: + + a : Num * + a = 1 + + b : Num * + b = 2 + "# + ), + ); + } + + #[test] + fn lookup_list_of_strings() { + run_expect_test( + indoc!( + r#" + app "test" provides [main] to "./platform" + + main = 0 + + expect + a = ["foo"] + b = ["a string so long that it cannot be short"] + + a == b + "# + ), + indoc!( + r#" + This expectation failed: + + 5│> expect + 6│> a = ["foo"] + 7│> b = ["a string so long that it cannot be short"] + 8│> + 9│> a == b + + When it failed, these variables had these values: + + a : List Str + a = ["foo"] + + b : List Str + b = ["a string so long that it cannot be short"] + "# + ), + ); + } + + #[test] + fn lookup_list_of_list_of_strings() { + run_expect_test( + indoc!( + r#" + app "test" provides [main] to "./platform" + + main = 0 + + expect + a = [["foo"], []] + b = [["a string so long that it cannot be short", "bar"]] + + a == b + "# + ), + indoc!( + r#" + This expectation failed: + + 5│> expect + 6│> a = [["foo"], []] + 7│> b = [["a string so long that it cannot be short", "bar"]] + 8│> + 9│> a == b + + When it failed, these variables had these values: + + a : List (List Str) + a = [["foo"], []] + + b : List (List Str) + b = [["a string so long that it cannot be short", "bar"]] + "# + ), + ); + } + + #[test] + fn lookup_copy_result() { + run_expect_test( + indoc!( + r#" + app "test" provides [main] to "./platform" + + main = 0 + + expect + items = [0, 1] + expected : Result I64 [OutOfBounds] + expected = Ok 42 + + List.get items 0 == expected + "# + ), + indoc!( + r#" + This expectation failed: + + 5│> expect + 6│> items = [0, 1] + 7│> expected : Result I64 [OutOfBounds] + 8│> expected = Ok 42 + 9│> + 10│> List.get items 0 == expected + + When it failed, these variables had these values: + + items : List (Int Signed64) + items = [0, 1] + + expected : Result I64 [OutOfBounds] + expected = Ok 42 + "# + ), + ); + } + + #[test] + fn lookup_clone_result() { + run_expect_test( + indoc!( + r#" + app "test" provides [main] to "./platform" + + main = 0 + + expect + a : Result Str Str + a = Ok "foo" + + b : Result Str Str + b = Err "bar" + + a == b + "# + ), + indoc!( + r#" + This expectation failed: + + 5│> expect + 6│> a : Result Str Str + 7│> a = Ok "foo" + 8│> + 9│> b : Result Str Str + 10│> b = Err "bar" + 11│> + 12│> a == b + + When it failed, these variables had these values: + + a : Result Str Str + a = Ok "foo" + + b : Result Str Str + b = Err "bar" + "# + ), + ); + } + + #[test] + fn lookup_copy_record() { + run_expect_test( + indoc!( + r#" + app "test" provides [main] to "./platform" + + main = 0 + + expect + vec1 = { x: 1u8, y: 2u8 } + vec2 = { x: 4u8, y: 8u8 } + + vec1 == vec2 + "# + ), + indoc!( + r#" + This expectation failed: + + 5│> expect + 6│> vec1 = { x: 1u8, y: 2u8 } + 7│> vec2 = { x: 4u8, y: 8u8 } + 8│> + 9│> vec1 == vec2 + + When it failed, these variables had these values: + + vec1 : { + x : U8, + y : U8, + } + vec1 = { x: 1, y: 2 } + + vec2 : { + x : U8, + y : U8, + } + vec2 = { x: 4, y: 8 } + "# + ), + ); + } + + #[test] + fn two_strings() { + run_expect_test( + indoc!( + r#" + app "test" provides [main] to "./platform" + + main = 0 + + expect + strings = ["Astra mortemque praestare gradatim", "Profundum et fundamentum"] + + strings == [] + "# + ), + indoc!( + r#" + This expectation failed: + + 5│> expect + 6│> strings = ["Astra mortemque praestare gradatim", "Profundum et fundamentum"] + 7│> + 8│> strings == [] + + When it failed, these variables had these values: + + strings : List Str + strings = ["Astra mortemque praestare gradatim", "Profundum et fundamentum"] + "# + ), + ); + } + + #[test] + fn compare_long_strings() { + run_expect_test( + indoc!( + r#" + app "test" provides [main] to "./platform" + + main = 0 + + expect + a = "Astra mortemque praestare gradatim" + b = "Profundum et fundamentum" + + a == b + "# + ), + indoc!( + r#" + This expectation failed: + + 5│> expect + 6│> a = "Astra mortemque praestare gradatim" + 7│> b = "Profundum et fundamentum" + 8│> + 9│> a == b + + When it failed, these variables had these values: + + a : Str + a = "Astra mortemque praestare gradatim" + + b : Str + b = "Profundum et fundamentum" + "# + ), + ); + } + + #[test] + fn struct_with_strings() { + run_expect_test( + indoc!( + r#" + app "test" provides [main] to "./platform" + + main = 0 + + expect + a = { + utopia: "Astra mortemque praestare gradatim", + brillist: "Profundum et fundamentum", + } + + a != a + "# + ), + indoc!( + r#" + This expectation failed: + + 5│> expect + 6│> a = { + 7│> utopia: "Astra mortemque praestare gradatim", + 8│> brillist: "Profundum et fundamentum", + 9│> } + 10│> + 11│> a != a + + When it failed, these variables had these values: + + a : { + brillist : Str, + utopia : Str, + } + a = { brillist: "Profundum et fundamentum", utopia: "Astra mortemque praestare gradatim" } + "# + ), + ); + } + + #[test] + fn box_with_strings() { + run_expect_test( + indoc!( + r#" + app "test" provides [main] to "./platform" + + main = 0 + + expect + a = Box.box "Astra mortemque praestare gradatim" + b = Box.box "Profundum et fundamentum" + + a == b + "# + ), + indoc!( + r#" + This expectation failed: + + 5│> expect + 6│> a = Box.box "Astra mortemque praestare gradatim" + 7│> b = Box.box "Profundum et fundamentum" + 8│> + 9│> a == b + + When it failed, these variables had these values: + + a : Box Str + a = Box.box "Astra mortemque praestare gradatim" + + b : Box Str + b = Box.box "Profundum et fundamentum" + "# + ), + ); + } + + #[test] + fn result_with_strings() { + run_expect_test( + indoc!( + r#" + app "test" provides [main] to "./platform" + + main = 0 + + expect + a = Ok "Astra mortemque praestare gradatim" + b = Err "Profundum et fundamentum" + + a == b + "# + ), + indoc!( + r#" + This expectation failed: + + 5│> expect + 6│> a = Ok "Astra mortemque praestare gradatim" + 7│> b = Err "Profundum et fundamentum" + 8│> + 9│> a == b + + When it failed, these variables had these values: + + a : [ + Err Str, + Ok Str, + ] + a = Ok "Astra mortemque praestare gradatim" + + b : [ + Err Str, + Ok Str, + ] + b = Err "Profundum et fundamentum" + "# + ), + ); + } + + #[test] + fn linked_list() { + run_expect_test( + indoc!( + r#" + app "test" provides [main] to "./platform" + + main = 0 + + ConsList a : [ Nil, Cons a (ConsList a) ] + + cons = \list, x -> Cons x list + + expect + a : ConsList Str + a = Nil + + b : ConsList Str + b = Nil + |> cons "Astra mortemque praestare gradatim" + |> cons "Profundum et fundamentum" + + a == b + "# + ), + indoc!( + r#" + This expectation failed: + + 9│> expect + 10│> a : ConsList Str + 11│> a = Nil + 12│> + 13│> b : ConsList Str + 14│> b = Nil + 15│> |> cons "Astra mortemque praestare gradatim" + 16│> |> cons "Profundum et fundamentum" + 17│> + 18│> a == b + + When it failed, these variables had these values: + + a : ConsList Str + a = Nil + + b : ConsList Str + b = Cons "Profundum et fundamentum" (Cons "Astra mortemque praestare gradatim" Nil) + "# + ), + ); + } + + #[test] + fn nullable_tree() { + run_expect_test( + indoc!( + r#" + app "test" provides [main] to "./platform" + + main = 0 + + Tree a : [ Empty, Leaf a, Node (Tree a) (Tree a) ] + + cons = \list, x -> Cons x list + + expect + a : Tree Str + a = Leaf "Astra mortemque praestare gradatim" + + b : Tree Str + b = Node Empty Empty + + a == b + "# + ), + indoc!( + r#" + This expectation failed: + + 9│> expect + 10│> a : Tree Str + 11│> a = Leaf "Astra mortemque praestare gradatim" + 12│> + 13│> b : Tree Str + 14│> b = Node Empty Empty + 15│> + 16│> a == b + + When it failed, these variables had these values: + + a : Tree Str + a = Leaf "Astra mortemque praestare gradatim" + + b : Tree Str + b = Node Empty Empty + "# + ), + ); + } + + #[test] + fn recursive_tree() { + run_expect_test( + indoc!( + r#" + app "test" provides [main] to "./platform" + + main = 0 + + Tree a : [ Leaf a, Node (Tree a) (Tree a) ] + + expect + a : Tree Str + a = Leaf "Astra mortemque praestare gradatim" + + b : Tree Str + b = Node (Leaf "a") (Leaf "b") + + a == b + "# + ), + indoc!( + r#" + This expectation failed: + + 7│> expect + 8│> a : Tree Str + 9│> a = Leaf "Astra mortemque praestare gradatim" + 10│> + 11│> b : Tree Str + 12│> b = Node (Leaf "a") (Leaf "b") + 13│> + 14│> a == b + + When it failed, these variables had these values: + + a : Tree Str + a = Leaf "Astra mortemque praestare gradatim" + + b : Tree Str + b = Node (Leaf "a") (Leaf "b") + "# + ), + ); + } + + #[test] + fn rose_tree() { + run_expect_test( + indoc!( + r#" + app "test" provides [main] to "./platform" + + main = 0 + + RoseTree a : [Tree a (List (RoseTree a))] + + expect + a : RoseTree Str + a = Tree "Astra mortemque praestare gradatim" [] + + b : RoseTree Str + b = Tree "foo" [ Tree "bar" [] ] + + a == b + "# + ), + indoc!( + r#" + This expectation failed: + + 7│> expect + 8│> a : RoseTree Str + 9│> a = Tree "Astra mortemque praestare gradatim" [] + 10│> + 11│> b : RoseTree Str + 12│> b = Tree "foo" [ Tree "bar" [] ] + 13│> + 14│> a == b + + When it failed, these variables had these values: + + a : RoseTree Str + a = Tree "Astra mortemque praestare gradatim" [] + + b : RoseTree Str + b = Tree "foo" [Tree "bar" []] + "# + ), + ); + } + + #[test] + fn big_recursive_tag_copied_back() { + run_expect_test( + indoc!( + r#" + interface Test exposes [] imports [] + + NonEmpty := [ + First Str U8, + Next (List { item: Str, rest: NonEmpty }), + ] + + expect + nonEmpty = + a = "abcdefgh" + b = @NonEmpty (First "ijkl" 67u8) + c = Next [{ item: a, rest: b }] + @NonEmpty c + + when nonEmpty is + _ -> Bool.false + "# + ), + indoc!( + r#" + This expectation failed: + + 8│> expect + 9│> nonEmpty = + 10│> a = "abcdefgh" + 11│> b = @NonEmpty (First "ijkl" 67u8) + 12│> c = Next [{ item: a, rest: b }] + 13│> @NonEmpty c + 14│> + 15│> when nonEmpty is + 16│> _ -> Bool.false + + When it failed, these variables had these values: + + nonEmpty : NonEmpty + nonEmpty = @NonEmpty (Next [{ item: "abcdefgh", rest: @NonEmpty (First "ijkl" 67) }]) + "# + ), + ); + } + + #[test] + fn arg_parser() { + run_expect_test( + indoc!( + r#" + interface Test exposes [] imports [] + + makeForcer : {} -> (Str -> U8) + makeForcer = \{} -> \_ -> 2u8 + + expect + forcer = makeForcer {} + + case = "" + + forcer case == 5u8 + "# + ), + indoc!( + r#" + This expectation failed: + + 6│> expect + 7│> forcer = makeForcer {} + 8│> + 9│> case = "" + 10│> + 11│> forcer case == 5u8 + + When it failed, these variables had these values: + + case : Str + case = "" + "# + ), + ); + } + + #[test] + fn issue_i4389() { + run_expect_test( + indoc!( + r#" + interface Test exposes [] imports [] + + expect + totalCount = \{} -> 1u8 + totalCount {} == 96u8 + "# + ), + indoc!( + r#" + This expectation failed: + + 3│> expect + 4│> totalCount = \{} -> 1u8 + 5│> totalCount {} == 96u8 + "# + ), + ); + } + + #[test] + fn adjacent_lists() { + run_expect_test( + indoc!( + r#" + interface Test exposes [] imports [] + + expect + actual : { headers: List U8, body: List U8, x: List U8 } + actual = { + body: [], + headers: [], + x: [], + } + + expected : { headers: List U8, body: List U8, x: List U8 } + expected = { + body: [ 42, 43, 44 ], + headers: [15, 16, 17], + x: [115, 116, 117], + } + actual == expected + "# + ), + indoc!( + r#" + This expectation failed: + + 3│> expect + 4│> actual : { headers: List U8, body: List U8, x: List U8 } + 5│> actual = { + 6│> body: [], + 7│> headers: [], + 8│> x: [], + 9│> } + 10│> + 11│> expected : { headers: List U8, body: List U8, x: List U8 } + 12│> expected = { + 13│> body: [ 42, 43, 44 ], + 14│> headers: [15, 16, 17], + 15│> x: [115, 116, 117], + 16│> } + 17│> actual == expected + + When it failed, these variables had these values: + + actual : { + body : List (Int Unsigned8), + headers : List (Int Unsigned8), + x : List (Int Unsigned8), + } + actual = { body: [], headers: [], x: [] } + + expected : { + body : List (Int Unsigned8), + headers : List (Int Unsigned8), + x : List (Int Unsigned8), + } + expected = { body: [42, 43, 44], headers: [15, 16, 17], x: [115, 116, 117] } + "# + ), + ); + } + + #[test] + fn record_field_ordering() { + run_expect_test( + indoc!( + r#" + interface Test exposes [] imports [] + + Request : { + fieldA : [Get, Post], + fieldB : Str, + } + + expect + + actual : Request + actual = { + fieldA: Get, + fieldB: "/things?id=2", + } + + expected : Request + expected = { + fieldA: Get, + fieldB: "/things?id=1", + } + actual == expected + "# + ), + indoc!( + r#" + This expectation failed: + + 8│> expect + 9│> + 10│> actual : Request + 11│> actual = { + 12│> fieldA: Get, + 13│> fieldB: "/things?id=2", + 14│> } + 15│> + 16│> expected : Request + 17│> expected = { + 18│> fieldA: Get, + 19│> fieldB: "/things?id=1", + 20│> } + 21│> actual == expected + + When it failed, these variables had these values: + + actual : Request + actual = { fieldA: Get, fieldB: "/things?id=2" } + + expected : Request + expected = { fieldA: Get, fieldB: "/things?id=1" } + + "# + ), + ); + } + + #[test] + fn tag_payloads_of_different_size() { + run_expect_test( + indoc!( + r#" + interface Test exposes [] imports [] + + actual : [Leftover (List U8), TooShort] + actual = Leftover [49, 93] + + expect + expected : [Leftover (List U8), TooShort] + expected = TooShort + + actual == expected + "# + ), + indoc!( + r#" + This expectation failed: + + 6│> expect + 7│> expected : [Leftover (List U8), TooShort] + 8│> expected = TooShort + 9│> + 10│> actual == expected + + When it failed, these variables had these values: + + expected : [ + Leftover (List U8), + TooShort, + ] + expected = TooShort + "# + ), + ); + } + + #[test] + fn extra_offset_in_tag_union() { + run_expect_test( + indoc!( + r#" + interface Test exposes [] imports [] + + actual : Result Str U64 + actual = Err 1 + + expect + expected : Result Str U64 + expected = Ok "foobar" + + actual == expected + "# + ), + indoc!( + r#" + This expectation failed: + + 6│> expect + 7│> expected : Result Str U64 + 8│> expected = Ok "foobar" + 9│> + 10│> actual == expected + + When it failed, these variables had these values: + + expected : Result Str U64 + expected = Ok "foobar" + "# + ), + ); + } + + #[test] + fn tuple_access() { + run_expect_test( + indoc!( + r#" + interface Test exposes [] imports [] + + expect + t = ("One", "Two") + t.1 == "One" + "# + ), + indoc!( + r#" + This expectation failed: + + 3│> expect + 4│> t = ("One", "Two") + 5│> t.1 == "One" + + When it failed, these variables had these values: + + t : ( + Str, + Str, + )a + t = ("One", "Two") + "# + ), + ); + } + + #[test] + fn match_on_opaque_number_type() { + run_expect_test( + indoc!( + r#" + interface Test exposes [] imports [] + + hexToByte : U8, U8 -> U8 + hexToByte = \upper, lower -> + Num.bitwiseOr (Num.shiftRightBy upper 4) lower + + expect + actual = hexToByte 7 4 + expected = 't' + actual == expected + "# + ), + indoc!( + r#" + This expectation failed: + + 7│> expect + 8│> actual = hexToByte 7 4 + 9│> expected = 't' + 10│> actual == expected + + When it failed, these variables had these values: + + actual : U8 + actual = 4 + + expected : Int Unsigned8 + expected = 116 + "# + ), + ); + } +} diff --git a/crates/repl_expect/src/run.rs b/crates/repl_expect/src/run.rs new file mode 100644 index 0000000000..202086aae8 --- /dev/null +++ b/crates/repl_expect/src/run.rs @@ -0,0 +1,738 @@ +use std::{ + os::unix::process::parent_id, + sync::{ + atomic::{AtomicBool, AtomicU32}, + Arc, + }, +}; + +use bumpalo::collections::Vec as BumpVec; +use bumpalo::Bump; +use inkwell::context::Context; +use roc_build::link::llvm_module_to_dylib; +use roc_can::expr::ExpectLookup; +use roc_collections::{MutSet, VecMap}; +use roc_error_macros::internal_error; +use roc_gen_llvm::{ + llvm::{build::LlvmBackendMode, externs::add_default_roc_externs}, + run_roc::RocCallResult, + run_roc_dylib, +}; +use roc_load::{Expectations, MonomorphizedModule}; +use roc_module::symbol::{Interns, ModuleId, Symbol}; +use roc_mono::{ + ir::OptLevel, + layout::{GlobalLayoutInterner, STLayoutInterner}, +}; +use roc_region::all::Region; +use roc_reporting::{error::expect::Renderer, report::RenderTarget}; +use roc_target::TargetInfo; +use roc_types::subs::Subs; +use target_lexicon::Triple; + +pub struct ExpectMemory<'a> { + ptr: *mut u8, + length: usize, + shm_name: Option, + _marker: std::marker::PhantomData<&'a ()>, +} + +impl<'a> ExpectMemory<'a> { + const SHM_SIZE: usize = 1024; + + #[cfg(test)] + pub(crate) fn from_slice(slice: &mut [u8]) -> Self { + Self { + ptr: slice.as_mut_ptr(), + length: slice.len(), + shm_name: None, + _marker: std::marker::PhantomData, + } + } + + pub fn create_or_reuse_mmap(shm_name: &str) -> Self { + let cstring = std::ffi::CString::new(shm_name).unwrap(); + Self::mmap_help(cstring, libc::O_RDWR | libc::O_CREAT) + } + + fn reuse_mmap(&mut self) -> Option { + let shm_name = self.shm_name.as_ref()?.clone(); + Some(Self::mmap_help(shm_name, libc::O_RDWR)) + } + + fn mmap_help(cstring: std::ffi::CString, shm_flags: i32) -> Self { + let ptr = unsafe { + let shared_fd = libc::shm_open(cstring.as_ptr().cast(), shm_flags, 0o666); + if shared_fd == -1 { + internal_error!("failed to shm_open fd"); + } + + let mut stat: libc::stat = std::mem::zeroed(); + if libc::fstat(shared_fd, &mut stat) == -1 { + internal_error!("failed to stat shared file, does it exist?"); + } + if stat.st_size < Self::SHM_SIZE as _ + && libc::ftruncate(shared_fd, Self::SHM_SIZE as _) == -1 + { + internal_error!("failed to truncate shared file, are the permissions wrong?"); + } + + let ptr = libc::mmap( + std::ptr::null_mut(), + Self::SHM_SIZE, + libc::PROT_WRITE | libc::PROT_READ, + libc::MAP_SHARED, + shared_fd, + 0, + ); + + if ptr as usize == usize::MAX { + // ptr = -1 + roc_error_macros::internal_error!("failed to mmap shared pointer") + } + + // fill the buffer with a fill pattern + libc::memset(ptr, 0xAA, Self::SHM_SIZE); + + ptr + }; + + // puts in the initial header + let _ = ExpectSequence::new(ptr as *mut u8); + + Self { + ptr: ptr.cast(), + length: Self::SHM_SIZE, + shm_name: Some(cstring), + _marker: std::marker::PhantomData, + } + } + + fn set_shared_buffer(&mut self, lib: &libloading::Library) { + let set_shared_buffer = run_roc_dylib!(lib, "set_shared_buffer", (*mut u8, usize), ()); + let mut result = RocCallResult::default(); + unsafe { set_shared_buffer((self.ptr, self.length), &mut result) }; + } + + pub fn wait_for_child(&self, sigchld: Arc) -> ChildProcessMsg { + let sequence = ExpectSequence { ptr: self.ptr }; + sequence.wait_for_child(sigchld) + } + + pub fn reset(&mut self) { + let mut sequence = ExpectSequence { ptr: self.ptr }; + sequence.reset(); + } +} + +#[allow(clippy::too_many_arguments)] +pub fn run_inline_expects<'a, W: std::io::Write>( + writer: &mut W, + render_target: RenderTarget, + arena: &'a Bump, + interns: &'a Interns, + layout_interner: &GlobalLayoutInterner<'a>, + lib: &libloading::Library, + expectations: &mut VecMap, + expects: ExpectFunctions<'_>, +) -> std::io::Result<(usize, usize)> { + let shm_name = format!("/roc_expect_buffer_{}", std::process::id()); + let mut memory = ExpectMemory::create_or_reuse_mmap(&shm_name); + + run_expects_with_memory( + writer, + render_target, + arena, + interns, + layout_interner, + lib, + expectations, + expects, + &mut memory, + ) +} + +#[allow(clippy::too_many_arguments)] +pub fn run_toplevel_expects<'a, W: std::io::Write>( + writer: &mut W, + render_target: RenderTarget, + arena: &'a Bump, + interns: &'a Interns, + layout_interner: &GlobalLayoutInterner<'a>, + lib: &libloading::Library, + expectations: &mut VecMap, + expects: ExpectFunctions<'_>, +) -> std::io::Result<(usize, usize)> { + let shm_name = format!("/roc_expect_buffer_{}", std::process::id()); + let mut memory = ExpectMemory::create_or_reuse_mmap(&shm_name); + + run_expects_with_memory( + writer, + render_target, + arena, + interns, + layout_interner, + lib, + expectations, + expects, + &mut memory, + ) +} + +#[allow(clippy::too_many_arguments)] +pub(crate) fn run_expects_with_memory<'a, W: std::io::Write>( + writer: &mut W, + render_target: RenderTarget, + arena: &'a Bump, + interns: &'a Interns, + layout_interner: &GlobalLayoutInterner<'a>, + lib: &libloading::Library, + expectations: &mut VecMap, + expects: ExpectFunctions<'_>, + memory: &mut ExpectMemory, +) -> std::io::Result<(usize, usize)> { + let mut failed = 0; + let mut passed = 0; + + for expect in expects.fx { + let result = run_expect_fx( + writer, + render_target, + arena, + interns, + layout_interner, + lib, + expectations, + memory, + expect, + )?; + + match result { + true => passed += 1, + false => failed += 1, + } + } + + memory.set_shared_buffer(lib); + + for expect in expects.pure { + let result = run_expect_pure( + writer, + render_target, + arena, + interns, + layout_interner, + lib, + expectations, + memory, + expect, + )?; + + match result { + true => passed += 1, + false => failed += 1, + } + } + + Ok((failed, passed)) +} + +#[allow(clippy::too_many_arguments)] +fn run_expect_pure<'a, W: std::io::Write>( + writer: &mut W, + render_target: RenderTarget, + arena: &'a Bump, + interns: &'a Interns, + layout_interner: &GlobalLayoutInterner<'a>, + lib: &libloading::Library, + expectations: &mut VecMap, + shared_memory: &mut ExpectMemory, + expect: ToplevelExpect<'_>, +) -> std::io::Result { + use roc_gen_llvm::try_run_jit_function; + + let sequence = ExpectSequence::new(shared_memory.ptr.cast()); + + let result: Result<(), (String, _)> = try_run_jit_function!(lib, expect.name, (), |v: ()| v); + + let shared_memory_ptr: *const u8 = shared_memory.ptr.cast(); + + if result.is_err() || sequence.count_failures() > 0 { + let module_id = expect.symbol.module_id(); + let data = expectations.get_mut(&module_id).unwrap(); + + let path = &data.path; + let filename = data.path.to_owned(); + let source = std::fs::read_to_string(path).unwrap(); + + let renderer = Renderer::new(arena, interns, render_target, module_id, filename, &source); + + if let Err((roc_panic_message, _roc_panic_tag)) = result { + renderer.render_panic(writer, &roc_panic_message, expect.region)?; + } else { + let mut offset = ExpectSequence::START_OFFSET; + + for _ in 0..sequence.count_failures() { + offset += render_expect_failure( + writer, + &renderer, + arena, + Some(expect), + expectations, + interns, + layout_interner, + shared_memory_ptr, + offset, + )?; + } + } + + writeln!(writer)?; + + Ok(false) + } else { + Ok(true) + } +} + +#[allow(clippy::too_many_arguments)] +fn run_expect_fx<'a, W: std::io::Write>( + writer: &mut W, + render_target: RenderTarget, + arena: &'a Bump, + interns: &'a Interns, + layout_interner: &GlobalLayoutInterner<'a>, + lib: &libloading::Library, + expectations: &mut VecMap, + parent_memory: &mut ExpectMemory, + expect: ToplevelExpect<'_>, +) -> std::io::Result { + use signal_hook::{consts::signal::SIGCHLD, consts::signal::SIGUSR1, iterator::Signals}; + + let mut signals = Signals::new([SIGCHLD, SIGUSR1]).unwrap(); + + match unsafe { libc::fork() } { + 0 => unsafe { + // we are the child + + use roc_gen_llvm::try_run_jit_function; + + let mut child_memory = parent_memory.reuse_mmap().unwrap(); + + let sequence = ExpectSequence::new(child_memory.ptr); + + child_memory.set_shared_buffer(lib); + + let result: Result<(), (String, _)> = + try_run_jit_function!(lib, expect.name, (), |v: ()| v); + + if let Err((msg, _)) = result { + internal_error!("roc panic {msg}"); + } + + if sequence.count_failures() > 0 { + libc::kill(parent_id() as _, SIGUSR1); + } + + std::process::exit(0) + }, + -1 => { + // something failed + + // Display a human-friendly error message + println!("Error {:?}", std::io::Error::last_os_error()); + + std::process::exit(1) + } + 1.. => { + let mut has_succeeded = true; + + for sig in &mut signals { + match sig { + SIGCHLD => { + // done! + return Ok(has_succeeded); + } + SIGUSR1 => { + // this is the signal we use for an expect failure. Let's see what the child told us + has_succeeded = false; + + let frame = + ExpectFrame::at_offset(parent_memory.ptr, ExpectSequence::START_OFFSET); + let module_id = frame.module_id; + + let data = expectations.get_mut(&module_id).unwrap(); + let filename = data.path.to_owned(); + let source = std::fs::read_to_string(&data.path).unwrap(); + + let renderer = Renderer::new( + arena, + interns, + render_target, + module_id, + filename, + &source, + ); + + render_expect_failure( + writer, + &renderer, + arena, + None, + expectations, + interns, + layout_interner, + parent_memory.ptr, + ExpectSequence::START_OFFSET, + )?; + } + _ => println!("received signal {sig}"), + } + } + + Ok(true) + } + _ => unreachable!(), + } +} + +pub fn render_expects_in_memory<'a>( + writer: &mut impl std::io::Write, + arena: &'a Bump, + expectations: &mut VecMap, + interns: &'a Interns, + layout_interner: &GlobalLayoutInterner<'a>, + memory: &ExpectMemory, +) -> std::io::Result { + let shared_ptr = memory.ptr; + + let frame = ExpectFrame::at_offset(shared_ptr, ExpectSequence::START_OFFSET); + let module_id = frame.module_id; + + let data = expectations.get_mut(&module_id).unwrap(); + let filename = data.path.to_owned(); + let source = std::fs::read_to_string(&data.path).unwrap(); + + let renderer = Renderer::new( + arena, + interns, + RenderTarget::ColorTerminal, + module_id, + filename, + &source, + ); + + render_expect_failure( + writer, + &renderer, + arena, + None, + expectations, + interns, + layout_interner, + shared_ptr, + ExpectSequence::START_OFFSET, + ) +} + +fn split_expect_lookups(subs: &Subs, lookups: &[ExpectLookup]) -> Vec { + lookups + .iter() + .filter_map( + |ExpectLookup { + symbol, + var, + ability_info: _, + }| { + // mono will have dropped lookups that resolve to functions, so we should not keep + // them either. + if subs.is_function(*var) { + None + } else { + Some(*symbol) + } + }, + ) + .collect() +} + +#[allow(clippy::too_many_arguments)] +fn render_expect_failure<'a>( + writer: &mut impl std::io::Write, + renderer: &Renderer, + arena: &'a Bump, + expect: Option, + expectations: &mut VecMap, + interns: &'a Interns, + layout_interner: &GlobalLayoutInterner<'a>, + start: *const u8, + offset: usize, +) -> std::io::Result { + // we always run programs as the host + let target_info = (&target_lexicon::Triple::host()).into(); + + let frame = ExpectFrame::at_offset(start, offset); + let module_id = frame.module_id; + + let failure_region = frame.region; + let expect_region = expect.map(|e| e.region); + + let data = expectations.get_mut(&module_id).unwrap(); + + let current = match data.expectations.get(&failure_region) { + None => internal_error!("region {failure_region:?} not in list of expects"), + Some(current) => current, + }; + + let symbols = split_expect_lookups(&data.subs, current); + + let (offset, expressions, variables) = crate::get_values( + target_info, + arena, + &data.subs, + interns, + layout_interner, + start, + frame.start_offset, + symbols.len(), + ); + + renderer.render_failure( + writer, + &mut data.subs, + &symbols, + &variables, + &expressions, + expect_region, + failure_region, + )?; + + Ok(offset) +} + +struct ExpectSequence { + ptr: *const u8, +} + +impl ExpectSequence { + const START_OFFSET: usize = 8 + 8 + 8; + + const COUNT_INDEX: usize = 0; + const OFFSET_INDEX: usize = 1; + const LOCK_INDEX: usize = 2; + + fn new(ptr: *mut u8) -> Self { + unsafe { + let ptr = ptr as *mut usize; + std::ptr::write_unaligned(ptr.add(Self::COUNT_INDEX), 0); + std::ptr::write_unaligned(ptr.add(Self::OFFSET_INDEX), Self::START_OFFSET); + std::ptr::write_unaligned(ptr.add(Self::LOCK_INDEX), 0); + } + + Self { + ptr: ptr as *const u8, + } + } + + fn count_failures(&self) -> usize { + unsafe { *(self.ptr as *const usize).add(Self::COUNT_INDEX) } + } + + fn wait_for_child(&self, sigchld: Arc) -> ChildProcessMsg { + use std::sync::atomic::Ordering; + let ptr = self.ptr as *const u32; + let atomic_ptr: *const AtomicU32 = unsafe { ptr.add(5).cast() }; + let atomic = unsafe { &*atomic_ptr }; + + loop { + if sigchld.load(Ordering::Relaxed) { + break ChildProcessMsg::Terminate; + } + + match atomic.load(Ordering::Acquire) { + 0 => std::hint::spin_loop(), + 1 => break ChildProcessMsg::Expect, + n => internal_error!("invalid atomic value set by the child: {n:#x}"), + } + } + } + + fn reset(&mut self) { + unsafe { + let ptr = self.ptr as *mut usize; + std::ptr::write_unaligned(ptr.add(Self::COUNT_INDEX), 0); + std::ptr::write_unaligned(ptr.add(Self::OFFSET_INDEX), Self::START_OFFSET); + std::ptr::write_unaligned(ptr.add(Self::LOCK_INDEX), 0); + } + } +} + +pub enum ChildProcessMsg { + Expect = 1, + Terminate = 2, +} + +struct ExpectFrame { + region: Region, + module_id: ModuleId, + + start_offset: usize, +} + +impl ExpectFrame { + fn at_offset(start: *const u8, offset: usize) -> Self { + let region_bytes: [u8; 8] = unsafe { *(start.add(offset).cast()) }; + let region: Region = unsafe { std::mem::transmute(region_bytes) }; + + let module_id_bytes: [u8; 4] = unsafe { *(start.add(offset + 8).cast()) }; + let module_id: ModuleId = unsafe { std::mem::transmute(module_id_bytes) }; + + // skip to frame + let start_offset = offset + 8 + 4; + + Self { + region, + module_id, + start_offset, + } + } +} + +#[derive(Debug, Clone, Copy)] +pub struct ToplevelExpect<'a> { + pub name: &'a str, + pub symbol: Symbol, + pub region: Region, +} + +#[derive(Debug)] +pub struct ExpectFunctions<'a> { + pub pure: BumpVec<'a, ToplevelExpect<'a>>, + pub fx: BumpVec<'a, ToplevelExpect<'a>>, +} + +pub fn expect_mono_module_to_dylib<'a>( + arena: &'a Bump, + target: Triple, + loaded: MonomorphizedModule<'a>, + opt_level: OptLevel, + mode: LlvmBackendMode, +) -> Result< + ( + libloading::Library, + ExpectFunctions<'a>, + STLayoutInterner<'a>, + ), + libloading::Error, +> { + let target_info = TargetInfo::from(&target); + + let MonomorphizedModule { + toplevel_expects, + procedures, + interns, + layout_interner, + .. + } = loaded; + + let context = Context::create(); + let builder = context.create_builder(); + let module = arena.alloc(roc_gen_llvm::llvm::build::module_from_builtins( + &target, &context, "", + )); + + let module = arena.alloc(module); + let (module_pass, _function_pass) = + roc_gen_llvm::llvm::build::construct_optimization_passes(module, opt_level); + + let (dibuilder, compile_unit) = roc_gen_llvm::llvm::build::Env::new_debug_info(module); + + // Compile and add all the Procs before adding main + let env = roc_gen_llvm::llvm::build::Env { + arena, + builder: &builder, + dibuilder: &dibuilder, + compile_unit: &compile_unit, + context: &context, + interns, + module, + target_info, + mode, + // important! we don't want any procedures to get the C calling convention + exposed_to_host: MutSet::default(), + }; + + // Add roc_alloc, roc_realloc, and roc_dealloc, since the repl has no + // platform to provide them. + add_default_roc_externs(&env); + + let capacity = toplevel_expects.pure.len() + toplevel_expects.fx.len(); + let mut expect_symbols = BumpVec::with_capacity_in(capacity, env.arena); + + expect_symbols.extend(toplevel_expects.pure.keys().copied()); + expect_symbols.extend(toplevel_expects.fx.keys().copied()); + + let expect_names = roc_gen_llvm::llvm::build::build_procedures_expose_expects( + &env, + &layout_interner, + opt_level, + expect_symbols.into_bump_slice(), + procedures, + ); + + let expects_fx = bumpalo::collections::Vec::from_iter_in( + toplevel_expects + .fx + .into_iter() + .zip(expect_names.iter().skip(toplevel_expects.pure.len())) + .map(|((symbol, region), name)| ToplevelExpect { + symbol, + region, + name, + }), + env.arena, + ); + + let expects_pure = bumpalo::collections::Vec::from_iter_in( + toplevel_expects + .pure + .into_iter() + .zip(expect_names.iter()) + .map(|((symbol, region), name)| ToplevelExpect { + symbol, + region, + name, + }), + env.arena, + ); + + let expects = ExpectFunctions { + pure: expects_pure, + fx: expects_fx, + }; + + env.dibuilder.finalize(); + + // we don't use the debug info, and it causes weird errors. + module.strip_debug_info(); + + // Uncomment this to see the module's un-optimized LLVM instruction output: + // env.module.print_to_stderr(); + + module_pass.run_on(env.module); + + // Uncomment this to see the module's optimized LLVM instruction output: + // env.module.print_to_stderr(); + + // Verify the module + if let Err(errors) = env.module.verify() { + let path = std::env::temp_dir().join("test.ll"); + env.module.print_to_file(&path).unwrap(); + internal_error!( + "Errors defining module:\n{}\n\nUncomment things nearby to see more details. IR written to `{:?}`", + errors.to_string(), path, + ); + } + + llvm_module_to_dylib(env.module, &target, opt_level).map(|lib| (lib, expects, layout_interner)) +} diff --git a/crates/repl_test/Cargo.toml b/crates/repl_test/Cargo.toml new file mode 100644 index 0000000000..be2755ff1d --- /dev/null +++ b/crates/repl_test/Cargo.toml @@ -0,0 +1,40 @@ +[package] +name = "repl_test" +description = "Tests the roc REPL." + +authors.workspace = true +edition.workspace = true +license.workspace = true +version.workspace = true + +[build-dependencies] +roc_cli = { path = "../cli" } + +[dev-dependencies] +roc_build = { path = "../compiler/build" } +roc_repl_cli = { path = "../repl_cli" } +roc_repl_ui = { path = "../repl_ui" } +roc_test_utils = { path = "../test_utils" } +roc_wasm_interp = { path = "../wasm_interp" } +roc_reporting = { path = "../reporting" } +roc_target = { path = "../compiler/roc_target" } + +bumpalo.workspace = true +indoc.workspace = true +strip-ansi-escapes.workspace = true +target-lexicon.workspace = true +rustyline.workspace = true + +[features] +default = ["target-aarch64", "target-x86_64", "target-wasm32"] +target-aarch64 = ["roc_build/target-aarch64", "roc_repl_cli/target-aarch64"] +target-arm = ["roc_build/target-arm", "roc_repl_cli/target-arm"] +target-wasm32 = ["roc_build/target-wasm32"] +target-x86 = ["roc_build/target-x86", "roc_repl_cli/target-x86"] +target-x86_64 = ["roc_build/target-x86_64", "roc_repl_cli/target-x86_64"] +wasm = ["target-wasm32"] + +target-all = ["target-aarch64", "target-arm", "target-x86", "target-x86_64", "target-wasm32"] + +[package.metadata.cargo-udeps.ignore] +development = ["roc_wasm_interp"] \ No newline at end of file diff --git a/crates/repl_test/src/cli.rs b/crates/repl_test/src/cli.rs new file mode 100644 index 0000000000..c61677417e --- /dev/null +++ b/crates/repl_test/src/cli.rs @@ -0,0 +1,161 @@ +use roc_repl_cli::WELCOME_MESSAGE; +use roc_repl_ui::SHORT_INSTRUCTIONS; +use roc_test_utils::assert_multiline_str_eq; +use std::env; +use std::io::Write; +use std::path::PathBuf; +use std::process::{Command, ExitStatus, Stdio}; + +const ERROR_MESSAGE_START: char = '─'; + +#[derive(Debug)] +pub struct Out { + pub stdout: String, + pub stderr: String, + pub status: ExitStatus, +} + +fn path_to_roc_binary() -> PathBuf { + // Adapted from https://github.com/volta-cli/volta/blob/cefdf7436a15af3ce3a38b8fe53bb0cfdb37d3dd/tests/acceptance/support/sandbox.rs#L680 + // by the Volta Contributors - license information can be found in + // the LEGAL_DETAILS file in the root directory of this distribution. + // + // Thank you, Volta contributors! + let mut path = env::var_os("CARGO_BIN_PATH") + .map(PathBuf::from) + .or_else(|| { + env::current_exe().ok().map(|mut path| { + path.pop(); + if path.ends_with("deps") { + path.pop(); + } + path + }) + }) + .unwrap_or_else(|| panic!("CARGO_BIN_PATH wasn't set, and couldn't be inferred from context. Can't run CLI tests.")); + + path.push("roc"); + + path +} + +pub fn repl_eval(input: &str) -> Out { + let mut cmd = Command::new(path_to_roc_binary()); + + cmd.arg("repl"); + + let mut child = cmd + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .spawn() + .expect("failed to execute compiled `roc` binary in CLI test"); + + { + let stdin = child.stdin.as_mut().expect("Failed to open stdin"); + + // Send the input expression + stdin + .write_all(input.as_bytes()) + .expect("Failed to write input to stdin"); + + // Evaluate the expression + stdin + .write_all(b"\n") + .expect("Failed to write newline to stdin"); + + // Gracefully exit the repl + stdin + .write_all(b":exit\n") + .expect("Failed to write :exit to stdin"); + } + + let output = child + .wait_with_output() + .expect("Error waiting for REPL child process to exit."); + + // Remove the initial instructions from the output. + + let expected_instructions = format!("{WELCOME_MESSAGE}{SHORT_INSTRUCTIONS}"); + let stdout = String::from_utf8(output.stdout).unwrap(); + + assert!( + stdout.starts_with(&expected_instructions), + "Unexpected repl output: {stdout}" + ); + + let (_, answer) = stdout.split_at(expected_instructions.len()); + let answer = if answer.is_empty() { + // The repl crashed before completing the evaluation. + // This is most likely due to a segfault. + if output.status.to_string() == "signal: 11" { + panic!( + "repl segfaulted during the test. Stderr was {:?}", + String::from_utf8(output.stderr).unwrap() + ); + } else { + panic!("repl exited unexpectedly before finishing evaluation. Exit status was {:?} and stderr was {:?}", output.status, String::from_utf8(output.stderr).unwrap()); + } + } else { + let expected_after_answer = "\n".to_string(); + + assert!( + answer.ends_with(&expected_after_answer), + "Unexpected repl output after answer: {answer}" + ); + + // Use [1..] to trim the leading '\n' + // and (len - 1) to trim the trailing '\n' + let (answer, _) = answer[1..].split_at(answer.len() - expected_after_answer.len() - 1); + + // Remove ANSI escape codes from the answer - for example: + // + // Before: "42 \u{1b}[35m:\u{1b}[0m Num *" + // After: "42 : Num *" + strip_ansi_escapes::strip(answer).unwrap() + }; + + Out { + stdout: String::from_utf8(answer).unwrap(), + stderr: String::from_utf8(output.stderr).unwrap(), + status: output.status, + } +} + +pub fn expect_success(input: &str, expected: &str) { + let out = repl_eval(input.trim()); + + assert_multiline_str_eq!("", out.stderr.as_str()); + + let mut iter = out.stdout.lines().rev(); + let line = iter.next().unwrap(); + + if line.is_empty() { + assert_multiline_str_eq!(expected, iter.next().unwrap().trim_end()); + } else { + assert_multiline_str_eq!(expected, line); + } + + assert!(out.status.success()); +} + +pub fn expect_failure(input: &str, expected: &str) { + let out = repl_eval(input); + + // there may be some other stuff printed (e.g. unification errors) + // so skip till the header of the first error + match out.stdout.find(ERROR_MESSAGE_START) { + Some(index) => { + assert_multiline_str_eq!("", out.stderr.as_str()); + assert_multiline_str_eq!(expected, &out.stdout[index..]); + assert!(out.status.success()); + } + None => { + assert_multiline_str_eq!("", out.stderr.as_str()); + assert!(out.status.success()); + panic!( + "I expected a failure, but there is no error message in stdout:\n\n{}", + &out.stdout + ); + } + } +} diff --git a/crates/repl_test/src/lib.rs b/crates/repl_test/src/lib.rs new file mode 100644 index 0000000000..832b2dc126 --- /dev/null +++ b/crates/repl_test/src/lib.rs @@ -0,0 +1,11 @@ +#[cfg(test)] +mod tests; + +#[cfg(test)] +mod state; + +#[cfg(all(test, not(feature = "wasm")))] +mod cli; + +#[cfg(all(test, feature = "wasm"))] +mod wasm; diff --git a/crates/repl_test/src/state.rs b/crates/repl_test/src/state.rs new file mode 100644 index 0000000000..a987b46c38 --- /dev/null +++ b/crates/repl_test/src/state.rs @@ -0,0 +1,212 @@ +use bumpalo::Bump; +use indoc::indoc; +use roc_repl_cli::{evaluate, ReplHelper}; +use roc_repl_ui::is_incomplete; +use roc_repl_ui::repl_state::{ReplAction, ReplState}; +use roc_reporting::report::DEFAULT_PALETTE; +use roc_target::TargetInfo; +use rustyline::Editor; +use target_lexicon::Triple; + +// These are tests of the REPL state machine. They work without actually +// running the CLI, and without using rustyline, and instead verify +// the expected outputs for various sequences of user input strings. + +#[test] +fn one_plus_one() { + complete("1 + 1", &mut ReplState::new(), "2 : Num *"); +} + +#[test] +fn persisted_defs() { + let mut state = ReplState::new(); + + complete("x = 5", &mut state, "5 : Num *"); + complete("7 - 3", &mut state, "4 : Num *"); + complete("y = 6", &mut state, "6 : Num *"); +} + +#[test] +fn annotated_body() { + let mut input = "t : [A, B, C]".to_string(); + + incomplete(&mut input); + + input.push_str("t = A"); + + complete(&input, &mut ReplState::new(), "A : [A, B, C]"); +} + +#[test] +fn exhaustiveness_problem() { + let mut state = ReplState::new(); + + // Enter an annotated tag union to make it exhaustive + { + let mut input = "t : [A, B, C]".to_string(); + + incomplete(&mut input); + + input.push_str("t = A"); + + complete(&input, &mut state, "A : [A, B, C]"); + } + + // Run a `when` on it that isn't exhaustive + { + let mut input = "when t is".to_string(); + incomplete(&mut input); + + input.push_str(" A -> 1"); + incomplete(&mut input); + + let expected_error: &str = indoc!( + r#" + ── UNSAFE PATTERN ────────────────────────────────────────────────────────────── + + This when does not cover all the possibilities: + + 7│> when t is + 8│> A -> 1 + + Other possibilities include: + + B + C + + I would have to crash if I saw one of those! Add branches for them!"# + ); + + error(&input, &mut state, expected_error.to_string()); + } +} + +#[test] +fn partial_record_definition() { + // Partially define a record successfully + { + let mut state = ReplState::new(); + let mut input = "successfulRecord = {".to_string(); + incomplete(&mut input); + + input.push_str("field: \"field\","); + incomplete(&mut input); + + input.push('}'); + complete(&input, &mut state, "{ field: \"field\" } : { field : Str }"); + } + + // Partially define a record incompletely + { + let mut state = ReplState::new(); + let mut input = "failedRecord = {".to_string(); + incomplete(&mut input); + + input.push_str("field: \"field\","); + incomplete(&mut input); + + input.push('\n'); + let expected_error: &str = indoc!( + r#" + ── RECORD PARSE PROBLEM ──────────────────────────────────────────────────────── + + I am partway through parsing a record, but I got stuck here: + + 1│ app "app" provides [replOutput] to "./platform" + 2│ + 3│ replOutput = + 4│ failedRecord = { + ^ + + TODO provide more context."# + ); + error(&input, &mut state, expected_error.to_string()); + } +} + +#[test] +fn tips() { + assert!(!is_incomplete("")); + let arena = Bump::new(); + let target = Triple::host(); + let target_info = TargetInfo::from(&target); + let action = ReplState::default().step(&arena, "", target_info, DEFAULT_PALETTE); + assert!(matches!(action, ReplAction::Help)); +} + +#[test] +fn standalone_annotation() { + let mut state = ReplState::new(); + let mut input = "x : Str".to_string(); + + incomplete(&mut input); + assert!(!is_incomplete(&input)); + let arena = Bump::new(); + let target = Triple::host(); + let target_info = TargetInfo::from(&target); + let action = state.step(&arena, &input, target_info, DEFAULT_PALETTE); + assert!(matches!(action, ReplAction::Nothing)); +} + +/// validate and step the given input, then check the Result vs the output +/// with ANSI escape codes stripped. +fn complete(input: &str, state: &mut ReplState, expected_start: &str) { + assert!(!is_incomplete(input)); + let arena = Bump::new(); + let target = Triple::host(); + let target_info = TargetInfo::from(&target); + let action = state.step(&arena, input, target_info, DEFAULT_PALETTE); + let repl_helper = ReplHelper::default(); + let mut editor = Editor::::new(); + editor.set_helper(Some(repl_helper)); + + match action { + ReplAction::Eval { opt_mono, problems } => { + let string = evaluate(opt_mono, problems, &target); + let escaped = + std::string::String::from_utf8(strip_ansi_escapes::strip(string.trim()).unwrap()) + .unwrap(); + + let comment_index = escaped.rfind('#').unwrap_or(escaped.len()); + + assert_eq!(expected_start, (escaped[0..comment_index].trim())); + } + _ => { + panic!("Unexpected action: {:?}", action); + } + } +} + +fn incomplete(input: &mut String) { + assert!(is_incomplete(input)); + + // Since this was incomplete, rustyline won't step the state. Instead, it will + // remember the input (with a newline appended) for next time. + input.push('\n'); +} + +/// validate and step the given input, then check the given string vs the output +/// with ANSI escape codes stripped. +fn error(input: &str, state: &mut ReplState, expected_step_result: String) { + assert!(!is_incomplete(input)); + let arena = Bump::new(); + let target = Triple::host(); + let target_info = TargetInfo::from(&target); + let action = state.step(&arena, input, target_info, DEFAULT_PALETTE); + let repl_helper = ReplHelper::default(); + let mut editor = Editor::::new(); + editor.set_helper(Some(repl_helper)); + + match action { + ReplAction::Eval { opt_mono, problems } => { + let string = evaluate(opt_mono, problems, &target); + let escaped = + std::string::String::from_utf8(strip_ansi_escapes::strip(string.trim()).unwrap()) + .unwrap(); + assert_eq!(expected_step_result, escaped); + } + _ => { + panic!("Unexpected action: {:?}", action); + } + } +} diff --git a/crates/repl_test/src/tests.rs b/crates/repl_test/src/tests.rs new file mode 100644 index 0000000000..f7816f0a30 --- /dev/null +++ b/crates/repl_test/src/tests.rs @@ -0,0 +1,1494 @@ +#[allow(unused_imports)] +use indoc::indoc; +#[allow(unused_imports)] +use roc_test_utils::assert_multiline_str_eq; + +#[cfg(not(feature = "wasm"))] +use crate::cli::{expect_failure, expect_success, repl_eval}; + +#[cfg(feature = "wasm")] +#[allow(unused_imports)] +use crate::wasm::{expect_failure, expect_success}; + +#[test] +fn literal_0() { + expect_success("0", "0 : Num *"); +} + +#[test] +fn literal_42() { + expect_success("42", "42 : Num *"); +} + +#[test] +fn literal_0x0() { + expect_success("0x0", "0 : Int *"); +} + +#[test] +fn literal_0x42() { + expect_success("0x42", "66 : Int *"); +} + +#[test] +fn literal_0point0() { + expect_success("0.0", "0 : Frac *"); +} + +#[test] +fn literal_4point2() { + expect_success("4.2", "4.2 : Frac *"); +} + +#[test] +fn num_addition() { + expect_success("1 + 2", "3 : Num *"); +} + +#[test] +fn int_addition() { + expect_success("0x1 + 2", "3 : Int *"); +} + +#[test] +fn float_addition() { + expect_success("1.1 + 2", "3.1 : Frac *"); +} + +#[cfg(not(feature = "wasm"))] +#[test] +fn num_rem() { + expect_success("299 % 10", "9 : Int *"); +} + +#[cfg(not(feature = "wasm"))] +#[test] +fn num_floor_division() { + expect_success("Num.divTrunc 4 3", "1 : Int *"); +} + +#[cfg(not(feature = "wasm"))] +#[test] +fn num_floor_checked_division_success() { + expect_success( + "Num.divTruncChecked 4 3", + "Ok 1 : Result (Int *) [DivByZero]", + ); +} + +#[cfg(not(feature = "wasm"))] +#[test] +fn num_floor_checked_division_divby_zero() { + expect_success( + "Num.divTruncChecked 4 0", + "Err DivByZero : Result (Int *) [DivByZero]", + ); +} + +#[cfg(not(feature = "wasm"))] +#[test] +fn num_ceil_division() { + expect_success("Num.divCeil 4 3", "2 : Int *") +} + +#[cfg(not(feature = "wasm"))] +#[test] +fn num_ceil_checked_division_success() { + expect_success( + "Num.divCeilChecked 4 3", + "Ok 2 : Result (Int *) [DivByZero]", + ) +} + +#[test] +fn bool_in_record() { + expect_success("{ x: 1 == 1 }", "{ x: Bool.true } : { x : Bool }"); + expect_success( + "{ z: { y: { x: 1 == 1 } } }", + "{ z: { y: { x: Bool.true } } } : { z : { y : { x : Bool } } }", + ); + expect_success("{ x: 1 != 1 }", "{ x: Bool.false } : { x : Bool }"); + expect_success( + "{ x: 1 == 1, y: 1 != 1 }", + "{ x: Bool.true, y: Bool.false } : { x : Bool, y : Bool }", + ); +} + +#[test] +fn bool_basic_equality() { + expect_success("1 == 1", "Bool.true : Bool"); + expect_success("1 != 1", "Bool.false : Bool"); +} + +#[test] +fn bool_true() { + expect_success( + indoc!( + r#" + Bool.true + "# + ), + r#"Bool.true : Bool"#, + ); +} + +#[test] +fn bool_false() { + expect_success( + indoc!( + r#" + Bool.false + "# + ), + r#"Bool.false : Bool"#, + ); +} + +#[test] +fn arbitrary_tag_unions() { + expect_success("if 1 == 1 then Red else Green", "Red : [Green, Red]"); + expect_success("if 1 != 1 then Red else Green", "Green : [Green, Red]"); +} + +#[test] +fn tag_without_arguments() { + expect_success("True", "True : [True]"); + expect_success("False", "False : [False]"); +} + +#[test] +fn byte_tag_union() { + expect_success( + "if 1 == 1 then Red else if 1 == 1 then Green else Blue", + "Red : [Blue, Green, Red]", + ); + + expect_success( + "{ y: { x: if 1 == 1 then Red else if 1 == 1 then Green else Blue } }", + "{ y: { x: Red } } : { y : { x : [Blue, Green, Red] } }", + ); +} + +#[test] +fn tag_in_record() { + expect_success( + "{ x: Foo 1 2 3, y : 4 }", + "{ x: Foo 1 2 3, y: 4 } : { x : [Foo (Num *) (Num *) (Num *)], y : Num * }", + ); + expect_success( + "{ x: Foo 1 2 3 }", + "{ x: Foo 1 2 3 } : { x : [Foo (Num *) (Num *) (Num *)] }", + ); + expect_success("{ x: Unit }", "{ x: Unit } : { x : [Unit] }"); +} + +#[test] +fn single_element_tag_union() { + expect_success("True 1", "True 1 : [True (Num *)]"); + expect_success("Foo 1 3.14", "Foo 1 3.14 : [Foo (Num *) (Frac *)]"); +} + +#[test] +fn newtype_of_unit() { + expect_success("Foo Bar", "Foo Bar : [Foo [Bar]]"); +} + +#[test] +fn newtype_of_big_data() { + expect_success( + indoc!( + r#" + Either a b : [Left a, Right b] + lefty : Either Str Str + lefty = Left "loosey" + A lefty"# + ), + r#"A (Left "loosey") : [A (Either Str Str)]"#, + ) +} + +#[test] +fn newtype_nested() { + expect_success( + indoc!( + r#" + Either a b : [Left a, Right b] + lefty : Either Str Str + lefty = Left "loosey" + A (B (C lefty))"# + ), + r#"A (B (C (Left "loosey"))) : [A [B [C (Either Str Str)]]]"#, + ) +} + +#[test] +fn newtype_of_big_of_newtype() { + expect_success( + indoc!( + r#" + Big a : [Big a [Wrapper [Newtype a]]] + big : Big Str + big = Big "s" (Wrapper (Newtype "t")) + A big"# + ), + r#"A (Big "s" (Wrapper (Newtype "t"))) : [A (Big Str)]"#, + ) +} + +#[test] +fn tag_with_arguments() { + expect_success("True 1", "True 1 : [True (Num *)]"); + + expect_success( + "if 1 == 1 then True 3 else False 3.14", + "True 3 : [False (Frac *), True (Num *)]", + ) +} + +#[test] +fn literal_empty_str() { + expect_success("\"\"", "\"\" : Str"); +} + +#[test] +fn literal_ascii_str() { + expect_success("\"Hello, World!\"", "\"Hello, World!\" : Str"); +} + +#[test] +fn literal_utf8_str() { + expect_success("\"👩‍👩‍👦‍👦\"", "\"👩‍👩‍👦‍👦\" : Str"); +} + +#[test] +fn str_concat() { + expect_success( + "Str.concat \"Hello, \" \"World!\"", + "\"Hello, World!\" : Str", + ); +} + +#[test] +fn str_count_graphemes() { + expect_success("Str.countGraphemes \"å🤔\"", "2 : Nat"); +} + +#[test] +fn literal_empty_list() { + expect_success("[]", "[] : List *"); +} + +#[cfg(not(feature = "wasm"))] +#[test] +fn literal_empty_list_empty_record() { + expect_success("[{}]", "[{}] : List {}"); +} + +#[test] +fn literal_num_list() { + expect_success("[1, 2, 3]", "[1, 2, 3] : List (Num *)"); +} + +#[test] +fn literal_int_list() { + expect_success("[0x1, 0x2, 0x3]", "[1, 2, 3] : List (Int *)"); +} + +#[test] +fn literal_float_list() { + expect_success("[1.1, 2.2, 3.3]", "[1.1, 2.2, 3.3] : List (Frac *)"); +} + +#[test] +fn literal_string_list() { + expect_success(r#"["a", "b", "cd"]"#, r#"["a", "b", "cd"] : List Str"#); +} + +#[test] +fn nested_string_list() { + expect_success( + r#"[[["a", "b", "cd"], ["y", "z"]], [[]], []]"#, + r#"[[["a", "b", "cd"], ["y", "z"]], [[]], []] : List (List (List Str))"#, + ); +} + +#[test] +fn nested_num_list() { + expect_success( + r#"[[[4, 3, 2], [1, 0]], [[]], []]"#, + r#"[[[4, 3, 2], [1, 0]], [[]], []] : List (List (List (Num *)))"#, + ); +} + +#[test] +fn nested_int_list() { + expect_success( + r#"[[[4, 3, 2], [1, 0x0]], [[]], []]"#, + r#"[[[4, 3, 2], [1, 0]], [[]], []] : List (List (List (Int *)))"#, + ); +} + +#[test] +fn nested_float_list() { + expect_success( + r#"[[[4, 3, 2], [1, 0.0]], [[]], []]"#, + r#"[[[4, 3, 2], [1, 0]], [[]], []] : List (List (List (Frac *)))"#, + ); +} + +#[test] +fn num_bitwise_and() { + expect_success("Num.bitwiseAnd 20 20", "20 : Int *"); + + expect_success("Num.bitwiseAnd 25 10", "8 : Int *"); + + expect_success("Num.bitwiseAnd 200 0", "0 : Int *") +} + +#[test] +fn num_bitwise_xor() { + expect_success("Num.bitwiseXor 20 20", "0 : Int *"); + + expect_success("Num.bitwiseXor 15 14", "1 : Int *"); + + expect_success("Num.bitwiseXor 7 15", "8 : Int *"); + + expect_success("Num.bitwiseXor 200 0", "200 : Int *") +} + +#[test] +fn num_add_wrap() { + expect_success("Num.addWrap Num.maxI64 1", "-9223372036854775808 : I64"); +} + +#[test] +fn num_sub_wrap() { + expect_success("Num.subWrap Num.minI64 1", "9223372036854775807 : I64"); +} + +#[test] +fn num_mul_wrap() { + expect_success("Num.mulWrap Num.maxI64 2", "-2 : I64"); +} + +#[test] +fn num_mul_saturated() { + expect_success("Num.mulSaturated Num.maxI64 2", "9223372036854775807 : I64"); +} + +#[cfg(not(feature = "wasm"))] +#[test] +fn num_add_checked() { + expect_success("Num.addChecked 1 1", "Ok 2 : Result (Num *) [Overflow]"); + expect_success( + "Num.addChecked Num.maxI64 1", + "Err Overflow : Result I64 [Overflow]", + ); +} + +#[cfg(not(feature = "wasm"))] +#[test] +fn num_sub_checked() { + expect_success("Num.subChecked 1 1", "Ok 0 : Result (Num *) [Overflow]"); + expect_success( + "Num.subChecked Num.minI64 1", + "Err Overflow : Result I64 [Overflow]", + ); +} + +#[cfg(not(feature = "wasm"))] +#[test] +fn num_mul_checked() { + expect_success("Num.mulChecked 20 2", "Ok 40 : Result (Num *) [Overflow]"); + expect_success( + "Num.mulChecked Num.maxI64 2", + "Err Overflow : Result I64 [Overflow]", + ); +} + +#[cfg(not(feature = "wasm"))] +#[test] +fn list_concat() { + expect_success( + "List.concat [1.1, 2.2] [3.3, 4.4, 5.5]", + "[1.1, 2.2, 3.3, 4.4, 5.5] : List (Frac *)", + ); +} + +#[cfg(not(feature = "wasm"))] +#[test] +fn list_contains() { + expect_success("List.contains [] 0", "Bool.false : Bool"); + expect_success("List.contains [1, 2, 3] 2", "Bool.true : Bool"); + expect_success("List.contains [1, 2, 3] 4", "Bool.false : Bool"); +} + +#[cfg(not(feature = "wasm"))] +#[test] +fn list_sum_empty() { + expect_success("List.sum []", "0 : Num a"); +} + +#[cfg(not(feature = "wasm"))] +#[test] +fn list_sum_num() { + expect_success("List.sum [1, 2, 3]", "6 : Num *"); +} + +#[cfg(not(feature = "wasm"))] +#[test] +fn list_sum_frac() { + expect_success("List.sum [1.1, 2.2, 3.3]", "6.6 : Frac *"); +} + +#[cfg(not(feature = "wasm"))] +#[test] +fn list_first() { + expect_success( + "List.first [12, 9, 6, 3]", + "Ok 12 : Result (Num *) [ListWasEmpty]", + ); + expect_success( + "List.first []", + "Err ListWasEmpty : Result a [ListWasEmpty]", + ); +} + +#[cfg(not(feature = "wasm"))] +#[test] +fn list_last() { + expect_success( + "List.last [12, 9, 6, 3]", + "Ok 3 : Result (Num *) [ListWasEmpty]", + ); + + expect_success("List.last []", "Err ListWasEmpty : Result a [ListWasEmpty]"); +} + +#[test] +fn empty_record() { + expect_success("{}", "{} : {}"); +} + +#[test] +fn basic_1_field_i64_record() { + // Even though this gets unwrapped at runtime, the repl should still + // report it as a record + expect_success("{ foo: 42 }", "{ foo: 42 } : { foo : Num * }"); +} + +#[test] +fn basic_1_field_f64_record() { + // Even though this gets unwrapped at runtime, the repl should still + // report it as a record + expect_success("{ foo: 4.2 }", "{ foo: 4.2 } : { foo : Frac * }"); +} + +#[test] +fn nested_1_field_i64_record() { + // Even though this gets unwrapped at runtime, the repl should still + // report it as a record + expect_success( + "{ foo: { bar: { baz: 42 } } }", + "{ foo: { bar: { baz: 42 } } } : { foo : { bar : { baz : Num * } } }", + ); +} + +#[test] +fn nested_1_field_f64_record() { + // Even though this gets unwrapped at runtime, the repl should still + // report it as a record + expect_success( + "{ foo: { bar: { baz: 4.2 } } }", + "{ foo: { bar: { baz: 4.2 } } } : { foo : { bar : { baz : Frac * } } }", + ); +} + +#[test] +fn basic_2_field_i64_record() { + expect_success( + "{ foo: 0x4, bar: 0x2 }", + "{ bar: 2, foo: 4 } : { bar : Int *, foo : Int * }", + ); +} + +#[test] +fn basic_2_field_f64_record() { + expect_success( + "{ foo: 4.1, bar: 2.3 }", + "{ bar: 2.3, foo: 4.1 } : { bar : Frac *, foo : Frac * }", + ); +} + +#[test] +fn basic_2_field_mixed_record() { + expect_success( + "{ foo: 4.1, bar: 2 }", + "{ bar: 2, foo: 4.1 } : { bar : Num *, foo : Frac * }", + ); +} + +#[test] +fn basic_3_field_record() { + expect_success( + "{ foo: 4.1, bar: 2, baz: 0x5 }", + "{ bar: 2, baz: 5, foo: 4.1 } : { bar : Num *, baz : Int *, foo : Frac * }", + ); +} + +#[test] +fn list_of_1_field_records() { + // Even though these get unwrapped at runtime, the repl should still + // report them as records + expect_success("[{ foo: 42 }]", "[{ foo: 42 }] : List { foo : Num * }"); +} + +#[test] +fn list_of_2_field_records() { + expect_success( + "[{ foo: 4.1, bar: 2 }]", + "[{ bar: 2, foo: 4.1 }] : List { bar : Num *, foo : Frac * }", + ); +} + +#[test] +fn three_element_record() { + // if this tests turns out to fail on 32-bit targets, look at jit_to_ast_help + expect_success( + "{ a: 1, b: 2, c: 3 }", + "{ a: 1, b: 2, c: 3 } : { a : Num *, b : Num *, c : Num * }", + ); +} + +#[test] +fn four_element_record() { + // if this tests turns out to fail on 32-bit targets, look at jit_to_ast_help + expect_success( + "{ a: 1, b: 2, c: 3, d: 4 }", + "{ a: 1, b: 2, c: 3, d: 4 } : { a : Num *, b : Num *, c : Num *, d : Num * }", + ); +} + +#[cfg(not(feature = "wasm"))] +#[test] +fn multiline_string_non_wasm() { + // If a string contains newlines, format it as a multiline string in the output. + + // We can't use expect_success to test this, because it only looks at the last + // line of output, and in this case we care about every line of output! + let out = repl_eval(r#""\n\nhi!\n\n""#); + let expected = indoc!( + r#"""" + + + hi! + + + """ : Str"# + ); + + assert_multiline_str_eq!("", out.stderr.as_str()); + assert_multiline_str_eq!(expected, out.stdout.trim()); + assert!(out.status.success()); +} + +#[cfg(feature = "wasm")] +#[test] +fn multiline_string_wasm() { + // If a string contains newlines, format it as a multiline string in the output + expect_success( + r#""\n\nhi!\n\n""#, + indoc!( + r#"""" + + + hi! + + + """ : Str"# + ), + ); +} + +#[test] +fn list_of_3_field_records() { + expect_success( + "[{ foo: 4.1, bar: 2, baz: 0x3 }]", + "[{ bar: 2, baz: 3, foo: 4.1 }] : List { bar : Num *, baz : Int *, foo : Frac * }", + ); +} + +#[test] +fn identity_lambda() { + expect_success("\\x -> x", " : a -> a"); +} + +#[cfg(not(feature = "wasm"))] +#[test] +fn sum_lambda() { + expect_success("\\x, y -> x + y", " : Num a, Num a -> Num a"); +} + +#[cfg(not(feature = "wasm"))] +#[test] +fn stdlib_function() { + expect_success("Num.abs", " : Num a -> Num a"); +} + +#[cfg(not(feature = "wasm"))] // TODO: mismatch is due to terminal control codes! +#[test] +fn too_few_args() { + expect_failure( + "Num.add 2", + indoc!( + r#" + ── TOO FEW ARGS ──────────────────────────────────────────────────────────────── + + The add function expects 2 arguments, but it got only 1: + + 4│ Num.add 2 + ^^^^^^^ + + Roc does not allow functions to be partially applied. Use a closure to + make partial application explicit. + "# + ), + ); +} + +#[cfg(not(feature = "wasm"))] // TODO: mismatch is due to terminal control codes! +#[test] +fn type_problem_function() { + expect_failure( + "Num.add 1 \"not a num\"", + indoc!( + r#" + ── TYPE MISMATCH ─────────────────────────────────────────────────────────────── + + This 2nd argument to add has an unexpected type: + + 4│ Num.add 1 "not a num" + ^^^^^^^^^^^ + + The argument is a string of type: + + Str + + But add needs its 2nd argument to be: + + Num * + "# + ), + ); +} + +#[cfg(not(feature = "wasm"))] // TODO: mismatch is due to terminal control codes! +#[test] +fn type_problem_binary_operator() { + expect_failure( + "1 + \"\"", + indoc!( + r#" + ── TYPE MISMATCH ─────────────────────────────────────────────────────────────── + + This 2nd argument to + has an unexpected type: + + 4│ 1 + "" + ^^ + + The argument is a string of type: + + Str + + But + needs its 2nd argument to be: + + Num * + "# + ), + ); +} + +#[cfg(not(feature = "wasm"))] // TODO: mismatch is due to terminal control codes! +#[test] +fn type_problem_unary_operator() { + expect_failure( + "!\"not a bool\"", + indoc!( + r#" + ── TYPE MISMATCH ─────────────────────────────────────────────────────────────── + + This 1st argument to ! has an unexpected type: + + 4│ !"not a bool" + ^^^^^^^^^^^^ + + The argument is a string of type: + + Str + + But ! needs its 1st argument to be: + + Bool + "# + ), + ); +} + +#[cfg(not(feature = "wasm"))] // TODO: mismatch is due to terminal control codes! +#[test] +fn type_problem_string_interpolation() { + expect_failure( + "\"This is not a string -> \\(1)\"", + indoc!( + r#" + ── TYPE MISMATCH ─────────────────────────────────────────────────────────────── + + This argument to this string interpolation has an unexpected type: + + 4│ "This is not a string -> \(1)" + ^ + + The argument is a number of type: + + Num * + + But this string interpolation needs its argument to be: + + Str + "# + ), + ); +} + +#[test] +fn issue_2149_i8_ok() { + expect_success(r#"Str.toI8 "127""#, "Ok 127 : Result I8 [InvalidNumStr]"); +} + +#[test] +fn issue_2149_i8_err() { + expect_success( + r#"Str.toI8 "128""#, + "Err InvalidNumStr : Result I8 [InvalidNumStr]", + ); +} + +#[test] +fn issue_2149_i16_ok() { + expect_success( + r#"Str.toI16 "32767""#, + "Ok 32767 : Result I16 [InvalidNumStr]", + ); +} + +#[test] +fn issue_2149_i16_err() { + expect_success( + r#"Str.toI16 "32768""#, + "Err InvalidNumStr : Result I16 [InvalidNumStr]", + ); +} + +#[test] +fn multiline_input() { + expect_success( + indoc!( + r#" + a : Str + a = "123" + a + "# + ), + r#""123" : Str"#, + ) +} + +#[test] +fn recursive_tag_union_flat_variant() { + expect_success( + indoc!( + r#" + Expr : [Sym Str, Add Expr Expr] + s : Expr + s = Sym "levitating" + s + "# + ), + r#"Sym "levitating" : Expr"#, + ) +} + +#[test] +fn large_recursive_tag_union_flat_variant() { + expect_success( + // > 7 variants so that to force tag storage alongside the data + indoc!( + r#" + Item : [A Str, B Str, C Str, D Str, E Str, F Str, G Str, H Str, I Str, J Str, K Item] + s : Item + s = H "woo" + s + "# + ), + r#"H "woo" : Item"#, + ) +} + +#[test] +fn recursive_tag_union_recursive_variant() { + expect_success( + indoc!( + r#" + Expr : [Sym Str, Add Expr Expr] + s : Expr + s = Add (Add (Sym "one") (Sym "two")) (Sym "four") + s + "# + ), + r#"Add (Add (Sym "one") (Sym "two")) (Sym "four") : Expr"#, + ) +} + +#[test] +fn large_recursive_tag_union_recursive_variant() { + expect_success( + // > 7 variants so that to force tag storage alongside the data + indoc!( + r#" + Item : [A Str, B Str, C Str, D Str, E Str, F Str, G Str, H Str, I Str, J Str, K Item, L Item] + s : Item + s = K (L (E "woo")) + s + "# + ), + r#"K (L (E "woo")) : Item"#, + ) +} + +#[test] +fn recursive_tag_union_into_flat_tag_union() { + expect_success( + indoc!( + r#" + Item : [One [A Str, B Str], Deep Item] + i : Item + i = Deep (One (A "woo")) + i + "# + ), + r#"Deep (One (A "woo")) : Item"#, + ) +} + +#[test] +fn non_nullable_unwrapped_tag_union() { + expect_success( + indoc!( + r#" + RoseTree a : [Tree a (List (RoseTree a))] + e1 : RoseTree Str + e1 = Tree "e1" [] + e2 : RoseTree Str + e2 = Tree "e2" [] + combo : RoseTree Str + combo = Tree "combo" [e1, e2] + combo + "# + ), + r#"Tree "combo" [Tree "e1" [], Tree "e2" []] : RoseTree Str"#, + ) +} + +#[test] +fn nullable_unwrapped_tag_union() { + expect_success( + indoc!( + r#" + LinkedList a : [Nil, Cons a (LinkedList a)] + c1 : LinkedList Str + c1 = Cons "Red" Nil + c2 : LinkedList Str + c2 = Cons "Yellow" c1 + c3 : LinkedList Str + c3 = Cons "Green" c2 + c3 + "# + ), + r#"Cons "Green" (Cons "Yellow" (Cons "Red" Nil)) : LinkedList Str"#, + ) +} + +#[test] +fn nullable_wrapped_tag_union() { + expect_success( + indoc!( + r#" + Container a : [Empty, Whole a, Halved (Container a) (Container a)] + + meats : Container Str + meats = Halved (Whole "Brisket") (Whole "Ribs") + + sides : Container Str + sides = Halved (Whole "Coleslaw") Empty + + bbqPlate : Container Str + bbqPlate = Halved meats sides + + bbqPlate + "# + ), + r#"Halved (Halved (Whole "Brisket") (Whole "Ribs")) (Halved (Whole "Coleslaw") Empty) : Container Str"#, + ) +} + +#[test] +fn large_nullable_wrapped_tag_union() { + // > 7 non-empty variants so that to force tag storage alongside the data + expect_success( + indoc!( + r#" + Cont a : [Empty, S1 a, S2 a, S3 a, S4 a, S5 a, S6 a, S7 a, Tup (Cont a) (Cont a)] + + fst : Cont Str + fst = Tup (S1 "S1") (S2 "S2") + + snd : Cont Str + snd = Tup (S5 "S5") Empty + + tup : Cont Str + tup = Tup fst snd + + tup + "# + ), + r#"Tup (Tup (S1 "S1") (S2 "S2")) (Tup (S5 "S5") Empty) : Cont Str"#, + ) +} + +#[cfg(not(feature = "wasm"))] +#[test] +fn issue_2300() { + expect_success( + r#"\Email str -> str == """#, + r#" : [Email Str] -> Bool"#, + ) +} + +#[cfg(not(feature = "wasm"))] +#[test] +fn function_in_list() { + expect_success( + r#"[\x -> x + 1, \s -> s * 2]"#, + r#"[, ] : List (Num a -> Num a)"#, + ) +} + +#[cfg(not(feature = "wasm"))] +#[test] +fn function_in_record() { + expect_success( + r#"{ n: 1, adder: \x -> x + 1 }"#, + r#"{ adder: , n: 1 } : { adder : Num a -> Num a, n : Num * }"#, + ) +} + +#[cfg(not(feature = "wasm"))] +#[test] +fn function_in_unwrapped_record() { + expect_success( + r#"{ adder: \x -> x + 1 }"#, + r#"{ adder: } : { adder : Num a -> Num a }"#, + ) +} + +#[cfg(not(feature = "wasm"))] +#[test] +fn function_in_tag() { + expect_success( + r#"Adder (\x -> x + 1)"#, + r#"Adder : [Adder (Num a -> Num a)]"#, + ) +} + +#[test] +fn newtype_of_record_of_tag_of_record_of_tag() { + expect_success( + r#"A {b: C {d: 1}}"#, + r#"A { b: C { d: 1 } } : [A { b : [C { d : Num * }] }]"#, + ) +} + +#[test] +fn print_u8s() { + expect_success( + indoc!( + r#" + x : U8 + x = 129 + x + "# + ), + "129 : U8", + ) +} + +#[cfg(not(feature = "wasm"))] // TODO: mismatch is due to terminal control codes! +#[test] +fn parse_problem() { + expect_failure( + "add m n = m + n", + indoc!( + r#" + ── ARGUMENTS BEFORE EQUALS ───────────────────────────────────────────────────── + + I am partway through parsing a definition, but I got stuck here: + + 1│ app "app" provides [replOutput] to "./platform" + 2│ + 3│ replOutput = + 4│ add m n = m + n + ^^^ + + Looks like you are trying to define a function. In roc, functions are + always written as a lambda, like increment = \n -> n + 1. + "# + ), + ); +} + +#[ignore] // re-enable (and fix) after https://github.com/roc-lang/roc/issues/4425 is done! +#[cfg(not(feature = "wasm"))] +#[test] +fn issue_2343_complete_mono_with_shadowed_vars() { + expect_failure( + indoc!( + r#" + b = False + f = \b -> + when b is + True -> 5 + False -> 15 + f b + "# + ), + indoc!( + r#" + ── DUPLICATE NAME ────────────────────────────────────────────────────────────── + + The b name is first defined here: + + 4│ b = False + ^ + + But then it's defined a second time here: + + 5│ f = \b -> + ^ + + Since these variables have the same name, it's easy to use the wrong + one by accident. Give one of them a new name. + "# + ), + ); +} + +#[test] +fn record_with_type_behind_alias() { + expect_success( + indoc!( + r#" + T : { a: Str } + v : T + v = { a: "value" } + v + "# + ), + r#"{ a: "value" } : T"#, + ); +} + +#[test] +fn tag_with_type_behind_alias() { + expect_success( + indoc!( + r#" + T : [A Str] + v : T + v = A "value" + v"# + ), + r#"A "value" : T"#, + ); +} + +#[cfg(not(feature = "wasm"))] +#[test] +fn issue_2588_record_with_function_and_nonfunction() { + expect_success( + indoc!( + r#" + x = 1 + f = \n -> n * 2 + { y: f x, f }"# + ), + r#"{ f: , y: 2 } : { f : Num a -> Num a, y : Num * }"#, + ) +} + +#[test] +fn opaque_apply() { + expect_success( + indoc!( + r#" + Age := U32 + + @Age 23"# + ), + "@Age 23 : Age", + ) +} + +#[test] +fn opaque_apply_polymorphic() { + expect_success( + indoc!( + r#" + F t u := [Package t u] + + @F (Package "" { a: "" })"# + ), + r#"@F (Package "" { a: "" }) : F Str { a : Str }"#, + ) +} + +#[test] +fn opaque_pattern_and_call() { + expect_success( + indoc!( + r#" + F t u := [Package t u] + + f = \@F (Package A {}) -> @F (Package {} A) + + f (@F (Package A {}))"# + ), + r#"@F (Package {} A) : F {} [A]"#, + ) +} + +#[test] +fn dec_in_repl() { + expect_success( + indoc!( + r#" + x: Dec + x=1.23 + x"# + ), + "1.23 : Dec", + ) +} + +#[test] +fn print_i8_issue_2710() { + expect_success( + indoc!( + r#" + a : I8 + a = -1 + a"# + ), + r#"-1 : I8"#, + ) +} + +#[cfg(not(feature = "wasm"))] +#[test] +fn box_box() { + expect_success( + indoc!( + r#" + Box.box "container store""# + ), + r#"Box.box "container store" : Box Str"#, + ) +} + +#[cfg(not(feature = "wasm"))] +#[test] +fn box_box_type_alias() { + expect_success( + indoc!( + r#" + HeapStr : Box Str + helloHeap : HeapStr + helloHeap = Box.box "bye stacks" + helloHeap"# + ), + r#"Box.box "bye stacks" : HeapStr"#, + ) +} + +#[test] +#[cfg(not(feature = "wasm"))] +fn issue_2582_specialize_result_value() { + expect_success( + r#"\x, list -> if x > 0 then List.first list else Ok """#, + r" : Num *, List Str -> Result Str [ListWasEmpty]", + ) +} + +#[test] +#[cfg(not(feature = "wasm"))] +fn issue_2818() { + expect_success( + indoc!( + r#" + f : {} -> List Str + f = \_ -> + x = [] + x"# + ), + r" : {} -> List Str", + ) +} + +#[test] +fn issue_2810_recursive_layout_inside_nonrecursive() { + expect_success( + indoc!( + r#" + Command : [Command Tool] + + Job : [Job Command] + + Tool : [SystemTool, FromJob Job] + + a : Job + a = Job (Command (FromJob (Job (Command SystemTool)))) + a"# + ), + "Job (Command (FromJob (Job (Command SystemTool)))) : Job", + ) +} + +#[test] +fn render_nullable_unwrapped_passing_through_alias() { + expect_success( + indoc!( + r#" + Deep : [L DeepList] + DeepList : [Nil, Cons Deep] + v : DeepList + v = (Cons (L (Cons (L (Cons (L Nil)))))) + v"# + ), + "Cons (L (Cons (L (Cons (L Nil))))) : DeepList", + ) +} + +#[test] +fn opaque_wrap_function() { + expect_success( + indoc!( + r#" + A a := a + List.map [1u8, 2u8, 3u8] @A"# + ), + "[@A 1, @A 2, @A 3] : List (A U8)", + ); +} + +#[test] +#[ignore] +// I think this is picking the wrong integer type on wasm I64 vs I32. +fn dict_get_single() { + expect_success( + indoc!( + r#" + Dict.single 0 {a: 1, c: 2} |> Dict.get 0"# + ), + r#"Ok { a: 1, c: 2 } : Result { a : Num *, c : Num * } [KeyNotFound]"#, + ) +} + +#[test] +fn record_of_poly_function() { + expect_success( + indoc!( + r#" + { a: \_ -> "a" }"# + ), + r#"{ a: } : { a : * -> Str }"#, + ); +} + +#[test] +fn record_of_poly_function_and_string() { + expect_success( + indoc!( + r#" + { a: \_ -> "a", b: "b" }"# + ), + r#"{ a: , b: "b" } : { a : * -> Str, b : Str }"#, + ); +} + +#[test] +fn newtype_by_void_is_wrapped() { + expect_success( + indoc!( + r#" + Result.try (Err 42) (\x -> Err (x+1))"# + ), + r#"Err 42 : Result b (Num *)"#, + ); + + expect_success( + indoc!( + r#" + Result.try (Ok 42) (\x -> Ok (x+1))"# + ), + r#"Ok 43 : Result (Num *) err"#, + ); +} + +#[test] +fn enum_tag_union_in_list() { + expect_success( + indoc!( + r#" + [E, F, G, H] + "# + ), + r#"[E, F, G, H] : List [E, F, G, H]"#, + ); +} + +#[test] +fn str_to_dec() { + expect_success( + indoc!( + r#" + Str.toDec "1234.1234" + "# + ), + r#"Ok 1234.1234 : Result Dec [InvalidNumStr]"#, + ); +} + +#[test] +fn tuple() { + expect_success( + indoc!( + r#" + ("a", 2u32) + "# + ), + r#"("a", 2) : ( Str, U32 )*"#, + ); +} + +#[test] +fn nested_tuple() { + expect_success( + indoc!( + r#" + ("a", (2u32, 3u32)) + "# + ), + r#"("a", (2, 3)) : ( Str, ( U32, U32 )a )a"#, + ); +} + +#[test] +fn ordered_tag_union_memory_layout() { + expect_success( + indoc!( + r#" + Loc : { line: U32, column: U32 } + + Node : [ A Loc, Height U8 Loc ] + + x : Node + x = Height 1 { line: 2, column: 3 } + x + "# + ), + r#"Height 1 { column: 3, line: 2 } : Node"#, + ); +} + +#[test] +fn interpolation_with_nested_strings() { + expect_success( + indoc!( + r#" + "foo \(Str.joinWith ["a", "b", "c"] ", ") bar" + "# + ), + r#""foo a, b, c bar" : Str"#, + ); +} + +#[test] +fn interpolation_with_num_to_str() { + expect_success( + indoc!( + r#" + "foo \(Num.toStr Num.maxI8) bar" + "# + ), + r#""foo 127 bar" : Str"#, + ); +} + +#[test] +fn interpolation_with_operator_desugaring() { + expect_success( + indoc!( + r#" + "foo \(Num.toStr (1 + 2)) bar" + "# + ), + r#""foo 3 bar" : Str"#, + ); +} + +// This test doesn't work on wasm because wasm expects s, but +// the point of the test is the string interpolation behavior. +#[cfg(not(feature = "wasm"))] +#[test] +fn interpolation_with_nested_interpolation() { + expect_failure( + indoc!( + r#" + "foo \(Str.joinWith ["a\(Num.toStr 5)", "b"] "c")" + "# + ), + indoc!( + r#" + ── SYNTAX PROBLEM ────────────────────────────────────────────────────────────── + + This string interpolation is invalid: + + 4│ "foo \(Str.joinWith ["a\(Num.toStr 5)", "b"] "c")" + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + String interpolations cannot contain newlines or other interpolations. + + You can learn more about string interpolation at + + + + Enter an expression to evaluate, or a definition (like x = 1) to use later. + + - ctrl-v + ctrl-j makes a newline + - :q quits + - :help shows this text again + "# + ), + // TODO figure out why the tests prints the repl help text at the end, but only after syntax errors or something? + // In the actual repl this doesn't happen, only in the test. + ); +} diff --git a/crates/repl_test/src/wasm.rs b/crates/repl_test/src/wasm.rs new file mode 100644 index 0000000000..73a8f8bf0a --- /dev/null +++ b/crates/repl_test/src/wasm.rs @@ -0,0 +1,168 @@ +use bumpalo::Bump; +use roc_wasm_interp::{ + wasi, DefaultImportDispatcher, ImportDispatcher, Instance, Value, WasiDispatcher, +}; + +const COMPILER_BYTES: &[u8] = + include_bytes!("../../../target/wasm32-wasi/release-with-lto/roc_repl_wasm.wasm"); + +struct CompilerDispatcher<'a> { + arena: &'a Bump, + src: &'a str, + answer: String, + wasi: WasiDispatcher<'a>, + app: Option>>, + result_addr: Option, +} + +impl<'a> ImportDispatcher for CompilerDispatcher<'a> { + fn dispatch( + &mut self, + module_name: &str, + function_name: &str, + arguments: &[Value], + compiler_memory: &mut [u8], + ) -> Option { + let unknown = || { + panic!( + "I could not find an implementation for import {}.{}", + module_name, function_name + ) + }; + + if module_name == wasi::MODULE_NAME { + self.wasi + .dispatch(function_name, arguments, compiler_memory) + } else if module_name == "env" { + match function_name { + "test_create_app" => { + // Get some bytes from the compiler Wasm instance and create the app Wasm instance + // fn test_create_app(app_bytes_ptr: *const u8, app_bytes_len: usize) -> u32; + assert_eq!(arguments.len(), 2); + let app_bytes_ptr = arguments[0].expect_i32().unwrap() as usize; + let app_bytes_len = arguments[1].expect_i32().unwrap() as usize; + let app_bytes = &compiler_memory[app_bytes_ptr..][..app_bytes_len]; + + let is_debug_mode = false; + let instance = Instance::from_bytes( + self.arena, + app_bytes, + DefaultImportDispatcher::default(), + is_debug_mode, + ) + .unwrap(); + + self.app = Some(instance); + let ok = Value::I32(true as i32); + Some(ok) + } + "test_run_app" => { + // fn test_run_app() -> usize; + assert_eq!(arguments.len(), 0); + match &mut self.app { + Some(instance) => { + let result_addr = instance + .call_export("wrapper", []) + .unwrap() + .expect("No return address from wrapper") + .expect_i32() + .unwrap(); + self.result_addr = Some(result_addr); + let memory_size = instance.memory.len(); + Some(Value::I32(memory_size as i32)) + } + None => panic!("Trying to run the app but it hasn't been created"), + } + } + "test_get_result_and_memory" => { + // Copy the app's entire memory buffer into the compiler's memory, + // and return the location in that buffer where we can find the app result. + // fn test_get_result_and_memory(buffer_alloc_addr: *mut u8) -> usize; + assert_eq!(arguments.len(), 1); + let buffer_alloc_addr = arguments[0].expect_i32().unwrap() as usize; + match &self.app { + Some(instance) => { + let len = instance.memory.len(); + compiler_memory[buffer_alloc_addr..][..len] + .copy_from_slice(&instance.memory); + self.result_addr.map(Value::I32) + } + None => panic!("Trying to get result and memory but there is no app"), + } + } + "test_copy_input_string" => { + // Copy the Roc source code from the test into the compiler Wasm instance + // fn test_copy_input_string(src_buffer_addr: *mut u8); + assert_eq!(arguments.len(), 1); + let src_buffer_addr = arguments[0].expect_i32().unwrap() as usize; + let len = self.src.len(); + compiler_memory[src_buffer_addr..][..len].copy_from_slice(self.src.as_bytes()); + None + } + "test_copy_output_string" => { + // The REPL now has a string representing the answer. Make it available to the test code. + // fn test_copy_output_string(output_ptr: *const u8, output_len: usize); + assert_eq!(arguments.len(), 2); + let output_ptr = arguments[0].expect_i32().unwrap() as usize; + let output_len = arguments[1].expect_i32().unwrap() as usize; + match std::str::from_utf8(&compiler_memory[output_ptr..][..output_len]) { + Ok(answer) => { + self.answer = answer.into(); + } + Err(e) => panic!("{:?}", e), + } + None + } + "now" => Some(Value::F64(0.0)), + _ => unknown(), + } + } else { + unknown() + } + } +} + +fn run(src: &'static str) -> String { + let arena = Bump::new(); + + let mut instance = { + let dispatcher = CompilerDispatcher { + arena: &arena, + src, + answer: String::new(), + wasi: WasiDispatcher::default(), + app: None, + result_addr: None, + }; + + let is_debug_mode = false; // logs every instruction! + Instance::from_bytes(&arena, COMPILER_BYTES, dispatcher, is_debug_mode).unwrap() + }; + + let len = Value::I32(src.len() as i32); + instance.call_export("entrypoint_from_test", [len]).unwrap(); + instance.import_dispatcher.answer.to_owned() +} + +#[allow(dead_code)] +pub fn expect_success(input: &'static str, expected: &str) { + expect(input, expected); +} + +#[allow(dead_code)] +pub fn expect_failure(input: &'static str, expected: &str) { + expect(input, expected); +} + +#[allow(dead_code)] +pub fn expect(input: &'static str, expected: &str) { + let raw_output = run(input); + + // We need to get rid of HTML tags, and we can be quite specific about it! + // If we ever write more complex test cases, we might need regex here. + let without_html = raw_output.replace(" : ", " : "); + + let clean_output = without_html.trim(); + + assert_eq!(clean_output, expected); +} diff --git a/crates/repl_test/test_wasm.sh b/crates/repl_test/test_wasm.sh new file mode 100755 index 0000000000..bbc8374d28 --- /dev/null +++ b/crates/repl_test/test_wasm.sh @@ -0,0 +1,15 @@ +# https://vaneyckt.io/posts/safer_bash_scripts_with_set_euxo_pipefail/ +set -euxo pipefail + +# Build the compiler for WebAssembly target +# We *could* write a build.rs to do this but we'd have nested cargo processes with different targets. +# If we ever decide to do that, one of the cargo processes will need to use the `--target-dir` option. + +# We need to clear RUSTFLAGS for this command, as CI sets normally some flags that are specific to CPU targets. +# Tests target wasm32-wasi instead of wasm32-unknown-unknown, so that we can debug with println! and dbg! +# This has to be built with lto for now. If we do not use lto, it will pull in system calls we don't yet support. +# TODO: Add system calls to our wasi interp so that this can just be built in release without lto. +RUSTFLAGS="" cargo build --locked --profile release-with-lto --target wasm32-wasi -p roc_repl_wasm --no-default-features --features wasi_test + +# Build & run the test code on *native* target, not WebAssembly +cargo test --locked --release -p repl_test --features wasm $@ diff --git a/crates/repl_ui/Cargo.toml b/crates/repl_ui/Cargo.toml new file mode 100644 index 0000000000..bc5fec60ec --- /dev/null +++ b/crates/repl_ui/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "roc_repl_ui" +description = "UI for the Roc REPL, shared between CLI and WebAssembly versions." + +authors.workspace = true +edition.workspace = true +license.workspace = true +version.workspace = true + +[dependencies] +roc_collections = { path = "../compiler/collections" } +roc_load = { path = "../compiler/load" } +roc_parse = { path = "../compiler/parse" } +roc_region = { path = "../compiler/region" } +roc_repl_eval = { path = "../repl_eval" } +roc_reporting = { path = "../reporting" } +roc_target = { path = "../compiler/roc_target" } + +bumpalo.workspace = true +const_format.workspace = true +unicode-segmentation.workspace = true + +[lib] +name = "roc_repl_ui" +path = "src/lib.rs" diff --git a/crates/repl_ui/src/colors.rs b/crates/repl_ui/src/colors.rs new file mode 100644 index 0000000000..0d35b9daf4 --- /dev/null +++ b/crates/repl_ui/src/colors.rs @@ -0,0 +1,12 @@ +use roc_reporting::report::{StyleCodes, ANSI_STYLE_CODES, HTML_STYLE_CODES}; + +const STYLE_CODES: StyleCodes = if cfg!(target_family = "wasm") { + HTML_STYLE_CODES +} else { + ANSI_STYLE_CODES +}; + +pub const BLUE: &str = STYLE_CODES.blue; +pub const PINK: &str = STYLE_CODES.magenta; +pub const GREEN: &str = STYLE_CODES.green; +pub const END_COL: &str = STYLE_CODES.reset; diff --git a/crates/repl_ui/src/lib.rs b/crates/repl_ui/src/lib.rs new file mode 100644 index 0000000000..2f20723446 --- /dev/null +++ b/crates/repl_ui/src/lib.rs @@ -0,0 +1,128 @@ +//! UI functionality, shared between CLI and web, for the Read-Evaluate-Print-Loop (REPL). +// We don't do anything here related to the terminal (doesn't exist on the web) or LLVM (too big for the web). +pub mod colors; +pub mod repl_state; + +use bumpalo::Bump; +use colors::{BLUE, END_COL, PINK}; +use const_format::concatcp; +use repl_state::{parse_src, ParseOutcome}; +use roc_parse::ast::{Expr, ValueDef}; +use roc_repl_eval::gen::{Problems, ReplOutput}; +use roc_reporting::report::StyleCodes; + +use crate::colors::GREEN; + +// TODO add link to repl tutorial (does not yet exist). +pub const TIPS: &str = concatcp!( + "\nEnter an expression to evaluate, or a definition (like ", + BLUE, + "x = 1", + END_COL, + ") to use later.\n\n", + if cfg!(target_family = "wasm") { + "" // In the web repl, we render tips in the UI around the repl instead of in the repl itself. + } else { + // We use ctrl-v + ctrl-j for newlines because on Unix, terminals cannot distinguish between Shift-Enter and Enter + concatcp!( + BLUE, + " - ", + END_COL, + PINK, + "ctrl-v", + END_COL, + " + ", + PINK, + "ctrl-j", + END_COL, + " makes a newline\n", + BLUE, + " - ", + END_COL, + GREEN, + ":q", + END_COL, + " quits\n", + BLUE, + " - ", + END_COL, + GREEN, + ":help", + END_COL, + " shows this text again\n", + ) + } +); + +// For when nothing is entered in the repl +// TODO add link to repl tutorial(does not yet exist). +pub const SHORT_INSTRUCTIONS: &str = "Enter an expression, or :help, or :q to quit.\n\n"; +pub const PROMPT: &str = concatcp!(BLUE, "»", END_COL, " "); +pub const CONT_PROMPT: &str = concatcp!(BLUE, "…", END_COL, " "); + +pub fn is_incomplete(input: &str) -> bool { + let arena = Bump::new(); + + match parse_src(&arena, input) { + ParseOutcome::Incomplete => !input.ends_with('\n'), + // Standalone annotations are default incomplete, because we can't know + // whether they're about to annotate a body on the next line + // (or if not, meaning they stay standalone) until you press Enter again! + // + // So it's Incomplete until you've pressed Enter again (causing the input to end in "\n") + ParseOutcome::ValueDef(ValueDef::Annotation(_, _)) if !input.ends_with('\n') => true, + ParseOutcome::Expr(Expr::When(_, _)) => { + // There might be lots of `when` branches, so don't assume the user is done entering + // them until they enter a blank line! + !input.ends_with('\n') + } + ParseOutcome::Empty + | ParseOutcome::Help + | ParseOutcome::Exit + | ParseOutcome::ValueDef(_) + | ParseOutcome::TypeDef(_) + | ParseOutcome::SyntaxErr + | ParseOutcome::Expr(_) => false, + } +} + +pub fn format_output( + style_codes: StyleCodes, + opt_output: Option, + problems: Problems, +) -> String { + let mut buf = String::new(); + + for message in problems.errors.iter().chain(problems.warnings.iter()) { + if !buf.is_empty() { + buf.push_str("\n\n"); + } + + buf.push('\n'); + buf.push_str(message); + buf.push('\n'); + } + + if let Some(ReplOutput { expr, expr_type }) = opt_output { + // If expr was empty, it was a type annotation or ability declaration; + // don't print anything! + // + // Also, for now we also don't print anything if there was a compile-time error. + // In the future, it would be great to run anyway and print useful output here! + if !expr.is_empty() && problems.errors.is_empty() { + const EXPR_TYPE_SEPARATOR: &str = " : "; // e.g. in "5 : Num *" + + // Print the expr and its type + { + buf.push('\n'); + buf.push_str(&expr); + buf.push_str(style_codes.magenta); // Color for the type separator + buf.push_str(EXPR_TYPE_SEPARATOR); + buf.push_str(style_codes.reset); + buf.push_str(&expr_type); + } + } + } + + buf +} diff --git a/crates/repl_ui/src/repl_state.rs b/crates/repl_ui/src/repl_state.rs new file mode 100644 index 0000000000..fb93db61bd --- /dev/null +++ b/crates/repl_ui/src/repl_state.rs @@ -0,0 +1,366 @@ +use bumpalo::Bump; +use roc_collections::MutSet; +use roc_load::MonomorphizedModule; +use roc_parse::ast::{Expr, Pattern, TypeDef, TypeHeader, ValueDef}; +use roc_parse::expr::{parse_single_def, ExprParseOptions, SingleDef}; +use roc_parse::parser::Parser; +use roc_parse::parser::{EClosure, EExpr, EPattern}; +use roc_parse::parser::{EWhen, Either}; +use roc_parse::state::State; +use roc_parse::{join_alias_to_body, join_ann_to_body}; +use roc_region::all::Loc; +use roc_repl_eval::gen::{compile_to_mono, Problems}; +use roc_reporting::report::Palette; +use roc_target::TargetInfo; + +#[derive(Debug, Clone, PartialEq)] +struct PastDef { + ident: String, + src: String, +} + +pub struct ReplState { + past_defs: Vec, + past_def_idents: MutSet, +} + +impl Default for ReplState { + fn default() -> Self { + Self::new() + } +} + +#[derive(Debug)] +#[allow(clippy::large_enum_variant)] +pub enum ReplAction<'a> { + Eval { + opt_mono: Option>, + problems: Problems, + }, + Exit, + Help, + Nothing, +} + +impl ReplState { + pub fn new() -> Self { + Self { + past_defs: Default::default(), + past_def_idents: Default::default(), + } + } + + pub fn step<'a>( + &mut self, + arena: &'a Bump, + line: &str, + target_info: TargetInfo, + palette: Palette, + ) -> ReplAction<'a> { + let pending_past_def; + let src: &str = match parse_src(arena, line) { + ParseOutcome::Empty | ParseOutcome::Help => return ReplAction::Help, + ParseOutcome::Exit => return ReplAction::Exit, + ParseOutcome::Expr(_) | ParseOutcome::Incomplete | ParseOutcome::SyntaxErr => { + pending_past_def = None; + + // If it's a SyntaxErr (or Incomplete at this point, meaning it will + // become a SyntaxErr as soon as we evaluate it), + // proceed as normal and let the error reporting happen during eval. + line + } + ParseOutcome::ValueDef(value_def) => { + match value_def { + ValueDef::Annotation( + Loc { + value: Pattern::Identifier(ident), + .. + }, + _, + ) => { + // Record the standalone type annotation for future use. + self.add_past_def(ident.trim_end().to_string(), line.to_string()); + + // Return early without running eval, since standalone annotations + // cannot be evaluated as expressions. + return ReplAction::Nothing; + } + ValueDef::Body( + Loc { + value: Pattern::Identifier(ident), + .. + }, + _, + ) + | ValueDef::AnnotatedBody { + body_pattern: + Loc { + value: Pattern::Identifier(ident), + .. + }, + .. + } => { + pending_past_def = Some((ident.to_string(), line.to_string())); + + // Recreate the body of the def and then evaluate it as a lookup. + // We do this so that any errors will get reported as part of this expr; + // if we just did a lookup on the past def, then errors wouldn't get + // reported because we filter out errors whose regions are in past defs. + let mut buf = bumpalo::collections::string::String::with_capacity_in( + ident.len() + line.len() + 1, + arena, + ); + + buf.push_str(line); + buf.push('\n'); + buf.push_str(ident); + + buf.into_bump_str() + } + ValueDef::Annotation(_, _) + | ValueDef::Body(_, _) + | ValueDef::AnnotatedBody { .. } => { + todo!("handle pattern other than identifier (which repl doesn't support)") + } + ValueDef::Dbg { .. } => { + todo!("handle receiving a `dbg` - what should the repl do for that?") + } + ValueDef::Expect { .. } => { + todo!("handle receiving an `expect` - what should the repl do for that?") + } + ValueDef::ExpectFx { .. } => { + todo!("handle receiving an `expect-fx` - what should the repl do for that?") + } + } + } + ParseOutcome::TypeDef(TypeDef::Alias { + header: + TypeHeader { + name: Loc { value: ident, .. }, + .. + }, + .. + }) + | ParseOutcome::TypeDef(TypeDef::Opaque { + header: + TypeHeader { + name: Loc { value: ident, .. }, + .. + }, + .. + }) + | ParseOutcome::TypeDef(TypeDef::Ability { + header: + TypeHeader { + name: Loc { value: ident, .. }, + .. + }, + .. + }) => { + // Record the type for future use. + self.add_past_def(ident.trim_end().to_string(), line.to_string()); + + // Return early without running eval, since none of these + // can be evaluated as expressions. + return ReplAction::Nothing; + } + }; + + let (opt_mono, problems) = compile_to_mono( + arena, + self.past_defs.iter().map(|def| def.src.as_str()), + src, + target_info, + palette, + ); + + if let Some((ident, src)) = pending_past_def { + self.add_past_def(ident, src); + } + + ReplAction::Eval { opt_mono, problems } + } + + fn add_past_def(&mut self, ident: String, src: String) { + let existing_idents = &mut self.past_def_idents; + + existing_idents.insert(ident.clone()); + + self.past_defs.push(PastDef { ident, src }); + } +} + +#[derive(Debug, PartialEq)] +pub enum ParseOutcome<'a> { + ValueDef(ValueDef<'a>), + TypeDef(TypeDef<'a>), + Expr(Expr<'a>), + Incomplete, + SyntaxErr, + Empty, + Help, + Exit, +} + +pub fn parse_src<'a>(arena: &'a Bump, line: &'a str) -> ParseOutcome<'a> { + match line.trim().to_lowercase().as_str() { + "" => ParseOutcome::Empty, + ":help" => ParseOutcome::Help, + ":exit" | ":quit" | ":q" => ParseOutcome::Exit, + _ => { + let src_bytes = line.as_bytes(); + + match roc_parse::expr::loc_expr(true).parse(arena, State::new(src_bytes), 0) { + Ok((_, loc_expr, _)) => ParseOutcome::Expr(loc_expr.value), + // Special case some syntax errors to allow for multi-line inputs + Err((_, EExpr::Closure(EClosure::Body(_, _), _))) + | Err((_, EExpr::When(EWhen::Pattern(EPattern::Start(_), _), _))) + | Err((_, EExpr::Record(_, _))) + | Err((_, EExpr::Start(_))) + | Err((_, EExpr::IndentStart(_))) => ParseOutcome::Incomplete, + Err((_, EExpr::DefMissingFinalExpr(_))) + | Err((_, EExpr::DefMissingFinalExpr2(_, _))) => { + // This indicates that we had an attempted def; re-parse it as a single-line def. + match parse_single_def( + ExprParseOptions { + accept_multi_backpassing: true, + check_for_arrow: true, + }, + 0, + arena, + State::new(src_bytes), + ) { + Ok(( + _, + Some(SingleDef { + type_or_value: Either::First(TypeDef::Alias { header, ann }), + .. + }), + state, + )) => { + // This *could* be an AnnotatedBody, e.g. in a case like this: + // + // UserId x : [UserId Int] + // UserId x = UserId 42 + // + // We optimistically parsed the first line as an alias; we might now + // turn it into an annotation. + match parse_single_def( + ExprParseOptions { + accept_multi_backpassing: true, + check_for_arrow: true, + }, + 0, + arena, + state, + ) { + Ok(( + _, + Some(SingleDef { + type_or_value: + Either::Second(ValueDef::Body(loc_pattern, loc_def_expr)), + region, + spaces_before, + }), + _, + )) if spaces_before.len() <= 1 => { + // This was, in fact, an AnnotatedBody! Build and return it. + let (value_def, _) = join_alias_to_body!( + arena, + loc_pattern, + loc_def_expr, + header, + &ann, + spaces_before, + region + ); + + ParseOutcome::ValueDef(value_def) + } + _ => { + // This was not an AnnotatedBody, so return the alias. + ParseOutcome::TypeDef(TypeDef::Alias { header, ann }) + } + } + } + Ok(( + _, + Some(SingleDef { + type_or_value: + Either::Second(ValueDef::Annotation(ann_pattern, ann_type)), + .. + }), + state, + )) => { + // This *could* be an AnnotatedBody, if the next line is a body. + match parse_single_def( + ExprParseOptions { + accept_multi_backpassing: true, + check_for_arrow: true, + }, + 0, + arena, + state, + ) { + Ok(( + _, + Some(SingleDef { + type_or_value: + Either::Second(ValueDef::Body(loc_pattern, loc_def_expr)), + region, + spaces_before, + }), + _, + )) if spaces_before.len() <= 1 => { + // Inlining this borrow makes clippy unhappy for some reason. + let ann_pattern = &ann_pattern; + + // This was, in fact, an AnnotatedBody! Build and return it. + let (value_def, _) = join_ann_to_body!( + arena, + loc_pattern, + loc_def_expr, + ann_pattern, + &ann_type, + spaces_before, + region + ); + + ParseOutcome::ValueDef(value_def) + } + _ => { + // This was not an AnnotatedBody, so return the standalone annotation. + ParseOutcome::ValueDef(ValueDef::Annotation( + ann_pattern, + ann_type, + )) + } + } + } + Ok(( + _, + Some(SingleDef { + type_or_value: Either::First(type_def), + .. + }), + _, + )) => ParseOutcome::TypeDef(type_def), + Ok(( + _, + Some(SingleDef { + type_or_value: Either::Second(value_def), + .. + }), + _, + )) => ParseOutcome::ValueDef(value_def), + Ok((_, None, _)) => { + todo!("TODO determine appropriate ParseOutcome for Ok(None)") + } + Err(_) => ParseOutcome::SyntaxErr, + } + } + Err(_) => ParseOutcome::SyntaxErr, + } + } + } +} diff --git a/crates/repl_wasm/.gitignore b/crates/repl_wasm/.gitignore new file mode 100644 index 0000000000..f80caa7cf3 --- /dev/null +++ b/crates/repl_wasm/.gitignore @@ -0,0 +1,5 @@ +# wasm-pack +/pkg + +# shell script output +/build diff --git a/crates/repl_wasm/Cargo.toml b/crates/repl_wasm/Cargo.toml new file mode 100644 index 0000000000..4718b680a9 --- /dev/null +++ b/crates/repl_wasm/Cargo.toml @@ -0,0 +1,46 @@ +[package] +name = "roc_repl_wasm" +description = "Provides a build of the REPL for the Roc website using WebAssembly." + +authors = ["The Roc Contributors"] +edition = "2021" +license = "UPL-1.0" +version = "0.0.1" + +[lib] +crate-type = ["cdylib"] + +[build-dependencies] +roc_bitcode = { path = "../compiler/builtins/bitcode" } +roc_builtins = { path = "../compiler/builtins" } +wasi_libc_sys = { path = "../wasi-libc-sys" } + +tempfile.workspace = true + +[dependencies] +bumpalo.workspace = true +console_error_panic_hook = { workspace = true, optional = true } +futures = { workspace = true, optional = true } +getrandom = { version = "0.2", features = ["js"] } # not a direct dependency, needed because of https://docs.rs/getrandom/latest/getrandom/#webassembly-support +js-sys.workspace = true +wasm-bindgen-futures.workspace = true +wasm-bindgen.workspace = true + +roc_collections = { path = "../compiler/collections" } +roc_gen_wasm = { path = "../compiler/gen_wasm" } +roc_load = { path = "../compiler/load" } +roc_parse = { path = "../compiler/parse" } +roc_repl_eval = { path = "../repl_eval" } +roc_repl_ui = { path = "../repl_ui" } +roc_reporting = { path = "../reporting" } +roc_solve = { path = "../compiler/solve" } +roc_target = { path = "../compiler/roc_target" } +roc_types = { path = "../compiler/types" } + +[features] +wasi_test = ["futures"] + +# Tell wasm-pack not to run wasm-opt automatically. We run it explicitly when we need to. +# (Workaround for a CI install issue with wasm-pack https://github.com/rustwasm/wasm-pack/issues/864) +[package.metadata.wasm-pack.profile.profiling] +wasm-opt = false diff --git a/crates/repl_wasm/README.md b/crates/repl_wasm/README.md new file mode 100644 index 0000000000..be6731426c --- /dev/null +++ b/crates/repl_wasm/README.md @@ -0,0 +1,73 @@ +# Web REPL + +## Running locally + +### 1. Build the web REPL + +This builds the compiler as a `.wasm` file, and generates JS glue code. +It will `cargo install` the `wasm-pack` command line tool if you don't already have it. + +```bash +crates/repl_wasm/build-www.sh +``` + +### 2. Make symlinks to the generated Wasm and JS + +```bash +cd www/public +ln -s ../../../crates/repl_wasm/build/roc_repl_wasm_bg.wasm +ln -s ../../../crates/repl_wasm/build/roc_repl_wasm.js +``` +These symlinks are ignored by Git. + +> This is a bit different from the production build, where we copy all the files to `www/build/`. But for development, it's convenient to have just one copy of files like `www/public/repl.js`. You can make changes, reload your browser to see them, and commit them to Git, without getting mixed up between different copies of the same file. + +### 3. Run a local HTTP server + +Browsers won't load .wasm files over the `file://` protocol, so you need to serve the files in `./www/build/` from a local web server. +Any server will do, but this example should work on any system that has Python 3 installed: + +```bash +cd www/public +python3 -m http.server +``` + +### 4. Open your browser + +You should be able to find the Roc REPL at (or whatever port your web server mentioned when it started up.) + +**Warning:** This is work in progress! Not all language features are implemented yet, error messages don't look nice yet, up/down arrows don't work for history, etc. + +![Screenshot](./screenshot.png) + +## How it works + +- User types text into the HTML `` tag +- JS detects the `onchange` event and passes the input text to the Roc compiler WebAssembly module +- Roc compiler WebAssembly module + - Parses the text (currently just a single line) + - Type checks + - Monomorphizes + - Generates WebAssembly using the development backend (not LLVM) + - Returns a slice of bytes to JavaScript +- JavaScript + - Takes the slice of bytes and creates a `WebAssembly.Instance` + - Runs the WebAssembly app + - Gets the memory address of the result and makes a copy of the app's entire memory buffer + - Passes the result address and the memory buffer to the compiler for analysis +- Roc compiler WebAssembly module + - Analyses the bytes of the result, based on the known return type from earlier + - Traverses the copied memory buffer to find any child values + - Produces a user-friendly String and passes it to JavaScript +- JavaScript + - Displays the input and output text on the web page + +![High-level diagram](./architecture.png) + +## Related crates + +There are several directories/packages involved here: + +- `www/public/repl/index.html`: The web page with its JavaScript and a build script +- `crates/repl_wasm`: The Rust crate that becomes the "compiler" WebAssembly module +- `crates/repl_eval`: REPL logic shared between `crates/repl_cli` and `crates/repl_wasm` diff --git a/repl_www/architecture.png b/crates/repl_wasm/architecture.png similarity index 100% rename from repl_www/architecture.png rename to crates/repl_wasm/architecture.png diff --git a/crates/repl_wasm/build-www.sh b/crates/repl_wasm/build-www.sh new file mode 100755 index 0000000000..bc439e05aa --- /dev/null +++ b/crates/repl_wasm/build-www.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash + +# This script +# 1. Uses cargo to build the Roc compiler as a .wasm file (build.rs is part of this step only) +# 2. Further optimizes the .wasm, and generates JavaScript code to interface with it +# +# After running this, we zip the generated assets and host them somewhere public on the web. +# Our website deployment script downloads that zipfile and copies the files into www/build/repl/ +# We use this two-step process because Netlify times out if we try to build the Web REPL there. + +# https://vaneyckt.io/posts/safer_bash_scripts_with_set_euxo_pipefail/ +set -euxo pipefail + +if ! which wasm-pack +then + echo "To build the Web REPL, you need to run 'cargo install wasm-pack'" + exit 1 +fi + +SCRIPT_RELATIVE_DIR=$(dirname "${BASH_SOURCE[0]}") +cd $SCRIPT_RELATIVE_DIR + +mkdir -p build +rm -rf build/* + +# We want a release build, but with debug info (to get stack traces for Wasm backend panics) +# This configuration is called `--profiling` +wasm-pack build --profiling --target web -- --features console_error_panic_hook +cp -v pkg/roc_repl_wasm.js build + +# To disable optimizations while debugging, do `export REPL_DEBUG=1` before running the script +if [ "${REPL_DEBUG:-}" == "" ] && which wasm-opt +then + wasm-opt -Os --debuginfo pkg/roc_repl_wasm_bg.wasm -o build/roc_repl_wasm_bg.wasm +else + echo "wasm-opt is not installed. Skipping .wasm optimization." + cp -v pkg/roc_repl_wasm_bg.wasm build +fi + +# Copy the JS from wasm_bindgen, replacing its invalid `import` statement with a `var`. +# The JS import from the invalid path 'env', seems to be generated when there are unresolved symbols. +BINDGEN_FILE="roc_repl_wasm.js" +echo 'var __wbg_star0 = { now: Date.now };' > build/$BINDGEN_FILE +grep -v '^import' pkg/$BINDGEN_FILE >> build/$BINDGEN_FILE + +# As of July 2022, the .wasm file is ~4MB, shrinking to ~1MB with Brotli compression (which Netlify does) +echo "Generated REPL assets for website:" +ls -l build + +TARFILE=${1:-roc_repl_wasm.tar.gz} +cd build +tar cvzf $TARFILE * diff --git a/crates/repl_wasm/build.rs b/crates/repl_wasm/build.rs new file mode 100644 index 0000000000..3dceb7cf89 --- /dev/null +++ b/crates/repl_wasm/build.rs @@ -0,0 +1,77 @@ +use std::env; +use std::path::PathBuf; +use std::process::Command; + +use wasi_libc_sys::{WASI_COMPILER_RT_PATH, WASI_LIBC_PATH}; + +const PLATFORM_FILENAME: &str = "repl_platform"; + +fn main() { + println!("cargo:rerun-if-changed=build.rs"); + let source_path = format!("src/{PLATFORM_FILENAME}.c"); + println!("cargo:rerun-if-changed={source_path}"); + + // Zig can produce *either* an object containing relocations OR an object containing libc code + // But we want both, so we have to compile twice with different flags, then link them + + // Create an object file with relocations + let out_dir = env::var("OUT_DIR").unwrap(); + let platform_obj = build_wasm_platform(&out_dir, &source_path); + + let mut pre_linked_binary_path = PathBuf::from(std::env::var("OUT_DIR").unwrap()); + pre_linked_binary_path.extend(["pre_linked_binary"]); + pre_linked_binary_path.set_extension("wasm"); + + let builtins_host_tempfile = roc_bitcode::host_wasm_tempfile() + .expect("failed to write host builtins object to tempfile"); + + let output = Command::new(zig_executable()) + .args([ + "wasm-ld", + builtins_host_tempfile.path().to_str().unwrap(), + platform_obj.to_str().unwrap(), + WASI_COMPILER_RT_PATH, + WASI_LIBC_PATH, + "-o", + pre_linked_binary_path.to_str().unwrap(), + "--export-all", + "--no-entry", + "--relocatable", + ]) + .output() + .unwrap(); + + // Extend the lifetime of the tempfile so it doesn't get dropped + // (and thus deleted) before the Zig process is done using it! + let _ = builtins_host_tempfile; + + assert!(output.status.success(), "{output:#?}"); + assert!(output.stdout.is_empty(), "{output:#?}"); + assert!(output.stderr.is_empty(), "{output:#?}"); +} + +fn zig_executable() -> String { + match std::env::var("ROC_ZIG") { + Ok(path) => path, + Err(_) => "zig".into(), + } +} + +fn build_wasm_platform(out_dir: &str, source_path: &str) -> PathBuf { + let mut platform_obj = PathBuf::from(out_dir).join(PLATFORM_FILENAME); + platform_obj.set_extension("wasm"); + + Command::new(zig_executable()) + .args([ + "build-lib", + "-target", + "wasm32-wasi", + "-lc", + source_path, + &format!("-femit-bin={}", platform_obj.to_str().unwrap()), + ]) + .output() + .unwrap(); + + platform_obj +} diff --git a/repl_www/screenshot.png b/crates/repl_wasm/screenshot.png similarity index 100% rename from repl_www/screenshot.png rename to crates/repl_wasm/screenshot.png diff --git a/repl_wasm/src/externs_js.rs b/crates/repl_wasm/src/externs_js.rs similarity index 88% rename from repl_wasm/src/externs_js.rs rename to crates/repl_wasm/src/externs_js.rs index de8d2b79e2..72ad6c20d1 100644 --- a/repl_wasm/src/externs_js.rs +++ b/crates/repl_wasm/src/externs_js.rs @@ -20,7 +20,7 @@ extern "C" { // To debug in the browser, start up the web REPL as per instructions in repl_www/README.md // and sprinkle your code with console_log!("{:?}", my_value); -// (Or if you're running the unit tests in Wasmer, you can just use println! or dbg!) +// (Or if you're running the unit tests with WASI, you can just use println! or dbg!) #[macro_export] macro_rules! console_log { ($($t:tt)*) => (log(&format_args!($($t)*).to_string())) @@ -30,6 +30,6 @@ macro_rules! console_log { /// The browser only has an async API to generate a Wasm module from bytes /// wasm_bindgen manages the interaction between Rust Futures and JS Promises #[wasm_bindgen] -pub async fn entrypoint_from_js(src: String) -> Result { +pub async fn entrypoint_from_js(src: String) -> String { crate::repl::entrypoint_from_js(src).await } diff --git a/crates/repl_wasm/src/externs_test.rs b/crates/repl_wasm/src/externs_test.rs new file mode 100644 index 0000000000..f9c6b02c2c --- /dev/null +++ b/crates/repl_wasm/src/externs_test.rs @@ -0,0 +1,43 @@ +use futures::executor; + +extern "C" { + fn test_create_app(app_bytes_ptr: *const u8, app_bytes_len: usize) -> u32; + fn test_run_app() -> usize; + fn test_get_result_and_memory(buffer_alloc_addr: *mut u8) -> usize; + fn test_copy_input_string(src_buffer_addr: *mut u8); + fn test_copy_output_string(output_ptr: *const u8, output_len: usize); +} + +/// Async wrapper to match the equivalent JS function +pub async fn js_create_app(wasm_module_bytes: &[u8]) -> Result<(), String> { + let ok = unsafe { test_create_app(wasm_module_bytes.as_ptr(), wasm_module_bytes.len()) } != 0; + if ok { + Ok(()) + } else { + Err("Compiler generated an invalid Wasm module".to_string()) + } +} + +pub fn js_run_app() -> usize { + unsafe { test_run_app() } +} + +pub fn js_get_result_and_memory(buffer_alloc_addr: *mut u8) -> usize { + unsafe { test_get_result_and_memory(buffer_alloc_addr) } +} + +/// Entrypoint for tests using WASI and a CLI interpreter +/// - Synchronous API, to avoid the need to run an async executor across the Wasm/native boundary. +/// - Uses an extra callback to allocate & copy the input string (in the browser version, wasm_bindgen does this) +#[no_mangle] +pub extern "C" fn entrypoint_from_test(src_len: usize) { + let mut src_buffer = std::vec![0; src_len]; + let src = unsafe { + test_copy_input_string(src_buffer.as_mut_ptr()); + String::from_utf8_unchecked(src_buffer) + }; + let result_async = crate::repl::entrypoint_from_js(src); + let output = executor::block_on(result_async); + + unsafe { test_copy_output_string(output.as_ptr(), output.len()) } +} diff --git a/crates/repl_wasm/src/lib.rs b/crates/repl_wasm/src/lib.rs new file mode 100644 index 0000000000..309dd99e8b --- /dev/null +++ b/crates/repl_wasm/src/lib.rs @@ -0,0 +1,20 @@ +//! Provides a build of the REPL for the Roc website using WebAssembly. +mod repl; + +// +// Interface with external JS in the browser +// +#[cfg(feature = "console_error_panic_hook")] +extern crate console_error_panic_hook; +#[cfg(not(feature = "wasi_test"))] +mod externs_js; +#[cfg(not(feature = "wasi_test"))] +pub use externs_js::{entrypoint_from_js, js_create_app, js_get_result_and_memory, js_run_app}; + +// +// Interface with test code outside the Wasm module +// +#[cfg(feature = "wasi_test")] +mod externs_test; +#[cfg(feature = "wasi_test")] +pub use externs_test::{entrypoint_from_test, js_create_app, js_get_result_and_memory, js_run_app}; diff --git a/crates/repl_wasm/src/repl.rs b/crates/repl_wasm/src/repl.rs new file mode 100644 index 0000000000..077808afe2 --- /dev/null +++ b/crates/repl_wasm/src/repl.rs @@ -0,0 +1,321 @@ +use bumpalo::{collections::vec::Vec, Bump}; +use roc_reporting::report::{DEFAULT_PALETTE_HTML, HTML_STYLE_CODES}; +use std::{cell::RefCell, mem::size_of}; + +use roc_collections::all::MutSet; +use roc_gen_wasm::wasm32_result; +use roc_load::MonomorphizedModule; +use roc_parse::ast::Expr; +use roc_repl_eval::{ + eval::jit_to_ast, + gen::{format_answer, ReplOutput}, + ReplApp, ReplAppMemory, +}; +use roc_repl_ui::{ + format_output, + repl_state::{ReplAction, ReplState}, + TIPS, +}; +use roc_target::TargetInfo; +use roc_types::pretty_print::{name_and_print_var, DebugPrint}; + +use crate::{js_create_app, js_get_result_and_memory, js_run_app}; + +const WRAPPER_NAME: &str = "wrapper"; + +// On the web, we keep the REPL state in a global variable, because `main` is not in our Rust code! +// We return back to JS after every line of input. `main` is in the browser engine, running the JS event loop. +std::thread_local! { + static REPL_STATE: RefCell = RefCell::new(ReplState::new()); +} + +pub struct WasmReplApp<'a> { + arena: &'a Bump, +} + +/// A copy of the app's memory, made after running the main function +/// The Wasm app ran in a separate address space from the compiler and the eval code. +/// This means we can't simply dereference its pointers as if they were local, because +/// an unrelated value may exist at the same-numbered address in our own address space! +/// Instead we have dereferencing methods that index into the copied bytes. +pub struct WasmMemory<'a> { + copied_bytes: &'a [u8], +} + +macro_rules! deref_number { + ($name: ident, $t: ty) => { + fn $name(&self, address: usize) -> $t { + const N: usize = size_of::<$t>(); + let mut array = [0; N]; + array.copy_from_slice(&self.copied_bytes[address..][..N]); + <$t>::from_le_bytes(array) + } + }; +} + +impl<'a> ReplAppMemory for WasmMemory<'a> { + fn deref_bool(&self, address: usize) -> bool { + self.copied_bytes[address] != 0 + } + + deref_number!(deref_u8, u8); + deref_number!(deref_u16, u16); + deref_number!(deref_u32, u32); + deref_number!(deref_u64, u64); + deref_number!(deref_u128, u128); + deref_number!(deref_usize, usize); + + deref_number!(deref_i8, i8); + deref_number!(deref_i16, i16); + deref_number!(deref_i32, i32); + deref_number!(deref_i64, i64); + deref_number!(deref_i128, i128); + deref_number!(deref_isize, isize); + + deref_number!(deref_f32, f32); + deref_number!(deref_f64, f64); + + fn deref_str(&self, addr: usize) -> &str { + // We can't use RocStr, we need our own small/big string logic. + // The first field is *not* a pointer. We can calculate a pointer for it, but only for big strings. + // If changing this code, remember it also runs in wasm32, not just the app. + let last_byte = self.copied_bytes[addr + 4 + 4 + 3] as i8; + let is_small = last_byte < 0; + + let str_bytes = if is_small { + let len = (last_byte & 0x7f) as usize; + &self.copied_bytes[addr..][..len] + } else { + let chars_index = self.deref_usize(addr); + let seamless_slice_mask = u32::MAX as usize >> 1; + let len = self.deref_usize(addr + 4) & seamless_slice_mask; + &self.copied_bytes[chars_index..][..len] + }; + + unsafe { std::str::from_utf8_unchecked(str_bytes) } + } + + fn deref_pointer_with_tag_id(&self, addr: usize) -> (u16, u64) { + let addr_with_id = self.deref_usize(addr); + let tag_id_mask = 0b11; + + let tag_id = addr_with_id & tag_id_mask; + let data_addr = addr_with_id & !tag_id_mask; + (tag_id as _, data_addr as _) + } +} + +impl<'a> WasmReplApp<'a> { + /// Allocate a buffer to copy the app memory into + /// Buffer is aligned to 64 bits to preserve the original alignment of all Wasm numbers + fn allocate_buffer(&self, size: usize) -> &'a mut [u8] { + let size64 = (size / size_of::()) + 1; + let buffer64: &mut [u64] = self.arena.alloc_slice_fill_default(size64); + + // Note: Need `from_raw_parts_mut` as well as `transmute` to ensure slice has correct length! + let buffer: &mut [u8] = unsafe { + let ptr8: *mut u8 = std::mem::transmute(buffer64.as_mut_ptr()); + std::slice::from_raw_parts_mut(ptr8, size) + }; + + buffer + } +} + +impl<'a> ReplApp<'a> for WasmReplApp<'a> { + type Memory = WasmMemory<'a>; + + /// Run user code that returns a type with a `Builtin` layout + /// Size of the return value is statically determined from its Rust type + /// The `transform` callback takes the app's memory and the returned value + /// _main_fn_name is always the same and we don't use it here + fn call_function(&mut self, _main_fn_name: &str, mut transform: F) -> Expr<'a> + where + F: FnMut(&'a Self::Memory, Return) -> Expr<'a>, + Self::Memory: 'a, + { + let app_final_memory_size: usize = js_run_app(); + + let copied_bytes: &mut [u8] = self.allocate_buffer(app_final_memory_size); + + let app_result_addr = js_get_result_and_memory(copied_bytes.as_mut_ptr()); + + let result_bytes = &copied_bytes[app_result_addr..]; + let result: Return = unsafe { + let ptr: *const Return = std::mem::transmute(result_bytes.as_ptr()); + ptr.read() + }; + + let mem = self.arena.alloc(WasmMemory { copied_bytes }); + + transform(mem, result) + } + + /// Run user code that returns a struct or union, whose size is provided as an argument + /// The `transform` callback takes the app's memory and the address of the returned value + /// _main_fn_name and _ret_bytes are only used for the CLI REPL. For Wasm they are compiled-in + /// to the test_wrapper function of the app itself + fn call_function_dynamic_size( + &mut self, + _main_fn_name: &str, + _ret_bytes: usize, + mut transform: F, + ) -> T + where + F: FnMut(&'a Self::Memory, usize) -> T, + Self::Memory: 'a, + { + let app_final_memory_size: usize = js_run_app(); + + let copied_bytes: &mut [u8] = self.allocate_buffer(app_final_memory_size); + + let app_result_addr = js_get_result_and_memory(copied_bytes.as_mut_ptr()); + let mem = self.arena.alloc(WasmMemory { copied_bytes }); + + transform(mem, app_result_addr) + } +} + +const PRE_LINKED_BINARY: &[u8] = + include_bytes!(concat!(env!("OUT_DIR"), "/pre_linked_binary.wasm")) as &[_]; + +pub async fn entrypoint_from_js(src: String) -> String { + // If our Rust code panics, redirect the error message to JS console.error + // Also, our JS code overrides console.error to display the error message text (including stack trace) in the REPL output. + #[cfg(feature = "console_error_panic_hook")] + console_error_panic_hook::set_once(); + + // TODO: make this a global and reset it? + let arena = &Bump::new(); + + // Compile the app + let target_info = TargetInfo::default_wasm32(); + + // Advance the REPL state machine + let action = REPL_STATE.with(|repl_state_cell| { + let mut repl_state = repl_state_cell.borrow_mut(); + repl_state.step(arena, &src, target_info, DEFAULT_PALETTE_HTML) + }); + + // Perform the action the state machine asked for, and return the appropriate output string + match action { + ReplAction::Help => TIPS.to_string(), + ReplAction::Exit => { + "To exit the web version of the REPL, just close the browser tab!".to_string() + } + ReplAction::Nothing => String::new(), + ReplAction::Eval { opt_mono, problems } => { + let opt_output = match opt_mono { + Some(mono) => eval_wasm(arena, target_info, mono).await, + None => None, + }; + + format_output(HTML_STYLE_CODES, opt_output, problems) + } + } +} + +async fn eval_wasm<'a>( + arena: &'a Bump, + target_info: TargetInfo, + mono: MonomorphizedModule<'a>, +) -> Option { + let MonomorphizedModule { + module_id, + procedures, + mut interns, + mut subs, + exposed_to_host, + mut layout_interner, + .. + } = mono; + + debug_assert_eq!(exposed_to_host.top_level_values.len(), 1); + let (main_fn_symbol, main_fn_var) = exposed_to_host.top_level_values.iter().next().unwrap(); + let main_fn_symbol = *main_fn_symbol; + let main_fn_var = *main_fn_var; + + // pretty-print the expr type string for later. + let expr_type = name_and_print_var( + main_fn_var, + &mut subs, + module_id, + &interns, + DebugPrint::NOTHING, + ); + + let (_, main_fn_layout) = *procedures.keys().find(|(s, _)| *s == main_fn_symbol)?; + + let app_module_bytes = { + let env = roc_gen_wasm::Env { + arena, + module_id, + stack_bytes: roc_gen_wasm::Env::DEFAULT_STACK_BYTES, + exposed_to_host: exposed_to_host + .top_level_values + .keys() + .copied() + .collect::>(), + }; + + let (mut module, mut called_fns, main_fn_index) = { + let host_module = roc_gen_wasm::parse_host(env.arena, PRE_LINKED_BINARY).unwrap(); + roc_gen_wasm::build_app_module( + &env, + &mut layout_interner, + &mut interns, // NOTE: must drop this mutable ref before jit_to_ast + host_module, + procedures, + ) + }; + + wasm32_result::insert_wrapper_for_layout( + arena, + &layout_interner, + &mut module, + WRAPPER_NAME, + main_fn_index, + main_fn_layout.result, + ); + called_fns.push(true); + + module.eliminate_dead_code(env.arena, called_fns); + + let mut buffer = Vec::with_capacity_in(module.size(), arena); + module.serialize(&mut buffer); + + buffer + }; + + // Send the compiled binary out to JS, which will asynchronously create an executable WebAssembly instance + match js_create_app(&app_module_bytes).await { + Ok(()) => {} + Err(js_exception) => { + return Some(ReplOutput { + expr: format!("{js_exception:?}"), + expr_type: String::new(), + }) + } + } + + let mut app = WasmReplApp { arena }; + + // Run the app and transform the result value to an AST `Expr` + // Restore type constructor names, and other user-facing info that was erased during compilation. + let res_answer = jit_to_ast( + arena, + &mut app, + "", // main_fn_name is ignored (only passed to WasmReplApp methods) + main_fn_layout, + main_fn_var, + &subs, + &interns, + layout_interner.into_global().fork(), + target_info, + ); + + // Transform the Expr to a string + let expr = format_answer(arena, res_answer).to_string(); + + Some(ReplOutput { expr, expr_type }) +} diff --git a/repl_wasm/src/repl_platform.c b/crates/repl_wasm/src/repl_platform.c similarity index 81% rename from repl_wasm/src/repl_platform.c rename to crates/repl_wasm/src/repl_platform.c index 1f288159e9..2b5a87f1fc 100644 --- a/repl_wasm/src/repl_platform.c +++ b/crates/repl_wasm/src/repl_platform.c @@ -1,10 +1,12 @@ #include +#include +#include /* A bare-bones Roc "platform" for REPL code, providing heap allocation for builtins. */ -// Enable/disable printf debugging. Leave disabled to avoid bloating .wasm files and slowing down Wasmer tests. +// Enable/disable printf debugging. Leave disabled to avoid bloating .wasm files and slowing down tests. #define ENABLE_PRINTF 0 //-------------------------- @@ -52,8 +54,11 @@ void roc_dealloc(void *ptr, unsigned int alignment) //-------------------------- -void roc_panic(void *ptr, unsigned int alignment) +extern void send_panic_msg_to_js(void *ptr, unsigned int panic_tag); + +void roc_panic(void *ptr, unsigned int panic_tag) { + send_panic_msg_to_js(ptr, panic_tag); #if ENABLE_PRINTF char *msg = (char *)ptr; fprintf(stderr, @@ -62,12 +67,8 @@ void roc_panic(void *ptr, unsigned int alignment) abort(); } -//-------------------------- - -void *roc_memcpy(void *dest, const void *src, size_t n) -{ - return memcpy(dest, src, n); -} +// TODO: add a way to send dbg to js. +void roc_debug(void* loc, void* msg) {} //-------------------------- diff --git a/reporting/.gitignore b/crates/reporting/.gitignore similarity index 100% rename from reporting/.gitignore rename to crates/reporting/.gitignore diff --git a/crates/reporting/Cargo.toml b/crates/reporting/Cargo.toml new file mode 100644 index 0000000000..17fcc35ed2 --- /dev/null +++ b/crates/reporting/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "roc_reporting" +description = "Responsible for generating warning and error messages." + +authors.workspace = true +edition.workspace = true +license.workspace = true +version.workspace = true + +[dependencies] +roc_can = { path = "../compiler/can" } +roc_collections = { path = "../compiler/collections" } +roc_error_macros = { path = "../error_macros" } +roc_exhaustive = { path = "../compiler/exhaustive" } +roc_fmt = { path = "../compiler/fmt" } +roc_module = { path = "../compiler/module" } +roc_parse = { path = "../compiler/parse" } +roc_packaging = { path = "../packaging" } +roc_problem = { path = "../compiler/problem" } +roc_region = { path = "../compiler/region" } +roc_solve_problem = { path = "../compiler/solve_problem" } +roc_std = { path = "../roc_std" } +roc_types = { path = "../compiler/types" } +ven_pretty = { path = "../vendor/pretty" } +byte-unit = "4.0.19" +itertools = "0.10.5" + +bumpalo.workspace = true +distance.workspace = true diff --git a/crates/reporting/src/cli.rs b/crates/reporting/src/cli.rs new file mode 100644 index 0000000000..9447fdf0d6 --- /dev/null +++ b/crates/reporting/src/cli.rs @@ -0,0 +1,173 @@ +use std::path::PathBuf; + +use roc_collections::MutMap; +use roc_module::symbol::{Interns, ModuleId}; +use roc_region::all::LineInfo; +use roc_solve_problem::TypeError; + +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] +pub struct Problems { + pub fatally_errored: bool, + pub errors: usize, + pub warnings: usize, +} + +impl Problems { + pub fn exit_code(&self) -> i32 { + // 0 means no problems, 1 means errors, 2 means warnings + if self.errors > 0 { + 1 + } else { + self.warnings.min(1) as i32 + } + } + + pub fn print_to_stdout(&self, total_time: std::time::Duration) { + const GREEN: usize = 32; + const YELLOW: usize = 33; + + print!( + "\x1B[{}m{}\x1B[39m {} and \x1B[{}m{}\x1B[39m {} found in {} ms", + match self.errors { + 0 => GREEN, + _ => YELLOW, + }, + self.errors, + match self.errors { + 1 => "error", + _ => "errors", + }, + match self.warnings { + 0 => GREEN, + _ => YELLOW, + }, + self.warnings, + match self.warnings { + 1 => "warning", + _ => "warnings", + }, + total_time.as_millis() + ); + } +} + +pub fn report_problems( + sources: &MutMap)>, + interns: &Interns, + can_problems: &mut MutMap>, + type_problems: &mut MutMap>, +) -> Problems { + use crate::report::{can_problem, type_problem, Report, RocDocAllocator, DEFAULT_PALETTE}; + use roc_problem::Severity::*; + + let palette = DEFAULT_PALETTE; + let mut total_problems = 0; + + for problems in can_problems.values() { + total_problems += problems.len(); + } + + for problems in type_problems.values() { + total_problems += problems.len(); + } + + // This will often over-allocate total memory, but it means we definitely + // never need to re-allocate either the warnings or the errors vec! + let mut warnings = Vec::with_capacity(total_problems); + let mut errors = Vec::with_capacity(total_problems); + let mut fatally_errored = false; + + for (home, (module_path, src)) in sources.iter() { + let mut src_lines: Vec<&str> = Vec::new(); + + src_lines.extend(src.split('\n')); + + let lines = LineInfo::new(&src_lines.join("\n")); + + // Report parsing and canonicalization problems + let alloc = RocDocAllocator::new(&src_lines, *home, interns); + + let problems = can_problems.remove(home).unwrap_or_default(); + + for problem in problems.into_iter() { + let report = can_problem(&alloc, &lines, module_path.clone(), problem); + let severity = report.severity; + let mut buf = String::new(); + + report.render_color_terminal(&mut buf, &alloc, &palette); + + match severity { + Warning => { + warnings.push(buf); + } + RuntimeError => { + errors.push(buf); + } + Fatal => { + fatally_errored = true; + errors.push(buf); + } + } + } + + let problems = type_problems.remove(home).unwrap_or_default(); + + for problem in problems { + if let Some(report) = type_problem(&alloc, &lines, module_path.clone(), problem) { + let severity = report.severity; + let mut buf = String::new(); + + report.render_color_terminal(&mut buf, &alloc, &palette); + + match severity { + Warning => { + warnings.push(buf); + } + RuntimeError => { + errors.push(buf); + } + Fatal => { + fatally_errored = true; + errors.push(buf); + } + } + } + } + } + + debug_assert!(can_problems.is_empty() && type_problems.is_empty(), "After reporting problems, there were {:?} can_problems and {:?} type_problems that could not be reported because they did not have corresponding entries in `sources`.", can_problems.len(), type_problems.len()); + debug_assert_eq!(errors.len() + warnings.len(), total_problems); + + let problems_reported; + + // Only print warnings if there are no errors + if errors.is_empty() { + problems_reported = warnings.len(); + + for warning in warnings.iter() { + println!("\n{warning}\n"); + } + } else { + problems_reported = errors.len(); + + for error in errors.iter() { + println!("\n{error}\n"); + } + } + + // If we printed any problems, print a horizontal rule at the end, + // and then clear any ANSI escape codes (e.g. colors) we've used. + // + // The horizontal rule is nice when running the program right after + // compiling it, as it lets you clearly see where the compiler + // errors/warnings end and the program output begins. + if problems_reported > 0 { + println!("{}\u{001B}[0m\n", Report::horizontal_rule(&palette)); + } + + Problems { + fatally_errored, + errors: errors.len(), + warnings: warnings.len(), + } +} diff --git a/crates/reporting/src/error/canonicalize.rs b/crates/reporting/src/error/canonicalize.rs new file mode 100644 index 0000000000..cad1da9879 --- /dev/null +++ b/crates/reporting/src/error/canonicalize.rs @@ -0,0 +1,2364 @@ +use roc_collections::all::MutSet; +use roc_module::ident::{Ident, Lowercase, ModuleName}; +use roc_module::symbol::DERIVABLE_ABILITIES; +use roc_problem::can::PrecedenceProblem::BothNonAssociative; +use roc_problem::can::{ + BadPattern, CycleEntry, ExtensionTypeKind, FloatErrorKind, IntErrorKind, Problem, RuntimeError, + ShadowKind, +}; +use roc_problem::Severity; +use roc_region::all::{LineColumn, LineColumnRegion, LineInfo, Loc, Region}; +use roc_types::types::AliasKind; +use std::path::PathBuf; + +use crate::error::r#type::suggest; +use crate::report::{to_file_problem_report, Annotation, Report, RocDocAllocator, RocDocBuilder}; +use ven_pretty::{text, DocAllocator}; + +const SYNTAX_PROBLEM: &str = "SYNTAX PROBLEM"; +const NAMING_PROBLEM: &str = "NAMING PROBLEM"; +const UNRECOGNIZED_NAME: &str = "UNRECOGNIZED NAME"; +const UNUSED_DEF: &str = "UNUSED DEFINITION"; +const UNUSED_IMPORT: &str = "UNUSED IMPORT"; +const UNUSED_ALIAS_PARAM: &str = "UNUSED TYPE ALIAS PARAMETER"; +const UNBOUND_TYPE_VARIABLE: &str = "UNBOUND TYPE VARIABLE"; +const UNUSED_ARG: &str = "UNUSED ARGUMENT"; +const MISSING_DEFINITION: &str = "MISSING DEFINITION"; +const UNKNOWN_GENERATES_WITH: &str = "UNKNOWN GENERATES FUNCTION"; +const DUPLICATE_FIELD_NAME: &str = "DUPLICATE FIELD NAME"; +const DUPLICATE_TAG_NAME: &str = "DUPLICATE TAG NAME"; +const INVALID_UNICODE: &str = "INVALID UNICODE"; +pub const CIRCULAR_DEF: &str = "CIRCULAR DEFINITION"; +const DUPLICATE_NAME: &str = "DUPLICATE NAME"; +const VALUE_NOT_EXPOSED: &str = "NOT EXPOSED"; +const MODULE_NOT_IMPORTED: &str = "MODULE NOT IMPORTED"; +const NESTED_DATATYPE: &str = "NESTED DATATYPE"; +const CONFLICTING_NUMBER_SUFFIX: &str = "CONFLICTING NUMBER SUFFIX"; +const NUMBER_OVERFLOWS_SUFFIX: &str = "NUMBER OVERFLOWS SUFFIX"; +const NUMBER_UNDERFLOWS_SUFFIX: &str = "NUMBER UNDERFLOWS SUFFIX"; +const OPAQUE_NOT_DEFINED: &str = "OPAQUE TYPE NOT DEFINED"; +const OPAQUE_DECLARED_OUTSIDE_SCOPE: &str = "OPAQUE TYPE DECLARED OUTSIDE SCOPE"; +const OPAQUE_NOT_APPLIED: &str = "OPAQUE TYPE NOT APPLIED"; +const OPAQUE_OVER_APPLIED: &str = "OPAQUE TYPE APPLIED TO TOO MANY ARGS"; +const INVALID_EXTENSION_TYPE: &str = "INVALID_EXTENSION_TYPE"; +const ABILITY_HAS_TYPE_VARIABLES: &str = "ABILITY HAS TYPE VARIABLES"; +const IMPLEMENTS_CLAUSE_IS_NOT_AN_ABILITY: &str = "IMPLEMENTS CLAUSE IS NOT AN ABILITY"; +const ILLEGAL_IMPLEMENTS_CLAUSE: &str = "ILLEGAL IMPLEMENTS CLAUSE"; +const ABILITY_MEMBER_MISSING_IMPLEMENTS_CLAUSE: &str = "ABILITY MEMBER MISSING IMPLEMENTS CLAUSE"; +const ABILITY_MEMBER_BINDS_MULTIPLE_VARIABLES: &str = "ABILITY MEMBER BINDS MULTIPLE VARIABLES"; +const ABILITY_NOT_ON_TOPLEVEL: &str = "ABILITY NOT ON TOP-LEVEL"; +const SPECIALIZATION_NOT_ON_TOPLEVEL: &str = "SPECIALIZATION NOT ON TOP-LEVEL"; +const ABILITY_USED_AS_TYPE: &str = "ABILITY USED AS TYPE"; +const ILLEGAL_DERIVE: &str = "ILLEGAL DERIVE"; +const IMPLEMENTATION_NOT_FOUND: &str = "IMPLEMENTATION NOT FOUND"; +const NOT_AN_ABILITY_MEMBER: &str = "NOT AN ABILITY MEMBER"; +const NOT_AN_ABILITY: &str = "NOT AN ABILITY"; +const OPTIONAL_ABILITY_IMPLEMENTATION: &str = "OPTIONAL ABILITY IMPLEMENTATION"; +const QUALIFIED_ABILITY_IMPLEMENTATION: &str = "QUALIFIED ABILITY IMPLEMENTATION"; +const ABILITY_IMPLEMENTATION_NOT_IDENTIFIER: &str = "ABILITY IMPLEMENTATION NOT IDENTIFIER"; +const DUPLICATE_IMPLEMENTATION: &str = "DUPLICATE IMPLEMENTATION"; +const UNNECESSARY_IMPLEMENTATIONS: &str = "UNNECESSARY IMPLEMENTATIONS"; +const INCOMPLETE_ABILITY_IMPLEMENTATION: &str = "INCOMPLETE ABILITY IMPLEMENTATION"; + +pub fn can_problem<'b>( + alloc: &'b RocDocAllocator<'b>, + lines: &LineInfo, + filename: PathBuf, + problem: Problem, +) -> Report<'b> { + let doc; + let title; + let severity = problem.severity(); + + match problem { + Problem::UnusedDef(symbol, region) => { + let line = + r#" then remove it so future readers of your code don't wonder why it is there."#; + + doc = alloc.stack([ + alloc + .symbol_unqualified(symbol) + .append(alloc.reflow(" is not used anywhere in your code.")), + alloc.region(lines.convert_region(region)), + alloc + .reflow("If you didn't intend on using ") + .append(alloc.symbol_unqualified(symbol)) + .append(alloc.reflow(line)), + ]); + + title = UNUSED_DEF.to_string(); + } + Problem::UnusedImport(symbol, region) => { + doc = alloc.stack([ + alloc.concat([ + alloc.symbol_qualified(symbol), + alloc.reflow(" is not used in this module."), + ]), + alloc.region(lines.convert_region(region)), + alloc.concat([ + alloc.reflow("Since "), + alloc.symbol_qualified(symbol), + alloc.reflow(" isn't used, you don't need to import it."), + ]), + ]); + + title = UNUSED_IMPORT.to_string(); + } + Problem::UnusedModuleImport(module_id, region) => { + doc = alloc.stack([ + alloc.concat([ + alloc.reflow("Nothing from "), + alloc.module(module_id), + alloc.reflow(" is used in this module."), + ]), + alloc.region(lines.convert_region(region)), + alloc.concat([ + alloc.reflow("Since "), + alloc.module(module_id), + alloc.reflow(" isn't used, you don't need to import it."), + ]), + ]); + + title = UNUSED_IMPORT.to_string(); + } + Problem::DefsOnlyUsedInRecursion(1, region) => { + doc = alloc.stack([ + alloc.reflow("This definition is only used in recursion with itself:"), + alloc.region(lines.convert_region(region)), + alloc.reflow( + "If you don't intend to use or export this definition, it should be removed!", + ), + ]); + + title = "DEFINITION ONLY USED IN RECURSION".to_string(); + } + Problem::DefsOnlyUsedInRecursion(n, region) => { + doc = alloc.stack([ + alloc.concat([ + alloc.reflow("These "), + alloc.string(n.to_string()), + alloc.reflow(" definitions are only used in mutual recursion with themselves:"), + ]), + alloc.region(lines.convert_region(region)), + alloc.reflow( + "If you don't intend to use or export any of them, they should all be removed!", + ), + ]); + + title = "DEFINITIONs ONLY USED IN RECURSION".to_string(); + } + Problem::ExposedButNotDefined(symbol) => { + doc = alloc.stack([ + alloc.symbol_unqualified(symbol).append( + alloc.reflow(" is listed as exposed, but it isn't defined in this module."), + ), + alloc + .reflow("You can fix this by adding a definition for ") + .append(alloc.symbol_unqualified(symbol)) + .append(alloc.reflow(", or by removing it from ")) + .append(alloc.keyword("exposes")) + .append(alloc.reflow(".")), + ]); + + title = MISSING_DEFINITION.to_string(); + } + Problem::UnknownGeneratesWith(loc_ident) => { + doc = alloc.stack([ + alloc + .reflow("I don't know how to generate the ") + .append(alloc.ident(loc_ident.value)) + .append(alloc.reflow(" function.")), + alloc.region(lines.convert_region(loc_ident.region)), + alloc + .reflow("Only specific functions like `after` and `map` can be generated.") + .append(alloc.reflow("Learn more about hosted modules at TODO.")), + ]); + + title = UNKNOWN_GENERATES_WITH.to_string(); + } + Problem::UnusedArgument(closure_symbol, is_anonymous, argument_symbol, region) => { + let line = "\". Adding an underscore at the start of a variable name is a way of saying that the variable is not used."; + + doc = alloc.stack([ + alloc.concat([ + if is_anonymous { + alloc.reflow("This function") + } else { + alloc.symbol_unqualified(closure_symbol) + }, + alloc.reflow(" doesn't use "), + alloc.symbol_unqualified(argument_symbol), + alloc.text("."), + ]), + alloc.region(lines.convert_region(region)), + alloc.concat([ + alloc.reflow("If you don't need "), + alloc.symbol_unqualified(argument_symbol), + alloc.reflow(", then you can just remove it. However, if you really do need "), + alloc.symbol_unqualified(argument_symbol), + alloc.reflow(" as an argument of "), + if is_anonymous { + alloc.reflow("this function") + } else { + alloc.symbol_unqualified(closure_symbol) + }, + alloc.reflow(", prefix it with an underscore, like this: \"_"), + alloc.symbol_unqualified(argument_symbol), + alloc.reflow(line), + ]), + ]); + + title = UNUSED_ARG.to_string(); + } + Problem::UnusedBranchDef(symbol, region) => { + doc = alloc.stack([ + alloc.concat([ + alloc.symbol_unqualified(symbol), + alloc.reflow(" is not used in this "), + alloc.keyword("when"), + alloc.reflow(" branch."), + ]), + alloc.region(lines.convert_region(region)), + alloc.concat([ + alloc.reflow("If you don't need to use "), + alloc.symbol_unqualified(symbol), + alloc.reflow(", prefix it with an underscore, like \"_"), + alloc.reflow(symbol.as_str(alloc.interns)), + alloc.reflow("\", or replace it with just an \"_\"."), + ]), + ]); + + title = UNUSED_DEF.to_string(); + } + Problem::PrecedenceProblem(BothNonAssociative(region, left_bin_op, right_bin_op)) => { + doc = alloc.stack([ + if left_bin_op.value == right_bin_op.value { + alloc.concat([ + alloc.reflow("Using more than one "), + alloc.binop(left_bin_op.value), + alloc.reflow(concat!( + " like this requires parentheses,", + " to clarify how things should be grouped.", + )), + ]) + } else { + alloc.concat([ + alloc.reflow("Using "), + alloc.binop(left_bin_op.value), + alloc.reflow(" and "), + alloc.binop(right_bin_op.value), + alloc.reflow(concat!( + " together requires parentheses, ", + "to clarify how they should be grouped." + )), + ]) + }, + alloc.region(lines.convert_region(region)), + ]); + + title = SYNTAX_PROBLEM.to_string(); + } + Problem::UnsupportedPattern(BadPattern::Unsupported(pattern_type), region) => { + use roc_parse::pattern::PatternType::*; + + let this_thing = match pattern_type { + TopLevelDef => "a top-level definition:", + DefExpr => "a value definition:", + FunctionArg => "function arguments:", + WhenBranch => unreachable!("all patterns are allowed in a When"), + }; + + let suggestion = [ + alloc.reflow( + "Patterns like this don't cover all possible shapes of the input type. Use a ", + ), + alloc.keyword("when"), + alloc.reflow(" ... "), + alloc.keyword("is"), + alloc.reflow(" instead."), + ]; + + doc = alloc.stack([ + alloc + .reflow("This pattern is not allowed in ") + .append(alloc.reflow(this_thing)), + alloc.region(lines.convert_region(region)), + alloc.concat(suggestion), + ]); + + title = SYNTAX_PROBLEM.to_string(); + } + Problem::Shadowing { + original_region, + shadow, + kind, + } => { + let (res_title, res_doc) = + report_shadowing(alloc, lines, original_region, shadow, kind); + + doc = res_doc; + title = res_title.to_string(); + } + Problem::CyclicAlias(symbol, region, others, alias_kind) => { + let answer = crate::error::r#type::cyclic_alias( + alloc, lines, symbol, region, others, alias_kind, + ); + + doc = answer.0; + title = answer.1; + } + Problem::PhantomTypeArgument { + typ: alias, + variable_region, + variable_name, + alias_kind, + } => { + doc = alloc.stack([ + alloc.concat([ + alloc.reflow("The "), + alloc.type_variable(variable_name), + alloc.reflow(" type parameter is not used in the "), + alloc.symbol_unqualified(alias), + alloc.reflow(" "), + alloc.reflow(alias_kind.as_str()), + alloc.reflow(" definition:"), + ]), + alloc.region(lines.convert_region(variable_region)), + alloc.reflow("Roc does not allow unused type parameters!"), + // TODO add link to this guide section + alloc.tip().append(alloc.reflow( + "If you want an unused type parameter (a so-called \"phantom type\"), \ + read the guide section on phantom values.", + )), + ]); + + title = UNUSED_ALIAS_PARAM.to_string(); + } + Problem::UnboundTypeVariable { + typ: alias, + num_unbound, + one_occurrence, + kind, + } => { + let mut stack = Vec::with_capacity(4); + if num_unbound == 1 { + stack.push(alloc.concat([ + alloc.reflow("The definition of "), + alloc.symbol_unqualified(alias), + alloc.reflow(" has an unbound type variable:"), + ])); + } else { + stack.push(alloc.concat([ + alloc.reflow("The definition of "), + alloc.symbol_unqualified(alias), + alloc.reflow(" has "), + text!(alloc, "{}", num_unbound), + alloc.reflow(" unbound type variables."), + ])); + stack.push(alloc.reflow("Here is one occurrence:")); + } + stack.push(alloc.region(lines.convert_region(one_occurrence))); + stack.push(alloc.tip().append(alloc.concat([ + alloc.reflow("Type variables must be bound before the "), + alloc.keyword(match kind { + AliasKind::Structural => ":", + AliasKind::Opaque => ":=", + }), + alloc.reflow(". Perhaps you intended to add a type parameter to this type?"), + ]))); + doc = alloc.stack(stack); + + title = UNBOUND_TYPE_VARIABLE.to_string(); + } + Problem::BadRecursion(entries) => { + doc = to_circular_def_doc(alloc, lines, &entries); + title = CIRCULAR_DEF.to_string(); + } + Problem::DuplicateRecordFieldValue { + field_name, + field_region, + record_region, + replaced_region, + } => { + doc = alloc.stack([ + alloc.concat([ + alloc.reflow("This record defines the "), + alloc.record_field(field_name.clone()), + alloc.reflow(" field twice!"), + ]), + alloc.region_all_the_things( + lines.convert_region(record_region), + lines.convert_region(replaced_region), + lines.convert_region(field_region), + Annotation::Error, + ), + alloc.reflow(r"In the rest of the program, I will only use the latter definition:"), + alloc.region_all_the_things( + lines.convert_region(record_region), + lines.convert_region(field_region), + lines.convert_region(field_region), + Annotation::TypoSuggestion, + ), + alloc.concat([ + alloc.reflow("For clarity, remove the previous "), + alloc.record_field(field_name), + alloc.reflow(" definitions from this record."), + ]), + ]); + + title = DUPLICATE_FIELD_NAME.to_string(); + } + Problem::InvalidOptionalValue { + field_name, + field_region, + record_region, + } => { + return to_invalid_optional_value_report( + alloc, + lines, + filename, + field_name, + field_region, + record_region, + ); + } + Problem::DuplicateRecordFieldType { + field_name, + field_region, + record_region, + replaced_region, + } => { + doc = alloc.stack([ + alloc.concat([ + alloc.reflow("This record type defines the "), + alloc.record_field(field_name.clone()), + alloc.reflow(" field twice!"), + ]), + alloc.region_all_the_things( + lines.convert_region(record_region), + lines.convert_region(replaced_region), + lines.convert_region(field_region), + Annotation::Error, + ), + alloc.reflow("In the rest of the program, I will only use the latter definition:"), + alloc.region_all_the_things( + lines.convert_region(record_region), + lines.convert_region(field_region), + lines.convert_region(field_region), + Annotation::TypoSuggestion, + ), + alloc.concat([ + alloc.reflow("For clarity, remove the previous "), + alloc.record_field(field_name), + alloc.reflow(" definitions from this record type."), + ]), + ]); + + title = DUPLICATE_FIELD_NAME.to_string(); + } + Problem::DuplicateTag { + tag_name, + tag_union_region, + tag_region, + replaced_region, + } => { + doc = alloc.stack([ + alloc.concat([ + alloc.reflow("This tag union type defines the "), + alloc.tag_name(tag_name.clone()), + alloc.reflow(" tag twice!"), + ]), + alloc.region_all_the_things( + lines.convert_region(tag_union_region), + lines.convert_region(replaced_region), + lines.convert_region(tag_region), + Annotation::Error, + ), + alloc.reflow("In the rest of the program, I will only use the latter definition:"), + alloc.region_all_the_things( + lines.convert_region(tag_union_region), + lines.convert_region(tag_region), + lines.convert_region(tag_region), + Annotation::TypoSuggestion, + ), + alloc.concat([ + alloc.reflow("For clarity, remove the previous "), + alloc.tag_name(tag_name), + alloc.reflow(" definitions from this tag union type."), + ]), + ]); + + title = DUPLICATE_TAG_NAME.to_string(); + } + Problem::SignatureDefMismatch { + ref annotation_pattern, + ref def_pattern, + } => { + doc = alloc.stack([ + alloc.reflow( + "This annotation does not match the definition immediately following it:", + ), + alloc.region( + lines.convert_region(Region::span_across(annotation_pattern, def_pattern)), + ), + alloc.reflow("Is it a typo? If not, put either a newline or comment between them."), + ]); + + title = NAMING_PROBLEM.to_string(); + } + Problem::InvalidAliasRigid { + alias_name: type_name, + region, + } => { + doc = alloc.stack([ + alloc.concat([ + alloc.reflow("This definition of "), + alloc.symbol_unqualified(type_name), + alloc.reflow(" has an unexpected pattern:"), + ]), + alloc.region(lines.convert_region(region)), + alloc.concat([ + alloc.reflow("Only type variables like "), + alloc.type_variable("a".into()), + alloc.reflow(" or "), + alloc.type_variable("value".into()), + alloc.reflow(" can occur in this position."), + ]), + ]); + + title = SYNTAX_PROBLEM.to_string(); + } + Problem::InvalidHexadecimal(region) => { + doc = alloc.stack([ + alloc.reflow("This unicode code point is invalid:"), + alloc.region(lines.convert_region(region)), + alloc.concat([ + alloc.reflow(r"I was expecting a hexadecimal number, like "), + alloc.parser_suggestion("\\u(1100)"), + alloc.reflow(" or "), + alloc.parser_suggestion("\\u(00FF)"), + alloc.text("."), + ]), + alloc.reflow(r"Learn more about working with unicode in roc at TODO"), + ]); + + title = INVALID_UNICODE.to_string(); + } + Problem::InvalidUnicodeCodePt(region) => { + doc = alloc.stack([ + alloc.reflow("This unicode code point is invalid:"), + alloc.region(lines.convert_region(region)), + alloc.reflow("Learn more about working with unicode in roc at TODO"), + ]); + + title = INVALID_UNICODE.to_string(); + } + Problem::InvalidInterpolation(region) => { + doc = alloc.stack([ + alloc.reflow("This string interpolation is invalid:"), + alloc.region(lines.convert_region(region)), + alloc.reflow(r"String interpolations cannot contain newlines or other interpolations."), + alloc.reflow(r"You can learn more about string interpolation at "), + ]); + + title = SYNTAX_PROBLEM.to_string(); + } + Problem::RuntimeError(runtime_error) => { + let answer = pretty_runtime_error(alloc, lines, runtime_error); + + doc = answer.0; + title = answer.1.to_string(); + } + Problem::NestedDatatype { + alias, + def_region, + differing_recursion_region, + } => { + doc = alloc.stack([ + alloc.concat([ + alloc.symbol_unqualified(alias), + alloc.reflow(" is a nested datatype. Here is one recursive usage of it:"), + ]), + alloc.region(lines.convert_region(differing_recursion_region)), + alloc.concat([ + alloc.reflow("But recursive usages of "), + alloc.symbol_unqualified(alias), + alloc.reflow(" must match its definition:"), + ]), + alloc.region(lines.convert_region(def_region)), + alloc.reflow("Nested datatypes are not supported in Roc."), + alloc.concat([ + alloc.hint("Consider rewriting the definition of "), + alloc.symbol_unqualified(alias), + alloc.text(" to use the recursive type with the same arguments."), + ]), + ]); + + title = NESTED_DATATYPE.to_string(); + } + + Problem::InvalidExtensionType { region, kind } => { + let (kind_str, can_only_contain) = match kind { + ExtensionTypeKind::Record => ("record", "a type variable or another record"), + ExtensionTypeKind::TagUnion => { + ("tag union", "a type variable or another tag union") + } + }; + + doc = alloc.stack([ + alloc.concat([ + alloc.reflow("This "), + alloc.text(kind_str), + alloc.reflow(" extension type is invalid:"), + ]), + alloc.region(lines.convert_region(region)), + alloc.concat([ + alloc.note("A "), + alloc.reflow(kind_str), + alloc.reflow(" extension variable can only contain "), + alloc.reflow(can_only_contain), + alloc.reflow("."), + ]), + ]); + + title = INVALID_EXTENSION_TYPE.to_string(); + } + + Problem::AbilityHasTypeVariables { + name, + variables_region, + } => { + doc = alloc.stack([ + alloc.concat([ + alloc.reflow("The definition of the "), + alloc.symbol_unqualified(name), + alloc.reflow(" ability includes type variables:"), + ]), + alloc.region(lines.convert_region(variables_region)), + alloc.reflow( + "Abilities cannot depend on type variables, but their member values can!", + ), + ]); + title = ABILITY_HAS_TYPE_VARIABLES.to_string(); + } + + Problem::ImplementsClauseIsNotAbility { + region: clause_region, + } => { + doc = alloc.stack([ + alloc.reflow( + r#"The type referenced in this "implements" clause is not an ability:"#, + ), + alloc.region(lines.convert_region(clause_region)), + ]); + title = IMPLEMENTS_CLAUSE_IS_NOT_AN_ABILITY.to_string(); + } + + Problem::IllegalImplementsClause { region } => { + doc = alloc.stack([ + alloc.concat([ + alloc.reflow("An "), + alloc.keyword(roc_parse::keyword::IMPLEMENTS), + alloc.reflow(" clause is not allowed here:"), + ]), + alloc.region(lines.convert_region(region)), + alloc.concat([ + alloc.keyword(roc_parse::keyword::IMPLEMENTS), + alloc.reflow( + " clauses can only be specified on the top-level type annotations.", + ), + ]), + ]); + title = ILLEGAL_IMPLEMENTS_CLAUSE.to_string(); + } + + Problem::DuplicateImplementsAbility { ability, region } => { + doc = alloc.stack([ + alloc.concat([ + alloc.reflow("I already saw that this type variable is bound to the "), + alloc.symbol_foreign_qualified(ability), + alloc.reflow(" ability once before:"), + ]), + alloc.region(lines.convert_region(region)), + alloc.concat([ + alloc.reflow("Abilities only need to bound to a type variable once in an "), + alloc.keyword(roc_parse::keyword::IMPLEMENTS), + alloc.reflow(" clause!"), + ]), + ]); + title = "DUPLICATE BOUND ABILITY".to_string(); + } + + Problem::AbilityMemberMissingImplementsClause { + member, + ability, + region, + } => { + doc = alloc.stack([ + alloc.concat([ + alloc.reflow("The definition of the ability member "), + alloc.symbol_unqualified(member), + alloc.reflow(" does not include an "), + alloc.keyword(roc_parse::keyword::IMPLEMENTS), + alloc.reflow(" clause binding a type variable to the ability "), + alloc.symbol_unqualified(ability), + alloc.reflow(":"), + ]), + alloc.region(lines.convert_region(region)), + alloc.concat([ + alloc.reflow("Ability members must include an "), + alloc.keyword(roc_parse::keyword::IMPLEMENTS), + alloc.reflow(" clause binding a type variable to an ability, like"), + ]), + alloc.type_block(alloc.concat([ + alloc.type_variable("a".into()), + alloc.space(), + alloc.keyword(roc_parse::keyword::IMPLEMENTS), + alloc.space(), + alloc.symbol_unqualified(ability), + ])), + alloc.concat([alloc + .reflow("Otherwise, the function does not need to be part of the ability!")]), + ]); + title = ABILITY_MEMBER_MISSING_IMPLEMENTS_CLAUSE.to_string(); + } + + Problem::AbilityMemberMultipleBoundVars { + member, + ability, + span_implements_clauses: span_has_clauses, + mut bound_var_names, + } => { + doc = alloc.stack([ + alloc.concat([ + alloc.reflow("The definition of the ability member "), + alloc.symbol_unqualified(member), + alloc.reflow(" includes multiple variables bound to the "), + alloc.symbol_unqualified(ability), + alloc.keyword(" ability:"), + ]), + alloc.region(lines.convert_region(span_has_clauses)), + alloc.reflow("Ability members can only bind one type variable to their parent ability. Otherwise, I wouldn't know what type implements an ability by looking at specializations!"), + alloc.concat([ + alloc.hint("Did you mean to only bind "), + alloc.type_variable(bound_var_names.swap_remove(0)), + alloc.reflow(" to "), + alloc.symbol_unqualified(ability), + alloc.reflow("?"), + ]) + ]); + title = ABILITY_MEMBER_BINDS_MULTIPLE_VARIABLES.to_string(); + } + + Problem::AbilityNotOnToplevel { region } => { + doc = alloc.stack([ + alloc + .concat([alloc + .reflow("This ability definition is not on the top-level of a module:")]), + alloc.region(lines.convert_region(region)), + alloc.reflow("Abilities can only be defined on the top-level of a Roc module."), + ]); + title = ABILITY_NOT_ON_TOPLEVEL.to_string(); + } + + Problem::AbilityUsedAsType(suggested_var_name, ability, region) => { + doc = alloc.stack([ + alloc.concat([ + alloc.reflow("You are attempting to use the ability "), + alloc.symbol_unqualified(ability), + alloc.reflow(" as a type directly:"), + ]), + alloc.region(lines.convert_region(region)), + alloc.reflow( + "Abilities can only be used in type annotations to constrain type variables.", + ), + alloc + .hint("") + .append(alloc.reflow("Perhaps you meant to include an ")) + .append(alloc.keyword(roc_parse::keyword::IMPLEMENTS)) + .append(alloc.reflow(" annotation, like")), + alloc.type_block(alloc.concat([ + alloc.type_variable(suggested_var_name), + alloc.space(), + alloc.keyword(roc_parse::keyword::IMPLEMENTS), + alloc.space(), + alloc.symbol_unqualified(ability), + ])), + ]); + title = ABILITY_USED_AS_TYPE.to_string(); + } + Problem::NestedSpecialization(member, region) => { + doc = alloc.stack([ + alloc.concat([ + alloc.reflow("This specialization of the "), + alloc.symbol_unqualified(member), + alloc.reflow(" ability member is in a nested scope:"), + ]), + alloc.region(lines.convert_region(region)), + alloc.reflow("Specializations can only be defined on the top-level of a module."), + ]); + title = SPECIALIZATION_NOT_ON_TOPLEVEL.to_string(); + } + Problem::IllegalDerivedAbility(region) => { + doc = alloc.stack([ + alloc.reflow("This ability cannot be derived:"), + alloc.region(lines.convert_region(region)), + alloc.reflow("Only builtin abilities can be derived."), + alloc + .note("The builtin abilities are ") + .append(list_builtin_abilities(alloc)), + ]); + title = ILLEGAL_DERIVE.to_string(); + } + Problem::NotAnAbility(region) => { + doc = alloc.stack([ + alloc.reflow("This identifier is not an ability in scope:"), + alloc.region(lines.convert_region(region)), + alloc.reflow("Only abilities can be implemented."), + ]); + title = NOT_AN_ABILITY.to_string(); + } + Problem::NotAnAbilityMember { + ability, + name, + region, + } => { + doc = alloc.stack([ + alloc.concat([ + alloc.reflow("The "), alloc.symbol_unqualified(ability), alloc.reflow(" ability does not have a member "), alloc.string(name), + ]), + alloc.region(lines.convert_region(region)), + alloc.reflow("Only implementations for members an ability has can be specified in this location.") + ]); + title = NOT_AN_ABILITY_MEMBER.to_string(); + } + Problem::ImplementationNotFound { member, region } => { + let member_str = member.as_str(alloc.interns); + doc = alloc.stack([ + alloc.concat([ + alloc.reflow("An implementation of "), alloc.symbol_unqualified(member), alloc.reflow(" could not be found in this scope:"), + ]), + alloc.region(lines.convert_region(region)), + alloc.tip().append(alloc.concat([alloc.reflow("consider adding a value of name "), alloc.symbol_unqualified(member), alloc.reflow(" in this scope, or using another variable that implements this ability member, like "), alloc.type_str(&format!("{{ {member_str}: my{member_str} }}"))])) + ]); + title = IMPLEMENTATION_NOT_FOUND.to_string(); + } + Problem::OptionalAbilityImpl { ability, region } => { + let hint = if ability.is_builtin() { + alloc.hint("").append( + alloc.reflow("if you want this implementation to be derived, don't include a record of implementations. For example,") + .append(alloc.type_block(alloc.concat([alloc.type_str(roc_parse::keyword::IMPLEMENTS), alloc.type_str(" ["), alloc.symbol_unqualified(ability), alloc.type_str("]")]))) + .append(alloc.reflow(" will attempt to derive ").append(alloc.symbol_unqualified(ability)))) + } else { + alloc.nil() + }; + + doc = alloc.stack([ + alloc.reflow("Ability implementations cannot be optional:"), + alloc.region(lines.convert_region(region)), + alloc.reflow("Custom implementations must be supplied fully."), + hint, + ]); + title = OPTIONAL_ABILITY_IMPLEMENTATION.to_string(); + } + Problem::QualifiedAbilityImpl { region } => { + doc = alloc.stack([ + alloc.reflow("This ability implementation is qualified:"), + alloc.region(lines.convert_region(region)), + alloc.reflow( + "Custom implementations must be defined in the local scope, and unqualified.", + ), + ]); + title = QUALIFIED_ABILITY_IMPLEMENTATION.to_string(); + } + Problem::AbilityImplNotIdent { region } => { + doc = alloc.stack([ + alloc.reflow("This ability implementation is not an identifier:"), + alloc.region(lines.convert_region(region)), + alloc.reflow( + "Custom ability implementations defined in this position can only be unqualified identifiers, not arbitrary expressions.", + ), + alloc.tip().append(alloc.reflow("consider defining this expression as a variable.")) + ]); + title = ABILITY_IMPLEMENTATION_NOT_IDENTIFIER.to_string(); + } + Problem::DuplicateImpl { + original, + duplicate, + } => { + doc = alloc.stack([ + alloc.reflow("This ability member implementation is duplicate:"), + alloc.region(lines.convert_region(duplicate)), + alloc.reflow("The first implementation was defined here:"), + alloc.region(lines.convert_region(original)), + alloc + .reflow("Only one custom implementation can be defined for an ability member."), + ]); + title = DUPLICATE_IMPLEMENTATION.to_string(); + } + Problem::ImplementsNonRequired { + region, + ability, + not_required, + } => { + doc = alloc.stack([ + alloc.concat([ + alloc.reflow("This type implements members that are not part of the "), + alloc.symbol_unqualified(ability), + alloc.reflow(" ability:"), + ]), + alloc.region(lines.convert_region(region)), + alloc.reflow("The following implemented members should not be listed:"), + alloc.type_block( + alloc.intersperse( + not_required + .into_iter() + .map(|sym| alloc.symbol_unqualified(sym)), + alloc.string(",".to_string()).append(alloc.space()), + ), + ), + ]); + title = UNNECESSARY_IMPLEMENTATIONS.to_string(); + } + Problem::DoesNotImplementAbility { + region, + ability, + not_implemented, + } => { + doc = alloc.stack([ + alloc.concat([ + alloc.reflow("This type does not fully implement the "), + alloc.symbol_unqualified(ability), + alloc.reflow(" ability:"), + ]), + alloc.region(lines.convert_region(region)), + alloc.reflow("The following necessary members are missing implementations:"), + alloc.type_block( + alloc.intersperse( + not_implemented + .into_iter() + .map(|sym| alloc.symbol_unqualified(sym)), + alloc.string(",".to_string()).append(alloc.space()), + ), + ), + ]); + title = INCOMPLETE_ABILITY_IMPLEMENTATION.to_string(); + } + Problem::NotBoundInAllPatterns { + unbound_symbol, + region, + } => { + doc = alloc.stack([ + alloc.concat([ + alloc.symbol_unqualified(unbound_symbol), + alloc.reflow(" is not bound in all patterns of this "), + alloc.keyword("when"), + alloc.reflow(" branch"), + ]), + alloc.region(lines.convert_region(region)), + alloc.concat([ + alloc.reflow("Identifiers introduced in a "), + alloc.keyword("when"), + alloc.reflow(" branch must be bound in all patterns of the branch. Otherwise, the program would crash when it tries to use an identifier that wasn't bound!"), + ]), + ]); + title = "NAME NOT BOUND IN ALL PATTERNS".to_string(); + } + Problem::NoIdentifiersIntroduced(region) => { + doc = alloc.stack([ + alloc.reflow("This destructure assignment doesn't introduce any new variables:"), + alloc.region(lines.convert_region(region)), + alloc.reflow("If you don't need to use the value on the right-hand-side of this assignment, consider removing the assignment. Since Roc is purely functional, assignments that don't introduce variables cannot affect a program's behavior!"), + ]); + title = "UNNECESSARY DEFINITION".to_string(); + } + Problem::OverloadedSpecialization { + ability_member, + overload, + original_opaque, + } => { + doc = alloc.stack([ + alloc.reflow("This ability member specialization is already claimed to specialize another opaque type:"), + alloc.region(lines.convert_region(overload)), + alloc.concat([ + alloc.reflow("Previously, we found it to specialize "), + alloc.symbol_unqualified(ability_member), + alloc.reflow(" for "), + alloc.symbol_unqualified(original_opaque), + alloc.reflow("."), + ]), + alloc.reflow("Ability specializations can only provide implementations for one opaque type, since all opaque types are different!"), + ]); + title = "OVERLOADED SPECIALIZATION".to_string(); + } + Problem::UnnecessaryOutputWildcard { region } => { + doc = alloc.stack([ + alloc.concat([ + alloc.reflow("This type annotation has a wildcard type variable ("), + alloc.keyword("*"), + alloc.reflow(") that isn't needed."), + ]), + alloc.region(lines.convert_region(region)), + alloc.concat([ + alloc.reflow("Annotations for tag unions which are constants, or which are returned from functions, work the same way with or without a "), + alloc.keyword("*"), + alloc.reflow(" at the end. (The "), + alloc.keyword("*"), + alloc.reflow(" means something different when the tag union is an argument to a function, though!)"), + ]), + alloc.reflow("You can safely remove this to make the code more concise without changing what it means."), + ]); + title = "UNNECESSARY WILDCARD".to_string(); + } + Problem::MultipleListRestPattern { region } => { + doc = alloc.stack([ + alloc.reflow("This list pattern match has multiple rest patterns:"), + alloc.region(lines.convert_region(region)), + alloc.concat([ + alloc.reflow("I only support compiling list patterns with one "), + alloc.parser_suggestion(".."), + alloc.reflow(" pattern! Can you remove this additional one?"), + ]), + ]); + title = "MULTIPLE LIST REST PATTERNS".to_string(); + } + Problem::BadTypeArguments { + symbol, + region, + type_got, + alias_needs, + alias_kind, + } => { + let needed_arguments = if alias_needs == 1 { + alloc.reflow("1 type argument") + } else { + alloc + .text(alias_needs.to_string()) + .append(alloc.reflow(" type arguments")) + }; + + let found_arguments = alloc.text(type_got.to_string()); + + doc = alloc.stack([ + alloc.concat([ + alloc.reflow("The "), + alloc.symbol_unqualified(symbol), + alloc.reflow(" "), + alloc.reflow(alias_kind.as_str()), + alloc.reflow(" expects "), + needed_arguments, + alloc.reflow(", but it got "), + found_arguments, + alloc.reflow(" instead:"), + ]), + alloc.region(lines.convert_region(region)), + alloc.reflow("Are there missing parentheses?"), + ]); + + title = if type_got > alias_needs { + "TOO MANY TYPE ARGUMENTS".to_string() + } else { + "TOO FEW TYPE ARGUMENTS".to_string() + }; + } + Problem::UnappliedCrash { region } => { + doc = alloc.stack([ + alloc.concat([ + alloc.reflow("This "), alloc.keyword("crash"), alloc.reflow(" doesn't have a message given to it:") + ]), + alloc.region(lines.convert_region(region)), + alloc.concat([ + alloc.keyword("crash"), alloc.reflow(" must be passed a message to crash with at the exact place it's used. "), + alloc.keyword("crash"), alloc.reflow(" can't be used as a value that's passed around, like functions can be - it must be applied immediately!"), + ]) + ]); + title = "UNAPPLIED CRASH".to_string(); + } + Problem::OverAppliedCrash { region } => { + doc = alloc.stack([ + alloc.concat([ + alloc.reflow("This "), + alloc.keyword("crash"), + alloc.reflow(" has too many values given to it:"), + ]), + alloc.region(lines.convert_region(region)), + alloc.concat([ + alloc.keyword("crash"), + alloc.reflow(" must be given exacly one message to crash with."), + ]), + ]); + title = "OVERAPPLIED CRASH".to_string(); + } + Problem::FileProblem { filename, error } => { + let report = to_file_problem_report(alloc, &filename, error); + doc = report.doc; + title = report.title; + } + }; + + Report { + title, + filename, + doc, + severity, + } +} + +fn list_builtin_abilities<'a>(alloc: &'a RocDocAllocator<'a>) -> RocDocBuilder<'a> { + alloc.intersperse( + DERIVABLE_ABILITIES + .iter() + .map(|(ab, _)| alloc.symbol_unqualified(*ab)), + alloc.reflow(", "), + ) +} + +fn to_invalid_optional_value_report<'b>( + alloc: &'b RocDocAllocator<'b>, + lines: &LineInfo, + filename: PathBuf, + field_name: Lowercase, + field_region: Region, + record_region: Region, +) -> Report<'b> { + let doc = to_invalid_optional_value_report_help( + alloc, + lines, + field_name, + field_region, + record_region, + ); + + Report { + title: "BAD OPTIONAL VALUE".to_string(), + filename, + doc, + severity: Severity::RuntimeError, + } +} + +fn to_invalid_optional_value_report_help<'b>( + alloc: &'b RocDocAllocator<'b>, + lines: &LineInfo, + field_name: Lowercase, + field_region: Region, + record_region: Region, +) -> RocDocBuilder<'b> { + alloc.stack([ + alloc.concat([ + alloc.reflow("This record uses an optional value for the "), + alloc.record_field(field_name), + alloc.reflow(" field in an incorrect context!"), + ]), + alloc.region_all_the_things( + lines.convert_region(record_region), + lines.convert_region(field_region), + lines.convert_region(field_region), + Annotation::Error, + ), + alloc.reflow(r"You can only use optional values in record destructuring, like:"), + alloc + .reflow(r"{ answer ? 42, otherField } = myRecord") + .indent(4), + ]) +} + +fn to_bad_ident_expr_report<'b>( + alloc: &'b RocDocAllocator<'b>, + lines: &LineInfo, + bad_ident: roc_parse::ident::BadIdent, + surroundings: Region, +) -> RocDocBuilder<'b> { + use roc_parse::ident::BadIdent::*; + + match bad_ident { + Start(_) | Space(_, _) => unreachable!("these are handled in the parser"), + WeirdDotAccess(pos) | StrayDot(pos) => { + let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); + + alloc.stack([ + alloc.reflow(r"I am trying to parse a record field access here:"), + alloc.region_with_subregion(lines.convert_region(surroundings), region), + alloc.concat([ + alloc.reflow("So I expect to see a lowercase letter next, like "), + alloc.parser_suggestion(".name"), + alloc.reflow(" or "), + alloc.parser_suggestion(".height"), + alloc.reflow("."), + ]), + ]) + } + + WeirdAccessor(_pos) => alloc.stack([ + alloc.reflow("I am very confused by this field access"), + alloc.region(lines.convert_region(surroundings)), + alloc.concat([ + alloc.reflow("It looks like a field access on an accessor. I parse"), + alloc.parser_suggestion(".client.name"), + alloc.reflow(" as "), + alloc.parser_suggestion("(.client).name"), + alloc.reflow(". Maybe use an anonymous function like "), + alloc.parser_suggestion("(\\r -> r.client.name)"), + alloc.reflow(" instead"), + alloc.reflow("?"), + ]), + ]), + + WeirdDotQualified(pos) => { + let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); + + alloc.stack([ + alloc.reflow("I am trying to parse a qualified name here:"), + alloc.region_with_subregion(lines.convert_region(surroundings), region), + alloc.concat([ + alloc.reflow("I was expecting to see an identifier next, like "), + alloc.parser_suggestion("height"), + alloc.reflow(". A complete qualified name looks something like "), + alloc.parser_suggestion("Json.Decode.string"), + alloc.text("."), + ]), + ]) + } + QualifiedTupleAccessor(pos) => { + let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); + + alloc.stack([ + alloc.reflow("I am trying to parse a qualified name here:"), + alloc.region_with_subregion(lines.convert_region(surroundings), region), + alloc.concat([ + alloc.reflow("This looks like a tuple accessor on a module or tag name,"), + alloc.reflow(r"but neither modules nor tags can have tuple elements! "), + alloc.reflow(r"Maybe you wanted a qualified name, something like "), + alloc.parser_suggestion("Json.Decode.string"), + alloc.text("."), + ]), + ]) + } + QualifiedTag(pos) => { + let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); + + alloc.stack([ + alloc.reflow("I am trying to parse a qualified name here:"), + alloc.region_with_subregion(lines.convert_region(surroundings), region), + alloc.concat([ + alloc.reflow(r"This looks like a qualified tag name to me, "), + alloc.reflow(r"but tags cannot be qualified! "), + alloc.reflow(r"Maybe you wanted a qualified name, something like "), + alloc.parser_suggestion("Json.Decode.string"), + alloc.text("?"), + ]), + ]) + } + + UnderscoreAlone(_pos) => { + alloc.stack([ + alloc.reflow("An underscore is being used as a variable here:"), + alloc.region(lines.convert_region(surroundings)), + alloc.concat([alloc + .reflow(r"An underscore can be used to ignore a value when pattern matching, but it cannot be used as a variable.")]), + ]) + } + + UnderscoreInMiddle(_pos) => { + alloc.stack([ + alloc.reflow("Underscores are not allowed in identifier names:"), + alloc.region(lines.convert_region(surroundings)), + alloc.concat([alloc + .reflow(r"I recommend using camelCase. It's the standard style in Roc code!")]), + ]) + } + + UnderscoreAtStart { + position: _pos, + declaration_region, + } => { + let line = "This variable's name starts with an underscore:"; + alloc.stack([ + match declaration_region { + None => alloc.reflow(line), + Some(declaration_region) => alloc.stack([ + alloc.reflow(line), + alloc.region(lines.convert_region(declaration_region)), + alloc.reflow("But then it is used here:"), + ]) + }, + alloc.region(lines.convert_region(surroundings)), + alloc.concat([ + alloc.reflow(r"A variable's name can only start with an underscore if the variable is unused. "), + match declaration_region { + None => alloc.reflow(r"But it looks like the variable is being used here!"), + Some(_) => alloc.reflow(r"Since you are using this variable, you could remove the underscore from its name in both places."), + } + ]), + ]) + } + + BadOpaqueRef(pos) => { + use BadIdentNext::*; + let kind = "an opaque reference"; + + match what_is_next(alloc.src_lines, lines.convert_pos(pos)) { + LowercaseAccess(width) => { + let region = Region::new(pos, pos.bump_column(width)); + alloc.stack([ + alloc.reflow("I am very confused by this field access:"), + alloc.region_with_subregion( + lines.convert_region(surroundings), + lines.convert_region(region), + ), + alloc.concat([ + alloc.reflow(r"It looks like a record field access on "), + alloc.reflow(kind), + alloc.text("."), + ]), + ]) + } + UppercaseAccess(width) => { + let region = Region::new(pos, pos.bump_column(width)); + alloc.stack([ + alloc.reflow("I am very confused by this expression:"), + alloc.region_with_subregion( + lines.convert_region(surroundings), + lines.convert_region(region), + ), + alloc.concat([ + alloc.reflow(r"Looks like "), + alloc.reflow(kind), + alloc.reflow(" is treated like a module name. "), + alloc.reflow(r"Maybe you wanted a qualified name, like "), + alloc.parser_suggestion("Json.Decode.string"), + alloc.text("?"), + ]), + ]) + } + Other(Some(c)) if c.is_lowercase() => { + let region = + Region::new(surroundings.start().bump_column(1), pos.bump_column(1)); + alloc.stack([ + alloc.concat([ + alloc.reflow("I am trying to parse "), + alloc.reflow(kind), + alloc.reflow(" here:"), + ]), + alloc.region_with_subregion( + lines.convert_region(surroundings), + lines.convert_region(region), + ), + alloc.concat([ + alloc.reflow(r"But after the "), + alloc.keyword("@"), + alloc.reflow(r" symbol I found a lowercase letter. "), + alloc.reflow(r"All opaque references "), + alloc.reflow(r" must start with an uppercase letter, like "), + alloc.parser_suggestion("@UUID"), + alloc.reflow(" or "), + alloc.parser_suggestion("@Secrets"), + alloc.reflow("."), + ]), + ]) + } + other => todo!("{:?}", other), + } + } + } +} + +fn to_bad_ident_pattern_report<'b>( + alloc: &'b RocDocAllocator<'b>, + lines: &LineInfo, + bad_ident: roc_parse::ident::BadIdent, + surroundings: Region, +) -> RocDocBuilder<'b> { + use roc_parse::ident::BadIdent::*; + + match bad_ident { + Start(_) | Space(_, _) => unreachable!("these are handled in the parser"), + WeirdDotAccess(pos) | StrayDot(pos) => { + let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); + + alloc.stack([ + alloc.reflow(r"I am trying to parse a record field accessor here:"), + alloc.region_with_subregion(lines.convert_region(surroundings), region), + alloc.concat([ + alloc.reflow("Something like "), + alloc.parser_suggestion(".name"), + alloc.reflow(" or "), + alloc.parser_suggestion(".height"), + alloc.reflow(" that accesses a value from a record."), + ]), + ]) + } + + WeirdAccessor(_pos) => alloc.stack([ + alloc.reflow("I am very confused by this field access"), + alloc.region(lines.convert_region(surroundings)), + alloc.concat([ + alloc.reflow("It looks like a field access on an accessor. I parse"), + alloc.parser_suggestion(".client.name"), + alloc.reflow(" as "), + alloc.parser_suggestion("(.client).name"), + alloc.reflow(". Maybe use an anonymous function like "), + alloc.parser_suggestion("(\\r -> r.client.name)"), + alloc.reflow(" instead"), + alloc.reflow("?"), + ]), + ]), + + QualifiedTupleAccessor(pos) | WeirdDotQualified(pos) => { + let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); + + alloc.stack([ + alloc.reflow("I am trying to parse a qualified name here:"), + alloc.region_with_subregion(lines.convert_region(surroundings), region), + alloc.concat([ + alloc.reflow("I was expecting to see an identifier next, like "), + alloc.parser_suggestion("height"), + alloc.reflow(". A complete qualified name looks something like "), + alloc.parser_suggestion("Json.Decode.string"), + alloc.text("."), + ]), + ]) + } + QualifiedTag(pos) => { + let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); + + alloc.stack([ + alloc.reflow("I am trying to parse a qualified name here:"), + alloc.region_with_subregion(lines.convert_region(surroundings), region), + alloc.concat([ + alloc.reflow(r"This looks like a qualified tag name to me, "), + alloc.reflow(r"but tags cannot be qualified! "), + alloc.reflow(r"Maybe you wanted a qualified name, something like "), + alloc.parser_suggestion("Json.Decode.string"), + alloc.text("?"), + ]), + ]) + } + + UnderscoreAlone(..) | UnderscoreAtStart { .. } => { + unreachable!( + "it's fine to have an underscore at the beginning of an identifier in a pattern" + ) + } + + UnderscoreInMiddle(pos) => { + let region = Region::from_pos(pos.sub(1)); + + alloc.stack([ + alloc.reflow("I am trying to parse an identifier here:"), + alloc.region_with_subregion( + lines.convert_region(surroundings), + lines.convert_region(region), + ), + alloc.concat([alloc.reflow( + r"Underscores are not allowed in identifiers. Use camelCase instead!", + )]), + ]) + } + + BadOpaqueRef(pos) => { + let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); + + alloc.stack([ + alloc.reflow("This opaque type reference has an invalid name:"), + alloc.region_with_subregion(lines.convert_region(surroundings), region), + alloc.concat([ + alloc.reflow(r"Opaque type names must begin with a capital letter, "), + alloc.reflow(r"and must contain only letters and numbers."), + ]), + ]) + } + } +} + +#[derive(Debug)] +enum BadIdentNext<'a> { + LowercaseAccess(u32), + UppercaseAccess(u32), + NumberAccess(u32), + Keyword(&'a str), + DanglingDot, + Other(Option), +} + +fn what_is_next<'a>(source_lines: &'a [&'a str], pos: LineColumn) -> BadIdentNext<'a> { + let row_index = pos.line as usize; + let col_index = pos.column as usize; + match source_lines.get(row_index) { + None => BadIdentNext::Other(None), + Some(line) => { + let chars = &line[col_index..]; + let mut it = chars.chars(); + + match roc_parse::keyword::KEYWORDS + .iter() + .find(|keyword| crate::error::parse::starts_with_keyword(chars, keyword)) + { + Some(keyword) => BadIdentNext::Keyword(keyword), + None => match it.next() { + None => BadIdentNext::Other(None), + Some('.') => match it.next() { + Some(c) if c.is_lowercase() => { + BadIdentNext::LowercaseAccess(2 + till_whitespace(it) as u32) + } + Some(c) if c.is_uppercase() => { + BadIdentNext::UppercaseAccess(2 + till_whitespace(it) as u32) + } + Some(c) if c.is_ascii_digit() => { + BadIdentNext::NumberAccess(2 + till_whitespace(it) as u32) + } + _ => BadIdentNext::DanglingDot, + }, + Some(c) => BadIdentNext::Other(Some(c)), + }, + } + } + } +} + +fn till_whitespace(it: I) -> usize +where + I: Iterator, +{ + let mut chomped = 0; + + for c in it { + if c.is_ascii_whitespace() || c == '#' { + break; + } else { + chomped += 1; + continue; + } + } + + chomped +} + +fn report_shadowing<'b>( + alloc: &'b RocDocAllocator<'b>, + lines: &LineInfo, + original_region: Region, + shadow: Loc, + kind: ShadowKind, +) -> (&'static str, RocDocBuilder<'b>) { + let (what, what_plural, is_builtin) = match kind { + ShadowKind::Variable => ("variable", "variables", false), + ShadowKind::Alias(sym) => ("alias", "aliases", sym.is_builtin()), + ShadowKind::Opaque(sym) => ("opaque type", "opaque types", sym.is_builtin()), + ShadowKind::Ability(sym) => ("ability", "abilities", sym.is_builtin()), + }; + + let doc = if is_builtin { + alloc.stack([ + alloc.concat([ + alloc.reflow("This "), + alloc.reflow(what), + alloc.reflow(" has the same name as a builtin:"), + ]), + alloc.region(lines.convert_region(shadow.region)), + alloc.concat([ + alloc.reflow("All builtin "), + alloc.reflow(what_plural), + alloc.reflow(" are in scope by default, so I need this "), + alloc.reflow(what), + alloc.reflow(" to have a different name!"), + ]), + ]) + } else { + alloc.stack([ + alloc + .text("The ") + .append(alloc.ident(shadow.value)) + .append(alloc.reflow(" name is first defined here:")), + alloc.region(lines.convert_region(original_region)), + alloc.reflow("But then it's defined a second time here:"), + alloc.region(lines.convert_region(shadow.region)), + alloc.concat([ + alloc.reflow("Since these "), + alloc.reflow(what_plural), + alloc.reflow(" have the same name, it's easy to use the wrong one by accident. Give one of them a new name."), + ]), + ]) + }; + + (DUPLICATE_NAME, doc) +} + +fn pretty_runtime_error<'b>( + alloc: &'b RocDocAllocator<'b>, + lines: &LineInfo, + runtime_error: RuntimeError, +) -> (RocDocBuilder<'b>, &'static str) { + let doc; + let title; + + match runtime_error { + RuntimeError::VoidValue => { + // is used to communicate to the compiler that + // a branch is unreachable; this should never reach a user + unreachable!(""); + } + + RuntimeError::UnresolvedTypeVar | RuntimeError::ErroneousType => { + // only generated during layout generation + unreachable!(""); + } + + RuntimeError::Shadowing { + original_region, + shadow, + kind, + } => { + (title, doc) = report_shadowing(alloc, lines, original_region, shadow, kind); + } + + RuntimeError::LookupNotInScope { + loc_name, + suggestion_options: options, + underscored_suggestion_region, + } => { + doc = not_found( + alloc, + lines, + loc_name.region, + &loc_name.value, + options, + underscored_suggestion_region, + ); + title = UNRECOGNIZED_NAME; + } + RuntimeError::CircularDef(entries) => { + doc = to_circular_def_doc(alloc, lines, &entries); + title = CIRCULAR_DEF; + } + RuntimeError::MalformedPattern(problem, region) => { + use roc_parse::ast::Base; + use roc_problem::can::MalformedPatternProblem::*; + + let name = match problem { + MalformedInt => " integer ", + MalformedFloat => " float ", + MalformedBase(Base::Hex) => " hex integer ", + MalformedBase(Base::Binary) => " binary integer ", + MalformedBase(Base::Octal) => " octal integer ", + MalformedBase(Base::Decimal) => " integer ", + BadIdent(bad_ident) => { + title = NAMING_PROBLEM; + doc = to_bad_ident_pattern_report(alloc, lines, bad_ident, region); + + return (doc, title); + } + Unknown => " ", + QualifiedIdentifier => " qualified ", + EmptySingleQuote => " empty character literal ", + MultipleCharsInSingleQuote => " overfull literal ", + DuplicateListRestPattern => " second rest pattern ", + }; + + let tip = match problem { + MalformedInt | MalformedFloat | MalformedBase(_) => alloc + .tip() + .append(alloc.reflow("Learn more about number literals at TODO")), + EmptySingleQuote | MultipleCharsInSingleQuote | Unknown | BadIdent(_) => { + alloc.nil() + } + QualifiedIdentifier => alloc + .tip() + .append(alloc.reflow("In patterns, only tags can be qualified")), + DuplicateListRestPattern => alloc + .tip() + .append(alloc.reflow("List patterns can only have one rest pattern")), + }; + + doc = alloc.stack([ + alloc.concat([ + alloc.reflow("This"), + alloc.text(name), + alloc.reflow("pattern is malformed:"), + ]), + alloc.region(lines.convert_region(region)), + tip, + ]); + + title = SYNTAX_PROBLEM; + } + RuntimeError::UnsupportedPattern(_) => { + todo!("unsupported patterns are currently not parsed!") + } + RuntimeError::ValueNotExposed { + module_name, + ident, + region, + exposed_values, + } => { + let mut suggestions = suggest::sort(ident.as_ref(), exposed_values); + suggestions.truncate(4); + + let did_you_mean = if suggestions.is_empty() { + alloc.concat([ + alloc.reflow("In fact, it looks like "), + alloc.module_name(module_name.clone()), + alloc.reflow(" doesn't expose any values!"), + ]) + } else { + let qualified_suggestions = suggestions + .into_iter() + .map(|v| alloc.string(module_name.to_string() + "." + v.as_str())); + alloc.stack([ + alloc.reflow("Did you mean one of these?"), + alloc.vcat(qualified_suggestions).indent(4), + ]) + }; + doc = alloc.stack([ + alloc.concat([ + alloc.reflow("The "), + alloc.module_name(module_name), + alloc.reflow(" module does not expose `"), + alloc.string(ident.to_string()), + alloc.reflow("`:"), + ]), + alloc.region(lines.convert_region(region)), + did_you_mean, + ]); + + title = VALUE_NOT_EXPOSED; + } + + RuntimeError::ModuleNotImported { + module_name, + imported_modules, + region, + module_exists, + } => { + doc = module_not_found( + alloc, + lines, + region, + &module_name, + imported_modules, + module_exists, + ); + + title = MODULE_NOT_IMPORTED; + } + RuntimeError::InvalidPrecedence(_, _) => { + // do nothing, reported with PrecedenceProblem + unreachable!(); + } + RuntimeError::MalformedIdentifier(_box_str, bad_ident, surroundings) => { + doc = to_bad_ident_expr_report(alloc, lines, bad_ident, surroundings); + + title = SYNTAX_PROBLEM; + } + RuntimeError::MalformedTypeName(_box_str, surroundings) => { + doc = alloc.stack([ + alloc.reflow(r"I am confused by this type name:"), + alloc.region(lines.convert_region(surroundings)), + alloc.concat([ + alloc.reflow("Type names start with an uppercase letter, "), + alloc.reflow("and can optionally be qualified by a module name, like "), + alloc.parser_suggestion("Bool"), + alloc.reflow(" or "), + alloc.parser_suggestion("Http.Request.Request"), + alloc.reflow("."), + ]), + ]); + + title = SYNTAX_PROBLEM; + } + RuntimeError::MalformedClosure(_) => { + todo!(""); + } + RuntimeError::InvalidFloat(sign @ FloatErrorKind::PositiveInfinity, region, _raw_str) + | RuntimeError::InvalidFloat(sign @ FloatErrorKind::NegativeInfinity, region, _raw_str) => { + let tip = alloc + .tip() + .append(alloc.reflow("Learn more about number literals at TODO")); + + let big_or_small = if let FloatErrorKind::PositiveInfinity = sign { + "big" + } else { + "small" + }; + + doc = alloc.stack([ + alloc.concat([ + alloc.reflow("This float literal is too "), + alloc.text(big_or_small), + alloc.reflow(":"), + ]), + alloc.region(lines.convert_region(region)), + alloc.concat([ + alloc + .reflow("Roc uses signed 64-bit floating points, allowing values between "), + text!(alloc, "{:e}", f64::MIN), + alloc.reflow(" and "), + text!(alloc, "{:e}", f64::MAX), + ]), + tip, + ]); + + title = SYNTAX_PROBLEM; + } + RuntimeError::InvalidFloat(FloatErrorKind::Error, region, _raw_str) => { + let tip = alloc + .tip() + .append(alloc.reflow("Learn more about number literals at TODO")); + + doc = alloc.stack([ + alloc.concat([ + alloc.reflow("This float literal contains an invalid digit:"), + ]), + alloc.region(lines.convert_region(region)), + alloc.concat([ + alloc.reflow("Floating point literals can only contain the digits 0-9, or use scientific notation 10e4, or have a float suffix."), + ]), + tip, + ]); + + title = SYNTAX_PROBLEM; + } + RuntimeError::InvalidFloat(FloatErrorKind::IntSuffix, region, _raw_str) => { + doc = alloc.stack([ + alloc + .concat([alloc + .reflow("This number literal is a float, but it has an integer suffix:")]), + alloc.region(lines.convert_region(region)), + ]); + + title = CONFLICTING_NUMBER_SUFFIX; + } + RuntimeError::InvalidInt(error @ IntErrorKind::InvalidDigit, base, region, _raw_str) + | RuntimeError::InvalidInt(error @ IntErrorKind::Empty, base, region, _raw_str) => { + use roc_parse::ast::Base::*; + + let (problem, contains) = if let IntErrorKind::InvalidDigit = error { + ( + "an invalid digit", + alloc.reflow(" can only contain the digits "), + ) + } else { + ( + "no digits", + alloc.reflow(" must contain at least one of the digits "), + ) + }; + + let name = match base { + Decimal => "integer", + Octal => "octal integer", + Hex => "hex integer", + Binary => "binary integer", + }; + + let plurals = match base { + Decimal => "Integer literals", + Octal => "Octal (base-8) integer literals", + Hex => "Hexadecimal (base-16) integer literals", + Binary => "Binary (base-2) integer literals", + }; + + let charset = match base { + Decimal => "0-9", + Octal => "0-7", + Hex => "0-9, a-f and A-F", + Binary => "0 and 1", + }; + + let tip = alloc + .tip() + .append(alloc.reflow("Learn more about number literals at TODO")); + + doc = alloc.stack([ + alloc.concat([ + alloc.reflow("This "), + alloc.text(name), + alloc.reflow(" literal contains "), + alloc.text(problem), + alloc.text(":"), + ]), + alloc.region(lines.convert_region(region)), + alloc.concat([ + alloc.text(plurals), + contains, + alloc.text(charset), + alloc.text(", or have an integer suffix."), + ]), + tip, + ]); + + title = SYNTAX_PROBLEM; + } + RuntimeError::InvalidInt(error_kind @ IntErrorKind::Underflow, _base, region, _raw_str) + | RuntimeError::InvalidInt(error_kind @ IntErrorKind::Overflow, _base, region, _raw_str) => { + let (big_or_small, info) = if let IntErrorKind::Underflow = error_kind { + ( + "small", + alloc.concat([ + alloc.reflow( + "The smallest number representable in Roc is the minimum I128 value, ", + ), + alloc.int_literal(i128::MIN), + alloc.text("."), + ]), + ) + } else { + ( + "big", + alloc.concat([ + alloc.reflow( + "The largest number representable in Roc is the maximum U128 value, ", + ), + alloc.int_literal(u128::MAX), + alloc.text("."), + ]), + ) + }; + + let tip = alloc + .tip() + .append(alloc.reflow("Learn more about number literals at TODO")); + + doc = alloc.stack([ + alloc.concat([ + alloc.reflow("This integer literal is too "), + alloc.text(big_or_small), + alloc.reflow(":"), + ]), + alloc.region(lines.convert_region(region)), + info, + tip, + ]); + + title = SYNTAX_PROBLEM; + } + RuntimeError::InvalidInt(IntErrorKind::FloatSuffix, _base, region, _raw_str) => { + doc = alloc.stack([ + alloc + .concat([alloc + .reflow("This number literal is an integer, but it has a float suffix:")]), + alloc.region(lines.convert_region(region)), + ]); + + title = CONFLICTING_NUMBER_SUFFIX; + } + RuntimeError::InvalidInt( + IntErrorKind::OverflowsSuffix { + suffix_type, + max_value, + }, + _base, + region, + _raw_str, + ) => { + doc = alloc.stack([ + alloc.concat([alloc + .reflow("This integer literal overflows the type indicated by its suffix:")]), + alloc.region(lines.convert_region(region)), + alloc.tip().append(alloc.concat([ + alloc.reflow("The suffix indicates this integer is a "), + alloc.type_str(suffix_type), + alloc.reflow(", whose maximum value is "), + alloc.int_literal(max_value), + alloc.reflow("."), + ])), + ]); + + title = NUMBER_OVERFLOWS_SUFFIX; + } + RuntimeError::InvalidInt( + IntErrorKind::UnderflowsSuffix { + suffix_type, + min_value, + }, + _base, + region, + _raw_str, + ) => { + doc = alloc.stack([ + alloc.concat([alloc + .reflow("This integer literal underflows the type indicated by its suffix:")]), + alloc.region(lines.convert_region(region)), + alloc.tip().append(alloc.concat([ + alloc.reflow("The suffix indicates this integer is a "), + alloc.type_str(suffix_type), + alloc.reflow(", whose minimum value is "), + alloc.int_literal(min_value), + alloc.reflow("."), + ])), + ]); + + title = NUMBER_UNDERFLOWS_SUFFIX; + } + RuntimeError::InvalidOptionalValue { + field_name, + field_region, + record_region, + } => { + doc = to_invalid_optional_value_report_help( + alloc, + lines, + field_name, + field_region, + record_region, + ); + + title = SYNTAX_PROBLEM; + } + RuntimeError::InvalidRecordUpdate { region } => { + doc = alloc.stack([ + alloc.concat([ + alloc.reflow("This expression cannot be updated"), + alloc.reflow(":"), + ]), + alloc.region(lines.convert_region(region)), + alloc.reflow("Only variables can be updated with record update syntax."), + ]); + + title = SYNTAX_PROBLEM; + } + RuntimeError::InvalidHexadecimal(region) => { + todo!( + "TODO runtime error for an invalid hexadecimal number in a \\u(...) code point at region {:?}", + region + ); + } + RuntimeError::InvalidUnicodeCodePt(region) => { + todo!( + "TODO runtime error for an invalid \\u(...) code point at region {:?}", + region + ); + } + RuntimeError::InvalidInterpolation(region) => { + todo!( + "TODO runtime error for an invalid string interpolation at region {:?}", + region + ); + } + RuntimeError::NoImplementation | RuntimeError::NoImplementationNamed { .. } => { + todo!("no implementation, unreachable") + } + RuntimeError::NonExhaustivePattern => { + unreachable!("not currently reported (but can blow up at runtime)") + } + RuntimeError::ExposedButNotDefined(symbol) => { + doc = alloc.stack([alloc + .symbol_unqualified(symbol) + .append(alloc.reflow(" was listed as exposed in ")) + .append(alloc.module(symbol.module_id())) + .append(alloc.reflow(", but it was not defined anywhere in that module."))]); + + title = MISSING_DEFINITION; + } + RuntimeError::EmptySingleQuote(region) => { + let tip = alloc + .tip() + .append(alloc.reflow("Learn more about character literals at TODO")); + + doc = alloc.stack([ + alloc.concat([alloc.reflow("This character literal is empty.")]), + alloc.region(lines.convert_region(region)), + tip, + ]); + + title = SYNTAX_PROBLEM; + } + RuntimeError::MultipleCharsInSingleQuote(region) => { + let tip = alloc + .tip() + .append(alloc.reflow("Learn more about character literals at TODO")); + + doc = alloc.stack([ + alloc.concat([ + alloc.reflow("This character literal contains more than one code point.") + ]), + alloc.region(lines.convert_region(region)), + alloc.concat([alloc.reflow("Character literals can only contain one code point.")]), + tip, + ]); + + title = SYNTAX_PROBLEM; + } + RuntimeError::OpaqueNotDefined { + usage: + Loc { + region: used_region, + value: opaque, + }, + opaques_in_scope, + opt_defined_alias, + } => { + let mut suggestions = suggest::sort( + opaque.as_inline_str().as_str(), + opaques_in_scope.iter().map(|v| v.as_ref()).collect(), + ); + suggestions.truncate(4); + + let details = if suggestions.is_empty() { + alloc.note("It looks like there are no opaque types declared in this scope yet!") + } else { + let qualified_suggestions = + suggestions.into_iter().map(|v| alloc.string(v.to_string())); + alloc.stack([ + alloc + .tip() + .append(alloc.reflow("Did you mean one of these opaque types?")), + alloc.vcat(qualified_suggestions).indent(4), + ]) + }; + + let mut stack = vec![ + alloc.concat([ + alloc.reflow("The opaque type "), + alloc.type_str(opaque.as_inline_str().as_str()), + alloc.reflow(" referenced here is not defined:"), + ]), + alloc.region(lines.convert_region(used_region)), + ]; + + if let Some(defined_alias_region) = opt_defined_alias { + stack.push(alloc.stack([ + alloc.note("There is an alias of the same name:"), + alloc.region(lines.convert_region(defined_alias_region)), + ])); + } + + stack.push(details); + + doc = alloc.stack(stack); + + title = OPAQUE_NOT_DEFINED; + } + RuntimeError::OpaqueOutsideScope { + opaque, + referenced_region, + imported_region, + } => { + doc = alloc.stack([ + alloc.concat([ + alloc.reflow("The unwrapped opaque type "), + alloc.type_str(opaque.as_inline_str().as_str()), + alloc.reflow(" referenced here:"), + ]), + alloc.region(lines.convert_region(referenced_region)), + alloc.reflow("is imported from another module:"), + alloc.region(lines.convert_region(imported_region)), + alloc.note( + "Opaque types can only be wrapped and unwrapped in the module they are defined in!", + ), + ]); + + title = OPAQUE_DECLARED_OUTSIDE_SCOPE; + } + RuntimeError::OpaqueNotApplied(loc_ident) => { + doc = alloc.stack([ + alloc.reflow("This opaque type is not applied to an argument:"), + alloc.region(lines.convert_region(loc_ident.region)), + alloc.note("Opaque types always wrap exactly one argument!"), + ]); + + title = OPAQUE_NOT_APPLIED; + } + RuntimeError::OpaqueAppliedToMultipleArgs(region) => { + doc = alloc.stack([ + alloc.reflow("This opaque type is applied to multiple arguments:"), + alloc.region(lines.convert_region(region)), + alloc.note("Opaque types always wrap exactly one argument!"), + ]); + + title = OPAQUE_OVER_APPLIED; + } + RuntimeError::DegenerateBranch(region) => { + doc = alloc.stack([ + alloc.reflow("This branch pattern does not bind all symbols its body needs:"), + alloc.region(lines.convert_region(region)), + ]); + + title = "DEGENERATE BRANCH"; + } + RuntimeError::MultipleRecordBuilders(region) => { + let tip = alloc + .tip() + .append(alloc.reflow("You can combine them or apply them separately.")); + + doc = alloc.stack([ + alloc.reflow("This function is applied to multiple record builders:"), + alloc.region(lines.convert_region(region)), + alloc.note("Functions can only take at most one record builder!"), + tip, + ]); + + title = "MULTIPLE RECORD BUILDERS"; + } + RuntimeError::UnappliedRecordBuilder(region) => { + doc = alloc.stack([ + alloc.reflow("This record builder was not applied to a function:"), + alloc.region(lines.convert_region(region)), + alloc.reflow("However, we need a function to construct the record."), + alloc.note( + "Functions must be applied directly. The pipe operator (|>) cannot be used.", + ), + ]); + + title = "UNAPPLIED RECORD BUILDER"; + } + } + + (doc, title) +} + +pub fn to_circular_def_doc<'b>( + alloc: &'b RocDocAllocator<'b>, + lines: &LineInfo, + entries: &[roc_problem::can::CycleEntry], +) -> RocDocBuilder<'b> { + // TODO "are you trying to mutate a variable? + // TODO tip? + match entries { + [] => unreachable!(), + [CycleEntry { symbol, symbol_region, expr_region }] => + alloc.stack([ + alloc.concat([ + alloc.symbol_unqualified(*symbol), + alloc.reflow(" is defined directly in terms of itself:"), + ]), + alloc.region(lines.convert_region(Region::span_across(symbol_region, expr_region))), + alloc.reflow("Roc evaluates values strictly, so running this program would enter an infinite loop!"), + alloc.hint("").append(alloc.concat([ + alloc.reflow("Did you mean to define "),alloc.symbol_unqualified(*symbol),alloc.reflow(" as a function?"), + ])), + ]), + [first, others @ ..] => { + alloc.stack([ + alloc + .reflow("The ") + .append(alloc.symbol_unqualified(first.symbol)) + .append(alloc.reflow(" definition is causing a very tricky infinite loop:")), + alloc.region(lines.convert_region(first.symbol_region)), + alloc + .reflow("The ") + .append(alloc.symbol_unqualified(first.symbol)) + .append(alloc.reflow( + " value depends on itself through the following chain of definitions:", + )), + crate::report::cycle( + alloc, + 4, + alloc.symbol_unqualified(first.symbol), + others + .iter() + .map(|s| alloc.symbol_unqualified(s.symbol)) + .collect::>(), + ), + // TODO tip? + ]) + } + } +} + +fn not_found<'b>( + alloc: &'b RocDocAllocator<'b>, + lines: &LineInfo, + region: roc_region::all::Region, + name: &Ident, + options: MutSet>, + underscored_suggestion_region: Option, +) -> RocDocBuilder<'b> { + let mut suggestions = suggest::sort( + name.as_inline_str().as_str(), + options.iter().map(|v| v.as_ref()).collect(), + ); + suggestions.truncate(4); + + let default_no = alloc.concat([ + alloc.reflow("Is there an "), + alloc.keyword("import"), + alloc.reflow(" or "), + alloc.keyword("exposing"), + alloc.reflow(" missing up-top"), + ]); + + let default_yes = match underscored_suggestion_region { + Some(underscored_region) => alloc.stack([ + alloc.reflow("There is an ignored identifier of a similar name here:"), + alloc.region(lines.convert_region(underscored_region)), + alloc.reflow("Did you mean to remove the leading underscore?"), + alloc.reflow("If not, did you mean one of these?"), + ]), + None => alloc.reflow("Did you mean one of these?"), + }; + + let to_details = |no_suggestion_details, yes_suggestion_details| { + if suggestions.is_empty() { + no_suggestion_details + } else { + alloc.stack([ + yes_suggestion_details, + alloc + .vcat(suggestions.into_iter().map(|v| alloc.string(v.to_string()))) + .indent(4), + ]) + } + }; + + alloc.stack([ + alloc.concat([ + alloc.reflow("Nothing is named `"), + alloc.string(name.to_string()), + alloc.reflow("` in this scope."), + ]), + alloc.region(lines.convert_region(region)), + to_details(default_no, default_yes), + ]) +} + +/// Generate a message informing the user that a module was referenced, but not found +/// +/// See [`roc_problem::can::ModuleNotImported`] +fn module_not_found<'b>( + alloc: &'b RocDocAllocator<'b>, + lines: &LineInfo, + region: roc_region::all::Region, + name: &ModuleName, + options: MutSet>, + module_exists: bool, +) -> RocDocBuilder<'b> { + // If the module exists, sugguest that the user import it + let details = if module_exists { + // TODO: Maybe give an example of how to do that + alloc.reflow("Did you mean to import it?") + } else { + // If the module might not exist, sugguest that it's a typo + let mut suggestions = + suggest::sort(name.as_str(), options.iter().map(|v| v.as_ref()).collect()); + suggestions.truncate(4); + + if suggestions.is_empty() { + // We don't have any recommended spelling corrections + alloc.concat([ + alloc.reflow("Is there an "), + alloc.keyword("import"), + alloc.reflow(" or "), + alloc.keyword("exposing"), + alloc.reflow(" missing up-top"), + ]) + } else { + alloc.stack([ + alloc.reflow("Is there an import missing? Perhaps there is a typo. Did you mean one of these?"), + alloc + .vcat(suggestions.into_iter().map(|v| alloc.string(v.to_string()))) + .indent(4), + ]) + } + }; + + alloc.stack([ + alloc.concat([ + alloc.reflow("The `"), + alloc.string(name.to_string()), + alloc.reflow("` module is not imported:"), + ]), + alloc.region(lines.convert_region(region)), + details, + ]) +} diff --git a/crates/reporting/src/error/expect.rs b/crates/reporting/src/error/expect.rs new file mode 100644 index 0000000000..8b5eaa1b20 --- /dev/null +++ b/crates/reporting/src/error/expect.rs @@ -0,0 +1,243 @@ +use std::path::PathBuf; + +use bumpalo::Bump; +use roc_module::symbol::{Interns, ModuleId, Symbol}; +use roc_parse::ast::Expr; +use roc_problem::Severity; +use roc_region::all::{LineColumnRegion, LineInfo, Region}; +use roc_types::{ + subs::{Subs, Variable}, + types::{ErrorType, Polarity}, +}; + +use crate::report::{RenderTarget, RocDocAllocator, RocDocBuilder}; + +pub struct Renderer<'a> { + arena: &'a Bump, + alloc: RocDocAllocator<'a>, + filename: PathBuf, + line_info: LineInfo, + render_target: RenderTarget, +} + +impl<'a> Renderer<'a> { + pub fn new( + arena: &'a Bump, + interns: &'a Interns, + render_target: RenderTarget, + module_id: ModuleId, + filename: PathBuf, + source: &'a str, + ) -> Self { + let source_lines = bumpalo::collections::Vec::from_iter_in(source.lines(), arena); + let line_info = roc_region::all::LineInfo::new(source); + + let alloc = RocDocAllocator::new(source_lines.into_bump_slice(), module_id, interns); + + Self { + arena, + alloc, + line_info, + filename, + render_target, + } + } + + fn render_expr(&'a self, error_type: ErrorType) -> RocDocBuilder<'a> { + use crate::error::r#type::error_type_to_doc; + + error_type_to_doc(&self.alloc, error_type) + } + + fn render_lookup( + &'a self, + symbol: Symbol, + expr: &Expr<'_>, + error_type: ErrorType, + ) -> RocDocBuilder<'a> { + use roc_fmt::annotation::Formattable; + + let mut buf = roc_fmt::Buf::new_in(self.arena); + expr.format(&mut buf, 0); + + self.alloc.vcat([ + self.alloc + .symbol_unqualified(symbol) + .append(" : ") + .append(self.render_expr(error_type)), + self.alloc + .symbol_unqualified(symbol) + .append(" = ") + .append(buf.into_bump_str()), + ]) + } + + fn render_lookups( + &'a self, + subs: &mut Subs, + line_col_region: LineColumnRegion, + + symbols: &[Symbol], + variables: &[Variable], + expressions: &[Expr<'_>], + ) -> RocDocBuilder<'a> { + use ven_pretty::DocAllocator; + + let it = + symbols + .iter() + .zip(variables) + .zip(expressions) + .map(|((symbol, variable), expr)| { + let error_type = subs.var_to_error_type(*variable, Polarity::OF_VALUE); + self.render_lookup(*symbol, expr, error_type) + }); + + if it.len() > 0 { + self.alloc.stack([ + self.alloc.text("This expectation failed:"), + self.alloc.region(line_col_region), + self.alloc + .text("When it failed, these variables had these values:"), + self.alloc.stack(it), + self.alloc.text(""), // Blank line at the end + ]) + } else { + self.alloc.stack([ + self.alloc.text("This expectation failed:"), + self.alloc.region(line_col_region), + self.alloc.text(""), // Blank line at the end + ]) + } + } + + fn to_line_col_region( + &self, + expect_region: Option, + failure_region: Region, + ) -> LineColumnRegion { + let display_region = match expect_region { + Some(expect_region) => { + if !expect_region.contains(&failure_region) { + // this is an expect outside of a toplevel expect, + // likely in some function we called + failure_region + } else { + Region::across_all([&expect_region, &failure_region]) + } + } + None => failure_region, + }; + + self.line_info.convert_region(display_region) + } + + #[allow(clippy::too_many_arguments)] + pub fn render_failure( + &self, + writer: &mut W, + subs: &mut Subs, + symbols: &[Symbol], + variables: &[Variable], + expressions: &[Expr<'_>], + expect_region: Option, + failure_region: Region, + ) -> std::io::Result<()> + where + W: std::io::Write, + { + use crate::report::Report; + + let line_col_region = self.to_line_col_region(expect_region, failure_region); + let doc = self.render_lookups(subs, line_col_region, symbols, variables, expressions); + + let report = Report { + title: "EXPECT FAILED".into(), + doc, + filename: self.filename.clone(), + severity: Severity::RuntimeError, + }; + + let mut buf = String::new(); + + report.render( + self.render_target, + &mut buf, + &self.alloc, + &crate::report::DEFAULT_PALETTE, + ); + + write!(writer, "{buf}") + } + + #[allow(clippy::too_many_arguments)] + pub fn render_dbg( + &self, + writer: &mut W, + expressions: &[Expr<'_>], + expect_region: Option, + dbg_expr_region: Region, + ) -> std::io::Result<()> + where + W: std::io::Write, + { + let line_col_region = self.to_line_col_region(expect_region, dbg_expr_region); + write!( + writer, + "\u{001b}[36m[{} {}:{}] \u{001b}[0m", + self.filename.display(), + line_col_region.start.line + 1, + line_col_region.start.column + 1 + )?; + + let expr = expressions[0]; + + let mut buf = roc_fmt::Buf::new_in(self.arena); + { + use roc_fmt::annotation::Formattable; + expr.format(&mut buf, 0); + } + + writeln!(writer, "{}", buf.as_str()) + } + + pub fn render_panic( + &self, + writer: &mut W, + message: &str, + expect_region: Region, + ) -> std::io::Result<()> + where + W: std::io::Write, + { + use crate::report::Report; + use ven_pretty::DocAllocator; + + let line_col_region = self.line_info.convert_region(expect_region); + + let doc = self.alloc.stack([ + self.alloc.text("This expectation crashed while running:"), + self.alloc.region(line_col_region), + self.alloc.text("The crash reported this message:"), + self.alloc.text(message), + ]); + + let report = Report { + title: "EXPECT PANICKED".into(), + doc, + filename: self.filename.clone(), + severity: Severity::RuntimeError, + }; + + let mut buf = String::new(); + + report.render( + self.render_target, + &mut buf, + &self.alloc, + &crate::report::DEFAULT_PALETTE, + ); + + write!(writer, "{buf}") + } +} diff --git a/crates/reporting/src/error/mod.rs b/crates/reporting/src/error/mod.rs new file mode 100644 index 0000000000..4ab10dbc13 --- /dev/null +++ b/crates/reporting/src/error/mod.rs @@ -0,0 +1,4 @@ +pub mod canonicalize; +pub mod expect; +pub mod parse; +pub mod r#type; diff --git a/crates/reporting/src/error/parse.rs b/crates/reporting/src/error/parse.rs new file mode 100644 index 0000000000..839da6c2f4 --- /dev/null +++ b/crates/reporting/src/error/parse.rs @@ -0,0 +1,4133 @@ +use roc_parse::parser::{ENumber, ESingleQuote, FileError, PList, SyntaxError}; +use roc_problem::Severity; +use roc_region::all::{LineColumn, LineColumnRegion, LineInfo, Position, Region}; +use std::path::PathBuf; + +use crate::report::{Report, RocDocAllocator, RocDocBuilder}; +use ven_pretty::DocAllocator; + +pub fn parse_problem<'a>( + alloc: &'a RocDocAllocator<'a>, + lines: &LineInfo, + filename: PathBuf, + _starting_line: u32, + parse_problem: FileError>, +) -> Report<'a> { + to_syntax_report(alloc, lines, filename, &parse_problem.problem.problem) +} + +fn note_for_record_type_indent<'a>(alloc: &'a RocDocAllocator<'a>) -> RocDocBuilder<'a> { + alloc.note("I may be confused by indentation") +} + +fn note_for_tag_union_type_indent<'a>(alloc: &'a RocDocAllocator<'a>) -> RocDocBuilder<'a> { + alloc.note("I may be confused by indentation") +} + +fn hint_for_tag_name<'a>(alloc: &'a RocDocAllocator<'a>) -> RocDocBuilder<'a> { + alloc.concat([ + alloc.hint("Tag names "), + alloc.reflow("start with an uppercase letter, like "), + alloc.parser_suggestion("Err"), + alloc.text(" or "), + alloc.parser_suggestion("Green"), + alloc.text("."), + ]) +} + +fn record_patterns_look_like<'a>(alloc: &'a RocDocAllocator<'a>) -> RocDocBuilder<'a> { + alloc.concat([ + alloc.reflow(r"Record pattern look like "), + alloc.parser_suggestion("{ name, age: currentAge },"), + alloc.reflow(" so I was expecting to see a field name next."), + ]) +} + +fn list_patterns_look_like<'a>(alloc: &'a RocDocAllocator<'a>) -> RocDocBuilder<'a> { + alloc.concat([ + alloc.reflow(r"Record pattern look like "), + alloc.parser_suggestion("[1, 2, ..]"), + alloc.reflow(" or "), + alloc.parser_suggestion("[first, .., last]"), + ]) +} + +fn to_syntax_report<'a>( + alloc: &'a RocDocAllocator<'a>, + lines: &LineInfo, + filename: PathBuf, + parse_problem: &roc_parse::parser::SyntaxError<'a>, +) -> Report<'a> { + use SyntaxError::*; + + let report = |doc| Report { + filename: filename.clone(), + doc, + title: "PARSE PROBLEM".to_string(), + severity: Severity::RuntimeError, + }; + + match parse_problem { + SyntaxError::ArgumentsBeforeEquals(region) => { + let doc = alloc.stack([ + alloc.reflow("Unexpected tokens in front of the `=` symbol:"), + alloc.region(lines.convert_region(*region)), + ]); + + Report { + filename, + doc, + title: "PARSE PROBLEM".to_string(), + severity: Severity::RuntimeError, + } + } + Unexpected(region) => { + let mut region = lines.convert_region(*region); + if region.start().column == region.end().column { + region = LineColumnRegion::new(region.start(), region.end().bump_column(1)); + } + + let doc = alloc.stack([ + alloc.concat([ + alloc.reflow("Unexpected token "), + // context(alloc, &parse_problem.context_stack, "here"), + alloc.text(":"), + ]), + alloc.region(region), + ]); + + report(doc) + } + NotEndOfFile(pos) => { + let region = LineColumnRegion::from_pos(lines.convert_pos(*pos)); + + let doc = alloc.stack([ + alloc.reflow(r"I expected to reach the end of the file, but got stuck here:"), + alloc.region(region), + ]); + + Report { + filename, + doc, + title: "NOT END OF FILE".to_string(), + severity: Severity::RuntimeError, + } + } + SyntaxError::Eof(region) => { + let doc = alloc.stack([ + alloc.reflow("End of Field"), + alloc.region(lines.convert_region(*region)), + ]); + + Report { + filename, + doc, + title: "PARSE PROBLEM".to_string(), + severity: Severity::RuntimeError, + } + } + SyntaxError::OutdentedTooFar => { + let doc = alloc.stack([alloc.reflow("OutdentedTooFar")]); + + Report { + filename, + doc, + title: "PARSE PROBLEM".to_string(), + severity: Severity::RuntimeError, + } + } + Type(typ) => to_type_report(alloc, lines, filename, typ, Position::default()), + Pattern(pat) => to_pattern_report(alloc, lines, filename, pat, Position::default()), + Expr(expr, start) => to_expr_report( + alloc, + lines, + filename, + Context::InDef(*start), + expr, + Position::default(), + ), + Header(header) => to_header_report(alloc, lines, filename, header, Position::default()), + _ => todo!("unhandled parse error: {:?}", parse_problem), + } +} + +#[allow(clippy::enum_variant_names)] +enum Context { + InNode(Node, Position, Box), + InDef(Position), + InDefFinalExpr(Position), +} + +enum Node { + WhenCondition, + WhenBranch, + WhenIfGuard, + IfCondition, + IfThenBranch, + IfElseBranch, + ListElement, + InsideParens, + RecordConditionalDefault, + StringFormat, + Dbg, + Expect, +} + +fn to_expr_report<'a>( + alloc: &'a RocDocAllocator<'a>, + lines: &LineInfo, + filename: PathBuf, + context: Context, + parse_problem: &roc_parse::parser::EExpr<'a>, + start: Position, +) -> Report<'a> { + use roc_parse::parser::EExpr; + + match parse_problem { + EExpr::If(if_, pos) => to_if_report(alloc, lines, filename, context, if_, *pos), + EExpr::When(when, pos) => to_when_report(alloc, lines, filename, context, when, *pos), + EExpr::Closure(lambda, pos) => { + to_lambda_report(alloc, lines, filename, context, lambda, *pos) + } + EExpr::List(list, pos) => to_list_report(alloc, lines, filename, context, list, *pos), + EExpr::Str(string, pos) => to_str_report(alloc, lines, filename, context, string, *pos), + EExpr::InParens(expr, pos) => { + to_expr_in_parens_report(alloc, lines, filename, context, expr, *pos) + } + EExpr::Type(tipe, pos) => to_type_report(alloc, lines, filename, tipe, *pos), + EExpr::ElmStyleFunction(region, pos) => { + let surroundings = Region::new(start, *pos); + let region = lines.convert_region(*region); + + let doc = alloc.stack([ + alloc.reflow(r"I am partway through parsing a definition, but I got stuck here:"), + alloc.region_with_subregion(lines.convert_region(surroundings), region), + alloc.concat([ + alloc.reflow("Looks like you are trying to define a function. "), + alloc.reflow("In roc, functions are always written as a lambda, like "), + alloc.parser_suggestion("increment = \\n -> n + 1"), + alloc.reflow("."), + ]), + ]); + + Report { + filename, + doc, + title: "ARGUMENTS BEFORE EQUALS".to_string(), + severity: Severity::RuntimeError, + } + } + + EExpr::BadOperator(op, pos) => { + let surroundings = Region::new(start, *pos); + let region = Region::new(*pos, pos.bump_column(op.len() as u32)); + + let suggestion = match *op { + "|" => vec![ + alloc.reflow("Maybe you want "), + alloc.parser_suggestion("||"), + alloc.reflow(" or "), + alloc.parser_suggestion("|>"), + alloc.reflow(" instead?"), + ], + "++" => vec![ + alloc.reflow("To concatenate two lists or strings, try using "), + alloc.parser_suggestion("List.concat"), + alloc.reflow(" or "), + alloc.parser_suggestion("Str.concat"), + alloc.reflow(" instead."), + ], + ":" => vec![alloc.stack([ + alloc.concat([ + alloc.reflow("The has-type operator "), + alloc.parser_suggestion(":"), + alloc.reflow(" can only occur in a definition's type signature, like"), + ]), + alloc + .vcat(vec![ + alloc.text("increment : I64 -> I64"), + alloc.text("increment = \\x -> x + 1"), + ]) + .indent(4), + ])], + "->" => match context { + Context::InNode(Node::WhenBranch, _pos, _) => { + return to_unexpected_arrow_report(alloc, lines, filename, *pos, start); + } + + Context::InDef(_pos) => { + vec![alloc.stack([ + alloc.reflow("Looks like you are trying to define a function. "), + alloc.reflow("In roc, functions are always written as a lambda, like "), + alloc + .parser_suggestion("increment = \\n -> n + 1") + .indent(4), + ])] + } + + _ => { + vec![alloc.stack([ + alloc.concat([ + alloc.reflow("The arrow "), + alloc.parser_suggestion("->"), + alloc.reflow(" is only used to define cases in a "), + alloc.keyword("`when`"), + alloc.reflow("expression:"), + ]), + alloc + .vcat(vec![ + alloc.text("when color is"), + alloc.text("Red -> \"stop!\"").indent(4), + alloc.text("Green -> \"go!\"").indent(4), + ]) + .indent(4), + alloc.reflow("And to define a function:"), + alloc + .vcat(vec![ + alloc.text("increment : I64 -> I64"), + alloc.text("increment = \\n -> n + 1"), + ]) + .indent(4), + ])] + } + }, + "!" => vec![ + alloc.reflow("The boolean negation operator "), + alloc.parser_suggestion("!"), + alloc.reflow(" must occur immediately before an expression, like "), + alloc.parser_suggestion("!(List.isEmpty primes)"), + alloc.reflow(". There cannot be a space between the "), + alloc.parser_suggestion("!"), + alloc.reflow(" and the expression after it."), + ], + "<|" => vec![ + alloc.reflow("Roc doesn't have a "), + alloc.parser_suggestion("<|"), + alloc.reflow(" operator. Please use parentheses "), + alloc.parser_suggestion("()"), + alloc.reflow(" or "), + alloc.parser_suggestion("|>"), + alloc.reflow(" instead."), + ], + _ => vec![ + alloc.reflow("I have no specific suggestion for this operator, "), + alloc.reflow("see TODO for the full list of operators in Roc."), + ], + }; + + let doc = alloc.stack([ + alloc.reflow(r"This looks like an operator, but it's not one I recognize!"), + alloc.region_with_subregion( + lines.convert_region(surroundings), + lines.convert_region(region), + ), + alloc.concat(suggestion), + ]); + + Report { + filename, + doc, + title: "UNKNOWN OPERATOR".to_string(), + severity: Severity::RuntimeError, + } + } + + EExpr::Ident(_pos) => unreachable!("another branch would be taken"), + + EExpr::QualifiedTag(pos) => { + let surroundings = Region::new(start, *pos); + let region = LineColumnRegion::from_pos(lines.convert_pos(*pos)); + + let doc = alloc.stack([ + alloc.reflow(r"I am very confused by this identifier:"), + alloc.region_with_subregion(lines.convert_region(surroundings), region), + alloc.concat([ + alloc.reflow("Are you trying to qualify a name? I am execting something like "), + alloc.parser_suggestion("Json.Decode.string"), + alloc.reflow(". Maybe you are trying to qualify a tag? Tags like "), + alloc.parser_suggestion("Err"), + alloc.reflow(" are globally scoped in roc, and cannot be qualified."), + ]), + ]); + + Report { + filename, + doc, + title: "WEIRD IDENTIFIER".to_string(), + severity: Severity::RuntimeError, + } + } + + EExpr::Start(pos) | EExpr::IndentStart(pos) => { + let (title, expecting) = match &context { + Context::InNode { .. } | Context::InDef { .. } => ( + "MISSING EXPRESSION", + alloc.concat([ + alloc.reflow("I was expecting to see an expression like "), + alloc.parser_suggestion("42"), + alloc.reflow(" or "), + alloc.parser_suggestion("\"hello\""), + alloc.text("."), + ]), + ), + Context::InDefFinalExpr { .. } => ( + "MISSING FINAL EXPRESSION", + alloc.stack([ + alloc.concat([ + alloc.reflow("This definition is missing a final expression."), + alloc.reflow(" A nested definition must be followed by"), + alloc.reflow(" either another definition, or an expression"), + ]), + alloc.vcat(vec![ + alloc.text("x = 4").indent(4), + alloc.text("y = 2").indent(4), + alloc.text(""), + alloc.text("x + y").indent(4), + ]), + ]), + ), + }; + + let (context_pos, a_thing) = match context { + Context::InNode(node, pos, _) => match node { + Node::WhenCondition | Node::WhenBranch | Node::WhenIfGuard => ( + pos, + alloc.concat([ + alloc.text("a "), + alloc.keyword("when"), + alloc.text(" expression"), + ]), + ), + Node::IfCondition | Node::IfThenBranch | Node::IfElseBranch => ( + pos, + alloc.concat([ + alloc.text("an "), + alloc.keyword("if"), + alloc.text(" expression"), + ]), + ), + Node::ListElement => (pos, alloc.text("a list")), + Node::Dbg => (pos, alloc.text("a dbg statement")), + Node::Expect => (pos, alloc.text("an expect statement")), + Node::RecordConditionalDefault => (pos, alloc.text("record field default")), + Node::StringFormat => (pos, alloc.text("a string format")), + Node::InsideParens => (pos, alloc.text("some parentheses")), + }, + Context::InDef(pos) => (pos, alloc.text("a definition")), + Context::InDefFinalExpr(pos) => { + (pos, alloc.text("a definition's final expression")) + } + }; + + let surroundings = Region::new(context_pos, *pos); + let region = LineColumnRegion::from_pos(lines.convert_pos(*pos)); + + let doc = alloc.stack([ + alloc.concat([ + alloc.reflow(r"I am partway through parsing "), + a_thing, + alloc.reflow(", but I got stuck here:"), + ]), + alloc.region_with_subregion(lines.convert_region(surroundings), region), + expecting, + ]); + + Report { + filename, + doc, + title: title.to_string(), + severity: Severity::RuntimeError, + } + } + + EExpr::DefMissingFinalExpr(pos) => { + let surroundings = Region::new(start, *pos); + let region = LineColumnRegion::from_pos(lines.convert_pos(*pos)); + + let doc = alloc.stack([ + alloc.reflow(r"I am partway through parsing a definition, but I got stuck here:"), + alloc.region_with_subregion(lines.convert_region(surroundings), region), + alloc.concat([ + alloc.reflow("This definition is missing a final expression."), + alloc.reflow(" A nested definition must be followed by"), + alloc.reflow(" either another definition, or an expression"), + ]), + alloc.vcat(vec![ + alloc.text("x = 4").indent(4), + alloc.text("y = 2").indent(4), + alloc.text(""), + alloc.text("x + y").indent(4), + ]), + ]); + + Report { + filename, + doc, + title: "MISSING FINAL EXPRESSION".to_string(), + severity: Severity::RuntimeError, + } + } + + EExpr::DefMissingFinalExpr2(expr, pos) => to_expr_report( + alloc, + lines, + filename, + Context::InDefFinalExpr(start), + expr, + *pos, + ), + + EExpr::BadExprEnd(pos) => { + let surroundings = Region::new(start, *pos); + let region = LineColumnRegion::from_pos(lines.convert_pos(*pos)); + + let doc = alloc.stack([ + alloc.reflow(r"I got stuck here:"), + alloc.region_with_subregion(lines.convert_region(surroundings), region), + alloc.concat([ + alloc.reflow("Whatever I am running into is confusing me a lot! "), + alloc.reflow("Normally I can give fairly specific hints, "), + alloc.reflow("but something is really tripping me up this time."), + ]), + ]); + + Report { + filename, + doc, + title: "SYNTAX PROBLEM".to_string(), + severity: Severity::RuntimeError, + } + } + + EExpr::Colon(pos) => { + let surroundings = Region::new(start, *pos); + let region = LineColumnRegion::from_pos(lines.convert_pos(*pos)); + + let doc = alloc.stack([ + alloc.reflow(r"I am partway through parsing a definition, but I got stuck here:"), + alloc.region_with_subregion(lines.convert_region(surroundings), region), + alloc.concat([ + alloc.reflow("Looks like you are trying to define a function. "), + alloc.reflow("In roc, functions are always written as a lambda, like "), + alloc.parser_suggestion("increment = \\n -> n + 1"), + alloc.reflow("."), + ]), + ]); + + Report { + filename, + doc, + title: "ARGUMENTS BEFORE EQUALS".to_string(), + severity: Severity::RuntimeError, + } + } + + EExpr::BackpassArrow(pos) => { + let surroundings = Region::new(start, *pos); + let region = LineColumnRegion::from_pos(lines.convert_pos(*pos)); + + let doc = alloc.stack([ + alloc.reflow(r"I am partway through parsing an expression, but I got stuck here:"), + alloc.region_with_subregion(lines.convert_region(surroundings), region), + alloc.concat([alloc.reflow("Looks like you are trying to define a function. ")]), + ]); + + Report { + filename, + doc, + title: "BAD BACKPASSING ARROW".to_string(), + severity: Severity::RuntimeError, + } + } + + EExpr::Record(_erecord, pos) => { + let surroundings = Region::new(start, *pos); + let region = LineColumnRegion::from_pos(lines.convert_pos(*pos)); + + let doc = alloc.stack([ + alloc.reflow(r"I am partway through parsing a record, but I got stuck here:"), + alloc.region_with_subregion(lines.convert_region(surroundings), region), + alloc.concat([alloc.reflow("TODO provide more context.")]), + ]); + + Report { + filename, + doc, + title: "RECORD PARSE PROBLEM".to_string(), + severity: Severity::RuntimeError, + } + } + + EExpr::OptionalValueInRecordBuilder(region) => { + let surroundings = Region::new(start, region.end()); + let region = lines.convert_region(*region); + + let doc = alloc.stack([ + alloc.reflow( + r"I am partway through parsing a record builder, and I found an optional field:", + ), + alloc.region_with_subregion(lines.convert_region(surroundings), region), + alloc.reflow("Optional fields can only appear when you destructure a record."), + ]); + + Report { + filename, + doc, + title: "BAD RECORD BUILDER".to_string(), + severity: Severity::RuntimeError, + } + } + + EExpr::RecordUpdateBuilder(region) => { + let surroundings = Region::new(start, region.end()); + let region = lines.convert_region(*region); + + let doc = alloc.stack([ + alloc.reflow( + r"I am partway through parsing a record update, and I found a record builder field:", + ), + alloc.region_with_subregion(lines.convert_region(surroundings), region), + alloc.reflow("Record builders cannot be updated like records."), + ]); + + Report { + filename, + doc, + title: "BAD RECORD UPDATE".to_string(), + severity: Severity::RuntimeError, + } + } + + EExpr::Space(error, pos) => to_space_report(alloc, lines, filename, error, *pos), + + &EExpr::Number(ENumber::End, pos) => { + to_malformed_number_literal_report(alloc, lines, filename, pos) + } + + EExpr::Ability(err, pos) => to_ability_def_report(alloc, lines, filename, err, *pos), + + EExpr::IndentEnd(pos) => { + let surroundings = Region::new(start, *pos); + let region = LineColumnRegion::from_pos(lines.convert_pos(*pos)); + + let snippet = alloc.region_with_subregion(lines.convert_region(surroundings), region); + + let doc = match context { + Context::InNode(Node::Dbg, _, _) => alloc.stack([ + alloc.reflow( + r"I am partway through parsing a dbg statement, but I got stuck here:", + ), + snippet, + alloc.stack([ + alloc.reflow(r"I was expecting a final expression, like so"), + alloc.vcat([ + alloc.parser_suggestion("dbg 42").indent(4), + alloc.parser_suggestion("\"done\"").indent(4), + ]), + ]), + ]), + Context::InNode(Node::Expect, _, _) => alloc.stack([ + alloc.reflow( + r"I am partway through parsing an expect statement, but I got stuck here:", + ), + snippet, + alloc.stack([ + alloc.reflow(r"I was expecting a final expression, like so"), + alloc.vcat([ + alloc.parser_suggestion("expect 1 + 1 == 2").indent(4), + alloc.parser_suggestion("\"done\"").indent(4), + ]), + ]), + ]), + _ => { + // generic + alloc.stack([ + alloc.reflow( + r"I am partway through parsing an expression, but I got stuck here:", + ), + snippet, + alloc.concat([ + alloc.reflow(r"Looks like the indentation ends prematurely here. "), + alloc.reflow( + r"Did you mean to have another expression after this line?", + ), + ]), + ]) + } + }; + + Report { + filename, + doc, + title: "INDENT ENDS AFTER EXPRESSION".to_string(), + severity: Severity::RuntimeError, + } + } + EExpr::Expect(e_expect, _position) => { + let node = Node::Expect; + to_dbg_or_expect_report(alloc, lines, filename, context, node, e_expect, start) + } + EExpr::Dbg(e_expect, _position) => { + to_dbg_or_expect_report(alloc, lines, filename, context, Node::Dbg, e_expect, start) + } + _ => todo!("unhandled parse error: {:?}", parse_problem), + } +} + +fn to_lambda_report<'a>( + alloc: &'a RocDocAllocator<'a>, + lines: &LineInfo, + filename: PathBuf, + _context: Context, + parse_problem: &roc_parse::parser::EClosure<'a>, + start: Position, +) -> Report<'a> { + use roc_parse::parser::EClosure; + + match *parse_problem { + EClosure::Arrow(pos) => match what_is_next(alloc.src_lines, lines.convert_pos(pos)) { + Next::Token("=>") => { + let surroundings = Region::new(start, pos); + let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); + + let doc = alloc.stack([ + alloc + .reflow(r"I am partway through parsing a function argument list, but I got stuck here:"), + alloc.region_with_subregion(lines.convert_region(surroundings), region), + alloc.concat([ + alloc.reflow("I was expecting a "), + alloc.parser_suggestion("->"), + alloc.reflow(" next."), + ]), + ]); + + Report { + filename, + doc, + title: "WEIRD ARROW".to_string(), + severity: Severity::RuntimeError, + } + } + _ => { + let surroundings = Region::new(start, pos); + let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); + + let doc = alloc.stack([ + alloc + .reflow(r"I am partway through parsing a function argument list, but I got stuck here:"), + alloc.region_with_subregion(lines.convert_region(surroundings), region), + alloc.concat([ + alloc.reflow("I was expecting a "), + alloc.parser_suggestion("->"), + alloc.reflow(" next."), + ]), + ]); + + Report { + filename, + doc, + title: "MISSING ARROW".to_string(), + severity: Severity::RuntimeError, + } + } + }, + + EClosure::Comma(pos) => match what_is_next(alloc.src_lines, lines.convert_pos(pos)) { + Next::Token("=>") => { + let surroundings = Region::new(start, pos); + let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); + + let doc = alloc.stack([ + alloc + .reflow(r"I am partway through parsing a function argument list, but I got stuck here:"), + alloc.region_with_subregion(lines.convert_region(surroundings), region), + alloc.concat([ + alloc.reflow("I was expecting a "), + alloc.parser_suggestion("->"), + alloc.reflow(" next."), + ]), + ]); + + Report { + filename, + doc, + title: "WEIRD ARROW".to_string(), + severity: Severity::RuntimeError, + } + } + _ => { + let surroundings = Region::new(start, pos); + let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); + + let doc = alloc.stack([ + alloc + .reflow(r"I am partway through parsing a function argument list, but I got stuck here:"), + alloc.region_with_subregion(lines.convert_region(surroundings), region), + alloc.concat([ + alloc.reflow("I was expecting a "), + alloc.parser_suggestion("->"), + alloc.reflow(" next."), + ]), + ]); + + Report { + filename, + doc, + title: "MISSING ARROW".to_string(), + severity: Severity::RuntimeError, + } + } + }, + + EClosure::Arg(pos) => match what_is_next(alloc.src_lines, lines.convert_pos(pos)) { + Next::Other(Some(',')) => { + let surroundings = Region::new(start, pos); + let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); + + let doc = alloc.stack([ + alloc + .reflow(r"I am partway through parsing a function argument list, but I got stuck at this comma:"), + alloc.region_with_subregion(lines.convert_region(surroundings), region), + alloc.concat([ + alloc.reflow("I was expecting an argument pattern before this, "), + alloc.reflow("so try adding an argument before the comma and see if that helps?"), + ]), + ]); + + Report { + filename, + doc, + title: "UNFINISHED ARGUMENT LIST".to_string(), + severity: Severity::RuntimeError, + } + } + _ => { + let surroundings = Region::new(start, pos); + let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); + + let doc = alloc.stack([ + alloc + .reflow(r"I am partway through parsing a function argument list, but I got stuck here:"), + alloc.region_with_subregion(lines.convert_region(surroundings), region), + alloc.concat([ + alloc.reflow("I was expecting an argument pattern before this, "), + alloc.reflow("so try adding an argument and see if that helps?"), + ]), + ]); + + Report { + filename, + doc, + title: "MISSING ARROW".to_string(), + severity: Severity::RuntimeError, + } + } + }, + + EClosure::Start(_pos) => unreachable!("another branch would have been taken"), + + EClosure::Body(expr, pos) => { + to_expr_report(alloc, lines, filename, Context::InDef(start), expr, pos) + } + EClosure::Pattern(ref pattern, pos) => { + to_pattern_report(alloc, lines, filename, pattern, pos) + } + EClosure::Space(error, pos) => to_space_report(alloc, lines, filename, &error, pos), + + EClosure::IndentArrow(pos) => to_unfinished_lambda_report( + alloc, + lines, + filename, + pos, + start, + alloc.concat([ + alloc.reflow(r"I just saw a pattern, so I was expecting to see a "), + alloc.parser_suggestion("->"), + alloc.reflow(" next."), + ]), + ), + + EClosure::IndentBody(pos) => to_unfinished_lambda_report( + alloc, + lines, + filename, + pos, + start, + alloc.concat([ + alloc.reflow(r"I just saw a pattern, so I was expecting to see a "), + alloc.parser_suggestion("->"), + alloc.reflow(" next."), + ]), + ), + + EClosure::IndentArg(pos) => to_unfinished_lambda_report( + alloc, + lines, + filename, + pos, + start, + alloc.concat([ + alloc.reflow(r"I just saw a pattern, so I was expecting to see a "), + alloc.parser_suggestion("->"), + alloc.reflow(" next."), + alloc.reflow(r"I was expecting to see a expression next"), + ]), + ), + } +} + +fn to_unfinished_lambda_report<'a>( + alloc: &'a RocDocAllocator<'a>, + lines: &LineInfo, + filename: PathBuf, + pos: Position, + start: Position, + message: RocDocBuilder<'a>, +) -> Report<'a> { + let surroundings = Region::new(start, pos); + let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); + + let doc = alloc.stack([ + alloc.concat([ + alloc.reflow(r"I was partway through parsing a "), + alloc.reflow(r" function, but I got stuck here:"), + ]), + alloc.region_with_subregion(lines.convert_region(surroundings), region), + message, + ]); + + Report { + filename, + doc, + title: "UNFINISHED FUNCTION".to_string(), + severity: Severity::RuntimeError, + } +} + +fn to_str_report<'a>( + alloc: &'a RocDocAllocator<'a>, + lines: &LineInfo, + filename: PathBuf, + context: Context, + parse_problem: &roc_parse::parser::EString<'a>, + start: Position, +) -> Report<'a> { + use roc_parse::parser::EString; + + match *parse_problem { + EString::Open(_pos) => unreachable!("another branch would be taken"), + EString::Format(expr, pos) => to_expr_report( + alloc, + lines, + filename, + Context::InNode(Node::StringFormat, start, Box::new(context)), + expr, + pos, + ), + EString::Space(error, pos) => to_space_report(alloc, lines, filename, &error, pos), + EString::UnknownEscape(pos) => { + let surroundings = Region::new(start, pos); + let region = Region::new(pos, pos.bump_column(2)); + + let suggestion = |msg, sugg| { + alloc + .text("- ") + .append(alloc.reflow(msg)) + .append(alloc.parser_suggestion(sugg)) + }; + + let doc = alloc.stack([ + alloc.concat([ + alloc.reflow(r"I was partway through parsing a "), + alloc.reflow(r" string literal, but I got stuck here:"), + ]), + alloc.region_with_subregion( + lines.convert_region(surroundings), + lines.convert_region(region), + ), + alloc.concat([ + alloc.reflow(r"This is not an escape sequence I recognize."), + alloc.reflow(r" After a backslash, I am looking for one of these:"), + ]), + alloc + .vcat(vec![ + suggestion("A newline: ", "\\n"), + suggestion("A caret return: ", "\\r"), + suggestion("A tab: ", "\\t"), + suggestion("An escaped quote: ", "\\\""), + suggestion("An escaped backslash: ", "\\\\"), + suggestion("A unicode code point: ", "\\u(00FF)"), + suggestion("An interpolated string: ", "\\(myVariable)"), + ]) + .indent(4), + ]); + + Report { + filename, + doc, + title: "WEIRD ESCAPE".to_string(), + severity: Severity::RuntimeError, + } + } + EString::CodePtOpen(pos) | EString::CodePtEnd(pos) => { + let surroundings = Region::new(start, pos); + let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); + + let doc = alloc.stack([ + alloc.reflow( + r"I am partway through parsing a unicode code point, but I got stuck here:", + ), + alloc.region_with_subregion(lines.convert_region(surroundings), region), + alloc.concat([ + alloc.reflow(r"I was expecting a hexadecimal number, like "), + alloc.parser_suggestion("\\u(1100)"), + alloc.reflow(" or "), + alloc.parser_suggestion("\\u(00FF)"), + alloc.text("."), + ]), + alloc.reflow(r"Learn more about working with unicode in roc at TODO"), + ]); + + Report { + filename, + doc, + title: "WEIRD CODE POINT".to_string(), + severity: Severity::RuntimeError, + } + } + EString::FormatEnd(pos) => { + let surroundings = Region::new(start, pos); + let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); + + let doc = alloc.stack([ + alloc.reflow(r"I cannot find the end of this format expression:"), + alloc.region_with_subregion(lines.convert_region(surroundings), region), + alloc.concat([ + alloc.reflow(r"You could change it to something like "), + alloc.parser_suggestion("\"The count is \\(count\\)\""), + alloc.reflow("."), + ]), + ]); + + Report { + filename, + doc, + title: "ENDLESS FORMAT".to_string(), + severity: Severity::RuntimeError, + } + } + EString::EndlessSingleQuote(pos) => { + let surroundings = Region::new(start, pos); + let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); + + let doc = alloc.stack([ + alloc.reflow(r"I cannot find the end of this scalar literal (character literal):"), + alloc.region_with_subregion(lines.convert_region(surroundings), region), + alloc.concat([ + alloc.reflow(r"You could change it to something like "), + alloc.parser_suggestion("'a'"), + alloc.reflow(" or "), + alloc.parser_suggestion("'\n'"), + alloc.reflow("."), + ]), + ]); + + Report { + filename, + doc, + title: "ENDLESS SCALAR".to_string(), + severity: Severity::RuntimeError, + } + } + EString::InvalidSingleQuote(e, pos) => { + let surroundings = Region::new(start, pos); + let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); + + let doc = match e { + ESingleQuote::Empty => { + alloc.stack([ + alloc.concat([ + alloc.reflow(r"I am part way through parsing this scalar literal (character literal), "), + alloc.reflow(r"but it appears to be empty - which is not a valid scalar."), + ]), + alloc.region_with_subregion(lines.convert_region(surroundings), region), + alloc.concat([ + alloc.reflow(r"You could change it to something like "), + alloc.parser_suggestion("'a'"), + alloc.reflow(" or "), + alloc.parser_suggestion("'\\n'"), + alloc.reflow(". "), + alloc.reflow("Note, roc strings use double quotes, like \"hello\".") + ]), + ]) + } + ESingleQuote::TooLong => { + alloc.stack([ + alloc.concat([ + alloc.reflow(r"I am part way through parsing this scalar literal (character literal), "), + alloc.reflow(r"but it's too long to fit in a U32 so it's not a valid scalar."), + ]), + alloc.region_with_subregion(lines.convert_region(surroundings), region), + alloc.concat([ + alloc.reflow(r"You could change it to something like "), + alloc.parser_suggestion("'a'"), + alloc.reflow(" or "), + alloc.parser_suggestion("'\\n'"), + alloc.reflow(". "), + alloc.reflow("Note, roc strings use double quotes, like \"hello\".") + ]), + ]) + } + ESingleQuote::InterpolationNotAllowed => { + alloc.stack([ + alloc.concat([ + alloc.reflow("I am part way through parsing this scalar literal (character literal), "), + alloc.reflow("but I encountered a string interpolation like \"\\(this)\", which is not "), + alloc.reflow("allowed in scalar literals."), + ]), + alloc.region_with_subregion(lines.convert_region(surroundings), region), + alloc.concat([ + alloc.reflow(r"You could change it to something like "), + alloc.parser_suggestion("'a'"), + alloc.reflow(" or "), + alloc.parser_suggestion("'\\n'"), + alloc.reflow(". "), + alloc.reflow("Note, roc strings use double quotes, like \"hello\".") + ]), + ]) + } + }; + + Report { + filename, + doc, + title: "INVALID SCALAR".to_string(), + severity: Severity::RuntimeError, + } + } + EString::EndlessSingleLine(pos) => { + let surroundings = Region::new(start, pos); + let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); + + let doc = alloc.stack([ + alloc.reflow(r"I cannot find the end of this string:"), + alloc.region_with_subregion(lines.convert_region(surroundings), region), + alloc.concat([ + alloc.reflow(r"You could change it to something like "), + alloc.parser_suggestion("\"to be or not to be\""), + alloc.reflow(" or even just "), + alloc.parser_suggestion("\"\""), + alloc.reflow("."), + ]), + ]); + + Report { + filename, + doc, + title: "ENDLESS STRING".to_string(), + severity: Severity::RuntimeError, + } + } + EString::ExpectedDoubleQuoteGotSingleQuote(pos) => { + let surroundings = Region::new(start, pos); + let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); + + let doc = alloc.stack([ + alloc.reflow(r"I was expecting to see a string here, but I got a scalar literal."), + alloc.region_with_subregion(lines.convert_region(surroundings), region), + alloc.concat([ + alloc.reflow(r"You could change it to something like "), + alloc.parser_suggestion("\"to be or not to be\""), + alloc.reflow(" or even just "), + alloc.parser_suggestion("\"\""), + alloc.reflow(". "), + alloc.reflow("Note, roc strings use double quotes."), + ]), + ]); + + Report { + filename, + doc, + title: "EXPECTED STRING".to_string(), + severity: Severity::RuntimeError, + } + } + EString::EndlessMultiLine(pos) => { + let surroundings = Region::new(start, pos); + let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); + + let doc = alloc.stack([ + alloc.reflow(r"I cannot find the end of this block string:"), + alloc.region_with_subregion(lines.convert_region(surroundings), region), + alloc.concat([ + alloc.reflow(r"You could change it to something like "), + alloc.parser_suggestion("\"\"\"to be or not to be\"\"\""), + alloc.reflow(" or even just "), + alloc.parser_suggestion("\"\"\"\"\"\""), + alloc.reflow("."), + ]), + ]); + + Report { + filename, + doc, + title: "ENDLESS STRING".to_string(), + severity: Severity::RuntimeError, + } + } + EString::MultilineInsufficientIndent(pos) => { + let surroundings = Region::new(start, pos); + let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); + + let doc = alloc.stack([ + alloc.reflow(r"This multiline string is not sufficiently indented:"), + alloc.region_with_subregion(lines.convert_region(surroundings), region), + alloc.concat([ + alloc.reflow(r"Lines in a multi-line string must be indented at least as "), + alloc.reflow("much as the beginning \"\"\". This extra indentation is automatically removed "), + alloc.reflow("from the string during compilation."), + ]), + ]); + + Report { + filename, + doc, + title: "INSUFFICIENT INDENT IN MULTI-LINE STRING".to_string(), + severity: Severity::RuntimeError, + } + } + } +} +fn to_expr_in_parens_report<'a>( + alloc: &'a RocDocAllocator<'a>, + lines: &LineInfo, + filename: PathBuf, + context: Context, + parse_problem: &roc_parse::parser::EInParens<'a>, + start: Position, +) -> Report<'a> { + use roc_parse::parser::EInParens; + + match *parse_problem { + EInParens::Space(error, pos) => to_space_report(alloc, lines, filename, &error, pos), + EInParens::Expr(expr, pos) => to_expr_report( + alloc, + lines, + filename, + Context::InNode(Node::InsideParens, start, Box::new(context)), + expr, + pos, + ), + EInParens::Empty(pos) => { + let surroundings = Region::new(start, pos); + let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); + + let doc = alloc.stack([ + alloc.reflow("I am partway through parsing a parenthesized expression or tuple:"), + alloc.region_with_subregion(lines.convert_region(surroundings), region), + alloc.concat([ + alloc.reflow(r"I was expecting to see an expression next."), + alloc.reflow(r"Note, Roc doesn't use '()' as a null type."), + ]), + ]); + + Report { + filename, + doc, + title: "EMPTY PARENTHESES".to_string(), + severity: Severity::RuntimeError, + } + } + EInParens::End(pos) => { + let surroundings = Region::new(start, pos); + let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); + + let doc = alloc.stack([ + alloc + .reflow("I am partway through parsing a record pattern, but I got stuck here:"), + alloc.region_with_subregion(lines.convert_region(surroundings), region), + alloc.concat([ + alloc.reflow( + r"I was expecting to see a closing parenthesis next, so try adding a ", + ), + alloc.parser_suggestion(")"), + alloc.reflow(" and see if that helps?"), + ]), + ]); + + Report { + filename, + doc, + title: "UNFINISHED PARENTHESES".to_string(), + severity: Severity::RuntimeError, + } + } + EInParens::Open(pos) => { + let surroundings = Region::new(start, pos); + let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); + + let doc = alloc.stack([ + alloc.reflow( + r"I just started parsing an expression in parentheses, but I got stuck here:", + ), + alloc.region_with_subregion(lines.convert_region(surroundings), region), + alloc.concat([ + alloc.reflow(r"An expression in parentheses looks like "), + alloc.parser_suggestion("(32)"), + alloc.reflow(r" or "), + alloc.parser_suggestion("(\"hello\")"), + alloc.reflow(" so I was expecting to see an expression next."), + ]), + ]); + + Report { + filename, + doc, + title: "UNFINISHED PARENTHESES".to_string(), + severity: Severity::RuntimeError, + } + } + } +} + +fn to_list_report<'a>( + alloc: &'a RocDocAllocator<'a>, + lines: &LineInfo, + filename: PathBuf, + context: Context, + parse_problem: &roc_parse::parser::EList<'a>, + start: Position, +) -> Report<'a> { + use roc_parse::parser::EList; + + match *parse_problem { + EList::Space(error, pos) => to_space_report(alloc, lines, filename, &error, pos), + + EList::Expr(expr, pos) => to_expr_report( + alloc, + lines, + filename, + Context::InNode(Node::ListElement, start, Box::new(context)), + expr, + pos, + ), + + EList::Open(pos) | EList::End(pos) => { + match what_is_next(alloc.src_lines, lines.convert_pos(pos)) { + Next::Other(Some(',')) => { + let surroundings = Region::new(start, pos); + let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); + + let doc = alloc.stack([ + alloc.reflow( + r"I am partway through started parsing a list, but I got stuck here:", + ), + alloc.region_with_subregion(lines.convert_region(surroundings), region), + alloc.concat([ + alloc + .reflow(r"I was expecting to see a list entry before this comma, "), + alloc.reflow(r"so try adding a list entry"), + alloc.reflow(r" and see if that helps?"), + ]), + ]); + Report { + filename, + doc, + title: "UNFINISHED LIST".to_string(), + severity: Severity::RuntimeError, + } + } + _ => { + let surroundings = Region::new(start, pos); + let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); + + let doc = alloc.stack([ + alloc.reflow( + r"I am partway through started parsing a list, but I got stuck here:", + ), + alloc.region_with_subregion(lines.convert_region(surroundings), region), + alloc.concat([ + alloc.reflow( + r"I was expecting to see a closing square bracket before this, ", + ), + alloc.reflow(r"so try adding a "), + alloc.parser_suggestion("]"), + alloc.reflow(r" and see if that helps?"), + ]), + alloc.concat([ + alloc.note("When "), + alloc.reflow(r"I get stuck like this, "), + alloc.reflow(r"it usually means that there is a missing parenthesis "), + alloc.reflow(r"or bracket somewhere earlier. "), + alloc.reflow(r"It could also be a stray keyword or operator."), + ]), + ]); + + Report { + filename, + doc, + title: "UNFINISHED LIST".to_string(), + severity: Severity::RuntimeError, + } + } + } + } + } +} + +fn to_dbg_or_expect_report<'a>( + alloc: &'a RocDocAllocator<'a>, + lines: &LineInfo, + filename: PathBuf, + context: Context, + node: Node, + parse_problem: &roc_parse::parser::EExpect<'a>, + start: Position, +) -> Report<'a> { + match parse_problem { + roc_parse::parser::EExpect::Space(err, pos) => { + to_space_report(alloc, lines, filename, err, *pos) + } + + roc_parse::parser::EExpect::Dbg(_) => unreachable!("another branch would be taken"), + roc_parse::parser::EExpect::Expect(_) => unreachable!("another branch would be taken"), + + roc_parse::parser::EExpect::Condition(e_expr, condition_start) => { + // is adding context helpful here? + to_expr_report(alloc, lines, filename, context, e_expr, *condition_start) + } + roc_parse::parser::EExpect::Continuation(e_expr, continuation_start) => { + let context = Context::InNode(node, start, Box::new(context)); + to_expr_report(alloc, lines, filename, context, e_expr, *continuation_start) + } + + roc_parse::parser::EExpect::IndentCondition(_) => todo!(), + } +} + +fn to_if_report<'a>( + alloc: &'a RocDocAllocator<'a>, + lines: &LineInfo, + filename: PathBuf, + context: Context, + parse_problem: &roc_parse::parser::EIf<'a>, + start: Position, +) -> Report<'a> { + use roc_parse::parser::EIf; + + match *parse_problem { + EIf::Space(error, pos) => to_space_report(alloc, lines, filename, &error, pos), + + EIf::Condition(expr, pos) => to_expr_report( + alloc, + lines, + filename, + Context::InNode(Node::IfCondition, start, Box::new(context)), + expr, + pos, + ), + + EIf::ThenBranch(expr, pos) => to_expr_report( + alloc, + lines, + filename, + Context::InNode(Node::IfThenBranch, start, Box::new(context)), + expr, + pos, + ), + + EIf::ElseBranch(expr, pos) => to_expr_report( + alloc, + lines, + filename, + Context::InNode(Node::IfElseBranch, start, Box::new(context)), + expr, + pos, + ), + + EIf::If(_pos) => unreachable!("another branch would be taken"), + EIf::IndentIf(_pos) => unreachable!("another branch would be taken"), + + EIf::Then(pos) | EIf::IndentThenBranch(pos) | EIf::IndentThenToken(pos) => { + to_unfinished_if_report( + alloc, + lines, + filename, + pos, + start, + alloc.concat([ + alloc.reflow(r"I was expecting to see the "), + alloc.keyword("then"), + alloc.reflow(r" keyword next."), + ]), + ) + } + + EIf::Else(pos) | EIf::IndentElseBranch(pos) | EIf::IndentElseToken(pos) => { + to_unfinished_if_report( + alloc, + lines, + filename, + pos, + start, + alloc.concat([ + alloc.reflow(r"I was expecting to see the "), + alloc.keyword("else"), + alloc.reflow(r" keyword next."), + ]), + ) + } + + EIf::IndentCondition(pos) => to_unfinished_if_report( + alloc, + lines, + filename, + pos, + start, + alloc.concat([alloc.reflow(r"I was expecting to see a expression next")]), + ), + } +} + +fn to_unfinished_if_report<'a>( + alloc: &'a RocDocAllocator<'a>, + lines: &LineInfo, + filename: PathBuf, + pos: Position, + start: Position, + message: RocDocBuilder<'a>, +) -> Report<'a> { + let surroundings = Region::new(start, pos); + let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); + + let doc = alloc.stack([ + alloc.concat([ + alloc.reflow(r"I was partway through parsing an "), + alloc.keyword("if"), + alloc.reflow(r" expression, but I got stuck here:"), + ]), + alloc.region_with_subregion(lines.convert_region(surroundings), region), + message, + ]); + + Report { + filename, + doc, + title: "UNFINISHED IF".to_string(), + severity: Severity::RuntimeError, + } +} + +fn to_when_report<'a>( + alloc: &'a RocDocAllocator<'a>, + lines: &LineInfo, + filename: PathBuf, + context: Context, + parse_problem: &roc_parse::parser::EWhen<'a>, + start: Position, +) -> Report<'a> { + use roc_parse::parser::EWhen; + + match *parse_problem { + EWhen::IfGuard(nested, pos) => { + match what_is_next(alloc.src_lines, lines.convert_pos(pos)) { + Next::Token("->") => { + let surroundings = Region::new(start, pos); + let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); + + let doc = alloc.stack([ + alloc.reflow( + r"I just started parsing an if guard, but there is no guard condition:", + ), + alloc.region_with_subregion(lines.convert_region(surroundings), region), + alloc.concat([alloc.reflow("Try adding an expression before the arrow!")]), + ]); + + Report { + filename, + doc, + title: "IF GUARD NO CONDITION".to_string(), + severity: Severity::RuntimeError, + } + } + _ => to_expr_report( + alloc, + lines, + filename, + Context::InNode(Node::WhenIfGuard, start, Box::new(context)), + nested, + pos, + ), + } + } + EWhen::Arrow(pos) => { + let surroundings = Region::new(start, pos); + let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); + + let doc = alloc.stack([ + alloc.concat([ + alloc.reflow(r"I am partway through parsing a "), + alloc.keyword("when"), + alloc.reflow(r" expression, but got stuck here:"), + ]), + alloc.region_with_subregion(lines.convert_region(surroundings), region), + alloc.concat([alloc.reflow("I was expecting to see an arrow next.")]), + note_for_when_indent_error(alloc), + ]); + + Report { + filename, + doc, + title: "MISSING ARROW".to_string(), + severity: Severity::RuntimeError, + } + } + + EWhen::Space(error, pos) => to_space_report(alloc, lines, filename, &error, pos), + + EWhen::Branch(expr, pos) => to_expr_report( + alloc, + lines, + filename, + Context::InNode(Node::WhenBranch, start, Box::new(context)), + expr, + pos, + ), + + EWhen::Condition(expr, pos) => to_expr_report( + alloc, + lines, + filename, + Context::InNode(Node::WhenCondition, start, Box::new(context)), + expr, + pos, + ), + + EWhen::Bar(pos) => to_unfinished_when_report( + alloc, + lines, + filename, + pos, + start, + alloc.concat([ + alloc.reflow(r"I just saw a "), + alloc.parser_suggestion(r"|"), + alloc.reflow(r" so I was expecting to see a pattern next."), + ]), + ), + + EWhen::IfToken(_pos) => unreachable!("the if-token is optional"), + EWhen::When(_pos) => unreachable!("another branch would be taken"), + + EWhen::Is(pos) => to_unfinished_when_report( + alloc, + lines, + filename, + pos, + start, + alloc.concat([ + alloc.reflow(r"I was expecting to see the "), + alloc.keyword("is"), + alloc.reflow(r" keyword next."), + ]), + ), + + EWhen::IndentCondition(pos) => to_unfinished_when_report( + alloc, + lines, + filename, + pos, + start, + alloc.concat([alloc.reflow(r"I was expecting to see a expression next")]), + ), + + EWhen::IndentPattern(pos) => to_unfinished_when_report( + alloc, + lines, + filename, + pos, + start, + alloc.concat([alloc.reflow(r"I was expecting to see a pattern next")]), + ), + + EWhen::IndentArrow(pos) => to_unfinished_when_report( + alloc, + lines, + filename, + pos, + start, + alloc.concat([ + alloc.reflow(r"I just saw a pattern, so I was expecting to see a "), + alloc.parser_suggestion("->"), + alloc.reflow(" next."), + ]), + ), + + EWhen::IndentIfGuard(pos) => to_unfinished_when_report( + alloc, + lines, + filename, + pos, + start, + alloc.concat([ + alloc.reflow(r"I just saw the "), + alloc.keyword("if"), + alloc.reflow(" keyword, so I was expecting to see an expression next."), + ]), + ), + + EWhen::IndentBranch(pos) => to_unfinished_when_report( + alloc, + lines, + filename, + pos, + start, + alloc.concat([ + alloc.reflow(r"I was expecting to see an expression next. "), + alloc.reflow("What should I do when I run into this particular pattern?"), + ]), + ), + + EWhen::PatternAlignment(indent, pos) => to_unfinished_when_report( + alloc, + lines, + filename, + pos, + start, + alloc.concat([ + alloc.reflow(r"I suspect this is a pattern that is not indented enough? (by "), + alloc.text(indent.to_string()), + alloc.reflow(" spaces)"), + ]), + ), + EWhen::Pattern(ref pat, pos) => to_pattern_report(alloc, lines, filename, pat, pos), + } +} + +fn to_unfinished_when_report<'a>( + alloc: &'a RocDocAllocator<'a>, + lines: &LineInfo, + filename: PathBuf, + pos: Position, + start: Position, + message: RocDocBuilder<'a>, +) -> Report<'a> { + match what_is_next(alloc.src_lines, lines.convert_pos(pos)) { + Next::Token("->") => to_unexpected_arrow_report(alloc, lines, filename, pos, start), + + _ => { + let surroundings = Region::new(start, pos); + let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); + + let doc = alloc.stack([ + alloc.concat([ + alloc.reflow(r"I was partway through parsing a "), + alloc.keyword("when"), + alloc.reflow(r" expression, but I got stuck here:"), + ]), + alloc.region_with_subregion(lines.convert_region(surroundings), region), + message, + note_for_when_error(alloc), + ]); + + Report { + filename, + doc, + title: "UNFINISHED WHEN".to_string(), + severity: Severity::RuntimeError, + } + } + } +} + +fn to_unexpected_arrow_report<'a>( + alloc: &'a RocDocAllocator<'a>, + lines: &LineInfo, + filename: PathBuf, + pos: Position, + start: Position, +) -> Report<'a> { + let surroundings = Region::new(start, pos); + let region = Region::new(pos, pos.bump_column(2)); + + let doc = alloc.stack([ + alloc.concat([ + alloc.reflow(r"I am parsing a "), + alloc.keyword("when"), + alloc.reflow(r" expression right now, but this arrow is confusing me:"), + ]), + alloc.region_with_subregion( + lines.convert_region(surroundings), + lines.convert_region(region), + ), + alloc.concat([ + alloc.reflow(r"It makes sense to see arrows around here, "), + alloc.reflow(r"so I suspect it is something earlier. "), + alloc.reflow( + r"Maybe this pattern is indented a bit farther from the previous patterns?", + ), + ]), + note_for_when_error(alloc), + ]); + + Report { + filename, + doc, + title: "UNEXPECTED ARROW".to_string(), + severity: Severity::RuntimeError, + } +} + +fn note_for_when_error<'a>(alloc: &'a RocDocAllocator<'a>) -> RocDocBuilder<'a> { + alloc.stack([ + alloc.concat([ + alloc.note("Here is an example of a valid "), + alloc.keyword("when"), + alloc.reflow(r" expression for reference."), + ]), + alloc.vcat(vec![ + alloc.text("when List.first plants is").indent(4), + alloc.text("Ok n ->").indent(6), + alloc.text("n").indent(8), + alloc.text(""), + alloc.text("Err _ ->").indent(6), + alloc.text("200").indent(8), + ]), + alloc.concat([ + alloc.reflow( + "Notice the indentation. All patterns are aligned, and each branch is indented", + ), + alloc.reflow(" a bit more than the corresponding pattern. That is important!"), + ]), + ]) +} + +fn note_for_when_indent_error<'a>(alloc: &'a RocDocAllocator<'a>) -> RocDocBuilder<'a> { + alloc.stack([ + alloc.concat([ + alloc.note("Sometimes I get confused by indentation, so try to make your "), + alloc.keyword("when"), + alloc.reflow(r" look something like this:"), + ]), + alloc.vcat(vec![ + alloc.text("when List.first plants is").indent(4), + alloc.text("Ok n ->").indent(6), + alloc.text("n").indent(8), + alloc.text(""), + alloc.text("Err _ ->").indent(6), + alloc.text("200").indent(8), + ]), + alloc.concat([ + alloc.reflow( + "Notice the indentation. All patterns are aligned, and each branch is indented", + ), + alloc.reflow(" a bit more than the corresponding pattern. That is important!"), + ]), + ]) +} + +fn to_pattern_report<'a>( + alloc: &'a RocDocAllocator<'a>, + lines: &LineInfo, + filename: PathBuf, + parse_problem: &roc_parse::parser::EPattern<'a>, + start: Position, +) -> Report<'a> { + use roc_parse::parser::EPattern; + + match parse_problem { + EPattern::Start(pos) => { + let surroundings = Region::new(start, *pos); + let region = LineColumnRegion::from_pos(lines.convert_pos(*pos)); + + let doc = alloc.stack([ + alloc.reflow(r"I just started parsing a pattern, but I got stuck here:"), + alloc.region_with_subregion(lines.convert_region(surroundings), region), + alloc.note("I may be confused by indentation"), + ]); + + Report { + filename, + doc, + title: "UNFINISHED PATTERN".to_string(), + severity: Severity::RuntimeError, + } + } + EPattern::Record(record, pos) => to_precord_report(alloc, lines, filename, record, *pos), + EPattern::List(list, pos) => to_plist_report(alloc, lines, filename, list, *pos), + EPattern::PInParens(inparens, pos) => { + to_pattern_in_parens_report(alloc, lines, filename, inparens, *pos) + } + &EPattern::NumLiteral(ENumber::End, pos) => { + to_malformed_number_literal_report(alloc, lines, filename, pos) + } + _ => todo!("unhandled parse error: {:?}", parse_problem), + } +} + +fn to_precord_report<'a>( + alloc: &'a RocDocAllocator<'a>, + lines: &LineInfo, + filename: PathBuf, + parse_problem: &roc_parse::parser::PRecord<'a>, + start: Position, +) -> Report<'a> { + use roc_parse::parser::PRecord; + + match *parse_problem { + PRecord::Open(pos) => match what_is_next(alloc.src_lines, lines.convert_pos(pos)) { + Next::Keyword(keyword) => { + let surroundings = Region::new(start, pos); + let region = to_keyword_region(lines.convert_pos(pos), keyword); + + let doc = alloc.stack([ + alloc.reflow(r"I just started parsing a record pattern, but I got stuck on this field name:"), + alloc.region_with_subregion(lines.convert_region(surroundings), region), + alloc.concat([ + alloc.reflow(r"Looks like you are trying to use "), + alloc.keyword(keyword), + alloc.reflow(" as a field name, but that is a reserved word. Try using a different name!"), + ]), + ]); + + Report { + filename, + doc, + title: "UNFINISHED RECORD PATTERN".to_string(), + severity: Severity::RuntimeError, + } + } + _ => { + let surroundings = Region::new(start, pos); + let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); + + let doc = alloc.stack([ + alloc.reflow(r"I just started parsing a record pattern, but I got stuck here:"), + alloc.region_with_subregion(lines.convert_region(surroundings), region), + record_patterns_look_like(alloc), + ]); + + Report { + filename, + doc, + title: "UNFINISHED RECORD PATTERN".to_string(), + severity: Severity::RuntimeError, + } + } + }, + + PRecord::End(pos) => { + let surroundings = Region::new(start, pos); + let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); + + match what_is_next(alloc.src_lines, lines.convert_pos(pos)) { + Next::Other(Some(c)) if c.is_alphabetic() => { + let doc = alloc.stack([ + alloc.reflow(r"I am partway through parsing a record pattern, but I got stuck here:"), + alloc.region_with_subregion(lines.convert_region(surroundings), region), + alloc.concat([ + alloc.reflow( + r"I was expecting to see a colon, question mark, comma or closing curly brace.", + ), + ]), + ]); + + Report { + filename, + doc, + title: "UNFINISHED RECORD PATTERN".to_string(), + severity: Severity::RuntimeError, + } + } + _ => { + let doc = alloc.stack([ + alloc.reflow("I am partway through parsing a record pattern, but I got stuck here:"), + alloc.region_with_subregion(lines.convert_region(surroundings), region), + alloc.concat([ + alloc.reflow( + r"I was expecting to see a closing curly brace before this, so try adding a ", + ), + alloc.parser_suggestion("}"), + alloc.reflow(" and see if that helps?"), + ]), + ]); + + Report { + filename, + doc, + title: "UNFINISHED RECORD PATTERN".to_string(), + severity: Severity::RuntimeError, + } + } + } + } + + PRecord::Field(pos) => match what_is_next(alloc.src_lines, lines.convert_pos(pos)) { + Next::Keyword(keyword) => { + let surroundings = Region::new(start, pos); + let region = to_keyword_region(lines.convert_pos(pos), keyword); + + let doc = alloc.stack([ + alloc.reflow(r"I just started parsing a record pattern, but I got stuck on this field name:"), + alloc.region_with_subregion(lines.convert_region(surroundings), region), + alloc.concat([ + alloc.reflow(r"Looks like you are trying to use "), + alloc.keyword(keyword), + alloc.reflow(" as a field name, but that is a reserved word. Try using a different name!"), + ]), + ]); + + Report { + filename, + doc, + title: "UNFINISHED RECORD PATTERN".to_string(), + severity: Severity::RuntimeError, + } + } + Next::Other(Some(',')) => todo!(), + Next::Other(Some('}')) => unreachable!("or is it?"), + _ => { + let surroundings = Region::new(start, pos); + let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); + + let doc = alloc.stack([ + alloc.reflow(r"I am partway through parsing a record pattern, but I got stuck here:"), + alloc.region_with_subregion(lines.convert_region(surroundings), region), + alloc.concat([ + alloc.reflow(r"I was expecting to see another record field defined next, so I am looking for a name like "), + alloc.parser_suggestion("userName"), + alloc.reflow(" or "), + alloc.parser_suggestion("plantHight"), + alloc.reflow("."), + ]), + ]); + + Report { + filename, + doc, + title: "PROBLEM IN RECORD PATTERN".to_string(), + severity: Severity::RuntimeError, + } + } + }, + + PRecord::Colon(_) => { + unreachable!("because `foo` is a valid field; the colon is not required") + } + PRecord::Optional(_) => { + unreachable!("because `foo` is a valid field; the question mark is not required") + } + + PRecord::Pattern(pattern, pos) => to_pattern_report(alloc, lines, filename, pattern, pos), + + PRecord::Expr(expr, pos) => to_expr_report( + alloc, + lines, + filename, + Context::InNode( + Node::RecordConditionalDefault, + start, + Box::new(Context::InDef(pos)), + ), + expr, + pos, + ), + + PRecord::Space(error, pos) => to_space_report(alloc, lines, filename, &error, pos), + } +} + +fn to_plist_report<'a>( + alloc: &'a RocDocAllocator<'a>, + lines: &LineInfo, + filename: PathBuf, + parse_problem: &PList<'a>, + start: Position, +) -> Report<'a> { + match *parse_problem { + PList::Open(pos) => { + let surroundings = Region::new(start, pos); + let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); + + let doc = alloc.stack([ + alloc.reflow(r"I just started parsing a list pattern, but I got stuck here:"), + alloc.region_with_subregion(lines.convert_region(surroundings), region), + list_patterns_look_like(alloc), + ]); + + Report { + filename, + doc, + title: "UNFINISHED LIST PATTERN".to_string(), + severity: Severity::RuntimeError, + } + } + + PList::End(pos) => { + let surroundings = Region::new(start, pos); + let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); + let doc = alloc.stack([ + alloc.reflow("I am partway through parsing a list pattern, but I got stuck here:"), + alloc.region_with_subregion(lines.convert_region(surroundings), region), + alloc.concat([ + alloc.reflow( + r"I was expecting to see a closing square brace before this, so try adding a ", + ), + alloc.parser_suggestion("]"), + alloc.reflow(" and see if that helps?"), + ])]); + + Report { + filename, + doc, + title: "UNFINISHED LIST PATTERN".to_string(), + severity: Severity::RuntimeError, + } + } + + PList::Rest(pos) => { + let surroundings = Region::new(start, pos); + let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); + let doc = alloc.stack([ + alloc.reflow("It looks like you may trying to write a list rest pattern, but it's not the form I expect:"), + alloc.region_with_subregion(lines.convert_region(surroundings), region), + alloc.concat([ + alloc.reflow( + r"List rest patterns, which match zero or more elements in a list, are denoted with ", + ), + alloc.parser_suggestion(".."), + alloc.reflow(" - is that what you meant?"), + ])]); + + Report { + filename, + doc, + title: "INCORRECT REST PATTERN".to_string(), + severity: Severity::RuntimeError, + } + } + + PList::Pattern(pattern, pos) => to_pattern_report(alloc, lines, filename, pattern, pos), + + PList::Space(error, pos) => to_space_report(alloc, lines, filename, &error, pos), + } +} + +fn to_pattern_in_parens_report<'a>( + alloc: &'a RocDocAllocator<'a>, + lines: &LineInfo, + filename: PathBuf, + parse_problem: &roc_parse::parser::PInParens<'a>, + start: Position, +) -> Report<'a> { + use roc_parse::parser::PInParens; + + match *parse_problem { + PInParens::Open(pos) => { + // `Open` case is for exhaustiveness, this case shouldn not be reachable practically. + let surroundings = Region::new(start, pos); + let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); + + let doc = alloc.stack([ + alloc.reflow( + r"I just started parsing a pattern in parentheses, but I got stuck here:", + ), + alloc.region_with_subregion(lines.convert_region(surroundings), region), + alloc.concat([ + alloc.reflow(r"A pattern in parentheses looks like "), + alloc.parser_suggestion("(Ok 32)"), + alloc.reflow(r" or "), + alloc.parser_suggestion("(\"hello\")"), + alloc.reflow(" so I was expecting to see an expression next."), + ]), + ]); + + Report { + filename, + doc, + title: "UNFINISHED PARENTHESES".to_string(), + severity: Severity::RuntimeError, + } + } + + PInParens::Empty(pos) => { + let surroundings = Region::new(start, pos); + let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); + + let doc = alloc.stack([ + alloc.reflow("I am partway through parsing a parenthesized pattern or tuple:"), + alloc.region_with_subregion(lines.convert_region(surroundings), region), + alloc.concat([ + alloc.reflow(r"I was expecting to see a pattern next."), + alloc.reflow(r"Note, Roc doesn't use '()' as a null type."), + ]), + ]); + + Report { + filename, + doc, + title: "EMPTY PARENTHESES".to_string(), + severity: Severity::RuntimeError, + } + } + + PInParens::End(pos) => { + let surroundings = Region::new(start, pos); + let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); + + let doc = alloc.stack([ + alloc.reflow("I am partway through parsing a pattern in parentheses, but I got stuck here:"), + alloc.region_with_subregion(lines.convert_region(surroundings), region), + alloc.concat([ + alloc.reflow( + r"I was expecting to see a closing parenthesis before this, so try adding a ", + ), + alloc.parser_suggestion(")"), + alloc.reflow(" and see if that helps?"), + ]), + ]); + + Report { + filename, + doc, + title: "UNFINISHED PARENTHESES".to_string(), + severity: Severity::RuntimeError, + } + } + + PInParens::Pattern(pattern, pos) => to_pattern_report(alloc, lines, filename, pattern, pos), + + PInParens::Space(error, pos) => to_space_report(alloc, lines, filename, &error, pos), + } +} + +fn to_malformed_number_literal_report<'a>( + alloc: &'a RocDocAllocator<'a>, + lines: &LineInfo, + filename: PathBuf, + start: Position, +) -> Report<'a> { + let surroundings = Region::new(start, start); + let region = LineColumnRegion::from_pos(lines.convert_pos(start)); + + let doc = alloc.stack([ + alloc.reflow(r"This number literal is malformed:"), + alloc.region_with_subregion(lines.convert_region(surroundings), region), + ]); + + Report { + filename, + doc, + title: "INVALID NUMBER LITERAL".to_string(), + severity: Severity::RuntimeError, + } +} + +fn to_type_report<'a>( + alloc: &'a RocDocAllocator<'a>, + lines: &LineInfo, + filename: PathBuf, + parse_problem: &roc_parse::parser::EType<'a>, + start: Position, +) -> Report<'a> { + use roc_parse::parser::EType; + + match parse_problem { + EType::TRecord(record, pos) => to_trecord_report(alloc, lines, filename, record, *pos), + EType::TTagUnion(tag_union, pos) => { + to_ttag_union_report(alloc, lines, filename, tag_union, *pos) + } + EType::TInParens(tinparens, pos) => { + to_tinparens_report(alloc, lines, filename, tinparens, *pos) + } + EType::TApply(tapply, pos) => to_tapply_report(alloc, lines, filename, tapply, *pos), + EType::TInlineAlias(talias, _) => to_talias_report(alloc, lines, filename, talias), + + EType::TFunctionArgument(pos) => { + match what_is_next(alloc.src_lines, lines.convert_pos(*pos)) { + Next::Other(Some(',')) => { + let surroundings = Region::new(start, *pos); + let region = LineColumnRegion::from_pos(lines.convert_pos(*pos)); + + let doc = alloc.stack([ + alloc.reflow(r"I just started parsing a function argument type, but I encountered two commas in a row:"), + alloc.region_with_subregion(lines.convert_region(surroundings), region), + alloc.concat([alloc.reflow("Try removing one of them.")]), + ]); + + Report { + filename, + doc, + title: "DOUBLE COMMA".to_string(), + severity: Severity::RuntimeError, + } + } + _ => todo!(), + } + } + + EType::TStart(pos) => { + let surroundings = Region::new(start, *pos); + let region = LineColumnRegion::from_pos(lines.convert_pos(*pos)); + + let doc = alloc.stack([ + alloc.reflow(r"I just started parsing a type, but I got stuck here:"), + alloc.region_with_subregion(lines.convert_region(surroundings), region), + alloc.concat([ + alloc.reflow(r"I am expecting a type next, like "), + alloc.parser_suggestion("Bool"), + alloc.reflow(r" or "), + alloc.parser_suggestion("List a"), + alloc.reflow("."), + ]), + ]); + + Report { + filename, + doc, + title: "UNFINISHED TYPE".to_string(), + severity: Severity::RuntimeError, + } + } + + EType::TIndentStart(pos) => { + let surroundings = Region::new(start, *pos); + let region = LineColumnRegion::from_pos(lines.convert_pos(*pos)); + + let doc = alloc.stack([ + alloc.reflow(r"I just started parsing a type, but I got stuck here:"), + alloc.region_with_subregion(lines.convert_region(surroundings), region), + alloc.note("I may be confused by indentation"), + ]); + + Report { + filename, + doc, + title: "UNFINISHED TYPE".to_string(), + severity: Severity::RuntimeError, + } + } + + EType::TIndentEnd(pos) => { + let surroundings = Region::new(start, *pos); + let region = LineColumnRegion::from_pos(lines.convert_pos(*pos)); + + let doc = alloc.stack([ + alloc.reflow(r"I am partway through parsing a type, but I got stuck here:"), + alloc.region_with_subregion(lines.convert_region(surroundings), region), + alloc.note("I may be confused by indentation"), + ]); + + Report { + filename, + doc, + title: "UNFINISHED TYPE".to_string(), + severity: Severity::RuntimeError, + } + } + + EType::TAsIndentStart(pos) => { + let surroundings = Region::new(start, *pos); + let region = LineColumnRegion::from_pos(lines.convert_pos(*pos)); + + let doc = alloc.stack([ + alloc.reflow(r"I just started parsing an inline type alias, but I got stuck here:"), + alloc.region_with_subregion(lines.convert_region(surroundings), region), + alloc.note("I may be confused by indentation"), + ]); + + Report { + filename, + doc, + title: "UNFINISHED INLINE ALIAS".to_string(), + severity: Severity::RuntimeError, + } + } + + EType::TBadTypeVariable(pos) => { + let surroundings = Region::new(start, *pos); + let region = LineColumnRegion::from_pos(lines.convert_pos(*pos)); + + let doc = alloc.stack([ + alloc.reflow(r"I am expecting a type variable, but I got stuck here:"), + alloc.region_with_subregion(lines.convert_region(surroundings), region), + ]); + + Report { + filename, + doc, + title: "BAD TYPE VARIABLE".to_string(), + severity: Severity::RuntimeError, + } + } + + _ => todo!("unhandled type parse error: {:?}", &parse_problem), + } +} + +fn to_trecord_report<'a>( + alloc: &'a RocDocAllocator<'a>, + lines: &LineInfo, + filename: PathBuf, + parse_problem: &roc_parse::parser::ETypeRecord<'a>, + start: Position, +) -> Report<'a> { + use roc_parse::parser::ETypeRecord; + + match *parse_problem { + ETypeRecord::Open(pos) => match what_is_next(alloc.src_lines, lines.convert_pos(pos)) { + Next::Keyword(keyword) => { + let surroundings = Region::new(start, pos); + let region = to_keyword_region(lines.convert_pos(pos), keyword); + + let doc = alloc.stack([ + alloc.reflow(r"I just started parsing a record type, but I got stuck on this field name:"), + alloc.region_with_subregion(lines.convert_region(surroundings), region), + alloc.concat([ + alloc.reflow(r"Looks like you are trying to use "), + alloc.keyword(keyword), + alloc.reflow(" as a field name, but that is a reserved word. Try using a different name!"), + ]), + ]); + + Report { + filename, + doc, + title: "UNFINISHED RECORD TYPE".to_string(), + severity: Severity::RuntimeError, + } + } + _ => { + let surroundings = Region::new(start, pos); + let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); + + let doc = alloc.stack([ + alloc.reflow(r"I just started parsing a record type, but I got stuck here:"), + alloc.region_with_subregion(lines.convert_region(surroundings), region), + alloc.concat([ + alloc.reflow(r"Record types look like "), + alloc.parser_suggestion("{ name : String, age : Int },"), + alloc.reflow(" so I was expecting to see a field name next."), + ]), + ]); + + Report { + filename, + doc, + title: "UNFINISHED RECORD TYPE".to_string(), + severity: Severity::RuntimeError, + } + } + }, + + ETypeRecord::End(pos) => { + let surroundings = Region::new(start, pos); + let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); + + match what_is_next(alloc.src_lines, lines.convert_pos(pos)) { + Next::Other(Some(c)) if c.is_alphabetic() => { + let doc = alloc.stack([ + alloc.reflow(r"I am partway through parsing a record type, but I got stuck here:"), + alloc.region_with_subregion(lines.convert_region(surroundings), region), + alloc.concat([ + alloc.reflow( + r"I was expecting to see a colon, question mark, comma or closing curly brace.", + ), + ]), + ]); + + Report { + filename, + doc, + title: "UNFINISHED RECORD TYPE".to_string(), + severity: Severity::RuntimeError, + } + } + _ => { + let doc = alloc.stack([ + alloc.reflow("I am partway through parsing a record type, but I got stuck here:"), + alloc.region_with_subregion(lines.convert_region(surroundings), region), + alloc.concat([ + alloc.reflow( + r"I was expecting to see a closing curly brace before this, so try adding a ", + ), + alloc.parser_suggestion("}"), + alloc.reflow(" and see if that helps?"), + ]), + ]); + + Report { + filename, + doc, + title: "UNFINISHED RECORD TYPE".to_string(), + severity: Severity::RuntimeError, + } + } + } + } + + ETypeRecord::Field(pos) => match what_is_next(alloc.src_lines, lines.convert_pos(pos)) { + Next::Keyword(keyword) => { + let surroundings = Region::new(start, pos); + let region = to_keyword_region(lines.convert_pos(pos), keyword); + + let doc = alloc.stack([ + alloc.reflow(r"I just started parsing a record type, but I got stuck on this field name:"), + alloc.region_with_subregion(lines.convert_region(surroundings), region), + alloc.concat([ + alloc.reflow(r"Looks like you are trying to use "), + alloc.keyword(keyword), + alloc.reflow(" as a field name, but that is a reserved word. Try using a different name!"), + ]), + ]); + + Report { + filename, + doc, + title: "UNFINISHED RECORD TYPE".to_string(), + severity: Severity::RuntimeError, + } + } + Next::Other(Some(',')) => todo!(), + Next::Other(Some('}')) => unreachable!("or is it?"), + _ => { + let surroundings = Region::new(start, pos); + let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); + + let doc = alloc.stack([ + alloc.reflow(r"I am partway through parsing a record type, but I got stuck here:"), + alloc.region_with_subregion(lines.convert_region(surroundings), region), + alloc.concat([ + alloc.reflow(r"I was expecting to see another record field defined next, so I am looking for a name like "), + alloc.parser_suggestion("userName"), + alloc.reflow(" or "), + alloc.parser_suggestion("plantHight"), + alloc.reflow("."), + ]), + ]); + + Report { + filename, + doc, + title: "PROBLEM IN RECORD TYPE".to_string(), + severity: Severity::RuntimeError, + } + } + }, + + ETypeRecord::Colon(_) => { + unreachable!("because `foo` is a valid field; the colon is not required") + } + ETypeRecord::Optional(_) => { + unreachable!("because `foo` is a valid field; the question mark is not required") + } + + ETypeRecord::Type(tipe, pos) => to_type_report(alloc, lines, filename, tipe, pos), + + ETypeRecord::IndentOpen(pos) => { + let surroundings = Region::new(start, pos); + let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); + + let doc = alloc.stack([ + alloc.reflow(r"I just started parsing a record type, but I got stuck here:"), + alloc.region_with_subregion(lines.convert_region(surroundings), region), + alloc.concat([ + alloc.reflow(r"Record types look like "), + alloc.parser_suggestion("{ name : String, age : Int },"), + alloc.reflow(" so I was expecting to see a field name next."), + ]), + note_for_record_type_indent(alloc), + ]); + + Report { + filename, + doc, + title: "UNFINISHED RECORD TYPE".to_string(), + severity: Severity::RuntimeError, + } + } + + ETypeRecord::IndentEnd(pos) => { + match next_line_starts_with_close_curly(alloc.src_lines, lines.convert_pos(pos)) { + Some(curly_pos) => { + let surroundings = LineColumnRegion::new(lines.convert_pos(start), curly_pos); + let region = LineColumnRegion::from_pos(curly_pos); + + let doc = alloc.stack([ + alloc.reflow( + "I am partway through parsing a record type, but I got stuck here:", + ), + alloc.region_with_subregion(surroundings, region), + alloc.concat([ + alloc.reflow("I need this curly brace to be indented more. Try adding more spaces before it!"), + ]), + ]); + + Report { + filename, + doc, + title: "NEED MORE INDENTATION".to_string(), + severity: Severity::RuntimeError, + } + } + None => { + let surroundings = Region::new(start, pos); + let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); + + let doc = alloc.stack([ + alloc.reflow( + r"I am partway through parsing a record type, but I got stuck here:", + ), + alloc.region_with_subregion(lines.convert_region(surroundings), region), + alloc.concat([ + alloc.reflow("I was expecting to see a closing curly "), + alloc.reflow("brace before this, so try adding a "), + alloc.parser_suggestion("}"), + alloc.reflow(" and see if that helps?"), + ]), + note_for_record_type_indent(alloc), + ]); + + Report { + filename, + doc, + title: "UNFINISHED RECORD TYPE".to_string(), + severity: Severity::RuntimeError, + } + } + } + } + + ETypeRecord::IndentColon(_) => { + unreachable!("because `foo` is a valid field; the colon is not required") + } + + ETypeRecord::IndentOptional(_) => { + unreachable!("because `foo` is a valid field; the question mark is not required") + } + + ETypeRecord::Space(error, pos) => to_space_report(alloc, lines, filename, &error, pos), + } +} + +fn to_ttag_union_report<'a>( + alloc: &'a RocDocAllocator<'a>, + lines: &LineInfo, + filename: PathBuf, + parse_problem: &roc_parse::parser::ETypeTagUnion<'a>, + start: Position, +) -> Report<'a> { + use roc_parse::parser::ETypeTagUnion; + + match *parse_problem { + ETypeTagUnion::Open(pos) => match what_is_next(alloc.src_lines, lines.convert_pos(pos)) { + Next::Keyword(keyword) => { + let surroundings = Region::new(start, pos); + let region = to_keyword_region(lines.convert_pos(pos), keyword); + + let doc = alloc.stack([ + alloc.reflow(r"I just started parsing a tag union, but I got stuck on this field name:"), + alloc.region_with_subregion(lines.convert_region(surroundings), region), + alloc.concat([ + alloc.reflow(r"Looks like you are trying to use "), + alloc.keyword(keyword), + alloc.reflow(" as a tag name, but that is a reserved word. Tag names must start with a uppercase letter."), + ]), + ]); + + Report { + filename, + doc, + title: "UNFINISHED TAG UNION TYPE".to_string(), + severity: Severity::RuntimeError, + } + } + Next::Other(Some(c)) if c.is_alphabetic() => { + debug_assert!(c.is_lowercase()); + + let surroundings = Region::new(start, pos); + let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); + + let doc = alloc.stack([ + alloc.reflow( + r"I am partway through parsing a tag union type, but I got stuck here:", + ), + alloc.region_with_subregion(lines.convert_region(surroundings), region), + alloc.reflow(r"I was expecting to see a tag name."), + hint_for_tag_name(alloc), + ]); + + Report { + filename, + doc, + title: "WEIRD TAG NAME".to_string(), + severity: Severity::RuntimeError, + } + } + _ => { + let surroundings = Region::new(start, pos); + let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); + + let doc = alloc.stack([ + alloc.reflow(r"I just started parsing a tag union type, but I got stuck here:"), + alloc.region_with_subregion(lines.convert_region(surroundings), region), + alloc.concat([ + alloc.reflow(r"Tag unions look like "), + alloc.parser_suggestion("[Many I64, None],"), + alloc.reflow(" so I was expecting to see a tag name next."), + ]), + ]); + + Report { + filename, + doc, + title: "UNFINISHED TAG UNION TYPE".to_string(), + severity: Severity::RuntimeError, + } + } + }, + + ETypeTagUnion::End(pos) => { + let surroundings = Region::new(start, pos); + let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); + + match what_is_next(alloc.src_lines, lines.convert_pos(pos)) { + Next::Other(Some(c)) if c.is_alphabetic() => { + debug_assert!(c.is_lowercase()); + + let doc = alloc.stack([ + alloc.reflow( + r"I am partway through parsing a tag union type, but I got stuck here:", + ), + alloc.region_with_subregion(lines.convert_region(surroundings), region), + alloc.reflow(r"I was expecting to see a tag name."), + hint_for_tag_name(alloc), + ]); + + Report { + filename, + doc, + title: "WEIRD TAG NAME".to_string(), + severity: Severity::RuntimeError, + } + } + _ => { + let doc = alloc.stack([ + alloc.reflow(r"I am partway through parsing a tag union type, but I got stuck here:"), + alloc.region_with_subregion(lines.convert_region(surroundings), region), + alloc.concat([ + alloc.reflow( + r"I was expecting to see a closing square bracket before this, so try adding a ", + ), + alloc.parser_suggestion("]"), + alloc.reflow(" and see if that helps?"), + ]), + ]); + + Report { + filename, + doc, + title: "UNFINISHED TAG UNION TYPE".to_string(), + severity: Severity::RuntimeError, + } + } + } + } + + ETypeTagUnion::Type(tipe, pos) => to_type_report(alloc, lines, filename, tipe, pos), + + ETypeTagUnion::Space(error, pos) => to_space_report(alloc, lines, filename, &error, pos), + } +} + +fn to_tinparens_report<'a>( + alloc: &'a RocDocAllocator<'a>, + lines: &LineInfo, + filename: PathBuf, + parse_problem: &roc_parse::parser::ETypeInParens<'a>, + start: Position, +) -> Report<'a> { + use roc_parse::parser::ETypeInParens; + + match *parse_problem { + ETypeInParens::Open(pos) => { + match what_is_next(alloc.src_lines, lines.convert_pos(pos)) { + Next::Keyword(keyword) => { + let surroundings = Region::new(start, pos); + let region = to_keyword_region(lines.convert_pos(pos), keyword); + + let doc = alloc.stack([ + alloc.reflow(r"I just saw an open parenthesis, so I was expecting to see a type next."), + alloc.region_with_subregion(lines.convert_region(surroundings), region), + alloc.concat([ + alloc.reflow(r"Something like "), + alloc.parser_suggestion("(List Person)"), + alloc.text(" or "), + alloc.parser_suggestion("(Result I64 Str)"), + ]), + ]); + + Report { + filename, + doc, + title: "UNFINISHED PARENTHESES".to_string(), + severity: Severity::RuntimeError, + } + } + Next::Other(Some(c)) if c.is_alphabetic() => { + debug_assert!(c.is_lowercase()); + + let surroundings = Region::new(start, pos); + let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); + + let doc = alloc.stack([ + alloc.reflow( + r"I am partway through parsing a type in parentheses, but I got stuck here:", + ), + alloc.region_with_subregion(lines.convert_region(surroundings), region), + alloc.reflow(r"I was expecting to see a tag name."), + hint_for_tag_name(alloc), + ]); + + Report { + filename, + doc, + title: "WEIRD TAG NAME".to_string(), + severity: Severity::RuntimeError, + } + } + _ => { + let surroundings = Region::new(start, pos); + let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); + + let doc = alloc.stack([ + alloc.reflow( + r"I just started parsing a type in parentheses, but I got stuck here:", + ), + alloc.region_with_subregion(lines.convert_region(surroundings), region), + alloc.concat([ + alloc.reflow(r"Tag unions look like "), + alloc.parser_suggestion("[Many I64, None],"), + alloc.reflow(" so I was expecting to see a tag name next."), + ]), + ]); + + Report { + filename, + doc, + title: "UNFINISHED PARENTHESES".to_string(), + severity: Severity::RuntimeError, + } + } + } + } + + ETypeInParens::Empty(pos) => { + let surroundings = Region::new(start, pos); + let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); + + let doc = alloc.stack([ + alloc.reflow("I am partway through parsing a parenthesized type:"), + alloc.region_with_subregion(lines.convert_region(surroundings), region), + alloc.concat([ + alloc.reflow(r"I was expecting to see an expression next."), + alloc.reflow(r"Note, Roc doesn't use '()' as a null type."), + ]), + ]); + + Report { + filename, + doc, + title: "EMPTY PARENTHESES".to_string(), + severity: Severity::RuntimeError, + } + } + + ETypeInParens::End(pos) => { + let surroundings = Region::new(start, pos); + let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); + + match what_is_next(alloc.src_lines, lines.convert_pos(pos)) { + Next::Other(Some(c)) if c.is_alphabetic() => { + debug_assert!(c.is_lowercase()); + + // TODO hint for tuples? + let doc = alloc.stack([ + alloc.reflow( + r"I am partway through parsing a type in parentheses, but I got stuck here:", + ), + alloc.region_with_subregion(lines.convert_region(surroundings), region), + alloc.reflow(r"I was expecting to see a tag name."), + hint_for_tag_name(alloc), + ]); + + Report { + filename, + doc, + title: "WEIRD TAG NAME".to_string(), + severity: Severity::RuntimeError, + } + } + _ => { + let doc = alloc.stack([ + alloc.reflow(r"I am partway through parsing a type in parentheses, but I got stuck here:"), + alloc.region_with_subregion(lines.convert_region(surroundings), region), + alloc.concat([ + alloc.reflow( + r"I was expecting to see a closing parenthesis before this, so try adding a ", + ), + alloc.parser_suggestion(")"), + alloc.reflow(" and see if that helps?"), + ]), + ]); + + Report { + filename, + doc, + title: "UNFINISHED PARENTHESES".to_string(), + severity: Severity::RuntimeError, + } + } + } + } + + ETypeInParens::Type(tipe, pos) => to_type_report(alloc, lines, filename, tipe, pos), + + ETypeInParens::IndentOpen(pos) => { + let surroundings = Region::new(start, pos); + let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); + + let doc = alloc.stack([ + alloc + .reflow(r"I just started parsing a type in parentheses, but I got stuck here:"), + alloc.region_with_subregion(lines.convert_region(surroundings), region), + alloc.concat([ + alloc.reflow(r"Tag unions look like "), + alloc.parser_suggestion("[Many I64, None],"), + alloc.reflow(" so I was expecting to see a tag name next."), + ]), + note_for_tag_union_type_indent(alloc), + ]); + + Report { + filename, + doc, + title: "UNFINISHED PARENTHESES".to_string(), + severity: Severity::RuntimeError, + } + } + + ETypeInParens::IndentEnd(pos) => { + match next_line_starts_with_close_parenthesis(alloc.src_lines, lines.convert_pos(pos)) { + Some(curly_pos) => { + let surroundings = LineColumnRegion::new(lines.convert_pos(start), curly_pos); + let region = LineColumnRegion::from_pos(curly_pos); + + let doc = alloc.stack([ + alloc.reflow( + "I am partway through parsing a type in parentheses, but I got stuck here:", + ), + alloc.region_with_subregion(surroundings, region), + alloc.concat([ + alloc.reflow("I need this parenthesis to be indented more. Try adding more spaces before it!"), + ]), + ]); + + Report { + filename, + doc, + title: "NEED MORE INDENTATION".to_string(), + severity: Severity::RuntimeError, + } + } + None => { + let surroundings = Region::new(start, pos); + let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); + + let doc = alloc.stack([ + alloc.reflow( + r"I am partway through parsing a type in parentheses, but I got stuck here:", + ), + alloc.region_with_subregion(lines.convert_region(surroundings), region), + alloc.concat([ + alloc.reflow("I was expecting to see a parenthesis "), + alloc.reflow("before this, so try adding a "), + alloc.parser_suggestion(")"), + alloc.reflow(" and see if that helps?"), + ]), + note_for_tag_union_type_indent(alloc), + ]); + + Report { + filename, + doc, + title: "UNFINISHED PARENTHESES".to_string(), + severity: Severity::RuntimeError, + } + } + } + } + + ETypeInParens::Space(error, pos) => to_space_report(alloc, lines, filename, &error, pos), + } +} + +fn to_tapply_report<'a>( + alloc: &'a RocDocAllocator<'a>, + lines: &LineInfo, + filename: PathBuf, + parse_problem: &roc_parse::parser::ETypeApply, + _start: Position, +) -> Report<'a> { + use roc_parse::parser::ETypeApply; + + match *parse_problem { + ETypeApply::DoubleDot(pos) => { + let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); + + let doc = alloc.stack([ + alloc.reflow(r"I encountered two dots in a row:"), + alloc.region(region), + alloc.concat([alloc.reflow("Try removing one of them.")]), + ]); + + Report { + filename, + doc, + title: "DOUBLE DOT".to_string(), + severity: Severity::RuntimeError, + } + } + ETypeApply::TrailingDot(pos) => { + let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); + + let doc = alloc.stack([ + alloc.reflow(r"I encountered a dot with nothing after it:"), + alloc.region(region), + alloc.concat([ + alloc.reflow("Dots are used to refer to a type in a qualified way, like "), + alloc.parser_suggestion("Num.I64"), + alloc.text(" or "), + alloc.parser_suggestion("List.List a"), + alloc.reflow(". Try adding a type name next."), + ]), + ]); + + Report { + filename, + doc, + title: "TRAILING DOT".to_string(), + severity: Severity::RuntimeError, + } + } + ETypeApply::StartIsNumber(pos) => { + let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); + + let doc = alloc.stack([ + alloc.reflow(r"I encountered a number at the start of a qualified name segment:"), + alloc.region(region), + alloc.concat([ + alloc.reflow("All parts of a qualified type name must start with an uppercase letter, like "), + alloc.parser_suggestion("Num.I64"), + alloc.text(" or "), + alloc.parser_suggestion("List.List a"), + alloc.text("."), + ]), + ]); + + Report { + filename, + doc, + title: "WEIRD QUALIFIED NAME".to_string(), + severity: Severity::RuntimeError, + } + } + ETypeApply::StartNotUppercase(pos) => { + let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); + + let doc = alloc.stack([ + alloc.reflow(r"I encountered a lowercase letter at the start of a qualified name segment:"), + alloc.region(region), + alloc.concat([ + alloc.reflow("All parts of a qualified type name must start with an uppercase letter, like "), + alloc.parser_suggestion("Num.I64"), + alloc.text(" or "), + alloc.parser_suggestion("List.List a"), + alloc.text("."), + ]), + ]); + + Report { + filename, + doc, + title: "WEIRD QUALIFIED NAME".to_string(), + severity: Severity::RuntimeError, + } + } + + ETypeApply::End(pos) => { + let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); + + let doc = alloc.stack([ + alloc.reflow( + r"I reached the end of the input file while parsing a qualified type name", + ), + alloc.region(region), + ]); + + Report { + filename, + doc, + title: "END OF FILE".to_string(), + severity: Severity::RuntimeError, + } + } + + ETypeApply::Space(error, pos) => to_space_report(alloc, lines, filename, &error, pos), + } +} + +fn to_talias_report<'a>( + alloc: &'a RocDocAllocator<'a>, + lines: &LineInfo, + filename: PathBuf, + parse_problem: &roc_parse::parser::ETypeInlineAlias, +) -> Report<'a> { + use roc_parse::parser::ETypeInlineAlias; + + match *parse_problem { + ETypeInlineAlias::NotAnAlias(pos) => { + let region = Region::from_pos(pos); + + let doc = alloc.stack([ + alloc.concat([ + alloc.reflow("The inline type after this "), + alloc.keyword("as"), + alloc.reflow(" is not a type alias:"), + ]), + alloc.region(lines.convert_region(region)), + alloc.concat([ + alloc.reflow("Inline alias types must start with an uppercase identifier and be followed by zero or more type arguments, like "), + alloc.type_str("Point"), + alloc.reflow(" or "), + alloc.type_str("List a"), + alloc.reflow("."), + ]), + ]); + + Report { + filename, + doc, + title: "NOT AN INLINE ALIAS".to_string(), + severity: Severity::RuntimeError, + } + } + ETypeInlineAlias::Qualified(pos) => { + let region = Region::from_pos(pos); + + let doc = alloc.stack([ + alloc.reflow(r"This type alias has a qualified name:"), + alloc.region(lines.convert_region(region)), + alloc.reflow("An alias introduces a new name to the current scope, so it must be unqualified."), + ]); + + Report { + filename, + doc, + title: "QUALIFIED ALIAS NAME".to_string(), + severity: Severity::RuntimeError, + } + } + ETypeInlineAlias::ArgumentNotLowercase(pos) => { + let region = Region::from_pos(pos); + + let doc = alloc.stack([ + alloc.reflow(r"This alias type argument is not lowercase:"), + alloc.region(lines.convert_region(region)), + alloc.reflow("All type arguments must be lowercase."), + ]); + + Report { + filename, + doc, + title: "TYPE ARGUMENT NOT LOWERCASE".to_string(), + severity: Severity::RuntimeError, + } + } + } +} + +fn to_header_report<'a>( + alloc: &'a RocDocAllocator<'a>, + lines: &LineInfo, + filename: PathBuf, + parse_problem: &roc_parse::parser::EHeader<'a>, + start: Position, +) -> Report<'a> { + use roc_parse::parser::EHeader; + + match parse_problem { + EHeader::Provides(provides, pos) => { + to_provides_report(alloc, lines, filename, provides, *pos) + } + + EHeader::Exposes(exposes, pos) => to_exposes_report(alloc, lines, filename, exposes, *pos), + + EHeader::Imports(imports, pos) => to_imports_report(alloc, lines, filename, imports, *pos), + + EHeader::Requires(requires, pos) => { + to_requires_report(alloc, lines, filename, requires, *pos) + } + + EHeader::Packages(packages, pos) => { + to_packages_report(alloc, lines, filename, packages, *pos) + } + + EHeader::IndentStart(pos) => { + let surroundings = Region::new(start, *pos); + let region = LineColumnRegion::from_pos(lines.convert_pos(*pos)); + + let doc = alloc.stack([ + alloc.reflow(r"I am partway through parsing a header, but got stuck here:"), + alloc.region_with_subregion(lines.convert_region(surroundings), region), + alloc.concat([alloc.reflow("I may be confused by indentation.")]), + ]); + + Report { + filename, + doc, + title: "INCOMPLETE HEADER".to_string(), + severity: Severity::RuntimeError, + } + } + + EHeader::Start(pos) => { + let surroundings = Region::new(start, *pos); + let region = LineColumnRegion::from_pos(lines.convert_pos(*pos)); + + let is_utf8 = alloc + .src_lines + .iter() + .all(|line| std::str::from_utf8(line.as_bytes()).is_ok()); + + let preamble = if is_utf8 { + vec![ + alloc.reflow(r"I am expecting a header, but got stuck here:"), + alloc.region_with_subregion(lines.convert_region(surroundings), region), + ] + } else { + vec![alloc.reflow(r"I am expecting a header, but the file is not UTF-8 encoded.")] + }; + + let doc = alloc.stack(preamble.into_iter().chain([alloc.concat([ + alloc.reflow("I am expecting a module keyword next, one of "), + alloc.keyword("interface"), + alloc.reflow(", "), + alloc.keyword("app"), + alloc.reflow(", "), + alloc.keyword("package"), + alloc.reflow(" or "), + alloc.keyword("platform"), + alloc.reflow("."), + ])])); + + Report { + filename, + doc, + title: "MISSING HEADER".to_string(), + severity: Severity::RuntimeError, + } + } + + EHeader::ModuleName(pos) => { + let surroundings = Region::new(start, *pos); + let region = LineColumnRegion::from_pos(lines.convert_pos(*pos)); + + let doc = alloc.stack([ + alloc.reflow(r"I am partway through parsing a header, but got stuck here:"), + alloc.region_with_subregion(lines.convert_region(surroundings), region), + alloc.concat([ + alloc.reflow("I am expecting a module name next, like "), + alloc.parser_suggestion("BigNum"), + alloc.reflow(" or "), + alloc.parser_suggestion("Main"), + alloc.reflow(". Module names must start with an uppercase letter."), + ]), + ]); + + Report { + filename, + doc, + title: "WEIRD MODULE NAME".to_string(), + severity: Severity::RuntimeError, + } + } + + EHeader::InconsistentModuleName(region) => { + let doc = alloc.stack([ + alloc.reflow( + r"This module name does not correspond with the file path it is defined in:", + ), + alloc.region(lines.convert_region(*region)), + alloc.concat([ + alloc.reflow("Module names must correspond with the file paths they are defined in. For example, I expect to see "), + alloc.parser_suggestion("BigNum"), + alloc.reflow(" defined in "), + alloc.parser_suggestion("BigNum.roc"), + alloc.reflow(", or "), + alloc.parser_suggestion("Math.Sin"), + alloc.reflow(" defined in "), + alloc.parser_suggestion("Math/Sin.roc"), + alloc.reflow("."), + ]), + ]); + + Report { + filename, + doc, + title: "WEIRD MODULE NAME".to_string(), + severity: Severity::RuntimeError, + } + } + + EHeader::AppName(_, pos) => { + let surroundings = Region::new(start, *pos); + let region = LineColumnRegion::from_pos(lines.convert_pos(*pos)); + + let doc = alloc.stack([ + alloc.reflow(r"I am partway through parsing a header, but got stuck here:"), + alloc.region_with_subregion(lines.convert_region(surroundings), region), + alloc.concat([ + alloc.reflow("I am expecting an application name next, like "), + alloc.parser_suggestion("app \"main\""), + alloc.reflow(" or "), + alloc.parser_suggestion("app \"editor\""), + alloc.reflow(". App names are surrounded by quotation marks."), + ]), + ]); + + Report { + filename, + doc, + title: "WEIRD APP NAME".to_string(), + severity: Severity::RuntimeError, + } + } + + EHeader::PackageName(_, pos) => { + let surroundings = Region::new(start, *pos); + let region = LineColumnRegion::from_pos(lines.convert_pos(*pos)); + + let doc = alloc.stack([ + alloc.reflow(r"I am partway through parsing a package header, but got stuck here:"), + alloc.region_with_subregion(lines.convert_region(surroundings), region), + alloc.concat([ + alloc.reflow("I am expecting a package name next, like "), + alloc.parser_suggestion("\"roc/core\""), + alloc.reflow(". Package names must be quoted."), + ]), + ]); + + Report { + filename, + doc, + title: "INVALID PACKAGE NAME".to_string(), + severity: Severity::RuntimeError, + } + } + + EHeader::PlatformName(_, pos) => { + let surroundings = Region::new(start, *pos); + let region = LineColumnRegion::from_pos(lines.convert_pos(*pos)); + + let doc = alloc.stack([ + alloc + .reflow(r"I am partway through parsing a platform header, but got stuck here:"), + alloc.region_with_subregion(lines.convert_region(surroundings), region), + alloc.concat([ + alloc.reflow("I am expecting a platform name next, like "), + alloc.parser_suggestion("\"roc/core\""), + alloc.reflow(". Platform names must be quoted."), + ]), + ]); + + Report { + filename, + doc, + title: "INVALID PLATFORM NAME".to_string(), + severity: Severity::RuntimeError, + } + } + + EHeader::Space(error, pos) => to_space_report(alloc, lines, filename, error, *pos), + EHeader::Generates(_, pos) => { + let surroundings = Region::new(start, *pos); + let region = LineColumnRegion::from_pos(lines.convert_pos(*pos)); + + let doc = alloc.stack([ + alloc.reflow(r"I am partway through parsing a header, but got stuck here:"), + alloc.region_with_subregion(lines.convert_region(surroundings), region), + alloc.concat([ + alloc.reflow("I am expecting a type name next, like "), + alloc.parser_suggestion("Effect"), + alloc.reflow(". Type names must start with an uppercase letter."), + ]), + ]); + + Report { + filename, + doc, + title: "WEIRD GENERATED TYPE NAME".to_string(), + severity: Severity::RuntimeError, + } + } + EHeader::GeneratesWith(generates_with, pos) => { + to_generates_with_report(alloc, lines, filename, generates_with, *pos) + } + } +} + +fn to_generates_with_report<'a>( + alloc: &'a RocDocAllocator<'a>, + lines: &LineInfo, + filename: PathBuf, + parse_problem: &roc_parse::parser::EGeneratesWith, + start: Position, +) -> Report<'a> { + use roc_parse::parser::EGeneratesWith; + + match *parse_problem { + EGeneratesWith::ListEnd(pos) | // TODO: give this its own error message + EGeneratesWith::Identifier(pos) => { + let surroundings = Region::new(start, pos); + let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); + + let doc = alloc.stack([ + alloc + .reflow(r"I am partway through parsing a provides list, but I got stuck here:"), + alloc.region_with_subregion(lines.convert_region(surroundings), region), + alloc.concat([alloc.reflow( + "I was expecting a type name, value name or function name next, like", + )]), + alloc + .parser_suggestion("provides [Animal, default, tame]") + .indent(4), + ]); + + Report { + filename, + doc, + title: "WEIRD GENERATES".to_string(), + severity: Severity::RuntimeError, + } + } + + EGeneratesWith::With(pos) => { + let surroundings = Region::new(start, pos); + let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); + + let doc = alloc.stack([ + alloc.reflow(r"I am partway through parsing a header, but I got stuck here:"), + alloc.region_with_subregion(lines.convert_region(surroundings), region), + alloc.concat([ + alloc.reflow("I am expecting the "), + alloc.keyword("with"), + alloc.reflow(" keyword next, like"), + ]), + alloc + .parser_suggestion("with [after, map]") + .indent(4), + ]); + + Report { + filename, + doc, + title: "WEIRD GENERATES".to_string(), + severity: Severity::RuntimeError, + } + } + + EGeneratesWith::Space(error, pos) => to_space_report(alloc, lines, filename, &error, pos), + + _ => todo!("unhandled parse error {:?}", parse_problem), + } +} + +fn to_provides_report<'a>( + alloc: &'a RocDocAllocator<'a>, + lines: &LineInfo, + filename: PathBuf, + parse_problem: &roc_parse::parser::EProvides, + start: Position, +) -> Report<'a> { + use roc_parse::parser::EProvides; + + match *parse_problem { + EProvides::ListEnd(pos) | // TODO: give this its own error message + EProvides::Identifier(pos) => { + let surroundings = Region::new(start, pos); + let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); + + let doc = alloc.stack([ + alloc + .reflow(r"I am partway through parsing a provides list, but I got stuck here:"), + alloc.region_with_subregion(lines.convert_region(surroundings), region), + alloc.concat([alloc.reflow( + "I was expecting a type name, value name or function name next, like", + )]), + alloc + .parser_suggestion("provides [Animal, default, tame]") + .indent(4), + ]); + + Report { + filename, + doc, + title: "WEIRD PROVIDES".to_string(), + severity: Severity::RuntimeError, + } + } + + EProvides::Provides(pos) | EProvides::IndentProvides(pos) => { + let surroundings = Region::new(start, pos); + let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); + + let doc = alloc.stack([ + alloc.reflow(r"I am partway through parsing a header, but I got stuck here:"), + alloc.region_with_subregion(lines.convert_region(surroundings), region), + alloc.concat([ + alloc.reflow("I am expecting the "), + alloc.keyword("provides"), + alloc.reflow(" keyword next, like"), + ]), + alloc + .parser_suggestion("provides [Animal, default, tame]") + .indent(4), + ]); + + Report { + filename, + doc, + title: "WEIRD PROVIDES".to_string(), + severity: Severity::RuntimeError, + } + } + + EProvides::Space(error, pos) => to_space_report(alloc, lines, filename, &error, pos), + + EProvides::IndentTo(pos) => { + let surroundings = Region::new(start, pos); + let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); + + let doc = alloc.stack([ + alloc.reflow(r"I am partway through parsing a header, but I got stuck here:"), + alloc.region_with_subregion(lines.convert_region(surroundings), region), + alloc.concat([ + alloc.reflow("I am expecting the "), + alloc.keyword("to"), + alloc.reflow(" keyword next, like:"), + ]), + alloc + .parser_suggestion("to pf") + .indent(4), + ]); + + Report { + filename, + doc, + title: "WEIRD PROVIDES".to_string(), + severity: Severity::RuntimeError, + } + } + + EProvides::IndentListStart(pos) => { + let surroundings = Region::new(start, pos); + let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); + + let doc = alloc.stack([ + alloc.reflow(r"I am partway through parsing a header, but I got stuck here:"), + alloc.region_with_subregion(lines.convert_region(surroundings), region), + alloc.reflow("I am expecting the platform name next, like:"), + alloc + .parser_suggestion("to pf") + .indent(4), + ]); + + Report { + filename, + doc, + title: "WEIRD PROVIDES".to_string(), + severity: Severity::RuntimeError, + } + } + + _ => todo!("unhandled parse error {:?}", parse_problem), + } +} + +fn to_exposes_report<'a>( + alloc: &'a RocDocAllocator<'a>, + lines: &LineInfo, + filename: PathBuf, + parse_problem: &roc_parse::parser::EExposes, + start: Position, +) -> Report<'a> { + use roc_parse::parser::EExposes; + + match *parse_problem { + EExposes::ListEnd(pos) | // TODO: give this its own error message + EExposes::Identifier(pos) => { + let surroundings = Region::new(start, pos); + let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); + + let doc = alloc.stack([ + alloc.reflow(r"I am partway through parsing an `exposes` list, but I got stuck here:"), + alloc.region_with_subregion(lines.convert_region(surroundings), region), + alloc.concat([alloc.reflow( + "I was expecting a type name, value name or function name next, like", + )]), + alloc + .parser_suggestion("exposes [Animal, default, tame]") + .indent(4), + ]); + + Report { + filename, + doc, + title: "WEIRD EXPOSES".to_string(), + severity: Severity::RuntimeError, + } + } + + EExposes::Exposes(pos) => { + let surroundings = Region::new(start, pos); + let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); + + let doc = alloc.stack([ + alloc.reflow(r"I am partway through parsing a header, but I got stuck here:"), + alloc.region_with_subregion(lines.convert_region(surroundings), region), + alloc.concat([ + alloc.reflow("I am expecting the "), + alloc.keyword("exposes"), + alloc.reflow(" keyword next, like"), + ]), + alloc + .parser_suggestion("exposes [Animal, default, tame]") + .indent(4), + ]); + + Report { + filename, + doc, + title: "WEIRD EXPOSES".to_string(), + severity: Severity::RuntimeError, + } + } + + EExposes::Space(error, pos) => to_space_report(alloc, lines, filename, &error, pos), + + _ => todo!("unhandled `exposes` parsing error {:?}", parse_problem), + } +} + +fn to_imports_report<'a>( + alloc: &'a RocDocAllocator<'a>, + lines: &LineInfo, + filename: PathBuf, + parse_problem: &roc_parse::parser::EImports, + start: Position, +) -> Report<'a> { + use roc_parse::parser::EImports; + + match *parse_problem { + EImports::Identifier(pos) => { + let surroundings = Region::new(start, pos); + let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); + + let doc = alloc.stack([ + alloc.reflow(r"I am partway through parsing a imports list, but I got stuck here:"), + alloc.region_with_subregion(lines.convert_region(surroundings), region), + alloc.concat([alloc.reflow( + "I was expecting a type name, value name or function name next, like ", + )]), + alloc + .parser_suggestion("imports [Animal, default, tame]") + .indent(4), + ]); + + Report { + filename, + doc, + title: "WEIRD IMPORTS".to_string(), + severity: Severity::RuntimeError, + } + } + + EImports::Imports(pos) | EImports::IndentImports(pos) => { + let surroundings = Region::new(start, pos); + let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); + + let doc = alloc.stack([ + alloc.reflow(r"I am partway through parsing a header, but I got stuck here:"), + alloc.region_with_subregion(lines.convert_region(surroundings), region), + alloc.concat([ + alloc.reflow("I am expecting the "), + alloc.keyword("imports"), + alloc.reflow(" keyword next, like"), + ]), + alloc + .parser_suggestion("imports [Animal, default, tame]") + .indent(4), + ]); + + Report { + filename, + doc, + title: "WEIRD IMPORTS".to_string(), + severity: Severity::RuntimeError, + } + } + + EImports::Space(error, pos) => to_space_report(alloc, lines, filename, &error, pos), + + EImports::ModuleName(pos) => { + let surroundings = Region::new(start, pos); + let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); + + let doc = alloc.stack([ + alloc.reflow(r"I am partway through parsing a header, but got stuck here:"), + alloc.region_with_subregion(lines.convert_region(surroundings), region), + alloc.concat([ + alloc.reflow("I am expecting a module name next, like "), + alloc.parser_suggestion("BigNum"), + alloc.reflow(" or "), + alloc.parser_suggestion("Main"), + alloc.reflow(". Module names must start with an uppercase letter."), + ]), + ]); + + Report { + filename, + doc, + title: "WEIRD MODULE NAME".to_string(), + severity: Severity::RuntimeError, + } + } + + EImports::ListEnd(pos) => { + let surroundings = Region::new(start, pos); + let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); + + let doc = alloc.stack([ + alloc.reflow(r"I am partway through parsing a imports list, but I got stuck here:"), + alloc.region_with_subregion(lines.convert_region(surroundings), region), + alloc.concat([alloc.reflow("I am expecting a comma or end of list, like")]), + alloc.parser_suggestion("imports [Shape, Vector]").indent(4), + ]); + + Report { + filename, + doc, + title: "WEIRD IMPORTS".to_string(), + severity: Severity::RuntimeError, + } + } + + _ => todo!("unhandled parse error {:?}", parse_problem), + } +} + +fn to_requires_report<'a>( + alloc: &'a RocDocAllocator<'a>, + lines: &LineInfo, + filename: PathBuf, + parse_problem: &roc_parse::parser::ERequires<'a>, + start: Position, +) -> Report<'a> { + use roc_parse::parser::ERequires; + + match *parse_problem { + ERequires::Requires(pos) => { + let surroundings = Region::new(start, pos); + let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); + + let doc = alloc.stack([ + alloc.reflow(r"I am partway through parsing a header, but I got stuck here:"), + alloc.region_with_subregion(lines.convert_region(surroundings), region), + alloc.concat([ + alloc.reflow("I am expecting the "), + alloc.keyword("requires"), + alloc.reflow(" keyword next, like"), + ]), + alloc + .parser_suggestion("requires { main : Task I64 Str }") + .indent(4), + ]); + + Report { + filename, + doc, + title: "MISSING REQUIRES".to_string(), + severity: Severity::RuntimeError, + } + } + + ERequires::Space(error, pos) => to_space_report(alloc, lines, filename, &error, pos), + + ERequires::ListStart(pos) => { + let surroundings = Region::new(start, pos); + let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); + + let doc = alloc.stack([ + alloc.reflow(r"I am partway through parsing a header, but I got stuck here:"), + alloc.region_with_subregion(lines.convert_region(surroundings), region), + alloc.concat([ + alloc.reflow("I am expecting the "), + alloc.keyword("requires"), + alloc.reflow(" keyword next, like"), + ]), + alloc + .parser_suggestion("requires { main : Task I64 Str }") + .indent(4), + ]); + + Report { + filename, + doc, + title: "MISSING REQUIRES".to_string(), + severity: Severity::RuntimeError, + } + } + + ERequires::Rigid(pos) => { + let surroundings = Region::new(start, pos); + let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); + + let doc = alloc.stack([ + alloc.reflow(r"I am partway through parsing a header, but I got stuck here:"), + alloc.region_with_subregion(lines.convert_region(surroundings), region), + alloc.concat([ + alloc.reflow("I am expecting a list of rigids like "), + alloc.keyword("{}"), + alloc.reflow(" or "), + alloc.keyword("{model=>Model}"), + alloc.reflow(" next. A full "), + alloc.keyword("requires"), + alloc.reflow(" definition looks like"), + ]), + alloc + .parser_suggestion("requires {model=>Model, msg=>Msg} {main : Effect {}}") + .indent(4), + ]); + + Report { + filename, + doc, + title: "BAD REQUIRES RIGIDS".to_string(), + severity: Severity::RuntimeError, + } + } + + ERequires::ListEnd(pos) | ERequires::Open(pos) => { + let surroundings = Region::new(start, pos); + let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); + + let doc = alloc.stack([ + alloc.reflow(r"I am partway through parsing a header, but I got stuck here:"), + alloc.region_with_subregion(lines.convert_region(surroundings), region), + alloc.concat([ + alloc.reflow("I am expecting a list of type names like "), + alloc.keyword("{}"), + alloc.reflow(" or "), + alloc.keyword("{ Model }"), + alloc.reflow(" next. A full "), + alloc.keyword("requires"), + alloc.reflow(" definition looks like"), + ]), + alloc + .parser_suggestion("requires { Model, Msg } {main : Effect {}}") + .indent(4), + ]); + + Report { + filename, + doc, + title: "BAD REQUIRES".to_string(), + severity: Severity::RuntimeError, + } + } + + _ => todo!("unhandled parse error {:?}", parse_problem), + } +} + +fn to_packages_report<'a>( + alloc: &'a RocDocAllocator<'a>, + lines: &LineInfo, + filename: PathBuf, + parse_problem: &roc_parse::parser::EPackages, + start: Position, +) -> Report<'a> { + use roc_parse::parser::EPackages; + + match *parse_problem { + EPackages::Packages(pos) => { + let surroundings = Region::new(start, pos); + let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); + + let doc = alloc.stack([ + alloc.reflow(r"I am partway through parsing a header, but I got stuck here:"), + alloc.region_with_subregion(lines.convert_region(surroundings), region), + alloc.concat([ + alloc.reflow("I am expecting the "), + alloc.keyword("packages"), + alloc.reflow(" keyword next, like"), + ]), + alloc.parser_suggestion("packages {}").indent(4), + ]); + + Report { + filename, + doc, + title: "MISSING PACKAGES".to_string(), + severity: Severity::RuntimeError, + } + } + + EPackages::Space(error, pos) => to_space_report(alloc, lines, filename, &error, pos), + + _ => todo!("unhandled parse error {:?}", parse_problem), + } +} + +fn to_space_report<'a>( + alloc: &'a RocDocAllocator<'a>, + lines: &LineInfo, + filename: PathBuf, + parse_problem: &roc_parse::parser::BadInputError, + pos: Position, +) -> Report<'a> { + use roc_parse::parser::BadInputError; + + match parse_problem { + BadInputError::HasTab => { + let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); + + let doc = alloc.stack([ + alloc.reflow("I encountered a tab character:"), + alloc.region(region), + alloc.reflow("Tab characters are not allowed, use spaces instead."), + ]); + + Report { + filename, + doc, + title: "TAB CHARACTER".to_string(), + severity: Severity::RuntimeError, + } + } + + BadInputError::HasAsciiControl => { + let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); + + let doc = alloc.stack([ + alloc.reflow("I encountered an ASCII control character:"), + alloc.region(region), + alloc.reflow("ASCII control characters are not allowed."), + ]); + + Report { + filename, + doc, + title: "ASCII CONTROL CHARACTER".to_string(), + severity: Severity::RuntimeError, + } + } + + BadInputError::HasMisplacedCarriageReturn => { + let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); + + let doc = alloc.stack([ + alloc.reflow(r"I encountered a stray carriage return (\r):"), + alloc.region(region), + alloc.reflow(r"A carriage return (\r) has to be followed by a newline (\n)."), + ]); + + Report { + filename, + doc, + title: "MISPLACED CARRIAGE RETURN".to_string(), + severity: Severity::RuntimeError, + } + } + + _ => todo!("unhandled type parse error: {:?}", &parse_problem), + } +} + +fn to_ability_def_report<'a>( + alloc: &'a RocDocAllocator<'a>, + lines: &LineInfo, + filename: PathBuf, + problem: &roc_parse::parser::EAbility<'a>, + start: Position, +) -> Report<'a> { + use roc_parse::parser::EAbility; + + match problem { + EAbility::Space(error, pos) => to_space_report(alloc, lines, filename, error, *pos), + EAbility::Type(tipe, pos) => to_type_report(alloc, lines, filename, tipe, *pos), + EAbility::DemandAlignment(over_under_indent, pos) => { + let over_under_msg = if *over_under_indent > 0 { + alloc.reflow("indented too much") + } else { + alloc.reflow("not indented enough") + }; + + let msg = alloc.concat([ + alloc.reflow("I suspect this line is "), + over_under_msg, + alloc.reflow(" (by "), + alloc.string(over_under_indent.abs().to_string()), + alloc.reflow(" spaces)"), + ]); + + to_unfinished_ability_report(alloc, lines, filename, *pos, start, msg) + } + EAbility::DemandName(pos) => to_unfinished_ability_report( + alloc, + lines, + filename, + *pos, + start, + alloc.reflow("I was expecting to see a value signature next."), + ), + EAbility::DemandColon(pos) => to_unfinished_ability_report( + alloc, + lines, + filename, + *pos, + start, + alloc.concat([ + alloc.reflow("I was expecting to see a "), + alloc.parser_suggestion(":"), + alloc.reflow(" annotating the signature of this value next."), + ]), + ), + } +} + +fn to_unfinished_ability_report<'a>( + alloc: &'a RocDocAllocator<'a>, + lines: &LineInfo, + filename: PathBuf, + pos: Position, + start: Position, + message: RocDocBuilder<'a>, +) -> Report<'a> { + let surroundings = Region::new(start, pos); + let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); + + let doc = alloc.stack([ + alloc.reflow(r"I was partway through parsing an ability definition, but I got stuck here:"), + alloc.region_with_subregion(lines.convert_region(surroundings), region), + message, + ]); + + Report { + filename, + doc, + title: "UNFINISHED ABILITY".to_string(), + severity: Severity::RuntimeError, + } +} + +#[derive(Debug)] +enum Next<'a> { + Keyword(&'a str), + // Operator(&'a str), + Close(&'a str, char), + Token(&'a str), + Other(Option), +} + +fn what_is_next<'a>(source_lines: &'a [&'a str], pos: LineColumn) -> Next<'a> { + let row_index = pos.line as usize; + let col_index = pos.column as usize; + match source_lines.get(row_index) { + None => Next::Other(None), + Some(line) => { + let chars = &line[col_index..]; + let mut it = chars.chars(); + + match roc_parse::keyword::KEYWORDS + .iter() + .find(|keyword| starts_with_keyword(chars, keyword)) + { + Some(keyword) => Next::Keyword(keyword), + None => match it.next() { + None => Next::Other(None), + Some(c) => match c { + ')' => Next::Close("parenthesis", ')'), + ']' => Next::Close("square bracket", ']'), + '}' => Next::Close("curly brace", '}'), + '-' if it.next() == Some('>') => Next::Token("->"), + '=' if it.next() == Some('>') => Next::Token("=>"), + // _ if is_symbol(c) => todo!("it's an operator"), + _ => Next::Other(Some(c)), + }, + }, + } + } + } +} + +pub fn starts_with_keyword(rest_of_line: &str, keyword: &str) -> bool { + if let Some(stripped) = rest_of_line.strip_prefix(keyword) { + match stripped.chars().next() { + None => true, + Some(c) => !c.is_alphanumeric(), + } + } else { + false + } +} + +fn next_line_starts_with_close_curly(source_lines: &[&str], pos: LineColumn) -> Option { + next_line_starts_with_char(source_lines, pos, '}') +} + +fn next_line_starts_with_close_parenthesis( + source_lines: &[&str], + pos: LineColumn, +) -> Option { + next_line_starts_with_char(source_lines, pos, ')') +} + +fn next_line_starts_with_char( + source_lines: &[&str], + pos: LineColumn, + character: char, +) -> Option { + match source_lines.get(pos.line as usize + 1) { + None => None, + + Some(line) => { + let spaces_dropped = line.trim_start_matches(' '); + match spaces_dropped.chars().next() { + Some(c) if c == character => Some(LineColumn { + line: pos.line + 1, + column: (line.len() - spaces_dropped.len()) as u32, + }), + _ => None, + } + } + } +} + +fn to_keyword_region(pos: LineColumn, keyword: &str) -> LineColumnRegion { + LineColumnRegion { + start: pos, + end: pos.bump_column(keyword.len() as u32), + } +} diff --git a/crates/reporting/src/error/type.rs b/crates/reporting/src/error/type.rs new file mode 100644 index 0000000000..e4b2ca711e --- /dev/null +++ b/crates/reporting/src/error/type.rs @@ -0,0 +1,5205 @@ +#![allow(clippy::too_many_arguments)] + +use crate::error::canonicalize::{to_circular_def_doc, CIRCULAR_DEF}; +use crate::report::{Annotation, Report, RocDocAllocator, RocDocBuilder}; +use itertools::EitherOrBoth; +use itertools::Itertools; +use roc_can::expected::{Expected, PExpected}; +use roc_collections::all::{HumanIndex, MutSet, SendMap}; +use roc_collections::VecMap; +use roc_error_macros::internal_error; +use roc_exhaustive::{CtorName, ListArity}; +use roc_module::called_via::{BinOp, CalledVia}; +use roc_module::ident::{IdentStr, Lowercase, TagName}; +use roc_module::symbol::Symbol; +use roc_problem::Severity; +use roc_region::all::{LineInfo, Region}; +use roc_solve_problem::{ + NotDerivableContext, NotDerivableDecode, NotDerivableEncode, NotDerivableEq, TypeError, + UnderivableReason, Unfulfilled, +}; +use roc_std::RocDec; +use roc_types::pretty_print::{Parens, WILDCARD}; +use roc_types::types::{ + AbilitySet, AliasKind, Category, ErrorType, IndexOrField, PatternCategory, Polarity, Reason, + RecordField, TypeExt, +}; +use std::path::PathBuf; +use ven_pretty::{text, DocAllocator}; + +const ADD_ANNOTATIONS: &str = r#"Can more type annotations be added? Type annotations always help me give more specific messages, and I think they could help a lot in this case"#; + +const OPAQUE_NUM_SYMBOLS: &[Symbol] = &[ + Symbol::NUM_NUM, + Symbol::NUM_INTEGER, + Symbol::NUM_FLOATINGPOINT, +]; + +pub fn type_problem<'b>( + alloc: &'b RocDocAllocator<'b>, + lines: &LineInfo, + filename: PathBuf, + problem: TypeError, +) -> Option> { + use TypeError::*; + + let severity = problem.severity(); + + let report = + move |title: String, doc: RocDocBuilder<'b>, filename: PathBuf| -> Option> { + Some(Report { + title, + filename, + doc, + severity, + }) + }; + + match problem { + BadExpr(region, category, found, expected) => Some(to_expr_report( + alloc, lines, filename, severity, region, category, found, expected, + )), + BadPattern(region, category, found, expected) => Some(to_pattern_report( + alloc, lines, filename, severity, region, category, found, expected, + )), + CircularType(region, symbol, overall_type) => Some(to_circular_report( + alloc, + lines, + filename, + severity, + region, + symbol, + overall_type, + )), + UnexposedLookup(_, symbol) => { + let title = "UNRECOGNIZED NAME".to_string(); + let doc = alloc + .stack(vec![alloc + .reflow("The ") + .append(alloc.module(symbol.module_id())) + .append(alloc.reflow(" module does not expose anything by the name ")) + .append(alloc.symbol_unqualified(symbol))]) + .append(alloc.reflow(".")); + + report(title, doc, filename) + } + UnfulfilledAbility(incomplete) => { + let title = "INCOMPLETE ABILITY IMPLEMENTATION".to_string(); + + let doc = report_unfulfilled_ability(alloc, lines, incomplete); + + report(title, doc, filename) + } + BadExprMissingAbility(region, _category, _found, incomplete) => { + if region == roc_can::DERIVED_REGION { + return None; + } + + let incomplete = incomplete + .into_iter() + .map(|unfulfilled| report_unfulfilled_ability(alloc, lines, unfulfilled)); + let note = alloc.stack(incomplete); + let snippet = alloc.region(lines.convert_region(region)); + let stack = [ + alloc.text( + "This expression has a type that does not implement the abilities it's expected to:", + ), + snippet, + note + ]; + + let report = Report { + title: "TYPE MISMATCH".to_string(), + filename, + doc: alloc.stack(stack), + severity, + }; + Some(report) + } + BadPatternMissingAbility(region, _category, _found, incomplete) => { + let incomplete = incomplete + .into_iter() + .map(|unfulfilled| report_unfulfilled_ability(alloc, lines, unfulfilled)); + let note = alloc.stack(incomplete); + let snippet = alloc.region(lines.convert_region(region)); + let stack = [ + alloc.text( + "This expression has a type does not implement the abilities it's expected to:", + ), + snippet, + note, + ]; + + let report = Report { + title: "TYPE MISMATCH".to_string(), + filename, + doc: alloc.stack(stack), + severity, + }; + Some(report) + } + Exhaustive(problem) => Some(exhaustive_problem(alloc, lines, filename, problem)), + CircularDef(entries) => { + let doc = to_circular_def_doc(alloc, lines, &entries); + let title = CIRCULAR_DEF.to_string(); + + Some(Report { + title, + filename, + doc, + severity, + }) + } + StructuralSpecialization { + region, + typ, + ability, + member, + } => { + let stack = [ + alloc.concat([ + alloc.reflow("This specialization of "), + alloc.symbol_unqualified(member), + alloc.reflow(" is for a non-opaque type:"), + ]), + alloc.region(lines.convert_region(region)), + alloc.reflow("It is specialized for"), + alloc.type_block(error_type_to_doc(alloc, typ)), + alloc.reflow("but structural types can never specialize abilities!"), + alloc.note("").append(alloc.concat([ + alloc.symbol_unqualified(member), + alloc.reflow(" is a member of "), + alloc.symbol_qualified(ability), + ])), + ]; + + Some(Report { + title: "ILLEGAL SPECIALIZATION".to_string(), + filename, + doc: alloc.stack(stack), + severity, + }) + } + WrongSpecialization { + region, + ability_member, + expected_opaque, + found_opaque, + } => { + let stack = [ + alloc.concat([ + alloc.reflow("This specialization of "), + alloc.symbol_unqualified(ability_member), + alloc.reflow(" is not for the expected type:"), + ]), + alloc.region(lines.convert_region(region)), + alloc.concat([ + alloc.reflow("It was previously claimed to be a specialization for "), + alloc.symbol_unqualified(expected_opaque), + alloc.reflow(", but was determined to actually specialize "), + alloc.symbol_unqualified(found_opaque), + alloc.reflow("!"), + ]), + ]; + + Some(Report { + title: "WRONG SPECIALIZATION TYPE".to_string(), + filename, + doc: alloc.stack(stack), + severity, + }) + } + IngestedFileBadUtf8(file_path, utf8_err) => { + let stack = [ + alloc.concat([ + alloc.reflow("Failed to load "), + text!(alloc, "{:?}", file_path), + alloc.reflow(" as Str:"), + ]), + text!(alloc, "{}", utf8_err), + ]; + Some(Report { + title: "INVALID UTF-8".to_string(), + filename, + doc: alloc.stack(stack), + severity, + }) + } + IngestedFileUnsupportedType(file_path, typ) => { + let stack = [ + alloc.concat([ + text!(alloc, "{:?}", file_path), + alloc.reflow(" is annotated to be a "), + alloc.inline_type_block(error_type_to_doc(alloc, typ)), + alloc.reflow("."), + ]), + alloc.concat([ + alloc.reflow("Ingested files can only be of type "), + alloc.type_str("List U8"), + alloc.reflow(" or "), + alloc.type_str("Str"), + alloc.reflow("."), + ]), + ]; + Some(Report { + title: "INVALID TYPE FOR INGESTED FILE".to_string(), + filename, + doc: alloc.stack(stack), + severity, + }) + } + } +} + +fn report_unfulfilled_ability<'a>( + alloc: &'a RocDocAllocator<'a>, + lines: &LineInfo, + unfulfilled: Unfulfilled, +) -> RocDocBuilder<'a> { + match unfulfilled { + Unfulfilled::OpaqueDoesNotImplement { typ, ability } => { + let stack = vec![alloc.concat([ + alloc.reflow("The type "), + alloc.symbol_unqualified(typ), + alloc.reflow(" does not fully implement the ability "), + alloc.symbol_unqualified(ability), + alloc.reflow("."), + ])]; + + alloc.stack(stack) + } + Unfulfilled::AdhocUnderivable { + typ, + ability, + reason, + } => { + let reason = report_underivable_reason(alloc, reason, ability, &typ); + let stack = [ + alloc.concat([ + alloc.reflow("I can't generate an implementation of the "), + alloc.symbol_foreign_qualified(ability), + alloc.reflow(" ability for"), + ]), + alloc.type_block(error_type_to_doc(alloc, typ)), + ] + .into_iter() + .chain(reason); + + alloc.stack(stack) + } + Unfulfilled::OpaqueUnderivable { + typ, + ability, + opaque, + derive_region, + reason, + } => { + let reason = report_underivable_reason(alloc, reason, ability, &typ); + let stack = [ + alloc.concat([ + alloc.reflow("I can't derive an implementation of the "), + alloc.symbol_foreign_qualified(ability), + alloc.reflow(" ability for "), + alloc.symbol_foreign_qualified(opaque), + alloc.reflow(":"), + ]), + alloc.region(lines.convert_region(derive_region)), + ] + .into_iter() + .chain(reason) + .chain(std::iter::once(alloc.tip().append(alloc.concat([ + alloc.reflow("You can define a custom implementation of "), + alloc.symbol_unqualified(ability), + alloc.reflow(" for "), + alloc.symbol_unqualified(opaque), + alloc.reflow("."), + ])))); + + alloc.stack(stack) + } + } +} + +fn report_underivable_reason<'a>( + alloc: &'a RocDocAllocator<'a>, + reason: UnderivableReason, + ability: Symbol, + typ: &ErrorType, +) -> Option> { + match reason { + UnderivableReason::NotABuiltin => { + Some(alloc.reflow("Only builtin abilities can have generated implementations!")) + } + UnderivableReason::SurfaceNotDerivable(context) => { + underivable_hint(alloc, ability, context, typ) + } + UnderivableReason::NestedNotDerivable(nested_typ, context) => { + let hint = underivable_hint(alloc, ability, context, &nested_typ); + let reason = alloc.stack( + [ + alloc.reflow("In particular, an implementation for"), + alloc.type_block(error_type_to_doc(alloc, nested_typ)), + alloc.reflow("cannot be generated."), + ] + .into_iter() + .chain(hint), + ); + Some(reason) + } + } +} + +fn underivable_hint<'b>( + alloc: &'b RocDocAllocator<'b>, + ability: Symbol, + context: NotDerivableContext, + typ: &ErrorType, +) -> Option> { + match context { + NotDerivableContext::NoContext => None, + NotDerivableContext::Function => Some(alloc.note("").append(alloc.concat([ + alloc.symbol_unqualified(ability), + alloc.reflow(" cannot be generated for functions."), + ]))), + NotDerivableContext::Opaque(symbol) => Some(alloc.tip().append(alloc.concat([ + alloc.symbol_unqualified(symbol), + alloc.reflow(" does not implement "), + alloc.symbol_unqualified(ability), + alloc.reflow("."), + if symbol.module_id() == alloc.home { + alloc.concat([ + alloc.reflow(" Consider adding a custom implementation"), + if ability.is_builtin() { + alloc.concat([ + alloc.reflow(" or "), + alloc.inline_type_block(alloc.concat([ + alloc.keyword(roc_parse::keyword::IMPLEMENTS), + alloc.space(), + alloc.symbol_qualified(ability), + ])), + alloc.reflow(" to the definition of "), + alloc.symbol_unqualified(symbol), + ]) + } else { + alloc.nil() + }, + alloc.reflow("."), + ]) + } else { + alloc.nil() + }, + ]))), + NotDerivableContext::UnboundVar => { + let v = match typ { + ErrorType::FlexVar(v) => v, + ErrorType::RigidVar(v) => v, + _ => internal_error!("unbound variable context only applicable for variables"), + }; + + Some(alloc.tip().append(alloc.concat([ + alloc.reflow("This type variable is not bound to "), + alloc.symbol_unqualified(ability), + alloc.reflow(". Consider adding an "), + alloc.keyword(roc_parse::keyword::IMPLEMENTS), + alloc.reflow(" clause to bind the type variable, like "), + alloc.inline_type_block(alloc.concat([ + alloc.keyword(roc_parse::keyword::WHERE), + alloc.space(), + alloc.type_variable(v.clone()), + alloc.space(), + alloc.keyword(roc_parse::keyword::IMPLEMENTS), + alloc.space(), + alloc.symbol_qualified(ability), + ])), + ]))) + } + NotDerivableContext::Encode(reason) => match reason { + NotDerivableEncode::Nat => { + Some(alloc.note("").append(alloc.concat([ + alloc.reflow("Encoding a "), + alloc.type_str("Nat"), + alloc.reflow(" is not supported. Consider using a fixed-sized unsigned integer, like a "), + alloc.type_str("U64"), + alloc.reflow(" instead."), + ]))) + } + }, + NotDerivableContext::Decode(reason) => match reason { + NotDerivableDecode::Nat => { + Some(alloc.note("").append(alloc.concat([ + alloc.reflow("Decoding to a "), + alloc.type_str("Nat"), + alloc.reflow(" is not supported. Consider decoding to a fixed-sized unsigned integer, like "), + alloc.type_str("U64"), + alloc.reflow(", then converting to a "), + alloc.type_str("Nat"), + alloc.reflow(" if needed."), + ]))) + } + NotDerivableDecode::OptionalRecordField(field) => { + Some(alloc.note("").append(alloc.concat([ + alloc.reflow("I can't derive decoding for a record with an optional field, which in this case is "), + alloc.record_field(field), + alloc.reflow(". Optional record fields are polymorphic over records that may or may not contain them at compile time, "), + alloc.reflow("but are not a concept that extends to runtime!"), + alloc.hardline(), + alloc.reflow("Maybe you wanted to use a "), + alloc.symbol_unqualified(Symbol::RESULT_RESULT), + alloc.reflow("?"), + ]))) + } + }, + NotDerivableContext::Eq(reason) => match reason { + NotDerivableEq::FloatingPoint => { + Some(alloc.note("").append(alloc.concat([ + alloc.reflow("I can't derive "), + alloc.symbol_qualified(Symbol::BOOL_IS_EQ), + alloc.reflow(" for floating-point types. That's because Roc's floating-point numbers cannot be compared for total equality - in Roc, `NaN` is never comparable to `NaN`."), + alloc.reflow(" If a type doesn't support total equality, it cannot support the "), + alloc.symbol_unqualified(Symbol::BOOL_EQ), + alloc.reflow(" ability!"), + ]))) + } + }, + } +} + +pub fn cyclic_alias<'b>( + alloc: &'b RocDocAllocator<'b>, + lines: &LineInfo, + symbol: Symbol, + region: roc_region::all::Region, + others: Vec, + alias_kind: AliasKind, +) -> (RocDocBuilder<'b>, String) { + let when_is_recursion_legal = + alloc.reflow("Recursion in ") + .append(alloc.reflow(alias_kind.as_str())) + .append(alloc.reflow("es is only allowed if recursion happens behind a tagged union, at least one variant of which is not recursive.")); + + let doc = if others.is_empty() { + alloc.stack([ + alloc + .reflow("The ") + .append(alloc.symbol_unqualified(symbol)) + .append(alloc.reflow(" ")) + .append(alloc.reflow(alias_kind.as_str())) + .append(alloc.reflow(" is self-recursive in an invalid way:")), + alloc.region(lines.convert_region(region)), + when_is_recursion_legal, + ]) + } else { + alloc.stack([ + alloc + .reflow("The ") + .append(alloc.symbol_unqualified(symbol)) + .append(alloc.reflow(" ")) + .append(alloc.reflow(alias_kind.as_str())) + .append(alloc.reflow(" is recursive in an invalid way:")), + alloc.region(lines.convert_region(region)), + alloc + .reflow("The ") + .append(alloc.symbol_unqualified(symbol)) + .append(alloc.reflow(" ")) + .append(alloc.reflow(alias_kind.as_str())) + .append( + alloc.reflow(" depends on itself through the following chain of definitions:"), + ), + crate::report::cycle( + alloc, + 4, + alloc.symbol_unqualified(symbol), + others + .into_iter() + .map(|other| alloc.symbol_unqualified(other)) + .collect::>(), + ), + when_is_recursion_legal, + ]) + }; + + (doc, "CYCLIC ALIAS".to_string()) +} + +fn report_mismatch<'b>( + alloc: &'b RocDocAllocator<'b>, + lines: &LineInfo, + filename: PathBuf, + severity: Severity, + category: &Category, + found: ErrorType, + expected_type: ErrorType, + region: roc_region::all::Region, + opt_highlight: Option, + problem: RocDocBuilder<'b>, + this_is: RocDocBuilder<'b>, + instead_of: RocDocBuilder<'b>, + further_details: Option>, +) -> Report<'b> { + let snippet = if let Some(highlight) = opt_highlight { + alloc.region_with_subregion( + lines.convert_region(highlight), + lines.convert_region(region), + ) + } else { + alloc.region(lines.convert_region(region)) + }; + let lines = vec![ + problem, + snippet, + type_comparison( + alloc, + found, + expected_type, + ExpectationContext::Arbitrary, + add_category(alloc, this_is, category), + instead_of, + further_details, + ), + ]; + + Report { + title: "TYPE MISMATCH".to_string(), + filename, + doc: alloc.stack(lines), + severity, + } +} + +fn report_bad_type<'b>( + alloc: &'b RocDocAllocator<'b>, + lines: &LineInfo, + filename: PathBuf, + severity: Severity, + category: &Category, + found: ErrorType, + expected_type: ErrorType, + region: roc_region::all::Region, + opt_highlight: Option, + problem: RocDocBuilder<'b>, + this_is: RocDocBuilder<'b>, + further_details: RocDocBuilder<'b>, +) -> Report<'b> { + let snippet = if let Some(highlight) = opt_highlight { + alloc.region_with_subregion( + lines.convert_region(highlight), + lines.convert_region(region), + ) + } else { + alloc.region(lines.convert_region(region)) + }; + let lines = vec![ + problem, + snippet, + lone_type( + alloc, + found, + expected_type, + ExpectationContext::Arbitrary, + add_category(alloc, this_is, category), + further_details, + ), + ]; + + Report { + title: "TYPE MISMATCH".to_string(), + filename, + doc: alloc.stack(lines), + severity, + } +} + +fn pattern_to_doc<'b>( + alloc: &'b RocDocAllocator<'b>, + pattern: &roc_can::pattern::Pattern, +) -> Option> { + use roc_can::pattern::Pattern::*; + + match pattern { + Identifier(symbol) => Some(alloc.symbol_unqualified(*symbol)), + _ => None, + } +} + +fn lowercase_first(s: &str) -> String { + let mut chars = s.chars(); + match chars.next() { + None => Default::default(), + Some(c) => c.to_lowercase().chain(chars).collect(), + } +} + +fn to_expr_report<'b>( + alloc: &'b RocDocAllocator<'b>, + lines: &LineInfo, + filename: PathBuf, + severity: Severity, + expr_region: roc_region::all::Region, + category: Category, + found: ErrorType, + expected: Expected, +) -> Report<'b> { + match expected { + Expected::NoExpectation(expected_type) => { + // If it looks like a record field typo, early return with a special report for that. + if let ErrorType::Record(expected_fields, _) = + expected_type.clone().unwrap_structural_alias() + { + if let ErrorType::Record(found_fields, found_ext) = + found.clone().unwrap_structural_alias() + { + let expected_set: MutSet<_> = expected_fields.keys().cloned().collect(); + let found_set: MutSet<_> = found_fields.keys().cloned().collect(); + let mut diff = expected_set.difference(&found_set); + + if let Some(field) = diff.next() { + let opt_sym = match category { + Category::Lookup(name) => Some(name), + _ => None, + }; + return report_record_field_typo( + alloc, + lines, + filename, + severity, + opt_sym, + ".", + field, + "", + expr_region, + found_fields, + found_ext, + ); + } + } + }; + + let comparison = type_comparison( + alloc, + found, + expected_type, + ExpectationContext::Arbitrary, + add_category(alloc, alloc.text("It is"), &category), + alloc.text("But you are trying to use it as:"), + None, + ); + + Report { + filename, + title: "TYPE MISMATCH".to_string(), + doc: alloc.stack([ + alloc.text("This expression is used in an unexpected way:"), + alloc.region(lines.convert_region(expr_region)), + comparison, + ]), + severity, + } + } + Expected::FromAnnotation(name, _arity, annotation_source, expected_type) => { + use roc_types::types::AnnotationSource::*; + + let (the_name_text, on_name_text) = match pattern_to_doc(alloc, &name.value) { + Some(doc) => ( + alloc.concat([alloc.reflow("the "), doc.clone()]), + alloc.concat([alloc.reflow(" on "), doc]), + ), + None => (alloc.text("this"), alloc.nil()), + }; + + let ann_region = annotation_source.region(); + + let thing = match annotation_source { + TypedIfBranch { + index, + num_branches, + .. + } if num_branches == 2 => alloc.concat([ + alloc.keyword(if index == HumanIndex::FIRST { + "then" + } else { + "else" + }), + alloc.reflow(" branch of this "), + alloc.keyword("if"), + alloc.text(" expression:"), + ]), + TypedIfBranch { index, .. } => alloc.concat([ + alloc.string(index.ordinal()), + alloc.reflow(" branch of this "), + alloc.keyword("if"), + alloc.text(" expression:"), + ]), + TypedWhenBranch { index, .. } => alloc.concat([ + alloc.string(index.ordinal()), + alloc.reflow(" branch of this "), + alloc.keyword("when"), + alloc.text(" expression:"), + ]), + TypedBody { .. } => alloc.concat([ + alloc.text("body of "), + the_name_text, + alloc.text(" definition:"), + ]), + RequiredSymbol { .. } => alloc.concat([ + alloc.text("type annotation of "), + the_name_text, + alloc.text(" required symbol:"), + ]), + }; + + let it_is = match annotation_source { + TypedIfBranch { .. } => "This branch is".to_string(), + TypedWhenBranch { index, .. } => format!("The {} branch is", index.ordinal()), + TypedBody { .. } => "The body is".into(), + RequiredSymbol { .. } => "The provided type is".into(), + }; + + let expectation_context = ExpectationContext::Annotation { + on: on_name_text.clone(), + }; + + let comparison = if diff_is_wildcard_comparison( + alloc, + found.clone(), + expected_type.clone(), + ) { + let it_is = lowercase_first(&it_is); + let (it, _) = format_category(alloc, alloc.text(it_is), &category, false); + lone_type( + alloc, + found, + expected_type, + expectation_context, + alloc.concat([ + alloc.reflow("The type annotation"), + on_name_text, + alloc.reflow(" says "), + it.clone(), + alloc.reflow(" should have the type:"), + ]), + alloc.concat([ + alloc.reflow("However, the type of "), + it, + alloc.reflow(" is connected to another type in a way that isn't reflected in this annotation.") + ]), + ) + } else { + type_comparison( + alloc, + found, + expected_type, + expectation_context, + add_category(alloc, alloc.text(it_is), &category), + alloc.concat([ + alloc.text("But the type annotation"), + on_name_text, + alloc.text(" says it should be:"), + ]), + None, + ) + }; + + Report { + title: "TYPE MISMATCH".to_string(), + filename, + doc: alloc.stack([ + alloc.text("Something is off with the ").append(thing), + { + // for typed bodies, include the line(s) with the signature + let joined = + roc_region::all::Region::span_across(&ann_region, &expr_region); + alloc.region_with_subregion( + lines.convert_region(joined), + lines.convert_region(expr_region), + ) + }, + comparison, + ]), + severity, + } + } + Expected::ForReason(reason, expected_type, region) => match reason { + Reason::ExpectCondition => { + let problem = alloc.concat([ + alloc.text("This "), + alloc.keyword("expect"), + alloc.text(" condition needs to be a "), + alloc.type_str("Bool"), + alloc.text(":"), + ]); + + report_bad_type( + alloc, + lines, + filename, + severity, + &category, + found, + expected_type, + region, + Some(expr_region), + problem, + alloc.text("Right now it’s"), + alloc.concat([ + alloc.reflow("But I need every "), + alloc.keyword("expect"), + alloc.reflow(" condition to evaluate to a "), + alloc.type_str("Bool"), + alloc.reflow("—either "), + alloc.tag("Bool.true".into()), + alloc.reflow(" or "), + alloc.tag("Bool.false".into()), + alloc.reflow("."), + ]), + // Note: Elm has a hint here about truthiness. I think that + // makes sense for Elm, since most Elm users will come from + // JS, where truthiness is a thing. I don't really know + // what the background of Roc programmers will be, and I'd + // rather not create a distraction by introducing a term + // they don't know. ("Wait, what's truthiness?") + ) + } + Reason::IfCondition => { + let problem = alloc.concat([ + alloc.text("This "), + alloc.keyword("if"), + alloc.text(" condition needs to be a "), + alloc.type_str("Bool"), + alloc.text(":"), + ]); + + report_bad_type( + alloc, + lines, + filename, + severity, + &category, + found, + expected_type, + region, + Some(expr_region), + problem, + alloc.text("Right now it’s"), + alloc.concat([ + alloc.reflow("But I need every "), + alloc.keyword("if"), + alloc.reflow(" condition to evaluate to a "), + alloc.type_str("Bool"), + alloc.reflow("—either "), + alloc.tag("Bool.true".into()), + alloc.reflow(" or "), + alloc.tag("Bool.false".into()), + alloc.reflow("."), + ]), + // Note: Elm has a hint here about truthiness. I think that + // makes sense for Elm, since most Elm users will come from + // JS, where truthiness is a thing. I don't really know + // what the background of Roc programmers will be, and I'd + // rather not create a distraction by introducing a term + // they don't know. ("Wait, what's truthiness?") + ) + } + Reason::WhenGuard => { + let problem = alloc.concat([ + alloc.text("This "), + alloc.keyword("if"), + alloc.text(" guard condition needs to be a "), + alloc.type_str("Bool"), + alloc.text(":"), + ]); + report_bad_type( + alloc, + lines, + filename, + severity, + &category, + found, + expected_type, + region, + Some(expr_region), + problem, + alloc.text("Right now it’s"), + alloc.concat([ + alloc.reflow("But I need every "), + alloc.keyword("if"), + alloc.reflow(" guard condition to evaluate to a "), + alloc.type_str("Bool"), + alloc.reflow("—either "), + alloc.tag("Bool.true".into()), + alloc.reflow(" or "), + alloc.tag("Bool.false".into()), + alloc.reflow("."), + ]), + ) + } + Reason::IfBranch { + index, + total_branches, + } => match total_branches { + 2 => report_mismatch( + alloc, + lines, + filename, + severity, + &category, + found, + expected_type, + region, + Some(expr_region), + alloc.concat([ + alloc.text("This "), + alloc.keyword("if"), + alloc.text(" has an "), + alloc.keyword("else"), + alloc.text(" branch with a different type from its "), + alloc.keyword("then"), + alloc.text(" branch:"), + ]), + alloc.concat([ + alloc.text("The "), + alloc.keyword("else"), + alloc.text(" branch is"), + ]), + alloc.concat([ + alloc.text("but the "), + alloc.keyword("then"), + alloc.text(" branch has the type:"), + ]), + Some(alloc.concat([ + alloc.text("All branches in an "), + alloc.keyword("if"), + alloc.text(" must have the same type!"), + ])), + ), + _ => report_mismatch( + alloc, + lines, + filename, + severity, + &category, + found, + expected_type, + region, + Some(expr_region), + alloc.concat([ + alloc.reflow("The "), + alloc.string(index.ordinal()), + alloc.reflow(" branch of this "), + alloc.keyword("if"), + alloc.reflow(" does not match all the previous branches:"), + ]), + alloc.string(format!("The {} branch is", index.ordinal())), + alloc.reflow("But all the previous branches have type:"), + Some(alloc.concat([ + alloc.reflow("All branches in an "), + alloc.keyword("if"), + alloc.reflow(" must have the same type!"), + ])), + ), + }, + Reason::WhenBranch { index } => report_mismatch( + alloc, + lines, + filename, + severity, + &category, + found, + expected_type, + region, + Some(expr_region), + alloc.concat([ + alloc.reflow("The "), + alloc.string(index.ordinal()), + alloc.reflow(" branch of this "), + alloc.keyword("when"), + alloc.reflow(" does not match all the previous branches:"), + ]), + alloc.concat([ + alloc.reflow("The "), + alloc.string(index.ordinal()), + alloc.reflow(" branch is"), + ]), + alloc.reflow("But all the previous branches have type:"), + Some(alloc.concat([ + alloc.reflow("All branches of a "), + alloc.keyword("when"), + alloc.reflow(" must have the same type!"), + ])), + ), + Reason::ElemInList { index } => { + let ith = index.ordinal(); + + // Don't say "the previous elements all have the type" if + // there was only 1 previous element! + let prev_elems_msg = if index.to_zero_based() == 1 { + "However, the 1st element has the type:" + } else { + "However, the preceding elements in the list all have the type:" + }; + + report_mismatch( + alloc, + lines, + filename, + severity, + &category, + found, + expected_type, + region, + Some(expr_region), + alloc.reflow("This list contains elements with different types:"), + alloc.string(format!("Its {ith} element is")), + alloc.reflow(prev_elems_msg), + Some(alloc.reflow("Every element in a list must have the same type!")), + ) + } + Reason::RecordUpdateValue(field) => report_mismatch( + alloc, + lines, + filename, + severity, + &category, + found, + expected_type, + region, + Some(expr_region), + alloc.concat([ + alloc.text("I cannot update the "), + alloc.record_field(field.to_owned()), + alloc.text(" field like this:"), + ]), + alloc.concat([ + alloc.text("You are trying to update "), + alloc.record_field(field), + alloc.text(" to be"), + ]), + alloc.text("But it should be:"), + Some(alloc.reflow( + "Record update syntax does not allow you \ + to change the type of fields. \ + You can achieve that with record literal syntax.", + )), + ), + Reason::RecordUpdateKeys(symbol, expected_fields) => { + match found.clone().unwrap_structural_alias() { + ErrorType::Record(actual_fields, ext) => { + let expected_set: MutSet<_> = expected_fields.keys().cloned().collect(); + let actual_set: MutSet<_> = actual_fields.keys().cloned().collect(); + + let mut diff = expected_set.difference(&actual_set); + + match diff.next().and_then(|k| Some((k, expected_fields.get(k)?))) { + None => report_mismatch( + alloc, + lines, + filename, + severity, + &category, + found, + expected_type, + region, + Some(expr_region), + alloc.reflow("Something is off with this record update:"), + alloc.concat([ + alloc.reflow("The"), + alloc.symbol_unqualified(symbol), + alloc.reflow(" record is"), + ]), + alloc.reflow("But this update needs it to be compatible with:"), + None, + ), + Some((field, field_region)) => report_record_field_typo( + alloc, + lines, + filename, + severity, + Some(symbol), + "", + field, + ":", + *field_region, + actual_fields, + ext, + ), + } + } + _ => report_bad_type( + alloc, + lines, + filename, + severity, + &category, + found, + expected_type, + region, + Some(expr_region), + alloc.reflow("This is not a record, so it has no fields to update!"), + alloc.reflow("It is"), + alloc.reflow("But I need a record!"), + ), + } + } + Reason::FnCall { + name, + arity, + called_via, + } => match describe_wanted_function(&found) { + DescribedFunction::NotAFunction(tag) => { + let this_value = match name { + None => alloc.text("This value"), + Some(symbol) => alloc.concat([ + alloc.text("The "), + alloc.symbol_unqualified(symbol), + alloc.text(" value"), + ]), + }; + + use NotAFunctionTag::*; + let doc = match tag { + OpaqueNeedsUnwrap => alloc.stack([ + alloc.concat([ + this_value, + alloc.reflow( + " is an opaque type, so it cannot be called with an argument:", + ), + ]), + alloc.region(lines.convert_region(expr_region)), + match called_via { + CalledVia::RecordBuilder => { + alloc.hint("Did you mean to apply it to a function first?") + }, + _ => { + alloc.reflow("I can't call an opaque type because I don't know what it is! Maybe you meant to unwrap it first?") + } + } + ]), + Other => alloc.stack([ + alloc.concat([ + this_value, + alloc.string(format!( + " is not a function, but it was given {}:", + if arity == 1 { + "1 argument".into() + } else { + format!("{arity} arguments") + } + )), + ]), + alloc.region(lines.convert_region(expr_region)), + match called_via { + CalledVia::RecordBuilder => { + alloc.concat([ + alloc.tip(), + alloc.reflow("Remove "), + alloc.keyword("<-"), + alloc.reflow(" to assign the field directly.") + ]) + } + _ => { + alloc.reflow("Are there any missing commas? Or missing parentheses?") + } + } + ]), + }; + + Report { + filename, + title: "TOO MANY ARGS".to_string(), + doc, + severity, + } + } + DescribedFunction::Arguments(n) => { + let this_function = match name { + None => alloc.text("This function"), + Some(symbol) => alloc.concat([ + alloc.text("The "), + alloc.symbol_unqualified(symbol), + alloc.text(" function"), + ]), + }; + + if n < arity as usize { + let lines = vec![ + alloc.concat([ + this_function, + alloc.string(format!( + " expects {}, but it got {} instead:", + if n == 1 { + "1 argument".into() + } else { + format!("{n} arguments") + }, + arity + )), + ]), + alloc.region(lines.convert_region(expr_region)), + alloc.reflow("Are there any missing commas? Or missing parentheses?"), + ]; + + Report { + filename, + title: "TOO MANY ARGS".to_string(), + doc: alloc.stack(lines), + severity, + } + } else { + let lines = vec![ + alloc.concat([ + this_function, + alloc.string(format!( + " expects {}, but it got only {}:", + if n == 1 { + "1 argument".into() + } else { + format!("{n} arguments") + }, + arity + )), + ]), + alloc.region(lines.convert_region(expr_region)), + alloc.reflow( + "Roc does not allow functions to be partially applied. \ + Use a closure to make partial application explicit.", + ), + ]; + + Report { + filename, + title: "TOO FEW ARGS".to_string(), + doc: alloc.stack(lines), + severity, + } + } + } + }, + Reason::FnArg { + name, + arg_index, + called_via, + } => { + let ith = arg_index.ordinal(); + + let this_function = match (called_via, name) { + (CalledVia::Space, Some(symbole)) => alloc.symbol_unqualified(symbole), + (CalledVia::BinOp(op), _) => alloc.binop(op), + (CalledVia::UnaryOp(op), _) => alloc.unop(op), + (CalledVia::StringInterpolation, _) => alloc.text("this string interpolation"), + _ => alloc.text("this function"), + }; + + let argument = match called_via { + CalledVia::StringInterpolation => "argument".to_string(), + _ => format!("{ith} argument"), + }; + + report_mismatch( + alloc, + lines, + filename, + severity, + &category, + found, + expected_type, + region, + Some(expr_region), + alloc.concat([ + alloc.string(format!("This {argument} to ")), + this_function.clone(), + alloc.text(" has an unexpected type:"), + ]), + alloc.text("The argument is"), + alloc.concat([ + alloc.text("But "), + this_function, + alloc.string(format!(" needs its {argument} to be:")), + ]), + None, + ) + } + + Reason::NumericLiteralSuffix => report_mismatch( + alloc, + lines, + filename, + severity, + &category, + found, + expected_type, + region, + Some(expr_region), + alloc.text("This numeric literal is being used improperly:"), + alloc.text("Here the value is used as a:"), + alloc.text("But its suffix says it's a:"), + None, + ), + + Reason::InvalidAbilityMemberSpecialization { + member_name, + def_region: _, + unimplemented_abilities, + } => { + let problem = alloc.concat([ + alloc.reflow("Something is off with this specialization of "), + alloc.symbol_unqualified(member_name), + alloc.reflow(":"), + ]); + let this_is = alloc.reflow("This value is"); + let instead_of = alloc.concat([ + alloc.reflow("But the type annotation on "), + alloc.symbol_unqualified(member_name), + alloc.reflow(" says it must match:"), + ]); + + let hint = if unimplemented_abilities.is_empty() { + None + } else { + let mut stack = Vec::with_capacity(unimplemented_abilities.len()); + for (err_type, ability) in unimplemented_abilities.into_iter() { + stack.push(does_not_implement(alloc, err_type, ability)); + } + + let hint = alloc.stack([ + alloc.concat([ + alloc.note(""), + alloc.reflow("Some types in this specialization don't implement the abilities they are expected to. I found the following missing implementations:"), + ]), + alloc.type_block(alloc.stack(stack)), + ]); + + Some(hint) + }; + + report_mismatch( + alloc, + lines, + filename, + severity, + &category, + found, + expected_type, + region, + Some(expr_region), + problem, + this_is, + instead_of, + hint, + ) + } + + Reason::GeneralizedAbilityMemberSpecialization { + member_name, + def_region: _, + } => { + let problem = alloc.concat([ + alloc.reflow("This specialization of "), + alloc.symbol_unqualified(member_name), + alloc.reflow(" is overly general:"), + ]); + let this_is = alloc.reflow("This value is"); + let instead_of = alloc.concat([ + alloc.reflow("But the type annotation on "), + alloc.symbol_unqualified(member_name), + alloc.reflow(" says it must match:"), + ]); + + let note = alloc.stack([ + alloc.concat([ + alloc.note(""), + alloc.reflow("The specialized type is too general, and does not provide a concrete type where a type variable is bound to an ability."), + ]), + alloc.reflow("Specializations can only be made for concrete types. If you have a generic implementation for this value, perhaps you don't need an ability?"), + ]); + + report_mismatch( + alloc, + lines, + filename, + severity, + &category, + found, + expected_type, + region, + Some(expr_region), + problem, + this_is, + instead_of, + Some(note), + ) + } + + Reason::WhenBranches => { + let snippet = alloc.region_with_subregion( + lines.convert_region(region), + lines.convert_region(expr_region), + ); + + let this_is = alloc.concat([ + alloc.reflow("The "), + alloc.keyword("when"), + alloc.reflow(" condition is"), + ]); + + let wanted = alloc.reflow("But the branch patterns have type:"); + let details = Some(alloc.concat([ + alloc.reflow("The branches must be cases of the "), + alloc.keyword("when"), + alloc.reflow(" condition's type!"), + ])); + + let lines = [ + alloc.concat([ + alloc.reflow("The branches of this "), + alloc.keyword("when"), + alloc.reflow(" expression don't match the condition:"), + ]), + snippet, + type_comparison( + alloc, + found, + expected_type, + ExpectationContext::WhenCondition, + add_category(alloc, this_is, &category), + wanted, + details, + ), + ]; + + Report { + title: "TYPE MISMATCH".to_string(), + filename, + doc: alloc.stack(lines), + severity, + } + } + + Reason::TypedArg { name, arg_index } => { + let name = match name { + Some(n) => alloc.symbol_unqualified(n), + None => alloc.text(" this definition "), + }; + let doc = alloc.stack([ + alloc + .text("The ") + .append(alloc.text(arg_index.ordinal())) + .append(alloc.text(" argument to ")) + .append(name.clone()) + .append(alloc.text(" is weird:")), + alloc.region(lines.convert_region(region)), + pattern_type_comparison( + alloc, + expected_type, + found, + add_category(alloc, alloc.text("The argument matches"), &category), + alloc.concat([ + alloc.text("But the annotation on "), + name, + alloc.text(" says the "), + alloc.text(arg_index.ordinal()), + alloc.text(" argument should be:"), + ]), + vec![], + ), + ]); + + Report { + filename, + title: "TYPE MISMATCH".to_string(), + doc, + severity, + } + } + + Reason::CrashArg => { + let this_is = alloc.reflow("The value is"); + + let wanted = alloc.concat([ + alloc.reflow("But I can only "), + alloc.keyword("crash"), + alloc.reflow(" with messages of type"), + ]); + + let details = None; + + let lines = [ + alloc + .reflow("This value passed to ") + .append(alloc.keyword("crash")) + .append(alloc.reflow(" is not a string:")), + alloc.region(lines.convert_region(region)), + type_comparison( + alloc, + found, + expected_type, + ExpectationContext::WhenCondition, + add_category(alloc, this_is, &category), + wanted, + details, + ), + ]; + + Report { + filename, + title: "TYPE MISMATCH".to_string(), + doc: alloc.stack(lines), + severity, + } + } + + Reason::LowLevelOpArg { op, arg_index } => { + panic!( + "Compiler bug: argument #{} to low-level operation {:?} was the wrong type!", + arg_index.ordinal(), + op + ); + } + + Reason::ForeignCallArg { + foreign_symbol, + arg_index, + } => { + panic!( + "Compiler bug: argument #{} to foreign symbol {:?} was the wrong type!", + arg_index.ordinal(), + foreign_symbol + ); + } + + Reason::FloatLiteral | Reason::IntLiteral | Reason::NumLiteral => { + unreachable!("I don't think these can be reached") + } + + Reason::StrInterpolation => { + unimplemented!("string interpolation is not implemented yet") + } + + Reason::RecordDefaultField(_) => { + unimplemented!("record default field is not implemented yet") + } + }, + } +} + +fn does_not_implement<'a>( + alloc: &'a RocDocAllocator<'a>, + err_type: ErrorType, + ability: Symbol, +) -> RocDocBuilder<'a> { + alloc.concat([ + to_doc(alloc, Parens::Unnecessary, err_type).0, + alloc.reflow(" does not implement "), + alloc.symbol_unqualified(ability), + ]) +} + +enum DescribedFunction { + Arguments(usize), + NotAFunction(NotAFunctionTag), +} + +enum NotAFunctionTag { + OpaqueNeedsUnwrap, + Other, +} + +fn describe_wanted_function(tipe: &ErrorType) -> DescribedFunction { + use ErrorType::*; + + match tipe { + Function(args, _, _) => DescribedFunction::Arguments(args.len()), + Alias(_, _, actual, AliasKind::Structural) => describe_wanted_function(actual), + Alias(_, _, actual, AliasKind::Opaque) => { + let tag = if matches!( + describe_wanted_function(actual), + DescribedFunction::Arguments(_) + ) { + NotAFunctionTag::OpaqueNeedsUnwrap + } else { + NotAFunctionTag::Other + }; + + DescribedFunction::NotAFunction(tag) + } + _ => DescribedFunction::NotAFunction(NotAFunctionTag::Other), + } +} + +/// The context a type expectation is derived from. +#[derive(Clone)] +enum ExpectationContext<'a> { + /// An expected type was discovered from a type annotation. Corresponds to + /// [`Expected::FromAnnotation`](Expected::FromAnnotation). + Annotation { + on: RocDocBuilder<'a>, + }, + WhenCondition, + /// When we don't know the context, or it's not relevant. + Arbitrary, +} + +impl<'a> std::fmt::Debug for ExpectationContext<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ExpectationContext::Annotation { .. } => f.write_str("Annotation"), + ExpectationContext::WhenCondition => f.write_str("WhenCondition"), + ExpectationContext::Arbitrary => f.write_str("Arbitrary"), + } + } +} + +fn type_comparison<'b>( + alloc: &'b RocDocAllocator<'b>, + actual: ErrorType, + expected: ErrorType, + expectation_context: ExpectationContext<'b>, + i_am_seeing: RocDocBuilder<'b>, + instead_of: RocDocBuilder<'b>, + context_hints: Option>, +) -> RocDocBuilder<'b> { + let comparison = to_comparison(alloc, actual, expected); + + let mut lines = vec![ + i_am_seeing, + comparison.actual, + instead_of, + comparison.expected, + ]; + + if context_hints.is_some() { + lines.push(alloc.concat(context_hints)); + } + + lines.extend(problems_to_tip( + alloc, + comparison.problems, + expectation_context, + )); + + alloc.stack(lines) +} + +fn lone_type<'b>( + alloc: &'b RocDocAllocator<'b>, + actual: ErrorType, + expected: ErrorType, + expectation_context: ExpectationContext<'b>, + i_am_seeing: RocDocBuilder<'b>, + further_details: RocDocBuilder<'b>, +) -> RocDocBuilder<'b> { + let comparison = to_comparison(alloc, actual, expected); + + let mut lines = vec![i_am_seeing, comparison.actual, further_details]; + + lines.extend(problems_to_tip( + alloc, + comparison.problems, + expectation_context, + )); + + alloc.stack(lines) +} + +/// Formats an item in a Roc program to a tuple (summary, has_type_colon), where +/// concatenation of the tuple items introduces the item and leads up to its type. +fn format_category<'b>( + alloc: &'b RocDocAllocator<'b>, + this_is: RocDocBuilder<'b>, + category: &Category, + capitalize_start: bool, +) -> (RocDocBuilder<'b>, RocDocBuilder<'b>) { + use Category::*; + + let t = if capitalize_start { "T" } else { "t" }; + + match category { + Lookup(name) => ( + alloc.concat([ + text!(alloc, "{}his ", t), + alloc.symbol_foreign_qualified(*name), + alloc.text(" value"), + ]), + alloc.text(" is a:"), + ), + + If => ( + alloc.concat([ + text!(alloc, "{}his ", t), + alloc.keyword("if"), + alloc.text(" expression"), + ]), + alloc.text(" produces:"), + ), + When => ( + alloc.concat([ + text!(alloc, "{}his ", t), + alloc.keyword("when"), + alloc.text(" expression"), + ]), + alloc.text(" produces:"), + ), + List => ( + alloc.concat([this_is, alloc.text(" a list")]), + alloc.text(" of type:"), + ), + Num => ( + alloc.concat([this_is, alloc.text(" a number")]), + alloc.text(" of type:"), + ), + Int => ( + alloc.concat([this_is, alloc.text(" an integer")]), + alloc.text(" of type:"), + ), + Frac => ( + alloc.concat([this_is, alloc.text(" a fraction")]), + alloc.text(" of type:"), + ), + Str => ( + alloc.concat([this_is, alloc.text(" a string")]), + alloc.text(" of type:"), + ), + StrInterpolation => ( + alloc.concat([this_is, alloc.text(" a value in a string interpolation,")]), + alloc.text(" which was of type:"), + ), + Character => ( + alloc.concat([this_is, alloc.text(" a Unicode scalar value")]), + alloc.text(" of type:"), + ), + IngestedFile(file_path) => ( + alloc.concat([this_is, text!(alloc, " an ingested file ({:?})", file_path)]), + alloc.text(" of type:"), + ), + Lambda => ( + alloc.concat([this_is, alloc.text(" an anonymous function")]), + alloc.text(" of type:"), + ), + ClosureSize => ( + alloc.concat([this_is, alloc.text(" the closure size of a function")]), + alloc.text(" of type:"), + ), + + OpaqueWrap(opaque) => ( + alloc.concat([ + text!(alloc, "{}his ", t), + alloc.opaque_name(*opaque), + alloc.text(" opaque wrapping"), + ]), + alloc.text(" has the type:"), + ), + + OpaqueArg => ( + alloc.concat([text!(alloc, "{}his argument to an opaque type", t)]), + alloc.text(" has type:"), + ), + + TagApply { + tag_name: TagName(name), + args_count: 0, + } => ( + alloc.concat([ + text!(alloc, "{}his ", t), + alloc.tag(name.to_owned()), + alloc.text(" tag"), + ]), + alloc.text(" has the type:"), + ), + + TagApply { + tag_name: TagName(name), + args_count: _, + } => ( + alloc.concat([ + text!(alloc, "{}his ", t), + alloc.tag(name.to_owned()), + alloc.text(" tag application"), + ]), + alloc.text(" has the type:"), + ), + + Record => ( + alloc.concat([this_is, alloc.text(" a record")]), + alloc.text(" of type:"), + ), + + Accessor(field) => ( + alloc.concat([ + text!(alloc, "{}his ", t), + match field { + IndexOrField::Index(index) => alloc.tuple_field(*index), + IndexOrField::Field(field) => alloc.record_field(field.to_owned()), + }, + alloc.text(" value"), + ]), + alloc.text(" is a:"), + ), + RecordAccess(field) => ( + alloc.concat([ + text!(alloc, "{}he value at ", t), + alloc.record_field(field.to_owned()), + ]), + alloc.text(" is a:"), + ), + + Tuple => ( + alloc.concat([this_is, alloc.text(" a tuple")]), + alloc.text(" of type:"), + ), + + TupleAccess(index) => ( + alloc.concat([text!(alloc, "{}he value at ", t), alloc.tuple_field(*index)]), + alloc.text(" is a:"), + ), + + CallResult( + Some(_), + CalledVia::BinOp( + BinOp::Equals + | BinOp::NotEquals + | BinOp::LessThan + | BinOp::GreaterThan + | BinOp::LessThanOrEq + | BinOp::GreaterThanOrEq, + ), + ) => ( + text!(alloc, "{}his comparison", t), + alloc.text(" produces:"), + ), + CallResult(Some(_), CalledVia::StringInterpolation) => ( + alloc.concat([this_is, alloc.text(" a string")]), + alloc.text(" of type:"), + ), + CallResult(Some(symbol), _) => ( + alloc.concat([ + text!(alloc, "{}his ", t), + alloc.symbol_foreign_qualified(*symbol), + alloc.text(" call"), + ]), + alloc.text(" produces:"), + ), + CallResult(None, _) => (this_is, alloc.text(":")), + LowLevelOpResult(op) => { + panic!("Compiler bug: invalid return type from low-level op {op:?}"); + } + ForeignCall => { + panic!("Compiler bug: invalid return type from foreign call",); + } + + Uniqueness => ( + alloc.concat([this_is, alloc.text(" an uniqueness attribute")]), + alloc.text(" of type:"), + ), + Crash => { + internal_error!("calls to crash should be unconditionally admitted in any context, unexpected reachability!"); + } + + Storage(..) | Unknown => ( + alloc.concat([this_is, alloc.text(" a value")]), + alloc.text(" of type:"), + ), + DefaultValue(_) => ( + alloc.concat([this_is, alloc.text(" a default field")]), + alloc.text(" of type:"), + ), + AbilityMemberSpecialization(_ability_member) => ( + alloc.concat([this_is, alloc.text(" a declared specialization")]), + alloc.text(" of type:"), + ), + Expect => ( + alloc.concat([this_is, alloc.text(" an expectation")]), + alloc.text(" of type:"), + ), + Dbg => ( + alloc.concat([this_is, alloc.text(" a dbg statement")]), + alloc.text(" of type:"), + ), + } +} + +fn add_category<'b>( + alloc: &'b RocDocAllocator<'b>, + this_is: RocDocBuilder<'b>, + category: &Category, +) -> RocDocBuilder<'b> { + let (summary, suffix) = format_category(alloc, this_is, category, true); + alloc.concat([summary, suffix]) +} + +fn to_pattern_report<'b>( + alloc: &'b RocDocAllocator<'b>, + lines: &LineInfo, + filename: PathBuf, + severity: Severity, + expr_region: roc_region::all::Region, + category: PatternCategory, + found: ErrorType, + expected: PExpected, +) -> Report<'b> { + use roc_types::types::PReason; + + match expected { + PExpected::NoExpectation(expected_type) => { + let doc = alloc.stack([ + alloc.text("This pattern is being used in an unexpected way:"), + alloc.region(lines.convert_region(expr_region)), + pattern_type_comparison( + alloc, + found, + expected_type, + add_pattern_category(alloc, alloc.text("It is"), &category), + alloc.text("But it needs to match:"), + vec![], + ), + ]); + + Report { + filename, + title: "TYPE MISMATCH".to_string(), + doc, + severity, + } + } + + PExpected::ForReason(reason, expected_type, region) => match reason { + PReason::OptionalField => unreachable!("this will never be reached I think"), + PReason::TypedArg { opt_name, index } => { + let name = match opt_name { + Some(n) => alloc.symbol_unqualified(n), + None => alloc.text(" this definition "), + }; + let doc = alloc.stack([ + alloc + .text("The ") + .append(alloc.text(index.ordinal())) + .append(alloc.text(" argument to ")) + .append(name.clone()) + .append(alloc.text(" is weird:")), + alloc.region(lines.convert_region(region)), + pattern_type_comparison( + alloc, + found, + expected_type, + add_pattern_category( + alloc, + alloc.text("The argument is a pattern that matches"), + &category, + ), + alloc.concat([ + alloc.text("But the annotation on "), + name, + alloc.text(" says the "), + alloc.text(index.ordinal()), + alloc.text(" argument should be:"), + ]), + vec![], + ), + ]); + + Report { + filename, + title: "TYPE MISMATCH".to_string(), + doc, + severity, + } + } + PReason::WhenMatch { index, sub_pattern } => { + let doc = match (index, sub_pattern) { + (HumanIndex::FIRST, HumanIndex::FIRST) => alloc.stack([ + alloc + .text("The 1st pattern in this ") + .append(alloc.keyword("when")) + .append(alloc.text(" is causing a mismatch:")), + alloc.region_with_subregion( + lines.convert_region(region), + lines.convert_region(expr_region), + ), + pattern_type_comparison( + alloc, + found, + expected_type, + add_pattern_category( + alloc, + alloc.text("The first pattern is trying to match"), + &category, + ), + alloc.concat([ + alloc.text("But the expression between "), + alloc.keyword("when"), + alloc.text(" and "), + alloc.keyword("is"), + alloc.text(" has the type:"), + ]), + vec![], + ), + ]), + (index, sub_pattern) => { + let (first, index) = match sub_pattern { + HumanIndex::FIRST => { + let doc = alloc + .string(format!("The {} pattern in this ", index.ordinal())) + .append(alloc.keyword("when")) + .append(alloc.text(" does not match the previous ones:")); + (doc, index) + } + + _ => { + let doc = alloc.string(format!( + "The {} pattern in this branch does not match the previous ones:", + sub_pattern.ordinal() + )); + (doc, sub_pattern) + } + }; + + alloc.stack([ + first, + alloc.region_with_subregion( + lines.convert_region(region), + lines.convert_region(expr_region), + ), + pattern_type_comparison( + alloc, + found, + expected_type, + add_pattern_category( + alloc, + alloc.string(format!( + "The {} pattern is trying to match", + index.ordinal() + )), + &category, + ), + alloc.text("But all the previous branches match:"), + vec![], + ), + ]) + } + }; + Report { + filename, + title: "TYPE MISMATCH".to_string(), + doc, + severity, + } + } + PReason::ListElem => { + let doc = alloc.stack([ + alloc.concat([alloc.reflow("This list element doesn't match the types of other elements in the pattern:")]), + alloc.region(lines.convert_region(region)), + pattern_type_comparison( + alloc, + found, + expected_type, + add_pattern_category( + alloc, + alloc.text("It matches"), + &category, + ), + alloc.concat([ + alloc.text("But the other elements in this list pattern match") + ]), + vec![], + ), + ]); + + Report { + filename, + title: "TYPE MISMATCH".to_string(), + doc, + severity, + } + } + PReason::TagArg { .. } | PReason::PatternGuard => { + internal_error!("We didn't think this could trigger. Please tell us about it on Zulip if it does!") + } + }, + } +} + +fn pattern_type_comparison<'b>( + alloc: &'b RocDocAllocator<'b>, + actual: ErrorType, + expected: ErrorType, + i_am_seeing: RocDocBuilder<'b>, + instead_of: RocDocBuilder<'b>, + reason_hints: Vec>, +) -> RocDocBuilder<'b> { + let comparison = to_comparison(alloc, actual, expected); + + let mut lines = vec![ + i_am_seeing, + comparison.actual, + instead_of, + comparison.expected, + ]; + + lines.extend(problems_to_tip( + alloc, + comparison.problems, + ExpectationContext::Arbitrary, + )); + lines.extend(reason_hints); + + alloc.stack(lines) +} + +fn add_pattern_category<'b>( + alloc: &'b RocDocAllocator<'b>, + i_am_trying_to_match: RocDocBuilder<'b>, + category: &PatternCategory, +) -> RocDocBuilder<'b> { + use PatternCategory::*; + + let rest = match category { + Record => alloc.reflow(" record values of type:"), + Tuple => alloc.reflow(" tuple values of type:"), + EmptyRecord => alloc.reflow(" an empty record:"), + PatternGuard => alloc.reflow(" a pattern guard of type:"), + PatternDefault => alloc.reflow(" an optional field of type:"), + Set => alloc.reflow(" sets of type:"), + Map => alloc.reflow(" maps of type:"), + List => alloc.reflow(" lists of type:"), + Ctor(tag_name) => alloc.concat([ + alloc.reflow(" a "), + alloc.tag_name(tag_name.clone()), + alloc.reflow(" tag of type:"), + ]), + Opaque(opaque) => alloc.concat([ + alloc.opaque_name(*opaque), + alloc.reflow(" unwrappings of type:"), + ]), + Str => alloc.reflow(" strings:"), + Num => alloc.reflow(" numbers:"), + Int => alloc.reflow(" integers:"), + Float => alloc.reflow(" floats:"), + Character => alloc.reflow(" characters:"), + }; + + alloc.concat([i_am_trying_to_match, rest]) +} + +fn to_circular_report<'b>( + alloc: &'b RocDocAllocator<'b>, + lines: &LineInfo, + filename: PathBuf, + severity: Severity, + region: roc_region::all::Region, + symbol: Symbol, + overall_type: ErrorType, +) -> Report<'b> { + Report { + title: "CIRCULAR TYPE".to_string(), + filename, + doc: { + alloc.stack([ + alloc + .reflow("I'm inferring a weird self-referential type for ") + .append(alloc.symbol_unqualified(symbol)) + .append(alloc.text(":")), + alloc.region(lines.convert_region(region)), + alloc.stack([ + alloc.reflow( + "Here is my best effort at writing down the type. \ + You will see ∞ for parts of the type that repeat \ + something already printed out infinitely.", + ), + alloc.type_block(to_doc(alloc, Parens::Unnecessary, overall_type).0), + ]), + ]) + }, + severity, + } +} + +#[derive(Debug, Clone)] +pub enum Problem { + IntFloat, + ArityMismatch(usize, usize), + FieldTypo(Lowercase, Vec), + FieldsMissing(Vec), + TagTypo(TagName, Vec), + TagsMissing(Vec), + BadRigidVar(Lowercase, ErrorType, Option), + OptionalRequiredMismatch(Lowercase), + OpaqueComparedToNonOpaque, + BoolVsBoolTag(TagName), +} + +fn problems_to_tip<'b>( + alloc: &'b RocDocAllocator<'b>, + mut problems: Vec, + expectation_context: ExpectationContext<'b>, +) -> Option> { + if problems.is_empty() { + None + } else { + let problem = problems.remove(problems.len() - 1); + Some(type_problem_to_pretty(alloc, problem, expectation_context)) + } +} + +pub mod suggest { + use roc_module::ident::Lowercase; + + pub trait ToStr { + fn to_str(&self) -> &str; + } + + impl ToStr for Lowercase { + fn to_str(&self) -> &str { + self.as_str() + } + } + + impl ToStr for &Lowercase { + fn to_str(&self) -> &str { + self.as_str() + } + } + + impl ToStr for &str { + fn to_str(&self) -> &str { + self + } + } + + impl ToStr for super::IdentStr { + fn to_str(&self) -> &str { + self.as_str() + } + } + + impl ToStr for (A, B) + where + A: ToStr, + { + fn to_str(&self) -> &str { + self.0.to_str() + } + } + + pub fn sort(typo: &str, mut options: Vec) -> Vec + where + T: ToStr, + { + options.sort_by(|a, b| { + let l = distance::damerau_levenshtein(typo, a.to_str()); + let r = distance::damerau_levenshtein(typo, b.to_str()); + + l.cmp(&r) + }); + + options + } +} + +pub struct Comparison<'b> { + actual: RocDocBuilder<'b>, + expected: RocDocBuilder<'b>, + problems: Vec, +} + +fn to_comparison<'b>( + alloc: &'b RocDocAllocator<'b>, + actual: ErrorType, + expected: ErrorType, +) -> Comparison<'b> { + let diff = to_diff(alloc, Parens::Unnecessary, actual, expected); + let actual = type_with_able_vars(alloc, diff.left, diff.left_able); + let expected = type_with_able_vars(alloc, diff.right, diff.right_able); + + Comparison { + actual: alloc.type_block(actual), + expected: alloc.type_block(expected), + problems: match diff.status { + Status::Similar => vec![], + Status::Different(problems) => problems, + }, + } +} + +fn diff_is_wildcard_comparison<'b>( + alloc: &'b RocDocAllocator<'b>, + actual: ErrorType, + expected: ErrorType, +) -> bool { + let Comparison { problems, .. } = to_comparison(alloc, actual, expected); + match problems.last() { + Some(Problem::BadRigidVar(v1, ErrorType::RigidVar(v2), None)) => { + v1.as_str() == WILDCARD && v2.as_str() == WILDCARD + } + _ => false, + } +} + +#[derive(Debug)] +pub enum Status { + Similar, // the structure is the same or e.g. record fields are different + Different(Vec), // e.g. found Bool, expected Int +} + +impl Status { + pub fn merge(&mut self, other: Self) { + use Status::*; + match self { + Similar => { + *self = other; + } + Different(problems1) => match other { + Similar => { /* nothing */ } + Different(problems2) => { + // TODO pick a data structure that makes this merge cheaper + let mut problems = Vec::with_capacity(problems1.len() + problems2.len()); + problems.extend(problems1.iter().cloned()); + problems.extend(problems2); + *self = Different(problems); + } + }, + } + } +} + +pub struct Diff { + left: T, + right: T, + status: Status, + // idea: lift "able" type variables so they are shown at the top of a type. + left_able: AbleVariables, + right_able: AbleVariables, +} + +fn tag_ext_to_doc<'b>( + alloc: &'b RocDocAllocator<'b>, + pol: Polarity, + gen_usages: &VecMap, + ext: TypeExt, +) -> Option> { + use TypeExt::*; + + match ext { + Closed => None, + FlexOpen(lowercase) if is_generated_name(&lowercase) => { + let &usages = gen_usages.get(&lowercase).unwrap_or(&1); + + if usages > 1 { + Some(alloc.type_variable(display_generated_name(&lowercase).into())) + } else { + match pol { + Polarity::Neg => Some(alloc.type_variable(WILDCARD.into())), + Polarity::Pos => { + // Wildcard in output position is irrelevant and is elided. + None + } + } + } + } + FlexOpen(lowercase) | RigidOpen(lowercase) => Some(alloc.type_variable(lowercase)), + } +} + +fn record_ext_to_doc<'b>( + alloc: &'b RocDocAllocator<'b>, + ext: TypeExt, +) -> Option> { + use TypeExt::*; + + match ext { + Closed => None, + FlexOpen(lowercase) if is_generated_name(&lowercase) => { + Some(alloc.type_variable(display_generated_name(&lowercase).into())) + } + FlexOpen(lowercase) | RigidOpen(lowercase) => Some(alloc.type_variable(lowercase)), + } +} + +type AbleVariables = Vec<(Lowercase, AbilitySet)>; + +#[derive(Default)] +struct Context { + able_variables: AbleVariables, +} + +pub fn to_doc<'b>( + alloc: &'b RocDocAllocator<'b>, + parens: Parens, + tipe: ErrorType, +) -> (RocDocBuilder<'b>, AbleVariables) { + let mut ctx = Context::default(); + + let mut generated_name_usages = VecMap::default(); + count_generated_name_usages(&mut generated_name_usages, [&tipe]); + let doc = to_doc_help(&mut ctx, &generated_name_usages, alloc, parens, tipe); + + (doc, ctx.able_variables) +} + +fn is_generated_name(name: &Lowercase) -> bool { + name.as_str().starts_with('#') +} + +fn display_generated_name(name: &Lowercase) -> &str { + &name.as_str()[1..] +} + +fn to_doc_help<'b>( + ctx: &mut Context, + gen_usages: &VecMap, + alloc: &'b RocDocAllocator<'b>, + parens: Parens, + tipe: ErrorType, +) -> RocDocBuilder<'b> { + use ErrorType::*; + + match tipe { + Function(args, _, ret) => report_text::function( + alloc, + parens, + args.into_iter() + .map(|arg| to_doc_help(ctx, gen_usages, alloc, Parens::InFn, arg)) + .collect(), + to_doc_help(ctx, gen_usages, alloc, Parens::InFn, *ret), + ), + Infinite => alloc.text("∞"), + Error => alloc.text("?"), + + FlexVar(lowercase) if is_generated_name(&lowercase) => { + let &usages = gen_usages + .get(&lowercase) + .expect("flex var appears, but not captured here"); + + if usages > 1 { + alloc.type_variable(display_generated_name(&lowercase).into()) + } else { + alloc.type_variable(WILDCARD.into()) + } + } + FlexVar(lowercase) | RigidVar(lowercase) => alloc.type_variable(lowercase), + FlexAbleVar(lowercase, ability) | RigidAbleVar(lowercase, ability) => { + ctx.able_variables.push((lowercase.clone(), ability)); + alloc.type_variable(lowercase) + } + + Type(symbol, args) => report_text::apply( + alloc, + parens, + alloc.symbol_foreign_qualified(symbol), + args.into_iter() + .map(|arg| to_doc_help(ctx, gen_usages, alloc, Parens::InTypeParam, arg)) + .collect(), + ), + + Alias(Symbol::NUM_NUM, mut args, _, _) => { + debug_assert!(args.len() == 1); + let type_arg = args.remove(0); + + let (symbol, args) = match type_arg { + Alias(Symbol::NUM_FLOATINGPOINT, inner_args, _, _) => { + (Symbol::NUM_FRAC, inner_args) + } + Alias(Symbol::NUM_INTEGER, inner_args, _, _) => (Symbol::NUM_INT, inner_args), + _ => (Symbol::NUM_NUM, vec![type_arg]), + }; + + report_text::apply( + alloc, + parens, + alloc.symbol_foreign_qualified(symbol), + args.into_iter() + .map(|arg| to_doc_help(ctx, gen_usages, alloc, Parens::InTypeParam, arg)) + .collect(), + ) + } + + Alias(symbol, args, _, _) => report_text::apply( + alloc, + parens, + alloc.symbol_foreign_qualified(symbol), + args.into_iter() + .map(|arg| to_doc_help(ctx, gen_usages, alloc, Parens::InTypeParam, arg)) + .collect(), + ), + + Record(fields_map, ext) => { + let mut fields = fields_map.into_iter().collect::>(); + fields.sort_by(|(a, _), (b, _)| a.cmp(b)); + + report_text::record( + alloc, + fields + .into_iter() + .map(|(k, value)| { + ( + alloc.string(k.as_str().to_string()), + match value { + RecordField::Optional(v) => RecordField::Optional(to_doc_help( + ctx, + gen_usages, + alloc, + Parens::Unnecessary, + v, + )), + RecordField::RigidOptional(v) => RecordField::RigidOptional( + to_doc_help(ctx, gen_usages, alloc, Parens::Unnecessary, v), + ), + RecordField::Required(v) => RecordField::Required(to_doc_help( + ctx, + gen_usages, + alloc, + Parens::Unnecessary, + v, + )), + RecordField::RigidRequired(v) => RecordField::RigidRequired( + to_doc_help(ctx, gen_usages, alloc, Parens::Unnecessary, v), + ), + RecordField::Demanded(v) => RecordField::Demanded(to_doc_help( + ctx, + gen_usages, + alloc, + Parens::Unnecessary, + v, + )), + }, + ) + }) + .collect(), + record_ext_to_doc(alloc, ext), + 0, // zero fields omitted, since this isn't a diff + ) + } + + Tuple(elems, ext) => { + report_text::tuple( + alloc, + elems + .into_iter() + .map(|(_, value)| { + to_doc_help(ctx, gen_usages, alloc, Parens::Unnecessary, value) + }) + .collect(), + record_ext_to_doc(alloc, ext), + 0, // zero elems omitted, since this isn't a diff + ) + } + + TagUnion(tags_map, ext, pol) => { + let mut tags = tags_map + .into_iter() + .map(|(name, args)| { + ( + name, + args.into_iter() + .map(|arg| { + to_doc_help(ctx, gen_usages, alloc, Parens::InTypeParam, arg) + }) + .collect::>(), + ) + }) + .collect::>(); + tags.sort_by(|(a, _), (b, _)| a.cmp(b)); + + report_text::tag_union( + alloc, + tags.into_iter() + .map(|(k, v)| (alloc.tag_name(k), v)) + .collect(), + tag_ext_to_doc(alloc, pol, gen_usages, ext), + 0, // zero tags omitted, since this isn't a diff + None, + ) + } + + RecursiveTagUnion(rec_var, tags_map, ext, pol) => { + let mut tags = tags_map + .into_iter() + .map(|(name, args)| { + ( + name, + args.into_iter() + .map(|arg| { + to_doc_help(ctx, gen_usages, alloc, Parens::InTypeParam, arg) + }) + .collect::>(), + ) + }) + .collect::>(); + tags.sort_by(|(a, _), (b, _)| a.cmp(b)); + + let rec_doc = to_doc_help(ctx, gen_usages, alloc, Parens::Unnecessary, *rec_var); + + report_text::tag_union( + alloc, + tags.into_iter() + .map(|(k, v)| (alloc.tag_name(k), v)) + .collect(), + tag_ext_to_doc(alloc, pol, gen_usages, ext), + 0, // zero tags omitted, since this isn't a diff + Some(rec_doc), + ) + } + + Range(range_types) => { + let range_types = range_types + .into_iter() + .map(|arg| to_doc_help(ctx, gen_usages, alloc, Parens::Unnecessary, arg)) + .collect(); + report_text::range(alloc, range_types) + } + } +} + +fn count_generated_name_usages<'a>( + usages: &mut VecMap, + types: impl IntoIterator, +) { + // Stack consists of (type, only_unseen) where if `only_unseen`, then the count should only be + // incremented if the variable has not already been seen. This is to deal with counting phantom + // variables in type aliases, while not double-counting alias type arguments that also appear + // in the real type. + let mut stack = types.into_iter().map(|t| (t, false)).collect::>(); + + let mut ext_stack = vec![]; + + use ErrorType::*; + while let Some((tipe, only_unseen)) = stack.pop() { + match tipe { + FlexVar(name) | FlexAbleVar(name, _) => { + if is_generated_name(name) { + let count = usages.get_or_insert(name.clone(), || 0); + if !only_unseen || *count == 0 { + *count += 1; + } + } + } + RigidVar(name) | RigidAbleVar(name, _) => { + debug_assert!(!is_generated_name(name)); + } + Type(_, tys) => { + stack.extend(tys.iter().map(|t| (t, only_unseen))); + } + Record(fields, ext) => { + stack.extend(fields.values().map(|f| (f.as_inner(), only_unseen))); + ext_stack.push((ext, only_unseen)); + } + Tuple(elems, ext) => { + stack.extend(elems.iter().map(|(_, f)| (f, only_unseen))); + ext_stack.push((ext, only_unseen)); + } + TagUnion(tags, ext, _) => { + stack.extend(tags.values().flatten().map(|t| (t, only_unseen))); + ext_stack.push((ext, only_unseen)); + } + RecursiveTagUnion(rec, tags, ext, _) => { + stack.push((rec, only_unseen)); + stack.extend(tags.values().flatten().map(|t| (t, only_unseen))); + ext_stack.push((ext, only_unseen)); + } + Function(args, _lset, ret) => { + stack.extend(args.iter().map(|t| (t, only_unseen))); + stack.push((ret, only_unseen)); + } + Alias(_, args, real, _) => { + // Then, count up any phantom args that were missed b/c they're not referenced in + // the real var. Set `only_unseen` so that we don not double-count vars that do + // appear in the real var. + stack.extend(args.iter().map(|t| (t, true))); + + // First, count the occurrences in the real var + stack.push((real, only_unseen)); + } + Infinite | Error => {} + Range(_) => {} + } + } + + count_generated_name_usages_in_exts(usages, ext_stack); +} + +fn count_generated_name_usages_in_exts<'a>( + usages: &mut VecMap, + exts: impl IntoIterator, +) { + for (ext, only_unseen) in exts { + match ext { + TypeExt::FlexOpen(name) => { + if is_generated_name(name) { + let count = usages.get_or_insert(name.clone(), || 0); + if !only_unseen || *count == 0 { + *count += 1; + } + } + } + TypeExt::RigidOpen(name) => { + debug_assert!(!is_generated_name(name)); + } + TypeExt::Closed => {} + } + } +} + +fn same<'b>( + alloc: &'b RocDocAllocator<'b>, + parens: Parens, + tipe: ErrorType, +) -> Diff> { + let (doc, able) = to_doc(alloc, parens, tipe); + + Diff { + left: doc.clone(), + right: doc, + status: Status::Similar, + left_able: able.clone(), + right_able: able, + } +} + +fn type_with_able_vars<'b>( + alloc: &'b RocDocAllocator<'b>, + typ: RocDocBuilder<'b>, + able: AbleVariables, +) -> RocDocBuilder<'b> { + if able.is_empty() { + // fast path: taken the vast majority of the time + return typ; + } + + let mut doc = Vec::with_capacity(1 + 6 * able.len()); + doc.push(typ); + + for (i, (var, abilities)) in able.into_iter().enumerate() { + if i == 0 { + doc.push(alloc.space()); + doc.push(alloc.keyword(roc_parse::keyword::WHERE)); + } else { + doc.push(alloc.string(",".to_string())); + } + doc.push(alloc.space()); + doc.push(alloc.type_variable(var)); + doc.push(alloc.space()); + doc.push(alloc.keyword(roc_parse::keyword::IMPLEMENTS)); + + for (i, ability) in abilities.into_sorted_iter().enumerate() { + if i > 0 { + doc.push(alloc.space()); + doc.push(alloc.text("&")); + } + doc.push(alloc.space()); + doc.push(alloc.symbol_foreign_qualified(ability)); + } + } + + alloc.concat(doc) +} + +pub fn error_type_to_doc<'b>( + alloc: &'b RocDocAllocator<'b>, + error_type: ErrorType, +) -> RocDocBuilder<'b> { + let (typ, able_vars) = to_doc(alloc, Parens::Unnecessary, error_type); + type_with_able_vars(alloc, typ, able_vars) +} + +fn compact_builtin_aliases(typ: ErrorType) -> ErrorType { + use ErrorType::*; + match typ { + Alias(Symbol::NUM_NUM, mut args, real, kind) => { + debug_assert!(args.len() == 1); + let type_arg = args.remove(0); + + match type_arg { + Alias(Symbol::NUM_FLOATINGPOINT, inner_args, real, _) => { + Alias(Symbol::NUM_FRAC, inner_args, real, AliasKind::Structural) + } + Alias(Symbol::NUM_INTEGER, inner_args, real, _) => { + Alias(Symbol::NUM_INT, inner_args, real, AliasKind::Structural) + } + _ => Alias(Symbol::NUM_NUM, vec![type_arg], real, kind), + } + } + typ => typ, + } +} + +fn to_diff<'b>( + alloc: &'b RocDocAllocator<'b>, + parens: Parens, + type1: ErrorType, + type2: ErrorType, +) -> Diff> { + use ErrorType::*; + + let (type1, type2) = ( + compact_builtin_aliases(type1), + compact_builtin_aliases(type2), + ); + + // TODO remove clone + match (type1.clone(), type2.clone()) { + (Error, Error) | (Infinite, Infinite) => same(alloc, parens, type1), + + (FlexVar(x), FlexVar(y)) if x == y => same(alloc, parens, type1), + // Wildcards are always different! + (RigidVar(x), RigidVar(y)) if x == y && x.as_str() != WILDCARD => { + same(alloc, parens, type1) + } + + (RigidVar(x), other) | (other, RigidVar(x)) => { + let (left, left_able) = to_doc(alloc, Parens::InFn, type1); + let (right, right_able) = to_doc(alloc, Parens::InFn, type2); + + Diff { + left, + right, + status: Status::Different(vec![Problem::BadRigidVar(x, other, None)]), + left_able, + right_able, + } + } + + (RigidAbleVar(x, abs), other) | (other, RigidAbleVar(x, abs)) => { + let (left, left_able) = to_doc(alloc, Parens::InFn, type1); + let (right, right_able) = to_doc(alloc, Parens::InFn, type2); + + Diff { + left, + right, + status: Status::Different(vec![Problem::BadRigidVar(x, other, Some(abs))]), + left_able, + right_able, + } + } + + (Function(args1, _, ret1), Function(args2, _, ret2)) => { + if args1.len() == args2.len() { + let mut status = Status::Similar; + let arg_diff = diff_args(alloc, Parens::InFn, args1, args2); + let ret_diff = to_diff(alloc, Parens::InFn, *ret1, *ret2); + status.merge(arg_diff.status); + status.merge(ret_diff.status); + + let left = report_text::function(alloc, parens, arg_diff.left, ret_diff.left); + let right = report_text::function(alloc, parens, arg_diff.right, ret_diff.right); + let mut left_able = arg_diff.left_able; + left_able.extend(ret_diff.left_able); + let mut right_able = arg_diff.right_able; + right_able.extend(ret_diff.right_able); + + Diff { + left, + right, + status, + left_able, + right_able, + } + } else { + let (left, left_able) = to_doc(alloc, Parens::InFn, type1); + let (right, right_able) = to_doc(alloc, Parens::InFn, type2); + + Diff { + left, + right, + status: Status::Different(vec![Problem::ArityMismatch( + args1.len(), + args2.len(), + )]), + left_able, + right_able, + } + } + } + (Type(symbol1, args1), Type(symbol2, args2)) if symbol1 == symbol2 => { + let args_diff = diff_args(alloc, Parens::InTypeParam, args1, args2); + let left = report_text::apply( + alloc, + parens, + alloc.symbol_unqualified(symbol1), + args_diff.left, + ); + let right = report_text::apply( + alloc, + parens, + alloc.symbol_unqualified(symbol2), + args_diff.right, + ); + + Diff { + left, + right, + status: args_diff.status, + left_able: args_diff.left_able, + right_able: args_diff.right_able, + } + } + + (Alias(symbol1, args1, _, _), Alias(symbol2, args2, _, _)) if symbol1 == symbol2 => { + let args_diff = diff_args(alloc, Parens::InTypeParam, args1, args2); + let left = report_text::apply( + alloc, + parens, + alloc.symbol_unqualified(symbol1), + args_diff.left, + ); + let right = report_text::apply( + alloc, + parens, + alloc.symbol_unqualified(symbol2), + args_diff.right, + ); + + Diff { + left, + right, + status: args_diff.status, + left_able: args_diff.left_able, + right_able: args_diff.right_able, + } + } + + (Alias(Symbol::BOOL_BOOL, _, _, _), TagUnion(tags, _, _)) | (TagUnion(tags, _, _), Alias(Symbol::BOOL_BOOL, _, _, _)) + if tags.len() == 1 + && tags.keys().all(|t| t.0.as_str() == "True" || t.0.as_str() == "False") => + { + let written_tag = tags.keys().next().unwrap().clone(); + let (left, left_able) = to_doc(alloc, Parens::InFn, type1); + let (right, right_able) = to_doc(alloc, Parens::InFn, type2); + + Diff { + left, + right, + status: Status::Different(vec![Problem::BoolVsBoolTag(written_tag)]), + left_able, + right_able, + } + } + + (Alias(sym, _, _, AliasKind::Opaque), _) | (_, Alias(sym, _, _, AliasKind::Opaque)) + // Skip the hint for numbers; it's not as useful as saying "this type is not a number" + if !OPAQUE_NUM_SYMBOLS.contains(&sym) + // And same for bools + && sym != Symbol::BOOL_BOOL => + { + let (left, left_able) = to_doc(alloc, Parens::InFn, type1); + let (right, right_able) = to_doc(alloc, Parens::InFn, type2); + + Diff { + left, + right, + status: Status::Different(vec![Problem::OpaqueComparedToNonOpaque]), + left_able, + right_able, + } + } + + (Alias(symbol, _, actual, AliasKind::Structural), other) + if !symbol.module_id().is_builtin() => + { + // when diffing a structural alias with a non-alias, de-alias + to_diff(alloc, parens, *actual, other) + } + (other, Alias(symbol, _, actual, AliasKind::Structural)) + if !symbol.module_id().is_builtin() => + { + // when diffing a structural alias with a non-alias, de-alias + to_diff(alloc, parens, other, *actual) + } + + (Record(fields1, ext1), Record(fields2, ext2)) => { + diff_record(alloc, fields1, ext1, fields2, ext2) + } + + (TagUnion(tags1, ext1, pol), TagUnion(tags2, ext2, _)) => { + diff_tag_union(alloc, pol, tags1, ext1, None, tags2, ext2, None) + } + + (RecursiveTagUnion(rec1, tags1, ext1, pol), RecursiveTagUnion(rec2, tags2, ext2, _)) => { + diff_tag_union(alloc, pol, tags1, ext1, Some(*rec1), tags2, ext2, Some(*rec2)) + } + + pair => { + // We hit none of the specific cases where we give more detailed information + let (left, left_able) = to_doc(alloc, parens, type1); + let (right, right_able) = to_doc(alloc, parens, type2); + + let is_int = |t: &ErrorType| match t { + ErrorType::Type(Symbol::NUM_INT, _) => true, + ErrorType::Alias(Symbol::NUM_INT, _, _, _) => true, + + ErrorType::Type(Symbol::NUM_NUM, args) => { + matches!( + &args.get(0), + Some(ErrorType::Type(Symbol::NUM_INTEGER, _)) + | Some(ErrorType::Alias(Symbol::NUM_INTEGER, _, _, _)) + ) + } + ErrorType::Alias(Symbol::NUM_NUM, args, _, _) => { + matches!( + &args.get(0), + Some(ErrorType::Type(Symbol::NUM_INTEGER, _)) + | Some(ErrorType::Alias(Symbol::NUM_INTEGER, _, _, _)) + ) + } + _ => false, + }; + let is_float = |t: &ErrorType| match t { + ErrorType::Type(Symbol::NUM_FRAC, _) => true, + ErrorType::Alias(Symbol::NUM_FRAC, _, _, _) => true, + + ErrorType::Type(Symbol::NUM_NUM, args) => { + matches!( + &args.get(0), + Some(ErrorType::Type(Symbol::NUM_FLOATINGPOINT, _)) + | Some(ErrorType::Alias(Symbol::NUM_FLOATINGPOINT, _, _, _)) + ) + } + + ErrorType::Alias(Symbol::NUM_NUM, args, _, _) => { + matches!( + &args.get(0), + Some(ErrorType::Type(Symbol::NUM_FLOATINGPOINT, _)) + | Some(ErrorType::Alias(Symbol::NUM_FLOATINGPOINT, _, _, _)) + ) + } + _ => false, + }; + + let problems = match pair { + (a, b) if (is_int(&a) && is_float(&b)) || (is_float(&a) && is_int(&b)) => { + vec![Problem::IntFloat] + } + _ => vec![], + }; + + Diff { + left, + right, + status: Status::Different(problems), + left_able, + right_able, + } + } + } +} + +fn diff_args<'b, I>( + alloc: &'b RocDocAllocator<'b>, + parens: Parens, + args1: I, + args2: I, +) -> Diff>> +where + I: IntoIterator, +{ + let mut status = Status::Similar; + + // TODO use ExactSizeIterator to pre-allocate here + let mut left = Vec::new(); + let mut right = Vec::new(); + let mut left_able = Vec::new(); + let mut right_able = Vec::new(); + + for (arg1, arg2) in args1.into_iter().zip(args2.into_iter()) { + let diff = to_diff(alloc, parens, arg1, arg2); + + left.push(diff.left); + right.push(diff.right); + status.merge(diff.status); + left_able.extend(diff.left_able); + right_able.extend(diff.right_able); + } + + Diff { + left, + right, + status, + left_able, + right_able, + } +} + +fn ext_has_fixed_fields(ext: &TypeExt) -> bool { + match ext { + TypeExt::Closed => true, + TypeExt::FlexOpen(_) => false, + TypeExt::RigidOpen(_) => true, + } +} + +fn diff_record<'b>( + alloc: &'b RocDocAllocator<'b>, + fields1: SendMap>, + ext1: TypeExt, + mut fields2: SendMap>, + ext2: TypeExt, +) -> Diff> { + let to_overlap_docs = + |(field, (t1, t2)): (Lowercase, (RecordField, RecordField))| { + let diff = to_diff( + alloc, + Parens::Unnecessary, + t1.clone().into_inner(), + t2.clone().into_inner(), + ); + + Diff { + left: ( + field.clone(), + alloc.string(field.as_str().to_string()), + t1.replace(diff.left), + ), + right: ( + field.clone(), + alloc.string(field.as_str().to_string()), + t2.replace(diff.right), + ), + status: { + match (&t1, &t2) { + (RecordField::Demanded(_), RecordField::Optional(_)) + | (RecordField::Optional(_), RecordField::Demanded(_)) + | ( + RecordField::Demanded(_) | RecordField::Required(_), + RecordField::RigidOptional(_), + ) + | ( + RecordField::RigidOptional(_), + RecordField::Demanded(_) | RecordField::Required(_), + ) => match diff.status { + Status::Similar => { + Status::Different(vec![Problem::OptionalRequiredMismatch(field)]) + } + Status::Different(mut problems) => { + problems.push(Problem::OptionalRequiredMismatch(field)); + + Status::Different(problems) + } + }, + _ => diff.status, + } + }, + left_able: diff.left_able, + right_able: diff.right_able, + } + }; + + let to_unknown_docs = |(field, tipe): (&Lowercase, &RecordField)| { + ( + field.clone(), + alloc.string(field.as_str().to_string()), + tipe.map(|t| to_doc(alloc, Parens::Unnecessary, t.clone()).0), + ) + }; + let mut same_fields_different_types = VecMap::default(); + let mut fields_in_left_only = Vec::default(); + let mut same_fields_same_types = 0; + + for (k1, v1) in fields1.into_iter() { + match fields2.remove(&k1) { + Some(v2) if should_show_field_diff(&v1, &v2) => { + // The field names are the same but the types are different + // (or at least should be rendered as different) + same_fields_different_types.insert(k1, (v1, v2)); + } + Some(_) => { + // They're both the same fields and the same types + same_fields_same_types += 1; + } + None => { + // Only fields1 has this field. + fields_in_left_only.push((k1, v1)); + } + } + } + + // We've removed all the fields that they had in common, so the remaining entries in fields2 + // are ones that appear on the right only. + let fields_in_right_only = fields2; + + let both = same_fields_different_types.into_iter().map(to_overlap_docs); + let mut left = fields_in_left_only + .iter() + .map(|(k, v)| to_unknown_docs((k, v))) + .peekable(); + let mut right = fields_in_right_only.iter().map(to_unknown_docs).peekable(); + + let all_fields_shared = left.peek().is_none() && right.peek().is_none(); + + let status = match (ext_has_fixed_fields(&ext1), ext_has_fixed_fields(&ext2)) { + (true, true) => match left.peek() { + Some((f, _, _)) => Status::Different(vec![Problem::FieldTypo( + f.clone(), + fields_in_right_only.keys().cloned().collect(), + )]), + None => { + if right.peek().is_none() { + Status::Similar + } else { + let result = Status::Different(vec![Problem::FieldsMissing( + right.map(|v| v.0).collect(), + )]); + // we just used the values in `right`. in + right = fields_in_right_only.iter().map(to_unknown_docs).peekable(); + result + } + } + }, + (false, true) => match left.peek() { + Some((f, _, _)) => Status::Different(vec![Problem::FieldTypo( + f.clone(), + fields_in_right_only.keys().cloned().collect(), + )]), + None => Status::Similar, + }, + (true, false) => match right.peek() { + Some((f, _, _)) => Status::Different(vec![Problem::FieldTypo( + f.clone(), + fields_in_left_only + .iter() + .map(|(field, _)| field.clone()) + .collect(), + )]), + None => Status::Similar, + }, + (false, false) => Status::Similar, + }; + + let ext_diff = record_ext_to_diff(alloc, ext1, ext2); + + let mut fields_diff: Diff, RecordField>)>> = + Diff { + left: vec![], + right: vec![], + status: Status::Similar, + left_able: vec![], + right_able: vec![], + }; + + for diff in both { + fields_diff.left.push(diff.left); + fields_diff.right.push(diff.right); + fields_diff.status.merge(diff.status); + fields_diff.left_able.extend(diff.left_able); + fields_diff.right_able.extend(diff.right_able); + } + + if !all_fields_shared { + fields_diff.left.extend(left); + fields_diff.right.extend(right); + fields_diff.status.merge(Status::Different(vec![])); + } + + // sort fields for display + fields_diff.left.sort_by(|a, b| a.0.cmp(&b.0)); + fields_diff.right.sort_by(|a, b| a.0.cmp(&b.0)); + + let doc1 = report_text::record( + alloc, + fields_diff + .left + .into_iter() + .map(|(_, b, c)| (b, c)) + .collect(), + ext_diff.left, + same_fields_same_types, + ); + let doc2 = report_text::record( + alloc, + fields_diff + .right + .into_iter() + .map(|(_, b, c)| (b, c)) + .collect(), + ext_diff.right, + same_fields_same_types, + ); + + fields_diff.status.merge(status); + + Diff { + left: doc1, + right: doc2, + status: fields_diff.status, + left_able: fields_diff.left_able, + right_able: fields_diff.right_able, + } +} + +/// This is a helper for should_show_field_diff - see its doc comment for details. +fn should_show_diff(t1: &ErrorType, t2: &ErrorType) -> bool { + use ErrorType::*; + + match (t1, t2) { + (Type(sym1, types1), Type(sym2, types2)) => { + if sym1 != sym2 || types1.len() != types2.len() { + return true; + } + + types1 + .iter() + .zip(types2.iter()) + .any(|(t1, t2)| should_show_diff(t1, t2)) + } + (Infinite, Infinite) | (Error, Error) => false, + (RigidVar(v1), RigidVar(v2)) => v1 != v2, + (FlexVar(_), _) | (_, FlexVar(_)) => { + // If either is flex, it will unify to the other type; no diff is needed. + false + } + (FlexAbleVar(v1, _set1), FlexAbleVar(v2, _set2)) + | (RigidAbleVar(v1, _set1), RigidAbleVar(v2, _set2)) => { + #[cfg(debug_assertions)] + { + if v1 == v2 { + // If v1 == v2, then the sets should be equal too! + debug_assert_eq!(_set1.len(), _set2.len()); + debug_assert!(_set1 + .sorted_iter() + .zip(_set2.sorted_iter()) + .all(|(t1, t2)| t1 == t2)); + } + } + + v1 != v2 + } + (Record(fields1, ext1), Record(fields2, ext2)) => { + let is_1_open = matches!(ext1, TypeExt::FlexOpen(_)); + let is_2_open = matches!(ext2, TypeExt::FlexOpen(_)); + + if !is_1_open && !is_2_open && fields1.len() != fields2.len() { + return true; + } + + // Check for diffs in any of the fields1 fields. + for (name, f1) in fields1.iter() { + match fields2.get(name) { + Some(f2) => { + // If the field is on both records, and the diff should be + // shown, then we should show a diff for the whole record. + if should_show_field_diff(f1, f2) { + return true; + } + + // (If the field is on both records, but no diff should be + // shown between those fields, continue checking other fields.) + } + None => { + // It's fine for 1 to have a field that the other doesn't have, + // so long as the other one is open. + if !is_2_open { + return true; + } + } + } + } + + // At this point we've checked all the fields that are in both records, + // as well as all the fields that are in 1 but not 2. + // All that remains is to check the fields that are in 2 but not 1, + // which we don't care about if 1 is open (because then it's fine + // for the other record to have fields it doesn't). + if !is_1_open { + for name in fields2.keys() { + if !fields1.contains_key(name) { + // fields1 is missing this field, and fields1 is not open, + // therefore this is a relevant diff. + return true; + } + } + } + + // We haven't early-returned true yet, so we didn't find any relevant diffs! + false + } + (Tuple(elems1, ext1), Tuple(elems2, ext2)) => { + if elems1.len() != elems2.len() || ext1 != ext2 { + return true; + } + + elems1 + .iter() + .zip(elems2.iter()) + .any(|((i1, e1), (i2, e2))| i1 != i2 || should_show_diff(e1, e2)) + } + (TagUnion(tags1, ext1, polarity1), TagUnion(tags2, ext2, polarity2)) => { + debug_assert_eq!( + polarity1, polarity2, + "Any two tag unions we're comparing should have the same polarity!" + ); + + if tags1.len() != tags2.len() || ext1 != ext2 { + return true; + } + + tags1 + .iter() + .zip(tags2.iter()) + .any(|((name1, payload1), (name2, payload2))| { + if name1 != name2 || payload1.len() != payload2.len() { + return true; + } + + payload1 + .iter() + .zip(payload2.iter()) + .any(|(p1, p2)| should_show_diff(p1, p2)) + }) + } + ( + RecursiveTagUnion(rec1, tags1, ext1, _polarity1), + RecursiveTagUnion(rec2, tags2, ext2, _polarity2), + ) => { + // If two tag unions differ only in polarity, don't show that as a diff; + // polarity is invisible to the reader! + + if tags1.len() != tags2.len() || ext1 != ext2 || should_show_diff(rec1, rec2) { + return true; + } + + tags1 + .iter() + .zip(tags2.iter()) + .any(|((name1, payload1), (name2, payload2))| { + if name1 != name2 || payload1.len() != payload2.len() { + return true; + } + + payload1 + .iter() + .zip(payload2.iter()) + .any(|(p1, p2)| should_show_diff(p1, p2)) + }) + } + (Function(params1, ret1, l1), Function(params2, ret2, l2)) => { + if params1.len() != params2.len() + || should_show_diff(ret1, ret2) + || should_show_diff(l1, l2) + { + return true; + } + + params1 + .iter() + .zip(params2.iter()) + .any(|(p1, p2)| should_show_diff(p1, p2)) + } + (Alias(sym1, params1, t1, kind1), Alias(sym2, params2, t2, kind2)) => { + if sym1 != sym2 + || kind1 != kind2 + || params1.len() != params2.len() + || should_show_diff(t1, t2) + { + return true; + } + + params1 + .iter() + .zip(params2.iter()) + .any(|(p1, p2)| should_show_diff(p1, p2)) + } + (Range(types1), Range(types2)) => { + if types1.len() != types2.len() { + return true; + } + + types1 + .iter() + .zip(types2.iter()) + .any(|(t1, t2)| should_show_diff(t1, t2)) + } + (Alias(_sym, _params, aliased, AliasKind::Structural), other) + | (other, Alias(_sym, _params, aliased, AliasKind::Structural)) => { + // Check to see if we should show the diff after unwrapping the alias + should_show_diff(aliased, other) + } + (Alias(_, _, _, AliasKind::Opaque), _) + | (_, Alias(_, _, _, AliasKind::Opaque)) + | (Infinite, _) + | (_, Infinite) + | (Error, _) + | (_, Error) + | (Type(_, _), _) + | (_, Type(_, _)) + | (RigidVar(_), _) + | (_, RigidVar(_)) + | (FlexAbleVar(_, _), _) + | (_, FlexAbleVar(_, _)) + | (RigidAbleVar(_, _), _) + | (_, RigidAbleVar(_, _)) + | (Record(_, _), _) + | (_, Record(_, _)) + | (Tuple(_, _), _) + | (_, Tuple(_, _)) + | (TagUnion(_, _, _), _) + | (_, TagUnion(_, _, _)) + | (RecursiveTagUnion(_, _, _, _), _) + | (_, RecursiveTagUnion(_, _, _, _)) + | (Function(_, _, _), _) + | (_, Function(_, _, _)) => true, + } +} + +/// If these are equivalent, we shouldn't bother showing them in a diff. +/// (For example, if one is Required and the other is Demanded, showing +/// them in a diff will be unhelpful; they'll both be rendered using : and will look +/// exactly the same to the reader!) +fn should_show_field_diff( + field1: &RecordField, + field2: &RecordField, +) -> bool { + use RecordField::*; + + match (field1, field2) { + // If they're both the same, they don't need a diff + (Demanded(t1), Demanded(t2)) + | (Required(t1), Required(t2)) + | (RigidRequired(t1), RigidRequired(t2)) + | (RigidOptional(t1), RigidOptional(t2)) + | (Optional(t1), Optional(t2)) + // Demanded and Required don't need a diff + | (Demanded(t1), Required(t2)) + | (Required(t1), Demanded(t2)) + // Demanded and RigidRequired don't need a diff + | (Demanded(t1), RigidRequired(t2)) + | (RigidRequired(t1), Demanded(t2)) + => should_show_diff(t1, t2), + // Everything else needs a diff + (Demanded(_), Optional(_)) + | (Demanded(_), RigidOptional(_)) + | (Required(_), RigidRequired(_)) + | (Required(_), Optional(_)) + | (Optional(_), Demanded(_)) + | (Optional(_), RigidRequired(_)) + | (Optional(_), RigidOptional(_)) + | (Optional(_), Required(_)) + | (RigidRequired(_), Required(_)) + | (RigidRequired(_), Optional(_)) + | (RigidRequired(_), RigidOptional(_)) + | (Required(_), RigidOptional(_)) + | (RigidOptional(_), Demanded(_)) + | (RigidOptional(_), Required(_)) + | (RigidOptional(_), Optional(_)) + | (RigidOptional(_), RigidRequired(_)) => true, + } +} + +fn same_tag_name_overlap_diff<'b>( + alloc: &'b RocDocAllocator<'b>, + field: TagName, + payload_vals1: Vec, + payload_vals2: Vec, +) -> Diff<(TagName, RocDocBuilder<'b>, Vec>)> { + // Render ellipses wherever the payload slots have the same type. + let mut left_doc = Vec::with_capacity(payload_vals1.len()); + let mut left_able = Vec::new(); + let mut right_doc = Vec::with_capacity(payload_vals2.len()); + let mut right_able = Vec::new(); + + // itertools::zip_longest is a zip that can continue past the end of one Vec. + // If they both have payload values in a given slot, and both are the same type, + // we render ellipsis instead of the actual type - since there's no diff between them. + // If one of them doesn't have a payload value in that slot, we always render its type. + for either_or_both in payload_vals1 + .into_iter() + .zip_longest(payload_vals2.into_iter()) + { + match either_or_both { + // Both tag unions have a payload value in this slot + EitherOrBoth::Both(t1, t2) => { + if should_show_diff(&t1, &t2) { + { + let (doc, able) = to_doc(alloc, Parens::InTypeParam, t1); + left_doc.push(doc); + left_able.extend(able); + } + + { + let (doc, able) = to_doc(alloc, Parens::InTypeParam, t2); + right_doc.push(doc); + right_able.extend(able); + } + } else { + left_doc.push(alloc.ellipsis()); + right_doc.push(alloc.ellipsis()); + } + } + // Only the left tag union has a payload value in this slot + EitherOrBoth::Left(t1) => { + let (doc, able) = to_doc(alloc, Parens::InTypeParam, t1); + left_doc.push(doc); + left_able.extend(able); + } + // Only the right tag union has a payload value in this slot + EitherOrBoth::Right(t2) => { + let (doc, able) = to_doc(alloc, Parens::InTypeParam, t2); + right_doc.push(doc); + right_able.extend(able); + } + } + } + + Diff { + left: (field.clone(), alloc.tag_name(field.clone()), left_doc), + right: (field.clone(), alloc.tag_name(field), right_doc), + status: Status::Similar, + left_able, + right_able, + } +} + +fn diff_tag_union<'b>( + alloc: &'b RocDocAllocator<'b>, + pol: Polarity, + tags1: SendMap>, + ext1: TypeExt, + rec1: Option, + mut tags2: SendMap>, + ext2: TypeExt, + rec2: Option, +) -> Diff> { + let gen_usages1 = { + let mut usages = VecMap::default(); + count_generated_name_usages(&mut usages, tags1.values().flatten()); + count_generated_name_usages_in_exts(&mut usages, [(&ext1, false)]); + usages + }; + let gen_usages2 = { + let mut usages = VecMap::default(); + count_generated_name_usages(&mut usages, tags2.values().flatten()); + count_generated_name_usages_in_exts(&mut usages, [(&ext2, false)]); + usages + }; + + let to_overlap_docs = |(tag_name, (t1, t2)): (TagName, (Vec, Vec))| { + same_tag_name_overlap_diff(alloc, tag_name, t1, t2) + }; + let to_unknown_docs = |(tag_name, args): (&TagName, &Vec)| -> ( + TagName, + RocDocBuilder<'b>, + Vec>, + AbleVariables, + ) { + let (args, able): (_, Vec) = + // TODO add spaces between args + args.iter() + .map(|arg| to_doc(alloc, Parens::InTypeParam, arg.clone())) + .unzip(); + ( + tag_name.clone(), + alloc.tag_name(tag_name.clone()), + args, + able.into_iter().flatten().collect(), + ) + }; + let mut same_tags_different_payloads = VecMap::default(); + let mut tags_in_left_only = Vec::default(); + let mut same_tags_same_payloads = 0; + + for (k1, v1) in tags1.into_iter() { + match tags2.remove(&k1) { + Some(v2) if should_show_payload_diff(&v1, &v2) => { + // The tag names are the same but the payload types are different + // (or at least should be rendered as different) + same_tags_different_payloads.insert(k1.clone(), (v1.clone(), v2)); + } + Some(_) => { + // They both have the same tag name as well as the same payload types + same_tags_same_payloads += 1; + } + None => { + // Only tags1 has this tag. + tags_in_left_only.push((k1, v1)); + } + } + } + + // We've removed all the tags that they had in common, so the remaining entries in tags2 + // are ones that appear on the right only. + let tags_in_right_only = tags2; + let no_tags_in_common = same_tags_different_payloads.is_empty() && same_tags_same_payloads == 0; + let both = same_tags_different_payloads + .into_iter() + .map(to_overlap_docs); + + let any_tags_on_one_side_only = !tags_in_left_only.is_empty() || !tags_in_right_only.is_empty(); + + let mut left = tags_in_left_only + .iter() + .map(|(k, v)| to_unknown_docs((k, v))) + .peekable(); + let mut right = tags_in_right_only.iter().map(to_unknown_docs).peekable(); + + let status = match (ext_has_fixed_fields(&ext1), ext_has_fixed_fields(&ext2)) { + (false, false) => Status::Similar, + _ => match (left.peek(), right.peek()) { + // At least one tag appeared only on the left, and also + // at least one tag appeared only on the right. There's a chance this is + // because of a typo, so we'll suggest that as a hint. + (Some((f, _, _, _)), Some(_)) => Status::Different(vec![Problem::TagTypo( + f.clone(), + tags_in_right_only.keys().cloned().collect(), + )]), + // At least one tag appeared only on the left, but all of the tags + // on the right also appeared on the left. So at least one tag is missing. + (Some(_), None) => Status::Different(vec![Problem::TagsMissing( + left.clone().map(|v| v.0).collect(), + )]), + // At least one tag appeared only on the right, but all of the tags + // on the left also appeared on the right. So at least one tag is missing. + (None, Some(_)) => { + let status = + Status::Different(vec![Problem::TagsMissing(right.map(|v| v.0).collect())]); + right = tags_in_right_only.iter().map(to_unknown_docs).peekable(); + status + } + // Left and right have the same set of tag names (but may have different payloads). + (None, None) => Status::Similar, + }, + }; + + let ext1_is_open = matches!(&ext1, TypeExt::FlexOpen(_)); + let ext2_is_open = matches!(&ext2, TypeExt::FlexOpen(_)); + let ext_diff = tag_ext_to_diff(alloc, pol, ext1, ext2, &gen_usages1, &gen_usages2); + + let mut tags_diff: Diff, Vec>)>> = Diff { + status: Status::Similar, + left: Vec::new(), + right: Vec::new(), + left_able: Vec::new(), + right_able: Vec::new(), + }; + + for diff in both { + tags_diff.status.merge(diff.status); + tags_diff.left.push(diff.left); + tags_diff.right.push(diff.right); + tags_diff.left_able.extend(diff.left_able); + tags_diff.right_able.extend(diff.right_able); + } + + let left_tags_omitted; + let right_tags_omitted; + + if no_tags_in_common { + // If they have no tags in common, we shouldn't omit any tags, + // because that would result in an unhelpful diff of + // […] on one side and another […] on the other side! + + left_tags_omitted = 0; + right_tags_omitted = 0; + + for (tag, tag_doc, payload_vals, able) in left { + tags_diff.left.push((tag, tag_doc, payload_vals)); + tags_diff.left_able.extend(able); + } + + for (tag, tag_doc, payload_vals, able) in right { + tags_diff.right.push((tag, tag_doc, payload_vals)); + tags_diff.right_able.extend(able); + } + + tags_diff.status.merge(Status::Different(Vec::new())); + } else if any_tags_on_one_side_only { + // If either tag union is open but the other is not, then omit the tags in the other. + // + // In other words, if one tag union is a pattern match which has _ ->, + // don't list the tags which fall under that catch-all pattern because + // they won't be helpful. By omitting them, we'll only show the tags that + // are actually matched. + // + // We shouldn't do this if they're both open though, + // because that would result in an unhelpful diff of + // […] on one side and another […] on the other side! + if ext2_is_open && !ext1_is_open { + left_tags_omitted = same_tags_same_payloads + left.len(); + } else { + left_tags_omitted = same_tags_same_payloads; + + for (tag, tag_doc, args, able) in left { + tags_diff.left.push((tag, tag_doc, args)); + tags_diff.left_able.extend(able); + } + } + + if ext1_is_open && !ext2_is_open { + right_tags_omitted = same_tags_same_payloads + right.len(); + } else { + right_tags_omitted = same_tags_same_payloads; + + for (tag, tag_doc, args, able) in right { + tags_diff.right.push((tag, tag_doc, args)); + tags_diff.right_able.extend(able); + } + } + + tags_diff.status.merge(Status::Different(Vec::new())); + } else { + left_tags_omitted = same_tags_same_payloads; + right_tags_omitted = same_tags_same_payloads; + } + + tags_diff.left.sort_by(|a, b| a.0.cmp(&b.0)); + tags_diff.right.sort_by(|a, b| a.0.cmp(&b.0)); + + let lefts = tags_diff.left.into_iter().map(|(_, a, b)| (a, b)).collect(); + let rights = tags_diff + .right + .into_iter() + .map(|(_, a, b)| (a, b)) + .collect(); + + let doc1 = match rec1 { + None => report_text::tag_union(alloc, lefts, ext_diff.left, left_tags_omitted, None), + Some(rec) => { + let (rec_doc, able) = to_doc(alloc, Parens::Unnecessary, rec); + + tags_diff.left_able.extend(able); + + report_text::tag_union( + alloc, + lefts, + ext_diff.left, + left_tags_omitted, + Some(rec_doc), + ) + } + }; + let doc2 = match rec2 { + None => report_text::tag_union(alloc, rights, ext_diff.right, right_tags_omitted, None), + Some(rec) => { + let (rec_doc, able) = to_doc(alloc, Parens::Unnecessary, rec); + + tags_diff.right_able.extend(able); + + report_text::tag_union( + alloc, + rights, + ext_diff.right, + right_tags_omitted, + Some(rec_doc), + ) + } + }; + + tags_diff.status.merge(status); + + Diff { + left: doc1, + right: doc2, + status: tags_diff.status, + left_able: tags_diff.left_able, + right_able: tags_diff.right_able, + } +} + +fn should_show_payload_diff(errs1: &[ErrorType], errs2: &[ErrorType]) -> bool { + if errs1.len() == errs2.len() { + errs1 + .iter() + .zip(errs2.iter()) + .any(|(err1, err2)| should_show_diff(err1, err2)) + } else { + true + } +} + +fn tag_ext_to_diff<'b>( + alloc: &'b RocDocAllocator<'b>, + pol: Polarity, + ext1: TypeExt, + ext2: TypeExt, + gen_usages1: &VecMap, + gen_usages2: &VecMap, +) -> Diff>> { + let status = ext_to_status(&ext1, &ext2); + let ext_doc_1 = tag_ext_to_doc(alloc, pol, gen_usages1, ext1); + let ext_doc_2 = tag_ext_to_doc(alloc, pol, gen_usages2, ext2); + + match &status { + Status::Similar => Diff { + left: ext_doc_1, + right: ext_doc_2, + status, + left_able: Vec::new(), + right_able: Vec::new(), + }, + Status::Different(_) => Diff { + left: ext_doc_1, + right: ext_doc_2, + status, + left_able: Vec::new(), + right_able: Vec::new(), + }, + } +} + +fn record_ext_to_diff<'b>( + alloc: &'b RocDocAllocator<'b>, + ext1: TypeExt, + ext2: TypeExt, +) -> Diff>> { + let status = ext_to_status(&ext1, &ext2); + let ext_doc_1 = record_ext_to_doc(alloc, ext1); + let ext_doc_2 = record_ext_to_doc(alloc, ext2); + + match &status { + Status::Similar => Diff { + left: ext_doc_1, + right: ext_doc_2, + status, + left_able: vec![], + right_able: vec![], + }, + Status::Different(_) => Diff { + // NOTE elm colors these differently at this point + left: ext_doc_1, + right: ext_doc_2, + status, + left_able: vec![], + right_able: vec![], + }, + } +} + +fn ext_to_status(ext1: &TypeExt, ext2: &TypeExt) -> Status { + use TypeExt::*; + match ext1 { + Closed => match ext2 { + Closed => Status::Similar, + FlexOpen(_) => Status::Similar, + RigidOpen(_) => Status::Different(vec![]), + }, + FlexOpen(_) => Status::Similar, + + RigidOpen(x) => match ext2 { + Closed => Status::Different(vec![]), + FlexOpen(_) => Status::Similar, + RigidOpen(y) => { + if x == y { + Status::Similar + } else { + Status::Different(vec![Problem::BadRigidVar( + x.clone(), + ErrorType::RigidVar(y.clone()), + None, + )]) + } + } + }, + } +} + +mod report_text { + use crate::report::{Annotation, RocDocAllocator, RocDocBuilder}; + use roc_module::ident::Lowercase; + use roc_types::pretty_print::Parens; + use roc_types::types::{ErrorType, RecordField, TypeExt}; + use ven_pretty::DocAllocator; + + fn with_parens<'b>( + alloc: &'b RocDocAllocator<'b>, + text: RocDocBuilder<'b>, + ) -> RocDocBuilder<'b> { + alloc.text("(").append(text).append(alloc.text(")")) + } + + pub fn function<'b>( + alloc: &'b RocDocAllocator<'b>, + parens: Parens, + args: Vec>, + ret: RocDocBuilder<'b>, + ) -> RocDocBuilder<'b> { + let function_doc = alloc.concat([ + alloc.intersperse(args, alloc.reflow(", ")), + alloc.reflow(" -> "), + ret, + ]); + + match parens { + Parens::Unnecessary => function_doc, + _ => with_parens(alloc, function_doc), + } + } + + pub fn apply<'b>( + alloc: &'b RocDocAllocator<'b>, + parens: Parens, + name: RocDocBuilder<'b>, + args: Vec>, + ) -> RocDocBuilder<'b> { + if args.is_empty() { + name + } else { + let apply_doc = + alloc.concat([name, alloc.space(), alloc.intersperse(args, alloc.space())]); + + match parens { + Parens::Unnecessary | Parens::InFn => apply_doc, + Parens::InTypeParam => with_parens(alloc, apply_doc), + } + } + } + + pub fn record<'b>( + alloc: &'b RocDocAllocator<'b>, + entries: Vec<(RocDocBuilder<'b>, RecordField>)>, + opt_ext: Option>, + fields_omitted: usize, + ) -> RocDocBuilder<'b> { + let ext_doc = if let Some(t) = opt_ext { + t + } else { + alloc.nil() + }; + + let entry_to_doc = + |(field_name, field_type): (RocDocBuilder<'b>, RecordField>)| { + match field_type { + RecordField::Demanded(field) + | RecordField::Required(field) + | RecordField::RigidRequired(field) => { + field_name.append(alloc.text(" : ")).append(field) + } + RecordField::Optional(field) | RecordField::RigidOptional(field) => { + field_name.append(alloc.text(" ? ")).append(field) + } + } + }; + + if entries.is_empty() { + if fields_omitted == 0 { + alloc.text("{}") + } else { + alloc + .text("{ ") + .append(alloc.ellipsis().append(alloc.text(" }"))) + } + .append(ext_doc) + } else if entries.len() == 1 { + // Single-field records get printed on one line; multi-field records get multiple lines + alloc + .text("{ ") + .append(entry_to_doc(entries.into_iter().next().unwrap())) + .append(if fields_omitted == 0 { + alloc.text("") + } else { + alloc.text(", ").append(alloc.ellipsis()) + }) + .append(alloc.text(" }")) + .append(ext_doc) + } else { + let ending = if fields_omitted == 0 { + alloc.reflow("}") + } else { + alloc.vcat([ + alloc.ellipsis().indent(super::RECORD_FIELD_INDENT), + alloc.reflow("}"), + ]) + }; + + // Multi-field records get printed on multiple lines, as do records with fields omitted. + alloc + .vcat( + std::iter::once(alloc.reflow("{")).chain( + entries + .into_iter() + .map(|entry| { + entry_to_doc(entry) + .indent(super::RECORD_FIELD_INDENT) + .append(alloc.reflow(",")) + }) + .chain(std::iter::once(ending)), + ), + ) + .append(ext_doc) + } + } + + pub fn tuple<'b>( + alloc: &'b RocDocAllocator<'b>, + entries: Vec>, + opt_ext: Option>, + fields_omitted: usize, + ) -> RocDocBuilder<'b> { + let ext_doc = if let Some(t) = opt_ext { + t + } else { + alloc.nil() + }; + + if entries.is_empty() { + if fields_omitted == 0 { + alloc.text("()") + } else { + alloc + .text("(") + .append(alloc.ellipsis().append(alloc.text(")"))) + } + .append(ext_doc) + } else if entries.len() == 1 { + // Single-field records get printed on one line; multi-field records get multiple lines + alloc + .text("(") + .append(entries.into_iter().next().unwrap()) + .append(if fields_omitted == 0 { + alloc.text("") + } else { + alloc.text(", ").append(alloc.ellipsis()) + }) + .append(alloc.text(")")) + .append(ext_doc) + } else { + let ending = if fields_omitted == 0 { + alloc.reflow(")") + } else { + alloc.vcat([ + alloc.ellipsis().indent(super::RECORD_FIELD_INDENT), + alloc.reflow(")"), + ]) + } + .append(ext_doc); + + // Multi-elem tuples get printed on multiple lines + alloc.vcat( + std::iter::once(alloc.reflow("(")).chain( + entries + .into_iter() + .map(|entry| { + entry + .indent(super::RECORD_FIELD_INDENT) + .append(alloc.reflow(",")) + }) + .chain(std::iter::once(ending)), + ), + ) + } + } + + pub fn to_suggestion_record<'b>( + alloc: &'b RocDocAllocator<'b>, + f: (Lowercase, RecordField), + fs: Vec<(Lowercase, RecordField)>, + ext: TypeExt, + ) -> RocDocBuilder<'b> { + use crate::error::r#type::{record_ext_to_doc, to_doc}; + + let entry_to_doc = |(name, tipe): (Lowercase, RecordField)| { + ( + alloc.string(name.as_str().to_string()), + to_doc(alloc, Parens::Unnecessary, tipe.into_inner()).0, + ) + }; + + let mut selection = vec![f]; + selection.extend(fs); + + let fields = selection.into_iter().map(entry_to_doc).collect(); + + vertical_record(alloc, fields, record_ext_to_doc(alloc, ext)) + .annotate(Annotation::TypeBlock) + .indent(4) + } + + fn vertical_record<'b>( + alloc: &'b RocDocAllocator<'b>, + entries: Vec<(RocDocBuilder<'b>, RocDocBuilder<'b>)>, + opt_ext: Option>, + ) -> RocDocBuilder<'b> { + let fields = if entries.is_empty() { + alloc.text("{}") + } else { + const MAX_ENTRIES_TO_DISPLAY: usize = 4; + + let is_truncated = entries.len() > MAX_ENTRIES_TO_DISPLAY; + let entry_to_doc = + |(field_name, field_type): (RocDocBuilder<'b>, RocDocBuilder<'b>)| { + field_name + .indent(4) + .append(alloc.text(" : ")) + .append(field_type) + .append(alloc.text(",")) + }; + + let closing = std::iter::once(alloc.text("}")); + let fields = std::iter::once(alloc.reflow("{")).chain( + entries + .into_iter() + .map(entry_to_doc) + .take(MAX_ENTRIES_TO_DISPLAY), + ); + + if is_truncated { + alloc.vcat( + fields + .chain(std::iter::once(alloc.text("…").indent(4))) + .chain(closing), + ) + } else { + alloc.vcat(fields.chain(closing)) + } + }; + + match opt_ext { + Some(ext) => fields.append(ext), + None => fields, + } + } + + pub fn tag_union<'b>( + alloc: &'b RocDocAllocator<'b>, + entries: Vec<(RocDocBuilder<'b>, Vec>)>, + opt_ext: Option>, + tags_omitted: usize, + opt_rec: Option>, + ) -> RocDocBuilder<'b> { + let ext_doc = if let Some(t) = opt_ext { + t + } else { + alloc.nil() + }; + + let entry_to_doc = |(tag_name, arguments): (RocDocBuilder<'b>, Vec<_>)| { + if arguments.is_empty() { + tag_name + } else { + tag_name + .append(alloc.space()) + .append(alloc.intersperse(arguments, alloc.space())) + } + }; + + let without_rec = if entries.is_empty() { + if tags_omitted == 0 { + alloc.text("[]") + } else { + alloc + .text("[") + .append(alloc.ellipsis().append(alloc.text("]"))) + } + .append(ext_doc) + } else if entries.len() == 1 { + // Single-tag unions get printed on one line; multi-tag unions get multiple lines + alloc + .text("[") + .append(entry_to_doc(entries.into_iter().next().unwrap())) + .append(if tags_omitted == 0 { + alloc.text("") + } else { + alloc.text(", ").append(alloc.ellipsis()) + }) + .append(alloc.text("]")) + .append(ext_doc) + } else { + let ending = if tags_omitted == 0 { + alloc.reflow("]") + } else { + alloc.vcat([ + alloc.ellipsis().indent(super::TAG_INDENT), + alloc.reflow("]"), + ]) + }; + + // Multi-tag unions get printed on multiple lines, as do tag unions with tags omitted. + alloc + .vcat( + std::iter::once(alloc.reflow("[")).chain( + entries + .into_iter() + .map(|entry| { + entry_to_doc(entry) + .indent(super::TAG_INDENT) + .append(alloc.reflow(",")) + }) + .chain(std::iter::once(ending)), + ), + ) + .append(ext_doc) + }; + + match opt_rec { + Some(rec) => without_rec.append(alloc.text(" as ")).append(rec), + None => without_rec, + } + } + + pub fn range<'b>( + alloc: &'b RocDocAllocator<'b>, + ranged_types: Vec>, + ) -> RocDocBuilder<'b> { + let mut doc = Vec::with_capacity(ranged_types.len() * 2); + + let last = ranged_types.len() - 1; + for (i, choice) in ranged_types.into_iter().enumerate() { + if i == last && i == 1 { + doc.push(alloc.reflow(" or ")); + } else if i == last && i > 1 { + doc.push(alloc.reflow(", or ")); + } else if i > 0 { + doc.push(alloc.reflow(", ")); + } + + doc.push(choice); + } + + alloc.concat(doc) + } +} + +fn list_abilities<'a>(alloc: &'a RocDocAllocator<'a>, abilities: &AbilitySet) -> RocDocBuilder<'a> { + let mut abilities = abilities.sorted_iter(); + if abilities.len() == 1 { + alloc.concat([ + alloc.reflow("ability "), + alloc.symbol_unqualified(*abilities.next().unwrap()), + ]) + } else if abilities.len() == 2 { + alloc.concat([ + alloc.reflow("abilities "), + alloc.symbol_unqualified(*abilities.next().unwrap()), + alloc.reflow(" and "), + alloc.symbol_unqualified(*abilities.next().unwrap()), + ]) + } else { + let last_ability = abilities.len() - 1; + + alloc.concat([ + alloc.reflow("abilities "), + alloc.intersperse( + abilities.enumerate().map(|(i, &ab)| { + if i == last_ability { + alloc.concat([alloc.reflow(" and "), alloc.symbol_unqualified(ab)]) + } else { + alloc.symbol_unqualified(ab) + } + }), + alloc.reflow(", "), + ), + alloc.reflow(" abilities"), + ]) + } +} + +fn type_problem_to_pretty<'b>( + alloc: &'b RocDocAllocator<'b>, + problem: crate::error::r#type::Problem, + expectation_context: ExpectationContext<'b>, +) -> RocDocBuilder<'b> { + use crate::error::r#type::Problem::*; + + match (problem, expectation_context) { + (FieldTypo(typo, possibilities), _) => { + let suggestions = suggest::sort(typo.as_str(), possibilities); + + match suggestions.get(0) { + None => alloc.nil(), + Some(nearest) => { + let typo_str = format!("{typo}"); + let nearest_str = format!("{nearest}"); + + let found = alloc.text(typo_str).annotate(Annotation::Typo); + let suggestion = alloc.text(nearest_str).annotate(Annotation::TypoSuggestion); + + let tip1 = alloc + .tip() + .append(alloc.reflow("Seems like a record field typo. Maybe ")) + .append(found) + .append(alloc.reflow(" should be ")) + .append(suggestion) + .append(alloc.text("?")); + + let tip2 = alloc.tip().append(alloc.reflow(ADD_ANNOTATIONS)); + + tip1.append(alloc.line()).append(alloc.line()).append(tip2) + } + } + } + (FieldsMissing(missing), _) => match missing.split_last() { + None => alloc.nil(), + Some((f1, [])) => alloc + .tip() + .append(alloc.reflow("Looks like the ")) + .append(f1.as_str().to_owned()) + .append(alloc.reflow(" field is missing.")), + Some((last, init)) => { + let separator = alloc.reflow(", "); + + alloc + .tip() + .append(alloc.reflow("Looks like the ")) + .append( + alloc.intersperse(init.iter().map(|v| v.as_str().to_owned()), separator), + ) + .append(alloc.reflow(" and ")) + .append(alloc.text(last.as_str().to_owned())) + .append(alloc.reflow(" fields are missing.")) + } + }, + (TagTypo(typo, possibilities_tn), _) => { + let possibilities: Vec = possibilities_tn + .into_iter() + .map(|tag_name| tag_name.as_ident_str()) + .collect(); + let typo_str = format!("{}", typo.as_ident_str()); + let suggestions = suggest::sort(&typo_str, possibilities); + + match suggestions.get(0) { + None => alloc.nil(), + Some(nearest) => { + let nearest_str = format!("{nearest}"); + + let found = alloc.text(typo_str).annotate(Annotation::Typo); + let suggestion = alloc.text(nearest_str).annotate(Annotation::TypoSuggestion); + + let tip1 = alloc + .tip() + .append(alloc.reflow("Seems like a tag typo. Maybe ")) + .append(found) + .append(" should be ") + .append(suggestion) + .append(alloc.text("?")); + + let tip2 = alloc.tip().append(alloc.reflow(ADD_ANNOTATIONS)); + + tip1.append(alloc.line()).append(alloc.line()).append(tip2) + } + } + } + (ArityMismatch(found, expected), _) => { + let line = if found < expected { + format!( + "It looks like it takes too few arguments. I was expecting {} more.", + expected - found + ) + } else { + format!( + "It looks like it takes too many arguments. I'm seeing {} extra.", + found - expected + ) + }; + + alloc.tip().append(line) + } + + (BadRigidVar(x, tipe, Some(abilities)), expectation) => { + use ErrorType::*; + + let rigid_able_vs_concrete = |name: Lowercase, a_thing| { + alloc.stack([ + alloc + .note("") + .append(alloc.reflow("The type variable ")) + .append(alloc.type_variable(name.clone())) + .append(alloc.reflow(" says it can take on any value that implements the ")) + .append(list_abilities(alloc, &abilities)) + .append(alloc.reflow(".")), + alloc.concat([ + alloc.reflow("But, I see that the type is only ever used as a "), + a_thing, + alloc.reflow(". Can you replace "), + alloc.type_variable(name), + alloc.reflow(" with a more specific type?"), + ]), + ]) + }; + + let rigid_able_vs_different_flex_able = + |name: Lowercase, abilities: AbilitySet, other_abilities: AbilitySet| { + let extra_abilities = other_abilities + .into_sorted_iter() + .filter(|ability| !abilities.contains(ability)) + .collect::(); + + let type_var_doc = match expectation { + ExpectationContext::Annotation { on } => alloc.concat([ + alloc.reflow("The type annotation "), + on, + alloc.reflow(" says that the type variable "), + alloc.type_variable(name.clone()), + ]), + ExpectationContext::WhenCondition | ExpectationContext::Arbitrary => alloc + .concat([ + alloc.reflow("The type variable "), + alloc.type_variable(name.clone()), + alloc.reflow(" says it"), + ]), + }; + + let n_extra_abilities = extra_abilities.sorted_iter().len(); + + alloc.stack([ + alloc + .note("") + .append(type_var_doc) + .append( + alloc.reflow(" can take on any value that implements only the "), + ) + .append(list_abilities(alloc, &abilities)) + .append(alloc.reflow(".")), + alloc.concat([ + alloc.reflow("But, I see that it's also used as if it implements the "), + list_abilities(alloc, &extra_abilities), + alloc.reflow(". Can you use "), + alloc.type_variable(name.clone()), + alloc.reflow(" without "), + if n_extra_abilities > 1 { + alloc.reflow("those abilities") + } else { + alloc.reflow("that ability") + }, + alloc.reflow("? If not, consider adding "), + if n_extra_abilities > 1 { + alloc.reflow("them") + } else { + alloc.reflow("it") + }, + alloc.reflow(" to the "), + alloc.keyword(roc_parse::keyword::IMPLEMENTS), + alloc.reflow(" clause of "), + alloc.type_variable(name), + alloc.reflow("."), + ]), + ]) + }; + + let bad_double_rigid = |a: Lowercase, b: Lowercase| { + alloc + .tip() + .append(alloc.reflow("Your type annotation uses ")) + .append(alloc.type_variable(a)) + .append(alloc.reflow(" and ")) + .append(alloc.type_variable(b)) + .append(alloc.reflow(" as separate type variables. Your code seems to be saying they are the same though. Maybe they should be the same in your type annotation? Maybe your code uses them in a weird way?")) + }; + + match tipe { + Infinite | Error | FlexVar(_) => alloc.nil(), + FlexAbleVar(_, other_abilities) => { + rigid_able_vs_different_flex_able(x, abilities, other_abilities) + } + RigidVar(y) | RigidAbleVar(y, _) => bad_double_rigid(x, y), + Function(_, _, _) => rigid_able_vs_concrete(x, alloc.reflow("a function value")), + Record(_, _) => rigid_able_vs_concrete(x, alloc.reflow("a record value")), + Tuple(_, _) => rigid_able_vs_concrete(x, alloc.reflow("a tuple value")), + TagUnion(_, _, _) | RecursiveTagUnion(_, _, _, _) => { + rigid_able_vs_concrete(x, alloc.reflow("a tag value")) + } + Alias(symbol, _, _, _) | Type(symbol, _) => rigid_able_vs_concrete( + x, + alloc.concat([ + alloc.reflow("a "), + alloc.symbol_unqualified(symbol), + alloc.reflow(" value"), + ]), + ), + Range(..) => rigid_able_vs_concrete(x, alloc.reflow("a range")), + } + } + + (BadRigidVar(x, tipe, None), expectation) => { + use ErrorType::*; + + let bad_rigid_var = |name: Lowercase, a_thing| { + alloc + .tip() + .append(alloc.reflow("The type annotation uses the type variable ")) + .append(alloc.type_variable(name)) + .append(alloc.reflow(" to say that this definition can produce any type of value.") + .append(alloc.reflow(" But in the body I see that it will only produce "))) + .append(a_thing) + .append(alloc.reflow(" of a single specific type. Maybe change the type annotation to be more specific? Maybe change the code to be more general?")) + }; + + let bad_double_wildcard = || { + let mut hints_lines = vec![ + alloc.reflow( + "Any connection between types must use a named type variable, not a ", + ), + alloc.type_variable(WILDCARD.into()), + alloc.reflow("!"), + ]; + if let ExpectationContext::Annotation { on } = expectation { + hints_lines.append(&mut vec![ + alloc.reflow(" Maybe the annotation "), + on, + alloc.reflow(" should have a named type variable in place of the "), + alloc.type_variable(WILDCARD.into()), + alloc.reflow("?"), + ]); + } + alloc.tip().append(alloc.concat(hints_lines)) + }; + + let bad_double_rigid = |a: Lowercase, b: Lowercase| { + if a.as_str() == WILDCARD && b.as_str() == WILDCARD { + return bad_double_wildcard(); + } + let line = r#" as separate type variables. Your code seems to be saying they are the same though. Maybe they should be the same in your type annotation? Maybe your code uses them in a weird way?"#; + + alloc + .tip() + .append(alloc.reflow("Your type annotation uses ")) + .append(alloc.type_variable(a)) + .append(alloc.reflow(" and ")) + .append(alloc.type_variable(b)) + .append(alloc.reflow(line)) + }; + + match tipe { + Infinite | Error | FlexVar(_) => alloc.nil(), + FlexAbleVar(_, abilities) => { + let mut abilities = abilities.into_sorted_iter(); + let msg = if abilities.len() == 1 { + alloc.concat([ + alloc.reflow("an instance of the ability "), + alloc.symbol_unqualified(abilities.next().unwrap()), + ]) + } else { + alloc.concat([ + alloc.reflow("an instance of the "), + alloc.intersperse( + abilities.map(|ab| alloc.symbol_unqualified(ab)), + alloc.reflow(", "), + ), + alloc.reflow(" abilities"), + ]) + }; + bad_rigid_var(x, msg) + } + RigidVar(y) | RigidAbleVar(y, _) => bad_double_rigid(x, y), + Function(_, _, _) => bad_rigid_var(x, alloc.reflow("a function value")), + Record(_, _) => bad_rigid_var(x, alloc.reflow("a record value")), + Tuple(_, _) => bad_rigid_var(x, alloc.reflow("a tuple value")), + TagUnion(_, _, _) | RecursiveTagUnion(_, _, _, _) => { + bad_rigid_var(x, alloc.reflow("a tag value")) + } + Alias(symbol, _, _, _) | Type(symbol, _) => bad_rigid_var( + x, + alloc.concat([ + alloc.reflow("a "), + alloc.symbol_unqualified(symbol), + alloc.reflow(" value"), + ]), + ), + Range(..) => bad_rigid_var(x, alloc.reflow("a range")), + } + } + + (IntFloat, _) => { + alloc.tip().append(alloc.concat( + [ + alloc.reflow( + "You can convert between integers and fractions using functions like ", + ), + alloc.symbol_qualified(Symbol::NUM_TO_FRAC), + alloc.reflow(" and "), + alloc.symbol_qualified(Symbol::NUM_ROUND), + alloc.reflow("."), + ], + )) + } + + (TagsMissing(missing), ExpectationContext::WhenCondition) => match missing.split_last() { + None => alloc.nil(), + Some(split) => { + let missing_tags = match split { + (f1, []) => alloc.tag_name(f1.clone()).append(alloc.reflow(" tag.")), + (last, init) => alloc + .intersperse(init.iter().map(|v| alloc.tag_name(v.clone())), ", ") + .append(alloc.reflow(" and ")) + .append(alloc.tag_name(last.clone())) + .append(alloc.reflow(" tags.")), + }; + + let tip1 = alloc + .tip() + .append(alloc.reflow("Looks like the branches are missing coverage of the ")) + .append(missing_tags); + + let tip2 = alloc + .tip() + .append(alloc.reflow("Maybe you need to add a catch-all branch, like ")) + .append(alloc.keyword("_")) + .append(alloc.reflow("?")); + + alloc.stack([tip1, tip2]) + } + }, + + (TagsMissing(missing), _) => match missing.split_last() { + None => alloc.nil(), + Some((f1, [])) => { + let tip1 = alloc + .tip() + .append(alloc.reflow("Looks like a closed tag union does not have the ")) + .append(alloc.tag_name(f1.clone())) + .append(alloc.reflow(" tag.")); + + let tip2 = alloc.tip().append(alloc.reflow( + "Closed tag unions can't grow, \ + because that might change the size in memory. \ + Can you use an open tag union?", + )); + + alloc.stack([tip1, tip2]) + } + + Some((last, init)) => { + let separator = alloc.reflow(", "); + + let tip1 = alloc + .tip() + .append(alloc.reflow("Looks like a closed tag union does not have the ")) + .append( + alloc + .intersperse(init.iter().map(|v| alloc.tag_name(v.clone())), separator), + ) + .append(alloc.reflow(" and ")) + .append(alloc.tag_name(last.clone())) + .append(alloc.reflow(" tags.")); + + let tip2 = alloc.tip().append(alloc.reflow( + "Closed tag unions can't grow, \ + because that might change the size in memory. \ + Can you use an open tag union?", + )); + + alloc.stack([tip1, tip2]) + } + }, + (OptionalRequiredMismatch(field), _) => alloc.tip().append(alloc.concat([ + alloc.reflow("To extract the "), + alloc.record_field(field), + alloc.reflow( + " field it must be non-optional, but the type says this field is optional. ", + ), + alloc.reflow("Learn more about optional fields at TODO."), + ])), + + (OpaqueComparedToNonOpaque, _) => alloc.tip().append(alloc.concat([ + alloc.reflow( + "Type comparisons between an opaque type are only ever \ + equal if both types are the same opaque type. Did you mean \ + to create an opaque type by wrapping it? If I have an opaque type ", + ), + alloc.type_str("Age := U32"), + alloc.reflow(" I can create an instance of this opaque type by doing "), + alloc.type_str("@Age 23"), + alloc.reflow("."), + ])), + + (BoolVsBoolTag(tag), _) => alloc.tip().append(alloc.concat([ + alloc.reflow("Did you mean to use "), + alloc.symbol_qualified(if tag.0.as_str() == "True" { + Symbol::BOOL_TRUE + } else { + Symbol::BOOL_FALSE + }), + alloc.reflow(" rather than "), + alloc.tag_name(tag), + alloc.reflow("?"), + ])), + } +} + +fn report_record_field_typo<'b>( + alloc: &'b RocDocAllocator<'b>, + lines: &LineInfo, + filename: PathBuf, + severity: Severity, + opt_sym: Option, + field_prefix: &str, + field: &Lowercase, + field_suffix: &str, + field_region: Region, + actual_fields: SendMap>, + ext: TypeExt, +) -> Report<'b> { + let header = { + let f_doc = alloc + .text(field.as_str().to_string()) + .annotate(Annotation::Typo); + + let r_doc = match opt_sym { + Some(symbol) => alloc.symbol_unqualified(symbol).append(" "), + None => alloc.text(""), + }; + + alloc.concat([ + alloc.reflow("This "), + r_doc, + alloc.reflow("record doesn’t have a "), + f_doc, + alloc.reflow(" field:"), + ]) + }; + + let mut suggestions = suggest::sort( + field.as_str(), + actual_fields.into_iter().collect::>(), + ); + + let doc = alloc.stack([ + header, + alloc.region(lines.convert_region(field_region)), + if suggestions.is_empty() { + let r_doc = match opt_sym { + Some(symbol) => alloc.symbol_unqualified(symbol).append(" is"), + None => alloc.text("it’s"), + }; + alloc.concat([ + alloc.reflow("In fact, "), + r_doc, + alloc.reflow(" a record with no fields at all!"), + ]) + } else { + let f = suggestions.remove(0); + let fs = suggestions; + let f_doc = text!(alloc, "{}{}{}", field_prefix, field, field_suffix) + .annotate(Annotation::Typo); + + let r_doc = match opt_sym { + Some(symbol) => alloc.symbol_unqualified(symbol).append(" fields"), + None => alloc.text("fields on the record"), + }; + + alloc.stack([ + alloc.concat([ + alloc.reflow("There may be a typo. These "), + r_doc, + alloc.reflow(" are the most similar:"), + ]), + report_text::to_suggestion_record(alloc, f.clone(), fs, ext), + alloc.concat([ + alloc.reflow("Maybe "), + f_doc, + alloc.reflow(" should be "), + text!(alloc, "{}{}{}", field_prefix, f.0, field_suffix) + .annotate(Annotation::TypoSuggestion), + alloc.reflow(" instead?"), + ]), + ]) + }, + ]); + + Report { + filename, + title: "TYPE MISMATCH".to_string(), + doc, + severity, + } +} + +fn exhaustive_problem<'a>( + alloc: &'a RocDocAllocator<'a>, + lines: &LineInfo, + filename: PathBuf, + problem: roc_exhaustive::Error, +) -> Report<'a> { + use roc_exhaustive::Context::*; + use roc_exhaustive::Error::*; + + let severity = problem.severity(); + + match problem { + Incomplete(region, context, missing) => match context { + BadArg => { + let doc = alloc.stack([ + alloc.reflow("This pattern does not cover all the possibilities:"), + alloc.region(lines.convert_region(region)), + alloc.reflow("Other possibilities include:"), + unhandled_patterns_to_doc_block(alloc, missing), + alloc.concat([ + alloc.reflow( + "I would have to crash if I saw one of those! \ + So rather than pattern matching in function arguments, put a ", + ), + alloc.keyword("when"), + alloc.reflow(" in the function body to account for all possibilities."), + ]), + ]); + + Report { + filename, + title: "UNSAFE PATTERN".to_string(), + doc, + severity, + } + } + BadDestruct => { + let doc = alloc.stack([ + alloc.reflow("This pattern does not cover all the possibilities:"), + alloc.region(lines.convert_region(region)), + alloc.reflow("Other possibilities include:"), + unhandled_patterns_to_doc_block(alloc, missing), + alloc.concat([ + alloc.reflow( + "I would have to crash if I saw one of those! \ + You can use a binding to deconstruct a value if there is only ONE possibility. \ + Use a " + ), + alloc.keyword("when"), + alloc.reflow(" to account for all possibilities."), + ]), + ]); + + Report { + filename, + title: "UNSAFE PATTERN".to_string(), + doc, + severity, + } + } + BadCase => { + let doc = alloc.stack([ + alloc.concat([ + alloc.reflow("This "), + alloc.keyword("when"), + alloc.reflow(" does not cover all the possibilities:"), + ]), + alloc.region(lines.convert_region(region)), + alloc.reflow("Other possibilities include:"), + unhandled_patterns_to_doc_block(alloc, missing), + alloc.reflow( + "I would have to crash if I saw one of those! \ + Add branches for them!", + ), + // alloc.hint().append(alloc.reflow("or use a hole.")), + ]); + + Report { + filename, + title: "UNSAFE PATTERN".to_string(), + doc, + severity, + } + } + }, + Redundant { + overall_region, + branch_region, + index, + } => { + let doc = alloc.stack([ + alloc.concat([ + alloc.reflow("The "), + alloc.string(index.ordinal()), + alloc.reflow(" pattern is redundant:"), + ]), + alloc.region_with_subregion( + lines.convert_region(overall_region), + lines.convert_region(branch_region), + ), + alloc.reflow( + "Any value of this shape will be handled by \ + a previous pattern, so this one should be removed.", + ), + ]); + + Report { + filename, + title: "REDUNDANT PATTERN".to_string(), + doc, + severity, + } + } + Unmatchable { + overall_region, + branch_region, + index, + } => { + let doc = alloc.stack([ + alloc.concat([ + alloc.reflow("The "), + alloc.string(index.ordinal()), + alloc.reflow(" pattern will never be matched:"), + ]), + alloc.region_with_subregion( + lines.convert_region(overall_region), + lines.convert_region(branch_region), + ), + alloc.reflow( + "It's impossible to create a value of this shape, \ + so this pattern can be safely removed!", + ), + ]); + + Report { + filename, + title: "UNMATCHABLE PATTERN".to_string(), + doc, + severity, + } + } + } +} + +pub fn unhandled_patterns_to_doc_block<'b>( + alloc: &'b RocDocAllocator<'b>, + patterns: Vec, +) -> RocDocBuilder<'b> { + alloc + .vcat( + patterns + .into_iter() + .map(|v| exhaustive_pattern_to_doc(alloc, v)), + ) + .indent(4) + .annotate(Annotation::TypeBlock) +} + +fn exhaustive_pattern_to_doc<'b>( + alloc: &'b RocDocAllocator<'b>, + pattern: roc_exhaustive::Pattern, +) -> RocDocBuilder<'b> { + pattern_to_doc_help(alloc, pattern, false) +} + +const AFTER_TAG_INDENT: &str = " "; +const TAG_INDENT: usize = 4; +const RECORD_FIELD_INDENT: usize = 4; + +fn pattern_to_doc_help<'b>( + alloc: &'b RocDocAllocator<'b>, + pattern: roc_exhaustive::Pattern, + in_type_param: bool, +) -> RocDocBuilder<'b> { + use roc_can::exhaustive::{GUARD_CTOR, NONEXHAUSIVE_CTOR}; + use roc_exhaustive::Literal::*; + use roc_exhaustive::Pattern::*; + use roc_exhaustive::RenderAs; + + match pattern { + Anything => alloc.text("_"), + Literal(l) => match l { + Int(i) => alloc.text(i128::from_ne_bytes(i).to_string()), + U128(i) => alloc.text(u128::from_ne_bytes(i).to_string()), + Bit(true) => alloc.text("Bool.true"), + Bit(false) => alloc.text("Bool.false"), + Byte(b) => alloc.text(b.to_string()), + Float(f) => alloc.text(f.to_string()), + Decimal(d) => alloc.text(RocDec::from_ne_bytes(d).to_string()), + Str(s) => alloc.string(s.into()), + }, + List(arity, patterns) => { + let inner = match arity { + ListArity::Exact(_) => alloc.intersperse( + patterns + .into_iter() + .map(|p| pattern_to_doc_help(alloc, p, false)), + alloc.text(",").append(alloc.space()), + ), + ListArity::Slice(num_before, num_after) => { + let mut all_patterns = patterns + .into_iter() + .map(|p| pattern_to_doc_help(alloc, p, in_type_param)); + + let spread = alloc.text(".."); + let comma_space = alloc.text(",").append(alloc.space()); + + let mut list = alloc.intersperse( + all_patterns.by_ref().take(num_before).chain([spread]), + comma_space.clone(), + ); + + if num_after > 0 { + let after = all_patterns; + list = alloc.intersperse([list].into_iter().chain(after), comma_space); + } + + list + } + }; + alloc.concat([alloc.text("["), inner, alloc.text("]")]) + } + Ctor(union, tag_id, args) => { + match union.render_as { + RenderAs::Guard => { + // #Guard + debug_assert!(union.alternatives[tag_id.0 as usize] + .name + .is_tag(&TagName(GUARD_CTOR.into()))); + debug_assert!(args.len() == 2); + let tag = pattern_to_doc_help(alloc, args[1].clone(), in_type_param); + alloc.concat([ + tag, + alloc.text(AFTER_TAG_INDENT), + alloc.text("(note the lack of an "), + alloc.keyword("if"), + alloc.text(" clause)"), + ]) + } + RenderAs::Record(field_names) => { + let mut arg_docs = Vec::with_capacity(args.len()); + + for (label, v) in field_names.into_iter().zip(args.into_iter()) { + match &v { + Anything => { + arg_docs.push(alloc.text(label.to_string())); + } + Literal(_) | Ctor(_, _, _) | List(..) => { + arg_docs.push( + alloc + .text(label.to_string()) + .append(alloc.reflow(": ")) + .append(pattern_to_doc_help(alloc, v, false)), + ); + } + } + } + + alloc + .text("{ ") + .append(alloc.intersperse(arg_docs, alloc.reflow(", "))) + .append(" }") + } + RenderAs::Tuple => { + let mut arg_docs = Vec::with_capacity(args.len()); + + for v in args.into_iter() { + arg_docs.push(pattern_to_doc_help(alloc, v, false)); + } + + alloc + .text("( ") + .append(alloc.intersperse(arg_docs, alloc.reflow(", "))) + .append(" )") + } + RenderAs::Tag | RenderAs::Opaque => { + let ctor = &union.alternatives[tag_id.0 as usize]; + match &ctor.name { + CtorName::Tag(TagName(name)) if name.as_str() == NONEXHAUSIVE_CTOR => { + return pattern_to_doc_help( + alloc, + roc_exhaustive::Pattern::Anything, + in_type_param, + ) + } + _ => {} + } + + let tag_name = match (union.render_as, &ctor.name) { + (RenderAs::Tag, CtorName::Tag(tag)) => alloc.tag_name(tag.clone()), + (RenderAs::Opaque, CtorName::Opaque(opaque)) => { + alloc.wrapped_opaque_name(*opaque) + } + _ => unreachable!(), + }; + + let has_args = !args.is_empty(); + let arg_docs = args + .into_iter() + .map(|v| pattern_to_doc_help(alloc, v, true)); + + // We assume the alternatives are sorted. If not, this assert will trigger + debug_assert!(tag_id == ctor.tag_id); + + let docs = std::iter::once(tag_name).chain(arg_docs); + + if in_type_param && has_args { + alloc + .text("(") + .append(alloc.intersperse(docs, alloc.space())) + .append(")") + } else { + alloc.intersperse(docs, alloc.space()) + } + } + } + } + } +} diff --git a/crates/reporting/src/lib.rs b/crates/reporting/src/lib.rs new file mode 100644 index 0000000000..7e24446664 --- /dev/null +++ b/crates/reporting/src/lib.rs @@ -0,0 +1,8 @@ +//! Responsible for generating warning and error messages. +#![warn(clippy::dbg_macro)] +// See github.com/roc-lang/roc/issues/800 for discussion of the large_enum_variant check. +#![allow(clippy::large_enum_variant)] + +pub mod cli; +pub mod error; +pub mod report; diff --git a/crates/reporting/src/report.rs b/crates/reporting/src/report.rs new file mode 100644 index 0000000000..cb86c5eac7 --- /dev/null +++ b/crates/reporting/src/report.rs @@ -0,0 +1,1651 @@ +use roc_module::ident::Ident; +use roc_module::ident::{Lowercase, ModuleName, TagName, Uppercase}; +use roc_module::symbol::{Interns, ModuleId, ModuleIds, PQModuleName, PackageQualified, Symbol}; +use roc_problem::Severity; +use roc_region::all::LineColumnRegion; +use std::path::{Path, PathBuf}; +use std::{fmt, io}; +use ven_pretty::{text, BoxAllocator, DocAllocator, DocBuilder, Render, RenderAnnotated}; + +#[cfg(not(target_family = "wasm"))] +use byte_unit::Byte; +#[cfg(not(target_family = "wasm"))] +use roc_packaging::https::Problem; + +pub use crate::error::canonicalize::can_problem; +pub use crate::error::parse::parse_problem; +pub use crate::error::r#type::type_problem; + +#[cfg(windows)] +const CYCLE_ELEMENTS: [&str; 4] = ["+-----+", "| ", "| |", "+-<---+"]; + +#[cfg(not(windows))] +const CYCLE_ELEMENTS: [&str; 4] = ["┌─────┐", "│ ", "│ ↓", "└─────┘"]; + +const CYCLE_TOP: &str = CYCLE_ELEMENTS[0]; +const CYCLE_LN: &str = CYCLE_ELEMENTS[1]; +const CYCLE_MID: &str = CYCLE_ELEMENTS[2]; +const CYCLE_END: &str = CYCLE_ELEMENTS[3]; + +const GUTTER_BAR: &str = "│"; +const ERROR_UNDERLINE: &str = "^"; + +/// The number of monospace spaces the gutter bar takes up. +/// (This is not necessarily the same as GUTTER_BAR.len()!) +const GUTTER_BAR_WIDTH: usize = 1; + +pub fn cycle<'b>( + alloc: &'b RocDocAllocator<'b>, + indent: usize, + name: RocDocBuilder<'b>, + names: Vec>, +) -> RocDocBuilder<'b> { + let mut lines = Vec::with_capacity(4 + (2 * names.len() - 1)); + + lines.push(alloc.text(CYCLE_TOP)); + + lines.push(alloc.text(CYCLE_LN).append(name)); + lines.push(alloc.text(CYCLE_MID)); + + let mut it = names.into_iter().peekable(); + + while let Some(other_name) = it.next() { + lines.push(alloc.text(CYCLE_LN).append(other_name)); + + if it.peek().is_some() { + lines.push(alloc.text(CYCLE_MID)); + } + } + + lines.push(alloc.text(CYCLE_END)); + + alloc + .vcat(lines) + .indent(indent) + .annotate(Annotation::TypeBlock) +} + +const HEADER_WIDTH: usize = 80; + +pub fn pretty_header(title: &str) -> String { + let title_width = title.len() + 4; + let header = format!("── {} {}", title, "─".repeat(HEADER_WIDTH - title_width)); + header +} + +pub fn pretty_header_with_path(title: &str, path: &Path) -> String { + let cwd = std::env::current_dir().unwrap(); + let relative_path = match path.strip_prefix(cwd) { + Ok(p) => p, + _ => path, + } + .to_str() + .unwrap(); + + let title_width = title.len() + 4; + let relative_path_width = relative_path.len() + 3; + let available_path_width = HEADER_WIDTH - title_width - 1; + + // If path is too long to fit in 80 characters with everything else then truncate it + let path_width = relative_path_width.min(available_path_width); + let path_trim = relative_path_width - path_width; + let path = if path_trim > 0 { + format!("...{}", &relative_path[(path_trim + 3)..]) + } else { + relative_path.to_string() + }; + + let header = format!( + "── {} {} {} ─", + title, + "─".repeat(HEADER_WIDTH - (title_width + path_width)), + path + ); + + header +} + +#[derive(Clone, Copy, Debug)] +pub enum RenderTarget { + ColorTerminal, + Generic, +} + +/// A textual report. +pub struct Report<'b> { + pub title: String, + pub filename: PathBuf, + pub doc: RocDocBuilder<'b>, + pub severity: Severity, +} + +impl<'b> Report<'b> { + pub fn render( + self, + target: RenderTarget, + buf: &'b mut String, + alloc: &'b RocDocAllocator<'b>, + palette: &'b Palette, + ) { + match target { + RenderTarget::Generic => self.render_ci(buf, alloc), + RenderTarget::ColorTerminal => self.render_color_terminal(buf, alloc, palette), + } + } + + /// Render to CI console output, where no colors are available. + pub fn render_ci(self, buf: &mut String, alloc: &'b RocDocAllocator<'b>) { + let err_msg = ""; + + self.pretty(alloc) + .1 + .render_raw(70, &mut CiWrite::new(buf)) + .expect(err_msg); + } + + /// Render to a color terminal using ANSI escape sequences, + /// or to the web REPL, using HTML tags. + pub fn render_color_terminal( + self, + buf: &mut String, + alloc: &'b RocDocAllocator<'b>, + palette: &'b Palette, + ) { + let err_msg = ""; + + self.pretty(alloc) + .1 + .render_raw(70, &mut ColorWrite::new(palette, buf)) + .expect(err_msg); + } + + pub fn pretty(self, alloc: &'b RocDocAllocator<'b>) -> RocDocBuilder<'b> { + if self.title.is_empty() { + self.doc + } else { + let header = if self.filename == PathBuf::from("") { + crate::report::pretty_header(&self.title) + } else { + crate::report::pretty_header_with_path(&self.title, &self.filename) + }; + + alloc.stack([alloc.text(header).annotate(Annotation::Header), self.doc]) + } + } + + pub fn horizontal_rule(palette: &'b Palette) -> String { + format!("{}{}", palette.header, "─".repeat(80)) + } +} + +/// This struct is a combination of several things +/// 1. A set of StyleCodes suitable for the environment we're running in (web or terminal) +/// 2. A set of colors we decided to use +/// 3. A mapping from UI elements to the styles we use for them +/// Note: This should really be called Theme! Usually a "palette" is just (2). +#[derive(Debug, Clone, Copy)] +pub struct Palette { + pub primary: &'static str, + pub code_block: &'static str, + pub keyword: &'static str, + pub ellipsis: &'static str, + pub variable: &'static str, + pub type_variable: &'static str, + pub structure: &'static str, + pub alias: &'static str, + pub opaque: &'static str, + pub error: &'static str, + pub line_number: &'static str, + pub header: &'static str, + pub gutter_bar: &'static str, + pub module_name: &'static str, + pub binop: &'static str, + pub typo: &'static str, + pub typo_suggestion: &'static str, + pub parser_suggestion: &'static str, + pub bold: &'static str, + pub underline: &'static str, + pub reset: &'static str, +} + +/// Set the default styles for various semantic elements, +/// given a set of StyleCodes for an environment (web or terminal). +const fn default_palette_from_style_codes(codes: StyleCodes) -> Palette { + Palette { + primary: codes.white, + code_block: codes.white, + keyword: codes.green, + ellipsis: codes.green, + variable: codes.blue, + type_variable: codes.yellow, + structure: codes.green, + alias: codes.yellow, + opaque: codes.yellow, + error: codes.red, + line_number: codes.cyan, + header: codes.cyan, + gutter_bar: codes.cyan, + module_name: codes.green, + binop: codes.green, + typo: codes.yellow, + typo_suggestion: codes.yellow, + parser_suggestion: codes.yellow, + bold: codes.bold, + underline: codes.underline, + reset: codes.reset, + } +} + +pub const DEFAULT_PALETTE: Palette = default_palette_from_style_codes(ANSI_STYLE_CODES); + +pub const DEFAULT_PALETTE_HTML: Palette = default_palette_from_style_codes(HTML_STYLE_CODES); + +/// A machine-readable format for text styles (colors and other styles) +#[derive(Debug, PartialEq)] +pub struct StyleCodes { + pub red: &'static str, + pub green: &'static str, + pub yellow: &'static str, + pub blue: &'static str, + pub magenta: &'static str, + pub cyan: &'static str, + pub white: &'static str, + pub bold: &'static str, + pub underline: &'static str, + pub reset: &'static str, + pub color_reset: &'static str, +} + +pub const ANSI_STYLE_CODES: StyleCodes = StyleCodes { + red: "\u{001b}[31m", + green: "\u{001b}[32m", + yellow: "\u{001b}[33m", + blue: "\u{001b}[34m", + magenta: "\u{001b}[35m", + cyan: "\u{001b}[36m", + white: "\u{001b}[37m", + bold: "\u{001b}[1m", + underline: "\u{001b}[4m", + reset: "\u{001b}[0m", + color_reset: "\u{1b}[39m", +}; + +macro_rules! html_color { + ($name: expr) => { + concat!("") + }; +} + +pub const HTML_STYLE_CODES: StyleCodes = StyleCodes { + red: html_color!("red"), + green: html_color!("green"), + yellow: html_color!("yellow"), + blue: html_color!("blue"), + magenta: html_color!("magenta"), + cyan: html_color!("cyan"), + white: html_color!("white"), + bold: "", + underline: "", + reset: "", + color_reset: "", +}; + +// define custom allocator struct so we can `impl RocDocAllocator` custom helpers +pub struct RocDocAllocator<'a> { + upstream: BoxAllocator, + pub src_lines: &'a [&'a str], + pub home: ModuleId, + pub interns: &'a Interns, +} + +pub type RocDocBuilder<'b> = DocBuilder<'b, RocDocAllocator<'b>, Annotation>; + +impl<'a, A> DocAllocator<'a, A> for RocDocAllocator<'a> +where + A: 'a, +{ + type Doc = ven_pretty::BoxDoc<'a, A>; + + fn alloc(&'a self, doc: ven_pretty::Doc<'a, Self::Doc, A>) -> Self::Doc { + self.upstream.alloc(doc) + } + + fn alloc_column_fn( + &'a self, + f: impl Fn(usize) -> Self::Doc + 'a, + ) -> >::ColumnFn { + self.upstream.alloc_column_fn(f) + } + + fn alloc_width_fn( + &'a self, + f: impl Fn(isize) -> Self::Doc + 'a, + ) -> >::WidthFn { + self.upstream.alloc_width_fn(f) + } +} + +impl<'a> RocDocAllocator<'a> { + pub fn new(src_lines: &'a [&'a str], home: ModuleId, interns: &'a Interns) -> Self { + RocDocAllocator { + upstream: BoxAllocator, + home, + src_lines, + interns, + } + } + + /// vertical concatenation. Adds a newline between elements + pub fn vcat(&'a self, docs: I) -> DocBuilder<'a, Self, A> + where + A: 'a + Clone, + I: IntoIterator, + I::Item: Into, A>>, + { + self.intersperse(docs, self.line()) + } + + /// like vcat, but adds a double line break between elements. Visually this means an empty line + /// between elements. + pub fn stack(&'a self, docs: I) -> DocBuilder<'a, Self, A> + where + A: 'a + Clone, + I: IntoIterator, + I::Item: Into, A>>, + { + self.intersperse(docs, self.line().append(self.line())) + } + + /// text from a String. Note that this does not reflow! + pub fn string(&'a self, string: String) -> DocBuilder<'a, Self, Annotation> { + let x: std::borrow::Cow<'a, str> = string.into(); + + self.text(x) + } + + pub fn keyword(&'a self, string: &'a str) -> DocBuilder<'a, Self, Annotation> { + self.text(string).annotate(Annotation::Keyword) + } + + pub fn ellipsis(&'a self) -> DocBuilder<'a, Self, Annotation> { + self.text("…").annotate(Annotation::Ellipsis) + } + + pub fn parser_suggestion(&'a self, string: &'a str) -> DocBuilder<'a, Self, Annotation> { + self.text(string).annotate(Annotation::ParserSuggestion) + } + + pub fn type_str(&'a self, content: &str) -> DocBuilder<'a, Self, Annotation> { + self.string(content.to_owned()).annotate(Annotation::Alias) + } + + pub fn type_variable(&'a self, content: Lowercase) -> DocBuilder<'a, Self, Annotation> { + // currently not annotated + self.string(content.to_string()) + .annotate(Annotation::TypeVariable) + } + + pub fn tag_name(&'a self, tn: TagName) -> DocBuilder<'a, Self, Annotation> { + self.tag(tn.0) + } + + pub fn symbol_unqualified(&'a self, symbol: Symbol) -> DocBuilder<'a, Self, Annotation> { + self.text(symbol.as_str(self.interns)) + .annotate(Annotation::Symbol) + } + pub fn symbol_foreign_qualified(&'a self, symbol: Symbol) -> DocBuilder<'a, Self, Annotation> { + if symbol.module_id() == self.home || symbol.module_id().is_builtin() { + // Render it unqualified if it's in the current module or a builtin + self.text(symbol.as_str(self.interns)) + .annotate(Annotation::Symbol) + } else { + text!( + self, + "{}.{}", + symbol.module_string(self.interns), + symbol.as_str(self.interns), + ) + .annotate(Annotation::Symbol) + } + } + pub fn symbol_qualified(&'a self, symbol: Symbol) -> DocBuilder<'a, Self, Annotation> { + text!( + self, + "{}.{}", + symbol.module_string(self.interns), + symbol.as_str(self.interns), + ) + .annotate(Annotation::Symbol) + } + + /// TODO: remove in favor of tag_name + pub fn tag(&'a self, uppercase: Uppercase) -> DocBuilder<'a, Self, Annotation> { + text!(self, "{}", uppercase).annotate(Annotation::Tag) + } + + pub fn opaque_name(&'a self, opaque: Symbol) -> DocBuilder<'a, Self, Annotation> { + let fmt = if opaque.module_id() == self.home { + // Render it unqualified if it's in the current module. + opaque.as_str(self.interns).to_string() + } else { + format!( + "{}.{}", + opaque.module_string(self.interns), + opaque.as_str(self.interns), + ) + }; + + self.text(fmt).annotate(Annotation::Opaque) + } + + pub fn wrapped_opaque_name(&'a self, opaque: Symbol) -> DocBuilder<'a, Self, Annotation> { + debug_assert_eq!(opaque.module_id(), self.home, "Opaque wrappings can only be defined in the same module they're defined in, but this one is defined elsewhere: {opaque:?}"); + + text!(self, "@{}", opaque.as_str(self.interns)).annotate(Annotation::Opaque) + } + + pub fn record_field(&'a self, lowercase: Lowercase) -> DocBuilder<'a, Self, Annotation> { + text!(self, ".{}", lowercase).annotate(Annotation::RecordField) + } + + pub fn tuple_field(&'a self, index: usize) -> DocBuilder<'a, Self, Annotation> { + text!(self, ".{}", index).annotate(Annotation::TupleElem) + } + + pub fn module(&'a self, module_id: ModuleId) -> DocBuilder<'a, Self, Annotation> { + let name = self.interns.module_name(module_id); + let name = if name.is_empty() { + // Render the app module as "app" + "app".to_string() + } else { + name.to_string() + }; + + self.text(name).annotate(Annotation::Module) + } + + pub fn pq_module_name(&'a self, name: PQModuleName<'a>) -> DocBuilder<'a, Self, Annotation> { + let name = match name { + PackageQualified::Unqualified(n) => n.to_string(), + PackageQualified::Qualified(prefix, n) => format!("{prefix}.{n}"), + }; + + self.text(name).annotate(Annotation::Module) + } + + pub fn module_name(&'a self, name: ModuleName) -> DocBuilder<'a, Self, Annotation> { + if name.is_empty() { + // Render the app module as "app" + self.text("app") + } else { + self.text(name.as_str().to_string()) + } + .annotate(Annotation::Module) + } + + pub fn binop( + &'a self, + content: roc_module::called_via::BinOp, + ) -> DocBuilder<'a, Self, Annotation> { + self.text(content.to_string()).annotate(Annotation::BinOp) + } + + pub fn unop( + &'a self, + content: roc_module::called_via::UnaryOp, + ) -> DocBuilder<'a, Self, Annotation> { + self.text(content.to_string()).annotate(Annotation::UnaryOp) + } + + /// Turns off backticks/colors in a block + pub fn type_block( + &'a self, + content: DocBuilder<'a, Self, Annotation>, + ) -> DocBuilder<'a, Self, Annotation> { + content.annotate(Annotation::TypeBlock).indent(4) + } + + /// Turns off backticks/colors in a block + pub fn inline_type_block( + &'a self, + content: DocBuilder<'a, Self, Annotation>, + ) -> DocBuilder<'a, Self, Annotation> { + content.annotate(Annotation::InlineTypeBlock) + } + + pub fn tip(&'a self) -> DocBuilder<'a, Self, Annotation> { + self.text("Tip") + .annotate(Annotation::Tip) + .append(":") + .append(self.softline()) + } + + pub fn note(&'a self, line: &'a str) -> DocBuilder<'a, Self, Annotation> { + self.text("Note") + .annotate(Annotation::Tip) + .append(": ") + .append(line) + } + + pub fn hint(&'a self, line: &'a str) -> DocBuilder<'a, Self, Annotation> { + self.text("Hint") + .annotate(Annotation::Tip) + .append(": ") + .append(line) + } + + pub fn region_all_the_things( + &'a self, + region: LineColumnRegion, + sub_region1: LineColumnRegion, + sub_region2: LineColumnRegion, + error_annotation: Annotation, + ) -> DocBuilder<'a, Self, Annotation> { + debug_assert!(region.contains(&sub_region1)); + debug_assert!(region.contains(&sub_region2)); + + // if true, the final line of the snippet will be some ^^^ that point to the region where + // the problem is. Otherwise, the snippet will have a > on the lines that are in the region + // where the problem is. + let error_highlight_line = region.start().line == region.end().line; + + let max_line_number_length = (region.end().line + 1).to_string().len(); + let indent = 2; + + let mut result = self.nil(); + for i in region.start().line..=region.end().line { + let line_number_string = (i + 1).to_string(); + let line_number = line_number_string; + let this_line_number_length = line_number.len(); + + let line = self.src_lines[i as usize]; + let is_line_empty = line.trim().is_empty(); + let rest_of_line = if !is_line_empty { + self.text(line).indent(indent) + } else { + self.nil() + }; + + let highlight = !error_highlight_line + && ((i >= sub_region1.start().line && i <= sub_region1.end().line) + || (i >= sub_region2.start().line && i <= sub_region2.end().line)); + + let source_line = if highlight { + self.text(" ".repeat(max_line_number_length - this_line_number_length)) + .append(self.text(line_number).annotate(Annotation::LineNumber)) + .append(self.text(GUTTER_BAR).annotate(Annotation::GutterBar)) + .append(self.text(">").annotate(error_annotation)) + .append(rest_of_line) + } else if error_highlight_line { + self.text(" ".repeat(max_line_number_length - this_line_number_length)) + .append(self.text(line_number).annotate(Annotation::LineNumber)) + .append(self.text(GUTTER_BAR).annotate(Annotation::GutterBar)) + .append(rest_of_line) + } else { + let up_to_gutter = self + .text(" ".repeat(max_line_number_length - this_line_number_length)) + .append(self.text(line_number).annotate(Annotation::LineNumber)) + .append(self.text(GUTTER_BAR).annotate(Annotation::GutterBar)); + + if is_line_empty { + // Don't put an trailing space after the gutter + up_to_gutter + } else { + up_to_gutter.append(self.text(" ")).append(rest_of_line) + } + }; + + result = result.append(source_line); + + if i != region.end().line { + result = result.append(self.line()) + } + } + + if error_highlight_line { + let overlapping = sub_region2.start().column < sub_region1.end().column; + + let highlight = if overlapping { + self.text( + ERROR_UNDERLINE + .repeat((sub_region2.end().column - sub_region1.start().column) as usize), + ) + } else { + let highlight1 = ERROR_UNDERLINE + .repeat((sub_region1.end().column - sub_region1.start().column) as usize); + let highlight2 = if sub_region1 == sub_region2 { + "".repeat(0) + } else { + ERROR_UNDERLINE + .repeat((sub_region2.end().column - sub_region2.start().column) as usize) + }; + let in_between = " ".repeat( + (sub_region2 + .start() + .column + .saturating_sub(sub_region1.end().column)) as usize, + ); + + self.text(highlight1) + .append(self.text(in_between)) + .append(self.text(highlight2)) + }; + + let highlight_line = self + .line() + // Omit the gutter bar when we know there are no further + // line numbers to be printed after this! + .append(self.text(" ".repeat(max_line_number_length + GUTTER_BAR_WIDTH))) + .append(if sub_region1.is_empty() && sub_region2.is_empty() { + self.nil() + } else { + self.text(" ".repeat(sub_region1.start().column as usize)) + .indent(indent) + .append(highlight) + .annotate(error_annotation) + }); + + result = result.append(highlight_line); + } + + result.annotate(Annotation::CodeBlock) + } + + pub fn region_with_subregion( + &'a self, + region: LineColumnRegion, + sub_region: LineColumnRegion, + ) -> DocBuilder<'a, Self, Annotation> { + // debug_assert!(region.contains(&sub_region)); + + // If the outer region takes more than 1 full screen (~60 lines), only show the inner region + if region.end().line.saturating_sub(region.start().line) > 60 { + // If the inner region contains the outer region (or if they are the same), + // attempting this will recurse forever, so don't do that! Instead, give up and + // accept that this report will take up more than 1 full screen. + if !sub_region.contains(®ion) { + return self.region_with_subregion(sub_region, sub_region); + } + } + + // if true, the final line of the snippet will be some ^^^ that point to the region where + // the problem is. Otherwise, the snippet will have a > on the lines that are in the region + // where the problem is. + let error_highlight_line = sub_region.start().line == region.end().line; + + let max_line_number_length = (region.end().line + 1).to_string().len(); + let indent = 2; + + let mut result = self.nil(); + for i in region.start().line..=region.end().line { + let line_number_string = (i + 1).to_string(); + let line_number = line_number_string; + let this_line_number_length = line_number.len(); + + // filter out any escape characters for the current line that could mess up the output. + let line: String = self + .src_lines + .get(i as usize) + .unwrap_or(&"") + .chars() + .filter(|&c| !c.is_ascii_control() || c == '\t') + .collect::(); + + let is_line_empty = line.trim().is_empty(); + let rest_of_line = if !is_line_empty { + self.text(line) + .annotate(Annotation::CodeBlock) + .indent(indent) + } else { + self.nil() + }; + + let source_line = if !error_highlight_line + && i >= sub_region.start().line + && i <= sub_region.end().line + { + self.text(" ".repeat(max_line_number_length - this_line_number_length)) + .append(self.text(line_number).annotate(Annotation::LineNumber)) + .append(self.text(GUTTER_BAR).annotate(Annotation::GutterBar)) + .append(self.text(">").annotate(Annotation::Error)) + .append(rest_of_line) + } else if error_highlight_line { + self.text(" ".repeat(max_line_number_length - this_line_number_length)) + .append(self.text(line_number).annotate(Annotation::LineNumber)) + .append(self.text(GUTTER_BAR).annotate(Annotation::GutterBar)) + .append(rest_of_line) + } else { + let up_to_gutter = self + .text(" ".repeat(max_line_number_length - this_line_number_length)) + .append(self.text(line_number).annotate(Annotation::LineNumber)) + .append(self.text(GUTTER_BAR).annotate(Annotation::GutterBar)); + + if is_line_empty { + // Don't put an trailing space after the gutter + up_to_gutter + } else { + up_to_gutter.append(self.text(" ")).append(rest_of_line) + } + }; + + result = result.append(source_line); + + if i != region.end().line { + result = result.append(self.line()) + } + } + + if error_highlight_line { + let highlight_text = ERROR_UNDERLINE + .repeat((sub_region.end().column - sub_region.start().column) as usize); + + let highlight_line = self + .line() + // Omit the gutter bar when we know there are no further + // line numbers to be printed after this! + .append(self.text(" ".repeat(max_line_number_length + GUTTER_BAR_WIDTH))) + .append(if highlight_text.is_empty() { + self.nil() + } else { + self.text(" ".repeat(sub_region.start().column as usize)) + .indent(indent) + .append(self.text(highlight_text).annotate(Annotation::Error)) + }); + + result = result.append(highlight_line); + } + + result + } + + pub fn region(&'a self, region: LineColumnRegion) -> DocBuilder<'a, Self, Annotation> { + self.region_with_subregion(region, region) + } + + pub fn region_without_error( + &'a self, + region: LineColumnRegion, + ) -> DocBuilder<'a, Self, Annotation> { + let mut result = self.nil(); + for i in region.start().line..=region.end().line { + let line = if i == region.start().line { + if i == region.end().line { + &self.src_lines[i as usize] + [region.start().column as usize..region.end().column as usize] + } else { + &self.src_lines[i as usize][region.start().column as usize..] + } + } else if i == region.end().line { + &self.src_lines[i as usize][0..region.end().column as usize] + } else { + self.src_lines[i as usize] + }; + + let rest_of_line = if !line.trim().is_empty() { + self.text(line).annotate(Annotation::CodeBlock) + } else { + self.nil() + }; + + result = result.append(rest_of_line); + + if i != region.end().line { + result = result.append(self.line()) + } + } + + result.indent(4) + } + + pub fn ident(&'a self, ident: Ident) -> DocBuilder<'a, Self, Annotation> { + text!(self, "{}", ident.as_inline_str()).annotate(Annotation::Symbol) + } + + pub fn int_literal(&'a self, int: I) -> DocBuilder<'a, Self, Annotation> + where + I: ToString, + { + let s = int.to_string(); + + let is_negative = s.starts_with('-'); + + if s.len() < 7 + (is_negative as usize) { + // If the number is not at least in the millions, return it as-is. + return self.text(s); + } + + // Otherwise, let's add numeric separators to make it easier to read. + let mut result = String::with_capacity(s.len() + s.len() / 3); + for (idx, c) in s + .get((is_negative as usize)..) + .unwrap() + .chars() + .rev() + .enumerate() + { + if idx != 0 && idx % 3 == 0 { + result.push('_'); + } + result.push(c); + } + if is_negative { + result.push('-'); + } + self.text(result.chars().rev().collect::()) + } +} + +#[derive(Copy, Clone, Debug)] +pub enum Annotation { + Emphasized, + Url, + Keyword, + Ellipsis, + Tag, + RecordField, + TupleElem, + TypeVariable, + Alias, + Opaque, + Structure, + Symbol, + BinOp, + UnaryOp, + Error, + GutterBar, + LineNumber, + PlainText, + CodeBlock, + TypeBlock, + InlineTypeBlock, + Module, + Typo, + TypoSuggestion, + Tip, + Header, + ParserSuggestion, +} + +/// Render with minimal formatting +pub struct CiWrite { + style_stack: Vec, + in_type_block: bool, + in_code_block: bool, + upstream: W, +} + +impl CiWrite { + pub fn new(upstream: W) -> CiWrite { + CiWrite { + style_stack: vec![], + in_type_block: false, + in_code_block: false, + upstream, + } + } +} + +/// Render with fancy formatting +pub struct ColorWrite<'a, W> { + style_stack: Vec, + palette: &'a Palette, + upstream: W, +} + +impl<'a, W> ColorWrite<'a, W> { + pub fn new(palette: &'a Palette, upstream: W) -> ColorWrite<'a, W> { + ColorWrite { + style_stack: vec![], + palette, + upstream, + } + } +} + +impl Render for CiWrite +where + W: fmt::Write, +{ + type Error = fmt::Error; + + fn write_str(&mut self, s: &str) -> Result { + self.write_str_all(s).map(|_| s.len()) + } + + fn write_str_all(&mut self, s: &str) -> fmt::Result { + self.upstream.write_str(s) + } +} + +impl RenderAnnotated for CiWrite +where + W: fmt::Write, +{ + fn push_annotation(&mut self, annotation: &Annotation) -> Result<(), Self::Error> { + use Annotation::*; + match annotation { + TypeBlock => { + self.in_type_block = true; + } + InlineTypeBlock => { + debug_assert!(!self.in_type_block); + self.write_str("`")?; + self.in_type_block = true; + } + CodeBlock => { + self.in_code_block = true; + } + Emphasized => { + self.write_str("*")?; + } + Url => { + self.write_str("<")?; + } + Tag | Keyword | RecordField | Symbol | Typo | TypoSuggestion | TypeVariable + if !self.in_type_block && !self.in_code_block => + { + self.write_str("`")?; + } + + _ => {} + } + self.style_stack.push(*annotation); + Ok(()) + } + + fn pop_annotation(&mut self) -> Result<(), Self::Error> { + use Annotation::*; + + match self.style_stack.pop() { + None => {} + Some(annotation) => match annotation { + TypeBlock => { + self.in_type_block = false; + } + InlineTypeBlock => { + debug_assert!(self.in_type_block); + self.write_str("`")?; + self.in_type_block = false; + } + CodeBlock => { + self.in_code_block = false; + } + Emphasized => { + self.write_str("*")?; + } + Url => { + self.write_str(">")?; + } + Tag | Keyword | RecordField | Symbol | Typo | TypoSuggestion | TypeVariable + if !self.in_type_block && !self.in_code_block => + { + self.write_str("`")?; + } + + _ => {} + }, + } + Ok(()) + } +} + +impl<'a, W> Render for ColorWrite<'a, W> +where + W: fmt::Write, +{ + type Error = fmt::Error; + + fn write_str(&mut self, s: &str) -> Result { + self.write_str_all(s).map(|_| s.len()) + } + + fn write_str_all(&mut self, s: &str) -> fmt::Result { + self.upstream.write_str(s) + } +} + +impl<'a, W> RenderAnnotated for ColorWrite<'a, W> +where + W: fmt::Write, +{ + fn push_annotation(&mut self, annotation: &Annotation) -> Result<(), Self::Error> { + use Annotation::*; + match annotation { + Emphasized => { + self.write_str(self.palette.bold)?; + } + Url | Tip => { + self.write_str(self.palette.underline)?; + } + PlainText => { + self.write_str(self.palette.primary)?; + } + CodeBlock => { + self.write_str(self.palette.code_block)?; + } + TypeVariable => { + self.write_str(self.palette.type_variable)?; + } + Alias => { + self.write_str(self.palette.alias)?; + } + Opaque => { + self.write_str(self.palette.alias)?; + } + BinOp => { + self.write_str(self.palette.alias)?; + } + UnaryOp => { + self.write_str(self.palette.alias)?; + } + Symbol => { + self.write_str(self.palette.variable)?; + } + Keyword => { + self.write_str(self.palette.keyword)?; + } + Ellipsis => { + self.write_str(self.palette.ellipsis)?; + } + GutterBar => { + self.write_str(self.palette.gutter_bar)?; + } + Error => { + self.write_str(self.palette.error)?; + } + Header => { + self.write_str(self.palette.header)?; + } + LineNumber => { + self.write_str(self.palette.line_number)?; + } + Structure => { + self.write_str(self.palette.structure)?; + } + Module => { + self.write_str(self.palette.module_name)?; + } + Typo => { + self.write_str(self.palette.typo)?; + } + TypoSuggestion => { + self.write_str(self.palette.typo_suggestion)?; + } + ParserSuggestion => { + self.write_str(self.palette.parser_suggestion)?; + } + TypeBlock | InlineTypeBlock | Tag | RecordField | TupleElem => { /* nothing yet */ } + } + self.style_stack.push(*annotation); + Ok(()) + } + + fn pop_annotation(&mut self) -> Result<(), Self::Error> { + use Annotation::*; + + match self.style_stack.pop() { + None => {} + Some(annotation) => match annotation { + Emphasized | Url | TypeVariable | Alias | Symbol | BinOp | UnaryOp | Error + | GutterBar | Ellipsis | Typo | TypoSuggestion | ParserSuggestion | Structure + | CodeBlock | PlainText | LineNumber | Tip | Module | Header | Keyword => { + self.write_str(self.palette.reset)?; + } + + TypeBlock | InlineTypeBlock | Tag | Opaque | RecordField | TupleElem => { /* nothing yet */ + } + }, + } + Ok(()) + } +} + +#[cfg(not(target_family = "wasm"))] +pub fn to_https_problem_report_string(url: &str, https_problem: Problem) -> String { + let src_lines: Vec<&str> = Vec::new(); + + let mut module_ids = ModuleIds::default(); + + let module_id = module_ids.get_or_insert(&"find module name somehow?".into()); + + let interns = Interns::default(); + + // Report parsing and canonicalization problems + let alloc = RocDocAllocator::new(&src_lines, module_id, &interns); + + let mut buf = String::new(); + let palette = DEFAULT_PALETTE; + let report = to_https_problem_report(&alloc, url, https_problem); + report.render_color_terminal(&mut buf, &alloc, &palette); + + buf +} + +#[cfg(not(target_family = "wasm"))] +pub fn to_https_problem_report<'b>( + alloc: &'b RocDocAllocator<'b>, + url: &'b str, + https_problem: Problem, +) -> Report<'b> { + match https_problem { + Problem::UnsupportedEncoding(not_supported_encoding) => { + let doc = alloc.stack([ + alloc.reflow(r"I was trying to download this URL:"), + alloc.string((&url).to_string()).annotate(Annotation::Url).indent(4), + alloc.concat([ + alloc.reflow(r"But the server replied with a "), + alloc.reflow(r"content encoding").annotate(Annotation::Emphasized), + alloc.reflow(r" that I do not understand ("), + alloc.string(not_supported_encoding).annotate(Annotation::Emphasized), + alloc.reflow(r")."), + ]), + alloc.concat([ + alloc.reflow(r"The supported content encodings are "), + alloc.keyword(r"br"), + alloc.reflow(r", "), + alloc.keyword(r"gzip"), + alloc.reflow(r" and "), + alloc.keyword(r"deflate"), + ]), + alloc.concat([ + alloc.tip(), + alloc.reflow(r"Perhaps you can check if the URL is correctly formed, or if the server is correctly configured."), + ]), + ]); + + Report { + filename: "UNKNOWN.roc".into(), + doc, + title: "UNSUPPORTED ENCODING".to_string(), + severity: Severity::Fatal, + } + } + Problem::MultipleEncodings(multiple_encodings) => { + let doc = alloc.stack([ + alloc.reflow(r"I was trying to download this URL:"), + alloc.string((&url).to_string()).annotate(Annotation::Url).indent(4), + alloc.concat([ + alloc.reflow(r"But the server replied with multiple "), + alloc.reflow(r"content encodings").annotate(Annotation::Emphasized), + alloc.reflow(r": "), + alloc.string(multiple_encodings).annotate(Annotation::Emphasized), + alloc.reflow(r"."), + ]), + alloc.concat([ + alloc.reflow(r"The supported content encodings are "), + alloc.keyword(r"br"), + alloc.reflow(r", "), + alloc.keyword(r"gzip"), + alloc.reflow(r" and "), + alloc.keyword(r"deflate"), + alloc.reflow(r". However, the server reply can only contain "), + alloc.reflow(r"one").annotate(Annotation::Emphasized), + alloc.reflow(r"."), + ]), + alloc.concat([ + alloc.tip(), + alloc.reflow(r"Perhaps you can check if the URL is correctly formed, or if the server is correctly configured."), + ]), + ]); + + Report { + filename: "UNKNOWN.roc".into(), + doc, + title: "MULTIPLE ENCODINGS".to_string(), + severity: Severity::Fatal, + } + } + Problem::InvalidContentHash { expected, actual } => { + let doc = alloc.stack([ + alloc.reflow(r"I was able to download this URL:"), + alloc.string((&url).to_string()).annotate(Annotation::Url).indent(4), + alloc.concat([ + alloc.reflow(r"I use a mechanism to detect if the file might "), + alloc.reflow(r"have been tampered with. This could happen if "), + alloc.reflow(r"the server or domain have been compromised."), + ]), + alloc.concat([ + alloc.reflow(r"This is the content signature I was "), + alloc.reflow(r"expecting").annotate(Annotation::Emphasized), + alloc.reflow(r":"), + ]), + alloc.string(expected).annotate(Annotation::PlainText).indent(4), + alloc.concat([ + alloc.reflow(r"However, this is the content signature I "), + alloc.reflow(r"obtained").annotate(Annotation::Emphasized), + alloc.reflow(r":"), + ]), + alloc.string(actual).annotate(Annotation::PlainText).indent(4), + alloc.reflow(r"To keep you secure, I will not execute this untrusted code."), + alloc.concat([ + alloc.tip(), + alloc.reflow(r"Check if the URL is correctly formed and if this is the server you are expecting to connect to."), + ]), + ]); + + Report { + filename: "UNKNOWN.roc".into(), + doc, + title: "INVALID CONTENT HASH".to_string(), + severity: Severity::Fatal, + } + } + // TODO: The reporting text for IoErr and FsExtraErr could probably be unified + Problem::IoErr(io_error) => { + let doc = alloc.stack([ + alloc.reflow(r"I was trying to download this URL:"), + alloc + .string((&url).to_string()) + .annotate(Annotation::Url) + .indent(4), + alloc.reflow(r"But I encountered an IO (input/output) error:"), + alloc + .string(io_error.to_string()) + .annotate(Annotation::PlainText) + .indent(4), + // TODO: What should the tip for IO errors be? + // alloc.concat([ + // alloc.tip(), + // alloc.reflow(r"Check the error message."), + // ]), + ]); + + Report { + filename: "UNKNOWN.roc".into(), + doc, + title: "IO ERROR".to_string(), + severity: Severity::Fatal, + } + } + // TODO: The reporting text for IoErr and FsExtraErr could probably be unified + Problem::FsExtraErr(fs_extra_error) => { + let doc = alloc.stack([ + alloc.reflow(r"I was trying to download this URL:"), + alloc + .string((&url).to_string()) + .annotate(Annotation::Url) + .indent(4), + alloc.reflow(r"But I encountered an IO (input/output) error:"), + alloc + .string(fs_extra_error.to_string()) + .annotate(Annotation::PlainText) + .indent(4), + // TODO: What should the tip for IO errors be? + // alloc.concat([ + // alloc.tip(), + // alloc.reflow(r"Check the error message."), + // ]), + ]); + + Report { + filename: "UNKNOWN.roc".into(), + doc, + title: "IO ERROR".to_string(), + severity: Severity::Fatal, + } + } + Problem::HttpErr(reqwest_error) => { + let doc = alloc.stack([ + alloc.reflow(r"I was trying to download this URL:"), + alloc + .string((&url).to_string()) + .annotate(Annotation::Url) + .indent(4), + alloc.reflow(r"But I encountered a network error:"), + alloc + .string(reqwest_error.to_string()) + .annotate(Annotation::PlainText) + .indent(4), + // TODO: What should the tip for HTTP IO errors be? + // Should we import reqwest and check stuff like + // reqwest_error.{ is_redirect(), is_status(), is_timeout(), ... } ? + // + // alloc.concat([ + // alloc.tip(), + // alloc.reflow(r"Check the error message."), + // ]), + ]); + + Report { + filename: "UNKNOWN.roc".into(), + doc, + title: "HTTP ERROR".to_string(), + severity: Severity::Fatal, + } + } + Problem::InvalidUrl(roc_packaging::https::UrlProblem::InvalidExtensionSuffix( + invalid_suffix, + )) => { + let (suffix_text, annotation_style) = if invalid_suffix.is_empty() { + (r"empty".to_string(), Annotation::PlainText) + } else { + (invalid_suffix, Annotation::Emphasized) + }; + + let doc = alloc.stack([ + alloc.reflow(r"I was trying to download this URL:"), + alloc + .string((&url).to_string()) + .annotate(Annotation::Url) + .indent(4), + alloc.concat([ + alloc.reflow(r"However, this file's extension ("), + alloc.string(suffix_text).annotate(annotation_style), + alloc.reflow(r") is not a supported extension."), + ]), + alloc.concat([ + alloc.reflow(r"The supported extensions are "), + alloc.keyword(r".tar"), + alloc.reflow(r", "), + alloc.keyword(r".tar.gz"), + alloc.reflow(r" and "), + alloc.keyword(r".tar.br"), + ]), + alloc.concat([ + alloc.tip(), + alloc.reflow(r"Check that you have the correct URL for this package/platform."), + ]), + ]); + + Report { + filename: "UNKNOWN.roc".into(), + doc, + title: "INVALID EXTENSION SUFFIX".to_string(), + severity: Severity::Fatal, + } + } + Problem::InvalidUrl(roc_packaging::https::UrlProblem::MissingTarExt) => { + let doc = alloc.stack([ + alloc.reflow(r"I was trying to download this URL:"), + alloc + .string((&url).to_string()) + .annotate(Annotation::Url) + .indent(4), + alloc.concat([ + alloc.reflow(r"However, this file's extension is not "), + alloc.keyword(r".tar"), + alloc.reflow(r"."), + ]), + alloc.concat([ + alloc.reflow(r"The supported extensions are "), + alloc.keyword(r".tar"), + alloc.reflow(r", "), + alloc.keyword(r".tar.gz"), + alloc.reflow(r" and "), + alloc.keyword(r".tar.br"), + ]), + alloc.concat([ + alloc.tip(), + alloc.reflow(r"Check that you have the correct URL for this package/platform."), + ]), + ]); + + Report { + filename: "UNKNOWN.roc".into(), + doc, + title: "INVALID EXTENSION".to_string(), + severity: Severity::Fatal, + } + } + Problem::InvalidUrl(roc_packaging::https::UrlProblem::InvalidFragment( + invalid_fragment, + )) => { + let doc = alloc.stack([ + alloc.reflow(r"I was trying to download this URL:"), + alloc + .string((&url).to_string()) + .annotate(Annotation::Url) + .indent(4), + alloc.concat([ + alloc.reflow(r"However, this URL's fragment (the part after #) "), + alloc.reflow(r"is not valid. When present, the fragment must point to "), + alloc.reflow(r"an existing "), + alloc.keyword(r".roc"), + alloc.reflow(r" file inside the package. Also, the filename can't be empty, "), + alloc.reflow(r"so a fragment of #.roc would also not be valid. This is the "), + alloc.reflow(r"invalid fragment I encountered: "), + ]), + alloc + .string(invalid_fragment) + .annotate(Annotation::Emphasized) + .indent(4), + alloc.concat([ + alloc.tip(), + alloc.reflow(r"Check that the fragment points to an existing "), + alloc.keyword(r".roc"), + alloc.reflow(r" file inside the package. You can download this package "), + alloc.reflow(r"and inspect it locally."), + ]), + ]); + + Report { + filename: "UNKNOWN.roc".into(), + doc, + title: "INVALID FRAGMENT".to_string(), + severity: Severity::Fatal, + } + } + Problem::InvalidUrl(roc_packaging::https::UrlProblem::MissingHash) => { + let doc = alloc.stack([ + alloc.reflow(r"I was trying to download this URL:"), + alloc + .string((&url).to_string()) + .annotate(Annotation::Url) + .indent(4), + alloc.concat([ + alloc.reflow(r"I use a content hash to detect if the file might "), + alloc.reflow(r"have been tampered with. This could happen if "), + alloc.reflow(r"the server or domain have been compromised."), + ]), + alloc.concat([ + alloc.reflow(r"The way this works is that the name of the file "), + alloc.reflow(r"is the BLAKE3 hash of the contents of the "), + alloc.reflow(r"file itself. If someone would tamper with the file, "), + alloc.reflow(r"I could notify and protect you. However, I could "), + alloc.reflow(r"not find the expected hash on the URL above, "), + alloc.reflow(r"so I cannot apply this tamper-check."), + ]), + alloc.concat([ + alloc.tip(), + alloc + .reflow(r"Check that you have the correct URL for this package/platform. "), + alloc.reflow(r"Here is an example of how such a hash looks like: "), + alloc + .string(r"tE4xS_zLdmmxmHwHih9kHWQ7fsXtJr7W7h3425-eZFk".to_string()) + .annotate(Annotation::Emphasized), + ]), + ]); + + Report { + filename: "UNKNOWN.roc".into(), + doc, + title: "MISSING PACKAGE HASH".to_string(), + severity: Severity::Fatal, + } + } + Problem::InvalidUrl(roc_packaging::https::UrlProblem::MissingHttps) => { + let doc = alloc.stack([ + alloc.reflow(r"I was trying to download this URL:"), + alloc + .string((&url).to_string()) + .annotate(Annotation::Url) + .indent(4), + alloc.concat([ + alloc.reflow(r"For your security, I will only attempt to download "), + alloc.reflow(r"files from servers which use the "), + alloc.keyword(r"https"), + alloc.reflow(r" protocol."), + ]), + alloc.concat([ + alloc.tip(), + alloc.reflow(r"Check that you have the correct URL for this package/platform."), + ]), + ]); + + Report { + filename: "UNKNOWN.roc".into(), + doc, + title: "HTTPS MANDATORY".to_string(), + severity: Severity::Fatal, + } + } + Problem::InvalidUrl(roc_packaging::https::UrlProblem::MisleadingCharacter) => { + let doc = alloc.stack([ + alloc.reflow(r"I was trying to download this URL:"), + alloc + .string((&url).to_string()) + .annotate(Annotation::Url) + .indent(4), + alloc.concat([ + alloc.reflow(r"I have found one or more potentially misleading "), + alloc.reflow(r"characters in this URL. Misleading characters are "), + alloc.reflow(r"characters that look like others but aren't the same. "), + alloc.reflow(r"The following characters are classified as misleading: "), + alloc.keyword(r"@"), + alloc.reflow(r", "), + alloc.keyword("\u{2044}"), + alloc.reflow(r" (unicode 2044), "), + alloc.keyword("\u{2215}"), + alloc.reflow(r" (unicode 2215), "), + alloc.keyword("\u{FF0F}"), + alloc.reflow(r" (unicode FF0F) and "), + alloc.keyword("\u{29F8}"), + alloc.reflow(r" (unicode 29F8). "), + ]), + alloc.concat([ + alloc.reflow(r"If you have a use-case for any of these characters we "), + alloc.reflow(r"would like to hear about it. Reach out on "), + alloc + .string(r"https://github.com/roc-lang/roc/issues/5487".to_string()) + .annotate(Annotation::Url), + ]), + alloc.concat([ + alloc.tip(), + alloc.reflow(r"Check that you have the correct URL for this package/platform."), + ]), + ]); + + Report { + filename: "UNKNOWN.roc".into(), + doc, + title: "MISLEADING CHARACTERS".to_string(), + severity: Severity::Fatal, + } + } + Problem::DownloadTooBig(content_len) => { + let nice_bytes = Byte::from_bytes(content_len.into()) + .get_appropriate_unit(false) + .format(3); + let doc = alloc.stack([ + alloc.reflow(r"I was trying to download this URL:"), + alloc + .string((&url).to_string()) + .annotate(Annotation::Url) + .indent(4), + alloc.concat([ + alloc.reflow(r"But the server stated this file is "), + alloc.string(nice_bytes).annotate(Annotation::Keyword), + alloc.reflow(r" in size. This is larger that the maximum size I can handle (around 32 GB)."), + ]), + alloc.concat([ + alloc.tip(), + alloc.reflow(r"Check that you have the correct URL for this package/platform. "), + alloc.reflow(r"If you do, you should contact the package/platform's author and "), + alloc.reflow(r"notify them about this issue."), + ]), + ]); + + Report { + filename: "UNKNOWN.roc".into(), + doc, + title: "FILE TOO LARGE".to_string(), + severity: Severity::Fatal, + } + } + } +} + +pub fn to_file_problem_report_string(filename: &Path, error: io::ErrorKind) -> String { + let src_lines: Vec<&str> = Vec::new(); + + let mut module_ids = ModuleIds::default(); + + let module_id = module_ids.get_or_insert(&"find module name somehow?".into()); + + let interns = Interns::default(); + + // Report parsing and canonicalization problems + let alloc = RocDocAllocator::new(&src_lines, module_id, &interns); + + let mut buf = String::new(); + let palette = DEFAULT_PALETTE; + let report = to_file_problem_report(&alloc, filename, error); + report.render_color_terminal(&mut buf, &alloc, &palette); + + buf +} + +pub fn to_file_problem_report<'b>( + alloc: &'b RocDocAllocator<'b>, + filename: &Path, + error: io::ErrorKind, +) -> Report<'b> { + let filename: String = filename.to_str().unwrap().to_string(); + match error { + io::ErrorKind::NotFound => { + let doc = alloc.stack([ + alloc.reflow(r"I am looking for this file, but it's not there:"), + alloc + .string(filename) + .annotate(Annotation::ParserSuggestion) + .indent(4), + alloc.concat([ + alloc.reflow(r"Is the file supposed to be there? "), + alloc.reflow("Maybe there is a typo in the file name?"), + ]), + ]); + + Report { + filename: "UNKNOWN.roc".into(), + doc, + title: "FILE NOT FOUND".to_string(), + severity: Severity::Fatal, + } + } + io::ErrorKind::PermissionDenied => { + let doc = alloc.stack([ + alloc.reflow(r"I don't have the required permissions to read this file:"), + alloc + .string(filename) + .annotate(Annotation::ParserSuggestion) + .indent(4), + alloc + .concat([alloc.reflow(r"Is it the right file? Maybe change its permissions?")]), + ]); + + Report { + filename: "UNKNOWN.roc".into(), + doc, + title: "FILE PERMISSION DENIED".to_string(), + severity: Severity::Fatal, + } + } + _ => { + let error = std::io::Error::from(error); + let formatted = format!("{error}"); + let doc = alloc.stack([ + alloc.reflow(r"I tried to read this file:"), + alloc.string(filename).annotate(Annotation::Error).indent(4), + alloc.reflow(r"But ran into:"), + alloc.text(formatted).annotate(Annotation::Error).indent(4), + ]); + + Report { + filename: "UNKNOWN.roc".into(), + doc, + title: "FILE PROBLEM".to_string(), + severity: Severity::Fatal, + } + } + } +} diff --git a/crates/roc_compiler_stages.svg b/crates/roc_compiler_stages.svg new file mode 100644 index 0000000000..c250b616ea --- /dev/null +++ b/crates/roc_compiler_stages.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/roc_std/.gitignore b/crates/roc_std/.gitignore similarity index 100% rename from roc_std/.gitignore rename to crates/roc_std/.gitignore diff --git a/crates/roc_std/Cargo.toml b/crates/roc_std/Cargo.toml new file mode 100644 index 0000000000..8318bdad41 --- /dev/null +++ b/crates/roc_std/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "roc_std" +description = "Rust representations of Roc data structures" + +authors = ["The Roc Contributors"] +edition = "2021" +license = "UPL-1.0" +repository = "https://github.com/roc-lang/roc" +version = "0.0.1" + +[dependencies] +arrayvec = "0.7.2" +serde = { version = "1.0.153", optional = true } +static_assertions = "1.1.0" + +[dev-dependencies] +libc = "0.2.139" +pretty_assertions = "1.3.0" +quickcheck = "1.0.3" +quickcheck_macros = "1.0.0" +serde_json = "1.0.94" + +[features] +serde = ["dep:serde"] +std = [] + +[package.metadata.cargo-udeps.ignore] +development = ["quickcheck_macros", "serde_json"] diff --git a/crates/roc_std/src/lib.rs b/crates/roc_std/src/lib.rs new file mode 100644 index 0000000000..587bd4fe27 --- /dev/null +++ b/crates/roc_std/src/lib.rs @@ -0,0 +1,564 @@ +//! Provides Rust representations of Roc data structures. +// #![cfg_attr(not(feature = "std"), no_std)] +#![crate_type = "lib"] + +use arrayvec::ArrayString; +use core::cmp::Ordering; +use core::ffi::c_void; +use core::fmt::{self, Debug}; +use core::hash::{Hash, Hasher}; +use core::mem::{ManuallyDrop, MaybeUninit}; +use core::ops::Drop; +use core::str; + +mod roc_box; +mod roc_dict; +mod roc_list; +mod roc_set; +mod roc_str; +mod storage; + +pub use roc_box::RocBox; +pub use roc_dict::RocDict; +pub use roc_list::{RocList, SendSafeRocList}; +pub use roc_set::RocSet; +pub use roc_str::{InteriorNulError, RocStr, SendSafeRocStr}; +pub use storage::Storage; + +// A list of C functions that are being imported +extern "C" { + pub fn roc_alloc(size: usize, alignment: u32) -> *mut c_void; + pub fn roc_realloc( + ptr: *mut c_void, + new_size: usize, + old_size: usize, + alignment: u32, + ) -> *mut c_void; + pub fn roc_dealloc(ptr: *mut c_void, alignment: u32); + pub fn roc_panic(c_ptr: *mut c_void, tag_id: u32); + pub fn roc_dbg(loc: *mut c_void, msg: *mut c_void); + pub fn roc_memset(dst: *mut c_void, c: i32, n: usize) -> *mut c_void; +} + +pub fn roc_alloc_refcounted() -> *mut T { + let size = core::mem::size_of::(); + let align = core::mem::align_of::(); + + roc_alloc_refcounted_help(size, align) as *mut T +} + +fn roc_alloc_refcounted_help(mut size: usize, mut align: usize) -> *mut u8 { + let prefix = if align > 8 { 16 } else { 8 }; + size += prefix; + align = align.max(core::mem::size_of::()); + + unsafe { + let allocation_ptr = roc_alloc(size, align as _) as *mut u8; + let data_ptr = allocation_ptr.add(prefix); + let storage_ptr = (data_ptr as *mut crate::Storage).sub(1); + + *storage_ptr = Storage::new_reference_counted(); + + data_ptr + } +} + +#[repr(u8)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum RocOrder { + Eq = 0, + Gt = 1, + Lt = 2, +} + +/// Like a Rust `Result`, but following Roc's ABI instead of Rust's. +/// (Using Rust's `Result` instead of this will not work properly with Roc code!) +/// +/// This can be converted to/from a Rust `Result` using `.into()` +#[repr(C)] +pub struct RocResult { + payload: RocResultPayload, + tag: RocResultTag, +} + +impl Debug for RocResult +where + T: Debug, + E: Debug, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self.as_result_of_refs() { + Ok(payload) => { + f.write_str("RocOk(")?; + payload.fmt(f)?; + f.write_str(")") + } + Err(payload) => { + f.write_str("RocErr(")?; + payload.fmt(f)?; + f.write_str(")") + } + } + } +} + +impl Eq for RocResult +where + T: Eq, + E: Eq, +{ +} + +impl PartialEq for RocResult +where + T: PartialEq, + E: PartialEq, +{ + fn eq(&self, other: &Self) -> bool { + self.as_result_of_refs() == other.as_result_of_refs() + } +} + +impl Ord for RocResult +where + T: Ord, + E: Ord, +{ + fn cmp(&self, other: &Self) -> Ordering { + self.as_result_of_refs().cmp(&other.as_result_of_refs()) + } +} + +impl PartialOrd for RocResult +where + T: PartialOrd, + E: PartialOrd, +{ + fn partial_cmp(&self, other: &Self) -> Option { + self.as_result_of_refs() + .partial_cmp(&other.as_result_of_refs()) + } +} + +impl Hash for RocResult +where + T: Hash, + E: Hash, +{ + fn hash(&self, state: &mut H) { + self.as_result_of_refs().hash(state) + } +} + +impl Clone for RocResult +where + T: Clone, + E: Clone, +{ + fn clone(&self) -> Self { + match self.as_result_of_refs() { + Ok(payload) => RocResult::ok(ManuallyDrop::into_inner(payload.clone())), + Err(payload) => RocResult::err(ManuallyDrop::into_inner(payload.clone())), + } + } +} + +impl RocResult { + pub fn ok(payload: T) -> Self { + Self { + tag: RocResultTag::RocOk, + payload: RocResultPayload { + ok: ManuallyDrop::new(payload), + }, + } + } + + pub fn err(payload: E) -> Self { + Self { + tag: RocResultTag::RocErr, + payload: RocResultPayload { + err: ManuallyDrop::new(payload), + }, + } + } + + pub fn is_ok(&self) -> bool { + matches!(self.tag, RocResultTag::RocOk) + } + + pub fn is_err(&self) -> bool { + matches!(self.tag, RocResultTag::RocErr) + } + + fn into_payload(self) -> RocResultPayload { + let mut value = MaybeUninit::uninit(); + + // copy the value into our MaybeUninit memory + unsafe { + core::ptr::copy_nonoverlapping(&self.payload, value.as_mut_ptr(), 1); + } + + // don't run the destructor on self; the `payload` briefly has two owners + // but only `value` is allowed to drop it (after initialization) + core::mem::forget(self); + + unsafe { value.assume_init() } + } + + fn as_result_of_refs(&self) -> Result<&ManuallyDrop, &ManuallyDrop> { + use RocResultTag::*; + + unsafe { + match self.tag { + RocOk => Ok(&self.payload.ok), + RocErr => Err(&self.payload.err), + } + } + } +} + +impl From> for Result { + fn from(roc_result: RocResult) -> Self { + use RocResultTag::*; + + let tag = roc_result.tag; + let payload = roc_result.into_payload(); + + unsafe { + match tag { + RocOk => Ok(ManuallyDrop::into_inner(payload.ok)), + RocErr => Err(ManuallyDrop::into_inner(payload.err)), + } + } + } +} + +impl From> for RocResult { + fn from(result: Result) -> Self { + match result { + Ok(payload) => RocResult::ok(payload), + Err(payload) => RocResult::err(payload), + } + } +} + +#[repr(u8)] +#[derive(Clone, Copy)] +enum RocResultTag { + RocErr = 0, + RocOk = 1, +} + +#[repr(C)] +union RocResultPayload { + ok: ManuallyDrop, + err: ManuallyDrop, +} + +impl Drop for RocResult { + fn drop(&mut self) { + use RocResultTag::*; + + match self.tag { + RocOk => unsafe { ManuallyDrop::drop(&mut self.payload.ok) }, + RocErr => unsafe { ManuallyDrop::drop(&mut self.payload.err) }, + } + } +} + +#[derive(Copy, Clone, PartialEq, Eq, Hash)] +#[repr(C, align(16))] +pub struct RocDec([u8; 16]); + +impl Debug for RocDec { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_tuple("RocDec") + .field(&self.0) + .field(&self.to_str()) + .finish() + } +} + +impl RocDec { + pub const MIN: Self = Self(i128::MIN.to_ne_bytes()); + pub const MAX: Self = Self(i128::MAX.to_ne_bytes()); + + const DECIMAL_PLACES: usize = 18; + const ONE_POINT_ZERO: i128 = 10i128.pow(Self::DECIMAL_PLACES as u32); + const MAX_DIGITS: usize = 39; + const MAX_STR_LENGTH: usize = Self::MAX_DIGITS + 2; // + 2 here to account for the sign & decimal dot + + pub fn new(num: i128) -> Self { + Self(num.to_ne_bytes()) + } + + pub fn as_bits(&self) -> (i64, u64) { + let lower_bits = self.as_i128() as u64; + let upper_bits = (self.as_i128() >> 64) as i64; + (upper_bits, lower_bits) + } + + #[allow(clippy::should_implement_trait)] + pub fn from_str(value: &str) -> Option { + // Split the string into the parts before and after the "." + let mut parts = value.split('.'); + + let before_point = match parts.next() { + Some(answer) => answer, + None => { + return None; + } + }; + + let opt_after_point = match parts.next() { + Some(answer) if answer.len() <= Self::DECIMAL_PLACES => Some(answer), + _ => None, + }; + + // There should have only been one "." in the string! + if parts.next().is_some() { + return None; + } + + // Calculate the low digits - the ones after the decimal point. + let lo = match opt_after_point { + Some(after_point) => { + match after_point.parse::() { + Ok(answer) => { + // Translate e.g. the 1 from 0.1 into 10000000000000000000 + // by "restoring" the elided trailing zeroes to the number! + let trailing_zeroes = Self::DECIMAL_PLACES - after_point.len(); + let lo = answer * 10i128.pow(trailing_zeroes as u32); + + if !before_point.starts_with('-') { + lo + } else { + -lo + } + } + Err(_) => { + return None; + } + } + } + None => 0, + }; + + // Calculate the high digits - the ones before the decimal point. + let (is_pos, digits) = match before_point.chars().next() { + Some('+') => (true, &before_point[1..]), + Some('-') => (false, &before_point[1..]), + _ => (true, before_point), + }; + + let mut hi: i128 = 0; + macro_rules! adjust_hi { + ($op:ident) => {{ + for digit in digits.chars() { + if digit == '_' { + continue; + } + + let digit = digit.to_digit(10)?; + hi = hi.checked_mul(10)?; + hi = hi.$op(digit as _)?; + } + }}; + } + + if is_pos { + adjust_hi!(checked_add); + } else { + adjust_hi!(checked_sub); + } + + match hi.checked_mul(Self::ONE_POINT_ZERO) { + Some(hi) => hi.checked_add(lo).map(|num| Self(num.to_ne_bytes())), + None => None, + } + } + + pub fn from_str_to_i128_unsafe(val: &str) -> i128 { + Self::from_str(val).unwrap().as_i128() + } + + /// This is private because RocDec being an i128 is an implementation detail + #[inline(always)] + fn as_i128(&self) -> i128 { + i128::from_ne_bytes(self.0) + } + + pub fn from_ne_bytes(bytes: [u8; 16]) -> Self { + Self(bytes) + } + + pub fn to_ne_bytes(&self) -> [u8; 16] { + self.0 + } + + fn to_str_helper(self, string: &mut ArrayString<{ Self::MAX_STR_LENGTH }>) -> &str { + use core::fmt::Write; + + if self.as_i128() == 0 { + return "0"; + } + + // The :019 in the following write! is computed as Self::DECIMAL_PLACES + 1. If you change + // Self::DECIMAL_PLACES, this assert should remind you to change that format string as well. + static_assertions::const_assert!(RocDec::DECIMAL_PLACES + 1 == 19); + + // By using the :019 format, we're guaranteeing that numbers less than 1, say 0.01234 + // get their leading zeros placed in bytes for us. i.e. `string = b"0012340000000000000"` + write!(string, "{:019}", self.as_i128()).unwrap(); + + let decimal_location = string.len() - Self::DECIMAL_PLACES; + // skip trailing zeros + let last_nonzero_byte = string.trim_end_matches('0').len(); + + if last_nonzero_byte <= decimal_location { + // This means that we've removed trailing zeros and are left with an integer. Our + // convention is to print these without a decimal point or trailing zeros, so we're done. + string.truncate(decimal_location); + return string.as_str(); + } + + // otherwise, we're dealing with a fraction, and need to insert the decimal dot + + // truncate all extra zeros off + string.truncate(last_nonzero_byte); + + // push a dummy character so we have space for the decimal dot + string.push('$'); + + // Safety: at any time, the string only contains ascii characters, so it is always valid utf8 + let bytes = unsafe { string.as_bytes_mut() }; + + // shift the fractional part by one + bytes.copy_within(decimal_location..last_nonzero_byte, decimal_location + 1); + + // and put in the decimal dot in the right place + bytes[decimal_location] = b'.'; + + string.as_str() + } + + pub fn to_str(&self) -> RocStr { + RocStr::from(self.to_str_helper(&mut ArrayString::new())) + } +} + +impl From for RocDec { + fn from(value: i32) -> Self { + RocDec::from_ne_bytes((RocDec::ONE_POINT_ZERO * value as i128).to_ne_bytes()) + } +} + +impl fmt::Display for RocDec { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(self.to_str_helper(&mut ArrayString::new())) + } +} + +#[repr(C, align(16))] +#[derive(Clone, Copy, Eq, Default)] +pub struct I128([u8; 16]); + +impl From for I128 { + fn from(other: i128) -> Self { + Self(other.to_ne_bytes()) + } +} + +impl From for i128 { + fn from(other: I128) -> Self { + unsafe { core::mem::transmute::(other) } + } +} + +impl fmt::Debug for I128 { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + i128::from(*self).fmt(f) + } +} + +impl fmt::Display for I128 { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + Debug::fmt(&i128::from(*self), f) + } +} + +impl PartialEq for I128 { + fn eq(&self, other: &Self) -> bool { + i128::from(*self).eq(&i128::from(*other)) + } +} + +impl PartialOrd for I128 { + fn partial_cmp(&self, other: &Self) -> Option { + i128::from(*self).partial_cmp(&i128::from(*other)) + } +} + +impl Ord for I128 { + fn cmp(&self, other: &Self) -> Ordering { + i128::from(*self).cmp(&i128::from(*other)) + } +} + +impl Hash for I128 { + fn hash(&self, state: &mut H) { + i128::from(*self).hash(state); + } +} + +#[repr(C, align(16))] +#[derive(Clone, Copy, Eq, Default)] +pub struct U128([u8; 16]); + +impl From for U128 { + fn from(other: u128) -> Self { + Self(other.to_ne_bytes()) + } +} + +impl From for u128 { + fn from(other: U128) -> Self { + unsafe { core::mem::transmute::(other) } + } +} + +impl fmt::Debug for U128 { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + u128::from(*self).fmt(f) + } +} + +impl fmt::Display for U128 { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + Debug::fmt(&u128::from(*self), f) + } +} + +impl PartialEq for U128 { + fn eq(&self, other: &Self) -> bool { + u128::from(*self).eq(&u128::from(*other)) + } +} + +impl PartialOrd for U128 { + fn partial_cmp(&self, other: &Self) -> Option { + u128::from(*self).partial_cmp(&u128::from(*other)) + } +} + +impl Ord for U128 { + fn cmp(&self, other: &Self) -> Ordering { + u128::from(*self).cmp(&u128::from(*other)) + } +} + +impl Hash for U128 { + fn hash(&self, state: &mut H) { + u128::from(*self).hash(state); + } +} diff --git a/crates/roc_std/src/roc_box.rs b/crates/roc_std/src/roc_box.rs new file mode 100644 index 0000000000..23429c38f3 --- /dev/null +++ b/crates/roc_std/src/roc_box.rs @@ -0,0 +1,183 @@ +#![deny(unsafe_op_in_unsafe_fn)] + +use crate::{roc_alloc, roc_dealloc, storage::Storage}; +use core::{ + cell::Cell, + cmp::{self, Ordering}, + fmt::Debug, + mem, + ops::Deref, + ptr::{self, NonNull}, +}; + +#[repr(C)] +pub struct RocBox { + contents: NonNull, +} + +impl RocBox { + pub fn new(contents: T) -> Self { + let alignment = Self::alloc_alignment(); + let bytes = mem::size_of::() + alignment; + + let ptr = unsafe { roc_alloc(bytes, alignment as u32) }; + + if ptr.is_null() { + todo!("Call roc_panic with the info that an allocation failed."); + } + + // Initialize the reference count. + let refcount_one = Storage::new_reference_counted(); + unsafe { ptr.cast::().write(refcount_one) }; + + let contents = unsafe { + let contents_ptr = ptr.cast::().add(alignment).cast::(); + + core::ptr::write(contents_ptr, contents); + + // We already verified that the original alloc pointer was non-null, + // and this one is the alloc pointer with `alignment` bytes added to it, + // so it should be non-null too. + NonNull::new_unchecked(contents_ptr) + }; + + Self { contents } + } + + /// # Safety + /// + /// The box must be unique in order to leak it safely + pub unsafe fn leak(self) -> *mut T { + let ptr = self.contents.as_ptr() as *mut T; + core::mem::forget(self); + ptr + } + + #[inline(always)] + fn alloc_alignment() -> usize { + mem::align_of::().max(mem::align_of::()) + } + + pub fn into_inner(self) -> T { + unsafe { ptr::read(self.contents.as_ptr() as *mut T) } + } + + fn storage(&self) -> &Cell { + let alignment = Self::alloc_alignment(); + + unsafe { + &*self + .contents + .as_ptr() + .cast::() + .sub(alignment) + .cast::>() + } + } +} + +impl Deref for RocBox { + type Target = T; + + fn deref(&self) -> &Self::Target { + unsafe { self.contents.as_ref() } + } +} + +impl PartialEq> for RocBox +where + T: PartialEq, +{ + fn eq(&self, other: &RocBox) -> bool { + self.deref() == other.deref() + } +} + +impl Eq for RocBox where T: Eq {} + +impl PartialOrd> for RocBox +where + T: PartialOrd, +{ + fn partial_cmp(&self, other: &RocBox) -> Option { + let self_contents = unsafe { self.contents.as_ref() }; + let other_contents = unsafe { other.contents.as_ref() }; + + self_contents.partial_cmp(other_contents) + } +} + +impl Ord for RocBox +where + T: Ord, +{ + fn cmp(&self, other: &Self) -> Ordering { + let self_contents = unsafe { self.contents.as_ref() }; + let other_contents = unsafe { other.contents.as_ref() }; + + self_contents.cmp(other_contents) + } +} + +impl core::hash::Hash for RocBox { + fn hash(&self, state: &mut H) { + self.contents.hash(state) + } +} + +impl Debug for RocBox +where + T: Debug, +{ + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + self.deref().fmt(f) + } +} + +impl Clone for RocBox { + fn clone(&self) -> Self { + let storage = self.storage(); + let mut new_storage = storage.get(); + + // Increment the reference count + if !new_storage.is_readonly() { + new_storage.increment_reference_count(); + storage.set(new_storage); + } + + Self { + contents: self.contents, + } + } +} + +impl Drop for RocBox { + fn drop(&mut self) { + let storage = self.storage(); + let contents = self.contents; + + // Decrease the list's reference count. + let mut new_storage = storage.get(); + let needs_dealloc = new_storage.decrease(); + + if needs_dealloc { + unsafe { + // Drop the stored contents. + let contents_ptr = contents.as_ptr(); + + mem::drop::(ptr::read(contents_ptr)); + + let alignment = Self::alloc_alignment(); + + // Release the memory. + roc_dealloc( + contents.as_ptr().cast::().sub(alignment).cast(), + alignment as u32, + ); + } + } else if !new_storage.is_readonly() { + // Write the storage back. + storage.set(new_storage); + } + } +} diff --git a/crates/roc_std/src/roc_dict.rs b/crates/roc_std/src/roc_dict.rs new file mode 100644 index 0000000000..918c9558c9 --- /dev/null +++ b/crates/roc_std/src/roc_dict.rs @@ -0,0 +1,210 @@ +use crate::roc_list::RocList; +use core::{ + fmt::{self, Debug}, + hash::Hash, + mem::{align_of, ManuallyDrop}, +}; + +/// At the moment, Roc's Dict is just an association list. Its lookups are O(n) but +/// we haven't grown such big programs that it's a problem yet! +/// +/// We do some things in this data structure that only make sense because the +/// memory is managed in Roc: +/// +/// 1. We don't implement an [`IntoIterator`] that iterates over owned values, +/// since Roc owns the memory, not rust. +/// 2. We use a union for [`RocDictItem`] instead of just a struct. See the +/// comment on that data structure for why. +#[derive(Default, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[repr(transparent)] +pub struct RocDict(RocList>); + +impl RocDict { + pub fn len(&self) -> usize { + self.0.len() + } + + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + pub fn with_capacity(capacity: usize) -> Self { + Self(RocList::with_capacity(capacity)) + } + + pub fn iter(&self) -> impl Iterator { + self.0.iter().map(|item| (item.key(), item.value())) + } + + pub fn iter_keys(&self) -> impl Iterator { + self.0.iter().map(|item| item.key()) + } + + pub fn iter_values(&self) -> impl Iterator { + self.0.iter().map(|item| item.value()) + } +} + +impl RocDict { + unsafe fn insert_unchecked(&mut self, _key: K, _val: V) { + todo!(); + } +} + +impl FromIterator<(K, V)> for RocDict { + fn from_iter>(into_iter: T) -> Self { + let src = into_iter.into_iter(); + let mut ret = Self::with_capacity(src.size_hint().0); + + for (key, val) in src { + unsafe { + ret.insert_unchecked(key, val); + } + } + + ret + } +} + +impl<'a, K, V> IntoIterator for &'a RocDict { + type Item = (&'a K, &'a V); + type IntoIter = IntoIter<'a, K, V>; + + fn into_iter(self) -> Self::IntoIter { + IntoIter { + index: 0, + items: self.0.as_slice(), + } + } +} + +pub struct IntoIter<'a, K, V> { + index: usize, + items: &'a [RocDictItem], +} + +impl<'a, K, V> Iterator for IntoIter<'a, K, V> { + type Item = (&'a K, &'a V); + + fn next(&mut self) -> Option { + let item = self + .items + .get(self.index) + .map(|item| (item.key(), item.value())); + + self.index += 1; + + item + } + + fn size_hint(&self) -> (usize, Option) { + let remaining = self.items.len() - self.index; + + (remaining, Some(remaining)) + } +} + +impl Debug for RocDict { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("RocDict ")?; + + f.debug_map() + .entries(self.iter().map(|(k, v)| (k, v))) + .finish() + } +} + +/// Roc is constructing these values according to its memory layout rules. +/// Specifically: +/// +/// 1. fields with the highest alignment go first +/// 2. then fields are sorted alphabetically +/// +/// Taken together, these mean that if we have a value with higher alignment +/// than the key, it'll be first in memory. Otherwise, the key will be first. +/// Fortunately, the total amount of memory doesn't change, so we can use a +/// union and disambiguate by examining the alignment of the key and value. +/// +/// However, note that this only makes sense while we're storing KV pairs +/// contiguously in memory. If we separate them at some point, we'll need to +/// change this implementation drastically! +#[derive(Eq)] +#[repr(C)] +union RocDictItem { + key_first: ManuallyDrop>, + value_first: ManuallyDrop>, +} + +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[repr(C)] +struct KeyFirst { + key: K, + value: V, +} + +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[repr(C)] +struct ValueFirst { + value: V, + key: K, +} + +impl RocDictItem { + fn key(&self) -> &K { + if align_of::() >= align_of::() { + unsafe { &self.key_first.key } + } else { + unsafe { &self.value_first.key } + } + } + + fn value(&self) -> &V { + if align_of::() >= align_of::() { + unsafe { &self.key_first.value } + } else { + unsafe { &self.value_first.value } + } + } +} + +impl Drop for RocDictItem { + fn drop(&mut self) { + if align_of::() >= align_of::() { + unsafe { ManuallyDrop::drop(&mut self.key_first) } + } else { + unsafe { ManuallyDrop::drop(&mut self.value_first) } + } + } +} + +impl PartialEq for RocDictItem { + fn eq(&self, other: &Self) -> bool { + self.key() == other.key() && self.value() == other.value() + } +} + +impl PartialOrd for RocDictItem { + fn partial_cmp(&self, other: &Self) -> Option { + self.key().partial_cmp(other.key()).map(|key_cmp| { + match self.value().partial_cmp(other.value()) { + Some(value_cmp) => key_cmp.then(value_cmp), + None => key_cmp, + } + }) + } +} + +impl Ord for RocDictItem { + fn cmp(&self, other: &Self) -> core::cmp::Ordering { + self.key() + .cmp(other.key()) + .then(self.value().cmp(other.value())) + } +} + +impl Hash for RocDictItem { + fn hash(&self, state: &mut H) { + self.key().hash(state); + self.value().hash(state); + } +} diff --git a/crates/roc_std/src/roc_list.rs b/crates/roc_std/src/roc_list.rs new file mode 100644 index 0000000000..5d454253de --- /dev/null +++ b/crates/roc_std/src/roc_list.rs @@ -0,0 +1,858 @@ +#![deny(unsafe_op_in_unsafe_fn)] + +use core::{ + cell::Cell, + cmp::{self, Ordering}, + ffi::c_void, + fmt::Debug, + hash::Hash, + intrinsics::copy_nonoverlapping, + iter::FromIterator, + mem::{self, ManuallyDrop}, + ops::{Deref, DerefMut}, + ptr::{self, NonNull}, +}; + +use crate::{roc_alloc, roc_dealloc, roc_realloc, storage::Storage}; + +#[cfg(feature = "serde")] +use core::marker::PhantomData; +#[cfg(feature = "serde")] +use serde::{ + de::{Deserializer, Visitor}, + ser::{SerializeSeq, Serializer}, + Deserialize, Serialize, +}; + +#[repr(C)] +pub struct RocList { + elements: Option>>, + length: usize, + // This technically points to directly after the refcount. + // This is an optimization that enables use one code path for regular lists and slices for geting the refcount ptr. + capacity_or_ref_ptr: usize, +} + +impl RocList { + #[inline(always)] + fn alloc_alignment() -> u32 { + mem::align_of::().max(mem::align_of::()) as u32 + } + + pub fn empty() -> Self { + Self { + elements: None, + length: 0, + capacity_or_ref_ptr: 0, + } + } + + /// Create an empty RocList with enough space preallocated to store + /// the requested number of elements. + pub fn with_capacity(num_elems: usize) -> Self { + Self { + elements: Some(Self::elems_with_capacity(num_elems)), + length: 0, + capacity_or_ref_ptr: num_elems, + } + } + + pub fn iter(&self) -> impl Iterator { + self.into_iter() + } + + /// Used for both roc_alloc and roc_realloc - given the number of elements, + /// returns the number of bytes needed to allocate, taking into account both the + /// size of the elements as well as the size of Storage. + fn alloc_bytes(num_elems: usize) -> usize { + next_multiple_of(mem::size_of::(), mem::align_of::()) + + (num_elems * mem::size_of::()) + } + + fn elems_with_capacity(num_elems: usize) -> NonNull> { + let alloc_ptr = unsafe { roc_alloc(Self::alloc_bytes(num_elems), Self::alloc_alignment()) }; + + Self::elems_from_allocation(NonNull::new(alloc_ptr).unwrap_or_else(|| { + todo!("Call roc_panic with the info that an allocation failed."); + })) + } + + fn elems_from_allocation(allocation: NonNull) -> NonNull> { + let offset = Self::alloc_alignment() - core::mem::size_of::<*const u8>() as u32; + let alloc_ptr = allocation.as_ptr(); + + unsafe { + let elem_ptr = Self::elem_ptr_from_alloc_ptr(alloc_ptr).cast::>(); + + // Initialize the reference count. + let rc_ptr = alloc_ptr.offset(offset as isize); + rc_ptr + .cast::() + .write(Storage::new_reference_counted()); + + // The original alloc pointer was non-null, and this one is the alloc pointer + // with `alignment` bytes added to it, so it should be non-null too. + NonNull::new_unchecked(elem_ptr) + } + } + + pub fn len(&self) -> usize { + self.length & (isize::MAX as usize) + } + + pub fn is_seamless_slice(&self) -> bool { + ((self.length | self.capacity_or_ref_ptr) as isize) < 0 + } + + pub fn capacity(&self) -> usize { + if !self.is_seamless_slice() { + self.capacity_or_ref_ptr + } else { + self.len() + } + } + + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + pub fn is_unique(&self) -> bool { + if let Some(storage) = self.storage() { + storage.is_unique() + } else { + // If there is no storage, this list is empty. + // An empty list is always unique. + true + } + } + + pub fn is_readonly(&self) -> bool { + if let Some(storage) = self.storage() { + storage.is_readonly() + } else { + false + } + } + + pub fn as_mut_ptr(&mut self) -> *mut T { + self.as_mut_slice().as_mut_ptr() + } + + pub fn as_ptr(&self) -> *const T { + self.as_slice().as_ptr() + } + + /// Marks a list as readonly. This means that it will be leaked. + /// For constants passed in from platform to application, this may be reasonable. + /// + /// # Safety + /// + /// A value can be read-only in Roc for 3 reasons: + /// 1. The value is stored in read-only memory like a constant in the app. + /// 2. Our refcounting maxes out. When that happens, we saturate to read-only. + /// 3. This function is called + /// + /// Any value that is set to read-only will be leaked. + /// There is no way to tell how many references it has and if it is safe to free. + /// As such, only values that should have a static lifetime for the entire application run + /// should be considered for marking read-only. + pub unsafe fn set_readonly(&self) { + if let Some((_, storage)) = self.elements_and_storage() { + storage.set(Storage::Readonly); + } + } + + /// Note that there is no way to convert directly to a Vec. + /// + /// This is because RocList values are not allocated using the system allocator, so + /// handing off any heap-allocated bytes to a Vec would not work because its Drop + /// implementation would try to free those bytes using the wrong allocator. + /// + /// Instead, if you want a Rust Vec, you need to do a fresh allocation and copy the + /// bytes over - in other words, calling this `as_slice` method and then calling `to_vec` + /// on that. + pub fn as_slice(&self) -> &[T] { + self + } + + /// Note that there is no way to convert directly to a Vec. + /// + /// This is because RocList values are not allocated using the system allocator, so + /// handing off any heap-allocated bytes to a Vec would not work because its Drop + /// implementation would try to free those bytes using the wrong allocator. + /// + /// Instead, if you want a Rust Vec, you need to do a fresh allocation and copy the + /// bytes over - in other words, calling this `as_slice` method and then calling `to_vec` + /// on that. + pub fn as_mut_slice(&mut self) -> &mut [T] { + &mut *self + } + + #[inline(always)] + fn elements_and_storage(&self) -> Option<(NonNull>, &Cell)> { + let elements = self.elements?; + + let offset = match mem::align_of::() { + 16 => 1, + 8 | 4 | 2 | 1 => 0, + other => unreachable!("invalid alignment {other}"), + }; + + let storage = unsafe { &*self.ptr_to_allocation().cast::>().add(offset) }; + Some((elements, storage)) + } + + pub(crate) fn storage(&self) -> Option { + self.elements_and_storage() + .map(|(_, storage)| storage.get()) + } + + /// Useful for doing memcpy on the elements. Returns NULL if list is empty. + pub(crate) unsafe fn ptr_to_first_elem(&self) -> *const T { + unsafe { core::mem::transmute(self.elements) } + } + + /// Useful for doing memcpy on the underlying allocation. Returns NULL if list is empty. + pub(crate) fn ptr_to_allocation(&self) -> *mut c_void { + let alignment = Self::alloc_alignment() as usize; + if self.is_seamless_slice() { + ((self.capacity_or_ref_ptr << 1) - alignment) as *mut _ + } else { + unsafe { self.ptr_to_first_elem().cast::().sub(alignment) as *mut _ } + } + } + + unsafe fn elem_ptr_from_alloc_ptr(alloc_ptr: *mut c_void) -> *mut c_void { + unsafe { + alloc_ptr + .cast::() + .add(Self::alloc_alignment() as usize) + .cast() + } + } +} + +impl RocList +where + T: Clone, +{ + pub fn from_slice(slice: &[T]) -> Self { + let mut list = Self::empty(); + list.extend_from_slice(slice); + list + } + + pub fn extend_from_slice(&mut self, slice: &[T]) { + // TODO: Can we do better for ZSTs? Alignment might be a problem. + if slice.is_empty() { + return; + } + + let new_len = self.len() + slice.len(); + let non_null_elements = if let Some((elements, storage)) = self.elements_and_storage() { + // Decrement the list's refence count. + let mut copy = storage.get(); + let is_unique = copy.decrease(); + + if is_unique { + // If we have enough capacity, we can add to the existing elements in-place. + if self.capacity() >= slice.len() { + elements + } else { + // There wasn't enough capacity, so we need a new allocation. + // Since this is a unique RocList, we can use realloc here. + let new_ptr = unsafe { + roc_realloc( + storage.as_ptr().cast(), + Self::alloc_bytes(new_len), + Self::alloc_bytes(self.capacity()), + Self::alloc_alignment(), + ) + }; + + self.capacity_or_ref_ptr = new_len; + + Self::elems_from_allocation(NonNull::new(new_ptr).unwrap_or_else(|| { + todo!("Reallocation failed"); + })) + } + } else { + if !copy.is_readonly() { + // Write the decremented reference count back. + storage.set(copy); + } + + // Allocate new memory. + self.capacity_or_ref_ptr = slice.len(); + let new_elements = Self::elems_with_capacity(slice.len()); + + // Copy the old elements to the new allocation. + unsafe { + copy_nonoverlapping(elements.as_ptr(), new_elements.as_ptr(), self.len()); + } + // Clear the seamless slice bit since we now have clear ownership. + self.length = self.len(); + + new_elements + } + } else { + self.capacity_or_ref_ptr = slice.len(); + Self::elems_with_capacity(slice.len()) + }; + + self.elements = Some(non_null_elements); + + let elements = self.elements.unwrap().as_ptr(); + + let append_ptr = unsafe { elements.add(self.len()) }; + + // Use .cloned() to increment the elements' reference counts, if needed. + for (i, new_elem) in slice.iter().cloned().enumerate() { + let dst = unsafe { append_ptr.add(i) }; + unsafe { + // Write the element into the slot, without dropping it. + ptr::write(dst, ManuallyDrop::new(new_elem)); + } + + // It's important that the length is increased one by one, to + // make sure that we don't drop uninitialized elements, even when + // a incrementing the reference count panics. + self.length += 1; + } + } +} + +impl RocList { + /// Increase a RocList's capacity by at least the requested number of elements (possibly more). + /// + /// May return a new RocList, if the provided one was not unique. + pub fn reserve(&mut self, num_elems: usize) { + let new_len = num_elems + self.len(); + let new_elems; + let old_elements_ptr; + + match self.elements_and_storage() { + Some((elements, storage)) => { + if storage.get().is_unique() { + unsafe { + let old_alloc = self.ptr_to_allocation(); + + // Try to reallocate in-place. + let new_alloc = roc_realloc( + old_alloc, + Self::alloc_bytes(new_len), + Self::alloc_bytes(self.capacity()), + Self::alloc_alignment(), + ); + + if new_alloc == old_alloc { + // We successfully reallocated in-place; we're done! + return; + } else { + // We got back a different allocation; copy the existing elements + // into it. We don't need to increment their refcounts because + // The existing allocation that references to them is now gone and + // no longer referencing them. + new_elems = Self::elems_from_allocation( + NonNull::new(new_alloc).unwrap_or_else(|| { + todo!("Reallocation failed"); + }), + ); + } + + // Note that realloc automatically deallocates the old allocation, + // so we don't need to call roc_dealloc here. + } + } else { + // Make a new allocation + new_elems = Self::elems_with_capacity(new_len); + old_elements_ptr = elements.as_ptr(); + + unsafe { + // Copy the old elements to the new allocation. + copy_nonoverlapping(old_elements_ptr, new_elems.as_ptr(), self.len()); + } + + // Decrease the current allocation's reference count. + let mut new_storage = storage.get(); + + if !new_storage.is_readonly() { + let needs_dealloc = new_storage.decrease(); + + if needs_dealloc { + // Unlike in Drop, do *not* decrement the refcounts of all the elements! + // The new allocation is referencing them, so instead of incrementing them all + // all just to decrement them again here, we neither increment nor decrement them. + unsafe { + roc_dealloc(self.ptr_to_allocation(), Self::alloc_alignment()); + } + } else { + // Write the storage back. + storage.set(new_storage); + } + } + } + } + None => { + // This is an empty list, so `reserve` is the same as `with_capacity`. + self.update_to(Self::with_capacity(new_len)); + + return; + } + } + + self.update_to(Self { + elements: Some(new_elems), + length: self.len(), + capacity_or_ref_ptr: new_len, + }); + } + + /// Replace self with a new version, without letting `drop` run in between. + fn update_to(&mut self, mut updated: Self) { + // We want to replace `self` with `updated` in a way that makes sure + // `self`'s `drop` never runs. This is the proper way to do that: + // swap them, and then forget the "updated" one (which is now pointing + // to the original allocation). + mem::swap(self, &mut updated); + mem::forget(updated); + } +} + +impl Deref for RocList { + type Target = [T]; + + fn deref(&self) -> &Self::Target { + if let Some(elements) = self.elements { + let elements = ptr::slice_from_raw_parts(elements.as_ptr().cast::(), self.len()); + + unsafe { &*elements } + } else { + &[] + } + } +} + +impl DerefMut for RocList { + fn deref_mut(&mut self) -> &mut Self::Target { + if let Some(elements) = self.elements { + let ptr = elements.as_ptr().cast::(); + let elements = ptr::slice_from_raw_parts_mut(ptr, self.length); + + unsafe { &mut *elements } + } else { + &mut [] + } + } +} + +impl Default for RocList { + fn default() -> Self { + Self::empty() + } +} + +impl PartialEq> for RocList +where + T: PartialEq, +{ + fn eq(&self, other: &RocList) -> bool { + self.as_slice() == other.as_slice() + } +} + +impl Eq for RocList where T: Eq {} + +impl PartialOrd> for RocList +where + T: PartialOrd, +{ + fn partial_cmp(&self, other: &RocList) -> Option { + // If one is longer than the other, use that as the ordering. + match self.len().partial_cmp(&other.len()) { + Some(Ordering::Equal) => {} + ord => return ord, + } + + // If they're the same length, compare their elements + for index in 0..self.len() { + match self[index].partial_cmp(&other[index]) { + Some(Ordering::Equal) => {} + ord => return ord, + } + } + + // Capacity is ignored for ordering purposes. + Some(Ordering::Equal) + } +} + +impl Ord for RocList +where + T: Ord, +{ + fn cmp(&self, other: &Self) -> Ordering { + // If one is longer than the other, use that as the ordering. + match self.len().cmp(&other.len()) { + Ordering::Equal => {} + ord => return ord, + } + + // If they're the same length, compare their elements + for index in 0..self.len() { + match self[index].cmp(&other[index]) { + Ordering::Equal => {} + ord => return ord, + } + } + + // Capacity is ignored for ordering purposes. + Ordering::Equal + } +} + +impl Debug for RocList +where + T: Debug, +{ + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + self.deref().fmt(f) + } +} + +impl Clone for RocList { + fn clone(&self) -> Self { + // Increment the reference count + if let Some((_, storage)) = self.elements_and_storage() { + let mut new_storage = storage.get(); + + if !new_storage.is_readonly() { + new_storage.increment_reference_count(); + storage.set(new_storage); + } + } + + Self { + elements: self.elements, + length: self.length, + capacity_or_ref_ptr: self.capacity_or_ref_ptr, + } + } +} + +impl Drop for RocList { + fn drop(&mut self) { + if let Some((elements, storage)) = self.elements_and_storage() { + // Decrease the list's reference count. + let mut new_storage = storage.get(); + + if !new_storage.is_readonly() { + let needs_dealloc = new_storage.decrease(); + + if needs_dealloc { + unsafe { + // Drop the stored elements. + for index in 0..self.len() { + ManuallyDrop::drop(&mut *elements.as_ptr().add(index)); + } + + // Release the memory. + roc_dealloc(self.ptr_to_allocation(), Self::alloc_alignment()); + } + } else { + // Write the storage back. + storage.set(new_storage); + } + } + } + } +} + +impl From<&[T]> for RocList +where + T: Clone, +{ + fn from(slice: &[T]) -> Self { + Self::from_slice(slice) + } +} + +impl From<[T; SIZE]> for RocList { + fn from(array: [T; SIZE]) -> Self { + Self::from_iter(array) + } +} + +impl<'a, T> IntoIterator for &'a RocList { + type Item = &'a T; + type IntoIter = core::slice::Iter<'a, T>; + + fn into_iter(self) -> Self::IntoIter { + self.as_slice().iter() + } +} + +impl Hash for RocList { + fn hash(&self, state: &mut H) { + // This is the same as Rust's Vec implementation, which + // just delegates to the slice implementation. It's a bit surprising + // that Hash::hash_slice doesn't automatically incorporate the length, + // but the slice implementation indeed does explicitly call self.len().hash(state); + // + // To verify, click the "source" links for: + // Vec: https://doc.rust-lang.org/std/vec/struct.Vec.html#impl-Hash + // slice: https://doc.rust-lang.org/std/primitive.slice.html#impl-Hash + self.len().hash(state); + + Hash::hash_slice(self.as_slice(), state); + } +} + +impl FromIterator for RocList { + fn from_iter(into: I) -> Self + where + I: IntoIterator, + { + let iter = into.into_iter(); + + if core::mem::size_of::() == 0 { + let count = iter.count(); + return Self { + elements: Some(Self::elems_with_capacity(count)), + length: count, + capacity_or_ref_ptr: count, + }; + } + + let mut list = { + let (min_len, maybe_max_len) = iter.size_hint(); + let init_capacity = maybe_max_len.unwrap_or(min_len); + Self::with_capacity(init_capacity) + }; + + let mut elements = list.elements.unwrap().as_ptr(); + for new_elem in iter { + // If the size_hint didn't give us a max, we may need to grow. 1.5x seems to be good, based on: + // https://archive.ph/Z2R8w and https://github.com/facebook/folly/blob/1f2706/folly/docs/FBVector.md + if list.length == list.capacity() { + list.reserve(list.capacity() / 2); + elements = list.elements.unwrap().as_ptr(); + } + + unsafe { + elements + .add(list.length) + .write(ptr::read(&ManuallyDrop::new(new_elem))); + } + list.length += 1; + } + + list + } +} + +#[cfg(feature = "serde")] +impl Serialize for RocList { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut seq = serializer.serialize_seq(Some(self.len()))?; + for item in self { + seq.serialize_element(item)?; + } + seq.end() + } +} + +#[cfg(feature = "serde")] +impl<'de, T> Deserialize<'de> for RocList +where + // TODO: I'm not sure about requiring clone here. Is that fine? Is that + // gonna mean lots of extra allocations? + T: Deserialize<'de> + core::clone::Clone, +{ + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + deserializer.deserialize_seq(RocListVisitor::new()) + } +} + +// This is a RocList that is checked to ensure it is unique or readonly such that it can be sent between threads safely. +#[repr(transparent)] +pub struct SendSafeRocList(RocList); + +unsafe impl Send for SendSafeRocList where T: Send {} + +impl Clone for SendSafeRocList +where + T: Clone, +{ + fn clone(&self) -> Self { + if self.0.is_readonly() { + SendSafeRocList(self.0.clone()) + } else { + // To keep self send safe, this must copy. + SendSafeRocList(RocList::from_slice(&self.0)) + } + } +} + +impl From> for SendSafeRocList +where + T: Clone, +{ + fn from(l: RocList) -> Self { + if l.is_unique() || l.is_readonly() { + SendSafeRocList(l) + } else { + // This is not unique, do a deep copy. + // TODO: look into proper into_iter that takes ownership. + // Then this won't need clone and will skip and refcount inc and dec for each element. + SendSafeRocList(RocList::from_slice(&l)) + } + } +} + +impl From> for RocList { + fn from(l: SendSafeRocList) -> Self { + l.0 + } +} + +#[cfg(feature = "serde")] +struct RocListVisitor { + marker: PhantomData, +} + +#[cfg(feature = "serde")] +impl RocListVisitor { + fn new() -> Self { + RocListVisitor { + marker: PhantomData, + } + } +} + +#[cfg(feature = "serde")] +impl<'de, T> Visitor<'de> for RocListVisitor +where + T: Deserialize<'de> + core::clone::Clone, +{ + type Value = RocList; + + fn expecting(&self, formatter: &mut core::fmt::Formatter) -> core::fmt::Result { + write!(formatter, "a list of strings") + } + + fn visit_seq(self, mut seq: A) -> Result + where + A: serde::de::SeqAccess<'de>, + { + let mut out = match seq.size_hint() { + Some(hint) => RocList::with_capacity(hint), + None => RocList::empty(), + }; + + while let Some(next) = seq.next_element()? { + // TODO: it would be ideal to call `out.push` here, but we haven't + // implemented that yet! I think this is also why we need Clone. + out.extend_from_slice(&[next]) + } + + Ok(out) + } +} + +const fn next_multiple_of(lhs: usize, rhs: usize) -> usize { + match lhs % rhs { + 0 => lhs, + r => lhs + (rhs - r), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::RocDec; + + #[no_mangle] + pub unsafe extern "C" fn roc_alloc(size: usize, _alignment: u32) -> *mut c_void { + unsafe { libc::malloc(size) } + } + + #[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 { + unsafe { libc::realloc(c_ptr, new_size) } + } + + #[no_mangle] + pub unsafe extern "C" fn roc_dealloc(c_ptr: *mut c_void, _alignment: u32) { + unsafe { libc::free(c_ptr) } + } + + #[test] + fn compare_list_dec() { + // RocDec is special because it's alignment is 16 + let a = RocList::from_slice(&[RocDec::from(1), RocDec::from(2)]); + let b = RocList::from_slice(&[RocDec::from(1), RocDec::from(2)]); + + assert_eq!(a, b); + } + + #[test] + fn clone_list_dec() { + // RocDec is special because it's alignment is 16 + let a = RocList::from_slice(&[RocDec::from(1), RocDec::from(2)]); + let b = a.clone(); + + assert_eq!(a, b); + + drop(a); + drop(b); + } + + #[test] + fn compare_list_str() { + let a = RocList::from_slice(&[crate::RocStr::from("ab")]); + let b = RocList::from_slice(&[crate::RocStr::from("ab")]); + + assert_eq!(a, b); + + drop(a); + drop(b); + } + + #[test] + fn readonly_list_is_sendsafe() { + let x = RocList::from_slice(&[1, 2, 3, 4, 5]); + unsafe { x.set_readonly() }; + assert!(x.is_readonly()); + + let y = x.clone(); + let z = y.clone(); + + let safe_x = SendSafeRocList::from(x); + let new_x = RocList::from(safe_x); + assert!(new_x.is_readonly()); + assert!(y.is_readonly()); + assert!(z.is_readonly()); + assert_eq!(new_x.as_slice(), &[1, 2, 3, 4, 5]); + + let ptr = new_x.ptr_to_allocation(); + + drop(y); + drop(z); + drop(new_x); + + // free the underlying memory + unsafe { crate::roc_dealloc(ptr, std::mem::align_of::() as u32) } + } +} diff --git a/crates/roc_std/src/roc_set.rs b/crates/roc_std/src/roc_set.rs new file mode 100644 index 0000000000..683045b8a4 --- /dev/null +++ b/crates/roc_std/src/roc_set.rs @@ -0,0 +1,44 @@ +use crate::roc_dict::RocDict; +use core::{ + fmt::{self, Debug}, + hash::Hash, +}; + +#[derive(Default, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct RocSet(RocDict); + +impl RocSet { + pub fn len(&self) -> usize { + self.0.len() + } + + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + #[allow(unused)] + pub fn with_capacity(capacity: usize) -> Self { + Self(RocDict::with_capacity(capacity)) + } + + #[allow(unused)] + pub fn iter(&self) -> impl Iterator { + self.0.iter_keys() + } +} + +impl FromIterator for RocSet { + fn from_iter>(into_iter: I) -> Self { + Self(RocDict::from_iter( + into_iter.into_iter().map(|elem| (elem, ())), + )) + } +} + +impl Debug for RocSet { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("RocSet ")?; + + f.debug_set().entries(self.iter()).finish() + } +} diff --git a/crates/roc_std/src/roc_str.rs b/crates/roc_std/src/roc_str.rs new file mode 100644 index 0000000000..b48be650aa --- /dev/null +++ b/crates/roc_std/src/roc_str.rs @@ -0,0 +1,834 @@ +#![deny(unsafe_op_in_unsafe_fn)] + +#[cfg(feature = "serde")] +use serde::{ + de::{Deserializer, Visitor}, + ser::Serializer, + Deserialize, Serialize, +}; + +use core::{ + cmp, + convert::TryFrom, + fmt, + hash::{self, Hash}, + mem::{self, size_of, ManuallyDrop}, + ops::{Deref, DerefMut}, + ptr, +}; + +#[cfg(feature = "std")] +use std::ffi::{CStr, CString}; + +use crate::RocList; + +#[repr(transparent)] +pub struct RocStr(RocStrInner); + +fn with_stack_bytes(length: usize, closure: F) -> T +where + F: FnOnce(*mut E) -> T, +{ + use crate::{roc_alloc, roc_dealloc}; + use core::mem::MaybeUninit; + + if length < RocStr::TEMP_STR_MAX_STACK_BYTES { + // TODO: once https://doc.rust-lang.org/std/mem/union.MaybeUninit.html#method.uninit_array + // has become stabilized, use that here in order to do a precise + // stack allocation instead of always over-allocating to 64B. + let mut bytes: MaybeUninit<[u8; RocStr::TEMP_STR_MAX_STACK_BYTES]> = MaybeUninit::uninit(); + + closure(bytes.as_mut_ptr() as *mut E) + } else { + let align = core::mem::align_of::() as u32; + // The string is too long to stack-allocate, so + // do a heap allocation and then free it afterwards. + let ptr = unsafe { roc_alloc(length, align) } as *mut E; + let answer = closure(ptr); + + // Free the heap allocation. + unsafe { roc_dealloc(ptr.cast(), align) }; + + answer + } +} + +impl RocStr { + pub const SIZE: usize = core::mem::size_of::(); + pub const MASK: u8 = 0b1000_0000; + + pub const fn empty() -> Self { + Self(RocStrInner { + small_string: SmallString::empty(), + }) + } + + /// Create a string from bytes. + /// + /// # Safety + /// + /// `slice` must be valid UTF-8. + pub unsafe fn from_slice_unchecked(slice: &[u8]) -> Self { + if let Some(small_string) = unsafe { SmallString::try_from_utf8_bytes(slice) } { + Self(RocStrInner { small_string }) + } else { + let heap_allocated = RocList::from_slice(slice); + Self(RocStrInner { + heap_allocated: ManuallyDrop::new(heap_allocated), + }) + } + } + + fn is_small_str(&self) -> bool { + unsafe { self.0.small_string.is_small_str() } + } + + fn as_enum_ref(&self) -> RocStrInnerRef { + if self.is_small_str() { + unsafe { RocStrInnerRef::SmallString(&self.0.small_string) } + } else { + unsafe { RocStrInnerRef::HeapAllocated(&self.0.heap_allocated) } + } + } + + pub fn capacity(&self) -> usize { + match self.as_enum_ref() { + RocStrInnerRef::HeapAllocated(roc_list) => roc_list.capacity(), + RocStrInnerRef::SmallString(_) => SmallString::CAPACITY, + } + } + + pub fn len(&self) -> usize { + match self.as_enum_ref() { + RocStrInnerRef::HeapAllocated(h) => h.len(), + RocStrInnerRef::SmallString(s) => s.len(), + } + } + + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + pub fn is_unique(&self) -> bool { + match self.as_enum_ref() { + RocStrInnerRef::HeapAllocated(roc_list) => roc_list.is_unique(), + RocStrInnerRef::SmallString(_) => true, + } + } + + pub fn is_readonly(&self) -> bool { + match self.as_enum_ref() { + RocStrInnerRef::HeapAllocated(roc_list) => roc_list.is_readonly(), + RocStrInnerRef::SmallString(_) => false, + } + } + + /// Marks a str as readonly. This means that it will be leaked. + /// For constants passed in from platform to application, this may be reasonable. + /// + /// # Safety + /// + /// A value can be read-only in Roc for 3 reasons: + /// 1. The value is stored in read-only memory like a constant in the app. + /// 2. Our refcounting maxes out. When that happens, we saturate to read-only. + /// 3. This function is called + /// + /// Any value that is set to read-only will be leaked. + /// There is no way to tell how many references it has and if it is safe to free. + /// As such, only values that should have a static lifetime for the entire application run + /// should be considered for marking read-only. + pub unsafe fn set_readonly(&self) { + match self.as_enum_ref() { + RocStrInnerRef::HeapAllocated(roc_list) => unsafe { roc_list.set_readonly() }, + RocStrInnerRef::SmallString(_) => {} + } + } + + /// Note that there is no way to convert directly to a String. + /// + /// This is because RocStr values are not allocated using the system allocator, so + /// handing off any heap-allocated bytes to a String would not work because its Drop + /// implementation would try to free those bytes using the wrong allocator. + /// + /// Instead, if you want a Rust String, you need to do a fresh allocation and copy the + /// bytes over - in other words, calling this `as_str` method and then calling `to_string` + /// on that. + pub fn as_str(&self) -> &str { + self + } + + /// Create an empty RocStr with enough space preallocated to store + /// the requested number of bytes. + pub fn with_capacity(bytes: usize) -> Self { + if bytes <= SmallString::CAPACITY { + RocStr(RocStrInner { + small_string: SmallString::empty(), + }) + } else { + // The requested capacity won't fit in a small string; we need to go big. + RocStr(RocStrInner { + heap_allocated: ManuallyDrop::new(RocList::with_capacity(bytes)), + }) + } + } + + /// Increase a RocStr's capacity by at least the requested number of bytes (possibly more). + /// + /// May return a new RocStr, if the provided one was not unique. + pub fn reserve(&mut self, bytes: usize) { + if self.is_small_str() { + let small_str = unsafe { self.0.small_string }; + let target_cap = small_str.len() + bytes; + + if target_cap > SmallString::CAPACITY { + // The requested capacity won't fit in a small string; we need to go big. + let mut roc_list = RocList::with_capacity(target_cap); + + roc_list.extend_from_slice(small_str.as_bytes()); + + *self = RocStr(RocStrInner { + heap_allocated: ManuallyDrop::new(roc_list), + }); + } + } else { + let mut roc_list = unsafe { ManuallyDrop::take(&mut self.0.heap_allocated) }; + + roc_list.reserve(bytes); + + let mut updated = RocStr(RocStrInner { + heap_allocated: ManuallyDrop::new(roc_list), + }); + + mem::swap(self, &mut updated); + mem::forget(updated); + } + } + + /// Returns the index of the first interior \0 byte in the string, or None if there are none. + fn first_nul_byte(&self) -> Option { + match self.as_enum_ref() { + RocStrInnerRef::HeapAllocated(roc_list) => roc_list.iter().position(|byte| *byte == 0), + RocStrInnerRef::SmallString(small_string) => small_string.first_nul_byte(), + } + } + + // If the string is under this many bytes, the with_terminator family + // of methods will allocate the terminated string on the stack when + // the RocStr is non-unique. + const TEMP_STR_MAX_STACK_BYTES: usize = 64; + + /// Like calling with_utf8_terminator passing \0 for the terminator, + /// except it can fail because a RocStr may contain \0 characters, + /// which a nul-terminated string must not. + pub fn utf8_nul_terminated T>( + self, + func: F, + ) -> Result { + if let Some(pos) = self.first_nul_byte() { + Err(InteriorNulError { pos, roc_str: self }) + } else { + Ok(self.with_utf8_terminator(b'\0', func)) + } + } + + /// Turn this RocStr into a UTF-8 `*mut u8`, terminate it with the given character + /// (commonly either `b'\n'` or b`\0`) and then provide access to that + /// `*mut u8` (as well as its length) for the duration of a given function. This is + /// designed to be an efficient way to turn a `RocStr` received from an application into + /// either the nul-terminated UTF-8 `char*` needed by UNIX syscalls, or into a + /// newline-terminated string to write to stdout or stderr (for a "println"-style effect). + /// + /// **NOTE:** The length passed to the function is the same value that `RocStr::len` will + /// return; it does not count the terminator. So to convert it to a nul-terminated slice + /// of Rust bytes (for example), call `slice::from_raw_parts` passing the given length + 1. + /// + /// This operation achieves efficiency by reusing allocated bytes from the RocStr itself, + /// and sometimes allocating on the stack. It does not allocate on the heap when given a + /// a small string or a string with unique refcount, but may allocate when given a large + /// string with non-unique refcount. (It will do a stack allocation if the string is under + /// 64 bytes; the stack allocation will only live for the duration of the called function.) + /// + /// If the given (owned) RocStr is unique, this can overwrite the underlying bytes + /// to terminate the string in-place. Small strings have an extra byte at the end + /// where the length is stored, which can be replaced with the terminator. Heap-allocated + /// strings can have excess capacity which can hold a terminator, or if they have no + /// excess capacity, all the bytes can be shifted over the refcount in order to free up + /// a `usize` worth of free space at the end - which can easily fit a 1-byte terminator. + pub fn with_utf8_terminator T>(self, terminator: u8, func: F) -> T { + // Note that this function does not use with_terminator because it can be + // more efficient than that - due to knowing that it's already in UTF-8 and always + // has room for a 1-byte terminator in the existing allocation (either in the refcount + // bytes, or, in a small string, in the length at the end of the string). + + let terminate = |alloc_ptr: *mut u8, len: usize| unsafe { + *(alloc_ptr.add(len)) = terminator; + + func(alloc_ptr, len) + }; + + match self.as_enum_ref() { + RocStrInnerRef::HeapAllocated(roc_list) => { + unsafe { + match roc_list.storage() { + Some(storage) if storage.is_unique() => { + // The backing RocList was unique, so we can mutate it in-place. + let len = roc_list.len(); + let ptr = if len < roc_list.capacity() { + // We happen to have excess capacity already, so we will be able + // to write the terminator into the first byte of excess capacity. + roc_list.ptr_to_first_elem() as *mut u8 + } else { + // We always have an allocation that's even bigger than necessary, + // because the refcount bytes take up more than the 1B needed for + // the terminator. We just need to shift the bytes over on top + // of the refcount. + let alloc_ptr = roc_list.ptr_to_allocation() as *mut u8; + + // First, copy the bytes over the original allocation - effectively + // shifting everything over by one `usize`. Now we no longer have a + // refcount (but the terminated won't use that anyway), but we do + // have a free `usize` at the end. + // + // IMPORTANT: Must use ptr::copy instead of ptr::copy_nonoverlapping + // because the regions definitely overlap! + ptr::copy(roc_list.ptr_to_first_elem() as *mut u8, alloc_ptr, len); + + alloc_ptr + }; + + terminate(ptr, len) + } + Some(_) => { + let len = roc_list.len(); + + // The backing list was not unique, so we can't mutate it in-place. + // ask for `len + 1` to store the original string and the terminator + with_stack_bytes(len + 1, |alloc_ptr: *mut u8| { + let alloc_ptr = alloc_ptr as *mut u8; + let elem_ptr = roc_list.ptr_to_first_elem() as *mut u8; + + // memcpy the bytes into the stack allocation + ptr::copy_nonoverlapping(elem_ptr, alloc_ptr, len); + + terminate(alloc_ptr, len) + }) + } + None => { + // The backing list was empty. + // + // No need to do a heap allocation for an empty string - we + // can just do a stack allocation that will live for the + // duration of the function. + func([terminator].as_mut_ptr(), 0) + } + } + } + } + RocStrInnerRef::SmallString(small_str) => { + let mut bytes = [0; size_of::>()]; + let mut it = small_str.bytes.iter(); + bytes = bytes.map(|_| it.next().copied().unwrap_or_default()); + + // Even if the small string is at capacity, there will be room to write + // a terminator in the byte that's used to store the length. + terminate(bytes.as_mut_ptr() as *mut u8, small_str.len()) + } + } + } + + /// Like calling with_utf16_terminator passing \0 for the terminator, + /// except it can fail because a RocStr may contain \0 characters, + /// which a nul-terminated string must not. + pub fn utf16_nul_terminated T>( + self, + func: F, + ) -> Result { + if let Some(pos) = self.first_nul_byte() { + Err(InteriorNulError { pos, roc_str: self }) + } else { + Ok(self.with_utf16_terminator(0, func)) + } + } + + /// Turn this RocStr into a nul-terminated UTF-16 `*mut u16` and then provide access to + /// that `*mut u16` (as well as its length) for the duration of a given function. This is + /// designed to be an efficient way to turn a RocStr received from an application into + /// the nul-terminated UTF-16 `wchar_t*` needed by Windows API calls. + /// + /// **NOTE:** The length passed to the function is the same value that `RocStr::len` will + /// return; it does not count the terminator. So to convert it to a nul-terminated + /// slice of Rust bytes, call `slice::from_raw_parts` passing the given length + 1. + /// + /// This operation achieves efficiency by reusing allocated bytes from the RocStr itself, + /// and sometimes allocating on the stack. It does not allocate on the heap when given a + /// a small string or a string with unique refcount, but may allocate when given a large + /// string with non-unique refcount. (It will do a stack allocation if the string is under + /// 64 bytes; the stack allocation will only live for the duration of the called function.) + /// + /// Because this works on an owned RocStr, it's able to overwrite the underlying bytes + /// to nul-terminate the string in-place. Small strings have an extra byte at the end + /// where the length is stored, which can become 0 for nul-termination. Heap-allocated + /// strings can have excess capacity which can hold a termiator, or if they have no + /// excess capacity, all the bytes can be shifted over the refcount in order to free up + /// a `usize` worth of free space at the end - which can easily fit a terminator. + /// + /// This operation can fail because a RocStr may contain \0 characters, which a + /// nul-terminated string must not. + pub fn with_utf16_terminator T>( + self, + terminator: u16, + func: F, + ) -> T { + self.with_terminator(terminator, |dest_ptr: *mut u16, str_slice: &str| { + // Translate UTF-8 source bytes into UTF-16 and write them into the destination. + for (index, wchar) in str_slice.encode_utf16().enumerate() { + unsafe { std::ptr::write_unaligned(dest_ptr.add(index), wchar) }; + } + + func(dest_ptr, str_slice.len()) + }) + } + + pub fn with_windows_path T>( + self, + func: F, + ) -> Result { + if let Some(pos) = self.first_nul_byte() { + Err(InteriorNulError { pos, roc_str: self }) + } else { + let answer = self.with_terminator(0u16, |dest_ptr: *mut u16, str_slice: &str| { + // Translate UTF-8 source bytes into UTF-16 and write them into the destination. + for (index, mut wchar) in str_slice.encode_utf16().enumerate() { + // Replace slashes with backslashes + if wchar == '/' as u16 { + wchar = '\\' as u16 + }; + + unsafe { + *(dest_ptr.add(index)) = wchar; + } + } + + func(dest_ptr, str_slice.len()) + }); + + Ok(answer) + } + } + + /// Generic version of temp_c_utf8 and temp_c_utf16. The given function will be + /// passed a pointer to elements of type E. The pointer will have enough room to hold + /// one element for each byte of the given `&str`'s length, plus the terminator element. + /// + /// The terminator will be written right after the end of the space for the other elements, + /// but all the memory in that space before the terminator will be uninitialized. This means + /// if you want to do something like copy the contents of the `&str` into there, that will + /// need to be done explicitly. + /// + /// The terminator is always written - even if there are no other elements present before it. + /// (In such a case, the `&str` argument will be empty and the `*mut E` will point directly + /// to the terminator). + /// + /// One use for this is to convert slashes to backslashes in Windows paths; + /// this function provides the most efficient way to do that, because no extra + /// iteration pass is necessary; the conversion can be done after each translation + /// of a UTF-8 character to UTF-16. Here's how that would look: + /// + /// use roc_std::{RocStr, InteriorNulError}; + /// + /// pub fn with_windows_path T>( + /// roc_str: RocStr, + /// func: F, + /// ) -> Result { + /// let answer = roc_str.with_terminator(0u16, |dest_ptr: *mut u16, str_slice: &str| { + /// // Translate UTF-8 source bytes into UTF-16 and write them into the destination. + /// for (index, mut wchar) in str_slice.encode_utf16().enumerate() { + /// // Replace slashes with backslashes + /// if wchar == '/' as u16 { + /// wchar = '\\' as u16 + /// }; + /// + /// unsafe { + /// *(dest_ptr.add(index)) = wchar; + /// } + /// } + /// + /// func(dest_ptr, str_slice.len()) + /// }); + /// + /// Ok(answer) + /// } + pub fn with_terminator A>( + self, + terminator: E, + func: F, + ) -> A { + use crate::Storage; + use core::mem::align_of; + + let terminate = |alloc_ptr: *mut E, str_slice: &str| unsafe { + std::ptr::write_unaligned(alloc_ptr.add(str_slice.len()), terminator); + + func(alloc_ptr, str_slice) + }; + + // When we don't have an existing allocation that can work, fall back on this. + // It uses either a stack allocation, or, if that would be too big, a heap allocation. + let fallback = |str_slice: &str| { + // We need 1 extra elem for the terminator. It must be an elem, + // not a byte, because we'll be providing a pointer to elems. + let needed_bytes = (str_slice.len() + 1) * size_of::(); + + with_stack_bytes(needed_bytes, |alloc_ptr: *mut E| { + terminate(alloc_ptr, str_slice) + }) + }; + + match self.as_enum_ref() { + RocStrInnerRef::HeapAllocated(roc_list) => { + let len = roc_list.len(); + + unsafe { + match roc_list.storage() { + Some(storage) if storage.is_unique() => { + // The backing RocList was unique, so we can mutate it in-place. + + // We need 1 extra elem for the terminator. It must be an elem, + // not a byte, because we'll be providing a pointer to elems. + let needed_bytes = (len + 1) * size_of::(); + + // We can use not only the capacity on the heap, but also + // the bytes originally used for the refcount. + let available_bytes = roc_list.capacity() + size_of::(); + + if needed_bytes < available_bytes { + debug_assert!(align_of::() >= align_of::()); + + // We happen to have sufficient excess capacity already, + // so we will be able to write the new elements as well as + // the terminator into the existing allocation. + let ptr = roc_list.ptr_to_allocation() as *mut E; + let answer = terminate(ptr, self.as_str()); + + // We cannot rely on the RocStr::drop implementation, because + // it tries to use the refcount - which we just overwrote + // with string bytes. + mem::forget(self); + crate::roc_dealloc(ptr.cast(), mem::align_of::() as u32); + + answer + } else { + // We didn't have sufficient excess capacity already, + // so we need to do either a new stack allocation or a new + // heap allocation. + fallback(self.as_str()) + } + } + Some(_) => { + // The backing list was not unique, so we can't mutate it in-place. + fallback(self.as_str()) + } + None => { + // The backing list was empty. + // + // No need to do a heap allocation for an empty string - we + // can just do a stack allocation that will live for the + // duration of the function. + func([terminator].as_mut_ptr() as *mut E, "") + } + } + } + } + RocStrInnerRef::SmallString(small_str) => { + let len = small_str.len(); + + // We need 1 extra elem for the terminator. It must be an elem, + // not a byte, because we'll be providing a pointer to elems. + let needed_bytes = (len + 1) * size_of::(); + let available_bytes = size_of::(); + + if needed_bytes < available_bytes { + let mut bytes = small_str.bytes; + terminate(&mut bytes as *mut u8 as *mut E, self.as_str()) + } else { + fallback(self.as_str()) + } + } + } + } +} + +impl Deref for RocStr { + type Target = str; + + fn deref(&self) -> &Self::Target { + match self.as_enum_ref() { + RocStrInnerRef::HeapAllocated(h) => unsafe { core::str::from_utf8_unchecked(h) }, + RocStrInnerRef::SmallString(s) => s, + } + } +} + +/// This can fail because a CStr may contain invalid UTF-8 characters +#[cfg(feature = "std")] +impl TryFrom<&CStr> for RocStr { + type Error = core::str::Utf8Error; + + fn try_from(c_str: &CStr) -> Result { + c_str.to_str().map(RocStr::from) + } +} + +/// This can fail because a CString may contain invalid UTF-8 characters +#[cfg(feature = "std")] +impl TryFrom for RocStr { + type Error = core::str::Utf8Error; + + fn try_from(c_string: CString) -> Result { + c_string.to_str().map(RocStr::from) + } +} + +#[cfg(not(feature = "no_std"))] +/// Like https://doc.rust-lang.org/std/ffi/struct.NulError.html but +/// only for interior nuls, not for missing nul terminators. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct InteriorNulError { + pub pos: usize, + pub roc_str: RocStr, +} + +impl Default for RocStr { + fn default() -> Self { + Self::empty() + } +} + +impl From<&str> for RocStr { + fn from(s: &str) -> Self { + unsafe { Self::from_slice_unchecked(s.as_bytes()) } + } +} + +impl PartialEq for RocStr { + fn eq(&self, other: &Self) -> bool { + self.deref() == other.deref() + } +} + +impl Eq for RocStr {} + +impl PartialOrd for RocStr { + fn partial_cmp(&self, other: &Self) -> Option { + self.as_str().partial_cmp(other.as_str()) + } +} + +impl Ord for RocStr { + fn cmp(&self, other: &Self) -> cmp::Ordering { + self.as_str().cmp(other.as_str()) + } +} + +impl fmt::Debug for RocStr { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + self.deref().fmt(f) + } +} + +impl fmt::Display for RocStr { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + self.deref().fmt(f) + } +} + +impl Clone for RocStr { + fn clone(&self) -> Self { + match self.as_enum_ref() { + RocStrInnerRef::HeapAllocated(h) => Self(RocStrInner { + heap_allocated: ManuallyDrop::new(h.clone()), + }), + RocStrInnerRef::SmallString(s) => Self(RocStrInner { small_string: *s }), + } + } +} + +impl Drop for RocStr { + fn drop(&mut self) { + if !self.is_small_str() { + unsafe { + ManuallyDrop::drop(&mut self.0.heap_allocated); + } + } + } +} + +// This is a RocStr that is checked to ensure it is unique or readonly such that it can be sent between threads safely. +#[repr(transparent)] +pub struct SendSafeRocStr(RocStr); + +unsafe impl Send for SendSafeRocStr {} + +impl Clone for SendSafeRocStr { + fn clone(&self) -> Self { + if self.0.is_readonly() { + SendSafeRocStr(self.0.clone()) + } else { + // To keep self send safe, this must copy. + SendSafeRocStr(RocStr::from(self.0.as_str())) + } + } +} + +impl From for SendSafeRocStr { + fn from(s: RocStr) -> Self { + if s.is_unique() || s.is_readonly() { + SendSafeRocStr(s) + } else { + // This is not unique, do a deep copy. + SendSafeRocStr(RocStr::from(s.as_str())) + } + } +} + +impl From for RocStr { + fn from(s: SendSafeRocStr) -> Self { + s.0 + } +} + +#[repr(C)] +union RocStrInner { + // TODO: this really should be separated from the List type. + // Due to length specifying seamless slices for Str and capacity for Lists they should not share the same code. + // Currently, there are work arounds in RocList to handle both via removing the highest bit of length in many cases. + // With glue changes, we should probably rewrite these cleanly to match what is in the zig bitcode. + // It is definitely a bit stale now and I think the storage mechanism can be quite confusing with our extra pieces of state. + heap_allocated: ManuallyDrop>, + small_string: SmallString, +} + +enum RocStrInnerRef<'a> { + HeapAllocated(&'a RocList), + SmallString(&'a SmallString), +} + +#[derive(Debug, Clone, Copy)] +#[repr(C)] +struct SmallString { + bytes: [u8; Self::CAPACITY], + len: u8, +} + +impl SmallString { + const CAPACITY: usize = size_of::>() - 1; + + const fn empty() -> Self { + Self { + bytes: [0; Self::CAPACITY], + len: RocStr::MASK, + } + } + + /// # Safety + /// + /// `slice` must be valid UTF-8. + unsafe fn try_from_utf8_bytes(slice: &[u8]) -> Option { + // Check the size of the slice. + let len_as_u8 = u8::try_from(slice.len()).ok()?; + if (len_as_u8 as usize) > Self::CAPACITY { + return None; + } + + // Construct the small string. + let mut bytes = [0; Self::CAPACITY]; + bytes[..slice.len()].copy_from_slice(slice); + Some(Self { + bytes, + len: len_as_u8 | RocStr::MASK, + }) + } + + fn is_small_str(&self) -> bool { + self.len & RocStr::MASK != 0 + } + + fn len(&self) -> usize { + usize::from(self.len & !RocStr::MASK) + } + + /// Returns the index of the first interior \0 byte in the string, or None if there are none. + fn first_nul_byte(&self) -> Option { + for (index, byte) in self.bytes[0..self.len()].iter().enumerate() { + if *byte == 0 { + return Some(index); + } + } + + None + } +} + +impl Deref for SmallString { + type Target = str; + + fn deref(&self) -> &Self::Target { + let len = self.len(); + unsafe { core::str::from_utf8_unchecked(self.bytes.get_unchecked(..len)) } + } +} + +impl DerefMut for SmallString { + fn deref_mut(&mut self) -> &mut Self::Target { + let len = self.len(); + unsafe { core::str::from_utf8_unchecked_mut(self.bytes.get_unchecked_mut(..len)) } + } +} + +impl Hash for RocStr { + fn hash(&self, state: &mut H) { + self.as_str().hash(state) + } +} + +#[cfg(feature = "serde")] +impl Serialize for RocStr { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(self.as_str()) + } +} + +#[cfg(feature = "serde")] +impl<'de> Deserialize<'de> for RocStr { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + // TODO: using deserialize_string here instead of deserialize_str here + // because I think we'd "benefit from taking ownership of buffered data + // owned by the Deserializer." is that correct? + deserializer.deserialize_string(RocStrVisitor {}) + } +} + +#[cfg(feature = "serde")] +struct RocStrVisitor {} + +#[cfg(feature = "serde")] +impl<'de> Visitor<'de> for RocStrVisitor { + type Value = RocStr; + + fn expecting(&self, formatter: &mut core::fmt::Formatter) -> core::fmt::Result { + write!(formatter, "a string") + } + + fn visit_str(self, value: &str) -> Result + where + E: serde::de::Error, + { + Ok(RocStr::from(value)) + } +} diff --git a/crates/roc_std/src/storage.rs b/crates/roc_std/src/storage.rs new file mode 100644 index 0000000000..6bbb6d18de --- /dev/null +++ b/crates/roc_std/src/storage.rs @@ -0,0 +1,66 @@ +use core::num::NonZeroIsize; + +/// # Safety +/// +/// isize::MIN is definitely not zero. This can become +/// https://doc.rust-lang.org/std/num/struct.NonZeroIsize.html#associatedconstant.MIN +/// once it has been stabilized. +const REFCOUNT_1: NonZeroIsize = unsafe { NonZeroIsize::new_unchecked(isize::MIN) }; + +const _ASSERT_STORAGE_SIZE: () = + assert!(core::mem::size_of::() == core::mem::size_of::()); + +#[derive(Clone, Copy, Debug)] +pub enum Storage { + Readonly, + ReferenceCounted(NonZeroIsize), +} + +impl Storage { + pub fn new_reference_counted() -> Self { + Self::ReferenceCounted(REFCOUNT_1) + } + + /// Increment the reference count. + pub fn increment_reference_count(&mut self) { + match self { + Storage::Readonly => { + // Do nothing. + } + Storage::ReferenceCounted(rc) => { + let new_rc = rc.get() + 1; + if let Some(new_rc) = NonZeroIsize::new(new_rc) { + *self = Storage::ReferenceCounted(new_rc); + } else { + *self = Storage::Readonly; + } + } + } + } + + /// Decrease the reference count. + /// + /// Returns `true` once there are no more references left. + pub fn decrease(&mut self) -> bool { + match self { + Storage::Readonly => false, + Storage::ReferenceCounted(rc) => { + if *rc == REFCOUNT_1 { + true + } else { + *rc = NonZeroIsize::new(rc.get() - 1).expect("A reference count was decremented all the way to zero, which should never happen."); + + false + } + } + } + } + + pub fn is_readonly(&self) -> bool { + matches!(self, Self::Readonly) + } + + pub fn is_unique(&self) -> bool { + matches!(self, Self::ReferenceCounted(REFCOUNT_1)) + } +} diff --git a/crates/roc_std/tests/test_roc_std.rs b/crates/roc_std/tests/test_roc_std.rs new file mode 100644 index 0000000000..9b1e8fd1f8 --- /dev/null +++ b/crates/roc_std/tests/test_roc_std.rs @@ -0,0 +1,467 @@ +#![allow(clippy::missing_safety_doc, clippy::redundant_clone)] +// TODO try removing allow clippy::redundant_clone if we're on rust 1.71 or later + +#[macro_use] +extern crate pretty_assertions; +extern crate quickcheck; +extern crate roc_std; + +use core::ffi::c_void; + +const ROC_SMALL_STR_CAPACITY: usize = core::mem::size_of::() - 1; + +#[no_mangle] +pub unsafe extern "C" fn roc_alloc(size: usize, _alignment: u32) -> *mut c_void { + libc::malloc(size) +} + +#[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) +} + +#[no_mangle] +pub unsafe extern "C" fn roc_dealloc(c_ptr: *mut c_void, _alignment: u32) { + libc::free(c_ptr) +} + +#[cfg(test)] +#[no_mangle] +pub unsafe extern "C" fn roc_panic(msg: *mut roc_std::RocStr, _tag_id: u32) { + panic!("roc_panic during test: {}", &*msg); +} + +#[cfg(test)] +#[no_mangle] +pub unsafe extern "C" fn roc_dbg(loc: *mut roc_std::RocStr, msg: *mut roc_std::RocStr) { + eprintln!("[{}] {}", &*loc, &*msg); +} + +#[cfg(test)] +#[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) +} + +#[cfg(test)] +mod test_roc_std { + use roc_std::{RocBox, RocDec, RocList, RocResult, RocStr, SendSafeRocStr}; + + fn roc_str_byte_representation(string: &RocStr) -> [u8; RocStr::SIZE] { + unsafe { core::mem::transmute_copy(string) } + } + + #[test] + fn roc_str_empty() { + let actual = roc_str_byte_representation(&RocStr::empty()); + + let mut expected = [0u8; RocStr::SIZE]; + expected[RocStr::SIZE - 1] = RocStr::MASK; + + assert_eq!(actual, expected); + } + + #[test] + fn roc_str_single_char() { + let actual = roc_str_byte_representation(&RocStr::from("a")); + + let mut expected = [0u8; RocStr::SIZE]; + expected[0] = b'a'; + expected[RocStr::SIZE - 1] = RocStr::MASK | 1; + + assert_eq!(actual, expected); + } + + #[test] + fn roc_str_max_small_string() { + let s = str::repeat("a", RocStr::SIZE - 1); + let actual = roc_str_byte_representation(&RocStr::from(s.as_str())); + + let mut expected = [0u8; RocStr::SIZE]; + expected[..RocStr::SIZE - 1].copy_from_slice(s.as_bytes()); + expected[RocStr::SIZE - 1] = RocStr::MASK | s.len() as u8; + + assert_eq!(actual, expected); + } + + #[test] + fn empty_string_from_str() { + let a = RocStr::from(""); + let b = RocStr::empty(); + + assert_eq!(a, b); + } + + #[test] + fn empty_string_length() { + let string = RocStr::from(""); + + assert_eq!(string.len(), 0); + } + + #[test] + fn empty_string_capacity() { + let string = RocStr::empty(); + + assert_eq!(string.capacity(), super::ROC_SMALL_STR_CAPACITY); + } + + #[test] + fn reserve_small_str() { + let mut roc_str = RocStr::empty(); + + roc_str.reserve(42); + + assert_eq!(roc_str.capacity() >= 42, true); + } + + #[test] + fn reserve_big_str() { + let mut roc_str = RocStr::empty(); + + roc_str.reserve(5000); + + assert_eq!(roc_str.capacity() >= 5000, true); + } + + #[test] + #[cfg(feature = "serde")] + fn str_short_serde_roundtrip() { + let orig = RocStr::from("x"); + + let serialized = serde_json::to_string(&orig).expect("failed to serialize string"); + let deserialized = serde_json::from_str(&serialized).expect("failed to deserialize string"); + + assert_eq!(orig, deserialized); + } + + #[test] + #[cfg(feature = "serde")] + fn str_long_serde_roundtrip() { + // How about a little philosophy to accompany test failures? + let orig = RocStr::from("If there's a remedy when trouble strikes, what reason is there for dejection? And if there is no help for it, what use is there in being glum? -- Shantideva, The Way of the Bodhisattva"); + + let serialized = serde_json::to_string(&orig).expect("failed to serialize string"); + let deserialized = serde_json::from_str(&serialized).expect("failed to deserialize string"); + + assert_eq!(orig, deserialized); + } + + #[test] + fn reserve_small_list() { + let mut roc_list = RocList::::empty(); + + roc_list.reserve(42); + + assert_eq!(roc_list.capacity(), 42); + } + + #[test] + fn reserve_big_list() { + let mut roc_list = RocList::::empty(); + + roc_list.reserve(5000); + + assert_eq!(roc_list.capacity(), 5000); + } + + #[test] + #[cfg(feature = "serde")] + fn short_list_roundtrip() { + let items: [u8; 4] = [1, 3, 3, 7]; + let orig = RocList::from_slice(&items); + + let serialized = serde_json::to_string(&orig).expect("failed to serialize string"); + let deserialized = + serde_json::from_str::>(&serialized).expect("failed to deserialize string"); + + assert_eq!(orig, deserialized); + } + + #[test] + #[cfg(feature = "serde")] + fn long_list_roundtrip() { + let orig = RocList::from_iter(1..100); + + let serialized = serde_json::to_string(&orig).expect("failed to serialize string"); + let deserialized = + serde_json::from_str::>(&serialized).expect("failed to deserialize string"); + + assert_eq!(orig, deserialized); + } + + #[test] + fn list_from_iter() { + let elems: [i64; 5] = [1, 2, 3, 4, 5]; + let from_slice = RocList::from_slice(&elems); + let from_iter = RocList::from_iter(elems); + assert_eq!(from_iter, from_slice); + assert_eq!(from_iter.capacity(), from_slice.capacity()); + } + + #[test] + fn list_from_iter_zero_size() { + let elems: [(); 5] = [(), (), (), (), ()]; + let from_slice = RocList::from_slice(&elems); + let from_iter = RocList::from_iter(elems); + assert_eq!(from_iter, from_slice); + } + + #[test] + fn list_from_array() { + let elems: [i64; 5] = [1, 2, 3, 4, 5]; + let from_slice = RocList::from_slice(&elems); + let from_array = RocList::from(elems); + assert_eq!(from_array, from_slice); + assert_eq!(from_array.capacity(), from_slice.capacity()); + } + + #[test] + fn list_from_array_zero_size() { + let elems: [(); 5] = [(), (), (), (), ()]; + let from_slice = RocList::from_slice(&elems); + let from_array = RocList::from(elems); + assert_eq!(from_array, from_slice); + assert_eq!(from_array.capacity(), from_slice.capacity()); + } + + #[test] + fn roc_result_to_rust_result() { + let greeting = "Hello, World!"; + let roc_result: RocResult = RocResult::ok(greeting.into()); + + match roc_result.into() { + Ok(answer) => { + assert_eq!(answer.as_str(), greeting); + } + Err(()) => { + panic!("Received an Err when Ok was expected.") + } + } + } + + #[test] + fn roc_result_is_ok() { + let greeting = "Hello, World!"; + let roc_result: RocResult = RocResult::ok(greeting.into()); + + assert!(roc_result.is_ok()); + assert!(!roc_result.is_err()); + } + + #[test] + fn roc_result_is_err() { + let greeting = "Hello, World!"; + let roc_result: RocResult<(), String> = RocResult::err(greeting.into()); + + assert!(!roc_result.is_ok()); + assert!(roc_result.is_err()); + } + + #[test] + fn create_roc_box() { + let contents = 42i32; + let roc_box = RocBox::new(contents); + + assert_eq!(roc_box.into_inner(), contents) + } + + #[test] + fn roc_dec_fmt() { + assert_eq!( + format!("{}", RocDec::MIN), + "-170141183460469231731.687303715884105728" + ); + + let half = RocDec::from_str("0.5").unwrap(); + assert_eq!(format!("{half}"), "0.5"); + + let ten = RocDec::from_str("10").unwrap(); + assert_eq!(format!("{ten}"), "10"); + + let example = RocDec::from_str("1234.5678").unwrap(); + assert_eq!(format!("{example}"), "1234.5678"); + + let example = RocDec::from_str("1_000.5678").unwrap(); + assert_eq!(format!("{example}"), "1000.5678"); + + let sample_negative = "-1.234"; + let example = RocDec::from_str(sample_negative).unwrap(); + assert_eq!(format!("{example}"), sample_negative); + + let example = RocDec::from_str("1000.000").unwrap(); + assert_eq!(format!("{example}"), "1000"); + } + + #[test] + fn safe_send_no_copy() { + let x = RocStr::from("This is a long string but still unique. Yay!!!"); + assert_eq!(x.is_unique(), true); + + let safe_x = SendSafeRocStr::from(x); + let new_x = RocStr::from(safe_x); + assert_eq!(new_x.is_unique(), true); + assert_eq!( + new_x.as_str(), + "This is a long string but still unique. Yay!!!" + ); + } + + #[test] + fn safe_send_requires_copy() { + let x = RocStr::from("This is a long string but still unique. Yay!!!"); + let y = x.clone(); + let z = y.clone(); + assert_eq!(x.is_unique(), false); + assert_eq!(y.is_unique(), false); + assert_eq!(z.is_unique(), false); + + let safe_x = SendSafeRocStr::from(x); + let new_x = RocStr::from(safe_x); + assert_eq!(new_x.is_unique(), true); + assert_eq!(y.is_unique(), false); + assert_eq!(z.is_unique(), false); + assert_eq!( + new_x.as_str(), + "This is a long string but still unique. Yay!!!" + ); + } + + #[test] + fn safe_send_small_str() { + let x = RocStr::from("short"); + let y = x.clone(); + let z = y.clone(); + assert!(x.is_unique()); + assert!(y.is_unique()); + assert!(z.is_unique()); + + let safe_x = SendSafeRocStr::from(x); + let new_x = RocStr::from(safe_x); + assert!(new_x.is_unique()); + assert!(y.is_unique(),); + assert!(z.is_unique(),); + assert_eq!(new_x.as_str(), "short"); + } + + #[test] + fn empty_list_is_unique() { + let roc_list = RocList::::empty(); + assert!(roc_list.is_unique()); + } +} + +#[cfg(test)] +mod with_terminator { + use core::slice; + use roc_std::RocStr; + use std::ffi::CStr; + + fn verify_temp_c(string: &str, excess_capacity: usize) { + let mut roc_str = RocStr::from(string); + + println!("-------------1--------------"); + if excess_capacity > 0 { + roc_str.reserve(excess_capacity); + } + + // utf8_nul_terminated + { + println!("-------------2--------------"); + let answer = roc_str.clone().utf8_nul_terminated(|ptr, len| { + println!("-------------3--------------"); + let bytes = unsafe { slice::from_raw_parts(ptr, len + 1) }; + println!("-------------4--------------"); + let c_str = CStr::from_bytes_with_nul(bytes).unwrap(); + println!("-------------5--------------"); + + assert_eq!(c_str.to_str(), Ok(string)); + println!("-------------6--------------"); + + 42 + }); + + assert_eq!(Ok(42), answer); + } + + // utf16_nul_terminated + { + let answer = roc_str.utf16_nul_terminated(|ptr, len| { + let bytes: &[u8] = unsafe { slice::from_raw_parts(ptr as *mut u8, 2 * (len + 1)) }; + + let items: Vec = bytes + .chunks(2) + .map(|c| c.try_into().unwrap()) + .map(u16::from_ne_bytes) + .collect(); + + // Verify that it's nul-terminated + assert_eq!(items[len], 0); + + let string = String::from_utf16(&items[0..len]).unwrap(); + + assert_eq!(string.as_str(), string); + + 42 + }); + + assert_eq!(Ok(42), answer); + } + } + + #[test] + fn empty_string() { + verify_temp_c("", 0); + } + + /// e.g. "a" or "abc" or "abcdefg" etc. + fn string_for_len(len: usize) -> String { + let first_index: usize = 97; // start with ASCII lowercase "a" + let bytes: Vec = (0..len) + .map(|index| { + let letter = (index % 26) + first_index; + + letter.try_into().unwrap() + }) + .collect(); + + assert_eq!(bytes.len(), len); + + // The bytes should contain no nul characters. + assert!(bytes.iter().all(|byte| *byte != 0)); + + String::from_utf8(bytes).unwrap() + } + + #[test] + fn small_strings() { + for len in 1..=super::ROC_SMALL_STR_CAPACITY { + verify_temp_c(&string_for_len(len), 0); + } + } + + #[test] + fn no_excess_capacity() { + // This is small enough that it should be a stack allocation for UTF-8 + verify_temp_c(&string_for_len(33), 0); + + // This is big enough that it should be a heap allocation for UTF-8 and UTF-16 + verify_temp_c(&string_for_len(65), 0); + } + + #[test] + fn with_excess_capacity() { + println!("Start!"); + // We should be able to use the excess capacity for all of these. + verify_temp_c(&string_for_len(33), 1); // TODO why isn't this unique?! ohh because I CLONED IT + println!("Success!"); + // verify_temp_c(&string_for_len(33), 33); + // verify_temp_c(&string_for_len(65), 1); + // verify_temp_c(&string_for_len(65), 64); + } +} diff --git a/crates/test_utils/Cargo.toml b/crates/test_utils/Cargo.toml new file mode 100644 index 0000000000..1d70df053d --- /dev/null +++ b/crates/test_utils/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "roc_test_utils" +description = "Utility functions used all over the code base." + +authors.workspace = true +edition.workspace = true +license.workspace = true +version.workspace = true + +[dependencies] +pretty_assertions.workspace = true +remove_dir_all.workspace = true + +[dev-dependencies] diff --git a/crates/test_utils/src/lib.rs b/crates/test_utils/src/lib.rs new file mode 100644 index 0000000000..f0e99282c3 --- /dev/null +++ b/crates/test_utils/src/lib.rs @@ -0,0 +1,57 @@ +//! Provides testing utility functions for use throughout the Rust code base. +use std::path::PathBuf; + +#[doc(hidden)] +pub use pretty_assertions::assert_eq as _pretty_assert_eq; + +#[derive(PartialEq, Eq)] +pub struct DebugAsDisplay(pub T); + +impl std::fmt::Debug for DebugAsDisplay { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} + +#[macro_export] +macro_rules! assert_multiline_str_eq { + ($a:expr, $b:expr) => { + $crate::_pretty_assert_eq!($crate::DebugAsDisplay($a), $crate::DebugAsDisplay($b)) + }; +} + +/** + * Creates a temporary empty directory that gets deleted when this goes out of scope. + */ +pub struct TmpDir { + path: std::path::PathBuf, +} + +impl TmpDir { + pub fn new(dir: &str) -> TmpDir { + let path = std::path::Path::new(dir); + // ensure_empty_dir will fail if the dir doesn't already exist + std::fs::create_dir_all(path).unwrap(); + remove_dir_all::ensure_empty_dir(path).unwrap(); + + let mut pathbuf = std::path::PathBuf::new(); + pathbuf.push(path); + TmpDir { path: pathbuf } + } + + pub fn path(&self) -> &std::path::Path { + self.path.as_path() + } +} + +impl Drop for TmpDir { + fn drop(&mut self) { + // we "discard" the Result because there is no problem when a dir was already removed before we call remove_dir_all + let _ = remove_dir_all::remove_dir_all(&self.path); + } +} + +pub fn workspace_root() -> PathBuf { + let root = std::env::var("ROC_WORKSPACE_DIR").expect("Can't find the ROC_WORKSPACE_DIR variable expected to be set in .cargo/config.toml. Are you running tests outside of cargo?"); + PathBuf::from(root) +} diff --git a/crates/tracing/Cargo.toml b/crates/tracing/Cargo.toml new file mode 100644 index 0000000000..07eb67cb78 --- /dev/null +++ b/crates/tracing/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "roc_tracing" +description = "Utilities for setting up tracing at various executable entry points." + +authors.workspace = true +edition.workspace = true +license.workspace = true +version.workspace = true + +[dependencies] +tracing-appender.workspace = true +tracing-subscriber.workspace = true +tracing.workspace = true diff --git a/crates/tracing/src/lib.rs b/crates/tracing/src/lib.rs new file mode 100644 index 0000000000..66854632a9 --- /dev/null +++ b/crates/tracing/src/lib.rs @@ -0,0 +1,79 @@ +//! Utilities for turning on tracing in user-facing or test executables of the Roc compiler. +//! +//! Tracing is controlled with the ROC_LOG environment variable. +//! If ROC_LOG is specified, logs are written to stderr. If ROC_LOGTO= is also specified, +//! logs are instead written to . +//! +//! See [directive-syntax] for the filtering directive syntax. +//! +//! Rather than using the Rust `tracing` crate (or any other tracing crate) directly, +//! you should use the exposed members of `roc_tracing` for your tracing needs. +//! This enables us to easily modify the tracing infrastructure without inducing sweeping changes. +//! +//! Tracing is only turned on in debug builds. Use the provided [setup_tracing] macro to turn on +//! tracing at an executable's entry point. +//! +//! [directive-syntax]: https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html#directives + +/// Sets up tracing of a Roc executable. The value of this macro must be bound to a variable that +/// is not dropped until tracing has completed. +/// +/// This macro should only be invoked at an executable's entry point. +/// Tracing will only be enabled in debug builds. +#[macro_export] +macro_rules! setup_tracing { + () => { + if cfg!(debug_assertions) { + $crate::setup_tracing() + } else { + $crate::TracingGuards::NONE + } + }; +} + +pub use tracing::debug; +pub use tracing::info; + +const ENV_FILTER: &str = "ROC_LOG"; +const LOGTO_VAR: &str = "ROC_LOGTO"; + +use tracing_subscriber::{fmt, prelude::*, EnvFilter, Layer, Registry}; + +/// Guards issued by the underlying library used for tracing. +/// Must not be dropped until all tracing is complete. +pub struct TracingGuards { + _file_appender_guard: Option, +} + +impl TracingGuards { + pub const NONE: TracingGuards = TracingGuards { + _file_appender_guard: None, + }; +} + +#[must_use] +pub fn setup_tracing() -> TracingGuards { + if let Ok(file) = std::env::var(LOGTO_VAR) { + let _ = std::fs::remove_file(&file); + let file_appender = tracing_appender::rolling::never(".", file); + let (non_blocking, guard) = tracing_appender::non_blocking(file_appender); + let file_layer = fmt::Layer::default() + .with_writer(non_blocking) + .with_ansi(false) + .with_filter(EnvFilter::from_env(ENV_FILTER)); + + Registry::default().with(file_layer).init(); + + TracingGuards { + _file_appender_guard: Some(guard), + } + } else { + let stderr_layer = fmt::Layer::default() + .with_writer(std::io::stderr) + .with_filter(EnvFilter::from_env(ENV_FILTER)); + + Registry::default().with(stderr_layer).init(); + + TracingGuards::NONE + } +} diff --git a/crates/utils/command/Cargo.toml b/crates/utils/command/Cargo.toml new file mode 100644 index 0000000000..132b228fb3 --- /dev/null +++ b/crates/utils/command/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "roc_command_utils" +description = "Utility functions used all over the code base to call eternal apps like zig." + +authors.workspace = true +edition.workspace = true +license.workspace = true +version.workspace = true + +[dependencies] + +[dev-dependencies] diff --git a/crates/utils/command/src/lib.rs b/crates/utils/command/src/lib.rs new file mode 100644 index 0000000000..80b43baf49 --- /dev/null +++ b/crates/utils/command/src/lib.rs @@ -0,0 +1,141 @@ +use std::{ + env::{self, VarError}, + path::PathBuf, + process::Command, +}; + +#[cfg(windows)] +use std::path::Path; + +/// On windows, the path is prefixed with `\\?\`, the "verbatim" prefix. +/// Such a path does not works as an argument to `zig` and other command line tools, +/// and there seems to be no good way to strip it. So we resort to some string manipulation. +#[cfg(windows)] +pub fn strip_windows_prefix(path_buf: &Path) -> PathBuf { + let path_str = path_buf.display().to_string(); + + Path::new(path_str.trim_start_matches(r"\\?\")).to_path_buf() +} + +/// get the Path of the root of the repository +pub fn root_dir() -> PathBuf { + let mut path = env::current_exe().ok().unwrap(); + + // Get rid of the filename in target/debug/deps/cli_run-99c65e4e9a1fbd06 + path.pop(); + + // If we're in deps/ get rid of deps/ in target/debug/deps/ + if path.ends_with("deps") { + path.pop(); + } + + // Get rid of target/debug/ so we're back at the project root + path.pop(); + path.pop(); + + // running cargo with --target will put us in the target dir + if path.ends_with("target") { + path.pop(); + } + + path +} + +/// Gives a friendly error if cargo is not installed. +/// Also makes it easy to track where we use cargo in the codebase. +pub fn cargo() -> Command { + let command_str = "cargo"; + + if check_command_available(command_str) { + Command::new(command_str) + } else { + panic!("I could not find the cargo command.\nVisit https://rustup.rs/ to install cargo.",) + } +} + +/// Gives a friendly error if rustup is not installed. +/// Also makes it easy to track where we use rustup in the codebase. +pub fn rustup() -> Command { + // on windows, we need the version of cargo installed by rustup. The meaning of `cargo` is + // different within a process that runs rust. So we need to explicitly refer to where + // rustup put the binary + let command_str = "rustup"; + + if check_command_available(command_str) { + Command::new(command_str) + } else { + panic!("I could not find the rustup command.\nVisit https://rustup.rs/ to install rustup.",) + } +} + +/// Gives a friendly error if clang is not installed. +/// Also makes it easy to track where we use clang in the codebase. +pub fn clang() -> Command { + let command_str = "clang"; + + if check_command_available(command_str) { + Command::new(command_str) + } else { + panic!("I could not find the clang command.\nPlease install clang.",) + //TODO detect OS and provide detailed install instructions + } +} + +/// Gives a friendly error if zig is not installed. +/// Also makes it easy to track where we use zig in the codebase. +pub fn zig() -> Command { + let command_str = match std::env::var("ROC_ZIG") { + Ok(path) => path, + Err(_) => "zig".into(), + }; + + if check_command_available(&command_str) { + Command::new(command_str) + } else { + panic!("I could not find the zig command.\nPlease install zig, see instructions at https://ziglang.org/learn/getting-started/.",) + } +} + +fn check_command_available(command_name: &str) -> bool { + if cfg!(target_family = "unix") { + let unparsed_path = match std::env::var("PATH") { + Ok(var) => var, + Err(VarError::NotPresent) => return false, + Err(VarError::NotUnicode(_)) => { + panic!("found PATH, but it included invalid unicode data!") + } + }; + + std::env::split_paths(&unparsed_path).any(|dir| dir.join(command_name).exists()) + } else if cfg!(target = "windows") { + let mut cmd = Command::new("Get-Command"); + + cmd.args([command_name]); + + let cmd_str = format!("{cmd:?}"); + + let cmd_out = cmd.output().unwrap_or_else(|err| { + panic!( + "Failed to execute `{cmd_str}` to check if {command_name} is available:\n {err}" + ) + }); + + cmd_out.status.success() + } else { + // We're in uncharted waters, best not to panic if + // things may end up working out down the line. + true + } +} + +pub fn pretty_command_string(command: &Command) -> std::ffi::OsString { + let mut command_string = std::ffi::OsString::new(); + command_string.push(command.get_program()); + + for arg in command.get_args() { + command_string.push(" "); + command_string.push(arg); + } + + command_string +} diff --git a/crates/utils/error/Cargo.toml b/crates/utils/error/Cargo.toml new file mode 100644 index 0000000000..85b4635dc8 --- /dev/null +++ b/crates/utils/error/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "roc_error_utils" +description = "Utility functions used all over the code base adding better errors to calls." + +authors.workspace = true +edition.workspace = true +license.workspace = true +version.workspace = true + +[dependencies] +snafu.workspace = true + +[dev-dependencies] diff --git a/crates/utils/error/src/lib.rs b/crates/utils/error/src/lib.rs new file mode 100644 index 0000000000..d6b3584512 --- /dev/null +++ b/crates/utils/error/src/lib.rs @@ -0,0 +1,127 @@ +//! Provides utility functions used all over the code base. +use snafu::{Backtrace, OptionExt, Snafu}; +use std::{collections::HashMap, slice::SliceIndex}; + +#[derive(Debug, Snafu)] +#[snafu(visibility(pub))] +pub enum UtilError { + #[snafu(display( + "IndexOfFailed: Element {} was not found in collection {}.", + elt_str, + collection_str + ))] + IndexOfFailed { + elt_str: String, + collection_str: String, + backtrace: Backtrace, + }, + #[snafu(display("KeyNotFound: key {} was not found in HashMap.", key_str,))] + KeyNotFound { + key_str: String, + backtrace: Backtrace, + }, + #[snafu(display( + "OutOfBounds: index {} was out of bounds for {} with length {}.", + index, + collection_name, + len + ))] + OutOfBounds { + index: usize, + collection_name: String, + len: usize, + backtrace: Backtrace, + }, +} + +pub type UtilResult = std::result::Result; + +// replace HashMap method that returns Option with one that returns Result and proper Error +pub fn map_get<'a, K: ::std::fmt::Debug + std::hash::Hash + std::cmp::Eq, V>( + hash_map: &'a HashMap, + key: &K, +) -> UtilResult<&'a V> { + let value = hash_map.get(key).context(KeyNotFoundSnafu { + key_str: format!("{key:?}"), + })?; + + Ok(value) +} + +pub fn index_of(elt: T, slice: &[T]) -> UtilResult { + let index = slice + .iter() + .position(|slice_elt| *slice_elt == elt) + .with_context(|| { + let elt_str = format!("{elt:?}"); + let collection_str = format!("{slice:?}"); + + IndexOfFailedSnafu { + elt_str, + collection_str, + } + })?; + + Ok(index) +} + +// replaces slice method that return Option with one that return Result and proper Error +pub fn slice_get(index: usize, slice: &[T]) -> UtilResult<&>::Output> { + let elt_ref = slice.get(index).context(OutOfBoundsSnafu { + index, + collection_name: "Slice", + len: slice.len(), + })?; + + Ok(elt_ref) +} + +pub fn slice_get_mut( + index: usize, + slice: &mut [T], +) -> UtilResult<&mut >::Output> { + let slice_len = slice.len(); + + let elt_ref = slice.get_mut(index).context(OutOfBoundsSnafu { + index, + collection_name: "Slice", + len: slice_len, + })?; + + Ok(elt_ref) +} + +// returns the index of the first occurrence of element and index of the last occurrence +pub fn first_last_index_of( + elt: T, + slice: &[T], +) -> UtilResult<(usize, usize)> { + let mut first_index_opt = None; + let mut last_index_opt = None; + + for (index, list_elt) in slice.iter().enumerate() { + if *list_elt == elt { + if first_index_opt.is_none() { + first_index_opt = Some(index); + last_index_opt = Some(index); + } else { + last_index_opt = Some(index) + } + } else if last_index_opt.is_some() { + break; + } + } + + if let (Some(first_index), Some(last_index)) = (first_index_opt, last_index_opt) { + Ok((first_index, last_index)) + } else { + let elt_str = format!("{elt:?}"); + let collection_str = format!("{slice:?}"); + + IndexOfFailedSnafu { + elt_str, + collection_str, + } + .fail() + } +} diff --git a/crates/valgrind/Cargo.toml b/crates/valgrind/Cargo.toml new file mode 100644 index 0000000000..ef3f14badd --- /dev/null +++ b/crates/valgrind/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "valgrind" + +authors.workspace = true +edition.workspace = true +license.workspace = true +version.workspace = true + +[dev-dependencies] +cli_utils = { path = "../cli_utils" } +roc_command_utils = { path = "../utils/command" } +roc_build = { path = "../compiler/build" } +roc_linker = { path = "../linker" } +roc_load = { path = "../compiler/load" } +roc_mono = { path = "../compiler/mono" } +roc_packaging = { path = "../packaging" } +roc_reporting = { path = "../reporting" } + +bumpalo.workspace = true +indoc.workspace = true +target-lexicon.workspace = true +tempfile.workspace = true + +[features] +default = ["target-aarch64", "target-x86_64", "target-wasm32"] + +target-aarch64 = ["roc_build/target-aarch64"] +target-arm = [] +target-wasm32 = [] +target-x86 = [] +target-x86_64 = ["roc_build/target-x86_64"] + +[package.metadata.cargo-udeps.ignore] +development = ["roc_build", "roc_linker"] diff --git a/crates/valgrind/src/lib.rs b/crates/valgrind/src/lib.rs new file mode 100644 index 0000000000..90e110c5b7 --- /dev/null +++ b/crates/valgrind/src/lib.rs @@ -0,0 +1,552 @@ +#![cfg(test)] + +use indoc::indoc; + +#[cfg(target_os = "linux")] +static BUILD_ONCE: std::sync::Once = std::sync::Once::new(); + +#[cfg(target_os = "linux")] +fn build_host() { + use roc_build::program::build_and_preprocess_host; + use roc_linker::preprocessed_host_filename; + + let platform_main_roc = + roc_command_utils::root_dir().join("crates/valgrind/zig-platform/main.roc"); + + // tests always run on the host + let target = target_lexicon::Triple::host(); + + // the preprocessed host is stored beside the platform's main.roc + let preprocessed_host_path = + platform_main_roc.with_file_name(preprocessed_host_filename(&target).unwrap()); + + // valgrind does not support avx512 yet: https://bugs.kde.org/show_bug.cgi?id=383010 + #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] + if is_x86_feature_detected!("avx512f") { + std::env::set_var("NO_AVX512", "1"); + } + + build_and_preprocess_host( + roc_mono::ir::OptLevel::Normal, + &target, + &platform_main_roc, + &preprocessed_host_path, + roc_linker::ExposedSymbols { + top_level_values: vec![String::from("mainForHost")], + exported_closure_types: vec![], + }, + ); +} + +fn valgrind_test(source: &str) { + #[cfg(target_os = "linux")] + { + valgrind_test_linux(source) + } + + #[cfg(not(target_os = "linux"))] + { + let _ = source; + } +} + +#[cfg(target_os = "linux")] +fn valgrind_test_linux(source: &str) { + use roc_build::program::BuiltFile; + + // the host is identical for all tests so we only want to build it once + BUILD_ONCE.call_once(build_host); + + let pf = roc_command_utils::root_dir().join("crates/valgrind/zig-platform/main.roc"); + + assert!(pf.exists(), "cannot find platform {:?}", &pf); + + let concat_header = !source.trim().starts_with("app "); + + let mut app_module_source = if concat_header { + format!( + indoc::indoc!( + r#" + app "test" + packages {{ pf: "{}" }} + imports [] + provides [main] to pf + + main = + "# + ), + pf.to_str().unwrap() + ) + } else { + String::new() + }; + + for line in source.lines() { + if concat_header { + app_module_source.push_str(" "); + } + app_module_source.push_str(line); + app_module_source.push('\n'); + } + + if !concat_header { + app_module_source = + app_module_source.replace("replace_me_platform_path", &pf.display().to_string()); + } + + let temp_dir = tempfile::tempdir().unwrap(); + let app_module_path = temp_dir.path().join("app.roc"); + + let arena = bumpalo::Bump::new(); + let assume_prebuilt = true; + let res_binary_path = roc_build::program::build_str_test( + &arena, + &app_module_path, + &app_module_source, + assume_prebuilt, + ); + + match res_binary_path { + Ok(BuiltFile { + binary_path, + problems, + total_time: _, + expect_metadata: _, + }) => { + if problems.exit_code() != 0 { + panic!("there are problems") + } + + run_with_valgrind(&binary_path); + } + Err(roc_build::program::BuildFileError::LoadingProblem( + roc_load::LoadingProblem::FormattedReport(report), + )) => { + eprintln!("{report}"); + panic!(""); + } + Err(e) => panic!("{e:?}"), + } + + drop(temp_dir) +} + +#[allow(unused)] +fn run_with_valgrind(binary_path: &std::path::Path) { + use cli_utils::helpers::{extract_valgrind_errors, ValgrindError, ValgrindErrorXWhat}; + + // If possible, report the generated executable name relative to the current dir. + let generated_filename = binary_path + .strip_prefix(std::env::current_dir().unwrap()) + .unwrap_or(binary_path) + .to_str() + .unwrap(); + + let (valgrind_out, raw_xml) = + cli_utils::helpers::run_with_valgrind([], &[generated_filename.to_string()]); + + if valgrind_out.status.success() { + let memory_errors = extract_valgrind_errors(&raw_xml).unwrap_or_else(|err| { + panic!( + indoc!( + r#" + failed to parse the `valgrind` xml output: + + Error was: + + {:?} + + valgrind xml was: + + {} + + valgrind stdout was: + + {} + + valgrind stderr was: + + {} + "# + ), + err, raw_xml, valgrind_out.stdout, valgrind_out.stderr + ); + }); + + if !memory_errors.is_empty() { + for error in memory_errors { + let ValgrindError { + kind, + what: _, + xwhat, + } = error; + println!("Valgrind Error: {kind}\n"); + + if let Some(ValgrindErrorXWhat { + text, + leakedbytes: _, + leakedblocks: _, + }) = xwhat + { + println!(" {text}"); + } + } + panic!("Valgrind found memory errors"); + } + } else { + let exit_code = match valgrind_out.status.code() { + Some(code) => format!("exit code {code}"), + None => "no exit code".to_string(), + }; + + panic!( + "`valgrind` exited with {}. valgrind stdout was: \"{}\"\n\nvalgrind stderr was: \"{}\"", + exit_code, valgrind_out.stdout, valgrind_out.stderr + ); + } +} + +#[test] +fn list_concat_consumes_first_argument() { + valgrind_test("List.concat (List.withCapacity 1024) [1,2,3] |> List.len |> Num.toStr"); +} + +#[test] +fn list_concat_consumes_second_argument() { + valgrind_test(indoc!( + r#" + ( + a : List U8 + a = [] + b = List.reserve [] 11 + List.concat a b + |> List.len + |> Num.toStr + ) + "# + )); +} + +#[test] +fn str_capacity_concat() { + valgrind_test(r#"Str.withCapacity 42 |> Str.concat "foobar""#); +} + +#[test] +fn append_scalar() { + valgrind_test(indoc!( + r#" + Str.appendScalar "abcd" 'A' + |> Result.withDefault "" + "# + )); +} + +#[test] +fn split_not_present() { + valgrind_test(indoc!( + r#" + Str.split (Str.concat "a string that is stored on the heap" "!") "\n" + |> Str.joinWith "" + "# + )); +} + +#[test] +fn str_concat_first_argument_not_unique() { + valgrind_test(indoc!( + r#" + ( + str1 = Str.reserve "" 48 + str2 = "a" + + out = Str.concat str1 str2 + if Bool.false then + out + else + str1 + ) + "# + )); +} + +#[test] +fn list_concat_empty_list_zero_sized_type() { + valgrind_test(indoc!( + r#" + ( + a = List.reserve [] 11 + b = [] + List.concat a b + |> List.len + |> Num.toStr + ) + "# + )); +} + +#[test] +fn str_trim_end_capacity() { + valgrind_test(indoc!( + r#" + ( + str = "a" |> Str.reserve 30 + out = str |> Str.trimEnd + + if out == "" then "A" else "B" + ) + "# + )); +} + +#[test] +fn str_trim_start_capacity() { + valgrind_test(indoc!( + r#" + ( + str = " a" |> Str.reserve 30 + out = str |> Str.trimStart + + if out == "" then "A" else "B" + ) + "# + )); +} + +#[test] +fn str_concat_later_referencing_empty_list_with_capacity() { + valgrind_test(indoc!( + r#" + ( + a : List U8 + a = List.withCapacity 1 + + List.concat a [58] + |> List.len + |> Num.addWrap (List.len a) + |> Num.toStr + ) + "# + )); +} + +#[test] +fn joinpoint_with_closure() { + valgrind_test(indoc!( + r#" + ( + Animal : [Cat, Dog, Goose] + + makeSound : Animal -> Str + makeSound = \animal -> + dogSound = "Woof" + when animal is + Cat | Dog if isCat animal -> "Miauw" + Goose -> "Honk" + _ -> dogSound + + isCat : Animal -> Bool + isCat = \animal -> + when animal is + Cat -> Bool.true + _ -> Bool.false + + test = + catSound = makeSound Cat + dogSound = makeSound Dog + gooseSound = makeSound Goose + "Cat: \(catSound), Dog: \(dogSound), Goose: \(gooseSound)" + + test + ) + "# + )); +} + +#[test] +fn joinpoint_with_reuse() { + valgrind_test(indoc!( + r#" + ( + LinkedList a : [Cons a (LinkedList a), Nil] + + # mapLinkedList : LinkedList a, (a -> b) -> LinkedList b + mapLinkedList = \linkedList, f -> when linkedList is + Nil -> Nil + Cons x xs -> + x2 = if Bool.true then x else x + Cons (f x2) (mapLinkedList xs f) + + # printLinkedList : LinkedList a, (a -> Str) -> Str + printLinkedList = \linkedList, f -> + when linkedList is + Nil -> "Nil" + Cons x xs -> + strX = f x + strXs = printLinkedList xs f + "Cons \(strX) (\(strXs))" + + test = + newList = mapLinkedList (Cons 1 (Cons 2 (Cons 3 Nil))) (\x -> x + 1) + printLinkedList newList Num.toStr + + test + ) + "# + )); +} + +#[test] +fn tree_rebalance() { + valgrind_test(indoc!( + r#" + app "test" + packages { pf: "replace_me_platform_path" } + imports [] + provides [main] to pf + + main = show (insert 0 {} Empty) + + insert : Key k, v, RedBlackTree (Key k) v -> RedBlackTree (Key k) v + insert = \key, value, dict -> + when insertHelp key value dict is + Node Red k v l r -> Node Black k v l r + x -> x + + insertHelp : Key k, v, RedBlackTree (Key k) v -> RedBlackTree (Key k) v + insertHelp = \key, value, dict -> + when dict is + Empty -> + # New nodes are always red. If it violates the rules, it will be fixed + # when balancing. + Node Red key value Empty Empty + + Node nColor nKey nValue nLeft nRight -> + when Num.compare key nKey is + LT -> balance nColor nKey nValue (insertHelp key value nLeft) nRight + EQ -> Node nColor nKey value nLeft nRight + GT -> balance nColor nKey nValue nLeft (insertHelp key value nRight) + + balance : NodeColor, k, v, RedBlackTree k v, RedBlackTree k v -> RedBlackTree k v + balance = \color, key, value, left, right -> + when right is + Node Red rK rV rLeft rRight -> + when left is + Node Red lK lV lLeft lRight -> + Node + Red + key + value + (Node Black lK lV lLeft lRight) + (Node Black rK rV rLeft rRight) + + _ -> + Node color rK rV (Node Red key value left rLeft) rRight + + _ -> + when left is + Node Red lK lV (Node Red llK llV llLeft llRight) lRight -> + Node + Red + lK + lV + (Node Black llK llV llLeft llRight) + (Node Black key value lRight right) + + _ -> + Node color key value left right + + + show : RedBlackTree I64 {} -> Str + show = \tree -> showRBTree tree Num.toStr (\{} -> "{}") + + showRBTree : RedBlackTree k v, (k -> Str), (v -> Str) -> Str + showRBTree = \tree, showKey, showValue -> + when tree is + Empty -> "Empty" + Node color key value left right -> + sColor = showColor color + sKey = showKey key + sValue = showValue value + sL = nodeInParens left showKey showValue + sR = nodeInParens right showKey showValue + + "Node \(sColor) \(sKey) \(sValue) \(sL) \(sR)" + + nodeInParens : RedBlackTree k v, (k -> Str), (v -> Str) -> Str + nodeInParens = \tree, showKey, showValue -> + when tree is + Empty -> + showRBTree tree showKey showValue + + Node _ _ _ _ _ -> + inner = showRBTree tree showKey showValue + + "(\(inner))" + + showColor : NodeColor -> Str + showColor = \color -> + when color is + Red -> "Red" + Black -> "Black" + + NodeColor : [Red, Black] + + RedBlackTree k v : [Node NodeColor k v (RedBlackTree k v) (RedBlackTree k v), Empty] + + Key k : Num k + + + "# + )); +} + +#[test] +fn lowlevel_list_calls() { + valgrind_test(indoc!( + r#" + ( + a = List.map [1,1,1,1,1] (\x -> x + 0) + b = List.map2 a [1,1,1,1,1] (\x, y -> x + y) + c = List.map3 a b [1,1,1,1,1] (\x, y, z -> x + y + z) + d = List.map4 a b c [1,1,1,1,1] (\x, y, z, w -> x + y + z + w) + e = List.sortWith d (\x, y -> Num.compare x y) + + Num.toStr (List.len e) + ) + "# + )); +} + +#[test] +fn joinpoint_nullpointer() { + valgrind_test(indoc!( + r#" + ( + LinkedList a : [Cons a (LinkedList a), Nil] + + printLinkedList : LinkedList Str -> Str + printLinkedList = \linkedList-> + when linkedList is + Nil -> "Nil" + Cons x xs -> + strXs = printLinkedList xs + "Cons \(x) (\(strXs))" + + linkedListHead : LinkedList Str -> LinkedList Str + linkedListHead = \linkedList -> + string = when linkedList is + Cons s _ -> s + Nil -> "" + Cons string Nil + + test = + cons = printLinkedList (linkedListHead (Cons "foo" Nil)) + nil = printLinkedList (linkedListHead (Nil)) + "\(cons) - \(nil)" + + test + ) + "# + )); +} diff --git a/crates/valgrind/zig-platform/.gitignore b/crates/valgrind/zig-platform/.gitignore new file mode 100644 index 0000000000..6bdb530fa2 --- /dev/null +++ b/crates/valgrind/zig-platform/.gitignore @@ -0,0 +1,2 @@ +dynhost +libapp.so \ No newline at end of file diff --git a/crates/valgrind/zig-platform/host.zig b/crates/valgrind/zig-platform/host.zig new file mode 100644 index 0000000000..84f1ac0117 --- /dev/null +++ b/crates/valgrind/zig-platform/host.zig @@ -0,0 +1,130 @@ +const std = @import("std"); +const builtin = @import("builtin"); +const str = @import("glue").str; +const RocStr = str.RocStr; +const testing = std.testing; +const expectEqual = testing.expectEqual; +const expect = testing.expect; + +const Align = 2 * @alignOf(usize); +extern fn malloc(size: usize) callconv(.C) ?*align(Align) anyopaque; +extern fn realloc(c_ptr: [*]align(Align) u8, size: usize) callconv(.C) ?*anyopaque; +extern fn free(c_ptr: [*]align(Align) u8) callconv(.C) void; +extern fn memcpy(dst: [*]u8, src: [*]u8, size: usize) callconv(.C) void; +extern fn memset(dst: [*]u8, value: i32, size: usize) callconv(.C) void; + +const DEBUG: bool = false; + +export fn roc_alloc(size: usize, alignment: u32) callconv(.C) ?*anyopaque { + if (DEBUG) { + var ptr = malloc(size); + const stdout = std.io.getStdOut().writer(); + stdout.print("alloc: {d} (alignment {d}, size {d})\n", .{ ptr, alignment, size }) catch unreachable; + return ptr; + } else { + return malloc(size); + } +} + +export fn roc_realloc(c_ptr: *anyopaque, new_size: usize, old_size: usize, alignment: u32) callconv(.C) ?*anyopaque { + if (DEBUG) { + const stdout = std.io.getStdOut().writer(); + stdout.print("realloc: {d} (alignment {d}, old_size {d})\n", .{ c_ptr, alignment, old_size }) catch unreachable; + } + + return realloc(@as([*]align(Align) u8, @alignCast(@ptrCast(c_ptr))), new_size); +} + +export fn roc_dealloc(c_ptr: *anyopaque, alignment: u32) callconv(.C) void { + if (DEBUG) { + const stdout = std.io.getStdOut().writer(); + stdout.print("dealloc: {d} (alignment {d})\n", .{ c_ptr, alignment }) catch unreachable; + } + + free(@as([*]align(Align) u8, @alignCast(@ptrCast(c_ptr)))); +} + +export fn roc_panic(msg: *RocStr, tag_id: u32) callconv(.C) void { + const stderr = std.io.getStdErr().writer(); + switch (tag_id) { + 0 => { + stderr.print("Roc standard library crashed with message\n\n {s}\n\nShutting down\n", .{msg.asSlice()}) catch unreachable; + }, + 1 => { + stderr.print("Application crashed with message\n\n {s}\n\nShutting down\n", .{msg.asSlice()}) catch unreachable; + }, + else => unreachable, + } + std.process.exit(1); +} + +export fn roc_dbg(loc: *RocStr, msg: *RocStr) callconv(.C) void { + const stderr = std.io.getStdErr().writer(); + stderr.print("[{s}] {s}\n", .{ loc.asSlice(), msg.asSlice() }) catch unreachable; +} + +export fn roc_memset(dst: [*]u8, value: i32, size: usize) callconv(.C) void { + return memset(dst, value, size); +} + +extern fn kill(pid: c_int, sig: c_int) c_int; +extern fn shm_open(name: *const i8, oflag: c_int, mode: c_uint) c_int; +extern fn mmap(addr: ?*anyopaque, length: c_uint, prot: c_int, flags: c_int, fd: c_int, offset: c_uint) *anyopaque; +extern fn getppid() c_int; + +fn roc_getppid() callconv(.C) c_int { + return getppid(); +} + +fn roc_getppid_windows_stub() callconv(.C) c_int { + return 0; +} + +fn roc_shm_open(name: *const i8, oflag: c_int, mode: c_uint) callconv(.C) c_int { + return shm_open(name, oflag, mode); +} +fn roc_mmap(addr: ?*anyopaque, length: c_uint, prot: c_int, flags: c_int, fd: c_int, offset: c_uint) callconv(.C) *anyopaque { + return mmap(addr, length, prot, flags, fd, offset); +} + +comptime { + if (builtin.os.tag == .macos or builtin.os.tag == .linux) { + @export(roc_getppid, .{ .name = "roc_getppid", .linkage = .Strong }); + @export(roc_mmap, .{ .name = "roc_mmap", .linkage = .Strong }); + @export(roc_shm_open, .{ .name = "roc_shm_open", .linkage = .Strong }); + } + + if (builtin.os.tag == .windows) { + @export(roc_getppid_windows_stub, .{ .name = "roc_getppid", .linkage = .Strong }); + } +} + +const mem = std.mem; +const Allocator = mem.Allocator; + +extern fn roc__mainForHost_1_exposed_generic(*RocStr) void; + +const Unit = extern struct {}; + +pub fn main() u8 { + const stdout = std.io.getStdOut().writer(); + const stderr = std.io.getStdErr().writer(); + + var timer = std.time.Timer.start() catch unreachable; + + // actually call roc to populate the callresult + var callresult = RocStr.empty(); + roc__mainForHost_1_exposed_generic(&callresult); + + const nanos = timer.read(); + const seconds = (@as(f64, @floatFromInt(nanos)) / 1_000_000_000.0); + + // stdout the result + stdout.print("{s}", .{callresult.asSlice()}) catch unreachable; + + callresult.decref(); + + stderr.print("runtime: {d:.3}ms\n", .{seconds * 1000}) catch unreachable; + + return 0; +} diff --git a/crates/valgrind/zig-platform/main.roc b/crates/valgrind/zig-platform/main.roc new file mode 100644 index 0000000000..a52fe9a480 --- /dev/null +++ b/crates/valgrind/zig-platform/main.roc @@ -0,0 +1,9 @@ +platform "echo-in-zig" + requires {} { main : Str } + exposes [] + packages {} + imports [] + provides [mainForHost] + +mainForHost : Str +mainForHost = main diff --git a/vendor/README.md b/crates/vendor/README.md similarity index 100% rename from vendor/README.md rename to crates/vendor/README.md diff --git a/vendor/morphic_lib/.gitignore b/crates/vendor/morphic_lib/.gitignore similarity index 100% rename from vendor/morphic_lib/.gitignore rename to crates/vendor/morphic_lib/.gitignore diff --git a/crates/vendor/morphic_lib/Cargo.toml b/crates/vendor/morphic_lib/Cargo.toml new file mode 100644 index 0000000000..40d63e7bfa --- /dev/null +++ b/crates/vendor/morphic_lib/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "morphic_lib" +version = "0.1.0" +authors = ["William Brandon", "Wilson Berkow", "Frank Dai", "Benjamin Driscoll"] +edition = "2018" + +[dependencies] +roc_collections = {path = "../../compiler/collections"} +thiserror = "1.0.48" +blake3 = "1.3.3" +smallvec = "1.10.0" +typed-arena = "2.0.2" diff --git a/vendor/morphic_lib/LICENSE-APACHE b/crates/vendor/morphic_lib/LICENSE-APACHE similarity index 100% rename from vendor/morphic_lib/LICENSE-APACHE rename to crates/vendor/morphic_lib/LICENSE-APACHE diff --git a/vendor/morphic_lib/LICENSE-MIT b/crates/vendor/morphic_lib/LICENSE-MIT similarity index 100% rename from vendor/morphic_lib/LICENSE-MIT rename to crates/vendor/morphic_lib/LICENSE-MIT diff --git a/vendor/morphic_lib/src/analyze.rs b/crates/vendor/morphic_lib/src/analyze.rs similarity index 94% rename from vendor/morphic_lib/src/analyze.rs rename to crates/vendor/morphic_lib/src/analyze.rs index 498a2c279b..1560802350 100644 --- a/vendor/morphic_lib/src/analyze.rs +++ b/crates/vendor/morphic_lib/src/analyze.rs @@ -1,8 +1,11 @@ use smallvec::SmallVec; -use std::collections::{BTreeSet, HashMap, HashSet}; +use std::collections::BTreeSet; use std::convert::TryInto; use typed_arena::Arena; +use roc_collections::MutMap as HashMap; +use roc_collections::MutSet as HashSet; + use crate::api; use crate::ir; use crate::name_cache::{EntryPointId, FuncId}; @@ -192,7 +195,7 @@ impl Origin { impl Default for Origin { fn default() -> Self { - Origin::FromArgSlots(Set::new()) + Origin::FromArgSlots(Set::default()) } } @@ -243,7 +246,7 @@ impl<'a> ForwardState<'a> { fn add_heap_cell(&mut self) -> HeapCellId { self.heap_cells.push(ForwardData { origin: Origin::default(), - aliases: Set::new(), + aliases: Set::default(), }) } @@ -256,7 +259,6 @@ impl<'a> ForwardState<'a> { // TODO: Optimize this so that it does not traverse the whole parent chain when the heap // cell is guaranteed to not have any back ref annotations before a certain point (i.e., // when we have some information about when the heap cell was created). - // TODO: Remove query points from back ref sets when they are set to 'DirectTouch'. if back_ref_states[version].overlay.contains_key(&heap_cell) { return back_ref_states[version] .overlay @@ -267,7 +269,7 @@ impl<'a> ForwardState<'a> { &[parent] => Self::back_refs_in_states(back_ref_states, parent, heap_cell).clone(), parents => { let num_parents = parents.len(); - let mut back_refs = HashSet::new(); + let mut back_refs = HashSet::default(); for parent_i in 0..num_parents { let parent = back_ref_states[version].parents[parent_i]; let parent_back_refs = @@ -358,10 +360,9 @@ impl<'a> ForwardState<'a> { fn touch(&mut self, version: BackRefStateVersionId, heap_cell: HeapCellId) { let back_refs = std::mem::take(self.back_refs(version, heap_cell)); - for &query_point in &back_refs { + for query_point in back_refs.into_iter() { self.fates.insert(query_point, Fate::DirectTouch); } - *self.back_refs(version, heap_cell) = back_refs; } fn recursive_touch(&mut self, version: BackRefStateVersionId, heap_cells: &[HeapCellId]) { @@ -429,14 +430,14 @@ impl<'a> ForwardState<'a> { debug_assert_eq!(input_slot_arrs.len(), 1); let arg_slots = input_slot_arrs[0]; // TODO: optimize this entire case! - let mut heap_cell_slots = HashMap::>::new(); + let mut heap_cell_slots = HashMap::>::default(); for (slot_i, &heap_cell) in arg_slots.iter().enumerate() { heap_cell_slots .entry(heap_cell) .or_insert_with(SmallVec::new) .push(slot_i.try_into().unwrap()); } - let mut arg_aliases = HashSet::new(); + let mut arg_aliases = HashSet::default(); for (heap_cell, slot_indices) in &heap_cell_slots { // Wire up to occurrences of the same heap cell in the argument slots for (i, &slot_i) in slot_indices.iter().enumerate() { @@ -832,7 +833,7 @@ impl<'a> ForwardState<'a> { ) { let block_info = graph.blocks().block_info(block); let new_version = self.back_ref_states.push(BackRefState { - overlay: HashMap::new(), + overlay: HashMap::default(), parents: block_info .predecessors .iter() @@ -872,7 +873,7 @@ impl<'a> ForwardState<'a> { graph: &ir::Graph, blocks: impl Iterator, ) -> HeapCellSlotMapping { - let mut heap_cell_to_slots = HashMap::new(); + let mut heap_cell_to_slots = HashMap::default(); for block in blocks { for val_id in block_values_inclusive(graph, block) { for (i, &heap_cell) in self.value_slots[val_id].unwrap().iter().enumerate() { @@ -894,7 +895,7 @@ impl<'a> ForwardState<'a> { heap_cell_slots_inductive: &HeapCellSlotMapping, heap_cell_slots_current: &HeapCellSlotMapping, ) -> ForwardSccSummary { - let mut summary = ForwardSccSummary::new(); + let mut summary = ForwardSccSummary::default(); for block in blocks { for val_id in block_values_inclusive(graph, block) { let block_version = self.block_versions[block].unwrap(); @@ -904,9 +905,9 @@ impl<'a> ForwardState<'a> { .enumerate() .map(|(slot_i, &heap_cell)| { let mut val_summary = ForwardSccSlotSummary { - pre_aliases: Set::new(), - inductive_aliases: Set::new(), - internal_aliases: Set::new(), + pre_aliases: Set::default(), + inductive_aliases: Set::default(), + internal_aliases: Set::default(), back_refs: Self::back_refs_in_states( &mut self.back_ref_states, block_version, @@ -914,35 +915,49 @@ impl<'a> ForwardState<'a> { ) .clone(), }; + let matching_slot: (ir::ValueId, u32) = + (val_id, slot_i.try_into().unwrap()); + + // Iterators and cloning are used here. + // These iterators are small with cheap to clone items so it is fast and ok. let aliased_heap_cells = std::iter::once(heap_cell) .chain(self.heap_cells[heap_cell].aliases.iter().cloned()); - for aliased in aliased_heap_cells { - if aliased < min_new_id { - val_summary.pre_aliases.insert(aliased); - } - for &aliased_slot in heap_cell_slots_current - .get(&aliased) - .iter() - .cloned() - .flatten() - { - if aliased_slot == (val_id, slot_i.try_into().unwrap()) { - continue; - } - val_summary.internal_aliases.insert(aliased_slot); - } - for &aliased_slot in heap_cell_slots_inductive - .get(&aliased) - .iter() - .cloned() - .flatten() - { - if aliased_slot == (val_id, slot_i.try_into().unwrap()) { - continue; - } - val_summary.inductive_aliases.insert(aliased_slot); - } - } + let pre_aliases_iter = aliased_heap_cells + .clone() + .filter(|&aliased| aliased < min_new_id); + let internal_aliases_iter = aliased_heap_cells + .clone() + .flat_map(|aliased| { + heap_cell_slots_current.get(&aliased).into_iter().flatten() + }) + .cloned() + .filter(|&aliased_slot| aliased_slot == matching_slot); + let inductive_aliases_iter = aliased_heap_cells + .flat_map(|aliased| { + heap_cell_slots_inductive + .get(&aliased) + .into_iter() + .flatten() + }) + .cloned() + .filter(|&aliased_slot| aliased_slot == matching_slot); + + // These are all cheap to clone. + // Clone them and count them to avoid expensive resizing of HashSets. + let pre_aliases_count = pre_aliases_iter.clone().count(); + val_summary.pre_aliases.reserve(pre_aliases_count); + val_summary.pre_aliases.extend(pre_aliases_iter); + + let internal_aliases_count = internal_aliases_iter.clone().count(); + val_summary.internal_aliases.reserve(internal_aliases_count); + val_summary.internal_aliases.extend(internal_aliases_iter); + + let inductive_aliases_count = inductive_aliases_iter.clone().count(); + val_summary + .inductive_aliases + .reserve(inductive_aliases_count); + val_summary.inductive_aliases.extend(inductive_aliases_iter); + val_summary }) .collect(); @@ -993,7 +1008,7 @@ impl<'a> ForwardState<'a> { .into_iter() .collect::>(); let init_version = self.back_ref_states.push(BackRefState { - overlay: HashMap::new(), + overlay: HashMap::default(), parents: init_version_parents, }); @@ -1114,11 +1129,11 @@ impl<'a> ForwardState<'a> { .map(|arg_val_id| { let slot_count = id_result_slot_count(sc, graph, arg_val_id); let arg_slots: &[_] = slots_arena.alloc_extend((0..slot_count).map(|i| { - let mut origin_arg_slots = Set::new(); + let mut origin_arg_slots = Set::default(); origin_arg_slots.insert(i); heap_cells.push(ForwardData { origin: Origin::FromArgSlots(origin_arg_slots), - aliases: Set::new(), + aliases: Set::default(), }) })); if let Some(arg_alias) = arg_alias { @@ -1138,7 +1153,7 @@ impl<'a> ForwardState<'a> { .flatten() .enumerate() .map(|(slot_i, &heap_cell)| { - let mut heap_cell_back_refs = HashSet::new(); + let mut heap_cell_back_refs = HashSet::default(); heap_cell_back_refs.insert(QueryPoint::EntryArg(slot_i.try_into().unwrap())); (heap_cell, heap_cell_back_refs) }) @@ -1160,7 +1175,7 @@ impl<'a> ForwardState<'a> { block_versions: IdVec::filled_with(graph.blocks().block_count(), || None), block_versions_inductive: IdVec::filled_with(graph.blocks().block_count(), || None), entry_version, - fates: HashMap::new(), + fates: HashMap::default(), }; for scc_id in graph.sccs().count().iter() { @@ -1168,7 +1183,7 @@ impl<'a> ForwardState<'a> { } let exit_version = state.back_ref_states.push(BackRefState { - overlay: HashMap::new(), + overlay: HashMap::default(), parents: graph .exit_blocks() .iter() @@ -1242,7 +1257,7 @@ impl Default for Fate { fn default() -> Self { Fate::Other { indirect_touch: false, - ret_slots: Set::new(), + ret_slots: Set::default(), } } } @@ -1256,12 +1271,12 @@ fn analyze_func( let slots_arena = Arena::new(); let (forward, ret_slots) = ForwardState::analyze_graph(&slots_arena, sc, ctx, &func_def.graph, arg_alias); - let mut heap_cell_to_arg_slot = HashMap::::new(); + let mut heap_cell_to_arg_slot = HashMap::::default(); for (slot_i, &heap_cell) in forward.arg_slots.unwrap().iter().enumerate() { let existing = heap_cell_to_arg_slot.insert(heap_cell, slot_i.try_into().unwrap()); debug_assert!(existing.is_none()); } - let mut heap_cell_to_ret_slots = HashMap::>::new(); + let mut heap_cell_to_ret_slots = HashMap::>::default(); for (slot_i, &heap_cell) in ret_slots.iter().enumerate() { heap_cell_to_ret_slots .entry(heap_cell) @@ -1281,8 +1296,8 @@ fn analyze_func( .iter() .enumerate() .map(|(this_ret_slot_i, &heap_cell)| { - let mut arg_aliases = Set::new(); - let mut ret_aliases = Set::new(); + let mut arg_aliases = Set::default(); + let mut ret_aliases = Set::default(); for other in std::iter::once(heap_cell) .chain(forward.heap_cells[heap_cell].aliases.iter().cloned()) { @@ -1437,8 +1452,8 @@ impl<'a> GlobalAnalysisContext<'a> { let mut scc_ctx = SccAnalysisContext { global: &mut *self, scc, - prev_iter: HashMap::new(), - curr_iter: HashMap::new(), + prev_iter: HashMap::default(), + curr_iter: HashMap::default(), }; match scc_kind { SccKind::Acyclic => { @@ -1552,19 +1567,18 @@ struct Query { impl Query { fn to_spec(&self, func: FuncId) -> api::FuncSpec { - use sha2::{Digest, Sha256}; - let mut hasher = Sha256::new(); - hasher.update(func.0.to_le_bytes()); - hasher.update((self.arg_aliases.len() as u64).to_le_bytes()); + let mut hasher = blake3::Hasher::new(); + hasher.update(&func.0.to_le_bytes()); + hasher.update(&(self.arg_aliases.len() as u64).to_le_bytes()); for arg_alias in &self.arg_aliases { - hasher.update(arg_alias.fst().to_le_bytes()); - hasher.update(arg_alias.snd().to_le_bytes()); + hasher.update(&arg_alias.fst().to_le_bytes()); + hasher.update(&arg_alias.snd().to_le_bytes()); } - hasher.update((self.arg_slots_touched.len() as u64).to_le_bytes()); + hasher.update(&(self.arg_slots_touched.len() as u64).to_le_bytes()); for &arg_touched in &self.arg_slots_touched { hasher.update(&[arg_touched as u8]); } - hasher.update((self.ret_slots_touched.len() as u64).to_le_bytes()); + hasher.update(&(self.ret_slots_touched.len() as u64).to_le_bytes()); for &ret_touched in &self.ret_slots_touched { hasher.update(&[ret_touched as u8]); } @@ -1738,7 +1752,7 @@ pub(crate) fn analyze(tc: TypeCache, program: &ir::Program) -> ProgramSolutions func_defs: &program.funcs, sccs: &func_sccs, func_to_scc: &func_to_scc, - committed: IdVec::filled_with(program.funcs.count(), HashMap::new), + committed: IdVec::filled_with(program.funcs.count(), HashMap::default), }; for (_, &func) in &program.entry_points { @@ -1748,7 +1762,7 @@ pub(crate) fn analyze(tc: TypeCache, program: &ir::Program) -> ProgramSolutions } let mut func_solutions = FuncSolutions { - solutions: IdVec::filled_with(program.funcs.count(), HashMap::new), + solutions: IdVec::filled_with(program.funcs.count(), HashMap::default), }; let entry_point_solutions = program.entry_points.map(|_, &func| { @@ -1773,8 +1787,7 @@ pub(crate) fn analyze(tc: TypeCache, program: &ir::Program) -> ProgramSolutions // specialization and all update modes are `Immutable`: fn hash_func_id_trivial(func_id: FuncId) -> api::FuncSpec { - use sha2::{Digest, Sha256}; - let mut hasher = Sha256::new(); + let mut hasher = blake3::Hasher::new(); hasher.update(&func_id.0.to_le_bytes()); api::FuncSpec(hasher.finalize().into()) } diff --git a/vendor/morphic_lib/src/api.rs b/crates/vendor/morphic_lib/src/api.rs similarity index 99% rename from vendor/morphic_lib/src/api.rs rename to crates/vendor/morphic_lib/src/api.rs index b93180764c..53b3335c12 100644 --- a/vendor/morphic_lib/src/api.rs +++ b/crates/vendor/morphic_lib/src/api.rs @@ -1,4 +1,6 @@ -use sha2::{digest::Digest, Sha256}; +// https://github.com/morphic-lang/morphic_lib/issues/19 +#![allow(clippy::result_large_err)] + use smallvec::SmallVec; use std::collections::{btree_map::Entry, BTreeMap}; use std::rc::Rc; @@ -1652,7 +1654,7 @@ pub fn solve_trivial(api_program: Program) -> Result { // TODO: Remove this; it's only used in obsolete logic for populating const definitions with trivial // solutions -fn hash_bstr(hasher: &mut Sha256, bstr: &[u8]) { +fn hash_bstr(hasher: &mut blake3::Hasher, bstr: &[u8]) { let header = (bstr.len() as u64).to_le_bytes(); hasher.update(&header); hasher.update(bstr); @@ -1661,7 +1663,7 @@ fn hash_bstr(hasher: &mut Sha256, bstr: &[u8]) { // TODO: Remove this; it's only used in obsolete logic for populating const definitions with trivial // solutions fn hash_func_name(mod_: ModName, func: FuncName) -> FuncSpec { - let mut hasher = Sha256::new(); + let mut hasher = blake3::Hasher::new(); hash_bstr(&mut hasher, mod_.0); hash_bstr(&mut hasher, func.0); FuncSpec(hasher.finalize().into()) diff --git a/vendor/morphic_lib/src/bindings.rs b/crates/vendor/morphic_lib/src/bindings.rs similarity index 100% rename from vendor/morphic_lib/src/bindings.rs rename to crates/vendor/morphic_lib/src/bindings.rs diff --git a/vendor/morphic_lib/src/ir.rs b/crates/vendor/morphic_lib/src/ir.rs similarity index 100% rename from vendor/morphic_lib/src/ir.rs rename to crates/vendor/morphic_lib/src/ir.rs diff --git a/vendor/morphic_lib/src/lib.rs b/crates/vendor/morphic_lib/src/lib.rs similarity index 100% rename from vendor/morphic_lib/src/lib.rs rename to crates/vendor/morphic_lib/src/lib.rs diff --git a/vendor/morphic_lib/src/name_cache.rs b/crates/vendor/morphic_lib/src/name_cache.rs similarity index 100% rename from vendor/morphic_lib/src/name_cache.rs rename to crates/vendor/morphic_lib/src/name_cache.rs diff --git a/vendor/morphic_lib/src/preprocess.rs b/crates/vendor/morphic_lib/src/preprocess.rs similarity index 98% rename from vendor/morphic_lib/src/preprocess.rs rename to crates/vendor/morphic_lib/src/preprocess.rs index 7126602c52..bd1f9e21f9 100644 --- a/vendor/morphic_lib/src/preprocess.rs +++ b/crates/vendor/morphic_lib/src/preprocess.rs @@ -11,6 +11,9 @@ //! - Scope checking for values //! - Typechecking +// https://github.com/morphic-lang/morphic_lib/issues/19 +#![allow(clippy::result_large_err)] + use smallvec::{smallvec, SmallVec}; use crate::api; @@ -157,23 +160,23 @@ impl std::fmt::Display for Error { if let Some(mod_) = &self.mod_ { loc_prefix(f)?; - write!(f, "module {:?}", mod_)?; + write!(f, "module {mod_:?}")?; } if let Some(def) = &self.def { loc_prefix(f)?; match def { DefName::Type(name) => { - write!(f, "named type definition {:?}", name)?; + write!(f, "named type definition {name:?}")?; } DefName::Func(name) => { - write!(f, "function definition {:?}", name)?; + write!(f, "function definition {name:?}")?; } DefName::Const(name) => { - write!(f, "constant definition {:?}", name)?; + write!(f, "constant definition {name:?}")?; } DefName::EntryPoint(name) => { - write!(f, "entry point definition {:?}", name)?; + write!(f, "entry point definition {name:?}")?; } } } @@ -182,13 +185,13 @@ impl std::fmt::Display for Error { loc_prefix(f)?; match binding { BindingLocation::Type(id) => { - write!(f, "definition of type binding {:?}", id)?; + write!(f, "definition of type binding {id:?}")?; } BindingLocation::Value(id) => { - write!(f, "definition of value binding {:?}", id)?; + write!(f, "definition of value binding {id:?}")?; } BindingLocation::Continuation(id) => { - write!(f, "definition of continuation binding {:?}", id)?; + write!(f, "definition of continuation binding {id:?}")?; } } } diff --git a/vendor/morphic_lib/src/render_api_ir.rs b/crates/vendor/morphic_lib/src/render_api_ir.rs similarity index 97% rename from vendor/morphic_lib/src/render_api_ir.rs rename to crates/vendor/morphic_lib/src/render_api_ir.rs index dadbec892d..f7db922b89 100644 --- a/vendor/morphic_lib/src/render_api_ir.rs +++ b/crates/vendor/morphic_lib/src/render_api_ir.rs @@ -39,7 +39,7 @@ impl RenderContext { .extend((0..self.indent_level * self.spaces_per_level).map(|_| ' ')); self.pending_indent = false; } - write!(&mut self.content, "{}", to_write).expect("writing to string failed"); + write!(&mut self.content, "{to_write}").expect("writing to string failed"); } fn writeln(&mut self, to_write: impl std::fmt::Display) { @@ -146,8 +146,7 @@ fn render_op(builder: &ExprBuilder, ctx: &mut RenderContext, op: &Op) { match op { Op::Arg | Op::ContinuationArg | Op::DeclareContinuation { .. } => { ctx.write(format_args!( - "/* internal error: {:?} should not be rendered as a value */", - op + "/* internal error: {op:?} should not be rendered as a value */" )); } @@ -252,7 +251,7 @@ fn render_op(builder: &ExprBuilder, ctx: &mut RenderContext, op: &Op) { } Op::GetTupleField { field_idx } => { - ctx.write(format_args!("get_tuple_field {}", field_idx)); + ctx.write(format_args!("get_tuple_field {field_idx}")); } Op::MakeUnion { @@ -270,11 +269,11 @@ fn render_op(builder: &ExprBuilder, ctx: &mut RenderContext, op: &Op) { ctx.write(type_ident(*variant_type)); }, ); - ctx.write(format_args!("> {}", variant_idx)); + ctx.write(format_args!("> {variant_idx}")); } Op::UnwrapUnion { variant_idx } => { - ctx.write(format_args!("unwrap_union {}", variant_idx)); + ctx.write(format_args!("unwrap_union {variant_idx}")); } Op::MakeNamed { named_mod, named } => { diff --git a/vendor/morphic_lib/src/type_cache.rs b/crates/vendor/morphic_lib/src/type_cache.rs similarity index 100% rename from vendor/morphic_lib/src/type_cache.rs rename to crates/vendor/morphic_lib/src/type_cache.rs diff --git a/vendor/morphic_lib/src/util/blocks.rs b/crates/vendor/morphic_lib/src/util/blocks.rs similarity index 100% rename from vendor/morphic_lib/src/util/blocks.rs rename to crates/vendor/morphic_lib/src/util/blocks.rs diff --git a/vendor/morphic_lib/src/util/bytes_id.rs b/crates/vendor/morphic_lib/src/util/bytes_id.rs similarity index 97% rename from vendor/morphic_lib/src/util/bytes_id.rs rename to crates/vendor/morphic_lib/src/util/bytes_id.rs index b75d618271..cbcd00fbc5 100644 --- a/vendor/morphic_lib/src/util/bytes_id.rs +++ b/crates/vendor/morphic_lib/src/util/bytes_id.rs @@ -15,7 +15,7 @@ macro_rules! bytes_id { $owned_vis struct $owned($owned_vis ::smallvec::SmallVec<[u8; 23]>); impl $owned { - fn borrowed<'a>(&'a self) -> $borrowed<'a> { + fn borrowed(&self) -> $borrowed<'_> { $borrowed(&self.0) } } diff --git a/vendor/morphic_lib/src/util/flat_slices.rs b/crates/vendor/morphic_lib/src/util/flat_slices.rs similarity index 100% rename from vendor/morphic_lib/src/util/flat_slices.rs rename to crates/vendor/morphic_lib/src/util/flat_slices.rs diff --git a/vendor/morphic_lib/src/util/forward_trait.rs b/crates/vendor/morphic_lib/src/util/forward_trait.rs similarity index 100% rename from vendor/morphic_lib/src/util/forward_trait.rs rename to crates/vendor/morphic_lib/src/util/forward_trait.rs diff --git a/vendor/morphic_lib/src/util/get2_mut.rs b/crates/vendor/morphic_lib/src/util/get2_mut.rs similarity index 100% rename from vendor/morphic_lib/src/util/get2_mut.rs rename to crates/vendor/morphic_lib/src/util/get2_mut.rs diff --git a/vendor/morphic_lib/src/util/id_bi_map.rs b/crates/vendor/morphic_lib/src/util/id_bi_map.rs similarity index 93% rename from vendor/morphic_lib/src/util/id_bi_map.rs rename to crates/vendor/morphic_lib/src/util/id_bi_map.rs index 876574c49f..d8e46ef215 100644 --- a/vendor/morphic_lib/src/util/id_bi_map.rs +++ b/crates/vendor/morphic_lib/src/util/id_bi_map.rs @@ -1,7 +1,10 @@ -use std::collections::hash_map::{Entry, HashMap}; +use std::collections::hash_map::Entry; use std::hash::Hash; use std::ops::Deref; +// use std::collections::HashMap; +use roc_collections::MutMap as HashMap; + use crate::util::id_type::Id; use crate::util::id_vec::IdVec; @@ -32,7 +35,7 @@ impl IdBiMap { pub fn new() -> Self { IdBiMap { key_to_val: IdVec::new(), - val_to_key: HashMap::new(), + val_to_key: HashMap::default(), } } diff --git a/vendor/morphic_lib/src/util/id_type.rs b/crates/vendor/morphic_lib/src/util/id_type.rs similarity index 100% rename from vendor/morphic_lib/src/util/id_type.rs rename to crates/vendor/morphic_lib/src/util/id_type.rs diff --git a/vendor/morphic_lib/src/util/id_vec.rs b/crates/vendor/morphic_lib/src/util/id_vec.rs similarity index 100% rename from vendor/morphic_lib/src/util/id_vec.rs rename to crates/vendor/morphic_lib/src/util/id_vec.rs diff --git a/vendor/morphic_lib/src/util/mod.rs b/crates/vendor/morphic_lib/src/util/mod.rs similarity index 100% rename from vendor/morphic_lib/src/util/mod.rs rename to crates/vendor/morphic_lib/src/util/mod.rs diff --git a/vendor/morphic_lib/src/util/norm_pair.rs b/crates/vendor/morphic_lib/src/util/norm_pair.rs similarity index 100% rename from vendor/morphic_lib/src/util/norm_pair.rs rename to crates/vendor/morphic_lib/src/util/norm_pair.rs diff --git a/vendor/morphic_lib/src/util/op_graph.rs b/crates/vendor/morphic_lib/src/util/op_graph.rs similarity index 100% rename from vendor/morphic_lib/src/util/op_graph.rs rename to crates/vendor/morphic_lib/src/util/op_graph.rs diff --git a/vendor/morphic_lib/src/util/replace_none.rs b/crates/vendor/morphic_lib/src/util/replace_none.rs similarity index 100% rename from vendor/morphic_lib/src/util/replace_none.rs rename to crates/vendor/morphic_lib/src/util/replace_none.rs diff --git a/vendor/morphic_lib/src/util/strongly_connected.rs b/crates/vendor/morphic_lib/src/util/strongly_connected.rs similarity index 100% rename from vendor/morphic_lib/src/util/strongly_connected.rs rename to crates/vendor/morphic_lib/src/util/strongly_connected.rs diff --git a/vendor/morphic_lib/tests/basic.rs b/crates/vendor/morphic_lib/tests/basic.rs similarity index 96% rename from vendor/morphic_lib/tests/basic.rs rename to crates/vendor/morphic_lib/tests/basic.rs index 2c7531c392..d975386091 100644 --- a/vendor/morphic_lib/tests/basic.rs +++ b/crates/vendor/morphic_lib/tests/basic.rs @@ -1,3 +1,6 @@ +// https://github.com/morphic-lang/morphic_lib/issues/19 +#![allow(clippy::result_large_err)] + use morphic_lib::{ BlockExpr, CalleeSpecVar, EntryPointName, Error, ExprContext, FuncDefBuilder, FuncName, ModDefBuilder, ModName, ProgramBuilder, TypeContext, UpdateModeVar, diff --git a/vendor/morphic_lib/tests/recursive.rs b/crates/vendor/morphic_lib/tests/recursive.rs similarity index 97% rename from vendor/morphic_lib/tests/recursive.rs rename to crates/vendor/morphic_lib/tests/recursive.rs index 629e19852d..d24271e417 100644 --- a/vendor/morphic_lib/tests/recursive.rs +++ b/crates/vendor/morphic_lib/tests/recursive.rs @@ -1,3 +1,6 @@ +// https://github.com/morphic-lang/morphic_lib/issues/19 +#![allow(clippy::result_large_err)] + use morphic_lib::{ BlockExpr, CalleeSpecVar, EntryPointName, Error, ExprContext, FuncDefBuilder, FuncName, ModDefBuilder, ModName, ProgramBuilder, TypeContext, UpdateMode, UpdateModeVar, diff --git a/vendor/morphic_lib/tests/structures.rs b/crates/vendor/morphic_lib/tests/structures.rs similarity index 96% rename from vendor/morphic_lib/tests/structures.rs rename to crates/vendor/morphic_lib/tests/structures.rs index 7143e3f9e1..7e09fc11ee 100644 --- a/vendor/morphic_lib/tests/structures.rs +++ b/crates/vendor/morphic_lib/tests/structures.rs @@ -1,3 +1,6 @@ +// https://github.com/morphic-lang/morphic_lib/issues/19 +#![allow(clippy::result_large_err)] + use morphic_lib::{ BlockExpr, EntryPointName, Error, ExprContext, FuncDefBuilder, FuncName, ModDefBuilder, ModName, ProgramBuilder, TypeContext, UpdateMode, UpdateModeVar, diff --git a/vendor/pathfinding/Cargo.toml b/crates/vendor/pathfinding/Cargo.toml similarity index 100% rename from vendor/pathfinding/Cargo.toml rename to crates/vendor/pathfinding/Cargo.toml diff --git a/crates/vendor/pathfinding/src/lib.rs b/crates/vendor/pathfinding/src/lib.rs new file mode 100644 index 0000000000..6846cc276c --- /dev/null +++ b/crates/vendor/pathfinding/src/lib.rs @@ -0,0 +1,387 @@ +// Adapted from the Pathfinding crate v2.0.3 by Samuel Tardieu , +// licensed under the Apache License, version 2.0 - https://www.apache.org/licenses/LICENSE-2.0 +// +// The original source code can be found at: https://github.com/samueltardieu/pathfinding +// +// Thank you, Samuel! +// +// +// +// This is modified from the original source to use the Roc compiler's preferred hashers +// instead of the SipHash hasher which Rust hash collections use by default. +// +// SipHash defends against hash flooding attacks by generating a random seed +// whenever a new hasher is instantiated, and which is designed to prevent attackers +// from crafting intentional collisions that amplify denial-of-service attacks. +// Since this is a compiler, we aren't worried about denial-of-service attacks. +// +// The primary motivation for this change is wanting the compiler to always give exactly +// the same answer given the same inputs. So if you give it the same source files, it should +// produce identical binaries every time. SipHash by design gives different answers on each run. +// +// Secondarily, SipHash isn't the fastest hashing algorithm out there, so we can get +// slightly better performance by using a faster hasher. + +use roc_collections::all::{default_hasher, BuildHasher, MutSet}; +use std::collections::{HashMap, HashSet, VecDeque}; +use std::hash::Hash; +use std::mem; + +/// Find a topological order in a directed graph if one exists. +/// +/// - `nodes` is a collection of nodes. +/// - `successors` returns a list of successors for a given node. +/// +/// The function returns either `Ok` with an acceptable topological order, +/// or `Err` with a node belonging to a cycle. In the latter case, the +/// strongly connected set can then be found using the +/// [`strongly_connected_component`](super::strongly_connected_components::strongly_connected_component) +/// function, or if only one of the loops is needed the [`bfs_loop`][super::bfs::bfs_loop] function +/// can be used instead to identify one of the shortest loops involving this node. +/// +/// # Examples +/// +/// We will sort integers from 1 to 9, each integer having its two immediate +/// greater numbers as successors: +/// +/// //``` +/// extern crate roc; +/// +/// use roc::graph::topological_sort; +/// +/// fn successors(node: &usize) -> Vec { +/// match *node { +/// n if n <= 7 => vec![n+1, n+2], +/// 8 => vec![9], +/// _ => vec![], +/// } +/// } +/// +/// let sorted = topological_sort(&[3, 7, 1, 4, 2, 9, 8, 6, 5], successors); +/// assert_eq!(sorted, Ok(vec![1, 2, 3, 4, 5, 6, 7, 8, 9])); +/// //``` +/// +/// If, however, there is a loop in the graph (for example, all nodes but 7 +/// have also 7 has a successor), one of the nodes in the loop will be returned as +/// an error: +/// +/// //``` +/// extern crate roc; +/// +/// use roc::graph::*; +/// +/// fn successors(node: &usize) -> Vec { +/// match *node { +/// n if n <= 6 => vec![n+1, n+2, 7], +/// 7 => vec![8, 9], +/// 8 => vec![7, 9], +/// _ => vec![7], +/// } +/// } +/// +/// let sorted = topological_sort(&[3, 7, 1, 4, 2, 9, 8, 6, 5], successors); +/// assert!(sorted.is_err()); +/// +/// // Let's assume that the returned node is 8 (it can be any node which is part +/// // of a loop). We can lookup up one of the shortest loops containing 8 +/// // (8 -> 7 -> 8 is the unique loop with two hops containing 8): +/// +/// // assert_eq!(bfs_loop(&8, successors), Some(vec![8, 7, 8])); +/// +/// // We can also request the whole strongly connected set containing 8. Here +/// // 7, 8, and 9 are all reachable from one another. +/// +/// let mut set = strongly_connected_component(&8, successors); +/// set.sort(); +/// assert_eq!(set, vec![7, 8, 9]); +/// //``` +pub fn topological_sort(nodes: I, mut successors: FN) -> Result, N> +where + N: Eq + Hash + Clone, + I: Iterator, + FN: FnMut(&N) -> IN, + IN: IntoIterator, +{ + let size_hint = nodes.size_hint().0; + let mut unmarked: MutSet = nodes.collect::>(); + let mut marked = HashSet::with_capacity_and_hasher(size_hint, default_hasher()); + let mut temp = MutSet::default(); + let mut sorted = VecDeque::with_capacity(size_hint); + while let Some(node) = unmarked.iter().next().cloned() { + temp.clear(); + visit( + &node, + &mut successors, + &mut unmarked, + &mut marked, + &mut temp, + &mut sorted, + )?; + } + Ok(sorted.into_iter().collect()) +} + +fn visit( + node: &N, + successors: &mut FN, + unmarked: &mut MutSet, + marked: &mut MutSet, + temp: &mut MutSet, + sorted: &mut VecDeque, +) -> Result<(), N> +where + N: Eq + Hash + Clone, + FN: FnMut(&N) -> IN, + IN: IntoIterator, +{ + unmarked.remove(node); + if marked.contains(node) { + return Ok(()); + } + if temp.contains(node) { + return Err(node.clone()); + } + temp.insert(node.clone()); + for n in successors(node) { + visit(&n, successors, unmarked, marked, temp, sorted)?; + } + marked.insert(node.clone()); + sorted.push_front(node.clone()); + Ok(()) +} + +/// Topologically sort a directed graph into groups of independent nodes. +/// +/// - `nodes` is a collection of nodes. +/// - `successors` returns a list of successors for a given node. +/// +/// This function works like [`topological_sort`](self::topological_sort), but +/// rather than producing a single ordering of nodes, this function partitions +/// the nodes into groups: the first group contains all nodes with no +/// dependencies, the second group contains all nodes whose only dependencies +/// are in the first group, and so on. Concatenating the groups produces a +/// valid topological sort regardless of how the nodes within each group are +/// reordered. No guarantees are made about the order of nodes within each +/// group. +/// +/// The function returns either `Ok` with a valid list of groups, or `Err` with +/// a (groups, remaining) tuple containing a (possibly empty) partial list of +/// groups, and a list of remaining nodes that could not be grouped due to +/// cycles. In the error case, the strongly connected set(s) can then be found +/// using the +/// [`strongly_connected_components`](super::strongly_connected_components::strongly_connected_components) +/// function on the list of remaining nodes. +/// +/// The current implementation uses a variation of [Kahn's +/// algorithm](https://en.wikipedia.org/wiki/Topological_sorting#Kahn's_algorithm), +/// and runs in O(|V| + |E|) time. +#[allow(clippy::type_complexity)] +#[allow(dead_code)] +pub fn topological_sort_into_groups( + nodes: &[N], + mut successors: FN, +) -> Result>, (Vec>, Vec)> +where + N: Eq + Hash + Clone, + FN: FnMut(&N) -> IN, + IN: IntoIterator, +{ + if nodes.is_empty() { + return Ok(Vec::new()); + } + let mut succs_map = HashMap::, BuildHasher>::with_capacity_and_hasher( + nodes.len(), + default_hasher(), + ); + let mut preds_map = + HashMap::::with_capacity_and_hasher(nodes.len(), default_hasher()); + for node in nodes.iter() { + succs_map.insert(node.clone(), successors(node).into_iter().collect()); + preds_map.insert(node.clone(), 0); + } + + for succs in succs_map.values() { + for succ in succs.iter() { + *preds_map + .get_mut(succ) + .unwrap_or_else(|| panic!("key missing from preds_map")) += 1; + } + } + let mut groups = Vec::>::new(); + let mut prev_group: Vec = preds_map + .iter() + .filter_map(|(node, &num_preds)| { + if num_preds == 0 { + Some(node.clone()) + } else { + None + } + }) + .collect(); + if prev_group.is_empty() { + let remaining: Vec = preds_map.into_keys().collect(); + return Err((Vec::new(), remaining)); + } + for node in &prev_group { + preds_map.remove(node); + } + while !preds_map.is_empty() { + let mut next_group = Vec::::new(); + for node in &prev_group { + for succ in &succs_map[node] { + { + let num_preds = preds_map.get_mut(succ).unwrap(); + *num_preds -= 1; + if *num_preds > 0 { + continue; + } + } + next_group.push(preds_map.remove_entry(succ).unwrap().0); + } + } + groups.push(mem::replace(&mut prev_group, next_group)); + if prev_group.is_empty() { + let remaining: Vec = preds_map.into_keys().collect(); + return Err((groups, remaining)); + } + } + groups.push(prev_group); + Ok(groups) +} + +// Separate nodes of a directed graph into [strongly connected +// components](https://en.wikipedia.org/wiki/Strongly_connected_component). +// +// A [path-based strong component +// algorithm](https://en.wikipedia.org/wiki/Path-based_strong_component_algorithm) +// is used. + +struct Params +where + N: Clone + Hash + Eq, + FN: FnMut(&N) -> IN, + IN: IntoIterator, +{ + preorders: HashMap, BuildHasher>, + c: usize, + successors: FN, + p: Vec, + s: Vec, + scc: Vec>, + scca: MutSet, +} + +impl Params +where + N: Clone + Hash + Eq, + FN: FnMut(&N) -> IN, + IN: IntoIterator, +{ + fn new(nodes: &[N], successors: FN) -> Self { + Self { + preorders: nodes.iter().map(|n| (n.clone(), None)).collect::, + BuildHasher, + >>(), + c: 0, + successors, + p: Vec::new(), + s: Vec::new(), + scc: Vec::new(), + scca: MutSet::default(), + } + } +} + +fn recurse_onto(v: &N, params: &mut Params) +where + N: Clone + Hash + Eq, + FN: FnMut(&N) -> IN, + IN: IntoIterator, +{ + params.preorders.insert(v.clone(), Some(params.c)); + params.c += 1; + params.s.push(v.clone()); + params.p.push(v.clone()); + for w in (params.successors)(v) { + if !params.scca.contains(&w) { + if let Some(pw) = params.preorders.get(&w).and_then(|w| *w) { + while params.preorders[¶ms.p[params.p.len() - 1]].unwrap() > pw { + params.p.pop(); + } + } else { + recurse_onto(&w, params); + } + } + } + if params.p[params.p.len() - 1] == *v { + params.p.pop(); + let mut component = Vec::new(); + while let Some(node) = params.s.pop() { + component.push(node.clone()); + params.scca.insert(node.clone()); + params.preorders.remove(&node); + if node == *v { + break; + } + } + params.scc.push(component); + } +} + +/// Partition nodes reachable from a starting point into strongly connected components. +/// +/// - `start` is the node we want to explore the graph from. +/// - `successors` returns a list of successors for a given node. +/// +/// The function returns a list of strongly connected components sets. It will contain +/// at least one component (the one containing the `start` node). +pub fn strongly_connected_components_from(start: &N, successors: FN) -> Vec> +where + N: Clone + Hash + Eq, + FN: FnMut(&N) -> IN, + IN: IntoIterator, +{ + let mut params = Params::new(&[], successors); + recurse_onto(start, &mut params); + params.scc +} + +/// Compute the strongly connected component containing a given node. +/// +/// - `node` is the node we want the strongly connected component for. +/// - `successors` returns a list of successors for a given node. +/// +/// The function returns the strongly connected component containing the node, +/// which is guaranteed to contain at least `node`. +pub fn strongly_connected_component(node: &N, successors: FN) -> Vec +where + N: Clone + Hash + Eq, + FN: FnMut(&N) -> IN, + IN: IntoIterator, +{ + strongly_connected_components_from(node, successors) + .pop() + .unwrap() +} + +/// Partition all strongly connected components in a graph. +/// +/// - `nodes` is a collection of nodes. +/// - `successors` returns a list of successors for a given node. +/// +/// The function returns a list of strongly connected components sets. +#[allow(dead_code)] +pub fn strongly_connected_components(nodes: &[N], successors: FN) -> Vec> +where + N: Clone + Hash + Eq, + FN: FnMut(&N) -> IN, + IN: IntoIterator, +{ + let mut params = Params::new(nodes, successors); + while let Some(node) = params.preorders.keys().find(|_| true).cloned() { + recurse_onto(&node, &mut params); + } + params.scc +} diff --git a/crates/vendor/pretty/Cargo.toml b/crates/vendor/pretty/Cargo.toml new file mode 100644 index 0000000000..78bf73d5b6 --- /dev/null +++ b/crates/vendor/pretty/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "ven_pretty" +version = "0.9.1-alpha.0" +authors = [ "Jonathan Sterling ", "Darin Morrison ", "Markus Westerlind "] +description = "Wadler-style pretty-printing combinators in Rust" +documentation = "https://docs.rs/pretty/" +keywords = ["console", "functional", "pretty-printing"] +license = "MIT" +readme = "README.md" +repository = "https://github.com/Marwes/pretty.rs" +edition = "2018" + +[package.metadata.docs.rs] +features = ["termcolor"] + +[dependencies] +arrayvec = "0.7.2" +typed-arena = "2.0.2" +termcolor = { version = "1.2.0", optional = true } diff --git a/vendor/pretty/LICENSE b/crates/vendor/pretty/LICENSE similarity index 100% rename from vendor/pretty/LICENSE rename to crates/vendor/pretty/LICENSE diff --git a/crates/vendor/pretty/README.md b/crates/vendor/pretty/README.md new file mode 100644 index 0000000000..e6e4262b66 --- /dev/null +++ b/crates/vendor/pretty/README.md @@ -0,0 +1,44 @@ +# pretty.rs + +[Original docs](https://docs.rs/pretty) + +Pretty printing combinators for Rust + +## Synopsis + +This crate provides functionality for defining pretty printers. It is +particularly useful for printing structured recursive data like trees. + +The implementation was originally based on Larsen's SML translation +(https://github.com/kfl/wpp) of Wadler's Haskell pretty printer +(https://homepages.inf.ed.ac.uk/wadler/papers/prettier/prettier.pdf). It +has since been modified in various ways to better fit Rust's +programming model. In particular, it uses iteration rather than +recursion and provides streaming output. + +## Documentation + +See the generated API documentation [here](https://docs.rs/pretty). + +## Requirements + +1. [Rust](https://www.rust-lang.org/) +2. [Cargo](https://crates.io/) + +You can install both with the following: + +``` +$ curl -s https://static.rust-lang.org/rustup.sh | sudo sh +``` + +See [Installation](https://doc.rust-lang.org/book/ch01-01-installation.html) for further details. + +## Usage + +``` +$ cargo build ## build library and binary +$ cargo run --example trees ## run the example (pretty trees) +$ cargo run --example colored --features termcolor ## run the example (pretty colored output) +$ cargo bench ## run benchmarks +$ cargo test ## run tests +``` diff --git a/crates/vendor/pretty/src/lib.rs b/crates/vendor/pretty/src/lib.rs new file mode 100644 index 0000000000..672d22cda8 --- /dev/null +++ b/crates/vendor/pretty/src/lib.rs @@ -0,0 +1,1033 @@ +#![allow(clippy::all)] +#[cfg(feature = "termcolor")] +pub extern crate termcolor; + +use std::{borrow::Cow, convert::TryInto, fmt, io, ops::Deref, rc::Rc}; +#[cfg(feature = "termcolor")] +use termcolor::{ColorSpec, WriteColor}; + +mod render; + +#[cfg(feature = "termcolor")] +pub use self::render::TermColored; +pub use self::render::{FmtWrite, IoWrite, Render, RenderAnnotated}; + +/// Macro to help build a text node with format arguments. It is semantically equivalent to `alloc.text(format(...))`, except +/// that it may not allocate a string at all. +#[macro_export] +macro_rules! text { + ($alloc:expr, $($args:tt)*) => { + $alloc.as_string(format_args!($($args)*)) + }; +} + +/// The concrete document type. This type is not meant to be used directly. Instead use the static +/// functions on `Doc` or the methods on an `DocAllocator`. +/// +/// The `T` parameter is used to abstract over pointers to `Doc`. See `RefDoc` and `BoxDoc` for how +/// it is used +#[derive(Clone)] +pub enum Doc<'a, T: DocPtr<'a, A>, A = ()> { + Nil, + Append(T, T), + Group(T), + FlatAlt(T, T), + Nest(isize, T), + Line, + OwnedText(Box), + BorrowedText(&'a str), + SmallText(SmallText), + Annotated(A, T), + Union(T, T), + Column(T::ColumnFn), + Nesting(T::ColumnFn), +} + +pub type SmallText = arrayvec::ArrayString<22>; + +impl<'a, T, A> fmt::Debug for Doc<'a, T, A> +where + T: DocPtr<'a, A> + fmt::Debug, + A: fmt::Debug, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Doc::Nil => f.debug_tuple("Nil").finish(), + Doc::Append(ref ldoc, ref rdoc) => { + f.debug_tuple("Append").field(ldoc).field(rdoc).finish() + } + Doc::FlatAlt(ref x, ref y) => f.debug_tuple("FlatAlt").field(x).field(y).finish(), + Doc::Group(ref doc) => f.debug_tuple("Group").field(doc).finish(), + Doc::Nest(off, ref doc) => f.debug_tuple("Nest").field(&off).field(doc).finish(), + Doc::Line => f.debug_tuple("Line").finish(), + Doc::OwnedText(ref s) => f.debug_tuple("Text").field(s).finish(), + Doc::BorrowedText(ref s) => f.debug_tuple("Text").field(s).finish(), + Doc::SmallText(ref s) => f.debug_tuple("Text").field(s).finish(), + Doc::Annotated(ref ann, ref doc) => { + f.debug_tuple("Annotated").field(ann).field(doc).finish() + } + Doc::Union(ref l, ref r) => f.debug_tuple("Union").field(l).field(r).finish(), + Doc::Column(_) => f.debug_tuple("Column(..)").finish(), + Doc::Nesting(_) => f.debug_tuple("Nesting(..)").finish(), + } + } +} + +macro_rules! impl_doc { + ($name: ident, $allocator: ident) => { + #[derive(Clone)] + pub struct $name<'a, A = ()>(Box, A>>); + + impl<'a, A> fmt::Debug for $name<'a, A> + where + A: fmt::Debug, + { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.fmt(f) + } + } + + impl<'a, A> $name<'a, A> { + pub fn new(doc: Doc<'a, $name<'a, A>, A>) -> $name<'a, A> { + $name(Box::new(doc)) + } + } + + impl<'a, A> From> for $name<'a, A> { + fn from(doc: Doc<'a, $name<'a, A>, A>) -> $name<'a, A> { + $name::new(doc) + } + } + + impl<'a, A> Deref for $name<'a, A> { + type Target = Doc<'a, $name<'a, A>, A>; + + fn deref(&self) -> &Self::Target { + &self.0 + } + } + + impl<'a, A> DocAllocator<'a, A> for $allocator + where + A: 'a, + { + type Doc = $name<'a, A>; + + #[inline] + fn alloc(&'a self, doc: Doc<'a, Self::Doc, A>) -> Self::Doc { + $name::new(doc) + } + fn alloc_column_fn( + &'a self, + f: impl Fn(usize) -> Self::Doc + 'a, + ) -> >::ColumnFn { + Rc::new(f) + } + fn alloc_width_fn( + &'a self, + f: impl Fn(isize) -> Self::Doc + 'a, + ) -> >::WidthFn { + Rc::new(f) + } + } + + impl<'a, A> DocPtr<'a, A> for $name<'a, A> { + type ColumnFn = std::rc::Rc Self + 'a>; + type WidthFn = std::rc::Rc Self + 'a>; + } + + impl<'a, A> StaticDoc<'a, A> for $name<'a, A> { + type Allocator = $allocator; + const ALLOCATOR: &'static Self::Allocator = &$allocator; + } + + impl_doc_methods!($name ('a, A) where () where ()); + + impl<'a, A> $name<'a, A> { + /// Append the given document after this document. + #[inline] + pub fn append(self, that: D) -> Self + where + D: Into>, + { + DocBuilder(&$allocator, self.into()).append(that).into_doc() + } + + /// A single document concatenating all the given documents. + #[inline] + pub fn concat(docs: I) -> Self + where + I: IntoIterator, + I::Item: Into>, + { + $allocator.concat(docs).into_doc() + } + + /// A single document interspersing the given separator `S` between the given documents. For + /// example, if the documents are `[A, B, C, ..., Z]`, this yields `[A, S, B, S, C, S, ..., S, Z]`. + /// + /// Compare [the `intersperse` method from the `itertools` crate](https://docs.rs/itertools/0.5.9/itertools/trait.Itertools.html#method.intersperse). + /// + /// NOTE: The separator type, `S` may need to be cloned. Consider using cheaply cloneable ptr + /// like `RefDoc` or `RcDoc` + #[inline] + pub fn intersperse(docs: I, separator: S) -> Self + where + I: IntoIterator, + I::Item: Into>, + S: Into> + Clone, + A: Clone, + { + $allocator.intersperse(docs, separator).into_doc() + } + + /// Acts as `self` when laid out on multiple lines and acts as `that` when laid out on a single line. + #[inline] + pub fn flat_alt(self, doc: D) -> Self + where + D: Into>, + { + DocBuilder(&$allocator, self.into()) + .flat_alt(doc) + .into_doc() + } + + /// Mark this document as a group. + /// + /// Groups are laid out on a single line if possible. Within a group, all basic documents with + /// several possible layouts are assigned the same layout, that is, they are all laid out + /// horizontally and combined into a one single line, or they are each laid out on their own + /// line. + #[inline] + pub fn group(self) -> Self { + DocBuilder(&$allocator, self.into()).group().into_doc() + } + + /// Increase the indentation level of this document. + #[inline] + pub fn nest(self, offset: isize) -> Self { + DocBuilder(&$allocator, self.into()).nest(offset).into_doc() + } + + #[inline] + pub fn annotate(self, ann: A) -> Self { + DocBuilder(&$allocator, self.into()) + .annotate(ann) + .into_doc() + } + + #[inline] + pub fn union(self, other: D) -> Self + where + D: Into>, + { + DocBuilder(&$allocator, self.into()).union(other).into_doc() + } + } + }; +} + +enum FmtText { + Small(SmallText), + Large(String), +} + +impl fmt::Write for FmtText { + fn write_str(&mut self, s: &str) -> fmt::Result { + match self { + FmtText::Small(buf) => { + if let Err(_) = buf.try_push_str(s) { + let mut new_str = String::with_capacity(buf.len() + s.len()); + new_str.push_str(buf); + new_str.push_str(s); + *self = FmtText::Large(new_str); + } + } + FmtText::Large(buf) => buf.push_str(s), + } + Ok(()) + } +} + +macro_rules! impl_doc_methods { + ($name: ident ( $($params: tt)* ) where ( $($where_: tt)* ) where ( $($where_2: tt)* )) => { + impl< $($params)* > $name< $($params)* > + where $($where_)* + { + /// An empty document. + #[inline] + pub fn nil() -> Self { + Doc::Nil.into() + } + + /// The text `t.to_string()`. + /// + /// The given text must not contain line breaks. + #[inline] + pub fn as_string(data: U) -> Self { + use std::fmt::Write; + let mut buf = FmtText::Small(SmallText::new()); + write!(buf, "{}", data).unwrap(); + (match buf { + FmtText::Small(b) => Doc::SmallText(b), + FmtText::Large(b) => Doc::OwnedText(b.into()), + }).into() + } + + /// A single hardline. + #[inline] + pub fn hardline() -> Self { + Doc::Line.into() + } + + /// The given text, which must not contain line breaks. + #[inline] + pub fn text>>(data: U) -> Self { + match data.into() { + Cow::Owned(t) => Doc::OwnedText(t.into()).into(), + Cow::Borrowed(t) => Doc::BorrowedText(t).into(), + } + } + + #[inline] + pub fn space() -> Self { + Doc::BorrowedText(" ").into() + } + } + + impl< $($params)* > $name< $($params)* > + where $($where_2)* + { + /// A line acts like a `\n` but behaves like `space` if it is grouped on a single line. + #[inline] + pub fn line() -> Self { + Self::hardline().flat_alt(Self::space()).into() + } + + /// Acts like `line` but behaves like `nil` if grouped on a single line + #[inline] + pub fn line_() -> Self { + Self::hardline().flat_alt(Self::nil()).into() + } + } + }; +} + +impl_doc!(BoxDoc, BoxAllocator); +impl_doc!(RcDoc, RcAllocator); + +impl_doc_methods!(Doc ('a, D, A) where (D: DocPtr<'a, A>) where (D: StaticDoc<'a, A>)); +impl_doc_methods!(BuildDoc ('a, D, A) where (D: DocPtr<'a, A>) where (D: StaticDoc<'a, A>)); + +pub struct BoxAllocator; + +pub struct RcAllocator; + +impl<'a, T, A> BuildDoc<'a, T, A> +where + T: StaticDoc<'a, A>, +{ + fn flat_alt(self, doc: D) -> Self + where + D: Into>, + { + DocBuilder(&T::ALLOCATOR, self.into()).flat_alt(doc).1 + } +} + +impl<'a, T, A> Doc<'a, T, A> +where + T: StaticDoc<'a, A>, +{ + fn flat_alt(self, doc: D) -> Self + where + D: Into>, + { + DocBuilder(&T::ALLOCATOR, self.into()) + .flat_alt(doc) + .into_plain_doc() + } +} + +pub trait StaticDoc<'a, A>: DocPtr<'a, A> +where + A: 'a, +{ + type Allocator: DocAllocator<'a, A, Doc = Self> + 'static; + const ALLOCATOR: &'static Self::Allocator; +} + +impl<'a, T, A, S> From for Doc<'a, T, A> +where + T: DocPtr<'a, A>, + S: Into>, +{ + fn from(s: S) -> Doc<'a, T, A> { + Doc::text(s) + } +} + +pub struct Pretty<'a, 'd, T, A> +where + A: 'a, + T: DocPtr<'a, A> + 'a, +{ + doc: &'d Doc<'a, T, A>, + width: usize, +} + +impl<'a, T, A> fmt::Display for Pretty<'a, '_, T, A> +where + T: DocPtr<'a, A>, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.doc.render_fmt(self.width, f) + } +} + +impl<'a, T, A> Doc<'a, T, A> +where + T: DocPtr<'a, A> + 'a, +{ + /// Writes a rendered document to a `std::io::Write` object. + #[inline] + pub fn render(&self, width: usize, out: &mut W) -> io::Result<()> + where + W: ?Sized + io::Write, + { + self.render_raw(width, &mut IoWrite::new(out)) + } + + /// Writes a rendered document to a `std::fmt::Write` object. + #[inline] + pub fn render_fmt(&self, width: usize, out: &mut W) -> fmt::Result + where + W: ?Sized + fmt::Write, + { + self.render_raw(width, &mut FmtWrite::new(out)) + } + + /// Writes a rendered document to a `RenderAnnotated` object. + #[inline] + pub fn render_raw(&self, width: usize, out: &mut W) -> Result<(), W::Error> + where + W: ?Sized + render::RenderAnnotated, + { + render::best(self, width, out) + } + + /// Returns a value which implements `std::fmt::Display` + /// + #[inline] + pub fn pretty<'d>(&'d self, width: usize) -> Pretty<'a, 'd, T, A> { + Pretty { doc: self, width } + } +} + +#[cfg(feature = "termcolor")] +impl<'a, T> Doc<'a, T, ColorSpec> +where + T: DocPtr<'a, ColorSpec> + 'a, +{ + #[inline] + pub fn render_colored(&self, width: usize, out: W) -> io::Result<()> + where + W: WriteColor, + { + render::best(self, width, &mut TermColored::new(out)) + } +} + +/// The `DocBuilder` type allows for convenient appending of documents even for arena allocated +/// documents by storing the arena inline. +pub struct DocBuilder<'a, D, A = ()>(pub &'a D, pub BuildDoc<'a, D::Doc, A>) +where + D: ?Sized + DocAllocator<'a, A>; + +impl<'a, A, D> Clone for DocBuilder<'a, D, A> +where + A: Clone, + D: DocAllocator<'a, A> + 'a, + D::Doc: Clone, +{ + fn clone(&self) -> Self { + DocBuilder(self.0, self.1.clone()) + } +} + +impl<'a, D, A> Into> for DocBuilder<'a, D, A> +where + D: ?Sized + DocAllocator<'a, A>, +{ + fn into(self) -> BuildDoc<'a, D::Doc, A> { + self.1 + } +} + +pub trait DocPtr<'a, A>: Deref> + Sized +where + A: 'a, +{ + type ColumnFn: Deref Self + 'a> + Clone + 'a; + type WidthFn: Deref Self + 'a> + Clone + 'a; +} + +impl<'a, A> DocPtr<'a, A> for RefDoc<'a, A> { + type ColumnFn = &'a (dyn Fn(usize) -> Self + 'a); + type WidthFn = &'a (dyn Fn(isize) -> Self + 'a); +} + +/// The `DocAllocator` trait abstracts over a type which can allocate (pointers to) `Doc`. +pub trait DocAllocator<'a, A = ()> +where + A: 'a, +{ + type Doc: DocPtr<'a, A>; + + fn alloc(&'a self, doc: Doc<'a, Self::Doc, A>) -> Self::Doc; + + fn alloc_column_fn( + &'a self, + f: impl Fn(usize) -> Self::Doc + 'a, + ) -> >::ColumnFn; + + fn alloc_width_fn( + &'a self, + f: impl Fn(isize) -> Self::Doc + 'a, + ) -> >::WidthFn; + + fn alloc_cow(&'a self, doc: BuildDoc<'a, Self::Doc, A>) -> Self::Doc { + match doc { + BuildDoc::DocPtr(d) => d, + BuildDoc::Doc(d) => self.alloc(d), + } + } + + /// Allocate an empty document. + #[inline] + fn nil(&'a self) -> DocBuilder<'a, Self, A> { + DocBuilder(self, Doc::Nil.into()) + } + + /// Allocate a single hardline. + #[inline] + fn hardline(&'a self) -> DocBuilder<'a, Self, A> { + DocBuilder(self, Doc::Line.into()) + } + + #[inline] + fn space(&'a self) -> DocBuilder<'a, Self, A> { + self.text(" ") + } + + /// A line acts like a `\n` but behaves like `space` if it is grouped on a single line. + #[inline] + fn line(&'a self) -> DocBuilder<'a, Self, A> { + self.hardline().flat_alt(self.space()) + } + + /// Acts like `line` but behaves like `nil` if grouped on a single line + /// + #[inline] + fn line_(&'a self) -> DocBuilder<'a, Self, A> { + self.hardline().flat_alt(self.nil()) + } + + /// A `softline` acts like `space` if the document fits the page, otherwise like `line` + #[inline] + fn softline(&'a self) -> DocBuilder<'a, Self, A> { + self.line().group() + } + + /// A `softline_` acts like `nil` if the document fits the page, otherwise like `line_` + #[inline] + fn softline_(&'a self) -> DocBuilder<'a, Self, A> { + self.line_().group() + } + + /// Allocate a document containing the text `t.to_string()`. + /// + /// The given text must not contain line breaks. + #[inline] + fn as_string(&'a self, data: U) -> DocBuilder<'a, Self, A> { + DocBuilder(self, Doc::as_string(data.to_string()).into()) + } + + /// Allocate a document containing the given text. + /// + /// The given text must not contain line breaks. + #[inline] + fn text>>(&'a self, data: U) -> DocBuilder<'a, Self, A> { + DocBuilder(self, Doc::text(data).into()) + } + + /// Allocate a document concatenating the given documents. + #[inline] + fn concat(&'a self, docs: I) -> DocBuilder<'a, Self, A> + where + I: IntoIterator, + I::Item: Into>, + { + docs.into_iter().fold(self.nil(), |a, b| a.append(b)) + } + + /// Allocate a document that intersperses the given separator `S` between the given documents + /// `[A, B, C, ..., Z]`, yielding `[A, S, B, S, C, S, ..., S, Z]`. + /// + /// Compare [the `intersperse` method from the `itertools` crate](https://docs.rs/itertools/0.5.9/itertools/trait.Itertools.html#method.intersperse). + /// + /// NOTE: The separator type, `S` may need to be cloned. Consider using cheaply cloneable ptr + /// like `RefDoc` or `RcDoc` + #[inline] + fn intersperse(&'a self, docs: I, separator: S) -> DocBuilder<'a, Self, A> + where + I: IntoIterator, + I::Item: Into>, + S: Into> + Clone, + { + let mut result = self.nil(); + let mut iter = docs.into_iter(); + + if let Some(first) = iter.next() { + result = result.append(first); + + for doc in iter { + result = result.append(separator.clone()); + result = result.append(doc); + } + } + + result + } + + /// Allocate a document that acts differently based on the position and page layout + /// + #[inline] + fn column(&'a self, f: impl Fn(usize) -> Self::Doc + 'a) -> DocBuilder<'a, Self, A> { + DocBuilder(self, Doc::Column(self.alloc_column_fn(f)).into()) + } + + /// Allocate a document that acts differently based on the current nesting level + /// + #[inline] + fn nesting(&'a self, f: impl Fn(usize) -> Self::Doc + 'a) -> DocBuilder<'a, Self, A> { + DocBuilder(self, Doc::Nesting(self.alloc_column_fn(f)).into()) + } + + /// Reflows `text` inserting `softline` in place of any whitespace + #[inline] + fn reflow(&'a self, text: &'a str) -> DocBuilder<'a, Self, A> + where + Self: Sized, + Self::Doc: Clone, + A: Clone, + { + self.intersperse(text.split(char::is_whitespace), self.line().group()) + } +} + +/// Either a `Doc` or a pointer to a `Doc` (`D`) +#[derive(Clone, Debug)] +pub enum BuildDoc<'a, D, A> +where + D: DocPtr<'a, A>, +{ + DocPtr(D), + Doc(Doc<'a, D, A>), +} + +impl<'a, D, A> Deref for BuildDoc<'a, D, A> +where + D: DocPtr<'a, A>, +{ + type Target = Doc<'a, D, A>; + fn deref(&self) -> &Self::Target { + match self { + BuildDoc::DocPtr(d) => d, + BuildDoc::Doc(d) => d, + } + } +} + +impl<'a, A> From> for BuildDoc<'a, RefDoc<'a, A>, A> { + fn from(s: RefDoc<'a, A>) -> Self { + BuildDoc::DocPtr(s) + } +} + +impl<'a, A> From> for BuildDoc<'a, BoxDoc<'a, A>, A> { + fn from(s: BoxDoc<'a, A>) -> Self { + BuildDoc::DocPtr(s) + } +} + +impl<'a, A> From> for BuildDoc<'a, RcDoc<'a, A>, A> { + fn from(s: RcDoc<'a, A>) -> Self { + BuildDoc::DocPtr(s) + } +} + +impl<'a, T, A> From> for BuildDoc<'a, T, A> +where + T: DocPtr<'a, A>, +{ + fn from(s: Doc<'a, T, A>) -> Self { + BuildDoc::Doc(s) + } +} + +impl<'a, T, A, S> From for BuildDoc<'a, T, A> +where + T: DocPtr<'a, A>, + S: Into>, +{ + fn from(s: S) -> Self { + BuildDoc::Doc(Doc::text(s)) + } +} + +impl<'a, 's, D, A> DocBuilder<'a, D, A> +where + D: ?Sized + DocAllocator<'a, A>, +{ + /// Append the given document after this document. + #[inline] + pub fn append(self, that: E) -> DocBuilder<'a, D, A> + where + E: Into>, + { + let DocBuilder(allocator, this) = self; + let that = that.into(); + let doc = match (&*this, &*that) { + (Doc::Nil, _) => that, + (_, Doc::Nil) => this, + _ => Doc::Append(allocator.alloc_cow(this), allocator.alloc_cow(that)).into(), + }; + DocBuilder(allocator, doc) + } + + /// Acts as `self` when laid out on multiple lines and acts as `that` when laid out on a single line. + /// + #[inline] + pub fn flat_alt(self, that: E) -> DocBuilder<'a, D, A> + where + E: Into>, + { + let DocBuilder(allocator, this) = self; + let that = that.into(); + DocBuilder( + allocator, + Doc::FlatAlt(allocator.alloc_cow(this.into()), allocator.alloc_cow(that)).into(), + ) + } + + /// Mark this document as a group. + /// + /// Groups are laid out on a single line if possible. Within a group, all basic documents with + /// several possible layouts are assigned the same layout, that is, they are all laid out + /// horizontally and combined into a one single line, or they are each laid out on their own + /// line. + #[inline] + pub fn group(self) -> DocBuilder<'a, D, A> { + let DocBuilder(allocator, this) = self; + DocBuilder(allocator, Doc::Group(allocator.alloc_cow(this)).into()) + } + + /// Increase the indentation level of this document. + #[inline] + pub fn nest(self, offset: isize) -> DocBuilder<'a, D, A> { + if let Doc::Nil = &*self.1 { + return self; + } + if offset == 0 { + return self; + } + let DocBuilder(allocator, this) = self; + DocBuilder( + allocator, + Doc::Nest(offset, allocator.alloc_cow(this)).into(), + ) + } + + #[inline] + pub fn annotate(self, ann: A) -> DocBuilder<'a, D, A> { + let DocBuilder(allocator, this) = self; + DocBuilder( + allocator, + Doc::Annotated(ann, allocator.alloc_cow(this)).into(), + ) + } + + #[inline] + pub fn union(self, other: E) -> DocBuilder<'a, D, A> + where + E: Into>, + { + let DocBuilder(allocator, this) = self; + let other = other.into(); + let doc = Doc::Union(allocator.alloc_cow(this), allocator.alloc_cow(other)); + DocBuilder(allocator, doc.into()) + } + + /// Lays out `self` so with the nesting level set to the current column + /// + /// NOTE: The doc pointer type, `D` may need to be cloned. Consider using cheaply cloneable ptr + /// like `RefDoc` or `RcDoc` + /// + #[inline] + pub fn align(self) -> DocBuilder<'a, D, A> + where + DocBuilder<'a, D, A>: Clone, + { + let allocator = self.0; + allocator.column(move |col| { + let self_ = self.clone(); + allocator + .nesting(move |nest| self_.clone().nest(col as isize - nest as isize).into_doc()) + .into_doc() + }) + } + + /// Lays out `self` with a nesting level set to the current level plus `adjust`. + /// + /// NOTE: The doc pointer type, `D` may need to be cloned. Consider using cheaply cloneable ptr + /// like `RefDoc` or `RcDoc` + /// + #[inline] + pub fn hang(self, adjust: isize) -> DocBuilder<'a, D, A> + where + DocBuilder<'a, D, A>: Clone, + { + self.nest(adjust).align() + } + + /// Indents `self` by `adjust` spaces from the current cursor position + /// + /// NOTE: The doc pointer type, `D` may need to be cloned. Consider using cheaply cloneable ptr + /// like `RefDoc` or `RcDoc` + /// + #[inline] + pub fn indent(self, adjust: usize) -> DocBuilder<'a, D, A> + where + DocBuilder<'a, D, A>: Clone, + { + let spaces = { + use crate::render::SPACES; + let DocBuilder(allocator, _) = self; + let mut doc = allocator.nil(); + let mut remaining = adjust; + while remaining != 0 { + let i = SPACES.len().min(remaining); + remaining -= i; + doc = doc.append(allocator.text(&SPACES[..i])) + } + doc + }; + spaces.append(self).hang(adjust.try_into().unwrap()) + } + + /// Lays out `self` and provides the column width of it available to `f` + /// + /// NOTE: The doc pointer type, `D` may need to be cloned. Consider using cheaply cloneable ptr + /// like `RefDoc` or `RcDoc` + /// + #[inline] + pub fn width(self, f: impl Fn(isize) -> D::Doc + 'a) -> DocBuilder<'a, D, A> + where + BuildDoc<'a, D::Doc, A>: Clone, + { + let DocBuilder(allocator, this) = self; + let f = allocator.alloc_width_fn(f); + allocator.column(move |start| { + let f = f.clone(); + + DocBuilder(allocator, this.clone()) + .append(allocator.column(move |end| f(end as isize - start as isize))) + .into_doc() + }) + } + + /// Puts `self` between `before` and `after` + #[inline] + pub fn enclose(self, before: E, after: F) -> DocBuilder<'a, D, A> + where + E: Into>, + F: Into>, + { + let DocBuilder(allocator, _) = self; + DocBuilder(allocator, before.into()) + .append(self) + .append(after) + } + + pub fn single_quotes(self) -> DocBuilder<'a, D, A> { + self.enclose("'", "'") + } + + pub fn double_quotes(self) -> DocBuilder<'a, D, A> { + self.enclose("\"", "\"") + } + pub fn parens(self) -> DocBuilder<'a, D, A> { + self.enclose("(", ")") + } + + pub fn angles(self) -> DocBuilder<'a, D, A> { + self.enclose("<", ">") + } + pub fn braces(self) -> DocBuilder<'a, D, A> { + self.enclose("{", "}") + } + + pub fn brackets(self) -> DocBuilder<'a, D, A> { + self.enclose("[", "]") + } + + pub fn into_doc(self) -> D::Doc { + match self.1 { + BuildDoc::DocPtr(d) => d, + BuildDoc::Doc(d) => self.0.alloc(d), + } + } + + fn into_plain_doc(self) -> Doc<'a, D::Doc, A> { + match self.1 { + BuildDoc::DocPtr(_) => unreachable!(), + BuildDoc::Doc(d) => d, + } + } +} + +/// Newtype wrapper for `&Doc` +pub struct RefDoc<'a, A = ()>(pub &'a Doc<'a, RefDoc<'a, A>, A>); + +impl Copy for RefDoc<'_, A> {} +impl Clone for RefDoc<'_, A> { + fn clone(&self) -> Self { + *self + } +} + +impl<'a, A> fmt::Debug for RefDoc<'a, A> +where + A: fmt::Debug, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.fmt(f) + } +} + +impl<'a, A> Deref for RefDoc<'a, A> { + type Target = Doc<'a, RefDoc<'a, A>, A>; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +trait DropT {} +impl DropT for T {} + +/// An arena which can be used to allocate `Doc` values. +pub struct Arena<'a, A = ()> { + docs: typed_arena::Arena, A>>, + column_fns: typed_arena::Arena>, +} + +impl Default for Arena<'_, A> { + fn default() -> Self { + Self::new() + } +} + +impl<'a, A> Arena<'a, A> { + pub fn new() -> Self { + Arena { + docs: typed_arena::Arena::new(), + column_fns: Default::default(), + } + } + + fn alloc_any(&'a self, f: T) -> &'a T + where + T: 'a, + { + let f = Box::new(f); + let f_ptr = &*f as *const T; + // Until #[may_dangle] https://github.com/rust-lang/rust/issues/34761 is stabilized (or + // equivalent) we need to use unsafe to cast away the lifetime of the function as we do not + // have any other way of asserting that the `typed_arena::Arena` destructor does not touch + // `'a` + // + // Since `'a` is used elsewhere in our `Arena` type we still have all the other lifetime + // checks in place (the other arena stores no `Drop` value which touches `'a` which lets it + // compile) + unsafe { + self.column_fns + .alloc(std::mem::transmute::, Box>(f)); + &*f_ptr + } + } +} + +impl<'a, D, A> DocAllocator<'a, A> for &'a D +where + D: ?Sized + DocAllocator<'a, A>, + A: 'a, +{ + type Doc = D::Doc; + + #[inline] + fn alloc(&'a self, doc: Doc<'a, Self::Doc, A>) -> Self::Doc { + (**self).alloc(doc) + } + + fn alloc_column_fn( + &'a self, + f: impl Fn(usize) -> Self::Doc + 'a, + ) -> >::ColumnFn { + (**self).alloc_column_fn(f) + } + + fn alloc_width_fn( + &'a self, + f: impl Fn(isize) -> Self::Doc + 'a, + ) -> >::WidthFn { + (**self).alloc_width_fn(f) + } +} + +impl<'a, A> DocAllocator<'a, A> for Arena<'a, A> { + type Doc = RefDoc<'a, A>; + + #[inline] + fn alloc(&'a self, doc: Doc<'a, Self::Doc, A>) -> Self::Doc { + RefDoc(match doc { + // Return 'static references for common variants to avoid some allocations + Doc::Nil => &Doc::Nil, + Doc::Line => &Doc::Line, + // line() + Doc::FlatAlt(RefDoc(Doc::Line), RefDoc(Doc::BorrowedText(" "))) => { + &Doc::FlatAlt(RefDoc(&Doc::Line), RefDoc(&Doc::BorrowedText(" "))) + } + // line_() + Doc::FlatAlt(RefDoc(Doc::Line), RefDoc(Doc::Nil)) => { + &Doc::FlatAlt(RefDoc(&Doc::Line), RefDoc(&Doc::Nil)) + } + _ => self.docs.alloc(doc), + }) + } + + fn alloc_column_fn( + &'a self, + f: impl Fn(usize) -> Self::Doc + 'a, + ) -> >::ColumnFn { + self.alloc_any(f) + } + + fn alloc_width_fn( + &'a self, + f: impl Fn(isize) -> Self::Doc + 'a, + ) -> >::WidthFn { + self.alloc_any(f) + } +} diff --git a/vendor/pretty/src/render.rs b/crates/vendor/pretty/src/render.rs similarity index 100% rename from vendor/pretty/src/render.rs rename to crates/vendor/pretty/src/render.rs diff --git a/crates/wasi-libc-sys/Cargo.toml b/crates/wasi-libc-sys/Cargo.toml new file mode 100644 index 0000000000..7bc52198fc --- /dev/null +++ b/crates/wasi-libc-sys/Cargo.toml @@ -0,0 +1,12 @@ +[package] +description = "Rust wrapper for a WebAssembly test platform built on libc" +name = "wasi_libc_sys" + +authors.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true +version.workspace = true + +[build-dependencies] +roc_command_utils = { path = "../utils/command" } diff --git a/crates/wasi-libc-sys/build.rs b/crates/wasi-libc-sys/build.rs new file mode 100644 index 0000000000..838a5dc2d0 --- /dev/null +++ b/crates/wasi-libc-sys/build.rs @@ -0,0 +1,68 @@ +use roc_command_utils::zig; +use std::env; +use std::ffi::OsString; +use std::fs; +use std::path::{Path, PathBuf}; + +fn main() { + println!("cargo:rerun-if-changed=build.rs"); + println!("cargo:rerun-if-changed=src/dummy.c"); + + let out_dir = env::var("OUT_DIR").unwrap(); + let zig_cache_dir = PathBuf::from(&out_dir).join("zig-cache"); + let out_file = PathBuf::from(&out_dir).join("wasi-libc.a"); + + // Compile a dummy C program with Zig, with our own private cache directory + zig() + .args([ + "build-exe", + "-target", + "wasm32-wasi", + "-lc", + "-O", + "ReleaseSmall", + "--global-cache-dir", + zig_cache_dir.to_str().unwrap(), + "src/dummy.c", + &format!("-femit-bin={out_dir}/dummy.wasm"), + ]) + .output() + .unwrap(); + + let libc_path = find(&zig_cache_dir, &OsString::from("libc.a")) + .unwrap() + .unwrap(); + + let compiler_rt_path = find(&zig_cache_dir, &OsString::from("libcompiler_rt.a")) + .unwrap() + .unwrap(); + + // Copy libc to where Cargo expects the output of this crate + fs::copy(libc_path, &out_file).unwrap(); + + println!( + "cargo:rustc-env=WASI_LIBC_PATH={}", + out_file.to_str().unwrap() + ); + + println!( + "cargo:rustc-env=WASI_COMPILER_RT_PATH={}", + compiler_rt_path.to_str().unwrap() + ); +} + +fn find(dir: &Path, filename: &OsString) -> std::io::Result> { + for entry in fs::read_dir(dir)? { + let entry = entry?; + let path = entry.path(); + if path.is_dir() { + let found = find(&path, filename)?; + if found.is_some() { + return Ok(found); + } + } else if &entry.file_name() == filename { + return Ok(Some(path)); + } + } + Ok(None) +} diff --git a/wasi-libc-sys/src/dummy.c b/crates/wasi-libc-sys/src/dummy.c similarity index 100% rename from wasi-libc-sys/src/dummy.c rename to crates/wasi-libc-sys/src/dummy.c diff --git a/crates/wasi-libc-sys/src/lib.rs b/crates/wasi-libc-sys/src/lib.rs new file mode 100644 index 0000000000..fb076d282a --- /dev/null +++ b/crates/wasi-libc-sys/src/lib.rs @@ -0,0 +1,23 @@ +//! Provides a Rust wrapper for the WebAssembly libc, which is used when +//! preprocessing hosts for use with the Wasm development back-end, and for the +//! mock hosts we use in our Wasm tests. +// Rust's libc crate doesn't support Wasm, so we provide an implementation from Zig +// We define Rust signatures here as we need them, rather than trying to cover all of libc +#[cfg(target_family = "wasm")] +use core::ffi::c_void; +#[cfg(target_family = "wasm")] +extern "C" { + pub fn malloc(size: usize) -> *mut c_void; + pub fn free(p: *mut c_void); + pub fn realloc(p: *mut c_void, size: usize) -> *mut c_void; + pub fn memcpy(dst: *mut c_void, src: *mut c_void, n: usize) -> *mut c_void; + pub fn memset(dst: *mut c_void, ch: i32, n: usize) -> *mut c_void; +} + +// Tell users of this crate where to find the Wasm .a file +// If a non-Wasm target is using this crate, we assume it is a build script that wants to emit Wasm +// For Wasm target, it won't ever be used, but we expose it just to keep things simple + +// these variables are set by build.rs +pub const WASI_LIBC_PATH: &str = env!("WASI_LIBC_PATH"); +pub const WASI_COMPILER_RT_PATH: &str = env!("WASI_COMPILER_RT_PATH"); diff --git a/crates/wasm_interp/Cargo.toml b/crates/wasm_interp/Cargo.toml new file mode 100644 index 0000000000..c4a6c84dd5 --- /dev/null +++ b/crates/wasm_interp/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "roc_wasm_interp" +description = "A WebAssembly interpreter for testing the compiler." + +authors.workspace = true +edition.workspace = true +license.workspace = true +version.workspace = true + +[[bin]] +name = "roc_wasm_interp" +path = "src/main.rs" + +[dependencies] +roc_wasm_module = { path = "../wasm_module" } + +bitvec.workspace = true +bumpalo.workspace = true +clap.workspace = true +rand.workspace = true diff --git a/crates/wasm_interp/src/frame.rs b/crates/wasm_interp/src/frame.rs new file mode 100644 index 0000000000..57f48bd580 --- /dev/null +++ b/crates/wasm_interp/src/frame.rs @@ -0,0 +1,82 @@ +use roc_wasm_module::{parse::Parse, Value, ValueType}; +use std::iter::repeat; + +use crate::value_store::ValueStore; + +#[derive(Debug)] +pub struct Frame { + /// The function this frame belongs to + pub fn_index: usize, + /// Address in the code section where this frame returns to + pub return_addr: usize, + /// Depth of the "function body block" for this frame + pub body_block_index: usize, + /// Offset in the ValueStore where the args & locals begin + pub locals_start: usize, + /// Number of args & locals in the frame + pub locals_count: usize, + /// Expected return type, if any + pub return_type: Option, +} + +impl Frame { + pub fn new() -> Self { + Frame { + fn_index: 0, + return_addr: 0, + body_block_index: 0, + locals_start: 0, + locals_count: 0, + return_type: None, + } + } + + #[allow(clippy::too_many_arguments)] + pub fn enter( + fn_index: usize, + return_addr: usize, + body_block_index: usize, + n_args: usize, + return_type: Option, + code_bytes: &[u8], + value_store: &mut ValueStore<'_>, + pc: &mut usize, + ) -> Self { + let locals_start = value_store.depth() - n_args; + + // Parse local variable declarations in the function header. They're grouped by type. + let local_group_count = u32::parse((), code_bytes, pc).unwrap(); + for _ in 0..local_group_count { + let (group_size, ty) = <(u32, ValueType)>::parse((), code_bytes, pc).unwrap(); + let n = group_size as usize; + let zero = match ty { + ValueType::I32 => Value::I32(0), + ValueType::I64 => Value::I64(0), + ValueType::F32 => Value::F32(0.0), + ValueType::F64 => Value::F64(0.0), + }; + value_store.extend(repeat(zero).take(n)); + } + + let locals_count = value_store.depth() - locals_start; + + Frame { + fn_index, + return_addr, + body_block_index, + locals_start, + locals_count, + return_type, + } + } + + pub fn get_local(&self, values: &ValueStore<'_>, index: u32) -> Value { + debug_assert!((index as usize) < self.locals_count); + *values.get(self.locals_start + index as usize).unwrap() + } + + pub fn set_local(&self, values: &mut ValueStore<'_>, index: u32, value: Value) { + debug_assert!((index as usize) < self.locals_count); + values.set(self.locals_start + index as usize, value) + } +} diff --git a/crates/wasm_interp/src/instance.rs b/crates/wasm_interp/src/instance.rs new file mode 100644 index 0000000000..0c094dcb9f --- /dev/null +++ b/crates/wasm_interp/src/instance.rs @@ -0,0 +1,1915 @@ +use bumpalo::{collections::Vec, Bump}; +use std::fmt::{self, Write}; +use std::iter::{self, once, Iterator}; + +use roc_wasm_module::opcodes::{MemoryInstruction, OpCode}; +use roc_wasm_module::parse::{Parse, SkipBytes}; +use roc_wasm_module::sections::{ImportDesc, MemorySection, SignatureParamsIter}; +use roc_wasm_module::{ExportType, WasmModule}; +use roc_wasm_module::{Value, ValueType}; + +use crate::frame::Frame; +use crate::value_store::ValueStore; +use crate::{Error, ImportDispatcher}; + +#[derive(Debug)] +pub enum Action { + Continue, + Break, +} + +#[derive(Debug, Clone, Copy)] +enum BlockType { + Loop(usize), // Loop block, with start address to loop back to + Normal, // Block created by `block` instruction + Locals(usize), // Special "block" for locals. Holds function index for debug + FunctionBody(usize), // Special block surrounding the function body. Holds function index for debug +} + +#[derive(Debug, Clone, Copy)] +struct Block { + ty: BlockType, + vstack: usize, +} + +#[derive(Debug, Clone)] +struct BranchCacheEntry { + addr: u32, + argument: u32, + target: u32, +} + +#[derive(Debug)] +pub struct Instance<'a, I: ImportDispatcher> { + pub(crate) module: &'a WasmModule<'a>, + /// Contents of the WebAssembly instance's memory + pub memory: Vec<'a, u8>, + /// The current call frame + pub(crate) current_frame: Frame, + /// Previous call frames + previous_frames: Vec<'a, Frame>, + /// The WebAssembly stack machine's stack of values + pub(crate) value_store: ValueStore<'a>, + /// Values of any global variables + pub(crate) globals: Vec<'a, Value>, + /// Index in the code section of the current instruction + pub(crate) program_counter: usize, + /// One entry per nested block. For loops, stores the address of the first instruction. + blocks: Vec<'a, Block>, + /// Cache for branching instructions, split into buckets for each function. + branch_cache: Vec<'a, Vec<'a, BranchCacheEntry>>, + /// Number of imports in the module + import_count: usize, + /// Import dispatcher from user code + pub import_dispatcher: I, + /// Temporary storage for import arguments + import_arguments: Vec<'a, Value>, + /// temporary storage for output using the --debug option + debug_string: Option, +} + +impl<'a, I: ImportDispatcher> Instance<'a, I> { + #[cfg(test)] + pub(crate) fn new( + arena: &'a Bump, + memory_pages: u32, + program_counter: usize, + globals: G, + import_dispatcher: I, + ) -> Self + where + G: IntoIterator, + { + let mem_bytes = memory_pages * MemorySection::PAGE_SIZE; + Instance { + module: arena.alloc(WasmModule::new(arena)), + memory: Vec::from_iter_in(iter::repeat(0).take(mem_bytes as usize), arena), + current_frame: Frame::new(), + previous_frames: Vec::new_in(arena), + value_store: ValueStore::new(arena), + globals: Vec::from_iter_in(globals, arena), + program_counter, + blocks: Vec::new_in(arena), + branch_cache: bumpalo::vec![in arena; bumpalo::vec![in arena]], + import_count: 0, + import_dispatcher, + import_arguments: Vec::new_in(arena), + debug_string: Some(String::new()), + } + } + + pub fn from_bytes( + arena: &'a Bump, + module_bytes: &[u8], + import_dispatcher: I, + is_debug_mode: bool, + ) -> Result { + let module = + WasmModule::preload(arena, module_bytes, false).map_err(|e| format!("{e:?}"))?; + Self::for_module(arena, arena.alloc(module), import_dispatcher, is_debug_mode) + } + + pub fn for_module( + arena: &'a Bump, + module: &'a WasmModule<'a>, + import_dispatcher: I, + is_debug_mode: bool, + ) -> Result { + let mem_bytes = module.memory.min_bytes().map_err(|e| { + format!( + "Error parsing Memory section at offset {:#x}:\n{}", + e.offset, e.message + ) + })?; + let mut memory = Vec::from_iter_in(iter::repeat(0).take(mem_bytes as usize), arena); + module.data.load_into(&mut memory)?; + + let globals = module.global.initial_values(arena); + + // We don't handle non-function import types (memories, tables, and globals), + // and it's nice for lookups to assume they're all functions, so let's assert that. + let all_imports_are_functions = module.import.imports.iter().all(|imp| imp.is_function()); + assert!( + all_imports_are_functions, + "This Wasm interpreter doesn't support non-function imports" + ); + + let value_store = ValueStore::new(arena); + + let debug_string = if is_debug_mode { + Some(String::new()) + } else { + None + }; + + let import_count = module.import.imports.len(); + let branch_cache = { + let num_functions = import_count + module.code.function_count as usize; + let empty_caches_iter = iter::repeat(Vec::new_in(arena)).take(num_functions); + Vec::from_iter_in(empty_caches_iter, arena) + }; + + Ok(Instance { + module, + memory, + current_frame: Frame::new(), + previous_frames: Vec::new_in(arena), + value_store, + globals, + program_counter: usize::MAX, + blocks: Vec::new_in(arena), + branch_cache, + import_count, + import_dispatcher, + import_arguments: Vec::new_in(arena), + debug_string, + }) + } + + pub fn call_export(&mut self, fn_name: &str, arg_values: A) -> Result, String> + where + A: IntoIterator, + { + let (fn_index, param_type_iter, ret_type) = + self.call_export_help_before_arg_load(self.module, fn_name)?; + let n_args = param_type_iter.len(); + + for (i, (value, expected_type)) in arg_values.into_iter().zip(param_type_iter).enumerate() { + let actual_type = ValueType::from(value); + if actual_type != expected_type { + return Err(format!( + "Type mismatch on argument {i} of {fn_name}. Expected {expected_type:?} but got {value:?}" + )); + } + self.value_store.push(value); + } + + self.call_export_help_after_arg_load(self.module, fn_index, n_args, ret_type) + } + + pub fn call_export_from_cli( + &mut self, + module: &WasmModule<'a>, + fn_name: &str, + arg_strings: &'a [&'a [u8]], + ) -> Result, String> { + // We have two different mechanisms for handling CLI arguments! + // 1. Basic numbers: + // e.g. `roc_wasm_interp fibonacci 12` + // Below, we check if the called Wasm function takes numeric arguments and, if so, parse them from the CLI. + // This is good for low-level test cases, for example while developing this interpreter. + // 2. WASI: + // POSIX-style array of strings. Much more high-level and complex than the "basic" version. + // The WASI `_start` function itself takes no arguments (its Wasm type signature is `() -> nil`). + // The program uses WASI syscalls to copy strings into Wasm memory and process them. + // But that happens *elsewhere*! Here, `arg_strings` is ignored because `_start` takes no arguments. + + // Implement the "basic numbers" CLI + // Check if the called Wasm function takes numeric arguments, and if so, try to parse them from the CLI. + let (fn_index, param_type_iter, ret_type) = + self.call_export_help_before_arg_load(module, fn_name)?; + let n_args = param_type_iter.len(); + for (value_bytes, value_type) in arg_strings + .iter() + .skip(1) // first string is the .wasm filename + .zip(param_type_iter) + { + use ValueType::*; + let value_str = String::from_utf8_lossy(value_bytes); + let value = match value_type { + I32 => Value::I32(value_str.parse::().map_err(|e| e.to_string())?), + I64 => Value::I64(value_str.parse::().map_err(|e| e.to_string())?), + F32 => Value::F32(value_str.parse::().map_err(|e| e.to_string())?), + F64 => Value::F64(value_str.parse::().map_err(|e| e.to_string())?), + }; + self.value_store.push(value); + } + + self.call_export_help_after_arg_load(module, fn_index, n_args, ret_type) + } + + fn call_export_help_before_arg_load<'m>( + &mut self, + module: &'m WasmModule<'a>, + fn_name: &str, + ) -> Result<(usize, SignatureParamsIter<'m>, Option), String> { + let fn_index = { + let mut export_iter = module.export.exports.iter(); + export_iter + // First look up the name in exports + .find_map(|ex| { + if ex.ty == ExportType::Func && ex.name == fn_name { + Some(ex.index) + } else { + None + } + }) + .or_else(|| { + // Then look it up in the debug info! + // This is non-spec behaviour that Wasm3 seems to implement, + // and that our wasm_linking tests accidentally rely on! + let mut names = module.names.function_names.iter(); + names.find_map( + |(index, name)| { + if *name == fn_name { + Some(*index) + } else { + None + } + }, + ) + }) + .ok_or_else(|| { + format!("I couldn't find a function '{fn_name}' in this WebAssembly module") + })? as usize + }; + + let internal_fn_index = fn_index - self.import_count; + + self.program_counter = { + let mut cursor = module.code.function_offsets[internal_fn_index] as usize; + let _start_fn_byte_length = u32::parse((), &module.code.bytes, &mut cursor); + cursor + }; + + let (param_type_iter, return_type) = { + let signature_index = module.function.signatures[internal_fn_index]; + module.types.look_up(signature_index) + }; + + if self.debug_string.is_some() { + println!( + "Calling export func[{}] '{}' at address {:#x}", + fn_index, + fn_name, + self.program_counter + module.code.section_offset as usize + ); + } + + Ok((fn_index, param_type_iter, return_type)) + } + + fn call_export_help_after_arg_load( + &mut self, + module: &WasmModule<'a>, + fn_index: usize, + n_args: usize, + return_type: Option, + ) -> Result, String> { + self.previous_frames.clear(); + self.blocks.clear(); + self.blocks.push(Block { + ty: BlockType::Locals(fn_index), + vstack: self.value_store.depth(), + }); + self.current_frame = Frame::enter( + fn_index, + 0, // return_addr + self.blocks.len(), + n_args, + return_type, + &module.code.bytes, + &mut self.value_store, + &mut self.program_counter, + ); + self.blocks.push(Block { + ty: BlockType::FunctionBody(fn_index), + vstack: self.value_store.depth(), + }); + + loop { + match self.execute_next_instruction(module) { + Ok(Action::Continue) => {} + Ok(Action::Break) => { + break; + } + Err(e) => { + let file_offset = self.program_counter + module.code.section_offset as usize; + let mut message = e.to_string_at(file_offset); + self.debug_stack_trace(&mut message).unwrap(); + return Err(message); + } + }; + } + + let return_value = if !self.value_store.is_empty() { + Some(self.value_store.pop()) + } else { + None + }; + + Ok(return_value) + } + + fn fetch_immediate_u32(&mut self, module: &WasmModule<'a>) -> u32 { + let x = u32::parse((), &module.code.bytes, &mut self.program_counter).unwrap(); + if let Some(debug_string) = self.debug_string.as_mut() { + write!(debug_string, "{x} ").unwrap(); + } + x + } + + fn do_return(&mut self) -> Action { + // self.debug_values_and_blocks("start do_return"); + + let Frame { + return_addr, + body_block_index, + return_type, + .. + } = self.current_frame; + + // Throw away all locals and values except the return value + let locals_block_index = body_block_index - 1; + let locals_block = &self.blocks[locals_block_index]; + let new_stack_depth = if return_type.is_some() { + self.value_store + .set(locals_block.vstack, self.value_store.peek()); + locals_block.vstack + 1 + } else { + locals_block.vstack + }; + self.value_store.truncate(new_stack_depth); + + // Resume executing at the next instruction in the caller function + let new_block_len = locals_block_index; // don't need a -1 because one is a length and the other is an index! + self.blocks.truncate(new_block_len); + self.program_counter = return_addr; + + // self.debug_values_and_blocks("end do_return"); + + if let Some(caller_frame) = self.previous_frames.pop() { + self.current_frame = caller_frame; + Action::Continue + } else { + // We just popped the stack frame for the entry function. Terminate the program. + Action::Break + } + } + + fn get_load_address(&mut self, module: &WasmModule<'a>) -> Result { + // Alignment is not used in the execution steps from the spec! Maybe it's just an optimization hint? + // https://webassembly.github.io/spec/core/exec/instructions.html#memory-instructions + // Also note: in the text format we can specify the useless `align=` but not the useful `offset=`! + let _alignment = self.fetch_immediate_u32(module); + let offset = self.fetch_immediate_u32(module); + let base_addr = self.value_store.pop_u32()?; + let addr = base_addr + offset; + let memory_size = self.memory.len() as u32; + if addr >= memory_size { + Err(Error::MemoryAccessOutOfBounds(addr, memory_size)) + } else { + Ok(addr) + } + } + + fn get_store_addr_value(&mut self, module: &WasmModule<'a>) -> Result<(usize, Value), Error> { + // Alignment is not used in the execution steps from the spec! Maybe it's just an optimization hint? + // https://webassembly.github.io/spec/core/exec/instructions.html#memory-instructions + // Also note: in the text format we can specify the useless `align=` but not the useful `offset=`! + let _alignment = self.fetch_immediate_u32(module); + let offset = self.fetch_immediate_u32(module); + let value = self.value_store.pop(); + let base_addr = self.value_store.pop_u32()?; + let addr = base_addr + offset; + let memory_size = self.memory.len() as u32; + if addr >= memory_size { + Err(Error::MemoryAccessOutOfBounds(addr, memory_size)) + } else { + Ok((addr as usize, value)) + } + } + + fn write_debug(&mut self, value: T) { + if let Some(debug_string) = self.debug_string.as_mut() { + std::write!(debug_string, "{value:?} ").unwrap(); + } + } + + fn do_break(&mut self, relative_blocks_outward: u32, module: &WasmModule<'a>) { + let block_index = self.blocks.len() - 1 - relative_blocks_outward as usize; + let Block { ty, vstack } = self.blocks[block_index]; + match ty { + BlockType::Loop(start_addr) => { + self.blocks.truncate(block_index + 1); + self.value_store.truncate(vstack); + self.program_counter = start_addr; + } + BlockType::FunctionBody(_) | BlockType::Normal => { + self.break_forward(relative_blocks_outward, module); + self.value_store.truncate(vstack); + } + BlockType::Locals(_) => unreachable!(), + } + } + + // Break to an outer block, going forward in the program + fn break_forward(&mut self, relative_blocks_outward: u32, module: &WasmModule<'a>) { + use OpCode::*; + + let addr = self.program_counter as u32; + let cache_result = self.branch_cache[self.current_frame.fn_index] + .iter() + .find(|entry| entry.addr == addr && entry.argument == relative_blocks_outward); + + let mut depth = self.blocks.len(); + let target_block_depth = depth - (relative_blocks_outward + 1) as usize; + + if let Some(entry) = cache_result { + self.program_counter = entry.target as usize; + } else { + loop { + let skipped_op = OpCode::from(module.code.bytes[self.program_counter]); + OpCode::skip_bytes(&module.code.bytes, &mut self.program_counter).unwrap(); + match skipped_op { + BLOCK | LOOP | IF => { + depth += 1; + } + END => { + depth -= 1; + if depth == target_block_depth { + break; + } + } + _ => {} + } + } + self.branch_cache[self.current_frame.fn_index].push(BranchCacheEntry { + addr, + argument: relative_blocks_outward, + target: self.program_counter as u32, + }); + } + self.blocks.truncate(target_block_depth); + } + + fn do_call( + &mut self, + expected_signature: Option, + fn_index: usize, + module: &WasmModule<'a>, + ) -> Result<(), Error> { + // self.debug_values_and_blocks(&format!("start do_call {}", fn_index)); + + let (signature_index, opt_import) = if fn_index < self.import_count { + // Imported non-Wasm function + let import = &module.import.imports[fn_index]; + let sig = match import.description { + ImportDesc::Func { signature_index } => signature_index, + _ => unreachable!(), + }; + (sig, Some(import)) + } else { + // Wasm function + let sig = module.function.signatures[fn_index - self.import_count]; + (sig, None) + }; + + if let Some(expected) = expected_signature { + assert_eq!( + expected, signature_index, + "Indirect function call failed. Expected signature {expected} but found {signature_index}", + ); + } + + let (arg_type_iter, ret_type) = module.types.look_up(signature_index); + let n_args = arg_type_iter.len(); + if self.debug_string.is_some() { + self.debug_call(n_args, ret_type); + } + + if let Some(import) = opt_import { + self.import_arguments.clear(); + self.import_arguments + .extend(std::iter::repeat(Value::I64(0)).take(n_args)); + for (i, expected) in arg_type_iter.enumerate().rev() { + let arg = self.value_store.pop(); + let actual = ValueType::from(arg); + if actual != expected { + return Err(Error::Type(expected, actual)); + } + self.import_arguments[i] = arg; + } + + let optional_return_val = self.import_dispatcher.dispatch( + import.module, + import.name, + &self.import_arguments, + &mut self.memory, + ); + if let Some(return_val) = optional_return_val { + self.value_store.push(return_val); + } + if let Some(debug_string) = self.debug_string.as_mut() { + write!(debug_string, " {}.{}", import.module, import.name).unwrap(); + } + } else { + let return_addr = self.program_counter; + // set PC to start of function bytes + let internal_fn_index = fn_index - self.import_count; + self.program_counter = module.code.function_offsets[internal_fn_index] as usize; + // advance PC to the start of the local variable declarations + u32::parse((), &module.code.bytes, &mut self.program_counter).unwrap(); + + self.blocks.push(Block { + ty: BlockType::Locals(fn_index), + vstack: self.value_store.depth() - n_args, + }); + let body_block_index = self.blocks.len(); + + let mut swap_frame = Frame::enter( + fn_index, + return_addr, + body_block_index, + n_args, + ret_type, + &module.code.bytes, + &mut self.value_store, + &mut self.program_counter, + ); + std::mem::swap(&mut swap_frame, &mut self.current_frame); + self.previous_frames.push(swap_frame); + + self.blocks.push(Block { + ty: BlockType::FunctionBody(fn_index), + vstack: self.value_store.depth(), + }); + } + // self.debug_values_and_blocks("end do_call"); + + Ok(()) + } + + fn debug_call(&mut self, n_args: usize, return_type: Option) { + if let Some(debug_string) = self.debug_string.as_mut() { + write!(debug_string, " args=[").unwrap(); + let arg_iter = self + .value_store + .iter() + .skip(self.value_store.depth() - n_args); + let mut first = true; + for arg in arg_iter { + if first { + first = false; + } else { + write!(debug_string, ", ").unwrap(); + } + write!(debug_string, "{arg:x?}").unwrap(); + } + writeln!(debug_string, "] return_type={return_type:?}").unwrap(); + } + } + + pub(crate) fn execute_next_instruction( + &mut self, + module: &WasmModule<'a>, + ) -> Result { + use OpCode::*; + + let file_offset = self.program_counter as u32 + module.code.section_offset; + let op_code = OpCode::from(module.code.bytes[self.program_counter]); + self.program_counter += 1; + + if let Some(debug_string) = self.debug_string.as_mut() { + debug_string.clear(); + self.write_debug(op_code); + } + + let mut action = Action::Continue; + let mut implicit_return = false; + + match op_code { + UNREACHABLE => { + return Err(Error::UnreachableOp); + } + NOP => {} + BLOCK => { + self.fetch_immediate_u32(module); // blocktype (ignored) + self.blocks.push(Block { + ty: BlockType::Normal, + vstack: self.value_store.depth(), + }); + } + LOOP => { + self.fetch_immediate_u32(module); // blocktype (ignored) + self.blocks.push(Block { + ty: BlockType::Loop(self.program_counter), + vstack: self.value_store.depth(), + }); + } + IF => { + self.fetch_immediate_u32(module); // blocktype (ignored) + let condition = self.value_store.pop_i32()?; + self.blocks.push(Block { + ty: BlockType::Normal, + vstack: self.value_store.depth(), + }); + if condition == 0 { + let addr = self.program_counter as u32; + let cache_result = self.branch_cache[self.current_frame.fn_index] + .iter() + .find(|entry| entry.addr == addr); + if let Some(entry) = cache_result { + self.program_counter = entry.target as usize; + } else { + let target_depth = self.blocks.len(); + let mut depth = target_depth; + loop { + let skipped_op = OpCode::from(module.code.bytes[self.program_counter]); + OpCode::skip_bytes(&module.code.bytes, &mut self.program_counter) + .unwrap(); + match skipped_op { + BLOCK | LOOP | IF => { + depth += 1; + } + END => { + if depth == target_depth { + // `if` without `else` + self.blocks.pop(); + break; + } else { + depth -= 1; + } + } + ELSE => { + if depth == target_depth { + break; + } + } + _ => {} + } + } + self.branch_cache[self.current_frame.fn_index].push(BranchCacheEntry { + addr, + argument: 0, + target: self.program_counter as u32, + }); + } + } + } + ELSE => { + // We only reach this point when we finish executing the "then" block of an IF statement + // (For a false condition, we would have skipped past the ELSE when we saw the IF) + // We don't want to execute the ELSE block, so we skip it, just like `br 0` would. + self.do_break(0, module); + } + END => { + if self.blocks.len() == (self.current_frame.body_block_index + 1) { + // implicit RETURN at end of function + action = self.do_return(); + implicit_return = true; + } else { + self.blocks.pop().unwrap(); + } + } + BR => { + let relative_blocks_outward = self.fetch_immediate_u32(module); + self.do_break(relative_blocks_outward, module); + } + BRIF => { + let relative_blocks_outward = self.fetch_immediate_u32(module); + let condition = self.value_store.pop_i32()?; + if condition != 0 { + self.do_break(relative_blocks_outward, module); + } + } + BRTABLE => { + let selector = self.value_store.pop_u32()?; + let nondefault_condition_count = self.fetch_immediate_u32(module); + let mut selected = None; + for i in 0..nondefault_condition_count { + let rel_blocks = self.fetch_immediate_u32(module); + if i == selector { + selected = Some(rel_blocks); + } + } + let fallback = self.fetch_immediate_u32(module); + let relative_blocks_outward = selected.unwrap_or(fallback); + self.do_break(relative_blocks_outward, module); + } + RETURN => { + action = self.do_return(); + } + CALL => { + let fn_index = self.fetch_immediate_u32(module) as usize; + self.do_call(None, fn_index, module)?; + } + CALLINDIRECT => { + let expected_signature = self.fetch_immediate_u32(module); + let table_index = self.fetch_immediate_u32(module); + let element_index = self.value_store.pop_u32()?; + + // So far, all compilers seem to be emitting MVP-compatible code. (Rust, Zig, Roc...) + assert_eq!( + table_index, 0, + "Table index {table_index} not supported at file offset {file_offset:#x}. This interpreter only supports Wasm MVP." + ); + + // Dereference the function pointer (look up the element index in the function table) + let fn_index = module.element.lookup(element_index).unwrap_or_else(|| { + panic!( + "Indirect function call failed. There is no function with element index {element_index}" + ) + }); + + self.do_call(Some(expected_signature), fn_index as usize, module)?; + } + DROP => { + self.value_store.pop(); + } + SELECT => { + let c = self.value_store.pop_i32()?; + let val2 = self.value_store.pop(); + let val1 = self.value_store.pop(); + let actual = ValueType::from(val2); + let expected = ValueType::from(val1); + if actual != expected { + return Err(Error::Type(expected, actual)); + } + let result = if c != 0 { val1 } else { val2 }; + self.value_store.push(result); + } + GETLOCAL => { + let index = self.fetch_immediate_u32(module); + let value = self.current_frame.get_local(&self.value_store, index); + self.value_store.push(value); + } + SETLOCAL => { + let index = self.fetch_immediate_u32(module); + let value = self.value_store.pop(); + self.current_frame + .set_local(&mut self.value_store, index, value); + } + TEELOCAL => { + let index = self.fetch_immediate_u32(module); + let value = self.value_store.peek(); + self.current_frame + .set_local(&mut self.value_store, index, value); + } + GETGLOBAL => { + let index = self.fetch_immediate_u32(module); + self.value_store.push(self.globals[index as usize]); + } + SETGLOBAL => { + let index = self.fetch_immediate_u32(module); + self.globals[index as usize] = self.value_store.pop(); + } + I32LOAD => { + let addr = self.get_load_address(module)? as usize; + let mut bytes = [0; 4]; + bytes.copy_from_slice(&self.memory[addr..][..4]); + let value = i32::from_le_bytes(bytes); + self.value_store.push(Value::I32(value)); + } + I64LOAD => { + let addr = self.get_load_address(module)? as usize; + let mut bytes = [0; 8]; + bytes.copy_from_slice(&self.memory[addr..][..8]); + let value = i64::from_le_bytes(bytes); + self.value_store.push(Value::I64(value)); + } + F32LOAD => { + let addr = self.get_load_address(module)? as usize; + let mut bytes = [0; 4]; + bytes.copy_from_slice(&self.memory[addr..][..4]); + let value = f32::from_le_bytes(bytes); + self.value_store.push(Value::F32(value)); + } + F64LOAD => { + let addr = self.get_load_address(module)? as usize; + let mut bytes = [0; 8]; + bytes.copy_from_slice(&self.memory[addr..][..8]); + let value = f64::from_le_bytes(bytes); + self.value_store.push(Value::F64(value)); + } + I32LOAD8S => { + let addr = self.get_load_address(module)? as usize; + let mut bytes = [0; 1]; + bytes.copy_from_slice(&self.memory[addr..][..1]); + let value = i8::from_le_bytes(bytes); + self.value_store.push(Value::I32(value as i32)); + } + I32LOAD8U => { + let addr = self.get_load_address(module)? as usize; + let value = self.memory[addr]; + self.value_store.push(Value::I32(value as i32)); + } + I32LOAD16S => { + let addr = self.get_load_address(module)? as usize; + let mut bytes = [0; 2]; + bytes.copy_from_slice(&self.memory[addr..][..2]); + let value = i16::from_le_bytes(bytes); + self.value_store.push(Value::I32(value as i32)); + } + I32LOAD16U => { + let addr = self.get_load_address(module)? as usize; + let mut bytes = [0; 2]; + bytes.copy_from_slice(&self.memory[addr..][..2]); + let value = u16::from_le_bytes(bytes); + self.value_store.push(Value::I32(value as i32)); + } + I64LOAD8S => { + let addr = self.get_load_address(module)? as usize; + let mut bytes = [0; 1]; + bytes.copy_from_slice(&self.memory[addr..][..1]); + let value = i8::from_le_bytes(bytes); + self.value_store.push(Value::I64(value as i64)); + } + I64LOAD8U => { + let addr = self.get_load_address(module)? as usize; + let value = self.memory[addr]; + self.value_store.push(Value::I64(value as i64)); + } + I64LOAD16S => { + let addr = self.get_load_address(module)? as usize; + let mut bytes = [0; 2]; + bytes.copy_from_slice(&self.memory[addr..][..2]); + let value = i16::from_le_bytes(bytes); + self.value_store.push(Value::I64(value as i64)); + } + I64LOAD16U => { + let addr = self.get_load_address(module)? as usize; + let mut bytes = [0; 2]; + bytes.copy_from_slice(&self.memory[addr..][..2]); + let value = u16::from_le_bytes(bytes); + self.value_store.push(Value::I64(value as i64)); + } + I64LOAD32S => { + let addr = self.get_load_address(module)? as usize; + let mut bytes = [0; 4]; + bytes.copy_from_slice(&self.memory[addr..][..4]); + let value = i32::from_le_bytes(bytes); + self.value_store.push(Value::I64(value as i64)); + } + I64LOAD32U => { + let addr = self.get_load_address(module)? as usize; + let mut bytes = [0; 4]; + bytes.copy_from_slice(&self.memory[addr..][..4]); + let value = u32::from_le_bytes(bytes); + self.value_store.push(Value::I64(value as i64)); + } + I32STORE => { + let (addr, value) = self.get_store_addr_value(module)?; + let unwrapped = value.expect_i32().map_err(Error::from)?; + let target = &mut self.memory[addr..][..4]; + target.copy_from_slice(&unwrapped.to_le_bytes()); + } + I64STORE => { + let (addr, value) = self.get_store_addr_value(module)?; + let unwrapped = value.expect_i64().map_err(Error::from)?; + let target = &mut self.memory[addr..][..8]; + target.copy_from_slice(&unwrapped.to_le_bytes()); + } + F32STORE => { + let (addr, value) = self.get_store_addr_value(module)?; + let unwrapped = value.expect_f32().map_err(Error::from)?; + let target = &mut self.memory[addr..][..4]; + target.copy_from_slice(&unwrapped.to_le_bytes()); + } + F64STORE => { + let (addr, value) = self.get_store_addr_value(module)?; + let unwrapped = value.expect_f64().map_err(Error::from)?; + let target = &mut self.memory[addr..][..8]; + target.copy_from_slice(&unwrapped.to_le_bytes()); + } + I32STORE8 => { + let (addr, value) = self.get_store_addr_value(module)?; + let unwrapped = value.expect_i32().map_err(Error::from)?; + let target = &mut self.memory[addr..][..1]; + target.copy_from_slice(&unwrapped.to_le_bytes()[..1]); + } + I32STORE16 => { + let (addr, value) = self.get_store_addr_value(module)?; + let unwrapped = value.expect_i32().map_err(Error::from)?; + let target = &mut self.memory[addr..][..2]; + target.copy_from_slice(&unwrapped.to_le_bytes()[..2]); + } + I64STORE8 => { + let (addr, value) = self.get_store_addr_value(module)?; + let unwrapped = value.expect_i64().map_err(Error::from)?; + let target = &mut self.memory[addr..][..1]; + target.copy_from_slice(&unwrapped.to_le_bytes()[..1]); + } + I64STORE16 => { + let (addr, value) = self.get_store_addr_value(module)?; + let unwrapped = value.expect_i64().map_err(Error::from)?; + let target = &mut self.memory[addr..][..2]; + target.copy_from_slice(&unwrapped.to_le_bytes()[..2]); + } + I64STORE32 => { + let (addr, value) = self.get_store_addr_value(module)?; + let unwrapped = value.expect_i64().map_err(Error::from)?; + let target = &mut self.memory[addr..][..4]; + target.copy_from_slice(&unwrapped.to_le_bytes()[..4]); + } + CURRENTMEMORY => { + let memory_index = self.fetch_immediate_u32(module); + assert_eq!(memory_index, 0); + let size = self.memory.len() as i32 / MemorySection::PAGE_SIZE as i32; + self.value_store.push(Value::I32(size)); + } + GROWMEMORY => { + let memory_index = self.fetch_immediate_u32(module); + assert_eq!(memory_index, 0); + let old_bytes = self.memory.len() as u32; + let old_pages = old_bytes / MemorySection::PAGE_SIZE; + let grow_pages = self.value_store.pop_u32()?; + let grow_bytes = grow_pages * MemorySection::PAGE_SIZE; + let new_bytes = old_bytes + grow_bytes; + + let success = match module.memory.max_bytes().unwrap() { + Some(max_bytes) => new_bytes <= max_bytes, + None => true, + }; + if success { + self.memory + .extend(iter::repeat(0).take(grow_bytes as usize)); + self.value_store.push(Value::I32(old_pages as i32)); + } else { + self.value_store.push(Value::I32(-1)); + } + } + MEMORY => { + // the first argument determines exactly which memory operation we have + match MemoryInstruction::try_from(module.code.bytes[self.program_counter]) { + Ok(op) => match op { + MemoryInstruction::MemoryInit => todo!("WASM instruction: memory.init"), + MemoryInstruction::DataDrop => todo!("WASM instruction: data.drop"), + MemoryInstruction::MemoryCopy => { + let size = self.value_store.pop_u32()? as usize; + let source = self.value_store.pop_u32()? as usize; + let destination = self.value_store.pop_u32()? as usize; + + // skip the op byte and an extra two zero bytes. + // in future versions of WebAssembly this byte may be used to index additional memories + self.program_counter += 1 + 2; + + self.memory.copy_within(source..source + size, destination) + } + MemoryInstruction::MemoryFill => { + let size = self.value_store.pop_u32()? as usize; + let byte_value = self.value_store.pop_u32()? as u8; + let destination = self.value_store.pop_u32()? as usize; + + // skip the op byte and an extra zero byte. + // in future versions of WebAssembly this byte may be used to index additional memories + self.program_counter += 1 + 1; + + self.memory[destination..][..size].fill(byte_value); + } + }, + Err(other) => unreachable!("invalid memory instruction {other:?}"), + }; + } + I32CONST => { + let value = i32::parse((), &module.code.bytes, &mut self.program_counter).unwrap(); + self.write_debug(value); + self.value_store.push(Value::I32(value)); + } + I64CONST => { + let value = i64::parse((), &module.code.bytes, &mut self.program_counter).unwrap(); + self.write_debug(value); + self.value_store.push(Value::I64(value)); + } + F32CONST => { + let mut bytes = [0; 4]; + bytes.copy_from_slice(&module.code.bytes[self.program_counter..][..4]); + let value = f32::from_le_bytes(bytes); + self.write_debug(value); + self.value_store.push(Value::F32(value)); + self.program_counter += 4; + } + F64CONST => { + let mut bytes = [0; 8]; + bytes.copy_from_slice(&module.code.bytes[self.program_counter..][..8]); + let value = f64::from_le_bytes(bytes); + self.write_debug(value); + self.value_store.push(Value::F64(value)); + self.program_counter += 8; + } + + I32EQZ => { + let arg = self.value_store.pop_i32()?; + let result: bool = arg == 0; + self.value_store.push(Value::I32(result as i32)); + } + I32EQ => { + let arg2 = self.value_store.pop_i32()?; + let arg1 = self.value_store.pop_i32()?; + let result: bool = arg1 == arg2; + self.value_store.push(Value::I32(result as i32)); + } + I32NE => { + let arg2 = self.value_store.pop_i32()?; + let arg1 = self.value_store.pop_i32()?; + let result: bool = arg1 != arg2; + self.value_store.push(Value::I32(result as i32)); + } + I32LTS => { + let arg2 = self.value_store.pop_i32()?; + let arg1 = self.value_store.pop_i32()?; + let result: bool = arg1 < arg2; + self.value_store.push(Value::I32(result as i32)); + } + I32LTU => { + let arg2 = self.value_store.pop_u32()?; + let arg1 = self.value_store.pop_u32()?; + let result: bool = arg1 < arg2; + self.value_store.push(Value::I32(result as i32)); + } + I32GTS => { + let arg2 = self.value_store.pop_i32()?; + let arg1 = self.value_store.pop_i32()?; + let result: bool = arg1 > arg2; + self.value_store.push(Value::I32(result as i32)); + } + I32GTU => { + let arg2 = self.value_store.pop_u32()?; + let arg1 = self.value_store.pop_u32()?; + let result: bool = arg1 > arg2; + self.value_store.push(Value::I32(result as i32)); + } + I32LES => { + let arg2 = self.value_store.pop_i32()?; + let arg1 = self.value_store.pop_i32()?; + let result: bool = arg1 <= arg2; + self.value_store.push(Value::I32(result as i32)); + } + I32LEU => { + let arg2 = self.value_store.pop_u32()?; + let arg1 = self.value_store.pop_u32()?; + let result: bool = arg1 <= arg2; + self.value_store.push(Value::I32(result as i32)); + } + I32GES => { + let arg2 = self.value_store.pop_i32()?; + let arg1 = self.value_store.pop_i32()?; + let result: bool = arg1 >= arg2; + self.value_store.push(Value::I32(result as i32)); + } + I32GEU => { + let arg2 = self.value_store.pop_u32()?; + let arg1 = self.value_store.pop_u32()?; + let result: bool = arg1 >= arg2; + self.value_store.push(Value::I32(result as i32)); + } + + I64EQZ => { + let arg = self.value_store.pop_i64()?; + let result: bool = arg == 0; + self.value_store.push(Value::I32(result as i32)); + } + I64EQ => { + let arg2 = self.value_store.pop_i64()?; + let arg1 = self.value_store.pop_i64()?; + let result: bool = arg1 == arg2; + self.value_store.push(Value::I32(result as i32)); + } + I64NE => { + let arg2 = self.value_store.pop_i64()?; + let arg1 = self.value_store.pop_i64()?; + let result: bool = arg1 != arg2; + self.value_store.push(Value::I32(result as i32)); + } + I64LTS => { + let arg2 = self.value_store.pop_i64()?; + let arg1 = self.value_store.pop_i64()?; + let result: bool = arg1 < arg2; + self.value_store.push(Value::I32(result as i32)); + } + I64LTU => { + let arg2 = self.value_store.pop_u64()?; + let arg1 = self.value_store.pop_u64()?; + let result: bool = arg1 < arg2; + self.value_store.push(Value::I32(result as i32)); + } + I64GTS => { + let arg2 = self.value_store.pop_i64()?; + let arg1 = self.value_store.pop_i64()?; + let result: bool = arg1 > arg2; + self.value_store.push(Value::I32(result as i32)); + } + I64GTU => { + let arg2 = self.value_store.pop_u64()?; + let arg1 = self.value_store.pop_u64()?; + let result: bool = arg1 > arg2; + self.value_store.push(Value::I32(result as i32)); + } + I64LES => { + let arg2 = self.value_store.pop_i64()?; + let arg1 = self.value_store.pop_i64()?; + let result: bool = arg1 <= arg2; + self.value_store.push(Value::I32(result as i32)); + } + I64LEU => { + let arg2 = self.value_store.pop_u64()?; + let arg1 = self.value_store.pop_u64()?; + let result: bool = arg1 <= arg2; + self.value_store.push(Value::I32(result as i32)); + } + I64GES => { + let arg2 = self.value_store.pop_i64()?; + let arg1 = self.value_store.pop_i64()?; + let result: bool = arg1 >= arg2; + self.value_store.push(Value::I32(result as i32)); + } + I64GEU => { + let arg2 = self.value_store.pop_u64()?; + let arg1 = self.value_store.pop_u64()?; + let result: bool = arg1 >= arg2; + self.value_store.push(Value::I32(result as i32)); + } + + F32EQ => { + let arg2 = self.value_store.pop_f32()?; + let arg1 = self.value_store.pop_f32()?; + let result: bool = arg1 == arg2; + self.value_store.push(Value::I32(result as i32)); + } + F32NE => { + let arg2 = self.value_store.pop_f32()?; + let arg1 = self.value_store.pop_f32()?; + let result: bool = arg1 != arg2; + self.value_store.push(Value::I32(result as i32)); + } + F32LT => { + let arg2 = self.value_store.pop_f32()?; + let arg1 = self.value_store.pop_f32()?; + let result: bool = arg1 < arg2; + self.value_store.push(Value::I32(result as i32)); + } + F32GT => { + let arg2 = self.value_store.pop_f32()?; + let arg1 = self.value_store.pop_f32()?; + let result: bool = arg1 > arg2; + self.value_store.push(Value::I32(result as i32)); + } + F32LE => { + let arg2 = self.value_store.pop_f32()?; + let arg1 = self.value_store.pop_f32()?; + let result: bool = arg1 <= arg2; + self.value_store.push(Value::I32(result as i32)); + } + F32GE => { + let arg2 = self.value_store.pop_f32()?; + let arg1 = self.value_store.pop_f32()?; + let result: bool = arg1 >= arg2; + self.value_store.push(Value::I32(result as i32)); + } + + F64EQ => { + let arg2 = self.value_store.pop_f64()?; + let arg1 = self.value_store.pop_f64()?; + let result: bool = arg1 == arg2; + self.value_store.push(Value::I32(result as i32)); + } + F64NE => { + let arg2 = self.value_store.pop_f64()?; + let arg1 = self.value_store.pop_f64()?; + let result: bool = arg1 != arg2; + self.value_store.push(Value::I32(result as i32)); + } + F64LT => { + let arg2 = self.value_store.pop_f64()?; + let arg1 = self.value_store.pop_f64()?; + let result: bool = arg1 < arg2; + self.value_store.push(Value::I32(result as i32)); + } + F64GT => { + let arg2 = self.value_store.pop_f64()?; + let arg1 = self.value_store.pop_f64()?; + let result: bool = arg1 > arg2; + self.value_store.push(Value::I32(result as i32)); + } + F64LE => { + let arg2 = self.value_store.pop_f64()?; + let arg1 = self.value_store.pop_f64()?; + let result: bool = arg1 <= arg2; + self.value_store.push(Value::I32(result as i32)); + } + F64GE => { + let arg2 = self.value_store.pop_f64()?; + let arg1 = self.value_store.pop_f64()?; + let result: bool = arg1 >= arg2; + self.value_store.push(Value::I32(result as i32)); + } + + I32CLZ => { + let arg = self.value_store.pop_u32()?; + self.value_store.push(Value::from(arg.leading_zeros())); + } + I32CTZ => { + let arg = self.value_store.pop_u32()?; + self.value_store.push(Value::from(arg.trailing_zeros())); + } + I32POPCNT => { + let arg = self.value_store.pop_u32()?; + self.value_store.push(Value::from(arg.count_ones())); + } + I32ADD => { + let arg2 = self.value_store.pop_i32()?; + let arg1 = self.value_store.pop_i32()?; + self.value_store.push(Value::from(arg1.wrapping_add(arg2))); + } + I32SUB => { + let arg2 = self.value_store.pop_i32()?; + let arg1 = self.value_store.pop_i32()?; + self.value_store.push(Value::from(arg1.wrapping_sub(arg2))); + } + I32MUL => { + let arg2 = self.value_store.pop_i32()?; + let arg1 = self.value_store.pop_i32()?; + self.value_store.push(Value::from(arg1.wrapping_mul(arg2))); + } + I32DIVS => { + let arg2 = self.value_store.pop_i32()?; + let arg1 = self.value_store.pop_i32()?; + self.value_store.push(Value::from(arg1.wrapping_div(arg2))); + } + I32DIVU => { + let arg2 = self.value_store.pop_u32()?; + let arg1 = self.value_store.pop_u32()?; + self.value_store.push(Value::from(arg1.wrapping_div(arg2))); + } + I32REMS => { + let arg2 = self.value_store.pop_i32()?; + let arg1 = self.value_store.pop_i32()?; + self.value_store.push(Value::from(arg1.wrapping_rem(arg2))); + } + I32REMU => { + let arg2 = self.value_store.pop_u32()?; + let arg1 = self.value_store.pop_u32()?; + self.value_store.push(Value::from(arg1.wrapping_rem(arg2))); + } + I32AND => { + let arg2 = self.value_store.pop_u32()?; + let arg1 = self.value_store.pop_u32()?; + self.value_store.push(Value::from(arg1 & arg2)); + } + I32OR => { + let arg2 = self.value_store.pop_u32()?; + let arg1 = self.value_store.pop_u32()?; + self.value_store.push(Value::from(arg1 | arg2)); + } + I32XOR => { + let arg2 = self.value_store.pop_u32()?; + let arg1 = self.value_store.pop_u32()?; + self.value_store.push(Value::from(arg1 ^ arg2)); + } + I32SHL => { + let arg2 = self.value_store.pop_u32()?; + let arg1 = self.value_store.pop_u32()?; + // Take modulo N as per the spec https://webassembly.github.io/spec/core/exec/numerics.html#op-ishl + let k = arg2 % 32; + self.value_store.push(Value::from(arg1 << k)); + } + I32SHRS => { + let arg2 = self.value_store.pop_i32()?; + let arg1 = self.value_store.pop_i32()?; + let k = arg2 % 32; + self.value_store.push(Value::from(arg1 >> k)); + } + I32SHRU => { + let arg2 = self.value_store.pop_u32()?; + let arg1 = self.value_store.pop_u32()?; + let k = arg2 % 32; + self.value_store.push(Value::from(arg1 >> k)); + } + I32ROTL => { + let arg2 = self.value_store.pop_u32()?; + let arg1 = self.value_store.pop_u32()?; + let k = arg2 % 32; + self.value_store.push(Value::from(arg1.rotate_left(k))); + } + I32ROTR => { + let arg2 = self.value_store.pop_u32()?; + let arg1 = self.value_store.pop_u32()?; + let k = arg2 % 32; + self.value_store.push(Value::from(arg1.rotate_right(k))); + } + + I64CLZ => { + let arg = self.value_store.pop_u64()?; + self.value_store + .push(Value::from(arg.leading_zeros() as u64)); + } + I64CTZ => { + let arg = self.value_store.pop_u64()?; + self.value_store + .push(Value::from(arg.trailing_zeros() as u64)); + } + I64POPCNT => { + let arg = self.value_store.pop_u64()?; + self.value_store.push(Value::from(arg.count_ones() as u64)); + } + I64ADD => { + let arg2 = self.value_store.pop_i64()?; + let arg1 = self.value_store.pop_i64()?; + self.value_store.push(Value::from(arg1.wrapping_add(arg2))); + } + I64SUB => { + let arg2 = self.value_store.pop_i64()?; + let arg1 = self.value_store.pop_i64()?; + self.value_store.push(Value::from(arg1.wrapping_sub(arg2))); + } + I64MUL => { + let arg2 = self.value_store.pop_i64()?; + let arg1 = self.value_store.pop_i64()?; + self.value_store.push(Value::from(arg1.wrapping_mul(arg2))); + } + I64DIVS => { + let arg2 = self.value_store.pop_i64()?; + let arg1 = self.value_store.pop_i64()?; + self.value_store.push(Value::from(arg1.wrapping_div(arg2))); + } + I64DIVU => { + let arg2 = self.value_store.pop_u64()?; + let arg1 = self.value_store.pop_u64()?; + self.value_store.push(Value::from(arg1.wrapping_div(arg2))); + } + I64REMS => { + let arg2 = self.value_store.pop_i64()?; + let arg1 = self.value_store.pop_i64()?; + self.value_store.push(Value::from(arg1.wrapping_rem(arg2))); + } + I64REMU => { + let arg2 = self.value_store.pop_u64()?; + let arg1 = self.value_store.pop_u64()?; + self.value_store.push(Value::from(arg1.wrapping_rem(arg2))); + } + I64AND => { + let arg2 = self.value_store.pop_u64()?; + let arg1 = self.value_store.pop_u64()?; + self.value_store.push(Value::from(arg1 & arg2)); + } + I64OR => { + let arg2 = self.value_store.pop_u64()?; + let arg1 = self.value_store.pop_u64()?; + self.value_store.push(Value::from(arg1 | arg2)); + } + I64XOR => { + let arg2 = self.value_store.pop_u64()?; + let arg1 = self.value_store.pop_u64()?; + self.value_store.push(Value::from(arg1 ^ arg2)); + } + I64SHL => { + let arg2 = self.value_store.pop_u64()?; + let arg1 = self.value_store.pop_u64()?; + // Take modulo N as per the spec https://webassembly.github.io/spec/core/exec/numerics.html#op-ishl + let k = arg2 % 64; + self.value_store.push(Value::from(arg1 << k)); + } + I64SHRS => { + let arg2 = self.value_store.pop_i64()?; + let arg1 = self.value_store.pop_i64()?; + let k = arg2 % 64; + self.value_store.push(Value::from(arg1 >> k)); + } + I64SHRU => { + let arg2 = self.value_store.pop_u64()?; + let arg1 = self.value_store.pop_u64()?; + let k = arg2 % 64; + self.value_store.push(Value::from(arg1 >> k)); + } + I64ROTL => { + let arg2 = self.value_store.pop_u64()?; + let arg1 = self.value_store.pop_u64()?; + let k = (arg2 % 64) as u32; + self.value_store.push(Value::from(arg1.rotate_left(k))); + } + I64ROTR => { + let arg2 = self.value_store.pop_u64()?; + let arg1 = self.value_store.pop_u64()?; + let k = (arg2 % 64) as u32; + self.value_store.push(Value::from(arg1.rotate_right(k))); + } + + F32ABS => { + let arg = self.value_store.pop_f32()?; + self.value_store.push(Value::F32(arg.abs())); + } + F32NEG => { + let arg = self.value_store.pop_f32()?; + self.value_store.push(Value::F32(-arg)); + } + F32CEIL => { + let arg = self.value_store.pop_f32()?; + self.value_store.push(Value::F32(arg.ceil())); + } + F32FLOOR => { + let arg = self.value_store.pop_f32()?; + self.value_store.push(Value::F32(arg.floor())); + } + F32TRUNC => { + let arg = self.value_store.pop_f32()?; + self.value_store.push(Value::F32(arg.trunc())); + } + F32NEAREST => { + // https://webassembly.github.io/spec/core/exec/numerics.html#op-fnearest + let arg = self.value_store.pop_f32()?; + let rounded = arg.round(); // "Rounds half-way cases away from 0.0" + let frac = arg - rounded; + let result = if frac == 0.5 || frac == -0.5 { + let rounded_half = rounded / 2.0; + let is_rounded_even = rounded_half.trunc() == rounded_half; + if is_rounded_even { + rounded + } else if rounded < arg { + rounded + 1.0 + } else { + rounded - 1.0 + } + } else { + rounded + }; + self.value_store.push(Value::F32(result)); + } + F32SQRT => { + let arg = self.value_store.pop_f32()?; + self.value_store.push(Value::F32(arg.sqrt())); + } + F32ADD => { + let arg2 = self.value_store.pop_f32()?; + let arg1 = self.value_store.pop_f32()?; + self.value_store.push(Value::F32(arg1 + arg2)); + } + F32SUB => { + let arg2 = self.value_store.pop_f32()?; + let arg1 = self.value_store.pop_f32()?; + self.value_store.push(Value::F32(arg1 - arg2)); + } + F32MUL => { + let arg2 = self.value_store.pop_f32()?; + let arg1 = self.value_store.pop_f32()?; + self.value_store.push(Value::F32(arg1 * arg2)); + } + F32DIV => { + let arg2 = self.value_store.pop_f32()?; + let arg1 = self.value_store.pop_f32()?; + self.value_store.push(Value::F32(arg1 / arg2)); + } + F32MIN => { + let arg2 = self.value_store.pop_f32()?; + let arg1 = self.value_store.pop_f32()?; + let result = if arg1 < arg2 { arg1 } else { arg2 }; + self.value_store.push(Value::F32(result)); + } + F32MAX => { + let arg2 = self.value_store.pop_f32()?; + let arg1 = self.value_store.pop_f32()?; + let result = if arg1 > arg2 { arg1 } else { arg2 }; + self.value_store.push(Value::F32(result)); + } + F32COPYSIGN => { + let arg2 = self.value_store.pop_f32()?; + let arg1 = self.value_store.pop_f32()?; + let result = if arg1.is_sign_negative() == arg2.is_sign_negative() { + arg1 + } else { + arg2 + }; + self.value_store.push(Value::F32(result)); + } + + F64ABS => { + let arg = self.value_store.pop_f64()?; + self.value_store.push(Value::F64(arg.abs())); + } + F64NEG => { + let arg = self.value_store.pop_f64()?; + self.value_store.push(Value::F64(-arg)); + } + F64CEIL => { + let arg = self.value_store.pop_f64()?; + self.value_store.push(Value::F64(arg.ceil())); + } + F64FLOOR => { + let arg = self.value_store.pop_f64()?; + self.value_store.push(Value::F64(arg.floor())); + } + F64TRUNC => { + let arg = self.value_store.pop_f64()?; + self.value_store.push(Value::F64(arg.trunc())); + } + F64NEAREST => { + // https://webassembly.github.io/spec/core/exec/numerics.html#op-fnearest + let arg = self.value_store.pop_f64()?; + let rounded = arg.round(); // "Rounds half-way cases away from 0.0" + let frac = arg - rounded; + let result = if frac == 0.5 || frac == -0.5 { + let rounded_half = rounded / 2.0; + let is_rounded_even = rounded_half.trunc() == rounded_half; + if is_rounded_even { + rounded + } else if rounded < arg { + rounded + 1.0 + } else { + rounded - 1.0 + } + } else { + rounded + }; + self.value_store.push(Value::F64(result)); + } + F64SQRT => { + let arg = self.value_store.pop_f64()?; + self.value_store.push(Value::F64(arg.sqrt())); + } + F64ADD => { + let arg2 = self.value_store.pop_f64()?; + let arg1 = self.value_store.pop_f64()?; + self.value_store.push(Value::F64(arg1 + arg2)); + } + F64SUB => { + let arg2 = self.value_store.pop_f64()?; + let arg1 = self.value_store.pop_f64()?; + self.value_store.push(Value::F64(arg1 - arg2)); + } + F64MUL => { + let arg2 = self.value_store.pop_f64()?; + let arg1 = self.value_store.pop_f64()?; + self.value_store.push(Value::F64(arg1 * arg2)); + } + F64DIV => { + let arg2 = self.value_store.pop_f64()?; + let arg1 = self.value_store.pop_f64()?; + self.value_store.push(Value::F64(arg1 / arg2)); + } + F64MIN => { + let arg2 = self.value_store.pop_f64()?; + let arg1 = self.value_store.pop_f64()?; + let result = if arg1 < arg2 { arg1 } else { arg2 }; + self.value_store.push(Value::F64(result)); + } + F64MAX => { + let arg2 = self.value_store.pop_f64()?; + let arg1 = self.value_store.pop_f64()?; + let result = if arg1 > arg2 { arg1 } else { arg2 }; + self.value_store.push(Value::F64(result)); + } + F64COPYSIGN => { + let arg2 = self.value_store.pop_f64()?; + let arg1 = self.value_store.pop_f64()?; + let result = if arg1.is_sign_negative() == arg2.is_sign_negative() { + arg1 + } else { + arg2 + }; + self.value_store.push(Value::F64(result)); + } + + I32WRAPI64 => { + let arg = self.value_store.pop_u64()?; + let wrapped: u32 = (arg & 0xffff_ffff) as u32; + self.value_store.push(Value::from(wrapped)); + } + I32TRUNCSF32 => { + let arg = self.value_store.pop_f32()?; + if arg < i32::MIN as f32 || arg > i32::MAX as f32 { + panic!("Cannot truncate {arg} from F32 to I32"); + } + self.value_store.push(Value::I32(arg as i32)); + } + I32TRUNCUF32 => { + let arg = self.value_store.pop_f32()?; + if arg < u32::MIN as f32 || arg > u32::MAX as f32 { + panic!("Cannot truncate {arg} from F32 to unsigned I32"); + } + self.value_store.push(Value::from(arg as u32)); + } + I32TRUNCSF64 => { + let arg = self.value_store.pop_f64()?; + if arg < i32::MIN as f64 || arg > i32::MAX as f64 { + panic!("Cannot truncate {arg} from F64 to I32"); + } + self.value_store.push(Value::I32(arg as i32)); + } + I32TRUNCUF64 => { + let arg = self.value_store.pop_f64()?; + if arg < u32::MIN as f64 || arg > u32::MAX as f64 { + panic!("Cannot truncate {arg} from F64 to unsigned I32"); + } + self.value_store.push(Value::from(arg as u32)); + } + I64EXTENDSI32 => { + let arg = self.value_store.pop_i32()?; + self.value_store.push(Value::I64(arg as i64)); + } + I64EXTENDUI32 => { + let arg = self.value_store.pop_u32()?; + self.value_store.push(Value::from(arg as u64)); + } + I64TRUNCSF32 => { + let arg = self.value_store.pop_f32()?; + if arg < i64::MIN as f32 || arg > i64::MAX as f32 { + panic!("Cannot truncate {arg} from F32 to I64"); + } + self.value_store.push(Value::I64(arg as i64)); + } + I64TRUNCUF32 => { + let arg = self.value_store.pop_f32()?; + if arg < u64::MIN as f32 || arg > u64::MAX as f32 { + panic!("Cannot truncate {arg} from F32 to unsigned I64"); + } + self.value_store.push(Value::from(arg as u64)); + } + I64TRUNCSF64 => { + let arg = self.value_store.pop_f64()?; + if arg < i64::MIN as f64 || arg > i64::MAX as f64 { + panic!("Cannot truncate {arg} from F64 to I64"); + } + self.value_store.push(Value::I64(arg as i64)); + } + I64TRUNCUF64 => { + let arg = self.value_store.pop_f64()?; + if arg < u64::MIN as f64 || arg > u64::MAX as f64 { + panic!("Cannot truncate {arg} from F64 to unsigned I64"); + } + self.value_store.push(Value::from(arg as u64)); + } + F32CONVERTSI32 => { + let arg = self.value_store.pop_i32()?; + self.value_store.push(Value::F32(arg as f32)); + } + F32CONVERTUI32 => { + let arg = self.value_store.pop_u32()?; + self.value_store.push(Value::F32(arg as f32)); + } + F32CONVERTSI64 => { + let arg = self.value_store.pop_i64()?; + self.value_store.push(Value::F32(arg as f32)); + } + F32CONVERTUI64 => { + let arg = self.value_store.pop_u64()?; + self.value_store.push(Value::F32(arg as f32)); + } + F32DEMOTEF64 => { + let arg = self.value_store.pop_f64()?; + self.value_store.push(Value::F32(arg as f32)); + } + F64CONVERTSI32 => { + let arg = self.value_store.pop_i32()?; + self.value_store.push(Value::F64(arg as f64)); + } + F64CONVERTUI32 => { + let arg = self.value_store.pop_u32()?; + self.value_store.push(Value::F64(arg as f64)); + } + F64CONVERTSI64 => { + let arg = self.value_store.pop_i64()?; + self.value_store.push(Value::F64(arg as f64)); + } + F64CONVERTUI64 => { + let arg = self.value_store.pop_u64()?; + self.value_store.push(Value::F64(arg as f64)); + } + F64PROMOTEF32 => { + let arg = self.value_store.pop_f32()?; + self.value_store.push(Value::F64(arg as f64)); + } + + I32REINTERPRETF32 => { + let x = self.value_store.pop_f32()?; + self.value_store + .push(Value::I32(i32::from_ne_bytes(x.to_ne_bytes()))); + } + I64REINTERPRETF64 => { + let x = self.value_store.pop_f64()?; + self.value_store + .push(Value::I64(i64::from_ne_bytes(x.to_ne_bytes()))); + } + F32REINTERPRETI32 => { + let x = self.value_store.pop_i32()?; + self.value_store + .push(Value::F32(f32::from_ne_bytes(x.to_ne_bytes()))); + } + F64REINTERPRETI64 => { + let x = self.value_store.pop_i64()?; + self.value_store + .push(Value::F64(f64::from_ne_bytes(x.to_ne_bytes()))); + } + I32EXTEND8S => { + let x = self.value_store.pop_i32()?; + self.value_store.push(Value::I32(x as i8 as i32)); + } + I32EXTEND16S => { + let x = self.value_store.pop_i32()?; + self.value_store.push(Value::I32(x as i16 as i32)); + } + I64EXTEND8S => { + let x = self.value_store.pop_i64()?; + self.value_store.push(Value::I64(x as i8 as i64)); + } + I64EXTEND16S => { + let x = self.value_store.pop_i64()?; + self.value_store.push(Value::I64(x as i16 as i64)); + } + I64EXTEND32S => { + let x = self.value_store.pop_i64()?; + self.value_store.push(Value::I64(x as i32 as i64)); + } + } + + if let Some(debug_string) = &self.debug_string { + if matches!(op_code, CALL | CALLINDIRECT) { + eprintln!("\n{file_offset:06x} {debug_string}"); + } else { + // For calls, we print special debug stuff in do_call + let base = self.current_frame.locals_start + self.current_frame.locals_count; + let slice = self.value_store.get_slice(base); + eprintln!("{file_offset:06x} {debug_string:17} {slice:x?}"); + } + let is_return = op_code == RETURN || (op_code == END && implicit_return); + let is_program_end = self.program_counter == 0; + if is_return && !is_program_end { + eprintln!( + "returning to function {} at {:06x}", + self.current_frame.fn_index, + self.program_counter + self.module.code.section_offset as usize, + ); + } + } + + Ok(action) + } + + #[allow(dead_code)] + fn debug_values_and_blocks(&self, label: &str) { + eprintln!("\n========== {label} =========="); + + let mut block_str = String::new(); + let mut block_iter = self.blocks.iter().enumerate(); + let mut block = block_iter.next(); + + let mut print_blocks = |i| { + block_str.clear(); + while let Some((b, Block { vstack, ty })) = block { + if *vstack > i { + break; + } + write!(block_str, "{b}:{ty:?} ").unwrap(); + block = block_iter.next(); + } + if !block_str.is_empty() { + eprintln!("--------------- {block_str}"); + } + }; + + for (i, v) in self.value_store.iter().enumerate() { + print_blocks(i); + eprintln!("{i:3} {v:x?}"); + } + print_blocks(self.value_store.depth()); + + eprintln!(); + } + + /// Dump a stack trace when an error occurs + /// -------------- + /// func[123] + /// address 0x12345 + /// args 0: I64(234), 1: F64(7.15) + /// locals 2: I32(412), 3: F64(3.14) + /// stack [I64(111), F64(3.14)] + /// -------------- + fn debug_stack_trace(&self, buffer: &mut String) -> fmt::Result { + let divider = "-------------------"; + writeln!(buffer, "{divider}")?; + + let frames = self.previous_frames.iter().chain(once(&self.current_frame)); + let next_frames = frames.clone().skip(1); + + // Find the code address to display for each frame + // For previous frames, show the address of the CALL instruction + // For the current frame, show the program counter value + let mut execution_addrs = { + // for each previous_frame, find return address of the *next* frame + let return_addrs = next_frames.clone().map(|f| f.return_addr); + // roll back to the CALL instruction before that return address, it's more meaningful. + let call_addrs = return_addrs.map(|ra| self.debug_return_addr_to_call_addr(ra)); + // For the current frame, show the program_counter + call_addrs.chain(once(self.program_counter)) + }; + + let mut frame_ends = next_frames.map(|f| f.locals_start); + + for frame in frames { + let Frame { + fn_index, + locals_count, + locals_start, + .. + } = frame; + + let arg_count = { + let signature_index = if *fn_index < self.import_count { + match self.module.import.imports[*fn_index].description { + ImportDesc::Func { signature_index } => signature_index, + _ => unreachable!(), + } + } else { + self.module.function.signatures[fn_index - self.import_count] + }; + self.module.types.look_up(signature_index).0.len() + }; + + let fn_name = self + .module + .names + .function_names + .iter() + .find(|(idx, _)| *idx == *fn_index as u32) + .map(|(_, name)| *name) + .unwrap_or(""); + + // Function and address match wasm-objdump formatting, for easy copy & find + writeln!(buffer, "func[{fn_index}] {fn_name}")?; + writeln!(buffer, " address {:06x}", execution_addrs.next().unwrap())?; + + write!(buffer, " args ")?; + for local_index in 0..*locals_count { + let value = self.value_store.get(locals_start + local_index).unwrap(); + if local_index == arg_count { + write!(buffer, "\n locals ")?; + } else if local_index != 0 { + write!(buffer, ", ")?; + } + write!(buffer, "{local_index}: {value:?}")?; + } + + write!(buffer, "\n stack [")?; + let frame_end = frame_ends + .next() + .unwrap_or_else(|| self.value_store.depth()); + let stack_start = locals_start + locals_count; + for i in stack_start..frame_end { + let value = self.value_store.get(i).unwrap(); + if i != stack_start { + write!(buffer, ", ")?; + } + write!(buffer, "{value:?}")?; + } + writeln!(buffer, "]")?; + writeln!(buffer, "{divider}")?; + } + + Ok(()) + } + + // Call address is more intuitive than the return address in the stack trace. Search backward for it. + fn debug_return_addr_to_call_addr(&self, return_addr: usize) -> usize { + // return_addr is pointing at the next instruction after the CALL/CALLINDIRECT. + // Just before that is the LEB-128 function index or type index. + // The last LEB-128 byte is <128, but the others are >=128 so we can't mistake them for CALL/CALLINDIRECT + let mut call_addr = return_addr - 2; + loop { + let byte = self.module.code.bytes[call_addr]; + if byte == OpCode::CALL as u8 || byte == OpCode::CALLINDIRECT as u8 { + break; + } else { + call_addr -= 1; + } + } + call_addr + } +} diff --git a/crates/wasm_interp/src/lib.rs b/crates/wasm_interp/src/lib.rs new file mode 100644 index 0000000000..8cc98d3e39 --- /dev/null +++ b/crates/wasm_interp/src/lib.rs @@ -0,0 +1,101 @@ +mod frame; +mod instance; +mod tests; +mod value_store; +pub mod wasi; + +// Main external interface +pub use instance::Instance; +pub use wasi::{WasiDispatcher, WasiFile}; + +pub use roc_wasm_module::Value; +use roc_wasm_module::ValueType; + +pub trait ImportDispatcher { + /// Dispatch a call from WebAssembly to your own code, based on module and function name. + fn dispatch( + &mut self, + module_name: &str, + function_name: &str, + arguments: &[Value], + memory: &mut [u8], + ) -> Option; +} + +impl Default for DefaultImportDispatcher<'_> { + fn default() -> Self { + DefaultImportDispatcher { + wasi: WasiDispatcher::new(&[]), + } + } +} + +pub struct DefaultImportDispatcher<'a> { + pub wasi: WasiDispatcher<'a>, +} + +impl<'a> DefaultImportDispatcher<'a> { + pub fn new(args: &'a [&'a [u8]]) -> Self { + DefaultImportDispatcher { + wasi: WasiDispatcher::new(args), + } + } +} + +impl<'a> ImportDispatcher for DefaultImportDispatcher<'a> { + fn dispatch( + &mut self, + module_name: &str, + function_name: &str, + arguments: &[Value], + memory: &mut [u8], + ) -> Option { + if module_name == wasi::MODULE_NAME { + self.wasi.dispatch(function_name, arguments, memory) + } else { + panic!("DefaultImportDispatcher does not implement {module_name}.{function_name}"); + } + } +} + +/// Errors that can happen while interpreting the program +/// All of these cause a WebAssembly stack trace to be dumped +#[derive(Debug, PartialEq)] +pub(crate) enum Error { + Type(ValueType, ValueType), + StackEmpty, + MemoryAccessOutOfBounds(u32, u32), + UnreachableOp, +} + +impl Error { + pub fn to_string_at(&self, file_offset: usize) -> String { + match self { + Error::Type(expected, actual) => { + format!( + "ERROR: I found a type mismatch at file offset {file_offset:#x}. Expected {expected:?}, but found {actual:?}.\n" + ) + } + Error::StackEmpty => { + format!( + "ERROR: I tried to pop a value from the stack at file offset {file_offset:#x}, but it was empty.\n" + ) + } + Error::MemoryAccessOutOfBounds(addr, memory_size) => { + format!( + "ERROR: A Wasm instruction at file offset {:#x} tried to access memory at {:#x} but the maximum address is {:#x}\n", + file_offset, addr, memory_size-1 + ) + } + Error::UnreachableOp => { + format!("WebAssembly `unreachable` instruction at file offset {file_offset:#x}.\n") + } + } + } +} + +impl From<(ValueType, ValueType)> for Error { + fn from((expected, actual): (ValueType, ValueType)) -> Self { + Error::Type(expected, actual) + } +} diff --git a/crates/wasm_interp/src/main.rs b/crates/wasm_interp/src/main.rs new file mode 100644 index 0000000000..86bce0dc90 --- /dev/null +++ b/crates/wasm_interp/src/main.rs @@ -0,0 +1,119 @@ +use bumpalo::{collections::Vec, Bump}; +use clap::ArgAction; +use clap::{Arg, Command}; +use std::fs; +use std::io; +use std::iter::once; +use std::process; + +use roc_wasm_interp::{DefaultImportDispatcher, Instance}; +use roc_wasm_module::WasmModule; + +pub const FLAG_FUNCTION: &str = "function"; +pub const FLAG_DEBUG: &str = "debug"; +pub const FLAG_HEX: &str = "hex"; +pub const WASM_FILE: &str = "WASM_FILE"; +pub const ARGS_FOR_APP: &str = "ARGS_FOR_APP"; + +fn main() -> io::Result<()> { + let arena = Bump::new(); + + // Define the command line arguments + + let flag_function = Arg::new(FLAG_FUNCTION) + .long(FLAG_FUNCTION) + .help("Call a specific function exported from the WebAssembly module") + .default_value("_start") + .required(false); + + let flag_debug = Arg::new(FLAG_DEBUG) + .long(FLAG_DEBUG) + .help("Print a log of every instruction executed, for debugging purposes.") + .action(ArgAction::SetTrue) + .required(false); + + let flag_hex = Arg::new(FLAG_HEX) + .long(FLAG_HEX) + .help("If the called function returns a value, print it in hexadecimal format.") + .action(ArgAction::SetTrue) + .required(false); + + let wasm_file_to_run = Arg::new(WASM_FILE) + .help("The .wasm file to run") + .required(true); + + let args_for_app = Arg::new(ARGS_FOR_APP) + .help("Arguments to pass into the WebAssembly app\ne.g. `roc_wasm_interp app.wasm 123 123.45`") + .num_args(0..); + + let app = Command::new("roc_wasm_interp") + .about("Run the given .wasm file") + .arg(flag_function) + .arg(flag_debug) + .arg(flag_hex) + .arg(wasm_file_to_run) + .arg(args_for_app); + + // Parse the command line arguments + + let matches = app.get_matches(); + let start_fn_name = matches.get_one::(FLAG_FUNCTION).unwrap(); + let is_debug_mode = matches.get_flag(FLAG_DEBUG); + let is_hex_format = matches.get_flag(FLAG_HEX); + let start_arg_strings = matches.get_many::(ARGS_FOR_APP).unwrap_or_default(); + let wasm_path = matches.get_one::(WASM_FILE).unwrap(); + // WASI expects the .wasm file to be argv[0] + let wasi_argv_iter = once(wasm_path) + .chain(start_arg_strings) + .map(|s| s.as_bytes()); + let wasi_argv = Vec::from_iter_in(wasi_argv_iter, &arena); + + // Load the WebAssembly binary file + + let module_bytes = fs::read(wasm_path)?; + + // Parse the binary data + + let require_relocatable = false; + let module = match WasmModule::preload(&arena, &module_bytes, require_relocatable) { + Ok(m) => m, + Err(e) => { + eprintln!("I couldn't parse this WebAssembly module! There's something wrong at byte offset {:#x}.", e.offset); + eprintln!("{}", e.message); + eprintln!("If you think this could be a code generation problem in the Roc compiler, see crates/compiler/gen_wasm/README.md for debugging tips."); + process::exit(1); + } + }; + + // Create an execution instance + + let dispatcher = DefaultImportDispatcher::new(&wasi_argv); + let mut inst = + Instance::for_module(&arena, &module, dispatcher, is_debug_mode).unwrap_or_else(|e| { + eprintln!("{e}"); + process::exit(2); + }); + + // Run + + let result = inst.call_export_from_cli(&module, start_fn_name, &wasi_argv); + + // Print out return value, if any + + match result { + Ok(Some(val)) => { + if is_hex_format { + println!("{val:#x?}") + } else { + println!("{val:?}") + } + } + Ok(None) => {} + Err(e) => { + eprintln!("{e}"); + process::exit(3); + } + } + + Ok(()) +} diff --git a/crates/wasm_interp/src/tests/mod.rs b/crates/wasm_interp/src/tests/mod.rs new file mode 100644 index 0000000000..9b83a81b98 --- /dev/null +++ b/crates/wasm_interp/src/tests/mod.rs @@ -0,0 +1,158 @@ +#![cfg(test)] + +mod test_basics; +mod test_convert; +mod test_f32; +mod test_f64; +mod test_i32; +mod test_i64; +mod test_mem; + +use crate::{DefaultImportDispatcher, Instance}; +use bumpalo::{collections::Vec, Bump}; +use roc_wasm_module::{ + opcodes::OpCode, Export, ExportType, SerialBuffer, Serialize, Signature, Value, ValueType, + WasmModule, +}; + +pub fn default_state(arena: &Bump) -> Instance { + let pages = 1; + let program_counter = 0; + let globals = []; + Instance::new( + arena, + pages, + program_counter, + globals, + DefaultImportDispatcher::default(), + ) +} + +pub fn const_value(buf: &mut Vec<'_, u8>, value: Value) { + use Value::*; + match value { + I32(x) => { + buf.push(OpCode::I32CONST as u8); + buf.encode_i32(x); + } + I64(x) => { + buf.push(OpCode::I64CONST as u8); + buf.encode_i64(x); + } + F32(x) => { + buf.push(OpCode::F32CONST as u8); + buf.encode_f32(x); + } + F64(x) => { + buf.push(OpCode::F64CONST as u8); + buf.encode_f64(x); + } + } +} + +pub fn test_op_example(op: OpCode, args: A, expected: Value) +where + A: IntoIterator, +{ + let arena = Bump::new(); + let mut module = WasmModule::new(&arena); + + { + let buf = &mut module.code.bytes; + let func_len_index = buf.encode_padded_u32(0); + let start = buf.len(); + buf.push(0); // no locals + for arg in args { + const_value(buf, arg); + } + buf.push(op as u8); + buf.push(OpCode::END as u8); // end function + + buf.overwrite_padded_u32(func_len_index, (buf.len() - start) as u32); + + module.code.function_count = 1; + module.code.function_offsets.push(0); + module.add_function_signature(Signature { + param_types: Vec::new_in(&arena), + ret_type: Some(ValueType::from(expected)), + }); + module.export.append(Export { + name: "test", + ty: ExportType::Func, + index: 0, + }); + } + + // Dump the generated module to a file (this is mainly for debugging the test itself) + if std::env::var("DEBUG_WASM_INTERP_TEST").is_ok() { + let filename = format!("/tmp/{op:?}.wasm"); + println!("\nDumping test module to {}\n", &filename); + let mut outfile_buf = Vec::new_in(&arena); + module.serialize(&mut outfile_buf); + std::fs::write(&filename, outfile_buf).unwrap(); + } + + let mut inst = + Instance::for_module(&arena, &module, DefaultImportDispatcher::default(), true).unwrap(); + + let return_val = inst.call_export("test", []).unwrap().unwrap(); + + assert_eq!(return_val, expected); +} + +pub fn create_exported_function_no_locals<'a, F>( + module: &mut WasmModule<'a>, + name: &'a str, + signature: Signature<'a>, + write_instructions: F, +) where + F: FnOnce(&mut Vec<'a, u8>), +{ + let internal_fn_index = module.code.function_offsets.len(); + let fn_index = module.import.function_count() + internal_fn_index; + module.export.exports.push(Export { + name, + ty: ExportType::Func, + index: fn_index as u32, + }); + module.add_function_signature(signature); + + let offset = module.code.bytes.encode_padded_u32(0); + let start = module.code.bytes.len(); + module.code.bytes.push(0); // no locals + write_instructions(&mut module.code.bytes); + let len = module.code.bytes.len() - start; + module.code.bytes.overwrite_padded_u32(offset, len as u32); + + module.code.function_count += 1; + module.code.function_offsets.push(offset as u32); +} + +pub fn create_exported_function_with_locals<'a, F>( + module: &mut WasmModule<'a>, + name: &'a str, + signature: Signature<'a>, + local_types: &[(u32, ValueType)], + write_instructions: F, +) where + F: FnOnce(&mut Vec<'a, u8>), +{ + let internal_fn_index = module.code.function_offsets.len(); + let fn_index = module.import.function_count() + internal_fn_index; + module.export.exports.push(Export { + name, + ty: ExportType::Func, + index: fn_index as u32, + }); + module.add_function_signature(signature); + + let offset = module.code.bytes.encode_padded_u32(0); + let start = module.code.bytes.len(); + local_types.serialize(&mut module.code.bytes); + write_instructions(&mut module.code.bytes); + let len = module.code.bytes.len() - start; + module.code.bytes.overwrite_padded_u32(offset, len as u32); + + module.code.function_count += 1; + module.code.function_offsets.push(offset as u32); +} diff --git a/crates/wasm_interp/src/tests/test_basics.rs b/crates/wasm_interp/src/tests/test_basics.rs new file mode 100644 index 0000000000..7ce3a11996 --- /dev/null +++ b/crates/wasm_interp/src/tests/test_basics.rs @@ -0,0 +1,994 @@ +#![cfg(test)] + +use crate::frame::Frame; +use crate::tests::{ + const_value, create_exported_function_no_locals, create_exported_function_with_locals, + default_state, +}; +use crate::{DefaultImportDispatcher, ImportDispatcher, Instance}; +use bumpalo::{collections::Vec, Bump}; +use roc_wasm_module::sections::{Import, ImportDesc}; +use roc_wasm_module::{ + opcodes::OpCode, sections::ElementSegment, Export, ExportType, SerialBuffer, Serialize, + Signature, Value, ValueType, WasmModule, +}; + +#[test] +fn test_loop() { + test_loop_help(10, 55); +} + +fn test_loop_help(end: i32, expected: i32) { + let arena = Bump::new(); + let mut module = WasmModule::new(&arena); + { + let buf = &mut module.code.bytes; + + // Loop from 0 to end, adding the loop variable to a total + let var_i = 0; + let var_total = 1; + + let fn_len_index = buf.encode_padded_u32(0); + + // (local i32 i32) + buf.push(1); // one group of the given type + buf.push(2); // two locals in the group + buf.push(ValueType::I32 as u8); + + // loop + buf.push(OpCode::LOOP as u8); + buf.push(ValueType::VOID); + + // local.get $i + buf.push(OpCode::GETLOCAL as u8); + buf.encode_u32(var_i); + + // i32.const 1 + buf.push(OpCode::I32CONST as u8); + buf.encode_i32(1); + + // i32.add + buf.push(OpCode::I32ADD as u8); + + // local.tee $i + buf.push(OpCode::TEELOCAL as u8); + buf.encode_u32(var_i); + + // local.get $total + buf.push(OpCode::GETLOCAL as u8); + buf.encode_u32(var_total); + + // i32.add + buf.push(OpCode::I32ADD as u8); + + // local.set $total + buf.push(OpCode::SETLOCAL as u8); + buf.encode_u32(var_total); + + // local.get $i + buf.push(OpCode::GETLOCAL as u8); + buf.encode_u32(var_i); + + // i32.const $end + buf.push(OpCode::I32CONST as u8); + buf.encode_i32(end); + + // i32.lt_s + buf.push(OpCode::I32LTS as u8); + + // br_if 0 + buf.push(OpCode::BRIF as u8); + buf.encode_u32(0); + + // end + buf.push(OpCode::END as u8); + + // local.get $total + buf.push(OpCode::GETLOCAL as u8); + buf.encode_u32(var_total); + + // end function + buf.push(OpCode::END as u8); + + buf.overwrite_padded_u32(fn_len_index, (buf.len() - fn_len_index) as u32); + } + module.code.function_offsets.push(0); + module.code.function_count = 1; + + module.add_function_signature(Signature { + param_types: Vec::new_in(&arena), + ret_type: Some(ValueType::I32), + }); + module.export.append(Export { + name: "test", + ty: ExportType::Func, + index: 0, + }); + + let mut inst = + Instance::for_module(&arena, &module, DefaultImportDispatcher::default(), false).unwrap(); + let return_val = inst.call_export("test", []).unwrap().unwrap(); + + assert_eq!(return_val, Value::I32(expected)); +} + +#[test] +fn test_if_else() { + test_if_else_help(0, 222); + test_if_else_help(1, 111); + test_if_else_help(-123, 111); +} + +fn test_if_else_help(condition: i32, expected: i32) { + let arena = Bump::new(); + let mut module = WasmModule::new(&arena); + + let signature = Signature { + param_types: bumpalo::vec![in &arena], + ret_type: Some(ValueType::I32), + }; + let local_types = [(1, ValueType::I32)]; + create_exported_function_with_locals(&mut module, "test", signature, &local_types, |buf| { + // i32.const + buf.push(OpCode::I32CONST as u8); + buf.encode_i32(condition); + + // if + buf.push(OpCode::IF as u8); + buf.push(ValueType::VOID); + + // i32.const 111 + buf.push(OpCode::I32CONST as u8); + buf.encode_i32(111); + + // local.set 0 + buf.push(OpCode::SETLOCAL as u8); + buf.encode_u32(0); + + // else + buf.push(OpCode::ELSE as u8); + + // i32.const 222 + buf.push(OpCode::I32CONST as u8); + buf.encode_i32(222); + + // local.set 0 + buf.push(OpCode::SETLOCAL as u8); + buf.encode_u32(0); + + // end + buf.push(OpCode::END as u8); + + // local.get 0 + buf.push(OpCode::GETLOCAL as u8); + buf.encode_u32(0); + + // end function + buf.push(OpCode::END as u8); + }); + + let is_debug_mode = false; + let mut inst = Instance::for_module( + &arena, + &module, + DefaultImportDispatcher::default(), + is_debug_mode, + ) + .unwrap(); + let result = inst.call_export("test", []).unwrap().unwrap(); + + assert_eq!(result, Value::I32(expected)); +} + +#[test] +fn test_br() { + let start_fn_name = "test"; + let arena = Bump::new(); + let mut module = WasmModule::new(&arena); + + let signature = Signature { + param_types: bumpalo::vec![in &arena], + ret_type: Some(ValueType::I32), + }; + let local_types = [(1, ValueType::I32)]; + create_exported_function_with_locals( + &mut module, + start_fn_name, + signature, + &local_types, + |buf| { + // i32.const 111 + buf.push(OpCode::I32CONST as u8); + buf.encode_i32(111); + + // local.set 0 + buf.push(OpCode::SETLOCAL as u8); + buf.encode_u32(0); + + // block ;; label = @1 + buf.push(OpCode::BLOCK as u8); + buf.push(ValueType::VOID); + + // block ;; label = @2 + buf.push(OpCode::BLOCK as u8); + buf.push(ValueType::VOID); + + // block ;; label = @3 + buf.push(OpCode::BLOCK as u8); + buf.push(ValueType::VOID); + + // br 2 (;@1;) + buf.push(OpCode::BR as u8); + buf.encode_u32(2); + + // i32.const 444 + buf.push(OpCode::I32CONST as u8); + buf.encode_i32(444); + + // local.set 0 + buf.push(OpCode::SETLOCAL as u8); + buf.encode_u32(0); + + // end + buf.push(OpCode::END as u8); + + // i32.const 333 + buf.push(OpCode::I32CONST as u8); + buf.encode_i32(333); + + // local.set 0 + buf.push(OpCode::SETLOCAL as u8); + buf.encode_u32(0); + + // end + buf.push(OpCode::END as u8); + + // i32.const 222 + buf.push(OpCode::I32CONST as u8); + buf.encode_i32(222); + + // local.set 0 + buf.push(OpCode::SETLOCAL as u8); + buf.encode_u32(0); + + // end + buf.push(OpCode::END as u8); + + // local.get 0) + buf.push(OpCode::GETLOCAL as u8); + buf.encode_u32(0); + + buf.push(OpCode::END as u8); + }, + ); + + let is_debug_mode = false; + let mut inst = Instance::for_module( + &arena, + &module, + DefaultImportDispatcher::default(), + is_debug_mode, + ) + .unwrap(); + let result = inst.call_export(start_fn_name, []).unwrap().unwrap(); + + assert_eq!(result, Value::I32(111)) +} + +#[test] +fn test_br_if() { + test_br_if_help(0, 222); + test_br_if_help(1, 111); +} + +fn test_br_if_help(condition: i32, expected: i32) { + let start_fn_name = "test"; + let arena = Bump::new(); + let mut module = WasmModule::new(&arena); + + let signature = Signature { + param_types: bumpalo::vec![in &arena], + ret_type: Some(ValueType::I32), + }; + let local_types = [(1, ValueType::I32)]; + create_exported_function_with_locals( + &mut module, + start_fn_name, + signature, + &local_types, + |buf| { + // i32.const 111 + buf.push(OpCode::I32CONST as u8); + buf.encode_i32(111); + + // local.set 0 + buf.push(OpCode::SETLOCAL as u8); + buf.encode_u32(0); + + // block ;; label = @1 + buf.push(OpCode::BLOCK as u8); + buf.push(ValueType::VOID); + + // block ;; label = @2 + buf.push(OpCode::BLOCK as u8); + buf.push(ValueType::VOID); + + // block ;; label = @3 + buf.push(OpCode::BLOCK as u8); + buf.push(ValueType::VOID); + + // i32.const + buf.push(OpCode::I32CONST as u8); + buf.encode_i32(condition); + + // br_if 2 (;@1;) + buf.push(OpCode::BRIF as u8); + buf.encode_u32(2); + + // i32.const 444 + buf.push(OpCode::I32CONST as u8); + buf.encode_i32(444); + + // local.set 0 + buf.push(OpCode::SETLOCAL as u8); + buf.encode_u32(0); + + // end + buf.push(OpCode::END as u8); + + // i32.const 333 + buf.push(OpCode::I32CONST as u8); + buf.encode_i32(333); + + // local.set 0 + buf.push(OpCode::SETLOCAL as u8); + buf.encode_u32(0); + + // end + buf.push(OpCode::END as u8); + + // i32.const 222 + buf.push(OpCode::I32CONST as u8); + buf.encode_i32(222); + + // local.set 0 + buf.push(OpCode::SETLOCAL as u8); + buf.encode_u32(0); + + // end + buf.push(OpCode::END as u8); + + // local.get 0) + buf.push(OpCode::GETLOCAL as u8); + buf.encode_u32(0); + + buf.push(OpCode::END as u8); + }, + ); + + let is_debug_mode = true; + let mut inst = Instance::for_module( + &arena, + &module, + DefaultImportDispatcher::default(), + is_debug_mode, + ) + .unwrap(); + let result = inst.call_export(start_fn_name, []).unwrap().unwrap(); + + assert_eq!(result, Value::I32(expected)) +} + +#[test] +fn test_br_table() { + test_br_table_help(0, 333); + test_br_table_help(1, 222); + test_br_table_help(2, 111); +} + +fn test_br_table_help(condition: i32, expected: i32) { + let start_fn_name = "test"; + let arena = Bump::new(); + let mut module = WasmModule::new(&arena); + + let signature = Signature { + param_types: bumpalo::vec![in &arena], + ret_type: Some(ValueType::I32), + }; + let local_types = [(1, ValueType::I32)]; + create_exported_function_with_locals( + &mut module, + start_fn_name, + signature, + &local_types, + |buf| { + // i32.const 111 + buf.push(OpCode::I32CONST as u8); + buf.encode_i32(111); + + // local.set 0 + buf.push(OpCode::SETLOCAL as u8); + buf.encode_u32(0); + + // block ;; label = @1 + buf.push(OpCode::BLOCK as u8); + buf.push(ValueType::VOID); + + // block ;; label = @2 + buf.push(OpCode::BLOCK as u8); + buf.push(ValueType::VOID); + + // block ;; label = @3 + buf.push(OpCode::BLOCK as u8); + buf.push(ValueType::VOID); + + // i32.const + buf.push(OpCode::I32CONST as u8); + buf.encode_i32(condition); + + // br_table 0 1 2 (;@1;) + buf.push(OpCode::BRTABLE as u8); + buf.encode_u32(2); // number of non-fallback branches + buf.encode_u32(0); + buf.encode_u32(1); + buf.encode_u32(2); + + // end + buf.push(OpCode::END as u8); + + // i32.const 333 + buf.push(OpCode::I32CONST as u8); + buf.encode_i32(333); + + // local.set 0 + buf.push(OpCode::SETLOCAL as u8); + buf.encode_u32(0); + + // br 1 + buf.push(OpCode::BR as u8); + buf.encode_u32(1); + + // end + buf.push(OpCode::END as u8); + + // i32.const 222 + buf.push(OpCode::I32CONST as u8); + buf.encode_i32(222); + + // local.set 0 + buf.push(OpCode::SETLOCAL as u8); + buf.encode_u32(0); + + // br 0 + buf.push(OpCode::BR as u8); + buf.encode_u32(0); + + // end + buf.push(OpCode::END as u8); + + // local.get 0) + buf.push(OpCode::GETLOCAL as u8); + buf.encode_u32(0); + + buf.push(OpCode::END as u8); + }, + ); + + let is_debug_mode = false; + let mut inst = Instance::for_module( + &arena, + &module, + DefaultImportDispatcher::default(), + is_debug_mode, + ) + .unwrap(); + let result = inst.call_export(start_fn_name, []).unwrap().unwrap(); + + assert_eq!(result, Value::I32(expected)) +} + +struct TestDispatcher { + internal_state: i32, +} + +impl ImportDispatcher for TestDispatcher { + fn dispatch( + &mut self, + module_name: &str, + function_name: &str, + arguments: &[Value], + _memory: &mut [u8], + ) -> Option { + assert_eq!(module_name, "env"); + assert_eq!(function_name, "increment_state"); + assert_eq!(arguments.len(), 1); + let val = arguments[0].expect_i32().unwrap(); + self.internal_state += val; + Some(Value::I32(self.internal_state)) + } +} + +#[test] +fn test_call_import() { + let arena = Bump::new(); + let mut module = WasmModule::new(&arena); + let start_fn_name = "test"; + + // User-provided non-Wasm code, with state + let import_dispatcher = TestDispatcher { + internal_state: 100, + }; + + // Function 0 is the import + module.import.imports.push(Import { + module: "env", + name: "increment_state", + description: ImportDesc::Func { signature_index: 0 }, + }); + module.types.insert(Signature { + param_types: bumpalo::vec![in &arena; ValueType::I32], + ret_type: Some(ValueType::I32), + }); + + // Function 1, which calls the import + module.code.function_count = 1; + let func0_offset = module.code.bytes.len() as u32; + module.code.function_offsets.push(func0_offset); + module.add_function_signature(Signature { + param_types: Vec::new_in(&arena), + ret_type: Some(ValueType::I32), + }); + module.export.append(Export { + name: start_fn_name, + ty: ExportType::Func, + index: 1, + }); + [ + 0, // no locals + OpCode::I32CONST as u8, + 11, // argument to increment_state + OpCode::CALL as u8, + 0, // function 0 + OpCode::I32CONST as u8, + 12, // argument to increment_state + OpCode::CALL as u8, + 0, // function 0 + OpCode::I32ADD as u8, + OpCode::END as u8, + ] + .serialize(&mut module.code.bytes); + + if false { + let mut buf = Vec::new_in(&arena); + module.serialize(&mut buf); + let filename = "/tmp/roc/call-return.wasm"; + std::fs::write(filename, buf).unwrap(); + println!("Wrote to {filename}"); + } + + let mut inst = Instance::for_module(&arena, &module, import_dispatcher, true).unwrap(); + + let return_val = inst.call_export(start_fn_name, []).unwrap().unwrap(); + + assert_eq!(return_val, Value::I32(234)); +} + +#[test] +fn test_call_return_no_args() { + let arena = Bump::new(); + let mut module = WasmModule::new(&arena); + let start_fn_name = "test"; + + module.code.function_count = 2; + + // Function 0 + let func0_offset = module.code.bytes.len() as u32; + module.code.function_offsets.push(func0_offset); + module.add_function_signature(Signature { + param_types: Vec::new_in(&arena), + ret_type: Some(ValueType::I32), + }); + module.export.append(Export { + name: start_fn_name, + ty: ExportType::Func, + index: 0, + }); + [ + 1, // 1 group of locals + 1, // 1 local + ValueType::I32 as u8, + OpCode::BLOCK as u8, /* */ + // call from inside a block. callee's implicit return should still work correctly. + ValueType::VOID, + OpCode::CALL as u8, + 1, // function 1 + OpCode::SETLOCAL as u8, + 0, // local 0 + OpCode::END as u8, + OpCode::GETLOCAL as u8, + 0, // local 0 + OpCode::END as u8, + ] + .serialize(&mut module.code.bytes); + + // Function 1 + let func1_offset = module.code.bytes.len() as u32; + module.code.function_offsets.push(func1_offset); + module.add_function_signature(Signature { + param_types: Vec::new_in(&arena), + ret_type: Some(ValueType::I32), + }); + [ + 0, // no locals + OpCode::I32CONST as u8, + 42, // constant value (<64 so that LEB-128 is just one byte) + OpCode::END as u8, + ] + .serialize(&mut module.code.bytes); + + if false { + let mut buf = Vec::new_in(&arena); + module.serialize(&mut buf); + let filename = "/tmp/roc/call-return.wasm"; + std::fs::write(filename, buf).unwrap(); + println!("Wrote to {filename}"); + } + + let mut inst = + Instance::for_module(&arena, &module, DefaultImportDispatcher::default(), true).unwrap(); + + let return_val = inst.call_export(start_fn_name, []).unwrap().unwrap(); + + assert_eq!(return_val, Value::I32(42)); +} + +#[test] +fn test_call_return_with_args() { + let arena = Bump::new(); + let mut module = WasmModule::new(&arena); + + // Function 0: calculate 2+2 + let signature0 = Signature { + param_types: bumpalo::vec![in &arena], + ret_type: Some(ValueType::I32), + }; + create_exported_function_no_locals(&mut module, "two_plus_two", signature0, |buf| { + buf.push(OpCode::I32CONST as u8); + buf.push(2); + buf.push(OpCode::I32CONST as u8); + buf.push(2); + buf.push(OpCode::CALL as u8); + buf.push(1); + buf.push(OpCode::END as u8); + }); + + // Function 1: add two numbers + let func1_offset = module.code.bytes.len() as u32; + module.code.function_offsets.push(func1_offset); + module.add_function_signature(Signature { + param_types: bumpalo::vec![in &arena; ValueType::I32, ValueType::I32], + ret_type: Some(ValueType::I32), + }); + [ + 0, // no locals + OpCode::GETLOCAL as u8, + 0, + OpCode::GETLOCAL as u8, + 1, + OpCode::I32ADD as u8, + OpCode::END as u8, + ] + .serialize(&mut module.code.bytes); + + let signature0 = Signature { + param_types: bumpalo::vec![in &arena; ValueType::I32, ValueType::I32], + ret_type: Some(ValueType::I32), + }; + create_exported_function_no_locals(&mut module, "add", signature0, |buf| { + buf.push(OpCode::GETLOCAL as u8); + buf.push(0); + buf.push(OpCode::GETLOCAL as u8); + buf.push(1); + buf.push(OpCode::I32ADD as u8); + buf.push(OpCode::END as u8); + }); + + let mut inst = + Instance::for_module(&arena, &module, DefaultImportDispatcher::default(), false).unwrap(); + let result = inst.call_export("two_plus_two", []).unwrap().unwrap(); + + assert_eq!(result, Value::I32(4)); +} + +#[test] +fn test_call_indirect_ok() { + let result = test_call_indirect_help(0, 0); + assert_eq!(result, Value::I32(111)); +} + +#[test] +#[should_panic(expected = "Expected signature")] +fn test_call_indirect_wrong_signature() { + test_call_indirect_help(0, 1); +} + +#[test] +#[should_panic(expected = "element index")] +fn test_call_indirect_index_out_of_bounds() { + test_call_indirect_help(0, 2); +} + +#[test] +#[should_panic(expected = "Table index")] +fn test_call_indirect_unsupported_table() { + test_call_indirect_help(1, 0); +} + +fn test_call_indirect_help(table_index: u32, elem_index: u32) -> Value { + let arena = Bump::new(); + let mut module = WasmModule::new(&arena); + + let is_debug_mode = true; + let start_fn_name = "test"; + + // function 0: caller + let signature0 = || Signature { + param_types: bumpalo::vec![in &arena], + ret_type: Some(ValueType::I32), + }; + create_exported_function_no_locals(&mut module, start_fn_name, signature0(), |buf| { + buf.append_u8(OpCode::I32CONST as u8); + buf.encode_u32(elem_index); + buf.append_u8(OpCode::CALLINDIRECT as u8); + buf.encode_u32(0); // signature index + buf.encode_u32(table_index); + buf.append_u8(OpCode::END as u8); + }); + + // function 1: callee, right signature + create_exported_function_no_locals(&mut module, "callee1", signature0(), |buf| { + buf.append_u8(OpCode::I32CONST as u8); + buf.encode_i32(111); + buf.append_u8(OpCode::END as u8); + }); + + // function 2: callee, wrong signature + let signature1 = Signature { + param_types: bumpalo::vec![in &arena], + ret_type: Some(ValueType::F32), + }; + create_exported_function_no_locals(&mut module, "callee2", signature1, |buf| { + buf.append_u8(OpCode::F32CONST as u8); + buf.encode_f32(2.22); + buf.append_u8(OpCode::END as u8); + }); + + // Put functions 1 and 2 in the function table + module.element.segments.push(ElementSegment::new(&arena)); + assert_eq!(module.element.get_or_insert_fn(1), 0); + assert_eq!(module.element.get_or_insert_fn(2), 1); + + if false { + let mut outfile_buf = Vec::new_in(&arena); + module.serialize(&mut outfile_buf); + let filename = format!("/tmp/roc/call_indirect_{table_index}_{elem_index}.wasm"); + std::fs::write(&filename, outfile_buf).unwrap(); + println!("\nWrote to {filename}\n"); + } + + let mut inst = Instance::for_module( + &arena, + &module, + DefaultImportDispatcher::default(), + is_debug_mode, + ) + .unwrap(); + inst.call_export(start_fn_name, []).unwrap().unwrap() +} + +// #[test] +// fn test_drop() {} + +#[test] +fn test_select() { + test_select_help(Value::F32(1.11), Value::F32(2.22), -100, Value::F32(1.11)); + test_select_help(Value::F64(1.11), Value::F64(2.22), 0, Value::F64(2.22)); +} + +fn test_select_help(first: Value, second: Value, condition: i32, expected: Value) { + let arena = Bump::new(); + let mut module = WasmModule::new(&arena); + + // Function 0: calculate 2+2 + let signature0 = Signature { + param_types: bumpalo::vec![in &arena], + ret_type: Some(ValueType::from(expected)), + }; + create_exported_function_no_locals(&mut module, "test", signature0, |buf| { + const_value(buf, first); + const_value(buf, second); + const_value(buf, Value::I32(condition)); + buf.push(OpCode::SELECT as u8); + buf.push(OpCode::END as u8); + }); + + let mut inst = + Instance::for_module(&arena, &module, DefaultImportDispatcher::default(), false).unwrap(); + let result = inst.call_export("test", []).unwrap().unwrap(); + + assert_eq!(result, expected); +} + +#[test] +fn test_set_get_local() { + let arena = Bump::new(); + let mut inst = default_state(&arena); + let mut module = WasmModule::new(&arena); + + let mut buffer = vec![]; + let mut cursor = 0; + [ + (1u32, ValueType::F32), + (1u32, ValueType::F64), + (1u32, ValueType::I32), + (1u32, ValueType::I64), + ] + .serialize(&mut buffer); + + let fn_index = 0; + let return_addr = 0x1234; + let return_block_depth = 0; + let n_args = 0; + let ret_type = Some(ValueType::I32); + inst.current_frame = Frame::enter( + fn_index, + return_addr, + return_block_depth, + n_args, + ret_type, + &buffer, + &mut inst.value_store, + &mut cursor, + ); + + module.code.bytes.push(OpCode::I32CONST as u8); + module.code.bytes.encode_i32(12345); + module.code.bytes.push(OpCode::SETLOCAL as u8); + module.code.bytes.encode_u32(2); + + module.code.bytes.push(OpCode::GETLOCAL as u8); + module.code.bytes.encode_u32(2); + + inst.execute_next_instruction(&module).unwrap(); + inst.execute_next_instruction(&module).unwrap(); + inst.execute_next_instruction(&module).unwrap(); + assert_eq!(inst.value_store.depth(), 5); + assert_eq!(inst.value_store.pop(), Value::I32(12345)); +} + +#[test] +fn test_tee_get_local() { + let arena = Bump::new(); + let mut inst = default_state(&arena); + let mut module = WasmModule::new(&arena); + + let mut buffer = vec![]; + let mut cursor = 0; + [ + (1u32, ValueType::F32), + (1u32, ValueType::F64), + (1u32, ValueType::I32), + (1u32, ValueType::I64), + ] + .serialize(&mut buffer); + + let fn_index = 0; + let return_addr = 0x1234; + let return_block_depth = 0; + let n_args = 0; + let ret_type = Some(ValueType::I32); + inst.current_frame = Frame::enter( + fn_index, + return_addr, + return_block_depth, + n_args, + ret_type, + &buffer, + &mut inst.value_store, + &mut cursor, + ); + + module.code.bytes.push(OpCode::I32CONST as u8); + module.code.bytes.encode_i32(12345); + module.code.bytes.push(OpCode::TEELOCAL as u8); + module.code.bytes.encode_u32(2); + + module.code.bytes.push(OpCode::GETLOCAL as u8); + module.code.bytes.encode_u32(2); + + inst.execute_next_instruction(&module).unwrap(); + inst.execute_next_instruction(&module).unwrap(); + inst.execute_next_instruction(&module).unwrap(); + assert_eq!(inst.value_store.depth(), 6); + assert_eq!(inst.value_store.pop(), Value::I32(12345)); + assert_eq!(inst.value_store.pop(), Value::I32(12345)); +} + +#[test] +fn test_global() { + let arena = Bump::new(); + let mut state = default_state(&arena); + state + .globals + .extend_from_slice(&[Value::F64(1.11), Value::I32(222), Value::F64(3.33)]); + let mut module = WasmModule::new(&arena); + + module.code.bytes.push(OpCode::GETGLOBAL as u8); + module.code.bytes.encode_u32(1); + module.code.bytes.push(OpCode::I32CONST as u8); + module.code.bytes.encode_i32(555); + module.code.bytes.push(OpCode::SETGLOBAL as u8); + module.code.bytes.encode_u32(1); + module.code.bytes.push(OpCode::GETGLOBAL as u8); + module.code.bytes.encode_u32(1); + + state.execute_next_instruction(&module).unwrap(); + state.execute_next_instruction(&module).unwrap(); + state.execute_next_instruction(&module).unwrap(); + state.execute_next_instruction(&module).unwrap(); + assert_eq!(state.value_store.depth(), 2); + assert_eq!(state.value_store.pop(), Value::I32(555)); + assert_eq!(state.value_store.pop(), Value::I32(222)); +} + +#[test] +fn test_i32const() { + let arena = Bump::new(); + let mut state = default_state(&arena); + let mut module = WasmModule::new(&arena); + + module.code.bytes.push(OpCode::I32CONST as u8); + module.code.bytes.encode_i32(12345); + + state.execute_next_instruction(&module).unwrap(); + assert_eq!(state.value_store.pop(), Value::I32(12345)) +} + +#[test] +fn test_i64const() { + let arena = Bump::new(); + let mut state = default_state(&arena); + let mut module = WasmModule::new(&arena); + + module.code.bytes.push(OpCode::I64CONST as u8); + module.code.bytes.encode_i64(1234567890); + + state.execute_next_instruction(&module).unwrap(); + assert_eq!(state.value_store.pop(), Value::I64(1234567890)) +} + +#[test] +fn test_f32const() { + let arena = Bump::new(); + let mut state = default_state(&arena); + let mut module = WasmModule::new(&arena); + + module.code.bytes.push(OpCode::F32CONST as u8); + module.code.bytes.encode_f32(123.45); + + state.execute_next_instruction(&module).unwrap(); + assert_eq!(state.value_store.pop(), Value::F32(123.45)) +} + +#[test] +fn test_f64const() { + let arena = Bump::new(); + let mut state = default_state(&arena); + let mut module = WasmModule::new(&arena); + + module.code.bytes.push(OpCode::F64CONST as u8); + module.code.bytes.encode_f64(12345.67890); + + state.execute_next_instruction(&module).unwrap(); + assert_eq!(state.value_store.pop(), Value::F64(12345.67890)) +} diff --git a/crates/wasm_interp/src/tests/test_convert.rs b/crates/wasm_interp/src/tests/test_convert.rs new file mode 100644 index 0000000000..40b545a536 --- /dev/null +++ b/crates/wasm_interp/src/tests/test_convert.rs @@ -0,0 +1,294 @@ +#![cfg(test)] + +use super::test_op_example; +use roc_wasm_module::{opcodes::OpCode::*, Value}; + +#[test] +fn test_i32wrapi64() { + test_op_example( + I32WRAPI64, + [Value::I64(0x123456789)], + Value::I32(0x23456789), + ); +} + +#[test] +fn test_i32truncsf32() { + test_op_example(I32TRUNCSF32, [Value::F32(2.9)], Value::I32(2)); +} + +#[test] +#[should_panic(expected = "Cannot truncate")] +fn test_i32truncsf32_oob() { + test_op_example( + I32TRUNCSF32, + [Value::F32(i32::MAX as f32 * 2.0)], + Value::I32(i32::MIN), + ); +} + +#[test] +fn test_i32truncuf32() { + test_op_example( + I32TRUNCUF32, + [Value::F32(i32::MAX as f32 + 1.0)], + Value::I32(i32::MIN), + ); +} + +#[test] +#[should_panic(expected = "Cannot truncate")] +fn test_i32truncuf32_oob() { + test_op_example( + I32TRUNCUF32, + [Value::F32(u32::MAX as f32 * 2.0)], + Value::I32(0), + ); +} + +#[test] +fn test_i32truncsf64() { + test_op_example(I32TRUNCSF64, [Value::F64(2.9)], Value::I32(2)); +} + +#[test] +#[should_panic(expected = "Cannot truncate")] +fn test_i32truncsf64_oob() { + test_op_example( + I32TRUNCSF64, + [Value::F64(i32::MAX as f64 * 2.0)], + Value::I32(i32::MIN), + ); +} + +#[test] +fn test_i32truncuf64() { + test_op_example( + I32TRUNCUF64, + [Value::F64(i32::MAX as f64 + 1.0)], + Value::I32(i32::MIN), + ); +} + +#[test] +#[should_panic(expected = "Cannot truncate")] +fn test_i32truncuf64_oob() { + test_op_example( + I32TRUNCUF64, + [Value::F64(u32::MAX as f64 * 2.0)], + Value::I32(0), + ); +} + +#[test] +fn test_i64extendsi32() { + test_op_example(I64EXTENDSI32, [Value::I32(-1)], Value::I64(-1)); +} + +#[test] +fn test_i64extendui32() { + test_op_example(I64EXTENDUI32, [Value::I32(-1)], Value::I64(0xffff_ffff)); +} + +#[test] +fn test_i64truncsf32() { + test_op_example(I64TRUNCSF32, [Value::F32(2.9)], Value::I64(2)); +} + +#[test] +#[should_panic(expected = "Cannot truncate")] +fn test_i64truncsf32_oob() { + test_op_example( + I64TRUNCSF32, + [Value::F32(i64::MAX as f32 * 2.0)], + Value::I64(i64::MIN), + ); +} + +#[test] +fn test_i64truncuf32() { + test_op_example( + I64TRUNCUF32, + [Value::F32(i64::MAX as f32 + 1.0)], + Value::I64(i64::MIN), + ); +} + +#[test] +#[should_panic(expected = "Cannot truncate")] +fn test_i64truncuf32_oob() { + test_op_example( + I64TRUNCUF32, + [Value::F32(u64::MAX as f32 * 2.0)], + Value::I64(0), + ); +} + +#[test] +fn test_i64truncsf64() { + test_op_example(I64TRUNCSF64, [Value::F64(2.9)], Value::I64(2)); +} + +#[test] +#[should_panic(expected = "Cannot truncate")] +fn test_i64truncsf64_oob() { + test_op_example( + I64TRUNCSF64, + [Value::F64(i64::MAX as f64 * 2.0)], + Value::I64(i64::MIN), + ); +} + +#[test] +fn test_i64truncuf64() { + test_op_example( + I64TRUNCUF64, + [Value::F64(i64::MAX as f64 + 1.0)], + Value::I64(i64::MIN), + ); +} + +#[test] +#[should_panic(expected = "Cannot truncate")] +fn test_i64truncuf64_oob() { + test_op_example( + I32TRUNCUF64, + [Value::F64(u64::MAX as f64 * 2.0)], + Value::I32(0), + ); +} + +#[test] +fn test_f32convertsi32() { + test_op_example(F32CONVERTSI32, [Value::I32(-1)], Value::F32(-1.0)); +} + +#[test] +fn test_f32convertui32() { + test_op_example(F32CONVERTUI32, [Value::I32(-1)], Value::F32(4294967295.0)); +} + +#[test] +fn test_f32convertsi64() { + test_op_example(F32CONVERTSI64, [Value::I64(-1)], Value::F32(-1.0)); +} + +#[test] +fn test_f32convertui64() { + test_op_example(F32CONVERTUI64, [Value::I64(-1)], Value::F32(1.844_674_4e19)); +} + +#[test] +fn test_f32demotef64() { + test_op_example(F32DEMOTEF64, [Value::F64(12.375)], Value::F32(12.375)); +} + +#[test] +fn test_f64convertsi32() { + test_op_example(F64CONVERTSI32, [Value::I32(-1)], Value::F64(-1.0)); +} + +#[test] +fn test_f64convertui32() { + test_op_example(F64CONVERTUI32, [Value::I32(-1)], Value::F64(4294967295.0)); +} + +#[test] +fn test_f64convertsi64() { + test_op_example(F64CONVERTSI64, [Value::I64(-1)], Value::F64(-1.0)); +} + +#[test] +fn test_f64convertui64() { + test_op_example( + F64CONVERTUI64, + [Value::I64(-1)], + Value::F64(1.8446744073709552e19), + ); +} + +#[test] +fn test_f64promotef32() { + test_op_example(F64PROMOTEF32, [Value::F32(12.375)], Value::F64(12.375)); +} + +#[test] +fn test_i32reinterpretf32() { + test_op_example( + I32REINTERPRETF32, + [Value::F32(12.375)], + Value::I32(0x4146_0000), + ); +} + +#[test] +fn test_i64reinterpretf64() { + test_op_example( + I64REINTERPRETF64, + [Value::F64(0.01171875)], + Value::from(0x3F88_0000_0000_0000u64), + ); +} + +#[test] +fn test_f32reinterpreti32() { + test_op_example( + F32REINTERPRETI32, + [Value::I32(0x4146_0000)], + Value::F32(12.375), + ); +} + +#[test] +fn test_f64reinterpreti64() { + test_op_example( + F64REINTERPRETI64, + [Value::from(0x3F88_0000_0000_0000u64)], + Value::F64(0.01171875), + ); +} + +#[test] +fn test_i32extend8s() { + test_op_example( + I32EXTEND8S, + [Value::from(0xFFu32 as i32)], + Value::I32(0xFFFFFFFFu32 as i32), + ) +} + +#[test] +fn test_i32extend16s() { + test_op_example( + I32EXTEND16S, + [Value::from(0xFFFFu32)], + Value::I32(0xFFFFFFFFu32 as i32), + ) +} + +#[test] +fn test_i64extend8s() { + test_op_example( + I64EXTEND8S, + [Value::from(0xFFu64 as i64)], + Value::I64(u64::MAX as i64), + ) +} + +#[test] +fn test_i64extend16s() { + test_op_example( + I64EXTEND16S, + [Value::from(0xFFFFu64 as i64)], + Value::I64(u64::MAX as i64), + ) +} + +#[test] +fn test_i64extend32s() { + test_op_example( + I64EXTEND32S, + [Value::from(0xFFFFFFFFu64 as i64)], + Value::I64(u64::MAX as i64), + ) +} diff --git a/crates/wasm_interp/src/tests/test_f32.rs b/crates/wasm_interp/src/tests/test_f32.rs new file mode 100644 index 0000000000..bdbd555996 --- /dev/null +++ b/crates/wasm_interp/src/tests/test_f32.rs @@ -0,0 +1,185 @@ +#![cfg(test)] + +use super::test_op_example; +use roc_wasm_module::{opcodes::OpCode, opcodes::OpCode::*, Value}; + +fn test_f32_comparison(op: OpCode, arg1: f32, arg2: f32, expected: bool) { + test_op_example( + op, + [Value::F32(arg1), Value::F32(arg2)], + Value::I32(expected as i32), + ) +} + +fn test_f32_binop(op: OpCode, arg1: f32, arg2: f32, expected: f32) { + test_op_example( + op, + [Value::F32(arg1), Value::F32(arg2)], + Value::F32(expected), + ) +} + +fn test_f32_unop(op: OpCode, arg: f32, expected: f32) { + test_op_example(op, [Value::F32(arg)], Value::F32(expected)) +} + +#[test] +fn test_f32eq() { + let op = F32EQ; + test_f32_comparison(op, 1.1, 1.1, true); + test_f32_comparison(op, 1.1, -1.1, false); +} + +#[test] +fn test_f32ne() { + let op = F32NE; + test_f32_comparison(op, 1.1, 1.1, false); + test_f32_comparison(op, 1.1, -1.1, true); +} + +#[test] +fn test_f32lt() { + let op = F32LT; + test_f32_comparison(op, 1.1, 1.1, false); + test_f32_comparison(op, 1.1, -1.1, false); + test_f32_comparison(op, -1.1, 1.1, true); +} + +#[test] +fn test_f32gt() { + let op = F32GT; + test_f32_comparison(op, 1.1, 1.1, false); + test_f32_comparison(op, 1.1, -1.1, true); + test_f32_comparison(op, -1.1, 1.1, false); +} + +#[test] +fn test_f32le() { + let op = F32LE; + test_f32_comparison(op, 1.1, 1.1, true); + test_f32_comparison(op, 1.1, -1.1, false); + test_f32_comparison(op, -1.1, 1.1, true); +} + +#[test] +fn test_f32ge() { + let op = F32GE; + test_f32_comparison(op, 1.1, 1.1, true); + test_f32_comparison(op, 1.1, -1.1, true); + test_f32_comparison(op, -1.1, 1.1, false); +} + +#[test] +fn test_f32abs() { + let op = F32ABS; + test_f32_unop(op, 0.0, 0.0); + test_f32_unop(op, 1.1, 1.1); + test_f32_unop(op, -1.1, 1.1); +} + +#[test] +fn test_f32neg() { + let op = F32NEG; + test_f32_unop(op, 0.0, 0.0); + test_f32_unop(op, 1.1, -1.1); + test_f32_unop(op, -1.1, 1.1); +} + +#[test] +fn test_f32ceil() { + let op = F32CEIL; + test_f32_unop(op, 1.1, 2.0); + test_f32_unop(op, -1.1, -1.0); +} + +#[test] +fn test_f32floor() { + let op = F32FLOOR; + test_f32_unop(op, 1.1, 1.0); + test_f32_unop(op, -1.1, -2.0); +} + +#[test] +fn test_f32trunc() { + let op = F32TRUNC; + test_f32_unop(op, 1.1, 1.0); + test_f32_unop(op, -1.1, -1.0); +} + +#[test] +fn test_f32nearest() { + let op = F32NEAREST; + test_f32_unop(op, 1.4, 1.0); + test_f32_unop(op, 1.6, 2.0); + test_f32_unop(op, -1.4, -1.0); + test_f32_unop(op, -1.6, -2.0); + test_f32_unop(op, 1.5, 2.0); + test_f32_unop(op, 2.5, 2.0); +} + +#[test] +fn test_f32sqrt() { + let op = F32SQRT; + test_f32_unop(op, 4.0, 2.0); +} + +#[test] +fn test_f32add() { + let op = F32ADD; + test_f32_binop(op, 0.0, 0.0, 0.0); + test_f32_binop(op, -1.1, -1.1, -2.2); + test_f32_binop(op, 1.1, 1.1, 2.2); + test_f32_binop(op, 1.1, -1.1, 0.0); +} + +#[test] +fn test_f32sub() { + let op = F32SUB; + test_f32_binop(op, 0.0, 0.0, 0.0); + test_f32_binop(op, -1.1, -1.1, 0.0); + test_f32_binop(op, 1.1, -1.1, 2.2); +} + +#[test] +fn test_f32mul() { + let op = F32MUL; + test_f32_binop(op, 1.1, 0.0, 0.0); + test_f32_binop(op, -1.5, -1.5, 2.25); + test_f32_binop(op, 1.5, 1.5, 2.25); + test_f32_binop(op, 1.5, -1.5, -2.25); +} + +#[test] +fn test_f32div() { + let op = F32DIV; + test_f32_binop(op, -1.1, -1.1, 1.0); + test_f32_binop(op, 1.1, -1.1, -1.0); + test_f32_binop(op, 5.0, 2.0, 2.5); + + test_f32_binop(op, 1.0, 0.0, f32::INFINITY); + test_f32_binop(op, -1.0, 0.0, f32::NEG_INFINITY); + // test_f32_binop(op, 0.0, 0.0, f32::NAN); // can't check NaN for equality! LOL +} + +#[test] +fn test_f32min() { + let op = F32MIN; + test_f32_binop(op, 1.1, 2.2, 1.1); + test_f32_binop(op, -1.1, -2.2, -2.2); +} + +#[test] +fn test_f32max() { + let op = F32MAX; + test_f32_binop(op, 1.1, 2.2, 2.2); + test_f32_binop(op, -1.1, -2.2, -1.1); +} + +#[test] +fn test_f32copysign() { + let op = F32COPYSIGN; + test_f32_binop(op, 1.1, 2.2, 1.1); + test_f32_binop(op, -1.1, -2.2, -1.1); + test_f32_binop(op, -1.1, 1.1, 1.1); + test_f32_binop(op, 1.1, -1.1, -1.1); +} diff --git a/crates/wasm_interp/src/tests/test_f64.rs b/crates/wasm_interp/src/tests/test_f64.rs new file mode 100644 index 0000000000..eeadde304c --- /dev/null +++ b/crates/wasm_interp/src/tests/test_f64.rs @@ -0,0 +1,185 @@ +#![cfg(test)] + +use super::test_op_example; +use roc_wasm_module::{opcodes::OpCode, opcodes::OpCode::*, Value}; + +fn test_f64_comparison(op: OpCode, arg1: f64, arg2: f64, expected: bool) { + test_op_example( + op, + [Value::F64(arg1), Value::F64(arg2)], + Value::I32(expected as i32), + ) +} + +fn test_f64_binop(op: OpCode, arg1: f64, arg2: f64, expected: f64) { + test_op_example( + op, + [Value::F64(arg1), Value::F64(arg2)], + Value::F64(expected), + ) +} + +fn test_f64_unop(op: OpCode, arg: f64, expected: f64) { + test_op_example(op, [Value::F64(arg)], Value::F64(expected)) +} + +#[test] +fn test_f64eq() { + let op = F64EQ; + test_f64_comparison(op, 1.1, 1.1, true); + test_f64_comparison(op, 1.1, -1.1, false); +} + +#[test] +fn test_f64ne() { + let op = F64NE; + test_f64_comparison(op, 1.1, 1.1, false); + test_f64_comparison(op, 1.1, -1.1, true); +} + +#[test] +fn test_f64lt() { + let op = F64LT; + test_f64_comparison(op, 1.1, 1.1, false); + test_f64_comparison(op, 1.1, -1.1, false); + test_f64_comparison(op, -1.1, 1.1, true); +} + +#[test] +fn test_f64gt() { + let op = F64GT; + test_f64_comparison(op, 1.1, 1.1, false); + test_f64_comparison(op, 1.1, -1.1, true); + test_f64_comparison(op, -1.1, 1.1, false); +} + +#[test] +fn test_f64le() { + let op = F64LE; + test_f64_comparison(op, 1.1, 1.1, true); + test_f64_comparison(op, 1.1, -1.1, false); + test_f64_comparison(op, -1.1, 1.1, true); +} + +#[test] +fn test_f64ge() { + let op = F64GE; + test_f64_comparison(op, 1.1, 1.1, true); + test_f64_comparison(op, 1.1, -1.1, true); + test_f64_comparison(op, -1.1, 1.1, false); +} + +#[test] +fn test_f64abs() { + let op = F64ABS; + test_f64_unop(op, 0.0, 0.0); + test_f64_unop(op, 1.1, 1.1); + test_f64_unop(op, -1.1, 1.1); +} + +#[test] +fn test_f64neg() { + let op = F64NEG; + test_f64_unop(op, 0.0, 0.0); + test_f64_unop(op, 1.1, -1.1); + test_f64_unop(op, -1.1, 1.1); +} + +#[test] +fn test_f64ceil() { + let op = F64CEIL; + test_f64_unop(op, 1.1, 2.0); + test_f64_unop(op, -1.1, -1.0); +} + +#[test] +fn test_f64floor() { + let op = F64FLOOR; + test_f64_unop(op, 1.1, 1.0); + test_f64_unop(op, -1.1, -2.0); +} + +#[test] +fn test_f64trunc() { + let op = F64TRUNC; + test_f64_unop(op, 1.1, 1.0); + test_f64_unop(op, -1.1, -1.0); +} + +#[test] +fn test_f64nearest() { + let op = F64NEAREST; + test_f64_unop(op, 1.4, 1.0); + test_f64_unop(op, 1.6, 2.0); + test_f64_unop(op, -1.4, -1.0); + test_f64_unop(op, -1.6, -2.0); + test_f64_unop(op, 1.5, 2.0); + test_f64_unop(op, 2.5, 2.0); +} + +#[test] +fn test_f64sqrt() { + let op = F64SQRT; + test_f64_unop(op, 4.0, 2.0); +} + +#[test] +fn test_f64add() { + let op = F64ADD; + test_f64_binop(op, 0.0, 0.0, 0.0); + test_f64_binop(op, -1.1, -1.1, -2.2); + test_f64_binop(op, 1.1, 1.1, 2.2); + test_f64_binop(op, 1.1, -1.1, 0.0); +} + +#[test] +fn test_f64sub() { + let op = F64SUB; + test_f64_binop(op, 0.0, 0.0, 0.0); + test_f64_binop(op, -1.1, -1.1, 0.0); + test_f64_binop(op, 1.1, -1.1, 2.2); +} + +#[test] +fn test_f64mul() { + let op = F64MUL; + test_f64_binop(op, 1.1, 0.0, 0.0); + test_f64_binop(op, -1.5, -1.5, 2.25); + test_f64_binop(op, 1.5, 1.5, 2.25); + test_f64_binop(op, 1.5, -1.5, -2.25); +} + +#[test] +fn test_f64div() { + let op = F64DIV; + test_f64_binop(op, -1.1, -1.1, 1.0); + test_f64_binop(op, 1.1, -1.1, -1.0); + test_f64_binop(op, 5.0, 2.0, 2.5); + + test_f64_binop(op, 1.0, 0.0, f64::INFINITY); + test_f64_binop(op, -1.0, 0.0, f64::NEG_INFINITY); + // to-probably-never-do: check NaN. It needs its own special test setup. +} + +#[test] +fn test_f64min() { + let op = F64MIN; + test_f64_binop(op, 1.1, 2.2, 1.1); + test_f64_binop(op, -1.1, -2.2, -2.2); +} + +#[test] +fn test_f64max() { + let op = F64MAX; + test_f64_binop(op, 1.1, 2.2, 2.2); + test_f64_binop(op, -1.1, -2.2, -1.1); +} + +#[test] +fn test_f64copysign() { + let op = F64COPYSIGN; + test_f64_binop(op, 1.1, 2.2, 1.1); + test_f64_binop(op, -1.1, -2.2, -1.1); + test_f64_binop(op, -1.1, 1.1, 1.1); + test_f64_binop(op, 1.1, -1.1, -1.1); +} diff --git a/crates/wasm_interp/src/tests/test_i32.rs b/crates/wasm_interp/src/tests/test_i32.rs new file mode 100644 index 0000000000..32001c953a --- /dev/null +++ b/crates/wasm_interp/src/tests/test_i32.rs @@ -0,0 +1,285 @@ +#![cfg(test)] + +use super::test_op_example; +use roc_wasm_module::{opcodes::OpCode, opcodes::OpCode::*, Value}; + +fn test_i32_binop(op: OpCode, arg1: i32, arg2: i32, expected: i32) { + test_op_example( + op, + [Value::from(arg1), Value::from(arg2)], + Value::from(expected), + ) +} + +fn test_u32_binop(op: OpCode, arg1: u32, arg2: u32, expected: u32) { + test_op_example( + op, + [Value::from(arg1), Value::from(arg2)], + Value::from(expected), + ) +} + +fn test_i32_unop(op: OpCode, arg: i32, expected: i32) { + test_op_example(op, [Value::from(arg)], Value::from(expected)) +} + +#[test] +fn test_i32eqz() { + let op = I32EQZ; + test_i32_unop(op, 0, 1); + test_i32_unop(op, i32::MIN, 0); + test_i32_unop(op, i32::MAX, 0); +} + +#[test] +fn test_i32eq() { + let op = I32EQ; + test_i32_binop(op, 0, 0, 1); + test_i32_binop(op, i32::MIN, i32::MIN, 1); + test_i32_binop(op, i32::MAX, i32::MAX, 1); + test_i32_binop(op, i32::MIN, i32::MAX, 0); + test_i32_binop(op, i32::MAX, i32::MIN, 0); +} + +#[test] +fn test_i32ne() { + let op = I32NE; + test_i32_binop(op, 0, 0, 0); + test_i32_binop(op, i32::MIN, i32::MIN, 0); + test_i32_binop(op, i32::MAX, i32::MAX, 0); + test_i32_binop(op, i32::MIN, i32::MAX, 1); + test_i32_binop(op, i32::MAX, i32::MIN, 1); +} + +#[test] +fn test_i32lts() { + let op = I32LTS; + test_i32_binop(op, 0, 0, 0); + test_i32_binop(op, i32::MIN, i32::MIN, 0); + test_i32_binop(op, i32::MAX, i32::MAX, 0); + test_i32_binop(op, i32::MIN, i32::MAX, 1); + test_i32_binop(op, i32::MAX, i32::MIN, 0); +} + +#[test] +fn test_i32ltu() { + let op = I32LTU; + test_u32_binop(op, 0, 0, 0); + test_u32_binop(op, u32::MIN, u32::MIN, 0); + test_u32_binop(op, u32::MAX, u32::MAX, 0); + test_u32_binop(op, u32::MIN, u32::MAX, 1); + test_u32_binop(op, u32::MAX, u32::MIN, 0); +} + +#[test] +fn test_i32gts() { + let op = I32GTS; + test_i32_binop(op, 0, 0, 0); + test_i32_binop(op, i32::MIN, i32::MIN, 0); + test_i32_binop(op, i32::MAX, i32::MAX, 0); + test_i32_binop(op, i32::MIN, i32::MAX, 0); + test_i32_binop(op, i32::MAX, i32::MIN, 1); +} + +#[test] +fn test_i32gtu() { + let op = I32GTU; + test_u32_binop(op, 0, 0, 0); + test_u32_binop(op, u32::MIN, u32::MIN, 0); + test_u32_binop(op, u32::MAX, u32::MAX, 0); + test_u32_binop(op, u32::MIN, u32::MAX, 0); + test_u32_binop(op, u32::MAX, u32::MIN, 1); +} + +#[test] +fn test_i32les() { + let op = I32LES; + test_i32_binop(op, 0, 0, 1); + test_i32_binop(op, i32::MIN, i32::MIN, 1); + test_i32_binop(op, i32::MAX, i32::MAX, 1); + test_i32_binop(op, i32::MIN, i32::MAX, 1); + test_i32_binop(op, i32::MAX, i32::MIN, 0); +} + +#[test] +fn test_i32leu() { + let op = I32LEU; + test_u32_binop(op, 0, 0, 1); + test_u32_binop(op, u32::MIN, u32::MIN, 1); + test_u32_binop(op, u32::MAX, u32::MAX, 1); + test_u32_binop(op, u32::MIN, u32::MAX, 1); + test_u32_binop(op, u32::MAX, u32::MIN, 0); +} + +#[test] +fn test_i32ges() { + let op = I32GES; + test_i32_binop(op, 0, 0, 1); + test_i32_binop(op, i32::MIN, i32::MIN, 1); + test_i32_binop(op, i32::MAX, i32::MAX, 1); + test_i32_binop(op, i32::MIN, i32::MAX, 0); + test_i32_binop(op, i32::MAX, i32::MIN, 1); +} + +#[test] +fn test_i32geu() { + let op = I32GEU; + test_u32_binop(op, 0, 0, 1); + test_u32_binop(op, u32::MIN, u32::MIN, 1); + test_u32_binop(op, u32::MAX, u32::MAX, 1); + test_u32_binop(op, u32::MIN, u32::MAX, 0); + test_u32_binop(op, u32::MAX, u32::MIN, 1); +} + +#[test] +fn test_i32clz() { + let op = I32CLZ; + test_i32_unop(op, 0, 32); + test_i32_unop(op, -1, 0); + test_i32_unop(op, 1, 31); + test_i32_unop(op, 1024, 21); +} + +#[test] +fn test_i32ctz() { + let op = I32CTZ; + test_i32_unop(op, 0, 32); + test_i32_unop(op, -1, 0); + test_i32_unop(op, 2, 1); + test_i32_unop(op, 1024, 10); +} + +#[test] +fn test_i32popcnt() { + let op = I32POPCNT; + test_i32_unop(op, 0, 0); + test_i32_unop(op, -1, 32); + test_i32_unop(op, 2, 1); + test_i32_unop(op, 96, 2); +} + +#[test] +fn test_i32add() { + let op = I32ADD; + test_i32_binop(op, 0, 0, 0); + test_i32_binop(op, -1, -1, -2); + test_i32_binop(op, 1, 1, 2); + test_i32_binop(op, i32::MAX, 1, i32::MIN); +} + +#[test] +fn test_i32sub() { + let op = I32SUB; + test_i32_binop(op, 0, 0, 0); + test_i32_binop(op, -1, 1, -2); + test_i32_binop(op, 1, 1, 0); + test_i32_binop(op, i32::MIN, 1, i32::MAX); +} + +#[test] +fn test_i32mul() { + let op = I32MUL; + test_i32_binop(op, 0, 0, 0); + test_i32_binop(op, -1, -1, 1); + test_i32_binop(op, 2, 3, 6); + test_i32_binop(op, i32::MAX, 2, -2); +} + +#[test] +fn test_i32divs() { + let op = I32DIVS; + test_i32_binop(op, -1, -1, 1); + test_i32_binop(op, 6, 3, 2); + test_i32_binop(op, i32::MIN, -1, i32::MIN); +} + +#[test] +#[should_panic(expected = "divide by zero")] +fn test_i32divs_zero() { + test_i32_binop(I32DIVS, 1, 0, -1); +} + +#[test] +fn test_i32divu() { + let op = I32DIVU; + test_u32_binop(op, 1, 1, 1); + test_u32_binop(op, 6, 3, 2); +} + +#[test] +#[should_panic(expected = "divide by zero")] +fn test_i32divu_zero() { + test_i32_binop(I32DIVU, 1, 0, -1); +} + +#[test] +fn test_i32rems() { + let op = I32REMS; + test_i32_binop(op, 5, 2, 1); + // test_i32_binop(op, 5, -2, 1); // TODO: we don't match Wasmer, we get 0 + test_i32_binop(op, -5, 2, -1); + // test_i32_binop(op, -5, -2, -1); // TODO: we don't match Wasmer, we get 0 +} + +#[test] +#[should_panic(expected = "divisor of zero")] +fn test_i32rems_zero() { + test_i32_binop(I32REMS, 1, 0, -1); +} + +#[test] +fn test_i32remu() { + let op = I32REMU; + test_i32_binop(op, 5, 2, 1); +} + +#[test] +#[should_panic(expected = "divisor of zero")] +fn test_i32remu_zero() { + test_i32_binop(I32REMU, 1, 0, -1); +} + +#[test] +fn test_i32and() { + test_u32_binop(I32AND, 0x0000_ffff, 0x00ff_00ff, 0x0000_00ff); +} + +#[test] +fn test_i32or() { + test_u32_binop(I32OR, 0x0000_ffff, 0x00ff_00ff, 0x00ff_ffff); +} + +#[test] +fn test_i32xor() { + test_u32_binop(I32XOR, 0x0000_ffff, 0x00ff_00ff, 0x00ff_ff00); +} + +#[test] +fn test_i32shl() { + test_u32_binop(I32SHL, 0xffff_ffff, 8, 0xffff_ff00); + test_u32_binop(I32SHL, 0xffff_ffff, 40, 0xffff_ff00); +} + +#[test] +fn test_i32shrs() { + test_u32_binop(I32SHRS, 0xffff_0000, 8, 0xffff_ff00); + test_u32_binop(I32SHRS, 0xffff_0000, 40, 0xffff_ff00); +} + +#[test] +fn test_i32shru() { + test_u32_binop(I32SHRU, 0xffff_0000, 8, 0x00ff_ff00); + test_u32_binop(I32SHRU, 0xffff_0000, 40, 0x00ff_ff00); +} + +#[test] +fn test_i32rotl() { + test_u32_binop(I32ROTL, 0xff00_0000, 4, 0xf000_000f); + test_u32_binop(I32ROTL, 0xff00_0000, 36, 0xf000_000f); +} + +#[test] +fn test_i32rotr() { + test_u32_binop(I32ROTR, 0x0000_00ff, 4, 0xf000_000f); + test_u32_binop(I32ROTR, 0x0000_00ff, 36, 0xf000_000f); +} diff --git a/crates/wasm_interp/src/tests/test_i64.rs b/crates/wasm_interp/src/tests/test_i64.rs new file mode 100644 index 0000000000..03c414b8b7 --- /dev/null +++ b/crates/wasm_interp/src/tests/test_i64.rs @@ -0,0 +1,316 @@ +#![cfg(test)] + +use super::test_op_example; +use roc_wasm_module::{opcodes::OpCode, opcodes::OpCode::*, Value}; + +fn test_i64_comparison(op: OpCode, arg1: i64, arg2: i64, expected: bool) { + test_op_example( + op, + [Value::from(arg1), Value::from(arg2)], + Value::I32(expected as i32), + ) +} + +fn test_u64_comparison(op: OpCode, arg1: u64, arg2: u64, expected: bool) { + test_op_example( + op, + [Value::from(arg1), Value::from(arg2)], + Value::I32(expected as i32), + ) +} + +fn test_i64_binop(op: OpCode, arg1: i64, arg2: i64, expected: i64) { + test_op_example( + op, + [Value::from(arg1), Value::from(arg2)], + Value::from(expected), + ) +} + +fn test_u64_binop(op: OpCode, arg1: u64, arg2: u64, expected: u64) { + test_op_example( + op, + [Value::from(arg1), Value::from(arg2)], + Value::from(expected), + ) +} + +fn test_i64_unop(op: OpCode, arg: i64, expected: i64) { + test_op_example(op, [Value::from(arg)], Value::from(expected)) +} + +#[test] +fn test_i64eqz() { + let op = I64EQZ; + test_op_example(op, [Value::I64(0)], Value::I32(true as i32)); + test_op_example(op, [Value::I64(i64::MIN)], Value::I32(false as i32)); + test_op_example(op, [Value::I64(i64::MAX)], Value::I32(false as i32)); +} + +#[test] +fn test_i64eq() { + let op = I64EQ; + test_i64_comparison(op, 0, 0, true); + test_i64_comparison(op, i64::MIN, i64::MIN, true); + test_i64_comparison(op, i64::MAX, i64::MAX, true); + test_i64_comparison(op, i64::MIN, i64::MAX, false); + test_i64_comparison(op, i64::MAX, i64::MIN, false); +} + +#[test] +fn test_i64ne() { + let op = I64NE; + test_i64_comparison(op, 0, 0, false); + test_i64_comparison(op, i64::MIN, i64::MIN, false); + test_i64_comparison(op, i64::MAX, i64::MAX, false); + test_i64_comparison(op, i64::MIN, i64::MAX, true); + test_i64_comparison(op, i64::MAX, i64::MIN, true); +} + +#[test] +fn test_i64lts() { + let op = I64LTS; + test_i64_comparison(op, 0, 0, false); + test_i64_comparison(op, i64::MIN, i64::MIN, false); + test_i64_comparison(op, i64::MAX, i64::MAX, false); + test_i64_comparison(op, i64::MIN, i64::MAX, true); + test_i64_comparison(op, i64::MAX, i64::MIN, false); +} + +#[test] +fn test_i64ltu() { + let op = I64LTU; + test_u64_comparison(op, 0, 0, false); + test_u64_comparison(op, u64::MIN, u64::MIN, false); + test_u64_comparison(op, u64::MAX, u64::MAX, false); + test_u64_comparison(op, u64::MIN, u64::MAX, true); + test_u64_comparison(op, u64::MAX, u64::MIN, false); +} + +#[test] +fn test_i64gts() { + let op = I64GTS; + test_i64_comparison(op, 0, 0, false); + test_i64_comparison(op, i64::MIN, i64::MIN, false); + test_i64_comparison(op, i64::MAX, i64::MAX, false); + test_i64_comparison(op, i64::MIN, i64::MAX, false); + test_i64_comparison(op, i64::MAX, i64::MIN, true); +} + +#[test] +fn test_i64gtu() { + let op = I64GTU; + test_u64_comparison(op, 0, 0, false); + test_u64_comparison(op, u64::MIN, u64::MIN, false); + test_u64_comparison(op, u64::MAX, u64::MAX, false); + test_u64_comparison(op, u64::MIN, u64::MAX, false); + test_u64_comparison(op, u64::MAX, u64::MIN, true); +} + +#[test] +fn test_i64les() { + let op = I64LES; + test_i64_comparison(op, 0, 0, true); + test_i64_comparison(op, i64::MIN, i64::MIN, true); + test_i64_comparison(op, i64::MAX, i64::MAX, true); + test_i64_comparison(op, i64::MIN, i64::MAX, true); + test_i64_comparison(op, i64::MAX, i64::MIN, false); +} + +#[test] +fn test_i64leu() { + let op = I64LEU; + test_u64_comparison(op, 0, 0, true); + test_u64_comparison(op, u64::MIN, u64::MIN, true); + test_u64_comparison(op, u64::MAX, u64::MAX, true); + test_u64_comparison(op, u64::MIN, u64::MAX, true); + test_u64_comparison(op, u64::MAX, u64::MIN, false); +} + +#[test] +fn test_i64ges() { + let op = I64GES; + test_i64_comparison(op, 0, 0, true); + test_i64_comparison(op, i64::MIN, i64::MIN, true); + test_i64_comparison(op, i64::MAX, i64::MAX, true); + test_i64_comparison(op, i64::MIN, i64::MAX, false); + test_i64_comparison(op, i64::MAX, i64::MIN, true); +} + +#[test] +fn test_i64geu() { + let op = I64GEU; + test_u64_comparison(op, 0, 0, true); + test_u64_comparison(op, u64::MIN, u64::MIN, true); + test_u64_comparison(op, u64::MAX, u64::MAX, true); + test_u64_comparison(op, u64::MIN, u64::MAX, false); + test_u64_comparison(op, u64::MAX, u64::MIN, true); +} + +#[test] +fn test_i64clz() { + let op = I64CLZ; + test_i64_unop(op, 0, 64); + test_i64_unop(op, -1, 0); + test_i64_unop(op, 1, 63); + test_i64_unop(op, 1024, 53); +} + +#[test] +fn test_i64ctz() { + let op = I64CTZ; + test_i64_unop(op, 0, 64); + test_i64_unop(op, -1, 0); + test_i64_unop(op, 2, 1); + test_i64_unop(op, 1024, 10); +} + +#[test] +fn test_i64popcnt() { + let op = I64POPCNT; + test_i64_unop(op, 0, 0); + test_i64_unop(op, -1, 64); + test_i64_unop(op, 2, 1); + test_i64_unop(op, 96, 2); +} + +#[test] +fn test_i64add() { + let op = I64ADD; + test_i64_binop(op, 0, 0, 0); + test_i64_binop(op, -1, -1, -2); + test_i64_binop(op, 1, 1, 2); + test_i64_binop(op, i64::MAX, 1, i64::MIN); +} + +#[test] +fn test_i64sub() { + let op = I64SUB; + test_i64_binop(op, 0, 0, 0); + test_i64_binop(op, -1, 1, -2); + test_i64_binop(op, 1, 1, 0); + test_i64_binop(op, i64::MIN, 1, i64::MAX); +} + +#[test] +fn test_i64mul() { + let op = I64MUL; + test_i64_binop(op, 0, 0, 0); + test_i64_binop(op, -1, -1, 1); + test_i64_binop(op, 2, 3, 6); + test_i64_binop(op, i64::MAX, 2, -2); +} + +#[test] +fn test_i64divs() { + let op = I64DIVS; + test_i64_binop(op, -1, -1, 1); + test_i64_binop(op, 6, 3, 2); + test_i64_binop(op, i64::MIN, -1, i64::MIN); +} + +#[test] +#[should_panic(expected = "divide by zero")] +fn test_i64divs_zero() { + test_i64_binop(I64DIVS, 1, 0, -1); +} + +#[test] +fn test_i64divu() { + let op = I64DIVU; + test_u64_binop(op, 1, 1, 1); + test_u64_binop(op, 6, 3, 2); +} + +#[test] +#[should_panic(expected = "divide by zero")] +fn test_i64divu_zero() { + test_i64_binop(I64DIVU, 1, 0, -1); +} + +#[test] +fn test_i64rems() { + let op = I64REMS; + test_i64_binop(op, 5, 2, 1); + // test_i64_binop(op, 5, -2, 1); // TODO: we don't match Wasmer, we get 0 + test_i64_binop(op, -5, 2, -1); + // test_i64_binop(op, -5, -2, -1); // TODO: we don't match Wasmer, we get 0 +} + +#[test] +#[should_panic(expected = "divisor of zero")] +fn test_i64rems_zero() { + test_i64_binop(I64REMS, 1, 0, -1); +} + +#[test] +fn test_i64remu() { + let op = I64REMU; + test_i64_binop(op, 5, 2, 1); +} + +#[test] +#[should_panic(expected = "divisor of zero")] +fn test_i64remu_zero() { + test_i64_binop(I64REMU, 1, 0, -1); +} + +#[test] +fn test_i64and() { + test_u64_binop( + I64AND, + 0x0000_0000_ffff_ffff, + 0x0000_ffff_0000_ffff, + 0x0000_0000_0000_ffff, + ); +} + +#[test] +fn test_i64or() { + test_u64_binop( + I64OR, + 0x0000_0000_ffff_ffff, + 0x0000_ffff_0000_ffff, + 0x0000_ffff_ffff_ffff, + ); +} + +#[test] +fn test_i64xor() { + test_u64_binop( + I64XOR, + 0x0000_0000_ffff_ffff, + 0x0000_ffff_0000_ffff, + 0x0000_ffff_ffff_0000, + ); +} + +#[test] +fn test_i64shl() { + test_u64_binop(I64SHL, 0xffff_ffff_ffff_ffff, 8, 0xffff_ffff_ffff_ff00); + test_u64_binop(I64SHL, 0xffff_ffff_ffff_ffff, 72, 0xffff_ffff_ffff_ff00); +} + +#[test] +fn test_i64shrs() { + test_u64_binop(I64SHRS, 0xffff_ffff_0000_0000, 8, 0xffff_ffff_ff00_0000); + test_u64_binop(I64SHRS, 0xffff_ffff_0000_0000, 72, 0xffff_ffff_ff00_0000); +} + +#[test] +fn test_i64shru() { + test_u64_binop(I64SHRU, 0xffff_ffff_0000_0000, 8, 0x00ff_ffff_ff00_0000); + test_u64_binop(I64SHRU, 0xffff_ffff_0000_0000, 72, 0x00ff_ffff_ff00_0000); +} + +#[test] +fn test_i64rotl() { + test_u64_binop(I64ROTL, 0xff00_0000_0000_0000, 4, 0xf000_0000_0000_000f); + test_u64_binop(I64ROTL, 0xff00_0000_0000_0000, 68, 0xf000_0000_0000_000f); +} + +#[test] +fn test_i64rotr() { + test_u64_binop(I64ROTR, 0x0000_0000_0000_00ff, 4, 0xf000_0000_0000_000f); + test_u64_binop(I64ROTR, 0x0000_0000_0000_00ff, 68, 0xf000_0000_0000_000f); +} diff --git a/crates/wasm_interp/src/tests/test_mem.rs b/crates/wasm_interp/src/tests/test_mem.rs new file mode 100644 index 0000000000..9d24e0df0d --- /dev/null +++ b/crates/wasm_interp/src/tests/test_mem.rs @@ -0,0 +1,518 @@ +use super::create_exported_function_no_locals; +use crate::{DefaultImportDispatcher, Instance}; +use bumpalo::{collections::Vec, Bump}; +use roc_wasm_module::{ + opcodes::OpCode, + sections::{DataMode, DataSegment, MemorySection}, + ConstExpr, SerialBuffer, Signature, Value, ValueType, WasmModule, +}; + +#[test] +fn test_currentmemory() { + let arena = Bump::new(); + let mut module = WasmModule::new(&arena); + + let pages = 3; + let pc = 0; + module.memory = MemorySection::new(&arena, pages * MemorySection::PAGE_SIZE); + module.code.bytes.push(OpCode::CURRENTMEMORY as u8); + module.code.bytes.encode_i32(0); + + let mut state = Instance::new(&arena, pages, pc, [], DefaultImportDispatcher::default()); + state.execute_next_instruction(&module).unwrap(); + assert_eq!(state.value_store.pop(), Value::I32(3)) +} + +#[test] +fn test_growmemory() { + let arena = Bump::new(); + let mut module = WasmModule::new(&arena); + + let existing_pages = 3; + let grow_pages = 2; + let pc = 0; + module.memory = MemorySection::new(&arena, existing_pages * MemorySection::PAGE_SIZE); + module.code.bytes.push(OpCode::I32CONST as u8); + module.code.bytes.encode_i32(grow_pages); + module.code.bytes.push(OpCode::GROWMEMORY as u8); + module.code.bytes.encode_i32(0); + + let mut state = Instance::new( + &arena, + existing_pages, + pc, + [], + DefaultImportDispatcher::default(), + ); + state.execute_next_instruction(&module).unwrap(); + state.execute_next_instruction(&module).unwrap(); + assert_eq!(state.memory.len(), 5 * MemorySection::PAGE_SIZE as usize); +} + +#[test] +fn test_memory_fill() { + let arena = Bump::new(); + let mut module = WasmModule::new(&arena); + + let pages = 3; + let pc = 0; + module.memory = MemorySection::new(&arena, pages * MemorySection::PAGE_SIZE); + + const SIZE: i32 = 16; + let byte_value = 0xAA; + let destination = 0x4; + + let bytes = [OpCode::MEMORY as u8, 11, 0x0]; + module.code.bytes.extend(bytes); + + let mut state = Instance::new(&arena, pages, pc, [], DefaultImportDispatcher::default()); + + state.value_store.push(Value::I32(destination)); + state.value_store.push(Value::I32(byte_value)); + state.value_store.push(Value::I32(SIZE)); + + // before the instruction, the memory is all zeros + let memory_before = &state.memory[destination as usize..][..SIZE as usize]; + assert_eq!(memory_before, &[0; SIZE as usize]); + + state.execute_next_instruction(&module).unwrap(); + + // after the fill, the same memory range is now all 0xAA bytes + let memory_after = &state.memory[destination as usize..][..SIZE as usize]; + assert_eq!(memory_after, &[byte_value as u8; SIZE as usize]) +} + +#[test] +fn test_memory_copy() { + let arena = Bump::new(); + let mut module = WasmModule::new(&arena); + + let pages = 3; + let pc = 0; + module.memory = MemorySection::new(&arena, pages * MemorySection::PAGE_SIZE); + + const SIZE: i32 = 4; + let source = 0x4; + let destination = 0x8; + + let bytes = [OpCode::MEMORY as u8, 10, 0x0, 0x0]; + module.code.bytes.extend(bytes); + + let mut state = Instance::new(&arena, pages, pc, [], DefaultImportDispatcher::default()); + + state.value_store.push(Value::I32(destination)); + state.value_store.push(Value::I32(source)); + state.value_store.push(Value::I32(SIZE)); + + // fill the source slice with 0xAA bytes + let source_slice = &mut state.memory[source as usize..][..SIZE as usize]; + source_slice.fill(0xAA); + + // before the copy, the destination slice is all 0x00 bytes + let dest_slice = &state.memory[destination as usize..][..SIZE as usize]; + assert_eq!(dest_slice, &[0x00; SIZE as usize]); + + state.execute_next_instruction(&module).unwrap(); + + // after the copy, the destination slice is all 0xAA bytes + let dest_slice = &state.memory[destination as usize..][..SIZE as usize]; + assert_eq!(dest_slice, &[0xAA; SIZE as usize]) +} + +fn test_load(load_op: OpCode, ty: ValueType, data: &[u8], addr: u32, offset: u32) -> Value { + let arena = Bump::new(); + let mut module = WasmModule::new(&arena); + + let is_debug_mode = false; + let start_fn_name = "test"; + + module.memory = MemorySection::new(&arena, MemorySection::PAGE_SIZE); + + module.data.append_segment(DataSegment { + mode: DataMode::Active { + offset: ConstExpr::I32(addr as i32), + }, + init: Vec::from_iter_in(data.iter().copied(), &arena), + }); + + let signature = Signature { + param_types: bumpalo::vec![in &arena], + ret_type: Some(ty), + }; + + create_exported_function_no_locals(&mut module, start_fn_name, signature, |buf| { + buf.append_u8(OpCode::I32CONST as u8); + buf.encode_u32(addr); + buf.append_u8(load_op as u8); + buf.encode_u32(0); // align + buf.encode_u32(offset); + buf.append_u8(OpCode::END as u8); + }); + + if false { + let mut outfile_buf = Vec::new_in(&arena); + module.serialize(&mut outfile_buf); + std::fs::write("/tmp/roc/interp_load_test.wasm", outfile_buf).unwrap(); + } + + let mut inst = Instance::for_module( + &arena, + &module, + DefaultImportDispatcher::default(), + is_debug_mode, + ) + .unwrap(); + inst.call_export(start_fn_name, []).unwrap().unwrap() +} + +#[test] +fn test_i32load() { + let bytes = "abcdefgh".as_bytes(); + assert_eq!( + test_load(OpCode::I32LOAD, ValueType::I32, bytes, 0x11, 0), + Value::I32(0x64636261) + ); + assert_eq!( + test_load(OpCode::I32LOAD, ValueType::I32, bytes, 0x11, 2), + Value::I32(0x66656463) + ); +} + +#[test] +fn test_i64load() { + let bytes = "abcdefghijkl".as_bytes(); + assert_eq!( + test_load(OpCode::I64LOAD, ValueType::I64, bytes, 0x11, 0), + Value::I64(0x6867666564636261) + ); + assert_eq!( + test_load(OpCode::I64LOAD, ValueType::I64, bytes, 0x11, 2), + Value::I64(0x6a69686766656463) + ); +} + +#[test] +fn test_f32load() { + let value: f32 = 1.23456; + let bytes = value.to_le_bytes(); + assert_eq!( + test_load(OpCode::F32LOAD, ValueType::F32, &bytes, 0x11, 0), + Value::F32(value) + ); +} + +#[test] +fn test_f64load() { + let value: f64 = 1.23456; + let bytes = value.to_le_bytes(); + assert_eq!( + test_load(OpCode::F64LOAD, ValueType::F64, &bytes, 0x11, 0), + Value::F64(value) + ); +} + +#[test] +fn test_i32load8s() { + let value: i8 = -42; + let bytes = value.to_le_bytes(); + assert_eq!( + test_load(OpCode::I32LOAD8S, ValueType::I32, &bytes, 0x11, 0), + Value::I32(value as i32) + ); +} + +#[test] +fn test_i32load8u() { + let value: u8 = 42; + let bytes = value.to_le_bytes(); + assert_eq!( + test_load(OpCode::I32LOAD8U, ValueType::I32, &bytes, 0x11, 0), + Value::I32(value as i32) + ); +} + +#[test] +fn test_i32load16s() { + let value: i16 = -42; + let bytes = value.to_le_bytes(); + assert_eq!( + test_load(OpCode::I32LOAD16S, ValueType::I32, &bytes, 0x11, 0), + Value::I32(value as i32) + ); +} + +#[test] +fn test_i32load16u() { + let value: u16 = 42; + let bytes = value.to_le_bytes(); + assert_eq!( + test_load(OpCode::I32LOAD16U, ValueType::I32, &bytes, 0x11, 0), + Value::I32(value as i32) + ); +} + +#[test] +fn test_i64load8s() { + let value: i8 = -42; + let bytes = value.to_le_bytes(); + assert_eq!( + test_load(OpCode::I64LOAD8S, ValueType::I64, &bytes, 0x11, 0), + Value::I64(value as i64) + ); +} + +#[test] +fn test_i64load8u() { + let value: u8 = 42; + let bytes = value.to_le_bytes(); + assert_eq!( + test_load(OpCode::I32LOAD8U, ValueType::I32, &bytes, 0x11, 0), + Value::I32(value as i32) + ); +} + +#[test] +fn test_i64load16s() { + let value: i16 = -42; + let bytes = value.to_le_bytes(); + assert_eq!( + test_load(OpCode::I64LOAD8S, ValueType::I64, &bytes, 0x11, 0), + Value::I64(value as i64) + ); +} + +#[test] +fn test_i64load16u() { + let value: u16 = 42; + let bytes = value.to_le_bytes(); + assert_eq!( + test_load(OpCode::I32LOAD8U, ValueType::I32, &bytes, 0x11, 0), + Value::I32(value as i32) + ); +} + +#[test] +fn test_i64load32s() { + let value: i32 = -42; + let bytes = value.to_le_bytes(); + assert_eq!( + test_load(OpCode::I64LOAD8S, ValueType::I64, &bytes, 0x11, 0), + Value::I64(value as i64) + ); +} + +#[test] +fn test_i64load32u() { + let value: u32 = 42; + let bytes = value.to_le_bytes(); + assert_eq!( + test_load(OpCode::I32LOAD8U, ValueType::I32, &bytes, 0x11, 0), + Value::I32(value as i32) + ); +} + +fn test_store<'a>( + arena: &'a Bump, + module: &'a mut WasmModule<'a>, + addr: u32, + store_op: OpCode, + offset: u32, + value: Value, +) -> Vec<'a, u8> { + let start_fn_name = "test"; + + module.memory = MemorySection::new(arena, MemorySection::PAGE_SIZE); + + let signature = Signature { + param_types: bumpalo::vec![in arena], + ret_type: None, + }; + + create_exported_function_no_locals(module, start_fn_name, signature, |buf| { + buf.append_u8(OpCode::I32CONST as u8); + buf.encode_u32(addr); + match value { + Value::I32(x) => { + buf.append_u8(OpCode::I32CONST as u8); + buf.encode_i32(x); + } + Value::I64(x) => { + buf.append_u8(OpCode::I64CONST as u8); + buf.encode_i64(x); + } + Value::F32(x) => { + buf.append_u8(OpCode::F32CONST as u8); + buf.encode_f32(x); + } + Value::F64(x) => { + buf.append_u8(OpCode::F64CONST as u8); + buf.encode_f64(x); + } + } + buf.append_u8(store_op as u8); + buf.encode_u32(0); // align + buf.encode_u32(offset); + buf.append_u8(OpCode::END as u8); + }); + + let is_debug_mode = false; + let mut inst = Instance::for_module( + arena, + module, + DefaultImportDispatcher::default(), + is_debug_mode, + ) + .unwrap(); + inst.call_export(start_fn_name, []).unwrap(); + + inst.memory +} + +#[test] +fn test_i32store() { + let arena = Bump::new(); + let module = arena.alloc(WasmModule::new(&arena)); + + let addr: u32 = 0x11; + let store_op = OpCode::I32STORE; + let offset = 1; + let value = Value::I32(0x12345678); + let memory = test_store(&arena, module, addr, store_op, offset, value); + + let index = (addr + offset) as usize; + assert_eq!(&memory[index..][..4], &[0x78, 0x56, 0x34, 0x12]); +} + +#[test] +fn test_i64store() { + let arena = Bump::new(); + let module = arena.alloc(WasmModule::new(&arena)); + + let addr: u32 = 0x11; + let store_op = OpCode::I64STORE; + let offset = 1; + let value = Value::I64(0x123456789abcdef0); + let memory = test_store(&arena, module, addr, store_op, offset, value); + + let index = (addr + offset) as usize; + assert_eq!( + &memory[index..][..8], + &[0xf0, 0xde, 0xbc, 0x9a, 0x78, 0x56, 0x34, 0x12] + ); +} + +#[test] +fn test_f32store() { + let arena = Bump::new(); + let module = arena.alloc(WasmModule::new(&arena)); + + let addr: u32 = 0x11; + let store_op = OpCode::F32STORE; + let offset = 1; + let inner: f32 = 1.23456; + let value = Value::F32(inner); + let memory = test_store(&arena, module, addr, store_op, offset, value); + + let index = (addr + offset) as usize; + assert_eq!(&memory[index..][..4], &inner.to_le_bytes()); +} + +#[test] +fn test_f64store() { + let arena = Bump::new(); + let module = arena.alloc(WasmModule::new(&arena)); + + let addr: u32 = 0x11; + let store_op = OpCode::F64STORE; + let offset = 1; + let inner: f64 = 1.23456; + let value = Value::F64(inner); + let memory = test_store(&arena, module, addr, store_op, offset, value); + + let index = (addr + offset) as usize; + assert_eq!(&memory[index..][..8], &inner.to_le_bytes()); +} + +#[test] +fn test_i32store8() { + let arena = Bump::new(); + let module = arena.alloc(WasmModule::new(&arena)); + + let addr: u32 = 0x11; + let store_op = OpCode::I32STORE8; + let offset = 1; + let value = Value::I32(0x12345678); + let memory = test_store(&arena, module, addr, store_op, offset, value); + + let index = (addr + offset) as usize; + assert_eq!(&memory[index..][..4], &[0x78, 0x00, 0x00, 0x00]); +} + +#[test] +fn test_i32store16() { + let arena = Bump::new(); + let module = arena.alloc(WasmModule::new(&arena)); + + let addr: u32 = 0x11; + let store_op = OpCode::I32STORE16; + let offset = 1; + let value = Value::I32(0x12345678); + let memory = test_store(&arena, module, addr, store_op, offset, value); + + let index = (addr + offset) as usize; + assert_eq!(&memory[index..][..4], &[0x78, 0x56, 0x00, 0x00]); +} + +#[test] +fn test_i64store8() { + let arena = Bump::new(); + let module = arena.alloc(WasmModule::new(&arena)); + + let addr: u32 = 0x11; + let store_op = OpCode::I64STORE8; + let offset = 1; + let value = Value::I64(0x123456789abcdef0); + let memory = test_store(&arena, module, addr, store_op, offset, value); + + let index = (addr + offset) as usize; + assert_eq!( + &memory[index..][..8], + &[0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00] + ); +} + +#[test] +fn test_i64store16() { + let arena = Bump::new(); + let module = arena.alloc(WasmModule::new(&arena)); + + let addr: u32 = 0x11; + let store_op = OpCode::I64STORE16; + let offset = 1; + let value = Value::I64(0x123456789abcdef0); + let memory = test_store(&arena, module, addr, store_op, offset, value); + + let index = (addr + offset) as usize; + assert_eq!( + &memory[index..][..8], + &[0xf0, 0xde, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00] + ); +} + +#[test] +fn test_i64store32() { + let arena = Bump::new(); + let module = arena.alloc(WasmModule::new(&arena)); + + let addr: u32 = 0x11; + let store_op = OpCode::I64STORE32; + let offset = 1; + let value = Value::I64(0x123456789abcdef0); + let memory = test_store(&arena, module, addr, store_op, offset, value); + + let index = (addr + offset) as usize; + assert_eq!( + &memory[index..][..8], + &[0xf0, 0xde, 0xbc, 0x9a, 0x00, 0x00, 0x00, 0x00] + ); +} diff --git a/crates/wasm_interp/src/value_store.rs b/crates/wasm_interp/src/value_store.rs new file mode 100644 index 0000000000..50287827c5 --- /dev/null +++ b/crates/wasm_interp/src/value_store.rs @@ -0,0 +1,163 @@ +use bumpalo::{collections::Vec, Bump}; +use roc_wasm_module::{Value, ValueType}; +use std::fmt::Debug; + +use crate::Error; + +/// Combined storage for the Wasm stack machine and local variables. +/// +/// All values are mixed together so that on function calls, "moving" +/// arguments from the stack machine to local variables is a no-op +/// (or rather, just a matter of recording block metadata in the Instance). +/// +/// We use a simple Vec. When we tried more densely-packed SoA structures, +/// they were slower due to more logic, and harder to debug. +pub struct ValueStore<'a> { + values: Vec<'a, Value>, +} + +impl<'a> ValueStore<'a> { + pub(crate) fn new(arena: &'a Bump) -> Self { + ValueStore { + values: Vec::with_capacity_in(1024, arena), + } + } + + pub(crate) fn depth(&self) -> usize { + self.values.len() + } + + pub(crate) fn is_empty(&self) -> bool { + self.values.is_empty() + } + + pub(crate) fn push(&mut self, value: Value) { + self.values.push(value); + } + + pub(crate) fn pop(&mut self) -> Value { + self.values.pop().unwrap() + } + + pub(crate) fn peek(&self) -> Value { + *self.values.last().unwrap() + } + + pub(crate) fn get(&self, index: usize) -> Option<&Value> { + self.values.get(index) + } + + pub(crate) fn set(&mut self, index: usize, value: Value) { + self.values[index] = value; + } + + pub(crate) fn extend>(&mut self, values: I) { + self.values.extend(values) + } + + /// Memory addresses etc + pub(crate) fn pop_u32(&mut self) -> Result { + match self.values.pop() { + Some(Value::I32(x)) => Ok(u32::from_ne_bytes(x.to_ne_bytes())), + Some(bad) => Err(Error::Type(ValueType::I32, ValueType::from(bad))), + None => Err(Error::StackEmpty), + } + } + + pub(crate) fn pop_i32(&mut self) -> Result { + match self.values.pop() { + Some(Value::I32(x)) => Ok(x), + Some(bad) => Err(Error::Type(ValueType::I32, ValueType::from(bad))), + None => Err(Error::StackEmpty), + } + } + + pub(crate) fn pop_u64(&mut self) -> Result { + match self.values.pop() { + Some(Value::I64(x)) => Ok(u64::from_ne_bytes(x.to_ne_bytes())), + Some(bad) => Err(Error::Type(ValueType::I64, ValueType::from(bad))), + None => Err(Error::StackEmpty), + } + } + + pub(crate) fn pop_i64(&mut self) -> Result { + match self.values.pop() { + Some(Value::I64(x)) => Ok(x), + Some(bad) => Err(Error::Type(ValueType::I64, ValueType::from(bad))), + None => Err(Error::StackEmpty), + } + } + + pub(crate) fn pop_f32(&mut self) -> Result { + match self.values.pop() { + Some(Value::F32(x)) => Ok(x), + Some(bad) => Err(Error::Type(ValueType::F32, ValueType::from(bad))), + None => Err(Error::StackEmpty), + } + } + + pub(crate) fn pop_f64(&mut self) -> Result { + match self.values.pop() { + Some(Value::F64(x)) => Ok(x), + Some(bad) => Err(Error::Type(ValueType::F64, ValueType::from(bad))), + None => Err(Error::StackEmpty), + } + } + + pub(crate) fn iter(&self) -> std::slice::Iter { + self.values.iter() + } + + pub(crate) fn truncate(&mut self, depth: usize) { + self.values.truncate(depth) + } + + pub(crate) fn get_slice(&mut self, from: usize) -> &[Value] { + &self.values[from..] + } +} + +impl Debug for ValueStore<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", &self.values) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + const VALUES: [Value; 4] = [ + Value::I32(123), + Value::I64(123456), + Value::F32(1.01), + Value::F64(-1.1), + ]; + + #[test] + fn test_push_pop() { + let arena = Bump::new(); + let mut stack = ValueStore::new(&arena); + + for val in VALUES { + stack.push(val); + } + + for val in VALUES.iter().rev() { + let popped = stack.pop(); + assert_eq!(popped, *val); + } + } + + #[test] + fn test_debug_fmt() { + let arena = Bump::new(); + let mut stack = ValueStore::new(&arena); + + for val in VALUES { + stack.push(val); + } + + assert_eq!(format!("{VALUES:?}"), format!("{stack:?}")); + } +} diff --git a/crates/wasm_interp/src/wasi.rs b/crates/wasm_interp/src/wasi.rs new file mode 100644 index 0000000000..5b3d8f285a --- /dev/null +++ b/crates/wasm_interp/src/wasi.rs @@ -0,0 +1,519 @@ +use rand::prelude::*; +use roc_wasm_module::Value; +use std::io::{self, Read, StderrLock, StdoutLock, Write}; +use std::process::exit; + +pub const MODULE_NAME: &str = "wasi_snapshot_preview1"; + +pub struct WasiDispatcher<'a> { + pub args: &'a [&'a [u8]], + pub rng: ThreadRng, + pub files: Vec, +} + +impl Default for WasiDispatcher<'_> { + fn default() -> Self { + WasiDispatcher::new(&[]) + } +} + +pub enum WasiFile { + ReadOnly(Vec), + WriteOnly(Vec), + ReadWrite(Vec), + HostSystemFile, +} + +enum WriteLock<'a> { + StdOut(StdoutLock<'a>), + Stderr(StderrLock<'a>), + RegularFile(&'a mut Vec), +} + +/// Implementation of WASI syscalls +/// References for other engines: +/// https://github.com/wasmerio/wasmer/blob/ef8d2f651ed29b4b06fdc2070eb8189922c54d82/lib/wasi/src/syscalls/mod.rs +/// https://github.com/wasm3/wasm3/blob/045040a97345e636b8be4f3086e6db59cdcc785f/source/extra/wasi_core.h +impl<'a> WasiDispatcher<'a> { + pub fn new(args: &'a [&'a [u8]]) -> Self { + WasiDispatcher { + args, + rng: thread_rng(), + files: vec![ + WasiFile::HostSystemFile, + WasiFile::HostSystemFile, + WasiFile::HostSystemFile, + ], + } + } + + pub fn dispatch( + &mut self, + function_name: &str, + arguments: &[Value], + memory: &mut [u8], + ) -> Option { + let success_code = Some(Value::I32(Errno::Success as i32)); + match function_name { + "args_get" => { + // uint8_t ** argv, + let mut ptr_ptr_argv = arguments[0].expect_i32().unwrap() as usize; + // uint8_t * argv_buf + let mut ptr_argv_buf = arguments[1].expect_i32().unwrap() as usize; + + for arg in self.args { + write_u32(memory, ptr_ptr_argv, ptr_argv_buf as u32); + let bytes_target = &mut memory[ptr_argv_buf..][..arg.len()]; + bytes_target.copy_from_slice(arg); + memory[ptr_argv_buf + arg.len()] = 0; // C string zero termination + ptr_argv_buf += arg.len() + 1; + ptr_ptr_argv += 4; + } + + success_code + } + "args_sizes_get" => { + // (i32, i32) -> i32 + + // number of string arguments + let ptr_argc = arguments[0].expect_i32().unwrap() as usize; + // size of string arguments buffer + let ptr_argv_buf_size = arguments[1].expect_i32().unwrap() as usize; + + let argc = self.args.len() as u32; + write_u32(memory, ptr_argc, argc); + + let argv_buf_size: u32 = self.args.iter().map(|a| 1 + a.len() as u32).sum(); + write_u32(memory, ptr_argv_buf_size, argv_buf_size); + + success_code + } + "environ_get" => { + // `environ_sizes_get` always reports 0 environment variables + // so we don't have to do anything here. + + success_code + } + "environ_sizes_get" => { + let num_env_ptr = arguments[0].expect_i32().unwrap() as usize; + let size_env_ptr = arguments[1].expect_i32().unwrap() as usize; + + // Calculate the total size required for environment variables + let total_size = 0; + let count = 0; + + write_u32(memory, num_env_ptr, count); + write_u32(memory, size_env_ptr, total_size as u32); + + success_code + } + "clock_res_get" => success_code, // this dummy implementation seems to be good enough for some functions + "clock_time_get" => success_code, + "fd_advise" => todo!("WASI {}({:?})", function_name, arguments), + "fd_allocate" => todo!("WASI {}({:?})", function_name, arguments), + "fd_close" => todo!("WASI {}({:?})", function_name, arguments), + "fd_datasync" => todo!("WASI {}({:?})", function_name, arguments), + "fd_fdstat_get" => { + // (i32, i32) -> i32 + + // file descriptor + let fd = arguments[0].expect_i32().unwrap() as usize; + // ptr to a wasi_fdstat_t + let stat_mut_ptr = arguments[1].expect_i32().unwrap() as usize; + + match fd { + 1 => { + // Tell WASI that stdout is a tty (no seek or tell) + // https://github.com/WebAssembly/wasi-libc/blob/659ff414560721b1660a19685110e484a081c3d4/libc-bottom-half/sources/isatty.c + // *Not* a tty if: + // (statbuf.fs_filetype != __WASI_FILETYPE_CHARACTER_DEVICE || + // (statbuf.fs_rights_base & (__WASI_RIGHTS_FD_SEEK | __WASI_RIGHTS_FD_TELL)) != 0) + // So it's sufficient to set: + // .fs_filetype = __WASI_FILETYPE_CHARACTER_DEVICE + // .fs_rights_base = 0 + + const WASI_FILETYPE_CHARACTER_DEVICE: u8 = 2; + memory[stat_mut_ptr] = WASI_FILETYPE_CHARACTER_DEVICE; + + for b in memory[stat_mut_ptr + 1..stat_mut_ptr + 24].iter_mut() { + *b = 0; + } + } + _ => todo!("WASI {}({:?})", function_name, arguments), + } + + success_code + } + "fd_fdstat_set_flags" => todo!("WASI {}({:?})", function_name, arguments), + "fd_fdstat_set_rights" => todo!("WASI {}({:?})", function_name, arguments), + "fd_filestat_get" => todo!("WASI {}({:?})", function_name, arguments), + "fd_filestat_set_size" => todo!("WASI {}({:?})", function_name, arguments), + "fd_filestat_set_times" => todo!("WASI {}({:?})", function_name, arguments), + "fd_pread" => todo!("WASI {}({:?})", function_name, arguments), + "fd_prestat_get" => { + // The preopened file descriptor to query + let fd = arguments[0].expect_i32().unwrap() as usize; + // ptr_buf: Where the metadata will be written + // preopen type: 4 bytes, where 0=dir is the only one supported, it seems + // preopen name length: 4 bytes + let ptr_buf = arguments[1].expect_i32().unwrap() as usize; + memory[ptr_buf..][..8].copy_from_slice(&0u64.to_le_bytes()); + if fd < self.files.len() { + success_code + } else { + println!("WASI warning: file descriptor {fd} does not exist"); + Some(Value::I32(Errno::Badf as i32)) + } + } + "fd_prestat_dir_name" => { + // We're not giving names to any of our files so just return success + success_code + } + "fd_pwrite" => todo!("WASI {}({:?})", function_name, arguments), + "fd_read" => { + use WasiFile::*; + + // file descriptor + let fd = arguments[0].expect_i32().unwrap() as usize; + // Array of IO vectors + let ptr_iovs = arguments[1].expect_i32().unwrap() as usize; + // Length of array + let iovs_len = arguments[2].expect_i32().unwrap(); + // Out param: number of bytes read + let ptr_nread = arguments[3].expect_i32().unwrap() as usize; + + // https://man7.org/linux/man-pages/man2/readv.2.html + // struct iovec { + // void *iov_base; /* Starting address */ + // size_t iov_len; /* Number of bytes to transfer */ + // }; + + let mut n_read: usize = 0; + match self.files.get(fd) { + Some(ReadOnly(content) | ReadWrite(content)) => { + for _ in 0..iovs_len { + let iov_base = read_u32(memory, ptr_iovs) as usize; + let iov_len = read_i32(memory, ptr_iovs + 4) as usize; + let remaining = content.len() - n_read; + let len = remaining.min(iov_len); + if len == 0 { + break; + } + memory[iov_base..][..len].copy_from_slice(&content[n_read..][..len]); + n_read += len; + } + } + Some(HostSystemFile) if fd == 0 => { + let mut stdin = io::stdin(); + for _ in 0..iovs_len { + let iov_base = read_u32(memory, ptr_iovs) as usize; + let iov_len = read_i32(memory, ptr_iovs + 4) as usize; + match stdin.read(&mut memory[iov_base..][..iov_len]) { + Ok(n) => { + n_read += n; + } + Err(_) => { + break; + } + } + } + } + _ => return Some(Value::I32(Errno::Badf as i32)), + }; + + memory[ptr_nread..][..4].copy_from_slice(&(n_read as u32).to_le_bytes()); + success_code + } + "fd_readdir" => todo!("WASI {}({:?})", function_name, arguments), + "fd_renumber" => todo!("WASI {}({:?})", function_name, arguments), + "fd_seek" => todo!("WASI {}({:?})", function_name, arguments), + "fd_sync" => todo!("WASI {}({:?})", function_name, arguments), + "fd_tell" => todo!("WASI {}({:?})", function_name, arguments), + "fd_write" => { + use WasiFile::*; + + // file descriptor + let fd = arguments[0].expect_i32().unwrap() as usize; + // Array of IO vectors + let ptr_iovs = arguments[1].expect_i32().unwrap() as usize; + // Length of array + let iovs_len = arguments[2].expect_i32().unwrap(); + // Out param: number of bytes written + let ptr_nwritten = arguments[3].expect_i32().unwrap() as usize; + + // Grab a lock for stdout/stderr before the loop rather than re-acquiring over and over. + // Not really necessary for other files, but it's easier to use the same structure. + let mut write_lock = match self.files.get_mut(fd) { + Some(HostSystemFile) => match fd { + 1 => WriteLock::StdOut(io::stdout().lock()), + 2 => WriteLock::Stderr(io::stderr().lock()), + _ => return Some(Value::I32(Errno::Inval as i32)), + }, + Some(WriteOnly(content) | ReadWrite(content)) => { + WriteLock::RegularFile(content) + } + _ => return Some(Value::I32(Errno::Badf as i32)), + }; + + let mut n_written: i32 = 0; + let mut negative_length_count = 0; + let mut write_result = Ok(()); + for i in 0..iovs_len { + // https://man7.org/linux/man-pages/man2/readv.2.html + // struct iovec { + // void *iov_base; /* Starting address */ + // size_t iov_len; /* Number of bytes to transfer */ + // }; + let ptr_iov = ptr_iovs + (8 * i as usize); // index into the array of iovec's + let iov_base = read_u32(memory, ptr_iov) as usize; + let iov_len = read_i32(memory, ptr_iov + 4); + if iov_len < 0 { + // I found negative-length iov's when I implemented this in JS for the web REPL (see wasi.js) + // I'm not sure why, but this solution worked, and it's the same WASI libc - there's only one. + n_written += iov_len; + negative_length_count += 1; + continue; + } + let bytes = &memory[iov_base..][..iov_len as usize]; + + write_result = match &mut write_lock { + WriteLock::StdOut(stdout) => stdout.write_all(bytes), + WriteLock::Stderr(stderr) => stderr.write_all(bytes), + WriteLock::RegularFile(content) => content.write_all(bytes), + }; + if write_result.is_err() { + break; + } + n_written += bytes.len() as i32; + } + + write_i32(memory, ptr_nwritten, n_written); + if negative_length_count > 0 { + // Let's see if we ever get this message. If not, we can remove this negative-length stuff. + eprintln!( + "WASI DEV INFO: found {negative_length_count} negative-length iovecs." + ); + } + + match write_result { + Ok(()) => success_code, + Err(_) => Some(Value::I32(Errno::Io as i32)), + } + } + "path_create_directory" => todo!("WASI {}({:?})", function_name, arguments), + "path_filestat_get" => todo!("WASI {}({:?})", function_name, arguments), + "path_filestat_set_times" => todo!("WASI {}({:?})", function_name, arguments), + "path_link" => todo!("WASI {}({:?})", function_name, arguments), + "path_open" => todo!("WASI {}({:?})", function_name, arguments), + "path_readlink" => todo!("WASI {}({:?})", function_name, arguments), + "path_remove_directory" => todo!("WASI {}({:?})", function_name, arguments), + "path_rename" => todo!("WASI {}({:?})", function_name, arguments), + "path_symlink" => todo!("WASI {}({:?})", function_name, arguments), + "path_unlink_file" => todo!("WASI {}({:?})", function_name, arguments), + "poll_oneoff" => todo!("WASI {}({:?})", function_name, arguments), + "proc_exit" => { + let exit_code = arguments[0].expect_i32().unwrap(); + exit(exit_code); + } + "proc_raise" => todo!("WASI {}({:?})", function_name, arguments), + "sched_yield" => todo!("WASI {}({:?})", function_name, arguments), + "random_get" => { + // A pointer to a buffer where the random bytes will be written + let ptr_buf = arguments[0].expect_i32().unwrap() as usize; + // The number of bytes that will be written + let buf_len = arguments[1].expect_i32().unwrap() as usize; + for i in 0..buf_len { + memory[ptr_buf + i] = self.rng.gen(); + } + success_code + } + "sock_recv" => todo!("WASI {}({:?})", function_name, arguments), + "sock_send" => todo!("WASI {}({:?})", function_name, arguments), + "sock_shutdown" => todo!("WASI {}({:?})", function_name, arguments), + _ => panic!("Unknown WASI function {function_name}({arguments:?})"), + } + } +} + +fn read_u32(memory: &[u8], addr: usize) -> u32 { + let mut bytes = [0; 4]; + bytes.copy_from_slice(&memory[addr..][..4]); + u32::from_le_bytes(bytes) +} + +fn read_i32(memory: &[u8], addr: usize) -> i32 { + let mut bytes = [0; 4]; + bytes.copy_from_slice(&memory[addr..][..4]); + i32::from_le_bytes(bytes) +} + +fn write_u32(memory: &mut [u8], addr: usize, value: u32) { + memory[addr..][..4].copy_from_slice(&value.to_le_bytes()); +} + +fn write_i32(memory: &mut [u8], addr: usize, value: i32) { + memory[addr..][..4].copy_from_slice(&value.to_le_bytes()); +} + +/// Error codes returned by functions. +/// Not all of these error codes are returned by the functions provided by this +/// API; some are used in higher-level library layers, and others are provided +/// merely for alignment with POSIX. +#[repr(u8)] +#[derive(Clone, Copy, PartialEq, Eq)] +pub enum Errno { + /// No error occurred. System call completed successfully. + Success, + /// Argument list too long. + Toobig, + /// Permission denied. + Access, + /// Address in use. + Addrinuse, + /// Address not available. + Addrnotavail, + /// Address family not supported. + Afnosupport, + /// Resource unavailable, or operation would block. + Again, + /// Connection already in progress. + Already, + /// Bad file descriptor. + Badf, + /// Bad message. + Badmsg, + /// Device or resource busy. + Busy, + /// Operation canceled. + Canceled, + /// No child processes. + Child, + /// Connection aborted. + Connaborted, + /// Connection refused. + Connrefused, + /// Connection reset. + Connreset, + /// Resource deadlock would occur. + Deadlk, + /// Destination address required. + Destaddrreq, + /// Mathematics argument out of domain of function. + Dom, + /// Reserved. + Dquot, + /// File exists. + Exist, + /// Bad address. + Fault, + /// File too large. + Fbig, + /// Host is unreachable. + Hostunreach, + /// Identifier removed. + Idrm, + /// Illegal byte sequence. + Ilseq, + /// Operation in progress. + Inprogress, + /// Interrupted function. + Intr, + /// Invalid argument. + Inval, + /// I/O error. + Io, + /// Socket is connected. + Isconn, + /// Is a directory. + Isdir, + /// Too many levels of symbolic links. + Loop, + /// File descriptor value too large. + Mfile, + /// Too many links. + Mlink, + /// Message too large. + Msgsize, + /// Reserved. + Multihop, + /// Filename too long. + Nametoolong, + /// Network is down. + Netdown, + /// Connection aborted by network. + Netreset, + /// Network unreachable. + Netunreach, + /// Too many files open in system. + Nfile, + /// No buffer space available. + Nobufs, + /// No such device. + Nodev, + /// No such file or directory. + Noent, + /// Executable file format error. + Noexec, + /// No locks available. + Nolck, + /// Reserved. + Nolink, + /// Not enough space. + Nomem, + /// No message of the desired type. + Nomsg, + /// Protocol not available. + Noprotoopt, + /// No space left on device. + Nospc, + /// Function not supported. + Nosys, + /// The socket is not connected. + Notconn, + /// Not a directory or a symbolic link to a directory. + Notdir, + /// Directory not empty. + Notempty, + /// State not recoverable. + Notrecoverable, + /// Not a socket. + Notsock, + /// Not supported, or operation not supported on socket. + Notsup, + /// Inappropriate I/O control operation. + Notty, + /// No such device or address. + Nxio, + /// Value too large to be stored in data type. + Overflow, + /// Previous owner died. + Ownerdead, + /// Operation not permitted. + Perm, + /// Broken pipe. + Pipe, + /// Protocol error. + Proto, + /// Protocol not supported. + Protonosupport, + /// Protocol wrong type for socket. + Prototype, + /// Result too large. + Range, + /// Read-only file system. + Rofs, + /// Invalid seek. + Spipe, + /// No such process. + Srch, + /// Reserved. + Stale, + /// Connection timed out. + Timedout, + /// Text file busy. + Txtbsy, + /// Cross-device link. + Xdev, + /// Extension: Capabilities insufficient. + Notcapable, +} diff --git a/crates/wasm_module/Cargo.toml b/crates/wasm_module/Cargo.toml new file mode 100644 index 0000000000..0ff4298cdd --- /dev/null +++ b/crates/wasm_module/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "roc_wasm_module" +description = "Parse, manipulate, and serialize WebAssembly modules." + +authors.workspace = true +edition.workspace = true +license.workspace = true +version.workspace = true + +[dependencies] +roc_error_macros = { path = "../error_macros" } + +bitvec.workspace = true +bumpalo.workspace = true diff --git a/crates/wasm_module/src/lib.rs b/crates/wasm_module/src/lib.rs new file mode 100644 index 0000000000..f29f413e53 --- /dev/null +++ b/crates/wasm_module/src/lib.rs @@ -0,0 +1,913 @@ +pub mod linking; +pub mod opcodes; +pub mod parse; +pub mod sections; +pub mod serialize; + +use std::iter::repeat; + +pub use linking::{OffsetRelocType, RelocationEntry, SymInfo}; +use opcodes::OpCode; +use roc_error_macros::internal_error; +pub use sections::{ConstExpr, Export, ExportType, Global, GlobalType, Signature}; + +use bitvec::vec::BitVec; +use bumpalo::{collections::Vec, Bump}; + +use self::linking::{IndexRelocType, LinkingSection, RelocationSection, WasmObjectSymbol}; +use self::parse::{Parse, ParseError}; +use self::sections::{ + CodeSection, DataSection, ElementSection, ExportSection, FunctionSection, GlobalSection, + ImportDesc, ImportSection, MemorySection, NameSection, OpaqueSection, Section, SectionId, + TableSection, TypeSection, +}; +pub use self::serialize::{SerialBuffer, Serialize}; + +pub const STACK_POINTER_GLOBAL_ID: u32 = 0; +pub const FRAME_ALIGNMENT_BYTES: i32 = 16; + +/// A representation of the WebAssembly binary file format +/// https://webassembly.github.io/spec/core/binary/modules.html +#[derive(Debug)] +pub struct WasmModule<'a> { + pub types: TypeSection<'a>, + pub import: ImportSection<'a>, + pub function: FunctionSection<'a>, + pub table: TableSection, + pub memory: MemorySection<'a>, + pub global: GlobalSection<'a>, + pub export: ExportSection<'a>, + pub start: OpaqueSection<'a>, + pub element: ElementSection<'a>, + pub code: CodeSection<'a>, + pub data: DataSection<'a>, + pub linking: LinkingSection<'a>, + pub reloc_code: RelocationSection<'a>, + pub reloc_data: RelocationSection<'a>, + pub names: NameSection<'a>, +} + +impl<'a> WasmModule<'a> { + pub const WASM_VERSION: u32 = 1; + + pub fn new(arena: &'a Bump) -> Self { + WasmModule { + types: TypeSection::new(arena), + import: ImportSection::new(arena), + function: FunctionSection::new(arena), + table: TableSection::new(), + memory: MemorySection::new(arena, 0), + global: GlobalSection::new(arena), + export: ExportSection::new(arena), + start: OpaqueSection::new(), + element: ElementSection::new(arena), + code: CodeSection::new(arena), + data: DataSection::new(arena), + linking: LinkingSection::new(arena), + reloc_code: RelocationSection::new(arena, "reloc.CODE"), + reloc_data: RelocationSection::new(arena, "reloc.DATA"), + names: NameSection::new(arena), + } + } + + /// Create entries in the Type and Function sections for a function signature + pub fn add_function_signature(&mut self, signature: Signature<'a>) { + let index = self.types.insert(signature); + self.function.add_sig(index); + } + + /// Serialize the module to bytes + pub fn serialize(&self, buffer: &mut T) { + buffer.append_u8(0); + buffer.append_slice("asm".as_bytes()); + buffer.write_unencoded_u32(Self::WASM_VERSION); + + self.types.serialize(buffer); + self.import.serialize(buffer); + self.function.serialize(buffer); + if !self.element.is_empty() { + self.table.serialize(buffer); + } + self.memory.serialize(buffer); + self.global.serialize(buffer); + self.export.serialize(buffer); + self.start.serialize(buffer); + self.element.serialize(buffer); + self.code.serialize(buffer); + self.data.serialize(buffer); + self.names.serialize(buffer); + } + + /// Module size in bytes (assuming no linker data) + /// May be slightly overestimated. Intended for allocating buffer capacity. + pub fn size(&self) -> usize { + self.types.size() + + self.import.size() + + self.function.size() + + self.table.size() + + self.memory.size() + + self.global.size() + + self.export.size() + + self.start.size() + + self.element.size() + + self.code.size() + + self.data.size() + + self.names.size() + } + + pub fn preload( + arena: &'a Bump, + bytes: &[u8], + require_relocatable: bool, + ) -> Result { + let is_valid_magic_number = &bytes[0..4] == "\0asm".as_bytes(); + let is_valid_version = bytes[4..8] == Self::WASM_VERSION.to_le_bytes(); + if !is_valid_magic_number || !is_valid_version { + return Err(ParseError { + offset: 0, + message: "This file is not a WebAssembly binary. The file header is not valid." + .into(), + }); + } + + let mut cursor: usize = 8; + + let types = TypeSection::parse(arena, bytes, &mut cursor)?; + let import = ImportSection::parse(arena, bytes, &mut cursor)?; + let function = FunctionSection::parse(arena, bytes, &mut cursor)?; + let table = TableSection::parse((), bytes, &mut cursor)?; + let memory = MemorySection::parse(arena, bytes, &mut cursor)?; + let global = GlobalSection::parse(arena, bytes, &mut cursor)?; + let export = ExportSection::parse(arena, bytes, &mut cursor)?; + let start = OpaqueSection::parse((arena, SectionId::Start), bytes, &mut cursor)?; + let element = ElementSection::parse(arena, bytes, &mut cursor)?; + let _data_count = OpaqueSection::parse((arena, SectionId::DataCount), bytes, &mut cursor)?; + let code = CodeSection::parse(arena, bytes, &mut cursor)?; + let data = DataSection::parse(arena, bytes, &mut cursor)?; + + // Initialise the Custom sections that we care about. All empty. + let mut linking = LinkingSection::new(arena); + let mut reloc_code = RelocationSection::new(arena, "reloc.CODE"); + let mut reloc_data = RelocationSection::new(arena, "reloc.DATA"); + let mut names = NameSection::new(arena); + + // Consume all remaining Custom sections + while let Ok((section_name, section_end)) = Self::peek_custom_section(arena, bytes, cursor) + { + match section_name { + "linking" => { + linking = LinkingSection::parse(arena, bytes, &mut cursor)?; + } + "reloc.CODE" => { + reloc_code = + RelocationSection::parse((arena, "reloc.CODE"), bytes, &mut cursor)?; + } + "reloc.DATA" => { + reloc_data = + RelocationSection::parse((arena, "reloc.DATA"), bytes, &mut cursor)?; + } + "name" => { + names = NameSection::parse(arena, bytes, &mut cursor)?; + } + _ => { + cursor = section_end; + } + } + } + + let mut module_errors = String::new(); + if types.is_empty() { + module_errors.push_str("Missing Type section\n"); + } + if function.signatures.is_empty() { + module_errors.push_str("Missing Function section\n"); + } + if code.bytes.is_empty() { + module_errors.push_str("Missing Code section\n"); + } + + if require_relocatable { + if linking.symbol_table.is_empty() { + module_errors.push_str("Missing \"linking\" Custom section\n"); + } + if reloc_code.entries.is_empty() { + module_errors.push_str("Missing \"reloc.CODE\" Custom section\n"); + } + if global.count != 0 { + let global_err_msg = + format!("All globals in a relocatable Wasm module should be imported, but found {} internally defined", global.count); + module_errors.push_str(&global_err_msg); + } + } + + if !module_errors.is_empty() { + let message = if require_relocatable { + format!( + "{}\n{}\n{}", + "The host file has the wrong structure. I need a relocatable WebAssembly binary file.", + "If you're using wasm-ld, try the --relocatable option.", + module_errors, + ) + } else { + format!("I wasn't able to understand this WebAssembly file.\n{module_errors}",) + }; + return Err(ParseError { offset: 0, message }); + } + + Ok(WasmModule { + types, + import, + function, + table, + memory, + global, + export, + start, + element, + code, + data, + linking, + reloc_code, + reloc_data, + names, + }) + } + + fn peek_custom_section( + arena: &'a Bump, + module_bytes: &[u8], + immutable_cursor: usize, + ) -> Result<(&'a str, usize), ParseError> { + let mut cursor = immutable_cursor; + + if cursor >= module_bytes.len() { + return Err(ParseError { + message: "EOF".into(), + offset: cursor, + }); + } + if module_bytes[cursor] != SectionId::Custom as u8 { + return Err(ParseError { + message: format!( + "Expected Custom section but found section ID 0x{:02x}", + module_bytes[cursor] + ), + offset: cursor, + }); + } + cursor += 1; + + let section_size = u32::parse((), module_bytes, &mut cursor)?; + let section_end = cursor + section_size as usize; + let section_name = <&'a str>::parse(arena, module_bytes, &mut cursor)?; + + Ok((section_name, section_end)) + } + + pub fn eliminate_dead_code(&mut self, arena: &'a Bump, called_fns: BitVec) { + if DEBUG_SETTINGS.skip_dead_code_elim { + return; + } + // + // Mark all live functions + // + + let import_count = self.import.imports.len(); + let fn_index_min = import_count as u32 + self.code.dead_import_dummy_count; + let fn_index_max = called_fns.len() as u32; + + // All functions exported to JS must be kept alive + let exported_fns = self + .export + .exports + .iter() + .filter(|ex| ex.ty == ExportType::Func) + .map(|ex| ex.index); + + // The ElementSection lists all functions whose "address" is taken. + // Find their signatures so we can trace all possible indirect calls. + // (The call_indirect instruction specifies a function signature.) + let indirect_callees_and_signatures = Vec::from_iter_in( + self.element + .segments + .iter() + .flat_map(|seg| seg.fn_indices.iter().copied()) + .map(|fn_index| { + let sig = self.function.signatures[fn_index as usize - import_count]; + (fn_index, sig) + }), + arena, + ); + + // Trace callees of the live functions, and mark those as live too + let live_flags = self.trace_live_functions( + arena, + called_fns, + exported_fns, + indirect_callees_and_signatures, + fn_index_min, + fn_index_max, + ); + + // + // Remove all unused JS imports + // We don't want to force the web page to provide dummy JS functions, it's a pain! + // + let mut live_import_fns = Vec::with_capacity_in(import_count, arena); + let mut fn_index = 0; + let mut eliminated_import_count = 0; + self.import.imports.retain(|import| { + if !matches!(import.description, ImportDesc::Func { .. }) { + true + } else { + let live = live_flags[fn_index]; + if live { + live_import_fns.push(fn_index); + } else { + eliminated_import_count += 1; + } + fn_index += 1; + live + } + }); + + // Update the count of JS imports to replace with Wasm dummies + // (In addition to the ones we already replaced for each host-to-app call) + self.code.dead_import_dummy_count += eliminated_import_count as u32; + + // FunctionSection + // Insert function signatures for the new Wasm dummy functions + let signature_count = self.function.signatures.len(); + self.function + .signatures + .extend(repeat(0).take(eliminated_import_count)); + self.function + .signatures + .copy_within(0..signature_count, eliminated_import_count); + + // NameSection + // For each live import, swap its debug name to the right position + for (new_index, &old_index) in live_import_fns.iter().enumerate() { + let old_name: &str = self.names.function_names[old_index].1; + let new_name: &str = self.names.function_names[new_index].1; + self.names.function_names[new_index].1 = old_name; + self.names.function_names[old_index].1 = new_name; + } + + // Relocate calls to JS imports + // This must happen *before* we run dead code elimination on the code section, + // so that byte offsets in the linking data will still be valid. + for (new_index, &old_index) in live_import_fns.iter().enumerate() { + if new_index == old_index { + continue; + } + let sym_index = self + .linking + .find_and_reindex_imported_fn(old_index as u32, new_index as u32) + .unwrap(); + self.reloc_code + .apply_relocs_u32(&mut self.code.bytes, sym_index, new_index as u32); + } + + // + // Code section: Replace dead functions with tiny dummies. + // Live function indices are unchanged, so no relocations are needed. + // + let mut buffer = Vec::with_capacity_in(self.code.bytes.len(), arena); + self.code.function_count.serialize(&mut buffer); + for (i, fn_index) in (fn_index_min..fn_index_max).enumerate() { + if live_flags[fn_index as usize] { + let code_start = self.code.function_offsets[i] as usize; + let code_end = if i < self.code.function_offsets.len() - 1 { + self.code.function_offsets[i + 1] as usize + } else { + self.code.bytes.len() + }; + buffer.extend_from_slice(&self.code.bytes[code_start..code_end]); + } else { + DUMMY_FUNCTION.serialize(&mut buffer); + } + } + + self.code.bytes = buffer; + } + + fn trace_live_functions>( + &self, + arena: &'a Bump, + called_fns: BitVec, + exported_fns: I, + indirect_callees_and_signatures: Vec<'a, (u32, u32)>, + fn_index_min: u32, + fn_index_max: u32, + ) -> BitVec { + let reloc_len = self.reloc_code.entries.len(); + + let mut call_offsets_and_symbols = Vec::with_capacity_in(reloc_len, arena); + let mut indirect_call_offsets_and_types = Vec::with_capacity_in(reloc_len, arena); + for entry in self.reloc_code.entries.iter() { + match entry { + RelocationEntry::Index { + type_id: IndexRelocType::FunctionIndexLeb, + offset, + symbol_index, + } => call_offsets_and_symbols.push((*offset, *symbol_index)), + RelocationEntry::Index { + type_id: IndexRelocType::TypeIndexLeb, + offset, + symbol_index, + } => indirect_call_offsets_and_types.push((*offset, *symbol_index)), + _ => {} + } + } + + // Create a fast lookup from symbol index to function index, for the inner loop below + // (Do all the matching and dereferencing outside the loop) + let symbol_fn_indices: Vec<'a, u32> = Vec::from_iter_in( + self.linking + .symbol_table + .iter() + .map(|sym_info| match sym_info { + SymInfo::Function(WasmObjectSymbol::ExplicitlyNamed { index, .. }) => *index, + SymInfo::Function(WasmObjectSymbol::ImplicitlyNamed { index, .. }) => *index, + _ => u32::MAX, // just use a dummy value for non-function symbols + }), + arena, + ); + + // Loop variables for the main loop below + let mut live_flags = BitVec::repeat(false, called_fns.len()); + let mut next_pass_fns = BitVec::repeat(false, called_fns.len()); + let mut current_pass_fns = called_fns; + for index in exported_fns { + current_pass_fns.set(index as usize, true); + } + + while current_pass_fns.count_ones() > 0 { + // All functions in this pass are live (they have been reached by earlier passes) + debug_assert_eq!(live_flags.len(), current_pass_fns.len()); + live_flags |= ¤t_pass_fns; + + // For each live function in the current pass + for fn_index in current_pass_fns.iter_ones() { + // Skip JS imports and Roc functions + if fn_index < fn_index_min as usize || fn_index >= fn_index_max as usize { + continue; + } + + // Find where the function body is + let offset_index = fn_index - fn_index_min as usize; + let code_start = self.code.function_offsets[offset_index]; + let code_end = if offset_index < self.code.function_offsets.len() - 1 { + self.code.function_offsets[offset_index + 1] + } else { + self.code.bytes.len() as u32 + }; + + // For each call in the body + for (offset, symbol) in call_offsets_and_symbols.iter() { + if *offset > code_start && *offset < code_end { + // Find out which other function is being called + let callee = symbol_fn_indices[*symbol as usize]; + + // If it's not already marked live, include it in the next pass + if live_flags.get(callee as usize).as_deref() == Some(&false) { + next_pass_fns.set(callee as usize, true); + } + } + } + + // For each indirect call in the body + for (offset, signature) in indirect_call_offsets_and_types.iter() { + if *offset > code_start && *offset < code_end { + // Find which indirect callees have the right type signature + let potential_callees = indirect_callees_and_signatures + .iter() + .filter(|(_, sig)| sig == signature) + .map(|(f, _)| *f); + // Mark them all as live + for f in potential_callees { + if live_flags.get(f as usize).as_deref() == Some(&false) { + next_pass_fns.set(f as usize, true); + } + } + } + } + } + + std::mem::swap(&mut current_pass_fns, &mut next_pass_fns); + next_pass_fns.fill(false); + } + + live_flags + } + + pub fn relocate_internal_symbol(&mut self, sym_name: &str, value: u32) -> Result { + self.linking + .find_internal_symbol(sym_name) + .map(|sym_index| { + self.reloc_code + .apply_relocs_u32(&mut self.code.bytes, sym_index as u32, value); + + sym_index as u32 + }) + } + + /// Linking steps for host-to-app functions like `roc__mainForHost_1_exposed` + /// (See further explanation in the gen_wasm README) + /// - Remove the target function from the ImportSection. It's not a JS import but the host declared it as one. + /// - Update all of its call sites to the new index in the app + /// - Swap the _last_ JavaScript import into the slot we just vacated + /// - Update all call sites for the swapped JS function + /// - Update the FunctionSection to show the correct type signature for the swapped JS function + /// - Insert a dummy function in the CodeSection, at the same index as the swapped JS function + pub fn link_host_to_app_calls( + &mut self, + arena: &'a Bump, + host_to_app_map: Vec<'a, (&'a str, u32)>, + ) { + for (app_fn_name, app_fn_index) in host_to_app_map.into_iter() { + // Find the host import, and the last imported function to swap with it. + // Not all imports are functions, so the function index and import index may be different + // (We could support imported globals if we relocated them, although we don't at the time of this comment) + let mut host_fn = None; + let mut swap_fn = None; + self.import + .imports + .iter() + .enumerate() + .filter(|(_import_index, import)| { + matches!(import.description, ImportDesc::Func { .. }) + }) + .enumerate() + .for_each(|(fn_index, (import_index, import))| { + swap_fn = Some((import_index, fn_index)); + if import.name == app_fn_name { + host_fn = Some((import_index, fn_index)); + } + }); + + let (host_import_index, host_fn_index) = match host_fn { + Some(x) => x, + None => { + // The Wasm host doesn't call our app function, so it must be called from JS. Export it. + self.export.append(Export { + name: app_fn_name, + ty: ExportType::Func, + index: app_fn_index, + }); + continue; + } + }; + let (swap_import_index, swap_fn_index) = swap_fn.unwrap(); + + // Note: swap_remove will not work, because some imports may not be functions. + let swap_import = self.import.imports.remove(swap_import_index); + if swap_import_index != host_import_index { + self.import.imports[host_import_index] = swap_import; + } + + // Find the host's symbol for the function we're linking + let host_sym_index = self + .linking + .find_and_reindex_imported_fn(host_fn_index as u32, app_fn_index) + .unwrap(); + + // Update calls to use the app function instead of the host import + self.reloc_code + .apply_relocs_u32(&mut self.code.bytes, host_sym_index, app_fn_index); + + if swap_import_index != host_import_index { + // get the name using the old host import index because we already swapped it! + let swap_fn_name = self.import.imports[host_import_index].name; + + // Find the symbol for the swapped JS import + let swap_sym_index = self + .linking + .find_and_reindex_imported_fn(swap_fn_index as u32, host_fn_index as u32) + .unwrap(); + + // Update calls to the swapped JS import + self.reloc_code.apply_relocs_u32( + &mut self.code.bytes, + swap_sym_index, + host_fn_index as u32, + ); + + // Update the name in the debug info + if let Some((_, debug_name)) = self + .names + .function_names + .iter_mut() + .find(|(i, _)| *i as usize == host_fn_index) + { + debug_name.clone_from(&swap_fn_name); + } + } + + // Remember to insert a dummy function at the beginning of the code section + // to compensate for having one less import, so that function indices don't change. + self.code.dead_import_dummy_count += 1; + + // Insert any type signature for the dummy. Signature index 0 will do. + self.function.signatures.insert(0, 0); + + // Update the debug name for the dummy + if let Some((_, debug_name)) = self + .names + .function_names + .iter_mut() + .find(|(i, _)| *i as usize == swap_fn_index) + { + debug_name.clone_from( + &bumpalo::format!(in arena, "linking_dummy_{}", debug_name).into_bump_str(), + ); + } + } + } + + /// Create a name->index lookup table for host functions that may be called from the app + pub fn get_host_function_lookup(&self, arena: &'a Bump) -> Vec<'a, (&'a str, u32)> { + // Functions beginning with `roc_` go first, since they're most likely to be called + let roc_global_fns = + self.linking + .symbol_table + .iter() + .filter_map(|sym_info| match sym_info { + SymInfo::Function(WasmObjectSymbol::ExplicitlyNamed { flags, index, name }) + if flags & linking::WASM_SYM_BINDING_LOCAL == 0 + && name.starts_with("roc_") => + { + Some((*name, *index)) + } + _ => None, + }); + + let other_global_fns = + self.linking + .symbol_table + .iter() + .filter_map(|sym_info| match sym_info { + SymInfo::Function(WasmObjectSymbol::ExplicitlyNamed { flags, index, name }) + if flags & linking::WASM_SYM_BINDING_LOCAL == 0 + && !name.starts_with("roc_") => + { + Some((*name, *index)) + } + _ => None, + }); + + // There are names available in the import section too, so let's use them! + // We don't know how the host was compiled, so we might as well just grab all the info we can get. + // If we end up with duplicate entries, that's OK. The backend will use the first matching entry. + let import_fns = self + .import + .imports + .iter() + .filter(|import| matches!(import.description, ImportDesc::Func { .. })) + .enumerate() + .map(|(fn_index, import)| (import.name, fn_index as u32)); + + Vec::from_iter_in( + roc_global_fns.chain(other_global_fns).chain(import_fns), + arena, + ) + } +} + +/******************************************************************* + * + * Common types & utility functions + * + *******************************************************************/ + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct LocalId(pub u32); + +/// Wasm value type. (Rust representation matches Wasm encoding) +#[repr(u8)] +#[derive(PartialEq, Eq, Clone, Copy, Debug)] +pub enum ValueType { + I32 = 0x7f, + I64 = 0x7e, + F32 = 0x7d, + F64 = 0x7c, +} + +impl ValueType { + pub const VOID: u8 = 0x40; +} + +impl Serialize for ValueType { + fn serialize(&self, buffer: &mut T) { + buffer.append_u8(*self as u8); + } +} + +impl From for ValueType { + fn from(x: u8) -> Self { + match x { + 0x7f => Self::I32, + 0x7e => Self::I64, + 0x7d => Self::F32, + 0x7c => Self::F64, + _ => internal_error!("Invalid ValueType 0x{:02x}", x), + } + } +} + +impl From for ValueType { + fn from(x: Value) -> Self { + match x { + Value::I32(_) => Self::I32, + Value::I64(_) => Self::I64, + Value::F32(_) => Self::F32, + Value::F64(_) => Self::F64, + } + } +} + +impl Parse<()> for ValueType { + fn parse(_: (), bytes: &[u8], cursor: &mut usize) -> Result { + let byte = u8::parse((), bytes, cursor)?; + Ok(ValueType::from(byte)) + } +} + +// A group of local variable declarations +impl Parse<()> for (u32, ValueType) { + fn parse(_: (), bytes: &[u8], cursor: &mut usize) -> Result { + let count = u32::parse((), bytes, cursor)?; + let ty = ValueType::parse((), bytes, cursor)?; + Ok((count, ty)) + } +} + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum Value { + I32(i32), + I64(i64), + F32(f32), + F64(f64), +} + +impl Value { + pub fn expect_i32(&self) -> Result { + match self { + Value::I32(x) => Ok(*x), + _ => Err((ValueType::I32, ValueType::from(*self))), + } + } + pub fn expect_i64(&self) -> Result { + match self { + Value::I64(x) => Ok(*x), + _ => Err((ValueType::I64, ValueType::from(*self))), + } + } + pub fn expect_f32(&self) -> Result { + match self { + Value::F32(x) => Ok(*x), + _ => Err((ValueType::F32, ValueType::from(*self))), + } + } + pub fn expect_f64(&self) -> Result { + match self { + Value::F64(x) => Ok(*x), + _ => Err((ValueType::F64, ValueType::from(*self))), + } + } +} + +impl From for Value { + fn from(x: u32) -> Self { + Value::I32(i32::from_ne_bytes(x.to_ne_bytes())) + } +} + +impl From for Value { + fn from(x: u64) -> Self { + Value::I64(i64::from_ne_bytes(x.to_ne_bytes())) + } +} + +impl From for Value { + fn from(x: i32) -> Self { + Value::I32(x) + } +} + +impl From for Value { + fn from(x: i64) -> Self { + Value::I64(x) + } +} + +/// Wasm memory alignment for load/store instructions. +/// Rust representation matches Wasm encoding. +/// It's an error to specify alignment higher than the "natural" alignment of the instruction +#[repr(u8)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd)] +pub enum Align { + Bytes1 = 0, + Bytes2 = 1, + Bytes4 = 2, + Bytes8 = 3, +} + +impl Align { + /// Calculate the largest possible alignment for a load/store at a given stack frame offset + /// Assumes the stack frame is aligned to at least 8 bytes + pub fn from_stack_offset(max_align: Align, offset: u32) -> Align { + if (max_align == Align::Bytes8) && (offset & 7 == 0) { + return Align::Bytes8; + } + if (max_align >= Align::Bytes4) && (offset & 3 == 0) { + return Align::Bytes4; + } + if (max_align >= Align::Bytes2) && (offset & 1 == 0) { + return Align::Bytes2; + } + Align::Bytes1 + } +} + +impl From for Align { + fn from(x: u32) -> Align { + match x { + 1 => Align::Bytes1, + 2 => Align::Bytes2, + 4 => Align::Bytes4, + _ => { + if x.count_ones() == 1 { + Align::Bytes8 // Max value supported by any Wasm instruction + } else { + internal_error!("Cannot align to {} bytes", x); + } + } + } + } +} + +/// Round up to alignment_bytes (which must be a power of 2) +#[macro_export] +macro_rules! round_up_to_alignment { + ($unaligned: expr, $alignment_bytes: expr) => { + if $alignment_bytes <= 1 { + $unaligned + } else if $alignment_bytes.count_ones() != 1 { + internal_error!( + "Cannot align to {} bytes. Not a power of 2.", + $alignment_bytes + ); + } else { + let mut aligned = $unaligned; + aligned += $alignment_bytes - 1; // if lower bits are non-zero, push it over the next boundary + aligned &= !$alignment_bytes + 1; // mask with a flag that has upper bits 1, lower bits 0 + aligned + } + }; +} + +/// # dbg_hex +/// display dbg result in hexadecimal `{:#x?}` format. +#[macro_export] +macro_rules! dbg_hex { + // NOTE: We cannot use `concat!` to make a static string as a format argument + // of `eprintln!` because `file!` could contain a `{` or + // `$val` expression could be a block (`{ .. }`), in which case the `eprintln!` + // will be malformed. + () => { + eprintln!("[{}:{}]", file!(), line!()); + }; + ($val:expr $(,)?) => { + // Use of `match` here is intentional because it affects the lifetimes + // of temporaries - https://stackoverflow.com/a/48732525/1063961 + match $val { + tmp => { + eprintln!("[{}:{}] {} = {:#x?}", + file!(), line!(), stringify!($val), &tmp); + tmp + } + } + }; + ($($val:expr),+ $(,)?) => { + ($($crate::dbg_hex!($val)),+,) + }; +} + +/// Bytes for a dummy function with just a single `unreachable` instruction. +/// Used in dead code elimination to replace unused functions. +const DUMMY_FUNCTION: [u8; 3] = [ + 0, // number of local variable declarations + OpCode::UNREACHABLE as u8, // panic if we were wrong to eliminate! + OpCode::END as u8, // end of function (required for validation) +]; + +// TODO: make this an environment variable +pub struct WasmDebugSettings { + pub skip_dead_code_elim: bool, +} + +pub const DEBUG_SETTINGS: WasmDebugSettings = WasmDebugSettings { + skip_dead_code_elim: false && cfg!(debug_assertions), +}; diff --git a/compiler/gen_wasm/src/wasm_module/linking.rs b/crates/wasm_module/src/linking.rs similarity index 91% rename from compiler/gen_wasm/src/wasm_module/linking.rs rename to crates/wasm_module/src/linking.rs index a9d2ecdafd..d8432ebd65 100644 --- a/compiler/gen_wasm/src/wasm_module/linking.rs +++ b/crates/wasm_module/src/linking.rs @@ -1,10 +1,9 @@ use bumpalo::collections::vec::Vec; use bumpalo::Bump; -use super::parse::parse_fixed_size_items; +use super::parse::{parse_fixed_size_items, Parse, ParseError, SkipBytes}; use super::sections::SectionId; use super::serialize::{overwrite_padded_i32, overwrite_padded_u32}; -use crate::wasm_module::parse::{Parse, ParseError, SkipBytes}; /******************************************************************* * @@ -145,7 +144,7 @@ impl Parse<()> for RelocationEntry { Err(ParseError { offset: *cursor, - message: format!("Unknown relocation type 0x{:2x}", type_id_byte), + message: format!("Unknown relocation type 0x{type_id_byte:2x}"), }) } } @@ -159,7 +158,7 @@ pub struct RelocationSection<'a> { } impl<'a> RelocationSection<'a> { - fn new(arena: &'a Bump, name: &'a str) -> Self { + pub(crate) fn new(arena: &'a Bump, name: &'a str) -> Self { RelocationSection { name, target_section_index: 0, @@ -167,13 +166,7 @@ impl<'a> RelocationSection<'a> { } } - pub fn apply_relocs_u32( - &self, - section_bytes: &mut [u8], - section_bytes_offset: u32, - sym_index: u32, - value: u32, - ) { + pub fn apply_relocs_u32(&self, section_bytes: &mut [u8], sym_index: u32, value: u32) { for entry in self.entries.iter() { match entry { RelocationEntry::Index { @@ -182,7 +175,7 @@ impl<'a> RelocationSection<'a> { symbol_index, } if *symbol_index == sym_index => { use IndexRelocType::*; - let idx = (*offset - section_bytes_offset) as usize; + let idx = *offset as usize; match type_id { FunctionIndexLeb | TypeIndexLeb | GlobalIndexLeb | EventIndexLeb | TableNumberLeb => { @@ -198,7 +191,7 @@ impl<'a> RelocationSection<'a> { addend, } if *symbol_index == sym_index => { use OffsetRelocType::*; - let idx = (*offset - section_bytes_offset) as usize; + let idx = *offset as usize; match type_id { MemoryAddrLeb => { overwrite_padded_u32(&mut section_bytes[idx..], value + *addend as u32); @@ -219,10 +212,12 @@ type RelocCtx<'a> = (&'a Bump, &'static str); impl<'a> Parse> for RelocationSection<'a> { fn parse(ctx: RelocCtx<'a>, bytes: &[u8], cursor: &mut usize) -> Result { + let cursor_reset = *cursor; let (arena, name) = ctx; - if *cursor > bytes.len() || bytes[*cursor] != SectionId::Custom as u8 { + if *cursor >= bytes.len() || bytes[*cursor] != SectionId::Custom as u8 { // The section we're looking for is missing, which is the same as being empty. + *cursor = cursor_reset; return Ok(RelocationSection::new(arena, name)); } *cursor += 1; @@ -231,6 +226,7 @@ impl<'a> Parse> for RelocationSection<'a> { let actual_name = <&'a str>::parse(arena, bytes, cursor)?; if actual_name != name { // The section we're looking for is missing, which is the same as being empty. + *cursor = cursor_reset; return Ok(RelocationSection::new(arena, name)); } @@ -495,7 +491,7 @@ impl Parse<()> for SymType { 5 => Ok(Self::Table), x => Err(ParseError { offset, - message: format!("Invalid symbol info type in linking section: {}", x), + message: format!("Invalid symbol info type in linking section: {x}"), }), } } @@ -540,7 +536,7 @@ impl Parse<()> for SubSectionId { 8 => Ok(Self::SymbolTable), x => Err(ParseError { offset, - message: format!("Invalid linking subsection ID {}", x), + message: format!("Invalid linking subsection ID {x}"), }), } } @@ -577,46 +573,59 @@ impl<'a> LinkingSection<'a> { } } - pub fn find_internal_symbol(&self, target_name: &str) -> Option { + pub fn find_internal_symbol(&self, target_name: &str) -> Result { self.symbol_table .iter() .position(|sym| sym.name() == Some(target_name)) - .map(|x| x as u32) + .ok_or_else(|| { + format!("Linking failed! Can't find `{target_name}` in host symbol table") + }) } - pub fn find_imported_function_symbol(&self, fn_index: u32) -> Option { + pub fn find_imported_fn_sym_index(&mut self, fn_index: u32) -> Result { self.symbol_table - .iter() + .iter_mut() .position(|sym| match sym { SymInfo::Function(WasmObjectSymbol::ImplicitlyNamed { flags, index, .. }) | SymInfo::Function(WasmObjectSymbol::ExplicitlyNamed { flags, index, .. }) => { - flags & WASM_SYM_UNDEFINED != 0 && *index == fn_index + *flags & WASM_SYM_UNDEFINED != 0 && *index == fn_index } _ => false, }) .map(|sym_index| sym_index as u32) + .ok_or_else(|| format!("Can't find fn #{fn_index} in host symbol table")) } - pub fn name_index_map(&self, arena: &'a Bump, prefix: &str) -> Vec<'a, (&'a str, u32)> { - let iter = self - .symbol_table - .iter() - .filter_map(|sym_info| match sym_info { - SymInfo::Function(WasmObjectSymbol::ExplicitlyNamed { flags, index, name }) - if flags & WASM_SYM_BINDING_LOCAL == 0 && name.starts_with(prefix) => - { - Some((*name, *index)) + pub fn find_and_reindex_imported_fn( + &mut self, + old_fn_index: u32, + new_fn_index: u32, + ) -> Result { + self.symbol_table + .iter_mut() + .position(|sym| match sym { + SymInfo::Function(WasmObjectSymbol::ImplicitlyNamed { flags, index, .. }) + | SymInfo::Function(WasmObjectSymbol::ExplicitlyNamed { flags, index, .. }) => { + let found = *flags & WASM_SYM_UNDEFINED != 0 && *index == old_fn_index; + if found { + *index = new_fn_index; + } + found } - _ => None, - }); - - Vec::from_iter_in(iter, arena) + _ => false, + }) + .map(|sym_index| sym_index as u32) + .ok_or_else(|| { + format!("Linking failed! Can't find fn #{old_fn_index} in host symbol table") + }) } } impl<'a> Parse<&'a Bump> for LinkingSection<'a> { fn parse(arena: &'a Bump, bytes: &[u8], cursor: &mut usize) -> Result { - if *cursor > bytes.len() || bytes[*cursor] != SectionId::Custom as u8 { + let cursor_reset = *cursor; + if *cursor >= bytes.len() || bytes[*cursor] != SectionId::Custom as u8 { + *cursor = cursor_reset; return Ok(LinkingSection::new(arena)); } *cursor += 1; @@ -626,6 +635,7 @@ impl<'a> Parse<&'a Bump> for LinkingSection<'a> { // Don't fail if it's the wrong section. Let the WasmModule validate presence/absence of sections let actual_name = <&'a str>::parse(arena, bytes, cursor)?; if actual_name != Self::NAME { + *cursor = cursor_reset; return Ok(LinkingSection::new(arena)); } @@ -634,8 +644,7 @@ impl<'a> Parse<&'a Bump> for LinkingSection<'a> { return Err(ParseError { offset: *cursor, message: format!( - "This file uses version {} of Wasm linking data, but only version {} is supported.", - linking_version, LINKING_VERSION + "This file uses version {linking_version} of Wasm linking data, but only version {LINKING_VERSION} is supported." ), }); } diff --git a/crates/wasm_module/src/opcodes.rs b/crates/wasm_module/src/opcodes.rs new file mode 100644 index 0000000000..948a75faab --- /dev/null +++ b/crates/wasm_module/src/opcodes.rs @@ -0,0 +1,578 @@ +use crate::Serialize; + +use super::parse::{Parse, ParseError, SkipBytes}; + +// NOTE: when adding a new variant, be sure to add it to LOOKUP_TABLE below as well +#[repr(u8)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum OpCode { + UNREACHABLE = 0x00, + NOP = 0x01, + BLOCK = 0x02, + LOOP = 0x03, + IF = 0x04, + ELSE = 0x05, + END = 0x0b, + BR = 0x0c, + BRIF = 0x0d, + BRTABLE = 0x0e, + RETURN = 0x0f, + CALL = 0x10, + CALLINDIRECT = 0x11, + DROP = 0x1a, + SELECT = 0x1b, + GETLOCAL = 0x20, + SETLOCAL = 0x21, + TEELOCAL = 0x22, + GETGLOBAL = 0x23, + SETGLOBAL = 0x24, + I32LOAD = 0x28, + I64LOAD = 0x29, + F32LOAD = 0x2a, + F64LOAD = 0x2b, + I32LOAD8S = 0x2c, + I32LOAD8U = 0x2d, + I32LOAD16S = 0x2e, + I32LOAD16U = 0x2f, + I64LOAD8S = 0x30, + I64LOAD8U = 0x31, + I64LOAD16S = 0x32, + I64LOAD16U = 0x33, + I64LOAD32S = 0x34, + I64LOAD32U = 0x35, + I32STORE = 0x36, + I64STORE = 0x37, + F32STORE = 0x38, + F64STORE = 0x39, + I32STORE8 = 0x3a, + I32STORE16 = 0x3b, + I64STORE8 = 0x3c, + I64STORE16 = 0x3d, + I64STORE32 = 0x3e, + CURRENTMEMORY = 0x3f, + GROWMEMORY = 0x40, + MEMORY = 0xFC, + I32CONST = 0x41, + I64CONST = 0x42, + F32CONST = 0x43, + F64CONST = 0x44, + I32EQZ = 0x45, + I32EQ = 0x46, + I32NE = 0x47, + I32LTS = 0x48, + I32LTU = 0x49, + I32GTS = 0x4a, + I32GTU = 0x4b, + I32LES = 0x4c, + I32LEU = 0x4d, + I32GES = 0x4e, + I32GEU = 0x4f, + I64EQZ = 0x50, + I64EQ = 0x51, + I64NE = 0x52, + I64LTS = 0x53, + I64LTU = 0x54, + I64GTS = 0x55, + I64GTU = 0x56, + I64LES = 0x57, + I64LEU = 0x58, + I64GES = 0x59, + I64GEU = 0x5a, + + F32EQ = 0x5b, + F32NE = 0x5c, + F32LT = 0x5d, + F32GT = 0x5e, + F32LE = 0x5f, + F32GE = 0x60, + + F64EQ = 0x61, + F64NE = 0x62, + F64LT = 0x63, + F64GT = 0x64, + F64LE = 0x65, + F64GE = 0x66, + + I32CLZ = 0x67, + I32CTZ = 0x68, + I32POPCNT = 0x69, + I32ADD = 0x6a, + I32SUB = 0x6b, + I32MUL = 0x6c, + I32DIVS = 0x6d, + I32DIVU = 0x6e, + I32REMS = 0x6f, + I32REMU = 0x70, + I32AND = 0x71, + I32OR = 0x72, + I32XOR = 0x73, + I32SHL = 0x74, + I32SHRS = 0x75, + I32SHRU = 0x76, + I32ROTL = 0x77, + I32ROTR = 0x78, + + I64CLZ = 0x79, + I64CTZ = 0x7a, + I64POPCNT = 0x7b, + I64ADD = 0x7c, + I64SUB = 0x7d, + I64MUL = 0x7e, + I64DIVS = 0x7f, + I64DIVU = 0x80, + I64REMS = 0x81, + I64REMU = 0x82, + I64AND = 0x83, + I64OR = 0x84, + I64XOR = 0x85, + I64SHL = 0x86, + I64SHRS = 0x87, + I64SHRU = 0x88, + I64ROTL = 0x89, + I64ROTR = 0x8a, + F32ABS = 0x8b, + F32NEG = 0x8c, + F32CEIL = 0x8d, + F32FLOOR = 0x8e, + F32TRUNC = 0x8f, + F32NEAREST = 0x90, + F32SQRT = 0x91, + F32ADD = 0x92, + F32SUB = 0x93, + F32MUL = 0x94, + F32DIV = 0x95, + F32MIN = 0x96, + F32MAX = 0x97, + F32COPYSIGN = 0x98, + F64ABS = 0x99, + F64NEG = 0x9a, + F64CEIL = 0x9b, + F64FLOOR = 0x9c, + F64TRUNC = 0x9d, + F64NEAREST = 0x9e, + F64SQRT = 0x9f, + F64ADD = 0xa0, + F64SUB = 0xa1, + F64MUL = 0xa2, + F64DIV = 0xa3, + F64MIN = 0xa4, + F64MAX = 0xa5, + F64COPYSIGN = 0xa6, + + I32WRAPI64 = 0xa7, + I32TRUNCSF32 = 0xa8, + I32TRUNCUF32 = 0xa9, + I32TRUNCSF64 = 0xaa, + I32TRUNCUF64 = 0xab, + I64EXTENDSI32 = 0xac, + I64EXTENDUI32 = 0xad, + I64TRUNCSF32 = 0xae, + I64TRUNCUF32 = 0xaf, + I64TRUNCSF64 = 0xb0, + I64TRUNCUF64 = 0xb1, + F32CONVERTSI32 = 0xb2, + F32CONVERTUI32 = 0xb3, + F32CONVERTSI64 = 0xb4, + F32CONVERTUI64 = 0xb5, + F32DEMOTEF64 = 0xb6, + F64CONVERTSI32 = 0xb7, + F64CONVERTUI32 = 0xb8, + F64CONVERTSI64 = 0xb9, + F64CONVERTUI64 = 0xba, + F64PROMOTEF32 = 0xbb, + + I32REINTERPRETF32 = 0xbc, + I64REINTERPRETF64 = 0xbd, + F32REINTERPRETI32 = 0xbe, + F64REINTERPRETI64 = 0xbf, + + I32EXTEND8S = 0xc0, + I32EXTEND16S = 0xc1, + I64EXTEND8S = 0xc2, + I64EXTEND16S = 0xc3, + I64EXTEND32S = 0xc4, +} + +pub const LOOKUP_TABLE: [Option; 256] = { + use OpCode::*; + + let mut result = [None; 256]; + + result[0x00] = Some(UNREACHABLE); + result[0x01] = Some(NOP); + result[0x02] = Some(BLOCK); + result[0x03] = Some(LOOP); + result[0x04] = Some(IF); + result[0x05] = Some(ELSE); + result[0x0b] = Some(END); + result[0x0c] = Some(BR); + result[0x0d] = Some(BRIF); + result[0x0e] = Some(BRTABLE); + result[0x0f] = Some(RETURN); + result[0x10] = Some(CALL); + result[0x11] = Some(CALLINDIRECT); + result[0x1a] = Some(DROP); + result[0x1b] = Some(SELECT); + result[0x20] = Some(GETLOCAL); + result[0x21] = Some(SETLOCAL); + result[0x22] = Some(TEELOCAL); + result[0x23] = Some(GETGLOBAL); + result[0x24] = Some(SETGLOBAL); + result[0x28] = Some(I32LOAD); + result[0x29] = Some(I64LOAD); + result[0x2a] = Some(F32LOAD); + result[0x2b] = Some(F64LOAD); + result[0x2c] = Some(I32LOAD8S); + result[0x2d] = Some(I32LOAD8U); + result[0x2e] = Some(I32LOAD16S); + result[0x2f] = Some(I32LOAD16U); + result[0x30] = Some(I64LOAD8S); + result[0x31] = Some(I64LOAD8U); + result[0x32] = Some(I64LOAD16S); + result[0x33] = Some(I64LOAD16U); + result[0x34] = Some(I64LOAD32S); + result[0x35] = Some(I64LOAD32U); + result[0x36] = Some(I32STORE); + result[0x37] = Some(I64STORE); + result[0x38] = Some(F32STORE); + result[0x39] = Some(F64STORE); + result[0x3a] = Some(I32STORE8); + result[0x3b] = Some(I32STORE16); + result[0x3c] = Some(I64STORE8); + result[0x3d] = Some(I64STORE16); + result[0x3e] = Some(I64STORE32); + result[0x3f] = Some(CURRENTMEMORY); + result[0x40] = Some(GROWMEMORY); + result[0xfc] = Some(MEMORY); + result[0x41] = Some(I32CONST); + result[0x42] = Some(I64CONST); + result[0x43] = Some(F32CONST); + result[0x44] = Some(F64CONST); + result[0x45] = Some(I32EQZ); + result[0x46] = Some(I32EQ); + result[0x47] = Some(I32NE); + result[0x48] = Some(I32LTS); + result[0x49] = Some(I32LTU); + result[0x4a] = Some(I32GTS); + result[0x4b] = Some(I32GTU); + result[0x4c] = Some(I32LES); + result[0x4d] = Some(I32LEU); + result[0x4e] = Some(I32GES); + result[0x4f] = Some(I32GEU); + result[0x50] = Some(I64EQZ); + result[0x51] = Some(I64EQ); + result[0x52] = Some(I64NE); + result[0x53] = Some(I64LTS); + result[0x54] = Some(I64LTU); + result[0x55] = Some(I64GTS); + result[0x56] = Some(I64GTU); + result[0x57] = Some(I64LES); + result[0x58] = Some(I64LEU); + result[0x59] = Some(I64GES); + result[0x5a] = Some(I64GEU); + result[0x5b] = Some(F32EQ); + result[0x5c] = Some(F32NE); + result[0x5d] = Some(F32LT); + result[0x5e] = Some(F32GT); + result[0x5f] = Some(F32LE); + result[0x60] = Some(F32GE); + result[0x61] = Some(F64EQ); + result[0x62] = Some(F64NE); + result[0x63] = Some(F64LT); + result[0x64] = Some(F64GT); + result[0x65] = Some(F64LE); + result[0x66] = Some(F64GE); + result[0x67] = Some(I32CLZ); + result[0x68] = Some(I32CTZ); + result[0x69] = Some(I32POPCNT); + result[0x6a] = Some(I32ADD); + result[0x6b] = Some(I32SUB); + result[0x6c] = Some(I32MUL); + result[0x6d] = Some(I32DIVS); + result[0x6e] = Some(I32DIVU); + result[0x6f] = Some(I32REMS); + result[0x70] = Some(I32REMU); + result[0x71] = Some(I32AND); + result[0x72] = Some(I32OR); + result[0x73] = Some(I32XOR); + result[0x74] = Some(I32SHL); + result[0x75] = Some(I32SHRS); + result[0x76] = Some(I32SHRU); + result[0x77] = Some(I32ROTL); + result[0x78] = Some(I32ROTR); + result[0x79] = Some(I64CLZ); + result[0x7a] = Some(I64CTZ); + result[0x7b] = Some(I64POPCNT); + result[0x7c] = Some(I64ADD); + result[0x7d] = Some(I64SUB); + result[0x7e] = Some(I64MUL); + result[0x7f] = Some(I64DIVS); + result[0x80] = Some(I64DIVU); + result[0x81] = Some(I64REMS); + result[0x82] = Some(I64REMU); + result[0x83] = Some(I64AND); + result[0x84] = Some(I64OR); + result[0x85] = Some(I64XOR); + result[0x86] = Some(I64SHL); + result[0x87] = Some(I64SHRS); + result[0x88] = Some(I64SHRU); + result[0x89] = Some(I64ROTL); + result[0x8a] = Some(I64ROTR); + result[0x8b] = Some(F32ABS); + result[0x8c] = Some(F32NEG); + result[0x8d] = Some(F32CEIL); + result[0x8e] = Some(F32FLOOR); + result[0x8f] = Some(F32TRUNC); + result[0x90] = Some(F32NEAREST); + result[0x91] = Some(F32SQRT); + result[0x92] = Some(F32ADD); + result[0x93] = Some(F32SUB); + result[0x94] = Some(F32MUL); + result[0x95] = Some(F32DIV); + result[0x96] = Some(F32MIN); + result[0x97] = Some(F32MAX); + result[0x98] = Some(F32COPYSIGN); + result[0x99] = Some(F64ABS); + result[0x9a] = Some(F64NEG); + result[0x9b] = Some(F64CEIL); + result[0x9c] = Some(F64FLOOR); + result[0x9d] = Some(F64TRUNC); + result[0x9e] = Some(F64NEAREST); + result[0x9f] = Some(F64SQRT); + result[0xa0] = Some(F64ADD); + result[0xa1] = Some(F64SUB); + result[0xa2] = Some(F64MUL); + result[0xa3] = Some(F64DIV); + result[0xa4] = Some(F64MIN); + result[0xa5] = Some(F64MAX); + result[0xa6] = Some(F64COPYSIGN); + result[0xa7] = Some(I32WRAPI64); + result[0xa8] = Some(I32TRUNCSF32); + result[0xa9] = Some(I32TRUNCUF32); + result[0xaa] = Some(I32TRUNCSF64); + result[0xab] = Some(I32TRUNCUF64); + result[0xac] = Some(I64EXTENDSI32); + result[0xad] = Some(I64EXTENDUI32); + result[0xae] = Some(I64TRUNCSF32); + result[0xaf] = Some(I64TRUNCUF32); + result[0xb0] = Some(I64TRUNCSF64); + result[0xb1] = Some(I64TRUNCUF64); + result[0xb2] = Some(F32CONVERTSI32); + result[0xb3] = Some(F32CONVERTUI32); + result[0xb4] = Some(F32CONVERTSI64); + result[0xb5] = Some(F32CONVERTUI64); + result[0xb6] = Some(F32DEMOTEF64); + result[0xb7] = Some(F64CONVERTSI32); + result[0xb8] = Some(F64CONVERTUI32); + result[0xb9] = Some(F64CONVERTSI64); + result[0xba] = Some(F64CONVERTUI64); + result[0xbb] = Some(F64PROMOTEF32); + result[0xbc] = Some(I32REINTERPRETF32); + result[0xbd] = Some(I64REINTERPRETF64); + result[0xbe] = Some(F32REINTERPRETI32); + result[0xbf] = Some(F64REINTERPRETI64); + + result[0xc0] = Some(I32EXTEND8S); + result[0xc1] = Some(I32EXTEND16S); + result[0xc2] = Some(I64EXTEND8S); + result[0xc3] = Some(I64EXTEND16S); + result[0xc4] = Some(I64EXTEND32S); + + result +}; + +impl From for OpCode { + fn from(value: u8) -> Self { + if false { + // considerably faster in practice + unsafe { std::mem::transmute(value) } + } else { + // invalid instruction bytes can be genuine because we don't support all instructions, and + // new ones get added or stabilized. It could also be a bug in the interpreter, e.g. some + // opcode does not move the instruction pointer correctly. + match LOOKUP_TABLE[value as usize] { + None => unreachable!("unsupported instruction byte {value:#x?}"), + Some(op) => op, + } + } + } +} + +#[repr(u8)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum MemoryInstruction { + MemoryInit = 8, + DataDrop = 9, + MemoryCopy = 10, + MemoryFill = 11, +} + +impl TryFrom for MemoryInstruction { + type Error = u8; + + fn try_from(value: u8) -> Result { + match value { + 8 => Ok(Self::MemoryInit), + 9 => Ok(Self::DataDrop), + 10 => Ok(Self::MemoryCopy), + 11 => Ok(Self::MemoryFill), + _ => Err(value), + } + } +} + +/// The format of the *immediate* operands of an operator +/// Immediates appear directly in the byte stream after the opcode, +/// rather than being popped off the value stack. These are the possible forms. +#[derive(Debug)] +enum OpImmediates { + NoImmediate, + Byte1, + Bytes4, + Bytes8, + Leb32x1, + Leb64x1, + Leb32x2, + BrTable, + Memory, +} + +fn immediates_for(op: OpCode) -> Result { + use OpCode::*; + use OpImmediates::*; + + let imm = match op { + UNREACHABLE => NoImmediate, + NOP => NoImmediate, + BLOCK | LOOP | IF => Byte1, + ELSE => NoImmediate, + END => NoImmediate, + BR | BRIF => Leb32x1, + BRTABLE => BrTable, + RETURN => NoImmediate, + CALL => Leb32x1, + CALLINDIRECT => Leb32x2, + DROP => NoImmediate, + SELECT => NoImmediate, + GETLOCAL | SETLOCAL | TEELOCAL => Leb32x1, + GETGLOBAL | SETGLOBAL => Leb32x1, + + I32LOAD | I64LOAD | F32LOAD | F64LOAD | I32LOAD8S | I32LOAD8U | I32LOAD16S | I32LOAD16U + | I64LOAD8S | I64LOAD8U | I64LOAD16S | I64LOAD16U | I64LOAD32S | I64LOAD32U | I32STORE + | I64STORE | F32STORE | F64STORE | I32STORE8 | I32STORE16 | I64STORE8 | I64STORE16 + | I64STORE32 => Leb32x2, + + CURRENTMEMORY | GROWMEMORY => Byte1, + MEMORY => Memory, + + I32CONST => Leb32x1, + I64CONST => Leb64x1, + F32CONST => Bytes4, + F64CONST => Bytes8, + + I32EQZ | I32EQ | I32NE | I32LTS | I32LTU | I32GTS | I32GTU | I32LES | I32LEU | I32GES + | I32GEU | I64EQZ | I64EQ | I64NE | I64LTS | I64LTU | I64GTS | I64GTU | I64LES | I64LEU + | I64GES | I64GEU | F32EQ | F32NE | F32LT | F32GT | F32LE | F32GE | F64EQ | F64NE + | F64LT | F64GT | F64LE | F64GE | I32CLZ | I32CTZ | I32POPCNT | I32ADD | I32SUB + | I32MUL | I32DIVS | I32DIVU | I32REMS | I32REMU | I32AND | I32OR | I32XOR | I32SHL + | I32SHRS | I32SHRU | I32ROTL | I32ROTR | I64CLZ | I64CTZ | I64POPCNT | I64ADD | I64SUB + | I64MUL | I64DIVS | I64DIVU | I64REMS | I64REMU | I64AND | I64OR | I64XOR | I64SHL + | I64SHRS | I64SHRU | I64ROTL | I64ROTR | F32ABS | F32NEG | F32CEIL | F32FLOOR + | F32TRUNC | F32NEAREST | F32SQRT | F32ADD | F32SUB | F32MUL | F32DIV | F32MIN | F32MAX + | F32COPYSIGN | F64ABS | F64NEG | F64CEIL | F64FLOOR | F64TRUNC | F64NEAREST | F64SQRT + | F64ADD | F64SUB | F64MUL | F64DIV | F64MIN | F64MAX | F64COPYSIGN | I32WRAPI64 + | I32TRUNCSF32 | I32TRUNCUF32 | I32TRUNCSF64 | I32TRUNCUF64 | I64EXTENDSI32 + | I64EXTENDUI32 | I64TRUNCSF32 | I64TRUNCUF32 | I64TRUNCSF64 | I64TRUNCUF64 + | F32CONVERTSI32 | F32CONVERTUI32 | F32CONVERTSI64 | F32CONVERTUI64 | F32DEMOTEF64 + | F64CONVERTSI32 | F64CONVERTUI32 | F64CONVERTSI64 | F64CONVERTUI64 | F64PROMOTEF32 + | I32REINTERPRETF32 | I64REINTERPRETF64 | F32REINTERPRETI32 | F64REINTERPRETI64 + | I32EXTEND8S | I32EXTEND16S | I64EXTEND8S | I64EXTEND16S | I64EXTEND32S => NoImmediate, + + // Catch-all in case of an invalid cast from u8 to OpCode while parsing binary + // (rustc keeps this code, I verified in Compiler Explorer) + #[allow(unreachable_patterns)] + _ => return Err(format!("Unknown Wasm instruction 0x{:02x}", op as u8)), + }; + + Ok(imm) +} + +impl SkipBytes for OpCode { + fn skip_bytes(bytes: &[u8], cursor: &mut usize) -> Result<(), ParseError> { + use OpImmediates::*; + + let opcode_byte: u8 = bytes[*cursor]; + + let opcode: OpCode = OpCode::from(opcode_byte); + // will return Err if transmute was invalid + let immediates = immediates_for(opcode).map_err(|message| ParseError { + message, + offset: *cursor, + })?; + + match immediates { + NoImmediate => { + *cursor += 1; + } + Byte1 => { + *cursor += 1 + 1; + } + Bytes4 => { + *cursor += 1 + 4; + } + Bytes8 => { + *cursor += 1 + 8; + } + Leb32x1 => { + *cursor += 1; + u32::skip_bytes(bytes, cursor)?; + } + Leb64x1 => { + *cursor += 1; + u64::skip_bytes(bytes, cursor)?; + } + Leb32x2 => { + *cursor += 1; + u32::skip_bytes(bytes, cursor)?; + u32::skip_bytes(bytes, cursor)?; + } + BrTable => { + *cursor += 1; + let n_labels = 1 + u32::parse((), bytes, cursor)?; + for _ in 0..n_labels { + u32::skip_bytes(bytes, cursor)?; + } + } + Memory => { + match MemoryInstruction::try_from(bytes[*cursor + 1]) { + Ok(op) => match op { + MemoryInstruction::MemoryInit => { + // memory.init + todo!("WASM instruction: memory.init") + } + MemoryInstruction::DataDrop => { + // data.drop x + todo!("WASM instruction: data.drop") + } + MemoryInstruction::MemoryCopy => { + // memory.copy + *cursor += 1 + 1 + 2; + } + MemoryInstruction::MemoryFill => { + // memory.fill + *cursor += 1 + 1 + 1; + } + }, + Err(other) => unreachable!("invalid memory instruction {other:?}"), + } + } + } + Ok(()) + } +} + +impl Serialize for OpCode { + fn serialize(&self, buffer: &mut T) { + (*self as u8).serialize(buffer) + } +} diff --git a/crates/wasm_module/src/parse.rs b/crates/wasm_module/src/parse.rs new file mode 100644 index 0000000000..753ebe9530 --- /dev/null +++ b/crates/wasm_module/src/parse.rs @@ -0,0 +1,297 @@ +use super::serialize::{MAX_SIZE_ENCODED_U32, MAX_SIZE_ENCODED_U64}; +use bumpalo::collections::vec::Vec; +use bumpalo::Bump; + +/// Parse serialized bytes into a data structure +/// Specific parsers may need contextual data from other parts of the .wasm file +pub trait Parse: Sized { + fn parse(ctx: ParseContext, bytes: &[u8], cursor: &mut usize) -> Result; +} + +#[derive(Debug)] +pub struct ParseError { + pub offset: usize, + pub message: String, +} + +/// Decode an unsigned 32-bit integer from the provided buffer in LEB-128 format +/// Return the integer itself and the offset after it ends +fn decode_u32(bytes: &[u8]) -> Result<(u32, usize), ()> { + let mut value = 0; + let mut shift = 0; + for (i, byte) in bytes.iter().take(MAX_SIZE_ENCODED_U32).enumerate() { + value += ((byte & 0x7f) as u32) << shift; + if (byte & 0x80) == 0 { + return Ok((value, i + 1)); + } + shift += 7; + } + Err(()) +} + +impl Parse<()> for u32 { + fn parse(_ctx: (), bytes: &[u8], cursor: &mut usize) -> Result { + match decode_u32(&bytes[*cursor..]) { + Ok((value, len)) => { + *cursor += len; + Ok(value) + } + Err(()) => Err(ParseError { + offset: *cursor, + message: format!( + "Failed to decode u32 as LEB-128 from bytes: {:2x?}", + &bytes[*cursor..][..MAX_SIZE_ENCODED_U32] + ), + }), + } + } +} + +impl Parse<()> for u8 { + fn parse(_ctx: (), bytes: &[u8], cursor: &mut usize) -> Result { + let byte = bytes[*cursor]; + *cursor += 1; + Ok(byte) + } +} + +/// Decode a signed 32-bit integer from the provided buffer in LEB-128 format +/// Return the integer itself and the offset after it ends +fn decode_i32(bytes: &[u8]) -> Result<(i32, usize), ()> { + let mut value = 0; + let mut shift = 0; + for (i, byte) in bytes.iter().take(MAX_SIZE_ENCODED_U32).enumerate() { + value |= ((byte & 0x7f) as i32) << shift; + shift += 7; + if (byte & 0x80) == 0 { + let is_negative = byte & 0x40 != 0; + if shift < 32 && is_negative { + value |= -1 << shift; + } + return Ok((value, i + 1)); + } + } + Err(()) +} + +impl Parse<()> for i32 { + fn parse(_ctx: (), bytes: &[u8], cursor: &mut usize) -> Result { + match decode_i32(&bytes[*cursor..]) { + Ok((value, len)) => { + *cursor += len; + Ok(value) + } + Err(()) => Err(ParseError { + offset: *cursor, + message: format!( + "Failed to decode i32 as LEB-128 from bytes: {:2x?}", + &bytes[*cursor..][..MAX_SIZE_ENCODED_U32] + ), + }), + } + } +} + +/// Decode a signed 64-bit integer from the provided buffer in LEB-128 format +/// Return the integer itself and the offset after it ends +fn decode_i64(bytes: &[u8]) -> Result<(i64, usize), ()> { + let mut value = 0; + let mut shift = 0; + for (i, byte) in bytes.iter().take(MAX_SIZE_ENCODED_U64).enumerate() { + value |= ((byte & 0x7f) as i64) << shift; + shift += 7; + if (byte & 0x80) == 0 { + let is_negative = byte & 0x40 != 0; + if shift < 64 && is_negative { + value |= -1 << shift; + } + return Ok((value, i + 1)); + } + } + Err(()) +} + +impl Parse<()> for i64 { + fn parse(_ctx: (), bytes: &[u8], cursor: &mut usize) -> Result { + match decode_i64(&bytes[*cursor..]) { + Ok((value, len)) => { + *cursor += len; + Ok(value) + } + Err(()) => Err(ParseError { + offset: *cursor, + message: format!( + "Failed to decode i64 as LEB-128 from bytes: {:2x?}", + &bytes[*cursor..][..MAX_SIZE_ENCODED_U64] + ), + }), + } + } +} + +impl<'a> Parse<&'a Bump> for &'a str { + fn parse(arena: &'a Bump, bytes: &[u8], cursor: &mut usize) -> Result { + let len = u32::parse((), bytes, cursor)?; + let end = *cursor + len as usize; + let bytes: &[u8] = &bytes[*cursor..end]; + let copy = arena.alloc_slice_copy(bytes); + let s = unsafe { std::str::from_utf8_unchecked(copy) }; + *cursor = end; + Ok(s) + } +} + +pub fn parse_variable_size_items<'a, T>( + arena: &'a Bump, + bytes: &[u8], + cursor: &mut usize, +) -> Result, ParseError> +where + T: Parse<&'a Bump>, +{ + let len = u32::parse((), bytes, cursor)?; + let mut vector: Vec<'a, T> = Vec::with_capacity_in(len as usize, arena); + for _ in 0..len { + let item = T::parse(arena, bytes, cursor)?; + vector.push(item); + } + Ok(vector) +} + +pub fn parse_fixed_size_items<'a, T>( + arena: &'a Bump, + bytes: &[u8], + cursor: &mut usize, +) -> Result, ParseError> +where + T: Parse<()>, +{ + let len = u32::parse((), bytes, cursor)?; + let mut vector: Vec<'a, T> = Vec::with_capacity_in(len as usize, arena); + for _ in 0..len { + let item = T::parse((), bytes, cursor)?; + vector.push(item); + } + Ok(vector) +} + +/// Skip over serialized bytes for a type +/// This may, or may not, require looking at the byte values +pub trait SkipBytes: Sized { + fn skip_bytes(bytes: &[u8], cursor: &mut usize) -> Result<(), ParseError>; +} + +impl SkipBytes for u32 { + fn skip_bytes(bytes: &[u8], cursor: &mut usize) -> Result<(), ParseError> { + const MAX_LEN: usize = 5; + for (i, byte) in bytes.iter().enumerate().skip(*cursor).take(MAX_LEN) { + if byte & 0x80 == 0 { + *cursor = i + 1; + return Ok(()); + } + } + Err(ParseError { + offset: *cursor, + message: "Invalid LEB encoding".into(), + }) + } +} + +impl SkipBytes for u64 { + fn skip_bytes(bytes: &[u8], cursor: &mut usize) -> Result<(), ParseError> { + const MAX_LEN: usize = 10; + for (i, byte) in bytes.iter().enumerate().skip(*cursor).take(MAX_LEN) { + if byte & 0x80 == 0 { + *cursor = i + 1; + return Ok(()); + } + } + Err(ParseError { + offset: *cursor, + message: "Invalid LEB encoding".into(), + }) + } +} + +impl SkipBytes for u8 { + fn skip_bytes(_bytes: &[u8], cursor: &mut usize) -> Result<(), ParseError> { + *cursor += 1; + Ok(()) + } +} + +/// Note: This is just for skipping over Wasm bytes. We don't actually care about String vs str! +impl SkipBytes for String { + fn skip_bytes(bytes: &[u8], cursor: &mut usize) -> Result<(), ParseError> { + let len = u32::parse((), bytes, cursor)?; + + if false { + let str_bytes = &bytes[*cursor..(*cursor + len as usize)]; + println!( + "Skipping string {:?}", + std::str::from_utf8(str_bytes).unwrap() + ); + } + + *cursor += len as usize; + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{parse::decode_u32, SerialBuffer}; + + #[test] + fn test_decode_u32() { + assert_eq!(decode_u32(&[0]), Ok((0, 1))); + assert_eq!(decode_u32(&[64]), Ok((64, 1))); + assert_eq!(decode_u32(&[0x7f]), Ok((0x7f, 1))); + assert_eq!(decode_u32(&[0x80, 0x01]), Ok((0x80, 2))); + assert_eq!(decode_u32(&[0xff, 0x7f]), Ok((0x3fff, 2))); + assert_eq!(decode_u32(&[0x80, 0x80, 0x01]), Ok((0x4000, 3))); + assert_eq!( + decode_u32(&[0xff, 0xff, 0xff, 0xff, 0x0f]), + Ok((u32::MAX, MAX_SIZE_ENCODED_U32)) + ); + assert!(matches!(decode_u32(&[0x80; 6]), Err(_))); + assert!(matches!(decode_u32(&[0x80; 2]), Err(_))); + assert!(matches!(decode_u32(&[]), Err(_))); + } + + #[test] + fn test_parse_u32_sequence() { + let bytes = &[0, 0x80, 0x01, 0xff, 0xff, 0xff, 0xff, 0x0f]; + let expected = [0, 128, u32::MAX]; + let mut cursor = 0; + + assert_eq!(u32::parse((), bytes, &mut cursor).unwrap(), expected[0]); + assert_eq!(cursor, 1); + + assert_eq!(u32::parse((), bytes, &mut cursor).unwrap(), expected[1]); + assert_eq!(cursor, 3); + + assert_eq!(u32::parse((), bytes, &mut cursor).unwrap(), expected[2]); + assert_eq!(cursor, 8); + } + + #[test] + fn test_encode_decode_i32() { + encode_decode_i32_help(3); + encode_decode_i32_help(65535); + encode_decode_i32_help(-2); + encode_decode_i32_help(-65536); + encode_decode_i32_help(i32::MIN); + encode_decode_i32_help(i32::MAX); + } + + fn encode_decode_i32_help(value: i32) { + let arena = &Bump::new(); + let mut buffer = Vec::with_capacity_in(MAX_SIZE_ENCODED_U32, arena); + buffer.encode_i32(value); + let mut cursor = 0; + let parsed = i32::parse((), &buffer, &mut cursor).unwrap(); + assert_eq!(parsed, value); + } +} diff --git a/compiler/gen_wasm/src/wasm_module/sections.rs b/crates/wasm_module/src/sections.rs similarity index 78% rename from compiler/gen_wasm/src/wasm_module/sections.rs rename to crates/wasm_module/src/sections.rs index 6d6b2b3bda..99ef4db60c 100644 --- a/compiler/gen_wasm/src/wasm_module/sections.rs +++ b/crates/wasm_module/src/sections.rs @@ -1,17 +1,17 @@ use std::fmt::{Debug, Formatter}; +use std::io::Write; use bumpalo::collections::vec::Vec; use bumpalo::Bump; use roc_error_macros::internal_error; -use super::dead_code::{ - copy_preloads_shrinking_dead_fns, parse_preloads_call_graph, trace_call_graph, - PreloadsCallGraph, -}; +use crate::{Value, DUMMY_FUNCTION}; + +use super::linking::{LinkingSection, SymInfo, WasmObjectSymbol}; use super::opcodes::OpCode; use super::parse::{Parse, ParseError, SkipBytes}; use super::serialize::{SerialBuffer, Serialize, MAX_SIZE_ENCODED_U32}; -use super::{CodeBuilder, ValueType}; +use super::ValueType; /******************************************************************* * @@ -212,6 +212,46 @@ impl<'a> Serialize for Signature<'a> { } } +#[derive(Debug)] +pub struct SignatureParamsIter<'a> { + bytes: &'a [u8], + index: usize, + end: usize, +} + +impl<'a> Iterator for SignatureParamsIter<'a> { + type Item = ValueType; + + fn next(&mut self) -> Option { + if self.index >= self.end { + None + } else { + self.bytes.get(self.index).map(|b| { + self.index += 1; + ValueType::from(*b) + }) + } + } + + fn size_hint(&self) -> (usize, Option) { + let size = self.end - self.index; + (size, Some(size)) + } +} + +impl<'a> ExactSizeIterator for SignatureParamsIter<'a> {} + +impl<'a> DoubleEndedIterator for SignatureParamsIter<'a> { + fn next_back(&mut self) -> Option { + if self.end == 0 { + None + } else { + self.end -= 1; + self.bytes.get(self.end).map(|b| ValueType::from(*b)) + } + } +} + #[derive(Debug)] pub struct TypeSection<'a> { /// Private. See WasmModule::add_function_signature @@ -221,6 +261,14 @@ pub struct TypeSection<'a> { } impl<'a> TypeSection<'a> { + pub fn new(arena: &'a Bump) -> Self { + TypeSection { + arena, + bytes: Vec::new_in(arena), + offsets: Vec::new_in(arena), + } + } + /// Find a matching signature or insert a new one. Return the index. pub fn insert(&mut self, signature: Signature<'a>) -> u32 { let mut sig_bytes = Vec::with_capacity_in(signature.param_types.len() + 4, self.arena); @@ -249,6 +297,25 @@ impl<'a> TypeSection<'a> { pub fn is_empty(&self) -> bool { self.bytes.is_empty() } + + pub fn look_up(&'a self, sig_index: u32) -> (SignatureParamsIter<'a>, Option) { + let mut offset = self.offsets[sig_index as usize]; + offset += 1; // separator + let param_count = u32::parse((), &self.bytes, &mut offset).unwrap() as usize; + let params_iter = SignatureParamsIter { + bytes: &self.bytes[offset..][..param_count], + index: 0, + end: param_count, + }; + offset += param_count; + + let return_type = if self.bytes[offset] == 0 { + None + } else { + Some(ValueType::from(self.bytes[offset + 1])) + }; + (params_iter, return_type) + } } impl<'a> Section<'a> for TypeSection<'a> { @@ -306,7 +373,7 @@ impl<'a> Serialize for TypeSection<'a> { * *******************************************************************/ -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Eq)] pub enum ImportDesc { Func { signature_index: u32 }, Table { ty: TableType }, @@ -362,7 +429,7 @@ impl Serialize for ImportDesc { } } -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Eq)] pub struct Import<'a> { pub module: &'a str, pub name: &'a str, @@ -404,6 +471,10 @@ impl<'a> Import<'a> { ImportDesc::Global { .. } => 2, } } + + pub fn is_function(&self) -> bool { + matches!(self.description, ImportDesc::Func { .. }) + } } impl<'a> Serialize for Import<'a> { @@ -422,6 +493,12 @@ pub struct ImportSection<'a> { impl<'a> ImportSection<'a> { const ID: SectionId = SectionId::Import; + pub fn new(arena: &'a Bump) -> Self { + ImportSection { + imports: Vec::new_in(arena), + } + } + pub fn size(&self) -> usize { self.imports.iter().map(|imp| imp.size()).sum() } @@ -435,10 +512,7 @@ impl<'a> ImportSection<'a> { } pub fn function_count(&self) -> usize { - self.imports - .iter() - .filter(|imp| matches!(imp.description, ImportDesc::Func { .. })) - .count() + self.imports.iter().filter(|imp| imp.is_function()).count() } } @@ -488,6 +562,12 @@ pub struct FunctionSection<'a> { } impl<'a> FunctionSection<'a> { + pub fn new(arena: &'a Bump) -> Self { + FunctionSection { + signatures: Vec::new_in(arena), + } + } + pub fn add_sig(&mut self, sig_id: u32) { self.signatures.push(sig_id); } @@ -527,8 +607,8 @@ impl<'a> Serialize for FunctionSection<'a> { * * Table section * - * Defines tables used for indirect references to host memory. - * The table *contents* are elsewhere, in the ElementSection. + * Defines tables used for indirect references to external code or data. + * The table *contents* are in the ElementSection. * *******************************************************************/ @@ -548,12 +628,12 @@ impl Parse<()> for RefType { 0x6f => Ok(Self::Extern), _ => Err(ParseError { offset: *cursor - 1, - message: format!("Invalid RefType 0x{:2x}", byte), + message: format!("Invalid RefType 0x{byte:2x}"), }), } } } -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Eq)] pub struct TableType { pub ref_type: RefType, pub limits: Limits, @@ -590,6 +670,16 @@ pub struct TableSection { impl TableSection { const ID: SectionId = SectionId::Table; + #[allow(clippy::new_without_default)] + pub fn new() -> Self { + TableSection { + function_table: TableType { + ref_type: RefType::Func, + limits: Limits::Min(0), + }, + } + } + pub fn size(&self) -> usize { let section_id_bytes = 1; let section_length_bytes = 1; @@ -661,7 +751,7 @@ impl Serialize for TableSection { * *******************************************************************/ -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Eq)] pub enum Limits { Min(u32), MinMax(u32, u32), @@ -703,6 +793,9 @@ impl SkipBytes for Limits { impl Parse<()> for Limits { fn parse(_: (), bytes: &[u8], cursor: &mut usize) -> Result { + if *cursor >= bytes.len() { + return Ok(Limits::Min(0)); + } let variant_id = bytes[*cursor]; *cursor += 1; @@ -741,6 +834,25 @@ impl<'a> MemorySection<'a> { MemorySection { count: 1, bytes } } } + + pub fn min_bytes(&self) -> Result { + let mut cursor = 0; + let memory_limits = Limits::parse((), &self.bytes, &mut cursor)?; + let min_pages = match memory_limits { + Limits::Min(pages) | Limits::MinMax(pages, _) => pages, + }; + Ok(min_pages * MemorySection::PAGE_SIZE) + } + + pub fn max_bytes(&self) -> Result, ParseError> { + let mut cursor = 0; + let memory_limits = Limits::parse((), &self.bytes, &mut cursor)?; + let bytes = match memory_limits { + Limits::Min(_) => None, + Limits::MinMax(_, pages) => Some(pages * MemorySection::PAGE_SIZE), + }; + Ok(bytes) + } } section_impl!(MemorySection, SectionId::Memory); @@ -751,7 +863,7 @@ section_impl!(MemorySection, SectionId::Memory); * *******************************************************************/ -#[derive(Debug, PartialEq, Clone, Copy)] +#[derive(Debug, PartialEq, Eq, Clone, Copy)] pub struct GlobalType { pub value_type: ValueType, pub is_mutable: bool, @@ -822,6 +934,59 @@ impl ConstExpr { _ => internal_error!("Expected ConstExpr to be I32"), } } + + // ConstExpr and Value are separate types in case we ever need to support + // arbitrary constant expressions, rather than just i32.const and friends. + fn as_value(&self) -> Value { + match self { + ConstExpr::I32(x) => Value::I32(*x), + ConstExpr::I64(x) => Value::I64(*x), + ConstExpr::F32(x) => Value::F32(*x), + ConstExpr::F64(x) => Value::F64(*x), + } + } +} + +impl Parse<()> for ConstExpr { + fn parse(_ctx: (), bytes: &[u8], cursor: &mut usize) -> Result { + let opcode = OpCode::from(bytes[*cursor]); + *cursor += 1; + + let result = match opcode { + OpCode::I32CONST => { + let x = i32::parse((), bytes, cursor)?; + Ok(ConstExpr::I32(x)) + } + OpCode::I64CONST => { + let x = i64::parse((), bytes, cursor)?; + Ok(ConstExpr::I64(x)) + } + OpCode::F32CONST => { + let mut b = [0; 4]; + b.copy_from_slice(&bytes[*cursor..][..4]); + Ok(ConstExpr::F32(f32::from_le_bytes(b))) + } + OpCode::F64CONST => { + let mut b = [0; 8]; + b.copy_from_slice(&bytes[*cursor..][..8]); + Ok(ConstExpr::F64(f64::from_le_bytes(b))) + } + _ => Err(ParseError { + offset: *cursor, + message: format!("Unsupported opcode {opcode:?} in constant expression."), + }), + }; + + if bytes[*cursor] != OpCode::END as u8 { + return Err(ParseError { + offset: *cursor, + message: "Expected END opcode in constant expression.".into(), + }); + } + *cursor += 1; + + result + } } impl Serialize for ConstExpr { @@ -880,6 +1045,13 @@ pub struct GlobalSection<'a> { } impl<'a> GlobalSection<'a> { + pub fn new(arena: &'a Bump) -> Self { + GlobalSection { + count: 0, + bytes: Vec::new_in(arena), + } + } + pub fn parse_u32_at_index(&self, index: u32) -> Result { let mut cursor = 0; for _ in 0..index { @@ -894,6 +1066,17 @@ impl<'a> GlobalSection<'a> { global.serialize(&mut self.bytes); self.count += 1; } + + pub fn initial_values<'b>(&self, arena: &'b Bump) -> Vec<'b, Value> { + let mut cursor = 0; + let iter = (0..self.count) + .map(|_| { + GlobalType::skip_bytes(&self.bytes, &mut cursor)?; + ConstExpr::parse((), &self.bytes, &mut cursor).map(|x| x.as_value()) + }) + .filter_map(|r| r.ok()); + Vec::from_iter_in(iter, arena) + } } section_impl!(GlobalSection, SectionId::Global); @@ -961,6 +1144,12 @@ pub struct ExportSection<'a> { impl<'a> ExportSection<'a> { const ID: SectionId = SectionId::Export; + pub fn new(arena: &'a Bump) -> Self { + ExportSection { + exports: Vec::new_in(arena), + } + } + pub fn append(&mut self, export: Export<'a>) { self.exports.push(export); } @@ -1022,12 +1211,19 @@ enum ElementSegmentFormatId { /// A Segment initialises a subrange of elements in a table. Normally there's just one Segment. #[derive(Debug)] -struct ElementSegment<'a> { - offset: ConstExpr, // The starting table index for the segment - fn_indices: Vec<'a, u32>, +pub struct ElementSegment<'a> { + pub offset: ConstExpr, // The starting table index for the segment + pub fn_indices: Vec<'a, u32>, } impl<'a> ElementSegment<'a> { + pub fn new(arena: &'a Bump) -> Self { + ElementSegment { + offset: ConstExpr::I32(0), + fn_indices: Vec::new_in(arena), + } + } + fn size(&self) -> usize { let variant_id = 1; let constexpr_opcode = 1; @@ -1080,17 +1276,23 @@ impl<'a> Serialize for ElementSegment<'a> { /// The only currently supported Element type is a function reference, used for indirect calls. #[derive(Debug)] pub struct ElementSection<'a> { - segments: Vec<'a, ElementSegment<'a>>, + pub segments: Vec<'a, ElementSegment<'a>>, } impl<'a> ElementSection<'a> { const ID: SectionId = SectionId::Element; + pub fn new(arena: &'a Bump) -> Self { + ElementSection { + segments: Vec::new_in(arena), + } + } + /// Get a table index for a function (equivalent to a function pointer) /// The function will be inserted into the table if it's not already there. /// This index is what the call_indirect instruction expects. /// (This works mostly the same as function pointers, except hackers can't jump to arbitrary code) - pub fn get_fn_table_index(&mut self, fn_index: u32) -> i32 { + pub fn get_or_insert_fn(&mut self, fn_index: u32) -> i32 { // In practice there is always one segment. We allow a bit more generality by using the last one. let segment = self.segments.last_mut().unwrap(); let offset = segment.offset.unwrap_i32(); @@ -1121,12 +1323,16 @@ impl<'a> ElementSection<'a> { self.segments.iter().map(|seg| seg.size()).sum() } - pub fn indirect_callees(&self, arena: &'a Bump) -> Vec<'a, u32> { - let mut result = bumpalo::vec![in arena]; - for segment in self.segments.iter() { - result.extend_from_slice(&segment.fn_indices); - } - result + pub fn is_empty(&self) -> bool { + self.segments.iter().all(|seg| seg.fn_indices.is_empty()) + } + + /// Look up a "function pointer" (element index) and return the function index. + pub fn lookup(&self, element_index: u32) -> Option { + self.segments.iter().find_map(|seg| { + let adjusted_index = element_index as usize - seg.offset.unwrap_i32() as usize; + seg.fn_indices.get(adjusted_index).copied() + }) } } @@ -1158,9 +1364,11 @@ impl<'a> Parse<&'a Bump> for ElementSection<'a> { impl<'a> Serialize for ElementSection<'a> { fn serialize(&self, buffer: &mut T) { - let header_indices = write_section_header(buffer, Self::ID); - self.segments.serialize(buffer); - update_section_size(buffer, header_indices); + if !self.is_empty() { + let header_indices = write_section_header(buffer, Self::ID); + self.segments.serialize(buffer); + update_section_size(buffer, header_indices); + } } } @@ -1172,28 +1380,34 @@ impl<'a> Serialize for ElementSection<'a> { #[derive(Debug)] pub struct CodeSection<'a> { - pub preloaded_count: u32, - pub preloaded_reloc_offset: u32, - pub preloaded_bytes: Vec<'a, u8>, - pub linking_dummy_count: u32, - pub code_builders: Vec<'a, CodeBuilder<'a>>, - dead_code_metadata: PreloadsCallGraph<'a>, + pub function_count: u32, + pub section_offset: u32, + pub bytes: Vec<'a, u8>, + /// The start of each function + pub function_offsets: Vec<'a, u32>, + /// Dead imports are replaced with dummy functions in CodeSection + pub dead_import_dummy_count: u32, } impl<'a> CodeSection<'a> { - pub fn size(&self) -> usize { - let builders_size: usize = self.code_builders.iter().map(|cb| cb.size()).sum(); + pub fn new(arena: &'a Bump) -> Self { + CodeSection { + function_count: 0, + section_offset: 0, + bytes: Vec::new_in(arena), + function_offsets: Vec::new_in(arena), + dead_import_dummy_count: 0, + } + } - MAX_SIZE_SECTION_HEADER + self.preloaded_bytes.len() + builders_size + pub fn size(&self) -> usize { + MAX_SIZE_SECTION_HEADER + self.bytes.len() } pub fn parse( arena: &'a Bump, module_bytes: &[u8], cursor: &mut usize, - import_signatures: &[u32], - function_signatures: &[u32], - indirect_callees: &[u32], ) -> Result { if module_bytes[*cursor] != SectionId::Code as u8 { return Err(ParseError { @@ -1202,94 +1416,53 @@ impl<'a> CodeSection<'a> { }); } *cursor += 1; - let section_size = u32::parse((), module_bytes, cursor)?; - let count_start = *cursor; - let count = u32::parse((), module_bytes, cursor)?; - let function_bodies_start = *cursor; - let next_section_start = count_start + section_size as usize; - *cursor = next_section_start; + let section_size = u32::parse((), module_bytes, cursor)? as usize; + let section_body_start = *cursor; + let function_count = u32::parse((), module_bytes, cursor)?; + let next_section_start = section_body_start + section_size; - // Relocation offsets are based from the start of the section body, which includes function count - // But preloaded_bytes does not include the function count, only the function bodies! - // When we do relocations, we need to account for this - let preloaded_reloc_offset = (function_bodies_start - count_start) as u32; + // `bytes` must include the function count for linker offsets to be correct. + let mut bytes = Vec::with_capacity_in(section_size + section_size / 2, arena); + bytes.extend_from_slice(&module_bytes[section_body_start..*cursor]); - let preloaded_bytes = Vec::from_iter_in( - module_bytes[function_bodies_start..next_section_start] - .iter() - .copied(), - arena, - ); + let mut function_offsets = Vec::with_capacity_in(function_count as usize, arena); - let dead_code_metadata = parse_preloads_call_graph( - arena, - &preloaded_bytes, - import_signatures, - function_signatures, - indirect_callees, - )?; + // While copying the code bytes, also note where each function starts & ends + // Later we will use this for dead code elimination + while *cursor < next_section_start { + let fn_start = *cursor; + function_offsets.push((fn_start - section_body_start) as u32); + let fn_length = u32::parse((), module_bytes, cursor)? as usize; + *cursor += fn_length; + bytes.extend_from_slice(&module_bytes[fn_start..*cursor]); + } + + debug_assert_eq!(function_offsets.len(), function_count as usize); Ok(CodeSection { - preloaded_count: count, - preloaded_reloc_offset, - preloaded_bytes, - linking_dummy_count: 0, - code_builders: Vec::with_capacity_in(0, arena), - dead_code_metadata, + function_count, + section_offset: section_body_start as u32, + bytes, + function_offsets, + dead_import_dummy_count: 0, }) } - - pub(super) fn remove_dead_preloads>( - &mut self, - arena: &'a Bump, - import_fn_count: usize, - exported_fns: &[u32], - called_preload_fns: T, - ) { - let live_ext_fn_indices = trace_call_graph( - arena, - &self.dead_code_metadata, - exported_fns, - called_preload_fns, - ); - - let mut buffer = Vec::with_capacity_in(self.preloaded_bytes.len(), arena); - - copy_preloads_shrinking_dead_fns( - arena, - &mut buffer, - &self.dead_code_metadata, - &self.preloaded_bytes, - import_fn_count + self.linking_dummy_count as usize, - live_ext_fn_indices, - ); - - self.preloaded_bytes = buffer; - } } impl<'a> Serialize for CodeSection<'a> { fn serialize(&self, buffer: &mut T) { let header_indices = write_section_header(buffer, SectionId::Code); - buffer.encode_u32( - self.linking_dummy_count + self.preloaded_count + self.code_builders.len() as u32, - ); + buffer.encode_u32(self.dead_import_dummy_count + self.function_count); // Insert dummy functions, requested by our linking logic. // This helps to minimise the number of functions we need to move around during linking. - let arena = self.code_builders[0].arena; - let dummy = CodeBuilder::dummy(arena); - for _ in 0..self.linking_dummy_count { - dummy.serialize(buffer); + for _ in 0..self.dead_import_dummy_count { + DUMMY_FUNCTION.serialize(buffer); } - // host + builtin functions - buffer.append_slice(&self.preloaded_bytes); - - // Roc functions - for code_builder in self.code_builders.iter() { - code_builder.serialize(buffer); - } + // real functions + let first_fn_start = self.function_offsets[0] as usize; + buffer.append_slice(&self.bytes[first_fn_start..]); update_section_size(buffer, header_indices); } @@ -1349,7 +1522,7 @@ impl Parse<()> for DataMode { } else { Err(ParseError { offset: *cursor - 1, - message: format!("Data section: invalid DataMode variant 0x{:x}", variant_id), + message: format!("Data section: invalid DataMode variant 0x{variant_id:x}"), }) } } @@ -1378,6 +1551,14 @@ pub struct DataSection<'a> { impl<'a> DataSection<'a> { const ID: SectionId = SectionId::Data; + pub fn new(arena: &'a Bump) -> Self { + DataSection { + end_addr: 0, + count: 0, + bytes: Vec::new_in(arena), + } + } + pub fn size(&self) -> usize { MAX_SIZE_SECTION_HEADER + self.bytes.len() } @@ -1388,10 +1569,41 @@ impl<'a> DataSection<'a> { segment.serialize(&mut self.bytes); index } + + pub fn load_into(&self, memory: &mut [u8]) -> Result<(), String> { + let mut cursor = 0; + for _ in 0..self.count { + let mode = + DataMode::parse((), &self.bytes, &mut cursor).map_err(|e| format!("{e:?}"))?; + let start = match mode { + DataMode::Active { + offset: ConstExpr::I32(addr), + } => addr as usize, + _ => { + continue; + } + }; + let len32 = u32::parse((), &self.bytes, &mut cursor).map_err(|e| format!("{e:?}"))?; + let len = len32 as usize; + let mut target_slice = &mut memory[start..][..len]; + target_slice + .write(&self.bytes[cursor..][..len]) + .map_err(|e| format!("{e:?}"))?; + cursor += len; + } + Ok(()) + } } impl<'a> Parse<&'a Bump> for DataSection<'a> { fn parse(arena: &'a Bump, module_bytes: &[u8], cursor: &mut usize) -> Result { + if *cursor >= module_bytes.len() { + return Ok(DataSection { + end_addr: 0, + count: 0, + bytes: Vec::::new_in(arena), + }); + } let (count, range) = parse_section(Self::ID, module_bytes, cursor)?; let end = range.end; @@ -1443,6 +1655,10 @@ pub struct OpaqueSection<'a> { } impl<'a> OpaqueSection<'a> { + pub fn new() -> Self { + OpaqueSection { bytes: &[] } + } + pub fn size(&self) -> usize { self.bytes.len() } @@ -1512,30 +1728,51 @@ impl<'a> NameSection<'a> { pub fn append_function(&mut self, index: u32, name: &'a str) { self.function_names.push((index, name)); } + + pub fn new(arena: &'a Bump) -> Self { + NameSection { + function_names: bumpalo::vec![in arena], + } + } + + pub fn from_imports_and_linking_data( + arena: &'a Bump, + import: &ImportSection<'a>, + linking: &LinkingSection<'a>, + ) -> Self { + let import_fns = import.imports.iter().filter(|imp| imp.is_function()); + let import_names = Vec::from_iter_in(import_fns.map(|imp| imp.name), arena); + + let symbols = linking.symbol_table.iter(); + let names = symbols.filter_map(|sym_info| match sym_info { + SymInfo::Function(WasmObjectSymbol::ExplicitlyNamed { index, name, .. }) => { + Some((*index, *name)) + } + SymInfo::Function(WasmObjectSymbol::ImplicitlyNamed { index, .. }) => { + Some((*index, import_names[*index as usize])) + } + _ => None, + }); + + let mut function_names = Vec::from_iter_in(names, arena); + function_names.sort_by_key(|(idx, _name)| *idx); + + NameSection { function_names } + } } impl<'a> Parse<&'a Bump> for NameSection<'a> { fn parse(arena: &'a Bump, module_bytes: &[u8], cursor: &mut usize) -> Result { + let cursor_start = *cursor; + // If we're already past the end of the preloaded file then there is no Name section if *cursor >= module_bytes.len() { - return Ok(NameSection { - function_names: bumpalo::vec![in arena], - }); + return Ok(Self::new(arena)); } // Custom section ID - let section_id_byte = module_bytes[*cursor]; - if section_id_byte != Self::ID as u8 { - let message = format!( - "Expected section ID 0x{:x}, but found 0x{:x} at offset 0x{:x}", - Self::ID as u8, - section_id_byte, - *cursor - ); - return Err(ParseError { - message, - offset: *cursor, - }); + if module_bytes[*cursor] != Self::ID as u8 { + return Ok(Self::new(arena)); } *cursor += 1; @@ -1545,15 +1782,10 @@ impl<'a> Parse<&'a Bump> for NameSection<'a> { let section_name = <&'a str>::parse(arena, module_bytes, cursor)?; if section_name != Self::NAME { - let message = format!( - "Expected Custom section {:?}, found {:?}", - Self::NAME, - section_name - ); - return Err(ParseError { - message, - offset: *cursor, - }); + // This is a different Custom section. This host has no debug info. + // Not a parse error, just an empty section. + *cursor = cursor_start; + return Ok(Self::new(arena)); } // Find function names subsection @@ -1627,7 +1859,7 @@ impl<'a> Debug for NameSection<'a> { writeln!(f, "NameSection")?; for (index, name) in self.function_names.iter() { - writeln!(f, " {:4}: {}", index, name)?; + writeln!(f, " {index:4}: {name}")?; } Ok(()) diff --git a/compiler/gen_wasm/src/wasm_module/serialize.rs b/crates/wasm_module/src/serialize.rs similarity index 99% rename from compiler/gen_wasm/src/wasm_module/serialize.rs rename to crates/wasm_module/src/serialize.rs index 64c45e02b5..0bad2c1c5f 100644 --- a/compiler/gen_wasm/src/wasm_module/serialize.rs +++ b/crates/wasm_module/src/serialize.rs @@ -6,8 +6,9 @@ use std::fmt::Debug; /// In practice, this saves space, since small numbers used more often than large numbers. /// Of course there is a price for this - an encoded U32 can be up to 5 bytes wide. pub const MAX_SIZE_ENCODED_U32: usize = 5; +pub const MAX_SIZE_ENCODED_U64: usize = 10; -pub(super) trait Serialize { +pub trait Serialize { fn serialize(&self, buffer: &mut T); } diff --git a/default.nix b/default.nix new file mode 100644 index 0000000000..f05dbb5e43 --- /dev/null +++ b/default.nix @@ -0,0 +1,10 @@ +(import + ( + let lock = builtins.fromJSON (builtins.readFile ./flake.lock); + in fetchTarball { + url = + "https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz"; + sha256 = lock.nodes.flake-compat.locked.narHash; + } + ) + { src = ./.; }).defaultNix diff --git a/design/editor/design.md b/design/editor/design.md new file mode 100644 index 0000000000..d5b17fc2be --- /dev/null +++ b/design/editor/design.md @@ -0,0 +1,24 @@ + +# Open Questions + +Should the editor organize all UI into a tree for easy altering/communication with plugins? + +# Why make a new editor? + +- Complete freedom and control to create a delightful roc editing, debugging, and execution monitoring experience. Everything can be optimized for this, there is no need to accommodate other use cases. +- So that plugins can be developed that can ship with roc packages. This allows all plugins to look and behave the same on all operating systems. + - Why plugins: + - To make it easier and more enjoyable to achieve your goal with the package. + - To provide transparency for how the package works. + - Opportunity to innovate on user experience with minimal friction install. +- Not limited by the language server protocol (LSP). +- To create a foundation that allows for easy experimentation. + +# Why make a projectional editor + + +- It requires a lot less work to communicate with the compiler because we have a valid AST at all time. +- Similarly, we never have to deal with partial expressions that have not been fully typed out yet. +- The user never has to fiddle with formatting. +- It allows plugins to work with typed values instead of a string that is connected with a typed value and where any changes to the typed value would have to produce a string that is sensibly formatted similar the formatting of the original string. + diff --git a/design/editor/editor-ideas.md b/design/editor/editor-ideas.md new file mode 100644 index 0000000000..5b89e18506 --- /dev/null +++ b/design/editor/editor-ideas.md @@ -0,0 +1,437 @@ +# Editor Ideas + +(For background, [this talk](https://youtu.be/ZnYa99QoznE?t=4790) has an overview of the design goals for the editor.) + +Here are some ideas and interesting resources for the editor. Feel free to make a PR to add more! + +## Sources of Potential Inspiration + +These are potentially inspirational resources for the editor's design. + +Nice collection of research on innovative editors, [link](https://futureofcoding.org/catalog/). + +### Package-specific editor integrations + +(Or possibly module-specific integrations, type-specific integrations, etc.) + +- [What FP can learn from Smalltalk](https://youtu.be/baxtyeFVn3w) by [Aditya Siram](https://github.com/deech) +- [Moldable development](https://youtu.be/Pot9GnHFOVU) by [Tudor Gîrba](https://github.com/girba) +- [Unity game engine](https://unity.com/) + - Scripts can expose values as text inputs, sliders, checkboxes, etc or even generate custom graphical inputs + - Drag-n-drop game objects and component into script interfaces +- [How to Visualize Data Structures in VS Code](https://addyosmani.com/blog/visualize-data-structures-vscode/) + +### Live Interactivity + +- [Up and Down the Ladder of Abstraction](http://worrydream.com/LadderOfAbstraction/) by [Bret Victor](http://worrydream.com/) +- [7 Bret Victor talks](https://www.youtube.com/watch?v=PUv66718DII&list=PLS4RYH2XfpAmswi1WDU6lwwggruEZrlPH) +- [Against the Current](https://youtu.be/WT2CMS0MxJ0) by [Chris Granger](https://github.com/ibdknox/) +- [Sketch-n-Sketch: Interactive SVG Programming with Direct Manipulation](https://youtu.be/YuGVC8VqXz0) by [Ravi Chugh](http://people.cs.uchicago.edu/~rchugh/) +- [Xi](https://xi-editor.io/) modern text editor with concurrent editing (related to [Druid](https://github.com/linebender/druid)) +- [Self](https://selflanguage.org/) programming language +- [Primitive](https://primitive.io/) code exploration in Virtual Reality +- [Luna](https://www.luna-lang.org/) language for interactive data processing and visualization +- [Hazel Livelits](https://hazel.org/papers/livelits-paper.pdf) interactive plugins, see GIF's [here](https://twitter.com/disconcision/status/1408155781120376833). +- [Thorough review](https://drossbucket.com/2021/06/30/hacker-news-folk-wisdom-on-visual-programming/) of pros and cons of text versus visual programming. + +### Good error messages + +- [https://twitter.com/firstdrafthell/status/1427364851593224197/photo/1] very clean error message layout +- [how to write good error message](https://twitter.com/vitalyf/status/1582270207229251585?s=20&t=MorLGshEbEVdRFJ10d2I4A) +- If the user explicitly allows it, we can keep record of which errors take a long time to fix. This way we know where to focus our efforts for improving error messages. + +### Debugging + +- [VS code debug visualization](https://marketplace.visualstudio.com/items?itemName=hediet.debug-visualizer) +- [Algorithm visualization for javascript](https://algorithm-visualizer.org) +- [godbolt.org Compiler Explorer](https://godbolt.org/) +- [whitebox debug visualization](https://vimeo.com/483795097) +- [Hest](https://ivanish.ca/hest-time-travel/) tool for making highly interactive simulations. +- [replit](https://replit.com/) collaborative browser based IDE. +- [paper](https://openreview.net/pdf?id=SJeqs6EFvB) on finding and fixing bugs automatically. +- [specialized editors that can be embedded in main editor](https://elliot.website/editor/) +- Say you have a failing test that used to work, it would be very valuable to see all code that was changed that was used only by that test. +e.g. you have a test `calculate_sum_test` that only uses the function `add`, when the test fails you should be able to see a diff showing only what changed for the function `add`. It would also be great to have a diff of [expression values](https://homepages.cwi.nl/~storm/livelit/images/bret.png) Bret Victor style. An ambitious project would be to suggest or automatically try fixes based on these diffs. +- I think it could be possible to create a minimal reproduction of a program / block of code / code used by a single test. So for a failing unit test I would expect it to extract imports, the platform, types and functions that are necessary to run only that unit test and put them in a standalone roc project. This would be useful for sharing bugs with library+application authors and colleagues, for profiling or debugging with all "clutter" removed. +- Ability to share program state at a breakpoint with someone else. +- For debugging we should aim for maximal useful observability. For example Rust's enum values can not be easily viewed in the CodeLLDB debugger, you actually need to call a print method that does pattern matching to be able to view useful information. +- We previuously discussed recording full traces of programs so they do not have to be re-run multiple times in the debugging process. We should encourage roc developers to experiment with creating debugging representations of this AST+"execution trace", it could lead to some cool stuff. +- We previuously mentioned showing expression values next to the code. I think when debugging it would be valuable to focus more on these valuas/data. A possible way to do this would be to create scrollable view(without need to jump between files) of inputs and outputs of user defined functions. Clicking on a function could then show the code with the expression values side by side. Having a good overview of how the values change could make it easy to find where exactly things go wrong. + +- (Machine learning) algorithms to extract and show useful information from debug values. +- Ability to mark e.g. a specific record field for tracking(filter out the noise) that is being repeatedly updated throughout the program. +- Ability to collapse/fold debug output coming from specific line. +- search bar to search through printed logs +- Turn an error listed in the console into editable section of code for easy quick fixing. +- Clickable backtrace of functions, user defined functions should be made extra visible. +- VR debugging: render massive curved screen with rectangle showing code (and expression values) for every function in call stack. +- Node and wire diagram of all modules(and functions?) used by a specific test. This will be much more digestible than a node and wire diagram of the whole project. +- Ability to generate project(folder) with code used by test and all else removed. Speedy build times and no distractions. +- After encountering an error with a stacktrace: highlight all lines in code editor that occurred in the stacktrace. +- [Nice visualization of intermediate values](https://twitter.com/ryrobes/status/1582968511713792000?s=20&t=WVj2tP5YwW6_MR5ndU_F4g) +- [Property probes](https://roc.zulipchat.com/#narrow/stream/257722-editor/topic/Property.20probes/near/305671704) + +### Testing + +- [Wallaby.js](https://wallabyjs.com/) could serve as inspiration for live gutters showing tested / untested / passing / failing code based on tests, combined with time travel debugging (inline runtime values / inline error reports / inline code coverage); could be useful for debugging as well + +### Cool regular editors + +- [Helix](https://github.com/helix-editor/helix) modal (terminal, for now) editor in rust. Good UX. +- [Kakoune](https://kakoune.org/) editor with advanced text selection and manipulation features. + +### Structured Editing + +- [Greenfoot](https://www.youtube.com/watch?v=uUVA7nTh0XY) +- [Deuce](http://ravichugh.github.io/sketch-n-sketch/) (videos on the right) by [Ravi Chugh](http://people.cs.uchicago.edu/~rchugh/) and others +- [Fructure: A Structured Editing Engine in Racket](https://youtu.be/CnbVCNIh1NA) by Andrew Blinn +- [Hazel: A Live FP Environment with Typed Holes](https://youtu.be/UkDSL0U9ndQ) by [Cyrus Omar](https://web.eecs.umich.edu/~comar/) +- [Dark Demo](https://youtu.be/QgimI2SnpTQ) by [Ellen Chisa](https://twitter.com/ellenchisa) +- [Introduction to JetBrains MPS](https://youtu.be/JoyzxjgVlQw) by [Kolja Dummann](https://www.youtube.com/channel/UCq_mWDvKdXYJJzBmXkci17w) +- [Eve](http://witheve.com/) + - code editor as prose writer + - live preview + - possible inspiration for live interactivity as well +- [Unreal Engine 4](https://www.unrealengine.com/en-US/) + - [Blueprints](https://docs.unrealengine.com/en-US/Engine/Blueprints/index.html) visual scripting (not suggesting visual scripting for Roc) + +- [Live Programming](https://www.microsoft.com/en-us/research/project/live-programming/?from=http%3A%2F%2Fresearch.microsoft.com%2Fen-us%2Fprojects%2Fliveprogramming%2Ftypography.aspx#!publications) by [Microsoft Research] it contains many interesting research papers. +- [Math Inspector](https://mathinspector.com/), [github](https://github.com/MathInspector/MathInspector) +- [Lamdu](http://www.lamdu.org/) live functional programming. +- [Sourcetrail](https://www.sourcetrail.com/) nice tree-like source explorer. +- [Unisonweb](https://www.unisonweb.org), definition based [editor](https://twitter.com/shojberg/status/1364666092598288385) as opposed to file based. +- [Utopia](https://utopia.app/) integrated design and development environment for React. Design and code update each other, in real time. +- [Paredit](https://calva.io/paredit/) structural clojure editing, navigation and selection. [Another overview](http://danmidwood.com/content/2014/11/21/animated-paredit.html) +- [tylr](https://tylr.fun/) projectional editor UX that helps you make it easier to do edits that are typically difficult with projectional editors but are easy with classic editors. + +### Project exploration + +- Tree view or circle view (like Github Next) of project where exposed values and functions can be seen on hover. + +#### Inspiration + +- [Github Next](https://next.github.com/projects/repo-visualization) each file and folder is visualised as a circle: the circle’s color is the type of file, and the circle’s size represents the size of the file. Side note: a cool addition to this might be to use heatmap colors for the circles; circles for files that have had lots of commits could be more red, files with few commits would be blue. +- [AppMap](https://appland.com/docs/appmap-overview.html) records code execution traces, collecting information about how your code works and what it does. Then it presents this information as interactive diagrams that you can search and navigate. In the diagrams, you can see exactly how functions, web services, data stores, security, I/O, and dependent services all work together when application code runs. +- [Discussion on flow based ( nodes and wires) programming](https://marianoguerra.github.io/future-of-coding-weekly/history/weekly/2022/08/W1/thinking-together.html#2022-07-25T00:47:49.408Z) if the wires are a mess, is your program a mess? + +### Voice Interaction Related + +- We should label as many things as possible and expose jumps to those labels as shortkeys. +- Update without user interaction. e.g. autosave. +- Could be efficient way to communicate with smart assistant. +- You don't have to remember complex keyboard shortcuts if you can describe actions to execute them. Examples: + - Add latest datetime package to dependencies. + - Generate unit test for this function. + - Show edit history for this function. + - Adjusting settings: switch to light theme, increase font size... +- Use (context specific) voice command state machine to assist Machine Learning voice recognition model. +- Nice special use case: using voice to code while on treadmill desk. +- Use word embeddings to find most similar voice command to recorded input in vector space. + +#### Useful voice commands + +- clear all breakpoints +- increase/decrease font size +- switch to dark/light/high-contrast mode +- open/go to file "Main"(fuzzy matching) +- go to function "foo" +- go to definition +- show all references(uses) of this function/type/... +- show history timeline of this function/file +- show recent projects +- generate unit test for this function +- generate unit test for this function based on debug trace (input and output is recorded and used in test) +- who wrote this line (git blame integration) +- search documentation of library X for Foo +- show example of how to use library function Foo +- open google/github/duckduckgo search for error... +- show editor plugins for library X +- commands to control log filtering +- collaps all arms of when +- "complex" filtered search: search for all occurrences of `"#` but ignore all like `"#,` +- color this debug print orange +- remove unused imports + +#### Inspiration + +- Voice control and eye tracking with [Talon](https://github.com/Gauteab/talon-tree-sitter-service) +- [Seminar about programming by voice](https://www.youtube.com/watch?v=G8B71MbA9u4) +- [Talon voice commands in elm](https://github.com/Gauteab/talon-tree-sitter-service) +- Mozilla DeepSpeech model runs fast, works pretty well for actions but would need additional training for code input. + Possible to reuse [Mozilla common voice](https://github.com/common-voice/common-voice) for creating more "spoken code" data. +- [Voice Attack](https://voiceattack.com/) voice recognition for apps and games. +- [OpenAI whisper](https://github.com/openai/whisper) excellent open source voice recognition model. + +### Beginner-focused Features + +- Show Roc cheat sheet on start-up. +- Plugin that translates short pieces of code from another programming language to Roc. [Relevant research](https://www.youtube.com/watch?v=xTzFJIknh7E). Someone who only knows the R language could get started with Roc with less friction if they could quickly define a list R style (`lst <- c(1,2,3)`) and get it translated to Roc. +- Being able to asses or ask the user for the amount of experience they have with Roc would be a valuable feature for recommending plugins, editor tips, recommending tutorials, automated error search (e.g searching common beginner errors first), ... . +- Adjust UI based on beginner/novice/expert? +- Click to explain type annotation + +### Productivity features + +- When refactoring; + - Cutting and pasting code to a new file should automatically add imports to the new file and delete them from the old file. + - Ability to link e.g. variable name in comments to actual variable name. Comment is automatically updated when variable name is changed. + - When updating dependencies with breaking changes; show similar diffs from github projects that have successfully updated that dependency. + - AST backed renaming, changing variable/function/type name should change it all over the codebase. +- Automatically create all "arms" when pattern matching after entering `when var is` based on the type. + - All `when ... is` should be updated if the type is changed, e.g. adding Indigo to the Color type should add an arm everywhere where `when color is` is used. +- When a function is called like `foo(false)`, the name of the boolean argument should be shown automatically; `foo(`*is_active:*`false)`. This should be done for booleans and numbers. +- Suggest automatically creating a function if the compiler says it does not exist. +- Integrated search: + - Searchbar for examples/docs. With permission search strings could be shared with the platform/package authors so they know exactly what their users are struggling with. +- Show productivity/feature tips on startup. Show link to page with all tips. Allow not seeing tips next time. +- Search friendly editor docs inside the editor. Offer to send search string to Roc maintainers when no results, or if no results were clicked. +- File history timeline view. Show timeline with commits that changed this file, the number of lines added and deleted as well as which user made the changes. Arrow navigation should allow you to quickly view different versions of the file. +- Suggested quick fixes should be directly visible and clickable. Not like in vs code where you put the caret on an error until a lightbulb appears in the margin which you have to click for the fixes to appear, after which you click to apply the fix you want :( . You should be able to apply suggestions in rapid succession. e.g. if you copy some roc code from the internet you should be able to apply 5 import suggestions quickly. +- Regex-like find and substitution based on plain english description and example (replacement). i.e. replace all `[` between double quotes with `{`. [Inspiration](https://alexmoltzau.medium.com/english-to-regex-thanks-to-gpt-3-13f03b68236e). +- Show productivity tips based on behavior. i.e. if the user is scrolling through the error bar and clicking on the next error several times, show a tip with "go to next error" shortcut. +- Command to "benchmark this function" or "benchmark this test" with flamegraph and execution time per line. +- Instead of going to definition and having to navigate back and forth between files, show an editable view inside the current file. See [this video](https://www.youtube.com/watch?v=EenznqbW5w8) +- When encountering an unexpected error in the user's program we show a button at the bottom to start an automated search on this error. The search would: + - look for similar errors in github issues of the relevant libraries + - search stackoverflow questions + - search a local history of previously encountered errors and fixes + - search through a database of our Zulip questions + - ... +- smart insert: press a shortcut and enter a plain english description of a code snippet you need. Examples: "convert string to list of chars", "sort list of records by field foo descending", "plot this list with date on x-axis"... +- After the user has refactored code to be simpler, try finding other places in the code base where the same simplification can be made. +- Show most commonly changed settings on first run so new users can quickly customize their experience. Keeping record of changed settings should be opt-in. +- Detection of multiple people within same company/team working on same code at the same time (opt-in). +- Autocorrect likely typos for stuff like `-<` when not in string. +- If multiple functions are available for import, use function were types would match in insertion position. +- Recommend imports based on imports in other files in same project. +- Machine Learning model to determine confidence in a possible auto import. Automatically add the import if confidence is very high. +- Ability to print logs in different color depending on which file they come from. +- Clicking on a log print should take you to the exact line of code that called the log function +- When detecting that the user is repeating a transformation such as replacing a string in a text manually, offer to do the replacement for all occurrences in this string/function/file/workspace. +- Auto remove unused imports? Perhaps save the removed imports on a scratchpad for easy re-enabling. +- It should be easy to toggle search and replace to apply to the whole project. +- Taking into account the eye position with eye tracking could make commands very powerful/accurate. e.g.: make `Num *` a `List (Num *)`, use eye position to determine which `Num *`. +- Feature to automatically minimize visibility(exposing values/functions/...) based on usage in tests. Suggested changes can be shown to the user for fine-grained control. +- Locally record file/function navigation behavior to offer suggestions where to navigate next. With user permission, this navigation behavior can be shared with their team so that e.g. new members get offered useful suggestions on navigating to the next relevant file. +- Intelligent search: "search this folder for ", "search all tests for " +- Show some kind of warning if path str in code does not exist locally. +- repl on panic/error: ability to inspect all values and try executing some things at the location of the error. +- show values in memory on panic/error +- automatic clustering of (text) search results in groups by similarity +- fill screen with little windows of clustered search results +- clustering of examples similar to current code +- ability to easily screenshot a subwindow -> create static duplicate of subwindow +- Show references is a common editor feature, often I only want to see non-recursive references in the case of a recursive function. +- ability to add error you were stuck on but have now solved to error database, to help others in the future. +- For quick navigation and good overview: whole file should be shown as folded tree showing only top level defs. Hovering with mouse should allow you to show and traverse the branches, with a click to keep this view. See also ginkowriter. +- clicking on any output should take you to the place in the code where that output was printed and/or calculated. +- ability to edit printed output in such a way that the appropriate changes are made in the code that produced it. Example: edit json key in output-> code is changed to print this new key. +- Idea for the free/legacy editing mode: ability to turn any node into a text buffer node as opposed to the whole file. If we can parse the updated text, it will be converted back into an AST node. +- Similar to predicting next file that will be accessed; put recently accessed folders/files (that are not in a tab) in a subwindow of tree file viewer. +- Automatically keep private local database of terminal output(errors that happened) and actions that were required to solve them. So we can show this to the user if this error pops up again. +- Similarly; errors file in special dir where people can add errors and some text. The editor will check this dir if errors pop up so that this text can be shown. That way developers know what to do when they see an error that someone else has seen before. The text could be something like: "you need to change this setting to prevent this error". +- When user is implementing something that is available in the stdlib; show a notification with the relevant stdlib function. +- Custom commands/aliases for a specific project. For example for navigation; e.g. go to "cli tests"(alias) which is defined to go to `crates/cli/tests/somefile.roc` +- Tool that changes code with duplications to use single source of truth. The reverse operation would also be nice, when changes need to be made for a single case. +- Ability to search all values of expressions of a run. Search would take you to the line of code that produced the value. +- Ability to link to other comments to prevent from having to repeat identical comments or having to update all of them when a change is necessary. +- In the file explorer, auto-close subtree after unused for certain time. +- Ability to right click error message > "create github issue". +- Record (locally) all steps performed by editor + logs and make them searchable for user. The user should be able to scrub through these steps. +- clustering of search results (rg like) based on statistics of result; amount of spaces, avg word length, amount of numbers... +- Search engine to answer questions about your code base, logs, all intermediary values. Examples: search for "xy" in a string, search for xy in a variable name, show all intermediary values of index variable... +- When a stacktrace is shown in the logs, highlight all lines of code in the editor that are in the stacktrace. +- Double-click to select all child expressions of parent: [example](https://twitter.com/disconcision/status/1587156531203678208?s=20&t=GySrwPVnMB6rDKFqRuUcBw). +- We should encourage users to ask editor something in the command window; e.g. "hide all types", "List docs", "switch light mode"... If the questions can not be matched to existing actions, the user should be given the option to share the question with editor developers so the action can be created or mapped to an existing action. + +#### Autocomplete + +- Use more space for autocomplete options: + - Multiple columns. Columns could have different sources, i.e. middle column suggests based on current folder, left column on whole project, right column on github. + - show cell with completion + auto import suggestion +- Webcam based eye tracking for quick selection. +- Machine Learning: + - GPT-3 can generate correct python functions based on a comment describing the functionality, video [here](https://www.youtube.com/watch?v=utuz7wBGjKM). It's possible that training a model using ast's may lead to better results than text based models. +- Current autocomplete lacks flow, moving through suggestions with arrows is slow. Being able to code by weaving together autocomplete suggestions laid out in rows using eye tracking, that could flow. +- It's possible that with strong static types, pure functions and a good search algorithm we can develop a more reliable autocomplete than one with machine learning. +- When ranking autocomplete suggestions, take into account how new a function is. Newly created functions are likely to be used soon. +- Ability to autocomplete file paths based on folders inside project workspace. +- Allow custom message to be shown when function is autocompleted; e.g. when using sinRadians: "sinTurns is better for high performance." + +#### Productivity Inspiration + +- [Kite](https://www.kite.com/) AI autocomplete and doc viewer. +- [Tabnine](https://www.tabnine.com/) AI autocomplete. +- [Codota](https://www.codota.com) AI autocomplete and example searching. +- [Github copilot](https://copilot.github.com/) AI autocomplete. +- [Aroma](https://ai.facebook.com/blog/aroma-ml-for-code-recommendation) showing examples similar to current code. +- [MISM](https://arxiv.org/abs/2006.05265) neural network based code similarity scoring. +- [Inquisitive code editor](https://web.archive.org/web/20221206102415/https://austinhenley.com/blog/inquisitivecodeeditor.html) Interactive bug detection with doc+test generation. +- [NextJournal](https://nextjournal.com/blog/command-bar) Discoverable commands and shortcuts. +- [Code Ribbon](https://web.archive.org/web/20230209062246/https://austinhenley.com/blog/coderibbon.html) fast navigation between files. Feature suggestion: top and down are filled with suggested files, whereas left and right are manually filled. +- [Automatic data transformation based on examples](https://youtu.be/Ej91F1fpmEw). Feature suggestion: use in combination with voice commands: e.g. "only keep time from list of datetimes". +- [Codesee](https://www.codesee.io/) code base visualization. +- [Loopy](https://dl.acm.org/doi/10.1145/3485530?sid=SCITRUS) interactive program synthesis. +- [bracket guides](https://mobile.twitter.com/elyktrix/status/1461380028609048576) +- [Frugel](https://github.com/cdfa/frugel) error-tolerant live programming +- [Barliman](https://www.youtube.com/watch?v=er_lLvkklsk) a smart editor capable of program synthesis: given part of a program and a set of tests to pass, Barliman attempts to complete the program for you. +- [UI fuzzer](https://www.fuzzmap.io/?welcome=1) fuzzes UI actions and builds a beautiful state machine like visual. +- [flowistry](https://github.com/willcrichton/flowistry) show relevant code based on current selection and fade out everything else. + +### Non-Code Related Inspiration + +- [Scrivner](https://www.literatureandlatte.com/scrivener/overview) writing app for novelists, screenwriters, and more +- Word processors (Word, Google Docs, etc) + - Comments that are parallel to the text of the document. + - Comments can act as discussions and not just statements. + - Easy tooling around adding tables and other stylised text +- Excel and Google Sheets + - Not sure, maybe something they do well that we (code editors) could learn from + +## Machine Learning Ideas + +- Ability to record all changes to abstract syntax tree with user permission. + - I think it is possible to create powerful automatic error resolution by having a dataset available of ast's with a specific error and the subsequent transformation that fixed the error. + - Users with large private code bases could (re)train a publicly available error recovery model to experience benefits without having to share their code. + - It could be useful to a user who is creating a function to show them the most similar function (type signature, name, comment) in a public+their private database. Say I was using a web framework and I just created a function that has a multipart form as argument, it would be great to have an example instantly available. + - A simpler start for this idea without user data gathering: how the user a code snippet that is most similar to what they are currently writing. Snippets can be aggregated from examples, tests, docstrings at zero cost to the package/platform authors. + - Train ML model to rank snippets by likely usefulness to user. + - See [codata](https://www.codota.com/code/java/classes/okhttp3.OkHttpClient) for inspiration on a snippet/example finder. +- Fuzzy natural language based setting adjustment in search bar or with voice input: increase font size, enable autosave, switch to light theme... +- Detect deviation of best practices, example case: alert developer when they are defining a color inline (rgb(30,30,30)) while all colors have been previously imported from a single file. See also [Codota](https://www.codota.com). +- It would be valuable to record the user's interactions with the editor when debugging as well as the AST. On enough data we could train a model to perform a bunch of debugging steps and show values of the most important variables in relation to the bug. Having assistance in finding the exact code that causes the problem could be super valuable. There could be sensitive data, so it should only be recorded and or shared for open source codebases with permissive licenses and with explicit user permission. +- To allow for more privacy; data gathering can be kept only local or only shared within a team/company. Say we offer the ability to save the changes made after an error occurred. Another developer in the company who encounters this error could be notified someone has previously encountered this error along with their changes made after the error. Optionally, the first developer's name can be shown (only within team/company) so the second developer can quickly ask for help. +- Smart assistant that attempts to "place debug prints"/"show relevant info" based on the error message. +- To get training data for machine learning model: ability to enter a text description for every code edit. +- Chatbot that can answer questions about the code base. +- Smart navigation assistant to help you navigate with fuzzy text: take me to the false-interpreter's platform. +- select some code (x='a'\ny='b') > Open transform command window > Type: concat chars > AI adds line: `concatenated = Char.concat 'a' 'b'` +- Use terminal output to predict suggestions for files to navigate to. + +## Testing + +- From Google Docs' comments, adding tests in a similar manner, where they exists in the same "document" but parallel to the code being written + - Makes sense for unit tests, keeps the test close to the source + - Doesn't necessarily make sense for integration or e2e testing + - Maybe easier to manually trigger a test related to exactly what code you're writing +- Ability to generate unit tests for a selected function in context menu + - A table should appear to enter input and expected output pairs quickly +- Ability to "record" unit tests + - Select a function to record. + - Do a normal run, and save the input and output of the selected function. + - Generate a unit test with that input-output pair +- [vitest](https://twitter.com/antfu7/status/1468233216939245579) only run tests that could possibly have changed (because the code they test/use has changed) +- Ability to show in sidebar if code is tested by a test. Clicking on the test in the sidebar should bring you to that test. + +### Inspiration + +- [Haskell language server plugin](https://github.com/haskell/haskell-language-server/blob/master/plugins/hls-eval-plugin/README.md) evaluate code in comments, to test and document functions and to quickly evaluate small expressions. +- [Hazel live test](https://mobile.twitter.com/disconcision/status/1459933500656730112) + +## Documentation + +- Ability to see module as it would be presented on a package website. + - Modern editors may guide developers to the source code too easily. + The API and documentation are meant to interface with humans. +- [DocC](https://developer.apple.com/videos/play/wwdc2021/10166/) neat documentation approach for swift. +- Make it easy to ask for/add examples and suggest improvements to a project's docs. +- Library should have cheat sheet with most used/important docs summarized. +- With explicit user permission, anonymously track viewing statistics for documentation. Can be used to show most important documentation, report pain points to library authors. +- Easy side-by-side docs for multiple versions of library. +- ability to add questions and answers to library documentation + +## Tutorials + +- Inclusion of step-by-step tutorials in Roc libraries, platforms or business specific code. +- Having to set up your own website for a tutorial can be a lot of work, making it easy to make quality tutorials would make for a more delightful experience. + +## High performance + +### Inspiration + +- [10x editor](https://www.10xeditor.com/) IDE/Editor targeted at the professional developer with an emphasis on performance and scalability. + +## Positive feedback + +- It's nice to enhance the feeling of reward after completing a task, this increases motivation. +- Great for tutorials and the first run of the editor. +- Suggestions of occasions for positive feedback: + - Being able to compile successfully after starting out with more than X errors. + - Making a test succeed after repeated failures. +- Positive feedback could be delivered with messages and/or animations. Animations could be with fireworks, flying roc logo birds, sounds... +- The intensity of the message/animation could be increased based on the duration/difficulty of the task. +- Suggest to search for help or take a break after being stuck on a test/compile errors... for some time. A search could be done for group chats for relevant libraries. + +### Inspiration + +- [Duolingo](https://www.duolingo.com) app to learn languages +- [Khan academy](https://www.khanacademy.org/) free quality education for everyone + +## Accessibility + + - Visual Impairments + No Animation is most benign form of cognitive disability but really important base line of people with tense nerve system. + Insensitivity to certain or all colors. + Need of high contrast + Or Everything Magnified for me with no glasses. + Or Total blindness where we need to trough sound to communicate to the user + Screen readers read trees of labeled elements. Each OS has different APIs, but I think they are horrible. Just close your eyes and imagine listening to screen reader all day while you are using these majestic machines called computers. + But blind people walk with a tool and they can react much better to sound/space relations than full on visual majority does. They are acute to sound as a spatial hint. And a hand for most of them is a very sensitive tool that can make sounds in space. + Imagine if every time the user doesn't want to rely on shining rendered pixels on the screen for a feedback from machine, we make a acoustic room simulation, where with moving the "stick", either with mouse or with key arrows, we bump into one of the objects and that produces certain contextually appropriate sound (clean)*ding* + + On the each level of abstraction they can make sounds more deeper, so then when you type letters you feel like you are playing with the sand (soft)*shh*. We would need help from some sound engineer about it, but imagine moving down, which can be voice triggered command for motion impaired, you hear (soft)*pup* and the name of the module, and then you have options and commands appropriate for the module, they could map to those basic 4 buttons that we trained user on, and he would shortcut all the soft talk with click of a button. Think of the satisfaction when you can skip the dialog of the game and get straight into action. (X) Open functions! Each function would make a sound and say its name, unless you press search and start searching for a specific function inside module, if you want one you select or move to next. + - Related idea: Playing sounds in rapid succession for different expressions in your program might be a high throughput alternative to stepping through your code line by line. I'd bet you quickly learn what your program should sound like. The difference in throughput would be even larger for those who need to rely on voice transcription. + + - Motor impairments + [rant]BACKS OF CODERS ARE NOT HEALTHY! We need to change that![/neverstop] + Too much mouse waving and sitting for too long is bad for humans. + Keyboard is basic accessibility tool but + Keyboard is also optional, some people have too shaky hands even for keyboard. + They rely on eye tracking to move mouse cursor around. + If we employ *some* voice recognition functions we could make same interface as we could do for consoles where 4+2 buttons and directional pad would suffice. + That is 10 phrases that need to be pulled trough as many possible translations so people don't have to pretend that they are from Maine or Texas so they get voice recognition to work. Believe me I was there with Apple's Siri :D That is why we have 10 phrases for movement and management and most basic syntax. + - Builtin fonts that can be read more easily by those with dyslexia. + - [Cross-platform UI accessibility WIP](UI accessibility infrastructure across platforms and programming languages https://github.com/AccessKit/accesskit) + - beware of eye strain for eye tracking features + +## UX testing + +- knowledge gap exercise: walk through the process of someone using your product for the first time, and imagine that you're sat next to them as they do it. +What do you feel the need to say to them? Which knowledge gaps are you filling with your voice? Most of these will feel intuitive—write them down. + +## Builtin docutorial + +- interactive code examples +- [Notebook](https://jupyter.org/try-jupyter/retro/notebooks/?path=notebooks/Intro.ipynb) like format? Could support images, svg, sound, math... . +- Should support those new to programming and experienced individuals. Probably need custom version for new programmers (need to go deep into simple things). +- Support ability to explore both broadly (many topics) and deeply (extensive detail). +- Write three versions of every "page" for "never programmed before", novice and pro? +- Should be completely navigable with the keyboard: + - Down arrow to go deeper into topic. It may start with a simple overview and examples; it then expands with further detailed information such as links to reference material, explanations, examples, and quizzes. + - Right arrow to go to next topic. +- Sort topics in order of importance to know. +- Docutorial should educate users about roc and about using the editor. +- At least some of this content should be bundled in the nightly so that it easily works offline. + +## General Thoughts/Ideas + +- Nice backtraces that highlight important information +- Ability to show import connection within project visually + - This could be done by drawing connections between files or functions in the tree view. This would make it easier for people to get their bearings in new big projects. +- Connections could also be drawn between functions that call each other in the tree view. The connections could be animated to show the execution flow of the program. +- Ability to inline statements contained in called functions into the callee function for debugging. + - The value of expressions can be shown at the end of the line like in the [Inventing on Principle talk](https://youtu.be/8QiPFmIMxFc?t=1181) + - This would give a clear overview of the execution and should make it easy to pinpoint the line where the bug originates. + - That specific line can then be right clicked to go to the actual function. + - Having to jump around between different functions and files is unnecessary and makes it difficult to see the forest through the trees. +- "Error mode" where the editor jumps you to the next error + - Similar in theory to diff tools that jump you to the next merge conflict +- dependency recommendation +- Command to change the file to put all exposed functions at the top of the file, private functions below. Other alternative; ability to show a "file explorer" that shows exposed functions first, followed by private functions. +- We could provide a more expansive explanation in errors that can benefit from it. This explanation could be folded(shown on click) by default in the editor. +- Code coverage visualization: allow to display code in different color when it is covered by test. +- Make "maximal privacy version" of editor available for download, next to regular version. This version would not be capable of sharing any usage/user data. +- Live code view with wasm editor. This saves bandwidth when pairing. +- [Gingkowriter](https://gingkowriter.com/) structured writing app. +- Performance improvement recommendation: show if code is eligible for tail call optimization or can do in place mutation. +- very small error squiggles should be made more visible diff --git a/design/editor/goals.md b/design/editor/goals.md new file mode 100644 index 0000000000..7a698f7b39 --- /dev/null +++ b/design/editor/goals.md @@ -0,0 +1,28 @@ +# Who is the editor for? + +Roc developers; beginners, experts, and everything in between. + +# What will the editor do? + +- Edit roc code. +- Debug roc programs. +- Allow everyone to make a plugin. Plugins can be standalone or come bundled with a package. +- Search and view documentation. +- Run tests with a nice UI. +- Make googling and stackoverflow redundant. You should be able to find the answer to your question with the editor. +- Accommodate those with disabilities. +- Provide traditional LSP functionality (without the actual language server); autocomplete, go to def, find references, rename, show type... + +# General goals + +- Follow UX best practices. Make the user feel comfortable, don't overload them with information or functionality. Offer a gentle learning curve. The editor should be usable without a tutorial. +- Maximal simplicity, strive for easy maintenance. +- Be respectful and transparent towards user privacy. Plugins should have permissions in accordance with this requirement. +- Editor code should be well-documented. Vital components should have live visualization to make it easy to understand and debug the editor. + +# Non-goals + +- Be a general-purpose editor for other programming languages. +- Take safe, familiar choices. +- Connect with LSP. +- Sell user data. diff --git a/design/editor/plugins/api.md b/design/editor/plugins/api.md new file mode 100644 index 0000000000..0ddb284b57 --- /dev/null +++ b/design/editor/plugins/api.md @@ -0,0 +1,3 @@ +- [Action state design document](https://docs.google.com/document/d/16qY4NGVOHu8mvInVD-ddTajZYSsFvFBvQON_hmyHGfo/edit?usp=drivesdk) +- [Roc theme changer plugin concept study](https://gist.github.com/lukewilliamboswell/1b921641e7d68457ba88d81747c1bd44) +- Plugin can register view (=UI) to editor; editor can use that view to render all occurrences of (a value of) that type. \ No newline at end of file diff --git a/design/editor/plugins/inspiration.md b/design/editor/plugins/inspiration.md new file mode 100644 index 0000000000..b6953bebe7 --- /dev/null +++ b/design/editor/plugins/inspiration.md @@ -0,0 +1,5 @@ +- [Hazel Livelits](https://hazel.org/papers/livelits-paper.pdf) interactive plugins, see GIF's [here](https://twitter.com/disconcision/status/1408155781120376833). +- [Boop](https://github.com/IvanMathy/Boop) scriptable scratchpad for developers. Contains collection of useful conversions: json formatting, url encoding, encode to base64... +- [processing](https://processing.org) Interactive editor, dragging left or right with mouse to change values. Instant results. +- [flowistry](https://github.com/willcrichton/flowistry) easily track all named values in a certain expression throughout your program. +- Match blocks of assembly with lines of code: [link](https://twitter.com/dgryski/status/1547952259828330498). \ No newline at end of file diff --git a/design/editor/plugins/plugin_use_cases.md b/design/editor/plugins/plugin_use_cases.md new file mode 100644 index 0000000000..3a3efac22b --- /dev/null +++ b/design/editor/plugins/plugin_use_cases.md @@ -0,0 +1,19 @@ +# Bundled with package (=library) + +- Regex description to actual regex. +- Color picker. +- Visualising parser execution: [example](https://github.com/fasterthanlime/pegviz). + +# Standalone + +- Translating short pieces of code from another programming language to Roc. [Relevant research](https://www.youtube.com/watch?v=xTzFJIknh7E). Someone who only knows the R language could get started with Roc with less friction if they could quickly define a list R style (`lst <- c(1,2,3)`) and get it translated to Roc. +- Translate linux commands like curl to Roc code. +- Diff viewer +- Plugin to present codebase to new developer or walk co-worker through a problem. Records sequence of filenames and line numbers. +- Logbook; I've found that writing down steps and thoughts when you're implementing or debugging something can be really useful for later. If we make an integrated terminal, we can automatically add executed commands to this logbook. This plugin could have a publish button so you can produce useful "blogs" for others with minimal effort. +- Upload selected code/file to gist. +- Friendly/smart text filtering; be able to filter values by selecting a part. For example; logs contain `roc_expect_buffer_109680`, we select `109680`, numbers at identical positions are extracted. +- Diagram rendering from text, similar to [mermaid](https://mermaid-js.github.io/mermaid/#/). +- Pomodoro timer to remind you to take breaks regularly. + + diff --git a/design/editor/plugins/security.md b/design/editor/plugins/security.md new file mode 100644 index 0000000000..539ad6ac21 --- /dev/null +++ b/design/editor/plugins/security.md @@ -0,0 +1,3 @@ +- Remove permissions if plugin has not been used for a long time. +- Log plugin actions that require a permission. +- Show plugin that is currently using a permission, e.g. roc-core is reading from folder /gitrepos/hello in status bar and with a scrollable log. \ No newline at end of file diff --git a/design/editor/plugins/ui_research.md b/design/editor/plugins/ui_research.md new file mode 100644 index 0000000000..4839f25892 --- /dev/null +++ b/design/editor/plugins/ui_research.md @@ -0,0 +1,33 @@ +# Inspiration + +## Plugin window organization + +- Notebooks: + + Jupyter Notebook + - ![notebook image](https://github.com/quantopian/pyfolio/blob/master/pyfolio/examples/sector_mappings_example.ipynb) + - The notebook’s scrollable nature makes embedding plugins in the UI easy and clean. + + Elixir Livebook + - [example video](https://youtu.be/lyiqw3O8d_A?t=246) + + Note: notebooks fit very well if you are trying to tell a story; for a tutorial, analyzing data, interactive textbook, prototyping… . I have not seen them used for large systems or codebases. + +- Glamorous toolkit: + + ![Gtoolkit overview](https://feenk.com/assets/pictures/gtr-overview.png) + + multi-language notebook, code editor, software analysis platform, visualization engine, knowledge management system +- Design software: + + blender + - ![blender screenshot](https://cdn.80.lv/api/upload/content/a0/60f703b067544.jpg) + + photoshop + - ![photoshop screenshot](https://www.startpage.com/av/proxy-image?piurl=https%3A%2F%2Fi.ytimg.com%2Fvi%2FiMo20Gqj-sM%2Fmaxresdefault.jpg&sp=1668869642T68632e7db288b83ef9c5bf202804c88f48718ba3fd052003e161e78c4e1adb79) + + gimp + - ![gimp screenshot](https://www.lifewire.com/thmb/R-Gy2C1RdRH_KbiaS04rPvehA7I=/1366x0/filters:no_upscale():max_bytes(150000):strip_icc():format(webp)/gimp-photo-open-6d392895064245ec8e3b1ecd09f0651a.jpg) +- Music production software: + + Ableton Live + - ![ableton screenshot](https://i.pcmag.com/imagery/reviews/026WYE4XN99dDtWflTxyi5l-33.fit_lim.size_1152x.jpg) +- Hazel livelit: + + Plugins are rendered inline. + + [link](https://hazel.org/build/livelits/), test with `let foo$color` + + +# Prototyping + +- [Visually organizing plugin windows](https://docs.google.com/presentation/d/1Oc8w5lZw2dg2L5evt42d3FsmT2deYo4JNoqxNsh4q9M/edit?usp=drivesdk) diff --git a/design/editor/requirements.md b/design/editor/requirements.md new file mode 100644 index 0000000000..2d58c79fe8 --- /dev/null +++ b/design/editor/requirements.md @@ -0,0 +1,45 @@ +# The Editor Must +- Edit roc code +- Run plugins asynchronously. Plugins can be standalone or come with roc packages. + +# The Editor Should +- Support beginner-friendly projectional editing. +- Support typed holes. +- Support temporarily switching to free/legacy editing for operations that are difficult in projectional mode. +- Allow everyone to write, publish and test plugins. +- Disregard optional whitespace; there should be only one way to format roc. +- Provide useful debug UI and relevant data about the state of values. +- Allow running tests and viewing test results. +- Search and view documentation. +- Apply and render basic editing operations in 8.3 ms. +- Be the most popular roc editor. +- Be bundled with the roc CLI/compiler. +- Help find answers to all questions you would normally google. +- Accommodate those with disabilities better than other popular editors. +- Provide key familiar features: autocomplete, go to def, find references, rename, show type... +- Be able to execute core functionality with keyboard, with the exception of plugin functionality. +- Allow for output and errors that are nicer to read and search through compared to terminal output. +- Have an integrated REPL. +- Allow measuring performance of running roc apps. +- Show which code is covered by tests. +- Support several chart types, ability to transform data coming from logs, output, and intermediary values (from roc program execution). +- Support easily generating tests for a function. +- Indicate progress for slower operations. +- Provide assistance when upgrading roc/libs to new versions. +- Make it easy for the user to provide feedback/report problems. +- Provide tutorials for people with different amounts of programming experience. +- Provide useful cheat sheets. +- Accommodate those new to functional programming. +- Allow viewing editor, library, platform… release notes. +- Support roc notebooks similar to Jupyter. +- Support publishing and searching notebooks. +- Encourage making your code transparent and having a meaningful visual overview. + +# The Editor Might +- Warn the user for code patterns that can be written to run significantly faster. +- Support publishing of roc libraries from the editor. +- Support conversion for snippets of code from different languages to roc. +- Support running in the browser. +- Support code execution on a remote machine while being able to view UI locally. +- Allow detailed logging so you can see everything you were doing(including plugin actions) when you were for example editing a specific file 3 months ago. +- Support entering a parseable formatted piece of roc code character by character so that the rendered result in the editor looks exactly like the input. This could make working with a projectional editor more intuitive. diff --git a/design/language/Abilities.md b/design/language/Abilities.md new file mode 100644 index 0000000000..7526f1eb86 --- /dev/null +++ b/design/language/Abilities.md @@ -0,0 +1,548 @@ +# Proposal: Abilities in Roc + +Status: we invite you to try out abilities for beta use, and are working on resolving known limitations (see issue [#2463](https://github.com/roc-lang/roc/issues/2463)). + +This design idea addresses a variety of problems in Roc at once. It also unlocks some very exciting benefits that I didn't expect at the outset! It's a significant addition to the language, but it also means two other language features can be removed, and numbers can get a lot simpler. + + +Thankfully it's a non-breaking change for most Roc code, and in the few places where it actually is a breaking change, the fix should consist only of shifting a handful of characters around. Still, it feels like a big change because of all the implications it brings. Here we go! + +## Background +Elm has a few specially constrained type variables: `number`, `comparable`, `appendable`, and the lesser-known `compappend`. Roc does not have these; it has no `appendable` or `compappend` equivalent, and instead of `number` and `comparable` it has: + + +- `Num *` as the type of number literals, with type aliases like `I64 : Num (Integer Signed64)` +- The functionless constraint for type variables; for example, the type of `Bool.isEq` is `'var, 'var -> Bool` - and the apostrophe at the beginning of `'var` means that it must represent a type that has no functions anywhere in it. + + +There are a few known problems with this design, as well as some missed opportunities. + +### Problem 1: Nonsense numbers type-check +Right now in Roc, the following type-checks: + +```coffee +x : Num [ Whatever Str, Blah (List {} -> Bool) ] +x = 5 +``` + +This type-checks because the number literal 5 has the type `Num *`, which unifies with any `Num` - even if the type variable is complete nonsense. + + +It's not clear what should happen here after type-checking. What machine instructions should Roc generate for this nonsense number type? Suppose I later wrote (`if x + 1 > 0 then … `) - what hardware addition instruction should be generated there? + + +Arguably the compiler should throw an error - but when? We could do it at compile time, during code generation, but that goes against the design goal of "you can always run your Roc program, even if there are compile-time errors, and it will get as far as it can." + + +So then do we generate a runtime exception as soon as you encounter this code? Now Roc's type system is arguably unsound, because this is a runtime type error which the type checker approved. + + +Do we add an extra special type constraint just for Num to detect this during the type-checking phase? Now Num is special-cased in a way that no other type is… + + +None of these potential solutions have ever felt great to me. + +### Problem 2: Custom number types can't use arithmetic operators +Roc's ordinary numbers should be enough for most use cases, but there are nice packages like [elm-units](https://package.elm-lang.org/packages/ianmackenzie/elm-units/latest/) which can prevent [really expensive errors](https://spacemath.gsfc.nasa.gov/weekly/6Page53.pdf) by raising compile-time errors for mismatched units...at the cost of having to sacrifice normal arithmetic operators. You can't use `+` on your unit-ful numbers, because `+` in Roc desugars to `Num.add`, not (for example) `Quantity.add`. + + +Also, if 128-bit integers aren't big enough, because the numbers you're working with are outside the undecillion range (perhaps recording the distance between the Earth and the edge of the universe in individual atoms or something?) maybe you want to make an arbitrary-sized integer package. Again, you can do that, but you can't use `+` with it. Same with vector packages, matrix packages, etc. + + +This might not sound like a big problem (e.g. people deal with it in Java land), but in domains where you want to use custom numeric types, not having this is (so I've heard) a significant incentive to use plain numbers instead of more helpful data types. + +### Problem 3: Decoders are still hard to learn +Roc is currently no different from Elm in this regard. I only recently realized that the design I'm about to describe can also address this problem, but I was very excited to discover that! + +### Problem 4: Custom collection equality +Let's suppose I'm creating a custom data structure: a dictionary, possibly backed by a hash map or a tree. We'll ignore the internal structure of the storage field for now, but the basic technique we'd use would be a private tag wrapper to make an opaque type: + +```coffee +Dict k v : [ @Dict { storage : … } ] +``` + +Today in Roc I can make a very nice API for this dictionary, but one thing I can't do is get `==` to do the right thing if my internal storage representation is sensitive to insertion order. + + +For example, suppose this `Dict` has an internal storage of a binary tree, which means it's possible to get two different internal storage representations depending on the order in which someone makes the same `Dict.insert` calls. Insertion order shouldn't affect equality - what matters is if the two dictionaries contain the same elements! - but by default it does, because `==` only knows how to check if the internal structures match exactly. + + +This feels like a significantly bigger problem in Roc than it is in Elm, because: + + +- It's more likely that people will have applications where custom data structures are valuable, e.g. to efficiently store and retrieve millions of values in memory on a server. (This wouldn't likely happen in a browser-based UI.) Discord [ran into a use case like this](https://discord.com/blog/using-rust-to-scale-elixir-for-11-million-concurrent-users) in Elixir, and ended up turning to Rust FFI to get the performance they needed; I'm optimistic that we can get acceptable performance for use cases like this out of pure Roc data structure implementations, and pure Roc data structures would be much more ergonomic than interop - since having to use Task for every operation would be a significant downside for a data structure. +- I want to make testing low-friction, especially within the editor, and some of the ideas I have for how to do that rely on `==` being automatically used behind the scenes to compare values against known good values. If someone wrote tests that relied on `==` and then wanted to swap out a data structure for a custom one (e.g. because they ran into the scaling issues Discord did), it would be extra bad if the new data structure stopped working with all the existing tests and they all had to be rewritten to no longer use these convenient testing features and instead use a custom `Dict.contentsEq` or something instead. + + +This is one of the most serious problems on this list. Not for the short term, but for the long term. + +### Problem 5: How to specify functionlessness in documentation +In Roc's current design, certain types have a functionless constraint. For example, in `Bool.isEq : 'val, 'val -> Bool`, the type variable `'val` means "a type that contains no functions, which we are naming val here." + + +In this design, it's necessarily a breaking change when a type goes from functionless to function-ful, because that type can no longer be used with the `==` operator (among other things). + + +How do we report on that breaking change? What's the type diff? Just write the sentence "The type Foo was functionless before, but now it isn't" and call it a day? There are solutions to this, but I haven't encountered any I'm particularly fond of. + + +There's also a related problem with how to display it in documentation. If I have an opaque type that is functionless (as they usually will be), how should the docs display that? A little icon or something? It's more noteworthy when a type is function-ful, so should that be displayed as an icon instead even though there's only syntax in the language for function-less? + + +This is definitely solvable, but once again I can't name any solutions I love. + +### Problem 6: No nice way to specify editor-specific code +One of the goals for Roc is to have packages ship with editor integrations. + + +For example, let's say I'm making a custom data structure like the `Dict` from earlier. I want to be able to render an interactive "expando" style `Dict` in the editor, so when someone is in the editor looking at a trace of the values running through their program, they can expand the dictionary to look at just its key-value pairs instead of having to wade through its potentially gnarly internal storage representation. It's a similar problem to equality: as the author of `Dict`, I want to customize that! + + +The question is how I should specify the rendering function for `Dict`. There isn't an obvious answer in current Roc. Would I write a view function in `Dict.roc`, and the editor just looks for a function by that name? If so, would I expose it directly from that module? If so, then does that mean the API docs for Dict will include a view function that's only there for the editor's benefit? Should there be some special language keyword to annotate it as "editor-only" so it doesn't clutter up the rest of the API docs? + + +As with the `Num *` problem, there are various ways to solve this using the current language primitives, but I haven't found any that seem really nice. + +### Problem 7: Record-of-function passing +This is a minor problem, but worth noting briefly. + + +In Roc's development backend, we do mostly the same thing when generating X86-64 instructions and ARM instructions. However, there are also several points in the process where slightly different things need to happen depending on what architecture we're targeting. + + +In Rust, we can use traits to specialize these function calls in a way where Rust's compiler will monomorphize specialized versions of one generic function for each architecture, such that each specialized function does direct calls to the appropriate architecture-specific functions at the appropriate moments. It's exactly as efficient as if those specialized functions had each been written by hand, except they all get to share code. + + +In Roc, you can achieve this same level of reuse by passing around a record of functions, and calling them at the appropriate moments. While this works, it has strictly more overhead potential than the trait-based approach we're using in Rust. Maybe after a bunch of LLVM inlining and optimization passes, it will end up being equivalent, but presumably there will be cases where it does not. + + +Is the amount of overhead we're talking about here a big deal? Maybe, maybe not, depending on the use case. This is definitely a niche situation, but nevertheless a missed opportunity for some amount of speed compared to what other languages can do. + +## Proposal: Abilities +This proposal is about a new language feature called "abilities," which addresses all of these problems in a nice way, while also making some other things possible in the language. + + +Abilities are similar to traits in Rust. Here's how the type of addition would change from today's world to the Abilities world: + + +**Today:** +```coffee +Num.add : Num a, Num a -> Num a +``` + +**Abilities:** +```coffee +Num.add : number, number -> number + where number has Num +``` + +The new language keywords are emphasized in bold. + + +That where `number` has `Num` part is saying that whatever type gets used in place of the number type variable needs to have the Num ability. All the current number types (`I64`, `Nat`, etc.) would have the `Num` ability, the integer types would have the `Int` ability, and the fractional types would have the `Frac` ability. + + +All of those numeric abilities would be builtins, but you could also define your own custom abilities. Like Rust traits today (that is, Rust 1.56), abilities would not be higher-kinded. The explicit plan would be that they would never be higher-kinded, so it would never be possible to make a `Functor` or `Monad` ability. + +### Number Literals +Abilities can require other abilities. For example, to have the `Int` ability, you also need to have the `Num` ability. This means that **has `Int`** is strictly more constraining than **has `Num`**, which in turn means that we can change the type of number literals to be "an unbound variable that has the `Num` ability," similarly to what Haskell does. + + +Here's how that would look in the REPL: + + +**Today:** + +```coffee +» 5 +5 : Num * +``` + +**Abilities:** +```coffee +» 5 +5 : number + where number has Num +``` + +I'm not sure which version is more beginner-friendly, to be honest. + + +The latter is more verbose, but it's much easier to guess roughly what it means. The `*` in `Num *` isn't really self-descriptive, so a beginner playing around in the repl who hasn't learned about type variables yet (let alone wildcard type variables) seems less likely to have any useful intuition about what `Num *` is saying compared to what `where number has Num` is saying. + + +This change to number literals would solve [Problem #1](#problem-1-nonsense-numbers-type-check) (nonsense numbers type-check) completely. The following would no longer type-check: + +```coffee +x : Num [ Whatever Str, Blah (List {} -> Bool) ] +x = 5 +``` + +You could write any of these instead: + +```coffee +x : number + where number has Num +x = 5 +``` + +```coffee +x : integer + where integer has Int +x = 5 +``` + +```coffee +x : fraction where fraction has Frac # single-line ok! +x = 5 +``` + +```coffee +x : Nat +x = 5 +``` + +...but there's no opportunity to inject any nonsense that the type checker would accept. + + +Since you can add abilities to your own custom types (as we'll see later), this means you can add `Num` to your own custom number types (as well as `Int` or `Frac`) and then use them with all the usual arithmetic operators. This solves [Problem #2](#problem-2-custom-number-types-cant-use-arithmetic-operators). + +### Functionless Constraints +Here's how the type of `Bool.isEq` would change from the current world (using the functionless constraint with the ' syntax) to an Abilities world: + + +**Today:** +```coffee +Bool.isEq : 'val, 'val -> Bool +``` + +**Abilities:** +```coffee +Bool.isEq : val, val -> Bool + where val has Eq +``` + +Similarly, a hash map collection could have: + +```coffee +Dict.insert : k, v, Dict k v -> Dict k v + where k has Hash +``` + +If Hash doesn't require `Eq` for some reason (although it probably should), then `Dict.insert` could require multiple abilities as part of the annotation, e.g. `where k has Hash, Eq` + + +In the Abilities world, Roc no longer needs the concept of the *functionless* constraint, and it can be removed from the language. Abilities can cover all those use cases. + +### Default Abilities +One of the many things I like about Elm is that I can make anonymous records and tuples have them Just Work with the `==` operator. In contrast, in Rust I have to name the struct and then add `#[deriving(Eq)]` to it if I want `==` to work on it. + + +However, in Rust, tuples work basically like how they do in Elm: equality Just Works as long as all the elements in the tuple have `Eq`. In fact, Rust tuples automatically derive a bunch of traits. We can do something similar in Roc. + + +Specifically, the idea would be to have all records and tags automatically have the following abilities by default, wherever possible. (For example, types that contain functions wouldn't get these abilities, because these operations are unsupported for functions!) + + +1. Eq +2. Hash +3. Sort +4. Encode +5. Decode + + +Eq and Hash work like they do in Rust, although as previously noted, I think Hash should probably require Eq. Sort is like Ord in Rust, although I prefer the name Sort because I think it should only be for sorting and not general ordering (e.g. I think the <, >, <=, and >= operators should continue to only accept numbers, not other sortable types like strings and booleans). + + +As for Encode and Decode...to put it mildly, they are exciting. + +### Encode and Decode +[serde](https://docs.serde.rs/serde/) is among the most widely used Rust crates in the world - maybe the absolute most. It's for **ser**ializing and **de**serializing; hence, **serde**. + + +The way it works is that it provides `Serializable` and `Deserializable` traits that you can derive for your types (e.g. for your User type), as well as `Serializer` and `Deserializer` traits that anyone can define for their encoding formats (e.g. a JSON serializer). + + +[Putting these together](https://github.com/serde-rs/json#parsing-json-as-strongly-typed-data-structures), I can add `#[deriving(Serialiable, Deserializable)]` to my struct User definition, and then run something like `let user: User = serde_json::from_str(json)?` to turn my JSON into a User while handling failed decoding along the way via a `Result`. + + +Having spent a lot of time teaching JSON decoders to beginning Elm programmers, I can confidently say this seems massively easier for beginners to learn - even if it means they will abruptly have a lot to learn on the day where they want to do some more advanced decoding. It's also much more concise. + + +In the Abilities world, we can take this a step further than Rust does. We can have `Encode` and `Decode` as builtin abilities (and then also `Encoder` and `Decoder`, except they work like Serializers and Deserializers do in serde; you have an Encoder or Decoder for a particular encoding - e.g. JSON or XML - rather than for the value you want to encode or decode), and we can have the compiler automatically define them when possible, just like it does for `Eq` and the others. + + +This would mean that in Roc you could do, without any setup other than importing a package to get a generic **Json.decoder**, the following: + +```coffee +result : Result User [ JsonDecodingErr ]* +result = Decode.decode Json.decoder jsonStr +``` + +So it would be like serde in Rust, except that - like with Elm records - you wouldn't even need to mark your User as deriving Encode and Decode; those abilities would already be there by default, just like `Eq`, `Hash`, and `Sort`. + + +This would solve [Problem #3](#problem-3-decoders-are-still-hard-to-learn), eliminating the need for a beginner curriculum to include the one technique I've seen beginning Elm programmers struggle the most to learn. That's a very big deal to me! I don't know whether decoding serialized data will be as common in Roc as it is in Elm, but I certainly expect it to come up often. + + +Other nice things about this design: + + +- Since Encode and Decode are builtins, no packages need to depend on anything to make use of them. In Rust, it's currently a bit awkward that all packages that want to offer serializability have to depend on serde; it has become a nearly ubiquitous dependency in the Cargo ecosystem. By making it a builtin, Roc can avoid that problem. +- Since Encode and Decode are agnostic to the actual encoding format, anyone can write a new Encoder and Decoder for whatever their new format is (e.g. XSON, the format that looks at XML and JSON and says "why not both?" - which I just made up) and have every serializable Roc type across the entire ecosystem instantly able to be serialized to/from that format. +- This design still allows for evolving a default decoder into a bespoke decoder that can cover the same use cases that elm/json does (and a potentially very similar API). + + +I haven't looked into the details of what the exact design of this system would be, but at a glance it seems like based on the design of abilities and the design of serde, it should work out. (There may always be unexpected issues though!) + +## Adding Abilities to a Type +So we've talked about default abilities, and how various builtins would use them. What about custom types? How would I make an actual `Dict` type with its own definition of equality? + + +To do that, we need to talk about a change to the language that was originally motivated by abilities, but which ultimately seems like a good change even if abilities weren't a thing. + +### Newtypes +Let's suppose Roc no longer has private tags, but does have this syntax: + +```coffee +UserId := U64 +``` + +This declares a new concrete type in scope called `UserId`, which at runtime is a `U64` with no additional overhead. + + +To create one of these `UserId` values, we put a @ before the type and call it: + +```coffee +userId : UserId +userId = @UserId 0 +``` + +The expression `@UserId` has the type `U64 -> UserId`, which the compiler knows because this declaration is in scope: + +```coffee +UserId := U64 +``` + +Trying to use `@UserId` when a `UserId :=` declaration isn't in scope would give a compiler error. + + +`@UserId` can also be used in a pattern, to destructure the wrapped `U64`: + +```coffee +getU64 : UserId -> U64 +getU64 = \@UserId u64 -> u64 +``` + +In this way, `@UserId` can be used almost identically to how private tags work today: call (`@UserId someU64`) to create a wrapped `U64`, and pattern match on `\@UserId someU64 ->` to destructure it. The only difference is that the resulting type is `UserId` instead of `[ @UserId ]`. + + +Because the `@` prefix syntax can only refer to a newtype declaration that's currently in scope, the newtype's implementation is hidden from other modules by default. (Of course you can still expose the type and functions to work on it.) + + +This design has a few advantages over private tags: +1. It's more focused. Wrapper types with hidden implementations are really the exact use case that private tags were designed for; the concept of a union of multiple private tags was never really necessary, and in this world it doesn't even exist. +2. It means there's just one "tags" concept, just like there's one "records" concept. No more "global tags and private tags" split. +3. The `UserId := U64` declaration is more concise than the private tag equivalent of `UserId : [ @UserId U64 ]`, and it speeds up type checking because there would be (many) fewer type aliases for the compiler to resolve. +4. It enables traditional phantom types, which Roc currently lacks - e.g. +`Quantity count units := count` +in Roc would make units a phantom type like in this Elm declaration: +`type Quantity count units = Quantity count` + + +Even considered completely separately from Abilities, this "newtypes" design seems like a better design than private tags. + +### Newtypes and Abilities +Another advantage the newtypes design has over private tags is that it offers a natural place to declare what abilities a type has. + + +With private tags, this isn't really possible because I can use @Foo in multiple different places in the same module, with multiple different payload arities and types - and even if I use a type alias to give it a shorter name, that type alias is still just an alias; it can't alter the characteristics of the type it's referring to. With the newtypes design, I can refer to a specific concrete type, and not just an alias of it - meaning I actually can alter its characteristics. + + +As an example, let's make a newtype declaration for Dict, and once again ignore the internal structure of the storage field for now: + +```coffee +Dict k v := { storage : … } +``` + +This lets us construct a Dict by calling `@Dict { storage }` and destructure it similarly. + + +As discussed earlier, one problem with creating custom data structures like this in today's Roc is that `==` doesn't necessarily do the right thing. Here's a way to solve this issue: + +```coffee +Dict k v := { storage : … } has + [ Eq { isEq, isNotEq } ] +``` + +This says (among other things) that the `Dict` type has the `Eq` ability. For a type to have `Eq`, it must provide two functions: `isEq` and `isNotEq`. Here's how those look: + +```coffee +isEq : Dict k v, Dict k v -> Bool +isNotEq : Dict k v, Dict k v -> Bool +``` + +In this `Eq { isEq, isNotEq }` declaration, I'm saying that `isEq` and `isNotEq` are functions already in scope. I could also choose different names using record literal syntax, e.g. `Eq { isEq: dictIsEq, isNotEq: dictIsNotEq }` - the relevant part is that I'm specifying the names of the functions (which must also be in scope) which specify how `Eq` for Dict should work. + + +Now that I've specified this, when I use `==` on two `Dict` values, this `isEq` function will get run instead of the default `==` implementation. This solves [Problem #3](#problem-3-decoders-are-still-hard-to-learn)! +I can also write something like has `Num` and provide the relevant functions to obtain a unit-ful number type - which solves [Problem #2](#problem-2-custom-number-types-cant-use-arithmetic-operators). + +### Default Abilities for Newtypes +By default, if I don't use the has keyword when defining a newtype, Roc will give the type all the default builtin abilities it's eligible to have - so for example, it would get `Eq` and `Hash` by default unless it contains a function, in which case it's not eligible. + + +In this example, because I wrote has, the `Dict` type has `Eq` as well as the other default ones. I could instead use has `only`, which means `Dict` should not have any of the default abilities, and should instead have only the ones I list. + +```coffee +Dict k v := { storage : … } has + [ + Eq { isEq, isNotEq }, + Hash { hash }, + Sort { compare }, + Foo { bar: baz } + ] +``` + +Using `has` means if new default abilities are later added to the language, `Dict` will get them automatically. This may or may not be desirable, depending on what the ability is; maybe, like equality, it will be wrong by default for `Dict`, and maybe I'll wish I had chosen has `only`. + + +On the other hand, if everyone uses has `only` everywhere as a precaution, and a new default ability gets added to the language, a staggering amount of collective hours would be spent going around adding it to all the has `only` declarations for `UserId` and such. So a good guideline might be for custom collections like `Dict` to recommend using has `only`, and for thin wrappers like `UserId` to use `has custom`. + + +Of note, this syntax neatly solves [Problem #5](#problem-5-how-to-specify-functionlessness-in-documentation) - where functionlessness is awkward to talk about in API type diffs and documentation. This is a straightforward way to render the `Dict` type in documentation: + +```coffee +Dict k v has only + [ Eq, Hash, Sort ] +``` + +I can immediately see exactly what abilities this type has. The same is true if I used has `custom` or omitted the has clause entirely. API diffs can use this same representation, with a diff like +Eq -Sort to show which abilities were added or removed. + +### Encode and Hash +I'm not sure if we actually need separate Hash and Encode abilities. At a high level, hashing is essentially encoding a value as an integer. Since all default types will get Encode anyway, maybe all we need is to have "hashers" be implemented as Encoders. This would mean there's one less default ability in the mix, which would be a nice simplification. + + +However, I'm not sure what the differences are between Rust's Hash trait and Hasher type, and serde's Serializable trait and Serializer types. Maybe there's a relevant difference that would justify having a separate Hash ability. I'm not sure! I figure it's at least worth exploring. + + +It might look surprising at first for a `Dict` implemented as a hash map to require that its keys have `Encode`, but I don't think that's a significant downside. + +### Encode and toStr +Similarly, anyone could write a `toStr` function that works on any type that has `Encode`, by using an Encoder which encodes strings. + + +In Elm, having a general toString function proved error-prone (because it was so flexible it masked type mismatches - at work I saw this cause a production bug!) which was why it was replaced by String.fromInt and String.fromFloat. I had originally planned to do the same in Roc, but Encode would mean that anyone can write a flexible toStr and publish it as a package without acknowledging the potential for masking bugs. + + +Knowing that there's a 100% chance that would happen eventually, it seems like it would be better to just publish an Encode.str which encodes values as strings, and which can be used like toStr except you have to actually call (`Encode.encode Encode.str value`) instead of `toStr`. This would mean that although it's an option, it's (by design!) less ergonomic than a flexible function like Num.fromStr, which means the path of least resistance (and least error-proneness) is to use `Num.fromStr` instead of this. + + +One benefit to having something like `Encode.str` available in the language is that it can be nice for logging - e.g. when sending tracing information to a server that only programmers will ever see, not users. That's the only situation where I've ever personally missed the old Elm `toString`. + +## Defining New Abilities +Here's how I might have defined Eq if it weren't already a builtin ability: + +```coffee +Eq has { isEq, isNotEq } + + +isEq : val, val -> Bool where val has Eq + + +isNotEq : val, val -> Bool where val has Eq +``` + +There are two steps here: +1. Define what functions Eq has +2. Declare those functions as top-level type annotations with no bodies + + +Having done both, now if anyone wants to say that another type **has Eq**, that type needs to implement these two functions. I can also expose these functions from this module directly - so for example, if I'm in the Bool module, I can have it do `exposes [ isEq, isNotEq ]`, and now anyone can call `Bool.isEq` and it will run this function (or rather, the implementation of this function on whatever type that **has Eq** which was passed to `Bool.isEq`!) + + +Within these `isEq` and `isNotEq` functions' types, **has Eq** is allowed even though those functions are part of the definition of what `Eq` means. The compiler detects these and treats them essentially like "Self" in Rust - that is, when I say that my `Dict k v` newtype **has Eq**, its `isEq` implementation will have the type` Dict k v, Dict k v -> Bool` because the compiler will have replaced the val in the `Eq` definition with `Dict k v`. + + +The compiler knew to do that substitution with **val** because of **val has Eq** in the declaration of `isEq` itself. If `isEq` also had other abilities in its has clause, e.g. **val has Eq, foo has Sort**, it wouldn't do the substitutions with foo because **Sort** is not the name of the ability currently being defined. + + +For this reason, if you are defining a function on **Eq** (such as **isEq**), and you have more than one type variable which **has Eq**, the result is a compiler error. This would be like trying to have more than one `Self` in Rust! + +### Abilities that depend on other abilities +I mentioned earlier that in order to have either Int or Frac, a type must also have the Num ability. You can add those constraints after the **has** keyword, like so: + +```coffee +Int has Num, { …functions go here as normal… } +``` + +Now whenever someone wants to make a newtype which **has Int**, that newtype must also explicitly specify that it **has Num** - otherwise, they'll get a compiler error. Similarly, any function which requires that an argument **has Num** will also accept any type that **has Int**. + +### Defining abilities for existing types after the fact +It's conceivable that defining a new ability could support adding that ability to existing types. For example, maybe I make a new ability called Foo, and I want all numbers to have Foo. + + +It's too late for me to go back and get Num's newtype declaration to specify has Foo, because Num existed before Foo did! + + +It's possible that Roc could support a way to do this when defining a new ability. It could say for example `Eq has {...} with [ Num { isEq: numIsEq, … } ]` + + +However, upon reflection, I think this idea is fatally flawed and we shouldn't do it. + + +On the positive side, this wouldn't introduce any ambiguity. Because Roc doesn't allow cyclic imports, it's already impossible to define two conflicting definitions for a given ability function (e.g. if I define isEq for numbers when defining Num, then Num must import the module where Eq is defined, meaning I can't possibly have Eq's definition mention Num - or else the module where Eq is defined would have had to import Num as well, creating an import cycle!) so that can't happen. + + +This also wouldn't necessarily introduce any "you need to import a trait for this to work" compiler errors like we see in Rust. + + +If I'm passing a newtype named Blah to a function which expects that it **has Baz**, then by virtue of the fact that I have a Blah at all, I must have the module where it's defined already loaded in the current build (maybe not as a direct dependency of my module, but definitely as an indirect dependency). Similarly, because I'm calling a function that **has Baz**, I must also (at least indirectly) have the module where Baz is defined loaded. If both modules are loaded, I will definitely be able to find the function implementation(s) I need in either the one or the other, and because Roc wouldn't support orphan instances, I don't need to check any other modules. + + +However, this can cause some serious problems. Once I've done this, now the module where the type is defined can never import the module where the ability is defined. What if the author of that module wants to define that a different type defined in that module has this ability? Tough luck; can't import the ability module, because that would create an import cycle. Gotta move that type out of that module, even if that would create other problems. + + +This is even worse if the type and the ability are in different packages; now your entire package can't even depend on the package where the ability is defined! What if the reason the author of the ability added it to that other type was just to avoid having to coordinate with the author of the other package (or just to save them some time)? Now they've locked that author out from controlling their own type! + + +From this point, even if both authors coordinate, the only way to permit the author of the type to take back control over the implementation of that ability on that type is if the ability author releases a breaking change of that package which drops the ability from the type - so that the author of the type can finally import it without causing a cyclic dependency. I want to incentivize strong backwards compatibility commitments for package authors once their APIs have settled, and this feature would make such commitments unworkable. + + +All of this makes me think that "if you want a type to have the ability you're defining, you should coordinate with that author" is the best policy to encourage, and in that world, the feature makes no sense except perhaps in the very specific case of builtin types (which necessarily can't depend on packages). Since there are a (small) finite number of those, it seems plausible that the ability author can do one-off special-case workarounds for those instead of needing a separate language feature. + +### Abilities for Editor-Specific Code +I don't know exactly what the API for editor plugins should be yet, but they do have some characteristics that are important: + * Making or modifying editor plugins should be so easy, basically everyone does it. This means that code for editor plugins should be written in normal Roc, and the API should have a shallow learning curve. + * Editor plugins should ship with packages (or even just modules within a local project), but should have no impact on runtime performance of those modules/packages. So it's located there, but can't affect the surrounding code. + * There's more than one way to integrate with the editor. For example: + * You can add entries to context menus for certain types + * You can override the default way a type would be rendered in the editor (e.g. an expando for a custom collection) + * You can make big, interactive integrations like a [regex explorer](https://www.youtube.com/watch?v=ZnYa99QoznE&t=6105s) + + +Abilities offer a nice way to address all of these. + * They can ship with modules and packages without affecting runtime performance. They describe a new ability for a type (namely, an editor integration for that type), but as long as no production code uses it, runtime performance is unaffected - they're just functions that never get called, and won't even be present in the final optimized binary. + * Since abilities are used elsewhere in the language, there's nothing editor-specific to learn other than the APIs themselves (which is unavoidable), so the learning curve for how to add editor plugins is minimal: just declare that your newtype has a particular ability, and the editor will pick up on it. + * Since any given type can have multiple abilities, the different ways to integrate with the editor can too. There can be one ability for adding context menu items, another for specifying how the type renders, etc. + + +In this way, abilities solve [problem #6](#problem-6-no-nice-way-to-specify-editor-specific-code). + +### Avoiding the Classification Trap +Although I think custom user-defined abilities are worth having in the language because they address [Problem #7](#problem-7-record-of-function-passing), I hope they are used rarely in practice. + + +I chose the name "ability" rather than like Trait or Typeclass because I don't want to encourage *classification* - that is, using the language feature to spend a bunch of time thinking about how to classify types by what they "are." + + +This seems to be a common exercise in statically typed languages with classes; see for example the well-known introductory example "`a Bicycle is a Vehicle`" which to me is primarily teaching students how to waste time adding complexity to their code bases for the satisfaction of classifying things, and no practical benefit. + + +(This happens in FP too; I doubt [Semiring](https://pursuit.purescript.org/packages/purescript-prelude/5.0.1/docs/Data.Semiring) ends up in a standard library because people kept opening issues saying they were unable to write some really valuable production code without it. A more likely history of that design decision is that a semiring is the mathematically proper way to `classify` those particular `types`, and `typeclasses` encourage classifying types right there in the name.) + + +In my view, type classification is a tempting but ultimately counterproductive exercise that puts a tax on a community which grows linearly with the size of that community: once enough people start doing it, everyone becomes under pressure to do the same, lest their code look suspiciously under-classified. I don't want this to happen in Roc. + + +Hopefully the name "abilities" will frame the feature as giving a type a new ability and nothing more. It's not about saying what the type *is*, but rather what you can do with it. diff --git a/design/language/RocStr.md b/design/language/RocStr.md new file mode 100644 index 0000000000..441ca1a3a7 --- /dev/null +++ b/design/language/RocStr.md @@ -0,0 +1,209 @@ +# RocStr + +This is the in-memory representation for Roc's `Str`. To explain how `Str` is laid out in memory, it's helpful to start with how `List` is laid out. + +## Empty list + +An empty `List Str` is essentially this Rust type with all 0s in memory: + +```rust +struct List { + pointer: *Str, // pointers are the same size as `usize` + length: usize +} +``` + +On a 64-bit system, this `struct` would take up 16B in memory. On a 32-bit system, it would take up 8B. + +Here's what the fields mean: + +- `pointer` is the memory address of the heap-allocated memory containing the `Bool` elements. For an empty list, the pointer is null (that is, 0). +- `length` is the number of `Bool` elements in the list. For an empty list, this is also 0. + +## Nonempty list + +Now let's say we define a `List Str` with two elements in it, like so: `["foo", "bar"]`. + +First we'd have the `struct` above, with both `length` and `capacity` set to 2. Then, we'd have some memory allocated on the heap, and `pointer` would store that memory's address. + +Here's how that heap memory would be laid out on a 64-bit system. It's a total of 48 bytes. + +```text +|------16B------|------16B------|---8B---|---8B---| + string #1 string #2 refcount unused +``` + +Just like how `List` is a `struct` that takes up `2 * usize` bytes in memory, `Str` takes up the same amount of memory - namely, 16B on a 64-bit system. That's why each of the two strings take up 16B of this heap-allocated memory. (Those structs may also point to other heap memory, but they could also be empty strings! Either way we just store the structs in the list, which take up 16B.) + +We'll get to what the refcount is for shortly, but first let's talk about the memory layout. The refcount is a `usize` integer, so 8B on our 64-bit system. Why is there 8B of unused memory after it? + +This is because of memory alignment. Whenever a system loads some memory from a memory address, it's much more efficient if the address is a multiple of the number of bytes it wants to get. So if we want to load a 16B string struct, we want its address to be a multiple of 16. + +When we're allocating memory on the heap, the way we specify what alignment we want is to say how big each element is, and how many of them we want. In this case, we say we want 16B elements, and we want 3 of them. Then we use the first 16B slot to store the 8B refcount, and the 8B after it are unused. + +This is memory-inefficient, but it's the price we pay for having all the 16B strings stored in addresses that are multiples of 16. It'd be worse for performance if we tried to pack everything tightly, so we accept the memory inefficiency as a cost of achieving better overall execution speed. + +> Note: if we happened to have 8B elements instead of 16B elements, the alignment would be 8 anyway and we'd have no unused memory. + +## Reference counting + +Let's go back to the refcount - short for "reference count." + +The refcount is a `usize` integer which counts how many times this `List` has been shared. For example, if we named this list `myList` and then wrote `[myList, myList, myList]` then we'd increment that refcount 3 times because `myList` is now being shared three more times. + +If we were to later call `List.pop` on that list, and the result was an in-place mutation that removed one of the `myList` entries, we'd decrement the refcount. If we did that again and again until the refcount got all the way down to 0, meaning nothing is using it anymore, then we'd deallocate these 48B of heap memory because nobody is using them anymore. + +In some cases, the compiler can detect that no reference counting is necessary. In that scenario, it doesn't bother allocating extra space for the refcount; instead, it inserts an instruction to allocate the memory at the appropriate place, another to free it later, and that's it. + +## Pointing to the first element + +The fact that the reference count may or may not be present could create a tricky situation for some `List` operations. + +For example, should `List.get 0` return the first 16B of the heap-allocated bytes, or the second 16B? If there's a reference count in the first 16B, it should return the second 16B. If there's no refcount, it should return the first 16B. + +To solve this, the pointer in the List struct *always* points to the first element in the list. That means to access the reference count, it does negative pointer arithmetic to get the address at 16B *preceding* the memory address it has stored in its pointer field. + +## Growing lists + +If uniqueness typing tells us that a list is Unique, we know two things about it: + +1. It doesn't need a refcount, because nothing else ever references it. +2. It can be mutated in-place. + +One of the in-place mutations that can happen to a list is that its length can increase. For example, if I call `List.append list1 list2`, and `list1` is unique, then we'll attempt to append `list2`'s contents in-place into `list1`. + +Calling `List.append` on a Shared list results in allocating a new chunk of heap memory large enough to hold both lists (with a fresh refcount, since nothing is referencing the new memory yet), then copying the contents of both lists into the new memory, and finally decrementing the refcount of the old memory. + +Calling `List.append` on a Unique list can potentially be done in-place instead. + +First, `List.append` repurposes the `usize` slot normally used to store refcount, and stores a `capacity` counter in there instead of a refcount. (After all, unique lists don't need to be refcounted.) A list's capacity refers to how many elements the list *can* hold given the memory it has allocated to it, which is always guaranteed to be at least as many as its length. + +When calling `List.append list1 list2` on a unique `list1`, first we'll check to see if `list1.capacity <= list1.length + list2.length`. If it is, then we can copy in the new values without needing to allocate more memory for `list1`. + +If there is not enough capacity to fit both lists, then we can try to call [`realloc`](https://docs.microsoft.com/en-us/cpp/c-runtime-library/reference/realloc?view=vs-2019) to hopefully extend the size of our allocated memory. If `realloc` succeeds (meaning there happened to be enough free memory right after our current allocation), then we update `capacity` to reflect the new amount of space, and move on. + +> **Note:** The reason we store capacity right after the last element in the list is because of how memory cache lines work. Whenever we need to access `capacity`, it's because we're about to increase the length of the list, which means that we will most certainly be writing to the memory location right after its last element. That in turn means that we'll need to have that memory location in cache, which in turn means that looking up the `capacity` there is guaranteed not to cause a cache miss. (It's possible that writing the new capacity value to a later address could cause a cache miss, but this strategy minimizes the chance of that happening.) An alternate design would be where we store the capacity right before the first element in the list. In that design we wouldn't have to re-write the capacity value at the end of the list every time we grew it, but we'd be much more likely to incur more cache misses that way - because we're working at the end of the list, not at the beginning. Cache misses are many times more expensive than an extra write to a memory address that's in cache already, not to mention the potential extra load instruction to add the length to the memory address of the first element (instead of subtracting 1 from that address), so we optimize for minimizing the highly expensive cache misses by always paying a tiny additional cost when increasing the length of the list, as well as a potential even tinier cost (zero, if the length already happens to be in a register) when looking up its capacity or refcount. + +If `realloc` fails, then we have to fall back on the same "allocate new memory and copy everything" strategy that we do with shared lists. + +When you have a way to anticipate that a list will want to grow incrementally to a certain size, you can avoid such extra allocations by using `List.reserve` to guarantee more capacity up front. (`List.reserve` works like Rust's [`Vec::reserve`](https://doc.rust-lang.org/std/vec/struct.Vec.html#method.reserve).) + +> **Note:** Calling `List.reserve 0 myList` will have no effect on a Unique list, but on a Shared list it will clone `myList` and return a Unique one. If you want to do a bunch of in-place mutations on a list, but it's currently Shared, calling `List.reserve 0` on it to get a Unique clone could actually be an effective performance optimization! + +## Capacity and alignment + +Some lists may end up beginning with excess capacity due to memory alignment requirements. Since the refcount is `usize`, all lists need a minimum of that alignment. For example, on a 64-bit system, a `List Bool` has an alignment of 8B even though bools can fit in 1B. + +This means the list `[True, True, False]` would have a memory layout like this): + +```text +|--------------8B--------------|--1B--|--1B--|--1B--|-----5B-----| + either refcount or capacity bool1 bool2 bool3 unused +``` + +As such, if this list is Unique, it would start out with a length of 3 and a capacity of 8. + +Since each bool value is a byte, it's okay for them to be packed side-by-side even though the overall alignment of the list elements is 8. This is fine because each of their individual memory addresses will end up being a multiple of their size in bytes. + +Note that unlike in the `List Str` example before, there wouldn't be any unused memory between the refcount (or capacity, depending on whether the list was shared or unique) and the first element in the list. That will always be the case when the size of the refcount is no bigger than the alignment of the list's elements. + +## Distinguishing between refcount and capacity in the host + +If I'm a platform author, and I receive a `List` from the application, it's important that I be able to tell whether I'm dealing with a refcount or a capacity. (The uniqueness type information will have been erased by this time, because some applications will return a Unique list while others return a Shared list, so I need to be able to tell using runtime information only which is which.) This way, I can know to either increment the refcount, or to feel free to mutate it in-place using the capacity value. + +We use a very simple system to distinguish the two: if the high bit in that `usize` value is 0, then it's capacity. If it's 1, then it's a refcount. + +This has a couple of implications: + +- `capacity` actually has a maximum of `isize::MAX`, not `usize::MAX` - because if its high bit flips to 1, then now suddenly it's considered a refcount by the host. As it happens, capacity's maximum value is naturally `isize::MAX` anyway, so there's no downside here. +- `refcount` actually begins at `isize::MIN` and increments towards 0, rather than beginning at 0 and incrementing towards a larger number. When a decrement instruction is executed and the refcount is `isize::MIN`, it gets freed instead. Since all refcounts do is count up and down, and they only ever compare the refcount to a fixed constant, there's no real performance cost to comparing to `isize::MIN` instead of to 0. So this has no significant downside either. + +Using this representation, hosts can trivially distinguish any list they receive as being either refcounted or having a capacity value, without any runtime cost in either the refcounted case or the capacity case. + +### Saturated reference count + +What happens if the reference count overflows? As in, we try to reference the same list more than `isize` times? + +In this situation, the reference count becomes unreliable. Suppose we try to increment it 3 more times after it's already been incremented `isize` times, and since we can't store any further numbers without flipping the high bit from 1 to 0 (meaning it will become a capacity value instead of a refcount), we leave it at -1. If we later decrement it `isize` times, we'll be down to `isize::MIN` and will free the memory, even though 3 things are still referencing that memory! + +This would be a total disaster, so what we do instead is that we decide to leak the memory. Once the reference count hits -1, we neither increment nor decrement it ever again, which in turn means we will never free it. So -1 is a special reference count meaning "this memory has become unreclaimable, and must never be freed." + +This has the downside of being potentially wasteful of the program's memory, but it's less detrimental to user experience than a crash, and it doesn't impact correctness at all. + +## Summary of Lists + +Lists are a `2 * usize` struct which contains a length and a pointer. + +That pointer is a memory address (null in the case of an empty list) which points to the first element in a sequential array of memory. + +If that pointer is shared in multiple places, then there will be a `usize` reference count stored right before the first element of the list. There may be unused memory after the refcount if `usize` is smaller than the alignment of one of the list's elements. + +Refcounts get incremented each time a list gets shared somewhere, and decremented each time that shared value is no longer referenced by anything else (for example, by going out of scope). Once there are no more references, the list's heap memory can be safely freed. If a reference count gets all the way up to `usize`, then it will never be decremented again and the memory will never be freed. + +Whenever a list grows, it will grow in-place if it's Unique and there is enough capacity. (Capacity is stored where a refcount would be in a Shared list.) If there isn't enough capacity - even after trying `realloc` - or if the list is Shared, then instead new heap memory will be allocated, all the necessary elements will get copied into it, and the original list's refcount will be decremented. + +## Strings + +Strings have several things in common with lists: + +- They are a `2 * usize` struct, sometimes with a non-null pointer to some heap memory +- They have a length and a capacity, and they can grow in basically the same way +- They are reference counted in basically the same way + +However, they also have two things going on that lists do not: + +- The Small String Optimization +- Literals stored in read-only memory + +## The Small String Optimization + +In practice, a lot of strings are pretty small. For example, the string `"Richard Feldman"` can be stored in 15 UTF-8 bytes. If we stored that string the same way we store a list, then on a 64-bit system we'd need a 16B struct, which would include a pointer to 24B of heap memory (including the refcount/capacity and one unused byte for alignment). + +That's a total of 48B to store 15B of data, when we could have fit the whole string into the original 16B we needed for the struct, with one byte left over. + +The Small String Optimization is where we store strings directly in the struct, assuming they can fit in there. We reserve one of those bytes to indicate whether this is a Small String or a larger one that actually uses a pointer. + +## String Memory Layout + +How do we tell small strings apart from nonsmall strings? + +We make use of the fact that lengths (for both strings *and* lists) are `usize` values which have a maximum value of `isize::MAX` rather than `usize::MAX`. This is because `List.get` compiles down to an array access operation, and LLVM uses `isize` indices for those because they do signed arithmetic on the pointer in case the caller wants to add a negative number to the address. (We don't want to, as it happens, but that's what the low-level API supports, so we are bound by its limitations.) + +Since the string's length is a `usize` value with a maximum of `isize::MAX`, we can be sure that its most significant bit will always be 0, not 1. (If it were a 1, that would be a negative `isize`!) We can use this fact to use that spare bit as a flag indicating whether the string is small: if that bit is a 1, it's a small string; otherwise, it's a nonsmall string. + +This makes calculating the length of the string a multi-step process: + +1. Get the length field out of the struct. +2. Look at its highest bit. If that bit is 0, return the length as-is. +3. If the bit is 1, then this is a small string, and its length is packed into the highest byte of the `usize` length field we're currently examining. Take that byte and bit shift it by 1 (to drop the `1` flag we used to indicate this is a small string), cast the resulting byte to `usize`, and that's our length. (Actually we bit shift by 4, not 1, because we only need 4 bits to store a length of 0-16, and the leftover 3 bits can potentially be useful in the future.) + +Using this strategy with a [conditional move instruction](https://stackoverflow.com/questions/14131096/why-is-a-conditional-move-not-vulnerable-for-branch-prediction-failure), we can always get the length of a `Str` in 2-3 cheap instructions on a single `usize` value, without any chance of a branch misprediction. + +Thus, the layout of a small string on a 64-bit big-endian architecture would be: + +```text +|-----------usize length field----------|-----------usize pointer field---------| +|-1B-|-1B-|-1B-|-1B-|-1B-|-1B-|-1B-|-1B-|-1B-|-1B-|-1B-|-1B-|-1B-|-1B-|-1B-|-1B-| + len 'R' 'i' 'c' 'h' 'a' 'r' 'd' ' ' 'F' 'e' 'l' 'd' 'm' 'a' 'n' +``` + +The `len` value here would be the number 15, plus a 1 (to flag that this is a small string) that would always get bit-shifted away. The capacity of a small Unique string is always equal to `2 * usize`, because that's how much you can fit without promoting to a nonsmall string. + +## Endianness + +The preceding memory layout example works on a big-endian architecture, but most CPUs are little-endian. That means the high bit where we want to store the flag (the 0 or 1 +that would make an `isize` either negative or positive) will actually be the `usize`'s last byte rather than its first byte. + +That means we'd have to move swap the order of the struct's length and pointer fields. Here's how the string `"Roc string"` would be stored on a little-endian system: + +```text +|-----------usize pointer field---------|-----------usize length field----------| +|-1B-|-1B-|-1B-|-1B-|-1B-|-1B-|-1B-|-1B-|-1B-|-1B-|-1B-|-1B-|-1B-|-1B-|-1B-|-1B-| + 'R' 'o' 'c' ' ' 's' 't' 'r' 'i' 'n' 'g' 0 0 0 0 0 len +``` + +Here, `len` would have the same format as before (including the extra 1 in the same position, which we'd bit shift away) except that it'd store a length of 10 instead of 15. + +Notice that the leftover bytes are stored as zeroes. This is handy because it means we can convert small Roc strings into C strings (which are 0-terminated) for free as long as they have at least one unused byte. Also notice that `usize pointer field` and `usize length field` have been swapped compared to the preceding example! + +## Storing string literals in read-only memory diff --git a/devtools/README.md b/devtools/README.md new file mode 100755 index 0000000000..96bcfde988 --- /dev/null +++ b/devtools/README.md @@ -0,0 +1,47 @@ +# devtools + +To make rust-analyzer and other vscode extensions work well you want them using the same rustc, glibc, zig... as specified in the roc nix flake. +The easiest way to do this is to use another flake for all your dev tools that takes the roc flake as an input. + +The flake in this folder is meant for vscode, feel free to create a PR if you'like to add a flake for a different editor. + +Further steps: + +1. Copy the flake.nix and flake.lock file from the devtools folder to a new folder outside of the roc repo folder. +1. Run `git init` in the new folder. +1. Execute `git add flake.nix flake.lock`, nix will error if you don't do this. +1. Change `roc.url = "path:/home/username/gitrepos/roc";` to the location of the roc folder on your machine. +1. Follow instructions about vscode extensions [here](#extensions). +1. add other dev tools you like in the `devInputs` list. You can search for those [here](https://search.nixos.org/packages). +1. Run `nix develop`. +1. `cd` to the folder of the roc repo +1. Run `code .` to start vscode. + +vscode is able to share settings between this nix version and your regular vscode so there is no need to set everything up from scratch. + +If you use lorri or direnv it is possible to load the dev flake instead of the roc flake. +For lorri: + +1. copy the `shell.nix` at the root of this repo to the folder containing your dev tools flake. +1. edit `.envrc` to contain: + +```sh +eval "$(lorri direnv --shell-file path-to-your-dev-flake-folder/shell.nix)" +``` + +## Extensions + +### for those who have some Nix experience + +Add vscode extensions you want to use below `vscodeExtensions = ...`. You can search for extension names [here](https://search.nixos.org/packages?channel=22.05&from=0&size=50&sort=relevance&type=packages&query=vscode-extensions+extensionYouAreSearchingFor). You may not be able to install extensions through the UI in vscode, I'd recommend always adding them to the flake. If you are inside a `nix develop` shell, run `exit` and `nix develop path-to-your-dev-flake-folder` to be able to run vscode with the newly added extensions. + +If your extension is not available on nix, you can add them [from the vscode marketplaces as well](https://stackoverflow.com/a/54812021/4200103). + +### for those with little to no Nix experience + +Instead of running `code` in the last step you can use the `--extensions-dir` flag to allow you to install extensions using the vscode GUI. +On MacOS or Linux: + +```sh +code --extensions-dir="$HOME/.vscode/extensions" +``` diff --git a/devtools/flake.lock b/devtools/flake.lock new file mode 100644 index 0000000000..d90cc13aad --- /dev/null +++ b/devtools/flake.lock @@ -0,0 +1,178 @@ +{ + "nodes": { + "flake-compat": { + "flake": false, + "locked": { + "lastModified": 1696426674, + "narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=", + "owner": "edolstra", + "repo": "flake-compat", + "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33", + "type": "github" + }, + "original": { + "owner": "edolstra", + "repo": "flake-compat", + "type": "github" + } + }, + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1694529238, + "narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "ff7b65b44d01cf9ba6a71320833626af21126384", + "type": "github" + }, + "original": { + "id": "flake-utils", + "type": "indirect" + } + }, + "flake-utils_2": { + "inputs": { + "systems": "systems_2" + }, + "locked": { + "lastModified": 1694529238, + "narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "ff7b65b44d01cf9ba6a71320833626af21126384", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixgl": { + "inputs": { + "flake-utils": [ + "roc", + "flake-utils" + ], + "nixpkgs": [ + "roc", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1685908677, + "narHash": "sha256-E4zUPEUFyVWjVm45zICaHRpfGepfkE9Z2OECV9HXfA4=", + "owner": "guibou", + "repo": "nixGL", + "rev": "489d6b095ab9d289fe11af0219a9ff00fe87c7c5", + "type": "github" + }, + "original": { + "owner": "guibou", + "repo": "nixGL", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1693140250, + "narHash": "sha256-URyIDETtu1bbxcSl83xp7irEV04dPEgj7O3LjHcD1Sk=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "676fe5e01b9a41fa14aaa48d87685677664104b1", + "type": "github" + }, + "original": { + "owner": "nixos", + "repo": "nixpkgs", + "rev": "676fe5e01b9a41fa14aaa48d87685677664104b1", + "type": "github" + } + }, + "roc": { + "inputs": { + "flake-compat": "flake-compat", + "flake-utils": "flake-utils_2", + "nixgl": "nixgl", + "nixpkgs": "nixpkgs", + "rust-overlay": "rust-overlay" + }, + "locked": { + "lastModified": 1700241573, + "narHash": "sha256-+hjY1FieVbF8jvRE3Cvo8GBWh1OlFrF+QDFF8OlWM/s=", + "path": "/home/username/gitrepos/JRMurr/roc", + "type": "path" + }, + "original": { + "path": "/home/username/gitrepos/JRMurr/roc", + "type": "path" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "roc": "roc" + } + }, + "rust-overlay": { + "inputs": { + "flake-utils": [ + "roc", + "flake-utils" + ], + "nixpkgs": [ + "roc", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1695694299, + "narHash": "sha256-0CucEiOZzOVHwmGDJKNXLj7aDYOqbRtqChp9nbGrh18=", + "owner": "oxalica", + "repo": "rust-overlay", + "rev": "c89a55d2d91cf55234466934b25deeffa365188a", + "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/devtools/flake.nix b/devtools/flake.nix new file mode 100755 index 0000000000..3c604b760d --- /dev/null +++ b/devtools/flake.nix @@ -0,0 +1,67 @@ +{ + description = "Allows sharing dependencies between dev tools and roc. Prevents version GLIBC_2.36 not found."; + + inputs = { + # change this path to the path of your roc folder + roc.url = "path:/home/username/gitrepos/roc"; + }; + outputs = { self, roc, flake-utils }: + let + supportedSystems = [ "x86_64-linux" "x86_64-darwin" "aarch64-darwin" ]; + flake-utils = roc.inputs.flake-utils; + in + flake-utils.lib.eachSystem supportedSystems (system: + let + pkgs = import roc.inputs.nixpkgs { + inherit system; + config.allowUnfree = true; + }; + + isAarch64Darwin = pkgs.stdenv.hostPlatform.system == "aarch64-darwin"; + + rocShell = roc.devShell.${system}; + in + { + devShell = pkgs.mkShell { + packages = + let + devInputs = with pkgs; + [ less bashInteractive ] + ++ (if isAarch64Darwin then [ ] else [ gdb ]); + + vscodeWithExtensions = pkgs.vscode-with-extensions.override { + vscodeExtensions = with pkgs.vscode-extensions; + [ + matklad.rust-analyzer + # eamodio.gitlens + bbenoist.nix + tamasfe.even-better-toml + ] ++ (if isAarch64Darwin then [ ] else [ vadimcn.vscode-lldb ]) + ++ pkgs.vscode-utils.extensionsFromVscodeMarketplace [ + { + name = "roc-lang-support"; + publisher = "benjamin-thomas"; + version = "0.0.4"; + # keep this sha for the first run, nix will tell you the correct one to change it to + sha256 = "sha256-USZiXdvYa8hxj62cy6hdiS5c2tIDIQxSyux684lyAEY="; + } + ] + ; + }; + in + [ vscodeWithExtensions devInputs ]; + + inputsFrom = [ rocShell ]; + + # env vars + NIX_GLIBC_PATH = rocShell.NIX_GLIBC_PATH; + LD_LIBRARY_PATH = rocShell.LD_LIBRARY_PATH; + NIXPKGS_ALLOW_UNFREE = rocShell.NIXPKGS_ALLOW_UNFREE; + + # to set the LLVM_SYS__PREFIX + shellHook = rocShell.shellHook; + + formatter = pkgs.nixpkgs-fmt; + }; + }); +} diff --git a/devtools/signing.md b/devtools/signing.md new file mode 100644 index 0000000000..16eb465422 --- /dev/null +++ b/devtools/signing.md @@ -0,0 +1,71 @@ +# Commit Signing Guide + +If you don't have signing set up on your device and you only want to make simple changes, it will be easier to use [github's edit button](https://docs.github.com/en/repositories/working-with-files/managing-files/editing-files) for single file changes or [github's online VSCode editor](https://docs.github.com/en/codespaces/the-githubdev-web-based-editor#opening-the-githubdev-editor) for multi-file changes. These tools will sign your commit automatically. + +For complex changes you will want to set up signing on your device. +Follow along with the subsection below that applies to you. + +If your situation is not listed below, consider adding your steps to help out others. + +## Setting up commit signing for the first time + +If you have a Yubikey, and use macOS or Linux, follow [guide 1](https://dev.to/paulmicheli/using-your-yubikey-to-get-started-with-gpg-3h4k) and [guide 2](https://dev.to/paulmicheli/using-your-yubikey-for-signed-git-commits-4l73). +For windows with a Yubikey, follow [this guide](https://scatteredcode.net/signing-git-commits-using-yubikey-on-windows/). + +Without a Yubikey: + 1. [Make a key to sign your commits.](https://docs.github.com/en/authentication/managing-commit-signature-verification/generating-a-new-gpg-key) + 2. [Configure git to use your key.](https://docs.github.com/en/authentication/managing-commit-signature-verification/telling-git-about-your-signing-key) + 3. Make git sign your commits automatically: + + ```sh + git config --global commit.gpgsign true + ``` + +## Transferring existing key from Linux to Windows + +### With Yubikey + +This explanation was based on the steps outlined [here](https://scatteredcode.net/signing-git-commits-using-yubikey-on-windows/). + +On linux, run: +``` +gpg --list-keys --keyid-format SHORT | grep ^pub +gpg --export --armor [Your_Key_ID] > public.asc +``` + +Copy the public.asc file to windows. + +Download and install [Gpg4win](https://www.gpg4win.org/get-gpg4win.html). + +Open the program Kleopatra (installed with gpg4win) and go to Smartcards. +You should see your Yubikey there, it should also say something like `failed to find public key locally`. Click the import button and open the `public.asc` file you created earlier. +Close Kleopatra. + +Install the [YubiKey Minidriver for 64-bit systems – Windows Installer](https://www.yubico.com/support/download/smart-card-drivers-tools/). + +Insert your Yubikey and check if it is mentioned in the output of `gpg --card-status` (powershell). + +Open powershell and execute: +``` +git config --global gpg.program "c:\Program Files (x86)\GnuPG\bin\gpg.exe" +git config --global commit.gpgsign true +gpg --list-secret-keys --keyid-format LONG +``` +The last command will show your keyid. On the line that says `[SC]`, copy the id. +In the example below the id is 683AB68D867FEB5C +``` +sec> rsa4096/683AB68D867FEB5C 2020-02-02 [SC] [expires: 2022-02-02] +``` + +Tell git your keyid: +``` +>git config --global user.signingkey YOUR_KEY_ID_HERE +``` + +That's it! + +### Without Yubikey + +TODO + + diff --git a/docker/nightly-debian-bookworm/Dockerfile b/docker/nightly-debian-bookworm/Dockerfile new file mode 100644 index 0000000000..1e5ad905f0 --- /dev/null +++ b/docker/nightly-debian-bookworm/Dockerfile @@ -0,0 +1,17 @@ +FROM debian:bookworm + +RUN apt-get update --fix-missing +RUN apt-get upgrade --yes + +RUN apt-get install --yes wget + +# you can leave out libc-dev and binutils if you don't need the REPL +RUN apt-get install --yes libc-dev binutils + +RUN wget -q -O roc.tar.gz https://github.com/roc-lang/roc/releases/download/nightly/roc_nightly-linux_x86_64-latest.tar.gz + +RUN mkdir /usr/lib/roc + +RUN tar -xvz -f roc.tar.gz --directory /usr/lib/roc --strip-components=1 + +ENV PATH="$PATH:/usr/lib/roc" diff --git a/docker/nightly-debian-bookworm/docker-compose.example.yml b/docker/nightly-debian-bookworm/docker-compose.example.yml new file mode 100644 index 0000000000..d33c7ff31f --- /dev/null +++ b/docker/nightly-debian-bookworm/docker-compose.example.yml @@ -0,0 +1,8 @@ +version: "3" +services: + roc: + build: . + entrypoint: roc + working_dir: /home/ubuntu/app + volumes: + - ../../:/home/ubuntu/app diff --git a/docker/nightly-debian-buster/Dockerfile b/docker/nightly-debian-buster/Dockerfile new file mode 100644 index 0000000000..c847fcd211 --- /dev/null +++ b/docker/nightly-debian-buster/Dockerfile @@ -0,0 +1,18 @@ +FROM debian:buster + +RUN apt-get update --fix-missing +RUN apt-get upgrade --yes + +RUN apt-get install --yes wget + +# you can leave out libc-dev and binutils if you don't need the REPL +RUN apt-get install --yes libc-dev binutils + +RUN wget -q -O roc.tar.gz https://github.com/roc-lang/roc/releases/download/nightly/roc_nightly-old_linux_x86_64-latest.tar.gz + +RUN mkdir /usr/lib/roc + +RUN tar -xvz -f roc.tar.gz --directory /usr/lib/roc --strip-components=1 + +ENV PATH="$PATH:/usr/lib/roc" + diff --git a/docker/nightly-debian-buster/docker-compose.example.yml b/docker/nightly-debian-buster/docker-compose.example.yml new file mode 100644 index 0000000000..d33c7ff31f --- /dev/null +++ b/docker/nightly-debian-buster/docker-compose.example.yml @@ -0,0 +1,8 @@ +version: "3" +services: + roc: + build: . + entrypoint: roc + working_dir: /home/ubuntu/app + volumes: + - ../../:/home/ubuntu/app diff --git a/docker/nightly-debian-latest/Dockerfile b/docker/nightly-debian-latest/Dockerfile new file mode 100644 index 0000000000..d2bab43987 --- /dev/null +++ b/docker/nightly-debian-latest/Dockerfile @@ -0,0 +1,17 @@ +FROM debian:latest + +RUN apt-get update --fix-missing +RUN apt-get upgrade --yes + +RUN apt-get install --yes wget + +# you can leave out libc-dev and binutils if you don't need the REPL +RUN apt-get install --yes libc-dev binutils + +RUN wget -q -O roc.tar.gz https://github.com/roc-lang/roc/releases/download/nightly/roc_nightly-linux_x86_64-latest.tar.gz + +RUN mkdir /usr/lib/roc + +RUN tar -xvz -f roc.tar.gz --directory /usr/lib/roc --strip-components=1 + +ENV PATH="$PATH:/usr/lib/roc" diff --git a/docker/nightly-debian-latest/docker-compose.example.yml b/docker/nightly-debian-latest/docker-compose.example.yml new file mode 100644 index 0000000000..d33c7ff31f --- /dev/null +++ b/docker/nightly-debian-latest/docker-compose.example.yml @@ -0,0 +1,8 @@ +version: "3" +services: + roc: + build: . + entrypoint: roc + working_dir: /home/ubuntu/app + volumes: + - ../../:/home/ubuntu/app diff --git a/docker/nightly-ubuntu-2004/Dockerfile b/docker/nightly-ubuntu-2004/Dockerfile new file mode 100644 index 0000000000..81a047e860 --- /dev/null +++ b/docker/nightly-ubuntu-2004/Dockerfile @@ -0,0 +1,17 @@ +FROM ubuntu:20.04 + +RUN apt-get update --fix-missing +RUN apt-get upgrade --yes + +RUN apt-get install --yes wget + +# you can leave out libc-dev and binutils if you don't need the REPL +RUN apt-get install --yes libc-dev binutils + +RUN wget -q -O roc.tar.gz https://github.com/roc-lang/roc/releases/download/nightly/roc_nightly-linux_x86_64-latest.tar.gz + +RUN mkdir /usr/lib/roc + +RUN tar -xvz -f roc.tar.gz --directory /usr/lib/roc --strip-components=1 + +ENV PATH="$PATH:/usr/lib/roc" diff --git a/docker/nightly-ubuntu-2004/docker-compose.example.yml b/docker/nightly-ubuntu-2004/docker-compose.example.yml new file mode 100644 index 0000000000..d33c7ff31f --- /dev/null +++ b/docker/nightly-ubuntu-2004/docker-compose.example.yml @@ -0,0 +1,8 @@ +version: "3" +services: + roc: + build: . + entrypoint: roc + working_dir: /home/ubuntu/app + volumes: + - ../../:/home/ubuntu/app diff --git a/docker/nightly-ubuntu-2204/Dockerfile b/docker/nightly-ubuntu-2204/Dockerfile new file mode 100644 index 0000000000..fab49301bc --- /dev/null +++ b/docker/nightly-ubuntu-2204/Dockerfile @@ -0,0 +1,17 @@ +FROM ubuntu:22.04 + +RUN apt-get update --fix-missing +RUN apt-get upgrade --yes + +RUN apt-get install --yes wget + +# you can leave out libc-dev and binutils if you don't need the REPL +RUN apt-get install --yes libc-dev binutils + +RUN wget -q -O roc.tar.gz https://github.com/roc-lang/roc/releases/download/nightly/roc_nightly-linux_x86_64-latest.tar.gz + +RUN mkdir /usr/lib/roc + +RUN tar -xvz -f roc.tar.gz --directory /usr/lib/roc --strip-components=1 + +ENV PATH="$PATH:/usr/lib/roc" diff --git a/docker/nightly-ubuntu-2204/docker-compose.example.yml b/docker/nightly-ubuntu-2204/docker-compose.example.yml new file mode 100644 index 0000000000..d33c7ff31f --- /dev/null +++ b/docker/nightly-ubuntu-2204/docker-compose.example.yml @@ -0,0 +1,8 @@ +version: "3" +services: + roc: + build: . + entrypoint: roc + working_dir: /home/ubuntu/app + volumes: + - ../../:/home/ubuntu/app diff --git a/docker/nightly-ubuntu-latest/Dockerfile b/docker/nightly-ubuntu-latest/Dockerfile new file mode 100644 index 0000000000..fa65a87928 --- /dev/null +++ b/docker/nightly-ubuntu-latest/Dockerfile @@ -0,0 +1,17 @@ +FROM ubuntu:latest + +RUN apt-get update --fix-missing +RUN apt-get upgrade --yes + +RUN apt-get install --yes wget + +# you can leave out libc-dev and binutils if you don't need the REPL +RUN apt-get install --yes libc-dev binutils + +RUN wget -q -O roc.tar.gz https://github.com/roc-lang/roc/releases/download/nightly/roc_nightly-linux_x86_64-latest.tar.gz + +RUN mkdir /usr/lib/roc + +RUN tar -xvz -f roc.tar.gz --directory /usr/lib/roc --strip-components=1 + +ENV PATH="$PATH:/usr/lib/roc" diff --git a/docker/nightly-ubuntu-latest/docker-compose.example.yml b/docker/nightly-ubuntu-latest/docker-compose.example.yml new file mode 100644 index 0000000000..d33c7ff31f --- /dev/null +++ b/docker/nightly-ubuntu-latest/docker-compose.example.yml @@ -0,0 +1,8 @@ +version: "3" +services: + roc: + build: . + entrypoint: roc + working_dir: /home/ubuntu/app + volumes: + - ../../:/home/ubuntu/app diff --git a/docs/Cargo.toml b/docs/Cargo.toml deleted file mode 100644 index 9f41af3a46..0000000000 --- a/docs/Cargo.toml +++ /dev/null @@ -1,28 +0,0 @@ -[package] -name = "roc_docs" -version = "0.1.0" -license = "UPL-1.0" -authors = ["The Roc Contributors"] -edition = "2021" - -[dependencies] -pulldown-cmark = { version = "0.8.0", default-features = false } -roc_ast = { path = "../ast" } -roc_load = { path = "../compiler/load" } -roc_builtins = { path = "../compiler/builtins" } -roc_can = { path = "../compiler/can" } -roc_code_markup = { path = "../code_markup"} -roc_module = { path = "../compiler/module" } -roc_region = { path = "../compiler/region" } -roc_types = { path = "../compiler/types" } -roc_parse = { path = "../compiler/parse" } -roc_target = { path = "../compiler/roc_target" } -roc_collections = { path = "../compiler/collections" } -roc_highlight = { path = "../highlight"} -roc_reporting = { path = "../reporting"} -bumpalo = { version = "3.8.0", features = ["collections"] } -snafu = { version = "0.6.10", features = ["backtraces"] } -peg = "0.8.0" - -[dev-dependencies] -pretty_assertions = "1.0.0" diff --git a/docs/src/docs_error.rs b/docs/src/docs_error.rs deleted file mode 100644 index 7ee2f06ced..0000000000 --- a/docs/src/docs_error.rs +++ /dev/null @@ -1,55 +0,0 @@ -use peg::error::ParseError; -use roc_ast::ast_error::ASTError; -use roc_module::module_err::ModuleError; -use roc_parse::parser::SyntaxError; -use snafu::Snafu; - -#[derive(Debug, Snafu)] -#[snafu(visibility(pub))] -#[allow(clippy::enum_variant_names)] -pub enum DocsError { - WrapASTError { - #[snafu(backtrace)] - source: ASTError, - }, - WrapModuleError { - #[snafu(backtrace)] - source: ModuleError, - }, - WrapSyntaxError { - msg: String, - }, - WrapPegParseError { - source: ParseError, - }, -} - -pub type DocsResult = std::result::Result; - -impl<'a> From> for DocsError { - fn from(syntax_err: SyntaxError) -> Self { - Self::WrapSyntaxError { - msg: format!("{:?}", syntax_err), - } - } -} - -impl From for DocsError { - fn from(ast_err: ASTError) -> Self { - Self::WrapASTError { source: ast_err } - } -} - -impl From for DocsError { - fn from(module_err: ModuleError) -> Self { - Self::WrapModuleError { source: module_err } - } -} - -impl From> for DocsError { - fn from(peg_parse_err: ParseError) -> Self { - Self::WrapPegParseError { - source: peg_parse_err, - } - } -} diff --git a/docs/src/html.rs b/docs/src/html.rs deleted file mode 100644 index baad3715db..0000000000 --- a/docs/src/html.rs +++ /dev/null @@ -1,85 +0,0 @@ -use roc_code_markup::{markup::nodes::MarkupNode, slow_pool::SlowPool}; - -// determine appropriate css class for MarkupNode -pub fn mark_node_to_html(mark_node: &MarkupNode, mark_node_pool: &SlowPool, buf: &mut String) { - let mut additional_newlines = 0; - - match mark_node { - MarkupNode::Nested { - children_ids, - newlines_at_end, - .. - } => { - for &child_id in children_ids { - mark_node_to_html(mark_node_pool.get(child_id), mark_node_pool, buf) - } - - additional_newlines = *newlines_at_end; - } - MarkupNode::Text { - content, - syn_high_style, - newlines_at_end, - .. - } => { - use roc_code_markup::syntax_highlight::HighlightStyle::*; - - let css_class = match syn_high_style { - Operator => "operator", - String => "string", - FunctionName => "function-name", - FunctionArgName => "function-arg-name", - Type => "type", - Bracket => "bracket", - Number => "number", - PackageRelated => "package-related", - Value => "value", - RecordField => "recordfield", - Import => "import", - Provides => "provides", - Blank => "blank", - Comment => "comment", - DocsComment => "docs-comment", - UppercaseIdent => "uppercase-ident", - LowercaseIdent => "lowercase-ident", - Keyword => "keyword-ident", - }; - - write_html_to_buf(content, css_class, buf); - - additional_newlines = *newlines_at_end; - } - MarkupNode::Blank { - newlines_at_end, .. - } => { - let mut content_str = " ".to_string(); - - for _ in 0..*newlines_at_end { - content_str.push('\n'); - } - - write_html_to_buf(&content_str, "blank", buf); - - additional_newlines = *newlines_at_end; - } - MarkupNode::Indent { .. } => { - let content_str = mark_node.get_content(); - - write_html_to_buf(&content_str, "indent", buf); - } - } - - for _ in 0..additional_newlines { - buf.push('\n') - } -} - -fn write_html_to_buf(content: &str, css_class: &'static str, buf: &mut String) { - let opening_tag: String = [""].concat(); - - buf.push_str(opening_tag.as_str()); - - buf.push_str(content); - - buf.push_str(""); -} diff --git a/docs/src/lib.rs b/docs/src/lib.rs deleted file mode 100644 index 7ed3544364..0000000000 --- a/docs/src/lib.rs +++ /dev/null @@ -1,1002 +0,0 @@ -extern crate pulldown_cmark; -extern crate roc_load; -use bumpalo::Bump; -use docs_error::{DocsError, DocsResult}; -use html::mark_node_to_html; -use roc_can::scope::Scope; -use roc_code_markup::markup::nodes::MarkupNode; -use roc_code_markup::slow_pool::SlowPool; -use roc_highlight::highlight_parser::{highlight_defs, highlight_expr}; -use roc_load::docs::DocEntry::DocDef; -use roc_load::docs::{DocEntry, TypeAnnotation}; -use roc_load::docs::{ModuleDocumentation, RecordField}; -use roc_load::{LoadedModule, LoadingProblem, Threading}; -use roc_module::symbol::{IdentIdsByModule, Interns, ModuleId}; -use roc_parse::ident::{parse_ident, Ident}; -use roc_parse::state::State; -use roc_region::all::Region; -use std::fs; -use std::path::{Path, PathBuf}; - -mod docs_error; -mod html; - -const BUILD_DIR: &str = "./generated-docs"; - -pub fn generate_docs_html(filenames: Vec) { - let build_dir = Path::new(BUILD_DIR); - let loaded_modules = load_modules_for_files(filenames); - - // TODO: get info from a package module; this is all hardcoded for now. - let mut package = roc_load::docs::Documentation { - name: "roc/builtins".to_string(), - version: "1.0.0".to_string(), - docs: "Package introduction or README.".to_string(), - modules: loaded_modules, - }; - - if !build_dir.exists() { - fs::create_dir_all(build_dir).expect("TODO gracefully handle unable to create build dir"); - } - - // Copy over the assets - fs::write( - build_dir.join("search.js"), - include_str!("./static/search.js"), - ) - .expect("TODO gracefully handle failing to make the search javascript"); - - fs::write( - build_dir.join("styles.css"), - include_str!("./static/styles.css"), - ) - .expect("TODO gracefully handle failing to make the stylesheet"); - - fs::write( - build_dir.join("favicon.svg"), - include_str!("./static/favicon.svg"), - ) - .expect("TODO gracefully handle failing to make the favicon"); - - let template_html = include_str!("./static/index.html") - .replace("", "/search.js") - .replace("", "/styles.css") - .replace("", "/favicon.svg") - .replace( - "", - render_sidebar(package.modules.iter().flat_map(|loaded_module| { - loaded_module - .documentation - .iter() - .filter_map(move |(module_id, module)| { - // TODO it seems this `documentation` dictionary has entries for - // every module, but only the current module has any info in it. - // We disregard the others, but probably this shouldn't bother - // being a hash map in the first place if only one of its entries - // actually has interesting information in it? - if *module_id == loaded_module.module_id { - let exposed_values = loaded_module - .exposed_values - .iter() - .map(|symbol| symbol.as_str(&loaded_module.interns).to_string()) - .collect::>(); - - Some((module, exposed_values)) - } else { - None - } - }) - })) - .as_str(), - ); - - // Write each package's module docs html file - for loaded_module in package.modules.iter_mut() { - for (module_id, module_docs) in loaded_module.documentation.iter() { - if *module_id == loaded_module.module_id { - let module_dir = build_dir.join(module_docs.name.replace('.', "/").as_str()); - - fs::create_dir_all(&module_dir) - .expect("TODO gracefully handle not being able to create the module dir"); - - let rendered_module = template_html - .replace( - "", - render_name_and_version(package.name.as_str(), package.version.as_str()) - .as_str(), - ) - .replace( - "", - render_module_documentation(module_docs, loaded_module).as_str(), - ); - - fs::write(module_dir.join("index.html"), rendered_module).expect( - "TODO gracefully handle failing to write index.html inside module's dir", - ); - } - } - } - - println!("🎉 Docs generated in {}", build_dir.display()); -} - -// converts plain-text code to highlighted html -pub fn syntax_highlight_expr(code_str: &str) -> DocsResult { - let trimmed_code_str = code_str.trim_end().trim(); - let mut mark_node_pool = SlowPool::default(); - - let mut highlighted_html_str = String::new(); - - match highlight_expr(trimmed_code_str, &mut mark_node_pool) { - Ok(root_mark_node_id) => { - let root_mark_node = mark_node_pool.get(root_mark_node_id); - mark_node_to_html(root_mark_node, &mark_node_pool, &mut highlighted_html_str); - - Ok(highlighted_html_str) - } - Err(err) => Err(DocsError::from(err)), - } -} - -// converts plain-text code to highlighted html -pub fn syntax_highlight_top_level_defs(code_str: &str) -> DocsResult { - let trimmed_code_str = code_str.trim_end().trim(); - - let mut mark_node_pool = SlowPool::default(); - - let mut highlighted_html_str = String::new(); - - match highlight_defs(trimmed_code_str, &mut mark_node_pool) { - Ok(mark_node_id_vec) => { - let def_mark_nodes: Vec<&MarkupNode> = mark_node_id_vec - .iter() - .map(|mn_id| mark_node_pool.get(*mn_id)) - .collect(); - - for mn in def_mark_nodes { - mark_node_to_html(mn, &mark_node_pool, &mut highlighted_html_str) - } - - Ok(highlighted_html_str) - } - Err(err) => Err(DocsError::from(err)), - } -} - -fn render_module_documentation( - module: &ModuleDocumentation, - loaded_module: &LoadedModule, -) -> String { - let mut buf = String::new(); - - buf.push_str( - html_to_string( - "h2", - vec![("class", "module-name")], - html_to_string("a", vec![("href", "/#")], module.name.as_str()).as_str(), - ) - .as_str(), - ); - - let exposed_values = loaded_module.exposed_values_str(); - - for entry in &module.entries { - let mut should_render_entry = true; - - if let DocDef(def) = entry { - // We dont want to render entries that arent exposed - should_render_entry = exposed_values.contains(&def.name.as_str()); - } - - if should_render_entry { - match entry { - DocEntry::DocDef(doc_def) => { - let mut href = String::new(); - href.push('#'); - href.push_str(doc_def.name.as_str()); - - let name = doc_def.name.as_str(); - - let mut content = String::new(); - - content.push_str( - html_to_string("a", vec![("href", href.as_str())], name).as_str(), - ); - - for type_var in &doc_def.type_vars { - content.push(' '); - content.push_str(type_var.as_str()); - } - - let type_ann = &doc_def.type_annotation; - - match type_ann { - TypeAnnotation::NoTypeAnn => {} - _ => { - content.push_str(" : "); - } - } - - type_annotation_to_html(0, &mut content, type_ann); - - buf.push_str( - html_to_string( - "h3", - vec![("id", name), ("class", "entry-name")], - content.as_str(), - ) - .as_str(), - ); - - if let Some(docs) = &doc_def.docs { - buf.push_str( - markdown_to_html( - &exposed_values, - &module.scope, - docs.to_string(), - loaded_module, - ) - .as_str(), - ); - } - } - DocEntry::DetachedDoc(docs) => { - let markdown = markdown_to_html( - &exposed_values, - &module.scope, - docs.to_string(), - loaded_module, - ); - buf.push_str(markdown.as_str()); - } - }; - } - } - - buf -} - -fn html_to_string(tag_name: &str, attrs: Vec<(&str, &str)>, content: &str) -> String { - let mut buf = String::new(); - - buf.push('<'); - buf.push_str(tag_name); - - for (key, value) in &attrs { - buf.push(' '); - buf.push_str(key); - buf.push_str("=\""); - buf.push_str(value); - buf.push('"'); - } - - if !&attrs.is_empty() { - buf.push(' '); - } - - buf.push('>'); - - buf.push_str(content); - - buf.push_str("'); - - buf -} - -fn base_url() -> String { - // e.g. "builtins/" in "https://roc-lang.org/builtins/Str" - // - // TODO make this a CLI flag to the `docs` subcommand instead of an env var - match std::env::var("ROC_DOCS_URL_ROOT") { - Ok(root_builtins_path) => { - let mut url_str = String::with_capacity(root_builtins_path.len() + 64); - - if !root_builtins_path.starts_with('/') { - url_str.push('/'); - } - - url_str.push_str(&root_builtins_path); - - if !root_builtins_path.ends_with('/') { - url_str.push('/'); - } - - url_str - } - _ => { - let mut url_str = String::with_capacity(64); - - url_str.push('/'); - - url_str - } - } -} - -fn render_name_and_version(name: &str, version: &str) -> String { - let mut buf = String::new(); - let mut url_str = base_url(); - - url_str.push_str(name); - - buf.push_str( - html_to_string( - "h1", - vec![("class", "pkg-full-name")], - html_to_string("a", vec![("href", url_str.as_str())], name).as_str(), - ) - .as_str(), - ); - - let mut versions_url_str = base_url(); - - versions_url_str.push('/'); - versions_url_str.push_str(name); - versions_url_str.push('/'); - versions_url_str.push_str(version); - - buf.push_str( - html_to_string( - "a", - vec![("class", "version"), ("href", versions_url_str.as_str())], - version, - ) - .as_str(), - ); - - buf -} - -fn render_sidebar<'a, I: Iterator)>>( - modules: I, -) -> String { - let mut buf = String::new(); - - for (module, exposed_values) in modules { - let mut sidebar_entry_content = String::new(); - - let name = module.name.as_str(); - - let href = { - let mut href_buf = base_url(); - href_buf.push_str(name); - href_buf - }; - - sidebar_entry_content.push_str( - html_to_string( - "a", - vec![("class", "sidebar-module-link"), ("href", href.as_str())], - name, - ) - .as_str(), - ); - - let entries = { - let mut entries_buf = String::new(); - - for entry in &module.entries { - if let DocEntry::DocDef(doc_def) = entry { - if exposed_values.contains(&doc_def.name) { - let mut entry_href = String::new(); - - entry_href.push_str(href.as_str()); - entry_href.push('#'); - entry_href.push_str(doc_def.name.as_str()); - - entries_buf.push_str( - html_to_string( - "a", - vec![("href", entry_href.as_str())], - doc_def.name.as_str(), - ) - .as_str(), - ); - } - } - } - - entries_buf - }; - - sidebar_entry_content.push_str( - html_to_string( - "div", - vec![("class", "sidebar-sub-entries")], - entries.as_str(), - ) - .as_str(), - ); - - buf.push_str( - html_to_string( - "div", - vec![("class", "sidebar-entry")], - sidebar_entry_content.as_str(), - ) - .as_str(), - ); - } - - buf -} - -pub fn load_modules_for_files(filenames: Vec) -> Vec { - let arena = Bump::new(); - let mut modules = Vec::with_capacity(filenames.len()); - - for filename in filenames { - let mut src_dir = filename.clone(); - src_dir.pop(); - - match roc_load::load_and_typecheck( - &arena, - filename, - src_dir.as_path(), - Default::default(), - roc_target::TargetInfo::default_x86_64(), // This is just type-checking for docs, so "target" doesn't matter - roc_reporting::report::RenderTarget::ColorTerminal, - Threading::AllAvailable, - ) { - Ok(loaded) => modules.push(loaded), - Err(LoadingProblem::FormattedReport(report)) => { - eprintln!("{}", report); - std::process::exit(1); - } - Err(e) => panic!("{:?}", e), - } - } - - modules -} - -const INDENT: &str = " "; - -fn indent(buf: &mut String, times: usize) { - for _ in 0..times { - buf.push_str(INDENT); - } -} - -fn new_line(buf: &mut String) { - buf.push('\n'); -} - -// html is written to buf -fn type_annotation_to_html(indent_level: usize, buf: &mut String, type_ann: &TypeAnnotation) { - let is_multiline = should_be_multiline(type_ann); - match type_ann { - TypeAnnotation::TagUnion { tags, extension } => { - let tags_len = tags.len(); - - let tag_union_indent = indent_level + 1; - - if is_multiline { - new_line(buf); - - indent(buf, tag_union_indent); - } - - buf.push('['); - - if is_multiline { - new_line(buf); - } - - let next_indent_level = tag_union_indent + 1; - - for (index, tag) in tags.iter().enumerate() { - if is_multiline { - indent(buf, next_indent_level); - } else { - buf.push(' '); - } - - buf.push_str(tag.name.as_str()); - - for type_value in &tag.values { - buf.push(' '); - type_annotation_to_html(next_indent_level, buf, type_value); - } - - if is_multiline { - if index < (tags_len - 1) { - buf.push(','); - } - - new_line(buf); - } - } - - if is_multiline { - indent(buf, tag_union_indent); - } else { - buf.push(' '); - } - - buf.push(']'); - - type_annotation_to_html(indent_level, buf, extension); - } - TypeAnnotation::BoundVariable(var_name) => { - buf.push_str(var_name); - } - TypeAnnotation::Apply { name, parts } => { - if parts.is_empty() { - buf.push_str(name); - } else { - buf.push('('); - buf.push_str(name); - for part in parts { - buf.push(' '); - type_annotation_to_html(indent_level, buf, part); - } - buf.push(')'); - } - } - TypeAnnotation::Record { fields, extension } => { - let fields_len = fields.len(); - - let record_indent = indent_level + 1; - - if is_multiline { - new_line(buf); - indent(buf, record_indent); - } - - buf.push('{'); - - if is_multiline { - new_line(buf); - } - - let next_indent_level = record_indent + 1; - - for (index, field) in fields.iter().enumerate() { - if is_multiline { - indent(buf, next_indent_level); - } else { - buf.push(' '); - } - - let fields_name = match field { - RecordField::RecordField { name, .. } => name, - RecordField::OptionalField { name, .. } => name, - RecordField::LabelOnly { name } => name, - }; - - buf.push_str(fields_name.as_str()); - - match field { - RecordField::RecordField { - type_annotation, .. - } => { - buf.push_str(" : "); - type_annotation_to_html(next_indent_level, buf, type_annotation); - } - RecordField::OptionalField { - type_annotation, .. - } => { - buf.push_str(" ? "); - type_annotation_to_html(next_indent_level, buf, type_annotation); - } - RecordField::LabelOnly { .. } => {} - } - - if is_multiline { - if index < (fields_len - 1) { - buf.push(','); - } - - new_line(buf); - } - } - - if is_multiline { - indent(buf, record_indent); - } else { - buf.push(' '); - } - - buf.push('}'); - - type_annotation_to_html(indent_level, buf, extension); - } - TypeAnnotation::Function { args, output } => { - let mut peekable_args = args.iter().peekable(); - while let Some(arg) = peekable_args.next() { - if is_multiline { - if !should_be_multiline(arg) { - new_line(buf); - } - indent(buf, indent_level + 1); - } - - type_annotation_to_html(indent_level, buf, arg); - - if peekable_args.peek().is_some() { - buf.push_str(", "); - } - } - - if is_multiline { - new_line(buf); - indent(buf, indent_level + 1); - } - - buf.push_str(" -> "); - - let mut next_indent_level = indent_level; - - if should_be_multiline(output) { - next_indent_level += 1; - } - - type_annotation_to_html(next_indent_level, buf, output); - } - TypeAnnotation::Ability { members: _ } => { - // TODO(abilities): fill me in - } - TypeAnnotation::ObscuredTagUnion => { - buf.push_str("[@..]"); - } - TypeAnnotation::ObscuredRecord => { - buf.push_str("{ @.. }"); - } - TypeAnnotation::NoTypeAnn => {} - TypeAnnotation::Wildcard => buf.push('*'), - } -} - -fn should_be_multiline(type_ann: &TypeAnnotation) -> bool { - match type_ann { - TypeAnnotation::TagUnion { tags, extension } => { - let mut is_multiline = should_be_multiline(extension) || tags.len() > 1; - - for tag in tags { - for value in &tag.values { - if is_multiline { - break; - } - is_multiline = should_be_multiline(value); - } - } - - is_multiline - } - TypeAnnotation::Function { args, output } => { - let mut is_multiline = should_be_multiline(output) || args.len() > 2; - - for arg in args { - if is_multiline { - break; - } - - is_multiline = should_be_multiline(arg); - } - - is_multiline - } - TypeAnnotation::ObscuredTagUnion => false, - TypeAnnotation::ObscuredRecord => false, - TypeAnnotation::BoundVariable(_) => false, - TypeAnnotation::Apply { parts, .. } => { - let mut is_multiline = false; - - for part in parts { - is_multiline = should_be_multiline(part); - - if is_multiline { - break; - } - } - - is_multiline - } - TypeAnnotation::Record { fields, extension } => { - let mut is_multiline = should_be_multiline(extension) || fields.len() > 1; - - for field in fields { - if is_multiline { - break; - } - match field { - RecordField::RecordField { - type_annotation, .. - } => is_multiline = should_be_multiline(type_annotation), - RecordField::OptionalField { - type_annotation, .. - } => is_multiline = should_be_multiline(type_annotation), - RecordField::LabelOnly { .. } => {} - } - } - - is_multiline - } - TypeAnnotation::Ability { .. } => true, - TypeAnnotation::Wildcard => false, - TypeAnnotation::NoTypeAnn => false, - } -} - -struct DocUrl { - url: String, - title: String, -} - -fn doc_url<'a>( - home: ModuleId, - exposed_values: &[&str], - dep_idents: &IdentIdsByModule, - scope: &Scope, - interns: &'a Interns, - mut module_name: &'a str, - ident: &str, -) -> DocUrl { - if module_name.is_empty() { - // This is an unqualified lookup, so look for the ident - // in scope! - match scope.lookup_str(ident, Region::zero()) { - Ok(symbol) => { - // Get the exact module_name from scope. It could be the - // current module's name, but it also could be a different - // module - for example, if this is in scope from an - // unqualified import. - module_name = symbol.module_string(interns); - } - Err(_) => { - // TODO return Err here - panic!( - "Tried to generate an automatic link in docs for symbol `{}`, but that symbol was not in scope in this module. Scope was: {:?}", - ident, scope - ); - } - } - } else { - match interns.module_ids.get_id(&module_name.into()) { - Some(&module_id) => { - // You can do qualified lookups on your own module, e.g. - // if I'm in the Foo module, I can do a `Foo.bar` lookup. - if module_id == home { - // Check to see if the value is exposed in this module. - // If it's not exposed, then we can't link to it! - if !exposed_values.contains(&ident) { - // TODO return Err here - panic!( - "Tried to generate an automatic link in docs for `{}.{}`, but `{}` does not expose `{}`.", - module_name, ident, module_name, ident); - } - } else { - // This is not the home module - match dep_idents - .get(&module_id) - .and_then(|exposed_ids| exposed_ids.get_id(&ident.into())) - { - Some(_) => { - // This is a valid symbol for this dependency, - // so proceed using the current module's name. - // - // TODO: In the future, this is where we'll - // incorporate the package name into the link. - } - _ => { - // TODO return Err here - panic!( - "Tried to generate an automatic link in docs for `{}.{}`, but `{}` is not exposed in `{}`.", - module_name, ident, ident, module_name); - } - } - } - } - None => { - // TODO return Err here - panic!("Tried to generate a doc link for `{}.{}` but the `{}` module was not imported!", module_name, ident, module_name); - } - } - } - - let mut url = base_url(); - - // Example: - // - // module_name: "Str", ident: "join" => "/Str#join" - url.push_str(module_name); - url.push('#'); - url.push_str(ident); - - DocUrl { - url, - title: format!("Docs for {}.{}", module_name, ident), - } -} - -fn markdown_to_html( - exposed_values: &[&str], - scope: &Scope, - markdown: String, - loaded_module: &LoadedModule, -) -> String { - use pulldown_cmark::{BrokenLink, CodeBlockKind, CowStr, Event, LinkType, Tag::*}; - - let mut arena = Bump::new(); - let mut broken_link_callback = |link: BrokenLink| { - // A shortcut link - see https://spec.commonmark.org/0.30/#shortcut-reference-link - - // is something like `[foo]` in markdown. If you have a shortcut link - // without a corresponding `[foo]: https://foo.com` entry - // at the end of the document, we resolve it as an identifier based on - // what's currently in scope, so you write things like [Str.join] or - // [myFunction] and have them resolve to the docs for what you wrote. - match link.link_type { - LinkType::Shortcut => { - let state = State::new(link.reference.as_bytes()); - - // Reset the bump arena so we aren't constantly reallocating - // more memory as we iterate through these. - arena.reset(); - - match parse_ident(&arena, state) { - Ok((_, Ident::Access { module_name, parts }, _)) => { - let mut iter = parts.iter(); - - match iter.next() { - Some(symbol_name) if iter.next().is_none() => { - let DocUrl { url, title } = doc_url( - loaded_module.module_id, - exposed_values, - &loaded_module.dep_idents, - scope, - &loaded_module.interns, - module_name, - symbol_name, - ); - - Some((url.into(), title.into())) - } - _ => { - // This had record field access, - // e.g. [foo.bar] - which we - // can't create a doc link to! - None - } - } - } - Ok((_, Ident::Tag(type_name), _)) => { - // This looks like a tag name, but it could - // be a type alias that's in scope, e.g. [I64] - let DocUrl { url, title } = doc_url( - loaded_module.module_id, - exposed_values, - &loaded_module.dep_idents, - scope, - &loaded_module.interns, - "", - type_name, - ); - - Some((url.into(), title.into())) - } - _ => None, - } - } - _ => None, - } - }; - - let markdown_options = pulldown_cmark::Options::empty(); - - let mut expecting_code_block = false; - - let mut docs_parser = vec![]; - let (_, _) = pulldown_cmark::Parser::new_with_broken_link_callback( - &markdown, - markdown_options, - Some(&mut broken_link_callback), - ) - .fold((0, 0), |(start_quote_count, end_quote_count), event| { - - match &event { - // Replace this sequence (`>>>` syntax): - // Start(BlockQuote) - // Start(BlockQuote) - // Start(BlockQuote) - // Start(Paragraph) - // For `Start(CodeBlock(Fenced(Borrowed("roc"))))` - Event::Start(BlockQuote) => { - docs_parser.push(event); - (start_quote_count + 1, 0) - } - Event::Start(Paragraph) => { - if start_quote_count == 3 { - docs_parser.pop(); - docs_parser.pop(); - docs_parser.pop(); - docs_parser.push(Event::Start(CodeBlock(CodeBlockKind::Fenced( - CowStr::Borrowed("roc"), - )))); - } else { - docs_parser.push(event); - } - (0, 0) - } - // Replace this sequence (`>>>` syntax): - // End(Paragraph) - // End(BlockQuote) - // End(BlockQuote) - // End(BlockQuote) - // For `End(CodeBlock(Fenced(Borrowed("roc"))))` - Event::End(Paragraph) => { - docs_parser.push(event); - (0, 1) - } - Event::End(BlockQuote) => { - if end_quote_count == 3 { - docs_parser.pop(); - docs_parser.pop(); - docs_parser.pop(); - docs_parser.push(Event::End(CodeBlock(CodeBlockKind::Fenced( - CowStr::Borrowed("roc"), - )))); - (0, 0) - } else { - docs_parser.push(event); - (0, end_quote_count + 1) - } - } - Event::End(Link(LinkType::ShortcutUnknown, _url, _title)) => { - // Replace the preceding Text node with a Code node, so it - // renders as the equivalent of [`List.len`] instead of [List.len] - match docs_parser.pop() { - Some(Event::Text(string)) => { - docs_parser.push(Event::Code(string)); - } - Some(first) => { - docs_parser.push(first); - } - None => {} - } - - docs_parser.push(event); - - (start_quote_count, end_quote_count) - } - Event::Start(CodeBlock(CodeBlockKind::Fenced(_))) => { - expecting_code_block = true; - docs_parser.push(event); - (0, 0) - } - Event::End(CodeBlock(_)) => { - expecting_code_block = false; - docs_parser.push(event); - (0, 0) - } - Event::Text(CowStr::Borrowed(code_str)) if expecting_code_block => { - - match syntax_highlight_expr( - code_str - ) - { - Ok(highlighted_code_str) => { - docs_parser.push(Event::Html(CowStr::from(highlighted_code_str))); - } - Err(syntax_error) => { - panic!("Unexpected parse failure when parsing this for rendering in docs:\n\n{}\n\nParse error was:\n\n{:?}\n\n", code_str, syntax_error) - } - }; - - (0, 0) - } - _ => { - docs_parser.push(event); - (0, 0) - } - } - }); - - let mut docs_html = String::new(); - - pulldown_cmark::html::push_html(&mut docs_html, docs_parser.into_iter()); - - docs_html -} diff --git a/docs/src/static/favicon.svg b/docs/src/static/favicon.svg deleted file mode 100644 index e0cff74b57..0000000000 --- a/docs/src/static/favicon.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/docs/src/static/index.html b/docs/src/static/index.html deleted file mode 100644 index 8a1ea794e8..0000000000 --- a/docs/src/static/index.html +++ /dev/null @@ -1,48 +0,0 @@ - - - - - - - - - - - - - - - -
- -
-
- -
- -
-
-
- -
-
-

Made by people who like to make nice things.

-

© 2021

-
- - - diff --git a/docs/src/static/search.js b/docs/src/static/search.js deleted file mode 100644 index fc0c04190f..0000000000 --- a/docs/src/static/search.js +++ /dev/null @@ -1,44 +0,0 @@ -(() => { - let sidebar = document.getElementById("sidebar-nav"); - let searchBox = document.getElementById("module-search"); - - function search() { - let text = searchBox.value.toLowerCase(); // Search is case-insensitive. - - if (text === "") { - // Un-hide everything - sidebar.querySelectorAll(".sidebar-entry a").forEach((entry) => entry.classList.remove("hidden")); - - // Re-hide all the sub-entries except for those of the current module - let currentModuleName = document.querySelector('.module-name').textContent; - - sidebar.querySelectorAll(".sidebar-entry").forEach((entry) => { - let entryName = entry.querySelector('.sidebar-module-link').textContent; - if (currentModuleName === entryName) return; - entry.querySelectorAll(".sidebar-sub-entries a").forEach((subEntry) => subEntry.classList.add("hidden")); - }) - } else { - // First, show/hide all the sub-entries within each module (top-level functions etc.) - sidebar.querySelectorAll(".sidebar-sub-entries a").forEach((entry) => { - if (entry.textContent.toLowerCase().includes(text)) { - entry.classList.remove("hidden"); - } else { - entry.classList.add("hidden"); - } - }); - - // Then, show/hide modules based on whether they match, or any of their sub-entries matched - sidebar.querySelectorAll(".sidebar-module-link").forEach((entry) => { - if (entry.textContent.toLowerCase().includes(text) || entry.parentNode.querySelectorAll(".sidebar-sub-entries a:not(.hidden)").length > 0) { - entry.classList.remove("hidden"); - } else { - entry.classList.add("hidden"); - } - }); - } - } - - searchBox.addEventListener("input", search); - - search(); -})(); \ No newline at end of file diff --git a/docs/src/static/styles.css b/docs/src/static/styles.css deleted file mode 100644 index 486fa8972a..0000000000 --- a/docs/src/static/styles.css +++ /dev/null @@ -1,518 +0,0 @@ -:root { - --link-color: #612bde; - --code-link-color: #5721d4; - --text-color: #333333; - --code-color: #222222; - --code-bg-color: #eeeeee; - --body-bg-color: #fdfdfd; - --border-color: #e9e9e9; - --faded-color: #4c4c4c; - --font-sans: -apple-system, BlinkMacSystemFont, Roboto, Helvetica, Arial, sans-serif; - --font-mono: SFMono-Regular, Consolas, "Liberation Mono", Menlo, Courier, monospace; - --top-header-height: 67px; - --sidebar-width: 280px; - --top-bar-bg: #8257e5; - --top-bar-fg: #ffffff; - --nav-link-hover-color: #000000; -} - -a { - color: #972395; -} - -.logo { - padding: 2px 8px; -} - -.logo svg { - height: 48px; - width: 48px; - fill: var(--top-bar-fg); -} - -.logo:hover { - text-decoration: none; -} - -.logo svg:hover { - fill: var(--nav-link-hover-color); -} - -.pkg-full-name { - color: var(--text-color); - display: flex; - align-items: center; - font-size: 32px; - margin: 0 8px; - font-weight: normal; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - height: 100%; -} - -.entry-name { - white-space: pre-wrap; - font-family: var(--font-mono); -} - -.pkg-full-name a { - padding-top: 12px; - padding-bottom: 16px; -} - -a { - text-decoration: none; -} - -a:hover, a:hover code { - text-decoration: underline; -} - -.pkg-and-logo { - min-width: 0; /* necessary for text-overflow: ellipsis to work in descendants */ - display: flex; - align-items: center; - height: 100%; - background-color: var(--top-bar-bg); -} - -.pkg-and-logo a, .pkg-and-logo a:visited { - color: var(--top-bar-fg); -} - -.pkg-and-logo a:hover { - color: var(--nav-link-hover-color); - text-decoration: none; -} - -.search-button { - flex-shrink: 0; /* always shrink the package name before these; they have a relatively constrained length */ - padding: 12px 18px; - margin-right: 42px; - display: none; /* only show this in the mobile view */ -} - -.version { - padding: 18px 10px; - min-width: 48px; - margin-right: 8px; -} - -body { - display: grid; - grid-template-columns: [before-sidebar] 1fr [sidebar] var(--sidebar-width) [main-content] fit-content(calc(1280px - var(--sidebar-width))) [end] 1fr; - grid-template-rows: [top-header] var(--top-header-height) [above-footer] auto [footer] auto; - box-sizing: border-box; - margin: 0; - padding: 0; - font-family: var(--font-sans); - color: var(--text-color); - background-color: var(--body-bg-color); -} - -main { - grid-column-start: main-content; - grid-column-end: main-content; - grid-row-start: above-footer; - grid-row-end: above-footer; - box-sizing: border-box; - position: relative; - font-size: 18px; - line-height: 1.85em; - margin-top: 2px; - padding: 48px; - - min-width: 0; /* necessary for text-overflow: ellipsis to work in descendants */ -} - -#sidebar-nav { - grid-column-start: sidebar; - grid-column-end: sidebar; - grid-row-start: above-footer; - grid-row-end: above-footer; - position: relative; - display: flex; - flex-direction: column; - box-sizing: border-box; - padding-left: 56px; - padding-top: 6px; - width: 100%; -} - -.top-header-extension { - grid-column-start: before-sidebar; - grid-column-end: sidebar; - grid-row-start: top-header; - grid-row-end: top-header; - background-color: var(--top-bar-bg); -} - -.top-header { - grid-column-start: sidebar; - grid-column-end: end; - grid-row-start: top-header; - grid-row-end: top-header; - display: flex; - flex-direction: row; - align-items: center; - flex-wrap: nowrap; - box-sizing: border-box; - font-family: var(--font-sans); - font-size: 24px; - height: 100%; - min-width: 0; /* necessary for text-overflow: ellipsis to work in descendants */ -} - -.top-header-triangle { - /* This used to be a clip-path, but Firefox on Android (at least as of early 2020) - * rendered the page extremely slowly in that version. With this approach it's super fast. - */ - width: 0; - height: 0; - border-style: solid; - border-width: var(--top-header-height) 0 0 48px; - border-color: transparent transparent transparent var(--top-bar-bg); -} - -p { - overflow-wrap: break-word; - margin: 24px 0; -} - -footer { - grid-column-start: main-content; - grid-column-end: main-content; - grid-row-start: footer; - grid-row-end: footer; - max-width: var(--main-content-max-width); - font-size: 14px; - box-sizing: border-box; - padding: 16px; -} - -footer p { - display: inline-block; - margin-top: 0; - margin-bottom: 8px; -} - -.content { - box-sizing: border-box; - display: flex; - flex-direction: row; - justify-content: space-between; -} - -.sidebar-entry ul { - list-style-type: none; - margin: 0; -} - -.sidebar-entry a { - box-sizing: border-box; - min-height: 48px; - min-width: 48px; - padding: 12px 16px; - font-family: var(--font-mono); -} - -.sidebar-sub-entries a { - display: block; - line-height: 24px; - width: 100%; - overflow: hidden; - text-overflow: ellipsis; - padding-left: 36px; -} - -.module-name { - font-size: 56px; - line-height: 1em; - font-family: var(--font-mono); - font-weight: bold; - margin-top: 18px; - margin-bottom: 48px; -} - -.module-name a, .module-name a:visited { - color: inherit; -} - -.sidebar-module-link { - box-sizing: border-box; - font-size: 18px; - line-height: 24px; - font-family: var(--font-mono); - font-weight: bold; - display: block; - width: 100%; - padding: 8px 0; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -a, a:visited { - color: var(--link-color); -} - -h3 { - font-size: 32px; - margin: 48px 0 24px 0; -} - -h4 { - font-size: 24px; -} - -.type-def { - font-size: 24px; - color: var(--link-color); -} - -.code-snippet { - padding: 12px 16px; - display: block; - box-sizing: border-box; - font-family: var(--font-mono); - background-color: var(--code-bg-color); -} - -code { - font-family: var(--font-mono); - color: var(--code-color); - background-color: var(--code-bg-color); - display: inline-block; -} - -code a, a code { - text-decoration: none; - color: var(--code-link-color); - background: none; - padding: 0; -} - -code a:visited, a:visited code { - color: var(--code-link-color); -} - -pre { - margin: 36px 0; - padding: 8px; - box-sizing: border-box; - background-color: var(--code-bg-color); - overflow-x: auto; -} - -.hidden { - /* Use !important to win all specificity fights. */ - display: none !important; -} - -.syntax-number { - color: #60B7BF; -} -.syntax-string { - color:#F7577C; -} - -.syntax-bracket { - color:#FF335F; -} -.syntax-closure-dash, -.syntax-closure-arrow, -.syntax-operator -{ - color: #ffffff; -} -.syntax-comma { - color: #9573E6; -} -.syntax-comment { - color: #ff0000; -} - -#module-search:placeholder-shown { - padding: 0; - opacity: 0; - height: 0; -} - -#module-search, #module-search:focus { - opacity: 1; - padding: 12px 16px; - height: 48px; -} - -/* Show the "Search" label link when the text input has a placeholder */ -#module-search:placeholder-shown + #search-link { - display: flex; -} - -/* Hide the "Search" label link when the text input has focus */ -#module-search:focus + #search-link { - display: none; -} - -#module-search { - display: block; - box-sizing: border-box; - background-color: var(--code-bg-color); - width: 100%; - box-sizing: border-box; - font-size: 18px; - line-height: 18px; - margin-top: 6px; - border: none; - color: var(--faded-color); - background-color: var(--code-bg-color); - font-family: var(--font-serif); -} - -#module-search::placeholder { - color: var(--faded-color); - opacity: 1; -} - -#search-link { - box-sizing: border-box; - display: none; - align-items: center; - font-size: 18px; - line-height: 18px; - padding: 12px 16px; - height: 48px; - cursor: pointer; - color: var(--link-color); -} - -#search-link:hover { - text-decoration: underline; -} - -@media (prefers-color-scheme: dark) { - :root { - --body-bg-color: #303030; - --code-bg-color: #393939; - --border-color: #555555; - --code-color: #eeeeee; - --text-color: #cccccc; - --logo-solid: #777777; - --faded-color: #bbbbbb; - --link-color: #c5a8ff; - --code-link-color: #b894ff; - --top-bar-bg: #6845b9; - --top-bar-fg: #eeeeee; - } - - html { - scrollbar-color: #444444 #2f2f2f; - } -} - -@media only screen and (max-device-width: 480px) and (orientation: portrait) { - .search-button { - display: block; /* This is only visible in mobile. */ - } - - .top-header { - justify-content: space-between; - width: auto; - } - - .pkg-full-name { - margin-left: 8px; - margin-right: 12px; - font-size: 24px; - padding-bottom: 14px; - } - - .pkg-full-name a { - vertical-align: middle; - padding: 18px 0; - } - - .logo { - padding-left: 2px; - width: 50px; - height: 54px; - } - - .version { - margin: 0; - font-weight: normal; - font-size: 18px; - padding-bottom: 16px; - } - - .module-name { - font-size: 36px; - margin-top: 8px; - margin-bottom: 8px; - max-width: calc(100% - 18px); - overflow: hidden; - text-overflow: ellipsis; - } - - main { - grid-column-start: none; - grid-column-end: none; - grid-row-start: above-footer; - grid-row-end: above-footer; - padding: 18px; - font-size: 16px; - } - - #sidebar-nav { - grid-column-start: none; - grid-column-end: none; - grid-row-start: sidebar; - grid-row-end: sidebar; - margin-top: 0; - padding-left: 0; - width: auto; - } - - #sidebar-heading { - font-size: 24px; - margin: 16px; - } - - h3 { - font-size: 18px; - margin: 0; - padding: 0; - } - - h4 { - font-size: 16px; - } - - body { - grid-template-columns: auto; - grid-template-rows: [top-header] var(--top-header-height) [before-sidebar] auto [sidebar] auto [above-footer] auto [footer] auto; -/* [before-sidebar] 1fr [sidebar] var(--sidebar-width) [main-content] fit-content(calc(1280px - var(--sidebar-width))) [end] 1fr; */ - - margin: 0; - min-width: 320px; - max-width: 100%; - } - - .top-header-triangle { - display: none; - } - - .pkg-and-logo { - width: 100%; - } - - .pkg-full-name { - flex-grow: 1; - } - - .pkg-full-name a { - padding-top: 24px; - padding-bottom: 12px; - } -} diff --git a/docs/tests/insert_syntax_highlighting.rs b/docs/tests/insert_syntax_highlighting.rs deleted file mode 100644 index e3ecc67195..0000000000 --- a/docs/tests/insert_syntax_highlighting.rs +++ /dev/null @@ -1,168 +0,0 @@ -#[macro_use] -extern crate pretty_assertions; -// Keep this around until the commented out tests can be enabled again. -/*#[macro_use] -extern crate indoc;*/ - -#[cfg(test)] -mod insert_doc_syntax_highlighting { - - use roc_docs::{syntax_highlight_expr, syntax_highlight_top_level_defs}; - - fn expect_html(code_str: &str, want: &str, use_expr: bool) { - if use_expr { - match syntax_highlight_expr(code_str) { - Ok(highlighted_code_str) => { - assert_eq!(highlighted_code_str, want); - } - Err(syntax_error) => { - panic!("Unexpected parse failure when parsing this for rendering in docs:\n\n{}\n\nParse error was:\n\n{:?}\n\n", code_str, syntax_error) - } - }; - } else { - match syntax_highlight_top_level_defs(code_str) { - Ok(highlighted_code_str) => { - assert_eq!(highlighted_code_str, want); - } - Err(syntax_error) => { - panic!("Unexpected parse failure when parsing this for rendering in docs:\n\n{}\n\nParse error was:\n\n{:?}\n\n", code_str, syntax_error) - } - }; - } - } - - fn expect_html_expr(code_str: &str, want: &str) { - expect_html(code_str, want, true) - } - - fn expect_html_def(code_str: &str, want: &str) { - expect_html(code_str, want, false) - } - - #[test] - fn number_expr() { - expect_html_expr("2", r#"2"#); - } - - // These tests have been commented out due to introduction of a new syntax highlighting approach. - // You can make these tests work by following the instructions at the top of this file here: roc/highlight/src/highlight_parser.rs - /*#[test] - fn string_expr() { - expect_html_expr(r#""abc""#, r#""abc""#); - } - - #[test] - fn empty_list_expr() { - expect_html_expr( - r#"[]"#, - r#"[ ]"#, - ); - } - - #[test] - fn single_elt_list_expr() { - expect_html_expr( - r#"[ 0 ]"#, - r#"[ 0 ]"#, - ); - } - - #[test] - fn multi_elt_list_expr() { - expect_html_expr( - r#"[ "hello", "WoRlD" ]"#, - r#"[ "hello", "WoRlD" ]"#, - ); - } - - #[test] - fn record_expr() { - expect_html_expr( - r#"{ a: "hello!" }"#, - "{ a: \"hello!\" }", - ); - } - - #[test] - fn nested_record_expr() { - expect_html_expr( - r#"{ a: { bB: "WoRlD" } }"#, - "{ a: { bB: \"WoRlD\" } }", - ); - }*/ - - #[test] - fn top_level_def_val_num() { - expect_html_def( - r#"myVal = 0"#, - "myVal = 0\n\n", - ); - } - - /*#[test] - fn top_level_def_val_str() { - expect_html_def( - r#"myVal = "Hello, World!""#, - "myVal = \"Hello, World!\"\n\n\n", - ); - } - - #[test] - fn tld_newline_in_str() { - expect_html_def( - r#"myVal = "Hello, Newline!\n""#, - "myVal = \"Hello, Newline!\n\"\n\n\n", - ); - } - - #[test] - fn tld_list() { - expect_html_def( - r#"myVal = [ 1, 2, 3 ]"#, - "myVal = [ 1, 2, 3 ]\n\n\n", - ); - } - - #[test] - fn call_builtin() { - expect_html_def( - r#"myVal = Num.toStr 1234"#, - "myVal = Num.toStr 1234\n\n\n", - ); - } - - #[test] - fn function() { - expect_html_def( - r#"myId = \something -> - something"#, - "myId = \\something -> \n something\n\n\n", - ); - } - - #[test] - fn tld_with_comment_before() { - expect_html_def( - indoc!( - r#" - # COMMENT - myVal = "Hello, World!" - "#, - ), - "# COMMENT\nmyVal = \"Hello, World!\"\n\n\n\n\n", - ); - }*/ - - // TODO see issue #2134 - /*#[test] - fn tld_with_comment_after() { - expect_html_def( - indoc!( - r#" - myVal = "Hello, World!" # COMMENT - "#, - ), - "myVal = \"Hello, World!\"# COMMENT\n\n\n\n", - ); - }*/ -} diff --git a/docs_cli/Cargo.toml b/docs_cli/Cargo.toml deleted file mode 100644 index 96600f1f17..0000000000 --- a/docs_cli/Cargo.toml +++ /dev/null @@ -1,21 +0,0 @@ -[package] -name = "roc_docs_cli" -version = "0.1.0" -license = "UPL-1.0" -authors = ["The Roc Contributors"] -edition = "2021" - -# This binary is only used on static build servers, e.g. Netlify. -# Having its own (extremely minimal) CLI means docs can be generated -# on a build server after building this crate from source, without -# having to install non-Rust dependencies (LLVM, Zig, wasm things, etc.) -# It gets called in www/build.sh via `cargo run --bin roc-docs` -[[bin]] -name = "roc-docs" -path = "src/main.rs" -test = false -bench = false - -[dependencies] -roc_docs = { path = "../docs" } -clap = { version = "3.1.15", default-features = false, features = ["std", "color", "suggestions", "derive"] } diff --git a/docs_cli/src/main.rs b/docs_cli/src/main.rs deleted file mode 100644 index 26ac6abcda..0000000000 --- a/docs_cli/src/main.rs +++ /dev/null @@ -1,50 +0,0 @@ -use clap::{Arg, Command}; -use roc_docs::generate_docs_html; -use std::fs::{self, FileType}; -use std::io; -use std::path::{Path, PathBuf}; - -pub const DIRECTORY_OR_FILES: &str = "DIRECTORY_OR_FILES"; - -fn main() -> io::Result<()> { - let matches = Command::new("roc-docs") - .about("Build HTML documentation files from the given .roc files") - .arg( - Arg::new(DIRECTORY_OR_FILES) - .multiple_values(true) - .required(true) - .help("The directory or files to build documentation for") - .allow_invalid_utf8(true), - ) - .get_matches(); - - let mut roc_files = Vec::new(); - - // Populate roc_files - for os_str in matches.values_of_os(DIRECTORY_OR_FILES).unwrap() { - let metadata = fs::metadata(os_str)?; - roc_files_recursive(os_str, metadata.file_type(), &mut roc_files)?; - } - - generate_docs_html(roc_files); - - Ok(()) -} - -fn roc_files_recursive>( - path: P, - file_type: FileType, - roc_files: &mut Vec, -) -> io::Result<()> { - if file_type.is_dir() { - for entry_res in fs::read_dir(path)? { - let entry = entry_res?; - - roc_files_recursive(entry.path(), entry.file_type()?, roc_files)?; - } - } else { - roc_files.push(path.as_ref().to_path_buf()); - } - - Ok(()) -} diff --git a/editor/.gitignore b/editor/.gitignore deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/editor/Cargo.toml b/editor/Cargo.toml deleted file mode 100644 index ef6058bff5..0000000000 --- a/editor/Cargo.toml +++ /dev/null @@ -1,70 +0,0 @@ -[package] -name = "roc_editor" -version = "0.1.0" -authors = ["The Roc Contributors"] -license = "UPL-1.0" -edition = "2021" -description = "An editor for Roc" - -[package.metadata.cargo-udeps.ignore] -# confy is currently unused but should not be removed -normal = ["confy"] -#development = [] -#build = [] - -[features] -default = [] -with_sound = ["rodio"] - -[dependencies] -roc_ast = { path = "../ast" } -roc_collections = { path = "../compiler/collections" } -roc_load = { path = "../compiler/load" } -roc_builtins = { path = "../compiler/builtins" } -roc_can = { path = "../compiler/can" } -roc_code_markup = { path = "../code_markup"} -roc_parse = { path = "../compiler/parse" } -roc_region = { path = "../compiler/region" } -roc_module = { path = "../compiler/module" } -roc_problem = { path = "../compiler/problem" } -roc_types = { path = "../compiler/types" } -roc_unify = { path = "../compiler/unify" } -roc_reporting = { path = "../reporting" } -roc_solve = { path = "../compiler/solve" } -ven_graph = { path = "../vendor/pathfinding" } -bumpalo = { version = "3.8.0", features = ["collections"] } -arrayvec = "0.7.2" -libc = "0.2.106" -page_size = "0.4.2" -# once winit 0.26 is out, check if copypasta can be updated simultaneously so they use the same versions for their dependencies. This will save build time. -winit = "0.25.0" -wgpu = "0.11.0" -wgpu_glyph = "0.15.1" -glyph_brush = "0.7.2" -log = "0.4.14" -env_logger = "0.9.0" -futures = "0.3.17" -cgmath = "0.18.0" -snafu = { version = "0.6.10", features = ["backtraces"] } -colored = "2.0.0" -pest = "2.1.3" -pest_derive = "2.1.0" -copypasta = "0.7.1" -palette = "0.6.0" -confy = { git = 'https://github.com/rust-cli/confy', features = [ - "yaml_conf" -], default-features = false } -serde = { version = "1.0.130", features = ["derive"] } -nonempty = "0.7.0" -fs_extra = "1.2.0" -rodio = { version = "0.14.0", optional = true } # to play sounds -threadpool = "1.8.1" - -[dependencies.bytemuck] -version = "1.7.2" -features = ["derive"] - -[dev-dependencies] -rand = "0.8.4" -tempfile = "3.2.0" -uuid = { version = "0.8.2", features = ["v4"] } diff --git a/editor/README.md b/editor/README.md deleted file mode 100644 index e462661928..0000000000 --- a/editor/README.md +++ /dev/null @@ -1,78 +0,0 @@ - - -## :construction: Work In Progress :construction: - -The editor is a work in progress, only a limited subset of Roc expressions are currently supported. - -Unlike most editors, we use projectional or structural editing to edit the [Abstract Syntax Tree](https://en.wikipedia.org/wiki/Abstract_syntax_tree) directly. This will allow for cool features like excellent auto-complete, refactoring and never needing to format your code. - -## Getting started - -- Install the compiler, see [here](../BUILDING_FROM_SOURCE.md). -- Run the following from the roc folder: - -``` -cargo run edit -``` - -## Troubleshooting - -If you encounter problems with integrated graphics hardware, install `mesa-vulkan-drivers` and `vulkan-tools`. - -If you encounter an error like `gfx_backend_vulkan ... Failed to detect any valid GPUs in the current config ...` make sure the correct graphics card drivers are installed. On ubuntu `sudo ubuntu-drivers autoinstall` can resolve the problem. -If the error persists, take a look [here](https://www.techpowerup.com/gpu-specs/) to see if your GPU supports vulkan. -Use of OpenGL instead of vulkan should be available in several months. - -Make sure to [create an issue](https://github.com/rtfeldman/roc/issues/new/choose) if you encounter any problems not listed above. - -## Inspiration - -We thank the following open source projects in particular for inspiring us when designing the Roc editor: -- [learn-wgpu](https://github.com/sotrh/learn-wgpu) -- [rgx](https://github.com/cloudhead/rgx) -- [elm-editor](https://github.com/jxxcarlson/elm-editor) -- [iced](https://github.com/hecrj/iced) - -## How does it work? - -To take a look behind the scenes, open the editor with `./roc edit` or `cargo run edit` and press F11. -This debug view shows important data structures that can be found in `editor/src/editor/mvc/ed_model.rs`. -Add or delete some code to see how these data structures are updated. - -From roc to render: -- `./roc edit` or `cargo run edit` is first handled in `cli/src/main.rs`, from there the editor's launch function is called (`editor/src/editor/main.rs`). -- In `run_event_loop` we initialize the winit window, wgpu, the editor's model(`EdModel`) and start the rendering loop. -- The `ed_model` is filled in part with data obtained by loading and typechecking the roc file with the same function (`load_and_typecheck`) that is used by the compiler. -- `ed_model` also contains an `EdModule`, which holds the parsed abstract syntax tree (AST). -- In the `init_model` function: - + The AST is converted into a tree of `MarkupNode`. The different types of `MarkupNode` are similar to the elements/nodes in HTML. A line of roc code is represented as a nested `MarkupNode` containing mostly text `MarkupNode`s. The line `foo = "bar"` is represented as - three text `MarkupNode`; representing `foo`, ` = ` and `bar`. Multiple lines of roc code are represented as nested `MarkupNode` that contain other nested `MarkupNode`. - + `CodeLines` holds a `Vec` of `String`, each line of code is a `String`. When saving the file, the content of `CodeLines` is written to disk. - + `GridNodeMap` maps every position of a char of roc code to a `MarkNodeId`, for easy interaction with the caret. -- Back in `editor/src/editor/main.rs` we convert the `EdModel` to `RenderedWgpu` by calling `model_to_wgpu`. -- The `RenderedWgpu` is passed to the `glyph_brush` to draw the characters(glyphs) on the screen. - - -### Important files - -To understand how the editor works it is useful to know the most important files: -- editor/src/editor/main.rs -- editor/src/editor/mvc/ed_update.rs -- editor/src/editor/mvc/ed_model.rs -- editor/src/editor/mvc/ed_view.rs -- editor/src/editor/render_ast.rs -- editor/src/editor/render_debug.rs - -Important folders/files outside the editor folder: -- code_markup/src/markup/convert -- code_markup/src/markup/nodes.rs -- ast/src/lang/core/def -- ast/src/lang/core/expr -- ast/src/lang/core/ast.rs -- ast/src/lang/env.rs - - -## Contributing - -We welcome new contributors :heart: and are happy to help you get started. -Check [CONTRIBUTING.md](../CONTRIBUTING.md) for more info. diff --git a/editor/creature.png b/editor/creature.png deleted file mode 100644 index f0fd87e6b0..0000000000 Binary files a/editor/creature.png and /dev/null differ diff --git a/editor/editor-ideas.md b/editor/editor-ideas.md deleted file mode 100644 index 4a227d6c96..0000000000 --- a/editor/editor-ideas.md +++ /dev/null @@ -1,394 +0,0 @@ -(For background, [this talk](https://youtu.be/ZnYa99QoznE?t=4790) has an overview of the design goals for the editor.) - -# Editor Ideas - -Here are some ideas and interesting resources for the editor. Feel free to make a PR to add more! - -## Sources of Potential Inspiration - -These are potentially inspirational resources for the editor's design. - -Nice collection of research on innovative editors, [link](https://futureofcoding.org/catalog/). - -### Package-specific editor integrations - -(Or possibly module-specific integrations, type-specific integrations, etc.) - -* [What FP can learn from Smalltalk](https://youtu.be/baxtyeFVn3w) by [Aditya Siram](https://github.com/deech) -* [Moldable development](https://youtu.be/Pot9GnHFOVU) by [Tudor Gîrba](https://github.com/girba) -* [Unity game engine](https://unity.com/) - * Scripts can expose values as text inputs, sliders, checkboxes, etc or even generate custom graphical inputs - * Drag-n-drop game objects and component into script interfaces -* [How to Visualize Data Structures in VS Code](https://addyosmani.com/blog/visualize-data-structures-vscode/) - -### Live Interactivity - -* [Up and Down the Ladder of Abstraction](http://worrydream.com/LadderOfAbstraction/) by [Bret Victor](http://worrydream.com/) -* [7 Bret Victor talks](https://www.youtube.com/watch?v=PUv66718DII&list=PLS4RYH2XfpAmswi1WDU6lwwggruEZrlPH) -* [Against the Current](https://youtu.be/WT2CMS0MxJ0) by [Chris Granger](https://github.com/ibdknox/) -* [Sketch-n-Sketch: Interactive SVG Programming with Direct Manipulation](https://youtu.be/YuGVC8VqXz0) by [Ravi Chugh](http://people.cs.uchicago.edu/~rchugh/) -* [Xi](https://xi-editor.io/) modern text editor with concurrent editing (related to [Druid](https://github.com/linebender/druid)) -* [Self](https://selflanguage.org/) programming language -* [Primitive](https://primitive.io/) code exploration in Virtual Reality -* [Luna](https://www.luna-lang.org/) language for interactive data processing and visualization -* [Hazel Livelits](https://hazel.org/papers/livelits-paper.pdf) interactive plugins, see GIF's [here](https://twitter.com/disconcision/status/1408155781120376833). -* [Thorough review](https://drossbucket.com/2021/06/30/hacker-news-folk-wisdom-on-visual-programming/) of pros and cons of text versus visual programming. - -### Good error messages - -* [https://twitter.com/firstdrafthell/status/1427364851593224197/photo/1] very clean error message layout -* If the user explicitly allows it, we can keep record of which errors take a long time to fix. This way we know where to focus our efforts for improving error messages. - -### Debugging - -* [VS code debug visualization](https://marketplace.visualstudio.com/items?itemName=hediet.debug-visualizer) -* [Algorithm visualization for javascript](https://algorithm-visualizer.org) -* [godbolt.org Compiler Explorer](https://godbolt.org/) -* [whitebox debug visualization](https://vimeo.com/483795097) -* [Hest](https://ivanish.ca/hest-time-travel/) tool for making highly interactive simulations. -* [replit](https://replit.com/) collaborative browser based IDE. -* [paper](https://openreview.net/pdf?id=SJeqs6EFvB) on finding and fixing bugs automatically. -* [specialized editors that can be embedded in main editor](https://elliot.website/editor/) -* Say you have a failing test that used to work, it would be very valuable to see all code that was changed that was used only by that test. -e.g. you have a test `calculate_sum_test` that only uses the function `add`, when the test fails you should be able to see a diff showing only what changed for the function `add`. It would also be great to have a diff of [expression values](https://homepages.cwi.nl/~storm/livelit/images/bret.png) Bret Victor style. An ambitious project would be to suggest or automatically try fixes based on these diffs. -* I think it could be possible to create a minimal reproduction of a program / block of code / code used by a single test. So for a failing unit test I would expect it to extract imports, the platform, types and functions that are necessary to run only that unit test and put them in a standalone roc project. This would be useful for sharing bugs with library+application authors and colleagues, for profiling or debugging with all "clutter" removed. -* Ability to share program state at a breakpoint with someone else. -* For debugging we should aim for maximal useful observability. For example Rust's enum values can not be easily viewed in the CodeLLDB debugger, you actually need to call a print method that does pattern matching to be able to view useful information. -* We previuously discussed recording full traces of programs so they do not have to be re-run multiple times in the debugging process. We should encourage roc developers to experiment with creating debugging representations of this AST+"execution trace", it could lead to some cool stuff. -* We previuously mentioned showing expression values next to the code. I think when debugging it would be valuable to focus more on these valuas/data. A possible way to do this would be to create scrollable view(without need to jump between files) of inputs and outputs of user defined functions. Clicking on a function could then show the code with the expression values side by side. Having a good overview of how the values change could make it easy to find where exactly things go wrong. -- (Machine learning) algorithms to extract and show useful information from debug values. -- Ability to mark e.g. a specific record field for tracking(filter out the noise) that is being repeatedly updated throughout the program. -- Ability to collapse/fold debug output coming from specific line. -- search bar to search through printed logs -- Turn an error listed in the console into editable section of code for easy quick fixing. -- Clickable backtrace of functions, user defined functions should be made extra visible. -- VR debugging: render massive curved screen with rectangle showing code (and expression values) for every function in call stack. - -### Cool regular editors - -* [Helix](https://github.com/helix-editor/helix) modal (terminal, for now) editor in rust. Good UX. -* [Kakoune](https://kakoune.org/) editor with advanced text selection and manipulation features. - - -### Structured Editing - -* [Greenfoot](https://www.youtube.com/watch?v=uUVA7nTh0XY) -* [Deuce](http://ravichugh.github.io/sketch-n-sketch/) (videos on the right) by [Ravi Chugh](http://people.cs.uchicago.edu/~rchugh/) and others -* [Fructure: A Structured Editing Engine in Racket](https://youtu.be/CnbVCNIh1NA) by Andrew Blinn -* [Hazel: A Live FP Environment with Typed Holes](https://youtu.be/UkDSL0U9ndQ) by [Cyrus Omar](https://web.eecs.umich.edu/~comar/) -* [Dark Demo](https://youtu.be/QgimI2SnpTQ) by [Ellen Chisa](https://twitter.com/ellenchisa) -* [Introduction to JetBrains MPS](https://youtu.be/JoyzxjgVlQw) by [Kolja Dummann](https://www.youtube.com/channel/UCq_mWDvKdXYJJzBmXkci17w) -* [Eve](http://witheve.com/) - * code editor as prose writer - * live preview - * possible inspiration for live interactivity as well -* [Unreal Engine 4](https://www.unrealengine.com/en-US/) - * [Blueprints](https://docs.unrealengine.com/en-US/Engine/Blueprints/index.html) visual scripting (not suggesting visual scripting for Roc) - -* [Live Programing](https://www.microsoft.com/en-us/research/project/live-programming/?from=http%3A%2F%2Fresearch.microsoft.com%2Fen-us%2Fprojects%2Fliveprogramming%2Ftypography.aspx#!publications) by [Microsoft Research] it contains many interesting research papers. -* [Math Inspector](https://mathinspector.com/), [github](https://github.com/MathInspector/MathInspector) -* [Lamdu](http://www.lamdu.org/) live functional programming. -* [Sourcetrail](https://www.sourcetrail.com/) nice tree-like source explorer. -* [Unisonweb](https://www.unisonweb.org), definition based [editor](https://twitter.com/shojberg/status/1364666092598288385) as opposed to file based. -* [Utopia](https://utopia.app/) integrated design and development environment for React. Design and code update each other, in real time. -* [Paredit](https://calva.io/paredit/) structural clojure editing, navigation and selection. [Another overview](http://danmidwood.com/content/2014/11/21/animated-paredit.html) - -### Project exploration - -* Tree view or circle view (like Github Next) of project where exposed values and functions can be seen on hover. - -#### Inspiration - -* [Github Next](https://next.github.com/projects/repo-visualization) each file and folder is visualised as a circle: the circle’s color is the type of file, and the circle’s size represents the size of the file. Sidenote, a cool addition to this might be to use heatmap colors for the circles; circles for files that have had lots of commits could be more red, files with few commits would be blue. - -### Voice Interaction Related - -* We should label as many things as possible and expose jumps to those labels as shortkeys. -* Update without user interaction. e.g. autosave. -* Could be efficient way to communicate with smart assistant. -* You don't have to remember complex keyboard shortcuts if you can describe actions to execute them. Examples: - * Add latest datetime package to dependencies. - * Generate unit test for this function. - * Show edit history for this function. - * Adjusting settings: switch to light theme, increase font size... -* Use (context specific) voice command state machine to assist Machine Learning voice recognition model. -* Nice special use case: using voice to code while on treadmill desk. -* Use word embeddings to find most similar voice command to recorded input in vector space. - -#### Useful voice commands - -* clear all breakpoints -* increase/decrease font size -* switch to dark/light/high-contrast mode -* open/go to file "Main"(fuzzy matching) -* go to function "foo" -* go to definition -* show all references(uses) of this function/type/... -* show history timeline of this function/file -* show recent projects -* generate unit test for this function -* generate unit test for this function based on debug trace (input and output is recorded and used in test) -* who wrote this line (git blame integration) -* search documentation of library X for Foo -* show example of how to use library function Foo -* open google/github/duckduckgo search for error... -* show editor plugins for library X -* commands to control log filtering -* collaps all arms of when -* "complex" filtered search: search for all occurrences of `"#` but ignore all like `"#,` -* color this debug print orange -* remove unused imports - -#### Inspiration - -* Voice control and eye tracking with [Talon](https://github.com/Gauteab/talon-tree-sitter-service) -* [Seminar about programming by voice](https://www.youtube.com/watch?v=G8B71MbA9u4) -* [Talon voice commands in elm](https://github.com/Gauteab/talon-tree-sitter-service) -* Mozilla DeepSpeech model runs fast, works pretty well for actions but would need additional training for code input. - Possible to reuse [Mozilla common voice](https://github.com/common-voice/common-voice) for creating more "spoken code" data. -* [Voice Attack](https://voiceattack.com/) voice recognition for apps and games. - -### Beginner-focused Features - - * Show Roc cheat sheet on start-up. - * Plugin that translates short pieces of code from another programming language to Roc. [Relevant research](https://www.youtube.com/watch?v=xTzFJIknh7E). Someone who only knows the R language could get started with Roc with less friction if they could quickly define a list R style (`lst <- c(1,2,3)`) and get it translated to Roc. - * Being able to asses or ask the user for the amount of experience they have with Roc would be a valuable feature for recommending plugins, editor tips, recommending tutorials, automated error search (e.g searching common beginner errors first), ... . -* Adjust UI based on beginner/novice/expert? - -### Productivity features - -* When refactoring; - - Cutting and pasting code to a new file should automatically add imports to the new file and delete them from the old file. - - Ability to link e.g. variable name in comments to actual variable name. Comment is automatically updated when variable name is changed. - - When updating dependencies with breaking changes; show similar diffs from github projects that have successfully updated that dependency. - - AST backed renaming, changing variable/function/type name should change it all over the codebase. -* Automatically create all "arms" when pattern matching after entering `when var is` based on the type. - - All `when ... is` should be updated if the type is changed, e.g. adding Indigo to the Color type should add an arm everywhere where `when color is` is used. -* When a function is called like `foo(false)`, the name of the boolean argument should be shown automatically; `foo(`*is_active:*`false)`. This should be done for booleans and numbers. -* Suggest automatically creating a function if the compiler says it does not exist. -* Integrated search: - * Searchbar for examples/docs. With permission search strings could be shared with the platform/package authors so they know exactly what their users are struggling with. -* Show productivity/feature tips on startup. Show link to page with all tips. Allow not seeing tips next time. -* Search friendly editor docs inside the editor. Offer to send search string to Roc maintainers when no results, or if no results were clicked. -* File history timeline view. Show timeline with commits that changed this file, the number of lines added and deleted as well as which user made the changes. Arrow navigation should allow you to quickly view different versions of the file. -* Suggested quick fixes should be directly visible and clickable. Not like in vs code where you put the caret on an error until a lightbulb appears in the margin which you have to click for the fixes to appear, after which you click to apply the fix you want :( . You should be able to apply suggestions in rapid succession. e.g. if you copy some roc code from the internet you should be able to apply 5 import suggestions quickly. -* Regex-like find and substitution based on plain english description and example (replacement). i.e. replace all `[` between double quotes with `{`. [Inspiration](https://alexmoltzau.medium.com/english-to-regex-thanks-to-gpt-3-13f03b68236e). -* Show productivity tips based on behavior. i.e. if the user is scrolling through the error bar and clicking on the next error several times, show a tip with "go to next error" shortcut. -* Command to "benchmark this function" or "benchmark this test" with flamegraph and execution time per line. -* Instead of going to definition and having to navigate back and forth between files, show an editable view inside the current file. See [this video](https://www.youtube.com/watch?v=EenznqbW5w8) -* When encountering an unexpected error in the user's program we show a button at the bottom to start an automated search on this error. The search would: - * look for similar errors in github issues of the relevant libraries - * search stackoverflow questions - * search a local history of previously encountered errors and fixes - * search through a database of our zullip questions - * ... -* smart insert: press a shortcut and enter a plain english description of a code snippet you need. Examples: "convert string to list of chars", "sort list of records by field foo descending", "plot this list with date on x-axis"... -* After the user has refactored code to be simpler, try finding other places in the code base where the same simplification can be made. -* Show most commonly changed settings on first run so new users can quickly customize their experience. Keeping record of changed settings should be opt-in. -* Detection of multiple people within same company/team working on same code at the same time (opt-in). -* Autocorrect likely typos for stuff like `-<` when not in string. -* If multiple functions are available for import, use function were types would match in insetion position. -* Recommend imports based on imports in other files in same project. -* Machine Learning model to determine confidence in a possiblte auto import. Automatically add the importt if confidence is very high. -* Ability to print logs in different color depending on which file they come from. -* Clicking on a log print should take you to the exact line of code that called the log function -* When detecting that the user is repeating a transformation such as replacing a string in a text manually, offer to do the replacement for all occurrences in this string/function/file/workspace. -* Auto remove unused imports? Perhaps save the removed imports on a scratchpad for easy re-enabling. -* It should be easy to toggle search and replace to apply to the whole project. -* Taking into account the eye position with eye tracking could make commands very powerful/accurate. e.g.: make `Num *` a `List (Num *)`, use eye position to determine which `Num *`. -* Feature to automatically minimize visibility(exposing values/functions/...) based on usage in tests. Suggested changes can be shown to the user for fine-grained control. -* Locally record file/function navigation behavior to offer suggestions where to navigate next. With user permission, this navigation behavior can be shared with their team so that e.g. new members get offered useful suggestions on navigating to the next relevant file. - * Intelligent search: "search this folder for ", "search all tests for " -* Show some kind of warning if path str in code does not exist locally. -* repl on panic/error: ability to inspect all values and try executing some things at the location of the error. -* show values in memory on panic/error -* automatic clustering of (text) search results in groups by similarity -* fill screen with little windows of clustered search results -* clustering of examples similar to current code -* ability to easily screenshot a subwindow -> create static duplicate of subwindow -* Show references is a common editor feature, often I only want to see non-recursive references in the case of a recursive function. -* ability to add error you were stuck on but have now solved to error database, to help others in the future. -* For quick navigation and good overview: whole file should be shown as folded tree showing only top level defs. Hovering with mouse should allow you to show and traverse the branches, with a click to keep this view. See also ginkowriter. -* clicking on any output should take you to the place in the code where that output was printed and/or calculated. -* ability to edit printed output in such a way that the appropriate changes are made in the code that produced it. Example: edit json key in output-> code is changed to print this new key. - -#### Autocomplete - -- Use more space for autocomplete options: - * Multiple columns. Columns could have different sources, i.e. middle column suggests based on current folder, left column on whole project, right column on github. - * show cell with completion + auto import suggestion -- Webcam based eye tracking for quick selection. -- Machine Learning: - * GPT-3 can generate correct python functions based on a comment describing the functionality, video [here](https://www.youtube.com/watch?v=utuz7wBGjKM). It's possible that training a model using ast's may lead to better results than text based models. -- Current autocomplete lacks flow, moving through suggestions with arrows is slow. Being able to code by weaving together autocomplete suggestions laid out in rows using eye tracking, that could flow. -- It's possible that with strong static types, pure functions and a good search algorithm we can develop a more reliable autocomplete than one with machine learning. -- When ranking autocomplete suggestions, take into account how new a function is. Newly created functions are likely to be used soon. - -#### Productivity Inspiration - -* [Kite](https://www.kite.com/) AI autocomplete and doc viewer. -* [Tabnine](https://www.tabnine.com/) AI autocomplete. -* [Codota](https://www.codota.com) AI autocomplete and example searching. -* [Github copilot](https://copilot.github.com/) AI autocomplete. -* [Aroma](https://ai.facebook.com/blog/aroma-ml-for-code-recommendation) showing examples similar to current code. -* [MISM](https://arxiv.org/abs/2006.05265) neural network based code similarity scoring. -* [Inquisitive code editor](https://web.eecs.utk.edu/~azh/blog/inquisitivecodeeditor.html) Interactive bug detection with doc+test generation. -* [NextJournal](https://nextjournal.com/joe-loco/command-bar?token=DpU6ewNQnLhYtVkwhs9GeX) Discoverable commands and shortcuts. -* [Code Ribbon](https://web.eecs.utk.edu/~azh/blog/coderibbon.html) fast navigation between files. Feature suggestion: top and down are filled with suggested files, whereas left and right are manually filled. -* [Automatic data transformation based on examples](https://youtu.be/Ej91F1fpmEw). Feature suggestion: use in combination with voice commands: e.g. "only keep time from list of datetimes". -* [Codesee](https://www.codesee.io/) code base visualization. -* [Loopy](https://dl.acm.org/doi/10.1145/3485530?sid=SCITRUS) interactive program synthesis. -* [bracket guides](https://mobile.twitter.com/elyktrix/status/1461380028609048576) - -### Non-Code Related Inspiration - -* [Scrivner](https://www.literatureandlatte.com/scrivener/overview) writing app for novelists, screenwriters, and more -* Word processors (Word, Google Docs, etc) - * Comments that are parallel to the text of the document. - * Comments can act as discussions and not just statements. - * Easy tooling around adding tables and other stylised text -* Excel and Google Sheets - * Not sure, maybe something they do well that we (code editors) could learn from - - -## Machine Learning Ideas - -* Ability to record all changes to abstract syntax tree with user permission. - * I think it is possible to create powerful automatic error resolution by having a dataset available of ast's with a specific error and the subsequent transformation that fixed the error. - * Users with large private code bases could (re)train a publicly available error recovery model to experience benefits without having to share their code. - * It could be useful to a user who is creating a function to show them the most similar function (type signature, name, comment) in a public+their private database. Say I was using a web framework and I just created a function that has a multipart form as argument, it would be great to have an example instantly available. - * A simpler start for this idea without user data gathering: how the user a code snippet that is most similar to what they are currently writing. Snippets can be aggregated from examples, tests, docstrings at zero cost to the package/platform authors. - * See [codata](https://www.codota.com/code/java/classes/okhttp3.OkHttpClient) for inspiration on a snippet/example finder. -* Fuzzy natural language based setting adjustment in search bar or with voice input: increase font size, enable autosave, switch to light theme... -* Detect deviation of best practices, example case: alert developer when they are defining a color inline (rgb(30,30,30)) while all colors have been previously imported from a single file. See also [Codota](https://www.codota.com). -* It would be valuable to record the user's interactions with the editor when debugging as well as the AST. On enough data we could train a model to perform a bunch of debugging steps and show values of the most important variables in relation to the bug. Having assistance in finding the exact code that causes the problem could be super valuable. There could be sensitive data, so it should only be recorded and or shared for open source codebases with permissive licenses and with explicit user permission. -* To allow for more privacy; data gathering can be kept only local or only shared within a team/company. Say we offer the ability to save the changes made after an error occurred. Another developer in the company who encounters this error could be notified someone has previously encountered this error along with their changes made after the error. Optionally, the first developer's name can be shown (only within team/company) so the second developer can quickly ask for help. - - -## Testing - -* From Google Docs' comments, adding tests in a similar manner, where they exists in the same "document" but parallel to the code being written - * Makes sense for unit tests, keeps the test close to the source - * Doesn't necessarily make sense for integration or e2e testing - * Maybe easier to manually trigger a test related to exactly what code you're writing -* Ability to generate unit tests for a selected function in context menu - * A table should appear to enter input and expected output pairs quickly -* Ability to "record" unit tests - * Select a function to record. - * Do a normal run, and save the input and output of the selected function. - * Generate a unit test with that input-output pair -* [vitest](https://twitter.com/antfu7/status/1468233216939245579) only run tests that could possibly have changed (because the code they test/use has changed) -* Ability to show in sidebar if code is tested by a test. Clicking on the test in the sidebar should bring you to that test. - -### Inspiration - -* [Haskell language server plugin](https://github.com/haskell/haskell-language-server/blob/master/plugins/hls-eval-plugin/README.md) evaluate code in comments, to test and document functions and to quickly evaluate small expressions. -* [Hazel live test](https://mobile.twitter.com/disconcision/status/1459933500656730112) - -## Documentation - -* Ability to see module as it would be presented on a package website. - * Modern editors may guide developers to the source code too easily. - The API and documentation are meant to interface with humans. -* [DocC](https://developer.apple.com/videos/play/wwdc2021/10166/) neat documentation approach for swift. -* Make it easy to ask for/add examples and suggest improvements to a project's docs. -* Library should have cheat sheet with most used/important docs summarized. -* With explicit user permission, anonymously track viewing statistics for documentation. Can be used to show most important documentation, report pain points to library authors. -* Easy side-by-side docs for multiple versions of library. -* ability to add questions and answers to library documentation - -## Tutorials - -* Inclusion of step-by-step tutrials in Roc libraries, platforms or business specific code. -* Having to set up your own website for a tutorial can be a lot of work, making it easy to make quality tutorials would make for a more delightful experience. - -## General Plugin Ideas - -### Ideas - -* Plugin to translate linux commands like curl to Roc code -* Plugin to view diff between two texts -* Plugin to present codebase to new developer or walk co-worker through a problem. Records sequence of filenames and line numbers. - -### Inspiration - -- [Boop](https://github.com/IvanMathy/Boop) scriptable scratchpad for developers. Contains collection of useful conversions: json formatting, url encoding, encode to base64... -- [processing](processing.org) Interactive editor, dragging left or right with mouse to change values. Instant results. -- [flowistry](https://github.com/willcrichton/flowistry) easily track all named values in a certain expression throughout your program. - -## High performance - -### Inspiration - -- [10x editor](http://www.10xeditor.com/) IDE/Editor targeted at the professional developer with an emphasis on performance and scalability. - - -## Positive feedback - -- It's nice to enhance the feeling of reward after completing a task, this increases motivation. -- Great for tutorials and the first run of the editor. -- Suggestions of occasions for positive feedback: - - Being able to compile successfully after starting out with more than X errors. - - Making a test succeed after repeated failures. -- Positive feedback could be delivered with messages and/or animations. Animations could be with fireworks, flying roc logo birds, sounds... -- The intensity of the message/animation could be increased based on the duration/difficulty of the task. -- Suggest to search for help or take a break after being stuck on a test/compile errors... for some time. A search could be done for group chats for relevant libraries. - -### Inspiration - -- [Duolingo](https://www.duolingo.com) app to learn languages -- [Khan academy](https://www.khanacademy.org/) free quality education for everyone - -## Security - -- Remove permissions if plugin has not been used for a long time. -- Log plugin actions that require a permission. -- Show plugin that is currently using a permission, e.g. roc-core is reading from folder /gitrepos/hello in status bar and with a scrollable log. - -## General Thoughts/Ideas - -Thoughts and ideas possibly taken from above inspirations or separate. - -* ACCESSIBILITY === EMPATHY - * Visual Imapirments - No Animation is most benign form of cognitive disabity but really important base line of people with tense nerve system. - Insensitivity to certain or all colors. - Need of highcontrast - Or Everything Magnified for me with no glasses. - Or Total blindness where we need to trough sound to communicate to the user - Screen readers read trees of labeled elements. Each platform has different apis, but I think they are horrible. Just close your eyes and imagine listening to screen reader all day while you are using this majectic machines called computers. - But blind people walk with a tool and they can react much better to sound/space relations than full on visal majority does. They are acute to sound as a spatial hint. And a hand for most of them is a very sensitive tool that can make sounds in space. - Imagine if everytime for the user doesnt want to rely on shining rendered pixels on the screen for a feedback from machine, we make a acoustic room simulation, where with moving the "stick", either with mouse or with key arrows, we bump into one of the objects and that produces certain contextually appropriate sound (clean)*ding* - - On the each level of abstraction they can make sounds more deeper, so then when you type letters you feel like you are playing with the sand (soft)*shh*. We would need help from some sound engineer about it, but imagine moving down, which can be voice triggered command for motion impaired, you hear (soft)*pup* and the name of the module, and then you have options and commands appropriate for the module, they could map to those basic 4 buttons that we trained user on, and he would shortcut all the soft talk with click of a button. Think of the satisfaction when you can skip the dialog of the game and get straight into action. (X) Open functions! each function would make a sound and say its name, unless you press search and start searching for a specific function inside module, if you want one you select or move to next. - - Related idea: Playing sounds in rapid succession for different expressions in your program might be a high throughput alternative to stepping through your code line by line. I'd bet you quickly learn what your program should sound like. The difference in throughput would be even larger for those who need to rely on voice transcription. - - * Motor impariments - [rant]BACKS OF CODERS ARE NOT HEALTHY! We need to change that![/neverstop] - Too much mouse waving and sitting for too long is bad for humans. - Keyboard is basic accessability tool but - Keyboard is also optional, some people have too shaky hands even for keyboard. - They rely on eye tracking to move mouse cursor arond. - If we employ _some_ voice recognition functions we could make same interface as we could do for consoles where 4+2 buttons and directional pad would suffice. - That is 10 phrases that need to be pulled trough as many possible translations so people don't have to pretend that they are from Maine or Texas so they get voice recognition to work. Believe me I was there with Apple's Siri :D That is why we have 10 phrases for movement and management and most basic syntax. - * Builtin fonts that can be read more easily by those with dyslexia. - -* Nice backtraces that highlight important information -* Ability to show import connection within project visually - * This could be done by drawing connections between files or functions in the tree view. This would make it easier for people to get their bearings in new big projects. -* Connections could also be drawn between functions that call each other in the tree view. The connections could be animated to show the execution flow of the program. -* Ability to inline statements contained in called functions into the callee function for debugging. - * The value of expressions can be shown at the end of the line like in the [Inventing on Principle talk](https://youtu.be/8QiPFmIMxFc?t=1181) - * This would give a clear overview of the execution and should make it easy to pinpoint the line where the bug originates. - * That specific line can then be right clicked to go to the actual function. - * Having to jump around between different functions and files is unnecessary and makes it difficult to see the forest through the trees. -* "Error mode" where the editor jumps you to the next error - * Similar in theory to diff tools that jump you to the next merge conflict -* dependency recommendation -* Command to change the file to put all exposed functions at the top of the file, private functions below. Other alternative; ability to show a "file explorer" that shows exposed functions first, followed by private functions. -* We could provide a more expansive explanation in errors that can benefit from it. This explanation could be folded(shown on click) by default in the editor. -* Code coverage visualization: allow to display code in different color when it is covered by test. -* Make "maximal privacy version" of editor available for download, next to regular version. This version would not be capable of sharing any usage/user data. -* Live code view with wasm editor. This saves bandwidth when pairing. -* [Gingkowriter](https://gingkowriter.com/) structured writing app. -* Performance improvement recommendation: show if code is eligible for tail call optimization or can do in place mutation. diff --git a/editor/snippet-ideas.md b/editor/snippet-ideas.md deleted file mode 100644 index 5bca8478ab..0000000000 --- a/editor/snippet-ideas.md +++ /dev/null @@ -1,113 +0,0 @@ -I think snippet insertion would make for an awesome demo that shows off the potential of the editor and a basic version would not be that difficult to implement. -With snippet insertion I mean the following: - -say you have a list of Person records, people in scope -you press some keyboard shortcut -a text field pops up -you enter "sort" -we show autocomplete options like sort people by firstName, sort people by lastName and sort people by age. The recommendations can be that good because we know people is the only list in scope and we know which fields are in the Person record. -you navigate to sort people by age, press enter and we show in the autocomplete popup: sort people by age descending and sort people by age ascending. -you navigate to sort people by age ascending and press Enter -The correct Roc code is inserted -This is most useful for beginning Roc programmers, but I could see it saving time for experts as well. -Novice to expert programmers who are new to Roc can also perfectly describe what they want to happen but may not know the correct syntax, names of builtin functions... -Other useful snippet commands for beginning Roc programmers might be empty dict, lambda function, split strVal into chars... -Some more advanced snippets: post jsonVal to urlVal, connect to amazon S3, insert function of common algorithm like: sieve of erathostenes, greatest common divider... - -This could remove the need for a lot of googling/stackoverflow, creating a delightful experience that sets us apart from other editors. -And contrary to stackoverflow/github copilot, snippets will be written by Roc experts or be easily editable by us. They can also be guaranteed to work for a specific Roc and library version because we update, version, and test them. - -A nice goal to aim for is that the user never needs/wants to leave the editor to look things up. -We have way more context inside the editor so we should be able to do better than any general-purpose search engine. - -I think the snippet insertion commands also set us up for quality interaction with users using voice input. - -The CC0 license seems like a good fit for the snippets. - -Fuzzy matching should be done to suggest a closest fuzzy match, so if the user types the snippet command `empty Map`, we should suggest `empty Dict`. - -# Snippet ideas - -## Pure Text Snippets - -Pure text snippets are not templates and do not contain typed holes. -Fish hooks are used when subvariants should be created e.g.: means this pure text snippets should be created for all Roc collections such as Dict, Set, List... - -- command: empty - + example: empty dict >> `{::}` -- command: - + example: sieve of erathostenes >> `inserts function for sieve of erathostenes` - + common algorithms: sieve of erathostenes, greatest common divisor, prime factorisation, A* path finding, Dijkstra's algorithm, Breadth First Search... -- command: current date/datetime - + example: current datetime >> `now <- Time.now\n` -- command: list range 1 to 5 - + example: [1, 2, 3, 4, 5] -- command: use commandline args -- command: post/get/put request -- command: extract float(s)/number/emal addresses from string. regex match float/number/email address/... -- command: execute (bash) command/script -- command: cast/convert/parse list of x to list of y -- command: pattern match/ match/ switch/ case - -## AST aware snippets - -Snippets are inserted based on type of value on which the cursor is located. - -- command: - + example: - * We have the cursor like this `people|` - * User presses snippet shortcut or dot key - * We show a list with all builtin functions for the List type - * User chooses contains - * We change code to `List.contains people |Blank` -- command: Str to chars/charlist - - -## Snippets with Typed Holes - -- command: sort ^List *^ (by ^Record Field^) {ascending/descending} - + example: sort people by age descending >> ... -- command: escape url - + example: >> `percEncodedString = Url.percentEncode ^String^` -- command: list files in directory - + example: >> - ``` - path <- File.pathFromStr ^String^ - dirContents <- File.enumerateDir path - ``` -- command: remove/create file -- command: read/write from file -- command: concatenate strings -- command: trim (newlines) at end/start/right/left -- command: evaluate predicate for all in slice/list/array -- command: get element at index -- command: get char at index -- command: reverse stirng -- command: lambda/anonymous function -- we should auto create type hole commands for all builtins. - + example: List has builtins reverse, repeat, len... generated snippet commands should be: - * reverse list > List.reverse ^List *^ - * repeat list > List.repeat ^elem^ ^Nat^ - * len list (fuzzy matches should be length of list) -- append element to list - -# fuzzy matching - - some pairs for fuzzy matching unit tests: - - hashmap > Dict - - map > map (function), Dict - - for > map, mapWithIndex, walk, walkBackwards, zip - - apply/for yield > map - - fold > walk, walkBackwards - - foldl > walkBackwards - - foldr > walk - - head > takeFirst - - filter > keepIf - -# Inspiration - -- [grepper](https://www.codegrepper.com/) snippet collection that embeds in google search results. See also this [collection of common questions](https://www.codegrepper.com/code-examples/rust). -- [github copilot](https://copilot.github.com/) snippet generation with machine learning -- [stackoverflow](https://stackoverflow.com) -- [rosetta code](http://www.rosettacode.org/wiki/Rosetta_Code) snippets in many different programming languages. Many [snippets](https://www.rosettacode.org/wiki/Category:Programming_Tasks) are programming contest style problems, but there also problems that demonstrate the use of JSON, SHA-256, read a file line by line... -- check docs of popular languages to cross reference function/snippet names for fuzzy matching diff --git a/editor/src/editor/code_lines.rs b/editor/src/editor/code_lines.rs deleted file mode 100644 index 521686e6e0..0000000000 --- a/editor/src/editor/code_lines.rs +++ /dev/null @@ -1,82 +0,0 @@ -use crate::ui::text::lines::Lines; -use crate::ui::text::text_pos::TextPos; -use crate::ui::ui_error::UIResult; -use crate::ui::util::slice_get; -use std::fmt; - -#[derive(Debug, Default)] -pub struct CodeLines { - pub lines: Vec, - pub nr_of_chars: usize, -} - -impl CodeLines { - pub fn from_str(code_str: &str) -> CodeLines { - CodeLines { - lines: code_str.split('\n').map(|s| s.to_owned()).collect(), - nr_of_chars: code_str.len(), - } - } - - // last column of last line - pub fn end_txt_pos(&self) -> TextPos { - let last_line_nr = self.nr_of_lines() - 1; - - TextPos { - line: last_line_nr, - column: self.line_len(last_line_nr).unwrap(), // safe because we just calculated last_line - } - } -} - -impl Lines for CodeLines { - fn get_line_ref(&self, line_nr: usize) -> UIResult<&str> { - let line_string = slice_get(line_nr, &self.lines)?; - - Ok(line_string) - } - - fn line_len(&self, line_nr: usize) -> UIResult { - self.get_line_ref(line_nr).map(|line| line.len()) - } - - fn nr_of_lines(&self) -> usize { - self.lines.len() - } - - fn nr_of_chars(&self) -> usize { - self.nr_of_chars - } - - fn all_lines_as_string(&self) -> String { - self.lines.join("\n") - } - - fn is_last_line(&self, line_nr: usize) -> bool { - line_nr == self.nr_of_lines() - 1 - } - - fn last_char(&self, line_nr: usize) -> UIResult> { - Ok(self.get_line_ref(line_nr)?.chars().last()) - } -} - -impl fmt::Display for CodeLines { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - for row in &self.lines { - let row_str = row - .chars() - .map(|code_char| format!("{}", code_char)) - .collect::>() - .join(" "); - - let escaped_row_str = row_str.replace('\n', "\\n"); - - write!(f, "\n{}", escaped_row_str)?; - } - - writeln!(f, " (code_lines, {:?} lines)", self.lines.len())?; - - Ok(()) - } -} diff --git a/editor/src/editor/config.rs b/editor/src/editor/config.rs deleted file mode 100644 index 8a16e3e70e..0000000000 --- a/editor/src/editor/config.rs +++ /dev/null @@ -1,31 +0,0 @@ -use serde::{Deserialize, Serialize}; - -use crate::editor::theme::EdTheme; - -use super::resources::strings::START_TIP; - -#[derive(Serialize, Deserialize)] -pub struct Config { - pub code_font_size: f32, - pub debug_font_size: f32, - pub ed_theme: EdTheme, -} - -impl Default for Config { - fn default() -> Self { - Self { - code_font_size: 30.0, - debug_font_size: 20.0, - ed_theme: EdTheme::default(), - } - } -} - -impl Config { - pub fn make_code_txt_xy(&self) -> (f32, f32) { - ( - self.code_font_size, - (START_TIP.matches('\n').count() as f32 + 2.0) * self.code_font_size, - ) - } -} diff --git a/editor/src/editor/ed_error.rs b/editor/src/editor/ed_error.rs deleted file mode 100644 index c5379ee1a1..0000000000 --- a/editor/src/editor/ed_error.rs +++ /dev/null @@ -1,365 +0,0 @@ -use crate::ui::text::text_pos::TextPos; -use colored::*; -use roc_ast::ast_error::ASTError; -use roc_ast::lang::core::ast::ASTNodeId; -use roc_code_markup::markup_error::MarkError; -use roc_code_markup::slow_pool::MarkNodeId; -use roc_module::module_err::ModuleError; -use snafu::{Backtrace, ErrorCompat, Snafu}; - -//import errors as follows: -// `use crate::error::OutOfBounds;` -// *not* `use crate::error::EdError::OutOfBounds;` -// see https://github.com/shepmaster/snafu/issues/211 - -#[derive(Debug, Snafu)] -#[snafu(visibility(pub))] -pub enum EdError { - #[snafu(display( - "ASTNodeIdWithoutDefId: The expr_id_opt in ASTNode({:?}) was `None` but I was expecting `Some(DefId)` .", - ast_node_id - ))] - ASTNodeIdWithoutDefId { - ast_node_id: ASTNodeId, - backtrace: Backtrace, - }, - - #[snafu(display( - "ASTNodeIdWithoutExprId: The expr_id_opt in ASTNode({:?}) was `None` but I was expecting `Some(ExprId)` .", - ast_node_id - ))] - ASTNodeIdWithoutExprId { - ast_node_id: ASTNodeId, - backtrace: Backtrace, - }, - - #[snafu(display( - "CaretNotFound: No carets were found in the expected node with id {}", - node_id - ))] - CaretNotFound { - node_id: MarkNodeId, - backtrace: Backtrace, - }, - - #[snafu(display("ClipboardReadFailed: could not get clipboard contents: {}", err_msg))] - ClipboardReadFailed { - err_msg: String, - }, - - #[snafu(display("ClipboardWriteFailed: could not set clipboard contents: {}", err_msg))] - ClipboardWriteFailed { - err_msg: String, - }, - - #[snafu(display( - "ClipboardInitFailed: could not initialize ClipboardContext: {}.", - err_msg - ))] - ClipboardInitFailed { - err_msg: String, - }, - - #[snafu(display( - "ExpectedTextNode: the function {} expected a Text node, got {} instead.", - function_name, - node_type - ))] - ExpectedTextNode { - function_name: String, - node_type: String, - backtrace: Backtrace, - }, - - #[snafu(display( - "EmptyCodeString: I need to have a code string (code_str) that contains either an app, interface or Package-Config header. The code string was empty.", - ))] - EmptyCodeString { - backtrace: Backtrace, - }, - - #[snafu(display("FailedToUpdateIdentIdName: {}", err_str))] - FailedToUpdateIdentIdName { - err_str: String, - backtrace: Backtrace, - }, - - #[snafu(display("GetContentOnNestedNode: tried to get string content from Nested MarkupNode. Can only get content from Text or Blank nodes."))] - GetContentOnNestedNode { - backtrace: Backtrace, - }, - - #[snafu(display( - "IndexOfFailed: Element {} was not found in collection {}.", - elt_str, - collection_str - ))] - IndexOfFailed { - elt_str: String, - collection_str: String, - backtrace: Backtrace, - }, - - #[snafu(display("KeyNotFound: key {} was not found in HashMap.", key_str,))] - KeyNotFound { - key_str: String, - backtrace: Backtrace, - }, - - #[snafu(display( - "MissingParent: MarkupNode with id {} should have a parent but there was none.", - node_id - ))] - MissingParent { - node_id: MarkNodeId, - backtrace: Backtrace, - }, - - #[snafu(display( - "MissingSelection: ed_model.selected_expr2_id was Some(ExprId) but ed_model.caret_w_sel_vec did not contain any Some(Selection)." - ))] - MissingSelection { - backtrace: Backtrace, - }, - - #[snafu(display("NestedNodeMissingChild: expected to find child with id {} in Nested MarkupNode, but it was missing. Id's of the children are {:?}.", node_id, children_ids))] - NestedNodeMissingChild { - node_id: MarkNodeId, - children_ids: Vec, - backtrace: Backtrace, - }, - - #[snafu(display( - "NestedNodeRequired: required a Nested node at this position, node was a {}.", - node_type - ))] - NestedNodeRequired { - node_type: String, - backtrace: Backtrace, - }, - - #[snafu(display("NestedNodeWithoutChildren: tried to retrieve child from Nested MarkupNode with id {} but it had no children.", node_id))] - NestedNodeWithoutChildren { - node_id: MarkNodeId, - backtrace: Backtrace, - }, - - #[snafu(display("NoDefMarkNodeBeforeLineNr: I could not find a MarkupNode whose root parent points to a DefId located before the given line number: {}.", line_nr))] - NoDefMarkNodeBeforeLineNr { - line_nr: usize, - backtrace: Backtrace, - }, - - #[snafu(display("NodeWithoutAttributes: expected to have a node with attributes. This is a Nested MarkupNode, only Text and Blank nodes have attributes."))] - NodeWithoutAttributes { - backtrace: Backtrace, - }, - - #[snafu(display( - "NodeIdNotInGridNodeMap: MarkNodeId {} was not found in ed_model.grid_node_map.", - node_id - ))] - NodeIdNotInGridNodeMap { - node_id: MarkNodeId, - backtrace: Backtrace, - }, - - #[snafu(display( - "NoNodeAtCaretPosition: there was no node at the current caret position {:?}.", - caret_pos, - ))] - NoNodeAtCaretPosition { - caret_pos: TextPos, - backtrace: Backtrace, - }, - - #[snafu(display( - "OutOfBounds: index {} was out of bounds for {} with length {}.", - index, - collection_name, - len - ))] - OutOfBounds { - index: usize, - collection_name: String, - len: usize, - backtrace: Backtrace, - }, - - #[snafu(display("RecordWithoutFields: expected record to have at least one field because it is not an EmptyRecord."))] - RecordWithoutFields { - backtrace: Backtrace, - }, - - #[snafu(display( - "RocCheckFailed: `cargo run check`/`roc check` detected errors(see terminal)." - ))] - RocCheckFailed, - - #[snafu(display("ParseError: Failed to parse AST: SyntaxError: {}.", syntax_err))] - SrcParseError { - syntax_err: String, - backtrace: Backtrace, - }, - - #[snafu(display("StringParseError: {}", msg))] - StringParseError { - msg: String, - backtrace: Backtrace, - }, - - #[snafu(display( - "UnexpectedASTNode: required a {} at this position, node was a {}.", - required_node_type, - encountered_node_type - ))] - UnexpectedASTNode { - required_node_type: String, - encountered_node_type: String, - backtrace: Backtrace, - }, - - #[snafu(display( - "UnexpectedEmptyPoolVec: expected PoolVec {} to have at least one element.", - descriptive_vec_name - ))] - UnexpectedEmptyPoolVec { - descriptive_vec_name: String, - backtrace: Backtrace, - }, - - #[snafu(display( - "UnexpectedPattern2Variant: required a {} at this position, Pattern2 was a {}.", - required_pattern2, - encountered_pattern2, - ))] - UnexpectedPattern2Variant { - required_pattern2: String, - encountered_pattern2: String, - backtrace: Backtrace, - }, - - #[snafu(display("ASTError: {}", msg))] - ASTErrorBacktrace { - msg: String, - backtrace: Backtrace, - }, - #[snafu(display("UIError: {}", msg))] - UIErrorBacktrace { - msg: String, - backtrace: Backtrace, - }, - #[snafu(display("MarkError: {}", msg))] - MarkErrorBacktrace { - msg: String, - backtrace: Backtrace, - }, - WrapASTError { - #[snafu(backtrace)] - source: ASTError, - }, - WrapUIError { - #[snafu(backtrace)] - source: UIError, - }, - WrapMarkError { - #[snafu(backtrace)] - source: MarkError, - }, - WrapModuleError { - #[snafu(backtrace)] - source: ModuleError, - }, - WrapIoError { - source: std::io::Error, - }, -} - -pub type EdResult = std::result::Result; - -pub fn print_err(err: &EdError) { - eprintln!("{}", format!("{}", err).truecolor(255, 0, 0)); - - if let Some(backtrace) = ErrorCompat::backtrace(err) { - eprintln!("{}", color_backtrace(backtrace)); - } -} - -fn color_backtrace(backtrace: &snafu::Backtrace) -> String { - let backtrace_str = format!("{}", backtrace); - let backtrace_split = backtrace_str.split('\n'); - let irrelevant_src = vec![".cargo", "registry", ".rustup", "rustc"]; - - let mut ret_str = String::new(); - let mut prev_line_opt: Option = None; - - for line in backtrace_split { - let new_line = if line.contains("src") { - if !contains_one_of(line, &irrelevant_src) { - if let Some(prev_line) = prev_line_opt { - prev_line_opt = Some(format!("{}", prev_line.truecolor(255, 30, 30))); - } - format!("{}\n", line.truecolor(255, 100, 100)) - } else { - format!("{}\n", line) - } - } else { - format!("{}\n", line) - }; - - if let Some(prev_line) = prev_line_opt { - ret_str.push_str(&prev_line); - } - prev_line_opt = Some(new_line); - } - - ret_str -} - -fn contains_one_of(main_str: &str, contain_slice: &[&str]) -> bool { - for contain_str in contain_slice { - if main_str.contains(contain_str) { - return true; - } - } - - false -} - -impl From for String { - fn from(ed_error: EdError) -> Self { - format!("{}", ed_error) - } -} - -use crate::ui::ui_error::UIError; - -impl From for EdError { - fn from(ui_err: UIError) -> Self { - Self::WrapUIError { source: ui_err } - } -} - -impl From for EdError { - fn from(mark_err: MarkError) -> Self { - Self::WrapMarkError { source: mark_err } - } -} - -impl From for EdError { - fn from(ast_err: ASTError) -> Self { - Self::WrapASTError { source: ast_err } - } -} - -impl From for EdError { - fn from(module_err: ModuleError) -> Self { - Self::WrapModuleError { source: module_err } - } -} - -impl From for EdError { - fn from(io_err: std::io::Error) -> Self { - Self::WrapIoError { source: io_err } - } -} diff --git a/editor/src/editor/grid_node_map.rs b/editor/src/editor/grid_node_map.rs deleted file mode 100644 index 04bc197d17..0000000000 --- a/editor/src/editor/grid_node_map.rs +++ /dev/null @@ -1,441 +0,0 @@ -use crate::editor::ed_error::EdResult; -use crate::editor::ed_error::NestedNodeWithoutChildren; -use crate::editor::ed_error::{NoDefMarkNodeBeforeLineNr, NodeIdNotInGridNodeMap}; -use crate::editor::mvc::ed_model::EdModel; -use crate::editor::util::first_last_index_of; -use crate::editor::util::index_of; -use crate::ui::text::selection::Selection; -use crate::ui::text::text_pos::TextPos; -use crate::ui::ui_error::{LineInsertionFailed, OutOfBounds, UIResult}; -use crate::ui::util::{slice_get, slice_get_mut}; -use roc_ast::lang::core::ast::ASTNodeId; -use roc_code_markup::markup::mark_id_ast_id_map::MarkIdAstIdMap; -use roc_code_markup::markup::nodes::get_root_mark_node_id; -use roc_code_markup::slow_pool::MarkNodeId; -use roc_code_markup::slow_pool::SlowPool; -use snafu::OptionExt; -use std::cmp::Ordering; -use std::fmt; - -#[derive(Debug)] -pub struct GridNodeMap { - pub lines: Vec>, -} - -impl GridNodeMap { - pub fn insert_between_line( - &mut self, - line_nr: usize, - index: usize, - len: usize, - node_id: MarkNodeId, - ) -> UIResult<()> { - let nr_of_lines = self.lines.len(); - - if line_nr < nr_of_lines { - let line_ref = slice_get_mut(line_nr, &mut self.lines)?; - let new_cols_vec: Vec = std::iter::repeat(node_id).take(len).collect(); - - line_ref.splice(index..index, new_cols_vec); - } else if line_nr >= nr_of_lines { - for _ in 0..((line_nr - nr_of_lines) + 1) { - self.push_empty_line(); - } - - self.insert_between_line(line_nr, index, len, node_id)?; - } else { - LineInsertionFailed { - line_nr, - nr_of_lines, - } - .fail()?; - } - - Ok(()) - } - - pub fn insert_empty_line(&mut self, line_nr: usize) -> UIResult<()> { - if line_nr <= self.lines.len() { - self.lines.insert(line_nr, Vec::new()); - - Ok(()) - } else { - OutOfBounds { - index: line_nr, - collection_name: "code_lines.lines".to_owned(), - len: self.lines.len(), - } - .fail() - } - } - - pub fn push_empty_line(&mut self) { - self.lines.push(vec![]); - } - - pub fn break_line(&mut self, line_nr: usize, col_nr: usize) -> UIResult<()> { - // clippy prefers this over if-else - match line_nr.cmp(&self.lines.len()) { - Ordering::Less => { - self.insert_empty_line(line_nr + 1)?; - - let line_ref = self.lines.get_mut(line_nr).unwrap(); // safe because we checked line_nr - - if col_nr < line_ref.len() { - let next_line_str: Vec = line_ref.drain(col_nr..).collect(); - - let next_line_ref = self.lines.get_mut(line_nr + 1).unwrap(); // safe because we just added the line - - *next_line_ref = next_line_str; - } - - Ok(()) - } - Ordering::Equal => self.insert_empty_line(line_nr + 1), - Ordering::Greater => OutOfBounds { - index: line_nr, - collection_name: "grid_node_map.lines".to_owned(), - len: self.lines.len(), - } - .fail(), - } - } - - pub fn clear_line(&mut self, line_nr: usize) -> UIResult<()> { - let line_ref = slice_get_mut(line_nr, &mut self.lines)?; - - *line_ref = vec![]; - - Ok(()) - } - - pub fn del_line(&mut self, line_nr: usize) { - self.lines.remove(line_nr); - } - - pub fn del_at_line(&mut self, line_nr: usize, column: usize) -> UIResult<()> { - let line_ref = slice_get_mut(line_nr, &mut self.lines)?; - - line_ref.remove(column); - - Ok(()) - } - - pub fn del_range_at_line( - &mut self, - line_nr: usize, - col_range: std::ops::Range, - ) -> UIResult<()> { - let line_ref = slice_get_mut(line_nr, &mut self.lines)?; - - line_ref.drain(col_range); - - Ok(()) - } - - pub fn del_selection(&mut self, selection: Selection) -> UIResult<()> { - if selection.is_on_same_line() { - let line_ref = slice_get_mut(selection.start_pos.line, &mut self.lines)?; - - line_ref.drain(selection.start_pos.column..selection.end_pos.column); - } else { - unimplemented!("TODO support deleting multiline selection") - } - - Ok(()) - } - - pub fn get_id_at_row_col(&self, caret_pos: TextPos) -> UIResult { - let line = slice_get(caret_pos.line, &self.lines)?; - let node_id = slice_get(caret_pos.column, line)?; - - Ok(*node_id) - } - - pub fn get_offset_to_node_id( - &self, - caret_pos: TextPos, - node_id: MarkNodeId, - ) -> EdResult { - let line = slice_get(caret_pos.line, &self.lines)?; - - let first_node_index = index_of(node_id, line)?; - - Ok(caret_pos.column - first_node_index) - } - - pub fn node_exists_at_pos(&self, pos: TextPos) -> bool { - if pos.line < self.lines.len() { - // safe unwrap because we checked the length - let line = self.lines.get(pos.line).unwrap(); - - pos.column < line.len() - } else { - false - } - } - - // get position of first occurrence of node_id if get_first_pos, else get the last occurrence - pub fn get_node_position(&self, node_id: MarkNodeId, get_first_pos: bool) -> EdResult { - let mut last_pos_opt = None; - - for (line_index, line) in self.lines.iter().enumerate() { - for (col_index, iter_node_id) in line.iter().enumerate() { - if node_id == *iter_node_id && get_first_pos { - return Ok(TextPos { - line: line_index, - column: col_index, - }); - } else if node_id == *iter_node_id { - last_pos_opt = Some(TextPos { - line: line_index, - column: col_index, - }) - } else if let Some(last_pos) = last_pos_opt { - return Ok(last_pos); - } - } - } - - if let Some(last_pos) = last_pos_opt { - Ok(last_pos) - } else { - NodeIdNotInGridNodeMap { node_id }.fail() - } - } - - // returns start and end pos of Expr2/Def2, relevant AST node and MarkNodeId of the corresponding MarkupNode - pub fn get_block_start_end_pos( - &self, - caret_pos: TextPos, - ed_model: &EdModel, - ) -> EdResult<(TextPos, TextPos, ASTNodeId, MarkNodeId)> { - let line = slice_get(caret_pos.line, &self.lines)?; - let node_id = *slice_get(caret_pos.column, line)?; - let node = ed_model.mark_node_pool.get(node_id); - - if node.is_nested() { - let (start_pos, end_pos) = self.get_nested_start_end_pos(node_id, ed_model)?; - - Ok(( - start_pos, - end_pos, - ed_model.mark_id_ast_id_map.get(node_id)?, - node_id, - )) - } else { - let (first_node_index, last_node_index) = first_last_index_of(node_id, line)?; - - let curr_node_id = *slice_get(first_node_index, line)?; - let curr_ast_node_id = ed_model.mark_id_ast_id_map.get(curr_node_id)?; - - let mut expr_start_index = first_node_index; - let mut expr_end_index = last_node_index; - - // we may encounter ast id's of children of the current node - let mut pos_extra_subtract = 0; - - for i in (0..first_node_index).rev() { - let prev_pos_node_id = *slice_get(i, line)?; - let prev_ast_node_id = ed_model.mark_id_ast_id_map.get(prev_pos_node_id)?; - - if prev_ast_node_id == curr_ast_node_id { - if pos_extra_subtract > 0 { - expr_start_index -= pos_extra_subtract + 1; - pos_extra_subtract = 0; - } else { - expr_start_index -= 1; - } - } else { - pos_extra_subtract += 1; - } - } - - // we may encounter ast id's of children of the current node - let mut pos_extra_add = 0; - - for i in last_node_index..line.len() { - let next_pos_node_id = slice_get(i, line)?; - let next_ast_node_id = ed_model.mark_id_ast_id_map.get(*next_pos_node_id)?; - - if next_ast_node_id == curr_ast_node_id { - if pos_extra_add > 0 { - expr_end_index += pos_extra_add + 1; - pos_extra_add = 0; - } else { - expr_end_index += 1; - } - } else { - pos_extra_add += 1; - } - } - - let correct_mark_node_id = GridNodeMap::get_top_node_with_expr_id( - curr_node_id, - &ed_model.mark_node_pool, - &ed_model.mark_id_ast_id_map, - )?; - - Ok(( - TextPos { - line: caret_pos.line, - column: expr_start_index, - }, - TextPos { - line: caret_pos.line, - column: expr_end_index, - }, - curr_ast_node_id, - correct_mark_node_id, - )) - } - } - - // A markup node may refer to a bracket `{`, in that case we want the parent, a Nested MarkNode. - // `{` is not the entire Expr2 - fn get_top_node_with_expr_id( - curr_node_id: MarkNodeId, - mark_node_pool: &SlowPool, - mark_id_ast_id_map: &MarkIdAstIdMap, - ) -> EdResult { - let curr_node = mark_node_pool.get(curr_node_id); - - if let Some(parent_id) = curr_node.get_parent_id_opt() { - if mark_id_ast_id_map.get(parent_id)? == mark_id_ast_id_map.get(curr_node_id)? { - Ok(parent_id) - } else { - Ok(curr_node_id) - } - } else { - Ok(curr_node_id) - } - } - - pub fn get_nested_start_end_pos( - &self, - nested_node_id: MarkNodeId, - ed_model: &EdModel, - ) -> EdResult<(TextPos, TextPos)> { - let left_most_leaf = self.get_leftmost_leaf(nested_node_id, ed_model)?; - - let right_most_leaf = self.get_rightmost_leaf(nested_node_id, ed_model)?; - - let expr_start_pos = ed_model - .grid_node_map - .get_node_position(left_most_leaf, true)?; - let expr_end_pos = ed_model - .grid_node_map - .get_node_position(right_most_leaf, false)? - .increment_col(); - - Ok((expr_start_pos, expr_end_pos)) - } - - fn get_leftmost_leaf( - &self, - nested_node_id: MarkNodeId, - ed_model: &EdModel, - ) -> EdResult { - let mut children_ids = ed_model - .mark_node_pool - .get(nested_node_id) - .get_children_ids(); - let mut first_child_id = 0; - - while !children_ids.is_empty() { - first_child_id = *children_ids - .first() - .with_context(|| NestedNodeWithoutChildren { - node_id: nested_node_id, - })?; - - children_ids = ed_model - .mark_node_pool - .get(first_child_id) - .get_children_ids(); - } - - Ok(first_child_id) - } - - fn get_rightmost_leaf( - &self, - nested_node_id: MarkNodeId, - ed_model: &EdModel, - ) -> EdResult { - let mut children_ids = ed_model - .mark_node_pool - .get(nested_node_id) - .get_children_ids(); - let mut last_child_id = 0; - - while !children_ids.is_empty() { - last_child_id = *children_ids - .last() - .with_context(|| NestedNodeWithoutChildren { - node_id: nested_node_id, - })?; - - children_ids = ed_model - .mark_node_pool - .get(last_child_id) - .get_children_ids(); - } - - Ok(last_child_id) - } - - // get id of root mark_node whose ast_node_id points to a DefId - pub fn get_def_mark_node_id_before_line( - &self, - line_nr: usize, - mark_node_pool: &SlowPool, - mark_id_ast_id_map: &MarkIdAstIdMap, - ) -> EdResult { - for curr_line_nr in (0..line_nr).rev() { - let first_col_pos = TextPos { - line: curr_line_nr, - column: 0, - }; - - if self.node_exists_at_pos(first_col_pos) { - let mark_node_id = self.get_id_at_row_col(first_col_pos)?; - let root_mark_node_id = get_root_mark_node_id(mark_node_id, mark_node_pool); - - let ast_node_id = mark_id_ast_id_map.get(root_mark_node_id)?; - - if let ASTNodeId::ADefId(_) = ast_node_id { - return Ok(root_mark_node_id); - } - } - } - - NoDefMarkNodeBeforeLineNr { line_nr }.fail() - } -} - -impl Default for GridNodeMap { - fn default() -> Self { - GridNodeMap { - lines: vec![Vec::new()], - } - } -} - -impl fmt::Display for GridNodeMap { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - for row in &self.lines { - let row_str = row - .iter() - .map(|mark_node_id| format!(" {} ", mark_node_id)) - .collect::>() - .join(", "); - - writeln!(f, "{}", row_str)?; - } - - writeln!(f, "(grid_node_map, {:?} lines)", self.lines.len())?; - - Ok(()) - } -} diff --git a/editor/src/editor/keyboard_input.rs b/editor/src/editor/keyboard_input.rs deleted file mode 100644 index f1dad9a2af..0000000000 --- a/editor/src/editor/keyboard_input.rs +++ /dev/null @@ -1,177 +0,0 @@ -use crate::editor::ed_error::EdResult; -use crate::editor::mvc::app_model::AppModel; -use crate::editor::mvc::app_update::{ - handle_copy, handle_cut, handle_paste, pass_keydown_to_focused, -}; -use crate::window::keyboard_input::from_winit; -use winit::event::VirtualKeyCode::*; -use winit::event::{ElementState, ModifiersState, VirtualKeyCode}; - -pub fn handle_keydown( - elem_state: ElementState, - virtual_keycode: VirtualKeyCode, - modifiers_winit: ModifiersState, - app_model: &mut AppModel, -) -> EdResult<()> { - if let ElementState::Released = elem_state { - return Ok(()); - } - - let modifiers = from_winit(&modifiers_winit); - - match virtual_keycode { - Left | Up | Right | Down => { - pass_keydown_to_focused(&modifiers, virtual_keycode, app_model)? - } - - Copy => handle_copy(app_model)?, - Paste => handle_paste(app_model)?, - Cut => handle_cut(app_model)?, - C => { - if modifiers.cmd_or_ctrl() { - handle_copy(app_model)? - } - } - V => { - if modifiers.cmd_or_ctrl() { - handle_paste(app_model)? - } - } - X => { - if modifiers.cmd_or_ctrl() { - handle_cut(app_model)? - } - } - - _ => pass_keydown_to_focused(&modifiers, virtual_keycode, app_model)?, - } - - Ok(()) -} - -// pub fn handle_text_input( -// text_state: &mut String, -// elem_state: ElementState, -// virtual_keycode: VirtualKeyCode, -// _modifiers: ModifiersState, -// ) { -// use winit::event::VirtualKeyCode::*; - -// if let ElementState::Released = elem_state { -// return; -// } - -// match virtual_keycode { -// Key1 | Numpad1 => text_state.push('1'), -// Key2 | Numpad2 => text_state.push('2'), -// Key3 | Numpad3 => text_state.push('3'), -// Key4 | Numpad4 => text_state.push('4'), -// Key5 | Numpad5 => text_state.push('5'), -// Key6 | Numpad6 => text_state.push('6'), -// Key7 | Numpad7 => text_state.push('7'), -// Key8 | Numpad8 => text_state.push('8'), -// Key9 | Numpad9 => text_state.push('9'), -// Key0 | Numpad0 => text_state.push('0'), -// A => text_state.push('a'), -// B => text_state.push('b'), -// C => text_state.push('c'), -// D => text_state.push('d'), -// E => text_state.push('e'), -// F => text_state.push('f'), -// G => text_state.push('g'), -// H => text_state.push('h'), -// I => text_state.push('i'), -// J => text_state.push('j'), -// K => text_state.push('k'), -// L => text_state.push('l'), -// M => text_state.push('m'), -// N => text_state.push('n'), -// O => text_state.push('o'), -// P => text_state.push('p'), -// Q => text_state.push('q'), -// R => text_state.push('r'), -// S => text_state.push('s'), -// T => text_state.push('t'), -// U => text_state.push('u'), -// V => text_state.push('v'), -// W => text_state.push('w'), -// X => text_state.push('x'), -// Y => text_state.push('y'), -// Z => text_state.push('z'), -// Escape | F1 | F2 | F3 | F4 | F5 | F6 | F7 | F8 | F9 | F10 | F11 | F12 | F13 | F14 | F15 -// | F16 | F17 | F18 | F19 | F20 | F21 | F22 | F23 | F24 | Snapshot | Scroll | Pause -// | Insert | Home | Delete | End | PageDown | PageUp | Left | Up | Right | Down | Compose -// | caret | Numlock | AbntC1 | AbntC2 | Ax | Calculator | Capital | Convert | Kana -// | Kanji | LAlt | LBracket | LControl | LShift | LWin | Mail | MediaSelect | PlayPause -// | Power | PrevTrack | MediaStop | Mute | MyComputer | NavigateForward -// | NavigateBackward | NextTrack | NoConvert | OEM102 | RAlt | Sysrq | RBracket -// | RControl | RShift | RWin | Sleep | Stop | Unlabeled | VolumeDown | VolumeUp | Wake -// | WebBack | WebFavorites | WebForward | WebHome | WebRefresh | WebSearch | Apps | Tab -// | WebStop => { -// // TODO handle -// } -// Back => { -// text_state.pop(); -// } -// Return | NumpadEnter => { -// text_state.push('\n'); -// } -// Space => { -// text_state.push(' '); -// } -// Comma | NumpadComma => { -// text_state.push(','); -// } -// Add => { -// text_state.push('+'); -// } -// Apostrophe => { -// text_state.push('\''); -// } -// At => { -// text_state.push('@'); -// } -// Backslash => { -// text_state.push('\\'); -// } -// Colon => { -// text_state.push(':'); -// } -// Period | Decimal => { -// text_state.push('.'); -// } -// Equals | NumpadEquals => { -// text_state.push('='); -// } -// Grave => { -// text_state.push('`'); -// } -// Minus | Subtract => { -// text_state.push('-'); -// } -// Multiply => { -// text_state.push('*'); -// } -// Semicolon => { -// text_state.push(';'); -// } -// Slash | Divide => { -// text_state.push('/'); -// } -// Underline => { -// text_state.push('_'); -// } -// Yen => { -// text_state.push('¥'); -// } -// Copy => { -// todo!("copy"); -// } -// Paste => { -// todo!("paste"); -// } -// Cut => { -// todo!("cut"); -// } -// } -// } diff --git a/editor/src/editor/main.rs b/editor/src/editor/main.rs deleted file mode 100644 index c3c5c63559..0000000000 --- a/editor/src/editor/main.rs +++ /dev/null @@ -1,616 +0,0 @@ -use super::keyboard_input; -use super::resources::strings::PLATFORM_NAME; -use crate::editor::mvc::ed_view; -use crate::editor::mvc::ed_view::RenderedWgpu; -use crate::editor::resources::strings::{HELLO_WORLD, NOTHING_OPENED}; -use crate::editor::{ - config::Config, - ed_error::print_err, - mvc::{app_model::AppModel, app_update, app_update::InputOutcome, ed_model}, - theme::EdTheme, -}; -use crate::graphics::{ - colors::to_wgpu_color, - lowlevel::buffer::create_rect_buffers, - lowlevel::ortho::update_ortho_buffer, - lowlevel::pipelines, - primitives::rect::Rect, - primitives::text::{build_glyph_brush, example_code_glyph_rect, queue_text_draw, Text}, -}; -use crate::ui::text::caret_w_select::CaretPos; -use bumpalo::Bump; -use cgmath::Vector2; -use fs_extra::dir::{copy, ls, CopyOptions, DirEntryAttr, DirEntryValue}; -use futures::TryFutureExt; -use pipelines::RectResources; -use roc_ast::lang::env::Env; -use roc_ast::mem_pool::pool::Pool; -use roc_ast::module::load_module; -use roc_load::Threading; -use roc_module::symbol::IdentIds; -use roc_types::subs::VarStore; -use std::collections::HashSet; -use std::fs::{self, File}; -use std::io::Write; -use std::path::PathBuf; -use std::{error::Error, io, path::Path}; -use wgpu::{CommandEncoder, LoadOp, RenderPass, TextureView}; -use wgpu_glyph::GlyphBrush; -use winit::{ - dpi::PhysicalSize, - event, - event::{Event, ModifiersState}, - event_loop::ControlFlow, - platform::run_return::EventLoopExtRunReturn, -}; - -// Inspired by: -// https://github.com/sotrh/learn-wgpu by Benjamin Hansen, which is licensed under the MIT license -// https://github.com/cloudhead/rgx by Alexis Sellier, which is licensed under the MIT license -// -// See this link to learn wgpu: https://sotrh.github.io/learn-wgpu/ - -/// The editor is actually launched from the CLI if you pass it zero arguments, -/// or if you provide it 1 or more files or directories to open on launch. -pub fn launch(project_dir_path_opt: Option<&Path>) -> io::Result<()> { - run_event_loop(project_dir_path_opt).expect("Error running event loop"); - - Ok(()) -} - -fn run_event_loop(project_dir_path_opt: Option<&Path>) -> Result<(), Box> { - env_logger::init(); - - // Open window and create a surface - let mut event_loop = winit::event_loop::EventLoop::new(); - - let window = winit::window::WindowBuilder::new() - .with_inner_size(PhysicalSize::new(1900.0, 1000.0)) - .with_title("The Roc Editor - Work In Progress") - .build(&event_loop) - .unwrap(); - - let instance = wgpu::Instance::new(wgpu::Backends::all()); - - let surface = unsafe { instance.create_surface(&window) }; - - // Initialize GPU - let (gpu_device, cmd_queue, color_format) = futures::executor::block_on(async { - create_device( - &instance, - &surface, - wgpu::PowerPreference::HighPerformance, - false, - ) - .or_else(|_| create_device(&instance, &surface, wgpu::PowerPreference::LowPower, false)) - .or_else(|_| { - create_device( - &instance, - &surface, - wgpu::PowerPreference::HighPerformance, - true, - ) - }) - .unwrap_or_else(|err| { - panic!("Failed to request device: `{}`", err); - }) - .await - }); - - // Create staging belt and a local pool - let mut staging_belt = wgpu::util::StagingBelt::new(1024); - let mut local_pool = futures::executor::LocalPool::new(); - let local_spawner = local_pool.spawner(); - - let mut size = window.inner_size(); - - let surface_config = wgpu::SurfaceConfiguration { - usage: wgpu::TextureUsages::RENDER_ATTACHMENT, - format: color_format, - width: size.width, - height: size.height, - present_mode: wgpu::PresentMode::Mailbox, - }; - - surface.configure(&gpu_device, &surface_config); - - let rect_resources = pipelines::make_rect_pipeline(&gpu_device, &surface_config); - - let mut glyph_brush = build_glyph_brush(&gpu_device, color_format)?; - - let is_animating = true; - - let mut env_pool = Pool::with_capacity(1024); - let env_arena = Bump::new(); - let code_arena = Bump::new(); - - let (file_path_str, code_str) = read_main_roc_file(project_dir_path_opt); - println!("Loading file {:?}...", file_path_str); - - let file_path = Path::new(&file_path_str); - - let loaded_module = load_module(file_path, Threading::AllAvailable); - - let mut var_store = VarStore::default(); - let dep_idents = IdentIds::exposed_builtins(8); - let exposed_ident_ids = IdentIds::default(); - let module_ids = loaded_module.interns.module_ids.clone(); - - let env = Env::new( - loaded_module.module_id, - &env_arena, - &mut env_pool, - &mut var_store, - dep_idents, - &module_ids, - exposed_ident_ids, - ); - - let config: Config = Config::default(); //confy::load("roc_editor", None)?; - let ed_model_opt = { - let ed_model_res = ed_model::init_model( - &code_str, - file_path, - env, - loaded_module, - &code_arena, - CaretPos::End, - ); - - match ed_model_res { - Ok(mut ed_model) => { - ed_model.glyph_dim_rect_opt = Some(example_code_glyph_rect( - &mut glyph_brush, - config.code_font_size, - )); - - Some(ed_model) - } - Err(e) => { - print_err(&e); - None - } - } - }; - - let mut rendered_wgpu_opt: Option = None; - - let mut app_model = AppModel::init(ed_model_opt); - - let mut keyboard_modifiers = ModifiersState::empty(); - let ed_theme = EdTheme::default(); - - // Render loop - window.request_redraw(); - - event_loop.run_return(|event, _, control_flow| { - // TODO dynamically switch this on/off depending on whether any - // animations are running. Should conserve CPU usage and battery life! - if is_animating { - *control_flow = ControlFlow::Poll; - } else { - *control_flow = ControlFlow::Wait; - } - - match event { - //Close - Event::WindowEvent { - event: event::WindowEvent::CloseRequested, - .. - } => *control_flow = ControlFlow::Exit, - //Resize - Event::WindowEvent { - event: event::WindowEvent::Resized(new_size), - .. - } => { - size = new_size; - - surface.configure( - &gpu_device, - &wgpu::SurfaceConfiguration { - usage: wgpu::TextureUsages::RENDER_ATTACHMENT, - format: color_format, - width: size.width, - height: size.height, - present_mode: wgpu::PresentMode::Mailbox, - }, - ); - - update_ortho_buffer( - size.width, - size.height, - &gpu_device, - &rect_resources.ortho.buffer, - &cmd_queue, - ); - } - //Received Character - Event::WindowEvent { - event: event::WindowEvent::ReceivedCharacter(ch), - .. - } => { - let input_outcome_res = - app_update::handle_new_char(&ch, &mut app_model, keyboard_modifiers); - if let Err(e) = input_outcome_res { - print_err(&e) - } else if let Ok(InputOutcome::Ignored) = input_outcome_res { - println!("Input '{}' ignored!", ch); - } else { - window.request_redraw() - } - } - //Keyboard Input - Event::WindowEvent { - event: event::WindowEvent::KeyboardInput { input, .. }, - .. - } => { - if let Some(virtual_keycode) = input.virtual_keycode { - if let Some(ref mut ed_model) = app_model.ed_model_opt { - if ed_model.has_focus { - let keydown_res = keyboard_input::handle_keydown( - input.state, - virtual_keycode, - keyboard_modifiers, - &mut app_model, - ); - - if let Err(e) = keydown_res { - print_err(&e) - } - - window.request_redraw() - } - } - } - } - //Modifiers Changed - Event::WindowEvent { - event: event::WindowEvent::ModifiersChanged(modifiers), - .. - } => { - keyboard_modifiers = modifiers; - } - Event::RedrawRequested { .. } => { - // Get a command encoder for the current frame - let mut encoder = - gpu_device.create_command_encoder(&wgpu::CommandEncoderDescriptor { - label: Some("Redraw"), - }); - - let surface_texture = surface - .get_current_texture() - .expect("Failed to acquire next SwapChainTexture"); - - let view = surface_texture - .texture - .create_view(&wgpu::TextureViewDescriptor::default()); - - if let Some(ref mut ed_model) = app_model.ed_model_opt { - if rendered_wgpu_opt.is_none() || ed_model.dirty { - let rendered_wgpu_res = ed_view::model_to_wgpu( - ed_model, - &size, - config.make_code_txt_xy().into(), - &config, - ); - - match rendered_wgpu_res { - Ok(rendered_wgpu) => rendered_wgpu_opt = Some(rendered_wgpu), - Err(e) => print_err(&e), - } - - ed_model.dirty = false; - } - - if let Some(ref rendered_wgpu) = rendered_wgpu_opt { - draw_rects( - &rendered_wgpu.rects_behind, - &mut encoder, - &view, - &gpu_device, - &rect_resources, - wgpu::LoadOp::Clear(to_wgpu_color(ed_theme.background)), - ); - - for text_section in &rendered_wgpu.text_sections_behind { - let borrowed_text = text_section.to_borrowed(); - - glyph_brush.queue(borrowed_text); - } - - // draw first layer of text - glyph_brush - .draw_queued( - &gpu_device, - &mut staging_belt, - &mut encoder, - &view, - size.width, - size.height, - ) - .expect("Failed to draw first layer of text."); - - // draw rects on top of first text layer - draw_rects( - &rendered_wgpu.rects_front, - &mut encoder, - &view, - &gpu_device, - &rect_resources, - wgpu::LoadOp::Load, - ); - - for text_section in &rendered_wgpu.text_sections_front { - let borrowed_text = text_section.to_borrowed(); - - glyph_brush.queue(borrowed_text); - } - } - } else { - begin_render_pass( - &mut encoder, - &view, - wgpu::LoadOp::Clear(to_wgpu_color(ed_theme.background)), - ); - - queue_no_file_text( - &size, - NOTHING_OPENED, - config.make_code_txt_xy().into(), - &config, - &mut glyph_brush, - ); - } - - // draw text - glyph_brush - .draw_queued( - &gpu_device, - &mut staging_belt, - &mut encoder, - &view, - size.width, - size.height, - ) - .expect("Failed to draw queued text."); - - staging_belt.finish(); - cmd_queue.submit(Some(encoder.finish())); - surface_texture.present(); - - // Recall unused staging buffers - use futures::task::SpawnExt; - - local_spawner - .spawn(staging_belt.recall()) - .expect("Recall staging belt"); - - local_pool.run_until_stalled(); - } - _ => { - *control_flow = winit::event_loop::ControlFlow::Wait; - } - } - }); - - Ok(()) -} - -async fn create_device( - instance: &wgpu::Instance, - surface: &wgpu::Surface, - power_preference: wgpu::PowerPreference, - force_fallback_adapter: bool, -) -> Result<(wgpu::Device, wgpu::Queue, wgpu::TextureFormat), wgpu::RequestDeviceError> { - if force_fallback_adapter { - log::error!("Falling back to software renderer. GPU acceleration has been disabled."); - } - - let adapter = instance - .request_adapter(&wgpu::RequestAdapterOptions { - power_preference, - compatible_surface: Some(surface), - force_fallback_adapter, - }) - .await - .expect(r#"Request adapter - If you're running this from inside nix, follow the instructions here to resolve this: https://github.com/rtfeldman/roc/blob/trunk/BUILDING_FROM_SOURCE.md#editor - "#); - - let color_format = surface.get_preferred_format(&adapter).unwrap(); - - if color_format != wgpu::TextureFormat::Bgra8UnormSrgb { - log::warn!("Your preferred TextureFormat {:?} is different than expected. Colors may look different, please report this issue on github and tag @Anton-4.", color_format); - } - - let request_res = adapter - .request_device( - &wgpu::DeviceDescriptor { - label: None, - features: wgpu::Features::empty(), - limits: wgpu::Limits::default(), - }, - None, - ) - .await; - - match request_res { - Ok((device, queue)) => Ok((device, queue, color_format)), - Err(err) => Err(err), - } -} - -fn draw_rects( - all_rects: &[Rect], - encoder: &mut CommandEncoder, - texture_view: &TextureView, - gpu_device: &wgpu::Device, - rect_resources: &RectResources, - load_op: LoadOp, -) { - let rect_buffers = create_rect_buffers(gpu_device, encoder, all_rects); - - let mut render_pass = begin_render_pass(encoder, texture_view, load_op); - - render_pass.set_pipeline(&rect_resources.pipeline); - render_pass.set_bind_group(0, &rect_resources.ortho.bind_group, &[]); - render_pass.set_vertex_buffer(0, rect_buffers.vertex_buffer.slice(..)); - render_pass.set_index_buffer( - rect_buffers.index_buffer.slice(..), - wgpu::IndexFormat::Uint32, - ); - render_pass.draw_indexed(0..rect_buffers.num_rects, 0, 0..1); -} - -fn begin_render_pass<'a>( - encoder: &'a mut CommandEncoder, - texture_view: &'a TextureView, - load_op: LoadOp, -) -> RenderPass<'a> { - encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - color_attachments: &[wgpu::RenderPassColorAttachment { - view: texture_view, - resolve_target: None, - ops: wgpu::Operations { - load: load_op, - store: true, - }, - }], - depth_stencil_attachment: None, - label: None, - }) -} - -fn read_main_roc_file(project_dir_path_opt: Option<&Path>) -> (PathBuf, String) { - if let Some(project_dir_path) = project_dir_path_opt { - let mut ls_config = HashSet::new(); - ls_config.insert(DirEntryAttr::FullName); - - let dir_items = ls(project_dir_path, &ls_config) - .unwrap_or_else(|err| panic!("Failed to list items in project directory: {:?}", err)) - .items; - - let file_names = dir_items.iter().flat_map(|info_hash_map| { - info_hash_map - .values() - .filter_map(|dir_entry_value| { - if let DirEntryValue::String(file_name) = dir_entry_value { - Some(file_name) - } else { - None - } - }) - .collect::>() - }); - - let roc_file_names: Vec<&String> = file_names - .filter(|file_name| file_name.contains(".roc")) - .collect(); - - if let Some(&roc_file_name) = roc_file_names.first() { - let full_roc_file_path = project_dir_path.join(roc_file_name); - let file_as_str = std::fs::read_to_string(&Path::new(&full_roc_file_path)) - .unwrap_or_else(|err| panic!("In the provided project {:?}, I found the roc file {:?}, but I failed to read it: {}", &project_dir_path, full_roc_file_path, err)); - - (full_roc_file_path, file_as_str) - } else { - init_new_roc_project(project_dir_path) - } - } else { - init_new_roc_project(Path::new("./new-roc-project")) - } -} - -// returns path and content of app file -fn init_new_roc_project(project_dir_path: &Path) -> (PathBuf, String) { - let orig_platform_path = Path::new("./examples/hello-world").join(PLATFORM_NAME); - - let roc_file_path = Path::new("./new-roc-project/UntitledApp.roc"); - - let project_platform_path = project_dir_path.join(PLATFORM_NAME); - - if !project_dir_path.exists() { - fs::create_dir(project_dir_path).expect("Failed to create dir for roc project."); - } - - copy_roc_platform_if_not_exists( - &orig_platform_path, - &project_platform_path, - project_dir_path, - ); - - let code_str = create_roc_file_if_not_exists(project_dir_path, roc_file_path); - - (roc_file_path.to_path_buf(), code_str) -} - -// returns contents of file -fn create_roc_file_if_not_exists(project_dir_path: &Path, roc_file_path: &Path) -> String { - if !roc_file_path.exists() { - let mut roc_file = File::create(roc_file_path).unwrap_or_else(|err| { - panic!("No roc file path was passed to the editor, so I wanted to create a new roc project with the file {:?}, but it failed: {}", roc_file_path, err) - }); - - write!(roc_file, "{}", HELLO_WORLD).unwrap_or_else(|err| { - panic!( - r#"No roc file path was passed to the editor, so I created a new roc project with the file {:?} - I wanted to write roc hello world to that file, but it failed: {:?}"#, - roc_file_path, - err - ) - }); - - HELLO_WORLD.to_string() - } else { - std::fs::read_to_string(roc_file_path).unwrap_or_else(|err| { - panic!( - "I detected an existing {:?} inside {:?}, but I failed to read from it: {}", - roc_file_path, project_dir_path, err - ) - }) - } -} - -fn copy_roc_platform_if_not_exists( - orig_platform_path: &Path, - project_platform_path: &Path, - project_dir_path: &Path, -) { - if !orig_platform_path.exists() && !project_platform_path.exists() { - panic!( - r#"No roc file path was passed to the editor, I wanted to create a new roc project but I could not find the platform at {:?}. - Are you at the root of the roc repository?"#, - orig_platform_path - ); - } else if !project_platform_path.exists() { - copy(orig_platform_path, project_dir_path, &CopyOptions::new()).unwrap_or_else(|err|{ - panic!(r#"No roc file path was passed to the editor, so I wanted to create a new roc project and roc projects require a platform, - I tried to copy the platform at {:?} to {:?} but it failed: {}"#, - orig_platform_path, - project_platform_path, - err - ) - }); - } -} - -fn queue_no_file_text( - size: &PhysicalSize, - text: &str, - text_coords: Vector2, - config: &Config, - glyph_brush: &mut GlyphBrush<()>, -) { - let area_bounds = (size.width as f32, size.height as f32).into(); - - let code_text = Text { - position: text_coords, - area_bounds, - color: config.ed_theme.ui_theme.text, - text, - size: config.code_font_size, - ..Default::default() - }; - - queue_text_draw(&code_text, glyph_brush); -} diff --git a/editor/src/editor/mod.rs b/editor/src/editor/mod.rs deleted file mode 100644 index 23d273400d..0000000000 --- a/editor/src/editor/mod.rs +++ /dev/null @@ -1,14 +0,0 @@ -mod code_lines; -mod config; -pub mod ed_error; -mod grid_node_map; -mod keyboard_input; -pub mod main; -mod mvc; -mod render_ast; -mod render_debug; -mod resources; -#[cfg(feature = "with_sound")] -mod sound; -mod theme; -mod util; diff --git a/editor/src/editor/mvc/app_model.rs b/editor/src/editor/mvc/app_model.rs deleted file mode 100644 index 357ab617de..0000000000 --- a/editor/src/editor/mvc/app_model.rs +++ /dev/null @@ -1,108 +0,0 @@ -#![allow(dead_code)] - -use super::ed_model::EdModel; -use crate::editor::ed_error::{ - print_err, - EdError::{ClipboardInitFailed, ClipboardReadFailed, ClipboardWriteFailed}, - EdResult, -}; -use copypasta::{ClipboardContext, ClipboardProvider}; -use std::fmt; -use threadpool::ThreadPool; - -pub struct AppModel<'a> { - pub ed_model_opt: Option>, - pub clipboard_opt: Option, - pub sound_thread_pool: ThreadPool, // thread is blocked while sound is played, hence the threadpool -} - -impl<'a> AppModel<'a> { - pub fn init(ed_model_opt: Option>) -> AppModel { - AppModel { - ed_model_opt, - clipboard_opt: AppModel::init_clipboard_opt(), - sound_thread_pool: ThreadPool::new(7), // can play up to 7 sounds simultaneously - } - } - - pub fn init_clipboard_opt() -> Option { - let clipboard_res = Clipboard::init(); - - match clipboard_res { - Ok(clipboard) => Some(clipboard), - Err(e) => { - print_err(&e); - None - } - } - } -} - -pub struct Clipboard { - context: ClipboardContext, -} - -impl Clipboard { - pub fn init() -> EdResult { - let context_res = ClipboardContext::new(); - - match context_res { - Ok(context) => Ok(Clipboard { context }), - Err(e) => Err(ClipboardInitFailed { - err_msg: e.to_string(), - }), - } - } - - // clipboard crate needs this to be mutable - pub fn get_content(&mut self) -> EdResult { - let content_res = self.context.get_contents(); - - match content_res { - Ok(content_str) => Ok(content_str), - Err(e) => Err(ClipboardReadFailed { - err_msg: e.to_string(), - }), - } - } - - pub fn set_content(&mut self, copy_str: String) -> EdResult<()> { - let content_set_res = self.context.set_contents(copy_str); - - match content_set_res { - Ok(_) => Ok(()), - Err(e) => Err(ClipboardWriteFailed { - err_msg: e.to_string(), - }), - } - } -} - -pub fn set_clipboard_txt(clipboard_opt: &mut Option, txt: &str) -> EdResult<()> { - if let Some(ref mut clipboard) = clipboard_opt { - clipboard.set_content(txt.to_owned())?; - } else { - return Err(ClipboardWriteFailed { - err_msg: "Clipboard was never initialized successfully.".to_owned(), - }); - } - - Ok(()) -} - -pub fn get_clipboard_txt(clipboard_opt: &mut Option) -> EdResult { - if let Some(ref mut clipboard) = clipboard_opt { - clipboard.get_content() - } else { - Err(ClipboardReadFailed { - err_msg: "Clipboard was never initialized successfully.".to_owned(), - }) - } -} - -impl fmt::Debug for Clipboard { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - // showing the clipboard would require a mut ref which is not possible - f.debug_struct("Clipboard (can't show)").finish() - } -} diff --git a/editor/src/editor/mvc/app_update.rs b/editor/src/editor/mvc/app_update.rs deleted file mode 100644 index 37715440ad..0000000000 --- a/editor/src/editor/mvc/app_update.rs +++ /dev/null @@ -1,229 +0,0 @@ -use super::app_model::AppModel; -use super::ed_update; -use crate::window::keyboard_input::Modifiers; -use crate::{editor::ed_error::EdResult, window::keyboard_input::from_winit}; -use winit::event::{ModifiersState, VirtualKeyCode}; - -pub fn handle_copy(app_model: &mut AppModel) -> EdResult<()> { - if let Some(ref mut ed_model) = app_model.ed_model_opt { - if ed_model.has_focus { - unimplemented!("TODO"); - } - } - - Ok(()) -} - -pub fn handle_paste(app_model: &mut AppModel) -> EdResult<()> { - if let Some(ref mut ed_model) = app_model.ed_model_opt { - if ed_model.has_focus { - unimplemented!("TODO"); - } - } - - Ok(()) -} - -pub fn handle_cut(app_model: &mut AppModel) -> EdResult<()> { - if let Some(ref mut ed_model) = app_model.ed_model_opt { - if ed_model.has_focus { - unimplemented!("TODO"); - } - } - - Ok(()) -} - -pub fn pass_keydown_to_focused( - modifiers: &Modifiers, - virtual_keycode: VirtualKeyCode, - app_model: &mut AppModel, -) -> EdResult<()> { - if let Some(ref mut ed_model) = app_model.ed_model_opt { - if ed_model.has_focus { - ed_model.ed_handle_key_down( - modifiers, - virtual_keycode, - &mut app_model.sound_thread_pool, - )?; - } - } - - Ok(()) -} - -#[derive(Debug)] -pub enum InputOutcome { - Accepted, - Ignored, - SilentIgnored, -} - -pub fn handle_new_char( - received_char: &char, - app_model: &mut AppModel, - modifiers_winit: ModifiersState, -) -> EdResult { - if let Some(ref mut ed_model) = app_model.ed_model_opt { - if ed_model.has_focus { - let modifiers = from_winit(&modifiers_winit); - - if modifiers.new_char_modifiers() { - // shortcuts with modifiers are handled by ed_handle_key_down - return ed_update::handle_new_char(received_char, ed_model); - } - } - } - - Ok(InputOutcome::SilentIgnored) -} - -/* -#[cfg(test)] -pub mod test_app_update { - use crate::editor::mvc::app_model; - use crate::editor::mvc::app_model::{AppModel, Clipboard}; - use crate::editor::mvc::app_update::{handle_copy, handle_cut, handle_paste}; - use crate::editor::mvc::ed_model::EdModel; - use crate::ui::text::{ - big_selectable_text::test_big_sel_text::{ - all_lines_vec, convert_selection_to_dsl, gen_big_text, - }, - big_selectable_text::BigSelectableText, - }; - - pub fn mock_app_model( - big_sel_text: BigSelectableText, - clipboard_opt: Option, - ) -> AppModel { - AppModel { - ed_model_opt: Some(EdModel { - text: big_sel_text, - glyph_dim_rect_opt: None, - has_focus: true, - }), - clipboard_opt, - } - } - - fn assert_copy( - pre_lines_str: &[&str], - expected_clipboard_content: &str, - clipboard_opt: Option, - ) -> Result, String> { - let pre_text_buf = gen_big_text(pre_lines_str)?; - - let mut app_model = mock_app_model(pre_text_buf, clipboard_opt); - - handle_copy(&mut app_model)?; - - let clipboard_content = app_model::get_clipboard_txt(&mut app_model.clipboard_opt)?; - - assert_eq!(clipboard_content, expected_clipboard_content); - - Ok(app_model.clipboard_opt) - } - - fn assert_paste( - pre_lines_str: &[&str], - clipboard_content: &str, - expected_post_lines_str: &[&str], - clipboard_opt: Option, - ) -> Result, String> { - let pre_big_text = gen_big_text(pre_lines_str)?; - - let mut app_model = mock_app_model(pre_big_text, clipboard_opt); - - app_model::set_clipboard_txt(&mut app_model.clipboard_opt, clipboard_content)?; - - handle_paste(&mut app_model)?; - - let ed_model = app_model.ed_model_opt.unwrap(); - let mut text_lines = all_lines_vec(&ed_model.text); - let post_lines_str = - convert_selection_to_dsl(ed_model.text.caret_w_select, &mut text_lines)?; - - assert_eq!(post_lines_str, expected_post_lines_str); - - Ok(app_model.clipboard_opt) - } - - fn assert_cut( - pre_lines_str: &[&str], - expected_clipboard_content: &str, - expected_post_lines_str: &[&str], - clipboard_opt: Option, - ) -> Result, String> { - let pre_big_text = gen_big_text(pre_lines_str)?; - - let mut app_model = mock_app_model(pre_big_text, clipboard_opt); - - handle_cut(&mut app_model)?; - - let clipboard_content = app_model::get_clipboard_txt(&mut app_model.clipboard_opt)?; - - assert_eq!(clipboard_content, expected_clipboard_content); - - let ed_model = app_model.ed_model_opt.unwrap(); - let mut text_lines = all_lines_vec(&ed_model.text); - let post_lines_str = - convert_selection_to_dsl(ed_model.text.caret_w_select, &mut text_lines)?; - - assert_eq!(post_lines_str, expected_post_lines_str); - - Ok(app_model.clipboard_opt) - } - - #[test] - #[ignore] // ignored because of clipboard problems on ci - fn copy_paste_cut() -> Result<(), String> { - // can only init clipboard once - let mut clipboard_opt = AppModel::init_clipboard_opt(); - - // copy - clipboard_opt = assert_copy(&["[a]|"], "a", clipboard_opt)?; - clipboard_opt = assert_copy(&["|[b]"], "b", clipboard_opt)?; - clipboard_opt = assert_copy(&["a[ ]|"], " ", clipboard_opt)?; - clipboard_opt = assert_copy(&["[ ]|b"], " ", clipboard_opt)?; - clipboard_opt = assert_copy(&["a\n", "[b\n", "]|"], "b\n", clipboard_opt)?; - clipboard_opt = assert_copy(&["[a\n", " b\n", "]|"], "a\n b\n", clipboard_opt)?; - clipboard_opt = assert_copy( - &["abc\n", "d[ef\n", "ghi]|\n", "jkl"], - "ef\nghi", - clipboard_opt, - )?; - - // paste - - clipboard_opt = assert_paste(&["|"], "", &["|"], clipboard_opt)?; - clipboard_opt = assert_paste(&["|"], "a", &["a|"], clipboard_opt)?; - clipboard_opt = assert_paste(&["a|"], "b", &["ab|"], clipboard_opt)?; - clipboard_opt = assert_paste(&["|a"], "b", &["b|a"], clipboard_opt)?; - clipboard_opt = assert_paste(&["[a]|"], "c", &["c|"], clipboard_opt)?; - clipboard_opt = assert_paste(&["[ab]|"], "d", &["d|"], clipboard_opt)?; - clipboard_opt = assert_paste(&["a[b]|c"], "e", &["ae|c"], clipboard_opt)?; - clipboard_opt = assert_paste(&["a\n", "[b\n", "]|"], "f", &["a\n", "f|"], clipboard_opt)?; - clipboard_opt = assert_paste( - &["abc\n", "d[ef\n", "ghi]|\n", "jkl"], - "ef\nghi", - &["abc\n", "def\n", "ghi|\n", "jkl"], - clipboard_opt, - )?; - - // cut - clipboard_opt = assert_cut(&["[a]|"], "a", &["|"], clipboard_opt)?; - clipboard_opt = assert_cut(&["|[b]"], "b", &["|"], clipboard_opt)?; - clipboard_opt = assert_cut(&["a[ ]|"], " ", &["a|"], clipboard_opt)?; - clipboard_opt = assert_cut(&["[ ]|b"], " ", &["|b"], clipboard_opt)?; - clipboard_opt = assert_cut(&["a\n", "[b\n", "]|"], "b\n", &["a\n", "|"], clipboard_opt)?; - clipboard_opt = assert_cut(&["[a\n", " b\n", "]|"], "a\n b\n", &["|"], clipboard_opt)?; - assert_cut( - &["abc\n", "d[ef\n", "ghi]|\n", "jkl"], - "ef\nghi", - &["abc\n", "d|\n", "jkl"], - clipboard_opt, - )?; - - Ok(()) - } -}*/ diff --git a/editor/src/editor/mvc/break_line.rs b/editor/src/editor/mvc/break_line.rs deleted file mode 100644 index c5264d7a9e..0000000000 --- a/editor/src/editor/mvc/break_line.rs +++ /dev/null @@ -1,63 +0,0 @@ -use roc_ast::lang::core::def::def2::Def2; -use roc_code_markup::markup::common_nodes::NEW_LINES_AFTER_DEF; - -use crate::editor::ed_error::EdResult; -use crate::editor::mvc::app_update::InputOutcome; -use crate::editor::mvc::ed_model::EdModel; -use crate::editor::util::index_of; -use crate::ui::text::lines::Lines; -use crate::ui::text::text_pos::TextPos; - -// put everything after caret on new line, create a Def2::Blank if there was nothing after the caret. -pub fn break_line(ed_model: &mut EdModel) -> EdResult { - let carets = ed_model.get_carets(); - - for caret_pos in carets.iter() { - let caret_line_nr = caret_pos.line; - - // don't allow adding new lines on empty line - if caret_pos.column > 0 - && ed_model.grid_node_map.node_exists_at_pos(TextPos { - line: caret_line_nr, - column: caret_pos.column - 1, - }) - { - let new_blank_line_nr = caret_line_nr + NEW_LINES_AFTER_DEF; - // if there already is a blank line at new_blank_line_nr just move the caret there, don't add extra lines - // safe unwrap, we already checked the nr_of_lines - if !(ed_model.code_lines.nr_of_lines() >= new_blank_line_nr - && ed_model.code_lines.line_len(new_blank_line_nr).unwrap() == 0) - { - for i in 1..=NEW_LINES_AFTER_DEF { - EdModel::insert_empty_line(caret_line_nr + i, &mut ed_model.grid_node_map)?; - } - - insert_new_blank(ed_model, caret_pos.line + NEW_LINES_AFTER_DEF + 1)?; - } - } - } - - ed_model.simple_move_carets_down(NEW_LINES_AFTER_DEF); // one blank lines between top level definitions - - Ok(InputOutcome::Accepted) -} - -pub fn insert_new_blank(ed_model: &mut EdModel, insert_on_line_nr: usize) -> EdResult<()> { - // find position of the previous ASTNode to figure out where to add this new Blank ASTNode - let def_mark_node_id = ed_model.grid_node_map.get_def_mark_node_id_before_line( - insert_on_line_nr, - &ed_model.mark_node_pool, - &ed_model.mark_id_ast_id_map, - )?; - - let new_line_blank = Def2::Blank; - let new_line_blank_id = ed_model.module.env.pool.add(new_line_blank); - - let insertion_index = index_of(def_mark_node_id, &ed_model.markup_ids)?; - ed_model - .module - .ast - .insert_def_at_index(new_line_blank_id, insertion_index); - - Ok(()) -} diff --git a/editor/src/editor/mvc/ed_model.rs b/editor/src/editor/mvc/ed_model.rs deleted file mode 100644 index b557623ffd..0000000000 --- a/editor/src/editor/mvc/ed_model.rs +++ /dev/null @@ -1,361 +0,0 @@ -use crate::editor::code_lines::CodeLines; -use crate::editor::grid_node_map::GridNodeMap; -use crate::editor::{ - ed_error::SrcParseError, - ed_error::{EdResult, EmptyCodeString, MissingParent, NoNodeAtCaretPosition}, -}; -use crate::graphics::primitives::rect::Rect; -use crate::ui::text::caret_w_select::{CaretPos, CaretWSelect}; -use crate::ui::text::lines::SelectableLines; -use crate::ui::text::text_pos::TextPos; -use crate::ui::ui_error::UIResult; -use bumpalo::Bump; -use nonempty::NonEmpty; -use roc_ast::lang::core::ast::{ASTNodeId, AST}; -use roc_ast::lang::env::Env; -use roc_ast::mem_pool::pool_str::PoolStr; -use roc_ast::parse::parse_ast; -use roc_code_markup::markup::convert::from_ast::ast_to_mark_nodes; -use roc_code_markup::markup::mark_id_ast_id_map::MarkIdAstIdMap; -use roc_code_markup::markup::nodes; -use roc_code_markup::slow_pool::{MarkNodeId, SlowPool}; -use roc_load::LoadedModule; -use roc_module::symbol::Interns; -use std::path::Path; - -/// Contains nearly all state related to a single roc file in the editor. -#[derive(Debug)] -pub struct EdModel<'a> { - pub module: EdModule<'a>, // contains Abstract Syntax Tree of code - pub file_path: &'a Path, - pub code_lines: CodeLines, // Vec of all code, this Vec is written to disk when saving a file. - pub grid_node_map: GridNodeMap, // allows us to map window coordinates to MarkNodeId's - pub markup_ids: Vec, // one root node for every top level definition - pub mark_node_pool: SlowPool, // all MarkupNodes for this file are saved into this pool and can be retrieved using their MarkNodeId - pub mark_id_ast_id_map: MarkIdAstIdMap, // To find the ASTNode that is represented by a MarkNode - pub glyph_dim_rect_opt: Option, // represents the width and height of single monospace glyph(char) - pub has_focus: bool, - pub caret_w_select_vec: NonEmpty<(CaretWSelect, Option)>, // the editor supports multiple carets/cursors and multiple selections - pub selected_block_opt: Option, // a selected AST node, the roc type of this node is shown in the editor on ctrl+shift+"up arrow" - pub loaded_module: LoadedModule, // contains all roc symbols, exposed values, exposed aliases, solved types... in the file(=module) - pub show_debug_view: bool, // see render_debug.rs for the debug view - pub dirty: bool, // EdModel is dirty if it has changed since the previous render. -} - -// a selected AST node, the roc type of this node is shown in the editor on ctrl+shift+"up arrow" -#[derive(Debug, Copy, Clone)] -pub struct SelectedBlock { - pub ast_node_id: ASTNodeId, - pub mark_node_id: MarkNodeId, - pub type_str: PoolStr, -} - -pub fn init_model<'a>( - code_str: &'a str, // entire roc file as one str - file_path: &'a Path, - env: Env<'a>, // contains all variables, identifiers, closures, top level symbols... - loaded_module: LoadedModule, // contains all roc symbols, exposed values, exposed aliases, solved types... in the file(=module) - code_arena: &'a Bump, // bump allocation arena, used for fast memory allocation - caret_pos: CaretPos, // to set caret position when the file is displayed -) -> EdResult> { - // for debugging - //println!("{}", code_str); - let mut owned_loaded_module = loaded_module; - - let mut module = EdModule::new(code_str, env, &mut owned_loaded_module.interns, code_arena)?; - - let mut mark_node_pool = SlowPool::default(); - - let (markup_ids, mark_id_ast_id_map) = if code_str.is_empty() { - EmptyCodeString {}.fail() - } else { - Ok(ast_to_mark_nodes( - &mut module.env, - &module.ast, - &mut mark_node_pool, - &owned_loaded_module.interns, - )?) - }?; - - let code_lines = - CodeLines::from_str(&nodes::mark_nodes_to_string(&markup_ids, &mark_node_pool)); - let mut grid_node_map = GridNodeMap::default(); - - let mut line_nr = 0; - let mut col_nr = 0; - - for mark_node_id in &markup_ids { - // for debugging: - //println!("{}", tree_as_string(*mark_node_id, &mark_node_pool)); - EdModel::insert_mark_node_between_line( - &mut line_nr, - &mut col_nr, - *mark_node_id, - &mut grid_node_map, - &mark_node_pool, - )? - } - - let caret = match caret_pos { - CaretPos::Start => CaretWSelect::default(), - CaretPos::Exact(txt_pos) => CaretWSelect::new(txt_pos, None), - CaretPos::End => CaretWSelect::new(code_lines.end_txt_pos(), None), - }; - - Ok(EdModel { - module, - file_path, - code_lines, - grid_node_map, - markup_ids, - mark_node_pool, - mark_id_ast_id_map, - glyph_dim_rect_opt: None, - has_focus: true, - caret_w_select_vec: NonEmpty::new((caret, None)), - selected_block_opt: None, - loaded_module: owned_loaded_module, - show_debug_view: false, - dirty: true, - }) -} - -impl<'a> EdModel<'a> { - pub fn get_carets(&self) -> Vec { - self.caret_w_select_vec - .iter() - .map(|tup| tup.0.caret_pos) - .collect() - } - - pub fn get_curr_mark_node_id(&self) -> UIResult { - let caret_pos = self.get_caret(); - self.grid_node_map.get_id_at_row_col(caret_pos) - } - - // get id of MarkNode that is located before the caret - pub fn get_prev_mark_node_id(&self) -> UIResult> { - let caret_pos = self.get_caret(); - - let prev_id_opt = if caret_pos.column > 0 { - let prev_mark_node_id = self.grid_node_map.get_id_at_row_col(TextPos { - line: caret_pos.line, - column: caret_pos.column - 1, - })?; - - Some(prev_mark_node_id) - } else { - None - }; - - Ok(prev_id_opt) - } - - pub fn node_exists_at_caret(&self) -> bool { - self.grid_node_map.node_exists_at_pos(self.get_caret()) - } - - // return (index of child in list of children, closest ast index of child corresponding to ast node) of MarkupNode at current caret position - pub fn get_curr_child_indices(&self) -> EdResult<(usize, usize)> { - if self.node_exists_at_caret() { - let curr_mark_node_id = self.get_curr_mark_node_id()?; - let curr_mark_node = self.mark_node_pool.get(curr_mark_node_id); - - if let Some(parent_id) = curr_mark_node.get_parent_id_opt() { - let parent = self.mark_node_pool.get(parent_id); - let ast_node_id = self.mark_id_ast_id_map.get(curr_mark_node_id)?; - Ok(parent.get_child_indices( - curr_mark_node_id, - ast_node_id, - &self.mark_id_ast_id_map, - )?) - } else { - MissingParent { - node_id: curr_mark_node_id, - } - .fail() - } - } else { - NoNodeAtCaretPosition { - caret_pos: self.get_caret(), - } - .fail() - } - } -} - -#[derive(Debug)] -pub struct EdModule<'a> { - pub env: Env<'a>, - pub ast: AST, -} - -// for debugging -//use crate::lang::ast::expr2_to_string; - -impl<'a> EdModule<'a> { - pub fn new( - code_str: &'a str, - mut env: Env<'a>, - interns: &mut Interns, // contains ids of all identifiers in this roc file - ast_arena: &'a Bump, - ) -> EdResult> { - if !code_str.is_empty() { - let parse_res = parse_ast::parse_from_string(code_str, &mut env, ast_arena, interns); - - match parse_res { - Ok(ast) => Ok(EdModule { env, ast }), - Err(err) => SrcParseError { - syntax_err: format!("{:?}", err), - } - .fail(), - } - } else { - EmptyCodeString {}.fail() - } - } -} - -#[cfg(test)] -pub mod test_ed_model { - use crate::editor::ed_error::EdResult; - use crate::editor::mvc::ed_model; - use crate::editor::resources::strings::{ - nr_hello_world_lines, HELLO_WORLD, PLATFORM_NAME, PLATFORM_STR, - }; - use crate::ui::text::caret_w_select::test_caret_w_select::convert_dsl_to_selection; - use crate::ui::text::caret_w_select::test_caret_w_select::convert_selection_to_dsl; - use crate::ui::text::caret_w_select::CaretPos; - use crate::ui::text::lines::SelectableLines; - use crate::ui::text::text_pos::TextPos; - use crate::ui::ui_error::UIResult; - use bumpalo::Bump; - use ed_model::EdModel; - use roc_ast::lang::env::Env; - use roc_ast::mem_pool::pool::Pool; - use roc_ast::module::load_module; - use roc_load::{LoadedModule, Threading}; - use roc_module::symbol::IdentIds; - use roc_module::symbol::ModuleIds; - use roc_types::subs::VarStore; - use std::fs; - use std::fs::File; - use std::io::Write; - use std::path::Path; - use std::path::PathBuf; - use tempfile::tempdir; - use uuid::Uuid; - - pub fn init_dummy_model<'a>( - code_str: &'a str, - loaded_module: LoadedModule, - module_ids: &'a ModuleIds, - ed_model_refs: &'a mut EdModelRefs, - code_arena: &'a Bump, - ) -> EdResult> { - let file_path = Path::new(""); - - let dep_idents = IdentIds::exposed_builtins(8); - let exposed_ident_ids = IdentIds::default(); - - let env = Env::new( - loaded_module.module_id, - &ed_model_refs.env_arena, - &mut ed_model_refs.env_pool, - &mut ed_model_refs.var_store, - dep_idents, - module_ids, - exposed_ident_ids, - ); - - ed_model::init_model( - code_str, - file_path, - env, - loaded_module, - code_arena, - CaretPos::End, - ) - } - - pub struct EdModelRefs { - env_arena: Bump, - env_pool: Pool, - var_store: VarStore, - } - - pub fn init_model_refs() -> EdModelRefs { - EdModelRefs { - env_arena: Bump::new(), - env_pool: Pool::with_capacity(1024), - var_store: VarStore::default(), - } - } - - // We use a Domain Specific Language to clearly represent some of the editor's state. - // Here we convert that DSL to an EdModel. - // Example of dsl: "val = ┃❮5❯", 5 is selected and the caret is located before the 5. - pub fn ed_model_from_dsl<'a>( - clean_code_str: &'a mut String, - code_lines: Vec, - ed_model_refs: &'a mut EdModelRefs, - module_ids: &'a ModuleIds, - code_arena: &'a Bump, - ) -> Result, String> { - // to be able to load the code as a LoadedModule we add a roc app header and a main function - *clean_code_str = vec![HELLO_WORLD, clean_code_str.as_str()].join(""); - // for debugging - //println!("{}", clean_code_str); - - let temp_dir = tempdir().expect("Failed to create temporary directory for test."); - - let platform_dir = temp_dir.path().join(PLATFORM_NAME); - fs::create_dir(platform_dir.clone()).expect("Failed to create platform directory"); - let package_config_path = platform_dir.join("Package-Config.roc"); - let mut package_config_file = - File::create(package_config_path).expect("Failed to create Package-Config.roc"); - writeln!(package_config_file, "{}", PLATFORM_STR) - .expect("Failed to write to Package-Config.roc"); - - let temp_file_path_buf = - PathBuf::from([Uuid::new_v4().to_string(), ".roc".to_string()].join("")); - let temp_file_full_path = temp_dir.path().join(temp_file_path_buf); - - let mut file = File::create(temp_file_full_path.clone()).unwrap_or_else(|_| { - panic!( - "Failed to create temporary file for path {:?}", - temp_file_full_path - ) - }); - writeln!(file, "{}", clean_code_str) - .unwrap_or_else(|_| panic!("Failed to write {:?} to file: {:?}", clean_code_str, file)); - - let loaded_module = load_module(&temp_file_full_path, Threading::AllAvailable); - - let mut ed_model = init_dummy_model( - clean_code_str, - loaded_module, - module_ids, - ed_model_refs, - code_arena, - )?; - - // adjust caret for header and main function - let caret_w_select = convert_dsl_to_selection(&code_lines)?; - let adjusted_caret_pos = TextPos { - line: caret_w_select.caret_pos.line + nr_hello_world_lines(), - column: caret_w_select.caret_pos.column, - }; - - ed_model.set_caret(adjusted_caret_pos); - - Ok(ed_model) - } - - pub fn ed_model_to_dsl(ed_model: &EdModel) -> UIResult> { - let caret_w_select = ed_model.caret_w_select_vec.first().0; - let code_lines = ed_model.code_lines.lines.clone(); - - convert_selection_to_dsl(caret_w_select, code_lines) - } -} diff --git a/editor/src/editor/mvc/ed_update.rs b/editor/src/editor/mvc/ed_update.rs deleted file mode 100644 index a7db882873..0000000000 --- a/editor/src/editor/mvc/ed_update.rs +++ /dev/null @@ -1,3475 +0,0 @@ -#![allow(dead_code)] - -use std::process::Command; -use std::process::Stdio; - -use crate::editor::code_lines::CodeLines; -use crate::editor::ed_error::EdResult; -use crate::editor::ed_error::{MissingSelection, RocCheckFailed}; -use crate::editor::grid_node_map::GridNodeMap; -use crate::editor::mvc::app_update::InputOutcome; -use crate::editor::mvc::ed_model::EdModel; -use crate::editor::mvc::ed_model::SelectedBlock; -use crate::editor::mvc::int_update::start_new_int; -use crate::editor::mvc::int_update::update_int; -use crate::editor::mvc::list_update::{add_blank_child, start_new_list}; -use crate::editor::mvc::lookup_update::update_invalid_lookup; -use crate::editor::mvc::record_update::start_new_record; -use crate::editor::mvc::record_update::update_empty_record; -use crate::editor::mvc::record_update::update_record_colon; -use crate::editor::mvc::record_update::update_record_field; -use crate::editor::mvc::string_update::start_new_string; -use crate::editor::mvc::string_update::update_small_string; -use crate::editor::mvc::string_update::update_string; -use crate::editor::mvc::tld_value_update::{start_new_tld_value, update_tld_val_name}; -#[cfg(feature = "with_sound")] -use crate::editor::sound::play_sound; -use crate::ui::text::caret_w_select::CaretWSelect; -use crate::ui::text::lines::MoveCaretFun; -use crate::ui::text::selection::validate_raw_sel; -use crate::ui::text::selection::RawSelection; -use crate::ui::text::selection::Selection; -use crate::ui::text::text_pos::TextPos; -use crate::ui::text::{lines, lines::Lines, lines::SelectableLines}; -use crate::ui::ui_error::UIResult; -use crate::ui::util::path_to_string; -use crate::ui::util::write_to_file; -use crate::window::keyboard_input::Modifiers; -use bumpalo::Bump; -use roc_ast::constrain::constrain_expr; -use roc_ast::constrain::Constraint; -use roc_ast::lang::core::ast::ASTNodeId; -use roc_ast::lang::core::def::def2::Def2; -use roc_ast::lang::core::def::def2::DefId; -use roc_ast::lang::core::expr::expr2::Expr2; -use roc_ast::lang::core::expr::expr2::ExprId; -use roc_ast::lang::core::types::Type2; -use roc_ast::mem_pool::pool::Pool; -use roc_ast::mem_pool::pool_str::PoolStr; -use roc_ast::solve_type; -use roc_can::expected::Expected; -use roc_code_markup::markup::attribute::Attributes; -use roc_code_markup::markup::convert::from_ast::ast_to_mark_nodes; -use roc_code_markup::markup::nodes; -use roc_code_markup::markup::nodes::MarkupNode; -use roc_code_markup::markup::nodes::EQUALS; -use roc_code_markup::slow_pool::MarkNodeId; -use roc_code_markup::slow_pool::SlowPool; -use roc_collections::all::MutMap; -use roc_module::ident::Lowercase; -use roc_module::symbol::Symbol; -use roc_region::all::Region; -use roc_types::pretty_print::name_and_print_var; -use roc_types::pretty_print::DebugPrint; -use roc_types::solved_types::Solved; -use roc_types::subs::{Subs, VarStore, Variable}; -use snafu::OptionExt; -use threadpool::ThreadPool; -use winit::event::VirtualKeyCode; -use VirtualKeyCode::*; - -use super::break_line::break_line; -use super::break_line::insert_new_blank; -use super::let_update::start_new_let_value; - -/// ed_update.rs contains all functions that change the ed_model. -/// Additions and deletions of new characters to the editor are handled here. -/// A large percentage of the editor's tests are at the end of this file. -impl<'a> EdModel<'a> { - pub fn move_caret( - &mut self, - move_fun: MoveCaretFun, - modifiers: &Modifiers, - ) -> UIResult<()> { - self.dirty = true; - - for caret_tup in self.caret_w_select_vec.iter_mut() { - caret_tup.0 = move_fun(&self.code_lines, caret_tup.0, modifiers)?; - caret_tup.1 = None; - } - self.selected_block_opt = None; - - Ok(()) - } - - // disregards EdModel.code_lines because the caller knows the resulting caret position will be valid. - // allows us to prevent multiple updates to EdModel.code_lines - pub fn simple_move_carets_right(&mut self, repeat: usize) { - for caret_tup in self.caret_w_select_vec.iter_mut() { - caret_tup.0.caret_pos.column += repeat; - caret_tup.1 = None; - } - } - - // disregards EdModel.code_lines because the caller knows the resulting caret position will be valid. - // allows us to prevent multiple updates to EdModel.code_lines - // TODO error if no match was found for old_caret_pos - pub fn simple_move_caret_right(&mut self, old_caret_pos: TextPos, repeat: usize) { - for caret_tup in self.caret_w_select_vec.iter_mut() { - if caret_tup.0.caret_pos == old_caret_pos { - caret_tup.0.caret_pos.column += repeat; - caret_tup.1 = None; - } - } - } - - // disregards EdModel.code_lines because the caller knows the resulting caret position will be valid. - // allows us to prevent multiple updates to EdModel.code_lines - pub fn simple_move_carets_left(&mut self, repeat: usize) { - for caret_tup in self.caret_w_select_vec.iter_mut() { - caret_tup.0.caret_pos.column -= repeat; - caret_tup.1 = None; - } - } - - // disregards EdModel.code_lines because the caller knows the resulting caret position will be valid. - // allows us to prevent multiple updates to EdModel.code_lines - pub fn simple_move_carets_down(&mut self, repeat: usize) { - for caret_tup in self.caret_w_select_vec.iter_mut() { - caret_tup.0.caret_pos.column = 0; - caret_tup.0.caret_pos.line += repeat; - caret_tup.1 = None; - } - } - - // disregards EdModel.code_lines because the caller knows the resulting caret position will be valid. - // allows us to prevent multiple updates to EdModel.code_lines - // TODO error if no match was found for old_caret_pos - pub fn simple_move_caret_down(&mut self, old_caret_pos: TextPos, repeat: usize) { - for caret_tup in self.caret_w_select_vec.iter_mut() { - if caret_tup.0.caret_pos == old_caret_pos { - caret_tup.0.caret_pos.column = 0; - caret_tup.0.caret_pos.line += repeat; - caret_tup.1 = None; - } - } - } - - // disregards EdModel.code_lines because the caller knows the resulting caret position will be valid. - // allows us to prevent multiple updates to EdModel.code_lines - pub fn simple_move_carets_up(&mut self, repeat: usize) { - for caret_tup in self.caret_w_select_vec.iter_mut() { - caret_tup.0.caret_pos.line -= repeat; - caret_tup.1 = None; - } - } - - pub fn add_mark_node(&mut self, node: MarkupNode) -> MarkNodeId { - self.mark_node_pool.add(node) - } - - fn build_markup_string( - node_id: MarkNodeId, - all_code_string: &mut String, - mark_node_pool: &SlowPool, - ) -> EdResult<()> { - let node = mark_node_pool.get(node_id); - - if node.is_nested() { - for child_id in node.get_children_ids() { - EdModel::build_markup_string(child_id, all_code_string, mark_node_pool)?; - } - } else { - let node_content_str = node.get_content(); - - all_code_string.push_str(&node_content_str); - } - - for _ in 0..node.get_newlines_at_end() { - all_code_string.push('\n'); - } - - Ok(()) - } - - // updates grid_node_map and code_lines but nothing else. - pub fn insert_between_line( - line_nr: usize, - index: usize, - new_str: &str, - node_id: MarkNodeId, - grid_node_map: &mut GridNodeMap, - ) -> UIResult<()> { - grid_node_map.insert_between_line(line_nr, index, new_str.len(), node_id) - } - - pub fn insert_all_between_line( - &mut self, - line_nr: usize, - index: usize, - leaf_node_ids: &[MarkNodeId], - ) -> UIResult<()> { - let mut col_nr = index; - let mut curr_line_nr = line_nr; - - for &node_id in leaf_node_ids { - let mark_node = self.mark_node_pool.get(node_id); - let node_full_content = mark_node.get_full_content(); - - if node_full_content.contains('\n') { - //insert separate lines separately - let split_lines = node_full_content.split('\n'); - - for line in split_lines { - self.grid_node_map.insert_between_line( - curr_line_nr, - col_nr, - line.len(), - node_id, - )?; - - curr_line_nr += 1; - col_nr = 0; - } - } else { - let node_content = mark_node.get_content(); - - self.grid_node_map.insert_between_line( - line_nr, - col_nr, - node_content.len(), - node_id, - )?; - - col_nr += node_content.len(); - } - } - - Ok(()) - } - - pub fn insert_mark_node_between_line( - line_nr: &mut usize, - col_nr: &mut usize, - mark_node_id: MarkNodeId, - grid_node_map: &mut GridNodeMap, - mark_node_pool: &SlowPool, - ) -> UIResult<()> { - let mark_node = mark_node_pool.get(mark_node_id); - - let node_newlines = mark_node.get_newlines_at_end(); - - if mark_node.is_nested() { - let children_ids = mark_node.get_children_ids(); - - for child_id in children_ids { - EdModel::insert_mark_node_between_line( - line_nr, - col_nr, - child_id, - grid_node_map, - mark_node_pool, - )?; - } - } else { - let node_content = mark_node.get_content(); - - EdModel::insert_between_line( - *line_nr, - *col_nr, - &node_content, - mark_node_id, - grid_node_map, - )?; - - *col_nr += node_content.len(); - } - - if node_newlines > 0 { - EdModel::break_line(*line_nr, *col_nr, grid_node_map)?; - - *line_nr += 1; - *col_nr = 0; - - for _ in 1..node_newlines { - EdModel::insert_empty_line(*line_nr, grid_node_map)?; - - *line_nr += 1; - *col_nr = 0; - } - } - - Ok(()) - } - - // break(split) line at col_nr and move everything after col_nr to the next line - pub fn break_line( - line_nr: usize, - col_nr: usize, - grid_node_map: &mut GridNodeMap, - ) -> UIResult<()> { - grid_node_map.break_line(line_nr, col_nr) - } - - pub fn insert_empty_line(line_nr: usize, grid_node_map: &mut GridNodeMap) -> UIResult<()> { - grid_node_map.insert_empty_line(line_nr) - } - - pub fn push_empty_line(grid_node_map: &mut GridNodeMap) { - grid_node_map.push_empty_line(); - } - - pub fn clear_line(&mut self, line_nr: usize) -> UIResult<()> { - self.grid_node_map.clear_line(line_nr) - } - - pub fn del_line(&mut self, line_nr: usize) { - self.grid_node_map.del_line(line_nr) - } - - pub fn del_at_line(&mut self, line_nr: usize, index: usize) -> UIResult<()> { - self.grid_node_map.del_at_line(line_nr, index) - } - - // updates grid_node_map and code_lines but nothing else. - pub fn del_range_at_line( - &mut self, - line_nr: usize, - col_range: std::ops::Range, - ) -> UIResult<()> { - self.grid_node_map.del_range_at_line(line_nr, col_range) - } - - pub fn del_blank_expr_node(&mut self, txt_pos: TextPos) -> UIResult<()> { - self.del_at_line(txt_pos.line, txt_pos.column) - } - - pub fn set_selected_expr( - &mut self, - expr_start_pos: TextPos, - expr_end_pos: TextPos, - ast_node_id: ASTNodeId, - mark_node_id: MarkNodeId, - ) -> EdResult<()> { - self.set_raw_sel(RawSelection { - start_pos: expr_start_pos, - end_pos: expr_end_pos, - })?; - - self.set_caret(expr_start_pos); - - let type_str = match ast_node_id { - ASTNodeId::ADefId(def_id) => { - if let Some(expr_id) = self.extract_expr_from_def(def_id) { - self.expr2_to_type(expr_id) - } else { - PoolStr::new(" * ", self.module.env.pool) - } - } - - ASTNodeId::AExprId(expr_id) => self.expr2_to_type(expr_id), - }; - - self.selected_block_opt = Some(SelectedBlock { - ast_node_id, - mark_node_id, - type_str, - }); - - self.dirty = true; - - Ok(()) - } - - // select all MarkupNodes that refer to specific ast node and its children. - pub fn select_expr(&mut self) -> EdResult<()> { - // include parent in selection if an `Expr2` was already selected - if let Some(selected_block) = &self.selected_block_opt { - let expr2_level_mark_node = self.mark_node_pool.get(selected_block.mark_node_id); - - if let Some(parent_id) = expr2_level_mark_node.get_parent_id_opt() { - let ast_node_id = self.mark_id_ast_id_map.get(parent_id)?; - - let (expr_start_pos, expr_end_pos) = self - .grid_node_map - .get_nested_start_end_pos(parent_id, self)?; - - self.set_selected_expr(expr_start_pos, expr_end_pos, ast_node_id, parent_id)?; - } - } else { - // select `Expr2` in which caret is currently positioned - let caret_pos = self.get_caret(); - if self.grid_node_map.node_exists_at_pos(caret_pos) { - let (expr_start_pos, expr_end_pos, ast_node_id, mark_node_id) = self - .grid_node_map - .get_block_start_end_pos(self.get_caret(), self)?; - - self.set_selected_expr(expr_start_pos, expr_end_pos, ast_node_id, mark_node_id)?; - } else if self - .grid_node_map - .node_exists_at_pos(caret_pos.decrement_col()) - { - let (expr_start_pos, expr_end_pos, ast_node_id, mark_node_id) = self - .grid_node_map - .get_block_start_end_pos(self.get_caret().decrement_col(), self)?; - - self.set_selected_expr(expr_start_pos, expr_end_pos, ast_node_id, mark_node_id)?; - } - } - - Ok(()) - } - - fn extract_expr_from_def(&self, def_id: DefId) -> Option { - let def = self.module.env.pool.get(def_id); - - match def { - Def2::ValueDef { - identifier_id: _, - expr_id, - } => Some(*expr_id), - Def2::Blank => None, - Def2::CommentsBefore { - comments: _, - def_id, - } => self.extract_expr_from_def(*def_id), - Def2::CommentsAfter { - comments: _, - def_id, - } => self.extract_expr_from_def(*def_id), - } - } - - fn expr2_to_type(&mut self, expr2_id: ExprId) -> PoolStr { - let var = self.module.env.var_store.fresh(); - let expr = self.module.env.pool.get(expr2_id); - let arena = Bump::new(); - - let constrained = constrain_expr( - &arena, - &mut self.module.env, - expr, - Expected::NoExpectation(Type2::Variable(var)), - Region::zero(), - ); - - // extract the var_store out of the env - let mut var_store = VarStore::default(); - std::mem::swap(self.module.env.var_store, &mut var_store); - - let (mut solved, _, _) = EdModel::run_solve( - self.module.env.pool, - Default::default(), - Default::default(), - constrained, - var_store, - ); - - // put the updated var_store back in env - std::mem::swap( - &mut VarStore::new_from_subs(solved.inner()), - self.module.env.var_store, - ); - - let subs = solved.inner_mut(); - - let pretty_var = name_and_print_var( - var, - subs, - self.module.env.home, - &self.loaded_module.interns, - DebugPrint::NOTHING, - ); - - PoolStr::new(&pretty_var, self.module.env.pool) - } - - fn run_solve( - mempool: &mut Pool, - aliases: MutMap, - rigid_variables: MutMap, - constraint: Constraint, - var_store: VarStore, - ) -> (Solved, solve_type::Env, Vec) { - let env = solve_type::Env { - vars_by_symbol: MutMap::default(), - aliases, - }; - let arena = Bump::new(); - - let mut subs = Subs::new_from_varstore(var_store); - - for (var, name) in rigid_variables { - subs.rigid_var(var, name); - } - - // Now that the module is parsed, canonicalized, and constrained, - // we need to type check it. - let mut problems = Vec::new(); - - // Run the solver to populate Subs. - let (solved_subs, solved_env) = - solve_type::run(&arena, mempool, &env, &mut problems, subs, &constraint); - - (solved_subs, solved_env, problems) - } - - pub fn ed_handle_key_down( - &mut self, - modifiers: &Modifiers, - virtual_keycode: VirtualKeyCode, - _sound_thread_pool: &mut ThreadPool, - ) -> EdResult<()> { - match virtual_keycode { - Left => self.move_caret_left(modifiers)?, - Up => { - if modifiers.cmd_or_ctrl() && modifiers.shift { - self.select_expr()? - } else { - self.move_caret_up(modifiers)? - } - } - Right => self.move_caret_right(modifiers)?, - Down => self.move_caret_down(modifiers)?, - - A => { - if modifiers.cmd_or_ctrl() { - self.select_all()? - } - } - S => { - if modifiers.cmd_or_ctrl() { - self.save_file()? - } - } - R => { - if modifiers.cmd_or_ctrl() { - self.check_file()?; - self.run_file()? - } - } - - Home => self.move_caret_home(modifiers)?, - End => self.move_caret_end(modifiers)?, - - F11 => { - self.show_debug_view = !self.show_debug_view; - self.dirty = true; - } - F12 => { - #[cfg(feature = "with_sound")] - _sound_thread_pool.execute(move || { - play_sound("./editor/src/editor/resources/sounds/bell_sound.mp3"); - }); - } - _ => (), - } - - Ok(()) - } - - // Replaces selected expression with blank. - // If no expression is selected, this function will select one to guide the user to using backspace in a projectional editing way - fn backspace(&mut self) -> EdResult<()> { - if let Some(sel_block) = &self.selected_block_opt { - let expr2_level_mark_node = self.mark_node_pool.get(sel_block.mark_node_id); - let newlines_at_end = expr2_level_mark_node.get_newlines_at_end(); - - let blank_replacement = MarkupNode::Blank { - attributes: Attributes::default(), - parent_id_opt: expr2_level_mark_node.get_parent_id_opt(), - newlines_at_end, - }; - - self.mark_node_pool - .replace_node(sel_block.mark_node_id, blank_replacement); - - let active_selection = self.get_selection().context(MissingSelection {})?; - - self.grid_node_map.del_selection(active_selection)?; - - match sel_block.ast_node_id { - ASTNodeId::ADefId(def_id) => { - self.module.env.pool.set(def_id, Def2::Blank); - } - ASTNodeId::AExprId(expr_id) => { - self.module.env.pool.set(expr_id, Expr2::Blank); - } - } - - let expr_mark_node_id = sel_block.mark_node_id; - - let caret_pos = self.get_caret(); - - EdModel::insert_between_line( - caret_pos.line, - caret_pos.column, - nodes::BLANK_PLACEHOLDER, - expr_mark_node_id, - &mut self.grid_node_map, - )?; - - self.set_sel_none(); - } else { - self.select_expr()?; - }; - - Ok(()) - } - - fn save_file(&mut self) -> UIResult<()> { - let all_lines_str = self.code_lines.all_lines_as_string(); - - write_to_file(self.file_path, &all_lines_str)?; - - println!("save successful!"); - - Ok(()) - } - - fn check_file(&mut self) -> EdResult<()> { - println!("Checking file (cargo run check )..."); - - let roc_file_str = path_to_string(self.file_path); - - let cmd_out = Command::new("cargo") - .arg("run") - .arg("check") - .arg(roc_file_str) - .stdout(Stdio::inherit()) - .output()?; - - if !cmd_out.status.success() { - RocCheckFailed.fail()? - } - - Ok(()) - } - - fn run_file(&mut self) -> EdResult<()> { - println!("Executing file (cargo run )..."); - - let roc_file_str = path_to_string(self.file_path); - - Command::new("cargo") - .arg("run") - .arg(roc_file_str) - .stdout(Stdio::inherit()) - .stderr(Stdio::inherit()) - .output()?; - - Ok(()) - } - - /// update MarkupNode's, grid_node_map, code_lines after the AST has been updated - fn post_process_ast_update(&mut self) -> EdResult<()> { - //dbg!("{}",self.module.ast.ast_to_string(self.module.env.pool)); - - let markup_ids_tup = ast_to_mark_nodes( - &mut self.module.env, - &self.module.ast, - &mut self.mark_node_pool, - &self.loaded_module.interns, - )?; - - self.markup_ids = markup_ids_tup.0; - self.mark_id_ast_id_map = markup_ids_tup.1; - - self.code_lines = CodeLines::from_str(&nodes::mark_nodes_to_string( - &self.markup_ids, - &self.mark_node_pool, - )); - self.grid_node_map = GridNodeMap::default(); - - let mut line_nr = 0; - let mut col_nr = 0; - - for mark_node_id in &self.markup_ids { - // for debugging: - //println!("{}", tree_as_string(*mark_node_id, &mark_node_pool)); - EdModel::insert_mark_node_between_line( - &mut line_nr, - &mut col_nr, - *mark_node_id, - &mut self.grid_node_map, - &self.mark_node_pool, - )? - } - - Ok(()) - } -} - -impl<'a> SelectableLines for EdModel<'a> { - fn get_caret(&self) -> TextPos { - self.caret_w_select_vec.first().0.caret_pos - } - - // keeps active selection - fn set_caret(&mut self, caret_pos: TextPos) { - let caret_tup = self.caret_w_select_vec.first_mut(); - caret_tup.0.caret_pos = caret_pos; - caret_tup.1 = None; - } - - fn move_caret_left(&mut self, modifiers: &Modifiers) -> UIResult<()> { - let move_fun: MoveCaretFun = lines::move_caret_left; - EdModel::move_caret(self, move_fun, modifiers)?; - - Ok(()) - } - - fn move_caret_right(&mut self, modifiers: &Modifiers) -> UIResult<()> { - let move_fun: MoveCaretFun = lines::move_caret_right; - EdModel::move_caret(self, move_fun, modifiers)?; - - Ok(()) - } - - fn move_caret_up(&mut self, modifiers: &Modifiers) -> UIResult<()> { - let move_fun: MoveCaretFun = lines::move_caret_up; - EdModel::move_caret(self, move_fun, modifiers)?; - - Ok(()) - } - - fn move_caret_down(&mut self, modifiers: &Modifiers) -> UIResult<()> { - let move_fun: MoveCaretFun = lines::move_caret_down; - EdModel::move_caret(self, move_fun, modifiers)?; - - Ok(()) - } - - fn move_caret_home(&mut self, modifiers: &Modifiers) -> UIResult<()> { - let move_fun: MoveCaretFun = lines::move_caret_home; - EdModel::move_caret(self, move_fun, modifiers)?; - - Ok(()) - } - - fn move_caret_end(&mut self, modifiers: &Modifiers) -> UIResult<()> { - let move_fun: MoveCaretFun = lines::move_caret_end; - EdModel::move_caret(self, move_fun, modifiers)?; - - Ok(()) - } - - fn get_selection(&self) -> Option { - self.caret_w_select_vec.first().0.selection_opt - } - - fn is_selection_active(&self) -> bool { - self.get_selection().is_some() - } - - fn get_selected_str(&self) -> UIResult> { - if let Some(selection) = self.get_selection() { - let start_line_index = selection.start_pos.line; - let start_col = selection.start_pos.column; - let end_line_index = selection.end_pos.line; - let end_col = selection.end_pos.column; - - if start_line_index == end_line_index { - let line_ref = self.code_lines.get_line_ref(start_line_index)?; - - Ok(Some(line_ref[start_col..end_col].to_string())) - } else { - let full_str = String::new(); - - // TODO - Ok(Some(full_str)) - } - } else { - Ok(None) - } - } - - fn set_raw_sel(&mut self, raw_sel: RawSelection) -> UIResult<()> { - self.caret_w_select_vec.first_mut().0.selection_opt = Some(validate_raw_sel(raw_sel)?); - - Ok(()) - } - - fn set_sel_none(&mut self) { - self.caret_w_select_vec.first_mut().0.selection_opt = None; - self.selected_block_opt = None; - } - - fn set_caret_w_sel(&mut self, caret_w_sel: CaretWSelect) { - self.caret_w_select_vec.first_mut().0 = caret_w_sel; - } - - fn select_all(&mut self) -> UIResult<()> { - if self.code_lines.nr_of_chars() > 0 { - let last_pos = self.last_text_pos()?; - - self.set_raw_sel(RawSelection { - start_pos: TextPos { line: 0, column: 0 }, - end_pos: last_pos, - })?; - - self.set_caret(last_pos); - } - - Ok(()) - } - - fn last_text_pos(&self) -> UIResult { - let nr_of_lines = self.code_lines.lines.len(); - let last_line_index = nr_of_lines - 1; - let last_line = self.code_lines.get_line_ref(last_line_index)?; - - Ok(TextPos { - line: self.code_lines.lines.len() - 1, - column: last_line.len(), - }) - } - - fn handle_key_down( - &mut self, - _modifiers: &Modifiers, - _virtual_keycode: VirtualKeyCode, - ) -> UIResult<()> { - unreachable!("Use EdModel::ed_handle_key_down instead.") - } -} - -pub struct NodeContext<'a> { - pub old_caret_pos: TextPos, - pub curr_mark_node_id: MarkNodeId, - pub curr_mark_node: &'a MarkupNode, - pub parent_id_opt: Option, - pub ast_node_id: ASTNodeId, -} - -pub fn get_node_context<'a>(ed_model: &'a EdModel) -> EdResult> { - let old_caret_pos = ed_model.get_caret(); - let curr_mark_node_id = ed_model - .grid_node_map - .get_id_at_row_col(ed_model.get_caret())?; - let curr_mark_node = ed_model.mark_node_pool.get(curr_mark_node_id); - let parent_id_opt = curr_mark_node.get_parent_id_opt(); - let ast_node_id = ed_model.mark_id_ast_id_map.get(curr_mark_node_id)?; - - Ok(NodeContext { - old_caret_pos, - curr_mark_node_id, - curr_mark_node, - parent_id_opt, - ast_node_id, - }) -} - -fn if_modifiers(modifiers: &Modifiers, shortcut_result: UIResult<()>) -> EdResult<()> { - if modifiers.cmd_or_ctrl() { - Ok(shortcut_result?) - } else { - Ok(()) - } -} - -// handle new char when current(=caret is here) MarkupNode corresponds to a Def2 in the AST -pub fn handle_new_char_def( - received_char: &char, - def_id: DefId, - ed_model: &mut EdModel, -) -> EdResult { - let def_ref = ed_model.module.env.pool.get(def_id); - let ch = received_char; - - let NodeContext { - old_caret_pos, - curr_mark_node_id, - curr_mark_node, - parent_id_opt: _, - ast_node_id: _, - } = get_node_context(ed_model)?; - - let outcome = match def_ref { - Def2::Blank { .. } => match ch { - 'a'..='z' => start_new_tld_value(ed_model, ch)?, - _ => InputOutcome::Ignored, - }, - Def2::ValueDef { .. } => { - if curr_mark_node.get_content() == EQUALS { - let node_caret_offset = ed_model - .grid_node_map - .get_offset_to_node_id(old_caret_pos, curr_mark_node_id)?; - - if node_caret_offset == 0 || node_caret_offset == EQUALS.len() { - let prev_mark_node_id_opt = ed_model.get_prev_mark_node_id()?; - - if let Some(prev_mark_node_id) = prev_mark_node_id_opt { - update_tld_val_name( - prev_mark_node_id, - ed_model.get_caret(), // TODO update for multiple carets - ed_model, - ch, - )? - } else { - unreachable!() - } - } else { - InputOutcome::Ignored - } - } else { - update_tld_val_name( - curr_mark_node_id, - ed_model.get_caret(), // TODO update for multiple carets - ed_model, - ch, - )? - } - } - Def2::CommentsBefore { .. } => { - todo!() - } - Def2::CommentsAfter { .. } => { - todo!() - } - }; - - Ok(outcome) -} - -// handle new char when the current(caret is here) MarkupNode corresponds to an Expr2 in the AST -pub fn handle_new_char_expr( - received_char: &char, - expr_id: ExprId, - ed_model: &mut EdModel, -) -> EdResult { - let expr_ref = ed_model.module.env.pool.get(expr_id); - let ch = received_char; - - let NodeContext { - old_caret_pos: _, - curr_mark_node_id, - curr_mark_node, - parent_id_opt: _, - ast_node_id: _, - } = get_node_context(ed_model)?; - - let prev_mark_node_id_opt = ed_model.get_prev_mark_node_id()?; - - let outcome = if let Expr2::Blank { .. } = expr_ref { - match ch { - 'a'..='z' => start_new_let_value(ed_model, ch)?, - '"' => start_new_string(ed_model)?, - '{' => start_new_record(ed_model)?, - '0'..='9' => start_new_int(ed_model, ch)?, - '[' => { - // this can also be a tag union or become a set, assuming list for now - start_new_list(ed_model)? - } - '\r' => { - println!("For convenience and consistency there is only one way to format Roc, you can't add extra blank lines."); - InputOutcome::Ignored - } - _ => InputOutcome::Ignored, - } - } else if let Some(prev_mark_node_id) = prev_mark_node_id_opt { - if prev_mark_node_id == curr_mark_node_id { - match expr_ref { - Expr2::SmallInt { .. } => update_int(ed_model, curr_mark_node_id, ch)?, - Expr2::SmallStr(old_arr_str) => update_small_string(ch, old_arr_str, ed_model)?, - Expr2::Str(..) => update_string(*ch, ed_model)?, - Expr2::InvalidLookup(old_pool_str) => update_invalid_lookup( - &ch.to_string(), - old_pool_str, - curr_mark_node_id, - expr_id, - ed_model, - )?, - Expr2::EmptyRecord => { - // prev_mark_node_id and curr_mark_node_id should be different to allow creating field at current caret position - InputOutcome::Ignored - } - Expr2::Record { - record_var: _, - fields, - } => { - if curr_mark_node - .get_content() - .chars() - .all(|chr| chr.is_ascii_alphanumeric()) - { - update_record_field( - &ch.to_string(), - ed_model.get_caret(), - curr_mark_node_id, - fields, - ed_model, - )? - } else { - InputOutcome::Ignored - } - } - _ => InputOutcome::Ignored, - } - } else if ch.is_ascii_alphanumeric() { - // prev_mark_node_id != curr_mark_node_id - - match expr_ref { - Expr2::SmallInt { .. } => update_int(ed_model, curr_mark_node_id, ch)?, - _ => { - let prev_ast_node_id = ed_model.mark_id_ast_id_map.get(prev_mark_node_id)?; - - match prev_ast_node_id { - ASTNodeId::ADefId(_) => InputOutcome::Ignored, - ASTNodeId::AExprId(prev_expr_id) => { - handle_new_char_diff_mark_nodes_prev_is_expr( - ch, - prev_expr_id, - expr_id, - prev_mark_node_id, - curr_mark_node_id, - ed_model, - )? - } - } - } - } - } else if *ch == ':' { - let mark_parent_id_opt = curr_mark_node.get_parent_id_opt(); - - if let Some(mark_parent_id) = mark_parent_id_opt { - let parent_ast_id = ed_model.mark_id_ast_id_map.get(mark_parent_id)?; - - match parent_ast_id { - ASTNodeId::ADefId(_) => InputOutcome::Ignored, - ASTNodeId::AExprId(parent_expr_id) => { - update_record_colon(ed_model, parent_expr_id)? - } - } - } else { - InputOutcome::Ignored - } - } else if *ch == ',' { - if curr_mark_node.get_content() == nodes::LEFT_SQUARE_BR { - InputOutcome::Ignored - } else { - let mark_parent_id_opt = curr_mark_node.get_parent_id_opt(); - - if let Some(mark_parent_id) = mark_parent_id_opt { - let parent_ast_id = ed_model.mark_id_ast_id_map.get(mark_parent_id)?; - - match parent_ast_id { - ASTNodeId::ADefId(_) => InputOutcome::Ignored, - ASTNodeId::AExprId(parent_expr_id) => { - let parent_expr2 = ed_model.module.env.pool.get(parent_expr_id); - - match parent_expr2 { - Expr2::List { - elem_var: _, - elems: _, - } => { - let (new_child_index, new_ast_child_index) = - ed_model.get_curr_child_indices()?; - // insert a Blank first, this results in cleaner code - add_blank_child(new_child_index, new_ast_child_index, ed_model)? - } - Expr2::Record { - record_var: _, - fields: _, - } => { - todo!("multiple record fields") - } - _ => InputOutcome::Ignored, - } - } - } - } else { - InputOutcome::Ignored - } - } - } else if "\"{[".contains(*ch) { - let prev_mark_node = ed_model.mark_node_pool.get(prev_mark_node_id); - - if prev_mark_node.get_content() == nodes::LEFT_SQUARE_BR - && curr_mark_node.get_content() == nodes::RIGHT_SQUARE_BR - { - let (new_child_index, new_ast_child_index) = ed_model.get_curr_child_indices()?; - // insert a Blank first, this results in cleaner code - add_blank_child(new_child_index, new_ast_child_index, ed_model)?; - ed_model.post_process_ast_update()?; - handle_new_char(received_char, ed_model)? - } else { - InputOutcome::Ignored - } - } else { - InputOutcome::Ignored - } - } else { - InputOutcome::Ignored - }; - - Ok(outcome) -} - -// handle new char when prev_mark_node != curr_mark_node and prev_mark_node's AST node is an Expr2 -pub fn handle_new_char_diff_mark_nodes_prev_is_expr( - received_char: &char, - prev_expr_id: ExprId, - curr_expr_id: ExprId, - prev_mark_node_id: MarkNodeId, - curr_mark_node_id: MarkNodeId, - ed_model: &mut EdModel, -) -> EdResult { - let prev_expr_ref = ed_model.module.env.pool.get(prev_expr_id); - let curr_expr_ref = ed_model.module.env.pool.get(curr_expr_id); - let ch = received_char; - let curr_mark_node = ed_model.mark_node_pool.get(curr_mark_node_id); - - let outcome = match prev_expr_ref { - Expr2::SmallInt { .. } => update_int(ed_model, prev_mark_node_id, ch)?, - Expr2::InvalidLookup(old_pool_str) => update_invalid_lookup( - &ch.to_string(), - old_pool_str, - prev_mark_node_id, - prev_expr_id, - ed_model, - )?, - Expr2::Record { - record_var: _, - fields, - } => { - let prev_mark_node = ed_model.mark_node_pool.get(prev_mark_node_id); - - if (curr_mark_node.get_content() == nodes::RIGHT_ACCOLADE - || curr_mark_node.get_content() == nodes::COLON) - && prev_mark_node.is_all_alphanumeric() - { - update_record_field( - &ch.to_string(), - ed_model.get_caret(), - prev_mark_node_id, - fields, - ed_model, - )? - } else if prev_mark_node.get_content() == nodes::LEFT_ACCOLADE - && curr_mark_node.is_all_alphanumeric() - { - update_record_field( - &ch.to_string(), - ed_model.get_caret(), - curr_mark_node_id, - fields, - ed_model, - )? - } else { - InputOutcome::Ignored - } - } - Expr2::List { - elem_var: _, - elems: _, - } => { - let prev_mark_node = ed_model.mark_node_pool.get(prev_mark_node_id); - - if prev_mark_node.get_content() == nodes::LEFT_SQUARE_BR - && curr_mark_node.get_content() == nodes::RIGHT_SQUARE_BR - { - // based on if, we are at the start of the list - let new_child_index = 1; - let new_ast_child_index = 0; - // insert a Blank first, this results in cleaner code - add_blank_child(new_child_index, new_ast_child_index, ed_model)?; - ed_model.post_process_ast_update()?; - handle_new_char(received_char, ed_model)? - } else { - InputOutcome::Ignored - } - } - _ => match curr_expr_ref { - Expr2::EmptyRecord => { - let sibling_ids = curr_mark_node.get_sibling_ids(&ed_model.mark_node_pool); - - update_empty_record(&ch.to_string(), prev_mark_node_id, sibling_ids, ed_model)? - } - _ => InputOutcome::Ignored, - }, - }; - - Ok(outcome) -} - -// updates the ed_model based on the char the user just typed if the result would be syntactically correct. -pub fn handle_new_char(received_char: &char, ed_model: &mut EdModel) -> EdResult { - //dbg!("{}", ed_model.module.ast.ast_to_string(ed_model.module.env.pool)); - - let input_outcome = match received_char { - '\u{e000}'..='\u{f8ff}' // http://www.unicode.org/faq/private_use.html - | '\u{f0000}'..='\u{ffffd}' // ^ - | '\u{100000}'..='\u{10fffd}' // ^ - => { - InputOutcome::Ignored - } - '\u{8}' | '\u{7f}' => { - // On Linux, '\u{8}' is backspace, - // on macOS '\u{7f}'. - - ed_model.backspace()?; - - InputOutcome::Accepted - } - ch => { - let outcome = - if ed_model.node_exists_at_caret() { - let curr_mark_node_id = ed_model.get_curr_mark_node_id()?; - let ast_node_id = ed_model.mark_id_ast_id_map.get(curr_mark_node_id)?; - - match ast_node_id { - ASTNodeId::ADefId(def_id) => { - handle_new_char_def(received_char, def_id, ed_model)? - }, - ASTNodeId::AExprId(expr_id) => { - handle_new_char_expr(received_char, expr_id, ed_model)? - } - } - - } else { //no MarkupNode at the current position - if *received_char == '\r' { - break_line(ed_model)? - } else { - let prev_mark_node_id_opt = ed_model.get_prev_mark_node_id()?; - if let Some(prev_mark_node_id) = prev_mark_node_id_opt { - - let prev_ast_node_id = ed_model.mark_id_ast_id_map.get(prev_mark_node_id)?.to_expr_id()?; - let prev_ast_node = ed_model.module.env.pool.get(prev_ast_node_id); - - match prev_ast_node { - Expr2::SmallInt{ .. } => { - update_int(ed_model, prev_mark_node_id, ch)? - }, - _ => { - InputOutcome::Ignored - } - } - } else { - match ch { - 'a'..='z' => { - for caret_pos in ed_model.get_carets() { - - if caret_pos.line > 0 { - insert_new_blank(ed_model, caret_pos.line)?; - ed_model.post_process_ast_update()?; - } - } - handle_new_char(received_char, ed_model)? - } - _ => { - InputOutcome::Ignored - } - } - } - } - }; - - - if let InputOutcome::Accepted = outcome { - ed_model.set_sel_none(); - } - - outcome - } - }; - - if let InputOutcome::Accepted = input_outcome { - ed_model.post_process_ast_update()?; - ed_model.dirty = true; - } - - Ok(input_outcome) -} - -#[cfg(test)] -pub mod test_ed_update { - use std::iter; - - use crate::editor::ed_error::print_err; - use crate::editor::mvc::ed_model::test_ed_model::ed_model_from_dsl; - use crate::editor::mvc::ed_model::test_ed_model::ed_model_to_dsl; - use crate::editor::mvc::ed_model::test_ed_model::init_model_refs; - use crate::editor::mvc::ed_update::handle_new_char; - use crate::editor::mvc::ed_update::EdModel; - use crate::editor::mvc::ed_update::EdResult; - use crate::editor::resources::strings::nr_hello_world_lines; - use crate::ui::text::lines::SelectableLines; - use crate::ui::ui_error::UIResult; - use crate::window::keyboard_input::no_mods; - use crate::window::keyboard_input::test_modifiers::ctrl_cmd_shift; - use crate::window::keyboard_input::Modifiers; - use bumpalo::Bump; - use roc_code_markup::markup::common_nodes::NEW_LINES_AFTER_DEF; - use roc_module::symbol::ModuleIds; - use threadpool::ThreadPool; - use winit::event::VirtualKeyCode::*; - - fn ed_res_to_res(ed_res: EdResult) -> Result { - match ed_res { - Ok(t) => Ok(t), - Err(e) => { - print_err(&e); - Err(e.to_string()) - } - } - } - - fn ui_res_to_res(ed_res: UIResult) -> Result { - match ed_res { - Ok(t) => Ok(t), - Err(e) => Err(e.to_string()), - } - } - - // Create ed_model from pre_lines DSL, do handle_new_char() with new_char, check if modified ed_model has expected - // string representation of code, caret position and active selection. - pub fn assert_insert( - pre_lines: Vec, - expected_post_lines: Vec, - new_char: char, - ) -> Result<(), String> { - assert_insert_seq(pre_lines, expected_post_lines, &new_char.to_string()) - } - - pub fn assert_insert_nls( - pre_lines: Vec, - expected_post_lines: Vec, - new_char: char, - ) -> Result<(), String> { - assert_insert(pre_lines, add_nls(expected_post_lines), new_char) - } - - pub fn assert_insert_no_pre( - expected_post_lines: Vec, - new_char: char, - ) -> Result<(), String> { - assert_insert_seq_no_pre(expected_post_lines, &new_char.to_string()) - } - - pub fn assert_insert_seq_no_pre( - expected_post_lines: Vec, - new_char_seq: &str, - ) -> Result<(), String> { - assert_insert_seq(vec!["┃".to_owned()], expected_post_lines, new_char_seq) - } - - // pre-insert `val = ` - pub fn assert_insert_in_def( - expected_post_lines: Vec, - new_char: char, - ) -> Result<(), String> { - assert_insert_seq_in_def(expected_post_lines, &new_char.to_string()) - } - - // pre-insert `val = ` - pub fn assert_insert_seq_in_def( - expected_post_lines: Vec, - new_char_seq: &str, - ) -> Result<(), String> { - let prefix = "val🡲🡲🡲"; - - let full_input = merge_strings(vec![prefix, new_char_seq]); - - let mut expected_post_lines_vec = expected_post_lines.to_vec(); - - let first_line_opt = expected_post_lines_vec.first(); - let val_str = "val = "; - - if let Some(first_line) = first_line_opt { - expected_post_lines_vec[0] = merge_strings(vec![val_str, first_line]); - } else { - expected_post_lines_vec = vec![val_str.to_owned()]; - } - - assert_insert_seq_no_pre(expected_post_lines_vec, &full_input) - } - - pub fn assert_insert_in_def_nls( - expected_post_lines: Vec, - new_char: char, - ) -> Result<(), String> { - assert_insert_seq_in_def(add_nls(expected_post_lines), &new_char.to_string()) - } - - // Create ed_model from pre_lines DSL, do handle_new_char() for every char in new_char_seq, check if modified ed_model has expected - // string representation of code, caret position and active selection. - pub fn assert_insert_seq( - pre_lines: Vec, - expected_post_lines: Vec, - new_char_seq: &str, - ) -> Result<(), String> { - let mut code_str = pre_lines.join("\n").replace('┃', ""); - - let mut model_refs = init_model_refs(); - let code_arena = Bump::new(); - let module_ids = ModuleIds::default(); - - let mut ed_model = ed_model_from_dsl( - &mut code_str, - pre_lines, - &mut model_refs, - &module_ids, - &code_arena, - )?; - - for input_char in new_char_seq.chars() { - if input_char == '🡲' { - ed_model.simple_move_carets_right(1); - } else if input_char == '🡰' { - ed_model.simple_move_carets_left(1); - } else if input_char == '🡱' { - ed_model.simple_move_carets_up(1); - } else { - //dbg!(input_char); - ed_res_to_res(handle_new_char(&input_char, &mut ed_model))?; - } - } - - let mut post_lines = ui_res_to_res(ed_model_to_dsl(&ed_model))?; - strip_header(&mut post_lines); // remove header for clean tests - - assert_eq!(post_lines, expected_post_lines); - - Ok(()) - } - - fn strip_header(lines: &mut Vec) { - lines.drain(0..nr_hello_world_lines()); - } - - pub fn assert_insert_seq_nls( - pre_lines: Vec, - expected_post_lines: Vec, - new_char_seq: &str, - ) -> Result<(), String> { - assert_insert_seq(pre_lines, add_nls(expected_post_lines), new_char_seq) - } - - pub fn assert_insert_seq_ignore(lines: Vec, new_char_seq: &str) -> Result<(), String> { - assert_insert_seq(lines.clone(), lines, new_char_seq) - } - - pub fn assert_insert_seq_ignore_nls( - lines: Vec, - new_char_seq: &str, - ) -> Result<(), String> { - assert_insert_seq_ignore(add_nls(lines), new_char_seq) - } - - pub fn assert_insert_ignore(lines: Vec, new_char: char) -> Result<(), String> { - assert_insert_seq_ignore(lines, &new_char.to_string()) - } - - pub fn assert_insert_ignore_nls(lines: Vec, new_char: char) -> Result<(), String> { - assert_insert_seq_ignore(add_nls(lines), &new_char.to_string()) - } - - // to create Vec from list of &str - macro_rules! ovec { - ( $( $x:expr ),* ) => { - { - vec![ - $( - $x.to_owned(), - )* - ] - } - }; - } - - #[test] - fn test_ignore_basic() -> Result<(), String> { - assert_insert_no_pre(ovec!["┃"], ';')?; - assert_insert_no_pre(ovec!["┃"], '-')?; - assert_insert_no_pre(ovec!["┃"], '_')?; - // extra space because of Expr2::Blank placeholder - assert_insert_in_def_nls(ovec!["┃ "], ';')?; - assert_insert_in_def_nls(ovec!["┃ "], '-')?; - assert_insert_in_def_nls(ovec!["┃ "], '_')?; - - Ok(()) - } - - // add newlines like the editor's formatting would add them - fn add_nls(lines: Vec) -> Vec { - let mut new_lines = lines; - //line(s) between TLD's, extra newline so the user can go to last line add new def there - let mut extra_empty_lines = iter::repeat("".to_owned()) - .take(NEW_LINES_AFTER_DEF) - .collect(); - new_lines.append(&mut extra_empty_lines); - - new_lines - } - - //TODO test_int arch bit limit - #[test] - fn test_int() -> Result<(), String> { - assert_insert_in_def_nls(ovec!["0┃"], '0')?; - assert_insert_in_def_nls(ovec!["1┃"], '1')?; - assert_insert_in_def_nls(ovec!["2┃"], '2')?; - assert_insert_in_def_nls(ovec!["3┃"], '3')?; - assert_insert_in_def_nls(ovec!["4┃"], '4')?; - assert_insert_in_def_nls(ovec!["5┃"], '5')?; - assert_insert_in_def_nls(ovec!["6┃"], '6')?; - assert_insert_in_def_nls(ovec!["7┃"], '7')?; - assert_insert_in_def_nls(ovec!["8┃"], '8')?; - assert_insert_in_def_nls(ovec!["9┃"], '9')?; - - assert_insert(ovec!["val = 1┃"], add_nls(ovec!["val = 19┃"]), '9')?; - assert_insert(ovec!["val = 9876┃"], add_nls(ovec!["val = 98769┃"]), '9')?; - assert_insert(ovec!["val = 10┃"], add_nls(ovec!["val = 103┃"]), '3')?; - assert_insert(ovec!["val = ┃0"], add_nls(ovec!["val = 1┃0"]), '1')?; - assert_insert(ovec!["val = 10000┃"], add_nls(ovec!["val = 100000┃"]), '0')?; - - assert_insert(ovec!["val = ┃1234"], add_nls(ovec!["val = 5┃1234"]), '5')?; - assert_insert(ovec!["val = 1┃234"], add_nls(ovec!["val = 10┃234"]), '0')?; - assert_insert(ovec!["val = 12┃34"], add_nls(ovec!["val = 121┃34"]), '1')?; - assert_insert(ovec!["val = 123┃4"], add_nls(ovec!["val = 1232┃4"]), '2')?; - - Ok(()) - } - - fn merge_strings(strings: Vec<&str>) -> String { - strings - .iter() - .map(|&some_str| some_str.to_owned()) - .collect::>() - .join("") - } - - const IGNORE_CHARS: &str = "{}()[]-><-_\"azAZ:@09"; - const IGNORE_CHARS_NO_NUM: &str = ",{}()[]-><-_\"azAZ:@"; - const IGNORE_NO_LTR: &str = "{\"5"; - const IGNORE_NO_NUM: &str = "a{\""; - - #[test] - fn test_ignore_int() -> Result<(), String> { - assert_insert_seq_ignore_nls(ovec!["vec = ┃0"], IGNORE_CHARS_NO_NUM)?; - assert_insert_seq_ignore_nls(ovec!["vec = ┃7"], IGNORE_CHARS_NO_NUM)?; - - assert_insert_seq_ignore_nls(ovec!["vec = 0┃"], IGNORE_CHARS_NO_NUM)?; - assert_insert_seq_ignore_nls(ovec!["vec = 8┃"], IGNORE_CHARS_NO_NUM)?; - assert_insert_seq_ignore_nls(ovec!["vec = 20┃"], IGNORE_CHARS_NO_NUM)?; - assert_insert_seq_ignore_nls(ovec!["vec = 83┃"], IGNORE_CHARS_NO_NUM)?; - - assert_insert_seq_ignore_nls(ovec!["vec = 1┃0"], IGNORE_CHARS_NO_NUM)?; - assert_insert_seq_ignore_nls(ovec!["vec = 8┃4"], IGNORE_CHARS_NO_NUM)?; - - assert_insert_seq_ignore_nls(ovec!["vec = ┃10"], IGNORE_CHARS_NO_NUM)?; - assert_insert_seq_ignore_nls(ovec!["vec = ┃84"], IGNORE_CHARS_NO_NUM)?; - - assert_insert_seq_ignore_nls(ovec!["vec = 129┃96"], IGNORE_CHARS_NO_NUM)?; - assert_insert_seq_ignore_nls(ovec!["vec = 97┃684"], IGNORE_CHARS_NO_NUM)?; - - assert_insert_ignore_nls(ovec!["vec = 0┃"], '0')?; - assert_insert_ignore_nls(ovec!["vec = 0┃"], '9')?; - assert_insert_ignore_nls(ovec!["vec = ┃0"], '0')?; - assert_insert_ignore_nls(ovec!["vec = ┃1234"], '0')?; - assert_insert_ignore_nls(ovec!["vec = ┃100"], '0')?; - - Ok(()) - } - - #[test] - fn test_string() -> Result<(), String> { - assert_insert_in_def_nls(ovec!["\"┃\""], '"')?; - assert_insert(ovec!["val = \"┃\""], add_nls(ovec!["val = \"a┃\""]), 'a')?; - assert_insert(ovec!["val = \"┃\""], add_nls(ovec!["val = \"{┃\""]), '{')?; - assert_insert(ovec!["val = \"┃\""], add_nls(ovec!["val = \"}┃\""]), '}')?; - assert_insert(ovec!["val = \"┃\""], add_nls(ovec!["val = \"[┃\""]), '[')?; - assert_insert(ovec!["val = \"┃\""], add_nls(ovec!["val = \"]┃\""]), ']')?; - assert_insert(ovec!["val = \"┃\""], add_nls(ovec!["val = \"-┃\""]), '-')?; - assert_insert(ovec!["val = \"┃-\""], add_nls(ovec!["val = \"<┃-\""]), '<')?; - assert_insert(ovec!["val = \"-┃\""], add_nls(ovec!["val = \"->┃\""]), '>')?; - - assert_insert(ovec!["val = \"a┃\""], add_nls(ovec!["val = \"ab┃\""]), 'b')?; - assert_insert( - ovec!["val = \"ab┃\""], - add_nls(ovec!["val = \"abc┃\""]), - 'c', - )?; - assert_insert(ovec!["val = \"┃a\""], add_nls(ovec!["val = \"z┃a\""]), 'z')?; - assert_insert(ovec!["val = \"┃a\""], add_nls(ovec!["val = \" ┃a\""]), ' ')?; - assert_insert( - ovec!["val = \"a┃b\""], - add_nls(ovec!["val = \"az┃b\""]), - 'z', - )?; - assert_insert( - ovec!["val = \"a┃b\""], - add_nls(ovec!["val = \"a ┃b\""]), - ' ', - )?; - - assert_insert( - ovec!["val = \"ab ┃\""], - add_nls(ovec!["val = \"ab {┃\""]), - '{', - )?; - assert_insert( - ovec!["val = \"ab ┃\""], - add_nls(ovec!["val = \"ab }┃\""]), - '}', - )?; - assert_insert( - ovec!["val = \"{ str: 4┃}\""], - add_nls(ovec!["val = \"{ str: 44┃}\""]), - '4', - )?; - assert_insert( - ovec!["val = \"┃ello, hello, hello\""], - add_nls(ovec!["val = \"h┃ello, hello, hello\""]), - 'h', - )?; - assert_insert( - ovec!["val = \"hello┃ hello, hello\""], - add_nls(ovec!["val = \"hello,┃ hello, hello\""]), - ',', - )?; - assert_insert( - ovec!["val = \"hello, hello, hello┃\""], - add_nls(ovec!["val = \"hello, hello, hello.┃\""]), - '.', - )?; - - Ok(()) - } - - #[test] - fn test_ignore_string() -> Result<(), String> { - assert_insert_ignore(add_nls(ovec!["val = ┃\"\""]), 'a')?; - assert_insert_ignore(add_nls(ovec!["val = ┃\"\""]), 'A')?; - assert_insert_ignore(add_nls(ovec!["val = ┃\"\""]), '"')?; - assert_insert_ignore(add_nls(ovec!["val = ┃\"\""]), '{')?; - assert_insert_ignore(add_nls(ovec!["val = ┃\"\""]), '[')?; - assert_insert_ignore(add_nls(ovec!["val = ┃\"\""]), '}')?; - assert_insert_ignore(add_nls(ovec!["val = ┃\"\""]), ']')?; - assert_insert_ignore(add_nls(ovec!["val = ┃\"\""]), '-')?; - - assert_insert_ignore(add_nls(ovec!["val = \"\"┃"]), 'a')?; - assert_insert_ignore(add_nls(ovec!["val = \"\"┃"]), 'A')?; - assert_insert_ignore(add_nls(ovec!["val = \"\"┃"]), '"')?; - assert_insert_ignore(add_nls(ovec!["val = \"\"┃"]), '{')?; - assert_insert_ignore(add_nls(ovec!["val = \"\"┃"]), '[')?; - assert_insert_ignore(add_nls(ovec!["val = \"\"┃"]), '}')?; - assert_insert_ignore(add_nls(ovec!["val = \"\"┃"]), ']')?; - assert_insert_ignore(add_nls(ovec!["val = \"\"┃"]), '-')?; - - assert_insert_ignore(add_nls(ovec!["val = ┃\"a\""]), 'a')?; - assert_insert_ignore(add_nls(ovec!["val = ┃\"a\""]), 'A')?; - assert_insert_ignore(add_nls(ovec!["val = ┃\"a\""]), '"')?; - assert_insert_ignore(add_nls(ovec!["val = ┃\"a\""]), '{')?; - assert_insert_ignore(add_nls(ovec!["val = ┃\"a\""]), '[')?; - assert_insert_ignore(add_nls(ovec!["val = ┃\"a\""]), '}')?; - assert_insert_ignore(add_nls(ovec!["val = ┃\"a\""]), ']')?; - assert_insert_ignore(add_nls(ovec!["val = ┃\"a\""]), '-')?; - - assert_insert_ignore(add_nls(ovec!["val = \"a\"┃"]), 'a')?; - assert_insert_ignore(add_nls(ovec!["val = \"a\"┃"]), 'A')?; - assert_insert_ignore(add_nls(ovec!["val = \"a\"┃"]), '"')?; - assert_insert_ignore(add_nls(ovec!["val = \"a\"┃"]), '{')?; - assert_insert_ignore(add_nls(ovec!["val = \"a\"┃"]), '[')?; - assert_insert_ignore(add_nls(ovec!["val = \"a\"┃"]), '}')?; - assert_insert_ignore(add_nls(ovec!["val = \"a\"┃"]), ']')?; - assert_insert_ignore(add_nls(ovec!["val = \"a\"┃"]), '-')?; - - assert_insert_ignore(add_nls(ovec!["val = ┃\"{ }\""]), 'a')?; - assert_insert_ignore(add_nls(ovec!["val = ┃\"{ }\""]), 'A')?; - assert_insert_ignore(add_nls(ovec!["val = ┃\"{ }\""]), '"')?; - assert_insert_ignore(add_nls(ovec!["val = ┃\"{ }\""]), '{')?; - assert_insert_ignore(add_nls(ovec!["val = ┃\"{ }\""]), '[')?; - assert_insert_ignore(add_nls(ovec!["val = ┃\"{ }\""]), '}')?; - assert_insert_ignore(add_nls(ovec!["val = ┃\"{ }\""]), ']')?; - assert_insert_ignore(add_nls(ovec!["val = ┃\"{ }\""]), '-')?; - - assert_insert_ignore(add_nls(ovec!["val = \"{ }\"┃"]), 'A')?; - assert_insert_ignore(add_nls(ovec!["val = \"{ }\"┃"]), 'a')?; - assert_insert_ignore(add_nls(ovec!["val = \"{ }\"┃"]), '"')?; - assert_insert_ignore(add_nls(ovec!["val = \"{ }\"┃"]), '{')?; - assert_insert_ignore(add_nls(ovec!["val = \"{ }\"┃"]), '[')?; - assert_insert_ignore(add_nls(ovec!["val = \"{ }\"┃"]), '}')?; - assert_insert_ignore(add_nls(ovec!["val = \"{ }\"┃"]), ']')?; - assert_insert_ignore(add_nls(ovec!["val = \"{ }\"┃"]), '-')?; - - assert_insert_ignore(add_nls(ovec!["val = \"[ 1, 2, 3 ]\"┃"]), '{')?; - assert_insert_ignore(add_nls(ovec!["val = ┃\"[ 1, 2, 3 ]\""]), '{')?; - assert_insert_ignore(add_nls(ovec!["val = \"hello, hello, hello\"┃"]), '.')?; - assert_insert_ignore(add_nls(ovec!["val = ┃\"hello, hello, hello\""]), '.')?; - - Ok(()) - } - - #[test] - fn test_record() -> Result<(), String> { - assert_insert_in_def_nls(ovec!["{ ┃ }"], '{')?; - assert_insert_nls(ovec!["val = { ┃ }"], ovec!["val = { a┃ }"], 'a')?; - assert_insert_nls(ovec!["val = { a┃ }"], ovec!["val = { ab┃ }"], 'b')?; - assert_insert_nls(ovec!["val = { a┃ }"], ovec!["val = { a1┃ }"], '1')?; - assert_insert_nls(ovec!["val = { a1┃ }"], ovec!["val = { a1z┃ }"], 'z')?; - assert_insert_nls(ovec!["val = { a1┃ }"], ovec!["val = { a15┃ }"], '5')?; - assert_insert_nls(ovec!["val = { ab┃ }"], ovec!["val = { abc┃ }"], 'c')?; - assert_insert_nls(ovec!["val = { ┃abc }"], ovec!["val = { z┃abc }"], 'z')?; - assert_insert_nls(ovec!["val = { a┃b }"], ovec!["val = { az┃b }"], 'z')?; - assert_insert_nls(ovec!["val = { a┃b }"], ovec!["val = { a9┃b }"], '9')?; - - assert_insert_nls(ovec!["val = { a┃ }"], ovec!["val = { a: ┃ }"], ':')?; - assert_insert_nls(ovec!["val = { abc┃ }"], ovec!["val = { abc: ┃ }"], ':')?; - assert_insert_nls(ovec!["val = { aBc┃ }"], ovec!["val = { aBc: ┃ }"], ':')?; - - assert_insert_seq_nls(ovec!["val = { a┃ }"], ovec!["val = { a: \"┃\" }"], ":\"")?; - assert_insert_seq_nls( - ovec!["val = { abc┃ }"], - ovec!["val = { abc: \"┃\" }"], - ":\"", - )?; - - assert_insert_seq_nls(ovec!["val = { a┃ }"], ovec!["val = { a: 0┃ }"], ":0")?; - assert_insert_seq_nls(ovec!["val = { abc┃ }"], ovec!["val = { abc: 9┃ }"], ":9")?; - assert_insert_seq_nls(ovec!["val = { a┃ }"], ovec!["val = { a: 1000┃ }"], ":1000")?; - assert_insert_seq_nls( - ovec!["val = { abc┃ }"], - ovec!["val = { abc: 98761┃ }"], - ":98761", - )?; - - assert_insert_nls( - ovec!["val = { a: \"┃\" }"], - ovec!["val = { a: \"a┃\" }"], - 'a', - )?; - assert_insert_nls( - ovec!["val = { a: \"a┃\" }"], - ovec!["val = { a: \"ab┃\" }"], - 'b', - )?; - assert_insert_nls( - ovec!["val = { a: \"a┃b\" }"], - ovec!["val = { a: \"az┃b\" }"], - 'z', - )?; - assert_insert_nls( - ovec!["val = { a: \"┃ab\" }"], - ovec!["val = { a: \"z┃ab\" }"], - 'z', - )?; - - assert_insert_nls(ovec!["val = { a: 1┃ }"], ovec!["val = { a: 10┃ }"], '0')?; - assert_insert_nls(ovec!["val = { a: 100┃ }"], ovec!["val = { a: 1004┃ }"], '4')?; - assert_insert_nls(ovec!["val = { a: 9┃76 }"], ovec!["val = { a: 98┃76 }"], '8')?; - assert_insert_nls( - ovec!["val = { a: 4┃691 }"], - ovec!["val = { a: 40┃691 }"], - '0', - )?; - assert_insert_nls( - ovec!["val = { a: 469┃1 }"], - ovec!["val = { a: 4699┃1 }"], - '9', - )?; - - assert_insert_nls( - ovec!["val = { camelCase: \"┃\" }"], - ovec!["val = { camelCase: \"a┃\" }"], - 'a', - )?; - assert_insert_nls( - ovec!["val = { camelCase: \"a┃\" }"], - ovec!["val = { camelCase: \"ab┃\" }"], - 'b', - )?; - - assert_insert_nls( - ovec!["val = { camelCase: 3┃ }"], - ovec!["val = { camelCase: 35┃ }"], - '5', - )?; - assert_insert_nls( - ovec!["val = { camelCase: ┃2 }"], - ovec!["val = { camelCase: 5┃2 }"], - '5', - )?; - assert_insert_nls( - ovec!["val = { camelCase: 10┃2 }"], - ovec!["val = { camelCase: 106┃2 }"], - '6', - )?; - - assert_insert_nls( - ovec!["val = { a┃: \"\" }"], - ovec!["val = { ab┃: \"\" }"], - 'b', - )?; - assert_insert_nls( - ovec!["val = { ┃a: \"\" }"], - ovec!["val = { z┃a: \"\" }"], - 'z', - )?; - assert_insert_nls( - ovec!["val = { ab┃: \"\" }"], - ovec!["val = { abc┃: \"\" }"], - 'c', - )?; - assert_insert_nls( - ovec!["val = { ┃ab: \"\" }"], - ovec!["val = { z┃ab: \"\" }"], - 'z', - )?; - assert_insert_nls( - ovec!["val = { camelCase┃: \"hello\" }"], - ovec!["val = { camelCaseB┃: \"hello\" }"], - 'B', - )?; - assert_insert_nls( - ovec!["val = { camel┃Case: \"hello\" }"], - ovec!["val = { camelZ┃Case: \"hello\" }"], - 'Z', - )?; - assert_insert_nls( - ovec!["val = { ┃camelCase: \"hello\" }"], - ovec!["val = { z┃camelCase: \"hello\" }"], - 'z', - )?; - - assert_insert_nls(ovec!["val = { a┃: 0 }"], ovec!["val = { ab┃: 0 }"], 'b')?; - assert_insert_nls( - ovec!["val = { ┃a: 2100 }"], - ovec!["val = { z┃a: 2100 }"], - 'z', - )?; - assert_insert_nls( - ovec!["val = { ab┃: 9876 }"], - ovec!["val = { abc┃: 9876 }"], - 'c', - )?; - assert_insert_nls( - ovec!["val = { ┃ab: 102 }"], - ovec!["val = { z┃ab: 102 }"], - 'z', - )?; - assert_insert_nls( - ovec!["val = { camelCase┃: 99999 }"], - ovec!["val = { camelCaseB┃: 99999 }"], - 'B', - )?; - assert_insert_nls( - ovec!["val = { camel┃Case: 88156 }"], - ovec!["val = { camelZ┃Case: 88156 }"], - 'Z', - )?; - assert_insert_nls( - ovec!["val = { ┃camelCase: 1 }"], - ovec!["val = { z┃camelCase: 1 }"], - 'z', - )?; - - assert_insert_seq_nls( - ovec!["val = { ┃ }"], - ovec!["val = { camelCase: \"hello┃\" }"], - "camelCase:\"hello", - )?; - assert_insert_seq_nls( - ovec!["val = { ┃ }"], - ovec!["val = { camelCase: 10009┃ }"], - "camelCase:10009", - )?; - - Ok(()) - } - - #[test] - fn test_nested_record() -> Result<(), String> { - assert_insert_seq_nls(ovec!["val = { a┃ }"], ovec!["val = { a: { ┃ } }"], ":{")?; - assert_insert_seq_nls(ovec!["val = { abc┃ }"], ovec!["val = { abc: { ┃ } }"], ":{")?; - assert_insert_seq_nls( - ovec!["val = { camelCase┃ }"], - ovec!["val = { camelCase: { ┃ } }"], - ":{", - )?; - - assert_insert_seq_nls( - ovec!["val = { a: { ┃ } }"], - ovec!["val = { a: { zulu┃ } }"], - "zulu", - )?; - assert_insert_seq_nls( - ovec!["val = { abc: { ┃ } }"], - ovec!["val = { abc: { camelCase┃ } }"], - "camelCase", - )?; - assert_insert_seq_nls( - ovec!["val = { camelCase: { ┃ } }"], - ovec!["val = { camelCase: { z┃ } }"], - "z", - )?; - - assert_insert_seq_nls( - ovec!["val = { a: { zulu┃ } }"], - ovec!["val = { a: { zulu: ┃ } }"], - ":", - )?; - assert_insert_seq_nls( - ovec!["val = { abc: { camelCase┃ } }"], - ovec!["val = { abc: { camelCase: ┃ } }"], - ":", - )?; - assert_insert_seq_nls( - ovec!["val = { camelCase: { z┃ } }"], - ovec!["val = { camelCase: { z: ┃ } }"], - ":", - )?; - - assert_insert_seq_nls( - ovec!["val = { a┃: { zulu } }"], - ovec!["val = { a0┃: { zulu } }"], - "0", - )?; - assert_insert_seq_nls( - ovec!["val = { ab┃c: { camelCase } }"], - ovec!["val = { abz┃c: { camelCase } }"], - "z", - )?; - assert_insert_seq_nls( - ovec!["val = { ┃camelCase: { z } }"], - ovec!["val = { x┃camelCase: { z } }"], - "x", - )?; - - assert_insert_seq_nls( - ovec!["val = { a: { zulu┃ } }"], - ovec!["val = { a: { zulu: \"┃\" } }"], - ":\"", - )?; - assert_insert_seq_nls( - ovec!["val = { abc: { camelCase┃ } }"], - ovec!["val = { abc: { camelCase: \"┃\" } }"], - ":\"", - )?; - assert_insert_seq_nls( - ovec!["val = { camelCase: { z┃ } }"], - ovec!["val = { camelCase: { z: \"┃\" } }"], - ":\"", - )?; - - assert_insert_seq_nls( - ovec!["val = { a: { zulu: \"┃\" } }"], - ovec!["val = { a: { zulu: \"azula┃\" } }"], - "azula", - )?; - assert_insert_seq_nls( - ovec!["val = { a: { zulu: \"az┃a\" } }"], - ovec!["val = { a: { zulu: \"azul┃a\" } }"], - "ul", - )?; - - assert_insert_seq_nls( - ovec!["val = { a: { zulu┃ } }"], - ovec!["val = { a: { zulu: 1┃ } }"], - ":1", - )?; - assert_insert_seq_nls( - ovec!["val = { abc: { camelCase┃ } }"], - ovec!["val = { abc: { camelCase: 0┃ } }"], - ":0", - )?; - assert_insert_seq_nls( - ovec!["val = { camelCase: { z┃ } }"], - ovec!["val = { camelCase: { z: 45┃ } }"], - ":45", - )?; - - assert_insert_seq_nls( - ovec!["val = { a: { zulu: ┃0 } }"], - ovec!["val = { a: { zulu: 4┃0 } }"], - "4", - )?; - assert_insert_seq_nls( - ovec!["val = { a: { zulu: 10┃98 } }"], - ovec!["val = { a: { zulu: 1077┃98 } }"], - "77", - )?; - - assert_insert_seq_nls( - ovec!["val = { a: { zulu┃ } }"], - ovec!["val = { a: { zulu: { ┃ } } }"], - ":{", - )?; - assert_insert_seq_nls( - ovec!["val = { abc: { camelCase┃ } }"], - ovec!["val = { abc: { camelCase: { ┃ } } }"], - ":{", - )?; - assert_insert_seq_nls( - ovec!["val = { camelCase: { z┃ } }"], - ovec!["val = { camelCase: { z: { ┃ } } }"], - ":{", - )?; - - assert_insert_seq_nls( - ovec!["val = { a: { zulu: { ┃ } } }"], - ovec!["val = { a: { zulu: { he┃ } } }"], - "he", - )?; - assert_insert_seq_nls( - ovec!["val = { a: { ┃zulu: { } } }"], - ovec!["val = { a: { x┃zulu: { } } }"], - "x", - )?; - assert_insert_seq_nls( - ovec!["val = { a: { z┃ulu: { } } }"], - ovec!["val = { a: { z9┃ulu: { } } }"], - "9", - )?; - assert_insert_seq_nls( - ovec!["val = { a: { zulu┃: { } } }"], - ovec!["val = { a: { zulu7┃: { } } }"], - "7", - )?; - - assert_insert_seq_nls( - ovec!["val = { a┃: { bcD: { eFgHij: { k15 } } } }"], - ovec!["val = { a4┃: { bcD: { eFgHij: { k15 } } } }"], - "4", - )?; - assert_insert_seq_nls( - ovec!["val = { ┃a: { bcD: { eFgHij: { k15 } } } }"], - ovec!["val = { y┃a: { bcD: { eFgHij: { k15 } } } }"], - "y", - )?; - assert_insert_seq_nls( - ovec!["val = { a: { bcD: { eF┃gHij: { k15 } } } }"], - ovec!["val = { a: { bcD: { eFxyz┃gHij: { k15 } } } }"], - "xyz", - )?; - - assert_insert_seq_nls( - ovec!["val = { ┃ }"], - ovec!["val = { g: { oi: { ng: { d: { e: { e: { p: { camelCase┃ } } } } } } } }"], - "g:{oi:{ng:{d:{e:{e:{p:{camelCase", - )?; - - Ok(()) - } - - fn concat_strings(str_a: &str, str_b: &str) -> String { - let mut string_a = str_a.to_owned(); - - string_a.push_str(str_b); - - string_a - } - - #[test] - fn test_ignore_record() -> Result<(), String> { - assert_insert_seq_ignore_nls(ovec!["val = ┃{ }"], IGNORE_CHARS)?; - assert_insert_seq_ignore_nls(ovec!["val = { }┃"], IGNORE_CHARS)?; - assert_insert_seq_ignore_nls(ovec!["val = {┃ }"], IGNORE_CHARS)?; - assert_insert_seq_ignore_nls(ovec!["val = { ┃}"], IGNORE_CHARS)?; - - assert_insert_seq_ignore_nls(ovec!["val = { ┃ }"], IGNORE_NO_LTR)?; - assert_insert_seq_ignore_nls(ovec!["val = { ┃a }"], IGNORE_NO_LTR)?; - assert_insert_seq_ignore_nls(ovec!["val = { ┃abc }"], IGNORE_NO_LTR)?; - - assert_insert_seq_ignore_nls(ovec!["val = ┃{ a }"], IGNORE_CHARS)?; - assert_insert_seq_nls( - ovec!["val = { a┃ }"], - ovec!["val = { a:┃ }"], - &concat_strings(":🡰", IGNORE_CHARS), - )?; - assert_insert_seq_nls( - ovec!["val = { a┃ }"], - ovec!["val = { a: ┃ }"], - &concat_strings(":🡲", IGNORE_CHARS), - )?; - assert_insert_seq_ignore_nls(ovec!["val = {┃ a }"], IGNORE_CHARS)?; - - assert_insert_seq_ignore_nls(ovec!["val = ┃{ a15 }"], IGNORE_CHARS)?; - assert_insert_seq_ignore_nls(ovec!["val = {┃ a15 }"], IGNORE_CHARS)?; - - assert_insert_seq_ignore_nls(ovec!["val = ┃{ camelCase }"], IGNORE_CHARS)?; - assert_insert_seq_ignore_nls(ovec!["val = {┃ camelCase }"], IGNORE_CHARS)?; - assert_insert_seq_nls( - ovec!["val = { camelCase┃ }"], - ovec!["val = { camelCase:┃ }"], - &concat_strings(":🡰", IGNORE_CHARS), - )?; - assert_insert_seq_nls( - ovec!["val = { camelCase┃ }"], - ovec!["val = { camelCase: ┃ }"], - &concat_strings(":🡲", IGNORE_CHARS), - )?; - - assert_insert_seq_ignore_nls(ovec!["val = ┃{ a: \"\" }"], IGNORE_CHARS)?; - assert_insert_seq_ignore_nls(ovec!["val = {┃ a: \"\" }"], IGNORE_CHARS)?; - assert_insert_seq_ignore_nls(ovec!["val = { a: ┃\"\" }"], IGNORE_CHARS)?; - assert_insert_seq_ignore_nls(ovec!["val = { a: \"\"┃ }"], IGNORE_CHARS)?; - assert_insert_seq_ignore_nls(ovec!["val = { a: \"\" }┃"], IGNORE_CHARS)?; - - assert_insert_seq_ignore_nls(ovec!["val = ┃{ a: 1 }"], IGNORE_CHARS)?; - assert_insert_seq_ignore_nls(ovec!["val = {┃ a: 2 }"], IGNORE_CHARS)?; - assert_insert_seq_ignore_nls(ovec!["val = { a: ┃6 }"], IGNORE_NO_NUM)?; - assert_insert_seq_ignore_nls(ovec!["val = { a: 8┃ }"], IGNORE_NO_NUM)?; - assert_insert_seq_ignore_nls(ovec!["val = { a: 0 }┃"], IGNORE_CHARS)?; - - assert_insert_seq_ignore_nls(ovec!["val = ┃{ camelCase: 1 }"], IGNORE_CHARS)?; - assert_insert_seq_ignore_nls(ovec!["val = {┃ camelCase: 7 }"], IGNORE_CHARS)?; - assert_insert_seq_ignore_nls(ovec!["val = { camelCase: ┃2 }"], IGNORE_NO_NUM)?; - assert_insert_seq_ignore_nls(ovec!["val = { camelCase: 4┃ }"], IGNORE_NO_NUM)?; - assert_insert_seq_ignore_nls(ovec!["val = { camelCase: 9 }┃"], IGNORE_CHARS)?; - - assert_insert_seq_ignore_nls(ovec!["val = ┃{ camelCase: \"\" }"], IGNORE_CHARS)?; - assert_insert_seq_ignore_nls(ovec!["val = {┃ camelCase: \"\" }"], IGNORE_CHARS)?; - assert_insert_seq_ignore_nls(ovec!["val = { camelCase: ┃\"\" }"], IGNORE_CHARS)?; - assert_insert_seq_ignore_nls(ovec!["val = { camelCase: \"\"┃ }"], IGNORE_CHARS)?; - assert_insert_seq_ignore_nls(ovec!["val = { camelCase: \"\" }┃"], IGNORE_CHARS)?; - - assert_insert_seq_ignore_nls(ovec!["val = ┃{ a: \"z\" }"], IGNORE_CHARS)?; - assert_insert_seq_ignore_nls(ovec!["val = {┃ a: \"z\" }"], IGNORE_CHARS)?; - assert_insert_seq_ignore_nls(ovec!["val = { a: ┃\"z\" }"], IGNORE_CHARS)?; - assert_insert_seq_ignore_nls(ovec!["val = { a: \"z\"┃ }"], IGNORE_CHARS)?; - assert_insert_seq_ignore_nls(ovec!["val = { a: \"z\" }┃"], IGNORE_CHARS)?; - - assert_insert_seq_ignore_nls( - ovec!["val = ┃{ a: \"hello, hello.0123456789ZXY{}[]-><-\" }"], - IGNORE_CHARS, - )?; - assert_insert_seq_ignore_nls( - ovec!["val = {┃ a: \"hello, hello.0123456789ZXY{}[]-><-\" }"], - IGNORE_CHARS, - )?; - assert_insert_seq_ignore_nls( - ovec!["val = { a: ┃\"hello, hello.0123456789ZXY{}[]-><-\" }"], - IGNORE_CHARS, - )?; - assert_insert_seq_ignore_nls( - ovec!["val = { a: \"hello, hello.0123456789ZXY{}[]-><-\"┃ }"], - IGNORE_CHARS, - )?; - assert_insert_seq_ignore_nls( - ovec!["val = { a: \"hello, hello.0123456789ZXY{}[]-><-\" }┃"], - IGNORE_CHARS, - )?; - - assert_insert_seq_ignore_nls(ovec!["val = ┃{ a: 915480 }"], IGNORE_CHARS)?; - assert_insert_seq_ignore_nls(ovec!["val = {┃ a: 915480 }"], IGNORE_CHARS)?; - assert_insert_seq_ignore_nls(ovec!["val = { a: ┃915480 }"], IGNORE_NO_NUM)?; - assert_insert_seq_ignore_nls(ovec!["val = { a: 915480┃ }"], IGNORE_NO_NUM)?; - assert_insert_seq_ignore_nls(ovec!["val = { a: 915480 }┃"], IGNORE_CHARS)?; - - Ok(()) - } - - #[test] - fn test_ignore_nested_record() -> Result<(), String> { - assert_insert_seq_ignore_nls(ovec!["val = { a: { ┃ } }"], IGNORE_NO_LTR)?; - assert_insert_seq_ignore_nls(ovec!["val = { a: ┃{ } }"], IGNORE_NO_LTR)?; - assert_insert_seq_ignore_nls(ovec!["val = { a: {┃ } }"], IGNORE_NO_LTR)?; - assert_insert_seq_ignore_nls(ovec!["val = { a: { }┃ }"], IGNORE_NO_LTR)?; - assert_insert_seq_ignore_nls(ovec!["val = { a: { } ┃}"], IGNORE_NO_LTR)?; - assert_insert_seq_ignore_nls(ovec!["val = { a: { } }┃"], IGNORE_NO_LTR)?; - assert_insert_seq_ignore_nls(ovec!["val = { a:┃ { } }"], IGNORE_NO_LTR)?; - assert_insert_seq_ignore_nls(ovec!["val = {┃ a: { } }"], IGNORE_NO_LTR)?; - assert_insert_seq_ignore_nls(ovec!["val = ┃{ a: { } }"], IGNORE_NO_LTR)?; - assert_insert_seq_ignore_nls(ovec!["val = { ┃a: { } }"], "1")?; - - assert_insert_seq_ignore_nls(ovec!["val = { camelCaseB1: {┃ z15a } }"], IGNORE_NO_LTR)?; - assert_insert_seq_ignore_nls(ovec!["val = { camelCaseB1: ┃{ z15a } }"], IGNORE_NO_LTR)?; - assert_insert_seq_nls( - ovec!["val = { camelCaseB1: { z15a┃ } }"], - ovec!["val = { camelCaseB1: { z15a:┃ } }"], - &concat_strings(":🡰", IGNORE_CHARS), - )?; - assert_insert_seq_nls( - ovec!["val = { camelCaseB1: { z15a┃ } }"], - ovec!["val = { camelCaseB1: { z15a: ┃ } }"], - &concat_strings(":🡲", IGNORE_CHARS), - )?; - assert_insert_seq_ignore_nls(ovec!["val = { camelCaseB1:┃ { z15a } }"], IGNORE_NO_LTR)?; - assert_insert_seq_ignore_nls(ovec!["val = {┃ camelCaseB1: { z15a } }"], IGNORE_NO_LTR)?; - assert_insert_seq_ignore_nls(ovec!["val = ┃{ camelCaseB1: { z15a } }"], IGNORE_NO_LTR)?; - assert_insert_seq_ignore_nls(ovec!["val = { ┃camelCaseB1: { z15a } }"], "1")?; - assert_insert_seq_ignore_nls(ovec!["val = { camelCaseB1: { ┃z15a } }"], "1")?; - - assert_insert_seq_ignore_nls( - ovec!["val = { camelCaseB1: { z15a: \"\"┃ } }"], - IGNORE_NO_LTR, - )?; - assert_insert_seq_ignore_nls( - ovec!["val = { camelCaseB1: { z15a: ┃\"\" } }"], - IGNORE_NO_LTR, - )?; - assert_insert_seq_ignore_nls( - ovec!["val = { camelCaseB1: { z15a:┃ \"\" } }"], - IGNORE_NO_LTR, - )?; - assert_insert_seq_ignore_nls( - ovec!["val = { camelCaseB1: { z15a: \"\" ┃} }"], - IGNORE_NO_LTR, - )?; - assert_insert_seq_ignore_nls( - ovec!["val = { camelCaseB1: {┃ z15a: \"\" } }"], - IGNORE_NO_LTR, - )?; - assert_insert_seq_ignore_nls( - ovec!["val = { camelCaseB1: ┃{ z15a: \"\" } }"], - IGNORE_NO_LTR, - )?; - assert_insert_seq_ignore_nls( - ovec!["val = { camelCaseB1: { z15a: \"\" }┃ }"], - IGNORE_NO_LTR, - )?; - assert_insert_seq_ignore_nls( - ovec!["val = { camelCaseB1: { z15a: \"\" } ┃}"], - IGNORE_NO_LTR, - )?; - assert_insert_seq_ignore_nls( - ovec!["val = { camelCaseB1: { z15a: \"\" } }┃"], - IGNORE_NO_LTR, - )?; - assert_insert_seq_ignore_nls( - ovec!["val = { camelCaseB1:┃ { z15a: \"\" } }"], - IGNORE_NO_LTR, - )?; - assert_insert_seq_ignore_nls( - ovec!["val = {┃ camelCaseB1: { z15a: \"\" } }"], - IGNORE_NO_LTR, - )?; - assert_insert_seq_ignore_nls( - ovec!["val = ┃{ camelCaseB1: { z15a: \"\" } }"], - IGNORE_NO_LTR, - )?; - assert_insert_seq_ignore_nls(ovec!["val = { ┃camelCaseB1: { z15a: \"\" } }"], "1")?; - assert_insert_seq_ignore_nls(ovec!["val = { camelCaseB1: { ┃z15a: \"\" } }"], "1")?; - - assert_insert_seq_ignore_nls(ovec!["val = { camelCaseB1: { z15a: 0┃ } }"], IGNORE_NO_NUM)?; - assert_insert_seq_ignore_nls( - ovec!["val = { camelCaseB1: { z15a: ┃123 } }"], - IGNORE_NO_NUM, - )?; - assert_insert_seq_ignore_nls( - ovec!["val = { camelCaseB1: { z15a:┃ 999 } }"], - IGNORE_NO_NUM, - )?; - assert_insert_seq_ignore_nls(ovec!["val = { camelCaseB1: { z15a: 80 ┃} }"], IGNORE_NO_NUM)?; - assert_insert_seq_ignore_nls( - ovec!["val = { camelCaseB1: {┃ z15a: 99000 } }"], - IGNORE_NO_NUM, - )?; - assert_insert_seq_ignore_nls(ovec!["val = { camelCaseB1: ┃{ z15a: 12 } }"], IGNORE_NO_NUM)?; - assert_insert_seq_ignore_nls(ovec!["val = { camelCaseB1: { z15a: 7 }┃ }"], IGNORE_NO_NUM)?; - assert_insert_seq_ignore_nls(ovec!["val = { camelCaseB1: { z15a: 98 } ┃}"], IGNORE_NO_NUM)?; - assert_insert_seq_ignore_nls( - ovec!["val = { camelCaseB1: { z15a: 4582 } }┃"], - IGNORE_NO_NUM, - )?; - assert_insert_seq_ignore_nls(ovec!["val = { camelCaseB1:┃ { z15a: 0 } }"], IGNORE_NO_NUM)?; - assert_insert_seq_ignore_nls(ovec!["val = {┃ camelCaseB1: { z15a: 44 } }"], IGNORE_NO_NUM)?; - assert_insert_seq_ignore_nls( - ovec!["val = ┃{ camelCaseB1: { z15a: 100123 } }"], - IGNORE_NO_NUM, - )?; - assert_insert_seq_ignore_nls(ovec!["val = { ┃camelCaseB1: { z15a: 5 } }"], "1")?; - assert_insert_seq_ignore_nls(ovec!["val = { camelCaseB1: { ┃z15a: 6 } }"], "1")?; - - assert_insert_seq_ignore_nls( - ovec!["val = { camelCaseB1: { z15a: \"hello, hello.0123456789ZXY{}[]-><-\"┃ } }"], - IGNORE_NO_LTR, - )?; - assert_insert_seq_ignore_nls( - ovec!["val = { camelCaseB1: { z15a: ┃\"hello, hello.0123456789ZXY{}[]-><-\" } }"], - IGNORE_NO_LTR, - )?; - assert_insert_seq_ignore_nls( - ovec!["val = { camelCaseB1: { z15a:┃ \"hello, hello.0123456789ZXY{}[]-><-\" } }"], - IGNORE_NO_LTR, - )?; - assert_insert_seq_ignore_nls( - ovec!["val = { camelCaseB1: { z15a: \"hello, hello.0123456789ZXY{}[]-><-\" ┃} }"], - IGNORE_NO_LTR, - )?; - assert_insert_seq_ignore_nls( - ovec!["val = { camelCaseB1: {┃ z15a: \"hello, hello.0123456789ZXY{}[]-><-\" } }"], - IGNORE_NO_LTR, - )?; - assert_insert_seq_ignore_nls( - ovec!["val = { camelCaseB1: ┃{ z15a: \"hello, hello.0123456789ZXY{}[]-><-\" } }"], - IGNORE_NO_LTR, - )?; - assert_insert_seq_ignore_nls( - ovec!["val = { camelCaseB1: { z15a: \"hello, hello.0123456789ZXY{}[]-><-\" }┃ }"], - IGNORE_NO_LTR, - )?; - assert_insert_seq_ignore_nls( - ovec!["val = { camelCaseB1: { z15a: \"hello, hello.0123456789ZXY{}[]-><-\" } ┃}"], - IGNORE_NO_LTR, - )?; - assert_insert_seq_ignore_nls( - ovec!["val = { camelCaseB1: { z15a: \"hello, hello.0123456789ZXY{}[]-><-\" } }┃"], - IGNORE_NO_LTR, - )?; - assert_insert_seq_ignore_nls( - ovec!["val = { camelCaseB1:┃ { z15a: \"hello, hello.0123456789ZXY{}[]-><-\" } }"], - IGNORE_NO_LTR, - )?; - assert_insert_seq_ignore_nls( - ovec!["val = {┃ camelCaseB1: { z15a: \"hello, hello.0123456789ZXY{}[]-><-\" } }"], - IGNORE_NO_LTR, - )?; - assert_insert_seq_ignore_nls( - ovec!["val = ┃{ camelCaseB1: { z15a: \"hello, hello.0123456789ZXY{}[]-><-\" } }"], - IGNORE_NO_LTR, - )?; - assert_insert_seq_ignore_nls( - ovec!["val = { ┃camelCaseB1: { z15a: \"hello, hello.0123456789ZXY{}[]-><-\" } }"], - "1", - )?; - assert_insert_seq_ignore_nls( - ovec!["val = { camelCaseB1: { ┃z15a: \"hello, hello.0123456789ZXY{}[]-><-\" } }"], - "1", - )?; - - assert_insert_seq_ignore_nls( - ovec!["val = { g: { oi: { ng: { d: { e: { e: { p: { camelCase } } } } } } } }┃"], - IGNORE_NO_LTR, - )?; - assert_insert_seq_ignore_nls( - ovec!["val = { g: { oi: { ng: { d: { e: {┃ e: { p: { camelCase } } } } } } } }"], - IGNORE_NO_LTR, - )?; - assert_insert_seq_ignore_nls( - ovec!["val = { g: { oi: { ng: { d: { e: { e:┃ { p: { camelCase } } } } } } } }"], - IGNORE_NO_LTR, - )?; - assert_insert_seq_ignore_nls( - ovec!["val = {┃ g: { oi: { ng: { d: { e: { e: { p: { camelCase } } } } } } } }"], - IGNORE_NO_LTR, - )?; - assert_insert_seq_ignore_nls( - ovec!["val = ┃{ g: { oi: { ng: { d: { e: { e: { p: { camelCase } } } } } } } }"], - IGNORE_NO_LTR, - )?; - assert_insert_seq_ignore_nls( - ovec!["val = { ┃g: { oi: { ng: { d: { e: { e: { p: { camelCase } } } } } } } }"], - "2", - )?; - Ok(()) - } - - #[test] - fn test_single_elt_list() -> Result<(), String> { - assert_insert_in_def_nls(ovec!["[ ┃ ]"], '[')?; - - assert_insert_nls(ovec!["val = [ ┃ ]"], ovec!["val = [ 0┃ ]"], '0')?; - assert_insert_nls(ovec!["val = [ ┃ ]"], ovec!["val = [ 1┃ ]"], '1')?; - assert_insert_nls(ovec!["val = [ ┃ ]"], ovec!["val = [ 9┃ ]"], '9')?; - - assert_insert_nls(ovec!["val = [ ┃ ]"], ovec!["val = [ \"┃\" ]"], '\"')?; - assert_insert_seq_nls( - ovec!["val = [ ┃ ]"], - ovec!["val = [ \"hello, hello.0123456789ZXY{}[]-><-┃\" ]"], - "\"hello, hello.0123456789ZXY{}[]-><-", - )?; - - assert_insert_nls(ovec!["val = [ ┃ ]"], ovec!["val = [ { ┃ } ]"], '{')?; - assert_insert_seq_nls(ovec!["val = [ ┃ ]"], ovec!["val = [ { a┃ } ]"], "{a")?; - assert_insert_seq_nls( - ovec!["val = [ ┃ ]"], - ovec!["val = [ { camelCase: { zulu: \"nested┃\" } } ]"], - "{camelCase:{zulu:\"nested", - )?; - - assert_insert_nls(ovec!["val = [ ┃ ]"], ovec!["val = [ [ ┃ ] ]"], '[')?; - assert_insert_seq_nls(ovec!["val = [ ┃ ]"], ovec!["val = [ [ [ ┃ ] ] ]"], "[[")?; - assert_insert_seq_nls(ovec!["val = [ ┃ ]"], ovec!["val = [ [ 0┃ ] ]"], "[0")?; - assert_insert_seq_nls( - ovec!["val = [ ┃ ]"], - ovec!["val = [ [ \"abc┃\" ] ]"], - "[\"abc", - )?; - assert_insert_seq_nls( - ovec!["val = [ ┃ ]"], - ovec!["val = [ [ { camelCase: { a: 79000┃ } } ] ]"], - "[{camelCase:{a:79000", - )?; - - Ok(()) - } - - #[test] - fn test_ignore_single_elt_list() -> Result<(), String> { - assert_insert_seq_ignore_nls(ovec!["val = ┃[ ]"], IGNORE_CHARS)?; - assert_insert_seq_ignore_nls(ovec!["val = [ ]┃"], IGNORE_CHARS)?; - assert_insert_seq_ignore_nls(ovec!["val = [┃ ]"], IGNORE_CHARS)?; - assert_insert_seq_ignore_nls(ovec!["val = [ ┃]"], IGNORE_CHARS)?; - - assert_insert_seq_ignore_nls(ovec!["val = ┃[ 0 ]"], IGNORE_CHARS)?; - assert_insert_seq_ignore_nls(ovec!["val = [ 0 ]┃"], IGNORE_CHARS)?; - assert_insert_seq_ignore_nls(ovec!["val = [┃ 0 ]"], IGNORE_CHARS)?; - assert_insert_seq_ignore_nls(ovec!["val = [ 0 ┃]"], IGNORE_CHARS)?; - - assert_insert_seq_ignore_nls(ovec!["val = ┃[ 137 ]"], IGNORE_CHARS)?; - assert_insert_seq_ignore_nls(ovec!["val = [ 137 ]┃"], IGNORE_CHARS)?; - assert_insert_seq_ignore_nls(ovec!["val = [┃ 137 ]"], IGNORE_CHARS)?; - assert_insert_seq_ignore_nls(ovec!["val = [ 137 ┃]"], IGNORE_CHARS)?; - assert_insert_seq_ignore_nls(ovec!["val = [ ┃137 ]"], IGNORE_NO_NUM)?; - assert_insert_seq_ignore_nls(ovec!["val = [ 137┃ ]"], IGNORE_NO_NUM)?; - - assert_insert_seq_ignore_nls(ovec!["val = ┃[ \"teststring\" ]"], IGNORE_CHARS)?; - assert_insert_seq_ignore_nls(ovec!["val = [ \"teststring\" ]┃"], IGNORE_CHARS)?; - assert_insert_seq_ignore_nls(ovec!["val = [┃ \"teststring\" ]"], IGNORE_CHARS)?; - assert_insert_seq_ignore_nls(ovec!["val = [ \"teststring\" ┃]"], IGNORE_CHARS)?; - assert_insert_seq_ignore_nls(ovec!["val = [ ┃\"teststring\" ]"], IGNORE_CHARS)?; - assert_insert_seq_ignore_nls(ovec!["val = [ \"teststring\"┃ ]"], IGNORE_CHARS)?; - - assert_insert_seq_ignore_nls(ovec!["val = ┃[ { a: 1 } ]"], IGNORE_CHARS)?; - assert_insert_seq_ignore_nls(ovec!["val = [ { a: 1 } ]┃"], IGNORE_CHARS)?; - assert_insert_seq_ignore_nls(ovec!["val = [┃ { a: 1 } ]"], IGNORE_CHARS)?; - assert_insert_seq_ignore_nls(ovec!["val = [ { a: 1 } ┃]"], IGNORE_CHARS)?; - assert_insert_seq_ignore_nls(ovec!["val = [ ┃{ a: 1 } ]"], IGNORE_CHARS)?; - assert_insert_seq_ignore_nls(ovec!["val = [ {┃ a: 1 } ]"], IGNORE_CHARS)?; - assert_insert_seq_ignore_nls(ovec!["val = [ { a:┃ 1 } ]"], IGNORE_CHARS)?; - assert_insert_seq_ignore_nls(ovec!["val = [ { a: 1 ┃} ]"], IGNORE_CHARS)?; - assert_insert_seq_ignore_nls(ovec!["val = [ { a: 1 }┃ ]"], IGNORE_CHARS)?; - - assert_insert_seq_ignore_nls(ovec!["val = ┃[ [ ] ]"], IGNORE_CHARS)?; - assert_insert_seq_ignore_nls(ovec!["val = [ [ ] ]┃"], IGNORE_CHARS)?; - assert_insert_seq_ignore_nls(ovec!["val = [┃ [ ] ]"], IGNORE_CHARS)?; - assert_insert_seq_ignore_nls(ovec!["val = [ [ ] ┃]"], IGNORE_CHARS)?; - assert_insert_seq_ignore_nls(ovec!["val = [ ┃[ ] ]"], IGNORE_CHARS)?; - assert_insert_seq_ignore_nls(ovec!["val = [ [ ]┃ ]"], IGNORE_CHARS)?; - assert_insert_seq_ignore_nls(ovec!["val = [ [┃ ] ]"], IGNORE_CHARS)?; - assert_insert_seq_ignore_nls(ovec!["val = [ [ ┃] ]"], IGNORE_CHARS)?; - - Ok(()) - } - - #[test] - fn test_multi_elt_list() -> Result<(), String> { - assert_insert_seq_nls(ovec!["val = [ ┃ ]"], ovec!["val = [ 0, 1┃ ]"], "0,1")?; - assert_insert_seq_nls( - ovec!["val = [ ┃ ]"], - ovec!["val = [ 987, 6543, 210┃ ]"], - "987,6543,210", - )?; - - assert_insert_seq_nls( - ovec!["val = [ ┃ ]"], - ovec!["val = [ \"a\", \"bcd\", \"EFGH┃\" ]"], - "\"a🡲,\"bcd🡲,\"EFGH", - )?; - - assert_insert_seq_nls( - ovec!["val = [ ┃ ]"], - ovec!["val = [ { a: 1 }, { b: 23 }, { c: 456┃ } ]"], - "{a:1🡲🡲,{b:23🡲🡲,{c:456", - )?; - - assert_insert_seq_nls( - ovec!["val = [ ┃ ]"], - ovec!["val = [ [ 1 ], [ 23 ], [ 456┃ ] ]"], - "[1🡲🡲,[23🡲🡲,[456", - )?; - - // insert element in between - assert_insert_seq_nls( - ovec!["val = [ ┃ ]"], - ovec!["val = [ 0, 2┃, 1 ]"], - "0,1🡰🡰🡰,2", - )?; - assert_insert_seq_nls( - ovec!["val = [ ┃ ]"], - ovec!["val = [ 0, 2, 3┃, 1 ]"], - "0,1🡰🡰🡰,2,3", - )?; - assert_insert_seq_nls( - ovec!["val = [ ┃ ]"], - ovec!["val = [ 0, 3┃, 2, 1 ]"], - "0,1🡰🡰🡰,2🡰🡰🡰,3", - )?; - - assert_insert_seq_nls( - ovec!["val = [ ┃ ]"], - ovec!["val = [ \"abc\", \"f┃\", \"de\" ]"], - "\"abc🡲,\"de🡰🡰🡰🡰🡰,\"f", - )?; - - assert_insert_seq_nls( - ovec!["val = [ ┃ ]"], - ovec!["val = [ [ 0 ], [ 2┃ ], [ 1 ] ]"], - "[0🡲🡲,[1🡰🡰🡰🡰🡰,[2", - )?; - - assert_insert_seq_nls( - ovec!["val = [ ┃ ]"], - ovec!["val = [ { a: 0 }, { a: 2┃ }, { a: 1 } ]"], - "{a:0🡲🡲,{a:1🡰🡰🡰🡰🡰🡰🡰🡰,{a:2", - )?; - - Ok(()) - } - - #[test] - fn test_ignore_multi_elt_list() -> Result<(), String> { - assert_insert_seq_ignore_nls(ovec!["val = ┃[ 0, 1 ]"], IGNORE_CHARS)?; - assert_insert_seq_ignore_nls(ovec!["val = [ 0, 1 ]┃"], IGNORE_CHARS)?; - assert_insert_seq_ignore_nls(ovec!["val = [┃ 0, 1 ]"], IGNORE_CHARS)?; - assert_insert_seq_ignore_nls(ovec!["val = [ 0, 1 ┃]"], IGNORE_CHARS)?; - assert_insert_seq_ignore_nls(ovec!["val = [ 0,┃ 1 ]"], IGNORE_CHARS)?; - - assert_insert_seq_ignore_nls(ovec!["val = ┃[ 123, 56, 7 ]"], IGNORE_CHARS)?; - assert_insert_seq_ignore_nls(ovec!["val = [ 123, 56, 7 ]┃"], IGNORE_CHARS)?; - assert_insert_seq_ignore_nls(ovec!["val = [┃ 123, 56, 7 ]"], IGNORE_CHARS)?; - assert_insert_seq_ignore_nls(ovec!["val = [ 123, 56, 7 ┃]"], IGNORE_CHARS)?; - assert_insert_seq_ignore_nls(ovec!["val = [ 123,┃ 56, 7 ]"], IGNORE_CHARS)?; - assert_insert_seq_ignore_nls(ovec!["val = [ 123, 56,┃ 7 ]"], IGNORE_CHARS)?; - - assert_insert_seq_ignore_nls(ovec!["val = ┃[ \"123\", \"56\", \"7\" ]"], IGNORE_CHARS)?; - assert_insert_seq_ignore_nls(ovec!["val = [ \"123\", \"56\", \"7\" ]┃"], IGNORE_CHARS)?; - assert_insert_seq_ignore_nls(ovec!["val = [┃ \"123\", \"56\", \"7\" ]"], IGNORE_CHARS)?; - assert_insert_seq_ignore_nls(ovec!["val = [ \"123\", \"56\", \"7\" ┃]"], IGNORE_CHARS)?; - assert_insert_seq_ignore_nls(ovec!["val = [ \"123\",┃ \"56\", \"7\" ]"], IGNORE_CHARS)?; - assert_insert_seq_ignore_nls(ovec!["val = [ \"123\", \"56\",┃ \"7\" ]"], IGNORE_CHARS)?; - - assert_insert_seq_ignore_nls(ovec!["val = ┃[ { a: 0 }, { a: 1 } ]"], IGNORE_CHARS)?; - assert_insert_seq_ignore_nls(ovec!["val = [ { a: 0 }, { a: 1 } ]┃"], IGNORE_CHARS)?; - assert_insert_seq_ignore_nls(ovec!["val = [┃ { a: 0 }, { a: 1 } ]"], IGNORE_CHARS)?; - assert_insert_seq_ignore_nls(ovec!["val = [ { a: 0 }, { a: 1 } ┃]"], IGNORE_CHARS)?; - assert_insert_seq_ignore_nls(ovec!["val = [ { a: 0 },┃ { a: 1 } ]"], IGNORE_CHARS)?; - - assert_insert_seq_ignore_nls(ovec!["val = ┃[ [ 0 ], [ 1 ] ]"], IGNORE_CHARS)?; - assert_insert_seq_ignore_nls(ovec!["val = [ [ 0 ], [ 1 ] ]┃"], IGNORE_CHARS)?; - assert_insert_seq_ignore_nls(ovec!["val = [┃ [ 0 ], [ 1 ] ]"], IGNORE_CHARS)?; - assert_insert_seq_ignore_nls(ovec!["val = [ [ 0 ], [ 1 ] ┃]"], IGNORE_CHARS)?; - assert_insert_seq_ignore_nls(ovec!["val = [ [ 0 ],┃ [ 1 ] ]"], IGNORE_CHARS)?; - assert_insert_seq_ignore_nls(ovec!["val = [ ┃[ 0 ], [ 1 ] ]"], IGNORE_CHARS)?; - assert_insert_seq_ignore_nls(ovec!["val = [ [ 0 ]┃, [ 1 ] ]"], IGNORE_CHARS)?; - assert_insert_seq_ignore_nls(ovec!["val = [ [┃ 0 ], [ 1 ] ]"], IGNORE_CHARS)?; - assert_insert_seq_ignore_nls(ovec!["val = [ [ 0 ┃], [ 1 ] ]"], IGNORE_CHARS)?; - assert_insert_seq_ignore_nls(ovec!["val = [ [ 0 ], ┃[ 1 ] ]"], IGNORE_CHARS)?; - assert_insert_seq_ignore_nls(ovec!["val = [ [ 0 ], [┃ 1 ] ]"], IGNORE_CHARS)?; - assert_insert_seq_ignore_nls(ovec!["val = [ [ 0 ], [ 1 ]┃ ]"], IGNORE_CHARS)?; - assert_insert_seq_ignore_nls(ovec!["val = [ [ 0 ], [ 1 ┃] ]"], IGNORE_CHARS)?; - - Ok(()) - } - - #[test] - fn test_tld_value() -> Result<(), String> { - assert_insert_nls(ovec!["┃"], ovec!["a┃ = "], 'a')?; - assert_insert_nls(ovec!["┃"], ovec!["m┃ = "], 'm')?; - assert_insert_nls(ovec!["┃"], ovec!["z┃ = "], 'z')?; - - assert_insert_seq_nls(ovec!["┃"], ovec!["ab┃ = "], "ab")?; - // TODO see issue #2548 - //assert_insert_seq_nls(ovec!["┃"], ovec!["mainVal┃ = "], "mainVal")?; - assert_insert_seq_nls(ovec!["┃"], ovec!["camelCase123┃ = "], "camelCase123")?; - assert_insert_seq_nls(ovec!["┃"], ovec!["c137┃ = "], "c137")?; - assert_insert_seq_nls(ovec!["┃"], ovec!["c137Bb┃ = "], "c137Bb")?; - assert_insert_seq_nls(ovec!["┃"], ovec!["bBbb┃ = "], "bBbb")?; - assert_insert_seq_nls(ovec!["┃"], ovec!["cC0Z┃ = "], "cC0Z")?; - - Ok(()) - } - - #[test] - fn test_ignore_tld_value() -> Result<(), String> { - assert_insert_seq_ignore_nls(ovec!["a ┃= 0"], IGNORE_CHARS)?; - assert_insert_seq_ignore_nls(ovec!["a =┃ 0"], IGNORE_CHARS)?; - - assert_insert_seq_ignore_nls(ovec!["aBC ┃= 0"], IGNORE_CHARS)?; - assert_insert_seq_ignore_nls(ovec!["aBC =┃ 0"], IGNORE_CHARS)?; - - assert_insert_seq_ignore_nls(ovec!["camelCase123 ┃= 0"], IGNORE_CHARS)?; - assert_insert_seq_ignore_nls(ovec!["camelCase123 =┃ 0"], IGNORE_CHARS)?; - - Ok(()) - } - - #[test] - fn test_enter() -> Result<(), String> { - assert_insert_seq( - ovec!["┃"], - add_nls(ovec!["ab = 5", "", "cd = \"good┃\""]), - "ab🡲🡲🡲5\rcd🡲🡲🡲\"good", - )?; - - Ok(()) - } - - // Create ed_model from pre_lines DSL, do handle_new_char for every char in input_seq, do ctrl+shift+up as many times as repeat. - // check if modified ed_model has expected string representation of code, caret position and active selection. - pub fn assert_ctrl_shift_up_repeat( - pre_lines: Vec, - expected_post_lines: Vec, - input_seq: &str, - repeats: usize, - ) -> Result<(), String> { - let mut code_str = pre_lines.join("").replace('┃', ""); - - let mut model_refs = init_model_refs(); - let code_arena = Bump::new(); - let module_ids = ModuleIds::default(); - - let mut ed_model = ed_model_from_dsl( - &mut code_str, - pre_lines, - &mut model_refs, - &module_ids, - &code_arena, - )?; - - for input_char in input_seq.chars() { - if input_char == '🡲' { - ed_model.simple_move_carets_right(1); - } else if input_char == '🡰' { - ed_model.simple_move_carets_left(1); - } else if input_char == '🡱' { - ed_model.simple_move_carets_up(1); - } else { - //dbg!(input_char); - ed_res_to_res(handle_new_char(&input_char, &mut ed_model))?; - } - } - - for _ in 0..repeats { - ed_model.ed_handle_key_down(&ctrl_cmd_shift(), Up, &mut ThreadPool::new(1))?; - } - - let mut post_lines = ui_res_to_res(ed_model_to_dsl(&ed_model))?; - strip_header(&mut post_lines); // remove header for clean tests - - assert_eq!(post_lines, add_nls(expected_post_lines)); - - Ok(()) - } - - pub fn assert_ctrl_shift_up_no_inp( - pre_lines: Vec, - expected_post_lines: Vec, - ) -> Result<(), String> { - assert_ctrl_shift_up_repeat(pre_lines, expected_post_lines, "", 1) - } - - pub fn assert_ctrl_shift_up_repeat_no_inp( - pre_lines: Vec, - expected_post_lines: Vec, - repeats: usize, - ) -> Result<(), String> { - assert_ctrl_shift_up_repeat(pre_lines, expected_post_lines, "", repeats) - } - - #[test] - fn test_ctrl_shift_up_blank() -> Result<(), String> { - // Blank is auto-inserted when creating top level def - assert_ctrl_shift_up_repeat(ovec!["┃"], ovec!["val = ┃❮ ❯"], "val=🡲🡲🡲", 1)?; - assert_ctrl_shift_up_repeat(ovec!["┃"], ovec!["┃❮val = ❯"], "val=🡲🡲🡲", 4)?; - - Ok(()) - } - - #[test] - fn test_ctrl_shift_up_int() -> Result<(), String> { - assert_ctrl_shift_up_no_inp(ovec!["val = 5┃"], ovec!["val = ┃❮5❯"])?; - assert_ctrl_shift_up_repeat_no_inp(ovec!["val = 0┃"], ovec!["┃❮val = 0❯"], 4)?; - assert_ctrl_shift_up_no_inp(ovec!["val = 12345┃"], ovec!["val = ┃❮12345❯"])?; - assert_ctrl_shift_up_no_inp(ovec!["val = ┃12345"], ovec!["val = ┃❮12345❯"])?; - assert_ctrl_shift_up_no_inp(ovec!["val = 1┃2345"], ovec!["val = ┃❮12345❯"])?; - assert_ctrl_shift_up_no_inp(ovec!["val = 12┃345"], ovec!["val = ┃❮12345❯"])?; - assert_ctrl_shift_up_no_inp(ovec!["val = 123┃45"], ovec!["val = ┃❮12345❯"])?; - assert_ctrl_shift_up_no_inp(ovec!["val = 1234┃5"], ovec!["val = ┃❮12345❯"])?; - - Ok(()) - } - - #[test] - fn test_ctrl_shift_up_string() -> Result<(), String> { - assert_ctrl_shift_up_no_inp(ovec!["val = \"┃\""], ovec!["val = ┃❮\"\"❯"])?; - assert_ctrl_shift_up_no_inp(ovec!["val = ┃\"\""], ovec!["val = ┃❮\"\"❯"])?; - assert_ctrl_shift_up_no_inp(ovec!["val = \"┃0\""], ovec!["val = ┃❮\"0\"❯"])?; - assert_ctrl_shift_up_no_inp(ovec!["val = \"0┃\""], ovec!["val = ┃❮\"0\"❯"])?; - assert_ctrl_shift_up_no_inp(ovec!["val = \"abc┃\""], ovec!["val = ┃❮\"abc\"❯"])?; - assert_ctrl_shift_up_no_inp(ovec!["val = \"ab┃c\""], ovec!["val = ┃❮\"abc\"❯"])?; - assert_ctrl_shift_up_no_inp(ovec!["val = \"┃abc\""], ovec!["val = ┃❮\"abc\"❯"])?; - assert_ctrl_shift_up_no_inp(ovec!["val = ┃\"abc\""], ovec!["val = ┃❮\"abc\"❯"])?; - assert_ctrl_shift_up_repeat_no_inp(ovec!["val = \"abc┃\""], ovec!["┃❮val = \"abc\"❯"], 4)?; - assert_ctrl_shift_up_no_inp( - ovec!["val = \"hello, hello.0123456789ZXY{}[]-><-┃\""], - ovec!["val = ┃❮\"hello, hello.0123456789ZXY{}[]-><-\"❯"], - )?; - - assert_ctrl_shift_up_no_inp(ovec!["val = \"\"┃"], ovec!["val = ┃❮\"\"❯"])?; - assert_ctrl_shift_up_no_inp(ovec!["val = \"abc\"┃"], ovec!["val = ┃❮\"abc\"❯"])?; - - Ok(()) - } - - #[test] - fn test_ctrl_shift_up_record() -> Result<(), String> { - assert_ctrl_shift_up_no_inp(ovec!["val = { ┃ }"], ovec!["val = ┃❮{ }❯"])?; - assert_ctrl_shift_up_no_inp(ovec!["val = {┃ }"], ovec!["val = ┃❮{ }❯"])?; - assert_ctrl_shift_up_no_inp(ovec!["val = ┃{ }"], ovec!["val = ┃❮{ }❯"])?; - assert_ctrl_shift_up_no_inp(ovec!["val = { ┃}"], ovec!["val = ┃❮{ }❯"])?; - assert_ctrl_shift_up_repeat_no_inp(ovec!["val = { ┃ }"], ovec!["┃❮val = { }❯"], 4)?; - assert_ctrl_shift_up_no_inp(ovec!["val = { }┃"], ovec!["val = ┃❮{ }❯"])?; - // TODO uncomment tests once #1649 is fixed - /*assert_ctrl_shift_up_no_inp(ovec!["val = { pear┃ }"], ovec!["val = ┃❮{ pear }❯"])?; - assert_ctrl_shift_up_no_inp(ovec!["val = { pea┃r }"], ovec!["val = ┃❮{ pear }❯"])?; - assert_ctrl_shift_up_no_inp(ovec!["val = { p┃ear }"], ovec!["val = ┃❮{ pear }❯"])?; - assert_ctrl_shift_up_no_inp(ovec!["val = { ┃pear }"], ovec!["val = ┃❮{ pear }❯"])?; - assert_ctrl_shift_up_no_inp(ovec!["val = {┃ pear }"], ovec!["val = ┃❮{ pear }❯"])?; - assert_ctrl_shift_up_no_inp(ovec!["val = ┃{ pear }"], ovec!["val = ┃❮{ pear }❯"])?; - assert_ctrl_shift_up_no_inp(ovec!["val = { pear ┃}"], ovec!["val = ┃❮{ pear }❯"])?; - assert_ctrl_shift_up_repeat(ovec!["val = { pear┃ }"], ovec!["val = ┃❮{ pear }❯"], 3)?; - assert_ctrl_shift_up_no_inp(ovec!["val = { pear }┃"], ovec!["val = ┃❮{ pear }❯"])?; - - assert_ctrl_shift_up_no_inp(ovec!["val = { camelCase123┃ }"], ovec!["val = ┃❮{ camelCase123 }❯"])?;*/ - - assert_ctrl_shift_up_no_inp(ovec!["val = { a: \"┃\" }"], ovec!["val = { a: ┃❮\"\"❯ }"])?; - assert_ctrl_shift_up_no_inp(ovec!["val = { a: ┃\"\" }"], ovec!["val = { a: ┃❮\"\"❯ }"])?; - assert_ctrl_shift_up_no_inp(ovec!["val = { a: \"\"┃ }"], ovec!["val = ┃❮{ a: \"\" }❯"])?; - assert_ctrl_shift_up_no_inp(ovec!["val = { a: \"\" ┃}"], ovec!["val = ┃❮{ a: \"\" }❯"])?; - assert_ctrl_shift_up_repeat_no_inp( - ovec!["val = { a: \"\" ┃}"], - ovec!["┃❮val = { a: \"\" }❯"], - 3, - )?; - assert_ctrl_shift_up_no_inp(ovec!["val = { a: \"\" }┃"], ovec!["val = ┃❮{ a: \"\" }❯"])?; - assert_ctrl_shift_up_no_inp(ovec!["val = { a:┃ \"\" }"], ovec!["val = ┃❮{ a: \"\" }❯"])?; - assert_ctrl_shift_up_no_inp(ovec!["val = { a┃: \"\" }"], ovec!["val = ┃❮{ a: \"\" }❯"])?; - assert_ctrl_shift_up_no_inp(ovec!["val = { ┃a: \"\" }"], ovec!["val = ┃❮{ a: \"\" }❯"])?; - assert_ctrl_shift_up_no_inp(ovec!["val = {┃ a: \"\" }"], ovec!["val = ┃❮{ a: \"\" }❯"])?; - assert_ctrl_shift_up_no_inp(ovec!["val = ┃{ a: \"\" }"], ovec!["val = ┃❮{ a: \"\" }❯"])?; - assert_ctrl_shift_up_repeat_no_inp( - ovec!["val = { a: \"┃\" }"], - ovec!["val = ┃❮{ a: \"\" }❯"], - 2, - )?; - assert_ctrl_shift_up_repeat_no_inp( - ovec!["val = { a: \"┃\" }"], - ovec!["┃❮val = { a: \"\" }❯"], - 4, - )?; - - assert_ctrl_shift_up_no_inp(ovec!["val = { a: 1┃0 }"], ovec!["val = { a: ┃❮10❯ }"])?; - assert_ctrl_shift_up_no_inp(ovec!["val = { a: ┃9 }"], ovec!["val = { a: ┃❮9❯ }"])?; - assert_ctrl_shift_up_no_inp(ovec!["val = { a: 98┃89 }"], ovec!["val = { a: ┃❮9889❯ }"])?; - assert_ctrl_shift_up_no_inp(ovec!["val = { a: 44┃ }"], ovec!["val = ┃❮{ a: 44 }❯"])?; - assert_ctrl_shift_up_no_inp(ovec!["val = { a: 0 ┃}"], ovec!["val = ┃❮{ a: 0 }❯"])?; - assert_ctrl_shift_up_repeat_no_inp( - ovec!["val = { a: 123 ┃}"], - ovec!["┃❮val = { a: 123 }❯"], - 3, - )?; - assert_ctrl_shift_up_no_inp(ovec!["val = { a: 96 }┃"], ovec!["val = ┃❮{ a: 96 }❯"])?; - assert_ctrl_shift_up_no_inp( - ovec!["val = { a:┃ 985600 }"], - ovec!["val = ┃❮{ a: 985600 }❯"], - )?; - assert_ctrl_shift_up_no_inp(ovec!["val = { a┃: 5648 }"], ovec!["val = ┃❮{ a: 5648 }❯"])?; - assert_ctrl_shift_up_no_inp( - ovec!["val = { ┃a: 1000000 }"], - ovec!["val = ┃❮{ a: 1000000 }❯"], - )?; - assert_ctrl_shift_up_no_inp(ovec!["val = {┃ a: 1 }"], ovec!["val = ┃❮{ a: 1 }❯"])?; - assert_ctrl_shift_up_no_inp( - ovec!["val = ┃{ a: 900600 }"], - ovec!["val = ┃❮{ a: 900600 }❯"], - )?; - assert_ctrl_shift_up_repeat_no_inp( - ovec!["val = { a: 10┃000 }"], - ovec!["val = ┃❮{ a: 10000 }❯"], - 2, - )?; - assert_ctrl_shift_up_repeat_no_inp( - ovec!["val = { a: ┃45 }"], - ovec!["┃❮val = { a: 45 }❯"], - 4, - )?; - - assert_ctrl_shift_up_no_inp( - ovec!["val = { abc: \"de┃\" }"], - ovec!["val = { abc: ┃❮\"de\"❯ }"], - )?; - assert_ctrl_shift_up_no_inp( - ovec!["val = { abc: \"d┃e\" }"], - ovec!["val = { abc: ┃❮\"de\"❯ }"], - )?; - assert_ctrl_shift_up_no_inp( - ovec!["val = { abc: \"┃de\" }"], - ovec!["val = { abc: ┃❮\"de\"❯ }"], - )?; - assert_ctrl_shift_up_no_inp( - ovec!["val = { abc: ┃\"de\" }"], - ovec!["val = { abc: ┃❮\"de\"❯ }"], - )?; - assert_ctrl_shift_up_no_inp( - ovec!["val = { abc: \"de\"┃ }"], - ovec!["val = ┃❮{ abc: \"de\" }❯"], - )?; - assert_ctrl_shift_up_repeat_no_inp( - ovec!["val = { abc: \"d┃e\" }"], - ovec!["val = ┃❮{ abc: \"de\" }❯"], - 2, - )?; - assert_ctrl_shift_up_repeat_no_inp( - ovec!["val = { abc: \"d┃e\" }"], - ovec!["┃❮val = { abc: \"de\" }❯"], - 3, - )?; - - assert_ctrl_shift_up_no_inp( - ovec!["val = { camelCase123: \"hello, hello.012┃3456789ZXY{}[]-><-\" }"], - ovec!["val = { camelCase123: ┃❮\"hello, hello.0123456789ZXY{}[]-><-\"❯ }"], - )?; - assert_ctrl_shift_up_no_inp( - ovec!["val = { camel┃Case123: \"hello, hello.0123456789ZXY{}[]-><-\" }"], - ovec!["val = ┃❮{ camelCase123: \"hello, hello.0123456789ZXY{}[]-><-\" }❯"], - )?; - assert_ctrl_shift_up_repeat_no_inp( - ovec!["val = { camelCase123: \"hello, hello┃.0123456789ZXY{}[]-><-\" }"], - ovec!["val = ┃❮{ camelCase123: \"hello, hello.0123456789ZXY{}[]-><-\" }❯"], - 2, - )?; - - Ok(()) - } - - #[test] - fn test_ctrl_shift_up_nested_record() -> Result<(), String> { - assert_ctrl_shift_up_no_inp( - ovec!["val = { abc: { ┃ } }"], - ovec!["val = { abc: ┃❮{ }❯ }"], - )?; - assert_ctrl_shift_up_no_inp( - ovec!["val = { abc: {┃ } }"], - ovec!["val = { abc: ┃❮{ }❯ }"], - )?; - assert_ctrl_shift_up_no_inp( - ovec!["val = { abc: ┃{ } }"], - ovec!["val = { abc: ┃❮{ }❯ }"], - )?; - assert_ctrl_shift_up_no_inp( - ovec!["val = { abc: { ┃} }"], - ovec!["val = { abc: ┃❮{ }❯ }"], - )?; - assert_ctrl_shift_up_no_inp( - ovec!["val = { abc: { }┃ }"], - ovec!["val = ┃❮{ abc: { } }❯"], - )?; - - // TODO uncomment tests once #1649 is fixed - /*assert_ctrl_shift_up_no_inp(ovec!["val = { abc: { ┃d } }"], ovec!["val = { abc: ┃❮{ d }❯ }"])?; - assert_ctrl_shift_up_no_inp(ovec!["val = { abc: {┃ d } }"], ovec!["val = { abc: ┃❮{ d }❯ }"])?; - assert_ctrl_shift_up_no_inp(ovec!["val = { abc: ┃{ d } }"], ovec!["val = { abc: ┃❮{ d }❯ }"])?; - assert_ctrl_shift_up_no_inp(ovec!["val = { abc: { d ┃} }"], ovec!["val = { abc: ┃❮{ d }❯ }"])?; - assert_ctrl_shift_up_no_inp(ovec!["val = { abc: { d┃e } }"], ovec!["val = { abc: ┃❮{ de }❯ }"])?; - assert_ctrl_shift_up_no_inp(ovec!["val = { abc: { d }┃ }"], ovec!["val = ┃❮{ abc: { d } }❯"])?; - assert_ctrl_shift_up_no_inp(ovec!["val = ┃{ abc: { d } }"], ovec!["val = ┃❮{ abc: { d } }❯"])?;*/ - - assert_ctrl_shift_up_no_inp( - ovec!["val = { abc: { de: { ┃ } } }"], - ovec!["val = { abc: { de: ┃❮{ }❯ } }"], - )?; - assert_ctrl_shift_up_no_inp( - ovec!["val = { abc: { de: ┃{ } } }"], - ovec!["val = { abc: { de: ┃❮{ }❯ } }"], - )?; - assert_ctrl_shift_up_no_inp( - ovec!["val = { abc: { de: { }┃ } }"], - ovec!["val = { abc: ┃❮{ de: { } }❯ }"], - )?; - - assert_ctrl_shift_up_no_inp( - ovec!["val = { abc: { de: \"┃\" } }"], - ovec!["val = { abc: { de: ┃❮\"\"❯ } }"], - )?; - assert_ctrl_shift_up_no_inp( - ovec!["val = { abc: { de: ┃\"\" } }"], - ovec!["val = { abc: { de: ┃❮\"\"❯ } }"], - )?; - assert_ctrl_shift_up_no_inp( - ovec!["val = { abc: { de: \"\"┃ } }"], - ovec!["val = { abc: ┃❮{ de: \"\" }❯ }"], - )?; - assert_ctrl_shift_up_no_inp( - ovec!["val = { abc: { de: \"f g┃\" } }"], - ovec!["val = { abc: { de: ┃❮\"f g\"❯ } }"], - )?; - assert_ctrl_shift_up_no_inp( - ovec!["val = { abc: { de┃: \"f g\" } }"], - ovec!["val = { abc: ┃❮{ de: \"f g\" }❯ }"], - )?; - assert_ctrl_shift_up_no_inp( - ovec!["val = { abc: {┃ de: \"f g\" } }"], - ovec!["val = { abc: ┃❮{ de: \"f g\" }❯ }"], - )?; - assert_ctrl_shift_up_no_inp( - ovec!["val = { abc: { de: \"f g\" ┃} }"], - ovec!["val = { abc: ┃❮{ de: \"f g\" }❯ }"], - )?; - assert_ctrl_shift_up_no_inp( - ovec!["val = { abc: { de: \"f g\" }┃ }"], - ovec!["val = ┃❮{ abc: { de: \"f g\" } }❯"], - )?; - assert_ctrl_shift_up_no_inp( - ovec!["val = ┃{ abc: { de: \"f g\" } }"], - ovec!["val = ┃❮{ abc: { de: \"f g\" } }❯"], - )?; - assert_ctrl_shift_up_no_inp( - ovec!["val = { abc: { de: \"f g\" } }┃"], - ovec!["val = ┃❮{ abc: { de: \"f g\" } }❯"], - )?; - - assert_ctrl_shift_up_repeat_no_inp( - ovec!["val = { abc: { de: \"f g┃\" } }"], - ovec!["val = { abc: ┃❮{ de: \"f g\" }❯ }"], - 2, - )?; - assert_ctrl_shift_up_repeat_no_inp( - ovec!["val = { abc: { de: ┃\"f g\" } }"], - ovec!["val = ┃❮{ abc: { de: \"f g\" } }❯"], - 3, - )?; - assert_ctrl_shift_up_repeat_no_inp( - ovec!["val = { abc: { de: ┃\"f g\" } }"], - ovec!["┃❮val = { abc: { de: \"f g\" } }❯"], - 4, - )?; - - assert_ctrl_shift_up_no_inp( - ovec!["val = { abc: { de: ┃951 } }"], - ovec!["val = { abc: { de: ┃❮951❯ } }"], - )?; - assert_ctrl_shift_up_no_inp( - ovec!["val = { abc: { de: 11┃0 } }"], - ovec!["val = { abc: { de: ┃❮110❯ } }"], - )?; - assert_ctrl_shift_up_no_inp( - ovec!["val = { abc: { de: 444┃ } }"], - ovec!["val = { abc: ┃❮{ de: 444 }❯ }"], - )?; - assert_ctrl_shift_up_no_inp( - ovec!["val = { abc: { de┃: 99 } }"], - ovec!["val = { abc: ┃❮{ de: 99 }❯ }"], - )?; - assert_ctrl_shift_up_no_inp( - ovec!["val = { abc: {┃ de: 0 } }"], - ovec!["val = { abc: ┃❮{ de: 0 }❯ }"], - )?; - assert_ctrl_shift_up_no_inp( - ovec!["val = { abc: { de: 230 ┃} }"], - ovec!["val = { abc: ┃❮{ de: 230 }❯ }"], - )?; - assert_ctrl_shift_up_no_inp( - ovec!["val = { abc: { de: 7 }┃ }"], - ovec!["val = ┃❮{ abc: { de: 7 } }❯"], - )?; - assert_ctrl_shift_up_no_inp( - ovec!["val = ┃{ abc: { de: 1 } }"], - ovec!["val = ┃❮{ abc: { de: 1 } }❯"], - )?; - assert_ctrl_shift_up_no_inp( - ovec!["val = { abc: { de: 111111 } }┃"], - ovec!["val = ┃❮{ abc: { de: 111111 } }❯"], - )?; - - assert_ctrl_shift_up_repeat_no_inp( - ovec!["val = { abc: { de: 1┃5 } }"], - ovec!["val = { abc: ┃❮{ de: 15 }❯ }"], - 2, - )?; - assert_ctrl_shift_up_repeat_no_inp( - ovec!["val = { abc: { de: ┃55 } }"], - ovec!["val = ┃❮{ abc: { de: 55 } }❯"], - 3, - )?; - assert_ctrl_shift_up_repeat_no_inp( - ovec!["val = { abc: { de: ┃400 } }"], - ovec!["┃❮val = { abc: { de: 400 } }❯"], - 5, - )?; - - // TODO uncomment tests once #1649 is fixed - /*assert_ctrl_shift_up_repeat_no_inp( - ovec!["val = { g: { oi: { ng: { d: { e: { e: { p: { camelCase┃ } } } } } } } }"], - ovec!["val = { g: { oi: { ng: { d: ┃❮{ e: { e: { p: { camelCase } } } }❯ } } } }"], - 4, - )?; - assert_ctrl_shift_up_repeat_no_inp( - ovec!["val = { g: { oi: { ng: { d: { e: { e: { p: { camelCase┃ } } } } } } } }"], - ovec!["val = { g: ┃❮{ oi: { ng: { d: { e: { e: { p: { camelCase } } } } } } }❯ }"], - 7, - )?; - assert_ctrl_shift_up_repeat_no_inp( - ovec!["val = { g: { oi: { ng: { d: { e: { e: { p: { camelCase┃ } } } } } } } }"], - ovec!["val = ┃❮{ g: { oi: { ng: { d: { e: { e: { p: { camelCase } } } } } } } }❯"], - 9, - )?;*/ - - Ok(()) - } - - // Create ed_model from pre_lines DSL, do handle_new_char() with new_char_seq, select current Expr2, - // check if generated tooltips match expected_tooltips. - pub fn assert_type_tooltips_seq( - pre_lines: Vec, - expected_tooltips: Vec, - new_char_seq: &str, - ) -> Result<(), String> { - let mut code_str = pre_lines.join("").replace('┃', ""); - - let mut model_refs = init_model_refs(); - let code_arena = Bump::new(); - let module_ids = ModuleIds::default(); - - let mut ed_model = ed_model_from_dsl( - &mut code_str, - pre_lines, - &mut model_refs, - &module_ids, - &code_arena, - )?; - - for input_char in new_char_seq.chars() { - if input_char == '🡲' { - ed_model.simple_move_carets_right(1); - } else { - ed_res_to_res(handle_new_char(&input_char, &mut ed_model))?; - } - } - - for expected_tooltip in expected_tooltips.iter() { - ed_model.select_expr()?; - - let created_tooltip = ed_model.selected_block_opt.unwrap().type_str; - - assert_eq!( - created_tooltip.as_str(ed_model.module.env.pool), - *expected_tooltip - ); - } - - Ok(()) - } - - // Create ed_model from pre_lines DSL, do handle_new_char() with new_char, select current Expr2, - // check if generated tooltip matches expected_tooltip. - pub fn assert_type_tooltip( - pre_lines: Vec, - expected_tooltip: &str, - new_char: char, - ) -> Result<(), String> { - assert_type_tooltips_seq(pre_lines, ovec![expected_tooltip], &new_char.to_string()) - } - - pub fn assert_type_tooltip_clean( - lines: Vec, - expected_tooltip: &str, - ) -> Result<(), String> { - assert_type_tooltips_seq(lines, ovec![expected_tooltip], "") - } - - // When doing ctrl+shift+up multiple times we select the surrounding expression every time, - // every new selection should have the correct tooltip - pub fn assert_type_tooltips_clean( - lines: Vec, - expected_tooltips: Vec, - ) -> Result<(), String> { - assert_type_tooltips_seq(lines, expected_tooltips, "") - } - - #[test] - fn test_type_tooltip() -> Result<(), String> { - assert_type_tooltip_clean(ovec!["val = ┃5"], "Num *")?; - assert_type_tooltip_clean(ovec!["val = 42┃"], "Num *")?; - assert_type_tooltip_clean(ovec!["val = 13┃7"], "Num *")?; - - assert_type_tooltip_clean(ovec!["val = \"┃abc\""], "Str")?; - assert_type_tooltip_clean(ovec!["val = ┃\"abc\""], "Str")?; - assert_type_tooltip_clean(ovec!["val = \"abc\"┃"], "Str")?; - - assert_type_tooltip_clean(ovec!["val = { ┃ }"], "{}")?; - assert_type_tooltip_clean(ovec!["val = { a: \"abc\" }┃"], "{ a : Str }")?; - assert_type_tooltip_clean(ovec!["val = { ┃a: 0 }"], "{ a : Num * }")?; - assert_type_tooltip_clean(ovec!["val = { ┃z: { } }"], "{ z : {} }")?; - assert_type_tooltip_clean(ovec!["val = { camelCase: ┃0 }"], "Num *")?; - - assert_type_tooltips_seq(ovec!["┃"], ovec!["*"], "val=🡲🡲🡲")?; - assert_type_tooltips_seq( - ovec!["┃"], - ovec!["*", "{ a : * }", "{ a : * }"], - "val=🡲🡲🡲{a:", - )?; - - assert_type_tooltips_clean( - ovec!["val = { camelCase: ┃0 }"], - ovec!["Num *", "{ camelCase : Num * }"], - )?; - assert_type_tooltips_clean( - ovec!["val = { a: { b: { c: \"hello┃, hello.0123456789ZXY{}[]-><-\" } } }"], - ovec![ - "Str", - "{ c : Str }", - "{ b : { c : Str } }", - "{ a : { b : { c : Str } } }" - ], - )?; - - Ok(()) - } - - #[test] - fn test_type_tooltip_list() -> Result<(), String> { - assert_type_tooltip_clean(ovec!["val = [ ┃ ]"], "List *")?; - assert_type_tooltips_clean(ovec!["val = [ ┃0 ]"], ovec!["Num *", "List (Num *)"])?; - assert_type_tooltips_clean( - ovec!["val = [ [ ┃0 ] ]"], - ovec!["Num *", "List (Num *)", "List (List (Num *))"], - )?; - - assert_type_tooltips_clean( - ovec!["val = [ [ [ \"ab┃c\" ] ] ]"], - ovec![ - "Str", - "List Str", - "List (List Str)", - "List (List (List Str))" - ], - )?; - assert_type_tooltips_clean( - ovec!["val = [ [ { a┃: 1 } ] ]"], - ovec![ - "{ a : Num * }", - "List { a : Num * }", - "List (List { a : Num * })" - ], - )?; - - // multi element lists - assert_type_tooltips_clean(ovec!["val = [ ┃1, 2, 3 ]"], ovec!["Num *", "List (Num *)"])?; - assert_type_tooltips_clean( - ovec!["val = [ \"┃abc\", \"de\", \"f\" ]"], - ovec!["Str", "List Str"], - )?; - assert_type_tooltips_clean( - ovec!["val = [ { a:┃ 1 }, { a: 12 }, { a: 444 } ]"], - ovec!["{ a : Num * }", "List { a : Num * }"], - )?; - - Ok(()) - } - - #[test] - fn test_type_tooltip_mismatch() -> Result<(), String> { - assert_type_tooltips_clean( - ovec!["val = [ 1, \"ab┃c\" ]"], - ovec!["Str", "List "], - )?; - assert_type_tooltips_clean( - ovec!["val = [ \"abc\", 5┃0 ]"], - ovec!["Num *", "List "], - )?; - - assert_type_tooltips_clean( - ovec!["val = [ { a: 0 }, { a: \"0┃\" } ]"], - ovec!["Str", "{ a : Str }", "List "], - )?; - - assert_type_tooltips_clean( - ovec!["val = [ [ 0, 1, \"2\" ], [ 3, 4, 5 ┃] ]"], - ovec!["List (Num *)", "List "], - )?; - - Ok(()) - } - - type ModelMoveCaretFun = fn(&mut EdModel<'_>, &Modifiers) -> UIResult<()>; - - // Create ed_model from pre_lines DSL, do ctrl+shift+up as many times as repeat. Then move the caret by executing - // move_caret_fun. Next check if modified ed_model has expected string representation of code, caret position and - // active selection. - fn assert_ctrl_shift_up_move( - pre_lines: Vec, - expected_post_lines: Vec, - repeats: usize, - move_caret_fun: ModelMoveCaretFun, - ) -> Result<(), String> { - let mut code_str = pre_lines.join("").replace('┃', ""); - - let mut model_refs = init_model_refs(); - let code_arena = Bump::new(); - let module_ids = ModuleIds::default(); - - let mut ed_model = ed_model_from_dsl( - &mut code_str, - pre_lines, - &mut model_refs, - &module_ids, - &code_arena, - )?; - - for _ in 0..repeats { - ed_model.ed_handle_key_down(&ctrl_cmd_shift(), Up, &mut ThreadPool::new(1))?; - } - - move_caret_fun(&mut ed_model, &no_mods())?; - - let mut post_lines = ui_res_to_res(ed_model_to_dsl(&ed_model))?; - strip_header(&mut post_lines); // remove header for clean tests - - assert_eq!(post_lines, expected_post_lines); - - Ok(()) - } - - fn assert_ctrl_shift_up_move_nls( - pre_lines: Vec, - expected_post_lines: Vec, - repeats: usize, - move_caret_fun: ModelMoveCaretFun, - ) -> Result<(), String> { - assert_ctrl_shift_up_move( - pre_lines, - add_nls(expected_post_lines), - repeats, - move_caret_fun, - ) - } - - fn assert_ctrl_shift_single_up_move( - pre_lines: Vec, - expected_post_lines: Vec, - move_caret_fun: ModelMoveCaretFun, - ) -> Result<(), String> { - assert_ctrl_shift_up_move(pre_lines, expected_post_lines, 1, move_caret_fun) - } - - fn assert_ctrl_shift_single_up_move_nls( - pre_lines: Vec, - expected_post_lines: Vec, - move_caret_fun: ModelMoveCaretFun, - ) -> Result<(), String> { - assert_ctrl_shift_single_up_move(pre_lines, add_nls(expected_post_lines), move_caret_fun) - } - - // because complex lifetime stuff - macro_rules! move_up { - () => { - |ed_model, modifiers| EdModel::move_caret_up(ed_model, modifiers) - }; - } - macro_rules! move_down { - () => { - |ed_model, modifiers| EdModel::move_caret_down(ed_model, modifiers) - }; - } - macro_rules! move_home { - () => { - |ed_model, modifiers| EdModel::move_caret_home(ed_model, modifiers) - }; - } - macro_rules! move_end { - () => { - |ed_model, modifiers| EdModel::move_caret_end(ed_model, modifiers) - }; - } - - #[test] - fn test_ctrl_shift_up_move_int() -> Result<(), String> { - assert_ctrl_shift_single_up_move_nls(ovec!["val = ┃0"], ovec!["val = 0┃"], move_down!())?; - assert_ctrl_shift_single_up_move_nls( - ovec!["val = ┃9654"], - ovec!["val = ┃9654"], - move_up!(), - )?; - assert_ctrl_shift_single_up_move_nls( - ovec!["val = ┃100546"], - ovec!["val = 100546┃"], - move_end!(), - )?; - - Ok(()) - } - - #[test] - fn test_ctrl_shift_up_move_string() -> Result<(), String> { - assert_ctrl_shift_single_up_move_nls( - ovec!["val = ┃\"\""], - ovec!["val = \"\"┃"], - move_down!(), - )?; - assert_ctrl_shift_single_up_move_nls( - ovec!["val = ┃\"abc\""], - ovec!["val = ┃\"abc\""], - move_up!(), - )?; - assert_ctrl_shift_single_up_move_nls( - ovec!["val = ┃\"hello, hello.0123456789ZXY{}[]-><-\""], - ovec!["val = \"hello, hello.0123456789ZXY{}[]-><-\"┃"], - move_end!(), - )?; - - Ok(()) - } - - #[test] - fn test_ctrl_shift_up_move_record() -> Result<(), String> { - assert_ctrl_shift_single_up_move_nls( - ovec!["val = ┃{ }"], - ovec!["┃val = { }"], - move_home!(), - )?; - // TODO uncomment tests once #1649 is fixed. - //assert_ctrl_shift_single_up_move(ovec!["┃{ a }"], ovec!["{ a }┃"], move_down!())?; - //assert_ctrl_shift_single_up_move(ovec!["┃{ a: { b } }"], ovec!["{ a: { b } }┃"], move_right!())?; - assert_ctrl_shift_single_up_move_nls( - ovec!["val = { a: { ┃ } }"], - ovec!["val = { a: { } }┃"], - move_end!(), - )?; - assert_ctrl_shift_up_move_nls( - ovec!["val = { a: { b: { ┃ } } }"], - ovec!["val = { a: ┃{ b: { } } }"], - 2, - move_up!(), - )?; - assert_ctrl_shift_up_move_nls( - ovec!["val = { camelCase: { cC123: \"hello┃, hello.0123456789ZXY{}[]-><-\" } }"], - ovec!["val = { camelCase: { cC123: \"hello, hello.0123456789ZXY{}[]-><-\" }┃ }"], - 2, - move_down!(), - )?; - - assert_ctrl_shift_up_move_nls( - ovec!["val = { camelCase: { cC123: 9┃5 } }"], - ovec!["val = { camelCase: { cC123: 95 }┃ }"], - 2, - move_down!(), - )?; - - Ok(()) - } - - // Create ed_model from pre_lines DSL, do ctrl+shift+up as many times as repeat. Then do backspace. - // Next check if modified ed_model has expected string representation of code, caret position and - // active selection. - fn assert_ctrl_shift_up_backspace( - pre_lines: Vec, - expected_post_lines: Vec, - repeats: usize, - ) -> Result<(), String> { - let mut code_str = pre_lines.join("").replace('┃', ""); - - let mut model_refs = init_model_refs(); - let code_arena = Bump::new(); - let module_ids = ModuleIds::default(); - - let mut ed_model = ed_model_from_dsl( - &mut code_str, - pre_lines, - &mut model_refs, - &module_ids, - &code_arena, - )?; - - for _ in 0..repeats { - ed_model.ed_handle_key_down(&ctrl_cmd_shift(), Up, &mut ThreadPool::new(1))?; - } - - handle_new_char(&'\u{8}', &mut ed_model)?; // \u{8} is the char for backspace on linux - - let mut post_lines = ui_res_to_res(ed_model_to_dsl(&ed_model))?; - strip_header(&mut post_lines); - - assert_eq!(post_lines, expected_post_lines); - - Ok(()) - } - fn assert_ctrl_shift_up_backspace_nls( - pre_lines: Vec, - expected_post_lines: Vec, - repeats: usize, - ) -> Result<(), String> { - assert_ctrl_shift_up_backspace(pre_lines, add_nls(expected_post_lines), repeats) - } - - fn assert_ctrl_shift_single_up_backspace( - pre_lines: Vec, - expected_post_lines: Vec, - ) -> Result<(), String> { - assert_ctrl_shift_up_backspace(pre_lines, expected_post_lines, 1) - } - - fn assert_ctrl_shift_single_up_backspace_nls( - pre_lines: Vec, - expected_post_lines: Vec, - ) -> Result<(), String> { - assert_ctrl_shift_single_up_backspace(pre_lines, add_nls(expected_post_lines)) - } - - #[test] - fn test_ctrl_shift_up_backspace_int() -> Result<(), String> { - // Blank is inserted when root is deleted - assert_ctrl_shift_single_up_backspace_nls(ovec!["val = 95┃21"], ovec!["val = ┃ "])?; - assert_ctrl_shift_single_up_backspace_nls(ovec!["val = 0┃"], ovec!["val = ┃ "])?; - assert_ctrl_shift_single_up_backspace_nls(ovec!["val = ┃10000"], ovec!["val = ┃ "])?; - - Ok(()) - } - - #[test] - fn test_ctrl_shift_up_backspace_string() -> Result<(), String> { - // Blank is inserted when root is deleted - assert_ctrl_shift_single_up_backspace_nls(ovec!["val = \"┃\""], ovec!["val = ┃ "])?; - assert_ctrl_shift_single_up_backspace_nls(ovec!["val = \"\"┃"], ovec!["val = ┃ "])?; - assert_ctrl_shift_single_up_backspace_nls(ovec!["val = ┃\"abc\""], ovec!["val = ┃ "])?; - assert_ctrl_shift_single_up_backspace_nls( - ovec!["val = \"hello┃, hello.0123456789ZXY{}[]-><-\""], - ovec!["val = ┃ "], - )?; - - Ok(()) - } - - #[test] - fn test_ctrl_shift_up_backspace_record() -> Result<(), String> { - // Blank is inserted when root of Expr2 is deleted - assert_ctrl_shift_single_up_backspace_nls(ovec!["val = {┃ }"], ovec!["val = ┃ "])?; - - // TODO: uncomment tests, once issue #1649 is fixed - //assert_ctrl_shift_single_up_backspace(ovec!["{ a┃ }"], ovec!["┃ "])?; - //assert_ctrl_shift_single_up_backspace(ovec!["{ a: { b }┃ }"], ovec!["┃ "])?; - assert_ctrl_shift_single_up_backspace_nls( - ovec!["val = { a: \"b cd\"┃ }"], - ovec!["val = ┃ "], - )?; - - //assert_ctrl_shift_single_up_backspace(ovec!["{ a: ┃{ b } }"], ovec!["{ a: ┃ }"])?; - assert_ctrl_shift_single_up_backspace_nls( - ovec!["val = { a: \"┃b cd\" }"], - ovec!["val = { a: ┃ }"], - )?; - assert_ctrl_shift_single_up_backspace_nls( - ovec!["val = { a: ┃12 }"], - ovec!["val = { a: ┃ }"], - )?; - /*assert_ctrl_shift_single_up_backspace( - ovec!["{ g: { oi: { ng: { d: { ┃e: { e: { p: { camelCase } } } } } } } }"], - ovec!["{ g: { oi: { ng: { d: ┃ } } } }"], - )?;*/ - - assert_ctrl_shift_up_backspace_nls( - ovec!["val = { a: { b: { c: \"abc┃ \" } } }"], - ovec!["val = { a: { b: ┃ } }"], - 2, - )?; - assert_ctrl_shift_up_backspace_nls( - ovec!["val = { a: { b: { c: 100┃000 } } }"], - ovec!["val = { a: { b: ┃ } }"], - 2, - )?; - assert_ctrl_shift_up_backspace_nls( - ovec!["val = { a: { b: { c: {┃ } } } }"], - ovec!["val = { a: { b: ┃ } }"], - 2, - )?; - /*assert_ctrl_shift_up_backspace( - ovec!["{ g: { oi: { ng: { d: { e: { e: { p┃: { camelCase } } } } } } } }"], - ovec!["{ g: ┃ }"], - 6, - )?;*/ - - Ok(()) - } -} diff --git a/editor/src/editor/mvc/ed_view.rs b/editor/src/editor/mvc/ed_view.rs deleted file mode 100644 index c4e0c434cf..0000000000 --- a/editor/src/editor/mvc/ed_view.rs +++ /dev/null @@ -1,197 +0,0 @@ -use super::ed_model::EdModel; -use crate::editor::config::Config; -use crate::editor::ed_error::EdResult; -use crate::editor::mvc::ed_model::SelectedBlock; -use crate::editor::render_ast::build_code_graphics; -use crate::editor::render_debug::build_debug_graphics; -use crate::editor::resources::strings::START_TIP; -use crate::graphics::primitives::rect::Rect; -use crate::graphics::primitives::text::{owned_section_from_text, Text}; -use crate::ui::text::caret_w_select::make_caret_rect; -use crate::ui::text::caret_w_select::make_selection_rect; -use crate::ui::text::caret_w_select::CaretWSelect; -use crate::ui::text::selection::Selection; -use crate::ui::tooltip::ToolTip; -use crate::ui::ui_error::MissingGlyphDims; -use cgmath::Vector2; -use roc_ast::mem_pool::pool::Pool; -use snafu::OptionExt; -use winit::dpi::PhysicalSize; - -#[derive(Debug)] -pub struct RenderedWgpu { - pub text_sections_behind: Vec, // displayed in front of rect_behind, behind everything else - pub text_sections_front: Vec, // displayed in front of everything - pub rects_behind: Vec, // displayed at lowest depth - pub rects_front: Vec, // displayed in front of text_sections_behind, behind text_sections_front -} - -impl RenderedWgpu { - pub fn new() -> Self { - Self { - text_sections_behind: Vec::new(), - text_sections_front: Vec::new(), - rects_behind: Vec::new(), - rects_front: Vec::new(), - } - } - - pub fn add_text_behind(&mut self, new_text_section: glyph_brush::OwnedSection) { - self.text_sections_behind.push(new_text_section); - } - - pub fn add_text_front(&mut self, new_text_section: glyph_brush::OwnedSection) { - self.text_sections_front.push(new_text_section); - } - - pub fn add_rect_behind(&mut self, new_rect: Rect) { - self.rects_behind.push(new_rect); - } - - pub fn add_rects_behind(&mut self, new_rects: Vec) { - self.rects_behind.extend(new_rects); - } - - pub fn add_rect_front(&mut self, new_rect: Rect) { - self.rects_front.push(new_rect); - } - - pub fn extend(&mut self, rendered_wgpu: RenderedWgpu) { - self.text_sections_behind - .extend(rendered_wgpu.text_sections_behind); - self.text_sections_front - .extend(rendered_wgpu.text_sections_front); - self.rects_behind.extend(rendered_wgpu.rects_behind); - self.rects_front.extend(rendered_wgpu.rects_front); - } -} - -// create text and rectangles based on EdModel's markup_root -pub fn model_to_wgpu<'a>( - ed_model: &'a mut EdModel, - size: &PhysicalSize, - txt_coords: Vector2, - config: &Config, -) -> EdResult { - let glyph_dim_rect = ed_model.glyph_dim_rect_opt.context(MissingGlyphDims {})?; - - let mut all_rendered = RenderedWgpu::new(); - - let tip_txt_coords = ( - txt_coords.x, - txt_coords.y - (START_TIP.matches('\n').count() as f32 + 1.0) * config.code_font_size, - ); - - let start_tip_text = owned_section_from_text(&Text { - position: tip_txt_coords.into(), - area_bounds: (size.width as f32, size.height as f32).into(), - color: config.ed_theme.subtle_text, - text: START_TIP, - size: config.code_font_size, - ..Default::default() - }); - - all_rendered.add_text_behind(start_tip_text); - - let rendered_code_graphics = build_code_graphics( - &ed_model.markup_ids, - size, - txt_coords, - config, - glyph_dim_rect, - &ed_model.mark_node_pool, - )?; - - all_rendered.extend(rendered_code_graphics); - - let caret_w_sel_vec = ed_model - .caret_w_select_vec - .iter() - .map(|(caret_w_sel, _)| *caret_w_sel) - .collect(); - - let rendered_selection = build_selection_graphics( - caret_w_sel_vec, - &ed_model.selected_block_opt, - txt_coords, - config, - glyph_dim_rect, - ed_model.module.env.pool, - )?; - - all_rendered.extend(rendered_selection); - - if ed_model.show_debug_view { - all_rendered.add_text_behind(build_debug_graphics(size, txt_coords, config, ed_model)?); - } - - Ok(all_rendered) -} - -pub fn build_selection_graphics( - caret_w_select_vec: Vec, - selected_expr_opt: &Option, - txt_coords: Vector2, - config: &Config, - glyph_dim_rect: Rect, - pool: &Pool, -) -> EdResult { - let mut all_rendered = RenderedWgpu::new(); - let char_width = glyph_dim_rect.width; - let char_height = glyph_dim_rect.height; - - let y_offset = 0.1 * char_height; - - for caret_w_sel in caret_w_select_vec { - let caret_row = caret_w_sel.caret_pos.line as f32; - let caret_col = caret_w_sel.caret_pos.column as f32; - - let top_left_x = txt_coords.x + caret_col * char_width; - let top_left_y = txt_coords.y + caret_row * char_height + y_offset; - - if let Some(selection) = caret_w_sel.selection_opt { - let Selection { start_pos, end_pos } = selection; - - let sel_rect_x = txt_coords.x + ((start_pos.column as f32) * char_width); - let sel_rect_y = txt_coords.y + char_height * (start_pos.line as f32) + y_offset; - - let width = - ((end_pos.column as f32) * char_width) - ((start_pos.column as f32) * char_width); - - all_rendered.add_rect_behind(make_selection_rect( - sel_rect_x, - sel_rect_y, - width, - &glyph_dim_rect, - &config.ed_theme.ui_theme, - )); - - // render tooltip showing type - if let Some(selected_expr) = selected_expr_opt { - let tooltip = ToolTip { - position_x: sel_rect_x, - position_y: sel_rect_y - glyph_dim_rect.height, - text: selected_expr.type_str.as_str(pool), - }; - - let (tip_rect, tip_text) = tooltip.render_tooltip( - &glyph_dim_rect, - &config.ed_theme.ui_theme, - config.code_font_size, - ); - - all_rendered.add_rect_front(tip_rect); - all_rendered.add_text_front(tip_text); - } - } - - all_rendered.add_rect_front(make_caret_rect( - top_left_x, - top_left_y, - &glyph_dim_rect, - &config.ed_theme.ui_theme, - )); - } - - Ok(all_rendered) -} diff --git a/editor/src/editor/mvc/int_update.rs b/editor/src/editor/mvc/int_update.rs deleted file mode 100644 index a30c9be1c0..0000000000 --- a/editor/src/editor/mvc/int_update.rs +++ /dev/null @@ -1,133 +0,0 @@ -use roc_ast::lang::core::expr::expr2::Expr2::SmallInt; -use roc_ast::lang::core::expr::expr2::IntStyle; -use roc_ast::lang::core::expr::expr2::IntVal; -use roc_ast::mem_pool::pool_str::PoolStr; -use roc_code_markup::slow_pool::MarkNodeId; - -use crate::editor::ed_error::EdResult; -use crate::editor::ed_error::StringParseError; -use crate::editor::mvc::app_update::InputOutcome; -use crate::editor::mvc::ed_model::EdModel; -use crate::editor::mvc::ed_update::get_node_context; -use crate::editor::mvc::ed_update::NodeContext; -use crate::ui::text::lines::SelectableLines; - -// digit_char should be verified to be a digit before calling this function -pub fn start_new_int(ed_model: &mut EdModel, digit_char: &char) -> EdResult { - let NodeContext { - old_caret_pos: _, - curr_mark_node_id: _, - curr_mark_node, - parent_id_opt: _, - ast_node_id, - } = get_node_context(ed_model)?; - - let is_blank_node = curr_mark_node.is_blank(); - - let int_var = ed_model.module.env.var_store.fresh(); - - let digit_string = digit_char.to_string(); - - let expr2_node = SmallInt { - number: IntVal::U64(*digit_char as u64), // TODO determine if u64 on wordlength of current arch, perhaps introduce Unknown(i64) - var: int_var, - style: IntStyle::Decimal, - text: PoolStr::new(&digit_string, ed_model.module.env.pool), - }; - - ed_model - .module - .env - .pool - .set(ast_node_id.to_expr_id()?, expr2_node); - - if is_blank_node { - let char_len = 1; - ed_model.simple_move_carets_right(char_len); - - Ok(InputOutcome::Accepted) - } else { - Ok(InputOutcome::Ignored) - } -} - -// TODO check if new int needs more than e.g. 64 bits -pub fn update_int( - ed_model: &mut EdModel, - int_mark_node_id: MarkNodeId, - ch: &char, -) -> EdResult { - if ch.is_ascii_digit() { - let old_caret_pos = ed_model.get_caret(); - - let node_caret_offset = ed_model - .grid_node_map - .get_offset_to_node_id(old_caret_pos, int_mark_node_id)?; - - let int_mark_node = ed_model.mark_node_pool.get_mut(int_mark_node_id); - let int_ast_node_id = ed_model.mark_id_ast_id_map.get(int_mark_node_id)?; - - let content_str_mut = int_mark_node.get_content_mut()?; - - // 00, 01 are not valid ints - if (content_str_mut == "0" && (node_caret_offset == 1 || *ch == '0')) - || (*ch == '0' && node_caret_offset == 0) - { - Ok(InputOutcome::Ignored) - } else { - content_str_mut.insert(node_caret_offset, *ch); - - let content_str = int_mark_node.get_content(); - - // update ast - let new_pool_str = PoolStr::new(&content_str, ed_model.module.env.pool); - let int_ast_node = ed_model - .module - .env - .pool - .get_mut(int_ast_node_id.to_expr_id()?); - match int_ast_node { - SmallInt { number, text, .. } => { - update_small_int_num(number, &content_str)?; - - *text = new_pool_str; - } - _ => unimplemented!("TODO implement updating this type of Number"), - } - - // update caret - ed_model.simple_move_carets_right(1); - - Ok(InputOutcome::Accepted) - } - } else { - Ok(InputOutcome::Ignored) - } -} - -fn update_small_int_num(number: &mut IntVal, updated_str: &str) -> EdResult<()> { - use IntVal::*; - - *number = match number { - I64(_) => I64(check_parse_res(updated_str.parse::())?), - U64(_) => U64(check_parse_res(updated_str.parse::())?), - I32(_) => I32(check_parse_res(updated_str.parse::())?), - U32(_) => U32(check_parse_res(updated_str.parse::())?), - I16(_) => I16(check_parse_res(updated_str.parse::())?), - U16(_) => U16(check_parse_res(updated_str.parse::())?), - I8(_) => I8(check_parse_res(updated_str.parse::())?), - U8(_) => U8(check_parse_res(updated_str.parse::())?), - }; - - Ok(()) -} - -fn check_parse_res(parse_res: Result) -> EdResult { - match parse_res { - Ok(some_type) => Ok(some_type), - Err(parse_err) => StringParseError { - msg: format!("{:?}", parse_err), - } - .fail(), - } -} diff --git a/editor/src/editor/mvc/let_update.rs b/editor/src/editor/mvc/let_update.rs deleted file mode 100644 index 482b5d4c9a..0000000000 --- a/editor/src/editor/mvc/let_update.rs +++ /dev/null @@ -1,135 +0,0 @@ -use roc_ast::lang::core::expr::expr2::Expr2; -use roc_ast::lang::core::pattern::Pattern2; -use roc_ast::lang::core::val_def::ValueDef; -use roc_module::symbol::Symbol; - -use crate::editor::ed_error::EdResult; -use crate::editor::mvc::app_update::InputOutcome; -use crate::editor::mvc::ed_model::EdModel; -use crate::editor::mvc::ed_update::get_node_context; -use crate::editor::mvc::ed_update::NodeContext; - -pub fn start_new_let_value(ed_model: &mut EdModel, new_char: &char) -> EdResult { - let NodeContext { - old_caret_pos: _, - curr_mark_node_id: _, - curr_mark_node, - parent_id_opt: _, - ast_node_id, - } = get_node_context(ed_model)?; - - let is_blank_node = curr_mark_node.is_blank(); - - let val_name_string = new_char.to_string(); - // safe unwrap because our ArrString has a 30B capacity - let val_expr2_node = Expr2::Blank; - let val_expr_id = ed_model.module.env.pool.add(val_expr2_node); - - let ident_id = ed_model.module.env.ident_ids.add_str(&val_name_string); - let var_symbol = Symbol::new(ed_model.module.env.home, ident_id); - let body = Expr2::Var(var_symbol); - let body_id = ed_model.module.env.pool.add(body); - - let pattern = Pattern2::Identifier(var_symbol); - let pattern_id = ed_model.module.env.pool.add(pattern); - - let value_def = ValueDef::NoAnnotation { - pattern_id, - expr_id: val_expr_id, - expr_var: ed_model.module.env.var_store.fresh(), - }; - let def_id = ed_model.module.env.pool.add(value_def); - - let expr2_node = Expr2::LetValue { - def_id, - body_id, - body_var: ed_model.module.env.var_store.fresh(), - }; - - ed_model - .module - .env - .pool - .set(ast_node_id.to_expr_id()?, expr2_node); - - if is_blank_node { - let char_len = 1; - ed_model.simple_move_carets_right(char_len); - - Ok(InputOutcome::Accepted) - } else { - Ok(InputOutcome::Ignored) - } -} - -// TODO reenable this for updating non-top level value defs -/* -pub fn update_let_value( - val_name_mn_id: MarkNodeId, - def_id: NodeId, - body_id: NodeId, - ed_model: &mut EdModel, - new_char: &char, -) -> EdResult { - if new_char.is_ascii_alphanumeric() { - let old_caret_pos = ed_model.get_caret(); - - // update markup - let val_name_mn_mut = ed_model.mark_node_pool.get_mut(val_name_mn_id); - let content_str_mut = val_name_mn_mut.get_content_mut()?; - - let old_val_name = content_str_mut.clone(); - - let node_caret_offset = ed_model - .grid_node_map - .get_offset_to_node_id(old_caret_pos, val_name_mn_id)?; - - if node_caret_offset <= content_str_mut.len() { - content_str_mut.insert(node_caret_offset, *new_char); - - // update ast - let value_def = ed_model.module.env.pool.get(def_id); - let value_ident_pattern_id = value_def.get_pattern_id(); - - // TODO no unwrap - let ident_id = ed_model - .module - .env - .ident_ids - .update_key(&old_val_name, content_str_mut) - .unwrap(); - - let new_var_symbol = Symbol::new(ed_model.module.env.home, ident_id); - - ed_model - .module - .env - .pool - .set(value_ident_pattern_id, Pattern2::Identifier(new_var_symbol)); - - ed_model - .module - .env - .pool - .set(body_id, Expr2::Var(new_var_symbol)); - - // update GridNodeMap and CodeLines - ed_model.insert_between_line( - old_caret_pos.line, - old_caret_pos.column, - &new_char.to_string(), - val_name_mn_id, - )?; - - // update caret - ed_model.simple_move_carets_right(1); - - Ok(InputOutcome::Accepted) - } else { - Ok(InputOutcome::Ignored) - } - } else { - Ok(InputOutcome::Ignored) - } -} -*/ diff --git a/editor/src/editor/mvc/list_update.rs b/editor/src/editor/mvc/list_update.rs deleted file mode 100644 index 2df46bc55e..0000000000 --- a/editor/src/editor/mvc/list_update.rs +++ /dev/null @@ -1,123 +0,0 @@ -use roc_ast::lang::core::ast::ast_node_to_string; -use roc_ast::lang::core::expr::expr2::{Expr2, ExprId}; -use roc_ast::mem_pool::pool_vec::PoolVec; -use roc_code_markup::markup::nodes::{self}; -use roc_code_markup::slow_pool::MarkNodeId; - -use crate::editor::ed_error::EdResult; -use crate::editor::ed_error::{MissingParent, UnexpectedASTNode}; -use crate::editor::mvc::app_update::InputOutcome; -use crate::editor::mvc::ed_model::EdModel; -use crate::editor::mvc::ed_update::get_node_context; -use crate::editor::mvc::ed_update::NodeContext; - -pub fn start_new_list(ed_model: &mut EdModel) -> EdResult { - let NodeContext { - old_caret_pos: _, - curr_mark_node_id: _, - curr_mark_node, - parent_id_opt: _, - ast_node_id, - } = get_node_context(ed_model)?; - - let is_blank_node = curr_mark_node.is_blank(); - - let expr2_node = Expr2::List { - elem_var: ed_model.module.env.var_store.fresh(), - elems: PoolVec::empty(ed_model.module.env.pool), - }; - - ed_model - .module - .env - .pool - .set(ast_node_id.to_expr_id()?, expr2_node); - - if is_blank_node { - ed_model.simple_move_carets_right(nodes::LEFT_SQUARE_BR.len()); - - Ok(InputOutcome::Accepted) - } else { - Ok(InputOutcome::Ignored) - } -} - -// insert Blank at current position for easy code reuse -pub fn add_blank_child( - new_child_index: usize, - new_ast_child_index: usize, - ed_model: &mut EdModel, -) -> EdResult { - let NodeContext { - old_caret_pos: _, - curr_mark_node_id, - curr_mark_node: _, - parent_id_opt, - ast_node_id, - } = get_node_context(ed_model)?; - - let trip_result: EdResult<(ExprId, ExprId, MarkNodeId)> = if let Some(parent_id) = parent_id_opt - { - let list_ast_node_id = ed_model.mark_id_ast_id_map.get(parent_id)?; - let list_ast_node = ed_model.module.env.pool.get(list_ast_node_id.to_expr_id()?); - - match list_ast_node { - Expr2::List { - elem_var: _, - elems: _, - } => { - let blank_elt = Expr2::Blank; - let blank_elt_id = ed_model.module.env.pool.add(blank_elt); - - Ok((blank_elt_id, list_ast_node_id.to_expr_id()?, parent_id)) - } - _ => UnexpectedASTNode { - required_node_type: "List".to_string(), - encountered_node_type: ast_node_to_string(ast_node_id, ed_model.module.env.pool), - } - .fail(), - } - } else { - MissingParent { - node_id: curr_mark_node_id, - } - .fail() - }; - - let (blank_elt_id, list_ast_node_id, _parent_id) = trip_result?; - - let list_ast_node = ed_model.module.env.pool.get(list_ast_node_id); - - match list_ast_node { - Expr2::List { elem_var, elems } => { - let mut new_elems: Vec = - elems.iter(ed_model.module.env.pool).copied().collect(); - - new_elems.insert(new_ast_child_index, blank_elt_id); - - let new_list_node = Expr2::List { - elem_var: *elem_var, - elems: PoolVec::new(new_elems.into_iter(), ed_model.module.env.pool), - }; - - ed_model - .module - .env - .pool - .set(list_ast_node_id, new_list_node); - - Ok(()) - } - _ => UnexpectedASTNode { - required_node_type: "List".to_string(), - encountered_node_type: ast_node_to_string(ast_node_id, ed_model.module.env.pool), - } - .fail(), - }?; - - if new_child_index > 1 { - ed_model.simple_move_carets_right(nodes::COMMA.len()); - } - - Ok(InputOutcome::Accepted) -} diff --git a/editor/src/editor/mvc/lookup_update.rs b/editor/src/editor/mvc/lookup_update.rs deleted file mode 100644 index cf08166243..0000000000 --- a/editor/src/editor/mvc/lookup_update.rs +++ /dev/null @@ -1,44 +0,0 @@ -use roc_ast::lang::core::expr::expr2::{Expr2, ExprId}; -use roc_ast::mem_pool::pool_str::PoolStr; -use roc_code_markup::slow_pool::MarkNodeId; - -use crate::editor::ed_error::EdResult; -use crate::editor::mvc::app_update::InputOutcome; -use crate::editor::mvc::ed_model::EdModel; -use crate::ui::text::lines::SelectableLines; - -pub fn update_invalid_lookup( - input_str: &str, - old_pool_str: &PoolStr, - curr_mark_node_id: MarkNodeId, - expr_id: ExprId, - ed_model: &mut EdModel, -) -> EdResult { - if input_str.chars().all(|ch| ch.is_ascii_alphanumeric()) { - let mut new_lookup_str = String::new(); - - new_lookup_str.push_str(old_pool_str.as_str(ed_model.module.env.pool)); - - let caret_offset = ed_model - .grid_node_map - .get_offset_to_node_id(ed_model.get_caret(), curr_mark_node_id)?; - - new_lookup_str.insert_str(caret_offset, input_str); - - let new_pool_str = PoolStr::new(&new_lookup_str, ed_model.module.env.pool); - - // update AST - ed_model - .module - .env - .pool - .set(expr_id, Expr2::InvalidLookup(new_pool_str)); - - // update caret - ed_model.simple_move_carets_right(input_str.len()); - - Ok(InputOutcome::Accepted) - } else { - Ok(InputOutcome::Ignored) - } -} diff --git a/editor/src/editor/mvc/mod.rs b/editor/src/editor/mvc/mod.rs deleted file mode 100644 index 5b4a11ad9a..0000000000 --- a/editor/src/editor/mvc/mod.rs +++ /dev/null @@ -1,13 +0,0 @@ -pub mod app_model; -pub mod app_update; -mod break_line; -pub mod ed_model; -pub mod ed_update; -pub mod ed_view; -mod int_update; -mod let_update; -mod list_update; -mod lookup_update; -mod record_update; -mod string_update; -pub mod tld_value_update; diff --git a/editor/src/editor/mvc/record_update.rs b/editor/src/editor/mvc/record_update.rs deleted file mode 100644 index ce172acbb3..0000000000 --- a/editor/src/editor/mvc/record_update.rs +++ /dev/null @@ -1,295 +0,0 @@ -use crate::editor::ed_error::EdResult; -use crate::editor::ed_error::MissingParent; -use crate::editor::ed_error::RecordWithoutFields; -use crate::editor::mvc::app_update::InputOutcome; -use crate::editor::mvc::ed_model::EdModel; -use crate::editor::mvc::ed_update::get_node_context; -use crate::editor::mvc::ed_update::NodeContext; -use crate::editor::util::index_of; -use crate::ui::text::text_pos::TextPos; -use roc_ast::lang::core::ast::ASTNodeId; -use roc_ast::lang::core::expr::expr2::Expr2; -use roc_ast::lang::core::expr::expr2::ExprId; -use roc_ast::lang::core::expr::record_field::RecordField; -use roc_ast::mem_pool::pool_str::PoolStr; -use roc_ast::mem_pool::pool_vec::PoolVec; -use roc_code_markup::markup::attribute::Attributes; -use roc_code_markup::markup::nodes; -use roc_code_markup::markup::nodes::MarkupNode; -use roc_code_markup::markup::nodes::COLON; -use roc_code_markup::slow_pool::MarkNodeId; -use roc_code_markup::syntax_highlight::HighlightStyle; -use snafu::OptionExt; - -pub fn start_new_record(ed_model: &mut EdModel) -> EdResult { - let NodeContext { - old_caret_pos: _, - curr_mark_node_id: _, - curr_mark_node, - parent_id_opt: _, - ast_node_id, - } = get_node_context(ed_model)?; - - let is_blank_node = curr_mark_node.is_blank(); - - let ast_pool = &mut ed_model.module.env.pool; - let expr2_node = Expr2::EmptyRecord; - - ast_pool.set(ast_node_id.to_expr_id()?, expr2_node); - - if is_blank_node { - ed_model.simple_move_carets_right(nodes::LEFT_ACCOLADE.len()); - - Ok(InputOutcome::Accepted) - } else { - Ok(InputOutcome::Ignored) - } -} - -pub fn update_empty_record( - new_input: &str, - prev_mark_node_id: MarkNodeId, - sibling_ids: Vec, - ed_model: &mut EdModel, -) -> EdResult { - let mut input_chars = new_input.chars(); - - if input_chars.all(|ch| ch.is_ascii_alphabetic()) - && input_chars.all(|ch| ch.is_ascii_lowercase()) - { - let prev_mark_node = ed_model.mark_node_pool.get(prev_mark_node_id); - - let NodeContext { - old_caret_pos, - curr_mark_node_id, - curr_mark_node, - parent_id_opt, - ast_node_id, - } = get_node_context(ed_model)?; - - if prev_mark_node.get_content() == nodes::LEFT_ACCOLADE - && curr_mark_node.get_content() == nodes::RIGHT_ACCOLADE - { - // update AST - let record_var = ed_model.module.env.var_store.fresh(); - let field_name = PoolStr::new(new_input, ed_model.module.env.pool); - let field_var = ed_model.module.env.var_store.fresh(); - //TODO actually check if field_str belongs to a previously defined variable - let first_field = RecordField::InvalidLabelOnly(field_name, field_var); - - let fields = PoolVec::new(vec![first_field].into_iter(), ed_model.module.env.pool); - - let new_ast_node = Expr2::Record { record_var, fields }; - - ed_model - .module - .env - .pool - .set(ast_node_id.to_expr_id()?, new_ast_node); - - // update Markup - - let record_field_node = MarkupNode::Text { - content: new_input.to_owned(), - syn_high_style: HighlightStyle::RecordField, - attributes: Attributes::default(), - parent_id_opt, - newlines_at_end: 0, - }; - - let record_field_node_id = ed_model.add_mark_node(record_field_node); - - if let Some(parent_id) = parent_id_opt { - let parent = ed_model.mark_node_pool.get_mut(parent_id); - - let new_child_index = index_of(curr_mark_node_id, &sibling_ids)?; - - parent.add_child_at_index(new_child_index, record_field_node_id)?; - } else { - MissingParent { - node_id: curr_mark_node_id, - } - .fail()? - } - - // update caret - ed_model.simple_move_carets_right(1); - - // update GridNodeMap and CodeLines - EdModel::insert_between_line( - old_caret_pos.line, - old_caret_pos.column, - new_input, - record_field_node_id, - &mut ed_model.grid_node_map, - )?; - - Ok(InputOutcome::Accepted) - } else { - Ok(InputOutcome::Ignored) - } - } else { - Ok(InputOutcome::Ignored) - } -} - -pub fn update_record_colon( - ed_model: &mut EdModel, - record_ast_node_id: ExprId, -) -> EdResult { - let NodeContext { - old_caret_pos, - curr_mark_node_id: _, - curr_mark_node: _, - parent_id_opt: _, - ast_node_id, - } = get_node_context(ed_model)?; - let curr_ast_node = ed_model.module.env.pool.get(ast_node_id.to_expr_id()?); - - let prev_mark_node_id_opt = ed_model.get_prev_mark_node_id()?; - if let Some(prev_mark_node_id) = prev_mark_node_id_opt { - let prev_mn_ast_node_id = ed_model.mark_id_ast_id_map.get(prev_mark_node_id)?; - - match prev_mn_ast_node_id { - ASTNodeId::ADefId(_) => Ok(InputOutcome::Ignored), - ASTNodeId::AExprId(prev_expr_id) => { - let prev_expr = ed_model.module.env.pool.get(prev_expr_id); - - // current and prev node should always point to record when in valid position to add ':' - if matches!(prev_expr, Expr2::Record { .. }) - && matches!(curr_ast_node, Expr2::Record { .. }) - { - let ast_node_ref = ed_model.module.env.pool.get(record_ast_node_id); - - match ast_node_ref { - Expr2::Record { - record_var: _, - fields, - } => { - if ed_model.node_exists_at_caret() { - let next_mark_node_id = - ed_model.grid_node_map.get_id_at_row_col(old_caret_pos)?; - let next_mark_node = ed_model.mark_node_pool.get(next_mark_node_id); - if next_mark_node.get_content() == nodes::RIGHT_ACCOLADE { - // update AST node - let new_field_val = Expr2::Blank; - let new_field_val_id = - ed_model.module.env.pool.add(new_field_val); - - let first_field_mut = fields - .iter_mut(ed_model.module.env.pool) - .next() - .with_context(|| RecordWithoutFields {})?; - - *first_field_mut = RecordField::LabeledValue( - *first_field_mut.get_record_field_pool_str(), - *first_field_mut.get_record_field_var(), - new_field_val_id, - ); - - // update caret - ed_model.simple_move_carets_right(COLON.len()); - - Ok(InputOutcome::Accepted) - } else { - Ok(InputOutcome::Ignored) - } - } else { - Ok(InputOutcome::Ignored) - } - } - _ => Ok(InputOutcome::Ignored), - } - } else { - Ok(InputOutcome::Ignored) - } - } - } - } else { - Ok(InputOutcome::Ignored) - } -} - -pub fn update_record_field( - new_input: &str, - old_caret_pos: TextPos, - curr_mark_node_id: MarkNodeId, - record_fields: &PoolVec, - ed_model: &mut EdModel, -) -> EdResult { - // update MarkupNode - let curr_mark_node_mut = ed_model.mark_node_pool.get_mut(curr_mark_node_id); - let content_str_mut = curr_mark_node_mut.get_content_mut()?; - let node_caret_offset = ed_model - .grid_node_map - .get_offset_to_node_id(old_caret_pos, curr_mark_node_id)?; - - if node_caret_offset == 0 { - let first_char_opt = new_input.chars().next(); - let first_char_is_num = first_char_opt.unwrap_or('0').is_ascii_digit(); - - // variable name can't start with number - if first_char_is_num { - return Ok(InputOutcome::Ignored); - } - } - - content_str_mut.insert_str(node_caret_offset, new_input); - - // update caret - ed_model.simple_move_carets_right(new_input.len()); - - // update AST Node - let first_field = record_fields - .iter(ed_model.module.env.pool) - .next() - .with_context(|| RecordWithoutFields {})?; - - let field_pool_str = first_field - .get_record_field_pool_str() - .as_str(ed_model.module.env.pool); - - let mut new_field_name = String::new(); - - // -push old field name - new_field_name.push_str(field_pool_str); - new_field_name.insert_str(node_caret_offset, new_input); - let new_field_pool_str = PoolStr::new(&new_field_name, ed_model.module.env.pool); - - let first_field_mut = record_fields - .iter_mut(ed_model.module.env.pool) - .next() - .with_context(|| RecordWithoutFields {})?; - - let field_pool_str_mut = first_field_mut.get_record_field_pool_str_mut(); - *field_pool_str_mut = new_field_pool_str; - - // because borrow issues - let first_field_b = record_fields - .iter(ed_model.module.env.pool) - .next() - .with_context(|| RecordWithoutFields {})?; - - match first_field_b { - RecordField::InvalidLabelOnly(_, _) => { - // TODO check if label is now valid. If it is, return LabelOnly - } - RecordField::LabelOnly(_, _, _symbol) => { - // TODO check if symbol is still valid. If not, return InvalidLabelOnly - } - RecordField::LabeledValue(_, _, field_val_id_ref) => { - let field_val_id = *field_val_id_ref; - let sub_expr2 = ed_model.module.env.pool.get(field_val_id); - - if let Expr2::InvalidLookup(_) = sub_expr2 { - ed_model - .module - .env - .pool - .set(field_val_id, Expr2::InvalidLookup(new_field_pool_str)); - } - } - } - - Ok(InputOutcome::Accepted) -} diff --git a/editor/src/editor/mvc/string_update.rs b/editor/src/editor/mvc/string_update.rs deleted file mode 100644 index dc0152aa40..0000000000 --- a/editor/src/editor/mvc/string_update.rs +++ /dev/null @@ -1,120 +0,0 @@ -use roc_ast::lang::core::expr::expr2::ArrString; -use roc_ast::lang::core::expr::expr2::Expr2; -use roc_ast::lang::core::str::update_str_expr; -use roc_ast::mem_pool::pool_str::PoolStr; - -use crate::editor::ed_error::EdResult; -use crate::editor::mvc::app_update::InputOutcome; -use crate::editor::mvc::ed_model::EdModel; -use crate::editor::mvc::ed_update::get_node_context; -use crate::editor::mvc::ed_update::NodeContext; - -pub fn update_small_string( - new_char: &char, - old_array_str: &ArrString, - ed_model: &mut EdModel, -) -> EdResult { - let NodeContext { - old_caret_pos, - curr_mark_node_id, - curr_mark_node, - parent_id_opt: _, - ast_node_id, - } = get_node_context(ed_model)?; - - let new_input = &new_char.to_string(); - - let content_str = curr_mark_node.get_content(); - let node_caret_offset = ed_model - .grid_node_map - .get_offset_to_node_id(old_caret_pos, curr_mark_node_id)?; - - if node_caret_offset != 0 && node_caret_offset < content_str.len() { - if old_array_str.len() < old_array_str.capacity() { - if let Expr2::SmallStr(ref mut mut_array_str) = - ed_model.module.env.pool.get_mut(ast_node_id.to_expr_id()?) - { - // safe because we checked the length - mut_array_str.push(*new_char); - } else { - unreachable!() - } - } else { - let mut new_str = old_array_str.as_str().to_owned(); - new_str.push(*new_char); - - let new_ast_node = Expr2::Str(PoolStr::new(&new_str, ed_model.module.env.pool)); - - ed_model - .module - .env - .pool - .set(ast_node_id.to_expr_id()?, new_ast_node); - } - - // update caret - ed_model.simple_move_carets_right(new_input.len()); - - Ok(InputOutcome::Accepted) - } else { - Ok(InputOutcome::Ignored) - } -} - -pub fn update_string(new_char: char, ed_model: &mut EdModel) -> EdResult { - let NodeContext { - old_caret_pos, - curr_mark_node_id, - curr_mark_node, - parent_id_opt: _, - ast_node_id, - } = get_node_context(ed_model)?; - - let content_str = curr_mark_node.get_content(); - let node_caret_offset = ed_model - .grid_node_map - .get_offset_to_node_id(old_caret_pos, curr_mark_node_id)?; - - if node_caret_offset != 0 && node_caret_offset < content_str.len() { - // update ast - update_str_expr( - ast_node_id.to_expr_id()?, - new_char, - node_caret_offset - 1, // -1 because offset was calculated with quotes - ed_model.module.env.pool, - )?; - - // update caret - ed_model.simple_move_carets_right(1); - - Ok(InputOutcome::Accepted) - } else { - Ok(InputOutcome::Ignored) - } -} - -pub fn start_new_string(ed_model: &mut EdModel) -> EdResult { - let NodeContext { - old_caret_pos: _, - curr_mark_node_id: _, - curr_mark_node, - parent_id_opt: _, - ast_node_id, - } = get_node_context(ed_model)?; - - if curr_mark_node.is_blank() { - let new_expr2_node = Expr2::SmallStr(arrayvec::ArrayString::new()); - - ed_model - .module - .env - .pool - .set(ast_node_id.to_expr_id()?, new_expr2_node); - - ed_model.simple_move_carets_right(1); - - Ok(InputOutcome::Accepted) - } else { - Ok(InputOutcome::Ignored) - } -} diff --git a/editor/src/editor/mvc/tld_value_update.rs b/editor/src/editor/mvc/tld_value_update.rs deleted file mode 100644 index 72323b25c2..0000000000 --- a/editor/src/editor/mvc/tld_value_update.rs +++ /dev/null @@ -1,104 +0,0 @@ -use roc_ast::lang::core::{def::def2::Def2, expr::expr2::Expr2}; -use roc_code_markup::slow_pool::MarkNodeId; - -use crate::{ - editor::ed_error::{EdResult, FailedToUpdateIdentIdName, KeyNotFound}, - ui::text::text_pos::TextPos, -}; - -use super::{ - app_update::InputOutcome, - ed_model::EdModel, - ed_update::{get_node_context, NodeContext}, -}; - -// Top Level Defined Value. example: `main = "Hello, World!"` -pub fn start_new_tld_value(ed_model: &mut EdModel, new_char: &char) -> EdResult { - let NodeContext { - old_caret_pos: _, - curr_mark_node_id: _, - curr_mark_node: _, - parent_id_opt: _, - ast_node_id, - } = get_node_context(ed_model)?; - - // create new blank >> m = Blank - let val_expr_node = Expr2::Blank; - let val_expr_id = ed_model.module.env.pool.add(val_expr_node); - - let ident_str = new_char.to_string(); - let ident_id = ed_model.module.env.ident_ids.add_str(&ident_str); - - let module_ident_ids_opt = ed_model - .loaded_module - .interns - .all_ident_ids - .get_mut(&ed_model.module.env.home); - - if let Some(module_ident_ids_ref) = module_ident_ids_opt { - // this might create different IdentId for interns and env.ident_ids which may be a problem - module_ident_ids_ref.add_str(&ident_str); - } else { - KeyNotFound { - key_str: format!("{:?}", ed_model.module.env.home), - } - .fail()? - } - - let new_ast_node = Def2::ValueDef { - identifier_id: ident_id, - expr_id: val_expr_id, - }; - - ed_model - .module - .env - .pool - .set(ast_node_id.to_def_id()?, new_ast_node); - - let char_len = 1; - ed_model.simple_move_carets_right(char_len); - - Ok(InputOutcome::Accepted) -} - -pub fn update_tld_val_name( - val_name_mn_id: MarkNodeId, - old_caret_pos: TextPos, - ed_model: &mut EdModel, - new_char: &char, -) -> EdResult { - if new_char.is_ascii_alphanumeric() { - // update markup - let val_name_mn = ed_model.mark_node_pool.get(val_name_mn_id); - let mut val_name_str = val_name_mn.get_content(); - - let old_val_name = val_name_str.clone(); - - let node_caret_offset = ed_model - .grid_node_map - .get_offset_to_node_id(old_caret_pos, val_name_mn_id)?; - - if node_caret_offset <= val_name_str.len() { - val_name_str.insert(node_caret_offset, *new_char); - - let update_val_name_res = ed_model - .module - .env - .ident_ids - .update_key(&old_val_name, &val_name_str); - - if let Err(err_str) = update_val_name_res { - FailedToUpdateIdentIdName { err_str }.fail()?; - } - - ed_model.simple_move_caret_right(old_caret_pos, 1); - - Ok(InputOutcome::Accepted) - } else { - Ok(InputOutcome::Ignored) - } - } else { - Ok(InputOutcome::Ignored) - } -} diff --git a/editor/src/editor/render_ast.rs b/editor/src/editor/render_ast.rs deleted file mode 100644 index ac0bcbc764..0000000000 --- a/editor/src/editor/render_ast.rs +++ /dev/null @@ -1,237 +0,0 @@ -use crate::editor::mvc::ed_view::RenderedWgpu; -use crate::editor::{ed_error::EdResult, theme::EdTheme, util::map_get}; -use crate::graphics::primitives::rect::Rect; -use crate::graphics::primitives::text as gr_text; -use cgmath::Vector2; -use roc_code_markup::{ - markup::{ - attribute::Attribute, - nodes::{MarkupNode, BLANK_PLACEHOLDER}, - }, - slow_pool::{MarkNodeId, SlowPool}, - syntax_highlight::HighlightStyle, - underline_style::UnderlineStyle, -}; -use winit::dpi::PhysicalSize; - -use crate::{editor::config::Config, graphics::colors}; - -pub fn build_code_graphics<'a>( - markup_ids: &[MarkNodeId], - size: &PhysicalSize, - txt_coords: Vector2, - config: &Config, - glyph_dim_rect: Rect, - mark_node_pool: &'a SlowPool, -) -> EdResult { - let area_bounds = (size.width as f32, size.height as f32); - let layout = wgpu_glyph::Layout::default().h_align(wgpu_glyph::HorizontalAlign::Left); - let mut rendered_wgpu = RenderedWgpu::new(); - - let mut all_glyph_text_vec = vec![]; - let mut all_rects = vec![]; - let mut txt_row_col = (0, 0); - - for markup_id in markup_ids.iter() { - let mark_node = mark_node_pool.get(*markup_id); - - let (mut glyph_text_vec, mut rects) = markup_to_wgpu( - mark_node, - &CodeStyle { - ed_theme: &config.ed_theme, - font_size: config.code_font_size, - txt_coords, - glyph_dim_rect, - }, - &mut txt_row_col, - mark_node_pool, - )?; - - all_glyph_text_vec.append(&mut glyph_text_vec); - all_rects.append(&mut rects) - } - - let section = gr_text::owned_section_from_glyph_texts( - all_glyph_text_vec, - txt_coords.into(), - area_bounds, - layout, - ); - - rendered_wgpu.add_rects_behind(all_rects); // currently only rects for Blank - rendered_wgpu.add_text_behind(section); - - Ok(rendered_wgpu) -} - -struct CodeStyle<'a> { - ed_theme: &'a EdTheme, - font_size: f32, - txt_coords: Vector2, - glyph_dim_rect: Rect, -} - -fn markup_to_wgpu<'a>( - markup_node: &'a MarkupNode, - code_style: &CodeStyle, - txt_row_col: &mut (usize, usize), - mark_node_pool: &'a SlowPool, -) -> EdResult<(Vec, Vec)> { - let mut wgpu_texts: Vec = Vec::new(); - let mut rects: Vec = Vec::new(); - - markup_to_wgpu_helper( - markup_node, - &mut wgpu_texts, - &mut rects, - code_style, - txt_row_col, - mark_node_pool, - )?; - - Ok((wgpu_texts, rects)) -} - -fn markup_to_wgpu_helper<'a>( - markup_node: &'a MarkupNode, - wgpu_texts: &mut Vec, - rects: &mut Vec, - code_style: &CodeStyle, - txt_row_col: &mut (usize, usize), - mark_node_pool: &'a SlowPool, -) -> EdResult<()> { - let char_width = code_style.glyph_dim_rect.width; - let char_height = code_style.glyph_dim_rect.height; - - match markup_node { - MarkupNode::Nested { - children_ids, - parent_id_opt: _, - newlines_at_end, - } => { - for child_id in children_ids.iter() { - let child = mark_node_pool.get(*child_id); - markup_to_wgpu_helper( - child, - wgpu_texts, - rects, - code_style, - txt_row_col, - mark_node_pool, - )?; - } - - for _ in 0..*newlines_at_end { - wgpu_texts.push(newline(code_style.font_size)); - - txt_row_col.0 += 1; - txt_row_col.1 = 0; - } - } - MarkupNode::Text { - content, - syn_high_style, - attributes, - parent_id_opt: _, - newlines_at_end, - } => { - let highlight_color = map_get(&code_style.ed_theme.syntax_high_map, syn_high_style)?; - - let full_content = markup_node.get_full_content().replace('\n', "\\n"); // any \n left here should be escaped so that it can be shown as \n - - let glyph_text = glyph_brush::OwnedText::new(&full_content) - .with_color(colors::to_slice(*highlight_color)) - .with_scale(code_style.font_size); - - for attribute in &attributes.all { - match attribute { - Attribute::Underline { underline_spec: _ } => { - // TODO use underline_spec - let top_left_coords = ( - code_style.txt_coords.x + (txt_row_col.1 as f32) * char_width, - code_style.txt_coords.y - + (txt_row_col.0 as f32) * char_height - + 1.0 * char_height, - ); - - let underline_rect = Rect { - top_left_coords: top_left_coords.into(), - width: char_width * (full_content.len() as f32), - height: 5.0, - color: *code_style - .ed_theme - .underline_color_map - .get(&UnderlineStyle::Error) - .unwrap(), - }; - - rects.push(underline_rect); - } - rest => todo!("handle Attribute: {:?}", rest), - } - } - - txt_row_col.1 += content.len(); - - for _ in 0..*newlines_at_end { - txt_row_col.0 += 1; - txt_row_col.1 = 0; - } - - wgpu_texts.push(glyph_text); - } - MarkupNode::Blank { - attributes: _, - parent_id_opt: _, - newlines_at_end, - } => { - let full_content = markup_node.get_full_content(); - - let glyph_text = glyph_brush::OwnedText::new(full_content) - .with_color(colors::to_slice(colors::WHITE)) - .with_scale(code_style.font_size); - - let highlight_color = - map_get(&code_style.ed_theme.syntax_high_map, &HighlightStyle::Blank)?; - - let blank_rect = Rect { - top_left_coords: ( - code_style.txt_coords.x + (txt_row_col.1 as f32) * char_width, - code_style.txt_coords.y - + (txt_row_col.0 as f32) * char_height - + 0.1 * char_height, - ) - .into(), - width: char_width, - height: char_height, - color: *highlight_color, - }; - rects.push(blank_rect); - - txt_row_col.1 += BLANK_PLACEHOLDER.len(); - wgpu_texts.push(glyph_text); - - for _ in 0..*newlines_at_end { - txt_row_col.0 += 1; - txt_row_col.1 = 0; - } - } - MarkupNode::Indent { .. } => { - let full_content: String = markup_node.get_content(); - - txt_row_col.1 += full_content.len(); - - let glyph_text = glyph_brush::OwnedText::new(full_content) - .with_color(colors::to_slice(colors::WHITE)) - .with_scale(code_style.font_size); - - wgpu_texts.push(glyph_text); - } - }; - - Ok(()) -} - -fn newline(font_size: f32) -> glyph_brush::OwnedText { - glyph_brush::OwnedText::new("\n").with_scale(font_size) -} diff --git a/editor/src/editor/render_debug.rs b/editor/src/editor/render_debug.rs deleted file mode 100644 index dec6950f60..0000000000 --- a/editor/src/editor/render_debug.rs +++ /dev/null @@ -1,81 +0,0 @@ -use crate::editor::ed_error::EdResult; -use crate::editor::mvc::ed_model::EdModel; -use crate::graphics::colors; -use crate::graphics::colors::from_hsb; -use crate::graphics::primitives::text as gr_text; -use cgmath::Vector2; -use roc_ast::lang::core::def::def2::def2_to_string; -use roc_code_markup::markup::nodes::tree_as_string; -use winit::dpi::PhysicalSize; - -use crate::editor::config::Config; - -pub fn build_debug_graphics( - size: &PhysicalSize, - txt_coords: Vector2, - config: &Config, - ed_model: &EdModel, -) -> EdResult { - let area_bounds = (size.width as f32, size.height as f32); - let layout = wgpu_glyph::Layout::default().h_align(wgpu_glyph::HorizontalAlign::Left); - - let debug_txt_coords: Vector2 = (txt_coords.x * 20.0, txt_coords.y).into(); - - let carets_text = - glyph_brush::OwnedText::new(format!("carets: {:?}\n\n", ed_model.get_carets())) - .with_color(colors::to_slice(from_hsb(0, 0, 100))) - .with_scale(config.debug_font_size); - - let grid_node_map_text = glyph_brush::OwnedText::new(format!("{}", ed_model.grid_node_map)) - .with_color(colors::to_slice(from_hsb(20, 41, 100))) - .with_scale(config.debug_font_size); - - let code_lines_text = glyph_brush::OwnedText::new(format!("{}", ed_model.code_lines)) - .with_color(colors::to_slice(from_hsb(0, 49, 96))) - .with_scale(config.debug_font_size); - - let mut mark_node_trees_string = "\nmark node trees:".to_owned(); - - for mark_id in ed_model.markup_ids[1..].iter() { - // 1.. -> skip header - mark_node_trees_string.push_str(&tree_as_string(*mark_id, &ed_model.mark_node_pool)); - } - - let mark_node_tree_text = glyph_brush::OwnedText::new(mark_node_trees_string) - .with_color(colors::to_slice(from_hsb(266, 31, 96))) - .with_scale(config.debug_font_size); - - let mark_node_pool_text = glyph_brush::OwnedText::new( - ed_model - .mark_node_pool - .debug_string(&ed_model.mark_id_ast_id_map), - ) - .with_color(colors::to_slice(from_hsb(110, 45, 82))) - .with_scale(config.debug_font_size); - - let mut ast_node_text_str = "AST:\n".to_owned(); - - for def_id in ed_model.module.ast.def_ids.iter() { - ast_node_text_str.push_str(&def2_to_string(*def_id, ed_model.module.env.pool)) - } - - let ast_node_text = glyph_brush::OwnedText::new(ast_node_text_str) - .with_color(colors::to_slice(from_hsb(211, 80, 100))) - .with_scale(config.debug_font_size); - - let section = gr_text::owned_section_from_glyph_texts( - vec![ - carets_text, - grid_node_map_text, - code_lines_text, - mark_node_tree_text, - mark_node_pool_text, - ast_node_text, - ], - debug_txt_coords.into(), - area_bounds, - layout, - ); - - Ok(section) -} diff --git a/editor/src/editor/resources/mod.rs b/editor/src/editor/resources/mod.rs deleted file mode 100644 index e8dfd788ff..0000000000 --- a/editor/src/editor/resources/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod strings; diff --git a/editor/src/editor/resources/sounds/bell_sound.mp3 b/editor/src/editor/resources/sounds/bell_sound.mp3 deleted file mode 100644 index c18b130858..0000000000 Binary files a/editor/src/editor/resources/sounds/bell_sound.mp3 and /dev/null differ diff --git a/editor/src/editor/resources/strings.rs b/editor/src/editor/resources/strings.rs deleted file mode 100644 index 0b521044af..0000000000 --- a/editor/src/editor/resources/strings.rs +++ /dev/null @@ -1,44 +0,0 @@ -#![allow(dead_code)] - -pub const NOTHING_OPENED: &str = - "Execute `cargo run edit` from the root folder of the repo to try the editor."; - -pub const START_TIP: &str = r#"Currently supported: lists, records, string, numbers and value definitions. - -Use `Ctrl+Shift+Up` or `Cmd+Shift+Up` to select surrounding expression. -Use backspace after `Ctrl+Shift+Up` to delete the selected expression. - -`Ctrl+S` or `Cmd+S` to save. -`Ctrl+R` to run. - -Input chars that would create parse errors or change formatting will be ignored. -For convenience and consistency, there is only one way to format roc. -"#; - -pub const HELLO_WORLD: &str = r#" -app "test-app" - packages { pf: "c-platform" } - imports [] - provides [ main ] to pf - -main = "Hello, world!" - -"#; - -pub fn nr_hello_world_lines() -> usize { - HELLO_WORLD.matches('\n').count() - 1 -} - -pub const PLATFORM_NAME: &str = "c-platform"; - -pub const PLATFORM_STR: &str = r#" -platform "test-platform" - requires {} { main : Str } - exposes [] - packages {} - imports [] - provides [ mainForHost ] - -mainForHost : Str -mainForHost = main -"#; diff --git a/editor/src/editor/sound.rs b/editor/src/editor/sound.rs deleted file mode 100644 index 76a9994de6..0000000000 --- a/editor/src/editor/sound.rs +++ /dev/null @@ -1,45 +0,0 @@ -use rodio::{Decoder, OutputStream, Sink}; -use std::fs::File; -use std::io::BufReader; - -pub(crate) fn play_sound(sound_path_str: &str) { - let out_stream_res = OutputStream::try_default(); - - match out_stream_res { - Ok((_, out_stream_handle)) => match Sink::try_new(&out_stream_handle) { - Ok(sink) => match File::open(sound_path_str) { - Ok(file) => { - let reader = BufReader::new(file); - - match Decoder::new(reader) { - Ok(decoder) => { - sink.append(decoder); - sink.sleep_until_end(); - } - Err(e) => { - println!("Failed to create Decoder from BufReader from sound file at {}. Error message: {:?}", sound_path_str, e); - } - } - } - Err(e) => { - println!( - "Failed to open sound file at {}. Error message: {}", - sound_path_str, e - ); - } - }, - Err(e) => { - println!( - "Failed to create Sink to play sound. Error message: {:?}", - e - ); - } - }, - Err(e) => { - println!( - "Failed to create OutputStream to play sound. Error message: {:?}", - e - ); - } - } -} diff --git a/editor/src/editor/theme.rs b/editor/src/editor/theme.rs deleted file mode 100644 index d3482f2cbe..0000000000 --- a/editor/src/editor/theme.rs +++ /dev/null @@ -1,31 +0,0 @@ -use gr_colors::{from_hsb, RgbaTup}; -use roc_code_markup::{ - syntax_highlight::{default_highlight_map, HighlightStyle}, - underline_style::{default_underline_color_map, UnderlineStyle}, -}; -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; - -use crate::graphics::colors as gr_colors; -use crate::ui::theme::UITheme; - -#[derive(Serialize, Deserialize)] -pub struct EdTheme { - pub background: RgbaTup, - pub subtle_text: RgbaTup, - pub syntax_high_map: HashMap, - pub ui_theme: UITheme, - pub underline_color_map: HashMap, -} - -impl Default for EdTheme { - fn default() -> Self { - Self { - background: from_hsb(240, 10, 19), // #2C2C35 - subtle_text: from_hsb(240, 5, 60), - syntax_high_map: default_highlight_map(), - ui_theme: UITheme::default(), - underline_color_map: default_underline_color_map(), - } - } -} diff --git a/editor/src/editor/util.rs b/editor/src/editor/util.rs deleted file mode 100644 index 5e0fa1bb97..0000000000 --- a/editor/src/editor/util.rs +++ /dev/null @@ -1,68 +0,0 @@ -use super::ed_error::{EdResult, KeyNotFound}; -use crate::editor::ed_error::IndexOfFailed; -use snafu::OptionExt; -use std::collections::HashMap; - -// replace HashMap method that returns Option with one that returns Result and proper Error -pub fn map_get<'a, K: ::std::fmt::Debug + std::hash::Hash + std::cmp::Eq, V>( - hash_map: &'a HashMap, - key: &K, -) -> EdResult<&'a V> { - let value = hash_map.get(key).context(KeyNotFound { - key_str: format!("{:?}", key), - })?; - - Ok(value) -} - -pub fn index_of(elt: T, slice: &[T]) -> EdResult { - let index = slice - .iter() - .position(|slice_elt| *slice_elt == elt) - .with_context(|| { - let elt_str = format!("{:?}", elt); - let collection_str = format!("{:?}", slice); - - IndexOfFailed { - elt_str, - collection_str, - } - })?; - - Ok(index) -} - -// returns the index of the first occurrence of element and index of the last occurrence -pub fn first_last_index_of( - elt: T, - slice: &[T], -) -> EdResult<(usize, usize)> { - let mut first_index_opt = None; - let mut last_index_opt = None; - - for (index, list_elt) in slice.iter().enumerate() { - if *list_elt == elt { - if first_index_opt.is_none() { - first_index_opt = Some(index); - last_index_opt = Some(index); - } else { - last_index_opt = Some(index) - } - } else if last_index_opt.is_some() { - break; - } - } - - if let (Some(first_index), Some(last_index)) = (first_index_opt, last_index_opt) { - Ok((first_index, last_index)) - } else { - let elt_str = format!("{:?}", elt); - let collection_str = format!("{:?}", slice); - - IndexOfFailed { - elt_str, - collection_str, - } - .fail() - } -} diff --git a/editor/src/graphics/colors.rs b/editor/src/graphics/colors.rs deleted file mode 100644 index 67dc2801a8..0000000000 --- a/editor/src/graphics/colors.rs +++ /dev/null @@ -1,31 +0,0 @@ -use palette::{FromColor, Hsv, LinSrgb, Srgb}; - -pub type RgbaTup = (f32, f32, f32, f32); -pub const WHITE: RgbaTup = (1.0, 1.0, 1.0, 1.0); - -pub fn to_wgpu_color((r, g, b, a): RgbaTup) -> wgpu::Color { - wgpu::Color { - r: r as f64, - g: g as f64, - b: b as f64, - a: a as f64, - } -} - -pub fn to_slice((r, g, b, a): RgbaTup) -> [f32; 4] { - [r, g, b, a] -} - -pub fn from_hsb(hue: usize, saturation: usize, brightness: usize) -> RgbaTup { - from_hsba(hue, saturation, brightness, 1.0) -} - -pub fn from_hsba(hue: usize, saturation: usize, brightness: usize, alpha: f32) -> RgbaTup { - let rgb = LinSrgb::from(Srgb::from_color(Hsv::new( - hue as f32, - (saturation as f32) / 100.0, - (brightness as f32) / 100.0, - ))); - - (rgb.red, rgb.green, rgb.blue, alpha) -} diff --git a/editor/src/graphics/lowlevel/buffer.rs b/editor/src/graphics/lowlevel/buffer.rs deleted file mode 100644 index 76f26e0480..0000000000 --- a/editor/src/graphics/lowlevel/buffer.rs +++ /dev/null @@ -1,168 +0,0 @@ -// Adapted from https://github.com/sotrh/learn-wgpu -// by Benjamin Hansen - license information can be found in the LEGAL_DETAILS -// file in the root directory of this distribution. -// -// Thank you, Benjamin! -use super::vertex::Vertex; -use crate::graphics::colors::to_slice; -use crate::graphics::primitives::rect::Rect; -use wgpu::util::{BufferInitDescriptor, DeviceExt}; - -pub struct QuadBufferBuilder { - vertex_data: Vec, - index_data: Vec, - current_quad: u32, -} - -impl QuadBufferBuilder { - pub fn new() -> Self { - Self { - vertex_data: Vec::new(), - index_data: Vec::new(), - current_quad: 0, - } - } - - pub fn push_rect(self, rect: &Rect) -> Self { - let coords = rect.top_left_coords; - self.push_quad( - coords.x, - coords.y, - coords.x + rect.width, - coords.y + rect.height, - to_slice(rect.color), - ) - } - - pub fn push_quad( - mut self, - min_x: f32, - min_y: f32, - max_x: f32, - max_y: f32, - color: [f32; 4], - ) -> Self { - self.vertex_data.extend(&[ - Vertex { - position: (min_x, min_y).into(), - color, - }, - Vertex { - position: (max_x, min_y).into(), - color, - }, - Vertex { - position: (max_x, max_y).into(), - color, - }, - Vertex { - position: (min_x, max_y).into(), - color, - }, - ]); - self.index_data.extend(&[ - self.current_quad * 4, - self.current_quad * 4 + 1, - self.current_quad * 4 + 2, - self.current_quad * 4, - self.current_quad * 4 + 2, - self.current_quad * 4 + 3, - ]); - self.current_quad += 1; - self - } - - pub fn build(self, device: &wgpu::Device) -> (StagingBuffer, StagingBuffer, u32) { - ( - StagingBuffer::new(device, &self.vertex_data), - StagingBuffer::new(device, &self.index_data), - self.index_data.len() as u32, - ) - } -} - -impl Default for QuadBufferBuilder { - fn default() -> Self { - Self::new() - } -} - -pub struct RectBuffers { - pub vertex_buffer: wgpu::Buffer, - pub index_buffer: wgpu::Buffer, - pub num_rects: u32, -} - -pub fn create_rect_buffers( - gpu_device: &wgpu::Device, - encoder: &mut wgpu::CommandEncoder, - rects: &[Rect], -) -> RectBuffers { - let nr_of_rects = rects.len() as u64; - - let vertex_buffer = gpu_device.create_buffer(&wgpu::BufferDescriptor { - label: None, - size: Vertex::SIZE * 4 * nr_of_rects, - usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, - mapped_at_creation: false, - }); - - let u32_size = std::mem::size_of::() as wgpu::BufferAddress; - - let index_buffer = gpu_device.create_buffer(&wgpu::BufferDescriptor { - label: None, - size: u32_size * 6 * nr_of_rects, - usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST, - mapped_at_creation: false, - }); - - let num_rects = { - let mut quad_buffer_builder = QuadBufferBuilder::new(); - for rect in rects { - quad_buffer_builder = quad_buffer_builder.push_rect(rect); - } - - let (stg_vertex, stg_index, num_indices) = quad_buffer_builder.build(gpu_device); - - stg_vertex.copy_to_buffer(encoder, &vertex_buffer); - stg_index.copy_to_buffer(encoder, &index_buffer); - num_indices - }; - - RectBuffers { - vertex_buffer, - index_buffer, - num_rects, - } -} - -pub struct StagingBuffer { - buffer: wgpu::Buffer, - size: wgpu::BufferAddress, -} - -impl StagingBuffer { - pub fn new(device: &wgpu::Device, data: &[T]) -> StagingBuffer { - StagingBuffer { - buffer: device.create_buffer_init(&BufferInitDescriptor { - contents: bytemuck::cast_slice(data), - usage: wgpu::BufferUsages::COPY_SRC, - label: Some("Staging Buffer"), - }), - size: size_of_slice(data) as wgpu::BufferAddress, - } - } - - pub fn copy_to_buffer(&self, encoder: &mut wgpu::CommandEncoder, other: &wgpu::Buffer) { - encoder.copy_buffer_to_buffer(&self.buffer, 0, other, 0, self.size) - } -} - -// Taken from https://github.com/sotrh/learn-wgpu -// by Benjamin Hansen - license information can be found in the LEGAL_DETAILS -// file in the root directory of this distribution. -// -// Thank you, Benjamin! -pub fn size_of_slice(slice: &[T]) -> usize { - std::mem::size_of::() * slice.len() -} diff --git a/editor/src/graphics/lowlevel/mod.rs b/editor/src/graphics/lowlevel/mod.rs deleted file mode 100644 index 71f3ecc015..0000000000 --- a/editor/src/graphics/lowlevel/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub mod buffer; -pub mod ortho; -pub mod pipelines; -pub mod vertex; diff --git a/editor/src/graphics/lowlevel/pipelines.rs b/editor/src/graphics/lowlevel/pipelines.rs deleted file mode 100644 index 5bc768a33f..0000000000 --- a/editor/src/graphics/lowlevel/pipelines.rs +++ /dev/null @@ -1,70 +0,0 @@ -use super::ortho::{init_ortho, OrthoResources}; -use super::vertex::Vertex; -use std::borrow::Cow; - -pub struct RectResources { - pub pipeline: wgpu::RenderPipeline, - pub ortho: OrthoResources, -} - -pub fn make_rect_pipeline( - gpu_device: &wgpu::Device, - surface_config: &wgpu::SurfaceConfiguration, -) -> RectResources { - let ortho = init_ortho(surface_config.width, surface_config.height, gpu_device); - - let pipeline_layout = gpu_device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { - bind_group_layouts: &[&ortho.bind_group_layout], - push_constant_ranges: &[], - label: Some("Rectangle pipeline layout"), - }); - let pipeline = create_render_pipeline( - gpu_device, - &pipeline_layout, - surface_config.format, - &wgpu::ShaderModuleDescriptor { - label: None, - source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!("../shaders/shader.wgsl"))), - }, - ); - - RectResources { pipeline, ortho } -} - -pub fn create_render_pipeline( - device: &wgpu::Device, - layout: &wgpu::PipelineLayout, - color_format: wgpu::TextureFormat, - shader_module_desc: &wgpu::ShaderModuleDescriptor, -) -> wgpu::RenderPipeline { - let shader = device.create_shader_module(shader_module_desc); - - device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { - label: Some("Render pipeline"), - layout: Some(layout), - vertex: wgpu::VertexState { - module: &shader, - entry_point: "vs_main", - buffers: &[Vertex::DESC], - }, - fragment: Some(wgpu::FragmentState { - module: &shader, - entry_point: "fs_main", - targets: &[wgpu::ColorTargetState { - format: color_format, - blend: Some(wgpu::BlendState { - color: wgpu::BlendComponent { - operation: wgpu::BlendOperation::Add, - src_factor: wgpu::BlendFactor::SrcAlpha, - dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha, - }, - alpha: wgpu::BlendComponent::REPLACE, - }), - write_mask: wgpu::ColorWrites::ALL, - }], - }), - primitive: wgpu::PrimitiveState::default(), - depth_stencil: None, - multisample: wgpu::MultisampleState::default(), - }) -} diff --git a/editor/src/graphics/lowlevel/vertex.rs b/editor/src/graphics/lowlevel/vertex.rs deleted file mode 100644 index 85f40bce46..0000000000 --- a/editor/src/graphics/lowlevel/vertex.rs +++ /dev/null @@ -1,37 +0,0 @@ -// Taken from https://github.com/sotrh/learn-wgpu -// by Benjamin Hansen - license information can be found in the LEGAL_DETAILS -// file in the root directory of this distribution. -// -// Thank you, Benjamin! -use cgmath::Vector2; - -#[derive(Copy, Clone)] -pub struct Vertex { - pub position: Vector2, - pub color: [f32; 4], -} - -unsafe impl bytemuck::Pod for Vertex {} -unsafe impl bytemuck::Zeroable for Vertex {} - -impl Vertex { - pub const SIZE: wgpu::BufferAddress = std::mem::size_of::() as wgpu::BufferAddress; - pub const DESC: wgpu::VertexBufferLayout<'static> = wgpu::VertexBufferLayout { - array_stride: Self::SIZE, - step_mode: wgpu::VertexStepMode::Vertex, - attributes: &[ - // position - wgpu::VertexAttribute { - offset: 0, - shader_location: 0, - format: wgpu::VertexFormat::Float32x2, - }, - // color - wgpu::VertexAttribute { - offset: std::mem::size_of::<[f32; 2]>() as wgpu::BufferAddress, - shader_location: 1, - format: wgpu::VertexFormat::Float32x4, - }, - ], - }; -} diff --git a/editor/src/graphics/primitives/rect.rs b/editor/src/graphics/primitives/rect.rs deleted file mode 100644 index ddf26f1199..0000000000 --- a/editor/src/graphics/primitives/rect.rs +++ /dev/null @@ -1,19 +0,0 @@ -use cgmath::Vector2; - -/// These fields are ordered this way because in Roc, the corresponding stuct is: -/// -/// { top : F32, left : F32, width : F32, height : F32 } -/// -/// alphabetically, that's { height, left, top, width } - which works out to the same as: -/// -/// height: f32, pos: Vector2, width: f32 -/// -/// ...because Vector2 is a repr(C) struct of { x: f32, y: f32 } -#[derive(Debug, Copy, Clone)] -#[repr(C)] -pub struct Rect { - pub color: (f32, f32, f32, f32), - pub height: f32, - pub top_left_coords: Vector2, - pub width: f32, -} diff --git a/editor/src/graphics/primitives/text.rs b/editor/src/graphics/primitives/text.rs deleted file mode 100644 index 57cd6d2ef9..0000000000 --- a/editor/src/graphics/primitives/text.rs +++ /dev/null @@ -1,160 +0,0 @@ -// Adapted from https://github.com/sotrh/learn-wgpu -// by Benjamin Hansen - license information can be found in the LEGAL_DETAILS -// file in the root directory of this distribution. -// -// Thank you, Benjamin! - -use super::rect::Rect; -use crate::graphics::colors; -use crate::graphics::colors::RgbaTup; -use crate::graphics::style::DEFAULT_FONT_SIZE; -use ab_glyph::{FontArc, Glyph, InvalidFont}; -use cgmath::{Vector2, Vector4}; -use glyph_brush::OwnedSection; -use wgpu_glyph::{ab_glyph, GlyphBrush, GlyphBrushBuilder, GlyphCruncher, Section}; - -#[derive(Debug)] -pub struct Text<'a> { - pub position: Vector2, - pub area_bounds: Vector2, - pub color: RgbaTup, - pub text: &'a str, - pub size: f32, - pub visible: bool, - pub centered: bool, -} - -impl<'a> Default for Text<'a> { - fn default() -> Self { - Self { - position: (0.0, 0.0).into(), - area_bounds: (std::f32::INFINITY, std::f32::INFINITY).into(), - color: colors::WHITE, - text: "", - size: DEFAULT_FONT_SIZE, - visible: true, - centered: false, - } - } -} - -// necessary to get dimensions for caret -pub fn example_code_glyph_rect(glyph_brush: &mut GlyphBrush<()>, font_size: f32) -> Rect { - let code_text = Text { - position: (0.0, 0.0).into(), - area_bounds: (std::f32::INFINITY, std::f32::INFINITY).into(), - color: colors::WHITE, - text: "a", - size: font_size, - ..Default::default() - }; - - let layout = layout_from_text(&code_text); - - let section = section_from_text(&code_text, layout); - - let mut glyph_section_iter = glyph_brush.glyphs_custom_layout(section, &layout); - - if let Some(glyph) = glyph_section_iter.next() { - glyph_to_rect(glyph) - } else { - unreachable!(); - } -} - -pub fn layout_from_text(text: &Text) -> wgpu_glyph::Layout { - wgpu_glyph::Layout::default().h_align(if text.centered { - wgpu_glyph::HorizontalAlign::Center - } else { - wgpu_glyph::HorizontalAlign::Left - }) -} - -fn section_from_text<'a>( - text: &'a Text, - layout: wgpu_glyph::Layout, -) -> wgpu_glyph::Section<'a> { - Section { - screen_position: text.position.into(), - bounds: text.area_bounds.into(), - layout, - ..Section::default() - } - .add_text( - wgpu_glyph::Text::new(text.text) - .with_color(Vector4::from(text.color)) - .with_scale(text.size), - ) -} - -pub fn owned_section_from_text(text: &Text) -> OwnedSection { - let layout = layout_from_text(text); - - OwnedSection { - screen_position: text.position.into(), - bounds: text.area_bounds.into(), - layout, - ..OwnedSection::default() - } - .add_text( - glyph_brush::OwnedText::new(text.text) - .with_color(Vector4::from(text.color)) - .with_scale(text.size), - ) -} - -pub fn owned_section_from_glyph_texts( - text: Vec, - screen_position: (f32, f32), - area_bounds: (f32, f32), - layout: wgpu_glyph::Layout, -) -> glyph_brush::OwnedSection { - glyph_brush::OwnedSection { - screen_position, - bounds: area_bounds, - layout, - text, - } -} - -pub fn queue_text_draw(text: &Text, glyph_brush: &mut GlyphBrush<()>) { - let layout = layout_from_text(text); - - let section = section_from_text(text, layout); - - glyph_brush.queue(section.clone()); -} - -fn glyph_to_rect(glyph: &wgpu_glyph::SectionGlyph) -> Rect { - let position = glyph.glyph.position; - let px_scale = glyph.glyph.scale; - let width = glyph_width(&glyph.glyph); - let height = px_scale.y; - let top_y = glyph_top_y(&glyph.glyph); - - Rect { - top_left_coords: [position.x, top_y].into(), - width, - height, - color: colors::WHITE, - } -} - -pub fn glyph_top_y(glyph: &Glyph) -> f32 { - let height = glyph.scale.y; - - glyph.position.y - height * 0.75 -} - -pub fn glyph_width(glyph: &Glyph) -> f32 { - glyph.scale.x * 0.4765 -} - -pub fn build_glyph_brush( - gpu_device: &wgpu::Device, - render_format: wgpu::TextureFormat, -) -> Result, InvalidFont> { - let inconsolata = FontArc::try_from_slice(include_bytes!("../../../Inconsolata-Regular.ttf"))?; - - Ok(GlyphBrushBuilder::using_font(inconsolata).build(gpu_device, render_format)) -} diff --git a/editor/src/graphics/shaders/shader.wgsl b/editor/src/graphics/shaders/shader.wgsl deleted file mode 100644 index e25484bdb1..0000000000 --- a/editor/src/graphics/shaders/shader.wgsl +++ /dev/null @@ -1,30 +0,0 @@ -struct VertexOutput { - [[location(0)]] color: vec4; - [[builtin(position)]] position: vec4; -}; - -[[block]] -struct Globals { - ortho: mat4x4; -}; - -[[group(0), binding(0)]] -var u_globals: Globals; - -[[stage(vertex)]] -fn vs_main( - [[location(0)]] in_position: vec2, - [[location(1)]] in_color: vec4, -) -> VertexOutput { - var out: VertexOutput; - - out.position = u_globals.ortho * vec4(in_position, 0.0, 1.0); - out.color = in_color; - - return out; -} - -[[stage(fragment)]] -fn fs_main(in: VertexOutput) -> [[location(0)]] vec4 { - return in.color; -} \ No newline at end of file diff --git a/editor/src/lib.rs b/editor/src/lib.rs deleted file mode 100644 index 5ee7ea9fd5..0000000000 --- a/editor/src/lib.rs +++ /dev/null @@ -1,20 +0,0 @@ -#![warn(clippy::dbg_macro)] -// See github.com/rtfeldman/roc/issues/800 for discussion of the large_enum_variant check. -#![allow(clippy::large_enum_variant, clippy::upper_case_acronyms)] - -#[cfg_attr(test, macro_use)] -extern crate pest; -#[cfg_attr(test, macro_use)] -extern crate pest_derive; - -mod editor; -mod graphics; -mod ui; -mod window; - -use std::io; -use std::path::Path; - -pub fn launch(project_dir_path_opt: Option<&Path>) -> io::Result<()> { - editor::main::launch(project_dir_path_opt) -} diff --git a/editor/src/ui/mod.rs b/editor/src/ui/mod.rs deleted file mode 100644 index 308df14af0..0000000000 --- a/editor/src/ui/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -pub mod text; -pub mod theme; -pub mod tooltip; -pub mod ui_error; -pub mod util; diff --git a/editor/src/ui/text/big_text_area.rs b/editor/src/ui/text/big_text_area.rs deleted file mode 100644 index 05fa498c9f..0000000000 --- a/editor/src/ui/text/big_text_area.rs +++ /dev/null @@ -1,3411 +0,0 @@ -// Adapted from https://github.com/cessen/ropey by Nathan Vegdahl, licensed under the MIT license - -#![allow(dead_code)] - -use crate::ui::text::{ - caret_w_select::CaretWSelect, - lines, - lines::{Lines, MutSelectableLines, SelectableLines}, - selection::{validate_raw_sel, RawSelection, Selection}, - text_pos::TextPos, -}; -use crate::ui::ui_error::{OutOfBounds, UIResult}; -use crate::ui::util::is_newline; -use crate::window::keyboard_input::{no_mods, Modifiers}; -use bumpalo::Bump; -use snafu::ensure; -use std::{fmt, path::Path}; -use winit::event::{VirtualKeyCode, VirtualKeyCode::*}; - -use super::text_buffer::TextBuffer; - -pub struct BigTextArea { - pub caret_w_select: CaretWSelect, - text_buffer: TextBuffer, - pub path_str: String, - arena: Bump, -} - -impl BigTextArea { - fn check_bounds(&self, char_indx: usize) -> UIResult<()> { - ensure!( - char_indx <= self.text_buffer.nr_of_chars(), - OutOfBounds { - index: char_indx, - collection_name: "TextBuffer", - len: self.text_buffer.nr_of_chars() - } - ); - - Ok(()) - } -} - -impl Lines for BigTextArea { - fn get_line_ref(&self, line_nr: usize) -> UIResult<&str> { - self.text_buffer.get_line_ref(line_nr) - } - - fn line_len(&self, line_nr: usize) -> UIResult { - self.get_line_ref(line_nr).map(|line| line.len()) - } - - fn nr_of_lines(&self) -> usize { - self.text_buffer.nr_of_lines() - } - - fn nr_of_chars(&self) -> usize { - self.text_buffer.nr_of_chars() - } - - fn all_lines_as_string(&self) -> String { - self.text_buffer.all_lines_ref().join("\n") - } - - fn is_last_line(&self, line_nr: usize) -> bool { - line_nr == self.nr_of_lines() - 1 - } - - fn last_char(&self, line_nr: usize) -> UIResult> { - Ok(self.get_line_ref(line_nr)?.chars().last()) - } -} - -impl SelectableLines for BigTextArea { - fn get_caret(&self) -> TextPos { - self.caret_w_select.caret_pos - } - - fn set_caret(&mut self, caret_pos: TextPos) { - self.caret_w_select.caret_pos = caret_pos; - } - - fn move_caret_left(&mut self, modifiers: &Modifiers) -> UIResult<()> { - self.caret_w_select = lines::move_caret_left(self, self.caret_w_select, modifiers)?; - - Ok(()) - } - - fn move_caret_right(&mut self, modifiers: &Modifiers) -> UIResult<()> { - self.caret_w_select = lines::move_caret_right(self, self.caret_w_select, modifiers)?; - - Ok(()) - } - - fn move_caret_up(&mut self, modifiers: &Modifiers) -> UIResult<()> { - self.caret_w_select = lines::move_caret_up(self, self.caret_w_select, modifiers)?; - - Ok(()) - } - - fn move_caret_down(&mut self, modifiers: &Modifiers) -> UIResult<()> { - self.caret_w_select = lines::move_caret_down(self, self.caret_w_select, modifiers)?; - - Ok(()) - } - - fn move_caret_home(&mut self, modifiers: &Modifiers) -> UIResult<()> { - self.caret_w_select = lines::move_caret_home(self, self.caret_w_select, modifiers)?; - - Ok(()) - } - - fn move_caret_end(&mut self, modifiers: &Modifiers) -> UIResult<()> { - self.caret_w_select = lines::move_caret_end(self, self.caret_w_select, modifiers)?; - - Ok(()) - } - - fn get_selection(&self) -> Option { - self.caret_w_select.selection_opt - } - - fn is_selection_active(&self) -> bool { - self.get_selection().is_some() - } - - fn get_selected_str(&self) -> UIResult> { - if let Some(val_sel) = self.caret_w_select.selection_opt { - Ok(Some(self.text_buffer.get_selected_str(val_sel)?)) - } else { - Ok(None) - } - } - - fn set_raw_sel(&mut self, raw_sel: RawSelection) -> UIResult<()> { - self.caret_w_select.selection_opt = Some(validate_raw_sel(raw_sel)?); - - Ok(()) - } - - fn set_caret_w_sel(&mut self, caret_w_sel: CaretWSelect) { - self.caret_w_select = caret_w_sel; - } - - fn set_sel_none(&mut self) { - self.caret_w_select.selection_opt = None; - } - - fn select_all(&mut self) -> UIResult<()> { - if self.nr_of_chars() > 0 { - let last_pos = self.last_text_pos()?; - - self.set_raw_sel(RawSelection { - start_pos: TextPos { line: 0, column: 0 }, - end_pos: last_pos, - })?; - - self.set_caret(last_pos); - } - - Ok(()) - } - - fn last_text_pos(&self) -> UIResult { - let line_nr = self.nr_of_lines() - 1; - let last_col = self.line_len(line_nr)?; - - Ok(TextPos { - line: line_nr, - column: last_col, - }) - } - - fn handle_key_down( - &mut self, - modifiers: &Modifiers, - virtual_keycode: VirtualKeyCode, - ) -> UIResult<()> { - match virtual_keycode { - Left => self.move_caret_left(modifiers), - Up => self.move_caret_up(modifiers), - Right => self.move_caret_right(modifiers), - Down => self.move_caret_down(modifiers), - - A => { - if modifiers.cmd_or_ctrl() { - self.select_all() - } else { - Ok(()) - } - } - Home => self.move_caret_home(modifiers), - End => self.move_caret_end(modifiers), - _ => Ok(()), - } - } -} - -impl MutSelectableLines for BigTextArea { - fn insert_char(&mut self, new_char: &char) -> UIResult<()> { - if self.is_selection_active() { - self.del_selection()?; - } - - self.insert_str(&new_char.to_string())?; - - if is_newline(new_char) { - self.set_caret(TextPos { - line: self.caret_w_select.caret_pos.line + 1, - column: 0, - }); - } else { - self.move_caret_right(&no_mods())?; - } - - self.set_sel_none(); - - Ok(()) - } - - fn handle_new_char(&mut self, received_char: &char) -> UIResult<()> { - match received_char { - '\u{8}' | '\u{7f}' => { - // On Linux, '\u{8}' is backspace, - // on macOS '\u{7f}'. - - self.backspace()? - } - - '\u{1}' // Ctrl + A - | '\u{3}' // Ctrl + C - | '\u{16}' // Ctrl + V - | '\u{18}' // Ctrl + X - | '\u{e000}'..='\u{f8ff}' // http://www.unicode.org/faq/private_use.html - | '\u{f0000}'..='\u{ffffd}' // ^ - | '\u{100000}'..='\u{10fffd}' // ^ - => { - // chars that can be ignored - } - - _ => { - self.insert_char(received_char)?; - } - } - - Ok(()) - } - - fn insert_str(&mut self, new_str: &str) -> UIResult<()> { - let caret_pos = self.caret_w_select.caret_pos; - - self.text_buffer.insert_str(caret_pos, new_str)?; - - Ok(()) - } - - fn backspace(&mut self) -> UIResult<()> { - if self.is_selection_active() { - self.del_selection()?; - } else { - let old_caret_pos = self.caret_w_select.caret_pos; - - self.move_caret_left(&no_mods())?; - - self.text_buffer.backspace_char(old_caret_pos)?; - } - - Ok(()) - } - - fn del_selection(&mut self) -> UIResult<()> { - if let Some(selection) = self.caret_w_select.selection_opt { - self.text_buffer.del_selection(selection)?; - - self.set_caret(selection.start_pos); - - self.set_sel_none(); - } - - Ok(()) - } -} - -impl Default for BigTextArea { - fn default() -> Self { - let caret_w_select = CaretWSelect::default(); - let text_buffer = TextBuffer { lines: Vec::new() }; - let path_str = "".to_owned(); - let arena = Bump::new(); - - Self { - caret_w_select, - text_buffer, - path_str, - arena, - } - } -} - -pub fn from_path(path: &Path) -> UIResult { - let text_buffer = TextBuffer::from_path(path)?; - let path_str = path_to_string(path); - - Ok(BigTextArea { - text_buffer, - path_str, - ..Default::default() - }) -} - -#[allow(dead_code)] -// used by tests but will also be used in the future -pub fn from_str_vec(lines: Vec) -> BigTextArea { - BigTextArea { - text_buffer: TextBuffer { lines }, - ..Default::default() - } -} - -fn path_to_string(path: &Path) -> String { - let mut path_str = String::new(); - path_str.push_str(&path.to_string_lossy()); - - path_str -} - -// need to explicitly omit arena -impl fmt::Debug for BigTextArea { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("BigTextArea") - .field("caret_w_select", &self.caret_w_select) - .field("text_buffer", &self.text_buffer) - .field("path_str", &self.path_str) - .finish() - } -} - -#[cfg(test)] -pub mod test_big_sel_text { - use crate::ui::text::caret_w_select::test_caret_w_select::convert_dsl_to_selection; - use crate::ui::text::caret_w_select::test_caret_w_select::convert_selection_to_dsl; - use crate::ui::text::{ - big_text_area::BigTextArea, - lines::{Lines, MutSelectableLines, SelectableLines}, - text_pos::TextPos, - }; - use crate::ui::ui_error::{OutOfBounds, UIResult}; - use crate::window::keyboard_input::{no_mods, Modifiers}; - use snafu::OptionExt; - use std::slice::SliceIndex; - - use super::from_str_vec; - - fn shift_pressed() -> Modifiers { - Modifiers { - shift: true, - ..Default::default() - } - } - - fn insert_at_pos(lines: &mut [String], pos: TextPos, insert_char: char) -> UIResult<()> { - let line = get_mut_res(pos.line, lines)?; - line.insert(pos.column, insert_char); - - Ok(()) - } - - // It's much nicer to have get_mut return a Result with clear error than an Option - fn get_mut_res( - index: usize, - vec: &mut [T], - ) -> UIResult<&mut >::Output> { - let vec_len = vec.len(); - - let elt_ref = vec.get_mut(index).context(OutOfBounds { - index, - collection_name: "Slice", - len: vec_len, - })?; - - Ok(elt_ref) - } - - pub fn big_text_from_dsl_str(lines: &[String]) -> BigTextArea { - from_str_vec( - lines - .iter() - .map(|line| line.replace(&['❮', '❯', '┃'][..], "")) - .collect::>(), - ) - } - - pub fn all_lines_vec(big_sel_text: &BigTextArea) -> Vec { - let mut lines: Vec = Vec::new(); - - for i in 0..big_sel_text.nr_of_lines() { - lines.push(big_sel_text.get_line_ref(i).unwrap().to_string()); - } - - lines - } - - pub fn gen_big_text(lines: &[&str]) -> Result { - let lines_string_slice: Vec = lines.iter().map(|l| l.to_string()).collect(); - let mut big_text = big_text_from_dsl_str(&lines_string_slice); - let caret_w_select = convert_dsl_to_selection(&lines_string_slice).unwrap(); - - big_text.caret_w_select = caret_w_select; - - Ok(big_text) - } - - fn assert_insert( - pre_lines_str: &[&str], - expected_post_lines_str: &[&str], - new_char: char, - ) -> Result<(), String> { - let mut big_text = gen_big_text(pre_lines_str)?; - - if let Err(e) = big_text.handle_new_char(&new_char) { - return Err(e.to_string()); - } - - let actual_lines = all_lines_vec(&big_text); - let dsl_slice = convert_selection_to_dsl(big_text.caret_w_select, actual_lines).unwrap(); - assert_eq!(dsl_slice, expected_post_lines_str); - - Ok(()) - } - - #[test] - fn insert_new_char_simple() -> Result<(), String> { - assert_insert(&["┃"], &["a┃"], 'a')?; - assert_insert(&["┃"], &[" ┃"], ' ')?; - assert_insert(&["a┃"], &["aa┃"], 'a')?; - assert_insert(&["a┃"], &["a ┃"], ' ')?; - assert_insert(&["a┃", ""], &["ab┃", ""], 'b')?; - assert_insert(&["a", "┃"], &["a", "b┃"], 'b')?; - assert_insert(&["a", "b", "c┃"], &["a", "b", "cd┃"], 'd')?; - - Ok(()) - } - - #[test] - fn insert_new_char_mid() -> Result<(), String> { - assert_insert(&["ab┃d"], &["abc┃d"], 'c')?; - assert_insert(&["a┃cd"], &["ab┃cd"], 'b')?; - assert_insert(&["abc", "┃e"], &["abc", "d┃e"], 'd')?; - assert_insert(&["abc", "def", "┃ "], &["abc", "def", "g┃ "], 'g')?; - assert_insert(&["abc", "def", "┃ "], &["abc", "def", " ┃ "], ' ')?; - - Ok(()) - } - - #[test] - fn simple_backspace() -> Result<(), String> { - assert_insert(&["┃"], &["┃"], '\u{8}')?; - assert_insert(&[" ┃"], &["┃"], '\u{8}')?; - assert_insert(&["a┃"], &["┃"], '\u{8}')?; - assert_insert(&["ab┃"], &["a┃"], '\u{8}')?; - assert_insert(&["a┃", ""], &["┃", ""], '\u{8}')?; - assert_insert(&["ab┃", ""], &["a┃", ""], '\u{8}')?; - assert_insert(&["a", "┃"], &["a┃"], '\u{8}')?; - assert_insert(&["a", "b", "c┃"], &["a", "b", "┃"], '\u{8}')?; - assert_insert(&["a", "b", "┃"], &["a", "b┃"], '\u{8}')?; - - Ok(()) - } - - #[test] - fn selection_backspace() -> Result<(), String> { - assert_insert(&["❮a❯┃"], &["┃"], '\u{8}')?; - assert_insert(&["a❮a❯┃"], &["a┃"], '\u{8}')?; - assert_insert(&["❮aa❯┃"], &["┃"], '\u{8}')?; - assert_insert(&["a❮b c❯┃"], &["a┃"], '\u{8}')?; - assert_insert(&["❮abc❯┃", ""], &["┃", ""], '\u{8}')?; - assert_insert(&["a", "❮abc❯┃"], &["a", "┃"], '\u{8}')?; - assert_insert(&["❮a", "abc❯┃"], &["┃"], '\u{8}')?; - assert_insert(&["a❮b", "cdef ghij❯┃"], &["a┃"], '\u{8}')?; - assert_insert(&["❮a", "b", "c❯┃"], &["┃"], '\u{8}')?; - assert_insert(&["a", "❮b", "❯┃"], &["a", "┃"], '\u{8}')?; - assert_insert( - &["abc", "d❮ef", "ghi❯┃", "jkl"], - &["abc", "d┃", "jkl"], - '\u{8}', - )?; - assert_insert( - &["abc", "❮def", "ghi❯┃", "jkl"], - &["abc", "┃", "jkl"], - '\u{8}', - )?; - assert_insert( - &["abc", "", "❮def", "ghi❯┃", "jkl"], - &["abc", "", "┃", "jkl"], - '\u{8}', - )?; - assert_insert(&["❮abc", "", "def", "ghi", "jkl❯┃"], &["┃"], '\u{8}')?; - - Ok(()) - } - - #[test] - fn insert_with_selection() -> Result<(), String> { - assert_insert(&["❮a❯┃"], &["z┃"], 'z')?; - assert_insert(&["a❮a❯┃"], &["az┃"], 'z')?; - assert_insert(&["❮aa❯┃"], &["z┃"], 'z')?; - assert_insert(&["a❮b c❯┃"], &["az┃"], 'z')?; - assert_insert(&["❮abc❯┃", ""], &["z┃", ""], 'z')?; - assert_insert(&["a", "❮abc❯┃"], &["a", "z┃"], 'z')?; - assert_insert(&["❮a", "abc❯┃"], &["z┃"], 'z')?; - assert_insert(&["a❮b", "cdef ghij❯┃"], &["az┃"], 'z')?; - assert_insert(&["❮a", "b", "c❯┃"], &["z┃"], 'z')?; - assert_insert(&["a", "❮b", "❯┃"], &["a", "z┃"], 'z')?; - assert_insert( - &["abc", "d❮ef", "ghi❯┃", "jkl"], - &["abc", "dz┃", "jkl"], - 'z', - )?; - assert_insert(&["abc", "❮def", "ghi❯┃", "jkl"], &["abc", "z┃", "jkl"], 'z')?; - assert_insert( - &["abc", "", "❮def", "ghi❯┃", "jkl"], - &["abc", "", "z┃", "jkl"], - 'z', - )?; - assert_insert(&["❮abc", "", "def", "ghi", "jkl❯┃"], &["z┃"], 'z')?; - - Ok(()) - } - - fn assert_select_all( - pre_lines_str: &[&str], - expected_post_lines_str: &[&str], - ) -> Result<(), String> { - let mut big_text = gen_big_text(pre_lines_str)?; - - big_text.select_all().unwrap(); - - let big_text_lines = all_lines_vec(&big_text); - let post_lines_str = convert_selection_to_dsl(big_text.caret_w_select, big_text_lines)?; - - assert_eq!(post_lines_str, expected_post_lines_str); - - Ok(()) - } - - #[test] - fn select_all() -> Result<(), String> { - assert_select_all(&["┃"], &["┃"])?; - assert_select_all(&["┃a"], &["❮a❯┃"])?; - assert_select_all(&["a┃"], &["❮a❯┃"])?; - assert_select_all(&["abc d┃ef ghi"], &["❮abc def ghi❯┃"])?; - assert_select_all(&["❮a❯┃"], &["❮a❯┃"])?; - assert_select_all(&["┃❮a❯"], &["❮a❯┃"])?; - assert_select_all(&["┃❮abc def ghi❯"], &["❮abc def ghi❯┃"])?; - assert_select_all(&["a", "❮b", "❯┃"], &["❮a", "b", "❯┃"])?; - assert_select_all(&["a", "❮b❯┃", ""], &["❮a", "b", "❯┃"])?; - assert_select_all(&["a", "┃❮b", "❯"], &["❮a", "b", "❯┃"])?; - assert_select_all( - &["abc", "def", "gh┃i", "jkl"], - &["❮abc", "def", "ghi", "jkl❯┃"], - )?; - assert_select_all( - &["┃❮abc", "def", "ghi", "jkl❯"], - &["❮abc", "def", "ghi", "jkl❯┃"], - )?; - - Ok(()) - } - - type MoveCaretFun = fn(&mut BigTextArea, &Modifiers) -> UIResult<()>; - - // Convert nice string representations and compare results - fn assert_move( - pre_lines_str: &[&str], - expected_post_lines_str: &[&str], - modifiers: &Modifiers, - move_fun: MoveCaretFun, - ) -> Result<(), String> { - let expected_post_lines: Vec = expected_post_lines_str - .iter() - .map(|l| l.to_string()) - .collect(); - - let mut big_text = gen_big_text(pre_lines_str)?; - - move_fun(&mut big_text, modifiers)?; - - let lines_vec = all_lines_vec(&big_text) - .iter() - .map(|l| l.replace('\n', "")) - .collect(); - let post_lines_res = convert_selection_to_dsl(big_text.caret_w_select, lines_vec); - - match post_lines_res { - Ok(post_lines) => { - assert_eq!(expected_post_lines, post_lines); - Ok(()) - } - Err(e) => Err(format!("{:?}", e)), - } - } - - #[test] - fn move_right() -> Result<(), String> { - let move_caret_right = SelectableLines::move_caret_right; - - assert_move(&["┃"], &["┃"], &no_mods(), move_caret_right)?; - assert_move(&["a┃"], &["a┃"], &no_mods(), move_caret_right)?; - assert_move(&["┃A"], &["A┃"], &no_mods(), move_caret_right)?; - assert_move(&["┃abc"], &["a┃bc"], &no_mods(), move_caret_right)?; - assert_move(&["a┃bc"], &["ab┃c"], &no_mods(), move_caret_right)?; - assert_move(&["abc┃"], &["abc┃"], &no_mods(), move_caret_right)?; - assert_move(&["┃ abc"], &[" ┃abc"], &no_mods(), move_caret_right)?; - assert_move(&["abc┃ "], &["abc ┃"], &no_mods(), move_caret_right)?; - assert_move(&["abc┃", "d"], &["abc", "┃d"], &no_mods(), move_caret_right)?; - assert_move(&["abc┃", ""], &["abc", "┃"], &no_mods(), move_caret_right)?; - assert_move( - &["abc", "┃def"], - &["abc", "d┃ef"], - &no_mods(), - move_caret_right, - )?; - assert_move( - &["abc", "def┃ "], - &["abc", "def ┃"], - &no_mods(), - move_caret_right, - )?; - assert_move( - &["abc", "def ┃", "ghi"], - &["abc", "def ", "┃ghi"], - &no_mods(), - move_caret_right, - )?; - assert_move( - &["abc", "def┃", ""], - &["abc", "def", "┃"], - &no_mods(), - move_caret_right, - )?; - assert_move( - &["abc", "def", "ghi┃", "jkl"], - &["abc", "def", "ghi", "┃jkl"], - &no_mods(), - move_caret_right, - )?; - assert_move( - &["abc", "def", "┃ghi", "jkl"], - &["abc", "def", "g┃hi", "jkl"], - &no_mods(), - move_caret_right, - )?; - assert_move( - &["abc", "def", "g┃hi", "jkl"], - &["abc", "def", "gh┃i", "jkl"], - &no_mods(), - move_caret_right, - )?; - - Ok(()) - } - - #[test] - fn move_left() -> Result<(), String> { - let move_caret_left = SelectableLines::move_caret_left; - - assert_move(&["┃"], &["┃"], &no_mods(), move_caret_left)?; - assert_move(&["┃a"], &["┃a"], &no_mods(), move_caret_left)?; - assert_move(&["┃A"], &["┃A"], &no_mods(), move_caret_left)?; - assert_move(&["a┃bc"], &["┃abc"], &no_mods(), move_caret_left)?; - assert_move(&["ab┃c"], &["a┃bc"], &no_mods(), move_caret_left)?; - assert_move(&["abc┃"], &["ab┃c"], &no_mods(), move_caret_left)?; - assert_move(&[" ┃abc"], &["┃ abc"], &no_mods(), move_caret_left)?; - assert_move(&["abc ┃"], &["abc┃ "], &no_mods(), move_caret_left)?; - assert_move(&["abc", "┃d"], &["abc┃", "d"], &no_mods(), move_caret_left)?; - assert_move(&["abc", "┃"], &["abc┃", ""], &no_mods(), move_caret_left)?; - assert_move( - &["abc", "d┃ef"], - &["abc", "┃def"], - &no_mods(), - move_caret_left, - )?; - assert_move( - &["abc", "def ┃"], - &["abc", "def┃ "], - &no_mods(), - move_caret_left, - )?; - assert_move( - &["abc", "def ", "┃ghi"], - &["abc", "def ┃", "ghi"], - &no_mods(), - move_caret_left, - )?; - assert_move( - &["abc", "def", "┃"], - &["abc", "def┃", ""], - &no_mods(), - move_caret_left, - )?; - assert_move( - &["abc", "def", "ghi", "┃jkl"], - &["abc", "def", "ghi┃", "jkl"], - &no_mods(), - move_caret_left, - )?; - assert_move( - &["abc", "def", "g┃hi", "jkl"], - &["abc", "def", "┃ghi", "jkl"], - &no_mods(), - move_caret_left, - )?; - assert_move( - &["abc", "def", "gh┃i", "jkl"], - &["abc", "def", "g┃hi", "jkl"], - &no_mods(), - move_caret_left, - )?; - - Ok(()) - } - - #[test] - fn move_up() -> Result<(), String> { - let move_caret_up = SelectableLines::move_caret_up; - - assert_move(&["┃"], &["┃"], &no_mods(), move_caret_up)?; - assert_move(&["┃a"], &["┃a"], &no_mods(), move_caret_up)?; - assert_move(&["A┃"], &["┃A"], &no_mods(), move_caret_up)?; - assert_move(&["a┃bc"], &["┃abc"], &no_mods(), move_caret_up)?; - assert_move(&["ab┃c"], &["┃abc"], &no_mods(), move_caret_up)?; - assert_move(&["abc┃"], &["┃abc"], &no_mods(), move_caret_up)?; - assert_move( - &["┃abc", "def"], - &["┃abc", "def"], - &no_mods(), - move_caret_up, - )?; - assert_move( - &["abc", "┃def"], - &["┃abc", "def"], - &no_mods(), - move_caret_up, - )?; - assert_move( - &["abc", "d┃ef"], - &["a┃bc", "def"], - &no_mods(), - move_caret_up, - )?; - assert_move( - &["abc", "de┃f"], - &["ab┃c", "def"], - &no_mods(), - move_caret_up, - )?; - assert_move( - &["abc", "def┃"], - &["abc┃", "def"], - &no_mods(), - move_caret_up, - )?; - assert_move( - &["abc", "def ", "┃ghi"], - &["abc", "┃def ", "ghi"], - &no_mods(), - move_caret_up, - )?; - assert_move( - &["abc", "def ", "g┃hi"], - &["abc", "d┃ef ", "ghi"], - &no_mods(), - move_caret_up, - )?; - assert_move( - &["abc", "def ", "gh┃i"], - &["abc", "de┃f ", "ghi"], - &no_mods(), - move_caret_up, - )?; - assert_move( - &["abc", "def ", "ghi┃"], - &["abc", "def┃ ", "ghi"], - &no_mods(), - move_caret_up, - )?; - assert_move( - &["abc", "de", "ghi┃"], - &["abc", "de┃", "ghi"], - &no_mods(), - move_caret_up, - )?; - assert_move(&["abc", "de┃"], &["ab┃c", "de"], &no_mods(), move_caret_up)?; - assert_move(&["abc", "d┃e"], &["a┃bc", "de"], &no_mods(), move_caret_up)?; - assert_move(&["abc", "┃de"], &["┃abc", "de"], &no_mods(), move_caret_up)?; - assert_move( - &["ab", "cdef", "ghijkl", "mnopqrst┃"], - &["ab", "cdef", "ghijkl┃", "mnopqrst"], - &no_mods(), - move_caret_up, - )?; - assert_move( - &["ab", "cdef", "ghijkl┃", "mnopqrst"], - &["ab", "cdef┃", "ghijkl", "mnopqrst"], - &no_mods(), - move_caret_up, - )?; - assert_move( - &["ab", "cdef", "ghijkl", "┃mnopqrst"], - &["ab", "cdef", "┃ghijkl", "mnopqrst"], - &no_mods(), - move_caret_up, - )?; - assert_move( - &[" ab", " ┃cdef", "ghijkl", "mnopqrst"], - &[" ┃ab", " cdef", "ghijkl", "mnopqrst"], - &no_mods(), - move_caret_up, - )?; - assert_move( - &["ab", "cdef", "ghijkl", "mnopqr┃st"], - &["ab", "cdef", "ghijkl┃", "mnopqrst"], - &no_mods(), - move_caret_up, - )?; - assert_move( - &["ab", "cde┃f", "ghijkl", "mnopqrst"], - &["ab┃", "cdef", "ghijkl", "mnopqrst"], - &no_mods(), - move_caret_up, - )?; - assert_move( - &["abcdefgh", "ijklmn", "opqr", "st┃"], - &["abcdefgh", "ijklmn", "op┃qr", "st"], - &no_mods(), - move_caret_up, - )?; - assert_move( - &["abcdefgh", "ijklmn", "opqr┃", "st"], - &["abcdefgh", "ijkl┃mn", "opqr", "st"], - &no_mods(), - move_caret_up, - )?; - assert_move( - &["abcdefgh", "ijklmn┃", "opqr", "st"], - &["abcdef┃gh", "ijklmn", "opqr", "st"], - &no_mods(), - move_caret_up, - )?; - assert_move( - &["abcdefgh┃", "ijklmn", "opqr", "st"], - &["┃abcdefgh", "ijklmn", "opqr", "st"], - &no_mods(), - move_caret_up, - )?; - assert_move( - &["abcdefg┃h", "ijklmn", "opqr", "st"], - &["┃abcdefgh", "ijklmn", "opqr", "st"], - &no_mods(), - move_caret_up, - )?; - assert_move( - &["a┃bcdefgh", "ijklmn", "opqr", "st"], - &["┃abcdefgh", "ijklmn", "opqr", "st"], - &no_mods(), - move_caret_up, - )?; - assert_move( - &["┃abcdefgh", "ijklmn", "opqr", "st"], - &["┃abcdefgh", "ijklmn", "opqr", "st"], - &no_mods(), - move_caret_up, - )?; - assert_move( - &["abc def gh ┃"], - &["┃abc def gh "], - &no_mods(), - move_caret_up, - )?; - assert_move( - &["abc de┃f gh "], - &["┃abc def gh "], - &no_mods(), - move_caret_up, - )?; - assert_move( - &["ab┃c def gh "], - &["┃abc def gh "], - &no_mods(), - move_caret_up, - )?; - assert_move( - &["a┃bc def gh "], - &["┃abc def gh "], - &no_mods(), - move_caret_up, - )?; - - Ok(()) - } - - #[test] - fn move_down() -> Result<(), String> { - let move_caret_down = SelectableLines::move_caret_down; - - assert_move(&["┃"], &["┃"], &no_mods(), move_caret_down)?; - assert_move(&["┃a"], &["a┃"], &no_mods(), move_caret_down)?; - assert_move(&["A┃"], &["A┃"], &no_mods(), move_caret_down)?; - assert_move(&["a┃bc"], &["abc┃"], &no_mods(), move_caret_down)?; - assert_move(&["ab┃c"], &["abc┃"], &no_mods(), move_caret_down)?; - assert_move(&["abc┃"], &["abc┃"], &no_mods(), move_caret_down)?; - assert_move(&["abc┃ "], &["abc ┃"], &no_mods(), move_caret_down)?; - assert_move( - &["abc", "┃def"], - &["abc", "def┃"], - &no_mods(), - move_caret_down, - )?; - assert_move( - &["abc", "d┃ef"], - &["abc", "def┃"], - &no_mods(), - move_caret_down, - )?; - assert_move( - &["abc", "de┃f"], - &["abc", "def┃"], - &no_mods(), - move_caret_down, - )?; - assert_move( - &["abc", "def┃"], - &["abc", "def┃"], - &no_mods(), - move_caret_down, - )?; - assert_move( - &["┃abc", "def"], - &["abc", "┃def"], - &no_mods(), - move_caret_down, - )?; - assert_move( - &["a┃bc", "def"], - &["abc", "d┃ef"], - &no_mods(), - move_caret_down, - )?; - assert_move( - &["ab┃c", "def"], - &["abc", "de┃f"], - &no_mods(), - move_caret_down, - )?; - assert_move( - &["abc┃", "def"], - &["abc", "def┃"], - &no_mods(), - move_caret_down, - )?; - assert_move( - &["abc", "┃def ", "ghi"], - &["abc", "def ", "┃ghi"], - &no_mods(), - move_caret_down, - )?; - assert_move( - &["abc", "d┃ef ", "ghi"], - &["abc", "def ", "g┃hi"], - &no_mods(), - move_caret_down, - )?; - assert_move( - &["abc", "de┃f ", "ghi"], - &["abc", "def ", "gh┃i"], - &no_mods(), - move_caret_down, - )?; - assert_move( - &["abc", "def┃ ", "ghi"], - &["abc", "def ", "ghi┃"], - &no_mods(), - move_caret_down, - )?; - assert_move( - &["abc", "def ┃", "ghi"], - &["abc", "def ", "ghi┃"], - &no_mods(), - move_caret_down, - )?; - assert_move( - &["abc", "de┃", "ghi"], - &["abc", "de", "gh┃i"], - &no_mods(), - move_caret_down, - )?; - assert_move( - &["abc┃", "de"], - &["abc", "de┃"], - &no_mods(), - move_caret_down, - )?; - assert_move( - &["ab┃c", "de"], - &["abc", "de┃"], - &no_mods(), - move_caret_down, - )?; - assert_move( - &["a┃bc", "de"], - &["abc", "d┃e"], - &no_mods(), - move_caret_down, - )?; - assert_move( - &["┃abc", "de"], - &["abc", "┃de"], - &no_mods(), - move_caret_down, - )?; - assert_move( - &["ab┃", "cdef", "ghijkl", "mnopqrst"], - &["ab", "cd┃ef", "ghijkl", "mnopqrst"], - &no_mods(), - move_caret_down, - )?; - assert_move( - &["ab", "cdef┃", "ghijkl", "mnopqrst"], - &["ab", "cdef", "ghij┃kl", "mnopqrst"], - &no_mods(), - move_caret_down, - )?; - assert_move( - &["ab", "cdef", "ghijkl┃", "mnopqrst"], - &["ab", "cdef", "ghijkl", "mnopqr┃st"], - &no_mods(), - move_caret_down, - )?; - assert_move( - &[" ┃ab", " cdef", "ghijkl", "mnopqrst"], - &[" ab", " ┃cdef", "ghijkl", "mnopqrst"], - &no_mods(), - move_caret_down, - )?; - assert_move( - &["ab", "┃cdef", "ghijkl", "mnopqrst"], - &["ab", "cdef", "┃ghijkl", "mnopqrst"], - &no_mods(), - move_caret_down, - )?; - assert_move( - &["ab", "cdef", "┃ghijkl", "mnopqrst"], - &["ab", "cdef", "ghijkl", "┃mnopqrst"], - &no_mods(), - move_caret_down, - )?; - assert_move( - &["abcdefgh┃", "ijklmn", "opqr", "st"], - &["abcdefgh", "ijklmn┃", "opqr", "st"], - &no_mods(), - move_caret_down, - )?; - assert_move( - &["abcdefgh", "ijklmn┃", "opqr", "st"], - &["abcdefgh", "ijklmn", "opqr┃", "st"], - &no_mods(), - move_caret_down, - )?; - assert_move( - &["abcdefgh", "ijklmn", "opqr┃", "st"], - &["abcdefgh", "ijklmn", "opqr", "st┃"], - &no_mods(), - move_caret_down, - )?; - assert_move( - &["abcdefgh", "ijklmn", "opqr", "┃st"], - &["abcdefgh", "ijklmn", "opqr", "st┃"], - &no_mods(), - move_caret_down, - )?; - assert_move( - &["abc def gh ┃"], - &["abc def gh ┃"], - &no_mods(), - move_caret_down, - )?; - assert_move( - &["abc de┃f gh "], - &["abc def gh ┃"], - &no_mods(), - move_caret_down, - )?; - assert_move( - &["ab┃c def gh "], - &["abc def gh ┃"], - &no_mods(), - move_caret_down, - )?; - assert_move( - &["a┃bc def gh "], - &["abc def gh ┃"], - &no_mods(), - move_caret_down, - )?; - assert_move( - &["┃abc def gh "], - &["abc def gh ┃"], - &no_mods(), - move_caret_down, - )?; - - Ok(()) - } - - #[test] - fn move_home() -> Result<(), String> { - let move_caret_home = BigTextArea::move_caret_home; - assert_move(&["┃"], &["┃"], &no_mods(), move_caret_home)?; - assert_move(&["a┃"], &["┃a"], &no_mods(), move_caret_home)?; - assert_move(&["┃a"], &["┃a"], &no_mods(), move_caret_home)?; - assert_move(&[" ┃a"], &["┃ a"], &no_mods(), move_caret_home)?; - assert_move(&["┃ a"], &[" ┃a"], &no_mods(), move_caret_home)?; - assert_move(&[" a┃"], &[" ┃a"], &no_mods(), move_caret_home)?; - assert_move(&[" abc ┃"], &[" ┃abc "], &no_mods(), move_caret_home)?; - assert_move(&["\tabc ┃"], &["\t┃abc "], &no_mods(), move_caret_home)?; - assert_move(&["\t┃abc "], &["┃\tabc "], &no_mods(), move_caret_home)?; - assert_move(&["┃\tabc "], &["\t┃abc "], &no_mods(), move_caret_home)?; - assert_move( - &[" abc def\tghi┃"], - &[" ┃abc def\tghi"], - &no_mods(), - move_caret_home, - )?; - assert_move( - &[" ┃abc def\tghi"], - &["┃ abc def\tghi"], - &no_mods(), - move_caret_home, - )?; - assert_move( - &["┃ abc def\tghi"], - &[" ┃abc def\tghi"], - &no_mods(), - move_caret_home, - )?; - - assert_move( - &["abc", "de┃", "ghi"], - &["abc", "┃de", "ghi"], - &no_mods(), - move_caret_home, - )?; - assert_move( - &["abc", " d┃e", "ghi"], - &["abc", " ┃de", "ghi"], - &no_mods(), - move_caret_home, - )?; - assert_move( - &["abc", "┃ de", "ghi"], - &["abc", " ┃de", "ghi"], - &no_mods(), - move_caret_home, - )?; - assert_move( - &["abc", " ┃de", "ghi"], - &["abc", "┃ de", "ghi"], - &no_mods(), - move_caret_home, - )?; - assert_move( - &["abc┃", "de", "ghi"], - &["┃abc", "de", "ghi"], - &no_mods(), - move_caret_home, - )?; - assert_move( - &[" ┃abc", "de", "ghi"], - &["┃ abc", "de", "ghi"], - &no_mods(), - move_caret_home, - )?; - assert_move( - &["┃ abc", "de", "ghi"], - &[" ┃abc", "de", "ghi"], - &no_mods(), - move_caret_home, - )?; - assert_move( - &["abc", "de", "ghi┃"], - &["abc", "de", "┃ghi"], - &no_mods(), - move_caret_home, - )?; - assert_move( - &["abc", "de", " ┃ghi"], - &["abc", "de", "┃ ghi"], - &no_mods(), - move_caret_home, - )?; - assert_move( - &["abc", "de", "┃ ghi"], - &["abc", "de", " ┃ghi"], - &no_mods(), - move_caret_home, - )?; - assert_move( - &["abc ", "de ", "┃ghi "], - &["abc ", "de ", "┃ghi "], - &no_mods(), - move_caret_home, - )?; - assert_move( - &["abc ", "┃de ", "ghi "], - &["abc ", "┃de ", "ghi "], - &no_mods(), - move_caret_home, - )?; - assert_move( - &["┃abc ", "de ", "ghi "], - &["┃abc ", "de ", "ghi "], - &no_mods(), - move_caret_home, - )?; - - Ok(()) - } - - #[test] - fn move_end() -> Result<(), String> { - let move_caret_end = BigTextArea::move_caret_end; - assert_move(&["┃"], &["┃"], &no_mods(), move_caret_end)?; - assert_move(&["┃a"], &["a┃"], &no_mods(), move_caret_end)?; - assert_move(&["a┃"], &["a┃"], &no_mods(), move_caret_end)?; - assert_move(&[" a┃ "], &[" a ┃"], &no_mods(), move_caret_end)?; - assert_move(&["┃ abc "], &[" abc ┃"], &no_mods(), move_caret_end)?; - assert_move(&["┃\tabc "], &["\tabc ┃"], &no_mods(), move_caret_end)?; - assert_move( - &[" abc d┃ef\tghi"], - &[" abc def\tghi┃"], - &no_mods(), - move_caret_end, - )?; - - assert_move( - &["abc", "┃de", "ghi"], - &["abc", "de┃", "ghi"], - &no_mods(), - move_caret_end, - )?; - assert_move( - &["abc", " d┃e", "ghi"], - &["abc", " de┃", "ghi"], - &no_mods(), - move_caret_end, - )?; - assert_move( - &["┃abc", "de", "ghi"], - &["abc┃", "de", "ghi"], - &no_mods(), - move_caret_end, - )?; - assert_move( - &["abc", "de", "g┃hi"], - &["abc", "de", "ghi┃"], - &no_mods(), - move_caret_end, - )?; - assert_move( - &["abc ", "de ", "ghi┃ "], - &["abc ", "de ", "ghi ┃"], - &no_mods(), - move_caret_end, - )?; - assert_move( - &["abc ", "┃de ", "ghi "], - &["abc ", "de ┃", "ghi "], - &no_mods(), - move_caret_end, - )?; - assert_move( - &["abc ┃", "de ", "ghi "], - &["abc ┃", "de ", "ghi "], - &no_mods(), - move_caret_end, - )?; - assert_move( - &["abc ", "de ┃", "ghi "], - &["abc ", "de ┃", "ghi "], - &no_mods(), - move_caret_end, - )?; - assert_move( - &["abc ", "de ", "ghi ┃"], - &["abc ", "de ", "ghi ┃"], - &no_mods(), - move_caret_end, - )?; - - Ok(()) - } - - #[test] - fn start_selection_right() -> Result<(), String> { - let move_caret_right = SelectableLines::move_caret_right; - - assert_move(&["┃"], &["┃"], &shift_pressed(), move_caret_right)?; - assert_move(&["a┃"], &["a┃"], &shift_pressed(), move_caret_right)?; - assert_move(&["┃A"], &["❮A❯┃"], &shift_pressed(), move_caret_right)?; - assert_move(&["┃abc"], &["❮a❯┃bc"], &shift_pressed(), move_caret_right)?; - assert_move(&["a┃bc"], &["a❮b❯┃c"], &shift_pressed(), move_caret_right)?; - assert_move(&["abc┃"], &["abc┃"], &shift_pressed(), move_caret_right)?; - assert_move(&["┃ abc"], &["❮ ❯┃abc"], &shift_pressed(), move_caret_right)?; - assert_move(&["abc┃ "], &["abc❮ ❯┃"], &shift_pressed(), move_caret_right)?; - assert_move( - &["abc┃", "d"], - &["abc❮", "❯┃d"], - &shift_pressed(), - move_caret_right, - )?; - assert_move( - &["abc┃", ""], - &["abc❮", "❯┃"], - &shift_pressed(), - move_caret_right, - )?; - assert_move( - &["abc", "┃def"], - &["abc", "❮d❯┃ef"], - &shift_pressed(), - move_caret_right, - )?; - assert_move( - &["abc", "def┃ "], - &["abc", "def❮ ❯┃"], - &shift_pressed(), - move_caret_right, - )?; - assert_move( - &["abc", "def ┃", "ghi"], - &["abc", "def ❮", "❯┃ghi"], - &shift_pressed(), - move_caret_right, - )?; - assert_move( - &["abc", "def┃", ""], - &["abc", "def❮", "❯┃"], - &shift_pressed(), - move_caret_right, - )?; - assert_move( - &["abc", "def", "ghi┃", "jkl"], - &["abc", "def", "ghi❮", "❯┃jkl"], - &shift_pressed(), - move_caret_right, - )?; - assert_move( - &["abc", "def", "┃ghi", "jkl"], - &["abc", "def", "❮g❯┃hi", "jkl"], - &shift_pressed(), - move_caret_right, - )?; - assert_move( - &["abc", "def", "g┃hi", "jkl"], - &["abc", "def", "g❮h❯┃i", "jkl"], - &shift_pressed(), - move_caret_right, - )?; - - Ok(()) - } - - #[test] - fn start_selection_left() -> Result<(), String> { - let move_caret_left = SelectableLines::move_caret_left; - - assert_move(&["┃"], &["┃"], &shift_pressed(), move_caret_left)?; - assert_move(&["a┃"], &["┃❮a❯"], &shift_pressed(), move_caret_left)?; - assert_move(&["┃A"], &["┃A"], &shift_pressed(), move_caret_left)?; - assert_move(&["┃abc"], &["┃abc"], &shift_pressed(), move_caret_left)?; - assert_move(&["a┃bc"], &["┃❮a❯bc"], &shift_pressed(), move_caret_left)?; - assert_move(&["abc┃"], &["ab┃❮c❯"], &shift_pressed(), move_caret_left)?; - assert_move(&[" ┃abc"], &["┃❮ ❯abc"], &shift_pressed(), move_caret_left)?; - assert_move(&["abc ┃"], &["abc┃❮ ❯"], &shift_pressed(), move_caret_left)?; - assert_move( - &["abc┃", "d"], - &["ab┃❮c❯", "d"], - &shift_pressed(), - move_caret_left, - )?; - assert_move( - &["abc", "┃d"], - &["abc┃❮", "❯d"], - &shift_pressed(), - move_caret_left, - )?; - assert_move( - &["abc", "┃"], - &["abc┃❮", "❯"], - &shift_pressed(), - move_caret_left, - )?; - assert_move( - &["abc", " ┃def"], - &["abc", "┃❮ ❯def"], - &shift_pressed(), - move_caret_left, - )?; - assert_move( - &["abc", "d┃ef"], - &["abc", "┃❮d❯ef"], - &shift_pressed(), - move_caret_left, - )?; - assert_move( - &["abc", "de┃f "], - &["abc", "d┃❮e❯f "], - &shift_pressed(), - move_caret_left, - )?; - assert_move( - &["abc", "def", "┃"], - &["abc", "def┃❮", "❯"], - &shift_pressed(), - move_caret_left, - )?; - assert_move( - &["abc", "def", "┃ghi", "jkl"], - &["abc", "def┃❮", "❯ghi", "jkl"], - &shift_pressed(), - move_caret_left, - )?; - assert_move( - &["abc", "def", "g┃hi", "jkl"], - &["abc", "def", "┃❮g❯hi", "jkl"], - &shift_pressed(), - move_caret_left, - )?; - assert_move( - &["abc", "def", "gh┃i", "jkl"], - &["abc", "def", "g┃❮h❯i", "jkl"], - &shift_pressed(), - move_caret_left, - )?; - assert_move( - &["abc", "def", "ghi┃", "jkl"], - &["abc", "def", "gh┃❮i❯", "jkl"], - &shift_pressed(), - move_caret_left, - )?; - - Ok(()) - } - - #[test] - fn start_selection_down() -> Result<(), String> { - let move_caret_down = SelectableLines::move_caret_down; - - assert_move(&["┃"], &["┃"], &shift_pressed(), move_caret_down)?; - assert_move(&["┃a"], &["❮a❯┃"], &shift_pressed(), move_caret_down)?; - assert_move(&["A┃"], &["A┃"], &shift_pressed(), move_caret_down)?; - assert_move(&["a┃bc"], &["a❮bc❯┃"], &shift_pressed(), move_caret_down)?; - assert_move(&["ab┃c"], &["ab❮c❯┃"], &shift_pressed(), move_caret_down)?; - assert_move(&["abc┃"], &["abc┃"], &shift_pressed(), move_caret_down)?; - assert_move(&["abc┃ "], &["abc❮ ❯┃"], &shift_pressed(), move_caret_down)?; - assert_move( - &["abc", "┃def"], - &["abc", "❮def❯┃"], - &shift_pressed(), - move_caret_down, - )?; - assert_move( - &["abc", "d┃ef"], - &["abc", "d❮ef❯┃"], - &shift_pressed(), - move_caret_down, - )?; - assert_move( - &["abc", "de┃f"], - &["abc", "de❮f❯┃"], - &shift_pressed(), - move_caret_down, - )?; - assert_move( - &["abc", "def┃"], - &["abc", "def┃"], - &shift_pressed(), - move_caret_down, - )?; - assert_move( - &["┃abc", "def"], - &["❮abc", "❯┃def"], - &shift_pressed(), - move_caret_down, - )?; - assert_move( - &["a┃bc", "def"], - &["a❮bc", "d❯┃ef"], - &shift_pressed(), - move_caret_down, - )?; - assert_move( - &["ab┃c", "def"], - &["ab❮c", "de❯┃f"], - &shift_pressed(), - move_caret_down, - )?; - assert_move( - &["abc┃", "def"], - &["abc❮", "def❯┃"], - &shift_pressed(), - move_caret_down, - )?; - assert_move( - &["abc", "┃def ", "ghi"], - &["abc", "❮def ", "❯┃ghi"], - &shift_pressed(), - move_caret_down, - )?; - assert_move( - &["abc", "d┃ef ", "ghi"], - &["abc", "d❮ef ", "g❯┃hi"], - &shift_pressed(), - move_caret_down, - )?; - assert_move( - &["abc", "de┃f ", "ghi"], - &["abc", "de❮f ", "gh❯┃i"], - &shift_pressed(), - move_caret_down, - )?; - assert_move( - &["abc", "def┃ ", "ghi"], - &["abc", "def❮ ", "ghi❯┃"], - &shift_pressed(), - move_caret_down, - )?; - assert_move( - &["abc", "def ┃", "ghi"], - &["abc", "def ❮", "ghi❯┃"], - &shift_pressed(), - move_caret_down, - )?; - assert_move( - &["abc", "de┃", "ghi"], - &["abc", "de❮", "gh❯┃i"], - &shift_pressed(), - move_caret_down, - )?; - assert_move( - &["abc┃", "de"], - &["abc❮", "de❯┃"], - &shift_pressed(), - move_caret_down, - )?; - assert_move( - &["ab┃c", "de"], - &["ab❮c", "de❯┃"], - &shift_pressed(), - move_caret_down, - )?; - assert_move( - &["a┃bc", "de"], - &["a❮bc", "d❯┃e"], - &shift_pressed(), - move_caret_down, - )?; - assert_move( - &["┃abc", "de"], - &["❮abc", "❯┃de"], - &shift_pressed(), - move_caret_down, - )?; - assert_move( - &["ab┃", "cdef", "ghijkl", "mnopqrst"], - &["ab❮", "cd❯┃ef", "ghijkl", "mnopqrst"], - &shift_pressed(), - move_caret_down, - )?; - assert_move( - &["ab", "cdef┃", "ghijkl", "mnopqrst"], - &["ab", "cdef❮", "ghij❯┃kl", "mnopqrst"], - &shift_pressed(), - move_caret_down, - )?; - assert_move( - &["ab", "cdef", "ghijkl┃", "mnopqrst"], - &["ab", "cdef", "ghijkl❮", "mnopqr❯┃st"], - &shift_pressed(), - move_caret_down, - )?; - assert_move( - &[" ┃ab", " cdef", "ghijkl", "mnopqrst"], - &[" ❮ab", " ❯┃cdef", "ghijkl", "mnopqrst"], - &shift_pressed(), - move_caret_down, - )?; - assert_move( - &["ab", "┃cdef", "ghijkl", "mnopqrst"], - &["ab", "❮cdef", "❯┃ghijkl", "mnopqrst"], - &shift_pressed(), - move_caret_down, - )?; - assert_move( - &["ab", "cdef", "┃ghijkl", "mnopqrst"], - &["ab", "cdef", "❮ghijkl", "❯┃mnopqrst"], - &shift_pressed(), - move_caret_down, - )?; - assert_move( - &["abcdefgh┃", "ijklmn", "opqr", "st"], - &["abcdefgh❮", "ijklmn❯┃", "opqr", "st"], - &shift_pressed(), - move_caret_down, - )?; - assert_move( - &["abcdefgh", "ijklmn┃", "opqr", "st"], - &["abcdefgh", "ijklmn❮", "opqr❯┃", "st"], - &shift_pressed(), - move_caret_down, - )?; - assert_move( - &["abcdefgh", "ijklmn", "opqr┃", "st"], - &["abcdefgh", "ijklmn", "opqr❮", "st❯┃"], - &shift_pressed(), - move_caret_down, - )?; - assert_move( - &["abcdefgh", "ijklmn", "opqr", "┃st"], - &["abcdefgh", "ijklmn", "opqr", "❮st❯┃"], - &shift_pressed(), - move_caret_down, - )?; - assert_move( - &["abc def gh ┃"], - &["abc def gh ┃"], - &shift_pressed(), - move_caret_down, - )?; - assert_move( - &["abc de┃f gh "], - &["abc de❮f gh ❯┃"], - &shift_pressed(), - move_caret_down, - )?; - assert_move( - &["ab┃c def gh "], - &["ab❮c def gh ❯┃"], - &shift_pressed(), - move_caret_down, - )?; - assert_move( - &["a┃bc def gh "], - &["a❮bc def gh ❯┃"], - &shift_pressed(), - move_caret_down, - )?; - assert_move( - &["┃abc def gh "], - &["❮abc def gh ❯┃"], - &shift_pressed(), - move_caret_down, - )?; - - Ok(()) - } - - #[test] - fn start_selection_up() -> Result<(), String> { - let move_caret_up = SelectableLines::move_caret_up; - - assert_move(&["┃"], &["┃"], &shift_pressed(), move_caret_up)?; - assert_move(&["┃a"], &["┃a"], &shift_pressed(), move_caret_up)?; - assert_move(&["A┃"], &["┃❮A❯"], &shift_pressed(), move_caret_up)?; - assert_move(&["a┃bc"], &["┃❮a❯bc"], &shift_pressed(), move_caret_up)?; - assert_move(&["ab┃c"], &["┃❮ab❯c"], &shift_pressed(), move_caret_up)?; - assert_move(&["abc┃"], &["┃❮abc❯"], &shift_pressed(), move_caret_up)?; - assert_move( - &["┃abc", "def"], - &["┃abc", "def"], - &shift_pressed(), - move_caret_up, - )?; - assert_move( - &["abc", "┃def"], - &["┃❮abc", "❯def"], - &shift_pressed(), - move_caret_up, - )?; - assert_move( - &["abc", "d┃ef"], - &["a┃❮bc", "d❯ef"], - &shift_pressed(), - move_caret_up, - )?; - assert_move( - &["abc", "de┃f"], - &["ab┃❮c", "de❯f"], - &shift_pressed(), - move_caret_up, - )?; - assert_move( - &["abc", "def┃"], - &["abc┃❮", "def❯"], - &shift_pressed(), - move_caret_up, - )?; - assert_move( - &["abc", "def ", "┃ghi"], - &["abc", "┃❮def ", "❯ghi"], - &shift_pressed(), - move_caret_up, - )?; - assert_move( - &["abc", "def ", "g┃hi"], - &["abc", "d┃❮ef ", "g❯hi"], - &shift_pressed(), - move_caret_up, - )?; - assert_move( - &["abc", "def ", "gh┃i"], - &["abc", "de┃❮f ", "gh❯i"], - &shift_pressed(), - move_caret_up, - )?; - assert_move( - &["abc", "def ", "ghi┃"], - &["abc", "def┃❮ ", "ghi❯"], - &shift_pressed(), - move_caret_up, - )?; - assert_move( - &["abc", "de", "ghi┃"], - &["abc", "de┃❮", "ghi❯"], - &shift_pressed(), - move_caret_up, - )?; - assert_move( - &["abc", "de┃"], - &["ab┃❮c", "de❯"], - &shift_pressed(), - move_caret_up, - )?; - assert_move( - &["abc", "d┃e"], - &["a┃❮bc", "d❯e"], - &shift_pressed(), - move_caret_up, - )?; - assert_move( - &["abc", "┃de"], - &["┃❮abc", "❯de"], - &shift_pressed(), - move_caret_up, - )?; - assert_move( - &["ab", "cdef", "ghijkl", "mnopqrst┃"], - &["ab", "cdef", "ghijkl┃❮", "mnopqrst❯"], - &shift_pressed(), - move_caret_up, - )?; - assert_move( - &["ab", "cdef", "ghijkl┃", "mnopqrst"], - &["ab", "cdef┃❮", "ghijkl❯", "mnopqrst"], - &shift_pressed(), - move_caret_up, - )?; - assert_move( - &["ab", "cdef", "ghijkl", "┃mnopqrst"], - &["ab", "cdef", "┃❮ghijkl", "❯mnopqrst"], - &shift_pressed(), - move_caret_up, - )?; - assert_move( - &[" ab", " ┃cdef", "ghijkl", "mnopqrst"], - &[" ┃❮ab", " ❯cdef", "ghijkl", "mnopqrst"], - &shift_pressed(), - move_caret_up, - )?; - assert_move( - &["ab", "cdef", "ghijkl", "mnopqr┃st"], - &["ab", "cdef", "ghijkl┃❮", "mnopqr❯st"], - &shift_pressed(), - move_caret_up, - )?; - assert_move( - &["ab", "cde┃f", "ghijkl", "mnopqrst"], - &["ab┃❮", "cde❯f", "ghijkl", "mnopqrst"], - &shift_pressed(), - move_caret_up, - )?; - assert_move( - &["abcdefgh", "ijklmn", "opqr", "st┃"], - &["abcdefgh", "ijklmn", "op┃❮qr", "st❯"], - &shift_pressed(), - move_caret_up, - )?; - assert_move( - &["abcdefgh", "ijklmn", "opqr┃", "st"], - &["abcdefgh", "ijkl┃❮mn", "opqr❯", "st"], - &shift_pressed(), - move_caret_up, - )?; - assert_move( - &["abcdefgh", "ijklmn┃", "opqr", "st"], - &["abcdef┃❮gh", "ijklmn❯", "opqr", "st"], - &shift_pressed(), - move_caret_up, - )?; - assert_move( - &["abcdefgh┃", "ijklmn", "opqr", "st"], - &["┃❮abcdefgh❯", "ijklmn", "opqr", "st"], - &shift_pressed(), - move_caret_up, - )?; - assert_move( - &["abcdefg┃h", "ijklmn", "opqr", "st"], - &["┃❮abcdefg❯h", "ijklmn", "opqr", "st"], - &shift_pressed(), - move_caret_up, - )?; - assert_move( - &["a┃bcdefgh", "ijklmn", "opqr", "st"], - &["┃❮a❯bcdefgh", "ijklmn", "opqr", "st"], - &shift_pressed(), - move_caret_up, - )?; - assert_move( - &["┃abcdefgh", "ijklmn", "opqr", "st"], - &["┃abcdefgh", "ijklmn", "opqr", "st"], - &shift_pressed(), - move_caret_up, - )?; - assert_move( - &["abc def gh ┃"], - &["┃❮abc def gh ❯"], - &shift_pressed(), - move_caret_up, - )?; - assert_move( - &["abc de┃f gh "], - &["┃❮abc de❯f gh "], - &shift_pressed(), - move_caret_up, - )?; - assert_move( - &["ab┃c def gh "], - &["┃❮ab❯c def gh "], - &shift_pressed(), - move_caret_up, - )?; - assert_move( - &["a┃bc def gh "], - &["┃❮a❯bc def gh "], - &shift_pressed(), - move_caret_up, - )?; - - Ok(()) - } - - #[test] - fn start_selection_home() -> Result<(), String> { - let move_caret_home = BigTextArea::move_caret_home; - assert_move(&["┃"], &["┃"], &shift_pressed(), move_caret_home)?; - assert_move(&["┃a"], &["┃a"], &shift_pressed(), move_caret_home)?; - assert_move(&["a┃"], &["┃❮a❯"], &shift_pressed(), move_caret_home)?; - assert_move(&[" a┃"], &[" ┃❮a❯"], &shift_pressed(), move_caret_home)?; - assert_move(&[" ┃a"], &["┃❮ ❯a"], &shift_pressed(), move_caret_home)?; - assert_move(&["\ta┃"], &["\t┃❮a❯"], &shift_pressed(), move_caret_home)?; - assert_move(&["abc┃"], &["┃❮abc❯"], &shift_pressed(), move_caret_home)?; - assert_move(&[" abc┃"], &[" ┃❮abc❯"], &shift_pressed(), move_caret_home)?; - assert_move( - &["\tabc┃"], - &["\t┃❮abc❯"], - &shift_pressed(), - move_caret_home, - )?; - assert_move(&["ab┃c"], &["┃❮ab❯c"], &shift_pressed(), move_caret_home)?; - assert_move(&[" ab┃c"], &[" ┃❮ab❯c"], &shift_pressed(), move_caret_home)?; - assert_move( - &["\tab┃c"], - &["\t┃❮ab❯c"], - &shift_pressed(), - move_caret_home, - )?; - assert_move( - &[" abc def ghi┃"], - &[" ┃❮abc def ghi❯"], - &shift_pressed(), - move_caret_home, - )?; - assert_move( - &["abc def ghi┃"], - &["┃❮abc def ghi❯"], - &shift_pressed(), - move_caret_home, - )?; - assert_move( - &["abc def ┃ghi"], - &["┃❮abc def ❯ghi"], - &shift_pressed(), - move_caret_home, - )?; - - assert_move( - &["abc", "def", "ghi┃"], - &["abc", "def", "┃❮ghi❯"], - &shift_pressed(), - move_caret_home, - )?; - assert_move( - &["abc", "def┃", "ghi"], - &["abc", "┃❮def❯", "ghi"], - &shift_pressed(), - move_caret_home, - )?; - assert_move( - &["abc┃", "def", "ghi"], - &["┃❮abc❯", "def", "ghi"], - &shift_pressed(), - move_caret_home, - )?; - assert_move( - &["┃abc", "def", "ghi"], - &["┃abc", "def", "ghi"], - &shift_pressed(), - move_caret_home, - )?; - assert_move( - &["abc", "┃def", "ghi"], - &["abc", "┃def", "ghi"], - &shift_pressed(), - move_caret_home, - )?; - assert_move( - &["abc", "def", "┃ghi"], - &["abc", "def", "┃ghi"], - &shift_pressed(), - move_caret_home, - )?; - assert_move( - &[" ┃abc", "def", "ghi"], - &["┃❮ ❯abc", "def", "ghi"], - &shift_pressed(), - move_caret_home, - )?; - assert_move( - &["abc", " ┃def", "ghi"], - &["abc", "┃❮ ❯def", "ghi"], - &shift_pressed(), - move_caret_home, - )?; - assert_move( - &["abc", "def", " ┃ghi"], - &["abc", "def", "┃❮ ❯ghi"], - &shift_pressed(), - move_caret_home, - )?; - assert_move( - &["┃ abc", "def", "ghi"], - &["❮ ❯┃abc", "def", "ghi"], - &shift_pressed(), - move_caret_home, - )?; - assert_move( - &["abc", "┃ def", "ghi"], - &["abc", "❮ ❯┃def", "ghi"], - &shift_pressed(), - move_caret_home, - )?; - assert_move( - &["abc", "def", "┃ ghi"], - &["abc", "def", "❮ ❯┃ghi"], - &shift_pressed(), - move_caret_home, - )?; - - Ok(()) - } - - #[test] - fn start_selection_end() -> Result<(), String> { - let move_caret_end = BigTextArea::move_caret_end; - assert_move(&["┃"], &["┃"], &shift_pressed(), move_caret_end)?; - assert_move(&["┃a"], &["❮a❯┃"], &shift_pressed(), move_caret_end)?; - assert_move(&["a┃"], &["a┃"], &shift_pressed(), move_caret_end)?; - assert_move(&[" a ┃"], &[" a ┃"], &shift_pressed(), move_caret_end)?; - assert_move(&["┃ a "], &["❮ a ❯┃"], &shift_pressed(), move_caret_end)?; - assert_move(&["┃abc"], &["❮abc❯┃"], &shift_pressed(), move_caret_end)?; - assert_move(&["┃abc\t"], &["❮abc\t❯┃"], &shift_pressed(), move_caret_end)?; - assert_move(&["┃abc "], &["❮abc ❯┃"], &shift_pressed(), move_caret_end)?; - assert_move(&["┃ abc "], &["❮ abc ❯┃"], &shift_pressed(), move_caret_end)?; - assert_move(&[" abc┃ "], &[" abc❮ ❯┃"], &shift_pressed(), move_caret_end)?; - assert_move(&[" ab┃c"], &[" ab❮c❯┃"], &shift_pressed(), move_caret_end)?; - assert_move( - &["abc", "def", "┃ghi"], - &["abc", "def", "❮ghi❯┃"], - &shift_pressed(), - move_caret_end, - )?; - assert_move( - &["abc", "┃def", "ghi"], - &["abc", "❮def❯┃", "ghi"], - &shift_pressed(), - move_caret_end, - )?; - assert_move( - &["┃abc", "def", "ghi"], - &["❮abc❯┃", "def", "ghi"], - &shift_pressed(), - move_caret_end, - )?; - assert_move( - &["abc", "def", "┃ghi "], - &["abc", "def", "❮ghi ❯┃"], - &shift_pressed(), - move_caret_end, - )?; - assert_move( - &["abc", "┃def ", "ghi"], - &["abc", "❮def ❯┃", "ghi"], - &shift_pressed(), - move_caret_end, - )?; - assert_move( - &["┃abc ", "def", "ghi"], - &["❮abc ❯┃", "def", "ghi"], - &shift_pressed(), - move_caret_end, - )?; - - Ok(()) - } - - #[test] - fn end_selection_right() -> Result<(), String> { - let move_caret_right = SelectableLines::move_caret_right; - - assert_move(&["❮A❯┃"], &["A┃"], &no_mods(), move_caret_right)?; - assert_move(&["❮a❯┃bc"], &["a┃bc"], &no_mods(), move_caret_right)?; - assert_move(&["a❮b❯┃c"], &["ab┃c"], &no_mods(), move_caret_right)?; - assert_move(&["ab❮c❯┃"], &["abc┃"], &no_mods(), move_caret_right)?; - assert_move(&["❮ ❯┃abc"], &[" ┃abc"], &no_mods(), move_caret_right)?; - assert_move(&["┃❮ ❯abc"], &[" ┃abc"], &no_mods(), move_caret_right)?; - assert_move(&["a┃❮b❯c"], &["ab┃c"], &no_mods(), move_caret_right)?; - assert_move( - &["abc❮", "❯┃d"], - &["abc", "┃d"], - &no_mods(), - move_caret_right, - )?; - assert_move( - &["abc┃❮", "❯d"], - &["abc", "┃d"], - &no_mods(), - move_caret_right, - )?; - assert_move(&["abc┃❮", "❯"], &["abc", "┃"], &no_mods(), move_caret_right)?; - assert_move( - &["abc", "❮d❯┃ef"], - &["abc", "d┃ef"], - &no_mods(), - move_caret_right, - )?; - assert_move( - &["abc", "def", "ghi❮", "❯┃jkl"], - &["abc", "def", "ghi", "┃jkl"], - &no_mods(), - move_caret_right, - )?; - assert_move(&["❮ab❯┃c"], &["ab┃c"], &no_mods(), move_caret_right)?; - assert_move(&["❮abc❯┃"], &["abc┃"], &no_mods(), move_caret_right)?; - assert_move( - &["ab┃❮c", "❯def", "ghi"], - &["abc", "┃def", "ghi"], - &no_mods(), - move_caret_right, - )?; - assert_move( - &["ab❮c", "❯┃def", "ghi"], - &["abc", "┃def", "ghi"], - &no_mods(), - move_caret_right, - )?; - assert_move( - &["a┃❮bc", "❯def", "ghi"], - &["abc", "┃def", "ghi"], - &no_mods(), - move_caret_right, - )?; - assert_move( - &["┃❮abc", "❯def", "ghi"], - &["abc", "┃def", "ghi"], - &no_mods(), - move_caret_right, - )?; - assert_move( - &["a┃❮bc", "d❯ef", "ghi"], - &["abc", "d┃ef", "ghi"], - &no_mods(), - move_caret_right, - )?; - assert_move( - &["┃❮abc", "def❯", "ghi"], - &["abc", "def┃", "ghi"], - &no_mods(), - move_caret_right, - )?; - assert_move( - &["❮ab", "cdef", "ghijkl", "mnopqrst❯┃"], - &["ab", "cdef", "ghijkl", "mnopqrst┃"], - &no_mods(), - move_caret_right, - )?; - assert_move( - &["┃❮ab", "cdef", "ghijkl", "mnopqrst❯"], - &["ab", "cdef", "ghijkl", "mnopqrst┃"], - &no_mods(), - move_caret_right, - )?; - assert_move( - &["ab", "c❮def", "ghijkl", "mno❯┃pqrst"], - &["ab", "cdef", "ghijkl", "mno┃pqrst"], - &no_mods(), - move_caret_right, - )?; - assert_move( - &["ab", "c┃❮def", "ghijkl", "mno❯pqrst"], - &["ab", "cdef", "ghijkl", "mno┃pqrst"], - &no_mods(), - move_caret_right, - )?; - - Ok(()) - } - - #[test] - fn end_selection_left() -> Result<(), String> { - let move_caret_left = SelectableLines::move_caret_left; - - assert_move(&["❮A❯┃"], &["┃A"], &no_mods(), move_caret_left)?; - /*assert_move(&["❮a❯┃bc"], &["┃abc"], &no_mods(), move_caret_left)?; - assert_move(&["a❮b❯┃c"], &["a┃bc"], &no_mods(), move_caret_left)?; - assert_move(&["ab❮c❯┃"], &["ab┃c"], &no_mods(), move_caret_left)?; - assert_move(&["❮ ❯┃abc"], &["┃ abc"], &no_mods(), move_caret_left)?; - assert_move(&["┃❮ ❯abc"], &["┃ abc"], &no_mods(), move_caret_left)?; - assert_move(&["a┃❮b❯c"], &["a┃bc"], &no_mods(), move_caret_left)?; - assert_move( - &["abc❮", "❯┃d"], - &["abc┃", "d"], - &no_mods(), - move_caret_left, - )?; - assert_move( - &["abc┃❮", "❯d"], - &["abc┃", "d"], - &no_mods(), - move_caret_left, - )?; - assert_move( - &["abc┃❮", "❯"], - &["abc┃", ""], - &no_mods(), - move_caret_left, - )?; - assert_move( - &["abc", "❮d❯┃ef"], - &["abc", "┃def"], - &no_mods(), - move_caret_left, - )?; - assert_move( - &["abc", "def", "ghi❮", "❯┃jkl"], - &["abc", "def", "ghi┃", "jkl"], - &no_mods(), - move_caret_left, - )?; - assert_move(&["❮ab❯┃c"], &["┃abc"], &no_mods(), move_caret_left)?; - assert_move(&["❮abc❯┃"], &["┃abc"], &no_mods(), move_caret_left)?; - assert_move( - &["ab┃❮c", "❯def", "ghi"], - &["ab┃c", "def", "ghi"], - &no_mods(), - move_caret_left, - )?; - assert_move( - &["ab❮c", "❯┃def", "ghi"], - &["ab┃c", "def", "ghi"], - &no_mods(), - move_caret_left, - )?; - assert_move( - &["a┃❮bc", "❯def", "ghi"], - &["a┃bc", "def", "ghi"], - &no_mods(), - move_caret_left, - )?; - assert_move( - &["┃❮abc", "❯def", "ghi"], - &["┃abc", "def", "ghi"], - &no_mods(), - move_caret_left, - )?; - assert_move( - &["a┃❮bc", "d❯ef", "ghi"], - &["a┃bc", "def", "ghi"], - &no_mods(), - move_caret_left, - )?; - assert_move( - &["┃❮abc", "def❯", "ghi"], - &["┃abc", "def", "ghi"], - &no_mods(), - move_caret_left, - )?; - assert_move( - &["❮ab", "cdef", "ghijkl", "mnopqrst❯┃"], - &["┃ab", "cdef", "ghijkl", "mnopqrst"], - &no_mods(), - move_caret_left, - )?; - assert_move( - &["┃❮ab", "cdef", "ghijkl", "mnopqrst❯"], - &["┃ab", "cdef", "ghijkl", "mnopqrst"], - &no_mods(), - move_caret_left, - )?; - assert_move( - &["ab", "c❮def", "ghijkl", "mno❯┃pqrst"], - &["ab", "c┃def", "ghijkl", "mnopqrst"], - &no_mods(), - move_caret_left, - )?; - assert_move( - &["ab", "c┃❮def", "ghijkl", "mno❯pqrst"], - &["ab", "c┃def", "ghijkl", "mnopqrst"], - &no_mods(), - move_caret_left, - )?;*/ - - Ok(()) - } - - #[test] - fn end_selection_down() -> Result<(), String> { - let move_caret_down = SelectableLines::move_caret_down; - - assert_move(&["❮a❯┃"], &["a┃"], &no_mods(), move_caret_down)?; - assert_move(&["┃❮a❯"], &["a┃"], &no_mods(), move_caret_down)?; - assert_move(&["a┃❮bc❯"], &["abc┃"], &no_mods(), move_caret_down)?; - assert_move(&["ab❮c❯┃"], &["abc┃"], &no_mods(), move_caret_down)?; - assert_move(&["abc┃❮ ❯"], &["abc ┃"], &no_mods(), move_caret_down)?; - assert_move( - &["abc", "┃❮def❯"], - &["abc", "def┃"], - &no_mods(), - move_caret_down, - )?; - assert_move( - &["abc", "d┃❮ef❯"], - &["abc", "def┃"], - &no_mods(), - move_caret_down, - )?; - assert_move( - &["abc", "de┃❮f❯"], - &["abc", "def┃"], - &no_mods(), - move_caret_down, - )?; - assert_move( - &["❮abc", "❯┃def"], - &["abc", "┃def"], - &no_mods(), - move_caret_down, - )?; - assert_move( - &["a❮bc", "d❯┃ef"], - &["abc", "d┃ef"], - &no_mods(), - move_caret_down, - )?; - assert_move( - &["ab┃❮c", "de❯f"], - &["abc", "de┃f"], - &no_mods(), - move_caret_down, - )?; - assert_move( - &["abc❮", "def❯┃"], - &["abc", "def┃"], - &no_mods(), - move_caret_down, - )?; - assert_move( - &["abc", "┃❮def ", "❯ghi"], - &["abc", "def ", "┃ghi"], - &no_mods(), - move_caret_down, - )?; - assert_move( - &["abc", "d❮ef ", "g❯┃hi"], - &["abc", "def ", "g┃hi"], - &no_mods(), - move_caret_down, - )?; - assert_move( - &["abc", "de❮f ", "gh❯┃i"], - &["abc", "def ", "gh┃i"], - &no_mods(), - move_caret_down, - )?; - assert_move( - &["abc", "def❮ ", "ghi❯┃"], - &["abc", "def ", "ghi┃"], - &no_mods(), - move_caret_down, - )?; - assert_move( - &["abc", "def ❮", "ghi❯┃"], - &["abc", "def ", "ghi┃"], - &no_mods(), - move_caret_down, - )?; - assert_move( - &["abc", "de❮", "gh❯┃i"], - &["abc", "de", "gh┃i"], - &no_mods(), - move_caret_down, - )?; - assert_move( - &["abc┃❮", "de❯"], - &["abc", "de┃"], - &no_mods(), - move_caret_down, - )?; - assert_move( - &["ab❮c", "de❯┃"], - &["abc", "de┃"], - &no_mods(), - move_caret_down, - )?; - assert_move( - &["a┃❮bc", "d❯e"], - &["abc", "d┃e"], - &no_mods(), - move_caret_down, - )?; - assert_move( - &["❮abc", "❯┃de"], - &["abc", "┃de"], - &no_mods(), - move_caret_down, - )?; - assert_move( - &["ab❮", "cd❯┃ef", "ghijkl", "mnopqrst"], - &["ab", "cd┃ef", "ghijkl", "mnopqrst"], - &no_mods(), - move_caret_down, - )?; - assert_move( - &["ab", "cdef┃❮", "ghij❯kl", "mnopqrst"], - &["ab", "cdef", "ghij┃kl", "mnopqrst"], - &no_mods(), - move_caret_down, - )?; - assert_move( - &["ab", "cdef", "ghijkl❮", "mnopqr❯┃st"], - &["ab", "cdef", "ghijkl", "mnopqr┃st"], - &no_mods(), - move_caret_down, - )?; - assert_move( - &[" ❮ab", " ❯┃cdef", "ghijkl", "mnopqrst"], - &[" ab", " ┃cdef", "ghijkl", "mnopqrst"], - &no_mods(), - move_caret_down, - )?; - assert_move( - &["ab", "┃❮cdef", "❯ghijkl", "mnopqrst"], - &["ab", "cdef", "┃ghijkl", "mnopqrst"], - &no_mods(), - move_caret_down, - )?; - assert_move( - &["ab", "cdef", "❮ghijkl", "❯┃mnopqrst"], - &["ab", "cdef", "ghijkl", "┃mnopqrst"], - &no_mods(), - move_caret_down, - )?; - assert_move( - &["abcdefgh❮", "ijklmn❯┃", "opqr", "st"], - &["abcdefgh", "ijklmn┃", "opqr", "st"], - &no_mods(), - move_caret_down, - )?; - assert_move( - &["abcdefgh", "ijklmn❮", "opqr❯┃", "st"], - &["abcdefgh", "ijklmn", "opqr┃", "st"], - &no_mods(), - move_caret_down, - )?; - assert_move( - &["abcdefgh", "ijklmn", "opqr❮", "st❯┃"], - &["abcdefgh", "ijklmn", "opqr", "st┃"], - &no_mods(), - move_caret_down, - )?; - assert_move( - &["abcdefgh", "ijklmn", "opqr", "❮st❯┃"], - &["abcdefgh", "ijklmn", "opqr", "st┃"], - &no_mods(), - move_caret_down, - )?; - assert_move( - &["abc de❮f gh ❯┃"], - &["abc def gh ┃"], - &no_mods(), - move_caret_down, - )?; - assert_move( - &["ab┃❮c def gh ❯"], - &["abc def gh ┃"], - &no_mods(), - move_caret_down, - )?; - assert_move( - &["a❮bc def gh ❯┃"], - &["abc def gh ┃"], - &no_mods(), - move_caret_down, - )?; - assert_move( - &["❮abc def gh ❯┃"], - &["abc def gh ┃"], - &no_mods(), - move_caret_down, - )?; - - Ok(()) - } - - #[test] - fn end_selection_up() -> Result<(), String> { - let move_caret_up = SelectableLines::move_caret_up; - - assert_move(&["❮a❯┃"], &["┃a"], &no_mods(), move_caret_up)?; - assert_move(&["┃❮a❯"], &["┃a"], &no_mods(), move_caret_up)?; - assert_move(&["a┃❮bc❯"], &["a┃bc"], &no_mods(), move_caret_up)?; - assert_move(&["ab❮c❯┃"], &["ab┃c"], &no_mods(), move_caret_up)?; - assert_move(&["abc┃❮ ❯"], &["abc┃ "], &no_mods(), move_caret_up)?; - assert_move( - &["abc", "┃❮def❯"], - &["abc", "┃def"], - &no_mods(), - move_caret_up, - )?; - assert_move( - &["abc", "d┃❮ef❯"], - &["abc", "d┃ef"], - &no_mods(), - move_caret_up, - )?; - assert_move( - &["abc", "de┃❮f❯"], - &["abc", "de┃f"], - &no_mods(), - move_caret_up, - )?; - assert_move( - &["❮abc", "❯┃def"], - &["┃abc", "def"], - &no_mods(), - move_caret_up, - )?; - assert_move( - &["a❮bc", "d❯┃ef"], - &["a┃bc", "def"], - &no_mods(), - move_caret_up, - )?; - assert_move( - &["ab┃❮c", "de❯f"], - &["ab┃c", "def"], - &no_mods(), - move_caret_up, - )?; - assert_move( - &["abc❮", "def❯┃"], - &["abc┃", "def"], - &no_mods(), - move_caret_up, - )?; - assert_move( - &["abc", "┃❮def ", "❯ghi"], - &["abc", "┃def ", "ghi"], - &no_mods(), - move_caret_up, - )?; - assert_move( - &["abc", "d❮ef ", "g❯┃hi"], - &["abc", "d┃ef ", "ghi"], - &no_mods(), - move_caret_up, - )?; - assert_move( - &["abc", "de┃❮f ", "gh❯i"], - &["abc", "de┃f ", "ghi"], - &no_mods(), - move_caret_up, - )?; - assert_move( - &["abc", "def❮ ", "ghi❯┃"], - &["abc", "def┃ ", "ghi"], - &no_mods(), - move_caret_up, - )?; - assert_move( - &["abc", "def ❮", "ghi❯┃"], - &["abc", "def ┃", "ghi"], - &no_mods(), - move_caret_up, - )?; - assert_move( - &["abc", "de❮", "gh❯┃i"], - &["abc", "de┃", "ghi"], - &no_mods(), - move_caret_up, - )?; - assert_move( - &["abc┃❮", "de❯"], - &["abc┃", "de"], - &no_mods(), - move_caret_up, - )?; - assert_move( - &["ab❮c", "de❯┃"], - &["ab┃c", "de"], - &no_mods(), - move_caret_up, - )?; - assert_move( - &["a┃❮bc", "d❯e"], - &["a┃bc", "de"], - &no_mods(), - move_caret_up, - )?; - assert_move( - &["❮abc", "❯┃de"], - &["┃abc", "de"], - &no_mods(), - move_caret_up, - )?; - assert_move( - &["ab❮", "cd❯┃ef", "ghijkl", "mnopqrst"], - &["ab┃", "cdef", "ghijkl", "mnopqrst"], - &no_mods(), - move_caret_up, - )?; - assert_move( - &["ab", "cdef┃❮", "ghij❯kl", "mnopqrst"], - &["ab", "cdef┃", "ghijkl", "mnopqrst"], - &no_mods(), - move_caret_up, - )?; - assert_move( - &["ab", "cdef", "ghijkl❮", "mnopqr❯┃st"], - &["ab", "cdef", "ghijkl┃", "mnopqrst"], - &no_mods(), - move_caret_up, - )?; - assert_move( - &[" ❮ab", " ❯┃cdef", "ghijkl", "mnopqrst"], - &[" ┃ab", " cdef", "ghijkl", "mnopqrst"], - &no_mods(), - move_caret_up, - )?; - assert_move( - &["ab", "┃❮cdef", "❯ghijkl", "mnopqrst"], - &["ab", "┃cdef", "ghijkl", "mnopqrst"], - &no_mods(), - move_caret_up, - )?; - assert_move( - &["ab", "cdef", "❮ghijkl", "❯┃mnopqrst"], - &["ab", "cdef", "┃ghijkl", "mnopqrst"], - &no_mods(), - move_caret_up, - )?; - assert_move( - &["abcdefgh❮", "ijklmn❯┃", "opqr", "st"], - &["abcdefgh┃", "ijklmn", "opqr", "st"], - &no_mods(), - move_caret_up, - )?; - assert_move( - &["abcdefgh", "ijklmn❮", "opqr❯┃", "st"], - &["abcdefgh", "ijklmn┃", "opqr", "st"], - &no_mods(), - move_caret_up, - )?; - assert_move( - &["abcdefgh", "ijklmn", "opqr❮", "st❯┃"], - &["abcdefgh", "ijklmn", "opqr┃", "st"], - &no_mods(), - move_caret_up, - )?; - assert_move( - &["abcdefgh", "ijklmn", "opqr", "❮st❯┃"], - &["abcdefgh", "ijklmn", "opqr", "┃st"], - &no_mods(), - move_caret_up, - )?; - assert_move( - &["abc de❮f gh ❯┃"], - &["abc de┃f gh "], - &no_mods(), - move_caret_up, - )?; - assert_move( - &["ab┃❮c def gh ❯"], - &["ab┃c def gh "], - &no_mods(), - move_caret_up, - )?; - assert_move( - &["a❮bc def gh ❯┃"], - &["a┃bc def gh "], - &no_mods(), - move_caret_up, - )?; - assert_move( - &["❮abc def gh ❯┃"], - &["┃abc def gh "], - &no_mods(), - move_caret_up, - )?; - - Ok(()) - } - - #[test] - fn end_selection_home() -> Result<(), String> { - let move_caret_home = BigTextArea::move_caret_home; - assert_move(&["❮a❯┃"], &["┃a"], &no_mods(), move_caret_home)?; - assert_move(&["┃❮a❯"], &["┃a"], &no_mods(), move_caret_home)?; - assert_move(&[" ┃❮a❯"], &["┃ a"], &no_mods(), move_caret_home)?; - assert_move(&["┃❮ a❯"], &[" ┃a"], &no_mods(), move_caret_home)?; - assert_move(&[" ❮a❯┃"], &[" ┃a"], &no_mods(), move_caret_home)?; - assert_move(&[" a❮bc ❯┃"], &[" ┃abc "], &no_mods(), move_caret_home)?; - assert_move(&["\t❮abc ❯┃"], &["\t┃abc "], &no_mods(), move_caret_home)?; - assert_move(&["\t┃❮abc❯ "], &["┃\tabc "], &no_mods(), move_caret_home)?; - assert_move(&["┃❮\tabc❯ "], &["\t┃abc "], &no_mods(), move_caret_home)?; - assert_move( - &["❮ abc def\tghi❯┃"], - &[" ┃abc def\tghi"], - &no_mods(), - move_caret_home, - )?; - assert_move( - &[" ┃❮abc❯ def\tghi"], - &["┃ abc def\tghi"], - &no_mods(), - move_caret_home, - )?; - assert_move( - &["┃❮ abc def\tghi❯"], - &[" ┃abc def\tghi"], - &no_mods(), - move_caret_home, - )?; - - assert_move( - &["abc", "d❮e❯┃", "ghi"], - &["abc", "┃de", "ghi"], - &no_mods(), - move_caret_home, - )?; - assert_move( - &["abc", " ❮d❯┃e", "ghi"], - &["abc", " ┃de", "ghi"], - &no_mods(), - move_caret_home, - )?; - assert_move( - &["❮abc", "❯┃ de", "ghi"], - &["abc", " ┃de", "ghi"], - &no_mods(), - move_caret_home, - )?; - assert_move( - &["abc", " ┃❮de❯", "ghi"], - &["abc", "┃ de", "ghi"], - &no_mods(), - move_caret_home, - )?; - assert_move( - &["abc┃❮", "de", "ghi❯"], - &["┃abc", "de", "ghi"], - &no_mods(), - move_caret_home, - )?; - - Ok(()) - } - - #[test] - fn end_selection_end() -> Result<(), String> { - let move_caret_end = BigTextArea::move_caret_end; - assert_move(&["┃❮a❯"], &["a┃"], &no_mods(), move_caret_end)?; - assert_move(&["❮a❯┃"], &["a┃"], &no_mods(), move_caret_end)?; - assert_move(&[" a┃❮ ❯"], &[" a ┃"], &no_mods(), move_caret_end)?; - assert_move(&["❮ a❯┃ "], &[" a ┃"], &no_mods(), move_caret_end)?; - assert_move(&[" ❮a❯┃ "], &[" a ┃"], &no_mods(), move_caret_end)?; - assert_move(&["❮ a ❯┃"], &[" a ┃"], &no_mods(), move_caret_end)?; - assert_move(&["┃❮ a❯bc "], &[" abc ┃"], &no_mods(), move_caret_end)?; - assert_move(&["┃❮\tabc❯ "], &["\tabc ┃"], &no_mods(), move_caret_end)?; - assert_move( - &[" abc d┃❮ef\tg❯hi"], - &[" abc def\tghi┃"], - &no_mods(), - move_caret_end, - )?; - - assert_move( - &["abc", "┃❮de", "ghi❯"], - &["abc", "de┃", "ghi"], - &no_mods(), - move_caret_end, - )?; - assert_move( - &["❮abc", " d❯┃e", "ghi"], - &["abc", " de┃", "ghi"], - &no_mods(), - move_caret_end, - )?; - assert_move( - &["┃❮abc", "de", "ghi❯"], - &["abc┃", "de", "ghi"], - &no_mods(), - move_caret_end, - )?; - assert_move( - &["abc", "de", "g┃❮hi❯"], - &["abc", "de", "ghi┃"], - &no_mods(), - move_caret_end, - )?; - - Ok(()) - } - - #[test] - fn extend_selection_right() -> Result<(), String> { - let move_caret_right = SelectableLines::move_caret_right; - - assert_move(&["❮a❯┃bc"], &["❮ab❯┃c"], &shift_pressed(), move_caret_right)?; - assert_move(&["a❮b❯┃c"], &["a❮bc❯┃"], &shift_pressed(), move_caret_right)?; - assert_move(&["❮ab❯┃c"], &["❮abc❯┃"], &shift_pressed(), move_caret_right)?; - assert_move( - &["❮ ❯┃abc"], - &["❮ a❯┃bc"], - &shift_pressed(), - move_caret_right, - )?; - assert_move(&["❮abc❯┃"], &["❮abc❯┃"], &shift_pressed(), move_caret_right)?; - assert_move(&["a❮bc❯┃"], &["a❮bc❯┃"], &shift_pressed(), move_caret_right)?; - assert_move(&["ab❮c❯┃"], &["ab❮c❯┃"], &shift_pressed(), move_caret_right)?; - assert_move( - &["abc❮", "❯┃d"], - &["abc❮", "d❯┃"], - &shift_pressed(), - move_caret_right, - )?; - assert_move( - &["ab❮c❯┃", ""], - &["ab❮c", "❯┃"], - &shift_pressed(), - move_caret_right, - )?; - assert_move( - &["ab❮c❯┃", "d"], - &["ab❮c", "❯┃d"], - &shift_pressed(), - move_caret_right, - )?; - assert_move( - &["abc", "def", "ghi❮", "❯┃jkl"], - &["abc", "def", "ghi❮", "j❯┃kl"], - &shift_pressed(), - move_caret_right, - )?; - assert_move( - &["ab❮c", "def", "ghi", "❯┃jkl"], - &["ab❮c", "def", "ghi", "j❯┃kl"], - &shift_pressed(), - move_caret_right, - )?; - assert_move( - &["ab❮c", "def", "❯┃ghi", "jkl"], - &["ab❮c", "def", "g❯┃hi", "jkl"], - &shift_pressed(), - move_caret_right, - )?; - assert_move( - &["❮abc", "def", "ghi", "jk❯┃l"], - &["❮abc", "def", "ghi", "jkl❯┃"], - &shift_pressed(), - move_caret_right, - )?; - assert_move( - &["❮abc", "def", "ghi", "jkl❯┃"], - &["❮abc", "def", "ghi", "jkl❯┃"], - &shift_pressed(), - move_caret_right, - )?; - - Ok(()) - } - - #[test] - fn extend_selection_left() -> Result<(), String> { - let move_caret_left = SelectableLines::move_caret_left; - - assert_move(&["ab┃❮c❯"], &["a┃❮bc❯"], &shift_pressed(), move_caret_left)?; - assert_move(&["a┃❮bc❯"], &["┃❮abc❯"], &shift_pressed(), move_caret_left)?; - assert_move(&["┃❮abc❯"], &["┃❮abc❯"], &shift_pressed(), move_caret_left)?; - assert_move(&["┃❮ab❯c"], &["┃❮ab❯c"], &shift_pressed(), move_caret_left)?; - assert_move(&["┃❮a❯bc"], &["┃❮a❯bc"], &shift_pressed(), move_caret_left)?; - assert_move( - &[" ┃❮a❯bc"], - &["┃❮ a❯bc"], - &shift_pressed(), - move_caret_left, - )?; - assert_move( - &["abc┃❮", "❯d"], - &["ab┃❮c", "❯d"], - &shift_pressed(), - move_caret_left, - )?; - assert_move( - &["abc", "┃❮d❯"], - &["abc┃❮", "d❯"], - &shift_pressed(), - move_caret_left, - )?; - assert_move( - &["ab┃❮c", "❯"], - &["a┃❮bc", "❯"], - &shift_pressed(), - move_caret_left, - )?; - assert_move( - &["abc", "def┃❮", "ghi", "j❯kl"], - &["abc", "de┃❮f", "ghi", "j❯kl"], - &shift_pressed(), - move_caret_left, - )?; - assert_move( - &["a┃❮bc", "def", "ghi", "jkl❯"], - &["┃❮abc", "def", "ghi", "jkl❯"], - &shift_pressed(), - move_caret_left, - )?; - assert_move( - &["abc", "def", "ghi", "┃❮jkl❯"], - &["abc", "def", "ghi┃❮", "jkl❯"], - &shift_pressed(), - move_caret_left, - )?; - - Ok(()) - } - - #[test] - fn extend_selection_up() -> Result<(), String> { - let move_caret_up = SelectableLines::move_caret_up; - - assert_move(&["ab┃❮c❯"], &["┃❮abc❯"], &shift_pressed(), move_caret_up)?; - assert_move(&["a┃❮bc❯"], &["┃❮abc❯"], &shift_pressed(), move_caret_up)?; - assert_move(&["┃❮abc❯"], &["┃❮abc❯"], &shift_pressed(), move_caret_up)?; - assert_move(&["┃❮ab❯c"], &["┃❮ab❯c"], &shift_pressed(), move_caret_up)?; - assert_move(&["┃❮a❯bc"], &["┃❮a❯bc"], &shift_pressed(), move_caret_up)?; - assert_move(&[" ┃❮a❯bc"], &["┃❮ a❯bc"], &shift_pressed(), move_caret_up)?; - assert_move(&["ab❮c❯┃"], &["┃❮ab❯c"], &shift_pressed(), move_caret_up)?; - assert_move(&["❮a❯┃"], &["┃a"], &shift_pressed(), move_caret_up)?; - assert_move(&["❮a❯┃bc"], &["┃abc"], &shift_pressed(), move_caret_up)?; - assert_move( - &["❮a❯┃bc", "d"], - &["┃abc", "d"], - &shift_pressed(), - move_caret_up, - )?; - assert_move( - &["abc", "de❮f❯┃"], - &["abc┃❮", "de❯f"], - &shift_pressed(), - move_caret_up, - )?; - assert_move( - &["abc", "de┃❮f❯"], - &["ab┃❮c", "def❯"], - &shift_pressed(), - move_caret_up, - )?; - assert_move( - &["ab┃❮c", "def❯"], - &["┃❮abc", "def❯"], - &shift_pressed(), - move_caret_up, - )?; - assert_move( - &["ab", "cdef", "ghijkl", "❮mnopqr❯┃st"], - &["ab", "cdef", "ghijkl┃❮", "❯mnopqrst"], - &shift_pressed(), - move_caret_up, - )?; - assert_move( - &["ab", "cdef", "ghijkl", "❮mnopqrs❯┃t"], - &["ab", "cdef", "ghijkl┃❮", "❯mnopqrst"], - &shift_pressed(), - move_caret_up, - )?; - assert_move( - &["abcdefgh", "ijklmn", "┃❮o❯pqr", "st"], - &["abcdefgh", "┃❮ijklmn", "o❯pqr", "st"], - &shift_pressed(), - move_caret_up, - )?; - - Ok(()) - } - - #[test] - fn extend_selection_down() -> Result<(), String> { - let move_caret_down = SelectableLines::move_caret_down; - - assert_move(&["❮ab❯┃c"], &["❮abc❯┃"], &shift_pressed(), move_caret_down)?; - assert_move(&["❮a❯┃bc"], &["❮abc❯┃"], &shift_pressed(), move_caret_down)?; - assert_move(&["❮abc❯┃"], &["❮abc❯┃"], &shift_pressed(), move_caret_down)?; - assert_move(&["┃❮ab❯c"], &["ab❮c❯┃"], &shift_pressed(), move_caret_down)?; - assert_move(&["┃❮a❯bc"], &["a❮bc❯┃"], &shift_pressed(), move_caret_down)?; - assert_move( - &["❮a❯┃bc", "d"], - &["❮abc", "d❯┃"], - &shift_pressed(), - move_caret_down, - )?; - assert_move( - &["❮a❯┃bc", "de"], - &["❮abc", "d❯┃e"], - &shift_pressed(), - move_caret_down, - )?; - assert_move( - &["❮abc", "d❯┃e"], - &["❮abc", "de❯┃"], - &shift_pressed(), - move_caret_down, - )?; - assert_move( - &["❮a❯┃bc", ""], - &["❮abc", "❯┃"], - &shift_pressed(), - move_caret_down, - )?; - assert_move( - &["ab", "cdef", "ghijkl", "❮mnopqr❯┃st"], - &["ab", "cdef", "ghijkl", "❮mnopqrst❯┃"], - &shift_pressed(), - move_caret_down, - )?; - assert_move( - &["a❮b", "cdef", "ghijkl", "mnopqr❯┃st"], - &["a❮b", "cdef", "ghijkl", "mnopqrst❯┃"], - &shift_pressed(), - move_caret_down, - )?; - assert_move( - &["❮ab", "cdef", "ghijkl", "mnopqrst❯┃"], - &["❮ab", "cdef", "ghijkl", "mnopqrst❯┃"], - &shift_pressed(), - move_caret_down, - )?; - assert_move( - &["abcd❮efgh❯┃", "ijklmn", "opqr", "st"], - &["abcd❮efgh", "ijklmn❯┃", "opqr", "st"], - &shift_pressed(), - move_caret_down, - )?; - assert_move( - &["abcd❮e❯┃fgh", "ijklmn", "opqr", "st"], - &["abcd❮efgh", "ijklm❯┃n", "opqr", "st"], - &shift_pressed(), - move_caret_down, - )?; - - Ok(()) - } - - #[test] - fn extend_selection_home() -> Result<(), String> { - let move_caret_home = SelectableLines::move_caret_home; - - assert_move(&["ab┃❮c❯"], &["┃❮abc❯"], &shift_pressed(), move_caret_home)?; - assert_move(&["a┃❮bc❯"], &["┃❮abc❯"], &shift_pressed(), move_caret_home)?; - assert_move(&["┃❮abc❯"], &["┃❮abc❯"], &shift_pressed(), move_caret_home)?; - assert_move(&["┃❮ab❯c"], &["┃❮ab❯c"], &shift_pressed(), move_caret_home)?; - assert_move(&["┃❮a❯bc"], &["┃❮a❯bc"], &shift_pressed(), move_caret_home)?; - assert_move( - &[" ┃❮a❯bc"], - &["┃❮ a❯bc"], - &shift_pressed(), - move_caret_home, - )?; - assert_move(&["ab❮c❯┃"], &["┃❮ab❯c"], &shift_pressed(), move_caret_home)?; - assert_move( - &["abc", "de❮f❯┃"], - &["abc", "┃❮de❯f"], - &shift_pressed(), - move_caret_home, - )?; - assert_move( - &["abc", "de┃❮f❯"], - &["abc", "┃❮def❯"], - &shift_pressed(), - move_caret_home, - )?; - assert_move( - &["ab┃❮c", "def❯"], - &["┃❮abc", "def❯"], - &shift_pressed(), - move_caret_home, - )?; - assert_move( - &[" ab┃❮c", "def❯"], - &[" ┃❮abc", "def❯"], - &shift_pressed(), - move_caret_home, - )?; - assert_move( - &[" ┃❮abc", "def❯"], - &["┃❮ abc", "def❯"], - &shift_pressed(), - move_caret_home, - )?; - assert_move( - &["ab", "cdef", "ghijkl", "┃❮mnopqr❯st"], - &["ab", "cdef", "ghijkl", "┃❮mnopqr❯st"], - &shift_pressed(), - move_caret_home, - )?; - assert_move( - &["ab", "cdef", "gh┃❮ijkl❯", "mnopqrst"], - &["ab", "cdef", "┃❮ghijkl❯", "mnopqrst"], - &shift_pressed(), - move_caret_home, - )?; - assert_move( - &["abcdefgh", "ijklmn", "op❮qr❯┃", "st"], - &["abcdefgh", "ijklmn", "┃❮op❯qr", "st"], - &shift_pressed(), - move_caret_home, - )?; - - Ok(()) - } - - #[test] - fn extend_selection_end() -> Result<(), String> { - let move_caret_end = SelectableLines::move_caret_end; - - assert_move(&["❮ab❯┃c"], &["❮abc❯┃"], &shift_pressed(), move_caret_end)?; - assert_move(&["❮a❯┃bc"], &["❮abc❯┃"], &shift_pressed(), move_caret_end)?; - assert_move(&["❮abc❯┃"], &["❮abc❯┃"], &shift_pressed(), move_caret_end)?; - assert_move(&["┃❮ab❯c"], &["ab❮c❯┃"], &shift_pressed(), move_caret_end)?; - assert_move(&["┃❮a❯bc"], &["a❮bc❯┃"], &shift_pressed(), move_caret_end)?; - assert_move( - &["❮a❯┃bc", "d"], - &["❮abc❯┃", "d"], - &shift_pressed(), - move_caret_end, - )?; - assert_move( - &["❮a❯┃bc ", "de"], - &["❮abc ❯┃", "de"], - &shift_pressed(), - move_caret_end, - )?; - assert_move( - &["❮abc", "d❯┃e"], - &["❮abc", "de❯┃"], - &shift_pressed(), - move_caret_end, - )?; - assert_move( - &["ab", "cdef", "ghijkl", "❮mnopqr❯┃st"], - &["ab", "cdef", "ghijkl", "❮mnopqrst❯┃"], - &shift_pressed(), - move_caret_end, - )?; - assert_move( - &["a❮b", "cdef", "ghijkl", "mnopqr❯┃st"], - &["a❮b", "cdef", "ghijkl", "mnopqrst❯┃"], - &shift_pressed(), - move_caret_end, - )?; - assert_move( - &["❮ab", "cdef", "ghijkl", "mnopqrst❯┃"], - &["❮ab", "cdef", "ghijkl", "mnopqrst❯┃"], - &shift_pressed(), - move_caret_end, - )?; - assert_move( - &["abcd❮e❯┃fgh", "ijklmn", "opqr", "st"], - &["abcd❮efgh❯┃", "ijklmn", "opqr", "st"], - &shift_pressed(), - move_caret_end, - )?; - - Ok(()) - } - - #[test] - fn shrink_selection_right() -> Result<(), String> { - let move_caret_right = SelectableLines::move_caret_right; - - assert_move(&["ab┃❮c❯"], &["abc┃"], &shift_pressed(), move_caret_right)?; - assert_move(&["a┃❮bc❯"], &["ab┃❮c❯"], &shift_pressed(), move_caret_right)?; - assert_move(&["┃❮abc❯"], &["a┃❮bc❯"], &shift_pressed(), move_caret_right)?; - assert_move( - &["┃❮abc", "def", "ghi", "jkl❯"], - &["a┃❮bc", "def", "ghi", "jkl❯"], - &shift_pressed(), - move_caret_right, - )?; - assert_move( - &["abc", "d┃❮ef", "❯ghi", "jkl"], - &["abc", "de┃❮f", "❯ghi", "jkl"], - &shift_pressed(), - move_caret_right, - )?; - assert_move( - &["abc", "de┃❮f❯", "ghi", "jkl"], - &["abc", "def┃", "ghi", "jkl"], - &shift_pressed(), - move_caret_right, - )?; - - Ok(()) - } - - #[test] - fn shrink_selection_left() -> Result<(), String> { - let move_caret_left = SelectableLines::move_caret_left; - - assert_move(&["ab❮c❯┃"], &["ab┃c"], &shift_pressed(), move_caret_left)?; - assert_move(&["a❮bc❯┃"], &["a❮b❯┃c"], &shift_pressed(), move_caret_left)?; - assert_move(&["❮abc❯┃"], &["❮ab❯┃c"], &shift_pressed(), move_caret_left)?; - assert_move( - &["❮abc", "def", "ghi", "jkl❯┃"], - &["❮abc", "def", "ghi", "jk❯┃l"], - &shift_pressed(), - move_caret_left, - )?; - assert_move( - &["┃❮abc", "def", "ghi", "jkl❯"], - &["┃❮abc", "def", "ghi", "jkl❯"], - &shift_pressed(), - move_caret_left, - )?; - assert_move( - &["abc", "def❮", "❯┃ghi", "jkl"], - &["abc", "def┃", "ghi", "jkl"], - &shift_pressed(), - move_caret_left, - )?; - assert_move( - &["abc", "d❮ef", "gh❯┃i", "jkl"], - &["abc", "d❮ef", "g❯┃hi", "jkl"], - &shift_pressed(), - move_caret_left, - )?; - - Ok(()) - } - - #[test] - fn shrink_selection_up() -> Result<(), String> { - let move_caret_up = SelectableLines::move_caret_up; - - assert_move(&["❮abc❯┃"], &["┃abc"], &shift_pressed(), move_caret_up)?; - assert_move(&["❮ab❯┃c"], &["┃abc"], &shift_pressed(), move_caret_up)?; - assert_move(&["❮a❯┃bc"], &["┃abc"], &shift_pressed(), move_caret_up)?; - assert_move(&["┃abc"], &["┃abc"], &shift_pressed(), move_caret_up)?; - assert_move( - &["❮abc", "def❯┃"], - &["❮abc❯┃", "def"], - &shift_pressed(), - move_caret_up, - )?; - assert_move( - &["❮abc", "de❯┃f"], - &["❮ab❯┃c", "def"], - &shift_pressed(), - move_caret_up, - )?; - assert_move( - &["❮abc", "def", "ghi", "jkl❯┃"], - &["❮abc", "def", "ghi❯┃", "jkl"], - &shift_pressed(), - move_caret_up, - )?; - assert_move( - &["abc", "def", "ghi❮", "jkl❯┃"], - &["abc", "def", "ghi┃", "jkl"], - &shift_pressed(), - move_caret_up, - )?; - assert_move( - &["abc", "d❮ef", "ghi", "jk❯┃l"], - &["abc", "d❮ef", "gh❯┃i", "jkl"], - &shift_pressed(), - move_caret_up, - )?; - assert_move( - &["❮abc", "d❯┃ef", "ghi", "jkl"], - &["❮a❯┃bc", "def", "ghi", "jkl"], - &shift_pressed(), - move_caret_up, - )?; - - Ok(()) - } - - #[test] - fn shrink_selection_down() -> Result<(), String> { - let move_caret_down = SelectableLines::move_caret_down; - - assert_move(&["┃❮abc❯"], &["abc┃"], &shift_pressed(), move_caret_down)?; - assert_move( - &["┃❮abc", "def❯"], - &["abc", "┃❮def❯"], - &shift_pressed(), - move_caret_down, - )?; - assert_move( - &["a┃❮bc", "def❯"], - &["abc", "d┃❮ef❯"], - &shift_pressed(), - move_caret_down, - )?; - assert_move( - &["┃❮abc", "def", "ghi❯"], - &["abc", "┃❮def", "ghi❯"], - &shift_pressed(), - move_caret_down, - )?; - assert_move( - &["ab┃❮c", "def", "ghi❯"], - &["abc", "de┃❮f", "ghi❯"], - &shift_pressed(), - move_caret_down, - )?; - assert_move( - &["abc", "de┃❮f", "ghi❯"], - &["abc", "def", "gh┃❮i❯"], - &shift_pressed(), - move_caret_down, - )?; - assert_move( - &["abcdef┃❮", "ghij", "kl❯"], - &["abcdef", "ghij┃❮", "kl❯"], - &shift_pressed(), - move_caret_down, - )?; - assert_move( - &["abcde┃❮f", "ghij", "kl❯"], - &["abcdef", "ghij┃❮", "kl❯"], - &shift_pressed(), - move_caret_down, - )?; - assert_move( - &["ab┃❮cdef", "ghij", "kl❯"], - &["abcdef", "gh┃❮ij", "kl❯"], - &shift_pressed(), - move_caret_down, - )?; - - Ok(()) - } - - #[test] - fn shrink_selection_home() -> Result<(), String> { - let move_caret_home = SelectableLines::move_caret_home; - - assert_move(&["❮abc❯┃"], &["┃abc"], &shift_pressed(), move_caret_home)?; - assert_move(&["❮ab❯┃c"], &["┃abc"], &shift_pressed(), move_caret_home)?; - assert_move(&["❮a❯┃bc"], &["┃abc"], &shift_pressed(), move_caret_home)?; - assert_move(&[" ❮abc❯┃"], &[" ┃abc"], &shift_pressed(), move_caret_home)?; - assert_move(&[" ❮ab❯┃c"], &[" ┃abc"], &shift_pressed(), move_caret_home)?; - assert_move(&[" ❮a❯┃bc"], &[" ┃abc"], &shift_pressed(), move_caret_home)?; - assert_move(&["ab❮c❯┃"], &["┃❮ab❯c"], &shift_pressed(), move_caret_home)?; - assert_move(&["a❮b❯┃c"], &["┃❮a❯bc"], &shift_pressed(), move_caret_home)?; - assert_move(&["a❮bc❯┃"], &["┃❮a❯bc"], &shift_pressed(), move_caret_home)?; - - assert_move( - &["❮abc", "def❯┃"], - &["❮abc", "❯┃def"], - &shift_pressed(), - move_caret_home, - )?; - assert_move( - &["❮abc", " de❯┃f"], - &["❮abc", " ❯┃def"], - &shift_pressed(), - move_caret_home, - )?; - assert_move( - &["❮abc", "def", "ghi", "jkl❯┃"], - &["❮abc", "def", "ghi", "❯┃jkl"], - &shift_pressed(), - move_caret_home, - )?; - assert_move( - &["abc", "def", "ghi❮", "jkl❯┃"], - &["abc", "def", "ghi❮", "❯┃jkl"], - &shift_pressed(), - move_caret_home, - )?; - assert_move( - &["abc", "d❮ef", " ghi", " jk❯┃l"], - &["abc", "d❮ef", " ghi", " ❯┃jkl"], - &shift_pressed(), - move_caret_home, - )?; - assert_move( - &["❮abc", "d❯┃ef", "ghi", "jkl"], - &["❮abc", "❯┃def", "ghi", "jkl"], - &shift_pressed(), - move_caret_home, - )?; - assert_move( - &["❮abc", "d❯┃ef", "ghi", "jkl"], - &["❮abc", "❯┃def", "ghi", "jkl"], - &shift_pressed(), - move_caret_home, - )?; - - Ok(()) - } - - #[test] - fn shrink_selection_end() -> Result<(), String> { - let move_caret_end = SelectableLines::move_caret_end; - - assert_move(&["┃❮abc❯"], &["abc┃"], &shift_pressed(), move_caret_end)?; - assert_move( - &["┃❮abc", "def❯"], - &["abc┃❮", "def❯"], - &shift_pressed(), - move_caret_end, - )?; - assert_move( - &["a┃❮bc", "def❯"], - &["abc┃❮", "def❯"], - &shift_pressed(), - move_caret_end, - )?; - assert_move( - &["a┃❮bc", "def", "ghi❯"], - &["abc┃❮", "def", "ghi❯"], - &shift_pressed(), - move_caret_end, - )?; - assert_move( - &["ab┃❮c", "def", "ghi❯"], - &["abc┃❮", "def", "ghi❯"], - &shift_pressed(), - move_caret_end, - )?; - assert_move( - &["abc", "de┃❮f", "ghi❯"], - &["abc", "def┃❮", "ghi❯"], - &shift_pressed(), - move_caret_end, - )?; - assert_move( - &["abcdef┃❮", "ghij", "kl❯"], - &["abcdef┃❮", "ghij", "kl❯"], - &shift_pressed(), - move_caret_end, - )?; - assert_move( - &["┃❮ abcdef", "ghij", "kl❯"], - &[" abcdef┃❮", "ghij", "kl❯"], - &shift_pressed(), - move_caret_end, - )?; - assert_move( - &["abcdef", "ghij", "┃❮kl❯"], - &["abcdef", "ghij", "kl┃"], - &shift_pressed(), - move_caret_end, - )?; - - Ok(()) - } -} diff --git a/editor/src/ui/text/caret_w_select.rs b/editor/src/ui/text/caret_w_select.rs deleted file mode 100644 index 29140ad2a6..0000000000 --- a/editor/src/ui/text/caret_w_select.rs +++ /dev/null @@ -1,325 +0,0 @@ -#![allow(dead_code)] - -use super::selection::validate_selection; -use super::selection::Selection; -use super::text_pos::TextPos; -use crate::ui::ui_error::UIResult; -use crate::window::keyboard_input::Modifiers; - -#[derive(Debug, Copy, Clone)] -pub struct CaretWSelect { - pub caret_pos: TextPos, - pub selection_opt: Option, -} - -pub enum CaretPos { - Start, - Exact(TextPos), - End, -} - -fn mk_some_sel(start_pos: TextPos, end_pos: TextPos) -> UIResult> { - if start_pos == end_pos { - Ok(None) - } else { - Ok(Some(validate_selection(start_pos, end_pos)?)) - } -} - -impl Default for CaretWSelect { - fn default() -> Self { - Self { - caret_pos: TextPos { line: 0, column: 0 }, - selection_opt: None, - } - } -} - -impl CaretWSelect { - pub fn new(caret_pos: TextPos, selection_opt: Option) -> Self { - Self { - caret_pos, - selection_opt, - } - } - - pub fn move_caret_w_mods(&self, new_pos: TextPos, mods: &Modifiers) -> UIResult { - let old_caret_pos = self.caret_pos; - - // one does not simply move the caret - let valid_sel_opt = if mods.shift { - if new_pos != old_caret_pos { - if let Some(old_sel) = self.selection_opt { - if new_pos < old_sel.start_pos { - if old_caret_pos > old_sel.start_pos { - mk_some_sel(new_pos, old_sel.start_pos)? - } else { - mk_some_sel(new_pos, old_sel.end_pos)? - } - } else if new_pos > old_sel.end_pos { - if old_caret_pos < old_sel.end_pos { - mk_some_sel(old_sel.end_pos, new_pos)? - } else { - mk_some_sel(old_sel.start_pos, new_pos)? - } - } else if new_pos > old_caret_pos { - mk_some_sel(new_pos, old_sel.end_pos)? - } else if new_pos < old_caret_pos { - mk_some_sel(old_sel.start_pos, new_pos)? - } else { - None - } - } else if new_pos < self.caret_pos { - mk_some_sel(new_pos, old_caret_pos)? - } else { - mk_some_sel(old_caret_pos, new_pos)? - } - } else { - self.selection_opt - } - } else { - None - }; - - Ok(CaretWSelect::new(new_pos, valid_sel_opt)) - } -} - -// VIEW -// ---- -use crate::graphics::primitives::rect::Rect; -use crate::ui::theme::UITheme; - -pub fn make_caret_rect_from_pos( - caret_pos: TextPos, - glyph_dim_rect: &Rect, - ui_theme: &UITheme, -) -> Rect { - let caret_x = - glyph_dim_rect.top_left_coords.x + glyph_dim_rect.width * (caret_pos.column as f32); - - let caret_y = - glyph_dim_rect.top_left_coords.y + (caret_pos.line as f32) * glyph_dim_rect.height; - - make_caret_rect(caret_x, caret_y, glyph_dim_rect, ui_theme) -} - -pub fn make_caret_rect( - caret_x: f32, - caret_y: f32, - glyph_dim_rect: &Rect, - ui_theme: &UITheme, -) -> Rect { - Rect { - top_left_coords: (caret_x, caret_y).into(), - height: glyph_dim_rect.height, - width: glyph_dim_rect.width / 6.0, - color: ui_theme.caret, - } -} - -pub fn make_selection_rect( - sel_rect_x: f32, - sel_rect_y: f32, - width: f32, - glyph_dim_rect: &Rect, - ui_theme: &UITheme, -) -> Rect { - Rect { - top_left_coords: (sel_rect_x, sel_rect_y).into(), - height: glyph_dim_rect.height, - width, - color: ui_theme.select_highlight, - } -} - -#[cfg(test)] -pub mod test_caret_w_select { - use crate::ui::text::caret_w_select::CaretWSelect; - use crate::ui::text::selection::validate_selection; - use crate::ui::text::text_pos::TextPos; - use crate::ui::ui_error::OutOfBounds; - use crate::ui::ui_error::UIResult; - use crate::ui::util::slice_get; - use core::cmp::Ordering; - use pest::Parser; - use snafu::OptionExt; - use std::{collections::HashMap, slice::SliceIndex}; - - #[derive(Parser)] - #[grammar = "../tests/selection.pest"] - pub struct LineParser; - - // Retrieve selection and position from formatted string - pub fn convert_dsl_to_selection(lines: &[String]) -> Result { - let lines_str: String = lines.join("\n"); - - let parsed = LineParser::parse(Rule::linesWithSelect, &lines_str) - .expect("Selection test DSL parsing failed"); - - let mut caret_opt: Option<(usize, usize)> = None; - let mut sel_start_opt: Option<(usize, usize)> = None; - let mut sel_end_opt: Option<(usize, usize)> = None; - let mut line_nr = 0; - let mut col_nr = 0; - - for line in parsed { - for elt in line.into_inner() { - match elt.as_rule() { - Rule::optCaret => { - if elt.as_span().as_str() == "┃" { - if caret_opt.is_some() { - return Err( - "Multiple carets found, there should be only one".to_owned() - ); - } else { - caret_opt = Some((line_nr, col_nr)); - } - } - } - Rule::optSelStart => { - if sel_start_opt.is_some() { - if elt.as_span().as_str() == "❮" { - return Err("Found start of selection more than once, there should be only one".to_owned()); - } - } else if elt.as_span().as_str() == "❮" { - sel_start_opt = Some((line_nr, col_nr)); - } - } - Rule::optSelEnd => { - if sel_end_opt.is_some() { - if elt.as_span().as_str() == "❯" { - return Err("Found end of selection more than once, there should be only one".to_owned()); - } - } else if elt.as_span().as_str() == "❯" { - sel_end_opt = Some((line_nr, col_nr)); - } - } - Rule::text => { - let split_str = elt - .as_span() - .as_str() - .split('\n') - .into_iter() - .collect::>(); - - if split_str.len() > 1 { - line_nr += split_str.len() - 1; - col_nr = 0 - } - if let Some(last_str) = split_str.last() { - col_nr += last_str.len() - } - } - _ => {} - } - } - } - - // Make sure return makes sense - if let Some((line, column)) = caret_opt { - let caret_pos = TextPos { line, column }; - if sel_start_opt.is_none() && sel_end_opt.is_none() { - Ok(CaretWSelect::new(caret_pos, None)) - } else if let Some((start_line, start_column)) = sel_start_opt { - if let Some((end_line, end_column)) = sel_end_opt { - Ok(CaretWSelect::new( - caret_pos, - Some( - validate_selection( - TextPos { - line: start_line, - column: start_column, - }, - TextPos { - line: end_line, - column: end_column, - }, - ) - .unwrap(), - ), - )) - } else { - Err("Selection end '❯' was not found, but selection start '❮' was. Bad input string.".to_owned()) - } - } else { - Err("Selection start '❮' was not found, but selection end '❯' was. Bad input string.".to_owned()) - } - } else { - Err("No caret was found in lines.".to_owned()) - } - } - - // show selection and caret position as symbols in lines for easy testing - pub fn convert_selection_to_dsl( - caret_w_select: CaretWSelect, - lines: Vec, - ) -> UIResult> { - let selection_opt = caret_w_select.selection_opt; - let caret_pos = caret_w_select.caret_pos; - let mut mut_lines = lines; - - if let Some(sel) = selection_opt { - let mut to_insert = vec![(sel.start_pos, '❮'), (sel.end_pos, '❯'), (caret_pos, '┃')]; - let symbol_map: HashMap = - [('❮', 2), ('❯', 0), ('┃', 1)].iter().cloned().collect(); - - // sort for nice printing - to_insert.sort_by(|a, b| { - let pos_cmp = a.0.cmp(&b.0); - if pos_cmp == Ordering::Equal { - symbol_map.get(&a.1).cmp(&symbol_map.get(&b.1)) - } else { - pos_cmp - } - }); - - // insert symbols into text lines - for i in 0..to_insert.len() { - let (pos, insert_char) = *slice_get(i, &to_insert)?; - - insert_at_pos(&mut mut_lines, pos, insert_char)?; - - // shift position of following symbols now that symbol is inserted - for j in i..to_insert.len() { - let (old_pos, _) = get_mut_res(j, &mut to_insert)?; - - if old_pos.line == pos.line { - old_pos.column += 1; - } - } - } - } else { - insert_at_pos(&mut mut_lines, caret_pos, '┃')?; - } - - Ok(mut_lines) - } - - fn insert_at_pos(lines: &mut [String], pos: TextPos, insert_char: char) -> UIResult<()> { - let line = get_mut_res(pos.line, lines)?; - - let mut chars: Vec = line.chars().collect(); - chars.insert(pos.column, insert_char); - - *line = chars.into_iter().collect::(); - - Ok(()) - } - - // It's much nicer to have get_mut return a Result with clear error than an Option - fn get_mut_res( - index: usize, - vec: &mut [T], - ) -> UIResult<&mut >::Output> { - let vec_len = vec.len(); - - let elt_ref = vec.get_mut(index).context(OutOfBounds { - index, - collection_name: "Slice", - len: vec_len, - })?; - - Ok(elt_ref) - } -} diff --git a/editor/src/ui/text/lines.rs b/editor/src/ui/text/lines.rs deleted file mode 100644 index 37e1d67452..0000000000 --- a/editor/src/ui/text/lines.rs +++ /dev/null @@ -1,445 +0,0 @@ -// Adapted from https://github.com/cessen/ropey -// by Nathan Vegdahl - license information can be found in the LEGAL_DETAILS -// file in the root directory of this distribution. -// -// Thank you, Nathan! - -use crate::ui::text::caret_w_select::CaretWSelect; -use crate::ui::text::selection::validate_sel_opt; -use crate::ui::text::{ - selection::{RawSelection, Selection}, - text_pos::TextPos, -}; -use crate::ui::ui_error::UIResult; -use crate::ui::util::is_newline; -use crate::window::keyboard_input::Modifiers; -use std::cmp::max; -use std::cmp::min; -use winit::event::VirtualKeyCode; - -pub trait Lines { - fn get_line_ref(&self, line_nr: usize) -> UIResult<&str>; - - fn line_len(&self, line_nr: usize) -> UIResult; - - fn nr_of_lines(&self) -> usize; - - fn nr_of_chars(&self) -> usize; - - fn all_lines_as_string(&self) -> String; - - fn is_last_line(&self, line_nr: usize) -> bool; - - fn last_char(&self, line_nr: usize) -> UIResult>; -} - -pub trait SelectableLines { - fn get_caret(&self) -> TextPos; - - fn set_caret(&mut self, caret_pos: TextPos); - - fn move_caret_left(&mut self, modifiers: &Modifiers) -> UIResult<()>; - - fn move_caret_right(&mut self, modifiers: &Modifiers) -> UIResult<()>; - - fn move_caret_up(&mut self, modifiers: &Modifiers) -> UIResult<()>; - - fn move_caret_down(&mut self, modifiers: &Modifiers) -> UIResult<()>; - - fn move_caret_home(&mut self, modifiers: &Modifiers) -> UIResult<()>; - - fn move_caret_end(&mut self, modifiers: &Modifiers) -> UIResult<()>; - - fn get_selection(&self) -> Option; - - fn is_selection_active(&self) -> bool; - - fn get_selected_str(&self) -> UIResult>; - - fn set_raw_sel(&mut self, raw_sel: RawSelection) -> UIResult<()>; - - fn set_sel_none(&mut self); - - fn set_caret_w_sel(&mut self, caret_w_sel: CaretWSelect); - - fn select_all(&mut self) -> UIResult<()>; - - fn last_text_pos(&self) -> UIResult; - - fn handle_key_down( - &mut self, - modifiers: &Modifiers, - virtual_keycode: VirtualKeyCode, - ) -> UIResult<()>; -} - -pub trait MutSelectableLines { - fn insert_char(&mut self, new_char: &char) -> UIResult<()>; - - // could be for insertion, backspace, del... - fn handle_new_char(&mut self, received_char: &char) -> UIResult<()>; - - fn insert_str(&mut self, new_str: &str) -> UIResult<()>; - - fn backspace(&mut self) -> UIResult<()>; - - fn del_selection(&mut self) -> UIResult<()>; -} - -// T: Lines -pub type MoveCaretFun = fn(&T, CaretWSelect, &Modifiers) -> UIResult; - -pub fn move_caret_left( - lines: &T, - caret_w_select: CaretWSelect, - modifiers: &Modifiers, -) -> UIResult { - let old_selection_opt = caret_w_select.selection_opt; - let old_caret_pos = caret_w_select.caret_pos; - let old_line_nr = old_caret_pos.line; - let old_col_nr = old_caret_pos.column; - - let shift_pressed = modifiers.shift; - - let (line_nr, col_nr) = if old_selection_opt.is_some() && !shift_pressed { - match old_selection_opt { - Some(old_selection) => (old_selection.start_pos.line, old_selection.start_pos.column), - None => unreachable!(), - } - } else if old_col_nr == 0 { - if old_line_nr == 0 { - (0, 0) - } else { - let curr_line_len = lines.line_len(old_line_nr - 1)?; - - (old_line_nr - 1, curr_line_len) - } - } else { - (old_line_nr, old_col_nr - 1) - }; - - let new_caret_pos = TextPos { - line: line_nr, - column: col_nr, - }; - - let new_selection_opt = if shift_pressed { - if let Some(old_selection) = old_selection_opt { - if old_caret_pos >= old_selection.end_pos { - if new_caret_pos == old_selection.start_pos { - None - } else { - validate_sel_opt(old_selection.start_pos, new_caret_pos)? - } - } else { - validate_sel_opt( - TextPos { - line: line_nr, - column: col_nr, - }, - old_selection.end_pos, - )? - } - } else if !(old_line_nr == line_nr && old_col_nr == col_nr) { - validate_sel_opt( - TextPos { - line: line_nr, - column: col_nr, - }, - TextPos { - line: old_line_nr, - column: old_col_nr, - }, - )? - } else { - None - } - } else { - None - }; - - Ok(CaretWSelect::new(new_caret_pos, new_selection_opt)) -} - -pub fn move_caret_right( - lines: &T, - caret_w_select: CaretWSelect, - modifiers: &Modifiers, -) -> UIResult { - let old_selection_opt = caret_w_select.selection_opt; - let old_caret_pos = caret_w_select.caret_pos; - let old_line_nr = old_caret_pos.line; - let old_col_nr = old_caret_pos.column; - - let shift_pressed = modifiers.shift; - - let (line_nr, col_nr) = if old_selection_opt.is_some() && !shift_pressed { - match old_selection_opt { - Some(old_selection) => (old_selection.end_pos.line, old_selection.end_pos.column), - None => unreachable!(), - } - } else { - let curr_line_len = lines.line_len(old_line_nr)?; - let is_last_line = lines.is_last_line(old_line_nr); - - if !is_last_line { - if old_col_nr + 1 > curr_line_len { - (old_line_nr + 1, 0) - } else { - (old_line_nr, old_col_nr + 1) - } - } else if old_col_nr < curr_line_len { - (old_line_nr, old_col_nr + 1) - } else { - (old_line_nr, old_col_nr) - } - }; - - let new_caret_pos = TextPos { - line: line_nr, - column: col_nr, - }; - - let new_selection_opt = if shift_pressed { - if let Some(old_selection) = old_selection_opt { - if old_caret_pos <= old_selection.start_pos { - if new_caret_pos == old_selection.end_pos { - None - } else { - validate_sel_opt(new_caret_pos, old_selection.end_pos)? - } - } else { - validate_sel_opt( - old_selection.start_pos, - TextPos { - line: line_nr, - column: col_nr, - }, - )? - } - } else if !(old_line_nr == line_nr && old_col_nr == col_nr) { - validate_sel_opt( - TextPos { - line: old_line_nr, - column: old_col_nr, - }, - TextPos { - line: line_nr, - column: col_nr, - }, - )? - } else { - None - } - } else { - None - }; - - Ok(CaretWSelect::new(new_caret_pos, new_selection_opt)) -} - -pub fn move_caret_up( - lines: &T, - caret_w_select: CaretWSelect, - modifiers: &Modifiers, -) -> UIResult { - let old_selection_opt = caret_w_select.selection_opt; - let old_caret_pos = caret_w_select.caret_pos; - let old_line_nr = old_caret_pos.line; - let old_col_nr = old_caret_pos.column; - - let shift_pressed = modifiers.shift; - - let (line_nr, col_nr) = if old_selection_opt.is_some() && !shift_pressed { - match old_selection_opt { - Some(old_selection) => (old_selection.start_pos.line, old_selection.start_pos.column), - None => unreachable!(), - } - } else if old_line_nr == 0 { - (old_line_nr, 0) - } else { - let prev_line_len = lines.line_len(old_line_nr - 1)?; - - if prev_line_len <= old_col_nr { - let new_column = if prev_line_len > 0 { prev_line_len } else { 0 }; - - (old_line_nr - 1, new_column) - } else { - (old_line_nr - 1, old_col_nr) - } - }; - - let new_caret_pos = TextPos { - line: line_nr, - column: col_nr, - }; - - let new_selection_opt = if shift_pressed { - if let Some(old_selection) = old_selection_opt { - if old_selection.end_pos <= old_caret_pos { - if new_caret_pos == old_selection.start_pos { - None - } else { - validate_sel_opt( - min(old_selection.start_pos, new_caret_pos), - max(old_selection.start_pos, new_caret_pos), - )? - } - } else { - validate_sel_opt(new_caret_pos, old_selection.end_pos)? - } - } else if !(old_line_nr == line_nr && old_col_nr == col_nr) { - validate_sel_opt( - min(old_caret_pos, new_caret_pos), - max(old_caret_pos, new_caret_pos), - )? - } else { - None - } - } else { - None - }; - - Ok(CaretWSelect::new(new_caret_pos, new_selection_opt)) -} - -pub fn move_caret_down( - lines: &T, - caret_w_select: CaretWSelect, - modifiers: &Modifiers, -) -> UIResult { - let old_selection_opt = caret_w_select.selection_opt; - let old_caret_pos = caret_w_select.caret_pos; - let old_line_nr = old_caret_pos.line; - let old_col_nr = old_caret_pos.column; - - let shift_pressed = modifiers.shift; - - let (line_nr, col_nr) = if old_selection_opt.is_some() && !shift_pressed { - match old_selection_opt { - Some(old_selection) => (old_selection.end_pos.line, old_selection.end_pos.column), - None => unreachable!(), - } - } else if old_line_nr + 1 >= lines.nr_of_lines() { - let curr_line_len = lines.line_len(old_line_nr)?; - - (old_line_nr, curr_line_len) - } else { - let next_line_index = old_line_nr + 1; - let next_line_len = lines.line_len(next_line_index)?; - let is_last_line = lines.is_last_line(next_line_index); - - if next_line_len <= old_col_nr { - if !is_last_line { - let new_column = if next_line_len > 0 { next_line_len } else { 0 }; - - (old_line_nr + 1, new_column) - } else { - (old_line_nr + 1, next_line_len) - } - } else { - (old_line_nr + 1, old_col_nr) - } - }; - - let new_caret_pos = TextPos { - line: line_nr, - column: col_nr, - }; - - let new_selection_opt = if shift_pressed { - if let Some(old_selection) = old_selection_opt { - if old_caret_pos <= old_selection.start_pos { - if new_caret_pos == old_selection.end_pos { - None - } else { - validate_sel_opt( - min(old_selection.end_pos, new_caret_pos), - max(old_selection.end_pos, new_caret_pos), - )? - } - } else { - validate_sel_opt(old_selection.start_pos, new_caret_pos)? - } - } else if !(old_line_nr == line_nr && old_col_nr == col_nr) { - validate_sel_opt( - min(old_caret_pos, new_caret_pos), - max(old_caret_pos, new_caret_pos), - )? - } else { - None - } - } else { - None - }; - - Ok(CaretWSelect::new(new_caret_pos, new_selection_opt)) -} - -pub fn move_caret_home( - lines: &T, - caret_w_select: CaretWSelect, - modifiers: &Modifiers, -) -> UIResult { - let curr_line_nr = caret_w_select.caret_pos.line; - let old_col_nr = caret_w_select.caret_pos.column; - - let curr_line_str = lines.get_line_ref(curr_line_nr)?; - let line_char_iter = curr_line_str.chars(); - - let mut first_no_space_char_col = 0; - let mut non_space_found = false; - - for c in line_char_iter { - if !c.is_whitespace() { - non_space_found = true; - break; - } else { - first_no_space_char_col += 1; - } - } - - if !non_space_found { - first_no_space_char_col = 0; - } - - let new_col_nr = if first_no_space_char_col == old_col_nr { - 0 - } else { - first_no_space_char_col - }; - - caret_w_select.move_caret_w_mods( - TextPos { - line: curr_line_nr, - column: new_col_nr, - }, - modifiers, - ) -} - -pub fn move_caret_end( - lines: &T, - caret_w_select: CaretWSelect, - modifiers: &Modifiers, -) -> UIResult { - let curr_line_nr = caret_w_select.caret_pos.line; - let curr_line_len = lines.line_len(curr_line_nr)?; - - let new_col = if let Some(last_char) = lines.last_char(curr_line_nr)? { - if is_newline(&last_char) { - curr_line_len - 1 - } else { - curr_line_len - } - } else { - 0 - }; - - let new_pos = TextPos { - line: curr_line_nr, - column: new_col, - }; - - caret_w_select.move_caret_w_mods(new_pos, modifiers) -} diff --git a/editor/src/ui/text/mod.rs b/editor/src/ui/text/mod.rs deleted file mode 100644 index feaa7946f8..0000000000 --- a/editor/src/ui/text/mod.rs +++ /dev/null @@ -1,6 +0,0 @@ -pub mod big_text_area; -pub mod caret_w_select; -pub mod lines; -pub mod selection; -mod text_buffer; -pub mod text_pos; diff --git a/editor/src/ui/text/selection.rs b/editor/src/ui/text/selection.rs deleted file mode 100644 index 70376b6c86..0000000000 --- a/editor/src/ui/text/selection.rs +++ /dev/null @@ -1,152 +0,0 @@ -#![allow(dead_code)] - -use super::lines::Lines; -use super::text_pos::TextPos; -use crate::ui::theme::UITheme; -use crate::ui::ui_error::{InvalidSelection, UIResult}; -use bumpalo::collections::Vec as BumpVec; -use snafu::ensure; -use std::fmt; - -#[derive(Debug, Copy, Clone)] -pub struct RawSelection { - pub start_pos: TextPos, - pub end_pos: TextPos, -} - -impl std::fmt::Display for RawSelection { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!( - f, - "RawSelection: start_pos: line:{} col:{}, end_pos: line:{} col:{}", - self.start_pos.line, self.start_pos.column, self.end_pos.line, self.end_pos.column - ) - } -} - -#[derive(Debug, Copy, Clone)] -pub struct Selection { - pub start_pos: TextPos, - pub end_pos: TextPos, -} - -impl Selection { - pub fn is_on_same_line(&self) -> bool { - self.start_pos.line == self.end_pos.line - } -} - -pub fn validate_raw_sel(raw_sel: RawSelection) -> UIResult { - validate_selection(raw_sel.start_pos, raw_sel.end_pos) -} - -pub fn validate_sel_opt(start_pos: TextPos, end_pos: TextPos) -> UIResult> { - Ok(Some(validate_selection(start_pos, end_pos)?)) -} - -pub fn validate_selection(start_pos: TextPos, end_pos: TextPos) -> UIResult { - ensure!( - start_pos.line <= end_pos.line, - InvalidSelection { - err_msg: format!( - "start_pos.line ({}) should be smaller than or equal to end_pos.line ({})", - start_pos.line, end_pos.line - ) - } - ); - - ensure!( - !(start_pos.line == end_pos.line && start_pos.column >= end_pos.column), - InvalidSelection { - err_msg: format!( - "start_pos.column ({}) should be smaller than end_pos.column ({}) when start_pos.line equals end_pos.line", - start_pos.column, - end_pos.column - ) - } - ); - - Ok(Selection { start_pos, end_pos }) -} - -use crate::graphics::primitives::rect::Rect; -use bumpalo::Bump; - -pub fn create_selection_rects<'a>( - valid_sel: Selection, - lines: &dyn Lines, - glyph_dim_rect: &Rect, - theme: &UITheme, - arena: &'a Bump, -) -> UIResult> { - let Selection { start_pos, end_pos } = valid_sel; - - let mut all_rects: BumpVec = BumpVec::new_in(arena); - - let height = glyph_dim_rect.height; - let start_y = glyph_dim_rect.top_left_coords.y + height * (start_pos.line as f32); - let line_start_x = glyph_dim_rect.top_left_coords.x; - - if start_pos.line == end_pos.line { - let width = ((end_pos.column as f32) * glyph_dim_rect.width) - - ((start_pos.column as f32) * glyph_dim_rect.width); - let sel_rect_x = line_start_x + ((start_pos.column as f32) * glyph_dim_rect.width); - - all_rects.push(Rect { - top_left_coords: (sel_rect_x, start_y).into(), - width, - height, - color: theme.select_highlight, - }); - } else { - // first line - let end_col = lines.line_len(start_pos.line)?; - let width = ((end_col as f32) * glyph_dim_rect.width) - - ((start_pos.column as f32) * glyph_dim_rect.width); - - let sel_rect_x = line_start_x + ((start_pos.column as f32) * glyph_dim_rect.width); - - all_rects.push(Rect { - top_left_coords: (sel_rect_x, start_y).into(), - width, - height, - color: theme.select_highlight, - }); - - //middle lines - let nr_mid_lines = (end_pos.line - start_pos.line) - 1; - let first_mid_line = start_pos.line + 1; - - for i in first_mid_line..(first_mid_line + nr_mid_lines) { - let mid_line_len = lines.line_len(i)?; - - let width = (mid_line_len as f32) * glyph_dim_rect.width; - - let sel_rect_y = start_y + ((i - start_pos.line) as f32) * glyph_dim_rect.height; - - all_rects.push(Rect { - top_left_coords: (line_start_x, sel_rect_y).into(), - width, - height, - color: theme.select_highlight, - }); - } - - //last line - if end_pos.column > 0 { - let sel_rect_y = - start_y + ((end_pos.line - start_pos.line) as f32) * glyph_dim_rect.height; - - let width = (end_pos.column as f32) * glyph_dim_rect.width; - - all_rects.push(Rect { - top_left_coords: (line_start_x, sel_rect_y).into(), - width, - height, - color: theme.select_highlight, - }); - } - } - - Ok(all_rects) -} diff --git a/editor/src/ui/text/text_buffer.rs b/editor/src/ui/text/text_buffer.rs deleted file mode 100644 index b846acb391..0000000000 --- a/editor/src/ui/text/text_buffer.rs +++ /dev/null @@ -1,193 +0,0 @@ -use std::path::Path; - -use crate::ui::{ - ui_error::{OutOfBounds, TextBufReadFailed, UIResult}, - util::{path_to_string, reader_from_path}, -}; -use snafu::ensure; - -use super::{selection::Selection, text_pos::TextPos}; -use std::io::BufRead; - -// Do not use for large amounts of text. -// This should become a trait in the future and be implemented by a SmallTextBuffer and Rope(for large amounts of text) -#[derive(Debug)] -pub struct TextBuffer { - pub lines: Vec, -} - -impl TextBuffer { - pub fn from_path(path: &Path) -> UIResult { - let buf_reader = reader_from_path(path)?; - let mut lines: Vec = Vec::new(); - - for line in buf_reader.lines() { - match line { - Ok(line_str) => lines.push(line_str), - Err(e) => { - TextBufReadFailed { - path_str: path_to_string(path), - err_msg: e.to_string(), - } - .fail()?; - } - } - } - - Ok(TextBuffer { lines }) - } - - pub fn nr_of_chars(&self) -> usize { - let mut nr_of_chars = 0; - - for line in self.lines.iter() { - nr_of_chars += line.len(); - } - - nr_of_chars - } - - pub fn nr_of_lines(&self) -> usize { - self.lines.len() - } - - pub fn get_line_ref(&self, line_nr: usize) -> UIResult<&str> { - self.ensure_bounds(line_nr)?; - // safe unwrap because we checked the length - Ok(self.lines.get(line_nr).unwrap()) - } - - pub fn line_len(&self, line_nr: usize) -> UIResult { - Ok(self.get_line_ref(line_nr)?.len()) - } - - fn ensure_bounds(&self, line_nr: usize) -> UIResult<()> { - ensure!( - line_nr < self.nr_of_lines(), - OutOfBounds { - index: line_nr, - collection_name: "TextBuffer", - len: self.nr_of_lines(), - } - ); - - Ok(()) - } - - fn ensure_bounds_txt_pos(&self, txt_pos: TextPos) -> UIResult<()> { - ensure!( - txt_pos.line < self.nr_of_lines(), - OutOfBounds { - index: txt_pos.line, - collection_name: "TextBuffer", - len: self.nr_of_lines(), - } - ); - - let line_ref = self.get_line_ref(txt_pos.line)?; - let line_len = line_ref.len(); - - ensure!( - txt_pos.column <= line_len, - OutOfBounds { - index: txt_pos.column, - collection_name: format!("Line in TextBuffer: {}", line_ref), - len: line_len, - } - ); - - Ok(()) - } - - pub fn all_lines_ref(&self) -> &[String] { - &self.lines - } - - pub fn get_selected_str(&self, selection: Selection) -> UIResult { - let start_line_nr = selection.start_pos.line; - let start_col_nr = selection.start_pos.column; - - let end_line_nr = selection.end_pos.line; - let end_col_nr = selection.end_pos.column; - - let mut selected_str = String::new(); - - if end_line_nr > start_line_nr { - selected_str.push_str(&self.get_line_ref(start_line_nr)?[start_col_nr..]); - - for line_nr in start_line_nr + 1..end_line_nr - 1 { - selected_str.push_str(self.get_line_ref(line_nr)?); - } - - selected_str.push_str(&self.get_line_ref(end_line_nr)?[..end_col_nr]); - } else { - // start_line_nr == end_line_nr - selected_str.push_str(&self.get_line_ref(start_line_nr)?[start_col_nr..end_col_nr]); - } - - Ok(selected_str) - } - - pub fn insert_str(&mut self, txt_pos: TextPos, new_str: &str) -> UIResult<()> { - self.ensure_bounds_txt_pos(txt_pos)?; - - // safe unwrap because we checked the length - self.lines - .get_mut(txt_pos.line) - .unwrap() - .insert_str(txt_pos.column, new_str); - - Ok(()) - } - - pub fn backspace_char(&mut self, txt_pos: TextPos) -> UIResult<()> { - if txt_pos.column > 0 { - let prev_col_pos = TextPos { - line: txt_pos.line, - column: txt_pos.column - 1, - }; - - self.ensure_bounds_txt_pos(prev_col_pos)?; - - let line_ref = self.lines.get_mut(prev_col_pos.line).unwrap(); // safe because of earlier bounds check - - line_ref.remove(prev_col_pos.column); - } else if txt_pos.line > 0 { - self.lines.remove(txt_pos.line); - } - - Ok(()) - } - - pub fn del_selection(&mut self, selection: Selection) -> UIResult<()> { - self.ensure_bounds_txt_pos(selection.start_pos)?; - self.ensure_bounds_txt_pos(selection.end_pos)?; - - let start_line_nr = selection.start_pos.line; - let start_col_nr = selection.start_pos.column; - let end_line_nr = selection.end_pos.line; - let end_col_nr = selection.end_pos.column; - - if end_line_nr > start_line_nr { - // remove in reverse to prevent shifting indices - if end_col_nr == self.line_len(end_line_nr)? { - self.lines.remove(end_line_nr); - } else { - let line_ref = self.lines.get_mut(end_line_nr).unwrap(); // safe because of earlier bounds check - line_ref.replace_range(..end_col_nr, ""); - } - - self.lines.drain(start_line_nr + 1..end_line_nr); - - let line_ref = self.lines.get_mut(start_line_nr).unwrap(); // safe because of earlier bounds check - line_ref.replace_range(start_col_nr.., "") - } else { - // selection.end_pos.line == selection.start_pos.line - let line_ref = self.lines.get_mut(selection.start_pos.line).unwrap(); // safe because of earlier bounds check - - line_ref.replace_range(selection.start_pos.column..selection.end_pos.column, "") - } - - Ok(()) - } -} diff --git a/editor/src/ui/text/text_pos.rs b/editor/src/ui/text/text_pos.rs deleted file mode 100644 index 894fb9c568..0000000000 --- a/editor/src/ui/text/text_pos.rs +++ /dev/null @@ -1,49 +0,0 @@ -use std::cmp::Ordering; - -#[derive(Debug, Copy, Clone)] -pub struct TextPos { - pub line: usize, - pub column: usize, -} - -impl TextPos { - pub fn increment_col(&self) -> TextPos { - TextPos { - line: self.line, - column: self.column + 1, - } - } - - pub fn decrement_col(&self) -> TextPos { - let new_col = if self.column > 0 { - self.column - 1 - } else { - self.column - }; - - TextPos { - line: self.line, - column: new_col, - } - } -} - -impl Ord for TextPos { - fn cmp(&self, other: &Self) -> Ordering { - (self.line, self.column).cmp(&(other.line, other.column)) - } -} - -impl PartialOrd for TextPos { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -impl PartialEq for TextPos { - fn eq(&self, other: &Self) -> bool { - (self.line, self.column) == (other.line, other.column) - } -} - -impl Eq for TextPos {} diff --git a/editor/src/ui/theme.rs b/editor/src/ui/theme.rs deleted file mode 100644 index 640c8daa6a..0000000000 --- a/editor/src/ui/theme.rs +++ /dev/null @@ -1,34 +0,0 @@ -use gr_colors::{from_hsb, from_hsba, RgbaTup}; -use serde::{Deserialize, Serialize}; - -use crate::graphics::colors as gr_colors; - -pub const LIGHT_BRAND_COL: RgbaTup = (0.506, 0.337, 0.902, 1.0); // #8257e5 hsb(258, 62, 90) -pub const DARK_BRAND_COL: RgbaTup = (0.380, 0.169, 0.871, 1.0); // #612bde hsb(258, 81, 87) - -#[derive(Deserialize, Serialize)] -pub struct UITheme { - pub light_brand: RgbaTup, - pub dark_brand: RgbaTup, - pub text: RgbaTup, - pub caret: RgbaTup, - pub select_highlight: RgbaTup, - pub tooltip_bg: RgbaTup, - pub tooltip_text: RgbaTup, - pub default_font_size: f32, -} - -impl Default for UITheme { - fn default() -> Self { - Self { - light_brand: LIGHT_BRAND_COL, - dark_brand: DARK_BRAND_COL, - text: gr_colors::WHITE, - caret: gr_colors::WHITE, - select_highlight: from_hsba(240, 55, 100, 0.3), - tooltip_bg: from_hsb(240, 60, 50), - tooltip_text: gr_colors::WHITE, - default_font_size: 30.0, - } - } -} diff --git a/editor/src/ui/tooltip.rs b/editor/src/ui/tooltip.rs deleted file mode 100644 index 2a488b0ccd..0000000000 --- a/editor/src/ui/tooltip.rs +++ /dev/null @@ -1,87 +0,0 @@ -use crate::graphics::primitives::rect::Rect; -use crate::graphics::primitives::text as gr_text; -use crate::graphics::primitives::text::Text; -use crate::ui::theme::UITheme; - -pub struct ToolTip<'a> { - pub position_x: f32, - pub position_y: f32, - pub text: &'a str, -} - -impl<'a> ToolTip<'a> { - fn make_tooltip_rect( - &self, - width: f32, - height: f32, - height_padding: f32, - y_margin: f32, - ui_theme: &UITheme, - ) -> Rect { - Rect { - top_left_coords: ( - self.position_x, - self.position_y - (height_padding + y_margin), - ) - .into(), - height: height + height_padding, - width, - color: ui_theme.tooltip_bg, - } - } - - fn make_tooltip_text<'b>( - &'b self, - x_offset: f32, - y_offset: f32, - y_margin: f32, - ui_theme: &UITheme, - code_font_size: f32, - ) -> Text<'b> { - Text { - position: ( - self.position_x + x_offset, - self.position_y - (y_offset + y_margin), - ) - .into(), - color: ui_theme.tooltip_text, - text: self.text, - size: code_font_size, - ..Default::default() - } - } - - pub fn render_tooltip( - &self, - glyph_dim_rect: &Rect, - ui_theme: &UITheme, - code_font_size: f32, - ) -> (Rect, glyph_brush::OwnedSection) { - let width_padding = glyph_dim_rect.height / 1.3; - let height_padding = width_padding / 1.3; - - let text_x_offset = width_padding / 2.0; - let text_y_offset = height_padding / 2.0; - - let y_margin = glyph_dim_rect.height / 4.0; - - let text = self.make_tooltip_text( - text_x_offset, - text_y_offset, - y_margin, - ui_theme, - code_font_size, - ); - let text_section = gr_text::owned_section_from_text(&text); - - let rect = self.make_tooltip_rect( - glyph_dim_rect.width * (text.text.len() as f32) + width_padding, - glyph_dim_rect.height, - height_padding, - y_margin, - ui_theme, - ); - - (rect, text_section) - } -} diff --git a/editor/src/ui/ui_error.rs b/editor/src/ui/ui_error.rs deleted file mode 100644 index 9f4d1a96b0..0000000000 --- a/editor/src/ui/ui_error.rs +++ /dev/null @@ -1,69 +0,0 @@ -use std::io; - -use snafu::{Backtrace, Snafu}; - -//import errors as follows: -// `use crate::error::OutOfBounds;` -// *not* `use crate::error::EdError::OutOfBounds;` -// see https://github.com/shepmaster/snafu/issues/211 - -#[derive(Debug, Snafu)] -#[snafu(visibility(pub))] -pub enum UIError { - #[snafu(display( - "LineInsertionFailed: line_nr ({}) needs to be <= nr_of_lines ({}).", - line_nr, - nr_of_lines - ))] - LineInsertionFailed { - line_nr: usize, - nr_of_lines: usize, - backtrace: Backtrace, - }, - #[snafu(display( - "OutOfBounds: index {} was out of bounds for {} with length {}.", - index, - collection_name, - len - ))] - OutOfBounds { - index: usize, - collection_name: String, - len: usize, - backtrace: Backtrace, - }, - - #[snafu(display("InvalidSelection: {}.", err_msg))] - InvalidSelection { - err_msg: String, - backtrace: Backtrace, - }, - - #[snafu(display( - "FileOpenFailed: failed to open file with path {} with the following error: {}.", - path_str, - err_msg - ))] - FileOpenFailed { path_str: String, err_msg: String }, - - #[snafu(display( - "FileWriteFailed: failed to write to file with path {}, I got this IO error: {}.", - path_str, - source - ))] - FileWriteFailed { source: io::Error, path_str: String }, - - #[snafu(display("TextBufReadFailed: the file {} could be opened but we encountered the following error while trying to read it: {}.", path_str, err_msg))] - TextBufReadFailed { path_str: String, err_msg: String }, - - #[snafu(display("MissingGlyphDims: glyph_dim_rect_opt was None. It needs to be set using the example_code_glyph_rect function."))] - MissingGlyphDims { backtrace: Backtrace }, -} - -pub type UIResult = std::result::Result; - -impl From for String { - fn from(ui_error: UIError) -> Self { - format!("{}", ui_error) - } -} diff --git a/editor/src/ui/util.rs b/editor/src/ui/util.rs deleted file mode 100644 index 97c51a0dd8..0000000000 --- a/editor/src/ui/util.rs +++ /dev/null @@ -1,59 +0,0 @@ -use super::ui_error::{FileOpenFailed, FileWriteFailed, OutOfBounds, UIResult}; -use snafu::{OptionExt, ResultExt}; -use std::{fs::File, io::BufReader, path::Path, slice::SliceIndex}; - -pub fn is_newline(char_ref: &char) -> bool { - let newline_codes = vec!['\u{d}', '\n']; - - newline_codes.contains(char_ref) -} - -// replace slice method that return Option with one that return Result and proper Error -pub fn slice_get(index: usize, slice: &[T]) -> UIResult<&>::Output> { - let elt_ref = slice.get(index).context(OutOfBounds { - index, - collection_name: "Slice", - len: slice.len(), - })?; - - Ok(elt_ref) -} - -pub fn slice_get_mut( - index: usize, - slice: &mut [T], -) -> UIResult<&mut >::Output> { - let slice_len = slice.len(); - - let elt_ref = slice.get_mut(index).context(OutOfBounds { - index, - collection_name: "Slice", - len: slice_len, - })?; - - Ok(elt_ref) -} - -pub fn reader_from_path(path: &Path) -> UIResult> { - match File::open(path) { - Ok(file) => Ok(BufReader::new(file)), - Err(e) => FileOpenFailed { - path_str: path_to_string(path), - err_msg: e.to_string(), - } - .fail()?, - } -} - -pub fn path_to_string(path: &Path) -> String { - let mut path_str = String::new(); - path_str.push_str(&path.to_string_lossy()); - - path_str -} - -pub fn write_to_file(path: &Path, content: &str) -> UIResult<()> { - std::fs::write(path, content).with_context(|| FileWriteFailed { - path_str: path_to_string(path), - }) -} diff --git a/editor/src/window/keyboard_input.rs b/editor/src/window/keyboard_input.rs deleted file mode 100644 index 1f06293d1c..0000000000 --- a/editor/src/window/keyboard_input.rs +++ /dev/null @@ -1,74 +0,0 @@ -// note: the Default is that these are all False -#[derive(Debug, Default)] -pub struct Modifiers { - pub shift: bool, - pub ctrl: bool, - pub alt: bool, - pub logo: bool, -} - -impl Modifiers { - pub fn cmd_or_ctrl(&self) -> bool { - #[cfg(target_os = "macos")] - let active = self.logo; - - #[cfg(not(target_os = "macos"))] - let active = self.ctrl; - - active - } - - // returns true if modifiers are active that can be active when the user wants to insert a new char; e.g.: shift+a to make A - pub fn new_char_modifiers(&self) -> bool { - self.no_modifiers() - || (self.shift && !self.ctrl && !self.alt && !self.logo) // e.g.: shift+a to make A - || (self.cmd_or_ctrl() && self.alt) // e.g.: ctrl+alt+2 to make @ on azerty keyboard - } - - fn no_modifiers(&self) -> bool { - !self.shift && !self.ctrl && !self.alt && !self.logo - } -} - -pub fn no_mods() -> Modifiers { - Modifiers { - shift: false, - ctrl: false, - alt: false, - logo: false, - } -} - -pub fn from_winit(winit_mods: &winit::event::ModifiersState) -> Modifiers { - Modifiers { - shift: winit_mods.shift(), - ctrl: winit_mods.ctrl(), - alt: winit_mods.alt(), - logo: winit_mods.logo(), - } -} - -#[cfg(test)] -pub mod test_modifiers { - use crate::window::keyboard_input::Modifiers; - - pub fn ctrl_cmd_shift() -> Modifiers { - #[cfg(target_os = "macos")] - let mods = Modifiers { - shift: true, - ctrl: false, - alt: false, - logo: true, - }; - - #[cfg(not(target_os = "macos"))] - let mods = Modifiers { - shift: true, - ctrl: true, - alt: false, - logo: false, - }; - - mods - } -} diff --git a/editor/src/window/mod.rs b/editor/src/window/mod.rs deleted file mode 100644 index 939d83aa33..0000000000 --- a/editor/src/window/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod keyboard_input; diff --git a/editor/tests/README.md b/editor/tests/README.md deleted file mode 100644 index 2e0697587d..0000000000 --- a/editor/tests/README.md +++ /dev/null @@ -1,5 +0,0 @@ - -# Where are the tests? - -We have a lot of tests at the end of source files, this allows us to test functions that are not exposed by the editor itself. -`editor/mvc/ed_update.rs` and `editor/ui/text/big_text_area.rs` have many important tests. \ No newline at end of file diff --git a/editor/tests/modules/SimpleUnformatted.roc b/editor/tests/modules/SimpleUnformatted.roc deleted file mode 100644 index 0b083ca390..0000000000 --- a/editor/tests/modules/SimpleUnformatted.roc +++ /dev/null @@ -1,26 +0,0 @@ -interface Simple - exposes [ - v, x - ] - imports [] - - - - - - - - - -v : Str - - - - - -v = "Value!" - - - -x : Int -x = 4 \ No newline at end of file diff --git a/editor/tests/selection.pest b/editor/tests/selection.pest deleted file mode 100644 index 6c70079a54..0000000000 --- a/editor/tests/selection.pest +++ /dev/null @@ -1,11 +0,0 @@ -text = { (ASCII_ALPHANUMERIC | " " | "\t" | "\n" | "{" | "}" | "," | "." | "[" | "]" | ":" | "<" | ">" | "-" | "\"" | "=" )* } - -caret = {"┃"} - -optSelStart = { "❮"{0,1} } - -optSelEnd = { "❯"{0,1} } - -optCaret = { caret{0,1} } - -linesWithSelect = { SOI ~ text ~ optCaret ~ text ~ optSelStart ~ text ~ optCaret ~ text ~ optCaret ~ text ~ optSelEnd ~ text ~ optCaret ~ text ~ EOI} diff --git a/error_macros/Cargo.toml b/error_macros/Cargo.toml deleted file mode 100644 index 83744ab39d..0000000000 --- a/error_macros/Cargo.toml +++ /dev/null @@ -1,8 +0,0 @@ -[package] -name = "roc_error_macros" -version = "0.1.0" -authors = ["The Roc Contributors"] -license = "UPL-1.0" -edition = "2021" - -[dependencies] diff --git a/error_macros/src/lib.rs b/error_macros/src/lib.rs deleted file mode 100644 index 679af370b9..0000000000 --- a/error_macros/src/lib.rs +++ /dev/null @@ -1,128 +0,0 @@ -/// `internal_error!` should be used whenever a compiler invariant is broken. -/// It is a wrapper around panic that tells the user to file a bug. -/// This should only be used in cases where there would be a compiler bug and the user can't fix it. -/// If there is simply an unimplemented feature, please use `unimplemented!` -/// If there is a user error, please use roc_reporting to print a nice error message. -#[macro_export] -macro_rules! internal_error { - ($($arg:tt)*) => ({ - eprintln!("An internal compiler expectation was broken."); - eprintln!("This is definitely a compiler bug."); - // TODO: update this to the new bug template. - eprintln!("Please file an issue here: https://github.com/rtfeldman/roc/issues/new/choose"); - #[allow(clippy::panic)] { - panic!($($arg)*); - } - }) -} - -/// `user_error!` should only ever be used temporarily. -/// It is a way to document locations where we do not yet have nice error reporting. -/// All cases of `user_error!` should eventually be replaced with pretty error printing using roc_reporting. -#[macro_export] -macro_rules! user_error { - ($($arg:tt)*) => ({ - eprintln!("We ran into an issue while compiling your code."); - eprintln!("Sadly, we don't havs a pretty error message for this case yet."); - eprintln!("If you can't figure out the problem from the context below, please reach out at: https://roc.zulipchat.com/"); - eprintln!($($arg)*); - std::process::exit(1); - }) -} - -/// Assert that a type has the expected size on ARM -#[macro_export] -macro_rules! assert_sizeof_aarch64 { - ($t: ty, $expected_size: expr) => { - #[cfg(target_arch = "aarch64")] - static_assertions::assert_eq_size!($t, [u8; $expected_size]); - }; -} - -/// Assert that a type has the expected size in Wasm -#[macro_export] -macro_rules! assert_sizeof_wasm { - ($t: ty, $expected_size: expr) => { - #[cfg(target_family = "wasm")] - static_assertions::assert_eq_size!($t, [u8; $expected_size]); - }; -} - -/// Assert that a type has the expected size on any target not covered above -/// In practice we use this for x86_64, and add specific macros for other platforms -#[macro_export] -macro_rules! assert_sizeof_default { - ($t: ty, $expected_size: expr) => { - #[cfg(not(any(target_family = "wasm", target_arch = "aarch64")))] - static_assertions::assert_eq_size!($t, [u8; $expected_size]); - }; -} - -/// Assert that a type has the expected size on all targets -#[macro_export] -macro_rules! assert_sizeof_all { - ($t: ty, $expected_size: expr) => { - static_assertions::assert_eq_size!($t, [u8; $expected_size]); - }; -} - -/// Assert that a type has the expected size on all targets except wasm -#[macro_export] -macro_rules! assert_sizeof_non_wasm { - ($t: ty, $expected_size: expr) => { - #[cfg(not(target_family = "wasm"))] - static_assertions::assert_eq_size!($t, [u8; $expected_size]); - }; -} - -/// Assert that a type has `Copy` -#[macro_export] -macro_rules! assert_copyable { - ($t: ty) => { - static_assertions::assert_impl_all!($t: Copy); - }; -} - -// LARGE SCALE PROJECTS -// -// This section is for "todo!"-style macros enabled in sections where large-scale changes to the -// language are in progress. - -#[macro_export] -macro_rules! _incomplete_project { - ($project_name:literal, $tracking_issue_no:literal) => { - panic!( - "[{}] not yet implemented. Tracking issue: https://github.com/rtfeldman/roc/issues/{}", - $project_name, $tracking_issue_no, - ) - }; - ($project_name:literal, $tracking_issue_no:literal, $($arg:tt)+) => { - panic!( - "[{}] not yet implemented. Tracking issue: https://github.com/rtfeldman/roc/issues/{}.\nAdditional information: {}", - $project_name, $tracking_issue_no, - format_args!($($arg)+), - ) - }; -} - -#[macro_export] -macro_rules! todo_abilities { - () => { - $crate::_incomplete_project!("Abilities", 2463) - }; - ($($arg:tt)+) => { - $crate::_incomplete_project!("Abilities", 2463, $($arg)+) - }; -} - -#[macro_export] -macro_rules! todo_opaques { - () => { - $crate::_incomplete_project!("Abilities (opaques)", 2463) - }; - ($($arg:tt)+) => { - $crate::_incomplete_project!("Abilities (opaques)", 2463, $($arg)+) - }; -} - -// END LARGE SCALE PROJECTS diff --git a/examples/.gitignore b/examples/.gitignore index 58cb449bb9..05228d9166 100644 --- a/examples/.gitignore +++ b/examples/.gitignore @@ -2,5 +2,17 @@ libhost.a libapp.so dynhost -preprocessedhost -metadata +*.rm +*.rh + +helloWorld +helloWorldNoURL +inspect-gui +inspect-logging +swiftui +parser/examples/example +gui/hello-gui +gui/breakout/breakout +gui/breakout/hello-gui +webserver/echo +webserver/http \ No newline at end of file diff --git a/examples/Community.roc b/examples/Community.roc new file mode 100644 index 0000000000..8a79d3dd44 --- /dev/null +++ b/examples/Community.roc @@ -0,0 +1,84 @@ +interface Community + exposes [ + Community, + empty, + addPerson, + addFriend, + Person, + walkFriendNames, + ] + imports [] + +## Datatype representing a community for demonstration purposes in inspect-gui.roc and inspect-logging.roc + +Community := { + people : List Person, + friends : List (Set Nat), +} + implements [Inspect] + +Person := { + firstName : Str, + lastName : Str, + age : U8, + hasBeard : Bool, + favoriteColor : Color, +} + implements [Inspect] + +Color : [ + Red, + Green, + Blue, + RGB (U8, U8, U8), +] + +empty = @Community { people: [], friends: [] } + +addPerson = \@Community { people, friends }, person -> + @Community { + people: List.append people (@Person person), + friends: List.append friends (Set.empty {}), + } + +addFriend = \@Community { people, friends }, from, to -> + when (List.get friends from, List.get friends to) is + (Ok fromSet, Ok toSet) -> + @Community { + people, + friends: friends + |> List.set from (Set.insert fromSet to) + |> List.set to (Set.insert toSet from), + } + + _ -> + @Community { people, friends } + +walkFriendNames : Community, state, (state, Str, Set Str -> state) -> state +walkFriendNames = \@Community { people, friends }, s0, nextFn -> + (out, _) = + (s1, id), friendSet <- List.walk friends (s0, 0) + (@Person person) = + when List.get people id is + Ok v -> v + Err _ -> crash "Unknown Person" + personName = + person.firstName + |> Str.concat " " + |> Str.concat person.lastName + + friendNames = + friendsSet, friendId <- Set.walk friendSet (Set.empty {}) + (@Person friend) = + when List.get people friendId is + Ok v -> v + Err _ -> crash "Unknown Person" + friendName = + friend.firstName + |> Str.concat " " + |> Str.concat friend.lastName + Set.insert friendsSet friendName + + (nextFn s1 personName friendNames, id + 1) + out + diff --git a/examples/GuiFormatter.roc b/examples/GuiFormatter.roc new file mode 100644 index 0000000000..1a060911a1 --- /dev/null +++ b/examples/GuiFormatter.roc @@ -0,0 +1,239 @@ +interface GuiFormatter + exposes [ + GuiFormatter, + toGui, + ] + imports [] + +## Creates GUI representations of Roc values, for use in inspect-gui.roc + +## This can't depend on the platform, so I just copied all of this. + +Rgba : { r : F32, g : F32, b : F32, a : F32 } +ButtonStyles : { bgColor : Rgba, borderColor : Rgba, borderWidth : F32, textColor : Rgba } +Elem : [Button Elem ButtonStyles, Col (List Elem), Row (List Elem), Text Str] + +GuiFormatter := { nodes : List Elem } + implements [ + InspectFormatter { + init: init, + list: list, + set: set, + dict: dict, + tag: tag, + tuple: tuple, + record: record, + bool: bool, + str: str, + function: function, + opaque: opaque, + u8: u8, + i8: i8, + u16: u16, + i16: i16, + u32: u32, + i32: i32, + u64: u64, + i64: i64, + u128: u128, + i128: i128, + nat: nat, + f32: f32, + f64: f64, + dec: dec, + + }, + ] + +init : {} -> GuiFormatter +init = \{} -> @GuiFormatter { nodes: [] } + +list : list, ElemWalker GuiFormatter list elem, (elem -> Inspector GuiFormatter) -> Inspector GuiFormatter +list = \content, walkFn, toInspector -> + f0 <- Inspect.custom + # Use a temporary buffer for the children nodes + (@GuiFormatter { nodes }) = + init {} + |> \f1 -> + f2, elem <- walkFn content f1 + elem + |> toInspector + |> Inspect.apply f2 + + addNode f0 (Col nodes) + +set : set, ElemWalker GuiFormatter set elem, (elem -> Inspector GuiFormatter) -> Inspector GuiFormatter +set = \content, walkFn, toInspector -> + f0 <- Inspect.custom + # Use a temporary buffer for the children nodes + (@GuiFormatter { nodes }) = + init {} + |> \f1 -> + f2, elem <- walkFn content f1 + elem + |> toInspector + |> Inspect.apply f2 + + addNode f0 (Col nodes) + +dict : dict, KeyValWalker GuiFormatter dict key value, (key -> Inspector GuiFormatter), (value -> Inspector GuiFormatter) -> Inspector GuiFormatter +dict = \d, walkFn, keyToInspector, valueToInspector -> + f0 <- Inspect.custom + # Use a temporary buffer for the children nodes + (@GuiFormatter { nodes }) = + init {} + |> \f1 -> + f2, key, value <- walkFn d f1 + (@GuiFormatter { nodes: innerNodes }) = + init {} + |> \x -> Inspect.apply (keyToInspector key) x + |> addNode (Text ":") + |> \x -> Inspect.apply (valueToInspector value) x + + addNode f2 (Row innerNodes) + + addNode f0 (Col nodes) + +tag : Str, List (Inspector GuiFormatter) -> Inspector GuiFormatter +tag = \name, fields -> + f0 <- Inspect.custom + # Use a temporary buffer for the children nodes + (@GuiFormatter { nodes }) = + init {} + |> addNode (Text name) + |> \f1 -> + f2, fieldInspector <- List.walk fields f1 + Inspect.apply fieldInspector f2 + + addNode f0 (Row nodes) + +tuple : List (Inspector GuiFormatter) -> Inspector GuiFormatter +tuple = \fields -> + f0 <- Inspect.custom + # Use a temporary buffer for the children nodes + (@GuiFormatter { nodes }) = + init {} + |> \f1 -> + f2, fieldInspector <- List.walk fields f1 + Inspect.apply fieldInspector f2 + + addNode f0 (Row nodes) + +record : List { key : Str, value : Inspector GuiFormatter } -> Inspector GuiFormatter +record = \fields -> + f0 <- Inspect.custom + # Use a temporary buffer for the children nodes + (@GuiFormatter { nodes }) = + init {} + |> \f1 -> + f2, { key, value } <- List.walk fields f1 + (@GuiFormatter { nodes: innerNodes }) = + init {} + |> addNode (Text key) + |> addNode (Text ":") + |> \x -> Inspect.apply value x + + addNode f2 (Row innerNodes) + + addNode f0 (Col nodes) + +bool : Bool -> Inspector GuiFormatter +bool = \b -> + if b then + f0 <- Inspect.custom + addNode f0 (Text "true") + else + f0 <- Inspect.custom + addNode f0 (Text "false") + +str : Str -> Inspector GuiFormatter +str = \s -> + f0 <- Inspect.custom + addNode f0 (Text "\"\(s)\"") + +opaque : * -> Inspector GuiFormatter +opaque = \_ -> + f0 <- Inspect.custom + addNode f0 (Text "") + +function : * -> Inspector GuiFormatter +function = \_ -> + f0 <- Inspect.custom + addNode f0 (Text "") + +u8 : U8 -> Inspector GuiFormatter +u8 = \num -> + f0 <- Inspect.custom + addNode f0 (num |> Num.toStr |> Text) + +i8 : I8 -> Inspector GuiFormatter +i8 = \num -> + f0 <- Inspect.custom + addNode f0 (num |> Num.toStr |> Text) + +u16 : U16 -> Inspector GuiFormatter +u16 = \num -> + f0 <- Inspect.custom + addNode f0 (num |> Num.toStr |> Text) + +i16 : I16 -> Inspector GuiFormatter +i16 = \num -> + f0 <- Inspect.custom + addNode f0 (num |> Num.toStr |> Text) + +u32 : U32 -> Inspector GuiFormatter +u32 = \num -> + f0 <- Inspect.custom + addNode f0 (num |> Num.toStr |> Text) + +i32 : I32 -> Inspector GuiFormatter +i32 = \num -> + f0 <- Inspect.custom + addNode f0 (num |> Num.toStr |> Text) + +u64 : U64 -> Inspector GuiFormatter +u64 = \num -> + f0 <- Inspect.custom + addNode f0 (num |> Num.toStr |> Text) + +i64 : I64 -> Inspector GuiFormatter +i64 = \num -> + f0 <- Inspect.custom + addNode f0 (num |> Num.toStr |> Text) + +u128 : U128 -> Inspector GuiFormatter +u128 = \num -> + f0 <- Inspect.custom + addNode f0 (num |> Num.toStr |> Text) + +i128 : I128 -> Inspector GuiFormatter +i128 = \num -> + f0 <- Inspect.custom + addNode f0 (num |> Num.toStr |> Text) + +nat : Nat -> Inspector GuiFormatter +nat = \num -> + f0 <- Inspect.custom + addNode f0 (num |> Num.toStr |> Text) + +f32 : F32 -> Inspector GuiFormatter +f32 = \num -> + f0 <- Inspect.custom + addNode f0 (num |> Num.toStr |> Text) + +f64 : F64 -> Inspector GuiFormatter +f64 = \num -> + f0 <- Inspect.custom + addNode f0 (num |> Num.toStr |> Text) + +dec : Dec -> Inspector GuiFormatter +dec = \num -> + f0 <- Inspect.custom + addNode f0 (num |> Num.toStr |> Text) + +addNode : GuiFormatter, Elem -> GuiFormatter +addNode = \@GuiFormatter { nodes }, node -> + @GuiFormatter { nodes: List.append nodes node } + +toGui : GuiFormatter -> Elem +toGui = \@GuiFormatter { nodes } -> Col nodes diff --git a/examples/README.md b/examples/README.md index 9399ba7fed..6f2c426009 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,20 +1,9 @@ # Examples -Took a look around in this folder; `examples/benchmarks/` contains some larger examples. +To run examples: -Run examples as follows: +```bash +roc run examples/hello-world/main.roc +``` -1. Navigate to the examples directory - - ```bash - cd examples - ``` - -2. Run "Hello, World!" example - - ```bash - cargo run hello-world/helloWorld.roc - ``` - -Some examples like `examples/benchmarks/NQueens.roc` require input after running. -For NQueens, input 10 in the terminal and press enter. +[More examples](https://github.com/roc-lang/examples) diff --git a/examples/algorithms/fibonacci-platform/host.zig b/examples/algorithms/fibonacci-platform/host.zig deleted file mode 100644 index 55f55e2658..0000000000 --- a/examples/algorithms/fibonacci-platform/host.zig +++ /dev/null @@ -1,109 +0,0 @@ -const std = @import("std"); -const testing = std.testing; -const expectEqual = testing.expectEqual; -const expect = testing.expect; -const maxInt = std.math.maxInt; - -comptime { - // This is a workaround for https://github.com/ziglang/zig/issues/8218 - // which is only necessary on macOS. - // - // Once that issue is fixed, we can undo the changes in - // 177cf12e0555147faa4d436e52fc15175c2c4ff0 and go back to passing - // -fcompiler-rt in link.rs instead of doing this. Note that this - // workaround is present in many host.zig files, so make sure to undo - // it everywhere! - const builtin = @import("builtin"); - if (builtin.os.tag == .macos) { - _ = @import("compiler_rt"); - } -} - -const mem = std.mem; -const Allocator = mem.Allocator; - -// NOTE the LLVM backend expects this signature -// extern fn roc__mainForHost_1_exposed(i64, *i64) void; -extern fn roc__mainForHost_1_exposed(i64) i64; - -const Align = 2 * @alignOf(usize); -extern fn malloc(size: usize) callconv(.C) ?*align(Align) anyopaque; -extern fn realloc(c_ptr: [*]align(Align) u8, size: usize) callconv(.C) ?*anyopaque; -extern fn free(c_ptr: [*]align(Align) u8) callconv(.C) void; -extern fn memcpy(dst: [*]u8, src: [*]u8, size: usize) callconv(.C) void; -extern fn memset(dst: [*]u8, value: i32, size: usize) callconv(.C) void; - -const DEBUG: bool = false; - -export fn roc_alloc(size: usize, alignment: u32) callconv(.C) ?*anyopaque { - if (DEBUG) { - var ptr = malloc(size); - const stdout = std.io.getStdOut().writer(); - stdout.print("alloc: {d} (alignment {d}, size {d})\n", .{ ptr, alignment, size }) catch unreachable; - return ptr; - } else { - return malloc(size); - } -} - -export fn roc_realloc(c_ptr: *anyopaque, new_size: usize, old_size: usize, alignment: u32) callconv(.C) ?*anyopaque { - if (DEBUG) { - const stdout = std.io.getStdOut().writer(); - stdout.print("realloc: {d} (alignment {d}, old_size {d})\n", .{ c_ptr, alignment, old_size }) catch unreachable; - } - - return realloc(@alignCast(Align, @ptrCast([*]u8, c_ptr)), new_size); -} - -export fn roc_dealloc(c_ptr: *anyopaque, alignment: u32) callconv(.C) void { - if (DEBUG) { - const stdout = std.io.getStdOut().writer(); - stdout.print("dealloc: {d} (alignment {d})\n", .{ c_ptr, alignment }) catch unreachable; - } - - free(@alignCast(Align, @ptrCast([*]u8, c_ptr))); -} - -export fn roc_panic(c_ptr: *anyopaque, tag_id: u32) callconv(.C) void { - _ = tag_id; - - const stderr = std.io.getStdErr().writer(); - const msg = @ptrCast([*:0]const u8, c_ptr); - stderr.print("Application crashed with message\n\n {s}\n\nShutting down\n", .{msg}) catch unreachable; - std.process.exit(0); -} - -export fn roc_memcpy(dst: [*]u8, src: [*]u8, size: usize) callconv(.C) void { - return memcpy(dst, src, size); -} - -export fn roc_memset(dst: [*]u8, value: i32, size: usize) callconv(.C) void { - return memset(dst, value, size); -} - -pub export fn main() u8 { - const stdout = std.io.getStdOut().writer(); - const stderr = std.io.getStdErr().writer(); - - // start time - var ts1: std.os.timespec = undefined; - std.os.clock_gettime(std.os.CLOCK.REALTIME, &ts1) catch unreachable; - - const result = roc__mainForHost_1_exposed(10); - - // end time - var ts2: std.os.timespec = undefined; - std.os.clock_gettime(std.os.CLOCK.REALTIME, &ts2) catch unreachable; - - stdout.print("{d}\n", .{result}) catch unreachable; - - const delta = to_seconds(ts2) - to_seconds(ts1); - - stderr.print("runtime: {d:.3}ms\n", .{delta * 1000}) catch unreachable; - - return 0; -} - -fn to_seconds(tms: std.os.timespec) f64 { - return @intToFloat(f64, tms.tv_sec) + (@intToFloat(f64, tms.tv_nsec) / 1_000_000_000.0); -} diff --git a/examples/algorithms/quicksort-platform/host.zig b/examples/algorithms/quicksort-platform/host.zig deleted file mode 100644 index a7f7f6b9b1..0000000000 --- a/examples/algorithms/quicksort-platform/host.zig +++ /dev/null @@ -1,143 +0,0 @@ -const std = @import("std"); -const builtin = @import("builtin"); -const str = @import("str"); -const RocStr = str.RocStr; -const testing = std.testing; -const expectEqual = testing.expectEqual; -const expect = testing.expect; - -comptime { - // This is a workaround for https://github.com/ziglang/zig/issues/8218 - // which is only necessary on macOS. - // - // Once that issue is fixed, we can undo the changes in - // 177cf12e0555147faa4d436e52fc15175c2c4ff0 and go back to passing - // -fcompiler-rt in link.rs instead of doing this. Note that this - // workaround is present in many host.zig files, so make sure to undo - // it everywhere! - if (builtin.os.tag == .macos) { - _ = @import("compiler_rt"); - } -} - -const mem = std.mem; -const Allocator = mem.Allocator; - -extern fn roc__mainForHost_1_exposed_generic(output: *RocList, input: *RocList) void; - -const Align = 2 * @alignOf(usize); -extern fn malloc(size: usize) callconv(.C) ?*align(Align) anyopaque; -extern fn realloc(c_ptr: [*]align(Align) u8, size: usize) callconv(.C) ?*anyopaque; -extern fn free(c_ptr: [*]align(Align) u8) callconv(.C) void; -extern fn memcpy(dst: [*]u8, src: [*]u8, size: usize) callconv(.C) void; -extern fn memset(dst: [*]u8, value: i32, size: usize) callconv(.C) void; - -const DEBUG: bool = false; - -export fn roc_alloc(size: usize, alignment: u32) callconv(.C) ?*anyopaque { - if (DEBUG) { - var ptr = malloc(size); - const stdout = std.io.getStdOut().writer(); - stdout.print("alloc: {d} (alignment {d}, size {d})\n", .{ ptr, alignment, size }) catch unreachable; - return ptr; - } else { - return malloc(size); - } -} - -export fn roc_realloc(c_ptr: *anyopaque, new_size: usize, old_size: usize, alignment: u32) callconv(.C) ?*anyopaque { - if (DEBUG) { - const stdout = std.io.getStdOut().writer(); - stdout.print("realloc: {d} (alignment {d}, old_size {d})\n", .{ c_ptr, alignment, old_size }) catch unreachable; - } - - return realloc(@alignCast(Align, @ptrCast([*]u8, c_ptr)), new_size); -} - -export fn roc_dealloc(c_ptr: *anyopaque, alignment: u32) callconv(.C) void { - if (DEBUG) { - const stdout = std.io.getStdOut().writer(); - stdout.print("dealloc: {d} (alignment {d})\n", .{ c_ptr, alignment }) catch unreachable; - } - - free(@alignCast(Align, @ptrCast([*]u8, c_ptr))); -} - -export fn roc_panic(c_ptr: *anyopaque, tag_id: u32) callconv(.C) void { - _ = tag_id; - - const stderr = std.io.getStdErr().writer(); - const msg = @ptrCast([*:0]const u8, c_ptr); - stderr.print("Application crashed with message\n\n {s}\n\nShutting down\n", .{msg}) catch unreachable; - std.process.exit(0); -} - -export fn roc_memcpy(dst: [*]u8, src: [*]u8, size: usize) callconv(.C) void { - return memcpy(dst, src, size); -} - -export fn roc_memset(dst: [*]u8, value: i32, size: usize) callconv(.C) void { - return memset(dst, value, size); -} - -// warning! the array is currently stack-allocated so don't make this too big -const NUM_NUMS = 100; - -const RocList = extern struct { elements: [*]i64, length: usize }; - -const Unit = extern struct {}; - -pub export fn main() u8 { - const stdout = std.io.getStdOut().writer(); - - var raw_numbers: [NUM_NUMS + 1]i64 = undefined; - - // set refcount to one - raw_numbers[0] = -9223372036854775808; - - var numbers = raw_numbers[1..]; - - for (numbers) |_, i| { - numbers[i] = @mod(@intCast(i64, i), 12); - } - - var roc_list = RocList{ .elements = numbers, .length = NUM_NUMS }; - - // start time - var ts1: std.os.timespec = undefined; - std.os.clock_gettime(std.os.CLOCK.REALTIME, &ts1) catch unreachable; - - // actually call roc to populate the callresult - var callresult: RocList = undefined; - roc__mainForHost_1_exposed_generic(&callresult, &roc_list); - - // const callresult: RocList = roc__mainForHost_1_exposed_generic(&roc_list); - - // stdout the result - const length = std.math.min(20, callresult.length); - var result = callresult.elements[0..length]; - - // end time - var ts2: std.os.timespec = undefined; - std.os.clock_gettime(std.os.CLOCK.REALTIME, &ts2) catch unreachable; - - for (result) |x, i| { - if (i == 0) { - stdout.print("[{}, ", .{x}) catch unreachable; - } else if (i == length - 1) { - stdout.print("{}]\n", .{x}) catch unreachable; - } else { - stdout.print("{}, ", .{x}) catch unreachable; - } - } - - // TODO apparently the typestamps are still (partially) undefined? - // const delta = to_seconds(ts2) - to_seconds(ts1); - // stderr.print("runtime: {d:.3}ms\n", .{delta * 1000}) catch unreachable; - - return 0; -} - -fn to_seconds(tms: std.os.timespec) f64 { - return @intToFloat(f64, tms.tv_sec) + (@intToFloat(f64, tms.tv_nsec) / 1_000_000_000.0); -} diff --git a/examples/benchmarks/.gitignore b/examples/benchmarks/.gitignore deleted file mode 100644 index bb8dcb472f..0000000000 --- a/examples/benchmarks/.gitignore +++ /dev/null @@ -1,12 +0,0 @@ -cfold -closure -deriv -issue2279 -nqueens -quicksortapp -rbtree-ck -rbtree-del -rbtree-insert -test-astar -test-base64 -*.wasm diff --git a/examples/benchmarks/AStar.roc b/examples/benchmarks/AStar.roc deleted file mode 100644 index 005734aa2b..0000000000 --- a/examples/benchmarks/AStar.roc +++ /dev/null @@ -1,127 +0,0 @@ -interface AStar exposes [findPath, Model, initialModel, cheapestOpen, reconstructPath] imports [Quicksort] - -findPath = \costFn, moveFn, start, end -> - astar costFn moveFn end (initialModel start) - -Model position : - { - evaluated : Set position, - openSet : Set position, - costs : Dict position F64, - cameFrom : Dict position position, - } - -initialModel : position -> Model position -initialModel = \start -> { - evaluated: Set.empty, - openSet: Set.single start, - costs: Dict.single start 0, - cameFrom: Dict.empty, -} - -cheapestOpen : (position -> F64), Model position -> Result position {} -cheapestOpen = \costFn, model -> - model.openSet - |> Set.toList - |> List.keepOks - (\position -> - when Dict.get model.costs position is - Err _ -> - Err {} - Ok cost -> - Ok { cost: cost + costFn position, position } - ) - |> Quicksort.sortBy .cost - |> List.first - |> Result.map .position - |> Result.mapErr (\_ -> {}) - -reconstructPath : Dict position position, position -> List position -reconstructPath = \cameFrom, goal -> - when Dict.get cameFrom goal is - Err _ -> - [] - Ok next -> - List.append (reconstructPath cameFrom next) goal - -updateCost : position, position, Model position -> Model position -updateCost = \current, neighbor, model -> - newCameFrom = - Dict.insert model.cameFrom neighbor current - - newCosts = - Dict.insert model.costs neighbor distanceTo - - distanceTo = - reconstructPath newCameFrom neighbor - |> List.len - |> Num.toFrac - - newModel = - { model & - costs: newCosts, - cameFrom: newCameFrom, - } - - when Dict.get model.costs neighbor is - Err _ -> - newModel - Ok previousDistance -> - if distanceTo < previousDistance then - newModel - else - model - -astar : (position, position -> F64), (position -> Set position), position, Model position -> Result (List position) {} -astar = \costFn, moveFn, goal, model -> - when cheapestOpen (\source -> costFn source goal) model is - Err {} -> - Err {} - Ok current -> - if current == goal then - Ok (reconstructPath model.cameFrom goal) - else - modelPopped = - { model & - openSet: Set.remove model.openSet current, - evaluated: Set.insert model.evaluated current, - } - - neighbors = - moveFn current - - newNeighbors = - Set.difference neighbors modelPopped.evaluated - - modelWithNeighbors : Model position - modelWithNeighbors = - { modelPopped & - openSet: Set.union modelPopped.openSet newNeighbors, - } - - walker : Model position, position -> Model position - walker = \amodel, n -> updateCost current n amodel - - modelWithCosts = - Set.walk newNeighbors modelWithNeighbors walker - - astar costFn moveFn goal modelWithCosts - -# takeStep = \moveFn, _goal, model, current -> -# modelPopped = -# { model & -# openSet: Set.remove model.openSet current, -# evaluated: Set.insert model.evaluated current, -# } -# -# neighbors = moveFn current -# -# newNeighbors = Set.difference neighbors modelPopped.evaluated -# -# modelWithNeighbors = { modelPopped & openSet: Set.union modelPopped.openSet newNeighbors } -# -# # a lot goes wrong here -# modelWithCosts = -# Set.walk newNeighbors modelWithNeighbors (\n, m -> updateCost current n m) -# -# modelWithCosts diff --git a/examples/benchmarks/Base64/Decode.roc b/examples/benchmarks/Base64/Decode.roc deleted file mode 100644 index 4d38b5a291..0000000000 --- a/examples/benchmarks/Base64/Decode.roc +++ /dev/null @@ -1,123 +0,0 @@ -interface Base64.Decode exposes [fromBytes] imports [Bytes.Decode.{ Decoder, DecodeProblem }] - -fromBytes : List U8 -> Result Str DecodeProblem -fromBytes = \bytes -> - Bytes.Decode.decode bytes (decodeBase64 (List.len bytes)) - -decodeBase64 : Nat -> Decoder Str -decodeBase64 = \width -> Bytes.Decode.loop loopHelp { remaining: width, string: "" } - -loopHelp : { remaining : Nat, string : Str } -> Decoder (Bytes.Decode.Step { remaining : Nat, string : Str } Str) -loopHelp = \{ remaining, string } -> - if remaining >= 3 then - x, y, z <- Bytes.Decode.map3 Bytes.Decode.u8 Bytes.Decode.u8 Bytes.Decode.u8 - - a : U32 - a = Num.intCast x - b : U32 - b = Num.intCast y - c : U32 - c = Num.intCast z - combined = Num.bitwiseOr (Num.bitwiseOr (Num.shiftLeftBy 16 a) (Num.shiftLeftBy 8 b)) c - - Loop { - remaining: remaining - 3, - string: Str.concat string (bitsToChars combined 0), - } - else if remaining == 0 then - Bytes.Decode.succeed (Done string) - else if remaining == 2 then - x, y <- Bytes.Decode.map2 Bytes.Decode.u8 Bytes.Decode.u8 - - a : U32 - a = Num.intCast x - b : U32 - b = Num.intCast y - combined = Num.bitwiseOr (Num.shiftLeftBy 16 a) (Num.shiftLeftBy 8 b) - - Done (Str.concat string (bitsToChars combined 1)) - else - # remaining = 1 - x <- Bytes.Decode.map Bytes.Decode.u8 - - a : U32 - a = Num.intCast x - - Done (Str.concat string (bitsToChars (Num.shiftLeftBy 16 a) 2)) - -bitsToChars : U32, Int * -> Str -bitsToChars = \bits, missing -> - when Str.fromUtf8 (bitsToCharsHelp bits missing) is - Ok str -> - str - Err _ -> - "" - -# Mask that can be used to get the lowest 6 bits of a binary number -lowest6BitsMask : Int * -lowest6BitsMask = 63 - -bitsToCharsHelp : U32, Int * -> List U8 -bitsToCharsHelp = \bits, missing -> - # The input is 24 bits, which we have to partition into 4 6-bit segments. We achieve this by - # shifting to the right by (a multiple of) 6 to remove unwanted bits on the right, then `Num.bitwiseAnd` - # with `0b111111` (which is 2^6 - 1 or 63) (so, 6 1s) to remove unwanted bits on the left. - # any 6-bit number is a valid base64 digit, so this is actually safe - p = - Num.shiftRightZfBy 18 bits - |> Num.intCast - |> unsafeToChar - - q = - Num.bitwiseAnd (Num.shiftRightZfBy 12 bits) lowest6BitsMask - |> Num.intCast - |> unsafeToChar - - r = - Num.bitwiseAnd (Num.shiftRightZfBy 6 bits) lowest6BitsMask - |> Num.intCast - |> unsafeToChar - - s = - Num.bitwiseAnd bits lowest6BitsMask - |> Num.intCast - |> unsafeToChar - - equals : U8 - equals = 61 - - when missing is - 0 -> - [p, q, r, s] - 1 -> - [p, q, r, equals] - 2 -> - [p, q, equals, equals] - _ -> - # unreachable - [] - -# Base64 index to character/digit -unsafeToChar : U8 -> U8 -unsafeToChar = \n -> - if n <= 25 then - # uppercase characters - 65 + n - else if n <= 51 then - # lowercase characters - 97 + (n - 26) - else if n <= 61 then - # digit characters - 48 + (n - 52) - else - # special cases - when n is - 62 -> - # '+' - 43 - 63 -> - # '/' - 47 - _ -> - # anything else is invalid '\u{0000}' - 0 diff --git a/examples/benchmarks/Base64/Encode.roc b/examples/benchmarks/Base64/Encode.roc deleted file mode 100644 index 5e9be0e09e..0000000000 --- a/examples/benchmarks/Base64/Encode.roc +++ /dev/null @@ -1,181 +0,0 @@ -interface Base64.Encode - exposes [toBytes] - imports [Bytes.Encode.{ Encoder }] - -InvalidChar : U8 - -# State : [None, One U8, Two U8, Three U8] -toBytes : Str -> List U8 -toBytes = \str -> - str - |> Str.toUtf8 - |> encodeChunks - |> Bytes.Encode.sequence - |> Bytes.Encode.encode - -encodeChunks : List U8 -> List Encoder -encodeChunks = \bytes -> - List.walk bytes { output: [], accum: None } folder - |> encodeResidual - -coerce : Nat, a -> a -coerce = \_, x -> x - -# folder : { output : List Encoder, accum : State }, U8 -> { output : List Encoder, accum : State } -folder = \{ output, accum }, char -> - when accum is - Unreachable n -> - coerce n { output, accum: Unreachable n } - None -> - { output, accum: One char } - One a -> - { output, accum: Two a char } - Two a b -> - { output, accum: Three a b char } - Three a b c -> - when encodeCharacters a b c char is - Ok encoder -> - { - output: List.append output encoder, - accum: None, - } - Err _ -> - { output, accum: None } - -# SGVs bG8g V29y bGQ= -# encodeResidual : { output : List Encoder, accum : State } -> List Encoder -encodeResidual = \{ output, accum } -> - when accum is - Unreachable _ -> - output - None -> - output - One _ -> - output - Two a b -> - when encodeCharacters a b equals equals is - Ok encoder -> - List.append output encoder - Err _ -> - output - Three a b c -> - when encodeCharacters a b c equals is - Ok encoder -> - List.append output encoder - Err _ -> - output - -equals : U8 -equals = 61 - -# Convert 4 characters to 24 bits (as an Encoder) -encodeCharacters : U8, U8, U8, U8 -> Result Encoder InvalidChar -encodeCharacters = \a, b, c, d -> - if !(isValidChar a) then - Err a - else if !(isValidChar b) then - Err b - else - # `=` is the padding character, and must be special-cased - # only the `c` and `d` char are allowed to be padding - n1 = unsafeConvertChar a - n2 = unsafeConvertChar b - - x : U32 - x = Num.intCast n1 - - y : U32 - y = Num.intCast n2 - - if d == equals then - if c == equals then - n = Num.bitwiseOr (Num.shiftLeftBy 18 x) (Num.shiftLeftBy 12 y) - - # masking higher bits is not needed, Encode.unsignedInt8 ignores higher bits - b1 : U8 - b1 = Num.intCast (Num.shiftRightBy 16 n) - - Ok (Bytes.Encode.u8 b1) - else if !(isValidChar c) then - Err c - else - n3 = unsafeConvertChar c - - z : U32 - z = Num.intCast n3 - - n = Num.bitwiseOr (Num.bitwiseOr (Num.shiftLeftBy 18 x) (Num.shiftLeftBy 12 y)) (Num.shiftLeftBy 6 z) - - combined : U16 - combined = Num.intCast (Num.shiftRightBy 8 n) - - Ok (Bytes.Encode.u16 BE combined) - else if !(isValidChar d) then - Err d - else - n3 = unsafeConvertChar c - n4 = unsafeConvertChar d - - z : U32 - z = Num.intCast n3 - - w : U32 - w = Num.intCast n4 - - n = - Num.bitwiseOr - (Num.bitwiseOr (Num.shiftLeftBy 18 x) (Num.shiftLeftBy 12 y)) - (Num.bitwiseOr (Num.shiftLeftBy 6 z) w) - - b3 : U8 - b3 = Num.intCast n - - combined : U16 - combined = Num.intCast (Num.shiftRightBy 8 n) - - Ok (Bytes.Encode.sequence [Bytes.Encode.u16 BE combined, Bytes.Encode.u8 b3]) - -# is the character a base64 digit? -# The base16 digits are: A-Z, a-z, 0-1, '+' and '/' -isValidChar : U8 -> Bool -isValidChar = \c -> - if isAlphaNum c then - True - else - when c is - 43 -> - # '+' - True - 47 -> - # '/' - True - _ -> - False - -isAlphaNum : U8 -> Bool -isAlphaNum = \key -> - (key >= 48 && key <= 57) || (key >= 64 && key <= 90) || (key >= 97 && key <= 122) - -# Convert a base64 character/digit to its index -# See also [Wikipedia](https://en.wikipedia.org/wiki/Base64#Base64_table) -unsafeConvertChar : U8 -> U8 -unsafeConvertChar = \key -> - if key >= 65 && key <= 90 then - # A-Z - key - 65 - else if key >= 97 && key <= 122 then - # a-z - (key - 97) + 26 - else if key >= 48 && key <= 57 then - # 0-9 - (key - 48) + 26 + 26 - else - when key is - 43 -> - # '+' - 62 - 47 -> - # '/' - 63 - _ -> - 0 diff --git a/examples/benchmarks/Bytes/Decode.roc b/examples/benchmarks/Bytes/Decode.roc deleted file mode 100644 index 35a7172ff7..0000000000 --- a/examples/benchmarks/Bytes/Decode.roc +++ /dev/null @@ -1,100 +0,0 @@ -interface Bytes.Decode exposes [Decoder, decode, map, map2, u8, loop, Step, succeed, DecodeProblem, after, map3] imports [] - -State : { bytes : List U8, cursor : Nat } - -DecodeProblem : [OutOfBytes] - -Decoder a := State -> [Good State a, Bad DecodeProblem] - -decode : List U8, Decoder a -> Result a DecodeProblem -decode = \bytes, @Decoder decoder -> - when decoder { bytes, cursor: 0 } is - Good _ value -> - Ok value - Bad e -> - Err e - -succeed : a -> Decoder a -succeed = \value -> @Decoder \state -> Good state value - -map : Decoder a, (a -> b) -> Decoder b -map = \@Decoder decoder, transform -> - @Decoder - \state -> - when decoder state is - Good state1 value -> - Good state1 (transform value) - Bad e -> - Bad e - -map2 : Decoder a, Decoder b, (a, b -> c) -> Decoder c -map2 = \@Decoder decoder1, @Decoder decoder2, transform -> - @Decoder - \state1 -> - when decoder1 state1 is - Good state2 a -> - when decoder2 state2 is - Good state3 b -> - Good state3 (transform a b) - Bad e -> - Bad e - Bad e -> - Bad e - -map3 : Decoder a, Decoder b, Decoder c, (a, b, c -> d) -> Decoder d -map3 = \@Decoder decoder1, @Decoder decoder2, @Decoder decoder3, transform -> - @Decoder - \state1 -> - when decoder1 state1 is - Good state2 a -> - when decoder2 state2 is - Good state3 b -> - when decoder3 state3 is - Good state4 c -> - Good state4 (transform a b c) - Bad e -> - Bad e - Bad e -> - Bad e - Bad e -> - Bad e - -after : Decoder a, (a -> Decoder b) -> Decoder b -after = \@Decoder decoder, transform -> - @Decoder - \state -> - when decoder state is - Good state1 value -> - (@Decoder decoder1) = transform value - - decoder1 state1 - Bad e -> - Bad e - -u8 : Decoder U8 -u8 = @Decoder - \state -> - when List.get state.bytes state.cursor is - Ok b -> - Good { state & cursor: state.cursor + 1 } b - Err _ -> - Bad OutOfBytes - -Step state b : [Loop state, Done b] - -loop : (state -> Decoder (Step state a)), state -> Decoder a -loop = \stepper, initial -> - @Decoder - \state -> - loopHelp stepper initial state - -loopHelp = \stepper, accum, state -> - (@Decoder stepper1) = stepper accum - - when stepper1 state is - Good newState (Done value) -> - Good newState value - Good newState (Loop newAccum) -> - loopHelp stepper newAccum newState - Bad e -> - Bad e diff --git a/examples/benchmarks/Bytes/Encode.roc b/examples/benchmarks/Bytes/Encode.roc deleted file mode 100644 index a95156a0b7..0000000000 --- a/examples/benchmarks/Bytes/Encode.roc +++ /dev/null @@ -1,133 +0,0 @@ -interface Bytes.Encode exposes [Encoder, sequence, u8, u16, bytes, empty, encode] imports [] - -Endianness : [BE, LE] - -Encoder : [Signed8 I8, Unsigned8 U8, Signed16 Endianness I16, Unsigned16 Endianness U16, Sequence Nat (List Encoder), Bytes (List U8)] - -u8 : U8 -> Encoder -u8 = \value -> Unsigned8 value - -empty : Encoder -empty = - foo : List Encoder - foo = [] - - Sequence 0 foo - -u16 : Endianness, U16 -> Encoder -u16 = \endianness, value -> Unsigned16 endianness value - -bytes : List U8 -> Encoder -bytes = \bs -> Bytes bs - -sequence : List Encoder -> Encoder -sequence = \encoders -> - Sequence (getWidths encoders 0) encoders - -getWidth : Encoder -> Nat -getWidth = \encoder -> - when encoder is - Signed8 _ -> - 1 - Unsigned8 _ -> - 1 - Signed16 _ _ -> - 2 - Unsigned16 _ _ -> - 2 - # Signed32 _ -> 4 - # Unsigned32 _ -> 4 - # Signed64 _ -> 8 - # Unsigned64 _ -> 8 - # Signed128 _ -> 16 - # Unsigned128 _ -> 16 - Sequence w _ -> - w - Bytes bs -> - List.len bs - -getWidths : List Encoder, Nat -> Nat -getWidths = \encoders, initial -> - List.walk encoders initial \accum, encoder -> accum + getWidth encoder - -encode : Encoder -> List U8 -encode = \encoder -> - output = List.repeat 0 (getWidth encoder) - - encodeHelp encoder 0 output - |> .output - -encodeHelp : Encoder, Nat, List U8 -> { output : List U8, offset : Nat } -encodeHelp = \encoder, offset, output -> - when encoder is - Unsigned8 value -> - { - output: List.set output offset value, - offset: offset + 1, - } - Signed8 value -> - cast : U8 - cast = Num.intCast value - - { - output: List.set output offset cast, - offset: offset + 1, - } - Unsigned16 endianness value -> - a : U8 - a = Num.intCast (Num.shiftRightBy 8 value) - - b : U8 - b = Num.intCast value - - newOutput = - when endianness is - BE -> - output - |> List.set (offset + 0) a - |> List.set (offset + 1) b - LE -> - output - |> List.set (offset + 0) b - |> List.set (offset + 1) a - - { - output: newOutput, - offset: offset + 2, - } - Signed16 endianness value -> - a : U8 - a = Num.intCast (Num.shiftRightBy 8 value) - - b : U8 - b = Num.intCast value - - newOutput = - when endianness is - BE -> - output - |> List.set (offset + 0) a - |> List.set (offset + 1) b - LE -> - output - |> List.set (offset + 0) b - |> List.set (offset + 1) a - - { - output: newOutput, - offset: offset + 1, - } - Bytes bs -> - List.walk - bs - { output, offset } - \accum, byte -> { - offset: accum.offset + 1, - output: List.set accum.output offset byte, - } - Sequence _ encoders -> - List.walk - encoders - { output, offset } - \accum, single -> - encodeHelp single accum.offset accum.output diff --git a/examples/benchmarks/CFold.roc b/examples/benchmarks/CFold.roc deleted file mode 100644 index ddd758416f..0000000000 --- a/examples/benchmarks/CFold.roc +++ /dev/null @@ -1,114 +0,0 @@ -app "cfold" - packages { pf: "platform" } - imports [pf.Task] - provides [main] to pf - -# adapted from https://github.com/koka-lang/koka/blob/master/test/bench/haskell/cfold.hs -main : Task.Task {} [] -main = - Task.after - Task.getInt - \n -> - e = mkExpr n 1# original koka n = 20 (set `ulimit -s unlimited` to avoid stack overflow for n = 20) - unoptimized = eval e - optimized = eval (constFolding (reassoc e)) - - unoptimized - |> Num.toStr - |> Str.concat " & " - |> Str.concat (Num.toStr optimized) - |> Task.putLine - -Expr : [ - Add Expr Expr, - Mul Expr Expr, - Val I64, - Var I64, - ] - -mkExpr : I64, I64 -> Expr -mkExpr = \n, v -> - when n is - 0 -> - if v == 0 then Var 1 else Val v - _ -> - Add (mkExpr (n - 1) (v + 1)) (mkExpr (n - 1) (max (v - 1) 0)) - -max : I64, I64 -> I64 -max = \a, b -> if a > b then a else b - -appendAdd : Expr, Expr -> Expr -appendAdd = \e1, e2 -> - when e1 is - Add a1 a2 -> - Add a1 (appendAdd a2 e2) - _ -> - Add e1 e2 - -appendMul : Expr, Expr -> Expr -appendMul = \e1, e2 -> - when e1 is - Mul a1 a2 -> - Mul a1 (appendMul a2 e2) - _ -> - Mul e1 e2 - -eval : Expr -> I64 -eval = \e -> - when e is - Var _ -> - 0 - Val v -> - v - Add l r -> - eval l + eval r - Mul l r -> - eval l * eval r - -reassoc : Expr -> Expr -reassoc = \e -> - when e is - Add e1 e2 -> - x1 = reassoc e1 - x2 = reassoc e2 - - appendAdd x1 x2 - Mul e1 e2 -> - x1 = reassoc e1 - x2 = reassoc e2 - - appendMul x1 x2 - _ -> - e - -constFolding : Expr -> Expr -constFolding = \e -> - when e is - Add e1 e2 -> - x1 = constFolding e1 - x2 = constFolding e2 - - when Pair x1 x2 is - Pair (Val a) (Val b) -> - Val (a + b) - Pair (Val a) (Add (Val b) x) -> - Add (Val (a + b)) x - Pair (Val a) (Add x (Val b)) -> - Add (Val (a + b)) x - Pair y1 y2 -> - Add y1 y2 - Mul e1 e2 -> - x1 = constFolding e1 - x2 = constFolding e2 - - when Pair x1 x2 is - Pair (Val a) (Val b) -> - Val (a * b) - Pair (Val a) (Mul (Val b) x) -> - Mul (Val (a * b)) x - Pair (Val a) (Mul x (Val b)) -> - Mul (Val (a * b)) x - Pair y1 y2 -> - Add y1 y2 - _ -> - e diff --git a/examples/benchmarks/Deriv.roc b/examples/benchmarks/Deriv.roc deleted file mode 100644 index 456a8549cc..0000000000 --- a/examples/benchmarks/Deriv.roc +++ /dev/null @@ -1,172 +0,0 @@ -app "deriv" - packages { pf: "platform" } - imports [pf.Task] - provides [main] to pf - -# based on: https://github.com/koka-lang/koka/blob/master/test/bench/haskell/deriv.hs -IO a : Task.Task a [] - -main : Task.Task {} [] -main = - Task.after - Task.getInt - \n -> - x : Expr - x = Var "x" - - f : Expr - f = pow x x - - nest deriv n f# original koka n = 10 - - |> Task.map (\_ -> {}) - -nest : (I64, Expr -> IO Expr), I64, Expr -> IO Expr -nest = \f, n, e -> Task.loop { s: n, f, m: n, x: e } nestHelp - -State : { s : I64, f : I64, Expr -> IO Expr, m : I64, x : Expr } - -nestHelp : State -> IO [Step State, Done Expr] -nestHelp = \{ s, f, m, x } -> - when m is - 0 -> - Task.succeed (Done x) - _ -> - w <- Task.after (f (s - m) x) - - Task.succeed (Step { s, f, m: (m - 1), x: w }) - -Expr : [Val I64, Var Str, Add Expr Expr, Mul Expr Expr, Pow Expr Expr, Ln Expr] - -divmod : I64, I64 -> Result { div : I64, mod : I64 } [DivByZero]* -divmod = \l, r -> - when Pair (Num.divTruncChecked l r) (Num.remChecked l r) is - Pair (Ok div) (Ok mod) -> - Ok { div, mod } - _ -> - Err DivByZero - -pown : I64, I64 -> I64 -pown = \a, n -> - when n is - 0 -> - 1 - 1 -> - a - _ -> - when divmod n 2 is - Ok { div, mod } -> - b = pown a div - - b * b * (if mod == 0 then 1 else a) - Err DivByZero -> - -1 - -add : Expr, Expr -> Expr -add = \a, b -> - when Pair a b is - Pair (Val n) (Val m) -> - Val (n + m) - Pair (Val 0) f -> - f - Pair f (Val 0) -> - f - Pair f (Val n) -> - add (Val n) f - Pair (Val n) (Add (Val m) f) -> - add (Val (n + m)) f - Pair f (Add (Val n) g) -> - add (Val n) (add f g) - Pair (Add f g) h -> - add f (add g h) - Pair f g -> - Add f g - -mul : Expr, Expr -> Expr -mul = \a, b -> - when Pair a b is - Pair (Val n) (Val m) -> - Val (n * m) - Pair (Val 0) _ -> - Val 0 - Pair _ (Val 0) -> - Val 0 - Pair (Val 1) f -> - f - Pair f (Val 1) -> - f - Pair f (Val n) -> - mul (Val n) f - Pair (Val n) (Mul (Val m) f) -> - mul (Val (n * m)) f - Pair f (Mul (Val n) g) -> - mul (Val n) (mul f g) - Pair (Mul f g) h -> - mul f (mul g h) - Pair f g -> - Mul f g - -pow : Expr, Expr -> Expr -pow = \a, b -> - when Pair a b is - Pair (Val m) (Val n) -> - Val (pown m n) - Pair _ (Val 0) -> - Val 1 - Pair f (Val 1) -> - f - Pair (Val 0) _ -> - Val 0 - Pair f g -> - Pow f g - -ln : Expr -> Expr -ln = \f -> - when f is - Val 1 -> - Val 0 - _ -> - Ln f - -d : Str, Expr -> Expr -d = \x, expr -> - when expr is - Val _ -> - Val 0 - Var y -> - if x == y then Val 1 else Val 0 - Add f g -> - add (d x f) (d x g) - Mul f g -> - add (mul f (d x g)) (mul g (d x f)) - Pow f g -> - mul (pow f g) (add (mul (mul g (d x f)) (pow f (Val (-1)))) (mul (ln f) (d x g))) - Ln f -> - mul (d x f) (pow f (Val (-1))) - -count : Expr -> I64 -count = \expr -> - when expr is - Val _ -> - 1 - Var _ -> - 1 - Add f g -> - count f + count g - Mul f g -> - count f + count g - Pow f g -> - count f + count g - Ln f -> - count f - -deriv : I64, Expr -> IO Expr -deriv = \i, f -> - fprime = d "x" f - line = - Num.toStr (i + 1) - |> Str.concat " count: " - |> Str.concat (Num.toStr (count fprime)) - - Task.putLine line - |> Task.after \_ -> Task.succeed fprime diff --git a/examples/benchmarks/NQueens.roc b/examples/benchmarks/NQueens.roc deleted file mode 100644 index 2634d8fa66..0000000000 --- a/examples/benchmarks/NQueens.roc +++ /dev/null @@ -1,59 +0,0 @@ -app "nqueens" - packages { pf: "platform" } - imports [pf.Task] - provides [main] to pf - -main : Task.Task {} [] -main = - Task.after - Task.getInt - \n -> - queens n# original koka 13 - - |> Num.toStr - |> Task.putLine - -ConsList a : [Nil, Cons a (ConsList a)] - -queens = \n -> length (findSolutions n n) - -length : ConsList a -> I64 -length = \xs -> lengthHelp xs 0 - -lengthHelp : ConsList a, I64 -> I64 -lengthHelp = \foobar, acc -> - when foobar is - Cons _ lrest -> - lengthHelp lrest (1 + acc) - Nil -> - acc - -safe : I64, I64, ConsList I64 -> Bool -safe = \queen, diagonal, xs -> - when xs is - Nil -> - True - Cons q t -> - queen != q && queen != q + diagonal && queen != q - diagonal && safe queen (diagonal + 1) t - -appendSafe : I64, ConsList I64, ConsList (ConsList I64) -> ConsList (ConsList I64) -appendSafe = \k, soln, solns -> - if k <= 0 then - solns - else if safe k 1 soln then - appendSafe (k - 1) soln (Cons (Cons k soln) solns) - else - appendSafe (k - 1) soln solns - -extend = \n, acc, solutions -> - when solutions is - Nil -> - acc - Cons soln rest -> - extend n (appendSafe n soln acc) rest - -findSolutions = \n, k -> - if k == 0 then - Cons Nil Nil - else - extend n Nil (findSolutions n (k - 1)) diff --git a/examples/benchmarks/Quicksort.roc b/examples/benchmarks/Quicksort.roc deleted file mode 100644 index b68324416f..0000000000 --- a/examples/benchmarks/Quicksort.roc +++ /dev/null @@ -1,71 +0,0 @@ -interface Quicksort exposes [sortBy, sortWith, show] imports [] - -show : List I64 -> Str -show = \list -> - if List.isEmpty list then - "[]" - else - content = - list - |> List.map Num.toStr - |> Str.joinWith ", " - - "[\(content)]" - -sortBy : List a, (a -> Num *) -> List a -sortBy = \list, toComparable -> - sortWith list (\x, y -> Num.compare (toComparable x) (toComparable y)) - -Order a : a, a -> [LT, GT, EQ] - -sortWith : List a, (a, a -> [LT, GT, EQ]) -> List a -sortWith = \list, order -> - n = List.len list - - quicksortHelp list order 0 (n - 1) - -quicksortHelp : List a, Order a, Nat, Nat -> List a -quicksortHelp = \list, order, low, high -> - if low < high then - when partition low high list order is - Pair partitionIndex partitioned -> - partitioned - |> quicksortHelp order low (Num.subSaturated partitionIndex 1) - |> quicksortHelp order (partitionIndex + 1) high - else - list - -partition : Nat, Nat, List a, Order a -> [Pair Nat (List a)] -partition = \low, high, initialList, order -> - when List.get initialList high is - Ok pivot -> - when partitionHelp low low initialList order high pivot is - Pair newI newList -> - Pair newI (swap newI high newList) - Err _ -> - Pair low initialList - -partitionHelp : Nat, Nat, List c, Order c, Nat, c -> [Pair Nat (List c)] -partitionHelp = \i, j, list, order, high, pivot -> - if j < high then - when List.get list j is - Ok value -> - when order value pivot is - LT | EQ -> - partitionHelp (i + 1) (j + 1) (swap i j list) order high pivot - GT -> - partitionHelp i (j + 1) list order high pivot - Err _ -> - Pair i list - else - Pair i list - -swap : Nat, Nat, List a -> List a -swap = \i, j, list -> - when Pair (List.get list i) (List.get list j) is - Pair (Ok atI) (Ok atJ) -> - list - |> List.set i atJ - |> List.set j atI - _ -> - [] diff --git a/examples/benchmarks/RBTreeDel.roc b/examples/benchmarks/RBTreeDel.roc deleted file mode 100644 index 6f57c01e32..0000000000 --- a/examples/benchmarks/RBTreeDel.roc +++ /dev/null @@ -1,247 +0,0 @@ -app "rbtree-del" - packages { pf: "platform" } - imports [pf.Task] - provides [main] to pf - -Color : [Red, Black] - -Tree a b : [Leaf, Node Color (Tree a b) a b (Tree a b)] - -Map : Tree I64 Bool - -ConsList a : [Nil, Cons a (ConsList a)] - -main : Task.Task {} [] -main = - Task.after - Task.getInt - \n -> - m = makeMap n# koka original n = 4_200_000 - val = fold (\_, v, r -> if v then r + 1 else r) m 0 - - val - |> Num.toStr - |> Task.putLine - -boom : Str -> a -boom = \_ -> boom "" - -makeMap : I64 -> Map -makeMap = \n -> - makeMapHelp n n Leaf - -makeMapHelp : I64, I64, Map -> Map -makeMapHelp = \total, n, m -> - when n is - 0 -> - m - _ -> - n1 = n - 1 - - powerOf10 = - n |> Num.isMultipleOf 10 - - t1 = insert m n powerOf10 - - isFrequency = - n |> Num.isMultipleOf 4 - - key = n1 + ((total - n1) // 5) - t2 = if isFrequency then delete t1 key else t1 - - makeMapHelp total n1 t2 - -fold : (a, b, omega -> omega), Tree a b, omega -> omega -fold = \f, tree, b -> - when tree is - Leaf -> - b - Node _ l k v r -> - fold f r (f k v (fold f l b)) - -depth : Tree * * -> I64 -depth = \tree -> - when tree is - Leaf -> - 1 - Node _ l _ _ r -> - 1 + depth l + depth r - -resultWithDefault : Result a e, a -> a -resultWithDefault = \res, default -> - when res is - Ok v -> - v - Err _ -> - default - -insert : Map, I64, Bool -> Map -insert = \t, k, v -> if isRed t then setBlack (ins t k v) else ins t k v - -setBlack : Tree a b -> Tree a b -setBlack = \tree -> - when tree is - Node _ l k v r -> - Node Black l k v r - _ -> - tree - -isRed : Tree a b -> Bool -isRed = \tree -> - when tree is - Node Red _ _ _ _ -> - True - _ -> - False - -ins : Tree I64 Bool, I64, Bool -> Tree I64 Bool -ins = \tree, kx, vx -> - when tree is - Leaf -> - Node Red Leaf kx vx Leaf - Node Red a ky vy b -> - when Num.compare kx ky is - LT -> - Node Red (ins a kx vx) ky vy b - GT -> - Node Red a ky vy (ins b kx vx) - EQ -> - Node Red a ky vy (ins b kx vx) - Node Black a ky vy b -> - when Num.compare kx ky is - LT -> - when isRed a is - True -> - balanceLeft (ins a kx vx) ky vy b - False -> - Node Black (ins a kx vx) ky vy b - GT -> - when isRed b is - True -> - balanceRight a ky vy (ins b kx vx) - False -> - Node Black a ky vy (ins b kx vx) - EQ -> - Node Black a kx vx b - -balanceLeft : Tree a b, a, b, Tree a b -> Tree a b -balanceLeft = \l, k, v, r -> - when l is - Leaf -> - Leaf - Node _ (Node Red lx kx vx rx) ky vy ry -> - Node Red (Node Black lx kx vx rx) ky vy (Node Black ry k v r) - Node _ ly ky vy (Node Red lx kx vx rx) -> - Node Red (Node Black ly ky vy lx) kx vx (Node Black rx k v r) - Node _ lx kx vx rx -> - Node Black (Node Red lx kx vx rx) k v r - -balanceRight : Tree a b, a, b, Tree a b -> Tree a b -balanceRight = \l, k, v, r -> - when r is - Leaf -> - Leaf - Node _ (Node Red lx kx vx rx) ky vy ry -> - Node Red (Node Black l k v lx) kx vx (Node Black rx ky vy ry) - Node _ lx kx vx (Node Red ly ky vy ry) -> - Node Red (Node Black l k v lx) kx vx (Node Black ly ky vy ry) - Node _ lx kx vx rx -> - Node Black l k v (Node Red lx kx vx rx) - -isBlack : Color -> Bool -isBlack = \c -> - when c is - Black -> - True - Red -> - False - -Del a b : [Del (Tree a b) Bool] - -setRed : Map -> Map -setRed = \t -> - when t is - Node _ l k v r -> - Node Red l k v r - _ -> - t - -makeBlack : Map -> Del I64 Bool -makeBlack = \t -> - when t is - Node Red l k v r -> - Del (Node Black l k v r) False - _ -> - Del t True - -rebalanceLeft = \c, l, k, v, r -> - when l is - Node Black _ _ _ _ -> - Del (balanceLeft (setRed l) k v r) (isBlack c) - Node Red lx kx vx rx -> - Del (Node Black lx kx vx (balanceLeft (setRed rx) k v r)) False - _ -> - boom "unreachable" - -rebalanceRight = \c, l, k, v, r -> - when r is - Node Black _ _ _ _ -> - Del (balanceRight l k v (setRed r)) (isBlack c) - Node Red lx kx vx rx -> - Del (Node Black (balanceRight l k v (setRed lx)) kx vx rx) False - _ -> - boom "unreachable" - -delMin = \t -> - when t is - Node Black Leaf k v r -> - when r is - Leaf -> - Delmin (Del Leaf True) k v - _ -> - Delmin (Del (setBlack r) False) k v - Node Red Leaf k v r -> - Delmin (Del r False) k v - Node c l k v r -> - when delMin l is - Delmin (Del lx True) kx vx -> - Delmin (rebalanceRight c lx k v r) kx vx - Delmin (Del lx False) kx vx -> - Delmin (Del (Node c lx k v r) False) kx vx - Leaf -> - Delmin (Del t False) 0 False - -delete : Tree I64 Bool, I64 -> Tree I64 Bool -delete = \t, k -> - when del t k is - Del tx _ -> - setBlack tx - -del : Tree I64 Bool, I64 -> Del I64 Bool -del = \t, k -> - when t is - Leaf -> - Del Leaf False - Node cx lx kx vx rx -> - if (k < kx) then - when del lx k is - Del ly True -> - rebalanceRight cx ly kx vx rx - Del ly False -> - Del (Node cx ly kx vx rx) False - else if (k > kx) then - when del rx k is - Del ry True -> - rebalanceLeft cx lx kx vx ry - Del ry False -> - Del (Node cx lx kx vx ry) False - else - when rx is - Leaf -> - if isBlack cx then makeBlack lx else Del lx False - Node _ _ _ _ _ -> - when delMin rx is - Delmin (Del ry True) ky vy -> - rebalanceLeft cx lx ky vy ry - Delmin (Del ry False) ky vy -> - Del (Node cx lx ky vy ry) False diff --git a/examples/benchmarks/TestAStar.roc b/examples/benchmarks/TestAStar.roc deleted file mode 100644 index 19cb1266a5..0000000000 --- a/examples/benchmarks/TestAStar.roc +++ /dev/null @@ -1,51 +0,0 @@ -app "test-astar" - packages { pf: "platform" } - imports [pf.Task, AStar] - provides [main] to pf - -main : Task.Task {} [] -main = - Task.putLine (showBool test1) - -# Task.after Task.getInt \n -> -# when n is -# 1 -> -# Task.putLine (showBool test1) -# -# _ -> -# ns = Num.toStr n -# Task.putLine "No test \(ns)" -showBool : Bool -> Str -showBool = \b -> - when b is - True -> - "True" - False -> - "False" - -test1 : Bool -test1 = - example1 == [2, 4] - -example1 : List I64 -example1 = - step : I64 -> Set I64 - step = \n -> - when n is - 1 -> - Set.fromList [2, 3] - 2 -> - Set.fromList [4] - 3 -> - Set.fromList [4] - _ -> - Set.fromList [] - - cost : I64, I64 -> F64 - cost = \_, _ -> 1 - - when AStar.findPath cost step 1 4 is - Ok path -> - path - Err _ -> - [] diff --git a/examples/benchmarks/TestBase64.roc b/examples/benchmarks/TestBase64.roc deleted file mode 100644 index 86f540711f..0000000000 --- a/examples/benchmarks/TestBase64.roc +++ /dev/null @@ -1,21 +0,0 @@ -app "test-base64" - packages { pf: "platform" } - imports [pf.Task, Base64] - provides [main] to pf - -IO a : Task.Task a [] - -main : IO {} -main = - when Base64.fromBytes (Str.toUtf8 "Hello World") is - Err _ -> - Task.putLine "sadness" - Ok encoded -> - Task.after - (Task.putLine (Str.concat "encoded: " encoded)) - \_ -> - when Base64.toStr encoded is - Ok decoded -> - Task.putLine (Str.concat "decoded: " decoded) - Err _ -> - Task.putLine "sadness" diff --git a/examples/benchmarks/platform/Package-Config.roc b/examples/benchmarks/platform/Package-Config.roc deleted file mode 100644 index 0b86055696..0000000000 --- a/examples/benchmarks/platform/Package-Config.roc +++ /dev/null @@ -1,9 +0,0 @@ -platform "benchmarks" - requires {} { main : Task {} [] } - exposes [] - packages {} - imports [Task.{ Task }] - provides [mainForHost] - -mainForHost : Task {} [] as Fx -mainForHost = main diff --git a/examples/benchmarks/platform/Task.roc b/examples/benchmarks/platform/Task.roc deleted file mode 100644 index ccbc78bda6..0000000000 --- a/examples/benchmarks/platform/Task.roc +++ /dev/null @@ -1,86 +0,0 @@ -interface Task - exposes [Task, succeed, fail, after, map, putLine, putInt, getInt, forever, loop] - imports [pf.Effect] - -Task ok err : Effect.Effect (Result ok err) - -forever : Task val err -> Task * err -forever = \task -> - looper = \{} -> - task - |> Effect.map - \res -> - when res is - Ok _ -> - Step {} - Err e -> - Done (Err e) - - Effect.loop {} looper - -loop : state, (state -> Task [Step state, Done done] err) -> Task done err -loop = \state, step -> - looper = \current -> - step current - |> Effect.map - \res -> - when res is - Ok (Step newState) -> - Step newState - Ok (Done result) -> - Done (Ok result) - Err e -> - Done (Err e) - - Effect.loop state looper - -succeed : val -> Task val * -succeed = \val -> - Effect.always (Ok val) - -fail : err -> Task * err -fail = \val -> - Effect.always (Err val) - -after : Task a err, (a -> Task b err) -> Task b err -after = \effect, transform -> - Effect.after - effect - \result -> - when result is - Ok a -> - transform a - Err err -> - Task.fail err - -map : Task a err, (a -> b) -> Task b err -map = \effect, transform -> - Effect.map - effect - \result -> - when result is - Ok a -> - Ok (transform a) - Err err -> - Err err - -putLine : Str -> Task {} * -putLine = \line -> Effect.map (Effect.putLine line) (\_ -> Ok {}) - -putInt : I64 -> Task {} * -putInt = \line -> Effect.map (Effect.putInt line) (\_ -> Ok {}) - -getInt : Task I64 [] -getInt = - Effect.after - Effect.getInt - \{ isError, value } -> - when isError is - True -> - # when errorCode is - # # A -> Task.fail InvalidCharacter - # # B -> Task.fail IOError - # _ -> - Task.succeed -1 - False -> - Task.succeed value diff --git a/examples/benchmarks/platform/host.zig b/examples/benchmarks/platform/host.zig deleted file mode 100644 index d349cb64b6..0000000000 --- a/examples/benchmarks/platform/host.zig +++ /dev/null @@ -1,216 +0,0 @@ -const std = @import("std"); -const str = @import("str"); -const RocStr = str.RocStr; -const testing = std.testing; -const expectEqual = testing.expectEqual; -const expect = testing.expect; -const maxInt = std.math.maxInt; - -comptime { - // This is a workaround for https://github.com/ziglang/zig/issues/8218 - // which is only necessary on macOS. - // - // Once that issue is fixed, we can undo the changes in - // 177cf12e0555147faa4d436e52fc15175c2c4ff0 and go back to passing - // -fcompiler-rt in link.rs instead of doing this. Note that this - // workaround is present in many host.zig files, so make sure to undo - // it everywhere! - const builtin = @import("builtin"); - if (builtin.os.tag == .macos) { - _ = @import("compiler_rt"); - } -} - -const mem = std.mem; -const Allocator = mem.Allocator; - -extern fn roc__mainForHost_1_exposed_generic([*]u8) void; -extern fn roc__mainForHost_size() i64; -extern fn roc__mainForHost_1_Fx_caller(*const u8, [*]u8, [*]u8) void; -extern fn roc__mainForHost_1_Fx_size() i64; -extern fn roc__mainForHost_1_Fx_result_size() i64; - -const Align = 2 * @alignOf(usize); -extern fn malloc(size: usize) callconv(.C) ?*align(Align) anyopaque; -extern fn realloc(c_ptr: [*]align(Align) u8, size: usize) callconv(.C) ?*anyopaque; -extern fn free(c_ptr: [*]align(Align) u8) callconv(.C) void; -extern fn memcpy(dst: [*]u8, src: [*]u8, size: usize) callconv(.C) void; -extern fn memset(dst: [*]u8, value: i32, size: usize) callconv(.C) void; - -const DEBUG: bool = false; - -export fn roc_alloc(size: usize, alignment: u32) callconv(.C) ?*anyopaque { - if (DEBUG) { - var ptr = malloc(size); - const stdout = std.io.getStdOut().writer(); - stdout.print("alloc: {d} (alignment {d}, size {d})\n", .{ ptr, alignment, size }) catch unreachable; - return ptr; - } else { - return malloc(size); - } -} - -export fn roc_realloc(c_ptr: *anyopaque, new_size: usize, old_size: usize, alignment: u32) callconv(.C) ?*anyopaque { - if (DEBUG) { - const stdout = std.io.getStdOut().writer(); - stdout.print("realloc: {d} (alignment {d}, old_size {d})\n", .{ c_ptr, alignment, old_size }) catch unreachable; - } - - return realloc(@alignCast(Align, @ptrCast([*]u8, c_ptr)), new_size); -} - -export fn roc_dealloc(c_ptr: *anyopaque, alignment: u32) callconv(.C) void { - if (DEBUG) { - const stdout = std.io.getStdOut().writer(); - stdout.print("dealloc: {d} (alignment {d})\n", .{ c_ptr, alignment }) catch unreachable; - } - - free(@alignCast(Align, @ptrCast([*]u8, c_ptr))); -} - -export fn roc_panic(c_ptr: *anyopaque, tag_id: u32) callconv(.C) void { - _ = tag_id; - - const stderr = std.io.getStdErr().writer(); - const msg = @ptrCast([*:0]const u8, c_ptr); - stderr.print("Application crashed with message\n\n {s}\n\nShutting down\n", .{msg}) catch unreachable; - std.process.exit(0); -} - -export fn roc_memcpy(dst: [*]u8, src: [*]u8, size: usize) callconv(.C) void { - return memcpy(dst, src, size); -} - -export fn roc_memset(dst: [*]u8, value: i32, size: usize) callconv(.C) void { - return memset(dst, value, size); -} - -const Unit = extern struct {}; - -pub export fn main() callconv(.C) u8 { - const allocator = std.heap.page_allocator; - - const size = @intCast(usize, roc__mainForHost_size()); - const raw_output = allocator.allocAdvanced(u8, @alignOf(u64), @intCast(usize, size), .at_least) catch unreachable; - var output = @ptrCast([*]u8, raw_output); - - defer { - allocator.free(raw_output); - } - - var ts1: std.os.timespec = undefined; - std.os.clock_gettime(std.os.CLOCK.REALTIME, &ts1) catch unreachable; - - roc__mainForHost_1_exposed_generic(output); - - const closure_data_pointer = @ptrCast([*]u8, output); - - call_the_closure(closure_data_pointer); - - var ts2: std.os.timespec = undefined; - std.os.clock_gettime(std.os.CLOCK.REALTIME, &ts2) catch unreachable; - - const delta = to_seconds(ts2) - to_seconds(ts1); - - const stderr = std.io.getStdErr().writer(); - stderr.print("runtime: {d:.3}ms\n", .{delta * 1000}) catch unreachable; - - return 0; -} - -fn to_seconds(tms: std.os.timespec) f64 { - return @intToFloat(f64, tms.tv_sec) + (@intToFloat(f64, tms.tv_nsec) / 1_000_000_000.0); -} - -fn call_the_closure(closure_data_pointer: [*]u8) void { - const allocator = std.heap.page_allocator; - - const size = roc__mainForHost_1_Fx_result_size(); - const raw_output = allocator.allocAdvanced(u8, @alignOf(u64), @intCast(usize, size), .at_least) catch unreachable; - var output = @ptrCast([*]u8, raw_output); - - defer { - allocator.free(raw_output); - } - - const flags: u8 = 0; - - roc__mainForHost_1_Fx_caller(&flags, closure_data_pointer, output); - - // The closure returns result, nothing interesting to do with it - return; -} - -pub export fn roc_fx_putInt(int: i64) i64 { - const stdout = std.io.getStdOut().writer(); - - stdout.print("{d}", .{int}) catch unreachable; - - stdout.print("\n", .{}) catch unreachable; - - return 0; -} - -export fn roc_fx_putLine(rocPath: *str.RocStr) callconv(.C) void { - const stdout = std.io.getStdOut().writer(); - - for (rocPath.asSlice()) |char| { - stdout.print("{c}", .{char}) catch unreachable; - } - - stdout.print("\n", .{}) catch unreachable; -} - -const GetInt = extern struct { - value: i64, - is_error: bool, -}; - -comptime { - if (@sizeOf(usize) == 8) { - @export(roc_fx_getInt_64bit, .{ .name = "roc_fx_getInt" }); - } else { - @export(roc_fx_getInt_32bit, .{ .name = "roc_fx_getInt" }); - } -} - -fn roc_fx_getInt_64bit() callconv(.C) GetInt { - if (roc_fx_getInt_help()) |value| { - const get_int = GetInt{ .is_error = false, .value = value }; - return get_int; - } else |err| switch (err) { - error.InvalidCharacter => { - return GetInt{ .is_error = true, .value = 0 }; - }, - else => { - return GetInt{ .is_error = true, .value = 0 }; - }, - } - - return 0; -} - -fn roc_fx_getInt_32bit(output: *GetInt) callconv(.C) void { - if (roc_fx_getInt_help()) |value| { - const get_int = GetInt{ .is_error = false, .value = value, .error_code = false }; - output.* = get_int; - } else |err| switch (err) { - error.InvalidCharacter => { - output.* = GetInt{ .is_error = true, .value = 0, .error_code = false }; - }, - else => { - output.* = GetInt{ .is_error = true, .value = 0, .error_code = true }; - }, - } - - return; -} - -fn roc_fx_getInt_help() !i64 { - const stdin = std.io.getStdIn().reader(); - var buf: [40]u8 = undefined; - - const line: []u8 = (try stdin.readUntilDelimiterOrEof(&buf, '\n')) orelse ""; - - return std.fmt.parseInt(i64, line, 10); -} diff --git a/examples/breakout/.gitignore b/examples/breakout/.gitignore deleted file mode 100644 index 593993d712..0000000000 --- a/examples/breakout/.gitignore +++ /dev/null @@ -1 +0,0 @@ -breakout diff --git a/examples/breakout/breakout.roc b/examples/breakout/breakout.roc deleted file mode 100644 index 89a883e4a2..0000000000 --- a/examples/breakout/breakout.roc +++ /dev/null @@ -1,161 +0,0 @@ -app "breakout" - packages { pf: "platform" } - imports [pf.Game.{ Bounds, Elem, Event }] - provides [program] { Model } to pf - -paddleWidth = 0.2# width of the paddle, as a % of screen width -paddleHeight = 50# height of the paddle, in pixels -paddleSpeed = 65# how many pixels the paddle moves per keypress -blockHeight = 80# height of a block, in pixels -blockBorder = 0.025# border of a block, as a % of its width -ballSize = 55 -numRows = 4 -numCols = 8 -numBlocks = numRows * numCols - -Model : { - # Screen height and width - height : F32, - width : F32, - # Paddle X-coordinate - paddleX : F32, - # Ball coordinates - ballX : F32, - ballY : F32, - dBallX : F32, - # delta x - how much it moves per tick - dBallY : F32, - # delta y - how much it moves per tick - } - -init : Bounds -> Model -init = \{ width, height } -> { - # Screen height and width - width, - height, - # Paddle X-coordinate - paddleX: (width * 0.5) - (paddleWidth * width * 0.5), - # Ball coordinates - ballX: width * 0.5, - ballY: height * 0.4, - # Delta - how much ball moves in each tick - dBallX: 4, - dBallY: 4, -} - -update : Model, Event -> Model -update = \model, event -> - when event is - Resize size -> - { model & width: size.width, height: size.height } - KeyDown Left -> - { model & paddleX: model.paddleX - paddleSpeed } - KeyDown Right -> - { model & paddleX: model.paddleX + paddleSpeed } - Tick _ -> - tick model - _ -> - model - -tick : Model -> Model -tick = \model -> - model - |> moveBall - -moveBall : Model -> Model -moveBall = \model -> - ballX = model.ballX + model.dBallX - ballY = model.ballY + model.dBallY - - paddleTop = model.height - blockHeight - (paddleHeight * 2) - paddleLeft = model.paddleX - paddleRight = paddleLeft + (model.width * paddleWidth) - - # If its y used to be less than the paddle, and now it's greater than or equal, - # then this is the frame where the ball collided with it. - crossingPaddle = model.ballY < paddleTop && ballY >= paddleTop - - # If it collided with the paddle, bounce off. - directionChange = - if crossingPaddle && (ballX >= paddleLeft && ballX <= paddleRight) then - -1f32 - else - 1f32 - - dBallX = model.dBallX * directionChange - dBallY = model.dBallY * directionChange - - { model & ballX, ballY, dBallX, dBallY } - -render : Model -> List Elem -render = \model -> - - blocks = List.map - (List.range 0 numBlocks) - \index -> - col = - Num.rem index numCols - |> Num.toF32 - - row = - index - // numCols - |> Num.toF32 - - red = col / Num.toF32 numCols - green = row / Num.toF32 numRows - blue = Num.toF32 index / Num.toF32 numBlocks - - color = { r: red * 0.8, g: 0.2 + green * 0.6, b: 0.2 + blue * 0.8, a: 1 } - - { row, col, color } - - blockWidth = model.width / numCols - - rects = - List.joinMap - blocks - \{ row, col, color } -> - left = Num.toF32 col * blockWidth - top = Num.toF32 (row * blockHeight) - border = blockBorder * blockWidth - - outer = Rect { - left, - top, - width: blockWidth, - height: blockHeight, - color: { r: color.r * 0.8, g: color.g * 0.8, b: color.b * 0.8, a: 1 }, - } - - inner = Rect { - left: left + border, - top: top + border, - width: blockWidth - (border * 2), - height: blockHeight - (border * 2), - color, - } - - [outer, inner] - - ball = - color = { r: 0.7, g: 0.3, b: 0.9, a: 1.0 } - width = ballSize - height = ballSize - left = model.ballX - top = model.ballY - - Rect { left, top, width, height, color } - - paddle = - color = { r: 0.8, g: 0.8, b: 0.8, a: 1.0 } - width = model.width * paddleWidth - height = paddleHeight - left = model.paddleX - top = model.height - blockHeight - height - - Rect { left, top, width, height, color } - - List.concat rects [paddle, ball] - -program = { init, update, render } diff --git a/examples/breakout/hello.roc b/examples/breakout/hello.roc deleted file mode 100644 index 3eced06f51..0000000000 --- a/examples/breakout/hello.roc +++ /dev/null @@ -1,17 +0,0 @@ -app "breakout" - packages { pf: "platform" } - imports [pf.Game.{ Bounds, Elem, Event }] - provides [program] { Model } to pf - -Model : { text : Str } - -init : Bounds -> Model -init = \_ -> { text: "Hello, World!" } - -update : Model, Event -> Model -update = \model, _ -> model - -render : Model -> List Elem -render = \model -> [Text model.text] - -program = { init, update, render } diff --git a/examples/breakout/platform/Action.roc b/examples/breakout/platform/Action.roc deleted file mode 100644 index 1a499db63f..0000000000 --- a/examples/breakout/platform/Action.roc +++ /dev/null @@ -1,19 +0,0 @@ -interface Action - exposes [Action, none, update, map] - imports [] - -Action state : [None, Update state] - -none : Action * -none = None - -update : state -> Action state -update = Update - -map : Action a, (a -> b) -> Action b -map = \action, transform -> - when action is - None -> - None - Update state -> - Update (transform state) diff --git a/examples/breakout/platform/Cargo.lock b/examples/breakout/platform/Cargo.lock deleted file mode 100644 index 8f8005c26d..0000000000 --- a/examples/breakout/platform/Cargo.lock +++ /dev/null @@ -1,2835 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 3 - -[[package]] -name = "ab_glyph" -version = "0.2.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61caed9aec6daeee1ea38ccf5fb225e4f96c1eeead1b4a5c267324a63cf02326" -dependencies = [ - "ab_glyph_rasterizer", - "owned_ttf_parser", -] - -[[package]] -name = "ab_glyph_rasterizer" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a13739d7177fbd22bb0ed28badfff9f372f8bef46c863db4e1c6248f6b223b6e" - -[[package]] -name = "addr2line" -version = "0.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9ecd88a8c8378ca913a680cd98f0f13ac67383d35993f86c90a70e3f137816b" -dependencies = [ - "gimli", -] - -[[package]] -name = "adler" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" - -[[package]] -name = "ahash" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" -dependencies = [ - "getrandom", - "once_cell", - "version_check", -] - -[[package]] -name = "aho-corasick" -version = "0.7.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" -dependencies = [ - "memchr", -] - -[[package]] -name = "alsa" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75c4da790adcb2ce5e758c064b4f3ec17a30349f9961d3e5e6c9688b052a9e18" -dependencies = [ - "alsa-sys", - "bitflags", - "libc", - "nix 0.20.0", -] - -[[package]] -name = "alsa-sys" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db8fee663d06c4e303404ef5f40488a53e062f89ba8bfed81f42325aafad1527" -dependencies = [ - "libc", - "pkg-config", -] - -[[package]] -name = "approx" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f2a05fd1bd10b2527e20a2cd32d8873d115b8b39fe219ee25f42a8aca6ba278" -dependencies = [ - "num-traits", -] - -[[package]] -name = "approx" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "072df7202e63b127ab55acfe16ce97013d5b97bf160489336d3f1840fd78e99e" -dependencies = [ - "num-traits", -] - -[[package]] -name = "arrayvec" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6" - -[[package]] -name = "ash" -version = "0.35.1+1.2.203" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7fd04def1c9101b5fb488c131022d2d6f87753ef4b1b11b279e2af404fae6b9" -dependencies = [ - "libloading", -] - -[[package]] -name = "atty" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" -dependencies = [ - "hermit-abi", - "libc", - "winapi", -] - -[[package]] -name = "autocfg" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" - -[[package]] -name = "backtrace" -version = "0.3.63" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "321629d8ba6513061f26707241fa9bc89524ff1cd7a915a97ef0c62c666ce1b6" -dependencies = [ - "addr2line", - "cc", - "cfg-if 1.0.0", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", -] - -[[package]] -name = "bindgen" -version = "0.56.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2da379dbebc0b76ef63ca68d8fc6e71c0f13e59432e0987e508c1820e6ab5239" -dependencies = [ - "bitflags", - "cexpr", - "clang-sys", - "lazy_static", - "lazycell", - "peeking_take_while", - "proc-macro2", - "quote", - "regex", - "rustc-hash", - "shlex", -] - -[[package]] -name = "bit-set" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e11e16035ea35e4e5997b393eacbf6f63983188f7a2ad25bfb13465f5ad59de" -dependencies = [ - "bit-vec", -] - -[[package]] -name = "bit-vec" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" - -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "block" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" - -[[package]] -name = "block-buffer" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0940dc441f31689269e10ac70eb1002a3a1d3ad1390e030043662eb7fe4688b" -dependencies = [ - "block-padding", - "byte-tools", - "byteorder", - "generic-array", -] - -[[package]] -name = "block-padding" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa79dedbb091f449f1f39e53edf88d5dbe95f895dae6135a8d7b881fb5af73f5" -dependencies = [ - "byte-tools", -] - -[[package]] -name = "bumpalo" -version = "3.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4a45a46ab1f2412e53d3a0ade76ffad2025804294569aae387231a0cd6e0899" - -[[package]] -name = "byte-tools" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7" - -[[package]] -name = "bytemuck" -version = "1.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439989e6b8c38d1b6570a384ef1e49c8848128f5a97f3914baef02920842712f" -dependencies = [ - "bytemuck_derive", -] - -[[package]] -name = "bytemuck_derive" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e215f8c2f9f79cb53c8335e687ffd07d5bfcb6fe5fc80723762d0be46e7cc54" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "byteorder" -version = "1.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" - -[[package]] -name = "bytes" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" - -[[package]] -name = "calloop" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf2eec61efe56aa1e813f5126959296933cf0700030e4314786c48779a66ab82" -dependencies = [ - "log", - "nix 0.22.0", -] - -[[package]] -name = "cc" -version = "1.0.72" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22a9137b95ea06864e018375b72adfb7db6e6f68cfc8df5a04d00288050485ee" -dependencies = [ - "jobserver", -] - -[[package]] -name = "cesu8" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" - -[[package]] -name = "cexpr" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4aedb84272dbe89af497cf81375129abda4fc0a9e7c5d317498c15cc30c0d27" -dependencies = [ - "nom 5.1.2", -] - -[[package]] -name = "cfg-if" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" - -[[package]] -name = "cfg-if" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" - -[[package]] -name = "cfg_aliases" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" - -[[package]] -name = "cgmath" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a98d30140e3296250832bbaaff83b27dcd6fa3cc70fb6f1f3e5c9c0023b5317" -dependencies = [ - "approx 0.4.0", - "num-traits", -] - -[[package]] -name = "clang-sys" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa66045b9cb23c2e9c1520732030608b02ee07e5cfaa5a521ec15ded7fa24c90" -dependencies = [ - "glob", - "libc", - "libloading", -] - -[[package]] -name = "claxon" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bfbf56724aa9eca8afa4fcfadeb479e722935bb2a0900c2d37e0cc477af0688" - -[[package]] -name = "clipboard-win" -version = "3.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fdf5e01086b6be750428ba4a40619f847eb2e95756eee84b18e06e5f0b50342" -dependencies = [ - "lazy-bytes-cast", - "winapi", -] - -[[package]] -name = "cocoa" -version = "0.24.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f63902e9223530efb4e26ccd0cf55ec30d592d3b42e21a28defc42a9586e832" -dependencies = [ - "bitflags", - "block", - "cocoa-foundation", - "core-foundation 0.9.2", - "core-graphics 0.22.3", - "foreign-types", - "libc", - "objc", -] - -[[package]] -name = "cocoa-foundation" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ade49b65d560ca58c403a479bb396592b155c0185eada742ee323d1d68d6318" -dependencies = [ - "bitflags", - "block", - "core-foundation 0.9.2", - "core-graphics-types", - "foreign-types", - "libc", - "objc", -] - -[[package]] -name = "codespan-reporting" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" -dependencies = [ - "termcolor", - "unicode-width", -] - -[[package]] -name = "colored" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3616f750b84d8f0de8a58bda93e08e2a81ad3f523089b05f1dffecab48c6cbd" -dependencies = [ - "atty", - "lazy_static", - "winapi", -] - -[[package]] -name = "combine" -version = "4.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50b727aacc797f9fc28e355d21f34709ac4fc9adecfe470ad07b8f4464f53062" -dependencies = [ - "bytes", - "memchr", -] - -[[package]] -name = "confy" -version = "0.4.0" -source = "git+https://github.com/rust-cli/confy#664992aecd97b4af0eda8d9d2825885662e1c6b4" -dependencies = [ - "directories-next", - "serde", - "serde_yaml", -] - -[[package]] -name = "copyless" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2df960f5d869b2dd8532793fde43eb5427cceb126c929747a26823ab0eeb536" - -[[package]] -name = "copypasta" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4423d79fed83ebd9ab81ec21fa97144300a961782158287dc9bf7eddac37ff0b" -dependencies = [ - "clipboard-win", - "objc", - "objc-foundation", - "objc_id", - "smithay-clipboard", - "x11-clipboard", -] - -[[package]] -name = "core-foundation" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57d24c7a13c43e870e37c1556b74555437870a04514f7685f5b354e090567171" -dependencies = [ - "core-foundation-sys 0.7.0", - "libc", -] - -[[package]] -name = "core-foundation" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6888e10551bb93e424d8df1d07f1a8b4fceb0001a3a4b048bfc47554946f47b3" -dependencies = [ - "core-foundation-sys 0.8.3", - "libc", -] - -[[package]] -name = "core-foundation-sys" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3a71ab494c0b5b860bdc8407ae08978052417070c2ced38573a9157ad75b8ac" - -[[package]] -name = "core-foundation-sys" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" - -[[package]] -name = "core-graphics" -version = "0.19.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3889374e6ea6ab25dba90bb5d96202f61108058361f6dc72e8b03e6f8bbe923" -dependencies = [ - "bitflags", - "core-foundation 0.7.0", - "foreign-types", - "libc", -] - -[[package]] -name = "core-graphics" -version = "0.22.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2581bbab3b8ffc6fcbd550bf46c355135d16e9ff2a6ea032ad6b9bf1d7efe4fb" -dependencies = [ - "bitflags", - "core-foundation 0.9.2", - "core-graphics-types", - "foreign-types", - "libc", -] - -[[package]] -name = "core-graphics-types" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a68b68b3446082644c91ac778bf50cd4104bfb002b5a6a7c44cca5a2c70788b" -dependencies = [ - "bitflags", - "core-foundation 0.9.2", - "foreign-types", - "libc", -] - -[[package]] -name = "core-video-sys" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34ecad23610ad9757664d644e369246edde1803fcb43ed72876565098a5d3828" -dependencies = [ - "cfg-if 0.1.10", - "core-foundation-sys 0.7.0", - "core-graphics 0.19.2", - "libc", - "objc", -] - -[[package]] -name = "coreaudio-rs" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11894b20ebfe1ff903cbdc52259693389eea03b94918a2def2c30c3bf227ad88" -dependencies = [ - "bitflags", - "coreaudio-sys", -] - -[[package]] -name = "coreaudio-sys" -version = "0.2.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b7e3347be6a09b46aba228d6608386739fb70beff4f61e07422da87b0bb31fa" -dependencies = [ - "bindgen", -] - -[[package]] -name = "cpal" -version = "0.13.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98f45f0a21f617cd2c788889ef710b63f075c949259593ea09c826f1e47a2418" -dependencies = [ - "alsa", - "core-foundation-sys 0.8.3", - "coreaudio-rs", - "jni", - "js-sys", - "lazy_static", - "libc", - "mach", - "ndk 0.3.0", - "ndk-glue 0.3.0", - "nix 0.20.0", - "oboe", - "parking_lot", - "stdweb", - "thiserror", - "web-sys", - "winapi", -] - -[[package]] -name = "crossbeam-channel" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e54ea8bc3fb1ee042f5aace6e3c6e025d3874866da222930f70ce62aceba0bfa" -dependencies = [ - "cfg-if 1.0.0", - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-deque" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6455c0ca19f0d2fbf751b908d5c55c1f5cbc65e03c4225427254b46890bdde1e" -dependencies = [ - "cfg-if 1.0.0", - "crossbeam-epoch", - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-epoch" -version = "0.9.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97242a70df9b89a65d0b6df3c4bf5b9ce03c5b7309019777fbde37e7537f8762" -dependencies = [ - "cfg-if 1.0.0", - "crossbeam-utils", - "lazy_static", - "memoffset", - "scopeguard", -] - -[[package]] -name = "crossbeam-utils" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcae03edb34f947e64acdb1c33ec169824e20657e9ecb61cef6c8c74dcb8120" -dependencies = [ - "cfg-if 1.0.0", - "lazy_static", -] - -[[package]] -name = "cty" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b365fabc795046672053e29c954733ec3b05e4be654ab130fe8f1f94d7051f35" - -[[package]] -name = "d3d12" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2daefd788d1e96e0a9d66dee4b828b883509bc3ea9ce30665f04c3246372690c" -dependencies = [ - "bitflags", - "libloading", - "winapi", -] - -[[package]] -name = "darling" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d706e75d87e35569db781a9b5e2416cff1236a47ed380831f959382ccd5f858" -dependencies = [ - "darling_core 0.10.2", - "darling_macro 0.10.2", -] - -[[package]] -name = "darling" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0d720b8683f8dd83c65155f0530560cba68cd2bf395f6513a483caee57ff7f4" -dependencies = [ - "darling_core 0.13.1", - "darling_macro 0.13.1", -] - -[[package]] -name = "darling_core" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0c960ae2da4de88a91b2d920c2a7233b400bc33cb28453a2987822d8392519b" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim 0.9.3", - "syn", -] - -[[package]] -name = "darling_core" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a340f241d2ceed1deb47ae36c4144b2707ec7dd0b649f894cb39bb595986324" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim 0.10.0", - "syn", -] - -[[package]] -name = "darling_macro" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b5a2f4ac4969822c62224815d069952656cadc7084fdca9751e6d959189b72" -dependencies = [ - "darling_core 0.10.2", - "quote", - "syn", -] - -[[package]] -name = "darling_macro" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72c41b3b7352feb3211a0d743dc5700a4e3b60f51bd2b368892d1e0f9a95f44b" -dependencies = [ - "darling_core 0.13.1", - "quote", - "syn", -] - -[[package]] -name = "digest" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5" -dependencies = [ - "generic-array", -] - -[[package]] -name = "directories-next" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "339ee130d97a610ea5a5872d2bbb130fdf68884ff09d3028b81bec8a1ac23bbc" -dependencies = [ - "cfg-if 1.0.0", - "dirs-sys-next", -] - -[[package]] -name = "dirs-sys-next" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" -dependencies = [ - "libc", - "redox_users", - "winapi", -] - -[[package]] -name = "dispatch" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" - -[[package]] -name = "dlib" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac1b7517328c04c2aa68422fc60a41b92208182142ed04a25879c26c8f878794" -dependencies = [ - "libloading", -] - -[[package]] -name = "doc-comment" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" - -[[package]] -name = "downcast-rs" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650" - -[[package]] -name = "either" -version = "1.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" - -[[package]] -name = "env_logger" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b2cf0344971ee6c64c31be0d530793fba457d322dfec2810c453d0ef228f9c3" -dependencies = [ - "atty", - "humantime", - "log", - "regex", - "termcolor", -] - -[[package]] -name = "fake-simd" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed" - -[[package]] -name = "find-crate" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59a98bbaacea1c0eb6a0876280051b892eb73594fd90cf3b20e9c817029c57d2" -dependencies = [ - "toml", -] - -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - -[[package]] -name = "foreign-types" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" -dependencies = [ - "foreign-types-shared", -] - -[[package]] -name = "foreign-types-shared" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" - -[[package]] -name = "fs_extra" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2022715d62ab30faffd124d40b76f4134a550a87792276512b18d63272333394" - -[[package]] -name = "futures" -version = "0.3.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28560757fe2bb34e79f907794bb6b22ae8b0e5c669b638a1132f2592b19035b4" -dependencies = [ - "futures-channel", - "futures-core", - "futures-executor", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-channel" -version = "0.3.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba3dda0b6588335f360afc675d0564c17a77a2bda81ca178a4b6081bd86c7f0b" -dependencies = [ - "futures-core", - "futures-sink", -] - -[[package]] -name = "futures-core" -version = "0.3.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0c8ff0461b82559810cdccfde3215c3f373807f5e5232b71479bff7bb2583d7" - -[[package]] -name = "futures-executor" -version = "0.3.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29d6d2ff5bb10fb95c85b8ce46538a2e5f5e7fdc755623a7d4529ab8a4ed9d2a" -dependencies = [ - "futures-core", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-io" -version = "0.3.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1f9d34af5a1aac6fb380f735fe510746c38067c5bf16c7fd250280503c971b2" - -[[package]] -name = "futures-macro" -version = "0.3.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dbd947adfffb0efc70599b3ddcf7b5597bb5fa9e245eb99f62b3a5f7bb8bd3c" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "futures-sink" -version = "0.3.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3055baccb68d74ff6480350f8d6eb8fcfa3aa11bdc1a1ae3afdd0514617d508" - -[[package]] -name = "futures-task" -version = "0.3.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ee7c6485c30167ce4dfb83ac568a849fe53274c831081476ee13e0dce1aad72" - -[[package]] -name = "futures-util" -version = "0.3.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b5cf40b47a271f77a8b1bec03ca09044d99d2372c0de244e66430761127164" -dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-macro", - "futures-sink", - "futures-task", - "memchr", - "pin-project-lite", - "pin-utils", - "slab", -] - -[[package]] -name = "fxhash" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" -dependencies = [ - "byteorder", -] - -[[package]] -name = "generic-array" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffdf9f34f1447443d37393cc6c2b8313aebddcd96906caf34e54c68d8e57d7bd" -dependencies = [ - "typenum", -] - -[[package]] -name = "getrandom" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "418d37c8b1d42553c93648be529cb70f920d3baf8ef469b74b9638df426e0b4c" -dependencies = [ - "cfg-if 1.0.0", - "libc", - "wasi", -] - -[[package]] -name = "gimli" -version = "0.26.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78cc372d058dcf6d5ecd98510e7fbc9e5aec4d21de70f65fea8fecebcd881bd4" - -[[package]] -name = "glob" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" - -[[package]] -name = "glow" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8bd5877156a19b8ac83a29b2306fe20537429d318f3ff0a1a2119f8d9c61919" -dependencies = [ - "js-sys", - "slotmap", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "glyph_brush" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21932fbf719272848eec4583740d978203c6e7da4c4e203358f5b95946c97409" -dependencies = [ - "glyph_brush_draw_cache", - "glyph_brush_layout", - "log", - "ordered-float", - "rustc-hash", - "twox-hash", -] - -[[package]] -name = "glyph_brush_draw_cache" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6010675390f6889e09a21e2c8b575b3ee25667ea8237a8d59423f73cb8c28610" -dependencies = [ - "ab_glyph", - "crossbeam-channel", - "crossbeam-deque", - "linked-hash-map", - "rayon", - "rustc-hash", -] - -[[package]] -name = "glyph_brush_layout" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc32c2334f00ca5ac3695c5009ae35da21da8c62d255b5b96d56e2597a637a38" -dependencies = [ - "ab_glyph", - "approx 0.5.0", - "xi-unicode", -] - -[[package]] -name = "gpu-alloc" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fc59e5f710e310e76e6707f86c561dd646f69a8876da9131703b2f717de818d" -dependencies = [ - "bitflags", - "gpu-alloc-types", -] - -[[package]] -name = "gpu-alloc-types" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54804d0d6bc9d7f26db4eaec1ad10def69b599315f487d32c334a80d1efe67a5" -dependencies = [ - "bitflags", -] - -[[package]] -name = "gpu-descriptor" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a538f217be4d405ff4719a283ca68323cc2384003eca5baaa87501e821c81dda" -dependencies = [ - "bitflags", - "gpu-descriptor-types", - "hashbrown", -] - -[[package]] -name = "gpu-descriptor-types" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "363e3677e55ad168fef68cf9de3a4a310b53124c5e784c53a1d70e92d23f2126" -dependencies = [ - "bitflags", -] - -[[package]] -name = "hashbrown" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" -dependencies = [ - "ahash", -] - -[[package]] -name = "hermit-abi" -version = "0.1.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" -dependencies = [ - "libc", -] - -[[package]] -name = "hexf-parse" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" - -[[package]] -name = "host" -version = "0.1.0" -dependencies = [ - "arrayvec", - "bytemuck", - "cgmath", - "colored", - "confy", - "copypasta", - "env_logger", - "fs_extra", - "futures", - "glyph_brush", - "libc", - "log", - "nonempty", - "page_size", - "palette", - "pest", - "pest_derive", - "roc_std", - "rodio", - "serde", - "snafu", - "threadpool", - "wgpu", - "wgpu_glyph", - "winit", -] - -[[package]] -name = "hound" -version = "3.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a164bb2ceaeff4f42542bdb847c41517c78a60f5649671b2a07312b6e117549" - -[[package]] -name = "humantime" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" - -[[package]] -name = "ident_case" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" - -[[package]] -name = "indexmap" -version = "1.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282a6247722caba404c065016bbfa522806e51714c34f5dfc3e4a3a46fcb4223" -dependencies = [ - "autocfg", - "hashbrown", -] - -[[package]] -name = "inplace_it" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90953f308a79fe6d62a4643e51f848fbfddcd05975a38e69fdf4ab86a7baf7ca" - -[[package]] -name = "instant" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" -dependencies = [ - "cfg-if 1.0.0", - "js-sys", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "jni" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6df18c2e3db7e453d3c6ac5b3e9d5182664d28788126d39b91f2d1e22b017ec" -dependencies = [ - "cesu8", - "combine", - "jni-sys", - "log", - "thiserror", - "walkdir", -] - -[[package]] -name = "jni-sys" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" - -[[package]] -name = "jobserver" -version = "0.1.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af25a77299a7f711a01975c35a6a424eb6862092cc2d6c72c4ed6cbc56dfc1fa" -dependencies = [ - "libc", -] - -[[package]] -name = "js-sys" -version = "0.3.56" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a38fc24e30fd564ce974c02bf1d337caddff65be6cc4735a1f7eab22a7440f04" -dependencies = [ - "wasm-bindgen", -] - -[[package]] -name = "khronos-egl" -version = "4.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c2352bd1d0bceb871cb9d40f24360c8133c11d7486b68b5381c1dd1a32015e3" -dependencies = [ - "libc", - "libloading", - "pkg-config", -] - -[[package]] -name = "lazy-bytes-cast" -version = "5.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10257499f089cd156ad82d0a9cd57d9501fa2c989068992a97eb3c27836f206b" - -[[package]] -name = "lazy_static" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" - -[[package]] -name = "lazycell" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" - -[[package]] -name = "lewton" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "777b48df9aaab155475a83a7df3070395ea1ac6902f5cd062b8f2b028075c030" -dependencies = [ - "byteorder", - "ogg", - "tinyvec", -] - -[[package]] -name = "libc" -version = "0.2.113" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eef78b64d87775463c549fbd80e19249ef436ea3bf1de2a1eb7e717ec7fab1e9" - -[[package]] -name = "libloading" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efbc0f03f9a775e9f6aed295c6a1ba2253c5757a9e03d55c6caa46a681abcddd" -dependencies = [ - "cfg-if 1.0.0", - "winapi", -] - -[[package]] -name = "linked-hash-map" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3" - -[[package]] -name = "lock_api" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712a4d093c9976e24e7dbca41db895dabcbac38eb5f4045393d17a95bdfb1109" -dependencies = [ - "scopeguard", -] - -[[package]] -name = "log" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" -dependencies = [ - "cfg-if 1.0.0", -] - -[[package]] -name = "mach" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b823e83b2affd8f40a9ee8c29dbc56404c1e34cd2710921f2801e2cf29527afa" -dependencies = [ - "libc", -] - -[[package]] -name = "malloc_buf" -version = "0.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" -dependencies = [ - "libc", -] - -[[package]] -name = "maplit" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" - -[[package]] -name = "memchr" -version = "2.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" - -[[package]] -name = "memmap2" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b6c2ebff6180198788f5db08d7ce3bc1d0b617176678831a7510825973e357" -dependencies = [ - "libc", -] - -[[package]] -name = "memoffset" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" -dependencies = [ - "autocfg", -] - -[[package]] -name = "metal" -version = "0.23.1" -source = "git+https://github.com/gfx-rs/metal-rs?rev=44af5cc#44af5cca340617d42d701264f9bf71d1f3e68096" -dependencies = [ - "bitflags", - "block", - "core-graphics-types", - "foreign-types", - "log", - "objc", -] - -[[package]] -name = "minimal-lexical" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" - -[[package]] -name = "minimp3" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "985438f75febf74c392071a975a29641b420dd84431135a6e6db721de4b74372" -dependencies = [ - "minimp3-sys", - "slice-deque", - "thiserror", -] - -[[package]] -name = "minimp3-sys" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e21c73734c69dc95696c9ed8926a2b393171d98b3f5f5935686a26a487ab9b90" -dependencies = [ - "cc", -] - -[[package]] -name = "miniz_oxide" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a92518e98c078586bc6c934028adcca4c92a53d6a958196de835170a01d84e4b" -dependencies = [ - "adler", - "autocfg", -] - -[[package]] -name = "mio" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba272f85fa0b41fc91872be579b3bbe0f56b792aa361a380eb669469f68dafb2" -dependencies = [ - "libc", - "log", - "miow", - "ntapi", - "winapi", -] - -[[package]] -name = "miow" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21" -dependencies = [ - "winapi", -] - -[[package]] -name = "naga" -version = "0.8.0" -source = "git+https://github.com/gfx-rs/naga?rev=8e2e39e#8e2e39e4d8fa5bbb657c3b170b4f6607d703e284" -dependencies = [ - "bit-set", - "bitflags", - "codespan-reporting", - "hexf-parse", - "indexmap", - "log", - "num-traits", - "rustc-hash", - "spirv", - "thiserror", -] - -[[package]] -name = "ndk" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8794322172319b972f528bf90c6b467be0079f1fa82780ffb431088e741a73ab" -dependencies = [ - "jni-sys", - "ndk-sys 0.2.2", - "num_enum", - "thiserror", -] - -[[package]] -name = "ndk" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96d868f654c72e75f8687572699cdabe755f03effbb62542768e995d5b8d699d" -dependencies = [ - "bitflags", - "jni-sys", - "ndk-sys 0.2.2", - "num_enum", - "thiserror", -] - -[[package]] -name = "ndk" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2032c77e030ddee34a6787a64166008da93f6a352b629261d0fee232b8742dd4" -dependencies = [ - "bitflags", - "jni-sys", - "ndk-sys 0.3.0", - "num_enum", - "thiserror", -] - -[[package]] -name = "ndk-glue" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5caf0c24d51ac1c905c27d4eda4fa0635bbe0de596b8f79235e0b17a4d29385" -dependencies = [ - "lazy_static", - "libc", - "log", - "ndk 0.3.0", - "ndk-macro 0.2.0", - "ndk-sys 0.2.2", -] - -[[package]] -name = "ndk-glue" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc291b8de2095cba8dab7cf381bf582ff4c17a09acf854c32e46545b08085d28" -dependencies = [ - "lazy_static", - "libc", - "log", - "ndk 0.5.0", - "ndk-macro 0.3.0", - "ndk-sys 0.2.2", -] - -[[package]] -name = "ndk-glue" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04c0d14b0858eb9962a5dac30b809b19f19da7e4547d64af2b0bb051d2e55d79" -dependencies = [ - "lazy_static", - "libc", - "log", - "ndk 0.6.0", - "ndk-macro 0.3.0", - "ndk-sys 0.3.0", -] - -[[package]] -name = "ndk-macro" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05d1c6307dc424d0f65b9b06e94f88248e6305726b14729fd67a5e47b2dc481d" -dependencies = [ - "darling 0.10.2", - "proc-macro-crate 0.1.5", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "ndk-macro" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0df7ac00c4672f9d5aece54ee3347520b7e20f158656c7db2e6de01902eb7a6c" -dependencies = [ - "darling 0.13.1", - "proc-macro-crate 1.1.0", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "ndk-sys" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1bcdd74c20ad5d95aacd60ef9ba40fdf77f767051040541df557b7a9b2a2121" - -[[package]] -name = "ndk-sys" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e5a6ae77c8ee183dcbbba6150e2e6b9f3f4196a7666c02a715a95692ec1fa97" -dependencies = [ - "jni-sys", -] - -[[package]] -name = "nix" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa9b4819da1bc61c0ea48b63b7bc8604064dd43013e7cc325df098d49cd7c18a" -dependencies = [ - "bitflags", - "cc", - "cfg-if 1.0.0", - "libc", -] - -[[package]] -name = "nix" -version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf1e25ee6b412c2a1e3fcb6a4499a5c1bfe7f43e014bdce9a6b6666e5aa2d187" -dependencies = [ - "bitflags", - "cc", - "cfg-if 1.0.0", - "libc", - "memoffset", -] - -[[package]] -name = "nom" -version = "5.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffb4262d26ed83a1c0a33a38fe2bb15797329c85770da05e6b828ddb782627af" -dependencies = [ - "memchr", - "version_check", -] - -[[package]] -name = "nom" -version = "7.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b1d11e1ef389c76fe5b81bcaf2ea32cf88b62bc494e19f493d0b30e7a930109" -dependencies = [ - "memchr", - "minimal-lexical", - "version_check", -] - -[[package]] -name = "nonempty" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9e591e719385e6ebaeb5ce5d3887f7d5676fceca6411d1925ccc95745f3d6f7" - -[[package]] -name = "ntapi" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f6bb902e437b6d86e03cce10a7e2af662292c5dfef23b65899ea3ac9354ad44" -dependencies = [ - "winapi", -] - -[[package]] -name = "num-derive" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "num-traits" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" -dependencies = [ - "autocfg", -] - -[[package]] -name = "num_cpus" -version = "1.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1" -dependencies = [ - "hermit-abi", - "libc", -] - -[[package]] -name = "num_enum" -version = "0.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "720d3ea1055e4e4574c0c0b0f8c3fd4f24c4cdaf465948206dea090b57b526ad" -dependencies = [ - "num_enum_derive", -] - -[[package]] -name = "num_enum_derive" -version = "0.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d992b768490d7fe0d8586d9b5745f6c49f557da6d81dc982b1d167ad4edbb21" -dependencies = [ - "proc-macro-crate 1.1.0", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "objc" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" -dependencies = [ - "malloc_buf", - "objc_exception", -] - -[[package]] -name = "objc-foundation" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" -dependencies = [ - "block", - "objc", - "objc_id", -] - -[[package]] -name = "objc_exception" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad970fb455818ad6cba4c122ad012fae53ae8b4795f86378bce65e4f6bab2ca4" -dependencies = [ - "cc", -] - -[[package]] -name = "objc_id" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" -dependencies = [ - "objc", -] - -[[package]] -name = "object" -version = "0.27.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67ac1d3f9a1d3616fd9a60c8d74296f22406a238b6a72f5cc1e6f314df4ffbf9" -dependencies = [ - "memchr", -] - -[[package]] -name = "oboe" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2463c8f2e19b4e0d0710a21f8e4011501ff28db1c95d7a5482a553b2100502d2" -dependencies = [ - "jni", - "ndk 0.6.0", - "ndk-glue 0.6.0", - "num-derive", - "num-traits", - "oboe-sys", -] - -[[package]] -name = "oboe-sys" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3370abb7372ed744232c12954d920d1a40f1c4686de9e79e800021ef492294bd" -dependencies = [ - "cc", -] - -[[package]] -name = "ogg" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6951b4e8bf21c8193da321bcce9c9dd2e13c858fe078bf9054a288b419ae5d6e" -dependencies = [ - "byteorder", -] - -[[package]] -name = "once_cell" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da32515d9f6e6e489d7bc9d84c71b060db7247dc035bbe44eac88cf87486d8d5" - -[[package]] -name = "opaque-debug" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c" - -[[package]] -name = "ordered-float" -version = "2.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7940cf2ca942593318d07fcf2596cdca60a85c9e7fab408a5e21a4f9dcd40d87" -dependencies = [ - "num-traits", -] - -[[package]] -name = "owned_ttf_parser" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ef05f2882a8b3e7acc10c153ade2631f7bfc8ce00d2bf3fb8f4e9d2ae6ea5c3" -dependencies = [ - "ttf-parser", -] - -[[package]] -name = "page_size" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eebde548fbbf1ea81a99b128872779c437752fb99f217c45245e1a61dcd9edcd" -dependencies = [ - "libc", - "winapi", -] - -[[package]] -name = "palette" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9735f7e1e51a3f740bacd5dc2724b61a7806f23597a8736e679f38ee3435d18" -dependencies = [ - "approx 0.5.0", - "num-traits", - "palette_derive", - "phf", -] - -[[package]] -name = "palette_derive" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7799c3053ea8a6d8a1193c7ba42f534e7863cf52e378a7f90406f4a645d33bad" -dependencies = [ - "find-crate", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "parking_lot" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" -dependencies = [ - "instant", - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216" -dependencies = [ - "cfg-if 1.0.0", - "instant", - "libc", - "redox_syscall", - "smallvec", - "winapi", -] - -[[package]] -name = "peeking_take_while" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" - -[[package]] -name = "percent-encoding" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" - -[[package]] -name = "pest" -version = "2.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10f4872ae94d7b90ae48754df22fd42ad52ce740b8f370b03da4835417403e53" -dependencies = [ - "ucd-trie", -] - -[[package]] -name = "pest_derive" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "833d1ae558dc601e9a60366421196a8d94bc0ac980476d0b67e1d0988d72b2d0" -dependencies = [ - "pest", - "pest_generator", -] - -[[package]] -name = "pest_generator" -version = "2.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99b8db626e31e5b81787b9783425769681b347011cc59471e33ea46d2ea0cf55" -dependencies = [ - "pest", - "pest_meta", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "pest_meta" -version = "2.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54be6e404f5317079812fc8f9f5279de376d8856929e21c184ecf6bbd692a11d" -dependencies = [ - "maplit", - "pest", - "sha-1", -] - -[[package]] -name = "phf" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2ac8b67553a7ca9457ce0e526948cad581819238f4a9d1ea74545851fa24f37" -dependencies = [ - "phf_macros", - "phf_shared", - "proc-macro-hack", -] - -[[package]] -name = "phf_generator" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d43f3220d96e0080cc9ea234978ccd80d904eafb17be31bb0f76daaea6493082" -dependencies = [ - "phf_shared", - "rand", -] - -[[package]] -name = "phf_macros" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b706f5936eb50ed880ae3009395b43ed19db5bff2ebd459c95e7bf013a89ab86" -dependencies = [ - "phf_generator", - "phf_shared", - "proc-macro-hack", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "phf_shared" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a68318426de33640f02be62b4ae8eb1261be2efbc337b60c54d845bf4484e0d9" -dependencies = [ - "siphasher", -] - -[[package]] -name = "pin-project-lite" -version = "0.2.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e280fbe77cc62c91527259e9442153f4688736748d24660126286329742b4c6c" - -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - -[[package]] -name = "pkg-config" -version = "0.3.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58893f751c9b0412871a09abd62ecd2a00298c6c83befa223ef98c52aef40cbe" - -[[package]] -name = "ppv-lite86" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" - -[[package]] -name = "proc-macro-crate" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d6ea3c4595b96363c13943497db34af4460fb474a95c43f4446ad341b8c9785" -dependencies = [ - "toml", -] - -[[package]] -name = "proc-macro-crate" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ebace6889caf889b4d3f76becee12e90353f2b8c7d875534a71e5742f8f6f83" -dependencies = [ - "thiserror", - "toml", -] - -[[package]] -name = "proc-macro-hack" -version = "0.5.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5" - -[[package]] -name = "proc-macro2" -version = "1.0.36" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7342d5883fbccae1cc37a2353b09c87c9b0f3afd73f5fb9bba687a1f733b029" -dependencies = [ - "unicode-xid", -] - -[[package]] -name = "profiling" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9145ac0af1d93c638c98c40cf7d25665f427b2a44ad0a99b1dccf3e2f25bb987" - -[[package]] -name = "quick-xml" -version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8533f14c8382aaad0d592c812ac3b826162128b65662331e1127b45c3d18536b" -dependencies = [ - "memchr", -] - -[[package]] -name = "quote" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "864d3e96a899863136fc6e99f3d7cae289dafe43bf2c5ac19b70df7210c0a145" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "rand" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8" -dependencies = [ - "libc", - "rand_chacha", - "rand_core", - "rand_hc", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core", -] - -[[package]] -name = "rand_core" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" -dependencies = [ - "getrandom", -] - -[[package]] -name = "rand_hc" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d51e9f596de227fda2ea6c84607f5558e196eeaf43c986b724ba4fb8fdf497e7" -dependencies = [ - "rand_core", -] - -[[package]] -name = "range-alloc" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63e935c45e09cc6dcf00d2f0b2d630a58f4095320223d47fc68918722f0538b6" - -[[package]] -name = "raw-window-handle" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fba75eee94a9d5273a68c9e1e105d9cffe1ef700532325788389e5a83e2522b7" -dependencies = [ - "cty", -] - -[[package]] -name = "rayon" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c06aca804d41dbc8ba42dfd964f0d01334eceb64314b9ecf7c5fad5188a06d90" -dependencies = [ - "autocfg", - "crossbeam-deque", - "either", - "rayon-core", -] - -[[package]] -name = "rayon-core" -version = "1.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d78120e2c850279833f1dd3582f730c4ab53ed95aeaaaa862a2a5c71b1656d8e" -dependencies = [ - "crossbeam-channel", - "crossbeam-deque", - "crossbeam-utils", - "lazy_static", - "num_cpus", -] - -[[package]] -name = "redox_syscall" -version = "0.2.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff" -dependencies = [ - "bitflags", -] - -[[package]] -name = "redox_users" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "528532f3d801c87aec9def2add9ca802fe569e44a544afe633765267840abe64" -dependencies = [ - "getrandom", - "redox_syscall", -] - -[[package]] -name = "regex" -version = "1.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.6.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" - -[[package]] -name = "renderdoc-sys" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1382d1f0a252c4bf97dc20d979a2fdd05b024acd7c2ed0f7595d7817666a157" - -[[package]] -name = "roc_std" -version = "0.1.0" -dependencies = [ - "static_assertions 0.1.1", -] - -[[package]] -name = "rodio" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d98f5e557b61525057e2bc142c8cd7f0e70d75dc32852309bec440e6e046bf9" -dependencies = [ - "claxon", - "cpal", - "hound", - "lewton", - "minimp3", -] - -[[package]] -name = "rustc-demangle" -version = "0.1.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342" - -[[package]] -name = "rustc-hash" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" - -[[package]] -name = "ryu" -version = "1.0.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f" - -[[package]] -name = "same-file" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "scoped-tls" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea6a9290e3c9cf0f18145ef7ffa62d68ee0bf5fcd651017e586dc7fd5da448c2" - -[[package]] -name = "scopeguard" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" - -[[package]] -name = "serde" -version = "1.0.135" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cf9235533494ea2ddcdb794665461814781c53f19d87b76e571a1c35acbad2b" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.135" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8dcde03d87d4c973c04be249e7d8f0b35db1c848c487bd43032808e59dd8328d" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "serde_yaml" -version = "0.8.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4a521f2940385c165a24ee286aa8599633d162077a54bdcae2a6fd5a7bfa7a0" -dependencies = [ - "indexmap", - "ryu", - "serde", - "yaml-rust", -] - -[[package]] -name = "sha-1" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7d94d0bede923b3cea61f3f1ff57ff8cdfd77b400fb8f9998949e0cf04163df" -dependencies = [ - "block-buffer", - "digest", - "fake-simd", - "opaque-debug", -] - -[[package]] -name = "shlex" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fdf1b9db47230893d76faad238fd6097fd6d6a9245cd7a4d90dbd639536bbd2" - -[[package]] -name = "siphasher" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a86232ab60fa71287d7f2ddae4a7073f6b7aac33631c3015abb556f08c6d0a3e" - -[[package]] -name = "slab" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9def91fd1e018fe007022791f865d0ccc9b3a0d5001e01aabb8b40e46000afb5" - -[[package]] -name = "slice-deque" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31ef6ee280cdefba6d2d0b4b78a84a1c1a3f3a4cec98c2d4231c8bc225de0f25" -dependencies = [ - "libc", - "mach", - "winapi", -] - -[[package]] -name = "slotmap" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1e08e261d0e8f5c43123b7adf3e4ca1690d655377ac93a03b2c9d3e98de1342" -dependencies = [ - "version_check", -] - -[[package]] -name = "smallvec" -version = "1.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83" - -[[package]] -name = "smithay-client-toolkit" -version = "0.15.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1325f292209cee78d5035530932422a30aa4c8fda1a16593ac083c1de211e68a" -dependencies = [ - "bitflags", - "calloop", - "dlib", - "lazy_static", - "log", - "memmap2", - "nix 0.22.0", - "pkg-config", - "wayland-client", - "wayland-cursor", - "wayland-protocols", -] - -[[package]] -name = "smithay-clipboard" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "610b551bd25378bfd2b8e7a0fcbd83d427e8f2f6a40c47ae0f70688e9949dd55" -dependencies = [ - "smithay-client-toolkit", - "wayland-client", -] - -[[package]] -name = "snafu" -version = "0.6.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eab12d3c261b2308b0d80c26fffb58d17eba81a4be97890101f416b478c79ca7" -dependencies = [ - "backtrace", - "doc-comment", - "snafu-derive", -] - -[[package]] -name = "snafu-derive" -version = "0.6.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1508efa03c362e23817f96cde18abed596a25219a8b2c66e8db33c03543d315b" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "spirv" -version = "0.2.0+1.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "246bfa38fe3db3f1dfc8ca5a2cdeb7348c78be2112740cc0ec8ef18b6d94f830" -dependencies = [ - "bitflags", - "num-traits", -] - -[[package]] -name = "static_assertions" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f406d6ee68db6796e11ffd7b4d171864c58b7451e79ef9460ea33c287a1f89a7" - -[[package]] -name = "static_assertions" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" - -[[package]] -name = "stdweb" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef5430c8e36b713e13b48a9f709cc21e046723fe44ce34587b73a830203b533e" - -[[package]] -name = "strsim" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6446ced80d6c486436db5c078dde11a9f73d42b57fb273121e160b84f63d894c" - -[[package]] -name = "strsim" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" - -[[package]] -name = "syn" -version = "1.0.86" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a65b3f4ffa0092e9887669db0eae07941f023991ab58ea44da8fe8e2d511c6b" -dependencies = [ - "proc-macro2", - "quote", - "unicode-xid", -] - -[[package]] -name = "termcolor" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "thiserror" -version = "1.0.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417" -dependencies = [ - "thiserror-impl", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "threadpool" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa" -dependencies = [ - "num_cpus", -] - -[[package]] -name = "tinyvec" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c1c1d5a42b6245520c249549ec267180beaffcc0615401ac8e31853d4b6d8d2" -dependencies = [ - "tinyvec_macros", -] - -[[package]] -name = "tinyvec_macros" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" - -[[package]] -name = "toml" -version = "0.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa" -dependencies = [ - "serde", -] - -[[package]] -name = "ttf-parser" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ccbe8381883510b6a2d8f1e32905bddd178c11caef8083086d0c0c9ab0ac281" - -[[package]] -name = "twox-hash" -version = "1.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ee73e6e4924fe940354b8d4d98cad5231175d615cd855b758adc658c0aac6a0" -dependencies = [ - "cfg-if 1.0.0", - "rand", - "static_assertions 1.1.0", -] - -[[package]] -name = "typenum" -version = "1.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" - -[[package]] -name = "ucd-trie" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c" - -[[package]] -name = "unicode-width" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" - -[[package]] -name = "unicode-xid" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" - -[[package]] -name = "version_check" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" - -[[package]] -name = "walkdir" -version = "2.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" -dependencies = [ - "same-file", - "winapi", - "winapi-util", -] - -[[package]] -name = "wasi" -version = "0.10.2+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" - -[[package]] -name = "wasm-bindgen" -version = "0.2.79" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25f1af7423d8588a3d840681122e72e6a24ddbcb3f0ec385cac0d12d24256c06" -dependencies = [ - "cfg-if 1.0.0", - "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.79" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b21c0df030f5a177f3cba22e9bc4322695ec43e7257d865302900290bcdedca" -dependencies = [ - "bumpalo", - "lazy_static", - "log", - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-futures" -version = "0.4.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2eb6ec270a31b1d3c7e266b999739109abce8b6c87e4b31fcfcd788b65267395" -dependencies = [ - "cfg-if 1.0.0", - "js-sys", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.79" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f4203d69e40a52ee523b2529a773d5ffc1dc0071801c87b3d270b471b80ed01" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.79" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa8a30d46208db204854cadbb5d4baf5fcf8071ba5bf48190c3e59937962ebc" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-backend", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.79" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d958d035c4438e28c70e4321a2911302f10135ce78a9c7834c0cab4123d06a2" - -[[package]] -name = "wayland-client" -version = "0.29.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91223460e73257f697d9e23d401279123d36039a3f7a449e983f123292d4458f" -dependencies = [ - "bitflags", - "downcast-rs", - "libc", - "nix 0.22.0", - "scoped-tls", - "wayland-commons", - "wayland-scanner", - "wayland-sys", -] - -[[package]] -name = "wayland-commons" -version = "0.29.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94f6e5e340d7c13490eca867898c4cec5af56c27a5ffe5c80c6fc4708e22d33e" -dependencies = [ - "nix 0.22.0", - "once_cell", - "smallvec", - "wayland-sys", -] - -[[package]] -name = "wayland-cursor" -version = "0.29.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c52758f13d5e7861fc83d942d3d99bf270c83269575e52ac29e5b73cb956a6bd" -dependencies = [ - "nix 0.22.0", - "wayland-client", - "xcursor", -] - -[[package]] -name = "wayland-protocols" -version = "0.29.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60147ae23303402e41fe034f74fb2c35ad0780ee88a1c40ac09a3be1e7465741" -dependencies = [ - "bitflags", - "wayland-client", - "wayland-commons", - "wayland-scanner", -] - -[[package]] -name = "wayland-scanner" -version = "0.29.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39a1ed3143f7a143187156a2ab52742e89dac33245ba505c17224df48939f9e0" -dependencies = [ - "proc-macro2", - "quote", - "xml-rs", -] - -[[package]] -name = "wayland-sys" -version = "0.29.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9341df79a8975679188e37dab3889bfa57c44ac2cb6da166f519a81cbe452d4" -dependencies = [ - "dlib", - "lazy_static", - "pkg-config", -] - -[[package]] -name = "web-sys" -version = "0.3.56" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c060b319f29dd25724f09a2ba1418f142f539b2be99fbf4d2d5a8f7330afb8eb" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "wgpu" -version = "0.12.0" -source = "git+https://github.com/gfx-rs/wgpu?rev=0545e36#0545e36aa82709cca78c14eb3813f10eea7a9275" -dependencies = [ - "arrayvec", - "js-sys", - "log", - "naga", - "parking_lot", - "raw-window-handle", - "smallvec", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "wgpu-core", - "wgpu-hal", - "wgpu-types", -] - -[[package]] -name = "wgpu-core" -version = "0.12.0" -source = "git+https://github.com/gfx-rs/wgpu?rev=0545e36#0545e36aa82709cca78c14eb3813f10eea7a9275" -dependencies = [ - "arrayvec", - "bitflags", - "cfg_aliases", - "codespan-reporting", - "copyless", - "fxhash", - "log", - "naga", - "parking_lot", - "profiling", - "raw-window-handle", - "smallvec", - "thiserror", - "wgpu-hal", - "wgpu-types", -] - -[[package]] -name = "wgpu-hal" -version = "0.12.0" -source = "git+https://github.com/gfx-rs/wgpu?rev=0545e36#0545e36aa82709cca78c14eb3813f10eea7a9275" -dependencies = [ - "arrayvec", - "ash", - "bit-set", - "bitflags", - "block", - "core-graphics-types", - "d3d12", - "foreign-types", - "fxhash", - "glow", - "gpu-alloc", - "gpu-descriptor", - "inplace_it", - "js-sys", - "khronos-egl", - "libloading", - "log", - "metal", - "naga", - "objc", - "parking_lot", - "profiling", - "range-alloc", - "raw-window-handle", - "renderdoc-sys", - "thiserror", - "wasm-bindgen", - "web-sys", - "wgpu-types", - "winapi", -] - -[[package]] -name = "wgpu-types" -version = "0.12.0" -source = "git+https://github.com/gfx-rs/wgpu?rev=0545e36#0545e36aa82709cca78c14eb3813f10eea7a9275" -dependencies = [ - "bitflags", -] - -[[package]] -name = "wgpu_glyph" -version = "0.16.0" -source = "git+https://github.com/Anton-4/wgpu_glyph?rev=257d109#257d1098cbafa3c8a0a2465937b06fc730fc6ffb" -dependencies = [ - "bytemuck", - "glyph_brush", - "log", - "wgpu", -] - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-util" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" -dependencies = [ - "winapi", -] - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[package]] -name = "winit" -version = "0.26.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b43cc931d58b99461188607efd7acb2a093e65fc621f54cad78517a6063e73a" -dependencies = [ - "bitflags", - "cocoa", - "core-foundation 0.9.2", - "core-graphics 0.22.3", - "core-video-sys", - "dispatch", - "instant", - "lazy_static", - "libc", - "log", - "mio", - "ndk 0.5.0", - "ndk-glue 0.5.0", - "ndk-sys 0.2.2", - "objc", - "parking_lot", - "percent-encoding", - "raw-window-handle", - "smithay-client-toolkit", - "wasm-bindgen", - "wayland-client", - "wayland-protocols", - "web-sys", - "winapi", - "x11-dl", -] - -[[package]] -name = "x11-clipboard" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "473068b7b80ac86a18328824f1054e5e007898c47b5bbc281bd7abe32bc3653c" -dependencies = [ - "xcb", -] - -[[package]] -name = "x11-dl" -version = "2.19.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea26926b4ce81a6f5d9d0f3a0bc401e5a37c6ae14a1bfaa8ff6099ca80038c59" -dependencies = [ - "lazy_static", - "libc", - "pkg-config", -] - -[[package]] -name = "xcb" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "771e2b996df720cd1c6dd9ff90f62d91698fd3610cc078388d0564bdd6622a9c" -dependencies = [ - "libc", - "log", - "quick-xml", -] - -[[package]] -name = "xcursor" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "463705a63313cd4301184381c5e8042f0a7e9b4bb63653f216311d4ae74690b7" -dependencies = [ - "nom 7.1.0", -] - -[[package]] -name = "xi-unicode" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a67300977d3dc3f8034dae89778f502b6ba20b269527b3223ba59c0cf393bb8a" - -[[package]] -name = "xml-rs" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2d7d3948613f75c98fd9328cfdcc45acc4d360655289d0a7d4ec931392200a3" - -[[package]] -name = "yaml-rust" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" -dependencies = [ - "linked-hash-map", -] diff --git a/examples/breakout/platform/Cargo.toml b/examples/breakout/platform/Cargo.toml deleted file mode 100644 index 3e110e5aaf..0000000000 --- a/examples/breakout/platform/Cargo.toml +++ /dev/null @@ -1,70 +0,0 @@ -[package] -name = "host" -version = "0.1.0" -authors = ["The Roc Contributors"] -license = "UPL-1.0" -edition = "2021" - -[lib] -name = "host" -path = "src/lib.rs" -crate-type = ["staticlib", "rlib"] - -[[bin]] -name = "host" -path = "src/main.rs" - -[dependencies] -roc_std = { path = "../../../roc_std" } -libc = "0.2" -arrayvec = "0.7.2" -page_size = "0.4.2" -# when changing winit version, check if copypasta can be updated simultaneously so they use the same versions for their dependencies. This will save build time. -winit = "0.26.1" -wgpu = { git = "https://github.com/gfx-rs/wgpu", rev = "0545e36" } -wgpu_glyph = { git = "https://github.com/Anton-4/wgpu_glyph", rev = "257d109" } -glyph_brush = "0.7.2" -log = "0.4.14" -env_logger = "0.9.0" -futures = "0.3.17" -cgmath = "0.18.0" -snafu = { version = "0.6.10", features = ["backtraces"] } -colored = "2.0.0" -pest = "2.1.3" -pest_derive = "2.1.0" -copypasta = "0.7.1" -palette = "0.6.0" -confy = { git = 'https://github.com/rust-cli/confy', features = [ - "yaml_conf" -], default-features = false } -serde = { version = "1.0.130", features = ["derive"] } -nonempty = "0.7.0" -fs_extra = "1.2.0" -rodio = { version = "0.14.0", optional = true } # to play sounds -threadpool = "1.8.1" - -[package.metadata.cargo-udeps.ignore] -# confy is currently unused but should not be removed -normal = ["confy"] -#development = [] -#build = [] - -[features] -default = [] -with_sound = ["rodio"] - -[dependencies.bytemuck] -version = "1.7.2" -features = ["derive"] - -[workspace] - -# Optimizations based on https://deterministic.space/high-performance-rust.html -[profile.release] -lto = "fat" -codegen-units = 1 - -# debug = true # enable when profiling -[profile.bench] -lto = "thin" -codegen-units = 1 diff --git a/examples/breakout/platform/Game.roc b/examples/breakout/platform/Game.roc deleted file mode 100644 index 0ce338ee3d..0000000000 --- a/examples/breakout/platform/Game.roc +++ /dev/null @@ -1,13 +0,0 @@ -interface Game - exposes [Bounds, Elem, Event] - imports [] - -Rgba : { r : F32, g : F32, b : F32, a : F32 } - -Bounds : { height : F32, width : F32 } - -Elem : [Rect { color : Rgba, left : F32, top : F32, width : F32, height : F32 }, Text Str] - -KeyCode : [Left, Right, Other] - -Event : [Resize { width : F32, height : F32 }, KeyDown KeyCode, KeyUp KeyCode, Tick U128] diff --git a/examples/breakout/platform/build.rs b/examples/breakout/platform/build.rs deleted file mode 100644 index 73159e387c..0000000000 --- a/examples/breakout/platform/build.rs +++ /dev/null @@ -1,4 +0,0 @@ -fn main() { - println!("cargo:rustc-link-lib=dylib=app"); - println!("cargo:rustc-link-search=."); -} diff --git a/examples/breakout/platform/src/graphics/lowlevel/ortho.rs b/examples/breakout/platform/src/graphics/lowlevel/ortho.rs deleted file mode 100644 index 2f4577871a..0000000000 --- a/examples/breakout/platform/src/graphics/lowlevel/ortho.rs +++ /dev/null @@ -1,118 +0,0 @@ -use cgmath::{Matrix4, Ortho}; -use wgpu::util::DeviceExt; -use wgpu::{ - BindGroup, BindGroupLayout, BindGroupLayoutDescriptor, BindGroupLayoutEntry, Buffer, - ShaderStages, -}; - -// orthographic projection is used to transform pixel coords to the coordinate system used by wgpu - -#[repr(C)] -#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] -struct Uniforms { - // We can't use cgmath with bytemuck directly so we'll have - // to convert the Matrix4 into a 4x4 f32 array - ortho: [[f32; 4]; 4], -} - -impl Uniforms { - fn new(w: u32, h: u32) -> Self { - let ortho: Matrix4 = Ortho:: { - left: 0.0, - right: w as f32, - bottom: h as f32, - top: 0.0, - near: -1.0, - far: 1.0, - } - .into(); - Self { - ortho: ortho.into(), - } - } -} - -// update orthographic buffer according to new window size -pub fn update_ortho_buffer( - inner_width: u32, - inner_height: u32, - gpu_device: &wgpu::Device, - ortho_buffer: &Buffer, - cmd_queue: &wgpu::Queue, -) { - let new_uniforms = Uniforms::new(inner_width, inner_height); - - let new_ortho_buffer = gpu_device.create_buffer_init(&wgpu::util::BufferInitDescriptor { - label: Some("Ortho uniform buffer"), - contents: bytemuck::cast_slice(&[new_uniforms]), - usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_SRC, - }); - - // get a command encoder for the current frame - let mut encoder = gpu_device.create_command_encoder(&wgpu::CommandEncoderDescriptor { - label: Some("Resize"), - }); - - // overwrite the new buffer over the old one - encoder.copy_buffer_to_buffer( - &new_ortho_buffer, - 0, - ortho_buffer, - 0, - (std::mem::size_of::() * vec![new_uniforms].as_slice().len()) - as wgpu::BufferAddress, - ); - - cmd_queue.submit(Some(encoder.finish())); -} - -#[derive(Debug)] -pub struct OrthoResources { - pub buffer: Buffer, - pub bind_group_layout: BindGroupLayout, - pub bind_group: BindGroup, -} - -pub fn init_ortho( - inner_width: u32, - inner_height: u32, - gpu_device: &wgpu::Device, -) -> OrthoResources { - let uniforms = Uniforms::new(inner_width, inner_height); - - let ortho_buffer = gpu_device.create_buffer_init(&wgpu::util::BufferInitDescriptor { - label: Some("Ortho uniform buffer"), - contents: bytemuck::cast_slice(&[uniforms]), - usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, - }); - - // bind groups consist of extra resources that are provided to the shaders - let ortho_bind_group_layout = gpu_device.create_bind_group_layout(&BindGroupLayoutDescriptor { - entries: &[BindGroupLayoutEntry { - binding: 0, - visibility: ShaderStages::VERTEX, - ty: wgpu::BindingType::Buffer { - ty: wgpu::BufferBindingType::Uniform, - has_dynamic_offset: false, - min_binding_size: None, - }, - count: None, - }], - label: Some("Ortho bind group layout"), - }); - - let ortho_bind_group = gpu_device.create_bind_group(&wgpu::BindGroupDescriptor { - layout: &ortho_bind_group_layout, - entries: &[wgpu::BindGroupEntry { - binding: 0, - resource: ortho_buffer.as_entire_binding(), - }], - label: Some("Ortho bind group"), - }); - - OrthoResources { - buffer: ortho_buffer, - bind_group_layout: ortho_bind_group_layout, - bind_group: ortho_bind_group, - } -} diff --git a/examples/breakout/platform/src/graphics/mod.rs b/examples/breakout/platform/src/graphics/mod.rs deleted file mode 100644 index 0eb7fcd6da..0000000000 --- a/examples/breakout/platform/src/graphics/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub mod colors; -pub mod lowlevel; -pub mod primitives; -pub mod style; diff --git a/examples/breakout/platform/src/graphics/primitives/mod.rs b/examples/breakout/platform/src/graphics/primitives/mod.rs deleted file mode 100644 index a9adb18862..0000000000 --- a/examples/breakout/platform/src/graphics/primitives/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod rect; -pub mod text; diff --git a/examples/breakout/platform/src/graphics/primitives/text.rs b/examples/breakout/platform/src/graphics/primitives/text.rs deleted file mode 100644 index 4ecaa28d9b..0000000000 --- a/examples/breakout/platform/src/graphics/primitives/text.rs +++ /dev/null @@ -1,134 +0,0 @@ -// Adapted from https://github.com/sotrh/learn-wgpu -// by Benjamin Hansen - license information can be found in the COPYRIGHT -// file in the root directory of this distribution. -// -// Thank you, Benjamin! - -use crate::graphics::colors::Rgba; -use crate::graphics::style::DEFAULT_FONT_SIZE; -use ab_glyph::{FontArc, InvalidFont}; -use cgmath::Vector2; -use wgpu_glyph::{ab_glyph, GlyphBrush, GlyphBrushBuilder}; - -#[derive(Debug)] -pub struct Text<'a> { - pub position: Vector2, - pub area_bounds: Vector2, - pub color: Rgba, - pub text: &'a str, - pub size: f32, - pub visible: bool, - pub centered: bool, -} - -impl<'a> Default for Text<'a> { - fn default() -> Self { - Self { - position: (0.0, 0.0).into(), - area_bounds: (std::f32::INFINITY, std::f32::INFINITY).into(), - color: Rgba::WHITE, - text: "", - size: DEFAULT_FONT_SIZE, - visible: true, - centered: false, - } - } -} - -// pub fn layout_from_text(text: &Text) -> wgpu_glyph::Layout { -// wgpu_glyph::Layout::default().h_align(if text.centered { -// wgpu_glyph::HorizontalAlign::Center -// } else { -// wgpu_glyph::HorizontalAlign::Left -// }) -// } - -// fn section_from_text<'a>( -// text: &'a Text, -// layout: wgpu_glyph::Layout, -// ) -> wgpu_glyph::Section<'a> { -// Section { -// screen_position: text.position.into(), -// bounds: text.area_bounds.into(), -// layout, -// ..Section::default() -// } -// .add_text( -// wgpu_glyph::Text::new(text.text) -// .with_color(text.color) -// .with_scale(text.size), -// ) -// } - -// pub fn owned_section_from_text(text: &Text) -> OwnedSection { -// let layout = layout_from_text(text); - -// OwnedSection { -// screen_position: text.position.into(), -// bounds: text.area_bounds.into(), -// layout, -// ..OwnedSection::default() -// } -// .add_text( -// glyph_brush::OwnedText::new(text.text) -// .with_color(Vector4::from(text.color)) -// .with_scale(text.size), -// ) -// } - -// pub fn owned_section_from_glyph_texts( -// text: Vec, -// screen_position: (f32, f32), -// area_bounds: (f32, f32), -// layout: wgpu_glyph::Layout, -// ) -> glyph_brush::OwnedSection { -// glyph_brush::OwnedSection { -// screen_position, -// bounds: area_bounds, -// layout, -// text, -// } -// } - -// pub fn queue_text_draw(text: &Text, glyph_brush: &mut GlyphBrush<()>) { -// let layout = layout_from_text(text); - -// let section = section_from_text(text, layout); - -// glyph_brush.queue(section.clone()); -// } - -// fn glyph_to_rect(glyph: &wgpu_glyph::SectionGlyph) -> Rect { -// let position = glyph.glyph.position; -// let px_scale = glyph.glyph.scale; -// let width = glyph_width(&glyph.glyph); -// let height = px_scale.y; -// let top_y = glyph_top_y(&glyph.glyph); - -// Rect { -// pos: [position.x, top_y].into(), -// width, -// height, -// } -// } - -// pub fn glyph_top_y(glyph: &Glyph) -> f32 { -// let height = glyph.scale.y; - -// glyph.position.y - height * 0.75 -// } - -// pub fn glyph_width(glyph: &Glyph) -> f32 { -// glyph.scale.x * 0.4765 -// } - -pub fn build_glyph_brush( - gpu_device: &wgpu::Device, - render_format: wgpu::TextureFormat, -) -> Result, InvalidFont> { - let inconsolata = FontArc::try_from_slice(include_bytes!( - "../../../../../../editor/Inconsolata-Regular.ttf" - ))?; - - Ok(GlyphBrushBuilder::using_font(inconsolata).build(gpu_device, render_format)) -} diff --git a/examples/breakout/platform/src/graphics/style.rs b/examples/breakout/platform/src/graphics/style.rs deleted file mode 100644 index 11e609075b..0000000000 --- a/examples/breakout/platform/src/graphics/style.rs +++ /dev/null @@ -1 +0,0 @@ -pub const DEFAULT_FONT_SIZE: f32 = 30.0; diff --git a/examples/breakout/platform/src/lib.rs b/examples/breakout/platform/src/lib.rs deleted file mode 100644 index 196a65c017..0000000000 --- a/examples/breakout/platform/src/lib.rs +++ /dev/null @@ -1,16 +0,0 @@ -mod graphics; -mod gui; -mod roc; - -#[no_mangle] -pub extern "C" fn rust_main() -> i32 { - let bounds = roc::Bounds { - width: 1900.0, - height: 1000.0, - }; - - gui::run_event_loop("RocOut!", bounds).expect("Error running event loop"); - - // Exit code - 0 -} diff --git a/examples/breakout/platform/src/main.rs b/examples/breakout/platform/src/main.rs deleted file mode 100644 index 51175f934b..0000000000 --- a/examples/breakout/platform/src/main.rs +++ /dev/null @@ -1,3 +0,0 @@ -fn main() { - std::process::exit(host::rust_main()); -} diff --git a/examples/breakout/platform/src/roc.rs b/examples/breakout/platform/src/roc.rs deleted file mode 100644 index 3c3121656a..0000000000 --- a/examples/breakout/platform/src/roc.rs +++ /dev/null @@ -1,438 +0,0 @@ -use crate::graphics::colors::Rgba; -use core::alloc::Layout; -use core::ffi::c_void; -use core::mem::{self, ManuallyDrop}; -use roc_std::{RocList, RocStr}; -use std::ffi::CStr; -use std::fmt::Debug; -use std::mem::MaybeUninit; -use std::os::raw::c_char; -use std::time::Duration; -use winit::event::VirtualKeyCode; - -extern "C" { - // program - - #[link_name = "roc__programForHost_1_exposed_generic"] - fn roc_program(); - - #[link_name = "roc__programForHost_size"] - fn roc_program_size() -> i64; - - // init - - #[link_name = "roc__programForHost_1_Init_caller"] - fn call_init(size: *const Bounds, closure_data: *const u8, output: *mut Model); - - #[link_name = "roc__programForHost_1_Init_size"] - fn init_size() -> i64; - - #[link_name = "roc__programForHost_1_Init_result_size"] - fn init_result_size() -> i64; - - // update - - #[link_name = "roc__programForHost_1_Update_caller"] - fn call_update( - model: *const Model, - event: *const RocEvent, - closure_data: *const u8, - output: *mut Model, - ); - - #[link_name = "roc__programForHost_1_Update_size"] - fn update_size() -> i64; - - #[link_name = "roc__programForHost_1_Update_result_size"] - fn update_result_size() -> i64; - - // render - - #[link_name = "roc__programForHost_1_Render_caller"] - fn call_render(model: *const Model, closure_data: *const u8, output: *mut RocList); - - #[link_name = "roc__programForHost_1_Render_size"] - fn roc_render_size() -> i64; -} - -#[repr(C)] -pub union RocEventEntry { - pub key_down: RocKeyCode, - pub key_up: RocKeyCode, - pub resize: Bounds, - pub tick: [u8; 16], // u128 is unsupported in repr(C) -} - -#[repr(u8)] -#[allow(unused)] -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum RocEventTag { - KeyDown = 0, - KeyUp, - Resize, - Tick, -} - -#[repr(C)] -#[cfg(target_pointer_width = "64")] // on a 64-bit system, the tag fits in this pointer's spare 3 bits -pub struct RocEvent { - entry: RocEventEntry, - tag: RocEventTag, -} - -impl Debug for RocEvent { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - use RocEventTag::*; - - match self.tag() { - KeyDown => unsafe { self.entry().key_down }.fmt(f), - KeyUp => unsafe { self.entry().key_up }.fmt(f), - Resize => unsafe { self.entry().resize }.fmt(f), - Tick => unsafe { self.entry().tick }.fmt(f), - } - } -} - -impl RocEvent { - #[cfg(target_pointer_width = "64")] - pub fn tag(&self) -> RocEventTag { - self.tag - } - - pub fn entry(&self) -> &RocEventEntry { - &self.entry - } - - #[allow(non_snake_case)] - pub fn Resize(size: Bounds) -> Self { - Self { - tag: RocEventTag::Resize, - entry: RocEventEntry { resize: size }, - } - } - - #[allow(non_snake_case)] - pub fn KeyDown(keycode: RocKeyCode) -> Self { - Self { - tag: RocEventTag::KeyDown, - entry: RocEventEntry { key_down: keycode }, - } - } - - #[allow(non_snake_case)] - pub fn KeyUp(keycode: RocKeyCode) -> Self { - Self { - tag: RocEventTag::KeyUp, - entry: RocEventEntry { key_up: keycode }, - } - } - - #[allow(non_snake_case)] - pub fn Tick(duration: Duration) -> Self { - Self { - tag: RocEventTag::Tick, - entry: RocEventEntry { - tick: duration.as_nanos().to_ne_bytes(), - }, - } - } -} - -#[repr(u8)] -#[allow(unused)] -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum RocKeyCode { - Left = 0, - Other, - Right, -} - -impl From for RocKeyCode { - fn from(keycode: VirtualKeyCode) -> Self { - use VirtualKeyCode::*; - - match keycode { - Left => RocKeyCode::Left, - Right => RocKeyCode::Right, - _ => RocKeyCode::Other, - } - } -} - -#[no_mangle] -pub unsafe extern "C" fn roc_alloc(size: usize, _alignment: u32) -> *mut c_void { - return libc::malloc(size); -} - -#[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 { - return libc::realloc(c_ptr, new_size); -} - -#[no_mangle] -pub unsafe extern "C" fn roc_dealloc(c_ptr: *mut c_void, _alignment: u32) { - return libc::free(c_ptr); -} - -#[no_mangle] -pub unsafe extern "C" fn roc_panic(c_ptr: *mut c_void, tag_id: u32) { - match tag_id { - 0 => { - let slice = CStr::from_ptr(c_ptr as *const c_char); - let string = slice.to_str().unwrap(); - eprintln!("Roc hit a panic: {}", string); - std::process::exit(1); - } - _ => todo!(), - } -} - -#[no_mangle] -pub unsafe extern "C" fn roc_memcpy(dst: *mut c_void, src: *mut c_void, n: usize) -> *mut c_void { - libc::memcpy(dst, src, n) -} - -#[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) -} - -#[repr(transparent)] -#[cfg(target_pointer_width = "64")] // on a 64-bit system, the tag fits in this pointer's spare 3 bits -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -pub struct ElemId(*const RocElemEntry); - -#[repr(C)] -pub union RocElemEntry { - pub rect: ManuallyDrop, - pub text: ManuallyDrop, -} - -#[repr(u8)] -#[allow(unused)] -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum RocElemTag { - Rect = 0, - Text = 1, -} - -#[repr(C)] -#[cfg(target_pointer_width = "64")] // on a 64-bit system, the tag fits in this pointer's spare 3 bits -pub struct RocElem { - entry: RocElemEntry, - tag: RocElemTag, -} - -impl Debug for RocElem { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - use RocElemTag::*; - - match self.tag() { - Rect => unsafe { &*self.entry().rect }.fmt(f), - Text => unsafe { &*self.entry().text }.fmt(f), - } - } -} - -impl RocElem { - #[cfg(target_pointer_width = "64")] - pub fn tag(&self) -> RocElemTag { - self.tag - } - - #[allow(unused)] - pub fn entry(&self) -> &RocElemEntry { - &self.entry - } - - #[allow(unused)] - pub fn rect(styles: ButtonStyles) -> RocElem { - todo!("restore rect() method") - // let rect = RocRect { styles }; - // let entry = RocElemEntry { - // rect: ManuallyDrop::new(rect), - // }; - - // Self::elem_from_tag(entry, RocElemTag::Rect) - } - - #[allow(unused)] - pub fn text>(into_roc_str: T) -> RocElem { - todo!("TODO restore text method") - // let entry = RocElemEntry { - // text: ManuallyDrop::new(into_roc_str.into()), - // }; - - // Self::elem_from_tag(entry, RocElemTag::Text) - } -} - -#[repr(C)] -#[derive(Debug, Clone, Copy)] -pub struct RocRect { - pub color: Rgba, - - // These must be in this order for alphabetization! - pub height: f32, - pub left: f32, - pub top: f32, - pub width: f32, -} - -impl Clone for RocElem { - fn clone(&self) -> Self { - unsafe { - match self.tag() { - RocElemTag::Rect => Self { - tag: RocElemTag::Rect, - entry: RocElemEntry { - rect: self.entry.rect.clone(), - }, - }, - RocElemTag::Text => Self { - tag: RocElemTag::Text, - entry: RocElemEntry { - text: self.entry.text.clone(), - }, - }, - } - } - } -} - -impl Drop for RocElem { - fn drop(&mut self) { - unsafe { - match self.tag() { - RocElemTag::Rect => mem::drop(ManuallyDrop::take(&mut self.entry.rect)), - RocElemTag::Text => mem::drop(ManuallyDrop::take(&mut self.entry.text)), - } - } - } -} - -#[repr(C)] -#[derive(Copy, Clone, Debug, Default)] -pub struct ButtonStyles { - pub bg_color: Rgba, - pub border_color: Rgba, - pub border_width: f32, - pub text_color: Rgba, -} - -#[derive(Copy, Clone, Debug, Default)] -#[repr(C)] -pub struct Bounds { - pub height: f32, - pub width: f32, -} - -type Model = c_void; - -/// Call the app's init function, then render and return that result -pub fn init_and_render(bounds: Bounds) -> (*const Model, RocList) { - let closure_data_buf; - let closure_layout; - - // Call init to get the initial model - let model = unsafe { - let ret_val_layout = Layout::array::(init_result_size() as usize).unwrap(); - - // TODO allocate on the stack if it's under a certain size - let ret_val_buf = std::alloc::alloc(ret_val_layout) as *mut Model; - - closure_layout = Layout::array::(init_size() as usize).unwrap(); - - // TODO allocate on the stack if it's under a certain size - closure_data_buf = std::alloc::alloc(closure_layout); - - call_init(&bounds, closure_data_buf, ret_val_buf); - - ret_val_buf - }; - - // Call render passing the model to get the initial Elems - let elems = unsafe { - let mut ret_val: MaybeUninit> = MaybeUninit::uninit(); - - // Reuse the buffer from the previous closure if possible - let closure_data_buf = - std::alloc::realloc(closure_data_buf, closure_layout, roc_render_size() as usize); - - call_render(model, closure_data_buf, ret_val.as_mut_ptr()); - - std::alloc::dealloc(closure_data_buf, closure_layout); - - ret_val.assume_init() - }; - - (model, elems) -} - -/// Call the app's update function, then render and return that result -pub fn update(model: *const Model, event: RocEvent) -> *const Model { - let closure_data_buf; - let closure_layout; - - // Call update to get the new model - unsafe { - let ret_val_layout = Layout::array::(update_result_size() as usize).unwrap(); - - // TODO allocate on the stack if it's under a certain size - let ret_val_buf = std::alloc::alloc(ret_val_layout) as *mut Model; - - closure_layout = Layout::array::(update_size() as usize).unwrap(); - - // TODO allocate on the stack if it's under a certain size - closure_data_buf = std::alloc::alloc(closure_layout); - - call_update(model, &event, closure_data_buf, ret_val_buf); - - ret_val_buf - } -} - -/// Call the app's update function, then render and return that result -pub fn update_and_render(model: *const Model, event: RocEvent) -> (*const Model, RocList) { - let closure_data_buf; - let closure_layout; - - // Call update to get the new model - let model = unsafe { - let ret_val_layout = Layout::array::(update_result_size() as usize).unwrap(); - - // TODO allocate on the stack if it's under a certain size - let ret_val_buf = std::alloc::alloc(ret_val_layout) as *mut Model; - - closure_layout = Layout::array::(update_size() as usize).unwrap(); - - // TODO allocate on the stack if it's under a certain size - closure_data_buf = std::alloc::alloc(closure_layout); - - call_update(model, &event, closure_data_buf, ret_val_buf); - - ret_val_buf - }; - - // Call render passing the model to get the initial Elems - let elems = unsafe { - let mut ret_val: MaybeUninit> = MaybeUninit::uninit(); - - // Reuse the buffer from the previous closure if possible - let closure_data_buf = - std::alloc::realloc(closure_data_buf, closure_layout, roc_render_size() as usize); - - call_render(model, closure_data_buf, ret_val.as_mut_ptr()); - - std::alloc::dealloc(closure_data_buf, closure_layout); - - ret_val.assume_init() - }; - - (model, elems) -} diff --git a/examples/cli/.gitignore b/examples/cli/.gitignore new file mode 100644 index 0000000000..95d47a0ea9 --- /dev/null +++ b/examples/cli/.gitignore @@ -0,0 +1,13 @@ +args +countdown +echo +effects +file +form +tui +http-get +file-io +env +ingested-file +ingested-file-bytes +out.txt diff --git a/examples/cli/README.md b/examples/cli/README.md new file mode 100644 index 0000000000..209a2eb67e --- /dev/null +++ b/examples/cli/README.md @@ -0,0 +1,5 @@ +# CLI examples + +**Note**: You probably will want to use the [`basic-cli` platform](https://github.com/roc-lang/basic-cli). This folder will be removed soon. + +These are examples of how to make basic CLI (command-line interface) and TUI (terminal user interface) apps in Roc. diff --git a/examples/cli/argsBROKEN.roc b/examples/cli/argsBROKEN.roc new file mode 100644 index 0000000000..c2fe7b1d4c --- /dev/null +++ b/examples/cli/argsBROKEN.roc @@ -0,0 +1,67 @@ +app "args" + packages { pf: "https://github.com/roc-lang/basic-cli/releases/download/0.7.0/bkGby8jb0tmZYsy2hg1E_B2QrCgcSTxdUlHtETwm5m4.tar.br" } + imports [pf.Stdout, pf.Arg, pf.Task.{ Task }] + provides [main] to pf + +main : Task {} I32 +main = + args <- Arg.list |> Task.await + parser = + divCmd = + Arg.succeed (\dividend -> \divisor -> Div (Num.toF64 dividend) (Num.toF64 divisor)) + |> Arg.withParser + ( + Arg.i64Option { + long: "dividend", + short: "n", + help: "the number to divide; corresponds to a numerator", + } + ) + |> Arg.withParser + ( + Arg.i64Option { + long: "divisor", + short: "d", + help: "the number to divide by; corresponds to a denominator", + } + ) + |> Arg.subCommand "div" + + logCmd = + Arg.succeed (\base -> \num -> Log (Num.toF64 base) (Num.toF64 num)) + |> Arg.withParser + ( + Arg.i64Option { + long: "base", + short: "b", + help: "base of the logarithm", + } + ) + |> Arg.withParser + ( + Arg.i64Option { + long: "num", + help: "the number to take the logarithm of", + } + ) + |> Arg.subCommand "log" + + Arg.choice [divCmd, logCmd] + |> Arg.program { name: "args-example", help: "A calculator example of the CLI platform argument parser" } + + when Arg.parseFormatted parser args is + Ok cmd -> + runCmd cmd + |> Num.toStr + |> Stdout.line + + Err helpMenu -> + {} <- Stdout.line helpMenu |> Task.await + Task.err 1 + +runCmd = \cmd -> + when cmd is + Div n d -> n / d + Log b n -> + # log_b(n) = log_x(n) / log_x(b) for all x + runCmd (Div (Num.log n) (Num.log b)) diff --git a/examples/cli/countdown.roc b/examples/cli/countdown.roc new file mode 100644 index 0000000000..917c30fda8 --- /dev/null +++ b/examples/cli/countdown.roc @@ -0,0 +1,18 @@ +app "countdown" + packages { pf: "https://github.com/roc-lang/basic-cli/releases/download/0.7.0/bkGby8jb0tmZYsy2hg1E_B2QrCgcSTxdUlHtETwm5m4.tar.br" } + imports [pf.Stdin, pf.Stdout, pf.Task.{ await, loop }] + provides [main] to pf + +main = + _ <- await (Stdout.line "\nLet's count down from 3 together - all you have to do is press .") + _ <- await Stdin.line + loop 3 tick + +tick = \n -> + if n == 0 then + _ <- await (Stdout.line "🎉 SURPRISE! Happy Birthday! 🎂") + Task.ok (Done {}) + else + _ <- await (n |> Num.toStr |> \s -> "\(s)..." |> Stdout.line) + _ <- await Stdin.line + Task.ok (Step (n - 1)) diff --git a/examples/cli/echo.roc b/examples/cli/echo.roc new file mode 100644 index 0000000000..88737ea6cc --- /dev/null +++ b/examples/cli/echo.roc @@ -0,0 +1,37 @@ +app "echo" + packages { pf: "https://github.com/roc-lang/basic-cli/releases/download/0.7.0/bkGby8jb0tmZYsy2hg1E_B2QrCgcSTxdUlHtETwm5m4.tar.br" } + imports [pf.Stdin, pf.Stdout, pf.Task.{ Task }] + provides [main] to pf + +main : Task {} I32 +main = + _ <- Task.await (Stdout.line "🗣 Shout into this cave and hear the echo! 👂👂👂") + + Task.loop {} tick + +tick : {} -> Task [Step {}, Done {}] * +tick = \{} -> + shout <- Task.await Stdin.line + + when shout is + Input s -> Stdout.line (echo s) |> Task.map Step + End -> Stdout.line (echo "Received end of input (EOF).") |> Task.map Done + +echo : Str -> Str +echo = \shout -> + silence = \length -> + spaceInUtf8 = 32 + + List.repeat spaceInUtf8 length + + shout + |> Str.toUtf8 + |> List.mapWithIndex + (\_, i -> + length = (List.len (Str.toUtf8 shout) - i) + phrase = (List.split (Str.toUtf8 shout) length).before + + List.concat (silence (if i == 0 then 2 * length else length)) phrase) + |> List.join + |> Str.fromUtf8 + |> Result.withDefault "" diff --git a/examples/interactive/effects-platform/Effect.roc b/examples/cli/effects-platform/Effect.roc similarity index 100% rename from examples/interactive/effects-platform/Effect.roc rename to examples/cli/effects-platform/Effect.roc diff --git a/examples/cli/effects-platform/host.zig b/examples/cli/effects-platform/host.zig new file mode 100644 index 0000000000..6d3ea3306e --- /dev/null +++ b/examples/cli/effects-platform/host.zig @@ -0,0 +1,229 @@ +const std = @import("std"); +const builtin = @import("builtin"); +const str = @import("glue").str; +const RocStr = str.RocStr; +const testing = std.testing; +const expectEqual = testing.expectEqual; +const expect = testing.expect; +const maxInt = std.math.maxInt; + +const mem = std.mem; +const Allocator = mem.Allocator; + +extern fn roc__mainForHost_1_exposed_generic([*]u8) void; +extern fn roc__mainForHost_1_exposed_size() i64; +extern fn roc__mainForHost_0_caller(*const u8, [*]u8, [*]u8) void; +extern fn roc__mainForHost_0_size() i64; +extern fn roc__mainForHost_0_result_size() i64; + +const Align = 2 * @alignOf(usize); +extern fn malloc(size: usize) callconv(.C) ?*align(Align) anyopaque; +extern fn realloc(c_ptr: [*]align(Align) u8, size: usize) callconv(.C) ?*anyopaque; +extern fn free(c_ptr: [*]align(Align) u8) callconv(.C) void; +extern fn memcpy(dst: [*]u8, src: [*]u8, size: usize) callconv(.C) void; +extern fn memset(dst: [*]u8, value: i32, size: usize) void; +extern fn kill(pid: c_int, sig: c_int) c_int; +extern fn shm_open(name: *const i8, oflag: c_int, mode: c_uint) c_int; +extern fn mmap(addr: ?*anyopaque, length: c_uint, prot: c_int, flags: c_int, fd: c_int, offset: c_uint) *anyopaque; +extern fn getppid() c_int; + +const DEBUG: bool = false; + +export fn roc_alloc(size: usize, alignment: u32) callconv(.C) ?*anyopaque { + if (DEBUG) { + var ptr = malloc(size); + const stdout = std.io.getStdOut().writer(); + stdout.print("alloc: {d} (alignment {d}, size {d})\n", .{ ptr, alignment, size }) catch unreachable; + return ptr; + } else { + return malloc(size); + } +} + +export fn roc_realloc(c_ptr: *anyopaque, new_size: usize, old_size: usize, alignment: u32) callconv(.C) ?*anyopaque { + if (DEBUG) { + const stdout = std.io.getStdOut().writer(); + stdout.print("realloc: {d} (alignment {d}, old_size {d})\n", .{ c_ptr, alignment, old_size }) catch unreachable; + } + + return realloc(@as([*]align(Align) u8, @alignCast(@ptrCast(c_ptr))), new_size); +} + +export fn roc_dealloc(c_ptr: *anyopaque, alignment: u32) callconv(.C) void { + if (DEBUG) { + const stdout = std.io.getStdOut().writer(); + stdout.print("dealloc: {d} (alignment {d})\n", .{ c_ptr, alignment }) catch unreachable; + } + + free(@as([*]align(Align) u8, @alignCast(@ptrCast(c_ptr)))); +} + +export fn roc_panic(msg: *RocStr, tag_id: u32) callconv(.C) void { + const stderr = std.io.getStdErr().writer(); + switch (tag_id) { + 0 => { + stderr.print("Roc standard library crashed with message\n\n {s}\n\nShutting down\n", .{msg.asSlice()}) catch unreachable; + }, + 1 => { + stderr.print("Application crashed with message\n\n {s}\n\nShutting down\n", .{msg.asSlice()}) catch unreachable; + }, + else => unreachable, + } + std.process.exit(1); +} + +export fn roc_dbg(loc: *RocStr, msg: *RocStr) callconv(.C) void { + const stderr = std.io.getStdErr().writer(); + stderr.print("[{s}] {s}\n", .{ loc.asSlice(), msg.asSlice() }) catch unreachable; +} + +export fn roc_memset(dst: [*]u8, value: i32, size: usize) callconv(.C) void { + return memset(dst, value, size); +} + +fn roc_getppid() callconv(.C) c_int { + return getppid(); +} + +fn roc_getppid_windows_stub() callconv(.C) c_int { + return 0; +} + +fn roc_shm_open(name: *const i8, oflag: c_int, mode: c_uint) callconv(.C) c_int { + return shm_open(name, oflag, mode); +} +fn roc_mmap(addr: ?*anyopaque, length: c_uint, prot: c_int, flags: c_int, fd: c_int, offset: c_uint) callconv(.C) *anyopaque { + return mmap(addr, length, prot, flags, fd, offset); +} + +comptime { + if (builtin.os.tag == .macos or builtin.os.tag == .linux) { + @export(roc_getppid, .{ .name = "roc_getppid", .linkage = .Strong }); + @export(roc_mmap, .{ .name = "roc_mmap", .linkage = .Strong }); + @export(roc_shm_open, .{ .name = "roc_shm_open", .linkage = .Strong }); + } + + if (builtin.os.tag == .windows) { + @export(roc_getppid_windows_stub, .{ .name = "roc_getppid", .linkage = .Strong }); + } +} + +const Unit = extern struct {}; + +pub export fn main() u8 { + const allocator = std.heap.page_allocator; + const stderr = std.io.getStdErr().writer(); + + // NOTE the return size can be zero, which will segfault. Always allocate at least 8 bytes + const size = @max(8, @as(usize, @intCast(roc__mainForHost_1_exposed_size()))); + const raw_output = allocator.alignedAlloc(u8, @alignOf(u64), @as(usize, @intCast(size))) catch unreachable; + var output = @as([*]u8, @ptrCast(raw_output)); + + defer { + allocator.free(raw_output); + } + + var timer = std.time.Timer.start() catch unreachable; + + roc__mainForHost_1_exposed_generic(output); + + call_the_closure(output); + + const nanos = timer.read(); + const seconds = (@as(f64, @floatFromInt(nanos)) / 1_000_000_000.0); + + stderr.print("runtime: {d:.3}ms\n", .{seconds * 1000}) catch unreachable; + + return 0; +} + +fn to_seconds(tms: std.os.timespec) f64 { + return @as(f64, @floatFromInt(tms.tv_sec)) + (@as(f64, @floatFromInt(tms.tv_nsec)) / 1_000_000_000.0); +} + +fn call_the_closure(closure_data_pointer: [*]u8) void { + const allocator = std.heap.page_allocator; + + const size = roc__mainForHost_0_result_size(); + + if (size == 0) { + // the function call returns an empty record + // allocating 0 bytes causes issues because the allocator will return a NULL pointer + // So it's special-cased + const flags: u8 = 0; + var result: [1]u8 = .{0}; + roc__mainForHost_0_caller(&flags, closure_data_pointer, &result); + + return; + } + + const raw_output = allocator.alignedAlloc(u8, @alignOf(u64), @as(usize, @intCast(size))) catch unreachable; + var output = @as([*]u8, @ptrCast(raw_output)); + + defer { + allocator.free(raw_output); + } + + const flags: u8 = 0; + roc__mainForHost_0_caller(&flags, closure_data_pointer, output); + + return; +} + +pub export fn roc_fx_getLine() str.RocStr { + return roc_fx_getLine_help() catch return str.RocStr.empty(); +} + +fn roc_fx_getLine_help() !RocStr { + const stdin = std.io.getStdIn().reader(); + var buf: [400]u8 = undefined; + + const line: []u8 = (try stdin.readUntilDelimiterOrEof(&buf, '\n')) orelse ""; + + return str.RocStr.init(@as([*]const u8, @ptrCast(line)), line.len); +} + +pub export fn roc_fx_putLine(rocPath: *str.RocStr) i64 { + const stdout = std.io.getStdOut().writer(); + + for (rocPath.asSlice()) |char| { + stdout.print("{c}", .{char}) catch unreachable; + } + + stdout.print("\n", .{}) catch unreachable; + + return 0; +} + +const GetInt = extern struct { + value: i64, + error_code: u8, + is_error: bool, +}; + +pub export fn roc_fx_getInt() GetInt { + if (roc_fx_getInt_help()) |value| { + const get_int = GetInt{ .is_error = false, .value = value, .error_code = 0 }; + return get_int; + } else |err| switch (err) { + error.InvalidCharacter => { + return GetInt{ .is_error = true, .value = 0, .error_code = 0 }; + }, + else => { + return GetInt{ .is_error = true, .value = 0, .error_code = 1 }; + }, + } + + return 0; +} + +fn roc_fx_getInt_help() !i64 { + const stdin = std.io.getStdIn().reader(); + var buf: [40]u8 = undefined; + + // make sure to strip `\r` on windows + const raw_line: []u8 = (try stdin.readUntilDelimiterOrEof(&buf, '\n')) orelse ""; + const line = std.mem.trimRight(u8, raw_line, &std.ascii.whitespace); + + return std.fmt.parseInt(i64, line, 10); +} diff --git a/examples/cli/effects-platform/main.roc b/examples/cli/effects-platform/main.roc new file mode 100644 index 0000000000..10da2898cb --- /dev/null +++ b/examples/cli/effects-platform/main.roc @@ -0,0 +1,9 @@ +platform "effects" + requires {} { main : Effect.Effect {} } + exposes [] + packages {} + imports [Effect] + provides [mainForHost] + +mainForHost : Effect.Effect {} +mainForHost = main diff --git a/examples/interactive/effects.roc b/examples/cli/effects.roc similarity index 89% rename from examples/interactive/effects.roc rename to examples/cli/effects.roc index 5eee79c45a..7c434bd79f 100644 --- a/examples/interactive/effects.roc +++ b/examples/cli/effects.roc @@ -1,5 +1,5 @@ app "effects" - packages { pf: "effects-platform" } + packages { pf: "effects-platform/main.roc" } imports [pf.Effect] provides [main] to pf diff --git a/examples/cli/env.roc b/examples/cli/env.roc new file mode 100644 index 0000000000..eb2a6184c9 --- /dev/null +++ b/examples/cli/env.roc @@ -0,0 +1,30 @@ +app "env" + packages { pf: "https://github.com/roc-lang/basic-cli/releases/download/0.7.0/bkGby8jb0tmZYsy2hg1E_B2QrCgcSTxdUlHtETwm5m4.tar.br" } + imports [pf.Stdout, pf.Stderr, pf.Env, pf.Task.{ Task }] + provides [main] to pf + +main : Task {} I32 +main = + task = + Env.decode "EDITOR" + |> Task.await (\editor -> Stdout.line "Your favorite editor is \(editor)!") + |> Task.await (\{} -> Env.decode "SHLVL") + |> Task.await + (\lvl -> + when lvl is + 1u8 -> Stdout.line "You're running this in a root shell!" + n -> + lvlStr = Num.toStr n + + Stdout.line "Your current shell level is \(lvlStr)!") + |> Task.await \{} -> Env.decode "LETTERS" + + Task.attempt task \result -> + when result is + Ok letters -> + joinedLetters = Str.joinWith letters " " + + Stdout.line "Your favorite letters are: \(joinedLetters)" + + Err _ -> + Stderr.line "I couldn't find your favorite letters in the environment variables!" diff --git a/examples/false-interpreter/.gitignore b/examples/cli/false-interpreter/.gitignore similarity index 100% rename from examples/false-interpreter/.gitignore rename to examples/cli/false-interpreter/.gitignore diff --git a/examples/false-interpreter/Context.roc b/examples/cli/false-interpreter/Context.roc similarity index 84% rename from examples/false-interpreter/Context.roc rename to examples/cli/false-interpreter/Context.roc index e840af873f..86cf21ca5e 100644 --- a/examples/false-interpreter/Context.roc +++ b/examples/cli/false-interpreter/Context.roc @@ -19,43 +19,34 @@ pushStack = \ctx, data -> # I think an open tag union should just work here. # Instead at a call sites, I need to match on the error and then return the same error. # Otherwise it hits unreachable code in ir.rs -popStack : Context -> Result [T Context Data] [EmptyStack]* +popStack : Context -> Result [T Context Data] [EmptyStack] popStack = \ctx -> when List.last ctx.stack is Ok val -> poppedCtx = { ctx & stack: List.dropAt ctx.stack (List.len ctx.stack - 1) } Ok (T poppedCtx val) + Err ListWasEmpty -> Err EmptyStack toStrData : Data -> Str toStrData = \data -> when data is - Lambda _ -> - "[]" - Number n -> - Num.toStr (Num.intCast n) - Var v -> - Variable.toStr v + Lambda _ -> "[]" + Number n -> Num.toStr (Num.intCast n) + Var v -> Variable.toStr v toStrState : State -> Str toStrState = \state -> when state is - Executing -> - "Executing" - InComment -> - "InComment" - InString _ -> - "InString" - InNumber _ -> - "InNumber" - InLambda _ _ -> - "InLambda" - InSpecialChar -> - "InSpecialChar" - LoadChar -> - "LoadChar" + Executing -> "Executing" + InComment -> "InComment" + InString _ -> "InString" + InNumber _ -> "InNumber" + InLambda _ _ -> "InLambda" + InSpecialChar -> "InSpecialChar" + LoadChar -> "LoadChar" toStr : Context -> Str toStr = \{ scopes, stack, state, vars } -> @@ -75,20 +66,22 @@ with = \path, callback -> callback { scopes: [{ data: Some handle, index: 0, buf: [], whileInfo: None }], state: Executing, stack: [], vars: List.repeat (Number 0) Variable.totalCount } # I am pretty sure there is a syntax to destructure and keep a reference to the whole, but Im not sure what it is. -getChar : Context -> Task [T U8 Context] [EndOfData, NoScope]* +getChar : Context -> Task [T U8 Context] [EndOfData, NoScope] getChar = \ctx -> when List.last ctx.scopes is Ok scope -> (T val newScope) <- Task.await (getCharScope scope) Task.succeed (T val { ctx & scopes: List.set ctx.scopes (List.len ctx.scopes - 1) newScope }) + Err ListWasEmpty -> Task.fail NoScope -getCharScope : Scope -> Task [T U8 Scope] [EndOfData, NoScope]* +getCharScope : Scope -> Task [T U8 Scope] [EndOfData, NoScope] getCharScope = \scope -> when List.get scope.buf scope.index is Ok val -> Task.succeed (T val { scope & index: scope.index + 1 }) + Err OutOfBounds -> when scope.data is Some h -> @@ -97,8 +90,10 @@ getCharScope = \scope -> Ok val -> # This starts at 1 because the first character is already being returned. Task.succeed (T val { scope & buf: bytes, index: 1 }) + Err ListWasEmpty -> Task.fail EndOfData + None -> Task.fail EndOfData @@ -107,5 +102,6 @@ inWhileScope = \ctx -> when List.last ctx.scopes is Ok scope -> scope.whileInfo != None + Err ListWasEmpty -> - False + Bool.false diff --git a/examples/false-interpreter/False.roc b/examples/cli/false-interpreter/False.roc similarity index 85% rename from examples/false-interpreter/False.roc rename to examples/cli/false-interpreter/False.roc index 55d1e2b81d..0007f13307 100644 --- a/examples/false-interpreter/False.roc +++ b/examples/cli/false-interpreter/False.roc @@ -1,5 +1,5 @@ app "false" - packages { pf: "platform" } + packages { pf: "platform/main.roc" } imports [pf.Task.{ Task }, pf.Stdout, pf.Stdin, Context.{ Context }, Variable.{ Variable }] provides [main] to pf @@ -21,7 +21,7 @@ InterpreterErrors : [BadUtf8, DivByZero, EmptyStack, InvalidBooleanValue, Invali main : Str -> Task {} [] main = \filename -> interpretFile filename - |> Task.onFail (\StringErr e -> Stdout.line "Ran into problem:\n\(e)\n") + |> Task.onFail \StringErr e -> Stdout.line "Ran into problem:\n\(e)\n" interpretFile : Str -> Task {} [StringErr Str] interpretFile = \filename -> @@ -30,51 +30,59 @@ interpretFile = \filename -> when result is Ok _ -> Task.succeed {} + Err BadUtf8 -> Task.fail (StringErr "Failed to convert string from Utf8 bytes") + Err DivByZero -> Task.fail (StringErr "Division by zero") + Err EmptyStack -> Task.fail (StringErr "Tried to pop a value off of the stack when it was empty") + Err InvalidBooleanValue -> Task.fail (StringErr "Ran into an invalid boolean that was neither false (0) or true (-1)") + Err (InvalidChar char) -> Task.fail (StringErr "Ran into an invalid character with ascii code: \(char)") + Err MaxInputNumber -> Task.fail (StringErr "Like the original false compiler, the max input number is 320,000") + Err NoLambdaOnStack -> Task.fail (StringErr "Tried to run a lambda when no lambda was on the stack") + Err NoNumberOnStack -> Task.fail (StringErr "Tried to run a number when no number was on the stack") + Err NoVariableOnStack -> Task.fail (StringErr "Tried to load a variable when no variable was on the stack") + Err NoScope -> Task.fail (StringErr "Tried to run code when not in any scope") + Err OutOfBounds -> Task.fail (StringErr "Tried to load from an offset that was outside of the stack") + Err UnexpectedEndOfData -> Task.fail (StringErr "Hit end of data while still parsing something") isDigit : U8 -> Bool isDigit = \char -> char - >= 0x30# `0` - - && char - <= 0x39# `0` + >= 0x30 # `0` + && char + <= 0x39 # `0` isWhitespace : U8 -> Bool isWhitespace = \char -> char - == 0xA# new line - - || char - == 0xB# carriage return - - || char - == 0x20# space - - || char - == 0x9# tab + == 0xA # new line + || char + == 0xD # carriage return + || char + == 0x20 # space + || char + == 0x9 # tab interpretCtx : Context -> Task Context InterpreterErrors interpretCtx = \ctx -> Task.loop ctx interpretCtxLoop @@ -101,17 +109,22 @@ interpretCtxLoop = \ctx -> newScope = { scope & whileInfo: Some { state: InBody, body, cond } } Task.succeed (Step { popCtx & scopes: List.append (List.set ctx.scopes last newScope) { data: None, buf: body, index: 0, whileInfo: None } }) + Err e -> Task.fail e + Some { state: InBody, body, cond } -> # Just rand the body. Run the condition again. newScope = { scope & whileInfo: Some { state: InCond, body, cond } } Task.succeed (Step { ctx & scopes: List.append (List.set ctx.scopes last newScope) { data: None, buf: cond, index: 0, whileInfo: None } }) + None -> Task.fail NoScope + Err OutOfBounds -> Task.fail NoScope + Executing -> # {} <- Task.await (Stdout.line (Context.toStr ctx)) result <- Task.attempt (Context.getChar ctx) @@ -119,8 +132,10 @@ interpretCtxLoop = \ctx -> Ok (T val newCtx) -> execCtx <- Task.await (stepExecCtx newCtx val) Task.succeed (Step execCtx) + Err NoScope -> Task.fail NoScope + Err EndOfData -> # Computation complete for this scope. # Drop a scope. @@ -131,6 +146,7 @@ interpretCtxLoop = \ctx -> Task.succeed (Done dropCtx) else Task.succeed (Step dropCtx) + InComment -> result <- Task.attempt (Context.getChar ctx) when result is @@ -140,10 +156,13 @@ interpretCtxLoop = \ctx -> Task.succeed (Step { newCtx & state: Executing }) else Task.succeed (Step { newCtx & state: InComment }) + Err NoScope -> Task.fail NoScope + Err EndOfData -> Task.fail UnexpectedEndOfData + InNumber accum -> result <- Task.attempt (Context.getChar ctx) when result is @@ -162,10 +181,13 @@ interpretCtxLoop = \ctx -> execCtx <- Task.await (stepExecCtx { pushCtx & state: Executing } val) Task.succeed (Step execCtx) + Err NoScope -> Task.fail NoScope + Err EndOfData -> Task.fail UnexpectedEndOfData + InString bytes -> result <- Task.attempt (Context.getChar ctx) when result is @@ -176,14 +198,18 @@ interpretCtxLoop = \ctx -> Ok str -> {} <- Task.await (Stdout.raw str) Task.succeed (Step { newCtx & state: Executing }) + Err _ -> Task.fail BadUtf8 else Task.succeed (Step { newCtx & state: InString (List.append bytes val) }) + Err NoScope -> Task.fail NoScope + Err EndOfData -> Task.fail UnexpectedEndOfData + InLambda depth bytes -> result <- Task.attempt (Context.getChar ctx) when result is @@ -201,49 +227,57 @@ interpretCtxLoop = \ctx -> Task.succeed (Step { newCtx & state: InLambda (depth - 1) (List.append bytes val) }) else Task.succeed (Step { newCtx & state: InLambda depth (List.append bytes val) }) + Err NoScope -> Task.fail NoScope + Err EndOfData -> Task.fail UnexpectedEndOfData + InSpecialChar -> result <- Task.attempt (Context.getChar { ctx & state: Executing }) when result is Ok (T 0xB8 newCtx) -> result2 = - (T popCtx index) <- Result.after (popNumber newCtx) + (T popCtx index) <- Result.try (popNumber newCtx) # I think Num.abs is too restrictive, it should be able to produce a natural number, but it seem to be restricted to signed numbers. size = List.len popCtx.stack - 1 offset = Num.intCast size - index if offset >= 0 then - stackVal <- Result.after (List.get popCtx.stack (Num.intCast offset)) + stackVal <- Result.try (List.get popCtx.stack (Num.intCast offset)) Ok (Context.pushStack popCtx stackVal) else Err OutOfBounds when result2 is - Ok a -> - Task.succeed (Step a) - Err e -> - Task.fail e + Ok a -> Task.succeed (Step a) + Err e -> Task.fail e + Ok (T 0x9F newCtx) -> # This is supposed to flush io buffers. We don't buffer, so it does nothing Task.succeed (Step newCtx) + Ok (T x _) -> data = Num.toStr (Num.intCast x) Task.fail (InvalidChar data) + Err NoScope -> Task.fail NoScope + Err EndOfData -> Task.fail UnexpectedEndOfData + LoadChar -> result <- Task.attempt (Context.getChar { ctx & state: Executing }) when result is Ok (T x newCtx) -> Task.succeed (Step (Context.pushStack newCtx (Number (Num.intCast x)))) + Err NoScope -> Task.fail NoScope + Err EndOfData -> Task.fail UnexpectedEndOfData @@ -255,26 +289,28 @@ stepExecCtx = \ctx, char -> # `!` execute lambda Task.fromResult ( - (T popCtx bytes) <- Result.after (popLambda ctx) + (T popCtx bytes) <- Result.try (popLambda ctx) Ok { popCtx & scopes: List.append popCtx.scopes { data: None, buf: bytes, index: 0, whileInfo: None } } ) + 0x3F -> # `?` if Task.fromResult ( - (T popCtx1 bytes) <- Result.after (popLambda ctx) - (T popCtx2 n1) <- Result.after (popNumber popCtx1) + (T popCtx1 bytes) <- Result.try (popLambda ctx) + (T popCtx2 n1) <- Result.try (popNumber popCtx1) if n1 == 0 then Ok popCtx2 else Ok { popCtx2 & scopes: List.append popCtx2.scopes { data: None, buf: bytes, index: 0, whileInfo: None } } ) + 0x23 -> # `#` while Task.fromResult ( - (T popCtx1 body) <- Result.after (popLambda ctx) - (T popCtx2 cond) <- Result.after (popLambda popCtx1) + (T popCtx1 body) <- Result.try (popLambda ctx) + (T popCtx2 cond) <- Result.try (popLambda popCtx1) last = (List.len popCtx2.scopes - 1) when List.get popCtx2.scopes last is @@ -284,132 +320,149 @@ stepExecCtx = \ctx, char -> # push a scope to execute the condition. Ok { popCtx2 & scopes: List.append scopes { data: None, buf: cond, index: 0, whileInfo: None } } + Err OutOfBounds -> Err NoScope ) + 0x24 -> # `$` dup # Switching this to List.last and changing the error to ListWasEmpty leads to a compiler bug. # Complains about the types eq not matching. when List.get ctx.stack (List.len ctx.stack - 1) is - Ok dupItem -> - Task.succeed (Context.pushStack ctx dupItem) - Err OutOfBounds -> - Task.fail EmptyStack + Ok dupItem -> Task.succeed (Context.pushStack ctx dupItem) + Err OutOfBounds -> Task.fail EmptyStack + 0x25 -> # `%` drop when Context.popStack ctx is # Dropping with an empty stack, all results here are fine - Ok (T popCtx _) -> - Task.succeed popCtx - Err _ -> - Task.succeed ctx + Ok (T popCtx _) -> Task.succeed popCtx + Err _ -> Task.succeed ctx + 0x5C -> # `\` swap result2 = - (T popCtx1 n1) <- Result.after (Context.popStack ctx) - (T popCtx2 n2) <- Result.after (Context.popStack popCtx1) + (T popCtx1 n1) <- Result.try (Context.popStack ctx) + (T popCtx2 n2) <- Result.try (Context.popStack popCtx1) Ok (Context.pushStack (Context.pushStack popCtx2 n1) n2) when result2 is Ok a -> Task.succeed a + # Being explicit with error type is required to stop the need to propogate the error parameters to Context.popStack Err EmptyStack -> Task.fail EmptyStack + 0x40 -> # `@` rot result2 = - (T popCtx1 n1) <- Result.after (Context.popStack ctx) - (T popCtx2 n2) <- Result.after (Context.popStack popCtx1) - (T popCtx3 n3) <- Result.after (Context.popStack popCtx2) + (T popCtx1 n1) <- Result.try (Context.popStack ctx) + (T popCtx2 n2) <- Result.try (Context.popStack popCtx1) + (T popCtx3 n3) <- Result.try (Context.popStack popCtx2) Ok (Context.pushStack (Context.pushStack (Context.pushStack popCtx3 n2) n1) n3) when result2 is Ok a -> Task.succeed a + # Being explicit with error type is required to stop the need to propogate the error parameters to Context.popStack Err EmptyStack -> Task.fail EmptyStack + 0xC3 -> # `ø` pick or `ß` flush # these are actually 2 bytes, 0xC3 0xB8 or 0xC3 0x9F # requires special parsing Task.succeed { ctx & state: InSpecialChar } + 0x4F -> # `O` also treat this as pick for easier script writing Task.fromResult ( - (T popCtx index) <- Result.after (popNumber ctx) + (T popCtx index) <- Result.try (popNumber ctx) # I think Num.abs is too restrictive, it should be able to produce a natural number, but it seem to be restricted to signed numbers. size = List.len popCtx.stack - 1 offset = Num.intCast size - index if offset >= 0 then - stackVal <- Result.after (List.get popCtx.stack (Num.intCast offset)) + stackVal <- Result.try (List.get popCtx.stack (Num.intCast offset)) Ok (Context.pushStack popCtx stackVal) else Err OutOfBounds ) + 0x42 -> # `B` also treat this as flush for easier script writing # This is supposed to flush io buffers. We don't buffer, so it does nothing Task.succeed ctx + 0x27 -> # `'` load next char Task.succeed { ctx & state: LoadChar } + 0x2B -> # `+` add Task.fromResult (binaryOp ctx Num.addWrap) + 0x2D -> # `-` sub Task.fromResult (binaryOp ctx Num.subWrap) + 0x2A -> # `*` mul Task.fromResult (binaryOp ctx Num.mulWrap) + 0x2F -> # `/` div # Due to possible division by zero error, this must be handled specially. Task.fromResult ( - (T popCtx1 numR) <- Result.after (popNumber ctx) - (T popCtx2 numL) <- Result.after (popNumber popCtx1) - res <- Result.after (Num.divTruncChecked numL numR) + (T popCtx1 numR) <- Result.try (popNumber ctx) + (T popCtx2 numL) <- Result.try (popNumber popCtx1) + res <- Result.try (Num.divTruncChecked numL numR) Ok (Context.pushStack popCtx2 (Number res)) ) + 0x26 -> # `&` bitwise and Task.fromResult (binaryOp ctx Num.bitwiseAnd) + 0x7C -> # `|` bitwise or Task.fromResult (binaryOp ctx Num.bitwiseOr) + 0x3D -> # `=` equals Task.fromResult ( - a, b <- binaryOp ctx - if a == b then - -1 - else - 0 + binaryOp ctx \a, b -> + if a == b then + -1 + else + 0 ) + 0x3E -> # `>` greater than Task.fromResult ( - a, b <- binaryOp ctx - if a > b then - -1 - else - 0 + binaryOp ctx \a, b -> + if a > b then + -1 + else + 0 ) + 0x5F -> # `_` negate Task.fromResult (unaryOp ctx Num.neg) + 0x7E -> # `~` bitwise not - Task.fromResult (unaryOp ctx (\x -> Num.bitwiseXor x -1)) - # xor with -1 should be bitwise not + Task.fromResult (unaryOp ctx (\x -> Num.bitwiseXor x -1)) # xor with -1 should be bitwise not + 0x2C -> # `,` write char when popNumber ctx is @@ -418,18 +471,23 @@ stepExecCtx = \ctx, char -> Ok str -> {} <- Task.await (Stdout.raw str) Task.succeed popCtx + Err _ -> Task.fail BadUtf8 + Err e -> Task.fail e + 0x2E -> # `.` write int when popNumber ctx is Ok (T popCtx num) -> {} <- Task.await (Stdout.raw (Num.toStr (Num.intCast num))) Task.succeed popCtx + Err e -> Task.fail e + 0x5E -> # `^` read char as int in <- Task.await Stdin.char @@ -438,42 +496,51 @@ stepExecCtx = \ctx, char -> Task.succeed (Context.pushStack ctx (Number -1)) else Task.succeed (Context.pushStack ctx (Number (Num.intCast in))) + 0x3A -> # `:` store to variable Task.fromResult ( - (T popCtx1 var) <- Result.after (popVariable ctx) + (T popCtx1 var) <- Result.try (popVariable ctx) # The Result.mapErr on the next line maps from EmptyStack in Context.roc to the full InterpreterErrors union here. - (T popCtx2 n1) <- Result.after (Result.mapErr (Context.popStack popCtx1) (\EmptyStack -> EmptyStack)) + (T popCtx2 n1) <- Result.try (Result.mapErr (Context.popStack popCtx1) (\EmptyStack -> EmptyStack)) Ok { popCtx2 & vars: List.set popCtx2.vars (Variable.toIndex var) n1 } ) + 0x3B -> # `;` load from variable Task.fromResult ( - (T popCtx var) <- Result.after (popVariable ctx) - elem <- Result.after (List.get popCtx.vars (Variable.toIndex var)) + (T popCtx var) <- Result.try (popVariable ctx) + elem <- Result.try (List.get popCtx.vars (Variable.toIndex var)) Ok (Context.pushStack popCtx elem) ) + 0x22 -> # `"` string start Task.succeed { ctx & state: InString [] } + 0x5B -> # `"` string start Task.succeed { ctx & state: InLambda 0 [] } + 0x7B -> # `{` comment start Task.succeed { ctx & state: InComment } + x if isDigit x -> # number start Task.succeed { ctx & state: InNumber (Num.intCast (x - 0x30)) } + x if isWhitespace x -> Task.succeed ctx + x -> when Variable.fromUtf8 x is # letters are variable names Ok var -> Task.succeed (Context.pushStack ctx (Var var)) + Err _ -> data = Num.toStr (Num.intCast x) @@ -481,41 +548,32 @@ stepExecCtx = \ctx, char -> unaryOp : Context, (I32 -> I32) -> Result Context InterpreterErrors unaryOp = \ctx, op -> - (T popCtx num) <- Result.after (popNumber ctx) + (T popCtx num) <- Result.try (popNumber ctx) Ok (Context.pushStack popCtx (Number (op num))) binaryOp : Context, (I32, I32 -> I32) -> Result Context InterpreterErrors binaryOp = \ctx, op -> - (T popCtx1 numR) <- Result.after (popNumber ctx) - (T popCtx2 numL) <- Result.after (popNumber popCtx1) + (T popCtx1 numR) <- Result.try (popNumber ctx) + (T popCtx2 numL) <- Result.try (popNumber popCtx1) Ok (Context.pushStack popCtx2 (Number (op numL numR))) popNumber : Context -> Result [T Context I32] InterpreterErrors popNumber = \ctx -> when Context.popStack ctx is - Ok (T popCtx (Number num)) -> - Ok (T popCtx num) - Ok _ -> - Err (NoNumberOnStack) - Err EmptyStack -> - Err EmptyStack + Ok (T popCtx (Number num)) -> Ok (T popCtx num) + Ok _ -> Err (NoNumberOnStack) + Err EmptyStack -> Err EmptyStack popLambda : Context -> Result [T Context (List U8)] InterpreterErrors popLambda = \ctx -> when Context.popStack ctx is - Ok (T popCtx (Lambda bytes)) -> - Ok (T popCtx bytes) - Ok _ -> - Err NoLambdaOnStack - Err EmptyStack -> - Err EmptyStack + Ok (T popCtx (Lambda bytes)) -> Ok (T popCtx bytes) + Ok _ -> Err NoLambdaOnStack + Err EmptyStack -> Err EmptyStack popVariable : Context -> Result [T Context Variable] InterpreterErrors popVariable = \ctx -> when Context.popStack ctx is - Ok (T popCtx (Var var)) -> - Ok (T popCtx var) - Ok _ -> - Err NoVariableOnStack - Err EmptyStack -> - Err EmptyStack + Ok (T popCtx (Var var)) -> Ok (T popCtx var) + Ok _ -> Err NoVariableOnStack + Err EmptyStack -> Err EmptyStack diff --git a/examples/false-interpreter/README.md b/examples/cli/false-interpreter/README.md similarity index 100% rename from examples/false-interpreter/README.md rename to examples/cli/false-interpreter/README.md diff --git a/examples/cli/false-interpreter/Variable.roc b/examples/cli/false-interpreter/Variable.roc new file mode 100644 index 0000000000..e7b959d2f1 --- /dev/null +++ b/examples/cli/false-interpreter/Variable.roc @@ -0,0 +1,36 @@ +interface Variable + exposes [Variable, fromUtf8, toIndex, totalCount, toStr] + imports [] + +# Variables in False can only be single letters. Thus, the valid variables are "a" to "z". +# This opaque type deals with ensure we always have valid variables. +Variable := U8 + +totalCount : Nat +totalCount = + 0x7A # "z" + - 0x61 # "a" + + 1 + +toStr : Variable -> Str +toStr = \@Variable char -> + when Str.fromUtf8 [char] is + Ok str -> str + _ -> "_" + +fromUtf8 : U8 -> Result Variable [InvalidVariableUtf8] +fromUtf8 = \char -> + if + char + >= 0x61 # "a" + && char + <= 0x7A # "z" + then + Ok (@Variable char) + else + Err InvalidVariableUtf8 + +toIndex : Variable -> Nat +toIndex = \@Variable char -> + Num.intCast (char - 0x61) # "a" +# List.first (Str.toUtf8 "a") diff --git a/examples/false-interpreter/examples/bottles.false b/examples/cli/false-interpreter/examples/bottles.false similarity index 100% rename from examples/false-interpreter/examples/bottles.false rename to examples/cli/false-interpreter/examples/bottles.false diff --git a/examples/false-interpreter/examples/cksum.false b/examples/cli/false-interpreter/examples/cksum.false similarity index 100% rename from examples/false-interpreter/examples/cksum.false rename to examples/cli/false-interpreter/examples/cksum.false diff --git a/examples/false-interpreter/examples/copy.false b/examples/cli/false-interpreter/examples/copy.false similarity index 100% rename from examples/false-interpreter/examples/copy.false rename to examples/cli/false-interpreter/examples/copy.false diff --git a/examples/false-interpreter/examples/crc32.false b/examples/cli/false-interpreter/examples/crc32.false similarity index 100% rename from examples/false-interpreter/examples/crc32.false rename to examples/cli/false-interpreter/examples/crc32.false diff --git a/examples/false-interpreter/examples/hello.false b/examples/cli/false-interpreter/examples/hello.false similarity index 100% rename from examples/false-interpreter/examples/hello.false rename to examples/cli/false-interpreter/examples/hello.false diff --git a/examples/false-interpreter/examples/in.txt b/examples/cli/false-interpreter/examples/in.txt similarity index 100% rename from examples/false-interpreter/examples/in.txt rename to examples/cli/false-interpreter/examples/in.txt diff --git a/examples/false-interpreter/examples/odd_words.false b/examples/cli/false-interpreter/examples/odd_words.false similarity index 100% rename from examples/false-interpreter/examples/odd_words.false rename to examples/cli/false-interpreter/examples/odd_words.false diff --git a/examples/false-interpreter/examples/primes.false b/examples/cli/false-interpreter/examples/primes.false similarity index 100% rename from examples/false-interpreter/examples/primes.false rename to examples/cli/false-interpreter/examples/primes.false diff --git a/examples/false-interpreter/examples/queens.false b/examples/cli/false-interpreter/examples/queens.false similarity index 100% rename from examples/false-interpreter/examples/queens.false rename to examples/cli/false-interpreter/examples/queens.false diff --git a/examples/false-interpreter/examples/sqrt.false b/examples/cli/false-interpreter/examples/sqrt.false similarity index 100% rename from examples/false-interpreter/examples/sqrt.false rename to examples/cli/false-interpreter/examples/sqrt.false diff --git a/examples/false-interpreter/examples/test.false b/examples/cli/false-interpreter/examples/test.false similarity index 100% rename from examples/false-interpreter/examples/test.false rename to examples/cli/false-interpreter/examples/test.false diff --git a/examples/cli/false-interpreter/platform/Cargo.toml b/examples/cli/false-interpreter/platform/Cargo.toml new file mode 100644 index 0000000000..1cce894c1d --- /dev/null +++ b/examples/cli/false-interpreter/platform/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "host" +authors = ["The Roc Contributors"] +edition = "2021" +license = "UPL-1.0" +version = "0.0.1" + +links = "app" + +[lib] +name = "host" +path = "src/lib.rs" +crate-type = ["staticlib", "rlib"] + +[[bin]] +name = "host" +path = "src/main.rs" + +[dependencies] +libc = "0.2" +roc_std = { path = "../../../../crates/roc_std" } + +[workspace] diff --git a/examples/false-interpreter/platform/Effect.roc b/examples/cli/false-interpreter/platform/Effect.roc similarity index 100% rename from examples/false-interpreter/platform/Effect.roc rename to examples/cli/false-interpreter/platform/Effect.roc diff --git a/examples/cli/false-interpreter/platform/File.roc b/examples/cli/false-interpreter/platform/File.roc new file mode 100644 index 0000000000..f112470a39 --- /dev/null +++ b/examples/cli/false-interpreter/platform/File.roc @@ -0,0 +1,27 @@ +interface File + exposes [line, Handle, withOpen, chunk] + imports [pf.Effect, Task.{ Task }] + +Handle := U64 + +line : Handle -> Task.Task Str * +line = \@Handle handle -> Effect.after (Effect.getFileLine handle) Task.succeed + +chunk : Handle -> Task.Task (List U8) * +chunk = \@Handle handle -> Effect.after (Effect.getFileBytes handle) Task.succeed + +open : Str -> Task.Task Handle * +open = \path -> + Effect.openFile path + |> Effect.map (\id -> @Handle id) + |> Effect.after Task.succeed + +close : Handle -> Task.Task {} * +close = \@Handle handle -> Effect.after (Effect.closeFile handle) Task.succeed + +withOpen : Str, (Handle -> Task {} a) -> Task {} a +withOpen = \path, callback -> + handle <- Task.await (open path) + result <- Task.attempt (callback handle) + {} <- Task.await (close handle) + Task.fromResult result diff --git a/examples/cli/false-interpreter/platform/Stdin.roc b/examples/cli/false-interpreter/platform/Stdin.roc new file mode 100644 index 0000000000..d734dffc1f --- /dev/null +++ b/examples/cli/false-interpreter/platform/Stdin.roc @@ -0,0 +1,8 @@ +interface Stdin + exposes [char] + imports [pf.Effect, Task] + +# line : Task.Task Str * +# line = Effect.after Effect.getLine Task.succeed # TODO FIXME Effect.getLine should suffice +char : Task.Task U8 * +char = Effect.after Effect.getChar Task.succeed # TODO FIXME Effect.getLine should suffice diff --git a/examples/false-interpreter/platform/Stdout.roc b/examples/cli/false-interpreter/platform/Stdout.roc similarity index 100% rename from examples/false-interpreter/platform/Stdout.roc rename to examples/cli/false-interpreter/platform/Stdout.roc diff --git a/examples/cli/false-interpreter/platform/Task.roc b/examples/cli/false-interpreter/platform/Task.roc new file mode 100644 index 0000000000..c6bbb698c9 --- /dev/null +++ b/examples/cli/false-interpreter/platform/Task.roc @@ -0,0 +1,68 @@ +interface Task + exposes [Task, succeed, fail, await, map, onFail, attempt, fromResult, loop] + imports [pf.Effect] + +Task ok err : Effect.Effect (Result ok err) + +loop : state, (state -> Task [Step state, Done done] err) -> Task done err +loop = \state, step -> + looper = \current -> + step current + |> Effect.map + \res -> + when res is + Ok (Step newState) -> Step newState + Ok (Done result) -> Done (Ok result) + Err e -> Done (Err e) + + Effect.loop state looper + +succeed : val -> Task val * +succeed = \val -> + Effect.always (Ok val) + +fail : err -> Task * err +fail = \val -> + Effect.always (Err val) + +fromResult : Result a e -> Task a e +fromResult = \result -> + when result is + Ok a -> succeed a + Err e -> fail e + +attempt : Task a b, (Result a b -> Task c d) -> Task c d +attempt = \effect, transform -> + Effect.after + effect + \result -> + when result is + Ok ok -> transform (Ok ok) + Err err -> transform (Err err) + +await : Task a err, (a -> Task b err) -> Task b err +await = \effect, transform -> + Effect.after + effect + \result -> + when result is + Ok a -> transform a + Err err -> Task.fail err + +onFail : Task ok a, (a -> Task ok b) -> Task ok b +onFail = \effect, transform -> + Effect.after + effect + \result -> + when result is + Ok a -> Task.succeed a + Err err -> transform err + +map : Task a err, (a -> b) -> Task b err +map = \effect, transform -> + Effect.after + effect + \result -> + when result is + Ok a -> Task.succeed (transform a) + Err err -> Task.fail err diff --git a/examples/cli/false-interpreter/platform/build.rs b/examples/cli/false-interpreter/platform/build.rs new file mode 100644 index 0000000000..47763b34c3 --- /dev/null +++ b/examples/cli/false-interpreter/platform/build.rs @@ -0,0 +1,9 @@ +fn main() { + #[cfg(not(windows))] + println!("cargo:rustc-link-lib=dylib=app"); + + #[cfg(windows)] + println!("cargo:rustc-link-lib=dylib=libapp"); + + println!("cargo:rustc-link-search=."); +} diff --git a/examples/false-interpreter/platform/host.c b/examples/cli/false-interpreter/platform/host.c similarity index 100% rename from examples/false-interpreter/platform/host.c rename to examples/cli/false-interpreter/platform/host.c diff --git a/examples/cli/false-interpreter/platform/main.roc b/examples/cli/false-interpreter/platform/main.roc new file mode 100644 index 0000000000..cf97352ce4 --- /dev/null +++ b/examples/cli/false-interpreter/platform/main.roc @@ -0,0 +1,9 @@ +platform "false-interpreter" + requires {} { main : Str -> Task {} [] } + exposes [] + packages {} + imports [Task.{ Task }] + provides [mainForHost] + +mainForHost : Str -> Task {} [] +mainForHost = \file -> main file diff --git a/examples/cli/false-interpreter/platform/src/lib.rs b/examples/cli/false-interpreter/platform/src/lib.rs new file mode 100644 index 0000000000..eb48d41bb3 --- /dev/null +++ b/examples/cli/false-interpreter/platform/src/lib.rs @@ -0,0 +1,241 @@ +#![allow(non_snake_case)] + +use core::ffi::c_void; +use core::mem::MaybeUninit; +use libc; +use roc_std::{RocList, RocStr}; +use std::env; +use std::ffi::CStr; +use std::fs::File; +use std::io::{BufRead, BufReader, Read, Write}; +use std::os::raw::c_char; + +extern "C" { + #[link_name = "roc__mainForHost_1_exposed_generic"] + fn roc_main(output: *mut u8, args: &RocStr); + + #[link_name = "roc__mainForHost_1_exposed_size"] + fn roc_main_size() -> i64; + + #[link_name = "roc__mainForHost_0_caller"] + fn call_Fx(flags: *const u8, closure_data: *const u8, output: *mut u8); + + #[allow(dead_code)] + #[link_name = "roc__mainForHost_0_size"] + fn size_Fx() -> i64; + + #[link_name = "roc__mainForHost_0_result_size"] + fn size_Fx_result() -> i64; +} + +#[no_mangle] +pub unsafe extern "C" fn roc_alloc(size: usize, _alignment: u32) -> *mut c_void { + libc::malloc(size) +} + +#[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) +} + +#[no_mangle] +pub unsafe extern "C" fn roc_dealloc(c_ptr: *mut c_void, _alignment: u32) { + libc::free(c_ptr) +} + +#[no_mangle] +pub unsafe extern "C" fn roc_panic(msg: *mut RocStr, tag_id: u32) { + match tag_id { + 0 => { + eprintln!("Roc standard library hit a panic: {}", &*msg); + } + 1 => { + eprintln!("Application hit a panic: {}", &*msg); + } + _ => unreachable!(), + } + std::process::exit(1); +} + +#[no_mangle] +pub unsafe extern "C" fn roc_dbg(loc: *mut RocStr, msg: *mut RocStr) { + eprintln!("[{}] {}", &*loc, &*msg); +} + +#[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) +} + +#[cfg(unix)] +#[no_mangle] +pub unsafe extern "C" fn roc_getppid() -> libc::pid_t { + libc::getppid() +} + +#[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) +} + +#[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) +} + +#[no_mangle] +pub extern "C" fn rust_main() -> i32 { + let arg = env::args() + .nth(1) + .expect("Please pass a .false file as a command-line argument to the false interpreter!"); + let arg = RocStr::from(arg.as_str()); + + let size = unsafe { roc_main_size() } as usize; + + unsafe { + let buffer = roc_alloc(size, 1) as *mut u8; + + roc_main(buffer, &arg); + + // arg has been passed to roc now, and it assumes ownership. + // so we must not touch its refcount now + std::mem::forget(arg); + + let result = call_the_closure(buffer); + + roc_dealloc(buffer as _, 1); + + result + }; + + // Exit code + 0 +} + +unsafe fn call_the_closure(closure_data_ptr: *const u8) -> i64 { + let size = size_Fx_result() as usize; + let buffer = roc_alloc(size, 1) as *mut u8; + + call_Fx( + // This flags pointer will never get dereferenced + MaybeUninit::uninit().as_ptr(), + closure_data_ptr as *const u8, + buffer as *mut u8, + ); + + roc_dealloc(buffer as _, 1); + 0 +} + +#[no_mangle] +pub extern "C" fn roc_fx_getLine() -> RocStr { + let stdin = std::io::stdin(); + let line1 = stdin.lock().lines().next().unwrap().unwrap(); + + RocStr::from(line1.as_str()) +} + +#[no_mangle] +pub extern "C" fn roc_fx_getChar() -> u8 { + let mut buffer = [0]; + + if let Err(ioerr) = std::io::stdin().lock().read_exact(&mut buffer[..]) { + if ioerr.kind() == std::io::ErrorKind::UnexpectedEof { + u8::MAX + } else { + panic!("Got an unexpected error while reading char from stdin"); + } + } else { + buffer[0] + } +} + +#[no_mangle] +pub extern "C" fn roc_fx_putLine(line: &RocStr) { + let string = line.as_str(); + println!("{}", string); + let _ = std::io::stdout().lock().flush(); +} + +#[no_mangle] +pub extern "C" fn roc_fx_putRaw(line: &RocStr) { + let string = line.as_str(); + print!("{}", string); + let _ = std::io::stdout().lock().flush(); +} + +#[no_mangle] +pub extern "C" fn roc_fx_getFileLine(br_ptr: *mut BufReader) -> RocStr { + let br = unsafe { &mut *br_ptr }; + let mut line1 = String::default(); + + br.read_line(&mut line1) + .expect("Failed to read line from file"); + + RocStr::from(line1.as_str()) +} + +#[no_mangle] +pub extern "C" fn roc_fx_getFileBytes(br_ptr: *mut BufReader) -> RocList { + let br = unsafe { &mut *br_ptr }; + let mut buffer = [0; 0x10 /* This is intentionally small to ensure correct implementation */]; + + let count = br + .read(&mut buffer[..]) + .expect("Failed to read bytes from file"); + + RocList::from_slice(&buffer[..count]) +} + +#[no_mangle] +pub extern "C" fn roc_fx_closeFile(br_ptr: *mut BufReader) { + unsafe { + let boxed = Box::from_raw(br_ptr); + drop(boxed) + } +} + +#[no_mangle] +pub extern "C" fn roc_fx_openFile(name: &RocStr) -> *mut BufReader { + let string = name.as_str(); + match File::open(string) { + Ok(f) => { + let br = BufReader::new(f); + + Box::into_raw(Box::new(br)) + } + Err(_) => { + panic!("unable to open file {:?}", name) + } + } +} + +#[no_mangle] +pub extern "C" fn roc_fx_withFileOpen(_name: &RocStr, _buffer: *const u8) { + // TODO: figure out accepting a closure in an fx and passing data to it. + // let f = File::open(name.as_str()).expect("Unable to open file"); + // let mut br = BufReader::new(f); + + // unsafe { + // let closure_data_ptr = buffer.offset(8); + // call_the_closure(closure_data_ptr); + // } +} diff --git a/examples/cli/false-interpreter/platform/src/main.rs b/examples/cli/false-interpreter/platform/src/main.rs new file mode 100644 index 0000000000..0765384f29 --- /dev/null +++ b/examples/cli/false-interpreter/platform/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + std::process::exit(host::rust_main() as _); +} diff --git a/examples/cli/fileBROKEN.roc b/examples/cli/fileBROKEN.roc new file mode 100644 index 0000000000..bc63e5655a --- /dev/null +++ b/examples/cli/fileBROKEN.roc @@ -0,0 +1,44 @@ +app "file-io" + packages { pf: "https://github.com/roc-lang/basic-cli/releases/download/0.7.0/bkGby8jb0tmZYsy2hg1E_B2QrCgcSTxdUlHtETwm5m4.tar.br" } + imports [ + pf.Stdout, + pf.Stderr, + pf.Task.{ Task }, + pf.File, + pf.Path, + pf.Env, + pf.Dir, + ] + provides [main] to pf + +main : Task {} I32 +main = + path = Path.fromStr "out.txt" + task = + cwd <- Env.cwd |> Task.await + cwdStr = Path.display cwd + + _ <- Stdout.line "cwd: \(cwdStr)" |> Task.await + dirEntries <- Dir.list cwd |> Task.await + contentsStr = Str.joinWith (List.map dirEntries Path.display) "\n " + + _ <- Stdout.line "Directory contents:\n \(contentsStr)\n" |> Task.await + _ <- Stdout.line "Writing a string to out.txt" |> Task.await + _ <- File.writeUtf8 path "a string!" |> Task.await + contents <- File.readUtf8 path |> Task.await + Stdout.line "I read the file back. Its contents: \"\(contents)\"" + + Task.attempt task \result -> + when result is + Ok {} -> Stdout.line "Successfully wrote a string to out.txt" + Err err -> + msg = + when err is + FileWriteErr _ PermissionDenied -> "PermissionDenied" + FileWriteErr _ Unsupported -> "Unsupported" + FileWriteErr _ (Unrecognized _ other) -> other + FileReadErr _ _ -> "Error reading file" + _ -> "Uh oh, there was an error!" + + {} <- Stderr.line msg |> Task.await + Task.err 1 diff --git a/examples/cli/form.roc b/examples/cli/form.roc new file mode 100644 index 0000000000..3df5cd2d12 --- /dev/null +++ b/examples/cli/form.roc @@ -0,0 +1,20 @@ +app "form" + packages { pf: "https://github.com/roc-lang/basic-cli/releases/download/0.7.0/bkGby8jb0tmZYsy2hg1E_B2QrCgcSTxdUlHtETwm5m4.tar.br" } + imports [pf.Stdin, pf.Stdout, pf.Task.{ await, Task }] + provides [main] to pf + +main : Task {} I32 +main = + _ <- await (Stdout.line "What's your first name?") + firstName <- await Stdin.line + + _ <- await (Stdout.line "What's your last name?") + lastName <- await Stdin.line + + Stdout.line "Hi, \(unwrap firstName) \(unwrap lastName)! 👋" + +unwrap : [Input Str, End] -> Str +unwrap = \input -> + when input is + Input line -> line + End -> "Received end of input (EOF)." diff --git a/examples/cli/http-get.roc b/examples/cli/http-get.roc new file mode 100644 index 0000000000..a1890bbc27 --- /dev/null +++ b/examples/cli/http-get.roc @@ -0,0 +1,31 @@ +app "http-get" + packages { pf: "https://github.com/roc-lang/basic-cli/releases/download/0.7.0/bkGby8jb0tmZYsy2hg1E_B2QrCgcSTxdUlHtETwm5m4.tar.br" } + imports [pf.Http, pf.Task.{ Task }, pf.Stdin, pf.Stdout] + provides [main] to pf + +main : Task {} I32 +main = + _ <- Task.await (Stdout.line "Enter a URL to fetch. It must contain a scheme like \"http://\" or \"https://\".") + + input <- Task.await Stdin.line + + when input is + End -> + Stdout.line "I received end-of-input (EOF) instead of a URL." + + Input url -> + request = { + method: Get, + headers: [], + url, + body: Http.emptyBody, + timeout: NoTimeout, + } + + output <- Http.send request + |> Task.onErr \err -> err + |> Http.errorToString + |> Task.ok + |> Task.await + + Stdout.line output diff --git a/examples/cli/ingested-file-bytes.roc b/examples/cli/ingested-file-bytes.roc new file mode 100644 index 0000000000..5f89b21bfa --- /dev/null +++ b/examples/cli/ingested-file-bytes.roc @@ -0,0 +1,15 @@ +app "ingested-file-bytes" + packages { pf: "https://github.com/roc-lang/basic-cli/releases/download/0.7.0/bkGby8jb0tmZYsy2hg1E_B2QrCgcSTxdUlHtETwm5m4.tar.br" } + imports [ + pf.Stdout, + "ingested-file.roc" as ownCode : _, # A type hole can also be used here. + ] + provides [main] to pf + +main = + # Due to how ownCode is used, it will be a List U8. + ownCode + |> List.map Num.toNat + |> List.sum + |> Num.toStr + |> Stdout.line diff --git a/examples/cli/ingested-file.roc b/examples/cli/ingested-file.roc new file mode 100644 index 0000000000..8c79c1e22c --- /dev/null +++ b/examples/cli/ingested-file.roc @@ -0,0 +1,10 @@ +app "ingested-file" + packages { pf: "https://github.com/roc-lang/basic-cli/releases/download/0.7.0/bkGby8jb0tmZYsy2hg1E_B2QrCgcSTxdUlHtETwm5m4.tar.br" } + imports [ + pf.Stdout, + "ingested-file.roc" as ownCode : Str, + ] + provides [main] to pf + +main = + Stdout.line "\nThis roc file can print it's own source code. The source is:\n\n\(ownCode)" diff --git a/examples/cli/tui-platform/Program.roc b/examples/cli/tui-platform/Program.roc new file mode 100644 index 0000000000..6e8bffb5aa --- /dev/null +++ b/examples/cli/tui-platform/Program.roc @@ -0,0 +1,9 @@ +interface Program + exposes [Program] + imports [] + +Program model : { + init : {} -> model, + update : model, Str -> model, + view : model -> Str, +} diff --git a/examples/cli/tui-platform/host.zig b/examples/cli/tui-platform/host.zig new file mode 100644 index 0000000000..842b3a8833 --- /dev/null +++ b/examples/cli/tui-platform/host.zig @@ -0,0 +1,296 @@ +const std = @import("std"); +const builtin = @import("builtin"); +const str = @import("glue").str; +const RocStr = str.RocStr; +const testing = std.testing; +const expectEqual = testing.expectEqual; +const expect = testing.expect; +const maxInt = std.math.maxInt; + +const mem = std.mem; +const Allocator = mem.Allocator; + +const Program = extern struct { init: RocStr, update: Unit, view: Unit }; + +extern fn roc__mainForHost_1_exposed() Program; +extern fn roc__mainForHost_size() i64; + +const ConstModel = [*]const u8; +const MutModel = [*]u8; + +extern fn roc__mainForHost_0_caller([*]u8, [*]u8, MutModel) void; +extern fn roc__mainForHost_0_size() i64; +extern fn roc__mainForHost_0_result_size() i64; + +fn allocate_model(allocator: *Allocator) MutModel { + const size = roc__mainForHost_0_result_size(); + const raw_output = allocator.alignedAlloc(u8, @alignOf(u64), @as(usize, @intCast(size))) catch unreachable; + var output = @as([*]u8, @ptrCast(raw_output)); + + return output; +} + +fn init(allocator: *Allocator) ConstModel { + const closure: [*]u8 = undefined; + const output = allocate_model(allocator); + + roc__mainForHost_0_caller(closure, closure, output); + + return output; +} + +extern fn roc__mainForHost_1_caller(ConstModel, *const RocStr, [*]u8, MutModel) void; +extern fn roc__mainForHost_1_size() i64; +extern fn roc__mainForHost_1_result_size() i64; + +fn update(allocator: *Allocator, model: ConstModel, msg: RocStr) ConstModel { + const closure: [*]u8 = undefined; + const output = allocate_model(allocator); + + roc__mainForHost_1_caller(model, &msg, closure, output); + + return output; +} + +extern fn roc__mainForHost_2_caller(ConstModel, [*]u8, *RocStr) void; +extern fn roc__mainForHost_2_size() i64; +extern fn roc__mainForHost_2_result_size() i64; + +fn view(input: ConstModel) RocStr { + const closure: [*]u8 = undefined; + var output: RocStr = undefined; + + roc__mainForHost_2_caller(input, closure, &output); + + return output; +} + +fn print_output(viewed: RocStr) void { + const stdout = std.io.getStdOut().writer(); + + for (viewed.asSlice()) |char| { + stdout.print("{c}", .{char}) catch unreachable; + } + + stdout.print("\n", .{}) catch unreachable; +} + +const Align = 2 * @alignOf(usize); +extern fn malloc(size: usize) callconv(.C) ?*align(Align) anyopaque; +extern fn realloc(c_ptr: [*]align(Align) u8, size: usize) callconv(.C) ?*anyopaque; +extern fn free(c_ptr: [*]align(Align) u8) callconv(.C) void; +extern fn memcpy(dst: [*]u8, src: [*]u8, size: usize) callconv(.C) void; +extern fn memset(dst: [*]u8, value: i32, size: usize) callconv(.C) void; + +const DEBUG: bool = false; + +export fn roc_alloc(size: usize, alignment: u32) callconv(.C) ?*anyopaque { + if (DEBUG) { + var ptr = malloc(size); + const stdout = std.io.getStdOut().writer(); + stdout.print("alloc: {d} (alignment {d}, size {d})\n", .{ ptr, alignment, size }) catch unreachable; + return ptr; + } else { + return malloc(size); + } +} + +export fn roc_realloc(c_ptr: *anyopaque, new_size: usize, old_size: usize, alignment: u32) callconv(.C) ?*anyopaque { + if (DEBUG) { + const stdout = std.io.getStdOut().writer(); + stdout.print("realloc: {d} (alignment {d}, old_size {d})\n", .{ c_ptr, alignment, old_size }) catch unreachable; + } + + return realloc(@as([*]align(Align) u8, @alignCast(@ptrCast(c_ptr))), new_size); +} + +export fn roc_dealloc(c_ptr: *anyopaque, alignment: u32) callconv(.C) void { + if (DEBUG) { + const stdout = std.io.getStdOut().writer(); + stdout.print("dealloc: {d} (alignment {d})\n", .{ c_ptr, alignment }) catch unreachable; + } + + free(@as([*]align(Align) u8, @alignCast(@ptrCast(c_ptr)))); +} + +export fn roc_panic(msg: *RocStr, tag_id: u32) callconv(.C) void { + const stderr = std.io.getStdErr().writer(); + switch (tag_id) { + 0 => { + stderr.print("Roc standard library crashed with message\n\n {s}\n\nShutting down\n", .{msg.asSlice()}) catch unreachable; + }, + 1 => { + stderr.print("Application crashed with message\n\n {s}\n\nShutting down\n", .{msg.asSlice()}) catch unreachable; + }, + else => unreachable, + } + std.process.exit(1); +} + +export fn roc_dbg(loc: *RocStr, msg: *RocStr) callconv(.C) void { + const stderr = std.io.getStdErr().writer(); + stderr.print("[{s}] {s}\n", .{ loc.asSlice(), msg.asSlice() }) catch unreachable; +} + +export fn roc_memset(dst: [*]u8, value: i32, size: usize) callconv(.C) void { + return memset(dst, value, size); +} + +extern fn kill(pid: c_int, sig: c_int) c_int; +extern fn shm_open(name: *const i8, oflag: c_int, mode: c_uint) c_int; +extern fn mmap(addr: ?*anyopaque, length: c_uint, prot: c_int, flags: c_int, fd: c_int, offset: c_uint) *anyopaque; +extern fn getppid() c_int; + +fn roc_getppid() callconv(.C) c_int { + return getppid(); +} + +fn roc_getppid_windows_stub() callconv(.C) c_int { + return 0; +} + +fn roc_shm_open(name: *const i8, oflag: c_int, mode: c_uint) callconv(.C) c_int { + return shm_open(name, oflag, mode); +} +fn roc_mmap(addr: ?*anyopaque, length: c_uint, prot: c_int, flags: c_int, fd: c_int, offset: c_uint) callconv(.C) *anyopaque { + return mmap(addr, length, prot, flags, fd, offset); +} + +comptime { + if (builtin.os.tag == .macos or builtin.os.tag == .linux) { + @export(roc_getppid, .{ .name = "roc_getppid", .linkage = .Strong }); + @export(roc_mmap, .{ .name = "roc_mmap", .linkage = .Strong }); + @export(roc_shm_open, .{ .name = "roc_shm_open", .linkage = .Strong }); + } + + if (builtin.os.tag == .windows) { + @export(roc_getppid_windows_stub, .{ .name = "roc_getppid", .linkage = .Strong }); + } +} + +const Unit = extern struct {}; + +pub export fn main() callconv(.C) u8 { + var timer = std.time.Timer.start() catch unreachable; + + const program = roc__mainForHost_1_exposed(); + + call_the_closure(program); + + const nanos = timer.read(); + const seconds = (@as(f64, @floatFromInt(nanos)) / 1_000_000_000.0); + + const stderr = std.io.getStdErr().writer(); + stderr.print("runtime: {d:.3}ms\n", .{seconds * 1000}) catch unreachable; + + return 0; +} + +fn to_seconds(tms: std.os.timespec) f64 { + return @as(f64, @floatFromInt(tms.tv_sec)) + (@as(f64, @floatFromInt(tms.tv_nsec)) / 1_000_000_000.0); +} + +fn call_the_closure(program: Program) void { + _ = program; + + var allocator = std.heap.page_allocator; + const stdin = std.io.getStdIn().reader(); + + var buf: [1000]u8 = undefined; + + var model = init(&allocator); + + while (true) { + print_output(view(model)); + + const line = (stdin.readUntilDelimiterOrEof(buf[0..], '\n') catch unreachable) orelse return; + + if (line.len == 1 and line[0] == 'q') { + return; + } + + const to_append = RocStr.init(line.ptr, line.len); + + model = update(&allocator, model, to_append); + } + + // The closure returns result, nothing interesting to do with it + return; +} + +pub export fn roc_fx_putInt(int: i64) i64 { + const stdout = std.io.getStdOut().writer(); + + stdout.print("{d}", .{int}) catch unreachable; + + stdout.print("\n", .{}) catch unreachable; + + return 0; +} + +export fn roc_fx_putLine(rocPath: str.RocStr) callconv(.C) void { + const stdout = std.io.getStdOut().writer(); + + for (rocPath.asSlice()) |char| { + stdout.print("{c}", .{char}) catch unreachable; + } + + stdout.print("\n", .{}) catch unreachable; +} + +const GetInt = extern struct { + value: i64, + error_code: bool, + is_error: bool, +}; + +comptime { + if (@sizeOf(usize) == 8) { + @export(roc_fx_getInt_64bit, .{ .name = "roc_fx_getInt" }); + } else { + @export(roc_fx_getInt_32bit, .{ .name = "roc_fx_getInt" }); + } +} + +fn roc_fx_getInt_64bit() callconv(.C) GetInt { + if (roc_fx_getInt_help()) |value| { + const get_int = GetInt{ .is_error = false, .value = value, .error_code = false }; + return get_int; + } else |err| switch (err) { + error.InvalidCharacter => { + return GetInt{ .is_error = true, .value = 0, .error_code = false }; + }, + else => { + return GetInt{ .is_error = true, .value = 0, .error_code = true }; + }, + } + + return 0; +} + +fn roc_fx_getInt_32bit(output: *GetInt) callconv(.C) void { + if (roc_fx_getInt_help()) |value| { + const get_int = GetInt{ .is_error = false, .value = value, .error_code = false }; + output.* = get_int; + } else |err| switch (err) { + error.InvalidCharacter => { + output.* = GetInt{ .is_error = true, .value = 0, .error_code = false }; + }, + else => { + output.* = GetInt{ .is_error = true, .value = 0, .error_code = true }; + }, + } + + return; +} + +fn roc_fx_getInt_help() !i64 { + const stdin = std.io.getStdIn().reader(); + var buf: [40]u8 = undefined; + + // make sure to strip `\r` on windows + const raw_line: []u8 = (try stdin.readUntilDelimiterOrEof(&buf, '\n')) orelse ""; + const line = std.mem.trimRight(u8, raw_line, &std.ascii.whitespace); + + return std.fmt.parseInt(i64, line, 10); +} diff --git a/examples/interactive/tui-platform/Package-Config.roc b/examples/cli/tui-platform/main.roc similarity index 100% rename from examples/interactive/tui-platform/Package-Config.roc rename to examples/cli/tui-platform/main.roc diff --git a/examples/interactive/tui.roc b/examples/cli/tui.roc similarity index 84% rename from examples/interactive/tui.roc rename to examples/cli/tui.roc index bc9b008080..a169dda0e5 100644 --- a/examples/interactive/tui.roc +++ b/examples/cli/tui.roc @@ -1,5 +1,5 @@ app "tui" - packages { pf: "tui-platform" } + packages { pf: "tui-platform/main.roc" } imports [pf.Program.{ Program }] provides [main] { Model } to pf diff --git a/examples/false-interpreter/Variable.roc b/examples/false-interpreter/Variable.roc deleted file mode 100644 index c44e3d73a2..0000000000 --- a/examples/false-interpreter/Variable.roc +++ /dev/null @@ -1,42 +0,0 @@ -interface Variable - exposes [Variable, fromUtf8, toIndex, totalCount, toStr] - imports [] - -# Variables in False can only be single letters. Thus, the valid variables are "a" to "z". -# This opaque type deals with ensure we always have valid variables. -Variable := U8 - -totalCount : Nat -totalCount = - 0x7A# "z" - - - 0x61# "a" - - + 1 - -toStr : Variable -> Str -toStr = \@Variable char -> - when Str.fromUtf8 [char] is - Ok str -> - str - _ -> - "_" - -fromUtf8 : U8 -> Result Variable [InvalidVariableUtf8] -fromUtf8 = \char -> - if - char - >= 0x61# "a" - - && char - <= 0x7A - # "z" - then - Ok (@Variable char) - else - Err InvalidVariableUtf8 - -toIndex : Variable -> Nat -toIndex = \@Variable char -> - Num.intCast (char - 0x61)# "a" -# List.first (Str.toUtf8 "a") diff --git a/examples/false-interpreter/platform/Cargo.lock b/examples/false-interpreter/platform/Cargo.lock deleted file mode 100644 index 9132862791..0000000000 --- a/examples/false-interpreter/platform/Cargo.lock +++ /dev/null @@ -1,30 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 3 - -[[package]] -name = "host" -version = "0.1.0" -dependencies = [ - "libc", - "roc_std", -] - -[[package]] -name = "libc" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1fa8cddc8fbbee11227ef194b5317ed014b8acbf15139bd716a18ad3fe99ec5" - -[[package]] -name = "roc_std" -version = "0.1.0" -dependencies = [ - "static_assertions", -] - -[[package]] -name = "static_assertions" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f406d6ee68db6796e11ffd7b4d171864c58b7451e79ef9460ea33c287a1f89a7" diff --git a/examples/false-interpreter/platform/Cargo.toml b/examples/false-interpreter/platform/Cargo.toml deleted file mode 100644 index 37d5561b6b..0000000000 --- a/examples/false-interpreter/platform/Cargo.toml +++ /dev/null @@ -1,23 +0,0 @@ -[package] -name = "host" -version = "0.1.0" -authors = ["The Roc Contributors"] -license = "UPL-1.0" -edition = "2021" - -links = "app" - -[lib] -name = "host" -path = "src/lib.rs" -crate-type = ["staticlib", "rlib"] - -[[bin]] -name = "host" -path = "src/main.rs" - -[dependencies] -roc_std = { path = "../../../roc_std" } -libc = "0.2" - -[workspace] diff --git a/examples/false-interpreter/platform/File.roc b/examples/false-interpreter/platform/File.roc deleted file mode 100644 index 22fc18811c..0000000000 --- a/examples/false-interpreter/platform/File.roc +++ /dev/null @@ -1,27 +0,0 @@ -interface File - exposes [line, Handle, withOpen, chunk] - imports [pf.Effect, Task.{ Task }] - -Handle := U64 - -line : Handle -> Task.Task Str * -line = \@Handle handle -> Effect.after (Effect.getFileLine handle) Task.succeed - -chunk : Handle -> Task.Task (List U8) * -chunk = \@Handle handle -> Effect.after (Effect.getFileBytes handle) Task.succeed - -open : Str -> Task.Task Handle * -open = \path -> - Effect.openFile path - |> Effect.map (\id -> @Handle id) - |> Effect.after Task.succeed - -close : Handle -> Task.Task {} * -close = \@Handle handle -> Effect.after (Effect.closeFile handle) Task.succeed - -withOpen : Str, (Handle -> Task {} a) -> Task {} a -withOpen = \path, callback -> - handle <- Task.await (open path) - result <- Task.attempt (callback handle) - {} <- Task.await (close handle) - Task.fromResult result diff --git a/examples/false-interpreter/platform/Package-Config.roc b/examples/false-interpreter/platform/Package-Config.roc deleted file mode 100644 index 2d737816a5..0000000000 --- a/examples/false-interpreter/platform/Package-Config.roc +++ /dev/null @@ -1,9 +0,0 @@ -platform "false-interpreter" - requires {} { main : Str -> Task {} [] } - exposes [] - packages {} - imports [Task.{ Task }] - provides [mainForHost] - -mainForHost : Str -> Task {} [] as Fx -mainForHost = \file -> main file diff --git a/examples/false-interpreter/platform/Stdin.roc b/examples/false-interpreter/platform/Stdin.roc deleted file mode 100644 index f4ac162755..0000000000 --- a/examples/false-interpreter/platform/Stdin.roc +++ /dev/null @@ -1,8 +0,0 @@ -interface Stdin - exposes [char] - imports [pf.Effect, Task] - -# line : Task.Task Str * -# line = Effect.after Effect.getLine Task.succeed # TODO FIXME Effect.getLine should suffice -char : Task.Task U8 * -char = Effect.after Effect.getChar Task.succeed# TODO FIXME Effect.getLine should suffice diff --git a/examples/false-interpreter/platform/Task.roc b/examples/false-interpreter/platform/Task.roc deleted file mode 100644 index 2e9cfc27a0..0000000000 --- a/examples/false-interpreter/platform/Task.roc +++ /dev/null @@ -1,81 +0,0 @@ -interface Task - exposes [Task, succeed, fail, await, map, onFail, attempt, fromResult, loop] - imports [pf.Effect] - -Task ok err : Effect.Effect (Result ok err) - -loop : state, (state -> Task [Step state, Done done] err) -> Task done err -loop = \state, step -> - looper = \current -> - step current - |> Effect.map - \res -> - when res is - Ok (Step newState) -> - Step newState - Ok (Done result) -> - Done (Ok result) - Err e -> - Done (Err e) - - Effect.loop state looper - -succeed : val -> Task val * -succeed = \val -> - Effect.always (Ok val) - -fail : err -> Task * err -fail = \val -> - Effect.always (Err val) - -fromResult : Result a e -> Task a e -fromResult = \result -> - when result is - Ok a -> - succeed a - Err e -> - fail e - -attempt : Task a b, (Result a b -> Task c d) -> Task c d -attempt = \effect, transform -> - Effect.after - effect - \result -> - when result is - Ok ok -> - transform (Ok ok) - Err err -> - transform (Err err) - -await : Task a err, (a -> Task b err) -> Task b err -await = \effect, transform -> - Effect.after - effect - \result -> - when result is - Ok a -> - transform a - Err err -> - Task.fail err - -onFail : Task ok a, (a -> Task ok b) -> Task ok b -onFail = \effect, transform -> - Effect.after - effect - \result -> - when result is - Ok a -> - Task.succeed a - Err err -> - transform err - -map : Task a err, (a -> b) -> Task b err -map = \effect, transform -> - Effect.after - effect - \result -> - when result is - Ok a -> - Task.succeed (transform a) - Err err -> - Task.fail err diff --git a/examples/false-interpreter/platform/build.rs b/examples/false-interpreter/platform/build.rs deleted file mode 100644 index 73159e387c..0000000000 --- a/examples/false-interpreter/platform/build.rs +++ /dev/null @@ -1,4 +0,0 @@ -fn main() { - println!("cargo:rustc-link-lib=dylib=app"); - println!("cargo:rustc-link-search=."); -} diff --git a/examples/false-interpreter/platform/src/lib.rs b/examples/false-interpreter/platform/src/lib.rs deleted file mode 100644 index 67c2dbe26d..0000000000 --- a/examples/false-interpreter/platform/src/lib.rs +++ /dev/null @@ -1,218 +0,0 @@ -#![allow(non_snake_case)] - -use core::alloc::Layout; -use core::ffi::c_void; -use core::mem::{ManuallyDrop, MaybeUninit}; -use libc; -use roc_std::{RocList, RocStr}; -use std::env; -use std::ffi::CStr; -use std::fs::File; -use std::io::{BufRead, BufReader, Read, Write}; -use std::os::raw::c_char; - -extern "C" { - #[link_name = "roc__mainForHost_1_exposed_generic"] - fn roc_main(output: *mut u8, args: &RocStr); - - #[link_name = "roc__mainForHost_size"] - fn roc_main_size() -> i64; - - #[link_name = "roc__mainForHost_1_Fx_caller"] - fn call_Fx(flags: *const u8, closure_data: *const u8, output: *mut u8); - - #[allow(dead_code)] - #[link_name = "roc__mainForHost_1_Fx_size"] - fn size_Fx() -> i64; - - #[link_name = "roc__mainForHost_1_Fx_result_size"] - fn size_Fx_result() -> i64; -} - -#[no_mangle] -pub unsafe extern "C" fn roc_alloc(size: usize, _alignment: u32) -> *mut c_void { - libc::malloc(size) -} - -#[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) -} - -#[no_mangle] -pub unsafe extern "C" fn roc_dealloc(c_ptr: *mut c_void, _alignment: u32) { - libc::free(c_ptr) -} - -#[no_mangle] -pub unsafe extern "C" fn roc_panic(c_ptr: *mut c_void, tag_id: u32) { - match tag_id { - 0 => { - let slice = CStr::from_ptr(c_ptr as *const c_char); - let string = slice.to_str().unwrap(); - eprintln!("Roc hit a panic: {}", string); - std::process::exit(1); - } - _ => todo!(), - } -} - -#[no_mangle] -pub unsafe extern "C" fn roc_memcpy(dst: *mut c_void, src: *mut c_void, n: usize) -> *mut c_void { - libc::memcpy(dst, src, n) -} - -#[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) -} - -#[no_mangle] -pub extern "C" fn rust_main() -> i32 { - let arg = env::args() - .nth(1) - .expect("Please pass a .false file as a command-line argument to the false interpreter!"); - let arg = RocStr::from(arg.as_str()); - - let size = unsafe { roc_main_size() } as usize; - let layout = Layout::array::(size).unwrap(); - - unsafe { - // TODO allocate on the stack if it's under a certain size - let buffer = std::alloc::alloc(layout); - - roc_main(buffer, &arg); - - // arg has been passed to roc now, and it assumes ownership. - // so we must not touch its refcount now - std::mem::forget(arg); - - let result = call_the_closure(buffer); - - std::alloc::dealloc(buffer, layout); - - result - }; - - // Exit code - 0 -} - -unsafe fn call_the_closure(closure_data_ptr: *const u8) -> i64 { - let size = size_Fx_result() as usize; - let layout = Layout::array::(size).unwrap(); - let buffer = std::alloc::alloc(layout) as *mut u8; - - call_Fx( - // This flags pointer will never get dereferenced - MaybeUninit::uninit().as_ptr(), - closure_data_ptr as *const u8, - buffer as *mut u8, - ); - - std::alloc::dealloc(buffer, layout); - - 0 -} - -#[no_mangle] -pub extern "C" fn roc_fx_getLine() -> RocStr { - use std::io::{self, BufRead}; - - let stdin = io::stdin(); - let line1 = stdin.lock().lines().next().unwrap().unwrap(); - - RocStr::from(line1.as_str()) -} - -#[no_mangle] -pub extern "C" fn roc_fx_getChar() -> u8 { - use std::io::{self, BufRead}; - let mut buffer = [0]; - - if let Err(ioerr) = io::stdin().lock().read_exact(&mut buffer[..]) { - if ioerr.kind() == io::ErrorKind::UnexpectedEof { - u8::MAX - } else { - panic!("Got an unexpected error while reading char from stdin"); - } - } else { - buffer[0] - } -} - -#[no_mangle] -pub extern "C" fn roc_fx_putLine(line: &RocStr) { - let string = line.as_str(); - println!("{}", string); - std::io::stdout().lock().flush(); -} - -#[no_mangle] -pub extern "C" fn roc_fx_putRaw(line: &RocStr) { - let string = line.as_str(); - print!("{}", string); - std::io::stdout().lock().flush(); -} - -#[no_mangle] -pub extern "C" fn roc_fx_getFileLine(br_ptr: *mut BufReader) -> RocStr { - let br = unsafe { &mut *br_ptr }; - let mut line1 = String::default(); - - br.read_line(&mut line1) - .expect("Failed to read line from file"); - - RocStr::from(line1.as_str()) -} - -#[no_mangle] -pub extern "C" fn roc_fx_getFileBytes(br_ptr: *mut BufReader) -> RocList { - let br = unsafe { &mut *br_ptr }; - let mut buffer = [0; 0x10 /* This is intentionally small to ensure correct implementation */]; - - let count = br - .read(&mut buffer[..]) - .expect("Failed to read bytes from file"); - - RocList::from_slice(&buffer[..count]) -} - -#[no_mangle] -pub extern "C" fn roc_fx_closeFile(br_ptr: *mut BufReader) { - unsafe { - Box::from_raw(br_ptr); - } -} - -#[no_mangle] -pub extern "C" fn roc_fx_openFile(name: &RocStr) -> *mut BufReader { - let string = name.as_str(); - match File::open(string) { - Ok(f) => { - let br = BufReader::new(f); - - Box::into_raw(Box::new(br)) - } - Err(_) => { - panic!("unable to open file {:?}", name) - } - } -} - -#[no_mangle] -pub extern "C" fn roc_fx_withFileOpen(name: &RocStr, buffer: *const u8) { - // TODO: figure out accepting a closure in an fx and passing data to it. - // let f = File::open(name.as_str()).expect("Unable to open file"); - // let mut br = BufReader::new(f); - - // unsafe { - // let closure_data_ptr = buffer.offset(8); - // call_the_closure(closure_data_ptr); - // } -} diff --git a/examples/false-interpreter/platform/src/main.rs b/examples/false-interpreter/platform/src/main.rs deleted file mode 100644 index 51175f934b..0000000000 --- a/examples/false-interpreter/platform/src/main.rs +++ /dev/null @@ -1,3 +0,0 @@ -fn main() { - std::process::exit(host::rust_main()); -} diff --git a/examples/glue/glue.roc b/examples/glue/glue.roc new file mode 100644 index 0000000000..f6722e32f8 --- /dev/null +++ b/examples/glue/glue.roc @@ -0,0 +1,10 @@ +app "rocLovesRust" + packages { pf: "rust-platform/main.roc" } + imports [] + provides [main] to pf + +main = + msg = "Roc <3 Rust, also on stderr!\n" + StdoutWrite "Roc <3 Rust!\n" \{} -> + StderrWrite msg \{} -> + Done diff --git a/examples/glue/rust-platform/.cargo/config b/examples/glue/rust-platform/.cargo/config new file mode 100644 index 0000000000..91cf6cf3f9 --- /dev/null +++ b/examples/glue/rust-platform/.cargo/config @@ -0,0 +1,2 @@ +[target.x86_64-pc-windows-gnu] +linker = "zig-cc" diff --git a/examples/glue/rust-platform/Cargo.lock b/examples/glue/rust-platform/Cargo.lock new file mode 100644 index 0000000000..e338549811 --- /dev/null +++ b/examples/glue/rust-platform/Cargo.lock @@ -0,0 +1,37 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "arrayvec" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6" + +[[package]] +name = "host" +version = "0.0.1" +dependencies = [ + "libc", + "roc_std", +] + +[[package]] +name = "libc" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56d855069fafbb9b344c0f962150cd2c1187975cb1c22c1522c240d8c4986714" + +[[package]] +name = "roc_std" +version = "0.0.1" +dependencies = [ + "arrayvec", + "static_assertions", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" diff --git a/examples/glue/rust-platform/Cargo.toml b/examples/glue/rust-platform/Cargo.toml new file mode 100644 index 0000000000..3cfadfba58 --- /dev/null +++ b/examples/glue/rust-platform/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "host" +version = "0.0.1" +authors = ["The Roc Contributors"] +license = "UPL-1.0" +edition = "2021" +links = "app" + +[lib] +name = "host" +path = "src/lib.rs" +crate-type = ["staticlib", "rlib"] + +[[bin]] +name = "host" +path = "src/main.rs" + +[dependencies] +roc_app = { path = "roc_app" } +roc_std = { path = "roc_std" } +libc = "0.2" + +[workspace] diff --git a/examples/glue/rust-platform/build.rs b/examples/glue/rust-platform/build.rs new file mode 100644 index 0000000000..47763b34c3 --- /dev/null +++ b/examples/glue/rust-platform/build.rs @@ -0,0 +1,9 @@ +fn main() { + #[cfg(not(windows))] + println!("cargo:rustc-link-lib=dylib=app"); + + #[cfg(windows)] + println!("cargo:rustc-link-lib=dylib=libapp"); + + println!("cargo:rustc-link-search=."); +} diff --git a/examples/breakout/platform/host.c b/examples/glue/rust-platform/host.c similarity index 100% rename from examples/breakout/platform/host.c rename to examples/glue/rust-platform/host.c diff --git a/examples/glue/rust-platform/main.roc b/examples/glue/rust-platform/main.roc new file mode 100644 index 0000000000..47017d926c --- /dev/null +++ b/examples/glue/rust-platform/main.roc @@ -0,0 +1,17 @@ +platform "echo-in-rust" + requires {} { main : _ } + exposes [] + packages {} + imports [] + provides [mainForHost] + +# mainForHost : [StdoutWrite Str (({} -> Op) as Fx0), StderrWrite Str (({} -> Op) as Fx1), Done] as Op +mainForHost : [StdoutWrite Str ({} -> Op), StderrWrite Str ({} -> Op), Done] as Op +mainForHost = main + +# mainForHost : { x: Str, y: {} -> Str } +# mainForHost = +# y = "foo" +# +# when main is +# _ -> { x: "bar", y: \{} -> y } diff --git a/examples/glue/rust-platform/rocLovesRust b/examples/glue/rust-platform/rocLovesRust new file mode 100755 index 0000000000..f06f59fc33 Binary files /dev/null and b/examples/glue/rust-platform/rocLovesRust differ diff --git a/examples/glue/rust-platform/rust-toolchain.toml b/examples/glue/rust-platform/rust-toolchain.toml new file mode 100644 index 0000000000..cc6d7e3f5d --- /dev/null +++ b/examples/glue/rust-platform/rust-toolchain.toml @@ -0,0 +1,9 @@ +[toolchain] +channel = "1.71.1" + +profile = "default" + +components = [ + # for usages of rust-analyzer or similar tools inside `nix develop` + "rust-src" +] \ No newline at end of file diff --git a/examples/glue/rust-platform/src/glue.rs b/examples/glue/rust-platform/src/glue.rs new file mode 100644 index 0000000000..e3e2e524b7 --- /dev/null +++ b/examples/glue/rust-platform/src/glue.rs @@ -0,0 +1,744 @@ +// ⚠️ GENERATED CODE ⚠️ - this entire file was generated by the `roc glue` CLI command + +#![allow(unused_unsafe)] +#![allow(unused_variables)] +#![allow(dead_code)] +#![allow(unused_mut)] +#![allow(non_snake_case)] +#![allow(non_camel_case_types)] +#![allow(non_upper_case_globals)] +#![allow(clippy::undocumented_unsafe_blocks)] +#![allow(clippy::redundant_static_lifetimes)] +#![allow(clippy::unused_unit)] +#![allow(clippy::missing_safety_doc)] +#![allow(clippy::let_and_return)] +#![allow(clippy::missing_safety_doc)] +#![allow(clippy::redundant_static_lifetimes)] +#![allow(clippy::needless_borrow)] +#![allow(clippy::clone_on_copy)] + +type Op_StderrWrite = roc_std::RocStr; +type Op_StdoutWrite = roc_std::RocStr; +type TODO_roc_function_69 = roc_std::RocStr; +type TODO_roc_function_70 = roc_std::RocStr; + +#[cfg(any( + target_arch = "arm", + target_arch = "aarch64", + target_arch = "wasm32", + target_arch = "x86", + target_arch = "x86_64" +))] +#[derive(Clone, Copy, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[repr(u8)] +pub enum discriminant_Op { + Done = 0, + StderrWrite = 1, + StdoutWrite = 2, +} + +impl core::fmt::Debug for discriminant_Op { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Self::Done => f.write_str("discriminant_Op::Done"), + Self::StderrWrite => f.write_str("discriminant_Op::StderrWrite"), + Self::StdoutWrite => f.write_str("discriminant_Op::StdoutWrite"), + } + } +} + +#[cfg(any( + target_arch = "arm", + target_arch = "aarch64", + target_arch = "wasm32", + target_arch = "x86", + target_arch = "x86_64" +))] +#[repr(transparent)] +pub struct Op { + pointer: *mut union_Op, +} + +#[cfg(any( + target_arch = "arm", + target_arch = "aarch64", + target_arch = "wasm32", + target_arch = "x86", + target_arch = "x86_64" +))] +#[repr(C)] +union union_Op { + StderrWrite: core::mem::ManuallyDrop, + StdoutWrite: core::mem::ManuallyDrop, + _sizer: [u8; 8], +} + +#[cfg(any( + target_arch = "arm", + target_arch = "arm", + target_arch = "aarch64", + target_arch = "aarch64", + target_arch = "wasm32", + target_arch = "wasm32", + target_arch = "x86", + target_arch = "x86", + target_arch = "x86_64", + target_arch = "x86_64" +))] +//TODO HAS CLOSURE 2 +#[cfg(any( + target_arch = "arm", + target_arch = "aarch64", + target_arch = "wasm32", + target_arch = "x86", + target_arch = "x86_64" +))] +#[repr(C)] +pub struct RocFunction_66 { + pub closure_data: roc_std::RocList, +} + +impl RocFunction_66 { + pub fn force_thunk(mut self, arg_0: ()) -> Op { + extern "C" { + fn roc__mainForHost_0_caller(arg_0: &(), closure_data: *mut u8, output: *mut Op); + } + + let mut output = std::mem::MaybeUninit::uninit(); + let ptr = self.closure_data.as_mut_ptr(); + unsafe { roc__mainForHost_0_caller(&arg_0, ptr, output.as_mut_ptr()) }; + unsafe { output.assume_init() } + } +} + +#[cfg(any( + target_arch = "arm", + target_arch = "aarch64", + target_arch = "wasm32", + target_arch = "x86", + target_arch = "x86_64" +))] +#[repr(C)] +pub struct RocFunction_67 { + pub closure_data: roc_std::RocList, +} + +impl RocFunction_67 { + pub fn force_thunk(mut self, arg_0: ()) -> Op { + extern "C" { + fn roc__mainForHost_1_caller(arg_0: &(), closure_data: *mut u8, output: *mut Op); + } + + let mut output = std::mem::MaybeUninit::uninit(); + let ptr = self.closure_data.as_mut_ptr(); + unsafe { roc__mainForHost_1_caller(&arg_0, ptr, output.as_mut_ptr()) }; + unsafe { output.assume_init() } + } +} + +impl Op { + #[cfg(any( + target_arch = "arm", + target_arch = "aarch64", + target_arch = "wasm32", + target_arch = "x86", + target_arch = "x86_64" + ))] + #[inline(always)] + fn storage(&self) -> Option<&core::cell::Cell> { + let mask = match std::mem::size_of::() { + 4 => 0b11, + 8 => 0b111, + _ => unreachable!(), + }; + + // NOTE: pointer provenance is probably lost here + let unmasked_address = (self.pointer as usize) & !mask; + let untagged = unmasked_address as *const core::cell::Cell; + + if untagged.is_null() { + None + } else { + unsafe { Some(&*untagged.sub(1)) } + } + } + + #[cfg(any(target_arch = "arm", target_arch = "wasm32", target_arch = "x86"))] + /// Returns which variant this tag union holds. Note that this never includes a payload! + pub fn discriminant(&self) -> discriminant_Op { + // The discriminant is stored in the unused bytes at the end of the recursive pointer + unsafe { core::mem::transmute::((self.pointer as u8) & 0b11) } + } + + #[cfg(any(target_arch = "arm", target_arch = "wasm32", target_arch = "x86"))] + /// Internal helper + fn tag_discriminant(pointer: *mut union_Op, discriminant: discriminant_Op) -> *mut union_Op { + // The discriminant is stored in the unused bytes at the end of the union pointer + let untagged = (pointer as usize) & (!0b11 as usize); + let tagged = untagged | (discriminant as usize); + + tagged as *mut union_Op + } + + #[cfg(any(target_arch = "arm", target_arch = "wasm32", target_arch = "x86"))] + /// Internal helper + fn union_pointer(&self) -> *mut union_Op { + // The discriminant is stored in the unused bytes at the end of the union pointer + ((self.pointer as usize) & (!0b11 as usize)) as *mut union_Op + } + + #[cfg(any( + target_arch = "arm", + target_arch = "aarch64", + target_arch = "wasm32", + target_arch = "x86", + target_arch = "x86_64" + ))] + /// A tag named Done, which has no payload. + pub const Done: Self = Self { + pointer: core::ptr::null_mut(), + }; + + #[cfg(any( + target_arch = "arm", + target_arch = "aarch64", + target_arch = "wasm32", + target_arch = "x86", + target_arch = "x86_64" + ))] + /// Unsafely assume this `Op` has a `.discriminant()` of `StderrWrite` and return its payload at index 0. + /// (Always examine `.discriminant()` first to make sure this is the correct variant!) + /// Panics in debug builds if the `.discriminant()` doesn't return `StderrWrite`. + pub unsafe fn get_StderrWrite_0(&self) -> roc_std::RocStr { + debug_assert_eq!(self.discriminant(), discriminant_Op::StderrWrite); + + extern "C" { + #[link_name = "roc__getter__2_generic"] + fn getter(_: *mut roc_std::RocStr, _: *const Op); + } + + let mut ret = core::mem::MaybeUninit::uninit(); + getter(ret.as_mut_ptr(), self); + ret.assume_init() + } + + #[cfg(any( + target_arch = "arm", + target_arch = "aarch64", + target_arch = "wasm32", + target_arch = "x86", + target_arch = "x86_64" + ))] + /// Unsafely assume this `Op` has a `.discriminant()` of `StderrWrite` and return its payload at index 1. + /// (Always examine `.discriminant()` first to make sure this is the correct variant!) + /// Panics in debug builds if the `.discriminant()` doesn't return `StderrWrite`. + pub unsafe fn get_StderrWrite_1(&self) -> RocFunction_67 { + debug_assert_eq!(self.discriminant(), discriminant_Op::StderrWrite); + + extern "C" { + #[link_name = "roc__getter__3_size"] + fn size() -> usize; + + #[link_name = "roc__getter__3_generic"] + fn getter(_: *mut u8, _: *const Op); + } + + // allocate memory to store this variably-sized value + // allocates with roc_alloc, but that likely still uses the heap + let it = std::iter::repeat(0xAAu8).take(size()); + let mut bytes = roc_std::RocList::from_iter(it); + + getter(bytes.as_mut_ptr(), self); + + RocFunction_67 { + closure_data: bytes, + } + } + + #[cfg(any( + target_arch = "arm", + target_arch = "aarch64", + target_arch = "wasm32", + target_arch = "x86", + target_arch = "x86_64" + ))] + /// Construct a tag named `StderrWrite`, with the appropriate payload + pub fn StderrWrite(arg: Op_StderrWrite) -> Self { + let size = core::mem::size_of::(); + let align = core::mem::align_of::() as u32; + + unsafe { + let ptr = roc_std::roc_alloc_refcounted::(); + + *ptr = union_Op { + StderrWrite: core::mem::ManuallyDrop::new(arg), + }; + + Self { + pointer: Self::tag_discriminant(ptr, discriminant_Op::StderrWrite), + } + } + } + + #[cfg(any(target_arch = "arm", target_arch = "wasm32", target_arch = "x86"))] + /// Unsafely assume this `Op` has a `.discriminant()` of `StderrWrite` and convert it to `StderrWrite`'s payload. + /// (Always examine `.discriminant()` first to make sure this is the correct variant!) + /// Panics in debug builds if the `.discriminant()` doesn't return `StderrWrite`. + pub unsafe fn into_StderrWrite(mut self) -> Op_StderrWrite { + debug_assert_eq!(self.discriminant(), discriminant_Op::StderrWrite); + let payload = { + let ptr = (self.pointer as usize & !0b11) as *mut union_Op; + let mut uninitialized = core::mem::MaybeUninit::uninit(); + let swapped = unsafe { + core::mem::replace( + &mut (*ptr).StderrWrite, + core::mem::ManuallyDrop::new(uninitialized.assume_init()), + ) + }; + + core::mem::forget(self); + + core::mem::ManuallyDrop::into_inner(swapped) + }; + + payload + } + + #[cfg(any(target_arch = "arm", target_arch = "wasm32", target_arch = "x86"))] + /// Unsafely assume this `Op` has a `.discriminant()` of `StderrWrite` and return its payload. + /// (Always examine `.discriminant()` first to make sure this is the correct variant!) + /// Panics in debug builds if the `.discriminant()` doesn't return `StderrWrite`. + pub unsafe fn as_StderrWrite(&self) -> &Op_StderrWrite { + debug_assert_eq!(self.discriminant(), discriminant_Op::StderrWrite); + let payload = { + let ptr = (self.pointer as usize & !0b11) as *mut union_Op; + + unsafe { &(*ptr).StderrWrite } + }; + + &payload + } + + #[cfg(any( + target_arch = "arm", + target_arch = "aarch64", + target_arch = "wasm32", + target_arch = "x86", + target_arch = "x86_64" + ))] + /// Unsafely assume this `Op` has a `.discriminant()` of `StdoutWrite` and return its payload at index 0. + /// (Always examine `.discriminant()` first to make sure this is the correct variant!) + /// Panics in debug builds if the `.discriminant()` doesn't return `StdoutWrite`. + pub unsafe fn get_StdoutWrite_0(&self) -> roc_std::RocStr { + debug_assert_eq!(self.discriminant(), discriminant_Op::StdoutWrite); + + extern "C" { + #[link_name = "roc__getter__2_generic"] + fn getter(_: *mut roc_std::RocStr, _: *const Op); + } + + let mut ret = core::mem::MaybeUninit::uninit(); + getter(ret.as_mut_ptr(), self); + ret.assume_init() + } + + #[cfg(any( + target_arch = "arm", + target_arch = "aarch64", + target_arch = "wasm32", + target_arch = "x86", + target_arch = "x86_64" + ))] + /// Unsafely assume this `Op` has a `.discriminant()` of `StdoutWrite` and return its payload at index 1. + /// (Always examine `.discriminant()` first to make sure this is the correct variant!) + /// Panics in debug builds if the `.discriminant()` doesn't return `StdoutWrite`. + pub unsafe fn get_StdoutWrite_1(&self) -> RocFunction_66 { + debug_assert_eq!(self.discriminant(), discriminant_Op::StdoutWrite); + + extern "C" { + #[link_name = "roc__getter__3_size"] + fn size() -> usize; + + #[link_name = "roc__getter__3_generic"] + fn getter(_: *mut u8, _: *const Op); + } + + // allocate memory to store this variably-sized value + // allocates with roc_alloc, but that likely still uses the heap + let it = std::iter::repeat(0xAAu8).take(size()); + let mut bytes = roc_std::RocList::from_iter(it); + + getter(bytes.as_mut_ptr(), self); + + RocFunction_66 { + closure_data: bytes, + } + } + + #[cfg(any( + target_arch = "arm", + target_arch = "aarch64", + target_arch = "wasm32", + target_arch = "x86", + target_arch = "x86_64" + ))] + /// Construct a tag named `StdoutWrite`, with the appropriate payload + pub fn StdoutWrite(arg: Op_StdoutWrite) -> Self { + let size = core::mem::size_of::(); + let align = core::mem::align_of::() as u32; + + unsafe { + let ptr = roc_std::roc_alloc_refcounted::(); + + *ptr = union_Op { + StdoutWrite: core::mem::ManuallyDrop::new(arg), + }; + + Self { + pointer: Self::tag_discriminant(ptr, discriminant_Op::StdoutWrite), + } + } + } + + #[cfg(any(target_arch = "arm", target_arch = "wasm32", target_arch = "x86"))] + /// Unsafely assume this `Op` has a `.discriminant()` of `StdoutWrite` and convert it to `StdoutWrite`'s payload. + /// (Always examine `.discriminant()` first to make sure this is the correct variant!) + /// Panics in debug builds if the `.discriminant()` doesn't return `StdoutWrite`. + pub unsafe fn into_StdoutWrite(mut self) -> Op_StdoutWrite { + debug_assert_eq!(self.discriminant(), discriminant_Op::StdoutWrite); + let payload = { + let ptr = (self.pointer as usize & !0b11) as *mut union_Op; + let mut uninitialized = core::mem::MaybeUninit::uninit(); + let swapped = unsafe { + core::mem::replace( + &mut (*ptr).StdoutWrite, + core::mem::ManuallyDrop::new(uninitialized.assume_init()), + ) + }; + + core::mem::forget(self); + + core::mem::ManuallyDrop::into_inner(swapped) + }; + + payload + } + + #[cfg(any(target_arch = "arm", target_arch = "wasm32", target_arch = "x86"))] + /// Unsafely assume this `Op` has a `.discriminant()` of `StdoutWrite` and return its payload. + /// (Always examine `.discriminant()` first to make sure this is the correct variant!) + /// Panics in debug builds if the `.discriminant()` doesn't return `StdoutWrite`. + pub unsafe fn as_StdoutWrite(&self) -> &Op_StdoutWrite { + debug_assert_eq!(self.discriminant(), discriminant_Op::StdoutWrite); + let payload = { + let ptr = (self.pointer as usize & !0b11) as *mut union_Op; + + unsafe { &(*ptr).StdoutWrite } + }; + + &payload + } + + #[cfg(any(target_arch = "aarch64", target_arch = "x86_64"))] + /// Returns which variant this tag union holds. Note that this never includes a payload! + pub fn discriminant(&self) -> discriminant_Op { + // The discriminant is stored in the unused bytes at the end of the recursive pointer + unsafe { core::mem::transmute::((self.pointer as u8) & 0b111) } + } + + #[cfg(any(target_arch = "aarch64", target_arch = "x86_64"))] + /// Internal helper + fn tag_discriminant(pointer: *mut union_Op, discriminant: discriminant_Op) -> *mut union_Op { + // The discriminant is stored in the unused bytes at the end of the union pointer + let untagged = (pointer as usize) & (!0b111 as usize); + let tagged = untagged | (discriminant as usize); + + tagged as *mut union_Op + } + + #[cfg(any(target_arch = "aarch64", target_arch = "x86_64"))] + /// Internal helper + fn union_pointer(&self) -> *mut union_Op { + // The discriminant is stored in the unused bytes at the end of the union pointer + ((self.pointer as usize) & (!0b111 as usize)) as *mut union_Op + } + + #[cfg(any(target_arch = "aarch64", target_arch = "x86_64"))] + /// Unsafely assume this `Op` has a `.discriminant()` of `StderrWrite` and convert it to `StderrWrite`'s payload. + /// (Always examine `.discriminant()` first to make sure this is the correct variant!) + /// Panics in debug builds if the `.discriminant()` doesn't return `StderrWrite`. + pub unsafe fn into_StderrWrite(mut self) -> Op_StderrWrite { + debug_assert_eq!(self.discriminant(), discriminant_Op::StderrWrite); + let payload = { + let ptr = (self.pointer as usize & !0b111) as *mut union_Op; + let mut uninitialized = core::mem::MaybeUninit::uninit(); + let swapped = unsafe { + core::mem::replace( + &mut (*ptr).StderrWrite, + core::mem::ManuallyDrop::new(uninitialized.assume_init()), + ) + }; + + core::mem::forget(self); + + core::mem::ManuallyDrop::into_inner(swapped) + }; + + payload + } + + #[cfg(any(target_arch = "aarch64", target_arch = "x86_64"))] + /// Unsafely assume this `Op` has a `.discriminant()` of `StderrWrite` and return its payload. + /// (Always examine `.discriminant()` first to make sure this is the correct variant!) + /// Panics in debug builds if the `.discriminant()` doesn't return `StderrWrite`. + pub unsafe fn as_StderrWrite(&self) -> &Op_StderrWrite { + debug_assert_eq!(self.discriminant(), discriminant_Op::StderrWrite); + let payload = { + let ptr = (self.pointer as usize & !0b111) as *mut union_Op; + + unsafe { &(*ptr).StderrWrite } + }; + + &payload + } + + #[cfg(any(target_arch = "aarch64", target_arch = "x86_64"))] + /// Unsafely assume this `Op` has a `.discriminant()` of `StdoutWrite` and convert it to `StdoutWrite`'s payload. + /// (Always examine `.discriminant()` first to make sure this is the correct variant!) + /// Panics in debug builds if the `.discriminant()` doesn't return `StdoutWrite`. + pub unsafe fn into_StdoutWrite(mut self) -> Op_StdoutWrite { + debug_assert_eq!(self.discriminant(), discriminant_Op::StdoutWrite); + let payload = { + let ptr = (self.pointer as usize & !0b111) as *mut union_Op; + let mut uninitialized = core::mem::MaybeUninit::uninit(); + let swapped = unsafe { + core::mem::replace( + &mut (*ptr).StdoutWrite, + core::mem::ManuallyDrop::new(uninitialized.assume_init()), + ) + }; + + core::mem::forget(self); + + core::mem::ManuallyDrop::into_inner(swapped) + }; + + payload + } + + #[cfg(any(target_arch = "aarch64", target_arch = "x86_64"))] + /// Unsafely assume this `Op` has a `.discriminant()` of `StdoutWrite` and return its payload. + /// (Always examine `.discriminant()` first to make sure this is the correct variant!) + /// Panics in debug builds if the `.discriminant()` doesn't return `StdoutWrite`. + pub unsafe fn as_StdoutWrite(&self) -> &Op_StdoutWrite { + debug_assert_eq!(self.discriminant(), discriminant_Op::StdoutWrite); + let payload = { + let ptr = (self.pointer as usize & !0b111) as *mut union_Op; + + unsafe { &(*ptr).StdoutWrite } + }; + + &payload + } +} + +impl Drop for Op { + #[cfg(any( + target_arch = "arm", + target_arch = "aarch64", + target_arch = "wasm32", + target_arch = "x86", + target_arch = "x86_64" + ))] + fn drop(&mut self) { + // We only need to do any work if there's actually a heap-allocated payload. + if let Some(storage) = self.storage() { + let mut new_storage = storage.get(); + + // Decrement the refcount + let needs_dealloc = !new_storage.is_readonly() && new_storage.decrease(); + + if needs_dealloc { + // Drop the payload first. + match self.discriminant() { + discriminant_Op::Done => {} + discriminant_Op::StderrWrite => unsafe { + core::mem::ManuallyDrop::drop(&mut (&mut *self.union_pointer()).StderrWrite) + }, + discriminant_Op::StdoutWrite => unsafe { + core::mem::ManuallyDrop::drop(&mut (&mut *self.union_pointer()).StdoutWrite) + }, + } + + // Dealloc the pointer + let alignment = + core::mem::align_of::().max(core::mem::align_of::()); + + unsafe { + crate::roc_dealloc(storage.as_ptr().cast(), alignment as u32); + } + } else { + // Write the storage back. + storage.set(new_storage); + } + } + } +} + +impl Eq for Op {} + +impl PartialEq for Op { + #[cfg(any( + target_arch = "arm", + target_arch = "aarch64", + target_arch = "wasm32", + target_arch = "x86", + target_arch = "x86_64" + ))] + fn eq(&self, other: &Self) -> bool { + if self.discriminant() != other.discriminant() { + return false; + } + + unsafe { + match self.discriminant() { + discriminant_Op::Done => true, + discriminant_Op::StderrWrite => { + (&*self.union_pointer()).StderrWrite == (&*other.union_pointer()).StderrWrite + } + discriminant_Op::StdoutWrite => { + (&*self.union_pointer()).StdoutWrite == (&*other.union_pointer()).StdoutWrite + } + } + } + } +} + +impl PartialOrd for Op { + #[cfg(any( + target_arch = "arm", + target_arch = "aarch64", + target_arch = "wasm32", + target_arch = "x86", + target_arch = "x86_64" + ))] + fn partial_cmp(&self, other: &Self) -> Option { + match self.discriminant().partial_cmp(&other.discriminant()) { + Some(core::cmp::Ordering::Equal) => {} + not_eq => return not_eq, + } + + unsafe { + match self.discriminant() { + discriminant_Op::Done => Some(core::cmp::Ordering::Equal), + discriminant_Op::StderrWrite => (&*self.union_pointer()) + .StderrWrite + .partial_cmp(&(&*other.union_pointer()).StderrWrite), + discriminant_Op::StdoutWrite => (&*self.union_pointer()) + .StdoutWrite + .partial_cmp(&(&*other.union_pointer()).StdoutWrite), + } + } + } +} + +impl Ord for Op { + #[cfg(any( + target_arch = "arm", + target_arch = "aarch64", + target_arch = "wasm32", + target_arch = "x86", + target_arch = "x86_64" + ))] + fn cmp(&self, other: &Self) -> core::cmp::Ordering { + match self.discriminant().cmp(&other.discriminant()) { + core::cmp::Ordering::Equal => {} + not_eq => return not_eq, + } + + unsafe { + match self.discriminant() { + discriminant_Op::Done => core::cmp::Ordering::Equal, + discriminant_Op::StderrWrite => (&*self.union_pointer()) + .StderrWrite + .cmp(&(&*other.union_pointer()).StderrWrite), + discriminant_Op::StdoutWrite => (&*self.union_pointer()) + .StdoutWrite + .cmp(&(&*other.union_pointer()).StdoutWrite), + } + } + } +} + +impl Clone for Op { + #[cfg(any( + target_arch = "arm", + target_arch = "aarch64", + target_arch = "wasm32", + target_arch = "x86", + target_arch = "x86_64" + ))] + fn clone(&self) -> Self { + if let Some(storage) = self.storage() { + let mut new_storage = storage.get(); + if !new_storage.is_readonly() { + new_storage.increment_reference_count(); + storage.set(new_storage); + } + } + + Self { + pointer: self.pointer, + } + } +} + +impl core::hash::Hash for Op { + #[cfg(any( + target_arch = "arm", + target_arch = "aarch64", + target_arch = "wasm32", + target_arch = "x86", + target_arch = "x86_64" + ))] + fn hash(&self, state: &mut H) { + match self.discriminant() { + discriminant_Op::Done => discriminant_Op::Done.hash(state), + discriminant_Op::StderrWrite => unsafe { + discriminant_Op::StderrWrite.hash(state); + (&*self.union_pointer()).StderrWrite.hash(state); + }, + discriminant_Op::StdoutWrite => unsafe { + discriminant_Op::StdoutWrite.hash(state); + (&*self.union_pointer()).StdoutWrite.hash(state); + }, + } + } +} + +impl core::fmt::Debug for Op { + #[cfg(any( + target_arch = "arm", + target_arch = "aarch64", + target_arch = "wasm32", + target_arch = "x86", + target_arch = "x86_64" + ))] + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.write_str("Op::")?; + + unsafe { + match self.discriminant() { + discriminant_Op::Done => f.write_str("Done"), + discriminant_Op::StderrWrite => f + .debug_tuple("StderrWrite") + // TODO HAS CLOSURE + .finish(), + discriminant_Op::StdoutWrite => f + .debug_tuple("StdoutWrite") + // TODO HAS CLOSURE + .finish(), + } + } + } +} diff --git a/examples/glue/rust-platform/src/lib.rs b/examples/glue/rust-platform/src/lib.rs new file mode 100644 index 0000000000..3665ed7b43 --- /dev/null +++ b/examples/glue/rust-platform/src/lib.rs @@ -0,0 +1,123 @@ +#![allow(non_snake_case)] + +use core::ffi::c_void; +use roc_app::Op; +use roc_std::RocStr; +use std::ffi::CStr; +use std::io::Write; +use std::os::raw::c_char; + +use roc_app::mainForHost as roc_main; + +#[no_mangle] +pub unsafe extern "C" fn roc_alloc(size: usize, _alignment: u32) -> *mut c_void { + return libc::malloc(size); +} + +#[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 { + return libc::realloc(c_ptr, new_size); +} + +#[no_mangle] +pub unsafe extern "C" fn roc_dealloc(c_ptr: *mut c_void, _alignment: u32) { + return libc::free(c_ptr); +} + +#[no_mangle] +pub unsafe extern "C" fn roc_panic(msg: *mut RocStr, tag_id: u32) { + match tag_id { + 0 => { + eprintln!("Roc standard library hit a panic: {}", &*msg); + } + 1 => { + eprintln!("Application hit a panic: {}", &*msg); + } + _ => unreachable!(), + } + std::process::exit(1); +} + +#[no_mangle] +pub unsafe extern "C" fn roc_dbg(loc: *mut RocStr, msg: *mut RocStr) { + eprintln!("[{}] {}", &*loc, &*msg); +} + +#[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) +} + +#[cfg(unix)] +#[no_mangle] +pub unsafe extern "C" fn roc_getppid() -> libc::pid_t { + libc::getppid() +} + +#[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) +} + +#[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) +} + +#[no_mangle] +pub extern "C" fn rust_main() -> i32 { + use roc_app::discriminant_Op::*; + + println!("Let's do things!"); + + let mut op: Op = roc_main(); + + loop { + match op.discriminant() { + StdoutWrite => { + let stdout_write = op.get_StdoutWrite(); + let output: RocStr = stdout_write.f0; + op = unsafe { stdout_write.f1.force_thunk() }; + + if let Err(e) = std::io::stdout().write_all(output.as_bytes()) { + panic!("Writing to stdout failed! {:?}", e); + } + } + StderrWrite => { + let stderr_write = op.get_StderrWrite(); + let output: RocStr = stderr_write.f0; + op = unsafe { stderr_write.f1.force_thunk() }; + + if let Err(e) = std::io::stderr().write_all(output.as_bytes()) { + panic!("Writing to stdout failed! {:?}", e); + } + } + Done => { + break; + } + } + } + + println!("Done!"); + + // Exit code + 0 +} diff --git a/examples/glue/rust-platform/src/main.rs b/examples/glue/rust-platform/src/main.rs new file mode 100644 index 0000000000..0765384f29 --- /dev/null +++ b/examples/glue/rust-platform/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + std::process::exit(host::rust_main() as _); +} diff --git a/examples/gui/.gitignore b/examples/gui/.gitignore index 8f5562e399..09b30e5fc4 100644 --- a/examples/gui/.gitignore +++ b/examples/gui/.gitignore @@ -1 +1 @@ -hello-gui +hello-guiBROKEN diff --git a/examples/gui/Hello.roc b/examples/gui/Hello.roc deleted file mode 100644 index b21a71e80f..0000000000 --- a/examples/gui/Hello.roc +++ /dev/null @@ -1,19 +0,0 @@ -app "hello-gui" - packages { pf: "platform" } - imports []# [pf.Action.{ Action }, pf.Elem.{ button, text, row, col }] - provides [render] to pf - -render = - rgba = \r, g, b, a -> { r: r / 255, g: g / 255, b: b / 255, a } - - styles = { bgColor: rgba 100 50 50 1, borderColor: rgba 10 20 30 1, borderWidth: 10, textColor: rgba 220 220 250 1 } - - Col [ - Row [ - Button (Text "Corner ") styles, - Button (Text "Top Mid ") { styles & bgColor: rgba 100 100 50 1 }, - Button (Text "Top Right ") { styles & bgColor: rgba 50 50 150 1 }, - ], - Button (Text "Mid Left ") { styles & bgColor: rgba 150 100 100 1 }, - Button (Text "Bottom Left") { styles & bgColor: rgba 150 50 50 1 }, - ] diff --git a/editor/Inconsolata-Regular.ttf b/examples/gui/Inconsolata-Regular.ttf similarity index 100% rename from editor/Inconsolata-Regular.ttf rename to examples/gui/Inconsolata-Regular.ttf diff --git a/examples/gui/breakout/.gitignore b/examples/gui/breakout/.gitignore new file mode 100644 index 0000000000..5b34d03882 --- /dev/null +++ b/examples/gui/breakout/.gitignore @@ -0,0 +1,2 @@ +breakoutBROKEN +hello-guiBROKEN diff --git a/examples/gui/breakout/breakoutBROKEN.roc b/examples/gui/breakout/breakoutBROKEN.roc new file mode 100644 index 0000000000..649722acb9 --- /dev/null +++ b/examples/gui/breakout/breakoutBROKEN.roc @@ -0,0 +1,165 @@ +app "breakout" + packages { pf: "platform/main.roc" } + imports [pf.Game.{ Bounds, Elem, Event }] + provides [program] { Model } to pf + +paddleWidth = 0.2 # width of the paddle, as a % of screen width +paddleHeight = 50 # height of the paddle, in pixels +paddleSpeed = 65 # how many pixels the paddle moves per keypress +blockHeight = 80 # height of a block, in pixels +blockBorder = 0.025 # border of a block, as a % of its width +ballSize = 55 +numRows = 4 +numCols = 8 +numBlocks = numRows * numCols + +Model : { + # Screen height and width + height : F32, + width : F32, + # Paddle X-coordinate + paddleX : F32, + # Ball coordinates + ballX : F32, + ballY : F32, + dBallX : F32, + # delta x - how much it moves per tick + dBallY : F32, + # delta y - how much it moves per tick +} + +init : Bounds -> Model +init = \{ width, height } -> { + # Screen height and width + width, + height, + # Paddle X-coordinate + paddleX: (width * 0.5) - (paddleWidth * width * 0.5), + # Ball coordinates + ballX: width * 0.5, + ballY: height * 0.4, + # Delta - how much ball moves in each tick + dBallX: 4, + dBallY: 4, +} + +update : Model, Event -> Model +update = \model, event -> + when event is + Resize size -> + { model & width: size.width, height: size.height } + + KeyDown Left -> + { model & paddleX: model.paddleX - paddleSpeed } + + KeyDown Right -> + { model & paddleX: model.paddleX + paddleSpeed } + + Tick _ -> + tick model + + _ -> + model + +tick : Model -> Model +tick = \model -> + model + |> moveBall + +moveBall : Model -> Model +moveBall = \model -> + ballX = model.ballX + model.dBallX + ballY = model.ballY + model.dBallY + + paddleTop = model.height - blockHeight - (paddleHeight * 2) + paddleLeft = model.paddleX + paddleRight = paddleLeft + (model.width * paddleWidth) + + # If its y used to be less than the paddle, and now it's greater than or equal, + # then this is the frame where the ball collided with it. + crossingPaddle = model.ballY < paddleTop && ballY >= paddleTop + + # If it collided with the paddle, bounce off. + directionChange = + if crossingPaddle && (ballX >= paddleLeft && ballX <= paddleRight) then + -1f32 + else + 1f32 + + dBallX = model.dBallX * directionChange + dBallY = model.dBallY * directionChange + + { model & ballX, ballY, dBallX, dBallY } + +render : Model -> List Elem +render = \model -> + + blocks = List.map + (List.range { start: At 0, end: Length numBlocks }) + \index -> + col = + Num.rem index numCols + |> Num.toF32 + + row = + index + // numCols + |> Num.toF32 + + red = col / Num.toF32 numCols + green = row / Num.toF32 numRows + blue = Num.toF32 index / Num.toF32 numBlocks + + color = { r: red * 0.8, g: 0.2 + green * 0.6, b: 0.2 + blue * 0.8, a: 1 } + + { row, col, color } + + blockWidth = model.width / numCols + + rects = + List.joinMap + blocks + \{ row, col, color } -> + left = Num.toF32 col * blockWidth + top = Num.toF32 (row * blockHeight) + border = blockBorder * blockWidth + + outer = Rect { + left, + top, + width: blockWidth, + height: blockHeight, + color: { r: color.r * 0.8, g: color.g * 0.8, b: color.b * 0.8, a: 1 }, + } + + inner = Rect { + left: left + border, + top: top + border, + width: blockWidth - (border * 2), + height: blockHeight - (border * 2), + color, + } + + [outer, inner] + + ball = + color = { r: 0.7, g: 0.3, b: 0.9, a: 1.0 } + width = ballSize + height = ballSize + left = model.ballX + top = model.ballY + + Rect { left, top, width, height, color } + + paddle = + color = { r: 0.8, g: 0.8, b: 0.8, a: 1.0 } + width = model.width * paddleWidth + height = paddleHeight + left = model.paddleX + top = model.height - blockHeight - height + + Rect { left, top, width, height, color } + + List.concat rects [paddle, ball] + +program = { init, update, render } diff --git a/examples/gui/breakout/hello-guiBROKEN.roc b/examples/gui/breakout/hello-guiBROKEN.roc new file mode 100644 index 0000000000..86e97a9f4d --- /dev/null +++ b/examples/gui/breakout/hello-guiBROKEN.roc @@ -0,0 +1,17 @@ +app "hello-gui" + packages { pf: "platform/main.roc" } + imports [pf.Game.{ Bounds, Elem, Event }] + provides [program] { Model } to pf + +Model : { text : Str } + +init : Bounds -> Model +init = \_ -> { text: "Hello, World!" } + +update : Model, Event -> Model +update = \model, _ -> model + +render : Model -> List Elem +render = \model -> [Text { text: model.text, top: 0, left: 0, size: 40, color: { r: 1, g: 1, b: 1, a: 1 } }] + +program = { init, update, render } diff --git a/examples/breakout/platform/.gitignore b/examples/gui/breakout/platform/.gitignore similarity index 100% rename from examples/breakout/platform/.gitignore rename to examples/gui/breakout/platform/.gitignore diff --git a/examples/gui/breakout/platform/Action.roc b/examples/gui/breakout/platform/Action.roc new file mode 100644 index 0000000000..6a5e019c73 --- /dev/null +++ b/examples/gui/breakout/platform/Action.roc @@ -0,0 +1,17 @@ +interface Action + exposes [Action, none, update, map] + imports [] + +Action state : [None, Update state] + +none : Action * +none = None + +update : state -> Action state +update = Update + +map : Action a, (a -> b) -> Action b +map = \action, transform -> + when action is + None -> None + Update state -> Update (transform state) diff --git a/examples/gui/breakout/platform/Cargo.toml b/examples/gui/breakout/platform/Cargo.toml new file mode 100644 index 0000000000..ec0b370ed8 --- /dev/null +++ b/examples/gui/breakout/platform/Cargo.toml @@ -0,0 +1,57 @@ +[package] +name = "host" +authors = ["The Roc Contributors"] +edition = "2021" +license = "UPL-1.0" +version = "0.0.1" + +[lib] +name = "host" +path = "src/lib.rs" +crate-type = ["staticlib", "rlib"] + +[[bin]] +name = "host" +path = "src/main.rs" + +[dependencies] +arrayvec = "0.7.2" +libc = "0.2" +page_size = "0.4.2" +roc_std = { path = "../../../../crates/roc_std" } +cgmath = "0.18.0" +colored = "2.0.0" +copypasta = "0.7.1" +fs_extra = "1.2.0" +futures = "0.3.17" +glyph_brush = "0.7.2" +log = "0.4.14" +nonempty = "0.7.0" +palette = "0.6.0" +pest = "2.1.3" +pest_derive = "2.1.0" +serde = { version = "1.0.130", features = ["derive"] } +snafu = { version = "0.6.10", features = ["backtraces"] } +threadpool = "1.8.1" +wgpu = { git = "https://github.com/gfx-rs/wgpu", rev = "0545e36" } +wgpu_glyph = { git = "https://github.com/Anton-4/wgpu_glyph", rev = "257d109" } +winit = "0.26.1" + +[features] +default = [] + +[dependencies.bytemuck] +version = "1.7.2" +features = ["derive"] + +[workspace] + +# Optimizations based on https://deterministic.space/high-performance-rust.html +[profile.release] +lto = "fat" +codegen-units = 1 + +# debug = true # enable when profiling +[profile.bench] +lto = "thin" +codegen-units = 1 diff --git a/examples/breakout/platform/Elem.roc b/examples/gui/breakout/platform/Elem.roc similarity index 82% rename from examples/breakout/platform/Elem.roc rename to examples/gui/breakout/platform/Elem.roc index b9a4d53d50..64aef819d9 100644 --- a/examples/breakout/platform/Elem.roc +++ b/examples/gui/breakout/platform/Elem.roc @@ -2,21 +2,20 @@ interface Elem exposes [Elem, PressEvent, row, col, text, button, none, translate, list] imports [Action.{ Action }] -Elem state : +Elem state : [ # PERFORMANCE NOTE: # If there are 8 or fewer tags here, then on a 64-bit system, the tag can be stored # in the pointer - for massive memory savings. Try extremely hard to always limit the number # of tags in this union to 8 or fewer! - [ - Button (ButtonConfig state) (Elem state), - Text Str, - Col (List (Elem state)), - Row (List (Elem state)), - Lazy (Result { state, elem : Elem state } [NotCached] -> { state, elem : Elem state }), - # TODO FIXME: using this definition of Lazy causes a stack overflow in the compiler! - # Lazy (Result (Cached state) [NotCached] -> Cached state), - None, - ] + Button (ButtonConfig state) (Elem state), + Text Str, + Col (List (Elem state)), + Row (List (Elem state)), + Lazy (Result { state, elem : Elem state } [NotCached] -> { state, elem : Elem state }), + # TODO FIXME: using this definition of Lazy causes a stack overflow in the compiler! + # Lazy (Result (Cached state) [NotCached] -> Cached state), + None, +] ## Used internally in the type definition of Lazy Cached state : { state, elem : Elem state } @@ -54,6 +53,7 @@ lazy = \state, render -> # same as the cached one, then we can return exactly # what we had cached. cached + _ -> # Either the state changed or else we didn't have a # cached value to use. Either way, we need to render @@ -61,10 +61,10 @@ lazy = \state, render -> { state, elem: render state } none : Elem * -none = None# I've often wanted this in elm/html. Usually end up resorting to (Html.text "") - this seems nicer. +none = None # I've often wanted this in elm/html. Usually end up resorting to (Html.text "") - this seems nicer. ## Change an element's state type. ## -## TODO: indent the following once https://github.com/rtfeldman/roc/issues/2585 is fixed. +## TODO: indent the following once https://github.com/roc-lang/roc/issues/2585 is fixed. ## State : { photo : Photo } ## ## render : State -> Elem State @@ -80,17 +80,21 @@ translate = \child, toChild, toParent -> when child is Text str -> Text str + Col elems -> Col (List.map elems \elem -> translate elem toChild toParent) + Row elems -> Row (List.map elems \elem -> translate elem toChild toParent) + Button config label -> onPress = \parentState, event -> toChild parentState - |> config.onPress event - |> Action.map \c -> toParent parentState c + |> config.onPress event + |> Action.map \c -> toParent parentState c Button { onPress } (translate label toChild toParent) + Lazy renderChild -> Lazy \parentState -> @@ -100,6 +104,7 @@ translate = \child, toChild, toParent -> elem: translate toChild toParent newChild, state: toParent parentState state, } + None -> None @@ -108,7 +113,7 @@ translate = \child, toChild, toParent -> ## Convenient when you have a [List] in your state and want to make ## a [List] of child elements out of it. ## -## TODO: indent the following once https://github.com/rtfeldman/roc/issues/2585 is fixed. +## TODO: indent the following once https://github.com/roc-lang/roc/issues/2585 is fixed. ## State : { photos : List Photo } ## ## render : State -> Elem State @@ -118,7 +123,7 @@ translate = \child, toChild, toParent -> ## Elem.list Photo.render state .photos &photos ## ## col {} children -## TODO: format as multiline type annotation once https://github.com/rtfeldman/roc/issues/2586 is fixed +## TODO: format as multiline type annotation once https://github.com/roc-lang/roc/issues/2586 is fixed list : (child -> Elem child), parent, (parent -> List child), (parent, List child -> parent) -> List (Elem parent) list = \renderChild, parent, toChildren, toParent -> List.mapWithIndex @@ -131,8 +136,8 @@ list = \renderChild, parent, toChildren, toParent -> toChild \par, ch -> toChildren par - |> List.set ch index - |> toParent + |> List.set ch index + |> toParent renderChild newChild @@ -142,29 +147,34 @@ list = \renderChild, parent, toChildren, toParent -> ## if the child has been removed from the parent, ## drops it. ## -## TODO: format as multiline type annotation once https://github.com/rtfeldman/roc/issues/2586 is fixed +## TODO: format as multiline type annotation once https://github.com/roc-lang/roc/issues/2586 is fixed translateOrDrop : Elem child, (parent -> Result child *), (parent, child -> parent) -> Elem parent translateOrDrop = \child, toChild, toParent -> when child is Text str -> Text str + Col elems -> Col (List.map elems \elem -> translateOrDrop elem toChild toParent) + Row elems -> Row (List.map elems \elem -> translateOrDrop elem toChild toParent) + Button config label -> onPress = \parentState, event -> when toChild parentState is Ok newChild -> newChild - |> config.onPress event - |> Action.map \c -> toParent parentState c + |> config.onPress event + |> Action.map \c -> toParent parentState c + Err _ -> # The child was removed from the list before this onPress handler resolved. # (For example, by a previous event handler that fired simultaneously.) Action.none Button { onPress } (translateOrDrop label toChild toParent) + Lazy childState renderChild -> Lazy (toParent childState) @@ -172,9 +182,11 @@ translateOrDrop = \child, toChild, toParent -> when toChild parentState is Ok newChild -> renderChild newChild - |> translateOrDrop toChild toParent + |> translateOrDrop toChild toParent + Err _ -> None + # I don't think this should ever happen in practice. None -> None diff --git a/examples/gui/breakout/platform/Game.roc b/examples/gui/breakout/platform/Game.roc new file mode 100644 index 0000000000..26b966bf8d --- /dev/null +++ b/examples/gui/breakout/platform/Game.roc @@ -0,0 +1,13 @@ +interface Game + exposes [Bounds, Elem, Event] + imports [] + +Rgba : { r : F32, g : F32, b : F32, a : F32 } + +Bounds : { height : F32, width : F32 } + +Elem : [Rect { color : Rgba, left : F32, top : F32, width : F32, height : F32 }, Text { text : Str, color : Rgba, left : F32, top : F32, size : F32 }] + +KeyCode : [Left, Right, Other, Up, Down] + +Event : [Resize { width : F32, height : F32 }, KeyDown KeyCode, KeyUp KeyCode, Tick U128] diff --git a/examples/gui/breakout/platform/build.rs b/examples/gui/breakout/platform/build.rs new file mode 100644 index 0000000000..47763b34c3 --- /dev/null +++ b/examples/gui/breakout/platform/build.rs @@ -0,0 +1,9 @@ +fn main() { + #[cfg(not(windows))] + println!("cargo:rustc-link-lib=dylib=app"); + + #[cfg(windows)] + println!("cargo:rustc-link-lib=dylib=libapp"); + + println!("cargo:rustc-link-search=."); +} diff --git a/examples/hello-world/rust-platform/host.c b/examples/gui/breakout/platform/host.c similarity index 100% rename from examples/hello-world/rust-platform/host.c rename to examples/gui/breakout/platform/host.c diff --git a/examples/breakout/platform/Package-Config.roc b/examples/gui/breakout/platform/main.roc similarity index 100% rename from examples/breakout/platform/Package-Config.roc rename to examples/gui/breakout/platform/main.roc diff --git a/examples/breakout/platform/src/graphics/colors.rs b/examples/gui/breakout/platform/src/graphics/colors.rs similarity index 100% rename from examples/breakout/platform/src/graphics/colors.rs rename to examples/gui/breakout/platform/src/graphics/colors.rs diff --git a/examples/breakout/platform/src/graphics/lowlevel/buffer.rs b/examples/gui/breakout/platform/src/graphics/lowlevel/buffer.rs similarity index 100% rename from examples/breakout/platform/src/graphics/lowlevel/buffer.rs rename to examples/gui/breakout/platform/src/graphics/lowlevel/buffer.rs diff --git a/examples/breakout/platform/src/graphics/lowlevel/mod.rs b/examples/gui/breakout/platform/src/graphics/lowlevel/mod.rs similarity index 100% rename from examples/breakout/platform/src/graphics/lowlevel/mod.rs rename to examples/gui/breakout/platform/src/graphics/lowlevel/mod.rs diff --git a/editor/src/graphics/lowlevel/ortho.rs b/examples/gui/breakout/platform/src/graphics/lowlevel/ortho.rs similarity index 100% rename from editor/src/graphics/lowlevel/ortho.rs rename to examples/gui/breakout/platform/src/graphics/lowlevel/ortho.rs diff --git a/examples/breakout/platform/src/graphics/lowlevel/pipelines.rs b/examples/gui/breakout/platform/src/graphics/lowlevel/pipelines.rs similarity index 100% rename from examples/breakout/platform/src/graphics/lowlevel/pipelines.rs rename to examples/gui/breakout/platform/src/graphics/lowlevel/pipelines.rs diff --git a/examples/breakout/platform/src/graphics/lowlevel/quad.rs b/examples/gui/breakout/platform/src/graphics/lowlevel/quad.rs similarity index 83% rename from examples/breakout/platform/src/graphics/lowlevel/quad.rs rename to examples/gui/breakout/platform/src/graphics/lowlevel/quad.rs index 9c1fd85ae6..7d8cf9dd8d 100644 --- a/examples/breakout/platform/src/graphics/lowlevel/quad.rs +++ b/examples/gui/breakout/platform/src/graphics/lowlevel/quad.rs @@ -2,6 +2,7 @@ /// A polygon with 4 corners #[derive(Copy, Clone)] +#[repr(C)] pub struct Quad { pub pos: [f32; 2], pub width: f32, @@ -11,6 +12,10 @@ pub struct Quad { pub border_width: f32, } +// Safety: Pod's contract says the type must +// not have any padding, and must be repr(C). +// As currrently defined, Quad does not have +// any padding. unsafe impl bytemuck::Pod for Quad {} unsafe impl bytemuck::Zeroable for Quad {} @@ -28,4 +33,4 @@ impl Quad { 6 => Float32, ), }; -} \ No newline at end of file +} diff --git a/examples/breakout/platform/src/graphics/lowlevel/vertex.rs b/examples/gui/breakout/platform/src/graphics/lowlevel/vertex.rs similarity index 100% rename from examples/breakout/platform/src/graphics/lowlevel/vertex.rs rename to examples/gui/breakout/platform/src/graphics/lowlevel/vertex.rs diff --git a/editor/src/graphics/mod.rs b/examples/gui/breakout/platform/src/graphics/mod.rs similarity index 100% rename from editor/src/graphics/mod.rs rename to examples/gui/breakout/platform/src/graphics/mod.rs diff --git a/editor/src/graphics/primitives/mod.rs b/examples/gui/breakout/platform/src/graphics/primitives/mod.rs similarity index 100% rename from editor/src/graphics/primitives/mod.rs rename to examples/gui/breakout/platform/src/graphics/primitives/mod.rs diff --git a/examples/breakout/platform/src/graphics/primitives/rect.rs b/examples/gui/breakout/platform/src/graphics/primitives/rect.rs similarity index 100% rename from examples/breakout/platform/src/graphics/primitives/rect.rs rename to examples/gui/breakout/platform/src/graphics/primitives/rect.rs diff --git a/examples/gui/breakout/platform/src/graphics/primitives/text.rs b/examples/gui/breakout/platform/src/graphics/primitives/text.rs new file mode 100644 index 0000000000..2ebccf9161 --- /dev/null +++ b/examples/gui/breakout/platform/src/graphics/primitives/text.rs @@ -0,0 +1,134 @@ +// Adapted from https://github.com/sotrh/learn-wgpu +// by Benjamin Hansen - license information can be found in the COPYRIGHT +// file in the root directory of this distribution. +// +// Thank you, Benjamin! + +use crate::graphics::colors::Rgba; +use crate::graphics::style::DEFAULT_FONT_SIZE; +use ab_glyph::{FontArc, InvalidFont}; +use cgmath::Vector2; +use wgpu_glyph::{ab_glyph, GlyphBrush, GlyphBrushBuilder}; + +#[derive(Debug)] +pub struct Text<'a> { + pub position: Vector2, + pub area_bounds: Vector2, + pub color: Rgba, + pub text: &'a str, + pub size: f32, + pub visible: bool, + pub centered: bool, +} + +impl<'a> Default for Text<'a> { + fn default() -> Self { + Self { + position: (0.0, 0.0).into(), + area_bounds: (std::f32::INFINITY, std::f32::INFINITY).into(), + color: Rgba::WHITE, + text: "", + size: DEFAULT_FONT_SIZE, + visible: true, + centered: false, + } + } +} + +// pub fn layout_from_text(text: &Text) -> wgpu_glyph::Layout { +// wgpu_glyph::Layout::default().h_align(if text.centered { +// wgpu_glyph::HorizontalAlign::Center +// } else { +// wgpu_glyph::HorizontalAlign::Left +// }) +// } + +// fn section_from_text<'a>( +// text: &'a Text, +// layout: wgpu_glyph::Layout, +// ) -> wgpu_glyph::Section<'a> { +// Section { +// screen_position: text.position.into(), +// bounds: text.area_bounds.into(), +// layout, +// ..Section::default() +// } +// .add_text( +// wgpu_glyph::Text::new(text.text) +// .with_color(text.color) +// .with_scale(text.size), +// ) +// } + +// pub fn owned_section_from_text(text: &Text) -> OwnedSection { +// let layout = layout_from_text(text); + +// OwnedSection { +// screen_position: text.position.into(), +// bounds: text.area_bounds.into(), +// layout, +// ..OwnedSection::default() +// } +// .add_text( +// glyph_brush::OwnedText::new(text.text) +// .with_color(Vector4::from(text.color)) +// .with_scale(text.size), +// ) +// } + +// pub fn owned_section_from_glyph_texts( +// text: Vec, +// screen_position: (f32, f32), +// area_bounds: (f32, f32), +// layout: wgpu_glyph::Layout, +// ) -> glyph_brush::OwnedSection { +// glyph_brush::OwnedSection { +// screen_position, +// bounds: area_bounds, +// layout, +// text, +// } +// } + +// pub fn queue_text_draw(text: &Text, glyph_brush: &mut GlyphBrush<()>) { +// let layout = layout_from_text(text); + +// let section = section_from_text(text, layout); + +// glyph_brush.queue(section.clone()); +// } + +// fn glyph_to_rect(glyph: &wgpu_glyph::SectionGlyph) -> Rect { +// let position = glyph.glyph.position; +// let px_scale = glyph.glyph.scale; +// let width = glyph_width(&glyph.glyph); +// let height = px_scale.y; +// let top_y = glyph_top_y(&glyph.glyph); + +// Rect { +// pos: [position.x, top_y].into(), +// width, +// height, +// } +// } + +// pub fn glyph_top_y(glyph: &Glyph) -> f32 { +// let height = glyph.scale.y; + +// glyph.position.y - height * 0.75 +// } + +// pub fn glyph_width(glyph: &Glyph) -> f32 { +// glyph.scale.x * 0.4765 +// } + +pub fn build_glyph_brush( + gpu_device: &wgpu::Device, + render_format: wgpu::TextureFormat, +) -> Result, InvalidFont> { + let inconsolata = FontArc::try_from_slice(include_bytes!( + "../../../../../Inconsolata-Regular.ttf" + ))?; + + Ok(GlyphBrushBuilder::using_font(inconsolata).build(gpu_device, render_format)) +} diff --git a/examples/breakout/platform/src/graphics/shaders/quad.wgsl b/examples/gui/breakout/platform/src/graphics/shaders/quad.wgsl similarity index 100% rename from examples/breakout/platform/src/graphics/shaders/quad.wgsl rename to examples/gui/breakout/platform/src/graphics/shaders/quad.wgsl diff --git a/editor/src/graphics/style.rs b/examples/gui/breakout/platform/src/graphics/style.rs similarity index 100% rename from editor/src/graphics/style.rs rename to examples/gui/breakout/platform/src/graphics/style.rs diff --git a/examples/breakout/platform/src/gui.rs b/examples/gui/breakout/platform/src/gui.rs similarity index 97% rename from examples/breakout/platform/src/gui.rs rename to examples/gui/breakout/platform/src/gui.rs index ef0412ea25..e4af20f705 100644 --- a/examples/breakout/platform/src/gui.rs +++ b/examples/gui/breakout/platform/src/gui.rs @@ -73,7 +73,9 @@ pub fn run_event_loop(title: &str, window_bounds: Bounds) -> Result<(), Box`. + See extra docs here: github.com/guibou/nixGL "#); adapter @@ -455,7 +457,7 @@ fn to_drawable( wgpu_glyph::HorizontalAlign::Left }); - let section = owned_section_from_str(text.as_str(), bounds, layout); + let section = owned_section_from_str(text.text.as_str(),text.color, text.size, bounds, layout); // Calculate the bounds and offset by measuring glyphs let text_bounds; @@ -481,7 +483,7 @@ fn to_drawable( } let drawable = Drawable { - pos: (0.0, 0.0).into(), // TODO store the pos in Text and read it here + pos: (text.left, text.top).into(), bounds: text_bounds, content: DrawableContent::Text(section, offset), }; @@ -493,13 +495,11 @@ fn to_drawable( fn owned_section_from_str( string: &str, + color: Rgba, + size: f32, bounds: Bounds, layout: wgpu_glyph::Layout, ) -> OwnedSection { - // TODO don't hardcode any of this! - let color = Rgba::WHITE; - let size: f32 = 40.0; - OwnedSection { bounds: (bounds.width, bounds.height), layout, diff --git a/examples/gui/breakout/platform/src/lib.rs b/examples/gui/breakout/platform/src/lib.rs new file mode 100644 index 0000000000..51378f8ddc --- /dev/null +++ b/examples/gui/breakout/platform/src/lib.rs @@ -0,0 +1,18 @@ +#![allow(unused)] + +mod graphics; +mod gui; +mod roc; + +#[no_mangle] +pub extern "C" fn rust_main() -> i32 { + let bounds = roc::Bounds { + width: 1900.0, + height: 1000.0, + }; + + gui::run_event_loop("RocOut!", bounds).expect("Error running event loop"); + + // Exit code + 0 +} diff --git a/examples/gui/breakout/platform/src/main.rs b/examples/gui/breakout/platform/src/main.rs new file mode 100644 index 0000000000..0765384f29 --- /dev/null +++ b/examples/gui/breakout/platform/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + std::process::exit(host::rust_main() as _); +} diff --git a/examples/gui/breakout/platform/src/roc.rs b/examples/gui/breakout/platform/src/roc.rs new file mode 100644 index 0000000000..fe6cbb4a69 --- /dev/null +++ b/examples/gui/breakout/platform/src/roc.rs @@ -0,0 +1,453 @@ +use crate::graphics::colors::Rgba; +use core::alloc::Layout; +use core::ffi::c_void; +use core::mem::{self, ManuallyDrop}; +use roc_std::{RocList, RocStr}; +use std::ffi::CStr; +use std::fmt::Debug; +use std::mem::MaybeUninit; +use std::os::raw::c_char; +use std::time::Duration; +use winit::event::VirtualKeyCode; + +extern "C" { + // program + + // #[link_name = "roc__programForHost_1_exposed_generic"] + // fn roc_program(); + + // #[link_name = "roc__programForHost_1_exposed_size"] + // fn roc_program_size() -> i64; + + // init + + #[link_name = "roc__programForHost_0_caller"] + fn call_init(size: *const Bounds, closure_data: *const u8, output: *mut Model); + + #[link_name = "roc__programForHost_0_size"] + fn init_size() -> i64; + + #[link_name = "roc__programForHost_0_result_size"] + fn init_result_size() -> i64; + + // update + + #[link_name = "roc__programForHost_1_caller"] + fn call_update( + model: *const Model, + event: *const RocEvent, + closure_data: *const u8, + output: *mut Model, + ); + + #[link_name = "roc__programForHost_1_size"] + fn update_size() -> i64; + + #[link_name = "roc__programForHost_1_result_size"] + fn update_result_size() -> i64; + + // render + + #[link_name = "roc__programForHost_2_caller"] + fn call_render(model: *const Model, closure_data: *const u8, output: *mut RocList); + + #[link_name = "roc__programForHost_2_size"] + fn roc_render_size() -> i64; +} + +#[repr(C)] +pub union RocEventEntry { + pub key_down: RocKeyCode, + pub key_up: RocKeyCode, + pub resize: Bounds, + pub tick: [u8; 16], // u128 is unsupported in repr(C) +} + +#[repr(u8)] +#[allow(unused)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum RocEventTag { + KeyDown = 0, + KeyUp, + Resize, + Tick, +} + +#[repr(C)] +#[cfg(target_pointer_width = "64")] // on a 64-bit system, the tag fits in this pointer's spare 3 bits +pub struct RocEvent { + entry: RocEventEntry, + tag: RocEventTag, +} + +impl Debug for RocEvent { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + use RocEventTag::*; + + match self.tag() { + KeyDown => unsafe { self.entry().key_down }.fmt(f), + KeyUp => unsafe { self.entry().key_up }.fmt(f), + Resize => unsafe { self.entry().resize }.fmt(f), + Tick => unsafe { self.entry().tick }.fmt(f), + } + } +} + +impl RocEvent { + #[cfg(target_pointer_width = "64")] + pub fn tag(&self) -> RocEventTag { + self.tag + } + + pub fn entry(&self) -> &RocEventEntry { + &self.entry + } + + #[allow(non_snake_case)] + pub fn Resize(size: Bounds) -> Self { + Self { + tag: RocEventTag::Resize, + entry: RocEventEntry { resize: size }, + } + } + + #[allow(non_snake_case)] + pub fn KeyDown(keycode: RocKeyCode) -> Self { + Self { + tag: RocEventTag::KeyDown, + entry: RocEventEntry { key_down: keycode }, + } + } + + #[allow(non_snake_case)] + pub fn KeyUp(keycode: RocKeyCode) -> Self { + Self { + tag: RocEventTag::KeyUp, + entry: RocEventEntry { key_up: keycode }, + } + } + + #[allow(non_snake_case)] + pub fn Tick(duration: Duration) -> Self { + Self { + tag: RocEventTag::Tick, + entry: RocEventEntry { + tick: duration.as_nanos().to_ne_bytes(), + }, + } + } +} + +#[repr(u8)] +#[allow(unused)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum RocKeyCode { + Down = 0, + Left, + Other, + Right, + Up, +} + +impl From for RocKeyCode { + fn from(keycode: VirtualKeyCode) -> Self { + use VirtualKeyCode::*; + + match keycode { + Left => RocKeyCode::Left, + Right => RocKeyCode::Right, + Up => RocKeyCode::Up, + Down => RocKeyCode::Down, + _ => RocKeyCode::Other, + } + } +} + +#[no_mangle] +pub unsafe extern "C" fn roc_alloc(size: usize, _alignment: u32) -> *mut c_void { + return libc::malloc(size); +} + +#[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 { + return libc::realloc(c_ptr, new_size); +} + +#[no_mangle] +pub unsafe extern "C" fn roc_dealloc(c_ptr: *mut c_void, _alignment: u32) { + return libc::free(c_ptr); +} + +#[no_mangle] +pub unsafe extern "C" fn roc_panic(msg: *mut RocStr, tag_id: u32) { + match tag_id { + 0 => { + eprintln!("Roc standard library hit a panic: {}", &*msg); + } + 1 => { + eprintln!("Application hit a panic: {}", &*msg); + } + _ => unreachable!(), + } + std::process::exit(1); +} + +#[no_mangle] +pub unsafe extern "C" fn roc_dbg(loc: *mut RocStr, msg: *mut RocStr) { + eprintln!("[{}] {}", &*loc, &*msg); +} + +#[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) +} + +#[repr(transparent)] +#[cfg(target_pointer_width = "64")] // on a 64-bit system, the tag fits in this pointer's spare 3 bits +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct ElemId(*const RocElemEntry); + +#[repr(C)] +pub union RocElemEntry { + pub rect: ManuallyDrop, + pub text: ManuallyDrop, +} + +#[repr(u8)] +#[allow(unused)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum RocElemTag { + Rect = 0, + Text = 1, +} + +#[repr(C)] +#[cfg(target_pointer_width = "64")] // on a 64-bit system, the tag fits in this pointer's spare 3 bits +pub struct RocElem { + entry: RocElemEntry, + tag: RocElemTag, +} + +impl Debug for RocElem { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + use RocElemTag::*; + + match self.tag() { + Rect => unsafe { &*self.entry().rect }.fmt(f), + Text => unsafe { &*self.entry().text }.fmt(f), + } + } +} + +impl RocElem { + #[cfg(target_pointer_width = "64")] + pub fn tag(&self) -> RocElemTag { + self.tag + } + + #[allow(unused)] + pub fn entry(&self) -> &RocElemEntry { + &self.entry + } + + #[allow(unused)] + pub fn rect(styles: ButtonStyles) -> RocElem { + todo!("restore rect() method") + // let rect = RocRect { styles }; + // let entry = RocElemEntry { + // rect: ManuallyDrop::new(rect), + // }; + + // Self::elem_from_tag(entry, RocElemTag::Rect) + } + + #[allow(unused)] + pub fn text>(into_roc_str: T) -> RocElem { + todo!("TODO restore text method") + // let entry = RocElemEntry { + // text: ManuallyDrop::new(into_roc_str.into()), + // }; + + // Self::elem_from_tag(entry, RocElemTag::Text) + } +} + +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub struct RocRect { + pub color: Rgba, + + // These must be in this order for alphabetization! + pub height: f32, + pub left: f32, + pub top: f32, + pub width: f32, +} + +#[repr(C)] +#[derive(Debug, Clone)] +pub struct RocText { + pub text: RocStr, + pub color: Rgba, + pub left: f32, + pub size: f32, + pub top: f32, +} + +impl Clone for RocElem { + fn clone(&self) -> Self { + unsafe { + match self.tag() { + RocElemTag::Rect => Self { + tag: RocElemTag::Rect, + entry: RocElemEntry { + rect: self.entry.rect.clone(), + }, + }, + RocElemTag::Text => Self { + tag: RocElemTag::Text, + entry: RocElemEntry { + text: self.entry.text.clone(), + }, + }, + } + } + } +} + +impl Drop for RocElem { + fn drop(&mut self) { + unsafe { + match self.tag() { + RocElemTag::Rect => mem::drop(ManuallyDrop::take(&mut self.entry.rect)), + RocElemTag::Text => mem::drop(ManuallyDrop::take(&mut self.entry.text)), + } + } + } +} + +#[repr(C)] +#[derive(Copy, Clone, Debug, Default)] +pub struct ButtonStyles { + pub bg_color: Rgba, + pub border_color: Rgba, + pub border_width: f32, + pub text_color: Rgba, +} + +#[derive(Copy, Clone, Debug, Default)] +#[repr(C)] +pub struct Bounds { + pub height: f32, + pub width: f32, +} + +type Model = c_void; + +/// Call the app's init function, then render and return that result +pub fn init_and_render(bounds: Bounds) -> (*const Model, RocList) { + let closure_data_buf; + let closure_layout; + + // Call init to get the initial model + let model = unsafe { + let ret_val_layout = Layout::array::(init_result_size() as usize).unwrap(); + + // TODO allocate on the stack if it's under a certain size + let ret_val_buf = std::alloc::alloc(ret_val_layout) as *mut Model; + + closure_layout = Layout::array::(init_size() as usize).unwrap(); + + // TODO allocate on the stack if it's under a certain size + closure_data_buf = std::alloc::alloc(closure_layout); + + call_init(&bounds, closure_data_buf, ret_val_buf); + + ret_val_buf + }; + + // Call render passing the model to get the initial Elems + let elems = unsafe { + let mut ret_val: MaybeUninit> = MaybeUninit::uninit(); + + // Reuse the buffer from the previous closure if possible + let closure_data_buf = + std::alloc::realloc(closure_data_buf, closure_layout, roc_render_size() as usize); + + call_render(model, closure_data_buf, ret_val.as_mut_ptr()); + + std::alloc::dealloc(closure_data_buf, closure_layout); + + ret_val.assume_init() + }; + + (model, elems) +} + +/// Call the app's update function, then render and return that result +pub fn update(model: *const Model, event: RocEvent) -> *const Model { + let closure_data_buf; + let closure_layout; + + // Call update to get the new model + unsafe { + let ret_val_layout = Layout::array::(update_result_size() as usize).unwrap(); + + // TODO allocate on the stack if it's under a certain size + let ret_val_buf = std::alloc::alloc(ret_val_layout) as *mut Model; + + closure_layout = Layout::array::(update_size() as usize).unwrap(); + + // TODO allocate on the stack if it's under a certain size + closure_data_buf = std::alloc::alloc(closure_layout); + + call_update(model, &event, closure_data_buf, ret_val_buf); + + ret_val_buf + } +} + +/// Call the app's update function, then render and return that result +pub fn update_and_render(model: *const Model, event: RocEvent) -> (*const Model, RocList) { + let closure_data_buf; + let closure_layout; + + // Call update to get the new model + let model = unsafe { + let ret_val_layout = Layout::array::(update_result_size() as usize).unwrap(); + + // TODO allocate on the stack if it's under a certain size + let ret_val_buf = std::alloc::alloc(ret_val_layout) as *mut Model; + + closure_layout = Layout::array::(update_size() as usize).unwrap(); + + // TODO allocate on the stack if it's under a certain size + closure_data_buf = std::alloc::alloc(closure_layout); + + call_update(model, &event, closure_data_buf, ret_val_buf); + + ret_val_buf + }; + + // Call render passing the model to get the initial Elems + let elems = unsafe { + let mut ret_val: MaybeUninit> = MaybeUninit::uninit(); + + // Reuse the buffer from the previous closure if possible + let closure_data_buf = + std::alloc::realloc(closure_data_buf, closure_layout, roc_render_size() as usize); + + call_render(model, closure_data_buf, ret_val.as_mut_ptr()); + + std::alloc::dealloc(closure_data_buf, closure_layout); + + ret_val.assume_init() + }; + + (model, elems) +} diff --git a/examples/gui/hello-guiBROKEN.roc b/examples/gui/hello-guiBROKEN.roc new file mode 100644 index 0000000000..efb4d68297 --- /dev/null +++ b/examples/gui/hello-guiBROKEN.roc @@ -0,0 +1,19 @@ +app "hello-gui" + packages { pf: "platform/main.roc" } + imports [] # [pf.Action.{ Action }, pf.Elem.{ button, text, row, col }] + provides [render] to pf + +render = + rgba = \r, g, b, a -> { r: r / 255, g: g / 255, b: b / 255, a } + + styles = { bgColor: rgba 100 50 50 1, borderColor: rgba 10 20 30 1, borderWidth: 10, textColor: rgba 220 220 250 1 } + + Col [ + Row [ + Button (Text "Corner ") styles, + Button (Text "Top Mid ") { styles & bgColor: rgba 100 100 50 1 }, + Button (Text "Top Right ") { styles & bgColor: rgba 50 50 150 1 }, + ], + Button (Text "Mid Left ") { styles & bgColor: rgba 150 100 100 1 }, + Button (Text "Bottom Left") { styles & bgColor: rgba 150 50 50 1 }, + ] diff --git a/examples/gui/platform/Action.roc b/examples/gui/platform/Action.roc index 1a499db63f..6a5e019c73 100644 --- a/examples/gui/platform/Action.roc +++ b/examples/gui/platform/Action.roc @@ -13,7 +13,5 @@ update = Update map : Action a, (a -> b) -> Action b map = \action, transform -> when action is - None -> - None - Update state -> - Update (transform state) + None -> None + Update state -> Update (transform state) diff --git a/examples/gui/platform/Cargo.lock b/examples/gui/platform/Cargo.lock deleted file mode 100644 index 8f8005c26d..0000000000 --- a/examples/gui/platform/Cargo.lock +++ /dev/null @@ -1,2835 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 3 - -[[package]] -name = "ab_glyph" -version = "0.2.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61caed9aec6daeee1ea38ccf5fb225e4f96c1eeead1b4a5c267324a63cf02326" -dependencies = [ - "ab_glyph_rasterizer", - "owned_ttf_parser", -] - -[[package]] -name = "ab_glyph_rasterizer" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a13739d7177fbd22bb0ed28badfff9f372f8bef46c863db4e1c6248f6b223b6e" - -[[package]] -name = "addr2line" -version = "0.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9ecd88a8c8378ca913a680cd98f0f13ac67383d35993f86c90a70e3f137816b" -dependencies = [ - "gimli", -] - -[[package]] -name = "adler" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" - -[[package]] -name = "ahash" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" -dependencies = [ - "getrandom", - "once_cell", - "version_check", -] - -[[package]] -name = "aho-corasick" -version = "0.7.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" -dependencies = [ - "memchr", -] - -[[package]] -name = "alsa" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75c4da790adcb2ce5e758c064b4f3ec17a30349f9961d3e5e6c9688b052a9e18" -dependencies = [ - "alsa-sys", - "bitflags", - "libc", - "nix 0.20.0", -] - -[[package]] -name = "alsa-sys" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db8fee663d06c4e303404ef5f40488a53e062f89ba8bfed81f42325aafad1527" -dependencies = [ - "libc", - "pkg-config", -] - -[[package]] -name = "approx" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f2a05fd1bd10b2527e20a2cd32d8873d115b8b39fe219ee25f42a8aca6ba278" -dependencies = [ - "num-traits", -] - -[[package]] -name = "approx" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "072df7202e63b127ab55acfe16ce97013d5b97bf160489336d3f1840fd78e99e" -dependencies = [ - "num-traits", -] - -[[package]] -name = "arrayvec" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6" - -[[package]] -name = "ash" -version = "0.35.1+1.2.203" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7fd04def1c9101b5fb488c131022d2d6f87753ef4b1b11b279e2af404fae6b9" -dependencies = [ - "libloading", -] - -[[package]] -name = "atty" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" -dependencies = [ - "hermit-abi", - "libc", - "winapi", -] - -[[package]] -name = "autocfg" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" - -[[package]] -name = "backtrace" -version = "0.3.63" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "321629d8ba6513061f26707241fa9bc89524ff1cd7a915a97ef0c62c666ce1b6" -dependencies = [ - "addr2line", - "cc", - "cfg-if 1.0.0", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", -] - -[[package]] -name = "bindgen" -version = "0.56.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2da379dbebc0b76ef63ca68d8fc6e71c0f13e59432e0987e508c1820e6ab5239" -dependencies = [ - "bitflags", - "cexpr", - "clang-sys", - "lazy_static", - "lazycell", - "peeking_take_while", - "proc-macro2", - "quote", - "regex", - "rustc-hash", - "shlex", -] - -[[package]] -name = "bit-set" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e11e16035ea35e4e5997b393eacbf6f63983188f7a2ad25bfb13465f5ad59de" -dependencies = [ - "bit-vec", -] - -[[package]] -name = "bit-vec" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" - -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "block" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" - -[[package]] -name = "block-buffer" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0940dc441f31689269e10ac70eb1002a3a1d3ad1390e030043662eb7fe4688b" -dependencies = [ - "block-padding", - "byte-tools", - "byteorder", - "generic-array", -] - -[[package]] -name = "block-padding" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa79dedbb091f449f1f39e53edf88d5dbe95f895dae6135a8d7b881fb5af73f5" -dependencies = [ - "byte-tools", -] - -[[package]] -name = "bumpalo" -version = "3.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4a45a46ab1f2412e53d3a0ade76ffad2025804294569aae387231a0cd6e0899" - -[[package]] -name = "byte-tools" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7" - -[[package]] -name = "bytemuck" -version = "1.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439989e6b8c38d1b6570a384ef1e49c8848128f5a97f3914baef02920842712f" -dependencies = [ - "bytemuck_derive", -] - -[[package]] -name = "bytemuck_derive" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e215f8c2f9f79cb53c8335e687ffd07d5bfcb6fe5fc80723762d0be46e7cc54" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "byteorder" -version = "1.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" - -[[package]] -name = "bytes" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" - -[[package]] -name = "calloop" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf2eec61efe56aa1e813f5126959296933cf0700030e4314786c48779a66ab82" -dependencies = [ - "log", - "nix 0.22.0", -] - -[[package]] -name = "cc" -version = "1.0.72" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22a9137b95ea06864e018375b72adfb7db6e6f68cfc8df5a04d00288050485ee" -dependencies = [ - "jobserver", -] - -[[package]] -name = "cesu8" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" - -[[package]] -name = "cexpr" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4aedb84272dbe89af497cf81375129abda4fc0a9e7c5d317498c15cc30c0d27" -dependencies = [ - "nom 5.1.2", -] - -[[package]] -name = "cfg-if" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" - -[[package]] -name = "cfg-if" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" - -[[package]] -name = "cfg_aliases" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" - -[[package]] -name = "cgmath" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a98d30140e3296250832bbaaff83b27dcd6fa3cc70fb6f1f3e5c9c0023b5317" -dependencies = [ - "approx 0.4.0", - "num-traits", -] - -[[package]] -name = "clang-sys" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa66045b9cb23c2e9c1520732030608b02ee07e5cfaa5a521ec15ded7fa24c90" -dependencies = [ - "glob", - "libc", - "libloading", -] - -[[package]] -name = "claxon" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bfbf56724aa9eca8afa4fcfadeb479e722935bb2a0900c2d37e0cc477af0688" - -[[package]] -name = "clipboard-win" -version = "3.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fdf5e01086b6be750428ba4a40619f847eb2e95756eee84b18e06e5f0b50342" -dependencies = [ - "lazy-bytes-cast", - "winapi", -] - -[[package]] -name = "cocoa" -version = "0.24.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f63902e9223530efb4e26ccd0cf55ec30d592d3b42e21a28defc42a9586e832" -dependencies = [ - "bitflags", - "block", - "cocoa-foundation", - "core-foundation 0.9.2", - "core-graphics 0.22.3", - "foreign-types", - "libc", - "objc", -] - -[[package]] -name = "cocoa-foundation" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ade49b65d560ca58c403a479bb396592b155c0185eada742ee323d1d68d6318" -dependencies = [ - "bitflags", - "block", - "core-foundation 0.9.2", - "core-graphics-types", - "foreign-types", - "libc", - "objc", -] - -[[package]] -name = "codespan-reporting" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" -dependencies = [ - "termcolor", - "unicode-width", -] - -[[package]] -name = "colored" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3616f750b84d8f0de8a58bda93e08e2a81ad3f523089b05f1dffecab48c6cbd" -dependencies = [ - "atty", - "lazy_static", - "winapi", -] - -[[package]] -name = "combine" -version = "4.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50b727aacc797f9fc28e355d21f34709ac4fc9adecfe470ad07b8f4464f53062" -dependencies = [ - "bytes", - "memchr", -] - -[[package]] -name = "confy" -version = "0.4.0" -source = "git+https://github.com/rust-cli/confy#664992aecd97b4af0eda8d9d2825885662e1c6b4" -dependencies = [ - "directories-next", - "serde", - "serde_yaml", -] - -[[package]] -name = "copyless" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2df960f5d869b2dd8532793fde43eb5427cceb126c929747a26823ab0eeb536" - -[[package]] -name = "copypasta" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4423d79fed83ebd9ab81ec21fa97144300a961782158287dc9bf7eddac37ff0b" -dependencies = [ - "clipboard-win", - "objc", - "objc-foundation", - "objc_id", - "smithay-clipboard", - "x11-clipboard", -] - -[[package]] -name = "core-foundation" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57d24c7a13c43e870e37c1556b74555437870a04514f7685f5b354e090567171" -dependencies = [ - "core-foundation-sys 0.7.0", - "libc", -] - -[[package]] -name = "core-foundation" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6888e10551bb93e424d8df1d07f1a8b4fceb0001a3a4b048bfc47554946f47b3" -dependencies = [ - "core-foundation-sys 0.8.3", - "libc", -] - -[[package]] -name = "core-foundation-sys" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3a71ab494c0b5b860bdc8407ae08978052417070c2ced38573a9157ad75b8ac" - -[[package]] -name = "core-foundation-sys" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" - -[[package]] -name = "core-graphics" -version = "0.19.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3889374e6ea6ab25dba90bb5d96202f61108058361f6dc72e8b03e6f8bbe923" -dependencies = [ - "bitflags", - "core-foundation 0.7.0", - "foreign-types", - "libc", -] - -[[package]] -name = "core-graphics" -version = "0.22.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2581bbab3b8ffc6fcbd550bf46c355135d16e9ff2a6ea032ad6b9bf1d7efe4fb" -dependencies = [ - "bitflags", - "core-foundation 0.9.2", - "core-graphics-types", - "foreign-types", - "libc", -] - -[[package]] -name = "core-graphics-types" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a68b68b3446082644c91ac778bf50cd4104bfb002b5a6a7c44cca5a2c70788b" -dependencies = [ - "bitflags", - "core-foundation 0.9.2", - "foreign-types", - "libc", -] - -[[package]] -name = "core-video-sys" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34ecad23610ad9757664d644e369246edde1803fcb43ed72876565098a5d3828" -dependencies = [ - "cfg-if 0.1.10", - "core-foundation-sys 0.7.0", - "core-graphics 0.19.2", - "libc", - "objc", -] - -[[package]] -name = "coreaudio-rs" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11894b20ebfe1ff903cbdc52259693389eea03b94918a2def2c30c3bf227ad88" -dependencies = [ - "bitflags", - "coreaudio-sys", -] - -[[package]] -name = "coreaudio-sys" -version = "0.2.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b7e3347be6a09b46aba228d6608386739fb70beff4f61e07422da87b0bb31fa" -dependencies = [ - "bindgen", -] - -[[package]] -name = "cpal" -version = "0.13.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98f45f0a21f617cd2c788889ef710b63f075c949259593ea09c826f1e47a2418" -dependencies = [ - "alsa", - "core-foundation-sys 0.8.3", - "coreaudio-rs", - "jni", - "js-sys", - "lazy_static", - "libc", - "mach", - "ndk 0.3.0", - "ndk-glue 0.3.0", - "nix 0.20.0", - "oboe", - "parking_lot", - "stdweb", - "thiserror", - "web-sys", - "winapi", -] - -[[package]] -name = "crossbeam-channel" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e54ea8bc3fb1ee042f5aace6e3c6e025d3874866da222930f70ce62aceba0bfa" -dependencies = [ - "cfg-if 1.0.0", - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-deque" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6455c0ca19f0d2fbf751b908d5c55c1f5cbc65e03c4225427254b46890bdde1e" -dependencies = [ - "cfg-if 1.0.0", - "crossbeam-epoch", - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-epoch" -version = "0.9.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97242a70df9b89a65d0b6df3c4bf5b9ce03c5b7309019777fbde37e7537f8762" -dependencies = [ - "cfg-if 1.0.0", - "crossbeam-utils", - "lazy_static", - "memoffset", - "scopeguard", -] - -[[package]] -name = "crossbeam-utils" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcae03edb34f947e64acdb1c33ec169824e20657e9ecb61cef6c8c74dcb8120" -dependencies = [ - "cfg-if 1.0.0", - "lazy_static", -] - -[[package]] -name = "cty" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b365fabc795046672053e29c954733ec3b05e4be654ab130fe8f1f94d7051f35" - -[[package]] -name = "d3d12" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2daefd788d1e96e0a9d66dee4b828b883509bc3ea9ce30665f04c3246372690c" -dependencies = [ - "bitflags", - "libloading", - "winapi", -] - -[[package]] -name = "darling" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d706e75d87e35569db781a9b5e2416cff1236a47ed380831f959382ccd5f858" -dependencies = [ - "darling_core 0.10.2", - "darling_macro 0.10.2", -] - -[[package]] -name = "darling" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0d720b8683f8dd83c65155f0530560cba68cd2bf395f6513a483caee57ff7f4" -dependencies = [ - "darling_core 0.13.1", - "darling_macro 0.13.1", -] - -[[package]] -name = "darling_core" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0c960ae2da4de88a91b2d920c2a7233b400bc33cb28453a2987822d8392519b" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim 0.9.3", - "syn", -] - -[[package]] -name = "darling_core" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a340f241d2ceed1deb47ae36c4144b2707ec7dd0b649f894cb39bb595986324" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim 0.10.0", - "syn", -] - -[[package]] -name = "darling_macro" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b5a2f4ac4969822c62224815d069952656cadc7084fdca9751e6d959189b72" -dependencies = [ - "darling_core 0.10.2", - "quote", - "syn", -] - -[[package]] -name = "darling_macro" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72c41b3b7352feb3211a0d743dc5700a4e3b60f51bd2b368892d1e0f9a95f44b" -dependencies = [ - "darling_core 0.13.1", - "quote", - "syn", -] - -[[package]] -name = "digest" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5" -dependencies = [ - "generic-array", -] - -[[package]] -name = "directories-next" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "339ee130d97a610ea5a5872d2bbb130fdf68884ff09d3028b81bec8a1ac23bbc" -dependencies = [ - "cfg-if 1.0.0", - "dirs-sys-next", -] - -[[package]] -name = "dirs-sys-next" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" -dependencies = [ - "libc", - "redox_users", - "winapi", -] - -[[package]] -name = "dispatch" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" - -[[package]] -name = "dlib" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac1b7517328c04c2aa68422fc60a41b92208182142ed04a25879c26c8f878794" -dependencies = [ - "libloading", -] - -[[package]] -name = "doc-comment" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" - -[[package]] -name = "downcast-rs" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650" - -[[package]] -name = "either" -version = "1.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" - -[[package]] -name = "env_logger" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b2cf0344971ee6c64c31be0d530793fba457d322dfec2810c453d0ef228f9c3" -dependencies = [ - "atty", - "humantime", - "log", - "regex", - "termcolor", -] - -[[package]] -name = "fake-simd" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed" - -[[package]] -name = "find-crate" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59a98bbaacea1c0eb6a0876280051b892eb73594fd90cf3b20e9c817029c57d2" -dependencies = [ - "toml", -] - -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - -[[package]] -name = "foreign-types" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" -dependencies = [ - "foreign-types-shared", -] - -[[package]] -name = "foreign-types-shared" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" - -[[package]] -name = "fs_extra" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2022715d62ab30faffd124d40b76f4134a550a87792276512b18d63272333394" - -[[package]] -name = "futures" -version = "0.3.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28560757fe2bb34e79f907794bb6b22ae8b0e5c669b638a1132f2592b19035b4" -dependencies = [ - "futures-channel", - "futures-core", - "futures-executor", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-channel" -version = "0.3.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba3dda0b6588335f360afc675d0564c17a77a2bda81ca178a4b6081bd86c7f0b" -dependencies = [ - "futures-core", - "futures-sink", -] - -[[package]] -name = "futures-core" -version = "0.3.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0c8ff0461b82559810cdccfde3215c3f373807f5e5232b71479bff7bb2583d7" - -[[package]] -name = "futures-executor" -version = "0.3.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29d6d2ff5bb10fb95c85b8ce46538a2e5f5e7fdc755623a7d4529ab8a4ed9d2a" -dependencies = [ - "futures-core", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-io" -version = "0.3.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1f9d34af5a1aac6fb380f735fe510746c38067c5bf16c7fd250280503c971b2" - -[[package]] -name = "futures-macro" -version = "0.3.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dbd947adfffb0efc70599b3ddcf7b5597bb5fa9e245eb99f62b3a5f7bb8bd3c" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "futures-sink" -version = "0.3.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3055baccb68d74ff6480350f8d6eb8fcfa3aa11bdc1a1ae3afdd0514617d508" - -[[package]] -name = "futures-task" -version = "0.3.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ee7c6485c30167ce4dfb83ac568a849fe53274c831081476ee13e0dce1aad72" - -[[package]] -name = "futures-util" -version = "0.3.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b5cf40b47a271f77a8b1bec03ca09044d99d2372c0de244e66430761127164" -dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-macro", - "futures-sink", - "futures-task", - "memchr", - "pin-project-lite", - "pin-utils", - "slab", -] - -[[package]] -name = "fxhash" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" -dependencies = [ - "byteorder", -] - -[[package]] -name = "generic-array" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffdf9f34f1447443d37393cc6c2b8313aebddcd96906caf34e54c68d8e57d7bd" -dependencies = [ - "typenum", -] - -[[package]] -name = "getrandom" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "418d37c8b1d42553c93648be529cb70f920d3baf8ef469b74b9638df426e0b4c" -dependencies = [ - "cfg-if 1.0.0", - "libc", - "wasi", -] - -[[package]] -name = "gimli" -version = "0.26.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78cc372d058dcf6d5ecd98510e7fbc9e5aec4d21de70f65fea8fecebcd881bd4" - -[[package]] -name = "glob" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" - -[[package]] -name = "glow" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8bd5877156a19b8ac83a29b2306fe20537429d318f3ff0a1a2119f8d9c61919" -dependencies = [ - "js-sys", - "slotmap", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "glyph_brush" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21932fbf719272848eec4583740d978203c6e7da4c4e203358f5b95946c97409" -dependencies = [ - "glyph_brush_draw_cache", - "glyph_brush_layout", - "log", - "ordered-float", - "rustc-hash", - "twox-hash", -] - -[[package]] -name = "glyph_brush_draw_cache" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6010675390f6889e09a21e2c8b575b3ee25667ea8237a8d59423f73cb8c28610" -dependencies = [ - "ab_glyph", - "crossbeam-channel", - "crossbeam-deque", - "linked-hash-map", - "rayon", - "rustc-hash", -] - -[[package]] -name = "glyph_brush_layout" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc32c2334f00ca5ac3695c5009ae35da21da8c62d255b5b96d56e2597a637a38" -dependencies = [ - "ab_glyph", - "approx 0.5.0", - "xi-unicode", -] - -[[package]] -name = "gpu-alloc" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fc59e5f710e310e76e6707f86c561dd646f69a8876da9131703b2f717de818d" -dependencies = [ - "bitflags", - "gpu-alloc-types", -] - -[[package]] -name = "gpu-alloc-types" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54804d0d6bc9d7f26db4eaec1ad10def69b599315f487d32c334a80d1efe67a5" -dependencies = [ - "bitflags", -] - -[[package]] -name = "gpu-descriptor" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a538f217be4d405ff4719a283ca68323cc2384003eca5baaa87501e821c81dda" -dependencies = [ - "bitflags", - "gpu-descriptor-types", - "hashbrown", -] - -[[package]] -name = "gpu-descriptor-types" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "363e3677e55ad168fef68cf9de3a4a310b53124c5e784c53a1d70e92d23f2126" -dependencies = [ - "bitflags", -] - -[[package]] -name = "hashbrown" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" -dependencies = [ - "ahash", -] - -[[package]] -name = "hermit-abi" -version = "0.1.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" -dependencies = [ - "libc", -] - -[[package]] -name = "hexf-parse" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" - -[[package]] -name = "host" -version = "0.1.0" -dependencies = [ - "arrayvec", - "bytemuck", - "cgmath", - "colored", - "confy", - "copypasta", - "env_logger", - "fs_extra", - "futures", - "glyph_brush", - "libc", - "log", - "nonempty", - "page_size", - "palette", - "pest", - "pest_derive", - "roc_std", - "rodio", - "serde", - "snafu", - "threadpool", - "wgpu", - "wgpu_glyph", - "winit", -] - -[[package]] -name = "hound" -version = "3.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a164bb2ceaeff4f42542bdb847c41517c78a60f5649671b2a07312b6e117549" - -[[package]] -name = "humantime" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" - -[[package]] -name = "ident_case" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" - -[[package]] -name = "indexmap" -version = "1.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282a6247722caba404c065016bbfa522806e51714c34f5dfc3e4a3a46fcb4223" -dependencies = [ - "autocfg", - "hashbrown", -] - -[[package]] -name = "inplace_it" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90953f308a79fe6d62a4643e51f848fbfddcd05975a38e69fdf4ab86a7baf7ca" - -[[package]] -name = "instant" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" -dependencies = [ - "cfg-if 1.0.0", - "js-sys", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "jni" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6df18c2e3db7e453d3c6ac5b3e9d5182664d28788126d39b91f2d1e22b017ec" -dependencies = [ - "cesu8", - "combine", - "jni-sys", - "log", - "thiserror", - "walkdir", -] - -[[package]] -name = "jni-sys" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" - -[[package]] -name = "jobserver" -version = "0.1.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af25a77299a7f711a01975c35a6a424eb6862092cc2d6c72c4ed6cbc56dfc1fa" -dependencies = [ - "libc", -] - -[[package]] -name = "js-sys" -version = "0.3.56" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a38fc24e30fd564ce974c02bf1d337caddff65be6cc4735a1f7eab22a7440f04" -dependencies = [ - "wasm-bindgen", -] - -[[package]] -name = "khronos-egl" -version = "4.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c2352bd1d0bceb871cb9d40f24360c8133c11d7486b68b5381c1dd1a32015e3" -dependencies = [ - "libc", - "libloading", - "pkg-config", -] - -[[package]] -name = "lazy-bytes-cast" -version = "5.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10257499f089cd156ad82d0a9cd57d9501fa2c989068992a97eb3c27836f206b" - -[[package]] -name = "lazy_static" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" - -[[package]] -name = "lazycell" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" - -[[package]] -name = "lewton" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "777b48df9aaab155475a83a7df3070395ea1ac6902f5cd062b8f2b028075c030" -dependencies = [ - "byteorder", - "ogg", - "tinyvec", -] - -[[package]] -name = "libc" -version = "0.2.113" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eef78b64d87775463c549fbd80e19249ef436ea3bf1de2a1eb7e717ec7fab1e9" - -[[package]] -name = "libloading" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efbc0f03f9a775e9f6aed295c6a1ba2253c5757a9e03d55c6caa46a681abcddd" -dependencies = [ - "cfg-if 1.0.0", - "winapi", -] - -[[package]] -name = "linked-hash-map" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3" - -[[package]] -name = "lock_api" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712a4d093c9976e24e7dbca41db895dabcbac38eb5f4045393d17a95bdfb1109" -dependencies = [ - "scopeguard", -] - -[[package]] -name = "log" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" -dependencies = [ - "cfg-if 1.0.0", -] - -[[package]] -name = "mach" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b823e83b2affd8f40a9ee8c29dbc56404c1e34cd2710921f2801e2cf29527afa" -dependencies = [ - "libc", -] - -[[package]] -name = "malloc_buf" -version = "0.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" -dependencies = [ - "libc", -] - -[[package]] -name = "maplit" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" - -[[package]] -name = "memchr" -version = "2.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" - -[[package]] -name = "memmap2" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b6c2ebff6180198788f5db08d7ce3bc1d0b617176678831a7510825973e357" -dependencies = [ - "libc", -] - -[[package]] -name = "memoffset" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" -dependencies = [ - "autocfg", -] - -[[package]] -name = "metal" -version = "0.23.1" -source = "git+https://github.com/gfx-rs/metal-rs?rev=44af5cc#44af5cca340617d42d701264f9bf71d1f3e68096" -dependencies = [ - "bitflags", - "block", - "core-graphics-types", - "foreign-types", - "log", - "objc", -] - -[[package]] -name = "minimal-lexical" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" - -[[package]] -name = "minimp3" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "985438f75febf74c392071a975a29641b420dd84431135a6e6db721de4b74372" -dependencies = [ - "minimp3-sys", - "slice-deque", - "thiserror", -] - -[[package]] -name = "minimp3-sys" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e21c73734c69dc95696c9ed8926a2b393171d98b3f5f5935686a26a487ab9b90" -dependencies = [ - "cc", -] - -[[package]] -name = "miniz_oxide" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a92518e98c078586bc6c934028adcca4c92a53d6a958196de835170a01d84e4b" -dependencies = [ - "adler", - "autocfg", -] - -[[package]] -name = "mio" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba272f85fa0b41fc91872be579b3bbe0f56b792aa361a380eb669469f68dafb2" -dependencies = [ - "libc", - "log", - "miow", - "ntapi", - "winapi", -] - -[[package]] -name = "miow" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21" -dependencies = [ - "winapi", -] - -[[package]] -name = "naga" -version = "0.8.0" -source = "git+https://github.com/gfx-rs/naga?rev=8e2e39e#8e2e39e4d8fa5bbb657c3b170b4f6607d703e284" -dependencies = [ - "bit-set", - "bitflags", - "codespan-reporting", - "hexf-parse", - "indexmap", - "log", - "num-traits", - "rustc-hash", - "spirv", - "thiserror", -] - -[[package]] -name = "ndk" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8794322172319b972f528bf90c6b467be0079f1fa82780ffb431088e741a73ab" -dependencies = [ - "jni-sys", - "ndk-sys 0.2.2", - "num_enum", - "thiserror", -] - -[[package]] -name = "ndk" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96d868f654c72e75f8687572699cdabe755f03effbb62542768e995d5b8d699d" -dependencies = [ - "bitflags", - "jni-sys", - "ndk-sys 0.2.2", - "num_enum", - "thiserror", -] - -[[package]] -name = "ndk" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2032c77e030ddee34a6787a64166008da93f6a352b629261d0fee232b8742dd4" -dependencies = [ - "bitflags", - "jni-sys", - "ndk-sys 0.3.0", - "num_enum", - "thiserror", -] - -[[package]] -name = "ndk-glue" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5caf0c24d51ac1c905c27d4eda4fa0635bbe0de596b8f79235e0b17a4d29385" -dependencies = [ - "lazy_static", - "libc", - "log", - "ndk 0.3.0", - "ndk-macro 0.2.0", - "ndk-sys 0.2.2", -] - -[[package]] -name = "ndk-glue" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc291b8de2095cba8dab7cf381bf582ff4c17a09acf854c32e46545b08085d28" -dependencies = [ - "lazy_static", - "libc", - "log", - "ndk 0.5.0", - "ndk-macro 0.3.0", - "ndk-sys 0.2.2", -] - -[[package]] -name = "ndk-glue" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04c0d14b0858eb9962a5dac30b809b19f19da7e4547d64af2b0bb051d2e55d79" -dependencies = [ - "lazy_static", - "libc", - "log", - "ndk 0.6.0", - "ndk-macro 0.3.0", - "ndk-sys 0.3.0", -] - -[[package]] -name = "ndk-macro" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05d1c6307dc424d0f65b9b06e94f88248e6305726b14729fd67a5e47b2dc481d" -dependencies = [ - "darling 0.10.2", - "proc-macro-crate 0.1.5", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "ndk-macro" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0df7ac00c4672f9d5aece54ee3347520b7e20f158656c7db2e6de01902eb7a6c" -dependencies = [ - "darling 0.13.1", - "proc-macro-crate 1.1.0", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "ndk-sys" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1bcdd74c20ad5d95aacd60ef9ba40fdf77f767051040541df557b7a9b2a2121" - -[[package]] -name = "ndk-sys" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e5a6ae77c8ee183dcbbba6150e2e6b9f3f4196a7666c02a715a95692ec1fa97" -dependencies = [ - "jni-sys", -] - -[[package]] -name = "nix" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa9b4819da1bc61c0ea48b63b7bc8604064dd43013e7cc325df098d49cd7c18a" -dependencies = [ - "bitflags", - "cc", - "cfg-if 1.0.0", - "libc", -] - -[[package]] -name = "nix" -version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf1e25ee6b412c2a1e3fcb6a4499a5c1bfe7f43e014bdce9a6b6666e5aa2d187" -dependencies = [ - "bitflags", - "cc", - "cfg-if 1.0.0", - "libc", - "memoffset", -] - -[[package]] -name = "nom" -version = "5.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffb4262d26ed83a1c0a33a38fe2bb15797329c85770da05e6b828ddb782627af" -dependencies = [ - "memchr", - "version_check", -] - -[[package]] -name = "nom" -version = "7.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b1d11e1ef389c76fe5b81bcaf2ea32cf88b62bc494e19f493d0b30e7a930109" -dependencies = [ - "memchr", - "minimal-lexical", - "version_check", -] - -[[package]] -name = "nonempty" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9e591e719385e6ebaeb5ce5d3887f7d5676fceca6411d1925ccc95745f3d6f7" - -[[package]] -name = "ntapi" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f6bb902e437b6d86e03cce10a7e2af662292c5dfef23b65899ea3ac9354ad44" -dependencies = [ - "winapi", -] - -[[package]] -name = "num-derive" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "num-traits" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" -dependencies = [ - "autocfg", -] - -[[package]] -name = "num_cpus" -version = "1.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1" -dependencies = [ - "hermit-abi", - "libc", -] - -[[package]] -name = "num_enum" -version = "0.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "720d3ea1055e4e4574c0c0b0f8c3fd4f24c4cdaf465948206dea090b57b526ad" -dependencies = [ - "num_enum_derive", -] - -[[package]] -name = "num_enum_derive" -version = "0.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d992b768490d7fe0d8586d9b5745f6c49f557da6d81dc982b1d167ad4edbb21" -dependencies = [ - "proc-macro-crate 1.1.0", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "objc" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" -dependencies = [ - "malloc_buf", - "objc_exception", -] - -[[package]] -name = "objc-foundation" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" -dependencies = [ - "block", - "objc", - "objc_id", -] - -[[package]] -name = "objc_exception" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad970fb455818ad6cba4c122ad012fae53ae8b4795f86378bce65e4f6bab2ca4" -dependencies = [ - "cc", -] - -[[package]] -name = "objc_id" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" -dependencies = [ - "objc", -] - -[[package]] -name = "object" -version = "0.27.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67ac1d3f9a1d3616fd9a60c8d74296f22406a238b6a72f5cc1e6f314df4ffbf9" -dependencies = [ - "memchr", -] - -[[package]] -name = "oboe" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2463c8f2e19b4e0d0710a21f8e4011501ff28db1c95d7a5482a553b2100502d2" -dependencies = [ - "jni", - "ndk 0.6.0", - "ndk-glue 0.6.0", - "num-derive", - "num-traits", - "oboe-sys", -] - -[[package]] -name = "oboe-sys" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3370abb7372ed744232c12954d920d1a40f1c4686de9e79e800021ef492294bd" -dependencies = [ - "cc", -] - -[[package]] -name = "ogg" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6951b4e8bf21c8193da321bcce9c9dd2e13c858fe078bf9054a288b419ae5d6e" -dependencies = [ - "byteorder", -] - -[[package]] -name = "once_cell" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da32515d9f6e6e489d7bc9d84c71b060db7247dc035bbe44eac88cf87486d8d5" - -[[package]] -name = "opaque-debug" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c" - -[[package]] -name = "ordered-float" -version = "2.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7940cf2ca942593318d07fcf2596cdca60a85c9e7fab408a5e21a4f9dcd40d87" -dependencies = [ - "num-traits", -] - -[[package]] -name = "owned_ttf_parser" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ef05f2882a8b3e7acc10c153ade2631f7bfc8ce00d2bf3fb8f4e9d2ae6ea5c3" -dependencies = [ - "ttf-parser", -] - -[[package]] -name = "page_size" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eebde548fbbf1ea81a99b128872779c437752fb99f217c45245e1a61dcd9edcd" -dependencies = [ - "libc", - "winapi", -] - -[[package]] -name = "palette" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9735f7e1e51a3f740bacd5dc2724b61a7806f23597a8736e679f38ee3435d18" -dependencies = [ - "approx 0.5.0", - "num-traits", - "palette_derive", - "phf", -] - -[[package]] -name = "palette_derive" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7799c3053ea8a6d8a1193c7ba42f534e7863cf52e378a7f90406f4a645d33bad" -dependencies = [ - "find-crate", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "parking_lot" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" -dependencies = [ - "instant", - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216" -dependencies = [ - "cfg-if 1.0.0", - "instant", - "libc", - "redox_syscall", - "smallvec", - "winapi", -] - -[[package]] -name = "peeking_take_while" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" - -[[package]] -name = "percent-encoding" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" - -[[package]] -name = "pest" -version = "2.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10f4872ae94d7b90ae48754df22fd42ad52ce740b8f370b03da4835417403e53" -dependencies = [ - "ucd-trie", -] - -[[package]] -name = "pest_derive" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "833d1ae558dc601e9a60366421196a8d94bc0ac980476d0b67e1d0988d72b2d0" -dependencies = [ - "pest", - "pest_generator", -] - -[[package]] -name = "pest_generator" -version = "2.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99b8db626e31e5b81787b9783425769681b347011cc59471e33ea46d2ea0cf55" -dependencies = [ - "pest", - "pest_meta", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "pest_meta" -version = "2.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54be6e404f5317079812fc8f9f5279de376d8856929e21c184ecf6bbd692a11d" -dependencies = [ - "maplit", - "pest", - "sha-1", -] - -[[package]] -name = "phf" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2ac8b67553a7ca9457ce0e526948cad581819238f4a9d1ea74545851fa24f37" -dependencies = [ - "phf_macros", - "phf_shared", - "proc-macro-hack", -] - -[[package]] -name = "phf_generator" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d43f3220d96e0080cc9ea234978ccd80d904eafb17be31bb0f76daaea6493082" -dependencies = [ - "phf_shared", - "rand", -] - -[[package]] -name = "phf_macros" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b706f5936eb50ed880ae3009395b43ed19db5bff2ebd459c95e7bf013a89ab86" -dependencies = [ - "phf_generator", - "phf_shared", - "proc-macro-hack", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "phf_shared" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a68318426de33640f02be62b4ae8eb1261be2efbc337b60c54d845bf4484e0d9" -dependencies = [ - "siphasher", -] - -[[package]] -name = "pin-project-lite" -version = "0.2.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e280fbe77cc62c91527259e9442153f4688736748d24660126286329742b4c6c" - -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - -[[package]] -name = "pkg-config" -version = "0.3.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58893f751c9b0412871a09abd62ecd2a00298c6c83befa223ef98c52aef40cbe" - -[[package]] -name = "ppv-lite86" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" - -[[package]] -name = "proc-macro-crate" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d6ea3c4595b96363c13943497db34af4460fb474a95c43f4446ad341b8c9785" -dependencies = [ - "toml", -] - -[[package]] -name = "proc-macro-crate" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ebace6889caf889b4d3f76becee12e90353f2b8c7d875534a71e5742f8f6f83" -dependencies = [ - "thiserror", - "toml", -] - -[[package]] -name = "proc-macro-hack" -version = "0.5.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5" - -[[package]] -name = "proc-macro2" -version = "1.0.36" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7342d5883fbccae1cc37a2353b09c87c9b0f3afd73f5fb9bba687a1f733b029" -dependencies = [ - "unicode-xid", -] - -[[package]] -name = "profiling" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9145ac0af1d93c638c98c40cf7d25665f427b2a44ad0a99b1dccf3e2f25bb987" - -[[package]] -name = "quick-xml" -version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8533f14c8382aaad0d592c812ac3b826162128b65662331e1127b45c3d18536b" -dependencies = [ - "memchr", -] - -[[package]] -name = "quote" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "864d3e96a899863136fc6e99f3d7cae289dafe43bf2c5ac19b70df7210c0a145" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "rand" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8" -dependencies = [ - "libc", - "rand_chacha", - "rand_core", - "rand_hc", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core", -] - -[[package]] -name = "rand_core" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" -dependencies = [ - "getrandom", -] - -[[package]] -name = "rand_hc" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d51e9f596de227fda2ea6c84607f5558e196eeaf43c986b724ba4fb8fdf497e7" -dependencies = [ - "rand_core", -] - -[[package]] -name = "range-alloc" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63e935c45e09cc6dcf00d2f0b2d630a58f4095320223d47fc68918722f0538b6" - -[[package]] -name = "raw-window-handle" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fba75eee94a9d5273a68c9e1e105d9cffe1ef700532325788389e5a83e2522b7" -dependencies = [ - "cty", -] - -[[package]] -name = "rayon" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c06aca804d41dbc8ba42dfd964f0d01334eceb64314b9ecf7c5fad5188a06d90" -dependencies = [ - "autocfg", - "crossbeam-deque", - "either", - "rayon-core", -] - -[[package]] -name = "rayon-core" -version = "1.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d78120e2c850279833f1dd3582f730c4ab53ed95aeaaaa862a2a5c71b1656d8e" -dependencies = [ - "crossbeam-channel", - "crossbeam-deque", - "crossbeam-utils", - "lazy_static", - "num_cpus", -] - -[[package]] -name = "redox_syscall" -version = "0.2.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff" -dependencies = [ - "bitflags", -] - -[[package]] -name = "redox_users" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "528532f3d801c87aec9def2add9ca802fe569e44a544afe633765267840abe64" -dependencies = [ - "getrandom", - "redox_syscall", -] - -[[package]] -name = "regex" -version = "1.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.6.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" - -[[package]] -name = "renderdoc-sys" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1382d1f0a252c4bf97dc20d979a2fdd05b024acd7c2ed0f7595d7817666a157" - -[[package]] -name = "roc_std" -version = "0.1.0" -dependencies = [ - "static_assertions 0.1.1", -] - -[[package]] -name = "rodio" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d98f5e557b61525057e2bc142c8cd7f0e70d75dc32852309bec440e6e046bf9" -dependencies = [ - "claxon", - "cpal", - "hound", - "lewton", - "minimp3", -] - -[[package]] -name = "rustc-demangle" -version = "0.1.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342" - -[[package]] -name = "rustc-hash" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" - -[[package]] -name = "ryu" -version = "1.0.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f" - -[[package]] -name = "same-file" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "scoped-tls" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea6a9290e3c9cf0f18145ef7ffa62d68ee0bf5fcd651017e586dc7fd5da448c2" - -[[package]] -name = "scopeguard" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" - -[[package]] -name = "serde" -version = "1.0.135" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cf9235533494ea2ddcdb794665461814781c53f19d87b76e571a1c35acbad2b" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.135" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8dcde03d87d4c973c04be249e7d8f0b35db1c848c487bd43032808e59dd8328d" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "serde_yaml" -version = "0.8.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4a521f2940385c165a24ee286aa8599633d162077a54bdcae2a6fd5a7bfa7a0" -dependencies = [ - "indexmap", - "ryu", - "serde", - "yaml-rust", -] - -[[package]] -name = "sha-1" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7d94d0bede923b3cea61f3f1ff57ff8cdfd77b400fb8f9998949e0cf04163df" -dependencies = [ - "block-buffer", - "digest", - "fake-simd", - "opaque-debug", -] - -[[package]] -name = "shlex" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fdf1b9db47230893d76faad238fd6097fd6d6a9245cd7a4d90dbd639536bbd2" - -[[package]] -name = "siphasher" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a86232ab60fa71287d7f2ddae4a7073f6b7aac33631c3015abb556f08c6d0a3e" - -[[package]] -name = "slab" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9def91fd1e018fe007022791f865d0ccc9b3a0d5001e01aabb8b40e46000afb5" - -[[package]] -name = "slice-deque" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31ef6ee280cdefba6d2d0b4b78a84a1c1a3f3a4cec98c2d4231c8bc225de0f25" -dependencies = [ - "libc", - "mach", - "winapi", -] - -[[package]] -name = "slotmap" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1e08e261d0e8f5c43123b7adf3e4ca1690d655377ac93a03b2c9d3e98de1342" -dependencies = [ - "version_check", -] - -[[package]] -name = "smallvec" -version = "1.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83" - -[[package]] -name = "smithay-client-toolkit" -version = "0.15.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1325f292209cee78d5035530932422a30aa4c8fda1a16593ac083c1de211e68a" -dependencies = [ - "bitflags", - "calloop", - "dlib", - "lazy_static", - "log", - "memmap2", - "nix 0.22.0", - "pkg-config", - "wayland-client", - "wayland-cursor", - "wayland-protocols", -] - -[[package]] -name = "smithay-clipboard" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "610b551bd25378bfd2b8e7a0fcbd83d427e8f2f6a40c47ae0f70688e9949dd55" -dependencies = [ - "smithay-client-toolkit", - "wayland-client", -] - -[[package]] -name = "snafu" -version = "0.6.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eab12d3c261b2308b0d80c26fffb58d17eba81a4be97890101f416b478c79ca7" -dependencies = [ - "backtrace", - "doc-comment", - "snafu-derive", -] - -[[package]] -name = "snafu-derive" -version = "0.6.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1508efa03c362e23817f96cde18abed596a25219a8b2c66e8db33c03543d315b" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "spirv" -version = "0.2.0+1.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "246bfa38fe3db3f1dfc8ca5a2cdeb7348c78be2112740cc0ec8ef18b6d94f830" -dependencies = [ - "bitflags", - "num-traits", -] - -[[package]] -name = "static_assertions" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f406d6ee68db6796e11ffd7b4d171864c58b7451e79ef9460ea33c287a1f89a7" - -[[package]] -name = "static_assertions" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" - -[[package]] -name = "stdweb" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef5430c8e36b713e13b48a9f709cc21e046723fe44ce34587b73a830203b533e" - -[[package]] -name = "strsim" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6446ced80d6c486436db5c078dde11a9f73d42b57fb273121e160b84f63d894c" - -[[package]] -name = "strsim" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" - -[[package]] -name = "syn" -version = "1.0.86" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a65b3f4ffa0092e9887669db0eae07941f023991ab58ea44da8fe8e2d511c6b" -dependencies = [ - "proc-macro2", - "quote", - "unicode-xid", -] - -[[package]] -name = "termcolor" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "thiserror" -version = "1.0.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417" -dependencies = [ - "thiserror-impl", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "threadpool" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa" -dependencies = [ - "num_cpus", -] - -[[package]] -name = "tinyvec" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c1c1d5a42b6245520c249549ec267180beaffcc0615401ac8e31853d4b6d8d2" -dependencies = [ - "tinyvec_macros", -] - -[[package]] -name = "tinyvec_macros" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" - -[[package]] -name = "toml" -version = "0.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa" -dependencies = [ - "serde", -] - -[[package]] -name = "ttf-parser" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ccbe8381883510b6a2d8f1e32905bddd178c11caef8083086d0c0c9ab0ac281" - -[[package]] -name = "twox-hash" -version = "1.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ee73e6e4924fe940354b8d4d98cad5231175d615cd855b758adc658c0aac6a0" -dependencies = [ - "cfg-if 1.0.0", - "rand", - "static_assertions 1.1.0", -] - -[[package]] -name = "typenum" -version = "1.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" - -[[package]] -name = "ucd-trie" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c" - -[[package]] -name = "unicode-width" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" - -[[package]] -name = "unicode-xid" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" - -[[package]] -name = "version_check" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" - -[[package]] -name = "walkdir" -version = "2.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" -dependencies = [ - "same-file", - "winapi", - "winapi-util", -] - -[[package]] -name = "wasi" -version = "0.10.2+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" - -[[package]] -name = "wasm-bindgen" -version = "0.2.79" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25f1af7423d8588a3d840681122e72e6a24ddbcb3f0ec385cac0d12d24256c06" -dependencies = [ - "cfg-if 1.0.0", - "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.79" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b21c0df030f5a177f3cba22e9bc4322695ec43e7257d865302900290bcdedca" -dependencies = [ - "bumpalo", - "lazy_static", - "log", - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-futures" -version = "0.4.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2eb6ec270a31b1d3c7e266b999739109abce8b6c87e4b31fcfcd788b65267395" -dependencies = [ - "cfg-if 1.0.0", - "js-sys", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.79" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f4203d69e40a52ee523b2529a773d5ffc1dc0071801c87b3d270b471b80ed01" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.79" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa8a30d46208db204854cadbb5d4baf5fcf8071ba5bf48190c3e59937962ebc" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-backend", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.79" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d958d035c4438e28c70e4321a2911302f10135ce78a9c7834c0cab4123d06a2" - -[[package]] -name = "wayland-client" -version = "0.29.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91223460e73257f697d9e23d401279123d36039a3f7a449e983f123292d4458f" -dependencies = [ - "bitflags", - "downcast-rs", - "libc", - "nix 0.22.0", - "scoped-tls", - "wayland-commons", - "wayland-scanner", - "wayland-sys", -] - -[[package]] -name = "wayland-commons" -version = "0.29.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94f6e5e340d7c13490eca867898c4cec5af56c27a5ffe5c80c6fc4708e22d33e" -dependencies = [ - "nix 0.22.0", - "once_cell", - "smallvec", - "wayland-sys", -] - -[[package]] -name = "wayland-cursor" -version = "0.29.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c52758f13d5e7861fc83d942d3d99bf270c83269575e52ac29e5b73cb956a6bd" -dependencies = [ - "nix 0.22.0", - "wayland-client", - "xcursor", -] - -[[package]] -name = "wayland-protocols" -version = "0.29.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60147ae23303402e41fe034f74fb2c35ad0780ee88a1c40ac09a3be1e7465741" -dependencies = [ - "bitflags", - "wayland-client", - "wayland-commons", - "wayland-scanner", -] - -[[package]] -name = "wayland-scanner" -version = "0.29.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39a1ed3143f7a143187156a2ab52742e89dac33245ba505c17224df48939f9e0" -dependencies = [ - "proc-macro2", - "quote", - "xml-rs", -] - -[[package]] -name = "wayland-sys" -version = "0.29.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9341df79a8975679188e37dab3889bfa57c44ac2cb6da166f519a81cbe452d4" -dependencies = [ - "dlib", - "lazy_static", - "pkg-config", -] - -[[package]] -name = "web-sys" -version = "0.3.56" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c060b319f29dd25724f09a2ba1418f142f539b2be99fbf4d2d5a8f7330afb8eb" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "wgpu" -version = "0.12.0" -source = "git+https://github.com/gfx-rs/wgpu?rev=0545e36#0545e36aa82709cca78c14eb3813f10eea7a9275" -dependencies = [ - "arrayvec", - "js-sys", - "log", - "naga", - "parking_lot", - "raw-window-handle", - "smallvec", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "wgpu-core", - "wgpu-hal", - "wgpu-types", -] - -[[package]] -name = "wgpu-core" -version = "0.12.0" -source = "git+https://github.com/gfx-rs/wgpu?rev=0545e36#0545e36aa82709cca78c14eb3813f10eea7a9275" -dependencies = [ - "arrayvec", - "bitflags", - "cfg_aliases", - "codespan-reporting", - "copyless", - "fxhash", - "log", - "naga", - "parking_lot", - "profiling", - "raw-window-handle", - "smallvec", - "thiserror", - "wgpu-hal", - "wgpu-types", -] - -[[package]] -name = "wgpu-hal" -version = "0.12.0" -source = "git+https://github.com/gfx-rs/wgpu?rev=0545e36#0545e36aa82709cca78c14eb3813f10eea7a9275" -dependencies = [ - "arrayvec", - "ash", - "bit-set", - "bitflags", - "block", - "core-graphics-types", - "d3d12", - "foreign-types", - "fxhash", - "glow", - "gpu-alloc", - "gpu-descriptor", - "inplace_it", - "js-sys", - "khronos-egl", - "libloading", - "log", - "metal", - "naga", - "objc", - "parking_lot", - "profiling", - "range-alloc", - "raw-window-handle", - "renderdoc-sys", - "thiserror", - "wasm-bindgen", - "web-sys", - "wgpu-types", - "winapi", -] - -[[package]] -name = "wgpu-types" -version = "0.12.0" -source = "git+https://github.com/gfx-rs/wgpu?rev=0545e36#0545e36aa82709cca78c14eb3813f10eea7a9275" -dependencies = [ - "bitflags", -] - -[[package]] -name = "wgpu_glyph" -version = "0.16.0" -source = "git+https://github.com/Anton-4/wgpu_glyph?rev=257d109#257d1098cbafa3c8a0a2465937b06fc730fc6ffb" -dependencies = [ - "bytemuck", - "glyph_brush", - "log", - "wgpu", -] - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-util" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" -dependencies = [ - "winapi", -] - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[package]] -name = "winit" -version = "0.26.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b43cc931d58b99461188607efd7acb2a093e65fc621f54cad78517a6063e73a" -dependencies = [ - "bitflags", - "cocoa", - "core-foundation 0.9.2", - "core-graphics 0.22.3", - "core-video-sys", - "dispatch", - "instant", - "lazy_static", - "libc", - "log", - "mio", - "ndk 0.5.0", - "ndk-glue 0.5.0", - "ndk-sys 0.2.2", - "objc", - "parking_lot", - "percent-encoding", - "raw-window-handle", - "smithay-client-toolkit", - "wasm-bindgen", - "wayland-client", - "wayland-protocols", - "web-sys", - "winapi", - "x11-dl", -] - -[[package]] -name = "x11-clipboard" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "473068b7b80ac86a18328824f1054e5e007898c47b5bbc281bd7abe32bc3653c" -dependencies = [ - "xcb", -] - -[[package]] -name = "x11-dl" -version = "2.19.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea26926b4ce81a6f5d9d0f3a0bc401e5a37c6ae14a1bfaa8ff6099ca80038c59" -dependencies = [ - "lazy_static", - "libc", - "pkg-config", -] - -[[package]] -name = "xcb" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "771e2b996df720cd1c6dd9ff90f62d91698fd3610cc078388d0564bdd6622a9c" -dependencies = [ - "libc", - "log", - "quick-xml", -] - -[[package]] -name = "xcursor" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "463705a63313cd4301184381c5e8042f0a7e9b4bb63653f216311d4ae74690b7" -dependencies = [ - "nom 7.1.0", -] - -[[package]] -name = "xi-unicode" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a67300977d3dc3f8034dae89778f502b6ba20b269527b3223ba59c0cf393bb8a" - -[[package]] -name = "xml-rs" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2d7d3948613f75c98fd9328cfdcc45acc4d360655289d0a7d4ec931392200a3" - -[[package]] -name = "yaml-rust" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" -dependencies = [ - "linked-hash-map", -] diff --git a/examples/gui/platform/Cargo.toml b/examples/gui/platform/Cargo.toml index 083f3e369e..3f312012bf 100644 --- a/examples/gui/platform/Cargo.toml +++ b/examples/gui/platform/Cargo.toml @@ -1,10 +1,10 @@ [package] name = "host" -version = "0.1.0" authors = ["The Roc Contributors"] -license = "UPL-1.0" edition = "2021" +license = "UPL-1.0" links = "app" +version = "0.0.1" [lib] name = "host" @@ -16,56 +16,43 @@ name = "host" path = "src/main.rs" [dependencies] -roc_std = { path = "../../../roc_std" } -libc = "0.2" arrayvec = "0.7.2" +libc = "0.2" page_size = "0.4.2" -# when changing winit version, check if copypasta can be updated simultaneously so they use the same versions for their dependencies. This will save build time. -winit = "0.26.1" -wgpu = { git = "https://github.com/gfx-rs/wgpu", rev = "0545e36" } -wgpu_glyph = { git = "https://github.com/Anton-4/wgpu_glyph", rev = "257d109" } +roc_std = { path = "../../../crates/roc_std" } +cgmath = "0.18.0" +colored = "2.0.0" +copypasta = "0.7.1" +fs_extra = "1.2.0" +futures = "0.3.17" glyph_brush = "0.7.2" log = "0.4.14" -env_logger = "0.9.0" -futures = "0.3.17" -cgmath = "0.18.0" -snafu = { version = "0.6.10", features = ["backtraces"] } -colored = "2.0.0" +nonempty = "0.7.0" +palette = "0.6.0" pest = "2.1.3" pest_derive = "2.1.0" -copypasta = "0.7.1" -palette = "0.6.0" -confy = { git = 'https://github.com/rust-cli/confy', features = [ - "yaml_conf" -], default-features = false } serde = { version = "1.0.130", features = ["derive"] } -nonempty = "0.7.0" -fs_extra = "1.2.0" -rodio = { version = "0.14.0", optional = true } # to play sounds +snafu = { version = "0.6.10", features = ["backtraces"] } threadpool = "1.8.1" - -[package.metadata.cargo-udeps.ignore] -# confy is currently unused but should not be removed -normal = ["confy"] -#development = [] -#build = [] +wgpu = { git = "https://github.com/gfx-rs/wgpu", rev = "0545e36" } +wgpu_glyph = { git = "https://github.com/Anton-4/wgpu_glyph", rev = "257d109" } +winit = "0.26.1" [features] default = [] -with_sound = ["rodio"] [dependencies.bytemuck] -version = "1.7.2" features = ["derive"] +version = "1.7.2" [workspace] # Optimizations based on https://deterministic.space/high-performance-rust.html [profile.release] -lto = "thin" codegen-units = 1 +lto = "thin" # debug = true # enable when profiling [profile.bench] -lto = "thin" codegen-units = 1 +lto = "thin" diff --git a/examples/gui/platform/Elem.roc b/examples/gui/platform/Elem.roc index b9a4d53d50..64aef819d9 100644 --- a/examples/gui/platform/Elem.roc +++ b/examples/gui/platform/Elem.roc @@ -2,21 +2,20 @@ interface Elem exposes [Elem, PressEvent, row, col, text, button, none, translate, list] imports [Action.{ Action }] -Elem state : +Elem state : [ # PERFORMANCE NOTE: # If there are 8 or fewer tags here, then on a 64-bit system, the tag can be stored # in the pointer - for massive memory savings. Try extremely hard to always limit the number # of tags in this union to 8 or fewer! - [ - Button (ButtonConfig state) (Elem state), - Text Str, - Col (List (Elem state)), - Row (List (Elem state)), - Lazy (Result { state, elem : Elem state } [NotCached] -> { state, elem : Elem state }), - # TODO FIXME: using this definition of Lazy causes a stack overflow in the compiler! - # Lazy (Result (Cached state) [NotCached] -> Cached state), - None, - ] + Button (ButtonConfig state) (Elem state), + Text Str, + Col (List (Elem state)), + Row (List (Elem state)), + Lazy (Result { state, elem : Elem state } [NotCached] -> { state, elem : Elem state }), + # TODO FIXME: using this definition of Lazy causes a stack overflow in the compiler! + # Lazy (Result (Cached state) [NotCached] -> Cached state), + None, +] ## Used internally in the type definition of Lazy Cached state : { state, elem : Elem state } @@ -54,6 +53,7 @@ lazy = \state, render -> # same as the cached one, then we can return exactly # what we had cached. cached + _ -> # Either the state changed or else we didn't have a # cached value to use. Either way, we need to render @@ -61,10 +61,10 @@ lazy = \state, render -> { state, elem: render state } none : Elem * -none = None# I've often wanted this in elm/html. Usually end up resorting to (Html.text "") - this seems nicer. +none = None # I've often wanted this in elm/html. Usually end up resorting to (Html.text "") - this seems nicer. ## Change an element's state type. ## -## TODO: indent the following once https://github.com/rtfeldman/roc/issues/2585 is fixed. +## TODO: indent the following once https://github.com/roc-lang/roc/issues/2585 is fixed. ## State : { photo : Photo } ## ## render : State -> Elem State @@ -80,17 +80,21 @@ translate = \child, toChild, toParent -> when child is Text str -> Text str + Col elems -> Col (List.map elems \elem -> translate elem toChild toParent) + Row elems -> Row (List.map elems \elem -> translate elem toChild toParent) + Button config label -> onPress = \parentState, event -> toChild parentState - |> config.onPress event - |> Action.map \c -> toParent parentState c + |> config.onPress event + |> Action.map \c -> toParent parentState c Button { onPress } (translate label toChild toParent) + Lazy renderChild -> Lazy \parentState -> @@ -100,6 +104,7 @@ translate = \child, toChild, toParent -> elem: translate toChild toParent newChild, state: toParent parentState state, } + None -> None @@ -108,7 +113,7 @@ translate = \child, toChild, toParent -> ## Convenient when you have a [List] in your state and want to make ## a [List] of child elements out of it. ## -## TODO: indent the following once https://github.com/rtfeldman/roc/issues/2585 is fixed. +## TODO: indent the following once https://github.com/roc-lang/roc/issues/2585 is fixed. ## State : { photos : List Photo } ## ## render : State -> Elem State @@ -118,7 +123,7 @@ translate = \child, toChild, toParent -> ## Elem.list Photo.render state .photos &photos ## ## col {} children -## TODO: format as multiline type annotation once https://github.com/rtfeldman/roc/issues/2586 is fixed +## TODO: format as multiline type annotation once https://github.com/roc-lang/roc/issues/2586 is fixed list : (child -> Elem child), parent, (parent -> List child), (parent, List child -> parent) -> List (Elem parent) list = \renderChild, parent, toChildren, toParent -> List.mapWithIndex @@ -131,8 +136,8 @@ list = \renderChild, parent, toChildren, toParent -> toChild \par, ch -> toChildren par - |> List.set ch index - |> toParent + |> List.set ch index + |> toParent renderChild newChild @@ -142,29 +147,34 @@ list = \renderChild, parent, toChildren, toParent -> ## if the child has been removed from the parent, ## drops it. ## -## TODO: format as multiline type annotation once https://github.com/rtfeldman/roc/issues/2586 is fixed +## TODO: format as multiline type annotation once https://github.com/roc-lang/roc/issues/2586 is fixed translateOrDrop : Elem child, (parent -> Result child *), (parent, child -> parent) -> Elem parent translateOrDrop = \child, toChild, toParent -> when child is Text str -> Text str + Col elems -> Col (List.map elems \elem -> translateOrDrop elem toChild toParent) + Row elems -> Row (List.map elems \elem -> translateOrDrop elem toChild toParent) + Button config label -> onPress = \parentState, event -> when toChild parentState is Ok newChild -> newChild - |> config.onPress event - |> Action.map \c -> toParent parentState c + |> config.onPress event + |> Action.map \c -> toParent parentState c + Err _ -> # The child was removed from the list before this onPress handler resolved. # (For example, by a previous event handler that fired simultaneously.) Action.none Button { onPress } (translateOrDrop label toChild toParent) + Lazy childState renderChild -> Lazy (toParent childState) @@ -172,9 +182,11 @@ translateOrDrop = \child, toChild, toParent -> when toChild parentState is Ok newChild -> renderChild newChild - |> translateOrDrop toChild toParent + |> translateOrDrop toChild toParent + Err _ -> None + # I don't think this should ever happen in practice. None -> None diff --git a/examples/gui/platform/build.rs b/examples/gui/platform/build.rs index 73159e387c..47763b34c3 100644 --- a/examples/gui/platform/build.rs +++ b/examples/gui/platform/build.rs @@ -1,4 +1,9 @@ fn main() { + #[cfg(not(windows))] println!("cargo:rustc-link-lib=dylib=app"); + + #[cfg(windows)] + println!("cargo:rustc-link-lib=dylib=libapp"); + println!("cargo:rustc-link-search=."); } diff --git a/examples/gui/platform/Package-Config.roc b/examples/gui/platform/main.roc similarity index 100% rename from examples/gui/platform/Package-Config.roc rename to examples/gui/platform/main.roc diff --git a/examples/gui/platform/src/graphics/lowlevel/quad.rs b/examples/gui/platform/src/graphics/lowlevel/quad.rs index 9c1fd85ae6..e8c1f1b568 100644 --- a/examples/gui/platform/src/graphics/lowlevel/quad.rs +++ b/examples/gui/platform/src/graphics/lowlevel/quad.rs @@ -1,6 +1,7 @@ /// A polygon with 4 corners +#[repr(C)] #[derive(Copy, Clone)] pub struct Quad { pub pos: [f32; 2], @@ -11,6 +12,7 @@ pub struct Quad { pub border_width: f32, } +// Safety: repr(C), and as defined will not having padding. unsafe impl bytemuck::Pod for Quad {} unsafe impl bytemuck::Zeroable for Quad {} @@ -28,4 +30,4 @@ impl Quad { 6 => Float32, ), }; -} \ No newline at end of file +} diff --git a/examples/gui/platform/src/graphics/primitives/text.rs b/examples/gui/platform/src/graphics/primitives/text.rs index f002d506e7..c746063b77 100644 --- a/examples/gui/platform/src/graphics/primitives/text.rs +++ b/examples/gui/platform/src/graphics/primitives/text.rs @@ -130,7 +130,7 @@ pub fn build_glyph_brush( render_format: wgpu::TextureFormat, ) -> Result, InvalidFont> { let inconsolata = FontArc::try_from_slice(include_bytes!( - "../../../../../../editor/Inconsolata-Regular.ttf" + "../../../../Inconsolata-Regular.ttf" ))?; Ok(GlyphBrushBuilder::using_font(inconsolata).build(gpu_device, render_format)) diff --git a/examples/gui/platform/src/gui.rs b/examples/gui/platform/src/gui.rs index 755fa40ae8..e054a1895b 100644 --- a/examples/gui/platform/src/gui.rs +++ b/examples/gui/platform/src/gui.rs @@ -56,7 +56,9 @@ fn run_event_loop(title: &str, root: RocElem) -> Result<(), Box> { }) .await .expect(r#"Request adapter - If you're running this from inside nix, follow the instructions here to resolve this: https://github.com/rtfeldman/roc/blob/trunk/BUILDING_FROM_SOURCE.md#editor + If you're running this from inside nix, run with: + `nixVulkanIntel `. + See extra docs here: github.com/guibou/nixGL "#); adapter diff --git a/examples/gui/platform/src/main.rs b/examples/gui/platform/src/main.rs index 51175f934b..0765384f29 100644 --- a/examples/gui/platform/src/main.rs +++ b/examples/gui/platform/src/main.rs @@ -1,3 +1,3 @@ fn main() { - std::process::exit(host::rust_main()); + std::process::exit(host::rust_main() as _); } diff --git a/examples/gui/platform/src/roc.rs b/examples/gui/platform/src/roc.rs index 2ad7732a99..f5b8f0480e 100644 --- a/examples/gui/platform/src/roc.rs +++ b/examples/gui/platform/src/roc.rs @@ -26,21 +26,22 @@ pub unsafe extern "C" fn roc_dealloc(c_ptr: *mut c_void, _alignment: u32) { } #[no_mangle] -pub unsafe extern "C" fn roc_panic(c_ptr: *mut c_void, tag_id: u32) { +pub unsafe extern "C" fn roc_panic(msg: *mut RocStr, tag_id: u32) { match tag_id { 0 => { - let slice = CStr::from_ptr(c_ptr as *const c_char); - let string = slice.to_str().unwrap(); - eprintln!("Roc hit a panic: {}", string); - std::process::exit(1); + eprintln!("Roc standard library hit a panic: {}", &*msg); } - _ => todo!(), + 1 => { + eprintln!("Application hit a panic: {}", &*msg); + } + _ => unreachable!(), } + std::process::exit(1); } #[no_mangle] -pub unsafe extern "C" fn roc_memcpy(dst: *mut c_void, src: *mut c_void, n: usize) -> *mut c_void { - libc::memcpy(dst, src, n) +pub unsafe extern "C" fn roc_dbg(loc: *mut RocStr, msg: *mut RocStr) { + eprintln!("[{}] {}", &*loc, &*msg); } #[no_mangle] diff --git a/examples/hello-world/.gitignore b/examples/hello-world/.gitignore deleted file mode 100644 index 0f55df43a2..0000000000 --- a/examples/hello-world/.gitignore +++ /dev/null @@ -1,6 +0,0 @@ -helloC -helloRust -helloSwift -helloWorld -helloZig -*.wasm diff --git a/examples/hello-world/README.md b/examples/hello-world/README.md deleted file mode 100644 index bd5fbda39f..0000000000 --- a/examples/hello-world/README.md +++ /dev/null @@ -1,44 +0,0 @@ -# Hello, World! - -To run, `cd` into this directory and run: - -```bash -cargo run helloWorld.roc -``` - -To run in release mode instead, do: - -```bash -cargo run --release helloWorld.roc -``` - -## Design Notes - -This demonstrates the basic design of hosts: Roc code gets compiled into a pure -function (in this case, a thunk that always returns `"Hello, World!\n"`) and -then the host calls that function. Fundamentally, that's the whole idea! The host -might not even have a `main` - it could be a library, a plugin, anything. -Everything else is built on this basic "hosts calling linked pure functions" design. - -For example, things get more interesting when the compiled Roc function returns -a `Task` - that is, a tagged union data structure containing function pointers -to callback closures. This lets the Roc pure function describe arbitrary -chainable effects, which the host can interpret to perform I/O as requested by -the Roc program. (The tagged union `Task` would have a variant for each supported -I/O operation.) - -In this trivial example, it's very easy to line up the API between the host and -the Roc program. In a more involved host, this would be much trickier - especially -if the API were changing frequently during development. - -The idea there is to have a first-class concept of "glue code" which host authors -can write (it would be plain Roc code, but with some extra keywords that aren't -available in normal modules - kinda like `port module` in Elm), and which -describe both the Roc-host/C boundary as well as the Roc-host/Roc-app boundary. -Roc application authors only care about the Roc-host/Roc-app portion, and the -host author only cares about the Roc-host/C boundary when implementing the host. - -Using this glue code, the Roc compiler can generate C header files describing the -boundary. This not only gets us host compatibility with C compilers, but also -Rust FFI for free, because [`rust-bindgen`](https://github.com/rust-lang/rust-bindgen) -generates correct Rust FFI bindings from C headers. diff --git a/examples/hello-world/c-platform/Package-Config.roc b/examples/hello-world/c-platform/Package-Config.roc deleted file mode 100644 index f18efa1b18..0000000000 --- a/examples/hello-world/c-platform/Package-Config.roc +++ /dev/null @@ -1,9 +0,0 @@ -platform "hello-world-in-c" - requires {} { main : Str } - exposes [] - packages {} - imports [] - provides [mainForHost] - -mainForHost : Str -mainForHost = main diff --git a/examples/hello-world/c-platform/helloC.roc b/examples/hello-world/c-platform/helloC.roc deleted file mode 100644 index 6781c0507d..0000000000 --- a/examples/hello-world/c-platform/helloC.roc +++ /dev/null @@ -1,6 +0,0 @@ -app "helloC" - packages { pf: "." } - imports [] - provides [main] to pf - -main = "Hello, World!\n" diff --git a/examples/hello-world/c-platform/host.c b/examples/hello-world/c-platform/host.c deleted file mode 100644 index 2129b11abb..0000000000 --- a/examples/hello-world/c-platform/host.c +++ /dev/null @@ -1,84 +0,0 @@ -#include -#include -#include -#include -#include -#include - -void* roc_alloc(size_t size, unsigned int alignment) { return malloc(size); } - -void* roc_realloc(void* ptr, size_t new_size, size_t old_size, - unsigned int alignment) { - return realloc(ptr, new_size); -} - -void roc_dealloc(void* ptr, unsigned int alignment) { free(ptr); } - -void roc_panic(void* ptr, unsigned int alignment) { - char* msg = (char*)ptr; - fprintf(stderr, - "Application crashed with message\n\n %s\n\nShutting down\n", msg); - exit(0); -} - -void* roc_memcpy(void* dest, const void* src, size_t n) { - return memcpy(dest, src, n); -} - -void* roc_memset(void* str, int c, size_t n) { return memset(str, c, n); } - -struct RocStr { - char* bytes; - size_t len; - size_t capacity; -}; - -bool is_small_str(struct RocStr str) { return ((ssize_t)str.capacity) < 0; } - -// Determine the length of the string, taking into -// account the small string optimization -size_t roc_str_len(struct RocStr str) { - char* bytes = (char*)&str; - char last_byte = bytes[sizeof(str) - 1]; - char last_byte_xored = last_byte ^ 0b10000000; - size_t small_len = (size_t)(last_byte_xored); - size_t big_len = str.len; - - // Avoid branch misprediction costs by always - // determining both small_len and big_len, - // so this compiles to a cmov instruction. - if (is_small_str(str)) { - return small_len; - } else { - return big_len; - } -} - -extern void roc__mainForHost_1_exposed_generic(struct RocStr *string); - -int main() { - - struct RocStr str; - roc__mainForHost_1_exposed_generic(&str); - - // Determine str_len and the str_bytes pointer, - // taking into account the small string optimization. - size_t str_len = roc_str_len(str); - char* str_bytes; - - if (is_small_str(str)) { - str_bytes = (char*)&str; - } else { - str_bytes = str.bytes; - } - - // Write to stdout - if (write(1, str_bytes, str_len) >= 0) { - // Writing succeeded! - return 0; - } else { - printf("Error writing to stdout: %s\n", strerror(errno)); - - return 1; - } -} diff --git a/examples/hello-world/helloWorld.roc b/examples/hello-world/helloWorld.roc deleted file mode 100644 index 7f90a48811..0000000000 --- a/examples/hello-world/helloWorld.roc +++ /dev/null @@ -1,11 +0,0 @@ -app "helloWorld" - packages { pf: "c-platform" } - # To switch platforms, comment-out the line above and un-comment one below. - # packages { pf: "rust-platform" } - # packages { pf: "swift-platform" } - # packages { pf: "web-platform" } # See ./web-platform/README.md - # packages { pf: "zig-platform" } - imports [] - provides [main] to pf - -main = "Hello, World!\n" diff --git a/examples/hello-world/rust-platform/Cargo.lock b/examples/hello-world/rust-platform/Cargo.lock deleted file mode 100644 index ebd0a436cf..0000000000 --- a/examples/hello-world/rust-platform/Cargo.lock +++ /dev/null @@ -1,30 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 3 - -[[package]] -name = "host" -version = "0.1.0" -dependencies = [ - "libc", - "roc_std", -] - -[[package]] -name = "libc" -version = "0.2.92" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56d855069fafbb9b344c0f962150cd2c1187975cb1c22c1522c240d8c4986714" - -[[package]] -name = "roc_std" -version = "0.1.0" -dependencies = [ - "static_assertions", -] - -[[package]] -name = "static_assertions" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f406d6ee68db6796e11ffd7b4d171864c58b7451e79ef9460ea33c287a1f89a7" diff --git a/examples/hello-world/rust-platform/Cargo.toml b/examples/hello-world/rust-platform/Cargo.toml deleted file mode 100644 index baad986559..0000000000 --- a/examples/hello-world/rust-platform/Cargo.toml +++ /dev/null @@ -1,22 +0,0 @@ -[package] -name = "host" -version = "0.1.0" -authors = ["The Roc Contributors"] -license = "UPL-1.0" -edition = "2021" -links = "app" - -[lib] -name = "host" -path = "src/lib.rs" -crate-type = ["staticlib", "rlib"] - -[[bin]] -name = "host" -path = "src/main.rs" - -[dependencies] -roc_std = { path = "../../../roc_std" } -libc = "0.2" - -[workspace] diff --git a/examples/hello-world/rust-platform/Package-Config.roc b/examples/hello-world/rust-platform/Package-Config.roc deleted file mode 100644 index a3661812af..0000000000 --- a/examples/hello-world/rust-platform/Package-Config.roc +++ /dev/null @@ -1,9 +0,0 @@ -platform "hello-world-in-rust" - requires {} { main : Str } - exposes [] - packages {} - imports [] - provides [mainForHost] - -mainForHost : Str -mainForHost = main diff --git a/examples/hello-world/rust-platform/build.rs b/examples/hello-world/rust-platform/build.rs deleted file mode 100644 index 73159e387c..0000000000 --- a/examples/hello-world/rust-platform/build.rs +++ /dev/null @@ -1,4 +0,0 @@ -fn main() { - println!("cargo:rustc-link-lib=dylib=app"); - println!("cargo:rustc-link-search=."); -} diff --git a/examples/hello-world/rust-platform/helloRust.roc b/examples/hello-world/rust-platform/helloRust.roc deleted file mode 100644 index a53b5cc047..0000000000 --- a/examples/hello-world/rust-platform/helloRust.roc +++ /dev/null @@ -1,6 +0,0 @@ -app "helloRust" - packages { pf: "." } - imports [] - provides [main] to pf - -main = "Hello, World!\n" diff --git a/examples/hello-world/rust-platform/src/lib.rs b/examples/hello-world/rust-platform/src/lib.rs deleted file mode 100644 index 7df8fe563c..0000000000 --- a/examples/hello-world/rust-platform/src/lib.rs +++ /dev/null @@ -1,72 +0,0 @@ -#![allow(non_snake_case)] - -use core::ffi::c_void; -use roc_std::RocStr; -use std::ffi::CStr; -use std::os::raw::c_char; - -extern "C" { - #[link_name = "roc__mainForHost_1_exposed_generic"] - fn roc_main(_: &mut RocStr); -} - -#[no_mangle] -pub unsafe extern "C" fn roc_alloc(size: usize, _alignment: u32) -> *mut c_void { - return libc::malloc(size); -} - -#[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 { - return libc::realloc(c_ptr, new_size); -} - -#[no_mangle] -pub unsafe extern "C" fn roc_dealloc(c_ptr: *mut c_void, _alignment: u32) { - return libc::free(c_ptr); -} - -#[no_mangle] -pub unsafe extern "C" fn roc_panic(c_ptr: *mut c_void, tag_id: u32) { - match tag_id { - 0 => { - let slice = CStr::from_ptr(c_ptr as *const c_char); - let string = slice.to_str().unwrap(); - eprintln!("Roc hit a panic: {}", string); - std::process::exit(1); - } - _ => todo!(), - } -} - -#[no_mangle] -pub unsafe extern "C" fn roc_memcpy(dst: *mut c_void, src: *mut c_void, n: usize) -> *mut c_void { - libc::memcpy(dst, src, n) -} - -#[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) -} - -#[no_mangle] -pub extern "C" fn rust_main() -> i32 { - unsafe { - let mut roc_str = RocStr::default(); - roc_main(&mut roc_str); - - let len = roc_str.len(); - let str_bytes = roc_str.as_bytes().as_ptr() as *const libc::c_void; - - if libc::write(1, str_bytes, len) < 0 { - panic!("Writing to stdout failed!"); - } - } - - // Exit code - 0 -} diff --git a/examples/hello-world/rust-platform/src/main.rs b/examples/hello-world/rust-platform/src/main.rs deleted file mode 100644 index 51175f934b..0000000000 --- a/examples/hello-world/rust-platform/src/main.rs +++ /dev/null @@ -1,3 +0,0 @@ -fn main() { - std::process::exit(host::rust_main()); -} diff --git a/examples/hello-world/swift-platform/Package-Config.roc b/examples/hello-world/swift-platform/Package-Config.roc deleted file mode 100644 index 2155fc172c..0000000000 --- a/examples/hello-world/swift-platform/Package-Config.roc +++ /dev/null @@ -1,9 +0,0 @@ -platform "hello-world-in-swift" - requires {} { main : Str } - exposes [] - packages {} - imports [] - provides [mainForHost] - -mainForHost : Str -mainForHost = main diff --git a/examples/hello-world/swift-platform/helloSwift.roc b/examples/hello-world/swift-platform/helloSwift.roc deleted file mode 100644 index 1a22419218..0000000000 --- a/examples/hello-world/swift-platform/helloSwift.roc +++ /dev/null @@ -1,6 +0,0 @@ -app "helloSwift" - packages { pf: "." } - imports [] - provides [main] to pf - -main = "Hello, World!\n" diff --git a/examples/hello-world/web-platform/Package-Config.roc b/examples/hello-world/web-platform/Package-Config.roc deleted file mode 100644 index 5443616bd4..0000000000 --- a/examples/hello-world/web-platform/Package-Config.roc +++ /dev/null @@ -1,9 +0,0 @@ -platform "hello-world-in-web-assembly" - requires {} { main : Str } - exposes [] - packages {} - imports [] - provides [mainForHost] - -mainForHost : Str -mainForHost = main diff --git a/examples/hello-world/web-platform/README.md b/examples/hello-world/web-platform/README.md deleted file mode 100644 index 9708431198..0000000000 --- a/examples/hello-world/web-platform/README.md +++ /dev/null @@ -1,55 +0,0 @@ -# Hello, World! - -To run this website, first compile either of these identical apps: - -```bash -# Option A: Compile helloWeb.roc -cargo run -- build --target=wasm32 examples/hello-world/web-platform/helloWeb.roc - -# Option B: Compile helloWorld.roc with `pf: "web-platform"` and move the result -cargo run -- build --target=wasm32 examples/hello-world/helloWorld.roc -(cd examples/hello-world && mv helloWorld.wasm web-platform/helloWeb.wasm) -``` - -Then `cd` into the website directory -and run any web server that can handle WebAssembly. -For example, with `http-server`: - -```bash -cd examples/hello-world/web-platform -npm install -g http-server -http-server -``` - -Now open your browser at http://localhost:8080 - -## Design Notes - -This demonstrates the basic design of hosts: Roc code gets compiled into a pure -function (in this case, a thunk that always returns `"Hello, World!\n"`) and -then the host calls that function. Fundamentally, that's the whole idea! The host -might not even have a `main` - it could be a library, a plugin, anything. -Everything else is built on this basic "hosts calling linked pure functions" design. - -For example, things get more interesting when the compiled Roc function returns -a `Task` - that is, a tagged union data structure containing function pointers -to callback closures. This lets the Roc pure function describe arbitrary -chainable effects, which the host can interpret to perform I/O as requested by -the Roc program. (The tagged union `Task` would have a variant for each supported -I/O operation.) - -In this trivial example, it's very easy to line up the API between the host and -the Roc program. In a more involved host, this would be much trickier - especially -if the API were changing frequently during development. - -The idea there is to have a first-class concept of "glue code" which host authors -can write (it would be plain Roc code, but with some extra keywords that aren't -available in normal modules - kinda like `port module` in Elm), and which -describe both the Roc-host/C boundary as well as the Roc-host/Roc-app boundary. -Roc application authors only care about the Roc-host/Roc-app portion, and the -host author only cares about the Roc-host/C boundary when implementing the host. - -Using this glue code, the Roc compiler can generate C header files describing the -boundary. This not only gets us host compatibility with C compilers, but also -Rust FFI for free, because [`rust-bindgen`](https://github.com/rust-lang/rust-bindgen) -generates correct Rust FFI bindings from C headers. diff --git a/examples/hello-world/web-platform/helloWeb.roc b/examples/hello-world/web-platform/helloWeb.roc deleted file mode 100644 index 944b267400..0000000000 --- a/examples/hello-world/web-platform/helloWeb.roc +++ /dev/null @@ -1,6 +0,0 @@ -app "helloWeb" - packages { pf: "." } - imports [] - provides [main] to pf - -main = "Hello, World!\n" diff --git a/examples/hello-world/web-platform/host.js b/examples/hello-world/web-platform/host.js deleted file mode 100644 index 77f2bbed78..0000000000 --- a/examples/hello-world/web-platform/host.js +++ /dev/null @@ -1,57 +0,0 @@ -async function roc_web_platform_run(wasm_filename, callback) { - const decoder = new TextDecoder(); - let memory_bytes; - let exit_code; - - function js_display_roc_string(str_bytes, str_len) { - const utf8_bytes = memory_bytes.subarray(str_bytes, str_bytes + str_len); - const js_string = decoder.decode(utf8_bytes); - callback(js_string); - } - - const importObj = { - wasi_snapshot_preview1: { - proc_exit: (code) => { - if (code !== 0) { - console.error(`Exited with code ${code}`); - } - exit_code = code; - }, - }, - env: { - js_display_roc_string, - roc_panic: (_pointer, _tag_id) => { - throw "Roc panicked!"; - }, - }, - }; - - let wasm; - - const response = await fetch(wasm_filename); - - if (WebAssembly.instantiateStreaming) { - // streaming API has better performance if available - wasm = await WebAssembly.instantiateStreaming(response, importObj); - } else { - const module_bytes = await response.arrayBuffer(); - wasm = await WebAssembly.instantiate(module_bytes, importObj); - } - - memory_bytes = new Uint8Array(wasm.instance.exports.memory.buffer); - - try { - wasm.instance.exports._start(); - } catch (e) { - const is_ok = e.message === "unreachable" && exit_code === 0; - if (!is_ok) { - console.error(e); - } - } -} - -if (typeof module !== "undefined") { - module.exports = { - roc_web_platform_run, - }; -} diff --git a/examples/hello-world/web-platform/host.zig b/examples/hello-world/web-platform/host.zig deleted file mode 100644 index b00d7918a3..0000000000 --- a/examples/hello-world/web-platform/host.zig +++ /dev/null @@ -1,74 +0,0 @@ -const std = @import("std"); -const str = @import("str"); -const builtin = @import("builtin"); -const RocStr = str.RocStr; -const testing = std.testing; -const expectEqual = testing.expectEqual; -const expect = testing.expect; - -comptime { - // This is a workaround for https://github.com/ziglang/zig/issues/8218 - // which is only necessary on macOS. - // - // Once that issue is fixed, we can undo the changes in - // 177cf12e0555147faa4d436e52fc15175c2c4ff0 and go back to passing - // -fcompiler-rt in link.rs instead of doing this. Note that this - // workaround is present in many host.zig files, so make sure to undo - // it everywhere! - if (builtin.os.tag == .macos) { - _ = @import("compiler_rt"); - } -} - -const Align = extern struct { a: usize, b: usize }; -extern fn malloc(size: usize) callconv(.C) ?*align(@alignOf(Align)) anyopaque; -extern fn realloc(c_ptr: [*]align(@alignOf(Align)) u8, size: usize) callconv(.C) ?*anyopaque; -extern fn free(c_ptr: [*]align(@alignOf(Align)) u8) callconv(.C) void; -extern fn memcpy(dest: *anyopaque, src: *anyopaque, count: usize) *anyopaque; - -export fn roc_alloc(size: usize, alignment: u32) callconv(.C) ?*anyopaque { - _ = alignment; - - return malloc(size); -} - -export fn roc_realloc(c_ptr: *anyopaque, new_size: usize, old_size: usize, alignment: u32) callconv(.C) ?*anyopaque { - _ = old_size; - _ = alignment; - - return realloc(@alignCast(@alignOf(Align), @ptrCast([*]u8, c_ptr)), new_size); -} - -export fn roc_dealloc(c_ptr: *anyopaque, alignment: u32) callconv(.C) void { - _ = alignment; - - free(@alignCast(@alignOf(Align), @ptrCast([*]u8, c_ptr))); -} - -export fn roc_memcpy(dest: *anyopaque, src: *anyopaque, count: usize) callconv(.C) void { - _ = memcpy(dest, src, count); -} - -// NOTE roc_panic is provided in the JS file, so it can throw an exception - -const mem = std.mem; -const Allocator = mem.Allocator; - -extern fn roc__mainForHost_1_exposed(*RocStr) void; - -const Unit = extern struct {}; - -extern fn js_display_roc_string(str_bytes: ?[*]u8, str_len: usize) void; - -pub fn main() u8 { - // actually call roc to populate the callresult - var callresult = RocStr.empty(); - roc__mainForHost_1_exposed(&callresult); - - // display the result using JavaScript - js_display_roc_string(callresult.asU8ptr(), callresult.len()); - - callresult.deinit(); - - return 0; -} diff --git a/examples/hello-world/web-platform/index.html b/examples/hello-world/web-platform/index.html deleted file mode 100644 index 0397305c5a..0000000000 --- a/examples/hello-world/web-platform/index.html +++ /dev/null @@ -1,12 +0,0 @@ - - -
- - - - diff --git a/examples/hello-world/zig-platform/Package-Config.roc b/examples/hello-world/zig-platform/Package-Config.roc deleted file mode 100644 index a36ab95c0b..0000000000 --- a/examples/hello-world/zig-platform/Package-Config.roc +++ /dev/null @@ -1,9 +0,0 @@ -platform "hello-world-in-zig" - requires {} { main : Str } - exposes [] - packages {} - imports [] - provides [mainForHost] - -mainForHost : Str -mainForHost = main diff --git a/examples/hello-world/zig-platform/helloZig.roc b/examples/hello-world/zig-platform/helloZig.roc deleted file mode 100644 index 9ef3b5d311..0000000000 --- a/examples/hello-world/zig-platform/helloZig.roc +++ /dev/null @@ -1,6 +0,0 @@ -app "helloZig" - packages { pf: "." } - imports [] - provides [main] to pf - -main = "Hello, World!\n" diff --git a/examples/hello-world/zig-platform/host.zig b/examples/hello-world/zig-platform/host.zig deleted file mode 100644 index 18a1427207..0000000000 --- a/examples/hello-world/zig-platform/host.zig +++ /dev/null @@ -1,115 +0,0 @@ -const std = @import("std"); -const builtin = @import("builtin"); -const str = @import("str"); -const RocStr = str.RocStr; -const testing = std.testing; -const expectEqual = testing.expectEqual; -const expect = testing.expect; - -comptime { - // This is a workaround for https://github.com/ziglang/zig/issues/8218 - // which is only necessary on macOS. - // - // Once that issue is fixed, we can undo the changes in - // 177cf12e0555147faa4d436e52fc15175c2c4ff0 and go back to passing - // -fcompiler-rt in link.rs instead of doing this. Note that this - // workaround is present in many host.zig files, so make sure to undo - // it everywhere! - if (builtin.os.tag == .macos) { - _ = @import("compiler_rt"); - } -} - -const Align = 2 * @alignOf(usize); -extern fn malloc(size: usize) callconv(.C) ?*align(Align) anyopaque; -extern fn realloc(c_ptr: [*]align(Align) u8, size: usize) callconv(.C) ?*anyopaque; -extern fn free(c_ptr: [*]align(Align) u8) callconv(.C) void; -extern fn memcpy(dst: [*]u8, src: [*]u8, size: usize) callconv(.C) void; -extern fn memset(dst: [*]u8, value: i32, size: usize) callconv(.C) void; - -const DEBUG: bool = false; - -export fn roc_alloc(size: usize, alignment: u32) callconv(.C) ?*anyopaque { - if (DEBUG) { - var ptr = malloc(size); - const stdout = std.io.getStdOut().writer(); - stdout.print("alloc: {d} (alignment {d}, size {d})\n", .{ ptr, alignment, size }) catch unreachable; - return ptr; - } else { - return malloc(size); - } -} - -export fn roc_realloc(c_ptr: *anyopaque, new_size: usize, old_size: usize, alignment: u32) callconv(.C) ?*anyopaque { - if (DEBUG) { - const stdout = std.io.getStdOut().writer(); - stdout.print("realloc: {d} (alignment {d}, old_size {d})\n", .{ c_ptr, alignment, old_size }) catch unreachable; - } - - return realloc(@alignCast(Align, @ptrCast([*]u8, c_ptr)), new_size); -} - -export fn roc_dealloc(c_ptr: *anyopaque, alignment: u32) callconv(.C) void { - if (DEBUG) { - const stdout = std.io.getStdOut().writer(); - stdout.print("dealloc: {d} (alignment {d})\n", .{ c_ptr, alignment }) catch unreachable; - } - - free(@alignCast(Align, @ptrCast([*]u8, c_ptr))); -} - -export fn roc_panic(c_ptr: *anyopaque, tag_id: u32) callconv(.C) void { - _ = tag_id; - - const stderr = std.io.getStdErr().writer(); - const msg = @ptrCast([*:0]const u8, c_ptr); - stderr.print("Application crashed with message\n\n {s}\n\nShutting down\n", .{msg}) catch unreachable; - std.process.exit(0); -} - -export fn roc_memcpy(dst: [*]u8, src: [*]u8, size: usize) callconv(.C) void { - return memcpy(dst, src, size); -} - -export fn roc_memset(dst: [*]u8, value: i32, size: usize) callconv(.C) void { - return memset(dst, value, size); -} - -const mem = std.mem; -const Allocator = mem.Allocator; - -extern fn roc__mainForHost_1_exposed_generic(*RocStr) void; - -const Unit = extern struct {}; - -pub fn main() u8 { - const stdout = std.io.getStdOut().writer(); - const stderr = std.io.getStdErr().writer(); - - // start time - var ts1: std.os.timespec = undefined; - std.os.clock_gettime(std.os.CLOCK.REALTIME, &ts1) catch unreachable; - - // actually call roc to populate the callresult - var callresult = RocStr.empty(); - roc__mainForHost_1_exposed_generic(&callresult); - - // end time - var ts2: std.os.timespec = undefined; - std.os.clock_gettime(std.os.CLOCK.REALTIME, &ts2) catch unreachable; - - // stdout the result - stdout.print("{s}", .{callresult.asSlice()}) catch unreachable; - - callresult.deinit(); - - const delta = to_seconds(ts2) - to_seconds(ts1); - - stderr.print("runtime: {d:.3}ms\n", .{delta * 1000}) catch unreachable; - - return 0; -} - -fn to_seconds(tms: std.os.timespec) f64 { - return @intToFloat(f64, tms.tv_sec) + (@intToFloat(f64, tms.tv_nsec) / 1_000_000_000.0); -} diff --git a/examples/helloWorld.roc b/examples/helloWorld.roc new file mode 100644 index 0000000000..7a30101ef2 --- /dev/null +++ b/examples/helloWorld.roc @@ -0,0 +1,7 @@ +app "helloWorld" + packages { pf: "https://github.com/roc-lang/basic-cli/releases/download/0.7.0/bkGby8jb0tmZYsy2hg1E_B2QrCgcSTxdUlHtETwm5m4.tar.br" } + imports [pf.Stdout] + provides [main] to pf + +main = + Stdout.line "Hello, World!" diff --git a/examples/inspect-gui.roc b/examples/inspect-gui.roc new file mode 100644 index 0000000000..a3e0aa120c --- /dev/null +++ b/examples/inspect-gui.roc @@ -0,0 +1,38 @@ +# +# Visualizes Roc values in a basic GUI +# +app "inspect-gui" + packages { pf: "gui/platform/main.roc" } + imports [ + Community, + GuiFormatter, + ] + provides [render] to pf + +render = + Community.empty + |> Community.addPerson { + firstName: "John", + lastName: "Smith", + age: 27, + hasBeard: Bool.true, + favoriteColor: Blue, + } + |> Community.addPerson { + firstName: "Debby", + lastName: "Johnson", + age: 47, + hasBeard: Bool.false, + favoriteColor: Green, + } + |> Community.addPerson { + firstName: "Jane", + lastName: "Doe", + age: 33, + hasBeard: Bool.false, + favoriteColor: RGB (255, 255, 0), + } + |> Community.addFriend 0 2 + |> Community.addFriend 1 2 + |> Inspect.inspect + |> GuiFormatter.toGui diff --git a/examples/inspect-logging.roc b/examples/inspect-logging.roc new file mode 100644 index 0000000000..f9b9c10c91 --- /dev/null +++ b/examples/inspect-logging.roc @@ -0,0 +1,39 @@ +# +# Shows how Roc values can be logged +# +app "inspect-logging" + packages { pf: "https://github.com/roc-lang/basic-cli/releases/download/0.7.0/bkGby8jb0tmZYsy2hg1E_B2QrCgcSTxdUlHtETwm5m4.tar.br" } + imports [ + pf.Stdout, + Community, + ] + provides [main] to pf + +main = + Community.empty + |> Community.addPerson { + firstName: "John", + lastName: "Smith", + age: 27, + hasBeard: Bool.true, + favoriteColor: Blue, + } + |> Community.addPerson { + firstName: "Debby", + lastName: "Johnson", + age: 47, + hasBeard: Bool.false, + favoriteColor: Green, + } + |> Community.addPerson { + firstName: "Jane", + lastName: "Doe", + age: 33, + hasBeard: Bool.false, + favoriteColor: RGB (255, 255, 0), + } + |> Community.addFriend 0 2 + |> Community.addFriend 1 2 + |> Inspect.inspect + |> Inspect.toDbgStr + |> Stdout.line diff --git a/examples/interactive/.gitignore b/examples/interactive/.gitignore deleted file mode 100644 index a36aa49837..0000000000 --- a/examples/interactive/.gitignore +++ /dev/null @@ -1,5 +0,0 @@ -countdown -echo -effects -form -tui diff --git a/examples/interactive/README.md b/examples/interactive/README.md deleted file mode 100644 index 92bc5563b0..0000000000 --- a/examples/interactive/README.md +++ /dev/null @@ -1,9 +0,0 @@ -# Interactive examples - -These are examples of how to make extremely basic -CLI (command-line interface) and TUI (terminal user interface) apps in Roc. - -There's not currently much documentation for the CLI platform (which also doesn't support many operations at this point!) -but you can look at [the modules it includes](cli-platform) - for example, -multiple other modules use the [`Task`](cli-platform/Task.roc) module, including the -[`Stdin`](cli-platform/Stdin.roc) and [`Stdout`](cli-platform/Stdout.roc) modules. diff --git a/examples/interactive/cli-platform/Cargo.lock b/examples/interactive/cli-platform/Cargo.lock deleted file mode 100644 index 39d14572e3..0000000000 --- a/examples/interactive/cli-platform/Cargo.lock +++ /dev/null @@ -1,30 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 3 - -[[package]] -name = "host" -version = "0.1.0" -dependencies = [ - "libc", - "roc_std", -] - -[[package]] -name = "libc" -version = "0.2.106" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a60553f9a9e039a333b4e9b20573b9e9b9c0bb3a11e201ccc48ef4283456d673" - -[[package]] -name = "roc_std" -version = "0.1.0" -dependencies = [ - "static_assertions", -] - -[[package]] -name = "static_assertions" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f406d6ee68db6796e11ffd7b4d171864c58b7451e79ef9460ea33c287a1f89a7" diff --git a/examples/interactive/cli-platform/Cargo.toml b/examples/interactive/cli-platform/Cargo.toml deleted file mode 100644 index 37d5561b6b..0000000000 --- a/examples/interactive/cli-platform/Cargo.toml +++ /dev/null @@ -1,23 +0,0 @@ -[package] -name = "host" -version = "0.1.0" -authors = ["The Roc Contributors"] -license = "UPL-1.0" -edition = "2021" - -links = "app" - -[lib] -name = "host" -path = "src/lib.rs" -crate-type = ["staticlib", "rlib"] - -[[bin]] -name = "host" -path = "src/main.rs" - -[dependencies] -roc_std = { path = "../../../roc_std" } -libc = "0.2" - -[workspace] diff --git a/examples/interactive/cli-platform/Effect.roc b/examples/interactive/cli-platform/Effect.roc deleted file mode 100644 index a7ebc51d8c..0000000000 --- a/examples/interactive/cli-platform/Effect.roc +++ /dev/null @@ -1,8 +0,0 @@ -hosted Effect - exposes [Effect, after, map, always, forever, loop, putLine, getLine] - imports [] - generates Effect with [after, map, always, forever, loop] - -putLine : Str -> Effect {} - -getLine : Effect Str diff --git a/examples/interactive/cli-platform/Package-Config.roc b/examples/interactive/cli-platform/Package-Config.roc deleted file mode 100644 index 8e185a6db1..0000000000 --- a/examples/interactive/cli-platform/Package-Config.roc +++ /dev/null @@ -1,9 +0,0 @@ -platform "cli" - requires {} { main : Task {} [] } - exposes [] - packages {} - imports [Task.{ Task }] - provides [mainForHost] - -mainForHost : Task {} [] as Fx -mainForHost = main diff --git a/examples/interactive/cli-platform/Stdin.roc b/examples/interactive/cli-platform/Stdin.roc deleted file mode 100644 index 070835fc21..0000000000 --- a/examples/interactive/cli-platform/Stdin.roc +++ /dev/null @@ -1,6 +0,0 @@ -interface Stdin - exposes [line] - imports [pf.Effect, Task] - -line : Task.Task Str * -line = Effect.after Effect.getLine Task.succeed# TODO FIXME Effect.getLine should suffice diff --git a/examples/interactive/cli-platform/Stdout.roc b/examples/interactive/cli-platform/Stdout.roc deleted file mode 100644 index 473b93a215..0000000000 --- a/examples/interactive/cli-platform/Stdout.roc +++ /dev/null @@ -1,6 +0,0 @@ -interface Stdout - exposes [line] - imports [pf.Effect, Task.{ Task }] - -line : Str -> Task {} * -line = \str -> Effect.map (Effect.putLine str) (\_ -> Ok {}) diff --git a/examples/interactive/cli-platform/Task.roc b/examples/interactive/cli-platform/Task.roc deleted file mode 100644 index 2561266da9..0000000000 --- a/examples/interactive/cli-platform/Task.roc +++ /dev/null @@ -1,87 +0,0 @@ -interface Task - exposes [Task, succeed, fail, await, map, onFail, attempt, forever, loop] - imports [pf.Effect] - -Task ok err : Effect.Effect (Result ok err) - -forever : Task val err -> Task * err -forever = \task -> - looper = \{} -> - task - |> Effect.map - \res -> - when res is - Ok _ -> - Step {} - Err e -> - Done (Err e) - - Effect.loop {} looper - -loop : state, (state -> Task [Step state, Done done] err) -> Task done err -loop = \state, step -> - looper = \current -> - step current - |> Effect.map - \res -> - when res is - Ok (Step newState) -> - Step newState - Ok (Done result) -> - Done (Ok result) - Err e -> - Done (Err e) - - Effect.loop state looper - -succeed : val -> Task val * -succeed = \val -> - Effect.always (Ok val) - -fail : err -> Task * err -fail = \val -> - Effect.always (Err val) - -attempt : Task a b, (Result a b -> Task c d) -> Task c d -attempt = \effect, transform -> - Effect.after - effect - \result -> - when result is - Ok ok -> - transform (Ok ok) - Err err -> - transform (Err err) - -await : Task a err, (a -> Task b err) -> Task b err -await = \effect, transform -> - Effect.after - effect - \result -> - when result is - Ok a -> - transform a - Err err -> - Task.fail err - -onFail : Task ok a, (a -> Task ok b) -> Task ok b -onFail = \effect, transform -> - Effect.after - effect - \result -> - when result is - Ok a -> - Task.succeed a - Err err -> - transform err - -map : Task a err, (a -> b) -> Task b err -map = \effect, transform -> - Effect.after - effect - \result -> - when result is - Ok a -> - Task.succeed (transform a) - Err err -> - Task.fail err diff --git a/examples/interactive/cli-platform/build.rs b/examples/interactive/cli-platform/build.rs deleted file mode 100644 index 73159e387c..0000000000 --- a/examples/interactive/cli-platform/build.rs +++ /dev/null @@ -1,4 +0,0 @@ -fn main() { - println!("cargo:rustc-link-lib=dylib=app"); - println!("cargo:rustc-link-search=."); -} diff --git a/examples/interactive/cli-platform/host.c b/examples/interactive/cli-platform/host.c deleted file mode 100644 index 645d900c8e..0000000000 --- a/examples/interactive/cli-platform/host.c +++ /dev/null @@ -1,3 +0,0 @@ -extern int rust_main(); - -int main() { return rust_main(); } diff --git a/examples/interactive/cli-platform/src/lib.rs b/examples/interactive/cli-platform/src/lib.rs deleted file mode 100644 index 47e6e62424..0000000000 --- a/examples/interactive/cli-platform/src/lib.rs +++ /dev/null @@ -1,125 +0,0 @@ -#![allow(non_snake_case)] - -use core::alloc::Layout; -use core::ffi::c_void; -use core::mem::{ManuallyDrop, MaybeUninit}; -use libc; -use roc_std::RocStr; -use std::ffi::CStr; -use std::os::raw::c_char; - -extern "C" { - #[link_name = "roc__mainForHost_1_exposed_generic"] - fn roc_main(output: *mut u8); - - #[link_name = "roc__mainForHost_size"] - fn roc_main_size() -> i64; - - #[link_name = "roc__mainForHost_1_Fx_caller"] - fn call_Fx(flags: *const u8, closure_data: *const u8, output: *mut u8); - - #[allow(dead_code)] - #[link_name = "roc__mainForHost_1_Fx_size"] - fn size_Fx() -> i64; - - #[link_name = "roc__mainForHost_1_Fx_result_size"] - fn size_Fx_result() -> i64; -} - -#[no_mangle] -pub unsafe extern "C" fn roc_alloc(size: usize, _alignment: u32) -> *mut c_void { - libc::malloc(size) -} - -#[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) -} - -#[no_mangle] -pub unsafe extern "C" fn roc_dealloc(c_ptr: *mut c_void, _alignment: u32) { - libc::free(c_ptr) -} - -#[no_mangle] -pub unsafe extern "C" fn roc_panic(c_ptr: *mut c_void, tag_id: u32) { - match tag_id { - 0 => { - let slice = CStr::from_ptr(c_ptr as *const c_char); - let string = slice.to_str().unwrap(); - eprintln!("Roc hit a panic: {}", string); - std::process::exit(1); - } - _ => todo!(), - } -} - -#[no_mangle] -pub unsafe extern "C" fn roc_memcpy(dst: *mut c_void, src: *mut c_void, n: usize) -> *mut c_void { - libc::memcpy(dst, src, n) -} - -#[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) -} - -#[no_mangle] -pub extern "C" fn rust_main() -> i32 { - let size = unsafe { roc_main_size() } as usize; - let layout = Layout::array::(size).unwrap(); - - unsafe { - // TODO allocate on the stack if it's under a certain size - let buffer = std::alloc::alloc(layout); - - roc_main(buffer); - - let result = call_the_closure(buffer); - - std::alloc::dealloc(buffer, layout); - - result - }; - - // Exit code - 0 -} - -unsafe fn call_the_closure(closure_data_ptr: *const u8) -> i64 { - let size = size_Fx_result() as usize; - let layout = Layout::array::(size).unwrap(); - let buffer = std::alloc::alloc(layout) as *mut u8; - - call_Fx( - // This flags pointer will never get dereferenced - MaybeUninit::uninit().as_ptr(), - closure_data_ptr as *const u8, - buffer as *mut u8, - ); - - std::alloc::dealloc(buffer, layout); - - 0 -} - -#[no_mangle] -pub extern "C" fn roc_fx_getLine() -> RocStr { - use std::io::{self, BufRead}; - - let stdin = io::stdin(); - let line1 = stdin.lock().lines().next().unwrap().unwrap(); - - RocStr::from(line1.as_str()) -} - -#[no_mangle] -pub extern "C" fn roc_fx_putLine(line: &RocStr) { - let string = line.as_str(); - println!("{}", string); -} diff --git a/examples/interactive/cli-platform/src/main.rs b/examples/interactive/cli-platform/src/main.rs deleted file mode 100644 index 51175f934b..0000000000 --- a/examples/interactive/cli-platform/src/main.rs +++ /dev/null @@ -1,3 +0,0 @@ -fn main() { - std::process::exit(host::rust_main()); -} diff --git a/examples/interactive/countdown.roc b/examples/interactive/countdown.roc deleted file mode 100644 index 360e926e68..0000000000 --- a/examples/interactive/countdown.roc +++ /dev/null @@ -1,18 +0,0 @@ -app "countdown" - packages { pf: "cli-platform" } - imports [pf.Stdin, pf.Stdout, pf.Task.{ await, loop, succeed }] - provides [main] to pf - -main = - _ <- await (Stdout.line "\nLet's count down from 10 together - all you have to do is press .") - _ <- await Stdin.line - loop 10 tick - -tick = \n -> - if n == 0 then - _ <- await (Stdout.line "🎉 SURPRISE! Happy Birthday! 🎂") - succeed (Done {}) - else - _ <- await (n |> Num.toStr |> \s -> "\(s)..." |> Stdout.line) - _ <- await Stdin.line - succeed (Step (n - 1)) diff --git a/examples/interactive/echo.roc b/examples/interactive/echo.roc deleted file mode 100644 index 794ec2ab99..0000000000 --- a/examples/interactive/echo.roc +++ /dev/null @@ -1,33 +0,0 @@ -app "echo" - packages { pf: "cli-platform" } - imports [pf.Stdin, pf.Stdout, pf.Task] - provides [main] to pf - -main : Task.Task {} [] -main = - _ <- Task.await (Stdout.line "🗣 Shout into this cave and hear the echo! 👂👂👂") - Task.loop {} (\_ -> Task.map tick Step) - -tick : Task.Task {} [] -tick = - shout <- Task.await Stdin.line - Stdout.line (echo shout) - -echo : Str -> Str -echo = \shout -> - silence = \length -> - spaceInUtf8 = 32 - - List.repeat spaceInUtf8 length - - shout - |> Str.toUtf8 - |> List.mapWithIndex - (\_, i -> - length = (List.len (Str.toUtf8 shout) - i) - phrase = (List.split (Str.toUtf8 shout) length).before - - List.concat (silence (if i == 0 then 2 * length else length)) phrase) - |> List.join - |> Str.fromUtf8 - |> Result.withDefault "" diff --git a/examples/interactive/effects-platform/Package-Config.roc b/examples/interactive/effects-platform/Package-Config.roc deleted file mode 100644 index 5e718ffc47..0000000000 --- a/examples/interactive/effects-platform/Package-Config.roc +++ /dev/null @@ -1,9 +0,0 @@ -platform "effects" - requires {} { main : Effect.Effect {} } - exposes [] - packages {} - imports [pf.Effect] - provides [mainForHost] - -mainForHost : Effect.Effect {} as Fx -mainForHost = main diff --git a/examples/interactive/effects-platform/host.zig b/examples/interactive/effects-platform/host.zig deleted file mode 100644 index 24a9b1eea4..0000000000 --- a/examples/interactive/effects-platform/host.zig +++ /dev/null @@ -1,208 +0,0 @@ -const std = @import("std"); -const str = @import("str"); -const RocStr = str.RocStr; -const testing = std.testing; -const expectEqual = testing.expectEqual; -const expect = testing.expect; -const maxInt = std.math.maxInt; - -comptime { - // This is a workaround for https://github.com/ziglang/zig/issues/8218 - // which is only necessary on macOS. - // - // Once that issue is fixed, we can undo the changes in - // 177cf12e0555147faa4d436e52fc15175c2c4ff0 and go back to passing - // -fcompiler-rt in link.rs instead of doing this. Note that this - // workaround is present in many host.zig files, so make sure to undo - // it everywhere! - const builtin = @import("builtin"); - if (builtin.os.tag == .macos) { - _ = @import("compiler_rt"); - } -} - -const mem = std.mem; -const Allocator = mem.Allocator; - -extern fn roc__mainForHost_1_exposed_generic([*]u8) void; -extern fn roc__mainForHost_size() i64; -extern fn roc__mainForHost_1_Fx_caller(*const u8, [*]u8, [*]u8) void; -extern fn roc__mainForHost_1_Fx_size() i64; -extern fn roc__mainForHost_1_Fx_result_size() i64; - -const Align = 2 * @alignOf(usize); -extern fn malloc(size: usize) callconv(.C) ?*align(Align) anyopaque; -extern fn realloc(c_ptr: [*]align(Align) u8, size: usize) callconv(.C) ?*anyopaque; -extern fn free(c_ptr: [*]align(Align) u8) callconv(.C) void; -extern fn memcpy(dst: [*]u8, src: [*]u8, size: usize) callconv(.C) void; -extern fn memset(dst: [*]u8, value: i32, size: usize) callconv(.C) void; - -const DEBUG: bool = false; - -export fn roc_alloc(size: usize, alignment: u32) callconv(.C) ?*anyopaque { - if (DEBUG) { - var ptr = malloc(size); - const stdout = std.io.getStdOut().writer(); - stdout.print("alloc: {d} (alignment {d}, size {d})\n", .{ ptr, alignment, size }) catch unreachable; - return ptr; - } else { - return malloc(size); - } -} - -export fn roc_realloc(c_ptr: *anyopaque, new_size: usize, old_size: usize, alignment: u32) callconv(.C) ?*anyopaque { - if (DEBUG) { - const stdout = std.io.getStdOut().writer(); - stdout.print("realloc: {d} (alignment {d}, old_size {d})\n", .{ c_ptr, alignment, old_size }) catch unreachable; - } - - return realloc(@alignCast(Align, @ptrCast([*]u8, c_ptr)), new_size); -} - -export fn roc_dealloc(c_ptr: *anyopaque, alignment: u32) callconv(.C) void { - if (DEBUG) { - const stdout = std.io.getStdOut().writer(); - stdout.print("dealloc: {d} (alignment {d})\n", .{ c_ptr, alignment }) catch unreachable; - } - - free(@alignCast(Align, @ptrCast([*]u8, c_ptr))); -} - -export fn roc_panic(c_ptr: *anyopaque, tag_id: u32) callconv(.C) void { - _ = tag_id; - - const stderr = std.io.getStdErr().writer(); - const msg = @ptrCast([*:0]const u8, c_ptr); - stderr.print("Application crashed with message\n\n {s}\n\nShutting down\n", .{msg}) catch unreachable; - std.process.exit(0); -} - -export fn roc_memcpy(dst: [*]u8, src: [*]u8, size: usize) callconv(.C) void { - return memcpy(dst, src, size); -} - -export fn roc_memset(dst: [*]u8, value: i32, size: usize) callconv(.C) void { - return memset(dst, value, size); -} - -const Unit = extern struct {}; - -pub export fn main() u8 { - const allocator = std.heap.page_allocator; - - const stderr = std.io.getStdErr().writer(); - - // NOTE the return size can be zero, which will segfault. Always allocate at least 8 bytes - const size = std.math.max(8, @intCast(usize, roc__mainForHost_size())); - const raw_output = allocator.allocAdvanced(u8, @alignOf(u64), @intCast(usize, size), .at_least) catch unreachable; - var output = @ptrCast([*]u8, raw_output); - - defer { - allocator.free(raw_output); - } - - var ts1: std.os.timespec = undefined; - std.os.clock_gettime(std.os.CLOCK.REALTIME, &ts1) catch unreachable; - - roc__mainForHost_1_exposed_generic(output); - - call_the_closure(output); - - var ts2: std.os.timespec = undefined; - std.os.clock_gettime(std.os.CLOCK.REALTIME, &ts2) catch unreachable; - - const delta = to_seconds(ts2) - to_seconds(ts1); - - stderr.print("runtime: {d:.3}ms\n", .{delta * 1000}) catch unreachable; - - return 0; -} - -fn to_seconds(tms: std.os.timespec) f64 { - return @intToFloat(f64, tms.tv_sec) + (@intToFloat(f64, tms.tv_nsec) / 1_000_000_000.0); -} - -fn call_the_closure(closure_data_pointer: [*]u8) void { - const allocator = std.heap.page_allocator; - - const size = roc__mainForHost_1_Fx_result_size(); - - if (size == 0) { - // the function call returns an empty record - // allocating 0 bytes causes issues because the allocator will return a NULL pointer - // So it's special-cased - const flags: u8 = 0; - var result: [1]u8 = .{0}; - roc__mainForHost_1_Fx_caller(&flags, closure_data_pointer, &result); - - return; - } - - const raw_output = allocator.allocAdvanced(u8, @alignOf(u64), @intCast(usize, size), .at_least) catch unreachable; - var output = @ptrCast([*]u8, raw_output); - - defer { - allocator.free(raw_output); - } - - const flags: u8 = 0; - roc__mainForHost_1_Fx_caller(&flags, closure_data_pointer, output); - - return; -} - -pub export fn roc_fx_getLine() str.RocStr { - return roc_fx_getLine_help() catch return str.RocStr.empty(); -} - -fn roc_fx_getLine_help() !RocStr { - const stdin = std.io.getStdIn().reader(); - var buf: [400]u8 = undefined; - - const line: []u8 = (try stdin.readUntilDelimiterOrEof(&buf, '\n')) orelse ""; - - return str.RocStr.init(@ptrCast([*]const u8, line), line.len); -} - -pub export fn roc_fx_putLine(rocPath: *str.RocStr) i64 { - const stdout = std.io.getStdOut().writer(); - - for (rocPath.asSlice()) |char| { - stdout.print("{c}", .{char}) catch unreachable; - } - - stdout.print("\n", .{}) catch unreachable; - - return 0; -} - -const GetInt = extern struct { - value: i64, - error_code: u8, - is_error: bool, -}; - -pub export fn roc_fx_getInt() GetInt { - if (roc_fx_getInt_help()) |value| { - const get_int = GetInt{ .is_error = false, .value = value, .error_code = 0 }; - return get_int; - } else |err| switch (err) { - error.InvalidCharacter => { - return GetInt{ .is_error = true, .value = 0, .error_code = 0 }; - }, - else => { - return GetInt{ .is_error = true, .value = 0, .error_code = 1 }; - }, - } - - return 0; -} - -fn roc_fx_getInt_help() !i64 { - const stdin = std.io.getStdIn().reader(); - var buf: [40]u8 = undefined; - - const line: []u8 = (try stdin.readUntilDelimiterOrEof(&buf, '\n')) orelse ""; - - return std.fmt.parseInt(i64, line, 10); -} diff --git a/examples/interactive/form.roc b/examples/interactive/form.roc deleted file mode 100644 index b6a52276e7..0000000000 --- a/examples/interactive/form.roc +++ /dev/null @@ -1,12 +0,0 @@ -app "form" - packages { pf: "cli-platform" } - imports [pf.Stdin, pf.Stdout, pf.Task.{ await, Task }] - provides [main] to pf - -main : Task {} * -main = - _ <- await (Stdout.line "What's your first name?") - firstName <- await Stdin.line - _ <- await (Stdout.line "What's your last name?") - lastName <- await Stdin.line - Stdout.line "Hi, \(firstName) \(lastName)! 👋" diff --git a/examples/interactive/tui-platform/Program.roc b/examples/interactive/tui-platform/Program.roc deleted file mode 100644 index 0023b16b59..0000000000 --- a/examples/interactive/tui-platform/Program.roc +++ /dev/null @@ -1,10 +0,0 @@ -interface Program - exposes [Program] - imports [] - -Program model : - { - init : {} -> model, - update : model, Str -> model, - view : model -> Str, - } diff --git a/examples/interactive/tui-platform/host.zig b/examples/interactive/tui-platform/host.zig deleted file mode 100644 index dfdf6f1707..0000000000 --- a/examples/interactive/tui-platform/host.zig +++ /dev/null @@ -1,269 +0,0 @@ -const std = @import("std"); -const str = @import("str"); -const RocStr = str.RocStr; -const testing = std.testing; -const expectEqual = testing.expectEqual; -const expect = testing.expect; -const maxInt = std.math.maxInt; - -comptime { - // This is a workaround for https://github.com/ziglang/zig/issues/8218 - // which is only necessary on macOS. - // - // Once that issue is fixed, we can undo the changes in - // 177cf12e0555147faa4d436e52fc15175c2c4ff0 and go back to passing - // -fcompiler-rt in link.rs instead of doing this. Note that this - // workaround is present in many host.zig files, so make sure to undo - // it everywhere! - const builtin = @import("builtin"); - if (builtin.os.tag == .macos) { - _ = @import("compiler_rt"); - } -} - -const mem = std.mem; -const Allocator = mem.Allocator; - -const Program = extern struct { init: RocStr, update: Unit, view: Unit }; - -extern fn roc__mainForHost_1_exposed() Program; -extern fn roc__mainForHost_size() i64; - -const ConstModel = [*]const u8; -const MutModel = [*]u8; - -extern fn roc__mainForHost_1_Init_caller([*]u8, [*]u8, MutModel) void; -extern fn roc__mainForHost_1_Init_size() i64; -extern fn roc__mainForHost_1_Init_result_size() i64; - -fn allocate_model(allocator: *Allocator) MutModel { - const size = roc__mainForHost_1_Init_result_size(); - const raw_output = allocator.allocAdvanced(u8, @alignOf(u64), @intCast(usize, size), .at_least) catch unreachable; - var output = @ptrCast([*]u8, raw_output); - - return output; -} - -fn init(allocator: *Allocator) ConstModel { - const closure: [*]u8 = undefined; - const output = allocate_model(allocator); - - roc__mainForHost_1_Init_caller(closure, closure, output); - - return output; -} - -extern fn roc__mainForHost_1_Update_caller(ConstModel, *const RocStr, [*]u8, MutModel) void; -extern fn roc__mainForHost_1_Update_size() i64; -extern fn roc__mainForHost_1_Update_result_size() i64; - -fn update(allocator: *Allocator, model: ConstModel, msg: RocStr) ConstModel { - const closure: [*]u8 = undefined; - const output = allocate_model(allocator); - - roc__mainForHost_1_Update_caller(model, &msg, closure, output); - - return output; -} - -extern fn roc__mainForHost_1_View_caller(ConstModel, [*]u8, *RocStr) void; -extern fn roc__mainForHost_1_View_size() i64; -extern fn roc__mainForHost_1_View_result_size() i64; - -fn view(input: ConstModel) RocStr { - const closure: [*]u8 = undefined; - var output: RocStr = undefined; - - roc__mainForHost_1_View_caller(input, closure, &output); - - return output; -} - -const Align = 2 * @alignOf(usize); -extern fn malloc(size: usize) callconv(.C) ?*align(Align) anyopaque; -extern fn realloc(c_ptr: [*]align(Align) u8, size: usize) callconv(.C) ?*anyopaque; -extern fn free(c_ptr: [*]align(Align) u8) callconv(.C) void; -extern fn memcpy(dst: [*]u8, src: [*]u8, size: usize) callconv(.C) void; -extern fn memset(dst: [*]u8, value: i32, size: usize) callconv(.C) void; - -const DEBUG: bool = false; - -export fn roc_alloc(size: usize, alignment: u32) callconv(.C) ?*anyopaque { - if (DEBUG) { - var ptr = malloc(size); - const stdout = std.io.getStdOut().writer(); - stdout.print("alloc: {d} (alignment {d}, size {d})\n", .{ ptr, alignment, size }) catch unreachable; - return ptr; - } else { - return malloc(size); - } -} - -export fn roc_realloc(c_ptr: *anyopaque, new_size: usize, old_size: usize, alignment: u32) callconv(.C) ?*anyopaque { - if (DEBUG) { - const stdout = std.io.getStdOut().writer(); - stdout.print("realloc: {d} (alignment {d}, old_size {d})\n", .{ c_ptr, alignment, old_size }) catch unreachable; - } - - return realloc(@alignCast(Align, @ptrCast([*]u8, c_ptr)), new_size); -} - -export fn roc_dealloc(c_ptr: *anyopaque, alignment: u32) callconv(.C) void { - if (DEBUG) { - const stdout = std.io.getStdOut().writer(); - stdout.print("dealloc: {d} (alignment {d})\n", .{ c_ptr, alignment }) catch unreachable; - } - - free(@alignCast(Align, @ptrCast([*]u8, c_ptr))); -} - -export fn roc_panic(c_ptr: *anyopaque, tag_id: u32) callconv(.C) void { - _ = tag_id; - - const stderr = std.io.getStdErr().writer(); - const msg = @ptrCast([*:0]const u8, c_ptr); - stderr.print("Application crashed with message\n\n {s}\n\nShutting down\n", .{msg}) catch unreachable; - std.process.exit(0); -} - -export fn roc_memcpy(dst: [*]u8, src: [*]u8, size: usize) callconv(.C) void { - return memcpy(dst, src, size); -} - -export fn roc_memset(dst: [*]u8, value: i32, size: usize) callconv(.C) void { - return memset(dst, value, size); -} - -const Unit = extern struct {}; - -pub export fn main() callconv(.C) u8 { - var ts1: std.os.timespec = undefined; - std.os.clock_gettime(std.os.CLOCK.REALTIME, &ts1) catch unreachable; - - const program = roc__mainForHost_1_exposed(); - - call_the_closure(program); - - var ts2: std.os.timespec = undefined; - std.os.clock_gettime(std.os.CLOCK.REALTIME, &ts2) catch unreachable; - - const delta = to_seconds(ts2) - to_seconds(ts1); - - const stderr = std.io.getStdErr().writer(); - stderr.print("runtime: {d:.3}ms\n", .{delta * 1000}) catch unreachable; - - return 0; -} - -fn to_seconds(tms: std.os.timespec) f64 { - return @intToFloat(f64, tms.tv_sec) + (@intToFloat(f64, tms.tv_nsec) / 1_000_000_000.0); -} - -fn call_the_closure(program: Program) void { - _ = program; - - var allocator = std.heap.page_allocator; - const stdout = std.io.getStdOut().writer(); - const stdin = std.io.getStdIn().reader(); - - var buf: [1000]u8 = undefined; - - var model = init(&allocator); - - while (true) { - const line = (stdin.readUntilDelimiterOrEof(buf[0..], '\n') catch unreachable) orelse return; - - if (line.len == 1 and line[0] == 'q') { - return; - } - - const to_append = RocStr.init(line.ptr, line.len); - - model = update(&allocator, model, to_append); - - const viewed = view(model); - for (viewed.asSlice()) |char| { - stdout.print("{c}", .{char}) catch unreachable; - } - - stdout.print("\n", .{}) catch unreachable; - } - - // The closure returns result, nothing interesting to do with it - return; -} - -pub export fn roc_fx_putInt(int: i64) i64 { - const stdout = std.io.getStdOut().writer(); - - stdout.print("{d}", .{int}) catch unreachable; - - stdout.print("\n", .{}) catch unreachable; - - return 0; -} - -export fn roc_fx_putLine(rocPath: str.RocStr) callconv(.C) void { - const stdout = std.io.getStdOut().writer(); - - for (rocPath.asSlice()) |char| { - stdout.print("{c}", .{char}) catch unreachable; - } - - stdout.print("\n", .{}) catch unreachable; -} - -const GetInt = extern struct { - value: i64, - error_code: bool, - is_error: bool, -}; - -comptime { - if (@sizeOf(usize) == 8) { - @export(roc_fx_getInt_64bit, .{ .name = "roc_fx_getInt" }); - } else { - @export(roc_fx_getInt_32bit, .{ .name = "roc_fx_getInt" }); - } -} - -fn roc_fx_getInt_64bit() callconv(.C) GetInt { - if (roc_fx_getInt_help()) |value| { - const get_int = GetInt{ .is_error = false, .value = value, .error_code = false }; - return get_int; - } else |err| switch (err) { - error.InvalidCharacter => { - return GetInt{ .is_error = true, .value = 0, .error_code = false }; - }, - else => { - return GetInt{ .is_error = true, .value = 0, .error_code = true }; - }, - } - - return 0; -} - -fn roc_fx_getInt_32bit(output: *GetInt) callconv(.C) void { - if (roc_fx_getInt_help()) |value| { - const get_int = GetInt{ .is_error = false, .value = value, .error_code = false }; - output.* = get_int; - } else |err| switch (err) { - error.InvalidCharacter => { - output.* = GetInt{ .is_error = true, .value = 0, .error_code = false }; - }, - else => { - output.* = GetInt{ .is_error = true, .value = 0, .error_code = true }; - }, - } - - return; -} - -fn roc_fx_getInt_help() !i64 { - const stdin = std.io.getStdIn().reader(); - var buf: [40]u8 = undefined; - - const line: []u8 = (try stdin.readUntilDelimiterOrEof(&buf, '\n')) orelse ""; - - return std.fmt.parseInt(i64, line, 10); -} diff --git a/examples/jvm-interop/.gitignore b/examples/jvm-interop/.gitignore new file mode 100644 index 0000000000..28b74819c0 --- /dev/null +++ b/examples/jvm-interop/.gitignore @@ -0,0 +1,7 @@ +*.log +*.class +*.o +*.so +*.jar +libhello* +*.h diff --git a/examples/jvm-interop/README.md b/examples/jvm-interop/README.md new file mode 100644 index 0000000000..0a014226cb --- /dev/null +++ b/examples/jvm-interop/README.md @@ -0,0 +1,210 @@ +# JVM interop +This is a demo for calling Roc code from Java, and some other JVM languages. + + +## Prerequisites + +The following was tested on NixOS, with `openjdk 17.0.5` and `clang 13.0.1` but should work with most recent versions of those (jdk>=10) on most modern Linux and MacOS.\ +You're welcome to test on your machine and tell me (via [Zulip](https://roc.zulipchat.com/#narrow/pm-with/583319-dank)) if you ran into any issues or limitations. + +## Goal +We'll make a few examples, showing basic data type convertions and function calls between Roc and Java (and later some other JVM languages): +- A string formatter. +- A Function that multiples an array by a scalar. +- A factorial function that, for the sake of demonstration, throws a RuntimeException for negative integers. + +This will be done with the help of [Java Native Interface](https://docs.oracle.com/javase/8/docs/technotes/guides/jni/). +We will be using C to bridge between Java and Roc. + +## Structure +As the time of writing this post, the following is the current bare bones tree of a jvm-interop: + +``` console +. +├── impl.roc +├── platform.roc +├── bridge.c +└── javaSource + └── Demo.java +``` + +impl.roc is the application where we actually implement our native Roc functions.\ +platform.roc as the name suggests contains platform logic, (but doesn't really have much here, mostly just) exposes functions to the host - bridge.c\ +bridge.c is the JNI bridge, it's the host that implements the Roc functions (e.g roc_alloc) and the JNI functions that act like the bridge between Roc and Java (bridge as in, doing type conversions between the languages, needed jvm boilerplate, etc). + +For each of our native Roc functions, in the application (impl.roc), we have a corresponding `Java_javaSource_Demo_FUNC` C function that handles the "behind the scenes", this includes type conversions between the languages, transforming roc panics into java exceptions and basically all the glue code necessary. + + +Just so you know what to expect, our Roc functions look like this; +``` coffee +interpolateString : Str -> Str +interpolateString = \name -> + "Hello from Roc \(name)!!!🤘🤘🤘" + + +mulArrByScalar : List I32, I32 -> List I32 +mulArrByScalar = \arr, scalar -> + List.map arr \x -> x * scalar + + +factorial : I64 -> I64 +factorial = \n -> + if n < 0 then + # while we get the chance, examplify a roc panic in an interop + crash "No negatives here!!!" + else if n == 0 then + 1 + else + n * (factorial (n - 1)) +``` + +Nothing too crazy. Again, do note how we crash if n < 0, see how this would play out from the Java side. + +Now let's take a quick look on the Java side of things; + +``` java +public class Demo { + + static { + System.loadLibrary("interop"); + } + + public static native String sayHello(String num); + + public static native int[] mulArrByScalar(int[] arr, int scalar); + + public static native long factorial(long n) throws RuntimeException; + + + public static void main(String[] args) { + + // string demo + System.out.println(sayHello("Brendan") + "\n"); + + // array demo + int[] arr = {10, 20, 30, 40}; + int x = 3; + System.out.println(Arrays.toString(arr) + + " multiplied by " + x + + " results in " + Arrays.toString(mulArrByScalar(arr, x)) + + "\n"); + + // number + panic demo + long n = 5; + System.out.println("Factorial of " + n + " is " + factorial(n)); + + } +} +``` +First we load our library - "interop", which is a shared library (`.so` file) that our Roc+C code compiles to.\ +Then, we declare our native functions with suitable types and throws annotation.\ +Finally in main we test it out with some inputs. + +## See it in action +##### For brevity's sake we'll run the build script and omit some of its (intentionally) verbose output: + +```console +[nix-shell:~/dev/roc/examples/jvm-interop]$ ./build.sh && java javaSource.Greeter +Hello from Roc Brendan!!!🤘🤘🤘 + +[10, 20, 30, 40] multiplied by 3 results in [30, 60, 90, 120] + +Factorial of 5 is 120 +``` +That's pretty cool!\ +Let's also see what happens if in the code above we define n to be -1: +``` console +[nix-shell:~/dev/roc/examples/jvm-interop]$ ./build.sh && java javaSource.Greeter +Hello from Roc Brendan!!!🤘🤘🤘 + +[10, 20, 30, 40] multiplied by 3 results in [30, 60, 90, 120] + +Exception in thread "main" java.lang.RuntimeException: No negatives here!!! + at javaSource.Demo.factorial(Native Method) + at javaSource.Demo.main(Demo.java:36) +``` +And as we expected, it runs the first two examples fine, throws a RuntimeException on the third. + +Since we're talking JVM Bytecode, we can pretty much call our native function from any language that speaks JVM Bytecode. + +Note: The JNI code depends on a dynamic lib, containing our native implementation, that now resides in our working directory.\ +So in the following examples, we'll make sure that our working directory is in LD_LIBRARY_PATH.\ +Generally speaking, you'd paobably add your dynamic library to a spot that's already on your path, for convenience sake.\ +So first, we run: + +```console +[nix-shell:~/dev/roc/examples/jvm-interop]$ export LD_LIBRARY_PATH=$(pwd):$LD_LIBRARY_PATH +``` + +Now, let's try Kotlin! +```console +[nix-shell:~/dev/roc/examples/jvm-interop]$ kotlin +Welcome to Kotlin version 1.7.20 (JRE 17.0.5+8-nixos) +Type :help for help, :quit for quit + +>>> import javaSource.Demo + +>>> Demo.sayHello("Kotlin Users") +res1: kotlin.String = Hello from Roc Kotlin Users!!!🤘🤘🤘 + +>>> Demo.mulArrByScalar(intArrayOf(10, 20, 30, 40), 101).contentToString() +res2: kotlin.String = [1010, 2020, 3030, 4040] + +>>> Demo.factorial(10) +res3: kotlin.Long = 3628800 +``` +And it just works, out of the box! + +Now let's do Scala + +```console +[nix-shell:~/dev/roc/examples/jvm-interop]$ scala +Welcome to Scala 2.13.10 (OpenJDK 64-Bit Server VM, Java 17.0.5). +Type in expressions for evaluation. Or try :help. + +scala> import javaSource.Demo +import javaSource.Demo + +scala> Demo.sayHello("Scala Users") +val res0: String = Hello from Roc Scala Users!!!🤘🤘🤘 + +scala> Demo.mulArrByScalar(Array(10, 20, 30, 40), 1001) +val res1: Array[Int] = Array(10010, 20020, 30030, 40040) + +scala> Demo.factorial(-2023) +java.lang.RuntimeException: No negatives here!!! + at javaSource.Demo.factorial(Native Method) + ... 32 elided +``` +And it also works beautifully. + +Last one - Clojure +Do note that in Clojure you need to add a `-Sdeps '{:paths ["."]}'` flag to add the working directory to paths. +``` console +[nix-shell:~/dev/roc/examples/jvm-interop]$ clj -Sdeps '{:paths ["."]}' +Clojure 1.11.1 +user=> (import 'javaSource.Demo) +javaSource.Demo + +user=> (Demo/sayHello "Clojure Users") +"Hello from Roc Clojure Users!!!🤘🤘🤘" + +user=> (seq (Demo/mulArrByScalar (int-array [10 20 30]) 9)) ; seq to pretty-print +(90 180 270) + +user=> (Demo/factorial 15) +1307674368000 +``` + +Test it out on your favorite JVM lang!\ +And again, if anything goes not according to plan, tell me in the link above and we'll figure it out. + +## Notes on building +The process is basically the following: +1. Build our application + platform .roc files with (`roc build impl.roc --no-link`) into an object file +2. Generate a C header file (for bridge.c's) using java. +3. Bundle up the C bridge together with our object file into a shared object. + +And that's it, use that shared object from your JVM language. Note every JVM language has its own way to declare that native library so you may want to look at it, or do like in the demo and declare it in java and use the binding from anywhere. + +I suggest reading the build script (build.sh) and adjusting according to your setup. diff --git a/examples/jvm-interop/bridge.c b/examples/jvm-interop/bridge.c new file mode 100644 index 0000000000..174d0c7cfd --- /dev/null +++ b/examples/jvm-interop/bridge.c @@ -0,0 +1,383 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include "javaSource_Demo.h" + +JavaVM* vm; + +#define ERR_MSG_MAX_SIZE 256 + +jmp_buf exception_buffer; +char* err_msg[ERR_MSG_MAX_SIZE] = {0}; + +jint JNI_OnLoad(JavaVM *loadedVM, void *reserved) +{ + // https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/invocation.html + vm = loadedVM; + return JNI_VERSION_1_2; +} + +void *roc_alloc(size_t size, unsigned int alignment) +{ + return malloc(size); +} + +void *roc_realloc(void *ptr, size_t new_size, size_t old_size, + unsigned int alignment) +{ + return realloc(ptr, new_size); +} + +void roc_dealloc(void *ptr, unsigned int alignment) +{ + free(ptr); +} + +void *roc_memset(void *str, int c, size_t n) +{ + return memset(str, c, n); +} + +// Reference counting + +// If the refcount is set to this, that means the allocation is +// stored in readonly memory in the binary, and we must not +// attempt to increment or decrement it; if we do, we'll segfault! +const ssize_t REFCOUNT_READONLY = 0; +const ssize_t REFCOUNT_ONE = (ssize_t)PTRDIFF_MIN; +const size_t MASK = (size_t)PTRDIFF_MIN; + +// Increment reference count, given a pointer to the first element in a collection. +// We don't need to check for overflow because in order to overflow a usize worth of refcounts, +// you'd need to somehow have more pointers in memory than the OS's virtual address space can hold. +void incref(uint8_t* bytes, uint32_t alignment) +{ + ssize_t *refcount_ptr = ((ssize_t *)bytes) - 1; + ssize_t refcount = *refcount_ptr; + + if (refcount != REFCOUNT_READONLY) { + *refcount_ptr = refcount + 1; + } +} + +// Decrement reference count, given a pointer to the first element in a collection. +// Then call roc_dealloc if nothing is referencing this collection anymore. +void decref(uint8_t* bytes, uint32_t alignment) +{ + if (bytes == NULL) { + return; + } + + size_t extra_bytes = (sizeof(size_t) >= (size_t)alignment) ? sizeof(size_t) : (size_t)alignment; + ssize_t *refcount_ptr = ((ssize_t *)bytes) - 1; + ssize_t refcount = *refcount_ptr; + + if (refcount != REFCOUNT_READONLY) { + *refcount_ptr = refcount - 1; + + if (refcount == REFCOUNT_ONE) { + void *original_allocation = (void *)(refcount_ptr - (extra_bytes - sizeof(size_t))); + + roc_dealloc(original_allocation, alignment); + } + } +} + +struct RocListI32 +{ + int32_t *bytes; + size_t len; + size_t capacity; +}; + +struct RocListI32 init_roclist_i32(int32_t *bytes, size_t len) +{ + if (len == 0) + { + struct RocListI32 ret = { + .len = 0, + .bytes = NULL, + .capacity = 0, + }; + + return ret; + } + else + { + size_t refcount_size = sizeof(size_t); + ssize_t* data = (ssize_t*)roc_alloc(len + refcount_size, alignof(size_t)); + data[0] = REFCOUNT_ONE; + int32_t *new_content = (int32_t *)(data + 1); + + struct RocListI32 ret; + + memcpy(new_content, bytes, len * sizeof(int32_t)); + + ret.bytes = new_content; + ret.len = len; + ret.capacity = len; + + return ret; + } +} +// RocListU8 (List U8) + +struct RocListU8 +{ + uint8_t *bytes; + size_t len; + size_t capacity; +}; + +struct RocListU8 init_roclist_u8(uint8_t *bytes, size_t len) +{ + if (len == 0) + { + struct RocListU8 ret = { + .len = 0, + .bytes = NULL, + .capacity = 0, + }; + + return ret; + } + else + { + + size_t refcount_size = sizeof(size_t); + ssize_t* data = (ssize_t*)roc_alloc(len + refcount_size, alignof(size_t)); + data[0] = REFCOUNT_ONE; + uint8_t *new_content = (uint8_t *)(data + 1); + + struct RocListU8 ret; + + memcpy(new_content, bytes, len * sizeof(uint8_t)); + + ret.bytes = new_content; + ret.len = len; + ret.capacity = len; + + return ret; + } +} + +// RocStr + +struct RocStr +{ + uint8_t *bytes; + size_t len; + size_t capacity; +}; + +struct RocStr init_rocstr(uint8_t *bytes, size_t len) +{ + if (len < sizeof(struct RocStr)) + { + // Start out with zeroed memory, so that + // if we end up comparing two small RocStr values + // for equality, we won't risk memory garbage resulting + // in two equal strings appearing unequal. + struct RocStr ret = { + .len = 0, + .bytes = NULL, + .capacity = 0, + }; + + // Copy the bytes into the stack allocation + memcpy(&ret, bytes, len); + + // Record the string's len in the last byte of the stack allocation + ((uint8_t *)&ret)[sizeof(struct RocStr) - 1] = (uint8_t)len | 0b10000000; + + return ret; + } + else + { + // A large RocStr is the same as a List U8 (aka RocListU8) in memory. + struct RocListU8 roc_bytes = init_roclist_u8(bytes, len); + + struct RocStr ret = { + .len = roc_bytes.len, + .bytes = roc_bytes.bytes, + .capacity = roc_bytes.capacity, + }; + + return ret; + } +} + +bool is_small_str(struct RocStr str) +{ + return ((ssize_t)str.capacity) < 0; +} + +bool is_seamless_str_slice(struct RocStr str) +{ + return ((ssize_t)str.len) < 0; +} + +bool is_seamless_listi32_slice(struct RocListI32 list) +{ + return ((ssize_t)list.capacity) < 0; +} + +// Determine the len of the string, taking into +// account the small string optimization +size_t roc_str_len(struct RocStr str) +{ + uint8_t *bytes = (uint8_t *)&str; + uint8_t last_byte = bytes[sizeof(str) - 1]; + uint8_t last_byte_xored = last_byte ^ 0b10000000; + size_t small_len = (size_t)(last_byte_xored); + size_t big_len = str.len; + + // Avoid branch misprediction costs by always + // determining both small_len and big_len, + // so this compiles to a cmov instruction. + if (is_small_str(str)) + { + return small_len; + } + else + { + return big_len; + } +} + +__attribute__((noreturn)) void roc_panic(struct RocStr *msg, unsigned int tag_id) +{ + char* bytes = is_small_str(*msg) ? (char*)msg : (char*)msg->bytes; + const size_t str_len = roc_str_len(*msg); + + int len = str_len > ERR_MSG_MAX_SIZE ? ERR_MSG_MAX_SIZE : str_len; + strncpy((char*)err_msg, bytes, len); + + // Free the underlying allocation if needed. + if (!is_small_str(*msg)) { + if (is_seamless_str_slice(*msg)){ + decref((uint8_t *)(msg->capacity << 1), alignof(uint8_t *)); + } + else { + decref(msg->bytes, alignof(uint8_t *)); + } + } + + longjmp(exception_buffer, 1); +} + +void roc_dbg(struct RocStr *loc, struct RocStr *msg) { + char* loc_bytes = is_small_str(*loc) ? (char*)loc : (char*)loc->bytes; + char* msg_bytes = is_small_str(*msg) ? (char*)msg : (char*)msg->bytes; + fprintf(stderr, "[%s] %s\n", loc_bytes, msg_bytes); +} + +extern void roc__programForHost_1__InterpolateString_caller(struct RocStr *name, char *closure_data, struct RocStr *ret); + +extern void roc__programForHost_1__MulArrByScalar_caller(struct RocListI32 *arr, int32_t *scalar, char *closure_data, struct RocListI32 *ret); + +extern void roc__programForHost_1__Factorial_caller(int64_t *scalar, char *closure_data, int64_t *ret); + + +JNIEXPORT jstring JNICALL Java_javaSource_Demo_sayHello + (JNIEnv *env, jobject thisObj, jstring name) +{ + const char *jnameChars = (*env)->GetStringUTFChars(env, name, 0); + // we copy just in case the jvm would try to reclaim that mem + uint8_t *cnameChars = (uint8_t *)strdup(jnameChars); + size_t nameLength = (size_t) (*env)->GetStringLength(env, name); + (*env)->ReleaseStringUTFChars(env, name, jnameChars); + + + struct RocStr rocName = init_rocstr(cnameChars, nameLength); + struct RocStr ret = {0}; + + // Call the Roc function to populate `ret`'s bytes. + roc__programForHost_1__InterpolateString_caller(&rocName, 0, &ret); + jbyte *bytes = (jbyte*)(is_small_str(ret) ? (uint8_t*)&ret : ret.bytes); + + // java being java making this a lot harder than it needs to be + // https://stackoverflow.com/questions/32205446/getting-true-utf-8-characters-in-java-jni + // https://docs.oracle.com/javase/1.5.0/docs/guide/jni/spec/types.html#wp16542 + // but as i refuse converting those manually to their correct form, we just let the jvm handle the conversion + // by first making a java byte array then converting the byte array to our final jstring + jbyteArray byteArray = (*env)->NewByteArray(env, ret.len); + (*env)->SetByteArrayRegion(env, byteArray, 0, ret.len, bytes); + + jstring charsetName = (*env)->NewStringUTF(env, "UTF-8"); + jclass stringClass = (*env)->FindClass(env, "java/lang/String"); + // https://docs.oracle.com/javase/7/docs/jdk/api/jpda/jdi/com/sun/jdi/doc-files/signature.html + jmethodID stringConstructor = (*env)->GetMethodID(env, stringClass, "", "([BLjava/lang/String;)V"); + jstring result = (*env)->NewObject(env, stringClass, stringConstructor, byteArray, charsetName); + + // cleanup + if (!is_seamless_str_slice(ret)) { + decref(ret.bytes, alignof(uint8_t *)); + } + + (*env)->DeleteLocalRef(env, charsetName); + (*env)->DeleteLocalRef(env, byteArray); + + free(cnameChars); + + return result; +} + + +JNIEXPORT jintArray JNICALL Java_javaSource_Demo_mulArrByScalar + (JNIEnv *env, jobject thisObj, jintArray arr, jint scalar) +{ + // extract data from jvm types + jint* jarr = (*env)->GetIntArrayElements(env, arr, NULL); + jsize len = (*env)->GetArrayLength(env, arr); + + // pass data to platform + struct RocListI32 originalArray = init_roclist_i32(jarr, len); + incref((void *)&originalArray, alignof(int32_t*)); + struct RocListI32 ret = {0}; + + roc__programForHost_1__MulArrByScalar_caller(&originalArray, &scalar, 0, &ret); + + // create jvm constructs + jintArray multiplied = (*env)->NewIntArray(env, ret.len); + (*env)->SetIntArrayRegion(env, multiplied, 0, ret.len, (jint*) ret.bytes); + + // cleanup + (*env)->ReleaseIntArrayElements(env, arr, jarr, 0); + + if (is_seamless_listi32_slice(ret)) { + decref((void *)(ret.capacity << 1), alignof(uint8_t *)); + } + else { + decref((void *)ret.bytes, alignof(uint8_t *)); + } + + return multiplied; +} + +JNIEXPORT jlong JNICALL Java_javaSource_Demo_factorial + (JNIEnv *env, jobject thisObj, jlong num) +{ + int64_t ret; + // can crash - meaning call roc_panic, so we set a jump here + if (setjmp(exception_buffer)) { + // exception was thrown, handle it + jclass exClass = (*env)->FindClass(env, "java/lang/RuntimeException"); + const char *msg = (const char *)err_msg; + return (*env)->ThrowNew(env, exClass, msg); + } + else { + int64_t n = (int64_t)num; + roc__programForHost_1__Factorial_caller(&n, 0, &ret); + return ret; + } +} diff --git a/examples/jvm-interop/build.sh b/examples/jvm-interop/build.sh new file mode 100755 index 0000000000..08feb805c4 --- /dev/null +++ b/examples/jvm-interop/build.sh @@ -0,0 +1,42 @@ +#!/bin/sh + +# https://vaneyckt.io/posts/safer_bash_scripts_with_set_euxo_pipefail/ +set -euxo pipefail + +# don't forget to validate that $JAVA_HOME is defined, the following would not work without it! +# set it either globally or here +# export JAVA_HOME=/your/java/installed/dir +# in nixos, to set it globally, i needed to say `programs.java.enable = true;` in `/etc/nixos/configuration.nix` + + +# if roc is in your path, you could +# roc build impl.roc --no-link +# else, assuming in roc repo and that you ran `cargo run --release` +../../target/release/roc build impl.roc --no-link + + +# make jvm look here to see libinterop.so +export LD_LIBRARY_PATH=$(pwd):$LD_LIBRARY_PATH + +# needs jdk10 + +# "-h ." is for placing the jni.h header in the cwd. +# the "javaSource" directory may seem redundant (why not just a plain java file), +# but this is the way of java packaging +# we could go without it with an "implicit" package, but that would ache later on, +# especially with other JVM langs +javac -h . javaSource/Demo.java + + +clang \ + -g -Wall \ + -fPIC \ + -I"$JAVA_HOME/include" \ + # -I"$JAVA_HOME/include/darwin" # for macos + -I"$JAVA_HOME/include/linux" \ + # -shared -o libinterop.dylib \ # for macos + -shared -o libinterop.so \ + rocdemo.o bridge.c + + +# then run +java javaSource.Demo diff --git a/examples/jvm-interop/impl.roc b/examples/jvm-interop/impl.roc new file mode 100644 index 0000000000..75c2ba6d94 --- /dev/null +++ b/examples/jvm-interop/impl.roc @@ -0,0 +1,26 @@ +app "rocdemo" + packages { pf: "platform.roc" } + imports [] + provides [program] to pf + +interpolateString : Str -> Str +interpolateString = \name -> + "Hello from Roc \(name)!!!🤘🤘🤘" + +# jint is i32 +mulArrByScalar : List I32, I32 -> List I32 +mulArrByScalar = \arr, scalar -> + List.map arr \x -> x * scalar + +# java doesn't have unsigned numbers so we cope with long +# factorial : I64 -> I64 +factorial = \n -> + if n < 0 then + # while we get the chance, exemplify a roc panic in an interop + crash "No negatives here!!!" + else if n == 0 then + 1 + else + n * (factorial (n - 1)) + +program = { interpolateString, factorial, mulArrByScalar } diff --git a/examples/jvm-interop/javaSource/Demo.java b/examples/jvm-interop/javaSource/Demo.java new file mode 100644 index 0000000000..2b5184e0e7 --- /dev/null +++ b/examples/jvm-interop/javaSource/Demo.java @@ -0,0 +1,39 @@ +package javaSource; + +import java.util.Arrays; + +public class Demo { + + static { + System.loadLibrary("interop"); + } + + public static native String sayHello(String num); + + public static native int[] mulArrByScalar(int[] arr, int scalar); + + public static native long factorial(long n) throws RuntimeException; + + + public static void main(String[] args) { + + // string demo + System.out.println(sayHello("Brendan") + "\n"); + + // array demo + int[] arr = {10, 20, 30, 40}; + int x = 3; + System.out.println(Arrays.toString(arr) + + " multiplied by " + x + + " results in " + Arrays.toString(mulArrByScalar(arr, x)) + + "\n"); + + // number + panic demo + // This can be implemented more peacefully but for sake of demonstration- + // this will panic from the roc side if n is negative + // and in turn will throw a JVM RuntimeException + long n = -1; + System.out.println("Factorial of " + n + " is " + factorial(n)); + + } +} diff --git a/examples/jvm-interop/platform.roc b/examples/jvm-interop/platform.roc new file mode 100644 index 0000000000..8931752ff0 --- /dev/null +++ b/examples/jvm-interop/platform.roc @@ -0,0 +1,13 @@ +platform "jvm-interop" + requires {} { program : _ } + exposes [] + packages {} + imports [] + provides [programForHost] + +programForHost : { + interpolateString : (Str -> Str) as InterpolateString, + mulArrByScalar : (List I32, I32 -> List I32) as MulArrByScalar, + factorial : (I64 -> I64) as Factorial, +} +programForHost = program diff --git a/examples/nodejs-interop/.gitignore b/examples/nodejs-interop/.gitignore new file mode 100644 index 0000000000..e3fbd98336 --- /dev/null +++ b/examples/nodejs-interop/.gitignore @@ -0,0 +1,2 @@ +build +node_modules diff --git a/examples/nodejs-interop/native-c-api/README.md b/examples/nodejs-interop/native-c-api/README.md new file mode 100644 index 0000000000..c6afd426f2 --- /dev/null +++ b/examples/nodejs-interop/native-c-api/README.md @@ -0,0 +1,64 @@ +# TypeScript Interop + +This is an example of calling Roc code from [TypeScript](https://www.typescriptlang.org/) on [Node.js](https://nodejs.org/en/). + +## Installation + +You'll need to have a C compiler installed, but most operating systems will have one already. +(e.g. macOS has `clang` installed by default, Linux usually has `gcc` by default, etc.) +All of these commands should be run from the same directory as this README file. + + +First, run this to install Node dependencies and generate the Makefile that will be +used by future commands. (You should only need to run this once.) + +``` +npm install +npx node-gyp configure +``` + +## Building the Roc library + +First, `cd` into this directory and run this in your terminal: + +``` +roc build --lib +``` + +This compiles your Roc code into a shared library in the current directory. The library's filename will be libhello plus an OS-specific extension (e.g. libhello.dylib on macOS). + +Next, run this to rebuild the C sources. + +``` +npx node-gyp build +``` + +Finally, run this to copy the generated TypeScript type definitions into the build directory: + +``` +cp addon.d.ts build/Release/ +``` + +You can verify that TypeScript sees the correct types with: + +``` +npx tsc hello.ts +``` + +### Try it out! + +Now that everything is built, you should be able to run the example with: + +``` +npx ts-node hello.ts +``` + +To rebuild after changing either the `demo.c` file or any `.roc` files, run: + +``` +roc build --lib && npx node-gyp build +``` + +## About this example + +This was created by following the [NodeJS addons](https://nodejs.org/dist/latest/docs/api/addons.html) tutorial and switching from C++ to C, then creating the `addon.d.ts` file to add types to the generated native Node module. \ No newline at end of file diff --git a/examples/nodejs-interop/native-c-api/addon.d.ts b/examples/nodejs-interop/native-c-api/addon.d.ts new file mode 100644 index 0000000000..513846bfad --- /dev/null +++ b/examples/nodejs-interop/native-c-api/addon.d.ts @@ -0,0 +1 @@ +export function hello(arg: string): string; diff --git a/examples/nodejs-interop/native-c-api/binding.gyp b/examples/nodejs-interop/native-c-api/binding.gyp new file mode 100644 index 0000000000..12882559cd --- /dev/null +++ b/examples/nodejs-interop/native-c-api/binding.gyp @@ -0,0 +1,12 @@ +{ + "targets": [ + { + "target_name": "addon", + "sources": [ "demo.c" ], + "libraries": [ + "-lhello", + "-L<(module_root_dir)" + ] + } + ] +} diff --git a/examples/nodejs-interop/native-c-api/demo.c b/examples/nodejs-interop/native-c-api/demo.c new file mode 100644 index 0000000000..72a3325df4 --- /dev/null +++ b/examples/nodejs-interop/native-c-api/demo.c @@ -0,0 +1,418 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +napi_env napi_global_env; + +void *roc_alloc(size_t size, unsigned int alignment) { return malloc(size); } + +void *roc_realloc(void *ptr, size_t new_size, size_t old_size, + unsigned int alignment) +{ + return realloc(ptr, new_size); +} + +void roc_dealloc(void *ptr, unsigned int alignment) { free(ptr); } + +void roc_panic(void *ptr, unsigned int alignment) +{ + // WARNING: If roc_panic is called before napi_global_env is set, + // the result will be undefined behavior. So never call any Roc + // functions before setting napi_global_env! + napi_throw_error(napi_global_env, NULL, (char *)ptr); +} + +void roc_dbg(char* loc, char* msg) { + fprintf(stderr, "[%s] %s\n", loc, msg); +} + +void *roc_memset(void *str, int c, size_t n) { return memset(str, c, n); } + +// Reference counting + +// If the refcount is set to this, that means the allocation is +// stored in readonly memory in the binary, and we must not +// attempt to increment or decrement it; if we do, we'll segfault! +const ssize_t REFCOUNT_READONLY = 0; +const ssize_t REFCOUNT_ONE = (ssize_t)PTRDIFF_MIN; +const size_t MASK = (size_t)PTRDIFF_MIN; + +// Increment reference count, given a pointer to the first element in a collection. +// We don't need to check for overflow because in order to overflow a usize worth of refcounts, +// you'd need to somehow have more pointers in memory than the OS's virtual address space can hold. +void incref(uint8_t* bytes, uint32_t alignment) +{ + ssize_t *refcount_ptr = ((ssize_t *)bytes) - 1; + ssize_t refcount = *refcount_ptr; + + if (refcount != REFCOUNT_READONLY) { + *refcount_ptr = refcount + 1; + } +} + +// Decrement reference count, given a pointer to the first byte of a collection's elements. +// Then call roc_dealloc if nothing is referencing this collection anymore. +void decref_heap_bytes(uint8_t* bytes, uint32_t alignment) +{ + size_t extra_bytes = (sizeof(size_t) >= (size_t)alignment) ? sizeof(size_t) : (size_t)alignment; + ssize_t *refcount_ptr = ((ssize_t *)bytes) - 1; + ssize_t refcount = *refcount_ptr; + + if (refcount != REFCOUNT_READONLY) { + *refcount_ptr = refcount - 1; + + if (refcount == REFCOUNT_ONE) { + void *original_allocation = (void *)(refcount_ptr - (extra_bytes - sizeof(size_t))); + + roc_dealloc(original_allocation, alignment); + } + } +} + +// RocBytes (List U8) + +struct RocBytes +{ + uint8_t *bytes; + size_t len; + size_t capacity; +}; + +struct RocBytes empty_rocbytes() +{ + struct RocBytes ret = { + .len = 0, + .bytes = NULL, + .capacity = 0, + }; + + return ret; +} + +struct RocBytes init_rocbytes(uint8_t *bytes, size_t len) +{ + if (len == 0) + { + return empty_rocbytes(); + } + else + { + struct RocBytes ret; + size_t refcount_size = sizeof(size_t); + uint8_t *new_refcount = (uint8_t *)roc_alloc(len + refcount_size, __alignof__(size_t)); + uint8_t *new_content = new_refcount + refcount_size; + + ((ssize_t *)new_refcount)[0] = REFCOUNT_ONE; + + memcpy(new_content, bytes, len); + + ret.bytes = new_content; + ret.len = len; + ret.capacity = len; + + return ret; + } +} + +// RocStr + +struct RocStr +{ + uint8_t *bytes; + size_t len; + size_t capacity; +}; + +struct RocStr empty_roc_str() +{ + struct RocStr ret = { + .len = 0, + .bytes = NULL, + .capacity = MASK, + }; + + return ret; +} + +// Record the small string's length in the last byte of the given stack allocation +void write_small_str_len(size_t len, struct RocStr *str) { + ((uint8_t *)str)[sizeof(struct RocStr) - 1] = (uint8_t)len | 0b10000000; +} + +struct RocStr roc_str_init_small(uint8_t *bytes, size_t len) +{ + // Start out with zeroed memory, so that + // if we end up comparing two small RocStr values + // for equality, we won't risk memory garbage resulting + // in two equal strings appearing unequal. + struct RocStr ret = empty_roc_str(); + + // Copy the bytes into the stack allocation + memcpy(&ret, bytes, len); + + write_small_str_len(len, &ret); + + return ret; +} + +struct RocStr roc_str_init_large(uint8_t *bytes, size_t len, size_t capacity) +{ + // A large RocStr is the same as a List U8 (aka RocBytes) in memory. + struct RocBytes roc_bytes = init_rocbytes(bytes, len); + + struct RocStr ret = { + .len = roc_bytes.len, + .bytes = roc_bytes.bytes, + .capacity = roc_bytes.capacity, + }; + + return ret; +} + +bool is_small_str(struct RocStr str) { return ((ssize_t)str.capacity) < 0; } + +// Determine the length of the string, taking into +// account the small string optimization +size_t roc_str_len(struct RocStr str) +{ + uint8_t *bytes = (uint8_t *)&str; + uint8_t last_byte = bytes[sizeof(str) - 1]; + uint8_t last_byte_xored = last_byte ^ 0b10000000; + size_t small_len = (size_t)(last_byte_xored); + size_t big_len = str.len & PTRDIFF_MAX; // Account for seamless slices + + // Avoid branch misprediction costs by always + // determining both small_len and big_len, + // so this compiles to a cmov instruction. + if (is_small_str(str)) + { + return small_len; + } + else + { + return big_len; + } +} + +void decref_large_str(struct RocStr str) +{ + uint8_t* bytes; + + if ((ssize_t)str.len < 0) + { + // This is a seamless slice, so the bytes are located in the capacity slot. + bytes = (uint8_t*)(str.capacity << 1); + } + else + { + bytes = str.bytes; + } + + decref_heap_bytes(bytes, __alignof__(uint8_t)); +} + + +// Turn the given Node string into a RocStr and return it +napi_status node_string_into_roc_str(napi_env env, napi_value node_string, struct RocStr *roc_str) { + size_t len; + napi_status status; + + // Passing NULL for a buffer (and size 0) will make it write the length of the string into `len`. + // https://nodejs.org/api/n-api.html#napi_get_value_string_utf8 + status = napi_get_value_string_utf8(env, node_string, NULL, 0, &len); + + if (status != napi_ok) + { + return status; + } + + // Node's "write a string into this buffer" function always writes a null terminator, + // so capacity will need to be length + 1. + // https://nodejs.org/api/n-api.html#napi_get_value_string_utf8 + size_t capacity = len + 1; + + // Create a RocStr and write it into the out param + if (capacity < sizeof(struct RocStr)) + { + // If it can fit in a small string, use the string itself as the buffer. + // First, zero out those bytes; small strings need to have zeroes for any bytes + // that are not part of the string, or else comparisons between small strings might fail. + *roc_str = empty_roc_str(); + + // This writes the actual number of bytes copied into len. Theoretically they should be the same, + // but it could be different if the buffer was somehow smaller. This way we guarantee that + // the RocStr does not present any memory garbage to the user. + status = napi_get_value_string_utf8(env, node_string, (char*)roc_str, sizeof(struct RocStr), &len); + + if (status != napi_ok) + { + return status; + } + + // We have to write the length into the buffer *after* Node copies its bytes in, + // because Node will have written a null terminator, which we may need to overwrite. + write_small_str_len(len, roc_str); + } + else + { + // capacity was too big for a small string, so make a heap allocation and write into that. + uint8_t *buf = (uint8_t*)roc_alloc(capacity, __alignof__(char)); + + // This writes the actual number of bytes copied into len. Theoretically they should be the same, + // but it could be different if the buffer was somehow smaller. This way we guarantee that + // the RocStr does not present any memory garbage to the user. + status = napi_get_value_string_utf8(env, node_string, (char*)buf, capacity, &len); + + if (status != napi_ok) + { + // Something went wrong, so free the bytes we just allocated before returning. + roc_dealloc((void *)&buf, __alignof__(char *)); + + return status; + } + + *roc_str = roc_str_init_large(buf, len, capacity); + } + + return status; +} + +// Consume the given RocStr (decrement its refcount) after creating a Node string from it. +napi_value roc_str_into_node_string(napi_env env, struct RocStr roc_str) { + bool is_small = is_small_str(roc_str); + char* roc_str_contents; + + if (is_small) + { + // In a small string, the string itself contains its contents. + roc_str_contents = (char*)&roc_str; + } + else + { + roc_str_contents = (char*)roc_str.bytes; + } + + napi_status status; + napi_value answer; + + status = napi_create_string_utf8(env, roc_str_contents, roc_str_len(roc_str), &answer); + + if (status != napi_ok) + { + answer = NULL; + } + + // Decrement the RocStr because we consumed it. + if (!is_small) + { + decref_large_str(roc_str); + } + + return answer; +} + +// Create a Node string from the given RocStr. +// Don't decrement the RocStr's refcount. (To decrement it, use roc_str_into_node_string instead.) +napi_value roc_str_as_node_string(napi_env env, struct RocStr roc_str) { + bool is_small = is_small_str(roc_str); + char* roc_str_contents; + + if (is_small) + { + // In a small string, the string itself contains its contents. + roc_str_contents = (char*)&roc_str; + } + else + { + roc_str_contents = (char*)roc_str.bytes; + } + + napi_status status; + napi_value answer; + + status = napi_create_string_utf8(env, roc_str_contents, roc_str_len(roc_str), &answer); + + if (status != napi_ok) + { + return NULL; + } + + // Do not decrement the RocStr's refcount because we did not consume it. + + return answer; +} + +extern void roc__mainForHost_1_exposed_generic(struct RocStr *ret, struct RocStr *arg); + +// Receive a string value from Node and pass it to Roc as a RocStr, then get a RocStr +// back from Roc and convert it into a Node string. +napi_value call_roc(napi_env env, napi_callback_info info) { + napi_status status; + + // roc_panic needs a napi_env in order to throw a Node exception, so we provide this + // one globally in case roc_panic gets called during the execution of our Roc function. + // + // According do the docs - https://nodejs.org/api/n-api.html#napi_env - + // it's very important that the napi_env that was passed into "the initial + // native function" is the one that's "passed to any subsequent nested Node-API calls," + // so we must override this every time we call this function (as opposed to, say, + // setting it once during init). + napi_global_env = env; + + // Get the argument passed to the Node function + size_t argc = 1; + napi_value argv[1]; + + status = napi_get_cb_info(env, info, &argc, argv, NULL, NULL); + + if (status != napi_ok) + { + return NULL; + } + + napi_value node_arg = argv[0]; + + struct RocStr roc_arg; + + status = node_string_into_roc_str(env, node_arg, &roc_arg); + + if (status != napi_ok) + { + return NULL; + } + + struct RocStr roc_ret; + // Call the Roc function to populate `roc_ret`'s bytes. + roc__mainForHost_1_exposed_generic(&roc_ret, &roc_arg); + + // Consume the RocStr to create the Node string. + return roc_str_into_node_string(env, roc_ret); +} + +napi_value init(napi_env env, napi_value exports) { + napi_status status; + napi_value fn; + + status = napi_create_function(env, NULL, 0, call_roc, NULL, &fn); + + if (status != napi_ok) + { + return NULL; + } + + status = napi_set_named_property(env, exports, "hello", fn); + + if (status != napi_ok) + { + return NULL; + } + + return exports; +} + +NAPI_MODULE(NODE_GYP_MODULE_NAME, init) diff --git a/examples/nodejs-interop/native-c-api/hello.ts b/examples/nodejs-interop/native-c-api/hello.ts new file mode 100644 index 0000000000..5156435002 --- /dev/null +++ b/examples/nodejs-interop/native-c-api/hello.ts @@ -0,0 +1,3 @@ +import { hello } from './build/Release/addon' + +console.log("Roc says the following:", hello("Hello from TypeScript")); diff --git a/examples/nodejs-interop/native-c-api/main.roc b/examples/nodejs-interop/native-c-api/main.roc new file mode 100644 index 0000000000..158be20c3c --- /dev/null +++ b/examples/nodejs-interop/native-c-api/main.roc @@ -0,0 +1,8 @@ +app "libhello" + packages { pf: "platform/main.roc" } + imports [] + provides [main] to pf + +main : Str -> Str +main = \message -> + "TypeScript said to Roc: \(message)! 🎉" diff --git a/examples/nodejs-interop/native-c-api/package-lock.json b/examples/nodejs-interop/native-c-api/package-lock.json new file mode 100644 index 0000000000..055bd71ea9 --- /dev/null +++ b/examples/nodejs-interop/native-c-api/package-lock.json @@ -0,0 +1,1246 @@ +{ + "name": "node", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "devDependencies": { + "@types/node": "^18.15.3", + "node-gyp": "^9.3.1", + "ts-node": "^10.9.1", + "typescript": "^4.9.5" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@gar/promisify": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", + "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", + "dev": true + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", + "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.14", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@npmcli/fs": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-2.1.2.tgz", + "integrity": "sha512-yOJKRvohFOaLqipNtwYB9WugyZKhC/DZC4VYPmpaCzDBrA8YpK3qHZ8/HGscMnE4GqbkLNuVcCnxkeQEdGt6LQ==", + "dev": true, + "dependencies": { + "@gar/promisify": "^1.1.3", + "semver": "^7.3.5" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/@npmcli/move-file": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-2.0.1.tgz", + "integrity": "sha512-mJd2Z5TjYWq/ttPLLGqArdtnC74J6bOzg4rMDnN+p1xTacZ2yPRCk2y0oSWQtygLR9YVQXgOcONrwtnk3JupxQ==", + "deprecated": "This functionality has been moved to @npmcli/fs", + "dev": true, + "dependencies": { + "mkdirp": "^1.0.4", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "dev": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", + "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==", + "dev": true + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.3.tgz", + "integrity": "sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==", + "dev": true + }, + "node_modules/@types/node": { + "version": "18.15.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.3.tgz", + "integrity": "sha512-p6ua9zBxz5otCmbpb5D3U4B5Nanw6Pk3PPyX05xnxbB/fRv71N7CPmORg7uAD5P70T0xmx1pzAx/FUfa5X+3cw==", + "dev": true + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true + }, + "node_modules/acorn": { + "version": "8.8.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", + "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", + "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/agentkeepalive": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.3.0.tgz", + "integrity": "sha512-7Epl1Blf4Sy37j4v9f9FjICCh4+KAQOyXgHEwlyBiAQLbhKdq/i2QQU3amQalS/wPhdPzDXPL5DMR5bkn+YeWg==", + "dev": true, + "dependencies": { + "debug": "^4.1.0", + "depd": "^2.0.0", + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dev": true, + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/aproba": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", + "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==", + "dev": true + }, + "node_modules/are-we-there-yet": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz", + "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==", + "dev": true, + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/cacache": { + "version": "16.1.3", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-16.1.3.tgz", + "integrity": "sha512-/+Emcj9DAXxX4cwlLmRI9c166RuL3w30zp4R7Joiv2cQTtTtA+jeuCAjH3ZlGnYS3tKENSrKhAzVVP9GVyzeYQ==", + "dev": true, + "dependencies": { + "@npmcli/fs": "^2.1.0", + "@npmcli/move-file": "^2.0.0", + "chownr": "^2.0.0", + "fs-minipass": "^2.1.0", + "glob": "^8.0.1", + "infer-owner": "^1.0.4", + "lru-cache": "^7.7.1", + "minipass": "^3.1.6", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "mkdirp": "^1.0.4", + "p-map": "^4.0.0", + "promise-inflight": "^1.0.1", + "rimraf": "^3.0.2", + "ssri": "^9.0.0", + "tar": "^6.1.11", + "unique-filename": "^2.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/cacache/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/cacache/node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/cacache/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "dev": true, + "bin": { + "color-support": "bin.js" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "dev": true + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "dev": true + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "dev": true, + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "dev": true + }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dev": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/gauge": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", + "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==", + "dev": true, + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.3", + "console-control-strings": "^1.1.0", + "has-unicode": "^2.0.1", + "signal-exit": "^3.0.7", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.5" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", + "dev": true + }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "dev": true + }, + "node_modules/http-cache-semantics": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", + "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", + "dev": true + }, + "node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "dev": true, + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "dev": true, + "dependencies": { + "ms": "^2.0.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/infer-owner": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", + "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", + "dev": true + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/ip": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.0.tgz", + "integrity": "sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==", + "dev": true + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-lambda": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", + "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", + "dev": true + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, + "node_modules/make-fetch-happen": { + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-10.2.1.tgz", + "integrity": "sha512-NgOPbRiaQM10DYXvN3/hhGVI2M5MtITFryzBGxHM5p4wnFxsVCbxkrBrDsk+EZ5OB4jEOT7AjDxtdF+KVEFT7w==", + "dev": true, + "dependencies": { + "agentkeepalive": "^4.2.1", + "cacache": "^16.1.0", + "http-cache-semantics": "^4.1.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^7.7.1", + "minipass": "^3.1.6", + "minipass-collect": "^1.0.2", + "minipass-fetch": "^2.0.3", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.3", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^7.0.0", + "ssri": "^9.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-collect": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", + "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", + "dev": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-fetch": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-2.1.2.tgz", + "integrity": "sha512-LT49Zi2/WMROHYoqGgdlQIZh8mLPZmOrN2NdJjMXxYe4nkN6FUyuPuOAOedNJDrx0IRGg9+4guZewtp8hE6TxA==", + "dev": true, + "dependencies": { + "minipass": "^3.1.6", + "minipass-sized": "^1.0.3", + "minizlib": "^2.1.2" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + }, + "optionalDependencies": { + "encoding": "^0.1.13" + } + }, + "node_modules/minipass-flush": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "dev": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "dev": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "dev": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dev": true, + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-gyp": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-9.3.1.tgz", + "integrity": "sha512-4Q16ZCqq3g8awk6UplT7AuxQ35XN4R/yf/+wSAwcBUAjg7l58RTactWaP8fIDTi0FzI7YcVLujwExakZlfWkXg==", + "dev": true, + "dependencies": { + "env-paths": "^2.2.0", + "glob": "^7.1.4", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^10.0.3", + "nopt": "^6.0.0", + "npmlog": "^6.0.0", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.2", + "which": "^2.0.2" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": "^12.13 || ^14.13 || >=16" + } + }, + "node_modules/nopt": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-6.0.0.tgz", + "integrity": "sha512-ZwLpbTgdhuZUnZzjd7nb1ZV+4DoiC6/sfiVKok72ym/4Tlf+DFdlHYmT2JPmcNNWV6Pi3SDf1kT+A4r9RTuT9g==", + "dev": true, + "dependencies": { + "abbrev": "^1.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/npmlog": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", + "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==", + "dev": true, + "dependencies": { + "are-we-there-yet": "^3.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^4.0.3", + "set-blocking": "^2.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "dev": true, + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", + "dev": true + }, + "node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "dev": true, + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "optional": true + }, + "node_modules/semver": { + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", + "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "dev": true + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true, + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.7.1.tgz", + "integrity": "sha512-7maUZy1N7uo6+WVEX6psASxtNlKaNVMlGQKkG/63nEDdLOWNbiUMoLK7X4uYoLhQstau72mLgfEWcXcwsaHbYQ==", + "dev": true, + "dependencies": { + "ip": "^2.0.0", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.13.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-7.0.0.tgz", + "integrity": "sha512-Fgl0YPZ902wEsAyiQ+idGd1A7rSFx/ayC1CQVMw5P+EQx2V0SgpGtf6OKFhVjPflPUl9YMmEOnmfjCdMUsygww==", + "dev": true, + "dependencies": { + "agent-base": "^6.0.2", + "debug": "^4.3.3", + "socks": "^2.6.2" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/ssri": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-9.0.1.tgz", + "integrity": "sha512-o57Wcn66jMQvfHG1FlYbWeZWW/dHZhJXjpIcTfXldXEk5nz5lStPo3mK0OJQfGR3RbZUlbISexbljkJzuEj/8Q==", + "dev": true, + "dependencies": { + "minipass": "^3.1.1" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar": { + "version": "6.1.13", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.13.tgz", + "integrity": "sha512-jdIBIN6LTIe2jqzay/2vtYLlBHa3JF42ot3h1dW8Q0PaAG4v8rm0cvpVePtau5C6OKXGGcgO9q2AMNSWxiLqKw==", + "dev": true, + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^4.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar/node_modules/minipass": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-4.2.5.tgz", + "integrity": "sha512-+yQl7SX3bIT83Lhb4BVorMAHVuqsskxRdlmO9kTpyukp8vsm2Sn/fUOV9xlnG8/a5JsypJzap21lz/y3FBMJ8Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ts-node": { + "version": "10.9.1", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", + "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==", + "dev": true, + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/typescript": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/unique-filename": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-2.0.1.tgz", + "integrity": "sha512-ODWHtkkdx3IAR+veKxFV+VBkUMcN+FaqzUUd7IZzt+0zhDZFPFxhlqwPF3YQvMHx1TD0tdgYl+kuPnJ8E6ql7A==", + "dev": true, + "dependencies": { + "unique-slug": "^3.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/unique-slug": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-3.0.0.tgz", + "integrity": "sha512-8EyMynh679x/0gqE9fT9oilG+qEt+ibFyqjuVTsZn1+CMxH+XLlpvr2UZx4nVcCwTpx81nICr2JQFkM+HPLq4w==", + "dev": true, + "dependencies": { + "imurmurhash": "^0.1.4" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "dev": true, + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "engines": { + "node": ">=6" + } + } + } +} diff --git a/examples/nodejs-interop/native-c-api/package.json b/examples/nodejs-interop/native-c-api/package.json new file mode 100644 index 0000000000..1ef892e6b8 --- /dev/null +++ b/examples/nodejs-interop/native-c-api/package.json @@ -0,0 +1,8 @@ +{ + "devDependencies": { + "@types/node": "^18.15.3", + "node-gyp": "^9.3.1", + "ts-node": "^10.9.1", + "typescript": "^4.9.5" + } +} diff --git a/examples/nodejs-interop/native-c-api/platform/main.roc b/examples/nodejs-interop/native-c-api/platform/main.roc new file mode 100644 index 0000000000..43294c9d6d --- /dev/null +++ b/examples/nodejs-interop/native-c-api/platform/main.roc @@ -0,0 +1,10 @@ +platform "nodejs-interop" + requires {} { main : Str -> Str } + exposes [] + packages {} + imports [] + provides [mainForHost] + +mainForHost : Str -> Str +mainForHost = \message -> + main message diff --git a/examples/nodejs-interop/wasm/.gitignore b/examples/nodejs-interop/wasm/.gitignore new file mode 100644 index 0000000000..f5c8d7d53c --- /dev/null +++ b/examples/nodejs-interop/wasm/.gitignore @@ -0,0 +1 @@ +roc-app.wasm diff --git a/examples/nodejs-interop/wasm/README.md b/examples/nodejs-interop/wasm/README.md new file mode 100644 index 0000000000..26bdf6f37e --- /dev/null +++ b/examples/nodejs-interop/wasm/README.md @@ -0,0 +1,12 @@ +# NodeJS Interop + +This is an example of calling Roc code from [Node.js](https://nodejs.org/en/). + +You'll need to have [Zig](https://zig-lang.org) installed. Run this from the current directory: + +``` +roc build --target=wasm32 +node hello.js +``` + +That's it! diff --git a/examples/nodejs-interop/wasm/hello.js b/examples/nodejs-interop/wasm/hello.js new file mode 100644 index 0000000000..41bf9272c1 --- /dev/null +++ b/examples/nodejs-interop/wasm/hello.js @@ -0,0 +1,66 @@ +const fs = require('fs'); +const { TextDecoder } = require('util'); + +const wasmFilename = './roc-app.wasm'; +const moduleBytes = fs.readFileSync(wasmFilename); +const wasmModule = new WebAssembly.Module(moduleBytes); + +function hello() { + const decoder = new TextDecoder(); + let wasmMemoryBuffer; + let exitCode; + let returnVal; + + function js_display_roc_string(strBytes, strLen) { + const utf8Bytes = wasmMemoryBuffer.subarray(strBytes, strBytes + strLen); + + returnVal = decoder.decode(utf8Bytes); + } + + const importObj = { + wasi_snapshot_preview1: { + proc_exit: (code) => { + if (code !== 0) { + console.error(`Exited with code ${code}`); + } + exitCode = code; + }, + fd_write: (x) => { + console.error(`fd_write not supported: ${x}`); + }, + }, + env: { + js_display_roc_string, + roc_panic: (_pointer, _tag_id) => { + throw "Roc panicked!"; + }, + roc_dbg: (_loc, _msg) => { + // TODO write a proper impl. + throw "Roc dbg not supported!"; + }, + }, + }; + + const instance = new WebAssembly.Instance(wasmModule, importObj); + + wasmMemoryBuffer = new Uint8Array(instance.exports.memory.buffer); + + try { + instance.exports._start(); + } catch (e) { + const isOk = e.message === "unreachable" && exitCode === 0; + + if (!isOk) { + throw e; + } + } + + return returnVal; +} + +if (typeof module === "object") { + module.exports = { hello }; +} + +// As an example, run hello() from Roc and print the string it returns +console.log(hello()); diff --git a/examples/nodejs-interop/wasm/main.roc b/examples/nodejs-interop/wasm/main.roc new file mode 100644 index 0000000000..fc6309f10d --- /dev/null +++ b/examples/nodejs-interop/wasm/main.roc @@ -0,0 +1,7 @@ +app "roc-app" + packages { pf: "platform/main.roc" } + imports [] + provides [main] to pf + +main : Str +main = "Hello from Roc!" diff --git a/examples/nodejs-interop/wasm/platform/host.js b/examples/nodejs-interop/wasm/platform/host.js new file mode 100644 index 0000000000..486776fc66 --- /dev/null +++ b/examples/nodejs-interop/wasm/platform/host.js @@ -0,0 +1,65 @@ +async function roc_web_platform_run(wasm_filename, callback) { + const decoder = new TextDecoder(); + let memory_bytes; + let exit_code; + + function js_display_roc_string(str_bytes, str_len) { + const utf8_bytes = memory_bytes.subarray(str_bytes, str_bytes + str_len); + const js_string = decoder.decode(utf8_bytes); + callback(js_string); + } + + const importObj = { + wasi_snapshot_preview1: { + proc_exit: (code) => { + if (code !== 0) { + console.error(`Exited with code ${code}`); + } + exit_code = code; + }, + fd_write: (x) => { + console.error(`fd_write not supported: ${x}`); + }, + }, + env: { + js_display_roc_string, + roc_panic: (_pointer, _tag_id) => { + throw "Roc panicked!"; + }, + roc_dbg: (_loc, _msg) => { + // TODO write a proper impl. + throw "Roc dbg not supported!"; + }, + }, + }; + + const fetchPromise = fetch(wasm_filename); + + let wasm; + if (WebAssembly.instantiateStreaming) { + // streaming API has better performance if available + // It can start compiling Wasm before it has fetched all of the bytes, so we don't `await` the request! + wasm = await WebAssembly.instantiateStreaming(fetchPromise, importObj); + } else { + const response = await fetchPromise; + const module_bytes = await response.arrayBuffer(); + wasm = await WebAssembly.instantiate(module_bytes, importObj); + } + + memory_bytes = new Uint8Array(wasm.instance.exports.memory.buffer); + + try { + wasm.instance.exports._start(); + } catch (e) { + const is_ok = e.message === "unreachable" && exit_code === 0; + if (!is_ok) { + console.error(e); + } + } +} + +if (typeof module !== "undefined") { + module.exports = { + roc_web_platform_run, + }; +} diff --git a/examples/nodejs-interop/wasm/platform/host.zig b/examples/nodejs-interop/wasm/platform/host.zig new file mode 100644 index 0000000000..3f1a176fa6 --- /dev/null +++ b/examples/nodejs-interop/wasm/platform/host.zig @@ -0,0 +1,53 @@ +const str = @import("glue").str; +const builtin = @import("builtin"); +const RocStr = str.RocStr; + +comptime { + if (builtin.target.cpu.arch != .wasm32) { + @compileError("This platform is for WebAssembly only. You need to pass `--target wasm32` to the Roc compiler."); + } +} + +const Align = extern struct { a: usize, b: usize }; +extern fn malloc(size: usize) callconv(.C) ?*align(@alignOf(Align)) anyopaque; +extern fn realloc(c_ptr: [*]align(@alignOf(Align)) u8, size: usize) callconv(.C) ?*anyopaque; +extern fn free(c_ptr: [*]align(@alignOf(Align)) u8) callconv(.C) void; +extern fn memcpy(dest: *anyopaque, src: *anyopaque, count: usize) *anyopaque; + +export fn roc_alloc(size: usize, alignment: u32) callconv(.C) ?*anyopaque { + _ = alignment; + + return malloc(size); +} + +export fn roc_realloc(c_ptr: *anyopaque, new_size: usize, old_size: usize, alignment: u32) callconv(.C) ?*anyopaque { + _ = old_size; + _ = alignment; + + return realloc(@as([*]align(Align) u8, @alignCast(@ptrCast(c_ptr))), new_size); +} + +export fn roc_dealloc(c_ptr: *anyopaque, alignment: u32) callconv(.C) void { + _ = alignment; + + free(@as([*]align(Align) u8, @alignCast(@ptrCast(c_ptr)))); +} + +// NOTE roc_panic and roc_dbg is provided in the JS file, so it can throw an exception + +extern fn roc__mainForHost_1_exposed(*RocStr) void; + +extern fn js_display_roc_string(str_bytes: ?[*]u8, str_len: usize) void; + +pub fn main() u8 { + // actually call roc to populate the callresult + var callresult = RocStr.empty(); + roc__mainForHost_1_exposed(&callresult); + + // display the result using JavaScript + js_display_roc_string(callresult.asU8ptrMut(), callresult.len()); + + callresult.decref(); + + return 0; +} diff --git a/examples/nodejs-interop/wasm/platform/main.roc b/examples/nodejs-interop/wasm/platform/main.roc new file mode 100644 index 0000000000..231cfb4fec --- /dev/null +++ b/examples/nodejs-interop/wasm/platform/main.roc @@ -0,0 +1,9 @@ +platform "wasm-nodejs-example-platform" + requires {} { main : Str } + exposes [] + packages {} + imports [] + provides [mainForHost] + +mainForHost : Str +mainForHost = main diff --git a/examples/parser/.gitignore b/examples/parser/.gitignore new file mode 100644 index 0000000000..14dd358511 --- /dev/null +++ b/examples/parser/.gitignore @@ -0,0 +1 @@ +parse-movies-csv diff --git a/examples/parser/Parser/CSV.roc b/examples/parser/Parser/CSV.roc new file mode 100644 index 0000000000..734e04eb4c --- /dev/null +++ b/examples/parser/Parser/CSV.roc @@ -0,0 +1,196 @@ +interface Parser.CSV + exposes [ + CSV, + CSVRecord, + file, + record, + parseStr, + parseCSV, + parseStrToCSVRecord, + field, + string, + nat, + f64, + ] + imports [ + Parser.Core.{ Parser, parse, buildPrimitiveParser, alt, map, many, sepBy1, between, ignore, flatten, sepBy }, + Parser.Str.{ RawStr, oneOf, codeunit, codeunitSatisfies, strFromRaw }, + ] + +## This is a CSV parser which follows RFC4180 +## +## For simplicity's sake, the following things are not yet supported: +## - CSV files with headings +## +## The following however *is* supported +## - A simple LF ("\n") instead of CRLF ("\r\n") to separate records. +CSV : List CSVRecord +CSVRecord : List CSVField +CSVField : RawStr + +## Attempts to parse an `a` from a `Str` that is encoded in CSV format. +parseStr : Parser CSVRecord a, Str -> Result (List a) [ParsingFailure Str, SyntaxError Str, ParsingIncomplete CSVRecord] +parseStr = \csvParser, input -> + when parseStrToCSV input is + Err (ParsingIncomplete rest) -> + restStr = Parser.Str.strFromRaw rest + + Err (SyntaxError restStr) + + Err (ParsingFailure str) -> + Err (ParsingFailure str) + + Ok csvData -> + when parseCSV csvParser csvData is + Err (ParsingFailure str) -> + Err (ParsingFailure str) + + Err (ParsingIncomplete problem) -> + Err (ParsingIncomplete problem) + + Ok vals -> + Ok vals + +## Attempts to parse an `a` from a `CSV` datastructure (a list of lists of bytestring-fields). +parseCSV : Parser CSVRecord a, CSV -> Result (List a) [ParsingFailure Str, ParsingIncomplete CSVRecord] +parseCSV = \csvParser, csvData -> + csvData + |> List.mapWithIndex (\recordFieldsList, index -> { record: recordFieldsList, index: index }) + |> List.walkUntil (Ok []) \state, { record: recordFieldsList, index: index } -> + when parseCSVRecord csvParser recordFieldsList is + Err (ParsingFailure problem) -> + indexStr = Num.toStr (index + 1) + recordStr = recordFieldsList |> List.map strFromRaw |> List.map (\val -> "\"\(val)\"") |> Str.joinWith ", " + problemStr = "\(problem)\nWhile parsing record no. \(indexStr): `\(recordStr)`" + + Break (Err (ParsingFailure problemStr)) + + Err (ParsingIncomplete problem) -> + Break (Err (ParsingIncomplete problem)) + + Ok val -> + state + |> Result.map (\vals -> List.append vals val) + |> Continue + +## Attempts to parse an `a` from a `CSVRecord` datastructure (a list of bytestring-fields) +## +## This parser succeeds when all fields of the CSVRecord are consumed by the parser. +parseCSVRecord : Parser CSVRecord a, CSVRecord -> Result a [ParsingFailure Str, ParsingIncomplete CSVRecord] +parseCSVRecord = \csvParser, recordFieldsList -> + parse csvParser recordFieldsList (\leftover -> leftover == []) + +## Wrapper function to combine a set of fields into your desired `a` +## +## ## Usage example +## +## >>> record (\firstName -> \lastName -> \age -> User {firstName, lastName, age}) +## >>> |> field string +## >>> |> field string +## >>> |> field nat +## +record : a -> Parser CSVRecord a +record = Parser.Core.const + +## Turns a parser for a `List U8` into a parser that parses part of a `CSVRecord`. +field : Parser RawStr a -> Parser CSVRecord a +field = \fieldParser -> + buildPrimitiveParser \fieldsList -> + when List.get fieldsList 0 is + Err OutOfBounds -> + Err (ParsingFailure "expected another CSV field but there are no more fields in this record") + + Ok rawStr -> + when Parser.Str.parseRawStr fieldParser rawStr is + Ok val -> + Ok { val: val, input: List.dropFirst fieldsList 1 } + + Err (ParsingFailure reason) -> + fieldStr = rawStr |> strFromRaw + + Err (ParsingFailure "Field `\(fieldStr)` could not be parsed. \(reason)") + + Err (ParsingIncomplete reason) -> + reasonStr = strFromRaw reason + fieldsStr = fieldsList |> List.map strFromRaw |> Str.joinWith ", " + + Err (ParsingFailure "The field parser was unable to read the whole field: `\(reasonStr)` while parsing the first field of leftover \(fieldsStr))") + +## Parser for a field containing a UTF8-encoded string +string : Parser CSVField Str +string = Parser.Str.anyString + +## Parse a natural number from a CSV field +nat : Parser CSVField Nat +nat = + string + |> map + (\val -> + when Str.toNat val is + Ok num -> + Ok num + + Err _ -> + Err "\(val) is not a Nat." + ) + |> flatten + +## Parse a 64-bit float from a CSV field +f64 : Parser CSVField F64 +f64 = + string + |> map + (\val -> + when Str.toF64 val is + Ok num -> + Ok num + + Err _ -> + Err "\(val) is not a F64." + ) + |> flatten + +## Attempts to parse a Str into the internal `CSV` datastructure (A list of lists of bytestring-fields). +parseStrToCSV : Str -> Result CSV [ParsingFailure Str, ParsingIncomplete RawStr] +parseStrToCSV = \input -> + parse file (Str.toUtf8 input) (\leftover -> leftover == []) + +## Attempts to parse a Str into the internal `CSVRecord` datastructure (A list of bytestring-fields). +parseStrToCSVRecord : Str -> Result CSVRecord [ParsingFailure Str, ParsingIncomplete RawStr] +parseStrToCSVRecord = \input -> + parse csvRecord (Str.toUtf8 input) (\leftover -> leftover == []) + +# The following are parsers to turn strings into CSV structures +file : Parser RawStr CSV +file = sepBy csvRecord endOfLine + +csvRecord : Parser RawStr CSVRecord +csvRecord = sepBy1 csvField comma + +csvField : Parser RawStr CSVField +csvField = alt escapedCsvField nonescapedCsvField + +escapedCsvField : Parser RawStr CSVField +escapedCsvField = between escapedContents dquote dquote +escapedContents = many + ( + oneOf [ + twodquotes |> map (\_ -> '"'), + comma, + cr, + lf, + textdata, + ] + ) + +twodquotes = Parser.Str.string "\"\"" + +nonescapedCsvField : Parser RawStr CSVField +nonescapedCsvField = many textdata +comma = codeunit ',' +dquote = codeunit '"' +endOfLine = alt (ignore crlf) (ignore lf) +cr = codeunit '\r' +lf = codeunit '\n' +crlf = Parser.Str.string "\r\n" +textdata = codeunitSatisfies (\x -> (x >= 32 && x <= 33) || (x >= 35 && x <= 43) || (x >= 45 && x <= 126)) # Any printable char except " (34) and , (44) diff --git a/examples/parser/Parser/Core.roc b/examples/parser/Parser/Core.roc new file mode 100644 index 0000000000..9001a113cb --- /dev/null +++ b/examples/parser/Parser/Core.roc @@ -0,0 +1,354 @@ +interface Parser.Core + exposes [ + Parser, + ParseResult, + parse, + parsePartial, + fail, + const, + alt, + keep, + skip, + oneOf, + map, + map2, + map3, + lazy, + maybe, + oneOrMore, + many, + between, + sepBy, + sepBy1, + ignore, + buildPrimitiveParser, + flatten, + ] + imports [] + +## Opaque type for a parser that will try to parse an `a` from an `input`. +## +## As a simple example, you might consider a parser that tries to parse a `U32` from a `Str`. +## Such a process might succeed or fail, depending on the current value of `input`. +## +## As such, a parser can be considered a recipe +## for a function of the type `input -> Result {val: a, input: input} [ParsingFailure Str]`. +## +## How a parser is _actually_ implemented internally is not important +## and this might change between versions; +## for instance to improve efficiency or error messages on parsing failures. +Parser input a := input -> ParseResult input a + +ParseResult input a : Result { val : a, input : input } [ParsingFailure Str] + +buildPrimitiveParser : (input -> ParseResult input a) -> Parser input a +buildPrimitiveParser = \fun -> + @Parser fun + +# -- Generic parsers: +## Most general way of running a parser. +## +## Can be tought of turning the recipe of a parser into its actual parsing function +## and running this function on the given input. +## +## Many (but not all!) parsers consume part of `input` when they succeed. +## This allows you to string parsers together that run one after the other: +## The part of the input that the first parser did not consume, is used by the next parser. +## This is why a parser returns on success both the resulting value and the leftover part of the input. +## +## Of course, this is mostly useful when creating your own internal parsing building blocks. +## `run` or `Parser.Str.runStr` etc. are more useful in daily usage. +parsePartial : Parser input a, input -> ParseResult input a +parsePartial = \@Parser parser, input -> + parser input + +## Runs a parser on the given input, expecting it to fully consume the input +## +## The `input -> Bool` parameter is used to check whether parsing has 'completed', +## (in other words: Whether all of the input has been consumed.) +## +## For most (but not all!) input types, a parsing run that leaves some unparsed input behind +## should be considered an error. +parse : Parser input a, input, (input -> Bool) -> Result a [ParsingFailure Str, ParsingIncomplete input] +parse = \parser, input, isParsingCompleted -> + when parsePartial parser input is + Ok { val: val, input: leftover } -> + if isParsingCompleted leftover then + Ok val + else + Err (ParsingIncomplete leftover) + + Err (ParsingFailure msg) -> + Err (ParsingFailure msg) + +## Parser that can never succeed, regardless of the given input. +## It will always fail with the given error message. +## +## This is mostly useful as 'base case' if all other parsers +## in a `oneOf` or `alt` have failed, to provide some more descriptive error message. +fail : Str -> Parser * * +fail = \msg -> + @Parser \_input -> Err (ParsingFailure msg) + +## Parser that will always produce the given `val`, without looking at the actual input. +## This is useful as basic building block, especially in combination with +## `map` and `keep`. +const : a -> Parser * a +const = \val -> + @Parser \input -> + Ok { val: val, input: input } + +## Try the `first` parser and (only) if it fails, try the `second` parser as fallback. +alt : Parser input a, Parser input a -> Parser input a +alt = \first, second -> + buildPrimitiveParser \input -> + when parsePartial first input is + Ok { val: val, input: rest } -> Ok { val: val, input: rest } + Err (ParsingFailure firstErr) -> + when parsePartial second input is + Ok { val: val, input: rest } -> Ok { val: val, input: rest } + Err (ParsingFailure secondErr) -> + Err (ParsingFailure ("\(firstErr) or \(secondErr)")) + +## Runs a parser building a function, then a parser building a value, +## and finally returns the result of calling the function with the value. +## +## This is useful if you are building up a structure that requires more parameters +## than there are variants of `map`, `map2`, `map3` etc. for. +## +## For instance, the following two are the same: +## +## >>> const (\x, y, z -> Triple x y z) +## >>> |> map3 Parser.Str.nat Parser.Str.nat Parser.Str.nat +## +## >>> const (\x -> \y -> \z -> Triple x y z) +## >>> |> keep Parser.Str.nat +## >>> |> keep Parser.Str.nat +## >>> |> keep Parser.Str.nat +## +## (And indeed, this is how `map`, `map2`, `map3` etc. are implemented under the hood.) +## +## # Currying +## Be aware that when using `keep`, you need to explicitly 'curry' the parameters to the construction function. +## This means that instead of writing `\x, y, z -> ...` +## you'll need to write `\x -> \y -> \z -> ...`. +## This is because the parameters to the function will be applied one-by-one as parsing continues. +keep : Parser input (a -> b), Parser input a -> Parser input b +keep = \funParser, valParser -> + @Parser \input -> + when parsePartial funParser input is + Ok { val: funVal, input: rest } -> + when parsePartial valParser rest is + Ok { val: val, input: rest2 } -> + Ok { val: funVal val, input: rest2 } + + Err e -> + Err e + + Err e -> + Err e + +## Skip over a parsed item as part of a pipeline +## +## This is useful if you are using a pipeline of parsers with `keep` but +## some parsed items are not part of the final result +## +## >>> const (\x -> \y -> \z -> Triple x y z) +## >>> |> keep Parser.Str.nat +## >>> |> skip (codeunit ',') +## >>> |> keep Parser.Str.nat +## >>> |> skip (codeunit ',') +## >>> |> keep Parser.Str.nat +## +skip : Parser input kept, Parser input skipped -> Parser input kept +skip = \kept, skipped -> + @Parser \input -> + when parsePartial kept input is + Ok step1 -> + when parsePartial skipped step1.input is + Ok step2 -> + Ok { val: step1.val, input: step2.input } + + Err e -> + Err e + + Err e -> + Err e + +# Internal utility function. Not exposed to users, since usage is discouraged! +# +# Runs `firstParser` and (only) if it succeeds, +# runs the function `buildNextParser` on its result value. +# This function returns a new parser, which is finally run. +# +# `andThen` is usually more flexible than necessary, and less efficient +# than using `const` with `map` and/or `keep`. +# Consider using those functions first. +andThen : Parser input a, (a -> Parser input b) -> Parser input b +andThen = \@Parser firstParser, buildNextParser -> + @Parser \input -> + when firstParser input is + Ok step -> + (@Parser nextParser) = buildNextParser step.val + nextParser step.input + + Err e -> + Err e + +## Try a list of parsers in turn, until one of them succeeds +oneOf : List (Parser input a) -> Parser input a +oneOf = \parsers -> + List.walkBackwards parsers (fail "oneOf: The list of parsers was empty") (\laterParser, earlierParser -> alt earlierParser laterParser) + +## Transforms the result of parsing into something else, +## using the given transformation function. +map : Parser input a, (a -> b) -> Parser input b +map = \@Parser simpleParser, transform -> + @Parser \input -> + when simpleParser input is + Ok step -> + Ok { val: transform step.val, input: step.input } + + Err e -> + Err e + +## Transforms the result of parsing into something else, +## using the given two-parameter transformation function. +map2 : Parser input a, Parser input b, (a, b -> c) -> Parser input c +map2 = \@Parser parserA, @Parser parserB, transform -> + @Parser \input -> + when parserA input is + Ok step1 -> + when parserB step1.input is + Ok step2 -> + Ok { val: transform step1.val step2.val, input: step2.input } + + Err e -> + Err e + + Err e -> + Err e + +## Transforms the result of parsing into something else, +## using the given three-parameter transformation function. +## +## If you need transformations with more inputs, +## take a look at `keep`. +map3 : Parser input a, Parser input b, Parser input c, (a, b, c -> d) -> Parser input d +map3 = \@Parser parserA, @Parser parserB, @Parser parserC, transform -> + @Parser \input -> + when parserA input is + Ok step1 -> + when parserB step1.input is + Ok step2 -> + when parserC step2.input is + Ok step3 -> + Ok { val: transform step1.val step2.val step3.val, input: step3.input } + + Err e -> + Err e + + Err e -> + Err e + + Err e -> + Err e + +# ^ And this could be repeated for as high as we want, of course. +# Removes a layer of 'result' from running the parser. +# +# This allows for instance to map functions that return a result over the parser, +# where errors are turned into `ParsingFailure` s. +flatten : Parser input (Result a Str) -> Parser input a +flatten = \parser -> + buildPrimitiveParser \input -> + result = parsePartial parser input + + when result is + Err problem -> + Err problem + + Ok { val: Ok val, input: inputRest } -> + Ok { val: val, input: inputRest } + + Ok { val: Err problem, input: _inputRest } -> + Err (ParsingFailure problem) + +## Runs a parser lazily +## +## This is (only) useful when dealing with a recursive structure. +## For instance, consider a type `Comment : { message: String, responses: List Comment }`. +## Without `lazy`, you would ask the compiler to build an infinitely deep parser. +## (Resulting in a compiler error.) +## +lazy : ({} -> Parser input a) -> Parser input a +lazy = \thunk -> + const {} + |> andThen thunk + +maybe : Parser input a -> Parser input (Result a [Nothing]) +maybe = \parser -> + alt (parser |> map (\val -> Ok val)) (const (Err Nothing)) + +manyImpl : Parser input a, List a, input -> ParseResult input (List a) +manyImpl = \@Parser parser, vals, input -> + result = parser input + + when result is + Err _ -> + Ok { val: vals, input: input } + + Ok { val: val, input: inputRest } -> + manyImpl (@Parser parser) (List.append vals val) inputRest + +## A parser which runs the element parser *zero* or more times on the input, +## returning a list containing all the parsed elements. +## +## Also see `oneOrMore`. +many : Parser input a -> Parser input (List a) +many = \parser -> + @Parser \input -> + manyImpl parser [] input + +## A parser which runs the element parser *one* or more times on the input, +## returning a list containing all the parsed elements. +## +## Also see `many`. +oneOrMore : Parser input a -> Parser input (List a) +oneOrMore = \@Parser parser -> + @Parser \input -> + when parser input is + Ok step -> + manyImpl (@Parser parser) [step.val] step.input + + Err e -> + Err e + +## Runs a parser for an 'opening' delimiter, then your main parser, then the 'closing' delimiter, +## and only returns the result of your main parser. +## +## Useful to recognize structures surrounded by delimiters (like braces, parentheses, quotes, etc.) +## +## >>> betweenBraces = \parser -> parser |> between (scalar '[') (scalar ']') +between : Parser input a, Parser input open, Parser input close -> Parser input a +between = \parser, open, close -> + map3 open parser close (\_, val, _ -> val) + +sepBy1 : Parser input a, Parser input sep -> Parser input (List a) +sepBy1 = \parser, separator -> + parserFollowedBySep = + const (\_ -> \val -> val) + |> keep separator + |> keep parser + + const (\val -> \vals -> List.prepend vals val) + |> keep parser + |> keep (many parserFollowedBySep) + +sepBy : Parser input a, Parser input sep -> Parser input (List a) +sepBy = \parser, separator -> + alt (sepBy1 parser separator) (const []) + +ignore : Parser input a -> Parser input {} +ignore = \parser -> + map parser (\_ -> {}) diff --git a/examples/parser/Parser/Http.roc b/examples/parser/Parser/Http.roc new file mode 100644 index 0000000000..472c79231e --- /dev/null +++ b/examples/parser/Parser/Http.roc @@ -0,0 +1,261 @@ +interface Parser.Http + exposes [ + Request, + Response, + request, + response, + ] + imports [ + Parser.Core.{ Parser, ParseResult, map, keep, skip, const, oneOrMore, many }, + Parser.Str.{ + RawStr, + oneOf, + string, + codeunit, + parseStr, + codeunitSatisfies, + strFromRaw, + anyRawString, + digits, + }, + ] + +# https://www.ietf.org/rfc/rfc2616.txt +Method : [Options, Get, Post, Put, Delete, Head, Trace, Connect, Patch] + +HttpVersion : { major : U8, minor : U8 } + +Request : { + method : Method, + uri : Str, + httpVersion : HttpVersion, + headers : List [Header Str Str], + body : List U8, +} + +Response : { + httpVersion : HttpVersion, + statusCode : U16, + status : Str, + headers : List [Header Str Str], + body : List U8, +} + +method : Parser RawStr Method +method = + oneOf [ + string "OPTIONS" |> map \_ -> Options, + string "GET" |> map \_ -> Get, + string "POST" |> map \_ -> Post, + string "PUT" |> map \_ -> Put, + string "DELETE" |> map \_ -> Delete, + string "HEAD" |> map \_ -> Head, + string "TRACE" |> map \_ -> Trace, + string "CONNECT" |> map \_ -> Connect, + string "PATCH" |> map \_ -> Patch, + ] + +expect parseStr method "GET" == Ok Get +expect parseStr method "DELETE" == Ok Delete + +# TODO: do we want more structure in the URI, or is Str actually what programs want anyway? +# This is not a full URL! +# Request-URI = "*" | absoluteURI | abs_path | authority +RequestUri : Str + +requestUri : Parser RawStr RequestUri +requestUri = + codeunitSatisfies \c -> c != ' ' + |> oneOrMore + |> map strFromRaw + +sp = codeunit ' ' +crlf = string "\r\n" + +httpVersion : Parser RawStr HttpVersion +httpVersion = + const (\major -> \minor -> { major, minor }) + |> skip (string "HTTP/") + |> keep digits + |> skip (codeunit '.') + |> keep digits + +expect + actual = parseStr httpVersion "HTTP/1.1" + expected = Ok { major: 1, minor: 1 } + actual == expected + +Header : [Header Str Str] + +stringWithoutColon : Parser RawStr Str +stringWithoutColon = + codeunitSatisfies \c -> c != ':' + |> oneOrMore + |> map strFromRaw + +stringWithoutCr : Parser RawStr Str +stringWithoutCr = + codeunitSatisfies \c -> c != '\r' + |> oneOrMore + |> map strFromRaw + +header : Parser RawStr Header +header = + const (\k -> \v -> Header k v) + |> keep stringWithoutColon + |> skip (string ": ") + |> keep stringWithoutCr + |> skip crlf + +expect + actual = parseStr header "Accept-Encoding: gzip, deflate\r\n" + expected = Ok (Header "Accept-Encoding" "gzip, deflate") + actual == expected + +request : Parser RawStr Request +request = + const (\m -> \u -> \hv -> \hs -> \b -> { method: m, uri: u, httpVersion: hv, headers: hs, body: b }) + |> keep method + |> skip sp + |> keep requestUri + |> skip sp + |> keep httpVersion + |> skip crlf + |> keep (many header) + |> skip crlf + |> keep anyRawString + +expect + requestText = + """ + GET /things?id=1 HTTP/1.1\r + Host: bar.example\r + Accept-Encoding: gzip, deflate\r + \r + Hello, world! + """ + actual = + parseStr request requestText + + expected : Result Request [ParsingFailure Str, ParsingIncomplete Str] + expected = Ok { + method: Get, + uri: "/things?id=1", + httpVersion: { major: 1, minor: 1 }, + headers: [ + Header "Host" "bar.example", + Header "Accept-Encoding" "gzip, deflate", + ], + body: "Hello, world!" |> Str.toUtf8, + } + actual == expected + +expect + requestText = + """ + OPTIONS /resources/post-here/ HTTP/1.1\r + Host: bar.example\r + Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r + Accept-Language: en-us,en;q=0.5\r + Accept-Encoding: gzip,deflate\r + Connection: keep-alive\r + Origin: https://foo.example\r + Access-Control-Request-Method: POST\r + Access-Control-Request-Headers: X-PINGOTHER, Content-Type\r + \r\n + """ + actual = + parseStr request requestText + expected = Ok { + method: Options, + uri: "/resources/post-here/", + httpVersion: { major: 1, minor: 1 }, + headers: [ + Header "Host" "bar.example", + Header "Accept" "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", + Header "Accept-Language" "en-us,en;q=0.5", + Header "Accept-Encoding" "gzip,deflate", + Header "Connection" "keep-alive", + Header "Origin" "https://foo.example", + Header "Access-Control-Request-Method" "POST", + Header "Access-Control-Request-Headers" "X-PINGOTHER, Content-Type", + ], + body: [], + } + actual == expected + +response : Parser RawStr Response +response = + const (\hv -> \sc -> \s -> \hs -> \b -> { httpVersion: hv, statusCode: sc, status: s, headers: hs, body: b }) + |> keep httpVersion + |> skip sp + |> keep digits + |> skip sp + |> keep stringWithoutCr + |> skip crlf + |> keep (many header) + |> skip crlf + |> keep anyRawString + +expect + body = + """ + \r + \r + \r + \r + A simple webpage\r + \r + \r +

Simple HTML webpage

\r +

Hello, world!

\r + \r + \r\n + """ + responseText = + """ + HTTP/1.1 200 OK\r + Content-Type: text/html; charset=utf-8\r + Content-Length: 55743\r + Connection: keep-alive\r + Cache-Control: s-maxage=300, public, max-age=0\r + Content-Language: en-US\r + Date: Thu, 06 Dec 2018 17:37:18 GMT\r + ETag: "2e77ad1dc6ab0b53a2996dfd4653c1c3"\r + Server: meinheld/0.6.1\r + Strict-Transport-Security: max-age=63072000\r + X-Content-Type-Options: nosniff\r + X-Frame-Options: DENY\r + X-XSS-Protection: 1; mode=block\r + Vary: Accept-Encoding,Cookie\r + Age: 7\r + \r + \(body) + """ + actual = + parseStr response responseText + expected = + Ok { + httpVersion: { major: 1, minor: 1 }, + statusCode: 200, + status: "OK", + headers: [ + Header "Content-Type" "text/html; charset=utf-8", + Header "Content-Length" "55743", + Header "Connection" "keep-alive", + Header "Cache-Control" "s-maxage=300, public, max-age=0", + Header "Content-Language" "en-US", + Header "Date" "Thu, 06 Dec 2018 17:37:18 GMT", + Header "ETag" "\"2e77ad1dc6ab0b53a2996dfd4653c1c3\"", + Header "Server" "meinheld/0.6.1", + Header "Strict-Transport-Security" "max-age=63072000", + Header "X-Content-Type-Options" "nosniff", + Header "X-Frame-Options" "DENY", + Header "X-XSS-Protection" "1; mode=block", + Header "Vary" "Accept-Encoding,Cookie", + Header "Age" "7", + ], + body: Str.toUtf8 body, + } + actual == expected + diff --git a/examples/parser/Parser/Str.roc b/examples/parser/Parser/Str.roc new file mode 100644 index 0000000000..2c5bb33e20 --- /dev/null +++ b/examples/parser/Parser/Str.roc @@ -0,0 +1,215 @@ +interface Parser.Str + exposes [ + RawStr, + parseStr, + parseStrPartial, + parseRawStr, + parseRawStrPartial, + string, + stringRaw, + codeunit, + codeunitSatisfies, + anyString, + anyRawString, + anyCodeunit, + scalar, + oneOf, + digit, + positiveInt, + strFromRaw, + ] + imports [Parser.Core.{ Parser, ParseResult, map, oneOrMore, parse, parsePartial, buildPrimitiveParser }] + +# Specific string-based parsers: +RawStr : List U8 + +strFromRaw : RawStr -> Str +strFromRaw = \rawStr -> + rawStr + |> Str.fromUtf8 + |> Result.withDefault "Unexpected problem while turning a List U8 (that was originally a Str) back into a Str. This should never happen!" + +strToRaw : Str -> RawStr +strToRaw = \str -> + str |> Str.toUtf8 + +strFromScalar : U32 -> Str +strFromScalar = \scalarVal -> + Str.appendScalar "" (Num.intCast scalarVal) + |> Result.withDefault "Unexpected problem while turning a U32 (that was probably originally a scalar constant) into a Str. This should never happen!" + +strFromCodeunit : U8 -> Str +strFromCodeunit = \cu -> + strFromRaw [cu] + +## Runs a parser against the start of a list of scalars, allowing the parser to consume it only partially. +parseRawStrPartial : Parser RawStr a, RawStr -> ParseResult RawStr a +parseRawStrPartial = \parser, input -> + parsePartial parser input + +## Runs a parser against the start of a string, allowing the parser to consume it only partially. +## +## - If the parser succeeds, returns the resulting value as well as the leftover input. +## - If the parser fails, returns `Err (ParsingFailure msg)` +parseStrPartial : Parser RawStr a, Str -> ParseResult Str a +parseStrPartial = \parser, input -> + parser + |> parseRawStrPartial (strToRaw input) + |> Result.map \{ val: val, input: restRaw } -> + { val: val, input: strFromRaw restRaw } + +## Runs a parser against a string, requiring the parser to consume it fully. +## +## - If the parser succeeds, returns `Ok val` +## - If the parser fails, returns `Err (ParsingFailure msg)` +## - If the parser succeeds but does not consume the full string, returns `Err (ParsingIncomplete leftover)` +parseRawStr : Parser RawStr a, RawStr -> Result a [ParsingFailure Str, ParsingIncomplete RawStr] +parseRawStr = \parser, input -> + parse parser input (\leftover -> List.len leftover == 0) + +parseStr : Parser RawStr a, Str -> Result a [ParsingFailure Str, ParsingIncomplete Str] +parseStr = \parser, input -> + parser + |> parseRawStr (strToRaw input) + |> Result.mapErr \problem -> + when problem is + ParsingFailure msg -> + ParsingFailure msg + + ParsingIncomplete leftoverRaw -> + ParsingIncomplete (strFromRaw leftoverRaw) + +codeunitSatisfies : (U8 -> Bool) -> Parser RawStr U8 +codeunitSatisfies = \check -> + buildPrimitiveParser \input -> + { before: start, others: inputRest } = List.split input 1 + + when List.get start 0 is + Err OutOfBounds -> + Err (ParsingFailure "expected a codeunit satisfying a condition, but input was empty.") + + Ok startCodeunit -> + if check startCodeunit then + Ok { val: startCodeunit, input: inputRest } + else + otherChar = strFromCodeunit startCodeunit + inputStr = strFromRaw input + + Err (ParsingFailure "expected a codeunit satisfying a condition but found `\(otherChar)`.\n While reading: `\(inputStr)`") + +# Implemented manually instead of on top of codeunitSatisfies +# because of better error messages +codeunit : U8 -> Parser RawStr U8 +codeunit = \expectedCodeUnit -> + buildPrimitiveParser \input -> + { before: start, others: inputRest } = List.split input 1 + + when List.get start 0 is + Err OutOfBounds -> + errorChar = strFromCodeunit expectedCodeUnit + + Err (ParsingFailure "expected char `\(errorChar)` but input was empty.") + + Ok startCodeunit -> + if startCodeunit == expectedCodeUnit then + Ok { val: expectedCodeUnit, input: inputRest } + else + errorChar = strFromCodeunit expectedCodeUnit + otherChar = strFromRaw start + inputStr = strFromRaw input + + Err (ParsingFailure "expected char `\(errorChar)` but found `\(otherChar)`.\n While reading: `\(inputStr)`") + +# Implemented manually instead of a sequence of codeunits +# because of efficiency and better error messages +stringRaw : List U8 -> Parser RawStr (List U8) +stringRaw = \expectedString -> + buildPrimitiveParser \input -> + { before: start, others: inputRest } = List.split input (List.len expectedString) + + if start == expectedString then + Ok { val: expectedString, input: inputRest } + else + errorString = strFromRaw expectedString + otherString = strFromRaw start + inputString = strFromRaw input + + Err (ParsingFailure "expected string `\(errorString)` but found `\(otherString)`.\nWhile reading: \(inputString)") + +string : Str -> Parser RawStr Str +string = \expectedString -> + strToRaw expectedString + |> stringRaw + |> map \_val -> expectedString + +scalar : U32 -> Parser RawStr U32 +scalar = \expectedScalar -> + expectedScalar + |> strFromScalar + |> string + |> map \_ -> expectedScalar + +# Matches any codeunit +anyCodeunit : Parser RawStr U8 +anyCodeunit = codeunitSatisfies (\_ -> Bool.true) + +# Matches any bytestring +# and consumes all of it. +# Does not fail. +anyRawString : Parser RawStr RawStr +anyRawString = buildPrimitiveParser \rawStringValue -> + Ok { val: rawStringValue, input: [] } + +# Matches any string +# as long as it is valid UTF8. +anyString : Parser RawStr Str +anyString = buildPrimitiveParser \fieldRawString -> + when Str.fromUtf8 fieldRawString is + Ok stringVal -> + Ok { val: stringVal, input: [] } + + Err (BadUtf8 _ _) -> + Err (ParsingFailure "Expected a string field, but its contents cannot be parsed as UTF8.") + +# betweenBraces : Parser RawStr a -> Parser RawStr a +# betweenBraces = \parser -> +# between parser (scalar '[') (scalar ']') +digit : Parser RawStr U8 +digit = + digitParsers = + List.range { start: At '0', end: At '9' } + |> List.map \digitCodeUnit -> + digitCodeUnit + |> codeunit + |> map \_ -> digitCodeUnit - '0' + + oneOf digitParsers + +# NOTE: Currently happily accepts leading zeroes +positiveInt : Parser RawStr (Int *) +positiveInt = + oneOrMore digit + |> map \digitsList -> + digitsList + |> List.map \char -> Num.intCast char - '0' + |> List.walk 0 \sum, digitVal -> 10 * sum + digitVal + +## Try a bunch of different parsers. +## +## The first parser which is tried is the one at the front of the list, +## and the next one is tried until one succeeds or the end of the list was reached. +## +## >>> boolParser : Parser RawStr Bool +## >>> boolParser = oneOf [string "true", string "false"] |> map (\x -> if x == "true" then True else False) +# NOTE: This implementation works, but is limited to parsing strings. +# Blocked until issue #3444 is fixed. +oneOf : List (Parser RawStr a) -> Parser RawStr a +oneOf = \parsers -> + buildPrimitiveParser \input -> + List.walkUntil parsers (Err (ParsingFailure "(no possibilities)")) \_, parser -> + when parseRawStrPartial parser input is + Ok val -> + Break (Ok val) + + Err problem -> + Continue (Err problem) diff --git a/examples/parser/examples/.gitignore b/examples/parser/examples/.gitignore new file mode 100644 index 0000000000..2ab3c9a91f --- /dev/null +++ b/examples/parser/examples/.gitignore @@ -0,0 +1 @@ +letter-counts diff --git a/examples/parser/examples/letter-counts.roc b/examples/parser/examples/letter-counts.roc new file mode 100644 index 0000000000..22ed5b3956 --- /dev/null +++ b/examples/parser/examples/letter-counts.roc @@ -0,0 +1,55 @@ +app "example" + packages { + cli: "https://github.com/roc-lang/basic-cli/releases/download/0.7.0/bkGby8jb0tmZYsy2hg1E_B2QrCgcSTxdUlHtETwm5m4.tar.br", + parser: "../package/main.roc", + } + imports [ + cli.Stdout, + cli.Stderr, + parser.ParserCore.{ Parser, buildPrimitiveParser, many }, + parser.ParserStr.{ parseStr }, + ] + provides [main] to cli + +main = + lettersInput = "AAAiBByAABBwBtCCCiAyArBBx" + ifLetterA = \l -> l == A + when parseStr (many letterParser) lettersInput is + Ok letters -> + letters + |> List.keepIf ifLetterA + |> List.map \_ -> 1 + |> List.sum + |> Num.toStr + |> \countLetterA -> Stdout.line "I counted \(countLetterA) letter A's!" + + Err _ -> Stderr.line "Ooops, something went wrong parsing letters" + +Letter : [A, B, C, Other] + +letterParser : Parser (List U8) Letter +letterParser = + input <- buildPrimitiveParser + + valResult = + when input is + [] -> Err (ParsingFailure "Nothing to parse") + ['A', ..] -> Ok A + ['B', ..] -> Ok B + ['C', ..] -> Ok C + _ -> Ok Other + + valResult + |> Result.map \val -> { val, input: List.dropFirst input 1 } + +expect + input = "B" + parser = letterParser + result = parseStr parser input + result == Ok B + +expect + input = "BCXA" + parser = many letterParser + result = parseStr parser input + result == Ok [B, C, Other, A] diff --git a/examples/parser/examples/parse-movies-csv.roc b/examples/parser/examples/parse-movies-csv.roc new file mode 100644 index 0000000000..580666f88d --- /dev/null +++ b/examples/parser/examples/parse-movies-csv.roc @@ -0,0 +1,65 @@ +app "example" + packages { + pf: "../../platform-switching/zig-platform/main.roc", + parser: "../package/main.roc", + } + imports [ + parser.ParserCore.{ Parser, map, keep }, + parser.ParserStr.{ RawStr, strFromRaw }, + parser.ParserCSV.{ CSV, record, field, string, nat, parseStr }, + ] + provides [main] to pf + +input : Str +input = "Airplane!,1980,\"Robert Hays,Julie Hagerty\"\r\nCaddyshack,1980,\"Chevy Chase,Rodney Dangerfield,Ted Knight,Michael O'Keefe,Bill Murray\"" + +main : Str +main = + when parseStr movieInfoParser input is + Ok movies -> + moviesString = + movies + |> List.map movieInfoExplanation + |> Str.joinWith ("\n") + nMovies = List.len movies |> Num.toStr + + "\(nMovies) movies were found:\n\n\(moviesString)\n\nParse success!\n" + + Err problem -> + when problem is + ParsingFailure failure -> + "Parsing failure: \(failure)\n" + + ParsingIncomplete leftover -> + leftoverStr = leftover |> List.map strFromRaw |> List.map (\val -> "\"\(val)\"") |> Str.joinWith ", " + + "Parsing incomplete. Following leftover fields while parsing a record: \(leftoverStr)\n" + + SyntaxError error -> + "Parsing failure. Syntax error in the CSV: \(error)" + +MovieInfo := { title : Str, releaseYear : Nat, actors : List Str } + +movieInfoParser = + record (\title -> \releaseYear -> \actors -> @MovieInfo { title, releaseYear, actors }) + |> keep (field string) + |> keep (field nat) + |> keep (field actorsParser) + +actorsParser = + string + |> map (\val -> Str.split val ",") + +movieInfoExplanation = \@MovieInfo { title, releaseYear, actors } -> + enumeratedActors = enumerate actors + releaseYearStr = Num.toStr releaseYear + + "The movie '\(title)' was released in \(releaseYearStr) and stars \(enumeratedActors)" + +enumerate : List Str -> Str +enumerate = \elements -> + { before: inits, others: last } = List.split elements (List.len elements - 1) + + last + |> List.prepend (inits |> Str.joinWith ", ") + |> Str.joinWith " and " diff --git a/examples/parser/package/ParserCSV.roc b/examples/parser/package/ParserCSV.roc new file mode 100644 index 0000000000..ee6f5928c2 --- /dev/null +++ b/examples/parser/package/ParserCSV.roc @@ -0,0 +1,196 @@ +interface ParserCSV + exposes [ + CSV, + CSVRecord, + file, + record, + parseStr, + parseCSV, + parseStrToCSVRecord, + field, + string, + nat, + f64, + ] + imports [ + ParserCore.{ Parser, parse, buildPrimitiveParser, alt, map, many, sepBy1, between, ignore, flatten, sepBy }, + ParserStr.{ RawStr, oneOf, codeunit, codeunitSatisfies, strFromRaw }, + ] + +## This is a CSV parser which follows RFC4180 +## +## For simplicity's sake, the following things are not yet supported: +## - CSV files with headings +## +## The following however *is* supported +## - A simple LF ("\n") instead of CRLF ("\r\n") to separate records. +CSV : List CSVRecord +CSVRecord : List CSVField +CSVField : RawStr + +## Attempts to parse an `a` from a `Str` that is encoded in CSV format. +parseStr : Parser CSVRecord a, Str -> Result (List a) [ParsingFailure Str, SyntaxError Str, ParsingIncomplete CSVRecord] +parseStr = \csvParser, input -> + when parseStrToCSV input is + Err (ParsingIncomplete rest) -> + restStr = ParserStr.strFromRaw rest + + Err (SyntaxError restStr) + + Err (ParsingFailure str) -> + Err (ParsingFailure str) + + Ok csvData -> + when parseCSV csvParser csvData is + Err (ParsingFailure str) -> + Err (ParsingFailure str) + + Err (ParsingIncomplete problem) -> + Err (ParsingIncomplete problem) + + Ok vals -> + Ok vals + +## Attempts to parse an `a` from a `CSV` datastructure (a list of lists of bytestring-fields). +parseCSV : Parser CSVRecord a, CSV -> Result (List a) [ParsingFailure Str, ParsingIncomplete CSVRecord] +parseCSV = \csvParser, csvData -> + csvData + |> List.mapWithIndex (\recordFieldsList, index -> { record: recordFieldsList, index: index }) + |> List.walkUntil (Ok []) \state, { record: recordFieldsList, index: index } -> + when parseCSVRecord csvParser recordFieldsList is + Err (ParsingFailure problem) -> + indexStr = Num.toStr (index + 1) + recordStr = recordFieldsList |> List.map strFromRaw |> List.map (\val -> "\"\(val)\"") |> Str.joinWith ", " + problemStr = "\(problem)\nWhile parsing record no. \(indexStr): `\(recordStr)`" + + Break (Err (ParsingFailure problemStr)) + + Err (ParsingIncomplete problem) -> + Break (Err (ParsingIncomplete problem)) + + Ok val -> + state + |> Result.map (\vals -> List.append vals val) + |> Continue + +## Attempts to parse an `a` from a `CSVRecord` datastructure (a list of bytestring-fields) +## +## This parser succeeds when all fields of the CSVRecord are consumed by the parser. +parseCSVRecord : Parser CSVRecord a, CSVRecord -> Result a [ParsingFailure Str, ParsingIncomplete CSVRecord] +parseCSVRecord = \csvParser, recordFieldsList -> + parse csvParser recordFieldsList (\leftover -> leftover == []) + +## Wrapper function to combine a set of fields into your desired `a` +## +## ## Usage example +## +## >>> record (\firstName -> \lastName -> \age -> User {firstName, lastName, age}) +## >>> |> field string +## >>> |> field string +## >>> |> field nat +## +record : a -> Parser CSVRecord a +record = ParserCore.const + +## Turns a parser for a `List U8` into a parser that parses part of a `CSVRecord`. +field : Parser RawStr a -> Parser CSVRecord a +field = \fieldParser -> + buildPrimitiveParser \fieldsList -> + when List.get fieldsList 0 is + Err OutOfBounds -> + Err (ParsingFailure "expected another CSV field but there are no more fields in this record") + + Ok rawStr -> + when ParserStr.parseRawStr fieldParser rawStr is + Ok val -> + Ok { val: val, input: List.dropFirst fieldsList 1 } + + Err (ParsingFailure reason) -> + fieldStr = rawStr |> strFromRaw + + Err (ParsingFailure "Field `\(fieldStr)` could not be parsed. \(reason)") + + Err (ParsingIncomplete reason) -> + reasonStr = strFromRaw reason + fieldsStr = fieldsList |> List.map strFromRaw |> Str.joinWith ", " + + Err (ParsingFailure "The field parser was unable to read the whole field: `\(reasonStr)` while parsing the first field of leftover \(fieldsStr))") + +## Parser for a field containing a UTF8-encoded string +string : Parser CSVField Str +string = ParserStr.anyString + +## Parse a natural number from a CSV field +nat : Parser CSVField Nat +nat = + string + |> map + (\val -> + when Str.toNat val is + Ok num -> + Ok num + + Err _ -> + Err "\(val) is not a Nat." + ) + |> flatten + +## Parse a 64-bit float from a CSV field +f64 : Parser CSVField F64 +f64 = + string + |> map + (\val -> + when Str.toF64 val is + Ok num -> + Ok num + + Err _ -> + Err "\(val) is not a F64." + ) + |> flatten + +## Attempts to parse a Str into the internal `CSV` datastructure (A list of lists of bytestring-fields). +parseStrToCSV : Str -> Result CSV [ParsingFailure Str, ParsingIncomplete RawStr] +parseStrToCSV = \input -> + parse file (Str.toUtf8 input) (\leftover -> leftover == []) + +## Attempts to parse a Str into the internal `CSVRecord` datastructure (A list of bytestring-fields). +parseStrToCSVRecord : Str -> Result CSVRecord [ParsingFailure Str, ParsingIncomplete RawStr] +parseStrToCSVRecord = \input -> + parse csvRecord (Str.toUtf8 input) (\leftover -> leftover == []) + +# The following are parsers to turn strings into CSV structures +file : Parser RawStr CSV +file = sepBy csvRecord endOfLine + +csvRecord : Parser RawStr CSVRecord +csvRecord = sepBy1 csvField comma + +csvField : Parser RawStr CSVField +csvField = alt escapedCsvField nonescapedCsvField + +escapedCsvField : Parser RawStr CSVField +escapedCsvField = between escapedContents dquote dquote +escapedContents = many + ( + oneOf [ + twodquotes |> map (\_ -> '"'), + comma, + cr, + lf, + textdata, + ] + ) + +twodquotes = ParserStr.string "\"\"" + +nonescapedCsvField : Parser RawStr CSVField +nonescapedCsvField = many textdata +comma = codeunit ',' +dquote = codeunit '"' +endOfLine = alt (ignore crlf) (ignore lf) +cr = codeunit '\r' +lf = codeunit '\n' +crlf = ParserStr.string "\r\n" +textdata = codeunitSatisfies (\x -> (x >= 32 && x <= 33) || (x >= 35 && x <= 43) || (x >= 45 && x <= 126)) # Any printable char except " (34) and , (44) diff --git a/examples/parser/package/ParserCore.roc b/examples/parser/package/ParserCore.roc new file mode 100644 index 0000000000..096ad6d9e0 --- /dev/null +++ b/examples/parser/package/ParserCore.roc @@ -0,0 +1,354 @@ +interface ParserCore + exposes [ + Parser, + ParseResult, + parse, + parsePartial, + fail, + const, + alt, + keep, + skip, + oneOf, + map, + map2, + map3, + lazy, + maybe, + oneOrMore, + many, + between, + sepBy, + sepBy1, + ignore, + buildPrimitiveParser, + flatten, + ] + imports [] + +## Opaque type for a parser that will try to parse an `a` from an `input`. +## +## As a simple example, you might consider a parser that tries to parse a `U32` from a `Str`. +## Such a process might succeed or fail, depending on the current value of `input`. +## +## As such, a parser can be considered a recipe +## for a function of the type `input -> Result {val: a, input: input} [ParsingFailure Str]`. +## +## How a parser is _actually_ implemented internally is not important +## and this might change between versions; +## for instance to improve efficiency or error messages on parsing failures. +Parser input a := input -> ParseResult input a + +ParseResult input a : Result { val : a, input : input } [ParsingFailure Str] + +buildPrimitiveParser : (input -> ParseResult input a) -> Parser input a +buildPrimitiveParser = \fun -> + @Parser fun + +# -- Generic parsers: +## Most general way of running a parser. +## +## Can be tought of turning the recipe of a parser into its actual parsing function +## and running this function on the given input. +## +## Many (but not all!) parsers consume part of `input` when they succeed. +## This allows you to string parsers together that run one after the other: +## The part of the input that the first parser did not consume, is used by the next parser. +## This is why a parser returns on success both the resulting value and the leftover part of the input. +## +## Of course, this is mostly useful when creating your own internal parsing building blocks. +## `run` or `Parser.Str.runStr` etc. are more useful in daily usage. +parsePartial : Parser input a, input -> ParseResult input a +parsePartial = \@Parser parser, input -> + parser input + +## Runs a parser on the given input, expecting it to fully consume the input +## +## The `input -> Bool` parameter is used to check whether parsing has 'completed', +## (in other words: Whether all of the input has been consumed.) +## +## For most (but not all!) input types, a parsing run that leaves some unparsed input behind +## should be considered an error. +parse : Parser input a, input, (input -> Bool) -> Result a [ParsingFailure Str, ParsingIncomplete input] +parse = \parser, input, isParsingCompleted -> + when parsePartial parser input is + Ok { val: val, input: leftover } -> + if isParsingCompleted leftover then + Ok val + else + Err (ParsingIncomplete leftover) + + Err (ParsingFailure msg) -> + Err (ParsingFailure msg) + +## Parser that can never succeed, regardless of the given input. +## It will always fail with the given error message. +## +## This is mostly useful as 'base case' if all other parsers +## in a `oneOf` or `alt` have failed, to provide some more descriptive error message. +fail : Str -> Parser * * +fail = \msg -> + @Parser \_input -> Err (ParsingFailure msg) + +## Parser that will always produce the given `val`, without looking at the actual input. +## This is useful as basic building block, especially in combination with +## `map` and `keep`. +const : a -> Parser * a +const = \val -> + @Parser \input -> + Ok { val: val, input: input } + +## Try the `first` parser and (only) if it fails, try the `second` parser as fallback. +alt : Parser input a, Parser input a -> Parser input a +alt = \first, second -> + buildPrimitiveParser \input -> + when parsePartial first input is + Ok { val: val, input: rest } -> Ok { val: val, input: rest } + Err (ParsingFailure firstErr) -> + when parsePartial second input is + Ok { val: val, input: rest } -> Ok { val: val, input: rest } + Err (ParsingFailure secondErr) -> + Err (ParsingFailure ("\(firstErr) or \(secondErr)")) + +## Runs a parser building a function, then a parser building a value, +## and finally returns the result of calling the function with the value. +## +## This is useful if you are building up a structure that requires more parameters +## than there are variants of `map`, `map2`, `map3` etc. for. +## +## For instance, the following two are the same: +## +## >>> const (\x, y, z -> Triple x y z) +## >>> |> map3 Parser.Str.nat Parser.Str.nat Parser.Str.nat +## +## >>> const (\x -> \y -> \z -> Triple x y z) +## >>> |> keep Parser.Str.nat +## >>> |> keep Parser.Str.nat +## >>> |> keep Parser.Str.nat +## +## (And indeed, this is how `map`, `map2`, `map3` etc. are implemented under the hood.) +## +## # Currying +## Be aware that when using `keep`, you need to explicitly 'curry' the parameters to the construction function. +## This means that instead of writing `\x, y, z -> ...` +## you'll need to write `\x -> \y -> \z -> ...`. +## This is because the parameters to the function will be applied one-by-one as parsing continues. +keep : Parser input (a -> b), Parser input a -> Parser input b +keep = \funParser, valParser -> + @Parser \input -> + when parsePartial funParser input is + Ok { val: funVal, input: rest } -> + when parsePartial valParser rest is + Ok { val: val, input: rest2 } -> + Ok { val: funVal val, input: rest2 } + + Err e -> + Err e + + Err e -> + Err e + +## Skip over a parsed item as part of a pipeline +## +## This is useful if you are using a pipeline of parsers with `keep` but +## some parsed items are not part of the final result +## +## >>> const (\x -> \y -> \z -> Triple x y z) +## >>> |> keep Parser.Str.nat +## >>> |> skip (codeunit ',') +## >>> |> keep Parser.Str.nat +## >>> |> skip (codeunit ',') +## >>> |> keep Parser.Str.nat +## +skip : Parser input kept, Parser input skipped -> Parser input kept +skip = \kept, skipped -> + @Parser \input -> + when parsePartial kept input is + Ok step1 -> + when parsePartial skipped step1.input is + Ok step2 -> + Ok { val: step1.val, input: step2.input } + + Err e -> + Err e + + Err e -> + Err e + +# Internal utility function. Not exposed to users, since usage is discouraged! +# +# Runs `firstParser` and (only) if it succeeds, +# runs the function `buildNextParser` on its result value. +# This function returns a new parser, which is finally run. +# +# `andThen` is usually more flexible than necessary, and less efficient +# than using `const` with `map` and/or `keep`. +# Consider using those functions first. +andThen : Parser input a, (a -> Parser input b) -> Parser input b +andThen = \@Parser firstParser, buildNextParser -> + @Parser \input -> + when firstParser input is + Ok step -> + (@Parser nextParser) = buildNextParser step.val + nextParser step.input + + Err e -> + Err e + +## Try a list of parsers in turn, until one of them succeeds +oneOf : List (Parser input a) -> Parser input a +oneOf = \parsers -> + List.walkBackwards parsers (fail "oneOf: The list of parsers was empty") (\laterParser, earlierParser -> alt earlierParser laterParser) + +## Transforms the result of parsing into something else, +## using the given transformation function. +map : Parser input a, (a -> b) -> Parser input b +map = \@Parser simpleParser, transform -> + @Parser \input -> + when simpleParser input is + Ok step -> + Ok { val: transform step.val, input: step.input } + + Err e -> + Err e + +## Transforms the result of parsing into something else, +## using the given two-parameter transformation function. +map2 : Parser input a, Parser input b, (a, b -> c) -> Parser input c +map2 = \@Parser parserA, @Parser parserB, transform -> + @Parser \input -> + when parserA input is + Ok step1 -> + when parserB step1.input is + Ok step2 -> + Ok { val: transform step1.val step2.val, input: step2.input } + + Err e -> + Err e + + Err e -> + Err e + +## Transforms the result of parsing into something else, +## using the given three-parameter transformation function. +## +## If you need transformations with more inputs, +## take a look at `keep`. +map3 : Parser input a, Parser input b, Parser input c, (a, b, c -> d) -> Parser input d +map3 = \@Parser parserA, @Parser parserB, @Parser parserC, transform -> + @Parser \input -> + when parserA input is + Ok step1 -> + when parserB step1.input is + Ok step2 -> + when parserC step2.input is + Ok step3 -> + Ok { val: transform step1.val step2.val step3.val, input: step3.input } + + Err e -> + Err e + + Err e -> + Err e + + Err e -> + Err e + +# ^ And this could be repeated for as high as we want, of course. +# Removes a layer of 'result' from running the parser. +# +# This allows for instance to map functions that return a result over the parser, +# where errors are turned into `ParsingFailure` s. +flatten : Parser input (Result a Str) -> Parser input a +flatten = \parser -> + buildPrimitiveParser \input -> + result = parsePartial parser input + + when result is + Err problem -> + Err problem + + Ok { val: Ok val, input: inputRest } -> + Ok { val: val, input: inputRest } + + Ok { val: Err problem, input: _inputRest } -> + Err (ParsingFailure problem) + +## Runs a parser lazily +## +## This is (only) useful when dealing with a recursive structure. +## For instance, consider a type `Comment : { message: String, responses: List Comment }`. +## Without `lazy`, you would ask the compiler to build an infinitely deep parser. +## (Resulting in a compiler error.) +## +lazy : ({} -> Parser input a) -> Parser input a +lazy = \thunk -> + const {} + |> andThen thunk + +maybe : Parser input a -> Parser input (Result a [Nothing]) +maybe = \parser -> + alt (parser |> map (\val -> Ok val)) (const (Err Nothing)) + +manyImpl : Parser input a, List a, input -> ParseResult input (List a) +manyImpl = \@Parser parser, vals, input -> + result = parser input + + when result is + Err _ -> + Ok { val: vals, input: input } + + Ok { val: val, input: inputRest } -> + manyImpl (@Parser parser) (List.append vals val) inputRest + +## A parser which runs the element parser *zero* or more times on the input, +## returning a list containing all the parsed elements. +## +## Also see `oneOrMore`. +many : Parser input a -> Parser input (List a) +many = \parser -> + @Parser \input -> + manyImpl parser [] input + +## A parser which runs the element parser *one* or more times on the input, +## returning a list containing all the parsed elements. +## +## Also see `many`. +oneOrMore : Parser input a -> Parser input (List a) +oneOrMore = \@Parser parser -> + @Parser \input -> + when parser input is + Ok step -> + manyImpl (@Parser parser) [step.val] step.input + + Err e -> + Err e + +## Runs a parser for an 'opening' delimiter, then your main parser, then the 'closing' delimiter, +## and only returns the result of your main parser. +## +## Useful to recognize structures surrounded by delimiters (like braces, parentheses, quotes, etc.) +## +## >>> betweenBraces = \parser -> parser |> between (scalar '[') (scalar ']') +between : Parser input a, Parser input open, Parser input close -> Parser input a +between = \parser, open, close -> + map3 open parser close (\_, val, _ -> val) + +sepBy1 : Parser input a, Parser input sep -> Parser input (List a) +sepBy1 = \parser, separator -> + parserFollowedBySep = + const (\_ -> \val -> val) + |> keep separator + |> keep parser + + const (\val -> \vals -> List.prepend vals val) + |> keep parser + |> keep (many parserFollowedBySep) + +sepBy : Parser input a, Parser input sep -> Parser input (List a) +sepBy = \parser, separator -> + alt (sepBy1 parser separator) (const []) + +ignore : Parser input a -> Parser input {} +ignore = \parser -> + map parser (\_ -> {}) diff --git a/examples/parser/package/ParserHttp.roc b/examples/parser/package/ParserHttp.roc new file mode 100644 index 0000000000..2d798a3cba --- /dev/null +++ b/examples/parser/package/ParserHttp.roc @@ -0,0 +1,250 @@ +interface ParserHttp + exposes [ + Request, + Response, + request, + response, + ] + imports [ + ParserCore.{ Parser, ParseResult, map, keep, skip, const, oneOrMore, many }, + ParserStr.{ RawStr, oneOf, string, codeunit, parseStr, codeunitSatisfies, strFromRaw, anyRawString }, + ] + +# https://www.ietf.org/rfc/rfc2616.txt +Method : [Options, Get, Post, Put, Delete, Head, Trace, Connect, Patch] + +HttpVersion : Str + +Request : { + method : Method, + uri : Str, + httpVersion : HttpVersion, + headers : List [Header Str Str], + body : List U8, +} + +Response : { + httpVersion : HttpVersion, + statusCode : Str, + status : Str, + headers : List [Header Str Str], + body : List U8, +} + +method : Parser RawStr Method +method = + oneOf [ + string "OPTIONS" |> map \_ -> Options, + string "GET" |> map \_ -> Get, + string "POST" |> map \_ -> Post, + string "PUT" |> map \_ -> Put, + string "DELETE" |> map \_ -> Delete, + string "HEAD" |> map \_ -> Head, + string "TRACE" |> map \_ -> Trace, + string "CONNECT" |> map \_ -> Connect, + string "PATCH" |> map \_ -> Patch, + ] + +expect parseStr method "GET" == Ok Get +expect parseStr method "DELETE" == Ok Delete + +# TODO: do we want more structure in the URI, or is Str actually what programs want anyway? +# This is not a full URL! +# Request-URI = "*" | absoluteURI | abs_path | authority +RequestUri : Str + +requestUri : Parser RawStr RequestUri +requestUri = + codeunitSatisfies \c -> c != ' ' + |> oneOrMore + |> map strFromRaw + +sp = codeunit ' ' +crlf = string "\r\n" + +# TODO: The 'digit' and 'digits' from ParserStr are causing repl.expect to blow up +digit = codeunitSatisfies \c -> c >= '0' && c <= '9' +digits = digit |> oneOrMore |> map strFromRaw + +httpVersion : Parser RawStr HttpVersion +httpVersion = + const (\major -> \minor -> "\(major).\(minor)") + |> skip (string "HTTP/") + |> keep digits + |> skip (codeunit '.') + |> keep digits + +Header : [Header Str Str] + +stringWithoutColon : Parser RawStr Str +stringWithoutColon = + codeunitSatisfies \c -> c != ':' + |> oneOrMore + |> map strFromRaw + +stringWithoutCr : Parser RawStr Str +stringWithoutCr = + codeunitSatisfies \c -> c != '\r' + |> oneOrMore + |> map strFromRaw + +header : Parser RawStr Header +header = + const (\k -> \v -> Header k v) + |> keep stringWithoutColon + |> skip (string ": ") + |> keep stringWithoutCr + |> skip crlf + +expect + actual = parseStr header "Accept-Encoding: gzip, deflate\r\n" + expected = Ok (Header "Accept-Encoding" "gzip, deflate") + actual == expected + +request : Parser RawStr Request +request = + const (\m -> \u -> \hv -> \hs -> \b -> { method: m, uri: u, httpVersion: hv, headers: hs, body: b }) + |> keep method + |> skip sp + |> keep requestUri + |> skip sp + |> keep httpVersion + |> skip crlf + |> keep (many header) + |> skip crlf + |> keep anyRawString + +expect + requestText = + """ + GET /things?id=1 HTTP/1.1\r + Host: bar.example\r + Accept-Encoding: gzip, deflate\r + \r + Hello, world! + """ + actual = + parseStr request requestText + + expected : Result Request [ParsingFailure Str, ParsingIncomplete Str] + expected = Ok { + method: Get, + uri: "/things?id=1", + httpVersion: "1.1", + headers: [ + Header "Host" "bar.example", + Header "Accept-Encoding" "gzip, deflate", + ], + body: "Hello, world!" |> Str.toUtf8, + } + actual == expected + +expect + requestText = + """ + OPTIONS /resources/post-here/ HTTP/1.1\r + Host: bar.example\r + Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r + Accept-Language: en-us,en;q=0.5\r + Accept-Encoding: gzip,deflate\r + Connection: keep-alive\r + Origin: https://foo.example\r + Access-Control-Request-Method: POST\r + Access-Control-Request-Headers: X-PINGOTHER, Content-Type\r + \r\n + """ + actual = + parseStr request requestText + expected = Ok { + method: Options, + uri: "/resources/post-here/", + httpVersion: "1.1", + headers: [ + Header "Host" "bar.example", + Header "Accept" "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", + Header "Accept-Language" "en-us,en;q=0.5", + Header "Accept-Encoding" "gzip,deflate", + Header "Connection" "keep-alive", + Header "Origin" "https://foo.example", + Header "Access-Control-Request-Method" "POST", + Header "Access-Control-Request-Headers" "X-PINGOTHER, Content-Type", + ], + body: [], + } + actual == expected + +response : Parser RawStr Response +response = + const (\hv -> \sc -> \s -> \hs -> \b -> { httpVersion: hv, statusCode: sc, status: s, headers: hs, body: b }) + |> keep httpVersion + |> skip sp + |> keep digits + |> skip sp + |> keep stringWithoutCr + |> skip crlf + |> keep (many header) + |> skip crlf + |> keep anyRawString + +expect + body = + """ + \r + \r + \r + \r + A simple webpage\r + \r + \r +

Simple HTML webpage

\r +

Hello, world!

\r + \r + \r\n + """ + responseText = + """ + HTTP/1.1 200 OK\r + Content-Type: text/html; charset=utf-8\r + Content-Length: 55743\r + Connection: keep-alive\r + Cache-Control: s-maxage=300, public, max-age=0\r + Content-Language: en-US\r + Date: Thu, 06 Dec 2018 17:37:18 GMT\r + ETag: "2e77ad1dc6ab0b53a2996dfd4653c1c3"\r + Server: meinheld/0.6.1\r + Strict-Transport-Security: max-age=63072000\r + X-Content-Type-Options: nosniff\r + X-Frame-Options: DENY\r + X-XSS-Protection: 1; mode=block\r + Vary: Accept-Encoding,Cookie\r + Age: 7\r + \r + \(body) + """ + actual = + parseStr response responseText + expected = + Ok { + httpVersion: "1.1", + statusCode: "200", + status: "OK", + headers: [ + Header "Content-Type" "text/html; charset=utf-8", + Header "Content-Length" "55743", + Header "Connection" "keep-alive", + Header "Cache-Control" "s-maxage=300, public, max-age=0", + Header "Content-Language" "en-US", + Header "Date" "Thu, 06 Dec 2018 17:37:18 GMT", + Header "ETag" "\"2e77ad1dc6ab0b53a2996dfd4653c1c3\"", + Header "Server" "meinheld/0.6.1", + Header "Strict-Transport-Security" "max-age=63072000", + Header "X-Content-Type-Options" "nosniff", + Header "X-Frame-Options" "DENY", + Header "X-XSS-Protection" "1; mode=block", + Header "Vary" "Accept-Encoding,Cookie", + Header "Age" "7", + ], + body: Str.toUtf8 body, + } + actual == expected + diff --git a/examples/parser/package/ParserStr.roc b/examples/parser/package/ParserStr.roc new file mode 100644 index 0000000000..f433c03023 --- /dev/null +++ b/examples/parser/package/ParserStr.roc @@ -0,0 +1,225 @@ +interface ParserStr + exposes [ + RawStr, + parseStr, + parseStrPartial, + parseRawStr, + parseRawStrPartial, + string, + stringRaw, + codeunit, + codeunitSatisfies, + anyString, + anyRawString, + anyCodeunit, + scalar, + oneOf, + digit, + positiveInt, + strFromRaw, + ] + imports [ + ParserCore.{ + Parser, + ParseResult, + map, + oneOrMore, + parse, + parsePartial, + buildPrimitiveParser, + }, + ] + +# Specific string-based parsers: +RawStr : List U8 + +strFromRaw : RawStr -> Str +strFromRaw = \rawStr -> + rawStr + |> Str.fromUtf8 + |> Result.withDefault "Unexpected problem while turning a List U8 (that was originally a Str) back into a Str. This should never happen!" + +strToRaw : Str -> RawStr +strToRaw = \str -> + str |> Str.toUtf8 + +strFromScalar : U32 -> Str +strFromScalar = \scalarVal -> + Str.appendScalar "" (Num.intCast scalarVal) + |> Result.withDefault "Unexpected problem while turning a U32 (that was probably originally a scalar constant) into a Str. This should never happen!" + +strFromCodeunit : U8 -> Str +strFromCodeunit = \cu -> + strFromRaw [cu] + +## Runs a parser against the start of a list of scalars, allowing the parser to consume it only partially. +parseRawStrPartial : Parser RawStr a, RawStr -> ParseResult RawStr a +parseRawStrPartial = \parser, input -> + parsePartial parser input + +## Runs a parser against the start of a string, allowing the parser to consume it only partially. +## +## - If the parser succeeds, returns the resulting value as well as the leftover input. +## - If the parser fails, returns `Err (ParsingFailure msg)` +parseStrPartial : Parser RawStr a, Str -> ParseResult Str a +parseStrPartial = \parser, input -> + parser + |> parseRawStrPartial (strToRaw input) + |> Result.map \{ val: val, input: restRaw } -> + { val: val, input: strFromRaw restRaw } + +## Runs a parser against a string, requiring the parser to consume it fully. +## +## - If the parser succeeds, returns `Ok val` +## - If the parser fails, returns `Err (ParsingFailure msg)` +## - If the parser succeeds but does not consume the full string, returns `Err (ParsingIncomplete leftover)` +parseRawStr : Parser RawStr a, RawStr -> Result a [ParsingFailure Str, ParsingIncomplete RawStr] +parseRawStr = \parser, input -> + parse parser input (\leftover -> List.len leftover == 0) + +parseStr : Parser RawStr a, Str -> Result a [ParsingFailure Str, ParsingIncomplete Str] +parseStr = \parser, input -> + parser + |> parseRawStr (strToRaw input) + |> Result.mapErr \problem -> + when problem is + ParsingFailure msg -> + ParsingFailure msg + + ParsingIncomplete leftoverRaw -> + ParsingIncomplete (strFromRaw leftoverRaw) + +codeunitSatisfies : (U8 -> Bool) -> Parser RawStr U8 +codeunitSatisfies = \check -> + buildPrimitiveParser \input -> + { before: start, others: inputRest } = List.split input 1 + + when List.get start 0 is + Err OutOfBounds -> + Err (ParsingFailure "expected a codeunit satisfying a condition, but input was empty.") + + Ok startCodeunit -> + if check startCodeunit then + Ok { val: startCodeunit, input: inputRest } + else + otherChar = strFromCodeunit startCodeunit + inputStr = strFromRaw input + + Err (ParsingFailure "expected a codeunit satisfying a condition but found `\(otherChar)`.\n While reading: `\(inputStr)`") + +# Implemented manually instead of on top of codeunitSatisfies +# because of better error messages +codeunit : U8 -> Parser RawStr U8 +codeunit = \expectedCodeUnit -> + buildPrimitiveParser \input -> + { before: start, others: inputRest } = List.split input 1 + + when List.get start 0 is + Err OutOfBounds -> + errorChar = strFromCodeunit expectedCodeUnit + + Err (ParsingFailure "expected char `\(errorChar)` but input was empty.") + + Ok startCodeunit -> + if startCodeunit == expectedCodeUnit then + Ok { val: expectedCodeUnit, input: inputRest } + else + errorChar = strFromCodeunit expectedCodeUnit + otherChar = strFromRaw start + inputStr = strFromRaw input + + Err (ParsingFailure "expected char `\(errorChar)` but found `\(otherChar)`.\n While reading: `\(inputStr)`") + +# Implemented manually instead of a sequence of codeunits +# because of efficiency and better error messages +stringRaw : List U8 -> Parser RawStr (List U8) +stringRaw = \expectedString -> + buildPrimitiveParser \input -> + { before: start, others: inputRest } = List.split input (List.len expectedString) + + if start == expectedString then + Ok { val: expectedString, input: inputRest } + else + errorString = strFromRaw expectedString + otherString = strFromRaw start + inputString = strFromRaw input + + Err (ParsingFailure "expected string `\(errorString)` but found `\(otherString)`.\nWhile reading: \(inputString)") + +string : Str -> Parser RawStr Str +string = \expectedString -> + strToRaw expectedString + |> stringRaw + |> map \_val -> expectedString + +scalar : U32 -> Parser RawStr U32 +scalar = \expectedScalar -> + expectedScalar + |> strFromScalar + |> string + |> map \_ -> expectedScalar + +# Matches any codeunit +anyCodeunit : Parser RawStr U8 +anyCodeunit = codeunitSatisfies (\_ -> Bool.true) + +# Matches any bytestring +# and consumes all of it. +# Does not fail. +anyRawString : Parser RawStr RawStr +anyRawString = buildPrimitiveParser \rawStringValue -> + Ok { val: rawStringValue, input: [] } + +# Matches any string +# as long as it is valid UTF8. +anyString : Parser RawStr Str +anyString = buildPrimitiveParser \fieldRawString -> + when Str.fromUtf8 fieldRawString is + Ok stringVal -> + Ok { val: stringVal, input: [] } + + Err (BadUtf8 _ _) -> + Err (ParsingFailure "Expected a string field, but its contents cannot be parsed as UTF8.") + +# betweenBraces : Parser RawStr a -> Parser RawStr a +# betweenBraces = \parser -> +# between parser (scalar '[') (scalar ']') +digit : Parser RawStr U8 +digit = + digitParsers = + List.range { start: At '0', end: At '9' } + |> List.map \digitNum -> + digitNum + |> codeunit + |> map \_ -> digitNum + + oneOf digitParsers + +# NOTE: Currently happily accepts leading zeroes +positiveInt : Parser RawStr (Int *) +positiveInt = + oneOrMore digit + |> map \digitsList -> + digitsList + |> List.map \char -> Num.intCast char - '0' + |> List.walk 0 \sum, digitVal -> 10 * sum + digitVal + +## Try a bunch of different parsers. +## +## The first parser which is tried is the one at the front of the list, +## and the next one is tried until one succeeds or the end of the list was reached. +## +## >>> boolParser : Parser RawStr Bool +## >>> boolParser = oneOf [string "true", string "false"] |> map (\x -> if x == "true" then True else False) +# NOTE: This implementation works, but is limited to parsing strings. +# Blocked until issue #3444 is fixed. +oneOf : List (Parser RawStr a) -> Parser RawStr a +oneOf = \parsers -> + buildPrimitiveParser \input -> + List.walkUntil parsers (Err (ParsingFailure "(no possibilities)")) \_, parser -> + when parseRawStrPartial parser input is + Ok val -> + Break (Ok val) + + Err problem -> + Continue (Err problem) diff --git a/examples/parser/package/main.roc b/examples/parser/package/main.roc new file mode 100644 index 0000000000..9ab47e3047 --- /dev/null +++ b/examples/parser/package/main.roc @@ -0,0 +1,8 @@ +package "parser" + exposes [ + ParserCore, + ParserCSV, + ParserStr, + ParserHttp, + ] + packages {} diff --git a/examples/platform-switching/.gitignore b/examples/platform-switching/.gitignore new file mode 100644 index 0000000000..5cbd00ddc7 --- /dev/null +++ b/examples/platform-switching/.gitignore @@ -0,0 +1,8 @@ +rocLovesC +rocLovesPlatforms +rocLovesRust +rocLovesSwift +rocLovesWebAssembly +rocLovesZig +main +*.wasm diff --git a/examples/platform-switching/README.md b/examples/platform-switching/README.md new file mode 100644 index 0000000000..1db17157f8 --- /dev/null +++ b/examples/platform-switching/README.md @@ -0,0 +1,20 @@ +# Platform switching + +To run, `cd` into this directory and run this in your terminal: + +```bash +roc run +``` + +This will run `main.roc` because, unless you explicitly give it a filename, `roc run` +defaults to running a file named `main.roc`. Other `roc` commands (like `roc build`, `roc test`, and so on) also default to `main.roc` unless you explicitly give them a filename. + +## About this example + +This uses a very simple platform which does nothing more than printing the string you give it. + +The line `main = "Which platform am I running on now?\n"` sets this string to be `"Which platform am I running on now?"` with a newline at the end, and the lines `packages { pf: "c-platform/main.roc" }` and `provides [main] to pf` specify that the `c-platform/` directory contains this app's platform. + +This platform is called `c-platform` because its lower-level code is written in C. There's also a `rust-platform`, `zig-platform`, and so on; if you like, you can try switching `pf: "c-platform/main.roc"` to `pf: "zig-platform/main.roc"` or `pf: "rust-platform/main.roc"` to try one of those platforms instead. They all do similar things, so the application won't look any different. + +If you want to start building your own platforms, these are some very simple example platforms to use as starting points. diff --git a/examples/platform-switching/c-platform/host.c b/examples/platform-switching/c-platform/host.c new file mode 100644 index 0000000000..3683afaa98 --- /dev/null +++ b/examples/platform-switching/c-platform/host.c @@ -0,0 +1,120 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#else +#include // shm_open +#include // for mmap +#include // for kill +#endif + +void* roc_alloc(size_t size, unsigned int alignment) { return malloc(size); } + +void* roc_realloc(void* ptr, size_t new_size, size_t old_size, unsigned int alignment) { + return realloc(ptr, new_size); +} + +void roc_dealloc(void* ptr, unsigned int alignment) { free(ptr); } + +void roc_panic(void* ptr, unsigned int alignment) { + char* msg = (char*)ptr; + fprintf(stderr, + "Application crashed with message\n\n %s\n\nShutting down\n", msg); + exit(1); +} + +void roc_dbg(char* loc, char* msg) { + fprintf(stderr, "[%s] %s\n", loc, msg); +} + +void* roc_memset(void* str, int c, size_t n) { return memset(str, c, n); } + +int roc_shm_open(char* name, int oflag, int mode) { +#ifdef _WIN32 + return 0; +#else + return shm_open(name, oflag, mode); +#endif +} +void* roc_mmap(void* addr, int length, int prot, int flags, int fd, int offset) { +#ifdef _WIN32 + return addr; +#else + return mmap(addr, length, prot, flags, fd, offset); +#endif +} + +int roc_getppid() { +#ifdef _WIN32 + return 0; +#else + return getppid(); +#endif +} + +struct RocStr { + char* bytes; + size_t len; + size_t capacity; +}; + +bool is_small_str(struct RocStr str) { return ((ssize_t)str.capacity) < 0; } + +// Determine the length of the string, taking into +// account the small string optimization +size_t roc_str_len(struct RocStr str) { + char* bytes = (char*)&str; + char last_byte = bytes[sizeof(str) - 1]; + char last_byte_xored = last_byte ^ 0b10000000; + size_t small_len = (size_t)(last_byte_xored); + size_t big_len = str.len; + + // Avoid branch misprediction costs by always + // determining both small_len and big_len, + // so this compiles to a cmov instruction. + if (is_small_str(str)) { + return small_len; + } else { + return big_len; + } +} + +extern void roc__mainForHost_1_exposed_generic(struct RocStr *string); + +int main() { + + struct RocStr str; + roc__mainForHost_1_exposed_generic(&str); + + // Determine str_len and the str_bytes pointer, + // taking into account the small string optimization. + size_t str_len = roc_str_len(str); + char* str_bytes; + + if (is_small_str(str)) { + str_bytes = (char*)&str; + } else { + str_bytes = str.bytes; + } + + // Write to stdout + if (write(1, str_bytes, str_len) >= 0) { + // Writing succeeded! + + // NOTE: the string is a static string, read from in the binary + // if you make it a heap-allocated string, it'll be leaked here + return 0; + } else { + printf("Error writing to stdout: %s\n", strerror(errno)); + + // NOTE: the string is a static string, read from in the binary + // if you make it a heap-allocated string, it'll be leaked here + return 1; + } +} diff --git a/examples/platform-switching/c-platform/main.roc b/examples/platform-switching/c-platform/main.roc new file mode 100644 index 0000000000..35c286a30e --- /dev/null +++ b/examples/platform-switching/c-platform/main.roc @@ -0,0 +1,9 @@ +platform "echo-in-c" + requires {} { main : Str } + exposes [] + packages {} + imports [] + provides [mainForHost] + +mainForHost : Str +mainForHost = main diff --git a/examples/platform-switching/main.roc b/examples/platform-switching/main.roc new file mode 100644 index 0000000000..8b4696cbaf --- /dev/null +++ b/examples/platform-switching/main.roc @@ -0,0 +1,11 @@ +app "rocLovesPlatforms" + packages { pf: "c-platform/main.roc" } + # To switch platforms, comment-out the line above and un-comment one below. + # packages { pf: "rust-platform/main.roc" } + # packages { pf: "swift-platform/main.roc" } + # packages { pf: "web-assembly-platform/main.roc" } # See ./web-assembly-platform/README.md + # packages { pf: "zig-platform/main.roc" } + imports [] + provides [main] to pf + +main = "Which platform am I running on now?\n" diff --git a/examples/platform-switching/rocLovesC.roc b/examples/platform-switching/rocLovesC.roc new file mode 100644 index 0000000000..43e7f606f6 --- /dev/null +++ b/examples/platform-switching/rocLovesC.roc @@ -0,0 +1,6 @@ +app "rocLovesC" + packages { pf: "c-platform/main.roc" } + imports [] + provides [main] to pf + +main = "Roc <3 C!\n" diff --git a/examples/platform-switching/rocLovesRust.roc b/examples/platform-switching/rocLovesRust.roc new file mode 100644 index 0000000000..7d9c8da26b --- /dev/null +++ b/examples/platform-switching/rocLovesRust.roc @@ -0,0 +1,6 @@ +app "rocLovesRust" + packages { pf: "rust-platform/main.roc" } + imports [] + provides [main] to pf + +main = "Roc <3 Rust!\n" diff --git a/examples/platform-switching/rocLovesSwift.roc b/examples/platform-switching/rocLovesSwift.roc new file mode 100644 index 0000000000..8dcf22a97f --- /dev/null +++ b/examples/platform-switching/rocLovesSwift.roc @@ -0,0 +1,6 @@ +app "rocLovesSwift" + packages { pf: "swift-platform/main.roc" } + imports [] + provides [main] to pf + +main = "Roc <3 Swift!\n" diff --git a/examples/platform-switching/rocLovesWebAssembly.roc b/examples/platform-switching/rocLovesWebAssembly.roc new file mode 100644 index 0000000000..ff550ad37a --- /dev/null +++ b/examples/platform-switching/rocLovesWebAssembly.roc @@ -0,0 +1,6 @@ +app "rocLovesWebAssembly" + packages { pf: "web-assembly-platform/main.roc" } + imports [] + provides [main] to pf + +main = "Roc <3 Web Assembly!\n" diff --git a/examples/platform-switching/rocLovesZig.roc b/examples/platform-switching/rocLovesZig.roc new file mode 100644 index 0000000000..fe838c5396 --- /dev/null +++ b/examples/platform-switching/rocLovesZig.roc @@ -0,0 +1,6 @@ +app "rocLovesZig" + packages { pf: "zig-platform/main.roc" } + imports [] + provides [main] to pf + +main = "Roc <3 Zig!\n" diff --git a/examples/platform-switching/rust-platform/Cargo.toml b/examples/platform-switching/rust-platform/Cargo.toml new file mode 100644 index 0000000000..b329e6472b --- /dev/null +++ b/examples/platform-switching/rust-platform/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "host" +authors = ["The Roc Contributors"] +edition = "2021" +license = "UPL-1.0" +links = "app" +version = "0.0.1" + +[lib] +name = "host" +path = "src/lib.rs" +crate-type = ["staticlib", "rlib"] + +[[bin]] +name = "host" +path = "src/main.rs" + +[dependencies] +libc = "0.2" +roc_std = { path = "../../../crates/roc_std" } + +[workspace] diff --git a/examples/platform-switching/rust-platform/build.rs b/examples/platform-switching/rust-platform/build.rs new file mode 100644 index 0000000000..47763b34c3 --- /dev/null +++ b/examples/platform-switching/rust-platform/build.rs @@ -0,0 +1,9 @@ +fn main() { + #[cfg(not(windows))] + println!("cargo:rustc-link-lib=dylib=app"); + + #[cfg(windows)] + println!("cargo:rustc-link-lib=dylib=libapp"); + + println!("cargo:rustc-link-search=."); +} diff --git a/examples/platform-switching/rust-platform/host.c b/examples/platform-switching/rust-platform/host.c new file mode 100644 index 0000000000..b9214bcf33 --- /dev/null +++ b/examples/platform-switching/rust-platform/host.c @@ -0,0 +1,3 @@ +extern int rust_main(); + +int main() { return rust_main(); } \ No newline at end of file diff --git a/examples/platform-switching/rust-platform/main.roc b/examples/platform-switching/rust-platform/main.roc new file mode 100644 index 0000000000..13dd322d6b --- /dev/null +++ b/examples/platform-switching/rust-platform/main.roc @@ -0,0 +1,9 @@ +platform "echo-in-rust" + requires {} { main : Str } + exposes [] + packages {} + imports [] + provides [mainForHost] + +mainForHost : Str +mainForHost = main diff --git a/examples/platform-switching/rust-platform/rust-toolchain.toml b/examples/platform-switching/rust-platform/rust-toolchain.toml new file mode 100644 index 0000000000..cc6d7e3f5d --- /dev/null +++ b/examples/platform-switching/rust-platform/rust-toolchain.toml @@ -0,0 +1,9 @@ +[toolchain] +channel = "1.71.1" + +profile = "default" + +components = [ + # for usages of rust-analyzer or similar tools inside `nix develop` + "rust-src" +] \ No newline at end of file diff --git a/examples/platform-switching/rust-platform/src/glue.rs b/examples/platform-switching/rust-platform/src/glue.rs new file mode 100644 index 0000000000..e3e2e524b7 --- /dev/null +++ b/examples/platform-switching/rust-platform/src/glue.rs @@ -0,0 +1,744 @@ +// ⚠️ GENERATED CODE ⚠️ - this entire file was generated by the `roc glue` CLI command + +#![allow(unused_unsafe)] +#![allow(unused_variables)] +#![allow(dead_code)] +#![allow(unused_mut)] +#![allow(non_snake_case)] +#![allow(non_camel_case_types)] +#![allow(non_upper_case_globals)] +#![allow(clippy::undocumented_unsafe_blocks)] +#![allow(clippy::redundant_static_lifetimes)] +#![allow(clippy::unused_unit)] +#![allow(clippy::missing_safety_doc)] +#![allow(clippy::let_and_return)] +#![allow(clippy::missing_safety_doc)] +#![allow(clippy::redundant_static_lifetimes)] +#![allow(clippy::needless_borrow)] +#![allow(clippy::clone_on_copy)] + +type Op_StderrWrite = roc_std::RocStr; +type Op_StdoutWrite = roc_std::RocStr; +type TODO_roc_function_69 = roc_std::RocStr; +type TODO_roc_function_70 = roc_std::RocStr; + +#[cfg(any( + target_arch = "arm", + target_arch = "aarch64", + target_arch = "wasm32", + target_arch = "x86", + target_arch = "x86_64" +))] +#[derive(Clone, Copy, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[repr(u8)] +pub enum discriminant_Op { + Done = 0, + StderrWrite = 1, + StdoutWrite = 2, +} + +impl core::fmt::Debug for discriminant_Op { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Self::Done => f.write_str("discriminant_Op::Done"), + Self::StderrWrite => f.write_str("discriminant_Op::StderrWrite"), + Self::StdoutWrite => f.write_str("discriminant_Op::StdoutWrite"), + } + } +} + +#[cfg(any( + target_arch = "arm", + target_arch = "aarch64", + target_arch = "wasm32", + target_arch = "x86", + target_arch = "x86_64" +))] +#[repr(transparent)] +pub struct Op { + pointer: *mut union_Op, +} + +#[cfg(any( + target_arch = "arm", + target_arch = "aarch64", + target_arch = "wasm32", + target_arch = "x86", + target_arch = "x86_64" +))] +#[repr(C)] +union union_Op { + StderrWrite: core::mem::ManuallyDrop, + StdoutWrite: core::mem::ManuallyDrop, + _sizer: [u8; 8], +} + +#[cfg(any( + target_arch = "arm", + target_arch = "arm", + target_arch = "aarch64", + target_arch = "aarch64", + target_arch = "wasm32", + target_arch = "wasm32", + target_arch = "x86", + target_arch = "x86", + target_arch = "x86_64", + target_arch = "x86_64" +))] +//TODO HAS CLOSURE 2 +#[cfg(any( + target_arch = "arm", + target_arch = "aarch64", + target_arch = "wasm32", + target_arch = "x86", + target_arch = "x86_64" +))] +#[repr(C)] +pub struct RocFunction_66 { + pub closure_data: roc_std::RocList, +} + +impl RocFunction_66 { + pub fn force_thunk(mut self, arg_0: ()) -> Op { + extern "C" { + fn roc__mainForHost_0_caller(arg_0: &(), closure_data: *mut u8, output: *mut Op); + } + + let mut output = std::mem::MaybeUninit::uninit(); + let ptr = self.closure_data.as_mut_ptr(); + unsafe { roc__mainForHost_0_caller(&arg_0, ptr, output.as_mut_ptr()) }; + unsafe { output.assume_init() } + } +} + +#[cfg(any( + target_arch = "arm", + target_arch = "aarch64", + target_arch = "wasm32", + target_arch = "x86", + target_arch = "x86_64" +))] +#[repr(C)] +pub struct RocFunction_67 { + pub closure_data: roc_std::RocList, +} + +impl RocFunction_67 { + pub fn force_thunk(mut self, arg_0: ()) -> Op { + extern "C" { + fn roc__mainForHost_1_caller(arg_0: &(), closure_data: *mut u8, output: *mut Op); + } + + let mut output = std::mem::MaybeUninit::uninit(); + let ptr = self.closure_data.as_mut_ptr(); + unsafe { roc__mainForHost_1_caller(&arg_0, ptr, output.as_mut_ptr()) }; + unsafe { output.assume_init() } + } +} + +impl Op { + #[cfg(any( + target_arch = "arm", + target_arch = "aarch64", + target_arch = "wasm32", + target_arch = "x86", + target_arch = "x86_64" + ))] + #[inline(always)] + fn storage(&self) -> Option<&core::cell::Cell> { + let mask = match std::mem::size_of::() { + 4 => 0b11, + 8 => 0b111, + _ => unreachable!(), + }; + + // NOTE: pointer provenance is probably lost here + let unmasked_address = (self.pointer as usize) & !mask; + let untagged = unmasked_address as *const core::cell::Cell; + + if untagged.is_null() { + None + } else { + unsafe { Some(&*untagged.sub(1)) } + } + } + + #[cfg(any(target_arch = "arm", target_arch = "wasm32", target_arch = "x86"))] + /// Returns which variant this tag union holds. Note that this never includes a payload! + pub fn discriminant(&self) -> discriminant_Op { + // The discriminant is stored in the unused bytes at the end of the recursive pointer + unsafe { core::mem::transmute::((self.pointer as u8) & 0b11) } + } + + #[cfg(any(target_arch = "arm", target_arch = "wasm32", target_arch = "x86"))] + /// Internal helper + fn tag_discriminant(pointer: *mut union_Op, discriminant: discriminant_Op) -> *mut union_Op { + // The discriminant is stored in the unused bytes at the end of the union pointer + let untagged = (pointer as usize) & (!0b11 as usize); + let tagged = untagged | (discriminant as usize); + + tagged as *mut union_Op + } + + #[cfg(any(target_arch = "arm", target_arch = "wasm32", target_arch = "x86"))] + /// Internal helper + fn union_pointer(&self) -> *mut union_Op { + // The discriminant is stored in the unused bytes at the end of the union pointer + ((self.pointer as usize) & (!0b11 as usize)) as *mut union_Op + } + + #[cfg(any( + target_arch = "arm", + target_arch = "aarch64", + target_arch = "wasm32", + target_arch = "x86", + target_arch = "x86_64" + ))] + /// A tag named Done, which has no payload. + pub const Done: Self = Self { + pointer: core::ptr::null_mut(), + }; + + #[cfg(any( + target_arch = "arm", + target_arch = "aarch64", + target_arch = "wasm32", + target_arch = "x86", + target_arch = "x86_64" + ))] + /// Unsafely assume this `Op` has a `.discriminant()` of `StderrWrite` and return its payload at index 0. + /// (Always examine `.discriminant()` first to make sure this is the correct variant!) + /// Panics in debug builds if the `.discriminant()` doesn't return `StderrWrite`. + pub unsafe fn get_StderrWrite_0(&self) -> roc_std::RocStr { + debug_assert_eq!(self.discriminant(), discriminant_Op::StderrWrite); + + extern "C" { + #[link_name = "roc__getter__2_generic"] + fn getter(_: *mut roc_std::RocStr, _: *const Op); + } + + let mut ret = core::mem::MaybeUninit::uninit(); + getter(ret.as_mut_ptr(), self); + ret.assume_init() + } + + #[cfg(any( + target_arch = "arm", + target_arch = "aarch64", + target_arch = "wasm32", + target_arch = "x86", + target_arch = "x86_64" + ))] + /// Unsafely assume this `Op` has a `.discriminant()` of `StderrWrite` and return its payload at index 1. + /// (Always examine `.discriminant()` first to make sure this is the correct variant!) + /// Panics in debug builds if the `.discriminant()` doesn't return `StderrWrite`. + pub unsafe fn get_StderrWrite_1(&self) -> RocFunction_67 { + debug_assert_eq!(self.discriminant(), discriminant_Op::StderrWrite); + + extern "C" { + #[link_name = "roc__getter__3_size"] + fn size() -> usize; + + #[link_name = "roc__getter__3_generic"] + fn getter(_: *mut u8, _: *const Op); + } + + // allocate memory to store this variably-sized value + // allocates with roc_alloc, but that likely still uses the heap + let it = std::iter::repeat(0xAAu8).take(size()); + let mut bytes = roc_std::RocList::from_iter(it); + + getter(bytes.as_mut_ptr(), self); + + RocFunction_67 { + closure_data: bytes, + } + } + + #[cfg(any( + target_arch = "arm", + target_arch = "aarch64", + target_arch = "wasm32", + target_arch = "x86", + target_arch = "x86_64" + ))] + /// Construct a tag named `StderrWrite`, with the appropriate payload + pub fn StderrWrite(arg: Op_StderrWrite) -> Self { + let size = core::mem::size_of::(); + let align = core::mem::align_of::() as u32; + + unsafe { + let ptr = roc_std::roc_alloc_refcounted::(); + + *ptr = union_Op { + StderrWrite: core::mem::ManuallyDrop::new(arg), + }; + + Self { + pointer: Self::tag_discriminant(ptr, discriminant_Op::StderrWrite), + } + } + } + + #[cfg(any(target_arch = "arm", target_arch = "wasm32", target_arch = "x86"))] + /// Unsafely assume this `Op` has a `.discriminant()` of `StderrWrite` and convert it to `StderrWrite`'s payload. + /// (Always examine `.discriminant()` first to make sure this is the correct variant!) + /// Panics in debug builds if the `.discriminant()` doesn't return `StderrWrite`. + pub unsafe fn into_StderrWrite(mut self) -> Op_StderrWrite { + debug_assert_eq!(self.discriminant(), discriminant_Op::StderrWrite); + let payload = { + let ptr = (self.pointer as usize & !0b11) as *mut union_Op; + let mut uninitialized = core::mem::MaybeUninit::uninit(); + let swapped = unsafe { + core::mem::replace( + &mut (*ptr).StderrWrite, + core::mem::ManuallyDrop::new(uninitialized.assume_init()), + ) + }; + + core::mem::forget(self); + + core::mem::ManuallyDrop::into_inner(swapped) + }; + + payload + } + + #[cfg(any(target_arch = "arm", target_arch = "wasm32", target_arch = "x86"))] + /// Unsafely assume this `Op` has a `.discriminant()` of `StderrWrite` and return its payload. + /// (Always examine `.discriminant()` first to make sure this is the correct variant!) + /// Panics in debug builds if the `.discriminant()` doesn't return `StderrWrite`. + pub unsafe fn as_StderrWrite(&self) -> &Op_StderrWrite { + debug_assert_eq!(self.discriminant(), discriminant_Op::StderrWrite); + let payload = { + let ptr = (self.pointer as usize & !0b11) as *mut union_Op; + + unsafe { &(*ptr).StderrWrite } + }; + + &payload + } + + #[cfg(any( + target_arch = "arm", + target_arch = "aarch64", + target_arch = "wasm32", + target_arch = "x86", + target_arch = "x86_64" + ))] + /// Unsafely assume this `Op` has a `.discriminant()` of `StdoutWrite` and return its payload at index 0. + /// (Always examine `.discriminant()` first to make sure this is the correct variant!) + /// Panics in debug builds if the `.discriminant()` doesn't return `StdoutWrite`. + pub unsafe fn get_StdoutWrite_0(&self) -> roc_std::RocStr { + debug_assert_eq!(self.discriminant(), discriminant_Op::StdoutWrite); + + extern "C" { + #[link_name = "roc__getter__2_generic"] + fn getter(_: *mut roc_std::RocStr, _: *const Op); + } + + let mut ret = core::mem::MaybeUninit::uninit(); + getter(ret.as_mut_ptr(), self); + ret.assume_init() + } + + #[cfg(any( + target_arch = "arm", + target_arch = "aarch64", + target_arch = "wasm32", + target_arch = "x86", + target_arch = "x86_64" + ))] + /// Unsafely assume this `Op` has a `.discriminant()` of `StdoutWrite` and return its payload at index 1. + /// (Always examine `.discriminant()` first to make sure this is the correct variant!) + /// Panics in debug builds if the `.discriminant()` doesn't return `StdoutWrite`. + pub unsafe fn get_StdoutWrite_1(&self) -> RocFunction_66 { + debug_assert_eq!(self.discriminant(), discriminant_Op::StdoutWrite); + + extern "C" { + #[link_name = "roc__getter__3_size"] + fn size() -> usize; + + #[link_name = "roc__getter__3_generic"] + fn getter(_: *mut u8, _: *const Op); + } + + // allocate memory to store this variably-sized value + // allocates with roc_alloc, but that likely still uses the heap + let it = std::iter::repeat(0xAAu8).take(size()); + let mut bytes = roc_std::RocList::from_iter(it); + + getter(bytes.as_mut_ptr(), self); + + RocFunction_66 { + closure_data: bytes, + } + } + + #[cfg(any( + target_arch = "arm", + target_arch = "aarch64", + target_arch = "wasm32", + target_arch = "x86", + target_arch = "x86_64" + ))] + /// Construct a tag named `StdoutWrite`, with the appropriate payload + pub fn StdoutWrite(arg: Op_StdoutWrite) -> Self { + let size = core::mem::size_of::(); + let align = core::mem::align_of::() as u32; + + unsafe { + let ptr = roc_std::roc_alloc_refcounted::(); + + *ptr = union_Op { + StdoutWrite: core::mem::ManuallyDrop::new(arg), + }; + + Self { + pointer: Self::tag_discriminant(ptr, discriminant_Op::StdoutWrite), + } + } + } + + #[cfg(any(target_arch = "arm", target_arch = "wasm32", target_arch = "x86"))] + /// Unsafely assume this `Op` has a `.discriminant()` of `StdoutWrite` and convert it to `StdoutWrite`'s payload. + /// (Always examine `.discriminant()` first to make sure this is the correct variant!) + /// Panics in debug builds if the `.discriminant()` doesn't return `StdoutWrite`. + pub unsafe fn into_StdoutWrite(mut self) -> Op_StdoutWrite { + debug_assert_eq!(self.discriminant(), discriminant_Op::StdoutWrite); + let payload = { + let ptr = (self.pointer as usize & !0b11) as *mut union_Op; + let mut uninitialized = core::mem::MaybeUninit::uninit(); + let swapped = unsafe { + core::mem::replace( + &mut (*ptr).StdoutWrite, + core::mem::ManuallyDrop::new(uninitialized.assume_init()), + ) + }; + + core::mem::forget(self); + + core::mem::ManuallyDrop::into_inner(swapped) + }; + + payload + } + + #[cfg(any(target_arch = "arm", target_arch = "wasm32", target_arch = "x86"))] + /// Unsafely assume this `Op` has a `.discriminant()` of `StdoutWrite` and return its payload. + /// (Always examine `.discriminant()` first to make sure this is the correct variant!) + /// Panics in debug builds if the `.discriminant()` doesn't return `StdoutWrite`. + pub unsafe fn as_StdoutWrite(&self) -> &Op_StdoutWrite { + debug_assert_eq!(self.discriminant(), discriminant_Op::StdoutWrite); + let payload = { + let ptr = (self.pointer as usize & !0b11) as *mut union_Op; + + unsafe { &(*ptr).StdoutWrite } + }; + + &payload + } + + #[cfg(any(target_arch = "aarch64", target_arch = "x86_64"))] + /// Returns which variant this tag union holds. Note that this never includes a payload! + pub fn discriminant(&self) -> discriminant_Op { + // The discriminant is stored in the unused bytes at the end of the recursive pointer + unsafe { core::mem::transmute::((self.pointer as u8) & 0b111) } + } + + #[cfg(any(target_arch = "aarch64", target_arch = "x86_64"))] + /// Internal helper + fn tag_discriminant(pointer: *mut union_Op, discriminant: discriminant_Op) -> *mut union_Op { + // The discriminant is stored in the unused bytes at the end of the union pointer + let untagged = (pointer as usize) & (!0b111 as usize); + let tagged = untagged | (discriminant as usize); + + tagged as *mut union_Op + } + + #[cfg(any(target_arch = "aarch64", target_arch = "x86_64"))] + /// Internal helper + fn union_pointer(&self) -> *mut union_Op { + // The discriminant is stored in the unused bytes at the end of the union pointer + ((self.pointer as usize) & (!0b111 as usize)) as *mut union_Op + } + + #[cfg(any(target_arch = "aarch64", target_arch = "x86_64"))] + /// Unsafely assume this `Op` has a `.discriminant()` of `StderrWrite` and convert it to `StderrWrite`'s payload. + /// (Always examine `.discriminant()` first to make sure this is the correct variant!) + /// Panics in debug builds if the `.discriminant()` doesn't return `StderrWrite`. + pub unsafe fn into_StderrWrite(mut self) -> Op_StderrWrite { + debug_assert_eq!(self.discriminant(), discriminant_Op::StderrWrite); + let payload = { + let ptr = (self.pointer as usize & !0b111) as *mut union_Op; + let mut uninitialized = core::mem::MaybeUninit::uninit(); + let swapped = unsafe { + core::mem::replace( + &mut (*ptr).StderrWrite, + core::mem::ManuallyDrop::new(uninitialized.assume_init()), + ) + }; + + core::mem::forget(self); + + core::mem::ManuallyDrop::into_inner(swapped) + }; + + payload + } + + #[cfg(any(target_arch = "aarch64", target_arch = "x86_64"))] + /// Unsafely assume this `Op` has a `.discriminant()` of `StderrWrite` and return its payload. + /// (Always examine `.discriminant()` first to make sure this is the correct variant!) + /// Panics in debug builds if the `.discriminant()` doesn't return `StderrWrite`. + pub unsafe fn as_StderrWrite(&self) -> &Op_StderrWrite { + debug_assert_eq!(self.discriminant(), discriminant_Op::StderrWrite); + let payload = { + let ptr = (self.pointer as usize & !0b111) as *mut union_Op; + + unsafe { &(*ptr).StderrWrite } + }; + + &payload + } + + #[cfg(any(target_arch = "aarch64", target_arch = "x86_64"))] + /// Unsafely assume this `Op` has a `.discriminant()` of `StdoutWrite` and convert it to `StdoutWrite`'s payload. + /// (Always examine `.discriminant()` first to make sure this is the correct variant!) + /// Panics in debug builds if the `.discriminant()` doesn't return `StdoutWrite`. + pub unsafe fn into_StdoutWrite(mut self) -> Op_StdoutWrite { + debug_assert_eq!(self.discriminant(), discriminant_Op::StdoutWrite); + let payload = { + let ptr = (self.pointer as usize & !0b111) as *mut union_Op; + let mut uninitialized = core::mem::MaybeUninit::uninit(); + let swapped = unsafe { + core::mem::replace( + &mut (*ptr).StdoutWrite, + core::mem::ManuallyDrop::new(uninitialized.assume_init()), + ) + }; + + core::mem::forget(self); + + core::mem::ManuallyDrop::into_inner(swapped) + }; + + payload + } + + #[cfg(any(target_arch = "aarch64", target_arch = "x86_64"))] + /// Unsafely assume this `Op` has a `.discriminant()` of `StdoutWrite` and return its payload. + /// (Always examine `.discriminant()` first to make sure this is the correct variant!) + /// Panics in debug builds if the `.discriminant()` doesn't return `StdoutWrite`. + pub unsafe fn as_StdoutWrite(&self) -> &Op_StdoutWrite { + debug_assert_eq!(self.discriminant(), discriminant_Op::StdoutWrite); + let payload = { + let ptr = (self.pointer as usize & !0b111) as *mut union_Op; + + unsafe { &(*ptr).StdoutWrite } + }; + + &payload + } +} + +impl Drop for Op { + #[cfg(any( + target_arch = "arm", + target_arch = "aarch64", + target_arch = "wasm32", + target_arch = "x86", + target_arch = "x86_64" + ))] + fn drop(&mut self) { + // We only need to do any work if there's actually a heap-allocated payload. + if let Some(storage) = self.storage() { + let mut new_storage = storage.get(); + + // Decrement the refcount + let needs_dealloc = !new_storage.is_readonly() && new_storage.decrease(); + + if needs_dealloc { + // Drop the payload first. + match self.discriminant() { + discriminant_Op::Done => {} + discriminant_Op::StderrWrite => unsafe { + core::mem::ManuallyDrop::drop(&mut (&mut *self.union_pointer()).StderrWrite) + }, + discriminant_Op::StdoutWrite => unsafe { + core::mem::ManuallyDrop::drop(&mut (&mut *self.union_pointer()).StdoutWrite) + }, + } + + // Dealloc the pointer + let alignment = + core::mem::align_of::().max(core::mem::align_of::()); + + unsafe { + crate::roc_dealloc(storage.as_ptr().cast(), alignment as u32); + } + } else { + // Write the storage back. + storage.set(new_storage); + } + } + } +} + +impl Eq for Op {} + +impl PartialEq for Op { + #[cfg(any( + target_arch = "arm", + target_arch = "aarch64", + target_arch = "wasm32", + target_arch = "x86", + target_arch = "x86_64" + ))] + fn eq(&self, other: &Self) -> bool { + if self.discriminant() != other.discriminant() { + return false; + } + + unsafe { + match self.discriminant() { + discriminant_Op::Done => true, + discriminant_Op::StderrWrite => { + (&*self.union_pointer()).StderrWrite == (&*other.union_pointer()).StderrWrite + } + discriminant_Op::StdoutWrite => { + (&*self.union_pointer()).StdoutWrite == (&*other.union_pointer()).StdoutWrite + } + } + } + } +} + +impl PartialOrd for Op { + #[cfg(any( + target_arch = "arm", + target_arch = "aarch64", + target_arch = "wasm32", + target_arch = "x86", + target_arch = "x86_64" + ))] + fn partial_cmp(&self, other: &Self) -> Option { + match self.discriminant().partial_cmp(&other.discriminant()) { + Some(core::cmp::Ordering::Equal) => {} + not_eq => return not_eq, + } + + unsafe { + match self.discriminant() { + discriminant_Op::Done => Some(core::cmp::Ordering::Equal), + discriminant_Op::StderrWrite => (&*self.union_pointer()) + .StderrWrite + .partial_cmp(&(&*other.union_pointer()).StderrWrite), + discriminant_Op::StdoutWrite => (&*self.union_pointer()) + .StdoutWrite + .partial_cmp(&(&*other.union_pointer()).StdoutWrite), + } + } + } +} + +impl Ord for Op { + #[cfg(any( + target_arch = "arm", + target_arch = "aarch64", + target_arch = "wasm32", + target_arch = "x86", + target_arch = "x86_64" + ))] + fn cmp(&self, other: &Self) -> core::cmp::Ordering { + match self.discriminant().cmp(&other.discriminant()) { + core::cmp::Ordering::Equal => {} + not_eq => return not_eq, + } + + unsafe { + match self.discriminant() { + discriminant_Op::Done => core::cmp::Ordering::Equal, + discriminant_Op::StderrWrite => (&*self.union_pointer()) + .StderrWrite + .cmp(&(&*other.union_pointer()).StderrWrite), + discriminant_Op::StdoutWrite => (&*self.union_pointer()) + .StdoutWrite + .cmp(&(&*other.union_pointer()).StdoutWrite), + } + } + } +} + +impl Clone for Op { + #[cfg(any( + target_arch = "arm", + target_arch = "aarch64", + target_arch = "wasm32", + target_arch = "x86", + target_arch = "x86_64" + ))] + fn clone(&self) -> Self { + if let Some(storage) = self.storage() { + let mut new_storage = storage.get(); + if !new_storage.is_readonly() { + new_storage.increment_reference_count(); + storage.set(new_storage); + } + } + + Self { + pointer: self.pointer, + } + } +} + +impl core::hash::Hash for Op { + #[cfg(any( + target_arch = "arm", + target_arch = "aarch64", + target_arch = "wasm32", + target_arch = "x86", + target_arch = "x86_64" + ))] + fn hash(&self, state: &mut H) { + match self.discriminant() { + discriminant_Op::Done => discriminant_Op::Done.hash(state), + discriminant_Op::StderrWrite => unsafe { + discriminant_Op::StderrWrite.hash(state); + (&*self.union_pointer()).StderrWrite.hash(state); + }, + discriminant_Op::StdoutWrite => unsafe { + discriminant_Op::StdoutWrite.hash(state); + (&*self.union_pointer()).StdoutWrite.hash(state); + }, + } + } +} + +impl core::fmt::Debug for Op { + #[cfg(any( + target_arch = "arm", + target_arch = "aarch64", + target_arch = "wasm32", + target_arch = "x86", + target_arch = "x86_64" + ))] + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.write_str("Op::")?; + + unsafe { + match self.discriminant() { + discriminant_Op::Done => f.write_str("Done"), + discriminant_Op::StderrWrite => f + .debug_tuple("StderrWrite") + // TODO HAS CLOSURE + .finish(), + discriminant_Op::StdoutWrite => f + .debug_tuple("StdoutWrite") + // TODO HAS CLOSURE + .finish(), + } + } + } +} diff --git a/examples/platform-switching/rust-platform/src/lib.rs b/examples/platform-switching/rust-platform/src/lib.rs new file mode 100644 index 0000000000..db4490425b --- /dev/null +++ b/examples/platform-switching/rust-platform/src/lib.rs @@ -0,0 +1,98 @@ +#![allow(non_snake_case)] + +use core::ffi::c_void; +use roc_std::RocStr; +use std::ffi::CStr; +use std::io::Write; +use std::os::raw::c_char; + +extern "C" { + #[link_name = "roc__mainForHost_1_exposed_generic"] + fn roc_main(_: &mut RocStr); +} + +#[no_mangle] +pub unsafe extern "C" fn roc_alloc(size: usize, _alignment: u32) -> *mut c_void { + return libc::malloc(size); +} + +#[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 { + return libc::realloc(c_ptr, new_size); +} + +#[no_mangle] +pub unsafe extern "C" fn roc_dealloc(c_ptr: *mut c_void, _alignment: u32) { + return libc::free(c_ptr); +} + +#[no_mangle] +pub unsafe extern "C" fn roc_panic(msg: *mut RocStr, tag_id: u32) { + match tag_id { + 0 => { + eprintln!("Roc standard library hit a panic: {}", &*msg); + } + 1 => { + eprintln!("Application hit a panic: {}", &*msg); + } + _ => unreachable!(), + } + std::process::exit(1); +} + +#[no_mangle] +pub unsafe extern "C" fn roc_dbg(loc: *mut RocStr, msg: *mut RocStr) { + eprintln!("[{}] {}", &*loc, &*msg); +} + +#[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) +} + +#[cfg(unix)] +#[no_mangle] +pub unsafe extern "C" fn roc_getppid() -> libc::pid_t { + libc::getppid() +} + +#[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) +} + +#[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) +} + +#[no_mangle] +pub extern "C" fn rust_main() -> i32 { + let mut roc_str = RocStr::default(); + unsafe { roc_main(&mut roc_str) }; + + if let Err(e) = std::io::stdout().write_all(roc_str.as_bytes()) { + panic!("Writing to stdout failed! {:?}", e); + } + + // Exit code + 0 +} diff --git a/examples/platform-switching/rust-platform/src/main.rs b/examples/platform-switching/rust-platform/src/main.rs new file mode 100644 index 0000000000..0765384f29 --- /dev/null +++ b/examples/platform-switching/rust-platform/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + std::process::exit(host::rust_main() as _); +} diff --git a/examples/hello-world/swift-platform/host.h b/examples/platform-switching/swift-platform/host.h similarity index 100% rename from examples/hello-world/swift-platform/host.h rename to examples/platform-switching/swift-platform/host.h diff --git a/examples/hello-world/swift-platform/host.swift b/examples/platform-switching/swift-platform/host.swift similarity index 100% rename from examples/hello-world/swift-platform/host.swift rename to examples/platform-switching/swift-platform/host.swift diff --git a/examples/platform-switching/swift-platform/main.roc b/examples/platform-switching/swift-platform/main.roc new file mode 100644 index 0000000000..a9eb403cd9 --- /dev/null +++ b/examples/platform-switching/swift-platform/main.roc @@ -0,0 +1,9 @@ +platform "echo-in-swift" + requires {} { main : Str } + exposes [] + packages {} + imports [] + provides [mainForHost] + +mainForHost : Str +mainForHost = main diff --git a/examples/platform-switching/web-assembly-platform/README.md b/examples/platform-switching/web-assembly-platform/README.md new file mode 100644 index 0000000000..b9d36b1b6b --- /dev/null +++ b/examples/platform-switching/web-assembly-platform/README.md @@ -0,0 +1,67 @@ +# Hello, World! + +To run this website, we first compile the app that uses the Wasm platform: + +- If you use the nightly roc release: +```bash +./roc build --target=wasm32 examples/platform-switching/rocLovesWebAssembly.roc +``` +- If you start from the compiler source code: +```bash +# Build roc compiler if you have not done so already +cargo build +target/debug/roc build --target=wasm32 examples/platform-switching/rocLovesWebAssembly.roc +``` +We then move the file: +```bash +# Go to the directory where index.html is +cd examples/platform-switching/web-assembly-platform/ +# Move the .wasm file so that it's beside index.html +mv ../rocLovesWebAssembly.wasm . +``` + +In the directory where index.html is, run any web server on localhost. + +For example if you have Python3 on your system, you can use `http.server`: +```bash +python3 -m http.server 8080 +``` + +Or if you have npm, you can use `http-server` +```bash +npm install -g http-server +http-server +``` + +Now open your browser at + +## Design Notes + +This demonstrates the basic design of hosts: Roc code gets compiled into a pure +function (in this case, a thunk that always returns `"Hello, World!\n"`) and +then the host calls that function. Fundamentally, that's the whole idea! The host +might not even have a `main` - it could be a library, a plugin, anything. +Everything else is built on this basic "hosts calling linked pure functions" design. + +For example, things get more interesting when the compiled Roc function returns +a `Task` - that is, a tagged union data structure containing function pointers +to callback closures. This lets the Roc pure function describe arbitrary +chainable effects, which the host can interpret to perform I/O as requested by +the Roc program. (The tagged union `Task` would have a variant for each supported +I/O operation.) + +In this trivial example, it's very easy to line up the API between the host and +the Roc program. In a more involved host, this would be much trickier - especially +if the API were changing frequently during development. + +The idea there is to have a first-class concept of "glue code" which host authors +can write (it would be plain Roc code, but with some extra keywords that aren't +available in normal modules - kinda like `port module` in Elm), and which +describe both the Roc-host/C boundary as well as the Roc-host/Roc-app boundary. +Roc application authors only care about the Roc-host/Roc-app portion, and the +host author only cares about the Roc-host/C boundary when implementing the host. + +Using this glue code, the Roc compiler can generate C header files describing the +boundary. This not only gets us host compatibility with C compilers, but also +Rust FFI for free, because [`rust-bindgen`](https://github.com/rust-lang/rust-bindgen) +generates correct Rust FFI bindings from C headers. diff --git a/examples/platform-switching/web-assembly-platform/host.js b/examples/platform-switching/web-assembly-platform/host.js new file mode 100644 index 0000000000..486776fc66 --- /dev/null +++ b/examples/platform-switching/web-assembly-platform/host.js @@ -0,0 +1,65 @@ +async function roc_web_platform_run(wasm_filename, callback) { + const decoder = new TextDecoder(); + let memory_bytes; + let exit_code; + + function js_display_roc_string(str_bytes, str_len) { + const utf8_bytes = memory_bytes.subarray(str_bytes, str_bytes + str_len); + const js_string = decoder.decode(utf8_bytes); + callback(js_string); + } + + const importObj = { + wasi_snapshot_preview1: { + proc_exit: (code) => { + if (code !== 0) { + console.error(`Exited with code ${code}`); + } + exit_code = code; + }, + fd_write: (x) => { + console.error(`fd_write not supported: ${x}`); + }, + }, + env: { + js_display_roc_string, + roc_panic: (_pointer, _tag_id) => { + throw "Roc panicked!"; + }, + roc_dbg: (_loc, _msg) => { + // TODO write a proper impl. + throw "Roc dbg not supported!"; + }, + }, + }; + + const fetchPromise = fetch(wasm_filename); + + let wasm; + if (WebAssembly.instantiateStreaming) { + // streaming API has better performance if available + // It can start compiling Wasm before it has fetched all of the bytes, so we don't `await` the request! + wasm = await WebAssembly.instantiateStreaming(fetchPromise, importObj); + } else { + const response = await fetchPromise; + const module_bytes = await response.arrayBuffer(); + wasm = await WebAssembly.instantiate(module_bytes, importObj); + } + + memory_bytes = new Uint8Array(wasm.instance.exports.memory.buffer); + + try { + wasm.instance.exports._start(); + } catch (e) { + const is_ok = e.message === "unreachable" && exit_code === 0; + if (!is_ok) { + console.error(e); + } + } +} + +if (typeof module !== "undefined") { + module.exports = { + roc_web_platform_run, + }; +} diff --git a/examples/hello-world/web-platform/host.test.js b/examples/platform-switching/web-assembly-platform/host.test.js similarity index 83% rename from examples/hello-world/web-platform/host.test.js rename to examples/platform-switching/web-assembly-platform/host.test.js index ac0b1f5dc7..0152e80a8d 100644 --- a/examples/hello-world/web-platform/host.test.js +++ b/examples/platform-switching/web-assembly-platform/host.test.js @@ -15,8 +15,8 @@ global.fetch = (filename) => const { roc_web_platform_run } = require("./host"); -roc_web_platform_run("../helloWeb.wasm", (string_from_roc) => { - const expected = "Hello, World!\n"; +roc_web_platform_run("./rocLovesWebAssembly.wasm", (string_from_roc) => { + const expected = "Roc <3 Web Assembly!\n"; if (string_from_roc !== expected) { console.error(`Expected "${expected}", but got "${string_from_roc}"`); process.exit(1); diff --git a/examples/platform-switching/web-assembly-platform/host.zig b/examples/platform-switching/web-assembly-platform/host.zig new file mode 100644 index 0000000000..b6babf362f --- /dev/null +++ b/examples/platform-switching/web-assembly-platform/host.zig @@ -0,0 +1,53 @@ +const str = @import("glue").str; +const builtin = @import("builtin"); +const RocStr = str.RocStr; + +comptime { + if (builtin.target.cpu.arch != .wasm32) { + @compileError("This platform is for WebAssembly only. You need to pass `--target wasm32` to the Roc compiler."); + } +} + +const Align = 2 * @alignOf(usize); +extern fn malloc(size: usize) callconv(.C) ?*align(Align) anyopaque; +extern fn realloc(c_ptr: [*]align(Align) u8, size: usize) callconv(.C) ?*anyopaque; +extern fn free(c_ptr: [*]align(Align) u8) callconv(.C) void; +extern fn memcpy(dest: *anyopaque, src: *anyopaque, count: usize) *anyopaque; + +export fn roc_alloc(size: usize, alignment: u32) callconv(.C) ?*anyopaque { + _ = alignment; + + return malloc(size); +} + +export fn roc_realloc(c_ptr: *anyopaque, new_size: usize, old_size: usize, alignment: u32) callconv(.C) ?*anyopaque { + _ = old_size; + _ = alignment; + + return realloc(@as([*]align(Align) u8, @alignCast(@ptrCast(c_ptr))), new_size); +} + +export fn roc_dealloc(c_ptr: *anyopaque, alignment: u32) callconv(.C) void { + _ = alignment; + + free(@as([*]align(Align) u8, @alignCast(@ptrCast(c_ptr)))); +} + +// NOTE roc_panic and roc_dbg is provided in the JS file, so it can throw an exception + +extern fn roc__mainForHost_1_exposed(*RocStr) void; + +extern fn js_display_roc_string(str_bytes: ?[*]u8, str_len: usize) void; + +pub fn main() u8 { + // actually call roc to populate the callresult + var callresult = RocStr.empty(); + roc__mainForHost_1_exposed(&callresult); + + // display the result using JavaScript + js_display_roc_string(callresult.asU8ptrMut(), callresult.len()); + + callresult.decref(); + + return 0; +} diff --git a/examples/platform-switching/web-assembly-platform/index.html b/examples/platform-switching/web-assembly-platform/index.html new file mode 100644 index 0000000000..a664e3469c --- /dev/null +++ b/examples/platform-switching/web-assembly-platform/index.html @@ -0,0 +1,12 @@ + + +
+ + + + diff --git a/examples/platform-switching/web-assembly-platform/main.roc b/examples/platform-switching/web-assembly-platform/main.roc new file mode 100644 index 0000000000..08830392cd --- /dev/null +++ b/examples/platform-switching/web-assembly-platform/main.roc @@ -0,0 +1,9 @@ +platform "echo-in-web-assembly" + requires {} { main : Str } + exposes [] + packages {} + imports [] + provides [mainForHost] + +mainForHost : Str +mainForHost = main diff --git a/examples/platform-switching/zig-platform/host.zig b/examples/platform-switching/zig-platform/host.zig new file mode 100644 index 0000000000..84f1ac0117 --- /dev/null +++ b/examples/platform-switching/zig-platform/host.zig @@ -0,0 +1,130 @@ +const std = @import("std"); +const builtin = @import("builtin"); +const str = @import("glue").str; +const RocStr = str.RocStr; +const testing = std.testing; +const expectEqual = testing.expectEqual; +const expect = testing.expect; + +const Align = 2 * @alignOf(usize); +extern fn malloc(size: usize) callconv(.C) ?*align(Align) anyopaque; +extern fn realloc(c_ptr: [*]align(Align) u8, size: usize) callconv(.C) ?*anyopaque; +extern fn free(c_ptr: [*]align(Align) u8) callconv(.C) void; +extern fn memcpy(dst: [*]u8, src: [*]u8, size: usize) callconv(.C) void; +extern fn memset(dst: [*]u8, value: i32, size: usize) callconv(.C) void; + +const DEBUG: bool = false; + +export fn roc_alloc(size: usize, alignment: u32) callconv(.C) ?*anyopaque { + if (DEBUG) { + var ptr = malloc(size); + const stdout = std.io.getStdOut().writer(); + stdout.print("alloc: {d} (alignment {d}, size {d})\n", .{ ptr, alignment, size }) catch unreachable; + return ptr; + } else { + return malloc(size); + } +} + +export fn roc_realloc(c_ptr: *anyopaque, new_size: usize, old_size: usize, alignment: u32) callconv(.C) ?*anyopaque { + if (DEBUG) { + const stdout = std.io.getStdOut().writer(); + stdout.print("realloc: {d} (alignment {d}, old_size {d})\n", .{ c_ptr, alignment, old_size }) catch unreachable; + } + + return realloc(@as([*]align(Align) u8, @alignCast(@ptrCast(c_ptr))), new_size); +} + +export fn roc_dealloc(c_ptr: *anyopaque, alignment: u32) callconv(.C) void { + if (DEBUG) { + const stdout = std.io.getStdOut().writer(); + stdout.print("dealloc: {d} (alignment {d})\n", .{ c_ptr, alignment }) catch unreachable; + } + + free(@as([*]align(Align) u8, @alignCast(@ptrCast(c_ptr)))); +} + +export fn roc_panic(msg: *RocStr, tag_id: u32) callconv(.C) void { + const stderr = std.io.getStdErr().writer(); + switch (tag_id) { + 0 => { + stderr.print("Roc standard library crashed with message\n\n {s}\n\nShutting down\n", .{msg.asSlice()}) catch unreachable; + }, + 1 => { + stderr.print("Application crashed with message\n\n {s}\n\nShutting down\n", .{msg.asSlice()}) catch unreachable; + }, + else => unreachable, + } + std.process.exit(1); +} + +export fn roc_dbg(loc: *RocStr, msg: *RocStr) callconv(.C) void { + const stderr = std.io.getStdErr().writer(); + stderr.print("[{s}] {s}\n", .{ loc.asSlice(), msg.asSlice() }) catch unreachable; +} + +export fn roc_memset(dst: [*]u8, value: i32, size: usize) callconv(.C) void { + return memset(dst, value, size); +} + +extern fn kill(pid: c_int, sig: c_int) c_int; +extern fn shm_open(name: *const i8, oflag: c_int, mode: c_uint) c_int; +extern fn mmap(addr: ?*anyopaque, length: c_uint, prot: c_int, flags: c_int, fd: c_int, offset: c_uint) *anyopaque; +extern fn getppid() c_int; + +fn roc_getppid() callconv(.C) c_int { + return getppid(); +} + +fn roc_getppid_windows_stub() callconv(.C) c_int { + return 0; +} + +fn roc_shm_open(name: *const i8, oflag: c_int, mode: c_uint) callconv(.C) c_int { + return shm_open(name, oflag, mode); +} +fn roc_mmap(addr: ?*anyopaque, length: c_uint, prot: c_int, flags: c_int, fd: c_int, offset: c_uint) callconv(.C) *anyopaque { + return mmap(addr, length, prot, flags, fd, offset); +} + +comptime { + if (builtin.os.tag == .macos or builtin.os.tag == .linux) { + @export(roc_getppid, .{ .name = "roc_getppid", .linkage = .Strong }); + @export(roc_mmap, .{ .name = "roc_mmap", .linkage = .Strong }); + @export(roc_shm_open, .{ .name = "roc_shm_open", .linkage = .Strong }); + } + + if (builtin.os.tag == .windows) { + @export(roc_getppid_windows_stub, .{ .name = "roc_getppid", .linkage = .Strong }); + } +} + +const mem = std.mem; +const Allocator = mem.Allocator; + +extern fn roc__mainForHost_1_exposed_generic(*RocStr) void; + +const Unit = extern struct {}; + +pub fn main() u8 { + const stdout = std.io.getStdOut().writer(); + const stderr = std.io.getStdErr().writer(); + + var timer = std.time.Timer.start() catch unreachable; + + // actually call roc to populate the callresult + var callresult = RocStr.empty(); + roc__mainForHost_1_exposed_generic(&callresult); + + const nanos = timer.read(); + const seconds = (@as(f64, @floatFromInt(nanos)) / 1_000_000_000.0); + + // stdout the result + stdout.print("{s}", .{callresult.asSlice()}) catch unreachable; + + callresult.decref(); + + stderr.print("runtime: {d:.3}ms\n", .{seconds * 1000}) catch unreachable; + + return 0; +} diff --git a/examples/platform-switching/zig-platform/main.roc b/examples/platform-switching/zig-platform/main.roc new file mode 100644 index 0000000000..a52fe9a480 --- /dev/null +++ b/examples/platform-switching/zig-platform/main.roc @@ -0,0 +1,9 @@ +platform "echo-in-zig" + requires {} { main : Str } + exposes [] + packages {} + imports [] + provides [mainForHost] + +mainForHost : Str +mainForHost = main diff --git a/examples/python-interop/.gitignore b/examples/python-interop/.gitignore new file mode 100644 index 0000000000..fbee8bbbfc --- /dev/null +++ b/examples/python-interop/.gitignore @@ -0,0 +1,5 @@ +libhello* +.interop_env/ +build/ +demo.egg-info/ +dist/ diff --git a/examples/python-interop/README.md b/examples/python-interop/README.md new file mode 100644 index 0000000000..4dbbed8f3e --- /dev/null +++ b/examples/python-interop/README.md @@ -0,0 +1,82 @@ +# Python Interop + +This is a demo for calling Roc code from [Python](https://www.python.org/). + +## Installation + +The following was tested on NixOS, with `Python 3.10`, `clang 13.0.1`, `gcc 11.3.0` but should work with most recent versions of those on most modern Linux and MacOS.\ +Of course you're welcome to test on your machine and tell me (via [Zulip](https://roc.zulipchat.com/#narrow/pm-with/583319-dank)) if you ran into any issues or limitations. + +> Because of some rough edges, the linux installation may be a bit more involved (nothing too bad, mostly stuff like renames), so for your convenience I made a small shell script to help out. + +Now in favor of users of all OSs, let's first do a step by step walkthrough on how the build process works, and later, a few key notes on the implementation. + +## Building the Roc library + +First, `cd` into this directory and run this in your terminal: + +```sh +roc build --lib +``` + +This compiles your Roc code into a binary library in the current directory. The library's filename will be `libhello` plus an OS-specific extension (e.g. `libhello.dylib` on macOS). + +## Some Linux Specific Prep Work +As of the time of writing this document, `roc build --lib` generates a shared object with the suffix `.so.1.0`.\ +This `.0` suffix is not needed by neither the application nor Python, so we can simply rename it. + +``` sh +mv libhello.so.1.0 libhello.so.1 +``` +But, one of which does expect plain libhello.so, so we symlink it: + +```sh +ln -sf libhello.so.1 libhello.so +``` + +Also, one thing about dynamically linked applications like this one, is that they need to know where to look for its shared object dependencies, so we need to let CPython know that we hold libhello in this directory, so: + +``` sh +export LD_LIBRARY_PATH=$(pwd):$LD_LIBRARY_PATH +``` + +That wasn't so bad and we're already done with prep work, all that's left it to build our C extension. +## Building the C Extension +``` sh +# If you want, you can set the environment variable cc, to compile with clang instead of gcc +python -m venv .interop_env +source .interop_env/bin/activate # activate.fish if you like fish +python setup.py install +``` +For cleanness sake, we make virtual environment here, but do note you don't have to if you don't want to. +You can also run `python setup.py build` and grab the output shared object from the `build/lib.*` directory.\ +Shared objects are simply importable in CPython (which is great!), so you can just start up an interpreter in the same directory as your new `demo.so` and get the same result. + +**Note -** after all is said and done, for prolonged use, you may want to move your shared library (lib hello) to somewhere permanent on your `LD_LIBRARY_PATH`, or add your desired directory to `LD_LIBRARY_PATH` in some way (e.g put it in your shell .rc). + +## Try it out! + +Now we can see our work by entering an interactive shell and calling our function! + +```py +❯ python -q +>>> import demo +>>> demo.call_roc(21) +'The number was 21, OH YEAH!!! 🤘🤘' +``` + +## Notes on implementation +The structure of python-interop is very similar to a C-Extension, in fact, it is one.\ +We have: +- [`PyModuleDef demoModule`](https://docs.python.org/3/c-api/module.html#c.PyModuleDef) struct which declares the `demo` python module, +- [`PyMethodDef DemoMethods`](https://docs.python.org/3/c-api/structures.html#c.PyMethodDef) struct which declares `demo`'s methods, +- [`PyMODINIT_FUNC PyInit_demo`](https://docs.python.org/3/extending/extending.html) which is `demo`’s initialization function, and of course, +- [`PyObject * call_roc`] which is our demo function! Getting args and returning our string to the interpreter. The Roc-Python bridge lives here, all the above are merely CPython boilerplate to wrap up a C implementation into a valid Python module. + +The first three are basically the backbone of any C-API extension.\ +In addition, a couple more functions and notes you may want to pay attention to: +- [`void roc_panic`] - When creating such interpreter-dependent code, it is reasonable to make the implementation of this function fire up an interpreter Exception (e.g `RuntimeError` or whatever suits). +- When I first came across another implementation, I was a bit confused about `extern void roc__mainForHost_1_exposed_generic`, so let me clarify - this is an external function, implemented by the application, that goes (on the application side-) by the name `mainForHost`. + +And one last thing - +- When writing such the C glue (here, `demo.c`), you need to pay attention to not only Python's reference counting, but also Roc's, so remember to wear seatbelts and decrement your ref counts. diff --git a/examples/python-interop/build.sh b/examples/python-interop/build.sh new file mode 100755 index 0000000000..b6fcaab081 --- /dev/null +++ b/examples/python-interop/build.sh @@ -0,0 +1,31 @@ +#!/bin/sh + +# https://vaneyckt.io/posts/safer_bash_scripts_with_set_euxo_pipefail/ +set -euxo pipefail + +# Could assume roc binary on path but this may be preferable +cargo build --release +../../target/release/roc build --lib + +# Neither the application nor python needs a .0, so we can just rename it +mv libhello.so.1.0 libhello.so.1 +# but one of which does expect plain libhello.so, so we symlink it +ln -sf libhello.so.1 libhello.so + +# For Python to find libhello, it needs it to be in a known library path, so we export +export LD_LIBRARY_PATH=$(pwd):$LD_LIBRARY_PATH + +# Optional. Flag to indicate CPython which compiler to use +export cc=clang + +# And we're done, now just pack it all together with python's build system +# setup.py will compile our demo.c to a shared library that depends on libhello. +# One of the nice things about CPython is that this demo shared library is simply importable in CPython +python -m venv .interop_env +source .interop_env/bin/activate +python setup.py install +echo "You may now enter your virtual environment. +In bash/zsh, run: source .interop_env/bin/activate +In fish, run: source .interop_env/bin/activate.fish +In csh/tcsh (really?), run: source .interop_env/bin/activate.csh +Then, try entring an interactive python shell, import demo and call demo.call_roc with your number of choice." diff --git a/examples/python-interop/demo.c b/examples/python-interop/demo.c new file mode 100644 index 0000000000..11c274f001 --- /dev/null +++ b/examples/python-interop/demo.c @@ -0,0 +1,261 @@ +#define PY_SSIZE_T_CLEAN +#include +#include +#include +#include +#include +#include +#include +#include +#include + +void *roc_alloc(size_t size, unsigned int alignment) +{ + return malloc(size); +} + +void *roc_realloc(void *ptr, size_t new_size, size_t old_size, + unsigned int alignment) +{ + return realloc(ptr, new_size); +} + +void roc_dealloc(void *ptr, unsigned int alignment) { free(ptr); } + +__attribute__((noreturn)) void roc_panic(void *ptr, unsigned int alignment) +{ + PyErr_SetString(PyExc_RuntimeError, (char *)ptr); +} + +void roc_dbg(char* loc, char* msg) { + fprintf(stderr, "[%s] %s\n", loc, msg); +} + +void *roc_memset(void *str, int c, size_t n) { return memset(str, c, n); } + +// Reference counting + +// If the refcount is set to this, that means the allocation is +// stored in readonly memory in the binary, and we must not +// attempt to increment or decrement it; if we do, we'll segfault! +const ssize_t REFCOUNT_READONLY = 0; +const ssize_t REFCOUNT_ONE = (ssize_t)PTRDIFF_MIN; +const size_t MASK = (size_t)PTRDIFF_MIN; + +// Increment reference count, given a pointer to the first element in a collection. +// We don't need to check for overflow because in order to overflow a usize worth of refcounts, +// you'd need to somehow have more pointers in memory than the OS's virtual address space can hold. +void incref(uint8_t* bytes, uint32_t alignment) +{ + ssize_t *refcount_ptr = ((ssize_t *)bytes) - 1; + ssize_t refcount = *refcount_ptr; + + if (refcount != REFCOUNT_READONLY) { + *refcount_ptr = refcount + 1; + } +} + +// Decrement reference count, given a pointer to the first element in a collection. +// Then call roc_dealloc if nothing is referencing this collection anymore. +void decref(uint8_t* bytes, uint32_t alignment) +{ + if (bytes == NULL) { + return; + } + + size_t extra_bytes = (sizeof(size_t) >= (size_t)alignment) ? sizeof(size_t) : (size_t)alignment; + ssize_t *refcount_ptr = ((ssize_t *)bytes) - 1; + ssize_t refcount = *refcount_ptr; + + if (refcount != REFCOUNT_READONLY) { + *refcount_ptr = refcount - 1; + + if (refcount == REFCOUNT_ONE) { + void *original_allocation = (void *)(refcount_ptr - (extra_bytes - sizeof(size_t))); + + roc_dealloc(original_allocation, alignment); + } + } +} + +// RocBytes (List U8) + +struct RocBytes +{ + uint8_t *bytes; + size_t len; + size_t capacity; +}; + +struct RocBytes init_rocbytes(uint8_t *bytes, size_t len) +{ + if (len == 0) + { + struct RocBytes ret = { + .len = 0, + .bytes = NULL, + .capacity = 0, + }; + + return ret; + } + else + { + struct RocBytes ret; + size_t refcount_size = sizeof(size_t); + uint8_t *new_content = ((uint8_t *)roc_alloc(len + refcount_size, alignof(size_t))) + refcount_size; + + memcpy(new_content, bytes, len); + + ret.bytes = new_content; + ret.len = len; + ret.capacity = len; + + return ret; + } +} + +// RocStr + +struct RocStr +{ + uint8_t *bytes; + size_t len; + size_t capacity; +}; + +struct RocStr init_rocstr(uint8_t *bytes, size_t len) +{ + if (len == 0) + { + struct RocStr ret = { + .len = 0, + .bytes = NULL, + .capacity = MASK, + }; + + return ret; + } + else if (len < sizeof(struct RocStr)) + { + // Start out with zeroed memory, so that + // if we end up comparing two small RocStr values + // for equality, we won't risk memory garbage resulting + // in two equal strings appearing unequal. + struct RocStr ret = { + .len = 0, + .bytes = NULL, + .capacity = MASK, + }; + + // Copy the bytes into the stack allocation + memcpy(&ret, bytes, len); + + // Record the string's length in the last byte of the stack allocation + ((uint8_t *)&ret)[sizeof(struct RocStr) - 1] = (uint8_t)len | 0b10000000; + + return ret; + } + else + { + // A large RocStr is the same as a List U8 (aka RocBytes) in memory. + struct RocBytes roc_bytes = init_rocbytes(bytes, len); + + struct RocStr ret = { + .len = roc_bytes.len, + .bytes = roc_bytes.bytes, + .capacity = roc_bytes.capacity, + }; + + return ret; + } +} + +bool is_small_str(struct RocStr str) { return ((ssize_t)str.capacity) < 0; } + +// Determine the length of the string, taking into +// account the small string optimization +size_t roc_str_len(struct RocStr str) +{ + uint8_t *bytes = (uint8_t *)&str; + uint8_t last_byte = bytes[sizeof(str) - 1]; + uint8_t last_byte_xored = last_byte ^ 0b10000000; + size_t small_len = (size_t)(last_byte_xored); + size_t big_len = str.len; + + // Avoid branch misprediction costs by always + // determining both small_len and big_len, + // so this compiles to a cmov instruction. + if (is_small_str(str)) + { + return small_len; + } + else + { + return big_len; + } +} + +extern void roc__mainForHost_1_exposed_generic(struct RocBytes *ret, struct RocBytes *arg); + +// Receive a value from Python, JSON serialized it and pass it to Roc as a List U8 +// (at which point the Roc platform will decode it and crash if it's invalid, +// which roc_panic will translate into a Python exception), then get some JSON back from Roc +// - also as a List U8 - and have Python JSON.parse it into a plain Python value to return. +PyObject * call_roc(PyObject *self, PyObject *args) +{ + int num; + + if(!PyArg_ParseTuple(args, "i", &num)) { + return NULL; + } + + char str_num[256] = {0}; + sprintf(str_num, "%d", num); + + // can also be done with python objects but im not sure what would be the benefit here + // PyObject* py_num_str = PyUnicode_FromFormat("%d", num); + // const char* c_str = PyUnicode_AsUTF8(py_num_str); + // size_t length = (size_t *)PyUnicode_GetLength(py_num_str); + // ...init_rocbytes((uint8_t *)c_str, length); + + // Turn the given Python number into a JSON string. + struct RocBytes arg = init_rocbytes((uint8_t *)str_num, strlen(str_num)); + struct RocBytes ret; + + // Call the Roc function to populate `ret`'s bytes. + roc__mainForHost_1_exposed_generic(&ret, &arg); + + // Create a Python string from the heap-allocated JSON bytes the Roc function returned. + PyObject* json_bytes = PyUnicode_FromStringAndSize((char*)ret.bytes, ret.len); + PyObject* json_module = PyImport_ImportModule("json"); + PyObject* loads_func = PyObject_GetAttrString(json_module, "loads"); + PyObject *loads_args = PyTuple_Pack(1, json_bytes); + PyObject* py_obj = PyObject_CallObject(loads_func, loads_args); + Py_XDECREF(loads_args); + Py_XDECREF(loads_func); + Py_XDECREF(json_module); + Py_XDECREF(json_bytes); + + // Now that we've created py_str, we're no longer referencing the RocBytes. + decref((void *)&ret, alignof(uint8_t *)); + + return py_obj; +} + +static PyMethodDef DemoMethods[] = { + {"call_roc", call_roc, METH_VARARGS, "Calls a Roc function with a number, returns a string interpolated with the number"}, + {NULL, NULL, 0, NULL} +}; + +static struct PyModuleDef demoModule = { + PyModuleDef_HEAD_INIT, + "call_roc", + "demo roc call", + -1, + DemoMethods +}; + +PyMODINIT_FUNC PyInit_demo(void) { + return PyModule_Create(&demoModule); +} diff --git a/examples/python-interop/main.roc b/examples/python-interop/main.roc new file mode 100644 index 0000000000..9b9d316dca --- /dev/null +++ b/examples/python-interop/main.roc @@ -0,0 +1,13 @@ +app "libhello" + packages { pf: "platform/main.roc" } + imports [] + provides [main] to pf + +main : U64 -> Str +main = \num -> + if num == 0 then + "I need a positive number here!" + else + str = Num.toStr num + + "The number was \(str), OH YEAH!!! 🤘🤘" diff --git a/examples/python-interop/platform/host.c b/examples/python-interop/platform/host.c new file mode 100644 index 0000000000..7560e3d5fd --- /dev/null +++ b/examples/python-interop/platform/host.c @@ -0,0 +1,115 @@ +#include +#include +#include +#include +#include +#include +#include // shm_open +#include // for mmap +#include // for kill + +void* roc_alloc(size_t size, unsigned int alignment) { return malloc(size); } + +void* roc_realloc(void* ptr, size_t new_size, size_t old_size, + unsigned int alignment) { + return realloc(ptr, new_size); +} + +void roc_dealloc(void* ptr, unsigned int alignment) { free(ptr); } + +void roc_panic(void* ptr, unsigned int alignment) { + char* msg = (char*)ptr; + fprintf(stderr, + "Application crashed with message\n\n %s\n\nShutting down\n", msg); + exit(1); +} + +void roc_dbg(char* loc, char* msg) { + fprintf(stderr, "[%s] %s\n", loc, msg); +} + +void* roc_memset(void* str, int c, size_t n) { return memset(str, c, n); } + +int roc_shm_open(char* name, int oflag, int mode) { +#ifdef _WIN32 + return 0; +#else + return shm_open(name, oflag, mode); +#endif +} +void* roc_mmap(void* addr, int length, int prot, int flags, int fd, int offset) { +#ifdef _WIN32 + return addr; +#else + return mmap(addr, length, prot, flags, fd, offset); +#endif +} + +int roc_getppid() { +#ifdef _WIN32 + return 0; +#else + return getppid(); +#endif +} + +struct RocStr { + char* bytes; + size_t len; + size_t capacity; +}; + +bool is_small_str(struct RocStr str) { return ((ssize_t)str.capacity) < 0; } + +// Determine the length of the string, taking into +// account the small string optimization +size_t roc_str_len(struct RocStr str) { + char* bytes = (char*)&str; + char last_byte = bytes[sizeof(str) - 1]; + char last_byte_xored = last_byte ^ 0b10000000; + size_t small_len = (size_t)(last_byte_xored); + size_t big_len = str.len; + + // Avoid branch misprediction costs by always + // determining both small_len and big_len, + // so this compiles to a cmov instruction. + if (is_small_str(str)) { + return small_len; + } else { + return big_len; + } +} + +extern void roc__mainForHost_1_exposed_generic(struct RocStr *string); + +int main() { + + struct RocStr str; + roc__mainForHost_1_exposed_generic(&str); + + // Determine str_len and the str_bytes pointer, + // taking into account the small string optimization. + size_t str_len = roc_str_len(str); + char* str_bytes; + + if (is_small_str(str)) { + str_bytes = (char*)&str; + } else { + str_bytes = str.bytes; + } + + // Write to stdout + if (write(1, str_bytes, str_len) >= 0) { + // Writing succeeded! + + // NOTE: the string is a static string, read from in the binary + // if you make it a heap-allocated string, it'll be leaked here + return 0; + } else { + printf("Error writing to stdout: %s\n", strerror(errno)); + + // NOTE: the string is a static string, read from in the binary + // if you make it a heap-allocated string, it'll be leaked here + return 1; + } +} diff --git a/examples/python-interop/platform/main.roc b/examples/python-interop/platform/main.roc new file mode 100644 index 0000000000..ffcf7cbfff --- /dev/null +++ b/examples/python-interop/platform/main.roc @@ -0,0 +1,12 @@ +platform "python-interop" + requires {} { main : arg -> ret where arg implements Decoding, ret implements Encoding } + exposes [] + packages {} + imports [TotallyNotJson] + provides [mainForHost] + +mainForHost : List U8 -> List U8 +mainForHost = \json -> + when Decode.fromBytes json TotallyNotJson.json is + Ok arg -> Encode.toBytes (main arg) TotallyNotJson.json + Err _ -> [] diff --git a/examples/python-interop/setup.py b/examples/python-interop/setup.py new file mode 100644 index 0000000000..d825c8414b --- /dev/null +++ b/examples/python-interop/setup.py @@ -0,0 +1,12 @@ +import os +from setuptools import setup, Extension + +def main(): + setup(name="demo", + description="Demo testing Roc function calls from Python", + ext_modules=[ + Extension("demo", sources=["demo.c"], + libraries=["hello"], library_dirs=[os.getcwd()])]) + +if __name__ == "__main__": + main() diff --git a/examples/ruby-interop/.gitignore b/examples/ruby-interop/.gitignore new file mode 100644 index 0000000000..11621592d6 --- /dev/null +++ b/examples/ruby-interop/.gitignore @@ -0,0 +1,9 @@ +libhello +libhello.o +libhello.dylib +libhello.so +libhello.so.* +*.log +Makefile +*.bundle +extconf.h diff --git a/examples/ruby-interop/README.md b/examples/ruby-interop/README.md new file mode 100644 index 0000000000..134e879f34 --- /dev/null +++ b/examples/ruby-interop/README.md @@ -0,0 +1,71 @@ +# Ruby Interop + +This is an example of calling Roc code from [Ruby](https://www.ruby-lang.org). + +## Installation + +To run this example, you will need these to be installed already (in addition to Roc): + +- [`ruby`](https://www.ruby-lang.org/en/downloads) version 2.7.6 or later +- [`clang`](https://clang.llvm.org/) version 11.0.0 or later +- [`make`](https://www.gnu.org/software/make/) version 4.0 or later + +Make sure you have the right versions of Ruby and Clang especially! This example won't work with earlier versions. + +## Building the Roc library + +First, `cd` into this directory and run this in your terminal: + +```sh +roc build --lib libhello.roc +``` + +This compiles your Roc code into a binary library in the current directory. The library's filename will be `libhello` plus an OS-specific extension (e.g. `libhello.dylib` on macOS). + +## Generating the Makefile + +Next, run this: (remember that you need Ruby 2.7.6 or higher - otherwise later steps will fail!) + +```sh +ruby extconf.rb +``` + +This generates a `Makefile`. (There are only two Roc-specific lines in `extconf.rb`; they are both commented.) You only need to do this step once; now that you have the `Makefile`, you can use it along with `roc build` to rebuild from now. + +## Building the Ruby Library from the Roc Library + +Finally, run this: + +```sh +make +``` + +This uses the `Makefile` generated earlier to take the compiled Roc library and combine it with `demo.c` to generate a Ruby library. + +## Try it out! + +You can now try this out in Ruby's REPL (`irb`), like so: + +```sh +$ irb +irb(main):001:0> require_relative 'demo' +=> true +irb(main):002:0> RocApp::call_roc 42 +=> "The number was 42, OH YEAH!!! 🤘🤘" +``` + +## Rebuilding after Changes + +To rebuild after changing either the `demo.c` file or any `.roc` files, run: + +```sh +roc build --lib && make -B +``` + +The `-B` flag is necessary when you only change .roc files, because otherwise `make` thinks there's no work to do and doesn't bother rebuilding. + +## About this example + +This was created by following a [tutorial on Ruby C extensions](https://silverhammermba.github.io/emberb/c/) and [some documentation](https://github.com/ruby/ruby/blob/master/doc/extension.rdoc#label-Prepare+extconf.rb) (along with [more nicely formatted, but potentially out-of-date docs](https://docs.ruby-lang.org/en/2.4.0/extension_rdoc.html)). + +The [mkmf](https://ruby-doc.org/stdlib-2.5.1/libdoc/mkmf/rdoc/MakeMakefile.html) ("make makefile") method in `extconf.rb` generates a Makefile, which can then be run (using `make` with no arguments) to generate a compiled library that Ruby knows how to import via `require` or `require_relative`. diff --git a/examples/ruby-interop/demo.c b/examples/ruby-interop/demo.c new file mode 100644 index 0000000000..068d8c5daa --- /dev/null +++ b/examples/ruby-interop/demo.c @@ -0,0 +1,229 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include "extconf.h" + +void *roc_alloc(size_t size, unsigned int alignment) { return malloc(size); } + +void *roc_realloc(void *ptr, size_t new_size, size_t old_size, + unsigned int alignment) +{ + return realloc(ptr, new_size); +} + +void roc_dealloc(void *ptr, unsigned int alignment) { free(ptr); } + +__attribute__((noreturn)) void roc_panic(void *ptr, unsigned int alignment) +{ + rb_raise(rb_eException, "%s", (char *)ptr); +} + +void roc_dbg(char* loc, char* msg) { + fprintf(stderr, "[%s] %s\n", loc, msg); +} + +void *roc_memset(void *str, int c, size_t n) { return memset(str, c, n); } + +// Reference counting + +// If the refcount is set to this, that means the allocation is +// stored in readonly memory in the binary, and we must not +// attempt to increment or decrement it; if we do, we'll segfault! +const ssize_t REFCOUNT_READONLY = 0; +const ssize_t REFCOUNT_ONE = (ssize_t)PTRDIFF_MIN; +const size_t MASK = (size_t)PTRDIFF_MIN; + +// Increment reference count, given a pointer to the first element in a collection. +// We don't need to check for overflow because in order to overflow a usize worth of refcounts, +// you'd need to somehow have more pointers in memory than the OS's virtual address space can hold. +void incref(uint8_t* bytes, uint32_t alignment) +{ + ssize_t *refcount_ptr = ((ssize_t *)bytes) - 1; + ssize_t refcount = *refcount_ptr; + + if (refcount != REFCOUNT_READONLY) { + *refcount_ptr = refcount + 1; + } +} + +// Decrement reference count, given a pointer to the first element in a collection. +// Then call roc_dealloc if nothing is referencing this collection anymore. +void decref(uint8_t* bytes, uint32_t alignment) +{ + if (bytes == NULL) { + return; + } + + size_t extra_bytes = (sizeof(size_t) >= (size_t)alignment) ? sizeof(size_t) : (size_t)alignment; + ssize_t *refcount_ptr = ((ssize_t *)bytes) - 1; + ssize_t refcount = *refcount_ptr; + + if (refcount != REFCOUNT_READONLY) { + *refcount_ptr = refcount - 1; + + if (refcount == REFCOUNT_ONE) { + void *original_allocation = (void *)(refcount_ptr - (extra_bytes - sizeof(size_t))); + + roc_dealloc(original_allocation, alignment); + } + } +} + +// RocBytes (List U8) + +struct RocBytes +{ + uint8_t *bytes; + size_t len; + size_t capacity; +}; + +struct RocBytes init_rocbytes(uint8_t *bytes, size_t len) +{ + if (len == 0) + { + struct RocBytes ret = { + .len = 0, + .bytes = NULL, + .capacity = 0, + }; + + return ret; + } + else + { + struct RocBytes ret; + size_t refcount_size = sizeof(size_t); + uint8_t *new_content = (uint8_t *)roc_alloc(len + refcount_size, alignof(size_t)) + refcount_size; + + memcpy(new_content, bytes, len); + + ret.bytes = new_content; + ret.len = len; + ret.capacity = len; + + return ret; + } +} + +// RocStr + +struct RocStr +{ + uint8_t *bytes; + size_t len; + size_t capacity; +}; + +struct RocStr init_rocstr(uint8_t *bytes, size_t len) +{ + if (len == 0) + { + struct RocStr ret = { + .len = 0, + .bytes = NULL, + .capacity = MASK, + }; + + return ret; + } + else if (len < sizeof(struct RocStr)) + { + // Start out with zeroed memory, so that + // if we end up comparing two small RocStr values + // for equality, we won't risk memory garbage resulting + // in two equal strings appearing unequal. + struct RocStr ret = { + .len = 0, + .bytes = NULL, + .capacity = MASK, + }; + + // Copy the bytes into the stack allocation + memcpy(&ret, bytes, len); + + // Record the string's length in the last byte of the stack allocation + ((uint8_t *)&ret)[sizeof(struct RocStr) - 1] = (uint8_t)len | 0b10000000; + + return ret; + } + else + { + // A large RocStr is the same as a List U8 (aka RocBytes) in memory. + struct RocBytes roc_bytes = init_rocbytes(bytes, len); + + struct RocStr ret = { + .len = roc_bytes.len, + .bytes = roc_bytes.bytes, + .capacity = roc_bytes.capacity, + }; + + return ret; + } +} + +bool is_small_str(struct RocStr str) { return ((ssize_t)str.capacity) < 0; } + +// Determine the length of the string, taking into +// account the small string optimization +size_t roc_str_len(struct RocStr str) +{ + uint8_t *bytes = (uint8_t *)&str; + uint8_t last_byte = bytes[sizeof(str) - 1]; + uint8_t last_byte_xored = last_byte ^ 0b10000000; + size_t small_len = (size_t)(last_byte_xored); + size_t big_len = str.len; + + // Avoid branch misprediction costs by always + // determining both small_len and big_len, + // so this compiles to a cmov instruction. + if (is_small_str(str)) + { + return small_len; + } + else + { + return big_len; + } +} + +extern void roc__mainForHost_1_exposed_generic(struct RocBytes *ret, struct RocBytes *arg); + +// Receive a value from Ruby, JSON serialized it and pass it to Roc as a List U8 +// (at which point the Roc platform will decode it and crash if it's invalid, +// which roc_panic will translate into a Ruby exception), then get some JSON back from Roc +// - also as a List U8 - and have Ruby JSON.parse it into a plain Ruby value to return. +VALUE call_roc(VALUE self, VALUE rb_arg) +{ + // This must be required before the to_json method will exist on String. + rb_require("json"); + + // Turn the given Ruby value into a JSON string. + // TODO should we defensively encode it as UTF-8 first? + VALUE json_arg = rb_funcall(rb_arg, rb_intern("to_json"), 0); + + struct RocBytes arg = init_rocbytes((uint8_t *)RSTRING_PTR(json_arg), RSTRING_LEN(json_arg)); + struct RocBytes ret; + + // Call the Roc function to populate `ret`'s bytes. + roc__mainForHost_1_exposed_generic(&ret, &arg); + + // Create a rb_utf8_str from the heap-allocated JSON bytes the Roc function returned. + VALUE returned_json = rb_utf8_str_new((char *)ret.bytes, ret.len); + + // Now that we've created our Ruby JSON string, we're no longer referencing the RocBytes. + decref((void *)&ret, alignof(uint8_t *)); + + return rb_funcall(rb_define_module("JSON"), rb_intern("parse"), 1, returned_json); +} + +void Init_demo() +{ + VALUE roc_app = rb_define_module("RocApp"); + rb_define_module_function(roc_app, "call_roc", &call_roc, 1); +} diff --git a/examples/ruby-interop/extconf.rb b/examples/ruby-interop/extconf.rb new file mode 100644 index 0000000000..1af49ae6b6 --- /dev/null +++ b/examples/ruby-interop/extconf.rb @@ -0,0 +1,11 @@ +#!/usr/bin/env ruby +require 'mkmf' + +# preparation for compilation goes here +dir_config('') # include the current directory in the library search path +have_library('hello') # depend on `libhello.dylib` being in the current path + # (.dylib is macOS-specific; other OSes would have different extensions) + +create_header +create_makefile 'demo' + diff --git a/examples/ruby-interop/libhello.roc b/examples/ruby-interop/libhello.roc new file mode 100644 index 0000000000..9b9d316dca --- /dev/null +++ b/examples/ruby-interop/libhello.roc @@ -0,0 +1,13 @@ +app "libhello" + packages { pf: "platform/main.roc" } + imports [] + provides [main] to pf + +main : U64 -> Str +main = \num -> + if num == 0 then + "I need a positive number here!" + else + str = Num.toStr num + + "The number was \(str), OH YEAH!!! 🤘🤘" diff --git a/examples/ruby-interop/platform/host.c b/examples/ruby-interop/platform/host.c new file mode 100644 index 0000000000..1d7b681dbb --- /dev/null +++ b/examples/ruby-interop/platform/host.c @@ -0,0 +1,119 @@ +#include +#include +#include +#include +#include +#include +#include // shm_open +#include // for mmap +#include // for kill + +void* roc_alloc(size_t size, unsigned int alignment) { return malloc(size); } + +void* roc_realloc(void* ptr, size_t new_size, size_t old_size, + unsigned int alignment) { + return realloc(ptr, new_size); +} + +void roc_dealloc(void* ptr, unsigned int alignment) { free(ptr); } + +void roc_panic(void* ptr, unsigned int alignment) { + char* msg = (char*)ptr; + fprintf(stderr, + "Application crashed with message\n\n %s\n\nShutting down\n", msg); + exit(1); +} + +void roc_dbg(char* loc, char* msg) { + fprintf(stderr, "[%s] %s\n", loc, msg); +} + +void* roc_memmove(void* dest, const void* src, size_t n){ + return memmove(dest, src, n); +} + +void* roc_memset(void* str, int c, size_t n) { return memset(str, c, n); } + +int roc_shm_open(char* name, int oflag, int mode) { +#ifdef _WIN32 + return 0; +#else + return shm_open(name, oflag, mode); +#endif +} +void* roc_mmap(void* addr, int length, int prot, int flags, int fd, int offset) { +#ifdef _WIN32 + return addr; +#else + return mmap(addr, length, prot, flags, fd, offset); +#endif +} + +int roc_getppid() { +#ifdef _WIN32 + return 0; +#else + return getppid(); +#endif +} + +struct RocStr { + char* bytes; + size_t len; + size_t capacity; +}; + +bool is_small_str(struct RocStr str) { return ((ssize_t)str.capacity) < 0; } + +// Determine the length of the string, taking into +// account the small string optimization +size_t roc_str_len(struct RocStr str) { + char* bytes = (char*)&str; + char last_byte = bytes[sizeof(str) - 1]; + char last_byte_xored = last_byte ^ 0b10000000; + size_t small_len = (size_t)(last_byte_xored); + size_t big_len = str.len; + + // Avoid branch misprediction costs by always + // determining both small_len and big_len, + // so this compiles to a cmov instruction. + if (is_small_str(str)) { + return small_len; + } else { + return big_len; + } +} + +extern void roc__mainForHost_1_exposed_generic(struct RocStr *string); + +int main() { + + struct RocStr str; + roc__mainForHost_1_exposed_generic(&str); + + // Determine str_len and the str_bytes pointer, + // taking into account the small string optimization. + size_t str_len = roc_str_len(str); + char* str_bytes; + + if (is_small_str(str)) { + str_bytes = (char*)&str; + } else { + str_bytes = str.bytes; + } + + // Write to stdout + if (write(1, str_bytes, str_len) >= 0) { + // Writing succeeded! + + // NOTE: the string is a static string, read from in the binary + // if you make it a heap-allocated string, it'll be leaked here + return 0; + } else { + printf("Error writing to stdout: %s\n", strerror(errno)); + + // NOTE: the string is a static string, read from in the binary + // if you make it a heap-allocated string, it'll be leaked here + return 1; + } +} diff --git a/examples/ruby-interop/platform/main.roc b/examples/ruby-interop/platform/main.roc new file mode 100644 index 0000000000..901c1a1298 --- /dev/null +++ b/examples/ruby-interop/platform/main.roc @@ -0,0 +1,12 @@ +platform "ruby-interop" + requires {} { main : arg -> ret where arg implements Decoding, ret implements Encoding } + exposes [] + packages {} + imports [TotallyNotJson] + provides [mainForHost] + +mainForHost : List U8 -> List U8 +mainForHost = \json -> + when Decode.fromBytes json TotallyNotJson.json is + Ok arg -> Encode.toBytes (main arg) TotallyNotJson.json + Err _ -> [] # TODO panic so that Ruby raises an exception diff --git a/examples/static-site-gen/.gitignore b/examples/static-site-gen/.gitignore new file mode 100644 index 0000000000..fdb13757e3 --- /dev/null +++ b/examples/static-site-gen/.gitignore @@ -0,0 +1,4 @@ +/output/*.html +/output/subFolder +/static-site +/bin diff --git a/examples/static-site-gen/README.md b/examples/static-site-gen/README.md new file mode 100644 index 0000000000..f8dd72ab56 --- /dev/null +++ b/examples/static-site-gen/README.md @@ -0,0 +1,22 @@ +# Static site generator + +This is an example of how you might build a static site generator using Roc. +It searches for Markdown (`.md`) files in the `input` directory, inserts them +into a HTML template defined in Roc, and writes the result into the +corresponding file path in the `output` directory. + +To run, `cd` into this directory and run this in your terminal: + +If `roc` is on your PATH: +```bash +roc run static-site.roc -- input/ output/ +``` + +If not, and you're building Roc from source: +``` +cargo run -- static-site.roc -- input/ output/ +``` + +The example in the `input` directory is a copy of the 2004 website +by John Gruber, introducing the Markdown format. +https://daringfireball.net/projects/markdown/ diff --git a/examples/static-site-gen/input/banana.md b/examples/static-site-gen/input/banana.md new file mode 100644 index 0000000000..767b6f6e27 --- /dev/null +++ b/examples/static-site-gen/input/banana.md @@ -0,0 +1,50 @@ +# Ira oculi amisere exsereret repetam corpora + +## Superasque hanc horret + +Lorem markdownum praecessit *cesserat suam multos* quam. Est Ulixis inter +fabricataque auxilioque fossae et *gravidis arbore*, una verum steterat conamina +videre negetur, ista. Isque nubibus et et iam postquam, madefacta cura lugenti +longique somnoque. Resurgere ceu [magnus et](http://example.org) Acheloe, Iovis +fatifero alias. In suo Venus nec fixerat Hercule quisve et coronat tauros sol +Erinys arces; postquam e natus, ille annis? + +Hoc sedes. Ait ait! Clarum excidit collocat, inpluit lacrimabile +[herbis](http://example.org), et **quod** quid nutricis auctor pompa: anguigenae +quo at trahit dum? Me sponsa confundere orant fecit tendentem flamina veteris; +ut. Dabat fecit. + +1. Ait est cauda noctisque scrobibus implesset vultu +2. Philomela metu +3. Partim inmemor +4. Lascivitque vietum potens +5. Addidit Troica tellusAndros induit rectum suos Aulidaque + +## Nece ramos comites dierum + +Et non miseranda eadem ferrum, mersaeque fossa lacrimam Dromas, tempore me iam +Perseus valido. Vinaque viderat habet curaque caelo sine *speluncae est* ignarus +Teucras quantum. Tanta timet auxilioque +[praecordia](http://example.org) unum: legi pondus nacta, est +est agnus erat solantia tenet? + +Scire virgo felix grandior haut. Montibus illi: vir prior, indutus est contra +hos, semina saltuque: hoc radiis alter, intravit. Oravique sed ipsa +[cornu](http://example.org), flere unam suis vindicta spumigeroque +munere fertur figurae saecula mutentur. In suam corpus nare tumulos ignorat. + +## Fraudo Sibyllae mare + +Suspendit erat tincta artus tetigere caret, decoris meorum: parentis. Corporis +prius: que auras insequitur adversas; ab miserata dominam genitor. + + aiff_bin_operating /= bsodMetadata; + terahertz_alignment_system.excel_bash_hdtv = saas_thread_character; + var server = lteHypermediaRom(menu_wan_of.coreNewbieGraphic(imapSector + + download_cluster_rate)); + +Oculos gravitate clipeum ab quid dubiaque apertum celerem vidit caput sine dant +*adstitit*. Acerbo in *bona verba* tutus est ullus metas primus: suo forte tuam +quaterque vincant Minos, [adverso](http://example.org). + +Longa velut pietate. Erat felix avis Erinyn? diff --git a/examples/static-site-gen/input/cherry.md b/examples/static-site-gen/input/cherry.md new file mode 100644 index 0000000000..2ae7efbb99 --- /dev/null +++ b/examples/static-site-gen/input/cherry.md @@ -0,0 +1,45 @@ +# Pete milite aura cuius laeva tenuerunt exprimit + +## Semina pectora + +Lorem markdownum auguror domos, tum cuspide induit officio Troia Propoetidas +fila Cyllenius retinacula. Progenies pepercit visa ausa amantes. Disertum sub, +et corpora, si credere cumque est meritumque moram videri levare praeterit +criminis quidem petita *ferreus*. + +Achaidas traxit ad, iacuere iamdudum ab fecit: vestra evicere lacrimis labore: +gavisus forti turba. A vixque incesto et tanti paries, columnae est signis +munera? Grata Parcarum [Aetolius modo](http://example.org): meis quanto +amico oravit venit mihi vertit summam. + +## Terras ab neque moveri mater oculos me + +Qua solet grandia parente, femur *poenas exspectatas dedit* atros abstulit +sumpsere interdum adfusaque utrumque tostae. Regni et ostendit exanimes armis +nec armis lacesse fretumque silvarum fusca ante laevo suggerit matre, **non +caespes** urbes. + +- Dixit qui amantis modo +- Pennas ut iaculo vellet in nec intellegit +- Summe ire annis tribusque haberet suo veteres +- Pede parvoque Tyrioque unum +- Terra traxisse meum Iuppiter in amato imagine + +## Fraude stimulis + +[Templis quotiens](http://example.org) tectus tuo ignara orbi +Zanclen hunc comprenditur *momordi*. Armis nymphae. Vocant rupe omnia +cognosceret fine; per sonant, deque magnum, suo. + +- Quid lamina +- Gravidus pulchros sacris suis est audenti +- Sede quamvis orbata cerva plena tua Hymenaeus +- Nec Achaia deprensi baculo cessit nec mitius +- Fretoque in volucres repperit uda Ixionis etiamnum +- Auras Ceycis petit mundo ter sensi sacrum + +Nosti et spem; tum illa parentes doctas nuntia **laeva virusque**. Magistris +[diversi](http://example.org) lacerare at data margine abesset inscribit +calidumque **copia vidit** attulerat altera incensaque crista versus. Fecit et +posco series dextra. Lacrimis inducta imis non apertos saeva **ripas sumpserat** +pariter quoque aequales. diff --git a/examples/static-site-gen/input/subFolder/apple.md b/examples/static-site-gen/input/subFolder/apple.md new file mode 100644 index 0000000000..eb853b1edf --- /dev/null +++ b/examples/static-site-gen/input/subFolder/apple.md @@ -0,0 +1,65 @@ +# Resedit tenax Oleniae nitor trunco somnus recessit + +## Quam quemque ensem mea scitusque iactatis ego + +Lorem markdownum nubes, instruis virgo perque victa Lacinia parenti, foret ramis +incessit, frustra illa; adiecit. Agit **memor** ignes paternos exhalarunt +dumque. + +> Fuit dotatissima Aeson curaque **gradu** cum quemque plures, quaque pater. +> Hanc iuvet, sententia inamabile: dum mollitaque plectro habent tacuit tibi. + +## Thracum non quoque arisque Elinque et criminibusque + +Deductus mihi lacrimaeque Apolline strictique, sanguine discite facinus se. +Livor nulla Pallantias virtutis fluctus testantia inponis et tenuerunt clipeo +non sacra date *partes* quoque fugae; cum. Audax contingere moresque dis +requirere senserit spectare generosos formatum modo edidit abluere his infixum +inductae dis concipe nisi redeuntia. Lutulenta [sublimibus pudori +iussis](http://example.org) volucrem recumbis foresque, tecum mater sidus est +imagine. + +Et dea diebus me corpora nunc. Iam parte medium obscuraque **Acheloia** ad iubes +inferius altera nervus! Irasci pes facit Solem, credimus, est aqua regia terrae +mirarique spectabat, dixit. Tuta nimis Canache Quirini et fessa, de nodoso se +procul errant. + +## Neptune longi bis lacertis tempore conpellat est + +Habenas cur pinus; qui vera recurvam? Audieris ex modo Achilles est quamquam +Occupat, rettulit vindicat timentes. + +- Non parere clamare +- Effigiem humana date deum duri +- Urbe minus +- Rettulit illud geminam curva Amuli furta scinditur +- Secutum infelix promissi boum latebras tamen +- Fata se felix poterant ter spoliantis in + +## Quos quoque et posses sororum prodere + +Candore fatebar *avertite abest* et fuerunt Nile inania domum: quid citius +quoque; pulsabant virum tamen ferro quaerens Osiris. Aethera acui regis exstat +ingeniis in pugna quadripedis glandes superos. Tanta quam, illo es prole est +telis **unus verba** quisquis iuvenci annis. Nec velox sed sacra gaudia vacuos, +Herculei undae calcata inmeriti quercus ignes parabant iam. + +### Example Table + +| Tables | Are | Cool | +| :------------ | :-----------: | ----: | +| col 3 is | right-aligned | $1600 | +| col 2 is | centered | $12 | +| zebra stripes | are neat | $1 | + +### Example Code Blocks + +```sh +# This isn't fenced roc code so its not formatted +# Use a fence like ```roc to format code blocks +``` + +```roc +file:codeExample.roc +``` + diff --git a/examples/static-site-gen/input/subFolder/codeExample.roc b/examples/static-site-gen/input/subFolder/codeExample.roc new file mode 100644 index 0000000000..76633762c6 --- /dev/null +++ b/examples/static-site-gen/input/subFolder/codeExample.roc @@ -0,0 +1,71 @@ +## This is a documentation comment +# This is a comment +app "static-site" + packages { pf: "platform/main.roc" } + imports [ + pf.Html.{ html, head, body, div, text, a, ul, li, link, meta }, + pf.Html.Attributes.{ httpEquiv, content, href, rel, lang, class, title }, + ] + provides [transformFileContent] to pf + +NavLink : { + # this is another comment + url : Str, + title : Str, + text : Str, +} + +navLinks : List NavLink +navLinks = [ + { url: "apple.html", title: "Exempli Gratia Pagina Pomi", text: "Apple" }, + { url: "banana.html", title: "Exempli Gratia Pagina Musa", text: "Banana" }, + { url: "cherry.html", title: "Exempli Pagina Cerasus", text: "Cherry" }, +] + +transformFileContent : Str, Str -> Str +transformFileContent = \currentUrl, htmlContent -> + List.findFirst navLinks (\{ url } -> url == currentUrl) + |> Result.map (\currentNavLink -> view currentNavLink htmlContent) + |> Result.map Html.render + |> Result.withDefault "" + +view : NavLink, Str -> Html.Node +view = \currentNavLink, htmlContent -> + html [lang "en"] [ + head [] [ + meta [httpEquiv "content-type", content "text/html; charset=utf-8"] [], + Html.title [] [text currentNavLink.title], + link [rel "stylesheet", href "style.css"] [], + ], + body [] [ + div [class "main"] [ + div [class "navbar"] [ + viewNavbar currentNavLink, + ], + div [class "article"] [ + # For now `text` is not escaped so we can use it to insert HTML + # We'll probably want something more explicit in the long term though! + text htmlContent, + ], + ], + ], + ] + +viewNavbar : NavLink -> Html.Node +viewNavbar = \currentNavLink -> + ul + [] + (List.map navLinks \nl -> viewNavLink (nl == currentNavLink) nl) + +viewNavLink : Bool, NavLink -> Html.Node +viewNavLink = \isCurrent, navlink -> + if isCurrent then + li [class "nav-link nav-link--current"] [ + text navlink.text, + ] + else + li [class "nav-link"] [ + a + [href navlink.url, title navlink.title] + [text navlink.text], + ] diff --git a/examples/static-site-gen/output/style.css b/examples/static-site-gen/output/style.css new file mode 100644 index 0000000000..0e018a7f36 --- /dev/null +++ b/examples/static-site-gen/output/style.css @@ -0,0 +1,135 @@ +.main { + display: flex; + max-width: 900px; + margin: 0 auto; + color: #222; +} +.navbar { + padding: 32px; + font-family: Verdana, Geneva, Tahoma, sans-serif; +} +.navbar ul { + margin: 0; + padding: 0; +} +.nav-link { + list-style: none; + margin: 8px; + padding: 8px 16px; + background-color: lightgray; + font-size: large; +} +.nav-link--current { + background-color: lightblue; +} + +.article { + font-family: "Times New Roman", Times, serif; +} +.article h1, h2 { + font-family: Arial, Helvetica, sans-serif; + color: #444; +} +.article blockquote { + margin: 8px 0; + padding: 2px 8px; + border-radius: 8px; + background-color: #eee; + font-style: italic; + color: #444; +} +.article pre { + background-color: rgb(241, 241, 241); + color: rgb(27, 27, 27); + padding: 16px; +} + +pre { + white-space: pre-wrap; +} + +samp .ann { + /* type annotation - purple in the repl */ + color: #f384fd; +} + +samp .comment { + color: #338545; +} + +samp .kw { + color: #004cc2; +} + +samp .arrow, samp .backslash, samp .bar { + color: #0600c2; +} + +samp .pipe { + color: #0600c2; +} + +samp .op { + color: #0600c2; +} + +samp .assign { + color: #48fd00; +} + +samp .paren, samp .bracket, samp .brace { + color: #ff0000; +} + +samp .comma { + color: #ff00fb; +} + +samp .colon { + color: #9b0098; +} + +samp .number { +/* number literals */ +color: #9669ff; +} + +samp .str { + /* string literals */ + color: #1dbf00; +} + +samp .str-esc, samp .str-interp { + /* escapes inside string literals, e.g. \t */ + color: #3474db; +} + +samp .dim { + opacity: 0.55; +} + +samp .comment { + color: #005a13; +} + + +table { + table-layout: fixed; + width: 100%; + border-collapse: collapse; + border: 3px solid rgb(161, 64, 0); +} + +tbody tr:nth-child(even) { + background-color: #c6f4ff; +} + +th { + background-color: #ffabab; +} + +th, +td { + + padding: 2px; +} \ No newline at end of file diff --git a/examples/static-site-gen/platform/Cargo.toml b/examples/static-site-gen/platform/Cargo.toml new file mode 100644 index 0000000000..b85f497424 --- /dev/null +++ b/examples/static-site-gen/platform/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "host" +authors = ["The Roc Contributors"] +edition = "2021" +license = "UPL-1.0" +version = "0.0.1" + +links = "app" + +[lib] +name = "host" +path = "src/lib.rs" +crate-type = ["staticlib", "rlib"] + +[[bin]] +name = "host" +path = "src/main.rs" + +[dependencies] +libc = "0.2" +syntect = "5.0" +roc_highlight = { path = "../../../crates/highlight" } +roc_std = { path = "../../../crates/roc_std" } + + +# Default features include building a binary that we don't need +pulldown-cmark = { version = "0.9.2", default-features = false } + +[workspace] diff --git a/examples/static-site-gen/platform/Html.roc b/examples/static-site-gen/platform/Html.roc new file mode 100644 index 0000000000..dc206d5662 --- /dev/null +++ b/examples/static-site-gen/platform/Html.roc @@ -0,0 +1,376 @@ +interface Html + exposes [ + Node, + Attribute, + render, + renderWithoutDocType, + element, + unclosedElem, + text, + attribute, + html, + base, + head, + link, + meta, + style, + title, + body, + address, + article, + aside, + footer, + header, + h1, + h2, + h3, + h4, + h5, + h6, + main, + nav, + section, + blockquote, + dd, + div, + dl, + dt, + figcaption, + figure, + hr, + li, + menu, + ol, + p, + pre, + ul, + a, + abbr, + b, + bdi, + bdo, + br, + cite, + code, + data, + dfn, + em, + i, + kbd, + mark, + q, + rp, + rt, + ruby, + s, + samp, + small, + span, + strong, + sub, + sup, + time, + u, + var, + wbr, + area, + audio, + img, + map, + track, + video, + embed, + iframe, + object, + picture, + portal, + source, + svg, + math, + canvas, + noscript, + script, + del, + ins, + caption, + col, + colgroup, + table, + tbody, + td, + tfoot, + th, + thead, + tr, + button, + datalist, + fieldset, + form, + input, + label, + legend, + meter, + optgroup, + option, + output, + progress, + select, + textarea, + details, + dialog, + summary, + slot, + template, + ] + imports [Html.Attributes] + +Node : [ + Text Str, + Element Str Nat (List Attribute) (List Node), + UnclosedElem Str Nat (List Attribute), +] + +Attribute : Html.Attributes.Attribute + +attribute : Str -> (Str -> Attribute) +attribute = Html.Attributes.attribute + +text : Str -> Node +text = Text + +## Define a non-standard HTML Element +## +## You can use this to add elements that are not already supported. +## For example, you could bring back the obsolete element, +## and add some 90's nostalgia to your web page! +## +## blink : List Attribute, List Node -> Node +## blink = element "blink" +## +## html = blink [] [ text "This text is blinking!" ] +## +element : Str -> (List Attribute, List Node -> Node) +element = \tagName -> + \attrs, children -> + # While building the node tree, calculate the size of Str it will render to + withTag = 2 * (3 + Str.countUtf8Bytes tagName) + withAttrs = List.walk attrs withTag \acc, Attribute name val -> + acc + Str.countUtf8Bytes name + Str.countUtf8Bytes val + 4 + totalSize = List.walk children withAttrs \acc, child -> + acc + nodeSize child + + Element tagName totalSize attrs children + +unclosedElem : Str -> (List Attribute -> Node) +unclosedElem = \tagName -> + \attrs -> + # While building the node tree, calculate the size of Str it will render to + withTag = 2 * (3 + Str.countUtf8Bytes tagName) + totalSize = List.walk attrs withTag \acc, Attribute name val -> + acc + Str.countUtf8Bytes name + Str.countUtf8Bytes val + 4 + + UnclosedElem tagName totalSize attrs + +# internal helper +nodeSize : Node -> Nat +nodeSize = \node -> + when node is + Text content -> + Str.countUtf8Bytes content + + Element _ size _ _ | UnclosedElem _ size _ -> + size + +## Render a Node to an HTML string +## +## The output has no whitespace between nodes, to make it small. +## This is intended for generating full HTML documents, so it +## automatically adds `` to the start of the string. +## See also `renderWithoutDocType`. +render : Node -> Str +render = \node -> + buffer = Str.reserve "" (nodeSize node) + + renderHelp buffer node + +## Render a Node to a string, without a DOCTYPE tag +renderWithoutDocType : Node -> Str +renderWithoutDocType = \node -> + buffer = Str.reserve "" (nodeSize node) + + renderHelp buffer node + +# internal helper +renderHelp : Str, Node -> Str +renderHelp = \buffer, node -> + when node is + Text content -> + Str.concat buffer content + + Element tagName _ attrs children -> + withTagName = "\(buffer)<\(tagName)" + withAttrs = + if List.isEmpty attrs then + withTagName + else + List.walk attrs "\(withTagName) " renderAttr + withTag = Str.concat withAttrs ">" + withChildren = List.walk children withTag renderHelp + + "\(withChildren)" + + UnclosedElem tagName _ attrs -> + if List.isEmpty attrs then + "\(buffer)<\(tagName)>" + else + attrs + |> List.walk "\(buffer)<\(tagName) " renderAttr + |> Str.concat ">" + +# internal helper +renderAttr : Str, Attribute -> Str +renderAttr = \buffer, Attribute key val -> + "\(buffer) \(key)=\"\(val)\"" + +# Main root +html = element "html" + +# Document metadata +base = element "base" +head = element "head" +link = unclosedElem "link" +meta = unclosedElem "meta" +style = element "style" +title = element "title" + +# Sectioning root +body = element "body" + +# Content sectioning +address = element "address" +article = element "article" +aside = element "aside" +footer = element "footer" +header = element "header" +h1 = element "h1" +h2 = element "h2" +h3 = element "h3" +h4 = element "h4" +h5 = element "h5" +h6 = element "h6" +main = element "main" +nav = element "nav" +section = element "section" + +# Text content +blockquote = element "blockquote" +dd = element "dd" +div = element "div" +dl = element "dl" +dt = element "dt" +figcaption = element "figcaption" +figure = element "figure" +hr = element "hr" +li = element "li" +menu = element "menu" +ol = element "ol" +p = element "p" +pre = element "pre" +ul = element "ul" + +# Inline text semantics +a = element "a" +abbr = element "abbr" +b = element "b" +bdi = element "bdi" +bdo = element "bdo" +br = element "br" +cite = element "cite" +code = element "code" +data = element "data" +dfn = element "dfn" +em = element "em" +i = element "i" +kbd = element "kbd" +mark = element "mark" +q = element "q" +rp = element "rp" +rt = element "rt" +ruby = element "ruby" +s = element "s" +samp = element "samp" +small = element "small" +span = element "span" +strong = element "strong" +sub = element "sub" +sup = element "sup" +time = element "time" +u = element "u" +var = element "var" +wbr = element "wbr" + +# Image and multimedia +area = element "area" +audio = element "audio" +img = unclosedElem "img" +map = element "map" +track = element "track" +video = element "video" + +# Embedded content +embed = element "embed" +iframe = element "iframe" +object = element "object" +picture = element "picture" +portal = element "portal" +source = element "source" + +# SVG and MathML +svg = element "svg" +math = element "math" + +# Scripting +canvas = element "canvas" +noscript = element "noscript" +script = element "script" + +# Demarcating edits +del = element "del" +ins = element "ins" + +# Table content +caption = element "caption" +col = element "col" +colgroup = element "colgroup" +table = element "table" +tbody = element "tbody" +td = element "td" +tfoot = element "tfoot" +th = element "th" +thead = element "thead" +tr = element "tr" + +# Forms +button = element "button" +datalist = element "datalist" +fieldset = element "fieldset" +form = element "form" +input = element "input" +label = element "label" +legend = element "legend" +meter = element "meter" +optgroup = element "optgroup" +option = element "option" +output = element "output" +progress = element "progress" +select = element "select" +textarea = element "textarea" + +# Interactive elements +details = element "details" +dialog = element "dialog" +summary = element "summary" + +# Web Components +slot = element "slot" +template = element "template" diff --git a/examples/static-site-gen/platform/Html/Attributes.roc b/examples/static-site-gen/platform/Html/Attributes.roc new file mode 100644 index 0000000000..5a0ea68cd3 --- /dev/null +++ b/examples/static-site-gen/platform/Html/Attributes.roc @@ -0,0 +1,283 @@ +interface Html.Attributes + exposes [ + Attribute, + attribute, + accept, + acceptCharset, + accesskey, + action, + align, + allow, + alt, + ariaLabel, + ariaLabelledBy, + ariaHidden, + async, + autocapitalize, + autocomplete, + autofocus, + autoplay, + background, + bgcolor, + border, + buffered, + capture, + challenge, + charset, + checked, + cite, + class, + code, + codebase, + color, + cols, + colspan, + content, + contenteditable, + contextmenu, + controls, + coords, + crossorigin, + csp, + data, + dataAttr, + datetime, + decoding, + default, + defer, + dir, + dirname, + disabled, + download, + draggable, + enctype, + enterkeyhint, + for, + form, + formaction, + formenctype, + formmethod, + formnovalidate, + formtarget, + headers, + height, + hidden, + high, + href, + hreflang, + httpEquiv, + icon, + id, + importance, + integrity, + intrinsicsize, + inputmode, + ismap, + itemprop, + keytype, + kind, + label, + lang, + language, + loading, + list, + loop, + low, + manifest, + max, + maxlength, + minlength, + media, + method, + min, + multiple, + muted, + name, + novalidate, + open, + optimum, + pattern, + ping, + placeholder, + poster, + preload, + radiogroup, + readonly, + referrerpolicy, + rel, + required, + reversed, + role, + rows, + rowspan, + sandbox, + scope, + scoped, + selected, + shape, + size, + sizes, + slot, + span, + spellcheck, + src, + srcdoc, + srclang, + srcset, + start, + step, + style, + summary, + tabindex, + target, + title, + translate, + type, + usemap, + value, + width, + wrap, + ] + imports [] + +Attribute : [Attribute Str Str] + +attribute : Str -> (Str -> Attribute) +attribute = \attrName -> + \attrValue -> Attribute attrName attrValue + +accept = attribute "accept" +acceptCharset = attribute "accept-charset" +accesskey = attribute "accesskey" +action = attribute "action" +align = attribute "align" +allow = attribute "allow" +alt = attribute "alt" +ariaLabel = attribute "aria-label" +ariaLabelledBy = attribute "aria-labelledby" +ariaHidden = attribute "aria-label" +async = attribute "async" +autocapitalize = attribute "autocapitalize" +autocomplete = attribute "autocomplete" +autofocus = attribute "autofocus" +autoplay = attribute "autoplay" +background = attribute "background" +bgcolor = attribute "bgcolor" +border = attribute "border" +buffered = attribute "buffered" +capture = attribute "capture" +challenge = attribute "challenge" +charset = attribute "charset" +checked = attribute "checked" +cite = attribute "cite" +class = attribute "class" +code = attribute "code" +codebase = attribute "codebase" +color = attribute "color" +cols = attribute "cols" +colspan = attribute "colspan" +content = attribute "content" +contenteditable = attribute "contenteditable" +contextmenu = attribute "contextmenu" +controls = attribute "controls" +coords = attribute "coords" +crossorigin = attribute "crossorigin" +csp = attribute "csp" +data = attribute "data" +dataAttr = \dataName, dataVal -> Attribute "data-\(dataName)" dataVal +datetime = attribute "datetime" +decoding = attribute "decoding" +default = attribute "default" +defer = attribute "defer" +dir = attribute "dir" +dirname = attribute "dirname" +disabled = attribute "disabled" +download = attribute "download" +draggable = attribute "draggable" +enctype = attribute "enctype" +enterkeyhint = attribute "enterkeyhint" +for = attribute "for" +form = attribute "form" +formaction = attribute "formaction" +formenctype = attribute "formenctype" +formmethod = attribute "formmethod" +formnovalidate = attribute "formnovalidate" +formtarget = attribute "formtarget" +headers = attribute "headers" +height = attribute "height" +hidden = attribute "hidden" +high = attribute "high" +href = attribute "href" +hreflang = attribute "hreflang" +httpEquiv = attribute "http-equiv" +icon = attribute "icon" +id = attribute "id" +importance = attribute "importance" +integrity = attribute "integrity" +intrinsicsize = attribute "intrinsicsize" +inputmode = attribute "inputmode" +ismap = attribute "ismap" +itemprop = attribute "itemprop" +keytype = attribute "keytype" +kind = attribute "kind" +label = attribute "label" +lang = attribute "lang" +language = attribute "language" +loading = attribute "loading" +list = attribute "list" +loop = attribute "loop" +low = attribute "low" +manifest = attribute "manifest" +max = attribute "max" +maxlength = attribute "maxlength" +minlength = attribute "minlength" +media = attribute "media" +method = attribute "method" +min = attribute "min" +multiple = attribute "multiple" +muted = attribute "muted" +name = attribute "name" +novalidate = attribute "novalidate" +open = attribute "open" +optimum = attribute "optimum" +pattern = attribute "pattern" +ping = attribute "ping" +placeholder = attribute "placeholder" +poster = attribute "poster" +preload = attribute "preload" +radiogroup = attribute "radiogroup" +readonly = attribute "readonly" +referrerpolicy = attribute "referrerpolicy" +rel = attribute "rel" +required = attribute "required" +reversed = attribute "reversed" +role = attribute "role" +rows = attribute "rows" +rowspan = attribute "rowspan" +sandbox = attribute "sandbox" +scope = attribute "scope" +scoped = attribute "scoped" +selected = attribute "selected" +shape = attribute "shape" +size = attribute "size" +sizes = attribute "sizes" +slot = attribute "slot" +span = attribute "span" +spellcheck = attribute "spellcheck" +src = attribute "src" +srcdoc = attribute "srcdoc" +srclang = attribute "srclang" +srcset = attribute "srcset" +start = attribute "start" +step = attribute "step" +style = attribute "style" +summary = attribute "summary" +tabindex = attribute "tabindex" +target = attribute "target" +title = attribute "title" +translate = attribute "translate" +type = attribute "type" +usemap = attribute "usemap" +value = attribute "value" +width = attribute "width" +wrap = attribute "wrap" diff --git a/examples/static-site-gen/platform/build.rs b/examples/static-site-gen/platform/build.rs new file mode 100644 index 0000000000..47763b34c3 --- /dev/null +++ b/examples/static-site-gen/platform/build.rs @@ -0,0 +1,9 @@ +fn main() { + #[cfg(not(windows))] + println!("cargo:rustc-link-lib=dylib=app"); + + #[cfg(windows)] + println!("cargo:rustc-link-lib=dylib=libapp"); + + println!("cargo:rustc-link-search=."); +} diff --git a/examples/static-site-gen/platform/host.c b/examples/static-site-gen/platform/host.c new file mode 100644 index 0000000000..b9214bcf33 --- /dev/null +++ b/examples/static-site-gen/platform/host.c @@ -0,0 +1,3 @@ +extern int rust_main(); + +int main() { return rust_main(); } \ No newline at end of file diff --git a/examples/static-site-gen/platform/main.roc b/examples/static-site-gen/platform/main.roc new file mode 100644 index 0000000000..30374d67a0 --- /dev/null +++ b/examples/static-site-gen/platform/main.roc @@ -0,0 +1,9 @@ +platform "static-site-gen" + requires {} { transformFileContent : Str, Str -> Str } + exposes [] + packages {} + imports [] + provides [transformFileContentForHost] + +transformFileContentForHost : Box Str, Box Str -> Str +transformFileContentForHost = \relPath, htmlContent -> transformFileContent (Box.unbox relPath) (Box.unbox htmlContent) diff --git a/examples/static-site-gen/platform/src/lib.rs b/examples/static-site-gen/platform/src/lib.rs new file mode 100644 index 0000000000..acbe32e079 --- /dev/null +++ b/examples/static-site-gen/platform/src/lib.rs @@ -0,0 +1,399 @@ +use core::ffi::c_void; +use libc; +use pulldown_cmark::{html, Options, Parser}; +use roc_std::{RocBox, RocStr}; +use std::env; +use std::ffi::CStr; +use std::fs; +use std::os::raw::c_char; +use std::path::{Path, PathBuf}; + +use syntect::easy::HighlightLines; +use syntect::highlighting::{Style, ThemeSet}; +use syntect::html::{ClassStyle, ClassedHTMLGenerator}; +use syntect::parsing::SyntaxSet; +use syntect::util::LinesWithEndings; + +extern "C" { + #[link_name = "roc__transformFileContentForHost_1_exposed"] + fn roc_transformFileContentForHost(relPath: RocBox, content: RocBox) -> RocStr; +} + +#[no_mangle] +pub unsafe extern "C" fn roc_alloc(size: usize, _alignment: u32) -> *mut c_void { + libc::malloc(size) +} + +#[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) +} + +#[no_mangle] +pub unsafe extern "C" fn roc_dealloc(c_ptr: *mut c_void, _alignment: u32) { + libc::free(c_ptr) +} + +#[cfg(unix)] +#[no_mangle] +pub unsafe extern "C" fn roc_getppid() -> libc::pid_t { + libc::getppid() +} + +#[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) +} + +#[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) +} + +#[no_mangle] +pub extern "C" fn rust_main() -> i32 { + let args: Vec = env::args().collect(); + if args.len() != 3 { + eprintln!("Usage: {} path/to/input/dir path/to/output/dir", args[0]); + return 1; + } + + match run(&args[1], &args[2]) { + Err(e) => { + eprintln!("{}", e); + 1 + } + Ok(()) => 0, + } +} + +#[no_mangle] +pub unsafe extern "C" fn roc_panic(msg: *mut RocStr, tag_id: u32) { + match tag_id { + 0 => { + eprintln!("Roc standard library hit a panic: {}", &*msg); + } + 1 => { + eprintln!("Application hit a panic: {}", &*msg); + } + _ => unreachable!(), + } + std::process::exit(1); +} + +#[no_mangle] +pub unsafe extern "C" fn roc_dbg(loc: *mut RocStr, msg: *mut RocStr) { + eprintln!("[{}] {}", &*loc, &*msg); +} + +#[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) +} + +fn run(input_dirname: &str, output_dirname: &str) -> Result<(), String> { + let input_dir = strip_windows_prefix( + PathBuf::from(input_dirname) + .canonicalize() + .map_err(|e| format!("{}: {}", input_dirname, e))?, + ); + + let output_dir = { + let dir = PathBuf::from(output_dirname); + if !dir.exists() { + fs::create_dir_all(&dir).unwrap(); + } + strip_windows_prefix( + dir.canonicalize() + .map_err(|e| format!("{}: {}", output_dirname, e))?, + ) + }; + + if !input_dir.exists() { + return Err(format!("{} does not exist. The first argument should be the directory where your Markdown files are.", input_dir.display())); + } + + if !output_dir.exists() { + return Err(format!("Could not create {}", output_dir.display())); + } + + let mut input_files: Vec = vec![]; + find_files(&input_dir, &mut input_files) + .map_err(|e| format!("Error finding input files: {}", e))?; + + println!("Processing {} input files...", input_files.len()); + + // TODO: process the files asynchronously + let num_files = input_files.len(); + let mut num_errors = 0; + let mut num_successes = 0; + for input_file in input_files { + match input_file.extension() { + Some(s) if s.eq("md".into()) => { + match process_file(&input_dir, &output_dir, &input_file) { + Ok(()) => { + num_successes += 1; + } + Err(e) => { + eprintln!( + "Failed to process file:\n\n ({:?})with error:\n\n {}", + &input_file, e + ); + num_errors += 1; + } + } + } + _ => {} + }; + } + + println!( + "Processed {} files with {} successes and {} errors", + num_files, num_successes, num_errors + ); + + if num_errors > 0 { + Err("Could not process all files".into()) + } else { + Ok(()) + } +} + +fn process_file(input_dir: &Path, output_dir: &Path, input_file: &Path) -> Result<(), String> { + let input_relpath = input_file + .strip_prefix(input_dir) + .map_err(|e| e.to_string())? + .to_path_buf(); + + let mut output_relpath = input_relpath.clone(); + output_relpath.set_extension("html"); + + let content_md = fs::read_to_string(input_file).map_err(|e| { + format!( + "Error reading {}: {}", + input_file.to_str().unwrap_or("an input file"), + e + ) + })?; + + let mut content_html = String::new(); + let mut options = Options::all(); + + // In the tutorial, this messes up string literals in blocks. + // Those could be done as markdown code blocks, but the repl ones need + // a special class, and there's no way to add that class using markdown alone. + // + // We could make this option user-configurable if people actually want it! + options.remove(Options::ENABLE_SMART_PUNCTUATION); + + let parser = Parser::new_ext(&content_md, options); + + // We'll build a new vector of events since we can only consume the parser once + let mut parser_with_highlighting = Vec::new(); + // As we go along, we'll want to highlight code in bundles, not lines + let mut code_to_highlight = String::new(); + // And track a little bit of state + let mut in_code_block = false; + let mut is_roc_code = false; + let syntax_set: syntect::parsing::SyntaxSet = SyntaxSet::load_defaults_newlines(); + let theme_set: syntect::highlighting::ThemeSet = ThemeSet::load_defaults(); + + for event in parser { + match event { + pulldown_cmark::Event::Code(code_str) => { + if code_str.starts_with("roc!") { + let stripped = code_str + .strip_prefix("roc!") + .expect("expected leading 'roc!'"); + + let highlighted_html = + roc_highlight::highlight_roc_code_inline(stripped.to_string().as_str()); + + parser_with_highlighting.push(pulldown_cmark::Event::Html( + pulldown_cmark::CowStr::from(highlighted_html), + )); + } else { + let inline_code = + pulldown_cmark::CowStr::from(format!("{}", code_str)); + parser_with_highlighting.push(pulldown_cmark::Event::Html(inline_code)); + } + } + pulldown_cmark::Event::Start(pulldown_cmark::Tag::CodeBlock(cbk)) => { + in_code_block = true; + is_roc_code = is_roc_code_block(&cbk); + } + pulldown_cmark::Event::End(pulldown_cmark::Tag::CodeBlock( + pulldown_cmark::CodeBlockKind::Fenced(extention_str), + )) => { + if in_code_block { + match replace_code_with_static_file(&code_to_highlight, input_file) { + None => {} + // Check if the code block is actually just a relative + // path to a static file, if so replace the code with + // the contents of the file. + // ``` + // file:myCodeFile.roc + // ``` + Some(new_code_to_highlight) => { + code_to_highlight = new_code_to_highlight; + } + } + + // Format the whole multi-line code block as HTML all at once + let highlighted_html: String; + if is_roc_code { + highlighted_html = roc_highlight::highlight_roc_code(&code_to_highlight) + } else if let Some(syntax) = syntax_set.find_syntax_by_token(&extention_str) { + let mut h = + HighlightLines::new(syntax, &theme_set.themes["base16-ocean.dark"]); + + let mut html_generator = ClassedHTMLGenerator::new_with_class_style( + syntax, + &syntax_set, + ClassStyle::Spaced, + ); + for line in LinesWithEndings::from(&code_to_highlight) { + html_generator.parse_html_for_line_which_includes_newline(line); + } + highlighted_html = + format!("
{}
", html_generator.finalize()) + } else { + highlighted_html = format!("
{}
", &code_to_highlight) + } + + // And put it into the vector + parser_with_highlighting.push(pulldown_cmark::Event::Html( + pulldown_cmark::CowStr::from(highlighted_html), + )); + code_to_highlight = String::new(); + in_code_block = false; + } + } + pulldown_cmark::Event::Text(t) => { + if in_code_block { + // If we're in a code block, build up the string of text + code_to_highlight.push_str(&t); + } else { + parser_with_highlighting.push(pulldown_cmark::Event::Text(t)) + } + } + e => { + parser_with_highlighting.push(e); + } + } + } + + html::push_html(&mut content_html, parser_with_highlighting.into_iter()); + + let roc_relpath = RocStr::from(output_relpath.to_str().unwrap()); + let roc_content_html = RocStr::from(content_html.as_str()); + let roc_output_str = unsafe { + roc_transformFileContentForHost(RocBox::new(roc_relpath), RocBox::new(roc_content_html)) + }; + + let output_file = output_dir.join(&output_relpath); + let rust_output_str: &str = &roc_output_str; + + println!("{} -> {}", input_file.display(), output_file.display()); + + // Create parent directory if it doesn't exist + let parent_dir = output_file.parent().unwrap(); + if !parent_dir.exists() { + fs::create_dir_all(&parent_dir).unwrap(); + } + + fs::write(output_file, rust_output_str).map_err(|e| format!("{}", e)) +} + +fn find_files(dir: &Path, file_paths: &mut Vec) -> std::io::Result<()> { + for entry in fs::read_dir(dir)? { + let pathbuf = entry?.path(); + if pathbuf.is_dir() { + find_files(&pathbuf, file_paths)?; + } else { + file_paths.push(pathbuf); + } + } + Ok(()) +} + +/// On windows, the path is prefixed with `\\?\`, the "verbatim" prefix. +/// Such a path does not works as an argument to `zig` and other command line tools, +/// and there seems to be no good way to strip it. So we resort to some string manipulation. +pub fn strip_windows_prefix(path_buf: PathBuf) -> std::path::PathBuf { + #[cfg(not(windows))] + { + path_buf + } + + #[cfg(windows)] + { + let path_str = path_buf.display().to_string(); + + std::path::Path::new(path_str.trim_start_matches(r"\\?\")).to_path_buf() + } +} + +fn is_roc_code_block(cbk: &pulldown_cmark::CodeBlockKind) -> bool { + match cbk { + pulldown_cmark::CodeBlockKind::Indented => false, + pulldown_cmark::CodeBlockKind::Fenced(cow_str) => { + if cow_str.contains("roc") { + true + } else { + false + } + } + } +} + +fn replace_code_with_static_file(code: &str, input_file: &Path) -> Option { + let input_dir = input_file.parent()?; + let trimmed_code = code.trim(); + + // Confirm the code block starts with a `file:` tag + match trimmed_code.strip_prefix("file:") { + None => None, + Some(path) => { + // File must be located in input folder or sub-directory + if path.contains("../") { + panic!("ERROR File must be located within the input diretory!"); + } + + let file_path = input_dir.join(path); + + // Check file exists before opening + match file_path.try_exists() { + Err(_) | Ok(false) => { + panic!( + "ERROR File does not exist: \"{}\"", + file_path.to_str().unwrap() + ); + } + Ok(true) => { + let vec_u8 = fs::read(file_path).ok()?; + + String::from_utf8(vec_u8).ok() + } + } + } + } +} diff --git a/examples/static-site-gen/platform/src/main.rs b/examples/static-site-gen/platform/src/main.rs new file mode 100644 index 0000000000..0765384f29 --- /dev/null +++ b/examples/static-site-gen/platform/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + std::process::exit(host::rust_main() as _); +} diff --git a/examples/static-site-gen/static-site.roc b/examples/static-site-gen/static-site.roc new file mode 100644 index 0000000000..b7276f5821 --- /dev/null +++ b/examples/static-site-gen/static-site.roc @@ -0,0 +1,68 @@ +app "static-site" + packages { pf: "platform/main.roc" } + imports [ + pf.Html.{ html, head, body, div, text, a, ul, li, link, meta }, + pf.Html.Attributes.{ httpEquiv, content, href, rel, lang, class, title }, + ] + provides [transformFileContent] to pf + +NavLink : { + url : Str, + title : Str, + text : Str, +} + +navLinks : List NavLink +navLinks = [ + { url: "subFolder/apple.html", title: "Exempli Gratia Pagina Pomi", text: "Apple" }, + { url: "banana.html", title: "Exempli Gratia Pagina Musa", text: "Banana" }, + { url: "cherry.html", title: "Exempli Pagina Cerasus", text: "Cherry" }, +] + +transformFileContent : Str, Str -> Str +transformFileContent = \currentUrl, htmlContent -> + List.findFirst navLinks (\{ url } -> url == currentUrl) + |> Result.map (\currentNavLink -> view currentNavLink htmlContent) + |> Result.map Html.render + |> Result.withDefault "" + +view : NavLink, Str -> Html.Node +view = \currentNavLink, htmlContent -> + html [lang "en"] [ + head [] [ + meta [httpEquiv "content-type", content "text/html; charset=utf-8"], + Html.title [] [text currentNavLink.title], + link [rel "stylesheet", href "style.css"], + ], + body [] [ + div [class "main"] [ + div [class "navbar"] [ + viewNavbar currentNavLink, + ], + div [class "article"] [ + # For now `text` is not escaped so we can use it to insert HTML + # We'll probably want something more explicit in the long term though! + text htmlContent, + ], + ], + ], + ] + +viewNavbar : NavLink -> Html.Node +viewNavbar = \currentNavLink -> + ul + [] + (List.map navLinks \nl -> viewNavLink (nl == currentNavLink) nl) + +viewNavLink : Bool, NavLink -> Html.Node +viewNavLink = \isCurrent, navlink -> + if isCurrent then + li [class "nav-link nav-link--current"] [ + text navlink.text, + ] + else + li [class "nav-link"] [ + a + [href navlink.url, title navlink.title] + [text navlink.text], + ] diff --git a/examples/swiftui/.gitignore b/examples/swiftui/.gitignore new file mode 100644 index 0000000000..ba2906d066 --- /dev/null +++ b/examples/swiftui/.gitignore @@ -0,0 +1 @@ +main diff --git a/examples/swiftui/main.roc b/examples/swiftui/main.roc new file mode 100644 index 0000000000..a084b79a9e --- /dev/null +++ b/examples/swiftui/main.roc @@ -0,0 +1,6 @@ +app "swiftui" + packages { pf: "platform/main.roc" } + imports [] + provides [main] to pf + +main = "Roc <3 Swift!\n" diff --git a/examples/swiftui/platform/host.h b/examples/swiftui/platform/host.h new file mode 100644 index 0000000000..7714980c93 --- /dev/null +++ b/examples/swiftui/platform/host.h @@ -0,0 +1,9 @@ +#include + +struct RocStr { + char* bytes; + size_t len; + size_t capacity; +}; + +extern void roc__mainForHost_1_exposed_generic(const struct RocStr *data); diff --git a/examples/swiftui/platform/host.swift b/examples/swiftui/platform/host.swift new file mode 100644 index 0000000000..371de8baf1 --- /dev/null +++ b/examples/swiftui/platform/host.swift @@ -0,0 +1,78 @@ +import Foundation +import SwiftUI + +@_cdecl("roc_alloc") +func rocAlloc(size: Int, _alignment: UInt) -> UInt { + guard let ptr = malloc(size) else { + return 0 + } + return UInt(bitPattern: ptr) +} + +@_cdecl("roc_dealloc") +func rocDealloc(ptr: UInt, _alignment: UInt) { + free(UnsafeMutableRawPointer(bitPattern: ptr)) +} + +@_cdecl("roc_realloc") +func rocRealloc(ptr: UInt, _oldSize: Int, newSize: Int, _alignment: UInt) -> UInt { + guard let ptr = realloc(UnsafeMutableRawPointer(bitPattern: ptr), newSize) else { + return 0 + } + return UInt(bitPattern: ptr) +} + +func isSmallString(rocStr: RocStr) -> Bool { + return rocStr.capacity < 0 +} + +func getStrLen(rocStr: RocStr) -> Int { + if isSmallString(rocStr: rocStr) { + // Small String length is last in the byte of capacity. + var cap = rocStr.capacity + let count = MemoryLayout.size(ofValue: cap) + let bytes = Data(bytes: &cap, count: count) + let lastByte = bytes[count - 1] + return Int(lastByte ^ 0b1000_0000) + } else { + return rocStr.len + } +} + +func getSwiftString(rocStr: RocStr) -> String { + let length = getStrLen(rocStr: rocStr) + + if isSmallString(rocStr: rocStr) { + let data: Data = withUnsafePointer(to: rocStr) { ptr in + Data(bytes: ptr, count: length) + } + return String(data: data, encoding: .utf8)! + } else { + let data = Data(bytes: rocStr.bytes, count: length) + return String(data: data, encoding: .utf8)! + } +} + +struct ContentView: View { + var str: String + + init() { + var rocStr = RocStr() + roc__mainForHost_1_exposed_generic(&rocStr) + self.str = getSwiftString(rocStr: rocStr) + } + + var body: some View { + Text(self.str) + .padding() + } +} + +@main +struct RocTestApp: App { + var body: some Scene { + WindowGroup { + ContentView() + } + } +} \ No newline at end of file diff --git a/examples/swiftui/platform/main.roc b/examples/swiftui/platform/main.roc new file mode 100644 index 0000000000..1aa031ea3d --- /dev/null +++ b/examples/swiftui/platform/main.roc @@ -0,0 +1,9 @@ +platform "swiftui-platform" + requires {} { main : Str } + exposes [] + packages {} + imports [] + provides [mainForHost] + +mainForHost : Str +mainForHost = main diff --git a/examples/swiftui/run.sh b/examples/swiftui/run.sh new file mode 100755 index 0000000000..d20a18d314 --- /dev/null +++ b/examples/swiftui/run.sh @@ -0,0 +1,9 @@ +#! /usr/bin/env bash + +# https://vaneyckt.io/posts/safer_bash_scripts_with_set_euxo_pipefail/ +set -euxo pipefail + +cargo run -- build +mkdir -p SwiftUIDemo.app/Contents/MacOS/ +mv swiftui SwiftUIDemo.app/Contents/MacOS/SwiftUIDemo +open SwiftUIDemo.app \ No newline at end of file diff --git a/examples/virtual-dom-wip/ExampleApp.roc b/examples/virtual-dom-wip/ExampleApp.roc new file mode 100644 index 0000000000..626f472b2e --- /dev/null +++ b/examples/virtual-dom-wip/ExampleApp.roc @@ -0,0 +1,38 @@ +interface ExampleApp + exposes [exampleApp, State] + imports [ + pf.Html.{ App, Html, html, head, body, div, text, h1 }, + ] + +State : { + answer : U32, +} + +exampleApp : App State State +exampleApp = { + init, + render, + wasmUrl: "assets/example-client.wasm", +} + +init = \result -> + when result is + Ok state -> state + Err _ -> { answer: 0 } + +render : State -> Html State +render = \state -> + num = Num.toStr state.answer + + html [] [ + head [] [], + body [] [ + h1 [] [text "The app"], + div [] [text "The answer is \(num)"], + ], + ] + +expect + Html.renderStatic (Html.translateStatic (render { answer: 42 })) + == + "

The app

The answer is 42
" diff --git a/examples/virtual-dom-wip/example-client.roc b/examples/virtual-dom-wip/example-client.roc new file mode 100644 index 0000000000..f58b7eddb7 --- /dev/null +++ b/examples/virtual-dom-wip/example-client.roc @@ -0,0 +1,6 @@ +app "example-client" + packages { pf: "platform/client-side.roc" } + imports [ExampleApp.{ exampleApp }] + provides [app] to pf + +app = exampleApp diff --git a/examples/virtual-dom-wip/example-server.roc b/examples/virtual-dom-wip/example-server.roc new file mode 100644 index 0000000000..3e05fb49f1 --- /dev/null +++ b/examples/virtual-dom-wip/example-server.roc @@ -0,0 +1,6 @@ +app "example-server" + packages { pf: "platform/server-side.roc" } + imports [ExampleApp.{ exampleApp }] + provides [app] to pf + +app = exampleApp diff --git a/examples/virtual-dom-wip/platform/Action.roc b/examples/virtual-dom-wip/platform/Action.roc new file mode 100644 index 0000000000..6a5e019c73 --- /dev/null +++ b/examples/virtual-dom-wip/platform/Action.roc @@ -0,0 +1,17 @@ +interface Action + exposes [Action, none, update, map] + imports [] + +Action state : [None, Update state] + +none : Action * +none = None + +update : state -> Action state +update = Update + +map : Action a, (a -> b) -> Action b +map = \action, transform -> + when action is + None -> None + Update state -> Update (transform state) diff --git a/examples/virtual-dom-wip/platform/Effect.roc b/examples/virtual-dom-wip/platform/Effect.roc new file mode 100644 index 0000000000..99e0fc5c5e --- /dev/null +++ b/examples/virtual-dom-wip/platform/Effect.roc @@ -0,0 +1,91 @@ +hosted Effect + exposes [ + Effect, + NodeId, + HandlerId, + TagName, + AttrType, + EventType, + after, + always, + map, + createElement, + createTextNode, + updateTextNode, + appendChild, + removeNode, + replaceNode, + setAttribute, + removeAttribute, + setProperty, + removeProperty, + setStyle, + setListener, + removeListener, + enableVdomAllocator, + disableVdomAllocator, + ] + imports [] + generates Effect with [after, always, map] + +# TODO: private types +NodeId : Nat +HandlerId : Nat + +# TODO: make these tag unions to avoid encoding/decoding standard names +# but for now, this is much easier to code and debug! +TagName : Str +AttrType : Str +EventType : Str + +## createElement tagName +createElement : NodeId, TagName -> Effect {} + +## createTextNode content +createTextNode : NodeId, Str -> Effect {} + +## updateTextNode content +updateTextNode : NodeId, Str -> Effect {} + +## appendChild parentId childId +appendChild : NodeId, NodeId -> Effect {} + +## removeNode id +removeNode : NodeId -> Effect {} + +## replaceNode oldId newId +replaceNode : NodeId, NodeId -> Effect {} + +## setAttribute nodeId attrName value +setAttribute : NodeId, AttrType, Str -> Effect {} + +## removeAttribute nodeId attrName +removeAttribute : NodeId, AttrType -> Effect {} + +## setProperty nodeId propName json +setProperty : NodeId, Str, List U8 -> Effect {} + +## removeProperty nodeId propName +removeProperty : NodeId, Str -> Effect {} + +## setStyle nodeId key value +setStyle : NodeId, Str, Str -> Effect {} + +## setListener nodeId eventType accessorsJson handlerId +setListener : NodeId, EventType, List U8, HandlerId -> Effect {} + +## removeListener nodeId handlerId +removeListener : NodeId, HandlerId -> Effect {} + +# Enable a special memory allocator for virtual DOM +# This consists of two arenas, "even" and "odd", which alternately hold the "old" and "new" VDOM. +# After we do a diff, the "old" virtual DOM can be dropped without checking refcounts. +# Danger: Could cause memory unsafety bugs if used incorrectly! Do not expose! +# Not suitable for values that have a different lifetime from the virtual DOM! +# TODO: actually implement this for real! LOL +enableVdomAllocator : Bool -> Effect {} + +# Switch back from the virtual DOM allocator to the "normal" +# allocator that is safe to use with long-lived values. +# At the same time, drop the entire "old" virtual DOM arena. +disableVdomAllocator : Effect {} diff --git a/examples/virtual-dom-wip/platform/Html.roc b/examples/virtual-dom-wip/platform/Html.roc new file mode 100644 index 0000000000..4df219012c --- /dev/null +++ b/examples/virtual-dom-wip/platform/Html.roc @@ -0,0 +1,270 @@ +interface Html + exposes [ + App, + Html, + Attribute, + renderStatic, + renderStaticWithoutDocType, + translate, + translateStatic, + text, + none, + html, + base, + head, + link, + meta, + style, + title, + body, + address, + article, + aside, + footer, + header, + h1, + h2, + h3, + h4, + h5, + h6, + main, + nav, + section, + blockquote, + dd, + div, + dl, + dt, + figcaption, + figure, + hr, + li, + menu, + ol, + p, + pre, + ul, + a, + abbr, + b, + bdi, + bdo, + br, + cite, + code, + data, + dfn, + em, + i, + kbd, + mark, + q, + rp, + rt, + ruby, + s, + samp, + small, + span, + strong, + sub, + sup, + time, + u, + var, + wbr, + area, + audio, + img, + map, + track, + video, + embed, + iframe, + object, + picture, + portal, + source, + svg, + math, + canvas, + noscript, + script, + del, + ins, + caption, + col, + colgroup, + table, + tbody, + td, + tfoot, + th, + thead, + tr, + button, + datalist, + fieldset, + form, + input, + label, + legend, + meter, + optgroup, + option, + output, + progress, + select, + textarea, + details, + dialog, + summary, + slot, + template, + ] + imports [Html.Internal.Shared, Html.Internal.Server] + +App state initData : Html.Internal.Shared.App state initData +Html state : Html.Internal.Shared.Html state +Attribute state : Html.Internal.Shared.Attribute state + +element = Html.Internal.Shared.element +text = Html.Internal.Shared.text +none = Html.Internal.Shared.none + +translate = Html.Internal.Shared.translate +translateStatic = Html.Internal.Shared.translateStatic + +## Render a static Html node to a string, for saving to disk or sending over a network +## +## The output has no whitespace between nodes, to make it small. +## This is intended for generating full HTML documents, so it +## automatically adds `` to the start of the string. +## See also `renderStaticWithoutDocType`. +renderStatic : Html [] -> Str +renderStatic = \node -> + buffer = Str.reserve "" (Html.Internal.Shared.nodeSize node) + + Html.Internal.Server.appendRenderedStatic buffer node + +## Render a Html node to a static string, without a DOCTYPE +renderStaticWithoutDocType : Html [] -> Str +renderStaticWithoutDocType = \node -> + buffer = Str.reserve "" (Html.Internal.Shared.nodeSize node) + + Html.Internal.Server.appendRenderedStatic buffer node + +html = element "html" +base = element "base" +head = element "head" +link = element "link" +meta = element "meta" +style = element "style" +title = element "title" +body = element "body" +address = element "address" +article = element "article" +aside = element "aside" +footer = element "footer" +header = element "header" +h1 = element "h1" +h2 = element "h2" +h3 = element "h3" +h4 = element "h4" +h5 = element "h5" +h6 = element "h6" +main = element "main" +nav = element "nav" +section = element "section" +blockquote = element "blockquote" +dd = element "dd" +div = element "div" +dl = element "dl" +dt = element "dt" +figcaption = element "figcaption" +figure = element "figure" +hr = element "hr" +li = element "li" +menu = element "menu" +ol = element "ol" +p = element "p" +pre = element "pre" +ul = element "ul" +a = element "a" +abbr = element "abbr" +b = element "b" +bdi = element "bdi" +bdo = element "bdo" +br = element "br" +cite = element "cite" +code = element "code" +data = element "data" +dfn = element "dfn" +em = element "em" +i = element "i" +kbd = element "kbd" +mark = element "mark" +q = element "q" +rp = element "rp" +rt = element "rt" +ruby = element "ruby" +s = element "s" +samp = element "samp" +small = element "small" +span = element "span" +strong = element "strong" +sub = element "sub" +sup = element "sup" +time = element "time" +u = element "u" +var = element "var" +wbr = element "wbr" +area = element "area" +audio = element "audio" +img = element "img" +map = element "map" +track = element "track" +video = element "video" +embed = element "embed" +iframe = element "iframe" +object = element "object" +picture = element "picture" +portal = element "portal" +source = element "source" +svg = element "svg" +math = element "math" +canvas = element "canvas" +noscript = element "noscript" +script = element "script" +del = element "del" +ins = element "ins" +caption = element "caption" +col = element "col" +colgroup = element "colgroup" +table = element "table" +tbody = element "tbody" +td = element "td" +tfoot = element "tfoot" +th = element "th" +thead = element "thead" +tr = element "tr" +button = element "button" +datalist = element "datalist" +fieldset = element "fieldset" +form = element "form" +input = element "input" +label = element "label" +legend = element "legend" +meter = element "meter" +optgroup = element "optgroup" +option = element "option" +output = element "output" +progress = element "progress" +select = element "select" +textarea = element "textarea" +details = element "details" +dialog = element "dialog" +summary = element "summary" +slot = element "slot" +template = element "template" diff --git a/examples/virtual-dom-wip/platform/Html/Attributes.roc b/examples/virtual-dom-wip/platform/Html/Attributes.roc new file mode 100644 index 0000000000..6d6f63d121 --- /dev/null +++ b/examples/virtual-dom-wip/platform/Html/Attributes.roc @@ -0,0 +1,273 @@ +interface Html.Attributes + exposes [ + attribute, + accept, + acceptCharset, + accesskey, + action, + align, + allow, + alt, + async, + autocapitalize, + autocomplete, + autofocus, + autoplay, + background, + bgcolor, + border, + buffered, + capture, + challenge, + charset, + checked, + cite, + class, + code, + codebase, + color, + cols, + colspan, + content, + contenteditable, + contextmenu, + controls, + coords, + crossorigin, + csp, + data, + # dataAttr, TODO + datetime, + decoding, + default, + defer, + dir, + dirname, + disabled, + download, + draggable, + enctype, + enterkeyhint, + for, + form, + formaction, + formenctype, + formmethod, + formnovalidate, + formtarget, + headers, + height, + hidden, + high, + href, + hreflang, + httpEquiv, + icon, + id, + importance, + integrity, + intrinsicsize, + inputmode, + ismap, + itemprop, + keytype, + kind, + label, + lang, + language, + loading, + list, + loop, + low, + manifest, + max, + maxlength, + minlength, + media, + method, + min, + multiple, + muted, + name, + novalidate, + open, + optimum, + pattern, + ping, + placeholder, + poster, + preload, + radiogroup, + readonly, + referrerpolicy, + rel, + required, + reversed, + role, + rows, + rowspan, + sandbox, + scope, + scoped, + selected, + shape, + size, + sizes, + slot, + span, + spellcheck, + src, + srcdoc, + srclang, + srcset, + start, + step, + style, + summary, + tabindex, + target, + title, + translate, + type, + usemap, + value, + width, + wrap, + ] + imports [Html.Internal.Shared.{ Attribute }] + +attribute : Str -> (Str -> Attribute state) +attribute = \attrType -> + \attrValue -> HtmlAttr attrType attrValue + +accept = attribute "accept" +acceptCharset = attribute "acceptCharset" +accesskey = attribute "accesskey" +action = attribute "action" +align = attribute "align" +allow = attribute "allow" +alt = attribute "alt" +async = attribute "async" +autocapitalize = attribute "autocapitalize" +autocomplete = attribute "autocomplete" +autofocus = attribute "autofocus" +autoplay = attribute "autoplay" +background = attribute "background" +bgcolor = attribute "bgcolor" +border = attribute "border" +buffered = attribute "buffered" +capture = attribute "capture" +challenge = attribute "challenge" +charset = attribute "charset" +checked = attribute "checked" +cite = attribute "cite" +class = attribute "class" +code = attribute "code" +codebase = attribute "codebase" +color = attribute "color" +cols = attribute "cols" +colspan = attribute "colspan" +content = attribute "content" +contenteditable = attribute "contenteditable" +contextmenu = attribute "contextmenu" +controls = attribute "controls" +coords = attribute "coords" +crossorigin = attribute "crossorigin" +csp = attribute "csp" +data = attribute "data" +datetime = attribute "datetime" +decoding = attribute "decoding" +default = attribute "default" +defer = attribute "defer" +dir = attribute "dir" +dirname = attribute "dirname" +disabled = attribute "disabled" +download = attribute "download" +draggable = attribute "draggable" +enctype = attribute "enctype" +enterkeyhint = attribute "enterkeyhint" +for = attribute "for" +form = attribute "form" +formaction = attribute "formaction" +formenctype = attribute "formenctype" +formmethod = attribute "formmethod" +formnovalidate = attribute "formnovalidate" +formtarget = attribute "formtarget" +headers = attribute "headers" +height = attribute "height" +hidden = attribute "hidden" +high = attribute "high" +href = attribute "href" +hreflang = attribute "hreflang" +httpEquiv = attribute "httpEquiv" +icon = attribute "icon" +id = attribute "id" +importance = attribute "importance" +integrity = attribute "integrity" +intrinsicsize = attribute "intrinsicsize" +inputmode = attribute "inputmode" +ismap = attribute "ismap" +itemprop = attribute "itemprop" +keytype = attribute "keytype" +kind = attribute "kind" +label = attribute "label" +lang = attribute "lang" +language = attribute "language" +loading = attribute "loading" +list = attribute "list" +loop = attribute "loop" +low = attribute "low" +manifest = attribute "manifest" +max = attribute "max" +maxlength = attribute "maxlength" +minlength = attribute "minlength" +media = attribute "media" +method = attribute "method" +min = attribute "min" +multiple = attribute "multiple" +muted = attribute "muted" +name = attribute "name" +novalidate = attribute "novalidate" +open = attribute "open" +optimum = attribute "optimum" +pattern = attribute "pattern" +ping = attribute "ping" +placeholder = attribute "placeholder" +poster = attribute "poster" +preload = attribute "preload" +radiogroup = attribute "radiogroup" +readonly = attribute "readonly" +referrerpolicy = attribute "referrerpolicy" +rel = attribute "rel" +required = attribute "required" +reversed = attribute "reversed" +role = attribute "role" +rows = attribute "rows" +rowspan = attribute "rowspan" +sandbox = attribute "sandbox" +scope = attribute "scope" +scoped = attribute "scoped" +selected = attribute "selected" +shape = attribute "shape" +size = attribute "size" +sizes = attribute "sizes" +slot = attribute "slot" +span = attribute "span" +spellcheck = attribute "spellcheck" +src = attribute "src" +srcdoc = attribute "srcdoc" +srclang = attribute "srclang" +srcset = attribute "srcset" +start = attribute "start" +step = attribute "step" +style = attribute "style" +summary = attribute "summary" +tabindex = attribute "tabindex" +target = attribute "target" +title = attribute "title" +translate = attribute "translate" +type = attribute "type" +usemap = attribute "usemap" +value = attribute "value" +width = attribute "width" +wrap = attribute "wrap" diff --git a/examples/virtual-dom-wip/platform/Html/Event.roc b/examples/virtual-dom-wip/platform/Html/Event.roc new file mode 100644 index 0000000000..95a7a8056c --- /dev/null +++ b/examples/virtual-dom-wip/platform/Html/Event.roc @@ -0,0 +1,79 @@ +interface Html.Event + exposes [ + Handler, + CyclicStructureAccessor, + on, + custom, + onClick, + onDoubleClick, + onMouseDown, + onMouseUp, + onMouseEnter, + onMouseLeave, + onMouseOver, + onMouseOut, + onCheck, + onBlur, + onFocus, + onInput, + onSubmit, + ] + imports [ + Action.{ Action }, + Html.Internal.Shared.{ Attribute }, + ] + +Handler state : Html.Internal.Shared.Handler state +CyclicStructureAccessor : Html.Internal.Shared.CyclicStructureAccessor + +custom : Str, List CyclicStructureAccessor, (state, List (List U8) -> { action : Action state, stopPropagation : Bool, preventDefault : Bool }) -> Attribute state +custom = \eventName, accessors, callback -> + EventListener eventName accessors (Custom callback) + +on : Str, List CyclicStructureAccessor, (state, List (List U8) -> Action state) -> Attribute state +on = \eventName, accessors, callback -> + EventListener eventName accessors (Normal callback) + +# Internal helper +curriedOn : Str -> (List CyclicStructureAccessor, (state, List (List U8) -> Action state) -> Attribute state) +curriedOn = \eventName -> + \accessors, callback -> + EventListener eventName accessors (Normal callback) + +onClick = curriedOn "click" +onDoubleClick = curriedOn "dblclick" +onMouseDown = curriedOn "mousedown" +onMouseUp = curriedOn "mouseup" +onMouseEnter = curriedOn "mouseenter" +onMouseLeave = curriedOn "mouseleave" +onMouseOver = curriedOn "mouseover" +onMouseOut = curriedOn "mouseout" +onCheck = curriedOn "check" +onBlur = curriedOn "blur" +onFocus = curriedOn "focus" + +onInput : List CyclicStructureAccessor, (state, List (List U8) -> Action state) -> Attribute state +onInput = \accessors, callback -> + customCallback : state, List (List U8) -> { action : Action state, stopPropagation : Bool, preventDefault : Bool } + customCallback = \state, jsons -> { + action: callback state jsons, + stopPropagation: Bool.true, + preventDefault: Bool.false, + } + + EventListener "input" accessors (Custom customCallback) + +onSubmit : List CyclicStructureAccessor, (state, List (List U8) -> Action state) -> Attribute state +onSubmit = \accessors, callback -> + customCallback = \state, jsons -> { + action: callback state jsons, + stopPropagation: Bool.false, + preventDefault: Bool.true, + } + + EventListener "submit" accessors (Custom customCallback) + +# Notes from Elm: +# - stopPropagation causes immediate view update, without waiting for animationFrame, +# to prevent input state getting out of sync with model state when typing fast. +# - Security-sensitive events trigger an immediate update within the same user-instigated tick diff --git a/examples/virtual-dom-wip/platform/Html/Internal/Client.roc b/examples/virtual-dom-wip/platform/Html/Internal/Client.roc new file mode 100644 index 0000000000..bbe2315106 --- /dev/null +++ b/examples/virtual-dom-wip/platform/Html/Internal/Client.roc @@ -0,0 +1,803 @@ +interface Html.Internal.Client + exposes [ + PlatformState, + initClientApp, + dispatchEvent, + ] + imports [ + Effect.{ + Effect, + NodeId, + HandlerId, + TagName, + AttrType, + EventType, + }, + Html.Internal.Shared.{ + App, + Html, + Attribute, + CyclicStructureAccessor, + Handler, + Size, + translateStatic, + }, + TotallyNotJson, + Action, + ] + +PlatformState state initData : { + app : App state initData, + state, + rendered : RenderedTree state, +} + +# The rendered tree uses indices rather than pointers +# This makes it easier to communicate with JS using integer indices. +# There is a JavaScript `nodes` array that matches the Roc `nodes` List +RenderedTree state : { + root : NodeId, + nodes : List (Result RenderedNode [DeletedNode]), + deletedNodeCache : List NodeId, + handlers : List (Result (Handler state) [DeletedHandler]), + deletedHandlerCache : List HandlerId, +} + +RenderedNode : [ + RenderedNone, + RenderedText Str, + RenderedElement Str RenderedAttributes (List NodeId), +] + +RenderedAttributes : { + eventListeners : Dict Str { accessors : List CyclicStructureAccessor, handlerId : HandlerId }, + htmlAttrs : Dict Str Str, + domProps : Dict Str (List U8), + styles : Dict Str Str, +} + +emptyRenderedAttrs = { + eventListeners: Dict.empty {}, + htmlAttrs: Dict.empty {}, + domProps: Dict.empty {}, + styles: Dict.empty {}, +} + +Patch : [ + CreateElement NodeId TagName, + CreateTextNode NodeId Str, + UpdateTextNode NodeId Str, + AppendChild NodeId NodeId, + RemoveNode NodeId, + ReplaceNode NodeId NodeId, + SetAttribute NodeId AttrType Str, + RemoveAttribute NodeId AttrType, + SetProperty NodeId Str (List U8), + RemoveProperty NodeId Str, + SetStyle NodeId Str Str, + SetListener NodeId EventType (List U8) HandlerId, + RemoveListener NodeId HandlerId, +] + +DiffState state : { rendered : RenderedTree state, patches : List Patch } + +# ------------------------------- +# INITIALISATION +# ------------------------------- +initClientApp : List U8, App state initData -> Effect (PlatformState state initData) where initData implements Decoding +initClientApp = \json, app -> + # Initialise the Roc representation of the rendered DOM, and calculate patches (for event listeners) + { state, rendered, patches } = + initClientAppHelp json app + + # Call out to JS to patch the DOM, attaching the event listeners + _ <- applyPatches patches |> Effect.after + + Effect.always { + app, + state, + rendered, + } + +# Testable helper function to initialise the app +initClientAppHelp : List U8, App state initData -> { state, rendered : RenderedTree state, patches : List Patch } where initData implements Decoding +initClientAppHelp = \json, app -> + state = + json + |> Decode.fromBytes TotallyNotJson.json + |> app.init + dynamicView = + app.render state + staticUnindexed = + translateStatic dynamicView + { nodes: staticNodes } = + indexNodes { nodes: [], siblingIds: [] } staticUnindexed + staticRendered = { + root: List.len staticNodes - 1, + nodes: List.map staticNodes Ok, + deletedNodeCache: [], + handlers: [], + deletedHandlerCache: [], + } + + # Run our first diff. The only differences will be event listeners, so we will generate patches to attach those. + { rendered, patches } = + diff { rendered: staticRendered, patches: [] } dynamicView + + { state, rendered, patches } + +# Assign an index to each (virtual) DOM node. +# In JavaScript, we maintain an array of references to real DOM nodes. +# In Roc, we maintain a matching List of virtual DOM nodes with the same indices. +# They are both initialised separately, but use the same indexing algorithm. +# (We *could* pass this data in as JSON from the HTML file, but it would roughly double the size of that HTML file!) +indexNodes : { nodes : List RenderedNode, siblingIds : List Nat }, Html state -> { nodes : List RenderedNode, siblingIds : List Nat } +indexNodes = \{ nodes, siblingIds }, unrendered -> + when unrendered is + Text content -> + { + nodes: List.append nodes (RenderedText content), + siblingIds: List.append siblingIds (List.len nodes), + } + + Element name _ attrs children -> + { nodes: listWithChildren, siblingIds: childIds } = + List.walk children { nodes, siblingIds: [] } indexNodes + renderedAttrs = + List.walk attrs emptyRenderedAttrs \walkedAttrs, attr -> + when attr is + EventListener _ _ _ -> walkedAttrs # Dropped! Server-rendered HTML has no listeners + HtmlAttr k v -> { walkedAttrs & htmlAttrs: Dict.insert walkedAttrs.htmlAttrs k v } + DomProp k v -> { walkedAttrs & domProps: Dict.insert walkedAttrs.domProps k v } + Style k v -> { walkedAttrs & styles: Dict.insert walkedAttrs.styles k v } + + { + nodes: List.append listWithChildren (RenderedElement name renderedAttrs childIds), + siblingIds: List.append siblingIds (List.len listWithChildren), + } + + None -> + { + nodes: List.append nodes RenderedNone, + siblingIds: List.append siblingIds (List.len nodes), + } + +# ------------------------------- +# Patches +# ------------------------------- +applyPatch : Patch -> Effect {} +applyPatch = \patch -> + when patch is + CreateElement nodeId tagName -> Effect.createElement nodeId tagName + CreateTextNode nodeId content -> Effect.createTextNode nodeId content + UpdateTextNode nodeId content -> Effect.updateTextNode nodeId content + AppendChild parentId childId -> Effect.appendChild parentId childId + RemoveNode id -> Effect.removeNode id + ReplaceNode oldId newId -> Effect.replaceNode oldId newId + SetAttribute nodeId attrName value -> Effect.setAttribute nodeId attrName value + RemoveAttribute nodeId attrName -> Effect.removeAttribute nodeId attrName + SetProperty nodeId propName json -> Effect.setProperty nodeId propName json + RemoveProperty nodeId propName -> Effect.removeProperty nodeId propName + SetStyle nodeId key value -> Effect.setStyle nodeId key value + SetListener nodeId eventType accessorsJson handlerId -> Effect.setListener nodeId eventType accessorsJson handlerId + RemoveListener nodeId handlerId -> Effect.removeListener nodeId handlerId + +walkPatches : Effect {}, Patch -> Effect {} +walkPatches = \previousEffects, patch -> + Effect.after previousEffects \{} -> applyPatch patch + +applyPatches : List Patch -> Effect {} +applyPatches = \patches -> + List.walk patches (Effect.always {}) walkPatches + +# ------------------------------- +# EVENT HANDLING +# ------------------------------- +JsEventResult state initData : { + platformState : PlatformState state initData, + stopPropagation : Bool, + preventDefault : Bool, +} + +## Dispatch a JavaScript event to a Roc handler, given the handler ID and some JSON event data. +dispatchEvent : PlatformState state initData, List (List U8), HandlerId -> Effect (JsEventResult state initData) where initData implements Decoding +dispatchEvent = \platformState, eventData, handlerId -> + { app, state, rendered } = + platformState + maybeHandler = + List.get rendered.handlers handlerId + |> Result.withDefault (Err DeletedHandler) + { action, stopPropagation, preventDefault } = + when maybeHandler is + Err DeletedHandler -> + { action: Action.none, stopPropagation: Bool.false, preventDefault: Bool.false } + + Ok (Normal handler) -> + { action: handler state eventData, stopPropagation: Bool.false, preventDefault: Bool.false } + + Ok (Custom handler) -> + handler state eventData + + when action is + Update newState -> + newViewUnrendered = + app.render newState + { rendered: newRendered, patches } = + diff { rendered, patches: [] } newViewUnrendered + + _ <- applyPatches patches |> Effect.after + Effect.always { + platformState: { + app, + state: newState, + rendered: newRendered, + }, + stopPropagation, + preventDefault, + } + + None -> + Effect.always { platformState, stopPropagation, preventDefault } + +# ------------------------------- +# DIFF +# ------------------------------- +diff : DiffState state, Html state -> DiffState state +diff = \{ rendered, patches }, newNode -> + root = + rendered.root + oldNode = + List.get rendered.nodes root + |> Result.withDefault (Ok RenderedNone) + |> Result.withDefault (RenderedNone) + + when { oldNode, newNode } is + { oldNode: RenderedText oldContent, newNode: Text newContent } -> + if newContent != oldContent then + newNodes = + List.set rendered.nodes rendered.root (Ok (RenderedText newContent)) + + { + rendered: { rendered & + nodes: newNodes, + }, + patches: List.append patches (UpdateTextNode rendered.root newContent), + } + else + { rendered, patches } + + { oldNode: RenderedElement oldName oldAttrs oldChildren, newNode: Element newName _ newAttrs newChildren } -> + if newName != oldName then + replaceNode { rendered, patches } root newNode + else + stateAttrs = + diffAttrs { rendered, patches } root oldAttrs newAttrs + stateChildPairs = + List.map2 oldChildren newChildren (\oldChildId, newChild -> { oldChildId, newChild }) + |> List.walk stateAttrs \childWalkState, { oldChildId, newChild } -> + { rendered: childWalkRendered, patches: childWalkPatches } = childWalkState + diff { rendered: { childWalkRendered & root: oldChildId }, patches: childWalkPatches } newChild + { rendered: renderedLeftOverChildren, patches: patchesLeftOverChildren } = + if List.len oldChildren > List.len newChildren then + List.walkFrom oldChildren (List.len newChildren) stateChildPairs deleteNode + else if List.len oldChildren < List.len newChildren then + stateBeforeCreate = { + rendered: stateChildPairs.rendered, + patches: stateChildPairs.patches, + ids: [], + } + { rendered: renderedAfterCreate, patches: patchesAfterCreate, ids: createdIds } = + List.walkFrom newChildren (List.len oldChildren) stateBeforeCreate createChildNode + # Look up the children again since they might have new node IDs! + nodeWithUpdatedChildren = + when List.get renderedAfterCreate.nodes root is + Ok (Ok (RenderedElement n a c)) -> RenderedElement n a (List.concat c createdIds) + _ -> crash "Bug in virtual-dom framework: nodeWithUpdatedChildren not found" + updatedNodes = + List.set renderedAfterCreate.nodes root (Ok nodeWithUpdatedChildren) + + { + rendered: { renderedAfterCreate & nodes: updatedNodes }, + patches: List.walk createdIds patchesAfterCreate \p, id -> List.append p (AppendChild root id), + } + else + stateChildPairs + + { + rendered: { renderedLeftOverChildren & root }, + patches: patchesLeftOverChildren, + } + + { oldNode: RenderedNone, newNode: None } -> + { rendered, patches } + + _ -> + # old node has been replaced with a totally different variant. There's no point in diffing, just replace. + replaceNode { rendered, patches } rendered.root newNode + +replaceNode : DiffState state, NodeId, Html state -> DiffState state +replaceNode = \diffState, oldNodeId, newNode -> + { rendered: createRendered, patches: createPatches, id: createNodeId } = + createNode diffState newNode + preDeleteState = { + rendered: createRendered, + patches: List.append createPatches (ReplaceNode oldNodeId createNodeId), + } + + deleteNode preDeleteState oldNodeId + +# Delete a node, and drop any JS references to its children and event listeners +# TODO: see if it would speed things up to leave this junk lying around until the slot is reused. +# Any danger of spurious events being sent to the wrong handler? +# Otherwise, can we sweep everything at once at the end of the diff? +# Let's be conservative on things like this until we have more test cases working. +deleteNode : DiffState state, NodeId -> DiffState state +deleteNode = \diffState, id -> + { rendered, patches } = + when List.get diffState.rendered.nodes id is + Ok node -> + when node is + Ok (RenderedElement _ _ children) -> + List.walk children diffState deleteNode + + _ -> diffState + + _ -> diffState + + patchesRemoveListeners = + when List.get rendered.nodes id is + Ok (Ok (RenderedElement _ attrs _)) -> + Dict.walk attrs.eventListeners patches \p, _, { handlerId } -> + List.append p (RemoveListener id handlerId) + + _ -> patches + + newNodes = + List.set rendered.nodes id (Err DeletedNode) + newDeletedNodeCache = + List.append rendered.deletedNodeCache id + newPatches = + List.append patchesRemoveListeners (RemoveNode id) + + { + rendered: { rendered & + nodes: newNodes, + deletedNodeCache: newDeletedNodeCache, + }, + patches: newPatches, + } + +createNode : DiffState state, Html state -> { rendered : RenderedTree state, patches : List Patch, id : NodeId } +createNode = \{ rendered, patches }, newNode -> + when newNode is + Text content -> + { rendered: newRendered, id } = + insertNode rendered (RenderedText content) + + { + rendered: newRendered, + patches: List.append patches (CreateTextNode id content), + id, + } + + None -> + { rendered: newRendered, id } = + insertNode rendered RenderedNone + + { rendered: newRendered, patches, id } + + Element tagName _ attrs children -> + { rendered: renderedWithChildren, patches: patchesWithChildren, ids: childIds } = + List.walk children { rendered, patches, ids: [] } createChildNode + nodeId = + nextNodeId renderedWithChildren + patchesWithElem = + List.append patchesWithChildren (CreateElement nodeId tagName) + { renderedAttrs, rendered: renderedWithAttrs, patches: patchesWithAttrs } = + renderAttrs attrs renderedWithChildren patchesWithElem nodeId + { rendered: renderedWithNode } = + insertNode renderedWithAttrs (RenderedElement tagName renderedAttrs childIds) + + { + rendered: renderedWithNode, + patches: patchesWithAttrs, + id: nodeId, + } + +AttrDiffState state : { + nodeId : NodeId, + attrs : RenderedAttributes, + patches : List Patch, + handlers : List (Result (Handler state) [DeletedHandler]), + deletedHandlerCache : List HandlerId, +} + +diffAttrs : DiffState state, NodeId, RenderedAttributes, List (Attribute state) -> DiffState state +diffAttrs = \{ rendered, patches }, nodeId, attrs, newAttrs -> + initState = { + nodeId, + attrs, + patches, + handlers: rendered.handlers, + deletedHandlerCache: rendered.deletedHandlerCache, + } + finalState = + List.walk newAttrs initState diffAttr + newRendered = + { rendered & + handlers: finalState.handlers, + deletedHandlerCache: finalState.deletedHandlerCache, + } + + { + rendered: newRendered, + patches: finalState.patches, + } + +diffAttr : AttrDiffState state, Attribute state -> AttrDiffState state +diffAttr = \{ nodeId, attrs, patches, handlers, deletedHandlerCache }, attr -> + when attr is + EventListener eventName newAccessors newHandler -> + when Dict.get attrs.eventListeners eventName is + Ok { accessors, handlerId } -> + (Tuple newAttrs newPatches) = + if accessors == newAccessors then + Tuple attrs patches + else + json = newAccessors |> Encode.toBytes TotallyNotJson.json + + Tuple + { attrs & eventListeners: Dict.insert attrs.eventListeners eventName { accessors, handlerId } } + ( + patches + |> List.append (RemoveListener nodeId handlerId) + |> List.append (SetListener nodeId eventName json handlerId) + ) + + { + nodeId, + attrs: newAttrs, + patches: newPatches, + handlers: List.set handlers handlerId (Ok newHandler), + deletedHandlerCache, + } + + Err KeyNotFound -> + renderAttr { nodeId, attrs, patches, handlers, deletedHandlerCache } attr + + HtmlAttr k v -> + when Dict.get attrs.htmlAttrs k is + Ok oldVal -> + (Tuple newAttrs newPatches) = + if oldVal == v then + Tuple attrs patches + else + Tuple + { attrs & htmlAttrs: Dict.insert attrs.htmlAttrs k v } + (patches |> List.append (SetAttribute nodeId k v)) + { + nodeId, + attrs: newAttrs, + patches: newPatches, + handlers, + deletedHandlerCache, + } + + Err KeyNotFound -> + renderAttr { nodeId, attrs, patches, handlers, deletedHandlerCache } attr + + DomProp k v -> + when Dict.get attrs.domProps k is + Ok oldVal -> + (Tuple newAttrs newPatches) = + if oldVal == v then + Tuple attrs patches + else + Tuple + { attrs & domProps: Dict.insert attrs.domProps k v } + (patches |> List.append (SetProperty nodeId k v)) + { + nodeId, + attrs: newAttrs, + patches: newPatches, + handlers, + deletedHandlerCache, + } + + Err KeyNotFound -> + renderAttr { nodeId, attrs, patches, handlers, deletedHandlerCache } attr + + Style k v -> + when Dict.get attrs.styles k is + Ok oldVal -> + (Tuple newAttrs newPatches) = + if oldVal == v then + Tuple attrs patches + else + Tuple + { attrs & styles: Dict.insert attrs.styles k v } + (patches |> List.append (SetStyle nodeId k v)) + { + nodeId, + attrs: newAttrs, + patches: newPatches, + handlers, + deletedHandlerCache, + } + + Err KeyNotFound -> + renderAttr { nodeId, attrs, patches, handlers, deletedHandlerCache } attr + +renderAttrs : List (Attribute state), RenderedTree state, List Patch, NodeId -> { renderedAttrs : RenderedAttributes, rendered : RenderedTree state, patches : List Patch } +renderAttrs = \attrs, rendered, patches, nodeId -> + initState = { + nodeId, + attrs: emptyRenderedAttrs, + patches, + handlers: rendered.handlers, + deletedHandlerCache: rendered.deletedHandlerCache, + } + finalState = + List.walk attrs initState renderAttr + + { + renderedAttrs: finalState.attrs, + rendered: { rendered & + handlers: finalState.handlers, + deletedHandlerCache: finalState.deletedHandlerCache, + }, + patches: finalState.patches, + } + +renderAttr : AttrDiffState state, Attribute state -> AttrDiffState state +renderAttr = \{ nodeId, attrs, patches, handlers, deletedHandlerCache }, attr -> + when attr is + HtmlAttr k v -> + { + nodeId, + handlers, + deletedHandlerCache, + attrs: { attrs & htmlAttrs: Dict.insert attrs.htmlAttrs k v }, + patches: List.append patches (SetAttribute nodeId k v), + } + + DomProp k v -> + { + nodeId, + handlers, + deletedHandlerCache, + attrs: { attrs & domProps: Dict.insert attrs.domProps k v }, + patches: List.append patches (SetProperty nodeId k v), + } + + Style k v -> + { + nodeId, + handlers, + deletedHandlerCache, + attrs: { attrs & styles: Dict.insert attrs.styles k v }, + patches: List.append patches (SetStyle nodeId k v), + } + + EventListener eventType accessors handler -> + { handlerId, newHandlers, newDeletedHandlerCache } = + when List.last deletedHandlerCache is + Ok id -> + { + handlerId: id, + newHandlers: List.set handlers id (Ok handler), + newDeletedHandlerCache: List.dropLast deletedHandlerCache 1, + } + + Err _ -> + { + handlerId: List.len handlers, + newHandlers: List.append handlers (Ok handler), + newDeletedHandlerCache: deletedHandlerCache, + } + accessorsJson = + accessors |> Encode.toBytes TotallyNotJson.json + patch = + SetListener nodeId eventType accessorsJson handlerId + + { + nodeId, + attrs: { attrs & eventListeners: Dict.insert attrs.eventListeners eventType { accessors, handlerId } }, + handlers: newHandlers, + deletedHandlerCache: newDeletedHandlerCache, + patches: List.append patches patch, + } + +createChildNode : + { rendered : RenderedTree state, patches : List Patch, ids : List NodeId }, + Html state + -> { rendered : RenderedTree state, patches : List Patch, ids : List NodeId } +createChildNode = \{ rendered, patches, ids }, childHtml -> + { rendered: renderedChild, patches: childPatches, id } = + createNode { rendered, patches } childHtml + + { + rendered: renderedChild, + patches: childPatches, + ids: List.append ids id, + } + +# insert a node into the nodes list, assigning it a NodeId +insertNode : RenderedTree state, RenderedNode -> { rendered : RenderedTree state, id : NodeId } +insertNode = \rendered, node -> + when List.last rendered.deletedNodeCache is + Ok id -> + newRendered = + { rendered & + nodes: List.set rendered.nodes id (Ok node), + deletedNodeCache: List.dropLast rendered.deletedNodeCache 1, + } + + { rendered: newRendered, id } + + Err _ -> + newRendered = + { rendered & + nodes: List.append rendered.nodes (Ok node), + } + + { rendered: newRendered, id: List.len rendered.nodes } + +# Predict what NodeId will be assigned next, without actually assigning it +nextNodeId : RenderedTree state -> NodeId +nextNodeId = \rendered -> + when List.last rendered.deletedNodeCache is + Ok id -> id + Err _ -> List.len rendered.nodes + +# ------------------------------- +# TESTS +# ------------------------------- +eqRenderedTree : RenderedTree state, RenderedTree state -> Bool +eqRenderedTree = \a, b -> + (a.root == b.root) + && (a.nodes == b.nodes) + && (List.len a.handlers == List.len b.handlers) + && (a.deletedNodeCache == b.deletedNodeCache) + && (a.deletedHandlerCache == b.deletedHandlerCache) + +# indexNodes +expect + html : Html {} + html = + Element "a" 43 [HtmlAttr "href" "https://www.roc-lang.org/"] [Text "Roc"] + + actual : { nodes : List RenderedNode, siblingIds : List Nat } + actual = + indexNodes { nodes: [], siblingIds: [] } html + + expected : { nodes : List RenderedNode, siblingIds : List Nat } + expected = { + nodes: [ + RenderedText "Roc", + RenderedElement "a" { emptyRenderedAttrs & htmlAttrs: Dict.fromList [("href", "https://www.roc-lang.org/")] } [0], + ], + siblingIds: [1], + } + + (actual.nodes == expected.nodes) + && (actual.siblingIds == expected.siblingIds) + +# diff +expect + State : { answer : U32 } + + diffStateBefore : DiffState State + diffStateBefore = { + rendered: { + root: 4, + nodes: [ + Ok (RenderedText "The app"), + Ok (RenderedElement "h1" emptyRenderedAttrs [0]), + Ok (RenderedText "The answer is 42"), + Ok (RenderedElement "div" emptyRenderedAttrs [2]), + Ok (RenderedElement "body" emptyRenderedAttrs [1, 3]), + ], + deletedNodeCache: [], + handlers: [], + deletedHandlerCache: [], + }, + patches: [], + } + + # Sizes don't matter, use zero. We are not creating a HTML string so we don't care what size it would be. + newNode : Html State + newNode = + Element "body" 0 [] [ + Element "h1" 0 [] [Text "The app"], + Element "div" 0 [] [Text "The answer is 111"], + ] + + expected : DiffState State + expected = { + rendered: { + root: 4, + nodes: [ + Ok (RenderedText "The app"), + Ok (RenderedElement "h1" emptyRenderedAttrs [0]), + Ok (RenderedText "The answer is 111"), + Ok (RenderedElement "div" emptyRenderedAttrs [2]), + Ok (RenderedElement "body" emptyRenderedAttrs [1, 3]), + ], + deletedNodeCache: [], + handlers: [], + deletedHandlerCache: [], + }, + patches: [UpdateTextNode 2 "The answer is 111"], + } + + actual : DiffState State + actual = + diff diffStateBefore newNode + + (actual.patches == expected.patches) + && eqRenderedTree actual.rendered expected.rendered + +# initClientAppHelp +expect + State : { answer : U32 } + + init = \result -> + when result is + Ok state -> state + Err _ -> { answer: 0 } + + onClickHandler : Handler State + onClickHandler = + Normal \state, _ -> Action.update { answer: state.answer + 1 } + + render : State -> Html State + render = \state -> + num = Num.toStr state.answer + + onClickAttr : Attribute State + onClickAttr = + EventListener "click" [] onClickHandler + + # Sizes don't matter, use zero. We are not creating a HTML string so we don't care what size it would be. + Element "body" 0 [] [ + Element "h1" 0 [] [Text "The app"], + Element "div" 0 [onClickAttr] [Text "The answer is \(num)"], + ] + + app : App State State + app = { + init, + render, + wasmUrl: "assets/test.wasm", + } + + initJson : List U8 + initJson = + { answer: 42 } |> Encode.toBytes TotallyNotJson.json # panics at mono/src/ir.rs:5739:56 + expected : { state : State, rendered : RenderedTree State, patches : List Patch } + expected = { + state: { answer: 42 }, + rendered: { + root: 4, + nodes: [ + Ok (RenderedText "The app"), + Ok (RenderedElement "h1" emptyRenderedAttrs [0]), + Ok (RenderedText "The answer is 42"), + Ok (RenderedElement "div" { emptyRenderedAttrs & eventListeners: Dict.fromList [("click", { accessors: [], handlerId: 0 })] } [2]), + Ok (RenderedElement "body" emptyRenderedAttrs [1, 3]), + ], + deletedNodeCache: [], + handlers: [Ok onClickHandler], + deletedHandlerCache: [], + }, + patches: [SetListener 3 "click" [] 0], + } + + actual : { state : State, rendered : RenderedTree State, patches : List Patch } + actual = + initClientAppHelp initJson app + + (actual.state == expected.state) + && eqRenderedTree actual.rendered expected.rendered + && (actual.patches == expected.patches) diff --git a/examples/virtual-dom-wip/platform/Html/Internal/Server.roc b/examples/virtual-dom-wip/platform/Html/Internal/Server.roc new file mode 100644 index 0000000000..aedd397af7 --- /dev/null +++ b/examples/virtual-dom-wip/platform/Html/Internal/Server.roc @@ -0,0 +1,123 @@ +interface Html.Internal.Server + exposes [ + appendRenderedStatic, + initServerApp, + ] + imports [ + Html.Internal.Shared.{ Html, Attribute, App, translateStatic, text, element }, + TotallyNotJson, + ] + +# ------------------------------- +# STATIC HTML +# ------------------------------- +appendRenderedStatic : Str, Html [] -> Str +appendRenderedStatic = \buffer, node -> + when node is + Text content -> + Str.concat buffer content + + Element name _ attrs children -> + withTagName = "\(buffer)<\(name)" + withAttrs = + if List.isEmpty attrs then + withTagName + else + init = { buffer: Str.concat withTagName " ", styles: "" } + { buffer: attrBuffer, styles } = + List.walk attrs init appendRenderedStaticAttr + + if Str.isEmpty styles then + attrBuffer + else + "\(attrBuffer) style=\"\(styles)\"" + + withTag = Str.concat withAttrs ">" + withChildren = List.walk children withTag appendRenderedStatic + + "\(withChildren)" + + None -> buffer + +appendRenderedStaticAttr : { buffer : Str, styles : Str }, Attribute [] -> { buffer : Str, styles : Str } +appendRenderedStaticAttr = \{ buffer, styles }, attr -> + when attr is + HtmlAttr key value -> + newBuffer = "\(buffer) \(key)=\"\(value)\"" + + { buffer: newBuffer, styles } + + Style key value -> + newStyles = "\(styles) \(key): \(value);" + + { buffer, styles: newStyles } + + DomProp _ _ -> { buffer, styles } + +# ------------------------------- +# INITIALISATION +# ------------------------------- +initServerApp : App state initData, initData, Str -> Result (Html []) [InvalidDocument] where initData implements Encoding +initServerApp = \app, initData, hostJavaScript -> + initData + |> Ok + |> app.init + |> app.render + |> translateStatic + |> insertRocScript initData app.wasmUrl hostJavaScript + +insertRocScript : Html [], initData, Str, Str -> Result (Html []) [InvalidDocument] where initData implements Encoding +insertRocScript = \document, initData, wasmUrl, hostJavaScript -> + encode = + \value -> + value + |> Encode.toBytes TotallyNotJson.json + |> Str.fromUtf8 + |> Result.withDefault "" + + # Convert initData to JSON as a Roc Str, then convert the Roc Str to a JS string. + # JSON won't have invalid UTF-8 in it, since it would be escaped as part of JSON encoding. + jsInitData = + initData |> encode |> encode + + jsWasmUrl = + encode wasmUrl + + script : Html [] + script = (element "script") [] [ + text + """ + \(hostJavaScript) + (function(){ + const initData = \(jsInitData); + const wasmUrl = \(jsWasmUrl); + window.roc = roc_init(initData, wasmUrl); + })(); + """, + ] + + # append the - - diff --git a/repl_www/public/wasi.js b/repl_www/public/wasi.js deleted file mode 100644 index f636d055c7..0000000000 --- a/repl_www/public/wasi.js +++ /dev/null @@ -1,192 +0,0 @@ -/** - * Browser implementation of the WebAssembly System Interface (WASI) - * The REPL generates an "app" from the user's Roc code and it can import from WASI - * - * We only implement writes to stdout/stderr as console.log/console.err, and proc_exit as `throw` - * The rest of the interface just consists of dummy functions with the right number of arguments - * - * The wasiLinkObject provides a reference to the app so we can write to its memory - */ - -export function getMockWasiImports(wasiLinkObject) { - const decoder = new TextDecoder(); - - // If the app ever resizes its memory, there will be a new buffer instance - // so we get a fresh reference every time, just in case - function getMemory8() { - return new Uint8Array(wasiLinkObject.instance.exports.memory.buffer); - } - - function getMemory32() { - return new Uint32Array(wasiLinkObject.instance.exports.memory.buffer); - } - - // fd_close : (i32) -> i32 - // Close a file descriptor. Note: This is similar to close in POSIX. - // https://docs.rs/wasi/latest/wasi/wasi_snapshot_preview1/fn.fd_close.html - function fd_close(fd) { - console.warn(`fd_close: ${{ fd }}`); - return 0; // error code - } - - // fd_fdstat_get : (i32, i32) -> i32 - // Get the attributes of a file descriptor. - // https://docs.rs/wasi/latest/wasi/wasi_snapshot_preview1/fn.fd_fdstat_get.html - function fd_fdstat_get(fd, stat_mut_ptr) { - /* - Tell WASI that stdout is a tty (no seek or tell) - - https://github.com/WebAssembly/wasi-libc/blob/659ff414560721b1660a19685110e484a081c3d4/libc-bottom-half/sources/isatty.c - - *Not* a tty if: - (statbuf.fs_filetype != __WASI_FILETYPE_CHARACTER_DEVICE || - (statbuf.fs_rights_base & (__WASI_RIGHTS_FD_SEEK | __WASI_RIGHTS_FD_TELL)) != 0) - - So it's sufficient to set: - .fs_filetype = __WASI_FILETYPE_CHARACTER_DEVICE - .fs_rights_base = 0 - - https://github.com/WebAssembly/wasi-libc/blob/659ff414560721b1660a19685110e484a081c3d4/libc-bottom-half/headers/public/wasi/api.h - - typedef uint8_t __wasi_filetype_t; - typedef uint16_t __wasi_fdflags_t; - typedef uint64_t __wasi_rights_t; - #define __WASI_FILETYPE_CHARACTER_DEVICE (UINT8_C(2)) - typedef struct __wasi_fdstat_t { // 24 bytes total - __wasi_filetype_t fs_filetype; // 1 byte - // 1 byte padding - __wasi_fdflags_t fs_flags; // 2 bytes - // 4 bytes padding - __wasi_rights_t fs_rights_base; // 8 bytes - __wasi_rights_t fs_rights_inheriting; // 8 bytes - } __wasi_fdstat_t; - */ - // console.warn(`fd_fdstat_get: ${{ fd, stat_mut_ptr }}`); - const WASI_FILETYPE_CHARACTER_DEVICE = 2; - const memory8 = getMemory8(); - memory8[stat_mut_ptr] = WASI_FILETYPE_CHARACTER_DEVICE; - memory8.slice(stat_mut_ptr + 1, stat_mut_ptr + 24).fill(0); - - return 0; // error code - } - - // fd_seek : (i32, i64, i32, i32) -> i32 - // Move the offset of a file descriptor. Note: This is similar to lseek in POSIX. - // https://docs.rs/wasi/latest/wasi/wasi_snapshot_preview1/fn.fd_seek.html - function fd_seek(fd, offset, whence, newoffset_mut_ptr) { - console.warn(`fd_seek: ${{ fd, offset, whence, newoffset_mut_ptr }}`); - return 0; - } - - // fd_write : (i32, i32, i32, i32) -> i32 - // Write to a file descriptor. Note: This is similar to `writev` in POSIX. - // https://docs.rs/wasi/latest/wasi/wasi_snapshot_preview1/fn.fd_write.html - function fd_write(fd, iovs_ptr, iovs_len, nwritten_mut_ptr) { - let string_buffer = ""; - let nwritten = 0; - const memory32 = getMemory32(); - - for (let i = 0; i < iovs_len; i++) { - const index32 = iovs_ptr >> 2; - const base = memory32[index32]; - const len = memory32[index32 + 1]; - iovs_ptr += 8; - - if (!len) continue; - - nwritten += len; - - // For some reason we often get negative-looking buffer lengths with junk data. - // Just skip the console.log, but still increase nwritten or it will loop forever. - // Dunno why this happens, but it's working fine for printf debugging ¯\_(ツ)_/¯ - if (len >> 31) { - break; - } - - const buf = getMemory8().slice(base, base + len); - const chunk = decoder.decode(buf); - string_buffer += chunk; - } - memory32[nwritten_mut_ptr >> 2] = nwritten; - if (string_buffer) { - console.log(string_buffer); - } - return 0; - } - - // proc_exit : (i32) -> nil - function proc_exit(exit_code) { - if (exit_code) { - throw new Error(`Wasm exited with code ${exit_code}`); - } - } - - // Signatures from wasm_test_platform.o - const sig2 = (i32) => {}; - const sig6 = (i32a, i32b) => 0; - const sig7 = (i32a, i32b, i32c) => 0; - const sig9 = (i32a, i64b, i32c) => 0; - const sig10 = (i32a, i64b, i64c, i32d) => 0; - const sig11 = (i32a, i64b, i64c) => 0; - const sig12 = (i32a) => 0; - const sig13 = (i32a, i64b) => 0; - const sig14 = (i32a, i32b, i32c, i64d, i32e) => 0; - const sig15 = (i32a, i32b, i32c, i32d) => 0; - const sig16 = (i32a, i64b, i32c, i32d) => 0; - const sig17 = (i32a, i32b, i32c, i32d, i32e) => 0; - const sig18 = (i32a, i32b, i32c, i32d, i64e, i64f, i32g) => 0; - const sig19 = (i32a, i32b, i32c, i32d, i32e, i32f, i32g) => 0; - const sig20 = (i32a, i32b, i32c, i32d, i32e, i64f, i64g, i32h, i32i) => 0; - const sig21 = (i32a, i32b, i32c, i32d, i32e, i32f) => 0; - const sig22 = () => 0; - - return { - wasi_snapshot_preview1: { - args_get: sig6, - args_sizes_get: sig6, - environ_get: sig6, - environ_sizes_get: sig6, - clock_res_get: sig6, - clock_time_get: sig9, - fd_advise: sig10, - fd_allocate: sig11, - fd_close, - fd_datasync: sig12, - fd_fdstat_get, - fd_fdstat_set_flags: sig6, - fd_fdstat_set_rights: sig11, - fd_filestat_get: sig6, - fd_filestat_set_size: sig13, - fd_filestat_set_times: sig10, - fd_pread: sig14, - fd_prestat_get: sig6, - fd_prestat_dir_name: sig7, - fd_pwrite: sig14, - fd_read: sig15, - fd_readdir: sig14, - fd_renumber: sig6, - fd_seek, - fd_sync: sig12, - fd_tell: sig6, - fd_write, - path_create_directory: sig7, - path_filestat_get: sig17, - path_filestat_set_times: sig18, - path_link: sig19, - path_open: sig20, - path_readlink: sig21, - path_remove_directory: sig7, - path_rename: sig21, - path_symlink: sig17, - path_unlink_file: sig7, - poll_oneoff: sig15, - proc_exit, - proc_raise: sig12, - sched_yield: sig22, - random_get: sig6, - sock_recv: sig21, - sock_send: sig17, - sock_shutdown: sig6, - }, - }; -} diff --git a/reporting/Cargo.toml b/reporting/Cargo.toml deleted file mode 100644 index 3261d6ecd9..0000000000 --- a/reporting/Cargo.toml +++ /dev/null @@ -1,32 +0,0 @@ -[package] -name = "roc_reporting" -version = "0.1.0" -authors = ["The Roc Contributors"] -license = "UPL-1.0" -edition = "2021" - -[dependencies] -roc_collections = { path = "../compiler/collections" } -roc_exhaustive = { path = "../compiler/exhaustive" } -roc_region = { path = "../compiler/region" } -roc_module = { path = "../compiler/module" } -roc_parse = { path = "../compiler/parse" } -roc_problem = { path = "../compiler/problem" } -roc_types = { path = "../compiler/types" } -roc_can = { path = "../compiler/can" } -roc_solve = { path = "../compiler/solve" } -roc_std = { path = "../roc_std" } -ven_pretty = { path = "../vendor/pretty" } -distance = "0.4.0" -bumpalo = { version = "3.8.0", features = ["collections"] } - -[dev-dependencies] -roc_constrain = { path = "../compiler/constrain" } -roc_builtins = { path = "../compiler/builtins" } -roc_load = { path = "../compiler/load" } -roc_problem = { path = "../compiler/problem" } -roc_parse = { path = "../compiler/parse" } -roc_target = { path = "../compiler/roc_target" } -roc_test_utils = { path = "../test_utils" } -pretty_assertions = "1.0.0" -indoc = "1.0.3" diff --git a/reporting/src/error/canonicalize.rs b/reporting/src/error/canonicalize.rs deleted file mode 100644 index da17170cad..0000000000 --- a/reporting/src/error/canonicalize.rs +++ /dev/null @@ -1,1873 +0,0 @@ -use roc_collections::all::MutSet; -use roc_module::ident::{Ident, Lowercase, ModuleName}; -use roc_module::symbol::BUILTIN_ABILITIES; -use roc_problem::can::PrecedenceProblem::BothNonAssociative; -use roc_problem::can::{ - BadPattern, ExtensionTypeKind, FloatErrorKind, IntErrorKind, Problem, RuntimeError, ShadowKind, -}; -use roc_region::all::{LineColumn, LineColumnRegion, LineInfo, Loc, Region}; -use roc_types::types::AliasKind; -use std::path::PathBuf; - -use crate::error::r#type::suggest; -use crate::report::{Annotation, Report, RocDocAllocator, RocDocBuilder, Severity}; -use ven_pretty::DocAllocator; - -const SYNTAX_PROBLEM: &str = "SYNTAX PROBLEM"; -const NAMING_PROBLEM: &str = "NAMING PROBLEM"; -const UNRECOGNIZED_NAME: &str = "UNRECOGNIZED NAME"; -const UNUSED_DEF: &str = "UNUSED DEFINITION"; -const UNUSED_IMPORT: &str = "UNUSED IMPORT"; -const UNUSED_ALIAS_PARAM: &str = "UNUSED TYPE ALIAS PARAMETER"; -const UNBOUND_TYPE_VARIABLE: &str = "UNBOUND TYPE VARIABLE"; -const UNUSED_ARG: &str = "UNUSED ARGUMENT"; -const MISSING_DEFINITION: &str = "MISSING DEFINITION"; -const UNKNOWN_GENERATES_WITH: &str = "UNKNOWN GENERATES FUNCTION"; -const DUPLICATE_FIELD_NAME: &str = "DUPLICATE FIELD NAME"; -const DUPLICATE_TAG_NAME: &str = "DUPLICATE TAG NAME"; -const INVALID_UNICODE: &str = "INVALID UNICODE"; -pub const CIRCULAR_DEF: &str = "CIRCULAR DEFINITION"; -const DUPLICATE_NAME: &str = "DUPLICATE NAME"; -const VALUE_NOT_EXPOSED: &str = "NOT EXPOSED"; -const MODULE_NOT_IMPORTED: &str = "MODULE NOT IMPORTED"; -const NESTED_DATATYPE: &str = "NESTED DATATYPE"; -const CONFLICTING_NUMBER_SUFFIX: &str = "CONFLICTING NUMBER SUFFIX"; -const NUMBER_OVERFLOWS_SUFFIX: &str = "NUMBER OVERFLOWS SUFFIX"; -const NUMBER_UNDERFLOWS_SUFFIX: &str = "NUMBER UNDERFLOWS SUFFIX"; -const OPAQUE_NOT_DEFINED: &str = "OPAQUE TYPE NOT DEFINED"; -const OPAQUE_DECLARED_OUTSIDE_SCOPE: &str = "OPAQUE TYPE DECLARED OUTSIDE SCOPE"; -const OPAQUE_NOT_APPLIED: &str = "OPAQUE TYPE NOT APPLIED"; -const OPAQUE_OVER_APPLIED: &str = "OPAQUE TYPE APPLIED TO TOO MANY ARGS"; -const INVALID_EXTENSION_TYPE: &str = "INVALID_EXTENSION_TYPE"; -const ABILITY_HAS_TYPE_VARIABLES: &str = "ABILITY HAS TYPE VARIABLES"; -const HAS_CLAUSE_IS_NOT_AN_ABILITY: &str = "HAS CLAUSE IS NOT AN ABILITY"; -const ILLEGAL_HAS_CLAUSE: &str = "ILLEGAL HAS CLAUSE"; -const ABILITY_MEMBER_MISSING_HAS_CLAUSE: &str = "ABILITY MEMBER MISSING HAS CLAUSE"; -const ABILITY_MEMBER_BINDS_MULTIPLE_VARIABLES: &str = "ABILITY MEMBER BINDS MULTIPLE VARIABLES"; -const ABILITY_NOT_ON_TOPLEVEL: &str = "ABILITY NOT ON TOP-LEVEL"; -const SPECIALIZATION_NOT_ON_TOPLEVEL: &str = "SPECIALIZATION NOT ON TOP-LEVEL"; -const ABILITY_USED_AS_TYPE: &str = "ABILITY USED AS TYPE"; -const ILLEGAL_DERIVE: &str = "ILLEGAL DERIVE"; - -pub fn can_problem<'b>( - alloc: &'b RocDocAllocator<'b>, - lines: &LineInfo, - filename: PathBuf, - problem: Problem, -) -> Report<'b> { - let doc; - let title; - let severity; - - match problem { - Problem::UnusedDef(symbol, region) => { - let line = - r#" then remove it so future readers of your code don't wonder why it is there."#; - - doc = alloc.stack([ - alloc - .symbol_unqualified(symbol) - .append(alloc.reflow(" is not used anywhere in your code.")), - alloc.region(lines.convert_region(region)), - alloc - .reflow("If you didn't intend on using ") - .append(alloc.symbol_unqualified(symbol)) - .append(alloc.reflow(line)), - ]); - - title = UNUSED_DEF.to_string(); - severity = Severity::Warning; - } - Problem::UnusedImport(module_id, region) => { - doc = alloc.stack([ - alloc.concat([ - alloc.reflow("Nothing from "), - alloc.module(module_id), - alloc.reflow(" is used in this module."), - ]), - alloc.region(lines.convert_region(region)), - alloc.concat([ - alloc.reflow("Since "), - alloc.module(module_id), - alloc.reflow(" isn't used, you don't need to import it."), - ]), - ]); - - title = UNUSED_IMPORT.to_string(); - severity = Severity::Warning; - } - Problem::ExposedButNotDefined(symbol) => { - doc = alloc.stack([ - alloc.symbol_unqualified(symbol).append( - alloc.reflow(" is listed as exposed, but it isn't defined in this module."), - ), - alloc - .reflow("You can fix this by adding a definition for ") - .append(alloc.symbol_unqualified(symbol)) - .append(alloc.reflow(", or by removing it from ")) - .append(alloc.keyword("exposes")) - .append(alloc.reflow(".")), - ]); - - title = MISSING_DEFINITION.to_string(); - severity = Severity::RuntimeError; - } - Problem::UnknownGeneratesWith(loc_ident) => { - doc = alloc.stack([ - alloc - .reflow("I don't know how to generate the ") - .append(alloc.ident(loc_ident.value)) - .append(alloc.reflow(" function.")), - alloc.region(lines.convert_region(loc_ident.region)), - alloc - .reflow("Only specific functions like `after` and `map` can be generated.") - .append(alloc.reflow("Learn more about hosted modules at TODO.")), - ]); - - title = UNKNOWN_GENERATES_WITH.to_string(); - severity = Severity::RuntimeError; - } - Problem::UnusedArgument(closure_symbol, argument_symbol, region) => { - let line = "\". Adding an underscore at the start of a variable name is a way of saying that the variable is not used."; - - doc = alloc.stack([ - alloc.concat([ - alloc.symbol_unqualified(closure_symbol), - alloc.reflow(" doesn't use "), - alloc.symbol_unqualified(argument_symbol), - alloc.text("."), - ]), - alloc.region(lines.convert_region(region)), - alloc.concat([ - alloc.reflow("If you don't need "), - alloc.symbol_unqualified(argument_symbol), - alloc.reflow(", then you can just remove it. However, if you really do need "), - alloc.symbol_unqualified(argument_symbol), - alloc.reflow(" as an argument of "), - alloc.symbol_unqualified(closure_symbol), - alloc.reflow(", prefix it with an underscore, like this: \"_"), - alloc.symbol_unqualified(argument_symbol), - alloc.reflow(line), - ]), - ]); - - title = UNUSED_ARG.to_string(); - severity = Severity::Warning; - } - Problem::PrecedenceProblem(BothNonAssociative(region, left_bin_op, right_bin_op)) => { - doc = alloc.stack([ - if left_bin_op.value == right_bin_op.value { - alloc.concat([ - alloc.reflow("Using more than one "), - alloc.binop(left_bin_op.value), - alloc.reflow(concat!( - " like this requires parentheses,", - " to clarify how things should be grouped.", - )), - ]) - } else { - alloc.concat([ - alloc.reflow("Using "), - alloc.binop(left_bin_op.value), - alloc.reflow(" and "), - alloc.binop(right_bin_op.value), - alloc.reflow(concat!( - " together requires parentheses, ", - "to clarify how they should be grouped." - )), - ]) - }, - alloc.region(lines.convert_region(region)), - ]); - - title = SYNTAX_PROBLEM.to_string(); - severity = Severity::RuntimeError; - } - Problem::UnsupportedPattern(BadPattern::UnderscoreInDef, region) => { - doc = alloc.stack([ - alloc.reflow("Underscore patterns are not allowed in definitions"), - alloc.region(lines.convert_region(region)), - ]); - - title = SYNTAX_PROBLEM.to_string(); - severity = Severity::RuntimeError; - } - Problem::UnsupportedPattern(BadPattern::Unsupported(pattern_type), region) => { - use roc_parse::pattern::PatternType::*; - - let this_thing = match pattern_type { - TopLevelDef => "a top-level definition:", - DefExpr => "a value definition:", - FunctionArg => "function arguments:", - WhenBranch => unreachable!("all patterns are allowed in a When"), - }; - - let suggestion = [ - alloc.reflow( - "Patterns like this don't cover all possible shapes of the input type. Use a ", - ), - alloc.keyword("when"), - alloc.reflow(" ... "), - alloc.keyword("is"), - alloc.reflow(" instead."), - ]; - - doc = alloc.stack([ - alloc - .reflow("This pattern is not allowed in ") - .append(alloc.reflow(this_thing)), - alloc.region(lines.convert_region(region)), - alloc.concat(suggestion), - ]); - - title = SYNTAX_PROBLEM.to_string(); - severity = Severity::RuntimeError; - } - Problem::Shadowing { - original_region, - shadow, - kind, - } => { - doc = report_shadowing(alloc, lines, original_region, shadow, kind); - - title = DUPLICATE_NAME.to_string(); - severity = Severity::RuntimeError; - } - Problem::CyclicAlias(symbol, region, others) => { - let answer = crate::error::r#type::cyclic_alias(alloc, lines, symbol, region, others); - - doc = answer.0; - title = answer.1; - severity = Severity::RuntimeError; - } - Problem::PhantomTypeArgument { - typ: alias, - variable_region, - variable_name, - } => { - doc = alloc.stack([ - alloc.concat([ - alloc.reflow("The "), - alloc.type_variable(variable_name), - alloc.reflow(" type parameter is not used in the "), - alloc.symbol_unqualified(alias), - alloc.reflow(" alias definition:"), - ]), - alloc.region(lines.convert_region(variable_region)), - alloc.reflow("Roc does not allow unused type alias parameters!"), - // TODO add link to this guide section - alloc.tip().append(alloc.reflow( - "If you want an unused type parameter (a so-called \"phantom type\"), \ - read the guide section on phantom values.", - )), - ]); - - title = UNUSED_ALIAS_PARAM.to_string(); - severity = Severity::RuntimeError; - } - Problem::UnboundTypeVariable { - typ: alias, - num_unbound, - one_occurrence, - kind, - } => { - let mut stack = Vec::with_capacity(4); - if num_unbound == 1 { - stack.push(alloc.concat([ - alloc.reflow("The definition of "), - alloc.symbol_unqualified(alias), - alloc.reflow(" has an unbound type variable:"), - ])); - } else { - stack.push(alloc.concat([ - alloc.reflow("The definition of "), - alloc.symbol_unqualified(alias), - alloc.reflow(" has "), - alloc.text(format!("{}", num_unbound)), - alloc.reflow(" unbound type variables."), - ])); - stack.push(alloc.reflow("Here is one occurrence:")); - } - stack.push(alloc.region(lines.convert_region(one_occurrence))); - stack.push(alloc.tip().append(alloc.concat([ - alloc.reflow("Type variables must be bound before the "), - alloc.keyword(match kind { - AliasKind::Structural => ":", - AliasKind::Opaque => ":=", - }), - alloc.reflow(". Perhaps you intended to add a type parameter to this type?"), - ]))); - doc = alloc.stack(stack); - - title = UNBOUND_TYPE_VARIABLE.to_string(); - severity = Severity::RuntimeError; - } - Problem::BadRecursion(entries) => { - doc = to_circular_def_doc(alloc, lines, &entries); - title = CIRCULAR_DEF.to_string(); - severity = Severity::RuntimeError; - } - Problem::DuplicateRecordFieldValue { - field_name, - field_region, - record_region, - replaced_region, - } => { - doc = alloc.stack([ - alloc.concat([ - alloc.reflow("This record defines the "), - alloc.record_field(field_name.clone()), - alloc.reflow(" field twice!"), - ]), - alloc.region_all_the_things( - lines.convert_region(record_region), - lines.convert_region(replaced_region), - lines.convert_region(field_region), - Annotation::Error, - ), - alloc.reflow(r"In the rest of the program, I will only use the latter definition:"), - alloc.region_all_the_things( - lines.convert_region(record_region), - lines.convert_region(field_region), - lines.convert_region(field_region), - Annotation::TypoSuggestion, - ), - alloc.concat([ - alloc.reflow("For clarity, remove the previous "), - alloc.record_field(field_name), - alloc.reflow(" definitions from this record."), - ]), - ]); - - title = DUPLICATE_FIELD_NAME.to_string(); - severity = Severity::Warning; - } - Problem::InvalidOptionalValue { - field_name, - field_region, - record_region, - } => { - return to_invalid_optional_value_report( - alloc, - lines, - filename, - field_name, - field_region, - record_region, - ); - } - Problem::DuplicateRecordFieldType { - field_name, - field_region, - record_region, - replaced_region, - } => { - doc = alloc.stack([ - alloc.concat([ - alloc.reflow("This record type defines the "), - alloc.record_field(field_name.clone()), - alloc.reflow(" field twice!"), - ]), - alloc.region_all_the_things( - lines.convert_region(record_region), - lines.convert_region(replaced_region), - lines.convert_region(field_region), - Annotation::Error, - ), - alloc.reflow("In the rest of the program, I will only use the latter definition:"), - alloc.region_all_the_things( - lines.convert_region(record_region), - lines.convert_region(field_region), - lines.convert_region(field_region), - Annotation::TypoSuggestion, - ), - alloc.concat([ - alloc.reflow("For clarity, remove the previous "), - alloc.record_field(field_name), - alloc.reflow(" definitions from this record type."), - ]), - ]); - - title = DUPLICATE_FIELD_NAME.to_string(); - severity = Severity::Warning; - } - Problem::DuplicateTag { - tag_name, - tag_union_region, - tag_region, - replaced_region, - } => { - doc = alloc.stack([ - alloc.concat([ - alloc.reflow("This tag union type defines the "), - alloc.tag_name(tag_name.clone()), - alloc.reflow(" tag twice!"), - ]), - alloc.region_all_the_things( - lines.convert_region(tag_union_region), - lines.convert_region(replaced_region), - lines.convert_region(tag_region), - Annotation::Error, - ), - alloc.reflow("In the rest of the program, I will only use the latter definition:"), - alloc.region_all_the_things( - lines.convert_region(tag_union_region), - lines.convert_region(tag_region), - lines.convert_region(tag_region), - Annotation::TypoSuggestion, - ), - alloc.concat([ - alloc.reflow("For clarity, remove the previous "), - alloc.tag_name(tag_name), - alloc.reflow(" definitions from this tag union type."), - ]), - ]); - - title = DUPLICATE_TAG_NAME.to_string(); - severity = Severity::Warning; - } - Problem::SignatureDefMismatch { - ref annotation_pattern, - ref def_pattern, - } => { - doc = alloc.stack([ - alloc.reflow( - "This annotation does not match the definition immediately following it:", - ), - alloc.region( - lines.convert_region(Region::span_across(annotation_pattern, def_pattern)), - ), - alloc.reflow("Is it a typo? If not, put either a newline or comment between them."), - ]); - - title = NAMING_PROBLEM.to_string(); - severity = Severity::RuntimeError; - } - Problem::InvalidAliasRigid { - alias_name: type_name, - region, - } => { - doc = alloc.stack([ - alloc.concat([ - alloc.reflow("This pattern in the definition of "), - alloc.symbol_unqualified(type_name), - alloc.reflow(" is not what I expect:"), - ]), - alloc.region(lines.convert_region(region)), - alloc.concat([ - alloc.reflow("Only type variables like "), - alloc.type_variable("a".into()), - alloc.reflow(" or "), - alloc.type_variable("value".into()), - alloc.reflow(" can occur in this position."), - ]), - ]); - - title = SYNTAX_PROBLEM.to_string(); - severity = Severity::RuntimeError; - } - Problem::InvalidHexadecimal(region) => { - doc = alloc.stack([ - alloc.reflow("This unicode code point is invalid:"), - alloc.region(lines.convert_region(region)), - alloc.concat([ - alloc.reflow(r"I was expecting a hexadecimal number, like "), - alloc.parser_suggestion("\\u(1100)"), - alloc.reflow(" or "), - alloc.parser_suggestion("\\u(00FF)"), - alloc.text("."), - ]), - alloc.reflow(r"Learn more about working with unicode in roc at TODO"), - ]); - - title = INVALID_UNICODE.to_string(); - severity = Severity::RuntimeError; - } - Problem::InvalidUnicodeCodePt(region) => { - doc = alloc.stack([ - alloc.reflow("This unicode code point is invalid:"), - alloc.region(lines.convert_region(region)), - alloc.reflow("Learn more about working with unicode in roc at TODO"), - ]); - - title = INVALID_UNICODE.to_string(); - severity = Severity::RuntimeError; - } - Problem::InvalidInterpolation(region) => { - doc = alloc.stack([ - alloc.reflow("This string interpolation is invalid:"), - alloc.region(lines.convert_region(region)), - alloc.concat([ - alloc.reflow(r"I was expecting an identifier, like "), - alloc.parser_suggestion("\\u(message)"), - alloc.reflow(" or "), - alloc.parser_suggestion("\\u(LoremIpsum.text)"), - alloc.text("."), - ]), - alloc.reflow(r"Learn more about string interpolation at TODO"), - ]); - - title = SYNTAX_PROBLEM.to_string(); - severity = Severity::RuntimeError; - } - Problem::RuntimeError(runtime_error) => { - let answer = pretty_runtime_error(alloc, lines, runtime_error); - - doc = answer.0; - title = answer.1.to_string(); - severity = Severity::RuntimeError; - } - Problem::NestedDatatype { - alias, - def_region, - differing_recursion_region, - } => { - doc = alloc.stack([ - alloc.concat([ - alloc.symbol_unqualified(alias), - alloc.reflow(" is a nested datatype. Here is one recursive usage of it:"), - ]), - alloc.region(lines.convert_region(differing_recursion_region)), - alloc.concat([ - alloc.reflow("But recursive usages of "), - alloc.symbol_unqualified(alias), - alloc.reflow(" must match its definition:"), - ]), - alloc.region(lines.convert_region(def_region)), - alloc.reflow("Nested datatypes are not supported in Roc."), - alloc.concat([ - alloc.hint("Consider rewriting the definition of "), - alloc.symbol_unqualified(alias), - alloc.text(" to use the recursive type with the same arguments."), - ]), - ]); - - title = NESTED_DATATYPE.to_string(); - severity = Severity::RuntimeError; - } - - Problem::InvalidExtensionType { region, kind } => { - let (kind_str, can_only_contain) = match kind { - ExtensionTypeKind::Record => ("record", "a type variable or another record"), - ExtensionTypeKind::TagUnion => { - ("tag union", "a type variable or another tag union") - } - }; - - doc = alloc.stack([ - alloc.concat([ - alloc.reflow("This "), - alloc.text(kind_str), - alloc.reflow(" extension type is invalid:"), - ]), - alloc.region(lines.convert_region(region)), - alloc.concat([ - alloc.note("A "), - alloc.reflow(kind_str), - alloc.reflow(" extension variable can only contain "), - alloc.reflow(can_only_contain), - alloc.reflow("."), - ]), - ]); - - title = INVALID_EXTENSION_TYPE.to_string(); - severity = Severity::RuntimeError; - } - - Problem::AbilityHasTypeVariables { - name, - variables_region, - } => { - doc = alloc.stack([ - alloc.concat([ - alloc.reflow("The definition of the "), - alloc.symbol_unqualified(name), - alloc.reflow(" ability includes type variables:"), - ]), - alloc.region(lines.convert_region(variables_region)), - alloc.reflow( - "Abilities cannot depend on type variables, but their member values can!", - ), - ]); - title = ABILITY_HAS_TYPE_VARIABLES.to_string(); - severity = Severity::RuntimeError; - } - - Problem::HasClauseIsNotAbility { - region: clause_region, - } => { - doc = alloc.stack([ - alloc.reflow(r#"The type referenced in this "has" clause is not an ability:"#), - alloc.region(lines.convert_region(clause_region)), - ]); - title = HAS_CLAUSE_IS_NOT_AN_ABILITY.to_string(); - severity = Severity::RuntimeError; - } - - Problem::IllegalHasClause { region } => { - doc = alloc.stack([ - alloc.concat([ - alloc.reflow("A "), - alloc.keyword("has"), - alloc.reflow(" clause is not allowed here:"), - ]), - alloc.region(lines.convert_region(region)), - alloc.concat([ - alloc.keyword("has"), - alloc.reflow( - " clauses can only be specified on the top-level type annotations.", - ), - ]), - ]); - title = ILLEGAL_HAS_CLAUSE.to_string(); - severity = Severity::RuntimeError; - } - - Problem::AbilityMemberMissingHasClause { - member, - ability, - region, - } => { - doc = alloc.stack([ - alloc.concat([ - alloc.reflow("The definition of the ability member "), - alloc.symbol_unqualified(member), - alloc.reflow(" does not include a "), - alloc.keyword("has"), - alloc.reflow(" clause binding a type variable to the ability "), - alloc.symbol_unqualified(ability), - alloc.reflow(":"), - ]), - alloc.region(lines.convert_region(region)), - alloc.concat([ - alloc.reflow("Ability members must include a "), - alloc.keyword("has"), - alloc.reflow(" clause binding a type variable to an ability, like"), - ]), - alloc.type_block(alloc.concat([ - alloc.type_variable("a".into()), - alloc.space(), - alloc.keyword("has"), - alloc.space(), - alloc.symbol_unqualified(ability), - ])), - alloc.concat([alloc - .reflow("Otherwise, the function does not need to be part of the ability!")]), - ]); - title = ABILITY_MEMBER_MISSING_HAS_CLAUSE.to_string(); - severity = Severity::RuntimeError; - } - - Problem::AbilityMemberMultipleBoundVars { - member, - ability, - span_has_clauses, - mut bound_var_names, - } => { - doc = alloc.stack([ - alloc.concat([ - alloc.reflow("The definition of the ability member "), - alloc.symbol_unqualified(member), - alloc.reflow(" includes multiple variables bound to the "), - alloc.symbol_unqualified(ability), - alloc.keyword(" ability:"), - ]), - alloc.region(lines.convert_region(span_has_clauses)), - alloc.reflow("Ability members can only bind one type variable to their parent ability. Otherwise, I wouldn't know what type implements an ability by looking at specializations!"), - alloc.concat([ - alloc.hint("Did you mean to only bind "), - alloc.type_variable(bound_var_names.swap_remove(0)), - alloc.reflow(" to "), - alloc.symbol_unqualified(ability), - alloc.reflow("?"), - ]) - ]); - title = ABILITY_MEMBER_BINDS_MULTIPLE_VARIABLES.to_string(); - severity = Severity::RuntimeError; - } - - Problem::AbilityNotOnToplevel { region } => { - doc = alloc.stack([ - alloc - .concat([alloc - .reflow("This ability definition is not on the top-level of a module:")]), - alloc.region(lines.convert_region(region)), - alloc.reflow("Abilities can only be defined on the top-level of a Roc module."), - ]); - title = ABILITY_NOT_ON_TOPLEVEL.to_string(); - severity = Severity::RuntimeError; - } - - Problem::AbilityUsedAsType(suggested_var_name, ability, region) => { - doc = alloc.stack([ - alloc.concat([ - alloc.reflow("You are attempting to use the ability "), - alloc.symbol_unqualified(ability), - alloc.reflow(" as a type directly:"), - ]), - alloc.region(lines.convert_region(region)), - alloc.reflow( - "Abilities can only be used in type annotations to constrain type variables.", - ), - alloc - .hint("") - .append(alloc.reflow("Perhaps you meant to include a ")) - .append(alloc.keyword("has")) - .append(alloc.reflow(" annotation, like")), - alloc.type_block(alloc.concat([ - alloc.type_variable(suggested_var_name), - alloc.space(), - alloc.keyword("has"), - alloc.space(), - alloc.symbol_unqualified(ability), - ])), - ]); - title = ABILITY_USED_AS_TYPE.to_string(); - severity = Severity::RuntimeError; - } - Problem::NestedSpecialization(member, region) => { - doc = alloc.stack([ - alloc.concat([ - alloc.reflow("This specialization of the "), - alloc.symbol_unqualified(member), - alloc.reflow(" ability member is in a nested scope:"), - ]), - alloc.region(lines.convert_region(region)), - alloc.reflow("Specializations can only be defined on the top-level of a module."), - ]); - title = SPECIALIZATION_NOT_ON_TOPLEVEL.to_string(); - severity = Severity::Warning; - } - Problem::IllegalDerive(region) => { - doc = alloc.stack([ - alloc.reflow("This ability cannot be derived:"), - alloc.region(lines.convert_region(region)), - alloc.reflow("Only builtin abilities can be derived."), - alloc - .note("The builtin abilities are ") - .append(list_builtin_abilities(alloc)), - ]); - title = ILLEGAL_DERIVE.to_string(); - severity = Severity::Warning; - } - }; - - Report { - title, - filename, - doc, - severity, - } -} - -fn list_builtin_abilities<'a>(alloc: &'a RocDocAllocator<'a>) -> RocDocBuilder<'a> { - let doc = alloc.concat([alloc.symbol_qualified(BUILTIN_ABILITIES[0])]); - debug_assert!(BUILTIN_ABILITIES.len() == 1); - doc -} - -fn to_invalid_optional_value_report<'b>( - alloc: &'b RocDocAllocator<'b>, - lines: &LineInfo, - filename: PathBuf, - field_name: Lowercase, - field_region: Region, - record_region: Region, -) -> Report<'b> { - let doc = to_invalid_optional_value_report_help( - alloc, - lines, - field_name, - field_region, - record_region, - ); - - Report { - title: "BAD OPTIONAL VALUE".to_string(), - filename, - doc, - severity: Severity::RuntimeError, - } -} - -fn to_invalid_optional_value_report_help<'b>( - alloc: &'b RocDocAllocator<'b>, - lines: &LineInfo, - field_name: Lowercase, - field_region: Region, - record_region: Region, -) -> RocDocBuilder<'b> { - alloc.stack([ - alloc.concat([ - alloc.reflow("This record uses an optional value for the "), - alloc.record_field(field_name), - alloc.reflow(" field in an incorrect context!"), - ]), - alloc.region_all_the_things( - lines.convert_region(record_region), - lines.convert_region(field_region), - lines.convert_region(field_region), - Annotation::Error, - ), - alloc.reflow(r"You can only use optional values in record destructuring, like:"), - alloc - .reflow(r"{ answer ? 42, otherField } = myRecord") - .indent(4), - ]) -} - -fn to_bad_ident_expr_report<'b>( - alloc: &'b RocDocAllocator<'b>, - lines: &LineInfo, - bad_ident: roc_parse::ident::BadIdent, - surroundings: Region, -) -> RocDocBuilder<'b> { - use roc_parse::ident::BadIdent::*; - - match bad_ident { - Start(_) | Space(_, _) => unreachable!("these are handled in the parser"), - WeirdDotAccess(pos) | StrayDot(pos) => { - let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); - - alloc.stack([ - alloc.reflow(r"I trying to parse a record field access here:"), - alloc.region_with_subregion(lines.convert_region(surroundings), region), - alloc.concat([ - alloc.reflow("So I expect to see a lowercase letter next, like "), - alloc.parser_suggestion(".name"), - alloc.reflow(" or "), - alloc.parser_suggestion(".height"), - alloc.reflow("."), - ]), - ]) - } - - WeirdAccessor(_pos) => alloc.stack([ - alloc.reflow("I am very confused by this field access"), - alloc.region(lines.convert_region(surroundings)), - alloc.concat([ - alloc.reflow("It looks like a field access on an accessor. I parse"), - alloc.parser_suggestion(".client.name"), - alloc.reflow(" as "), - alloc.parser_suggestion("(.client).name"), - alloc.reflow(". Maybe use an anonymous function like "), - alloc.parser_suggestion("(\\r -> r.client.name)"), - alloc.reflow(" instead"), - alloc.reflow("?"), - ]), - ]), - - WeirdDotQualified(pos) => { - let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); - - alloc.stack([ - alloc.reflow("I am trying to parse a qualified name here:"), - alloc.region_with_subregion(lines.convert_region(surroundings), region), - alloc.concat([ - alloc.reflow("I was expecting to see an identifier next, like "), - alloc.parser_suggestion("height"), - alloc.reflow(". A complete qualified name looks something like "), - alloc.parser_suggestion("Json.Decode.string"), - alloc.text("."), - ]), - ]) - } - QualifiedTag(pos) => { - let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); - - alloc.stack([ - alloc.reflow("I am trying to parse a qualified name here:"), - alloc.region_with_subregion(lines.convert_region(surroundings), region), - alloc.concat([ - alloc.reflow(r"This looks like a qualified tag name to me, "), - alloc.reflow(r"but tags cannot be qualified! "), - alloc.reflow(r"Maybe you wanted a qualified name, something like "), - alloc.parser_suggestion("Json.Decode.string"), - alloc.text("?"), - ]), - ]) - } - - Underscore(pos) => { - let region = Region::new(surroundings.start(), pos); - alloc.stack([ - alloc.reflow("Underscores are not allowed in identifier names:"), - alloc.region_with_subregion( - lines.convert_region(surroundings), - lines.convert_region(region), - ), - alloc.concat([alloc.reflow( - r"I recommend using camelCase, it is the standard in the Roc ecosystem.", - )]), - ]) - } - - BadOpaqueRef(pos) => { - use BadIdentNext::*; - let kind = "an opaque reference"; - - match what_is_next(alloc.src_lines, lines.convert_pos(pos)) { - LowercaseAccess(width) => { - let region = Region::new(pos, pos.bump_column(width)); - alloc.stack([ - alloc.reflow("I am very confused by this field access:"), - alloc.region_with_subregion( - lines.convert_region(surroundings), - lines.convert_region(region), - ), - alloc.concat([ - alloc.reflow(r"It looks like a record field access on "), - alloc.reflow(kind), - alloc.text("."), - ]), - ]) - } - UppercaseAccess(width) => { - let region = Region::new(pos, pos.bump_column(width)); - alloc.stack([ - alloc.reflow("I am very confused by this expression:"), - alloc.region_with_subregion( - lines.convert_region(surroundings), - lines.convert_region(region), - ), - alloc.concat([ - alloc.reflow(r"Looks like "), - alloc.reflow(kind), - alloc.reflow(" is treated like a module name. "), - alloc.reflow(r"Maybe you wanted a qualified name, like "), - alloc.parser_suggestion("Json.Decode.string"), - alloc.text("?"), - ]), - ]) - } - Other(Some(c)) if c.is_lowercase() => { - let region = - Region::new(surroundings.start().bump_column(1), pos.bump_column(1)); - alloc.stack([ - alloc.concat([ - alloc.reflow("I am trying to parse "), - alloc.reflow(kind), - alloc.reflow(" here:"), - ]), - alloc.region_with_subregion( - lines.convert_region(surroundings), - lines.convert_region(region), - ), - alloc.concat([ - alloc.reflow(r"But after the "), - alloc.keyword("@"), - alloc.reflow(r" symbol I found a lowercase letter. "), - alloc.reflow(r"All opaque references "), - alloc.reflow(r" must start with an uppercase letter, like "), - alloc.parser_suggestion("@UUID"), - alloc.reflow(" or "), - alloc.parser_suggestion("@Secrets"), - alloc.reflow("."), - ]), - ]) - } - other => todo!("{:?}", other), - } - } - } -} - -fn to_bad_ident_pattern_report<'b>( - alloc: &'b RocDocAllocator<'b>, - lines: &LineInfo, - bad_ident: roc_parse::ident::BadIdent, - surroundings: Region, -) -> RocDocBuilder<'b> { - use roc_parse::ident::BadIdent::*; - - match bad_ident { - Start(_) | Space(_, _) => unreachable!("these are handled in the parser"), - WeirdDotAccess(pos) | StrayDot(pos) => { - let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); - - alloc.stack([ - alloc.reflow(r"I trying to parse a record field accessor here:"), - alloc.region_with_subregion(lines.convert_region(surroundings), region), - alloc.concat([ - alloc.reflow("Something like "), - alloc.parser_suggestion(".name"), - alloc.reflow(" or "), - alloc.parser_suggestion(".height"), - alloc.reflow(" that accesses a value from a record."), - ]), - ]) - } - - WeirdAccessor(_pos) => alloc.stack([ - alloc.reflow("I am very confused by this field access"), - alloc.region(lines.convert_region(surroundings)), - alloc.concat([ - alloc.reflow("It looks like a field access on an accessor. I parse"), - alloc.parser_suggestion(".client.name"), - alloc.reflow(" as "), - alloc.parser_suggestion("(.client).name"), - alloc.reflow(". Maybe use an anonymous function like "), - alloc.parser_suggestion("(\\r -> r.client.name)"), - alloc.reflow(" instead"), - alloc.reflow("?"), - ]), - ]), - - WeirdDotQualified(pos) => { - let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); - - alloc.stack([ - alloc.reflow("I am trying to parse a qualified name here:"), - alloc.region_with_subregion(lines.convert_region(surroundings), region), - alloc.concat([ - alloc.reflow("I was expecting to see an identifier next, like "), - alloc.parser_suggestion("height"), - alloc.reflow(". A complete qualified name looks something like "), - alloc.parser_suggestion("Json.Decode.string"), - alloc.text("."), - ]), - ]) - } - QualifiedTag(pos) => { - let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); - - alloc.stack([ - alloc.reflow("I am trying to parse a qualified name here:"), - alloc.region_with_subregion(lines.convert_region(surroundings), region), - alloc.concat([ - alloc.reflow(r"This looks like a qualified tag name to me, "), - alloc.reflow(r"but tags cannot be qualified! "), - alloc.reflow(r"Maybe you wanted a qualified name, something like "), - alloc.parser_suggestion("Json.Decode.string"), - alloc.text("?"), - ]), - ]) - } - - Underscore(pos) => { - let region = Region::from_pos(pos.sub(1)); - - alloc.stack([ - alloc.reflow("I am trying to parse an identifier here:"), - alloc.region_with_subregion( - lines.convert_region(surroundings), - lines.convert_region(region), - ), - alloc.concat([alloc.reflow( - r"Underscores are not allowed in identifiers. Use camelCase instead!", - )]), - ]) - } - - _ => todo!(), - } -} - -#[derive(Debug)] -enum BadIdentNext<'a> { - LowercaseAccess(u32), - UppercaseAccess(u32), - NumberAccess(u32), - Keyword(&'a str), - DanglingDot, - Other(Option), -} - -fn what_is_next<'a>(source_lines: &'a [&'a str], pos: LineColumn) -> BadIdentNext<'a> { - let row_index = pos.line as usize; - let col_index = pos.column as usize; - match source_lines.get(row_index) { - None => BadIdentNext::Other(None), - Some(line) => { - let chars = &line[col_index..]; - let mut it = chars.chars(); - - match roc_parse::keyword::KEYWORDS - .iter() - .find(|keyword| crate::error::parse::starts_with_keyword(chars, keyword)) - { - Some(keyword) => BadIdentNext::Keyword(keyword), - None => match it.next() { - None => BadIdentNext::Other(None), - Some('.') => match it.next() { - Some(c) if c.is_lowercase() => { - BadIdentNext::LowercaseAccess(2 + till_whitespace(it) as u32) - } - Some(c) if c.is_uppercase() => { - BadIdentNext::UppercaseAccess(2 + till_whitespace(it) as u32) - } - Some(c) if c.is_ascii_digit() => { - BadIdentNext::NumberAccess(2 + till_whitespace(it) as u32) - } - _ => BadIdentNext::DanglingDot, - }, - Some(c) => BadIdentNext::Other(Some(c)), - }, - } - } - } -} - -fn till_whitespace(it: I) -> usize -where - I: Iterator, -{ - let mut chomped = 0; - - for c in it { - if c.is_ascii_whitespace() || c == '#' { - break; - } else { - chomped += 1; - continue; - } - } - - chomped -} - -fn report_shadowing<'b>( - alloc: &'b RocDocAllocator<'b>, - lines: &LineInfo, - original_region: Region, - shadow: Loc, - kind: ShadowKind, -) -> RocDocBuilder<'b> { - let what = match kind { - ShadowKind::Variable => "variables", - ShadowKind::Alias => "aliases", - ShadowKind::Opaque => "opaques", - ShadowKind::Ability => "abilities", - }; - - alloc.stack([ - alloc - .text("The ") - .append(alloc.ident(shadow.value)) - .append(alloc.reflow(" name is first defined here:")), - alloc.region(lines.convert_region(original_region)), - alloc.reflow("But then it's defined a second time here:"), - alloc.region(lines.convert_region(shadow.region)), - alloc.concat([ - alloc.reflow("Since these "), - alloc.reflow(what), - alloc.reflow(" have the same name, it's easy to use the wrong one on accident. Give one of them a new name."), - ]), - ]) -} - -fn pretty_runtime_error<'b>( - alloc: &'b RocDocAllocator<'b>, - lines: &LineInfo, - runtime_error: RuntimeError, -) -> (RocDocBuilder<'b>, &'static str) { - let doc; - let title; - - match runtime_error { - RuntimeError::VoidValue => { - // is used to communicate to the compiler that - // a branch is unreachable; this should never reach a user - unreachable!(""); - } - - RuntimeError::UnresolvedTypeVar | RuntimeError::ErroneousType => { - // only generated during layout generation - unreachable!(""); - } - - RuntimeError::Shadowing { - original_region, - shadow, - kind, - } => { - doc = report_shadowing(alloc, lines, original_region, shadow, kind); - title = DUPLICATE_NAME; - } - - RuntimeError::LookupNotInScope(loc_name, options) => { - doc = not_found(alloc, lines, loc_name.region, &loc_name.value, options); - title = UNRECOGNIZED_NAME; - } - RuntimeError::CircularDef(entries) => { - doc = to_circular_def_doc(alloc, lines, &entries); - title = CIRCULAR_DEF; - } - RuntimeError::MalformedPattern(problem, region) => { - use roc_parse::ast::Base; - use roc_problem::can::MalformedPatternProblem::*; - - let name = match problem { - MalformedInt => " integer ", - MalformedFloat => " float ", - MalformedBase(Base::Hex) => " hex integer ", - MalformedBase(Base::Binary) => " binary integer ", - MalformedBase(Base::Octal) => " octal integer ", - MalformedBase(Base::Decimal) => " integer ", - BadIdent(bad_ident) => { - title = NAMING_PROBLEM; - doc = to_bad_ident_pattern_report(alloc, lines, bad_ident, region); - - return (doc, title); - } - Unknown => " ", - QualifiedIdentifier => " qualified ", - EmptySingleQuote => " empty character literal ", - MultipleCharsInSingleQuote => " overfull literal ", - }; - - let tip = match problem { - MalformedInt | MalformedFloat | MalformedBase(_) => alloc - .tip() - .append(alloc.reflow("Learn more about number literals at TODO")), - EmptySingleQuote | MultipleCharsInSingleQuote | Unknown | BadIdent(_) => { - alloc.nil() - } - QualifiedIdentifier => alloc - .tip() - .append(alloc.reflow("In patterns, only tags can be qualified")), - }; - - doc = alloc.stack([ - alloc.concat([ - alloc.reflow("This"), - alloc.text(name), - alloc.reflow("pattern is malformed:"), - ]), - alloc.region(lines.convert_region(region)), - tip, - ]); - - title = SYNTAX_PROBLEM; - } - RuntimeError::UnsupportedPattern(_) => { - todo!("unsupported patterns are currently not parsed!") - } - RuntimeError::ValueNotExposed { - module_name, - ident, - region, - exposed_values, - } => { - let mut suggestions = suggest::sort(ident.as_ref(), exposed_values); - suggestions.truncate(4); - - let did_you_mean = if suggestions.is_empty() { - alloc.concat([ - alloc.reflow("In fact, it looks like "), - alloc.module_name(module_name.clone()), - alloc.reflow(" doesn't expose any values!"), - ]) - } else { - let qualified_suggestions = suggestions - .into_iter() - .map(|v| alloc.string(module_name.to_string() + "." + v.as_str())); - alloc.stack([ - alloc.reflow("Did you mean one of these?"), - alloc.vcat(qualified_suggestions).indent(4), - ]) - }; - doc = alloc.stack([ - alloc.concat([ - alloc.reflow("The "), - alloc.module_name(module_name), - alloc.reflow(" module does not expose `"), - alloc.string(ident.to_string()), - alloc.reflow("`:"), - ]), - alloc.region(lines.convert_region(region)), - did_you_mean, - ]); - - title = VALUE_NOT_EXPOSED; - } - - RuntimeError::ModuleNotImported { - module_name, - imported_modules, - region, - module_exists, - } => { - doc = module_not_found( - alloc, - lines, - region, - &module_name, - imported_modules, - module_exists, - ); - - title = MODULE_NOT_IMPORTED; - } - RuntimeError::InvalidPrecedence(_, _) => { - // do nothing, reported with PrecedenceProblem - unreachable!(); - } - RuntimeError::MalformedIdentifier(_box_str, bad_ident, surroundings) => { - doc = to_bad_ident_expr_report(alloc, lines, bad_ident, surroundings); - - title = SYNTAX_PROBLEM; - } - RuntimeError::MalformedTypeName(_box_str, surroundings) => { - doc = alloc.stack([ - alloc.reflow(r"I am confused by this type name:"), - alloc.region(lines.convert_region(surroundings)), - alloc.concat([ - alloc.reflow("Type names start with an uppercase letter, "), - alloc.reflow("and can optionally be qualified by a module name, like "), - alloc.parser_suggestion("Bool"), - alloc.reflow(" or "), - alloc.parser_suggestion("Http.Request.Request"), - alloc.reflow("."), - ]), - ]); - - title = SYNTAX_PROBLEM; - } - RuntimeError::MalformedClosure(_) => { - todo!(""); - } - RuntimeError::InvalidFloat(sign @ FloatErrorKind::PositiveInfinity, region, _raw_str) - | RuntimeError::InvalidFloat(sign @ FloatErrorKind::NegativeInfinity, region, _raw_str) => { - let tip = alloc - .tip() - .append(alloc.reflow("Learn more about number literals at TODO")); - - let big_or_small = if let FloatErrorKind::PositiveInfinity = sign { - "big" - } else { - "small" - }; - - doc = alloc.stack([ - alloc.concat([ - alloc.reflow("This float literal is too "), - alloc.text(big_or_small), - alloc.reflow(":"), - ]), - alloc.region(lines.convert_region(region)), - alloc.concat([ - alloc - .reflow("Roc uses signed 64-bit floating points, allowing values between "), - alloc.text(format!("{:e}", f64::MIN)), - alloc.reflow(" and "), - alloc.text(format!("{:e}", f64::MAX)), - ]), - tip, - ]); - - title = SYNTAX_PROBLEM; - } - RuntimeError::InvalidFloat(FloatErrorKind::Error, region, _raw_str) => { - let tip = alloc - .tip() - .append(alloc.reflow("Learn more about number literals at TODO")); - - doc = alloc.stack([ - alloc.concat([ - alloc.reflow("This float literal contains an invalid digit:"), - ]), - alloc.region(lines.convert_region(region)), - alloc.concat([ - alloc.reflow("Floating point literals can only contain the digits 0-9, or use scientific notation 10e4, or have a float suffix."), - ]), - tip, - ]); - - title = SYNTAX_PROBLEM; - } - RuntimeError::InvalidFloat(FloatErrorKind::IntSuffix, region, _raw_str) => { - doc = alloc.stack([ - alloc - .concat([alloc - .reflow("This number literal is a float, but it has an integer suffix:")]), - alloc.region(lines.convert_region(region)), - ]); - - title = CONFLICTING_NUMBER_SUFFIX; - } - RuntimeError::InvalidInt(error @ IntErrorKind::InvalidDigit, base, region, _raw_str) - | RuntimeError::InvalidInt(error @ IntErrorKind::Empty, base, region, _raw_str) => { - use roc_parse::ast::Base::*; - - let (problem, contains) = if let IntErrorKind::InvalidDigit = error { - ( - "an invalid digit", - alloc.reflow(" can only contain the digits "), - ) - } else { - ( - "no digits", - alloc.reflow(" must contain at least one of the digits "), - ) - }; - - let name = match base { - Decimal => "integer", - Octal => "octal integer", - Hex => "hex integer", - Binary => "binary integer", - }; - - let plurals = match base { - Decimal => "Integer literals", - Octal => "Octal (base-8) integer literals", - Hex => "Hexadecimal (base-16) integer literals", - Binary => "Binary (base-2) integer literals", - }; - - let charset = match base { - Decimal => "0-9", - Octal => "0-7", - Hex => "0-9, a-f and A-F", - Binary => "0 and 1", - }; - - let tip = alloc - .tip() - .append(alloc.reflow("Learn more about number literals at TODO")); - - doc = alloc.stack([ - alloc.concat([ - alloc.reflow("This "), - alloc.text(name), - alloc.reflow(" literal contains "), - alloc.text(problem), - alloc.text(":"), - ]), - alloc.region(lines.convert_region(region)), - alloc.concat([ - alloc.text(plurals), - contains, - alloc.text(charset), - alloc.text(", or have an integer suffix."), - ]), - tip, - ]); - - title = SYNTAX_PROBLEM; - } - RuntimeError::InvalidInt(error_kind @ IntErrorKind::Underflow, _base, region, _raw_str) - | RuntimeError::InvalidInt(error_kind @ IntErrorKind::Overflow, _base, region, _raw_str) => { - let (big_or_small, info) = if let IntErrorKind::Underflow = error_kind { - ( - "small", - alloc.concat([ - alloc.reflow( - "The smallest number representable in Roc is the minimum I128 value, ", - ), - alloc.int_literal(i128::MIN), - alloc.text("."), - ]), - ) - } else { - ( - "big", - alloc.concat([ - alloc.reflow( - "The largest number representable in Roc is the maximum U128 value, ", - ), - alloc.int_literal(u128::MAX), - alloc.text("."), - ]), - ) - }; - - let tip = alloc - .tip() - .append(alloc.reflow("Learn more about number literals at TODO")); - - doc = alloc.stack([ - alloc.concat([ - alloc.reflow("This integer literal is too "), - alloc.text(big_or_small), - alloc.reflow(":"), - ]), - alloc.region(lines.convert_region(region)), - info, - tip, - ]); - - title = SYNTAX_PROBLEM; - } - RuntimeError::InvalidInt(IntErrorKind::FloatSuffix, _base, region, _raw_str) => { - doc = alloc.stack([ - alloc - .concat([alloc - .reflow("This number literal is an integer, but it has a float suffix:")]), - alloc.region(lines.convert_region(region)), - ]); - - title = CONFLICTING_NUMBER_SUFFIX; - } - RuntimeError::InvalidInt( - IntErrorKind::OverflowsSuffix { - suffix_type, - max_value, - }, - _base, - region, - _raw_str, - ) => { - doc = alloc.stack([ - alloc.concat([alloc - .reflow("This integer literal overflows the type indicated by its suffix:")]), - alloc.region(lines.convert_region(region)), - alloc.tip().append(alloc.concat([ - alloc.reflow("The suffix indicates this integer is a "), - alloc.type_str(suffix_type), - alloc.reflow(", whose maximum value is "), - alloc.int_literal(max_value), - alloc.reflow("."), - ])), - ]); - - title = NUMBER_OVERFLOWS_SUFFIX; - } - RuntimeError::InvalidInt( - IntErrorKind::UnderflowsSuffix { - suffix_type, - min_value, - }, - _base, - region, - _raw_str, - ) => { - doc = alloc.stack([ - alloc.concat([alloc - .reflow("This integer literal underflows the type indicated by its suffix:")]), - alloc.region(lines.convert_region(region)), - alloc.tip().append(alloc.concat([ - alloc.reflow("The suffix indicates this integer is a "), - alloc.type_str(suffix_type), - alloc.reflow(", whose minimum value is "), - alloc.int_literal(min_value), - alloc.reflow("."), - ])), - ]); - - title = NUMBER_UNDERFLOWS_SUFFIX; - } - RuntimeError::InvalidOptionalValue { - field_name, - field_region, - record_region, - } => { - doc = to_invalid_optional_value_report_help( - alloc, - lines, - field_name, - field_region, - record_region, - ); - - title = SYNTAX_PROBLEM; - } - RuntimeError::InvalidRecordUpdate { region } => { - doc = alloc.stack([ - alloc.concat([ - alloc.reflow("This expression cannot be updated"), - alloc.reflow(":"), - ]), - alloc.region(lines.convert_region(region)), - alloc.reflow("Only variables can be updated with record update syntax."), - ]); - - title = SYNTAX_PROBLEM; - } - RuntimeError::InvalidHexadecimal(region) => { - todo!( - "TODO runtime error for an invalid hexadecimal number in a \\u(...) code point at region {:?}", - region - ); - } - RuntimeError::InvalidUnicodeCodePt(region) => { - todo!( - "TODO runtime error for an invalid \\u(...) code point at region {:?}", - region - ); - } - RuntimeError::InvalidInterpolation(region) => { - todo!( - "TODO runtime error for an invalid string interpolation at region {:?}", - region - ); - } - RuntimeError::NoImplementation | RuntimeError::NoImplementationNamed { .. } => { - todo!("no implementation, unreachable") - } - RuntimeError::NonExhaustivePattern => { - unreachable!("not currently reported (but can blow up at runtime)") - } - RuntimeError::ExposedButNotDefined(symbol) => { - doc = alloc.stack([alloc - .symbol_unqualified(symbol) - .append(alloc.reflow(" was listed as exposed in ")) - .append(alloc.module(symbol.module_id())) - .append(alloc.reflow(", but it was not defined anywhere in that module."))]); - - title = MISSING_DEFINITION; - } - RuntimeError::EmptySingleQuote(region) => { - let tip = alloc - .tip() - .append(alloc.reflow("Learn more about character literals at TODO")); - - doc = alloc.stack([ - alloc.concat([alloc.reflow("This character literal is empty.")]), - alloc.region(lines.convert_region(region)), - tip, - ]); - - title = SYNTAX_PROBLEM; - } - RuntimeError::MultipleCharsInSingleQuote(region) => { - let tip = alloc - .tip() - .append(alloc.reflow("Learn more about character literals at TODO")); - - doc = alloc.stack([ - alloc.concat([ - alloc.reflow("This character literal contains more than one code point.") - ]), - alloc.region(lines.convert_region(region)), - alloc.concat([alloc.reflow("Character literals can only contain one code point.")]), - tip, - ]); - - title = SYNTAX_PROBLEM; - } - RuntimeError::OpaqueNotDefined { - usage: - Loc { - region: used_region, - value: opaque, - }, - opaques_in_scope, - opt_defined_alias, - } => { - let mut suggestions = suggest::sort( - opaque.as_inline_str().as_str(), - opaques_in_scope.iter().map(|v| v.as_ref()).collect(), - ); - suggestions.truncate(4); - - let details = if suggestions.is_empty() { - alloc.note("It looks like there are no opaque types declared in this scope yet!") - } else { - let qualified_suggestions = - suggestions.into_iter().map(|v| alloc.string(v.to_string())); - alloc.stack([ - alloc - .tip() - .append(alloc.reflow("Did you mean one of these opaque types?")), - alloc.vcat(qualified_suggestions).indent(4), - ]) - }; - - let mut stack = vec![ - alloc.concat([ - alloc.reflow("The opaque type "), - alloc.type_str(opaque.as_inline_str().as_str()), - alloc.reflow(" referenced here is not defined:"), - ]), - alloc.region(lines.convert_region(used_region)), - ]; - - if let Some(defined_alias_region) = opt_defined_alias { - stack.push(alloc.stack([ - alloc.note("There is an alias of the same name:"), - alloc.region(lines.convert_region(defined_alias_region)), - ])); - } - - stack.push(details); - - doc = alloc.stack(stack); - - title = OPAQUE_NOT_DEFINED; - } - RuntimeError::OpaqueOutsideScope { - opaque, - referenced_region, - imported_region, - } => { - doc = alloc.stack([ - alloc.concat([ - alloc.reflow("The unwrapped opaque type "), - alloc.type_str(opaque.as_inline_str().as_str()), - alloc.reflow(" referenced here:"), - ]), - alloc.region(lines.convert_region(referenced_region)), - alloc.reflow("is imported from another module:"), - alloc.region(lines.convert_region(imported_region)), - alloc.note( - "Opaque types can only be wrapped and unwrapped in the module they are defined in!", - ), - ]); - - title = OPAQUE_DECLARED_OUTSIDE_SCOPE; - } - RuntimeError::OpaqueNotApplied(loc_ident) => { - doc = alloc.stack([ - alloc.reflow("This opaque type is not applied to an argument:"), - alloc.region(lines.convert_region(loc_ident.region)), - alloc.note("Opaque types always wrap exactly one argument!"), - ]); - - title = OPAQUE_NOT_APPLIED; - } - RuntimeError::OpaqueAppliedToMultipleArgs(region) => { - doc = alloc.stack([ - alloc.reflow("This opaque type is applied to multiple arguments:"), - alloc.region(lines.convert_region(region)), - alloc.note("Opaque types always wrap exactly one argument!"), - ]); - - title = OPAQUE_OVER_APPLIED; - } - } - - (doc, title) -} - -pub fn to_circular_def_doc<'b>( - alloc: &'b RocDocAllocator<'b>, - lines: &LineInfo, - entries: &[roc_problem::can::CycleEntry], -) -> RocDocBuilder<'b> { - // TODO "are you trying to mutate a variable? - // TODO tip? - match entries { - [] => unreachable!(), - [first] => alloc - .reflow("The ") - .append(alloc.symbol_unqualified(first.symbol)) - .append(alloc.reflow( - " value is defined directly in terms of itself, causing an infinite loop.", - )), - [first, others @ ..] => { - alloc.stack([ - alloc - .reflow("The ") - .append(alloc.symbol_unqualified(first.symbol)) - .append(alloc.reflow(" definition is causing a very tricky infinite loop:")), - alloc.region(lines.convert_region(first.symbol_region)), - alloc - .reflow("The ") - .append(alloc.symbol_unqualified(first.symbol)) - .append(alloc.reflow( - " value depends on itself through the following chain of definitions:", - )), - crate::report::cycle( - alloc, - 4, - alloc.symbol_unqualified(first.symbol), - others - .iter() - .map(|s| alloc.symbol_unqualified(s.symbol)) - .collect::>(), - ), - // TODO tip? - ]) - } - } -} - -fn not_found<'b>( - alloc: &'b RocDocAllocator<'b>, - lines: &LineInfo, - region: roc_region::all::Region, - name: &Ident, - options: MutSet>, -) -> RocDocBuilder<'b> { - let mut suggestions = suggest::sort( - name.as_inline_str().as_str(), - options.iter().map(|v| v.as_ref()).collect(), - ); - suggestions.truncate(4); - - let default_no = alloc.concat([ - alloc.reflow("Is there an "), - alloc.keyword("import"), - alloc.reflow(" or "), - alloc.keyword("exposing"), - alloc.reflow(" missing up-top"), - ]); - - let default_yes = alloc.reflow("Did you mean one of these?"); - - let to_details = |no_suggestion_details, yes_suggestion_details| { - if suggestions.is_empty() { - no_suggestion_details - } else { - alloc.stack([ - yes_suggestion_details, - alloc - .vcat(suggestions.into_iter().map(|v| alloc.string(v.to_string()))) - .indent(4), - ]) - } - }; - - alloc.stack([ - alloc.concat([ - alloc.reflow("Nothing is named `"), - alloc.string(name.to_string()), - alloc.reflow("` in this scope."), - ]), - alloc.region(lines.convert_region(region)), - to_details(default_no, default_yes), - ]) -} - -/// Generate a message informing the user that a module was referenced, but not found -/// -/// See [`roc_problem::can::ModuleNotImported`] -fn module_not_found<'b>( - alloc: &'b RocDocAllocator<'b>, - lines: &LineInfo, - region: roc_region::all::Region, - name: &ModuleName, - options: MutSet>, - module_exists: bool, -) -> RocDocBuilder<'b> { - // If the module exists, sugguest that the user import it - let details = if module_exists { - // TODO: Maybe give an example of how to do that - alloc.reflow("Did you mean to import it?") - } else { - // If the module might not exist, sugguest that it's a typo - let mut suggestions = - suggest::sort(name.as_str(), options.iter().map(|v| v.as_ref()).collect()); - suggestions.truncate(4); - - if suggestions.is_empty() { - // We don't have any recommended spelling corrections - alloc.concat([ - alloc.reflow("Is there an "), - alloc.keyword("import"), - alloc.reflow(" or "), - alloc.keyword("exposing"), - alloc.reflow(" missing up-top"), - ]) - } else { - alloc.stack([ - alloc.reflow("Is there an import missing? Perhaps there is a typo. Did you mean one of these?"), - alloc - .vcat(suggestions.into_iter().map(|v| alloc.string(v.to_string()))) - .indent(4), - ]) - } - }; - - alloc.stack([ - alloc.concat([ - alloc.reflow("The `"), - alloc.string(name.to_string()), - alloc.reflow("` module is not imported:"), - ]), - alloc.region(lines.convert_region(region)), - details, - ]) -} diff --git a/reporting/src/error/mod.rs b/reporting/src/error/mod.rs deleted file mode 100644 index 88379cf382..0000000000 --- a/reporting/src/error/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod canonicalize; -pub mod parse; -pub mod r#type; diff --git a/reporting/src/error/parse.rs b/reporting/src/error/parse.rs deleted file mode 100644 index 2d6baf9ef7..0000000000 --- a/reporting/src/error/parse.rs +++ /dev/null @@ -1,3807 +0,0 @@ -use roc_parse::parser::{ENumber, FileError, SyntaxError}; -use roc_region::all::{LineColumn, LineColumnRegion, LineInfo, Position, Region}; -use std::path::PathBuf; - -use crate::report::{Report, RocDocAllocator, RocDocBuilder, Severity}; -use ven_pretty::DocAllocator; - -pub fn parse_problem<'a>( - alloc: &'a RocDocAllocator<'a>, - lines: &LineInfo, - filename: PathBuf, - _starting_line: u32, - parse_problem: FileError>, -) -> Report<'a> { - to_syntax_report(alloc, lines, filename, &parse_problem.problem.problem) -} - -fn note_for_record_type_indent<'a>(alloc: &'a RocDocAllocator<'a>) -> RocDocBuilder<'a> { - alloc.note("I may be confused by indentation") -} - -fn note_for_record_pattern_indent<'a>(alloc: &'a RocDocAllocator<'a>) -> RocDocBuilder<'a> { - alloc.note("I may be confused by indentation") -} - -fn note_for_tag_union_type_indent<'a>(alloc: &'a RocDocAllocator<'a>) -> RocDocBuilder<'a> { - alloc.note("I may be confused by indentation") -} - -fn hint_for_tag_name<'a>(alloc: &'a RocDocAllocator<'a>) -> RocDocBuilder<'a> { - alloc.concat([ - alloc.hint("Tag names "), - alloc.reflow("start with an uppercase letter, like "), - alloc.parser_suggestion("Err"), - alloc.text(" or "), - alloc.parser_suggestion("Green"), - alloc.text("."), - ]) -} - -fn record_patterns_look_like<'a>(alloc: &'a RocDocAllocator<'a>) -> RocDocBuilder<'a> { - alloc.concat([ - alloc.reflow(r"Record pattern look like "), - alloc.parser_suggestion("{ name, age: currentAge },"), - alloc.reflow(" so I was expecting to see a field name next."), - ]) -} - -fn to_syntax_report<'a>( - alloc: &'a RocDocAllocator<'a>, - lines: &LineInfo, - filename: PathBuf, - parse_problem: &roc_parse::parser::SyntaxError<'a>, -) -> Report<'a> { - use SyntaxError::*; - - let report = |doc| Report { - filename: filename.clone(), - doc, - title: "PARSE PROBLEM".to_string(), - severity: Severity::RuntimeError, - }; - - match parse_problem { - SyntaxError::ArgumentsBeforeEquals(region) => { - let doc = alloc.stack([ - alloc.reflow("Unexpected tokens in front of the `=` symbol:"), - alloc.region(lines.convert_region(*region)), - ]); - - Report { - filename, - doc, - title: "PARSE PROBLEM".to_string(), - severity: Severity::RuntimeError, - } - } - Unexpected(region) => { - let mut region = lines.convert_region(*region); - if region.start().column == region.end().column { - region = LineColumnRegion::new(region.start(), region.end().bump_column(1)); - } - - let doc = alloc.stack([ - alloc.concat([ - alloc.reflow("Unexpected token "), - // context(alloc, &parse_problem.context_stack, "here"), - alloc.text(":"), - ]), - alloc.region(region), - ]); - - report(doc) - } - NotEndOfFile(pos) => { - let region = LineColumnRegion::from_pos(lines.convert_pos(*pos)); - - let doc = alloc.stack([ - alloc.reflow(r"I expected to reach the end of the file, but got stuck here:"), - alloc.region(region), - ]); - - Report { - filename, - doc, - title: "NOT END OF FILE".to_string(), - severity: Severity::RuntimeError, - } - } - SyntaxError::Eof(region) => { - let doc = alloc.stack([ - alloc.reflow("End of Field"), - alloc.region(lines.convert_region(*region)), - ]); - - Report { - filename, - doc, - title: "PARSE PROBLEM".to_string(), - severity: Severity::RuntimeError, - } - } - SyntaxError::OutdentedTooFar => { - let doc = alloc.stack([alloc.reflow("OutdentedTooFar")]); - - Report { - filename, - doc, - title: "PARSE PROBLEM".to_string(), - severity: Severity::RuntimeError, - } - } - Type(typ) => to_type_report(alloc, lines, filename, typ, Position::default()), - Pattern(pat) => to_pattern_report(alloc, lines, filename, pat, Position::default()), - Expr(expr, start) => to_expr_report( - alloc, - lines, - filename, - Context::InDef(*start), - expr, - Position::default(), - ), - Header(header) => to_header_report(alloc, lines, filename, header, Position::default()), - _ => todo!("unhandled parse error: {:?}", parse_problem), - } -} - -#[allow(clippy::enum_variant_names)] -enum Context { - InNode(Node, Position, Box), - InDef(Position), - InDefFinalExpr(Position), -} - -enum Node { - WhenCondition, - WhenBranch, - WhenIfGuard, - IfCondition, - IfThenBranch, - IfElseBranch, - ListElement, - InsideParens, - RecordConditionalDefault, - StringFormat, -} - -fn to_expr_report<'a>( - alloc: &'a RocDocAllocator<'a>, - lines: &LineInfo, - filename: PathBuf, - context: Context, - parse_problem: &roc_parse::parser::EExpr<'a>, - start: Position, -) -> Report<'a> { - use roc_parse::parser::EExpr; - - match parse_problem { - EExpr::If(if_, pos) => to_if_report(alloc, lines, filename, context, if_, *pos), - EExpr::When(when, pos) => to_when_report(alloc, lines, filename, context, when, *pos), - EExpr::Lambda(lambda, pos) => { - to_lambda_report(alloc, lines, filename, context, lambda, *pos) - } - EExpr::List(list, pos) => to_list_report(alloc, lines, filename, context, list, *pos), - EExpr::Str(string, pos) => to_str_report(alloc, lines, filename, context, string, *pos), - EExpr::InParens(expr, pos) => { - to_expr_in_parens_report(alloc, lines, filename, context, expr, *pos) - } - EExpr::Type(tipe, pos) => to_type_report(alloc, lines, filename, tipe, *pos), - EExpr::ElmStyleFunction(region, pos) => { - let surroundings = Region::new(start, *pos); - let region = lines.convert_region(*region); - - let doc = alloc.stack([ - alloc.reflow(r"I am partway through parsing a definition, but I got stuck here:"), - alloc.region_with_subregion(lines.convert_region(surroundings), region), - alloc.concat([ - alloc.reflow("Looks like you are trying to define a function. "), - alloc.reflow("In roc, functions are always written as a lambda, like "), - alloc.parser_suggestion("increment = \\n -> n + 1"), - alloc.reflow("."), - ]), - ]); - - Report { - filename, - doc, - title: "ARGUMENTS BEFORE EQUALS".to_string(), - severity: Severity::RuntimeError, - } - } - - EExpr::BadOperator(op, pos) => { - let surroundings = Region::new(start, *pos); - let region = Region::new(*pos, pos.bump_column(op.len() as u32)); - - let suggestion = match *op { - "|" => vec![ - alloc.reflow("Maybe you want "), - alloc.parser_suggestion("||"), - alloc.reflow(" or "), - alloc.parser_suggestion("|>"), - alloc.reflow(" instead?"), - ], - "++" => vec![ - alloc.reflow("To concatenate two lists or strings, try using "), - alloc.parser_suggestion("List.concat"), - alloc.reflow(" or "), - alloc.parser_suggestion("Str.concat"), - alloc.reflow(" instead."), - ], - ":" => vec![alloc.stack([ - alloc.concat([ - alloc.reflow("The has-type operator "), - alloc.parser_suggestion(":"), - alloc.reflow(" can only occur in a definition's type signature, like"), - ]), - alloc - .vcat(vec![ - alloc.text("increment : I64 -> I64"), - alloc.text("increment = \\x -> x + 1"), - ]) - .indent(4), - ])], - "->" => match context { - Context::InNode(Node::WhenBranch, _pos, _) => { - return to_unexpected_arrow_report(alloc, lines, filename, *pos, start); - } - _ => { - vec![alloc.stack([ - alloc.concat([ - alloc.reflow("The arrow "), - alloc.parser_suggestion("->"), - alloc.reflow(" is only used to define cases in a "), - alloc.keyword("when"), - alloc.reflow("."), - ]), - alloc - .vcat(vec![ - alloc.text("when color is"), - alloc.text("Red -> \"stop!\"").indent(4), - alloc.text("Green -> \"go!\"").indent(4), - ]) - .indent(4), - ])] - } - }, - "!" => vec![ - alloc.reflow("The boolean negation operator "), - alloc.parser_suggestion("!"), - alloc.reflow(" must occur immediately before an expression, like "), - alloc.parser_suggestion("!(List.isEmpty primes)"), - alloc.reflow(". There cannot be a space between the "), - alloc.parser_suggestion("!"), - alloc.reflow(" and the expression after it."), - ], - _ => vec![ - alloc.reflow("I have no specific suggestion for this operator, "), - alloc.reflow("see TODO for the full list of operators in Roc."), - ], - }; - - let doc = alloc.stack([ - alloc.reflow(r"This looks like an operator, but it's not one I recognize!"), - alloc.region_with_subregion( - lines.convert_region(surroundings), - lines.convert_region(region), - ), - alloc.concat(suggestion), - ]); - - Report { - filename, - doc, - title: "UNKNOWN OPERATOR".to_string(), - severity: Severity::RuntimeError, - } - } - - EExpr::Ident(_pos) => unreachable!("another branch would be taken"), - - EExpr::QualifiedTag(pos) => { - let surroundings = Region::new(start, *pos); - let region = LineColumnRegion::from_pos(lines.convert_pos(*pos)); - - let doc = alloc.stack([ - alloc.reflow(r"I am very confused by this identifier:"), - alloc.region_with_subregion(lines.convert_region(surroundings), region), - alloc.concat([ - alloc.reflow("Are you trying to qualify a name? I am execting something like "), - alloc.parser_suggestion("Json.Decode.string"), - alloc.reflow(". Maybe you are trying to qualify a tag? Tags like "), - alloc.parser_suggestion("Err"), - alloc.reflow(" are globally scoped in roc, and cannot be qualified."), - ]), - ]); - - Report { - filename, - doc, - title: "WEIRD IDENTIFIER".to_string(), - severity: Severity::RuntimeError, - } - } - - EExpr::Start(pos) | EExpr::IndentStart(pos) => { - let (title, expecting) = match &context { - Context::InNode { .. } | Context::InDef { .. } => ( - "MISSING EXPRESSION", - alloc.concat([ - alloc.reflow("I was expecting to see an expression like "), - alloc.parser_suggestion("42"), - alloc.reflow(" or "), - alloc.parser_suggestion("\"hello\""), - alloc.text("."), - ]), - ), - Context::InDefFinalExpr { .. } => ( - "MISSING FINAL EXPRESSION", - alloc.stack([ - alloc.concat([ - alloc.reflow("This definition is missing a final expression."), - alloc.reflow(" A nested definition must be followed by"), - alloc.reflow(" either another definition, or an expression"), - ]), - alloc.vcat(vec![ - alloc.text("x = 4").indent(4), - alloc.text("y = 2").indent(4), - alloc.text(""), - alloc.text("x + y").indent(4), - ]), - ]), - ), - }; - - let (context_pos, a_thing) = match context { - Context::InNode(node, pos, _) => match node { - Node::WhenCondition | Node::WhenBranch | Node::WhenIfGuard => ( - pos, - alloc.concat([ - alloc.text("an "), - alloc.keyword("when"), - alloc.text(" expression"), - ]), - ), - Node::IfCondition | Node::IfThenBranch | Node::IfElseBranch => ( - pos, - alloc.concat([ - alloc.text("an "), - alloc.keyword("if"), - alloc.text(" expression"), - ]), - ), - Node::ListElement => (pos, alloc.text("a list")), - Node::RecordConditionalDefault => (pos, alloc.text("record field default")), - Node::StringFormat => (pos, alloc.text("a string format")), - Node::InsideParens => (pos, alloc.text("some parentheses")), - }, - Context::InDef(pos) => (pos, alloc.text("a definition")), - Context::InDefFinalExpr(pos) => { - (pos, alloc.text("a definition's final expression")) - } - }; - - let surroundings = Region::new(context_pos, *pos); - let region = LineColumnRegion::from_pos(lines.convert_pos(*pos)); - - let doc = alloc.stack([ - alloc.concat([ - alloc.reflow(r"I am partway through parsing "), - a_thing, - alloc.reflow(", but I got stuck here:"), - ]), - alloc.region_with_subregion(lines.convert_region(surroundings), region), - expecting, - ]); - - Report { - filename, - doc, - title: title.to_string(), - severity: Severity::RuntimeError, - } - } - - EExpr::DefMissingFinalExpr(pos) => { - let surroundings = Region::new(start, *pos); - let region = LineColumnRegion::from_pos(lines.convert_pos(*pos)); - - let doc = alloc.stack([ - alloc.reflow(r"I am partway through parsing a definition, but I got stuck here:"), - alloc.region_with_subregion(lines.convert_region(surroundings), region), - alloc.concat([ - alloc.reflow("This definition is missing a final expression."), - alloc.reflow(" A nested definition must be followed by"), - alloc.reflow(" either another definition, or an expression"), - ]), - alloc.vcat(vec![ - alloc.text("x = 4").indent(4), - alloc.text("y = 2").indent(4), - alloc.text(""), - alloc.text("x + y").indent(4), - ]), - ]); - - Report { - filename, - doc, - title: "MISSING FINAL EXPRESSION".to_string(), - severity: Severity::RuntimeError, - } - } - - EExpr::DefMissingFinalExpr2(expr, pos) => to_expr_report( - alloc, - lines, - filename, - Context::InDefFinalExpr(start), - expr, - *pos, - ), - - EExpr::BadExprEnd(pos) => { - let surroundings = Region::new(start, *pos); - let region = LineColumnRegion::from_pos(lines.convert_pos(*pos)); - - let doc = alloc.stack([ - alloc.reflow(r"I got stuck here:"), - alloc.region_with_subregion(lines.convert_region(surroundings), region), - alloc.concat([ - alloc.reflow("Whatever I am running into is confusing me a lot! "), - alloc.reflow("Normally I can give fairly specific hints, "), - alloc.reflow("but something is really tripping me up this time."), - ]), - ]); - - Report { - filename, - doc, - title: "SYNTAX PROBLEM".to_string(), - severity: Severity::RuntimeError, - } - } - - EExpr::Colon(pos) => { - let surroundings = Region::new(start, *pos); - let region = LineColumnRegion::from_pos(lines.convert_pos(*pos)); - - let doc = alloc.stack([ - alloc.reflow(r"I am partway through parsing a definition, but I got stuck here:"), - alloc.region_with_subregion(lines.convert_region(surroundings), region), - alloc.concat([ - alloc.reflow("Looks like you are trying to define a function. "), - alloc.reflow("In roc, functions are always written as a lambda, like "), - alloc.parser_suggestion("increment = \\n -> n + 1"), - alloc.reflow("."), - ]), - ]); - - Report { - filename, - doc, - title: "ARGUMENTS BEFORE EQUALS".to_string(), - severity: Severity::RuntimeError, - } - } - - EExpr::BackpassArrow(pos) => { - let surroundings = Region::new(start, *pos); - let region = LineColumnRegion::from_pos(lines.convert_pos(*pos)); - - let doc = alloc.stack([ - alloc.reflow(r"I am partway through parsing an expression, but I got stuck here:"), - alloc.region_with_subregion(lines.convert_region(surroundings), region), - alloc.concat([alloc.reflow("Looks like you are trying to define a function. ")]), - ]); - - Report { - filename, - doc, - title: "BAD BACKPASSING ARROW".to_string(), - severity: Severity::RuntimeError, - } - } - - EExpr::Record(_erecord, pos) => { - let surroundings = Region::new(start, *pos); - let region = LineColumnRegion::from_pos(lines.convert_pos(*pos)); - - let doc = alloc.stack([ - alloc.reflow(r"I am partway through parsing an record, but I got stuck here:"), - alloc.region_with_subregion(lines.convert_region(surroundings), region), - alloc.concat([alloc.reflow("TODO provide more context.")]), - ]); - - Report { - filename, - doc, - title: "RECORD PARSE PROBLEM".to_string(), - severity: Severity::RuntimeError, - } - } - - EExpr::Space(error, pos) => to_space_report(alloc, lines, filename, error, *pos), - - &EExpr::Number(ENumber::End, pos) => { - to_malformed_number_literal_report(alloc, lines, filename, pos) - } - - EExpr::Ability(err, pos) => to_ability_def_report(alloc, lines, filename, err, *pos), - - _ => todo!("unhandled parse error: {:?}", parse_problem), - } -} - -fn to_lambda_report<'a>( - alloc: &'a RocDocAllocator<'a>, - lines: &LineInfo, - filename: PathBuf, - _context: Context, - parse_problem: &roc_parse::parser::ELambda<'a>, - start: Position, -) -> Report<'a> { - use roc_parse::parser::ELambda; - - match *parse_problem { - ELambda::Arrow(pos) => match what_is_next(alloc.src_lines, lines.convert_pos(pos)) { - Next::Token("=>") => { - let surroundings = Region::new(start, pos); - let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); - - let doc = alloc.stack([ - alloc - .reflow(r"I am partway through parsing a function argument list, but I got stuck here:"), - alloc.region_with_subregion(lines.convert_region(surroundings), region), - alloc.concat([ - alloc.reflow("I was expecting a "), - alloc.parser_suggestion("->"), - alloc.reflow(" next."), - ]), - ]); - - Report { - filename, - doc, - title: "WEIRD ARROW".to_string(), - severity: Severity::RuntimeError, - } - } - _ => { - let surroundings = Region::new(start, pos); - let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); - - let doc = alloc.stack([ - alloc - .reflow(r"I am partway through parsing a function argument list, but I got stuck here:"), - alloc.region_with_subregion(lines.convert_region(surroundings), region), - alloc.concat([ - alloc.reflow("I was expecting a "), - alloc.parser_suggestion("->"), - alloc.reflow(" next."), - ]), - ]); - - Report { - filename, - doc, - title: "MISSING ARROW".to_string(), - severity: Severity::RuntimeError, - } - } - }, - - ELambda::Comma(pos) => match what_is_next(alloc.src_lines, lines.convert_pos(pos)) { - Next::Token("=>") => { - let surroundings = Region::new(start, pos); - let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); - - let doc = alloc.stack([ - alloc - .reflow(r"I am partway through parsing a function argument list, but I got stuck here:"), - alloc.region_with_subregion(lines.convert_region(surroundings), region), - alloc.concat([ - alloc.reflow("I was expecting a "), - alloc.parser_suggestion("->"), - alloc.reflow(" next."), - ]), - ]); - - Report { - filename, - doc, - title: "WEIRD ARROW".to_string(), - severity: Severity::RuntimeError, - } - } - _ => { - let surroundings = Region::new(start, pos); - let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); - - let doc = alloc.stack([ - alloc - .reflow(r"I am partway through parsing a function argument list, but I got stuck here:"), - alloc.region_with_subregion(lines.convert_region(surroundings), region), - alloc.concat([ - alloc.reflow("I was expecting a "), - alloc.parser_suggestion("->"), - alloc.reflow(" next."), - ]), - ]); - - Report { - filename, - doc, - title: "MISSING ARROW".to_string(), - severity: Severity::RuntimeError, - } - } - }, - - ELambda::Arg(pos) => match what_is_next(alloc.src_lines, lines.convert_pos(pos)) { - Next::Other(Some(',')) => { - let surroundings = Region::new(start, pos); - let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); - - let doc = alloc.stack([ - alloc - .reflow(r"I am partway through parsing a function argument list, but I got stuck at this comma:"), - alloc.region_with_subregion(lines.convert_region(surroundings), region), - alloc.concat([ - alloc.reflow("I was expecting an argument pattern before this, "), - alloc.reflow("so try adding an argument before the comma and see if that helps?"), - ]), - ]); - - Report { - filename, - doc, - title: "UNFINISHED ARGUMENT LIST".to_string(), - severity: Severity::RuntimeError, - } - } - _ => { - let surroundings = Region::new(start, pos); - let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); - - let doc = alloc.stack([ - alloc - .reflow(r"I am partway through parsing a function argument list, but I got stuck here:"), - alloc.region_with_subregion(lines.convert_region(surroundings), region), - alloc.concat([ - alloc.reflow("I was expecting an argument pattern before this, "), - alloc.reflow("so try adding an argument and see if that helps?"), - ]), - ]); - - Report { - filename, - doc, - title: "MISSING ARROW".to_string(), - severity: Severity::RuntimeError, - } - } - }, - - ELambda::Start(_pos) => unreachable!("another branch would have been taken"), - - ELambda::Body(expr, pos) => { - to_expr_report(alloc, lines, filename, Context::InDef(start), expr, pos) - } - ELambda::Pattern(ref pattern, pos) => { - to_pattern_report(alloc, lines, filename, pattern, pos) - } - ELambda::Space(error, pos) => to_space_report(alloc, lines, filename, &error, pos), - - ELambda::IndentArrow(pos) => to_unfinished_lambda_report( - alloc, - lines, - filename, - pos, - start, - alloc.concat([ - alloc.reflow(r"I just saw a pattern, so I was expecting to see a "), - alloc.parser_suggestion("->"), - alloc.reflow(" next."), - ]), - ), - - ELambda::IndentBody(pos) => to_unfinished_lambda_report( - alloc, - lines, - filename, - pos, - start, - alloc.concat([ - alloc.reflow(r"I just saw a pattern, so I was expecting to see a "), - alloc.parser_suggestion("->"), - alloc.reflow(" next."), - ]), - ), - - ELambda::IndentArg(pos) => to_unfinished_lambda_report( - alloc, - lines, - filename, - pos, - start, - alloc.concat([ - alloc.reflow(r"I just saw a pattern, so I was expecting to see a "), - alloc.parser_suggestion("->"), - alloc.reflow(" next."), - alloc.reflow(r"I was expecting to see a expression next"), - ]), - ), - } -} - -fn to_unfinished_lambda_report<'a>( - alloc: &'a RocDocAllocator<'a>, - lines: &LineInfo, - filename: PathBuf, - pos: Position, - start: Position, - message: RocDocBuilder<'a>, -) -> Report<'a> { - let surroundings = Region::new(start, pos); - let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); - - let doc = alloc.stack([ - alloc.concat([ - alloc.reflow(r"I was partway through parsing a "), - alloc.reflow(r" function, but I got stuck here:"), - ]), - alloc.region_with_subregion(lines.convert_region(surroundings), region), - message, - ]); - - Report { - filename, - doc, - title: "UNFINISHED FUNCTION".to_string(), - severity: Severity::RuntimeError, - } -} - -fn to_str_report<'a>( - alloc: &'a RocDocAllocator<'a>, - lines: &LineInfo, - filename: PathBuf, - context: Context, - parse_problem: &roc_parse::parser::EString<'a>, - start: Position, -) -> Report<'a> { - use roc_parse::parser::EString; - - match *parse_problem { - EString::Open(_pos) => unreachable!("another branch would be taken"), - EString::Format(expr, pos) => to_expr_report( - alloc, - lines, - filename, - Context::InNode(Node::StringFormat, start, Box::new(context)), - expr, - pos, - ), - EString::Space(error, pos) => to_space_report(alloc, lines, filename, &error, pos), - EString::UnknownEscape(pos) => { - let surroundings = Region::new(start, pos); - let region = Region::new(pos, pos.bump_column(2)); - - let suggestion = |msg, sugg| { - alloc - .text("- ") - .append(alloc.reflow(msg)) - .append(alloc.parser_suggestion(sugg)) - }; - - let doc = alloc.stack([ - alloc.concat([ - alloc.reflow(r"I was partway through parsing a "), - alloc.reflow(r" string literal, but I got stuck here:"), - ]), - alloc.region_with_subregion( - lines.convert_region(surroundings), - lines.convert_region(region), - ), - alloc.concat([ - alloc.reflow(r"This is not an escape sequence I recognize."), - alloc.reflow(r" After a backslash, I am looking for one of these:"), - ]), - alloc - .vcat(vec![ - suggestion("A newline: ", "\\n"), - suggestion("A caret return: ", "\\r"), - suggestion("A tab: ", "\\t"), - suggestion("An escaped quote: ", "\\\""), - suggestion("An escaped backslash: ", "\\\\"), - suggestion("A unicode code point: ", "\\u(00FF)"), - suggestion("An interpolated string: ", "\\(myVariable)"), - ]) - .indent(4), - ]); - - Report { - filename, - doc, - title: "WEIRD ESCAPE".to_string(), - severity: Severity::RuntimeError, - } - } - EString::CodePtOpen(pos) | EString::CodePtEnd(pos) => { - let surroundings = Region::new(start, pos); - let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); - - let doc = alloc.stack([ - alloc.reflow( - r"I am partway through parsing a unicode code point, but I got stuck here:", - ), - alloc.region_with_subregion(lines.convert_region(surroundings), region), - alloc.concat([ - alloc.reflow(r"I was expecting a hexadecimal number, like "), - alloc.parser_suggestion("\\u(1100)"), - alloc.reflow(" or "), - alloc.parser_suggestion("\\u(00FF)"), - alloc.text("."), - ]), - alloc.reflow(r"Learn more about working with unicode in roc at TODO"), - ]); - - Report { - filename, - doc, - title: "WEIRD CODE POINT".to_string(), - severity: Severity::RuntimeError, - } - } - EString::FormatEnd(pos) => { - let surroundings = Region::new(start, pos); - let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); - - let doc = alloc.stack([ - alloc.reflow(r"I cannot find the end of this format expression:"), - alloc.region_with_subregion(lines.convert_region(surroundings), region), - alloc.concat([ - alloc.reflow(r"You could change it to something like "), - alloc.parser_suggestion("\"The count is \\(count\\)\""), - alloc.reflow("."), - ]), - ]); - - Report { - filename, - doc, - title: "ENDLESS FORMAT".to_string(), - severity: Severity::RuntimeError, - } - } - EString::EndlessSingle(pos) => { - let surroundings = Region::new(start, pos); - let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); - - let doc = alloc.stack([ - alloc.reflow(r"I cannot find the end of this string:"), - alloc.region_with_subregion(lines.convert_region(surroundings), region), - alloc.concat([ - alloc.reflow(r"You could change it to something like "), - alloc.parser_suggestion("\"to be or not to be\""), - alloc.reflow(" or even just "), - alloc.parser_suggestion("\"\""), - alloc.reflow("."), - ]), - ]); - - Report { - filename, - doc, - title: "ENDLESS STRING".to_string(), - severity: Severity::RuntimeError, - } - } - EString::EndlessMulti(pos) => { - let surroundings = Region::new(start, pos); - let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); - - let doc = alloc.stack([ - alloc.reflow(r"I cannot find the end of this block string:"), - alloc.region_with_subregion(lines.convert_region(surroundings), region), - alloc.concat([ - alloc.reflow(r"You could change it to something like "), - alloc.parser_suggestion("\"\"\"to be or not to be\"\"\""), - alloc.reflow(" or even just "), - alloc.parser_suggestion("\"\"\"\"\"\""), - alloc.reflow("."), - ]), - ]); - - Report { - filename, - doc, - title: "ENDLESS STRING".to_string(), - severity: Severity::RuntimeError, - } - } - } -} -fn to_expr_in_parens_report<'a>( - alloc: &'a RocDocAllocator<'a>, - lines: &LineInfo, - filename: PathBuf, - context: Context, - parse_problem: &roc_parse::parser::EInParens<'a>, - start: Position, -) -> Report<'a> { - use roc_parse::parser::EInParens; - - match *parse_problem { - EInParens::Space(error, pos) => to_space_report(alloc, lines, filename, &error, pos), - EInParens::Expr(expr, pos) => to_expr_report( - alloc, - lines, - filename, - Context::InNode(Node::InsideParens, start, Box::new(context)), - expr, - pos, - ), - EInParens::End(pos) | EInParens::IndentEnd(pos) => { - let surroundings = Region::new(start, pos); - let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); - - let doc = alloc.stack([ - alloc - .reflow("I am partway through parsing a record pattern, but I got stuck here:"), - alloc.region_with_subregion(lines.convert_region(surroundings), region), - alloc.concat([ - alloc.reflow( - r"I was expecting to see a closing parenthesis next, so try adding a ", - ), - alloc.parser_suggestion(")"), - alloc.reflow(" and see if that helps?"), - ]), - ]); - - Report { - filename, - doc, - title: "UNFINISHED PARENTHESES".to_string(), - severity: Severity::RuntimeError, - } - } - EInParens::Open(pos) | EInParens::IndentOpen(pos) => { - let surroundings = Region::new(start, pos); - let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); - - let doc = alloc.stack([ - alloc.reflow( - r"I just started parsing an expression in parentheses, but I got stuck here:", - ), - alloc.region_with_subregion(lines.convert_region(surroundings), region), - alloc.concat([ - alloc.reflow(r"An expression in parentheses looks like "), - alloc.parser_suggestion("(32)"), - alloc.reflow(r" or "), - alloc.parser_suggestion("(\"hello\")"), - alloc.reflow(" so I was expecting to see an expression next."), - ]), - ]); - - Report { - filename, - doc, - title: "UNFINISHED PARENTHESES".to_string(), - severity: Severity::RuntimeError, - } - } - } -} - -fn to_list_report<'a>( - alloc: &'a RocDocAllocator<'a>, - lines: &LineInfo, - filename: PathBuf, - context: Context, - parse_problem: &roc_parse::parser::EList<'a>, - start: Position, -) -> Report<'a> { - use roc_parse::parser::EList; - - match *parse_problem { - EList::Space(error, pos) => to_space_report(alloc, lines, filename, &error, pos), - - EList::Expr(expr, pos) => to_expr_report( - alloc, - lines, - filename, - Context::InNode(Node::ListElement, start, Box::new(context)), - expr, - pos, - ), - - EList::Open(pos) | EList::End(pos) => { - match what_is_next(alloc.src_lines, lines.convert_pos(pos)) { - Next::Other(Some(',')) => { - let surroundings = Region::new(start, pos); - let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); - - let doc = alloc.stack([ - alloc.reflow( - r"I am partway through started parsing a list, but I got stuck here:", - ), - alloc.region_with_subregion(lines.convert_region(surroundings), region), - alloc.concat([ - alloc - .reflow(r"I was expecting to see a list entry before this comma, "), - alloc.reflow(r"so try adding a list entry"), - alloc.reflow(r" and see if that helps?"), - ]), - ]); - Report { - filename, - doc, - title: "UNFINISHED LIST".to_string(), - severity: Severity::RuntimeError, - } - } - _ => { - let surroundings = Region::new(start, pos); - let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); - - let doc = alloc.stack([ - alloc.reflow( - r"I am partway through started parsing a list, but I got stuck here:", - ), - alloc.region_with_subregion(lines.convert_region(surroundings), region), - alloc.concat([ - alloc.reflow( - r"I was expecting to see a closing square bracket before this, ", - ), - alloc.reflow(r"so try adding a "), - alloc.parser_suggestion("]"), - alloc.reflow(r" and see if that helps?"), - ]), - alloc.concat([ - alloc.note("When "), - alloc.reflow(r"I get stuck like this, "), - alloc.reflow(r"it usually means that there is a missing parenthesis "), - alloc.reflow(r"or bracket somewhere earlier. "), - alloc.reflow(r"It could also be a stray keyword or operator."), - ]), - ]); - - Report { - filename, - doc, - title: "UNFINISHED LIST".to_string(), - severity: Severity::RuntimeError, - } - } - } - } - - EList::IndentOpen(pos) | EList::IndentEnd(pos) => { - let surroundings = Region::new(start, pos); - let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); - - let doc = alloc.stack([ - alloc.reflow(r"I cannot find the end of this list:"), - alloc.region_with_subregion(lines.convert_region(surroundings), region), - alloc.concat([ - alloc.reflow(r"You could change it to something like "), - alloc.parser_suggestion("[1, 2, 3]"), - alloc.reflow(" or even just "), - alloc.parser_suggestion("[]"), - alloc.reflow(". Anything where there is an open and a close square bracket, "), - alloc.reflow("and where the elements of the list are separated by commas."), - ]), - note_for_tag_union_type_indent(alloc), - ]); - - Report { - filename, - doc, - title: "UNFINISHED LIST".to_string(), - severity: Severity::RuntimeError, - } - } - } -} - -fn to_if_report<'a>( - alloc: &'a RocDocAllocator<'a>, - lines: &LineInfo, - filename: PathBuf, - context: Context, - parse_problem: &roc_parse::parser::EIf<'a>, - start: Position, -) -> Report<'a> { - use roc_parse::parser::EIf; - - match *parse_problem { - EIf::Space(error, pos) => to_space_report(alloc, lines, filename, &error, pos), - - EIf::Condition(expr, pos) => to_expr_report( - alloc, - lines, - filename, - Context::InNode(Node::IfCondition, start, Box::new(context)), - expr, - pos, - ), - - EIf::ThenBranch(expr, pos) => to_expr_report( - alloc, - lines, - filename, - Context::InNode(Node::IfThenBranch, start, Box::new(context)), - expr, - pos, - ), - - EIf::ElseBranch(expr, pos) => to_expr_report( - alloc, - lines, - filename, - Context::InNode(Node::IfElseBranch, start, Box::new(context)), - expr, - pos, - ), - - EIf::If(_pos) => unreachable!("another branch would be taken"), - EIf::IndentIf(_pos) => unreachable!("another branch would be taken"), - - EIf::Then(pos) | EIf::IndentThenBranch(pos) | EIf::IndentThenToken(pos) => { - to_unfinished_if_report( - alloc, - lines, - filename, - pos, - start, - alloc.concat([ - alloc.reflow(r"I was expecting to see the "), - alloc.keyword("then"), - alloc.reflow(r" keyword next."), - ]), - ) - } - - EIf::Else(pos) | EIf::IndentElseBranch(pos) | EIf::IndentElseToken(pos) => { - to_unfinished_if_report( - alloc, - lines, - filename, - pos, - start, - alloc.concat([ - alloc.reflow(r"I was expecting to see the "), - alloc.keyword("else"), - alloc.reflow(r" keyword next."), - ]), - ) - } - - EIf::IndentCondition(pos) => to_unfinished_if_report( - alloc, - lines, - filename, - pos, - start, - alloc.concat([alloc.reflow(r"I was expecting to see a expression next")]), - ), - } -} - -fn to_unfinished_if_report<'a>( - alloc: &'a RocDocAllocator<'a>, - lines: &LineInfo, - filename: PathBuf, - pos: Position, - start: Position, - message: RocDocBuilder<'a>, -) -> Report<'a> { - let surroundings = Region::new(start, pos); - let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); - - let doc = alloc.stack([ - alloc.concat([ - alloc.reflow(r"I was partway through parsing an "), - alloc.keyword("if"), - alloc.reflow(r" expression, but I got stuck here:"), - ]), - alloc.region_with_subregion(lines.convert_region(surroundings), region), - message, - ]); - - Report { - filename, - doc, - title: "UNFINISHED IF".to_string(), - severity: Severity::RuntimeError, - } -} - -fn to_when_report<'a>( - alloc: &'a RocDocAllocator<'a>, - lines: &LineInfo, - filename: PathBuf, - context: Context, - parse_problem: &roc_parse::parser::EWhen<'a>, - start: Position, -) -> Report<'a> { - use roc_parse::parser::EWhen; - - match *parse_problem { - EWhen::IfGuard(nested, pos) => { - match what_is_next(alloc.src_lines, lines.convert_pos(pos)) { - Next::Token("->") => { - let surroundings = Region::new(start, pos); - let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); - - let doc = alloc.stack([ - alloc.reflow( - r"I just started parsing an if guard, but there is no guard condition:", - ), - alloc.region_with_subregion(lines.convert_region(surroundings), region), - alloc.concat([alloc.reflow("Try adding an expression before the arrow!")]), - ]); - - Report { - filename, - doc, - title: "IF GUARD NO CONDITION".to_string(), - severity: Severity::RuntimeError, - } - } - _ => to_expr_report( - alloc, - lines, - filename, - Context::InNode(Node::WhenIfGuard, start, Box::new(context)), - nested, - pos, - ), - } - } - EWhen::Arrow(pos) => { - let surroundings = Region::new(start, pos); - let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); - - let doc = alloc.stack([ - alloc.concat([ - alloc.reflow(r"I am partway through parsing a "), - alloc.keyword("when"), - alloc.reflow(r" expression, but got stuck here:"), - ]), - alloc.region_with_subregion(lines.convert_region(surroundings), region), - alloc.concat([alloc.reflow("I was expecting to see an arrow next.")]), - note_for_when_indent_error(alloc), - ]); - - Report { - filename, - doc, - title: "MISSING ARROW".to_string(), - severity: Severity::RuntimeError, - } - } - - EWhen::Space(error, pos) => to_space_report(alloc, lines, filename, &error, pos), - - EWhen::Branch(expr, pos) => to_expr_report( - alloc, - lines, - filename, - Context::InNode(Node::WhenBranch, start, Box::new(context)), - expr, - pos, - ), - - EWhen::Condition(expr, pos) => to_expr_report( - alloc, - lines, - filename, - Context::InNode(Node::WhenCondition, start, Box::new(context)), - expr, - pos, - ), - - EWhen::Bar(pos) => to_unfinished_when_report( - alloc, - lines, - filename, - pos, - start, - alloc.concat([ - alloc.reflow(r"I just saw a "), - alloc.parser_suggestion(r"|"), - alloc.reflow(r" so I was expecting to see a pattern next."), - ]), - ), - - EWhen::IfToken(_pos) => unreachable!("the if-token is optional"), - EWhen::When(_pos) => unreachable!("another branch would be taken"), - - EWhen::Is(pos) | EWhen::IndentIs(pos) => to_unfinished_when_report( - alloc, - lines, - filename, - pos, - start, - alloc.concat([ - alloc.reflow(r"I was expecting to see the "), - alloc.keyword("is"), - alloc.reflow(r" keyword next."), - ]), - ), - - EWhen::IndentCondition(pos) => to_unfinished_when_report( - alloc, - lines, - filename, - pos, - start, - alloc.concat([alloc.reflow(r"I was expecting to see a expression next")]), - ), - - EWhen::IndentPattern(pos) => to_unfinished_when_report( - alloc, - lines, - filename, - pos, - start, - alloc.concat([alloc.reflow(r"I was expecting to see a pattern next")]), - ), - - EWhen::IndentArrow(pos) => to_unfinished_when_report( - alloc, - lines, - filename, - pos, - start, - alloc.concat([ - alloc.reflow(r"I just saw a pattern, so I was expecting to see a "), - alloc.parser_suggestion("->"), - alloc.reflow(" next."), - ]), - ), - - EWhen::IndentIfGuard(pos) => to_unfinished_when_report( - alloc, - lines, - filename, - pos, - start, - alloc.concat([ - alloc.reflow(r"I just saw the "), - alloc.keyword("if"), - alloc.reflow(" keyword, so I was expecting to see an expression next."), - ]), - ), - - EWhen::IndentBranch(pos) => to_unfinished_when_report( - alloc, - lines, - filename, - pos, - start, - alloc.concat([ - alloc.reflow(r"I was expecting to see an expression next. "), - alloc.reflow("What should I do when I run into this particular pattern?"), - ]), - ), - - EWhen::PatternAlignment(indent, pos) => to_unfinished_when_report( - alloc, - lines, - filename, - pos, - start, - alloc.concat([ - alloc.reflow(r"I suspect this is a pattern that is not indented enough? (by "), - alloc.text(indent.to_string()), - alloc.reflow(" spaces)"), - ]), - ), - EWhen::Pattern(ref pat, pos) => to_pattern_report(alloc, lines, filename, pat, pos), - } -} - -fn to_unfinished_when_report<'a>( - alloc: &'a RocDocAllocator<'a>, - lines: &LineInfo, - filename: PathBuf, - pos: Position, - start: Position, - message: RocDocBuilder<'a>, -) -> Report<'a> { - match what_is_next(alloc.src_lines, lines.convert_pos(pos)) { - Next::Token("->") => to_unexpected_arrow_report(alloc, lines, filename, pos, start), - - _ => { - let surroundings = Region::new(start, pos); - let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); - - let doc = alloc.stack([ - alloc.concat([ - alloc.reflow(r"I was partway through parsing a "), - alloc.keyword("when"), - alloc.reflow(r" expression, but I got stuck here:"), - ]), - alloc.region_with_subregion(lines.convert_region(surroundings), region), - message, - note_for_when_error(alloc), - ]); - - Report { - filename, - doc, - title: "UNFINISHED WHEN".to_string(), - severity: Severity::RuntimeError, - } - } - } -} - -fn to_unexpected_arrow_report<'a>( - alloc: &'a RocDocAllocator<'a>, - lines: &LineInfo, - filename: PathBuf, - pos: Position, - start: Position, -) -> Report<'a> { - let surroundings = Region::new(start, pos); - let region = Region::new(pos, pos.bump_column(2)); - - let doc = alloc.stack([ - alloc.concat([ - alloc.reflow(r"I am parsing a "), - alloc.keyword("when"), - alloc.reflow(r" expression right now, but this arrow is confusing me:"), - ]), - alloc.region_with_subregion( - lines.convert_region(surroundings), - lines.convert_region(region), - ), - alloc.concat([ - alloc.reflow(r"It makes sense to see arrows around here, "), - alloc.reflow(r"so I suspect it is something earlier."), - alloc.reflow( - r"Maybe this pattern is indented a bit farther from the previous patterns?", - ), - ]), - note_for_when_error(alloc), - ]); - - Report { - filename, - doc, - title: "UNEXPECTED ARROW".to_string(), - severity: Severity::RuntimeError, - } -} - -fn note_for_when_error<'a>(alloc: &'a RocDocAllocator<'a>) -> RocDocBuilder<'a> { - alloc.stack([ - alloc.concat([ - alloc.note("Here is an example of a valid "), - alloc.keyword("when"), - alloc.reflow(r" expression for reference."), - ]), - alloc.vcat(vec![ - alloc.text("when List.first plants is").indent(4), - alloc.text("Ok n ->").indent(6), - alloc.text("n").indent(8), - alloc.text(""), - alloc.text("Err _ ->").indent(6), - alloc.text("200").indent(8), - ]), - alloc.concat([ - alloc.reflow( - "Notice the indentation. All patterns are aligned, and each branch is indented", - ), - alloc.reflow(" a bit more than the corresponding pattern. That is important!"), - ]), - ]) -} - -fn note_for_when_indent_error<'a>(alloc: &'a RocDocAllocator<'a>) -> RocDocBuilder<'a> { - alloc.stack([ - alloc.concat([ - alloc.note("Sometimes I get confused by indentation, so try to make your "), - alloc.keyword("when"), - alloc.reflow(r" look something like this:"), - ]), - alloc.vcat(vec![ - alloc.text("when List.first plants is").indent(4), - alloc.text("Ok n ->").indent(6), - alloc.text("n").indent(8), - alloc.text(""), - alloc.text("Err _ ->").indent(6), - alloc.text("200").indent(8), - ]), - alloc.concat([ - alloc.reflow( - "Notice the indentation. All patterns are aligned, and each branch is indented", - ), - alloc.reflow(" a bit more than the corresponding pattern. That is important!"), - ]), - ]) -} - -fn to_pattern_report<'a>( - alloc: &'a RocDocAllocator<'a>, - lines: &LineInfo, - filename: PathBuf, - parse_problem: &roc_parse::parser::EPattern<'a>, - start: Position, -) -> Report<'a> { - use roc_parse::parser::EPattern; - - match parse_problem { - EPattern::Start(pos) => { - let surroundings = Region::new(start, *pos); - let region = LineColumnRegion::from_pos(lines.convert_pos(*pos)); - - let doc = alloc.stack([ - alloc.reflow(r"I just started parsing a pattern, but I got stuck here:"), - alloc.region_with_subregion(lines.convert_region(surroundings), region), - alloc.note("I may be confused by indentation"), - ]); - - Report { - filename, - doc, - title: "UNFINISHED PATTERN".to_string(), - severity: Severity::RuntimeError, - } - } - EPattern::Record(record, pos) => to_precord_report(alloc, lines, filename, record, *pos), - EPattern::PInParens(inparens, pos) => { - to_pattern_in_parens_report(alloc, lines, filename, inparens, *pos) - } - &EPattern::NumLiteral(ENumber::End, pos) => { - to_malformed_number_literal_report(alloc, lines, filename, pos) - } - _ => todo!("unhandled parse error: {:?}", parse_problem), - } -} - -fn to_precord_report<'a>( - alloc: &'a RocDocAllocator<'a>, - lines: &LineInfo, - filename: PathBuf, - parse_problem: &roc_parse::parser::PRecord<'a>, - start: Position, -) -> Report<'a> { - use roc_parse::parser::PRecord; - - match *parse_problem { - PRecord::Open(pos) => match what_is_next(alloc.src_lines, lines.convert_pos(pos)) { - Next::Keyword(keyword) => { - let surroundings = Region::new(start, pos); - let region = to_keyword_region(lines.convert_pos(pos), keyword); - - let doc = alloc.stack([ - alloc.reflow(r"I just started parsing a record pattern, but I got stuck on this field name:"), - alloc.region_with_subregion(lines.convert_region(surroundings), region), - alloc.concat([ - alloc.reflow(r"Looks like you are trying to use "), - alloc.keyword(keyword), - alloc.reflow(" as a field name, but that is a reserved word. Try using a different name!"), - ]), - ]); - - Report { - filename, - doc, - title: "UNFINISHED RECORD PATTERN".to_string(), - severity: Severity::RuntimeError, - } - } - _ => { - let surroundings = Region::new(start, pos); - let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); - - let doc = alloc.stack([ - alloc.reflow(r"I just started parsing a record pattern, but I got stuck here:"), - alloc.region_with_subregion(lines.convert_region(surroundings), region), - record_patterns_look_like(alloc), - ]); - - Report { - filename, - doc, - title: "UNFINISHED RECORD PATTERN".to_string(), - severity: Severity::RuntimeError, - } - } - }, - - PRecord::End(pos) => { - let surroundings = Region::new(start, pos); - let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); - - match what_is_next(alloc.src_lines, lines.convert_pos(pos)) { - Next::Other(Some(c)) if c.is_alphabetic() => { - let doc = alloc.stack([ - alloc.reflow(r"I am partway through parsing a record pattern, but I got stuck here:"), - alloc.region_with_subregion(lines.convert_region(surroundings), region), - alloc.concat([ - alloc.reflow( - r"I was expecting to see a colon, question mark, comma or closing curly brace.", - ), - ]), - ]); - - Report { - filename, - doc, - title: "UNFINISHED RECORD PATTERN".to_string(), - severity: Severity::RuntimeError, - } - } - _ => { - let doc = alloc.stack([ - alloc.reflow("I am partway through parsing a record pattern, but I got stuck here:"), - alloc.region_with_subregion(lines.convert_region(surroundings), region), - alloc.concat([ - alloc.reflow( - r"I was expecting to see a closing curly brace before this, so try adding a ", - ), - alloc.parser_suggestion("}"), - alloc.reflow(" and see if that helps?"), - ]), - ]); - - Report { - filename, - doc, - title: "UNFINISHED RECORD PATTERN".to_string(), - severity: Severity::RuntimeError, - } - } - } - } - - PRecord::Field(pos) => match what_is_next(alloc.src_lines, lines.convert_pos(pos)) { - Next::Keyword(keyword) => { - let surroundings = Region::new(start, pos); - let region = to_keyword_region(lines.convert_pos(pos), keyword); - - let doc = alloc.stack([ - alloc.reflow(r"I just started parsing a record pattern, but I got stuck on this field name:"), - alloc.region_with_subregion(lines.convert_region(surroundings), region), - alloc.concat([ - alloc.reflow(r"Looks like you are trying to use "), - alloc.keyword(keyword), - alloc.reflow(" as a field name, but that is a reserved word. Try using a different name!"), - ]), - ]); - - Report { - filename, - doc, - title: "UNFINISHED RECORD PATTERN".to_string(), - severity: Severity::RuntimeError, - } - } - Next::Other(Some(',')) => todo!(), - Next::Other(Some('}')) => unreachable!("or is it?"), - _ => { - let surroundings = Region::new(start, pos); - let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); - - let doc = alloc.stack([ - alloc.reflow(r"I am partway through parsing a record pattern, but I got stuck here:"), - alloc.region_with_subregion(lines.convert_region(surroundings), region), - alloc.concat([ - alloc.reflow(r"I was expecting to see another record field defined next, so I am looking for a name like "), - alloc.parser_suggestion("userName"), - alloc.reflow(" or "), - alloc.parser_suggestion("plantHight"), - alloc.reflow("."), - ]), - ]); - - Report { - filename, - doc, - title: "PROBLEM IN RECORD PATTERN".to_string(), - severity: Severity::RuntimeError, - } - } - }, - - PRecord::Colon(_) => { - unreachable!("because `foo` is a valid field; the colon is not required") - } - PRecord::Optional(_) => { - unreachable!("because `foo` is a valid field; the question mark is not required") - } - - PRecord::Pattern(pattern, pos) => to_pattern_report(alloc, lines, filename, pattern, pos), - - PRecord::Expr(expr, pos) => to_expr_report( - alloc, - lines, - filename, - Context::InNode( - Node::RecordConditionalDefault, - start, - Box::new(Context::InDef(pos)), - ), - expr, - pos, - ), - - PRecord::IndentOpen(pos) => { - let surroundings = Region::new(start, pos); - let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); - - let doc = alloc.stack([ - alloc.reflow(r"I just started parsing a record pattern, but I got stuck here:"), - alloc.region_with_subregion(lines.convert_region(surroundings), region), - record_patterns_look_like(alloc), - note_for_record_pattern_indent(alloc), - ]); - - Report { - filename, - doc, - title: "UNFINISHED RECORD PATTERN".to_string(), - severity: Severity::RuntimeError, - } - } - - PRecord::IndentEnd(pos) => { - match next_line_starts_with_close_curly(alloc.src_lines, lines.convert_pos(pos)) { - Some(curly_pos) => { - let surroundings = LineColumnRegion::new(lines.convert_pos(start), curly_pos); - let region = LineColumnRegion::from_pos(curly_pos); - - let doc = alloc.stack([ - alloc.reflow( - "I am partway through parsing a record pattern, but I got stuck here:", - ), - alloc.region_with_subregion(surroundings, region), - alloc.concat([ - alloc.reflow("I need this curly brace to be indented more. Try adding more spaces before it!"), - ]), - ]); - - Report { - filename, - doc, - title: "NEED MORE INDENTATION".to_string(), - severity: Severity::RuntimeError, - } - } - None => { - let surroundings = Region::new(start, pos); - let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); - - let doc = alloc.stack([ - alloc.reflow( - r"I am partway through parsing a record pattern, but I got stuck here:", - ), - alloc.region_with_subregion(lines.convert_region(surroundings), region), - alloc.concat([ - alloc.reflow("I was expecting to see a closing curly "), - alloc.reflow("brace before this, so try adding a "), - alloc.parser_suggestion("}"), - alloc.reflow(" and see if that helps?"), - ]), - note_for_record_pattern_indent(alloc), - ]); - - Report { - filename, - doc, - title: "UNFINISHED RECORD PATTERN".to_string(), - severity: Severity::RuntimeError, - } - } - } - } - - PRecord::IndentColon(_) => { - unreachable!("because `foo` is a valid field; the colon is not required") - } - - PRecord::IndentOptional(_) => { - unreachable!("because `foo` is a valid field; the question mark is not required") - } - - PRecord::Space(error, pos) => to_space_report(alloc, lines, filename, &error, pos), - } -} - -fn to_pattern_in_parens_report<'a>( - alloc: &'a RocDocAllocator<'a>, - lines: &LineInfo, - filename: PathBuf, - parse_problem: &roc_parse::parser::PInParens<'a>, - start: Position, -) -> Report<'a> { - use roc_parse::parser::PInParens; - - match *parse_problem { - PInParens::Open(pos) => { - // `Open` case is for exhaustiveness, this case shouldn not be reachable practically. - let surroundings = Region::new(start, pos); - let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); - - let doc = alloc.stack([ - alloc.reflow( - r"I just started parsing a pattern in parentheses, but I got stuck here:", - ), - alloc.region_with_subregion(lines.convert_region(surroundings), region), - alloc.concat([ - alloc.reflow(r"A pattern in parentheses looks like "), - alloc.parser_suggestion("(Ok 32)"), - alloc.reflow(r" or "), - alloc.parser_suggestion("(\"hello\")"), - alloc.reflow(" so I was expecting to see an expression next."), - ]), - ]); - - Report { - filename, - doc, - title: "UNFINISHED PARENTHESES".to_string(), - severity: Severity::RuntimeError, - } - } - - PInParens::End(pos) => { - let surroundings = Region::new(start, pos); - let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); - - let doc = alloc.stack([ - alloc.reflow("I am partway through parsing a pattern in parentheses, but I got stuck here:"), - alloc.region_with_subregion(lines.convert_region(surroundings), region), - alloc.concat([ - alloc.reflow( - r"I was expecting to see a closing parenthesis before this, so try adding a ", - ), - alloc.parser_suggestion(")"), - alloc.reflow(" and see if that helps?"), - ]), - ]); - - Report { - filename, - doc, - title: "UNFINISHED PARENTHESES".to_string(), - severity: Severity::RuntimeError, - } - } - - PInParens::Pattern(pattern, pos) => to_pattern_report(alloc, lines, filename, pattern, pos), - - PInParens::IndentOpen(pos) => { - let surroundings = Region::new(start, pos); - let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); - - let doc = alloc.stack([ - alloc.reflow( - r"I just started parsing a pattern in parentheses, but I got stuck here:", - ), - alloc.region_with_subregion(lines.convert_region(surroundings), region), - record_patterns_look_like(alloc), - note_for_record_pattern_indent(alloc), - ]); - - Report { - filename, - doc, - title: "UNFINISHED PARENTHESES".to_string(), - severity: Severity::RuntimeError, - } - } - - PInParens::IndentEnd(pos) => { - match next_line_starts_with_close_parenthesis(alloc.src_lines, lines.convert_pos(pos)) { - Some(curly_pos) => { - let surroundings = LineColumnRegion::new(lines.convert_pos(start), curly_pos); - let region = LineColumnRegion::from_pos(curly_pos); - - let doc = alloc.stack([ - alloc.reflow( - "I am partway through parsing a pattern in parentheses, but I got stuck here:", - ), - alloc.region_with_subregion(surroundings, region), - alloc.concat([ - alloc.reflow("I need this parenthesis to be indented more. Try adding more spaces before it!"), - ]), - ]); - - Report { - filename, - doc, - title: "NEED MORE INDENTATION".to_string(), - severity: Severity::RuntimeError, - } - } - None => { - let surroundings = Region::new(start, pos); - let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); - - let doc = alloc.stack([ - alloc.reflow( - r"I am partway through parsing a pattern in parentheses, but I got stuck here:", - ), - alloc.region_with_subregion(lines.convert_region(surroundings), region), - alloc.concat([ - alloc.reflow("I was expecting to see a closing parenthesis "), - alloc.reflow("before this, so try adding a "), - alloc.parser_suggestion(")"), - alloc.reflow(" and see if that helps?"), - ]), - note_for_record_pattern_indent(alloc), - ]); - - Report { - filename, - doc, - title: "UNFINISHED PARENTHESES".to_string(), - severity: Severity::RuntimeError, - } - } - } - } - - PInParens::Space(error, pos) => to_space_report(alloc, lines, filename, &error, pos), - } -} - -fn to_malformed_number_literal_report<'a>( - alloc: &'a RocDocAllocator<'a>, - lines: &LineInfo, - filename: PathBuf, - start: Position, -) -> Report<'a> { - let surroundings = Region::new(start, start); - let region = LineColumnRegion::from_pos(lines.convert_pos(start)); - - let doc = alloc.stack([ - alloc.reflow(r"This number literal is malformed:"), - alloc.region_with_subregion(lines.convert_region(surroundings), region), - ]); - - Report { - filename, - doc, - title: "INVALID NUMBER LITERAL".to_string(), - severity: Severity::RuntimeError, - } -} - -fn to_type_report<'a>( - alloc: &'a RocDocAllocator<'a>, - lines: &LineInfo, - filename: PathBuf, - parse_problem: &roc_parse::parser::EType<'a>, - start: Position, -) -> Report<'a> { - use roc_parse::parser::EType; - - match parse_problem { - EType::TRecord(record, pos) => to_trecord_report(alloc, lines, filename, record, *pos), - EType::TTagUnion(tag_union, pos) => { - to_ttag_union_report(alloc, lines, filename, tag_union, *pos) - } - EType::TInParens(tinparens, pos) => { - to_tinparens_report(alloc, lines, filename, tinparens, *pos) - } - EType::TApply(tapply, pos) => to_tapply_report(alloc, lines, filename, tapply, *pos), - EType::TInlineAlias(talias, _) => to_talias_report(alloc, lines, filename, talias), - - EType::TFunctionArgument(pos) => { - match what_is_next(alloc.src_lines, lines.convert_pos(*pos)) { - Next::Other(Some(',')) => { - let surroundings = Region::new(start, *pos); - let region = LineColumnRegion::from_pos(lines.convert_pos(*pos)); - - let doc = alloc.stack([ - alloc.reflow(r"I just started parsing a function argument type, but I encountered two commas in a row:"), - alloc.region_with_subregion(lines.convert_region(surroundings), region), - alloc.concat([alloc.reflow("Try removing one of them.")]), - ]); - - Report { - filename, - doc, - title: "DOUBLE COMMA".to_string(), - severity: Severity::RuntimeError, - } - } - _ => todo!(), - } - } - - EType::TStart(pos) => { - let surroundings = Region::new(start, *pos); - let region = LineColumnRegion::from_pos(lines.convert_pos(*pos)); - - let doc = alloc.stack([ - alloc.reflow(r"I just started parsing a type, but I got stuck here:"), - alloc.region_with_subregion(lines.convert_region(surroundings), region), - alloc.concat([ - alloc.reflow(r"I am expecting a type next, like "), - alloc.parser_suggestion("Bool"), - alloc.reflow(r" or "), - alloc.parser_suggestion("List a"), - alloc.reflow("."), - ]), - ]); - - Report { - filename, - doc, - title: "UNFINISHED TYPE".to_string(), - severity: Severity::RuntimeError, - } - } - - EType::TIndentStart(pos) => { - let surroundings = Region::new(start, *pos); - let region = LineColumnRegion::from_pos(lines.convert_pos(*pos)); - - let doc = alloc.stack([ - alloc.reflow(r"I just started parsing a type, but I got stuck here:"), - alloc.region_with_subregion(lines.convert_region(surroundings), region), - alloc.note("I may be confused by indentation"), - ]); - - Report { - filename, - doc, - title: "UNFINISHED TYPE".to_string(), - severity: Severity::RuntimeError, - } - } - - EType::TIndentEnd(pos) => { - let surroundings = Region::new(start, *pos); - let region = LineColumnRegion::from_pos(lines.convert_pos(*pos)); - - let doc = alloc.stack([ - alloc.reflow(r"I am partway through parsing a type, but I got stuck here:"), - alloc.region_with_subregion(lines.convert_region(surroundings), region), - alloc.note("I may be confused by indentation"), - ]); - - Report { - filename, - doc, - title: "UNFINISHED TYPE".to_string(), - severity: Severity::RuntimeError, - } - } - - EType::TAsIndentStart(pos) => { - let surroundings = Region::new(start, *pos); - let region = LineColumnRegion::from_pos(lines.convert_pos(*pos)); - - let doc = alloc.stack([ - alloc.reflow(r"I just started parsing an inline type alias, but I got stuck here:"), - alloc.region_with_subregion(lines.convert_region(surroundings), region), - alloc.note("I may be confused by indentation"), - ]); - - Report { - filename, - doc, - title: "UNFINISHED INLINE ALIAS".to_string(), - severity: Severity::RuntimeError, - } - } - - EType::TBadTypeVariable(pos) => { - let surroundings = Region::new(start, *pos); - let region = LineColumnRegion::from_pos(lines.convert_pos(*pos)); - - let doc = alloc.stack([ - alloc.reflow(r"I am expecting a type variable, but I got stuck here:"), - alloc.region_with_subregion(lines.convert_region(surroundings), region), - ]); - - Report { - filename, - doc, - title: "BAD TYPE VARIABLE".to_string(), - severity: Severity::RuntimeError, - } - } - - _ => todo!("unhandled type parse error: {:?}", &parse_problem), - } -} - -fn to_trecord_report<'a>( - alloc: &'a RocDocAllocator<'a>, - lines: &LineInfo, - filename: PathBuf, - parse_problem: &roc_parse::parser::ETypeRecord<'a>, - start: Position, -) -> Report<'a> { - use roc_parse::parser::ETypeRecord; - - match *parse_problem { - ETypeRecord::Open(pos) => match what_is_next(alloc.src_lines, lines.convert_pos(pos)) { - Next::Keyword(keyword) => { - let surroundings = Region::new(start, pos); - let region = to_keyword_region(lines.convert_pos(pos), keyword); - - let doc = alloc.stack([ - alloc.reflow(r"I just started parsing a record type, but I got stuck on this field name:"), - alloc.region_with_subregion(lines.convert_region(surroundings), region), - alloc.concat([ - alloc.reflow(r"Looks like you are trying to use "), - alloc.keyword(keyword), - alloc.reflow(" as a field name, but that is a reserved word. Try using a different name!"), - ]), - ]); - - Report { - filename, - doc, - title: "UNFINISHED RECORD TYPE".to_string(), - severity: Severity::RuntimeError, - } - } - _ => { - let surroundings = Region::new(start, pos); - let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); - - let doc = alloc.stack([ - alloc.reflow(r"I just started parsing a record type, but I got stuck here:"), - alloc.region_with_subregion(lines.convert_region(surroundings), region), - alloc.concat([ - alloc.reflow(r"Record types look like "), - alloc.parser_suggestion("{ name : String, age : Int },"), - alloc.reflow(" so I was expecting to see a field name next."), - ]), - ]); - - Report { - filename, - doc, - title: "UNFINISHED RECORD TYPE".to_string(), - severity: Severity::RuntimeError, - } - } - }, - - ETypeRecord::End(pos) => { - let surroundings = Region::new(start, pos); - let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); - - match what_is_next(alloc.src_lines, lines.convert_pos(pos)) { - Next::Other(Some(c)) if c.is_alphabetic() => { - let doc = alloc.stack([ - alloc.reflow(r"I am partway through parsing a record type, but I got stuck here:"), - alloc.region_with_subregion(lines.convert_region(surroundings), region), - alloc.concat([ - alloc.reflow( - r"I was expecting to see a colon, question mark, comma or closing curly brace.", - ), - ]), - ]); - - Report { - filename, - doc, - title: "UNFINISHED RECORD TYPE".to_string(), - severity: Severity::RuntimeError, - } - } - _ => { - let doc = alloc.stack([ - alloc.reflow("I am partway through parsing a record type, but I got stuck here:"), - alloc.region_with_subregion(lines.convert_region(surroundings), region), - alloc.concat([ - alloc.reflow( - r"I was expecting to see a closing curly brace before this, so try adding a ", - ), - alloc.parser_suggestion("}"), - alloc.reflow(" and see if that helps?"), - ]), - ]); - - Report { - filename, - doc, - title: "UNFINISHED RECORD TYPE".to_string(), - severity: Severity::RuntimeError, - } - } - } - } - - ETypeRecord::Field(pos) => match what_is_next(alloc.src_lines, lines.convert_pos(pos)) { - Next::Keyword(keyword) => { - let surroundings = Region::new(start, pos); - let region = to_keyword_region(lines.convert_pos(pos), keyword); - - let doc = alloc.stack([ - alloc.reflow(r"I just started parsing a record type, but I got stuck on this field name:"), - alloc.region_with_subregion(lines.convert_region(surroundings), region), - alloc.concat([ - alloc.reflow(r"Looks like you are trying to use "), - alloc.keyword(keyword), - alloc.reflow(" as a field name, but that is a reserved word. Try using a different name!"), - ]), - ]); - - Report { - filename, - doc, - title: "UNFINISHED RECORD TYPE".to_string(), - severity: Severity::RuntimeError, - } - } - Next::Other(Some(',')) => todo!(), - Next::Other(Some('}')) => unreachable!("or is it?"), - _ => { - let surroundings = Region::new(start, pos); - let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); - - let doc = alloc.stack([ - alloc.reflow(r"I am partway through parsing a record type, but I got stuck here:"), - alloc.region_with_subregion(lines.convert_region(surroundings), region), - alloc.concat([ - alloc.reflow(r"I was expecting to see another record field defined next, so I am looking for a name like "), - alloc.parser_suggestion("userName"), - alloc.reflow(" or "), - alloc.parser_suggestion("plantHight"), - alloc.reflow("."), - ]), - ]); - - Report { - filename, - doc, - title: "PROBLEM IN RECORD TYPE".to_string(), - severity: Severity::RuntimeError, - } - } - }, - - ETypeRecord::Colon(_) => { - unreachable!("because `foo` is a valid field; the colon is not required") - } - ETypeRecord::Optional(_) => { - unreachable!("because `foo` is a valid field; the question mark is not required") - } - - ETypeRecord::Type(tipe, pos) => to_type_report(alloc, lines, filename, tipe, pos), - - ETypeRecord::IndentOpen(pos) => { - let surroundings = Region::new(start, pos); - let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); - - let doc = alloc.stack([ - alloc.reflow(r"I just started parsing a record type, but I got stuck here:"), - alloc.region_with_subregion(lines.convert_region(surroundings), region), - alloc.concat([ - alloc.reflow(r"Record types look like "), - alloc.parser_suggestion("{ name : String, age : Int },"), - alloc.reflow(" so I was expecting to see a field name next."), - ]), - note_for_record_type_indent(alloc), - ]); - - Report { - filename, - doc, - title: "UNFINISHED RECORD TYPE".to_string(), - severity: Severity::RuntimeError, - } - } - - ETypeRecord::IndentEnd(pos) => { - match next_line_starts_with_close_curly(alloc.src_lines, lines.convert_pos(pos)) { - Some(curly_pos) => { - let surroundings = LineColumnRegion::new(lines.convert_pos(start), curly_pos); - let region = LineColumnRegion::from_pos(curly_pos); - - let doc = alloc.stack([ - alloc.reflow( - "I am partway through parsing a record type, but I got stuck here:", - ), - alloc.region_with_subregion(surroundings, region), - alloc.concat([ - alloc.reflow("I need this curly brace to be indented more. Try adding more spaces before it!"), - ]), - ]); - - Report { - filename, - doc, - title: "NEED MORE INDENTATION".to_string(), - severity: Severity::RuntimeError, - } - } - None => { - let surroundings = Region::new(start, pos); - let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); - - let doc = alloc.stack([ - alloc.reflow( - r"I am partway through parsing a record type, but I got stuck here:", - ), - alloc.region_with_subregion(lines.convert_region(surroundings), region), - alloc.concat([ - alloc.reflow("I was expecting to see a closing curly "), - alloc.reflow("brace before this, so try adding a "), - alloc.parser_suggestion("}"), - alloc.reflow(" and see if that helps?"), - ]), - note_for_record_type_indent(alloc), - ]); - - Report { - filename, - doc, - title: "UNFINISHED RECORD TYPE".to_string(), - severity: Severity::RuntimeError, - } - } - } - } - - ETypeRecord::IndentColon(_) => { - unreachable!("because `foo` is a valid field; the colon is not required") - } - - ETypeRecord::IndentOptional(_) => { - unreachable!("because `foo` is a valid field; the question mark is not required") - } - - ETypeRecord::Space(error, pos) => to_space_report(alloc, lines, filename, &error, pos), - } -} - -fn to_ttag_union_report<'a>( - alloc: &'a RocDocAllocator<'a>, - lines: &LineInfo, - filename: PathBuf, - parse_problem: &roc_parse::parser::ETypeTagUnion<'a>, - start: Position, -) -> Report<'a> { - use roc_parse::parser::ETypeTagUnion; - - match *parse_problem { - ETypeTagUnion::Open(pos) => match what_is_next(alloc.src_lines, lines.convert_pos(pos)) { - Next::Keyword(keyword) => { - let surroundings = Region::new(start, pos); - let region = to_keyword_region(lines.convert_pos(pos), keyword); - - let doc = alloc.stack([ - alloc.reflow(r"I just started parsing a tag union, but I got stuck on this field name:"), - alloc.region_with_subregion(lines.convert_region(surroundings), region), - alloc.concat([ - alloc.reflow(r"Looks like you are trying to use "), - alloc.keyword(keyword), - alloc.reflow(" as a tag name, but that is a reserved word. Tag names must start with a uppercase letter."), - ]), - ]); - - Report { - filename, - doc, - title: "UNFINISHED TAG UNION TYPE".to_string(), - severity: Severity::RuntimeError, - } - } - Next::Other(Some(c)) if c.is_alphabetic() => { - debug_assert!(c.is_lowercase()); - - let surroundings = Region::new(start, pos); - let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); - - let doc = alloc.stack([ - alloc.reflow( - r"I am partway through parsing a tag union type, but I got stuck here:", - ), - alloc.region_with_subregion(lines.convert_region(surroundings), region), - alloc.reflow(r"I was expecting to see a tag name."), - hint_for_tag_name(alloc), - ]); - - Report { - filename, - doc, - title: "WEIRD TAG NAME".to_string(), - severity: Severity::RuntimeError, - } - } - _ => { - let surroundings = Region::new(start, pos); - let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); - - let doc = alloc.stack([ - alloc.reflow(r"I just started parsing a tag union type, but I got stuck here:"), - alloc.region_with_subregion(lines.convert_region(surroundings), region), - alloc.concat([ - alloc.reflow(r"Tag unions look like "), - alloc.parser_suggestion("[Many I64, None],"), - alloc.reflow(" so I was expecting to see a tag name next."), - ]), - ]); - - Report { - filename, - doc, - title: "UNFINISHED TAG UNION TYPE".to_string(), - severity: Severity::RuntimeError, - } - } - }, - - ETypeTagUnion::End(pos) => { - let surroundings = Region::new(start, pos); - let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); - - match what_is_next(alloc.src_lines, lines.convert_pos(pos)) { - Next::Other(Some(c)) if c.is_alphabetic() => { - debug_assert!(c.is_lowercase()); - - let doc = alloc.stack([ - alloc.reflow( - r"I am partway through parsing a tag union type, but I got stuck here:", - ), - alloc.region_with_subregion(lines.convert_region(surroundings), region), - alloc.reflow(r"I was expecting to see a tag name."), - hint_for_tag_name(alloc), - ]); - - Report { - filename, - doc, - title: "WEIRD TAG NAME".to_string(), - severity: Severity::RuntimeError, - } - } - _ => { - let doc = alloc.stack([ - alloc.reflow(r"I am partway through parsing a tag union type, but I got stuck here:"), - alloc.region_with_subregion(lines.convert_region(surroundings), region), - alloc.concat([ - alloc.reflow( - r"I was expecting to see a closing square bracket before this, so try adding a ", - ), - alloc.parser_suggestion("]"), - alloc.reflow(" and see if that helps?"), - ]), - ]); - - Report { - filename, - doc, - title: "UNFINISHED TAG UNION TYPE".to_string(), - severity: Severity::RuntimeError, - } - } - } - } - - ETypeTagUnion::Type(tipe, pos) => to_type_report(alloc, lines, filename, tipe, pos), - - ETypeTagUnion::IndentOpen(pos) => { - let surroundings = Region::new(start, pos); - let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); - - let doc = alloc.stack([ - alloc.reflow(r"I just started parsing a tag union type, but I got stuck here:"), - alloc.region_with_subregion(lines.convert_region(surroundings), region), - alloc.concat([ - alloc.reflow(r"Tag unions look like "), - alloc.parser_suggestion("[Many I64, None],"), - alloc.reflow(" so I was expecting to see a tag name next."), - ]), - note_for_tag_union_type_indent(alloc), - ]); - - Report { - filename, - doc, - title: "UNFINISHED TAG UNION TYPE".to_string(), - severity: Severity::RuntimeError, - } - } - - ETypeTagUnion::IndentEnd(pos) => { - match next_line_starts_with_close_square_bracket( - alloc.src_lines, - lines.convert_pos(pos), - ) { - Some(curly_pos) => { - let surroundings = LineColumnRegion::new(lines.convert_pos(start), curly_pos); - let region = LineColumnRegion::from_pos(curly_pos); - - let doc = alloc.stack([ - alloc.reflow( - "I am partway through parsing a tag union type, but I got stuck here:", - ), - alloc.region_with_subregion(surroundings, region), - alloc.concat([ - alloc.reflow("I need this square bracket to be indented more. Try adding more spaces before it!"), - ]), - ]); - - Report { - filename, - doc, - title: "NEED MORE INDENTATION".to_string(), - severity: Severity::RuntimeError, - } - } - None => { - let surroundings = Region::new(start, pos); - let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); - - let doc = alloc.stack([ - alloc.reflow( - r"I am partway through parsing a tag union type, but I got stuck here:", - ), - alloc.region_with_subregion(lines.convert_region(surroundings), region), - alloc.concat([ - alloc.reflow("I was expecting to see a closing square "), - alloc.reflow("bracket before this, so try adding a "), - alloc.parser_suggestion("]"), - alloc.reflow(" and see if that helps?"), - ]), - note_for_tag_union_type_indent(alloc), - ]); - - Report { - filename, - doc, - title: "UNFINISHED TAG UNION TYPE".to_string(), - severity: Severity::RuntimeError, - } - } - } - } - - ETypeTagUnion::Space(error, pos) => to_space_report(alloc, lines, filename, &error, pos), - } -} - -fn to_tinparens_report<'a>( - alloc: &'a RocDocAllocator<'a>, - lines: &LineInfo, - filename: PathBuf, - parse_problem: &roc_parse::parser::ETypeInParens<'a>, - start: Position, -) -> Report<'a> { - use roc_parse::parser::ETypeInParens; - - match *parse_problem { - ETypeInParens::Open(pos) => { - match what_is_next(alloc.src_lines, lines.convert_pos(pos)) { - Next::Keyword(keyword) => { - let surroundings = Region::new(start, pos); - let region = to_keyword_region(lines.convert_pos(pos), keyword); - - let doc = alloc.stack([ - alloc.reflow(r"I just saw an open parenthesis, so I was expecting to see a type next."), - alloc.region_with_subregion(lines.convert_region(surroundings), region), - alloc.concat([ - alloc.reflow(r"Something like "), - alloc.parser_suggestion("(List Person)"), - alloc.text(" or "), - alloc.parser_suggestion("(Result I64 Str)"), - ]), - ]); - - Report { - filename, - doc, - title: "UNFINISHED PARENTHESES".to_string(), - severity: Severity::RuntimeError, - } - } - Next::Other(Some(c)) if c.is_alphabetic() => { - debug_assert!(c.is_lowercase()); - - let surroundings = Region::new(start, pos); - let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); - - let doc = alloc.stack([ - alloc.reflow( - r"I am partway through parsing a type in parentheses, but I got stuck here:", - ), - alloc.region_with_subregion(lines.convert_region(surroundings), region), - alloc.reflow(r"I was expecting to see a tag name."), - hint_for_tag_name(alloc), - ]); - - Report { - filename, - doc, - title: "WEIRD TAG NAME".to_string(), - severity: Severity::RuntimeError, - } - } - _ => { - let surroundings = Region::new(start, pos); - let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); - - let doc = alloc.stack([ - alloc.reflow( - r"I just started parsing a type in parentheses, but I got stuck here:", - ), - alloc.region_with_subregion(lines.convert_region(surroundings), region), - alloc.concat([ - alloc.reflow(r"Tag unions look like "), - alloc.parser_suggestion("[Many I64, None],"), - alloc.reflow(" so I was expecting to see a tag name next."), - ]), - ]); - - Report { - filename, - doc, - title: "UNFINISHED PARENTHESES".to_string(), - severity: Severity::RuntimeError, - } - } - } - } - - ETypeInParens::End(pos) => { - let surroundings = Region::new(start, pos); - let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); - - match what_is_next(alloc.src_lines, lines.convert_pos(pos)) { - Next::Other(Some(c)) if c.is_alphabetic() => { - debug_assert!(c.is_lowercase()); - - // TODO hint for tuples? - let doc = alloc.stack([ - alloc.reflow( - r"I am partway through parsing a type in parentheses, but I got stuck here:", - ), - alloc.region_with_subregion(lines.convert_region(surroundings), region), - alloc.reflow(r"I was expecting to see a tag name."), - hint_for_tag_name(alloc), - ]); - - Report { - filename, - doc, - title: "WEIRD TAG NAME".to_string(), - severity: Severity::RuntimeError, - } - } - _ => { - let doc = alloc.stack([ - alloc.reflow(r"I am partway through parsing a type in parentheses, but I got stuck here:"), - alloc.region_with_subregion(lines.convert_region(surroundings), region), - alloc.concat([ - alloc.reflow( - r"I was expecting to see a closing parenthesis before this, so try adding a ", - ), - alloc.parser_suggestion(")"), - alloc.reflow(" and see if that helps?"), - ]), - ]); - - Report { - filename, - doc, - title: "UNFINISHED PARENTHESES".to_string(), - severity: Severity::RuntimeError, - } - } - } - } - - ETypeInParens::Type(tipe, pos) => to_type_report(alloc, lines, filename, tipe, pos), - - ETypeInParens::IndentOpen(pos) => { - let surroundings = Region::new(start, pos); - let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); - - let doc = alloc.stack([ - alloc - .reflow(r"I just started parsing a type in parentheses, but I got stuck here:"), - alloc.region_with_subregion(lines.convert_region(surroundings), region), - alloc.concat([ - alloc.reflow(r"Tag unions look like "), - alloc.parser_suggestion("[Many I64, None],"), - alloc.reflow(" so I was expecting to see a tag name next."), - ]), - note_for_tag_union_type_indent(alloc), - ]); - - Report { - filename, - doc, - title: "UNFINISHED PARENTHESES".to_string(), - severity: Severity::RuntimeError, - } - } - - ETypeInParens::IndentEnd(pos) => { - match next_line_starts_with_close_parenthesis(alloc.src_lines, lines.convert_pos(pos)) { - Some(curly_pos) => { - let surroundings = LineColumnRegion::new(lines.convert_pos(start), curly_pos); - let region = LineColumnRegion::from_pos(curly_pos); - - let doc = alloc.stack([ - alloc.reflow( - "I am partway through parsing a type in parentheses, but I got stuck here:", - ), - alloc.region_with_subregion(surroundings, region), - alloc.concat([ - alloc.reflow("I need this parenthesis to be indented more. Try adding more spaces before it!"), - ]), - ]); - - Report { - filename, - doc, - title: "NEED MORE INDENTATION".to_string(), - severity: Severity::RuntimeError, - } - } - None => { - let surroundings = Region::new(start, pos); - let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); - - let doc = alloc.stack([ - alloc.reflow( - r"I am partway through parsing a type in parentheses, but I got stuck here:", - ), - alloc.region_with_subregion(lines.convert_region(surroundings), region), - alloc.concat([ - alloc.reflow("I was expecting to see a parenthesis "), - alloc.reflow("before this, so try adding a "), - alloc.parser_suggestion(")"), - alloc.reflow(" and see if that helps?"), - ]), - note_for_tag_union_type_indent(alloc), - ]); - - Report { - filename, - doc, - title: "UNFINISHED PARENTHESES".to_string(), - severity: Severity::RuntimeError, - } - } - } - } - - ETypeInParens::Space(error, pos) => to_space_report(alloc, lines, filename, &error, pos), - } -} - -fn to_tapply_report<'a>( - alloc: &'a RocDocAllocator<'a>, - lines: &LineInfo, - filename: PathBuf, - parse_problem: &roc_parse::parser::ETypeApply, - _start: Position, -) -> Report<'a> { - use roc_parse::parser::ETypeApply; - - match *parse_problem { - ETypeApply::DoubleDot(pos) => { - let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); - - let doc = alloc.stack([ - alloc.reflow(r"I encountered two dots in a row:"), - alloc.region(region), - alloc.concat([alloc.reflow("Try removing one of them.")]), - ]); - - Report { - filename, - doc, - title: "DOUBLE DOT".to_string(), - severity: Severity::RuntimeError, - } - } - ETypeApply::TrailingDot(pos) => { - let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); - - let doc = alloc.stack([ - alloc.reflow(r"I encountered a dot with nothing after it:"), - alloc.region(region), - alloc.concat([ - alloc.reflow("Dots are used to refer to a type in a qualified way, like "), - alloc.parser_suggestion("Num.I64"), - alloc.text(" or "), - alloc.parser_suggestion("List.List a"), - alloc.reflow(". Try adding a type name next."), - ]), - ]); - - Report { - filename, - doc, - title: "TRAILING DOT".to_string(), - severity: Severity::RuntimeError, - } - } - ETypeApply::StartIsNumber(pos) => { - let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); - - let doc = alloc.stack([ - alloc.reflow(r"I encountered a number at the start of a qualified name segment:"), - alloc.region(region), - alloc.concat([ - alloc.reflow("All parts of a qualified type name must start with an uppercase letter, like "), - alloc.parser_suggestion("Num.I64"), - alloc.text(" or "), - alloc.parser_suggestion("List.List a"), - alloc.text("."), - ]), - ]); - - Report { - filename, - doc, - title: "WEIRD QUALIFIED NAME".to_string(), - severity: Severity::RuntimeError, - } - } - ETypeApply::StartNotUppercase(pos) => { - let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); - - let doc = alloc.stack([ - alloc.reflow(r"I encountered a lowercase letter at the start of a qualified name segment:"), - alloc.region(region), - alloc.concat([ - alloc.reflow("All parts of a qualified type name must start with an uppercase letter, like "), - alloc.parser_suggestion("Num.I64"), - alloc.text(" or "), - alloc.parser_suggestion("List.List a"), - alloc.text("."), - ]), - ]); - - Report { - filename, - doc, - title: "WEIRD QUALIFIED NAME".to_string(), - severity: Severity::RuntimeError, - } - } - - ETypeApply::End(pos) => { - let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); - - let doc = alloc.stack([ - alloc.reflow( - r"I reached the end of the input file while parsing a qualified type name", - ), - alloc.region(region), - ]); - - Report { - filename, - doc, - title: "END OF FILE".to_string(), - severity: Severity::RuntimeError, - } - } - - ETypeApply::Space(error, pos) => to_space_report(alloc, lines, filename, &error, pos), - } -} - -fn to_talias_report<'a>( - alloc: &'a RocDocAllocator<'a>, - lines: &LineInfo, - filename: PathBuf, - parse_problem: &roc_parse::parser::ETypeInlineAlias, -) -> Report<'a> { - use roc_parse::parser::ETypeInlineAlias; - - match *parse_problem { - ETypeInlineAlias::NotAnAlias(pos) => { - let region = Region::from_pos(pos); - - let doc = alloc.stack([ - alloc.concat([ - alloc.reflow("The inline type after this "), - alloc.keyword("as"), - alloc.reflow(" is not a type alias:"), - ]), - alloc.region(lines.convert_region(region)), - alloc.concat([ - alloc.reflow("Inline alias types must start with an uppercase identifier and be followed by zero or more type arguments, like "), - alloc.type_str("Point"), - alloc.reflow(" or "), - alloc.type_str("List a"), - alloc.reflow("."), - ]), - ]); - - Report { - filename, - doc, - title: "NOT AN INLINE ALIAS".to_string(), - severity: Severity::RuntimeError, - } - } - ETypeInlineAlias::Qualified(pos) => { - let region = Region::from_pos(pos); - - let doc = alloc.stack([ - alloc.reflow(r"This type alias has a qualified name:"), - alloc.region(lines.convert_region(region)), - alloc.reflow("An alias introduces a new name to the current scope, so it must be unqualified."), - ]); - - Report { - filename, - doc, - title: "QUALIFIED ALIAS NAME".to_string(), - severity: Severity::RuntimeError, - } - } - ETypeInlineAlias::ArgumentNotLowercase(pos) => { - let region = Region::from_pos(pos); - - let doc = alloc.stack([ - alloc.reflow(r"This alias type argument is not lowercase:"), - alloc.region(lines.convert_region(region)), - alloc.reflow("All type arguments must be lowercase."), - ]); - - Report { - filename, - doc, - title: "TYPE ARGUMENT NOT LOWERCASE".to_string(), - severity: Severity::RuntimeError, - } - } - } -} - -fn to_header_report<'a>( - alloc: &'a RocDocAllocator<'a>, - lines: &LineInfo, - filename: PathBuf, - parse_problem: &roc_parse::parser::EHeader<'a>, - start: Position, -) -> Report<'a> { - use roc_parse::parser::EHeader; - - match parse_problem { - EHeader::Provides(provides, pos) => { - to_provides_report(alloc, lines, filename, provides, *pos) - } - - EHeader::Exposes(exposes, pos) => to_exposes_report(alloc, lines, filename, exposes, *pos), - - EHeader::Imports(imports, pos) => to_imports_report(alloc, lines, filename, imports, *pos), - - EHeader::Requires(requires, pos) => { - to_requires_report(alloc, lines, filename, requires, *pos) - } - - EHeader::Packages(packages, pos) => { - to_packages_report(alloc, lines, filename, packages, *pos) - } - - EHeader::IndentStart(pos) => { - let surroundings = Region::new(start, *pos); - let region = LineColumnRegion::from_pos(lines.convert_pos(*pos)); - - let doc = alloc.stack([ - alloc.reflow(r"I am partway through parsing a header, but got stuck here:"), - alloc.region_with_subregion(lines.convert_region(surroundings), region), - alloc.concat([alloc.reflow("I may be confused by indentation.")]), - ]); - - Report { - filename, - doc, - title: "INCOMPLETE HEADER".to_string(), - severity: Severity::RuntimeError, - } - } - - EHeader::Start(pos) => { - let surroundings = Region::new(start, *pos); - let region = LineColumnRegion::from_pos(lines.convert_pos(*pos)); - - let doc = alloc.stack([ - alloc.reflow(r"I am expecting a header, but got stuck here:"), - alloc.region_with_subregion(lines.convert_region(surroundings), region), - alloc.concat([ - alloc.reflow("I am expecting a module keyword next, one of "), - alloc.keyword("interface"), - alloc.reflow(", "), - alloc.keyword("app"), - alloc.reflow(" or "), - alloc.keyword("platform"), - alloc.reflow("."), - ]), - ]); - - Report { - filename, - doc, - title: "MISSING HEADER".to_string(), - severity: Severity::RuntimeError, - } - } - - EHeader::ModuleName(pos) => { - let surroundings = Region::new(start, *pos); - let region = LineColumnRegion::from_pos(lines.convert_pos(*pos)); - - let doc = alloc.stack([ - alloc.reflow(r"I am partway through parsing a header, but got stuck here:"), - alloc.region_with_subregion(lines.convert_region(surroundings), region), - alloc.concat([ - alloc.reflow("I am expecting a module name next, like "), - alloc.parser_suggestion("BigNum"), - alloc.reflow(" or "), - alloc.parser_suggestion("Main"), - alloc.reflow(". Module names must start with an uppercase letter."), - ]), - ]); - - Report { - filename, - doc, - title: "WEIRD MODULE NAME".to_string(), - severity: Severity::RuntimeError, - } - } - - EHeader::AppName(_, pos) => { - let surroundings = Region::new(start, *pos); - let region = LineColumnRegion::from_pos(lines.convert_pos(*pos)); - - let doc = alloc.stack([ - alloc.reflow(r"I am partway through parsing a header, but got stuck here:"), - alloc.region_with_subregion(lines.convert_region(surroundings), region), - alloc.concat([ - alloc.reflow("I am expecting an application name next, like "), - alloc.parser_suggestion("app \"main\""), - alloc.reflow(" or "), - alloc.parser_suggestion("app \"editor\""), - alloc.reflow(". App names are surrounded by quotation marks."), - ]), - ]); - - Report { - filename, - doc, - title: "WEIRD APP NAME".to_string(), - severity: Severity::RuntimeError, - } - } - - EHeader::PlatformName(_, pos) => { - let surroundings = Region::new(start, *pos); - let region = LineColumnRegion::from_pos(lines.convert_pos(*pos)); - - let doc = alloc.stack([ - alloc.reflow(r"I am partway through parsing a header, but got stuck here:"), - alloc.region_with_subregion(lines.convert_region(surroundings), region), - alloc.concat([ - alloc.reflow("I am expecting a platform name next, like "), - alloc.parser_suggestion("\"roc/core\""), - alloc.reflow(". Platform names must be quoted."), - ]), - ]); - - Report { - filename, - doc, - title: "WEIRD MODULE NAME".to_string(), - severity: Severity::RuntimeError, - } - } - - EHeader::Space(error, pos) => to_space_report(alloc, lines, filename, error, *pos), - EHeader::Generates(_, pos) => { - let surroundings = Region::new(start, *pos); - let region = LineColumnRegion::from_pos(lines.convert_pos(*pos)); - - let doc = alloc.stack([ - alloc.reflow(r"I am partway through parsing a header, but got stuck here:"), - alloc.region_with_subregion(lines.convert_region(surroundings), region), - alloc.concat([ - alloc.reflow("I am expecting a type name next, like "), - alloc.parser_suggestion("Effect"), - alloc.reflow(". Type names must start with an uppercase letter."), - ]), - ]); - - Report { - filename, - doc, - title: "WEIRD GENERATED TYPE NAME".to_string(), - severity: Severity::RuntimeError, - } - } - EHeader::GeneratesWith(generates_with, pos) => { - to_generates_with_report(alloc, lines, filename, generates_with, *pos) - } - } -} - -fn to_generates_with_report<'a>( - alloc: &'a RocDocAllocator<'a>, - lines: &LineInfo, - filename: PathBuf, - parse_problem: &roc_parse::parser::EGeneratesWith, - start: Position, -) -> Report<'a> { - use roc_parse::parser::EGeneratesWith; - - match *parse_problem { - EGeneratesWith::ListEnd(pos) | // TODO: give this its own error message - EGeneratesWith::Identifier(pos) => { - let surroundings = Region::new(start, pos); - let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); - - let doc = alloc.stack([ - alloc - .reflow(r"I am partway through parsing a provides list, but I got stuck here:"), - alloc.region_with_subregion(lines.convert_region(surroundings), region), - alloc.concat([alloc.reflow( - "I was expecting a type name, value name or function name next, like", - )]), - alloc - .parser_suggestion("provides [Animal, default, tame]") - .indent(4), - ]); - - Report { - filename, - doc, - title: "WEIRD GENERATES".to_string(), - severity: Severity::RuntimeError, - } - } - - EGeneratesWith::With(pos) => { - let surroundings = Region::new(start, pos); - let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); - - let doc = alloc.stack([ - alloc.reflow(r"I am partway through parsing a header, but I got stuck here:"), - alloc.region_with_subregion(lines.convert_region(surroundings), region), - alloc.concat([ - alloc.reflow("I am expecting the "), - alloc.keyword("with"), - alloc.reflow(" keyword next, like"), - ]), - alloc - .parser_suggestion("with [after, map]") - .indent(4), - ]); - - Report { - filename, - doc, - title: "WEIRD GENERATES".to_string(), - severity: Severity::RuntimeError, - } - } - - EGeneratesWith::Space(error, pos) => to_space_report(alloc, lines, filename, &error, pos), - - _ => todo!("unhandled parse error {:?}", parse_problem), - } -} - -fn to_provides_report<'a>( - alloc: &'a RocDocAllocator<'a>, - lines: &LineInfo, - filename: PathBuf, - parse_problem: &roc_parse::parser::EProvides, - start: Position, -) -> Report<'a> { - use roc_parse::parser::EProvides; - - match *parse_problem { - EProvides::ListEnd(pos) | // TODO: give this its own error message - EProvides::Identifier(pos) => { - let surroundings = Region::new(start, pos); - let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); - - let doc = alloc.stack([ - alloc - .reflow(r"I am partway through parsing a provides list, but I got stuck here:"), - alloc.region_with_subregion(lines.convert_region(surroundings), region), - alloc.concat([alloc.reflow( - "I was expecting a type name, value name or function name next, like", - )]), - alloc - .parser_suggestion("provides [Animal, default, tame]") - .indent(4), - ]); - - Report { - filename, - doc, - title: "WEIRD PROVIDES".to_string(), - severity: Severity::RuntimeError, - } - } - - EProvides::Provides(pos) => { - let surroundings = Region::new(start, pos); - let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); - - let doc = alloc.stack([ - alloc.reflow(r"I am partway through parsing a header, but I got stuck here:"), - alloc.region_with_subregion(lines.convert_region(surroundings), region), - alloc.concat([ - alloc.reflow("I am expecting the "), - alloc.keyword("provides"), - alloc.reflow(" keyword next, like"), - ]), - alloc - .parser_suggestion("provides [Animal, default, tame]") - .indent(4), - ]); - - Report { - filename, - doc, - title: "WEIRD PROVIDES".to_string(), - severity: Severity::RuntimeError, - } - } - - EProvides::Space(error, pos) => to_space_report(alloc, lines, filename, &error, pos), - - _ => todo!("unhandled parse error {:?}", parse_problem), - } -} - -fn to_exposes_report<'a>( - alloc: &'a RocDocAllocator<'a>, - lines: &LineInfo, - filename: PathBuf, - parse_problem: &roc_parse::parser::EExposes, - start: Position, -) -> Report<'a> { - use roc_parse::parser::EExposes; - - match *parse_problem { - EExposes::ListEnd(pos) | // TODO: give this its own error message - EExposes::Identifier(pos) => { - let surroundings = Region::new(start, pos); - let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); - - let doc = alloc.stack([ - alloc.reflow(r"I am partway through parsing an `exposes` list, but I got stuck here:"), - alloc.region_with_subregion(lines.convert_region(surroundings), region), - alloc.concat([alloc.reflow( - "I was expecting a type name, value name or function name next, like", - )]), - alloc - .parser_suggestion("exposes [Animal, default, tame]") - .indent(4), - ]); - - Report { - filename, - doc, - title: "WEIRD EXPOSES".to_string(), - severity: Severity::RuntimeError, - } - } - - EExposes::Exposes(pos) => { - let surroundings = Region::new(start, pos); - let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); - - let doc = alloc.stack([ - alloc.reflow(r"I am partway through parsing a header, but I got stuck here:"), - alloc.region_with_subregion(lines.convert_region(surroundings), region), - alloc.concat([ - alloc.reflow("I am expecting the "), - alloc.keyword("exposes"), - alloc.reflow(" keyword next, like"), - ]), - alloc - .parser_suggestion("exposes [Animal, default, tame]") - .indent(4), - ]); - - Report { - filename, - doc, - title: "WEIRD EXPOSES".to_string(), - severity: Severity::RuntimeError, - } - } - - EExposes::Space(error, pos) => to_space_report(alloc, lines, filename, &error, pos), - - _ => todo!("unhandled `exposes` parsing error {:?}", parse_problem), - } -} - -fn to_imports_report<'a>( - alloc: &'a RocDocAllocator<'a>, - lines: &LineInfo, - filename: PathBuf, - parse_problem: &roc_parse::parser::EImports, - start: Position, -) -> Report<'a> { - use roc_parse::parser::EImports; - - match *parse_problem { - EImports::Identifier(pos) => { - let surroundings = Region::new(start, pos); - let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); - - let doc = alloc.stack([ - alloc.reflow(r"I am partway through parsing a imports list, but I got stuck here:"), - alloc.region_with_subregion(lines.convert_region(surroundings), region), - alloc.concat([alloc.reflow( - "I was expecting a type name, value name or function name next, like ", - )]), - alloc - .parser_suggestion("imports [Animal, default, tame]") - .indent(4), - ]); - - Report { - filename, - doc, - title: "WEIRD IMPORTS".to_string(), - severity: Severity::RuntimeError, - } - } - - EImports::Imports(pos) | EImports::IndentImports(pos) => { - let surroundings = Region::new(start, pos); - let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); - - let doc = alloc.stack([ - alloc.reflow(r"I am partway through parsing a header, but I got stuck here:"), - alloc.region_with_subregion(lines.convert_region(surroundings), region), - alloc.concat([ - alloc.reflow("I am expecting the "), - alloc.keyword("imports"), - alloc.reflow(" keyword next, like"), - ]), - alloc - .parser_suggestion("imports [Animal, default, tame]") - .indent(4), - ]); - - Report { - filename, - doc, - title: "WEIRD IMPORTS".to_string(), - severity: Severity::RuntimeError, - } - } - - EImports::Space(error, pos) => to_space_report(alloc, lines, filename, &error, pos), - - EImports::ModuleName(pos) => { - let surroundings = Region::new(start, pos); - let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); - - let doc = alloc.stack([ - alloc.reflow(r"I am partway through parsing a header, but got stuck here:"), - alloc.region_with_subregion(lines.convert_region(surroundings), region), - alloc.concat([ - alloc.reflow("I am expecting a module name next, like "), - alloc.parser_suggestion("BigNum"), - alloc.reflow(" or "), - alloc.parser_suggestion("Main"), - alloc.reflow(". Module names must start with an uppercase letter."), - ]), - ]); - - Report { - filename, - doc, - title: "WEIRD MODULE NAME".to_string(), - severity: Severity::RuntimeError, - } - } - - EImports::ListEnd(pos) => { - let surroundings = Region::new(start, pos); - let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); - - let doc = alloc.stack([ - alloc.reflow(r"I am partway through parsing a imports list, but I got stuck here:"), - alloc.region_with_subregion(lines.convert_region(surroundings), region), - alloc.concat([alloc.reflow("I am expecting a comma or end of list, like")]), - alloc.parser_suggestion("imports [Shape, Vector]").indent(4), - ]); - - Report { - filename, - doc, - title: "WEIRD IMPORTS".to_string(), - severity: Severity::RuntimeError, - } - } - - _ => todo!("unhandled parse error {:?}", parse_problem), - } -} - -fn to_requires_report<'a>( - alloc: &'a RocDocAllocator<'a>, - lines: &LineInfo, - filename: PathBuf, - parse_problem: &roc_parse::parser::ERequires<'a>, - start: Position, -) -> Report<'a> { - use roc_parse::parser::ERequires; - - match *parse_problem { - ERequires::Requires(pos) => { - let surroundings = Region::new(start, pos); - let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); - - let doc = alloc.stack([ - alloc.reflow(r"I am partway through parsing a header, but I got stuck here:"), - alloc.region_with_subregion(lines.convert_region(surroundings), region), - alloc.concat([ - alloc.reflow("I am expecting the "), - alloc.keyword("requires"), - alloc.reflow(" keyword next, like"), - ]), - alloc - .parser_suggestion("requires { main : Task I64 Str }") - .indent(4), - ]); - - Report { - filename, - doc, - title: "MISSING REQUIRES".to_string(), - severity: Severity::RuntimeError, - } - } - - ERequires::Space(error, pos) => to_space_report(alloc, lines, filename, &error, pos), - - ERequires::ListStart(pos) => { - let surroundings = Region::new(start, pos); - let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); - - let doc = alloc.stack([ - alloc.reflow(r"I am partway through parsing a header, but I got stuck here:"), - alloc.region_with_subregion(lines.convert_region(surroundings), region), - alloc.concat([ - alloc.reflow("I am expecting the "), - alloc.keyword("requires"), - alloc.reflow(" keyword next, like"), - ]), - alloc - .parser_suggestion("requires { main : Task I64 Str }") - .indent(4), - ]); - - Report { - filename, - doc, - title: "MISSING REQUIRES".to_string(), - severity: Severity::RuntimeError, - } - } - - ERequires::Rigid(pos) => { - let surroundings = Region::new(start, pos); - let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); - - let doc = alloc.stack([ - alloc.reflow(r"I am partway through parsing a header, but I got stuck here:"), - alloc.region_with_subregion(lines.convert_region(surroundings), region), - alloc.concat([ - alloc.reflow("I am expecting a list of rigids like "), - alloc.keyword("{}"), - alloc.reflow(" or "), - alloc.keyword("{model=>Model}"), - alloc.reflow(" next. A full "), - alloc.keyword("requires"), - alloc.reflow(" definition looks like"), - ]), - alloc - .parser_suggestion("requires {model=>Model, msg=>Msg} {main : Effect {}}") - .indent(4), - ]); - - Report { - filename, - doc, - title: "BAD REQUIRES RIGIDS".to_string(), - severity: Severity::RuntimeError, - } - } - - ERequires::Open(pos) => { - let surroundings = Region::new(start, pos); - let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); - - let doc = alloc.stack([ - alloc.reflow(r"I am partway through parsing a header, but I got stuck here:"), - alloc.region_with_subregion(lines.convert_region(surroundings), region), - alloc.concat([ - alloc.reflow("I am expecting a list of type names like "), - alloc.keyword("{}"), - alloc.reflow(" or "), - alloc.keyword("{ Model }"), - alloc.reflow(" next. A full "), - alloc.keyword("requires"), - alloc.reflow(" definition looks like"), - ]), - alloc - .parser_suggestion("requires { Model, Msg } {main : Effect {}}") - .indent(4), - ]); - - Report { - filename, - doc, - title: "BAD REQUIRES".to_string(), - severity: Severity::RuntimeError, - } - } - - _ => todo!("unhandled parse error {:?}", parse_problem), - } -} - -fn to_packages_report<'a>( - alloc: &'a RocDocAllocator<'a>, - lines: &LineInfo, - filename: PathBuf, - parse_problem: &roc_parse::parser::EPackages, - start: Position, -) -> Report<'a> { - use roc_parse::parser::EPackages; - - match *parse_problem { - EPackages::Packages(pos) => { - let surroundings = Region::new(start, pos); - let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); - - let doc = alloc.stack([ - alloc.reflow(r"I am partway through parsing a header, but I got stuck here:"), - alloc.region_with_subregion(lines.convert_region(surroundings), region), - alloc.concat([ - alloc.reflow("I am expecting the "), - alloc.keyword("packages"), - alloc.reflow(" keyword next, like"), - ]), - alloc.parser_suggestion("packages {}").indent(4), - ]); - - Report { - filename, - doc, - title: "MISSING PACKAGES".to_string(), - severity: Severity::RuntimeError, - } - } - - EPackages::Space(error, pos) => to_space_report(alloc, lines, filename, &error, pos), - - _ => todo!("unhandled parse error {:?}", parse_problem), - } -} - -fn to_space_report<'a>( - alloc: &'a RocDocAllocator<'a>, - lines: &LineInfo, - filename: PathBuf, - parse_problem: &roc_parse::parser::BadInputError, - pos: Position, -) -> Report<'a> { - use roc_parse::parser::BadInputError; - - match parse_problem { - BadInputError::HasTab => { - let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); - - let doc = alloc.stack([ - alloc.reflow(r"I encountered a tab character"), - alloc.region(region), - alloc.concat([alloc.reflow("Tab characters are not allowed.")]), - ]); - - Report { - filename, - doc, - title: "TAB CHARACTER".to_string(), - severity: Severity::RuntimeError, - } - } - - _ => todo!("unhandled type parse error: {:?}", &parse_problem), - } -} - -fn to_ability_def_report<'a>( - alloc: &'a RocDocAllocator<'a>, - lines: &LineInfo, - filename: PathBuf, - problem: &roc_parse::parser::EAbility<'a>, - start: Position, -) -> Report<'a> { - use roc_parse::parser::EAbility; - - match problem { - EAbility::Space(error, pos) => to_space_report(alloc, lines, filename, error, *pos), - EAbility::Type(tipe, pos) => to_type_report(alloc, lines, filename, tipe, *pos), - EAbility::DemandAlignment(over_under_indent, pos) => { - let over_under_msg = if *over_under_indent > 0 { - alloc.reflow("indented too much") - } else { - alloc.reflow("not indented enough") - }; - - let msg = alloc.concat([ - alloc.reflow("I suspect this line is "), - over_under_msg, - alloc.reflow(" (by "), - alloc.string(over_under_indent.abs().to_string()), - alloc.reflow(" spaces)"), - ]); - - to_unfinished_ability_report(alloc, lines, filename, *pos, start, msg) - } - EAbility::DemandName(pos) => to_unfinished_ability_report( - alloc, - lines, - filename, - *pos, - start, - alloc.reflow("I was expecting to see a value signature next."), - ), - EAbility::DemandColon(pos) => to_unfinished_ability_report( - alloc, - lines, - filename, - *pos, - start, - alloc.concat([ - alloc.reflow("I was expecting to see a "), - alloc.parser_suggestion(":"), - alloc.reflow(" annotating the signature of this value next."), - ]), - ), - } -} - -fn to_unfinished_ability_report<'a>( - alloc: &'a RocDocAllocator<'a>, - lines: &LineInfo, - filename: PathBuf, - pos: Position, - start: Position, - message: RocDocBuilder<'a>, -) -> Report<'a> { - let surroundings = Region::new(start, pos); - let region = LineColumnRegion::from_pos(lines.convert_pos(pos)); - - let doc = alloc.stack([ - alloc.reflow(r"I was partway through parsing an ability definition, but I got stuck here:"), - alloc.region_with_subregion(lines.convert_region(surroundings), region), - message, - ]); - - Report { - filename, - doc, - title: "UNFINISHED ABILITY".to_string(), - severity: Severity::RuntimeError, - } -} - -#[derive(Debug)] -enum Next<'a> { - Keyword(&'a str), - // Operator(&'a str), - Close(&'a str, char), - Token(&'a str), - Other(Option), -} - -fn what_is_next<'a>(source_lines: &'a [&'a str], pos: LineColumn) -> Next<'a> { - let row_index = pos.line as usize; - let col_index = pos.column as usize; - match source_lines.get(row_index) { - None => Next::Other(None), - Some(line) => { - let chars = &line[col_index..]; - let mut it = chars.chars(); - - match roc_parse::keyword::KEYWORDS - .iter() - .find(|keyword| starts_with_keyword(chars, keyword)) - { - Some(keyword) => Next::Keyword(keyword), - None => match it.next() { - None => Next::Other(None), - Some(c) => match c { - ')' => Next::Close("parenthesis", ')'), - ']' => Next::Close("square bracket", ']'), - '}' => Next::Close("curly brace", '}'), - '-' if it.next() == Some('>') => Next::Token("->"), - '=' if it.next() == Some('>') => Next::Token("=>"), - // _ if is_symbol(c) => todo!("it's an operator"), - _ => Next::Other(Some(c)), - }, - }, - } - } - } -} - -pub fn starts_with_keyword(rest_of_line: &str, keyword: &str) -> bool { - if let Some(stripped) = rest_of_line.strip_prefix(keyword) { - match stripped.chars().next() { - None => true, - Some(c) => !c.is_alphanumeric(), - } - } else { - false - } -} - -fn next_line_starts_with_close_curly(source_lines: &[&str], pos: LineColumn) -> Option { - next_line_starts_with_char(source_lines, pos, '}') -} - -fn next_line_starts_with_close_parenthesis( - source_lines: &[&str], - pos: LineColumn, -) -> Option { - next_line_starts_with_char(source_lines, pos, ')') -} - -fn next_line_starts_with_close_square_bracket( - source_lines: &[&str], - pos: LineColumn, -) -> Option { - next_line_starts_with_char(source_lines, pos, ']') -} - -fn next_line_starts_with_char( - source_lines: &[&str], - pos: LineColumn, - character: char, -) -> Option { - match source_lines.get(pos.line as usize + 1) { - None => None, - - Some(line) => { - let spaces_dropped = line.trim_start_matches(' '); - match spaces_dropped.chars().next() { - Some(c) if c == character => Some(LineColumn { - line: pos.line + 1, - column: (line.len() - spaces_dropped.len()) as u32, - }), - _ => None, - } - } - } -} - -fn to_keyword_region(pos: LineColumn, keyword: &str) -> LineColumnRegion { - LineColumnRegion { - start: pos, - end: pos.bump_column(keyword.len() as u32), - } -} diff --git a/reporting/src/error/type.rs b/reporting/src/error/type.rs deleted file mode 100644 index ba907d2b27..0000000000 --- a/reporting/src/error/type.rs +++ /dev/null @@ -1,3943 +0,0 @@ -use crate::error::canonicalize::{to_circular_def_doc, CIRCULAR_DEF}; -use crate::report::{Annotation, Report, RocDocAllocator, RocDocBuilder, Severity}; -use roc_can::expected::{Expected, PExpected}; -use roc_collections::all::{HumanIndex, MutSet, SendMap}; -use roc_exhaustive::CtorName; -use roc_module::called_via::{BinOp, CalledVia}; -use roc_module::ident::{Ident, IdentStr, Lowercase, TagName}; -use roc_module::symbol::Symbol; -use roc_region::all::{LineInfo, Loc, Region}; -use roc_solve::ability::{UnderivableReason, Unfulfilled}; -use roc_solve::solve; -use roc_std::RocDec; -use roc_types::pretty_print::{Parens, WILDCARD}; -use roc_types::types::{ - AliasKind, Category, ErrorType, PatternCategory, Reason, RecordField, TypeExt, -}; -use std::path::PathBuf; -use ven_pretty::DocAllocator; - -const DUPLICATE_NAME: &str = "DUPLICATE NAME"; -const ADD_ANNOTATIONS: &str = r#"Can more type annotations be added? Type annotations always help me give more specific messages, and I think they could help a lot in this case"#; - -const OPAQUE_NUM_SYMBOLS: &[Symbol] = &[ - Symbol::NUM_NUM, - Symbol::NUM_INTEGER, - Symbol::NUM_FLOATINGPOINT, -]; - -pub fn type_problem<'b>( - alloc: &'b RocDocAllocator<'b>, - lines: &LineInfo, - filename: PathBuf, - problem: solve::TypeError, -) -> Option> { - use solve::TypeError::*; - - fn report(title: String, doc: RocDocBuilder<'_>, filename: PathBuf) -> Option> { - Some(Report { - title, - filename, - doc, - severity: Severity::RuntimeError, - }) - } - - match problem { - BadExpr(region, category, found, expected) => Some(to_expr_report( - alloc, lines, filename, region, category, found, expected, - )), - BadPattern(region, category, found, expected) => Some(to_pattern_report( - alloc, lines, filename, region, category, found, expected, - )), - CircularType(region, symbol, overall_type) => Some(to_circular_report( - alloc, - lines, - filename, - region, - symbol, - overall_type, - )), - UnexposedLookup(symbol) => { - let title = "UNRECOGNIZED NAME".to_string(); - let doc = alloc - .stack(vec![alloc - .reflow("The ") - .append(alloc.module(symbol.module_id())) - .append(alloc.reflow(" module does not expose anything by the name ")) - .append(alloc.symbol_unqualified(symbol))]) - .append(alloc.reflow(".")); - - report(title, doc, filename) - } - BadType(type_problem) => { - use roc_types::types::Problem::*; - match type_problem { - BadTypeArguments { - symbol, - region, - type_got, - alias_needs, - } => { - let needed_arguments = if alias_needs == 1 { - alloc.reflow("1 type argument") - } else { - alloc - .text(alias_needs.to_string()) - .append(alloc.reflow(" type arguments")) - }; - - let found_arguments = alloc.text(type_got.to_string()); - - let doc = alloc.stack([ - alloc.concat([ - alloc.reflow("The "), - alloc.symbol_unqualified(symbol), - alloc.reflow(" alias expects "), - needed_arguments, - alloc.reflow(", but it got "), - found_arguments, - alloc.reflow(" instead:"), - ]), - alloc.region(lines.convert_region(region)), - alloc.reflow("Are there missing parentheses?"), - ]); - - let title = if type_got > alias_needs { - "TOO MANY TYPE ARGUMENTS".to_string() - } else { - "TOO FEW TYPE ARGUMENTS".to_string() - }; - - report(title, doc, filename) - } - Shadowed(original_region, shadow) => { - let doc = report_shadowing(alloc, lines, original_region, shadow); - let title = DUPLICATE_NAME.to_string(); - - report(title, doc, filename) - } - - SolvedTypeError => None, // Don't re-report cascading errors - see https://github.com/rtfeldman/roc/pull/1711 - - // We'll also report these as a canonicalization problem, no need to re-report them. - CyclicAlias(..) => None, - UnrecognizedIdent(..) => None, - - other => panic!("unhandled bad type: {:?}", other), - } - } - UnfulfilledAbility(incomplete) => { - let title = "INCOMPLETE ABILITY IMPLEMENTATION".to_string(); - - let doc = report_unfulfilled_ability(alloc, lines, incomplete); - - report(title, doc, filename) - } - BadExprMissingAbility(region, _category, _found, incomplete) => { - let incomplete = incomplete - .into_iter() - .map(|unfulfilled| report_unfulfilled_ability(alloc, lines, unfulfilled)); - let note = alloc.stack(incomplete); - let snippet = alloc.region(lines.convert_region(region)); - let stack = [ - alloc.text( - "This expression has a type that does not implement the abilities it's expected to:", - ), - snippet, - note - ]; - - let report = Report { - title: "TYPE MISMATCH".to_string(), - filename, - doc: alloc.stack(stack), - severity: Severity::RuntimeError, - }; - Some(report) - } - BadPatternMissingAbility(region, _category, _found, incomplete) => { - let incomplete = incomplete - .into_iter() - .map(|unfulfilled| report_unfulfilled_ability(alloc, lines, unfulfilled)); - let note = alloc.stack(incomplete); - let snippet = alloc.region(lines.convert_region(region)); - let stack = [ - alloc.text( - "This expression has a type does not implement the abilities it's expected to:", - ), - snippet, - note, - ]; - - let report = Report { - title: "TYPE MISMATCH".to_string(), - filename, - doc: alloc.stack(stack), - severity: Severity::RuntimeError, - }; - Some(report) - } - Exhaustive(problem) => Some(exhaustive_problem(alloc, lines, filename, problem)), - CircularDef(entries) => { - let doc = to_circular_def_doc(alloc, lines, &entries); - let title = CIRCULAR_DEF.to_string(); - let severity = Severity::RuntimeError; - - Some(Report { - title, - filename, - doc, - severity, - }) - } - StructuralSpecialization { - region, - typ, - ability, - member, - } => { - let stack = [ - alloc.concat([ - alloc.reflow("This specialization of "), - alloc.symbol_unqualified(member), - alloc.reflow(" is for a non-opaque type:"), - ]), - alloc.region(lines.convert_region(region)), - alloc.reflow("It is specialized for"), - alloc.type_block(error_type_to_doc(alloc, typ)), - alloc.reflow("but structural types can never specialize abilities!"), - alloc.note("").append(alloc.concat([ - alloc.symbol_unqualified(member), - alloc.reflow(" is a member of "), - alloc.symbol_qualified(ability), - ])), - ]; - - Some(Report { - title: "ILLEGAL SPECIALIZATION".to_string(), - filename, - doc: alloc.stack(stack), - severity: Severity::RuntimeError, - }) - } - DominatedDerive { - opaque, - ability, - derive_region, - impl_region, - } => { - let stack = [ - alloc.concat([ - alloc.symbol_unqualified(opaque), - alloc.reflow(" both derives and custom-implements "), - alloc.symbol_qualified(ability), - alloc.reflow(". We found the derive here:"), - ]), - alloc.region(lines.convert_region(derive_region)), - alloc.concat([ - alloc.reflow("and one custom implementation of "), - alloc.symbol_qualified(ability), - alloc.reflow(" here:"), - ]), - alloc.region(lines.convert_region(impl_region)), - alloc.concat([ - alloc.reflow("Derived and custom implementations can conflict, so one of them needs to be removed!"), - ]), - alloc.note("").append(alloc.reflow("We'll try to compile your program using the custom implementation first, and fall-back on the derived implementation if needed. Make sure to disambiguate which one you want!")), - ]; - - Some(Report { - title: "CONFLICTING DERIVE AND IMPLEMENTATION".to_string(), - filename, - doc: alloc.stack(stack), - severity: Severity::Warning, - }) - } - } -} - -fn report_unfulfilled_ability<'a>( - alloc: &'a RocDocAllocator<'a>, - lines: &LineInfo, - unfulfilled: Unfulfilled, -) -> RocDocBuilder<'a> { - match unfulfilled { - Unfulfilled::Incomplete { - typ, - ability, - missing_members, - } => { - debug_assert!(!missing_members.is_empty()); - - let mut stack = vec![alloc.concat([ - alloc.reflow("The type "), - alloc.symbol_unqualified(typ), - alloc.reflow(" does not fully implement the ability "), - alloc.symbol_unqualified(ability), - alloc.reflow(". The following specializations are missing:"), - ])]; - - for member in missing_members.into_iter() { - stack.push(alloc.concat([ - alloc.reflow("A specialization for "), - alloc.symbol_unqualified(member.value), - alloc.reflow(", which is defined here:"), - ])); - stack.push(alloc.region(lines.convert_region(member.region))); - } - - alloc.stack(stack) - } - Unfulfilled::AdhocUnderivable { - typ, - ability, - reason, - } => { - let reason = report_underivable_reason(alloc, reason, ability, &typ); - let stack = [ - alloc.concat([ - alloc.reflow("Roc can't generate an implementation of the "), - alloc.symbol_qualified(ability), - alloc.reflow(" ability for"), - ]), - alloc.type_block(error_type_to_doc(alloc, typ)), - ] - .into_iter() - .chain(reason); - - alloc.stack(stack) - } - Unfulfilled::OpaqueUnderivable { - typ, - ability, - opaque, - derive_region, - reason, - } => { - let reason = report_underivable_reason(alloc, reason, ability, &typ); - let stack = [ - alloc.concat([ - alloc.reflow("Roc can't derive an implementation of the "), - alloc.symbol_qualified(ability), - alloc.reflow(" for "), - alloc.symbol_unqualified(opaque), - alloc.reflow(":"), - ]), - alloc.region(lines.convert_region(derive_region)), - ] - .into_iter() - .chain(reason) - .chain(std::iter::once(alloc.tip().append(alloc.concat([ - alloc.reflow("You can define a custom implementation of "), - alloc.symbol_qualified(ability), - alloc.reflow(" for "), - alloc.symbol_unqualified(opaque), - alloc.reflow("."), - ])))); - - alloc.stack(stack) - } - } -} - -fn report_underivable_reason<'a>( - alloc: &'a RocDocAllocator<'a>, - reason: UnderivableReason, - ability: Symbol, - typ: &ErrorType, -) -> Option> { - match reason { - UnderivableReason::NotABuiltin => { - Some(alloc.reflow("Only builtin abilities can have generated implementations!")) - } - UnderivableReason::SurfaceNotDerivable => underivable_hint(alloc, ability, typ), - UnderivableReason::NestedNotDerivable(nested_typ) => { - let hint = underivable_hint(alloc, ability, &nested_typ); - let reason = alloc.stack( - [ - alloc.reflow("In particular, an implementation for"), - alloc.type_block(error_type_to_doc(alloc, nested_typ)), - alloc.reflow("cannot be generated."), - ] - .into_iter() - .chain(hint), - ); - Some(reason) - } - } -} - -fn underivable_hint<'b>( - alloc: &'b RocDocAllocator<'b>, - ability: Symbol, - typ: &ErrorType, -) -> Option> { - match typ { - ErrorType::Function(..) => Some(alloc.note("").append(alloc.concat([ - alloc.symbol_unqualified(ability), - alloc.reflow(" cannot be generated for functions."), - ]))), - ErrorType::FlexVar(v) | ErrorType::RigidVar(v) => Some(alloc.tip().append(alloc.concat([ - alloc.reflow("This type variable is not bound to "), - alloc.symbol_unqualified(ability), - alloc.reflow(". Consider adding a "), - alloc.keyword("has"), - alloc.reflow(" clause to bind the type variable, like "), - alloc.inline_type_block(alloc.concat([ - alloc.string("| ".to_string()), - alloc.type_variable(v.clone()), - alloc.space(), - alloc.keyword("has"), - alloc.space(), - alloc.symbol_qualified(ability), - ])), - ]))), - ErrorType::Alias(symbol, _, _, AliasKind::Opaque) => { - Some(alloc.tip().append(alloc.concat([ - alloc.symbol_unqualified(*symbol), - alloc.reflow(" does not implement "), - alloc.symbol_unqualified(ability), - alloc.reflow("."), - if symbol.module_id() == alloc.home { - alloc.concat([ - alloc.reflow(" Consider adding a custom implementation"), - if ability.is_builtin() { - alloc.concat([ - alloc.reflow(" or "), - alloc.inline_type_block(alloc.concat([ - alloc.keyword("has"), - alloc.space(), - alloc.symbol_qualified(ability), - ])), - alloc.reflow(" to the definition of "), - alloc.symbol_unqualified(*symbol), - ]) - } else { - alloc.nil() - }, - alloc.reflow("."), - ]) - } else { - alloc.nil() - }, - ]))) - } - _ => None, - } -} - -fn report_shadowing<'b>( - alloc: &'b RocDocAllocator<'b>, - lines: &LineInfo, - original_region: Region, - shadow: Loc, -) -> RocDocBuilder<'b> { - let line = r#"Since these types have the same name, it's easy to use the wrong one on accident. Give one of them a new name."#; - - alloc.stack([ - alloc - .text("The ") - .append(alloc.ident(shadow.value)) - .append(alloc.reflow(" name is first defined here:")), - alloc.region(lines.convert_region(original_region)), - alloc.reflow("But then it's defined a second time here:"), - alloc.region(lines.convert_region(shadow.region)), - alloc.reflow(line), - ]) -} - -pub fn cyclic_alias<'b>( - alloc: &'b RocDocAllocator<'b>, - lines: &LineInfo, - symbol: Symbol, - region: roc_region::all::Region, - others: Vec, -) -> (RocDocBuilder<'b>, String) { - let when_is_recursion_legal = - alloc.reflow("Recursion in aliases is only allowed if recursion happens behind a tagged union, at least one variant of which is not recursive."); - - let doc = if others.is_empty() { - alloc.stack([ - alloc - .reflow("The ") - .append(alloc.symbol_unqualified(symbol)) - .append(alloc.reflow(" alias is self-recursive in an invalid way:")), - alloc.region(lines.convert_region(region)), - when_is_recursion_legal, - ]) - } else { - alloc.stack([ - alloc - .reflow("The ") - .append(alloc.symbol_unqualified(symbol)) - .append(alloc.reflow(" alias is recursive in an invalid way:")), - alloc.region(lines.convert_region(region)), - alloc - .reflow("The ") - .append(alloc.symbol_unqualified(symbol)) - .append(alloc.reflow( - " alias depends on itself through the following chain of definitions:", - )), - crate::report::cycle( - alloc, - 4, - alloc.symbol_unqualified(symbol), - others - .into_iter() - .map(|other| alloc.symbol_unqualified(other)) - .collect::>(), - ), - when_is_recursion_legal, - ]) - }; - - (doc, "CYCLIC ALIAS".to_string()) -} - -#[allow(clippy::too_many_arguments)] -fn report_mismatch<'b>( - alloc: &'b RocDocAllocator<'b>, - lines: &LineInfo, - filename: PathBuf, - category: &Category, - found: ErrorType, - expected_type: ErrorType, - region: roc_region::all::Region, - opt_highlight: Option, - problem: RocDocBuilder<'b>, - this_is: RocDocBuilder<'b>, - instead_of: RocDocBuilder<'b>, - further_details: Option>, -) -> Report<'b> { - let snippet = if let Some(highlight) = opt_highlight { - alloc.region_with_subregion( - lines.convert_region(highlight), - lines.convert_region(region), - ) - } else { - alloc.region(lines.convert_region(region)) - }; - let lines = vec![ - problem, - snippet, - type_comparison( - alloc, - found, - expected_type, - ExpectationContext::Arbitrary, - add_category(alloc, this_is, category), - instead_of, - further_details, - ), - ]; - - Report { - title: "TYPE MISMATCH".to_string(), - filename, - doc: alloc.stack(lines), - severity: Severity::RuntimeError, - } -} - -#[allow(clippy::too_many_arguments)] -fn report_bad_type<'b>( - alloc: &'b RocDocAllocator<'b>, - lines: &LineInfo, - filename: PathBuf, - category: &Category, - found: ErrorType, - expected_type: ErrorType, - region: roc_region::all::Region, - opt_highlight: Option, - problem: RocDocBuilder<'b>, - this_is: RocDocBuilder<'b>, - further_details: RocDocBuilder<'b>, -) -> Report<'b> { - let snippet = if let Some(highlight) = opt_highlight { - alloc.region_with_subregion( - lines.convert_region(highlight), - lines.convert_region(region), - ) - } else { - alloc.region(lines.convert_region(region)) - }; - let lines = vec![ - problem, - snippet, - lone_type( - alloc, - found, - expected_type, - ExpectationContext::Arbitrary, - add_category(alloc, this_is, category), - further_details, - ), - ]; - - Report { - title: "TYPE MISMATCH".to_string(), - filename, - doc: alloc.stack(lines), - severity: Severity::RuntimeError, - } -} - -fn pattern_to_doc<'b>( - alloc: &'b RocDocAllocator<'b>, - pattern: &roc_can::pattern::Pattern, -) -> Option> { - use roc_can::pattern::Pattern::*; - - match pattern { - Identifier(symbol) => Some(alloc.symbol_unqualified(*symbol)), - _ => None, - } -} - -fn lowercase_first(s: &str) -> String { - let mut chars = s.chars(); - match chars.next() { - None => Default::default(), - Some(c) => c.to_lowercase().chain(chars).collect(), - } -} - -fn to_expr_report<'b>( - alloc: &'b RocDocAllocator<'b>, - lines: &LineInfo, - filename: PathBuf, - expr_region: roc_region::all::Region, - category: Category, - found: ErrorType, - expected: Expected, -) -> Report<'b> { - match expected { - Expected::NoExpectation(expected_type) => { - // If it looks like a record field typo, early return with a special report for that. - if let ErrorType::Record(expected_fields, _) = - expected_type.clone().unwrap_structural_alias() - { - if let ErrorType::Record(found_fields, found_ext) = - found.clone().unwrap_structural_alias() - { - let expected_set: MutSet<_> = expected_fields.keys().cloned().collect(); - let found_set: MutSet<_> = found_fields.keys().cloned().collect(); - let mut diff = expected_set.difference(&found_set); - - if let Some(field) = diff.next() { - let opt_sym = match category { - Category::Lookup(name) => Some(name), - _ => None, - }; - return report_record_field_typo( - alloc, - lines, - filename, - opt_sym, - ".", - field, - "", - expr_region, - found_fields, - found_ext, - ); - } - } - }; - - let comparison = type_comparison( - alloc, - found, - expected_type, - ExpectationContext::Arbitrary, - add_category(alloc, alloc.text("It is"), &category), - alloc.text("But you are trying to use it as:"), - None, - ); - - Report { - filename, - title: "TYPE MISMATCH".to_string(), - doc: alloc.stack([ - alloc.text("This expression is used in an unexpected way:"), - alloc.region(lines.convert_region(expr_region)), - comparison, - ]), - severity: Severity::RuntimeError, - } - } - Expected::FromAnnotation(name, _arity, annotation_source, expected_type) => { - use roc_types::types::AnnotationSource::*; - - let (the_name_text, on_name_text) = match pattern_to_doc(alloc, &name.value) { - Some(doc) => ( - alloc.concat([alloc.reflow("the "), doc.clone()]), - alloc.concat([alloc.reflow(" on "), doc]), - ), - None => (alloc.text("this"), alloc.nil()), - }; - - let ann_region = annotation_source.region(); - - let thing = match annotation_source { - TypedIfBranch { - index, - num_branches, - .. - } if num_branches == 2 => alloc.concat([ - alloc.keyword(if index == HumanIndex::FIRST { - "then" - } else { - "else" - }), - alloc.reflow(" branch of this "), - alloc.keyword("if"), - alloc.text(" expression:"), - ]), - TypedIfBranch { index, .. } => alloc.concat([ - alloc.string(index.ordinal()), - alloc.reflow(" branch of this "), - alloc.keyword("if"), - alloc.text(" expression:"), - ]), - TypedWhenBranch { index, .. } => alloc.concat([ - alloc.string(index.ordinal()), - alloc.reflow(" branch of this "), - alloc.keyword("when"), - alloc.text(" expression:"), - ]), - TypedBody { .. } => alloc.concat([ - alloc.text("body of "), - the_name_text, - alloc.text(" definition:"), - ]), - RequiredSymbol { .. } => alloc.concat([ - alloc.text("type annotation of "), - the_name_text, - alloc.text(" required symbol:"), - ]), - }; - - let it_is = match annotation_source { - TypedIfBranch { index, .. } => format!("The {} branch is", index.ordinal()), - TypedWhenBranch { index, .. } => format!("The {} branch is", index.ordinal()), - TypedBody { .. } => "The body is".into(), - RequiredSymbol { .. } => "The provided type is".into(), - }; - - let expectation_context = ExpectationContext::Annotation { - on: on_name_text.clone(), - }; - - let comparison = if diff_is_wildcard_comparison( - alloc, - found.clone(), - expected_type.clone(), - ) { - let it_is = lowercase_first(&it_is); - let (it, _) = format_category(alloc, alloc.text(it_is), &category, false); - lone_type( - alloc, - found, - expected_type, - expectation_context, - alloc.concat([ - alloc.reflow("The type annotation"), - on_name_text, - alloc.reflow(" says "), - it.clone(), - alloc.reflow(" should have the type:"), - ]), - alloc.concat([ - alloc.reflow("However, the type of "), - it, - alloc.reflow(" is connected to another type in a way that isn't reflected in this annotation.") - ]), - ) - } else { - type_comparison( - alloc, - found, - expected_type, - expectation_context, - add_category(alloc, alloc.text(it_is), &category), - alloc.concat([ - alloc.text("But the type annotation"), - on_name_text, - alloc.text(" says it should be:"), - ]), - None, - ) - }; - - Report { - title: "TYPE MISMATCH".to_string(), - filename, - doc: alloc.stack([ - alloc.text("Something is off with the ").append(thing), - { - // for typed bodies, include the line(s) with the signature - let joined = - roc_region::all::Region::span_across(&ann_region, &expr_region); - alloc.region_with_subregion( - lines.convert_region(joined), - lines.convert_region(expr_region), - ) - }, - comparison, - ]), - severity: Severity::RuntimeError, - } - } - Expected::ForReason(reason, expected_type, region) => match reason { - Reason::ExpectCondition => { - let problem = alloc.concat([ - alloc.text("This "), - alloc.keyword("expect"), - alloc.text(" condition needs to be a "), - alloc.type_str("Bool"), - alloc.text(":"), - ]); - - report_bad_type( - alloc, - lines, - filename, - &category, - found, - expected_type, - region, - Some(expr_region), - problem, - alloc.text("Right now it’s"), - alloc.concat([ - alloc.reflow("But I need every "), - alloc.keyword("expect"), - alloc.reflow(" condition to evaluate to a "), - alloc.type_str("Bool"), - alloc.reflow("—either "), - alloc.tag("True".into()), - alloc.reflow(" or "), - alloc.tag("False".into()), - alloc.reflow("."), - ]), - // Note: Elm has a hint here about truthiness. I think that - // makes sense for Elm, since most Elm users will come from - // JS, where truthiness is a thing. I don't really know - // what the background of Roc programmers will be, and I'd - // rather not create a distraction by introducing a term - // they don't know. ("Wait, what's truthiness?") - ) - } - Reason::IfCondition => { - let problem = alloc.concat([ - alloc.text("This "), - alloc.keyword("if"), - alloc.text(" condition needs to be a "), - alloc.type_str("Bool"), - alloc.text(":"), - ]); - - report_bad_type( - alloc, - lines, - filename, - &category, - found, - expected_type, - region, - Some(expr_region), - problem, - alloc.text("Right now it’s"), - alloc.concat([ - alloc.reflow("But I need every "), - alloc.keyword("if"), - alloc.reflow(" condition to evaluate to a "), - alloc.type_str("Bool"), - alloc.reflow("—either "), - alloc.tag("True".into()), - alloc.reflow(" or "), - alloc.tag("False".into()), - alloc.reflow("."), - ]), - // Note: Elm has a hint here about truthiness. I think that - // makes sense for Elm, since most Elm users will come from - // JS, where truthiness is a thing. I don't really know - // what the background of Roc programmers will be, and I'd - // rather not create a distraction by introducing a term - // they don't know. ("Wait, what's truthiness?") - ) - } - Reason::WhenGuard => { - let problem = alloc.concat([ - alloc.text("This "), - alloc.keyword("if"), - alloc.text(" guard condition needs to be a "), - alloc.type_str("Bool"), - alloc.text(":"), - ]); - report_bad_type( - alloc, - lines, - filename, - &category, - found, - expected_type, - region, - Some(expr_region), - problem, - alloc.text("Right now it’s"), - alloc.concat([ - alloc.reflow("But I need every "), - alloc.keyword("if"), - alloc.reflow(" guard condition to evaluate to a "), - alloc.type_str("Bool"), - alloc.reflow("—either "), - alloc.tag("True".into()), - alloc.reflow(" or "), - alloc.tag("False".into()), - alloc.reflow("."), - ]), - ) - } - Reason::IfBranch { - index, - total_branches, - } => match total_branches { - 2 => report_mismatch( - alloc, - lines, - filename, - &category, - found, - expected_type, - region, - Some(expr_region), - alloc.concat([ - alloc.text("This "), - alloc.keyword("if"), - alloc.text(" has an "), - alloc.keyword("else"), - alloc.text(" branch with a different type from its "), - alloc.keyword("then"), - alloc.text(" branch:"), - ]), - alloc.concat([ - alloc.text("The "), - alloc.keyword("else"), - alloc.text(" branch is"), - ]), - alloc.concat([ - alloc.text("but the "), - alloc.keyword("then"), - alloc.text(" branch has the type:"), - ]), - Some(alloc.concat([ - alloc.text("All branches in an "), - alloc.keyword("if"), - alloc.text(" must have the same type!"), - ])), - ), - _ => report_mismatch( - alloc, - lines, - filename, - &category, - found, - expected_type, - region, - Some(expr_region), - alloc.concat([ - alloc.reflow("The "), - alloc.string(index.ordinal()), - alloc.reflow(" branch of this "), - alloc.keyword("if"), - alloc.reflow(" does not match all the previous branches:"), - ]), - alloc.string(format!("The {} branch is", index.ordinal())), - alloc.reflow("But all the previous branches have type:"), - Some(alloc.concat([ - alloc.reflow("All branches in an "), - alloc.keyword("if"), - alloc.reflow(" must have the same type!"), - ])), - ), - }, - Reason::WhenBranch { index } => report_mismatch( - alloc, - lines, - filename, - &category, - found, - expected_type, - region, - Some(expr_region), - alloc.concat([ - alloc.reflow("The "), - alloc.string(index.ordinal()), - alloc.reflow(" branch of this "), - alloc.keyword("when"), - alloc.reflow(" does not match all the previous branches:"), - ]), - alloc.concat([ - alloc.reflow("The "), - alloc.string(index.ordinal()), - alloc.reflow(" branch is"), - ]), - alloc.reflow("But all the previous branches have type:"), - Some(alloc.concat([ - alloc.reflow("All branches of a "), - alloc.keyword("when"), - alloc.reflow(" must have the same type!"), - ])), - ), - Reason::ElemInList { index } => { - let ith = index.ordinal(); - - // Don't say "the previous elements all have the type" if - // there was only 1 previous element! - let prev_elems_msg = if index.to_zero_based() == 1 { - "However, the 1st element has the type:" - } else { - "However, the preceding elements in the list all have the type:" - }; - - report_mismatch( - alloc, - lines, - filename, - &category, - found, - expected_type, - region, - Some(expr_region), - alloc.reflow("This list contains elements with different types:"), - alloc.string(format!("Its {} element is", ith)), - alloc.reflow(prev_elems_msg), - Some(alloc.reflow("Every element in a list must have the same type!")), - ) - } - Reason::RecordUpdateValue(field) => report_mismatch( - alloc, - lines, - filename, - &category, - found, - expected_type, - region, - Some(expr_region), - alloc.concat([ - alloc.text("I cannot update the "), - alloc.record_field(field.to_owned()), - alloc.text(" field like this:"), - ]), - alloc.concat([ - alloc.text("You are trying to update "), - alloc.record_field(field), - alloc.text(" to be"), - ]), - alloc.text("But it should be:"), - Some(alloc.reflow( - "Record update syntax does not allow you \ - to change the type of fields. \ - You can achieve that with record literal syntax.", - )), - ), - Reason::RecordUpdateKeys(symbol, expected_fields) => { - match found.clone().unwrap_structural_alias() { - ErrorType::Record(actual_fields, ext) => { - let expected_set: MutSet<_> = expected_fields.keys().cloned().collect(); - let actual_set: MutSet<_> = actual_fields.keys().cloned().collect(); - - let mut diff = expected_set.difference(&actual_set); - - match diff.next().and_then(|k| Some((k, expected_fields.get(k)?))) { - None => report_mismatch( - alloc, - lines, - filename, - &category, - found, - expected_type, - region, - Some(expr_region), - alloc.reflow("Something is off with this record update:"), - alloc.concat([ - alloc.reflow("The"), - alloc.symbol_unqualified(symbol), - alloc.reflow(" record is"), - ]), - alloc.reflow("But this update needs it to be compatible with:"), - None, - ), - Some((field, field_region)) => report_record_field_typo( - alloc, - lines, - filename, - Some(symbol), - "", - field, - ":", - *field_region, - actual_fields, - ext, - ), - } - } - _ => report_bad_type( - alloc, - lines, - filename, - &category, - found, - expected_type, - region, - Some(expr_region), - alloc.reflow("This is not a record, so it has no fields to update!"), - alloc.reflow("It is"), - alloc.reflow("But I need a record!"), - ), - } - } - Reason::FnCall { name, arity } => match count_arguments(&found) { - 0 => { - let this_value = match name { - None => alloc.text("This value"), - Some(symbol) => alloc.concat([ - alloc.text("The "), - alloc.symbol_unqualified(symbol), - alloc.text(" value"), - ]), - }; - - let lines = vec![ - alloc.concat([ - this_value, - alloc.string(format!( - " is not a function, but it was given {}:", - if arity == 1 { - "1 argument".into() - } else { - format!("{} arguments", arity) - } - )), - ]), - alloc.region(lines.convert_region(expr_region)), - alloc.reflow("Are there any missing commas? Or missing parentheses?"), - ]; - - Report { - filename, - title: "TOO MANY ARGS".to_string(), - doc: alloc.stack(lines), - severity: Severity::RuntimeError, - } - } - n => { - let this_function = match name { - None => alloc.text("This function"), - Some(symbol) => alloc.concat([ - alloc.text("The "), - alloc.symbol_unqualified(symbol), - alloc.text(" function"), - ]), - }; - - if n < arity as usize { - let lines = vec![ - alloc.concat([ - this_function, - alloc.string(format!( - " expects {}, but it got {} instead:", - if n == 1 { - "1 argument".into() - } else { - format!("{} arguments", n) - }, - arity - )), - ]), - alloc.region(lines.convert_region(expr_region)), - alloc.reflow("Are there any missing commas? Or missing parentheses?"), - ]; - - Report { - filename, - title: "TOO MANY ARGS".to_string(), - doc: alloc.stack(lines), - severity: Severity::RuntimeError, - } - } else { - let lines = vec![ - alloc.concat([ - this_function, - alloc.string(format!( - " expects {}, but it got only {}:", - if n == 1 { - "1 argument".into() - } else { - format!("{} arguments", n) - }, - arity - )), - ]), - alloc.region(lines.convert_region(expr_region)), - alloc.reflow( - "Roc does not allow functions to be partially applied. \ - Use a closure to make partial application explicit.", - ), - ]; - - Report { - filename, - title: "TOO FEW ARGS".to_string(), - doc: alloc.stack(lines), - severity: Severity::RuntimeError, - } - } - } - }, - Reason::FnArg { name, arg_index } => { - let ith = arg_index.ordinal(); - - let this_function = match name { - None => alloc.text("this function"), - Some(symbol) => alloc.symbol_unqualified(symbol), - }; - - report_mismatch( - alloc, - lines, - filename, - &category, - found, - expected_type, - region, - Some(expr_region), - alloc.concat([ - alloc.string(format!("The {ith} argument to ")), - this_function.clone(), - alloc.text(" is not what I expect:"), - ]), - alloc.text("This argument is"), - alloc.concat([ - alloc.text("But "), - this_function, - alloc.string(format!(" needs the {ith} argument to be:")), - ]), - None, - ) - } - - Reason::NumericLiteralSuffix => report_mismatch( - alloc, - lines, - filename, - &category, - found, - expected_type, - region, - Some(expr_region), - alloc.text("This numeric literal is being used improperly:"), - alloc.text("Here the value is used as a:"), - alloc.text("But its suffix says it's a:"), - None, - ), - - Reason::InvalidAbilityMemberSpecialization { - member_name, - def_region: _, - unimplemented_abilities, - } => { - let problem = alloc.concat([ - alloc.reflow("Something is off with this specialization of "), - alloc.symbol_unqualified(member_name), - alloc.reflow(":"), - ]); - let this_is = alloc.reflow("This value is"); - let instead_of = alloc.concat([ - alloc.reflow("But the type annotation on "), - alloc.symbol_unqualified(member_name), - alloc.reflow(" says it must match:"), - ]); - - let hint = if unimplemented_abilities.is_empty() { - None - } else { - let mut stack = Vec::with_capacity(unimplemented_abilities.len()); - for (err_type, ability) in unimplemented_abilities.into_iter() { - stack.push(does_not_implement(alloc, err_type, ability)); - } - - let hint = alloc.stack([ - alloc.concat([ - alloc.note(""), - alloc.reflow("Some types in this specialization don't implement the abilities they are expected to. I found the following missing implementations:"), - ]), - alloc.type_block(alloc.stack(stack)), - ]); - - Some(hint) - }; - - report_mismatch( - alloc, - lines, - filename, - &category, - found, - expected_type, - region, - Some(expr_region), - problem, - this_is, - instead_of, - hint, - ) - } - - Reason::GeneralizedAbilityMemberSpecialization { - member_name, - def_region: _, - } => { - let problem = alloc.concat([ - alloc.reflow("This specialization of "), - alloc.symbol_unqualified(member_name), - alloc.reflow(" is overly general:"), - ]); - let this_is = alloc.reflow("This value is"); - let instead_of = alloc.concat([ - alloc.reflow("But the type annotation on "), - alloc.symbol_unqualified(member_name), - alloc.reflow(" says it must match:"), - ]); - - let note = alloc.stack([ - alloc.concat([ - alloc.note(""), - alloc.reflow("The specialized type is too general, and does not provide a concrete type where a type variable is bound to an ability."), - ]), - alloc.reflow("Specializations can only be made for concrete types. If you have a generic implementation for this value, perhaps you don't need an ability?"), - ]); - - report_mismatch( - alloc, - lines, - filename, - &category, - found, - expected_type, - region, - Some(expr_region), - problem, - this_is, - instead_of, - Some(note), - ) - } - - Reason::WhenBranches => { - let snippet = alloc.region_with_subregion( - lines.convert_region(region), - lines.convert_region(expr_region), - ); - - let this_is = alloc.concat([ - alloc.reflow("The "), - alloc.keyword("when"), - alloc.reflow(" condition is"), - ]); - - let wanted = alloc.reflow("But the branch patterns have type:"); - let details = Some(alloc.concat([ - alloc.reflow("The branches must be cases of the "), - alloc.keyword("when"), - alloc.reflow(" condition's type!"), - ])); - - let lines = [ - alloc.concat([ - alloc.reflow("The branches of this "), - alloc.keyword("when"), - alloc.reflow(" expression don't match the condition:"), - ]), - snippet, - type_comparison( - alloc, - found, - expected_type, - ExpectationContext::WhenCondition, - add_category(alloc, this_is, &category), - wanted, - details, - ), - ]; - - Report { - title: "TYPE MISMATCH".to_string(), - filename, - doc: alloc.stack(lines), - severity: Severity::RuntimeError, - } - } - - Reason::TypedArg { name, arg_index } => { - let name = match name { - Some(n) => alloc.symbol_unqualified(n), - None => alloc.text(" this definition "), - }; - let doc = alloc.stack([ - alloc - .text("The ") - .append(alloc.text(arg_index.ordinal())) - .append(alloc.text(" argument to ")) - .append(name.clone()) - .append(alloc.text(" is weird:")), - alloc.region(lines.convert_region(region)), - pattern_type_comparison( - alloc, - expected_type, - found, - add_category(alloc, alloc.text("The argument matches"), &category), - alloc.concat([ - alloc.text("But the annotation on "), - name, - alloc.text(" says the "), - alloc.text(arg_index.ordinal()), - alloc.text(" argument should be:"), - ]), - vec![], - ), - ]); - - Report { - filename, - title: "TYPE MISMATCH".to_string(), - doc, - severity: Severity::RuntimeError, - } - } - - Reason::LowLevelOpArg { op, arg_index } => { - panic!( - "Compiler bug: argument #{} to low-level operation {:?} was the wrong type!", - arg_index.ordinal(), - op - ); - } - - Reason::ForeignCallArg { - foreign_symbol, - arg_index, - } => { - panic!( - "Compiler bug: argument #{} to foreign symbol {:?} was the wrong type!", - arg_index.ordinal(), - foreign_symbol - ); - } - - Reason::FloatLiteral | Reason::IntLiteral | Reason::NumLiteral => { - unreachable!("I don't think these can be reached") - } - - Reason::StrInterpolation => { - unimplemented!("string interpolation is not implemented yet") - } - - Reason::RecordDefaultField(_) => { - unimplemented!("record default field is not implemented yet") - } - }, - } -} - -fn does_not_implement<'a>( - alloc: &'a RocDocAllocator<'a>, - err_type: ErrorType, - ability: Symbol, -) -> RocDocBuilder<'a> { - alloc.concat([ - to_doc(alloc, Parens::Unnecessary, err_type).0, - alloc.reflow(" does not implement "), - alloc.symbol_unqualified(ability), - ]) -} - -fn count_arguments(tipe: &ErrorType) -> usize { - use ErrorType::*; - - match tipe { - Function(args, _, _) => args.len(), - Alias(_, _, actual, _) => count_arguments(actual), - _ => 0, - } -} - -/// The context a type expectation is derived from. -#[derive(Clone)] -enum ExpectationContext<'a> { - /// An expected type was discovered from a type annotation. Corresponds to - /// [`Expected::FromAnnotation`](Expected::FromAnnotation). - Annotation { - on: RocDocBuilder<'a>, - }, - WhenCondition, - /// When we don't know the context, or it's not relevant. - Arbitrary, -} - -impl<'a> std::fmt::Debug for ExpectationContext<'a> { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - ExpectationContext::Annotation { .. } => f.write_str("Annotation"), - ExpectationContext::WhenCondition => f.write_str("WhenCondition"), - ExpectationContext::Arbitrary => f.write_str("Arbitrary"), - } - } -} - -fn type_comparison<'b>( - alloc: &'b RocDocAllocator<'b>, - actual: ErrorType, - expected: ErrorType, - expectation_context: ExpectationContext<'b>, - i_am_seeing: RocDocBuilder<'b>, - instead_of: RocDocBuilder<'b>, - context_hints: Option>, -) -> RocDocBuilder<'b> { - let comparison = to_comparison(alloc, actual, expected); - - let mut lines = vec![ - i_am_seeing, - comparison.actual, - instead_of, - comparison.expected, - ]; - - if context_hints.is_some() { - lines.push(alloc.concat(context_hints)); - } - - lines.extend(problems_to_tip( - alloc, - comparison.problems, - expectation_context, - )); - - alloc.stack(lines) -} - -fn lone_type<'b>( - alloc: &'b RocDocAllocator<'b>, - actual: ErrorType, - expected: ErrorType, - expectation_context: ExpectationContext<'b>, - i_am_seeing: RocDocBuilder<'b>, - further_details: RocDocBuilder<'b>, -) -> RocDocBuilder<'b> { - let comparison = to_comparison(alloc, actual, expected); - - let mut lines = vec![i_am_seeing, comparison.actual, further_details]; - - lines.extend(problems_to_tip( - alloc, - comparison.problems, - expectation_context, - )); - - alloc.stack(lines) -} - -/// Formats an item in a Roc program to a tuple (summary, has_type_colon), where -/// concatenation of the tuple items introduces the item and leads up to its type. -fn format_category<'b>( - alloc: &'b RocDocAllocator<'b>, - this_is: RocDocBuilder<'b>, - category: &Category, - capitalize_start: bool, -) -> (RocDocBuilder<'b>, RocDocBuilder<'b>) { - use Category::*; - - let t = if capitalize_start { "T" } else { "t" }; - - match category { - Lookup(name) => ( - alloc.concat([ - alloc.text(format!("{}his ", t)), - alloc.symbol_foreign_qualified(*name), - alloc.text(" value"), - ]), - alloc.text(" is a:"), - ), - - If => ( - alloc.concat([ - alloc.text(format!("{}his ", t)), - alloc.keyword("if"), - alloc.text(" expression"), - ]), - alloc.text(" produces:"), - ), - When => ( - alloc.concat([ - alloc.text(format!("{}his ", t)), - alloc.keyword("when"), - alloc.text(" expression"), - ]), - alloc.text(" produces:"), - ), - List => ( - alloc.concat([this_is, alloc.text(" a list")]), - alloc.text(" of type:"), - ), - Num => ( - alloc.concat([this_is, alloc.text(" a number")]), - alloc.text(" of type:"), - ), - Int => ( - alloc.concat([this_is, alloc.text(" an integer")]), - alloc.text(" of type:"), - ), - Float => ( - alloc.concat([this_is, alloc.text(" a frac")]), - alloc.text(" of type:"), - ), - Str => ( - alloc.concat([this_is, alloc.text(" a string")]), - alloc.text(" of type:"), - ), - StrInterpolation => ( - alloc.concat([this_is, alloc.text(" a value in a string interpolation,")]), - alloc.text(" which was of type:"), - ), - Character => ( - alloc.concat([this_is, alloc.text(" a character")]), - alloc.text(" of type:"), - ), - Lambda => ( - alloc.concat([this_is, alloc.text(" an anonymous function")]), - alloc.text(" of type:"), - ), - ClosureSize => ( - alloc.concat([this_is, alloc.text(" the closure size of a function")]), - alloc.text(" of type:"), - ), - - OpaqueWrap(opaque) => ( - alloc.concat([ - alloc.text(format!("{}his ", t)), - alloc.opaque_name(*opaque), - alloc.text(" opaque wrapping"), - ]), - alloc.text(" has the type:"), - ), - - OpaqueArg => ( - alloc.concat([alloc.text(format!("{}his argument to an opaque type", t))]), - alloc.text(" has type:"), - ), - - TagApply { - tag_name: TagName(name), - args_count: 0, - } => ( - alloc.concat([ - alloc.text(format!("{}his ", t)), - alloc.tag(name.to_owned()), - if name.as_str() == "True" || name.as_str() == "False" { - alloc.text(" boolean") - } else { - alloc.text(" tag") - }, - ]), - alloc.text(" has the type:"), - ), - - TagApply { - tag_name: TagName(name), - args_count: _, - } => ( - alloc.concat([ - alloc.text(format!("{}his ", t)), - alloc.tag(name.to_owned()), - alloc.text(" tag application"), - ]), - alloc.text(" has the type:"), - ), - - Record => ( - alloc.concat([this_is, alloc.text(" a record")]), - alloc.text(" of type:"), - ), - - Accessor(field) => ( - alloc.concat([ - alloc.text(format!("{}his ", t)), - alloc.record_field(field.to_owned()), - alloc.text(" value"), - ]), - alloc.text(" is a:"), - ), - Access(field) => ( - alloc.concat([ - alloc.text(format!("{}he value at ", t)), - alloc.record_field(field.to_owned()), - ]), - alloc.text(" is a:"), - ), - CallResult( - Some(_), - CalledVia::BinOp( - BinOp::Equals - | BinOp::NotEquals - | BinOp::LessThan - | BinOp::GreaterThan - | BinOp::LessThanOrEq - | BinOp::GreaterThanOrEq, - ), - ) => ( - alloc.text(format!("{}his comparison", t)), - alloc.text(" produces:"), - ), - CallResult(Some(_), CalledVia::StringInterpolation) => ( - alloc.concat([this_is, alloc.text(" a string")]), - alloc.text(" of type:"), - ), - CallResult(Some(symbol), _) => ( - alloc.concat([ - alloc.text(format!("{}his ", t)), - alloc.symbol_foreign_qualified(*symbol), - alloc.text(" call"), - ]), - alloc.text(" produces:"), - ), - CallResult(None, _) => (this_is, alloc.text(":")), - LowLevelOpResult(op) => { - panic!( - "Compiler bug: invalid return type from low-level op {:?}", - op - ); - } - ForeignCall => { - panic!("Compiler bug: invalid return type from foreign call",); - } - - Uniqueness => ( - alloc.concat([this_is, alloc.text(" an uniqueness attribute")]), - alloc.text(" of type:"), - ), - Storage(..) | Unknown => ( - alloc.concat([this_is, alloc.text(" a value")]), - alloc.text(" of type:"), - ), - DefaultValue(_) => ( - alloc.concat([this_is, alloc.text(" a default field")]), - alloc.text(" of type:"), - ), - AbilityMemberSpecialization(_ability_member) => ( - alloc.concat([this_is, alloc.text(" a declared specialization")]), - alloc.text(" of type:"), - ), - Expect => ( - alloc.concat([this_is, alloc.text(" an expectation")]), - alloc.text(" of type:"), - ), - } -} - -fn add_category<'b>( - alloc: &'b RocDocAllocator<'b>, - this_is: RocDocBuilder<'b>, - category: &Category, -) -> RocDocBuilder<'b> { - let (summary, suffix) = format_category(alloc, this_is, category, true); - alloc.concat([summary, suffix]) -} - -fn to_pattern_report<'b>( - alloc: &'b RocDocAllocator<'b>, - lines: &LineInfo, - filename: PathBuf, - expr_region: roc_region::all::Region, - category: PatternCategory, - found: ErrorType, - expected: PExpected, -) -> Report<'b> { - use roc_types::types::PReason; - - match expected { - PExpected::NoExpectation(expected_type) => { - let doc = alloc.stack([ - alloc.text("This pattern is being used in an unexpected way:"), - alloc.region(lines.convert_region(expr_region)), - pattern_type_comparison( - alloc, - found, - expected_type, - add_pattern_category(alloc, alloc.text("It is"), &category), - alloc.text("But it needs to match:"), - vec![], - ), - ]); - - Report { - filename, - title: "TYPE MISMATCH".to_string(), - doc, - severity: Severity::RuntimeError, - } - } - - PExpected::ForReason(reason, expected_type, region) => match reason { - PReason::OptionalField => unreachable!("this will never be reached I think"), - PReason::TypedArg { opt_name, index } => { - let name = match opt_name { - Some(n) => alloc.symbol_unqualified(n), - None => alloc.text(" this definition "), - }; - let doc = alloc.stack([ - alloc - .text("The ") - .append(alloc.text(index.ordinal())) - .append(alloc.text(" argument to ")) - .append(name.clone()) - .append(alloc.text(" is weird:")), - alloc.region(lines.convert_region(region)), - pattern_type_comparison( - alloc, - found, - expected_type, - add_pattern_category( - alloc, - alloc.text("The argument is a pattern that matches"), - &category, - ), - alloc.concat([ - alloc.text("But the annotation on "), - name, - alloc.text(" says the "), - alloc.text(index.ordinal()), - alloc.text(" argument should be:"), - ]), - vec![], - ), - ]); - - Report { - filename, - title: "TYPE MISMATCH".to_string(), - doc, - severity: Severity::RuntimeError, - } - } - PReason::WhenMatch { index, sub_pattern } => { - let doc = match (index, sub_pattern) { - (HumanIndex::FIRST, HumanIndex::FIRST) => alloc.stack([ - alloc - .text("The 1st pattern in this ") - .append(alloc.keyword("when")) - .append(alloc.text(" is causing a mismatch:")), - alloc.region_with_subregion( - lines.convert_region(region), - lines.convert_region(expr_region), - ), - pattern_type_comparison( - alloc, - found, - expected_type, - add_pattern_category( - alloc, - alloc.text("The first pattern is trying to match"), - &category, - ), - alloc.concat([ - alloc.text("But the expression between "), - alloc.keyword("when"), - alloc.text(" and "), - alloc.keyword("is"), - alloc.text(" has the type:"), - ]), - vec![], - ), - ]), - (index, sub_pattern) => { - let (first, index) = match sub_pattern { - HumanIndex::FIRST => { - let doc = alloc - .string(format!("The {} pattern in this ", index.ordinal())) - .append(alloc.keyword("when")) - .append(alloc.text(" does not match the previous ones:")); - (doc, index) - } - - _ => { - let doc = alloc.string(format!( - "The {} pattern in this branch does not match the previous ones:", - sub_pattern.ordinal() - )); - (doc, sub_pattern) - } - }; - - alloc.stack([ - first, - alloc.region_with_subregion( - lines.convert_region(region), - lines.convert_region(expr_region), - ), - pattern_type_comparison( - alloc, - found, - expected_type, - add_pattern_category( - alloc, - alloc.string(format!( - "The {} pattern is trying to match", - index.ordinal() - )), - &category, - ), - alloc.text("But all the previous branches match:"), - vec![], - ), - ]) - } - }; - Report { - filename, - title: "TYPE MISMATCH".to_string(), - doc, - severity: Severity::RuntimeError, - } - } - PReason::TagArg { .. } | PReason::PatternGuard => { - unreachable!("I didn't think this could trigger. Please tell Folkert about it!") - } - }, - } -} - -fn pattern_type_comparison<'b>( - alloc: &'b RocDocAllocator<'b>, - actual: ErrorType, - expected: ErrorType, - i_am_seeing: RocDocBuilder<'b>, - instead_of: RocDocBuilder<'b>, - reason_hints: Vec>, -) -> RocDocBuilder<'b> { - let comparison = to_comparison(alloc, actual, expected); - - let mut lines = vec![ - i_am_seeing, - comparison.actual, - instead_of, - comparison.expected, - ]; - - lines.extend(problems_to_tip( - alloc, - comparison.problems, - ExpectationContext::Arbitrary, - )); - lines.extend(reason_hints); - - alloc.stack(lines) -} - -fn add_pattern_category<'b>( - alloc: &'b RocDocAllocator<'b>, - i_am_trying_to_match: RocDocBuilder<'b>, - category: &PatternCategory, -) -> RocDocBuilder<'b> { - use PatternCategory::*; - - let rest = match category { - Record => alloc.reflow(" record values of type:"), - EmptyRecord => alloc.reflow(" an empty record:"), - PatternGuard => alloc.reflow(" a pattern guard of type:"), - PatternDefault => alloc.reflow(" an optional field of type:"), - Set => alloc.reflow(" sets of type:"), - Map => alloc.reflow(" maps of type:"), - Ctor(tag_name) => alloc.concat([ - alloc.reflow(" a "), - alloc.tag_name(tag_name.clone()), - alloc.reflow(" tag of type:"), - ]), - Opaque(opaque) => alloc.concat([ - alloc.opaque_name(*opaque), - alloc.reflow(" unwrappings of type:"), - ]), - Str => alloc.reflow(" strings:"), - Num => alloc.reflow(" numbers:"), - Int => alloc.reflow(" integers:"), - Float => alloc.reflow(" floats:"), - Character => alloc.reflow(" characters:"), - }; - - alloc.concat([i_am_trying_to_match, rest]) -} - -fn to_circular_report<'b>( - alloc: &'b RocDocAllocator<'b>, - lines: &LineInfo, - filename: PathBuf, - region: roc_region::all::Region, - symbol: Symbol, - overall_type: ErrorType, -) -> Report<'b> { - Report { - title: "CIRCULAR TYPE".to_string(), - filename, - doc: { - alloc.stack([ - alloc - .reflow("I'm inferring a weird self-referential type for ") - .append(alloc.symbol_unqualified(symbol)) - .append(alloc.text(":")), - alloc.region(lines.convert_region(region)), - alloc.stack([ - alloc.reflow( - "Here is my best effort at writing down the type. \ - You will see ∞ for parts of the type that repeat \ - something already printed out infinitely.", - ), - alloc.type_block(to_doc(alloc, Parens::Unnecessary, overall_type).0), - ]), - ]) - }, - severity: Severity::RuntimeError, - } -} - -#[derive(Debug, Clone)] -pub enum Problem { - IntFloat, - ArityMismatch(usize, usize), - FieldTypo(Lowercase, Vec), - FieldsMissing(Vec), - TagTypo(TagName, Vec), - TagsMissing(Vec), - BadRigidVar(Lowercase, ErrorType, Option), - OptionalRequiredMismatch(Lowercase), - OpaqueComparedToNonOpaque, -} - -fn problems_to_tip<'b>( - alloc: &'b RocDocAllocator<'b>, - mut problems: Vec, - expectation_context: ExpectationContext<'b>, -) -> Option> { - if problems.is_empty() { - None - } else { - let problem = problems.remove(problems.len() - 1); - Some(type_problem_to_pretty(alloc, problem, expectation_context)) - } -} - -pub mod suggest { - use roc_module::ident::Lowercase; - - pub trait ToStr { - fn to_str(&self) -> &str; - } - - impl ToStr for Lowercase { - fn to_str(&self) -> &str { - self.as_str() - } - } - - impl ToStr for &Lowercase { - fn to_str(&self) -> &str { - self.as_str() - } - } - - impl ToStr for &str { - fn to_str(&self) -> &str { - self - } - } - - impl ToStr for super::IdentStr { - fn to_str(&self) -> &str { - self.as_str() - } - } - - impl ToStr for (A, B) - where - A: ToStr, - { - fn to_str(&self) -> &str { - self.0.to_str() - } - } - - pub fn sort(typo: &str, mut options: Vec) -> Vec - where - T: ToStr, - { - options.sort_by(|a, b| { - let l = distance::damerau_levenshtein(typo, a.to_str()); - let r = distance::damerau_levenshtein(typo, b.to_str()); - - l.cmp(&r) - }); - - options - } -} - -pub struct Comparison<'b> { - actual: RocDocBuilder<'b>, - expected: RocDocBuilder<'b>, - problems: Vec, -} - -fn to_comparison<'b>( - alloc: &'b RocDocAllocator<'b>, - actual: ErrorType, - expected: ErrorType, -) -> Comparison<'b> { - let diff = to_diff(alloc, Parens::Unnecessary, actual, expected); - let actual = type_with_able_vars(alloc, diff.left, diff.left_able); - let expected = type_with_able_vars(alloc, diff.right, diff.right_able); - - Comparison { - actual: alloc.type_block(actual), - expected: alloc.type_block(expected), - problems: match diff.status { - Status::Similar => vec![], - Status::Different(problems) => problems, - }, - } -} - -fn diff_is_wildcard_comparison<'b>( - alloc: &'b RocDocAllocator<'b>, - actual: ErrorType, - expected: ErrorType, -) -> bool { - let Comparison { problems, .. } = to_comparison(alloc, actual, expected); - match problems.last() { - Some(Problem::BadRigidVar(v1, ErrorType::RigidVar(v2), None)) => { - v1.as_str() == WILDCARD && v2.as_str() == WILDCARD - } - _ => false, - } -} - -#[derive(Debug)] -pub enum Status { - Similar, // the structure is the same or e.g. record fields are different - Different(Vec), // e.g. found Bool, expected Int -} - -impl Status { - pub fn merge(&mut self, other: Self) { - use Status::*; - match self { - Similar => { - *self = other; - } - Different(problems1) => match other { - Similar => { /* nothing */ } - Different(problems2) => { - // TODO pick a data structure that makes this merge cheaper - let mut problems = Vec::with_capacity(problems1.len() + problems2.len()); - problems.extend(problems1.iter().cloned()); - problems.extend(problems2); - *self = Different(problems); - } - }, - } - } -} - -pub struct Diff { - left: T, - right: T, - status: Status, - // idea: lift "able" type variables so they are shown at the top of a type. - left_able: AbleVariables, - right_able: AbleVariables, -} - -fn ext_to_doc<'b>(alloc: &'b RocDocAllocator<'b>, ext: TypeExt) -> Option> { - use TypeExt::*; - - match ext { - Closed => None, - FlexOpen(lowercase) | RigidOpen(lowercase) => Some(alloc.type_variable(lowercase)), - } -} - -type AbleVariables = Vec<(Lowercase, Symbol)>; - -#[derive(Default)] -struct Context { - able_variables: AbleVariables, -} - -pub fn to_doc<'b>( - alloc: &'b RocDocAllocator<'b>, - parens: Parens, - tipe: ErrorType, -) -> (RocDocBuilder<'b>, AbleVariables) { - let mut ctx = Context::default(); - - let doc = to_doc_help(&mut ctx, alloc, parens, tipe); - - (doc, ctx.able_variables) -} - -fn to_doc_help<'b>( - ctx: &mut Context, - alloc: &'b RocDocAllocator<'b>, - parens: Parens, - tipe: ErrorType, -) -> RocDocBuilder<'b> { - use ErrorType::*; - - match tipe { - Function(args, _, ret) => report_text::function( - alloc, - parens, - args.into_iter() - .map(|arg| to_doc_help(ctx, alloc, Parens::InFn, arg)) - .collect(), - to_doc_help(ctx, alloc, Parens::InFn, *ret), - ), - Infinite => alloc.text("∞"), - Error => alloc.text("?"), - - FlexVar(lowercase) | RigidVar(lowercase) => alloc.type_variable(lowercase), - FlexAbleVar(lowercase, ability) | RigidAbleVar(lowercase, ability) => { - // TODO we should be putting able variables on the toplevel of the type, not here - ctx.able_variables.push((lowercase.clone(), ability)); - alloc.type_variable(lowercase) - } - - Type(symbol, args) => report_text::apply( - alloc, - parens, - alloc.symbol_foreign_qualified(symbol), - args.into_iter() - .map(|arg| to_doc_help(ctx, alloc, Parens::InTypeParam, arg)) - .collect(), - ), - - Alias(symbol, args, _, _) => report_text::apply( - alloc, - parens, - alloc.symbol_foreign_qualified(symbol), - args.into_iter() - .map(|arg| to_doc_help(ctx, alloc, Parens::InTypeParam, arg)) - .collect(), - ), - - Record(fields_map, ext) => { - let mut fields = fields_map.into_iter().collect::>(); - fields.sort_by(|(a, _), (b, _)| a.cmp(b)); - - report_text::record( - alloc, - fields - .into_iter() - .map(|(k, value)| { - ( - alloc.string(k.as_str().to_string()), - match value { - RecordField::Optional(v) => RecordField::Optional(to_doc_help( - ctx, - alloc, - Parens::Unnecessary, - v, - )), - RecordField::Required(v) => RecordField::Required(to_doc_help( - ctx, - alloc, - Parens::Unnecessary, - v, - )), - RecordField::Demanded(v) => RecordField::Demanded(to_doc_help( - ctx, - alloc, - Parens::Unnecessary, - v, - )), - }, - ) - }) - .collect(), - ext_to_doc(alloc, ext), - ) - } - - TagUnion(tags_map, ext) => { - let mut tags = tags_map - .into_iter() - .map(|(name, args)| { - ( - name, - args.into_iter() - .map(|arg| to_doc_help(ctx, alloc, Parens::InTypeParam, arg)) - .collect::>(), - ) - }) - .collect::>(); - tags.sort_by(|(a, _), (b, _)| a.cmp(b)); - - report_text::tag_union( - alloc, - tags.into_iter() - .map(|(k, v)| (alloc.tag_name(k), v)) - .collect(), - ext_to_doc(alloc, ext), - ) - } - - RecursiveTagUnion(rec_var, tags_map, ext) => { - let mut tags = tags_map - .into_iter() - .map(|(name, args)| { - ( - name, - args.into_iter() - .map(|arg| to_doc_help(ctx, alloc, Parens::InTypeParam, arg)) - .collect::>(), - ) - }) - .collect::>(); - tags.sort_by(|(a, _), (b, _)| a.cmp(b)); - - report_text::recursive_tag_union( - alloc, - to_doc_help(ctx, alloc, Parens::Unnecessary, *rec_var), - tags.into_iter() - .map(|(k, v)| (alloc.tag_name(k), v)) - .collect(), - ext_to_doc(alloc, ext), - ) - } - - Range(typ, range_types) => { - let typ = to_doc_help(ctx, alloc, parens, *typ); - let range_types = range_types - .into_iter() - .map(|arg| to_doc_help(ctx, alloc, Parens::Unnecessary, arg)) - .collect(); - report_text::range(alloc, typ, range_types) - } - } -} - -fn same<'b>( - alloc: &'b RocDocAllocator<'b>, - parens: Parens, - tipe: ErrorType, -) -> Diff> { - let (doc, able) = to_doc(alloc, parens, tipe); - - Diff { - left: doc.clone(), - right: doc, - status: Status::Similar, - left_able: able.clone(), - right_able: able, - } -} - -fn type_with_able_vars<'b>( - alloc: &'b RocDocAllocator<'b>, - typ: RocDocBuilder<'b>, - able: AbleVariables, -) -> RocDocBuilder<'b> { - if able.is_empty() { - // fast path: taken the vast majority of the time - return typ; - } - - let mut doc = Vec::with_capacity(1 + 6 * able.len()); - doc.push(typ); - - for (i, (var, ability)) in able.into_iter().enumerate() { - doc.push(alloc.string(if i == 0 { " | " } else { ", " }.to_string())); - doc.push(alloc.type_variable(var)); - doc.push(alloc.space()); - doc.push(alloc.keyword("has")); - doc.push(alloc.space()); - doc.push(alloc.symbol_foreign_qualified(ability)); - } - - alloc.concat(doc) -} - -fn error_type_to_doc<'b>( - alloc: &'b RocDocAllocator<'b>, - error_type: ErrorType, -) -> RocDocBuilder<'b> { - let (typ, able_vars) = to_doc(alloc, Parens::Unnecessary, error_type); - type_with_able_vars(alloc, typ, able_vars) -} - -fn to_diff<'b>( - alloc: &'b RocDocAllocator<'b>, - parens: Parens, - type1: ErrorType, - type2: ErrorType, -) -> Diff> { - use ErrorType::*; - - // TODO remove clone - match (type1.clone(), type2.clone()) { - (Error, Error) | (Infinite, Infinite) => same(alloc, parens, type1), - - (FlexVar(x), FlexVar(y)) if x == y => same(alloc, parens, type1), - // Wildcards are always different! - (RigidVar(x), RigidVar(y)) if x == y && x.as_str() != WILDCARD => { - same(alloc, parens, type1) - } - - (RigidVar(x), other) | (other, RigidVar(x)) => { - let (left, left_able) = to_doc(alloc, Parens::InFn, type1); - let (right, right_able) = to_doc(alloc, Parens::InFn, type2); - - Diff { - left, - right, - status: Status::Different(vec![Problem::BadRigidVar(x, other, None)]), - left_able, - right_able, - } - } - - (RigidAbleVar(x, ab), other) | (other, RigidAbleVar(x, ab)) => { - let (left, left_able) = to_doc(alloc, Parens::InFn, type1); - let (right, right_able) = to_doc(alloc, Parens::InFn, type2); - - Diff { - left, - right, - status: Status::Different(vec![Problem::BadRigidVar(x, other, Some(ab))]), - left_able, - right_able, - } - } - - (Function(args1, _, ret1), Function(args2, _, ret2)) => { - if args1.len() == args2.len() { - let mut status = Status::Similar; - let arg_diff = traverse(alloc, Parens::InFn, args1, args2); - let ret_diff = to_diff(alloc, Parens::InFn, *ret1, *ret2); - status.merge(arg_diff.status); - status.merge(ret_diff.status); - - let left = report_text::function(alloc, parens, arg_diff.left, ret_diff.left); - let right = report_text::function(alloc, parens, arg_diff.right, ret_diff.right); - let mut left_able = arg_diff.left_able; - left_able.extend(ret_diff.left_able); - let mut right_able = arg_diff.right_able; - right_able.extend(ret_diff.right_able); - - Diff { - left, - right, - status, - left_able, - right_able, - } - } else { - let (left, left_able) = to_doc(alloc, Parens::InFn, type1); - let (right, right_able) = to_doc(alloc, Parens::InFn, type2); - - Diff { - left, - right, - status: Status::Different(vec![Problem::ArityMismatch( - args1.len(), - args2.len(), - )]), - left_able, - right_able, - } - } - } - (Type(symbol1, args1), Type(symbol2, args2)) if symbol1 == symbol2 => { - let args_diff = traverse(alloc, Parens::InTypeParam, args1, args2); - let left = report_text::apply( - alloc, - parens, - alloc.symbol_unqualified(symbol1), - args_diff.left, - ); - let right = report_text::apply( - alloc, - parens, - alloc.symbol_unqualified(symbol2), - args_diff.right, - ); - - Diff { - left, - right, - status: args_diff.status, - left_able: args_diff.left_able, - right_able: args_diff.right_able, - } - } - - (Alias(symbol1, args1, _, _), Alias(symbol2, args2, _, _)) if symbol1 == symbol2 => { - let args_diff = traverse(alloc, Parens::InTypeParam, args1, args2); - let left = report_text::apply( - alloc, - parens, - alloc.symbol_unqualified(symbol1), - args_diff.left, - ); - let right = report_text::apply( - alloc, - parens, - alloc.symbol_unqualified(symbol2), - args_diff.right, - ); - - Diff { - left, - right, - status: args_diff.status, - left_able: args_diff.left_able, - right_able: args_diff.right_able, - } - } - - (Alias(sym, _, _, AliasKind::Opaque), _) | (_, Alias(sym, _, _, AliasKind::Opaque)) - // Skip the hint for numbers; it's not as useful as saying "this type is not a number" - if !OPAQUE_NUM_SYMBOLS.contains(&sym) => - { - let (left, left_able) = to_doc(alloc, Parens::InFn, type1); - let (right, right_able) = to_doc(alloc, Parens::InFn, type2); - - Diff { - left, - right, - status: Status::Different(vec![Problem::OpaqueComparedToNonOpaque]), - left_able, - right_able, - } - } - - (Alias(symbol, _, actual, AliasKind::Structural), other) - if !symbol.module_id().is_builtin() => - { - // when diffing a structural alias with a non-alias, de-alias - to_diff(alloc, parens, *actual, other) - } - (other, Alias(symbol, _, actual, AliasKind::Structural)) - if !symbol.module_id().is_builtin() => - { - // when diffing a structural alias with a non-alias, de-alias - to_diff(alloc, parens, other, *actual) - } - - (Record(fields1, ext1), Record(fields2, ext2)) => { - diff_record(alloc, fields1, ext1, fields2, ext2) - } - - (TagUnion(tags1, ext1), TagUnion(tags2, ext2)) => { - diff_tag_union(alloc, &tags1, ext1, &tags2, ext2) - } - - (RecursiveTagUnion(_rec1, _tags1, _ext1), RecursiveTagUnion(_rec2, _tags2, _ext2)) => { - // TODO do a better job here - let (left, left_able) = to_doc(alloc, Parens::Unnecessary, type1); - let (right, right_able) = to_doc(alloc, Parens::Unnecessary, type2); - - Diff { - left, - right, - status: Status::Similar, - left_able, - right_able, - } - } - - pair => { - // We hit none of the specific cases where we give more detailed information - let (left, left_able) = to_doc(alloc, parens, type1); - let (right, right_able) = to_doc(alloc, parens, type2); - - let is_int = |t: &ErrorType| match t { - ErrorType::Type(Symbol::NUM_INT, _) => true, - ErrorType::Alias(Symbol::NUM_INT, _, _, _) => true, - - ErrorType::Type(Symbol::NUM_NUM, args) => { - matches!( - &args.get(0), - Some(ErrorType::Type(Symbol::NUM_INTEGER, _)) - | Some(ErrorType::Alias(Symbol::NUM_INTEGER, _, _, _)) - ) - } - ErrorType::Alias(Symbol::NUM_NUM, args, _, _) => { - matches!( - &args.get(0), - Some(ErrorType::Type(Symbol::NUM_INTEGER, _)) - | Some(ErrorType::Alias(Symbol::NUM_INTEGER, _, _, _)) - ) - } - _ => false, - }; - let is_float = |t: &ErrorType| match t { - ErrorType::Type(Symbol::NUM_FRAC, _) => true, - ErrorType::Alias(Symbol::NUM_FRAC, _, _, _) => true, - - ErrorType::Type(Symbol::NUM_NUM, args) => { - matches!( - &args.get(0), - Some(ErrorType::Type(Symbol::NUM_FLOATINGPOINT, _)) - | Some(ErrorType::Alias(Symbol::NUM_FLOATINGPOINT, _, _, _)) - ) - } - - ErrorType::Alias(Symbol::NUM_NUM, args, _, _) => { - matches!( - &args.get(0), - Some(ErrorType::Type(Symbol::NUM_FLOATINGPOINT, _)) - | Some(ErrorType::Alias(Symbol::NUM_FLOATINGPOINT, _, _, _)) - ) - } - _ => false, - }; - - let problems = match pair { - (a, b) if (is_int(&a) && is_float(&b)) || (is_float(&a) && is_int(&b)) => { - vec![Problem::IntFloat] - } - _ => vec![], - }; - - Diff { - left, - right, - status: Status::Different(problems), - left_able, - right_able, - } - } - } -} - -fn traverse<'b, I>( - alloc: &'b RocDocAllocator<'b>, - parens: Parens, - args1: I, - args2: I, -) -> Diff>> -where - I: IntoIterator, -{ - let mut status = Status::Similar; - - // TODO use ExactSizeIterator to pre-allocate here - let mut left = Vec::new(); - let mut right = Vec::new(); - let mut left_able = Vec::new(); - let mut right_able = Vec::new(); - - for (arg1, arg2) in args1.into_iter().zip(args2.into_iter()) { - let diff = to_diff(alloc, parens, arg1, arg2); - - left.push(diff.left); - right.push(diff.right); - status.merge(diff.status); - left_able.extend(diff.left_able); - right_able.extend(diff.right_able); - } - - Diff { - left, - right, - status, - left_able, - right_able, - } -} - -fn ext_has_fixed_fields(ext: &TypeExt) -> bool { - match ext { - TypeExt::Closed => true, - TypeExt::FlexOpen(_) => false, - TypeExt::RigidOpen(_) => true, - } -} - -fn diff_record<'b>( - alloc: &'b RocDocAllocator<'b>, - fields1: SendMap>, - ext1: TypeExt, - fields2: SendMap>, - ext2: TypeExt, -) -> Diff> { - let to_overlap_docs = |(field, (t1, t2)): ( - &Lowercase, - &(RecordField, RecordField), - )| { - let diff = to_diff( - alloc, - Parens::Unnecessary, - t1.clone().into_inner(), - t2.clone().into_inner(), - ); - - Diff { - left: ( - field.clone(), - alloc.string(field.as_str().to_string()), - match t1 { - RecordField::Optional(_) => RecordField::Optional(diff.left), - RecordField::Required(_) => RecordField::Required(diff.left), - RecordField::Demanded(_) => RecordField::Demanded(diff.left), - }, - ), - right: ( - field.clone(), - alloc.string(field.as_str().to_string()), - match t2 { - RecordField::Optional(_) => RecordField::Optional(diff.right), - RecordField::Required(_) => RecordField::Required(diff.right), - RecordField::Demanded(_) => RecordField::Demanded(diff.right), - }, - ), - status: { - match (&t1, &t2) { - (RecordField::Demanded(_), RecordField::Optional(_)) - | (RecordField::Optional(_), RecordField::Demanded(_)) => match diff.status { - Status::Similar => { - Status::Different(vec![Problem::OptionalRequiredMismatch( - field.clone(), - )]) - } - Status::Different(mut problems) => { - problems.push(Problem::OptionalRequiredMismatch(field.clone())); - - Status::Different(problems) - } - }, - _ => diff.status, - } - }, - left_able: diff.left_able, - right_able: diff.right_able, - } - }; - - let to_unknown_docs = |(field, tipe): (&Lowercase, &RecordField)| { - ( - field.clone(), - alloc.string(field.as_str().to_string()), - tipe.map(|t| to_doc(alloc, Parens::Unnecessary, t.clone()).0), - ) - }; - let shared_keys = fields1 - .clone() - .intersection_with(fields2.clone(), |v1, v2| (v1, v2)); - let left_keys = fields1.clone().relative_complement(fields2.clone()); - let right_keys = fields2.clone().relative_complement(fields1.clone()); - - let both = shared_keys.iter().map(to_overlap_docs); - let mut left = left_keys.iter().map(to_unknown_docs).peekable(); - let mut right = right_keys.iter().map(to_unknown_docs).peekable(); - - let all_fields_shared = left.peek().is_none() && right.peek().is_none(); - - let status = match (ext_has_fixed_fields(&ext1), ext_has_fixed_fields(&ext2)) { - (true, true) => match left.peek() { - Some((f, _, _)) => Status::Different(vec![Problem::FieldTypo( - f.clone(), - fields2.keys().cloned().collect(), - )]), - None => { - if right.peek().is_none() { - Status::Similar - } else { - let result = Status::Different(vec![Problem::FieldsMissing( - right.map(|v| v.0).collect(), - )]); - // we just used the values in `right`. in - right = right_keys.iter().map(to_unknown_docs).peekable(); - result - } - } - }, - (false, true) => match left.peek() { - Some((f, _, _)) => Status::Different(vec![Problem::FieldTypo( - f.clone(), - fields2.keys().cloned().collect(), - )]), - None => Status::Similar, - }, - (true, false) => match right.peek() { - Some((f, _, _)) => Status::Different(vec![Problem::FieldTypo( - f.clone(), - fields1.keys().cloned().collect(), - )]), - None => Status::Similar, - }, - (false, false) => Status::Similar, - }; - - let ext_diff = ext_to_diff(alloc, ext1, ext2); - - let mut fields_diff: Diff, RecordField>)>> = - Diff { - left: vec![], - right: vec![], - status: Status::Similar, - left_able: vec![], - right_able: vec![], - }; - - for diff in both { - fields_diff.left.push(diff.left); - fields_diff.right.push(diff.right); - fields_diff.status.merge(diff.status); - fields_diff.left_able.extend(diff.left_able); - fields_diff.right_able.extend(diff.right_able); - } - - if !all_fields_shared { - fields_diff.left.extend(left); - fields_diff.right.extend(right); - fields_diff.status.merge(Status::Different(vec![])); - } - - // sort fields for display - fields_diff.left.sort_by(|a, b| a.0.cmp(&b.0)); - fields_diff.right.sort_by(|a, b| a.0.cmp(&b.0)); - - let doc1 = report_text::record( - alloc, - fields_diff - .left - .into_iter() - .map(|(_, b, c)| (b, c)) - .collect(), - ext_diff.left, - ); - let doc2 = report_text::record( - alloc, - fields_diff - .right - .into_iter() - .map(|(_, b, c)| (b, c)) - .collect(), - ext_diff.right, - ); - - fields_diff.status.merge(status); - - Diff { - left: doc1, - right: doc2, - status: fields_diff.status, - left_able: fields_diff.left_able, - right_able: fields_diff.right_able, - } -} - -fn diff_tag_union<'b>( - alloc: &'b RocDocAllocator<'b>, - fields1: &SendMap>, - ext1: TypeExt, - fields2: &SendMap>, - ext2: TypeExt, -) -> Diff> { - let to_overlap_docs = |(field, (t1, t2)): (TagName, (Vec, Vec))| { - let diff = traverse(alloc, Parens::Unnecessary, t1, t2); - - Diff { - left: (field.clone(), alloc.tag_name(field.clone()), diff.left), - right: (field.clone(), alloc.tag_name(field), diff.right), - status: diff.status, - left_able: diff.left_able, - right_able: diff.right_able, - } - }; - let to_unknown_docs = |(field, args): (&TagName, &Vec)| -> ( - TagName, - RocDocBuilder<'b>, - Vec>, - AbleVariables, - ) { - let (args, able): (_, Vec) = - // TODO add spaces between args - args.iter() - .map(|arg| to_doc(alloc, Parens::InTypeParam, arg.clone())) - .unzip(); - ( - field.clone(), - alloc.tag_name(field.clone()), - args, - able.into_iter().flatten().collect(), - ) - }; - let shared_keys = fields1 - .clone() - .intersection_with(fields2.clone(), |v1, v2| (v1, v2)); - - let left_keys = fields1.clone().relative_complement(fields2.clone()); - let right_keys = fields2.clone().relative_complement(fields1.clone()); - - let both = shared_keys.into_iter().map(to_overlap_docs); - let mut left = left_keys.iter().map(to_unknown_docs).peekable(); - let mut right = right_keys.iter().map(to_unknown_docs).peekable(); - - let all_fields_shared = left.peek().is_none() && right.peek().is_none(); - - let status = match (ext_has_fixed_fields(&ext1), ext_has_fixed_fields(&ext2)) { - (true, true) => match (left.peek(), right.peek()) { - (Some((f, _, _, _)), Some(_)) => Status::Different(vec![Problem::TagTypo( - f.clone(), - fields2.keys().cloned().collect(), - )]), - (Some(_), None) => { - let status = - Status::Different(vec![Problem::TagsMissing(left.map(|v| v.0).collect())]); - left = left_keys.iter().map(to_unknown_docs).peekable(); - status - } - (None, Some(_)) => { - let status = - Status::Different(vec![Problem::TagsMissing(right.map(|v| v.0).collect())]); - right = right_keys.iter().map(to_unknown_docs).peekable(); - status - } - (None, None) => Status::Similar, - }, - (false, true) => match left.peek() { - Some((f, _, _, _)) => Status::Different(vec![Problem::TagTypo( - f.clone(), - fields2.keys().cloned().collect(), - )]), - None => Status::Similar, - }, - (true, false) => match right.peek() { - Some((f, _, _, _)) => Status::Different(vec![Problem::TagTypo( - f.clone(), - fields1.keys().cloned().collect(), - )]), - None => Status::Similar, - }, - (false, false) => Status::Similar, - }; - - let ext_diff = ext_to_diff(alloc, ext1, ext2); - - let mut fields_diff: Diff, Vec>)>> = Diff { - left: vec![], - right: vec![], - status: Status::Similar, - left_able: vec![], - right_able: vec![], - }; - - for diff in both { - fields_diff.left.push(diff.left); - fields_diff.right.push(diff.right); - fields_diff.status.merge(diff.status); - fields_diff.left_able.extend(diff.left_able); - fields_diff.right_able.extend(diff.right_able); - } - - if !all_fields_shared { - for (tag, tag_doc, args, able) in left { - fields_diff.left.push((tag, tag_doc, args)); - fields_diff.left_able.extend(able); - } - for (tag, tag_doc, args, able) in right { - fields_diff.right.push((tag, tag_doc, args)); - fields_diff.right_able.extend(able); - } - fields_diff.status.merge(Status::Different(vec![])); - } - - fields_diff.left.sort_by(|a, b| a.0.cmp(&b.0)); - fields_diff.right.sort_by(|a, b| a.0.cmp(&b.0)); - - let lefts = fields_diff - .left - .into_iter() - .map(|(_, a, b)| (a, b)) - .collect(); - let rights = fields_diff - .right - .into_iter() - .map(|(_, a, b)| (a, b)) - .collect(); - - let doc1 = report_text::tag_union(alloc, lefts, ext_diff.left); - let doc2 = report_text::tag_union(alloc, rights, ext_diff.right); - - fields_diff.status.merge(status); - - Diff { - left: doc1, - right: doc2, - status: fields_diff.status, - left_able: fields_diff.left_able, - right_able: fields_diff.right_able, - } -} - -fn ext_to_diff<'b>( - alloc: &'b RocDocAllocator<'b>, - ext1: TypeExt, - ext2: TypeExt, -) -> Diff>> { - let status = ext_to_status(&ext1, &ext2); - let ext_doc_1 = ext_to_doc(alloc, ext1); - let ext_doc_2 = ext_to_doc(alloc, ext2); - - match &status { - Status::Similar => Diff { - left: ext_doc_1, - right: ext_doc_2, - status, - left_able: vec![], - right_able: vec![], - }, - Status::Different(_) => Diff { - // NOTE elm colors these differently at this point - left: ext_doc_1, - right: ext_doc_2, - status, - left_able: vec![], - right_able: vec![], - }, - } -} - -fn ext_to_status(ext1: &TypeExt, ext2: &TypeExt) -> Status { - use TypeExt::*; - match ext1 { - Closed => match ext2 { - Closed => Status::Similar, - FlexOpen(_) => Status::Similar, - RigidOpen(_) => Status::Different(vec![]), - }, - FlexOpen(_) => Status::Similar, - - RigidOpen(x) => match ext2 { - Closed => Status::Different(vec![]), - FlexOpen(_) => Status::Similar, - RigidOpen(y) => { - if x == y { - Status::Similar - } else { - Status::Different(vec![Problem::BadRigidVar( - x.clone(), - ErrorType::RigidVar(y.clone()), - None, - )]) - } - } - }, - } -} - -mod report_text { - use crate::report::{Annotation, RocDocAllocator, RocDocBuilder}; - use roc_module::ident::Lowercase; - use roc_types::pretty_print::Parens; - use roc_types::types::{ErrorType, RecordField, TypeExt}; - use ven_pretty::DocAllocator; - - fn with_parens<'b>( - alloc: &'b RocDocAllocator<'b>, - text: RocDocBuilder<'b>, - ) -> RocDocBuilder<'b> { - alloc.text("(").append(text).append(alloc.text(")")) - } - - pub fn function<'b>( - alloc: &'b RocDocAllocator<'b>, - parens: Parens, - args: Vec>, - ret: RocDocBuilder<'b>, - ) -> RocDocBuilder<'b> { - let function_doc = alloc.concat([ - alloc.intersperse(args, alloc.reflow(", ")), - alloc.reflow(" -> "), - ret, - ]); - - match parens { - Parens::Unnecessary => function_doc, - _ => with_parens(alloc, function_doc), - } - } - - pub fn apply<'b>( - alloc: &'b RocDocAllocator<'b>, - parens: Parens, - name: RocDocBuilder<'b>, - args: Vec>, - ) -> RocDocBuilder<'b> { - if args.is_empty() { - name - } else { - let apply_doc = - alloc.concat([name, alloc.space(), alloc.intersperse(args, alloc.space())]); - - match parens { - Parens::Unnecessary | Parens::InFn => apply_doc, - Parens::InTypeParam => with_parens(alloc, apply_doc), - } - } - } - - pub fn record<'b>( - alloc: &'b RocDocAllocator<'b>, - entries: Vec<(RocDocBuilder<'b>, RecordField>)>, - opt_ext: Option>, - ) -> RocDocBuilder<'b> { - let ext_doc = if let Some(t) = opt_ext { - t - } else { - alloc.nil() - }; - - if entries.is_empty() { - alloc.text("{}").append(ext_doc) - } else { - let entry_to_doc = - |(field_name, field_type): (RocDocBuilder<'b>, RecordField>)| { - match field_type { - RecordField::Demanded(field) => { - field_name.append(alloc.text(" : ")).append(field) - } - RecordField::Required(field) => { - field_name.append(alloc.text(" : ")).append(field) - } - RecordField::Optional(field) => { - field_name.append(alloc.text(" ? ")).append(field) - } - } - }; - - let starts = - std::iter::once(alloc.reflow("{ ")).chain(std::iter::repeat(alloc.reflow(", "))); - - let entries_doc = alloc.concat( - entries - .into_iter() - .zip(starts) - .map(|(entry, start)| start.append(entry_to_doc(entry))), - ); - - entries_doc.append(alloc.reflow(" }")).append(ext_doc) - } - } - - pub fn to_suggestion_record<'b>( - alloc: &'b RocDocAllocator<'b>, - f: (Lowercase, RecordField), - fs: Vec<(Lowercase, RecordField)>, - ext: TypeExt, - ) -> RocDocBuilder<'b> { - use crate::error::r#type::{ext_to_doc, to_doc}; - - let entry_to_doc = |(name, tipe): (Lowercase, RecordField)| { - ( - alloc.string(name.as_str().to_string()), - to_doc(alloc, Parens::Unnecessary, tipe.into_inner()).0, - ) - }; - - let mut selection = vec![f]; - selection.extend(fs); - - let fields = selection.into_iter().map(entry_to_doc).collect(); - - vertical_record(alloc, fields, ext_to_doc(alloc, ext)) - .annotate(Annotation::TypeBlock) - .indent(4) - } - - fn vertical_record<'b>( - alloc: &'b RocDocAllocator<'b>, - entries: Vec<(RocDocBuilder<'b>, RocDocBuilder<'b>)>, - opt_ext: Option>, - ) -> RocDocBuilder<'b> { - let fields = if entries.is_empty() { - alloc.text("{}") - } else { - const MAX_ENTRIES_TO_DISPLAY: usize = 4; - - let is_truncated = entries.len() > MAX_ENTRIES_TO_DISPLAY; - let entry_to_doc = - |(field_name, field_type): (RocDocBuilder<'b>, RocDocBuilder<'b>)| { - field_name - .indent(4) - .append(alloc.text(" : ")) - .append(field_type) - .append(alloc.text(",")) - }; - - let closing = std::iter::once(alloc.text("}")); - let fields = std::iter::once(alloc.reflow("{")).chain( - entries - .into_iter() - .map(entry_to_doc) - .take(MAX_ENTRIES_TO_DISPLAY), - ); - - if is_truncated { - alloc.vcat( - fields - .chain(std::iter::once(alloc.text("…").indent(4))) - .chain(closing), - ) - } else { - alloc.vcat(fields.chain(closing)) - } - }; - - match opt_ext { - Some(ext) => fields.append(ext), - None => fields, - } - } - - pub fn tag_union<'b>( - alloc: &'b RocDocAllocator<'b>, - entries: Vec<(RocDocBuilder<'b>, Vec>)>, - opt_ext: Option>, - ) -> RocDocBuilder<'b> { - let ext_doc = if let Some(t) = opt_ext { - t - } else { - alloc.nil() - }; - - if entries.is_empty() { - alloc.text("[]") - } else { - let entry_to_doc = |(tag_name, arguments): (RocDocBuilder<'b>, Vec<_>)| { - if arguments.is_empty() { - tag_name - } else { - tag_name - .append(alloc.space()) - .append(alloc.intersperse(arguments, alloc.space())) - } - }; - - let starts = - std::iter::once(alloc.reflow("[")).chain(std::iter::repeat(alloc.reflow(", "))); - - let entries_doc = alloc.concat( - entries - .into_iter() - .zip(starts) - .map(|(entry, start)| start.append(entry_to_doc(entry))), - ); - - entries_doc.append(alloc.reflow("]")).append(ext_doc) - } - } - - pub fn recursive_tag_union<'b>( - alloc: &'b RocDocAllocator<'b>, - rec_var: RocDocBuilder<'b>, - entries: Vec<(RocDocBuilder<'b>, Vec>)>, - opt_ext: Option>, - ) -> RocDocBuilder<'b> { - let ext_doc = if let Some(t) = opt_ext { - t - } else { - alloc.nil() - }; - - if entries.is_empty() { - alloc.text("[]") - } else { - let entry_to_doc = |(tag_name, arguments): (RocDocBuilder<'b>, Vec<_>)| { - if arguments.is_empty() { - tag_name - } else { - tag_name - .append(alloc.space()) - .append(alloc.intersperse(arguments, alloc.space())) - } - }; - - let starts = - std::iter::once(alloc.reflow("[")).chain(std::iter::repeat(alloc.reflow(", "))); - - let entries_doc = alloc.concat( - entries - .into_iter() - .zip(starts) - .map(|(entry, start)| start.append(entry_to_doc(entry))), - ); - - entries_doc - .append(alloc.reflow("]")) - .append(ext_doc) - .append(alloc.text(" as ")) - .append(rec_var) - } - } - - pub fn range<'b>( - alloc: &'b RocDocAllocator<'b>, - _encompassing_type: RocDocBuilder<'b>, - ranged_types: Vec>, - ) -> RocDocBuilder<'b> { - let mut doc = Vec::with_capacity(ranged_types.len() * 2); - - let last = ranged_types.len() - 1; - for (i, choice) in ranged_types.into_iter().enumerate() { - if i == last && i == 1 { - doc.push(alloc.reflow(" or ")); - } else if i == last && i > 1 { - doc.push(alloc.reflow(", or ")); - } else if i > 0 { - doc.push(alloc.reflow(", ")); - } - - doc.push(choice); - } - - alloc.concat(doc) - } -} - -fn type_problem_to_pretty<'b>( - alloc: &'b RocDocAllocator<'b>, - problem: crate::error::r#type::Problem, - expectation_context: ExpectationContext<'b>, -) -> RocDocBuilder<'b> { - use crate::error::r#type::Problem::*; - - match (problem, expectation_context) { - (FieldTypo(typo, possibilities), _) => { - let suggestions = suggest::sort(typo.as_str(), possibilities); - - match suggestions.get(0) { - None => alloc.nil(), - Some(nearest) => { - let typo_str = format!("{}", typo); - let nearest_str = format!("{}", nearest); - - let found = alloc.text(typo_str).annotate(Annotation::Typo); - let suggestion = alloc.text(nearest_str).annotate(Annotation::TypoSuggestion); - - let tip1 = alloc - .tip() - .append(alloc.reflow("Seems like a record field typo. Maybe ")) - .append(found) - .append(alloc.reflow(" should be ")) - .append(suggestion) - .append(alloc.text("?")); - - let tip2 = alloc.tip().append(alloc.reflow(ADD_ANNOTATIONS)); - - tip1.append(alloc.line()).append(alloc.line()).append(tip2) - } - } - } - (FieldsMissing(missing), _) => match missing.split_last() { - None => alloc.nil(), - Some((f1, [])) => alloc - .tip() - .append(alloc.reflow("Looks like the ")) - .append(f1.as_str().to_owned()) - .append(alloc.reflow(" field is missing.")), - Some((last, init)) => { - let separator = alloc.reflow(", "); - - alloc - .tip() - .append(alloc.reflow("Looks like the ")) - .append( - alloc.intersperse(init.iter().map(|v| v.as_str().to_owned()), separator), - ) - .append(alloc.reflow(" and ")) - .append(alloc.text(last.as_str().to_owned())) - .append(alloc.reflow(" fields are missing.")) - } - }, - (TagTypo(typo, possibilities_tn), _) => { - let possibilities: Vec = possibilities_tn - .into_iter() - .map(|tag_name| tag_name.as_ident_str()) - .collect(); - let typo_str = format!("{}", typo.as_ident_str()); - let suggestions = suggest::sort(&typo_str, possibilities); - - match suggestions.get(0) { - None => alloc.nil(), - Some(nearest) => { - let nearest_str = format!("{}", nearest); - - let found = alloc.text(typo_str).annotate(Annotation::Typo); - let suggestion = alloc.text(nearest_str).annotate(Annotation::TypoSuggestion); - - let tip1 = alloc - .tip() - .append(alloc.reflow("Seems like a tag typo. Maybe ")) - .append(found) - .append(" should be ") - .append(suggestion) - .append(alloc.text("?")); - - let tip2 = alloc.tip().append(alloc.reflow(ADD_ANNOTATIONS)); - - tip1.append(alloc.line()).append(alloc.line()).append(tip2) - } - } - } - (ArityMismatch(found, expected), _) => { - let line = if found < expected { - format!( - "It looks like it takes too few arguments. I was expecting {} more.", - expected - found - ) - } else { - format!( - "It looks like it takes too many arguments. I'm seeing {} extra.", - found - expected - ) - }; - - alloc.tip().append(line) - } - - (BadRigidVar(x, tipe, opt_ability), expectation) => { - use ErrorType::*; - - let bad_rigid_var = |name: Lowercase, a_thing| { - let kind_of_value = match opt_ability { - Some(ability) => alloc.concat([ - alloc.reflow("any value implementing the "), - alloc.symbol_unqualified(ability), - alloc.reflow(" ability"), - ]), - None => alloc.reflow("any type of value"), - }; - alloc - .tip() - .append(alloc.reflow("The type annotation uses the type variable ")) - .append(alloc.type_variable(name)) - .append(alloc.reflow(" to say that this definition can produce ") - .append(kind_of_value) - .append(alloc.reflow(". But in the body I see that it will only produce "))) - .append(a_thing) - .append(alloc.reflow(" of a single specific type. Maybe change the type annotation to be more specific? Maybe change the code to be more general?")) - }; - - let bad_double_wildcard = || { - let mut hints_lines = vec![ - alloc.reflow( - "Any connection between types must use a named type variable, not a ", - ), - alloc.type_variable(WILDCARD.into()), - alloc.reflow("!"), - ]; - if let ExpectationContext::Annotation { on } = expectation { - hints_lines.append(&mut vec![ - alloc.reflow(" Maybe the annotation "), - on, - alloc.reflow(" should have a named type variable in place of the "), - alloc.type_variable(WILDCARD.into()), - alloc.reflow("?"), - ]); - } - alloc.tip().append(alloc.concat(hints_lines)) - }; - - let bad_double_rigid = |a: Lowercase, b: Lowercase| { - if a.as_str() == WILDCARD && b.as_str() == WILDCARD { - return bad_double_wildcard(); - } - let line = r#" as separate type variables. Your code seems to be saying they are the same though. Maybe they should be the same in your type annotation? Maybe your code uses them in a weird way?"#; - - alloc - .tip() - .append(alloc.reflow("Your type annotation uses ")) - .append(alloc.type_variable(a)) - .append(alloc.reflow(" and ")) - .append(alloc.type_variable(b)) - .append(alloc.reflow(line)) - }; - - match tipe { - Infinite | Error | FlexVar(_) => alloc.nil(), - FlexAbleVar(_, ability) => bad_rigid_var( - x, - alloc.concat([ - alloc.reflow("an instance of the ability "), - alloc.symbol_unqualified(ability), - ]), - ), - RigidVar(y) | RigidAbleVar(y, _) => bad_double_rigid(x, y), - Function(_, _, _) => bad_rigid_var(x, alloc.reflow("a function value")), - Record(_, _) => bad_rigid_var(x, alloc.reflow("a record value")), - TagUnion(_, _) | RecursiveTagUnion(_, _, _) => { - bad_rigid_var(x, alloc.reflow("a tag value")) - } - Alias(symbol, _, _, _) | Type(symbol, _) => bad_rigid_var( - x, - alloc.concat([ - alloc.reflow("a "), - alloc.symbol_unqualified(symbol), - alloc.reflow(" value"), - ]), - ), - Range(..) => bad_rigid_var(x, alloc.reflow("a range")), - } - } - - (IntFloat, _) => alloc.tip().append(alloc.concat([ - alloc.reflow("You can convert between "), - alloc.type_str("Int"), - alloc.reflow(" and "), - alloc.type_str("Frac"), - alloc.reflow(" using functions like "), - alloc.symbol_qualified(Symbol::NUM_TO_FRAC), - alloc.reflow(" and "), - alloc.symbol_qualified(Symbol::NUM_ROUND), - alloc.reflow("."), - ])), - - (TagsMissing(missing), ExpectationContext::WhenCondition) => match missing.split_last() { - None => alloc.nil(), - Some(split) => { - let missing_tags = match split { - (f1, []) => alloc.tag_name(f1.clone()).append(alloc.reflow(" tag.")), - (last, init) => alloc - .intersperse(init.iter().map(|v| alloc.tag_name(v.clone())), ", ") - .append(alloc.reflow(" and ")) - .append(alloc.tag_name(last.clone())) - .append(alloc.reflow(" tags.")), - }; - - let tip1 = alloc - .tip() - .append(alloc.reflow("Looks like the branches are missing coverage of the ")) - .append(missing_tags); - - let tip2 = alloc - .tip() - .append(alloc.reflow("Maybe you need to add a catch-all branch, like ")) - .append(alloc.keyword("_")) - .append(alloc.reflow("?")); - - alloc.stack([tip1, tip2]) - } - }, - - (TagsMissing(missing), _) => match missing.split_last() { - None => alloc.nil(), - Some((f1, [])) => { - let tip1 = alloc - .tip() - .append(alloc.reflow("Looks like a closed tag union does not have the ")) - .append(alloc.tag_name(f1.clone())) - .append(alloc.reflow(" tag.")); - - let tip2 = alloc.tip().append(alloc.reflow( - "Closed tag unions can't grow, \ - because that might change the size in memory. \ - Can you use an open tag union?", - )); - - alloc.stack([tip1, tip2]) - } - - Some((last, init)) => { - let separator = alloc.reflow(", "); - - let tip1 = alloc - .tip() - .append(alloc.reflow("Looks like a closed tag union does not have the ")) - .append( - alloc - .intersperse(init.iter().map(|v| alloc.tag_name(v.clone())), separator), - ) - .append(alloc.reflow(" and ")) - .append(alloc.tag_name(last.clone())) - .append(alloc.reflow(" tags.")); - - let tip2 = alloc.tip().append(alloc.reflow( - "Closed tag unions can't grow, \ - because that might change the size in memory. \ - Can you use an open tag union?", - )); - - alloc.stack([tip1, tip2]) - } - }, - (OptionalRequiredMismatch(field), _) => alloc.tip().append(alloc.concat([ - alloc.reflow("To extract the "), - alloc.record_field(field), - alloc.reflow( - " field it must be non-optional, but the type says this field is optional. ", - ), - alloc.reflow("Learn more about optional fields at TODO."), - ])), - - (OpaqueComparedToNonOpaque, _) => alloc.tip().append(alloc.concat([ - alloc.reflow( - "Type comparisons between an opaque type are only ever \ - equal if both types are the same opaque type. Did you mean \ - to create an opaque type by wrapping it? If I have an opaque type ", - ), - alloc.type_str("Age := U32"), - alloc.reflow(" I can create an instance of this opaque type by doing "), - alloc.type_str("@Age 23"), - alloc.reflow("."), - ])), - } -} - -#[allow(clippy::too_many_arguments)] -fn report_record_field_typo<'b>( - alloc: &'b RocDocAllocator<'b>, - lines: &LineInfo, - filename: PathBuf, - opt_sym: Option, - field_prefix: &str, - field: &Lowercase, - field_suffix: &str, - field_region: Region, - actual_fields: SendMap>, - ext: TypeExt, -) -> Report<'b> { - let header = { - let f_doc = alloc - .text(field.as_str().to_string()) - .annotate(Annotation::Typo); - - let r_doc = match opt_sym { - Some(symbol) => alloc.symbol_unqualified(symbol).append(" "), - None => alloc.text(""), - }; - - alloc.concat([ - alloc.reflow("This "), - r_doc, - alloc.reflow("record doesn’t have a "), - f_doc, - alloc.reflow(" field:"), - ]) - }; - - let mut suggestions = suggest::sort( - field.as_str(), - actual_fields.into_iter().collect::>(), - ); - - let doc = alloc.stack([ - header, - alloc.region(lines.convert_region(field_region)), - if suggestions.is_empty() { - let r_doc = match opt_sym { - Some(symbol) => alloc.symbol_unqualified(symbol).append(" is"), - None => alloc.text("it’s"), - }; - alloc.concat([ - alloc.reflow("In fact, "), - r_doc, - alloc.reflow(" a record with no fields at all!"), - ]) - } else { - let f = suggestions.remove(0); - let fs = suggestions; - let f_doc = alloc - .text(format!("{}{}{}", field_prefix, field, field_suffix)) - .annotate(Annotation::Typo); - - let r_doc = match opt_sym { - Some(symbol) => alloc.symbol_unqualified(symbol).append(" fields"), - None => alloc.text("fields on the record"), - }; - - alloc.stack([ - alloc.concat([ - alloc.reflow("There may be a typo. These "), - r_doc, - alloc.reflow(" are the most similar:"), - ]), - report_text::to_suggestion_record(alloc, f.clone(), fs, ext), - alloc.concat([ - alloc.reflow("Maybe "), - f_doc, - alloc.reflow(" should be "), - alloc - .text(format!("{}{}{}", field_prefix, f.0, field_suffix)) - .annotate(Annotation::TypoSuggestion), - alloc.reflow(" instead?"), - ]), - ]) - }, - ]); - - Report { - filename, - title: "TYPE MISMATCH".to_string(), - doc, - severity: Severity::RuntimeError, - } -} - -fn exhaustive_problem<'a>( - alloc: &'a RocDocAllocator<'a>, - lines: &LineInfo, - filename: PathBuf, - problem: roc_exhaustive::Error, -) -> Report<'a> { - use roc_exhaustive::Context::*; - use roc_exhaustive::Error::*; - - match problem { - Incomplete(region, context, missing) => match context { - BadArg => { - let doc = alloc.stack([ - alloc.reflow("This pattern does not cover all the possibilities:"), - alloc.region(lines.convert_region(region)), - alloc.reflow("Other possibilities include:"), - unhandled_patterns_to_doc_block(alloc, missing), - alloc.concat([ - alloc.reflow( - "I would have to crash if I saw one of those! \ - So rather than pattern matching in function arguments, put a ", - ), - alloc.keyword("when"), - alloc.reflow(" in the function body to account for all possibilities."), - ]), - ]); - - Report { - filename, - title: "UNSAFE PATTERN".to_string(), - doc, - severity: Severity::RuntimeError, - } - } - BadDestruct => { - let doc = alloc.stack([ - alloc.reflow("This pattern does not cover all the possibilities:"), - alloc.region(lines.convert_region(region)), - alloc.reflow("Other possibilities include:"), - unhandled_patterns_to_doc_block(alloc, missing), - alloc.concat([ - alloc.reflow( - "I would have to crash if I saw one of those! \ - You can use a binding to deconstruct a value if there is only ONE possibility. \ - Use a " - ), - alloc.keyword("when"), - alloc.reflow(" to account for all possibilities."), - ]), - ]); - - Report { - filename, - title: "UNSAFE PATTERN".to_string(), - doc, - severity: Severity::RuntimeError, - } - } - BadCase => { - let doc = alloc.stack([ - alloc.concat([ - alloc.reflow("This "), - alloc.keyword("when"), - alloc.reflow(" does not cover all the possibilities:"), - ]), - alloc.region(lines.convert_region(region)), - alloc.reflow("Other possibilities include:"), - unhandled_patterns_to_doc_block(alloc, missing), - alloc.reflow( - "I would have to crash if I saw one of those! \ - Add branches for them!", - ), - // alloc.hint().append(alloc.reflow("or use a hole.")), - ]); - - Report { - filename, - title: "UNSAFE PATTERN".to_string(), - doc, - severity: Severity::RuntimeError, - } - } - }, - Redundant { - overall_region, - branch_region, - index, - } => { - let doc = alloc.stack([ - alloc.concat([ - alloc.reflow("The "), - alloc.string(index.ordinal()), - alloc.reflow(" pattern is redundant:"), - ]), - alloc.region_with_subregion( - lines.convert_region(overall_region), - lines.convert_region(branch_region), - ), - alloc.reflow( - "Any value of this shape will be handled by \ - a previous pattern, so this one should be removed.", - ), - ]); - - Report { - filename, - title: "REDUNDANT PATTERN".to_string(), - doc, - severity: Severity::Warning, - } - } - } -} - -pub fn unhandled_patterns_to_doc_block<'b>( - alloc: &'b RocDocAllocator<'b>, - patterns: Vec, -) -> RocDocBuilder<'b> { - alloc - .vcat( - patterns - .into_iter() - .map(|v| exhaustive_pattern_to_doc(alloc, v)), - ) - .indent(4) - .annotate(Annotation::TypeBlock) -} - -fn exhaustive_pattern_to_doc<'b>( - alloc: &'b RocDocAllocator<'b>, - pattern: roc_exhaustive::Pattern, -) -> RocDocBuilder<'b> { - pattern_to_doc_help(alloc, pattern, false) -} - -const AFTER_TAG_INDENT: &str = " "; - -fn pattern_to_doc_help<'b>( - alloc: &'b RocDocAllocator<'b>, - pattern: roc_exhaustive::Pattern, - in_type_param: bool, -) -> RocDocBuilder<'b> { - use roc_can::exhaustive::{GUARD_CTOR, NONEXHAUSIVE_CTOR}; - use roc_exhaustive::Literal::*; - use roc_exhaustive::Pattern::*; - use roc_exhaustive::RenderAs; - - match pattern { - Anything => alloc.text("_"), - Literal(l) => match l { - Int(i) => alloc.text(i128::from_ne_bytes(i).to_string()), - U128(i) => alloc.text(u128::from_ne_bytes(i).to_string()), - Bit(true) => alloc.text("True"), - Bit(false) => alloc.text("False"), - Byte(b) => alloc.text(b.to_string()), - Float(f) => alloc.text(f.to_string()), - Decimal(d) => alloc.text(RocDec::from_ne_bytes(d).to_string()), - Str(s) => alloc.string(s.into()), - }, - Ctor(union, tag_id, args) => { - match union.render_as { - RenderAs::Guard => { - // #Guard - debug_assert!(union.alternatives[tag_id.0 as usize] - .name - .is_tag(&TagName(GUARD_CTOR.into()))); - debug_assert!(args.len() == 2); - let tag = pattern_to_doc_help(alloc, args[1].clone(), in_type_param); - alloc.concat([ - tag, - alloc.text(AFTER_TAG_INDENT), - alloc.text("(note the lack of an "), - alloc.keyword("if"), - alloc.text(" clause)"), - ]) - } - RenderAs::Record(field_names) => { - let mut arg_docs = Vec::with_capacity(args.len()); - - for (label, v) in field_names.into_iter().zip(args.into_iter()) { - match &v { - Anything => { - arg_docs.push(alloc.text(label.to_string())); - } - Literal(_) | Ctor(_, _, _) => { - arg_docs.push( - alloc - .text(label.to_string()) - .append(alloc.reflow(": ")) - .append(pattern_to_doc_help(alloc, v, false)), - ); - } - } - } - - alloc - .text("{ ") - .append(alloc.intersperse(arg_docs, alloc.reflow(", "))) - .append(" }") - } - RenderAs::Tag | RenderAs::Opaque => { - let ctor = &union.alternatives[tag_id.0 as usize]; - match &ctor.name { - CtorName::Tag(TagName(name)) if name.as_str() == NONEXHAUSIVE_CTOR => { - return pattern_to_doc_help( - alloc, - roc_exhaustive::Pattern::Anything, - in_type_param, - ) - } - _ => {} - } - - let tag_name = match (union.render_as, &ctor.name) { - (RenderAs::Tag, CtorName::Tag(tag)) => alloc.tag_name(tag.clone()), - (RenderAs::Opaque, CtorName::Opaque(opaque)) => { - alloc.wrapped_opaque_name(*opaque) - } - _ => unreachable!(), - }; - - let has_args = !args.is_empty(); - let arg_docs = args - .into_iter() - .map(|v| pattern_to_doc_help(alloc, v, true)); - - // We assume the alternatives are sorted. If not, this assert will trigger - debug_assert!(tag_id == ctor.tag_id); - - let docs = std::iter::once(tag_name).chain(arg_docs); - - if in_type_param && has_args { - alloc - .text("(") - .append(alloc.intersperse(docs, alloc.space())) - .append(")") - } else { - alloc.intersperse(docs, alloc.space()) - } - } - } - } - } -} diff --git a/reporting/src/lib.rs b/reporting/src/lib.rs deleted file mode 100644 index ff2791a5f8..0000000000 --- a/reporting/src/lib.rs +++ /dev/null @@ -1,6 +0,0 @@ -#![warn(clippy::dbg_macro)] -// See github.com/rtfeldman/roc/issues/800 for discussion of the large_enum_variant check. -#![allow(clippy::large_enum_variant)] - -pub mod error; -pub mod report; diff --git a/reporting/src/report.rs b/reporting/src/report.rs deleted file mode 100644 index 010c900f29..0000000000 --- a/reporting/src/report.rs +++ /dev/null @@ -1,1046 +0,0 @@ -use roc_module::ident::Ident; -use roc_module::ident::{Lowercase, ModuleName, TagName, Uppercase}; -use roc_module::symbol::{Interns, ModuleId, Symbol}; -use roc_region::all::LineColumnRegion; -use std::fmt; -use std::path::{Path, PathBuf}; -use ven_pretty::{BoxAllocator, DocAllocator, DocBuilder, Render, RenderAnnotated}; - -pub use crate::error::canonicalize::can_problem; -pub use crate::error::parse::parse_problem; -pub use crate::error::r#type::type_problem; - -#[cfg(windows)] -const CYCLE_ELEMENTS: [&str; 4] = ["+-----+", "| ", "| |", "+-<---+"]; - -#[cfg(not(windows))] -const CYCLE_ELEMENTS: [&str; 4] = ["┌─────┐", "│ ", "│ ↓", "└─────┘"]; - -const CYCLE_TOP: &str = CYCLE_ELEMENTS[0]; -const CYCLE_LN: &str = CYCLE_ELEMENTS[1]; -const CYCLE_MID: &str = CYCLE_ELEMENTS[2]; -const CYCLE_END: &str = CYCLE_ELEMENTS[3]; - -const GUTTER_BAR: &str = "│"; -const ERROR_UNDERLINE: &str = "^"; - -/// The number of monospace spaces the gutter bar takes up. -/// (This is not necessarily the same as GUTTER_BAR.len()!) -const GUTTER_BAR_WIDTH: usize = 1; - -pub fn cycle<'b>( - alloc: &'b RocDocAllocator<'b>, - indent: usize, - name: RocDocBuilder<'b>, - names: Vec>, -) -> RocDocBuilder<'b> { - let mut lines = Vec::with_capacity(4 + (2 * names.len() - 1)); - - lines.push(alloc.text(CYCLE_TOP)); - - lines.push(alloc.text(CYCLE_LN).append(name)); - lines.push(alloc.text(CYCLE_MID)); - - let mut it = names.into_iter().peekable(); - - while let Some(other_name) = it.next() { - lines.push(alloc.text(CYCLE_LN).append(other_name)); - - if it.peek().is_some() { - lines.push(alloc.text(CYCLE_MID)); - } - } - - lines.push(alloc.text(CYCLE_END)); - - alloc - .vcat(lines) - .indent(indent) - .annotate(Annotation::TypeBlock) -} - -const HEADER_WIDTH: usize = 80; - -pub fn pretty_header(title: &str) -> String { - let title_width = title.len() + 4; - let header = format!("── {} {}", title, "─".repeat(HEADER_WIDTH - title_width)); - header -} - -pub fn pretty_header_with_path(title: &str, path: &Path) -> String { - let cwd = std::env::current_dir().unwrap(); - let relative_path = match path.strip_prefix(cwd) { - Ok(p) => p, - _ => path, - } - .to_str() - .unwrap(); - - let title_width = title.len() + 4; - let relative_path_width = relative_path.len() + 3; - let available_path_width = HEADER_WIDTH - title_width - 1; - - // If path is too long to fit in 80 characters with everything else then truncate it - let path_width = relative_path_width.min(available_path_width); - let path_trim = relative_path_width - path_width; - let path = if path_trim > 0 { - format!("...{}", &relative_path[(path_trim + 3)..]) - } else { - relative_path.to_string() - }; - - let header = format!( - "── {} {} {} ─", - title, - "─".repeat(HEADER_WIDTH - (title_width + path_width)), - path - ); - - header -} - -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -pub enum Severity { - /// This will cause a runtime error if some code get srun - /// (e.g. type mismatch, naming error) - RuntimeError, - - /// This will never cause the code to misbehave, - /// but should be cleaned up - /// (e.g. unused def, unused import) - Warning, -} - -#[derive(Clone, Copy, Debug)] -pub enum RenderTarget { - ColorTerminal, - Generic, -} - -/// A textual report. -pub struct Report<'b> { - pub title: String, - pub filename: PathBuf, - pub doc: RocDocBuilder<'b>, - pub severity: Severity, -} - -impl<'b> Report<'b> { - pub fn render( - self, - target: RenderTarget, - buf: &'b mut String, - alloc: &'b RocDocAllocator<'b>, - palette: &'b Palette, - ) { - match target { - RenderTarget::Generic => self.render_ci(buf, alloc), - RenderTarget::ColorTerminal => self.render_color_terminal(buf, alloc, palette), - } - } - - /// Render to CI console output, where no colors are available. - pub fn render_ci(self, buf: &'b mut String, alloc: &'b RocDocAllocator<'b>) { - let err_msg = ""; - - self.pretty(alloc) - .1 - .render_raw(70, &mut CiWrite::new(buf)) - .expect(err_msg); - } - - /// Render to a color terminal using ANSI escape sequences - pub fn render_color_terminal( - self, - buf: &mut String, - alloc: &'b RocDocAllocator<'b>, - palette: &'b Palette, - ) { - let err_msg = ""; - - self.pretty(alloc) - .1 - .render_raw(70, &mut ColorWrite::new(palette, buf)) - .expect(err_msg); - } - - pub fn pretty(self, alloc: &'b RocDocAllocator<'b>) -> RocDocBuilder<'b> { - if self.title.is_empty() { - self.doc - } else { - let header = if self.filename == PathBuf::from("") { - crate::report::pretty_header(&self.title) - } else { - crate::report::pretty_header_with_path(&self.title, &self.filename) - }; - - alloc.stack([alloc.text(header).annotate(Annotation::Header), self.doc]) - } - } - - pub fn horizontal_rule(palette: &'b Palette) -> String { - format!("{}{}", palette.header, "─".repeat(80)) - } -} - -/// This struct is a combination of several things -/// 1. A set of StyleCodes suitable for the platform we're running on (web or terminal) -/// 2. A set of colors we decided to use -/// 3. A mapping from UI elements to the styles we use for them -/// Note: This should really be called Theme! Usually a "palette" is just (2). -pub struct Palette { - pub primary: &'static str, - pub code_block: &'static str, - pub keyword: &'static str, - pub variable: &'static str, - pub type_variable: &'static str, - pub structure: &'static str, - pub alias: &'static str, - pub opaque: &'static str, - pub error: &'static str, - pub line_number: &'static str, - pub header: &'static str, - pub gutter_bar: &'static str, - pub module_name: &'static str, - pub binop: &'static str, - pub typo: &'static str, - pub typo_suggestion: &'static str, - pub parser_suggestion: &'static str, - pub bold: &'static str, - pub underline: &'static str, - pub reset: &'static str, -} - -/// Set the default styles for various semantic elements, -/// given a set of StyleCodes for a platform (web or terminal). -const fn default_palette_from_style_codes(codes: StyleCodes) -> Palette { - Palette { - primary: codes.white, - code_block: codes.white, - keyword: codes.green, - variable: codes.blue, - type_variable: codes.yellow, - structure: codes.green, - alias: codes.yellow, - opaque: codes.yellow, - error: codes.red, - line_number: codes.cyan, - header: codes.cyan, - gutter_bar: codes.cyan, - module_name: codes.green, - binop: codes.green, - typo: codes.yellow, - typo_suggestion: codes.yellow, - parser_suggestion: codes.yellow, - bold: codes.bold, - underline: codes.underline, - reset: codes.reset, - } -} - -pub const DEFAULT_PALETTE: Palette = default_palette_from_style_codes(ANSI_STYLE_CODES); - -pub const DEFAULT_PALETTE_HTML: Palette = default_palette_from_style_codes(HTML_STYLE_CODES); - -/// A machine-readable format for text styles (colors and other styles) -pub struct StyleCodes { - pub red: &'static str, - pub green: &'static str, - pub yellow: &'static str, - pub blue: &'static str, - pub magenta: &'static str, - pub cyan: &'static str, - pub white: &'static str, - pub bold: &'static str, - pub underline: &'static str, - pub reset: &'static str, - pub color_reset: &'static str, -} - -pub const ANSI_STYLE_CODES: StyleCodes = StyleCodes { - red: "\u{001b}[31m", - green: "\u{001b}[32m", - yellow: "\u{001b}[33m", - blue: "\u{001b}[34m", - magenta: "\u{001b}[35m", - cyan: "\u{001b}[36m", - white: "\u{001b}[37m", - bold: "\u{001b}[1m", - underline: "\u{001b}[4m", - reset: "\u{001b}[0m", - color_reset: "\u{1b}[39m", -}; - -macro_rules! html_color { - ($name: expr) => { - concat!("") - }; -} - -pub const HTML_STYLE_CODES: StyleCodes = StyleCodes { - red: html_color!("red"), - green: html_color!("green"), - yellow: html_color!("yellow"), - blue: html_color!("blue"), - magenta: html_color!("magenta"), - cyan: html_color!("cyan"), - white: html_color!("white"), - bold: "", - underline: "", - reset: "", - color_reset: "", -}; - -// define custom allocator struct so we can `impl RocDocAllocator` custom helpers -pub struct RocDocAllocator<'a> { - upstream: BoxAllocator, - pub src_lines: &'a [&'a str], - pub home: ModuleId, - pub interns: &'a Interns, -} - -pub type RocDocBuilder<'b> = DocBuilder<'b, RocDocAllocator<'b>, Annotation>; - -impl<'a, A> DocAllocator<'a, A> for RocDocAllocator<'a> -where - A: 'a, -{ - type Doc = ven_pretty::BoxDoc<'a, A>; - - fn alloc(&'a self, doc: ven_pretty::Doc<'a, Self::Doc, A>) -> Self::Doc { - self.upstream.alloc(doc) - } - - fn alloc_column_fn( - &'a self, - f: impl Fn(usize) -> Self::Doc + 'a, - ) -> >::ColumnFn { - self.upstream.alloc_column_fn(f) - } - - fn alloc_width_fn( - &'a self, - f: impl Fn(isize) -> Self::Doc + 'a, - ) -> >::WidthFn { - self.upstream.alloc_width_fn(f) - } -} - -impl<'a> RocDocAllocator<'a> { - pub fn new(src_lines: &'a [&'a str], home: ModuleId, interns: &'a Interns) -> Self { - RocDocAllocator { - upstream: BoxAllocator, - home, - src_lines, - interns, - } - } - - /// vertical concatenation. Adds a newline between elements - pub fn vcat(&'a self, docs: I) -> DocBuilder<'a, Self, A> - where - A: 'a + Clone, - I: IntoIterator, - I::Item: Into, A>>, - { - self.intersperse(docs, self.line()) - } - - /// like vcat, but adds a double line break between elements. Visually this means an empty line - /// between elements. - pub fn stack(&'a self, docs: I) -> DocBuilder<'a, Self, A> - where - A: 'a + Clone, - I: IntoIterator, - I::Item: Into, A>>, - { - self.intersperse(docs, self.line().append(self.line())) - } - - /// text from a String. Note that this does not reflow! - pub fn string(&'a self, string: String) -> DocBuilder<'a, Self, Annotation> { - let x: std::borrow::Cow<'a, str> = string.into(); - - self.text(x) - } - - pub fn keyword(&'a self, string: &'a str) -> DocBuilder<'a, Self, Annotation> { - self.text(string).annotate(Annotation::Keyword) - } - - pub fn parser_suggestion(&'a self, string: &'a str) -> DocBuilder<'a, Self, Annotation> { - self.text(string).annotate(Annotation::ParserSuggestion) - } - - pub fn type_str(&'a self, content: &str) -> DocBuilder<'a, Self, Annotation> { - self.string(content.to_owned()).annotate(Annotation::Alias) - } - - pub fn type_variable(&'a self, content: Lowercase) -> DocBuilder<'a, Self, Annotation> { - // currently not annotated - self.string(content.to_string()) - .annotate(Annotation::TypeVariable) - } - - pub fn tag_name(&'a self, tn: TagName) -> DocBuilder<'a, Self, Annotation> { - self.tag(tn.0) - } - - pub fn symbol_unqualified(&'a self, symbol: Symbol) -> DocBuilder<'a, Self, Annotation> { - self.text(symbol.as_str(self.interns)) - .annotate(Annotation::Symbol) - } - pub fn symbol_foreign_qualified(&'a self, symbol: Symbol) -> DocBuilder<'a, Self, Annotation> { - if symbol.module_id() == self.home || symbol.module_id().is_builtin() { - // Render it unqualified if it's in the current module or a builtin - self.text(symbol.as_str(self.interns)) - .annotate(Annotation::Symbol) - } else { - self.text(format!( - "{}.{}", - symbol.module_string(self.interns), - symbol.as_str(self.interns), - )) - .annotate(Annotation::Symbol) - } - } - pub fn symbol_qualified(&'a self, symbol: Symbol) -> DocBuilder<'a, Self, Annotation> { - self.text(format!( - "{}.{}", - symbol.module_string(self.interns), - symbol.as_str(self.interns), - )) - .annotate(Annotation::Symbol) - } - - /// TODO: remove in favor of tag_name - pub fn tag(&'a self, uppercase: Uppercase) -> DocBuilder<'a, Self, Annotation> { - self.text(format!("{}", uppercase)) - .annotate(Annotation::Tag) - } - - pub fn opaque_name(&'a self, opaque: Symbol) -> DocBuilder<'a, Self, Annotation> { - let fmt = if opaque.module_id() == self.home { - // Render it unqualified if it's in the current module. - opaque.as_str(self.interns).to_string() - } else { - format!( - "{}.{}", - opaque.module_string(self.interns), - opaque.as_str(self.interns), - ) - }; - - self.text(fmt).annotate(Annotation::Opaque) - } - - pub fn wrapped_opaque_name(&'a self, opaque: Symbol) -> DocBuilder<'a, Self, Annotation> { - debug_assert_eq!(opaque.module_id(), self.home, "Opaque wrappings can only be defined in the same module they're defined in, but this one is defined elsewhere: {:?}", opaque); - - self.text(format!("@{}", opaque.as_str(self.interns))) - .annotate(Annotation::Opaque) - } - - pub fn record_field(&'a self, lowercase: Lowercase) -> DocBuilder<'a, Self, Annotation> { - self.text(format!(".{}", lowercase)) - .annotate(Annotation::RecordField) - } - - pub fn module(&'a self, module_id: ModuleId) -> DocBuilder<'a, Self, Annotation> { - let name = self.interns.module_name(module_id); - let name = if name.is_empty() { - // Render the app module as "app" - "app".to_string() - } else { - name.to_string() - }; - - self.text(name).annotate(Annotation::Module) - } - - pub fn module_name(&'a self, name: ModuleName) -> DocBuilder<'a, Self, Annotation> { - let name = if name.is_empty() { - // Render the app module as "app" - "app".to_string() - } else { - name.as_str().to_string() - }; - - self.text(name).annotate(Annotation::Module) - } - - pub fn binop( - &'a self, - content: roc_module::called_via::BinOp, - ) -> DocBuilder<'a, Self, Annotation> { - self.text(content.to_string()).annotate(Annotation::BinOp) - } - - /// Turns off backticks/colors in a block - pub fn type_block( - &'a self, - content: DocBuilder<'a, Self, Annotation>, - ) -> DocBuilder<'a, Self, Annotation> { - content.annotate(Annotation::TypeBlock).indent(4) - } - - /// Turns off backticks/colors in a block - pub fn inline_type_block( - &'a self, - content: DocBuilder<'a, Self, Annotation>, - ) -> DocBuilder<'a, Self, Annotation> { - content.annotate(Annotation::InlineTypeBlock) - } - - pub fn tip(&'a self) -> DocBuilder<'a, Self, Annotation> { - self.text("Tip") - .annotate(Annotation::Tip) - .append(":") - .append(self.softline()) - } - - pub fn note(&'a self, line: &'a str) -> DocBuilder<'a, Self, Annotation> { - self.text("Note") - .annotate(Annotation::Tip) - .append(": ") - .append(line) - } - - pub fn hint(&'a self, line: &'a str) -> DocBuilder<'a, Self, Annotation> { - self.text("Hint") - .annotate(Annotation::Tip) - .append(": ") - .append(line) - } - - pub fn region_all_the_things( - &'a self, - region: LineColumnRegion, - sub_region1: LineColumnRegion, - sub_region2: LineColumnRegion, - error_annotation: Annotation, - ) -> DocBuilder<'a, Self, Annotation> { - debug_assert!(region.contains(&sub_region1)); - debug_assert!(region.contains(&sub_region2)); - - // if true, the final line of the snippet will be some ^^^ that point to the region where - // the problem is. Otherwise, the snippet will have a > on the lines that are in the region - // where the problem is. - let error_highlight_line = region.start().line == region.end().line; - - let max_line_number_length = (region.end().line + 1).to_string().len(); - let indent = 2; - - let mut result = self.nil(); - for i in region.start().line..=region.end().line { - let line_number_string = (i + 1).to_string(); - let line_number = line_number_string; - let this_line_number_length = line_number.len(); - - let line = self.src_lines[i as usize]; - - let rest_of_line = if !line.trim().is_empty() { - self.text(line).indent(indent) - } else { - self.nil() - }; - - let highlight = !error_highlight_line - && ((i >= sub_region1.start().line && i <= sub_region1.end().line) - || (i >= sub_region2.start().line && i <= sub_region2.end().line)); - - let source_line = if highlight { - self.text(" ".repeat(max_line_number_length - this_line_number_length)) - .append(self.text(line_number).annotate(Annotation::LineNumber)) - .append(self.text(GUTTER_BAR).annotate(Annotation::GutterBar)) - .append(self.text(">").annotate(error_annotation)) - .append(rest_of_line) - } else if error_highlight_line { - self.text(" ".repeat(max_line_number_length - this_line_number_length)) - .append(self.text(line_number).annotate(Annotation::LineNumber)) - .append(self.text(GUTTER_BAR).annotate(Annotation::GutterBar)) - .append(rest_of_line) - } else { - self.text(" ".repeat(max_line_number_length - this_line_number_length)) - .append(self.text(line_number).annotate(Annotation::LineNumber)) - .append(self.text(GUTTER_BAR).annotate(Annotation::GutterBar)) - .append(self.text(" ")) - .append(rest_of_line) - }; - - result = result.append(source_line); - - if i != region.end().line { - result = result.append(self.line()) - } - } - - if error_highlight_line { - let overlapping = sub_region2.start().column < sub_region1.end().column; - - let highlight = if overlapping { - self.text( - ERROR_UNDERLINE - .repeat((sub_region2.end().column - sub_region1.start().column) as usize), - ) - } else { - let highlight1 = ERROR_UNDERLINE - .repeat((sub_region1.end().column - sub_region1.start().column) as usize); - let highlight2 = if sub_region1 == sub_region2 { - "".repeat(0) - } else { - ERROR_UNDERLINE - .repeat((sub_region2.end().column - sub_region2.start().column) as usize) - }; - let in_between = " ".repeat( - (sub_region2 - .start() - .column - .saturating_sub(sub_region1.end().column)) as usize, - ); - - self.text(highlight1) - .append(self.text(in_between)) - .append(self.text(highlight2)) - }; - - let highlight_line = self - .line() - // Omit the gutter bar when we know there are no further - // line numbers to be printed after this! - .append(self.text(" ".repeat(max_line_number_length + GUTTER_BAR_WIDTH))) - .append(if sub_region1.is_empty() && sub_region2.is_empty() { - self.nil() - } else { - self.text(" ".repeat(sub_region1.start().column as usize)) - .indent(indent) - .append(highlight) - .annotate(error_annotation) - }); - - result = result.append(highlight_line); - } - - result.annotate(Annotation::CodeBlock) - } - - pub fn region_with_subregion( - &'a self, - region: LineColumnRegion, - sub_region: LineColumnRegion, - ) -> DocBuilder<'a, Self, Annotation> { - // debug_assert!(region.contains(&sub_region)); - - // If the outer region takes more than 1 full screen (~60 lines), only show the inner region - if region.end().line.saturating_sub(region.start().line) > 60 { - return self.region_with_subregion(sub_region, sub_region); - } - - // if true, the final line of the snippet will be some ^^^ that point to the region where - // the problem is. Otherwise, the snippet will have a > on the lines that are in the region - // where the problem is. - let error_highlight_line = sub_region.start().line == region.end().line; - - let max_line_number_length = (region.end().line + 1).to_string().len(); - let indent = 2; - - let mut result = self.nil(); - for i in region.start().line..=region.end().line { - let line_number_string = (i + 1).to_string(); - let line_number = line_number_string; - let this_line_number_length = line_number.len(); - - let line = self.src_lines[i as usize]; - - let rest_of_line = if !line.trim().is_empty() { - self.text(line) - .annotate(Annotation::CodeBlock) - .indent(indent) - } else { - self.nil() - }; - - let source_line = if !error_highlight_line - && i >= sub_region.start().line - && i <= sub_region.end().line - { - self.text(" ".repeat(max_line_number_length - this_line_number_length)) - .append(self.text(line_number).annotate(Annotation::LineNumber)) - .append(self.text(GUTTER_BAR).annotate(Annotation::GutterBar)) - .append(self.text(">").annotate(Annotation::Error)) - .append(rest_of_line) - } else if error_highlight_line { - self.text(" ".repeat(max_line_number_length - this_line_number_length)) - .append(self.text(line_number).annotate(Annotation::LineNumber)) - .append(self.text(GUTTER_BAR).annotate(Annotation::GutterBar)) - .append(rest_of_line) - } else { - self.text(" ".repeat(max_line_number_length - this_line_number_length)) - .append(self.text(line_number).annotate(Annotation::LineNumber)) - .append(self.text(GUTTER_BAR).annotate(Annotation::GutterBar)) - .append(self.text(" ")) - .append(rest_of_line) - }; - - result = result.append(source_line); - - if i != region.end().line { - result = result.append(self.line()) - } - } - - if error_highlight_line { - let highlight_text = ERROR_UNDERLINE - .repeat((sub_region.end().column - sub_region.start().column) as usize); - - let highlight_line = self - .line() - // Omit the gutter bar when we know there are no further - // line numbers to be printed after this! - .append(self.text(" ".repeat(max_line_number_length + GUTTER_BAR_WIDTH))) - .append(if highlight_text.is_empty() { - self.nil() - } else { - self.text(" ".repeat(sub_region.start().column as usize)) - .indent(indent) - .append(self.text(highlight_text).annotate(Annotation::Error)) - }); - - result = result.append(highlight_line); - } - - result - } - - pub fn region(&'a self, region: LineColumnRegion) -> DocBuilder<'a, Self, Annotation> { - self.region_with_subregion(region, region) - } - - pub fn region_without_error( - &'a self, - region: LineColumnRegion, - ) -> DocBuilder<'a, Self, Annotation> { - let mut result = self.nil(); - for i in region.start().line..=region.end().line { - let line = if i == region.start().line { - if i == region.end().line { - &self.src_lines[i as usize] - [region.start().column as usize..region.end().column as usize] - } else { - &self.src_lines[i as usize][region.start().column as usize..] - } - } else if i == region.end().line { - &self.src_lines[i as usize][0..region.end().column as usize] - } else { - self.src_lines[i as usize] - }; - - let rest_of_line = if !line.trim().is_empty() { - self.text(line).annotate(Annotation::CodeBlock) - } else { - self.nil() - }; - - result = result.append(rest_of_line); - - if i != region.end().line { - result = result.append(self.line()) - } - } - - result.indent(4) - } - - pub fn ident(&'a self, ident: Ident) -> DocBuilder<'a, Self, Annotation> { - self.text(format!("{}", ident.as_inline_str())) - .annotate(Annotation::Symbol) - } - - pub fn int_literal(&'a self, int: I) -> DocBuilder<'a, Self, Annotation> - where - I: ToString, - { - let s = int.to_string(); - - let is_negative = s.starts_with('-'); - - if s.len() < 7 + (is_negative as usize) { - // If the number is not at least in the millions, return it as-is. - return self.text(s); - } - - // Otherwise, let's add numeric separators to make it easier to read. - let mut result = String::with_capacity(s.len() + s.len() / 3); - for (idx, c) in s - .get((is_negative as usize)..) - .unwrap() - .chars() - .rev() - .enumerate() - { - if idx != 0 && idx % 3 == 0 { - result.push('_'); - } - result.push(c); - } - if is_negative { - result.push('-'); - } - self.text(result.chars().rev().collect::()) - } -} - -#[derive(Copy, Clone, Debug)] -pub enum Annotation { - Emphasized, - Url, - Keyword, - Tag, - RecordField, - TypeVariable, - Alias, - Opaque, - Structure, - Symbol, - BinOp, - Error, - GutterBar, - LineNumber, - PlainText, - CodeBlock, - TypeBlock, - InlineTypeBlock, - Module, - Typo, - TypoSuggestion, - Tip, - Header, - ParserSuggestion, -} - -/// Render with minimal formatting -pub struct CiWrite { - style_stack: Vec, - in_type_block: bool, - in_code_block: bool, - upstream: W, -} - -impl CiWrite { - pub fn new(upstream: W) -> CiWrite { - CiWrite { - style_stack: vec![], - in_type_block: false, - in_code_block: false, - upstream, - } - } -} - -/// Render with fancy formatting -pub struct ColorWrite<'a, W> { - style_stack: Vec, - palette: &'a Palette, - upstream: W, -} - -impl<'a, W> ColorWrite<'a, W> { - pub fn new(palette: &'a Palette, upstream: W) -> ColorWrite<'a, W> { - ColorWrite { - style_stack: vec![], - palette, - upstream, - } - } -} - -impl Render for CiWrite -where - W: fmt::Write, -{ - type Error = fmt::Error; - - fn write_str(&mut self, s: &str) -> Result { - self.write_str_all(s).map(|_| s.len()) - } - - fn write_str_all(&mut self, s: &str) -> fmt::Result { - self.upstream.write_str(s) - } -} - -impl RenderAnnotated for CiWrite -where - W: fmt::Write, -{ - fn push_annotation(&mut self, annotation: &Annotation) -> Result<(), Self::Error> { - use Annotation::*; - match annotation { - TypeBlock => { - self.in_type_block = true; - } - InlineTypeBlock => { - debug_assert!(!self.in_type_block); - self.write_str("`")?; - self.in_type_block = true; - } - CodeBlock => { - self.in_code_block = true; - } - Emphasized => { - self.write_str("*")?; - } - Url => { - self.write_str("<")?; - } - Tag | Keyword | RecordField | Symbol | Typo | TypoSuggestion | TypeVariable - if !self.in_type_block && !self.in_code_block => - { - self.write_str("`")?; - } - - _ => {} - } - self.style_stack.push(*annotation); - Ok(()) - } - - fn pop_annotation(&mut self) -> Result<(), Self::Error> { - use Annotation::*; - - match self.style_stack.pop() { - None => {} - Some(annotation) => match annotation { - TypeBlock => { - self.in_type_block = false; - } - InlineTypeBlock => { - debug_assert!(self.in_type_block); - self.write_str("`")?; - self.in_type_block = false; - } - CodeBlock => { - self.in_code_block = false; - } - Emphasized => { - self.write_str("*")?; - } - Url => { - self.write_str(">")?; - } - Tag | Keyword | RecordField | Symbol | Typo | TypoSuggestion | TypeVariable - if !self.in_type_block && !self.in_code_block => - { - self.write_str("`")?; - } - - _ => {} - }, - } - Ok(()) - } -} - -impl<'a, W> Render for ColorWrite<'a, W> -where - W: fmt::Write, -{ - type Error = fmt::Error; - - fn write_str(&mut self, s: &str) -> Result { - self.write_str_all(s).map(|_| s.len()) - } - - fn write_str_all(&mut self, s: &str) -> fmt::Result { - self.upstream.write_str(s) - } -} - -impl<'a, W> RenderAnnotated for ColorWrite<'a, W> -where - W: fmt::Write, -{ - fn push_annotation(&mut self, annotation: &Annotation) -> Result<(), Self::Error> { - use Annotation::*; - match annotation { - Emphasized => { - self.write_str(self.palette.bold)?; - } - Url | Tip => { - self.write_str(self.palette.underline)?; - } - PlainText => { - self.write_str(self.palette.primary)?; - } - CodeBlock => { - self.write_str(self.palette.code_block)?; - } - TypeVariable => { - self.write_str(self.palette.type_variable)?; - } - Alias => { - self.write_str(self.palette.alias)?; - } - Opaque => { - self.write_str(self.palette.alias)?; - } - BinOp => { - self.write_str(self.palette.alias)?; - } - Symbol => { - self.write_str(self.palette.variable)?; - } - Keyword => { - self.write_str(self.palette.keyword)?; - } - GutterBar => { - self.write_str(self.palette.gutter_bar)?; - } - Error => { - self.write_str(self.palette.error)?; - } - Header => { - self.write_str(self.palette.header)?; - } - LineNumber => { - self.write_str(self.palette.line_number)?; - } - Structure => { - self.write_str(self.palette.structure)?; - } - Module => { - self.write_str(self.palette.module_name)?; - } - Typo => { - self.write_str(self.palette.typo)?; - } - TypoSuggestion => { - self.write_str(self.palette.typo_suggestion)?; - } - ParserSuggestion => { - self.write_str(self.palette.parser_suggestion)?; - } - TypeBlock | InlineTypeBlock | Tag | RecordField => { /* nothing yet */ } - } - self.style_stack.push(*annotation); - Ok(()) - } - - fn pop_annotation(&mut self) -> Result<(), Self::Error> { - use Annotation::*; - - match self.style_stack.pop() { - None => {} - Some(annotation) => match annotation { - Emphasized | Url | TypeVariable | Alias | Symbol | BinOp | Error | GutterBar - | Typo | TypoSuggestion | ParserSuggestion | Structure | CodeBlock | PlainText - | LineNumber | Tip | Module | Header | Keyword => { - self.write_str(self.palette.reset)?; - } - - TypeBlock | InlineTypeBlock | Tag | Opaque | RecordField => { /* nothing yet */ } - }, - } - Ok(()) - } -} diff --git a/reporting/tests/helpers/mod.rs b/reporting/tests/helpers/mod.rs deleted file mode 100644 index cf87f0d8a6..0000000000 --- a/reporting/tests/helpers/mod.rs +++ /dev/null @@ -1,299 +0,0 @@ -extern crate bumpalo; - -use self::bumpalo::Bump; -use roc_can::abilities::AbilitiesStore; -use roc_can::constraint::{Constraint, Constraints}; -use roc_can::env::Env; -use roc_can::expected::Expected; -use roc_can::expr::{canonicalize_expr, Expr, Output, PendingDerives}; -use roc_can::operator; -use roc_can::scope::Scope; -use roc_collections::all::{ImMap, MutMap, SendSet}; -use roc_constrain::expr::constrain_expr; -use roc_constrain::module::introduce_builtin_imports; -use roc_module::symbol::{IdentIds, Interns, ModuleId, ModuleIds}; -use roc_parse::parser::{SourceError, SyntaxError}; -use roc_problem::can::Problem; -use roc_region::all::Loc; -use roc_solve::solve::{self, Aliases}; -use roc_types::subs::{Content, Subs, VarStore, Variable}; -use roc_types::types::{AliasVar, Type}; -use std::hash::Hash; -use std::path::{Path, PathBuf}; - -pub fn test_home() -> ModuleId { - ModuleIds::default().get_or_insert(&"Test".into()) -} - -#[allow(dead_code)] -#[allow(clippy::too_many_arguments)] -pub fn infer_expr( - subs: Subs, - problems: &mut Vec, - constraints: &Constraints, - constraint: &Constraint, - pending_derives: PendingDerives, - aliases: &mut Aliases, - abilities_store: &mut AbilitiesStore, - expr_var: Variable, -) -> (Content, Subs) { - let (solved, _) = solve::run( - constraints, - problems, - subs, - aliases, - constraint, - pending_derives, - abilities_store, - ); - - let content = *solved.inner().get_content_without_compacting(expr_var); - - (content, solved.into_inner()) -} - -/// Used in the with_larger_debug_stack() function, for tests that otherwise -/// run out of stack space in debug builds (but don't in --release builds) -#[allow(dead_code)] -const EXPANDED_STACK_SIZE: usize = 4 * 1024 * 1024; - -/// Without this, some tests pass in `cargo test --release` but fail without -/// the --release flag because they run out of stack space. This increases -/// stack size for debug builds only, while leaving the stack space at the default -/// amount for release builds. -#[allow(dead_code)] -#[cfg(debug_assertions)] -pub fn with_larger_debug_stack(run_test: F) -where - F: FnOnce(), - F: Send, - F: 'static, -{ - std::thread::Builder::new() - .stack_size(EXPANDED_STACK_SIZE) - .spawn(run_test) - .expect("Error while spawning expanded dev stack size thread") - .join() - .expect("Error while joining expanded dev stack size thread") -} - -/// In --release builds, don't increase the stack size. Run the test normally. -/// This way, we find out if any of our tests are blowing the stack even after -/// optimizations in release builds. -#[allow(dead_code)] -#[cfg(not(debug_assertions))] -#[inline(always)] -pub fn with_larger_debug_stack(run_test: F) -where - F: FnOnce(), - F: Send, - F: 'static, -{ - run_test() -} - -#[allow(dead_code)] -pub fn can_expr<'a>(arena: &'a Bump, expr_str: &'a str) -> Result> { - can_expr_with(arena, test_home(), expr_str) -} - -pub struct CanExprOut { - pub loc_expr: Loc, - pub output: Output, - pub problems: Vec, - pub home: ModuleId, - pub interns: Interns, - pub var_store: VarStore, - pub var: Variable, - pub constraint: Constraint, - pub constraints: Constraints, -} - -#[derive(Debug)] -pub struct ParseErrOut<'a> { - pub fail: SourceError<'a, SyntaxError<'a>>, - pub home: ModuleId, - pub interns: Interns, -} - -#[allow(dead_code)] -pub fn can_expr_with<'a>( - arena: &'a Bump, - home: ModuleId, - expr_str: &'a str, -) -> Result> { - let loc_expr = match roc_parse::test_helpers::parse_loc_with(arena, expr_str) { - Ok(e) => e, - Err(fail) => { - let interns = Interns::default(); - - return Err(ParseErrOut { - fail, - interns, - home, - }); - } - }; - - let mut var_store = VarStore::default(); - let var = var_store.fresh(); - let expected = Expected::NoExpectation(Type::Variable(var)); - let mut module_ids = ModuleIds::default(); - - // ensure the Test module is accessible in our tests - module_ids.get_or_insert(&"Test".into()); - - // Desugar operators (convert them to Apply calls, taking into account - // operator precedence and associativity rules), before doing other canonicalization. - // - // If we did this *during* canonicalization, then each time we - // visited a BinOp node we'd recursively try to apply this to each of its nested - // operators, and then again on *their* nested operators, ultimately applying the - // rules multiple times unnecessarily. - let loc_expr = operator::desugar_expr(arena, &loc_expr); - - let mut scope = Scope::new(home, IdentIds::default(), Default::default()); - - // to skip loading other modules, we populate the scope with the builtin aliases - // that makes the reporting tests much faster - add_aliases(&mut scope, &mut var_store); - - let dep_idents = IdentIds::exposed_builtins(0); - let mut env = Env::new(home, &dep_idents, &module_ids); - let (loc_expr, output) = canonicalize_expr( - &mut env, - &mut var_store, - &mut scope, - loc_expr.region, - &loc_expr.value, - ); - - let mut constraints = Constraints::new(); - let constraint = constrain_expr( - &mut constraints, - &mut roc_constrain::expr::Env { - rigids: MutMap::default(), - home, - resolutions_to_make: vec![], - }, - loc_expr.region, - &loc_expr.value, - expected, - ); - - let imports = roc_builtins::std::borrow_stdlib() - .types - .keys() - .copied() - .collect(); - - let constraint = - introduce_builtin_imports(&mut constraints, imports, constraint, &mut var_store); - - let mut all_ident_ids = IdentIds::exposed_builtins(1); - all_ident_ids.insert(home, scope.locals.ident_ids); - - let interns = Interns { - module_ids: env.module_ids.clone(), - all_ident_ids, - }; - - Ok(CanExprOut { - loc_expr, - output, - problems: env.problems, - home: env.home, - var_store, - interns, - var, - constraint, - constraints, - }) -} - -fn add_aliases(scope: &mut Scope, var_store: &mut VarStore) { - use roc_types::solved_types::{BuiltinAlias, FreeVars}; - - let solved_aliases = roc_types::builtin_aliases::aliases(); - - for (symbol, builtin_alias) in solved_aliases { - let BuiltinAlias { - region, - vars, - typ, - kind, - } = builtin_alias; - - let mut free_vars = FreeVars::default(); - let typ = roc_types::solved_types::to_type(&typ, &mut free_vars, var_store); - - let mut variables = Vec::new(); - // make sure to sort these variables to make them line up with the type arguments - let mut type_variables: Vec<_> = free_vars.unnamed_vars.into_iter().collect(); - type_variables.sort(); - for (loc_name, (_, var)) in vars.iter().zip(type_variables) { - variables.push(Loc::at( - loc_name.region, - AliasVar::unbound(loc_name.value.clone(), var), - )); - } - - scope.add_alias(symbol, region, variables, typ, kind); - } -} - -#[allow(dead_code)] -pub fn mut_map_from_pairs(pairs: I) -> MutMap -where - I: IntoIterator, - K: Hash + Eq, -{ - let mut answer = MutMap::default(); - - for (key, value) in pairs { - answer.insert(key, value); - } - - answer -} - -#[allow(dead_code)] -pub fn im_map_from_pairs(pairs: I) -> ImMap -where - I: IntoIterator, - K: Hash + Eq + Clone, - V: Clone, -{ - let mut answer = ImMap::default(); - - for (key, value) in pairs { - answer.insert(key, value); - } - - answer -} - -#[allow(dead_code)] -pub fn send_set_from(elems: I) -> SendSet -where - I: IntoIterator, - V: Hash + Eq + Clone, -{ - let mut answer = SendSet::default(); - - for elem in elems { - answer.insert(elem); - } - - answer -} - -#[allow(dead_code)] -pub fn fixtures_dir() -> PathBuf { - Path::new("tests").join("fixtures").join("build") -} - -#[allow(dead_code)] -pub fn builtins_dir() -> PathBuf { - PathBuf::new().join("builtins") -} diff --git a/reporting/tests/test_reporting.rs b/reporting/tests/test_reporting.rs deleted file mode 100644 index fa1994f1eb..0000000000 --- a/reporting/tests/test_reporting.rs +++ /dev/null @@ -1,10351 +0,0 @@ -#[macro_use] -extern crate pretty_assertions; -extern crate bumpalo; -extern crate indoc; -extern crate roc_reporting; - -mod helpers; - -#[cfg(test)] -mod test_reporting { - use crate::helpers::{can_expr, infer_expr, test_home, CanExprOut, ParseErrOut}; - use bumpalo::Bump; - use indoc::indoc; - use roc_can::abilities::AbilitiesStore; - use roc_can::expr::PendingDerives; - use roc_load::{self, LoadedModule, LoadingProblem, Threading}; - use roc_module::symbol::{Interns, ModuleId}; - use roc_region::all::LineInfo; - use roc_reporting::report::{ - can_problem, parse_problem, type_problem, RenderTarget, Report, Severity, ANSI_STYLE_CODES, - DEFAULT_PALETTE, - }; - use roc_reporting::report::{RocDocAllocator, RocDocBuilder}; - use roc_solve::solve; - use roc_test_utils::assert_multiline_str_eq; - use roc_types::subs::Subs; - use std::path::PathBuf; - - fn filename_from_string(str: &str) -> PathBuf { - let mut filename = PathBuf::new(); - filename.push(str); - - filename - } - - fn to_simple_report(doc: RocDocBuilder) -> Report { - Report { - title: "".to_string(), - doc, - filename: filename_from_string(r"/code/proj/Main.roc"), - severity: Severity::RuntimeError, - } - } - - fn promote_expr_to_module(src: &str) -> String { - let mut buffer = String::from("app \"test\" provides [main] to \"./platform\"\n\nmain =\n"); - - for line in src.lines() { - // indent the body! - buffer.push_str(" "); - buffer.push_str(line); - buffer.push('\n'); - } - - buffer - } - - fn run_load_and_infer<'a>( - subdir: &str, - arena: &'a Bump, - src: &'a str, - ) -> (String, Result>) { - use std::fs::File; - use std::io::Write; - - let module_src = if src.starts_with("app") { - // this is already a module - src.to_string() - } else { - // this is an expression, promote it to a module - promote_expr_to_module(src) - }; - - let exposed_types = Default::default(); - let loaded = { - // Use a deterministic temporary directory. - // We can't have all tests use "tmp" because tests run in parallel, - // so append the test name to the tmp path. - let tmp = format!("tmp/{}", subdir); - let dir = roc_test_utils::TmpDir::new(&tmp); - - let filename = PathBuf::from("Test.roc"); - let file_path = dir.path().join(filename); - let full_file_path = file_path.clone(); - let mut file = File::create(file_path).unwrap(); - writeln!(file, "{}", module_src).unwrap(); - let result = roc_load::load_and_typecheck( - arena, - full_file_path, - dir.path(), - exposed_types, - roc_target::TargetInfo::default_x86_64(), - RenderTarget::Generic, - Threading::Single, - ); - drop(file); - - result - }; - - (module_src, loaded) - } - - #[allow(clippy::type_complexity)] - fn infer_expr_help_new<'a>( - subdir: &str, - arena: &'a Bump, - expr_src: &'a str, - ) -> Result< - ( - String, - Vec, - Vec, - ModuleId, - Interns, - ), - LoadingProblem<'a>, - > { - let (module_src, result) = run_load_and_infer(subdir, arena, expr_src); - let LoadedModule { - module_id: home, - mut can_problems, - mut type_problems, - interns, - .. - } = result?; - - let can_problems = can_problems.remove(&home).unwrap_or_default(); - let type_problems = type_problems.remove(&home).unwrap_or_default(); - - Ok((module_src, type_problems, can_problems, home, interns)) - } - - fn list_reports_new(subdir: &str, arena: &Bump, src: &str, finalize_render: F) -> String - where - F: FnOnce(RocDocBuilder<'_>, &mut String), - { - use ven_pretty::DocAllocator; - - let filename = filename_from_string(r"/code/proj/Main.roc"); - - let mut buf = String::new(); - - match infer_expr_help_new(subdir, arena, src) { - Err(LoadingProblem::FormattedReport(fail)) => fail, - Ok((module_src, type_problems, can_problems, home, interns)) => { - let lines = LineInfo::new(&module_src); - let src_lines: Vec<&str> = module_src.split('\n').collect(); - let mut reports = Vec::new(); - - let alloc = RocDocAllocator::new(&src_lines, home, &interns); - - for problem in can_problems { - let report = can_problem(&alloc, &lines, filename.clone(), problem.clone()); - reports.push(report); - } - - for problem in type_problems { - if let Some(report) = - type_problem(&alloc, &lines, filename.clone(), problem.clone()) - { - reports.push(report); - } - } - - let has_reports = !reports.is_empty(); - - let doc = alloc - .stack(reports.into_iter().map(|v| v.pretty(&alloc))) - .append(if has_reports { - alloc.line() - } else { - alloc.nil() - }); - - finalize_render(doc, &mut buf); - buf - } - Err(other) => { - panic!("failed to load: {:?}", other); - } - } - } - - fn infer_expr_help<'a>( - arena: &'a Bump, - expr_src: &'a str, - ) -> Result< - ( - Vec, - Vec, - ModuleId, - Interns, - ), - ParseErrOut<'a>, - > { - let CanExprOut { - loc_expr: _, - output, - var_store, - var, - constraints, - constraint, - home, - interns, - problems: can_problems, - .. - } = can_expr(arena, expr_src)?; - let mut subs = Subs::new_from_varstore(var_store); - - for named in output.introduced_variables.named { - subs.rigid_var(named.variable, named.name); - } - - for var in output.introduced_variables.wildcards { - subs.rigid_var(var.value, "*".into()); - } - - let mut solve_aliases = roc_solve::solve::Aliases::default(); - - for (name, alias) in output.aliases { - solve_aliases.insert(name, alias); - } - - let mut unify_problems = Vec::new(); - let mut abilities_store = AbilitiesStore::default(); - let (_content, _subs) = infer_expr( - subs, - &mut unify_problems, - &constraints, - &constraint, - // Use `new_report_problem_as` in order to get proper derives. - // TODO: remove the non-new reporting test infra. - PendingDerives::default(), - &mut solve_aliases, - &mut abilities_store, - var, - ); - - Ok((unify_problems, can_problems, home, interns)) - } - - fn list_reports(arena: &Bump, src: &str, buf: &mut String, callback: F) - where - F: FnOnce(RocDocBuilder<'_>, &mut String), - { - use ven_pretty::DocAllocator; - - let src_lines: Vec<&str> = src.split('\n').collect(); - let lines = LineInfo::new(src); - - let filename = filename_from_string(r"/code/proj/Main.roc"); - - match infer_expr_help(arena, src) { - Err(parse_err) => { - let ParseErrOut { - fail, - home, - interns, - } = parse_err; - - let alloc = RocDocAllocator::new(&src_lines, home, &interns); - - let problem = fail.into_file_error(filename.clone()); - let doc = parse_problem(&alloc, &lines, filename, 0, problem); - - callback(doc.pretty(&alloc).append(alloc.line()), buf) - } - Ok((type_problems, can_problems, home, interns)) => { - let mut reports = Vec::new(); - - let alloc = RocDocAllocator::new(&src_lines, home, &interns); - - for problem in can_problems { - let report = can_problem(&alloc, &lines, filename.clone(), problem.clone()); - reports.push(report); - } - - for problem in type_problems { - if let Some(report) = - type_problem(&alloc, &lines, filename.clone(), problem.clone()) - { - reports.push(report); - } - } - - let has_reports = !reports.is_empty(); - - let doc = alloc - .stack(reports.into_iter().map(|v| v.pretty(&alloc))) - .append(if has_reports { - alloc.line() - } else { - alloc.nil() - }); - - callback(doc, buf) - } - } - } - - fn list_header_reports(arena: &Bump, src: &str, buf: &mut String, callback: F) - where - F: FnOnce(RocDocBuilder<'_>, &mut String), - { - use ven_pretty::DocAllocator; - - use roc_parse::state::State; - - let state = State::new(src.as_bytes()); - - let filename = filename_from_string(r"/code/proj/Main.roc"); - let src_lines: Vec<&str> = src.split('\n').collect(); - let lines = LineInfo::new(src); - - match roc_parse::module::parse_header(arena, state) { - Err(fail) => { - let interns = Interns::default(); - let home = crate::helpers::test_home(); - - let alloc = RocDocAllocator::new(&src_lines, home, &interns); - - use roc_parse::parser::SyntaxError; - let problem = fail - .map_problem(SyntaxError::Header) - .into_file_error(filename.clone()); - let doc = parse_problem(&alloc, &lines, filename, 0, problem); - - callback(doc.pretty(&alloc).append(alloc.line()), buf) - } - Ok(_) => todo!(), - } - } - - fn report_problem_as(src: &str, expected_rendering: &str) { - let mut buf: String = String::new(); - let arena = Bump::new(); - - let callback = |doc: RocDocBuilder<'_>, buf: &mut String| { - doc.1 - .render_raw(70, &mut roc_reporting::report::CiWrite::new(buf)) - .expect("list_reports") - }; - - list_reports(&arena, src, &mut buf, callback); - - // convenient to copy-paste the generated message - if buf != expected_rendering { - for line in buf.split('\n') { - println!(" {}", line); - } - } - - assert_multiline_str_eq!(expected_rendering, buf.as_str()); - } - - fn report_header_problem_as(src: &str, expected_rendering: &str) { - let mut buf: String = String::new(); - let arena = Bump::new(); - - let callback = |doc: RocDocBuilder<'_>, buf: &mut String| { - doc.1 - .render_raw(70, &mut roc_reporting::report::CiWrite::new(buf)) - .expect("list_reports") - }; - - list_header_reports(&arena, src, &mut buf, callback); - - // convenient to copy-paste the generated message - if buf != expected_rendering { - for line in buf.split('\n') { - println!(" {}", line); - } - } - - assert_eq!(buf, expected_rendering); - } - - fn color_report_problem_as(src: &str, expected_rendering: &str) { - let mut buf: String = String::new(); - let arena = Bump::new(); - - let callback = |doc: RocDocBuilder<'_>, buf: &mut String| { - doc.1 - .render_raw( - 70, - &mut roc_reporting::report::ColorWrite::new( - &roc_reporting::report::DEFAULT_PALETTE, - buf, - ), - ) - .expect("list_reports") - }; - - list_reports(&arena, src, &mut buf, callback); - - let readable = human_readable(&buf); - - assert_eq!(readable, expected_rendering); - } - - fn new_report_problem_as(subdir: &str, src: &str, expected_rendering: &str) { - let arena = Bump::new(); - - let finalize_render = |doc: RocDocBuilder<'_>, buf: &mut String| { - doc.1 - .render_raw(70, &mut roc_reporting::report::CiWrite::new(buf)) - .expect("list_reports") - }; - - let buf = list_reports_new(subdir, &arena, src, finalize_render); - - // convenient to copy-paste the generated message - if buf != expected_rendering { - for line in buf.split('\n') { - println!(" {}", line); - } - } - - assert_multiline_str_eq!(expected_rendering, buf.as_str()); - } - - fn human_readable(str: &str) -> String { - str.replace(ANSI_STYLE_CODES.red, "") - .replace(ANSI_STYLE_CODES.white, "") - .replace(ANSI_STYLE_CODES.blue, "") - .replace(ANSI_STYLE_CODES.yellow, "") - .replace(ANSI_STYLE_CODES.green, "") - .replace(ANSI_STYLE_CODES.cyan, "") - .replace(ANSI_STYLE_CODES.magenta, "") - .replace(ANSI_STYLE_CODES.reset, "") - .replace(ANSI_STYLE_CODES.bold, "") - .replace(ANSI_STYLE_CODES.underline, "") - } - - #[test] - fn value_not_exposed() { - report_problem_as( - indoc!( - r#" - List.isempty 1 2 - "# - ), - indoc!( - r#" - ── NOT EXPOSED ─────────────────────────────────────────── /code/proj/Main.roc ─ - - The List module does not expose `isempty`: - - 1│ List.isempty 1 2 - ^^^^^^^^^^^^ - - Did you mean one of these? - - List.isEmpty - List.set - List.get - List.keepIf - "# - ), - ) - } - - #[test] - fn report_unused_def() { - report_problem_as( - indoc!( - r#" - x = 1 - y = 2 - - x - "# - ), - indoc!( - r#" - ── UNUSED DEFINITION ───────────────────────────────────── /code/proj/Main.roc ─ - - `y` is not used anywhere in your code. - - 2│ y = 2 - ^ - - If you didn't intend on using `y` then remove it so future readers of - your code don't wonder why it is there. - "# - ), - ) - } - - #[test] - fn report_shadowing() { - report_problem_as( - indoc!( - r#" - i = 1 - - s = \i -> - i + 1 - - s i - "# - ), - indoc!( - r#" - ── DUPLICATE NAME ──────────────────────────────────────── /code/proj/Main.roc ─ - - The `i` name is first defined here: - - 1│ i = 1 - ^ - - But then it's defined a second time here: - - 3│ s = \i -> - ^ - - Since these variables have the same name, it's easy to use the wrong - one on accident. Give one of them a new name. - "# - ), - ) - } - - #[test] - fn report_shadowing_in_annotation() { - report_problem_as( - indoc!( - r#" - Booly : [Yes, No] - - Booly : [Yes, No, Maybe] - - x : List Booly - x = [] - - x - "# - ), - indoc!( - r#" - ── DUPLICATE NAME ──────────────────────────────────────── /code/proj/Main.roc ─ - - The `Booly` name is first defined here: - - 1│ Booly : [Yes, No] - ^^^^^^^^^^^^^^^^^ - - But then it's defined a second time here: - - 3│ Booly : [Yes, No, Maybe] - ^^^^^^^^^^^^^^^^^^^^^^^^ - - Since these aliases have the same name, it's easy to use the wrong one - on accident. Give one of them a new name. - "# - ), - ) - } - - // #[test] - // fn report_multi_line_shadowing_in_annotation() { - // report_problem_as( - // indoc!( - // r#" - // Booly : - // [ - // Yes, - // No - // ] - // - // Booly : - // [ - // Yes, - // No, - // Maybe - // ] - // - // x = - // No - // - // x - // "# - // ), - // indoc!( - // r#" - // Booly is first defined here: - // - // 1│> Booly : - // 2│> [ - // 3│> Yes, - // 4│> No - // 5│> ] - // - // But then it's defined a second time here: - // - // 7 │> Booly : - // 8 │> [ - // 9 │> Yes, - // 10│> No, - // 11│> Maybe - // 12│> ] - // - // Since these variables have the same name, it's easy to use the wrong one on accident. Give one of them a new name."# - // ), - // ) - // } - - // #[test] - // fn report_unsupported_top_level_def() { - // report_problem_as( - // indoc!( - // r#" - // x = 1 - // - // 5 = 2 + 1 - // - // x - // "# - // ), - // indoc!(r#" "#), - // ) - // } - - #[test] - fn report_precedence_problem_single_line() { - report_problem_as( - indoc!( - r#"x = 1 - y = - if selectedId != thisId == adminsId then - 4 - - else - 5 - - { x, y } - "# - ), - indoc!( - r#" - ── SYNTAX PROBLEM ──────────────────────────────────────── /code/proj/Main.roc ─ - - Using != and == together requires parentheses, to clarify how they - should be grouped. - - 3│ if selectedId != thisId == adminsId then - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - "# - ), - ) - } - - #[test] - fn unrecognized_name() { - report_problem_as( - indoc!( - r#" - foo = { x: 1 == 1, y: 0x4 } - - baz = 3 - - main : Str - main = - when foo.y is - 4 -> bar baz "yay" - _ -> "nay" - - main - "# - ), - indoc!( - r#" - ── UNRECOGNIZED NAME ───────────────────────────────────── /code/proj/Main.roc ─ - - Nothing is named `bar` in this scope. - - 8│ 4 -> bar baz "yay" - ^^^ - - Did you mean one of these? - - baz - Str - Err - main - "# - ), - ) - } - - #[test] - fn lowercase_primitive_tag_bool() { - report_problem_as( - indoc!( - r#" - if true then 1 else 2 - "# - ), - indoc!( - r#" - ── UNRECOGNIZED NAME ───────────────────────────────────── /code/proj/Main.roc ─ - - Nothing is named `true` in this scope. - - 1│ if true then 1 else 2 - ^^^^ - - Did you mean one of these? - - True - Str - Err - List - "# - ), - ) - } - - #[test] - fn report_precedence_problem_multiline() { - report_problem_as( - indoc!( - r#" - if - 1 - == 2 - == 3 - then - 2 - - else - 3 - "# - ), - indoc!( - r#" - ── SYNTAX PROBLEM ──────────────────────────────────────── /code/proj/Main.roc ─ - - Using more than one == like this requires parentheses, to clarify how - things should be grouped. - - 2│> 1 - 3│> == 2 - 4│> == 3 - "# - ), - ) - } - - #[test] - fn unused_arg_and_unused_def() { - report_problem_as( - indoc!( - r#" - y = 9 - - box = \class, htmlChildren -> - div [class] [] - - div = \_, _ -> 4 - - box "wizard" [] - "# - ), - indoc!( - r#" - ── UNUSED ARGUMENT ─────────────────────────────────────── /code/proj/Main.roc ─ - - `box` doesn't use `htmlChildren`. - - 3│ box = \class, htmlChildren -> - ^^^^^^^^^^^^ - - If you don't need `htmlChildren`, then you can just remove it. However, - if you really do need `htmlChildren` as an argument of `box`, prefix it - with an underscore, like this: "_`htmlChildren`". Adding an underscore - at the start of a variable name is a way of saying that the variable - is not used. - - ── UNUSED DEFINITION ───────────────────────────────────── /code/proj/Main.roc ─ - - `y` is not used anywhere in your code. - - 1│ y = 9 - ^ - - If you didn't intend on using `y` then remove it so future readers of - your code don't wonder why it is there. - "# - ), - ); - } - - #[test] - fn report_value_color() { - let src: &str = indoc!( - r#" - activityIndicatorLarge = div - - view activityIndicatorLarge - "# - ); - - let arena = Bump::new(); - let (_type_problems, _can_problems, home, interns) = - infer_expr_help(&arena, src).expect("parse error"); - - let mut buf = String::new(); - let src_lines: Vec<&str> = src.split('\n').collect(); - - let alloc = RocDocAllocator::new(&src_lines, home, &interns); - - let symbol = interns.symbol(test_home(), "activityIndicatorLarge".into()); - - to_simple_report(alloc.symbol_unqualified(symbol)).render_color_terminal( - &mut buf, - &alloc, - &DEFAULT_PALETTE, - ); - - assert_eq!(human_readable(&buf), "activityIndicatorLarge"); - } - - #[test] - fn report_module_color() { - let src: &str = indoc!( - r#" - x = 1 - y = 2 - - x - "# - ); - - let arena = Bump::new(); - let (_type_problems, _can_problems, home, mut interns) = - infer_expr_help(&arena, src).expect("parse error"); - - let mut buf = String::new(); - let src_lines: Vec<&str> = src.split('\n').collect(); - let module_id = interns.module_id(&"Util.Int".into()); - - let alloc = RocDocAllocator::new(&src_lines, home, &interns); - to_simple_report(alloc.module(module_id)).render_color_terminal( - &mut buf, - &alloc, - &DEFAULT_PALETTE, - ); - - assert_eq!(human_readable(&buf), "Util.Int"); - } - - #[test] - fn report_region_in_color() { - color_report_problem_as( - indoc!( - r#" - isDisabled = \user -> user.isAdmin - - theAdmin - |> isDisabled - "# - ), - indoc!( - r#" - ── UNRECOGNIZED NAME ───────────────────────────────────── /code/proj/Main.roc ─ - - Nothing is named `theAdmin` in this scope. - - 3 theAdmin - ^^^^^^^^ - - Did you mean one of these? - - Set - List - True - Box - "# - ), - ); - } - - // #[test] - // fn shadowing_type_alias() { - // report_problem_as( - // indoc!( - // r#" - // foo : I64 as I64 - // foo = 42 - - // foo - // "# - // ), - // indoc!( - // r#" - // You cannot mix (!=) and (==) without parentheses - - // 3│ if selectedId != thisId == adminsId then - // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - - // "# - // ), - // ) - // } - - // #[test] - // fn invalid_as_type_alias() { - // report_problem_as( - // indoc!( - // r#" - // foo : I64 as a - // foo = 42 - - // foo - // "# - // ), - // indoc!( - // r#" - // You cannot mix (!=) and (==) without parentheses - - // 3│ if selectedId != thisId == adminsId then - // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - - // "# - // ), - // ) - // } - - #[test] - fn if_condition_not_bool() { - report_problem_as( - indoc!( - r#" - if "foo" then 2 else 3 - "# - ), - indoc!( - r#" - ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ - - This `if` condition needs to be a Bool: - - 1│ if "foo" then 2 else 3 - ^^^^^ - - Right now it’s a string of type: - - Str - - But I need every `if` condition to evaluate to a Bool—either `True` or - `False`. - "# - ), - ) - } - - #[test] - fn when_if_guard() { - report_problem_as( - indoc!( - r#" - when 1 is - 2 if 1 -> 0x0 - _ -> 0x1 - "# - ), - indoc!( - r#" - ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ - - This `if` guard condition needs to be a Bool: - - 1│ when 1 is - 2│> 2 if 1 -> 0x0 - 3│ _ -> 0x1 - - Right now it’s a number of type: - - Num a - - But I need every `if` guard condition to evaluate to a Bool—either - `True` or `False`. - "# - ), - ) - } - - #[test] - fn if_2_branch_mismatch() { - report_problem_as( - indoc!( - r#" - if True then 2 else "foo" - "# - ), - indoc!( - r#" - ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ - - This `if` has an `else` branch with a different type from its `then` branch: - - 1│ if True then 2 else "foo" - ^^^^^ - - The `else` branch is a string of type: - - Str - - but the `then` branch has the type: - - Num a - - All branches in an `if` must have the same type! - "# - ), - ) - } - - #[test] - fn if_3_branch_mismatch() { - report_problem_as( - indoc!( - r#" - if True then 2 else if False then 2 else "foo" - "# - ), - indoc!( - r#" - ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ - - The 3rd branch of this `if` does not match all the previous branches: - - 1│ if True then 2 else if False then 2 else "foo" - ^^^^^ - - The 3rd branch is a string of type: - - Str - - But all the previous branches have type: - - Num a - - All branches in an `if` must have the same type! - "# - ), - ) - } - - #[test] - fn when_branch_mismatch() { - report_problem_as( - indoc!( - r#" - when 1 is - 2 -> "foo" - 3 -> {} - _ -> "" - "# - ), - indoc!( - r#" - ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ - - The 2nd branch of this `when` does not match all the previous branches: - - 1│ when 1 is - 2│ 2 -> "foo" - 3│> 3 -> {} - 4│ _ -> "" - - The 2nd branch is a record of type: - - {} - - But all the previous branches have type: - - Str - - All branches of a `when` must have the same type! - "# - ), - ) - } - - #[test] - fn elem_in_list() { - report_problem_as( - indoc!( - r#" - [1, 3, "foo"] - "# - ), - indoc!( - r#" - ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ - - This list contains elements with different types: - - 1│ [1, 3, "foo"] - ^^^^^ - - Its 3rd element is a string of type: - - Str - - However, the preceding elements in the list all have the type: - - Num a - - Every element in a list must have the same type! - "# - ), - ) - } - - #[test] - fn record_update_value() { - report_problem_as( - indoc!( - r#" - x : { foo : {} } - x = { foo: {} } - - { x & foo: "bar" } - "# - ), - indoc!( - r#" - ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ - - I cannot update the `.foo` field like this: - - 4│ { x & foo: "bar" } - ^^^^^ - - You are trying to update `.foo` to be a string of type: - - Str - - But it should be: - - {} - - Record update syntax does not allow you to change the type of fields. - You can achieve that with record literal syntax. - "# - ), - ) - } - - #[test] - fn circular_type() { - report_problem_as( - indoc!( - r#" - f = \g -> g g - - f - "# - ), - indoc!( - r#" - ── CIRCULAR TYPE ───────────────────────────────────────── /code/proj/Main.roc ─ - - I'm inferring a weird self-referential type for `g`: - - 1│ f = \g -> g g - ^ - - Here is my best effort at writing down the type. You will see ∞ for - parts of the type that repeat something already printed out - infinitely. - - ∞ -> a - "# - ), - ) - } - - #[test] - fn polymorphic_recursion() { - report_problem_as( - indoc!( - r#" - f = \x -> f [x] - - f - "# - ), - indoc!( - r#" - ── CIRCULAR TYPE ───────────────────────────────────────── /code/proj/Main.roc ─ - - I'm inferring a weird self-referential type for `f`: - - 1│ f = \x -> f [x] - ^ - - Here is my best effort at writing down the type. You will see ∞ for - parts of the type that repeat something already printed out - infinitely. - - List ∞ -> a - "# - ), - ) - } - - #[test] - fn polymorphic_mutual_recursion() { - report_problem_as( - indoc!( - r#" - f = \x -> g x - g = \x -> f [x] - - f - "# - ), - indoc!( - r#" - ── CIRCULAR TYPE ───────────────────────────────────────── /code/proj/Main.roc ─ - - I'm inferring a weird self-referential type for `g`: - - 2│ g = \x -> f [x] - ^ - - Here is my best effort at writing down the type. You will see ∞ for - parts of the type that repeat something already printed out - infinitely. - - List ∞ -> a - - ── CIRCULAR TYPE ───────────────────────────────────────── /code/proj/Main.roc ─ - - I'm inferring a weird self-referential type for `f`: - - 1│ f = \x -> g x - ^ - - Here is my best effort at writing down the type. You will see ∞ for - parts of the type that repeat something already printed out - infinitely. - - List ∞ -> a - "# - ), - ) - } - - #[test] - fn polymorphic_mutual_recursion_annotated() { - report_problem_as( - indoc!( - r#" - f : a -> List a - f = \x -> g x - g = \x -> f [x] - - f - "# - ), - indoc!( - r#" - ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ - - This expression is used in an unexpected way: - - 2│ f = \x -> g x - ^^^ - - This `g` call produces: - - List List a - - But you are trying to use it as: - - List a - - Tip: The type annotation uses the type variable `a` to say that this - definition can produce any type of value. But in the body I see that - it will only produce a `List` value of a single specific type. Maybe - change the type annotation to be more specific? Maybe change the code - to be more general? - "# - ), - ) - } - - #[test] - fn polymorphic_mutual_recursion_dually_annotated_lie() { - report_problem_as( - indoc!( - r#" - f : a -> List a - f = \x -> g x - g : b -> List b - g = \x -> f [x] - - f - "# - ), - indoc!( - r#" - ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ - - This expression is used in an unexpected way: - - 4│ g = \x -> f [x] - ^^^^^ - - This `f` call produces: - - List List b - - But you are trying to use it as: - - List b - - Tip: The type annotation uses the type variable `b` to say that this - definition can produce any type of value. But in the body I see that - it will only produce a `List` value of a single specific type. Maybe - change the type annotation to be more specific? Maybe change the code - to be more general? - "# - ), - ) - } - - #[test] - fn record_field_mismatch() { - report_problem_as( - indoc!( - r#" - bar = { bar : 0x3 } - - f : { foo : Num.Int * } -> [Yes, No] - f = \_ -> Yes - - f bar - "# - ), - indoc!( - r#" - ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ - - The 1st argument to `f` is not what I expect: - - 6│ f bar - ^^^ - - This `bar` value is a: - - { bar : Int a } - - But `f` needs the 1st argument to be: - - { foo : Int * } - - Tip: Seems like a record field typo. Maybe `bar` should be `foo`? - - Tip: Can more type annotations be added? Type annotations always help - me give more specific messages, and I think they could help a lot in - this case - "# - ), - ) - } - - #[test] - fn tag_mismatch() { - report_problem_as( - indoc!( - r#" - f : [Red, Green] -> [Yes, No] - f = \_ -> Yes - - f Blue - "# - ), - indoc!( - r#" - ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ - - The 1st argument to `f` is not what I expect: - - 4│ f Blue - ^^^^ - - This `Blue` tag has the type: - - [Blue]a - - But `f` needs the 1st argument to be: - - [Green, Red] - - Tip: Seems like a tag typo. Maybe `Blue` should be `Red`? - - Tip: Can more type annotations be added? Type annotations always help - me give more specific messages, and I think they could help a lot in - this case - "# - ), - ) - } - - #[test] - fn tag_with_arguments_mismatch() { - report_problem_as( - indoc!( - r#" - f : [Red (Num.Int *), Green Str] -> Str - f = \_ -> "yes" - - f (Blue 3.14) - "# - ), - indoc!( - r#" - ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ - - The 1st argument to `f` is not what I expect: - - 4│ f (Blue 3.14) - ^^^^^^^^^ - - This `Blue` tag application has the type: - - [Blue (Frac a)]b - - But `f` needs the 1st argument to be: - - [Green Str, Red (Int *)] - - Tip: Seems like a tag typo. Maybe `Blue` should be `Red`? - - Tip: Can more type annotations be added? Type annotations always help - me give more specific messages, and I think they could help a lot in - this case - "# - ), - ) - } - - #[test] - fn from_annotation_if() { - report_problem_as( - indoc!( - r#" - x : Num.Int * - x = if True then 3.14 else 4 - - x - "# - ), - indoc!( - r#" - ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ - - Something is off with the `then` branch of this `if` expression: - - 1│ x : Num.Int * - 2│ x = if True then 3.14 else 4 - ^^^^ - - The 1st branch is a frac of type: - - Frac a - - But the type annotation on `x` says it should be: - - Int * - - Tip: You can convert between Int and Frac using functions like - `Num.toFrac` and `Num.round`. - "# - ), - ) - } - - #[test] - fn from_annotation_when() { - report_problem_as( - indoc!( - r#" - x : Num.Int * - x = - when True is - _ -> 3.14 - - x - "# - ), - indoc!( - r#" - ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ - - Something is off with the body of the `x` definition: - - 1│ x : Num.Int * - 2│ x = - 3│> when True is - 4│> _ -> 3.14 - - This `when` expression produces: - - Frac a - - But the type annotation on `x` says it should be: - - Int * - - Tip: You can convert between Int and Frac using functions like - `Num.toFrac` and `Num.round`. - "# - ), - ) - } - - #[test] - fn from_annotation_function() { - report_problem_as( - indoc!( - r#" - x : Num.Int * -> Num.Int * - x = \_ -> 3.14 - - x - "# - ), - indoc!( - r#" - ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ - - Something is off with the body of the `x` definition: - - 1│ x : Num.Int * -> Num.Int * - 2│ x = \_ -> 3.14 - ^^^^ - - The body is a frac of type: - - Frac a - - But the type annotation on `x` says it should be: - - Int * - - Tip: You can convert between Int and Frac using functions like - `Num.toFrac` and `Num.round`. - "# - ), - ) - } - - #[test] - fn fncall_value() { - report_problem_as( - indoc!( - r#" - x : Num.I64 - x = 42 - - x 3 - "# - ), - indoc!( - r#" - ── TOO MANY ARGS ───────────────────────────────────────── /code/proj/Main.roc ─ - - The `x` value is not a function, but it was given 1 argument: - - 4│ x 3 - ^ - - Are there any missing commas? Or missing parentheses? - "# - ), - ) - } - - #[test] - fn fncall_overapplied() { - report_problem_as( - indoc!( - r#" - f : Num.I64 -> Num.I64 - f = \_ -> 42 - - f 1 2 - "# - ), - indoc!( - r#" - ── TOO MANY ARGS ───────────────────────────────────────── /code/proj/Main.roc ─ - - The `f` function expects 1 argument, but it got 2 instead: - - 4│ f 1 2 - ^ - - Are there any missing commas? Or missing parentheses? - "# - ), - ) - } - - #[test] - fn fncall_underapplied() { - report_problem_as( - indoc!( - r#" - f : Num.I64, Num.I64 -> Num.I64 - f = \_, _ -> 42 - - f 1 - "# - ), - indoc!( - r#" - ── TOO FEW ARGS ────────────────────────────────────────── /code/proj/Main.roc ─ - - The `f` function expects 2 arguments, but it got only 1: - - 4│ f 1 - ^ - - Roc does not allow functions to be partially applied. Use a closure to - make partial application explicit. - "# - ), - ) - } - - #[test] - fn pattern_when_condition() { - report_problem_as( - indoc!( - r#" - when 1 is - {} -> 42 - "# - ), - indoc!( - r#" - ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ - - The branches of this `when` expression don't match the condition: - - 1│> when 1 is - 2│ {} -> 42 - - The `when` condition is a number of type: - - Num a - - But the branch patterns have type: - - {}a - - The branches must be cases of the `when` condition's type! - "# - ), - ) - } - - #[test] - fn pattern_when_pattern() { - report_problem_as( - indoc!( - r#" - when 1 is - 2 -> 3 - {} -> 42 - "# - ), - indoc!( - r#" - ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ - - The 2nd pattern in this `when` does not match the previous ones: - - 3│ {} -> 42 - ^^ - - The 2nd pattern is trying to match record values of type: - - {}a - - But all the previous branches match: - - Num a - "# - ), - ) - } - - #[test] - fn pattern_guard_mismatch_alias() { - report_problem_as( - indoc!( - r#" - when { foo: 1 } is - { foo: True } -> 42 - "# - ), - indoc!( - r#" - ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ - - The branches of this `when` expression don't match the condition: - - 1│> when { foo: 1 } is - 2│ { foo: True } -> 42 - - The `when` condition is a record of type: - - { foo : Num a } - - But the branch patterns have type: - - { foo : [True] } - - The branches must be cases of the `when` condition's type! - "# - ), - ) - } - - #[test] - fn pattern_guard_mismatch() { - report_problem_as( - indoc!( - r#" - when { foo: "" } is - { foo: True } -> 42 - "# - ), - indoc!( - r#" - ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ - - The branches of this `when` expression don't match the condition: - - 1│> when { foo: "" } is - 2│ { foo: True } -> 42 - - The `when` condition is a record of type: - - { foo : Str } - - But the branch patterns have type: - - { foo : [True] } - - The branches must be cases of the `when` condition's type! - "# - ), - ) - } - - #[test] - fn pattern_guard_does_not_bind_label() { - // needs some improvement, but the principle works - report_problem_as( - indoc!( - r#" - when { foo: 1 } is - { foo: _ } -> foo - "# - ), - indoc!( - r#" - ── UNRECOGNIZED NAME ───────────────────────────────────── /code/proj/Main.roc ─ - - Nothing is named `foo` in this scope. - - 2│ { foo: _ } -> foo - ^^^ - - Did you mean one of these? - - Box - Set - Str - Ok - "# - ), - ) - } - - #[test] - fn pattern_guard_can_be_shadowed_above() { - report_problem_as( - indoc!( - r#" - foo = 3 - - when { foo: 1 } is - { foo: 2 } -> foo - _ -> foo - "# - ), - // should give no error - "", - ) - } - - #[test] - fn pattern_guard_can_be_shadowed_below() { - report_problem_as( - indoc!( - r#" - when { foo: 1 } is - { foo: 2 } -> - foo = 3 - - foo - _ -> 3 - "# - ), - // should give no error - "", - ) - } - - #[test] - fn pattern_or_pattern_mismatch() { - report_problem_as( - indoc!( - r#" - when { foo: 1 } is - {} | 1 -> 3 - "# - ), - // Just putting this here. We should probably handle or-patterns better - indoc!( - r#" - ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ - - The 2nd pattern in this branch does not match the previous ones: - - 2│ {} | 1 -> 3 - ^ - - The 2nd pattern is trying to match numbers: - - Num a - - But all the previous branches match: - - {}a - "# - ), - ) - } - - #[test] - fn pattern_let_mismatch() { - report_problem_as( - indoc!( - r#" - (Foo x) = 42 - - x - "# - ), - // Maybe this should specifically say the pattern doesn't work? - indoc!( - r#" - ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ - - This expression is used in an unexpected way: - - 1│ (Foo x) = 42 - ^^ - - It is a number of type: - - Num a - - But you are trying to use it as: - - [Foo a] - "# - ), - ) - } - - #[test] - fn from_annotation_complex_pattern() { - report_problem_as( - indoc!( - r#" - { x } : { x : Num.Int * } - { x } = { x: 4.0 } - - x - "# - ), - indoc!( - r#" - ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ - - Something is off with the body of this definition: - - 1│ { x } : { x : Num.Int * } - 2│ { x } = { x: 4.0 } - ^^^^^^^^^^ - - The body is a record of type: - - { x : Frac a } - - But the type annotation says it should be: - - { x : Int * } - - Tip: You can convert between Int and Frac using functions like - `Num.toFrac` and `Num.round`. - "# - ), - ) - } - - #[test] - fn malformed_int_pattern() { - report_problem_as( - indoc!( - r#" - when 1 is - 100A -> 3 - _ -> 4 - "# - ), - indoc!( - r#" - ── SYNTAX PROBLEM ──────────────────────────────────────── /code/proj/Main.roc ─ - - This integer pattern is malformed: - - 2│ 100A -> 3 - ^^^^ - - Tip: Learn more about number literals at TODO - "# - ), - ) - } - - #[test] - fn malformed_float_pattern() { - report_problem_as( - indoc!( - r#" - when 1 is - 2.X -> 3 - _ -> 4 - "# - ), - indoc!( - r#" - ── SYNTAX PROBLEM ──────────────────────────────────────── /code/proj/Main.roc ─ - - This float pattern is malformed: - - 2│ 2.X -> 3 - ^^^ - - Tip: Learn more about number literals at TODO - "# - ), - ) - } - - #[test] - fn malformed_hex_pattern() { - report_problem_as( - indoc!( - r#" - when 1 is - 0xZ -> 3 - _ -> 4 - "# - ), - indoc!( - r#" - ── SYNTAX PROBLEM ──────────────────────────────────────── /code/proj/Main.roc ─ - - This hex integer pattern is malformed: - - 2│ 0xZ -> 3 - ^^^ - - Tip: Learn more about number literals at TODO - "# - ), - ) - } - - #[test] - fn malformed_oct_pattern() { - report_problem_as( - indoc!( - r#" - when 1 is - 0o9 -> 3 - _ -> 4 - "# - ), - indoc!( - r#" - ── SYNTAX PROBLEM ──────────────────────────────────────── /code/proj/Main.roc ─ - - This octal integer pattern is malformed: - - 2│ 0o9 -> 3 - ^^^ - - Tip: Learn more about number literals at TODO - "# - ), - ) - } - - #[test] - fn malformed_bin_pattern() { - report_problem_as( - indoc!( - r#" - when 1 is - 0b4 -> 3 - _ -> 4 - "# - ), - indoc!( - r#" - ── SYNTAX PROBLEM ──────────────────────────────────────── /code/proj/Main.roc ─ - - This binary integer pattern is malformed: - - 2│ 0b4 -> 3 - ^^^ - - Tip: Learn more about number literals at TODO - "# - ), - ) - } - - #[test] - fn missing_fields() { - report_problem_as( - indoc!( - r#" - x : { a : Num.Int *, b : Num.Frac *, c : Str } - x = { b: 4.0 } - - x - "# - ), - indoc!( - r#" - ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ - - Something is off with the body of the `x` definition: - - 1│ x : { a : Num.Int *, b : Num.Frac *, c : Str } - 2│ x = { b: 4.0 } - ^^^^^^^^^^ - - The body is a record of type: - - { b : Frac a } - - But the type annotation on `x` says it should be: - - { a : Int *, b : Frac *, c : Str } - - Tip: Looks like the c and a fields are missing. - "# - ), - ) - } - - #[test] - fn bad_double_rigid() { - // this previously reported the message below, not sure which is better - // - // Something is off with the body of the `f` definition: - // - // 1│ f : a, b -> a - // 2│ f = \x, y -> if True then x else y - // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - // - // The body is an anonymous function of type: - // - // a, a -> a - // - // But the type annotation on `f` says it should be: - // - // a, b -> a - report_problem_as( - indoc!( - r#" - f : a, b -> a - f = \x, y -> if True then x else y - - f - "# - ), - indoc!( - r#" - ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ - - Something is off with the `else` branch of this `if` expression: - - 1│ f : a, b -> a - 2│ f = \x, y -> if True then x else y - ^ - - This `y` value is a: - - b - - But the type annotation on `f` says it should be: - - a - - Tip: Your type annotation uses `b` and `a` as separate type variables. - Your code seems to be saying they are the same though. Maybe they - should be the same in your type annotation? Maybe your code uses them - in a weird way? - "# - ), - ) - } - - #[test] - fn bad_rigid_function() { - report_problem_as( - indoc!( - r#" - f : Str -> msg - f = \_ -> Foo - - f - "# - ), - indoc!( - r#" - ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ - - Something is off with the body of the `f` definition: - - 1│ f : Str -> msg - 2│ f = \_ -> Foo - ^^^ - - This `Foo` tag has the type: - - [Foo]a - - But the type annotation on `f` says it should be: - - msg - - Tip: The type annotation uses the type variable `msg` to say that this - definition can produce any type of value. But in the body I see that - it will only produce a tag value of a single specific type. Maybe - change the type annotation to be more specific? Maybe change the code - to be more general? - "# - ), - ) - } - - #[test] - fn bad_rigid_value() { - report_problem_as( - indoc!( - r#" - f : msg - f = 0x3 - - f - "# - ), - indoc!( - r#" - ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ - - Something is off with the body of the `f` definition: - - 1│ f : msg - 2│ f = 0x3 - ^^^ - - The body is an integer of type: - - Int a - - But the type annotation on `f` says it should be: - - msg - - Tip: The type annotation uses the type variable `msg` to say that this - definition can produce any type of value. But in the body I see that - it will only produce a `Int` value of a single specific type. Maybe - change the type annotation to be more specific? Maybe change the code - to be more general? - "# - ), - ) - } - - #[test] - fn typo_lowercase_ok() { - // TODO improve tag suggestions - report_problem_as( - indoc!( - r#" - f : Str -> [Ok Num.I64, InvalidFoo] - f = \_ -> ok 4 - - f - "# - ), - indoc!( - r#" - ── UNRECOGNIZED NAME ───────────────────────────────────── /code/proj/Main.roc ─ - - Nothing is named `ok` in this scope. - - 2│ f = \_ -> ok 4 - ^^ - - Did you mean one of these? - - Ok - f - Box - Set - "# - ), - ) - } - - #[test] - fn typo_uppercase_ok() { - // these error messages seem pretty helpful - report_problem_as( - indoc!( - r#" - f : Str -> Num.I64 - f = \_ -> - ok = 3 - - Ok - - f - "# - ), - indoc!( - r#" - ── UNUSED DEFINITION ───────────────────────────────────── /code/proj/Main.roc ─ - - `ok` is not used anywhere in your code. - - 3│ ok = 3 - ^^ - - If you didn't intend on using `ok` then remove it so future readers of - your code don't wonder why it is there. - - ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ - - Something is off with the body of the `f` definition: - - 1│ f : Str -> Num.I64 - 2│ f = \_ -> - 3│ ok = 3 - 4│ - 5│ Ok - ^^ - - This `Ok` tag has the type: - - [Ok]a - - But the type annotation on `f` says it should be: - - I64 - "# - ), - ) - } - - #[test] - fn circular_definition_self() { - // invalid recursion - report_problem_as( - indoc!( - r#" - f = f - - f - "# - ), - indoc!( - r#" - ── CIRCULAR DEFINITION ─────────────────────────────────── /code/proj/Main.roc ─ - - The `f` value is defined directly in terms of itself, causing an - infinite loop. - "# - ), - ) - } - - #[test] - fn circular_definition() { - // invalid mutual recursion - report_problem_as( - indoc!( - r#" - foo = bar - - bar = foo - - foo - "# - ), - indoc!( - r#" - ── CIRCULAR DEFINITION ─────────────────────────────────── /code/proj/Main.roc ─ - - The `foo` definition is causing a very tricky infinite loop: - - 1│ foo = bar - ^^^ - - The `foo` value depends on itself through the following chain of - definitions: - - ┌─────┐ - │ foo - │ ↓ - │ bar - └─────┘ - "# - ), - ) - } - - #[test] - fn update_empty_record() { - report_problem_as( - indoc!( - r#" - x = {} - - { x & foo: 3 } - "# - ), - indoc!( - r#" - ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ - - This `x` record doesn’t have a `foo` field: - - 3│ { x & foo: 3 } - ^^^^^^ - - In fact, `x` is a record with no fields at all! - "# - ), - ) - } - - #[test] - fn update_record() { - report_problem_as( - indoc!( - r#" - x = { fo: 3, bar: 4 } - - { x & foo: 3 } - "# - ), - // TODO also suggest fields with the correct type - indoc!( - r#" - ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ - - This `x` record doesn’t have a `foo` field: - - 3│ { x & foo: 3 } - ^^^^^^ - - There may be a typo. These `x` fields are the most similar: - - { - fo : Num b, - bar : Num a, - } - - Maybe `foo:` should be `fo:` instead? - "# - ), - ) - } - - #[test] - fn update_record_ext() { - report_problem_as( - indoc!( - r#" - f : { fo: Num.I64 }ext -> Num.I64 - f = \r -> - r2 = { r & foo: r.fo } - - r2.fo - - f - "# - ), - // TODO also suggest fields with the correct type - indoc!( - r#" - ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ - - This `r` record doesn’t have a `foo` field: - - 3│ r2 = { r & foo: r.fo } - ^^^^^^^^^ - - There may be a typo. These `r` fields are the most similar: - - { - fo : I64, - }ext - - Maybe `foo:` should be `fo:` instead? - "# - ), - ) - } - - #[test] - fn update_record_snippet() { - report_problem_as( - indoc!( - r#" - x = { fo: 3, bar: 4, baz: 3, spam: 42, foobar: 3 } - - { x & foo: 3 } - "# - ), - // TODO also suggest fields with the correct type - indoc!( - r#" - ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ - - This `x` record doesn’t have a `foo` field: - - 3│ { x & foo: 3 } - ^^^^^^ - - There may be a typo. These `x` fields are the most similar: - - { - fo : Num c, - foobar : Num d, - bar : Num a, - baz : Num b, - … - } - - Maybe `foo:` should be `fo:` instead? - "# - ), - ) - } - - #[test] - fn plus_on_str() { - report_problem_as( - indoc!( - r#" - 0x4 + "foo" - "# - ), - // TODO also suggest fields with the correct type - indoc!( - r#" - ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ - - The 2nd argument to `add` is not what I expect: - - 1│ 0x4 + "foo" - ^^^^^ - - This argument is a string of type: - - Str - - But `add` needs the 2nd argument to be: - - Num (Integer a) - "# - ), - ) - } - - #[test] - fn int_frac() { - report_problem_as( - indoc!( - r#" - 0x4 + 3.14 - "# - ), - indoc!( - r#" - ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ - - The 2nd argument to `add` is not what I expect: - - 1│ 0x4 + 3.14 - ^^^^ - - This argument is a frac of type: - - Frac a - - But `add` needs the 2nd argument to be: - - Num (Integer a) - - Tip: You can convert between Int and Frac using functions like - `Num.toFrac` and `Num.round`. - "# - ), - ) - } - - #[test] - fn boolean_tag() { - report_problem_as( - indoc!( - r#" - 42 + True - "# - ), - indoc!( - r#" - ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ - - The 2nd argument to `add` is not what I expect: - - 1│ 42 + True - ^^^^ - - This `True` boolean has the type: - - [True]a - - But `add` needs the 2nd argument to be: - - Num a - "# - ), - ) - } - - #[test] - fn tag_missing() { - report_problem_as( - indoc!( - r#" - f : [A] -> [A, B] - f = \a -> a - - f - "# - ), - indoc!( - r#" - ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ - - Something is off with the body of the `f` definition: - - 1│ f : [A] -> [A, B] - 2│ f = \a -> a - ^ - - This `a` value is a: - - [A] - - But the type annotation on `f` says it should be: - - [A, B] - - Tip: Looks like a closed tag union does not have the `B` tag. - - Tip: Closed tag unions can't grow, because that might change the size - in memory. Can you use an open tag union? - "# - ), - ) - } - - #[test] - fn tags_missing() { - report_problem_as( - indoc!( - r#" - f : [A] -> [A, B, C] - f = \a -> a - - f - "# - ), - indoc!( - r#" - ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ - - Something is off with the body of the `f` definition: - - 1│ f : [A] -> [A, B, C] - 2│ f = \a -> a - ^ - - This `a` value is a: - - [A] - - But the type annotation on `f` says it should be: - - [A, B, C] - - Tip: Looks like a closed tag union does not have the `B` and `C` tags. - - Tip: Closed tag unions can't grow, because that might change the size - in memory. Can you use an open tag union? - "# - ), - ) - } - - #[test] - fn patterns_fn_not_exhaustive() { - report_problem_as( - indoc!( - r#" - Either : [Left {}, Right Str] - - x : Either - x = Left {} - - f : Either -> {} - f = \Left v -> v - - f x - "# - ), - indoc!( - r#" - ── UNSAFE PATTERN ──────────────────────────────────────── /code/proj/Main.roc ─ - - This pattern does not cover all the possibilities: - - 7│ f = \Left v -> v - ^^^^^^ - - Other possibilities include: - - Right _ - - I would have to crash if I saw one of those! So rather than pattern - matching in function arguments, put a `when` in the function body to - account for all possibilities. - "# - ), - ) - } - - #[test] - fn patterns_let_not_exhaustive() { - report_problem_as( - indoc!( - r#" - x : [Left {}, Right Str] - x = Left {} - - - (Left y) = x - - y - "# - ), - indoc!( - r#" - ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ - - This expression is used in an unexpected way: - - 5│ (Left y) = x - ^ - - This `x` value is a: - - [Left {}, Right Str] - - But you are trying to use it as: - - [Left a] - - Tip: Looks like a closed tag union does not have the `Right` tag. - - Tip: Closed tag unions can't grow, because that might change the size - in memory. Can you use an open tag union? - "# - ), - ) - } - - #[test] - fn patterns_when_not_exhaustive() { - report_problem_as( - indoc!( - r#" - when 0x1 is - 2 -> 0x3 - "# - ), - indoc!( - r#" - ── UNSAFE PATTERN ──────────────────────────────────────── /code/proj/Main.roc ─ - - This `when` does not cover all the possibilities: - - 1│> when 0x1 is - 2│> 2 -> 0x3 - - Other possibilities include: - - _ - - I would have to crash if I saw one of those! Add branches for them! - "# - ), - ) - } - - #[test] - fn patterns_bool_not_exhaustive() { - report_problem_as( - indoc!( - r#" - x : [Red, Green] - x = Green - - when x is - Red -> 3 - "# - ), - indoc!( - r#" - ── UNSAFE PATTERN ──────────────────────────────────────── /code/proj/Main.roc ─ - - This `when` does not cover all the possibilities: - - 4│> when x is - 5│> Red -> 3 - - Other possibilities include: - - Green - - I would have to crash if I saw one of those! Add branches for them! - "# - ), - ) - } - - #[test] - fn patterns_enum_not_exhaustive() { - report_problem_as( - indoc!( - r#" - x : [Red, Green, Blue] - x = Red - - when x is - Red -> 0 - Green -> 1 - "# - ), - indoc!( - r#" - ── UNSAFE PATTERN ──────────────────────────────────────── /code/proj/Main.roc ─ - - This `when` does not cover all the possibilities: - - 4│> when x is - 5│> Red -> 0 - 6│> Green -> 1 - - Other possibilities include: - - Blue - - I would have to crash if I saw one of those! Add branches for them! - "# - ), - ) - } - - #[test] - fn patterns_remote_data_not_exhaustive() { - report_problem_as( - indoc!( - r#" - RemoteData e a : [NotAsked, Loading, Failure e, Success a] - - x : RemoteData Num.I64 Str - - when x is - NotAsked -> 3 - "# - ), - indoc!( - r#" - ── UNSAFE PATTERN ──────────────────────────────────────── /code/proj/Main.roc ─ - - This `when` does not cover all the possibilities: - - 5│> when x is - 6│> NotAsked -> 3 - - Other possibilities include: - - Failure _ - Loading - Success _ - - I would have to crash if I saw one of those! Add branches for them! - "# - ), - ) - } - - #[test] - fn patterns_record_not_exhaustive() { - report_problem_as( - indoc!( - r#" - x = { a: 3 } - - when x is - { a: 4 } -> 4 - "# - ), - // Tip: Looks like a record field guard is not exhaustive. Learn more about record pattern matches at TODO. - indoc!( - r#" - ── UNSAFE PATTERN ──────────────────────────────────────── /code/proj/Main.roc ─ - - This `when` does not cover all the possibilities: - - 3│> when x is - 4│> { a: 4 } -> 4 - - Other possibilities include: - - { a } - - I would have to crash if I saw one of those! Add branches for them! - "# - ), - ) - } - - #[test] - fn patterns_record_guard_not_exhaustive() { - report_problem_as( - indoc!( - r#" - y : [Nothing, Just Num.I64] - y = Just 4 - x = { a: y, b: 42} - - when x is - { a: Nothing } -> 4 - { a: Just 3 } -> 4 - "# - ), - indoc!( - r#" - ── UNSAFE PATTERN ──────────────────────────────────────── /code/proj/Main.roc ─ - - This `when` does not cover all the possibilities: - - 5│> when x is - 6│> { a: Nothing } -> 4 - 7│> { a: Just 3 } -> 4 - - Other possibilities include: - - { a: Just _ } - - I would have to crash if I saw one of those! Add branches for them! - "# - ), - ) - } - - #[test] - fn patterns_nested_tag_not_exhaustive() { - report_problem_as( - indoc!( - r#" - when Record Nothing 1 is - Record (Nothing) b -> b - Record (Just 3) b -> b - "# - ), - indoc!( - r#" - ── UNSAFE PATTERN ──────────────────────────────────────── /code/proj/Main.roc ─ - - This `when` does not cover all the possibilities: - - 1│> when Record Nothing 1 is - 2│> Record (Nothing) b -> b - 3│> Record (Just 3) b -> b - - Other possibilities include: - - Record (Just _) _ - - I would have to crash if I saw one of those! Add branches for them! - "# - ), - ) - } - - #[test] - fn patterns_int_redundant() { - report_problem_as( - indoc!( - r#" - when 0x1 is - 2 -> 3 - 2 -> 4 - _ -> 5 - "# - ), - indoc!( - r#" - ── REDUNDANT PATTERN ───────────────────────────────────── /code/proj/Main.roc ─ - - The 2nd pattern is redundant: - - 1│ when 0x1 is - 2│ 2 -> 3 - 3│> 2 -> 4 - 4│ _ -> 5 - - Any value of this shape will be handled by a previous pattern, so this - one should be removed. - "# - ), - ) - } - - #[test] - fn unify_alias_other() { - report_problem_as( - indoc!( - r#" - Foo a : { x : Num.Int a } - - f : Foo a -> Num.Int a - f = \r -> r.x - - f { y: 3.14 } - "# - ), - // de-aliases the alias to give a better error message - indoc!( - r#" - ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ - - The 1st argument to `f` is not what I expect: - - 6│ f { y: 3.14 } - ^^^^^^^^^^^ - - This argument is a record of type: - - { y : Frac a } - - But `f` needs the 1st argument to be: - - { x : Int a } - - Tip: Seems like a record field typo. Maybe `y` should be `x`? - - Tip: Can more type annotations be added? Type annotations always help - me give more specific messages, and I think they could help a lot in - this case - "# - ), - ) - } - - #[test] - #[ignore] - fn cyclic_alias() { - report_problem_as( - indoc!( - r#" - Foo : { x : Bar } - Bar : { y : Foo } - - f : Foo - - f - "# - ), - // should not report Bar as unused! - indoc!( - r#" - ── CYCLIC ALIAS ────────────────────────────────────────── /code/proj/Main.roc ─ - - The `Foo` alias is recursive in an invalid way: - - 1│ Foo : { x : Bar } - ^^^^^^^^^^^ - - The `Foo` alias depends on itself through the following chain of - definitions: - - ┌─────┐ - │ Foo - │ ↓ - │ Bar - └─────┘ - - Recursion in aliases is only allowed if recursion happens behind a - tag. - - ── SYNTAX PROBLEM ──────────────────────────────────────── /code/proj/Main.roc ─ - - `Bar` is not used anywhere in your code. - - 2│ Bar : { y : Foo } - ^^^^^^^^^^^^^^^^^ - - If you didn't intend on using `Bar` then remove it so future readers of - your code don't wonder why it is there. - "# - ), - ) - } - - #[test] - fn self_recursive_alias() { - report_problem_as( - indoc!( - r#" - Foo : { x : Foo } - - f : Foo - f = 3 - - f - "# - ), - // should not report Bar as unused! - indoc!( - r#" - ── CYCLIC ALIAS ────────────────────────────────────────── /code/proj/Main.roc ─ - - The `Foo` alias is self-recursive in an invalid way: - - 1│ Foo : { x : Foo } - ^^^ - - Recursion in aliases is only allowed if recursion happens behind a - tagged union, at least one variant of which is not recursive. - "# - ), - ) - } - - #[test] - fn record_duplicate_field_same_type() { - report_problem_as( - indoc!( - r#" - { x: 4, y: 3, x: 4 } - "# - ), - indoc!( - r#" - ── DUPLICATE FIELD NAME ────────────────────────────────── /code/proj/Main.roc ─ - - This record defines the `.x` field twice! - - 1│ { x: 4, y: 3, x: 4 } - ^^^^ ^^^^ - - In the rest of the program, I will only use the latter definition: - - 1│ { x: 4, y: 3, x: 4 } - ^^^^ - - For clarity, remove the previous `.x` definitions from this record. - "# - ), - ) - } - - #[test] - fn record_duplicate_field_different_types() { - report_problem_as( - indoc!( - r#" - { x: 4, y: 3, x: "foo" } - "# - ), - indoc!( - r#" - ── DUPLICATE FIELD NAME ────────────────────────────────── /code/proj/Main.roc ─ - - This record defines the `.x` field twice! - - 1│ { x: 4, y: 3, x: "foo" } - ^^^^ ^^^^^^^^ - - In the rest of the program, I will only use the latter definition: - - 1│ { x: 4, y: 3, x: "foo" } - ^^^^^^^^ - - For clarity, remove the previous `.x` definitions from this record. - "# - ), - ) - } - - #[test] - fn record_duplicate_field_multiline() { - report_problem_as( - indoc!( - r#" - { - x: 4, - y: 3, - x: "foo" - } - "# - ), - indoc!( - r#" - ── DUPLICATE FIELD NAME ────────────────────────────────── /code/proj/Main.roc ─ - - This record defines the `.x` field twice! - - 1│ { - 2│> x: 4, - 3│ y: 3, - 4│> x: "foo" - 5│ } - - In the rest of the program, I will only use the latter definition: - - 1│ { - 2│ x: 4, - 3│ y: 3, - 4│> x: "foo" - 5│ } - - For clarity, remove the previous `.x` definitions from this record. - "# - ), - ) - } - - #[test] - fn record_update_duplicate_field_multiline() { - report_problem_as( - indoc!( - r#" - \r -> - { r & - x: 4, - y: 3, - x: "foo" - } - "# - ), - indoc!( - r#" - ── DUPLICATE FIELD NAME ────────────────────────────────── /code/proj/Main.roc ─ - - This record defines the `.x` field twice! - - 2│ { r & - 3│> x: 4, - 4│ y: 3, - 5│> x: "foo" - 6│ } - - In the rest of the program, I will only use the latter definition: - - 2│ { r & - 3│ x: 4, - 4│ y: 3, - 5│> x: "foo" - 6│ } - - For clarity, remove the previous `.x` definitions from this record. - "# - ), - ) - } - - #[test] - fn record_type_duplicate_field() { - report_problem_as( - indoc!( - r#" - a : { foo : Num.I64, bar : {}, foo : Str } - a = { bar: {}, foo: "foo" } - - a - "# - ), - indoc!( - r#" - ── DUPLICATE FIELD NAME ────────────────────────────────── /code/proj/Main.roc ─ - - This record type defines the `.foo` field twice! - - 1│ a : { foo : Num.I64, bar : {}, foo : Str } - ^^^^^^^^^^^^^ ^^^^^^^^^ - - In the rest of the program, I will only use the latter definition: - - 1│ a : { foo : Num.I64, bar : {}, foo : Str } - ^^^^^^^^^ - - For clarity, remove the previous `.foo` definitions from this record - type. - "# - ), - ) - } - - #[test] - fn tag_union_duplicate_tag() { - report_problem_as( - indoc!( - r#" - a : [Foo Num.I64, Bar {}, Foo Str] - a = Foo "foo" - - a - "# - ), - indoc!( - r#" - ── DUPLICATE TAG NAME ──────────────────────────────────── /code/proj/Main.roc ─ - - This tag union type defines the `Foo` tag twice! - - 1│ a : [Foo Num.I64, Bar {}, Foo Str] - ^^^^^^^^^^^ ^^^^^^^ - - In the rest of the program, I will only use the latter definition: - - 1│ a : [Foo Num.I64, Bar {}, Foo Str] - ^^^^^^^ - - For clarity, remove the previous `Foo` definitions from this tag union - type. - "# - ), - ) - } - - #[test] - fn annotation_definition_mismatch() { - report_problem_as( - indoc!( - r#" - bar : Num.I64 - foo = \x -> x - - # NOTE: neither bar or foo are defined at this point - 4 - "# - ), - indoc!( - r#" - ── NAMING PROBLEM ──────────────────────────────────────── /code/proj/Main.roc ─ - - This annotation does not match the definition immediately following - it: - - 1│> bar : Num.I64 - 2│> foo = \x -> x - - Is it a typo? If not, put either a newline or comment between them. - "# - ), - ) - } - - #[test] - fn annotation_newline_body_is_fine() { - report_problem_as( - indoc!( - r#" - bar : Num.I64 - - foo = \x -> x - - foo bar - "# - ), - indoc!(""), - ) - } - - #[test] - fn invalid_alias_rigid_var_pattern() { - report_problem_as( - indoc!( - r#" - MyAlias 1 : Num.I64 - - 4 - "# - ), - indoc!( - r#" - ── SYNTAX PROBLEM ──────────────────────────────────────── /code/proj/Main.roc ─ - - This pattern in the definition of `MyAlias` is not what I expect: - - 1│ MyAlias 1 : Num.I64 - ^ - - Only type variables like `a` or `value` can occur in this position. - - ── UNUSED DEFINITION ───────────────────────────────────── /code/proj/Main.roc ─ - - `MyAlias` is not used anywhere in your code. - - 1│ MyAlias 1 : Num.I64 - ^^^^^^^^^^^^^^^^^^^ - - If you didn't intend on using `MyAlias` then remove it so future readers - of your code don't wonder why it is there. - "# - ), - ) - } - - #[test] - fn invalid_opaque_rigid_var_pattern() { - report_problem_as( - indoc!( - r#" - Age 1 := Num.I64 - - a : Age - a - "# - ), - indoc!( - r#" - ── SYNTAX PROBLEM ──────────────────────────────────────── /code/proj/Main.roc ─ - - This pattern in the definition of `Age` is not what I expect: - - 1│ Age 1 := Num.I64 - ^ - - Only type variables like `a` or `value` can occur in this position. - "# - ), - ) - } - - #[test] - fn invalid_num() { - report_problem_as( - indoc!( - r#" - a : Num.Num Num.I64 Num.F64 - a = 3 - - a - "# - ), - indoc!( - r#" - ── TOO MANY TYPE ARGUMENTS ─────────────────────────────── /code/proj/Main.roc ─ - - The `Num` alias expects 1 type argument, but it got 2 instead: - - 1│ a : Num.Num Num.I64 Num.F64 - ^^^^^^^^^^^^^^^^^^^^^^^ - - Are there missing parentheses? - "# - ), - ) - } - - #[test] - fn invalid_num_fn() { - report_problem_as( - indoc!( - r#" - f : Str -> Num.Num Num.I64 Num.F64 - f = \_ -> 3 - - f - "# - ), - indoc!( - r#" - ── TOO MANY TYPE ARGUMENTS ─────────────────────────────── /code/proj/Main.roc ─ - - The `Num` alias expects 1 type argument, but it got 2 instead: - - 1│ f : Str -> Num.Num Num.I64 Num.F64 - ^^^^^^^^^^^^^^^^^^^^^^^ - - Are there missing parentheses? - "# - ), - ) - } - - #[test] - fn too_few_type_arguments() { - report_problem_as( - indoc!( - r#" - Pair a b : [Pair a b] - - x : Pair Num.I64 - x = Pair 2 3 - - x - "# - ), - indoc!( - r#" - ── TOO FEW TYPE ARGUMENTS ──────────────────────────────── /code/proj/Main.roc ─ - - The `Pair` alias expects 2 type arguments, but it got 1 instead: - - 3│ x : Pair Num.I64 - ^^^^^^^^^^^^ - - Are there missing parentheses? - "# - ), - ) - } - - #[test] - fn too_many_type_arguments() { - report_problem_as( - indoc!( - r#" - Pair a b : [Pair a b] - - x : Pair Num.I64 Num.I64 Num.I64 - x = 3 - - x - "# - ), - indoc!( - r#" - ── TOO MANY TYPE ARGUMENTS ─────────────────────────────── /code/proj/Main.roc ─ - - The `Pair` alias expects 2 type arguments, but it got 3 instead: - - 3│ x : Pair Num.I64 Num.I64 Num.I64 - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - - Are there missing parentheses? - "# - ), - ) - } - - #[test] - fn phantom_type_variable() { - report_problem_as( - indoc!( - r#" - Foo a : [Foo] - - f : Foo Num.I64 - - f - "# - ), - indoc!( - r#" - ── UNUSED TYPE ALIAS PARAMETER ─────────────────────────── /code/proj/Main.roc ─ - - The `a` type parameter is not used in the `Foo` alias definition: - - 1│ Foo a : [Foo] - ^ - - Roc does not allow unused type alias parameters! - - Tip: If you want an unused type parameter (a so-called "phantom - type"), read the guide section on phantom values. - "# - ), - ) - } - - #[test] - fn elm_function_syntax() { - report_problem_as( - indoc!( - r#" - f x y = x - "# - ), - indoc!( - r#" - ── ARGUMENTS BEFORE EQUALS ─────────────────────────────── /code/proj/Main.roc ─ - - I am partway through parsing a definition, but I got stuck here: - - 1│ f x y = x - ^^^ - - Looks like you are trying to define a function. In roc, functions are - always written as a lambda, like increment = \n -> n + 1. - "# - ), - ) - } - - #[test] - fn two_different_cons() { - report_problem_as( - indoc!( - r#" - ConsList a : [Cons a (ConsList a), Nil] - - x : ConsList {} - x = Cons {} (Cons "foo" Nil) - - x - "# - ), - indoc!( - r#" - ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ - - Something is off with the body of the `x` definition: - - 3│ x : ConsList {} - 4│ x = Cons {} (Cons "foo" Nil) - ^^^^^^^^^^^^^^^^^^^^^^^^ - - This `Cons` tag application has the type: - - [Cons {} [Cons Str [Cons {} a, Nil] as a, Nil], Nil] - - But the type annotation on `x` says it should be: - - [Cons {} a, Nil] as a - "# - ), - ) - } - - #[test] - fn mutually_recursive_types_with_type_error() { - report_problem_as( - indoc!( - r#" - AList a b : [ACons a (BList a b), ANil] - BList a b : [BCons a (AList a b), BNil] - - x : AList Num.I64 Num.I64 - x = ACons 0 (BCons 1 (ACons "foo" BNil )) - - y : BList a a - y = BNil - - { x, y } - "# - ), - // TODO render tag unions across multiple lines - // TODO do not show recursion var if the recursion var does not render on the surface of a type - indoc!( - r#" - ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ - - Something is off with the body of the `x` definition: - - 4│ x : AList Num.I64 Num.I64 - 5│ x = ACons 0 (BCons 1 (ACons "foo" BNil )) - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - - This `ACons` tag application has the type: - - [ACons (Num (Integer Signed64)) [BCons (Num (Integer Signed64)) [ACons Str [BCons I64 [ACons I64 (BList I64 I64), - ANil] as ∞, BNil], ANil], BNil], ANil] - - But the type annotation on `x` says it should be: - - [ACons I64 (BList I64 I64), ANil] as a - "# - ), - ) - } - - #[test] - fn integer_out_of_range() { - report_problem_as( - indoc!( - r#" - x = 170_141_183_460_469_231_731_687_303_715_884_105_728_000 - - y = -170_141_183_460_469_231_731_687_303_715_884_105_728_000 - - h = 0xFFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF - l = -0xFFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF - - minlit = -170_141_183_460_469_231_731_687_303_715_884_105_728 - maxlit = 340_282_366_920_938_463_463_374_607_431_768_211_455 - - x + y + h + l + minlit + maxlit - "# - ), - indoc!( - r#" - ── SYNTAX PROBLEM ──────────────────────────────────────── /code/proj/Main.roc ─ - - This integer literal is too big: - - 1│ x = 170_141_183_460_469_231_731_687_303_715_884_105_728_000 - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - - The largest number representable in Roc is the maximum U128 value, - 340_282_366_920_938_463_463_374_607_431_768_211_455. - - Tip: Learn more about number literals at TODO - - ── SYNTAX PROBLEM ──────────────────────────────────────── /code/proj/Main.roc ─ - - This integer literal is too small: - - 3│ y = -170_141_183_460_469_231_731_687_303_715_884_105_728_000 - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - - The smallest number representable in Roc is the minimum I128 value, - -170_141_183_460_469_231_731_687_303_715_884_105_728. - - Tip: Learn more about number literals at TODO - - ── SYNTAX PROBLEM ──────────────────────────────────────── /code/proj/Main.roc ─ - - This integer literal is too big: - - 5│ h = 0xFFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - - The largest number representable in Roc is the maximum U128 value, - 340_282_366_920_938_463_463_374_607_431_768_211_455. - - Tip: Learn more about number literals at TODO - - ── SYNTAX PROBLEM ──────────────────────────────────────── /code/proj/Main.roc ─ - - This integer literal is too small: - - 6│ l = -0xFFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - - The smallest number representable in Roc is the minimum I128 value, - -170_141_183_460_469_231_731_687_303_715_884_105_728. - - Tip: Learn more about number literals at TODO - "# - ), - ) - } - - #[test] - fn float_out_of_range() { - // have to deal with some whitespace issues because of the format! macro - report_problem_as( - indoc!( - r#" - overflow = 11.7976931348623157e308 - underflow = -11.7976931348623157e308 - - overflow + underflow - "# - ), - indoc!( - r#" - ── SYNTAX PROBLEM ──────────────────────────────────────── /code/proj/Main.roc ─ - - This float literal is too big: - - 1│ overflow = 11.7976931348623157e308 - ^^^^^^^^^^^^^^^^^^^^^^^ - - Roc uses signed 64-bit floating points, allowing values between - -1.7976931348623157e308 and 1.7976931348623157e308 - - Tip: Learn more about number literals at TODO - - ── SYNTAX PROBLEM ──────────────────────────────────────── /code/proj/Main.roc ─ - - This float literal is too small: - - 2│ underflow = -11.7976931348623157e308 - ^^^^^^^^^^^^^^^^^^^^^^^^ - - Roc uses signed 64-bit floating points, allowing values between - -1.7976931348623157e308 and 1.7976931348623157e308 - - Tip: Learn more about number literals at TODO - "# - ), - ) - } - - #[test] - fn integer_malformed() { - // the generated messages here are incorrect. Waiting for a rust nightly feature to land, - // see https://github.com/rust-lang/rust/issues/22639 - // this test is here to spot regressions in error reporting - report_problem_as( - indoc!( - r#" - dec = 100A - - hex = 0xZZZ - - oct = 0o9 - - bin = 0b2 - - dec + hex + oct + bin - "# - ), - indoc!( - r#" - ── SYNTAX PROBLEM ──────────────────────────────────────── /code/proj/Main.roc ─ - - This integer literal contains an invalid digit: - - 1│ dec = 100A - ^^^^ - - Integer literals can only contain the digits - 0-9, or have an integer suffix. - - Tip: Learn more about number literals at TODO - - ── SYNTAX PROBLEM ──────────────────────────────────────── /code/proj/Main.roc ─ - - This hex integer literal contains an invalid digit: - - 3│ hex = 0xZZZ - ^^^^^ - - Hexadecimal (base-16) integer literals can only contain the digits - 0-9, a-f and A-F, or have an integer suffix. - - Tip: Learn more about number literals at TODO - - ── SYNTAX PROBLEM ──────────────────────────────────────── /code/proj/Main.roc ─ - - This octal integer literal contains an invalid digit: - - 5│ oct = 0o9 - ^^^ - - Octal (base-8) integer literals can only contain the digits - 0-7, or have an integer suffix. - - Tip: Learn more about number literals at TODO - - ── SYNTAX PROBLEM ──────────────────────────────────────── /code/proj/Main.roc ─ - - This binary integer literal contains an invalid digit: - - 7│ bin = 0b2 - ^^^ - - Binary (base-2) integer literals can only contain the digits - 0 and 1, or have an integer suffix. - - Tip: Learn more about number literals at TODO - "# - ), - ) - } - - #[test] - fn integer_empty() { - report_problem_as( - indoc!( - r#" - dec = 20 - - hex = 0x - - oct = 0o - - bin = 0b - - dec + hex + oct + bin - "# - ), - indoc!( - r#" - ── SYNTAX PROBLEM ──────────────────────────────────────── /code/proj/Main.roc ─ - - This hex integer literal contains no digits: - - 3│ hex = 0x - ^^ - - Hexadecimal (base-16) integer literals must contain at least one of - the digits 0-9, a-f and A-F, or have an integer suffix. - - Tip: Learn more about number literals at TODO - - ── SYNTAX PROBLEM ──────────────────────────────────────── /code/proj/Main.roc ─ - - This octal integer literal contains no digits: - - 5│ oct = 0o - ^^ - - Octal (base-8) integer literals must contain at least one of the - digits 0-7, or have an integer suffix. - - Tip: Learn more about number literals at TODO - - ── SYNTAX PROBLEM ──────────────────────────────────────── /code/proj/Main.roc ─ - - This binary integer literal contains no digits: - - 7│ bin = 0b - ^^ - - Binary (base-2) integer literals must contain at least one of the - digits 0 and 1, or have an integer suffix. - - Tip: Learn more about number literals at TODO - "# - ), - ) - } - - #[test] - fn float_malformed() { - report_problem_as( - indoc!( - r#" - x = 3.0A - - x - "# - ), - indoc!( - r#" - ── SYNTAX PROBLEM ──────────────────────────────────────── /code/proj/Main.roc ─ - - This float literal contains an invalid digit: - - 1│ x = 3.0A - ^^^^ - - Floating point literals can only contain the digits 0-9, or use - scientific notation 10e4, or have a float suffix. - - Tip: Learn more about number literals at TODO - "# - ), - ) - } - - #[test] - fn invalid_record_update() { - report_problem_as( - indoc!( - r#" - foo = { bar: 3 } - updateNestedRecord = { foo.bar & x: 4 } - - example = { age: 42 } - - # these should work - y = { Test.example & age: 3 } - x = { example & age: 4 } - - { updateNestedRecord, foo, x, y } - "# - ), - indoc!( - r#" - ── SYNTAX PROBLEM ──────────────────────────────────────── /code/proj/Main.roc ─ - - This expression cannot be updated: - - 2│ updateNestedRecord = { foo.bar & x: 4 } - ^^^^^^^ - - Only variables can be updated with record update syntax. - "# - ), - ) - } - - #[test] - fn module_not_imported() { - report_problem_as( - indoc!( - r#" - Foo.test - "# - ), - indoc!( - r#" - ── MODULE NOT IMPORTED ─────────────────────────────────── /code/proj/Main.roc ─ - - The `Foo` module is not imported: - - 1│ Foo.test - ^^^^^^^^ - - Is there an import missing? Perhaps there is a typo. Did you mean one - of these? - - Box - Bool - Num - Set - "# - ), - ) - } - - #[test] - fn optional_record_default_type_error() { - report_problem_as( - indoc!( - r#" - \{ x, y ? True } -> x + y - "# - ), - indoc!( - r#" - ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ - - The 2nd argument to `add` is not what I expect: - - 1│ \{ x, y ? True } -> x + y - ^ - - This `y` value is a: - - [True]a - - But `add` needs the 2nd argument to be: - - Num a - "# - ), - ) - } - - #[test] - fn optional_record_default_with_signature() { - report_problem_as( - indoc!( - r#" - f : { x : Num.I64, y ? Num.I64 } -> Num.I64 - f = \{ x, y ? "foo" } -> (\g, _ -> g) x y - - f - "# - ), - indoc!( - r#" - ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ - - The 1st argument to `f` is weird: - - 2│ f = \{ x, y ? "foo" } -> (\g, _ -> g) x y - ^^^^^^^^^^^^^^^^ - - The argument is a pattern that matches record values of type: - - { x : I64, y ? Str } - - But the annotation on `f` says the 1st argument should be: - - { x : I64, y ? I64 } - "# - ), - ) - } - - #[test] - fn optional_record_invalid_let_binding() { - report_problem_as( - indoc!( - r#" - \rec -> - { x, y } : { x : Num.I64, y ? Str } - { x, y } = rec - - { x, y } - "# - ), - indoc!( - r#" - ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ - - Something is off with the body of this definition: - - 2│> { x, y } : { x : Num.I64, y ? Str } - 3│> { x, y } = rec - - The body is a value of type: - - { x : I64, y : Str } - - But the type annotation says it should be: - - { x : I64, y ? Str } - - Tip: To extract the `.y` field it must be non-optional, but the type - says this field is optional. Learn more about optional fields at TODO. - "# - ), - ) - } - - #[test] - fn optional_record_invalid_function() { - report_problem_as( - indoc!( - r#" - f : { x : Num.I64, y ? Num.I64 } -> Num.I64 - f = \{ x, y } -> x + y - - f - "# - ), - indoc!( - r#" - ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ - - The 1st argument to `f` is weird: - - 2│ f = \{ x, y } -> x + y - ^^^^^^^^ - - The argument is a pattern that matches record values of type: - - { x : I64, y : I64 } - - But the annotation on `f` says the 1st argument should be: - - { x : I64, y ? I64 } - - Tip: To extract the `.y` field it must be non-optional, but the type - says this field is optional. Learn more about optional fields at TODO. - "# - ), - ) - } - - #[test] - fn optional_record_invalid_when() { - report_problem_as( - indoc!( - r#" - f : { x : Num.I64, y ? Num.I64 } -> Num.I64 - f = \r -> - when r is - { x, y } -> x + y - - f - "# - ), - indoc!( - r#" - ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ - - The branches of this `when` expression don't match the condition: - - 3│> when r is - 4│ { x, y } -> x + y - - This `r` value is a: - - { x : I64, y ? I64 } - - But the branch patterns have type: - - { x : I64, y : I64 } - - The branches must be cases of the `when` condition's type! - - Tip: To extract the `.y` field it must be non-optional, but the type - says this field is optional. Learn more about optional fields at TODO. - "# - ), - ) - } - - #[test] - fn optional_record_invalid_access() { - report_problem_as( - indoc!( - r#" - f : { x : Num.I64, y ? Num.I64 } -> Num.I64 - f = \r -> r.y - - f - "# - ), - indoc!( - r#" - ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ - - This expression is used in an unexpected way: - - 2│ f = \r -> r.y - ^^^ - - This `r` value is a: - - { x : I64, y ? I64 } - - But you are trying to use it as: - - { x : I64, y : I64 } - - Tip: To extract the `.y` field it must be non-optional, but the type - says this field is optional. Learn more about optional fields at TODO. - "# - ), - ) - } - - #[test] - fn optional_record_invalid_accessor() { - report_problem_as( - indoc!( - r#" - f : { x : Num.I64, y ? Num.I64 } -> Num.I64 - f = \r -> .y r - - f - "# - ), - indoc!( - r#" - ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ - - The 1st argument to this function is not what I expect: - - 2│ f = \r -> .y r - ^ - - This `r` value is a: - - { x : I64, y ? I64 } - - But this function needs the 1st argument to be: - - { x : I64, y : I64 } - - Tip: To extract the `.y` field it must be non-optional, but the type - says this field is optional. Learn more about optional fields at TODO. - "# - ), - ) - } - - #[test] - fn guard_mismatch_with_annotation() { - report_problem_as( - indoc!( - r#" - f : { x : Num.I64, y : Num.I64 } -> Num.I64 - f = \r -> - when r is - { x, y : "foo" } -> x + 0 - _ -> 0 - - f - "# - ), - indoc!( - r#" - ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ - - The branches of this `when` expression don't match the condition: - - 3│> when r is - 4│ { x, y : "foo" } -> x + 0 - 5│ _ -> 0 - - This `r` value is a: - - { x : I64, y : I64 } - - But the branch patterns have type: - - { x : I64, y : Str } - - The branches must be cases of the `when` condition's type! - "# - ), - ) - } - - #[test] - fn optional_field_mismatch_with_annotation() { - report_problem_as( - indoc!( - r#" - f : { x : Num.I64, y ? Num.I64 } -> Num.I64 - f = \r -> - when r is - { x, y ? "foo" } -> (\g, _ -> g) x y - _ -> 0 - - f - "# - ), - indoc!( - r#" - ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ - - The branches of this `when` expression don't match the condition: - - 3│> when r is - 4│ { x, y ? "foo" } -> (\g, _ -> g) x y - 5│ _ -> 0 - - This `r` value is a: - - { x : I64, y ? I64 } - - But the branch patterns have type: - - { x : I64, y ? Str } - - The branches must be cases of the `when` condition's type! - "# - ), - ) - } - - #[test] - fn incorrect_optional_field() { - report_problem_as( - indoc!( - r#" - { x: 5, y ? 42 } - "# - ), - indoc!( - r#" - ── BAD OPTIONAL VALUE ──────────────────────────────────── /code/proj/Main.roc ─ - - This record uses an optional value for the `.y` field in an incorrect - context! - - 1│ { x: 5, y ? 42 } - ^^^^^^ - - You can only use optional values in record destructuring, like: - - { answer ? 42, otherField } = myRecord - "# - ), - ) - } - #[test] - fn first_wildcard_is_required() { - report_problem_as( - indoc!( - r#" - when Foo 1 2 3 is - Foo _ 1 _ -> 1 - _ -> 2 - "# - ), - "", - ) - } - - #[test] - fn second_wildcard_is_redundant() { - report_problem_as( - indoc!( - r#" - when Foo 1 2 3 is - Foo _ 1 _ -> 1 - _ -> 2 - _ -> 3 - "# - ), - indoc!( - r#" - ── REDUNDANT PATTERN ───────────────────────────────────── /code/proj/Main.roc ─ - - The 3rd pattern is redundant: - - 1│ when Foo 1 2 3 is - 2│ Foo _ 1 _ -> 1 - 3│ _ -> 2 - 4│ _ -> 3 - ^ - - Any value of this shape will be handled by a previous pattern, so this - one should be removed. - "# - ), - ) - } - - #[test] - fn alias_using_alias() { - report_problem_as( - indoc!( - r#" - # The color of a node. Leaves are considered Black. - NodeColor : [Red, Black] - - RBTree k v : [Node NodeColor k v (RBTree k v) (RBTree k v), Empty] - - # Create an empty dictionary. - empty : RBTree k v - empty = - Empty - - empty - "# - ), - "", - ) - } - - #[test] - fn unused_argument() { - report_problem_as( - indoc!( - r#" - f = \foo -> 1 - - f - "# - ), - indoc!( - r#" - ── UNUSED ARGUMENT ─────────────────────────────────────── /code/proj/Main.roc ─ - - `f` doesn't use `foo`. - - 1│ f = \foo -> 1 - ^^^ - - If you don't need `foo`, then you can just remove it. However, if you - really do need `foo` as an argument of `f`, prefix it with an underscore, - like this: "_`foo`". Adding an underscore at the start of a variable - name is a way of saying that the variable is not used. - "# - ), - ) - } - - #[test] - fn qualified_tag() { - report_problem_as( - indoc!( - r#" - Foo.Bar - "# - ), - indoc!( - r#" - ── SYNTAX PROBLEM ──────────────────────────────────────── /code/proj/Main.roc ─ - - I am trying to parse a qualified name here: - - 1│ Foo.Bar - ^ - - This looks like a qualified tag name to me, but tags cannot be - qualified! Maybe you wanted a qualified name, something like - Json.Decode.string? - "# - ), - ) - } - - #[test] - fn module_ident_ends_with_dot() { - report_problem_as( - indoc!( - r#" - Foo.Bar. - "# - ), - indoc!( - r#" - ── SYNTAX PROBLEM ──────────────────────────────────────── /code/proj/Main.roc ─ - - I am trying to parse a qualified name here: - - 1│ Foo.Bar. - ^ - - I was expecting to see an identifier next, like height. A complete - qualified name looks something like Json.Decode.string. - "# - ), - ) - } - - #[test] - fn record_access_ends_with_dot() { - report_problem_as( - indoc!( - r#" - foo.bar. - "# - ), - indoc!( - r#" - ── SYNTAX PROBLEM ──────────────────────────────────────── /code/proj/Main.roc ─ - - I trying to parse a record field access here: - - 1│ foo.bar. - ^ - - So I expect to see a lowercase letter next, like .name or .height. - "# - ), - ) - } - - #[test] - fn type_annotation_double_colon() { - report_problem_as( - indoc!( - r#" - f :: I64 - f = 42 - - f - "# - ), - indoc!( - r#" - ── UNKNOWN OPERATOR ────────────────────────────────────── /code/proj/Main.roc ─ - - This looks like an operator, but it's not one I recognize! - - 1│ f :: I64 - ^^ - - I have no specific suggestion for this operator, see TODO for the full - list of operators in Roc. - "# - ), - ) - } - - #[test] - fn double_equals_in_def() { - // NOTE: VERY BAD ERROR MESSAGE - // - // looks like `x y` are considered argument to the add, even though they are - // on a lower indentation level - report_problem_as( - indoc!( - r#" - x = 3 - y = - x == 5 - Num.add 1 2 - - { x, y } - "# - ), - indoc!( - r#" - ── TOO MANY ARGS ───────────────────────────────────────── /code/proj/Main.roc ─ - - This value is not a function, but it was given 3 arguments: - - 3│ x == 5 - ^ - - Are there any missing commas? Or missing parentheses? - "# - ), - ) - } - - #[test] - fn tag_union_open() { - report_problem_as( - indoc!( - r#" - f : [ - "# - ), - indoc!( - r#" - ── UNFINISHED TAG UNION TYPE ───────────────────────────── /code/proj/Main.roc ─ - - I just started parsing a tag union type, but I got stuck here: - - 1│ f : [ - ^ - - Tag unions look like [Many I64, None], so I was expecting to see a tag - name next. - "# - ), - ) - } - - #[test] - fn tag_union_end() { - report_problem_as( - indoc!( - r#" - f : [Yes, - "# - ), - indoc!( - r#" - ── UNFINISHED TAG UNION TYPE ───────────────────────────── /code/proj/Main.roc ─ - - I am partway through parsing a tag union type, but I got stuck here: - - 1│ f : [Yes, - ^ - - I was expecting to see a closing square bracket before this, so try - adding a ] and see if that helps? - "# - ), - ) - } - - #[test] - fn tag_union_lowercase_tag_name() { - report_problem_as( - indoc!( - r#" - f : [lowercase] - "# - ), - indoc!( - r#" - ── WEIRD TAG NAME ──────────────────────────────────────── /code/proj/Main.roc ─ - - I am partway through parsing a tag union type, but I got stuck here: - - 1│ f : [lowercase] - ^ - - I was expecting to see a tag name. - - Hint: Tag names start with an uppercase letter, like Err or Green. - "# - ), - ) - } - - #[test] - fn tag_union_second_lowercase_tag_name() { - report_problem_as( - indoc!( - r#" - f : [Good, bad] - "# - ), - indoc!( - r#" - ── WEIRD TAG NAME ──────────────────────────────────────── /code/proj/Main.roc ─ - - I am partway through parsing a tag union type, but I got stuck here: - - 1│ f : [Good, bad] - ^ - - I was expecting to see a tag name. - - Hint: Tag names start with an uppercase letter, like Err or Green. - "# - ), - ) - } - - #[test] - fn record_type_open() { - report_problem_as( - indoc!( - r#" - f : { - "# - ), - indoc!( - r#" - ── UNFINISHED RECORD TYPE ──────────────────────────────── /code/proj/Main.roc ─ - - I just started parsing a record type, but I got stuck here: - - 1│ f : { - ^ - - Record types look like { name : String, age : Int }, so I was - expecting to see a field name next. - "# - ), - ) - } - - #[test] - fn record_type_open_indent() { - report_problem_as( - indoc!( - r#" - f : { - foo : I64, - "# - ), - indoc!( - r#" - ── UNFINISHED RECORD TYPE ──────────────────────────────── /code/proj/Main.roc ─ - - I am partway through parsing a record type, but I got stuck here: - - 1│ f : { - ^ - - I was expecting to see a closing curly brace before this, so try - adding a } and see if that helps? - - Note: I may be confused by indentation - "# - ), - ) - } - - #[test] - fn record_type_end() { - report_problem_as( - indoc!( - r#" - f : { a: Int, - "# - ), - indoc!( - r#" - ── UNFINISHED RECORD TYPE ──────────────────────────────── /code/proj/Main.roc ─ - - I am partway through parsing a record type, but I got stuck here: - - 1│ f : { a: Int, - ^ - - I was expecting to see a closing curly brace before this, so try - adding a } and see if that helps? - "# - ), - ) - } - - #[test] - fn record_type_keyword_field_name() { - report_problem_as( - indoc!( - r#" - f : { if : I64 } - "# - ), - indoc!( - r#" - ── UNFINISHED RECORD TYPE ──────────────────────────────── /code/proj/Main.roc ─ - - I just started parsing a record type, but I got stuck on this field - name: - - 1│ f : { if : I64 } - ^^ - - Looks like you are trying to use `if` as a field name, but that is a - reserved word. Try using a different name! - "# - ), - ) - } - - #[test] - fn record_type_missing_comma() { - // a case where the message cannot be as good as elm's - report_problem_as( - indoc!( - r#" - f : { foo bar } - "# - ), - indoc!( - r#" - ── UNFINISHED RECORD TYPE ──────────────────────────────── /code/proj/Main.roc ─ - - I am partway through parsing a record type, but I got stuck here: - - 1│ f : { foo bar } - ^ - - I was expecting to see a colon, question mark, comma or closing curly - brace. - "# - ), - ) - } - - #[test] - fn record_type_tab() { - // a case where the message cannot be as good as elm's - report_problem_as( - "f : { foo \t }", - indoc!( - r#" - ── TAB CHARACTER ───────────────────────────────────────── /code/proj/Main.roc ─ - - I encountered a tab character - - 1│ f : { foo } - ^ - - Tab characters are not allowed. - "# - ), - ) - } - - #[test] - fn comment_with_tab() { - report_problem_as( - "# comment with a \t\n4", - indoc!( - " - ── TAB CHARACTER ───────────────────────────────────────── /code/proj/Main.roc ─ - - I encountered a tab character - - 1│ # comment with a \t - ^ - - Tab characters are not allowed. - " - ), - ) - } - - #[test] - fn type_in_parens_start() { - // TODO bad error message - report_problem_as( - indoc!( - r#" - f : ( - "# - ), - indoc!( - r#" - ── UNFINISHED TYPE ─────────────────────────────────────── /code/proj/Main.roc ─ - - I just started parsing a type, but I got stuck here: - - 1│ f : ( - ^ - - I am expecting a type next, like Bool or List a. - "# - ), - ) - } - - #[test] - fn type_in_parens_end() { - report_problem_as( - indoc!( - r#" - f : ( I64 - "# - ), - indoc!( - r#" - ── UNFINISHED PARENTHESES ──────────────────────────────── /code/proj/Main.roc ─ - - I am partway through parsing a type in parentheses, but I got stuck - here: - - 1│ f : ( I64 - ^ - - I was expecting to see a parenthesis before this, so try adding a ) - and see if that helps? - - Note: I may be confused by indentation - "# - ), - ) - } - - #[test] - fn type_apply_double_dot() { - report_problem_as( - indoc!( - r#" - f : Foo..Bar - - f - "# - ), - indoc!( - r#" - ── SYNTAX PROBLEM ──────────────────────────────────────── /code/proj/Main.roc ─ - - I am confused by this type name: - - 1│ f : Foo..Bar - ^^^^^^^^ - - Type names start with an uppercase letter, and can optionally be - qualified by a module name, like Bool or Http.Request.Request. - "# - ), - ) - - // ── DOUBLE DOT ────────────────────────────────────────────────────────────────── - // - // I encountered two dots in a row: - // - // 1│ f : Foo..Bar - // ^ - // - // Try removing one of them. - } - - #[test] - fn type_apply_trailing_dot() { - report_problem_as( - indoc!( - r#" - f : Foo.Bar. - - f - "# - ), - indoc!( - r#" - ── SYNTAX PROBLEM ──────────────────────────────────────── /code/proj/Main.roc ─ - - I am confused by this type name: - - 1│ f : Foo.Bar. - ^^^^^^^^ - - Type names start with an uppercase letter, and can optionally be - qualified by a module name, like Bool or Http.Request.Request. - "# - ), - ) - - // ── TRAILING DOT ──────────────────────────────────────────────────────────────── - // - // I encountered a dot with nothing after it: - // - // 1│ f : Foo.Bar. - // ^ - // - // Dots are used to refer to a type in a qualified way, like - // Num.I64 or List.List a. Try adding a type name next. - } - - #[test] - fn type_apply_stray_dot() { - report_problem_as( - indoc!( - r#" - f : . - "# - ), - indoc!( - r#" - ── UNFINISHED TYPE ─────────────────────────────────────── /code/proj/Main.roc ─ - - I just started parsing a type, but I got stuck here: - - 1│ f : . - ^ - - I am expecting a type next, like Bool or List a. - "# - ), - ) - } - - #[test] - fn type_apply_start_with_number() { - report_problem_as( - indoc!( - r#" - f : Foo.1 - - f - "# - ), - indoc!( - r#" - ── SYNTAX PROBLEM ──────────────────────────────────────── /code/proj/Main.roc ─ - - I am confused by this type name: - - 1│ f : Foo.1 - ^^^^^ - - Type names start with an uppercase letter, and can optionally be - qualified by a module name, like Bool or Http.Request.Request. - "# - ), - ) - - // ── WEIRD QUALIFIED NAME ──────────────────────────────────────────────────────── - // - // I encountered a number at the start of a qualified name segment: - // - // 1│ f : Foo.1 - // ^ - // - // All parts of a qualified type name must start with an uppercase - // letter, like Num.I64 or List.List a. - } - - #[test] - fn type_apply_start_with_lowercase() { - report_problem_as( - indoc!( - r#" - f : Foo.foo - - f - "# - ), - indoc!( - r#" - ── SYNTAX PROBLEM ──────────────────────────────────────── /code/proj/Main.roc ─ - - I am confused by this type name: - - 1│ f : Foo.foo - ^^^^^^^ - - Type names start with an uppercase letter, and can optionally be - qualified by a module name, like Bool or Http.Request.Request. - "# - ), - ) - } - - #[test] - fn def_missing_final_expression() { - report_problem_as( - indoc!( - r#" - f : Foo.foo - "# - ), - indoc!( - r#" - ── MISSING FINAL EXPRESSION ────────────────────────────── /code/proj/Main.roc ─ - - I am partway through parsing a definition's final expression, but I - got stuck here: - - 1│ f : Foo.foo - ^ - - This definition is missing a final expression. A nested definition - must be followed by either another definition, or an expression - - x = 4 - y = 2 - - x + y - "# - ), - ) - } - - #[test] - fn type_inline_alias() { - report_problem_as( - indoc!( - r#" - f : I64 as - f = 0 - - f - "# - ), - indoc!( - r#" - ── UNFINISHED INLINE ALIAS ─────────────────────────────── /code/proj/Main.roc ─ - - I just started parsing an inline type alias, but I got stuck here: - - 1│ f : I64 as - ^ - - Note: I may be confused by indentation - "# - ), - ) - } - - #[test] - fn type_double_comma() { - report_problem_as( - indoc!( - r#" - f : I64,,I64 -> I64 - f = 0 - - f - "# - ), - indoc!( - r#" - ── DOUBLE COMMA ────────────────────────────────────────── /code/proj/Main.roc ─ - - I just started parsing a function argument type, but I encountered two - commas in a row: - - 1│ f : I64,,I64 -> I64 - ^ - - Try removing one of them. - "# - ), - ) - } - - #[test] - fn type_argument_no_arrow() { - report_problem_as( - indoc!( - r#" - f : I64, I64 - f = 0 - - f - "# - ), - indoc!( - r#" - ── UNFINISHED TYPE ─────────────────────────────────────── /code/proj/Main.roc ─ - - I am partway through parsing a type, but I got stuck here: - - 1│ f : I64, I64 - ^ - - Note: I may be confused by indentation - "# - ), - ) - } - - #[test] - fn type_argument_arrow_then_nothing() { - // TODO could do better by pointing out we're parsing a function type - report_problem_as( - indoc!( - r#" - f : I64, I64 -> - f = 0 - - f - "# - ), - indoc!( - r#" - ── UNFINISHED TYPE ─────────────────────────────────────── /code/proj/Main.roc ─ - - I just started parsing a type, but I got stuck here: - - 1│ f : I64, I64 -> - ^ - - Note: I may be confused by indentation - "# - ), - ) - } - - #[test] - fn dict_type_formatting() { - // TODO could do better by pointing out we're parsing a function type - report_problem_as( - indoc!( - r#" - myDict : Dict Num.I64 Str - myDict = Dict.insert Dict.empty "foo" 42 - - myDict - "# - ), - indoc!( - r#" - ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ - - Something is off with the body of the `myDict` definition: - - 1│ myDict : Dict Num.I64 Str - 2│ myDict = Dict.insert Dict.empty "foo" 42 - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - - This `insert` call produces: - - Dict Str (Num a) - - But the type annotation on `myDict` says it should be: - - Dict I64 Str - "# - ), - ) - } - - #[test] - fn alias_type_diff() { - report_problem_as( - indoc!( - r#" - HSet a : Set a - - foo : Str -> HSet {} - - myDict : HSet Str - myDict = foo "bar" - - myDict - "# - ), - indoc!( - r#" - ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ - - Something is off with the body of the `myDict` definition: - - 5│ myDict : HSet Str - 6│ myDict = foo "bar" - ^^^^^^^^^ - - This `foo` call produces: - - HSet {} - - But the type annotation on `myDict` says it should be: - - HSet Str - "# - ), - ) - } - - #[test] - fn if_guard_without_condition() { - // this should get better with time - report_problem_as( - indoc!( - r#" - when Just 4 is - Just if -> - 4 - - _ -> - 2 - "# - ), - indoc!( - r#" - ── IF GUARD NO CONDITION ───────────────────────────────── /code/proj/Main.roc ─ - - I just started parsing an if guard, but there is no guard condition: - - 1│ when Just 4 is - 2│ Just if -> - ^ - - Try adding an expression before the arrow! - "# - ), - ) - } - - #[test] - fn empty_or_pattern() { - report_problem_as( - indoc!( - r#" - when Just 4 is - Just 4 | -> - 4 - - _ -> - 2 - "# - ), - indoc!( - r#" - ── UNFINISHED PATTERN ──────────────────────────────────── /code/proj/Main.roc ─ - - I just started parsing a pattern, but I got stuck here: - - 2│ Just 4 | -> - ^ - - Note: I may be confused by indentation - "# - ), - ) - } - - #[test] - fn pattern_binds_keyword() { - // TODO check if "what_is_next" is a keyword - report_problem_as( - indoc!( - r#" - when Just 4 is - Just when -> - 4 - - _ -> - 2 - "# - ), - indoc!( - r#" - ── MISSING EXPRESSION ──────────────────────────────────── /code/proj/Main.roc ─ - - I am partway through parsing a definition, but I got stuck here: - - 1│ when Just 4 is - 2│ Just when -> - ^ - - I was expecting to see an expression like 42 or "hello". - "# - ), - ) - } - - #[test] - fn when_missing_arrow() { - // this should get better with time - report_problem_as( - indoc!( - r#" - when 5 is - 1 -> 2 - _ - "# - ), - indoc!( - r#" - ── MISSING ARROW ───────────────────────────────────────── /code/proj/Main.roc ─ - - I am partway through parsing a `when` expression, but got stuck here: - - 2│ 1 -> 2 - 3│ _ - ^ - - I was expecting to see an arrow next. - - Note: Sometimes I get confused by indentation, so try to make your `when` - look something like this: - - when List.first plants is - Ok n -> - n - - Err _ -> - 200 - - Notice the indentation. All patterns are aligned, and each branch is - indented a bit more than the corresponding pattern. That is important! - "# - ), - ) - } - - #[test] - fn lambda_double_comma() { - report_problem_as( - indoc!( - r#" - \a,,b -> 1 - "# - ), - indoc!( - r#" - ── UNFINISHED ARGUMENT LIST ────────────────────────────── /code/proj/Main.roc ─ - - I am partway through parsing a function argument list, but I got stuck - at this comma: - - 1│ \a,,b -> 1 - ^ - - I was expecting an argument pattern before this, so try adding an - argument before the comma and see if that helps? - "# - ), - ) - } - - #[test] - fn lambda_leading_comma() { - report_problem_as( - indoc!( - r#" - \,b -> 1 - "# - ), - indoc!( - r#" - ── UNFINISHED ARGUMENT LIST ────────────────────────────── /code/proj/Main.roc ─ - - I am partway through parsing a function argument list, but I got stuck - at this comma: - - 1│ \,b -> 1 - ^ - - I was expecting an argument pattern before this, so try adding an - argument before the comma and see if that helps? - "# - ), - ) - } - - #[test] - fn when_outdented_branch() { - // this should get better with time - report_problem_as( - indoc!( - r#" - when 4 is - 5 -> 2 - 2 -> 2 - "# - ), - indoc!( - r#" - ── SYNTAX PROBLEM ──────────────────────────────────────── /code/proj/Main.roc ─ - - I got stuck here: - - 1│ when 4 is - 2│ 5 -> 2 - ^ - - Whatever I am running into is confusing me a lot! Normally I can give - fairly specific hints, but something is really tripping me up this - time. - "# - ), - // TODO this formerly gave - // - // ── UNFINISHED WHEN ───────────────────────────────────────────────────────────── - // - // I was partway through parsing a `when` expression, but I got stuck here: - // - // 3│ _ -> 2 - // ^ - // - // I suspect this is a pattern that is not indented enough? (by 2 spaces) - // - // but that requires parsing the next pattern blindly, irrespective of indentation. Can - // we find an efficient solution that doesn't require parsing an extra pattern for - // every `when`, i.e. we want a good error message for the test case above, but for - // a valid `when`, we don't want to do extra work, e.g. here - // - // x - // when x is - // n -> n - // - // 4 - // - // We don't want to parse the `4` and say it's an outdented pattern! - ) - } - - #[test] - fn when_over_indented_underscore() { - report_problem_as( - indoc!( - r#" - when 4 is - 5 -> 2 - _ -> 2 - "# - ), - indoc!( - r#" - ── SYNTAX PROBLEM ──────────────────────────────────────── /code/proj/Main.roc ─ - - I got stuck here: - - 1│ when 4 is - 2│ 5 -> 2 - ^ - - Whatever I am running into is confusing me a lot! Normally I can give - fairly specific hints, but something is really tripping me up this - time. - "# - ), - ) - } - - #[test] - fn when_over_indented_int() { - report_problem_as( - indoc!( - r#" - when 4 is - 5 -> Num.neg - 2 -> 2 - "# - ), - indoc!( - r#" - ── UNEXPECTED ARROW ────────────────────────────────────── /code/proj/Main.roc ─ - - I am parsing a `when` expression right now, but this arrow is confusing - me: - - 3│ 2 -> 2 - ^^ - - It makes sense to see arrows around here, so I suspect it is something - earlier.Maybe this pattern is indented a bit farther from the previous - patterns? - - Note: Here is an example of a valid `when` expression for reference. - - when List.first plants is - Ok n -> - n - - Err _ -> - 200 - - Notice the indentation. All patterns are aligned, and each branch is - indented a bit more than the corresponding pattern. That is important! - "# - ), - ) - } - - #[test] - fn if_outdented_then() { - // TODO I think we can do better here - report_problem_as( - indoc!( - r#" - x = - if 5 == 5 - then 2 else 3 - - x - "# - ), - indoc!( - r#" - ── UNFINISHED IF ───────────────────────────────────────── /code/proj/Main.roc ─ - - I was partway through parsing an `if` expression, but I got stuck here: - - 2│ if 5 == 5 - ^ - - I was expecting to see the `then` keyword next. - "# - ), - ) - } - - #[test] - fn if_missing_else() { - // this should get better with time - report_problem_as( - indoc!( - r#" - if 5 == 5 then 2 - "# - ), - indoc!( - r#" - ── UNFINISHED IF ───────────────────────────────────────── /code/proj/Main.roc ─ - - I was partway through parsing an `if` expression, but I got stuck here: - - 1│ if 5 == 5 then 2 - ^ - - I was expecting to see the `else` keyword next. - "# - ), - ) - } - - #[test] - fn list_double_comma() { - report_problem_as( - indoc!( - r#" - [1, 2, , 3] - "# - ), - indoc!( - r#" - ── UNFINISHED LIST ─────────────────────────────────────── /code/proj/Main.roc ─ - - I am partway through started parsing a list, but I got stuck here: - - 1│ [1, 2, , 3] - ^ - - I was expecting to see a list entry before this comma, so try adding a - list entry and see if that helps? - "# - ), - ) - } - - #[test] - fn list_without_end() { - report_problem_as( - indoc!( - r#" - [1, 2, - "# - ), - indoc!( - r#" - ── UNFINISHED LIST ─────────────────────────────────────── /code/proj/Main.roc ─ - - I am partway through started parsing a list, but I got stuck here: - - 1│ [1, 2, - ^ - - I was expecting to see a closing square bracket before this, so try - adding a ] and see if that helps? - - Note: When I get stuck like this, it usually means that there is a - missing parenthesis or bracket somewhere earlier. It could also be a - stray keyword or operator. - "# - ), - ) - } - - #[test] - fn number_double_dot() { - report_problem_as( - indoc!( - r#" - 1.1.1 - "# - ), - indoc!( - r#" - ── SYNTAX PROBLEM ──────────────────────────────────────── /code/proj/Main.roc ─ - - This float literal contains an invalid digit: - - 1│ 1.1.1 - ^^^^^ - - Floating point literals can only contain the digits 0-9, or use - scientific notation 10e4, or have a float suffix. - - Tip: Learn more about number literals at TODO - "# - ), - ) - } - - #[test] - fn unicode_not_hex() { - report_problem_as( - r#""abc\u(zzzz)def""#, - indoc!( - r#" - ── WEIRD CODE POINT ────────────────────────────────────── /code/proj/Main.roc ─ - - I am partway through parsing a unicode code point, but I got stuck - here: - - 1│ "abc\u(zzzz)def" - ^ - - I was expecting a hexadecimal number, like \u(1100) or \u(00FF). - - Learn more about working with unicode in roc at TODO - "# - ), - ) - } - - #[test] - fn interpolate_not_identifier() { - report_problem_as( - r#""abc\(32)def""#, - indoc!( - r#" - ── SYNTAX PROBLEM ──────────────────────────────────────── /code/proj/Main.roc ─ - - This string interpolation is invalid: - - 1│ "abc\(32)def" - ^^ - - I was expecting an identifier, like \u(message) or - \u(LoremIpsum.text). - - Learn more about string interpolation at TODO - "# - ), - ) - } - - #[test] - fn unicode_too_large() { - report_problem_as( - r#""abc\u(110000)def""#, - indoc!( - r#" - ── INVALID UNICODE ─────────────────────────────────────── /code/proj/Main.roc ─ - - This unicode code point is invalid: - - 1│ "abc\u(110000)def" - ^^^^^^ - - Learn more about working with unicode in roc at TODO - "# - ), - ) - } - - #[test] - fn weird_escape() { - report_problem_as( - r#""abc\qdef""#, - indoc!( - r#" - ── WEIRD ESCAPE ────────────────────────────────────────── /code/proj/Main.roc ─ - - I was partway through parsing a string literal, but I got stuck here: - - 1│ "abc\qdef" - ^^ - - This is not an escape sequence I recognize. After a backslash, I am - looking for one of these: - - - A newline: \n - - A caret return: \r - - A tab: \t - - An escaped quote: \" - - An escaped backslash: \\ - - A unicode code point: \u(00FF) - - An interpolated string: \(myVariable) - "# - ), - ) - } - - #[test] - fn single_no_end() { - report_problem_as( - r#""there is no end"#, - indoc!( - r#" - ── ENDLESS STRING ──────────────────────────────────────── /code/proj/Main.roc ─ - - I cannot find the end of this string: - - 1│ "there is no end - ^ - - You could change it to something like "to be or not to be" or even - just "". - "# - ), - ) - } - - #[test] - fn multi_no_end() { - report_problem_as( - r#""""there is no end"#, - indoc!( - r#" - ── ENDLESS STRING ──────────────────────────────────────── /code/proj/Main.roc ─ - - I cannot find the end of this block string: - - 1│ """there is no end - ^ - - You could change it to something like """to be or not to be""" or even - just """""". - "# - ), - ) - } - - #[test] - // https://github.com/rtfeldman/roc/issues/1714 - fn interpolate_concat_is_transparent_1714() { - report_problem_as( - indoc!( - r#" - greeting = "Privet" - - if True then 1 else "\(greeting), World!" - "#, - ), - indoc!( - r#" - ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ - - This `if` has an `else` branch with a different type from its `then` branch: - - 3│ if True then 1 else "\(greeting), World!" - ^^^^^^^^^^^^^^^^^^^^^ - - The `else` branch is a string of type: - - Str - - but the `then` branch has the type: - - Num a - - All branches in an `if` must have the same type! - "# - ), - ) - } - - macro_rules! comparison_binop_transparency_tests { - ($($op:expr, $name:ident),* $(,)?) => { - $( - #[test] - fn $name() { - report_problem_as( - &format!(r#"if True then "abc" else 1 {} 2"#, $op), - &format!( -r#"── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ - -This `if` has an `else` branch with a different type from its `then` branch: - -1│ if True then "abc" else 1 {} 2 - ^^{}^^ - -This comparison produces: - - Bool - -but the `then` branch has the type: - - Str - -All branches in an `if` must have the same type! -"#, - $op, "^".repeat($op.len()) - ), - ) - } - )* - } - } - - comparison_binop_transparency_tests! { - "<", lt_binop_is_transparent, - ">", gt_binop_is_transparent, - "==", eq_binop_is_transparent, - "!=", neq_binop_is_transparent, - "<=", leq_binop_is_transparent, - ">=", geq_binop_is_transparent, - } - - #[test] - fn keyword_record_field_access() { - report_problem_as( - indoc!( - r#" - foo = {} - - foo.if - "# - ), - indoc!( - r#" - ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ - - This `foo` record doesn’t have a `if` field: - - 3│ foo.if - ^^^^^^ - - In fact, `foo` is a record with no fields at all! - "# - ), - ) - } - - #[test] - fn keyword_qualified_import() { - report_problem_as( - indoc!( - r#" - Num.if - "# - ), - indoc!( - r#" - ── NOT EXPOSED ─────────────────────────────────────────── /code/proj/Main.roc ─ - - The Num module does not expose `if`: - - 1│ Num.if - ^^^^^^ - - Did you mean one of these? - - Num.sin - Num.div - Num.abs - Num.neg - "# - ), - ) - } - - #[test] - fn stray_dot_expr() { - report_problem_as( - indoc!( - r#" - Num.add . 23 - "# - ), - indoc!( - r#" - ── SYNTAX PROBLEM ──────────────────────────────────────── /code/proj/Main.roc ─ - - I trying to parse a record field access here: - - 1│ Num.add . 23 - ^ - - So I expect to see a lowercase letter next, like .name or .height. - "# - ), - ) - } - - #[test] - fn opaque_ref_field_access() { - report_problem_as( - indoc!( - r#" - @UUID.bar - "# - ), - indoc!( - r#" - ── SYNTAX PROBLEM ──────────────────────────────────────── /code/proj/Main.roc ─ - - I am very confused by this field access: - - 1│ @UUID.bar - ^^^^ - - It looks like a record field access on an opaque reference. - "# - ), - ) - } - - #[test] - fn weird_accessor() { - report_problem_as( - indoc!( - r#" - .foo.bar - "# - ), - indoc!( - r#" - ── SYNTAX PROBLEM ──────────────────────────────────────── /code/proj/Main.roc ─ - - I am very confused by this field access - - 1│ .foo.bar - ^^^^^^^^ - - It looks like a field access on an accessor. I parse.client.name as - (.client).name. Maybe use an anonymous function like - (\r -> r.client.name) instead? - "# - ), - ) - } - - #[test] - fn part_starts_with_number() { - report_problem_as( - indoc!( - r#" - foo.100 - "# - ), - indoc!( - r#" - ── SYNTAX PROBLEM ──────────────────────────────────────── /code/proj/Main.roc ─ - - I trying to parse a record field access here: - - 1│ foo.100 - ^ - - So I expect to see a lowercase letter next, like .name or .height. - "# - ), - ) - } - - #[test] - fn closure_underscore_ident() { - report_problem_as( - indoc!( - r#" - \the_answer -> 100 - "# - ), - indoc!( - r#" - ── NAMING PROBLEM ──────────────────────────────────────── /code/proj/Main.roc ─ - - I am trying to parse an identifier here: - - 1│ \the_answer -> 100 - ^ - - Underscores are not allowed in identifiers. Use camelCase instead! - "# - ), - ) - } - - #[test] - #[ignore] - fn double_binop() { - report_problem_as( - indoc!( - r#" - key >= 97 && <= 122 - "# - ), - indoc!( - r#" - "# - ), - ) - } - - #[test] - #[ignore] - fn case_of() { - report_problem_as( - indoc!( - r#" - case 1 of - 1 -> True - _ -> False - "# - ), - indoc!( - r#" - "# - ), - ) - } - - #[test] - fn argument_without_space() { - report_problem_as( - indoc!( - r#" - ["foo", bar("")] - "# - ), - indoc!( - r#" - ── UNRECOGNIZED NAME ───────────────────────────────────── /code/proj/Main.roc ─ - - Nothing is named `bar` in this scope. - - 1│ ["foo", bar("")] - ^^^ - - Did you mean one of these? - - Str - Err - Box - Set - "# - ), - ) - } - - #[test] - fn invalid_operator() { - report_problem_as( - indoc!( - r#" - main = - 5 ** 3 - "# - ), - indoc!( - r#" - ── UNKNOWN OPERATOR ────────────────────────────────────── /code/proj/Main.roc ─ - - This looks like an operator, but it's not one I recognize! - - 1│ main = - 2│ 5 ** 3 - ^^ - - I have no specific suggestion for this operator, see TODO for the full - list of operators in Roc. - "# - ), - ) - } - - #[test] - fn double_plus() { - report_problem_as( - indoc!( - r#" - main = - [] ++ [] - "# - ), - indoc!( - r#" - ── UNKNOWN OPERATOR ────────────────────────────────────── /code/proj/Main.roc ─ - - This looks like an operator, but it's not one I recognize! - - 1│ main = - 2│ [] ++ [] - ^^ - - To concatenate two lists or strings, try using List.concat or - Str.concat instead. - "# - ), - ) - } - - #[test] - fn inline_hastype() { - report_problem_as( - indoc!( - r#" - main = - (\x -> x) : I64 - - 3 - "# - ), - indoc!( - r#" - ── UNKNOWN OPERATOR ────────────────────────────────────── /code/proj/Main.roc ─ - - This looks like an operator, but it's not one I recognize! - - 1│ main = - 2│ (\x -> x) : I64 - ^ - - The has-type operator : can only occur in a definition's type - signature, like - - increment : I64 -> I64 - increment = \x -> x + 1 - "# - ), - ) - } - - #[test] - fn wild_case_arrow() { - // this is still bad, but changing the order and progress of other parsers should improve it - // down the line - report_problem_as( - indoc!( - r#" - main = 5 -> 3 - "# - ), - indoc!( - r#" - ── UNKNOWN OPERATOR ────────────────────────────────────── /code/proj/Main.roc ─ - - This looks like an operator, but it's not one I recognize! - - 1│ main = 5 -> 3 - ^^ - - The arrow -> is only used to define cases in a `when`. - - when color is - Red -> "stop!" - Green -> "go!" - "# - ), - ) - } - - #[test] - fn provides_to_identifier() { - report_header_problem_as( - indoc!( - r#" - app "test-base64" - packages { pf: "platform" } - imports [pf.Task, Base64] - provides [main, @Foo] to pf - "# - ), - indoc!( - r#" - ── WEIRD PROVIDES ──────────────────────────────────────── /code/proj/Main.roc ─ - - I am partway through parsing a provides list, but I got stuck here: - - 3│ imports [pf.Task, Base64] - 4│ provides [main, @Foo] to pf - ^ - - I was expecting a type name, value name or function name next, like - - provides [Animal, default, tame] - "# - ), - ) - } - - #[test] - fn platform_requires_rigids() { - report_header_problem_as( - indoc!( - r#" - platform "folkertdev/foo" - requires { main : Effect {} } - exposes [] - packages {} - imports [Task] - provides [mainForHost] - effects fx.Effect - { - putChar : I64 -> Effect {}, - putLine : Str -> Effect {}, - getLine : Effect Str - } - "# - ), - indoc!( - r#" - ── BAD REQUIRES ────────────────────────────────────────── /code/proj/Main.roc ─ - - I am partway through parsing a header, but I got stuck here: - - 1│ platform "folkertdev/foo" - 2│ requires { main : Effect {} } - ^ - - I am expecting a list of type names like `{}` or `{ Model }` next. A full - `requires` definition looks like - - requires { Model, Msg } {main : Effect {}} - "# - ), - ) - } - - #[test] - fn missing_imports() { - report_header_problem_as( - indoc!( - r#" - interface Foobar - exposes [main, Foo] - "# - ), - indoc!( - r#" - ── WEIRD IMPORTS ───────────────────────────────────────── /code/proj/Main.roc ─ - - I am partway through parsing a header, but I got stuck here: - - 2│ exposes [main, Foo] - ^ - - I am expecting the `imports` keyword next, like - - imports [Animal, default, tame] - "# - ), - ) - } - - #[test] - fn exposes_identifier() { - report_header_problem_as( - indoc!( - r#" - interface Foobar - exposes [main, @Foo] - imports [pf.Task, Base64] - "# - ), - indoc!( - r#" - ── WEIRD EXPOSES ───────────────────────────────────────── /code/proj/Main.roc ─ - - I am partway through parsing an `exposes` list, but I got stuck here: - - 1│ interface Foobar - 2│ exposes [main, @Foo] - ^ - - I was expecting a type name, value name or function name next, like - - exposes [Animal, default, tame] - "# - ), - ) - } - - #[test] - fn invalid_module_name() { - report_header_problem_as( - indoc!( - r#" - interface foobar - exposes [main, @Foo] - imports [pf.Task, Base64] - "# - ), - indoc!( - r#" - ── WEIRD MODULE NAME ───────────────────────────────────── /code/proj/Main.roc ─ - - I am partway through parsing a header, but got stuck here: - - 1│ interface foobar - ^ - - I am expecting a module name next, like BigNum or Main. Module names - must start with an uppercase letter. - "# - ), - ) - } - - #[test] - fn invalid_app_name() { - report_header_problem_as( - indoc!( - r#" - app foobar - exposes [main, @Foo] - imports [pf.Task, Base64] - "# - ), - indoc!( - r#" - ── WEIRD APP NAME ──────────────────────────────────────── /code/proj/Main.roc ─ - - I am partway through parsing a header, but got stuck here: - - 1│ app foobar - ^ - - I am expecting an application name next, like app "main" or - app "editor". App names are surrounded by quotation marks. - "# - ), - ) - } - - #[test] - fn apply_unary_negative() { - report_problem_as( - indoc!( - r#" - foo = 3 - - -foo 1 2 - "# - ), - indoc!( - r#" - ── TOO MANY ARGS ───────────────────────────────────────── /code/proj/Main.roc ─ - - This value is not a function, but it was given 2 arguments: - - 3│ -foo 1 2 - ^^^^ - - Are there any missing commas? Or missing parentheses? - "# - ), - ) - } - - #[test] - fn apply_unary_not() { - report_problem_as( - indoc!( - r#" - foo = True - - !foo 1 2 - "# - ), - indoc!( - r#" - ── TOO MANY ARGS ───────────────────────────────────────── /code/proj/Main.roc ─ - - This value is not a function, but it was given 2 arguments: - - 3│ !foo 1 2 - ^^^^ - - Are there any missing commas? Or missing parentheses? - "# - ), - ) - } - - #[test] - fn applied_tag_function() { - report_problem_as( - indoc!( - r#" - x : List [Foo Str] - x = List.map [1, 2] Foo - - x - "# - ), - indoc!( - r#" - ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ - - Something is off with the body of the `x` definition: - - 1│ x : List [Foo Str] - 2│ x = List.map [1, 2] Foo - ^^^^^^^^^^^^^^^^^^^ - - This `map` call produces: - - List [Foo Num a] - - But the type annotation on `x` says it should be: - - List [Foo Str] - "# - ), - ) - } - - #[test] - fn pattern_in_parens_open() { - report_problem_as( - indoc!( - r#" - \( a - "# - ), - indoc!( - r#" - ── UNFINISHED PARENTHESES ──────────────────────────────── /code/proj/Main.roc ─ - - I am partway through parsing a pattern in parentheses, but I got stuck - here: - - 1│ \( a - ^ - - I was expecting to see a closing parenthesis before this, so try - adding a ) and see if that helps? - "# - ), - ) - } - - #[test] - fn pattern_in_parens_end_comma() { - report_problem_as( - indoc!( - r#" - \( a, - "# - ), - indoc!( - r#" - ── UNFINISHED PARENTHESES ──────────────────────────────── /code/proj/Main.roc ─ - - I am partway through parsing a pattern in parentheses, but I got stuck - here: - - 1│ \( a, - ^ - - I was expecting to see a closing parenthesis before this, so try - adding a ) and see if that helps? - "# - ), - ) - } - - #[test] - fn pattern_in_parens_end() { - report_problem_as( - indoc!( - r#" - \( a - "# - ), - indoc!( - r#" - ── UNFINISHED PARENTHESES ──────────────────────────────── /code/proj/Main.roc ─ - - I am partway through parsing a pattern in parentheses, but I got stuck - here: - - 1│ \( a - ^ - - I was expecting to see a closing parenthesis before this, so try - adding a ) and see if that helps? - "# - ), - ) - } - - #[test] - fn pattern_in_parens_indent_end() { - report_problem_as( - indoc!( - r#" - x = \( a - ) - "# - ), - indoc!( - r#" - ── NEED MORE INDENTATION ───────────────────────────────── /code/proj/Main.roc ─ - - I am partway through parsing a pattern in parentheses, but I got stuck - here: - - 1│ x = \( a - 2│ ) - ^ - - I need this parenthesis to be indented more. Try adding more spaces - before it! - "# - ), - ) - } - - #[test] - fn pattern_in_parens_indent_open() { - report_problem_as( - indoc!( - r#" - \( - "# - ), - indoc!( - r#" - ── UNFINISHED PATTERN ──────────────────────────────────── /code/proj/Main.roc ─ - - I just started parsing a pattern, but I got stuck here: - - 1│ \( - ^ - - Note: I may be confused by indentation - "# - ), - ) - } - - #[test] - fn outdented_in_parens() { - report_problem_as( - indoc!( - r#" - Box : ( - Str - ) - - 4 - "# - ), - indoc!( - r#" - ── NEED MORE INDENTATION ───────────────────────────────── /code/proj/Main.roc ─ - - I am partway through parsing a type in parentheses, but I got stuck - here: - - 1│ Box : ( - 2│ Str - 3│ ) - ^ - - I need this parenthesis to be indented more. Try adding more spaces - before it! - "# - ), - ) - } - - #[test] - fn backpassing_type_error() { - report_problem_as( - indoc!( - r#" - x <- List.map ["a", "b"] - - x + 1 - "# - ), - indoc!( - r#" - ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ - - The 2nd argument to `map` is not what I expect: - - 1│> x <- List.map ["a", "b"] - 2│> - 3│> x + 1 - - This argument is an anonymous function of type: - - Num a -> Num a - - But `map` needs the 2nd argument to be: - - Str -> Num a - "# - ), - ) - } - - #[test] - fn underscore_let() { - report_problem_as( - indoc!( - r#" - _ = 3 - - 4 - "# - ), - indoc!( - r#" - ── SYNTAX PROBLEM ──────────────────────────────────────── /code/proj/Main.roc ─ - - Underscore patterns are not allowed in definitions - - 1│ _ = 3 - ^ - "# - ), - ) - } - - #[test] - fn expect_expr_type_error() { - report_problem_as( - indoc!( - r#" - expect "foobar" - - 4 - "# - ), - indoc!( - r#" - ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ - - This `expect` condition needs to be a Bool: - - 1│ expect "foobar" - ^^^^^^^^ - - Right now it’s a string of type: - - Str - - But I need every `expect` condition to evaluate to a Bool—either `True` - or `False`. - "# - ), - ) - } - - #[test] - fn num_too_general_wildcard() { - report_problem_as( - indoc!( - r#" - mult : Num.Num *, Num.F64 -> Num.F64 - mult = \a, b -> a * b - - mult 0 0 - "# - ), - indoc!( - r#" - ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ - - The 2nd argument to `mul` is not what I expect: - - 2│ mult = \a, b -> a * b - ^ - - This `b` value is a: - - F64 - - But `mul` needs the 2nd argument to be: - - Num * - - ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ - - Something is off with the body of the `mult` definition: - - 1│ mult : Num.Num *, Num.F64 -> Num.F64 - 2│ mult = \a, b -> a * b - ^^^^^ - - This `mul` call produces: - - Num * - - But the type annotation on `mult` says it should be: - - F64 - "# - ), - ) - } - - #[test] - fn num_too_general_named() { - report_problem_as( - indoc!( - r#" - mult : Num.Num a, Num.F64 -> Num.F64 - mult = \a, b -> a * b - - mult 0 0 - "# - ), - indoc!( - r#" - ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ - - The 2nd argument to `mul` is not what I expect: - - 2│ mult = \a, b -> a * b - ^ - - This `b` value is a: - - F64 - - But `mul` needs the 2nd argument to be: - - Num a - - ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ - - Something is off with the body of the `mult` definition: - - 1│ mult : Num.Num a, Num.F64 -> Num.F64 - 2│ mult = \a, b -> a * b - ^^^^^ - - This `mul` call produces: - - Num a - - But the type annotation on `mult` says it should be: - - F64 - "# - ), - ) - } - - #[test] - fn inference_var_not_enough_in_alias() { - report_problem_as( - indoc!( - r#" - Result a b : [Ok a, Err b] - - canIGo : _ -> Result _ - canIGo = \color -> - when color is - "green" -> Ok "go!" - "yellow" -> Err (SlowIt "whoa, let's slow down!") - "red" -> Err (StopIt "absolutely not") - _ -> Err (UnknownColor "this is a weird stoplight") - canIGo - "# - ), - indoc!( - r#" - ── TOO FEW TYPE ARGUMENTS ──────────────────────────────── /code/proj/Main.roc ─ - - The `Result` alias expects 2 type arguments, but it got 1 instead: - - 3│ canIGo : _ -> Result _ - ^^^^^^^^ - - Are there missing parentheses? - "# - ), - ) - } - - #[test] - fn inference_var_too_many_in_alias() { - report_problem_as( - indoc!( - r#" - Result a b : [Ok a, Err b] - - canIGo : _ -> Result _ _ _ - canIGo = \color -> - when color is - "green" -> Ok "go!" - "yellow" -> Err (SlowIt "whoa, let's slow down!") - "red" -> Err (StopIt "absolutely not") - _ -> Err (UnknownColor "this is a weird stoplight") - canIGo - "# - ), - indoc!( - r#" - ── TOO MANY TYPE ARGUMENTS ─────────────────────────────── /code/proj/Main.roc ─ - - The `Result` alias expects 2 type arguments, but it got 3 instead: - - 3│ canIGo : _ -> Result _ _ _ - ^^^^^^^^^^^^ - - Are there missing parentheses? - "# - ), - ) - } - - #[test] - fn inference_var_conflict_in_rigid_links() { - report_problem_as( - indoc!( - r#" - f : a -> (_ -> b) - f = \x -> \y -> if x == y then x else y - f - "# - ), - // TODO: We should tell the user that we inferred `_` as `a` - indoc!( - r#" - ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ - - Something is off with the body of the `f` definition: - - 1│ f : a -> (_ -> b) - 2│ f = \x -> \y -> if x == y then x else y - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - - The body is an anonymous function of type: - - a -> a - - But the type annotation on `f` says it should be: - - a -> b - - Tip: Your type annotation uses `a` and `b` as separate type variables. - Your code seems to be saying they are the same though. Maybe they - should be the same in your type annotation? Maybe your code uses them - in a weird way? - "# - ), - ) - } - - #[test] - fn error_wildcards_are_related() { - report_problem_as( - indoc!( - r#" - f : * -> * - f = \x -> x - - f - "# - ), - indoc!( - r#" - ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ - - Something is off with the body of the `f` definition: - - 1│ f : * -> * - 2│ f = \x -> x - ^ - - The type annotation on `f` says this `x` value should have the type: - - * - - However, the type of this `x` value is connected to another type in a - way that isn't reflected in this annotation. - - Tip: Any connection between types must use a named type variable, not - a `*`! Maybe the annotation on `f` should have a named type variable in - place of the `*`? - "# - ), - ) - } - - #[test] - fn error_nested_wildcards_are_related() { - report_problem_as( - indoc!( - r#" - f : a, b, * -> {x: a, y: b, z: *} - f = \x, y, z -> {x, y, z} - - f - "# - ), - indoc!( - r#" - ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ - - Something is off with the body of the `f` definition: - - 1│ f : a, b, * -> {x: a, y: b, z: *} - 2│ f = \x, y, z -> {x, y, z} - ^^^^^^^^^ - - The type annotation on `f` says the body is a record should have the - type: - - { x : a, y : b, z : * } - - However, the type of the body is a record is connected to another type - in a way that isn't reflected in this annotation. - - Tip: Any connection between types must use a named type variable, not - a `*`! Maybe the annotation on `f` should have a named type variable in - place of the `*`? - "# - ), - ) - } - - #[test] - fn error_wildcards_are_related_in_nested_defs() { - report_problem_as( - indoc!( - r#" - f : a, b, * -> * - f = \_, _, x2 -> - inner : * -> * - inner = \y -> y - inner x2 - - f - "# - ), - indoc!( - r#" - ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ - - Something is off with the body of the `inner` definition: - - 3│ inner : * -> * - 4│ inner = \y -> y - ^ - - The type annotation on `inner` says this `y` value should have the type: - - * - - However, the type of this `y` value is connected to another type in a - way that isn't reflected in this annotation. - - Tip: Any connection between types must use a named type variable, not - a `*`! Maybe the annotation on `inner` should have a named type variable - in place of the `*`? - "# - ), - ) - } - - #[test] - fn error_inline_alias_not_an_alias() { - report_problem_as( - indoc!( - r#" - f : List elem -> [Nil, Cons elem a] as a - "# - ), - indoc!( - r#" - ── NOT AN INLINE ALIAS ─────────────────────────────────── /code/proj/Main.roc ─ - - The inline type after this `as` is not a type alias: - - 1│ f : List elem -> [Nil, Cons elem a] as a - ^ - - Inline alias types must start with an uppercase identifier and be - followed by zero or more type arguments, like Point or List a. - "# - ), - ) - } - - #[test] - fn error_inline_alias_qualified() { - report_problem_as( - indoc!( - r#" - f : List elem -> [Nil, Cons elem a] as Module.LinkedList a - "# - ), - indoc!( - r#" - ── QUALIFIED ALIAS NAME ────────────────────────────────── /code/proj/Main.roc ─ - - This type alias has a qualified name: - - 1│ f : List elem -> [Nil, Cons elem a] as Module.LinkedList a - ^ - - An alias introduces a new name to the current scope, so it must be - unqualified. - "# - ), - ) - } - - #[test] - fn error_inline_alias_argument_uppercase() { - report_problem_as( - indoc!( - r#" - f : List elem -> [Nil, Cons elem a] as LinkedList U - "# - ), - indoc!( - r#" - ── TYPE ARGUMENT NOT LOWERCASE ─────────────────────────── /code/proj/Main.roc ─ - - This alias type argument is not lowercase: - - 1│ f : List elem -> [Nil, Cons elem a] as LinkedList U - ^ - - All type arguments must be lowercase. - "# - ), - ) - } - - #[test] - fn mismatched_single_tag_arg() { - report_problem_as( - indoc!( - r#" - isEmpty = - \email -> - Email str = email - Str.isEmpty str - - isEmpty (Name "boo") - "# - ), - indoc!( - r#" - ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ - - The 1st argument to `isEmpty` is not what I expect: - - 6│ isEmpty (Name "boo") - ^^^^^^^^^^ - - This `Name` tag application has the type: - - [Name Str]a - - But `isEmpty` needs the 1st argument to be: - - [Email Str] - - Tip: Seems like a tag typo. Maybe `Name` should be `Email`? - - Tip: Can more type annotations be added? Type annotations always help - me give more specific messages, and I think they could help a lot in - this case - "# - ), - ) - } - - #[test] - fn issue_2326() { - report_problem_as( - indoc!( - r#" - C a b : a -> D a b - D a b : { a, b } - - f : C a Num.Nat -> D a Num.Nat - f = \c -> c 6 - f - "# - ), - indoc!( - r#" - ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ - - The 1st argument to `c` is not what I expect: - - 5│ f = \c -> c 6 - ^ - - This argument is a number of type: - - Num a - - But `c` needs the 1st argument to be: - - a - - Tip: The type annotation uses the type variable `a` to say that this - definition can produce any type of value. But in the body I see that - it will only produce a `Num` value of a single specific type. Maybe - change the type annotation to be more specific? Maybe change the code - to be more general? - "# - ), - ) - } - - #[test] - fn issue_2380_annotations_only() { - report_problem_as( - indoc!( - r#" - F : F - a : F - a - "# - ), - indoc!( - r#" - ── CYCLIC ALIAS ────────────────────────────────────────── /code/proj/Main.roc ─ - - The `F` alias is self-recursive in an invalid way: - - 1│ F : F - ^ - - Recursion in aliases is only allowed if recursion happens behind a - tagged union, at least one variant of which is not recursive. - "# - ), - ) - } - - #[test] - fn issue_2380_typed_body() { - report_problem_as( - indoc!( - r#" - F : F - a : F - a = 1 - a - "# - ), - indoc!( - r#" - ── CYCLIC ALIAS ────────────────────────────────────────── /code/proj/Main.roc ─ - - The `F` alias is self-recursive in an invalid way: - - 1│ F : F - ^ - - Recursion in aliases is only allowed if recursion happens behind a - tagged union, at least one variant of which is not recursive. - "# - ), - ) - } - - #[test] - fn issue_2380_alias_with_vars() { - report_problem_as( - indoc!( - r#" - F a b : F a b - a : F Str Str - a - "# - ), - indoc!( - r#" - ── CYCLIC ALIAS ────────────────────────────────────────── /code/proj/Main.roc ─ - - The `F` alias is self-recursive in an invalid way: - - 1│ F a b : F a b - ^ - - Recursion in aliases is only allowed if recursion happens behind a - tagged union, at least one variant of which is not recursive. - "# - ), - ) - } - - #[test] - fn issue_2167_record_field_optional_and_required_mismatch() { - report_problem_as( - indoc!( - r#" - Job : [Job { inputs : List Str }] - job : { inputs ? List Str } -> Job - job = \{ inputs } -> - Job { inputs } - - job { inputs: ["build", "test"] } - "# - ), - indoc!( - r#" - ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ - - The 1st argument to `job` is weird: - - 3│ job = \{ inputs } -> - ^^^^^^^^^^ - - The argument is a pattern that matches record values of type: - - { inputs : List Str } - - But the annotation on `job` says the 1st argument should be: - - { inputs ? List Str } - - Tip: To extract the `.inputs` field it must be non-optional, but the - type says this field is optional. Learn more about optional fields at - TODO. - "# - ), - ) - } - - #[test] - fn unify_recursive_with_nonrecursive() { - report_problem_as( - indoc!( - r#" - Job : [Job { inputs : List Job }] - - job : { inputs : List Str } -> Job - job = \{ inputs } -> - Job { inputs } - - job { inputs: ["build", "test"] } - "# - ), - indoc!( - r#" - ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ - - Something is off with the body of the `job` definition: - - 3│ job : { inputs : List Str } -> Job - 4│ job = \{ inputs } -> - 5│ Job { inputs } - ^^^^^^^^^^^^^^ - - This `Job` tag application has the type: - - [Job { inputs : List Str }] - - But the type annotation on `job` says it should be: - - [Job { inputs : List a }] as a - "# - ), - ) - } - - #[test] - fn nested_datatype() { - report_problem_as( - indoc!( - r#" - Nested a : [Chain a (Nested (List a)), Term] - - s : Nested Str - - s - "# - ), - indoc!( - r#" - ── NESTED DATATYPE ─────────────────────────────────────── /code/proj/Main.roc ─ - - `Nested` is a nested datatype. Here is one recursive usage of it: - - 1│ Nested a : [Chain a (Nested (List a)), Term] - ^^^^^^^^^^^^^^^ - - But recursive usages of `Nested` must match its definition: - - 1│ Nested a : [Chain a (Nested (List a)), Term] - ^^^^^^^^ - - Nested datatypes are not supported in Roc. - - Hint: Consider rewriting the definition of `Nested` to use the recursive type with the same arguments. - "# - ), - ) - } - - #[test] - fn nested_datatype_inline() { - report_problem_as( - indoc!( - r#" - f : {} -> [Chain a (Nested (List a)), Term] as Nested a - - f - "# - ), - indoc!( - r#" - ── NESTED DATATYPE ─────────────────────────────────────── /code/proj/Main.roc ─ - - `Nested` is a nested datatype. Here is one recursive usage of it: - - 1│ f : {} -> [Chain a (Nested (List a)), Term] as Nested a - ^^^^^^^^^^^^^^^ - - But recursive usages of `Nested` must match its definition: - - 1│ f : {} -> [Chain a (Nested (List a)), Term] as Nested a - ^^^^^^^^ - - Nested datatypes are not supported in Roc. - - Hint: Consider rewriting the definition of `Nested` to use the recursive type with the same arguments. - "# - ), - ) - } - - macro_rules! mismatched_suffix_tests { - ($($number:expr, $suffix:expr, $name:ident)*) => {$( - #[test] - fn $name() { - let number = $number.to_string(); - let mut typ = $suffix.to_string(); - typ.get_mut(0..1).unwrap().make_ascii_uppercase(); - let bad_type = if $suffix == "u8" { "I8" } else { "U8" }; - let carets = "^".repeat(number.len() + $suffix.len()); - let kind = match $suffix { - "dec"|"f32"|"f64" => "a frac", - _ => "an integer", - }; - - report_problem_as( - &format!(indoc!( - r#" - use : Num.{} -> Num.U8 - use {}{} - "# - ), bad_type, number, $suffix), - &format!(indoc!( - r#" - ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ - - The 1st argument to `use` is not what I expect: - - 2│ use {}{} - {} - - This argument is {} of type: - - {} - - But `use` needs the 1st argument to be: - - {} - "# - ), number, $suffix, carets, kind, typ, bad_type), - ) - } - )*} - } - - mismatched_suffix_tests! { - 1, "u8", mismatched_suffix_u8 - 1, "u16", mismatched_suffix_u16 - 1, "u32", mismatched_suffix_u32 - 1, "u64", mismatched_suffix_u64 - 1, "u128", mismatched_suffix_u128 - 1, "i8", mismatched_suffix_i8 - 1, "i16", mismatched_suffix_i16 - 1, "i32", mismatched_suffix_i32 - 1, "i64", mismatched_suffix_i64 - 1, "i128", mismatched_suffix_i128 - 1, "nat", mismatched_suffix_nat - 1, "dec", mismatched_suffix_dec - 1, "f32", mismatched_suffix_f32 - 1, "f64", mismatched_suffix_f64 - } - - macro_rules! mismatched_suffix_tests_in_pattern { - ($($number:expr, $suffix:expr, $name:ident)*) => {$( - #[test] - fn $name() { - let number = $number.to_string(); - let mut typ = $suffix.to_string(); - typ.get_mut(0..1).unwrap().make_ascii_uppercase(); - let bad_suffix = if $suffix == "u8" { "i8" } else { "u8" }; - let bad_type = if $suffix == "u8" { "I8" } else { "U8" }; - - report_problem_as( - &format!(indoc!( - r#" - when {}{} is - {}{} -> 1 - _ -> 1 - "# - ), number, bad_suffix, number, $suffix), - &format!(indoc!( - r#" - ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ - - The branches of this `when` expression don't match the condition: - - 1│> when {}{} is - 2│ {}{} -> 1 - 3│ _ -> 1 - - The `when` condition is an integer of type: - - {} - - But the branch patterns have type: - - {} - - The branches must be cases of the `when` condition's type! - "# - ), number, bad_suffix, number, $suffix, bad_type, typ), - ) - } - )*} - } - - mismatched_suffix_tests_in_pattern! { - 1, "u8", mismatched_suffix_u8_pattern - 1, "u16", mismatched_suffix_u16_pattern - 1, "u32", mismatched_suffix_u32_pattern - 1, "u64", mismatched_suffix_u64_pattern - 1, "u128", mismatched_suffix_u128_pattern - 1, "i8", mismatched_suffix_i8_pattern - 1, "i16", mismatched_suffix_i16_pattern - 1, "i32", mismatched_suffix_i32_pattern - 1, "i64", mismatched_suffix_i64_pattern - 1, "i128", mismatched_suffix_i128_pattern - 1, "nat", mismatched_suffix_nat_pattern - 1, "dec", mismatched_suffix_dec_pattern - 1, "f32", mismatched_suffix_f32_pattern - 1, "f64", mismatched_suffix_f64_pattern - } - - #[test] - fn bad_numeric_literal_suffix() { - report_problem_as( - indoc!( - r#" - 1u256 - "# - ), - // TODO: link to number suffixes - indoc!( - r#" - ── SYNTAX PROBLEM ──────────────────────────────────────── /code/proj/Main.roc ─ - - This integer literal contains an invalid digit: - - 1│ 1u256 - ^^^^^ - - Integer literals can only contain the digits - 0-9, or have an integer suffix. - - Tip: Learn more about number literals at TODO - "# - ), - ) - } - - #[test] - fn numer_literal_multi_suffix() { - report_problem_as( - indoc!( - r#" - 1u8u8 - "# - ), - // TODO: link to number suffixes - indoc!( - r#" - ── SYNTAX PROBLEM ──────────────────────────────────────── /code/proj/Main.roc ─ - - This integer literal contains an invalid digit: - - 1│ 1u8u8 - ^^^^^ - - Integer literals can only contain the digits - 0-9, or have an integer suffix. - - Tip: Learn more about number literals at TODO - "# - ), - ) - } - - #[test] - fn int_literal_has_float_suffix() { - report_problem_as( - indoc!( - r#" - 0b1f32 - "# - ), - indoc!( - r#" - ── CONFLICTING NUMBER SUFFIX ───────────────────────────── /code/proj/Main.roc ─ - - This number literal is an integer, but it has a float suffix: - - 1│ 0b1f32 - ^^^^^^ - "# - ), - ) - } - - #[test] - fn float_literal_has_int_suffix() { - report_problem_as( - indoc!( - r#" - 1.0u8 - "# - ), - indoc!( - r#" - ── CONFLICTING NUMBER SUFFIX ───────────────────────────── /code/proj/Main.roc ─ - - This number literal is a float, but it has an integer suffix: - - 1│ 1.0u8 - ^^^^^ - "# - ), - ) - } - - #[test] - fn u8_overflow() { - report_problem_as( - "256u8", - indoc!( - r#" - ── NUMBER OVERFLOWS SUFFIX ─────────────────────────────── /code/proj/Main.roc ─ - - This integer literal overflows the type indicated by its suffix: - - 1│ 256u8 - ^^^^^ - - Tip: The suffix indicates this integer is a U8, whose maximum value is - 255. - "# - ), - ) - } - - #[test] - fn negative_u8() { - report_problem_as( - "-1u8", - indoc!( - r#" - ── NUMBER UNDERFLOWS SUFFIX ────────────────────────────── /code/proj/Main.roc ─ - - This integer literal underflows the type indicated by its suffix: - - 1│ -1u8 - ^^^^ - - Tip: The suffix indicates this integer is a U8, whose minimum value is - 0. - "# - ), - ) - } - - #[test] - fn u16_overflow() { - report_problem_as( - "65536u16", - indoc!( - r#" - ── NUMBER OVERFLOWS SUFFIX ─────────────────────────────── /code/proj/Main.roc ─ - - This integer literal overflows the type indicated by its suffix: - - 1│ 65536u16 - ^^^^^^^^ - - Tip: The suffix indicates this integer is a U16, whose maximum value - is 65535. - "# - ), - ) - } - - #[test] - fn negative_u16() { - report_problem_as( - "-1u16", - indoc!( - r#" - ── NUMBER UNDERFLOWS SUFFIX ────────────────────────────── /code/proj/Main.roc ─ - - This integer literal underflows the type indicated by its suffix: - - 1│ -1u16 - ^^^^^ - - Tip: The suffix indicates this integer is a U16, whose minimum value - is 0. - "# - ), - ) - } - - #[test] - fn u32_overflow() { - report_problem_as( - "4_294_967_296u32", - indoc!( - r#" - ── NUMBER OVERFLOWS SUFFIX ─────────────────────────────── /code/proj/Main.roc ─ - - This integer literal overflows the type indicated by its suffix: - - 1│ 4_294_967_296u32 - ^^^^^^^^^^^^^^^^ - - Tip: The suffix indicates this integer is a U32, whose maximum value - is 4_294_967_295. - "# - ), - ) - } - - #[test] - fn negative_u32() { - report_problem_as( - "-1u32", - indoc!( - r#" - ── NUMBER UNDERFLOWS SUFFIX ────────────────────────────── /code/proj/Main.roc ─ - - This integer literal underflows the type indicated by its suffix: - - 1│ -1u32 - ^^^^^ - - Tip: The suffix indicates this integer is a U32, whose minimum value - is 0. - "# - ), - ) - } - - #[test] - fn u64_overflow() { - report_problem_as( - "18_446_744_073_709_551_616u64", - indoc!( - r#" - ── NUMBER OVERFLOWS SUFFIX ─────────────────────────────── /code/proj/Main.roc ─ - - This integer literal overflows the type indicated by its suffix: - - 1│ 18_446_744_073_709_551_616u64 - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - - Tip: The suffix indicates this integer is a U64, whose maximum value - is 18_446_744_073_709_551_615. - "# - ), - ) - } - - #[test] - fn negative_u64() { - report_problem_as( - "-1u64", - indoc!( - r#" - ── NUMBER UNDERFLOWS SUFFIX ────────────────────────────── /code/proj/Main.roc ─ - - This integer literal underflows the type indicated by its suffix: - - 1│ -1u64 - ^^^^^ - - Tip: The suffix indicates this integer is a U64, whose minimum value - is 0. - "# - ), - ) - } - - #[test] - fn negative_u128() { - report_problem_as( - "-1u128", - indoc!( - r#" - ── NUMBER UNDERFLOWS SUFFIX ────────────────────────────── /code/proj/Main.roc ─ - - This integer literal underflows the type indicated by its suffix: - - 1│ -1u128 - ^^^^^^ - - Tip: The suffix indicates this integer is a U128, whose minimum value - is 0. - "# - ), - ) - } - - #[test] - fn i8_overflow() { - report_problem_as( - "128i8", - indoc!( - r#" - ── NUMBER OVERFLOWS SUFFIX ─────────────────────────────── /code/proj/Main.roc ─ - - This integer literal overflows the type indicated by its suffix: - - 1│ 128i8 - ^^^^^ - - Tip: The suffix indicates this integer is a I8, whose maximum value is - 127. - "# - ), - ) - } - - #[test] - fn i8_underflow() { - report_problem_as( - "-129i8", - indoc!( - r#" - ── NUMBER UNDERFLOWS SUFFIX ────────────────────────────── /code/proj/Main.roc ─ - - This integer literal underflows the type indicated by its suffix: - - 1│ -129i8 - ^^^^^^ - - Tip: The suffix indicates this integer is a I8, whose minimum value is - -128. - "# - ), - ) - } - - #[test] - fn i16_overflow() { - report_problem_as( - "32768i16", - indoc!( - r#" - ── NUMBER OVERFLOWS SUFFIX ─────────────────────────────── /code/proj/Main.roc ─ - - This integer literal overflows the type indicated by its suffix: - - 1│ 32768i16 - ^^^^^^^^ - - Tip: The suffix indicates this integer is a I16, whose maximum value - is 32767. - "# - ), - ) - } - - #[test] - fn i16_underflow() { - report_problem_as( - "-32769i16", - indoc!( - r#" - ── NUMBER UNDERFLOWS SUFFIX ────────────────────────────── /code/proj/Main.roc ─ - - This integer literal underflows the type indicated by its suffix: - - 1│ -32769i16 - ^^^^^^^^^ - - Tip: The suffix indicates this integer is a I16, whose minimum value - is -32768. - "# - ), - ) - } - - #[test] - fn i32_overflow() { - report_problem_as( - "2_147_483_648i32", - indoc!( - r#" - ── NUMBER OVERFLOWS SUFFIX ─────────────────────────────── /code/proj/Main.roc ─ - - This integer literal overflows the type indicated by its suffix: - - 1│ 2_147_483_648i32 - ^^^^^^^^^^^^^^^^ - - Tip: The suffix indicates this integer is a I32, whose maximum value - is 2_147_483_647. - "# - ), - ) - } - - #[test] - fn i32_underflow() { - report_problem_as( - "-2_147_483_649i32", - indoc!( - r#" - ── NUMBER UNDERFLOWS SUFFIX ────────────────────────────── /code/proj/Main.roc ─ - - This integer literal underflows the type indicated by its suffix: - - 1│ -2_147_483_649i32 - ^^^^^^^^^^^^^^^^^ - - Tip: The suffix indicates this integer is a I32, whose minimum value - is -2_147_483_648. - "# - ), - ) - } - - #[test] - fn i64_overflow() { - report_problem_as( - "9_223_372_036_854_775_808i64", - indoc!( - r#" - ── NUMBER OVERFLOWS SUFFIX ─────────────────────────────── /code/proj/Main.roc ─ - - This integer literal overflows the type indicated by its suffix: - - 1│ 9_223_372_036_854_775_808i64 - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - - Tip: The suffix indicates this integer is a I64, whose maximum value - is 9_223_372_036_854_775_807. - "# - ), - ) - } - - #[test] - fn i64_underflow() { - report_problem_as( - "-9_223_372_036_854_775_809i64", - indoc!( - r#" - ── NUMBER UNDERFLOWS SUFFIX ────────────────────────────── /code/proj/Main.roc ─ - - This integer literal underflows the type indicated by its suffix: - - 1│ -9_223_372_036_854_775_809i64 - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - - Tip: The suffix indicates this integer is a I64, whose minimum value - is -9_223_372_036_854_775_808. - "# - ), - ) - } - - #[test] - fn i128_overflow() { - report_problem_as( - "170_141_183_460_469_231_731_687_303_715_884_105_728i128", - indoc!( - r#" - ── NUMBER OVERFLOWS SUFFIX ─────────────────────────────── /code/proj/Main.roc ─ - - This integer literal overflows the type indicated by its suffix: - - 1│ 170_141_183_460_469_231_731_687_303_715_884_105_728i128 - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - - Tip: The suffix indicates this integer is a I128, whose maximum value - is 170_141_183_460_469_231_731_687_303_715_884_105_727. - "# - ), - ) - } - - #[test] - fn list_get_negative_number() { - report_problem_as( - indoc!( - r#" - List.get [1,2,3] -1 - "# - ), - // TODO: this error message could be improved, e.g. something like "This argument can - // be used as ... because of its literal value" - indoc!( - r#" - ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ - - The 2nd argument to `get` is not what I expect: - - 1│ List.get [1,2,3] -1 - ^^ - - This argument is a number of type: - - I8, I16, I32, I64, I128, F32, F64, or Dec - - But `get` needs the 2nd argument to be: - - Nat - "# - ), - ) - } - - #[test] - fn list_get_negative_number_indirect() { - report_problem_as( - indoc!( - r#" - a = -9_223_372_036_854 - List.get [1,2,3] a - "# - ), - indoc!( - r#" - ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ - - The 2nd argument to `get` is not what I expect: - - 2│ List.get [1,2,3] a - ^ - - This `a` value is a: - - I64, I128, F32, F64, or Dec - - But `get` needs the 2nd argument to be: - - Nat - "# - ), - ) - } - - #[test] - fn list_get_negative_number_double_indirect() { - report_problem_as( - indoc!( - r#" - a = -9_223_372_036_854 - b = a - List.get [1,2,3] b - "# - ), - indoc!( - r#" - ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ - - The 2nd argument to `get` is not what I expect: - - 3│ List.get [1,2,3] b - ^ - - This `b` value is a: - - I64, I128, F32, F64, or Dec - - But `get` needs the 2nd argument to be: - - Nat - "# - ), - ) - } - - #[test] - fn compare_unsigned_to_signed() { - report_problem_as( - indoc!( - r#" - when -1 is - 1u8 -> 1 - _ -> 1 - "# - ), - indoc!( - r#" - ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ - - The branches of this `when` expression don't match the condition: - - 1│> when -1 is - 2│ 1u8 -> 1 - 3│ _ -> 1 - - The `when` condition is a number of type: - - I8, I16, I32, I64, I128, F32, F64, or Dec - - But the branch patterns have type: - - U8 - - The branches must be cases of the `when` condition's type! - "# - ), - ) - } - - #[test] - fn recursive_type_alias_is_newtype() { - report_problem_as( - indoc!( - r#" - R a : [Only (R a)] - - v : R Str - v - "# - ), - indoc!( - r#" - ── CYCLIC ALIAS ────────────────────────────────────────── /code/proj/Main.roc ─ - - The `R` alias is self-recursive in an invalid way: - - 1│ R a : [Only (R a)] - ^ - - Recursion in aliases is only allowed if recursion happens behind a - tagged union, at least one variant of which is not recursive. - "# - ), - ) - } - - #[test] - fn recursive_type_alias_is_newtype_deep() { - report_problem_as( - indoc!( - r#" - R a : [Only { very: [Deep (R a)] }] - - v : R Str - v - "# - ), - indoc!( - r#" - ── CYCLIC ALIAS ────────────────────────────────────────── /code/proj/Main.roc ─ - - The `R` alias is self-recursive in an invalid way: - - 1│ R a : [Only { very: [Deep (R a)] }] - ^ - - Recursion in aliases is only allowed if recursion happens behind a - tagged union, at least one variant of which is not recursive. - "# - ), - ) - } - - #[test] - fn recursive_type_alias_is_newtype_mutual() { - report_problem_as( - indoc!( - r#" - Foo a : [Thing (Bar a)] - Bar a : [Stuff (Foo a)] - - v : Bar Str - v - "# - ), - indoc!( - r#" - ── CYCLIC ALIAS ────────────────────────────────────────── /code/proj/Main.roc ─ - - The `Foo` alias is recursive in an invalid way: - - 1│ Foo a : [Thing (Bar a)] - ^^^ - - The `Foo` alias depends on itself through the following chain of - definitions: - - ┌─────┐ - │ Foo - │ ↓ - │ Bar - └─────┘ - - Recursion in aliases is only allowed if recursion happens behind a - tagged union, at least one variant of which is not recursive. - "# - ), - ) - } - - #[test] - fn issue_2458() { - report_problem_as( - indoc!( - r#" - Result a b : [Ok a, Err b] - - Foo a : [Blah (Result (Bar a) [])] - Bar a : Foo a - - v : Bar Str - v - "# - ), - "", - ) - } - - #[test] - fn opaque_type_not_in_scope() { - report_problem_as( - indoc!( - r#" - @Age 21 - "# - ), - indoc!( - r#" - ── OPAQUE TYPE NOT DEFINED ─────────────────────────────── /code/proj/Main.roc ─ - - The opaque type Age referenced here is not defined: - - 1│ @Age 21 - ^^^^ - - Note: It looks like there are no opaque types declared in this scope yet! - "# - ), - ) - } - - #[test] - fn opaque_reference_not_opaque_type() { - report_problem_as( - indoc!( - r#" - Age : Num.U8 - - @Age 21 - "# - ), - indoc!( - r#" - ── OPAQUE TYPE NOT DEFINED ─────────────────────────────── /code/proj/Main.roc ─ - - The opaque type Age referenced here is not defined: - - 3│ @Age 21 - ^^^^ - - Note: There is an alias of the same name: - - 1│ Age : Num.U8 - ^^^ - - Note: It looks like there are no opaque types declared in this scope yet! - - ── UNUSED DEFINITION ───────────────────────────────────── /code/proj/Main.roc ─ - - `Age` is not used anywhere in your code. - - 1│ Age : Num.U8 - ^^^^^^^^^^^^ - - If you didn't intend on using `Age` then remove it so future readers of - your code don't wonder why it is there. - "# - ), - ) - } - - #[test] - fn qualified_opaque_reference() { - report_problem_as( - indoc!( - r#" - OtherModule.@Age 21 - "# - ), - // TODO: get rid of the first error. Consider parsing OtherModule.@Age to completion - // and checking it during can. The reason the error appears is because it is parsed as - // Apply(Error(OtherModule), [@Age, 21]) - indoc!( - r#" - ── OPAQUE TYPE NOT APPLIED ─────────────────────────────── /code/proj/Main.roc ─ - - This opaque type is not applied to an argument: - - 1│ OtherModule.@Age 21 - ^^^^ - - Note: Opaque types always wrap exactly one argument! - - ── SYNTAX PROBLEM ──────────────────────────────────────── /code/proj/Main.roc ─ - - I am trying to parse a qualified name here: - - 1│ OtherModule.@Age 21 - ^ - - I was expecting to see an identifier next, like height. A complete - qualified name looks something like Json.Decode.string. - "# - ), - ) - } - - #[test] - fn opaque_used_outside_declaration_scope() { - report_problem_as( - indoc!( - r#" - age = - Age := Num.U8 - 21u8 - - @Age age - "# - ), - // TODO(opaques): there is a potential for a better error message here, if the usage of - // `@Age` can be linked to the declaration of `Age` inside `age`, and a suggestion to - // raise that declaration to the outer scope. - indoc!( - r#" - ── UNUSED DEFINITION ───────────────────────────────────── /code/proj/Main.roc ─ - - `Age` is not used anywhere in your code. - - 2│ Age := Num.U8 - ^^^^^^^^^^^^^ - - If you didn't intend on using `Age` then remove it so future readers of - your code don't wonder why it is there. - - ── OPAQUE TYPE NOT DEFINED ─────────────────────────────── /code/proj/Main.roc ─ - - The opaque type Age referenced here is not defined: - - 5│ @Age age - ^^^^ - - Note: It looks like there are no opaque types declared in this scope yet! - "# - ), - ) - } - - #[test] - fn unimported_modules_reported() { - report_problem_as( - indoc!( - r#" - main : Task.Task {} [] - main = "whatever man you don't even know my type" - main - "# - ), - indoc!( - r#" - ── MODULE NOT IMPORTED ─────────────────────────────────── /code/proj/Main.roc ─ - - The `Task` module is not imported: - - 1│ main : Task.Task {} [] - ^^^^^^^^^^^^^^^ - - Is there an import missing? Perhaps there is a typo. Did you mean one - of these? - - Test - List - Num - Box - "# - ), - ) - } - - #[test] - fn opaque_mismatch_check() { - report_problem_as( - indoc!( - r#" - Age := Num.U8 - - n : Age - n = @Age "" - - n - "# - ), - // TODO(opaques): error could be improved by saying that the opaque definition demands - // that the argument be a U8, and linking to the definitin! - indoc!( - r#" - ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ - - This expression is used in an unexpected way: - - 4│ n = @Age "" - ^^ - - This argument to an opaque type has type: - - Str - - But you are trying to use it as: - - U8 - "# - ), - ) - } - - #[test] - fn opaque_mismatch_infer() { - report_problem_as( - indoc!( - r#" - F n := n - - if True - then @F "" - else @F {} - "# - ), - indoc!( - r#" - ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ - - This expression is used in an unexpected way: - - 5│ else @F {} - ^^ - - This argument to an opaque type has type: - - {} - - But you are trying to use it as: - - Str - "# - ), - ) - } - - #[test] - fn opaque_creation_is_not_wrapped() { - report_problem_as( - indoc!( - r#" - F n := n - - v : F Str - v = "" - - v - "# - ), - indoc!( - r#" - ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ - - Something is off with the body of the `v` definition: - - 3│ v : F Str - 4│ v = "" - ^^ - - The body is a string of type: - - Str - - But the type annotation on `v` says it should be: - - F Str - - Tip: Type comparisons between an opaque type are only ever equal if - both types are the same opaque type. Did you mean to create an opaque - type by wrapping it? If I have an opaque type Age := U32 I can create - an instance of this opaque type by doing @Age 23. - "# - ), - ) - } - - #[test] - fn opaque_mismatch_pattern_check() { - report_problem_as( - indoc!( - r#" - Age := Num.U8 - - f : Age -> Num.U8 - f = \Age n -> n - - f - "# - ), - // TODO(opaques): error could be improved by saying that the user-provided pattern - // probably wants to change "Age" to "@Age"! - indoc!( - r#" - ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ - - The 1st argument to `f` is weird: - - 4│ f = \Age n -> n - ^^^^^ - - The argument is a pattern that matches a `Age` tag of type: - - [Age a] - - But the annotation on `f` says the 1st argument should be: - - Age - - Tip: Type comparisons between an opaque type are only ever equal if - both types are the same opaque type. Did you mean to create an opaque - type by wrapping it? If I have an opaque type Age := U32 I can create - an instance of this opaque type by doing @Age 23. - "# - ), - ) - } - - #[test] - fn opaque_mismatch_pattern_infer() { - report_problem_as( - indoc!( - r#" - F n := n - - \x -> - when x is - @F A -> "" - @F {} -> "" - "# - ), - indoc!( - r#" - ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ - - The 2nd pattern in this `when` does not match the previous ones: - - 6│ @F {} -> "" - ^^^^^ - - The 2nd pattern is trying to matchF unwrappings of type: - - F {}a - - But all the previous branches match: - - F [A]a - "# - ), - ) - } - - #[test] - fn opaque_pattern_match_not_exhaustive_tag() { - report_problem_as( - indoc!( - r#" - F n := n - - v : F [A, B, C] - - when v is - @F A -> "" - @F B -> "" - "# - ), - indoc!( - r#" - ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ - - The branches of this `when` expression don't match the condition: - - 5│> when v is - 6│ @F A -> "" - 7│ @F B -> "" - - This `v` value is a: - - F [A, B, C] - - But the branch patterns have type: - - F [A, B] - - The branches must be cases of the `when` condition's type! - - Tip: Looks like the branches are missing coverage of the `C` tag. - - Tip: Maybe you need to add a catch-all branch, like `_`? - "# - ), - ) - } - - #[test] - fn opaque_pattern_match_not_exhaustive_int() { - report_problem_as( - indoc!( - r#" - F n := n - - v : F Num.U8 - - when v is - @F 1 -> "" - @F 2 -> "" - "# - ), - indoc!( - r#" - ── UNSAFE PATTERN ──────────────────────────────────────── /code/proj/Main.roc ─ - - This `when` does not cover all the possibilities: - - 5│> when v is - 6│> @F 1 -> "" - 7│> @F 2 -> "" - - Other possibilities include: - - @F _ - - I would have to crash if I saw one of those! Add branches for them! - "# - ), - ) - } - - #[test] - fn let_polymorphism_with_scoped_type_variables() { - report_problem_as( - indoc!( - r#" - f : a -> a - f = \x -> - y : a -> a - y = \z -> z - - n = y 1u8 - x1 = y x - (\_ -> x1) n - - f - "# - ), - indoc!( - r#" - ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ - - The 1st argument to `y` is not what I expect: - - 6│ n = y 1u8 - ^^^ - - This argument is an integer of type: - - U8 - - But `y` needs the 1st argument to be: - - a - - Tip: The type annotation uses the type variable `a` to say that this - definition can produce any type of value. But in the body I see that - it will only produce a `U8` value of a single specific type. Maybe - change the type annotation to be more specific? Maybe change the code - to be more general? - "# - ), - ) - } - - #[test] - fn non_exhaustive_with_guard() { - report_problem_as( - indoc!( - r#" - x : [A] - when x is - A if True -> "" - "# - ), - indoc!( - r#" - ── UNSAFE PATTERN ──────────────────────────────────────── /code/proj/Main.roc ─ - - This `when` does not cover all the possibilities: - - 2│> when x is - 3│> A if True -> "" - - Other possibilities include: - - A (note the lack of an if clause) - - I would have to crash if I saw one of those! Add branches for them! - "# - ), - ) - } - - #[test] - fn invalid_record_extension_type() { - report_problem_as( - indoc!( - r#" - f : { x : Num.Nat }[] - f - "# - ), - indoc!( - r#" - ── INVALID_EXTENSION_TYPE ──────────────────────────────── /code/proj/Main.roc ─ - - This record extension type is invalid: - - 1│ f : { x : Num.Nat }[] - ^^ - - Note: A record extension variable can only contain a type variable or - another record. - "# - ), - ) - } - - #[test] - fn invalid_tag_extension_type() { - report_problem_as( - indoc!( - r#" - f : [A]Str - f - "# - ), - indoc!( - r#" - ── INVALID_EXTENSION_TYPE ──────────────────────────────── /code/proj/Main.roc ─ - - This tag union extension type is invalid: - - 1│ f : [A]Str - ^^^ - - Note: A tag union extension variable can only contain a type variable - or another tag union. - "# - ), - ) - } - - #[test] - fn unknown_type() { - report_problem_as( - indoc!( - r#" - Type : [Constructor UnknownType] - - insertHelper : UnknownType, Type -> Type - insertHelper = \h, m -> - when m is - Constructor _ -> Constructor h - - insertHelper - "# - ), - indoc!( - r#" - ── UNRECOGNIZED NAME ───────────────────────────────────── /code/proj/Main.roc ─ - - Nothing is named `UnknownType` in this scope. - - 1│ Type : [Constructor UnknownType] - ^^^^^^^^^^^ - - Did you mean one of these? - - Type - True - Box - Ok - - ── UNRECOGNIZED NAME ───────────────────────────────────── /code/proj/Main.roc ─ - - Nothing is named `UnknownType` in this scope. - - 3│ insertHelper : UnknownType, Type -> Type - ^^^^^^^^^^^ - - Did you mean one of these? - - Type - True - insertHelper - Box - "# - ), - ) - } - - #[test] - fn ability_first_demand_not_indented_enough() { - report_problem_as( - indoc!( - r#" - Eq has - eq : a, a -> U64 | a has Eq - - 1 - "# - ), - indoc!( - r#" - ── UNFINISHED ABILITY ──────────────────────────────────── /code/proj/Main.roc ─ - - I was partway through parsing an ability definition, but I got stuck - here: - - 1│ Eq has - 2│ eq : a, a -> U64 | a has Eq - ^ - - I suspect this line is not indented enough (by 1 spaces) - "# - ), - ) - } - - #[test] - fn ability_demands_not_indented_with_first() { - new_report_problem_as( - "ability_demands_not_indented_with_first", - indoc!( - r#" - Eq has - eq : a, a -> U64 | a has Eq - neq : a, a -> U64 | a has Eq - - 1 - "# - ), - indoc!( - r#" - ── UNFINISHED ABILITY ─── tmp/ability_demands_not_indented_with_first/Test.roc ─ - - I was partway through parsing an ability definition, but I got stuck - here: - - 5│ eq : a, a -> U64 | a has Eq - 6│ neq : a, a -> U64 | a has Eq - ^ - - I suspect this line is indented too much (by 4 spaces)"# - ), - ) - } - - #[test] - fn ability_demand_value_has_args() { - new_report_problem_as( - "ability_demand_value_has_args", - indoc!( - r#" - Eq has - eq b c : a, a -> U64 | a has Eq - - 1 - "# - ), - indoc!( - r#" - ── UNFINISHED ABILITY ───────────── tmp/ability_demand_value_has_args/Test.roc ─ - - I was partway through parsing an ability definition, but I got stuck - here: - - 5│ eq b c : a, a -> U64 | a has Eq - ^ - - I was expecting to see a : annotating the signature of this value - next."# - ), - ) - } - - #[test] - fn ability_non_signature_expression() { - report_problem_as( - indoc!( - r#" - Eq has - 123 - - 1 - "# - ), - indoc!( - r#" - ── UNFINISHED ABILITY ──────────────────────────────────── /code/proj/Main.roc ─ - - I was partway through parsing an ability definition, but I got stuck - here: - - 1│ Eq has - 2│ 123 - ^ - - I was expecting to see a value signature next. - "# - ), - ) - } - - #[test] - fn wildcard_in_alias() { - report_problem_as( - indoc!( - r#" - I : Num.Int * - a : I - a - "# - ), - indoc!( - r#" - ── UNBOUND TYPE VARIABLE ───────────────────────────────── /code/proj/Main.roc ─ - - The definition of `I` has an unbound type variable: - - 1│ I : Num.Int * - ^ - - Tip: Type variables must be bound before the `:`. Perhaps you intended - to add a type parameter to this type? - "# - ), - ) - } - - #[test] - fn wildcard_in_opaque() { - report_problem_as( - indoc!( - r#" - I := Num.Int * - a : I - a - "# - ), - indoc!( - r#" - ── UNBOUND TYPE VARIABLE ───────────────────────────────── /code/proj/Main.roc ─ - - The definition of `I` has an unbound type variable: - - 1│ I := Num.Int * - ^ - - Tip: Type variables must be bound before the `:=`. Perhaps you intended - to add a type parameter to this type? - "# - ), - ) - } - - #[test] - fn multiple_wildcards_in_alias() { - report_problem_as( - indoc!( - r#" - I : [A (Num.Int *), B (Num.Int *)] - a : I - a - "# - ), - indoc!( - r#" - ── UNBOUND TYPE VARIABLE ───────────────────────────────── /code/proj/Main.roc ─ - - The definition of `I` has 2 unbound type variables. - - Here is one occurrence: - - 1│ I : [A (Num.Int *), B (Num.Int *)] - ^ - - Tip: Type variables must be bound before the `:`. Perhaps you intended - to add a type parameter to this type? - "# - ), - ) - } - - #[test] - fn inference_var_in_alias() { - report_problem_as( - indoc!( - r#" - I : Num.Int _ - a : I - a - "# - ), - indoc!( - r#" - ── UNBOUND TYPE VARIABLE ───────────────────────────────── /code/proj/Main.roc ─ - - The definition of `I` has an unbound type variable: - - 1│ I : Num.Int _ - ^ - - Tip: Type variables must be bound before the `:`. Perhaps you intended - to add a type parameter to this type? - "# - ), - ) - } - - #[test] - fn unbound_var_in_alias() { - report_problem_as( - indoc!( - r#" - I : Num.Int a - a : I - a - "# - ), - indoc!( - r#" - ── UNBOUND TYPE VARIABLE ───────────────────────────────── /code/proj/Main.roc ─ - - The definition of `I` has an unbound type variable: - - 1│ I : Num.Int a - ^ - - Tip: Type variables must be bound before the `:`. Perhaps you intended - to add a type parameter to this type? - "# - ), - ) - } - - #[test] - fn ability_bad_type_parameter() { - new_report_problem_as( - "ability_bad_type_parameter", - indoc!( - r#" - app "test" provides [] to "./platform" - - Hash a b c has - hash : a -> U64 | a has Hash - "# - ), - indoc!( - r#" - ── ABILITY HAS TYPE VARIABLES ──────────────────────────── /code/proj/Main.roc ─ - - The definition of the `Hash` ability includes type variables: - - 3│ Hash a b c has - ^^^^^ - - Abilities cannot depend on type variables, but their member values - can! - - ── UNUSED DEFINITION ───────────────────────────────────── /code/proj/Main.roc ─ - - `Hash` is not used anywhere in your code. - - 3│ Hash a b c has - ^^^^ - - If you didn't intend on using `Hash` then remove it so future readers of - your code don't wonder why it is there. - "# - ), - ) - } - - #[test] - fn alias_in_has_clause() { - new_report_problem_as( - "alias_in_has_clause", - indoc!( - r#" - app "test" provides [hash] to "./platform" - - Hash has hash : a, b -> Num.U64 | a has Hash, b has Bool.Bool - "# - ), - indoc!( - r#" - ── HAS CLAUSE IS NOT AN ABILITY ────────────────────────── /code/proj/Main.roc ─ - - The type referenced in this "has" clause is not an ability: - - 3│ Hash has hash : a, b -> Num.U64 | a has Hash, b has Bool.Bool - ^^^^^^^^^ - "# - ), - ) - } - - #[test] - fn shadowed_type_variable_in_has_clause() { - new_report_problem_as( - "shadowed_type_variable_in_has_clause", - indoc!( - r#" - app "test" provides [ab1] to "./platform" - - Ab1 has ab1 : a -> {} | a has Ab1, a has Ab1 - "# - ), - indoc!( - r#" - ── DUPLICATE NAME ──────────────────────────────────────── /code/proj/Main.roc ─ - - The `a` name is first defined here: - - 3│ Ab1 has ab1 : a -> {} | a has Ab1, a has Ab1 - ^^^^^^^^^ - - But then it's defined a second time here: - - 3│ Ab1 has ab1 : a -> {} | a has Ab1, a has Ab1 - ^^^^^^^^^ - - Since these variables have the same name, it's easy to use the wrong - one on accident. Give one of them a new name. - "# - ), - ) - } - - #[test] - fn ability_shadows_ability() { - new_report_problem_as( - "ability_shadows_ability", - indoc!( - r#" - app "test" provides [ab] to "./platform" - - Ability has ab : a -> U64 | a has Ability - - Ability has ab1 : a -> U64 | a has Ability - "# - ), - indoc!( - r#" - ── DUPLICATE NAME ──────────────────────────────────────── /code/proj/Main.roc ─ - - The `Ability` name is first defined here: - - 3│ Ability has ab : a -> U64 | a has Ability - ^^^^^^^ - - But then it's defined a second time here: - - 5│ Ability has ab1 : a -> U64 | a has Ability - ^^^^^^^ - - Since these abilities have the same name, it's easy to use the wrong - one on accident. Give one of them a new name. - "# - ), - ) - } - - #[test] - fn ability_member_does_not_bind_ability() { - new_report_problem_as( - "ability_member_does_not_bind_ability", - indoc!( - r#" - app "test" provides [] to "./platform" - - Ability has ab : {} -> {} - "# - ), - indoc!( - r#" - ── ABILITY MEMBER MISSING HAS CLAUSE ───────────────────── /code/proj/Main.roc ─ - - The definition of the ability member `ab` does not include a `has` clause - binding a type variable to the ability `Ability`: - - 3│ Ability has ab : {} -> {} - ^^ - - Ability members must include a `has` clause binding a type variable to - an ability, like - - a has Ability - - Otherwise, the function does not need to be part of the ability! - - ── UNUSED DEFINITION ───────────────────────────────────── /code/proj/Main.roc ─ - - `Ability` is not used anywhere in your code. - - 3│ Ability has ab : {} -> {} - ^^^^^^^ - - If you didn't intend on using `Ability` then remove it so future readers - of your code don't wonder why it is there. - "# - ), - ) - } - - #[test] - fn ability_member_binds_parent_twice() { - new_report_problem_as( - "ability_member_binds_parent_twice", - indoc!( - r#" - app "test" provides [] to "./platform" - - Eq has eq : a, b -> Bool.Bool | a has Eq, b has Eq - "# - ), - indoc!( - r#" - ── ABILITY MEMBER BINDS MULTIPLE VARIABLES ─────────────── /code/proj/Main.roc ─ - - The definition of the ability member `eq` includes multiple variables - bound to the `Eq`` ability:` - - 3│ Eq has eq : a, b -> Bool.Bool | a has Eq, b has Eq - ^^^^^^^^^^^^^^^^^^ - - Ability members can only bind one type variable to their parent - ability. Otherwise, I wouldn't know what type implements an ability by - looking at specializations! - - Hint: Did you mean to only bind `a` to `Eq`? - "# - ), - ) - } - - #[test] - fn has_clause_not_on_toplevel() { - new_report_problem_as( - "has_clause_outside_of_ability", - indoc!( - r#" - app "test" provides [f] to "./platform" - - Hash has hash : (a | a has Hash) -> Num.U64 - - f : a -> Num.U64 | a has Hash - "# - ), - indoc!( - r#" - ── ILLEGAL HAS CLAUSE ──────────────────────────────────── /code/proj/Main.roc ─ - - A `has` clause is not allowed here: - - 3│ Hash has hash : (a | a has Hash) -> Num.U64 - ^^^^^^^^^^ - - `has` clauses can only be specified on the top-level type annotations. - - ── ABILITY MEMBER MISSING HAS CLAUSE ───────────────────── /code/proj/Main.roc ─ - - The definition of the ability member `hash` does not include a `has` - clause binding a type variable to the ability `Hash`: - - 3│ Hash has hash : (a | a has Hash) -> Num.U64 - ^^^^ - - Ability members must include a `has` clause binding a type variable to - an ability, like - - a has Hash - - Otherwise, the function does not need to be part of the ability! - "# - ), - ) - } - - #[test] - fn ability_specialization_with_non_implementing_type() { - new_report_problem_as( - "ability_specialization_with_non_implementing_type", - indoc!( - r#" - app "test" provides [hash] to "./platform" - - Hash has hash : a -> Num.U64 | a has Hash - - hash = \{} -> 0u64 - "# - ), - indoc!( - r#" - ── ILLEGAL SPECIALIZATION ──────────────────────────────── /code/proj/Main.roc ─ - - This specialization of `hash` is for a non-opaque type: - - 5│ hash = \{} -> 0u64 - ^^^^ - - It is specialized for - - {}a - - but structural types can never specialize abilities! - - Note: `hash` is a member of `#UserApp.Hash` - "# - ), - ) - } - - #[test] - fn ability_specialization_does_not_match_type() { - new_report_problem_as( - "ability_specialization_does_not_match_type", - indoc!( - r#" - app "test" provides [hash] to "./platform" - - Hash has hash : a -> U64 | a has Hash - - Id := U32 - - hash = \@Id n -> n - "# - ), - indoc!( - r#" - ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ - - Something is off with this specialization of `hash`: - - 7│ hash = \@Id n -> n - ^^^^ - - This value is a declared specialization of type: - - Id -> U32 - - But the type annotation on `hash` says it must match: - - Id -> U64 - "# - ), - ) - } - - #[test] - fn ability_specialization_is_incomplete() { - new_report_problem_as( - "ability_specialization_is_incomplete", - indoc!( - r#" - app "test" provides [eq, le] to "./platform" - - Eq has - eq : a, a -> Bool | a has Eq - le : a, a -> Bool | a has Eq - - Id := U64 - - eq = \@Id m, @Id n -> m == n - "# - ), - indoc!( - r#" - ── INCOMPLETE ABILITY IMPLEMENTATION ───────────────────── /code/proj/Main.roc ─ - - The type `Id` does not fully implement the ability `Eq`. The following - specializations are missing: - - A specialization for `le`, which is defined here: - - 5│ le : a, a -> Bool | a has Eq - ^^ - "# - ), - ) - } - - #[test] - fn ability_specialization_overly_generalized() { - new_report_problem_as( - "ability_specialization_overly_generalized", - indoc!( - r#" - app "test" provides [hash] to "./platform" - - Hash has - hash : a -> U64 | a has Hash - - hash = \_ -> 0u64 - "# - ), - indoc!( - r#" - ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ - - This specialization of `hash` is overly general: - - 6│ hash = \_ -> 0u64 - ^^^^ - - This value is a declared specialization of type: - - a -> U64 - - But the type annotation on `hash` says it must match: - - a -> U64 | a has Hash - - Note: The specialized type is too general, and does not provide a - concrete type where a type variable is bound to an ability. - - Specializations can only be made for concrete types. If you have a - generic implementation for this value, perhaps you don't need an - ability? - "# - ), - ) - } - - #[test] - fn ability_specialization_conflicting_specialization_types() { - new_report_problem_as( - "ability_specialization_conflicting_specialization_types", - indoc!( - r#" - app "test" provides [eq] to "./platform" - - Eq has - eq : a, a -> Bool | a has Eq - - You := {} - AndI := {} - - eq = \@You {}, @AndI {} -> False - "# - ), - indoc!( - r#" - ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ - - Something is off with this specialization of `eq`: - - 9│ eq = \@You {}, @AndI {} -> False - ^^ - - This value is a declared specialization of type: - - You, AndI -> [False, True] - - But the type annotation on `eq` says it must match: - - You, You -> Bool - - Tip: Type comparisons between an opaque type are only ever equal if - both types are the same opaque type. Did you mean to create an opaque - type by wrapping it? If I have an opaque type Age := U32 I can create - an instance of this opaque type by doing @Age 23. - "# - ), - ) - } - - #[test] - fn ability_specialization_checked_against_annotation() { - new_report_problem_as( - "ability_specialization_checked_against_annotation", - indoc!( - r#" - app "test" provides [hash] to "./platform" - - Hash has - hash : a -> U64 | a has Hash - - Id := U64 - - hash : Id -> U32 - hash = \@Id n -> n - "# - ), - indoc!( - r#" - ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ - - Something is off with the body of this definition: - - 8│ hash : Id -> U32 - 9│ hash = \@Id n -> n - ^ - - This `n` value is a: - - U64 - - But the type annotation says it should be: - - U32 - - ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ - - Something is off with this specialization of `hash`: - - 9│ hash = \@Id n -> n - ^^^^ - - This value is a declared specialization of type: - - Id -> U32 - - But the type annotation on `hash` says it must match: - - Id -> U64 - "# - ), - ) - } - - #[test] - fn ability_specialization_called_with_non_specializing() { - new_report_problem_as( - "ability_specialization_called_with_non_specializing", - indoc!( - r#" - app "test" provides [noGoodVeryBadTerrible] to "./platform" - - Hash has - hash : a -> U64 | a has Hash - - Id := U64 - - hash = \@Id n -> n - - User := {} - - noGoodVeryBadTerrible = - { - nope: hash (@User {}), - notYet: hash (A 1), - } - "# - ), - indoc!( - r#" - ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ - - This expression has a type that does not implement the abilities it's expected to: - - 15│ notYet: hash (A 1), - ^^^ - - Roc can't generate an implementation of the `#UserApp.Hash` ability for - - [A (Num a)]b - - Only builtin abilities can have generated implementations! - - ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ - - This expression has a type that does not implement the abilities it's expected to: - - 14│ nope: hash (@User {}), - ^^^^^^^^ - - The type `User` does not fully implement the ability `Hash`. The following - specializations are missing: - - A specialization for `hash`, which is defined here: - - 4│ hash : a -> U64 | a has Hash - ^^^^ - "# - ), - ) - } - - #[test] - fn ability_not_on_toplevel() { - new_report_problem_as( - "ability_not_on_toplevel", - indoc!( - r#" - app "test" provides [main] to "./platform" - - main = - Hash has - hash : a -> U64 | a has Hash - - 123 - "# - ), - indoc!( - r#" - ── ABILITY NOT ON TOP-LEVEL ────────────────────────────── /code/proj/Main.roc ─ - - This ability definition is not on the top-level of a module: - - 4│> Hash has - 5│> hash : a -> U64 | a has Hash - - Abilities can only be defined on the top-level of a Roc module. - "# - ), - ) - } - - #[test] - fn expression_generalization_to_ability_is_an_error() { - new_report_problem_as( - "expression_generalization_to_ability_is_an_error", - indoc!( - r#" - app "test" provides [hash, hashable] to "./platform" - - Hash has - hash : a -> U64 | a has Hash - - Id := U64 - hash = \@Id n -> n - - hashable : a | a has Hash - hashable = @Id 15 - "# - ), - indoc!( - r#" - ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ - - Something is off with the body of the `hashable` definition: - - 9│ hashable : a | a has Hash - 10│ hashable = @Id 15 - ^^^^^^ - - This Id opaque wrapping has the type: - - Id - - But the type annotation on `hashable` says it should be: - - a | a has Hash - - Tip: The type annotation uses the type variable `a` to say that this - definition can produce any value implementing the `Hash` ability. But in - the body I see that it will only produce a `Id` value of a single - specific type. Maybe change the type annotation to be more specific? - Maybe change the code to be more general? - "# - ), - ) - } - - #[test] - fn ability_value_annotations_are_an_error() { - new_report_problem_as( - "ability_value_annotations_are_an_error", - indoc!( - r#" - app "test" provides [result] to "./platform" - - Hash has - hash : a -> U64 | a has Hash - - mulHashes : Hash, Hash -> U64 - mulHashes = \x, y -> hash x * hash y - - Id := U64 - hash = \@Id n -> n - - Three := {} - hash = \@Three _ -> 3 - - result = mulHashes (@Id 100) (@Three {}) - "# - ), - indoc!( - r#" - ── ABILITY USED AS TYPE ────────────────────────────────── /code/proj/Main.roc ─ - - You are attempting to use the ability `Hash` as a type directly: - - 6│ mulHashes : Hash, Hash -> U64 - ^^^^ - - Abilities can only be used in type annotations to constrain type - variables. - - Hint: Perhaps you meant to include a `has` annotation, like - - a has Hash - - ── ABILITY USED AS TYPE ────────────────────────────────── /code/proj/Main.roc ─ - - You are attempting to use the ability `Hash` as a type directly: - - 6│ mulHashes : Hash, Hash -> U64 - ^^^^ - - Abilities can only be used in type annotations to constrain type - variables. - - Hint: Perhaps you meant to include a `has` annotation, like - - b has Hash - "# - ), - ) - } - - #[test] - fn branches_have_more_cases_than_condition() { - new_report_problem_as( - "branches_have_more_cases_than_condition", - indoc!( - r#" - foo : Bool -> Str - foo = \bool -> - when bool is - True -> "true" - False -> "false" - Wat -> "surprise!" - foo - "# - ), - indoc!( - r#" - ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ - - The branches of this `when` expression don't match the condition: - - 6│> when bool is - 7│ True -> "true" - 8│ False -> "false" - 9│ Wat -> "surprise!" - - This `bool` value is a: - - Bool - - But the branch patterns have type: - - [False, True, Wat] - - The branches must be cases of the `when` condition's type! - "# - ), - ) - } - - #[test] - fn always_function() { - // from https://github.com/rtfeldman/roc/commit/1372737f5e53ee5bb96d7e1b9593985e5537023a - // There was a bug where this reported UnusedArgument("val") - // since it was used only in the returned function only. - // - // we want this to not give any warnings/errors! - report_problem_as( - indoc!( - r#" - always = \val -> \_ -> val - - always - "# - ), - "", - ) - } - - #[test] - fn imports_missing_comma() { - new_report_problem_as( - "imports_missing_comma", - indoc!( - r#" - app "test-missing-comma" - packages { pf: "platform" } - imports [pf.Task Base64] - provides [main, @Foo] to pf - "# - ), - indoc!( - r#" - ── WEIRD IMPORTS ────────────────────────── tmp/imports_missing_comma/Test.roc ─ - - I am partway through parsing a imports list, but I got stuck here: - - 2│ packages { pf: "platform" } - 3│ imports [pf.Task Base64] - ^ - - I am expecting a comma or end of list, like - - imports [Shape, Vector]"# - ), - ) - } - - #[test] - fn not_enough_cases_for_open_union() { - new_report_problem_as( - "branches_have_more_cases_than_condition", - indoc!( - r#" - foo : [A, B]a -> Str - foo = \it -> - when it is - A -> "" - foo - "# - ), - indoc!( - r#" - ── UNSAFE PATTERN ──────────────────────────────────────── /code/proj/Main.roc ─ - - This `when` does not cover all the possibilities: - - 6│> when it is - 7│> A -> "" - - Other possibilities include: - - B - _ - - I would have to crash if I saw one of those! Add branches for them! - "# - ), - ) - } - - #[test] - fn issue_2778_specialization_is_not_a_redundant_pattern() { - new_report_problem_as( - "issue_2778_specialization_is_not_a_redundant_pattern", - indoc!( - r#" - formatColor = \color -> - when color is - Red -> "red" - Yellow -> "yellow" - _ -> "unknown" - - Red |> formatColor |> Str.concat (formatColor Orange) - "# - ), - "", // no problem - ) - } - - #[test] - fn nested_specialization() { - new_report_problem_as( - "nested_specialization", - indoc!( - r#" - app "test" provides [main] to "./platform" - - Default has default : {} -> a | a has Default - - main = - A := {} - default = \{} -> @A {} - default {} - "# - ), - indoc!( - r#" - ── SPECIALIZATION NOT ON TOP-LEVEL ─────────────────────── /code/proj/Main.roc ─ - - This specialization of the `default` ability member is in a nested - scope: - - 7│ default = \{} -> @A {} - ^^^^^^^ - - Specializations can only be defined on the top-level of a module. - "# - ), - ) - } - - #[test] - fn recursion_var_specialization_error() { - new_report_problem_as( - "recursion_var_specialization_error", - indoc!( - r#" - Job a : [Job (List (Job a))] - - job : Job Str - - when job is - Job lst -> lst == "" - "# - ), - indoc!( - r#" - ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ - - The 2nd argument to `isEq` is not what I expect: - - 9│ Job lst -> lst == "" - ^^ - - This argument is a string of type: - - Str - - But `isEq` needs the 2nd argument to be: - - List [Job ∞] as ∞ - "# - ), - ) - } - - #[test] - fn type_error_in_apply_is_circular() { - new_report_problem_as( - "type_error_in_apply_is_circular", - indoc!( - r#" - app "test" provides [go] to "./platform" - - S a : { set : Set a } - - go : a, S a -> Result (List a) * - go = \goal, model -> - if goal == goal - then Ok [] - else - new = { model & set : Set.remove goal model.set } - go goal new - "# - ), - indoc!( - r#" - ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ - - The 1st argument to `remove` is not what I expect: - - 10│ new = { model & set : Set.remove goal model.set } - ^^^^ - - This `goal` value is a: - - a - - But `remove` needs the 1st argument to be: - - Set a - - Tip: The type annotation uses the type variable `a` to say that this - definition can produce any type of value. But in the body I see that - it will only produce a `Set` value of a single specific type. Maybe - change the type annotation to be more specific? Maybe change the code - to be more general? - - ── CIRCULAR TYPE ───────────────────────────────────────── /code/proj/Main.roc ─ - - I'm inferring a weird self-referential type for `new`: - - 10│ new = { model & set : Set.remove goal model.set } - ^^^ - - Here is my best effort at writing down the type. You will see ∞ for - parts of the type that repeat something already printed out - infinitely. - - { set : Set ∞ } - - ── CIRCULAR TYPE ───────────────────────────────────────── /code/proj/Main.roc ─ - - I'm inferring a weird self-referential type for `goal`: - - 6│ go = \goal, model -> - ^^^^ - - Here is my best effort at writing down the type. You will see ∞ for - parts of the type that repeat something already printed out - infinitely. - - Set ∞ - "# - ), - ) - } - - #[test] - fn cycle_through_non_function() { - new_report_problem_as( - "cycle_through_non_function", - indoc!( - r#" - force : ({} -> I64) -> I64 - force = \eval -> eval {} - - t1 = \_ -> force (\_ -> t2) - - t2 = t1 {} - - t2 - "# - ), - indoc!( - r#" - ── CIRCULAR DEFINITION ─────────────────────────────────── /code/proj/Main.roc ─ - - The `t1` definition is causing a very tricky infinite loop: - - 7│ t1 = \_ -> force (\_ -> t2) - ^^ - - The `t1` value depends on itself through the following chain of - definitions: - - ┌─────┐ - │ t1 - │ ↓ - │ t2 - └─────┘ - "# - ), - ) - } - - #[test] - fn function_does_not_implement_encoding() { - new_report_problem_as( - "function_does_not_implement_encoding", - indoc!( - r#" - app "test" imports [Encode] provides [main] to "./platform" - - main = Encode.toEncoder \x -> x - "# - ), - indoc!( - r#" - ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ - - This expression has a type that does not implement the abilities it's expected to: - - 3│ main = Encode.toEncoder \x -> x - ^^^^^^^ - - Roc can't generate an implementation of the `Encode.Encoding` ability - for - - a -> a - - Note: `Encoding` cannot be generated for functions. - "# - ), - ) - } - - #[test] - fn unbound_type_in_record_does_not_implement_encoding() { - new_report_problem_as( - "unbound_type_in_record_does_not_implement_encoding", - indoc!( - r#" - app "test" imports [Encode] provides [main] to "./platform" - - main = \x -> Encode.toEncoder { x: x } - "# - ), - indoc!( - r#" - ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ - - This expression has a type that does not implement the abilities it's expected to: - - 3│ main = \x -> Encode.toEncoder { x: x } - ^^^^^^^^ - - Roc can't generate an implementation of the `Encode.Encoding` ability - for - - { x : a } - - In particular, an implementation for - - a - - cannot be generated. - - Tip: This type variable is not bound to `Encoding`. Consider adding a - `has` clause to bind the type variable, like `| a has Encode.Encoding` - "# - ), - ) - } - - #[test] - fn nested_opaque_does_not_implement_encoding() { - new_report_problem_as( - "nested_opaque_does_not_implement_encoding", - indoc!( - r#" - app "test" imports [Encode] provides [main] to "./platform" - - A := {} - main = Encode.toEncoder { x: @A {} } - "# - ), - indoc!( - r#" - ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ - - This expression has a type that does not implement the abilities it's expected to: - - 4│ main = Encode.toEncoder { x: @A {} } - ^^^^^^^^^^^^ - - Roc can't generate an implementation of the `Encode.Encoding` ability - for - - { x : A } - - In particular, an implementation for - - A - - cannot be generated. - - Tip: `A` does not implement `Encoding`. Consider adding a custom - implementation or `has Encode.Encoding` to the definition of `A`. - "# - ), - ) - } - - #[test] - fn derive_non_builtin_ability() { - new_report_problem_as( - "derive_non_builtin_ability", - indoc!( - r#" - app "test" provides [A] to "./platform" - - Ab has ab : a -> a | a has Ab - - A := {} has [Ab] - "# - ), - indoc!( - r#" - ── ILLEGAL DERIVE ──────────────────────────────────────── /code/proj/Main.roc ─ - - This ability cannot be derived: - - 5│ A := {} has [Ab] - ^^ - - Only builtin abilities can be derived. - - Note: The builtin abilities are `Encode.Encoding` - "# - ), - ) - } - - #[test] - fn has_encoding_for_function() { - new_report_problem_as( - "has_encoding_for_function", - indoc!( - r#" - app "test" imports [Encode] provides [A] to "./platform" - - A a := a -> a has [Encode.Encoding] - "# - ), - indoc!( - r#" - ── INCOMPLETE ABILITY IMPLEMENTATION ───────────────────── /code/proj/Main.roc ─ - - Roc can't derive an implementation of the `Encode.Encoding` for `A`: - - 3│ A a := a -> a has [Encode.Encoding] - ^^^^^^^^^^^^^^^ - - Note: `Encoding` cannot be generated for functions. - - Tip: You can define a custom implementation of `Encode.Encoding` for `A`. - "# - ), - ) - } - - #[test] - fn has_encoding_for_non_encoding_alias() { - new_report_problem_as( - "has_encoding_for_non_encoding_alias", - indoc!( - r#" - app "test" imports [Encode] provides [A] to "./platform" - - A := B has [Encode.Encoding] - - B := {} - "# - ), - indoc!( - r#" - ── INCOMPLETE ABILITY IMPLEMENTATION ───────────────────── /code/proj/Main.roc ─ - - Roc can't derive an implementation of the `Encode.Encoding` for `A`: - - 3│ A := B has [Encode.Encoding] - ^^^^^^^^^^^^^^^ - - Tip: `B` does not implement `Encoding`. Consider adding a custom - implementation or `has Encode.Encoding` to the definition of `B`. - - Tip: You can define a custom implementation of `Encode.Encoding` for `A`. - "# - ), - ) - } - - #[test] - fn has_encoding_for_other_has_encoding() { - new_report_problem_as( - "has_encoding_for_other_has_encoding", - indoc!( - r#" - app "test" imports [Encode] provides [A] to "./platform" - - A := B has [Encode.Encoding] - - B := {} has [Encode.Encoding] - "# - ), - indoc!(""), // no error - ) - } - - #[test] - fn has_encoding_for_recursive_deriving() { - new_report_problem_as( - "has_encoding_for_recursive_deriving", - indoc!( - r#" - app "test" imports [Encode] provides [MyNat] to "./platform" - - MyNat := [S MyNat, Z] has [Encode.Encoding] - "# - ), - indoc!(""), // no error - ) - } - - #[test] - fn has_encoding_dominated_by_custom() { - new_report_problem_as( - "has_encoding_dominated_by_custom", - indoc!( - r#" - app "test" imports [Encode.{ Encoding, toEncoder, custom }] provides [A] to "./platform" - - A := {} has [Encode.Encoding] - - toEncoder = \@A {} -> custom \l, _ -> l - "# - ), - indoc!( - r#" - ── CONFLICTING DERIVE AND IMPLEMENTATION ───────────────── /code/proj/Main.roc ─ - - `A` both derives and custom-implements `Encode.Encoding`. We found the - derive here: - - 3│ A := {} has [Encode.Encoding] - ^^^^^^^^^^^^^^^ - - and one custom implementation of `Encode.Encoding` here: - - 5│ toEncoder = \@A {} -> custom \l, _ -> l - ^^^^^^^^^ - - Derived and custom implementations can conflict, so one of them needs - to be removed! - - Note: We'll try to compile your program using the custom - implementation first, and fall-back on the derived implementation if - needed. Make sure to disambiguate which one you want! - "# - ), - ) - } - - #[test] - fn issue_1755() { - new_report_problem_as( - "issue_1755", - indoc!( - r#" - Handle := {} - - await : Result a err, (a -> Result b err) -> Result b err - open : {} -> Result Handle * - close : Handle -> Result {} * - - withOpen : (Handle -> Result {} *) -> Result {} * - withOpen = \callback -> - handle <- await (open {}) - {} <- await (callback handle) - close handle - - withOpen - "# - ), - indoc!( - r#" - ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ - - Something is off with the body of the `withOpen` definition: - - 10│ withOpen : (Handle -> Result {} *) -> Result {} * - 11│ withOpen = \callback -> - 12│> handle <- await (open {}) - 13│> {} <- await (callback handle) - 14│> close handle - - The type annotation on `withOpen` says this `await` call should have the - type: - - Result {} * - - However, the type of this `await` call is connected to another type in a - way that isn't reflected in this annotation. - - Tip: Any connection between types must use a named type variable, not - a `*`! Maybe the annotation on `withOpen` should have a named type - variable in place of the `*`? - "# - ), - ) - } -} diff --git a/roc-for-elm-programmers.md b/roc-for-elm-programmers.md index 788c9d4c28..95b8f30963 100644 --- a/roc-for-elm-programmers.md +++ b/roc-for-elm-programmers.md @@ -14,6 +14,15 @@ Doc comments begin with `##` instead. Like Python, Roc does not have multiline comment syntax. +## Casing + +Like Elm, Roc uses camelCase for identifiers. Modules and types begin with a capital letter (eg. +Decode), variables (including type variables!) begin with a lower case letter (eg. request). +Acronyms also use camelCase despite being capitalized in English, eg. `xmlHttpRequest` for a +variable and `XmlHttpRequest` for a type. Each word starts with a capital letter, so if acronyms are +only capitals it's harder to see where the words start. eg. `XMLHTTPRequest` is less clear than +`XmlHttpRequest`, unless you already know the acronyms. + ## String Interpolation Roc strings work like Elm strings except that they support string interpolation. @@ -67,7 +76,7 @@ APIs, you don't need to bother writing `getUsername = Debug.todo "implement"`. Imagine if Elm's `let`...`in` worked exactly the same way, except you removed the `let` and `in` keywords. That's how it works in Roc. -For example, this Elm code computes `someNumber` to be `1234`: +For example, consider this Elm code: ```elm numbers = @@ -125,6 +134,7 @@ to destructure variants inline in function declarations, like in these two examp ```elm \(UserId id1) (UserId id2) -> ``` + ```elm \(UserId id) -> ``` @@ -137,6 +147,7 @@ You can write the above like so in Roc: ```elm \UserId id1, UserId id2 -> ``` + ```elm \UserId id -> ``` @@ -214,13 +225,13 @@ Closed record annotations look the same as they do in Elm, e.g. In Elm: -``` +```elm { a | name : Str, email : Str } -> Str ``` In Roc: -``` +```coffee { name : Str, email : Str }* -> Str ``` @@ -295,20 +306,20 @@ table { defaultConfig | height = 800, width = 600 } This way, as the caller I'm specifying only the `height` and `width` fields, and leaving the others to whatever is inside `defaultConfig`. Perhaps it also -has the fields `x` and `y`. +has the fields `title` and `description`. In Roc, you can do this like so: -```elm +```javascript table { height: 800, width: 600 } ``` -...and the `table` function will fill in its default values for `x` and `y`. +...and the `table` function will fill in its default values for `title` and `description`. There is no need to use a `defaultConfig` record. -Here's how `table` would be defined in Roc: +Here's how that `table` function would be implemented in Roc: -``` +```elixir table = \{ height, width, title ? "", description ? "" } -> ``` @@ -316,7 +327,7 @@ This is using *optional field destructuring* to destructure a record while also providing default values for any fields that might be missing. Here's the type of `table`: -``` +```elixir table : { height : Pixels, @@ -356,8 +367,8 @@ ergonomics of destructuring mean this wouldn't be a good fit for data modeling. Roc's pattern matching conditionals work about the same as how they do in Elm. Here are two differences: -* Roc uses the syntax `when`...`is` instead of `case`...`of` -* In Roc, you can use `|` to handle multiple patterns in the same way +- Roc uses the syntax `when`...`is` instead of `case`...`of` +- In Roc, you can use `|` to handle multiple patterns in the same way For example: @@ -391,6 +402,12 @@ when number is _ -> "" ``` +## Booleans + +Roc has a `Bool` module (with operations like `Bool.and` and `Bool.or`; Roc does not have +a `Basics` module), and `Bool` is an opaque type. The values `Bool.true` and `Bool.false` work +like `True` and `False` do in Elm. + ## Custom Types This is the biggest semantic difference between Roc and Elm. @@ -398,9 +415,9 @@ This is the biggest semantic difference between Roc and Elm. Let's start with the motivation. Suppose I'm using a platform for making a web server, and I want to: -* Read some data from a file -* Send an HTTP request containing some of the data from the file -* Write some data to a file containing some of the data from the HTTP response +- Read some data from a file +- Send an HTTP request containing some of the data from the file +- Write some data to a file containing some of the data from the HTTP response Assuming I'm writing this on a Roc platform which has a `Task`-based API, and that `Task.await` is like Elm's `Task.andThen` but with the arguments @@ -524,12 +541,12 @@ the type of the union it goes in. Here are some examples of using tags in a REPL: -``` -> True -True : [True]* +```coffee +> Red +Red : [Red]* -> False -False : [False]* +> Blue +Blue : [Blue]* > Ok "hi" Ok "hi" : [Ok Str]* @@ -541,7 +558,7 @@ SomethingIJustMadeUp "hi" "there" : [SomethingIJustMadeUp Str Str]* Foo : [Foo]* > y = Foo "hi" Bar -Foo "hi" 5 : [Foo Str [Bar]*]* +Foo "hi" Bar : [Foo Str [Bar]*]* > z = Foo ["str1", "str2"] Foo ["str1", "str2"] : [Foo (List Str)]* @@ -599,8 +616,8 @@ tagToStr = \tag -> Each of these type annotations involves a *tag union* - a collection of tags bracketed by `[` and `]`. -* The type `[Foo, Bar Str]` is a **closed** tag union. -* The type `[Foo]*` is an **open** tag union. +- The type `[Foo, Bar Str]` is a **closed** tag union. +- The type `[Foo]*` is an **open** tag union. You can pass `x` to `tagToStr` because an open tag union is type-compatible with any closed tag union which contains its tags (in this case, the `Foo` tag). You can also @@ -641,7 +658,7 @@ by removing the `*`. For example, if you changed the annotation to this... alwaysFoo : [Foo Str, Bar Bool] -> [Foo Str]* ``` -...then the function would only accept tags like `Foo "hi"` and `Bar False`. By writing +...then the function would only accept tags like `Foo "hi"` and `Bar (x == y)`. By writing out your own annotations, you can get the same level of restriction you get with traditional algebraic data types (which, after all, come with the requirement that you write out their annotations). Using annotations, you can restrict even @@ -688,18 +705,8 @@ includes in its union." ## Opaque Types In Elm, you can choose to expose (or not) custom types' constructors in order to create [opaque types](http://sporto.github.io/elm-patterns/advanced/opaque-types.html). -Since Roc's _tags_ can be constructed in any module without importing anything, Roc has a separate -_opaque type_ language feature to enable information hiding. - -Opaque types in Roc have some similarities to type aliases, but also some important differences. - -* Opaque type are defined with `:=` (e.g. `Username := Str` instead of `Username : Str`) -* You can get an _opaque wrapper_ by writing an `@` symbol before the name of an opaque type. For example, `@Username` would be an opaque wrapper for the opaque type `Username`. -* Applying an _opaque wrapper_ to another value creates an _opaque value_, whose type is the one referred to by the opaque wrapper. So the expression `@Username "Sasha"` has the type `Username`. -* Applying and destructuring opaque wrappers works like tags; you can write `@Username str = user` to unwrap an opaque wrapper's payload, just like you would with a tag payload. -* Opaque types can only be wrapped and unwrapped in the same module where the opaque type itself is defined. -* You can export opaque type names (e.g. `Username`) to other modules, allowing them to be used in type annotations, but there is no way to export the opaque wrappers themselves. This means that an opaque type can only be wrapped and unwrapped (using `@` syntax) in the same module where it was defined. -* Opaque types are only equal if their names are the same _and_ they were defined in the same module. +Since Roc's *tags* can be constructed in any module without importing anything, Roc has a separate +*opaque type* language feature to enable information hiding. As an example, suppose I define these inside the `Username` module: @@ -715,10 +722,14 @@ toStr = \@Username str -> str ``` -I can now expose the `Username` opaque type, which other modules can use in type annotations. -However, it's not even syntactically possible for me to expose the `@Username` opaque wrapper, -because `@` is not allowed in the `exposing` list. Only code written in this `Username` module -can use the `@Username` wrapper. +Here, `Username` is an opaque type. The `fromStr` function turns a string into a `Username` +by "calling" `@Username` on that string. The `toStr` function turns a `Username` back into +a string by pattern matching `@Username str ->` to unwrap the string from the `Username`. + +Now that I've defined the `Username` opaque type, I can expose it so that other modules can use +it in type annotations. However, other modules can't use the `@Username` syntax to wrap or unwrap +`Username` values. That operation is only available in the same scope where `Username` itself was +defined; trying to use it outside that scope will give an error. > If I were to define `Username := Str` inside another module (e.g. `Main`) and use `@Username`, > it would compile, but that `Username` opaque type would not be considered equal to the one defined in @@ -1017,6 +1028,7 @@ list = num + 1 ``` + Both snippets are calling `List.map` passing `numbers` as the first argument, and a `\num -> num + 1` function for the other argument. @@ -1129,8 +1141,8 @@ Like Elm, Roc organizes numbers into integers and floating-point numbers. However, Roc breaks them down even further. For example, Roc has two different sizes of float types to choose from: -* `F64` - a 64-bit [IEEE 754 binary floating point number](https://en.wikipedia.org/wiki/IEEE_754#Binary) -* `F32` - a 32-bit [IEEE 754 binary floating point number](https://en.wikipedia.org/wiki/IEEE_754#Binary) +- `F64` - a 64-bit [IEEE 754 binary floating point number](https://en.wikipedia.org/wiki/IEEE_754#Binary) +- `F32` - a 32-bit [IEEE 754 binary floating point number](https://en.wikipedia.org/wiki/IEEE_754#Binary) Both types are desirable in different situations. For example, when doing simulations, the precision of the `F64` type is desirable. On the other hand, @@ -1138,25 +1150,20 @@ GPUs tend to heavily prefer 32-bit floats because a serious bottleneck is how long it takes data to transfer from CPU to GPU, so having to send half as many bytes per render (compared to 64-bit floats) can be huge for performance. -Roc also supports `D64` and `D32`, which are [IEEE 754 decimal floating point numbers](https://en.wikipedia.org/wiki/IEEE_754#Decimal). The upside of these is that they are decimal-based, so -`0.1 + 0.2 == 0.3` (whereas in binary floats [this is not true](https://0.30000000000000004.com/)), -which makes them much better for calculations involving currency, among others. -The downside of decimal floats is that they do not have hardware support -(except on certain [highly uncommon processors](https://en.wikipedia.org/wiki/IEEE_754#See_also)), -so calculations involving them take longer. - -Roc does not let floating point calculations result in `Infinity`, `-Infinity`, -or `NaN`. Any operation which would result in one of these -(such as `sqrt` or `/`) will panic. +Roc also supports `Dec`, which is a 128-bit [fixed-point decimal](https://en.wikipedia.org/wiki/Fixed-point_arithmetic) number. `Dec` is decimal-based, so `0.1 + 0.2 == 0.3` +(whereas in binary floats [this is not true](https://0.30000000000000004.com/)), +which makes it much better for calculations involving currency, among other use cases. +The downside of `Dec` is that it does not have hardware support, so calculations involving +them take longer than they do with floats. Similarly to how there are different sizes of floating point numbers, there are also different sizes of integer to choose from: -* `I8` -* `I16` -* `I32` -* `I64` -* `I128` +- `I8` +- `I16` +- `I32` +- `I64` +- `I128` Roc also has *unsigned* integers which are never negative. They are `U8`, `U16`, `U32`, `U64`, `U128`, and `Nat`. @@ -1167,9 +1174,9 @@ target (for example, WebAssembly) at runtime it will be the same as `U32` instea `Nat` comes up most often with collection lengths and indexing into collections. For example: -* `List.len : List * -> Nat` -* `List.get : List elem, Nat -> Result elem [OutOfBounds]*` -* `List.set : List elem, Nat, elem -> List elem` +- `List.len : List * -> Nat` +- `List.get : List elem, Nat -> Result elem [OutOfBounds]*` +- `List.set : List elem, Nat, elem -> List elem` As with floats, which integer type to use depends on the values you want to support as well as your performance needs. For example, raw sequences of bytes are typically @@ -1188,45 +1195,36 @@ This accepts any of the numeric types discussed above, from `I128` to `F32` to `D64` and everything in between. This is because those are all type aliases for `Num` types. For example: -* `I64` is a type alias for `Num (Integer Signed64)` -* `U8` is a type alias for `Num (Integer Unsigned8)` -* `F32` is a type alias for `Num (FloatingPoint Binary32)` -* `D64` is a type alias for `Num (FloatingPoint Decimal64)` +- `I64` is a type alias for `Num (Integer Signed64)` +- `U8` is a type alias for `Num (Integer Unsigned8)` +- `F32` is a type alias for `Num (Fraction Binary32)` +- `Dec` is a type alias for `Num (Fraction Decimal)` -(Those types like `Integer`, `FloatingPoint`, and `Signed64` are all defined like `Never`; you can never instantiate one. -They are used only as phantom types.) +(Those types like `Integer`, `Fraction`, and `Signed64` are all defined like `Never`; +you can never instantiate one. They are used only as phantom types.) So Roc does not use `number`, but rather uses `Num` - which works more like `List`. Either way, you get `+` being able to work on both integers and floats! Separately, there's also `Int a`, which is a type alias for `Num (Integer a)`, -and `Float a`, which is a type alias for `Num (FloatingPoint a)`. These allow functions -that can work on any integer or any float. For example, +and `Frac a`, which is a type alias for `Num (Fraction a)`. These allow functions +that can work on any integer or any fractional number. For example, `Num.bitwiseAnd : Int a, Int a -> Int a`. -In Roc, number literals with decimal points are `Float *` values. +In Roc, number literals with decimal points are `Frac *` values. Number literals *without* a decimal point are `Num *` values. Almost always these will end up becoming something more specific, but in the unlikely event (most often in a REPL) that you actually do end up with an operation that runs on either an `Int *` or a `Num *` value, it will default to being treated as -an `I64`. Similarly, a `Float *` value will default to being treated as a `D64`, +an `I64`. Similarly, a `Frac *` value will default to being treated as a `D64`, which means if someone is learning Roc as their first programming language and they type `0.1 + 0.2` into a REPL, they won't be confused by the answer. -If you encounter overflow with either integers or floats in Roc, you get a runtime -exception rather than wrapping overflow behavior (or a float becoming `Infinity` -or `-Infinity`). You can opt into wrapping overflow instead with functions like +If you encounter integer or `Dec` overflow in Roc, by default you get a runtime +exception. You can opt into wrapping integer overflow instead with functions like `Num.addWrap : Int a, Int a -> Int a`, or use a function that gives `Err` if it overflows, like `Num.addChecked : Num a, Num a -> Result (Num a) [Overflow]*`. -## `comparable`, `appendable`, and `number` - -These don't exist in Roc. - -* `appendable` is only used in Elm for the `(++)` operator, and Roc doesn't have that operator. -* `comparable` is used for comparison operators (like `<` and such), plus `List.sort`, `Dict`, and `Set`. Roc's `List.sort` accepts a "sorting function" argument which specifies how to sort the elements. Roc's comparison operators (like `<`) only accept numbers; `"foo" < "bar"` is valid Elm, but will not compile in Roc. Roc's dictionaries and sets are hashmaps behind the scenes (rather than ordered trees), and their keys only need the functionless restriction. -* `number` is replaced by `Num`, as described previously. - Also [like Python](https://www.python.org/dev/peps/pep-0515/) Roc permits underscores in number literals for readability purposes. Roc also supports hexadecimal (`0x01`), octal (`0o01`), and binary (`0b01`) integer literals; these @@ -1239,39 +1237,58 @@ If you put these into a hypothetical Roc REPL, here's what you'd see: 2048 : Num * > 1 + 2.14 -3.14 : Float * +3.14 : Frac * > 1.0 + 1 -2.0 : Float * +2.0 : Frac * > 1.1 + 0x11 - + > 11 + 0x11 28 : Int * ``` +## Testing + +Instead of a separate testing tool, Roc has a built-in `expect` keyword, which +you can use in conjunction with `roc test` to run tests. + +See [the tutorial section on testing](https://www.roc-lang.org/tutorial#tests-and-expectations) +for details. + +## Abilities + +`comparable`, `appendable`, and `number` don't exist in Roc. + +- `number` is replaced by `Num`, as described previously. +- `appendable` is only used in Elm for the `(++)` operator, and Roc doesn't have that operator. +- `comparable` is used in Elm for comparison operators (like `<` and such), plus `List.sort`, `Dict`, and `Set`. Roc's comparison operators (like `<`) only accept numbers; `"foo" < "bar"` is valid Elm, but will not compile in Roc. Roc's dictionaries and sets are hashmaps behind the scenes (rather than ordered trees), so their keys need to be hashable but not necessarily comparable. + +That said, Roc's `Dict` and `Set` do have a restriction on their keys, just not `comparable`. +See the section on Abilities in [the tutorial](https://roc-lang.org/tutorial) for details. + ## Standard library `elm/core` has these modules: -* `Array` -* `Basics` -* `Bitwise` -* `Char` -* `Debug` -* `Dict` -* `List` -* `Maybe` -* `Platform` -* `Platform.Cmd` -* `Platform.Sub` -* `Process` -* `Result` -* `Set` -* `String` -* `Task` -* `Tuple` +- `Array` +- `Basics` +- `Bitwise` +- `Char` +- `Debug` +- `Dict` +- `List` +- `Maybe` +- `Platform` +- `Platform.Cmd` +- `Platform.Sub` +- `Process` +- `Result` +- `Set` +- `String` +- `Task` +- `Tuple` In Roc, the standard library is not a standalone package. It is baked into the compiler, and you can't upgrade it independently of a compiler release; whatever version of @@ -1281,48 +1298,49 @@ possible to ship Roc's standard library as a separate package!) Roc's standard library has these modules: -* `Str` -* `Bool` -* `Num` -* `List` -* `Dict` -* `Set` -* `Result` +- `Str` +- `Bool` +- `Num` +- `List` +- `Dict` +- `Set` +- `Result` Some differences to note: -* All these standard modules are imported by default into every module. They also expose all their types (e.g. `Bool`, `List`, `Result`) but they do not expose any values - not even `negate` or `not`. (`True`, `False`, `Ok`, and `Err` are all tags, so they do not need to be exposed; they are globally available regardless!) -* In Roc it's called `Str` instead of `String`. -* `List` refers to something more like Elm's `Array`, as noted earlier. -* No `Char`. This is by design. What most people think of as a "character" is a rendered glyph. However, rendered glyphs are comprised of [grapheme clusters](https://stackoverflow.com/a/27331885), which are a variable number of Unicode code points - and there's no upper bound on how many code points there can be in a single cluster. In a world of emoji, I think this makes `Char` error-prone and it's better to have `Str` be the only first-class unit. For convenience when working with unicode code points (e.g. for performance-critical tasks like parsing), the single-quote syntax is sugar for the corresponding `U32` code point - for example, writing `'鹏'` is exactly the same as writing `40527`. Like Rust, you get a compiler error if you put something in single quotes that's not a valid [Unicode scalar value](http://www.unicode.org/glossary/#unicode_scalar_value). -* No `Basics`. You use everything from the standard library fully-qualified; e.g. `Bool.not` or `Num.negate` or `Num.ceiling`. There is no `Never` because `[]` already serves that purpose. (Roc's standard library doesn't include an equivalent of `Basics.never`, but it's one line of code and anyone can implement it: `never = \a -> never a`.) -* No `Tuple`. Roc doesn't have tuple syntax. As a convention, `Pair` can be used to represent tuples (e.g. `List.zip : List a, List b -> List [Pair a b]*`), but this comes up infrequently compared to languages that have dedicated syntax for it. -* No `Task`. By design, platform authors implement `Task` (or don't; it's up to them) - it's not something that really *could* be usefully present in Roc's standard library. -* No `Process`, `Platform`, `Cmd`, or `Sub` - similarly to `Task`, these are things platform authors would include, or not. -* No `Maybe`. This is by design. If a function returns a potential error, use `Result` with an error type that uses a zero-arg tag to describe what went wrong. (For example, `List.first : List a -> Result a [ListWasEmpty]*` instead of `List.first : List a -> Maybe a`.) If you want to have a record field be optional, use an Optional Record Field directly (see earlier). If you want to describe something that's neither an operation that can fail nor an optional field, use a more descriptive tag - e.g. for a nullable JSON decoder, instead of `nullable : Decoder a -> Decoder (Maybe a)`, make a self-documenting API like `nullable : Decoder a -> Decoder [Null, NonNull a]*`. +- All these standard modules are imported by default into every module. They also expose all their types (e.g. `Bool`, `List`, `Result`) but they do not expose any values - not even `negate` or `not`. (`Ok` and `Err` are ordinary tags, so they do not need to be exposed; they are globally available regardless!) +- In Roc it's called `Str` instead of `String`. +- `List` refers to something more like Elm's `Array`, as noted earlier. +- No `Char`. This is by design. What most people think of as a "character" is a rendered glyph. However, rendered glyphs are comprised of [grapheme clusters](https://stackoverflow.com/a/27331885), which are a variable number of Unicode code points - and there's no upper bound on how many code points there can be in a single cluster. In a world of emoji, I think this makes `Char` error-prone and it's better to have `Str` be the only first-class unit. For convenience when working with unicode code points (e.g. for performance-critical tasks like parsing), the single-quote syntax is sugar for the corresponding `U32` code point - for example, writing `'鹏'` is exactly the same as writing `40527`. Like Rust, you get a compiler error if you put something in single quotes that's not a valid [Unicode scalar value](http://www.unicode.org/glossary/#unicode_scalar_value). +- No `Basics`. You use everything from the standard library fully-qualified; e.g. `Bool.not` or `Num.negate` or `Num.ceiling`. There is no `Never` because `[]` already serves that purpose. (Roc's standard library doesn't include an equivalent of `Basics.never`, but it's one line of code and anyone can implement it: `never = \a -> never a`.) +- No `Tuple` module, but there is syntax support for tuples which allows not only destructuring (like in Elm) but also direct field access - which looks like record field access, but with numbered indices instead of named fields. For example, the Elm code `Tuple.first ( "a", "b" )` and `Tuple.second ( "a", "b" )` could be written in Roc as `("a", "b").0` and `("a", "b").1`. Roc tuples can also have more than two fields. +- No `Task`. By design, platform authors implement `Task` (or don't; it's up to them) - it's not something that really *could* be usefully present in Roc's standard library. +- No `Process`, `Platform`, `Cmd`, or `Sub` - similarly to `Task`, these are things platform authors would include, or not. +- No `Debug`. Roc has a [built-in `dbg` keyword](https://www.roc-lang.org/tutorial#debugging) instead of `Debug.log` and a [`crash` keyword](https://www.roc-lang.org/tutorial#crashing) instead of `Debug.todo`. +- No `Maybe`. This is by design. If a function returns a potential error, use `Result` with an error type that uses a zero-arg tag to describe what went wrong. (For example, `List.first : List a -> Result a [ListWasEmpty]*` instead of `List.first : List a -> Maybe a`.) If you want to have a record field be optional, use an Optional Record Field directly (see earlier). If you want to describe something that's neither an operation that can fail nor an optional field, use a more descriptive tag - e.g. for a nullable JSON decoder, instead of `nullable : Decoder a -> Decoder (Maybe a)`, make a self-documenting API like `nullable : Decoder a -> Decoder [Null, NonNull a]*`. ## Operator Desugaring Table Here are various Roc expressions involving operators, and what they desugar to. -| Expression | Desugars to | -| --------------- | ---------------- | +| Expression | Desugars to | +| ----------------- | ------------------ | | `a + b` | `Num.add a b` | | `a - b` | `Num.sub a b` | | `a * b` | `Num.mul a b` | -| `a / b` | `Num.div a b` | -| `a // b` | `Num.divTrunc a b` | +| `a / b` | `Num.div a b` | +| `a // b` | `Num.divTrunc a b` | | `a ^ b` | `Num.pow a b` | -| `a % b` | `Num.rem a b` | -| `a >> b` | `Num.shr a b` | -| `a << b` | `Num.shl a b` | +| `a % b` | `Num.rem a b` | +| `a >> b` | `Num.shr a b` | +| `a << b` | `Num.shl a b` | | `-a` | `Num.neg a` | | `-f x y` | `Num.neg (f x y)` | | `a == b` | `Bool.isEq a b` | | `a != b` | `Bool.isNotEq a b` | | `a && b` | `Bool.and a b` | -| `a \|\| b` | `Bool.or a b` | +| `a \|\| b` | `Bool.or a b` | | `!a` | `Bool.not a` | | `!f x y` | `Bool.not (f x y)` | -| `a \|> b` | `b a` | -| `a b c \|> f x y` | `f (a b c) x y` | +| `a \|> b` | `b a` | +| `a b c \|> f x y` | `f (a b c) x y` | diff --git a/roc_std/Cargo.lock b/roc_std/Cargo.lock deleted file mode 100644 index 5494d0afd5..0000000000 --- a/roc_std/Cargo.lock +++ /dev/null @@ -1,252 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 3 - -[[package]] -name = "aho-corasick" -version = "0.7.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" -dependencies = [ - "memchr", -] - -[[package]] -name = "ansi_term" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" -dependencies = [ - "winapi", -] - -[[package]] -name = "cfg-if" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" - -[[package]] -name = "ctor" -version = "0.1.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccc0a48a9b826acdf4028595adc9db92caea352f7af011a3034acd172a52a0aa" -dependencies = [ - "quote", - "syn", -] - -[[package]] -name = "diff" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e25ea47919b1560c4e3b7fe0aaab9becf5b84a10325ddf7db0f0ba5e1026499" - -[[package]] -name = "env_logger" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a19187fea3ac7e84da7dacf48de0c45d63c6a76f9490dae389aead16c243fce3" -dependencies = [ - "log", - "regex", -] - -[[package]] -name = "getrandom" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d39cd93900197114fa1fcb7ae84ca742095eed9442088988ae74fa744e930e77" -dependencies = [ - "cfg-if", - "libc", - "wasi", -] - -[[package]] -name = "indoc" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7906a9fababaeacb774f72410e497a1d18de916322e33797bb2cd29baa23c9e" -dependencies = [ - "unindent", -] - -[[package]] -name = "libc" -version = "0.2.119" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bf2e165bb3457c8e098ea76f3e3bc9db55f87aa90d52d0e6be741470916aaa4" - -[[package]] -name = "log" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "memchr" -version = "2.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" - -[[package]] -name = "output_vt100" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "628223faebab4e3e40667ee0b2336d34a5b960ff60ea743ddfdbcf7770bcfb66" -dependencies = [ - "winapi", -] - -[[package]] -name = "pretty_assertions" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76d5b548b725018ab5496482b45cb8bef21e9fed1858a6d674e3a8a0f0bb5d50" -dependencies = [ - "ansi_term", - "ctor", - "diff", - "output_vt100", -] - -[[package]] -name = "proc-macro2" -version = "1.0.36" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7342d5883fbccae1cc37a2353b09c87c9b0f3afd73f5fb9bba687a1f733b029" -dependencies = [ - "unicode-xid", -] - -[[package]] -name = "quickcheck" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "588f6378e4dd99458b60ec275b4477add41ce4fa9f64dcba6f15adccb19b50d6" -dependencies = [ - "env_logger", - "log", - "rand", -] - -[[package]] -name = "quickcheck_macros" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b22a693222d716a9587786f37ac3f6b4faedb5b80c23914e7303ff5a1d8016e9" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "quote" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "864d3e96a899863136fc6e99f3d7cae289dafe43bf2c5ac19b70df7210c0a145" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "rand" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "rand_core", -] - -[[package]] -name = "rand_core" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" -dependencies = [ - "getrandom", -] - -[[package]] -name = "regex" -version = "1.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.6.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" - -[[package]] -name = "roc_std" -version = "0.1.0" -dependencies = [ - "indoc", - "libc", - "pretty_assertions", - "quickcheck", - "quickcheck_macros", -] - -[[package]] -name = "syn" -version = "1.0.86" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a65b3f4ffa0092e9887669db0eae07941f023991ab58ea44da8fe8e2d511c6b" -dependencies = [ - "proc-macro2", - "quote", - "unicode-xid", -] - -[[package]] -name = "unicode-xid" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" - -[[package]] -name = "unindent" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "514672a55d7380da379785a4d70ca8386c8883ff7eaae877be4d2081cebe73d8" - -[[package]] -name = "wasi" -version = "0.10.2+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/roc_std/Cargo.toml b/roc_std/Cargo.toml deleted file mode 100644 index 5f18ae3060..0000000000 --- a/roc_std/Cargo.toml +++ /dev/null @@ -1,23 +0,0 @@ -[package] -authors = ["The Roc Contributors"] -description = "Rust representations of Roc data structures" -edition = "2021" -license = "UPL-1.0" -name = "roc_std" -readme = "README.md" -repository = "https://github.com/rtfeldman/roc" -version = "0.1.0" - -[dependencies] -static_assertions = "0.1" - -[dev-dependencies] -indoc = "1.0.3" -pretty_assertions = "1.0.0" -quickcheck = "1.0.3" -quickcheck_macros = "1.0.0" -libc = "0.2.106" - -[features] -default = ["platform"] -platform = [] diff --git a/roc_std/src/lib.rs b/roc_std/src/lib.rs deleted file mode 100644 index a38fd75c84..0000000000 --- a/roc_std/src/lib.rs +++ /dev/null @@ -1,511 +0,0 @@ -#![crate_type = "lib"] -// #![no_std] -use core::ffi::c_void; -use core::fmt; -use core::mem::{ManuallyDrop, MaybeUninit}; -use core::ops::Drop; -use core::str; -use std::hash::{Hash, Hasher}; -use std::io::Write; - -mod roc_list; -mod roc_str; -mod storage; - -pub use roc_list::RocList; -pub use roc_str::RocStr; -pub use storage::Storage; - -// A list of C functions that are being imported -#[cfg(feature = "platform")] -extern "C" { - pub fn roc_alloc(size: usize, alignment: u32) -> *mut c_void; - pub fn roc_realloc( - ptr: *mut c_void, - new_size: usize, - old_size: usize, - alignment: u32, - ) -> *mut c_void; - pub fn roc_dealloc(ptr: *mut c_void, alignment: u32); -} - -/// # Safety -/// This is only marked unsafe to typecheck without warnings in the rest of the code here. -#[cfg(not(feature = "platform"))] -pub unsafe extern "C" fn roc_alloc(_size: usize, _alignment: u32) -> *mut c_void { - unimplemented!("It is not valid to call roc alloc from within the compiler. Please use the \"platform\" feature if this is a platform.") -} -/// # Safety -/// This is only marked unsafe to typecheck without warnings in the rest of the code here. -#[cfg(not(feature = "platform"))] -pub unsafe extern "C" fn roc_realloc( - _ptr: *mut c_void, - _new_size: usize, - _old_size: usize, - _alignment: u32, -) -> *mut c_void { - unimplemented!("It is not valid to call roc realloc from within the compiler. Please use the \"platform\" feature if this is a platform.") -} -/// # Safety -/// This is only marked unsafe to typecheck without warnings in the rest of the code here. -#[cfg(not(feature = "platform"))] -pub unsafe extern "C" fn roc_dealloc(_ptr: *mut c_void, _alignment: u32) { - unimplemented!("It is not valid to call roc dealloc from within the compiler. Please use the \"platform\" feature if this is a platform.") -} - -#[repr(u8)] -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum RocOrder { - Eq = 0, - Gt = 1, - Lt = 2, -} - -/// Like a Rust `Result`, but following Roc's ABI instead of Rust's. -/// (Using Rust's `Result` instead of this will not work properly with Roc code!) -/// -/// This can be converted to/from a Rust `Result` using `.into()` -#[repr(C)] -pub struct RocResult { - payload: RocResultPayload, - tag: RocResultTag, -} - -impl core::fmt::Debug for RocResult -where - T: core::fmt::Debug, - E: core::fmt::Debug, -{ - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self.as_result_of_refs() { - Ok(payload) => write!(f, "RocOk({:?})", payload), - Err(payload) => write!(f, "RocErr({:?})", payload), - } - } -} - -impl PartialEq for RocResult -where - T: PartialEq, - E: PartialEq, -{ - fn eq(&self, other: &Self) -> bool { - self.as_result_of_refs() == other.as_result_of_refs() - } -} - -impl Clone for RocResult -where - T: Clone, - E: Clone, -{ - fn clone(&self) -> Self { - match self.as_result_of_refs() { - Ok(payload) => RocResult::ok(ManuallyDrop::into_inner(payload.clone())), - Err(payload) => RocResult::err(ManuallyDrop::into_inner(payload.clone())), - } - } -} - -impl RocResult { - pub fn ok(payload: T) -> Self { - Self { - tag: RocResultTag::RocOk, - payload: RocResultPayload { - ok: ManuallyDrop::new(payload), - }, - } - } - - pub fn err(payload: E) -> Self { - Self { - tag: RocResultTag::RocErr, - payload: RocResultPayload { - err: ManuallyDrop::new(payload), - }, - } - } - - pub fn is_ok(&self) -> bool { - matches!(self.tag, RocResultTag::RocOk) - } - - pub fn is_err(&self) -> bool { - matches!(self.tag, RocResultTag::RocErr) - } - - fn into_payload(mut self) -> RocResultPayload { - let mut value = MaybeUninit::uninit(); - let ref_mut_value = unsafe { &mut *value.as_mut_ptr() }; - - // move the value into our MaybeUninit memory - core::mem::swap(&mut self.payload, ref_mut_value); - - // don't run the destructor on self; the `payload` has been moved out - // and replaced by uninitialized memory - core::mem::forget(self); - - unsafe { value.assume_init() } - } - - fn as_result_of_refs(&self) -> Result<&ManuallyDrop, &ManuallyDrop> { - use RocResultTag::*; - - unsafe { - match self.tag { - RocOk => Ok(&self.payload.ok), - RocErr => Err(&self.payload.err), - } - } - } -} - -impl From> for Result { - fn from(roc_result: RocResult) -> Self { - use RocResultTag::*; - - let tag = roc_result.tag; - let payload = roc_result.into_payload(); - - unsafe { - match tag { - RocOk => Ok(ManuallyDrop::into_inner(payload.ok)), - RocErr => Err(ManuallyDrop::into_inner(payload.err)), - } - } - } -} - -impl From> for RocResult { - fn from(result: Result) -> Self { - match result { - Ok(payload) => RocResult::ok(payload), - Err(payload) => RocResult::err(payload), - } - } -} - -#[repr(u8)] -#[derive(Clone, Copy)] -enum RocResultTag { - RocErr = 0, - RocOk = 1, -} - -#[repr(C)] -union RocResultPayload { - ok: ManuallyDrop, - err: ManuallyDrop, -} - -impl Drop for RocResult { - fn drop(&mut self) { - use RocResultTag::*; - - match self.tag { - RocOk => unsafe { ManuallyDrop::drop(&mut self.payload.ok) }, - RocErr => unsafe { ManuallyDrop::drop(&mut self.payload.err) }, - } - } -} - -#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)] -#[repr(C)] -pub struct RocDec([u8; 16]); - -impl RocDec { - pub const MIN: Self = Self(i128::MIN.to_ne_bytes()); - pub const MAX: Self = Self(i128::MAX.to_ne_bytes()); - - const DECIMAL_PLACES: usize = 18; - const ONE_POINT_ZERO: i128 = 10i128.pow(Self::DECIMAL_PLACES as u32); - const MAX_DIGITS: usize = 39; - const MAX_STR_LENGTH: usize = Self::MAX_DIGITS + 2; // + 2 here to account for the sign & decimal dot - - pub fn new(num: i128) -> Self { - Self(num.to_ne_bytes()) - } - - pub fn as_bits(&self) -> (i64, u64) { - let lower_bits = self.as_i128() as u64; - let upper_bits = (self.as_i128() >> 64) as i64; - (upper_bits, lower_bits) - } - - #[allow(clippy::should_implement_trait)] - pub fn from_str(value: &str) -> Option { - // Split the string into the parts before and after the "." - let mut parts = value.split('.'); - - let before_point = match parts.next() { - Some(answer) => answer, - None => { - return None; - } - }; - - let opt_after_point = match parts.next() { - Some(answer) if answer.len() <= Self::DECIMAL_PLACES => Some(answer), - _ => None, - }; - - // There should have only been one "." in the string! - if parts.next().is_some() { - return None; - } - - // Calculate the low digits - the ones after the decimal point. - let lo = match opt_after_point { - Some(after_point) => { - match after_point.parse::() { - Ok(answer) => { - // Translate e.g. the 1 from 0.1 into 10000000000000000000 - // by "restoring" the elided trailing zeroes to the number! - let trailing_zeroes = Self::DECIMAL_PLACES - after_point.len(); - let lo = answer * 10i128.pow(trailing_zeroes as u32); - - if !before_point.starts_with('-') { - lo - } else { - -lo - } - } - Err(_) => { - return None; - } - } - } - None => 0, - }; - - // Calculate the high digits - the ones before the decimal point. - match before_point.parse::() { - Ok(answer) => match answer.checked_mul(Self::ONE_POINT_ZERO) { - Some(hi) => hi.checked_add(lo).map(|num| Self(num.to_ne_bytes())), - None => None, - }, - Err(_) => None, - } - } - - pub fn from_str_to_i128_unsafe(val: &str) -> i128 { - Self::from_str(val).unwrap().as_i128() - } - - /// This is private because RocDec being an i128 is an implementation detail - #[inline(always)] - fn as_i128(&self) -> i128 { - i128::from_ne_bytes(self.0) - } - - pub fn from_ne_bytes(bytes: [u8; 16]) -> Self { - Self(bytes) - } - - pub fn to_ne_bytes(&self) -> [u8; 16] { - self.0 - } - - fn to_str_helper(&self, bytes: &mut [u8; Self::MAX_STR_LENGTH]) -> usize { - if self.as_i128() == 0 { - write!(&mut bytes[..], "{}", "0").unwrap(); - return 1; - } - - let is_negative = (self.as_i128() < 0) as usize; - - static_assertions::const_assert!(Self::DECIMAL_PLACES + 1 == 19); - // The :019 in the following write! is computed as Self::DECIMAL_PLACES + 1. If you change - // Self::DECIMAL_PLACES, this assert should remind you to change that format string as - // well. - // - // By using the :019 format, we're guaranteeing that numbers less than 1, say 0.01234 - // get their leading zeros placed in bytes for us. i.e. bytes = b"0012340000000000000" - write!(&mut bytes[..], "{:019}", self.as_i128()).unwrap(); - - // If self represents 1234.5678, then bytes is b"1234567800000000000000". - let mut i = Self::MAX_STR_LENGTH - 1; - // Find the last place where we have actual data. - while bytes[i] == 0 { - i = i - 1; - } - // At this point i is 21 because bytes[21] is the final '0' in b"1234567800000000000000". - - let decimal_location = i - Self::DECIMAL_PLACES + 1 + is_negative; - // decimal_location = 4 - - while bytes[i] == ('0' as u8) && i >= decimal_location { - bytes[i] = 0; - i = i - 1; - } - // Now i = 7, because bytes[7] = '8', and bytes = b"12345678" - - if i < decimal_location { - // This means that we've removed trailing zeros and are left with an integer. Our - // convention is to print these without a decimal point or trailing zeros, so we're done. - return i + 1; - } - - let ret = i + 1; - while i >= decimal_location { - bytes[i + 1] = bytes[i]; - i = i - 1; - } - bytes[i + 1] = bytes[i]; - // Now i = 4, and bytes = b"123455678" - - bytes[decimal_location] = '.' as u8; - // Finally bytes = b"1234.5678" - - ret + 1 - } - - pub fn to_str(&self) -> RocStr { - let mut bytes = [0 as u8; Self::MAX_STR_LENGTH]; - let last_idx = self.to_str_helper(&mut bytes); - unsafe { RocStr::from_slice(&bytes[0..last_idx]) } - } -} - -impl fmt::Display for RocDec { - fn fmt(&self, fmtr: &mut fmt::Formatter<'_>) -> fmt::Result { - let mut bytes = [0 as u8; Self::MAX_STR_LENGTH]; - let last_idx = self.to_str_helper(&mut bytes); - let result = unsafe { str::from_utf8_unchecked(&bytes[0..last_idx]) }; - write!(fmtr, "{}", result) - } -} - -#[repr(C, align(16))] -#[derive(Clone, Copy, Eq, Default)] -pub struct I128([u8; 16]); - -impl From for I128 { - fn from(other: i128) -> Self { - Self(other.to_ne_bytes()) - } -} - -impl From for i128 { - fn from(other: I128) -> Self { - unsafe { core::mem::transmute::(other) } - } -} - -impl fmt::Debug for I128 { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let i128: i128 = (*self).into(); - - i128.fmt(f) - } -} - -impl fmt::Display for I128 { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let i128: i128 = (*self).into(); - - i128.fmt(f) - } -} - -impl PartialEq for I128 { - fn eq(&self, other: &Self) -> bool { - let i128_self: i128 = (*self).into(); - let i128_other: i128 = (*other).into(); - - i128_self.eq(&i128_other) - } -} - -impl PartialOrd for I128 { - fn partial_cmp(&self, other: &Self) -> Option { - let i128_self: i128 = (*self).into(); - let i128_other: i128 = (*other).into(); - - i128_self.partial_cmp(&i128_other) - } -} - -impl Ord for I128 { - fn cmp(&self, other: &Self) -> std::cmp::Ordering { - let i128_self: i128 = (*self).into(); - let i128_other: i128 = (*other).into(); - - i128_self.cmp(&i128_other) - } -} - -impl Hash for I128 { - fn hash(&self, state: &mut H) { - let i128: i128 = (*self).into(); - - i128.hash(state); - } -} - -#[repr(C, align(16))] -#[derive(Clone, Copy, Eq, Default)] -pub struct U128([u8; 16]); - -impl From for U128 { - fn from(other: u128) -> Self { - Self(other.to_ne_bytes()) - } -} - -impl From for u128 { - fn from(other: U128) -> Self { - unsafe { core::mem::transmute::(other) } - } -} - -impl fmt::Debug for U128 { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let u128: u128 = (*self).into(); - - u128.fmt(f) - } -} - -impl fmt::Display for U128 { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let u128: u128 = (*self).into(); - - u128.fmt(f) - } -} - -impl PartialEq for U128 { - fn eq(&self, other: &Self) -> bool { - let u128_self: u128 = (*self).into(); - let u128_other: u128 = (*other).into(); - - u128_self.eq(&u128_other) - } -} - -impl PartialOrd for U128 { - fn partial_cmp(&self, other: &Self) -> Option { - let u128_self: u128 = (*self).into(); - let u128_other: u128 = (*other).into(); - - u128_self.partial_cmp(&u128_other) - } -} - -impl Ord for U128 { - fn cmp(&self, other: &Self) -> std::cmp::Ordering { - let u128_self: u128 = (*self).into(); - let u128_other: u128 = (*other).into(); - - u128_self.cmp(&u128_other) - } -} - -impl Hash for U128 { - fn hash(&self, state: &mut H) { - let u128: u128 = (*self).into(); - - u128.hash(state); - } -} diff --git a/roc_std/src/roc_list.rs b/roc_std/src/roc_list.rs deleted file mode 100644 index 5abdfa69be..0000000000 --- a/roc_std/src/roc_list.rs +++ /dev/null @@ -1,360 +0,0 @@ -#![deny(unsafe_op_in_unsafe_fn)] - -use core::{ - cell::Cell, - cmp::{self, Ordering}, - fmt::Debug, - intrinsics::copy_nonoverlapping, - mem::{self, ManuallyDrop}, - ops::Deref, - ptr, - ptr::NonNull, -}; - -use crate::{roc_alloc, roc_dealloc, roc_realloc, storage::Storage}; - -#[repr(C)] -pub struct RocList { - elements: Option>>, - length: usize, - capacity: usize, -} - -impl RocList { - pub fn empty() -> Self { - RocList { - elements: None, - length: 0, - capacity: 0, - } - } - - pub fn len(&self) -> usize { - self.length - } - - pub fn capacity(&self) -> usize { - self.capacity - } - - pub fn is_empty(&self) -> bool { - self.len() == 0 - } - - pub fn as_slice(&self) -> &[T] { - &*self - } - - fn elements_and_storage(&self) -> Option<(NonNull>, &Cell)> { - let elements = self.elements?; - let storage = unsafe { &*elements.as_ptr().cast::>().sub(1) }; - Some((elements, storage)) - } -} - -impl RocList -where - T: Clone, -{ - pub fn from_slice(slice: &[T]) -> Self { - let mut list = Self::empty(); - list.extend_from_slice(slice); - list - } - - pub fn extend_from_slice(&mut self, slice: &[T]) { - // TODO: Can we do better for ZSTs? Alignment might be a problem. - - if slice.is_empty() { - return; - } - - let alignment = cmp::max(mem::align_of::(), mem::align_of::()); - let elements_offset = alignment; - - let new_size = elements_offset + mem::size_of::() * (self.len() + slice.len()); - - let new_ptr = if let Some((elements, storage)) = self.elements_and_storage() { - // Decrement the list's refence count. - let mut copy = storage.get(); - let is_unique = copy.decrease(); - - if is_unique { - // If the memory is not shared, we can reuse the memory. - let old_size = elements_offset + mem::size_of::() * self.len(); - unsafe { - let ptr = elements.as_ptr().cast::().sub(alignment).cast(); - roc_realloc(ptr, new_size, old_size, alignment as u32).cast() - } - } else { - if !copy.is_readonly() { - // Write the decremented reference count back. - storage.set(copy); - } - - // Allocate new memory. - let new_ptr = unsafe { roc_alloc(new_size, alignment as u32) }; - let new_elements = unsafe { - new_ptr - .cast::() - .add(alignment) - .cast::>() - }; - - // Initialize the reference count. - unsafe { - let storage_ptr = new_elements.cast::().sub(1); - storage_ptr.write(Storage::new_reference_counted()); - } - - // Copy the old elements to the new allocation. - unsafe { - copy_nonoverlapping(elements.as_ptr(), new_elements, self.length); - } - - new_ptr - } - } else { - // Allocate new memory. - let new_ptr = unsafe { roc_alloc(new_size, alignment as u32) }; - let new_elements = unsafe { new_ptr.cast::().add(elements_offset).cast::() }; - - // Initialize the reference count. - unsafe { - let storage_ptr = new_elements.cast::().sub(1); - storage_ptr.write(Storage::new_reference_counted()); - } - - new_ptr - }; - - let elements = unsafe { - new_ptr - .cast::() - .add(elements_offset) - .cast::>() - }; - - let non_null_elements = NonNull::new(elements).unwrap(); - self.elements = Some(non_null_elements); - - let elements = self.elements.unwrap().as_ptr(); - - let append_ptr = unsafe { elements.add(self.len()) }; - - // Use .cloned() to increment the elements' reference counts, if needed. - for (i, new_elem) in slice.iter().cloned().enumerate() { - unsafe { - // Write the element into the slot, without dropping it. - append_ptr - .add(i) - .write(ptr::read(&ManuallyDrop::new(new_elem))); - } - - // It's important that the length is increased one by one, to - // make sure that we don't drop uninitialized elements, even when - // a incrementing the reference count panics. - self.length += 1; - } - - self.capacity = self.length - } -} - -impl Deref for RocList { - type Target = [T]; - - fn deref(&self) -> &Self::Target { - if let Some(elements) = self.elements { - let elements = ptr::slice_from_raw_parts(elements.as_ptr().cast::(), self.length); - - unsafe { &*elements } - } else { - &[] - } - } -} - -impl Default for RocList { - fn default() -> Self { - Self::empty() - } -} - -impl PartialEq> for RocList -where - T: PartialEq, -{ - fn eq(&self, other: &RocList) -> bool { - self.deref() == other.deref() - } -} - -impl Eq for RocList where T: Eq {} - -impl PartialOrd> for RocList -where - T: PartialOrd, -{ - fn partial_cmp(&self, other: &RocList) -> Option { - // If one is longer than the other, use that as the ordering. - match self.length.partial_cmp(&other.length) { - Some(Ordering::Equal) => {} - ord => return ord, - } - - // If they're the same length, compare their elements - for index in 0..self.len() { - match self[index].partial_cmp(&other[index]) { - Some(Ordering::Equal) => {} - ord => return ord, - } - } - - // Capacity is ignored for ordering purposes. - Some(Ordering::Equal) - } -} - -impl Ord for RocList -where - T: Ord, -{ - fn cmp(&self, other: &Self) -> Ordering { - // If one is longer than the other, use that as the ordering. - match self.length.cmp(&other.length) { - Ordering::Equal => {} - ord => return ord, - } - - // If they're the same length, compare their elements - for index in 0..self.len() { - match self[index].cmp(&other[index]) { - Ordering::Equal => {} - ord => return ord, - } - } - - // Capacity is ignored for ordering purposes. - Ordering::Equal - } -} - -impl Debug for RocList -where - T: Debug, -{ - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - self.deref().fmt(f) - } -} - -impl Clone for RocList { - fn clone(&self) -> Self { - // Increment the reference count - if let Some((_, storage)) = self.elements_and_storage() { - let mut new_storage = storage.get(); - - if !new_storage.is_readonly() { - new_storage.increment_reference_count(); - storage.set(new_storage); - } - } - - Self { - elements: self.elements, - length: self.length, - capacity: self.capacity, - } - } -} - -impl Drop for RocList { - fn drop(&mut self) { - if let Some((elements, storage)) = self.elements_and_storage() { - // Decrease the list's reference count. - let mut new_storage = storage.get(); - let needs_dealloc = new_storage.decrease(); - - if needs_dealloc { - unsafe { - // Drop the stored elements. - for index in 0..self.len() { - let elem_ptr = elements.as_ptr().add(index); - - mem::drop::(ManuallyDrop::take(&mut *elem_ptr)); - } - - let alignment = cmp::max(mem::align_of::(), mem::align_of::()); - - // Release the memory. - roc_dealloc( - elements.as_ptr().cast::().sub(alignment).cast(), - alignment as u32, - ); - } - } else { - if !new_storage.is_readonly() { - // Write the storage back. - storage.set(new_storage); - } - } - } - } -} - -impl From<&[T]> for RocList -where - T: Clone, -{ - fn from(slice: &[T]) -> Self { - Self::from_slice(slice) - } -} - -impl IntoIterator for RocList { - type Item = T; - type IntoIter = IntoIter; - - fn into_iter(self) -> Self::IntoIter { - IntoIter { list: self, idx: 0 } - } -} - -pub struct IntoIter { - list: RocList, - idx: usize, -} - -impl Iterator for IntoIter { - type Item = T; - - fn next(&mut self) -> Option { - if self.list.len() <= self.idx { - return None; - } - - let elements = self.list.elements?; - let element_ptr = unsafe { elements.as_ptr().add(self.idx) }; - self.idx += 1; - - // Return the element. - Some(unsafe { ManuallyDrop::into_inner(element_ptr.read()) }) - } -} - -impl Drop for IntoIter { - fn drop(&mut self) { - // If there are any elements left that need to be dropped, drop them. - if let Some(elements) = self.list.elements { - // Set the list's length to zero to prevent double-frees. - // Note that this leaks if dropping any of the elements panics. - let len = mem::take(&mut self.list.length); - - // Drop the elements that haven't been returned from the iterator. - for i in self.idx..len { - mem::drop::(unsafe { ManuallyDrop::take(&mut *elements.as_ptr().add(i)) }) - } - } - } -} diff --git a/roc_std/src/roc_str.rs b/roc_std/src/roc_str.rs deleted file mode 100644 index 00f3a52fe7..0000000000 --- a/roc_std/src/roc_str.rs +++ /dev/null @@ -1,216 +0,0 @@ -#![deny(unsafe_op_in_unsafe_fn)] - -use core::{ - convert::TryFrom, - fmt::Debug, - mem::{size_of, ManuallyDrop}, - ops::{Deref, DerefMut}, -}; -use std::hash::Hash; - -use crate::RocList; - -#[repr(transparent)] -pub struct RocStr(RocStrInner); - -impl RocStr { - pub const SIZE: usize = core::mem::size_of::(); - pub const MASK: u8 = 0b1000_0000; - - pub const fn empty() -> Self { - Self(RocStrInner { - small_string: SmallString::empty(), - }) - } - - /// Create a string from bytes. - /// - /// # Safety - /// - /// `slice` must be valid UTF-8. - pub unsafe fn from_slice(slice: &[u8]) -> Self { - if let Some(small_string) = unsafe { SmallString::try_from(slice) } { - Self(RocStrInner { small_string }) - } else { - let heap_allocated = RocList::from_slice(slice); - Self(RocStrInner { - heap_allocated: ManuallyDrop::new(heap_allocated), - }) - } - } - - fn is_small_str(&self) -> bool { - unsafe { self.0.small_string.is_small_str() } - } - - fn as_enum_ref(&self) -> RocStrInnerRef { - if self.is_small_str() { - unsafe { RocStrInnerRef::SmallString(&self.0.small_string) } - } else { - unsafe { RocStrInnerRef::HeapAllocated(&self.0.heap_allocated) } - } - } - - pub fn len(&self) -> usize { - match self.as_enum_ref() { - RocStrInnerRef::HeapAllocated(h) => h.len(), - RocStrInnerRef::SmallString(s) => s.len(), - } - } - - pub fn is_empty(&self) -> bool { - self.len() == 0 - } - - pub fn as_str(&self) -> &str { - &*self - } -} - -impl Deref for RocStr { - type Target = str; - - fn deref(&self) -> &Self::Target { - match self.as_enum_ref() { - RocStrInnerRef::HeapAllocated(h) => unsafe { core::str::from_utf8_unchecked(&*h) }, - RocStrInnerRef::SmallString(s) => &*s, - } - } -} - -impl Default for RocStr { - fn default() -> Self { - Self::empty() - } -} - -impl From<&str> for RocStr { - fn from(s: &str) -> Self { - unsafe { Self::from_slice(s.as_bytes()) } - } -} - -impl PartialEq for RocStr { - fn eq(&self, other: &Self) -> bool { - self.deref() == other.deref() - } -} - -impl Eq for RocStr {} - -impl PartialOrd for RocStr { - fn partial_cmp(&self, other: &Self) -> Option { - self.as_str().partial_cmp(other.as_str()) - } -} - -impl Ord for RocStr { - fn cmp(&self, other: &Self) -> std::cmp::Ordering { - self.as_str().cmp(other.as_str()) - } -} - -impl Debug for RocStr { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - self.deref().fmt(f) - } -} - -impl Clone for RocStr { - fn clone(&self) -> Self { - match self.as_enum_ref() { - RocStrInnerRef::HeapAllocated(h) => Self(RocStrInner { - heap_allocated: ManuallyDrop::new(h.clone()), - }), - RocStrInnerRef::SmallString(s) => Self(RocStrInner { small_string: *s }), - } - } -} - -impl Drop for RocStr { - fn drop(&mut self) { - if !self.is_small_str() { - unsafe { - ManuallyDrop::drop(&mut self.0.heap_allocated); - } - } - } -} - -#[repr(C)] -union RocStrInner { - heap_allocated: ManuallyDrop>, - small_string: SmallString, -} - -enum RocStrInnerRef<'a> { - HeapAllocated(&'a RocList), - SmallString(&'a SmallString), -} - -#[derive(Debug, Clone, Copy)] -#[repr(C)] -struct SmallString { - bytes: [u8; Self::CAPACITY], - len: u8, -} - -impl SmallString { - const CAPACITY: usize = size_of::>() - 1; - - const fn empty() -> Self { - Self { - bytes: [0; Self::CAPACITY], - len: RocStr::MASK, - } - } - - /// # Safety - /// - /// `slice` must be valid UTF-8. - unsafe fn try_from(slice: &[u8]) -> Option { - // Check the size of the slice. - let len_as_u8 = u8::try_from(slice.len()).ok()?; - if (len_as_u8 as usize) > Self::CAPACITY { - return None; - } - - // Construct the small string. - let mut bytes = [0; Self::CAPACITY]; - bytes[..slice.len()].copy_from_slice(slice); - Some(Self { - bytes, - len: len_as_u8 | RocStr::MASK, - }) - } - - fn is_small_str(&self) -> bool { - self.len & RocStr::MASK != 0 - } - - fn len(&self) -> usize { - usize::from(self.len & !RocStr::MASK) - } -} - -impl Deref for SmallString { - type Target = str; - - fn deref(&self) -> &Self::Target { - let len = self.len(); - unsafe { core::str::from_utf8_unchecked(self.bytes.get_unchecked(..len)) } - } -} - -impl DerefMut for SmallString { - fn deref_mut(&mut self) -> &mut Self::Target { - let len = self.len(); - unsafe { core::str::from_utf8_unchecked_mut(self.bytes.get_unchecked_mut(..len)) } - } -} - -impl Hash for RocStr { - fn hash(&self, state: &mut H) { - self.as_str().hash(state) - } -} diff --git a/roc_std/src/storage.rs b/roc_std/src/storage.rs deleted file mode 100644 index 2be9976fca..0000000000 --- a/roc_std/src/storage.rs +++ /dev/null @@ -1,54 +0,0 @@ -use core::num::NonZeroIsize; - -const REFCOUNT_1: isize = isize::MIN; - -#[derive(Clone, Copy, Debug)] -pub enum Storage { - Readonly, - ReferenceCounted(NonZeroIsize), -} - -impl Storage { - pub fn new_reference_counted() -> Self { - Self::ReferenceCounted(NonZeroIsize::new(REFCOUNT_1).unwrap()) - } - - /// Increment the reference count. - pub fn increment_reference_count(&mut self) { - match self { - Storage::Readonly => { - // Do nothing. - } - Storage::ReferenceCounted(rc) => { - let new_rc = rc.get() + 1; - if let Some(new_rc) = NonZeroIsize::new(new_rc) { - *self = Storage::ReferenceCounted(new_rc); - } else { - *self = Storage::Readonly; - } - } - } - } - - /// Decrease the reference count. - /// - /// Returns `true` once there are no more references left. - pub fn decrease(&mut self) -> bool { - match self { - Storage::Readonly => false, - Storage::ReferenceCounted(rc) => { - let rc_as_isize = rc.get(); - if rc_as_isize == REFCOUNT_1 { - true - } else { - *rc = NonZeroIsize::new(rc_as_isize - 1).unwrap(); - false - } - } - } - } - - pub fn is_readonly(&self) -> bool { - matches!(self, Self::Readonly) - } -} diff --git a/roc_std/tests/test_roc_std.rs b/roc_std/tests/test_roc_std.rs deleted file mode 100644 index 2466deb628..0000000000 --- a/roc_std/tests/test_roc_std.rs +++ /dev/null @@ -1,126 +0,0 @@ -#[macro_use] -extern crate pretty_assertions; -// #[macro_use] -// extern crate indoc; -extern crate quickcheck; -extern crate roc_std; - -use core::ffi::c_void; - -#[no_mangle] -pub unsafe extern "C" fn roc_alloc(size: usize, _alignment: u32) -> *mut c_void { - libc::malloc(size) -} - -#[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) -} - -#[no_mangle] -pub unsafe extern "C" fn roc_dealloc(c_ptr: *mut c_void, _alignment: u32) { - libc::free(c_ptr) -} - -#[cfg(test)] -mod test_roc_std { - use roc_std::RocResult; - use roc_std::RocStr; - - fn roc_str_byte_representation(string: &RocStr) -> [u8; RocStr::SIZE] { - unsafe { core::mem::transmute_copy(string) } - } - - #[test] - fn roc_str_empty() { - let actual = roc_str_byte_representation(&RocStr::empty()); - - let mut expected = [0u8; RocStr::SIZE]; - expected[RocStr::SIZE - 1] = RocStr::MASK; - - assert_eq!(actual, expected); - } - - #[test] - fn roc_str_single_char() { - let actual = roc_str_byte_representation(&RocStr::from("a")); - - let mut expected = [0u8; RocStr::SIZE]; - expected[0] = b'a'; - expected[RocStr::SIZE - 1] = RocStr::MASK | 1; - - assert_eq!(actual, expected); - } - - #[test] - fn roc_str_max_small_string() { - let s = str::repeat("a", RocStr::SIZE - 1); - let actual = roc_str_byte_representation(&RocStr::from(s.as_str())); - - let mut expected = [0u8; RocStr::SIZE]; - expected[..RocStr::SIZE - 1].copy_from_slice(s.as_bytes()); - expected[RocStr::SIZE - 1] = RocStr::MASK | s.len() as u8; - - assert_eq!(actual, expected); - } - - #[test] - fn empty_string_from_str() { - let a = RocStr::from(""); - let b = RocStr::empty(); - - assert_eq!(a, b); - } - - #[test] - fn empty_string_length() { - let string = RocStr::from(""); - - assert_eq!(string.len(), 0); - } - - #[test] - fn empty_string_capacity() { - let string = RocStr::from(""); - - assert_eq!(string.capacity(), 0); - } - - #[test] - fn roc_result_to_rust_result() { - let greeting = "Hello, World!"; - let roc_result: RocResult = RocResult::ok(greeting.into()); - - match roc_result.into() { - Ok(answer) => { - assert_eq!(answer.as_str(), greeting); - } - Err(()) => { - panic!("Received an Err when Ok was expected.") - } - } - } - - #[test] - fn roc_result_is_ok() { - let greeting = "Hello, World!"; - let roc_result: RocResult = RocResult::ok(greeting.into()); - - assert!(roc_result.is_ok()); - assert!(!roc_result.is_err()); - } - - #[test] - fn roc_result_is_err() { - let greeting = "Hello, World!"; - let roc_result: RocResult<(), String> = RocResult::err(greeting.into()); - - assert!(!roc_result.is_ok()); - assert!(roc_result.is_err()); - } -} diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 4466e38ff1..48dc4f568a 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,7 +1,25 @@ -[toolchain] -channel = "1.60.0" # make sure to update the rust version in Earthfile as well -profile = "default" -components = [ - # for usages of rust-analyzer or similar tools inside `nix develop` - "rust-src" -] +[toolchain] +# How to update version: +# - update `channel = "RUST_VERSION"` +# - update `channel = "RUST_VERSION"` in examples/platform-switching/rust-platform/rust-toolchain.toml +# - update FROM rust:RUST_VERSION-slim-buster in Earthfile +# - to update the nightly version: +# - Find the latest nightly release that matches RUST_VERSION here: https://github.com/oxalica/rust-overlay/tree/master/manifests/nightly/2023 +# - update `channel = "nightly-OLD_DATE"` below +# - update nightly-OLD_DATE in .github/workflows/windows_tests.yml +# - update nightly-OLD_DATE in .github/workflows/windows_release_build.yml +# - update nightly-OLD_DATE in crates/compiler/build/src/link.rs + +channel = "1.71.1" # check ^^^ when changing this +# +# channel = "nightly-2023-05-28" # 1.71.0 nightly to be able to use unstable features +profile = "default" +components = [ + # for usages of rust-analyzer or similar tools inside `nix develop` + "rust-src" +] + +targets = [ + "wasm32-wasi", # for test_wasm.sh + "wasm32-unknown-unknown", # for repl_wasm +] diff --git a/test_utils/Cargo.toml b/test_utils/Cargo.toml deleted file mode 100644 index c053106f67..0000000000 --- a/test_utils/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "roc_test_utils" -version = "0.1.0" -authors = ["The Roc Contributors"] -license = "UPL-1.0" -edition = "2021" -description = "Utility functions used all over the code base." - -[dependencies] -pretty_assertions = "1.0.0" -remove_dir_all = "0.7.0" - -[dev-dependencies] diff --git a/test_utils/src/lib.rs b/test_utils/src/lib.rs deleted file mode 100644 index 78619e5e3c..0000000000 --- a/test_utils/src/lib.rs +++ /dev/null @@ -1,56 +0,0 @@ -use std::path::PathBuf; - -#[doc(hidden)] -pub use pretty_assertions::assert_eq as _pretty_assert_eq; - -#[derive(PartialEq)] -pub struct DebugAsDisplay(pub T); - -impl std::fmt::Debug for DebugAsDisplay { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.0.fmt(f) - } -} - -#[macro_export] -macro_rules! assert_multiline_str_eq { - ($a:expr, $b:expr) => { - $crate::_pretty_assert_eq!($crate::DebugAsDisplay($a), $crate::DebugAsDisplay($b)) - }; -} - -/** - * Creates a temporary empty directory that gets deleted when this goes out of scope. - */ -pub struct TmpDir { - path: std::path::PathBuf, -} - -impl TmpDir { - pub fn new(dir: &str) -> TmpDir { - let path = std::path::Path::new(dir); - // ensure_empty_dir will fail if the dir doesn't already exist - std::fs::create_dir_all(path).unwrap(); - remove_dir_all::ensure_empty_dir(&path).unwrap(); - - let mut pathbuf = std::path::PathBuf::new(); - pathbuf.push(path); - TmpDir { path: pathbuf } - } - - pub fn path(&self) -> &std::path::Path { - self.path.as_path() - } -} - -impl Drop for TmpDir { - fn drop(&mut self) { - // we "discard" the Result because there is no problem when a dir was already removed before we call remove_dir_all - let _ = remove_dir_all::remove_dir_all(&self.path); - } -} - -pub fn workspace_root() -> PathBuf { - let root = std::env::var("ROC_WORKSPACE_DIR").expect("Can't find the ROC_WORKSPACE_DIR variable expected to be set in .cargo/config.toml. Are you running tests outside of cargo?"); - PathBuf::from(root) -} diff --git a/typos.toml b/typos.toml new file mode 100644 index 0000000000..991b0f8c4b --- /dev/null +++ b/typos.toml @@ -0,0 +1,3 @@ +[files] +extend-exclude = ["crates/vendor/", "examples/static-site-gen/input/", "COPYRIGHT", "crates/compiler/checkmate/www/package-lock.json"] + diff --git a/utils/Cargo.toml b/utils/Cargo.toml deleted file mode 100644 index de90f5ecd7..0000000000 --- a/utils/Cargo.toml +++ /dev/null @@ -1,12 +0,0 @@ -[package] -name = "roc_utils" -version = "0.1.0" -authors = ["The Roc Contributors"] -license = "UPL-1.0" -edition = "2021" -description = "Utility functions used all over the code base." - -[dependencies] -snafu = { version = "0.6.10", features = ["backtraces"] } - -[dev-dependencies] diff --git a/utils/src/lib.rs b/utils/src/lib.rs deleted file mode 100644 index 25db29a929..0000000000 --- a/utils/src/lib.rs +++ /dev/null @@ -1,95 +0,0 @@ -use snafu::OptionExt; -use std::{collections::HashMap, slice::SliceIndex}; -use util_error::{IndexOfFailed, KeyNotFound, OutOfBounds, UtilResult}; - -pub mod util_error; - -// replace HashMap method that returns Option with one that returns Result and proper Error -pub fn map_get<'a, K: ::std::fmt::Debug + std::hash::Hash + std::cmp::Eq, V>( - hash_map: &'a HashMap, - key: &K, -) -> UtilResult<&'a V> { - let value = hash_map.get(key).context(KeyNotFound { - key_str: format!("{:?}", key), - })?; - - Ok(value) -} - -pub fn index_of(elt: T, slice: &[T]) -> UtilResult { - let index = slice - .iter() - .position(|slice_elt| *slice_elt == elt) - .with_context(|| { - let elt_str = format!("{:?}", elt); - let collection_str = format!("{:?}", slice); - - IndexOfFailed { - elt_str, - collection_str, - } - })?; - - Ok(index) -} - -// replaces slice method that return Option with one that return Result and proper Error -pub fn slice_get(index: usize, slice: &[T]) -> UtilResult<&>::Output> { - let elt_ref = slice.get(index).context(OutOfBounds { - index, - collection_name: "Slice", - len: slice.len(), - })?; - - Ok(elt_ref) -} - -pub fn slice_get_mut( - index: usize, - slice: &mut [T], -) -> UtilResult<&mut >::Output> { - let slice_len = slice.len(); - - let elt_ref = slice.get_mut(index).context(OutOfBounds { - index, - collection_name: "Slice", - len: slice_len, - })?; - - Ok(elt_ref) -} - -// returns the index of the first occurrence of element and index of the last occurrence -pub fn first_last_index_of( - elt: T, - slice: &[T], -) -> UtilResult<(usize, usize)> { - let mut first_index_opt = None; - let mut last_index_opt = None; - - for (index, list_elt) in slice.iter().enumerate() { - if *list_elt == elt { - if first_index_opt.is_none() { - first_index_opt = Some(index); - last_index_opt = Some(index); - } else { - last_index_opt = Some(index) - } - } else if last_index_opt.is_some() { - break; - } - } - - if let (Some(first_index), Some(last_index)) = (first_index_opt, last_index_opt) { - Ok((first_index, last_index)) - } else { - let elt_str = format!("{:?}", elt); - let collection_str = format!("{:?}", slice); - - IndexOfFailed { - elt_str, - collection_str, - } - .fail() - } -} diff --git a/utils/src/util_error.rs b/utils/src/util_error.rs deleted file mode 100644 index d19c230f11..0000000000 --- a/utils/src/util_error.rs +++ /dev/null @@ -1,35 +0,0 @@ -use snafu::{Backtrace, Snafu}; - -#[derive(Debug, Snafu)] -#[snafu(visibility(pub))] -pub enum UtilError { - #[snafu(display( - "IndexOfFailed: Element {} was not found in collection {}.", - elt_str, - collection_str - ))] - IndexOfFailed { - elt_str: String, - collection_str: String, - backtrace: Backtrace, - }, - #[snafu(display("KeyNotFound: key {} was not found in HashMap.", key_str,))] - KeyNotFound { - key_str: String, - backtrace: Backtrace, - }, - #[snafu(display( - "OutOfBounds: index {} was out of bounds for {} with length {}.", - index, - collection_name, - len - ))] - OutOfBounds { - index: usize, - collection_name: String, - len: usize, - backtrace: Backtrace, - }, -} - -pub type UtilResult = std::result::Result; diff --git a/vendor/inkwell/Cargo.toml b/vendor/inkwell/Cargo.toml deleted file mode 100644 index 64833638f4..0000000000 --- a/vendor/inkwell/Cargo.toml +++ /dev/null @@ -1,31 +0,0 @@ -[package] -name = "inkwell" -version = "0.1.0" -authors = ["The Roc Contributors"] -license = "UPL-1.0" -edition = "2018" - -[dependencies] -# NOTE: rtfeldman/inkwell is a fork of TheDan64/inkwell which does not change anything. -# -# The reason for this fork is that the way Inkwell is designed, you have to use -# a particular branch (e.g. "llvm8-0") in Cargo.toml. That would be fine, except that -# breaking changes get pushed directly to that branch, which breaks our build -# without warning. -# -# We tried referencing a specific rev on TheDan64/inkwell directly (instead of branch), -# but although that worked locally, it did not work on GitHub Actions. (After a few -# hours of investigation, gave up trying to figure out why.) So this is the workaround: -# having an immutable tag on the rtfeldman/inkwell fork which points to -# a particular "release" of Inkwell. -# -# When we want to update Inkwell, we can sync up rtfeldman/inkwell to the latest -# commit of TheDan64/inkwell, push a new tag which points to the latest commit, -# change the tag value in this Cargo.toml to point to that tag, and `cargo update`. -# This way, GitHub Actions works and nobody's builds get broken. -inkwell = { git = "https://github.com/rtfeldman/inkwell", branch = "master", features = [ "llvm13-0" ] } - -[features] -target-arm = [] -target-aarch64 = [] -target-webassembly = [] diff --git a/vendor/inkwell/src/lib.rs b/vendor/inkwell/src/lib.rs deleted file mode 100644 index 8136cc5eae..0000000000 --- a/vendor/inkwell/src/lib.rs +++ /dev/null @@ -1,4 +0,0 @@ -#![cfg(not(doctest))] -// re-export all inkwell members. This way we can switch -// inkwell versions by making changes in just one place. -pub use inkwell::*; diff --git a/vendor/morphic_lib/Cargo.toml b/vendor/morphic_lib/Cargo.toml deleted file mode 100644 index 55ece7212a..0000000000 --- a/vendor/morphic_lib/Cargo.toml +++ /dev/null @@ -1,11 +0,0 @@ -[package] -name = "morphic_lib" -version = "0.1.0" -authors = ["William Brandon", "Wilson Berkow", "Frank Dai", "Benjamin Driscoll"] -edition = "2018" - -[dependencies] -thiserror = "1.0.30" -sha2 = "0.9.8" -smallvec = "1.7.0" -typed-arena = "2.0.1" diff --git a/vendor/pathfinding/src/lib.rs b/vendor/pathfinding/src/lib.rs deleted file mode 100644 index 8044a7fed9..0000000000 --- a/vendor/pathfinding/src/lib.rs +++ /dev/null @@ -1,387 +0,0 @@ -// Adapted from the Pathfinding crate v2.0.3 by Samuel Tardieu , -// licensed under the Apache License, version 2.0 - https://www.apache.org/licenses/LICENSE-2.0 -// -// The original source code can be found at: https://github.com/samueltardieu/pathfinding -// -// Thank you, Samuel! -// -// -// -// This is modified from the original source to use the Roc compiler's preferred hashers -// instead of the SipHash hasher which Rust hash collections use by default. -// -// SipHash defends against hash flooding attacks by generating a random seed -// whenever a new hasher is instantiated, and which is designed to prevent attackers -// from crafting intentional collisions that amplify denial-of-service attacks. -// Since this is a compiler, we aren't worried about denial-of-service attacks. -// -// The primary motivation for this change is wanting the compiler to always give exactly -// the same answer given the same inputs. So if you give it the same source files, it should -// produce identical binaries every time. SipHash by design gives different answers on each run. -// -// Secondarily, SipHash isn't the fastest hashing algorithm out there, so we can get -// slightly better performance by using a faster hasher. - -use roc_collections::all::{default_hasher, BuildHasher, MutSet}; -use std::collections::{HashMap, HashSet, VecDeque}; -use std::hash::Hash; -use std::mem; - -/// Find a topological order in a directed graph if one exists. -/// -/// - `nodes` is a collection of nodes. -/// - `successors` returns a list of successors for a given node. -/// -/// The function returns either `Ok` with an acceptable topological order, -/// or `Err` with a node belonging to a cycle. In the latter case, the -/// strongly connected set can then be found using the -/// [`strongly_connected_component`](super::strongly_connected_components::strongly_connected_component) -/// function, or if only one of the loops is needed the [`bfs_loop`][super::bfs::bfs_loop] function -/// can be used instead to identify one of the shortest loops involving this node. -/// -/// # Examples -/// -/// We will sort integers from 1 to 9, each integer having its two immediate -/// greater numbers as successors: -/// -/// //``` -/// extern crate roc; -/// -/// use roc::graph::topological_sort; -/// -/// fn successors(node: &usize) -> Vec { -/// match *node { -/// n if n <= 7 => vec![n+1, n+2], -/// 8 => vec![9], -/// _ => vec![], -/// } -/// } -/// -/// let sorted = topological_sort(&[3, 7, 1, 4, 2, 9, 8, 6, 5], successors); -/// assert_eq!(sorted, Ok(vec![1, 2, 3, 4, 5, 6, 7, 8, 9])); -/// //``` -/// -/// If, however, there is a loop in the graph (for example, all nodes but 7 -/// have also 7 has a successor), one of the nodes in the loop will be returned as -/// an error: -/// -/// //``` -/// extern crate roc; -/// -/// use roc::graph::*; -/// -/// fn successors(node: &usize) -> Vec { -/// match *node { -/// n if n <= 6 => vec![n+1, n+2, 7], -/// 7 => vec![8, 9], -/// 8 => vec![7, 9], -/// _ => vec![7], -/// } -/// } -/// -/// let sorted = topological_sort(&[3, 7, 1, 4, 2, 9, 8, 6, 5], successors); -/// assert!(sorted.is_err()); -/// -/// // Let's assume that the returned node is 8 (it can be any node which is part -/// // of a loop). We can lookup up one of the shortest loops containing 8 -/// // (8 -> 7 -> 8 is the unique loop with two hops containing 8): -/// -/// // assert_eq!(bfs_loop(&8, successors), Some(vec![8, 7, 8])); -/// -/// // We can also request the whole strongly connected set containing 8. Here -/// // 7, 8, and 9 are all reachable from one another. -/// -/// let mut set = strongly_connected_component(&8, successors); -/// set.sort(); -/// assert_eq!(set, vec![7, 8, 9]); -/// //``` -pub fn topological_sort(nodes: I, mut successors: FN) -> Result, N> -where - N: Eq + Hash + Clone, - I: Iterator, - FN: FnMut(&N) -> IN, - IN: IntoIterator, -{ - let size_hint = nodes.size_hint().0; - let mut unmarked: MutSet = nodes.collect::>(); - let mut marked = HashSet::with_capacity_and_hasher(size_hint, default_hasher()); - let mut temp = MutSet::default(); - let mut sorted = VecDeque::with_capacity(size_hint); - while let Some(node) = unmarked.iter().next().cloned() { - temp.clear(); - visit( - &node, - &mut successors, - &mut unmarked, - &mut marked, - &mut temp, - &mut sorted, - )?; - } - Ok(sorted.into_iter().collect()) -} - -fn visit( - node: &N, - successors: &mut FN, - unmarked: &mut MutSet, - marked: &mut MutSet, - temp: &mut MutSet, - sorted: &mut VecDeque, -) -> Result<(), N> -where - N: Eq + Hash + Clone, - FN: FnMut(&N) -> IN, - IN: IntoIterator, -{ - unmarked.remove(node); - if marked.contains(node) { - return Ok(()); - } - if temp.contains(node) { - return Err(node.clone()); - } - temp.insert(node.clone()); - for n in successors(node) { - visit(&n, successors, unmarked, marked, temp, sorted)?; - } - marked.insert(node.clone()); - sorted.push_front(node.clone()); - Ok(()) -} - -/// Topologically sort a directed graph into groups of independent nodes. -/// -/// - `nodes` is a collection of nodes. -/// - `successors` returns a list of successors for a given node. -/// -/// This function works like [`topological_sort`](self::topological_sort), but -/// rather than producing a single ordering of nodes, this function partitions -/// the nodes into groups: the first group contains all nodes with no -/// dependencies, the second group contains all nodes whose only dependencies -/// are in the first group, and so on. Concatenating the groups produces a -/// valid topological sort regardless of how the nodes within each group are -/// reordered. No guarantees are made about the order of nodes within each -/// group. -/// -/// The function returns either `Ok` with a valid list of groups, or `Err` with -/// a (groups, remaining) tuple containing a (possibly empty) partial list of -/// groups, and a list of remaining nodes that could not be grouped due to -/// cycles. In the error case, the strongly connected set(s) can then be found -/// using the -/// [`strongly_connected_components`](super::strongly_connected_components::strongly_connected_components) -/// function on the list of remaining nodes. -/// -/// The current implementation uses a variation of [Kahn's -/// algorithm](https://en.wikipedia.org/wiki/Topological_sorting#Kahn's_algorithm), -/// and runs in O(|V| + |E|) time. -#[allow(clippy::type_complexity)] -#[allow(dead_code)] -pub fn topological_sort_into_groups( - nodes: &[N], - mut successors: FN, -) -> Result>, (Vec>, Vec)> -where - N: Eq + Hash + Clone, - FN: FnMut(&N) -> IN, - IN: IntoIterator, -{ - if nodes.is_empty() { - return Ok(Vec::new()); - } - let mut succs_map = HashMap::, BuildHasher>::with_capacity_and_hasher( - nodes.len(), - default_hasher(), - ); - let mut preds_map = - HashMap::::with_capacity_and_hasher(nodes.len(), default_hasher()); - for node in nodes.iter() { - succs_map.insert(node.clone(), successors(node).into_iter().collect()); - preds_map.insert(node.clone(), 0); - } - - for succs in succs_map.values() { - for succ in succs.iter() { - *preds_map - .get_mut(succ) - .unwrap_or_else(|| panic!("key missing from preds_map")) += 1; - } - } - let mut groups = Vec::>::new(); - let mut prev_group: Vec = preds_map - .iter() - .filter_map(|(node, &num_preds)| { - if num_preds == 0 { - Some(node.clone()) - } else { - None - } - }) - .collect(); - if prev_group.is_empty() { - let remaining: Vec = preds_map.into_iter().map(|(node, _)| node).collect(); - return Err((Vec::new(), remaining)); - } - for node in &prev_group { - preds_map.remove(node); - } - while !preds_map.is_empty() { - let mut next_group = Vec::::new(); - for node in &prev_group { - for succ in &succs_map[node] { - { - let num_preds = preds_map.get_mut(succ).unwrap(); - *num_preds -= 1; - if *num_preds > 0 { - continue; - } - } - next_group.push(preds_map.remove_entry(succ).unwrap().0); - } - } - groups.push(mem::replace(&mut prev_group, next_group)); - if prev_group.is_empty() { - let remaining: Vec = preds_map.into_iter().map(|(node, _)| node).collect(); - return Err((groups, remaining)); - } - } - groups.push(prev_group); - Ok(groups) -} - -// Separate nodes of a directed graph into [strongly connected -// components](https://en.wikipedia.org/wiki/Strongly_connected_component). -// -// A [path-based strong component -// algorithm](https://en.wikipedia.org/wiki/Path-based_strong_component_algorithm) -// is used. - -struct Params -where - N: Clone + Hash + Eq, - FN: FnMut(&N) -> IN, - IN: IntoIterator, -{ - preorders: HashMap, BuildHasher>, - c: usize, - successors: FN, - p: Vec, - s: Vec, - scc: Vec>, - scca: MutSet, -} - -impl Params -where - N: Clone + Hash + Eq, - FN: FnMut(&N) -> IN, - IN: IntoIterator, -{ - fn new(nodes: &[N], successors: FN) -> Self { - Self { - preorders: nodes.iter().map(|n| (n.clone(), None)).collect::, - BuildHasher, - >>(), - c: 0, - successors, - p: Vec::new(), - s: Vec::new(), - scc: Vec::new(), - scca: MutSet::default(), - } - } -} - -fn recurse_onto(v: &N, params: &mut Params) -where - N: Clone + Hash + Eq, - FN: FnMut(&N) -> IN, - IN: IntoIterator, -{ - params.preorders.insert(v.clone(), Some(params.c)); - params.c += 1; - params.s.push(v.clone()); - params.p.push(v.clone()); - for w in (params.successors)(v) { - if !params.scca.contains(&w) { - if let Some(pw) = params.preorders.get(&w).and_then(|w| *w) { - while params.preorders[¶ms.p[params.p.len() - 1]].unwrap() > pw { - params.p.pop(); - } - } else { - recurse_onto(&w, params); - } - } - } - if params.p[params.p.len() - 1] == *v { - params.p.pop(); - let mut component = Vec::new(); - while let Some(node) = params.s.pop() { - component.push(node.clone()); - params.scca.insert(node.clone()); - params.preorders.remove(&node); - if node == *v { - break; - } - } - params.scc.push(component); - } -} - -/// Partition nodes reachable from a starting point into strongly connected components. -/// -/// - `start` is the node we want to explore the graph from. -/// - `successors` returns a list of successors for a given node. -/// -/// The function returns a list of strongly connected components sets. It will contain -/// at least one component (the one containing the `start` node). -pub fn strongly_connected_components_from(start: &N, successors: FN) -> Vec> -where - N: Clone + Hash + Eq, - FN: FnMut(&N) -> IN, - IN: IntoIterator, -{ - let mut params = Params::new(&[], successors); - recurse_onto(start, &mut params); - params.scc -} - -/// Compute the strongly connected component containing a given node. -/// -/// - `node` is the node we want the strongly connected component for. -/// - `successors` returns a list of successors for a given node. -/// -/// The function returns the strongly connected component containing the node, -/// which is guaranteed to contain at least `node`. -pub fn strongly_connected_component(node: &N, successors: FN) -> Vec -where - N: Clone + Hash + Eq, - FN: FnMut(&N) -> IN, - IN: IntoIterator, -{ - strongly_connected_components_from(node, successors) - .pop() - .unwrap() -} - -/// Partition all strongly connected components in a graph. -/// -/// - `nodes` is a collection of nodes. -/// - `successors` returns a list of successors for a given node. -/// -/// The function returns a list of strongly connected components sets. -#[allow(dead_code)] -pub fn strongly_connected_components(nodes: &[N], successors: FN) -> Vec> -where - N: Clone + Hash + Eq, - FN: FnMut(&N) -> IN, - IN: IntoIterator, -{ - let mut params = Params::new(nodes, successors); - while let Some(node) = params.preorders.keys().find(|_| true).cloned() { - recurse_onto(&node, &mut params); - } - params.scc -} diff --git a/vendor/pretty/Cargo.toml b/vendor/pretty/Cargo.toml deleted file mode 100644 index 7df7a3b737..0000000000 --- a/vendor/pretty/Cargo.toml +++ /dev/null @@ -1,19 +0,0 @@ -[package] -name = "ven_pretty" -version = "0.9.1-alpha.0" -authors = [ "Jonathan Sterling ", "Darin Morrison ", "Markus Westerlind "] -description = "Wadler-style pretty-printing combinators in Rust" -documentation = "https://docs.rs/pretty/" -keywords = ["console", "functional", "pretty-printing"] -license = "MIT" -readme = "README.md" -repository = "https://github.com/Marwes/pretty.rs" -edition = "2018" - -[package.metadata.docs.rs] -features = ["termcolor"] - -[dependencies] -arrayvec = "0.7.2" -typed-arena = "2.0.1" -termcolor = { version = "1.1.2", optional = true } diff --git a/vendor/pretty/README.md b/vendor/pretty/README.md deleted file mode 100644 index 760678de21..0000000000 --- a/vendor/pretty/README.md +++ /dev/null @@ -1,44 +0,0 @@ -# pretty.rs - -[![Build Status](https://travis-ci.org/Marwes/pretty.rs.svg?branch=master)](https://travis-ci.org/Marwes/pretty.rs) [![Docs](https://docs.rs/pretty/badge.svg)](https://docs.rs/pretty) - -Pretty printing combinators for Rust - -## Synopsis - -This crate provides functionality for defining pretty printers. It is -particularly useful for printing structured recursive data like trees. - -The implementation was originally based on Larsen's SML translation -(https://github.com/kfl/wpp) of Wadler's Haskell pretty printer -(https://homepages.inf.ed.ac.uk/wadler/papers/prettier/prettier.pdf). It -has since been modified in various ways to better fit Rust's -programming model. In particular, it uses iteration rather than -recursion and provides streaming output. - -## Documentation - -See the generated API documentation [here](https://docs.rs/pretty). - -## Requirements - -1. [Rust](https://www.rust-lang.org/) -2. [Cargo](https://crates.io/) - -You can install both with the following: - -``` -$ curl -s https://static.rust-lang.org/rustup.sh | sudo sh -``` - -See [Installation](https://doc.rust-lang.org/book/ch01-01-installation.html) for further details. - -## Usage - -``` -$ cargo build ## build library and binary -$ cargo run --example trees ## run the example (pretty trees) -$ cargo run --example colored --features termcolor ## run the example (pretty colored output) -$ cargo bench ## run benchmarks -$ cargo test ## run tests -``` diff --git a/vendor/pretty/src/lib.rs b/vendor/pretty/src/lib.rs deleted file mode 100644 index ef64dc1652..0000000000 --- a/vendor/pretty/src/lib.rs +++ /dev/null @@ -1,1024 +0,0 @@ -#![allow(clippy::all)] -#[cfg(feature = "termcolor")] -pub extern crate termcolor; - -use std::{borrow::Cow, convert::TryInto, fmt, io, ops::Deref, rc::Rc}; -#[cfg(feature = "termcolor")] -use termcolor::{ColorSpec, WriteColor}; - -mod render; - -#[cfg(feature = "termcolor")] -pub use self::render::TermColored; -pub use self::render::{FmtWrite, IoWrite, Render, RenderAnnotated}; - -/// The concrete document type. This type is not meant to be used directly. Instead use the static -/// functions on `Doc` or the methods on an `DocAllocator`. -/// -/// The `T` parameter is used to abstract over pointers to `Doc`. See `RefDoc` and `BoxDoc` for how -/// it is used -#[derive(Clone)] -pub enum Doc<'a, T: DocPtr<'a, A>, A = ()> { - Nil, - Append(T, T), - Group(T), - FlatAlt(T, T), - Nest(isize, T), - Line, - OwnedText(Box), - BorrowedText(&'a str), - SmallText(SmallText), - Annotated(A, T), - Union(T, T), - Column(T::ColumnFn), - Nesting(T::ColumnFn), -} - -pub type SmallText = arrayvec::ArrayString<22>; - -impl<'a, T, A> fmt::Debug for Doc<'a, T, A> -where - T: DocPtr<'a, A> + fmt::Debug, - A: fmt::Debug, -{ - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Doc::Nil => f.debug_tuple("Nil").finish(), - Doc::Append(ref ldoc, ref rdoc) => { - f.debug_tuple("Append").field(ldoc).field(rdoc).finish() - } - Doc::FlatAlt(ref x, ref y) => f.debug_tuple("FlatAlt").field(x).field(y).finish(), - Doc::Group(ref doc) => f.debug_tuple("Group").field(doc).finish(), - Doc::Nest(off, ref doc) => f.debug_tuple("Nest").field(&off).field(doc).finish(), - Doc::Line => f.debug_tuple("Line").finish(), - Doc::OwnedText(ref s) => f.debug_tuple("Text").field(s).finish(), - Doc::BorrowedText(ref s) => f.debug_tuple("Text").field(s).finish(), - Doc::SmallText(ref s) => f.debug_tuple("Text").field(s).finish(), - Doc::Annotated(ref ann, ref doc) => { - f.debug_tuple("Annotated").field(ann).field(doc).finish() - } - Doc::Union(ref l, ref r) => f.debug_tuple("Union").field(l).field(r).finish(), - Doc::Column(_) => f.debug_tuple("Column(..)").finish(), - Doc::Nesting(_) => f.debug_tuple("Nesting(..)").finish(), - } - } -} - -macro_rules! impl_doc { - ($name: ident, $allocator: ident) => { - #[derive(Clone)] - pub struct $name<'a, A = ()>(Box, A>>); - - impl<'a, A> fmt::Debug for $name<'a, A> - where - A: fmt::Debug, - { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - self.0.fmt(f) - } - } - - impl<'a, A> $name<'a, A> { - pub fn new(doc: Doc<'a, $name<'a, A>, A>) -> $name<'a, A> { - $name(Box::new(doc)) - } - } - - impl<'a, A> From> for $name<'a, A> { - fn from(doc: Doc<'a, $name<'a, A>, A>) -> $name<'a, A> { - $name::new(doc) - } - } - - impl<'a, A> Deref for $name<'a, A> { - type Target = Doc<'a, $name<'a, A>, A>; - - fn deref(&self) -> &Self::Target { - &self.0 - } - } - - impl<'a, A> DocAllocator<'a, A> for $allocator - where - A: 'a, - { - type Doc = $name<'a, A>; - - #[inline] - fn alloc(&'a self, doc: Doc<'a, Self::Doc, A>) -> Self::Doc { - $name::new(doc) - } - fn alloc_column_fn( - &'a self, - f: impl Fn(usize) -> Self::Doc + 'a, - ) -> >::ColumnFn { - Rc::new(f) - } - fn alloc_width_fn( - &'a self, - f: impl Fn(isize) -> Self::Doc + 'a, - ) -> >::WidthFn { - Rc::new(f) - } - } - - impl<'a, A> DocPtr<'a, A> for $name<'a, A> { - type ColumnFn = std::rc::Rc Self + 'a>; - type WidthFn = std::rc::Rc Self + 'a>; - } - - impl<'a, A> StaticDoc<'a, A> for $name<'a, A> { - type Allocator = $allocator; - const ALLOCATOR: &'static Self::Allocator = &$allocator; - } - - impl_doc_methods!($name ('a, A) where () where ()); - - impl<'a, A> $name<'a, A> { - /// Append the given document after this document. - #[inline] - pub fn append(self, that: D) -> Self - where - D: Into>, - { - DocBuilder(&$allocator, self.into()).append(that).into_doc() - } - - /// A single document concatenating all the given documents. - #[inline] - pub fn concat(docs: I) -> Self - where - I: IntoIterator, - I::Item: Into>, - { - $allocator.concat(docs).into_doc() - } - - /// A single document interspersing the given separator `S` between the given documents. For - /// example, if the documents are `[A, B, C, ..., Z]`, this yields `[A, S, B, S, C, S, ..., S, Z]`. - /// - /// Compare [the `intersperse` method from the `itertools` crate](https://docs.rs/itertools/0.5.9/itertools/trait.Itertools.html#method.intersperse). - /// - /// NOTE: The separator type, `S` may need to be cloned. Consider using cheaply cloneable ptr - /// like `RefDoc` or `RcDoc` - #[inline] - pub fn intersperse(docs: I, separator: S) -> Self - where - I: IntoIterator, - I::Item: Into>, - S: Into> + Clone, - A: Clone, - { - $allocator.intersperse(docs, separator).into_doc() - } - - /// Acts as `self` when laid out on multiple lines and acts as `that` when laid out on a single line. - #[inline] - pub fn flat_alt(self, doc: D) -> Self - where - D: Into>, - { - DocBuilder(&$allocator, self.into()) - .flat_alt(doc) - .into_doc() - } - - /// Mark this document as a group. - /// - /// Groups are laid out on a single line if possible. Within a group, all basic documents with - /// several possible layouts are assigned the same layout, that is, they are all laid out - /// horizontally and combined into a one single line, or they are each laid out on their own - /// line. - #[inline] - pub fn group(self) -> Self { - DocBuilder(&$allocator, self.into()).group().into_doc() - } - - /// Increase the indentation level of this document. - #[inline] - pub fn nest(self, offset: isize) -> Self { - DocBuilder(&$allocator, self.into()).nest(offset).into_doc() - } - - #[inline] - pub fn annotate(self, ann: A) -> Self { - DocBuilder(&$allocator, self.into()) - .annotate(ann) - .into_doc() - } - - #[inline] - pub fn union(self, other: D) -> Self - where - D: Into>, - { - DocBuilder(&$allocator, self.into()).union(other).into_doc() - } - } - }; -} - -enum FmtText { - Small(SmallText), - Large(String), -} - -impl fmt::Write for FmtText { - fn write_str(&mut self, s: &str) -> fmt::Result { - match self { - FmtText::Small(buf) => { - if let Err(_) = buf.try_push_str(s) { - let mut new_str = String::with_capacity(buf.len() + s.len()); - new_str.push_str(buf); - new_str.push_str(s); - *self = FmtText::Large(new_str); - } - } - FmtText::Large(buf) => buf.push_str(s), - } - Ok(()) - } -} - -macro_rules! impl_doc_methods { - ($name: ident ( $($params: tt)* ) where ( $($where_: tt)* ) where ( $($where_2: tt)* )) => { - impl< $($params)* > $name< $($params)* > - where $($where_)* - { - /// An empty document. - #[inline] - pub fn nil() -> Self { - Doc::Nil.into() - } - - /// The text `t.to_string()`. - /// - /// The given text must not contain line breaks. - #[inline] - pub fn as_string(data: U) -> Self { - use std::fmt::Write; - let mut buf = FmtText::Small(SmallText::new()); - write!(buf, "{}", data).unwrap(); - (match buf { - FmtText::Small(b) => Doc::SmallText(b), - FmtText::Large(b) => Doc::OwnedText(b.into()), - }).into() - } - - /// A single hardline. - #[inline] - pub fn hardline() -> Self { - Doc::Line.into() - } - - /// The given text, which must not contain line breaks. - #[inline] - pub fn text>>(data: U) -> Self { - match data.into() { - Cow::Owned(t) => Doc::OwnedText(t.into()).into(), - Cow::Borrowed(t) => Doc::BorrowedText(t).into(), - } - } - - #[inline] - pub fn space() -> Self { - Doc::BorrowedText(" ").into() - } - } - - impl< $($params)* > $name< $($params)* > - where $($where_2)* - { - /// A line acts like a `\n` but behaves like `space` if it is grouped on a single line. - #[inline] - pub fn line() -> Self { - Self::hardline().flat_alt(Self::space()).into() - } - - /// Acts like `line` but behaves like `nil` if grouped on a single line - #[inline] - pub fn line_() -> Self { - Self::hardline().flat_alt(Self::nil()).into() - } - } - }; -} - -impl_doc!(BoxDoc, BoxAllocator); -impl_doc!(RcDoc, RcAllocator); - -impl_doc_methods!(Doc ('a, D, A) where (D: DocPtr<'a, A>) where (D: StaticDoc<'a, A>)); -impl_doc_methods!(BuildDoc ('a, D, A) where (D: DocPtr<'a, A>) where (D: StaticDoc<'a, A>)); - -pub struct BoxAllocator; - -pub struct RcAllocator; - -impl<'a, T, A> BuildDoc<'a, T, A> -where - T: StaticDoc<'a, A>, -{ - fn flat_alt(self, doc: D) -> Self - where - D: Into>, - { - DocBuilder(&T::ALLOCATOR, self.into()).flat_alt(doc).1 - } -} - -impl<'a, T, A> Doc<'a, T, A> -where - T: StaticDoc<'a, A>, -{ - fn flat_alt(self, doc: D) -> Self - where - D: Into>, - { - DocBuilder(&T::ALLOCATOR, self.into()) - .flat_alt(doc) - .into_plain_doc() - } -} - -pub trait StaticDoc<'a, A>: DocPtr<'a, A> -where - A: 'a, -{ - type Allocator: DocAllocator<'a, A, Doc = Self> + 'static; - const ALLOCATOR: &'static Self::Allocator; -} - -impl<'a, T, A, S> From for Doc<'a, T, A> -where - T: DocPtr<'a, A>, - S: Into>, -{ - fn from(s: S) -> Doc<'a, T, A> { - Doc::text(s) - } -} - -pub struct Pretty<'a, 'd, T, A> -where - A: 'a, - T: DocPtr<'a, A> + 'a, -{ - doc: &'d Doc<'a, T, A>, - width: usize, -} - -impl<'a, T, A> fmt::Display for Pretty<'a, '_, T, A> -where - T: DocPtr<'a, A>, -{ - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - self.doc.render_fmt(self.width, f) - } -} - -impl<'a, T, A> Doc<'a, T, A> -where - T: DocPtr<'a, A> + 'a, -{ - /// Writes a rendered document to a `std::io::Write` object. - #[inline] - pub fn render(&self, width: usize, out: &mut W) -> io::Result<()> - where - W: ?Sized + io::Write, - { - self.render_raw(width, &mut IoWrite::new(out)) - } - - /// Writes a rendered document to a `std::fmt::Write` object. - #[inline] - pub fn render_fmt(&self, width: usize, out: &mut W) -> fmt::Result - where - W: ?Sized + fmt::Write, - { - self.render_raw(width, &mut FmtWrite::new(out)) - } - - /// Writes a rendered document to a `RenderAnnotated` object. - #[inline] - pub fn render_raw(&self, width: usize, out: &mut W) -> Result<(), W::Error> - where - W: ?Sized + render::RenderAnnotated, - { - render::best(self, width, out) - } - - /// Returns a value which implements `std::fmt::Display` - /// - #[inline] - pub fn pretty<'d>(&'d self, width: usize) -> Pretty<'a, 'd, T, A> { - Pretty { doc: self, width } - } -} - -#[cfg(feature = "termcolor")] -impl<'a, T> Doc<'a, T, ColorSpec> -where - T: DocPtr<'a, ColorSpec> + 'a, -{ - #[inline] - pub fn render_colored(&self, width: usize, out: W) -> io::Result<()> - where - W: WriteColor, - { - render::best(self, width, &mut TermColored::new(out)) - } -} - -/// The `DocBuilder` type allows for convenient appending of documents even for arena allocated -/// documents by storing the arena inline. -pub struct DocBuilder<'a, D, A = ()>(pub &'a D, pub BuildDoc<'a, D::Doc, A>) -where - D: ?Sized + DocAllocator<'a, A>; - -impl<'a, A, D> Clone for DocBuilder<'a, D, A> -where - A: Clone, - D: DocAllocator<'a, A> + 'a, - D::Doc: Clone, -{ - fn clone(&self) -> Self { - DocBuilder(self.0, self.1.clone()) - } -} - -impl<'a, D, A> Into> for DocBuilder<'a, D, A> -where - D: ?Sized + DocAllocator<'a, A>, -{ - fn into(self) -> BuildDoc<'a, D::Doc, A> { - self.1 - } -} - -pub trait DocPtr<'a, A>: Deref> + Sized -where - A: 'a, -{ - type ColumnFn: Deref Self + 'a> + Clone + 'a; - type WidthFn: Deref Self + 'a> + Clone + 'a; -} - -impl<'a, A> DocPtr<'a, A> for RefDoc<'a, A> { - type ColumnFn = &'a (dyn Fn(usize) -> Self + 'a); - type WidthFn = &'a (dyn Fn(isize) -> Self + 'a); -} - -/// The `DocAllocator` trait abstracts over a type which can allocate (pointers to) `Doc`. -pub trait DocAllocator<'a, A = ()> -where - A: 'a, -{ - type Doc: DocPtr<'a, A>; - - fn alloc(&'a self, doc: Doc<'a, Self::Doc, A>) -> Self::Doc; - - fn alloc_column_fn( - &'a self, - f: impl Fn(usize) -> Self::Doc + 'a, - ) -> >::ColumnFn; - - fn alloc_width_fn( - &'a self, - f: impl Fn(isize) -> Self::Doc + 'a, - ) -> >::WidthFn; - - fn alloc_cow(&'a self, doc: BuildDoc<'a, Self::Doc, A>) -> Self::Doc { - match doc { - BuildDoc::DocPtr(d) => d, - BuildDoc::Doc(d) => self.alloc(d), - } - } - - /// Allocate an empty document. - #[inline] - fn nil(&'a self) -> DocBuilder<'a, Self, A> { - DocBuilder(self, Doc::Nil.into()) - } - - /// Allocate a single hardline. - #[inline] - fn hardline(&'a self) -> DocBuilder<'a, Self, A> { - DocBuilder(self, Doc::Line.into()) - } - - #[inline] - fn space(&'a self) -> DocBuilder<'a, Self, A> { - self.text(" ") - } - - /// A line acts like a `\n` but behaves like `space` if it is grouped on a single line. - #[inline] - fn line(&'a self) -> DocBuilder<'a, Self, A> { - self.hardline().flat_alt(self.space()) - } - - /// Acts like `line` but behaves like `nil` if grouped on a single line - /// - #[inline] - fn line_(&'a self) -> DocBuilder<'a, Self, A> { - self.hardline().flat_alt(self.nil()) - } - - /// A `softline` acts like `space` if the document fits the page, otherwise like `line` - #[inline] - fn softline(&'a self) -> DocBuilder<'a, Self, A> { - self.line().group() - } - - /// A `softline_` acts like `nil` if the document fits the page, otherwise like `line_` - #[inline] - fn softline_(&'a self) -> DocBuilder<'a, Self, A> { - self.line_().group() - } - - /// Allocate a document containing the text `t.to_string()`. - /// - /// The given text must not contain line breaks. - #[inline] - fn as_string(&'a self, data: U) -> DocBuilder<'a, Self, A> { - DocBuilder(self, Doc::as_string(data.to_string()).into()) - } - - /// Allocate a document containing the given text. - /// - /// The given text must not contain line breaks. - #[inline] - fn text>>(&'a self, data: U) -> DocBuilder<'a, Self, A> { - DocBuilder(self, Doc::text(data).into()) - } - - /// Allocate a document concatenating the given documents. - #[inline] - fn concat(&'a self, docs: I) -> DocBuilder<'a, Self, A> - where - I: IntoIterator, - I::Item: Into>, - { - docs.into_iter().fold(self.nil(), |a, b| a.append(b)) - } - - /// Allocate a document that intersperses the given separator `S` between the given documents - /// `[A, B, C, ..., Z]`, yielding `[A, S, B, S, C, S, ..., S, Z]`. - /// - /// Compare [the `intersperse` method from the `itertools` crate](https://docs.rs/itertools/0.5.9/itertools/trait.Itertools.html#method.intersperse). - /// - /// NOTE: The separator type, `S` may need to be cloned. Consider using cheaply cloneable ptr - /// like `RefDoc` or `RcDoc` - #[inline] - fn intersperse(&'a self, docs: I, separator: S) -> DocBuilder<'a, Self, A> - where - I: IntoIterator, - I::Item: Into>, - S: Into> + Clone, - { - let mut result = self.nil(); - let mut iter = docs.into_iter(); - - if let Some(first) = iter.next() { - result = result.append(first); - - for doc in iter { - result = result.append(separator.clone()); - result = result.append(doc); - } - } - - result - } - - /// Allocate a document that acts differently based on the position and page layout - /// - #[inline] - fn column(&'a self, f: impl Fn(usize) -> Self::Doc + 'a) -> DocBuilder<'a, Self, A> { - DocBuilder(self, Doc::Column(self.alloc_column_fn(f)).into()) - } - - /// Allocate a document that acts differently based on the current nesting level - /// - #[inline] - fn nesting(&'a self, f: impl Fn(usize) -> Self::Doc + 'a) -> DocBuilder<'a, Self, A> { - DocBuilder(self, Doc::Nesting(self.alloc_column_fn(f)).into()) - } - - /// Reflows `text` inserting `softline` in place of any whitespace - #[inline] - fn reflow(&'a self, text: &'a str) -> DocBuilder<'a, Self, A> - where - Self: Sized, - Self::Doc: Clone, - A: Clone, - { - self.intersperse(text.split(char::is_whitespace), self.line().group()) - } -} - -/// Either a `Doc` or a pointer to a `Doc` (`D`) -#[derive(Clone, Debug)] -pub enum BuildDoc<'a, D, A> -where - D: DocPtr<'a, A>, -{ - DocPtr(D), - Doc(Doc<'a, D, A>), -} - -impl<'a, D, A> Deref for BuildDoc<'a, D, A> -where - D: DocPtr<'a, A>, -{ - type Target = Doc<'a, D, A>; - fn deref(&self) -> &Self::Target { - match self { - BuildDoc::DocPtr(d) => d, - BuildDoc::Doc(d) => d, - } - } -} - -impl<'a, A> From> for BuildDoc<'a, RefDoc<'a, A>, A> { - fn from(s: RefDoc<'a, A>) -> Self { - BuildDoc::DocPtr(s) - } -} - -impl<'a, A> From> for BuildDoc<'a, BoxDoc<'a, A>, A> { - fn from(s: BoxDoc<'a, A>) -> Self { - BuildDoc::DocPtr(s) - } -} - -impl<'a, A> From> for BuildDoc<'a, RcDoc<'a, A>, A> { - fn from(s: RcDoc<'a, A>) -> Self { - BuildDoc::DocPtr(s) - } -} - -impl<'a, T, A> From> for BuildDoc<'a, T, A> -where - T: DocPtr<'a, A>, -{ - fn from(s: Doc<'a, T, A>) -> Self { - BuildDoc::Doc(s) - } -} - -impl<'a, T, A, S> From for BuildDoc<'a, T, A> -where - T: DocPtr<'a, A>, - S: Into>, -{ - fn from(s: S) -> Self { - BuildDoc::Doc(Doc::text(s)) - } -} - -impl<'a, 's, D, A> DocBuilder<'a, D, A> -where - D: ?Sized + DocAllocator<'a, A>, -{ - /// Append the given document after this document. - #[inline] - pub fn append(self, that: E) -> DocBuilder<'a, D, A> - where - E: Into>, - { - let DocBuilder(allocator, this) = self; - let that = that.into(); - let doc = match (&*this, &*that) { - (Doc::Nil, _) => that, - (_, Doc::Nil) => this, - _ => Doc::Append(allocator.alloc_cow(this), allocator.alloc_cow(that)).into(), - }; - DocBuilder(allocator, doc) - } - - /// Acts as `self` when laid out on multiple lines and acts as `that` when laid out on a single line. - /// - #[inline] - pub fn flat_alt(self, that: E) -> DocBuilder<'a, D, A> - where - E: Into>, - { - let DocBuilder(allocator, this) = self; - let that = that.into(); - DocBuilder( - allocator, - Doc::FlatAlt(allocator.alloc_cow(this.into()), allocator.alloc_cow(that)).into(), - ) - } - - /// Mark this document as a group. - /// - /// Groups are laid out on a single line if possible. Within a group, all basic documents with - /// several possible layouts are assigned the same layout, that is, they are all laid out - /// horizontally and combined into a one single line, or they are each laid out on their own - /// line. - #[inline] - pub fn group(self) -> DocBuilder<'a, D, A> { - let DocBuilder(allocator, this) = self; - DocBuilder(allocator, Doc::Group(allocator.alloc_cow(this)).into()) - } - - /// Increase the indentation level of this document. - #[inline] - pub fn nest(self, offset: isize) -> DocBuilder<'a, D, A> { - if let Doc::Nil = &*self.1 { - return self; - } - if offset == 0 { - return self; - } - let DocBuilder(allocator, this) = self; - DocBuilder( - allocator, - Doc::Nest(offset, allocator.alloc_cow(this)).into(), - ) - } - - #[inline] - pub fn annotate(self, ann: A) -> DocBuilder<'a, D, A> { - let DocBuilder(allocator, this) = self; - DocBuilder( - allocator, - Doc::Annotated(ann, allocator.alloc_cow(this)).into(), - ) - } - - #[inline] - pub fn union(self, other: E) -> DocBuilder<'a, D, A> - where - E: Into>, - { - let DocBuilder(allocator, this) = self; - let other = other.into(); - let doc = Doc::Union(allocator.alloc_cow(this), allocator.alloc_cow(other)); - DocBuilder(allocator, doc.into()) - } - - /// Lays out `self` so with the nesting level set to the current column - /// - /// NOTE: The doc pointer type, `D` may need to be cloned. Consider using cheaply cloneable ptr - /// like `RefDoc` or `RcDoc` - /// - #[inline] - pub fn align(self) -> DocBuilder<'a, D, A> - where - DocBuilder<'a, D, A>: Clone, - { - let allocator = self.0; - allocator.column(move |col| { - let self_ = self.clone(); - allocator - .nesting(move |nest| self_.clone().nest(col as isize - nest as isize).into_doc()) - .into_doc() - }) - } - - /// Lays out `self` with a nesting level set to the current level plus `adjust`. - /// - /// NOTE: The doc pointer type, `D` may need to be cloned. Consider using cheaply cloneable ptr - /// like `RefDoc` or `RcDoc` - /// - #[inline] - pub fn hang(self, adjust: isize) -> DocBuilder<'a, D, A> - where - DocBuilder<'a, D, A>: Clone, - { - self.nest(adjust).align() - } - - /// Indents `self` by `adjust` spaces from the current cursor position - /// - /// NOTE: The doc pointer type, `D` may need to be cloned. Consider using cheaply cloneable ptr - /// like `RefDoc` or `RcDoc` - /// - #[inline] - pub fn indent(self, adjust: usize) -> DocBuilder<'a, D, A> - where - DocBuilder<'a, D, A>: Clone, - { - let spaces = { - use crate::render::SPACES; - let DocBuilder(allocator, _) = self; - let mut doc = allocator.nil(); - let mut remaining = adjust; - while remaining != 0 { - let i = SPACES.len().min(remaining); - remaining -= i; - doc = doc.append(allocator.text(&SPACES[..i])) - } - doc - }; - spaces.append(self).hang(adjust.try_into().unwrap()) - } - - /// Lays out `self` and provides the column width of it available to `f` - /// - /// NOTE: The doc pointer type, `D` may need to be cloned. Consider using cheaply cloneable ptr - /// like `RefDoc` or `RcDoc` - /// - #[inline] - pub fn width(self, f: impl Fn(isize) -> D::Doc + 'a) -> DocBuilder<'a, D, A> - where - BuildDoc<'a, D::Doc, A>: Clone, - { - let DocBuilder(allocator, this) = self; - let f = allocator.alloc_width_fn(f); - allocator.column(move |start| { - let f = f.clone(); - - DocBuilder(allocator, this.clone()) - .append(allocator.column(move |end| f(end as isize - start as isize))) - .into_doc() - }) - } - - /// Puts `self` between `before` and `after` - #[inline] - pub fn enclose(self, before: E, after: F) -> DocBuilder<'a, D, A> - where - E: Into>, - F: Into>, - { - let DocBuilder(allocator, _) = self; - DocBuilder(allocator, before.into()) - .append(self) - .append(after) - } - - pub fn single_quotes(self) -> DocBuilder<'a, D, A> { - self.enclose("'", "'") - } - - pub fn double_quotes(self) -> DocBuilder<'a, D, A> { - self.enclose("\"", "\"") - } - pub fn parens(self) -> DocBuilder<'a, D, A> { - self.enclose("(", ")") - } - - pub fn angles(self) -> DocBuilder<'a, D, A> { - self.enclose("<", ">") - } - pub fn braces(self) -> DocBuilder<'a, D, A> { - self.enclose("{", "}") - } - - pub fn brackets(self) -> DocBuilder<'a, D, A> { - self.enclose("[", "]") - } - - pub fn into_doc(self) -> D::Doc { - match self.1 { - BuildDoc::DocPtr(d) => d, - BuildDoc::Doc(d) => self.0.alloc(d), - } - } - - fn into_plain_doc(self) -> Doc<'a, D::Doc, A> { - match self.1 { - BuildDoc::DocPtr(_) => unreachable!(), - BuildDoc::Doc(d) => d, - } - } -} - -/// Newtype wrapper for `&Doc` -pub struct RefDoc<'a, A = ()>(pub &'a Doc<'a, RefDoc<'a, A>, A>); - -impl Copy for RefDoc<'_, A> {} -impl Clone for RefDoc<'_, A> { - fn clone(&self) -> Self { - *self - } -} - -impl<'a, A> fmt::Debug for RefDoc<'a, A> -where - A: fmt::Debug, -{ - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - self.0.fmt(f) - } -} - -impl<'a, A> Deref for RefDoc<'a, A> { - type Target = Doc<'a, RefDoc<'a, A>, A>; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -trait DropT {} -impl DropT for T {} - -/// An arena which can be used to allocate `Doc` values. -pub struct Arena<'a, A = ()> { - docs: typed_arena::Arena, A>>, - column_fns: typed_arena::Arena>, -} - -impl Default for Arena<'_, A> { - fn default() -> Self { - Self::new() - } -} - -impl<'a, A> Arena<'a, A> { - pub fn new() -> Self { - Arena { - docs: typed_arena::Arena::new(), - column_fns: Default::default(), - } - } - - fn alloc_any(&'a self, f: T) -> &'a T - where - T: 'a, - { - let f = Box::new(f); - let f_ptr = &*f as *const T; - // Until #[may_dangle] https://github.com/rust-lang/rust/issues/34761 is stabilized (or - // equivalent) we need to use unsafe to cast away the lifetime of the function as we do not - // have any other way of asserting that the `typed_arena::Arena` destructor does not touch - // `'a` - // - // Since `'a` is used elsewhere in our `Arena` type we still have all the other lifetime - // checks in place (the other arena stores no `Drop` value which touches `'a` which lets it - // compile) - unsafe { - self.column_fns - .alloc(std::mem::transmute::, Box>(f)); - &*f_ptr - } - } -} - -impl<'a, D, A> DocAllocator<'a, A> for &'a D -where - D: ?Sized + DocAllocator<'a, A>, - A: 'a, -{ - type Doc = D::Doc; - - #[inline] - fn alloc(&'a self, doc: Doc<'a, Self::Doc, A>) -> Self::Doc { - (**self).alloc(doc) - } - - fn alloc_column_fn( - &'a self, - f: impl Fn(usize) -> Self::Doc + 'a, - ) -> >::ColumnFn { - (**self).alloc_column_fn(f) - } - - fn alloc_width_fn( - &'a self, - f: impl Fn(isize) -> Self::Doc + 'a, - ) -> >::WidthFn { - (**self).alloc_width_fn(f) - } -} - -impl<'a, A> DocAllocator<'a, A> for Arena<'a, A> { - type Doc = RefDoc<'a, A>; - - #[inline] - fn alloc(&'a self, doc: Doc<'a, Self::Doc, A>) -> Self::Doc { - RefDoc(match doc { - // Return 'static references for common variants to avoid some allocations - Doc::Nil => &Doc::Nil, - Doc::Line => &Doc::Line, - // line() - Doc::FlatAlt(RefDoc(Doc::Line), RefDoc(Doc::BorrowedText(" "))) => { - &Doc::FlatAlt(RefDoc(&Doc::Line), RefDoc(&Doc::BorrowedText(" "))) - } - // line_() - Doc::FlatAlt(RefDoc(Doc::Line), RefDoc(Doc::Nil)) => { - &Doc::FlatAlt(RefDoc(&Doc::Line), RefDoc(&Doc::Nil)) - } - _ => self.docs.alloc(doc), - }) - } - - fn alloc_column_fn( - &'a self, - f: impl Fn(usize) -> Self::Doc + 'a, - ) -> >::ColumnFn { - self.alloc_any(f) - } - - fn alloc_width_fn( - &'a self, - f: impl Fn(isize) -> Self::Doc + 'a, - ) -> >::WidthFn { - self.alloc_any(f) - } -} diff --git a/version.txt b/version.txt index 3fed276742..7db322a98c 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -(built from source) \ No newline at end of file +built-from-source diff --git a/wasi-libc-sys/.gitignore b/wasi-libc-sys/.gitignore deleted file mode 100644 index 9802e8fdbf..0000000000 --- a/wasi-libc-sys/.gitignore +++ /dev/null @@ -1 +0,0 @@ -src/generated.rs diff --git a/wasi-libc-sys/Cargo.toml b/wasi-libc-sys/Cargo.toml deleted file mode 100644 index 259a38b2cf..0000000000 --- a/wasi-libc-sys/Cargo.toml +++ /dev/null @@ -1,8 +0,0 @@ -[package] -authors = ["The Roc Contributors"] -description = "Rust wrapper for a WebAssembly test platform built on libc" -edition = "2021" -license = "UPL-1.0" -name = "wasi_libc_sys" -repository = "https://github.com/rtfeldman/roc" -version = "0.1.0" diff --git a/wasi-libc-sys/build.rs b/wasi-libc-sys/build.rs deleted file mode 100644 index cb84b4027a..0000000000 --- a/wasi-libc-sys/build.rs +++ /dev/null @@ -1,89 +0,0 @@ -use std::env; -use std::ffi::OsStr; -use std::fs; -use std::path::Path; -use std::process::Command; - -fn main() { - println!("cargo:rerun-if-changed=build.rs"); - println!("cargo:rerun-if-changed=src/dummy.c"); - - let out_dir = env::var("OUT_DIR").unwrap(); - let zig_cache_dir = format!("{out_dir}/zig-cache"); - let out_file = format!("{out_dir}/wasi-libc.a"); - - // Compile a dummy C program with Zig, with our own private cache directory - let zig = zig_executable(); - run_command( - Path::new("."), - &zig, - [ - "build-exe", - "-target", - "wasm32-wasi", - "-lc", - "-O", - "ReleaseSmall", - "--global-cache-dir", - &zig_cache_dir, - "src/dummy.c", - &format!("-femit-bin={}/dummy.wasm", out_dir), - ], - ); - - // Find the libc.a and compiler_rt.o files that Zig wrote (as a side-effect of compiling the dummy program) - let cwd = std::env::current_dir().unwrap(); - let find_libc_output = run_command(&cwd, "find", [&zig_cache_dir, "-name", "libc.a"]); - // If `find` printed multiple results, take the first. - let zig_libc_path = find_libc_output.split('\n').next().unwrap(); - - let find_crt_output = run_command(&cwd, "find", [&zig_cache_dir, "-name", "compiler_rt.o"]); - // If `find` printed multiple results, take the first. - let zig_crt_path = find_crt_output.split('\n').next().unwrap(); - - // Copy libc to where Cargo expects the output of this crate - fs::copy(&zig_libc_path, &out_file).unwrap(); - - // Generate some Rust code to indicate where the file is - let generated_rust = [ - "pub const WASI_LIBC_PATH: &str =", - &format!(" \"{}\";", out_file), - "pub const WASI_COMPILER_RT_PATH: &str =", - &format!(" \"{}\";", zig_crt_path), - "", - ] - .join("\n"); - - fs::write("src/generated.rs", generated_rust).unwrap(); -} - -fn zig_executable() -> String { - match std::env::var("ROC_ZIG") { - Ok(path) => path, - Err(_) => "zig".into(), - } -} - -fn run_command>(path: P, command_str: &str, args: I) -> String -where - I: IntoIterator, - S: AsRef, -{ - let output_result = Command::new(OsStr::new(&command_str)) - .current_dir(path) - .args(args) - .output(); - match output_result { - Ok(output) => match output.status.success() { - true => std::str::from_utf8(&output.stdout).unwrap().to_string(), - false => { - let error_str = match std::str::from_utf8(&output.stderr) { - Ok(stderr) => stderr.to_string(), - Err(_) => format!("Failed to run \"{}\"", command_str), - }; - panic!("{} failed: {}", command_str, error_str); - } - }, - Err(reason) => panic!("{} failed: {}", command_str, reason), - } -} diff --git a/wasi-libc-sys/src/lib.rs b/wasi-libc-sys/src/lib.rs deleted file mode 100644 index bc487bd282..0000000000 --- a/wasi-libc-sys/src/lib.rs +++ /dev/null @@ -1,18 +0,0 @@ -// Rust's libc crate doesn't support Wasm, so we provide an implementation from Zig -// We define Rust signatures here as we need them, rather than trying to cover all of libc -#[cfg(target_family = "wasm")] -use core::ffi::c_void; -#[cfg(target_family = "wasm")] -extern "C" { - pub fn malloc(size: usize) -> *mut c_void; - pub fn free(p: *mut c_void); - pub fn realloc(p: *mut c_void, size: usize) -> *mut c_void; - pub fn memcpy(dst: *mut c_void, src: *mut c_void, n: usize) -> *mut c_void; - pub fn memset(dst: *mut c_void, ch: i32, n: usize) -> *mut c_void; -} - -// Tell users of this crate where to find the Wasm .a file -// If a non-Wasm target is using this crate, we assume it is a build script that wants to emit Wasm -// For Wasm target, it won't ever be used, but we expose it just to keep things simple -mod generated; -pub use generated::{WASI_COMPILER_RT_PATH, WASI_LIBC_PATH}; diff --git a/www/.gitignore b/www/.gitignore deleted file mode 100644 index 378eac25d3..0000000000 --- a/www/.gitignore +++ /dev/null @@ -1 +0,0 @@ -build diff --git a/www/InteractiveExample.roc b/www/InteractiveExample.roc new file mode 100644 index 0000000000..752a7b697f --- /dev/null +++ b/www/InteractiveExample.roc @@ -0,0 +1,172 @@ +interface InteractiveExample + exposes [view] + imports [pf.Html.{ pre, samp, div, text, a, class, p }, pf.Html.Attributes.{ class, role, href, id }] + +Section : [Desc (List Token) Str, Indent, Outdent, Newline] +Token : [ + Kw Str, + Ident Str, + Str Str, + Num Str, + Comment Str, + Literal Str, + ParensAround (List Token), + Lambda (List Str), + StrInterpolation Str Str Str, +] + +view : Html.Node +view = + output = + sectionsToStr [ + Desc [Comment "Click anything here to see an explanation.Tap anything here to\n# see an explanation."] "

Comments in Roc begin with a # and go to the end of the line.

", + Newline, + Desc [Ident "main", Kw "="] "

This defines main, which is where our program will begin.

In Roc, all definitions are constant, so writing main = again in the same scope would give an error.

", + Indent, + Desc [Ident "Path.fromStr", Str "\"url.txt\""] "

This converts the string \"url.txt\" into a Path by passing it to Path.fromStr.

Function arguments are separated with whitespace. Parentheses are only needed in nested function calls.

", + Newline, + Desc [Kw "|>", Ident "storeEmail"] "

The pipe operator (|>) is syntax sugar for passing the previous value to the next function in the \"pipeline.\"

This line takes the value that Path.fromStr \"url.txt\" returns and passes it to storeEmail.

The next |> continues the pipeline.

", + Newline, + Desc [Kw "|>", Ident "Task.onErr", Ident "handleErr"] "

If the task returned by the previous step in the pipeline fails, pass its error to handleErr.

The pipeline essentially does this:

a = Path.fromStr \"url.txt\"\nb = storeEmail a\n\nTask.onErr b handleErr

It creates a Path from a string, stores an email based on that path, and then does error handling.

", + Outdent, + Newline, + Desc [Ident "storeEmail", Kw "=", Lambda ["path"]] "

This defines a function named storeEmail. It takes one argument, named path.

In Roc, functions are ordinary values, so we assign names to them using = like with any other value.

The \\arg1, arg2 -> syntax begins a function, and the part after -> is the function's body.

", + Indent, + Desc [Ident "url", Kw "<-", Ident "File.readUtf8", Ident "path", Kw "|>", Ident "Task.await"] "

This reads the contents of the file (as a UTF-8 string) into url.

The <- does backpassing, which is syntax sugar for defining a function. This line desugars to:

Task.await\n    (File.readUtf8 path)\n    \\url ->

The lines after this one form the body of the \\url -> callback, which runs if the file read succeeds.

", + Newline, + Desc [Ident "user", Kw "<-", Ident "Http.get", Ident "url", Ident "Json.codec", Kw "|>", Ident "Task.await"] "

This fetches the contents of the URL and decodes them as JSON.

If the shape of the JSON isn't compatible with the type of user (based on type inference), this will give a decoding error immediately.

As with all the other lines ending in |> Task.await, if there's an error, nothing else in storeEmail will be run, and handleErr will end up handling the error.

", + Newline, + Desc [Ident "dest", Kw "=", Ident "Path.fromStr", StrInterpolation "\"" "user.name" ".txt\""] "

The \\(user.name) in this string literal will be replaced with the value in name. This is string interpolation.

Note that this line doesn't end with |> Task.await. Earlier lines needed that because they were I/O tasks, but this is a plain old definition, so there's no task to await.

", + Newline, + Desc [Literal "_", Kw "<-", Ident "File.writeUtf8", Ident "dest", Ident "user.email", Kw "|>", Ident "Task.await"] "

This writes user.email to the file, encoded as UTF-8.

We won't be using the output of writeUtf8, so we name it _. The special name _ is for when you don't want to use something.

You can name as many things as you like _, but you can never reference anything named _. So it's only useful for when you don't want to choose a name.

", + Newline, + Desc [Ident "Stdout.line", StrInterpolation "\"Wrote email to " "Path.display dest" "\""] "

This prints what we did to stdout.

Note that this line doesn't end with |> Task.await. That's because, although Stdout.line returns a task, we don't need to await it because nothing happens after it.

", + Outdent, + Newline, + Desc [Ident "handleErr", Kw "=", Lambda ["err"]] "

Like storeEmail, handleErr is also a function.

Although type annotations are optional everywhere in Roc—because the language has 100% type inference—you could add type annotations to main, storeEmail, and handleErr if you wanted to.

", + Indent, + Desc [Kw "when", Ident "err", Kw "is"] "

This will run one of the following lines depending on what value the err argument has.

Each line does a pattern match on the shape of the error to decide whether to run, or to move on and try the next line's pattern.

Roc will do compile-time exhaustiveness checking and tell you if you forgot to handle any error cases here that could have occurred, based on the tasks that were run in storeEmail.

", + Indent, + Desc [Literal "HttpErr", Ident "url", Kw "_", Kw "->", Ident "Stderr.line", StrInterpolation "\"Error fetching URL " "url" "\""] "

This line will run if the Http.get request from earlier encountered an HTTP error.

It handles the error by printing an error message to stderr.

The _ is where more information about the error is stored in the HttpErr. If we wanted to print more detail about what the error was, we'd name that something other than _ and actually use it.

", + Newline, + Desc [Literal "FileReadErr", Ident "path", Kw "_", Kw "->", Ident "Stderr.line", StrInterpolation "\"Error reading from " "Path.display path" "\""] "

This line will run if the File.readUtf8 from earlier encountered a file I/O error.

It handles the error by printing an error message to stderr.

The _ is where more information about the error is stored in the FileReadErr. If we wanted to print more detail about what the error was, we'd name that something other than _ and actually use it.

", + Newline, + Desc [Literal "FileWriteErr", Ident "path", Kw "_", Kw "->", Ident "Stderr.line", StrInterpolation "\"Error writing to " "Path.display path" "\""] "

This line will run if the File.writeUtf8 from earlier encountered a file I/O error.

It handles the error by printing an error message to stderr.

The _ is where more information about the error is stored in the FileWriteErr. If we wanted to print more detail about what the error was, we'd name that something other than _ and actually use it.

", + ] + + div [role "presentation"] [ + pre [class "interactive-example"] [ + samp [] [text output], + ], + p [] [ + text "To get started with the language, try the ", + a [href "/tutorial"] [text "tutorial"], + text "!", + ], + p [id "final-tutorial-link"] [ + a [class "btn-small", href "/tutorial"] [text "Start Tutorial"] + ] + ] + +tokensToStr : List Token -> Str +tokensToStr = \tokens -> + List.walk tokens "" \buf, token -> + bufWithSpace = + if Str.isEmpty buf || token == Literal "," then + buf + else + Str.concat buf " " + + when token is + ParensAround wrapped -> + # Don't put spaces after opening parens or before closing parens + bufWithSpace + |> Str.concat "(" + |> Str.concat (tokensToStr wrapped) + |> Str.concat ")" + + Lambda args -> + # Don't put spaces after opening parens or before closing parens + argsWithCommas = + args + |> List.map \ident -> "\(ident)" + |> Str.joinWith ", " + + bufWithSpace + |> Str.concat "\\" + |> Str.concat argsWithCommas + |> Str.concat " ->" + + Kw str -> + Str.concat bufWithSpace "\(str)" + + Num str | Str str | Literal str -> # We may render these differently in the future + Str.concat bufWithSpace "\(str)" + + Comment str -> + Str.concat bufWithSpace "# \(str)" + + Ident str -> + Str.concat bufWithSpace (identToHtml str) + + StrInterpolation before interp after -> + bufWithSpace + |> Str.concat (if Str.isEmpty before then "" else "\(before)") + |> Str.concat "\\(\(identToHtml interp))" + |> Str.concat (if Str.isEmpty after then "" else "\(after)") + +identToHtml : Str -> Str +identToHtml = \str -> + List.walk (Str.split str ".") "" \accum, ident -> + identHtml = "\(ident)" + + if Str.isEmpty accum then + identHtml + else + "\(accum).\(identHtml)" + +sectionsToStr : List Section -> Str +sectionsToStr = \sections -> + answer = List.walk sections { buf: "", count: 0, indent: 0 } \{ buf, count, indent }, section -> + bufWithSpace = + if Str.isEmpty buf then + buf + else if buf |> Str.endsWith "\n" then + Str.concat buf (Str.repeat " " indent) + else + Str.concat buf " " + + (afterSpaces, nextCount) = + when section is + Newline | Indent | Outdent -> + # Indent and outdent changes happen on the next iteration, + # so we only need a newline for them here. + (Str.concat buf "\n", count) + + Desc tokens str -> + html = radio count (tokensToStr tokens) str + + (Str.concat bufWithSpace html, count + 1) + + nextIndent = + when section is + Indent -> indent + 4 + Outdent -> indent - 4 + Newline | Desc _ _ -> indent + + { + buf: afterSpaces, + count: nextCount, + indent: nextIndent, + } + + answer.buf + +radio : U16, Str, Str -> Str +radio = \index, labelHtml, descHtml -> + # The first radio button should always be checked, and none of the others should be. + checkedHtml = if index == 0 then " checked" else "" + + """ + \(descHtml) + """ diff --git a/www/README.md b/www/README.md index 180931fbcc..016e87cb75 100644 --- a/www/README.md +++ b/www/README.md @@ -1,10 +1,37 @@ +# www.roc-lang.org + +## Prerequisites + +- Linux or MacOS operating system, Windows users can use linux through WSL. +- Install [git](https://chat.openai.com/share/71fb3ae6-80d7-478c-8a27-a36aaa5ba921) +- Install [nix](https://nixos.org/download.html) + +## Building the website from scratch + +```bash +git clone https://github.com/roc-lang/roc.git +cd roc +nix develop +./www/build.sh +# make the roc command available +export PATH="$(pwd)/target/release/:$PATH" +bash build-dev-local.sh +``` + +Open http://0.0.0.0:8080 in your browser. + +## After you've made a change locally + +In the terminal where the web server is running: +1. kill the server with Ctrl+C +2. re-run the build script +3. refresh the page in your browser + +To view the website after you've made a change, execute: +```bash +bash build-dev-local.sh +``` +Open http://0.0.0.0:8080 in your browser. -How to update www.roc-lang.org : -- create a new branch, for example `update-www` based on `www` -- pull `trunk` into `update-www` -- update for example the file `www/public/index.html` -- do a PR against `www` -- check deploy preview -- review and merge diff --git a/www/build-dev-local.sh b/www/build-dev-local.sh new file mode 100755 index 0000000000..37c62eb167 --- /dev/null +++ b/www/build-dev-local.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash + +# Use this script to for testing the WIP site locally without downloading assets every time. + +# NOTE run `bash www/build.sh` to cache local copy of fonts, and repl assets etc + +## Get the directory of the currently executing script +DIR="$(dirname "$0")" + +# Change to that directory +cd "$DIR" || exit + +rm -rf dist/ +cp -r build dist/ +cp -r public/* dist/ +roc run main.roc -- content/ dist/ + +npx http-server dist/ -p 8080 -c-1 --cors diff --git a/www/build.sh b/www/build.sh index 8788c67269..78a2244682 100755 --- a/www/build.sh +++ b/www/build.sh @@ -1,7 +1,14 @@ -#!/bin/bash +#!/usr/bin/env bash +# run from root of repo with `./www/build.sh` +# check www/README.md to run a test server + +# https://vaneyckt.io/posts/safer_bash_scripts_with_set_euxo_pipefail/ set -euxo pipefail +# check if jq is installed +jq --version + # cd into the directory where this script lives. # This allows us to run this script from the root project directory, # which is what Netlify wants to do. @@ -11,32 +18,127 @@ cd $SCRIPT_RELATIVE_DIR rm -rf build/ cp -r public/ build/ +# download the latest code for the examples +echo 'Downloading latest examples...' +curl -fL -o examples-main.zip https://github.com/roc-lang/examples/archive/refs/heads/main.zip +rm -rf examples-main/ +unzip -o examples-main.zip +cp -R examples-main/examples/ content/examples/ + +# relace links in content/examples/index.md to work on the WIP site +perl -pi -e 's|\]\(/|\]\(/examples/|g' content/examples/index.md + +# clean up examples artifacts +rm -rf examples-main examples-main.zip + +# download fonts just-in-time so we don't have to bloat the repo with them. +DESIGN_ASSETS_COMMIT="4d949642ebc56ca455cf270b288382788bce5873" +DESIGN_ASSETS_TARFILE="roc-lang-design-assets-4d94964.tar.gz" +DESIGN_ASSETS_DIR="roc-lang-design-assets-4d94964" + +curl -fLJO https://github.com/roc-lang/design-assets/tarball/$DESIGN_ASSETS_COMMIT +tar -xzf $DESIGN_ASSETS_TARFILE +mv $DESIGN_ASSETS_DIR/fonts build/ +rm -rf $DESIGN_ASSETS_TARFILE $DESIGN_ASSETS_DIR + # grab the source code and copy it to Netlify's server; if it's not there, fail the build. pushd build -wget https://github.com/rtfeldman/elm-css/files/8849069/roc-source-code.zip +curl -fLJO https://github.com/roc-lang/roc/archive/www.tar.gz + +# Download the latest pre-built Web REPL as a zip file. (Build takes longer than Netlify's timeout.) +REPL_TARFILE="roc_repl_wasm.tar.gz" +curl -fLJO https://github.com/roc-lang/roc/releases/download/nightly/$REPL_TARFILE +mkdir repl +tar -xzf $REPL_TARFILE -C repl +rm $REPL_TARFILE +ls -lh repl + popd pushd .. -echo 'Generating docs...' cargo --version -rustc --version - -# removing `-C link-arg=-fuse-ld=lld` from RUSTFLAGS because this causes an error when compiling `roc_repl_wasm` -RUSTFLAGS="-C target-cpu=native" # We set ROC_DOCS_ROOT_DIR=builtins so that links will be generated relative to # "/builtins/" rather than "/" - which is what we want based on how the server # is set up to serve them. export ROC_DOCS_URL_ROOT=/builtins -cargo run --bin roc-docs compiler/builtins/roc/*.roc -mv generated-docs/*.* www/build # move all the .js, .css, etc. files to build/ -mv generated-docs/ www/build/builtins # move all the folders to build/builtins/ +cargo run --release --bin roc-docs crates/compiler/builtins/roc/main.roc -echo "Building Web REPL..." -repl_www/build.sh www/build +mv generated-docs/ www/build/builtins # move everything to build/builtins/ -echo "Asset sizes:" -ls -lh www/build/* +# Manually add this tip to all the builtin docs. +find www/build/builtins -type f -name 'index.html' -exec sed -i 's!!
Tip: Some names differ from other languages.
!' {} \; -popd + +# cleanup files that could have stayed behind if the script failed +rm -rf roc_nightly roc_releases.json + +# we use `! [ -v GITHUB_TOKEN_READ_ONLY ];` to check if we're on a netlify server +if ! [ -v GITHUB_TOKEN_READ_ONLY ]; then + cargo build --release --bin roc + + roc=target/release/roc +else + echo 'Fetching latest roc nightly...' + + # get roc release archive + curl -fOL https://github.com/roc-lang/roc/releases/download/nightly/roc_nightly-linux_x86_64-latest.tar.gz + # extract archive + ls | grep "roc_nightly" | xargs tar -xzvf + # delete archive + ls | grep "roc_nightly.*tar.gz" | xargs rm + # simplify dir name + mv roc_nightly* roc_nightly + + roc='./roc_nightly/roc' +fi + +$roc version + +echo 'Building site markdown content' +$roc run www/main.roc -- www/content/ www/build/ + +# cleanup +rm -rf roc_nightly roc_releases.json + +echo 'Generating CLI example platform docs...' +# Change ROC_DOCS_ROOT_DIR=builtins so that links will be generated relative to +# "/packages/basic-cli/" rather than "/builtins/" +export ROC_DOCS_URL_ROOT=/packages/basic-cli + +rm -rf ./downloaded-basic-cli + +git clone --depth 1 https://github.com/roc-lang/basic-cli.git downloaded-basic-cli + +cargo run --bin roc-docs downloaded-basic-cli/src/main.roc + +rm -rf ./downloaded-basic-cli + +BASIC_CLI_PACKAGE_DIR="www/build/packages/basic-cli" +mkdir -p $BASIC_CLI_PACKAGE_DIR +mv generated-docs/* $BASIC_CLI_PACKAGE_DIR # move all the folders to build/packages/basic-cli + +# set up docs for older basic-cli versions +# we need a github token +if [ -v GITHUB_TOKEN_READ_ONLY ]; then + + curl -v -H "Authorization: $GITHUB_TOKEN_READ_ONLY" -fL -o basic_cli_releases.json "https://api.github.com/repos/roc-lang/basic-cli/releases" + + DOCS_LINKS=$(cat basic_cli_releases.json | jq -r '.[] | .assets[] | select(.name=="docs.tar.gz") | .browser_download_url') + + rm basic_cli_releases.json + + VERSION_NUMBERS=$(echo "$DOCS_LINKS" | grep -oP '(?<=/download/)[^/]+(?=/docs.tar.gz)') + + while read -r VERSION_NR; do + echo $VERSION_NR + BASIC_CLI_DIR=$BASIC_CLI_PACKAGE_DIR/$VERSION_NR + mkdir -p $BASIC_CLI_DIR + curl -fL --output $BASIC_CLI_DIR/docs.tar.gz https://github.com/roc-lang/basic-cli/releases/download/$VERSION_NR/docs.tar.gz + tar -xf $BASIC_CLI_DIR/docs.tar.gz -C $BASIC_CLI_DIR/ + rm $BASIC_CLI_DIR/docs.tar.gz + mv $BASIC_CLI_DIR/generated-docs/* $BASIC_CLI_DIR + rm -rf $BASIC_CLI_DIR/generated-docs + done <<< "$VERSION_NUMBERS" +fi diff --git a/www/content/abilities.md b/www/content/abilities.md new file mode 100644 index 0000000000..06a8356a65 --- /dev/null +++ b/www/content/abilities.md @@ -0,0 +1,286 @@ + +# Abilities + +An Ability defines a set of functions that can be implemented by different types. + +Abilities are used to constrain the types of function arguments to only those which implement the required functions. + +The function `toJson` below is an example which uses the `Encoding` Ability. + +```roc +toJson : a -> List U8 where a implements Encoding +toJson = \val -> + val |> Encode.toBytes JSON.encoder +``` + +By specifying the type variable `a` implements the `Encoding` Ability, this function can make use of `Encode.toBytes` and `JSON.encoder` to serialise `val`, without knowing its specific type. + +All types which implement the `Encoding` Ability can therefore use the `Encode.toBytes` (and also `Encode.append`) functions to conveniently serialise values to bytes. + +- [Builtins](#builtins) + - [`Eq` Ability](#eq-ability) + - [`Hash` Ability](#hash-ability) + - [`Sort` Ability](#sort-ability) + - [`Encoding` Ability](#encoding-ability) + - [`Decoding` Ability](#decoding-ability) + - [`Inspect` Ability](#inspect-ability) +- [Opaque Types](#opaque-types) + - [Derived Implementions](#derived-implementions) + - [Custom Implementations ](#custom-implementations) +- [Advanced Topic: Defining a new Ability](#defining-a-new-ability) + +## [Builtins](#builtins) {#builtins} + +Roc's Builtin types such as numbers, records, and tags, are automatically derived for the builtin Abilities. This means that you can use these Abilities without needing to provide a custom implementation. + +### [`Eq` Ability](#eq-ability) {#eq-ability} + +The `Eq` Ability defines the `isEq` function, which can be used to compare two values for structural equality. The infix operator `==` can be used as shorthand for `isEq`. + +`Eq` is not derived for `F32` or `F64` as these types do not support structural equality. If you need to compare floating point numbers, you must provide your own function for comparison. + +**Example** showing the use of `isEq` and `==` to compare two values. + +```roc +Colors : [Red, Green, Blue] + +red = Red +blue = Blue + +expect isEq red Red # true +expect red == blue # false +``` + +**Definition** of the `Eq` Ability. + +```roc +# Bool.roc +Eq implements + isEq : a, a -> Bool where a implements Eq +``` + +**Structural equality** is defined as follows: +1. Tags are equal if their name and also contents are equal. +2. Records are equal if their fields are equal. +3. The collections `Str`, `List`, `Dict`, and `Set` are equal iff they are the same length and their elements are equal. +4. `Num` values are equal if their numbers are equal. However, if both inputs are *NaN* then `isEq` returns `Bool.false`. Refer to `Num.isNaN` for more detail. +5. Functions cannot be compared for structural equality, therefore Roc cannot derive `isEq` for types that contain functions. + +### [`Hash` Ability](#hash-ability) {#hash-ability} + +The `Hash` Ability defines the `hash` function, which can be used to hash a value. The `hash` function takes a `Hasher` as an argument, which is used to compute the hash. + +```roc +# Hash.roc +Hash implements + hash : hasher, a -> hasher where a implements Hash, hasher implements Hasher +``` + +### [`Sort` Ability](#sort-ability) {#sort-ability} + +**Implementation Status** - Design Proposal, implementation has not yet started. See [zulip discussion thread](https://roc.zulipchat.com/#narrow/stream/304641-ideas/topic/ordering.2Fsorting.20ability/near/395539545) for more information. If you would like to help implement this, please let us know. + +The `Sort` Ability defines the `compare` function, which can be used to compare two values for ordering. + +`Sort` is not derived for `Str` as working with utf-8 strings which is a variable length encoding scheme is complex and is achieved through a dedicated library such as [roc-lang/unicode](https://github.com/roc-lang/unicode). + +**Proposed Definition** of the `Sort` Ability. + +```roc +# Sort.roc +Sort implements + compare : a, a -> [LessThan, Equals, GreaterThan] where a implements Sort +``` + +### [`Encoding` Ability](#encoding-ability) {#encoding-ability} + +The `Encoding` Ability defines `toEncoder` which can be used with an Encoder to serialise value from Roc to bytes using the `Encoding.toBytes` and `Encoding.append` functions. + +Functions are not serialisable, therefore Roc does not derive `Encoding` for types that contain functions. + +Encoding for `Dict` values **has not been implemened**, see [#5294](https://github.com/roc-lang/roc/issues/5294) for more details. If you would like to help implement this, please let us know. + +**Example** showing the use of `Encoding.toBytes` to serialise a Roc `List (Str, U32)` to a [JSON](https://www.json.org/json-en.html) encoded string. + +```roc +bytes : List U8 +bytes = "[[\"Apples\",10],[\"Bananas\",12],[\"Oranges\",5]]" |> Str.toUtf8 + +fruitBasket : List (Str, U32) +fruitBasket = [ + ("Apples", 10), + ("Bananas", 12), + ("Oranges", 5) +] + +expect Encode.toBytes fruitBasket json == bytes # true +``` + +**Definition** of the `Encoding` Ability. + +```roc +# Encode.roc +Encoding implements + toEncoder : val -> Encoder fmt where val implements Encoding, fmt implements EncoderFormatting +``` + +### [`Decoding` Ability](#decoding-ability) {#decoding-ability} + +The `Decoding` Ability defines `decoder` which can be used with an Decoder to de-serialise from bytes to Roc values using the `Decoding.fromBytesPartial` and `Decoding.fromBytes` functions. + +Decoding for `Dict` values **has not been implemened**, see [#5294](https://github.com/roc-lang/roc/issues/5294) for more details. If you would like to help implement this, please let us know. + +**Example** showing the use of `Decoding.fromBytes` to decode a Roc `List (U32, Str)` from a [JSON](https://www.json.org/json-en.html) encoded string. + +```roc +bytes : List U8 +bytes = + """ + [ + [ 10, \"Green Bottles\" ], + [ 12, \"Buckle My Shoe\" ], + [ 5, \"Little Ducks\" ] + ] + """ + |> Str.toUtf8 + +result : Result (List (U32, Str)) _ +result = Decode.fromBytes bytes json + +expect result == Ok [(10, "Green Bottles"), (12, "Buckle My Shoe"), (5, "Little Ducks")] # true +``` + +**Definition** of the `Decoding` Ability. + +```roc +# Decode.roc +Decoding implements + decoder : Decoder val fmt where val implements Decoding, fmt implements DecoderFormatting +``` + +### [`Inspect` Ability](#inspect-ability) {#inspect-ability} + +**Implementation Status** - Accepted Proposal, implementation in-progress. See [#5775](https://github.com/roc-lang/roc/pull/5775) for progress on auto-deriving `Inspect` for builtin types. + +The `Inspect` Ability defines the `toInspector` function which can be used with an Inspector to inspect Roc values using the `Inspect.inspect` function. + +Example Inspectors: +- A [LogFormatter](https://github.com/roc-lang/roc/blob/main/examples/LogFormatter.roc) which creates a string representation of Roc values, for use e.g. debug printing to the console. +- A [GuiFormatter](https://github.com/roc-lang/roc/blob/main/examples/GuiFormatter.roc) which creates a GUI representation of Roc values for use e.g. debug visualization in a graphical application. + +**Definition** of the `Inspect` Ability. + +```roc +# Inspect.roc +Inspect implements + toInspector : val -> Inspector f where val implements Inspect, f implements InspectFormatter +``` + +## [Opaque Types](#opaque-types) {#opaque-types} + +Opaque Types are used to hide implementation details of a type. Modules export functions to define a *public* API for working with a type. + +By default abilities are not derieved for Opaque Types. However, [Derived](#derived-implementions) and [Custom](#custom-implementations) implementations are two ways to work with abilities for your Opaque Types. + +### [Derived Implementions](#derived-implementions) {#derived-implementions} + +Abilities can be automatically derived for Opaque Types where the type is an alias for a builtin, or it is composed of other types which also implement that ability. + +For example you can automatically derive the `Eq` and `Hash` abilities using `implements [ Eq, Hash ]`. + +**Example** showing how to automatically derive the `Eq` and `Hash` abilities for an Opaque Type. + +```roc +StatsDB := Dict Str { score : Dec, average : Dec } implements [ Eq, Hash ] + +add : StatsDB, Str, { score : Dec, average : Dec } -> StatsDB +add = \@StatsDB db, name, stats -> db |> Dict.insert name stats |> @StatsDB + +expect + db1 = Dict.empty {} |> @StatsDB |> add "John" { score: 10, average: 10 } + db2 = Dict.empty {} |> @StatsDB |> add "John" { score: 10, average: 10 } + + db1 == db2 # true +``` + +### [Custom Implementations](#custom-implementations) {#custom-implementations} + +You can provide a custom implementation for an ability. This may be useful if a type is composed of other types which do not implement an ability, or if you would like to override the default behaviour. + +**Example** showing how to provide a custom implementation for the `Eq` ability. + +```roc +Color := [ + RGBAu8 U8 U8 U8 U8, + RGBAf32 F32 F32 F32 F32, + ] + implements [ + Eq { isEq: colorEquality }, + ] + +# Note that Eq is not available for an F32, hence we provide a custom implementation here. +colorEquality : Color, Color -> Bool +colorEquality = \a, b -> colorToU8 a == colorToU8 b + +colorToU8 : Color -> (U8, U8, U8, U8) +colorToU8 = \@Color c-> + when c is + RGBAu8 r g b a -> (r, g, b, a) + RGBAf32 r g b a -> (f32toU8 r, f32toU8 g, f32toU8 b, f32toU8 a) + +f32toU8 : F32 -> U8 +f32toU8 = \f -> + Num.floor (f * 255.0) + +fromU8 : U8, U8, U8, U8 -> Color +fromU8 = \r, g, b, a -> @Color (RGBAu8 r g b a) + +fromI16 : I16, I16, I16, I16 -> Result Color [OutOfRange] +fromI16 = \r, g, b, a -> + if r < 0 || r > 255 || g < 0 || g > 255 || b < 0 || b > 255 || a < 0 || a > 255 then + Err OutOfRange + else + Ok (@Color (RGBAu8 (Num.toU8 r) (Num.toU8 g) (Num.toU8 b) (Num.toU8 a))) + +fromF32 : F32, F32, F32, F32 -> Result Color [OutOfRange] +fromF32 = \r, g, b, a -> + if r < 0.0 || r > 1.0 || g < 0.0 || g > 1.0 || b < 0.0 || b > 1.0 || a < 0.0 || a > 1.0 then + Err OutOfRange + else + Ok (@Color (RGBAf32 r g b a)) +``` + +## [Advanced Topic: Defining a new Ability](#defining-a-new-ability) {#defining-a-new-ability} + +It is possible to define a new Ability in addition to those provided in builtins. This should be avoided if possible and only used in rare circumstances by package authors. + +**Example** showing how to define a new Ability. + +```roc +CustomInspect implements + inspectMe : val -> Str where val implements CustomInspect + +inspect : val -> Str where val implements CustomInspect +inspect = \val -> inspectMe val + +Color := [Red, Green, Blue] + implements [ + Eq, + CustomInspect { + inspectMe: inspectColor, + }, + ] + +inspectColor : Color -> Str +inspectColor = \@Color color -> + when color is + Red -> "Red" + Green -> "Green" + Blue -> "Blue" + +expect + [@Color Red, @Color Green, @Color Blue] + |> List.map inspect + |> Str.joinWith "," + |> Bool.isEq "Red,Green,Blue" +``` diff --git a/www/content/bdfn.md b/www/content/bdfn.md new file mode 100644 index 0000000000..0a0e4dc86b --- /dev/null +++ b/www/content/bdfn.md @@ -0,0 +1,13 @@ +# BDFN + +Hi, I'm [Richard](https://github.com/rtfeldman)! I created Roc in 2018 and am currently its BDFN. + +BDFN stands for Benevolent Dictator For Now. There's a whole group of wonderful people collaborating to advance Roc, and we discuss major decisions together before committing to a particular direction. That said, once we reach the point in our discussions where a decision has to be made, I—as BDFN—have the final say...for now. + +The difference between BDFN and [Benevolent Dictator For Life](https://en.wikipedia.org/wiki/Benevolent_dictator_for_life) is that I plan to transition away from being in charge of Roc with plenty of time left in my life to watch others take the helm and see how they run things. My last act as BDFN should be to hand over decision-making responsibility to a new system, whatever that ends up being. + +Something that needs figuring out between now and then is how to keep the language from growing excessively complex over time. Many committee-driven languages accumulate new features at a rate that would be bad for Roc's long-term health, and I hope we can find a system which minimizes the erosion of simplicity that seems to happen by default. + +(This would be easy to solve if zero happened to be the optimal number of new features to add over time, but unfortunately the programming landscape will change around the language, so remaining static is unlikely to be best. As such, the goal becomes finding a system which keeps the number of new features low without artificially forcing it to zero.) + +I don't have any plans for when the transition away from BDFN will happen, other than that I expect it to be well after a 1.0 release of the language! diff --git a/www/content/community.md b/www/content/community.md new file mode 100644 index 0000000000..70a68b1843 --- /dev/null +++ b/www/content/community.md @@ -0,0 +1,38 @@ +# Community + +[Roc Zulip Chat](https://roc.zulipchat.com/) is the most active community gathering place. +We love it when new people stop by and introduce themselves in [`#introductions`](https://roc.zulipchat.com/#narrow/stream/387892-introductions) so others can welcome you to the community! + +The Roc Online Meetup meets about once per month. The organizer posts a [when2meet](https://when2meet.com) for the next month's meetup in [`#gatherings` on Zulip](https://roc.zulipchat.com/#narrow/stream/303057-gatherings), so we can find the best time that works for everyone +who's interested in attending. The organizer then posts a finalized time and a link to where people can join. They usually last between 1-2 hours, and consist of some casual show-and-tell presentations followed by conversation. They're fun! + +We have not had a Roc conference yet, although there have been a few Roc talks given at conferences, +and a few times when Roc community members have met up in person. + +### [Code of Conduct](#code-of-conduct) {#code-of-conduct} + +The Roc project enforces [a code of conduct](https://github.com/roc-lang/roc/blob/main/CODE_OF_CONDUCT.md). Please read and follow it! + +### [Contributing](#contributing) {#contributing} + +All the source code to the Roc project is on GitHub in the [roc-lang](https://github.com/roc-lang) organization. The compiler and CLI are at [roc-lang/roc](https://github.com/roc-lang/roc), and there's a tag for [Good First Issues](https://github.com/roc-lang/roc/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22). + +The project has [many contributors](https://github.com/roc-lang/roc/graphs/contributors), and we like to invest in helping new contributors get started. If you'd like to become a new contributor (even if you don't know what you'd like that contribution to be yet), just make a post in [the "new contributors" topic](https://roc.zulipchat.com/#narrow/stream/316715-contributing/topic/new.20contributors) and say hello! + +### [Ideas, proposals, and feature requests](#ideas) {#ideas} + +Roc doesn't have a formal process for managing design proposals. + +At the current size of the project, having a formal process (like a [RFC](https://en.wikipedia.org/wiki/Change_request) system) would be more heavyweight than it's worth. Fow now, the guiding principle is that as a community we should all be friendly, supportive, and openly share and discuss ideas without the expectation that they will necessarily be accepted or not. We follow a [BDFN](/bdfn) leadership model today, although this is planned to change someday. + +There are three loose stages that a design proposal can go through in Roc: idea, proposal, and implementation. These are guidelines, not strict requirements; their goal is to prevent the outcome where someone does a lot of implementation work only to have their contribution never make it into the code base because it's determined that we wanted to go in a different design direction. Confirming ahead of time that the design direction is desired can prevent implementing something that ends up not being used. + +In the idea stage, people are encouraged to describe their idea and explore the problem, potential solutions, and tradeoffs. It's a good idea to share the idea in [`#ideas` on Zulip](https://roc.zulipchat.com/#narrow/stream/304641-ideas). There's no prerequisite for sharing an idea (it's only an idea, after all!) and likewise there's also no obligation for any contributor to necessarily act on it. + +If the idea seems promising and worth developing further (as confirmed by a Roc contributor with expertise in the relevant area—not necessarily the [BDFN](/bdfn)), usually the next step is to get more specific with a written proposal that details all the necessary information about what the change would involve. + +A written proposal isn't always necessary (for example, it may be deemed a simple and uncontroversial enough change that we're comfortable proceeding straight to implementation), but since writing proposals can be time-consuming, it's definitely a good idea to get confirmation at the idea stage from an experienced contributor before taking the time to write one up. + +There's no guarantee that a proposal will be accepted, and even if it is, there's no guarantee that something won't come up during implementation that changes the tradeoffs and means it doesn't end up making it into the language after all. But if it is accepted (again, doesn't have to be by the [BDFN](/bdfn) - although the BDFN does have final say if there's a disagreement at some point), that means a [Pull Request](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request) should be reviewed, and the general understanding is that the PR will be accepted unless some problem is discovered during implementation that wasn't anticipated at the proposal stage. + +This is the process we're using for now, but of course it may change in the future! diff --git a/www/content/docs.md b/www/content/docs.md new file mode 100644 index 0000000000..97201f6a2b --- /dev/null +++ b/www/content/docs.md @@ -0,0 +1,20 @@ + +# Documentation + +- [builtins](/builtins) - docs for modules built into the language—`Str`, `Num`, etc. +- [basic-webserver](https://roc-lang.github.io/basic-webserver/) - a platform for making Web servers ([source code](https://github.com/roc-lang/basic-webserver)) +- [basic-cli](/packages/basic-cli) - a platform for making command-line interfaces ([source code](https://github.com/roc-lang/basic-cli)) +- [plans](/plans) - current plans for future changes to the language + +In the future, a language reference will be on this page too. + +## [Guides](#guides) {#guides} + +- [Frequently Asked Questions](https://github.com/roc-lang/roc/blob/main/FAQ.md) +- [Roc for Elm Programmers](https://github.com/roc-lang/roc/blob/main/roc-for-elm-programmers.md) +- [Tutorial](/tutorial) + +## [Language Reference](#language-reference) {#language-reference} + +- [Platforms & Applications](/platforms) +- [Abilities](/abilities) diff --git a/www/content/donate.md b/www/content/donate.md new file mode 100644 index 0000000000..7350a79eed --- /dev/null +++ b/www/content/donate.md @@ -0,0 +1,13 @@ +# Donate + +We are a small group trying to do big things, and we are grateful for every donation! Right now we are trying to raise $4,000 USD/month in donations to fund one longtime Roc contributor to continue his work on Roc full-time. + +You can donate to Roc's development using: +- [GitHub Sponsors](https://github.com/sponsors/roc-lang) +- [Liberapay](https://liberapay.com/roc_lang) + +All donations go through the [Roc Programming Language Foundation](https://foundation.roc-lang.org/), a registered US 501(c)(3) nonprofit organization, which means these donations are tax-exempt in the US. + +It also means that the foundation's tax filings are a [matter of public record](https://en.wikipedia.org/wiki/501(c)(3)_organization#Transparency), so you have real transparency into how your donations are advancing Roc! + +If you would like your organization to become an official sponsor of Roc's development, please [DM Richard Feldman on Zulip](https://roc.zulipchat.com/#narrow/pm-with/281383-user281383). We'd love to talk to you! diff --git a/www/content/fast.md b/www/content/fast.md new file mode 100644 index 0000000000..b92ba3ad3d --- /dev/null +++ b/www/content/fast.md @@ -0,0 +1,84 @@ +# Fast + +Roc code is designed to build fast and run fast...but what does "fast" mean here? And how close is Roc's current implementation to realizing that goal? + +## [Fast programs](#fast-programs) {#fast-programs} + +What "fast" means in embedded systems is different from what it means in games, which in turn is different from what it means on the Web. To better understand Roc's performance capabilities, let's look at the upper bound of how fast optimized Roc programs are capable of running, and the lower bound of what types of languages Roc should generally outperform. + +### [Limiting factors: memory management and async I/O](#limiting-factors) {#limiting-factors} + +Roc is a [memory-safe](https://en.wikipedia.org/wiki/Memory_safety) language with [automatic memory management](https://en.wikipedia.org/wiki/Garbage_collection_(computer_science)#Reference_counting). Automatic memory management has some unavoidable runtime overhead, and memory safety based on static analysis rules out certain performance optimizations—which is why [unsafe Rust](https://doc.rust-lang.org/book/ch19-01-unsafe-rust.html) can outperform safe Rust. This gives Roc a lower performance ceiling than languages which support memory unsafety and manual memory management, such as C, C++, Zig, and Rust. + +Another part of Roc's design is that all I/O operations are done using a lightweight state machine so that they can be asynchronous. This has potential performance benefits compared to synchronous I/O, but it also has some unavoidable overhead. + +### [Generally faster than dynamic or gradual languages](#faster-than) {#faster-than} + +As a general rule, Roc programs should have almost strictly less runtime overhead than equivalent programs written in languages with dynamic types and automatic memory management. This doesn't mean all Roc programs will outperform all programs in these languages, but it does mean Roc should have a higher ceiling on what performance is achievable. + +This is because dynamic typing (and gradual typing) requires tracking types at runtime, which has overhead. Roc tracks types only at compile time, and tends to have [minimal (often zero) runtime overhead](https://vimeo.com/653510682) for language constructs compared to the top performers in industry. For example, Roc's generics, records, functions, numbers, and tag unions have no more runtime overhead than they would in their Rust or C++ equivalents. + +When [benchmarking compiled Roc programs](https://www.youtube.com/watch?v=vzfy4EKwG_Y), the goal is to have them normally outperform the fastest mainstream garbage-collected languages (for example, Go, C#, Java, and JavaScript), but it's a non-goal to outperform languages that support memory unsafety or manual memory management. There will always be some individual benchmarks where mainstream garbage-collected languages outperform Roc, but the goal is for these to be uncommon rather than the norm. + +### [Domain-specific memory management](#domain-specific-memory-management) {#domain-specific-memory-management} + +Roc's ["platforms and applications" design](/platforms) means its automatic memory management can take advantage of domain-specific properties to improve performance. + +For example, if you build an application on the [`basic-cli` platform](https://github.com/roc-lang/basic-cli) compared to the [`basic-webserver` platform](https://github.com/roc-lang/basic-webserver), each of those platforms may use a different memory management strategy under the hood that's tailored to their respective use cases. Your application's performance can benefit from this, even though building on either of those platforms feels like using ordinary automatic memory management. + +This is because Roc [platforms](/platforms) get to determine how memory gets allocated and deallocated in applications built on them. ([`basic-cli`](https://github.com/roc-lang/basic-cli) and [`basic-webserver`](https://github.com/roc-lang/basic-webserver) are examples of platforms, but anyone can build their own platform.) Here are some examples of how platforms can use this to improve application performance: + +- A platform for noninteractive command-line scripts can skip deallocations altogether, since any allocated memory will be cheaply reclaimed by the operating system anyway once the script exits. (This strategy is domain-specific; it would not work well for a long-running, interactive program!) +- A platform for Web servers can put all allocations for each request into a particular [region of memory](https://en.wikipedia.org/wiki/Region-based_memory_management) (this is known as "arena allocation" or "bump allocation") and then deallocate the entire region in one cheap operation after the response has been sent. This would essentially drop memory reclamation times to zero. (This strategy relies on Web servers' request/response architecture, and wouldn't make sense in other use cases. [`nea`](https://github.com/tweedegolf/nea) is a platform in early development that is working towards implementing this.) +- A platform for applications that have very long-lived state could implement [meshing compaction](https://youtu.be/c1UBJbfR-H0?si=D9Gp0cdpjZ_Is5v8) to decrease memory fragmentation. (Compaction would probably be a net negative for performance in the previous two examples.) + +[This talk](https://www.youtube.com/watch?v=cpQwtwVKAfU&t=75s) has more information about platforms and applications, including demos and examples of other benefits they unlock besides performance. + +### [Current progress](#current-progress) {#current-progress} + +Roc's "platforms and applications" design already works, including the domain-specific memory management. Most of Roc's data structures are already close to their theoretical limit in terms of performance, at least without changing their behavior or introducing memory unsafety. [This talk](https://vimeo.com/653510682) explains how they're implemented under the hood. + +That said, [the current implementation](https://ayazhafiz.com/articles/23/a-lambda-calculus-with-coroutines-and-heapless-closures) of [defunctionalization](https://blog.sigplan.org/2019/12/30/defunctionalization-everybody-does-it-nobody-talks-about-it/) (based on [this paper](https://dl.acm.org/doi/pdf/10.1145/3591260))—which unlocks stack-allocated closures, among other optimizations—has [significant known gaps](https://github.com/roc-lang/roc/issues/5969), and has a ways to go before it works across the board. (If you're interested in getting involved in that implementation, [we'd love to hear from you](https://roc.zulipchat.com)!) + +Current optimizations that are completely implemented (give or take the occasional bug) include [LLVM](https://llvm.org/), [Morphic](https://www.youtube.com/watch?v=F3z39M0gdJU&t=3547s), [Perceus](https://www.microsoft.com/en-us/research/uploads/prod/2020/11/perceus-tr-v1.pdf), and tail recursion optimization (including [modulo cons](https://en.wikipedia.org/wiki/Tail_call#Tail_recursion_modulo_cons)). Promising examples of potential future optimizations include closure-aware [inlining](https://en.wikipedia.org/wiki/Inline_expansion), [automatic deforestation](https://www.cs.tufts.edu/~nr/cs257/archive/duncan-coutts/stream-fusion.pdf), and full [compile-time evaluation](https://en.wikipedia.org/wiki/Constant_folding) of top-level declarations. + +We're also interested in improving the performance of the Morphic alias analysis pass itself; if contributing to that project (or any other optimization project) interests you, please [let us know in the `#contributing` channel](https://roc.zulipchat.com/#narrow/stream/316715-contributing)! + +## Fast Feedback Loops + +One of Roc's goals is to provide fast feedback loops by making builds normally feel "instant" except on truly enormous projects. + +It's a concrete goal to have them almost always complete in under 1 second on the median computer being used to write Roc (assuming that system is not bogged down with other programs using up its resources), and ideally under the threshold at which humans typically find latency perceptible (around 100 milliseconds). In the future, hot code loading can make the feedback loop even faster, by letting you see changes without having to restart your program. + +Note that although having fast "clean" builds (without the benefit of caching) is a goal, the "normally feels instant" goal refers to builds where caching was involved. After all, the main downside of build latency is that it comes up over and over in a feedback loop; a fast initial "clean" build is valuable too, but it comes up rarely by comparison. + +### Current Progress + +`roc check` checks your code for errors (such as invalid syntax, naming errors, and type mismatches) and reports problems it finds. On typical development laptops, this usually takes well under 1 second for small projects (for very small projects, it can be around 10 milliseconds on some popular machines). To date, the largest known Roc projects have lines of code numbering in the low thousands, so there's no data yet on `roc check` times for larger projects. + +`roc build` does everything `roc check` does, but it additionally builds a runnable binary of your program. You may notice that `roc build` takes much longer to complete! This is because +of two projects that are underway but not completed yet: +- *Development backend* refers to generating machine code directly instead of asking [LLVM](https://llvm.org/) to generate it. LLVM is great at generating optimized machine code, but it takes a long time to generate it—even if you turn off all the optimizations (and `roc` only has LLVM perform optimizations when the `--optimize` flag is set). The dev backend is currently implemented for WebAssembly, which you can see in the [Web REPL](https://www.roc-lang.org/repl), and in `roc repl` except on Windows. Work is underway to implement it for `roc build` and `roc run`, as well as macOS, Windows, and the ARM versions of all of these. +- *Surgical linking* refers to a fast way of combining the platform and application into one binary. Today, this works on x64 Linux, x64 Windows, and WebAssembly. `roc build` on macOS is noticeably slower because it falls back on non-surgical linking. + +Here's a table summarizing the current progress: + +Target | Dev backend | Surgical linking | +------------|-------------|-------------------| +WebAssembly | yes | yes | +macOS ARM | repl only | | +macOS x64 | repl only | | +Linux ARM | repl only | | +Linux x64 | repl only | yes | +Windows x64 | | yes | +Windows ARM | | | + +Once we have full coverage, `roc build` (and `roc run` and `roc test`, which also perform builds) should take only a bit longer than `roc check`. + +The next major performance improvement will be caching. Currently, `roc` always builds everything from scratch. Most of the time, it could benefit from caching some of the work it had done in a previous build, but today it doesn't do that. There's a design for the caching system, but essentially none of the implementation has started yet. Hot code loading will be the next major improvement after caching, but it requires full dev backend coverage, and does not have a concrete design yet. + +## Friendly + +In addition to being fast, Roc also aims to be a friendly programming language. + +[What does _friendly_ mean here?](/friendly) diff --git a/www/content/friendly.md b/www/content/friendly.md new file mode 100644 index 0000000000..38b7431e8d --- /dev/null +++ b/www/content/friendly.md @@ -0,0 +1,100 @@ +# Friendly + +Besides having a [friendly community](/community), Roc also prioritizes being a user-friendly language. This impacts the syntax, semantics, and tools Roc ships with. + +## [Syntax and Formatter](#syntax) {#syntax} + +Roc's syntax isn't trivial, but there also isn't much of it to learn. It's designed to be uncluttered and unambiguous. A goal is that you can normally look at a piece of code and quickly get an accurate mental model of what it means, without having to think through several layers of indirection. Here are some examples: + +- `user.email` always accesses the `email` field of a record named `user`. (Roc has no inheritance, subclassing, or proxying.) +- `Email.isValid` always refers to something named `isValid` exported by a module named `Email`. (Module names are always capitalized, and variables/constants never are.) Modules are always defined statically and can't be modified at runtime; there's no [monkey patching](https://en.wikipedia.org/wiki/Monkey_patch) to consider either. +- `x = doSomething y z` always declares a new constant `x` (Roc has [no mutable variables, reassignment, or shadowing](/functional)) to be whatever the `doSomething` function returns when passed the arguments `y` and `z`. (Function calls in Roc don't need parentheses or commas.) +- `"Name: \(Str.trim name)"` uses *string interpolation* syntax: a backslash inside a string literal, followed by an expression in parentheses. This code is the same as combining the string `"Name: "` with the string returned by the function call `Str.trim name`.

Because Roc's string interpolation syntax begins with a backslash (just like other backlash-escapes such as `\n` and `\"`), you can always tell which parts of a string involve special handling: the parts that begin with backslashes. Everything else works as normal. + +Roc also ships with a source code formatter that helps you maintain a consistent style with little effort. The `roc format` command neatly formats your source code according to a common style, and it's designed with the time-saving feature of having no configuration options. This feature saves teams all the time they would otherwise spend debating which stylistic tweaks to settle on! + +## [Helpful compiler](#helpful-compiler) {#helpful-compiler} + +Roc's compiler is designed to help you out. It does complete type inference across all your code, and the type system is [sound](https://en.wikipedia.org/wiki/Type_safety). This means you'll never get a runtime type mismatch if everything type-checked (including null exceptions; Roc doesn't have the [billion-dollar mistake](https://en.wikipedia.org/wiki/Null_pointer#History)), and you also don't have to write any type annotations—the compiler can infer all the types in your program. + +If there's a problem at compile time, the compiler is designed to report it in a helpful way. Here's an example: + +
── TYPE MISMATCH ─────── /home/my-roc-project/main.roc ─
+
+Something is off with the then branch of this if:
+
+4│      someInt : I64
+5│      someInt =
+6│          if someDecimal > 0 then
+7│              someDecimal + 1
+                ^^^^^^^^^^^^^^^
+
+This branch is a fraction of type:
+
+    Dec
+
+But the type annotation on `someInt` says it should be:
+
+    I64
+
+Tip: You can convert between integers and fractions
+using functions like `Num.toFrac` and `Num.round`.
+ +If you like, you can run a program that has compile-time errors like this. (If the program reaches the error at runtime, it will crash.) + +This lets you do things like trying out code that's only partially finished, or running tests for one part of your code base while other parts have compile errors. (Note that this feature is only partially completed, and often errors out; it has a ways to go before it works for all compile errors!) + +## [Serialization inference](#serialization-inference) {#serialization-inference} + +When dealing with [serialized data](https://en.wikipedia.org/wiki/Serialization), an important question is how and when that data will be decoded from a binary format (such as network packets or bytes on disk) into your program's data structures in memory. + +A technique used in some popular languages today is to decode without validation. For example, some languages parse [JSON](https://www.json.org) using a function whose return type is unchecked at compile time (commonly called an `any` type). This technique has a low up-front cost, because it does not require specifying the expected shape of the JSON data. + +Unfortunately, if there's any mismatch between the way that returned value ends up being used and the runtime shape of the JSON, it can result in errors that are time-consuming to debug because they are distant from (and may appear unrelated to) the JSON decoding where the problem originated. Since Roc has a [sound type system](https://en.wikipedia.org/wiki/Type_safety), it does not have an `any` type, and cannot support this technique. + +Another technique is to validate the serialized data against a schema specified at compile time, and give an error during decoding if the data doesn't match this schema. Serialization formats like [protocol buffers](https://protobuf.dev/) require this approach, but some languages encourage (or require) doing it for _all_ serialized data formats, which prevents decoding errors from propagating throughout the program and causing distant errors. Roc supports and encourages using this technique. + +In addition to this, Roc also supports serialization _inference_. It has some characteristics of both other approaches: +- Like the first technique, it does not require specifying a schema up front. +- Like the second technique, it reports any errors immediately during decoding rather than letting the problems propagate through the program. + +This technique works by using Roc's type inference to infer the expected shape of serialized data based on how it's used in your program. Here's an example, using [`Decode.fromBytes`](https://www.roc-lang.org/builtins/Decode#fromBytes) to decode some JSON: + +
when Decode.fromBytes data Json.codec is
+    Ok decoded -> # (use the decoded data here)
+    Err err -> # handle the decoding failure
+ +In this example, whether the `Ok` or `Err` branch gets taken at runtime is determined by the way the `decoded` value is used in the source code. + +For example, if `decoded` is used like a record with a `username` field and an `email` field, both of which are strings, then this will fail at runtime if the JSON doesn't have fields with those names and those types. No type annotations are needed for this; it relies entirely on Roc's type inference, which by design can correctly infer types for your entire program even without annotations. + +Serialization inference has a low up-front cost in the same way that the decode-without-validating technique does, but it doesn't have the downside of decoding failures propagating throughout your program to cause distant errors at runtime. (It also works for encoding; there is an [Encode.toBytes](https://www.roc-lang.org/builtins/Encode#toBytes) function which encodes similarly to how [`Decode.fromBytes`](https://www.roc-lang.org/builtins/Decode#fromBytes) decodes.) + +Explicitly writing out a schema has its own benefits that can balance out the extra up-front time investment, but having both techniques available means you can choose whatever will work best for you in a given scenario. + +## [Testing](#testing) {#testing} + +The `roc test` command runs a Roc program's tests. Each test is declared with the `expect` keyword, and can be as short as one line. For example, this is a complete test: + +```roc +## One plus one should equal two. +expect 1 + 1 == 2 +``` + +If the test fails, `roc test` will show you the source code of the `expect`, along with the values of any named variables inside it, so you don't have to separately check what they were. + +If you write a documentation comment right before it (like `## One plus one should equal two` here), it will appear in the test output, so you can use that to add some descriptive context to the test if you want to. + +## [Inline expectations](#inline-expect) {#inline-expect} + +You can also use `expect` in the middle of functions. This lets you verify assumptions that can't reasonably be encoded in types, but which can be checked at runtime. Similarly to [assertions](https://en.wikipedia.org/wiki/Assertion_(software_development)) in other languages, these will run not only during normal program execution, but also during your tests—and they will fail the test if any of them fails. + +Unlike assertions (and unlike the `crash` keyword), failed `expect`s do not halt the program; instead, the failure will be reported and the program will continue. This means all `expect`s can be safely removed during `--optimize` builds without affecting program behavior—and so `--optimize` does remove them. This means you can add inline `expect`s without having to weigh each one's helpfulness against the performance cost of its runtime check, because they won't have any runtime cost after `--optimize` removes them. + +In the future, there are plans to add built-in support for [benchmarking](https://en.wikipedia.org/wiki/Benchmark_(computing)), [generative tests](https://en.wikipedia.org/wiki/Software_testing#Property_testing), [snapshot tests](https://en.wikipedia.org/wiki/Software_testing#Output_comparison_testing), simulated I/O (so you don't have to actually run the real I/O operations, but also don't have to change your code to accommodate the tests), and "reproduction replays"—tests generated from a recording of what actually happened during a particular run of your program, which deterministically simulate all the I/O that happened. + +## Functional + +Besides being designed to be [fast](/fast) and friendly, Roc is also a functional programming language. + +[What does _functional_ mean here?](/functional) diff --git a/www/content/functional.md b/www/content/functional.md new file mode 100644 index 0000000000..28b2581865 --- /dev/null +++ b/www/content/functional.md @@ -0,0 +1,143 @@ +# Functional + +Roc is designed to have a small number of simple language primitives. This goal leads Roc to be a single-paradigm [functional](https://en.wikipedia.org/wiki/Functional_programming) language, while its [performance goals](/fast) lead to some design choices that are uncommon in functional languages. + +## [Opportunistic mutation](#opportunistic-mutation) {#opportunistic-mutation} + +All Roc values are semantically immutable, but may be opportunistically mutated behind the scenes when it would improve performance (without affecting the program's behavior). For example: + +```roc +colors +|> Set.insert "Purple" +|> Set.insert "Orange" +|> Set.insert "Blue" +``` + +The [`Set.insert`](https://www.roc-lang.org/builtins/Set#insert) function takes a `Set` and returns a `Set` with the given value inserted. It might seem like these three `Set.insert` calls would result in the creation of three brand-new sets, but Roc's *opportunistic mutation* optimizations mean this will be much more efficient. + +Opportunistic mutation works by detecting when a semantically immutable value can be safely mutated in-place without changing the behavior of the program. If `colors` is *unique* here—that is, nothing else is currently referencing it—then `Set.insert` will mutate it and then return it. Cloning it first would have no benefit, because nothing in the program could possibly tell the difference! + +If `colors` is _not_ unique, however, then the first call to `Set.insert` will not mutate it. Instead, it will clone `colors`, insert `"Purple"` into the clone, and then return that. At that point, since the clone will be unique (nothing else is referencing it, since it was just created), the subsequent `Set.insert` calls will all mutate in-place. + +Roc has ways of detecting uniqueness at compile time, so this optimization will often have no runtime cost, but in some cases it instead uses automatic reference counting to tell when something that was previously shared has become unique over the course of the running program. + +## [Everything is immutable (semantically)](#everything-is-immutable) {#everything-is-immutable} + +Since mutation is only ever done when it wouldn't change the behavior of the program, all Roc values are semantically immutable—even though they can still benefit from the performance of in-place mutation. + +In many languages, this is reversed; everything is mutable by default, and it's up to the programmer to "defensively" clone to avoid undesirable modification. Roc's approach means that cloning happens automatically, which can be less error-prone than defensive cloning (which might be forgotten), but which—to be fair—can also increase unintentional cloning. It's a different default with different tradeoffs. + +A reliability benefit of semantic immutability everywhere is that it rules out [data races](https://en.wikipedia.org/wiki/Race_condition#Data_race). These concurrency bugs can be difficult to reproduce and time-consuming to debug, and they are only possible through direct mutation. Roc's semantic immutability rules out this category of bugs. + +A performance benefit of having no direct mutation is that it lets Roc rule out [reference cycles](https://en.wikipedia.org/wiki/Reference_counting#Dealing_with_reference_cycles). Languages which support direct mutation can have reference cycles, and detecting these cycles automatically at runtime has a cost. (Failing to detect them can result in memory leaks in languages that use reference counting.) Roc's automatic reference counting neither pays for runtime cycle collection nor memory leaks from cycles, because the language's lack of direct mutation primitives lets it rule out reference cycles at language design time. + +An ergonomics benefit of having no direct mutation primitives is that functions in Roc tend to be chainable by default. For example, consider the `Set.insert` function. In many languages, this function would be written to accept a `Set` to mutate, and then return nothing. In contrast, in Roc it will necessarily be written to return a (potentially) new `Set`, even if in-place mutation will end up happening anyway if it's unique. + +This makes Roc functions naturally amenable to pipelining, as we saw in the earlier example: + +```roc +colors +|> Set.insert "Purple" +|> Set.insert "Orange" +|> Set.insert "Blue" +``` + +To be fair, direct mutation primitives have benefits too. Some algorithms are more concise or otherwise easier to read when written with direct mutation, and direct mutation can make the performance characteristics of some operations clearer. + +As such, Roc's opportunistic mutation design means that data races and reference cycles can be ruled out, and that functions will tend to be more amenable for chaining, but also that some algorithms will be harder to express, and that performance optimization will likely tend to involve more profiling. These tradeoffs fit well with the language's overall design goals. + +## [No reassignment or shadowing](#no-reassignment) {#no-reassignment} + +In some languages, the following is allowed. + +
x = 1
+x = 2
+ +In Roc, this will give a compile-time error. Once a name has been assigned to a value, nothing in the same scope can assign it again. (This includes [shadowing](https://en.wikipedia.org/wiki/Variable_shadowing), which is disallowed.) + +This can make Roc code easier to read, because the answer to the question "might this have a different value later on in the scope?" is always "no." + +That said, this can also make Roc code take longer to write, due to needing to come up with unique names to avoid shadowing—although pipelining (as shown in the previous section) reduces how often intermediate values need names. + +### [Avoiding regressions](#avoiding-regressions) {#avoiding-regressions} + +A benefit of this design is that it makes Roc code easier to rearrange without causing regressions. Consider this code: + +
func = \arg ->
+    greeting = "Hello"
+    welcome = \name -> "\(greeting), \(name)!"
+
+    # …
+
+    message = welcome "friend"
+
+    # …
+ +Suppose I decide to extract the `welcome` function to the top level, so I can reuse it elsewhere: + +
func = \arg ->
+    # …
+
+    message = welcome "Hello" "friend"
+
+    # …
+
+welcome = \prefix, name -> "\(prefix), \(name)!"
+ +Even without knowing the rest of `func`, we can be confident this change will not alter the code's behavior. + +In contrast, suppose Roc allowed reassignment. Then it's possible something in the `# …` parts of the code could have modified `greeting` before it was used in the `message =` declaration. For example: + +
func = \arg ->
+    greeting = "Hello"
+    welcome = \name -> "\(greeting), \(name)!"
+
+    # …
+
+    if someCondition then
+        greeting = "Hi"
+        # …
+    else
+        # …
+
+    # …
+    message = welcome "friend"
+    # …
+ +If we didn't read the whole function and notice that `greeting` was sometimes (but not always) reassigned from `"Hello"` to `"Hi"`, we might not have known that changing it to `message = welcome "Hello" "friend"` would cause a regression due to having the greeting always be `"Hello"`. + +Even if Roc disallowed reassignment but allowed shadowing, a similar regression could happen if the `welcome` function were shadowed between when it was defined here and when `message` later called it in the same scope. Because Roc allows neither shadowing nor reassignment, these regressions can't happen, and rearranging code can be done with more confidence. + +In fairness, reassignment has benefits too. For example, using it with [early-exit control flow operations](https://en.wikipedia.org/wiki/Control_flow#Early_exit_from_loops) such as a `break` keyword can be a nice way to represent certain types of logic without incurring extra runtime overhead. + +Roc does not have early-exits or loop syntax; looping is done either with convenience functions like [`List.walkUntil`](https://www.roc-lang.org/builtins/List#walkUntil) or with recursion (Roc implements [tail-call optimization](https://en.wikipedia.org/wiki/Tail_call), including [modulo cons](https://en.wikipedia.org/wiki/Tail_call#Tail_recursion_modulo_cons)), but early-exit operators can potentially make some code easier to follow (and potentially even slightly more efficient) when used in scenarios where breaking out of nested loops with a single instruction is desirable. + +## [Managed effects over side effects](#managed-effects) {#managed-effects} + +Many languages support first-class [asynchronous](https://en.wikipedia.org/wiki/Asynchronous_I/O) effects, which can improve a system's throughput (usually at the cost of some latency) especially in the presence of long-running I/O operations like network requests. + +Asynchronous effects are commonly represented by a value such as a [Promise or Future](https://en.wikipedia.org/wiki/Futures_and_promises) (Roc calls these Tasks), which represent an effect to be performed. Tasks can be composed together, potentially while customizing concurrency properties and supporting I/O interruptions like cancellation and timeouts. + +Most languages also have a separate system for synchronous effects, namely [side effects](https://en.wikipedia.org/wiki/Side_effect_(computer_science)). Having two different ways to perform every I/O operation—one synchronous and one asynchronous—can lead to a lot of duplication across a language's ecosystem. + +Instead of having [side effects](https://en.wikipedia.org/wiki/Side_effect_(computer_science)), Roc functions exclusively use *managed effects* in which they return descriptions of effects to run, in the form of Tasks. Tasks can be composed and chained together, until they are ultimately handed off (usually via a `main` function or something similar) to an effect runner outside the program, which actually performs the effects the tasks describe. + +Having only (potentially asynchronous) *managed effects* and no (synchronous) *side effects* both simplifies the language's ecosystem and makes certain guarantees possible. For example, the combination of managed effects and semantically immutable values means all Roc functions are [pure](https://en.wikipedia.org/wiki/Pure_function)—that is, they have no side effects and always return the same answer when called with the same arguments. + +## [Pure functions](#pure-functions) {#pure-functions} + +Pure functions have some valuable properties, such as [referential transparency](https://en.wikipedia.org/wiki/Referential_transparency) and being trivial to [memoize](https://en.wikipedia.org/wiki/Memoization). They also have testing benefits; for example, all Roc tests which either use simulated effects (or which do not involve Tasks at all) can never flake. They either consistently pass or consistently fail. Because of this, their results can be cached, so `roc test` can skip re-running them unless their source code (including dependencies) changed. (This caching has not yet been implemented, but is planned.) + +Roc does support [tracing](https://en.wikipedia.org/wiki/Tracing_(software)) via the `dbg` keyword, an essential [debugging](https://en.wikipedia.org/wiki/Debugging) tool which is unusual among side effects in that—similarly to opportunistic mutation—using it should not affect the behavior of the program. As such, it typically does not impact the guarantees of pure functions in practice. + +Pure functions are notably amenable to compiler optimizations, and Roc already takes advantage of them to implement [function-level dead code elimination](https://elm-lang.org/news/small-assets-without-the-headache). Here are some other examples of optimizations that will benefit from this in the future; these are planned, but not yet implemented: + +- [Loop fusion](https://en.wikipedia.org/wiki/Loop_fission_and_fusion), which can do things like combining consecutive `List.map` calls (potentially intermingled with other operations that traverse the list) into one pass over the list. +- [Compile-time evaluation](https://en.wikipedia.org/wiki/Compile-time_function_execution), which basically takes [constant folding](https://en.wikipedia.org/wiki/Constant_folding) to its natural limit: anything that can be evaluated at compile time is evaluated then. This saves work at runtime, and is easy to opt out of: if you want evaluation to happen at runtime, you can instead wrap the logic in a function and call it as needed. +- [Hoisting](https://en.wikipedia.org/wiki/Loop-invariant_code_motion), which moves certain operations outside loops to prevent them from being re-evaluated unnecessarily on each step of the loop. It's always safe to hoist calls to pure functions, and in some cases they can be hoisted all the way to the top level, at which point they become eligible for compile-time evaluation. + +There are other optimizations (some of which have yet to be considered) that pure functions enable; this is just a sample! + +## Get started + +If this design sounds interesting to you, you can give Roc a try by heading over to the [tutorial](/tutorial)! diff --git a/www/content/index.md b/www/content/index.md new file mode 100644 index 0000000000..d0e2bf8b33 --- /dev/null +++ b/www/content/index.md @@ -0,0 +1,169 @@ + + +
+ + + +
+ +
+

Try Roc

+ + + +
+ +## [Examples](#examples) {#examples} + +Roc is a young language. It doesn't even have a numbered release yet, just nightly builds! + +However, it can already be used for several things if you're up for being an early adopter—
+with all the bugs and missing features which come with that territory. + +Here are some examples of how it can be used today. + + + +### [Other Examples](#other-examples) {#other-examples} +You can find more use cases and examples on the [examples page](/examples)! +
+ + +## [Code Sample with Explanations](#code-sample) {#code-sample} + +Here's a code sample that shows a few different aspects of Roc: +* File I/O and HTTP requests +* Pattern matching for error handling +* JSON deserialization via type inference +* Common syntax sugar: string interpolation, pipelines, and backpassing + +The [tutorial](/tutorial) introduces these gradually and in more depth, but this gives a brief overview. + + + +## [Sponsors](#sponsors) {#sponsors} + +We are very grateful for our corporate sponsors [Vendr](https://www.vendr.com/), [RWX](https://www.rwx.com), [Tweede golf](https://tweedegolf.nl/en), and [ohne-makler](https://www.ohne-makler.net): + + + +If you would like your organization to become an official sponsor of Roc's development, please [DM Richard Feldman on Zulip](https://roc.zulipchat.com/#narrow/pm-with/281383-user281383)! + +We'd also like to express our gratitude to our generous [individual sponsors](https://github.com/sponsors/roc-lang/)! A special thanks to those sponsoring $25/month or more: + + + +Thank you all for your contributions! Roc would not be what it is without your generosity. 💜 + +We are currently trying to raise $4,000 USD/month in donations to fund one longtime Roc contributor to continue his work on Roc full-time. We are a small group trying to do big things, and every donation helps! You can donate using: +- [GitHub Sponsors](https://github.com/sponsors/roc-lang) +- [Liberapay](https://liberapay.com/roc_lang) + +All donations go through the [Roc Programming Language Foundation](https://foundation.roc-lang.org/), a registered US 501(c)(3) nonprofit organization, which means these donations are tax-exempt in the US. diff --git a/www/content/install.md b/www/content/install.md new file mode 100644 index 0000000000..ddaa322cdf --- /dev/null +++ b/www/content/install.md @@ -0,0 +1,32 @@ +# Install + +Roc is a very young language with many incomplete features and known bugs. It doesn't even have a numbered release yet, but it does have [nightly builds](https://github.com/roc-lang/roc/releases) that you can download if you'd like to try it out without [building from source](https://github.com/roc-lang/roc/blob/main/BUILDING_FROM_SOURCE.md)! + +There are currently a few known OS-specific issues: +* **macOS:** There are no known compatibility issues, but the compiler doesn't run as fast as it does on Linux or Windows, because we don't (yet) do our own linking like we do on those targets. (Linking works similarly on Linux and Windows, but the way macOS does it is both different and significantly more complicated.) +* **Windows:** There are some known Windows-specific compiler bugs, and probably some other unknown ones because more people have tried out Roc on Mac and Linux than on Windows. +* **Linux:** The nightlies are built with glibc, so they aren't usable on distros that don't use (dynamically linked) glibc, like Alpine or NixOS. In the future we plan to build Linux releases with [musl libc](https://wiki.musl-libc.org/) to address this, but this requires [building LLVM from source with musl](https://wiki.musl-libc.org/building-llvm.html). +* **Other operating systems:** Roc has not been built on any other operating systems. [Building from source](https://github.com/roc-lang/roc/blob/main/BUILDING_FROM_SOURCE.md) on another OS might work, but you might very well be the first person ever to try it! + +### [Getting Started](#getting-started) {#getting-started} + +Here are some Getting Started guides for different operating systems: + + +- [Linux x86-64](https://github.com/roc-lang/roc/blob/main/getting_started/linux_x86_64.md) +- [MacOS Apple Silicon](https://github.com/roc-lang/roc/blob/main/getting_started/macos_apple_silicon.md) +- [MacOS x86-64](https://github.com/roc-lang/roc/blob/main/getting_started/macos_x86_64.md) +- [Windows](https://github.com/roc-lang/roc/blob/main/getting_started/windows.md) +- [Other Systems](https://github.com/roc-lang/roc/blob/main/getting_started/other.md) + +### [Editor Extensions](#editor-extensions) {#editor-extensions} + +There is currently a [VS Code extension](https://marketplace.visualstudio.com/items?itemName=IvanDemchenko.roc-lang-unofficial) for Roc which includes instructions in its README for how to enable a Roc language server. + +Currently that language server has to be built from source; it would be a fantastic contribution if anyone could get it incorporated it into the extension directly. If you'd like to help with this, just make a post in [the "new contributors" topic on Zulip](https://roc.zulipchat.com/#narrow/stream/316715-contributing/topic/new.20contributors) and say hello! + +### [Tutorial](#tutorial) {#tutorial} + +Once you've installed roc, check out the [tutorial](/tutorial) to learn how to Roc! + +Start Tutorial diff --git a/www/content/plans.md b/www/content/plans.md new file mode 100644 index 0000000000..1495a32c8a --- /dev/null +++ b/www/content/plans.md @@ -0,0 +1,47 @@ +# Plans + +This is not a roadmap, but it is a set of current plans for the language. Plans can change, of course, but the goal here is to lay out some of the current plans. It won't be an exhaustive list, but it should give you the highlights. + +## Planned Breaking Changes + +These are changes that are both planned and planned to be breaking (so, not backwards-compatible), meaning you may need to make changes to your Roc code if they end up getting released. + +The best time to make breaking changes (that will benefit more and more people as the community grows) is when the number of affected code bases is small. That said, the frequency of breaking changes should naturally decrease over time, and of course past a certain level of maturity, the number of justifiable breaking changes approaches zero. (That level of maturity is quite a ways away!) + +### Builtins + +Currently, [builtins](https://www.roc-lang.org/builtins) get breaking changes from time to time. There aren't any specific plans to make particular breaking changes to them, because typically when we decide a change is warranted, we discuss and implement the change pretty quickly. + +As an example, we had [a discussion](https://roc.zulipchat.com/#narrow/stream/304641-ideas/topic/Drop.20n.20elements.20from.20the.20end.20of.20a.20list) about changing the API for how elements get dropped from a list, and then a week later [announced](https://roc.zulipchat.com/#narrow/stream/397893-announcements/topic/List.2Edrop.2C.20dropFirst.2C.20dropLast) that the change had shipped. + +This has been consistently happening a few times per year. It's hard to predict exactly what the next one will be, but it's a safe bet that it will happen again. + +### Glue + +This one only applies to platform authors. + +Much like with builtins, we periodically make changes to code generation or to the `roc glue` API that aren't backwards compatible. When this happens, relevant glue scripts need to be updated and then `roc glue` needs to be re-run on platforms to regenerate their host glue. + +As with builtins, it's hard to predict when these will happen and what they'll be, but right now it's a safe bet that they will happen from time to time. + +### Import syntax + +Implementing the very important [module params](https://docs.google.com/document/d/110MwQi7Dpo1Y69ECFXyyvDWzF4OYv1BLojIm08qDTvg/edit?usp=sharing) feature requires a breaking syntax change to how imports work. This plan is not at all tentative; there is a high degree of confidence that it will happen! + +Work has not started on this yet, but we'd like to have the project completed sometime in 2024. + +## Planned Non-Breaking Changes + +These are planned changes to how things work, which should be backwards-compatible and require no code changes. These won't include bugfixes, just changing something that currently works as designed to have a different design. + +### Tag Union Refinement + +This doesn't come up a lot, but [the feature](https://github.com/roc-lang/roc/issues/5504) basically means you can match on some tags in a `when`, and then have an `other ->` branch which has the tags you already matched on removed from the union. That means if you later do another `when` on the `other` value, you won't have to match on (or use `_ ->` to ignore) the tags you already matched in the first `when`, like you do today. + +This is planned but nobody is currently working on it. It's a quality of life improvement but doesn't unblock anything; today you can just add a `_ ->` branch to the inner `when`, which is undesirable but not a blocker. + +### `Inspect` Inference + +When this lands, all Roc types will have a default `implements Inspect`, which you can override if desired. `dbg` will use it to display things, which in turn means you'll be able to customize `dbg` output. Also it will mean you can do things like turning any Roc type into a string and writing it to a log file. + +Note that in this design, functions will have an `Inspect` implementation which essentially renders them as `""` with no other information, and opaque types will be `""` by default unless you customize them. This is important because neither functions nor opaque types should expose their internal details, so that you can safely refactor them without causing regressions in distant parts of the code base because something depended on an internal implementation detail. diff --git a/www/content/platforms.md b/www/content/platforms.md new file mode 100644 index 0000000000..46943e9afc --- /dev/null +++ b/www/content/platforms.md @@ -0,0 +1,114 @@ +# Platforms + +Something that sets Roc apart from other programming languages is its *platforms and applications* architecture. + +## [Applications](#applications) {#applications} + +Here is a Roc application that prints `"Hello, World!"` to the command line: + +```roc +app "hello" + packages { pf: "https://github.com/roc-lang/basic-cli/releases/download/0.5.0/Cufzl36_SnJ4QbOoEmiJ5dIpUxBvdB3NEySvuH82Wio.tar.br" } + imports [pf.Stdout] + provides [main] to pf + +main = + Stdout.line "Hello, World!" +``` + +Every Roc application, including this one, is built on a _platform_. This application happens to be built on a platform called [basic-cli](https://github.com/roc-lang/basic-cli), which is a platform for building command-line interfaces. + +## [Domain-specific functionality](#domain-specific) {#domain-specific} + +Roc platforms provide domain-specific functionality that multiple applications can use as a foundation to build on, much like game engines and Web frameworks do. + +Also like many game engines and Web frameworks, Roc platforms have a high-level Roc API which presents a nice interface to a lower-level implementation (written in a different language), which provides the foundational primitives that platform needs to operate—such as a C++ 3D rendering system in a game engine, or a Rust HTTP networking system in a Web framework. + +Here are some example Roc platforms, and functionality they might provide: + +* A Roc game engine platform might provide functionality for rendering and sound. +* A Roc Web server platform (like [basic-webserver](https://github.com/roc-lang/basic-webserver)) probably would not provide functionality for rendering and sound, but it might provide functionality for responding to incoming HTTP requests—which a game engine platform likely would not. +* A Roc native [GUI](https://en.wikipedia.org/wiki/Graphical_user_interface) platform might provide functionality for defining native operating system UI elements, whereas a game engine platform might focus more on rendering with [shaders](https://en.wikipedia.org/wiki/Shader), and a Web server platform would not have GUI functionality at all. + +These are broad domains, but platforms can be much more specific than this. For example, anyone could make a platform for writing [Vim](https://en.wikipedia.org/wiki/Vim_(text_editor)) plugins, or [Postgres](https://en.wikipedia.org/wiki/PostgreSQL) extensions, or robots ([which has already happened](https://roc.zulipchat.com/#narrow/stream/304902-show-and-tell/topic/Roc.20on.20a.20microcontroller/near/286678630)), or even [implementing servo logic for a clock that physically turns panels to simulate an LCD](https://roc.zulipchat.com/#narrow/stream/304641-ideas/topic/Roc.20Clock/near/327939600). You really can get as specific as you like! + +Platforms can also be designed to have a single, specific application run on them. For example, you can make a platform that is essentially "your entire existing code base in another language," and then use Roc as an embedded language within that code base. For example, [Vendr](https://www.vendr.com/careers) is using this strategy to call Roc functions from their [Node.js](https://nodejs.org/en) backend using [roc-esbuild](https://github.com/vendrinc/roc-esbuild), as a way to incrementally transition code from Node to Roc. + +## [Platform scope](#scope) {#scope} + +Roc platforms have a broader scope of responsibility than game engines or Web frameworks. In addition to providing a nice domain-specific interface, platforms are also responsible for: +* Tailoring memory management to that domain (more on this later) +* Providing all I/O primitives + +In most languages, I/O primitives come with the standard library. In Roc, the [standard library](https://www.roc-lang.org/builtins/) contains only data structures; an application gets all of its I/O primitives from its platform. For example, in the "Hello, World" application above, the `Stdout.line` function comes from the `basic-cli` platform itself, not from Roc's standard library. + +This design has a few benefits. + +### [Ecosystem benefits](#ecosystem) {#ecosystem} + +Some I/O operations make sense in some use cases but not others. + +For example, suppose I'm building an application on a platform for command-line interfaces, and I use a third-party package which sometimes blocks the program while it waits for [standard input](https://en.wikipedia.org/wiki/Standard_streams#Standard_input_(stdin)). This might be fine for my command-line application, but it would probably be a very bad fit if I'm using a webserver. Similarly, a package which does some occasional file I/O for caching might work fine on either of those platforms, but might break in surprising ways when used in a platform that's designed to run in a browser on WebAssembly—since browsers don't offer arbitrary file I/O access! + +Because Roc's I/O primitives come from platforms, these mismatches can be prevented at build time. The browser-based platform would not expose file I/O primitives, the webserver wouldn't expose a way to block on reading from standard input, and so on. (Note that there's a design in the works for allowing packages which perform I/O to work across multiple platforms—but only platforms which support the I/O primitives it requires—but this design has not yet been implemented.) + +### [Security benefits](#security) {#security} + +Since platforms have exclusive control over all I/O primitives, one of the things they can do is create domain-specific security guarantees around them. For example, a platform for writing text editor plugins might want to display a prompt to the end user before performing any file I/O operations outside the directory that's currently open in the editor. + +[This talk](https://www.youtube.com/watch?v=cpQwtwVKAfU&t=75s) shows an example of taking this idea a step further with a "safe scripting" platform for writing command-line scripts. The idea is that you could download a script from the Internet and run it on this platform without worrying that the script would do bad things to your computer, because the platform would (much like a Web browser) show you specific prompts before allowing the script to do potentially harmful I/O, such as filesystem operations. + +These security guarantees can be relied on because platforms have *exclusive* control over all I/O primitives, including how they are implemented. There are no escape hatches that a malicious program could use to get around these, either; for example, Roc programs that want to call functions in other languages must do so using primitives provided by the platform, which the platform can disallow (or sandbox with end-user prompts) in the same way. + +### [Performance benefits](#performance) {#performance} + +Many I/O operations can benefit from being run concurrently. Since platforms are in charge of how those I/O operations are implemented, they can also determine how they are scheduled. This means that both applications and packages can describe which operations they want to run concurrently, and then the platform can optimize the scheduling of these operations using its domain-specific knowledge. + +For example, a command-line platform might schedule concurrent operations across all available cores (or some lower number specified by a command-line argument). In contrast, a Web server platform might try to balance available cores across multiple request handlers—to prevent undesirable scenarios like one handler getting all the cores (meaning none of the others can progress). + +Note that although platform-implemented scheduling of concurrent operations is theoretically possible today, there are currently some missing pieces to make it practical for platform authors to implement. Implementing those missing pieces is already in progress, but is not yet complete. + +## [How platforms are implemented](#implementation) {#implementation} + +To understand how platforms can tailor automatic memory management to their particular domain, it's helpful to understand how platforms are implemented. + +### [The Host and the Roc API](#host-and-roc-api) {#host-and-roc-api} + +Each platform consists of two parts: +* **The Roc API** is the part that application authors see. For example, `Stdout.line` is part of basic-cli's Roc API. +* **The Host** is the under-the-hood implementation written in a language other than Roc. For example, basic-cli's host is written in Rust. It has a Rust function which implements the behavior of the `Stdout.line` operation, and all the other I/O operations it supports. + +This design means that application authors don't necessarily need to know (or care) about the non-Roc language being used to implement the platform's host. That can be a behind-the-scenes implementation detail that only the platform's author(s) are concerned with. Application authors only interact with the public-facing Roc API. + +### [Memory management](#memory) {#memory} + +Host authors implement not only the platform's I/O primitives, but also functions for memory allocation and deallocation. In C terms, the host provides [`malloc` and `free`](https://en.wikipedia.org/wiki/C_dynamic_memory_allocation) implementations which the compiled Roc application will automatically call whenever it needs to allocate or deallocate memory. + +[The same talk mentioned earlier](https://www.youtube.com/watch?v=cpQwtwVKAfU&t=75s) in the context of [security benefits](#security) demonstrates some benefits of this, such as being able to get accurate diagnostics on how much memory the Roc part (or even specific Roc parts) of a running program are using. + +The bigger benefit is tailoring memory management itself based on the domain. For example, [nea](https://github.com/tweedegolf/nea/) is a work-in-progress Web server which performs [arena allocation](https://en.wikipedia.org/wiki/Region-based_memory_management) on each request handler. In Roc terms, this means the host's implementation of `malloc` can allocate into the current handler's arena, and `free` can be a no-op. Instead, the arena can be reset when the response has been sent. + +In this design, heap allocations in a Web server running on `nea` are about as cheap as stack allocations, and deallocations are essentially free. This is much better for the server's throughput, latency, and predictability than (for example) having to pay for periodic garbage collection! + +### [Program start](#program-start) {#program-start} + +When a compiled Roc program runs, it's actually the host—not the Roc application—which starts running first. In C terms, the host implements `main()`, and then at some point it calls a function exposed by the compiled Roc application. + +Knowing this, a useful mental model for how Roc platforms and applications interact at the implementation level is: the Roc application compiles down to a C library which the platform can choose to call (or not). + +This is essentially what's happening behind the scenes when you run `roc build`. Specifically: +1. The Roc compiler builds the Roc application into a binary [object file](https://en.wikipedia.org/wiki/Object_file) +2. Since that application specified its platform, the compiler then looks up the platform's host implementation (which the platform will have provided as an already-compiled binary) +3. Now that it has a binary for the Roc application and a binary for the host, it links them together into one combined binary in which the host portion calls the application portion as many times as it likes. + +This process works for small platforms and large applications (for example, a very large Web server application) as well as for large platforms and small applications (for example, a very large C++ game which serves as a platform for a small amount of Roc application code that the game uses for scripting). + +## [Summary](#summary) {#summary} + +Every Roc application has exactly one platform. That platform provides all the I/O primitives that the application can use; Roc's standard library provides no I/O operations, and the only way for a Roc application to execute functions in other languages is if the platform offers a way to do that. + +This I/O design has [security benefits](#security), [ecosystem benefits](#ecosystem), and [performance benefits](#performance). The [domain-specific memory management](#memory) platforms can implement can offer additional benefits as well. + +Applications only interact with the *Roc API* portion of a platform, but there is also a *host* portion (written in a different language) that works behind the scenes. The host determines how the program starts, how memory is allocated and deallocated, and how I/O primitives are implemented. + +Anyone can implement their own platform. There isn't yet an official guide about how to do this, so the best way to get help if you'd like to create a platform is to [say hi in the `#beginners` channel](https://roc.zulipchat.com/#narrow/stream/231634-beginners) on [Roc Zulip!](https://roc.zulipchat.com) diff --git a/www/content/repl/index.md b/www/content/repl/index.md new file mode 100644 index 0000000000..d7a63acdab --- /dev/null +++ b/www/content/repl/index.md @@ -0,0 +1,15 @@ + +## The rockin' Roc REPL + + + +
diff --git a/www/content/tutorial.md b/www/content/tutorial.md new file mode 100644 index 0000000000..14a3ddf5ef --- /dev/null +++ b/www/content/tutorial.md @@ -0,0 +1,2080 @@ + + +
+
+

Tutorial

+

Welcome to Roc!

+

This tutorial will teach you how to build Roc applications. Along the way, you'll learn how to write tests, use the REPL, and more!

+
+
+

Installation

+

Roc doesn’t have a numbered release or an installer yet, but you can follow the install instructions for your OS here . If you get stuck, friendly people will be happy to help if you open a topic in #beginners on Roc Zulip Chat and ask for assistance!

+
+ +## [REPL](#repl) {#repl} + +Let's start by getting acquainted with Roc's [_Read-Eval-Print-Loop_](https://en.wikipedia.org/wiki/Read%E2%80%93eval%E2%80%93print_loop), or **REPL** for short. + +You can use the online REPL at [roc-lang.org/repl](https://www.roc-lang.org/repl). + +Or you can run this in a terminal: roc repl, and if Roc is [installed](/install), you should see this: + +
+
+  The rockin’ roc repl
+────────────────────────
+
+Enter an expression, or :help, or :q to quit.
+
+
+ +So far, so good! + +### [Hello, World!](#hello-world) {#hello-world} + +Try typing this in the REPL and pressing Enter: + +"Hello, World!" + +The REPL should cheerfully display the following: + +
"Hello, World!" : Str
+ +Congratulations! You've just written your first Roc code. + +### [Naming Things](#naming-things) {#naming-things} + +When you entered the _expression_ `"Hello, World!"`, the REPL printed it back out. It also printed `: Str`, because `Str` is that expression's type. We'll talk about types later; for now, let's ignore the `:` and whatever comes after it whenever we see them. + +You can assign specific names to expressions. Try entering these lines: + +``` +greeting = "Hi" +audience = "World" +``` + +From now until you exit the REPL, you can refer to either `greeting` or `audience` by those names! We'll use these later on in the tutorial. + +### [Arithmetic](#arithmetic) {#arithmetic} + +Now let's try using an _operator_, specifically the `+` operator. Enter this: + +
1 + 1
+ +You should see this output: + +
2 : Num * 
+ +According to the REPL, one plus one equals two. Sounds right! + +Roc will respect [order of operations](https://en.wikipedia.org/wiki/Order_of_operations) when using multiple arithmetic operators like `+` and `-`, but you can use parentheses to specify exactly how they should be grouped. + +
1 + 2 * (3 - 4)
+
+-1 : Num *
+
+ +### [Calling Functions](#calling-functions) {#calling-functions} + +Let's try calling a function: + +
Str.concat "Hi " "there!"
+
+"Hi there!" : Str
+
+ +Here we're calling the `Str.concat` function and passing two arguments: the string `"Hi "` and the string `"there!"`. This _concatenates_ the two strings together (that is, it puts one after the other) and returns the resulting combined string of `"Hi there!"`. + +Note that in Roc, we don't need parentheses or commas to call functions. We don't write `Str.concat("Hi ", "there!")` but rather `Str.concat "Hi " "there!"`. + +That said, just like in the arithmetic example above, we can use parentheses to specify how nested function calls should work. For example, we could write this: + +
Str.concat "Birds: " (Num.toStr 42)
+
+"Birds: 42" : Str
+
+ +This calls `Num.toStr` on the number `42`, which converts it into the string `"42"`, and then passes that string as the second argument to `Str.concat`. + +The parentheses are important here to specify how the function calls nest. Try removing them, and see what happens: + +
Str.concat "Birds: " Num.toStr 42
+
+<error>
+
+ +The error tells us that we've given `Str.concat` too many arguments. Indeed we have! We've passed it three arguments: + +1. The string `"Birds"` +2. The function `Num.toStr` +3. The number `42` + +That's not what we intended to do. Putting parentheses around the `Num.toStr 42` call clarifies that we want it to be evaluated as its own expression, rather than being two arguments to `Str.concat`. + +Both the `Str.concat` function and the `Num.toStr` function have a dot in their names. In `Str.concat`, `Str` is the name of a _module_, and `concat` is the name of a function inside that module. Similarly, `Num` is a module, and `toStr` is a function inside that module. + +We'll get into more depth about modules later, but for now you can think of a module as a named collection of functions. Eventually we'll discuss how to use them for more than that. + +### [String Interpolation](#string-interpolation) {#string-interpolation} + +An alternative syntax for `Str.concat` is _string interpolation_, which looks like this: + +
"\(greeting) there, \(audience)!"
+ +This is syntax sugar for calling `Str.concat` several times, like so: + +```roc +Str.concat greeting (Str.concat " there, " (Str.concat audience "!")) +``` + +You can put entire single-line expressions inside the parentheses in string interpolation. For example: + +
"Two plus three is: \(Num.toStr (2 + 3))"
+ +By the way, there are many other ways to put strings together! Check out the [documentation](https://www.roc-lang.org/builtins/Str) for the `Str` module for more. + +## [Building an Application](#building-an-application) {#building-an-application} + +Let's move out of the REPL and create our first Roc application! + +Make a file named `main.roc` and put this in it: + +```roc +app "hello" + packages { pf: "https://github.com/roc-lang/basic-cli/releases/download/0.5.0/Cufzl36_SnJ4QbOoEmiJ5dIpUxBvdB3NEySvuH82Wio.tar.br" } + imports [pf.Stdout] + provides [main] to pf + +main = + Stdout.line "I'm a Roc application!" +``` + +Try running this with: + +roc dev + +You should see a message about a file being downloaded, followed by this: + +I'm a Roc application! + +Congratulations, you've written your first Roc application! We'll go over what the parts above `main` do later, but let's play around a bit first. + +### [Defs](#defs) {#defs} + +Try replacing the `main` line with this: + +```roc +birds = 3 + +iguanas = 2 + +total = Num.toStr (birds + iguanas) + +main = + Stdout.line "There are \(total) animals." +``` + +Now run `roc dev` again. This time the "Downloading ..." message won't appear; the file has been cached from last time, and won't need to be downloaded again. + +You should see this: + +There are 5 animals. + +`main.roc` now has four definitions (_defs_ for short) `birds`, `iguanas`, `total`, and `main`. + +A definition names an expression. + +- The first two defs assign the names `birds` and `iguanas` to the expressions `3` and `2`. +- The next def assigns the name `total` to the expression `Num.toStr (birds + iguanas)`. +- The last def assigns the name `main` to an expression which returns a `Task`. We'll [discuss tasks later](#tasks). + +Once we have a def, we can use its name in other expressions. For example, the `total` expression refers to `birds` and `iguanas`, and `Stdout.line "There are \(total) animals."` refers to `total`. + +You can name a def using any combination of letters and numbers, but they have to start with a lowercase letter. + +**Note:** Defs are constant; they can't be reassigned. We'd get an error if we wrote these two defs in the same scope: + +```roc +birds = 3 +birds = 2 +``` + +### [Defining Functions](#defining-functions) {#defining-functions} + +So far we've called functions like `Num.toStr`, `Str.concat`, and `Stdout.line`. Next let's try defining a function of our own. + +```roc +birds = 3 + +iguanas = 2 + +total = addAndStringify birds iguanas + +main = + Stdout.line "There are \(total) animals." + +addAndStringify = \num1, num2 -> + Num.toStr (num1 + num2) +``` + +This new `addAndStringify` function we've defined accepts two numbers, adds them, calls `Num.toStr` on the result, and returns that. + +The `\num1, num2 ->` syntax defines a function's arguments, and the expression after the `->` is the body of the function. Whenever a function gets called, its body expression gets evaluated and returned. + +### [if-then-else](#if-then-else) {#if-then-else} + +Let's modify this function to return an empty string if the numbers add to zero. + +```roc +addAndStringify = \num1, num2 -> + sum = num1 + num2 + + if sum == 0 then + "" + else + Num.toStr sum +``` + +We did two things here: + +- We introduced a _local def_ named `sum`, and set it equal to `num1 + num2`. Because we defined `sum` inside `addAndStringify`, it's _local_ to that scope and can't be accessed outside that function. +- We added an `if`\-`then`\-`else` conditional to return either `""` or `Num.toStr sum` depending on whether `sum == 0`. + +Every `if` must be accompanied by both `then` and also `else`. Having an `if` without an `else` is an error, because `if` is an expression, and all expressions must evaluate to a value. If there were ever an `if` without an `else`, that would be an expression that might not evaluate to a value! + +### [else if](#else-if) {#else-if} + +We can combine `if` and `else` to get `else if`, like so: + +```roc +addAndStringify = \num1, num2 -> + sum = num1 + num2 + + if sum == 0 then + "" + else if sum < 0 then + "negative" + else + Num.toStr sum +``` + +Note that `else if` is not a separate language keyword! It's just an `if`/`else` where the `else` branch contains another `if`/`else`. This is easier to see with different indentation: + +```roc +addAndStringify = \num1, num2 -> + sum = num1 + num2 + + if sum == 0 then + "" + else + if sum < 0 then + "negative" + else + Num.toStr sum +``` + +This differently-indented version is equivalent to writing `else if sum < 0 then` on the same line, although the convention is to use the original version's style. + +### [Comments](#comments) {#comments} + +This is a comment in Roc: + +```roc +# The 'name' field is unused by addAndStringify +``` + +Whenever you write `#` it means that the rest of the line is a comment, and will not affect the +running program. Roc does not have multiline comment syntax. + +### [Doc Comments](#doc-comments) {#doc-comments} + +Comments that begin with `##` are "doc comments" which will be included in generated documentation (`roc docs`). They can include code blocks by adding five spaces after `##`. + +```roc +## This is a comment for documentation, and includes a code block. +## +## x = 2 +## expect x == 2 +``` + +Like other comments, doc comments do not affect the running program. + +## [Debugging](#debugging) {#debugging} + +[Print debugging](https://en.wikipedia.org/wiki/Debugging#Techniques) is the most common debugging technique in the history of programming, and Roc has a `dbg` keyword to facilitate it. Here's an example of how to use `dbg`: + +```roc +pluralize = \singular, plural, count -> + dbg count + + if count == 1 then + singular + else + plural +``` + +Whenever this `dbg` line of code is reached, the value of `count` will be printed to [stderr](), along with the source code file and line number where the `dbg` itself was written: + +[pluralize.roc 6:8] 5 + +Here, `[pluralize.roc 6:8]` tells us that this `dbg` was written in the file `pluralize.roc` on line 6, column 8. + +You can give `dbg` any expression you like, for example: + +```roc +dbg Str.concat singular plural +``` + +An easy way to print multiple values at a time is to wrap them in a tag, for example a concise tag like `T`: + +```roc +dbg T "the value of count is:" count +``` + +> **Note:** `dbg` is a debugging tool, and is only available when running your program via a `roc` subcommand (for example using `roc dev`, `roc run`, or `roc test`). When you build a standalone application with `roc build`, any uses of `dbg` won't be included! + +## [Records](#records) {#records} + +Currently our `addAndStringify` function takes two arguments. We can instead make it take one argument like so: + +```roc +total = addAndStringify { birds: 5, iguanas: 7 } + +addAndStringify = \counts -> + Num.toStr (counts.birds + counts.iguanas) +``` + +The function now takes a _record_, which is a group of named values. Records are not [objects](https://en.wikipedia.org/wiki/Object_(computer_science)); they don't have methods or inheritance, they just store information. + +The expression `{ birds: 5, iguanas: 7 }` defines a record with two _fields_ (the `birds` field and the `iguanas` field) and then assigns the number `5` to the `birds` field and the number `7` to the `iguanas` field. Order doesn't matter with record fields; we could have also specified `iguanas` first and `birds` second, and Roc would consider it the exact same record. + +When we write `counts.birds`, it accesses the `birds` field of the `counts` record, and when we write `counts.iguanas` it accesses the `iguanas` field. + +When we use [`==`](/builtins/Bool#isEq) on records, it compares all the fields in both records with [`==`](/builtins/Bool#isEq), and only considers the two records equal if all of their fields are equal. If one record has more fields than the other, or if the types associated with a given field are different between one field and the other, the Roc compiler will give an error at build time. + +> **Note:** Some other languages have a concept of "identity equality" that's separate from the "structural equality" we just described. Roc does not have a concept of identity equality; this is the only way equality works! + +### [Accepting extra fields](#accepting-extra-fields) {#accepting-extra-fields} + +The `addAndStringify` function will accept any record with at least the fields `birds` and `iguanas`, but it will also accept records with more fields. For example: + +```roc +total = addAndStringify { birds: 5, iguanas: 7 } + +# The `note` field is unused by addAndStringify +totalWithNote = addAndStringify { birds: 4, iguanas: 3, note: "Whee!" } + +addAndStringify = \counts -> + Num.toStr (counts.birds + counts.iguanas) +``` + +This works because `addAndStringify` only uses `counts.birds` and `counts.iguanas`. If we were to use `counts.note` inside `addAndStringify`, then we would get an error because `total` is calling `addAndStringify` passing a record that doesn't have a `note` field. + +### [Record shorthands](#record-shorthands) {#record-shorthands} + +Roc has a couple of shorthands you can use to express some record-related operations more concisely. + +Instead of writing `\record -> record.x` we can write `.x` and it will evaluate to the same thing: a function that takes a record and returns its `x` field. You can do this with any field you want. For example: + +```roc +# returnFoo is a function that takes a record +# and returns the `foo` field of that record. +returnFoo = .foo + +returnFoo { foo: "hi!", bar: "blah" } +# returns "hi!" +``` + +Sometimes we assign a def to a field that happens to have the same name—for example, `{ x: x }`. +In these cases, we shorten it to writing the name of the def alone—for example, `{ x }`. We can do this with as many fields as we like; here are several different ways to define the same record: + +- `{ x: x, y: y }` +- `{ x, y }` +- `{ x: x, y }` +- `{ x, y: y }` + +### [Record destructuring](#record-destructuring) {#record-destructuring} + +We can use _destructuring_ to avoid naming a record in a function argument, instead giving names to its individual fields: + +```roc +addAndStringify = \{ birds, iguanas } -> + Num.toStr (birds + iguanas) +``` + +Here, we've _destructured_ the record to create a `birds` def that's assigned to its `birds` field, and an `iguanas` def that's assigned to its `iguanas` field. We can customize this if we like: + +```roc +addAndStringify = \{ birds, iguanas: lizards } -> + Num.toStr (birds + lizards) +``` + +In this version, we created a `lizards` def that's assigned to the record's `iguanas` field. (We could also do something similar with the `birds` field if we like.) + +Finally, destructuring can be used in defs too: + +```roc +{ x, y } = { x: 5, y: 10 } +``` + +### [Making records from other records](#making-records-from-other-records) {#making-records-from-other-records} + +So far we've only constructed records from scratch, by specifying all of their fields. We can also construct new records by using another record to use as a starting point, and then specifying only the fields we want to be different. For example, here are two ways to get the same record: + +```roc +original = { birds: 5, zebras: 2, iguanas: 7, goats: 1 } +fromScratch = { birds: 4, zebras: 2, iguanas: 3, goats: 1 } +fromOriginal = { original & birds: 4, iguanas: 3 } +``` + +The `fromScratch` and `fromOriginal` records are equal, although they're defined in different ways. + +- `fromScratch` was built using the same record syntax we've been using up to this point. +- `fromOriginal` created a new record using the contents of `original` as defaults for fields that it didn't specify after the `&`. + +Note that `&` can't introduce new fields to a record, or change the types of existing fields. +(Trying to do either of these will result in an error at build time!) + +## [Optional Record Fields](#optional-record-fields) {#optional-record-fields} + +Roc supports optional record fields using the `?` operator. This can be a useful pattern where you pass a function a record of configuration values, some of which you'd like to provide defaults for. + +In Roc you can write a function like: + +```roc +table = \{ + height, + width, + title? "oak", + description? "a wooden table" + } + -> +``` + +This is using *optional field destructuring* to destructure a record while +also providing default values for any fields that might be missing. + +Here's the type of `table`: + +```roc +table : + { + height : Pixels, + width : Pixels, + title ? Str, + description ? Str, + } + -> Table +``` + +This says that `table` takes a record with two *required* fields, `height` and +`width`, and two *optional* fields, `title` and `description`. It also says that +the `height` and `width` fields have the type `Pixels`, a type alias for some +numeric type, and the `title` and `description` fields have the type `Str`. +This means you can choose to omit the `title`, `description`, or both fields, when calling the function... but if you provide them, they must have the type `Str`. + +This is also the type that would have been inferred for `table` if no annotation +had been written. Roc's compiler can tell from the destructuring syntax +`title ? ""` that `title` is an optional field, and that it has the type `Str`. +These default values can reference other expressions in the record destructure; if you wanted, you could write `{ height, width, title ? "", description ? Str.concat "A table called " title }`. + +Destructuring is the only way to implement a record with optional fields. For example, if you write the expression `config.title` and `title` is an +optional field, you'll get a compile error. + +This means it's never possible to end up with an *optional value* that exists +outside a record field. Optionality is a concept that exists only in record +fields, and it's intended for the use case of config records like this. The +ergonomics of destructuring mean this wouldn't be a good fit for data modeling, consider using a `Result` type instead. + +## [Tags & Pattern Matching](#tags) {#tags} + +Sometimes we want to represent that something can have one of several values. For example: + +```roc +stoplightColor = + if something > 0 then + Red + else if something == 0 then + Yellow + else + Green +``` + +Here, `stoplightColor` can have one of three values: `Red`, `Yellow`, or `Green`. The capitalization is very important! If these were lowercase (`red`, `yellow`, `green`), then they would refer to defs. However, because they are capitalized, they instead refer to _tags_. + +A tag is a literal value just like a number or a string. Similarly to how I can write the number `42` or the string `"forty-two"` without defining them first, I can also write the tag `FortyTwo` without defining it first. Also, similarly to how `42 == 42` and `"forty-two" == "forty-two"`, it's also the case that `FortyTwo == FortyTwo`. + +Let's say we wanted to turn `stoplightColor` from a `Red`, `Green`, or `Yellow` into a string. Here's one way we could do that: + +```roc +stoplightStr = + if stoplightColor == Red then + "red" + else if stoplightColor == Green then + "green" + else + "yellow" +``` + +We can express this logic more concisely using `when`/`is` instead of `if`/`then`: + +```roc +stoplightStr = + when stoplightColor is + Red -> "red" + Green -> "green" + Yellow -> "yellow" +``` + +This results in the same value for `stoplightStr`. In both the `when` version and the `if` version, we have three conditional branches, and each of them evaluates to a string. The difference is how the conditions are specified; here, we specify between `when` and `is` that we're making comparisons against `stoplightColor`, and then we specify the different things we're comparing it to: `Red`, `Green`, and `Yellow`. + +Besides being more concise, there are other advantages to using `when` here. + +1. We don't have to specify an `else` branch, so the code can be more self-documenting about exactly what all the options are. +2. We get more compiler help. If we try deleting any of these branches, we'll get a compile-time error saying that we forgot to cover a case that could come up. For example, if we delete the `Green ->` branch, the compiler will say that we didn't handle the possibility that `stoplightColor` could be `Green`. It knows this because `Green` is one of the possibilities in our `stoplightColor = if ...` definition. + +We can still have the equivalent of an `else` branch in our `when` if we like. Instead of writing `else`, we write `_ ->` like so: + +```roc +stoplightStr = + when stoplightColor is + Red -> "red" + _ -> "not red" +``` + +This lets us more concisely handle multiple cases. However, it has the downside that if we add a new case - for example, if we introduce the possibility of `stoplightColor` being `Orange`, the compiler can no longer tell us we forgot to handle that possibility in our `when`. After all, we are handling it - just maybe not in the way we'd decide to if the compiler had drawn our attention to it! + +We can make this `when` _exhaustive_ (that is, covering all possibilities) without using `_ ->` by using `|` to specify multiple matching conditions for the same branch: + +```roc +stoplightStr = + when stoplightColor is + Red -> "red" + Green | Yellow -> "not red" +``` + +You can read `Green | Yellow` as "either `Green` or `Yellow`". By writing it this way, if we introduce the possibility that `stoplightColor` can be `Orange`, we'll get a compiler error telling us we forgot to cover that case in this `when`, and then we can handle it however we think is best. + +We can also combine `if` and `when` to make branches more specific: + +```roc +stoplightStr = + when stoplightColor is + Red -> "red" + Green | Yellow if contrast > 75 -> "not red, but very high contrast" + Green | Yellow if contrast > 50 -> "not red, but high contrast" + Green | Yellow -> "not red" +``` + +This will give the same answer for `stoplightStr` as if we had written the following: + +```roc +stoplightStr = + when stoplightColor is + Red -> "red" + Green | Yellow -> + if contrast > 75 then + "not red, but very high contrast" + else if contrast > 50 then + "not red, but high contrast" + else + "not red" +``` + +Either style can be a reasonable choice depending on the circumstances. + +### [Tags with payloads](#tags-with-payloads) {#tags-with-payloads} + +Tags can have _payloads_—that is, values inside them. For example: + +```roc +stoplightColor = + if something > 100 then + Red + else if something > 0 then + Yellow + else if something == 0 then + Green + else + Custom "some other color" + +stoplightStr = + when stoplightColor is + Red -> "red" + Green | Yellow -> "not red" + Custom description -> description +``` + +This makes two changes to our earlier `stoplightColor` / `stoplightStr` example. + +1. We sometimes chose to set `stoplightColor` to be `Custom "some other color"`. When we did this, we gave the `Custom` tag a _payload_ of the string `"some other color"`. +2. We added a `Custom` tag in our `when`, with a payload which we named `description`. Because we did this, we were able to refer to `description` in the body of the branch (that is, the part after the `->`) just like a def or a function argument. + +Any tag can be given a payload like this. A payload doesn't have to be a string; we could also have said (for example) `Custom { r: 40, g: 60, b: 80 }` to specify an RGB color instead of a string. Then in our `when` we could have written `Custom record ->` and then after the `->` used `record.r`, `record.g`, and `record.b` to access the `40`, `60`, `80` values. We could also have written `Custom { r, g, b } ->` to _destructure_ the record, and then accessed these `r`, `g`, and `b` defs after the `->` instead. + +A tag can also have a payload with more than one value. Instead of `Custom { r: 40, g: 60, b: 80 }` we could write `Custom 40 60 80`. If we did that, then instead of destructuring a record with `Custom { r, g, b } ->` inside a `when`, we would write `Custom r g b ->` to destructure the values directly out of the payload. + +We refer to whatever comes before a `->` in a `when` expression as a _pattern_—so for example, in the `Custom description -> description` branch, `Custom description` would be a pattern. In programming, using patterns in branching conditionals like `when` is known as [pattern matching](https://en.wikipedia.org/wiki/Pattern_matching). You may hear people say things like "let's pattern match on `Custom` here" as a way to suggest making a `when` branch that begins with something like `Custom description ->`. + +### [Pattern Matching on Lists](#pattern-matching-on-lists) {#pattern-matching-on-lists} + +You can also pattern match on lists, like so: + +```roc +when myList is + [] -> 0 # the list is empty + [Foo, ..] -> 1 # it starts with a Foo tag + [_, ..] -> 2 # it contains at least one element, which we ignore + [Foo, Bar, ..] -> 3 # it starts with a Foo tag followed by a Bar tag + [Foo, Bar, Baz] -> 4 # it has exactly 3 elements: Foo, Bar, and Baz + [Foo, a, ..] -> 5 # its first element is Foo, and its second we name `a` + [Ok a, ..] -> 6 # it starts with an Ok containing a payload named `a` + [.., Foo] -> 7 # it ends with a Foo tag + [A, B, .., C, D] -> 8 # it has certain elements at the beginning and end +``` + +This can be both more concise and more efficient (at runtime) than calling [`List.get`](https://www.roc-lang.org/builtins/List#get) multiple times, since each call to `get` requires a separate conditional to handle the different `Result`s they return. + +> **Note:** Each list pattern can only have one `..`, which is known as the "rest pattern" because it's where the _rest_ of the list goes. + +See the [Pattern Matching example](https://www.roc-lang.org/examples/PatternMatching/README.html) which shows different ways to do pattern matching in Roc using tags, strings, and numbers. + +## [Booleans](#booleans) {#booleans} + +In many programming languages, `true` and `false` are special language keywords that refer to the two [boolean](https://en.wikipedia.org/wiki/Boolean_data_type) values. In Roc, booleans do not get special keywords; instead, they are exposed as the ordinary values `Bool.true` and `Bool.false`. + +This design is partly to keep the number of special keywords in the language smaller, but mainly to suggest how booleans are intended to be used in Roc: for [_boolean logic_](https://en.wikipedia.org/wiki/Boolean_algebra) (`&&`, `||`, and so on) as opposed to for data modeling. Tags are the preferred choice for data modeling, and having tag values be more concise than boolean values helps make this preference clear. + +As an example of why tags are encouraged for data modeling, in many languages it would be common to write a record like `{ name: "Richard", isAdmin: Bool.true }`, but in Roc it would be preferable to write something like `{ name: "Richard", role: Admin }`. At first, the `role` field might only ever be set to `Admin` or `Normal`, but because the data has been modeled using tags instead of booleans, it's much easier to add other alternatives in the future, like `Guest` or `Moderator` - some of which might also want payloads. + +## [Lists](#lists) {#lists} + +Another thing we can do in Roc is to make a _list_ of values. Here's an example: + +```roc +names = ["Sam", "Lee", "Ari"] +``` + +This is a list with three elements in it, all strings. We can add a fourth element using `List.append` like so: + +```roc +List.append names "Jess" +``` + +This returns a **new** list with `"Jess"` after `"Ari"`, and doesn't modify the original list at all. All values in Roc (including lists, but also records, strings, numbers, and so on) are immutable, meaning whenever we want to "change" them, we want to instead pass them to a function which returns some variation of what was passed in. + +### [List.map](#list-map) {#list-map} + +A common way to transform one list into another is to use `List.map`. Here's an example of how to use it: + +```roc +List.map [1, 2, 3] \num -> num * 2 +``` + +This returns `[2, 4, 6]`. + +`List.map` takes two arguments: + +1. An input list +2. A function that will be called on each element of that list + +It then returns a list which it creates by calling the given function on each element in the input list. In this example, `List.map` calls the function `\num -> num * 2` on each element in `[1, 2, 3]` to get a new list of `[2, 4, 6]`. + +We can also give `List.map` a named function, instead of an anonymous one: + +```roc +List.map [1, 2, 3] Num.isOdd +``` + +This `Num.isOdd` function returns `Bool.true` if it's given an odd number, and `Bool.false` otherwise. So `Num.isOdd 5` returns `Bool.true` and `Num.isOdd 2` returns `Bool.false`. + +As such, calling `List.map [1, 2, 3] Num.isOdd` returns a new list of `[Bool.true, Bool.false, Bool.true]`. + +### [List element type compatibility](#list-element-type-compatibility) {#list-element-type-compatibility} + +If we tried to give `List.map` a function that didn't work on the elements in the list, then we'd get an error at compile time. Here's a valid, and then an invalid example: + +```roc +# working example +List.map [-1, 2, 3, -4] Num.isNegative +# returns [Bool.true, Bool.false, Bool.false, Bool.true] +``` + +```roc +# invalid example +List.map ["A", "B", "C"] Num.isNegative +# error: isNegative doesn't work on strings! +``` + +Because `Num.isNegative` works on numbers and not strings, calling `List.map` with `Num.isNegative` and a list of numbers works, but doing the same with a list of strings doesn't work. + +This wouldn't work either: + +```roc +List.map ["A", "B", "C", 1, 2, 3] Num.isNegative +``` + +Every element in a Roc list has to share the same type. For example, we can have a list of strings like `["Sam", "Lee", "Ari"]`, or a list of numbers like `[1, 2, 3, 4, 5]` but we can't have a list which mixes strings and numbers like `["Sam", 1, "Lee", 2, 3]`, that would be a compile-time error. + +Ensuring that all elements in a list share a type eliminates entire categories of problems. For example, it means that whenever you use `List.append` to add elements to a list, as long as you don't have any compile-time errors, you won't get any runtime errors from calling `List.map` afterwards, no matter what you appended to the list! More generally, it's safe to assume that unless you run out of memory, `List.map` will run successfully unless you got a compile-time error about an incompatibility (like `Num.neg` on a list of strings). + +### [Lists that hold elements of different types](#lists-that-hold-elements-of-different-types) {#lists-that-hold-elements-of-different-types} + +We can use tags with payloads to make a list that contains a mixture of different types. For example: + +```roc +List.map [StrElem "A", StrElem "b", NumElem 1, StrElem "c", NumElem -3] \elem -> + when elem is + NumElem num -> Num.isNegative num + StrElem str -> Str.isCapitalized str +# returns [Bool.true, Bool.false, Bool.false, Bool.false, Bool.true] +``` + +Compare this with the example from earlier, which caused a compile-time error: + +```roc +List.map ["A", "B", "C", 1, 2, 3] Num.isNegative +``` + +The version that uses tags works because we aren't trying to call `Num.isNegative` on each element. Instead, we're using a `when` to tell when we've got a string or a number, and then calling either `Num.isNegative` or `Str.isCapitalized` depending on which type we have. + +We could take this as far as we like, adding more different tags (e.g. `BoolElem Bool.true`) and then adding more branches to the `when` to handle them appropriately. + +### [Using tags as functions](#using-tags-as-functions) {#using-tags-as-functions} + +Let's say I want to apply a tag to a bunch of elements in a list. For example: + +```roc +List.map ["a", "b", "c"] \str -> Foo str +``` + +This is a perfectly reasonable way to write it, but I can also write it like this: + +```roc +List.map ["a", "b", "c"] Foo +``` + +These two versions compile to the same thing. As a convenience, Roc lets you specify a tag name where a function is expected; when you do this, the compiler infers that you want a function which uses all of its arguments as the payload to the given tag. + +### [List.any and List.all](#list-any-and-list-all) {#list-any-and-list-all} + +There are several functions that work like `List.map`, they walk through each element of a list and do something with it. Another is `List.any`, which returns `Bool.true` if calling the given function on any element in the list returns `Bool.true`: + +```roc +List.any [1, 2, 3] Num.isOdd +# returns `Bool.true` because 1 and 3 are odd +``` + +```roc +List.any [1, 2, 3] Num.isNegative +# returns `Bool.false` because none of these is negative +``` + +There's also `List.all` which only returns `Bool.true` if all the elements in the list pass the test: + +```roc +List.all [1, 2, 3] Num.isOdd +# returns `Bool.false` because 2 is not odd +``` + +```roc +List.all [1, 2, 3] Num.isPositive +# returns `Bool.true` because all of these are positive +``` + +### [Removing elements from a list](#removing-elements-from-a-list) {#removing-elements-from-a-list} + +You can also drop elements from a list. One way is `List.dropAt` - for example: + +```roc +List.dropAt ["Sam", "Lee", "Ari"] 1 +# drops the element at offset 1 ("Lee") and returns ["Sam", "Ari"] +``` + +Another way is to use `List.keepIf`, which passes each of the list's elements to the given function, and then keeps them only if that function returns `Bool.true`. + +```roc +List.keepIf [1, 2, 3, 4, 5] Num.isEven +# returns [2, 4] +``` + +There's also `List.dropIf`, which does the opposite: + +```roc +List.dropIf [1, 2, 3, 4, 5] Num.isEven +# returns [1, 3, 5] +``` + +### [Getting an individual element from a list](#getting-an-individual-element-from-a-list) {#getting-an-individual-element-from-a-list} + +Another thing we can do with a list is to get an individual element out of it. `List.get` is a common way to do this; it takes a list and an index, and then returns the element at that index... if there is one. But what if there isn't? + +For example, what do each of these return? + +```roc +List.get ["a", "b", "c"] 1 +``` + +```roc +List.get ["a", "b", "c"] 100 +``` + +The answer is that the first one returns `Ok "b"` and the second one returns `Err OutOfBounds`. They both return tags! This is done so that the caller becomes responsible for handling the possibility that the index is outside the bounds of that particular list. + +Here's how calling `List.get` can look in practice: + +```roc +when List.get ["a", "b", "c"] index is + Ok str -> "I got this string: \(str)" + Err OutOfBounds -> "That index was out of bounds, sorry!" +``` + +There's also `List.first`, which always gets the first element, and `List.last` which always gets the last. They return `Err ListWasEmpty` instead of `Err OutOfBounds`, because the only way they can fail is if you pass them an empty list! + +These functions demonstrate a common pattern in Roc: operations that can fail returning either an `Ok` tag with the answer (if successful), or an `Err` tag with another tag describing what went wrong (if unsuccessful). In fact, it's such a common pattern that there's a whole module called `Result` which deals with these two tags. Here are some examples of `Result` functions: + +```roc +Result.withDefault (List.get ["a", "b", "c"] 100) "" +# returns "" because that's the default we said to use if List.get returned an Err +``` +```roc +Result.isOk (List.get ["a", "b", "c"] 1) +# returns `Bool.true` because `List.get` returned an `Ok` tag. (The payload gets ignored.) + +# Note: There's a Result.isErr function that works similarly. +``` + +### [Walking the elements in a list](#walking-the-elements-in-a-list) {#walking-the-elements-in-a-list} + +We've now seen a few different ways you can transform lists. Sometimes, though, there's nothing +that quite does what you want, and you might find yourself calling `List.get` repeatedly to +retrieve every element in the list and use it to build up the new value you want. That approach +can work, but it has a few downsides: + +* Each `List.get` call returns a `Result` that must be dealt with, even though you plan to use every element in the list anyway +* There's a runtime performance overhead associated with each of these `Result`s, which you won't find in other "look at every element in the list" operations like `List.keepIf`. +* It's more verbose than the alternative we're about to discuss + +The `List.walk` function gives you a way to walk over the elements in a list and build up whatever +return value you like. It's a great alternative to calling `List.get` on every element in the list +because it's more concise, runs faster, and doesn't give you any `Result`s to deal with. + +Here's an example: + +```roc +List.walk [1, 2, 3, 4, 5] { evens: [], odds: [] } \state, elem -> + if Num.isEven elem then + { state & evens: List.append state.evens elem } + else + { state & odds: List.append state.odds elem } + +# returns { evens: [2, 4], odds: [1, 3, 5] } +``` + +In this example, we walk over the list `[1, 2, 3, 4, 5]` and add each element to either the `evens` or `odds` field of a `state` record: `{ evens, odds }`. By the end, that record has a list of all the even numbers in the list and a list of all the odd numbers. + +`List.walk` takes a few ingredients: + +1. A list. (`[1, 2, 3, 4, 5]`) +2. An initial `state` value. (`{ evens: [], odds: [] }`) +3. A function which takes the current `state` and element, and returns a new `state`. (`\state, elem -> ...`) + +It then proceeds to walk over each element in the list and call that function. Each time, the state that function returns becomes the argument to the next function call. Here are the arguments the function will receive, and what it will return, as `List.walk` walks over the list `[1, 2, 3, 4, 5]`: + +| State | Element | Return Value | +| --------------------------------- | ------- | ------------------------------------ | +| `{ evens: [], odds: [] }` | `1` | `{ evens: [], odds: [1] }` | +| `{ evens: [], odds: [1] }` | `2` | `{ evens: [2], odds: [1] }` | +| `{ evens: [2], odds: [1] }` | `3` | `{ evens: [2], odds: [1, 3] }` | +| `{ evens: [2], odds: [1, 3] }` | `4` | `{ evens: [2, 4], odds: [1, 3] }` | +| `{ evens: [2, 4], odds: [1, 3] }` | `5` | `{ evens: [2, 4], odds: [1, 3, 5] }` | + +Note that the initial `state` argument is `{ evens: [], odds: [] }` because that's the argument +we passed `List.walk` for its initial state. From then on, each `state` argument is whatever the +previous function call returned. + +Once the list has run out of elements, `List.walk` returns whatever the final function call returned—in this case, `{ evens: [2, 4], odds: [1, 3, 5] }`. (If the list was empty, the function never gets called and `List.walk` returns the initial state.) + +Note that the state doesn't have to be a record; it can be anything you want. For example, if you made it a `Bool`, you could implement `List.any` using `List.walk`. You could also make the state be a list, and implement `List.map`, `List.keepIf`, or `List.dropIf`. There are a lot of things you can do with `List.walk`! + +A helpful way to remember the argument order for `List.walk` is that that its arguments follow the same pattern as what we've seen with `List.map`, `List.any`, `List.keepIf`, and `List.dropIf`: the first argument is a list, and the last argument is a function. The difference here is that `List.walk` has one more argument than those other functions; the only place it could go while preserving that pattern is in the middle! + +> **Note:** Other languages give this operation different names, such as `fold`, `reduce`, `accumulate`, `aggregate`, `compress`, and `inject`. Some languages also have operations like `forEach` or `for...in` syntax, which walk across every element and perform potentially side-effecting operations on them; `List.walk` can be used to replace these too, if you include a `Task` in the state. We'll talk about tasks, and how to use them with `List.walk`, later on. + +### [The pipe operator](#the-pipe-operator) {#the-pipe-operator} + +When you have nested function calls, sometimes it can be clearer to write them in a "pipelined" style using the `|>` operator. Here are three examples of writing the same expression; they all compile to exactly the same thing, but two of them use the `|>` operator to change how the calls look. + +```roc +Result.withDefault (List.get ["a", "b", "c"] 1) "" +``` +```roc +List.get ["a", "b", "c"] 1 +|> Result.withDefault "" +``` + +The `|>` operator takes the value that comes before the `|>` and passes it as the first argument to whatever comes after the `|>`. So in the example above, the `|>` takes `List.get ["a", "b", "c"] 1` and passes that value as the first argument to `Result.withDefault`, making `""` the second argument to `Result.withDefault`. + +We can take this a step further like so: + +```roc +["a", "b", "c"] +|> List.get 1 +|> Result.withDefault "" +``` + +This is still equivalent to the first expression. Since `|>` is known as the "pipe operator," we can read this as "start with `["a", "b", "c"]`, then pipe it to `List.get`, then pipe it to `Result.withDefault`." + +One reason the `|>` operator injects the value as the first argument is to make it work better with functions where argument order matters. For example, these two uses of `List.append` are equivalent: + +```roc +List.append ["a", "b", "c"] "d" +``` +```roc +["a", "b", "c"] +|> List.append "d" +``` + +Another example is `Num.div`. All three of the following do the same thing, because `a / b` in Roc is syntax sugar for `Num.div a b`: + +```roc +first / second +``` +```roc +Num.div first second +``` +```roc +first |> Num.div second +``` + +All operators in Roc are syntax sugar for normal function calls. See the [Operator Desugaring Table](https://www.roc-lang.org/tutorial#operator-desugaring-table) at the end of this tutorial for a complete list of them. + +## [Types](#types) {#types} + +Sometimes you may want to document the type of a definition. For example, you might write: + +```roc +# Takes a firstName string and a lastName string, and returns a string +fullName = \firstName, lastName -> + "\(firstName) \(lastName)" +``` + +Comments can be valuable documentation, but they can also get out of date and become misleading. If someone changes this function and forgets to update the comment, it will no longer be accurate. + +### [Type Annotations](#type-annotations) {#type-annotations} + +Here's another way to document this function's type, which doesn't have that problem: + +```roc +fullName : Str, Str -> Str +fullName = \firstName, lastName -> + "\(firstName) \(lastName)" +``` + +The `fullName :` line is a _type annotation_. It's a strictly optional piece of metadata we can add above a def to describe its type. Unlike a comment, the Roc compiler will check type annotations for accuracy. If the annotation ever doesn't fit with the implementation, we'll get a compile-time error. + +The annotation `fullName : Str, Str -> Str` says "`fullName` is a function that takes two strings as arguments and returns a string." + +We can give type annotations to any value, not just functions. For example: + +```roc +firstName : Str +firstName = "Amy" + +lastName : Str +lastName = "Lee" +``` + +These annotations say that both `firstName` and `lastName` have the type `Str`. + +We can annotate records similarly. For example, we could move `firstName` and `lastName` into a record like so: + +```roc +amy : { firstName : Str, lastName : Str } +amy = { firstName: "Amy", lastName: "Lee" } + +jen : { firstName : Str, lastName : Str } +jen = { firstName: "Jen", lastName: "Majura" } +``` + +### [Type Aliases](#type-aliases) {#type-aliases} + +When we have a recurring type annotation like this, it can be nice to give it its own name. We do this like so: + +```roc +Musician : { firstName : Str, lastName : Str } + +amy : Musician +amy = { firstName: "Amy", lastName: "Lee" } + +simone : Musician +simone = { firstName: "Simone", lastName: "Simons" } +``` + +Here, `Musician` is a _type alias_. A type alias is like a def, except it gives a name to a type instead of to a value. Just like how you can read `name : Str` as "`name` has the type `Str`," you can also read `Musician : { firstName : Str, lastName : Str }` as "`Musician` has the type `{ firstName : Str, lastName : Str }`." + +### [Type Parameters](#type-parameters) {#type-parameters} + +Annotations for lists must specify what type the list's elements have: + +```roc +names : List Str +names = ["Amy", "Simone", "Tarja"] +``` + +You can read `List Str` as "a list of strings." Here, `Str` is a _type parameter_ that tells us what type of `List` we're dealing with. `List` is a _parameterized type_, which means it's a type that requires a type parameter. There's no way to give something a type of `List` without a type parameter. You have to specify what type of list it is, such as `List Str` or `List Bool` or `List { firstName : Str, lastName : Str }`. + +### [Wildcard Types (\*)](#wildcard-type) {#wildcard-type} + +There are some functions that work on any list, regardless of its type parameter. For example, `List.isEmpty` has this type: + +```roc +isEmpty : List * -> Bool +``` + +The `*` is a _wildcard type_; a type that's compatible with any other type. `List *` is compatible with any type of `List` like `List Str`, `List Bool`, and so on. So you can call `List.isEmpty ["I am a List Str"]` as well as `List.isEmpty [Bool.true]`, and they will both work fine. + +The wildcard type also comes up with empty lists. Suppose we have one function that takes a `List Str` and another function that takes a `List Bool`. We might reasonably expect to be able to pass an empty list (that is, `[]`) to either of these functions, and we can! This is because a `[]` value has the type `List *`. It is a "list with a wildcard type parameter", or a "list whose element type could be anything." + +### [Type Variables](#type-variables) {#type-variables} + +`List.reverse` works similarly to `List.isEmpty`, but with an important distinction. As with `isEmpty`, we can call `List.reverse` on any list, regardless of its type parameter. However, consider these calls: + +```roc +strings : List Str +strings = List.reverse ["a", "b"] + +bools : List Bool +bools = List.reverse [Bool.true, Bool.false] +``` + +In the `strings` example, we have `List.reverse` returning a `List Str`. In the `bools` example, it's returning a `List Bool`. So what's the type of `List.reverse`? + +We saw that `List.isEmpty` has the type `List * -> Bool`, so we might think the type of `List.reverse` would be `reverse : List * -> List *`. However, remember that we also saw that the type of the empty list is `List *`? `List * -> List *` is actually the type of a function that always returns empty lists! That's not what we want. + +What we want is something like one of these: + +```roc +reverse : List elem -> List elem +``` + +```roc +reverse : List value -> List value +``` + +```roc +reverse : List a -> List a +``` + +Any of these will work, because `elem`, `value`, and `a` are all _type variables_. A type variable connects two or more types in the same annotation. So you can read `List elem -> List elem` as "takes a list and returns a list that has **the same element type**." Just like `List.reverse` does! + +You can choose any name you like for a type variable, but it has to be lowercase. (You may have noticed all the types we've used until now are uppercase; that is no accident! Lowercase types are always type variables, so all other named types have to be uppercase.) All three of the above type annotations are equivalent; the only difference is that we chose different names (`elem`, `value`, and `a`) for their type variables. + +You can tell some interesting things about functions based on the type parameters involved. For example, any function that returns `List *` definitely always returns an empty list. You don't need to look at the rest of the type annotation, or even the function's implementation! The only way to have a function that returns `List *` is if it returns an empty list. + +Similarly, the only way to have a function whose type is `a -> a` is if the function's implementation returns its argument without modifying it in any way. This is known as [the identity function](https://en.wikipedia.org/wiki/Identity_function). + +### [Tag Union Types](#tag-union-types) {#tag-union-types} + +We can also annotate types that include tags: + +```roc +colorFromStr : Str -> [Red, Green, Yellow] +colorFromStr = \string -> + when string is + "red" -> Red + "green" -> Green + _ -> Yellow +``` + +You can read the type `[Red, Green, Yellow]` as "a tag union of the tags `Red`, `Green`, and `Yellow`." + +Some tag unions have only one tag in them. For example: + +```roc +redTag : [Red] +redTag = Red +``` + +### [Accumulating Tag Types](#accumulating-tag-types) {#accumulating-tag-types} + +Tag union types can accumulate more tags based on how they're used. Consider this `if` expression: + +```roc +\str -> + if Str.isEmpty str then + Ok "it was empty" + else + Err ["it was not empty"] +``` + +Here, Roc sees that the first branch has the type `[Ok Str]` and that the `else` branch has the type `[Err (List Str)]`, so it concludes that the whole `if` expression evaluates to the combination of those two tag unions: `[Ok Str, Err (List Str)]`. + +This means this entire `\str -> ...` function has the type `Str -> [Ok Str, Err (List Str)]`. However, it would be most common to annotate it as `Result Str (List Str)` instead, because the `Result` type (for operations like `Result.withDefault`, which we saw earlier) is a type alias for a tag union with `Ok` and `Err` tags that each have one payload: + +```roc +Result ok err : [Ok ok, Err err] +``` + +We just saw how tag unions get combined when different branches of a conditional return different tags. Another way tag unions can get combined is through pattern matching. For example: + +```roc +when color is + Red -> "red" + Yellow -> "yellow" + Green -> "green" +``` + +Here, Roc's compiler will infer that `color`'s type is `[Red, Yellow, Green]`, because those are the three possibilities this `when` handles. + +### [Opaque Types](#opaque-types) {#opaque-types} + +A type can be defined to be opaque to hide its internal structure. This is a lot more amazing than it may seem. It can make your code more modular, robust, and easier to read: +- If a type is opaque you can modify its internal structure and be certain that no dependencies need to be updated. +- You can prevent that data needs to be checked multiple times. For example, you can create an opaque `NonEmptyList` from a `List` after you've checked it. Now all functions that you pass this `NonEmptyList` to do not need to handle the empty list case. +- Having the type `Username` in a type signature gives you more context compared to `Str`. Even if the `Username` is an opaque type for `Str`. + +You can create an opaque type with the `:=` operator. Let's make one called `Username`: + +```roc +Username := Str + +fromStr : Str -> Username +fromStr = \str -> + @Username str + +toStr : Username -> Str +toStr = \@Username str -> + str +``` + +The `fromStr` function turns a string into a `Username` by calling `@Username` on that string. The `toStr` function turns a `Username` back into a string by pattern matching `@Username str` to unwrap the string from the `Username` opaque type. + +Now we can expose the `Username` opaque type so that other modules can use it in type annotations. However, other modules can't use the `@Username` syntax to wrap or unwrap `Username` values. That operation is only available in the same scope where `Username` itself was defined; trying to use it outside that scope will give an error. + +Note that if we define `Username := Str` inside another module (e.g. `Main`) and also use `@Username`, this will compile, however the new `Username` type in main would not be equal to the one defined in the `Username` module. Although both opaque types have the name `Username`, they were defined in different modules and so they are type-incompatible with each other, and even attempting to use `==` to compare them would be a type mismatch. + +## [Numeric types](#numeric-types) {#numeric-types} + +Roc has different numeric types that each have different tradeoffs. They can all be broken down into two categories: [fractions](https://en.wikipedia.org/wiki/Fraction), and [integers](https://en.wikipedia.org/wiki/Integer). In Roc we call these `Frac` and `Int` for short. + +### [Integers](#integers) {#integers} + +Roc's integer types have two important characteristics: their _size_ and their [_signedness_](https://en.wikipedia.org/wiki/Signedness). Together, these two characteristics determine the range of numbers the integer type can represent. + +For example, the Roc type `U8` can represent the numbers 0 through 255, whereas the `I16` type can represent the numbers -32768 through 32767. You can actually infer these ranges from their names (`U8` and `I16`) alone! + +The `U` in `U8` indicates that it's _unsigned_, meaning that it can't have a minus [sign](), and therefore can't be negative. The fact that it's unsigned tells us immediately that its lowest value is zero. The 8 in `U8` means it is 8 [bits](https://en.wikipedia.org/wiki/Bit) in size, which means it has room to represent 2⁸ (=256) different numbers. Since one of those 256 different numbers is 0, we can look at `U8` and know that it goes from `0` (since it's unsigned) to `255` (2⁸ - 1, since it's 8 bits). + +If we change `U8` to `I8`, making it a _signed_ 8-bit integer, the range changes. Because it's still 8 bits, it still has room to represent 2⁸ different numbers. However, now in addition to one of those 256 numbers being zero, about half of the rest will be negative, and the others positive. So instead of ranging from, say -255 to 255 (which, counting zero, would represent 511 different numbers; too many to fit in 8 bits!) an `I8` value ranges from -128 to 127. + +Notice that the negative extreme is `-128` versus `127` (not `128`) on the positive side. That's because of needing room for zero; the slot for zero is taken from the positive range because zero doesn't have a minus sign. + +Following this pattern, the 16 in `I16` means that it's a signed 16-bit integer. That tells us it has room to represent 2¹⁶ (=65536) different numbers. Half of 65536 is 32768, so the lowest `I16` would be -32768, and the highest would be 32767. + +Choosing a size depends on your performance needs and the range of numbers you want to represent. Consider: + +- Larger integer sizes can represent a wider range of numbers. If you absolutely need to represent numbers in a certain range, make sure to pick an integer size that can hold them! +- Smaller integer sizes take up less memory. These savings rarely matters in variables and function arguments, but the sizes of integers that you use in data structures can add up. This can also affect whether those data structures fit in [cache lines](https://en.wikipedia.org/wiki/CPU_cache#Cache_performance), which can easily be a performance bottleneck. +- Certain processors work faster on some numeric sizes than others. There isn't even a general rule like "larger numeric sizes run slower" (or the reverse, for that matter) that applies to all processors. In fact, if the CPU is taking too long to run numeric calculations, you may find a performance improvement by experimenting with numeric sizes that are larger than otherwise necessary. However, in practice, doing this typically degrades overall performance, so be careful to measure properly! + +Here are the different fixed-size integer types that Roc supports: + +| Range | Type | +|-------------------------------------------------------------------------------------------------------------------|--------| +| `-128`
`127` | `I8` | +| `0`
`255` | `U8` | +| `-32_768`
`32_767` | `I16` | +| `0`
`65_535` | `U16` | +| `-2_147_483_648`
`2_147_483_647` | `I32` | +| `0`
(over 4 billion) `4_294_967_295` | `U32` | +| `-9_223_372_036_854_775_808`
`9_223_372_036_854_775_807` | `I64` | +| `0`
_(over 18 quintillion)_`18_446_744_073_709_551_615` | `U64` | +| `-170_141_183_460_469_231_731_687_303_715_884_105_728`
`170_141_183_460_469_231_731_687_303_715_884_105_727` | `I128` | +| `0`
_(over 340 undecillion)_`340_282_366_920_938_463_463_374_607_431_768_211_455` | `U128` | + +Roc also has one variable-size integer type: `Nat` (short for "natural number"). The size of `Nat` is equal to the size of a memory address, which varies by system. For example, when compiling for a 64-bit system, `Nat` works the same way as `U64`. When compiling for a 32-bit system, it works the same way as `U32`. Most popular computing devices today are 64-bit, so `Nat` is usually the same as `U64`, but Web Assembly is typically 32-bit - so when running a Roc program built for Web Assembly, `Nat` will work like a `U32` in that program. + +A common use for `Nat` is to store the length of a collection like a `List`; there's a function `List.len : List * -> Nat` which returns the length of the given list. 64-bit systems can represent longer lists in memory than 32-bit systems can, which is why the length of a list is represented as a `Nat`. + +If any operation would result in an integer that is either too big or too small to fit in that range (e.g. calling `Int.maxI32 + 1`, which adds 1 to the highest possible 32-bit integer), then the operation will [overflow](https://en.wikipedia.org/wiki/Integer_overflow). When an overflow occurs, the program will crash. + +As such, it's very important to design your integer operations not to exceed these bounds! + +### [Fractions](#fractions) {#fractions} + +Roc has three fractional types: + +- `F32`, a 32-bit [floating-point number](https://en.wikipedia.org/wiki/IEEE_754) +- `F64`, a 64-bit [floating-point number](https://en.wikipedia.org/wiki/IEEE_754) +- `Dec`, a 128-bit decimal [fixed-point number](https://en.wikipedia.org/wiki/Fixed-point_arithmetic) + +These are different from integers, they can represent numbers with fractional components, such as 1.5 and -0.123. + +`Dec` is the best default choice for representing [base-10 decimal numbers](https://en.wikipedia.org/wiki/Decimal) like [currency](https://en.wikipedia.org/wiki/Currency), because it is base-10 under the hood. In contrast, `F64` and `F32` are [base-2](https://en.wikipedia.org/wiki/Binary_number) under the hood, which can lead to decimal precision loss even when doing addition and subtraction. For example, when using `F64`, running 0.1 + 0.2 returns 0.3000000000000000444089209850062616169452667236328125, whereas when using `Dec`, 0.1 + 0.2 returns 0.3. + +`F32` and `F64` have direct hardware support on common processors today. There is no hardware support for fixed-point decimals, so under the hood, a `Dec` is an `I128`; operations on it perform [base-10 fixed-point arithmetic](https://en.wikipedia.org/wiki/Fixed-point_arithmetic) with 18 decimal places of precision. + +This means a `Dec` can represent whole numbers up to slightly over 170 quintillion, along with 18 decimal places. (To be precise, it can store numbers between `-170_141_183_460_469_231_731.687303715884105728` and `170_141_183_460_469_231_731.687303715884105727`.) Why 18 decimal places? It's the highest number of decimal places where you can still convert any `U64` to a `Dec` without losing information. + +While the fixed-point `Dec` has a fixed range, the floating-point `F32` and `F64` do not. Instead, outside of a certain range they start to lose precision instead of immediately overflowing the way integers and `Dec` do. `F64` can represent [between 15 and 17 significant digits](https://en.wikipedia.org/wiki/Double-precision_floating-point_format) before losing precision, whereas `F32` can only represent [between 6 and 9](https://en.wikipedia.org/wiki/Single-precision_floating-point_format#IEEE_754_single-precision_binary_floating-point_format:_binary32). + +There are some use cases where `F64` and `F32` can be better choices than `Dec` despite their precision drawbacks. For example, in graphical applications they can be a better choice for representing coordinates because they take up less memory, various relevant calculations run faster, and decimal precision loss isn't as big a concern when dealing with screen coordinates as it is when dealing with something like currency. + +### [Num, Int, and Frac](#num-int-and-frac) {#num-int-and-frac} + +Some operations work on specific numeric types - such as `I64` or `Dec` - but operations support multiple numeric types. For example, the `Num.abs` function works on any number, since you can take the [absolute value](https://en.wikipedia.org/wiki/Absolute_value) of integers and fractions alike. Its type is: + +```roc +abs : Num a -> Num a +``` + +This type says `abs` takes a number and then returns a number of the same type. Remember that we can see the type of number is the same because the [type variable](#type-variables) `a` is used on both sides. That's because the `Num` type is compatible with both integers and fractions. + +There's also an `Int` type which is only compatible with integers, and a `Frac` type which is only compatible with fractions. For example: + +```roc +Num.xor : Int a, Int a -> Int a +``` +```roc +Num.cos : Frac a -> Frac a +``` +When you write a number literal in Roc, it has the type `Num *`. So you could call `Num.xor 1 1` and also `Num.cos 1` and have them all work as expected; the number literal `1` has the type `Num *`, which is compatible with the more constrained types `Int` and `Frac`. For the same reason, you can pass number literals to functions expecting even more constrained types, like `I32` or `F64`. + +### [Number Literals](#number-literals) {#number-literals} + +By default, a number literal with no decimal point has the type `Num *`—that is, we know it's "a number" but nothing more specific. (Number literals with decimal points have the type `Frac *` instead.) + +You can give a number literal a more specific type by adding the type you want as a lowercase suffix. For example, `1u8` specifies `1` with the type `U8`, and `5dec` specifies `5` with the type `Dec`. + +The full list of possible suffixes includes: + +`u8`, `i8`, `u16`, `i16`, `u32`, `i32`, `u64`, `i64`, `u128`, `i128`, `nat`, `f32`, `f64`, `dec` + +Integer literals can be written in [hexadecimal](https://en.wikipedia.org/wiki/Hexadecimal) form by prefixing with `0x` followed by hexadecimal characters (`a` - `f` in addition to `0` - `9`). For example, writing `0xfe` is the same as writing `254`. Similarly, the prefix `0b` specifies binary integers. Writing `0b0000_1000` is the same as writing `8`. + +## [Crashing](#crashing) {#crashing} + +Ideally, Roc programs would never crash. However, there are some situations where they may. For example: + +1. When doing normal integer arithmetic (e.g. `x + y`) that [overflows](https://en.wikipedia.org/wiki/Integer_overflow). +2. When the system runs out of memory. +3. When a variable-length collection (like a `List` or `Str`) gets too long to be representable in the operating system's address space. (A 64-bit operating system's address space can represent several [exabytes](https://en.wikipedia.org/wiki/Byte#Multiple-byte_units) of data, so this case should not come up often.) + +Crashes in Roc are not like [try/catch exceptions](https://en.wikipedia.org/wiki/Exception_handling) found in some other programming languages. There is no way to "catch" a crash. It immediately ends the program, and what happens next is defined by the [platform](https://github.com/roc-lang/roc/wiki/Roc-concepts-explained#platform). For example, a command-line interface platform might exit with a nonzero [exit code](https://en.wikipedia.org/wiki/Exit_status), whereas a web server platform might have the current request respond with a [HTTP 500 error](https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#500). + +### [Crashing in unreachable branches](#crashing-in-unreachable-branches) {#crashing-in-unreachable-branches} + +You can intentionally crash a Roc program, for example inside a conditional branch that you believe is unreachable. Suppose you're certain that a particular `List U8` contains valid UTF-8 bytes, which means when you call `Str.fromUtf8` on it, the `Result` it returns will always be `Ok`. In that scenario, you can use the `crash` keyword to handle the `Err` case like so: + +```roc +answer : Str +answer = + when Str.fromUtf8 definitelyValidUtf8 is + Ok str -> str + Err _ -> crash "This should never happen!" +``` + +If the unthinkable happens, and somehow the program reaches this `Err` branch even though that was thought to be impossible, then it will crash - just like if the system had run out of memory. The string passed to `crash` will be provided to the platform as context; each platform may do something different with it. + +> **Note:** `crash` is a language keyword and not a function; you can't assign `crash` to a variable or pass it to a function. + +### [Crashing for TODOs](#crashing-for-todos) {#crashing-for-todos} + +Another use for `crash` is as a TODO marker when you're in the middle of building something: + +```roc +if x > y then + transmogrify (x * 2) +else + crash "TODO handle the x <= y case" +``` + +This lets you do things like write tests for the non-`crash` branch, and then come back and finish the other branch later. + +### [Crashing for error handling](#crashing-for-error-handling) {#crashing-for-error-handling} + +`crash` is not for error handling. + +The reason Roc has a `crash` keyword is for scenarios where it's expected that no error will ever happen (like in [unreachable branches](#crashing-in-unreachable-branches)), or where graceful error handling is infeasible (like running out of memory). + +Errors that are recoverable should be represented using normal Roc types (like [Result](https://www.roc-lang.org/builtins/Result)) and then handled without crashing. For example, by having the application report that something went wrong, and then continue running from there. + +## [Tests and expectations](#tests-and-expectations) {#tests-and-expectations} + +You can write automated tests for your Roc code like so: + +```roc +pluralize = \singular, plural, count -> + countStr = Num.toStr count + + if count == 1 then + "\(countStr) \(singular)" + else + "\(countStr) \(plural)" + +expect pluralize "cactus" "cacti" 1 == "1 cactus" + +expect pluralize "cactus" "cacti" 2 == "2 cacti" +``` + +If you put this in a file named `main.roc` and run `roc test`, Roc will execute the two `expect` expressions (that is, the two `pluralize` calls) and report any that returned `Bool.false`. + +If a test fails, it will not show the actual value that differs from the expected value. To show the actual value, you can write the expect like this: + +```roc +expect + funcOut = pluralize "cactus" "cacti" 1 + + funcOut == "2 cactus" +``` + +### [Inline Expectations](#inline-expects) {#inline-expects} + +Expects do not have to be at the top level: + +```roc +pluralize = \singular, plural, count -> + countStr = Num.toStr count + + if count == 1 then + "\(countStr) \(singular)" + else + expect count > 0 + + "\(countStr) \(plural)" +``` + +This `expect` will fail if you call `pluralize` passing a count of 0. + +Note that inline `expect`s do not halt the program! They are designed to inform, not to affect control flow. In fact, if you do `roc build`, they are not even included in the final binary. +So you'll want to use `roc dev` or `roc test` to get the output for `expect`. + +## [Modules](#modules) {#modules} + +Each `.roc` file is a separate module and contains Roc code for different purposes. Here are all of the different types of modules that Roc supports; + +- **Builtins** provide functions that are automatically imported into every module. +- **Applications** are combined with a platform and compiled into an executable. +- **Interfaces** provide functions which can be imported into other modules. +- **Packages** organise modules to share functionality across applications and platforms. +- **Platforms** provide effects such as IO to interface with the outside world. +- **Hosted** *note this module type is likely to be deprecated soon*. + +### [Builtin Modules](#builtin-modules) {#builtin-modules} + +There are several modules that are built into the Roc compiler, which are imported automatically into every Roc module. They are: + +1. `Bool` +2. `Str` +3. `Num` +4. `List` +5. `Result` +6. `Dict` +7. `Set` + +You may have noticed that we already used the first five. For example, when we wrote `Str.concat` and `Num.isEven`, we were referencing functions stored in the `Str` and `Num` modules. + +These modules are not ordinary `.roc` files that live on your filesystem. Rather, they are built directly into the Roc compiler. That's why they're called "builtins!" + +Besides being built into the compiler, the builtin modules are different from other modules in that: + +- They are always imported. You never need to add them to `imports`. +- All their types are imported unqualified automatically. So you never need to write `Num.Nat`, because it's as if the `Num` module was imported using `imports [Num.{ Nat }]` (the same is true for all the other types in the `Num` module. + +### [App Module Header](#app-module-header) {#app-module-header} + +Let's take a closer look at the part of `main.roc` above the `main` def: + +```roc +app "hello" + packages { pf: "https://github.com/roc-lang/basic-cli/releases/download/0.5.0/Cufzl36_SnJ4QbOoEmiJ5dIpUxBvdB3NEySvuH82Wio.tar.br" } + imports [pf.Stdout] + provides [main] to pf +``` + +This is known as a _module header_. Every `.roc` file is a _module_, and there are different types of modules. We know this particular one is an _application module_ because it begins with the `app` keyword. + +The line `app "hello"` shows that this module is a Roc application. The "hello" after the `app` keyword will be removed soon and is no longer used. If the file is named hello.roc, building this application should produce an executable named `hello`. This means when you run `roc dev`, the Roc compiler will build an executable named `hello` (or `hello.exe` on Windows) and run it. You can also build the executable without running it by running `roc build`. + +The remaining lines all involve the [platform](https://github.com/roc-lang/roc/wiki/Roc-concepts-explained#platform) this application is built on: + +```roc +packages { pf: "https://github.com/roc-lang/basic-cli/releases/download/0.5.0/Cufzl36_SnJ4QbOoEmiJ5dIpUxBvdB3NEySvuH82Wio.tar.br" } + imports [pf.Stdout] + provides [main] to pf +``` + +The `packages { pf: "https://...tar.br" }` part says three things: + +- We're going to be using a _package_ (a collection of modules) that can be downloaded from the URL `"https://...tar.br"` +- That package's [base64](https://en.wikipedia.org/wiki/Base64#URL_applications)\-encoded [BLAKE3]() cryptographic hash is the long string at the end (before the `.tar.br` file extension). Once the file has been downloaded, its contents will be verified against this hash, and it will only be installed if they match. This way, you can be confident the download was neither corrupted nor changed since it was originally published. +- We're going to name that package `pf` so we can refer to it more concisely in the future. + +The `imports [pf.Stdout]` line says that we want to import the `Stdout` module from the `pf` package, and make it available in the current module. + +This import has a direct interaction with our definition of `main`. Let's look at that again: + +```roc +main = Stdout.line "I'm a Roc application!" +``` + +Here, `main` is calling a function called `Stdout.line`. More specifically, it's calling a function named `line` which is exposed by a module named `Stdout`. + +When we write `imports [pf.Stdout]`, it specifies that the `Stdout` module comes from the package we named `pf` in the `packages { pf: ... }` section. + +If we would like to include other modules in our application, say `AdditionalModule.roc` and `AnotherModule.roc`, then they can be imported directly in `imports` like this: + +```roc +imports [pf.Stdout, AdditionalModule, AnotherModule] +``` + +You can find documentation for the `Stdout.line` function in the [Stdout](https://www.roc-lang.org/packages/basic-cli/Stdout#line) module documentation. + +### [Package Modules](#interface-modules) {#interface-modules} + +Package modules enable Roc code to be easily re-used and shared. This is achieved by organizing code into different Interface modules and then including these in the `exposes` field of the package file structure, `package "name" exposes [ MyInterface ] packages {}`. The modules that are listed in the `exposes` field are then available for use in applications, platforms, or other packages. Internal modules that are not listed will be unavailable for use outside of the package. + +See [Parser Package](https://github.com/roc-lang/roc/tree/main/examples/parser/package) for an example. + +Package documentation can be generated using the Roc cli with `roc docs /package/*.roc`. + +Build a package for distribution with `roc build --bundle .tar.br /package/main.roc`. This will create a single tarball that can then be easily shared online using a URL. + +You can import a package that is available either locally, or from a URL into a Roc application or platform. This is achieved by specifying the package in the `packages` section of the application or platform file structure. For example, `packages { .., parser: "" }` is an example that imports a parser module from a URL. + +How does the Roc cli import and download a package from a URL? + +1. First it checks to see whether the relevant folder already exists in the local filesystem and if not, creates it. If there is a package already downloaded then there is no need to download or extract anything. Packages are cached in a directory, typically `~/.cache/roc` on UNIX, and `%APPDATA%\\Roc` on Windows. +2. It then downloads the file at that URL and verifies that the hash of the file matches the hash at the end of the URL. +3. If the hash of the file matches the hash in the URL, then decompress and extract its contents into the cache folder so that it can be used. + +Why is a Roc package URL so long? + +Including the hash solves a number of problems: + +1. The package at the URL can not suddenly change and cause different behavior. +2. Because of 1. there is no need to check the URL on every compilation to see if we have the latest version. +3. If the domain of the URL expires, a malicious actor can change the package but the hash will not match so the roc cli will reject it. + +### [Interface Modules](#interface-modules) {#interface-modules} + +\[This part of the tutorial has not been written yet. Coming soon!\] + +See [Html Interface](https://github.com/roc-lang/roc/blob/main/examples/virtual-dom-wip/platform/Html.roc) for an example. + +### [Platform Modules](#interface-modules) {#interface-modules} + +\[This part of the tutorial has not been written yet. Coming soon!\] + +See [Platform Switching Rust](https://github.com/roc-lang/roc/blob/main/examples/platform-switching/rust-platform/main.roc) for an example. + +### [Importing Files](#importing-files) {#importing-files} + +You can import files directly into your module as a `Str` or a `List U8` at compile time. This is can be useful for when working with data you would like to keep in a separate file, e.g. JSON or YAML configuration. + +```roc +imports [ + "some-file" as someStr : Str, + "some-file" as someBytes : List U8, +] +``` + +See the [Ingest Files Example](https://www.roc-lang.org/examples/IngestFiles/README.html) for a demonstration on using this feature. + +## [Tasks](#tasks) {#tasks} + +Tasks are technically not part of the Roc language, but they're very common in platforms. Let's continue using the [basic-cli](https://github.com/roc-lang/basic-cli) platform we've been using up to this point as an example! + +In the `basic-cli` platform, we have four operations we can do: + +- Write a string to the terminal +- Read a string from user input +- Write a string to a file +- Read a string from a file + +We'll use these four operations to learn about tasks. + +Let's start with a basic "Hello World" program. + +```roc +app "cli-tutorial" + packages { pf: "https://github.com/roc-lang/basic-cli/releases/download/0.5.0/Cufzl36_SnJ4QbOoEmiJ5dIpUxBvdB3NEySvuH82Wio.tar.br" } + imports [pf.Stdout] + provides [main] to pf + +main = + Stdout.line "Hello, World!" +``` + +The `Stdout.line` function takes a `Str` and writes it to [standard output](). It has this type: + +```roc +Stdout.line : Str -> Task {} * +``` + +A `Task` represents an _effect_; an interaction with state outside your Roc program, such as the terminal's standard output, or a file. + +When we set `main` to be a `Task`, the task will get run when we run our program. Here, we've set `main` to be a task that writes `"Hello, World!"` to `stdout` when it gets run, so that's what our program does! + +`Task` has two type parameters: the type of value it produces when it finishes running, and any errors that might happen when running it. `Stdout.line` has the type `Task {} *` because it doesn't produce any values when it finishes (hence the `{}`) and there aren't any errors that can happen when it runs (hence the `*`). + +In contrast, `Stdin.line` produces a `Str` when it finishes reading from [standard input](). That `Str` is reflected in its type: + +```roc +Stdin.line : Task Str * +``` + +Let's change `main` to read a line from `stdin`, and then print it back out again: + +```roc +app "cli-tutorial" + packages { pf: "https://github.com/roc-lang/basic-cli/releases/download/0.5.0/Cufzl36_SnJ4QbOoEmiJ5dIpUxBvdB3NEySvuH82Wio.tar.br" } + imports [pf.Stdout, pf.Stdin, pf.Task] + provides [main] to pf + +main = + Task.await Stdin.line \text -> + Stdout.line "You just entered: \(text)" +``` + +If you run this program, at first it won't do anything. It's waiting for you to type something in and press Enter! Once you do, it should print back out what you entered. + +The `Task.await` function combines two tasks into one bigger `Task` which first runs one of the given tasks and then the other. In this case, it's combining a `Stdin.line` task with a `Stdout.line` task into one bigger `Task`, and then setting `main` to be that bigger task. + +The type of `Task.await` is: + +```roc +Task.await : Task a err, (a -> Task b err) -> Task b err +``` + +The second argument to `Task.await` is a "callback function" which runs after the first task completes. This callback function receives the output of that first task, and then returns the second task. This means the second task can make use of output from the first task, like we did in our `\text -> ...` callback function here: + +```roc +\text -> + Stdout.line "You just entered: \(text)" +``` + +Notice that, just like before, we're still building `main` from a single `Task`. This is how we'll always do it! We'll keep building up bigger and bigger `Task`s out of smaller tasks, and then setting `main` to be that one big `Task`. + +For example, we can print a prompt before we pause to read from `stdin`, so it no longer looks like the program isn't doing anything when we start it up: + +```roc +main = + Task.await (Stdout.line "Type something press Enter:") \_ -> + Task.await Stdin.line \text -> + Stdout.line "You just entered: \(text)" +``` + +This works, but we can make it a little nicer to read. Let's change it to the following: + +```roc +app "cli-tutorial" + packages { pf: "https://github.com/roc-lang/basic-cli/releases/download/0.5.0/Cufzl36_SnJ4QbOoEmiJ5dIpUxBvdB3NEySvuH82Wio.tar.br" } + imports [pf.Stdout, pf.Stdin, pf.Task.{ await }] + provides [main] to pf + +main = + await (Stdout.line "Type something press Enter:") \_ -> + await Stdin.line \text -> + Stdout.line "You just entered: \(text)" +``` + +Here we've changed how we're importing the `Task` module. Before it was `pf.Task` and now it's `pf.Task.{ await }`. The difference is that we're importing `await` in an _unqualified_ way, meaning that whenever we write `await` in this module, it will refer to `Task.await`. Now we no longer need to write `Task.` every time we want to use `await`. + +It's most common in Roc to call functions from other modules in a _qualified_ way (`Task.await`) rather than unqualified (`await`) like this, but it can be nice for a function with an uncommon name (like "await") which often gets called repeatedly across a small number of lines of code. + +Speaking of calling `await` repeatedly, if we keep calling it more and more on this code, we'll end up doing a lot of indenting. If we'd rather not indent so much, we can rewrite `task` into this style which looks different but does the same thing: + +```roc +main = + _ <- await (Stdout.line "Type something press Enter:") + text <- await Stdin.line + + Stdout.line "You just entered: \(text)" +``` + +## [Backpassing](#backpassing) {#backpassing} + +This `<-` syntax is called _backpassing_. The `<-` is a way to define an anonymous function, just like `\ ... ->` is. + +Here, we're using backpassing to define two anonymous functions. Here's one of them: + +```roc +text <- + +Stdout.line "You just entered: \(text)" +``` + +It may not look like it, but this code is defining an anonymous function! You might remember it as the anonymous function we previously defined like this: + +```roc +\text -> + Stdout.line "You just entered: \(text)" +``` + +These two anonymous functions are the same, just defined using different syntax. + +The reason the `<-` syntax is called _backpassing_ is because it both defines a function and passes that function _back_ as an argument to whatever comes after the `<-` (which in this case is `await Stdin.line`). + +Let's look at these two complete expressions side by side. They are both saying exactly the same thing, with different syntax! + +Here's the original: + +```roc +await Stdin.line \text -> + Stdout.line "You just entered: \(text)" +``` + +And here's the equivalent expression with backpassing syntax: + +```roc +text <- await Stdin.line + +Stdout.line "You just entered: \(text)" +``` + +Here's the other function we're defining with backpassing: + +```roc +_ <- +text <- await Stdin.line + +Stdout.line "You just entered: \(text)" +``` + +We could also have written that function this way if we preferred: + +```roc +_ <- + +await Stdin.line \text -> + Stdout.line "You just entered: \(text)" +``` + +This is using a mix of a backpassing function `_ <-` and a normal function `\text ->`, which is totally allowed! Since backpassing is nothing more than syntax sugar for defining a function and passing back as an argument to another function, there's no reason we can't mix and match if we like. + +That said, the typical style in which this `task` would be written in Roc is using backpassing for all the `await` calls, like we had above: + +```roc +main = + _ <- await (Stdout.line "Type something press Enter:") + text <- await Stdin.line + + Stdout.line "You just entered: \(text)" +``` + +This way, it reads like a series of instructions: + +1. First, run the `Stdout.line` task and await its completion. Ignore its output (hence the underscore in `_ <-`) +2. Next, run the `Stdin.line` task and await its completion. Name its output `text`. +3. Finally, run the `Stdout.line` task again, using the `text` value we got from the `Stdin.line` effect. + +Some important things to note about backpassing and `await`: + +- `await` is not a language keyword in Roc! It's referring to the `Task.await` function, which we imported unqualified by writing `Task.{ await }` in our module imports. (That said, it is playing a similar role here to the `await` keyword in languages that have `async`/`await` keywords, even though in this case it's a function instead of a special keyword.) +- Backpassing syntax does not need to be used with `await` in particular. It can be used with any function. +- Roc's compiler treats functions defined with backpassing exactly the same way as functions defined the other way. The only difference between `\text ->` and `text <-` is how they look, so feel free to use whichever looks nicer to you! + +See the [Task & Error Handling example](https://www.roc-lang.org/examples/Tasks/README.html) for a more detailed explanation of how to use tasks to help with error handling in a larger program. + +## [Abilities](#abilities) {#abilities} + +\[This part of the tutorial has not been written yet. Coming soon!\] + +## Examples + +Well done on making it this far! + +We've covered all of the basic syntax and features of Roc in this Tutorial. You should now have a good foundation and be ready to start writing your own applications. + +You can continue reading through more advanced topics below, or perhaps checkout some of the [Examples](/examples) for more a detailed exploration of ways to do various things. + +## [Appendix: Advanced Concepts](#appendix-advanced-concepts) {#appendix-advanced-concepts} + +Here are some concepts you likely won't need as a beginner, but may want to know about eventually. This is listed as an appendix rather than the main tutorial, to emphasize that it's totally fine to stop reading here and go build things! + +### [Open Records and Closed Records](#open-records-and-closed-records) {#open-records-and-closed-records} + +Let's say I write a function which takes a record with a `firstName` and `lastName` field, and puts them together with a space in between: + +```roc +fullName = \user -> + "\(user.firstName) \(user.lastName)" +``` + +I can pass this function a record that has more fields than just `firstName` and `lastName`, as long as it has _at least_ both of those fields (and both of them are strings). So any of these calls would work: + +- `fullName { firstName: "Sam", lastName: "Sample" }` +- `fullName { firstName: "Sam", lastName: "Sample", email: "blah@example.com" }` +- `fullName { age: 5, firstName: "Sam", things: 3, lastName: "Sample", role: Admin }` + +This `user` argument is an _open record_ - that is, a description of a minimum set of fields on a record, and their types. When a function takes an open record as an argument, it's okay if you pass it a record with more fields than just the ones specified. + +In contrast, a _closed record_ is one that requires an exact set of fields (and their types), with no additional fields accepted. + +If we add a type annotation to this `fullName` function, we can choose to have it accept either an open record or a closed record: + +```roc +# Closed record +fullName : { firstName : Str, lastName : Str } -> Str +fullName = \user -> + "\(user.firstName) \(user.lastName)" +``` + +```roc +# Open record (because of the `*`) +fullName : { firstName : Str, lastName : Str }* -> Str +fullName = \user -> + "\(user.firstName) \(user.lastName)" +``` + +The `*` in the type `{ firstName : Str, lastName : Str }*` is what makes it an open record type. This `*` is the _wildcard type_ we saw earlier with empty lists. (An empty list has the type `List *`, in contrast to something like `List Str` which is a list of strings.) + +This is because record types can optionally end in a type variable. Just like how we can have `List *` or `List a -> List a`, we can also have `{ first : Str, last : Str }*` or `{ first : Str, last : Str }a -> { first : Str, last : Str }a`. The differences are that in `List a`, the type variable is required and appears with a space after `List`; in a record, the type variable is optional, and appears (with no space) immediately after `}`. + +If the type variable in a record type is a `*` (such as in `{ first : Str, last : Str }*`), then it's an open record. If the type variable is missing, then it's a closed record. You can also specify a closed record by putting a `{}` as the type variable (so for example, `{ email : Str }{}` is another way to write `{ email : Str }`). In practice, closed records are basically always written without the `{}` on the end, but later on we'll see a situation where putting types other than `*` in that spot can be useful. + +### [Constrained Records](#constrained-records) {#constrained-records} + +The type variable can also be a named type variable, like so: + +```roc +addHttps : { url : Str }a -> { url : Str }a +addHttps = \record -> + { record & url: "https://\(record.url)" } +``` + +This function uses _constrained records_ in its type. The annotation is saying: + +- This function takes a record which has at least a `url` field, and possibly others +- That `url` field has the type `Str` +- It returns a record of exactly the same type as the one it was given + +So if we give this function a record with five fields, it will return a record with those same five fields. The only requirement is that one of those fields must be `url: Str`. + +In practice, constrained records appear in type annotations much less often than open or closed records do. + +Here's when you can typically expect to encounter these three flavors of type variables in records: + +- _Open records_ are what the compiler infers when you use a record as an argument, or when destructuring it (for example, `{ x, y } =`). +- _Closed records_ are what the compiler infers when you create a new record (for example, `{ x: 5, y: 6 }`) +- _Constrained records_ are what the compiler infers when you do a record update (for example, `{ user & email: newEmail }`) + +Of note, you can pass a closed record to a function that accepts a smaller open record, but not the reverse. So a function `{ a : Str, b : Bool }* -> Str` can accept an `{ a : Str, b : Bool, c : Bool }` record, but a function `{ a : Str, b : Bool, c : Bool } -> Str` would not accept an `{ a : Str, b : Bool }*` record. + +This is because if a function accepts `{ a : Str, b : Bool, c : Bool }`, that means it might access the `c` field of that record. So if you passed it a record that was not guaranteed to have all three of those fields present (such as an `{ a : Str, b : Bool }*` record, which only guarantees that the fields `a` and `b` are present), the function might try to access a `c` field at runtime that did not exist! + +### [Type Variables in Record Annotations](#type-variables-in-record-annotations) {#type-variables-in-record-annotations} + +You can add type annotations to make record types less flexible than what the compiler infers, but not more flexible. For example, you can use an annotation to tell the compiler to treat a record as closed when it would be inferred as open (or constrained), but you can't use an annotation to make a record open when it would be inferred as closed. + +If you like, you can always annotate your functions as accepting open records. However, in practice this may not always be the nicest choice. For example, let's say you have a `User` type alias, like so: + +```roc +User : { + email : Str, + firstName : Str, + lastName : Str, +} +``` + +This defines `User` to be a closed record, which in practice is the most common way records named `User` tend to be defined. + +If you want to have a function take a `User`, you might write its type like so: + +```roc +isValid : User -> Bool +``` + +If you want to have a function return a `User`, you might write its type like so: + +```roc +userFromEmail : Str -> User +``` + +A function which takes a user and returns a user might look like this: + +```roc +capitalizeNames : User -> User +``` + +This is a perfectly reasonable way to write all of these functions. However, I might decide that I really want the `isValid` function to take an open record; a record with _at least_ the fields of this `User` record, but possibly others as well. + +Since open records have a type variable (like `*` in `{ email : Str }*` or `a` in `{ email : Str }a -> { email : Str }a`), in order to do this I'd need to add a type variable to the `User` type alias: + +```roc +User a : { + email : Str + firstName : Str + lastName : Str +}a +``` + +Notice that the `a` type variable appears not only in `User a` but also in `}a` at the end of the record type! + +Using `User a` type alias, I can still write the same three functions, but now their types need to look different. This is what the first one would look like: + +```roc +isValid : User * -> Bool +``` + +Here, the `User *` type alias substitutes `*` for the type variable `a` in the type alias, which takes it from `{ email : Str, ... }a` to `{ email : Str, ... }*`. Now I can pass it any record that has at least the fields in `User`, and possibly others as well, which was my goal. + +```roc +userFromEmail : Str -> User {} +``` + +Here, the `User {}` type alias substitutes `{}` for the type variable `a` in the type alias, which takes it from `{ email : Str, ... }a` to `{ email : Str, ... }{}`. As noted earlier, this is another way to specify a closed record: putting a `{}` after it, in the same place that you'd find a `*` in an open record. + +> **Aside:** This works because you can form new record types by replacing the type variable with other record types. For example, `{ a : Str, b : Str }` can also be written `{ a : Str }{ b : Str }`. You can chain these more than once, e.g. `{ a : Str }{ b : Str }{ c : Str, d : Str }`. This is more useful when used with type annotations; for example, `{ a : Str, b : Str }User` describes a closed record consisting of all the fields in the closed record `User`, plus `a : Str` and `b : Str`. + +This function still returns the same record as it always did, it just needs to be annotated as `User {}` now instead of just `User`, because the `User` type alias has a variable in it that must be specified. + +The third function might need to use a named type variable: + +```roc +capitalizeNames : User a -> User a +``` + +If this function does a record update on the given user, and returns that - for example, if its definition were `capitalizeNames = \user -> { user & email: "blah" }` - then it needs to use the same named type variable for both the argument and return value. + +However, if returns a new `User` that it created from scratch, then its type could instead be: + +```roc +capitalizeNames : User * -> User {} +``` + +This says that it takes a record with at least the fields specified in the `User` type alias, and possibly others...and then returns a record with exactly the fields specified in the `User` type alias, and no others. + +These three examples illustrate why it's relatively uncommon to use open records for type aliases: it makes a lot of types need to incorporate a type variable that otherwise they could omit, all so that `isValid` can be given something that has not only the fields `User` has, but some others as well. (In the case of a `User` record in particular, it may be that the extra fields were included due to a mistake rather than on purpose, and accepting an open record could prevent the compiler from raising an error that would have revealed the mistake.) + +That said, this is a useful technique to know about if you want to (for example) make a record type that accumulates more and more fields as it progresses through a series of operations. + +### [Open and Closed Tag Unions](#open-and-closed-tag-unions) {#open-and-closed-tag-unions} + +Just like how Roc has open records and closed records, it also has open and closed tag unions. + +The _open tag union_ (or _open union_ for short) `[Foo Str, Bar Bool]*` represents a tag that might be `Foo Str` and might be `Bar Bool`, but might also be some other tag whose type isn't known at compile time. + +Because an open union represents possibilities that are impossible to know ahead of time, any `when` I use on a `[Foo Str, Bar Bool]*` value must include a catch-all `_ ->` branch. Otherwise, if one of those unknown tags were to come up, the `when` would not know what to do with it! For example: + +```roc +example : [Foo Str, Bar Bool]* -> Bool +example = \tag -> + when tag is + Foo str -> Str.isEmpty str + Bar bool -> bool + _ -> Bool.false +``` + +In contrast, a _closed tag union_ (or _closed union_) like `[Foo Str, Bar Bool]` (without the `*`) represents the set of all possible tags. If I use a `when` on one of these, I can match on `Foo` only and then on `Bar` only, with no need for a catch-all branch. For example: + +```roc +example : [Foo Str, Bar Bool] -> Bool +example = \tag -> + when tag is + Foo str -> Str.isEmpty str + Bar bool -> bool +``` + +If we were to remove the type annotations from the previous two code examples, Roc would infer the same types for them anyway. + +It would infer `tag : [Foo Str, Bar Bool]` for the latter example because the `when tag is` expression only includes a `Foo Str` branch and a `Bar Bool` branch, and nothing else. Since the `when` doesn't handle any other possibilities, these two tags must be the only possible ones. + +It would infer `tag : [Foo Str, Bar Bool]*` for the former example because the `when tag is` expression includes a `Foo Str` branch and a `Bar Bool` branch but also a `_ ->` branch, indicating that there may be other tags we don't know about. Since the `when` is flexible enough to handle all possible tags, `tag` gets inferred as an open union. + +Putting these together, whether a tag union is inferred to be open or closed depends on which possibilities the implementation actually handles. + +> **Aside:** As with open and closed records, we can use type annotations to make tag union types less flexible than what would be inferred. If we added a `_ ->` branch to the second example above, the compiler would still accept `example : [Foo Str, Bar Bool] -> Bool` as the type annotation, even though the catch-all branch would permit the more flexible `example : [Foo Str, Bar Bool]* -> Bool` annotation instead. + +### [Combining Open Unions](#combining-open-unions) {#combining-open-unions} + +When we make a new record, it's inferred to be a closed record. For example, in `foo { a: "hi" }`, the type of `{ a: "hi" }` is inferred to be `{ a : Str }`. In contrast, when we make a new tag, it's inferred to be an open union. So in `foo (Bar "hi")`, the type of `Bar "hi"` is inferred to be `[Bar Str]*`. + +This is because open unions can accumulate additional tags based on how they're used in the program, whereas closed unions cannot. For example, let's look at this conditional: + +```roc +if x > 5 then + "foo" +else + 7 +``` + +This will be a type mismatch because the two branches have incompatible types. Strings and numbers are not type-compatible! Now let's look at another example: + +```roc +if x > 5 then + Ok "foo" +else + Err "bar" +``` + +This shouldn't be a type mismatch, because we can see that the two branches are compatible; they are both tags that could easily coexist in the same tag union. But if the compiler inferred the type of `Ok "foo"` to be the closed union `[Ok Str]`, and likewise for `Err "bar"` and `[Err Str]`, then this would have to be a type mismatch - because those two closed unions are incompatible. + +Instead, the compiler infers `Ok "foo"` to be the open union `[Ok Str]*`, and `Err "bar"` to be the open union `[Err Str]*`. Then, when using them together in this conditional, the inferred type of the conditional becomes `[Ok Str, Err Str]*` - that is, the combination of the unions in each of its branches. (Branches in a `when` work the same way with open unions.) + +Earlier we saw how a function which accepts an open union must account for more possibilities, by including catch-all `_ ->` patterns in its `when` expressions. So _accepting_ an open union means you have more requirements. In contrast, when you already _have_ a value which is an open union, you have fewer requirements. A value which is an open union (like `Ok "foo"`, which has the type `[Ok Str]*`) can be provided to anything that's expecting a tag union (no matter whether it's open or closed), as long as the expected tag union includes at least the tags in the open union you're providing. + +So if I have an `[Ok Str]*` value, I can pass it to functions with any of these types (among others): + +| Function Type | Can it receive `[Ok Str]*`? | +| --------------------------------------- | --------------------------- | +| `[Ok Str]* -> Bool` | Yes | +| `[Ok Str] -> Bool` | Yes | +| `[Ok Str, Err Bool]* -> Bool` | Yes | +| `[Ok Str, Err Bool] -> Bool` | Yes | +| `[Ok Str, Err Bool, Whatever]* -> Bool` | Yes | +| `[Ok Str, Err Bool, Whatever] -> Bool` | Yes | +| `Result Str Bool -> Bool` | Yes | +| `[Err Bool, Whatever]* -> Bool` | Yes | + +That last one works because a function accepting an open union can accept any unrecognized tag (including `Ok Str`) even though it is not mentioned as one of the tags in `[Err Bool, Whatever]*`! Remember, when a function accepts an open tag union, any `when` branches on that union must include a catch-all `_ ->` branch, which is the branch that will end up handling the `Ok Str` value we pass in. + +However, I could not pass an `[Ok Str]*` to a function with a _closed_ tag union argument that did not mention `Ok Str` as one of its tags. So if I tried to pass `[Ok Str]*` to a function with the type `[Err Bool, Whatever] -> Str`, I would get a type mismatch - because a `when` in that function could be handling the `Err Bool` possibility and the `Whatever` possibility, and since it would not necessarily have a catch-all `_ ->` branch, it might not know what to do with an `Ok Str` if it received one. + +> **Note:** It wouldn't be accurate to say that a function which accepts an open union handles "all possible tags." For example, if I have a function `[Ok Str]* -> Bool` and I pass it `Ok 5`, that will still be a type mismatch. If you think about it, a `when` in that function might have the branch `Ok str ->` which assumes there's a string inside that `Ok`, and if `Ok 5` type-checked, then that assumption would be false and things would break! +> +> So `[Ok Str]*` is more restrictive than `[]*`. It's basically saying "this may or may not be an `Ok` tag, but if it is an `Ok` tag, then it's guaranteed to have a payload of exactly `Str`." + +In summary, here's a way to think about the difference between open unions in a value you have, compared to a value you're accepting: + +- If you _have_ a closed union, that means it has all the tags it ever will, and can't accumulate more. +- If you _have_ an open union, that means it can accumulate more tags through conditional branches. +- If you _accept_ a closed union, that means you only have to handle the possibilities listed in the union. +- If you _accept_ an open union, that means you have to handle the possibility that it has a tag you can't know about. + +### [Type Variables in Tag Unions](#type-variables-in-tag-unions) {#type-variables-in-tag-unions} + +Earlier we saw these two examples, one with an open tag union and the other with a closed one: + +```roc +example : [Foo Str, Bar Bool]* -> Bool +example = \tag -> + when tag is + Foo str -> Str.isEmpty str + Bar bool -> bool + _ -> Bool.false +``` + +```roc +example : [Foo Str, Bar Bool] -> Bool +example = \tag -> + when tag is + Foo str -> Str.isEmpty str + Bar bool -> bool +``` + +Similarly to how there are open records with a `*`, closed records with nothing, and constrained records with a named type variable, we can also have _constrained tag unions_ with a named type variable. Here's an example: + +```roc +example : [Foo Str, Bar Bool]a -> [Foo Str, Bar Bool]a +example = \tag -> + when tag is + Foo str -> Bar (Str.isEmpty str) + Bar bool -> Bar Bool.false + other -> other +``` + +This type says that the `example` function will take either a `Foo Str` tag, or a `Bar Bool` tag, or possibly another tag we don't know about at compile time and it also says that the function's return type is the same as the type of its argument. + +So if we give this function a `[Foo Str, Bar Bool, Baz (List Str)]` argument, then it will be guaranteed to return a `[Foo Str, Bar Bool, Baz (List Str)]` value. This is more constrained than a function that returned `[Foo Str, Bar Bool]*` because that would say it could return _any_ other tag (in addition to the `Foo Str` and `Bar Bool` we already know about). + +If we removed the type annotation from `example` above, Roc's compiler would infer the same type anyway. This may be surprising if you look closely at the body of the function, because: + +- The return type includes `Foo Str`, but no branch explicitly returns `Foo`. Couldn't the return type be `[Bar Bool]a` instead? +- The argument type includes `Bar Bool` even though we never look at `Bar`'s payload. Couldn't the argument type be inferred to be `Bar *` instead of `Bar Bool`, since we never look at it? + +The reason it has this type is the `other -> other` branch. Take a look at that branch, and ask this question: "What is the type of `other`?" There has to be exactly one answer! It can't be the case that `other` has one type before the `->` and another type after it; whenever you see a named value in Roc, it is guaranteed to have the same type everywhere it appears in that scope. + +For this reason, any time you see a function that only runs a `when` on its only argument, and that `when` includes a branch like `x -> x` or `other -> other`, the function's argument type and return type must necessarily be equivalent. + +> **Note:** Just like with records, you can also replace the type variable in tag union types with a concrete type. For example, `[Foo Str][Bar Bool][Baz (List Str)]` is equivalent to `[Foo Str, Bar Bool, Baz (List Str)]`. +> +> Also just like with records, you can use this to compose tag union type aliases. For example, you can write `NetworkError : [Timeout, Disconnected]` and then `Problem : [InvalidInput, UnknownFormat]NetworkError` + +### [Record Builder](#record-builder) {#record-builder} + +The record builder syntax sugar is a useful feature which leverages the functional programming concept of [applicative functors](https://lucamug.medium.com/functors-applicatives-and-monads-in-pictures-784c2b5786f7), to provide a flexible method for constructing complex types. + +The record builder syntax sugar helps to build up a record by applying a series of functions to it. + +For example, let's say we write a record-builder as follows: + +```roc +{ aliceID, bobID, trudyID } = + initIDCount { + aliceID: <- incID, + bobID: <- incID, + trudyID: <- incID, + } |> extractState +``` + +The above desguars to the following. + +```roc +{ aliceID, bobID, trudyID } = + initIDCount (\aID -> \bID -> \cID -> { aliceID: aID, bobID: bID, trudyID: cID }) + |> incID + |> incID + |> incID + |> extractState +``` + +See the [Record Builder Example](https://www.roc-lang.org/examples/RecordBuilder/README.html) for an explanation of how to use this feature. + +### [Reserved Keywords](#reserved-keywords) {#reserved-keywords} + +These are all the reserved keywords in Roc. You can't choose any of these as names, except as record field names. + +`if`, `then`, `else`, `when`, `as`, `is`, `dbg`, `expect`, `expect-fx`, `crash`, `interface`, `app`, `package`, `platform`, `hosted`, `exposes`, `imports`, `with`, `generates`, `packages`, `requires`, `provides`, `to` + +### [Operator Desugaring Table](#operator-desugaring-table) {#operator-desugaring-table} + +Here are various Roc expressions involving operators, and what they desugar to. + +| Expression | Desugars To | +| ----------------------------- | ------------------ | +| `a + b` | `Num.add a b` | +| `a - b` | `Num.sub a b` | +| `a * b` | `Num.mul a b` | +| `a / b` | `Num.div a b` | +| `a // b` | `Num.divTrunc a b` | +| `a ^ b` | `Num.pow a b` | +| `a % b` | `Num.rem a b` | +| `-a` | `Num.neg a` | +| `a == b` | `Bool.isEq a b` | +| `a != b` | `Bool.isNotEq a b` | +| `a && b` | `Bool.and a b` | +| a \|\| b | `Bool.or a b` | +| `!a` | `Bool.not a` | +| a \|> b | `b a` | +| a b c \|> f x y | `f (a b c) x y` | + + +
+ diff --git a/www/main.roc b/www/main.roc new file mode 100644 index 0000000000..6e98b49afc --- /dev/null +++ b/www/main.roc @@ -0,0 +1,173 @@ +app "roc-website" + packages { pf: "../examples/static-site-gen/platform/main.roc" } + imports [ + pf.Html.{ Node, html, head, body, header, footer, div, span, main, text, nav, a, link, meta, script, br }, + pf.Html.Attributes.{ attribute, content, name, id, href, rel, lang, class, title, charset, color, ariaLabel, ariaHidden, type }, + InteractiveExample, + ] + provides [transformFileContent] to pf + +pageData = + Dict.empty {} + |> Dict.insert "community.html" { title: "Roc Community", description: "Connect with the Roc programming language community" } + |> Dict.insert "docs.html" { title: "Roc Docs", description: "Documentation for the Roc programming language, including builtins" } + |> Dict.insert "index.html" { title: "The Roc Programming Language", description: "A fast, friendly, functional language" } + |> Dict.insert "install.html" { title: "Install Roc", description: "Install the Roc programming language" } + |> Dict.insert "donate.html" { title: "Donate to Roc", description: "Support the Roc programming language by donating or sponsoring" } + |> Dict.insert "tutorial.html" { title: "Roc Tutorial", description: "Learn the Roc programming language" } + +getPage : Str -> { title : Str, description : Str } +getPage = \current -> + Dict.get pageData current + |> Result.withDefault { title: "", description: "" } + +getTitle : Str -> Str +getTitle = \current -> + getPage current |> .title + +getDescription : Str -> Str +getDescription = \current -> + getPage current |> .description + +transformFileContent : Str, Str -> Str +transformFileContent = \page, htmlContent -> + Html.render (view page htmlContent) + +preloadWoff2 : Str -> Node +preloadWoff2 = \url -> + link [ + rel "preload", + (attribute "as") "font", + type "font/woff2", + href url, + # Necessary for preloading fonts, even if the request won't be cross-origin + # https://stackoverflow.com/a/70878420 + (attribute "crossorigin") "anonymous", + ] + +view : Str, Str -> Html.Node +view = \page, htmlContent -> + mainBody = + if page == "index.html" then + when Str.splitFirst htmlContent "" is + Ok { before, after } -> [text before, InteractiveExample.view, text after] + Err NotFound -> crash "Could not find the comment where the larger example on the homepage should have been inserted. Was it removed or edited?" + else + [text htmlContent] + + bodyAttrs = + when page is + "index.html" -> [id "homepage-main"] + "tutorial.html" -> [id "tutorial-main", class "article-layout"] + _ -> + if Str.startsWith page "examples/" && page != "examples/index.html" then + # Individual examples should render wider than articles. + # Otherwise the width is unreasonably low for the code blocks, + # and those pages don't tend to have big paragraphs anyway. + # Keep the article width on examples/index.html though, + # because otherwise when you're clicking through the top nav links, + # /examples has a surprisingly different width from the other links. + [id "example-main"] + else + [class "article-layout"] + + html [lang "en", class "no-js"] [ + head [] [ + meta [charset "utf-8"], + Html.title [] [text (getTitle page)], + meta [name "description", content (getDescription page)], + meta [name "viewport", content "width=device-width"], + link [rel "icon", href "/favicon.svg"], + # Preload the latin-regular (but not latin-ext) unicode ranges of our fonts. + # The homepage doesn't actually use latin-ext + preloadWoff2 "/fonts/lato-v23-latin/lato-v23-latin-regular.woff2", + preloadWoff2 "/fonts/source-code-pro-v22-latin/source-code-pro-v22-latin-regular.woff2", + preloadWoff2 "/fonts/permanent-marker-v16-latin/permanent-marker-v16-latin-regular.woff2", + link [rel "prefetch", href "/repl/roc_repl_wasm.js"], + link [rel "stylesheet", href "/site.css"], + # Safari ignores rel="icon" and only respects rel="mask-icon". It will render the SVG with + # fill="#000" unless this `color` attribute here is hardcoded (not a CSS `var()`) to override it. + link [rel "mask-icon", href "/favicon.svg", color "#7d59dd"], + # Remove the .no-js class from before the body renders, so anything + # hidden via CSS using a .no-js selector will apply to the initial layout + # of the body instead of having a flash of content that immediately gets hidden. + # + # WARNING: Updating this requires updating its sha256 in netlify.toml under Content-Security-Policy. + # Otherwise, this will work locally and then fail in production! + script [] [text "document.documentElement.className = document.documentElement.className.replace('no-js', '');"], + ], + body bodyAttrs [ + viewNavbar page, + main [] mainBody, + footer [] [ + div [id "footer"] [ + div [id "gh-link"] [ + a [id "gh-centered-link", href "https://github.com/roc-lang/roc"] [ + ghLogo, + span [id "gh-link-text"] [text "roc-lang/roc"], + ], + ], + br [] [], + text " powered by ", + a [href "https://www.netlify.com"] [text "Netlify"], + ], + ], + ], + ] + +viewNavbar : Str -> Html.Node +viewNavbar = \page -> + isHomepage = page == "index.html" + + homeLinkAttrs = + [id "nav-home-link", href "/", title "The Roc Programming Language Homepage"] + |> List.concat (if isHomepage then [ariaHidden "true"] else []) + + header [id "top-bar"] [ + nav [ariaLabel "primary"] [ + a homeLinkAttrs [rocLogo, span [class "home-link-text"] [text "Roc"]], + div [id "top-bar-links"] [ + a [href "/tutorial"] [text "tutorial"], + a [href "/install"] [text "install"], + a [href "/examples"] [text "examples"], + a [href "/community"] [text "community"], + a [href "/docs"] [text "docs"], + a [href "/donate"] [text "donate"], + ], + ], + ] + +rocLogo : Html.Node +rocLogo = + (Html.element "svg") + [ + (Html.attribute "viewBox") "0 -6 51 58", + (Html.attribute "xmlns") "http://www.w3.org/2000/svg", + (Html.attribute "aria-labelledby") "logo-link", + (Html.attribute "role") "img", + class "roc-logo", + ] + [ + (Html.element "title") [id "logo-link"] [text "Return to Roc Home"], + (Html.element "polygon") + [ + (Html.attribute "role") "presentation", + (Html.attribute "points") "0,0 23.8834,3.21052 37.2438,19.0101 45.9665,16.6324 50.5,22 45,22 44.0315,26.3689 26.4673,39.3424 27.4527,45.2132 17.655,53 23.6751,22.7086", + ] + [], + ] + +ghLogo = + (Html.element "svg") + [ + (Html.attribute "viewBox") "0 0 98 96", + (Html.attribute "height") "25", + (Html.attribute "xmlns") "http://www.w3.org/2000/svg", + (Html.attribute "fill-rule") "evenodd", + (Html.attribute "clip-rule") "evenodd", + (Html.attribute "role") "img", + id "gh-logo", + ] + [ + (Html.element "path") [(Html.attribute "d") "M48.854 0C21.839 0 0 22 0 49.217c0 21.756 13.993 40.172 33.405 46.69 2.427.49 3.316-1.059 3.316-2.362 0-1.141-.08-5.052-.08-9.127-13.59 2.934-16.42-5.867-16.42-5.867-2.184-5.704-5.42-7.17-5.42-7.17-4.448-3.015.324-3.015.324-3.015 4.934.326 7.523 5.052 7.523 5.052 4.367 7.496 11.404 5.378 14.235 4.074.404-3.178 1.699-5.378 3.074-6.6-10.839-1.141-22.243-5.378-22.243-24.283 0-5.378 1.94-9.778 5.014-13.2-.485-1.222-2.184-6.275.486-13.038 0 0 4.125-1.304 13.426 5.052a46.97 46.97 0 0 1 12.214-1.63c4.125 0 8.33.571 12.213 1.63 9.302-6.356 13.427-5.052 13.427-5.052 2.67 6.763.97 11.816.485 13.038 3.155 3.422 5.015 7.822 5.015 13.2 0 18.905-11.404 23.06-22.324 24.283 1.78 1.548 3.316 4.481 3.316 9.126 0 6.6-.08 11.897-.08 13.526 0 1.304.89 2.853 3.316 2.364 19.412-6.52 33.405-24.935 33.405-46.691C97.707 22 75.788 0 48.854 0z"] [], + ] diff --git a/www/netlify.sh b/www/netlify.sh index 20ea56bfc0..ad949d18ff 100644 --- a/www/netlify.sh +++ b/www/netlify.sh @@ -1,24 +1,17 @@ -#!/bin/bash +#!/usr/bin/env bash # Runs on every Netlify build, to set up the Netlify server. +# https://vaneyckt.io/posts/safer_bash_scripts_with_set_euxo_pipefail/ set -euxo pipefail rustup update rustup default stable -rustup target add wasm32-unknown-unknown wasm32-wasi -ZIG_DIRNAME="zig-linux-x86_64-0.9.1" -wget https://ziglang.org/download/0.9.1/${ZIG_DIRNAME}.tar.xz +ZIG_DIRNAME="zig-linux-x86_64-0.11.0" +wget https://ziglang.org/download/0.11.0/${ZIG_DIRNAME}.tar.xz tar --extract --xz --file=${ZIG_DIRNAME}.tar.xz PATH="$(pwd)/${ZIG_DIRNAME}:${PATH}" -# Work around an issue with wasm-pack where it fails to install wasm-opt (from binaryen) on some CI systems -# https://github.com/rustwasm/wasm-pack/issues/864 -BINARYEN_DIRNAME="binaryen-version_108-x86_64-linux" -wget https://github.com/WebAssembly/binaryen/releases/download/version_108/${BINARYEN_DIRNAME}.tar.gz -tar --extract --gzip --file=${BINARYEN_DIRNAME}.tar.gz -PATH="$(pwd)/${BINARYEN_DIRNAME}/bin:${PATH}" - export PATH bash build.sh diff --git a/www/netlify.toml b/www/netlify.toml index 640b5268f9..c4d63e8333 100644 --- a/www/netlify.toml +++ b/www/netlify.toml @@ -14,11 +14,47 @@ [headers.values] X-Frame-Options = "DENY" X-XSS-Protection = "1; mode=block" + X-Content-Type-Options = "nosniff" + # Firefox prefetch requires some cache-control to be set + # See https://bugzilla.mozilla.org/show_bug.cgi?id=1527334 + Cache-Control = "public, max-age=1200" + +[[headers]] + for = "/packages/*" + [headers.values] + X-Frame-Options = "DENY" + X-XSS-Protection = "1; mode=block" + X-Content-Type-Options = "nosniff" # unsafe-eval is needed for wasm compilation in the repl to work on Safari and Chrome; # otherwise they block it. # TODO figure out how to tell Netlify to apply that policy only to the repl, not to everything. - Content-Security-Policy = "default-src 'self'; img-src *; script-src 'self' 'unsafe-eval';" - X-Content-Type-Options = "nosniff" + # + # This style-src hash is to permit the - + + + diff --git a/www/public/index.html b/www/public/index.html deleted file mode 100644 index b8b5835b99..0000000000 --- a/www/public/index.html +++ /dev/null @@ -1,52 +0,0 @@ - - - - - - - - The Roc Programming Language - - - - - - - -

Work in Progress

-

Roc's initial release is still under development, and this website is a placeholder until that release is ready. -

-

In the meantime, if you'd like to learn more about Roc, here are some videos:

- -

To set clear expectations around Roc's readiness for serious use - it's not ready yet! - the repository where it's - developed is private for now.

-

Anyone can get access to the repo in this way, or - you can download a zip of the source code, although the zip isn't updated very - often and is - definitely behind what's in the repo.

- - - diff --git a/www/public/logo.svg b/www/public/logo.svg deleted file mode 100644 index bb673d5013..0000000000 --- a/www/public/logo.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/www/public/site.css b/www/public/site.css new file mode 100644 index 0000000000..97591f815b --- /dev/null +++ b/www/public/site.css @@ -0,0 +1,1542 @@ +:root { + /* WCAG AAA Compliant colors */ + --code-bg: #f4f8f9; + --gray-bg: #f4f8f9; + --gray: #717171; + --orange: #bf5000; + --green: #0b8400; + --light-cyan: #8af4e6; + --dark-cyan: #4eefd9; + --blue: #05006d; + --violet: #7c38f5; + --violet-bg: #ece2fd; + --magenta: #ff32cf; + + --primary-1: #9b6bf2; + --primary-2: #7c38f5; + --highlight: #1bd6bd; + --code-color: white; + --link-color: var(--primary-2); + --code-link-color: var(--primary-2); + --text-color: #000; + --heading-color: #333; + --text-hover-color: var(--primary-2); + --body-bg-color: #ffffff; + --border-color: #717171; + --faded-color: #4c4c4c; + --font-sans: -apple-system, BlinkMacSystemFont, Roboto, Helvetica, Arial, + sans-serif; + --font-mono: "Source Code Pro", SFMono-Regular, Consolas, "Liberation Mono", + Menlo, Courier, monospace; + --top-header-height: 67px; + --sidebar-width: 280px; + + --font-size-normal: 18px; + --body-max-width: 1024px; + --dark-code-bg: #202746; + + /* Tutorial */ + --header-link-color: #1bbcb3; + --header-link-hover: #222; + --h1-color: #8055e4; +} + +html { + line-height: 1.5rem; + background: var(--body-bg-color); + color: var(--text-color); + font-family: "Lato", sans-serif; +} + +html, +body { + margin: 0; + padding: 0; + width: 100%; + height: 100%; + box-sizing: border-box; + overflow-x: hidden; /* This shouldn't be necessary, but without it mobile has a right gutter. */ +} + +p { + margin-top: 0; +} + +p, +li { + max-width: 720px; +} + +footer { + width: 100%; + color: var(--text-color); + text-align: center; + font-size: var(--font-size-normal); + padding: 20px; + box-sizing: border-box; + margin-top: 24px; +} + +#footer { + max-width: var(--body-max-width); + margin: 0 auto; +} + +hr { + color: var(--primary-1); + margin-bottom: 1rem; +} + +.svg-text { + fill: #000; +} + +.logo-dark { + fill: #612bde; +} + +.logo-light { + fill: #8257e5; +} + +.btn-small { + white-space: nowrap; + background: #7c38f5; + border: 4px solid #9b6bf2; + color: #fff !important; + cursor: pointer; + text-decoration: none !important; + padding: 12px; +} + +.btn-small .roc-logo { + fill: #fff; + position: relative; + left: -4px; + top: 11px; +} + +.btn-small:hover { + background: #9b6bf2; + border-color: #7c38f5; +} + +#individual-sponsors { + list-style-type: none; + padding: 24px 40px; + max-width: 720px; +} + +#individual-sponsors li { + display: inline; + white-space: nowrap; + margin: 0.2rem; +} + +#individual-sponsors li::after { + content: ","; + white-space: pre; /* Preserve the space after the comma */ +} + +#individual-sponsors li:last-child::after { + content: ""; /* No comma after the last one */ +} + +#sponsor-logos { + padding: 24px 36px; + padding-bottom: 36px; + min-width: 308px; /* Widest logo plus padding - Firefox on Android needs this */ + max-width: none !important; +} + +#sponsor-logos svg { + height: 64px; + margin-right: 72px; + margin-top: 32px; +} + +#sponsor-logos .logo-rwx { + position: relative; + top: 6px; +} + +#sponsor-logos .logo-tweede-golf { + position: relative; + top: 14px; + height: 4.5rem; +} + +#sponsor-logos + p { + margin-bottom: 3em; +} + +/* Used for e.g. displaying the instruction "Click" on desktop and "Touch" on mobile. + * When we think we're on mobile (based on max-width), we can switch the instruction. +*/ +.desktop { + display: inline; +} + +.mobile { + display: none; +} + +section p:last-child { + margin-bottom: 0; +} + +aside { + margin-left: 4rem; +} + +a { + text-decoration: none; + color: var(--link-color); +} + +a:hover { + text-decoration: underline; +} + +a:hover code { + text-decoration: inherit; +} + +li { + margin-bottom: 0.5rem; +} + +h1, +h2, +h3, +h4 { + font-weight: bold; +} + +h1 { + font-size: 5rem; +} + +h2 { + display: inline-block; + font-size: 2.5rem; + line-height: 5rem; + border-bottom: 4px solid var(--dark-cyan); + padding: 0; + margin: 0; + margin-bottom: 2rem; + color: var(--heading-color); +} + +.article-layout main, +.article-layout pre { + max-width: 720px; +} + +.article-layout p, +.article-layout li, +.article-layout pre { + font-size: 20px; +} + +#homepage-main h2 { + margin-top: 60px; /* On the homepage, these need to be spaced out more. */ +} + +#homepage-main #nav-home-link { + visibility: hidden; +} + +h2 a, +h3 a { + color: var(--heading-color); +} + +h2:hover a { + color: var(--link-color); + text-decoration: none; +} + +h3 { + font-size: 1.5rem; +} + +#top-bar, +#top-bar nav { + background-color: var(--gray-bg); +} + +#top-bar { + box-sizing: border-box; + width: 100%; +} + +#top-bar nav { + max-width: var(--body-max-width); + margin: 0 auto; + display: flex; + justify-content: space-between; + padding-right: 9px; +} + +#nav-home-link { + display: inline-block; + color: var(--top-bar-fg); + font-size: 1.8rem; + padding: 4px; +} + +#tutorial-toc-toggle:checked + #tutorial-toc { + display: block; +} + +.home-link-text { + padding: 8px; + font-size: 24px; + position: relative; + top: -0.6rem; +} + +.home-examples-title { + margin-bottom: 4px; +} + +#top-bar-links a, +#top-bar-links label { + box-sizing: border-box; + color: var(--top-bar-fg); + display: inline-block; + padding: 12px 16px; + margin: 0 2px; +} + +main { + max-width: var(--body-max-width); + margin: auto; + padding: 12px; + box-sizing: border-box; +} + +.welcome-to-roc { + white-space: nowrap; + overflow-x: hidden; + padding-right: 60px; + margin-bottom: 12px; +} + +code, +samp { + font-family: var(--font-mono); + color: var(--text-color); + background-color: var(--gray-bg); + display: inline-block; + padding: 5px; +} + +p code, +td code, +li code, +th code { + padding: 0 8px; +} + +code a, +a code { + text-decoration: none; + color: var(--code-link-color); + background: none; + padding: 0; +} + +code a:visited, +a:visited code { + color: var(--code-link-color); +} + +pre { + position: relative; + margin-bottom: 16px; + padding: 0 0.35rem; + box-sizing: border-box; + background-color: var(--gray-bg); + overflow-x: hidden; + word-wrap: normal; + font-size: var(--font-size-normal); + line-height: 1.76em; + white-space: pre; + background-color: var(--dark-code-bg); +} + +pre > samp, +pre > code { + overflow-x: auto; + display: block; + background-color: var(--dark-code-bg); + color: var(--code-color); +} + +/* The repl won't work at all if you have JS disabled. */ +.no-js #try-roc { + display: none !important; +} + +#homepage-repl-container { + display: flex; + flex-direction: row-reverse; +} + +#homepage-repl-container #repl-description { + padding: 0 30px; + margin-top: 2px; + flex: 1; +} + +#homepage-repl-container #repl-description a { + color: inherit; + text-decoration: underline; +} + +#homepage-repl-container #repl-description a:hover { + color: var(--primary-1); +} + +#homepage-repl-container #repl { + flex: 1; + border: 2px solid #444; + font-size: var(--font-size-normal); + min-height: 0; /* On /repl on mobile, this expands to fill the screen, which we don't want */ + margin-right: 6px; + max-width: 50%; +} + +#homepage-repl-container #repl, +#homepage-repl-container #repl code, +#homepage-repl-container #source-input { + color: white; + background-color: var(--dark-code-bg); +} + +#homepage-repl-container #source-input { + margin-bottom: 0; + margin-top: 6px; + font-size: var(--font-size-normal); + height: 57px; +} + +#homepage-repl-container p { + position: relative; /* Needed for the repl arrow's position: absolute */ +} + +#homepage-repl-container #repl-arrow { + cursor: default; + font-weight: bold; + font-size: 48px; + position: absolute; + top: -9px; + left: -79px; + text-shadow: 1px 1px 1px #444; + z-index: 3; + fill: var(--primary-1); +} + +.repl-err { + color: var(--magenta); +} + +/* Tables */ + +table { + border-collapse: collapse; + overflow-x: auto; + border: 2px solid #f0f0f0; + margin-bottom: 1rem; +} + +thead { + border: none; +} + +tbody { + border: none; +} + +tr { + border: none; + border-top: 2px solid #f0f0f0; +} + +th, +td { + border: none; + border-right: 2px solid #f0f0f0; + padding: 12px; +} + +th:last-child, +td:last-child { + border-right: none; +} + +p, +aside, +li { + font-size: var(--font-size-normal); + line-height: 1.85rem; +} + +/* Homepage */ +#homepage-intro-outer { + margin: 60px auto; + text-align: center; +} + +#homepage-intro-box { + position: relative; + display: inline-block; + text-align: left; +} + +#homepage-h1 { + color: #222; + text-shadow: none; + font-family: inherit; + font-size: 64px; + padding: 0; + padding-top: 60px; + position: relative; + left: -5px; +} + +#homepage-logo { + height: 176px; + width: auto; + position: absolute; + left: -200px; + top: -100px; +} + +#first-code-sample { + margin-top: 60px; + line-height: 1.85em; + color: #fcf9fd; +} + +#first-code-sample .kw, +#first-code-sample .punctuation, +.interactive-example .kw, +.interactive-example .punctuation { + color: #9c7cea; +} + +#first-code-sample, +#first-code-sample .code-snippet { + background-color: var(--dark-code-bg); +} + +#homepage-tagline { + font-size: 20px; +} + +.nowrap { + white-space: nowrap; +} + +/* Mobile-friendly screen width */ +@media only screen and (max-width: 1023px) { + :root { + --font-size-normal: 16px; + --body-max-width: none; + } + + #tutorial-main main, + #tutorial-toc-toggle-label, + #close-tutorial-toc { + display: block !important; + } + + #tutorial-toc { + display: none; + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + overflow-y: auto; + margin: 0 !important; + padding-right: 120px; + border: 0; + } + + #homepage-logo { + /* The bird runs off the screen unless we shrink it */ + height: 80px; + width: auto; + position: absolute; + left: 227px; + top: -28px; + } + + #homepage-main #nav-home-link { + display: none; + } + + #sponsor-logos { + padding: 4px; + } + + .home-examples-column { + padding-right: 0 !important; + border-right: none !important; + } + + /* Used for e.g. displaying the instruction "Click" on desktop and "Touch" on mobile. */ + .desktop { + display: none; + } + + .mobile { + display: inline; + } + + h2 { + margin-top: 48px; + padding: 12px 0; + } + + .home-link-text { + display: none; /* Don't show "Roc" in the header next to the logo, to save space */ + } + + h1 code, + h2 code, + h3 code, + h4 code, + h5 code { + font-size: inherit; + } + + code { + white-space: normal; + } + + /* Homepage */ + + #homepage-intro-box { + margin: 30px auto; + } + + #homepage-tagline { + margin-top: 0; + } + + #homepage-h1 { + /* TODO remove !important from repl.css (increasing specificity instead), and then this one too. */ + font-size: 48px !important; + padding: 0; + margin: 0; + text-align: left; + } + + #first-code-sample { + margin: 64px auto; + margin-bottom: 0; + } + + #homepage-tagline { + font-size: 16px; + } + + .home-goals-container, + .home-examples-container { + /* It's unclear why this !important is necessary, since its specificity + should theoretically be higher than what it's overriding. In practice, + at least in Chrome, removing the !important breaks this. */ + display: grid !important; + grid-template-columns: 1fr; + } + + h1, + h2, + h3, + h4, + h5, + h6, + p, + code { + word-break: break-word !important; + } + + h1, + h2, + h3, + h4, + h5 { + line-height: 1.2em !important; + font-size: 2rem !important; + width: auto; + } + + #top-bar-links { + width: 100%; + display: grid; + grid-template-columns: 1fr 1fr 1fr; /* Three equal-width columns */ + grid-template-rows: auto auto; /* Two rows */ + } + + /* Left-align the first link in each row, right-align the last one, and center the middle one. */ + #top-bar-links > :nth-child(3n + 1) { + text-align: left; + } + + #top-bar-links > :nth-child(3n + 2) { + text-align: center; + } + + #top-bar-links > :nth-child(3n + 3) { + text-align: right; + } + + #top-bar-links a, + #top-bar-links label { + font-size: 1.2rem; + padding: 12px 0.5rem; + margin: 0; + } + + #homepage-repl-container #repl { + max-width: none; + } +} + +/* iPhone SE and similar */ +@media only screen and (max-width: 320px) { + #homepage-logo { + /* The bird runs off the screen unless we shrink it */ + left: 188px; + top: -30px; + } + + :root { + --font-size-normal: 14px; + --body-max-width: 320px; + } +} + +@font-face { + font-family: "Permanent Marker"; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url("/fonts/permanent-marker-v16-latin/permanent-marker-v16-latin-regular.woff2") + format("woff2"), + url("/fonts/permanent-marker-v16-latin/permanent-marker-v16-latin-regular.woff") + format("woff"); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, + U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, + U+2215, U+FEFF, U+FFFD; +} + +/* latin-ext */ +@font-face { + font-family: "Lato"; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url("/fonts/lato-v23-latin-ext_latin/lato-v23-latin-ext_latin-regular.woff2") + format("woff2"), + url("/fonts/lato-v23-latin-ext_latin/lato-v23-latin-ext_latin-regular.woff") + format("woff"); + unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, + U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; +} + +/* latin */ +@font-face { + font-family: "Lato"; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url("/fonts/lato-v23-latin/lato-v23-latin-regular.woff2") + format("woff2"), + url("/fonts/lato-v23-latin/lato-v23-latin-regular.woff") format("woff"); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, + U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, + U+2215, U+FEFF, U+FFFD; +} + +/* latin-ext */ +@font-face { + font-family: "Source Code Pro"; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url("/fonts/source-code-pro-v22-latin-ext_latin/source-code-pro-v22-latin-ext_latin-regular.woff2") + format("woff2"), + url("/fonts/source-code-pro-v22-latin-ext_latin/source-code-pro-v22-latin-ext_latin-regular.woff") + format("woff"); + unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, + U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; +} + +/* latin */ +@font-face { + font-family: "Source Code Pro"; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url("/fonts/source-code-pro-v22-latin/source-code-pro-v22-latin-regular.woff2") + format("woff2"), + url("/fonts/source-code-pro-v22-latin/source-code-pro-v22-latin-regular.woff") + format("woff"); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, + U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, + U+2215, U+FEFF, U+FFFD; +} + +@media (prefers-color-scheme: dark) { + :root { + /* WCAG AAA Compliant colors */ + --code-bg: #202746; + --gray-bg: #202746; + --gray: #b6b6b6; + --orange: #fd6e08; + --green: #8ecc88; + --cyan: #12c9be; + --blue: #b1afdf; + --violet-bg: #332944; + --magenta: #f39bac; + + --primary-1: #9c7cea; + --primary-2: #1bd6bd; + --text-color: #ccc; + --body-bg-color: #151517; + --border-color: var(--gray); + --code-color: #eeeeee; + --logo-solid: #8f8f8f; + --faded-color: #bbbbbb; + --gray: #6e6e6e; + --heading-color: #eee; + + /* Tutorial */ + --header-link-color: #9c7cea; + --header-link-hover: #ddd; + --h1-color: #1bc6bd; + } + + .logo-dark { + fill: #6c3bdc; + } + + .logo-light { + fill: #8a66de; + } + + .svg-text { + fill: #fff; + } + + #homepage-h1 { + color: #fcf9fd; + } + + h3 { + color: #fff; + } + + h1, + h2, + h3, + h4, + h5 { + text-shadow: none; + } + + html { + scrollbar-color: #444444 #2f2f2f; + } + + table, + tr, + th, + td { + border-color: var(--gray); + } + + #first-code-sample, + #homepage-repl-container #repl { + border: 1px solid #ddd; + } + + .home-goals-content:hover { + background-color: #481870 !important; + } +} + +/* Comments `#` and Documentation comments `##` */ +samp .comment, +code .comment { + color: #ccc; +} + +/* Number, String, Tag literals */ +samp .storage.type, +code .storage.type, +samp .string, +code .string, +samp .string.begin, +code .string.begin, +samp .string.end, +code .string.end, +samp .constant, +code .constant, +samp .literal, +code .literal { + color: var(--dark-cyan); +} + +/* Keywords and punctuation */ +samp .keyword, +code .keyword, +samp .punctuation.section, +code .punctuation.section, +samp .punctuation.separator, +code .punctuation.separator, +samp .punctuation.terminator, +code .punctuation.terminator, +samp .kw, +code .kw { + color: var(--primary-1); +} + +/* Operators */ +samp .op, +code .op, +samp .keyword.operator, +code .keyword.operator, +samp .colon, +code .colon { + color: var(--primary-1); +} + +/* Delimieters */ +samp .delimeter, +code .delimeter { + color: var(--primary-1); +} + +/* Variables modules and field names */ +samp .function, +code .function, +samp .meta.group, +code .meta.group, +samp .meta.block, +code .meta.block, +samp .lowerident, +code .lowerident { + color: white; +} + +samp .error, +code .error { + color: hsl(0, 96%, 67%); +} + +/* Types, Tags, and Modules */ +samp .type, +code .type, +samp .meta.path, +code .meta.path, +samp .upperident, +code .upperident { + color: var(--dark-cyan); +} + +samp .dim, +code .dim { + opacity: 0.55; +} + +.button-container { + position: absolute; + top: 0; + right: 0; +} + +.copy-button { + background: var(--dark-code-bg); + border: 1px solid var(--dark-cyan); + color: var(--dark-cyan); + display: inline-block; + cursor: pointer; + margin: 8px; +} + +.copy-button:hover { + border-color: var(--code-color); + color: var(--code-color); +} + +.roc-logo { + width: 40px; + height: 40px; + margin: 0 auto; + fill: var(--primary-1); + text-shadow: 1px 1px 1px #010101; + position: relative; + top: -2px; +} + +/* HOME GOALS */ + +.home-goals-container { + display: flex; + flex-direction: row; + justify-content: space-between; + gap: 45px; + width: 100%; +} + +.home-goals-column { + display: flex; + flex-direction: column; + flex: 1; + width: 100%; +} + +.home-goals-content { + flex: 1; + display: flex; + flex-direction: column; + padding: 20px; + border: 4px solid var(--light-cyan); + color: inherit; + cursor: pointer; +} + +.home-goals-content:hover { + text-decoration: none; + cursor: pointer; + background-color: var(--light-cyan); +} + +.home-goals-learn-more { + text-decoration: underline; +} + +.home-examples-container { + display: flex; + flex-direction: row; + justify-content: space-between; + gap: 24px; + width: 100%; +} + +.home-examples-column { + display: flex; + flex-direction: column; + flex: 1; + width: 100%; +} + +.home-examples-column:not(:last-of-type) { + padding-right: 24px; + border-right: 2px solid var(--primary-1); +} + +/* Wider screens */ +@media only screen and (min-width: 768px) { + .home-goals-column { + margin-bottom: 0; + } + + .home-goals-column:last-child { + margin-right: 0; + } +} + +.home-goals-learn-more { + margin-top: auto; + white-space: nowrap; +} + +.home-goals-title { + padding: 0; + font-weight: bold; + margin-bottom: 10px; + font-size: 32px; + border-bottom: none; + font-family: inherit; + text-transform: lowercase; + padding-bottom: 42px; + padding-top: 2px; + font-style: italic; + letter-spacing: 1px; + word-spacing: 3px; + margin: 0; + color: var(--text-color); +} + +.home-goals-description { + line-height: 1.5; + margin-bottom: 2em; +} + +/* Interactive example on homepage */ + +.interactive-example { + font-size: var(--font-size-normal); +} + +.interactive-example, +.interactive-example samp { + background-color: #202746; + color: white; +} + +.interactive-example samp { + position: relative; + display: block; + width: 100%; + height: 580px; + padding-right: 300px; + cursor: default; +} + +.interactive-example label:hover, +.interactive-radio:checked + label { + background-color: #000; + cursor: pointer; +} + +.interactive-desc { + display: none; + position: absolute; + top: 0; + right: 300px; + width: 300px; + background-color: #ede6ff; + border: 1px solid black; + color: black; + padding: 0 16px; + padding-top: 12px; + margin-top: 9px; + cursor: text; + white-space: normal; + font-family: -apple-system, BlinkMacSystemFont, Roboto, Helvetica, Arial; +} + +.interactive-desc a { + color: #7c38f5; +} + +.interactive-radio { + display: none; +} + +.interactive-radio:checked + label + .interactive-desc { + display: block; +} + +.close-desc { + display: none; + position: absolute; + height: 40px; + width: 40px; + font-size: 24px; + top: -12px; + right: -12px; + color: #fff; + background: #9b6bf2; + border: 2px solid #7c38f5; + border-radius: 100%; + z-index: 4; +} + +.close-desc:hover { + color: #222; + background: var(--light-cyan); + border-color: var(--light-cyan); +} + +/* Tutorial */ + +#tutorial-main main { + display: flex; + flex-direction: row-reverse; + max-width: 1024px; +} + +#tutorial-main h1, +#tutorial-main h2, +#tutorial-main h3, +#tutorial-main h4, +#tutorial-main h5 { + font-family: "Permanent Marker"; + line-height: 1rem; + margin-top: 1.75rem; + margin-bottom: 0; + border: none; +} + +#tutorial-main h1 a, +#tutorial-main h2 a, +#tutorial-main h3 a, +#tutorial-main h4 a, +#tutorial-main h5 a { + color: var(--header-link-color); +} + +#tutorial-main h1 a:hover, +#tutorial-main h2 a:hover, +#tutorial-main h3 a:hover, +#tutorial-main h4 a:hover, +#tutorial-main h5 a:hover { + text-decoration: none; + color: var(--header-link-hover); +} + +#tutorial-main h1 { + font-size: 7rem; + line-height: 7rem; + color: var(--h1-color); + margin-top: 24px; + margin-bottom: 1.75rem; + text-shadow: 1px 1px 1px #010101; +} + +#tutorial-main h2 { + font-size: 4rem; + line-height: 4rem; + text-shadow: 1px 1px 1px #010101; + padding: 0.8rem 0; + margin-top: 2.5rem; + width: 60rem; /* Without this, "Building an application" wraps and looks awkward */ +} + +#tutorial-main h3 { + font-size: 3rem; + line-height: 3rem; + text-shadow: 1px 1px 1px #010101; + margin-bottom: 0.5rem; +} + +#tutorial-main h4 { + font-size: 2rem; + text-shadow: 1px 1px 1px #010101; +} + +#tutorial-body, +#tutorial-body pre { + max-width: 646px; +} + +#tutorial-toc { + position: relative; + background-color: var(--gray-bg); + flex: 0 0 auto; /* Take up as much space as it needs */ + margin-top: 30px; + background: var(--code-bg); + padding: 12px 24px; + margin-left: 64px; + align-self: flex-start; /* Aligns to the start, not stretching in height */ + z-index: 2; +} + +#tutorial-toc > ul { + display: flex; + flex-wrap: wrap; + list-style-type: none; + padding: 16px 16px; + margin: 0px; +} + +#tutorial-toc > ul > li { + flex: 1 1 50%; /* Adjust the percentage to control how many items per row */ + margin-bottom: 0; /* Reset the margin as they are now side by side */ + white-space: nowrap; + overflow: hidden; /* Ensures content doesn't overflow its container */ + text-overflow: ellipsis; /* Adds an ellipsis if the content overflows */ +} + +#tutorial-toc code { + background: none; + color: inherit; + margin: 0; + padding: 0; +} + +#tutorial-toc ol { + padding: 3px; + margin: 8px 0; + list-style: none; + padding-bottom: 0; + margin-bottom: 0; +} + +#tutorial-toc h2 { + font-family: inherit; + font-size: 2em; + text-shadow: none; + margin: 0; + padding: 16px 0; +} + +#toc-search { + background-color: var(--toc-search-bg); + border: 1px solid var(--toc-search-border); + color: inherit; + padding: 6px 8px; + margin-top: 16px; + margin-bottom: 4px; + box-sizing: border-box; + width: 100%; + font-size: inherit; +} + +#tutorial-toc-toggle, +#tutorial-toc-toggle-label, +#close-tutorial-toc { + display: none; + /* This may be overridden on mobile-friendly screen widths */ +} + +#tutorial-toc-toggle:hover, +#tutorial-toc-toggle-label:hover, +#close-tutorial-toc:hover { + text-decoration: underline; + cursor: pointer; +} + +#tutorial-toc-toggle, +#tutorial-toc-toggle-label { + font-size: 1.2rem; + float: right; + padding: 0 1rem; + font-family: Lato; + font-weight: normal; +} + +#close-tutorial-toc { + position: absolute; + top: 1rem; + right: 1rem; + font-size: 1.2rem; + padding: 12px 24px; + font-weight: bold; +} + +/* for larger screens */ +@media only screen and (min-width: 768px) { + #tutorial-toc > ul > li { + flex: 1 1 33%; /* Adjust the percentage to control how many items per row */ + } +} + +/* REPL */ + +#repl { + position: relative; + display: flex; + flex-direction: column; + font-size: 18px; +} + +#homepage-repl-container #repl-prompt, #homepage-repl-container .input-line-prefix { + top: 1.25rem; + color: var(--light-cyan); +} + +.input-line-prefix, #repl-prompt { + color: var(--cyan); + color: var(--primary-2); +} + +#repl-prompt { + position: relative; + left: 16px; + top: 0.95rem; + font-size: 1.25rem; + height: 0; + z-index: 2; + font-family: var(--font-mono); + /* Let clicks pass through to the textarea */ + pointer-events: none; + user-select: none; +} + +#homepage-repl-container #source-input { + color: var(--code-color); +} + +#source-input { + width: 100%; + font-family: var(--font-mono); + background-color: var(--code-bg); + display: inline-block; + height: 78px; + padding: 16px; + padding-left: 36px; + border: 1px solid transparent; + margin: 0; + margin-bottom: 2em; + box-sizing: border-box; + font-size: 18px; + resize: none; + color: inherit; +} + +#source-input:focus { + outline: 2px solid var(--primary-1); + box-sizing: border-box; +} + +.history { + padding: 1em; + padding-bottom: 0; + flex: 1; +} + +#help-text, +#history-text { + white-space: pre-wrap; +} + +#history-text { + margin-top: 16px; + min-height: 26px; +} + +#loading-message { + text-align: center; + /* approximately match height after loading and printing help message */ + height: 96px; +} + +.history-item { + margin-bottom: 24px; + overflow-x: hidden; +} + +.history-item .input { + margin: 0; + margin-bottom: 8px; +} + +.history-item .output { + margin: 0; +} + +.panic { + color: #ff6666; +} + +.color-red { + color: #ff6666; +} + +.color-green { + color: var(--green); +} + +.color-yellow { + color: var(--orange); +} + +.color-blue { + color: var(--cyan); +} + +.color-magenta { + color: var(--primary-1); +} + +.color-cyan { + color: var(--cyan); +} + +/* Really this isn't white so much as "default text color." For the repl, this should be black + in a light color scheme, and only white in dark mode. The name could be better! +*/ +#homepage-repl-container .color-white { + color: #FFF; +} + +#repl-container .color-white { + color: #000; +} + +@media (prefers-color-scheme: dark) { + + #homepage-repl-container .color-white { + color: #FFF; + } + + #repl-container .color-white { + color: #FFF; + } + +} + +.bold { + font-weight: bold; +} + +.underline { + text-decoration: underline; +} + +/* Mobile-friendly screen width */ +@media only screen and (max-width: 767px) { + #repl { + margin: 0; + padding: 0; + min-height: calc(100vh - var(--top-bar-height)); + } + + code.history { + flex-grow: 1; + } + + #source-input { + margin: 0; + } + + #loading-message { + margin: 0; + } + + #homepage-repl-container { + flex-direction: column; + } + + #homepage-repl-container #repl-description { + padding: 0; + margin-bottom: 1.5em; + } + + #repl-arrow { + display: none; + } +} + +#gh-logo { + fill: var(--text-color); +} + +#gh-link { + display: flex; + justify-content: center; + align-items: center; + margin: 0; +} + +#gh-centered-link { + text-decoration: none; + display: flex; + align-items: center; +} + +#gh-link-text { + margin-left: 8px; + vertical-align: middle; +} \ No newline at end of file diff --git a/www/public/site.js b/www/public/site.js new file mode 100644 index 0000000000..ebdd90065a --- /dev/null +++ b/www/public/site.js @@ -0,0 +1,516 @@ +const isOnMobile = window.innerWidth <= 1024; + +// The only way we can provide values to wasm_bindgen's generated code is to set globals +window.js_create_app = js_create_app; +window.js_run_app = js_run_app; +window.js_get_result_and_memory = js_get_result_and_memory; + +// The only place we use console.error is in wasm_bindgen, where it gets a single string argument. +console.error = function displayErrorInHistoryPanel(string) { + const html = `
${string}
`; + updateHistoryEntry(repl.inputHistoryIndex, false, html); +}; + +import * as roc_repl_wasm from "./repl/roc_repl_wasm.js"; + +const isHomepage = document.getElementById("homepage-repl-container") != null; + +const tutorialButtonSvg = ``; + +// ---------------------------------------------------------------------------- +// REPL state +// ---------------------------------------------------------------------------- + +const repl = { + elemHistory: document.getElementById("history-text"), + elemSourceInput: document.getElementById("source-input"), + description: document.getElementById("repl-description"), + + inputQueue: [], + inputHistory: [], + inputHistoryIndex: 0, + inputStash: "", // stash the user input while we're toggling through history with up/down arrows + + // Current progress through the repl tutorial + tutorialStep: 0, + tutorialSteps: [ + { + match: (input) => input.replace(/ /g, "") === "0.1+0.2", + show: '

Was this the answer you expected? (If so, try this in other programming languages and see what their answers are.)

Roc has a decimal type as well as floating-point for when performance is more important than decimal precision.

Next, enter name = "(put your name here)"

', + }, + { + match: (input) => input.replace(/ /g, "").match(/^name="/i), + show: '

This created a new definitionname is now defined to be equal to the string you entered.

Try using this definition by entering "Hi, \\(name)!"

', + }, + { + match: (input) => input.match(/^["][^\\]+\\\(name\)/i), + show: `

Nicely done! This is an example of string interpolation, which replaces part of a string with whatever you put inside the parentheses after a \\.

Now that you’ve written a few expressions, you can either continue exploring in this REPL, or move on to the tutorial to learn how to make full programs.

Welcome to Roc! ${tutorialButtonSvg} Start Tutorial

`, + }, + ], + + textDecoder: new TextDecoder(), + textEncoder: new TextEncoder(), + + compiler: null, + app: null, + + // Temporary storage for the address of the result of running the user's code. + // Used while control flow returns to Rust to allocate space to copy the app's memory buffer. + result_addr: 0, +}; + +// Initialise +repl.elemSourceInput.value = ""; // Some browsers remember the input across refreshes +resetSourceInputHeight(); +repl.elemSourceInput.addEventListener("input", resetSourceInputHeight); +repl.elemSourceInput.addEventListener("keydown", onInputKeydown); +repl.elemSourceInput.addEventListener("keyup", onInputKeyup); +roc_repl_wasm.default("/repl/roc_repl_wasm_bg.wasm").then(async (instance) => { + const loadingMessage = repl.elemHistory.querySelector("#loading-message"); + + if (loadingMessage != null) { + loadingMessage.remove(); + } + + repl.elemSourceInput.placeholder = "Enter some Roc code here."; + repl.compiler = instance; + + // Get help text from the compiler, and display it at top of the history panel + try { + const helpText = await roc_repl_wasm.entrypoint_from_js(":help"); + const helpElem = document.getElementById("help-text"); + + if (helpElem != null) { + helpElem.innerHTML = helpText.trim(); + } + } catch (e) { + // Print error for Roc devs. Don't use console.error, we overrode that above to display on the page! + console.warn(e); + } +}); + +// Focus the repl input the first time it scrolls into view +// (but not on mobile, because that would pop up the whole keyboard abruptly) +if (!isOnMobile) { + // Function to be called when the input enters the viewport + function handleIntersect(entries, observer) { + entries.forEach((entry) => { + // Check if the input is intersecting + if (entry.isIntersecting) { + // Apply focus to it, then unobserve it because we only want to do this once. + entry.target.focus(); + observer.unobserve(entry.target); + } + }); + } + + // Set up the Intersection Observer + let observer = new IntersectionObserver(handleIntersect, { + // Use the whole viewport for the intersection + root: null, + // Trigger the callback when the input is fully visible + threshold: 1.0, + }); + + observer.observe(repl.elemSourceInput); +} + +// ---------------------------------------------------------------------------- +// Handle inputs +// ---------------------------------------------------------------------------- + +function resetSourceInputHeight() { + repl.elemSourceInput.style.height = + repl.elemSourceInput.scrollHeight + 2 + "px"; // +2 for the border +} + +function onInputKeydown(event) { + const ENTER = 13; + + const { keyCode } = event; + + if (keyCode === ENTER) { + if (!event.shiftKey && !event.ctrlKey && !event.altKey) { + // Don't advance the caret to the next line + event.preventDefault(); + + const inputText = repl.elemSourceInput.value.trim(); + + repl.elemSourceInput.value = ""; + repl.elemSourceInput.style.height = ""; + + repl.inputQueue.push(inputText); + if (repl.inputQueue.length === 1) { + processInputQueue(); + } + + // Hide the arrow on the homepage that prompts you to enter something + const replArrow = document.getElementById("repl-arrow"); + + if (replArrow != null) { + replArrow.style.display = "none"; + } + } + } +} + +function onInputKeyup(event) { + const UP = 38; + const DOWN = 40; + + const { keyCode } = event; + + const el = repl.elemSourceInput; + + switch (keyCode) { + case UP: + if (repl.inputHistory.length === 0) { + return; + } + if (repl.inputHistoryIndex == repl.inputHistory.length - 1) { + repl.inputStash = el.value; + } + setInput(repl.inputHistory[repl.inputHistoryIndex]); + + if (repl.inputHistoryIndex > 0) { + repl.inputHistoryIndex--; + } + break; + + case DOWN: + if (repl.inputHistory.length === 0) { + return; + } + if (repl.inputHistoryIndex === repl.inputHistory.length - 1) { + setInput(repl.inputStash); + } else { + repl.inputHistoryIndex++; + setInput(repl.inputHistory[repl.inputHistoryIndex]); + } + break; + + default: + break; + } +} + +function setInput(value) { + const el = repl.elemSourceInput; + el.value = value; + el.selectionStart = value.length; + el.selectionEnd = value.length; +} + +function showNextReplTutorialEntry(inputText) { + const nextStep = repl.tutorialSteps[repl.tutorialStep]; + + if (repl.description != null && typeof nextStep === "object" && nextStep.match(inputText)) { + repl.description.innerHTML = + repl.description.innerHTML + "
" + nextStep.show; + + repl.tutorialStep = repl.tutorialStep + 1; + } +} + +// Use a queue just in case we somehow get inputs very fast +// We want the REPL to only process one at a time, since we're using some global state. +// In normal usage we shouldn't see this edge case anyway. Maybe with copy/paste? +async function processInputQueue() { + while (repl.inputQueue.length) { + const inputText = repl.inputQueue[0]; + + if (inputText) { + repl.inputHistoryIndex = createHistoryEntry(inputText); + repl.inputStash = ""; + + let outputText = ""; + let ok = true; + try { + outputText = await roc_repl_wasm.entrypoint_from_js(inputText); + } catch (e) { + outputText = `${e}`; + ok = false; + } + + updateHistoryEntry(repl.inputHistoryIndex, ok, outputText); + showNextReplTutorialEntry(inputText); + } + + repl.inputQueue.shift(); + } +} + +// ---------------------------------------------------------------------------- +// Callbacks to JS from Rust +// ---------------------------------------------------------------------------- + +var ROC_PANIC_INFO = null; + +function send_panic_msg_to_js(rocstr_ptr, panic_tag) { + const { memory } = repl.app.exports; + + const rocStrBytes = new Int8Array(memory.buffer, rocstr_ptr, 12); + const finalByte = rocStrBytes[11]; + + let stringBytes = ""; + if (finalByte < 0) { + // small string + + // bitwise ops on negative JS numbers are weird. This clears the bit that we + // use to indicate a small string. In rust it's `finalByte as u8 ^ 0b1000_0000` + const length = finalByte + 128; + stringBytes = new Uint8Array(memory.buffer, rocstr_ptr, length); + } else { + // big string + const rocStrWords = new Uint32Array(memory.buffer, rocstr_ptr, 3); + const [ptr, len, _cap] = rocStrWords; + + const SEAMLESS_SLICE_BIT = 1 << 31; + const length = len & ~SEAMLESS_SLICE_BIT; + + stringBytes = new Uint8Array(memory.buffer, ptr, length); + } + + const decodedString = repl.textDecoder.decode(stringBytes); + + ROC_PANIC_INFO = { + msg: decodedString, + panic_tag: panic_tag, + }; +} + +// Load Wasm code into the browser's virtual machine, so we can run it later. +// This operation is async, so we call it before entering any code shared +// with the command-line REPL, which is sync. +async function js_create_app(wasm_module_bytes) { + const { instance } = await WebAssembly.instantiate(wasm_module_bytes, { + env: { + send_panic_msg_to_js: send_panic_msg_to_js, + }, + }); + + // Keep the instance alive so we can run it later from shared REPL code + repl.app = instance; +} + +// Call the `main` function of the user app, via the `wrapper` function. +function js_run_app() { + const { wrapper, memory } = repl.app.exports; + + // Run the user code, and remember the result address + // We'll pass it to Rust in the next callback + try { + repl.result_addr = wrapper(); + } catch (e) { + // an exception could be that roc_panic was invoked, + // or some other crash (likely a compiler bug) + if (ROC_PANIC_INFO === null) { + throw e; + } else { + // when roc_panic set an error message, display it + const { msg, panic_tag } = ROC_PANIC_INFO; + ROC_PANIC_INFO = null; + + console.error(format_roc_panic_message(msg, panic_tag)); + } + } + + // Tell Rust how much space to reserve for its copy of the app's memory buffer. + // We couldn't know that size until we actually ran the app. + return memory.buffer.byteLength; +} + +function format_roc_panic_message(msg, panic_tag) { + switch (panic_tag) { + case 0: { + return `Roc failed with message: "${msg}"`; + } + case 1: { + return `User crash with message: "${msg}"`; + } + default: { + return `Got an invalid panic tag: "${panic_tag}"`; + } + } +} + +// After Rust has allocated space for the app's memory buffer, +// we copy it, and return the result address too +function js_get_result_and_memory(buffer_alloc_addr) { + const appMemory = new Uint8Array(repl.app.exports.memory.buffer); + const compilerMemory = new Uint8Array(repl.compiler.memory.buffer); + compilerMemory.set(appMemory, buffer_alloc_addr); + return repl.result_addr; +} + +// ---------------------------------------------------------------------------- +// Rendering +// ---------------------------------------------------------------------------- + +function createHistoryEntry(inputText) { + const historyIndex = repl.inputHistory.length; + repl.inputHistory.push(inputText); + + const firstLinePrefix = '» '; + const otherLinePrefix = '
'; + const inputLines = inputText.split("\n"); + if (inputLines[inputLines.length - 1] === "") { + inputLines.pop(); + } + const inputWithPrefixes = firstLinePrefix + inputLines.join(otherLinePrefix); + + const inputElem = document.createElement("div"); + inputElem.innerHTML = inputWithPrefixes; + inputElem.classList.add("input"); + + const historyItem = document.createElement("div"); + historyItem.appendChild(inputElem); + historyItem.classList.add("history-item"); + + repl.elemHistory.appendChild(historyItem); + + return historyIndex; +} + +function updateHistoryEntry(index, ok, outputText) { + const outputElem = document.createElement("div"); + outputElem.innerHTML = outputText; + outputElem.classList.add("output", ok ? "output-ok" : "output-error"); + + const historyItem = repl.elemHistory.children[index]; + historyItem.appendChild(outputElem); + + if (isHomepage) { + // Scroll the input element into view so you can see the most recent output. + // Only do this if it's currently out of view though! + const bounds = repl.elemSourceInput.getBoundingClientRect(); + const isInView = + bounds.top >= 0 && + bounds.left >= 0 && + bounds.bottom <= + (window.innerHeight || document.documentElement.clientHeight) && + bounds.right <= + (window.innerWidth || document.documentElement.clientWidth); + + if (!isInView) { + repl.elemSourceInput.scrollIntoView({ + behavior: "instant", + block: "end", + inline: "nearest", + }); + } + } else { + // Scroll the page to the bottom so you can see the most recent output. + window.scrollTo(0, document.body.scrollHeight); + } +} + +// TUTORIAL // + +const tutorialTocToggle = document.querySelector("#tutorial-toc-toggle"); + +if (tutorialTocToggle != null) { + document.querySelectorAll("#tutorial-toc li a").forEach((elem) => { + // Clicking any of the ToC links closes the ToC + elem.addEventListener("click", (event) => { + tutorialTocToggle.checked = false; + }); + }); + + document.addEventListener("keydown", (event) => { + // Escape closes the ToC + if (event.key == "Escape") { + tutorialTocToggle.checked = false; + } + }); + + const isTouchSupported = () => { + try { + document.createEvent("TouchEvent"); + return true; + } catch (e) { + return false; + } + }; + + // Select all elements that are children of
 elements
+  const codeBlocks = document.querySelectorAll("pre > samp");
+
+  // Iterate over each code block
+  codeBlocks.forEach((codeBlock) => {
+    // Create a "Copy" button
+    const copyButton = document.createElement("button");
+    copyButton.classList.add("copy-button");
+    copyButton.textContent = "Copy";
+
+    // Add event listener to copy button
+    copyButton.addEventListener("click", () => {
+      const codeText = codeBlock.innerText;
+      navigator.clipboard.writeText(codeText);
+      copyButton.textContent = "Copied!";
+      copyButton.classList.add("copy-button-copied");
+      copyButton.addEventListener("mouseleave", () => {
+        copyButton.textContent = "Copy";
+        copyButton.classList.remove("copy-button-copied");
+      });
+    });
+
+    // Create a container for the copy button and append it to the document
+    const buttonContainer = document.createElement("div");
+    buttonContainer.classList.add("button-container");
+    buttonContainer.appendChild(copyButton);
+    codeBlock.parentNode.insertBefore(buttonContainer, codeBlock);
+
+    // Hide the button container by default
+    buttonContainer.style.display = "none";
+
+    if (isTouchSupported()) {
+      // Show the button container on click for touch support (e.g. mobile)
+      document.addEventListener("click", (event) => {
+        if (event.target.closest("pre > samp") !== codeBlock) {
+          buttonContainer.style.display = "none";
+        } else {
+          buttonContainer.style.display = "block";
+        }
+      });
+    } else {
+      // Show the button container on hover for non-touch support (e.g. desktop)
+      codeBlock.parentNode.addEventListener("mouseenter", () => {
+        buttonContainer.style.display = "block";
+      });
+
+      codeBlock.parentNode.addEventListener("mouseleave", () => {
+        buttonContainer.style.display = "none";
+      });
+    }
+  });
+}
+
+// HOMEPAGE //
+
+if (isOnMobile) {
+  const hideDesc = () => {
+    document.querySelectorAll(".interactive-radio").forEach((radio) => {
+      radio.checked = false;
+    });
+  };
+
+  hideDesc(); // On mobile, start out with all the descriptions hidden.
+
+  document.querySelectorAll(".interactive-example").forEach((example) => {
+    example.querySelectorAll("label").forEach((label) => {
+      label.addEventListener("click", (event) => {
+        const desc = label.nextSibling; // The description node always comes next
+
+        // Set the position of the target element
+        desc.style.top = label.offsetTop + label.offsetHeight + "px"; // Position below the button
+        desc.style.left = label.offsetLeft + "px"; // Align with the left of the button
+      });
+    });
+
+    example.querySelectorAll(".close-desc").forEach((button) => {
+      button.style.display = "block";
+      button.addEventListener("click", hideDesc);
+    });
+  });
+}
diff --git a/www/public/styles.css b/www/public/styles.css
deleted file mode 100644
index 3d1a47ec35..0000000000
--- a/www/public/styles.css
+++ /dev/null
@@ -1,479 +0,0 @@
-:root {
-  --link-color: #612bde;
-  --code-link-color: #5721d4;
-  --text-color: #333333;
-  --code-color: #222222;
-  --code-bg-color: #eeeeee;
-  --body-bg-color: #fdfdfd;
-  --border-color: #e9e9e9;
-  --faded-color: #4c4c4c;
-  --monospace-font;
-  --font-sans: -apple-system, BlinkMacSystemFont, Roboto, Helvetica, Arial, sans-serif;
-  --font-mono: SFMono-Regular, Consolas, "Liberation Mono", Menlo, Courier, monospace;
-  --top-header-height: 67px;
-  --sidebar-width: 280px;
-  --top-bar-bg: #8257e5;
-  --top-bar-fg: #ffffff;
-  --nav-link-hover-color: #000000;
-}
-
-a {
-  color: #972395;
-}
-
-.logo {
-  padding: 2px 8px;
-}
-
-.logo svg {
-  height: 48px;
-  width: 48px;
-  fill: var(--top-bar-fg);
-}
-
-.logo:hover {
-  text-decoration: none;
-}
-
-.logo svg:hover {
-  fill: var(--nav-link-hover-color);
-}
-
-.pkg-full-name {
-  color: var(--text-color);
-  display: flex;
-  align-items: center;
-  font-size: 32px;
-  margin: 0 8px;
-  font-weight: normal;
-  white-space: nowrap;
-  overflow: hidden;
-  text-overflow: ellipsis;
-  height: 100%;
-}
-
-.pkg-full-name a {
-  padding-top: 12px;
-  padding-bottom: 16px;
-}
-
-a {
-  text-decoration: none;
-}
-
-a:hover {
-  text-decoration: underline;
-}
-
-.pkg-and-logo {
-  min-width: 0; /* necessary for text-overflow: ellipsis to work in descendants */
-  display: flex;
-  align-items: center;
-  height: 100%;
-  background-color: var(--top-bar-bg);
-}
-
-.pkg-and-logo a, .pkg-and-logo a:visited {
-  color: var(--top-bar-fg);
-}
-
-.pkg-and-logo a:hover {
-  color: var(--nav-link-hover-color);
-  text-decoration: none;
-}
-
-.main-container {
-  min-width: 0; /* necessary for text-overflow: ellipsis to work in descendants */
-}
-
-.search-button {
-  flex-shrink: 0; /* always shrink the package name before these; they have a relatively constrained length */
-  padding: 12px 18px;
-  margin-right: 42px;
-  display: none; /* only show this in the mobile view */
-}
-
-.version {
-  padding: 18px 10px;
-  min-width: 48px;
-  margin-right: 8px;
-}
-
-body {
-  display: grid;
-  grid-template-columns: [before-sidebar] 1fr [sidebar] var(--sidebar-width) [main-content] fit-content(calc(1280px - var(--sidebar-width))) [end] 1fr;
-  grid-template-rows: [top-header] var(--top-header-height) [above-footer] auto [footer] auto;
-  box-sizing: border-box;
-  margin: 0;
-  padding: 0;
-  font-family: var(--font-sans);
-  color: var(--text-color);
-  background-color: var(--body-bg-color);
-}
-
-main {
-  grid-column-start: main-content;
-  grid-column-end: main-content;
-  grid-row-start: above-footer;
-  grid-row-end: above-footer;
-  box-sizing: border-box;
-  position: relative;
-  font-size: 18px;
-  line-height: 1.85em;
-  margin-top: 2px;
-  padding: 48px;
-}
-
-#sidebar-nav {
-  grid-column-start: sidebar;
-  grid-column-end: sidebar;
-  grid-row-start: above-footer;
-  grid-row-end: above-footer;
-  position: relative;
-  display: flex;
-  flex-direction: column;
-  box-sizing: border-box;
-  padding-left: 56px;
-  padding-top: 6px;
-  width: 100%;
-}
-
-.top-header-extension {
-  grid-column-start: before-sidebar;
-  grid-column-end: sidebar;
-  grid-row-start: top-header;
-  grid-row-end: top-header;
-  background-color: var(--top-bar-bg);
-}
-
-.top-header {
-  grid-column-start: sidebar;
-  grid-column-end: end;
-  grid-row-start: top-header;
-  grid-row-end: top-header;
-  display: flex;
-  flex-direction: row;
-  align-items: center;
-  flex-wrap: nowrap;
-  flex-grow: 1;
-  box-sizing: border-box;
-  font-family: var(--font-sans);
-  font-size: 24px;
-  height: 100%;
-  min-width: 0; /* necessary for text-overflow: ellipsis to work in descendants */
-}
-
-.top-header-triangle {
-  /* This used to be a clip-path, but Firefox on Android (at least as of early 2020)
-   * rendered the page extremely slowly in that version. With this approach it's super fast.
-   */
-  width: 0;
-  height: 0;
-  border-style: solid;
-  border-width: var(--top-header-height) 0 0 48px;
-  border-color: transparent transparent transparent var(--top-bar-bg);
-}
-
-p {
-    overflow-wrap: break-word;
-    margin: 24px 0;
-}
-
-footer {
-  grid-column-start: main-content;
-  grid-column-end: main-content;
-  grid-row-start: footer;
-  grid-row-end: footer;
-  max-width: var(--main-content-max-width);
-  font-size: 14px;
-  box-sizing: border-box;
-  padding: 16px;
-}
-
-footer p {
-  display: inline-block;
-  margin-top: 0;
-  margin-bottom: 8px;
-}
-
-.content {
-  box-sizing: border-box;
-  display: flex;
-  flex-direction: row;
-  justify-content: space-between;
-}
-
-.sidebar-entry ul {
-  list-style-type: none;
-  margin: 0;
-}
-
-.sidebar-entry a {
-  box-sizing: border-box;
-  min-height: 48px;
-  min-width: 48px;
-  padding: 12px 16px;
-  font-family: var(--font-mono);
-}
-
-.sidebar-sub-entries a {
-  display: block;
-  line-height: 24px;
-  width: 100%;
-  overflow: hidden;
-  text-overflow: ellipsis;
-  padding-left: 36px;
-}
-
-.module-name {
-  font-size: 56px;
-  line-height: 1em;
-  font-family: var(--font-mono);
-  font-weight: bold;
-  margin-top: 18px;
-  margin-bottom: 48px;
-}
-
-.module-name a, .module-name a:visited {
-  color: inherit;
-}
-
-.sidebar-module-link {
-  box-sizing: border-box;
-  font-size: 18px;
-  line-height: 24px;
-  font-family: var(--font-mono);
-  font-weight: bold;
-  display: block;
-  width: 100%;
-  padding: 8px 0;
-  white-space: nowrap;
-  overflow: hidden;
-  text-overflow: ellipsis;
-}
-
-a, a:visited {
-  color: var(--link-color);
-}
-
-h3 {
-  font-size: 32px;
-  margin: 48px 0 24px 0;
-}
-
-h4 {
-  font-size: 24px;
-}
-
-.type-def {
-  font-size: 24px;
-  color: var(--link-color);
-}
-
-.code-snippet {
-  padding: 12px 16px;
-  display: block;
-  box-sizing: border-box;
-  font-family: var(--font-mono);
-  background-color: var(--code-bg-color);
-}
-
-code {
-  font-family: var(--font-mono);
-  color: var(--code-color);
-  background-color: var(--code-bg-color);
-  display: inline-block;
-  line-height: 28px;
-}
-
-code a {
-  color: var(--code-link-color);
-}
-
-code a:visited {
-  color: var(--code-link-color);
-}
-
-pre {
-  margin: 36px 0;
-  padding: 8px;
-  box-sizing: border-box;
-  background-color: var(--code-bg-color);
-  overflow-x: auto;
-}
-
-.hidden {
-  /* Use !important to win all specificity fights. */
-  display: none !important;
-}
-
-.syntax-comment {
-  color: #ff0000;
-}
-
-#module-search:placeholder-shown {
-  padding: 0;
-  opacity: 0;
-  height: 0;
-}
-
-#module-search, #module-search:focus {
-  opacity: 1;
-  padding: 12px 16px;
-  height: 48px;
-}
-
-/* Show the "Search" label link when the text input has a placeholder */
-#module-search:placeholder-shown + #search-link {
-  display: flex;
-}
-
-/* Hide the "Search" label link when the text input has focus */
-#module-search:focus + #search-link {
-  display: none;
-}
-
-#module-search {
-  display: block;
-  box-sizing: border-box;
-  background-color: var(--code-bg-color);
-  width: 100%;
-  box-sizing: border-box;
-  font-size: 18px;
-  line-height: 18px;
-  margin-top: 6px;
-  border: none;
-  color: var(--faded-color);
-  background-color: var(--code-bg-color);
-  font-family: var(--font-serif);
-}
-
-#module-search::placeholder {
-  color: var(--faded-color);
-  opacity: 1;
-}
-
-#search-link {
-  box-sizing: border-box;
-  display: none;
-  align-items: center;
-  font-size: 18px;
-  line-height: 18px;
-  padding: 12px 16px;
-  height: 48px;
-  cursor: pointer;
-  color: var(--link-color);
-}
-
-#search-link:hover {
-  text-decoration: underline;
-}
-
-@media (prefers-color-scheme: dark) {
-  :root {
-    --body-bg-color: #303030;
-    --code-bg-color: #393939;
-    --border-color: #555555;
-    --code-color: #eeeeee;
-    --text-color: #cccccc;
-    --logo-solid: #777777;
-    --faded-color: #bbbbbb;
-    --link-color: #c5a8ff;
-    --code-link-color: #b894ff;
-    --top-bar-bg: #6845b9;
-    --top-bar-fg: #eeeeee;
-  }
-
-  html {
-    scrollbar-color: #444444 #2f2f2f;
-  }
-}
-
-@media only screen and (max-device-width: 480px) {
-  .search-button {
-    display: block; /* This is only visible in mobile. */
-  }
-
-  .top-header {
-    width: auto;
-  }
-
-  .pkg-full-name {
-    margin-left: 8px;
-    margin-right: 12px;
-    font-size: 24px;
-    padding-bottom: 14px;
-  }
-
-  .pkg-full-name a {
-    vertical-align: middle;
-    padding: 18px 0;
-  }
-
-  .logo {
-    padding-left: 2px;
-    width: 50px;
-    height: 54px;
-  }
-
-  .version {
-    margin: 0;
-    font-weight: normal;
-    font-size: 18px;
-    padding-bottom: 16px;
-  }
-
-  .module-name {
-    font-size: 36px;
-    margin-top: 8px;
-    margin-bottom: 8px;
-    max-width: calc(100% - 18px);
-    overflow: hidden;
-    text-overflow: ellipsis;
-  }
-
-  main {
-    padding: 18px;
-    font-size: 16px;
-  }
-
-  .container {
-    margin: 0;
-    min-width: 320px;
-    max-width: 100%;
-  }
-
-  .content {
-    flex-direction: column;
-  }
-
-  .sidebar {
-    margin-top: 0;
-    padding-left: 0;
-    width: auto;
-  }
-
-  #sidebar-heading {
-    font-size: 24px;
-    margin: 16px;
-  }
-
-  h3 {
-    font-size: 18px;
-    margin: 0;
-    padding: 0;
-  }
-
-  h4 {
-    font-size: 16px;
-  }
-
-  .top-header {
-    justify-content: space-between;
-  }
-
-  .content {
-    /* Display the sidebar below 
without affecting tab index */ - flex-direction: column-reverse; - } -} diff --git a/www/public/zulip-icon-circle.svg b/www/public/zulip-icon-circle.svg new file mode 100644 index 0000000000..72466de7fb --- /dev/null +++ b/www/public/zulip-icon-circle.svg @@ -0,0 +1 @@ + \ No newline at end of file
+ +
+ +
+